From 19371b7a93a595726f3db3fa31dc4caebb811233 Mon Sep 17 00:00:00 2001 From: hz Date: Wed, 29 Jul 2026 19:01:23 -0500 Subject: [PATCH] Restyle the RZ Coin Shop screen as a card grid Replaces the plain title/list layout with a header, a coin-balance summary card, and a product card grid (icon, description, price, solid Buy button) matching the app's existing card/badge visual language. Only the two real products render - no fabricated categories or bundles, since the backend doesn't have that data yet. --- assets/icons/lucide/rotate-ccw.svg | 14 + assets/icons/lucide/rotate-ccw.svg.import | 43 +++ scripts/ui/main.gd | 2 +- scripts/ui/rz_store_screen.gd | 388 +++++++++++++++++++--- 4 files changed, 396 insertions(+), 51 deletions(-) create mode 100644 assets/icons/lucide/rotate-ccw.svg create mode 100644 assets/icons/lucide/rotate-ccw.svg.import diff --git a/assets/icons/lucide/rotate-ccw.svg b/assets/icons/lucide/rotate-ccw.svg new file mode 100644 index 0000000..717b7c0 --- /dev/null +++ b/assets/icons/lucide/rotate-ccw.svg @@ -0,0 +1,14 @@ + + + + diff --git a/assets/icons/lucide/rotate-ccw.svg.import b/assets/icons/lucide/rotate-ccw.svg.import new file mode 100644 index 0000000..80b5844 --- /dev/null +++ b/assets/icons/lucide/rotate-ccw.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cwt1kxtnhfgqo" +path="res://.godot/imported/rotate-ccw.svg-72097dc869bb37466442f5645fc6d506.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/icons/lucide/rotate-ccw.svg" +dest_files=["res://.godot/imported/rotate-ccw.svg-72097dc869bb37466442f5645fc6d506.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/scripts/ui/main.gd b/scripts/ui/main.gd index ae84710..d0c5cf7 100644 --- a/scripts/ui/main.gd +++ b/scripts/ui/main.gd @@ -432,7 +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) + _add_nav_button(container, "rz_store", "RZ Coin Shop", "star", true) var spacer := Control.new() spacer.size_flags_vertical = Control.SIZE_EXPAND_FILL diff --git a/scripts/ui/rz_store_screen.gd b/scripts/ui/rz_store_screen.gd index af393b9..81be5e1 100644 --- a/scripts/ui/rz_store_screen.gd +++ b/scripts/ui/rz_store_screen.gd @@ -1,13 +1,27 @@ 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 balance_label: Label +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_box: VBoxContainer +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 := "" @@ -15,32 +29,36 @@ 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) + + _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() @@ -49,45 +67,301 @@ func on_show(_payload: Variant = null) -> void: _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: - balance_label.text = "%d Rz Coins" % CreditsService.credits - inventory_label.text = "Inventory: %d exam token(s) · %d QBank reset token(s)" % [ + 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_box.get_children(): + 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 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) + products_grid.add_child(_build_product_card(dark, product)) 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) + 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: @@ -114,8 +388,22 @@ func _ask_reset() -> void: 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