/* ========================================================= * 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 = '' + BICON[defId] + '' + '' + def.name + '' + '' + costStr(def.cost) + '' + '' + (hkNum || '') + ''; 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 'Uses ⚑' + def.energyUse + (def.workers ? ' Β· πŸ‘·' + def.workers : '') + ''; if (def.energyProd) return 'Makes ⚑' + def.energyProd + ''; return 'No power needed'; } function buildTip(def) { return '' + BICON[def.id] + ' ' + def.name + '
' + def.desc + '
' + costStr(def.cost) + '
' + energyStr(def) + (def.hp ? '
HP ' + def.hp + '' : ''); } 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 = '

πŸŽ–οΈ

' + '
' + '

Right-click: move Β· Right-click a dino: focus it
Drag-select more, double-click: all on screen

'; 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 = '

πŸ’™ ' + d.name + ' (tamed)

' + '
' + '

Fighting for the colony! ' + (d.flying ? 'Air power!' : '') + '

' + '

Right-click ground: new guard post
Right-click a wild dino: attack it
Your Primal Pen heals it nearby.

'; } else { el.panel.innerHTML = '

πŸ¦– ' + d.name + '

' + '
' + '

' + (d.flying ? '☠ Flying β€” ignores walls!' : (d.amphibious ? '🌊 Amphibious β€” strikes from lakes!' : 'Ground')) + ' Β· ' + (d.mode === 'roam' ? 'Roaming the wilds' : 'ATTACKING!') + '

' + (C.UNTAMEABLE[d.dinoId] ? '

Too powerful to tame.

' : '

Weaken below 32% HP, then send a Tamer to collar it.

'); } 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 = '

' + BICON[b.defId] + ' ' + def.name + '

'; html += '
'; html += '

'; if (b.defId === 'hq') { html += '

Research

'; for (const up of C.UPGRADES) { html += '
' + up.name + '
' + up.desc + '
' + '
'; } html += '
'; } 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 = '

πŸ₯š Breeding: needs 2+ tamed dinos nearby Β· ' + C.BREED.foodPerEgg + ' 🌾 per egg
' + (eggs.length ? 'Incubating ' + eggs.length + '/' + C.BREED.maxPerPen + (e0 ? ' β€” ' + prog + '%' : '') : 'No eggs yet (pair up your pets here)') + '

'; } const extra = b.defId === 'primalpen' ? '

🦴 +2 tame slots · heals tamed dinos nearby.
Tamers collar weakened dinos (<32% HP) automatically.

' + breed : ''; html += '

Train

' + '' + '' + extra + '
'; } if (def.workers > 0) html += '

'; if (def.range && (b.defId === 'forester' || b.defId === 'quarry')) { html += '

'; } html += '
'; if (def.workers > 0) html += ''; if (b.defId !== 'hq') html += ''; html += '
'; 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 = '⚠ No power β€” build a Generator!'; else if (b.active === false) stat.innerHTML = 'Production halted'; 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 ? 'Deposits exhausted' : '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 += '' + (f != null ? f + '%' : 'Β·') + ''; }); qq.innerHTML = s || 'Queue empty'; } } 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 = '

How To Play

' + '
' + '
' + '
'; 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 = '
SurvivedDay ' + st.day + ' (' + mins + 'm ' + secs + 's)
' + '
Dinosaurs slain' + st.stats.kills + '
' + '
Structures built' + st.stats.built + '
' + '
Structures lost' + st.stats.lost + '
' + '
Bounty earned' + st.stats.goldEarned + ' πŸ’°
' + (win ? '

The herds are broken. Repterra breathes again…

' : '

The reptiles reclaim the land. Rebuild, and try again.

'); }; UI.hideEnd = function () { el.endscreen.style.display = 'none'; }; return UI; })();