# First-person controller — TODO

Working notes. Things we've identified as worth doing but haven't pulled
the trigger on yet. Companion to DESIGN.md (base) and DESIGN_EXTENSIONS.md
(abilities + mastery roadmap).

## Mastery evaluators — additional

We ship StrideTimingJump, FootAsymmetryTurn, BreathRhythm, and
SlideInitiationTiming today. The remaining ones from the
DESIGN_EXTENSIONS Phase 10 list, ordered by what would give the most
texture per unit of work:

### WallRunEntryTimingEvaluator
Decision point: `WallRunGravity` (currently un-wired — Wall-run reads
`cfg.wallRun.gravityFactor` straight, no mastery hook). Wire it.

Input: angle-of-attack against the wall at activation. Lower angle of
attack (entering near-parallel) is the canonical "good" entry — converts
forward momentum into the run smoothly. High angle of attack (entering
near-perpendicular) is "bad" — you're crashing into the wall.

Curve shape: peak at small angle (1.0 → 0.8, less effective gravity =
more hang time); penalty at high angle (1.0 → 1.15, gravity ramped =
shorter run). Symmetric in behaviour, asymmetric in magnitude.

Estimated effort: 0.5 day. Capture entry angle in WallRun.onActivate,
stash it on the evaluator's per-instance state OR on a new
`runtime.wallRunEntryAngle` field. Curve + spec.

### WallRunTakeoffTimingEvaluator
Decision point: `WallJumpImpulse` (already wired — base WallJump runs
mastery on it).

Input: how late in the wall-run the wall-jump is fired. Late in the
run (near the timer expiry) is the "explosive last-second push-off"
— bonus. Right at activation is "panicked early jump" — penalty.

Curve shape: time-based, normalized to [0, 1] over wall-run duration.
Peak at 0.6–0.9 (committed but not desperate). Trough near 0.0
(panicked).

Estimated effort: 0.5 day. WallJump needs to read how long WallRun was
active when it preempted. Simplest: track `runtime.wallRunElapsedAt
Takeoff` in WallRun.onDeactivate (or via a signal).

### WallJumpEntryAngleEvaluator
Decision point: `WallJumpImpulse`.

Input: angle between the player's velocity vector and the wall normal
at the moment of jump press. A near-perpendicular push-off (velocity
along normal at impact) converts the most into the impulse — bonus.
A near-tangent push (sliding along) is "weak push-off" — penalty.

Curve shape: peak at dot(velocity, normal) approaching 1.0 (head-on);
penalty near 0 (parallel skim).

Composes multiplicatively with the takeoff-timing evaluator above —
both fire on `WallJumpImpulse`. That's the point of the multiplicative
composition rule.

Estimated effort: 0.5 day.

### LedgeCatchTimingEvaluator
Decision point: NEW — would need `LedgeCatchExertionScale` or similar.
Currently LedgeGrab uses a flat `cfg.ledgeGrab.exertionRiseRate`.

Input: |velocityY| at the moment of catch. A gentle catch (low vy) is
forgiving — low exertion rise (longer hang). A desperate save (high
|vy|) is harder — higher exertion rise (shorter hang).

Curve shape: identity 1.0 at low |vy| (≤ 2 m/s), ramping up to ~1.5×
at high |vy| (≥ 8 m/s).

Estimated effort: 0.5 day, plus adding the new decision point.

### MantleExitSpeedEvaluator
Decision point: `MantleExitSpeed` (currently un-wired).

Input: stride-phase at the moment of mantle completion. Landing on a
midstance push-off bonus the exit velocity (player runs off the mantle
into the next surface); landing on footfall takes a small penalty.

Currently Mantle's exit doesn't set any forward velocity — the player
lands at rest. To make this evaluator meaningful, we'd also need
Mantle to apply a small forward push on completion, which the
evaluator then scales. Two-step change.

Estimated effort: 1 day (more wiring than the others).

## Collision (KinematicMover) follow-ups

The legacy `_moveAndSlide` + scalar-resolver regime was replaced by the
`collision/KinematicMover` rebuild (Phases 1–4; see DESIGN_COLLISION.md).
Slide-along-walls (axis + oblique), vertical anti-tunnel (floors and
ceilings), depenetration, slopes, stick-to-ground, and stairs all landed
there.

### Route WallRun through the mover ✅ done
WallRun used to self-integrate its reduced-gravity model (`position._add`
+ a hand-rolled groundResolver catch), so it got no sweep-and-slide and
could drift into / off geometry. It now applies its reduced gravity and
routes the move through the shared `_resolveMotion` (the mover when
physics is present, else the flat-ground fallback) — same motor as base
locomotion and Slide. So **every** motion path goes through one collision
seam now: it can't drift into a wall (the sweep clips it) nor sail off a
ledge / through a floor the mover would catch. Guard: `WallRun.spec.js`
"physics: a wall-runner slides along a real wall and is caught by the
mover's real floor". No remaining collision follow-ups.

## Other open notes

### Horizontal-capsule Prone collider
The posture → collider sync uses a vertical capsule for Prone, sized
to `cfg.body.proneHeight`. When that's below `2·radius` (the default)
the capsule degenerates to a sphere of `radius` — Y extent
`2·radius ≈ 0.68 m`. That fits "low-profile shape" honestly but it
doesn't reflect what a sliding body actually looks like: long along
the direction of travel, narrow in the cross-section. A horizontal
capsule (Y-axis cylinder rotated 90° around X via TransformedShape3D
matrix, oriented along body-local +Z) would let the player fit
under 0.4-m ledges while still occupying the full body length
horizontally. Tracked because the Prone test currently asserts the
vertical-capsule extent — see PostureCollider.spec.js geometry note.

### Refactor `pose.crouchAmount` away
Now redundant with `pose.postureAmount`. Leave for one or two builds so
any external consumers can migrate, then delete.

### Stride-mastery score telemetry
`MasterySet` records EMA scores per decision point. We never surface
these to the HUD or any feedback system. A simple debug overlay
showing the running scores (a sparkline per decision point?) would be
helpful when tuning curves.

### Tune the slide bob fade-out
`bobIntensityHalfLife` controls how fast the lateral bob fades when
posture changes to Prone. May be too slow — feels like a brief wobble
before the slide settles into "pure motion". Worth play-test tuning.

### Posture-aware footstep audio
Surface tag is on `state.surfaceTag`. The footstep signal carries
side + speed + surfaceTag, but emission is gated on
`feetStriking` (Stand or Crouch posture). When other postures want
audio cues (a slide-along-pavement sound, a hang-grip creak), they
need their own signal channels — not piggyback on `onFootStep`.

### WallRun direction sense (push-back behaviour)
Currently WallRun projects velocity onto the wall tangent each tick.
If the player turns AWAY from the wall mid-run, the wall-tangent
projection still keeps them pinned. Might want a "lean-away exits the
run" condition: if `intent.move` rotates more than X degrees from the
wall tangent, exit the wall-run.

### Animation track for Hang posture
The eye stays at body height while hanging (we don't drop the eye
height in Hang because the body root is already below the ledge). But
the rig should probably show arms reaching up. That's a skeleton
concern, not a controller one — just flagging that `pose.posture =
Hang` is the signal the rig should branch on.
