[lane F] Mid-round-2 recovery: the glue that makes L2 a game
All round-2 lane sessions were cut off 15:08-15:16 before NOTES/commits; the three prior commits preserve their work verbatim. This commit is the wiring B was cut off inside of, plus the run-state boot owes per ruling #4: - combat/index.js: fiction-id -> archetype resolve via C's getEnemy() (the round's declared ONE blocker), theta-array + spread fan-out for group spawns, hazards + pickups constructed and updated (order: enemies -> weapons -> hazards -> pickups, so a torpedo can neutralize a surge the frame it detonates), pickup score/samples onto the combat:state scoreboard, combat.reset(fromS) for respawn. - flight/player.js: kill/shove/refill — the API B's new modules call; kill ignores iframes by design (boost-dodging through C's lethal acid wall is not a read). Deleted the crestSpeed fallback guard per its own comment — A's law landed. - boot.js: run state. checkpoint crossed -> remembered; death -> 2s -> respawn at it + combat.reset() re-arms hazard timelines (pump deliberately does NOT rewind: no double-spawns, collected stays collected); gate -> level:complete {stats, par} for E's medal card. Exported step(dt) so the stepped-sim harness drives the REAL loop. - ROUND2_INSTRUCTIONS: MID-ROUND STATUS box for the resuming lanes; f-progress entry. Verified (deterministic stepped sim via boot.step, :8140): full L2 run fires all 47 events with ZERO console warns (was: 11 spawn no-ops), all 5 hazard telegraphs, 2400 proximity ticks, 14 pickups collected, finish 157s vs par 180 while surfing 94s of it. Death path: throttle 0.7 into the finale -> killed by acid s=3386 -> respawn at Cardia Approach 2980 -> surge re-arms -> second attempt escapes -> level:complete {deaths: 1}. Survivable AND losable. Evidence: docs/shots/laneF/round2_L2_integrated_full_run.png. qa.sh GREEN. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
2e85ec0622
commit
29cca53675
@ -1,5 +1,35 @@
|
|||||||
# GUTS — Round 2 lane instructions (from Fable, integrator)
|
# GUTS — Round 2 lane instructions (from Fable, integrator)
|
||||||
|
|
||||||
|
> ## ⚡ MID-ROUND STATUS (F, 2026-07-16 evening) — read this first if you are resuming
|
||||||
|
>
|
||||||
|
> All round-2 sessions were **cut off 15:08–15:16** before NOTES or commits. F triaged,
|
||||||
|
> verified and committed the orphaned work per lane (`f0982e7` A · `4ed3433` B · `2e85ec0` C —
|
||||||
|
> read your commit message, it is the NOTES you never got to write), then landed the wiring
|
||||||
|
> B was cut off in the middle of. **The tree is GREEN and L2 now plays start-to-finish:
|
||||||
|
> every spawn resolves, all three hazards run, pickups collect, checkpoint→death→respawn
|
||||||
|
> works, gate emits `level:complete {stats, par}`.** Proof: f-progress + laneF shots.
|
||||||
|
>
|
||||||
|
> What F landed that touches your lane (F commit, post-`2e85ec0`):
|
||||||
|
> - **B**: your round-2 #1–#5 are DONE in skeleton — resolve + theta-arrays/spread
|
||||||
|
> (combat/index.js), hazards+pickups wired, `player.kill/shove/refill` added,
|
||||||
|
> respawn/`level:complete` glue in boot. Your crestSpeed fallback guard is deleted (A
|
||||||
|
> landed the law). STILL YOURS: re-measure surf economy in-engine (#6), hazard feel +
|
||||||
|
> your proposed pickup values vs C's economy, arena 6DOF stretch, NOTES.
|
||||||
|
> - **A**: nothing of yours changed. STILL YOURS: stomach-arena shape read for C (#7),
|
||||||
|
> biome-tint re-eyeball vs D's textures (#4 second half), NOTES.
|
||||||
|
> - **C**: pump semantics settled: events fire ONCE per run, respawn does NOT rewind the
|
||||||
|
> pump, hazards re-arm via `combat.reset(fromS)`. `boot.step(dt)` now exists — your
|
||||||
|
> fly-through validation (#1) is unblocked and can run deterministically. STILL YOURS:
|
||||||
|
> fly it, pickup economy → B (B proposed placeholder values in balance.js §pickups),
|
||||||
|
> L1 design doc (#5), GDD boss folding (#4), NOTES.
|
||||||
|
> - **D/E**: no round-2 files reached the tree before the cut. Your lists below stand
|
||||||
|
> unchanged. E: the `level:complete` payload for your medal card is live and verified.
|
||||||
|
> - Ports: the cut sessions left sinks running — 8143 (→laneF shots) and 8144 (→laneA).
|
||||||
|
> Reuse them or pick a fresh port.
|
||||||
|
>
|
||||||
|
> Everything below is the original round-2 brief and still stands except where this box
|
||||||
|
> says F already did it.
|
||||||
|
|
||||||
Date: 2026-07-16 · Written after reviewing all four round-1 lanes (A/B/C/D — E did not run
|
Date: 2026-07-16 · Written after reviewing all four round-1 lanes (A/B/C/D — E did not run
|
||||||
and joins this round) and integrating their offered wiring. Every round-1 claim I checked
|
and joins this round) and integrating their offered wiring. Every round-1 claim I checked
|
||||||
held up: A's hash `cefc4f83` and 8-draw sweep, B's `node --check` ESM no-op (reproduced on a
|
held up: A's hash `cefc4f83` and 8-draw sweep, B's `node --check` ESM no-op (reproduced on a
|
||||||
|
|||||||
@ -1,5 +1,34 @@
|
|||||||
# Lane F (Fable) — progress
|
# Lane F (Fable) — progress
|
||||||
|
|
||||||
|
## 2026-07-16 (evening) — Mid-round-2 recovery: cut-off triage + the glue that makes L2 a game
|
||||||
|
|
||||||
|
All round-2 lane sessions (A/B/C + one more that left no files) were cut off 15:08–15:16,
|
||||||
|
mid-work, before NOTES/progress/commits. Reviewed the orphaned tree file by file, verified it
|
||||||
|
(qa GREEN, both selfchecks, 60s stepped sim clean), then committed each lane's work path-scoped
|
||||||
|
on their behalf: A `f0982e7` (crest law, taper, schema-v2 wave, TBN walls — essentially done),
|
||||||
|
B `4ed3433` (surf speed-lock + hazards/pickups modules — written but UNWIRED), C `2e85ec0`
|
||||||
|
(schema v2, par retune, chase analysis, --probe — done). Details in each commit message.
|
||||||
|
|
||||||
|
Then landed the integration glue as F (B's unfinished wiring was the round's critical path):
|
||||||
|
fiction-id → archetype resolve via C's `getEnemy()` + theta-array/spread fan-out
|
||||||
|
(combat/index.js), hazards + pickups constructed and updated there too, `player.kill/shove/
|
||||||
|
refill` (the API B's new modules call — it didn't exist), boot run-state (checkpoint → death →
|
||||||
|
2s → respawn at last checkpoint + `combat.reset()` re-arms hazard timelines; gate →
|
||||||
|
`level:complete {stats, par}`), deleted B's crestSpeed fallback guard (A landed the real one),
|
||||||
|
and exported `boot.step(dt)` so the stepped-sim harness drives the REAL loop, pump included.
|
||||||
|
|
||||||
|
Verified on :8140, deterministic stepped sim through boot.step:
|
||||||
|
- Full run at throttle 1.38: **all 47 L2 events fire, 0 console warns** (the round-2 blocker
|
||||||
|
— 11 spawn no-ops — is gone), all 5 hazard telegraphs, 2400 hazard:proximity ticks,
|
||||||
|
14 pickups collected, gate → level:complete {time 157s vs par 180, score 2650, kills 15}.
|
||||||
|
- Death path: dawdle at 0.7 into the finale → killed by acid at s=3386 → respawns at
|
||||||
|
checkpoint 2980 (Cardia Approach, exactly as C authored) → surge re-arms → second attempt
|
||||||
|
escapes → level:complete {deaths: 1}. **L2 is survivable AND losable.**
|
||||||
|
Evidence: docs/shots/laneF/round2_L2_integrated_full_run.png (posted via the 8143 sink).
|
||||||
|
|
||||||
|
Still open for the resumed lanes: see ROUND2_INSTRUCTIONS §MID-ROUND STATUS. Port note:
|
||||||
|
8143 (laneF sink) and 8144 (laneA sink) are still held by the cut-off sessions' processes.
|
||||||
|
|
||||||
## 2026-07-16 — Round 1 review + integration (round 2 opened)
|
## 2026-07-16 — Round 1 review + integration (round 2 opened)
|
||||||
|
|
||||||
Reviewed all four lanes; every checked claim held (A's hash cefc4f83 + 8-draw sweep, B's
|
Reviewed all four lanes; every checked claim held (A's hash cefc4f83 + 8-draw sweep, B's
|
||||||
|
|||||||
BIN
docs/shots/laneF/round2_L2_integrated_full_run.png
Normal file
BIN
docs/shots/laneF/round2_L2_integrated_full_run.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.9 MiB |
@ -58,10 +58,46 @@ const player = flags.fly ? null
|
|||||||
const combat = player ? createCombat({ scene, world, bus, rng, flags, player }) : null;
|
const combat = player ? createCombat({ scene, world, bus, rng, flags, player }) : null;
|
||||||
|
|
||||||
// --- level-event pump (owned by boot per round-2 ruling): emits C's events as the
|
// --- level-event pump (owned by boot per round-2 ruling): emits C's events as the
|
||||||
// player crosses their s. Checkpoint/respawn replay semantics: revisit with B in round 2.
|
// player crosses their s. Each event fires ONCE per run — respawn does not rewind the pump
|
||||||
|
// (no double-spawns, collected pickups stay collected); hazards re-arm via combat.reset(),
|
||||||
|
// which is the one replay a death actually needs (the thing that killed you works again).
|
||||||
let _evts = [...(world.level?.events ?? [])].sort((a, b) => a.s - b.s), _ei = 0;
|
let _evts = [...(world.level?.events ?? [])].sort((a, b) => a.s - b.s), _ei = 0;
|
||||||
const pumpTo = (s) => { while (_ei < _evts.length && s >= _evts[_ei].s) bus.emit('level:event', _evts[_ei++]); };
|
const pumpTo = (s) => { while (_ei < _evts.length && s >= _evts[_ei].s) bus.emit('level:event', _evts[_ei++]); };
|
||||||
|
|
||||||
|
// --- run state: checkpoint -> death -> respawn, gate -> level:complete ------------------
|
||||||
|
// C's authoring laws carry this: checkpoints sit outside hazard zones and <=30s apart, so
|
||||||
|
// "respawn at the last checkpoint crossed" is safe by construction. Stats accumulate across
|
||||||
|
// deaths (a run's story includes its deaths); E renders the medal card off level:complete.
|
||||||
|
const run = { t: 0, deaths: 0, checkpointS: player?.state.s ?? 0, respawnT: 0, done: false };
|
||||||
|
bus.on('level:event', (ev) => {
|
||||||
|
if (ev.type === 'checkpoint') {
|
||||||
|
run.checkpointS = ev.s;
|
||||||
|
bus.emit('audio:cue', { name: 'checkpoint' });
|
||||||
|
} else if (ev.type === 'gate' && !run.done) {
|
||||||
|
run.done = true;
|
||||||
|
bus.emit('level:complete', {
|
||||||
|
id: world.level?.id ?? null, to: ev.to ?? null, gate: ev.id ?? null,
|
||||||
|
stats: {
|
||||||
|
time: run.t, deaths: run.deaths,
|
||||||
|
score: combat?.score.value ?? 0, kills: combat?.score.kills ?? 0,
|
||||||
|
samples: combat?.score.samples ?? 0,
|
||||||
|
},
|
||||||
|
par: world.level?.par ?? null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
bus.on('player:death', () => { run.deaths++; run.respawnT = 2.0; }); // 2s of feed-drop, then back
|
||||||
|
function updateRun(dt) {
|
||||||
|
if (!run.done) run.t += dt;
|
||||||
|
if (run.respawnT > 0 && player) {
|
||||||
|
run.respawnT -= dt;
|
||||||
|
if (run.respawnT <= 0) {
|
||||||
|
combat?.reset(run.checkpointS); // re-arm hazards at/ahead of the checkpoint
|
||||||
|
player.respawn(run.checkpointS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- debug drift/fly camera (?fly=1, or fallback when B's player is absent) ---
|
// --- debug drift/fly camera (?fly=1, or fallback when B's player is absent) ---
|
||||||
let sCam = 5, look = { yaw: 0, pitch: 0 }, dragging = false, speed = world.biomeAt(0).flow * 0.6;
|
let sCam = 5, look = { yaw: 0, pitch: 0 }, dragging = false, speed = world.biomeAt(0).flow * 0.6;
|
||||||
const keys = new Set();
|
const keys = new Set();
|
||||||
@ -116,19 +152,27 @@ addEventListener('resize', () => {
|
|||||||
renderer.setSize(innerWidth, innerHeight);
|
renderer.setSize(innerWidth, innerHeight);
|
||||||
});
|
});
|
||||||
|
|
||||||
let last = performance.now();
|
// One deterministic simulation step — the whole game minus rendering. Exported so the
|
||||||
function frame(now) {
|
// stepped-sim harness (f-progress documents it; rAF never fires in a hidden pane) exercises
|
||||||
const dt = Math.min((now - last) / 1000, 0.05);
|
// the REAL loop — pump, run state and all — instead of a hand-rolled copy that drifts.
|
||||||
last = now;
|
function step(dt) {
|
||||||
if (player) {
|
if (player) {
|
||||||
player.update(dt); // must run first: combat hit-tests this frame's ship position
|
player.update(dt); // must run first: combat hit-tests this frame's ship position
|
||||||
pumpTo(player.state.s);
|
pumpTo(player.state.s);
|
||||||
combat.update(dt);
|
combat.update(dt);
|
||||||
|
updateRun(dt);
|
||||||
world.update(dt, player.state.s);
|
world.update(dt, player.state.s);
|
||||||
} else {
|
} else {
|
||||||
updateCamera(dt); // ?fly=1 noclip / title drift
|
updateCamera(dt); // ?fly=1 noclip / title drift
|
||||||
world.update(dt, sCam);
|
world.update(dt, sCam);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let last = performance.now();
|
||||||
|
function frame(now) {
|
||||||
|
const dt = Math.min((now - last) / 1000, 0.05);
|
||||||
|
last = now;
|
||||||
|
step(dt);
|
||||||
renderer.render(scene, camera);
|
renderer.render(scene, camera);
|
||||||
stats.draws = renderer.info.render.calls;
|
stats.draws = renderer.info.render.calls;
|
||||||
stats.tris = renderer.info.render.triangles;
|
stats.tris = renderer.info.render.triangles;
|
||||||
@ -146,4 +190,4 @@ function frame(now) {
|
|||||||
if (flags.dbg && !flags.shots) dbgEl.style.display = 'block';
|
if (flags.dbg && !flags.shots) dbgEl.style.display = 'block';
|
||||||
requestAnimationFrame(frame);
|
requestAnimationFrame(frame);
|
||||||
|
|
||||||
export { scene, camera, renderer, bus, flags, world, player, combat, assets };
|
export { scene, camera, renderer, bus, flags, world, player, combat, assets, step };
|
||||||
|
|||||||
@ -1,33 +1,61 @@
|
|||||||
// combat/index.js (Lane B) — the createCombat factory. Composes enemies + weapons and
|
// combat/index.js (Lane B) — the createCombat factory. Composes enemies + weapons + hazards
|
||||||
// feeds them Lane C's level events. All the wiring, none of the behaviour.
|
// + pickups and feeds them Lane C's level events. All the wiring, none of the behaviour.
|
||||||
//
|
//
|
||||||
// Update order inside a frame is load-bearing:
|
// Update order inside a frame is load-bearing:
|
||||||
// boot: player.update(dt) -> combat.update(dt)
|
// boot: player.update(dt) -> combat.update(dt)
|
||||||
// here: enemies.update(dt) -> weapons.update(dt)
|
// here: enemies.update(dt) -> weapons.update(dt) -> hazards.update(dt) -> pickups.update(dt)
|
||||||
// Enemies move first so weapons hit-test against this frame's positions, not last frame's.
|
// Enemies move first so weapons hit-test against this frame's positions, not last frame's.
|
||||||
|
// Hazards after weapons so a torpedo fired THIS frame can neutralize a surge THIS frame
|
||||||
|
// (weapons emits level:neutralize on detonation; hazards listens).
|
||||||
|
|
||||||
import { createEnemies } from './enemies.js';
|
import { createEnemies } from './enemies.js';
|
||||||
import { createWeapons } from './weapons.js';
|
import { createWeapons } from './weapons.js';
|
||||||
|
import { createHazards } from './hazards.js';
|
||||||
|
import { createPickups } from './pickups.js';
|
||||||
|
import { getEnemy } from '../levels/enemies.js';
|
||||||
|
|
||||||
export function createCombat({ scene, world, bus, rng, flags = {}, assets = null, player }) {
|
export function createCombat({ scene, world, bus, rng, flags = {}, assets = null, player }) {
|
||||||
const enemies = createEnemies({ scene, world, bus, rng, assets });
|
const enemies = createEnemies({ scene, world, bus, rng, assets });
|
||||||
const weapons = createWeapons({ scene, world, bus, rng, player, targets: enemies });
|
const weapons = createWeapons({ scene, world, bus, rng, player, targets: enemies });
|
||||||
|
const hazards = createHazards({ scene, world, bus, rng, player });
|
||||||
|
const pickups = createPickups({ scene, world, bus, rng, player, weapons });
|
||||||
|
|
||||||
enemies.setBlastFx((pos, size, dur) => weapons.spawnBlast(pos, size, dur));
|
enemies.setBlastFx((pos, size, dur) => weapons.spawnBlast(pos, size, dur));
|
||||||
enemies.setOnDeath((e) => weapons.spawnBlast(e.pos, e.radius * 1.6, 0.3));
|
enemies.setOnDeath((e) => weapons.spawnBlast(e.pos, e.radius * 1.6, 0.3));
|
||||||
|
|
||||||
const score = { value: 0, kills: 0 };
|
const score = { value: 0, kills: 0, samples: 0 };
|
||||||
|
|
||||||
// Lane C's events arrive on the bus as the player crosses their `s`. We only care about
|
// Fan a group of `count` out along s. C's `spread` is the authored span of the group
|
||||||
// `spawn`; checkpoints/hazards/bosses belong to C and E this round. Unknown types are
|
// ("count 4 spread 120" = a field, not a wall); without one, a tight default fan keeps
|
||||||
// ignored on purpose — C may add event kinds without breaking combat.
|
// a count from stacking at a single arclength.
|
||||||
const offEvent = bus.on('level:event', (ev) => {
|
const fan = (ev, i) => {
|
||||||
if (!ev || ev.type !== 'spawn') return;
|
|
||||||
const n = ev.count ?? 1;
|
const n = ev.count ?? 1;
|
||||||
for (let i = 0; i < n; i++) {
|
if (n <= 1) return ev.s ?? 0;
|
||||||
// fan the group out over a short stretch of s so a count of 6 is an encounter,
|
const span = ev.spread ?? (n - 1) * 3.5;
|
||||||
// not a wall of enemies at one arclength
|
return (ev.s ?? 0) + (i / (n - 1)) * span;
|
||||||
enemies.spawn(ev.enemy, { s: (ev.s ?? 0) + i * 3.5, theta: ev.theta ?? null, rho: ev.rho ?? null });
|
};
|
||||||
|
// C authors `theta` as a scalar (whole group) or an array (per-member — "two blooms
|
||||||
|
// covering opposite arcs"). Placement is composition, so C's word is law here.
|
||||||
|
const thetaFor = (ev, i) =>
|
||||||
|
(Array.isArray(ev.theta) ? ev.theta[i % ev.theta.length] : (ev.theta ?? null));
|
||||||
|
|
||||||
|
// Lane C's events arrive on the bus as the player crosses their `s`. Spawns and pickups are
|
||||||
|
// consumed here; hazards run their own timeline off world.level.events (they need lookahead
|
||||||
|
// for the warn telegraph — see hazards.js header); checkpoints/gates are boot's (the pump
|
||||||
|
// owns respawn/complete semantics); unknown types are ignored on purpose so C can add event
|
||||||
|
// kinds without breaking combat.
|
||||||
|
const offEvent = bus.on('level:event', (ev) => {
|
||||||
|
if (!ev) return;
|
||||||
|
if (ev.type === 'spawn') {
|
||||||
|
// Levels name enemies in fiction ("bolus_chunk"); pools are keyed by archetype
|
||||||
|
// ("floater"). levels/enemies.js is the join table (C's catalogue, round-2 task #1).
|
||||||
|
const spec = getEnemy(ev.enemy);
|
||||||
|
if (!spec) { console.warn(`[combat] spawn "${ev.enemy}" is not in levels/enemies.js — skipped`); return; }
|
||||||
|
for (let i = 0; i < (ev.count ?? 1); i++)
|
||||||
|
enemies.spawn(spec.archetype, { s: fan(ev, i), theta: thetaFor(ev, i), rho: ev.rho ?? null });
|
||||||
|
} else if (ev.type === 'pickup') {
|
||||||
|
for (let i = 0; i < (ev.count ?? 1); i++)
|
||||||
|
pickups.spawn(ev.kind, { s: fan(ev, i), theta: thetaFor(ev, i), rho: ev.rho ?? null });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -36,23 +64,36 @@ export function createCombat({ scene, world, bus, rng, flags = {}, assets = null
|
|||||||
score.kills++;
|
score.kills++;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// pickups.js applies the effect (refill/ammo) and announces; the score lives here so
|
||||||
|
// combat:state stays the one scoreboard E reads.
|
||||||
|
const offPickup = bus.on('pickup', (e) => {
|
||||||
|
score.value += e.score ?? 0;
|
||||||
|
score.samples += e.sample ?? 0;
|
||||||
|
});
|
||||||
|
|
||||||
function update(dt) {
|
function update(dt) {
|
||||||
enemies.update(dt, player);
|
enemies.update(dt, player);
|
||||||
weapons.update(dt);
|
weapons.update(dt);
|
||||||
|
hazards.update(dt);
|
||||||
|
pickups.update(dt);
|
||||||
// combat's slice of the HUD state (E reads player:state for the flight half)
|
// combat's slice of the HUD state (E reads player:state for the flight half)
|
||||||
bus.emit('combat:state', {
|
bus.emit('combat:state', {
|
||||||
heat: weapons.heat, heatMax: weapons.heatMax, overheated: weapons.overheated,
|
heat: weapons.heat, heatMax: weapons.heatMax, overheated: weapons.overheated,
|
||||||
ammo: weapons.ammo, ammoMax: weapons.ammoMax,
|
ammo: weapons.ammo, ammoMax: weapons.ammoMax,
|
||||||
score: score.value, kills: score.kills, enemies: enemies.count,
|
score: score.value, kills: score.kills, samples: score.samples, enemies: enemies.count,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
update,
|
update,
|
||||||
enemies, weapons, score,
|
enemies, weapons, hazards, pickups, score,
|
||||||
|
// Respawn support (boot calls this from its death->checkpoint glue): re-arm hazard
|
||||||
|
// timelines at/ahead of the respawn s. Enemies and uncollected pickups persist — the
|
||||||
|
// pump never re-fires past events, so nothing double-spawns.
|
||||||
|
reset(fromS = 0) { hazards.reset(fromS); },
|
||||||
dispose() {
|
dispose() {
|
||||||
offEvent(); offDie();
|
offEvent(); offDie(); offPickup();
|
||||||
enemies.dispose(); weapons.dispose();
|
enemies.dispose(); weapons.dispose(); hazards.dispose(); pickups.dispose();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -48,14 +48,6 @@ export function createPlayer({ scene, world, bus, rng, flags = {}, assets = null
|
|||||||
|
|
||||||
const clampS = (s) => THREE.MathUtils.clamp(s, 0, world.length);
|
const clampS = (s) => THREE.MathUtils.clamp(s, 0, world.length);
|
||||||
|
|
||||||
// `world.crestSpeed(s)` is world contract v1.1, but Lane A's world had not exposed it yet
|
|
||||||
// when this was written (their round-2 task #1, in flight — the stub already complies). The
|
|
||||||
// fallback mirrors TECH §Crest-speed law rather than inventing a number, so surf behaves
|
|
||||||
// identically the moment A lands theirs, and this guard can then be deleted.
|
|
||||||
const CREST_FACTOR = 1.6;
|
|
||||||
const crestSpeedAt = (s, biome) =>
|
|
||||||
(world.crestSpeed ? world.crestSpeed(s) : biome.flow * CREST_FACTOR);
|
|
||||||
|
|
||||||
// --- durability -------------------------------------------------------------------
|
// --- durability -------------------------------------------------------------------
|
||||||
// Discrete hits emit player:damage. Continuous drain (biome coatDrain, enemy auras) does
|
// Discrete hits emit player:damage. Continuous drain (biome coatDrain, enemy auras) does
|
||||||
// NOT — it would spam the bus 60×/s and E would render a permanent damage flash. Lane E
|
// NOT — it would spam the bus 60×/s and E would render a permanent damage flash. Lane E
|
||||||
@ -78,6 +70,30 @@ export function createPlayer({ scene, world, bus, rng, flags = {}, assets = null
|
|||||||
st.coat = Math.max(0, st.coat - amount);
|
st.coat = Math.max(0, st.coat - amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unconditional death — lethal hazards (C's reflux finale) call this. Deliberately ignores
|
||||||
|
// iframes: boost-dodging THROUGH an acid wall isn't a read, it's a bug C's design forbids.
|
||||||
|
function kill(kind = 'hazard') {
|
||||||
|
if (!st.alive) return;
|
||||||
|
st.coat = 0; st.hull = 0; st.alive = false;
|
||||||
|
bus.emit('player:death', { s: st.s, kind });
|
||||||
|
bus.emit('audio:cue', { name: 'death' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disc-velocity impulse (u/s) — hazards push the ship around with this (squeeze/gate).
|
||||||
|
function shove(dx, dy) {
|
||||||
|
if (!st.alive) return;
|
||||||
|
st.vx += dx; st.vy += dy;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pickup economy (balance.js §pickups): coat/hull points, boost = seconds shaved off the
|
||||||
|
// cooldown (0.6 off a 2.4s cd — a recharge nudge, not a free boost).
|
||||||
|
function refill({ coat = 0, hull = 0, boost = 0 } = {}) {
|
||||||
|
if (!st.alive) return;
|
||||||
|
st.coat = Math.min(T.coat.max, st.coat + coat);
|
||||||
|
st.hull = Math.min(T.hull.max, st.hull + hull);
|
||||||
|
st.boostCd = Math.max(0, st.boostCd - boost);
|
||||||
|
}
|
||||||
|
|
||||||
// --- the loop ---------------------------------------------------------------------
|
// --- the loop ---------------------------------------------------------------------
|
||||||
function update(dt) {
|
function update(dt) {
|
||||||
const intent = input.sample();
|
const intent = input.sample();
|
||||||
@ -123,7 +139,7 @@ export function createPlayer({ scene, world, bus, rng, flags = {}, assets = null
|
|||||||
// (crestSpeed = 1.6 × flow > throttleMax 1.4), so riding means being *carried at the
|
// (crestSpeed = 1.6 × flow > throttleMax 1.4), so riding means being *carried at the
|
||||||
// wave's own speed* — which is also why you stay on it instead of shooting off the front.
|
// wave's own speed* — which is also why you stay on it instead of shooting off the front.
|
||||||
const flowSpeed = biome.flow * st.throttle;
|
const flowSpeed = biome.flow * st.throttle;
|
||||||
const crest = crestSpeedAt(st.s, biome);
|
const crest = world.crestSpeed(st.s); // contract v1.1 law: CREST_FACTOR × flow(s)
|
||||||
// Only ever accelerates. If you're already faster than the crest (boosting, or a fast
|
// Only ever accelerates. If you're already faster than the crest (boosting, or a fast
|
||||||
// segment), the wave must not brake you — it just stops being relevant.
|
// segment), the wave must not brake you — it just stops being relevant.
|
||||||
const carried = crest > flowSpeed
|
const carried = crest > flowSpeed
|
||||||
@ -283,6 +299,9 @@ export function createPlayer({ scene, world, bus, rng, flags = {}, assets = null
|
|||||||
get forward() { return world.sample(st.s).tan; },
|
get forward() { return world.sample(st.s).tan; },
|
||||||
damage,
|
damage,
|
||||||
drain,
|
drain,
|
||||||
|
kill,
|
||||||
|
shove,
|
||||||
|
refill,
|
||||||
respawn(s = 0) {
|
respawn(s = 0) {
|
||||||
Object.assign(st, { s, x: 0, y: 0, vx: 0, vy: 0, coat: T.coat.max, hull: T.hull.max, alive: true });
|
Object.assign(st, { s, x: 0, y: 0, vx: 0, vy: 0, coat: T.coat.max, hull: T.hull.max, alive: true });
|
||||||
cam.init = false; aimLagInit = false;
|
cam.init = false; aimLagInit = false;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user