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
+231
View File
@@ -0,0 +1,231 @@
/* =========================================================
* REPRTERRA WEB — main.js
* Bootstrap + game loop + app states.
* ========================================================= */
'use strict';
window.RTS = window.RTS || {};
RTS.main = (function () {
let appState = 'menu'; // 'menu' | 'playing' | 'over'
let paused = false;
let lastT = 0;
let acc = 0;
let hudT = 0, mmT = 0;
let lastDaySeen = 1;
const M = {};
M.state = () => (appState === 'playing' && paused) ? 'playing' : appState;
M.isPaused = () => paused;
// ---------------------------------------------------------
// SAVE STORAGE — localStorage glue around sim.serialize
// ---------------------------------------------------------
const SAVE_KEY = 'repterra-web-save-v2';
const storage = {
ok: (() => { try { localStorage.setItem('_rt', '1'); localStorage.removeItem('_rt'); return true; } catch (e) { return false; } })(),
has() { try { return !!localStorage.getItem(SAVE_KEY); } catch (e) { return false; } },
save(manual) {
if (!this.ok || appState !== 'playing') return false;
const snap = RTS.sim.serialize();
if (!snap || snap.st.over) return false;
try {
localStorage.setItem(SAVE_KEY, JSON.stringify(snap));
RTS.ui.toast(manual ? '💾 Colony saved.' : '💾 Dawn autosave.', '');
return true;
} catch (e) { return false; }
},
autosave() { this.save(false); },
load() {
if (!this.ok) return false;
try {
const raw = localStorage.getItem(SAVE_KEY);
if (!raw) return false;
if (!RTS.sim.deserialize(raw)) return false;
const q = RTS.sim.hq();
if (q) { RTS.render.cam.x = q.x; RTS.render.cam.y = q.y; RTS.render.cam.zoom = Math.max(RTS.render.cam.zoom, 1.0); }
paused = false;
document.getElementById('pauseoverlay').style.display = 'none';
appState = 'playing';
lastDaySeen = RTS.sim.state().day;
RTS.ui.hideEnd && RTS.ui.hideEnd();
RTS.ui.showMenu(false);
RTS.ui.select(null);
RTS.ui.setPlacing(null);
RTS.ui.toast('📂 Welcome back — Day ' + RTS.sim.state().day + '.', '');
return true;
} catch (e) { console.error('load failed', e); return false; }
},
};
RTS.storage = storage;
M.boot = function () {
const cv = document.getElementById('game');
RTS.render.init(cv);
RTS.ui.init();
RTS.input.attach(cv);
RTS.input.ui = RTS.ui;
// minimap
const mm = document.getElementById('minimap');
mm.width = 176; mm.height = 176;
window.addEventListener('resize', () => RTS.render.resize());
// minimap click-to-jump
mm.addEventListener('mousedown', (e) => {
const st = RTS.sim.state();
if (!st) return;
const r = mm.getBoundingClientRect();
RTS.render.cam.x = (e.clientX - r.left) / r.width * st.world.tiles.W;
RTS.render.cam.y = (e.clientY - r.top) / r.height * st.world.tiles.H;
RTS.render.clampCam();
});
RTS.ui.showMenu(true);
requestAnimationFrame(loop);
};
M.startGame = function (diff) {
RTS.audio.resume();
RTS.sim.newGame(diff);
const q = RTS.sim.hq();
RTS.render.cam.x = q.x; RTS.render.cam.y = q.y; RTS.render.cam.zoom = 1.1;
paused = false;
appState = 'playing';
lastDaySeen = 1;
RTS.ui.hideEnd();
RTS.ui.showMenu(false);
RTS.ui.setPlacing(null);
RTS.ui.select(null);
RTS.ui.toast('Colony established. Build houses and generators first!', '');
setTimeout(() => { if (appState === 'playing') RTS.ui.toast('Tip: Pteranodons FLY over walls — keep Watchtowers ready.', ''); }, 6000);
};
M.restart = function () { M.startGame(RTS.sim.state() ? RTS.sim.state().diff : 'normal'); };
M.loadSave = function () { storage.load(); };
M.toMenu = function () {
appState = 'menu';
RTS.ui.showEnd ? null : null;
document.getElementById('endscreen').style.display = 'none';
RTS.ui.showMenu(true);
};
M.togglePause = function () {
if (appState !== 'playing') return;
paused = !paused;
document.getElementById('pauseoverlay').style.display = paused ? 'flex' : 'none';
};
M.setSpeed = function (s) {
if (!RTS.sim.state()) return;
RTS.sim.state().speed = s;
if (paused && s > 0) M.togglePause();
markSpeedBtns(s);
};
M.bumpSpeed = function (d) {
const st = RTS.sim.state();
if (!st) return;
M.setSpeed(Math.min(3, Math.max(1, st.speed + d)));
};
function markSpeedBtns(s) {
document.querySelectorAll('#topbtns .tbtn').forEach((b) => {
b.classList.toggle('active', b.textContent === s + '×');
});
}
M.hotkey = function (key) {
if (key === 'Enter') {
// start with selected/default difficulty from menu
const sel = document.querySelector('.diffbtn.sel') || document.querySelector('[data-diff="normal"]');
if (sel && appState === 'menu') sel.click();
}
};
// ---------------------------------------------------------
function loop(t) {
requestAnimationFrame(loop);
const dtReal = Math.min(0.05, (t - lastT) / 1000 || 0.016);
lastT = t;
RTS.input.tick(dtReal);
const st = RTS.sim.state();
if (st && appState === 'playing') {
if (!paused) {
acc += dtReal * st.speed;
let steps = 0;
while (acc > 1 / 120 && steps < 12) {
const step = Math.min(acc, 1 / 30);
RTS.sim.tick(step);
acc -= step;
steps++;
}
checkEvents(st);
if (st.over && !st.endShown) {
st.endShown = true;
setTimeout(() => { if (st.over) RTS.ui.showEnd(st); }, 1400);
}
}
// draw
RTS.render.draw(st, RTS.ui, t / 1000);
hudT -= dtReal;
if (hudT <= 0) { hudT = 0.25; RTS.ui.updateHUD(); }
mmT -= dtReal;
if (mmT <= 0) {
mmT = 0.3;
const mmCtx = document.getElementById('minimap').getContext('2d');
RTS.render.drawMinimap(mmCtx, st, 176);
}
drawPings(st, dtReal);
} else if (st && appState === 'over') {
RTS.render.draw(st, RTS.ui, t / 1000);
}
}
function drawPings(st, dt) {
for (const p of RTS.ui.pings) p.t += dt;
RTS.ui.pings = RTS.ui.pings.filter(p => p.t < 0.8);
}
// ---------------------------------------------------------
let wasWarned = false, hqHitToast = false, brownToast = false;
function checkEvents(st) {
// wave spawn toast
const w = st.waves[Math.max(0, st.waveIdx - 1)];
if (w && w.spawned && !w._toast) {
w._toast = true;
RTS.ui.toast(w.final ? '☠ THE FINAL ASSAULT HAS BEGUN!' : '🌊 A dinosaur wave is attacking!', w.final ? 'bad' : 'warn');
if (w.final) RTS.audio.roar();
}
// warning siren once
if (st.warnT > 0 && !wasWarned) {
wasWarned = true;
RTS.ui.toast('⚠ Dinosaur horde spotted approaching the colony!', 'bad');
}
if (st.warnT <= 0) wasWarned = false;
// HQ under attack
const q = RTS.sim.hq();
if (q && q.hp < q.maxHp && !hqHitToast) {
hqHitToast = true;
RTS.ui.toast('🏛️ THE COMMAND CENTER IS UNDER ATTACK!', 'bad');
RTS.audio.alarm();
}
if (q && q.hp >= q.maxHp) hqHitToast = false;
// brownout
if (st.energyUse > st.energyCap && !brownToast) {
brownToast = true;
RTS.ui.toast('⚡ Power shortage! Newest buildings are offline — build Generators.', 'warn');
}
if (st.energyUse <= st.energyCap) brownToast = false;
// starving
if (st.starving && !checkEvents._starve) {
checkEvents._starve = true;
RTS.ui.toast('🍖 Food shortage! Colonists are starving.', 'warn');
}
if (!st.starving) checkEvents._starve = false;
}
return M;
})();
window.addEventListener('DOMContentLoaded', () => RTS.main.boot());