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
+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