Files
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

527 lines
24 KiB
JavaScript
Raw Permalink 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 — ui.js
* HUD: resource bar, build palette, selection panel,
* warnings, toasts, minimap frame, menus & end screens.
* ========================================================= */
'use strict';
window.RTS = window.RTS || {};
RTS.ui = (function () {
const U = RTS.util;
const C = RTS.CONFIG;
const UI = {};
UI.sel = null; // {kind:'building'|'dino'|'units', id?, ids?}
Object.defineProperty(UI, 'selUnits', {
get() { return (UI.sel && UI.sel.kind === 'units') ? UI.sel.ids.filter(id => !isDead(id)) : []; },
});
UI.placing = null; // {defId,x,y}
UI.dragRect = null;
UI.pings = []; // {x,y,t,color}
UI.tooltipEl = null;
let el = {}; // cached elements
let paletteBtns = [];
const ICONS = { gold: '💰', wood: '🪵', stone: '🪨', food: '🍖', energy: '⚡', pop: '👥' };
const BICON = {
hq: '🏛️', house: '🏠', farm: '🌾', forester: '🌲', quarry: '⛏️',
generator: '🔋', wall: '🧱', gate: '🚪', watchtower: '🏹', cannon: '💣',
barracks: '🎖️', primalpen: '🦴',
};
// ---------------------------------------------------------
UI.init = function () {
el.hud = document.getElementById('hud');
el.top = document.getElementById('topbar');
el.resGold = q('#res-gold .v'); el.resGoldR = q('#res-gold .r');
el.resWood = q('#res-wood .v'); el.resWoodR = q('#res-wood .r');
el.resStone = q('#res-stone .v'); el.resStoneR = q('#res-stone .r');
el.resFood = q('#res-food .v'); el.resFoodR = q('#res-food .r');
el.energy = q('#res-energy .v');
el.pop = q('#res-pop .v');
el.day = document.getElementById('daylabel');
el.wave = document.getElementById('wavelabel');
el.banner = document.getElementById('wavebanner');
el.bannerTxt = document.getElementById('wavebanner-text');
buildPalette();
buildTopButtons();
el.panel = document.getElementById('selpanel');
el.toasts = document.getElementById('toasts');
el.minimap = document.getElementById('minimap');
el.mmWrap = document.getElementById('mmwrap');
// tooltip
el.tip = document.getElementById('tooltip');
window.addEventListener('mousemove', (e) => {
if (el.tip.style.display === 'block') {
el.tip.style.left = Math.min(window.innerWidth - 260, e.clientX + 14) + 'px';
el.tip.style.top = Math.min(window.innerHeight - 120, e.clientY + 16) + 'px';
}
});
// menu
el.menu = document.getElementById('menu');
el.endscreen = document.getElementById('endscreen');
document.querySelectorAll('[data-diff]').forEach(b => {
b.addEventListener('click', () => { RTS.audio.resume(); RTS.audio.click(); RTS.main.startGame(b.dataset.diff); });
});
document.getElementById('btn-how').addEventListener('click', () => toggleHelp());
document.querySelectorAll('.backtomenu').forEach(b => b.addEventListener('click', () => RTS.main.toMenu()));
document.querySelectorAll('[data-restart]').forEach(b => b.addEventListener('click', () => RTS.main.restart()));
// save / load / continue
const cont = document.getElementById('continueBtn');
if (cont) {
if (RTS.storage && RTS.storage.has()) cont.style.display = '';
else if (RTS.storage) { // re-check shortly (storage may just have become available)
setTimeout(() => { if (RTS.storage.has()) cont.style.display = ''; }, 300);
}
cont.addEventListener('click', () => { RTS.audio.resume(); RTS.audio.click(); RTS.main.loadSave(); });
}
const sb = document.getElementById('savebtn');
if (sb) sb.addEventListener('click', () => { RTS.storage.save(true); });
const lb = document.getElementById('loadbtn');
if (lb) {
const refresh = () => { lb.disabled = !RTS.storage.has(); };
refresh();
setInterval(refresh, 1500);
lb.addEventListener('click', () => { RTS.audio.click(); RTS.main.loadSave(); });
}
};
function q(s) { return document.querySelector(s); }
function buildTopButtons() {
const tb = document.getElementById('topbtns');
tb.innerHTML = '';
mkBtn('⏸', 'Pause (Space)', () => RTS.main.togglePause());
mkBtn('1×', 'Normal speed', () => RTS.main.setSpeed(1));
mkBtn('2×', 'Fast forward', () => RTS.main.setSpeed(2));
mkBtn('3×', 'Very fast', () => RTS.main.setSpeed(3));
mkBtn('🔊', 'Mute (M)', () => UI.toggleMute(), 'mutebtn');
mkBtn('❓', 'Help', () => toggleHelp());
function mkBtn(label, tip, fn, id) {
const b = document.createElement('button');
b.className = 'tbtn';
if (id) b.id = id;
b.textContent = label;
b.title = tip;
b.addEventListener('click', () => { RTS.audio.click(); fn(); });
tb.appendChild(b);
return b;
}
}
function buildPalette() {
const pal = document.getElementById('palette');
pal.innerHTML = '';
paletteBtns = [];
let slot = 0;
C.PALETTE.forEach((group) => {
const col = document.createElement('div');
col.className = 'palcol';
group.forEach((defId) => {
const def = C.BUILDINGS[defId];
const hkNum = slot < 9 ? (slot + 1) : 0;
const b = document.createElement('button');
b.className = 'palbtn';
b.innerHTML =
'<span class="ic">' + BICON[defId] + '</span>' +
'<span class="nm">' + def.name + '</span>' +
'<span class="cost">' + costStr(def.cost) + '</span>' +
'<span class="hk">' + (hkNum || '') + '</span>';
b.addEventListener('click', () => { RTS.audio.click(); UI.togglePlacing(defId); });
b.addEventListener('mouseenter', (e) => showTip(buildTip(def), e));
b.addEventListener('mouseleave', hideTip);
col.appendChild(b);
paletteBtns.push({ defId, btn: b });
slot++;
});
pal.appendChild(col);
});
}
function costStr(cost) {
return Object.entries(cost).map(([k, v]) => ICONS[k] + v).join(' ');
}
function energyStr(def) {
if (def.energyUse) return '<i>Uses ⚡' + def.energyUse + (def.workers ? ' · 👷' + def.workers : '') + '</i>';
if (def.energyProd) return '<i>Makes ⚡' + def.energyProd + '</i>';
return '<i>No power needed</i>';
}
function buildTip(def) {
return '<b>' + BICON[def.id] + ' ' + def.name + '</b><br>' + def.desc + '<br>' +
costStr(def.cost) + '<br>' + energyStr(def) + (def.hp ? '<br><i>HP ' + def.hp + '</i>' : '');
}
function showTip(html, e) {
el.tip.innerHTML = html;
el.tip.style.display = 'block';
el.tip.style.left = Math.min(window.innerWidth - 260, e.clientX + 14) + 'px';
el.tip.style.top = Math.min(window.innerHeight - 140, e.clientY + 16) + 'px';
}
function hideTip() { el.tip.style.display = 'none'; }
UI.flatPalette = function () { return paletteBtns.map(p => p.defId); };
// ---------------------------------------------------------
UI.togglePlacing = function (defId) {
if (UI.placing && UI.placing.defId === defId) { UI.setPlacing(null); return; }
const def = C.BUILDINGS[defId];
UI.setPlacing({ defId, x: Math.round(RTS.input.mouse.wx), y: Math.round(RTS.input.mouse.wy) });
};
UI.setPlacing = function (p) {
UI.placing = p;
paletteBtns.forEach(pb => pb.btn.classList.toggle('on', !!p && pb.defId === p.defId));
};
UI.select = function (sel) {
UI.sel = sel;
refreshPanel(true);
};
UI.fxPing = function (x, y, color) {
UI.pings.push({ x, y, t: 0, color: color || '#fff' });
};
UI.toast = function (msg, cls) {
const t = document.createElement('div');
t.className = 'toast ' + (cls || '');
t.textContent = msg;
el.toasts.appendChild(t);
setTimeout(() => t.classList.add('show'), 10);
setTimeout(() => { t.classList.remove('show'); setTimeout(() => t.remove(), 400); }, 3800);
while (el.toasts.children.length > 5) el.toasts.firstChild.remove();
};
UI.toggleMute = function () {
const muted = RTS.audio.toggleMute();
const mb = document.getElementById('mutebtn');
if (mb) mb.textContent = muted ? '🔇' : '🔊';
};
// ---------------------------------------------------------
function isDead(id) {
const st = RTS.sim.state();
return !st.units.some(u => u.id === id && !u.dead);
}
function refreshPanel(rebuild) {
const st = RTS.sim.state();
if (!st || !UI.sel) { el.panel.style.display = 'none'; return; }
el.panel.style.display = 'block';
if (UI.sel.kind === 'units') {
const us = UI.sel.ids.map(id => st.units.find(u => u.id === id && !u.dead)).filter(Boolean);
if (!us.length) { UI.select(null); return; }
if (rebuild || !el.panel.dataset.units) {
el.panel.dataset.units = '1'; delete el.panel.dataset.bld;
const counts = {};
us.forEach(u => { counts[u.unitId] = (counts[u.unitId] || 0) + 1; });
const title = Object.entries(counts).map(([k, n]) => C.UNITS[k].name + ' ×' + n).join(' · ');
el.panel.innerHTML =
'<h3>🎖️ <span id="ucount"></span></h3>' +
'<div class="hpbar"><div id="uhp"></div></div>' +
'<p class="hint">Right-click: move · Right-click a dino: focus it<br>Drag-select more, double-click: all on screen</p>';
setTimeout(() => { const e2 = q('#ucount'); if (e2) e2.textContent = title; }, 0);
}
// keep the title fresh as units die
const counts = {};
us.forEach(u => { counts[u.unitId] = (counts[u.unitId] || 0) + 1; });
const titleEl = q('#ucount');
if (titleEl) titleEl.textContent = Object.entries(counts).map(([k, n]) => C.UNITS[k].name + ' ×' + n).join(' · ');
const frac = us.reduce((n, u) => n + u.hp / u.maxHp, 0) / us.length;
q('#uhp').style.width = (frac * 100) + '%';
return;
}
if (UI.sel.kind === 'dino') {
const d = RTS.sim.getDino(UI.sel.id);
if (!d) { UI.select(null); return; }
el.panel.dataset.units = ''; delete el.panel.dataset.bld;
if (d.tamed) {
el.panel.innerHTML =
'<h3>💙 ' + d.name + ' <small>(tamed)</small></h3>' +
'<div class="hpbar"><div style="width:' + (d.hp / d.maxHp * 100) + '%"></div></div>' +
'<p><b class=good>Fighting for the colony!</b> ' +
(d.flying ? 'Air power!' : '') + '</p>' +
'<p class="hint">Right-click ground: new guard post<br>Right-click a wild dino: attack it<br>Your Primal Pen heals it nearby.</p>';
} else {
el.panel.innerHTML =
'<h3>🦖 ' + d.name + '</h3>' +
'<div class="hpbar"><div style="width:' + (d.hp / d.maxHp * 100) + '%"></div></div>' +
'<p>' + (d.flying ? '☠ Flying — ignores walls!' : (d.amphibious ? '🌊 Amphibious — strikes from lakes!' : 'Ground')) +
' · ' + (d.mode === 'roam' ? 'Roaming the wilds' : '<b class=bad>ATTACKING!</b>') + '</p>' +
(C.UNTAMEABLE[d.dinoId]
? '<p class="hint">Too powerful to tame.</p>'
: '<p class="hint">Weaken below 32% HP, then send a Tamer to collar it.</p>');
}
return;
}
// building
const b = RTS.sim.getBuilding(UI.sel.id);
if (!b) { UI.select(null); return; }
el.panel.dataset.units = '';
if (el.panel.dataset.bld !== String(b.id)) {
el.panel.dataset.bld = String(b.id);
rebuildBuildingPanel(b);
}
updateBuildingPanel(b);
}
function rebuildBuildingPanel(b) {
const def = C.BUILDINGS[b.defId];
let html = '<h3>' + BICON[b.defId] + ' ' + def.name + '</h3>';
html += '<div class="hpbar"><div id="b-hp"></div></div>';
html += '<p id="b-status" class="status"></p>';
if (b.defId === 'hq') {
html += '<div class="sect"><h4>Research</h4>';
for (const up of C.UPGRADES) {
html += '<div class="upg"><div><b>' + up.name + '</b> <span class="pips" data-up="' + up.id + '"></span><br><small>' + up.desc + '</small></div>' +
'<button class="buy" data-buy="' + up.id + '">Buy</button></div>';
}
html += '</div>';
}
if (b.defId === 'barracks' || b.defId === 'primalpen') {
const unitId = b.defId === 'barracks' ? 'ranger' : 'tamer';
const udef = C.UNITS[unitId];
let breed = '';
if (b.defId === 'primalpen') {
const eggs = (st.eggs || []).filter(e => e.penId === b.id);
const e0 = eggs[0];
const prog = e0 ? Math.round(U.clamp(e0.t / e0.total, 0, 1) * 100) : 0;
breed = '<p class="hint">🥚 Breeding: needs <b>2+ tamed dinos</b> nearby · '
+ C.BREED.foodPerEgg + ' 🌾 per egg<br>'
+ (eggs.length ? 'Incubating ' + eggs.length + '/' + C.BREED.maxPerPen + (e0 ? ' — ' + prog + '%' : '')
: 'No eggs yet (pair up your pets here)')
+ '</p>';
}
const extra = b.defId === 'primalpen'
? '<p class="hint">🦴 +2 tame slots · heals tamed dinos nearby.<br>Tamers collar weakened dinos (<32% HP) automatically.</p>' + breed
: '';
html += '<div class="sect"><h4>Train</h4><div id="b-queue" class="queue"></div>' +
'<button class="bigbtn" id="train1"> Train ' + udef.name + ' (' + costStr(udef.cost) + ')</button>' +
'<button class="bigbtn subtle" id="train5">Train ×5</button>' + extra + '</div>';
}
if (def.workers > 0) html += '<p id="b-workers"></p>';
if (def.range && (b.defId === 'forester' || b.defId === 'quarry')) {
html += '<p id="b-deposit"></p>';
}
html += '<div class="rowbtns">';
if (def.workers > 0) html += '<button class="bigbtn subtle" id="b-toggle"></button>';
if (b.defId !== 'hq') html += '<button class="bigbtn danger" id="b-demolish">Demolish (+50%)</button>';
html += '</div>';
el.panel.innerHTML = html;
const t1 = document.getElementById('train1');
if (t1) {
const unitId = b.defId === 'barracks' ? 'ranger' : 'tamer';
t1.addEventListener('click', () => { if (!RTS.sim.trainUnit(b, unitId)) RTS.audio.deny(); });
document.getElementById('train5').addEventListener('click', () => {
for (let i = 0; i < 5; i++) if (!RTS.sim.trainUnit(b, unitId)) break;
});
}
document.querySelectorAll('[data-buy]').forEach(btn => {
btn.addEventListener('click', () => {
if (!RTS.sim.buyUpgrade(btn.dataset.buy)) RTS.audio.deny();
else refreshPanel(true);
});
});
const tg = document.getElementById('b-toggle');
if (tg) tg.addEventListener('click', () => { RTS.sim.toggleActive(b.id); updateBuildingPanel(b); });
const dm = document.getElementById('b-demolish');
if (dm) dm.addEventListener('click', () => { RTS.sim.demolish(b.id); UI.select(null); });
}
function updateBuildingPanel(b) {
const def = C.BUILDINGS[b.defId];
const hpEl = document.getElementById('b-hp');
if (hpEl) hpEl.style.width = (b.hp / b.maxHp * 100) + '%';
const stat = document.getElementById('b-status');
if (stat) {
if (!b.done) stat.innerHTML = '🏗️ Under construction… ' + Math.floor(b.progress * 100) + '%';
else if (!b.powered) stat.innerHTML = '<b class="bad">⚠ No power — build a Generator!</b>';
else if (b.active === false) stat.innerHTML = '<span class="warn">Production halted</span>';
else if (b.defId === 'forester' || b.defId === 'quarry') {
const amt = RTS.sim.depositInRange(b.defId === 'forester' ? 'tree' : 'rock', b.x, b.y, def.range);
stat.innerHTML = amt <= 0 ? '<span class="warn">Deposits exhausted</span>' : 'Working — deposits left nearby: ' + Math.round(amt);
} else stat.textContent = 'Operational';
}
const wk = document.getElementById('b-workers');
if (wk) wk.textContent = '👷 Workers: ' + b.workers + ' / ' + b.workersNeed + (b.workers < b.workersNeed ? ' — need more colonists (build Houses)' : '');
const dp = document.getElementById('b-deposit');
if (dp) {
const kind = b.defId === 'forester' ? 'tree' : 'rock';
dp.textContent = 'Resource in range: ' + Math.round(RTS.sim.depositInRange(kind, b.x, b.y, def.range)) + ' / need ' + def.needRes;
}
const tg = document.getElementById('b-toggle');
if (tg) tg.textContent = b.active === false ? '▶ Resume' : '⏸ Halt';
// upgrades
if (b.defId === 'hq') {
document.querySelectorAll('[data-up]').forEach(sp => {
const lvl = st_lvl(sp.dataset.up);
const def2 = C.UPGRADES.find(u => u.id === sp.dataset.up);
let s = '';
for (let i = 0; i < def2.tiers; i++) s += i < lvl ? '◆' : '◇';
sp.textContent = s;
});
document.querySelectorAll('[data-buy]').forEach(btn => {
const id = btn.dataset.buy;
const def2 = C.UPGRADES.find(u => u.id === id);
const lvl = st_lvl(id);
if (lvl >= def2.tiers) { btn.disabled = true; btn.textContent = 'MAX'; }
else {
const c = RTS.sim.upgradeCost(id);
btn.textContent = costStr(c);
btn.disabled = !canAfford(c);
}
});
}
// queue
const qq = document.getElementById('b-queue');
if (qq) {
let s = '';
b.trainQ.forEach((job, i) => {
const f = i === 0 ? Math.round((1 - job.t / job.total) * 100) : null;
s += '<span class="qslot">' + (f != null ? f + '%' : '·') + '</span>';
});
qq.innerHTML = s || '<small>Queue empty</small>';
}
}
function st_lvl(id) { return RTS.sim.state().upgrades[id]; }
function canAfford(cost) {
const st = RTS.sim.state();
for (const k in cost) if (st.res[k] < cost[k]) return false;
return true;
}
// ---------------------------------------------------------
// HUD refresh (~4x/sec)
// ---------------------------------------------------------
UI.updateHUD = function () {
const st = RTS.sim.state();
if (!st) return;
const fmt = (n) => Math.floor(n);
el.resGold.textContent = fmt(st.res.gold);
el.resWood.textContent = fmt(st.res.wood);
el.resStone.textContent = fmt(st.res.stone);
el.resFood.textContent = fmt(st.res.food);
setRate(el.resGoldR, st.rate.gold);
setRate(el.resWoodR, st.rate.wood);
setRate(el.resStoneR, st.rate.stone);
setRate(el.resFoodR, st.rate.food, st.starving);
el.energy.textContent = st.energyUse + '/' + st.energyCap;
el.energy.parentElement.classList.toggle('bad', st.energyUse >= st.energyCap && st.energyCap > 0);
el.pop.textContent = st.pop + '/' + st.popCap;
const DAYL = C.WORLD.DAY_LENGTH;
const phase = (st.time % DAYL) / DAYL;
el.day.textContent = (phase > 0.5 ? '🌙 Day ' : '☀️ Day ') + st.day;
// wave countdown
const nw = st.waves[st.waveIdx];
if (nw && !st.finalTriggered) {
const waveAbsT = (nw.day - 1) * C.WORLD.DAY_LENGTH;
const tAbs = st.dayT + (st.day - 1) * C.WORLD.DAY_LENGTH;
const rem = Math.max(0, waveAbsT - tAbs);
const mm = Math.floor(rem / 60), ss = Math.floor(rem % 60);
el.wave.textContent = (st.warnT > 0 ? '⚠ ATTACK IMMINENT' :
(nw.final ? '☠ FINAL WAVE in ' : '🌊 Wave in ') + mm + ':' + String(ss).padStart(2, '0'));
el.wave.classList.toggle('bad', st.warnT > 0 || rem < 60);
} else {
el.wave.textContent = st.finalTriggered ? '☠ FINAL WAVE!' : '';
el.wave.classList.toggle('bad', true);
}
// banner
if (st.warnT > 0 && !st.over) {
el.banner.style.display = 'flex';
const dirs = ['E', 'SE', 'S', 'SW', 'W', 'NW', 'N', 'NE'];
const ang = Math.atan2(st.warnDirY, st.warnDirX);
let di = Math.round(ang / (Math.PI / 4)); di = ((di % 8) + 8) % 8;
const mm = Math.floor(st.warnT / 60), ss = Math.floor(st.warnT % 60);
el.bannerTxt.innerHTML = '⚠ DINOSAURS APPROACH FROM THE ' + dirs[di] + ' — ' + mm + ':' + String(ss).padStart(2, '0');
el.banner.classList.add('pulse');
} else {
el.banner.style.display = 'none';
}
// palette affordability
paletteBtns.forEach(pb => {
const def = C.BUILDINGS[pb.defId];
pb.btn.classList.toggle('cant', !canAfford(def.cost));
});
refreshPanel(false);
function setRate(elm, r, starving) {
const rr = Math.round(r * 100) / 100;
elm.textContent = (rr >= 0 ? '+' : '') + rr.toFixed(2) + '/s';
elm.classList.toggle('neg', starving || rr < 0);
}
};
// ---------------------------------------------------------
function toggleHelp() {
let hv = document.getElementById('helpoverlay');
if (!hv) {
hv = document.createElement('div');
hv.id = 'helpoverlay';
hv.className = 'overlay';
hv.innerHTML = '<div class="card wide"><h2>How To Play</h2>' +
'<div class="helpcols">' +
'<ul>' +
'<li><b>Goal:</b> grow the colony and survive until the <b>FINAL WAVE</b>, then wipe out every last dinosaur.</li>' +
'<li>If your <b>Command Center 🏛️</b> falls, the colony falls.</li>' +
'<li><b>Houses 🏠</b> raise population cap and pay taxes. Colonists arrive automatically and work your buildings.</li>' +
'<li><b>Farms 🌾</b> feed everyone. <b>Foresters 🌲</b> need forest nearby, <b>Quarries ⛏️</b> need rocks.</li>' +
'<li><b>Generators 🔋</b> make energy AND extend the power grid — you can only build touching your grid. Exceed capacity and newest buildings go dark.</li>' +
'</ul><ul>' +
'<li><b>Walls 🧱</b> block ground dinos… but <b>Pteranodons FLY over walls!</b> Cover your base with Watchtowers 🏹.</li>' +
'<li>Cannon Towers 💣 hit hard but <b>cannot shoot air</b>.</li>' +
'<li><b>Barracks 🎖️</b> train Rangers — set a rally point (right-click while selected).</li>' +
'<li><b>Taming:</b> build a <b>Primal Pen 🦴</b>, train a <b>Tamer</b>, weaken a dino below 32% HP and he\'ll collar it. Right-click to command your pets!</li>' +
'<li><b>Breeding:</b> park 2+ tamed dinos by the Pen and they lay 🥚 eggs (60 food each). Hatchlings grow into fighting adults!</li>' +
'<li><b>Watch the water 🌊</b> — Suchomimus raids emerge from the lakes.</li>' +
'<li><b>Save anytime</b> from the pause menu; the colony also autosaves at every dawn. Continue from the main menu.</li>' +
'<li>Kill roaming packs before the final wave — every survivor joins it!</li>' +
'<li><b>Controls:</b> WASD/arrows/edge scroll · wheel zoom · drag-select · 1-9 build · Shift multi-build · Space pause · 1×/2×/3× speed</li>' +
'</ul></div>' +
'<button class="bigbtn" onclick="document.getElementById(\'helpoverlay\').remove()">Got it!</button></div>';
document.body.appendChild(hv);
} else hv.remove();
}
UI.showHelp = toggleHelp;
UI.showMenu = function (show) {
el.menu.style.display = show ? 'flex' : 'none';
const cont = document.getElementById('continueBtn');
if (cont && RTS.storage) cont.style.display = RTS.storage.has() ? '' : 'none';
};
UI.showEnd = function (st) {
const win = st.victory;
el.endscreen.style.display = 'flex';
el.endscreen.querySelector('h1').textContent = win ? '🏆 COLONY SAVED!' : '💀 THE COLONY HAS FALLEN';
el.endscreen.querySelector('h1').className = win ? 'good' : 'bad';
const mins = Math.floor(st.time / 60), secs = Math.floor(st.time % 60);
el.endscreen.querySelector('.stats').innerHTML =
'<div><span>Survived</span><b>Day ' + st.day + ' (' + mins + 'm ' + secs + 's)</b></div>' +
'<div><span>Dinosaurs slain</span><b>' + st.stats.kills + '</b></div>' +
'<div><span>Structures built</span><b>' + st.stats.built + '</b></div>' +
'<div><span>Structures lost</span><b>' + st.stats.lost + '</b></div>' +
'<div><span>Bounty earned</span><b>' + st.stats.goldEarned + ' 💰</b></div>' +
(win ? '<p class="flavor">The herds are broken. Repterra breathes again…</p>'
: '<p class="flavor">The reptiles reclaim the land. Rebuild, and try again.</p>');
};
UI.hideEnd = function () { el.endscreen.style.display = 'none'; };
return UI;
})();