193 lines
10 KiB
JavaScript
193 lines
10 KiB
JavaScript
/**
|
|
* dev/zenith-arena-browser.mjs — the ?combat=zenith duel arena in a REAL
|
|
* browser (Playwright, headless Chromium; the same environment the
|
|
* *-browser.test.mjs files use):
|
|
*
|
|
* node dev/server.mjs 8123 & # the static dev server
|
|
* node dev/zenith-arena-browser.mjs
|
|
*
|
|
* What it pins (the goal's verification contract, in the browser):
|
|
* · ?combat=zenith boots the duel arena — zero console errors, the
|
|
* picker's 8 rows (Wingblade → Tempest), the duel state;
|
|
* · the COMBO archetype: the Wingblade's wing volley — seekers out of
|
|
* both wings, the SLOW capped seek (out-turnable), homing on the
|
|
* player;
|
|
* · the ELEMENTAL archetype: the Voltspire's dart detonating into a
|
|
* GROWING electrified zone that ticks damage and then dies;
|
|
* · '0' = the whole pack (8 fighters, all duel-marked);
|
|
* · the regressions: ?combat=apex still boots the APEX arena, and a
|
|
* bare ?combat still boots the seven-type demo.
|
|
*/
|
|
import { pathToFileURL } from 'node:url';
|
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
|
|
const { chromium } = await import(
|
|
pathToFileURL('/home/brianfertig/.npm/_npx/e41f203b7505f1fb/node_modules/playwright/index.mjs').href
|
|
);
|
|
const BASE = process.env.ORBIT_URL || 'http://127.0.0.1:8123';
|
|
|
|
let fails = 0;
|
|
const check = (msg, cond, extra = '') => {
|
|
console.log(` ${cond ? '✔' : '✘ FAIL'} ${msg}${extra && !cond ? ` [${extra}]` : ''}`);
|
|
if (!cond) fails++;
|
|
};
|
|
|
|
const browser = await chromium.launch({
|
|
headless: true,
|
|
executablePath: '/home/brianfertig/.cache/ms-playwright/chromium-1208/chrome-linux64/chrome',
|
|
args: ['--no-sandbox', '--use-gl=swiftshader', '--enable-unsafe-swiftshader', '--autoplay-policy=no-user-gesture-required'],
|
|
});
|
|
const errors = [];
|
|
const S = `window.game.scene.getScene('GameScene')`;
|
|
const C = `${S}.combat`;
|
|
|
|
async function openScene(url) {
|
|
const page = await browser.newPage({ viewport: { width: 1440, height: 860 } });
|
|
const log = [];
|
|
page.on('pageerror', (e) => log.push(String(e)));
|
|
page.on('console', (m) => {
|
|
// the repo's own browser tests filter the dev-server favicon 404 (no
|
|
// favicon in the repo) — 'Failed to load resource' with no stack is that.
|
|
if (m.type() === 'error' && !/Failed to load resource/i.test(m.text())) log.push(m.text());
|
|
});
|
|
await page.goto(`${BASE}${url}`, { waitUntil: 'load' });
|
|
for (let i = 0; i < 80; i++) {
|
|
const ready = await page.evaluate(`!!(window.game && window.game.scene && window.game.scene.getScene('MenuScene') && window.game.scene.getScene('MenuScene').newGameBtn)`);
|
|
if (ready) break;
|
|
await sleep(250);
|
|
}
|
|
await page.evaluate(`window.game.scene.getScene('MenuScene').newGameBtn.panel.emit('pointerdown'); true`);
|
|
for (let i = 0; i < 80; i++) {
|
|
const ready = await page.evaluate(`!!(window.game && window.game.scene && window.game.scene.getScene('GameScene') && window.game.scene.getScene('GameScene').combat)`);
|
|
if (ready) break;
|
|
await sleep(250);
|
|
}
|
|
await sleep(2500); // the scene's settle
|
|
return { page, log };
|
|
}
|
|
|
|
// ------------------------------------------------------------------ zenith
|
|
{
|
|
const { page, log } = await openScene('/index.html?combat=zenith');
|
|
check('zenith: zero console/page errors through the boot', log.length === 0, JSON.stringify(log.slice(0, 3)));
|
|
|
|
const boot = await page.evaluate(`JSON.stringify({
|
|
boot: ${S}._combatBootMode(),
|
|
duel: ${C}.duel,
|
|
rows: ${S}.duelPicker ? ${S}.duelPicker.rows().map(r => r.id) : null,
|
|
styles: ${S}.duelPicker ? ${S}.duelPicker.rows().length : 0,
|
|
})`);
|
|
const b = JSON.parse(boot);
|
|
check('zenith: ?combat=zenith boots the arena (8 rows, Wingblade first, Tempest last)',
|
|
b.boot === 'zenith' && b.duel.faction === 'zenith' && b.duel.active === false &&
|
|
b.rows && b.rows.length === 8 && b.rows[0] === 'zenith_wingblade' && b.rows[7] === 'zenith_tempest', boot);
|
|
|
|
// THE COMBO ARCHETYPE — the Wingblade (pick 1): the wing volley.
|
|
await page.evaluate(`${S}.duelPicker.select(1); true`);
|
|
await sleep(1200);
|
|
const f0 = JSON.parse(await page.evaluate(`(() => { const e = ${C}.enemies[0]; return JSON.stringify({ active: ${C}.duel.active, n: ${C}.enemies.length, name: e && e.name, pattern: e && e.wpn.pattern, ab: e && e.abilities.map(a => a.id), armor: e ? e.hull + (e.shields || 0) : null, sprite: e ? !!(e.texture || e.sheetFrame) : null }); })()`));
|
|
check('zenith: pick 1 = the Wingblade (rapid + flank_barrage), 210 armor, a visible ship',
|
|
f0.active === true && f0.n === 1 && f0.name === 'Wingblade' && f0.pattern === 'rapid' &&
|
|
f0.ab.includes('flank_barrage') && f0.armor === 210, JSON.stringify(f0));
|
|
|
|
const f1 = JSON.parse(await page.evaluate(`(() => {
|
|
const c = ${C}; const e = c.enemies[0]; const pl = c.ship;
|
|
if (!e || !pl) return JSON.stringify({ reason: 'no-enemy-or-player' });
|
|
c.hull = 999999; c.shields = 999999;
|
|
let t = (c._now ? c._now() : 100000) + 1000;
|
|
for (let i = 0; i < 2000 && (c.missiles || []).length === 0; i++) { t += 16; c.update(t, 16); }
|
|
const ms = c.missiles || [];
|
|
const eH = typeof e.heading === 'function' ? e.heading() : null;
|
|
return JSON.stringify({
|
|
ms: ms.length,
|
|
turn: ms.map(m => m.turnRate),
|
|
dmg: ms.map(m => m.damage),
|
|
target: ms.length > 0 ? ms[0].targetRef === pl : null,
|
|
wing90: eH == null ? null : ms.every(m => Math.abs(((m.heading - eH - Math.PI / 2) % (Math.PI * 2) + Math.PI * 3) % (Math.PI * 2) - Math.PI) <= 0.4 || Math.abs(((m.heading - eH + Math.PI / 2) % (Math.PI * 2) + Math.PI * 3) % (Math.PI * 2) - Math.PI) <= 0.4),
|
|
missileSprite: ms.length > 0 ? !!ms[0].texture : null,
|
|
});
|
|
})()`));
|
|
check('zenith: the wing volley flies (seekers out of BOTH wings, ≤1.3 rad/s seek, 16 dmg, homing)',
|
|
f1.ms >= 6 && f1.turn.every((r) => r <= 1.3) && f1.dmg.every((d) => d === 16) &&
|
|
f1.target === true && f1.wing90 === true && f1.missileSprite === true, JSON.stringify(f1));
|
|
|
|
await page.evaluate(`const e=${C}.enemies[0]; if (e) { e.damage(9999); ${C}.killEnemy(e, 0); } true`);
|
|
await sleep(700);
|
|
check('zenith: the Wingblade kill releases the arena', (await page.evaluate(`${C}.duel.active`)) === false);
|
|
|
|
// THE ELEMENTAL ARCHETYPE — the Voltspire (pick 5): the shock line.
|
|
await page.evaluate(`${S}.duelPicker.select(5); true`);
|
|
await sleep(1200);
|
|
const f2 = JSON.parse(await page.evaluate(`(() => { const e = ${C}.enemies[0]; return JSON.stringify({ active: ${C}.duel.active, n: ${C}.enemies.length, name: e && e.name, pattern: e && e.wpn.pattern, ab: e && e.abilities.map(a => a.id) }); })()`));
|
|
check('zenith: pick 5 = the Voltspire (lance + shock_dart)',
|
|
f2.active === true && f2.n === 1 && f2.name === 'Voltspire' && f2.pattern === 'lance' && f2.ab.includes('shock_dart'), JSON.stringify(f2));
|
|
|
|
const f3 = JSON.parse(await page.evaluate(`(() => {
|
|
const c = ${C}; const e = c.enemies[0]; const pl = c.ship;
|
|
if (!e || !pl) return JSON.stringify({ reason: 'no-enemy-or-player' });
|
|
c.hull = 999999; c.shields = 999999;
|
|
let armor0 = c.hull + c.shields;
|
|
let t = (c._now ? c._now() : 100000) + 1000;
|
|
let zone = null;
|
|
for (let i = 0; i < 2000; i++) { t += 16; c.update(t, 16); if ((c.shockzones || []).length > 0) { zone = c.shockzones[0]; break; } }
|
|
if (!zone) return JSON.stringify({ zone: false });
|
|
const r0 = zone.r;
|
|
let dmg = 0;
|
|
for (let i = 0; i < 300; i++) {
|
|
t += 16; c.update(t, 16);
|
|
zone = (c.shockzones || [])[0] || zone;
|
|
if (c.hull + c.shields < armor0 - 3) { dmg = armor0 - (c.hull + c.shields); break; }
|
|
}
|
|
return JSON.stringify({
|
|
zone: true,
|
|
zoneSprite: !!zone.sprite,
|
|
r0: Math.round(r0), r1: zone ? Math.round(zone.r) : null,
|
|
maxR: zone ? zone.maxR : null,
|
|
finite: zone ? zone.end - zone.born : null,
|
|
dmg: Math.round(dmg),
|
|
});
|
|
})()`));
|
|
check('zenith: the shock dart blooms a VISIBLE GROWING zone that ticks damage (finite)',
|
|
f3.zone === true && f3.zoneSprite === true && f3.r1 > f3.r0 && f3.r1 <= f3.maxR &&
|
|
f3.finite <= 5000 && f3.dmg >= 3, JSON.stringify(f3));
|
|
|
|
await page.evaluate(`const e=${C}.enemies[0]; if (e) { e.damage(9999); ${C}.killEnemy(e, 0); } true`);
|
|
await sleep(700);
|
|
check('zenith: the Voltspire kill releases the arena', (await page.evaluate(`${C}.duel.active`)) === false);
|
|
|
|
// THE PACK (pick 0): all eight at once, both archetypes.
|
|
await page.evaluate(`${S}.duelPicker.select(0); true`);
|
|
await sleep(1200);
|
|
const f4 = JSON.parse(await page.evaluate(`JSON.stringify({ n: ${C}.enemies.length, names: ${C}.enemies.map(e => e.name), allDuel: ${C}.enemies.every(e => e.duel === true) })`));
|
|
check('zenith: pick 0 = the whole pack (8 fighters, all marked)',
|
|
f4.n === 8 && f4.allDuel === true && f4.names.includes('Tempest') && f4.names.includes('Kestrel'), JSON.stringify(f4));
|
|
await page.evaluate(`${C}.enemies.slice().forEach(e => { e.damage(9999); ${C}.killEnemy(e, 0); }); true`);
|
|
await sleep(700);
|
|
check('zenith: the pack down-drop releases the arena', (await page.evaluate(`${C}.duel.active`)) === false);
|
|
await page.close();
|
|
}
|
|
|
|
// ------------------------------------------------------------------ apex regression
|
|
{
|
|
const { page, log } = await openScene('/index.html?combat=apex');
|
|
const boot = JSON.parse(await page.evaluate(`JSON.stringify({ boot: ${S}._combatBootMode(), rows: ${S}.duelPicker ? ${S}.duelPicker.rows().map(r => r.id) : null })`));
|
|
check('regression: ?combat=apex STILL boots the APEX arena (8 rows, Raptor first)',
|
|
boot.boot === 'apex' && boot.rows && boot.rows.length === 8 && boot.rows[0] === 'apex_raptor', JSON.stringify(boot));
|
|
check('regression: the apex boot is quiet (no console errors)', log.length === 0, JSON.stringify(log.slice(0, 3)));
|
|
await page.close();
|
|
}
|
|
|
|
// ------------------------------------------------------------------ the demo regression
|
|
{
|
|
const { page, log } = await openScene('/index.html?combat');
|
|
const d = JSON.parse(await page.evaluate(`JSON.stringify({ boot: ${S}._combatBootMode(), wave: ${C}.waveRun ? ${C}.waveRun.state : null, picker: !!${S}.duelPicker })`));
|
|
check('regression: the plain ?combat STILL boots the seven-type demo',
|
|
d.boot === 'demo' && d.wave && !['cancelled', 'done'].includes(d.wave) && d.picker === false, JSON.stringify(d));
|
|
check('regression: the demo boot is quiet (no console errors)', log.length === 0, JSON.stringify(log.slice(0, 3)));
|
|
await page.close();
|
|
}
|
|
|
|
await browser.close();
|
|
console.log(fails === 0 ? '\nzenith arena (browser): ALL CHECKS PASSED' : `\n${fails} check(s) FAILED`);
|
|
process.exit(fails === 0 ? 0 : 1);
|