Commit graph

4 commits

Author SHA1 Message Date
Erik
b7e954e50b fix(camera): retail-faithful jump-tracking via contact-plane projection
Original symptom: jumping made the camera swing around the player
vertically — the basis tilted up/down with the player's Z velocity.

Root cause: ComputeHeading used the raw 3D velocity vector as the
heading direction. During a jump, velocity has a substantial Z
component (vy ≈ jump speed), and `normalize((vx, vy, vz))` produced
a heading pointing up. The basis tilted accordingly and the camera
went under/over the player.

Retail's actual ALIGN_WITH_PLANE algorithm (decomp at
acclient_2013_pseudo_c.txt:95644-95795) is different:

  1. Velocity is only used as a gate. If |vx| AND |vy| > epsilon
     (player is moving in XY), proceed; otherwise fall back to the
     LOOK_IN_DIRECTION path (player's facing direction unchanged).
  2. The base heading is `localtoglobalvec(player, (0, 1, 0))` —
     the player's local +Y axis in world space, which in our
     convention is `(cos yaw, sin yaw, 0)`.
  3. Pick a surface normal:
       grounded:  contact_plane.N
       airborne:  (0, 0, 1)  [world up]
  4. Project the base heading onto the plane perpendicular to that
     normal:  projected = forward - normal * dot(forward, normal).
  5. Normalize. Fall back to the base if projection collapses.

Behaviorally:
  * Standing jump (vx≈0, vy≈0):  gate fails → base heading. Camera
    doesn't move with the jump.
  * Running jump (vx, vy, vz all nonzero, airborne):  projects onto
    world up → no-op since base is already horizontal. Camera basis
    stays horizontal; player visibly rises in frame.
  * Walking uphill (grounded, slope normal tilted):  projection
    adds a Z component matching the slope angle. Camera basis tilts
    with the terrain.
  * Walking on flat ground:  projection is a no-op. Camera basis
    horizontal.

Surface changes:
  * RetailChaseCamera.ComputeHeading gains `isOnGround` and
    `contactPlaneNormal` parameters.
  * RetailChaseCamera.Update gains the same two parameters and
    threads them through.
  * GameWindow's two Update call sites pass `result.IsOnGround` and
    `_playerController.ContactPlane.Normal` (already exposed on
    PlayerMovementController — no plumbing change there).
  * Tests: 2 existing heading tests reshaped (Moving* and Uphill);
    2 new tests added (AirborneJumping straight-up + running-jump);
    1 renamed (SlopeAlignDisabled). Net 25 → 27 tests in
    RetailChaseCameraTests; full AcDream.App.Tests: 39 → 41.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 09:32:50 +02:00
Erik
e5a5916679 feat(camera): CameraController carries both legacy + retail chase cams
EnterChaseMode now takes (ChaseCamera, RetailChaseCamera); Active
consults CameraDiagnostics.UseRetailChaseCamera to pick which to
expose. Flag flip at runtime swaps cameras instantly (both are kept
warm). GameWindow's two EnterChaseMode call sites get a temporary
stub RetailChaseCamera; Task 7 wires proper construction +
per-frame updates.

Also folds in two minor cleanups from the Task 3 code review:
- Update() discards the unused `right` axis from BuildBasis (no
  caller in the chase-cam math; viewer_offset.X is always 0)
- The three CameraDiagnostics-mutating integration tests now
  save and restore the static state in try/finally to avoid
  ordering-dependent contamination

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 19:56:24 +02:00
Erik
0c1403f2e6 feat(camera): wire RetailChaseCamera Update() + tunables + state
Adds the per-frame Update(playerPos, yaw, velocity, dt) entrypoint
that composes the math primitives into a renderable View matrix +
PlayerTranslucency. State: 5-frame velocity ring, damped eye + forward
unit vector, first-frame snap flag, mouse-filter shared state.
Public surface: Distance/Pitch/YawOffset/PivotHeight tunables,
AdjustDistance/Pitch (with clamps), FilterMouseDelta entry, View +
Position + PlayerTranslucency outputs. 5 new integration tests, all
pass; total RetailChaseCamera test count 25.

Also folds in two minor cleanups from the Task 2 code review:
- AverageVelocity uses ring.Length instead of hardcoded 5
- Basis_NearVerticalHeading test asserts orthogonality of right & up

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 19:44:13 +02:00
Erik
8ebd33dc8f feat(camera): add RetailChaseCamera math primitives
Seven pure-math helpers in the new RetailChaseCamera class:
ComputeHeading (slope-align with flat fallback), BuildBasis (heading
→ orthonormal frame, near-vertical fallback), PushVelocity +
AverageVelocity (5-entry FIFO ring), ComputeDampingAlpha (retail's
stiffness*dt*10), FilterMouseAxis (0.25s low-pass), ComputeTranslucency
(linear ramp 0.20..0.45 m). 20 tests, all pass. State machine + Update()
land in the next commit.

Per spec docs/superpowers/specs/2026-05-18-retail-chase-camera-design.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 19:36:24 +02:00