Compare commits
No commits in common. "de86aa1662ae461d74996d9c2f2534788ecc4953" and "0dba2d889114f94eecf3613057729be1dc6f1da2" have entirely different histories.
de86aa1662
...
0dba2d8891
@ -4,8 +4,24 @@
|
|||||||
{
|
{
|
||||||
"name": "shades3d",
|
"name": "shades3d",
|
||||||
"runtimeExecutable": "python3",
|
"runtimeExecutable": "python3",
|
||||||
"runtimeArgs": ["server.py", "--port", "8811"],
|
"runtimeArgs": [
|
||||||
"port": 8811
|
"server.py",
|
||||||
|
"--port",
|
||||||
|
"8809"
|
||||||
|
],
|
||||||
|
"port": 8809
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "shades-proto",
|
||||||
|
"runtimeExecutable": "python3",
|
||||||
|
"runtimeArgs": [
|
||||||
|
"-m",
|
||||||
|
"http.server",
|
||||||
|
"8642",
|
||||||
|
"--directory",
|
||||||
|
"prototype"
|
||||||
|
],
|
||||||
|
"port": 8642
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
57
THREADS.md
57
THREADS.md
@ -448,60 +448,3 @@ Format: `[lane letter] YYYY-MM-DD — note`
|
|||||||
PickUp, Carry, CarryTurn, CarryIdle, StandUp, TakeCover, StumbleBack, PlantSeeds); rebuilt on the M1
|
PickUp, Carry, CarryTurn, CarryIdle, StandUp, TakeCover, StumbleBack, PlantSeeds); rebuilt on the M1
|
||||||
(Blender 5.0.1, 17 NLA tracks, 2.3 MB) and committed. Verified: GLTFLoader reads all 17 clips with
|
(Blender 5.0.1, 17 NLA tracks, 2.3 MB) and committed. Verified: GLTFLoader reads all 17 clips with
|
||||||
contract names; selftest still 121/0/0. Lane D: your M3 verbs are on disk — wire when ready.
|
contract names; selftest still 121/0/0. Lane D: your M3 verbs are on disk — wire when ready.
|
||||||
|
|
||||||
[A] 2026-07-16 — 🚩 **GATE 1 — the yard is live. LANE D: START.** SPRINT2 §Lane A steps 1–4 are on main.
|
|
||||||
The placeholder capsule and stub wind are gone. `python3 server.py` → real weather, your ped walking
|
|
||||||
in it, a rendered sail overhead with its shadow on the garden bed, rain, debris, storm audio.
|
|
||||||
Selftest **121/0/0** after the assembly — nobody's suite moved. 0.63 ms/frame in mid-storm_02
|
|
||||||
(0.17 sim + 0.45 render) against a 16.67 ms budget, 120 k tris / 74 draw calls, so there is a LOT of
|
|
||||||
headroom to spend. Note my clone runs `--port 8811` (8801 and 8809 are held by other sessions).
|
|
||||||
|
|
||||||
[A] 2026-07-16 — **It works. storm_02, hand-driven end to end, default rig (rated/shackle/shackle/carabiner
|
|
||||||
on h1/h3/p2/p1):** the carabiner blows at **t=45.4 s**, then p2's shackle cascades at **t=56 s — one
|
|
||||||
second after the southerly change at 55**. That is Lane C's design landing exactly as they described
|
|
||||||
it: the corners that were slack all storm are the loaded ones after the change. Coverage over the bed
|
|
||||||
is **1.0 with the rig intact and 0.0 once two corners are gone** — the whole game in one number.
|
|
||||||
Peak corner load 5427 N; cloth never went non-finite. Nothing here is asserted-only; I drove it.
|
|
||||||
|
|
||||||
[A] 2026-07-16 — ❗ **LANE B — two things about wiring your sail, one is a real trap.**
|
|
||||||
· `createSailView(rig)` reads `rig.pos`/`rig.tris`, which don't exist until `attach()` allocates
|
|
||||||
them in `_build()`. Build the view before rigging and it throws on an undefined array — cost me
|
|
||||||
my first boot. Not asking you to change it; just documenting the order.
|
|
||||||
· **Call `SHADES.rigSail(anchorIds, hwChoices, tension)`, NOT `rig.attach()` directly**, from your
|
|
||||||
picking adapter. `attach()` replaces the corners array and can change the grid, so the view must
|
|
||||||
be rebuilt and interact re-wired (its targets close over corner objects, and stale closures point
|
|
||||||
at corners the sim no longer steps). `rigSail()` does attach + view rebuild + re-wire behind one
|
|
||||||
door, and it's `async`. Ids are stable so re-wiring replaces rather than stacking duplicates.
|
|
||||||
· Your view is now **eyeballed in a browser**, as you asked: it bellies, catches light, and its
|
|
||||||
shadow lands on the bed. Screenshot going in DESIGN.md with the assembled-yard sheet.
|
|
||||||
· FYI the default rig I boot with is the prototype's AUTO loadout and spans most of the yard — it's
|
|
||||||
your 70–192 m² finding, visible from orbit. Decision 2 (my step 6) shrinks it; not a cloth fault.
|
|
||||||
|
|
||||||
[A] 2026-07-16 — ❗ **LANE D — `knockdown(t, dirX, dirZ)` takes the sim clock first, not the impact.**
|
|
||||||
Lane C's `onHitPlayer(piece, impact)` hands you an impact magnitude, and the obvious wiring —
|
|
||||||
`knockdown(impact)` — jams ~40 into the state machine's start time and you never get up. I wired it
|
|
||||||
`knockdown(windT, piece.vx, piece.vz)`, so you also fall the way the crate was travelling. Flagging
|
|
||||||
in case anything else calls it. Also: `player.pos` is a plain `{x,y,z}`, not a `Vector3` — contracts.js
|
|
||||||
says Vector3. Everything only reads `.x/.y/.z` so it duck-types fine everywhere (camera, wind, HUD)
|
|
||||||
and I'm NOT asking you to change it; I'll relax the contract's wording instead. Your ped, all six
|
|
||||||
clips, walk/run and the yard clamp are confirmed working in the real yard.
|
|
||||||
|
|
||||||
[A] 2026-07-16 — ✅ **LANE C — your §Lane C.5 ask, answered with evidence: `dispose()` restores the lights
|
|
||||||
exactly.** Tested inside the real main.js scene, not a bench. After a 40 s storm_02 dragged sun to
|
|
||||||
1.067 and hemi to 1.132, a bare `sky.dispose()` with no rebuild put them back at **exactly 2.0 and
|
|
||||||
1.8**. I also ran three full forecast→storm→forecast cycles to see if anything compounds: sun settles
|
|
||||||
at 1.939 → 1.941 → 1.951, i.e. converging on storm_01's calm-day target, not decaying. No leak, no
|
|
||||||
flicker. Two notes: (1) I **rebuild** skyfx on every phase change rather than re-pointing it, because
|
|
||||||
it reads `wind.def.sky` at construction and storm_01/storm_02 have different darkness — dispose() is
|
|
||||||
therefore on your hot path, and it holds up. (2) `dispose()` restores sun/hemi but leaves `scene.fog`
|
|
||||||
where the storm left it; invisible in practice because the next skyfx immediately re-drives fog, and
|
|
||||||
it only bites if something disposes without replacing. Your call whether that's worth a line.
|
|
||||||
Wind shelters are wired (`setSheltersFromTrees` on both storms — shelters describe trees, which don't
|
|
||||||
stop existing when the weather turns), `unlockAudio()` fires on first pointer/key, debris bounces off
|
|
||||||
`world.heightAt`, and all four of Lane E's crate/tub GLBs load into `setModels`.
|
|
||||||
|
|
||||||
[A] 2026-07-16 — 🔧 `SHADES.step(dt)` and `SHADES.render()` are exposed on the debug api. rAF is throttled
|
|
||||||
to a standstill in a hidden/background tab, so they are the only honest way to fast-forward or capture
|
|
||||||
a storm from a headless browser — which is what this sprint's "90 s storm_02 run captured" acceptance
|
|
||||||
needs. Same code path the rAF loop uses; no test-only branch that can drift. Everything I reported
|
|
||||||
above was measured through them.
|
|
||||||
|
|||||||
@ -36,13 +36,7 @@
|
|||||||
</script>
|
</script>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import { boot } from './js/main.js';
|
import { boot } from './js/main.js';
|
||||||
// boot() is async now — it fetches two storm defs, the ped, the clip pack
|
boot();
|
||||||
// and Lane E's debris GLBs. Surface a failure on the page rather than
|
|
||||||
// letting it die as an unhandled rejection behind a blue screen.
|
|
||||||
boot().catch((err) => {
|
|
||||||
console.error(err);
|
|
||||||
document.getElementById('dev').textContent = `BOOT FAILED — ${err.message} (see console)`;
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -1,32 +1,18 @@
|
|||||||
/**
|
/**
|
||||||
* SHADES — boot, game loop, phase machine. Lane A owns this file.
|
* SHADES — boot, game loop, phase machine. Lane A owns this file.
|
||||||
*
|
*
|
||||||
* This is the assembly point: every other lane's module is proven in isolation,
|
* Nothing is auto-run on import: index.html calls boot(). That keeps createGame()
|
||||||
* and this file is where they become one game. Two rules make that possible and
|
* importable from selftest.html, which must never construct a WebGLRenderer.
|
||||||
* are worth not breaking:
|
|
||||||
*
|
*
|
||||||
* - **Nothing auto-runs on import.** index.html calls boot(). That keeps
|
* The loop is a fixed-dt accumulator. Sim modules only ever see FIXED_DT, never
|
||||||
* createGame() importable from selftest.html, which must never construct a
|
* a real frame delta — that is the whole reason selftest can fast-forward a 90 s
|
||||||
* WebGLRenderer.
|
* storm in a few milliseconds and get the same numbers the player got.
|
||||||
* - **The loop is a fixed-dt accumulator.** Sim modules only ever see FIXED_DT,
|
|
||||||
* never a real frame delta. That is the whole reason selftest can fast-forward
|
|
||||||
* a 90 s storm in milliseconds and get the numbers the player got.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as THREE from '../vendor/three.module.js';
|
import * as THREE from '../vendor/three.module.js';
|
||||||
import { FIXED_DT, PHASES, STORM_LEN, HARDWARE, Emitter } from './contracts.js';
|
import { FIXED_DT, PHASES, STORM_LEN, YARD, Emitter, createStubWind } from './contracts.js';
|
||||||
import { createWorld } from './world.js';
|
import { createWorld, heightAt } from './world.js';
|
||||||
import { createCameraRig } from './camera.js';
|
import { createCameraRig } from './camera.js';
|
||||||
import { loadStorm, createWind } from './weather.js';
|
|
||||||
import { SailRig, createSailView } from './sail.js';
|
|
||||||
import { createPlayer } from './player.js';
|
|
||||||
import { Interact, wireYardActions } from './interact.js';
|
|
||||||
import { createDebris } from './debris.js';
|
|
||||||
import { createSkyFx } from './skyfx.js';
|
|
||||||
|
|
||||||
/** Which storm each phase runs under (SPRINT2 §Lane A.1). */
|
|
||||||
const CALM_STORM = 'storm_01_gentle';
|
|
||||||
const WILD_STORM = 'storm_02_wildnight';
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Phase machine
|
// Phase machine
|
||||||
@ -72,91 +58,81 @@ export function createGame() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Wind router
|
// M0 placeholder player
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* One wind object whose identity never changes, delegating to whichever storm
|
* A 1.7 m capsule that walks. This exists ONLY so the camera has something to
|
||||||
* is currently running.
|
* follow and the yard scale is legible before Lane D lands.
|
||||||
*
|
*
|
||||||
* Every consumer binds to wind exactly once, at construction — the yard closes
|
* Lane D: replace the call site in boot() with your player.js factory — the
|
||||||
* over it for tree sway, createPlayer takes it in opts, createDebris reads its
|
* shape you need to satisfy is `Player` in contracts.js ({pos, carrying, busy,
|
||||||
* event stream. So swapping storm_01 for storm_02 at the phase change has to be
|
* update}) — then delete this function. Everything else in this file already
|
||||||
* a re-point, not a re-wire, or half the game would still be sampling the calm
|
* talks to you through that contract, so nothing else should need to change.
|
||||||
* day while the other half is in a gale.
|
|
||||||
*
|
|
||||||
* Shelters are applied to every storm rather than just the active one: they
|
|
||||||
* describe the yard's trees, which don't stop existing when the weather turns.
|
|
||||||
*
|
|
||||||
* @param {object[]} all every wind this session can switch between
|
|
||||||
*/
|
*/
|
||||||
function createWindRouter(all) {
|
function createPlaceholderPlayer(scene, world, cameraRig) {
|
||||||
let active = all[0];
|
const WALK = 2.2, RUN = 4.5; // m/s
|
||||||
|
|
||||||
const router = {
|
const mesh = new THREE.Mesh(
|
||||||
/** The wind currently in force. Assign through use(). */
|
new THREE.CapsuleGeometry(0.28, 1.14, 4, 12),
|
||||||
get active() { return active; },
|
new THREE.MeshStandardMaterial({ color: 0xffd27a, roughness: 0.7 }),
|
||||||
use(w) { active = w; return router; },
|
);
|
||||||
|
mesh.name = 'player_placeholder';
|
||||||
|
mesh.castShadow = true;
|
||||||
|
scene.add(mesh);
|
||||||
|
|
||||||
sample: (pos, t, out) => active.sample(pos, t, out),
|
const keys = new Set();
|
||||||
speedAt: (pos, t) => active.speedAt(pos, t),
|
const onDown = (e) => {
|
||||||
gustTelegraph: (t) => active.gustTelegraph(t),
|
keys.add(e.key.toLowerCase());
|
||||||
eventsBetween: (a, b) => active.eventsBetween(a, b),
|
if ([' ', 'arrowup', 'arrowdown', 'arrowleft', 'arrowright'].includes(e.key.toLowerCase())) e.preventDefault();
|
||||||
rainAt: (t) => active.rainAt(t),
|
|
||||||
dirAt: (t) => active.dirAt(t),
|
|
||||||
|
|
||||||
setShelters(list) {
|
|
||||||
for (const w of all) w.setShelters(list);
|
|
||||||
return router;
|
|
||||||
},
|
|
||||||
setSheltersFromTrees(trees, o = {}) {
|
|
||||||
return router.setShelters(trees.map((tr) => ({
|
|
||||||
x: tr.pos ? tr.pos.x : tr.x,
|
|
||||||
z: tr.pos ? tr.pos.z : tr.z,
|
|
||||||
radius: o.radius ?? tr.radius ?? 3,
|
|
||||||
strength: o.strength ?? 0.45,
|
|
||||||
length: o.length ?? 14,
|
|
||||||
})));
|
|
||||||
},
|
|
||||||
|
|
||||||
get duration() { return active.duration; },
|
|
||||||
get gusts() { return active.gusts; },
|
|
||||||
get def() { return active.def; },
|
|
||||||
get seed() { return active.seed; },
|
|
||||||
get core() { return active.core; },
|
|
||||||
};
|
};
|
||||||
return router;
|
const onUp = (e) => keys.delete(e.key.toLowerCase());
|
||||||
}
|
addEventListener('keydown', onDown);
|
||||||
|
addEventListener('keyup', onUp);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
const pos = new THREE.Vector3(0, heightAt(0, 6), 6);
|
||||||
// Debris models
|
const move = new THREE.Vector3();
|
||||||
// ---------------------------------------------------------------------------
|
const fwd = new THREE.Vector3();
|
||||||
|
const right = new THREE.Vector3();
|
||||||
|
let facing = 0;
|
||||||
|
|
||||||
/**
|
const hx = YARD.width / 2 - 0.5, hz = YARD.depth / 2 - 0.5;
|
||||||
* Lane E's crates and tubs, keyed by the names storm JSON spawns and debris.js
|
|
||||||
* has radii for. A browser can't glob a directory, so the list is explicit —
|
|
||||||
* and it should stay matched to MODEL_SPEC in debris.js (Lane C's ask: tell them
|
|
||||||
* rather than fighting the radii).
|
|
||||||
*
|
|
||||||
* Missing files are not fatal: debris.js falls back to a graybox box per piece,
|
|
||||||
* which is exactly the degrade-quietly behaviour Lane C designed for.
|
|
||||||
*/
|
|
||||||
const DEBRIS_MODELS = ['BlueCrate_v2', 'BlackTub_v2', 'WhiteTub_v2', 'WoodenBin_v2'];
|
|
||||||
|
|
||||||
async function loadDebrisModels() {
|
return {
|
||||||
const { GLTFLoader } = await import('../vendor/addons/loaders/GLTFLoader.js');
|
pos,
|
||||||
const loader = new GLTFLoader();
|
carrying: null,
|
||||||
const out = {};
|
busy: false,
|
||||||
await Promise.all(DEBRIS_MODELS.map(async (name) => {
|
mesh,
|
||||||
try {
|
|
||||||
const gltf = await loader.loadAsync(`./models/debris/${name}.glb`);
|
update(dt) {
|
||||||
gltf.scene.traverse((o) => { if (o.isMesh) { o.castShadow = true; o.receiveShadow = true; } });
|
const yaw = cameraRig.yaw;
|
||||||
out[name] = gltf.scene;
|
// Camera-relative: forward is where the camera is looking, flattened.
|
||||||
} catch (err) {
|
fwd.set(-Math.sin(yaw), 0, -Math.cos(yaw));
|
||||||
console.warn(`[main] debris model ${name} unavailable, using graybox:`, err.message);
|
right.set(Math.cos(yaw), 0, -Math.sin(yaw));
|
||||||
}
|
|
||||||
}));
|
move.set(0, 0, 0);
|
||||||
return out;
|
if (keys.has('w') || keys.has('arrowup')) move.add(fwd);
|
||||||
|
if (keys.has('s') || keys.has('arrowdown')) move.sub(fwd);
|
||||||
|
if (keys.has('d') || keys.has('arrowright')) move.add(right);
|
||||||
|
if (keys.has('a') || keys.has('arrowleft')) move.sub(right);
|
||||||
|
|
||||||
|
if (move.lengthSq() > 0) {
|
||||||
|
move.normalize().multiplyScalar((keys.has('shift') ? RUN : WALK) * dt);
|
||||||
|
pos.x = Math.max(-hx, Math.min(hx, pos.x + move.x));
|
||||||
|
pos.z = Math.max(-hz, Math.min(hz, pos.z + move.z));
|
||||||
|
facing = Math.atan2(move.x, move.z);
|
||||||
|
}
|
||||||
|
pos.y = world.heightAt(pos.x, pos.z);
|
||||||
|
|
||||||
|
mesh.position.set(pos.x, pos.y + 0.85, pos.z); // capsule centre
|
||||||
|
mesh.rotation.y = facing;
|
||||||
|
},
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
removeEventListener('keydown', onDown);
|
||||||
|
removeEventListener('keyup', onUp);
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@ -167,7 +143,7 @@ async function loadDebrisModels() {
|
|||||||
* @param {object} [opts]
|
* @param {object} [opts]
|
||||||
* @param {HTMLCanvasElement} [opts.canvas]
|
* @param {HTMLCanvasElement} [opts.canvas]
|
||||||
*/
|
*/
|
||||||
export async function boot(opts = {}) {
|
export function boot(opts = {}) {
|
||||||
const canvas = opts.canvas ?? document.getElementById('c');
|
const canvas = opts.canvas ?? document.getElementById('c');
|
||||||
|
|
||||||
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
|
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
|
||||||
@ -179,154 +155,33 @@ export async function boot(opts = {}) {
|
|||||||
|
|
||||||
const scene = new THREE.Scene();
|
const scene = new THREE.Scene();
|
||||||
|
|
||||||
// --- 1. weather ---------------------------------------------------------
|
// Lane C: swap createStubWind() for your createWeather(). Everything that
|
||||||
// Both storms load up front: the forecast card needs to read storm_02's shape
|
// moves reads this one object, so that swap is the whole integration.
|
||||||
// before the player has agreed to face it.
|
const wind = createStubWind({ calm: true });
|
||||||
const [calmDef, wildDef] = await Promise.all([loadStorm(CALM_STORM), loadStorm(WILD_STORM)]);
|
|
||||||
const calmWind = createWind(calmDef);
|
|
||||||
const wildWind = createWind(wildDef);
|
|
||||||
const wind = createWindRouter([calmWind, wildWind]);
|
|
||||||
|
|
||||||
// --- world & camera -----------------------------------------------------
|
|
||||||
const world = createWorld(scene, { wind });
|
const world = createWorld(scene, { wind });
|
||||||
const cameraRig = createCameraRig(canvas);
|
const cameraRig = createCameraRig(canvas);
|
||||||
cameraRig.setSolids(world.solids);
|
cameraRig.setSolids(world.solids);
|
||||||
cameraRig.setGround(world.heightAt);
|
cameraRig.setGround(world.heightAt);
|
||||||
|
|
||||||
// Lane C: trees don't shelter anything until they're told where they are.
|
const player = createPlaceholderPlayer(scene, world, cameraRig);
|
||||||
wind.setSheltersFromTrees(world.anchors.filter((a) => a.type === 'tree'));
|
|
||||||
|
|
||||||
// --- 2. player ----------------------------------------------------------
|
|
||||||
const interact = new Interact();
|
|
||||||
const player = await createPlayer(scene, world, cameraRig, { wind, interact });
|
|
||||||
|
|
||||||
// --- 3. sail ------------------------------------------------------------
|
|
||||||
const rig = new SailRig({ anchors: world.anchors });
|
|
||||||
let sailView = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attach the cloth across 4 anchors and (re)build its view.
|
|
||||||
*
|
|
||||||
* The order here is load-bearing. createSailView reads rig.pos and rig.tris,
|
|
||||||
* which don't exist until attach() allocates them in _build() — build the view
|
|
||||||
* first and it throws on an undefined array. A re-rig can also change the grid,
|
|
||||||
* so the view has to be rebuilt rather than reused. Both facts make this the
|
|
||||||
* single door that boot and Lane B's picking adapter should come through.
|
|
||||||
*
|
|
||||||
* Re-wiring interact each time is deliberate: its targets close over corner
|
|
||||||
* objects and attach() makes a fresh corners array, so stale closures would
|
|
||||||
* point at corners the sim no longer steps. The ids are stable, so this
|
|
||||||
* replaces the old targets rather than stacking duplicates.
|
|
||||||
*/
|
|
||||||
async function rigSail(anchorIds, hwChoices, tension = 1.0) {
|
|
||||||
rig.attach(anchorIds, hwChoices, tension);
|
|
||||||
if (sailView) {
|
|
||||||
scene.remove(sailView);
|
|
||||||
sailView.traverse((o) => { o.geometry?.dispose(); o.material?.dispose(); });
|
|
||||||
}
|
|
||||||
sailView = await createSailView(rig);
|
|
||||||
scene.add(sailView);
|
|
||||||
wireYardActions(interact, { sailRig: rig, world });
|
|
||||||
return sailView;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Until Lane B's prep-phase picking adapter lands (SPRINT2 §B.3), rig a
|
|
||||||
// default quad so the yard has a live sail and Lane D has something to
|
|
||||||
// repair. Deliberately the prototype's AUTO loadout — one dodgy carabiner
|
|
||||||
// corner. It also spans most of the yard, which is the 70–192 m² problem
|
|
||||||
// decision 2 fixes in step 6, not a fault in the cloth.
|
|
||||||
await rigSail(['h1', 'h3', 'p2', 'p1'], [HARDWARE[2], HARDWARE[1], HARDWARE[1], HARDWARE[0]]);
|
|
||||||
|
|
||||||
const game = createGame();
|
const game = createGame();
|
||||||
|
|
||||||
// --- clocks -------------------------------------------------------------
|
// --- dev overlay (temporary — Lane A's hud.js replaces it after M0) ----
|
||||||
// Two of them, and the distinction matters. `simT` is wall-clock seconds since
|
|
||||||
// boot. `windT` is STORM time — storm JSON is authored with t=0 at the storm's
|
|
||||||
// first gust, so it's phase time during the storm, and off-storm it wraps the
|
|
||||||
// calm day around its own duration so the breeze keeps breathing however long
|
|
||||||
// you spend rigging. Every sim module samples windT; nothing samples simT.
|
|
||||||
let simT = 0;
|
|
||||||
let windT = 0;
|
|
||||||
let acc = 0;
|
|
||||||
|
|
||||||
function windTime() {
|
|
||||||
if (game.phase === 'storm') return game.phaseT;
|
|
||||||
return simT % Math.max(1, calmWind.duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- 4. sky, audio, debris ---------------------------------------------
|
|
||||||
const events = [];
|
|
||||||
const pushEvent = (text) => {
|
|
||||||
events.push({ t: game.phaseT, text });
|
|
||||||
if (events.length > 4) events.shift();
|
|
||||||
};
|
|
||||||
|
|
||||||
const debris = createDebris({
|
|
||||||
wind,
|
|
||||||
scene,
|
|
||||||
heightAt: world.heightAt,
|
|
||||||
// knockdown(t, dirX, dirZ) — the first arg is the sim clock, NOT the impact
|
|
||||||
// magnitude. Passing `impact` here would jam ~40 into the state machine's
|
|
||||||
// start time and the player would never get up. The piece's own velocity is
|
|
||||||
// the direction, so you fall the way the crate was travelling.
|
|
||||||
onHitPlayer: (piece) => player.sim.knockdown(windT, piece.vx, piece.vz),
|
|
||||||
onEvent: pushEvent,
|
|
||||||
});
|
|
||||||
debris.setModels(await loadDebrisModels());
|
|
||||||
|
|
||||||
// skyfx reads the storm's `sky` block at construction (darkness, cloud scroll,
|
|
||||||
// night), so it is rebuilt when the storm changes rather than re-pointed like
|
|
||||||
// wind. dispose() hands world.sun/world.hemi back exactly as they were, which
|
|
||||||
// is what makes that safe to do mid-session.
|
|
||||||
let sky = null;
|
|
||||||
let audioUnlocked = false;
|
|
||||||
function makeSky() {
|
|
||||||
if (sky) sky.dispose();
|
|
||||||
sky = createSkyFx({
|
|
||||||
scene,
|
|
||||||
camera: cameraRig.object,
|
|
||||||
wind,
|
|
||||||
sun: world.sun,
|
|
||||||
hemi: world.hemi,
|
|
||||||
onEvent: pushEvent,
|
|
||||||
});
|
|
||||||
if (audioUnlocked) sky.unlockAudio();
|
|
||||||
return sky;
|
|
||||||
}
|
|
||||||
makeSky();
|
|
||||||
|
|
||||||
// Browsers won't start an AudioContext without a gesture. Without this the
|
|
||||||
// storm is silent, and half of DESIGN.md's threat model is audible.
|
|
||||||
const unlock = () => {
|
|
||||||
if (audioUnlocked) return;
|
|
||||||
audioUnlocked = true;
|
|
||||||
sky?.unlockAudio();
|
|
||||||
removeEventListener('pointerdown', unlock);
|
|
||||||
removeEventListener('keydown', unlock);
|
|
||||||
};
|
|
||||||
addEventListener('pointerdown', unlock);
|
|
||||||
addEventListener('keydown', unlock);
|
|
||||||
|
|
||||||
// --- dev overlay (temporary — hud.js replaces it in step 7) -------------
|
|
||||||
const hud = document.getElementById('dev');
|
const hud = document.getElementById('dev');
|
||||||
const banner = document.getElementById('banner');
|
const banner = document.getElementById('banner');
|
||||||
addEventListener('keydown', (e) => {
|
|
||||||
if (e.key === 'Enter') game.advance();
|
|
||||||
});
|
|
||||||
|
|
||||||
// --- phases -------------------------------------------------------------
|
|
||||||
game.on('phaseChange', ({ to }) => {
|
game.on('phaseChange', ({ to }) => {
|
||||||
wind.use(to === 'storm' ? wildWind : calmWind);
|
|
||||||
makeSky();
|
|
||||||
events.length = 0;
|
|
||||||
if (banner) {
|
if (banner) {
|
||||||
banner.textContent = to.toUpperCase();
|
banner.textContent = to.toUpperCase();
|
||||||
banner.style.opacity = '1';
|
banner.style.opacity = '1';
|
||||||
setTimeout(() => { banner.style.opacity = '0'; }, 1400);
|
setTimeout(() => { banner.style.opacity = '0'; }, 1400);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter') game.advance();
|
||||||
|
});
|
||||||
|
|
||||||
// --- resize -------------------------------------------------------------
|
// --- resize ------------------------------------------------------------
|
||||||
function resize() {
|
function resize() {
|
||||||
const w = canvas.clientWidth || innerWidth;
|
const w = canvas.clientWidth || innerWidth;
|
||||||
const h = canvas.clientHeight || innerHeight;
|
const h = canvas.clientHeight || innerHeight;
|
||||||
@ -336,19 +191,18 @@ export async function boot(opts = {}) {
|
|||||||
addEventListener('resize', resize);
|
addEventListener('resize', resize);
|
||||||
resize();
|
resize();
|
||||||
|
|
||||||
// --- loop ---------------------------------------------------------------
|
// --- loop --------------------------------------------------------------
|
||||||
const clock = new THREE.Clock();
|
const clock = new THREE.Clock();
|
||||||
|
let acc = 0;
|
||||||
|
let simT = 0;
|
||||||
let frames = 0, fpsT = 0, fps = 0;
|
let frames = 0, fpsT = 0, fps = 0;
|
||||||
|
|
||||||
function step(dt) {
|
function step(dt, t) {
|
||||||
game.tick(dt);
|
game.tick(dt);
|
||||||
simT += dt;
|
world.update(dt, t);
|
||||||
windT = windTime();
|
player.update(dt, t);
|
||||||
world.update(dt, windT);
|
// Lane B: sailRig.step(dt, wind, t) goes here.
|
||||||
player.update(dt, windT);
|
// Lane C: debris.step(dt, wind, t) goes here.
|
||||||
rig.step(dt, wind, windT);
|
|
||||||
debris.step(dt, windT, { player: player.sim, sail: rig });
|
|
||||||
sky?.step(dt, windT, { sail: rig });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function frame() {
|
function frame() {
|
||||||
@ -358,55 +212,28 @@ export async function boot(opts = {}) {
|
|||||||
acc += raw;
|
acc += raw;
|
||||||
let guard = 0;
|
let guard = 0;
|
||||||
while (acc >= FIXED_DT && guard++ < 60) {
|
while (acc >= FIXED_DT && guard++ < 60) {
|
||||||
step(FIXED_DT);
|
step(FIXED_DT, simT);
|
||||||
|
simT += FIXED_DT;
|
||||||
acc -= FIXED_DT;
|
acc -= FIXED_DT;
|
||||||
}
|
}
|
||||||
|
|
||||||
cameraRig.update(raw, player.pos);
|
cameraRig.update(raw, player.pos);
|
||||||
sailView?.update();
|
|
||||||
renderer.render(scene, cameraRig.object);
|
renderer.render(scene, cameraRig.object);
|
||||||
|
|
||||||
frames++; fpsT += raw;
|
frames++; fpsT += raw;
|
||||||
if (fpsT >= 0.5) { fps = frames / fpsT; frames = 0; fpsT = 0; }
|
if (fpsT >= 0.5) { fps = frames / fpsT; frames = 0; fpsT = 0; }
|
||||||
if (hud) {
|
if (hud) {
|
||||||
const wt = windT;
|
const w = wind.sample(player.pos, simT);
|
||||||
const speed = wind.speedAt(player.pos, wt);
|
|
||||||
const tel = wind.gustTelegraph(wt);
|
|
||||||
const worst = rig.corners.reduce((m, c) => Math.max(m, c.load || 0), 0);
|
|
||||||
hud.textContent =
|
hud.textContent =
|
||||||
`${fps.toFixed(0)} fps | ${game.phase} ${game.phaseT.toFixed(1)}s | ` +
|
`${fps.toFixed(0)} fps | phase ${game.phase} ${game.phaseT.toFixed(1)}s | ` +
|
||||||
`wind ${speed.toFixed(1)} m/s${tel ? ` | GUST in ${tel.eta.toFixed(1)}s` : ''} | ` +
|
`wind ${w.length().toFixed(1)} m/s | t ${simT.toFixed(1)}s`;
|
||||||
`worst corner ${worst.toFixed(1)} | debris ${debris.pieces.length}` +
|
|
||||||
`${events.length ? ` | ${events[events.length - 1].text}` : ''}`;
|
|
||||||
}
|
}
|
||||||
requestAnimationFrame(frame);
|
requestAnimationFrame(frame);
|
||||||
}
|
}
|
||||||
requestAnimationFrame(frame);
|
requestAnimationFrame(frame);
|
||||||
|
|
||||||
// Handy for poking at the world from the console, and for the selftest-free
|
// Handy for poking at the world from the console.
|
||||||
// hand checks the sprint's acceptance actually turns on.
|
const api = { renderer, scene, world, cameraRig, player, game, wind, get simT() { return simT; } };
|
||||||
const api = {
|
|
||||||
renderer, scene, world, cameraRig, player, game, wind, rig, rigSail,
|
|
||||||
get sailView() { return sailView; },
|
|
||||||
debris, interact, events,
|
|
||||||
get sky() { return sky; },
|
|
||||||
get simT() { return simT; },
|
|
||||||
windTime,
|
|
||||||
calmWind, wildWind,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Drive the sim by hand at fixed dt, and draw on demand. rAF is throttled to
|
|
||||||
* a standstill in a hidden tab, so these are the only honest way to
|
|
||||||
* fast-forward a storm or capture one from a headless browser — which is
|
|
||||||
* exactly what this sprint's "90 s storm_02 run captured" acceptance needs.
|
|
||||||
* Same code path the rAF loop uses; no test-only branch to drift.
|
|
||||||
*/
|
|
||||||
step,
|
|
||||||
render() {
|
|
||||||
sailView?.update();
|
|
||||||
renderer.render(scene, cameraRig.object);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
globalThis.SHADES = api;
|
globalThis.SHADES = api;
|
||||||
return api;
|
return api;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -343,11 +343,7 @@ export function createWorld(scene, opts = {}) {
|
|||||||
sunDir: SUN_DIR.clone(),
|
sunDir: SUN_DIR.clone(),
|
||||||
solids,
|
solids,
|
||||||
root,
|
root,
|
||||||
// Lane C's skyfx MODULATES these as the storm builds and hands them back
|
|
||||||
// untouched on dispose() — it doesn't own them. That's why the yard exposes
|
|
||||||
// its lights rather than keeping them private.
|
|
||||||
sun,
|
sun,
|
||||||
hemi,
|
|
||||||
|
|
||||||
/** @param {string} id */
|
/** @param {string} id */
|
||||||
anchor(id) {
|
anchor(id) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user