Commit Graph

4 Commits

Author SHA1 Message Date
m3ultra
6382e604b8 Lane D: the small person — player, interactions, selftests
player.sim.js  Deterministic core, zero imports: camera-relative movement, the
               state-machine table, wind slow/gust-shove/knockdown. step(dt,t)
               is the whole clock — no Date.now, no Math.random, no rAF — so a
               storm fast-forwards identically in selftest and in the game.
player.js      The view: rig load per the 90sDJsim DEVMANUAL rig rules
               (SkeletonUtils.clone, height-normalise off the MEASURED head
               bone, canonicalised bone namespace), clip retarget, and
               createPlayer() satisfying the Player contract.
interact.js    Hold-E with radial progress + wireYardActions (re-rig 2.5 s,
               turnbuckle trim 1.2 s, carry-one-item), duck-typed so it no-ops
               cleanly until Lane B lands sailRig.repair/trim.
d.test.js      20 asserts in Lane A's harness: 38 pass / 3 skip overall.

Ported from the 2D prototype's shape (game.js:250-252), retuned to m/s: the
slow curve, and shove gated to gusts only and scaling with speed² so gusts have
teeth. Knockdown needs 0.5 s of SUSTAINED overload — deliberately the same rule
as a sail corner letting go, so cloth and people speak one language.

Two decisions worth the review:
- The knockdown pitches the root in code rather than playing the Falling clip's
  root. Shared clips must drop Hips.quaternion (a different-orientation source
  lays the target flat), so a clip physically cannot lie the body down. Doing it
  in code also lets the fall go DOWNWIND of the gust that caused it, which a
  canned clip could never do, and keeps it deterministic.
- Gust magnitude is recovered from a slow EMA of local wind rather than widening
  Lane C's contract: wind.sample() gives the total and gustTelegraph() only fires
  BEFORE a gust, so nothing reports gust size during the hold. The EMA
  self-calibrates to whatever storm JSON Lane C authors.

Verified in a real scene, not only in asserts: head bone 1.715 m at fig scale
0.983, all six clips bound, walk/run at the tuned speeds, the body lies down and
gets back up, hold-E fires exactly once per press.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 21:55:16 +10:00
m3ultra
1049dece24 Add selftest harness with one suite per lane
selftest.html only imports; each lane owns js/tests/<letter>.test.js. Five lanes
sharing one selftest.html would be the single guaranteed merge conflict in the
repo, so the stubs are pre-created with each lane's PLAN3D asserts written into
the header.

No rAF anywhere: it's throttled in a hidden tab, so a rAF-driven selftest would
pass only while you watch it. fixedLoop() drives time instead.

Lane A's suite covers the §5-A acceptance criteria, including "camera never
clips through house" — stated as the underlying invariant (no solid between the
player's head and the camera) so it survives Lane E swapping the house GLB.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 21:33:42 +10:00
m3ultra
a41328a219 Add graybox yard, third-person camera and game shell
The yard's meshes are placeholders; its layout is not. Anchor positions, ground
heights, the garden rect and the sun direction are what the other lanes build
against, so Lane E's GLBs drop into named slots without anyone re-deriving them.

Sun sits in the north (Australian yard), which puts the house's yard-facing wall
in permanent backlight. That's correct, but the fascia line carries three of the
seven anchors, so sky fill is turned up to keep it readable rather than a void.

Camera collision resolves toward the geometry, not away from it: clamping up to
a comfort distance after the raycast is what put the camera 17cm inside the
north wall. Ground is handled by heightAt() instead of a raycast — exact, can't
be tunnelled, and free.

main.js runs a fixed-dt accumulator so sim modules only ever see FIXED_DT. That
is what lets selftest fast-forward a 90s storm and get the numbers the player
got. Nothing auto-runs on import; index.html calls boot().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 21:33:42 +10:00
m3ultra
ae6e444996 Add contracts.js: the shared integration spine
Types, tuning constants ported from the prototype, a seeded PRNG and Emitter,
plus checkContract() — the tripwire that catches a lane drifting from its
interface at merge time instead of three lanes later.

createStubWind() ports the prototype's gust shape (telegraph 1.5s, ramp 0.8s,
hold 1.7s, fade 1.0s) so B and D can develop before Lane C lands weather.js.
Its schedule is precomputed at construction, which keeps sample() pure in t —
the selftest samples out of order and must get the same answer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 21:33:42 +10:00