fix(client): address PR #95 review — 8 items
SLOT_TO_BONE expanded to 12 slots (earring_l/r, necklace, wrist_l/r). _clear() outline teardown ordering fixed. toon_masked.gdshader: removed cull_disabled, added ALPHA output. _attach_to_bone() returns node instead of bool. skin_tone clamped in from_dict(). D-160 segment count 15→17. Blender scripts: docstrings + __main__ guards. Reload test tightened. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -54,13 +54,19 @@ const HIDDEN_BY_DEFAULT: Array[String] = ["torso_upper"]
|
||||
## Accessory slot → Quaternius bone name (Section 3 of character-asset-organization.md).
|
||||
## Bone names verified against armature.glb — Quaternius rig uses "Head" (capital H),
|
||||
## lowercase for everything else. NOT Mixamo names (no "Hips", "LeftHand", "Spine2").
|
||||
## 12 slots total: head-attached (hat/goggles/mask/earring_l/earring_r/necklace),
|
||||
## spine-attached (backpack/belt), wrist-attached, hand-held.
|
||||
const SLOT_TO_BONE: Dictionary = {
|
||||
"hat": "Head",
|
||||
"goggles": "Head",
|
||||
"mask": "Head",
|
||||
"earring": "Head",
|
||||
"earring_l": "Head",
|
||||
"earring_r": "Head",
|
||||
"necklace": "Head",
|
||||
"backpack": "spine_03",
|
||||
"belt": "pelvis",
|
||||
"wrist_l": "lowerarm_l",
|
||||
"wrist_r": "lowerarm_r",
|
||||
"held_l": "hand_l",
|
||||
"held_r": "hand_r",
|
||||
}
|
||||
@@ -204,6 +210,17 @@ func get_skin_tone_texture_name(index: int) -> String:
|
||||
# =============================================================================
|
||||
|
||||
func _clear() -> void:
|
||||
# Outline nodes are duplicates of body/clothing meshes — must be freed FIRST,
|
||||
# before the source meshes are removed. _rebuild_outlines() is not called here
|
||||
# because we are tearing down, not rebuilding; the rebuild happens at the end
|
||||
# of load_descriptor() once the new character is assembled.
|
||||
for node in _outline_nodes:
|
||||
if is_instance_valid(node):
|
||||
if node.get_parent():
|
||||
node.get_parent().remove_child(node)
|
||||
node.free()
|
||||
_outline_nodes.clear()
|
||||
|
||||
for att in _bone_attachments:
|
||||
if is_instance_valid(att) and att.get_parent():
|
||||
att.get_parent().remove_child(att)
|
||||
@@ -222,8 +239,6 @@ func _clear() -> void:
|
||||
_active_coverages.clear()
|
||||
_active_torso_variant = "full"
|
||||
|
||||
_rebuild_outlines() # clears outline nodes before removing skeleton
|
||||
|
||||
if is_instance_valid(_body_root) and _body_root.get_parent():
|
||||
_body_root.get_parent().remove_child(_body_root)
|
||||
_body_root.queue_free()
|
||||
@@ -324,7 +339,7 @@ func _load_head(desc: CharacterVisualDescriptor) -> void:
|
||||
if desc.head_id.is_empty():
|
||||
return
|
||||
var path := BASE_PATH + "heads/templates/%s.glb" % desc.head_id
|
||||
if _attach_to_bone(path, "Head"):
|
||||
if _attach_to_bone(path, "Head") != null:
|
||||
_loaded_slots.append("head")
|
||||
|
||||
|
||||
@@ -332,7 +347,7 @@ func _load_hair(desc: CharacterVisualDescriptor) -> void:
|
||||
if desc.hair_id.is_empty() or desc.hair_id == "bald":
|
||||
return
|
||||
var path := BASE_PATH + "hair/%s.glb" % desc.hair_id
|
||||
if _attach_to_bone(path, "Head", desc.hair_tint):
|
||||
if _attach_to_bone(path, "Head", desc.hair_tint) != null:
|
||||
_loaded_slots.append("hair")
|
||||
|
||||
|
||||
@@ -340,7 +355,7 @@ func _load_facial_hair(desc: CharacterVisualDescriptor) -> void:
|
||||
if desc.facial_hair_id.is_empty():
|
||||
return
|
||||
var path := BASE_PATH + "facial_hair/%s.glb" % desc.facial_hair_id
|
||||
if _attach_to_bone(path, "Head", desc.facial_hair_tint):
|
||||
if _attach_to_bone(path, "Head", desc.facial_hair_tint) != null:
|
||||
_loaded_slots.append("facial_hair")
|
||||
|
||||
|
||||
@@ -350,23 +365,23 @@ func _load_eyebrows(desc: CharacterVisualDescriptor) -> void:
|
||||
# Convention: eyebrow_id uses the short visual name (e.g. "regular", "thick", "female"),
|
||||
# NOT the full filename prefix. Path: eyebrows/{eyebrow_id}.glb → e.g. eyebrows/regular.glb
|
||||
var path := BASE_PATH + "eyebrows/%s.glb" % desc.eyebrow_id
|
||||
if _attach_to_bone(path, "Head", desc.eyebrow_tint):
|
||||
if _attach_to_bone(path, "Head", desc.eyebrow_tint) != null:
|
||||
_loaded_slots.append("eyebrow")
|
||||
|
||||
|
||||
func _attach_to_bone(path: String, bone_name: String, tint: Color = Color.WHITE) -> bool:
|
||||
func _attach_to_bone(path: String, bone_name: String, tint: Color = Color.WHITE) -> BoneAttachment3D:
|
||||
if _skeleton == null:
|
||||
return false
|
||||
return null
|
||||
if not ResourceLoader.exists(path):
|
||||
push_warning("CharacterVisual: asset not found (expected): %s" % path)
|
||||
return false
|
||||
return null
|
||||
var scene := load(path) as PackedScene
|
||||
if scene == null:
|
||||
return false
|
||||
return null
|
||||
var bone_idx := _skeleton.find_bone(bone_name)
|
||||
if bone_idx == -1:
|
||||
push_error("CharacterVisual: bone '%s' not found in skeleton" % bone_name)
|
||||
return false
|
||||
return null
|
||||
var attachment := BoneAttachment3D.new()
|
||||
attachment.bone_name = bone_name
|
||||
attachment.bone_idx = bone_idx
|
||||
@@ -384,7 +399,7 @@ func _attach_to_bone(path: String, bone_name: String, tint: Color = Color.WHITE)
|
||||
attachment.add_child(inst)
|
||||
for mi in _collect_meshes(inst):
|
||||
_apply_tinted_shader(mi, tint, mask_tex)
|
||||
return true
|
||||
return attachment
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -504,9 +519,10 @@ func _load_accessories(desc: CharacterVisualDescriptor) -> void:
|
||||
continue
|
||||
var path := BASE_PATH + "accessories/%s.glb" % item_id
|
||||
var tint: Color = desc.accessory_tints.get(item_id, Color.WHITE)
|
||||
if _attach_to_bone(path, bone_name, tint):
|
||||
var att := _attach_to_bone(path, bone_name, tint)
|
||||
if att != null:
|
||||
# Track accessory attachments separately for get_accessory_node_count()
|
||||
_accessory_attachments.append(_bone_attachments.back())
|
||||
_accessory_attachments.append(att)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
|
||||
@@ -129,7 +129,7 @@ static func from_dict(data: Dictionary) -> CharacterVisualDescriptor:
|
||||
desc.facial_hair_tint = _decode_color(data.get("facial_hair_tint"), Color.WHITE)
|
||||
desc.eyebrow_id = data.get("eyebrow_id", "")
|
||||
desc.eyebrow_tint = _decode_color(data.get("eyebrow_tint"), Color.WHITE)
|
||||
desc.skin_tone = data.get("skin_tone", 0)
|
||||
desc.skin_tone = clampi(data.get("skin_tone", 0), 0, 8)
|
||||
desc.clothing_slots = data.get("clothing_slots", {})
|
||||
desc.clothing_tints = _decode_tint_map(data.get("clothing_tints", {}))
|
||||
desc.accessory_slots = data.get("accessory_slots", {})
|
||||
|
||||
Reference in New Issue
Block a user