extends VBoxContainer const Brand := preload("res://scripts/ui/medmind_style.gd") const ICON_BY_SKU := { "exam_token_1": "clock-3", "qbank_reset_1": "rotate-ccw", } var main: Control var subtitle_label: Label var summary_card: PanelContainer var summary_icon_panel: PanelContainer var summary_icon: TextureRect var balance_value_label: Label var coin_caption_label: Label var inventory_label: Label var products_grid: HFlowContainer var status_label: Label var confirm_dialog: ConfirmationDialog var footer_card: PanelContainer var footer_icon_panel: PanelContainer var footer_icon: TextureRect var footer_heading: Label var footer_description: Label 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) _build_header() _build_summary_card() var section_title := Label.new() section_title.text = "Featured" section_title.add_theme_font_override("font", Brand.font) section_title.add_theme_font_size_override("font_size", 18) add_child(section_title) products_grid = HFlowContainer.new() products_grid.add_theme_constant_override("h_separation", 14) products_grid.add_theme_constant_override("v_separation", 14) add_child(products_grid) status_label = Label.new() status_label.add_theme_font_override("font", Brand.font) 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) _build_footer() CreditsService.credits_changed.connect(_refresh) CreditsService.credits_failed.connect(_show_error) CreditsService.store_operation_completed.connect(_show_message) UIThemeService.theme_changed.connect(func(_dark: bool): _refresh_theme_controls(); _refresh()) _refresh() func on_show(_payload: Variant = null) -> void: CreditsService.refresh() _refresh() func _build_header() -> void: var title := Label.new() title.text = "RZ Coin Shop" title.add_theme_font_override("font", Brand.font) title.add_theme_font_size_override("font_size", 30) add_child(title) subtitle_label = Label.new() subtitle_label.text = "Spend your RZ Coins on premium content and boosts." subtitle_label.add_theme_font_override("font", Brand.font) subtitle_label.add_theme_font_size_override("font_size", 14) subtitle_label.add_theme_color_override("font_color", Brand.muted_color(UIThemeService.is_dark_mode())) add_child(subtitle_label) func _build_summary_card() -> void: var dark := UIThemeService.is_dark_mode() summary_card = PanelContainer.new() summary_card.add_theme_stylebox_override("panel", Brand.content_card_box(dark, true, Brand.AMBER)) summary_card.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN add_child(summary_card) var margin := MarginContainer.new() margin.add_theme_constant_override("margin_left", 16) margin.add_theme_constant_override("margin_right", 16) margin.add_theme_constant_override("margin_top", 12) margin.add_theme_constant_override("margin_bottom", 12) summary_card.add_child(margin) var row := HBoxContainer.new() row.add_theme_constant_override("separation", 14) margin.add_child(row) summary_icon_panel = PanelContainer.new() summary_icon_panel.custom_minimum_size = Vector2(40, 40) summary_icon_panel.size_flags_vertical = Control.SIZE_SHRINK_CENTER summary_icon_panel.add_theme_stylebox_override("panel", Brand.icon_circle_box(dark, Brand.AMBER, 20)) summary_icon = TextureRect.new() summary_icon.texture = load("res://assets/icons/lucide/star.svg") summary_icon.custom_minimum_size = Vector2(22, 22) summary_icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE summary_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED summary_icon.modulate = Brand.AMBER summary_icon_panel.add_child(summary_icon) row.add_child(summary_icon_panel) var copy := VBoxContainer.new() copy.size_flags_vertical = Control.SIZE_SHRINK_CENTER copy.add_theme_constant_override("separation", 0) balance_value_label = Label.new() balance_value_label.add_theme_font_override("font", Brand.font) balance_value_label.add_theme_font_size_override("font_size", 22) balance_value_label.add_theme_color_override("font_color", Brand.text_color(dark)) copy.add_child(balance_value_label) coin_caption_label = Label.new() coin_caption_label.text = "RZ Coins" coin_caption_label.add_theme_font_override("font", Brand.font) coin_caption_label.add_theme_font_size_override("font_size", 12) coin_caption_label.add_theme_color_override("font_color", Brand.muted_color(dark)) copy.add_child(coin_caption_label) inventory_label = Label.new() inventory_label.add_theme_font_override("font", Brand.font) inventory_label.add_theme_font_size_override("font_size", 12) inventory_label.add_theme_color_override("font_color", Brand.muted_color(dark)) copy.add_child(inventory_label) row.add_child(copy) func _build_footer() -> void: var dark := UIThemeService.is_dark_mode() footer_card = PanelContainer.new() footer_card.add_theme_stylebox_override("panel", Brand.content_card_box(dark, false)) add_child(footer_card) var margin := MarginContainer.new() margin.add_theme_constant_override("margin_left", 16) margin.add_theme_constant_override("margin_right", 16) margin.add_theme_constant_override("margin_top", 12) margin.add_theme_constant_override("margin_bottom", 12) footer_card.add_child(margin) var row := HBoxContainer.new() row.add_theme_constant_override("separation", 14) margin.add_child(row) footer_icon_panel = PanelContainer.new() footer_icon_panel.custom_minimum_size = Vector2(36, 36) footer_icon_panel.size_flags_vertical = Control.SIZE_SHRINK_CENTER footer_icon_panel.add_theme_stylebox_override("panel", Brand.icon_circle_box(dark, Brand.ROYAL_BLUE, 18)) footer_icon = TextureRect.new() footer_icon.texture = load("res://assets/icons/lucide/activity.svg") footer_icon.custom_minimum_size = Vector2(18, 18) footer_icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE footer_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED footer_icon.modulate = Brand.ROYAL_BLUE footer_icon_panel.add_child(footer_icon) row.add_child(footer_icon_panel) var copy := VBoxContainer.new() copy.size_flags_horizontal = Control.SIZE_EXPAND_FILL copy.size_flags_vertical = Control.SIZE_SHRINK_CENTER copy.add_theme_constant_override("separation", 2) footer_heading = Label.new() footer_heading.text = "How to earn RZ Coins" footer_heading.add_theme_font_override("font", Brand.font) footer_heading.add_theme_font_size_override("font_size", 15) footer_heading.add_theme_color_override("font_color", Brand.text_color(dark)) copy.add_child(footer_heading) footer_description = Label.new() footer_description.text = "Answer questions correctly on the first try, within the time limit." footer_description.add_theme_font_override("font", Brand.font) footer_description.add_theme_font_size_override("font_size", 12) footer_description.add_theme_color_override("font_color", Brand.muted_color(dark)) footer_description.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART copy.add_child(footer_description) row.add_child(copy) func _refresh_theme_controls() -> void: var dark := UIThemeService.is_dark_mode() subtitle_label.add_theme_color_override("font_color", Brand.muted_color(dark)) summary_card.add_theme_stylebox_override("panel", Brand.content_card_box(dark, true, Brand.AMBER)) summary_icon_panel.add_theme_stylebox_override("panel", Brand.icon_circle_box(dark, Brand.AMBER, 20)) balance_value_label.add_theme_color_override("font_color", Brand.text_color(dark)) coin_caption_label.add_theme_color_override("font_color", Brand.muted_color(dark)) inventory_label.add_theme_color_override("font_color", Brand.muted_color(dark)) footer_card.add_theme_stylebox_override("panel", Brand.content_card_box(dark, false)) footer_icon_panel.add_theme_stylebox_override("panel", Brand.icon_circle_box(dark, Brand.ROYAL_BLUE, 18)) footer_heading.add_theme_color_override("font_color", Brand.text_color(dark)) footer_description.add_theme_color_override("font_color", Brand.muted_color(dark)) func _refresh() -> void: var dark := UIThemeService.is_dark_mode() balance_value_label.text = _format_thousands(CreditsService.credits) inventory_label.text = "%d exam token(s) ยท %d QBank reset token(s)" % [ CreditsService.exam_tokens, CreditsService.qbank_reset_tokens] for child in products_grid.get_children(): child.queue_free() for product_variant in CreditsService.products: if typeof(product_variant) != TYPE_DICTIONARY: continue var product: Dictionary = product_variant products_grid.add_child(_build_product_card(dark, product)) if CreditsService.qbank_reset_tokens > 0: products_grid.add_child(_build_reset_card(dark)) func _build_product_card(dark: bool, product: Dictionary) -> PanelContainer: var sku := str(product.get("sku", "")) var price := int(product.get("coinPrice", 0)) var card := _new_card(dark) var content: VBoxContainer = card.get_child(0).get_child(0) content.add_child(_card_icon(dark, ICON_BY_SKU.get(sku, "star"), Brand.ROYAL_BLUE)) var name_label := Label.new() name_label.text = str(product.get("name", "Store item")) name_label.add_theme_font_override("font", Brand.font) name_label.add_theme_font_size_override("font_size", 16) name_label.add_theme_color_override("font_color", Brand.text_color(dark)) content.add_child(name_label) var description := Label.new() description.text = str(product.get("description", "")) description.add_theme_font_override("font", Brand.font) description.add_theme_font_size_override("font_size", 12) description.add_theme_color_override("font_color", Brand.muted_color(dark)) description.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART description.custom_minimum_size = Vector2(190, 0) content.add_child(description) var footer_row := HBoxContainer.new() footer_row.add_theme_constant_override("separation", 8) content.add_child(footer_row) footer_row.add_child(_price_tag(dark, price)) var spacer := Control.new() spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL footer_row.add_child(spacer) var buy := Button.new() buy.text = "Buy" buy.custom_minimum_size = Vector2(64, 34) buy.add_theme_font_override("font", Brand.font) _style_buy_button(buy) buy.disabled = CreditsService.credits < price buy.pressed.connect(_ask_purchase.bind(sku, str(product.get("name", "item")), price)) footer_row.add_child(buy) return card func _build_reset_card(dark: bool) -> PanelContainer: var card := _new_card(dark) var content: VBoxContainer = card.get_child(0).get_child(0) content.add_child(_card_icon(dark, "rotate-ccw", Brand.SUCCESS)) var name_label := Label.new() name_label.text = "Use QBank Reset Token" name_label.add_theme_font_override("font", Brand.font) name_label.add_theme_font_size_override("font_size", 16) name_label.add_theme_color_override("font_color", Brand.text_color(dark)) content.add_child(name_label) var description := Label.new() description.text = "Question eligibility resets; ELO, history, bookmarks, notes, reviews, and earning limits are preserved." description.add_theme_font_override("font", Brand.font) description.add_theme_font_size_override("font_size", 12) description.add_theme_color_override("font_color", Brand.muted_color(dark)) description.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART description.custom_minimum_size = Vector2(190, 0) content.add_child(description) var footer_row := HBoxContainer.new() content.add_child(footer_row) var spacer := Control.new() spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL footer_row.add_child(spacer) var use_button := Button.new() use_button.text = "Use token" use_button.custom_minimum_size = Vector2(90, 34) use_button.add_theme_font_override("font", Brand.font) _style_buy_button(use_button) use_button.pressed.connect(_ask_reset) footer_row.add_child(use_button) return card func _new_card(dark: bool) -> PanelContainer: var card := PanelContainer.new() card.custom_minimum_size = Vector2(230, 0) card.add_theme_stylebox_override("panel", Brand.content_card_box(dark, true, Brand.ROYAL_BLUE)) var margin := MarginContainer.new() margin.add_theme_constant_override("margin_left", 16) margin.add_theme_constant_override("margin_right", 16) margin.add_theme_constant_override("margin_top", 14) margin.add_theme_constant_override("margin_bottom", 14) card.add_child(margin) var content := VBoxContainer.new() content.add_theme_constant_override("separation", 8) margin.add_child(content) return card func _card_icon(dark: bool, icon_name: String, accent: Color) -> PanelContainer: var icon_panel := PanelContainer.new() icon_panel.custom_minimum_size = Vector2(48, 48) icon_panel.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN icon_panel.add_theme_stylebox_override("panel", Brand.icon_circle_box(dark, accent, 24)) var icon := TextureRect.new() icon.texture = load("res://assets/icons/lucide/%s.svg" % icon_name) icon.custom_minimum_size = Vector2(24, 24) icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED icon.modulate = accent icon_panel.add_child(icon) return icon_panel func _price_tag(dark: bool, price: int) -> HBoxContainer: var row := HBoxContainer.new() row.add_theme_constant_override("separation", 4) var icon := TextureRect.new() icon.texture = load("res://assets/icons/lucide/star.svg") icon.custom_minimum_size = Vector2(14, 14) icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED icon.modulate = Brand.AMBER row.add_child(icon) var label := Label.new() label.text = _format_thousands(price) label.add_theme_font_override("font", Brand.font) label.add_theme_font_size_override("font_size", 14) label.add_theme_color_override("font_color", Brand.text_color(dark)) row.add_child(label) return row func _style_buy_button(button: Button) -> void: var normal := StyleBoxFlat.new() normal.bg_color = Brand.ROYAL_BLUE normal.set_corner_radius_all(8) var hover := StyleBoxFlat.new() hover.bg_color = Brand.SKY_BLUE hover.set_corner_radius_all(8) var disabled := StyleBoxFlat.new() disabled.bg_color = Color(Brand.ROYAL_BLUE, 0.35) disabled.set_corner_radius_all(8) button.add_theme_stylebox_override("normal", normal) button.add_theme_stylebox_override("hover", hover) button.add_theme_stylebox_override("pressed", normal) button.add_theme_stylebox_override("disabled", disabled) button.add_theme_color_override("font_color", Brand.DARK_TEXT) button.add_theme_color_override("font_hover_color", Brand.DARK_TEXT) button.add_theme_color_override("font_disabled_color", Color(Brand.DARK_TEXT, 0.7)) 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 status_label.add_theme_color_override("font_color", Brand.MAGENTA) func _show_message(message: String) -> void: status_label.text = message status_label.add_theme_color_override("font_color", Brand.SUCCESS) _refresh() func _format_thousands(value: int) -> String: var digits := str(absi(value)) var grouped := "" var count := 0 for index in range(digits.length() - 1, -1, -1): grouped = digits[index] + grouped count += 1 if count % 3 == 0 and index != 0: grouped = "," + grouped return ("-" if value < 0 else "") + grouped