/* ============================================================ Wildlife Kingdom — js/ui.js HUD · toolbar & palettes · side panel · selection cards · toasts · modals. Original cream/rounded visual identity. ============================================================ */ (function(){ 'use strict'; const WK = window.WK; const D = WK.Data; const $=id=>document.getElementById(id); const UI = (WK.UI = {}); UI.game=null; UI.cam=null; UI.renderer=null; UI.tool=null; UI.bulldoze=false; UI._last={}; UI._selTarget=null; UI._sideTab='objectives'; UI._sideT=0; UI._hudT=0; UI.init=function(game,cam,renderer){ if(UI._inited){ // re-attach to a fresh game without duplicating DOM work UI.game=game; UI.cam=cam; UI.renderer=renderer; game.ui=UI; UI._last={}; UI._selTarget=null; UI.cancelTool(); UI.hideSelection(); UI.renderSide(); return; } UI._inited=true; UI.game=game; UI.cam=cam; UI.renderer=renderer; game.ui=UI; // inject resource icons document.querySelectorAll('.stat .ic[data-ic]').forEach(el=>{ const s=WK.Sprites.getIcon('res:'+el.dataset.ic,26); s.cv.style.width='100%'; s.cv.style.height='100%'; el.appendChild(s.cv); }); $('btn-sound').textContent = WK.AudioSys.enabled?'🔊':'🔇'; $('btn-settings').textContent='⚙'; const spdIcons={ 'spd-pause':'⏸', 'spd-play':'▶', 'spd-fast':'⏩', 'spd-ultra':'⚡' }; for(const id in spdIcons) $(id).textContent=spdIcons[id]; $('btn-bulldoze').appendChild(WK.Sprites.getIcon('res:dozer',30).cv); UI.buildToolbar(); UI.bind(); UI.selectCat('paths'); // sensible default tab shown when opened }; /* ---------------- toolbar ---------------- */ const CAT_ICON={ ground:'ground:grass', paths:'path:gravel', fences:'fence:wood', nature:'nature:tree', animals:'animal:zebra', facilities:'facility:restroom' }; const FENCE_ITEMS=[['wood','Wood Fence'],['hedge','Hedge'],['gate','Keeper Gate']]; UI.buildToolbar=function(){ const cats=$('tb-categories'); cats.innerHTML=''; for(const c of D.TOOLCATS){ const b=document.createElement('button'); b.className='tb-cat'+(c.id===UI._cat?' active':''); b.appendChild(WK.Sprites.getIcon(CAT_ICON[c.id],34).cv); const l=document.createElement('span'); l.className='lbl'; l.textContent=c.label; b.appendChild(l); b.onclick=()=>{ WK.AudioSys.sfx('click'); UI.selectCat(c.id); }; b.dataset.cat=c.id; cats.appendChild(b); } }; UI.paletteItems=function(cat){ switch(cat){ case 'ground': return Object.keys(D.GROUND).map(id=>({id,name:D.GROUND[id].name,cost:D.GROUND[id].cost,tier:D.GROUND[id].tier})); case 'paths': return Object.keys(D.PATH).map(id=>({id,name:D.PATH[id].name,cost:D.PATH[id].cost,tier:D.PATH[id].tier})); case 'fences': return FENCE_ITEMS.map(([id,name])=>({id,name,cost:id==='wood'?12:id==='hedge'?18:40,tier:id==='hedge'?2:1})); case 'nature': return Object.keys(D.NATURE).map(id=>({id,name:D.NATURE[id].name,cost:D.NATURE[id].cost,tier:D.NATURE[id].tier})); case 'animals': return Object.keys(D.SPECIES).sort((a,b)=>D.SPECIES[a].cost-D.SPECIES[b].cost) .map(id=>({id,name:D.SPECIES[id].name,cost:D.SPECIES[id].cost,tier:D.SPECIES[id].tier})); case 'facilities': return Object.keys(D.BUILDING).sort((a,b)=>D.BUILDING[a].cost-D.BUILDING[b].cost) .map(id=>({id,name:D.BUILDING[id].name,cost:D.BUILDING[id].cost,tier:D.BUILDING[id].tier})); } return []; }; UI.iconKind=function(cat,id){ switch(cat){ case 'ground': return 'ground:'+id; case 'paths': return 'path:'+id; case 'fences': return 'fence:'+id; case 'nature': return 'nature:'+id; case 'animals': return 'animal:'+id; case 'facilities': return 'facility:'+id; } }; UI.selectCat=function(cat){ UI._cat=cat; document.querySelectorAll('.tb-cat').forEach(b=>b.classList.toggle('active',b.dataset.cat===cat)); const pal=$('tb-palette'); pal.innerHTML=''; for(const it of UI.paletteItems(cat)){ const locked=(it.tier||1)>UI.game.researchTier; const d=document.createElement('button'); d.className='pal-item'+(locked?' locked':'')+((UI.tool&&UI.tool.item===it.id&&UI.tool.cat===cat)?' active':''); const cv=WK.Sprites.getIcon(UI.iconKind(cat,it.id),54); d.appendChild(cv.cv); const nm=document.createElement('span'); nm.className='nm'; nm.textContent=it.name; const pr=document.createElement('span'); pr.className='pr'; pr.textContent=locked?('Tier '+it.tier):WK.fmtMoney(it.cost); d.appendChild(nm); d.appendChild(pr); d.title=locked?'Unlock via Research':(D.BUILDING[it.id]&&D.BUILDING[it.id].desc||D.SPECIES[it.id]&&(D.SPECIES[it.id].minArea+'+ tiles · likes '+Object.keys(D.SPECIES[it.id].biome).join('/'))||''); d.onclick=()=>{ WK.AudioSys.sfx('click'); if(locked){ UI.toast('Locked! Unlock Tier '+it.tier+' in the Research panel.','bad'); return; } UI.bulldoze=false; $('btn-bulldoze').classList.remove('active'); UI.selectTool({cat,item:it.id}); pal.querySelectorAll('.pal-item').forEach(x=>x.classList.remove('active')); d.classList.add('active'); }; pal.appendChild(d); } pal.classList.add('hidden'); if(UI.tool||UI.bulldoze) pal.classList.remove('hidden'); }; UI.selectTool=function(t){ UI.tool=t; UI.bulldoze=false; $('btn-bulldoze').classList.toggle('active',false); const pal=$('tb-palette'), info=$('tool-info'); if(t){ pal.classList.remove('hidden'); info.classList.remove('hidden'); const def= t.cat==='animals'?D.SPECIES[t.item] : t.cat==='facilities'?D.BUILDING[t.item] : t.cat==='fences'?{name:t.item[0].toUpperCase()+t.item.slice(1)+' Fence'} : null; $('tool-name').textContent=def?def.name:t.item; const cost=def?(typeof def.cost==='number'?def.cost:null):null; $('tool-cost').textContent=cost!=null?WK.fmtMoney(cost)+(t.cat==='animals'?' · needs suitable habitat':''):'paint'; document.querySelectorAll('#game').forEach(c=>c.classList.add('tooling')); document.querySelectorAll('.pal-item').forEach(x=>x.classList.toggle('active',x.querySelector('.nm') && x.querySelector('.nm').textContent===($('tool-name').textContent))); // re-mark active within current cat UI.selectCatRefreshActive(); }else{ pal.classList.add('hidden'); info.classList.add('hidden'); document.getElementById('game').classList.remove('tooling'); document.querySelectorAll('.pal-item').forEach(x=>x.classList.remove('active')); } }; UI.selectCatRefreshActive=function(){ document.querySelectorAll('.pal-item').forEach(x=>x.classList.remove('active')); }; UI.toggleBulldoze=function(){ UI.bulldoze=!UI.bulldoze; if(UI.bulldoze){ UI.tool=null; $('tb-palette').classList.add('hidden'); $('tool-info').classList.add('hidden'); } $('btn-bulldoze').classList.toggle('active',UI.bulldoze); document.getElementById('game').classList.toggle('tooling',UI.bulldoze); }; UI.cancelTool=function(){ UI.selectTool(null); UI.bulldoze=false; $('btn-bulldoze').classList.remove('active'); document.getElementById('game').classList.remove('tooling'); }; /* ---------------- HUD ---------------- */ UI.setSpeed=function(s){ UI.game.speed=s; ['spd-pause','spd-play','spd-fast','spd-ultra'].forEach(id=>$(id).classList.remove('active')); $({'0':'spd-pause','1':'spd-play','2':'spd-fast','4':'spd-ultra'}[s]).classList.add('active'); }; UI.togglePause=function(){ UI.setSpeed(UI.game.speed===0?(UI._lastSpeed||1):(UI._lastSpeed=UI.game.speed,0)); }; UI.updateHUD=function(){ const g=UI.game, L=UI._last; const set=(id,v)=>{ if(L[id]!==v){ L[id]=v; $(id).querySelector('b').textContent=v; } }; set('st-money',WK.fmtMoney(g.money)); set('st-guests',String(g.guests.length)); const joy=g.guests.length?Math.round(g.guests.reduce((s,x)=>s+x.joy,0)/g.guests.length):null; set('st-joy',(joy!=null?joy+'%':'—')); set('st-research',Math.floor(g.rp)+' RP'); set('st-stars',g.rating.toFixed(1)+'★'); $('st-day').textContent='Day '+g.day; $('st-clock').textContent=WK.fmtClock(g.minutes); // side panel throttle UI._sideT++; if(UI._sideT>30){ UI._sideT=0; UI.renderSide(); } // selection follow UI.trackSelection(); }; UI.flashStat=function(id){ const el=$(id==='money'?'st-money':id); el.classList.remove('flash'); void el.offsetWidth; el.classList.add('flash'); }; /* ---------------- side panel ---------------- */ UI.renderSide=function(){ const g=UI.game; const c=$('sp-content'); document.querySelectorAll('.sp-tab').forEach(b=>b.classList.toggle('active',b.dataset.tab===UI._sideTab)); if(UI._sideTab==='objectives'){ let h=''; D.MISSIONS.forEach((m,i)=>{ const done=ig.missionIdx&&!cur){} if(done||cur){ const rw= typeof m.reward==='number'? WK.fmtMoney(m.reward) : (m.reward.cash?WK.fmtMoney(m.reward.cash):'')+(m.reward.rp?(' +'+m.reward.rp+' RP'):''); h+=`
${m.label}
${m.hint&&!done?`
${m.hint}
`:''} ${!done?`
Reward: ${rw}
`:''}
`; } }); if(g.missionIdx>=D.MISSIONS.length) h+='
All missions complete — Wildlife Legend! 🏆
'; c.innerHTML=h; }else if(UI._sideTab==='research'){ const next=g.researchCost(); let h=`
Research pointsEarn passively · Research Center boosts${Math.floor(g.rp)} RP
`; for(const t of D.RESEARCH_TIERS){ const owned=g.researchTier>=t.tier; h+=`
${t.label}${owned?'Unlocked ✔':t.cost+' RP'} ${owned?'':``}
`; } h+='

New terrain, buildings & rare animals arrive with each tier.

'; c.innerHTML=h; c.querySelectorAll('[data-unlock]').forEach(b=>{ b.onclick=()=>{ g.buyResearch(); UI.renderSide(); }; }); }else{ const P=g.ratingParts||{}; const pc=k=>Math.round((P[k]||0)*100); let occ={}; for(const a of g.animals) occ[a.species]=(occ[a.species]||0)+1; let occStr=Object.keys(occ).map(s=>`${D.SPECIES[s].name} ×${occ[s]}`).join(', ')||'None yet'; c.innerHTML=`
Rating${g.rating.toFixed(1)}★
Animal welfare${pc('welfare')}%
Guest joy${pc('joy')}%
Species variety${pc('variety')}%
Facilities${pc('facilities')}%
Decor${pc('decor')}%
Species kept${g.speciesCount()}/15
Animals${g.animals.length}
Staff${g.staff.length}
Guests today${g.stats.guestsToday}
Total revenue${WK.fmtMoney(g.stats.revenueTotal)}
Inhabitants: ${occStr}
`; } }; UI.switchTab=function(tab){ UI._sideTab=tab; UI.renderSide(); }; /* ---------------- selection popup ---------------- */ UI.showSelection=function(target){ UI._selTarget=target; const el=$('sel-popup'); const g=UI.game; let html=''; if(target.kind==='animal'){ const sp=D.SPECIES[target.species]; const score=Math.round(g.habitatScore(target)); html=`

${WK.Sprites.iconImg('animal:'+target.species,40)}${target.name}

${sp.name} · appeal ${'★'.repeat(Math.min(5,Math.ceil(sp.appeal/1.6)))}
Happiness
Hunger
Habitat match${score}%
${target.sick?'
Sick — needs a vet!
':''}
`; }else if(target.kind==='guest'){ html=`

${target.kid?'Young guest':'Guest'}

Joy
${target.wantsRestroom?'
Looking for a restroom…
':''}`; }else if(target.kind==='staff'){ html=`

${target.role==='keeper'?'Keeper':'Vet'}

${target.workT>0?'Working…':target.job?'On the way!':'Patrolling the zoo'}
`; }else if(target.w&&target.h){ // building const def=D.BUILDING[target.type]; html=`

${WK.Sprites.iconImg('facility:'+target.type,40)}${def.name}

${def.desc?`
${def.desc}
`:''} ${def.income?`
Avg spend per guest${WK.fmtMoney(def.income)}
`:''} ${def.rp?`
Generates+${def.rp} RP/min
`:''}
`; }else if(target.kind==='habitat'){ const reg=g.world.regions.find(r=>r.id===target.regionId); const anims=g.animals.filter(a=>a.regionId===target.regionId); const occ={}; for(const a of anims) occ[a.name]=D.SPECIES[a.species].name; const occStr=anims.map(a=>a.name+' ('+D.SPECIES[a.species].name+')').join('
')||'Empty habitat'; html=`

Habitat

Area${reg.area} tiles
Inhabitants
${occStr}
`; }else return; el.innerHTML=html; el.classList.remove('hidden'); const sellBtn=el.querySelector('[data-sell]'); if(sellBtn) sellBtn.onclick=()=>{ const a=g.animals.find(x=>x.id==sellBtn.dataset.sell); if(a) g.sellAnimal(a); }; const demoBtn=el.querySelector('[data-demo]'); if(demoBtn) demoBtn.onclick=()=>{ const b=g.world.buildings.find(x=>x.id==demoBtn.dataset.demo); if(b) g.demolishBuilding(b); }; UI.positionSelection(); }; function barColor(v){ return v>60?'#63b34c':v>30?'#ffc93c':'#e85d5d'; } UI.positionSelection=function(){ const el=$('sel-popup'), t=UI._selTarget; if(!el||!t||el.classList.contains('hidden'))return; const p=UI.cam.project(t.x,t.y); let x=p.x+24,y=p.y-70; x=WK.clamp(x,8,UI.cam.w-el.offsetWidth-8); y=WK.clamp(y,60,UI.cam.h-el.offsetHeight-90); el.style.left=x+'px'; el.style.top=y+'px'; }; UI.trackSelection=function(){ const t=UI._selTarget; if(!t)return; if(t.kind==='animal'&&!UI.game.animals.includes(t)){ UI.hideSelection(); return; } if(t.kind==='guest'&&!UI.game.guests.includes(t)){ UI.hideSelection(); return; } if(t.kind==='staff'&&!UI.game.staff.includes(t)){ UI.hideSelection(); return; } if(t.w&&!UI.game.world.buildings.includes(t)){ UI.hideSelection(); return; } UI.positionSelection(); }; UI.hideSelection=function(){ UI._selTarget=null; $('sel-popup').classList.add('hidden'); }; /* ---------------- toasts ---------------- */ UI.toast=function(msg,type){ const box=$('toasts'); while(box.children.length>=4) box.firstChild.remove(); const t=document.createElement('div'); t.className='toast '+(type||''); t.textContent=msg; box.appendChild(t); setTimeout(()=>t.classList.add('fade'),3200); setTimeout(()=>t.remove(),3700); }; /* ---------------- modals ---------------- */ UI.modal=function(inner,opts){ opts=opts||{}; const root=$('modal-root'); root.innerHTML=``; root.firstChild.addEventListener('pointerdown',e=>{ if(e.target===root.firstChild&&!opts.sticky) UI.closeModal(); }); return root.querySelector('.modal'); }; UI.closeModal=function(){ $('modal-root').innerHTML=''; }; UI.settingsModal=function(){ const g=UI.game; const m=UI.modal(`

Settings

Sound effects
Music
Save game
New zoo
`); m.querySelector('#set-sfx').onclick=e=>{ WK.AudioSys.toggleSound(); e.target.textContent=WK.AudioSys.enabled?'On':'Off'; $('btn-sound').textContent=WK.AudioSys.enabled?'🔊':'🔇'; }; m.querySelector('#set-music').onclick=e=>{ WK.AudioSys.ensure(); WK.AudioSys.toggleMusic(); e.target.textContent=WK.AudioSys.musicOn?'On':'Off'; }; m.querySelector('#set-save').onclick=()=>{ g.save(); UI.toast('Zoo saved!','good'); }; m.querySelector('#set-new').onclick=()=>{ UI.modal(`

Start a new zoo?

Your current zoo and progress will be lost.

`,{sticky:true}); $('nc').onclick=()=>UI.settingsModal(); $('ny').onclick=()=>{ Game_restart(); }; }; m.querySelector('#set-close').onclick=()=>UI.closeModal(); }; function Game_restart(){ /* replaced by main.js */ } /* how-to */ UI.howtoModal=function(fromMenu){ UI.modal(`

How to Play

Hotkeys: Space pause · 1–3 speed · X bulldoze · Esc cancel tool

`); $('how-ok').onclick=()=>{ UI.closeModal(); if(fromMenu&&UI._onHowClose) UI._onHowClose(); }; }; UI.confirmNewFromMenu=function(){ /* main.js overrides menu flows */ }; /* ---------------- bindings ---------------- */ UI.bind=function(){ $('spd-pause').onclick=()=>{WK.AudioSys.sfx('click');UI.setSpeed(0);}; $('spd-play').onclick=()=>{WK.AudioSys.sfx('click');UI.setSpeed(1);}; $('spd-fast').onclick=()=>{WK.AudioSys.sfx('click');UI.setSpeed(2);}; $('spd-ultra').onclick=()=>{WK.AudioSys.sfx('click');UI.setSpeed(4);}; $('btn-sound').onclick=()=>{ WK.AudioSys.ensure(); const on=WK.AudioSys.toggleSound(); $('btn-sound').textContent=on?'🔊':'🔇'; }; $('btn-settings').onclick=()=>{ WK.AudioSys.sfx('click'); UI.settingsModal(); }; $('btn-bulldoze').onclick=()=>{ WK.AudioSys.sfx('click'); UI.toggleBulldoze(); }; document.querySelectorAll('.sp-tab').forEach(b=>b.onclick=()=>{ UI.switchTab(b.dataset.tab); }); $('sp-toggle').onclick=()=>{ const sp=$('side-panel'); sp.classList.toggle('hidden'); }; window.Game_restart = function(){ WK.Main.startNew(); UI.closeModal(); }; }; })();