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
+139
View File
@@ -0,0 +1,139 @@
/* Feature screenshots: taming at dusk, lake raider, wall assault */
'use strict';
const fs = require('fs');
const path = require('path');
const { chromium } = require('/tmp/node_modules/playwright-core');
const EXE = '/root/.cache/ms-playwright/chromium_headless_shell-1148/chrome-linux/headless_shell';
const URL = 'http://127.0.0.1:8933/index.html';
const OUT = path.join(__dirname, '..', 'shots');
(async () => {
const browser = await chromium.launch({ executablePath: EXE, args: ['--no-sandbox'] });
const page = await browser.newPage({ viewport: { width: 1440, height: 860 } });
const errors = [];
page.on('pageerror', e => errors.push(String(e)));
await page.goto(URL);
await page.waitForTimeout(700);
// start a normal game
await page.evaluate(() => {
document.querySelector('[data-diff="normal"]').click();
});
await page.waitForTimeout(500);
// ---- diorama 1: dusk base with Primal Pen, tamed raptor, tamer at work ----
await page.evaluate(() => {
const s = RTS.sim.state();
s.res.gold += 9000; s.res.wood += 9000; s.res.stone += 9000;
const hq = RTS.sim.hq();
const put = (id, dx, dy) => {
const x = Math.round(hq.x + dx), y = Math.round(hq.y + dy);
if (!RTS.sim.canPlace(id, x, y).ok) return null;
const r = RTS.sim.place(id, x, y);
if (!r.ok) return null;
r.b.done = true; r.b.progress = 1; r.b.hp = r.b.maxHp;
return r.b;
};
const tryPut = (id, spots) => { for (const [dx, dy] of spots) { const b = put(id, dx, dy); if (b) return b; } return null; };
// a tidy little colony
put('generator', -4, -3); put('generator', 4, 3);
put('house', -3, 2); put('house', 3, -3); put('house', -5, 1);
put('farm', 2, 4); put('forester', -6, -2); put('quarry', 6, -1);
put('watchtower', -2, -5); put('watchtower', 5, 1); put('watchtower', 0, 5); put('watchtower', -6, 4);
put('barracks', 1, -5);
window.__pen = tryPut('primalpen', [[-2, 3], [-3, 3], [-2, 4], [2, -4], [4, -1]]);
// rangers on guard
for (let i = 0; i < 5; i++) {
const u = RTS.entities.makeUnit('ranger', hq.x + (i - 2) * 1.2, hq.y + 2.5);
s.units.push(u);
}
const tamer = RTS.entities.makeUnit('tamer',
(window.__pen ? window.__pen.x : hq.x) + 1.5,
(window.__pen ? window.__pen.y : hq.y) + 1.8);
s.units.push(tamer);
// a weakened raptor right next to him → capture starts on its own
const wild = RTS.entities.makeDino('raptor', tamer.x + 1.2, tamer.y + 0.4, 'roam');
wild.hp = wild.maxHp * 0.2;
s.dinos.push(wild);
// an already-loyal trike guarding the gate side
const pet = RTS.entities.makeDino('trike', hq.x + 4.5, hq.y + 4, 'roam');
pet.tamed = true; pet.mode = 'pet'; pet.homeX = pet.x; pet.homeY = pet.y;
pet.hp = pet.maxHp * 0.85;
s.dinos.push(pet);
// golden hour
s.time = Math.floor(RTS.CONFIG.WORLD.DAY_LENGTH * 0.52);
});
// let the capture channel run while rendering
await page.evaluate(() => { for (let i = 0; i < 30 * 2; i++) RTS.sim.tick(1 / 30); });
await page.waitForTimeout(400);
await page.screenshot({ path: OUT + '/taming.png' });
// ---- diorama 2: suchomimus rising from a lake ----
await page.evaluate(() => {
const s = RTS.sim.state();
const W = RTS.CONFIG.WORLD.W, H = RTS.CONFIG.WORLD.H;
// find a lake tile reasonably near the visible area
let spot = null;
outer:
for (let y = 6; y < H - 6; y++)
for (let x = 6; x < W - 6; x++)
if (s.world.tiles.terrain[y * W + x] === 3) { spot = { x: x + .5, y: y + .5 }; break outer; }
if (spot) {
// a colonist on the shore keeps the fog revealed (updateFog stamps units)
const shore = { x: spot.x + 1.5, y: spot.y - 1 };
s.units.push(RTS.entities.makeUnit('colonist', shore.x, shore.y));
const d = RTS.entities.makeDino('sucho', spot.x - 1, spot.y + 0.5, 'final');
d.aggro = true;
s.dinos.push(d);
RTS.render.cam.x = (spot.x + shore.x) / 2;
RTS.render.cam.y = (spot.y + shore.y) / 2;
RTS.render.cam.zoom = 1.3;
s.shakeT = 0; // keep the shot steady
}
s.time = Math.floor(RTS.CONFIG.WORLD.DAY_LENGTH * 0.30); // daylight
});
await page.waitForTimeout(600);
await page.screenshot({ path: OUT + '/lake.png' });
// ---- diorama 3: full assault on the walls ----
await page.evaluate(() => {
const s = RTS.sim.state();
const hq = RTS.sim.hq();
// wall arc between the horde and the colony
for (let k = -4; k <= 4; k++) {
if (k === 0) continue;
const x = Math.round(hq.x + 7), y = Math.round(hq.y + k * 1.15);
if (RTS.sim.canPlace('wall', x, y).ok) {
const r = RTS.sim.place('wall', x, y);
if (r.ok) { r.b.done = true; r.b.progress = 1; }
}
}
// horde: mixed ground force + air support
const comp = { raptor: 8, compy: 12, trike: 2, dilo: 3 };
let i = 0;
for (const type in comp)
for (let k = 0; k < comp[type]; k++) {
const d = RTS.entities.makeDino(type, hq.x + 13 + (i % 4), hq.y - 6 + (i % 13), 'final');
d.aggro = true; i++;
s.dinos.push(d);
}
for (let k = 0; k < 6; k++) {
const d = RTS.entities.makeDino('ptera', hq.x + 9 + (k % 3), hq.y - 4 + k * 1.4, 'final');
d.aggro = true;
s.dinos.push(d);
}
s.time = Math.floor(RTS.CONFIG.WORLD.DAY_LENGTH * 0.62); // night falls on battle
RTS.render.cam.x = hq.x + 5; RTS.render.cam.y = hq.y;
s.warnT = 0;
});
// simulate ~4s so projectiles fly and muzzle flashes show
await page.evaluate(() => { for (let k = 0; k < 30 * 4; k++) RTS.sim.tick(1 / 30); });
await page.waitForTimeout(300);
await page.screenshot({ path: OUT + '/battle2.png' });
console.log('screenshots written:', ['taming.png', 'lake.png', 'battle2.png'].map(f => 'shots/' + f).join(', '));
console.log('console errors:', errors.length ? errors : 'none');
await browser.close();
process.exit(errors.length ? 1 : 0);
})().catch(e => { console.error(e); process.exit(1); });