SPAWN FRAMING. The QA pass: "a pole dead-centre through the player on every single first impression." Measured, not eyeballed — player spawns at (0,6) looking up the yard, and post p3 stands at (0,7), one metre behind, dead on the view line. spawnYawFor() sweeps outward from the ideal frame (camera opposite the bed, so the player stands in front of what they protect) and takes the first yaw that clears every obstacle. Per YARD, off site data: site_02 spawns elsewhere with its posts elsewhere, so a hand-tuned constant would frame the corner block by luck. Two numbers here are measured and both were wrong first. Clearance 0.6, not 1.1 — my first pass asked 1.1m and produced a WORSE frame than the bug, swinging 105° to stare at a fence, because the greatest distance any obstacle can have from the camera->head line is its own distance from the player, and p3 is only 1m away, so 1.1 was unreachable by construction. maxOff 90° — a frame turned more than a quarter-circle off the bed is no longer a frame of the bed, so the "roomiest" fallback is bounded to the arc. The assert reproduces the 105° from the real yard and pins that the cap brings it to 68°. AFTERMATH CLOTH-SWALLOW. "The dead draped sail can swallow the camera whole." The camera has collided with the house since Sprint 2; the sail was just never in the list. While up it hangs above head height and nothing notices — but a FAILED sail lies in the yard at head height, the one moment you most want to look at it. refreshCameraSolids() adds the cloth to the set, called from both rebuilds (loadSiteInto makes new world.solids, rigSail makes a new cloth; either alone strands the camera on a disposed mesh). Its bounding sphere recomputes every frame, so the raycast reads live geometry. Verified live: camera solids 9 -> 10 the moment a sail rigs, and the cloth is in the set. Added camera.solids getter so that was checkable at all — "did the sail join the set" was otherwise a claim with no seam. And hoisted sailView's declaration with rig/rigging: loadSiteInto's boot-time call reads it before any cloth exists, which is a TDZ throw (page goes blank) that the same block already documents for rig/rigging. selftest 339/0/0 (was 338). |
||
|---|---|---|
| .claude | ||
| docs | ||
| prototype | ||
| tools | ||
| web/world | ||
| .gitignore | ||
| DESIGN.md | ||
| LANE_PROMPTS.md | ||
| PLAN3D.md | ||
| README.md | ||
| server.py | ||
| SPRINT2.md | ||
| SPRINT3.md | ||
| SPRINT4.md | ||
| SPRINT5.md | ||
| SPRINT6.md | ||
| SPRINT7.md | ||
| SPRINT8.md | ||
| SPRINT9.md | ||
| SPRINT10.md | ||
| SPRINT11.md | ||
| SPRINT12.md | ||
| SPRINT13.md | ||
| THREADS.md | ||
SHADES — rig it, then survive the night
Geometry/tower-defense: you're a small person rigging shade sails against Australian storms. three.js r175 (vendored, no build step) + a zero-dep Python server. Everything the repo needs is in the repo.
Run it
python3 server.py --port 8801 # any free port
# game: http://localhost:8801/web/world/index.html
# selftest: http://localhost:8801/web/world/selftest.html (must be all green)
# 2D proto: http://localhost:8801/prototype/index.html (the original thesis)
No pip, no npm, no build. Python 3.8+ and a browser.
Read in this order
DESIGN.md— the design canon (what the game is)PLAN3D.md— the build canon (stack, contracts, lane model)THREADS.md— the append-only lane log. The project's memory. Read the last few[I]integrator entries to know the current state.SPRINT12.md— the current sprint (highest-numbered SPRINTn.md wins)LANE_PROMPTS.md— copy-paste prompts per lane, per sprint (bottom = newest)
The lane model (how this repo is built)
Five parallel Opus agents ("lanes"), each in its OWN clone, on its own branch:
| lane | owns |
|---|---|
| A | main.js, world.js, camera, hud, week, sites, server — shell & integration, merge shepherd |
| B | sail.js, rigging — cloth sim, economy, balance.test pen |
| C | weather, skyfx, debris, storm/site wind data |
| D | player, interact, ladder, broom — and the designated playtester |
| E | Blender asset factory (tools/blender), models/, textures |
Hard rules (they exist because each was earned — see THREADS):
- One clone per lane. Never two agents in one working tree.
- Rebase onto latest
mainbefore starting; small commits; push your branch. - File ownership is the merge strategy; cross-lane needs go in THREADS.md.
- Selftest green after every landing. Skips must be real. An assert that cannot fail is decoration.
- All sim code is deterministic
(dt, t)— no Date.now, no rAF. - Measure, don't reason: the repo's biggest bugs were all "a number gathered from the wrong harness". When two harnesses disagree, find the variable.
Setup on a new machine
Gitea's SSH runs over tailscale (the public host is Cloudflare-proxied, no
direct SSH). Add to ~/.ssh/config:
Host gitea.partly.party
HostName 100.71.119.27
Port 222
User git
Then:
git clone git@gitea.partly.party:monster/shades.git ~/Documents/shades
for L in A B C D E; do
git clone git@gitea.partly.party:monster/shades.git ~/Documents/shades-lane$L
git -C ~/Documents/shades-lane$L checkout -B lane/$(echo $L | tr 'A-Z' 'a-z') origin/lane/$(echo $L | tr 'A-Z' 'a-z')
done
~/Documents/shades is the integration checkout (main; where merges and
sprint docs happen). The five shades-lane* clones are where lane agents run.
Per clone, once: cp .claude/launch.example.json .claude/launch.json and pick
a port of your own (integration 8823, lanes A–E 8824–8828). The real
launch.json is untracked — six checkouts on one laptop can't share one port.
Sprint cycle
- Fire lanes with the newest prompts at the bottom of
LANE_PROMPTS.md(Lane A usually first if the sprint gives it rulings/blocking items). - Lanes work, push their branches, log in THREADS.md.
- Integrate in the main checkout: merge
origin/lane/{e,c,b,d}(THREADS.md conflicts resolve keep-both-chronological; real code conflicts get read), run selftest, play the result, writeSPRINT(n+1).md+ prompts, push main AND fast-forward all fivelane/*branches to main.