extends Control signal closed # --- Node References (% = unique name, path-independent) --- @onready var star_label: Label = %StarLabel @onready var gold_label: Label = %GoldLabel @onready var item_grid: GridContainer = %ItemGrid @onready var back_btn: Button = %BackBtn @onready var status_label: Label = %StatusLabel # Tabs @onready var tab_head: Button = %TabHead @onready var tab_costume: Button = %TabCostume @onready var tab_glove: Button = %TabGlove @onready var tab_acc: Button = %TabAccessory @onready var tab_gold: Button = %TabGold @onready var tab_star: Button = %TabStar # 3D Preview @onready var character_root: Node3D = %CharacterRoot @onready var anim_player: AnimationPlayer = %AnimationPlayer @onready var prev_btn: Button = %PrevBtn @onready var next_btn: Button = %NextBtn @onready var char_name_label: Label = %CharName # --- State --- var current_category: String = "head" var current_char_idx: int = 0 # Node names inside the GLB scene (CharacterRoot children) var available_chars: Array[String] = ["Bob", "Masbro", "Gatot", "Oldpop"] # Display name shown in the label -> node name in GLB const DISPLAY_TO_NODE: Dictionary = { "Copper": "Oldpop", "Dabro": "Masbro", "Pip": "Bob", "Gatot": "Gatot", } # Reverse: node name -> display name shown to player const NODE_TO_DISPLAY: Dictionary = { "Oldpop": "Copper", "Masbro": "Dabro", "Bob": "Pip", "Gatot": "Gatot", } # Drag tracking var _is_dragging: bool = false var _last_mouse_x: float = 0.0 # ----------------------------------------------------------------------- # Gold-pack catalog: { id, label, amount, bonus, usd } # Matches the mockup tiers: 100 / 500+50 / 1000+150 / 2000+400 / 5000+1250 / 10000+3000 # ----------------------------------------------------------------------- const GOLD_PACKS: Array = [ {"id": "gold_100", "label": "100", "amount": 100, "bonus": 0, "usd": 0.99}, {"id": "gold_500", "label": "500", "amount": 500, "bonus": 50, "usd": 4.99}, {"id": "gold_1000", "label": "1000", "amount": 1000, "bonus": 150, "usd": 9.99}, {"id": "gold_2000", "label": "2000", "amount": 2000, "bonus": 400, "usd": 19.99}, {"id": "gold_5000", "label": "5000", "amount": 5000, "bonus": 1250, "usd": 49.99}, {"id": "gold_10000", "label": "10000", "amount": 10000, "bonus": 3000, "usd": 99.99}, ] # Star-to-gold conversion rates (spent in gold, received in star) const STAR_PACKS: Array = [ {"id": "star_100", "label": "100 Star", "amount": 100, "gold_cost": 500}, {"id": "star_250", "label": "250 Star", "amount": 250, "gold_cost": 1100}, {"id": "star_600", "label": "600 Star", "amount": 600, "gold_cost": 2500}, ] # ----------------------------------------------------------------------- # _ready # ----------------------------------------------------------------------- func _ready() -> void: back_btn.pressed.connect(_on_close) tab_head.pressed.connect(_on_tab_selected.bind("head")) tab_costume.pressed.connect(_on_tab_selected.bind("costume")) tab_glove.pressed.connect(_on_tab_selected.bind("glove")) tab_acc.pressed.connect(_on_tab_selected.bind("accessory")) tab_gold.pressed.connect(_on_tab_selected.bind("gold_packs")) tab_star.pressed.connect(_on_tab_selected.bind("star_packs")) prev_btn.pressed.connect(_on_prev_char) next_btn.pressed.connect(_on_next_char) if UserProfileManager.profile_updated.connect(_refresh_wallet) != OK: pass _setup_3d_preview() if UserProfileManager.shop_catalog.is_empty(): _fetch_and_build() else: _build_shop() # ----------------------------------------------------------------------- # Local 3D preview # ----------------------------------------------------------------------- func _setup_3d_preview() -> void: # loadout_character stores display names (e.g. "Copper") — convert to node name first var def_raw: String = UserProfileManager.profile.get("loadout_character", "") var def_node: String = DISPLAY_TO_NODE.get(def_raw, def_raw) # "Copper" -> "Oldpop" var idx: int = available_chars.find(def_node) if idx != -1: current_char_idx = idx _update_char_name_label() _update_preview_char() func _on_prev_char() -> void: current_char_idx = (current_char_idx - 1 + available_chars.size()) % available_chars.size() _update_char_name_label() _update_preview_char() func _on_next_char() -> void: current_char_idx = (current_char_idx + 1) % available_chars.size() _update_char_name_label() _update_preview_char() func _update_char_name_label() -> void: var node_name: String = available_chars[current_char_idx] # Show the player-facing display name (e.g. "Copper" instead of "Oldpop") char_name_label.text = NODE_TO_DISPLAY.get(node_name, node_name) func _update_preview_char() -> void: if not character_root: return var target_node_name := available_chars[current_char_idx] for child in character_root.get_children(): if child is Node3D: child.visible = (child.name == target_node_name) var active_char_node := character_root.get_node_or_null(target_node_name) as Node3D if active_char_node and anim_player: anim_player.root_node = active_char_node.get_path() if anim_player.has_animation("animation-pack/idle"): anim_player.play("animation-pack/idle") elif anim_player.get_animation_list().size() > 0: anim_player.play(anim_player.get_animation_list()[0]) # Apply the player's current loadout materials SkinManager.apply_loadout(character_root, UserProfileManager.loadout) # ----------------------------------------------------------------------- # Drag-to-rotate # ----------------------------------------------------------------------- func _input(event: InputEvent) -> void: if not visible: return if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT: _is_dragging = event.pressed if event.pressed: _last_mouse_x = event.position.x elif event is InputEventMouseMotion and _is_dragging and character_root: var delta: float = event.position.x - _last_mouse_x character_root.rotation_degrees.y += delta * 0.5 _last_mouse_x = event.position.x # ----------------------------------------------------------------------- # Public entry point called by the parent scene # ----------------------------------------------------------------------- func show_panel() -> void: show() _setup_3d_preview() if UserProfileManager.shop_catalog.is_empty(): _fetch_and_build() else: _refresh_wallet() func _fetch_and_build() -> void: status_label.text = "Loading catalog..." await UserProfileManager.fetch_shop_catalog() _build_shop() func _build_shop() -> void: _refresh_wallet() _populate_current_tab() func _on_tab_selected(category: String) -> void: current_category = category _populate_current_tab() # ----------------------------------------------------------------------- # Grid population — builds cards dynamically from localized templates # ----------------------------------------------------------------------- func _populate_current_tab() -> void: for child in item_grid.get_children(): child.queue_free() var catalog := UserProfileManager.shop_catalog match current_category: "gold_packs": for pack in GOLD_PACKS: item_grid.add_child(_make_gold_card(pack)) "star_packs": for pack in STAR_PACKS: item_grid.add_child(_make_star_card(pack)) _: var items: Array = catalog.get(current_category, []) for item in items: item_grid.add_child(_make_cosmetic_card(item)) # ----------------------------------------------------------------------- # Card builders — instantiated from scene templates # ----------------------------------------------------------------------- @onready var template_gold_card: Control = %GoldCard @onready var template_star_card: Control = %StarCard @onready var template_cosmetic_card: Control = %CosmeticCard func _make_gold_card(pack: Dictionary) -> Control: var card: Control = template_gold_card.duplicate() card.visible = true var amount_lbl: Label = card.find_child("AmountLabel", true, false) as Label if amount_lbl: amount_lbl.text = "⭐ %d" % pack.amount var bonus_lbl: Label = card.find_child("BonusLabel", true, false) as Label if bonus_lbl: if pack.bonus > 0: bonus_lbl.text = "+%d" % pack.bonus bonus_lbl.show() else: bonus_lbl.hide() var price_lbl: Label = card.find_child("PriceLabel", true, false) as Label if price_lbl: price_lbl.text = "$ %.2f" % pack.usd var buy_btn: Button = card.find_child("BuyBtn", true, false) as Button if buy_btn: buy_btn.pressed.connect(_on_buy_gold_pressed.bind(pack)) return card func _make_star_card(pack: Dictionary) -> Control: var card: Control = template_star_card.duplicate() card.visible = true var amount_lbl: Label = card.find_child("AmountLabel", true, false) as Label if amount_lbl: amount_lbl.text = "✦ %d" % pack.amount var cost_lbl: Label = card.find_child("CostLabel", true, false) as Label if cost_lbl: cost_lbl.text = "Cost: ⭐ %d Gold" % pack.gold_cost var buy_btn: Button = card.find_child("BuyBtn", true, false) as Button if buy_btn: buy_btn.pressed.connect(_on_buy_star_pressed.bind(pack)) return card func _make_cosmetic_card(item: Dictionary) -> Control: var card: Control = template_cosmetic_card.duplicate() card.visible = true var item_id: String = item.get("id", "") var already_owned: bool = UserProfileManager.inventory.has(item_id) var name_lbl: Label = card.find_child("NameLabel", true, false) as Label if name_lbl: name_lbl.text = item.get("name", item_id) var rarity: String = item.get("rarity", "Common") var rarity_lbl: Label = card.find_child("RarityLabel", true, false) as Label if rarity_lbl: rarity_lbl.text = rarity var rarity_col: Color = { "Common": Color(0.50, 0.50, 0.50), "Rare": Color(0.20, 0.50, 0.90), "Epic": Color(0.60, 0.20, 0.80), "Legendary": Color(0.90, 0.60, 0.10), }.get(rarity, Color(0.50, 0.50, 0.50)) rarity_lbl.add_theme_color_override("font_color", rarity_col) var price_lbl: Label = card.find_child("PriceLabel", true, false) as Label if price_lbl: var g: int = int(item.get("gold", 0)) var s: int = int(item.get("star", 0)) if g > 0 and s > 0: price_lbl.text = "⭐ %d ✦ %d" % [g, s] elif g > 0: price_lbl.text = "⭐ %d" % g else: price_lbl.text = "✦ %d" % s var try_btn: Button = card.find_child("TryBtn", true, false) as Button if try_btn: try_btn.pressed.connect(_on_try_pressed.bind(item)) var buy_btn: Button = card.find_child("BuyBtn", true, false) as Button if buy_btn: if already_owned: buy_btn.text = "✓ Owned" buy_btn.disabled = true # Dim the entire card to signal it's already purchased card.modulate = Color(0.55, 0.55, 0.55, 0.85) else: buy_btn.pressed.connect(_on_buy_cosmetic_pressed.bind(item)) return card # ----------------------------------------------------------------------- # Wallet refresh # ----------------------------------------------------------------------- func _refresh_wallet() -> void: var g: int = UserProfileManager.wallet.get("gold", 0) var s: int = UserProfileManager.wallet.get("star", 0) gold_label.text = "⭐ %d" % g star_label.text = "✦ %d" % s status_label.text = "" # ----------------------------------------------------------------------- # Button callbacks # ----------------------------------------------------------------------- # Tracks a revert callable from SkinManager.preview_skin var _preview_revert: Callable = Callable() func _on_try_pressed(item: Dictionary) -> void: status_label.text = "Previewing: " + item.get("name", item.get("id", "?")) # Auto-switch to the character this skin belongs to (if specified) if item.has("character"): var char_name: String = item.get("character") var idx: int = available_chars.find(char_name) if idx != -1 and current_char_idx != idx: current_char_idx = idx _update_char_name_label() _update_preview_char() # Revert any previous preview first if _preview_revert.is_valid(): _preview_revert.call() # Live material preview — SkinManager records a revert snapshot automatically _preview_revert = SkinManager.preview_skin(character_root, item.get("id", "")) func _on_buy_gold_pressed(pack: Dictionary) -> void: status_label.text = "Processing purchase..." var success: bool = await UserProfileManager.buy_currency(pack.id) status_label.text = "Purchased %s Gold!" % pack.label if success else "Purchase failed." if success: _refresh_wallet() func _on_buy_star_pressed(pack: Dictionary) -> void: var cost: int = pack.gold_cost if UserProfileManager.wallet.get("gold", 0) < cost: status_label.text = "Not enough Gold. Need ⭐ %d." % cost return status_label.text = "Converting..." var success: bool = await UserProfileManager.buy_currency(pack.id) status_label.text = "Received ✦ %d Star!" % pack.amount if success else "Conversion failed." if success: _refresh_wallet() func _on_buy_cosmetic_pressed(item: Dictionary) -> void: if UserProfileManager.inventory.has(item.id): status_label.text = "Already owned: " + item.get("name", item.id) return var price_gold: int = item.get("gold", 0) var price_star: int = item.get("star", 0) if UserProfileManager.wallet.get("gold", 0) < price_gold \ or UserProfileManager.wallet.get("star", 0) < price_star: status_label.text = "Not enough currency." return status_label.text = "Purchasing..." var success: bool = await UserProfileManager.purchase_item( item.id, price_gold, price_star, current_category) status_label.text = ("Purchased: " + item.get("name", item.id)) if success else "Purchase failed." if success: _refresh_wallet() # Refresh preview to show newly purchased skin's materials if _preview_revert.is_valid(): _preview_revert.call() _preview_revert = Callable() SkinManager.apply_loadout(character_root, UserProfileManager.loadout) func _on_close() -> void: # Clean up any open preview when closing the shop if _preview_revert.is_valid(): _preview_revert.call() _preview_revert = Callable() hide() emit_signal("closed")