HardYards/SPRINT2.md
m3ultra 76421a7f86 Add Sprint 2 assembly instructions: wire proven modules into one playable storm
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 22:24:35 +10:00

133 lines
7.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# SPRINT 2 — ASSEMBLY (instructions for Opus 4.8 lanes)
*Sprint 1 verdict: every module is built and proven in isolation — 121/121
selftest asserts green on merged main — but the game is not assembled. main.js
still drives the M0 placeholder capsule and stub wind. Sprint 2 is one thing:
**wire the proven modules into one playable storm.** Read THREADS.md from your
last entry down before starting; the integrator [I] entry lists what changed
under you.*
## Decisions (made — stop waiting on them)
1. **Lanes run on m3ultra.** Lane D's recommendation is adopted: the M1 Ultra
(`johnking@100.91.239.7`) is an asset-build box you SSH to; GLBs get committed;
the game never needs it at runtime. PLAN3D §0 is amended by this line.
2. **Sail-area problem (B's 70192 m² finding): fix the yard, not the physics.**
Lane A: move p1/p2 in to roughly (4.5, 5.5) and (4.0, 6.0), add a third post
p3 near (0, 7), and register the trees' `branch_anchor_*` empties (E shipped
them with `rating_hint`) as anchors. Target: at least three pickable quads in
the 1845 m² range that can shade the garden bed, verified by a new a.test
assert that enumerates quad areas. The huge quads stay possible — the load
bars teaching "you cannot span the whole yard" is design working as intended.
3. **Flat-horizontal loophole: Lane C closes it with vertical gust structure.**
Real gusts aren't horizontal; add a per-gust vertical component (downdraft
fraction in storm JSON, default ~0.25 of gust power, validated) so a
horizontal plate carries real load. Lane B adds the assert: over 8 directions
in storm_02 wind, flat-horizontal peak load ≥ 60% of flat-pitched peak (i.e.
no longer a free lunch). Ponding stays out of scope (M4 water spike).
4. **repair/trim seam: Lane B conforms to Lane D's call sites** (D landed first,
duck-typed): add `repair(i)` (→ repairCorner with the spare's hw),
`trim(i, delta)` (→ trimCorner) and `cornerPos(i) -> Vector3` (live world
position, fresh vector) to the rig object. Contract entries + b.test asserts.
5. **debris↔sail seam: option (b)** — Lane B reads `debris.pieces`
({x,y,z,vx,vy,vz,r,mass}) inside `sail.step()` and applies impulses; momentum
bookkeeping stays in the one integrator. Lane C freezes the `pieces` shape.
6. **House GLB: no re-cut.** Lane A reads `fascia_anchor_*` positions out of
`house_yardside_v1.glb` at load and places anchors there (data wins over the
yard constants). E's 2.80 m fascia replaces the 2.6 m graybox number.
## Lane A — assemble the game (this is the sprint)
main.js boot(), in order; keep each step behind a small commit:
1. Stub wind → `createWind(storm)``storm_01_gentle` for prep/forecast calm,
`storm_02_wildnight` when the storm phase starts. Call
`wind.setSheltersFromTrees(...)` after the yard builds, per C's ask.
2. Placeholder → `await createPlayer(scene, world, cameraRig, {wind, interact})`
(async boot; D says same first three args). Delete the placeholder factory.
3. Sail: `const view = await createSailView(rig); scene.add(view); view.update()`
per frame after `rig.step(dt, wind, t)`. B's THREADS entry has the exact shape.
4. `createSkyFx({scene, camera, wind, sun, hemi})` + `unlockAudio()` on first
input; `createDebris({heightAt: world.heightAt, onHitPlayer: player.knockdown})`,
`debris.setModels()` from `models/debris/` (glob the dir).
5. Yard dressing: swap graybox house → `house_yardside_v1.glb` (decision 6),
shed + shed_table (D's spare pickup), sail posts as `sail_post_v1.glb` rotated
about `rake_pivot`, fence set, grass billboards off `textures/grass_atlas.png`.
6. Anchor rework per decision 2.
7. HUD: loads in **kN** (B's units note), per-corner bars vs rating, wind meter +
gust telegraph banner, garden % (wire `rig.coverageOver(world.gardenBed)`
HP → E's `plants_full/tattered/dead` visibility swap), phase banner, forecast
card (storm JSON summary: peak wind, change time), aftermath screen (garden %,
corners lost, budget delta).
8. Phase machine: forecast (show card, Enter) → prep (rigging UI live, budget
$80, optional timer OFF this sprint) → storm (90 s, storm_02) → aftermath.
Acceptance: `python3 server.py` → rig a sail with the mouse, press Enter, watch
storm_02 try to kill it, repair a corner mid-storm, see the aftermath screen.
60 fps during the storm on this box. Selftest stays green after every merge.
## Lane B — sail in the world
1. Decision 4 API (repair/trim/cornerPos) + asserts.
2. Decision 5: consume `debris.pieces` in step(); assert momentum is conserved
within tolerance on a crate-through-sail scenario.
3. Prep-phase picking adapter: RiggingSession → clicks. Raycast against Lane A's
anchor markers (A exposes `world.anchorMarkers` if you need meshes — agree in
THREADS), corner cycling + tension dial + spare purchase, HUD summary line
from `summary()`. This unblocks A step 8.
4. Joint tuning session with C (their THREADS ask): retune cloth ρ against real
m/s storms; then re-run the §7 gate against REAL wind (current assert used the
stub) — flat cheap rig cascades in storm_02, twisted mixed rig + one repair
survives. Log tuned constants in THREADS.
5. Small fix: `coverageOver()` rays start at `heightAt(x,z)`, not y=0 (your nit).
6. Decision 3 assert (flat-horizontal no longer dominant) once C lands vertical gusts.
## Lane C — weather in the game
1. Decision 3: vertical gust component in storm JSON + validator + asserts.
2. Decision 5: freeze and document `debris.pieces` shape in contracts.js.
3. Rain must react to the sail: cheap occlusion — sample `rig.coverageOver` cells
or raycast a handful of drops so the garden visibly stays dry under cloth.
(Coordinate the API with B; don't ray-test every drop.)
4. Storm tuning session with B (see B-4).
5. skyfx: verify light restoration on `dispose()` inside the real main.js scene —
A will call you if teardown flickers phase transitions.
6. Consider retiring weather_demo.html once main.js hosts the storm — your call,
it stops earning its place when the game IS the bench.
## Lane D — player in the storm
1. After A's step 2 swap: verify controls + camera feel in the real yard (slopes,
fence collision against `world.solids`), tune walk/run speeds against yard
scale, make gust shove + knockdown fire from real wind and real debris hits.
2. Wire the spare pickup loop end-to-end: shed_table `pickup_anchor` → carrying →
`repair(i)` consumes it (B's decision-4 API). The §7 "one mid-storm repair"
scenario must be playable by hand.
3. Interaction prompts follow `cornerPos(i)` live (flogging corner = moving prompt).
4. Kill dev_player.html only when the main game covers everything it proves.
## Lane E — polish targets (small sprint)
1. Wind-reactive foliage handles: verify canopy nodes sway cleanly when A drives
them; add `sway_hint` custom props if A needs per-tree tuning.
2. Sail cloth texture (512² weave/UV-stripe atlas) so the membrane reads as
fabric, + a simple tear decal strip for M3.
3. Storm dressing set: wheelie bin (debris, `mass_hint`), washing line, garden
gnome (collateral scoring bait). One script run, same acceptance as before.
4. Contact-sheet the assembled yard from A's camera for DESIGN.md.
## Order & gates
```
gate 1: A steps 1-4 land → the yard has weather, a person and a live sail
gate 2: B-3 picking + A-8 → the loop is playable start to finish
gate 3: B/C tuning + D-2 → §7 scenario passes BY HAND and in selftest
```
Definition of done = gate 3 + THREADS updated + selftest green + a 90 s
storm_02 run captured (screenshots or video) for the log.
## For John (human-only items)
- Mixamo login on the M1 for D's M3 wishlist (`tools/character/mixamo_wishlist.txt`
— Climbing Ladder, Turning Key, Digging). Not blocking this sprint.
- Two dev servers float around: another session holds :8801; this repo's
launch.json now uses :8809. Kill either freely; `server.py --port N` works.