feat(client): follow-facing — body commits to path, mouse drives layered head/torso look-at (T-1088)
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>
This commit is contained in:
@@ -41,6 +41,11 @@ var _aim_provider: SandboxMouseAimProvider = null
|
||||
## per frame). Installed in _ready(); the RMB gesture handling is in _unhandled_input.
|
||||
var _path_preview: PathPreview = null
|
||||
var _path_follower: PathFollower = null
|
||||
## T-1088 follow-facing: layered head/torso look-at (active during follows) and
|
||||
## its shared aim-target node (WorldRoot-local, positioned at the mouse ground
|
||||
## point each frame).
|
||||
var _head_look: SandboxHeadLook = null
|
||||
var _head_aim_target: Node3D = null
|
||||
## Time (ms) a non-double RMB press began, -1 = none pending. Resolves click vs
|
||||
## long-press on RELEASE (RMB_LONG_PRESS_MS threshold).
|
||||
var _rmb_press_msec: int = -1
|
||||
@@ -97,6 +102,15 @@ func _ready() -> void:
|
||||
_anim = LocomotionAnim.new()
|
||||
_anim.setup(player_rig, character_visual)
|
||||
|
||||
# (3d) Follow-facing look-at (T-1088): aim target under WorldRoot (its local
|
||||
# space IS the sim-aligned space ground_hit_local returns) + the modifier
|
||||
# pair on the freshly-built skeleton.
|
||||
_head_aim_target = Node3D.new()
|
||||
_head_aim_target.name = "HeadAimTarget"
|
||||
world_root.add_child(_head_aim_target)
|
||||
_head_look = SandboxHeadLook.new()
|
||||
_head_look.setup(character_visual.get_skeleton(), _head_aim_target)
|
||||
|
||||
# (4) InputMapper facing provider (design §9).
|
||||
_install_facing_provider()
|
||||
|
||||
@@ -390,6 +404,32 @@ func _per_frame_update(delta: float) -> void:
|
||||
_path_preview.update(char_tile, is_floor, greybox.store.size())
|
||||
_path_follower.tick(char_tile, is_floor)
|
||||
|
||||
# T-1088 follow-facing: while a follow is active the body commits to the leg
|
||||
# direction and the mouse drives the layered head/torso look-at instead.
|
||||
# SetFacing still rides the wire (the server vision cone follows the mouse).
|
||||
var follow_active := _path_follower.is_active()
|
||||
player_rig.commit_body_to_motion = follow_active
|
||||
_head_look.set_active(follow_active and not _input_suppressed())
|
||||
_update_head_aim_target()
|
||||
_head_look.update(delta)
|
||||
|
||||
|
||||
## Position the shared look-at target at the mouse's ground point (WorldRoot-
|
||||
## local — the same sim-aligned space the aim provider returns) at eye height.
|
||||
func _update_head_aim_target() -> void:
|
||||
var cam := get_viewport().get_camera_3d()
|
||||
if cam == null or _head_aim_target == null:
|
||||
return
|
||||
var mouse := get_viewport().get_mouse_position()
|
||||
var hit: Variant = SandboxMouseAimProvider.ground_hit_local(
|
||||
cam.project_ray_origin(mouse), cam.project_ray_normal(mouse),
|
||||
world_root.global_transform
|
||||
)
|
||||
if hit != null:
|
||||
var p: Vector3 = hit
|
||||
p.y = SandboxConstants.HEAD_LOOK_EYE_HEIGHT_M
|
||||
_head_aim_target.position = p
|
||||
|
||||
|
||||
## Hard camera-pivot snap (design §7) — first-snapshot latch + teleport fan-out.
|
||||
func _snap_camera_to_player() -> void:
|
||||
|
||||
Reference in New Issue
Block a user