Replaces the skip stub with the checks Lane A's header asked for — every GLB
loads, is metre-scale with height on +Y, and keeps the nodes other lanes query
by name — plus anchor world-position and the garden bed's three damage states.
This catches what the Blender side structurally cannot: Blender exports
Z-up->Y-up and imports Y-up->Z-up, so a broken export_yup round-trips green.
A native glTF reader is the only thing that can prove the file.
GLTFLoader is imported dynamically on purpose. Three.js addons import the bare
specifier `three`, no page in the repo has an importmap yet, and selftest.html
turns an un-importable lane module into a hard FAIL — so a static import would
redden Lane A's merge gate over a harness gap rather than a real defect. It
skips with the fix instead, and lights up by itself once the importmap lands.
Verified behind a temporary probe first: 36/36 pass. Need logged in THREADS.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Verifying the GLBs in three.js (not just Blender) showed four assets reporting
inflated bounds: tramp_01 came back 3.29 x 1.27 m against a true 2.96 x 0.78.
Box3.setFromObject expands each mesh's LOCAL box by the world matrix, so a node
carrying a rotation over-reports — the same trap Blender's obj.bound_box sets,
and what three uses for frustum culling. Joined nodes inherited parts[0]'s
rotation, which for an arc is half a segment step off-axis.
Applying rotation at join time makes every local box axis-aligned, so the
default Box3 path is now correct for consumers and culling is tight. World
geometry is unchanged; the exported dims are identical.
Adds tools/assetcheck/, the three.js harness that caught this. It's the only
check that can: a Blender round-trip exports Z-up->Y-up and imports Y-up->Z-up,
so a broken export_yup flips back and passes green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
16 generated assets plus the grass billboard atlas and the four debris models
copied verbatim from the 3D-STORE library (copies rule, §0). All meter-scale,
Y-up, and far under the 15k tri budget — garden_bed is heaviest at 2,580.
Regeneration is byte-deterministic: two consecutive runs produce identical
files, so re-running the factory causes no churn.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
selftest.html only imports; each lane owns js/tests/<letter>.test.js. Five lanes
sharing one selftest.html would be the single guaranteed merge conflict in the
repo, so the stubs are pre-created with each lane's PLAN3D asserts written into
the header.
No rAF anywhere: it's throttled in a hidden tab, so a rAF-driven selftest would
pass only while you watch it. fixedLoop() drives time instead.
Lane A's suite covers the §5-A acceptance criteria, including "camera never
clips through house" — stated as the underlying invariant (no solid between the
player's head and the camera) so it survives Lane E swapping the house GLB.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The yard's meshes are placeholders; its layout is not. Anchor positions, ground
heights, the garden rect and the sun direction are what the other lanes build
against, so Lane E's GLBs drop into named slots without anyone re-deriving them.
Sun sits in the north (Australian yard), which puts the house's yard-facing wall
in permanent backlight. That's correct, but the fascia line carries three of the
seven anchors, so sky fill is turned up to keep it readable rather than a void.
Camera collision resolves toward the geometry, not away from it: clamping up to
a comfort distance after the raycast is what put the camera 17cm inside the
north wall. Ground is handled by heightAt() instead of a raycast — exact, can't
be tunnelled, and free.
main.js runs a fixed-dt accumulator so sim modules only ever see FIXED_DT. That
is what lets selftest fast-forward a 90s storm and get the numbers the player
got. Nothing auto-runs on import; index.html calls boot().
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Types, tuning constants ported from the prototype, a seeded PRNG and Emitter,
plus checkContract() — the tripwire that catches a lane drifting from its
interface at merge time instead of three lanes later.
createStubWind() ports the prototype's gust shape (telegraph 1.5s, ramp 0.8s,
hold 1.7s, fade 1.0s) so B and D can develop before Lane C lands weather.js.
Its schedule is precomputed at construction, which keeps sample() pure in t —
the selftest samples out of order and must get the same answer.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
server.py serves the repo root rather than web/, so the 2D prototype stays
reachable next to the 3D port — you want to flip between the reference
implementation and the thing you're porting without stopping the server.
--selfcheck verifies the tree and prints URLs; no node, no network.
three r175 is copied in from 90sDJsim rather than referenced, so the game has
no runtime dependency on a sibling checkout.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Character is a byte copy of 90sDJsim's man_casual_01 ped (already metre-scale,
head bone 1.75 m, bind pose proven in that game). Clips ride beside it in an
anim-only carrier built from the FBX libraries on the M1 Ultra.
The peds cannot round-trip through Blender: they encode metre scale as a node
scale of 0.01 on mixamorig*:Hips with child bones in centimetres, and Blender
bones carry no rest scale, so the glTF importer silently drops it — the rig is
exploded on import (LeftToe_End 96 m below the floor) before any merge happens.
So merging clips onto the ped (PLAN3D 5-D.1) is not viable; we ship the ped
untouched and retarget rotation-only clips onto it at runtime, the same shape
90sDJsim already uses for peds/idle.glb + peds/walk.glb.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>