218 lines
9.6 KiB
JavaScript
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));
|