Files
repterra-web/js/main.js
T
deepseek 8fbe70d0b0 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
2026-08-23 07:00:23 +00:00

232 lines
7.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =========================================================
* 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());