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>
100 lines
4.6 KiB
JavaScript
100 lines
4.6 KiB
JavaScript
// combat/index.js (Lane B) — the createCombat factory. Composes enemies + weapons + hazards
|
|
// + pickups and feeds them Lane C's level events. All the wiring, none of the behaviour.
|
|
//
|
|
// Update order inside a frame is load-bearing:
|
|
// boot: player.update(dt) -> combat.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.
|
|
// 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 { 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 }) {
|
|
const enemies = createEnemies({ scene, world, bus, rng, assets });
|
|
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.setOnDeath((e) => weapons.spawnBlast(e.pos, e.radius * 1.6, 0.3));
|
|
|
|
const score = { value: 0, kills: 0, samples: 0 };
|
|
|
|
// Fan a group of `count` out along s. C's `spread` is the authored span of the group
|
|
// ("count 4 spread 120" = a field, not a wall); without one, a tight default fan keeps
|
|
// a count from stacking at a single arclength.
|
|
const fan = (ev, i) => {
|
|
const n = ev.count ?? 1;
|
|
if (n <= 1) return ev.s ?? 0;
|
|
const span = ev.spread ?? (n - 1) * 3.5;
|
|
return (ev.s ?? 0) + (i / (n - 1)) * span;
|
|
};
|
|
// 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 });
|
|
}
|
|
});
|
|
|
|
const offDie = bus.on('enemy:die', (e) => {
|
|
score.value += e.score ?? 0;
|
|
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) {
|
|
enemies.update(dt, player);
|
|
weapons.update(dt);
|
|
hazards.update(dt);
|
|
pickups.update(dt);
|
|
// combat's slice of the HUD state (E reads player:state for the flight half)
|
|
bus.emit('combat:state', {
|
|
heat: weapons.heat, heatMax: weapons.heatMax, overheated: weapons.overheated,
|
|
ammo: weapons.ammo, ammoMax: weapons.ammoMax,
|
|
score: score.value, kills: score.kills, samples: score.samples, enemies: enemies.count,
|
|
});
|
|
}
|
|
|
|
return {
|
|
update,
|
|
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() {
|
|
offEvent(); offDie(); offPickup();
|
|
enemies.dispose(); weapons.dispose(); hazards.dispose(); pickups.dispose();
|
|
},
|
|
};
|
|
}
|