feat: update shop

This commit is contained in:
2026-04-17 00:17:37 +08:00
parent f10d777c90
commit ff0a2e0f41
5 changed files with 914 additions and 130 deletions
+319 -69
View File
@@ -2,90 +2,340 @@ extends Control
signal closed
@onready var tab_container = $Panel/VBoxContainer/TabContainer
@onready var head_grid = $Panel/VBoxContainer/TabContainer/Head/GridContainer
@onready var costume_grid = $Panel/VBoxContainer/TabContainer/Costume/GridContainer
@onready var glove_grid = $Panel/VBoxContainer/TabContainer/Glove/GridContainer
@onready var accessory_grid = $Panel/VBoxContainer/TabContainer/Accessory/GridContainer
@onready var status_label = $Panel/VBoxContainer/StatusLabel
@onready var back_btn = $Panel/VBoxContainer/Header/BackBtn
# --- 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
var shop_items = {
"head": [
{"id": "head_hat1", "name": "Cap", "gold": 100, "star": 0},
{"id": "head_crown", "name": "Crown", "gold": 0, "star": 50}
],
"costume": [
{"id": "costume_red", "name": "Red Suit", "gold": 200, "star": 0},
{"id": "costume_gold", "name": "Gold Suit", "gold": 0, "star": 100}
],
"glove": [
{"id": "glove_leather", "name": "Leather Gloves", "gold": 50, "star": 0}
],
"accessory": [
{"id": "acc_glasses", "name": "Sunglasses", "gold": 80, "star": 0}
]
}
# 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
func _ready():
# 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
var available_chars: Array[String] = ["Bob", "Masbro", "Gatot", "Oldpop"]
# 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:
# Attempt to match the user's currently saved loadout character
var def_char: String = UserProfileManager.profile.get("loadout_character", "Bob")
var idx = available_chars.find(def_char)
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:
char_name_label.text = available_chars[current_char_idx]
func _update_preview_char() -> void:
if not character_root: return
var target_node_name = available_chars[current_char_idx]
var active_char_node: Node3D = null
for child in character_root.get_children():
if child is Node3D:
child.visible = (child.name == target_node_name)
if child.name == target_node_name:
active_char_node = child
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])
if active_char_node:
var p = preload("res://scenes/player.gd").new()
p.apply_loadout(active_char_node)
p.free()
# -----------------------------------------------------------------------
# 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 show_panel():
show()
_refresh_shop()
func _build_shop() -> void:
_refresh_wallet()
_populate_current_tab()
func _build_shop():
_populate_grid(head_grid, shop_items["head"])
_populate_grid(costume_grid, shop_items["costume"])
_populate_grid(glove_grid, shop_items["glove"])
_populate_grid(accessory_grid, shop_items["accessory"])
func _on_tab_selected(category: String) -> void:
current_category = category
_populate_current_tab()
func _populate_grid(grid, items):
for child in grid.get_children():
# -----------------------------------------------------------------------
# Grid population — builds cards dynamically from localized templates
# -----------------------------------------------------------------------
func _populate_current_tab() -> void:
for child in item_grid.get_children():
child.queue_free()
for item in items:
var btn = Button.new()
btn.custom_minimum_size = Vector2(120, 80)
btn.add_theme_font_size_override("font_size", 14)
btn.text = "%s\nGold: %d\nStar: %d" % [item.name, item.gold, item.star]
btn.pressed.connect(_on_buy_pressed.bind(item))
grid.add_child(btn)
func _refresh_shop():
# Visual update to show which items are owned
# (For simplicity, not disabling buttons directly, relying on backend check)
status_label.text = "Welcome to the Shop!"
var catalog := UserProfileManager.shop_catalog
func _on_buy_pressed(item: Dictionary):
if UserProfileManager.inventory.has(item.id):
status_label.text = "Already owned: " + item.name
return
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 price_gold = item.gold
var price_star = item.star
if UserProfileManager.wallet.get("gold", 0) < price_gold or UserProfileManager.wallet.get("star", 0) < price_star:
status_label.text = "Not enough currency for " + item.name
return
var amount_lbl: Label = card.find_child("AmountLabel", true, false) as Label
if amount_lbl: amount_lbl.text = "%d" % pack.amount
status_label.text = "Purchasing " + item.name + "..."
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
# Determine category
var category = ""
if shop_items["head"].has(item): category = "head"
elif shop_items["costume"].has(item): category = "costume"
elif shop_items["glove"].has(item): category = "glove"
elif shop_items["accessory"].has(item): category = "accessory"
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
var success = await UserProfileManager.purchase_item(item.id, price_gold, price_star, category)
func _make_cosmetic_card(item: Dictionary) -> Control:
var card: Control = template_cosmetic_card.duplicate()
card.visible = true
var name_lbl: Label = card.find_child("NameLabel", true, false) as Label
if name_lbl: name_lbl.text = item.get("name", item.get("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: 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
# -----------------------------------------------------------------------
func _on_try_pressed(item: Dictionary) -> void:
status_label.text = "Previewing: " + item.get("name", item.get("id", "?"))
# Auto-switch character if the catalog item targets a specific one.
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()
# Inject into loadout temporarily to preview it without saving
var prev: String = UserProfileManager.loadout.get(current_category, "")
UserProfileManager.loadout[current_category] = item.id
_update_preview_char()
# Revert immediately, so jumping to next character drops preview.
UserProfileManager.loadout[current_category] = prev
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:
status_label.text = "Successfully purchased: " + item.name
_refresh_shop()
else:
status_label.text = "Failed to purchase. Backend error."
_refresh_wallet()
func _on_close():
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()
func _on_close() -> void:
hide()
emit_signal("closed")