feat: skin update
This commit is contained in:
+107
-19
@@ -54,6 +54,7 @@ signal profile_updated
|
||||
|
||||
# Back button + popups
|
||||
@onready var back_btn := %BackBtn as Button
|
||||
@onready var topup_gold_btn := %TopupGoldBtn as Button
|
||||
@onready var avatar_popup := %AvatarSelectionPopup as PopupPanel
|
||||
@onready var avatar_grid := %GridContainer as GridContainer
|
||||
@onready var acc_settings_dialog := %AccountSettingsDialog as AcceptDialog
|
||||
@@ -89,14 +90,30 @@ const RARITY_COLORS: Dictionary = {
|
||||
"Legendary": Color(1.00, 0.70, 0.10, 1)
|
||||
}
|
||||
## Client-side item catalog: item_id -> { name, rarity, star_value }
|
||||
## Add new entries here whenever a new item is added to the shop.
|
||||
const ITEM_CATALOG: Dictionary = {
|
||||
"head_hat1": {"name": "Cap", "rarity": "Common", "star_value": 50},
|
||||
"head_crown": {"name": "Crown", "rarity": "Epic", "star_value": 1500},
|
||||
"costume_red": {"name": "Red Suit", "rarity": "Rare", "star_value": 200},
|
||||
"costume_gold": {"name": "Gold Suit", "rarity": "Epic", "star_value": 1000},
|
||||
"glove_leather": {"name": "Leather Gloves", "rarity": "Common", "star_value": 50},
|
||||
"acc_glasses": {"name": "Sunglasses", "rarity": "Rare", "star_value": 300},
|
||||
# ── Generic items ──────────────────────────────────────────────────────
|
||||
"head_hat1": {"name": "Cap", "rarity": "Common", "star_value": 50},
|
||||
"head_crown": {"name": "Crown", "rarity": "Epic", "star_value": 1500},
|
||||
"costume_red": {"name": "Red Suit", "rarity": "Rare", "star_value": 200},
|
||||
"costume_gold": {"name": "Gold Suit", "rarity": "Epic", "star_value": 1000},
|
||||
"glove_leather": {"name": "Leather Gloves", "rarity": "Common", "star_value": 50},
|
||||
"acc_glasses": {"name": "Sunglasses", "rarity": "Rare", "star_value": 300},
|
||||
# ── Oldpop (Copper) — Hat ─────────────────────────────────────────────
|
||||
"oldpop_hat1": {"name": "Oldpop Hat I", "rarity": "Common", "star_value": 100},
|
||||
"oldpop_hat2": {"name": "Oldpop Hat II", "rarity": "Rare", "star_value": 250},
|
||||
"oldpop_hat3": {"name": "Oldpop Hat III", "rarity": "Epic", "star_value": 800},
|
||||
# ── Oldpop (Copper) — Costume ─────────────────────────────────────────
|
||||
"oldpop_body": {"name": "Oldpop Body", "rarity": "Rare", "star_value": 200},
|
||||
"oldpop_arms": {"name": "Oldpop Arms", "rarity": "Common", "star_value": 100},
|
||||
"oldpop-grey-pant": {"name": "Grey Pants", "rarity": "Common", "star_value": 80},
|
||||
"oldpop-clothing-original": {"name": "Original Pants", "rarity": "Common", "star_value": 80},
|
||||
# kept for backward compat with older purchases
|
||||
"oldpop_clothing_original": {"name": "Original Pants", "rarity": "Common", "star_value": 80},
|
||||
# ── Oldpop (Copper) — Gloves ──────────────────────────────────────────
|
||||
"oldpop_gloves": {"name": "Oldpop Gloves", "rarity": "Common", "star_value": 60},
|
||||
}
|
||||
|
||||
const ITEMS_PER_PAGE: int = 9
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
@@ -149,6 +166,7 @@ func _connect_signals() -> void:
|
||||
char_left_btn.pressed.connect(func(): _cycle_loadout_char(-1))
|
||||
char_right_btn.pressed.connect(func(): _cycle_loadout_char(1))
|
||||
set_default_btn.pressed.connect(_on_set_default_pressed)
|
||||
topup_gold_btn.pressed.connect(_on_topup_gold_pressed)
|
||||
|
||||
# Category tabs
|
||||
head_tab_btn.pressed.connect(func(): _on_category_tab_pressed("head"))
|
||||
@@ -237,10 +255,30 @@ func _highlight_active_tab() -> void:
|
||||
|
||||
func _rebuild_category_items() -> void:
|
||||
_category_items.clear()
|
||||
var prefix := _current_category + "_"
|
||||
var prefix := _current_category + "_"
|
||||
# Resolve the current character's node name (e.g. "Copper" → "Oldpop")
|
||||
var current_char_display: String = CHARACTERS[_loadout_index]
|
||||
var current_char_node: String = CHAR_NODE_MAP.get(current_char_display, current_char_display)
|
||||
|
||||
for item_id: String in UserProfileManager.inventory:
|
||||
if item_id.begins_with(prefix):
|
||||
_category_items.append(item_id)
|
||||
# Look up the skin data from SkinManager first (handles all id formats)
|
||||
var skin_data: Dictionary = SkinManager.SKIN_CATALOG.get(item_id, {})
|
||||
|
||||
if not skin_data.is_empty():
|
||||
# Only show items that match the current category
|
||||
if skin_data.get("category", "") != _current_category:
|
||||
continue
|
||||
# Only show items that belong to this character (or have no character restriction)
|
||||
var item_char: String = skin_data.get("character", "")
|
||||
if item_char.is_empty() or item_char == current_char_node:
|
||||
if not _category_items.has(item_id):
|
||||
_category_items.append(item_id)
|
||||
else:
|
||||
# Fallback: generic prefix-based match (e.g. "head_hat1" under "head" tab)
|
||||
if item_id.begins_with(prefix):
|
||||
if not _category_items.has(item_id):
|
||||
_category_items.append(item_id)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Item grid
|
||||
@@ -291,8 +329,12 @@ func _show_item_info(item_id: String) -> void:
|
||||
item_price_label.text = str(sv) if sv > 0 else ""
|
||||
|
||||
var equipped: String = UserProfileManager.loadout.get(_current_category, "")
|
||||
equip_btn.text = "✓ Equipped" if equipped == item_id else "Equip"
|
||||
equip_btn.disabled = (equipped == item_id)
|
||||
if equipped == item_id:
|
||||
equip_btn.text = "Unequip"
|
||||
equip_btn.disabled = false
|
||||
else:
|
||||
equip_btn.text = "Equip"
|
||||
equip_btn.disabled = false
|
||||
dismantle_btn.disabled = false
|
||||
|
||||
func _clear_item_info() -> void:
|
||||
@@ -308,13 +350,28 @@ func _clear_item_info() -> void:
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
func _on_equip_pressed() -> void:
|
||||
if _selected_item_id.is_empty(): return
|
||||
var ok: bool = await UserProfileManager.update_loadout(_current_category, _selected_item_id)
|
||||
if ok:
|
||||
_set_status("Equipped: " + _selected_item_id, Color(0.4, 1.0, 0.4))
|
||||
_populate_item_grid()
|
||||
_show_item_info(_selected_item_id)
|
||||
var equipped: String = UserProfileManager.loadout.get(_current_category, "")
|
||||
|
||||
if equipped == _selected_item_id:
|
||||
# ── UNEQUIP ──────────────────────────────────────────────
|
||||
var ok: bool = await UserProfileManager.update_loadout(_current_category, "")
|
||||
if ok:
|
||||
_set_status("Unequipped: " + _selected_item_id, Color(1.0, 0.7, 0.3))
|
||||
SkinManager.apply_loadout(character_root, UserProfileManager.loadout)
|
||||
_populate_item_grid()
|
||||
_show_item_info(_selected_item_id)
|
||||
else:
|
||||
_set_status("Failed to unequip.", Color.RED)
|
||||
else:
|
||||
_set_status("Failed to equip item.", Color.RED)
|
||||
# ── EQUIP ────────────────────────────────────────────────
|
||||
var ok: bool = await UserProfileManager.update_loadout(_current_category, _selected_item_id)
|
||||
if ok:
|
||||
_set_status("Equipped: " + _selected_item_id, Color(0.4, 1.0, 0.4))
|
||||
SkinManager.apply_loadout(character_root, UserProfileManager.loadout)
|
||||
_populate_item_grid()
|
||||
_show_item_info(_selected_item_id)
|
||||
else:
|
||||
_set_status("Failed to equip item.", Color.RED)
|
||||
|
||||
func _on_dismantle_pressed() -> void:
|
||||
if _selected_item_id.is_empty(): return
|
||||
@@ -413,6 +470,8 @@ func _update_3d_preview(character_name: String) -> void:
|
||||
anim_player.play("animation-pack/idle")
|
||||
elif anim_player.get_animation_list().size() > 0:
|
||||
anim_player.play(anim_player.get_animation_list()[0])
|
||||
# Apply equipped skins on the newly-visible character
|
||||
SkinManager.apply_loadout(character_root, UserProfileManager.loadout)
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Drag-to-rotate
|
||||
@@ -570,6 +629,17 @@ func _on_logout_pressed() -> void:
|
||||
func _on_admin_panel_pressed() -> void:
|
||||
AdminManager.toggle_admin_panel()
|
||||
|
||||
func _on_topup_gold_pressed() -> void:
|
||||
_set_status("Topping up gold...", Color.WHITE)
|
||||
topup_gold_btn.disabled = true
|
||||
var ok: bool = await UserProfileManager.admin_topup_gold()
|
||||
topup_gold_btn.disabled = false
|
||||
if ok:
|
||||
gold_label.text = str(UserProfileManager.wallet.get("gold", 0))
|
||||
_set_status("Top-up successful!", Color(0.4, 1.0, 0.4))
|
||||
else:
|
||||
_set_status("Top-up failed.", Color.RED)
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Show / Close
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
@@ -582,16 +652,33 @@ func show_panel() -> void:
|
||||
_set_status("Loading profile...", Color.YELLOW)
|
||||
_load_profile_data()
|
||||
_load_loadout()
|
||||
_rebuild_category_items()
|
||||
_populate_item_grid()
|
||||
_check_admin_visibility()
|
||||
show()
|
||||
if AuthManager.is_guest:
|
||||
_on_link_account_pressed()
|
||||
_set_status("Link an email to save progress permanently!", Color.YELLOW)
|
||||
# Reload inventory from server to guarantee fresh data, then refresh the grid
|
||||
_set_status("Refreshing inventory...", Color.WHITE)
|
||||
await UserProfileManager.load_inventory()
|
||||
# Auto-select the first tab that actually has items (avoids confusing empty grid)
|
||||
var found_tab := false
|
||||
for cat in ["head", "costume", "glove", "accessory"]:
|
||||
_current_category = cat
|
||||
_rebuild_category_items()
|
||||
if not _category_items.is_empty():
|
||||
found_tab = true
|
||||
break
|
||||
if not found_tab:
|
||||
_current_category = "head"
|
||||
_rebuild_category_items()
|
||||
_current_page = 0
|
||||
_populate_item_grid()
|
||||
_highlight_active_tab()
|
||||
_set_status("", Color.WHITE)
|
||||
|
||||
func _check_admin_visibility() -> void:
|
||||
admin_panel_btn.hide()
|
||||
topup_gold_btn.hide()
|
||||
if not NakamaManager.client or not NakamaManager.session: return
|
||||
var account = await NakamaManager.client.get_account_async(NakamaManager.session)
|
||||
if account.is_exception(): return
|
||||
@@ -599,6 +686,7 @@ func _check_admin_visibility() -> void:
|
||||
var meta = JSON.parse_string(raw)
|
||||
if meta is Dictionary and meta.get("role", "") in ["owner", "admin"]:
|
||||
admin_panel_btn.show()
|
||||
topup_gold_btn.show()
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Helpers
|
||||
|
||||
+67
-30
@@ -27,7 +27,22 @@ signal closed
|
||||
# --- 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
|
||||
@@ -81,12 +96,13 @@ func _ready() -> void:
|
||||
# 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)
|
||||
# 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()
|
||||
|
||||
@@ -101,31 +117,30 @@ func _on_next_char() -> void:
|
||||
_update_preview_char()
|
||||
|
||||
func _update_char_name_label() -> void:
|
||||
char_name_label.text = available_chars[current_char_idx]
|
||||
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]
|
||||
var active_char_node: Node3D = null
|
||||
|
||||
|
||||
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)
|
||||
if child.name == target_node_name:
|
||||
active_char_node = child
|
||||
|
||||
|
||||
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])
|
||||
|
||||
if active_char_node:
|
||||
var p = preload("res://scenes/player.gd").new()
|
||||
p.apply_loadout(active_char_node)
|
||||
p.free()
|
||||
|
||||
# Apply the player's current loadout materials
|
||||
SkinManager.apply_loadout(character_root, UserProfileManager.loadout)
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Drag-to-rotate
|
||||
@@ -236,9 +251,12 @@ func _make_star_card(pack: Dictionary) -> Control:
|
||||
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.get("id", "?"))
|
||||
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
|
||||
@@ -264,10 +282,18 @@ func _make_cosmetic_card(item: Dictionary) -> Control:
|
||||
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))
|
||||
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
|
||||
# -----------------------------------------------------------------------
|
||||
@@ -281,25 +307,27 @@ func _refresh_wallet() -> void:
|
||||
# -----------------------------------------------------------------------
|
||||
# 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 character if the catalog item targets a specific one.
|
||||
# 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()
|
||||
|
||||
# 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
|
||||
_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..."
|
||||
@@ -335,7 +363,16 @@ func _on_buy_cosmetic_pressed(item: Dictionary) -> void:
|
||||
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")
|
||||
|
||||
Reference in New Issue
Block a user