HardYards/docs/MANUAL.md

9.1 KiB
Raw Blame History

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 <name> 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 1013 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) / 88248828 (AE) 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. 1845 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.