- 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
232 lines
7.7 KiB
JavaScript
232 lines
7.7 KiB
JavaScript
/* =========================================================
|
||
* 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());
|