HardYards/PLAN3D.md
m3ultra 8338c0fa99 Add 3D execution plan, lane prompts, and THREADS log
PLAN3D.md: locked stack (vendored three.js r175 + stdlib server, DJsim
house style), verified asset inventory on the M1 Ultra, module contracts,
five parallel lanes (world/sail/weather/player/assets) with acceptance
criteria and integration order.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 21:05:57 +10:00

18 KiB
Raw Blame History

SHADES 3D — execution plan & lane instructions (for Opus 4.8)

Port the proven 2D prototype (see prototype/, play it before writing code) to a 3D third-person game: you are a small person in a real backyard, rigging shade sails against a storm you can see coming, then running repairs in wind and rain.

Read this whole file before starting your lane. DESIGN.md is the design canon; this file is the build canon.


0. Ground rules (all lanes)

  • Stack — locked: three.js r175 vendored (copy ~/Documents/90sDJsim/web/world/vendor/ — three.core.js, three.module.js, addons) + zero-dependency Python stdlib server (server.py, http.server, no pip, no DB, no build step, no CDN, no npm). This is the proven house stack from 90sDJsim; do not relitigate it.
  • Where you run: on the M1 Ultra (johnking@100.91.239.7, Tailscale). All asset paths below are local there. Clone your lane from gitea: git clone git@gitea.partly.party:monster/shades.git ~/Documents/shades-lane<X> then work on branch lane/<x>. Push your branch; never force-push main.
  • Units: meters, +Y up, world origin at yard center on the ground. Player height ~1.7 m. The yard is ~30×20 m. GLB exports: Y-up, meter scale, *_v1.glb naming into web/world/models/.
  • Asset copies rule (house rule from DJsim): runtime models in web/world/models/ are COPIES. Canonical sources stay in the shared libraries (~/Documents/3D=models/, ~/Documents/character_kit/, ~/Documents/3D-STORE/, ~/Documents/FBX/). Never hand-edit a game-local GLB; rebuild at source and re-copy. Raw .fbx/.blend are gitignored; runtime asset is always .glb.
  • Rigged characters: follow 90sDJsim DEVMANUAL "rigged characters" rules — SkeletonUtils.clone() for instances, height-normalize by head-bone height, never blind setScalar. The Mixamo pipeline is ~/Documents/mixamo-fetch/ (README inside; wishlist → fetch.cjs); already-fetched clips sit in ~/Documents/FBX/ and ~/Documents/3D=models/animations/.
  • Testing: the game must run headless-testable. web/world/selftest.html loads the sim modules WITHOUT rendering and runs assert suites (fast-forward a 90 s storm in a loop, print pass/fail JSON to console + DOM). Every lane keeps its module importable there. NOTE: requestAnimationFrame pauses in hidden tabs — selftest must drive the sim with fixed-dt loops, not rAF.
  • Commit style: small commits on your branch, imperative messages, no binary churn (don't re-export unchanged GLBs).
  • File ownership is the merge strategy. Lanes only edit files they own (see lane sections). Shared contracts live in web/world/js/contracts.js — Lane A lands it in M0 and after that it changes only by agreement (note in THREADS.md).
  • Coordination: append-only log in THREADS.md at repo root — one line per landed feature / contract change / open question, prefixed with your lane letter.

1. What we're building (M0 → M3)

M0 — walkable yard (Lane A, serial, ~first). Skeleton everyone builds on: index.html + server.py + vendored three; terrain plane, house shell, placeholder trees/posts; third-person camera; contracts.js. Merged to main before other lanes' first merge (they develop against contracts + mock harnesses meanwhile).

M1 — the rig. Sail cloth sim attached to anchors with per-corner hardware; prep-phase UI (pick 4 anchors, cycle hardware, tension); calm-day wind makes the sail breathe. Character walks, camera follows, anims play.

M2 — the storm. Storm timeline: wind ramp + gusts (telegraphed), direction change, rain; corner loads → weakest-link breaks → cascade; flogging sail; player runs repairs (hold-E re-rig with spare); wind shoves player; debris (3D-STORE crates/tubs) tumbles through.

M3 — the game. Garden bed coverage scoring, prep timer, forecast card, end-of-storm aftermath screen, sound (wind/creak/flog/rain layers), and the full loop: forecast → prep → storm → aftermath. Optional: deploy via the 90sDJsim pattern (docker on dealgod VPS).

The 2D prototype (prototype/game.js) is the reference implementation for: gust scheduler shape, load = spring-stretch at corners smoothed, break = 0.4 s sustained overload, hardware tiers/prices, garden damage = f(uncovered), wind-change event, repair hold. Port the behavior, retune the constants.


2. Asset inventory (M1 Ultra, verified 2026-07-16)

Need Have Path
three.js r175 + addons (GLTFLoader, SkeletonUtils…) vendored ~/Documents/90sDJsim/web/world/vendor/
Player character (small person) rigged fleet ~/Documents/90sDJsim/web/world/models/peds/ (man_casual_01-03, woman, elder…); canonical rigs ~/Documents/character_kit/ (hum_character.glb, female/); more in ~/Documents/3D=models/characters/ (53 people-normal + comical)
Locomotion anims ~/Documents/FBX/ (Running, Falling, Crouch To Stand, Reaction…), ~/Documents/3D=models/animations/ (Walk, Start Walking, Happy Idle, Injured Walk…, 32 clips)
Missing anims (climb ladder, crank/wrench, dig, hammer) fetch ~/Documents/mixamo-fetch/ — add to wishlist, run fetcher
Debris + yard clutter perfect fits ~/Documents/3D-STORE/clean_glbs/ (BlueCrate, BlackTub, WhiteTub, WoodenBin, LibraryTrolley…) + ~/Documents/3D=models/street-furniture/ (benches, streetlight, market_stall, food_cart, building_shell_01)
Furniture/props (granny-flat interiors later) ~/Documents/3D=models/furniture/, props-scenes/
Trees, fence, house exterior, shed, garden bed, grass, shade-sail hardware GAP Lane E builds these (Blender is installed; house style is scripted builds — see ~/Documents/3D-STORE/build_booth_room_v23.py for the idiom)
Blender / Godot / UE 5.7 installed /Applications, /Users/Shared/Epic Games/UE_5.7 (UE/Godot NOT used for this — three.js only)

3. Repo layout (target)

shades/
├── DESIGN.md, PLAN3D.md, THREADS.md
├── prototype/                  # 2D reference — do not modify, do not delete
├── server.py                   # stdlib static server + /selftest passthrough, port 8801
├── .claude/launch.json         # "shades3d" config → python3 server.py
└── web/world/
    ├── index.html              # game shell
    ├── selftest.html           # headless sim asserts (fixed-dt, no rAF)
    ├── vendor/                 # three r175 (copied, committed)
    ├── models/                 # runtime GLBs (copies)
    ├── audio/
    └── js/
        ├── contracts.js        # SHARED interfaces — Lane A owns, change by agreement
        ├── main.js             # boot + game loop + phase machine   (A)
        ├── world.js            # terrain, yard, props, lighting, sky (A)
        ├── camera.js           # third-person rig                    (A)
        ├── sail.js             # cloth sim, anchors, hardware, loads (B)
        ├── rigging.js          # prep-phase UI + hardware economy    (B)
        ├── weather.js          # wind field, storm timeline, rain    (C)
        ├── skyfx.js            # storm sky, lightning, audio layers  (C)
        ├── player.js           # character, controls, anims          (D)
        ├── interact.js         # tool prompts, hold-E actions        (D)
        ├── debris.js           # tumbling objects, collisions        (C)
        └── hud.js              # meters, forecast card, aftermath    (A after M0, else propose in THREADS)

4. Contracts (the integration spine)

Lane A lands contracts.js in M0 with exactly these shapes (JSDoc-typed):

// Wind: THE shared primitive. Everything samples wind through this.
// weather.js implements; world/sail/player/debris consume.
wind.sample(pos /*Vector3*/, t) -> Vector3        // m/s, includes gusts & local effects
wind.gustTelegraph(t) -> {eta, dir, power}|null    // for HUD + grass wave + player audio cue

// Storm timeline (data, not code): storms are JSON so design can tune without code.
// web/world/data/storms/*.json: {duration, baseCurve:[[t,speed]...], gusts:{minGap,maxGap,pow...},
//   dirCurve:[[t,rad]...], events:[{t,type:"windchange"|"debris",...}], rain:{...}}

// Sail rig
sailRig.corners -> [{anchorId, hw:{name,cost,rating}, load, broken}]
sailRig.attach(anchorId[4], hwChoices, tension)
sailRig.step(dt, wind, t)                          // fixed-dt, deterministic
sailRig.coverageOver(rect) -> 0..1                 // ground-projected shade
sailRig.events -> emits {type:"break"|"repair", corner}

// Anchors (world provides; sail consumes)
world.anchors -> [{id, pos, type:"house"|"tree"|"post", sway(t)->Vector3}]

// Player
player.pos, player.carrying, player.busy
interact.register({id, pos, radius, holdSecs, label, canUse()->bool, onDone()})

// Game phases
game.phase: "forecast"|"prep"|"storm"|"aftermath"; game.on(phaseChange)

Determinism rule: sail.js, weather.js, debris.js take (dt, t) — no Date.now(), no internal rAF — so selftest can fast-forward storms.


5. Lanes (run these in parallel)

Lane A — World, shell & integration (start first; owns main.js, world.js, camera.js, hud.js, contracts.js, server.py, index.html, selftest.html)

  1. M0 skeleton per §3: copy vendor from DJsim, stdlib server.py (port 8801, --selfcheck flag runs a headless node-free check by printing the selftest URL; keep it dumb), launch.json, contracts.js, and a walkable graybox yard: 30×20 m ground with gentle height variation, house shell along north edge (use street-furniture/building_shell_01.glb scaled, or graybox), 2 placeholder trees, 2 posts, garden bed rect, boundary fence graybox. Third-person follow camera (orbit on RMB, shoulder-follow default). Directional sun + hemisphere light, calm sky. Merge to main fast — everyone rebases on this.
  2. HUD after M0: wind meter + arrow, corner load bars (world-anchored sprites), garden health, phase banner, forecast card, aftermath screen. Consume contracts only; mock data until B/C land.
  3. Integration duty: you are the merge shepherd. Rebase lane branches onto main in order (E-assets swap is a drop-in; B and C rebase next; D last), run selftest.html + a manual storm after each merge, log regressions in THREADS.md.
  4. Swap in Lane E's GLBs as they land (tree_v1, fence, shed, house when ready).

Acceptance: 60 fps on the M1 Ultra in graybox storm scene; camera never clips through house; selftest green after every merge.

Lane B — Sail simulation & rigging (owns sail.js, rigging.js)

  1. Port prototype cloth to 3D verlet: grid N=10 per side, nodes are Vector3, structural+shear+bend constraints, 5 relaxation iterations, fixed dt 1/60. Wind force per FACE (not node): F = area * clamp(dot(wind, normal))² * ρ along face normal + tangential drag — this gives real hypar behavior: a twisted quad sheds load, a flat one catches it. Prove it in selftest: assert twisted-rig peak corner load < flat-rig peak load, same storm.
  2. Corner load = smoothed stretch of springs incident to corner (prototype formula); hardware tiers/prices from prototype; break after 0.4 s sustained overload; broken corner frees the node → flogging is emergent. Sag under gravity when calm; tension parameter scales rest lengths (0.61.4).
  3. Rendering: BufferGeometry updated from nodes, double-sided, cloth material, per-vertex normal recompute each frame. Sail casts real shadows (this IS the shade mechanic — coverageOver() can raycast sun direction or ground-project).
  4. Rigging UI (prep phase): click anchor markers to assign corners (max 4, ring-order by angle), click corner to cycle hardware with budget, tension slider, spare purchase. Reuse prototype economy numbers as the starting tune.
  5. Selftest suite: flat-vs-hypar assert; cascade assert (break one corner at fixed t → neighbor load must spike ≥2×); determinism assert (two runs, same inputs → byte-equal load traces).

Lane C — Weather, sky & debris (owns weather.js, skyfx.js, debris.js, data/storms/)

  1. wind.sample(pos,t): base curve + scheduled gusts (port prototype scheduler: telegraph 1.5 s, ramp 0.8 s, hold ~2 s, fade) + direction curve with one mid-storm change + spatial variation (smooth noise over x,z so the yard isn't uniform; add tree wind-shadow discs downwind of trees — cheap, juicy). Storm definitions load from JSON (§4). Author storm_01_gentle.json, storm_02_wildnight.json to bracket difficulty.
  2. Rain: GPU-friendly instanced streaks (~24 k), angle/speed follow local wind, fade by intensity from storm JSON; splash rings on ground optional. Storm sky: darken + cloud dome scroll + rumble; one cheap lightning flash event type.
  3. Audio layers (files or synthesized WebAudio): wind bed pitched by speed, gust whoosh keyed to telegraph, rain layer, rope-creak keyed to max corner load (hook via sailRig.corners), fabric flog when any corner broken. Audio unlock on first input (browser rule).
  4. Debris: spawn events from storm JSON — pick from models/debris/ (Lane E copies 3D-STORE crates/tubs there in M0 week). Kinematic tumble along wind with bounce on ground plane, sphere-vs-player knockdown (1.5 s stagger, drop carried), sphere-vs-sail node impulse. No physics engine — hand-roll, ~150 lines.
  5. Selftest: gust telegraph always ≥1.2 s before ramp; wind.sample continuity (no frame-to-frame jumps >X); storm JSON schema validator.

Lane D — Player character & interaction (owns player.js, interact.js)

  1. Character: pick a ped from 90sDJsim/web/world/models/peds/ (copy into models/, per copies-rule; man_casual_01 or woman — small relative to 4 m posts reads "small person"). Load with DEVMANUAL rig rules. Clips: Walk + Running + Happy Idle from the libraries; retarget/merge via character_kit pipeline. Fetch "Climbing Ladder", "Turning Key/crank", "Digging" via mixamo-fetch wishlist for M3.
  2. Controls: WASD relative to camera, walk/run (shift), acceleration + turn smoothing, anim state machine (idle/walk/run + one-shot interact). Gamepad optional. Slope clamp to terrain height.
  3. Weather effects on player: movement slow factor from local wind speed + rain; gust shove = impulse along wind during gust hold (scale with speed²); knockdown state (from debris or extreme gust): Falling clip + get-up (Crouch To Stand), drop carried item.
  4. Interactions via interact.register: hold-E with radial progress — re-rig broken corner (needs spare, 2.5 s), tighten/ease turnbuckle at corner (±tension at ONE corner, 1.2 s — new vs prototype, makes corners individual), pick up/carry spare hardware from shed table (one item, hands-full rule).
  5. Selftest: state machine table test (no stuck transitions); interact radius respects busy/carrying flags.

Lane E — Nature & hardware assets (Blender, scripted; owns tools/blender/, models/)

The gap lane. House idiom: scripted Blender builds (see ~/Documents/3D-STORE/build_booth_room_v23.py and racks_to_glb.py for the export pattern). One tools/blender/build_yard_assets.py that regenerates everything → clean/ GLBs; commit script + GLBs.

Deliverables (meters, Y-up, low-poly stylized to match ped fleet, baked or flat colors, <15 k tris each):

  1. tree_gum_01/02.glb — trunk + 2-3 canopy blobs as SEPARATE named nodes (trunk, canopy_*, branch_anchor_* empties at rig points) so Lane A can sway canopies in wind and Lane B can query branch anchor positions.
  2. fence_panel.glb, fence_post.glb (tileable), gate.glb.
  3. house_yardside.glb — single-story rear façade: wall, 3 fascia_anchor_* empties, gutter line, window, door. Doesn't need interior.
  4. shed_01.glb + shed_table.glb (spare-hardware pickup point).
  5. garden_bed.glb — raised bed + plant billboard tufts (damage states: full / tattered / dead as morph or 3 variants).
  6. sail_post.glb (raked steel post + footing collar), ladder_01.glb.
  7. Hardware close-ups (visible at corners): shackle.glb, carabiner.glb, turnbuckle.glb — tiny, but they're the fetish objects of the game; make them read at 1 m distance.
  8. models/debris/: copy + sanity-scale BlueCrate_v2, BlackTub_v2, WhiteTub_v2, WoodenBin_v2 from 3D-STORE/clean_glbs/; add tramp_01.glb (trampoline!) if time.
  9. Grass: NOT geometry — ship a 4-tuft billboard atlas texture; Lane A instances.

Acceptance: blender -b -P tools/blender/build_yard_assets.py regenerates all GLBs deterministically; each GLB loads in a viewer with correct scale vs a 1.7 m reference capsule (include ref_capsule.glb in the script).


6. Integration order & schedule

day 0:   A lands M0 skeleton → main          (B,C,D,E develop against contracts + mocks)
day 1-2: E drops first GLBs (trees/fence/house) → A swaps graybox
         B lands cloth in selftest, then rendered sail on main
         C lands wind+rain on main (sail immediately reacts — first magic moment)
day 3-4: D lands character + controls + weather effects
         C lands debris + audio; B lands rigging UI/economy
day 5:   M2 storm playable end-to-end; tune pass (everyone plays, THREADS notes)
day 6+:  M3: scoring, forecast/aftermath, prep timer, sound polish, storm JSON tuning

Merge conflicts should be ~zero if file ownership is respected. If you need to touch another lane's file: don't — post the need in THREADS.md instead.

7. Definition of done (M3)

  • Full loop: forecast card → timed prep (rig 4 corners on any anchors, budget $80, buy spare) → 90 s storm (gusts telegraphed, wind change, rain, ≥1 debris event) → aftermath (garden %, hardware losses, cash delta).
  • A flat drum-tight cheap rig MUST cascade-fail in storm_02; a well-twisted mixed rig with one mid-storm repair MUST be survivable. Verify in selftest AND by hand.
  • 60 fps on the M1 Ultra, no build step, python3 server.py and it runs.
  • Selftest green; THREADS.md tells the story; DESIGN.md updated where reality diverged from plan.