- 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)
179 lines
5.4 KiB
JavaScript
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&®.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);
|
|
})();
|