Files
wildlife-kingdom/js/main.js
T
deepseek c06786518c Wildlife Kingdom — original browser zoo-management sim
- 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)
2026-08-23 07:01:21 +00:00

179 lines
5.4 KiB
JavaScript

/* ============================================================
Wildlife Kingdom — js/main.js
Boot · animated hero menu · game loop · autosave · resizing
============================================================ */
(function(){
'use strict';
const WK = window.WK;
const Main = (WK.Main = {});
let canvas, renderer, cam, input;
let game=null;
let mode='menu';
let heroGame=null;
let heroT=0;
let lastT=performance.now();
let saveT=0;
/* ---------------- boot ---------------- */
function boot(){
canvas=document.getElementById('game');
renderer=new WK.Renderer(canvas);
cam=new WK.Camera();
cam.resize(renderer.cssW,renderer.cssH);
const small=Math.min(window.innerWidth,window.innerHeight)<720;
cam.zoom=small?0.8:1.05;
input=new WK.Input(canvas,null,cam,renderer,WK.UI);
// QA hooks: ?autostart=1&fast=NN jumps straight into a running zoo
const qa=new URLSearchParams(location.search);
Main.qaMode=qa.has('autostart');
window.addEventListener('resize',onResize);
document.addEventListener('visibilitychange',()=>{
if(document.hidden&&mode==='game'&&game){ game.save(); }
});
// menu buttons
const cont=document.getElementById('btn-continue');
cont.disabled=!WK.Game.hasSave();
cont.onclick=()=>{ click(); const g=WK.Game.load(); if(g) enterGame(g); else { startNew(); } };
document.getElementById('btn-new').onclick=()=>{ click(); startNew(); };
document.getElementById('btn-how').onclick=()=>{ click(); WK.UI._onHowClose=()=>{}; WK.UI.howtoModal(true); };
function click(){ WK.AudioSys.ensure(); WK.AudioSys.sfx('click'); }
startHero();
if(Main.qaMode){
startNew();
const steps=parseInt(qa.get('fast')||'0',10)||0;
for(let i=0;i<steps;i++) game.tick(1/30);
const focus=qa.get('focus');
if(focus){
const [fx,fy]=focus.split(',').map(Number);
cam.centerOnTile(fx,fy);
}
}
requestAnimationFrame(loop);
}
function onResize(){
renderer.resize();
cam.resize(renderer.cssW,renderer.cssH);
}
/* ---------------- hero (menu) zoo ---------------- */
function startHero(){
heroGame=new WK.Game();
WK.Game.buildDemo(heroGame);
heroGame.speed=1;
// pre-seed wandering guests on the paths
const w=heroGame.world;
const pathTiles=[];
for(let y=0;y<w.rows;y++)for(let x=0;x<w.cols;x++) if(w.pid(x,y)) pathTiles.push([x,y]);
for(let i=0;i<70&&pathTiles.length;i++){
const [x,y]=pathTiles[(Math.random()*pathTiles.length)|0];
heroGame.guests.push(new WK.Guest(heroGame,x+0.5,y+0.5));
}
heroGame.rebuildPois();
cam.centerOnTile(w.cols/2,w.rows*0.42);
}
function heroDrift(dt){
heroT+=dt;
const w=heroGame.world;
const cx=w.cols/2, cy=w.rows*0.44;
cam.centerOnTile(
cx+Math.sin(heroT*0.11)*w.cols*0.16,
cy+Math.cos(heroT*0.07)*w.rows*0.10);
}
/* ---------------- game lifecycle ---------------- */
function startNew(){
const g=new WK.Game();
WK.Game.buildStarter(g);
g.world.computeRegions();
g.rebuildPois();
enterGame(g,true);
}
Main.startNew=startNew;
function enterGame(g,fresh){
game=g;
mode='game';
document.getElementById('menu').classList.add('hidden');
document.getElementById('hud-top').classList.remove('hidden');
document.getElementById('toolbar').classList.remove('hidden');
document.getElementById('side-panel').classList.remove('hidden');
document.getElementById('sp-toggle').classList.remove('hidden');
if(window.innerWidth<1100) document.getElementById('side-panel').classList.add('hidden');
input.game=game;
WK.UI.init(game,cam,renderer);
if(fresh&&!Main.qaMode){
g.save();
setTimeout(()=>{ WK.UI.howtoModal(false); },350);
g.notify('Welcome to Wildlife Kingdom! Follow the missions panel.','gold');
}
// camera on the entrance
const e=g.exitTile||[g.world.cols/2,g.world.rows/2];
cam.centerOnTile(e[0],e[1]-4);
cam.clampToWorld(g.world);
WK.AudioSys.setScene('game');
if(WK.AudioSys.enabled&&!WK.AudioSys.musicOn===false){} /* music pref respected internally */
}
function exitToMenu(){ /* kept simple: no in-game exit button besides reload */
}
/* ---------------- main loop ---------------- */
function loop(now){
requestAnimationFrame(loop);
let dt=(now-lastT)/1000;
lastT=now;
if(dt>0.12) dt=0.12;
input.update(dt);
if(mode==='menu'){
if(heroGame){
heroGame.timeAbs+=dt;
const sdt=dt; // gentle life even on menu
heroGame.minutes+=sdt*2;
if(heroGame.minutes>=1440)heroGame.minutes-=1440;
heroGame.spawnAcc+=sdt*0.02;
for(const a of heroGame.animals) a.update(sdt*0.6,heroGame);
for(const gu of heroGame.guests) gu.update(sdt*0.75);
for(const s of heroGame.staff) s.update(sdt,heroGame);
heroGame.particles.update(sdt);
heroDrift(dt);
renderer.frame(heroGame,cam,{});
}
return;
}
// gameplay
game.tick(dt);
const grid=!!(WK.UI.tool||WK.UI.bulldoze);
let tint=null;
if(input.hover&&(WK.UI.tool&&['fences','animals'].includes(WK.UI.tool.cat)||WK.UI.bulldoze)){
game.world.ensureRegions();
const rid=game.world.regionOf(input.hover.x,input.hover.y);
if(rid>=0){
const reg=game.world.regions.find(r=>r.id===rid);
if(reg&&reg.openEdges===0&&!reg.hasPath) tint=rid;
else if(WK.UI.tool&&WK.UI.tool.cat==='animals') tint=rid;
}
}
renderer.frame(game,cam,{
grid,
tintRegionId:tint,
overlay:(ctx,c)=>input.overlay(ctx,c),
});
WK.UI.updateHUD();
saveT+=dt;
if(saveT>30){ saveT=0; if(game.save()){} }
}
window.addEventListener('DOMContentLoaded',boot);
})();