Add Rz Coin utility store

This commit is contained in:
hz
2026-07-29 18:34:54 -05:00
parent c14ac1d65b
commit da641a8d1d
21 changed files with 656 additions and 21 deletions
+9
View File
@@ -1,5 +1,6 @@
extends PanelContainer
signal store_requested
const Brand := preload("res://scripts/ui/medmind_style.gd")
const SHIELD_ICON := preload("res://assets/icons/lucide/shield.svg")
const STAR_ICON := preload("res://assets/icons/lucide/star.svg")
@@ -21,6 +22,9 @@ var _initialized := false
func _ready() -> void:
size_flags_vertical = Control.SIZE_SHRINK_CENTER
mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
tooltip_text = "Open Rz Coin Store"
gui_input.connect(_on_gui_input)
_build_ui()
refresh_theme_controls()
_on_credits_changed()
@@ -30,6 +34,11 @@ func _ready() -> void:
resized.connect(func(): pivot_offset = size * 0.5)
func _on_gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
store_requested.emit()
func _build_ui() -> void:
var margin := MarginContainer.new()
margin.add_theme_constant_override("margin_left", 7)
+8
View File
@@ -207,6 +207,7 @@ func _build_ui() -> void:
header.add_child(account_chevron_button)
credits_badge = CreditsBadge.new()
credits_badge.store_requested.connect(func(): show_screen("rz_store"))
header.add_child(credits_badge)
var screens_container := MarginContainer.new()
@@ -355,6 +356,11 @@ func _build_ui() -> void:
donations.main = self
screens_container.add_child(donations)
screens["donations"] = donations
var rz_store := preload("res://scripts/ui/rz_store_screen.gd").new()
rz_store.name = "RzCoinStore"
rz_store.main = self
screens_container.add_child(rz_store)
screens["rz_store"] = rz_store
feedback_dialog = preload("res://scripts/ui/app_feedback_dialog.gd").new()
feedback_dialog.name = "AppFeedbackDialog"
@@ -426,6 +432,7 @@ func _populate_navigation(container: VBoxContainer, include_brand: bool) -> void
_add_nav_button(container, "flashcards", "Flashcards", "clipboard-plus", true, Brand.is_uat_environment())
_add_nav_button(container, "bookmarks", "Bookmarks", "bookmark", true)
_add_nav_button(container, "notes", "Notes", "notebook-pen", true)
_add_nav_button(container, "rz_store", "Rz Coin Store", "star", true)
var spacer := Control.new()
spacer.size_flags_vertical = Control.SIZE_EXPAND_FILL
@@ -476,6 +483,7 @@ func _on_navigation_pressed(key: String) -> void:
"notes": show_screen("notes")
"bookmarks": show_screen("bookmarks")
"donations": show_screen("donations")
"rz_store": show_screen("rz_store")
"logout": _logout()
+2
View File
@@ -29,4 +29,6 @@ static func key_for_screen(screen_name: String, payload: Variant = null) -> Stri
key = "bookmarks"
elif screen_name == "donations":
key = "donations"
elif screen_name == "rz_store":
key = "rz_store"
return key
+121
View File
@@ -0,0 +1,121 @@
extends VBoxContainer
const Brand := preload("res://scripts/ui/medmind_style.gd")
var main: Control
var balance_label: Label
var inventory_label: Label
var products_box: VBoxContainer
var status_label: Label
var confirm_dialog: ConfirmationDialog
var pending_sku := ""
func _ready() -> void:
size_flags_horizontal = Control.SIZE_EXPAND_FILL
size_flags_vertical = Control.SIZE_EXPAND_FILL
add_theme_constant_override("separation", 18)
var title := Label.new()
title.text = "Rz Coin Store"
title.add_theme_font_override("font", Brand.font)
title.add_theme_font_size_override("font_size", 30)
add_child(title)
var intro := Label.new()
intro.text = "Spend coins earned from correct, on-time first answers. Purchases use Rz Coins only."
intro.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
add_child(intro)
balance_label = Label.new()
balance_label.add_theme_font_size_override("font_size", 20)
add_child(balance_label)
inventory_label = Label.new()
add_child(inventory_label)
products_box = VBoxContainer.new()
products_box.add_theme_constant_override("separation", 12)
add_child(products_box)
status_label = Label.new()
status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
add_child(status_label)
confirm_dialog = ConfirmationDialog.new()
confirm_dialog.confirmed.connect(_confirm_purchase)
add_child(confirm_dialog)
CreditsService.credits_changed.connect(_refresh)
CreditsService.credits_failed.connect(_show_error)
CreditsService.store_operation_completed.connect(_show_message)
_refresh()
func on_show(_payload: Variant = null) -> void:
CreditsService.refresh()
_refresh()
func _refresh() -> void:
balance_label.text = "%d Rz Coins" % CreditsService.credits
inventory_label.text = "Inventory: %d exam token(s) · %d QBank reset token(s)" % [
CreditsService.exam_tokens, CreditsService.qbank_reset_tokens]
for child in products_box.get_children():
child.queue_free()
for product_variant in CreditsService.products:
if typeof(product_variant) != TYPE_DICTIONARY:
continue
var product: Dictionary = product_variant
var card := PanelContainer.new()
card.add_theme_stylebox_override("panel", Brand.content_card_box(UIThemeService.is_dark_mode(), true, Brand.ROYAL_BLUE))
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 16)
card.add_child(row)
var copy := VBoxContainer.new()
copy.size_flags_horizontal = Control.SIZE_EXPAND_FILL
var name_label := Label.new()
name_label.text = str(product.get("name", "Store item"))
name_label.add_theme_font_size_override("font_size", 19)
copy.add_child(name_label)
var description := Label.new()
description.text = str(product.get("description", ""))
description.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
copy.add_child(description)
row.add_child(copy)
var buy := Button.new()
var price := int(product.get("coinPrice", 0))
buy.text = "Buy · %d coins" % price
buy.disabled = CreditsService.credits < price
buy.pressed.connect(_ask_purchase.bind(str(product.get("sku", "")), str(product.get("name", "item")), price))
row.add_child(buy)
products_box.add_child(card)
if CreditsService.qbank_reset_tokens > 0:
var reset := Button.new()
reset.text = "Use QBank Reset Token"
reset.tooltip_text = "Question eligibility resets; ELO, history, bookmarks, notes, reviews, and earning limits are preserved."
reset.pressed.connect(_ask_reset)
products_box.add_child(reset)
func _ask_purchase(sku: String, product_name: String, price: int) -> void:
pending_sku = sku
confirm_dialog.title = "Confirm purchase"
confirm_dialog.dialog_text = "Buy %s for %d Rz Coins?" % [product_name, price]
confirm_dialog.popup_centered()
func _confirm_purchase() -> void:
if pending_sku == "__reset__":
CreditsService.reset_qbank()
elif not pending_sku.is_empty():
CreditsService.purchase(pending_sku)
pending_sku = ""
func _ask_reset() -> void:
pending_sku = "__reset__"
confirm_dialog.title = "Reset QBank eligibility"
confirm_dialog.dialog_text = "Make all QBank questions eligible again? ELO, answer history, bookmarks, notes, reviews, and the rolling coin cap will be preserved."
confirm_dialog.popup_centered()
func _show_error(message: String) -> void:
status_label.text = message
func _show_message(message: String) -> void:
status_label.text = message
_refresh()
+1
View File
@@ -0,0 +1 @@
uid://cldpnvs2k6r3t
+7
View File
@@ -137,6 +137,8 @@ func _connect_services() -> void:
ExamSimulationService.state_changed.connect(_on_simulation_loaded)
if not ExamSimulationService.request_failed.is_connected(_on_request_failed):
ExamSimulationService.request_failed.connect(_on_request_failed)
if not ExamSimulationService.exam_token_required.is_connected(_on_exam_token_required):
ExamSimulationService.exam_token_required.connect(_on_exam_token_required)
func on_show(_payload: Variant = null) -> void:
@@ -538,6 +540,11 @@ func _on_request_failed(message: String) -> void:
_render_active_section()
func _on_exam_token_required() -> void:
pending_launch = false
main.show_screen("rz_store")
func _update_responsive_layout() -> void:
var use_compact := size.x < 940
if compact_layout == use_compact: