Repterra Web — full game: base building, power grid, taming & breeding, aquatic raiders, day/night, save/load

- 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
This commit is contained in:
2026-08-23 07:00:23 +00:00
commit 8fbe70d0b0
21 changed files with 6583 additions and 0 deletions
+108
View File
@@ -0,0 +1,108 @@
/* Browser boot test: loads the game, starts a match, screenshots it. */
'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));
await page.goto('http://127.0.0.1:8933/', { waitUntil: 'load' });
await page.waitForTimeout(1200);
await page.screenshot({ path: '/root/dinorts/shots/01-menu.png' });
console.log('menu loaded');
// start normal difficulty
await page.click('[data-diff="normal"]');
await page.waitForTimeout(2500);
await page.screenshot({ path: '/root/dinorts/shots/02-start.png' });
const st1 = await page.evaluate(() => {
const s = RTS.sim.state();
return { day: s.day, dinos: s.dinos.length, units: s.units.length, blds: s.buildings.length, res: { ...s.res } };
});
console.log('game state:', JSON.stringify(st1));
// select HQ by clicking near center of screen
await page.mouse.click(720, 430);
await page.waitForTimeout(400);
await page.screenshot({ path: '/root/dinorts/shots/03-selected.png' });
// place a house via the palette (target the button by its label, not position)
await page.evaluate(() => {
const btns = [...document.querySelectorAll('.palbtn')];
const house = btns.find(b => b.textContent.includes('House'));
house.click();
});
await page.waitForTimeout(200);
// click a spot anchored to the HQ (world is randomly generated each run)
for (const [dx, dy] of [[4, 1], [-4, 2], [3, -4], [-3, -4], [5, 4]]) {
const pt = await page.evaluate(([dx2, dy2]) => {
const hq = RTS.sim.hq();
return RTS.render.worldToScreen(hq.x + dx2, hq.y + dy2);
}, [dx, dy]);
await page.mouse.click(pt.x, pt.y);
await page.waitForTimeout(250);
const done = await page.evaluate(() => RTS.sim.state().buildings.some(b => b.defId === 'house'));
if (done) break;
}
const st2 = await page.evaluate(() => {
const s = RTS.sim.state();
return { blds: s.buildings.map(b => b.defId), gold: Math.floor(s.res.gold) };
});
console.log('after house placement:', JSON.stringify(st2));
await page.keyboard.press('Escape');
await page.keyboard.press('Escape');
// fast-forward 30s of game time at 3x
await page.keyboard.press('3'); // no-op key (speed buttons are UI) — use UI button instead
await page.click('#topbtns .tbtn:nth-child(3)'); // 2×
await page.waitForTimeout(9000);
const st3 = await page.evaluate(() => {
const s = RTS.sim.state();
return { time: Math.floor(s.time), kills: s.stats.kills, dinos: s.dinos.length };
});
console.log('after fast-forward:', JSON.stringify(st3));
await page.screenshot({ path: '/root/dinorts/shots/04-later.png' });
// pause overlay (Save/Load buttons visible)
await page.keyboard.press('Space');
await page.waitForTimeout(300);
const pauseBtns = await page.evaluate(() => ({
save: !!document.getElementById('savebtn'),
loadDisabled: document.getElementById('loadbtn') && document.getElementById('loadbtn').disabled,
}));
console.log('pause menu:', JSON.stringify(pauseBtns));
await page.screenshot({ path: '/root/dinorts/shots/05-paused.png' });
await page.keyboard.press('Space');
// ---- save → reload → continue from the main menu ----
const saved = await page.evaluate(() => RTS.storage.save(true) && RTS.storage.has());
console.log('saved to localStorage:', saved);
if (!saved) throw new Error('save failed');
await page.reload();
await page.waitForTimeout(700);
const contVisible = await page.evaluate(() => {
const b = document.getElementById('continueBtn');
return b && b.style.display !== 'none';
});
if (!contVisible) throw new Error('Continue button not shown despite existing save');
await page.click('#continueBtn');
await page.waitForTimeout(600);
const st4 = await page.evaluate(() => {
const s = RTS.sim.state();
return { day: s.day, time: Math.floor(s.time), blds: s.buildings.length };
});
if (!(st4.time > 5)) throw new Error('loaded colony did not restore progress: ' + JSON.stringify(st4));
console.log('after continue:', JSON.stringify(st4));
console.log('console errors:', errors.length ? errors : 'none');
await browser.close();
if (errors.length) process.exit(2);
console.log('BROWSER BOOT TEST PASSED ✔');
})().catch(e => { console.error('TEST CRASH:', e); process.exit(1); });