Files
restitution/tests/e2e/combat-loop.e2e.js
kaykayyali 804aec6a11 M2.4: combat loop e2e + unit data fields
- Unit.js: add health/armor to entity data
- CombatSystem.js: integration hooks for M2 combat loop
- EntityStateMachine.js: orchestrator.registerEntity refactor
- playwright.config.js: M2 e2e config
- tests/e2e/combat-loop.{spec,e2e}.js: M2 combat loop verification
- tests/e2e/milestone-2-combat-loop.spec.js: M2 acceptance test
- .gitignore: exclude debug scripts + screenshots from commits
2026-06-27 16:44:41 +00:00

161 lines
6.5 KiB
JavaScript

const { chromium } = require('playwright');
const assert = require('assert');
const SITE = process.env.RESTITUTION_URL || 'https://restitution.damascusfront.net';
(async () => {
let browser;
try {
browser = await chromium.launch({
executablePath: '/usr/lib/chromium/chromium',
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu'],
});
const page = await browser.newPage();
const logs = [];
page.on('console', msg => logs.push({ type: msg.type(), text: msg.text() }));
page.on('pageerror', err => logs.push({ type: 'pageerror', text: err.message }));
// 1. Load game and navigate lobby
await page.goto(SITE, { waitUntil: 'networkidle' });
await page.click('button:has-text("Create Game")');
await page.waitForSelector('button:has-text("Start Game")', { timeout: 15000 });
await page.click('button:has-text("Start Game")');
// Wait for canvas + game boot
let hasCanvas = false;
let gameExists = false;
for (let i = 0; i < 60; i++) {
hasCanvas = await page.evaluate(() => !!document.querySelector('canvas'));
gameExists = await page.evaluate(() => !!window.game);
if (hasCanvas && gameExists) break;
await page.waitForTimeout(500);
}
assert.strictEqual(hasCanvas, true, 'Canvas not found after lobby');
assert.strictEqual(gameExists, true, 'window.game not exposed after lobby');
// Let scene.create() finish (map + units spawn)
await page.waitForTimeout(3000);
// Verify scene booted
const sceneReady = await page.evaluate(() => {
const scene = window.game.scene?.getScene('Map_Player');
return scene?.sys?.settings?.active === true;
});
assert.strictEqual(sceneReady, true, 'Map_Player scene not active');
// 2. Spawn player unit via F key at camera center
await page.keyboard.press('KeyF');
await page.waitForTimeout(500);
// 3. Spawn enemy unit adjacent to first friendly (using groundLayer.worldToTileXY)
const spawnResult = await page.evaluate(() => {
try {
const scene = window.game.scene.getScene('Map_Player');
const tm = scene.teamManager;
const all = tm.getAllUnits();
const friendly = all.find(u => tm.getEntityTeam(u) === 'team-A');
if (!friendly) return { error: 'no friendly' };
// Use groundLayer to convert world->tile (accounts for layer offset)
const raw = scene.groundLayer.worldToTileXY(friendly.x, friendly.y);
const ft = { x: Math.floor(raw.x), y: Math.floor(raw.y) };
const enemyTile = { x: ft.x + 1, y: ft.y };
const enemy = scene.unitFactory.spawnInfantry(enemyTile, 'team-B');
// Set low health for fast death
if (enemy) {
enemy.setData('health', 15);
enemy.setData('armor', 0);
if (enemy.components?.health) {
enemy.components.health.current = 15;
enemy.components.health.armor = 0;
}
}
return { friendlyTile: ft, enemyTile, enemySpawned: !!enemy, enemyWorld: { x: enemy?.x, y: enemy?.y } };
} catch (e) {
return { error: e.message, stack: e.stack };
}
});
console.log('spawnResult:', spawnResult);
assert.ok(!spawnResult.error, 'spawn enemy failed: ' + (spawnResult.error + ' ' + (spawnResult.stack || '')));
assert.strictEqual(spawnResult.enemySpawned, true, 'enemy unit not spawned');
await page.waitForTimeout(1000);
// 4. Verify both factions exist and are alive
const bothSides = await page.evaluate(() => {
const scene = window.game.scene.getScene('Map_Player');
const tm = scene.teamManager;
const grouped = tm.getAllUnitsGrouped();
const teamA = Array.from(grouped.get('team-A') || []).filter(u => !u.dead && !(u.isDead && u.isDead()));
const teamB = Array.from(grouped.get('team-B') || []).filter(u => !u.dead && !(u.isDead && u.isDead()));
return { teamA: teamA.length, teamB: teamB.length };
});
assert.ok(bothSides.teamA > 0, 'team-A has no alive units');
assert.ok(bothSides.teamB > 0, 'team-B has no alive units');
// Screenshot: after auto-engage setup
await page.screenshot({ path: 'test-results/e2e-01-spawned.png', fullPage: false });
// 5. Let combat tick and verify projectiles exist
let projectilesFired = false;
for (let tick = 0; tick < 40; tick++) {
await page.waitForTimeout(250);
const projCount = await page.evaluate(() => {
const scene = window.game.scene.getScene('Map_Player');
const cs = scene.orchestrator.systems.combat;
return cs.projectiles.countActive();
});
if (projCount > 0) {
projectilesFired = true;
break;
}
}
assert.strictEqual(projectilesFired, true, 'No projectiles fired within 10s');
// Screenshot: mid-combat
await page.screenshot({ path: 'test-results/e2e-02-midcombat.png', fullPage: false });
// 6. Verify health bars exist
const healthBarsExist = await page.evaluate(() => {
const scene = window.game.scene.getScene('Map_Player');
const hb = scene.healthBars;
return hb && Object.keys(hb.bars || {}).length > 0;
});
assert.strictEqual(healthBarsExist, true, 'Health bars not found');
// 7. Let combat run until someone dies
let deathObserved = false;
let deathInfo = null;
for (let tick = 0; tick < 60; tick++) {
await page.waitForTimeout(500);
const state = await page.evaluate(() => {
const scene = window.game.scene.getScene('Map_Player');
const tm = scene.teamManager;
const grouped = tm.getAllUnitsGrouped();
const teamA = Array.from(grouped.get('team-A') || []);
const teamB = Array.from(grouped.get('team-B') || []);
const deadA = teamA.filter(u => u.dead || (u.isDead && u.isDead())).length;
const deadB = teamB.filter(u => u.dead || (u.isDead && u.isDead())).length;
return { deadA, deadB, totalA: teamA.length, totalB: teamB.length };
});
if (state.deadA > 0 || state.deadB > 0) {
deathObserved = true;
deathInfo = state;
break;
}
}
assert.strictEqual(deathObserved, true, 'No unit died within 30s of combat');
console.log('Death observed:', deathInfo);
// Screenshot: post-death
await page.screenshot({ path: 'test-results/e2e-03-postdeath.png', fullPage: false });
console.log('E2E PASSED: spawn units, auto-engage, projectiles, health bars, death');
process.exitCode = 0;
} catch (err) {
console.error('E2E FAILED:', err.message);
process.exitCode = 1;
} finally {
if (browser) await browser.close();
}
})();