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)
This commit is contained in:
+178
@@ -0,0 +1,178 @@
|
||||
/* ============================================================
|
||||
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&®.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);
|
||||
})();
|
||||
Reference in New Issue
Block a user