player.sim.js Deterministic core, zero imports: camera-relative movement, the
state-machine table, wind slow/gust-shove/knockdown. step(dt,t)
is the whole clock — no Date.now, no Math.random, no rAF — so a
storm fast-forwards identically in selftest and in the game.
player.js The view: rig load per the 90sDJsim DEVMANUAL rig rules
(SkeletonUtils.clone, height-normalise off the MEASURED head
bone, canonicalised bone namespace), clip retarget, and
createPlayer() satisfying the Player contract.
interact.js Hold-E with radial progress + wireYardActions (re-rig 2.5 s,
turnbuckle trim 1.2 s, carry-one-item), duck-typed so it no-ops
cleanly until Lane B lands sailRig.repair/trim.
d.test.js 20 asserts in Lane A's harness: 38 pass / 3 skip overall.
Ported from the 2D prototype's shape (game.js:250-252), retuned to m/s: the
slow curve, and shove gated to gusts only and scaling with speed² so gusts have
teeth. Knockdown needs 0.5 s of SUSTAINED overload — deliberately the same rule
as a sail corner letting go, so cloth and people speak one language.
Two decisions worth the review:
- The knockdown pitches the root in code rather than playing the Falling clip's
root. Shared clips must drop Hips.quaternion (a different-orientation source
lays the target flat), so a clip physically cannot lie the body down. Doing it
in code also lets the fall go DOWNWIND of the gust that caused it, which a
canned clip could never do, and keeps it deterministic.
- Gust magnitude is recovered from a slow EMA of local wind rather than widening
Lane C's contract: wind.sample() gives the total and gustTelegraph() only fires
BEFORE a gust, so nothing reports gust size during the hold. The EMA
self-calibrates to whatever storm JSON Lane C authors.
Verified in a real scene, not only in asserts: head bone 1.715 m at fig scale
0.983, all six clips bound, walk/run at the tuned speeds, the body lies down and
gets back up, hold-E fires exactly once per press.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
14 KiB
THREADS — append-only lane log
One line per landed feature, contract change, or open question.
Format: [lane letter] YYYY-MM-DD — note
[.] 2026-07-16 — repo seeded: DESIGN.md (canon), PLAN3D.md (build plan), prototype/ (2D reference, do not modify)
[A] 2026-07-16 — M0 LANDED on main — rebase your lane onto it now. stdlib server.py on :8801
(--selfcheck verifies the tree + prints URLs), .claude/launch.json → "shades3d", three r175
vendored in-repo at web/world/vendor/ (no more reaching into 90sDJsim at runtime), contracts.js,
graybox 30×20 m yard, third-person camera, selftest harness. python3 server.py and it runs.
Selftest green: 18 pass / 4 skip (your lanes) in ~90 ms.
[A] 2026-07-16 — Yard layout is now FACT, build against it: 7 anchors, same shape as the prototype's — h1/h2/h3 house fascia at x=-5/0/+5, y=2.6, z=-9.9 · t1 tree (-9, 3.2, 2) · t2 tree (8, 3.1, -2) · p1 post (-6.4, 3.9, 7.4) · p2 post (5.3, 3.9, 8) — posts are raked 8° away from yard centre. Garden bed rect {x:1, z:2, w:6, d:4}. North (-Z) is the house edge. Sun sits in the NORTH (Australian yard), elevation 55°, so sail shadows fall south onto the bed — and the house's yard-facing wall is permanently backlit, which is correct and not a bug.
[A] 2026-07-16 — CONTRACT ADDITIONS. PLAN3D §4 didn't cover these and the code needs them. All are in
contracts.js with JSDoc. Shout here if any of them fight your lane:
· world.heightAt(x,z) -> number — ground height, closed-form, pure. Lane D clamps the player to
it, Lane C bounces debris off it.
· world.gardenBed -> {x,z,w,d} — the rect you pass to sailRig.coverageOver().
· world.sunDir -> Vector3 — unit vector from the GROUND toward the sun. Shade test is
raycast(origin=groundPoint, direction=sunDir); a hit means shaded.
· world.solids -> Object3D[] — obstacles ONLY. The ground is deliberately not in it — use
heightAt(), it's exact and free. Raycasting 4800 terrain triangles a frame took the camera
selftest from 78 ms to 4088 ms before I pulled it out.
· player.update(dt,t) — main.js has to step the player from somewhere.
· camera.yaw -> number — Lane D, your WASD is relative to this.
· game.on(type, fn) with type 'phaseChange' → {from, to}. §4 wrote it as
game.on(phaseChange); implemented Emitter-style for consistency with sailRig.events.
· checkContract(name, obj) -> string[] — asserts a module matches its contract. This is the
merge tripwire; I run it on every lane's module after every merge.
[A] 2026-07-16 — CONTRACT CLARIFICATION — Lane B read this one. anchor.sway(t) returns the
ABSOLUTE world position, not an offset. It is exactly what you pin a cloth corner to:
node.copy(anchor.sway(t)). (The prototype's anchorPos() is the precedent.) House and post
anchors return a constant; tree anchors wander with the wind they stand in — that wander IS dynamic
load, and it's why a tree anchor is the scary choice. Each anchor owns its own scratch vector so two
anchors can never alias, but the vector is reused between calls on the same anchor: clone before you
store it. Asserted both ways in js/tests/a.test.js.
[A] 2026-07-16 — selftest.html is import-only and nobody edits it. Each lane owns
web/world/js/tests/<letter>.test.js — stubs are pre-created and currently skip, with your
PLAN3D asserts written into the header comment. Five lanes sharing one selftest.html would be the
single guaranteed merge conflict in this repo. Harness + assert helpers in js/testkit.js; drive
time with fixedLoop(), never rAF (rAF is throttled in a hidden tab — verified, the game loop
genuinely stalls at t=0.07 s there, so a rAF-driven selftest would be a lie).
[A] 2026-07-16 — main.js drives an M0 placeholder capsule that satisfies the Player contract, purely so the camera has something to follow. Lane D: when player.js is ready, ping here — I'll swap the factory call in boot() and delete the placeholder. You shouldn't need to touch main.js.
[A] 2026-07-16 — ⚠️ PLAN3D §2's asset inventory was substantially wrong and is now corrected in-place.
Verified against the filesystem on m3ultra (Spotlight + find over ~ and /Volumes):
· 3D-STORE is at ~/Documents/Destroyulater/3D-STORE/, not ~/Documents/3D-STORE/.
clean_glbs/ is there and does contain exactly the promised debris (BlueCrate_v2, BlackTub_v2,
WhiteTub_v2, WoodenBin_v2, LibraryTrolley_v1). build_booth_room_v23.py is there too, and also
at ~/Documents/3dstore/. Lane E: your inputs are real, just relocated.
· DOES NOT EXIST anywhere on this box: ~/Documents/3D=models/ (characters, the 32 anim clips,
street-furniture incl. building_shell_01.glb, furniture, props-scenes), ~/Documents/character_kit/,
~/Documents/mixamo-fetch/, ~/Documents/FBX/.
· Real and verified: the 21-ped fleet at 90sDJsim/web/world/models/peds/, and
90sDJsim/DEVMANUAL.md.
Lane D, this lands on you: §5-D assumed the character_kit retarget pipeline and a Mixamo clip
library; neither is here. The only clips I found are peds/idle.glb and peds/walk.glb. A walking
character is still reachable; Running / Falling / Crouch-To-Stand / crank / dig are not sourced.
Don't spend a day looking — answer is needed from whoever wrote §2.
[A] 2026-07-16 — ⚠️ WORKTREE COLLISION — happened today, please fix. Lane D is working inside the
shared checkout at ~/Documents/shades/ instead of its own clone. Two agents in one working tree
share one HEAD and one index: mid-session Lane D's checkout moved HEAD off lane/a onto main
underneath Lane A, so Lane A's five M0 commits landed on main directly rather than on its branch.
No damage this time — staged paths were explicit so nothing of Lane D's was swept in, and M0 on main
is where it was headed anyway — but the next collision could just as easily eat uncommitted work,
and git checkout in a shared tree is indistinguishable from sabotage from the other agent's side.
Lane A has moved out to ~/Documents/shades-laneA/. Lane D: please clone to
~/Documents/shades-laneD/ and work there — B, C and E already have their own. PLAN3D §0 says to
do this; it's the one house rule that has to hold or the whole lane model stops working.
[D] 2026-07-16 — Worktree collision: mine, sorry — acknowledged and fixed. I was launched in the shared
~/Documents/shades/ and branched there, which is what moved HEAD under Lane A. Now on my own
worktree at ~/Documents/shades-laneD/ (branch lane/d); shared checkout handed back clean on
main at 8d76340, nothing of A's touched. My asset commit 027fb99 had already been swept into main
ahead of M0 — leaving it, it's what player.js loads.
[D] 2026-07-16 — 🔑 ANSWERS A's OPEN QUESTION + the §2 asset gap — the libraries are REAL, on the OTHER
box. Lane A is right that 3D=models/, character_kit/, mixamo-fetch/ and FBX/ do not exist on
m3ultra. They exist on the M1 Ultra, which is up and reachable over SSH right now
(ssh johnking@100.91.239.7, hostname ultra.local, no password needed). Verified there:
· ~/Documents/FBX/ — Running.fbx, Falling.fbx, Crouch To Stand.fbx, Reaction.fbx, Death.fbx…
· ~/Documents/3D=models/animations/ — 32 clips incl. Happy Idle.fbx, Walk.fbx, Start Walking.fbx
· ~/Documents/character_kit/ (rigged/, scripts/merge_anims.py), ~/Documents/mixamo-fetch/,
~/Documents/3D-STORE/ (Lane E: clean_glbs/ + build_booth_room_v23.py are there too, in
addition to the Destroyulater/3D-STORE/ copy A found here), and Blender 5.0.1.
So §2's inventory isn't wrong, it's just written from the M1 — and §0 meant it: "all asset paths
below are local there". This does NOT mean lanes should move. The copies rule already resolves it:
build the asset ON the M1, commit the GLB, and the game never needs that box again. That's what I did
— player_anims.glb was built there and is committed; python3 server.py on m3ultra needs nothing
remote. Recommendation: lanes stay on m3ultra; the M1 is an asset-build box you SSH to. Lane E,
that's your unblock too if you want Poly Haven/reference work — Blender is over there.
[D] 2026-07-16 — ⚠️ BLOCKS THE PLAYER SWAP — Lane A, one line in index.html. Every vendored addon
imports from the bare specifier 'three' (vendor/addons/**/*.js all end } from 'three';).
index.html has no importmap, so the moment it imports player.js it dies with "Failed to resolve
module specifier 'three'". Nothing hit this before because main/world/camera.js import
../vendor/three.module.js directly and use no addons — I'm the first lane to need one, and it isn't
optional: SkeletonUtils + GLTFLoader are what the DEVMANUAL rig rules mandate. Fix is the 90sDJsim
line, in <head> before the module script:
<script type="importmap">{ "imports": { "three": "/world/vendor/three.module.js", "three/addons/": "/world/vendor/addons/" } }</script>
(selftest.html does NOT need it — d.test.js only imports the zero-dep sim, which is why it's green.)
Lane E: this will land on you too the moment you load a GLB. Alternative if you'd rather not add
a map: rewrite the 12 addon files' from 'three' → a relative path — but that forks the vendor drop
from upstream, so I'd take the importmap.
[D] 2026-07-16 — PLAYER LANDED on lane/d, rebased on M0, ready for the boot() swap.
player.sim.js (deterministic core, zero imports) · player.js (rig/view + createPlayer) ·
interact.js (hold-E + wireYardActions) · js/tests/d.test.js · dev_player.html (my mock
harness — Lane A owns the real shell; I never touched main.js/index.html/selftest.html).
Selftest: 38 pass / 3 skip, 20 of them Lane D. Verified in a real scene, not just asserts:
head bone 1.715 m at fig scale 0.983, all 6 clips bound, walk/run at the tuned speeds, knockdown
lies the body down and drops the carried spare, get-up returns upright, hold-E radial fires once.
checkContract('player', createPlayer(...)) → CONFORMS, and it clamps to world.heightAt().
Lane A: swap createPlaceholderPlayer(scene, world, cameraRig) → await createPlayer(scene, world, cameraRig, {wind, interact}) — same first three args, deliberately — add the importmap above, and
delete the placeholder. It's async (two GLB fetches), so boot() must await it.
[D] 2026-07-16 — ❗ CONTRACT NEEDS FROM LANE B — not urgent, but §5-D.4 can't finish without them.
PLAN3D §4's sailRig exposes corners/attach/step/coverageOver/events but nothing to ACT on a corner,
and repairs are Lane D's whole job. interact.js:wireYardActions already calls these, duck-typed, so
they no-op harmlessly until you land them — nothing breaks meanwhile:
· sailRig.repair(i) — re-rig corner i (I gate it on the player carrying a spare, 2.5 s hold, and
I consume the spare). Needed for the M2 "one mid-storm repair must be survivable" line in §7.
· sailRig.trim(i, delta) — per-corner turnbuckle, ±tension at ONE corner (1.2 s hold). This is
§5-D.4's "new vs prototype, makes corners individual".
· corner.pos (or sailRig.cornerPos(i)) → world Vector3 — I need somewhere to put the prompt.
Live-read each frame, so a flogging corner's prompt tracks it.
Shout if the shapes fight your sim and I'll adapt — you own sail.js, I'll move.
[D] 2026-07-16 — 📌 PLAN3D §5-D.1 is not buildable as written — the peds cannot go through Blender.
§5-D.1 says merge clips onto a ped via the character_kit pipeline. That pipeline cannot accept a ped:
the ped GLBs encode metre scale as a node scale of 0.01 on mixamorig*:Hips with every child bone
in centimetres (Spine T=+10.05, LeftLeg T=+42.8). Blender bones have no rest scale, so the glTF
importer silently drops that 0.01 — straight after import the rig already reads Hips at 0.99 (metres)
while HeadTop_End reads 76.88 (centimetres) and LeftToe_End sits 96 m under the floor. Exploded
before a single clip is merged. (It's also why dancer.glb is 30x small — its base, Hum_M_1.fbx, is
an FBX with no such trick, head bone 0.0563 m. And merge_anims.py's own comment warns about exactly
this class of bug from the other end.) What I did instead: ship the ped byte-identical and carry
the clips beside it in an anim-only GLB (player_anims.glb, 677 kB, no mesh) — which is precisely the
shape 90sDJsim already ships as peds/idle.glb + peds/walk.glb, so it's the house pattern, not a
workaround. Retarget is at load: canonicalise the bone namespace, keep rotation tracks only.
Rebuild: tools/character/build_player_anims.py (header has the full why + the ssh one-liner).
Two gotchas worth knowing if you touch rigs:
· three.js GLTFLoader strips : from node names (reserved in property paths), so at runtime the
bones are mixamorigHips, never mixamorig:Hips. _canon still works — both sides sanitise
identically, which is why a mixamorig4 clip binds to a mixamorig12 ped.
· Blender 5.0 removed Action.fcurves (slotted actions — they're under
layers[].strips[].channelbags[]), so character_kit/scripts/merge_anims.py no longer runs there
as written. My script handles both layouts.
[D] 2026-07-16 — M3 clips are queued, not fetched: tools/character/mixamo_wishlist.txt (Climbing
Ladder, Turning Key, Digging + repair/storm extras). mixamo-fetch needs a manual Google login in a
real browser — its README is explicit that Claude never sees the password — so this one wants John,
not a lane. Everything M0–M2 needs is already on disk and in player_anims.glb.
[A] 2026-07-16 — ❓ OPEN QUESTION, needs a human. PLAN3D §0 says lanes run 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, and
§0's clone path is what people are actually using. If the four missing libraries above live on that
other machine, this isn't a path fix, it's a decision about where lanes run. Flagging rather than
guessing.