# 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` then work on branch `lane/`. 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 — **see §2 for the verified paths, several of which are not what this plan originally claimed.** Never hand-edit a game-local GLB; rebuild at source and re-copy. Raw `.fbx`/`.blend` are gitignored (`.gitignore` enforces it); 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 (re-verified on `m3ultra` 2026-07-16 by Lane A) **This table was wrong in the first draft and has been corrected against the actual filesystem.** Several libraries it promised are not on this box at all. Trust this table over any path you remember; if you find one of the missing libraries somewhere, say so in THREADS.md rather than fixing it locally. | Need | Have | Path | |---|---|---| | three.js r175 + addons (GLTFLoader, SkeletonUtils…) | ✅ verified, now vendored in-repo | `web/world/vendor/` (copied from `~/Documents/90sDJsim/web/world/vendor/`) | | Player character (small person) | ✅ 21 rigged peds | `~/Documents/90sDJsim/web/world/models/peds/` (man_casual_01-03, woman_casual_01-02, elder, hivis worker…) | | Rigged-character loading rules | ✅ | `~/Documents/90sDJsim/DEVMANUAL.md` — Lane D: this is law | | Locomotion anims | ⚠️ **thin** | `~/Documents/90sDJsim/web/world/models/peds/idle.glb`, `walk.glb` are the only clips found. Run/Falling/Crouch-To-Stand are **not on this box** | | Debris + yard clutter | ✅ exactly as promised, **new path** | `~/Documents/Destroyulater/3D-STORE/clean_glbs/` — BlueCrate_v2, BlackTub_v2, WhiteTub_v2, WoodenBin_v2, LibraryTrolley_v1, AirCon_v1 | | Scripted-Blender build idiom | ✅ **new path** | `~/Documents/Destroyulater/3D-STORE/build_booth_room_v23.py` (also `~/Documents/3dstore/`) | | Trees, fence, house exterior, shed, garden bed, grass, shade-sail hardware | ❌ **GAP** | Lane E builds these. Lane A has graybox stand-ins at the right scale and positions in `world.js` — match those and they drop straight in | | Blender | ✅ installed | `/Applications/Blender.app` | | ~~`~/Documents/3D=models/`~~ (characters, 32 anim clips, street-furniture incl. `building_shell_01.glb`, furniture, props-scenes) | ❌ **DOES NOT EXIST** | searched `~` and `/Volumes` + Spotlight. Nothing at this path or any similar name | | ~~`~/Documents/character_kit/`~~ (hum_character.glb, retarget pipeline) | ❌ **DOES NOT EXIST** | — | | ~~`~/Documents/mixamo-fetch/`~~ (wishlist → fetch.cjs) | ❌ **DOES NOT EXIST** | closest is `~/Documents/mixamo_upload/` (`Hum_M_1_mixamo.fbx/.obj`) — an upload staging dir, not the fetcher | | ~~`~/Documents/FBX/`~~ (Running, Falling, Crouch To Stand…) | ❌ **DOES NOT EXIST** | only Unreal's own internal `FBX` dirs match the name | **Consequences, unresolved — Lane D is the one this hurts.** §5-D assumes the `character_kit` retarget pipeline and a library of Mixamo clips. Neither is here. The ped fleet and DEVMANUAL are real, so a walking character is still reachable, but Run / Falling / Crouch-To-Stand / crank / dig have to come from somewhere. Post in THREADS.md before burning a day looking for them. **Open question:** PLAN3D §0 says the work happens on "the M1 Ultra (`johnking@100.91.239.7`, Tailscale)", but this box is `m3ultra` and already has `~/Documents/shades-laneB/` and `shades-laneE/` checked out, so lanes are in fact running here. If the missing libraries live on that other machine, §0 needs a decision about where lanes run — not just a path fix. --- ## 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) ├── testkit.js # assert/fixedLoop harness (A) ├── tests/.test.js # ONE PER LANE — you own yours, nobody │ # edits selftest.html (it just imports these) ├── 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): ```js // 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 (graybox — `building_shell_01.glb` does not exist on this box, see §2), 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.6–1.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 (~2–4 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 are a live problem — read §2 before you start.** `character_kit/`, `mixamo-fetch/`, `FBX/` and `3D=models/animations/` are NOT on this box. The only clips found are `peds/idle.glb` and `peds/walk.glb`. Walking is reachable from those; Running / Falling / Crouch-To-Stand / crank / dig are not sourced yet. Raise it in THREADS.md rather than hunting. 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/Destroyulater/3D-STORE/build_booth_room_v23.py` and `racks_to_glb.py` for the export pattern — note the corrected path, §2). 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 `~/Documents/Destroyulater/3D-STORE/clean_glbs/` (verified present, corrected path — §2); 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.