- Isometric canvas RTS vs dinosaur waves (fan demake of Repterra) - Economy: houses/taxes, farms, foresters, quarries; colonist staffing - Power grid: generators extend build range; brownout + recovery - Defense: walls/gates, watchtowers (AA), cannon towers (ground-only) - Taming: Primal Pen + Tamers collar weakened dinos; pets obey commands - Breeding: tamed pairs incubate eggs at the pen; hatchlings grow up - 7 dino species incl. flying Pteranodons and lake-raiding Suchomimus - Telegraphed waves with direction arrows; day-15 final horde; 3 difficulties - Day/night cycle, fog of war, minimap, synth audio, 1x-3x speeds - Save/Load/Continue + dawn autosave (full JSON state snapshots) - Tests: 80-assertion headless suite, browser boot + E2E, balance harness
138 lines
6.1 KiB
JavaScript
138 lines
6.1 KiB
JavaScript
/* Deeper E2E: pixels, HUD updates, placement ghost, wave banner. */
|
|
'use strict';
|
|
const { chromium } = require('/tmp/node_modules/playwright-core');
|
|
|
|
(async () => {
|
|
const browser = await chromium.launch({
|
|
executablePath: '/root/.cache/ms-playwright/chromium_headless_shell-1148/chrome-linux/headless_shell',
|
|
args: ['--no-sandbox', '--disable-gpu'],
|
|
});
|
|
const page = await browser.newPage({ viewport: { width: 1440, height: 860 } });
|
|
const errors = [];
|
|
page.on('console', m => { if (m.type() === 'error') errors.push(m.text()); });
|
|
page.on('pageerror', e => errors.push('PAGEERROR: ' + e.message));
|
|
|
|
let fails = 0;
|
|
const ok = (c, m) => { if (c) console.log(' ✔ ' + m); else { fails++; console.error(' ✘ ' + m); } };
|
|
|
|
await page.goto('http://127.0.0.1:8933/', { waitUntil: 'load' });
|
|
await page.waitForTimeout(800);
|
|
await page.click('[data-diff="normal"]');
|
|
await page.waitForTimeout(2000);
|
|
// keep the test window peaceful — wave destruction timing would make the
|
|
// economy assertions flaky (this suite tests UI/HUD, not combat)
|
|
await page.evaluate(() => { RTS.sim.state().waves.forEach(w => { w.day += 40; }); });
|
|
|
|
// ---- canvas pixel richness ----
|
|
const pix = await page.evaluate(() => {
|
|
const cv = document.getElementById('game');
|
|
const ctx = cv.getContext('2d');
|
|
const img = ctx.getImageData(0, 0, cv.width, cv.height).data;
|
|
const colors = new Set();
|
|
let nonBlack = 0, total = 0;
|
|
for (let i = 0; i < img.length; i += 40) { // sample
|
|
const r = img[i], g = img[i + 1], b = img[i + 2];
|
|
colors.add((r >> 4) + ',' + (g >> 4) + ',' + (b >> 4));
|
|
if (r + g + b > 30) nonBlack++;
|
|
total++;
|
|
}
|
|
return { uniqueColors: colors.size, nonBlackRatio: nonBlack / total };
|
|
});
|
|
ok(pix.uniqueColors > 60, 'canvas is richly colored (' + pix.uniqueColors + ' quantized colors)');
|
|
ok(pix.nonBlackRatio > 0.5, 'canvas mostly rendered (' + Math.round(pix.nonBlackRatio * 100) + '% non-dark)');
|
|
|
|
// ---- HUD present ----
|
|
const hud1 = await page.evaluate(() => ({
|
|
gold: document.querySelector('#res-gold .v').textContent,
|
|
day: document.getElementById('daylabel').textContent,
|
|
time: RTS.sim.state().time,
|
|
}));
|
|
ok(hud1.gold === '550', 'HUD shows starting gold (' + hud1.gold + ')');
|
|
ok(/Day 1/.test(hud1.day), 'day label renders (' + hud1.day + ')'); // sun/moon icon prefix
|
|
|
|
// ---- placement via UI ----
|
|
await page.click('#palette .palcol:first-child .palbtn:first-child'); // house
|
|
await page.waitForTimeout(150);
|
|
const ghost = await page.evaluate(() => !!RTS.ui.placing && RTS.ui.placing.defId);
|
|
ok(ghost === 'house', 'palette activates ghost placement (' + ghost + ')');
|
|
// find a valid tile near HQ on the right side of screen
|
|
const spot = await page.evaluate(() => {
|
|
const hq = RTS.sim.hq();
|
|
for (let r = 2; r < 8; r++) {
|
|
for (let a = 0; a < 12; a++) {
|
|
const x = Math.round(hq.x + r * Math.cos(a / 12 * 6.28));
|
|
const y = Math.round(hq.y + r * Math.sin(a / 12 * 6.28));
|
|
if (RTS.sim.canPlace('house', x, y).ok) {
|
|
const sp = RTS.render.worldToScreen(x, y);
|
|
return { sx: sp.x, sy: sp.y, x, y };
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
});
|
|
ok(spot, 'found valid house spot');
|
|
if (spot) {
|
|
await page.mouse.click(spot.sx, spot.sy);
|
|
await page.waitForTimeout(300);
|
|
const placed = await page.evaluate(({ x, y }) => {
|
|
const s = RTS.sim.state();
|
|
return s.buildings.some(b => b.defId === 'house' && b.x === x && b.y === y);
|
|
}, { x: spot.x, y: spot.y });
|
|
ok(placed, 'house placed by clicking at (' + spot.x + ',' + spot.y + ')');
|
|
}
|
|
await page.keyboard.press('Escape');
|
|
|
|
// ---- income flows into HUD once a house exists ----
|
|
await page.click('#topbtns .tbtn:nth-child(4)'); // 3x
|
|
await page.waitForTimeout(9000);
|
|
const hud2 = await page.evaluate(() => ({
|
|
gold: document.querySelector('#res-gold .v').textContent,
|
|
rate: document.querySelector('#res-gold .r').textContent,
|
|
pop: document.querySelector('#res-pop .v').textContent,
|
|
}));
|
|
ok(hud2.rate !== '+0.00/s' && hud2.rate.startsWith('+'), 'tax income shown in HUD (rate ' + hud2.rate + ', gold ' + hud2.gold + ')');
|
|
ok(hud2.pop !== '4/0', 'population grows under housing (' + hud2.pop + ')');
|
|
|
|
// ---- minimap: terrain lit + fog dark both present ----
|
|
const mm = await page.evaluate(() => {
|
|
const cv = document.getElementById('minimap');
|
|
const d = cv.getContext('2d').getImageData(0, 0, cv.width, cv.height).data;
|
|
let lit = 0, dark = 0;
|
|
for (let i = 0; i < d.length; i += 40) {
|
|
const g = d[i + 1];
|
|
if (g > 60) lit++;
|
|
else if (g < 20) dark++;
|
|
}
|
|
return { lit, dark };
|
|
});
|
|
ok(mm.lit > 60 && mm.dark > 500, 'minimap renders terrain + fog (' + mm.lit + ' lit / ' + mm.dark + ' fog samples)');
|
|
|
|
// ---- run to first wave: restore real schedule, day 2 = 40s game time; 3x speed ----
|
|
await page.evaluate(() => {
|
|
const s = RTS.sim.state();
|
|
s.waves.forEach(w => { w.day -= 40; w.warned = false; w.spawned = false; });
|
|
s.waveIdx = 0;
|
|
s.warnT = 0;
|
|
});
|
|
await page.click('#topbtns .tbtn:nth-child(4)'); // 3x
|
|
let bannerSeen = false, waveSpawned = false;
|
|
for (let i = 0; i < 40 && !waveSpawned; i++) {
|
|
await page.waitForTimeout(1000);
|
|
bannerSeen = bannerSeen || await page.evaluate(() => document.getElementById('wavebanner').style.display === 'flex');
|
|
waveSpawned = await page.evaluate(() => RTS.sim.state().waveIdx > 0);
|
|
}
|
|
ok(bannerSeen, 'wave warning banner appeared');
|
|
ok(waveSpawned, 'first wave spawned');
|
|
await page.screenshot({ path: '/root/dinorts/shots/06-wave.png' });
|
|
const waveState = await page.evaluate(() => {
|
|
const s = RTS.sim.state();
|
|
return { dinos: s.dinos.length, kills: s.stats.kills, day: s.day, wave: s.waveIdx };
|
|
});
|
|
console.log(' wave state:', JSON.stringify(waveState));
|
|
|
|
console.log('console errors:', errors.length ? errors : 'none');
|
|
await browser.close();
|
|
if (errors.length || fails) { console.error('E2E FAILED: ' + fails + ' assertion(s), ' + errors.length + ' console errors'); process.exit(1); }
|
|
console.log('E2E PASSED ✔');
|
|
})().catch(e => { console.error('TEST CRASH:', e); process.exit(1); });
|