feat(client): move-here path preview + RMB gesture vocabulary (T-1088, Q-084)
Hover marker + optimal path line over the KNOWN tile store only — the character plans through what they know; fog is unpathable (info boundary at the planning layer; the follower additionally revalidates every remaining tile per step). Pure static 8-dir A*: uniform cost 1 incl. diagonals (D-248 time-optimal, no sqrt2), no corner-cutting, terrain-cost provider seam for Phase-4 terrain. Execution streams ordinary Move* steps through the existing throttle — zero protocol change, server validates every step. RMB vocabulary (live-session spec): click = walk there at current stance; double-click = sprint there (ToggleStanceUp burst — server toggle handler verified cooldown-free so bursts climb deterministically — with net-zero restore on arrival; a double upgrades the active follow in place); long-press >=400ms = go there then Crouch on arrival, no restore (input- vocabulary prototype; real cover mechanics are future combat design). Cancellation: WASD override (stance kept), invalidation, teleport, suppression. Two additive InputMapper seams (queue_move_step, queue_stance_toggle); mouse unproject shared with the facing provider (ground_hit_local). 44 new gdUnit tests across finder + follower ladders. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -79,20 +79,34 @@ static func compute_facing_angle(
|
||||
rig_local_pos: Vector3,
|
||||
deadzone_m: float
|
||||
) -> float:
|
||||
# Ray -> y=0 ground plane, world space.
|
||||
if absf(ray_dir.y) < _RAY_PARALLEL_EPS:
|
||||
var hit_local: Variant = ground_hit_local(ray_origin, ray_dir, world_root_transform)
|
||||
if hit_local == null:
|
||||
return NAN
|
||||
var t := -ray_origin.y / ray_dir.y
|
||||
if t < 0.0:
|
||||
return NAN
|
||||
var hit_world := ray_origin + ray_dir * t
|
||||
# Undo the 45° map rotation: WorldRoot-local axes are sim axes (SandboxSpace §2:
|
||||
# local +X = sim East, local +Z = sim South).
|
||||
var hit_local := world_root_transform.affine_inverse() * hit_world
|
||||
var delta := hit_local - rig_local_pos
|
||||
var delta: Vector3 = (hit_local as Vector3) - rig_local_pos
|
||||
var planar := Vector2(delta.x, delta.z)
|
||||
if planar.length_squared() < deadzone_m * deadzone_m:
|
||||
return NAN
|
||||
# atan2(z, x) IS the sim convention (0 = East, +PI/2 = South) because local
|
||||
# +Z = sim +y (South, Y-down radians — server vision_cone.rs).
|
||||
return atan2(delta.z, delta.x)
|
||||
|
||||
|
||||
## Pure ground-plane pick (headless-testable): a world-space mouse ray -> its y=0
|
||||
## ground-plane intersection expressed in WorldRoot-LOCAL metres (undoing the 45°
|
||||
## map rotation, so the result is sim-aligned space per SandboxSpace §2). Shared by
|
||||
## the facing provider above and the T-1088 hover-tile picker (path_preview.gd),
|
||||
## which feeds the returned point straight into SandboxSpace.world_to_tile — one
|
||||
## unproject, one place. Returns null (not a Vector3) when the ray is parallel to
|
||||
## the ground or the plane sits behind the ray origin.
|
||||
static func ground_hit_local(
|
||||
ray_origin: Vector3, ray_dir: Vector3, world_root_transform: Transform3D
|
||||
) -> Variant:
|
||||
if absf(ray_dir.y) < _RAY_PARALLEL_EPS:
|
||||
return null
|
||||
var t := -ray_origin.y / ray_dir.y
|
||||
if t < 0.0:
|
||||
return null
|
||||
var hit_world := ray_origin + ray_dir * t
|
||||
# Undo the 45° map rotation: WorldRoot-local axes are sim axes (SandboxSpace §2:
|
||||
# local +X = sim East, local +Z = sim South).
|
||||
return world_root_transform.affine_inverse() * hit_world
|
||||
|
||||
Reference in New Issue
Block a user