diff --git a/.pql/changelog/ticket_idmap/2026-07.sql b/.pql/changelog/ticket_idmap/2026-07.sql index deae50dc6..e51ee6339 100644 --- a/.pql/changelog/ticket_idmap/2026-07.sql +++ b/.pql/changelog/ticket_idmap/2026-07.sql @@ -6,3 +6,4 @@ INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_ INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FKE0EW2617K9VE4QHSYSR9HW', 'T-1090', '2026-07-06 10:38:16.595', '2026-07-06 10:38:16.595', NULL, '38d331606c3d8de414dec0c2c971ad71', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FKE0J4B8GQ17NFFV8N7D45BW', 'T-1091', '2026-07-06 10:38:43.290', '2026-07-06 10:38:43.290', NULL, 'da0796307c06acd6609ac466d6ea48c3', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FKH8SJRMXTP6XQNBXHRD4CQ0', 'T-1092', '2026-07-06 18:14:07.303', '2026-07-06 18:14:07.303', NULL, 'e34fe437eac3bdbd6a73f8d98bc871a9', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); +INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FKHFRCP5R0VBA9BKR97RRBJ0', 'T-1093', '2026-07-06 18:44:32.562', '2026-07-06 18:44:32.562', NULL, 'ce581695739eea8cd11433cb9f6c86d1', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); diff --git a/.pql/changelog/tickets/2026-07.sql b/.pql/changelog/tickets/2026-07.sql index e62bddb60..9c1e9f7cf 100644 --- a/.pql/changelog/tickets/2026-07.sql +++ b/.pql/changelog/tickets/2026-07.sql @@ -708,3 +708,4 @@ Live-session feedback (user, 2026-07-06): (1) Corridor cutaway verdict — two m RMB gesture vocabulary (user, live session 2026-07-06, added to the path-preview feature): single right-click = walk there (current stance); DOUBLE right-click = sprint there (ladder ToggleStanceUp burst to Sprint via the ordinary command queue, pre-sprint stance RESTORED on arrival; a double upgrades the already-started follow in place); LONG-PRESS right-click (~0.4s, commit on release) = ''take cover at location'', prototyped as go-there-then-Crouch-on-arrival (no restore — crouching was the point). Real cover mechanics (wall adjacency, directional protection) are future combat design; the gesture is the input-vocabulary prototype. WASD always cancels any follow without stance restore (player override is deliberate). Follow-facing refinement (user, live session 2026-07-06): during an RMB path-follow the BODY commits to the leg/movement direction (mouse no longer swings body yaw); the mouse instead drives a LAYERED LOOK-AT — Head bone (~70deg limit) plus torso twist via a spine bone (~30deg) for looking far lateral/behind; beyond their sum the character physically cannot look further without turning (correct). Pure client presentation on the D-249 model; SetFacing continues to ride the wire during follows, so the SERVER vision cone still follows the mouse (diegetic: the character looks where the player points while walking a committed path). Implementation: Godot LookAtModifier3D pair under the CharacterVisual skeleton, influence-faded in/out on follow start/end; rig gains a commit_body_to_motion flag (yaw from leg velocity while set).', 'backlog', 'high', NULL, 'client', NULL, '2026-07-06 07:01:06.475', '2026-07-06 18:29:55.012', NULL, 'c45dd9ede3dbadcc20b4ce12b90a94f5', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FKHFRCP5R0VBA9BKR97RRBJ0', 'task', '06FB0TNSRXX29H5X0EB0F0GBW4', 'Server aim-lock: explicit SetFacing should survive accepted moves (vision-cone flap during path-follows)', 'Found live in the T-1088 sandbox: during a client path-follow the vision cone flaps to path-forward on every accepted step because movement.rs facing_from_delta overwrites Facing on each move, and D-054''s change-gated SetFacing never re-asserts an unchanged octant. Client mitigation shipped (InputMapper.reassert_facing ~2 ticks after each emitted step) shrinks the flap to ~100ms per step but cannot eliminate it — movement wins within a tick. Proper fix is server-side facing decoupling (e.g. an explicit-aim flag/timestamp on Facing that accepted moves do not overwrite while fresh), which is exactly Q-084''s parked walk-vs-aim split now with play evidence. Needs a D-record before wire semantics change; touch nothing until the sidequest''s presentation-layer constraint lifts.', 'backlog', 'medium', NULL, 'server', NULL, '2026-07-06 18:44:32.561', '2026-07-06 18:44:32.561', NULL, 'c83a2bdbf3c41b678f04db6e5077b78a', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); diff --git a/client/scripts/autoloads/input_mapper.gd b/client/scripts/autoloads/input_mapper.gd index 47d92bc86..3a0fedfc0 100644 --- a/client/scripts/autoloads/input_mapper.gd +++ b/client/scripts/autoloads/input_mapper.gd @@ -208,6 +208,26 @@ func queue_move_step(tile_delta: Vector2i) -> bool: # so no throttle — the server's handle_toggle_stance has no cooldown, so the sandbox # follower can burst the exact ladder distance and trust it (server validates). No-op # while input is suppressed (dialogue / free camera), matching movement. +## T-1088 follow-facing: unconditionally re-send the CURRENT facing octant. +## The server overwrites Facing from the move delta on every accepted step +## (movement.rs facing_from_delta), and the change-gated send above never +## re-asserts an unchanged octant — during a path-follow the vision cone would +## stick path-forward until the mouse crossed an octant boundary. The follower +## calls this ~2 server ticks after each emitted step (same-tick re-asserts +## lose: movement wins within a tick). Suppression-guarded like all sends. +func reassert_facing() -> void: + if GameState.dialogue_active or GameState.free_camera_mode: + return + _last_sent_octant = facing_octant + input_queue.append( + { + "action": Action.SET_FACING, + "timestamp_msec": Time.get_ticks_msec(), + "action_data": {"facing": facing_octant}, + } + ) + + func queue_stance_toggle(up: bool) -> void: if GameState.dialogue_active or GameState.free_camera_mode: return diff --git a/client/scripts/sandbox/path_follower.gd b/client/scripts/sandbox/path_follower.gd index 8a4c9001a..aa8b0663d 100644 --- a/client/scripts/sandbox/path_follower.gd +++ b/client/scripts/sandbox/path_follower.gd @@ -48,7 +48,14 @@ enum Mode { NORMAL, SPRINT, CROUCH } ## last element. Empty while inactive. var _path: Array[Vector3i] = [] var _goal: Vector3i = Vector3i.ZERO +## T-1088 follow-facing: two server ticks (50 ms each) after an emitted step — +## past the tick that applies the move (whose facing_from_delta would win) but +## as soon after as the wire allows. +const FACING_REASSERT_DELAY_MS := 100 + var _active: bool = false +## 0 = nothing pending; else the msec deadline for the post-step facing re-assert. +var _reassert_at_ms: int = 0 var _mode: int = Mode.NORMAL ## SPRINT only: the pre-sprint stance (kept for readability/debug) and the exact @@ -120,6 +127,13 @@ func cancel() -> void: func tick(current_tile: Vector3i, is_floor: Callable) -> void: if not _active: return + # T-1088 follow-facing: the server overwrote Facing from this follow's last + # accepted step — re-assert the mouse octant once the overwrite tick has + # passed (~2 server ticks; a same-tick re-assert loses, movement wins within + # a tick). Keeps the vision cone on the mouse while the body walks the path. + if _reassert_at_ms > 0 and Time.get_ticks_msec() >= _reassert_at_ms: + _reassert_at_ms = 0 + InputMapper.reassert_facing() # Cancel (no stance change): input suppression — dialogue / free camera. if GameState.dialogue_active or GameState.free_camera_mode: cancel() @@ -147,7 +161,8 @@ func tick(current_tile: Vector3i, is_floor: Callable) -> void: # the 200 ms cadence automatically — the character accelerates into the sprint). var next_tile: Vector3i = _path[idx + 1] var delta := Vector2i(next_tile.x - current_tile.x, next_tile.y - current_tile.y) - InputMapper.queue_move_step(delta) + if InputMapper.queue_move_step(delta): + _reassert_at_ms = Time.get_ticks_msec() + FACING_REASSERT_DELAY_MS # ---------------------------------------------------------------------------