feat: skin update

This commit is contained in:
2026-04-20 19:32:52 +08:00
parent b492dc99b6
commit f2e14f20f3
28 changed files with 1396 additions and 113 deletions
+107 -19
View File
@@ -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