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", } const FEATURES_BY_SKU := { "exam_token_1": ["Full 6-block exam", "Timed & scored", "Performance analytics"], "qbank_reset_1": ["Reset eligibility", "Keep your stats", "Smart spaced retention"], } const POPULAR_SKUS := ["exam_token_1"] const TAB_DEFS := [ {"label": "Featured", "icon": "star", "active": true}, {"label": "Tokens", "icon": "clipboard-list", "active": false}, {"label": "Boosts", "icon": "activity", "active": false}, {"label": "Content", "icon": "book-open", "active": false}, {"label": "Customization", "icon": "settings", "active": false}, {"label": "Bundles", "icon": "clipboard-plus", "active": false}, ] 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 tab_controls: Array = [] var banner_card: PanelContainer var banner_icon_panel: PanelContainer var banner_heading: Label var banner_description: Label var section_heading: Label var section_subtitle: 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() _build_tabs() _build_slot_machine_banner() section_heading = Label.new() section_heading.text = "Featured" section_heading.add_theme_font_override("font", Brand.font) section_heading.add_theme_font_size_override("font_size", 18) add_child(section_heading) section_subtitle = Label.new() section_subtitle.text = "Hand-picked items to supercharge your Step 3 prep." section_subtitle.add_theme_font_override("font", Brand.font) section_subtitle.add_theme_font_size_override("font_size", 12) section_subtitle.add_theme_color_override("font_color", Brand.muted_color(UIThemeService.is_dark_mode())) add_child(section_subtitle) 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_tabs() -> void: var dark := UIThemeService.is_dark_mode() tab_controls.clear() var tabs_row := HFlowContainer.new() tabs_row.add_theme_constant_override("h_separation", 20) tabs_row.add_theme_constant_override("v_separation", 8) add_child(tabs_row) for tab_def_variant in TAB_DEFS: var tab_def: Dictionary = tab_def_variant var tab := VBoxContainer.new() tab.add_theme_constant_override("separation", 6) tabs_row.add_child(tab) var content := HBoxContainer.new() content.add_theme_constant_override("separation", 6) tab.add_child(content) var icon := TextureRect.new() icon.texture = load("res://assets/icons/lucide/%s.svg" % str(tab_def.get("icon", "star"))) icon.custom_minimum_size = Vector2(16, 16) icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED content.add_child(icon) var label := Label.new() label.text = str(tab_def.get("label", "")) label.add_theme_font_override("font", Brand.font) label.add_theme_font_size_override("font_size", 14) content.add_child(label) var underline := ColorRect.new() underline.custom_minimum_size = Vector2(0, 2) tab.add_child(underline) var active := bool(tab_def.get("active", false)) tab_controls.append({"icon": icon, "label": label, "underline": underline, "active": active}) var divider := ColorRect.new() divider.custom_minimum_size = Vector2(0, 1) add_child(divider) tab_controls.append({"divider": divider}) _apply_tab_theme(dark) func _apply_tab_theme(dark: bool) -> void: for entry_variant in tab_controls: var entry: Dictionary = entry_variant if entry.has("divider"): (entry["divider"] as ColorRect).color = Color("2a365a") if dark else Brand.LIGHT_BORDER continue var active := bool(entry["active"]) var color := Brand.ROYAL_BLUE if active else Brand.muted_color(dark) (entry["icon"] as TextureRect).modulate = color (entry["label"] as Label).add_theme_color_override("font_color", color) (entry["underline"] as ColorRect).color = Brand.ROYAL_BLUE if active else Color(0, 0, 0, 0) func _build_slot_machine_banner() -> void: var dark := UIThemeService.is_dark_mode() banner_card = PanelContainer.new() banner_card.add_theme_stylebox_override("panel", Brand.content_card_box(dark, true, Brand.ROYAL_BLUE)) add_child(banner_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) banner_card.add_child(margin) var row := HBoxContainer.new() row.add_theme_constant_override("separation", 14) margin.add_child(row) banner_icon_panel = PanelContainer.new() banner_icon_panel.custom_minimum_size = Vector2(40, 40) banner_icon_panel.size_flags_vertical = Control.SIZE_SHRINK_CENTER banner_icon_panel.add_theme_stylebox_override("panel", Brand.icon_circle_box(dark, Brand.ROYAL_BLUE, 20)) var icon := TextureRect.new() icon.texture = load("res://assets/icons/lucide/star.svg") icon.custom_minimum_size = Vector2(20, 20) icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED icon.modulate = Brand.ROYAL_BLUE banner_icon_panel.add_child(icon) row.add_child(banner_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) banner_heading = Label.new() banner_heading.text = "RZ Coin Slot Machine" banner_heading.add_theme_font_override("font", Brand.font) banner_heading.add_theme_font_size_override("font_size", 15) banner_heading.add_theme_color_override("font_color", Brand.text_color(dark)) copy.add_child(banner_heading) banner_description = Label.new() banner_description.text = "Spend RZ Coins for a chance at bigger rewards." banner_description.add_theme_font_override("font", Brand.font) banner_description.add_theme_font_size_override("font_size", 12) banner_description.add_theme_color_override("font_color", Brand.muted_color(dark)) copy.add_child(banner_description) row.add_child(copy) var play_button := Button.new() play_button.text = "Play Now" play_button.custom_minimum_size = Vector2(96, 36) play_button.add_theme_font_override("font", Brand.font) _style_buy_button(play_button) play_button.pressed.connect(func(): main.show_screen("slot_machine")) row.add_child(play_button) 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)) _apply_tab_theme(dark) banner_card.add_theme_stylebox_override("panel", Brand.content_card_box(dark, true, Brand.ROYAL_BLUE)) banner_icon_panel.add_theme_stylebox_override("panel", Brand.icon_circle_box(dark, Brand.ROYAL_BLUE, 20)) banner_heading.add_theme_color_override("font_color", Brand.text_color(dark)) banner_description.add_theme_color_override("font_color", Brand.muted_color(dark)) section_subtitle.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 var sku := str(product.get("sku", "")) var price := int(product.get("coinPrice", 0)) products_grid.add_child(_build_item_card( dark, ICON_BY_SKU.get(sku, "star"), Brand.ROYAL_BLUE, str(product.get("name", "Store item")), str(product.get("description", "")), FEATURES_BY_SKU.get(sku, []), POPULAR_SKUS.has(sku), price, "Buy", CreditsService.credits < price, _ask_purchase.bind(sku, str(product.get("name", "item")), price))) if CreditsService.qbank_reset_tokens > 0: products_grid.add_child(_build_item_card( dark, "rotate-ccw", Brand.SUCCESS, "Use QBank Reset Token", "Question eligibility resets; ELO, history, bookmarks, notes, reviews, and earning limits are preserved.", FEATURES_BY_SKU.get("qbank_reset_1", []), false, -1, "Use token", false, _ask_reset)) func _build_item_card( dark: bool, icon_name: String, accent: Color, title: String, description_text: String, features: Array, popular: bool, price: int, button_text: String, button_disabled: bool, on_press: Callable ) -> PanelContainer: var card := _new_card(dark) var content: VBoxContainer = card.get_child(0).get_child(0) var top_row := HBoxContainer.new() content.add_child(top_row) top_row.add_child(_card_icon(dark, icon_name, accent)) var badge_spacer := Control.new() badge_spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL top_row.add_child(badge_spacer) if popular: top_row.add_child(_popular_badge()) var name_label := Label.new() name_label.text = title 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 = description_text 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) if not features.is_empty(): var bullets := VBoxContainer.new() bullets.add_theme_constant_override("separation", 4) content.add_child(bullets) for feature_text in features: bullets.add_child(_feature_row(dark, accent, str(feature_text))) var divider := ColorRect.new() divider.custom_minimum_size = Vector2(0, 1) divider.color = Color("2a365a") if dark else Brand.LIGHT_BORDER content.add_child(divider) var footer_row := HBoxContainer.new() footer_row.add_theme_constant_override("separation", 8) content.add_child(footer_row) if price >= 0: 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 button := Button.new() button.text = button_text button.custom_minimum_size = Vector2(64, 34) button.add_theme_font_override("font", Brand.font) _style_buy_button(button) button.disabled = button_disabled button.pressed.connect(on_press) footer_row.add_child(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 _popular_badge() -> PanelContainer: var badge := PanelContainer.new() badge.size_flags_vertical = Control.SIZE_SHRINK_BEGIN var box := StyleBoxFlat.new() box.bg_color = Brand.ROYAL_BLUE box.set_corner_radius_all(6) box.content_margin_left = 8 box.content_margin_right = 8 box.content_margin_top = 3 box.content_margin_bottom = 3 badge.add_theme_stylebox_override("panel", box) var label := Label.new() label.text = "Popular" label.add_theme_font_override("font", Brand.font) label.add_theme_font_size_override("font_size", 11) label.add_theme_color_override("font_color", Brand.DARK_TEXT) badge.add_child(label) return badge func _feature_row(dark: bool, accent: Color, text: String) -> HBoxContainer: var row := HBoxContainer.new() row.add_theme_constant_override("separation", 6) var icon := TextureRect.new() icon.texture = load("res://assets/icons/lucide/circle-check.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 = accent row.add_child(icon) var label := Label.new() label.text = text label.add_theme_font_override("font", Brand.font) label.add_theme_font_size_override("font_size", 12) label.add_theme_color_override("font_color", Brand.muted_color(dark)) row.add_child(label) return row 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