orbit/dev/escort-delivery-repro.mjs

218 lines
9.6 KiB
JavaScript

// dev/escort-delivery-repro.mjs — REPRO: escort delivery with the convoy
// OFF-SCREEN at the destination (the real CombatSystem + real GameScene
// escort law, run in the true per-frame order; node dev/escort-delivery-repro.mjs)
process.env.NODE_ENV = 'dev';
import { pathToFileURL } from 'node:url';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import './phaser-loader.mjs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const fs = await import('node:fs');
const { config } = await import(pathToFileURL(join(__dirname, '../js/config/Config.js')).href);
const configData = {};
for (const f of fs.readdirSync(join(__dirname, '../data'))) {
if (!f.endsWith('.json') || f === 'manifest.json') continue;
configData[f.replace(/\.json$/i, '')] = JSON.parse(fs.readFileSync(join(__dirname, '../data', f), 'utf8'));
}
config.init(configData);
const {
sideQuestPostingsFor, escortDestinationFor, SideQuestState,
} = await import(pathToFileURL(join(__dirname, '../js/quests/SideQuests.js')).href);
const { CombatSystem } = await import(pathToFileURL(join(__dirname, '../js/combat/CombatSystem.js')).href);
const { GameScene } = await import(pathToFileURL(join(__dirname, '../js/scenes/GameScene.js')).href);
// ------------------------------------------------------------------ scene
class V2 {
constructor(x = 0, y = 0) { this.x = x; this.y = y; }
set(x, y) { this.x = x; this.y = y; return this; }
length() { return Math.hypot(this.x, this.y); }
}
function makeBody() {
const base = {
velocity: new V2(),
setVelocity(x, y) { this.velocity.set(x, y); },
setCircle() {},
setAllowGravity() {},
};
return new Proxy(base, {
get(t, k) {
if (k in t) return t[k];
return () => {};
},
});
}
const SEED = 77031;
const JOB = 'sq-sys-home-p1-escort-0';
const RECS = {
'sys-home': { id: 'sys-home', name: 'Hearth', faction: null, d: 5, zone: 'near', type: 'main' },
'sys-a': { id: 'sys-a', name: 'Aldrin', faction: null, d: 35, zone: 'mid', type: 'void' },
'sys-b': { id: 'sys-b', name: 'Basalt', faction: null, d: 65, zone: 'far', type: 'void' },
};
const ADJ = {
'sys-home': ['sys-a', 'sys-zenith-excluded'],
'sys-a': ['sys-home', 'sys-b'],
'sys-b': ['sys-a'],
};
const CONTENT = {
'sys-home': { planets: [{ id: 'sys-home-p1', name: 'Hearth One', ordinal: 1, x: 0, y: 0 }], settlements: [] },
'sys-a': { planets: [{ id: 'sys-a-p1', name: 'Aldrin One', ordinal: 1, x: 100, y: 200 }], settlements: [] },
'sys-b': { planets: [], settlements: [{ id: 'sys-b-s1', name: 'Basalt Relay', anchor: { type: 'space' }, x: 300, y: 400 }] },
};
const galaxy = {
seed: SEED,
byId: new Map(Object.entries(RECS).map(([k, v]) => [k, v])),
records: Object.values(RECS),
jumpGatesFor: (id) => (ADJ[id] ?? []).map((t) => RECS[t]).filter(Boolean),
ensureContent: (id) => CONTENT[id],
contentOf: (id) => CONTENT[id],
};
function makeScene(systemId) {
const view = { left: 0, top: 0, width: 1280, height: 720 };
const rec = RECS[systemId];
const scene = Object.create(GameScene.prototype);
scene.playSfx = () => {};
scene.consoleToast = (msg) => { console.log(' [toast]', msg); };
scene.time = { now: 0 };
scene.galaxy = galaxy;
scene.systemRecord = { ...rec };
scene.systemContent = galaxy.ensureContent(systemId);
scene.systemPlanets = [];
scene.systemStations = [];
scene.systemGates = [];
scene.planet = null;
scene.tetherField = null;
scene.isHomeSystem = false;
scene.ship = { x: 0, y: 0, radius: 16, body: null, stop() {}, setVisible() {} };
scene.scale = { width: 1280, height: 720 };
// Both camera surfaces: GameScene's witness (scrollX/scrollY) and
// CombatSystem's worldView (the on-screen rule).
const syncView = () => {
view.left = view._sx; view.top = view._sy;
cam.worldView = { x: view._sx, y: view._sy, width: 1280, height: 720, left: view._sx, top: view._sy };
};
const cam = { scrollX: 0, scrollY: 0, worldView: null };
Object.defineProperty(cam, 'scrollX', { get: () => view._sx, set: (v) => { view._sx = v; syncView(); } });
Object.defineProperty(cam, 'scrollY', { get: () => view._sy, set: (v) => { view._sy = v; syncView(); } });
view._sx = 0; view._sy = 0; syncView();
scene.cameras = { main: cam };
// Combat scene surface (combat.test.mjs's makeScene shape).
scene.textures = { exists: () => true };
scene.make = { graphics: () => ({ fillStyle() {}, beginPath() {}, moveTo() {}, lineTo() {}, closePath() {}, fillPath() {}, fillCircle() {}, generateTexture() {}, destroy() {} }) };
scene.add = {
existing: (o) => o,
image: (x, y) => ({ x, y, setTint() {}, setAlpha() {}, setVisible() {}, setScale() {}, destroy() {} }),
graphics: () => ({ fillStyle() {}, beginPath() {}, moveTo() {}, lineTo() {}, closePath() {}, fillPath() {}, fillCircle() {}, generateTexture() {}, destroy() {} }),
text: () => ({ setText() {}, setDepth() {}, setVisible() {}, destroy() {}, set() { return this; } }),
rectangle: (x, y) => ({ x, y, setFillStyle() {}, setAlpha() {}, destroy() {} }),
};
scene.physics = { add: { existing: (o) => { if (!o.body) o.body = makeBody(); return o; } } };
scene.refreshCreditsHud = () => {};
scene.sideQuestState = new SideQuestState();
scene.claimSideQuest = GameScene.prototype.claimSideQuest;
return scene;
}
const DEST = escortDestinationFor(SEED, JOB, 'sys-home', galaxy);
console.log('destination:', DEST.destSystemId, DEST.destObjectId, 'hops', DEST.hops);
const DEST_REC = RECS[DEST.destSystemId];
const DEST_OBJ = (galaxy.ensureContent(DEST.destSystemId).planets.concat(galaxy.ensureContent(DEST.destSystemId).settlements))
.find((o) => o.id === DEST.destObjectId);
const DEST_X = DEST_OBJ.x + 500; // park the ship in front of the world
const DEST_Y = DEST_OBJ.y;
// ------------------------------------------------------------------ boot
const scene = makeScene('sys-home');
const st = scene.sideQuestState;
const posting = { id: JOB, systemId: 'sys-home', systemName: 'Hearth', category: 'escort',
title: 'Convoy Night', label: 'ESCORT', reward: { credits: 150 } };
st.accept(JOB);
st.setActive(JOB);
scene._establishEscort(JOB, posting);
const combat = new CombatSystem(scene, { ship: scene.ship, onKill: () => {} });
scene.combat = combat;
scene.escortDirector = { state: 'idle', nextRollAt: null, quietUntil: 0, update() { return null; } };
// Frame driver — the true order from GameScene.update:
// time.update → combat.update → (windows) → updateSideQuestRoute → [physics step]
let now = 0;
const DT = 16.666;
function frame() {
now += DT;
scene.time.now = now;
if (scene.combat) scene.combat.update(now, DT); // its ally AI + steering
scene.updateEscort();
// Arcade physics step (the engine integrates body velocity after update).
const dt = DT / 1000;
for (const c of [...combat.allies, ...combat.enemies]) {
if (c && !c.dead && c.body?.velocity) {
c.x += c.body.velocity.x * dt;
c.y += c.body.velocity.y * dt;
}
}
}
const inView = (c) => {
const v = scene.cameras.main.worldView;
return c.x >= v.x - 20 && c.x <= v.x + v.width + 20 && c.y >= v.y - 20 && c.y <= v.y + v.height + 20;
};
function convoy() {
return combat.allies.filter((a) => a && !a.dead && (a._escortCrew || a.isTransport));
}
// ------------------------------------------------------------------ run
// LINK at the rendezvous (origin system).
{
const wp = st.routeFor(JOB).waypoints[0];
scene.ship.x = wp[0]; scene.ship.y = wp[1];
scene.cameras.main.scrollX = wp[0] - 640;
scene.cameras.main.scrollY = wp[1] - 360;
frame();
console.log('after link: phase =', st.routeFor(JOB).escort.phase,
'| convoy =', convoy().map((c) => `${c.isTransport ? 'T' : 'F'}(${Math.round(c.x)},${Math.round(c.y)}) onscreen=${inView(c)}`).join(' '));
}
// JUMP to the destination system (scene restart tears the sprites — the
// record re-stands the convoy). Sky clear. The ship parks in front of the
// destination world; the world is ON SCREEN; the convoy is OFF SCREEN
// (far behind the player — the reported condition).
{
scene.systemRecord = { ...DEST_REC };
scene.systemContent = galaxy.ensureContent(DEST.destSystemId);
scene.ship.x = DEST_X; scene.ship.y = DEST_Y;
scene.cameras.main.scrollX = scene.ship.x - 640;
scene.cameras.main.scrollY = scene.ship.y - 360;
// The scene restart: the sprites are gone.
for (const a of combat.allies) a.destroy();
combat.allies.length = 0;
scene._escortTransport = null;
frame(); // the convoy re-forms (transit law)
// Push the convoy OFF-SCREEN (far behind the player).
for (const c of convoy()) { c.x = scene.ship.x - 2200; c.y = scene.ship.y; c.body?.setVelocity?.(0, 0); }
console.log('\ndestination reached — world on screen, sky clear, convoy off-screen:');
console.log(' phase =', st.routeFor(JOB).escort.phase,
'| convoy =', convoy().map((c) => `${c.isTransport ? 'T' : 'F'}(${Math.round(c.x)},${Math.round(c.y)}) onscreen=${inView(c)} state=${c.state}`).join(' '));
}
let sealed = false;
let moved = false;
const t0 = convoy().map((c) => [c, c.x, c.y]);
for (let i = 1; i <= 420; i++) { // up to 7 s of frames
frame();
const ph = st.routeFor(JOB).escort.phase;
if (i % 30 === 0 || ph === 'delivering' || ph === 'delivered') {
console.log(` t=${(i * DT / 1000).toFixed(2)}s phase=${ph} convoy=${convoy().map((c) => `${c.isTransport ? 'T' : 'F'}(${Math.round(c.x)},${Math.round(c.y)}) onscreen=${inView(c)} state=${c.state} alpha=${(c.alpha ?? 1).toFixed(2)}`).join(' ')}`);
}
for (const [c, x0, y0] of t0) if (Math.hypot(c.x - x0, c.y - y0) > 5) moved = true;
if (ph === 'delivered') { sealed = true; break; }
}
console.log('\nRESULT: convoy moved =', moved, '| delivered =', sealed, '| isCompleted =', st.isCompleted(JOB));