During an RMB path-follow the body yaw locks to the leg direction (rig commit_body_to_motion, from interpolated velocity — immune to SetFacing interleaving between throttled steps). The mouse instead drives a layered look-at: LookAtModifier3D pair on Head (±70°) + spine_02 (±30° torso twist for looking far lateral/behind — past their sum the character physically cannot look further without turning). Forward axis measured from the armature rest pose (+Z), not guessed. Influence fades in/out on follow start/end. Pure client presentation per D-249; SetFacing still rides the wire, so the server vision cone follows the mouse while walking — the character looks where the player points. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
58 lines
2.3 KiB
GDScript
58 lines
2.3 KiB
GDScript
class_name SandboxHeadLook
|
|
extends RefCounted
|
|
## T-1088 follow-facing refinement: layered mouse look-at while a path-follow
|
|
## commits the body to the leg direction (locomotion_rig.commit_body_to_motion).
|
|
##
|
|
## Two LookAtModifier3D nodes under the CharacterVisual skeleton aim at the
|
|
## shared HeadAimTarget: spine_02 contributes a small torso twist (looking far
|
|
## lateral/behind), Head does the bulk. Past their combined limits the character
|
|
## physically cannot look further without turning — correct. Pure client
|
|
## presentation on the D-249 model; SetFacing still rides the wire, so the
|
|
## SERVER vision cone follows the mouse while the body walks the path.
|
|
##
|
|
## Bone axes verified from armature.glb rest pose (bone_axes_dump.py): both
|
|
## bones are Y-up with +Z as the facing axis in Godot bone space — forward_axis
|
|
## is set from measurement, not guessed (the E/W-mirror lesson).
|
|
|
|
var _mods: Array[LookAtModifier3D] = []
|
|
var _target_influence: float = 0.0
|
|
|
|
|
|
## Create + configure the modifier pair. Call once after CharacterVisual has
|
|
## built its skeleton (the sandbox never rebuilds the descriptor; a rebuild
|
|
## would free the skeleton and require a re-setup).
|
|
func setup(skeleton: Skeleton3D, aim_target: Node3D) -> void:
|
|
_mods.clear()
|
|
var configs: Array[Dictionary] = [
|
|
{"bone": "spine_02", "limit_deg": SandboxConstants.HEAD_LOOK_TORSO_LIMIT_DEG},
|
|
{"bone": "Head", "limit_deg": SandboxConstants.HEAD_LOOK_HEAD_LIMIT_DEG},
|
|
]
|
|
for cfg in configs:
|
|
var m := LookAtModifier3D.new()
|
|
m.name = "LookAt_%s" % cfg["bone"]
|
|
skeleton.add_child(m)
|
|
m.bone_name = str(cfg["bone"])
|
|
m.target_node = m.get_path_to(aim_target)
|
|
m.forward_axis = SkeletonModifier3D.BONE_AXIS_PLUS_Z
|
|
m.primary_rotation_axis = Vector3.AXIS_Y
|
|
m.use_secondary_rotation = true # slight pitch toward the ground point
|
|
m.use_angle_limitation = true
|
|
m.symmetry_limitation = true
|
|
m.primary_limit_angle = deg_to_rad(float(cfg["limit_deg"]))
|
|
m.influence = 0.0
|
|
_mods.append(m)
|
|
|
|
|
|
## Fade the look-at in while a follow is active, out otherwise.
|
|
func set_active(active: bool) -> void:
|
|
_target_influence = 1.0 if active else 0.0
|
|
|
|
|
|
func update(delta: float) -> void:
|
|
if _mods.is_empty():
|
|
return
|
|
var step := delta / maxf(SandboxConstants.HEAD_LOOK_FADE_S, 0.001)
|
|
for m in _mods:
|
|
if is_instance_valid(m):
|
|
m.influence = move_toward(m.influence, _target_influence, step)
|