463 lines
17 KiB
GDScript
463 lines
17 KiB
GDScript
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
|
|
@onready var banner1: Button = %Banner1
|
|
@onready var banner2: Button = %Banner2
|
|
@onready var banner3: Button = %Banner3
|
|
|
|
# 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
|
|
|
|
# Maps category -> tab button (populated in _ready)
|
|
var _tab_map: Dictionary = {}
|
|
|
|
# 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:
|
|
_tab_map = {
|
|
"head": tab_head,
|
|
"costume": tab_costume,
|
|
"glove": tab_glove,
|
|
"accessory": tab_acc,
|
|
"gold_packs": tab_gold,
|
|
"star_packs": tab_star,
|
|
}
|
|
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
|
|
|
|
_set_active_tab(current_category)
|
|
_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_banners()
|
|
_populate_current_tab()
|
|
|
|
func _on_tab_selected(category: String) -> void:
|
|
current_category = category
|
|
_set_active_tab(category)
|
|
_populate_current_tab()
|
|
|
|
func _set_active_tab(active_category: String) -> void:
|
|
var style_active := StyleBoxFlat.new()
|
|
style_active.bg_color = Color(1, 1, 1, 1)
|
|
style_active.content_margin_left = 16.0
|
|
style_active.content_margin_top = 10.0
|
|
style_active.content_margin_right = 16.0
|
|
style_active.content_margin_bottom = 10.0
|
|
style_active.corner_radius_top_left = 6
|
|
style_active.corner_radius_top_right = 6
|
|
style_active.corner_radius_bottom_right = 6
|
|
style_active.corner_radius_bottom_left = 6
|
|
|
|
var style_inactive := StyleBoxFlat.new()
|
|
style_inactive.bg_color = Color(0.15, 0.18, 0.22, 1)
|
|
style_inactive.content_margin_left = 16.0
|
|
style_inactive.content_margin_top = 10.0
|
|
style_inactive.content_margin_right = 16.0
|
|
style_inactive.content_margin_bottom = 10.0
|
|
style_inactive.corner_radius_top_left = 6
|
|
style_inactive.corner_radius_top_right = 6
|
|
style_inactive.corner_radius_bottom_right = 6
|
|
style_inactive.corner_radius_bottom_left = 6
|
|
|
|
var style_hover := StyleBoxFlat.new()
|
|
style_hover.bg_color = Color(0.22, 0.26, 0.30, 1)
|
|
style_hover.content_margin_left = 16.0
|
|
style_hover.content_margin_top = 10.0
|
|
style_hover.content_margin_right = 16.0
|
|
style_hover.content_margin_bottom = 10.0
|
|
style_hover.corner_radius_top_left = 6
|
|
style_hover.corner_radius_top_right = 6
|
|
style_hover.corner_radius_bottom_right = 6
|
|
style_hover.corner_radius_bottom_left = 6
|
|
|
|
for cat in _tab_map:
|
|
var btn: Button = _tab_map[cat]
|
|
if cat == active_category:
|
|
btn.add_theme_stylebox_override("normal", style_active)
|
|
btn.add_theme_stylebox_override("hover", style_active)
|
|
btn.add_theme_stylebox_override("pressed", style_active)
|
|
btn.add_theme_color_override("font_color", Color(0.08, 0.09, 0.12))
|
|
btn.add_theme_color_override("font_hover_color", Color(0.08, 0.09, 0.12))
|
|
else:
|
|
btn.add_theme_stylebox_override("normal", style_inactive)
|
|
btn.add_theme_stylebox_override("hover", style_hover)
|
|
btn.add_theme_stylebox_override("pressed", style_inactive)
|
|
btn.add_theme_color_override("font_color", Color(0.7, 0.7, 0.7))
|
|
btn.add_theme_color_override("font_hover_color", Color(1, 1, 1))
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Banner population — promotional / featured items
|
|
# -----------------------------------------------------------------------
|
|
func _populate_banners() -> void:
|
|
var banners: Array[Button] = [banner1, banner2, banner3]
|
|
var promos: Array = UserProfileManager.shop_catalog.get("banners", [])
|
|
for i in banners.size():
|
|
var btn: Button = banners[i]
|
|
if i < promos.size():
|
|
btn.text = promos[i].get("label", "")
|
|
btn.tooltip_text = promos[i].get("id", "")
|
|
btn.show()
|
|
else:
|
|
btn.hide()
|
|
|
|
# -----------------------------------------------------------------------
|
|
# 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: RichTextLabel = card.find_child("AmountLabel", true, false) as RichTextLabel
|
|
if amount_lbl: amount_lbl.text = "[right][img=24x24]res://assets/graphics/gui/lobby/gold.png[/img] %d[/right]" % 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: RichTextLabel = card.find_child("AmountLabel", true, false) as RichTextLabel
|
|
if amount_lbl: amount_lbl.text = "[right][img=24x24]res://assets/graphics/gui/lobby/star.png[/img] %d[/right]" % pack.amount
|
|
|
|
var cost_lbl: RichTextLabel = card.find_child("CostLabel", true, false) as RichTextLabel
|
|
if cost_lbl: cost_lbl.text = "[center]Cost: [img=20x20]res://assets/graphics/gui/lobby/gold.png[/img] %d[/center]" % 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: RichTextLabel = card.find_child("PriceLabel", true, false) as RichTextLabel
|
|
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 = "[center][img=20x20]res://assets/graphics/gui/lobby/gold.png[/img] %d [img=20x20]res://assets/graphics/gui/lobby/star.png[/img] %d[/center]" % [g, s]
|
|
elif g > 0:
|
|
price_lbl.text = "[center][img=20x20]res://assets/graphics/gui/lobby/gold.png[/img] %d[/center]" % g
|
|
else:
|
|
price_lbl.text = "[center][img=20x20]res://assets/graphics/gui/lobby/star.png[/img] %d[/center]" % 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 = str(g)
|
|
star_label.text = str(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
|
|
|
|
status_label.text = "Purchasing..."
|
|
var err: String = await UserProfileManager.purchase_item(item.id)
|
|
|
|
if err == "":
|
|
status_label.text = "Purchased: " + item.get("name", item.id)
|
|
_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)
|
|
else:
|
|
if "NotEnoughFunds" in err or "funds" in err.to_lower() or "wallet" in err.to_lower():
|
|
status_label.text = "Not enough currency."
|
|
else:
|
|
status_label.text = "Purchase failed."
|
|
|
|
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")
|