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>
176 lines
14 KiB
Markdown
176 lines
14 KiB
Markdown
# 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.
|