diff --git a/client/scripts/rendering/character_visual.gd b/client/scripts/rendering/character_visual.gd index 9e4a4ce18..4627fddcc 100644 --- a/client/scripts/rendering/character_visual.gd +++ b/client/scripts/rendering/character_visual.gd @@ -23,6 +23,7 @@ extends Node3D ## get_clothing_node_count() — number of clothing MeshInstance3D nodes ## get_accessory_node_count() — number of accessory BoneAttachment3D nodes ## get_skin_tone_texture_name(index) — skin tone texture filename key for index +## get_overhead_anchor() — Marker3D above Head bone for floating UI (#712) ## ## D-159 (11 body types), D-160 (18 segments), D-161 (head separate), ## D-162 (clothing pre-fitted), D-163 (heads via BoneAttachment3D), D-164 (skeleton fork) @@ -101,6 +102,8 @@ var _clothing_meshes: Array[MeshInstance3D] = [] var _bone_attachments: Array[BoneAttachment3D] = [] var _outline_nodes: Array[MeshInstance3D] = [] var _accessory_attachments: Array[BoneAttachment3D] = [] +var _overhead_anchor: Marker3D = null +var _overhead_attachment: BoneAttachment3D = null # Inspectable state for tests var _active_torso_variant: String = "full" @@ -218,6 +221,13 @@ func get_skin_tone_texture_name(index: int) -> String: return SKIN_TONES[clampi(index, 0, SKIN_TONES.size() - 1)]["tex"] +## Return the overhead anchor Marker3D (#712). Null if skeleton not loaded. +## Anchor point for floating UI elements: status indicators, thought bubbles, +## alert markers, speech icons. Positioned ~0.3m above the Head bone. +func get_overhead_anchor() -> Marker3D: + return _overhead_anchor + + # ============================================================================= # Internal — teardown # ============================================================================= @@ -234,6 +244,13 @@ func _clear() -> void: node.free() _outline_nodes.clear() + # #712: free overhead anchor before general bone attachments + if is_instance_valid(_overhead_attachment) and _overhead_attachment.get_parent(): + _overhead_attachment.get_parent().remove_child(_overhead_attachment) + _overhead_attachment.free() + _overhead_attachment = null + _overhead_anchor = null + for att in _bone_attachments: if is_instance_valid(att) and att.get_parent(): att.get_parent().remove_child(att) @@ -291,6 +308,7 @@ func _load_skeleton() -> void: for m in armature_meshes: print(" armature mesh: ", m.name, " visible=", m.visible) _validate_slot_bones() + _create_overhead_anchor() ## Warn on any SLOT_TO_BONE entry that doesn't exist in the loaded skeleton. @@ -302,6 +320,27 @@ func _validate_slot_bones() -> void: push_warning("CharacterVisual: SLOT_TO_BONE['%s'] = '%s' — bone not found in skeleton" % [slot, bone_name]) +## #712: Create a Marker3D anchored ~0.3m above the Head bone via BoneAttachment3D. +## Anchor point for floating UI elements (status indicators, thought bubbles, etc.). +func _create_overhead_anchor() -> void: + if _skeleton == null: + return + var bone_idx := _skeleton.find_bone("Head") + if bone_idx == -1: + push_warning("CharacterVisual: Head bone not found — overhead anchor not created") + return + _overhead_attachment = BoneAttachment3D.new() + _overhead_attachment.bone_name = "Head" + _overhead_attachment.bone_idx = bone_idx + _overhead_attachment.name = "OverheadAttachment" + _skeleton.add_child(_overhead_attachment) + + _overhead_anchor = Marker3D.new() + _overhead_anchor.name = "OverheadAnchor" + _overhead_anchor.position = Vector3(0, 0.3, 0) + _overhead_attachment.add_child(_overhead_anchor) + + # ============================================================================= # Internal — body segments (D-160) # =============================================================================