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:
@@ -0,0 +1,526 @@
|
||||
/* =========================================================
|
||||
* 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;
|
||||
})();
|
||||
Reference in New Issue
Block a user