// Combat / heroes / magic / save-load E2E import { chromium } from 'playwright-core'; const exe = '/root/.cache/ms-playwright/chromium_headless_shell-1148/chrome-linux/headless_shell'; const BASE = process.env.BASE || 'http://127.0.0.1:5173/'; const errors = []; const browser = await chromium.launch({ executablePath: exe, args: ['--no-sandbox'] }); const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }); page.on('console', m => { if (m.type() === 'error') errors.push(m.text()); }); page.on('pageerror', e => errors.push('PAGEERROR: ' + e.message)); await page.goto(BASE, { waitUntil: 'networkidle' }); await page.waitForTimeout(600); await page.click('#mm-sandbox'); await page.waitForTimeout(900); // minimal town + defenses + hero hall const setup = await page.evaluate(() => { const I = window.__IA; const c = I.app.city; let ox = -1, oy = -1; outer: for (let y = 8; y < c.H - 14; y++) for (let x = 4; x < c.W - 24; x++) { let ok = true; for (let dy = -2; dy < 9 && ok; dy++) for (let dx = -2; dx < 20; dx++) { const t = c.tiles[(y + dy) * c.W + (x + dx)]; if (!t || t.terrain === 'water') { ok = false; break; } } if (ok) { ox = x; oy = y; break outer; } } for (let i = 0; i < 14; i++) I.place('road', ox + i, oy); for (let i = 1; i < 13; i++) I.place('house', ox + i, oy - 1); I.place('well', ox + 2, oy + 1); I.place('market', ox + 4, oy + 1); I.place('granary', ox + 7, oy + 1); const placeNear = (key, cx, cy, R = 7) => { for (let dy = -R; dy <= R; dy++) for (let dx = -R; dx <= R; dx++) { if (I.place(key, cx + dx, cy + dy).ok) return true; } return false; }; for (let i = 0; i < 8; i++) I.place('wall', ox + 2 + i, oy + 4); I.place('tower', ox + 4, oy + 5); placeNear('fort', ox + 11, oy + 5); placeNear('heroHall', ox + 13, oy - 3); I.give('weapons', 60); I.give('iron', 100); return { ox, oy, hall: [...c.buildings.values()].some(b => b.def === 'heroHall'), fort: [...c.buildings.values()].some(b => b.def === 'fort') }; }); console.log('setup:', JSON.stringify(setup)); // run to month 8 so fort can recruit & pop exists await page.evaluate(() => { const I = window.__IA; for (let i = 0; i < 8 * 20; i++) I.step(0.5); }); const mid = await page.evaluate(() => { const c = window.__IA.app.city; return { pop: c.population, legions: [...c.walkers.values()].filter(w => w.unit?.kind === 'soldiers').length }; }); console.log('mid:', JSON.stringify(mid)); // summon hero const heroOk = await page.evaluate(() => { const I = window.__IA; I.app.city.funds += 5000; // ensure affordable for test return I.summon('champion'); }); console.log('summoned:', heroOk); // spawn a hydra in front of the walls and let combat resolve await page.evaluate((pos) => { const I = window.__IA; I.spawnWave('hydra', 1, 1); // teleport hydra near the town for a quick fight const foe = [...I.app.city.walkers.values()].find(w => w.unit?.side === 'foe'); if (foe) { foe.x = pos.ox + 6; foe.y = pos.oy + 6; } }, setup); // cast firestorm at the foe location (grant spell first) const castRes = await page.evaluate((pos) => { const I = window.__IA; I.grantSpell('firestorm'); I.app.city.mana = 500; return I.cast('firestorm', pos.ox + 7, pos.oy + 7); }, setup); console.log('firestorm cast:', castRes); // step until hydra dies (max ~60s game time) const fight = await page.evaluate(() => { const I = window.__IA; let slain0 = I.app.city.stats.monstersSlain; for (let i = 0; i < 120; i++) { I.step(0.5); const foes = [...I.app.city.walkers.values()].filter(w => w.unit?.side === 'foe'); if (foes.length === 0) break; } return { slain: I.app.city.stats.monstersSlain - slain0, foesLeft: [...I.app.city.walkers.values()].filter(w => w.unit?.side === 'foe').length, heroAlive: [...I.app.city.walkers.values()].some(w => w.kind === 'hero'), golems: [...c_walkers(I)].length, }; function c_walkers(I) { return []; } }); console.log('fight:', JSON.stringify(fight)); // SAVE / LOAD round-trip const before = await page.evaluate(() => { const I = window.__IA; I.save(); return { month: I.app.city.month, pop: I.app.city.population }; }); await page.reload({ waitUntil: 'networkidle' }); await page.waitForTimeout(700); const after = await page.evaluate(() => { const I = window.__IA; const ok = I.loadAutosave(); return { ok, month: I.app?.city.month ?? -1, pop: I.app?.city.population ?? -1, before: null }; }); console.log(`save/load: before=${JSON.stringify(before)} after=${JSON.stringify(after)}`); console.log('CONSOLE ERRORS:', errors.length); errors.slice(0, 8).forEach(e => console.log(' -', e.slice(0, 180))); const pass = heroOk && castRes && fight.slain >= 1 && after.ok && Math.abs(after.month - before.month) <= 1 && errors.length === 0; console.log(pass ? '✅ COMBAT/MAGIC/SAVE E2E PASS' : '❌ FAIL'); await browser.close(); process.exit(pass ? 0 : 1);