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
+3
View File
@@ -296,6 +296,9 @@ func _setup_3d_preview() -> 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 cosmetics so the lobby preview shows the current loadout
SkinManager.apply_loadout(character_root, UserProfileManager.loadout)
func _load_character_textures() -> void:
"""Load character preview textures."""
+26 -42
View File
@@ -180,6 +180,7 @@ var _is_highlighting: bool = false
@onready var vfx_stunned: AnimatedSprite3D = $receiver_skill_stunned
var _selected_character: String = "Masbro"
var _player_loadout: Dictionary = {} ## Synced from the owning client via sync_loadout RPC
var selected_character: String:
get: return _selected_character
set(value):
@@ -501,8 +502,9 @@ func set_character(character_name: String) -> void:
if active_character:
_apply_outline_recursive(active_character)
# Apply cosmetic loadout to active model (local auth only)
if active_character and is_multiplayer_authority():
# Apply cosmetic loadout to active model.
# Uses _player_loadout (synced by owning client) so ALL peers see the correct skin.
if active_character:
apply_loadout(active_character)
func _apply_outline_recursive(node: Node):
@@ -561,45 +563,17 @@ const COSMETIC_MAPPING = {
}
func apply_loadout(character_node: Node3D) -> void:
"""Apply equipped cosmetics from UserProfileManager.loadout onto the active character model.
It uses COSMETIC_MAPPING to dynamically swap visibility and materials of internal meshes."""
if not Engine.get_main_loop().root.has_node("UserProfileManager"):
return
var loadout: Dictionary = UserProfileManager.loadout
var all_meshes = _get_all_mesh_instances(character_node)
for category in ["head", "costume", "glove", "accessory"]:
var equipped: String = loadout.get(category, "")
# Fallback basic logic for direct children
for child in character_node.get_children():
if child.name.begins_with(category):
child.visible = (child.name == equipped)
# Advanced mapping logic for deep node part swapping and materials
if equipped and COSMETIC_MAPPING.has(equipped):
var mapping = COSMETIC_MAPPING[equipped]
# Visibility overrides
var to_hide = mapping.get("hide", [])
var to_show = mapping.get("show", [])
for mesh in all_meshes:
if mesh.name in to_hide:
mesh.visible = false
if mesh.name in to_show:
mesh.visible = true
# Material overrides
var mats = mapping.get("materials", {})
for mesh_name in mats:
var mat_path = mats[mesh_name]
for mesh in all_meshes:
if mesh.name == mesh_name:
if ResourceLoader.exists(mat_path):
var new_mat = load(mat_path)
mesh.material_override = new_mat
"""Apply equipped cosmetics via SkinManager — the single source of truth.
Uses _player_loadout (synced from the owning client via sync_loadout RPC)
so all peers render the same skin regardless of their own loadout."""
# Fall back to UserProfileManager for the local player before network sync arrives
var loadout: Dictionary = _player_loadout if not _player_loadout.is_empty() \
else UserProfileManager.loadout
# Pass 'self' (the player CharacterBody3D) as character_root because SkinManager
# looks for a CHILD node named e.g. "Oldpop" inside character_root.
# self.$Oldpop, self.$Bob etc. are direct children, matching SKIN_CATALOG "character" keys.
SkinManager.apply_loadout(self, loadout)
func _get_all_mesh_instances(node: Node) -> Array:
var result = []
@@ -614,6 +588,13 @@ func sync_character(character_name: String) -> void:
"""Sync character selection across all clients."""
set_character(character_name)
@rpc("any_peer", "call_local", "reliable")
func sync_loadout(loadout_data: Dictionary) -> void:
"""Sync the player's skin loadout to all peers so everyone sees the correct skin."""
_player_loadout = loadout_data
# Pass 'self' as character_root — SkinManager will find $Oldpop/$Bob/etc. by name
SkinManager.apply_loadout(self, _player_loadout)
func _setup_character() -> void:
"""Initialize character based on LobbyManager selection or defaults."""
# Bots self-assign characters based on ID in _ready()
@@ -634,9 +615,12 @@ func _setup_character() -> void:
set_character(character_name)
# If this is our local player, also sync to other clients for late joiners
# If this is our local player, sync character AND loadout to all peers
if is_multiplayer_authority() and can_rpc():
# Populate _player_loadout from UserProfileManager and broadcast
_player_loadout = UserProfileManager.loadout.duplicate()
rpc("sync_character", character_name)
rpc("sync_loadout", _player_loadout)
# =============================================================================
# Animation Functions
+10
View File
@@ -0,0 +1,10 @@
[gd_scene format=3 uid="uid://cxctemsxkw6p8"]
[ext_resource type="Script" uid="uid://d3wlsx2lbdlge" path="res://scripts/tools/skin_catalog_editor.gd" id="1_editor"]
[node name="SkinCatalogEditor" type="Control" unique_id=1992555523]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource("1_editor")
+17
View File
@@ -249,6 +249,14 @@ theme_override_colors/font_color = Color(1, 0.4, 0.4, 1)
theme_override_fonts/font = ExtResource("3_font")
text = "Server Admin"
[node name="TopupGoldBtn" type="Button" parent="MainMargin/MainHBox/LeftCol" unique_id=1155262038]
unique_name_in_owner = true
visible = false
layout_mode = 2
theme_override_colors/font_color = Color(0.4, 1, 0.4, 1)
theme_override_fonts/font = ExtResource("3_font")
text = "⭐ Top-up Gold"
[node name="LogoutBtn" type="Button" parent="MainMargin/MainHBox/LeftCol" unique_id=612591334]
unique_name_in_owner = true
layout_mode = 2
@@ -559,54 +567,63 @@ custom_minimum_size = Vector2(88, 88)
layout_mode = 2
size_flags_horizontal = 2
size_flags_vertical = 2
autowrap_mode = 1
[node name="Slot1" type="Button" parent="MainMargin/MainHBox/RightCol/ItemGridCard/Margin/VBox/ItemGrid" unique_id=1570438748]
custom_minimum_size = Vector2(88, 88)
layout_mode = 2
size_flags_horizontal = 2
size_flags_vertical = 2
autowrap_mode = 1
[node name="Slot2" type="Button" parent="MainMargin/MainHBox/RightCol/ItemGridCard/Margin/VBox/ItemGrid" unique_id=1730218687]
custom_minimum_size = Vector2(88, 88)
layout_mode = 2
size_flags_horizontal = 2
size_flags_vertical = 2
autowrap_mode = 1
[node name="Slot3" type="Button" parent="MainMargin/MainHBox/RightCol/ItemGridCard/Margin/VBox/ItemGrid" unique_id=533222646]
custom_minimum_size = Vector2(88, 88)
layout_mode = 2
size_flags_horizontal = 2
size_flags_vertical = 2
autowrap_mode = 1
[node name="Slot4" type="Button" parent="MainMargin/MainHBox/RightCol/ItemGridCard/Margin/VBox/ItemGrid" unique_id=1359133451]
custom_minimum_size = Vector2(88, 88)
layout_mode = 2
size_flags_horizontal = 2
size_flags_vertical = 2
autowrap_mode = 1
[node name="Slot5" type="Button" parent="MainMargin/MainHBox/RightCol/ItemGridCard/Margin/VBox/ItemGrid" unique_id=212812652]
custom_minimum_size = Vector2(88, 88)
layout_mode = 2
size_flags_horizontal = 2
size_flags_vertical = 2
autowrap_mode = 1
[node name="Slot6" type="Button" parent="MainMargin/MainHBox/RightCol/ItemGridCard/Margin/VBox/ItemGrid" unique_id=464526955]
custom_minimum_size = Vector2(88, 88)
layout_mode = 2
size_flags_horizontal = 2
size_flags_vertical = 2
autowrap_mode = 1
[node name="Slot7" type="Button" parent="MainMargin/MainHBox/RightCol/ItemGridCard/Margin/VBox/ItemGrid" unique_id=87751984]
custom_minimum_size = Vector2(88, 88)
layout_mode = 2
size_flags_horizontal = 2
size_flags_vertical = 2
autowrap_mode = 1
[node name="Slot8" type="Button" parent="MainMargin/MainHBox/RightCol/ItemGridCard/Margin/VBox/ItemGrid" unique_id=1866702419]
custom_minimum_size = Vector2(88, 88)
layout_mode = 2
size_flags_horizontal = 2
size_flags_vertical = 2
autowrap_mode = 1
[node name="Pagination" type="HBoxContainer" parent="MainMargin/MainHBox/RightCol/ItemGridCard/Margin/VBox" unique_id=1821323993]
layout_mode = 2