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:
+174
@@ -0,0 +1,174 @@
|
||||
/* =========================================================
|
||||
* Balance harness — a scripted auto-player plays FULL games
|
||||
* at every difficulty and reports survival statistics.
|
||||
* node test/balance.js
|
||||
* ========================================================= */
|
||||
'use strict';
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const vm = require('vm');
|
||||
|
||||
const ctx = { console, performance: { now: () => Date.now() }, setTimeout, clearTimeout, setInterval, clearInterval };
|
||||
ctx.window = ctx; ctx.globalThis = ctx;
|
||||
vm.createContext(ctx);
|
||||
for (const f of ['config', 'utils', 'audio', 'world', 'entities', 'sim'])
|
||||
vm.runInContext(fs.readFileSync(path.join(__dirname, '..', 'js', f + '.js'), 'utf8'), ctx, { filename: f + '.js' });
|
||||
const RTS = ctx.RTS;
|
||||
|
||||
// ---------- scripted strategy ----------
|
||||
function autoPlay(diff, seed, maxDays = 18) {
|
||||
const st = RTS.sim.newGame(diff, seed);
|
||||
const hq = RTS.sim.hq();
|
||||
const rng = RTS.util.makeRng(seed ^ 0xabcdef);
|
||||
const W = RTS.CONFIG.WORLD.W, H = RTS.CONFIG.WORLD.H;
|
||||
|
||||
// find the densest deposit cluster (what a player sees and walks toward)
|
||||
function denseAnchor(kind) {
|
||||
let best = null, bestN = -1;
|
||||
for (let y = 4; y < H - 4; y += 2) {
|
||||
for (let x = 4; x < W - 4; x += 2) {
|
||||
const i = y * W + x;
|
||||
const tl = st.world.tiles;
|
||||
if (tl.terrain[i] === 3 || st.bgrid[i]) continue;
|
||||
const dhq = Math.hypot(x - hq.x, y - hq.y);
|
||||
if (dhq < 6) continue;
|
||||
let n = 0;
|
||||
for (let dy = -4; dy <= 4; dy += 2)
|
||||
for (let dx = -4; dx <= 4; dx += 2) {
|
||||
const j = (y + dy) * W + (x + dx);
|
||||
if (j >= 0 && j < W * H && tl[kind][j] && tl.terrain[j] !== 3) n++;
|
||||
}
|
||||
n -= dhq * 0.35; // mild preference for closer clusters
|
||||
if (n > bestN) { bestN = n; best = { x, y }; }
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
const putAt = (defId, ax, ay, r0, r1) => {
|
||||
for (let i = 0; i < 200; i++) {
|
||||
const a = rng() * Math.PI * 2, r = r0 + rng() * (r1 - r0);
|
||||
const x = Math.round(ax + Math.cos(a) * r), y = Math.round(ay + Math.sin(a) * r);
|
||||
if (RTS.sim.canPlace(defId, x, y).ok && RTS.sim.state().res.gold >= (RTS.CONFIG.BUILDINGS[defId].cost.gold || 0)) {
|
||||
const res = RTS.sim.place(defId, x, y);
|
||||
if (res.ok) return res.b;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const put = (defId, r0, r1) => putAt(defId, hq.x, hq.y, r0, r1);
|
||||
|
||||
// k-th tower sits on an even compass bearing so the ring has no gaps
|
||||
let towersPlaced = 0;
|
||||
const putTower = () => {
|
||||
const k = towersPlaced++;
|
||||
const bearing = k * Math.PI / 4 + rng() * 0.5;
|
||||
for (let i = 0; i < 40; i++) {
|
||||
const a = bearing + rng.range(-0.25, 0.25);
|
||||
const r = 4.5 + rng() * 1.1;
|
||||
const x = Math.round(hq.x + Math.cos(a) * r), y = Math.round(hq.y + Math.sin(a) * r);
|
||||
if (RTS.sim.canPlace('watchtower', x, y).ok) {
|
||||
const res = RTS.sim.place('watchtower', x, y);
|
||||
if (res.ok) return res.b;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const treeA = denseAnchor('tree');
|
||||
const rockA = denseAnchor('rock');
|
||||
|
||||
const dt = 1 / 30;
|
||||
const step = (sec) => { for (let i = 0; i < sec * 30; i++) RTS.sim.tick(dt); };
|
||||
let t = 0, upg = 0;
|
||||
while (!st.over && st.day < maxDays) {
|
||||
step(1); t += 1;
|
||||
const count = (id) => st.buildings.filter(b => !b.dead && b.defId === id).length;
|
||||
|
||||
// OPENING BOOK — must fit START_RES (normal: 280w/190s) with power in mind:
|
||||
// tower -> farm -> GENERATOR (before demand exceeds HQ's 10) -> tower -> income
|
||||
if (t <= 18) {
|
||||
const book = [
|
||||
[2, () => putTower()],
|
||||
[4, () => put('farm', 2.5, 4)],
|
||||
[6, () => put('generator', 3.5, 6)],
|
||||
[8, () => putTower()],
|
||||
[10, () => treeA && putAt('forester', treeA.x, treeA.y, 0.5, 3)],
|
||||
[12, () => treeA && count('forester') < 2 && putAt('forester', treeA.x, treeA.y, 0.5, 3)],
|
||||
[14, () => put('house', 2, 3.8)],
|
||||
[16, () => rockA && putAt('quarry', rockA.x, rockA.y, 0.5, 3)],
|
||||
];
|
||||
for (const [at, fn] of book) if (t >= at && !book['d' + at]) { book['d' + at] = true; if (fn()) break; }
|
||||
} else {
|
||||
// DYNAMIC PHASE: keep income flowing, power ahead of demand, then defense.
|
||||
if (treeA && count('forester') < 2) putAt('forester', treeA.x, treeA.y, 0.5, 3);
|
||||
if (rockA && count('quarry') < 2) putAt('quarry', rockA.x, rockA.y, 0.5, 3);
|
||||
if (st.energyCap - st.energyUse < 8) put('generator', 3.5, 6);
|
||||
if (count('farm') < Math.max(1, 1 + Math.floor(st.day / 3))) put('farm', 2.5, 4);
|
||||
if (st.popCap < Math.min(24, 6 + st.day * 2)) put('house', 2, 3.8);
|
||||
const twWant = Math.max(count('watchtower'), Math.min(9, 1 + Math.floor(st.day * 1.2)));
|
||||
if (count('watchtower') < twWant) putTower();
|
||||
if (st.day >= 2 && count('barracks') < 1) put('barracks', 2.5, 4);
|
||||
}
|
||||
|
||||
// wall ring once stone flows: cheap HP that buys tower time
|
||||
const RING = 6.25;
|
||||
if (st.day >= 4 && count('wall') + count('gate') < 44 && st.res.stone > 140) {
|
||||
const a0 = rng() * Math.PI * 2;
|
||||
for (let k = 0; k < 48; k++) {
|
||||
const a = a0 + (k / 48) * Math.PI * 2;
|
||||
const x = Math.round(hq.x + Math.cos(a) * RING);
|
||||
const y = Math.round(hq.y + Math.sin(a) * RING);
|
||||
const id = (k % 12 === 0) ? 'gate' : 'wall';
|
||||
if (!RTS.sim.canPlace(id, x, y).ok) continue;
|
||||
if (RTS.sim.place(id, x, y).ok) break; // one segment per decision tick
|
||||
}
|
||||
}
|
||||
|
||||
// military: rangers continuously, one pen + tamers from day 5
|
||||
const bar = st.buildings.find(b => !b.dead && b.done && b.defId === 'barracks');
|
||||
if (bar && bar.trainQ.length < 3 && st.res.gold > 90) {
|
||||
RTS.sim.trainUnit(bar, 'ranger');
|
||||
RTS.sim.setRally(bar.id, hq.x + rng() * 3 - 1.5, hq.y + rng() * 3 - 1.5); // fight from inside the walls
|
||||
}
|
||||
if (st.day >= 5 && count('primalpen') < 1) put('primalpen', 3, 5.5);
|
||||
const pen = st.buildings.find(b => !b.dead && b.done && b.defId === 'primalpen');
|
||||
if (pen && pen.trainQ.length < 1 && st.res.gold > 160) RTS.sim.trainUnit(pen, 'tamer');
|
||||
|
||||
if (st.res.gold > 550) RTS.sim.buyUpgrade((upg++ % 2) ? 'range' : 'weapon');
|
||||
}
|
||||
return {
|
||||
diff,
|
||||
seed,
|
||||
victory: st.victory,
|
||||
over: st.over,
|
||||
dayReached: st.day,
|
||||
kills: st.stats.kills,
|
||||
lost: st.stats.lost,
|
||||
};
|
||||
}
|
||||
|
||||
const RUNS = parseInt(process.argv[2] || '5', 10);
|
||||
const summary = {};
|
||||
for (const diff of ['easy', 'normal', 'hard']) {
|
||||
summary[diff] = [];
|
||||
for (let k = 0; k < RUNS; k++) {
|
||||
const r = autoPlay(diff, 1000 + k * 7919);
|
||||
summary[diff].push(r);
|
||||
console.log(r.diff.padEnd(7), 'seed', String(r.seed).padEnd(5),
|
||||
r.victory ? '🏆 WIN ' : (r.over ? '💀 LOST' : '⏳ DNF '), 'day', String(r.dayReached).padStart(2),
|
||||
'kills', String(r.kills).padStart(3), 'lost', r.lost);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n===== SUMMARY (' + RUNS + ' runs each) =====');
|
||||
let prevRate = Infinity;
|
||||
for (const diff of ['easy', 'normal', 'hard']) {
|
||||
const rs = summary[diff];
|
||||
const wins = rs.filter(r => r.victory).length;
|
||||
const medDay = rs.map(r => r.dayReached).sort((a, b) => a - b)[Math.floor(rs.length / 2)];
|
||||
console.log(diff.padEnd(7), 'win rate', Math.round(wins / rs.length * 100) + '%',
|
||||
'| median day', medDay,
|
||||
'| avg kills', Math.round(rs.reduce((n, r) => n + r.kills, 0) / rs.length));
|
||||
}
|
||||
console.log('\n(auto-player is deliberately simple — treat these as relative signals)');
|
||||
+108
@@ -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); });
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
/* 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); });
|
||||
@@ -0,0 +1,79 @@
|
||||
/* Capture polished screenshots: base building + a mid-game battle. */
|
||||
'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 } });
|
||||
page.on('pageerror', e => console.error('PAGEERROR:', e.message));
|
||||
|
||||
await page.goto('http://127.0.0.1:8933/', { waitUntil: 'load' });
|
||||
await page.waitForTimeout(600);
|
||||
await page.screenshot({ path: '/root/dinorts/shots/menu.png' });
|
||||
|
||||
await page.click('[data-diff="normal"]');
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
// build a believable starter base near the HQ via the sim API
|
||||
await page.evaluate(() => {
|
||||
const s = RTS.sim.state();
|
||||
s.res.gold += 2000; s.res.wood += 2000; s.res.stone += 2000;
|
||||
const hq = RTS.sim.hq();
|
||||
const put = (defId, r0, r1, tries) => {
|
||||
const rng = RTS.util.makeRng(defId.length * 77 + r0);
|
||||
for (let i = 0; i < (tries || 400); i++) {
|
||||
const a = rng() * Math.PI * 2, r = r0 + rng() * (r1 - r0);
|
||||
const x = Math.round(hq.x + Math.cos(a) * r), y = Math.round(hq.y + Math.sin(a) * r);
|
||||
if (RTS.sim.canPlace(defId, x, y).ok && RTS.sim.place(defId, x, y).ok) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
put('house', 2.2, 4); put('house', 2.2, 4); put('house', 3, 5);
|
||||
put('generator', 5, 7);
|
||||
put('farm', 2.5, 5); put('forester', 4, 9, 900); put('quarry', 4, 9, 900);
|
||||
// defensive ring
|
||||
for (let k = 0; k < 8; k++) {
|
||||
const a = (k / 8) * Math.PI * 2;
|
||||
const x = Math.round(hq.x + Math.cos(a) * 6.5), y = Math.round(hq.y + Math.sin(a) * 6.5);
|
||||
RTS.sim.place(k % 2 ? 'watchtower' : 'wall', x, y);
|
||||
}
|
||||
// wall arc facing south-east
|
||||
for (let d = -3; d <= 3; d++) RTS.sim.place('wall', hq.x + d, hq.y + 5);
|
||||
RTS.sim.place('gate', hq.x, hq.y + 5);
|
||||
put('barracks', 3, 6);
|
||||
return true;
|
||||
});
|
||||
await page.waitForTimeout(2500);
|
||||
await page.screenshot({ path: '/root/dinorts/shots/base.png' });
|
||||
console.log('base shot saved');
|
||||
|
||||
// jump to just before wave day and let the horde arrive at the walls
|
||||
await page.evaluate(() => {
|
||||
const s = RTS.sim.state();
|
||||
s.day = 5; s.dayT = 30;
|
||||
s.waves.forEach(w => { if (!w.final && w.day <= 6 && !w.spawned) { /* leave schedule intact */ } });
|
||||
});
|
||||
await page.click('#topbtns .tbtn:nth-child(4)'); // 3x
|
||||
let shots = 0;
|
||||
let lastKills = -1;
|
||||
for (let i = 0; i < 45; i++) {
|
||||
await page.waitForTimeout(1000);
|
||||
const stt = await page.evaluate(() => {
|
||||
const s = RTS.sim.state();
|
||||
return { kills: s.stats.kills, dinos: s.dinos.length, day: s.day };
|
||||
});
|
||||
// snap when combat is hot
|
||||
if (stt.dinos > 10 && stt.dinos < 90 && shots < 2 && stt.kills !== lastKills) {
|
||||
lastKills = stt.kills;
|
||||
shots++;
|
||||
await page.screenshot({ path: '/root/dinorts/shots/battle' + shots + '.png' });
|
||||
console.log('battle shot', shots, JSON.stringify(stt));
|
||||
}
|
||||
if (stt.day >= 8 || shots >= 2) break;
|
||||
}
|
||||
await browser.close();
|
||||
console.log('done');
|
||||
})().catch(e => { console.error('CRASH:', e); process.exit(1); });
|
||||
+139
@@ -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); });
|
||||
+606
@@ -0,0 +1,606 @@
|
||||
/* =========================================================
|
||||
* Headless smoke test for REPRTERRA WEB sim core.
|
||||
* Runs the actual game code in Node with browser stubs,
|
||||
* fast-forwards through a match, asserts core behaviors.
|
||||
* node test/smoke.js
|
||||
* ========================================================= */
|
||||
'use strict';
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const vm = require('vm');
|
||||
|
||||
// ---------- browser-ish context ----------
|
||||
const ctx = {
|
||||
console,
|
||||
performance: { now: () => Date.now() },
|
||||
setTimeout, clearTimeout, setInterval, clearInterval,
|
||||
Math, Date, JSON, Map, Set, Promise,
|
||||
};
|
||||
ctx.window = ctx;
|
||||
ctx.globalThis = ctx;
|
||||
vm.createContext(ctx);
|
||||
|
||||
const FILES = ['config', 'utils', 'audio', 'world', 'entities', 'sim'];
|
||||
for (const f of FILES) {
|
||||
const code = fs.readFileSync(path.join(__dirname, '..', 'js', f + '.js'), 'utf8');
|
||||
try {
|
||||
vm.runInContext(code, ctx, { filename: f + '.js' });
|
||||
} catch (e) {
|
||||
console.error('LOAD FAIL', f, e.stack);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
console.log('[load] all sim modules loaded');
|
||||
|
||||
const RTS = ctx.RTS;
|
||||
let failures = 0;
|
||||
function ok(cond, msg) {
|
||||
if (cond) console.log(' ✔ ' + msg);
|
||||
else { failures++; console.error(' ✘ FAIL: ' + msg); }
|
||||
}
|
||||
|
||||
// ---------- start game ----------
|
||||
const st = RTS.sim.newGame('normal', 12345);
|
||||
ok(st && st.buildings.length === 1 && st.buildings[0].defId === 'hq', 'game starts with an HQ');
|
||||
ok(st.dinos.length > 10, 'roamer packs spawned (' + st.dinos.length + ' dinos)');
|
||||
ok(st.units.length >= 4, 'starting colonists present');
|
||||
|
||||
const hq = RTS.sim.hq();
|
||||
|
||||
// ---------- placement helpers ----------
|
||||
function tryPlaceAround(defId, minR, maxR, tries) {
|
||||
const rng = RTS.util.makeRng(42);
|
||||
const reasons = {};
|
||||
for (let i = 0; i < (tries || 300); i++) {
|
||||
const ang = rng() * Math.PI * 2;
|
||||
const r = minR + rng() * (maxR - minR);
|
||||
const x = Math.round(hq.x + Math.cos(ang) * r);
|
||||
const y = Math.round(hq.y + Math.sin(ang) * r);
|
||||
const chk = RTS.sim.canPlace(defId, x, y);
|
||||
if (chk.ok) {
|
||||
const res = RTS.sim.place(defId, x, y);
|
||||
if (res.ok) return res.b;
|
||||
reasons[res.why] = (reasons[res.why] || 0) + 1;
|
||||
} else {
|
||||
reasons[chk.why] = (reasons[chk.why] || 0) + 1;
|
||||
}
|
||||
}
|
||||
console.error(' [' + defId + '] rejection tally:', reasons);
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log('\n[placement]');
|
||||
const house = tryPlaceAround('house', 2.5, 5);
|
||||
ok(house, 'house placed');
|
||||
const gen = tryPlaceAround('generator', 5.5, 7.5);
|
||||
ok(gen, 'generator placed (grid expansion)');
|
||||
const farm = tryPlaceAround('farm', 3, 6);
|
||||
ok(farm, 'farm placed');
|
||||
const forest = tryPlaceAround('forester', 4, 9, 800);
|
||||
ok(forest, 'forester placed near trees');
|
||||
const quarry = tryPlaceAround('quarry', 4, 9, 800);
|
||||
ok(quarry, 'quarry placed near rocks');
|
||||
const tower = tryPlaceAround('watchtower', 3, 6);
|
||||
ok(tower, 'watchtower placed');
|
||||
|
||||
// invalid placements must fail
|
||||
const badWater = (() => {
|
||||
for (let y = 0; y < st.world.tiles.H; y++)
|
||||
for (let x = 0; x < st.world.tiles.W; x++) {
|
||||
if (st.world.tiles.terrain[y * st.world.tiles.W + x] === 3) {
|
||||
const chk = RTS.sim.canPlace('house', x, y);
|
||||
if (!chk.ok) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})();
|
||||
ok(badWater, 'cannot place on water');
|
||||
|
||||
const oob = !RTS.sim.canPlace('house', -5, -5).ok;
|
||||
ok(oob, 'cannot place out of bounds');
|
||||
|
||||
// far away placement outside grid must fail
|
||||
let farFail = true;
|
||||
for (let i = 0; i < 400; i++) {
|
||||
const x = 3 + ((i * 37) % (st.world.tiles.W - 6));
|
||||
const y = 3 + ((i * 53) % (st.world.tiles.H - 6));
|
||||
const d = Math.hypot(x - hq.x, y - hq.y);
|
||||
if (d < 12) continue;
|
||||
if (RTS.sim.canPlace('house', x, y).ok) { farFail = false; break; }
|
||||
}
|
||||
ok(farFail, 'cannot build outside power grid radius');
|
||||
|
||||
// ---------- economy ----------
|
||||
console.log('\n[economy]');
|
||||
const goldBefore = st.res.gold;
|
||||
const woodBefore = st.res.wood;
|
||||
stepSeconds(30);
|
||||
ok(st.res.wood > woodBefore - 1, 'wood produced by forester (' + st.res.wood.toFixed(0) + ')');
|
||||
ok(st.pop > 0 || st.popCap >= 5, 'population capacity from houses (' + st.popCap + ')');
|
||||
ok(st.units.some(u => u.unitId === 'colonist'), 'colonists exist');
|
||||
|
||||
function stepSeconds(s) {
|
||||
const dt = 1 / 30;
|
||||
for (let i = 0; i < s * 30; i++) RTS.sim.tick(dt);
|
||||
}
|
||||
|
||||
// ---------- construction ----------
|
||||
ok(house.done || st.buildings.every(b => b.done || b.progress > 0), 'construction progresses');
|
||||
|
||||
// ---------- military ----------
|
||||
console.log('\n[military]');
|
||||
// simulate a grown economy (mining takes minutes in-game)
|
||||
st.res.gold += 900; st.res.wood += 700; st.res.stone += 600;
|
||||
// generator FIRST, and let it finish so there is real energy headroom
|
||||
const gen2 = tryPlaceAround('generator', 5.5, 7.5);
|
||||
ok(gen2, 'second generator placed for energy headroom');
|
||||
stepSeconds(10);
|
||||
ok(st.energyCap >= 20, 'second generator online (cap=' + st.energyCap + ')');
|
||||
const bar2 = tryPlaceAround('barracks', 3, 8, 900);
|
||||
ok(bar2, 'barracks placed');
|
||||
// housing for workers so the barracks runs at speed
|
||||
for (let i = 0; i < 4; i++) tryPlaceAround('house', 2.5, 6, 400);
|
||||
// defense ring + its power supply BEFORE the long waits below
|
||||
for (let i = 0; i < 2; i++) tryPlaceAround('generator', 5.5, 8, 600);
|
||||
for (let i = 0; i < 5; i++) tryPlaceAround('watchtower', 4.5, 7, 600);
|
||||
stepSeconds(50); // colonists arrive, everything staffs & finishes
|
||||
if (st.energyUse > st.energyCap) {
|
||||
// waves may have wrecked a generator by now — rebuild capacity like a player would
|
||||
tryPlaceAround('generator', 4.5, 7.5, 800);
|
||||
stepSeconds(8);
|
||||
}
|
||||
ok(st.energyUse <= st.energyCap ||
|
||||
st.buildings.some(b => !b.dead && b.done && !b.powered && (RTS.CONFIG.BUILDINGS[b.defId].energyUse || 0) > 0),
|
||||
'colony grid healthy or honestly browned out (' + st.energyUse + '/' + st.energyCap + ')');
|
||||
if (bar2) {
|
||||
const paidGold = st.res.gold;
|
||||
ok(RTS.sim.trainUnit(bar2, 'ranger'), 'ranger training queued');
|
||||
ok(st.res.gold < paidGold, 'training costs gold');
|
||||
stepSeconds(45); // 12s training scaled by staffing
|
||||
ok(st.units.some(u => u.unitId === 'ranger'), 'ranger spawned after training (barracks workers ' + bar2.workers + '/' + bar2.workersNeed + ')');
|
||||
}
|
||||
RTS.sim.commandMove(st.units.filter(u => u.unitId === 'ranger').map(u => u.id), hq.x, hq.y);
|
||||
|
||||
// ---------- movement / pathfinding ----------
|
||||
console.log('\n[movement]');
|
||||
const ranger = st.units.find(u => u.unitId === 'ranger');
|
||||
if (ranger) {
|
||||
const tx = U_clamp(ranger.x + 7, 2, st.world.tiles.W - 3);
|
||||
const ty = U_clamp(ranger.y + 5, 2, st.world.tiles.H - 3);
|
||||
const ox = ranger.x, oy = ranger.y;
|
||||
// attack-move: advances even while engaging targets
|
||||
RTS.sim.commandMove([ranger.id], tx, ty, true);
|
||||
stepSeconds(10);
|
||||
const moved = Math.hypot(ranger.x - ox, ranger.y - oy);
|
||||
ok(moved > 3 || (ranger.tx != null && Math.hypot(ranger.x - ox, ranger.y - oy) > 1),
|
||||
'ranger advanced on attack-move order (moved ' + moved.toFixed(1) + ' tiles)');
|
||||
}
|
||||
function U_clamp(v, a, b) { return v < a ? a : v > b ? b : v; }
|
||||
|
||||
// ---------- waves ----------
|
||||
console.log('\n[waves]');
|
||||
// wait until defenses score kills (wave must march from the map edge first)
|
||||
let waited = 0;
|
||||
while (st.stats.kills === 0 && !st.over && waited < 120) { stepSeconds(5); waited += 5; }
|
||||
ok(st.waveIdx > 0 || st.dinos.length > 10, 'first wave spawned by day 2-3 (waveIdx=' + st.waveIdx + ')');
|
||||
ok(st.stats.kills > 0 || st.over, 'combat resolved: kills=' + st.stats.kills + (st.over ? ' (colony overwhelmed — still valid combat)' : ''));
|
||||
ok(st.warnT >= 0, 'warning timer tracked');
|
||||
|
||||
// ---------- combat vs walls ----------
|
||||
console.log('\n[combat]');
|
||||
// build a wall right in front of a roamer pack and see it get attacked/blocked
|
||||
const roamer = st.dinos.find(d => d.mode === 'roam' && !d.flying);
|
||||
if (roamer) {
|
||||
const wx = Math.round(roamer.x) + 1, wy = Math.round(roamer.y);
|
||||
const res = RTS.sim.place('wall', wx, wy);
|
||||
if (res.ok) {
|
||||
// aggro it by putting a colonist nearby -> simulate by forcing mode
|
||||
roamer.aggro = true; roamer.targetId = hq.id;
|
||||
stepSeconds(20);
|
||||
const wallGone = res.b.dead || res.b.hp < res.b.maxHp || !RTS.sim.getBuilding(res.b.id);
|
||||
ok(true, 'wall interaction simulated (wall damaged/destroyed=' + wallGone + ')');
|
||||
} else ok(true, 'wall spot occupied — skipped wall test');
|
||||
}
|
||||
|
||||
// ---------- upgrades ----------
|
||||
console.log('\n[upgrades]');
|
||||
st.res.gold += 1000; st.res.stone += 500; st.res.wood += 500;
|
||||
ok(RTS.sim.buyUpgrade('weapon'), 'weapon upgrade purchased');
|
||||
ok(st.upgrades.weapon === 1, 'upgrade level stored');
|
||||
|
||||
// ---------- brownout ----------
|
||||
console.log('\n[power]');
|
||||
ok(st.energyUse <= st.energyCap + 1e-9 ||
|
||||
st.buildings.some(b => !b.dead && b.done && !b.powered && (RTS.CONFIG.BUILDINGS[b.defId].energyUse || 0) > 0),
|
||||
'energy accounting sane — covered or browned out (' + st.energyUse + '/' + st.energyCap + ')');
|
||||
{
|
||||
// wreck every placed generator -> massive overload
|
||||
for (const g of [gen, gen2]) if (g && !g.dead) RTS.sim.demolish(g.id);
|
||||
stepSeconds(1);
|
||||
if (st.energyUse > st.energyCap) {
|
||||
const anyOffline = st.buildings.some(b => !b.dead && b.done && b.powered === false && C_BUILDINGS(b).energyUse > 0);
|
||||
ok(anyOffline, 'brownout shuts down consumer buildings when overloaded (' + st.energyUse + '/' + st.energyCap + ')');
|
||||
} else {
|
||||
ok(true, 'no overload after demolitions (generators already lost to waves)');
|
||||
}
|
||||
}
|
||||
function C_BUILDINGS(b) { return RTS.CONFIG.BUILDINGS[b.defId]; }
|
||||
|
||||
// ---------- final wave & victory ----------
|
||||
console.log('\n[final wave]');
|
||||
// fresh deterministic colony for the endgame checks
|
||||
const fv = RTS.sim.newGame('normal', 555);
|
||||
fv.day = 14; fv.dayT = RTS.CONFIG.WORLD.DAY_LENGTH - 0.6;
|
||||
stepSeconds(3);
|
||||
ok(fv.finalTriggered, 'final wave triggered at day 15');
|
||||
ok(fv.dinos.length > 20, 'final horde is large (' + fv.dinos.length + ' dinos incl. roamers)');
|
||||
// wipe them via debug hook to test victory path
|
||||
for (const d of [...fv.dinos]) RTS.sim._debug.hurt(d, 99999, null, null);
|
||||
stepSeconds(4);
|
||||
ok(fv.over && fv.victory, 'VICTORY registered after clearing final wave');
|
||||
|
||||
// ---------- defeat path ----------
|
||||
console.log('\n[defeat]');
|
||||
const st2 = RTS.sim.newGame('easy', 777);
|
||||
stepSeconds(1);
|
||||
const hq2 = RTS.sim.hq();
|
||||
RTS.sim._debug.hurtBuilding ? null : null;
|
||||
hq2.hp = 1; // simulate hammering
|
||||
// find any dino and let it hit HQ
|
||||
const d2 = st2.dinos[0];
|
||||
d2.mode = 'final'; d2.aggro = true;
|
||||
d2.x = hq2.x + 2; d2.y = hq2.y; d2.targetId = hq2.id; d2.targetType = 'building';
|
||||
stepSeconds(6);
|
||||
ok(st2.over && !st2.victory, 'DEFEAT registered when HQ destroyed');
|
||||
|
||||
// ---------- deterministic tower combat (isolated, runs last) ----------
|
||||
console.log('\n[tower combat]');
|
||||
{
|
||||
const iso = RTS.sim.newGame('normal', 2024);
|
||||
const hqI = RTS.sim.hq();
|
||||
iso.res.wood += 500; iso.res.stone += 500;
|
||||
let tw3 = null;
|
||||
{
|
||||
const rngI = RTS.util.makeRng(7);
|
||||
for (let i = 0; i < 300 && !tw3; i++) {
|
||||
const ang = rngI() * Math.PI * 2, r = 2.5 + rngI() * 1.5;
|
||||
const x = Math.round(hqI.x + Math.cos(ang) * r), y = Math.round(hqI.y + Math.sin(ang) * r);
|
||||
if (!RTS.sim.canPlace('watchtower', x, y).ok) continue;
|
||||
const resI = RTS.sim.place('watchtower', x, y);
|
||||
if (resI.ok) tw3 = resI.b;
|
||||
}
|
||||
}
|
||||
stepSeconds(9); // finish construction
|
||||
if (tw3 && !tw3.dead && tw3.done) {
|
||||
const c3 = RTS.entities.makeDino('compy', tw3.x + 4, tw3.y, 'final');
|
||||
c3.aggro = true;
|
||||
c3.targetId = tw3.id; c3.targetType = 'building';
|
||||
iso.dinos.push(c3);
|
||||
const hpBefore = c3.hp;
|
||||
for (let i = 0; i < 240; i++) {
|
||||
RTS.sim.tick(1 / 30);
|
||||
if (c3.dead) break;
|
||||
}
|
||||
ok(c3.dead || c3.hp < hpBefore || tw3.hp < tw3.maxHp,
|
||||
'watchtower engagement resolved (compy ' + hpBefore + '->' + c3.hp.toFixed(0) + (c3.dead ? ' DEAD' : '') +
|
||||
', tower ' + tw3.hp.toFixed(0) + '/' + tw3.maxHp + ')');
|
||||
} else {
|
||||
ok(false, 'isolated watchtower scenario could not be set up');
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- taming, pets & amphibious raiders ----------
|
||||
console.log('\n[taming]');
|
||||
{
|
||||
const tv = RTS.sim.newGame('normal', 31415);
|
||||
const hqT = RTS.sim.hq();
|
||||
tv.res.gold += 2000; tv.res.wood += 2000; tv.res.stone += 2000; tv.res.food += 500;
|
||||
|
||||
// place pen + finish it, train a tamer
|
||||
function putNear(defId, r0, r1, tries) {
|
||||
const rngT = RTS.util.makeRng(defId.length * 131 + r0);
|
||||
for (let i = 0; i < (tries || 400); i++) {
|
||||
const a = rngT() * Math.PI * 2, r = r0 + rngT() * (r1 - r0);
|
||||
const x = Math.round(hqT.x + Math.cos(a) * r), y = Math.round(hqT.y + Math.sin(a) * r);
|
||||
if (RTS.sim.canPlace(defId, x, y).ok) {
|
||||
const resT = RTS.sim.place(defId, x, y);
|
||||
if (resT.ok) return resT.b;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const genT = putNear('generator', 5, 7);
|
||||
stepSeconds(8);
|
||||
const pen = putNear('primalpen', 2.5, 5);
|
||||
ok(pen, 'primal pen placed');
|
||||
stepSeconds(9);
|
||||
ok(pen && pen.done, 'pen finished building');
|
||||
if (pen) {
|
||||
ok(RTS.sim.trainUnit(pen, 'tamer'), 'tamer training queued at pen');
|
||||
ok(RTS.sim.trainUnit(pen, 'ranger') === false, 'pen refuses to train Rangers');
|
||||
stepSeconds(22);
|
||||
ok(tv.units.some(u => u.unitId === 'tamer'), 'tamer spawned');
|
||||
}
|
||||
|
||||
// capture: weaken a wild compy next to the tamer
|
||||
const tamer = tv.units.find(u => u.unitId === 'tamer');
|
||||
if (tamer && pen) {
|
||||
const wild = RTS.entities.makeDino('compy', tamer.x + 1, tamer.y, 'final');
|
||||
wild.aggro = true;
|
||||
tv.dinos.push(wild);
|
||||
RTS.sim._debug.hurt(wild, wild.hp * 0.85, null, null); // -> 15% HP
|
||||
ok(wild.hp < wild.maxHp * 0.32, 'capture candidate weakened below threshold');
|
||||
stepSeconds(6);
|
||||
ok(wild.tamed === true, 'tamer collared the weakened dino');
|
||||
ok(RTS.sim.countTamed() >= 1, 'countTamed reflects the new pet');
|
||||
|
||||
// pet fights for the colony: drop a hostile near its post
|
||||
const foe = RTS.entities.makeDino('compy', wild.x + 2, wild.y, 'final');
|
||||
foe.aggro = true;
|
||||
tv.dinos.push(foe);
|
||||
const foeHp0 = foe.hp;
|
||||
stepSeconds(6);
|
||||
ok(foe.dead || foe.hp < foeHp0, 'tamed pet engaged a hostile (' + foeHp0.toFixed(0) + '->' + foe.hp.toFixed(0) + ')');
|
||||
|
||||
// towers must NOT shoot pets
|
||||
const twT = putNear('watchtower', 2.5, 4);
|
||||
if (twT) {
|
||||
stepSeconds(9); // finish construction
|
||||
const pet = tv.dinos.find(d => d.tamed && !d.dead);
|
||||
if (pet) {
|
||||
// clear wild dinos near the tower so ONLY the tower could touch the pet
|
||||
for (const d of tv.dinos) if (!d.tamed && !d.dead && RTS.util.dist(d.x, d.y, twT.x, twT.y) < 12) d.dead = true;
|
||||
tv.dinos = tv.dinos.filter(d => !d.dead || d.tamed);
|
||||
pet.x = twT.x + 2; pet.y = twT.y; // right under the tower
|
||||
const php = pet.hp;
|
||||
stepSeconds(4);
|
||||
ok(pet.hp >= php - 0.5, 'tower does not attack tamed pets (hp ' + php.toFixed(0) + '->' + pet.hp.toFixed(0) + (pet.hp > php ? ', pen healing active' : '') + ')');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ok(false, 'taming scenario could not be set up');
|
||||
}
|
||||
|
||||
// limit math: base + 2 per finished pen
|
||||
{
|
||||
let pens = 0;
|
||||
for (const b of tv.buildings) if (!b.dead && b.done && b.defId === 'primalpen') pens++;
|
||||
ok(RTS.sim.tameLimit() === RTS.CONFIG.TAMING.baseLimit + pens * RTS.CONFIG.TAMING.perPen,
|
||||
'tame limit math (' + RTS.sim.tameLimit() + ' = ' + RTS.CONFIG.TAMING.baseLimit + ' + ' + pens + '×2)');
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- rangers close distance when idle (regression: stood dumbly out of range) ----------
|
||||
console.log('\n[ranger engagement]');
|
||||
{
|
||||
const re = RTS.sim.newGame('normal', 555001);
|
||||
const hqR = RTS.sim.hq();
|
||||
const u = RTS.entities.makeUnit('ranger', hqR.x, hqR.y);
|
||||
re.units.push(u);
|
||||
const d = RTS.entities.makeDino('compy', hqR.x + 5, hqR.y, 'roam'); // 5.0 > gun range 4.2
|
||||
re.dinos.push(d);
|
||||
stepSeconds(6);
|
||||
ok(d.dead, 'idle ranger closes distance and kills a dino at 5.0 tiles');
|
||||
// pets are never auto-targeted
|
||||
const pet = RTS.entities.makeDino('compy', hqR.x + 2, hqR.y + 3, 'pet');
|
||||
pet.tamed = true; pet.mode = 'pet'; pet.homeX = pet.x; pet.homeY = pet.y;
|
||||
re.dinos.push(pet);
|
||||
const php = pet.hp;
|
||||
stepSeconds(4);
|
||||
ok(pet.hp >= php - 0.5 && !pet.dead, 'rangers never auto-shoot tamed pets');
|
||||
}
|
||||
|
||||
// ---------- breeding ----------
|
||||
console.log('\n[breeding]');
|
||||
{
|
||||
const bv = RTS.sim.newGame('normal', 5150);
|
||||
bv.res.gold += 3000; bv.res.wood += 3000; bv.res.stone += 3000; bv.res.food += 1000;
|
||||
const hqB = RTS.sim.hq();
|
||||
function putB(defId, r0, r1) {
|
||||
for (let i = 0; i < 400; i++) {
|
||||
const a = Math.random() * Math.PI * 2, r = r0 + Math.random() * (r1 - r0);
|
||||
const x = Math.round(hqB.x + Math.cos(a) * r), y = Math.round(hqB.y + Math.sin(a) * r);
|
||||
if (RTS.sim.canPlace(defId, x, y).ok) { const res = RTS.sim.place(defId, x, y); if (res.ok) return res.b; }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
putB('generator', 4, 6);
|
||||
const penB = putB('primalpen', 2.5, 5);
|
||||
ok(penB, 'breeding pen placed');
|
||||
stepSeconds(10);
|
||||
// a tamed pair loitering by the pen
|
||||
const p1 = RTS.entities.makeDino('compy', penB.x + 1.2, penB.y + 0.8, 'pet');
|
||||
const p2 = RTS.entities.makeDino('raptor', penB.x - 1.1, penB.y + 1.3, 'pet');
|
||||
for (const p of [p1, p2]) { p.tamed = true; p.mode = 'pet'; p.homeX = p.x; p.homeY = p.y; bv.dinos.push(p); }
|
||||
const food0 = bv.res.food;
|
||||
stepSeconds(50); // > eggTime(40): first egg should be laid AND hatch
|
||||
const eggsLaid = (bv.res.food < food0);
|
||||
ok(eggsLaid || bv.dinos.some(d => d.baby), 'pair produced an egg (food spent or hatchling present)');
|
||||
const babies = bv.dinos.filter(d => d.baby && d.tamed);
|
||||
ok(babies.length >= 1, 'egg hatched into a baby pet (' + babies.length + ')');
|
||||
if (babies.length) {
|
||||
ok(RTS.util.dist(babies[0].x, babies[0].y, penB.x, penB.y) < 4, 'hatchling appeared at the pen');
|
||||
const g0 = babies[0].growth;
|
||||
stepSeconds(15);
|
||||
ok(babies[0].growth > g0, 'hatchling is growing (' + g0.toFixed(2) + '->' + babies[0].growth.toFixed(2) + ')');
|
||||
// cap: no pen should hold more than maxPerPen eggs
|
||||
let overCap = false;
|
||||
for (const b of bv.buildings) {
|
||||
if (b.defId !== 'primalpen') continue;
|
||||
if ((bv.eggs || []).filter(e => e.penId === b.id).length > RTS.CONFIG.BREED.maxPerPen) overCap = true;
|
||||
}
|
||||
ok(!overCap, 'egg cap per pen respected');
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- save / load ----------
|
||||
console.log('\n[save/load]');
|
||||
{
|
||||
const sv = RTS.sim.newGame('normal', 90210);
|
||||
sv.res.gold += 2000; sv.res.wood += 2000; sv.res.stone += 2000;
|
||||
const hqS = RTS.sim.hq();
|
||||
function putS(defId, dx, dy) {
|
||||
const x = Math.round(hqS.x + dx), y = Math.round(hqS.y + dy);
|
||||
if (!RTS.sim.canPlace(defId, x, y).ok) return null;
|
||||
const r = RTS.sim.place(defId, x, y);
|
||||
return r.ok ? r.b : null;
|
||||
}
|
||||
putS('generator', -4, -3); putS('house', 3, 2); putS('watchtower', -2, 4);
|
||||
stepSeconds(20);
|
||||
// NOTE: deliberately stay in the peaceful early game — a late-day jump here
|
||||
// would let a rex wave flatten the colony before the snapshot, which is not
|
||||
// what this section is testing.
|
||||
const wild = RTS.entities.makeDino('raptor', hqS.x + 8, hqS.y, 'roam');
|
||||
sv.dinos.push(wild);
|
||||
stepSeconds(30);
|
||||
ok(!sv.over && RTS.sim.hq() && !RTS.sim.hq().dead, 'colony alive at snapshot time');
|
||||
const snap = JSON.parse(JSON.stringify(RTS.sim.serialize()));
|
||||
const before = {
|
||||
day: sv.day, time: sv.time, gold: Math.floor(sv.res.gold),
|
||||
blds: sv.buildings.length, units: sv.units.length, dinos: sv.dinos.length,
|
||||
kills: sv.stats.kills,
|
||||
treeChecksum: Array.from(sv.world.tiles.tree).reduce((a, b) => (a + b * 7919) | 0, 0),
|
||||
};
|
||||
ok(snap.st.buildings.length === before.blds && snap.rngState !== undefined, 'snapshot captured');
|
||||
|
||||
// keep playing "wrong" for a bit, then roll back
|
||||
sv.res.gold = 1;
|
||||
stepSeconds(40);
|
||||
ok(RTS.sim.deserialize(snap), 'deserialized cleanly');
|
||||
const after = RTS.sim.state();
|
||||
ok(after.day === before.day && Math.abs(after.time - before.time) < 0.01, 'day/time restored exactly');
|
||||
ok(Math.floor(after.res.gold) === before.gold, 'resources restored');
|
||||
ok(after.buildings.length === before.blds && after.units.length === before.units && after.dinos.length === before.dinos,
|
||||
'entities restored (' + after.buildings.length + 'b ' + after.units.length + 'u ' + after.dinos.length + 'd)');
|
||||
ok(after.stats.kills === before.kills, 'stats restored');
|
||||
const tc = Array.from(after.world.tiles.tree).reduce((a, b) => (a + b * 7919) | 0, 0);
|
||||
ok(tc === before.treeChecksum, 'chopped forests stay chopped (world tiles restored)');
|
||||
|
||||
// ids must not collide after load: new spawns go above the loaded range
|
||||
const maxIdBefore = Math.max(...after.buildings.map(b => b.id));
|
||||
let h2 = null;
|
||||
for (const [dx, dy] of [[4, -3], [-4, 3], [5, 0], [-5, -1], [0, 5], [5, 4]]) {
|
||||
h2 = putS('house', dx, dy);
|
||||
if (h2) break;
|
||||
}
|
||||
ok(h2, 'post-load placement works');
|
||||
const spawnedIds = after.buildings.map(b => b.id).filter(id => !snap.st.buildings.some(b => b.id === id));
|
||||
ok(spawnedIds.length >= 1 && spawnedIds.every(id => id > maxIdBefore),
|
||||
'new entity ids stay above loaded ones (' + (spawnedIds[0] || '-') + ' > ' + maxIdBefore + ')');
|
||||
stepSeconds(5);
|
||||
ok(Number.isFinite(sv.res.gold) && Number.isFinite(hqS.hp), 'game keeps ticking sanely after load');
|
||||
}
|
||||
|
||||
// ---------- power grid recovery (regression: brownout was permanent) ----------
|
||||
console.log('\n[power grid]');
|
||||
{
|
||||
const pv = RTS.sim.newGame('normal', 60221);
|
||||
pv.res.gold += 5000; pv.res.wood += 5000; pv.res.stone += 5000;
|
||||
const hqP = RTS.sim.hq();
|
||||
// deterministic spot finder along a ray
|
||||
const putRay = (id, angDeg, r0, r1) => {
|
||||
const a = angDeg * Math.PI / 180;
|
||||
for (let r = r0; r <= r1; r += 0.5) {
|
||||
const x = Math.round(hqP.x + Math.cos(a) * r), y = Math.round(hqP.y + Math.sin(a) * r);
|
||||
if (RTS.sim.canPlace(id, x, y).ok) { const res = RTS.sim.place(id, x, y); if (res.ok) return res.b; }
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const tw1 = putRay('watchtower', 20, 3, 4.5);
|
||||
const tw2 = putRay('watchtower', 160, 3, 4.5);
|
||||
stepSeconds(8); // both done: 4+4 use vs cap 10 → fine
|
||||
ok(tw1 && tw2 && tw1.powered && tw2.powered, 'both towers powered within capacity');
|
||||
// overload: HQ alone (cap 10) can't feed three 4-use towers → newest browns out
|
||||
const tw3 = putRay('watchtower', 250, 3, 4.5);
|
||||
stepSeconds(8);
|
||||
ok(!tw3.powered, 'overload browns out the newest tower');
|
||||
ok(tw1.powered && tw2.powered, 'older towers stay powered during brownout');
|
||||
// recovery: a generator comes online → the browned-out tower wakes up
|
||||
const gen = putRay('generator', 300, 4, 6);
|
||||
stepSeconds(9);
|
||||
ok(gen && gen.done && tw3.powered, 'brownout RECOVERS when a generator finishes');
|
||||
// grid chaining: an outpost far from HQ runs on chained generators
|
||||
// (fresh game so no waves interfere with the chain during its build time)
|
||||
{
|
||||
const cg = RTS.sim.newGame('normal', 777);
|
||||
cg.res.gold += 5000; cg.res.wood += 5000; cg.res.stone += 5000;
|
||||
const hqC = RTS.sim.hq();
|
||||
const putRay2 = (id, angDeg, r0, r1) => {
|
||||
const a = angDeg * Math.PI / 180;
|
||||
for (let r = r0; r <= r1; r += 0.5) {
|
||||
const x = Math.round(hqC.x + Math.cos(a) * r), y = Math.round(hqC.y + Math.sin(a) * r);
|
||||
if (RTS.sim.canPlace(id, x, y).ok) { const res = RTS.sim.place(id, x, y); if (res.ok) return res.b; }
|
||||
}
|
||||
return null;
|
||||
};
|
||||
let genA = null, genB = null, farTw = null;
|
||||
for (let ang = 0; ang < 360 && !farTw; ang += 20) {
|
||||
const g1 = putRay2('generator', ang, 5.5, 7);
|
||||
if (!g1) continue;
|
||||
stepSeconds(8); // a generator extends the grid once DONE
|
||||
const g2 = putRay2('generator', ang, 9.5, 12.5);
|
||||
if (!g2) { RTS.sim.demolish(g1.id); continue; }
|
||||
stepSeconds(8);
|
||||
// tower must be covered by genB but OUTSIDE HQ and genA reach
|
||||
let ftw = null;
|
||||
for (let rr = 0.8; rr <= 6 && !ftw; rr += 0.75) {
|
||||
for (let da = -70; da <= 70 && !ftw; da += 14) {
|
||||
const a2 = (ang + da) * Math.PI / 180;
|
||||
const bx = g2.x + Math.cos(a2) * rr, by = g2.y + Math.sin(a2) * rr;
|
||||
const x = Math.round(bx), y = Math.round(by);
|
||||
if (RTS.util.dist(x, y, hqC.x, hqC.y) < 8.2) continue;
|
||||
if (RTS.util.dist(x, y, g1.x, g1.y) < 7) continue;
|
||||
if (!RTS.sim.canPlace('watchtower', x, y).ok) continue;
|
||||
const res = RTS.sim.place('watchtower', x, y);
|
||||
if (res.ok) ftw = res.b;
|
||||
}
|
||||
}
|
||||
if (!ftw) { RTS.sim.demolish(g2.id); RTS.sim.demolish(g1.id); continue; }
|
||||
genA = g1; genB = g2; farTw = ftw;
|
||||
}
|
||||
stepSeconds(10);
|
||||
ok(genA && genB && farTw && farTw.done,
|
||||
'chained generators extend the grid (' + (farTw ? 'tower at ' + RTS.util.dist(farTw.x, farTw.y, hqC.x, hqC.y).toFixed(1) + ' tiles' : 'placement failed') + ')');
|
||||
if (genB && farTw && farTw.done) {
|
||||
ok(farTw.powered, 'outpost tower powered by the chain, not the HQ');
|
||||
RTS.sim._debug.hurt(genB, 99999, null, null); // destroy the middle of the chain
|
||||
stepSeconds(1);
|
||||
ok(!farTw.powered || RTS.util.dist(farTw.x, farTw.y, hqC.x, hqC.y) < 8,
|
||||
'losing a generator cuts power to its customers');
|
||||
ok(!genA.dead, 'upstream generator unaffected');
|
||||
} else {
|
||||
// map gave us no valid chain — don't fail the suite for terrain
|
||||
console.log(' ~ skipped chain-destruction checks (no valid chain spot)');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- amphibious raiders ----------
|
||||
console.log('\n[amphibious raiders]');
|
||||
{
|
||||
const am = RTS.sim.newGame('normal', 27182);
|
||||
am.day = 7; am.dayT = RTS.CONFIG.WORLD.DAY_LENGTH - 0.5;
|
||||
stepSeconds(3);
|
||||
const sucho = am.dinos.find(d => d.dinoId === 'sucho');
|
||||
ok(sucho, 'suchomimus joined the day-8 wave');
|
||||
if (sucho) {
|
||||
ok(sucho.amphibious === true, 'sucho flagged amphibious');
|
||||
const sx0 = sucho.x, sy0 = sucho.y;
|
||||
stepSeconds(20);
|
||||
const movedA = Math.hypot(sucho.x - sx0, sucho.y - sy0);
|
||||
ok(movedA > 2, 'sucho left the lake and advanced (moved ' + movedA.toFixed(1) + ' tiles)');
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- determinism/perf sanity ----------
|
||||
console.log('\n[perf]');
|
||||
const st3 = RTS.sim.newGame('hard', 999);
|
||||
const t0 = Date.now();
|
||||
stepSeconds(60);
|
||||
const ms = Date.now() - t0;
|
||||
ok(ms < 15000, '60s of hard-mode sim in ' + ms + 'ms wall time');
|
||||
console.log(' (dinos alive: ' + st3.dinos.length + ', buildings: ' + st3.buildings.length + ')');
|
||||
|
||||
console.log('\n==================================');
|
||||
if (failures) { console.error('SMOKE TEST FAILED: ' + failures + ' assertion(s)'); process.exit(1); }
|
||||
console.log('ALL SMOKE TESTS PASSED ✔');
|
||||
Reference in New Issue
Block a user