- Procedural canvas art: 15 species × 4 poses, 15 buildings, modular terrain, UI icons - Habitat enclosure detection, biome suitability, staff/guest AI pathfinding - Economy: tickets, shops, wages, star rating, research tiers, 15 missions - Day/night cycle, particles, WebAudio soundtrack & synth SFX - Mouse + touch controls, responsive UI, localStorage autosave - Verified: 31/31 logic tests (test/smoke.js) · 12/12 render audits (test/qa.html)
415 lines
20 KiB
JavaScript
415 lines
20 KiB
JavaScript
/* ============================================================
|
||
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=i<g.missionIdx, cur=i===g.missionIdx;
|
||
if(i>g.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+=`<div class="mission ${done?'done':''} ${cur?'current':''}">
|
||
<div class="m-title"><span>${m.label}</span></div>
|
||
${m.hint&&!done?`<div class="m-hint">${m.hint}</div>`:''}
|
||
${!done?`<div class="m-reward">Reward: ${rw}</div>`:''}
|
||
</div>`;
|
||
}
|
||
});
|
||
if(g.missionIdx>=D.MISSIONS.length) h+='<div class="mission done"><div class="m-title"><span>All missions complete — Wildlife Legend! 🏆</span></div></div>';
|
||
c.innerHTML=h;
|
||
}else if(UI._sideTab==='research'){
|
||
const next=g.researchCost();
|
||
let h=`<div class="res-row"><span>Research points<small>Earn passively · Research Center boosts</small></span><b>${Math.floor(g.rp)} RP</b></div>`;
|
||
for(const t of D.RESEARCH_TIERS){
|
||
const owned=g.researchTier>=t.tier;
|
||
h+=`<div class="res-row">
|
||
<span>${t.label}<small>${owned?'Unlocked ✔':t.cost+' RP'}</small></span>
|
||
${owned?'':`<button class="btn" data-unlock="${t.tier}" ${(next!==t.cost)?'disabled':''}>Unlock</button>`}
|
||
</div>`;
|
||
}
|
||
h+='<p style="font-size:11px;color:var(--ink-soft);margin:6px 2px;">New terrain, buildings & rare animals arrive with each tier.</p>';
|
||
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=`
|
||
<div class="zoo-stat"><span>Rating</span><b>${g.rating.toFixed(1)}★</b></div>
|
||
<div class="zoo-stat"><span>Animal welfare</span><b>${pc('welfare')}%</b></div>
|
||
<div class="zoo-stat"><span>Guest joy</span><b>${pc('joy')}%</b></div>
|
||
<div class="zoo-stat"><span>Species variety</span><b>${pc('variety')}%</b></div>
|
||
<div class="zoo-stat"><span>Facilities</span><b>${pc('facilities')}%</b></div>
|
||
<div class="zoo-stat"><span>Decor</span><b>${pc('decor')}%</b></div>
|
||
<div class="zoo-stat"><span>Species kept</span><b>${g.speciesCount()}/15</b></div>
|
||
<div class="zoo-stat"><span>Animals</span><b>${g.animals.length}</b></div>
|
||
<div class="zoo-stat"><span>Staff</span><b>${g.staff.length}</b></div>
|
||
<div class="zoo-stat"><span>Guests today</span><b>${g.stats.guestsToday}</b></div>
|
||
<div class="zoo-stat"><span>Total revenue</span><b>${WK.fmtMoney(g.stats.revenueTotal)}</b></div>
|
||
<div style="margin-top:8px;font-size:12px;line-height:1.4;color:var(--ink-soft)"><b>Inhabitants:</b> ${occStr}</div>`;
|
||
}
|
||
};
|
||
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=`
|
||
<h3>${WK.Sprites.iconImg('animal:'+target.species,40)}<span>${target.name}</span></h3>
|
||
<div class="row"><span>${sp.name} · appeal ${'★'.repeat(Math.min(5,Math.ceil(sp.appeal/1.6)))}</span></div>
|
||
<div class="row"><span>Happiness</span></div>
|
||
<div class="bar"><i style="width:${target.happiness|0}%;background:${barColor(target.happiness)}"></i></div>
|
||
<div class="row"><span>Hunger</span></div>
|
||
<div class="bar"><i style="width:${target.hunger|0}%;background:${barColor(100-target.hunger)}"></i></div>
|
||
<div class="row"><span>Habitat match</span><b>${score}%</b></div>
|
||
${target.sick?'<div class="row"><b style="color:var(--red)">Sick — needs a vet!</b></div>':''}
|
||
<div class="btns"><button class="btn danger" data-sell="${target.id}">Rehome +${WK.fmtMoney(sp.cost*0.45)}</button></div>`;
|
||
}else if(target.kind==='guest'){
|
||
html=`
|
||
<h3><span>${target.kid?'Young guest':'Guest'}</span></h3>
|
||
<div class="row"><span>Joy</span></div>
|
||
<div class="bar"><i style="width:${target.joy|0}%;background:${barColor(target.joy)}"></i></div>
|
||
${target.wantsRestroom?'<div class="row"><b style="color:var(--orange)">Looking for a restroom…</b></div>':''}`;
|
||
}else if(target.kind==='staff'){
|
||
html=`
|
||
<h3><span>${target.role==='keeper'?'Keeper':'Vet'}</span></h3>
|
||
<div class="row"><span>${target.workT>0?'Working…':target.job?'On the way!':'Patrolling the zoo'}</span></div>`;
|
||
}else if(target.w&&target.h){ // building
|
||
const def=D.BUILDING[target.type];
|
||
html=`
|
||
<h3>${WK.Sprites.iconImg('facility:'+target.type,40)}<span>${def.name}</span></h3>
|
||
${def.desc?`<div class="row"><span>${def.desc}</span></div>`:''}
|
||
${def.income?`<div class="row"><span>Avg spend per guest</span><b>${WK.fmtMoney(def.income)}</b></div>`:''}
|
||
${def.rp?`<div class="row"><span>Generates</span><b>+${def.rp} RP/min</b></div>`:''}
|
||
<div class="btns"><button class="btn danger" data-demo="${target.id}">Demolish +${WK.fmtMoney(def.cost*0.5)}</button></div>`;
|
||
}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('<br>')||'<i>Empty habitat</i>';
|
||
html=`
|
||
<h3><span>Habitat</span></h3>
|
||
<div class="row"><span>Area</span><b>${reg.area} tiles</b></div>
|
||
<div class="row"><span>Inhabitants</span></div>
|
||
<div style="font-size:12px;margin:2px 0 4px">${occStr}</div>`;
|
||
}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=`<div class="modal-backdrop"><div class="modal panel">${inner}</div></div>`;
|
||
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(`
|
||
<h2>Settings</h2>
|
||
<div class="set-row"><span>Sound effects</span><button class="btn" id="set-sfx">${WK.AudioSys.enabled?'On':'Off'}</button></div>
|
||
<div class="set-row"><span>Music</span><button class="btn" id="set-music">${WK.AudioSys.musicOn?'On':'Off'}</button></div>
|
||
<div class="set-row"><span>Save game</span><button class="btn" id="set-save">Save now</button></div>
|
||
<div class="set-row"><span>New zoo</span><button class="btn danger" id="set-new">Start over</button></div>
|
||
<div class="modal-btns"><button class="btn primary" id="set-close">Done</button></div>`);
|
||
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(`<h2>Start a new zoo?</h2><p>Your current zoo and progress will be lost.</p>
|
||
<div class="modal-btns"><button class="btn" id="nc">Cancel</button><button class="btn danger" id="ny">Yes, start over</button></div>`,{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(`
|
||
<h2>How to Play</h2>
|
||
<ul>
|
||
<li><b>Pan</b> by dragging (right-click / empty hand / one finger), <b>zoom</b> with the wheel or pinch.</li>
|
||
<li><b>Paths first!</b> Guests only walk on paths. Connect everything to your entrance.</li>
|
||
<li><b>Habitats:</b> fence an area completely, paint matching terrain inside, then adopt animals into it. Each species has favorite biomes and needs space.</li>
|
||
<li><b>Keepers feed animals</b> — hire one with a Keeper Hut. Add a Keeper Gate so they can walk in.</li>
|
||
<li><b>Guests need</b> restrooms, food stands, benches and beautiful decor. Happy guests pay more entry and spend more.</li>
|
||
<li><b>Research</b> unlocks rare species and fancy buildings. A Research Center speeds it up.</li>
|
||
<li>Watch the <b>Missions</b> panel — rewards guide your way to a 5★ zoo!</li>
|
||
</ul>
|
||
<p style="font-size:12px;color:var(--ink-soft)">Hotkeys: <span class="kbd">Space</span> pause · <span class="kbd">1–3</span> speed · <span class="kbd">X</span> bulldoze · <span class="kbd">Esc</span> cancel tool</p>
|
||
<div class="modal-btns"><button class="btn primary" id="how-ok">Let's build!</button></div>`);
|
||
$('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();
|
||
};
|
||
};
|
||
})();
|