diff --git a/scripts/ui/rz_store_screen.gd b/scripts/ui/rz_store_screen.gd index 81be5e1..f0231cf 100644 --- a/scripts/ui/rz_store_screen.gd +++ b/scripts/ui/rz_store_screen.gd @@ -5,6 +5,19 @@ 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 @@ -14,6 +27,9 @@ var summary_icon: TextureRect var balance_value_label: Label var coin_caption_label: Label var inventory_label: Label +var tab_controls: Array = [] +var section_heading: Label +var section_subtitle: Label var products_grid: HFlowContainer var status_label: Label var confirm_dialog: ConfirmationDialog @@ -32,12 +48,19 @@ func _ready() -> void: _build_header() _build_summary_card() + _build_tabs() - 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) + 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) @@ -134,6 +157,66 @@ func _build_summary_card() -> void: 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_footer() -> void: var dark := UIThemeService.is_dark_mode() footer_card = PanelContainer.new() @@ -192,6 +275,8 @@ func _refresh_theme_controls() -> void: 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) + 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)) @@ -209,28 +294,69 @@ func _refresh() -> void: if typeof(product_variant) != TYPE_DICTIONARY: continue var product: Dictionary = product_variant - products_grid.add_child(_build_product_card(dark, product)) + 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_reset_card(dark)) + 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_product_card(dark: bool, product: Dictionary) -> PanelContainer: - var sku := str(product.get("sku", "")) - var price := int(product.get("coinPrice", 0)) +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) - content.add_child(_card_icon(dark, ICON_BY_SKU.get(sku, "star"), Brand.ROYAL_BLUE)) + 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 = str(product.get("name", "Store item")) + 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 = str(product.get("description", "")) + 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)) @@ -238,59 +364,34 @@ func _build_product_card(dark: bool, product: Dictionary) -> PanelContainer: 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) - footer_row.add_child(_price_tag(dark, price)) + 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 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) + 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 @@ -326,6 +427,45 @@ func _card_icon(dark: bool, icon_name: String, accent: Color) -> PanelContainer: 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)