diff --git a/README.md b/README.md index 676706d..ad07813 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ No pip, no npm, no build. Python 3.8+ and a browser. 1. `DESIGN.md` — the design canon (what the game is) 2. `PLAN3D.md` — the build canon (stack, contracts, lane model) + · `docs/MANUAL.md` — the HOW: tool map, asset/site cookbooks, earned gotchas 3. `THREADS.md` — the append-only lane log. **The project's memory.** Read the last few `[I]` integrator entries to know the current state. 4. `SPRINT12.md` — the current sprint (highest-numbered SPRINTn.md wins) diff --git a/docs/MANUAL.md b/docs/MANUAL.md new file mode 100644 index 0000000..3dfab33 --- /dev/null +++ b/docs/MANUAL.md @@ -0,0 +1,150 @@ +# HARD YARDS — dev manual & cookbooks + +*Written at Sprint 13 integration, distilled from THREADS. The canon docs +(DESIGN.md, PLAN3D.md) say what the game IS; this says HOW to work on it. +When this file and a lane's THREADS entry disagree, THREADS wins — fix this +file.* + +## The tool map — what answers what + +| question | tool | notes | +|---|---|---| +| does everything still work? | `web/world/selftest.html` | must be N/0/0; the count is the per-lane sum | +| is this yard rig-able by mouse? | `web/world/dev_rigging.html` | N switches yards; `__laneB.switchSite(i)` | +| what does the sky/storm look like at t=X? | `web/world/dev_skyfx.html` | scrubbable clock; flies debris; warning: it aims you SOUTH | +| player anims/movement | `web/world/dev_player.html` | | +| is this site winnable, at what price? | `tools/site_audit/` (audit.html browser, audit.mjs node) | funnel state printed in the header — check it | +| does rigging save the garden here? | `tools/garden_bench/` + `tools/site_audit/gardenfly.js` | garden outcomes, separation targets, margin rule | +| per-anchor wind exposure over a storm | `tools/storm_envelope/` | the storm-side second harness | +| regenerate all canon GLBs | `blender -b -P tools/blender/build_yard_assets.py` | deterministic; `--only ` for one asset | +| job-sheet/invoice design preview | `tools/jobsheet/index.html?v=...` | E's design-ahead handover pattern | +| deploy to partly.party/hardyards | `sh tools/deploy_hardyards.sh` | ships `web/` ONLY; self-verifies | + +## Adding an asset (a prop, a structure, a thing in the yard) + +**House rules first (PLAN3D §0):** the runtime asset is always `.glb`, committed +in `web/world/models/`. Raw `.blend`/`.fbx` never enter the repo (gitignored). +Two legitimate routes in: + +**Route 1 — the factory (canon assets).** Add a builder to +`tools/blender/build_yard_assets.py`, re-run, commit the GLBs. Nothing is +hand-edited after export; the factory must produce byte-identical output across +runs (this is asserted — three full runs, same hashes). This is Lane E's lane. + +**Route 2 — vendored external (John's hand-built / MeshGod / library assets).** +Precedent exists: `models/debris/*` is copied verbatim from the 3D-STORE +library. Drop the GLB in `web/world/models/`, then wire it (below). If it's a +one-off, say in THREADS where it came from; if it'll be regenerated, it belongs +in the factory instead. + +**Wiring a prop — the gnome pattern (~5 lines + data, one commit).** There is +deliberately no generic `props: []` — every prop is a named top-level key in +the site JSON, wired explicitly, so a site says what it contains. The bike +(Sprint 12) is the worked example — copy its diff: +1. Put `bike_kid_01_v1.glb` in `web/world/models/`. +2. Site JSON gains a key: `"bike": { "x": -7, "z": 9.69, "rotYDeg": 180 }`. +3. `world.js` loader: one entry in the `Promise.all` + a placement block + (position via `heightAt(x,z)`, rotation from the JSON). +4. Optional collateral: bake `collateral` / `collateral_value` extras in the + GLB as the *proposal*; the site JSON's number is *canonical* (the carport's + `collateralValue: 180` pattern). Nothing is billed until `collateralFor()` + can resolve it — an unpriced collateral returns null and reads as "not + scored", never as free… and never as billed either (the gutter was a free + failure for two sprints). +5. Wrecks: same origin, same footprint, mesh-for-mesh swap on + `world.wreckStructure(id)` — build the wreck variant from a shared helper + so intact/wrecked can't drift (E's `_house_facade()` pattern). +6. An assert that can actually fail, negative-control-proven (E rebuilt the + bike at lean 0° to watch the suite go red before trusting it). + +**Conventions the loader reads (bake as GLB extras / node names):** +- `rating_hint` (float) on anchor nodes — effective failure = `hw.rating × + ratingHint`. Unset ⇒ 1 at adoption; the sim reads the anchor, live. +- `collateral` (string) on anchor nodes — what breaks when this anchor's + corner blows. `collateral_key` on a structure whose priced thing has a + different name than the structure (house → "gutter"). +- Named nodes matter: `pickup_anchor` (where carried items sit), the + `fascia_anchor_*` family, etc. Check `world.js` adoptAnchor before renaming + anything. + +**⚠ THE AXIS TRAP (bitten every sprint since 3):** the exporter maps Blender +`(x, y, z)` → three.js `(x, z, −y)`. A lean toward Blender +Y arrives leaning +toward three.js **−Z**. Never write a docstring about orientation without +measuring the exported GLB in three.js coords — E's bike comment lied about its +own geometry and only a browser-coords assert caught the flip. Dims/tri-count +verifies CANNOT catch a wrong shape or a flipped lean; look at the render. + +## Authoring a site (a "level") + +Sites are data: one JSON in `web/world/data/sites/`. Top-level shape (see +`site_02_corner_block.json`): `id, name, blurb, yard, sun, gardenBed, +structures, trees, posts, fence, wind, [props…], [separation], _design/_why`. +Anchors are not listed directly — they come from structures/trees/posts (and +GLB nodes), typed against the checked `ANCHOR_TYPE` enum in contracts.js. + +The authoring loop that works (Sprint 10–13 distilled): +1. Copy the nearest existing site JSON; keep `_design`/`_why` notes in-file — + they're the level's design doc and the repo expects them. +2. `validateSite` runs at load and fails loud (types, wind block, enum). Boot + with `boot({site:'your_site', bank:N})` to jump straight to it. +3. Wind personality lives in the SITE (venturi axis/gain/throat, tree + shelters), storm personality in the storm def. **The venturi axis is site + GEOMETRY (a line, mod π), not a wind heading** — reconciled twice in + THREADS before that sentence earned its place. +4. Every trap must be: visible before commit (the enum gives it its pre-rig + read), priced (collateral), and mechanically real (rating_hint) — "the + price is the trap" vs "the steel is the trap" is a ruling, not an accident. +5. Run the gauntlet before calling it a level: `site_audit` (winnable lines at + $80, funnel ON — check the header), `gardenfly`/`garden_bench` (does + rigging separate held-vs-bare? pin a `separation` block if this site makes + that promise), `storm_envelope` (per-anchor exposure), then **a cold + playthrough by someone who didn't author it** (Lane D's job) — Sprint 11's + soft-lock shipped behind 296 green tests and was found in one minute of + play. +6. The blurb and brief carry real design load (night 3's "plenty to tie off + to" is the trap's cover story). Client/brief/pay live in week.js NIGHTS; + a site without a night is only half real (`boot({site})` seeks the night). + +## The earned gotchas (each cost a real sprint-day; receipts in THREADS) + +- **Measure through the real chain.** Any garden/load number that didn't go + through `commit → attach → skyfx` is suspect: session-level `commit()` + bypasses attach and scores every rig as bare (D, S13); a 12 s pre-settle + skews SailRig's internal storm clock (B catching A, S13). +- **Site wind is built in ONE place**: `windForSite()` in weather.js. Three + harnesses independently rebuilt it wrong (funnel off ×2, frozen tree sway). + Import the helper; never hand-wire venturi/shelters in a tool again. +- **Margin rule:** any bench corner within ~15% of its rating breaks in the + real game (residual bench-vs-UI under-read, cause unfound). Marginal ≠ PASS. +- **`world.dress()` needs the importmap** — without it the bare-specifier + throw is swallowed by callers' try/catch and every ratingHint silently + becomes 1.0 (no carport trap, invincible house). +- **`setHardware()` refuses over-budget quietly** — check `.ok` or fly with + `budget: 9999` in tools (this ate four of C's measurements). +- Documentation cannot fail: an unchecked enum, a JSDoc contract, a comment + about geometry — if nothing asserts it, expect it to be wrong someday. +- Two harnesses, one number. When they disagree, find the variable before + tuning anything; the variable is usually the harness. +- One clone per lane; ports 8823 (integration) / 8824–8828 (A–E) via + untracked `.claude/launch.json` (copy the example). +- Hidden tabs throttle rAF dead: drive with `SHADES.step(dt)` / `.render()`; + hidden-canvas boot needs one resize before projections are sane. + +## Design canon quick-reference (ruled, don't relitigate casually) + +- Business: HARD YARDS. ABN stays sequential (never make it realistic). +- Carport $180 / gnome $25 / gutter $90; bike deliberately unpriced until the + sim can knock it over (tripwire assert enforces this). +- The wild night sells no clean line — the pyrrhic win is intended. Night 5's + garden is beyond saving BY DESIGN and the brief says so. GARDEN_DRAIN does + not move. 18–45 m² is an availability floor, not a ceiling. +- Hail is why sails matter (steep, blockable); rain walks under cloth (don't + reopen the rain-angle argument). + +## Where knowledge actually lives + +READ ORDER for anyone new: README → DESIGN.md → PLAN3D.md → THREADS from the +last `[I]` entry backwards until you have context → the current SPRINTn.md. +THREADS is append-only memory, not a reference — when you learn something from +it the hard way, promote it into THIS file so the next person reads one page +instead of 300 entries.