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:
+195
@@ -0,0 +1,195 @@
|
||||
<!DOCTYPE html>
|
||||
<html><head><meta charset="utf8"><title>QARUN</title></head><body>
|
||||
<canvas id="game" width="1200" height="800"></canvas>
|
||||
<pre id="out">PENDING</pre>
|
||||
<script src="../js/util.js"></script>
|
||||
<script src="../js/data.js"></script>
|
||||
<script src="../js/audio.js"></script>
|
||||
<script src="../js/sprites.js"></script>
|
||||
<script src="../js/world.js"></script>
|
||||
<script src="../js/entities.js"></script>
|
||||
<script src="../js/sim.js"></script>
|
||||
<script src="../js/render.js"></script>
|
||||
<script>
|
||||
'use strict';
|
||||
window.onerror=(m,s,l)=>{ document.getElementById('out').textContent='JSERROR: '+m+' @'+s+':'+l; };
|
||||
(async function(){
|
||||
const out={checks:[],errors:[]};
|
||||
const T=(name,cond,info)=>out.checks.push((cond?'PASS ':'FAIL ')+name+(info?(' :: '+info):''));
|
||||
try{
|
||||
const WK=window.WK;
|
||||
const canvas=document.getElementById('game');
|
||||
const renderer=new WK.Renderer(canvas);
|
||||
// force css size (headless has no layout box matching)
|
||||
renderer.cssW=1200; renderer.cssH=800;
|
||||
renderer.canvas.width=1200; renderer.canvas.height=800;
|
||||
renderer.canvas.style.width='1200px'; renderer.canvas.style.height='800px';
|
||||
const cam=new WK.Camera();
|
||||
cam.resize(1200,800);
|
||||
|
||||
/* ---------- hero demo zoo render ---------- */
|
||||
const hero=new WK.Game();
|
||||
WK.Game.buildDemo(hero);
|
||||
hero.timeAbs=1.2;
|
||||
cam.centerOnTile(hero.world.cols-2,hero.world.rows-3);
|
||||
cam.zoom=0.95;
|
||||
renderer.frame(hero,cam,{});
|
||||
await new Promise(r=>setTimeout(r,50));
|
||||
let img=renderer.ctx.getImageData(0,0,1200,800).data;
|
||||
const colors=new Set();
|
||||
for(let i=0;i<img.length;i+=40){ colors.add((img[i]>>3)+','+(img[i+1]>>3)+','+(img[i+2]>>3)); }
|
||||
T('demo frame rich color count',colors.size>400,String(colors.size));
|
||||
// sky should be blue-ish at top (camera at south corner → upper screen is off-map)
|
||||
const px=(x,y)=>{ const k=(y*1200+x)*4; return [img[k],img[k+1],img[k+2]]; };
|
||||
const sky=px(600,8);
|
||||
T('sky blue at top',sky[2]>140&&sky[2]>sky[0],sky.join(','));
|
||||
// center should contain green grass tones somewhere in middle band
|
||||
let greens=0,total=0;
|
||||
for(let y=300;y<500;y+=10)for(let x=100;x<1100;x+=10){
|
||||
const c=px(x,y); total++;
|
||||
if(c[1]>c[0]&&c[1]>c[2]&&c[1]>90) greens++;
|
||||
}
|
||||
T('green terrain visible mid-frame',greens/total>0.25,Math.round(greens/total*100)+'%');
|
||||
// water shimmer: some cyan-blue pixels
|
||||
let blues=0;
|
||||
for(let y=250;y<700;y+=7)for(let x=150;x<1050;x+=7){
|
||||
const c=px(x,y);
|
||||
if(c[2]>130&&c[2]>c[0]+30&&c[1]>110) blues++;
|
||||
}
|
||||
T('water tones present',blues>12,String(blues));
|
||||
|
||||
/* ---------- night frame ---------- */
|
||||
hero.minutes=23*60;
|
||||
renderer.frame(hero,cam,{});
|
||||
await new Promise(r=>setTimeout(r,20));
|
||||
img=renderer.ctx.getImageData(0,0,1200,800).data;
|
||||
const night=px(600,8);
|
||||
T('night darkens sky',night[2]<sky[2],night.join(','));
|
||||
|
||||
/* ---------- starter zoo + entities draw ---------- */
|
||||
const g=new WK.Game();
|
||||
WK.Game.buildStarter(g);
|
||||
g.rebuildPois();
|
||||
// add animal & guest & keeper directly into view
|
||||
g.world.computeRegions();
|
||||
const rid=g.world.regionOf(23,36);
|
||||
if(rid>0){}else{ /* no habitat in starter; force one */ }
|
||||
g.money=99999;
|
||||
// build quick habitat near starter (interior 18..24 × 28..33)
|
||||
const w=g.world;
|
||||
for(let y=28;y<=33;y++)for(let x=18;x<=24;x++) w.setGround(x,y,'grass');
|
||||
for(let x=17;x<=24;x++){ w.setFence(x,27,'S',1); w.setFence(x,33,'S',1); }
|
||||
for(let y=28;y<=33;y++){ w.setFence(17,y,'E',1); w.setFence(24,y,'E',1); }
|
||||
w.computeRegions();
|
||||
const reg=w.validHabitats()[0];
|
||||
T('qa habitat valid',!!reg,String(reg&®.area));
|
||||
if(reg){
|
||||
g.tryAdopt('zebra',21,30);
|
||||
g.tryAdopt('flamingo',21,31);
|
||||
}
|
||||
g.guests.push(new WK.Guest(g,23,41));
|
||||
g.staff.push(new WK.Staff('keeper',22,41));
|
||||
g.animals[0]&&(g.animals[0].state='happy');
|
||||
cam.centerOnTile(22,34);
|
||||
cam.zoom=1.15;
|
||||
hero.minutes=12*60;
|
||||
g.timeAbs=2.5; g.minutes=12*60;
|
||||
renderer.frame(g,cam,{grid:true});
|
||||
await new Promise(r=>setTimeout(r,50));
|
||||
img=renderer.ctx.getImageData(0,0,1200,800).data;
|
||||
const px2=(x,y)=>{ const k=(y*1200+x)*4; return [img[k],img[k+1],img[k+2]]; };
|
||||
// HUD-free center: expect path beige tones near plaza
|
||||
let beiges=0;
|
||||
for(let y=350;y<650;y+=6)for(let x=200;x<1000;x+=6){
|
||||
const c=px2(x,y);
|
||||
if(c[0]>190&&c[1]>170&&c[2]>130&&c[0]>=c[1]&&c[1]>c[2]) beiges++;
|
||||
}
|
||||
T('starter paths visible',beiges>40,String(beiges));
|
||||
|
||||
/* ---------- sprite icon audit ---------- */
|
||||
let blankIcons=[];
|
||||
const kinds=['res:coin','res:guest','res:heart','res:flask','res:star','ground:grass','ground:snow','path:gravel','path:stonep','fence:wood','fence:gate','nature:tree','nature:palm','nature:lamp','animal:lion','animal:panda','animal:penguin','animal:elephant','facility:aquarium','facility:hut','facility:entrance_g'];
|
||||
for(const kd of kinds){
|
||||
const s=WK.Sprites.getIcon(kd,52);
|
||||
const d=s.cv.getContext('2d').getImageData(0,0,s.cv.width,s.cv.height).data;
|
||||
let a=0; for(let i=3;i<d.length;i+=16) a+=d[i];
|
||||
if(a<40) blankIcons.push(kd);
|
||||
}
|
||||
T('all UI icons painted',blankIcons.length===0,blankIcons.join(',')||'ok');
|
||||
|
||||
/* ---------- all species draw without error ---------- */
|
||||
let badSpecies=[];
|
||||
for(const sp of Object.keys(WK.Data.SPECIES)){
|
||||
const cv=WK.mkCanvas(160,160); const cx=cv.getContext('2d');
|
||||
cx.translate(80,140);
|
||||
try{
|
||||
for(const st of ['idle','walk','eat','happy'])
|
||||
WK.Sprites.drawAnimal(cx,sp,{state:st,ph:1.3,t:1.7});
|
||||
const d=cv.getContext('2d').getImageData(0,0,160,160).data;
|
||||
let a=0; for(let i=3;i<d.length;i+=64) a+=d[i];
|
||||
if(a<25) badSpecies.push(sp+':dim');
|
||||
}catch(e){ badSpecies.push(sp+':'+e.message); }
|
||||
}
|
||||
T('15 species × 4 poses render',badSpecies.length===0,badSpecies.join(',')||'ok');
|
||||
|
||||
/* ---------- buildings render ---------- */
|
||||
let badBld=[];
|
||||
for(const b of Object.keys(WK.Data.BUILDING)){
|
||||
try{
|
||||
const s=WK.Sprites.getBuilding(b);
|
||||
const d=s.cv.getContext('2d').getImageData(0,0,s.cv.width,s.cv.height).data;
|
||||
let a=0; for(let i=3;i<d.length;i+=97) a+=d[i];
|
||||
if(a<30) badBld.push(b+':dim');
|
||||
}catch(e){ badBld.push(b+':'+e.message); }
|
||||
}
|
||||
T('15 buildings render',badBld.length===0,badBld.join(',')||'ok');
|
||||
|
||||
/* ---------- fence continuity (regression: corner-exact endpoints) ---------- */
|
||||
/* Wall topology in this iso model:
|
||||
· S-edges chain over +x → "\" wall · E-edges chain over +y → "/" wall */
|
||||
{
|
||||
const w2=new WK.World(20,20);
|
||||
w2.setFence(5,5,'E',1); w2.setFence(5,6,'E',1); w2.setFence(5,7,'E',1); // "/" run over +y
|
||||
const cam2=new WK.Camera(); cam2.resize(420,320);
|
||||
cam2.centerOnTile(5.4,6.4); cam2.zoom=1.5;
|
||||
const cv2=WK.mkCanvas(420,320), c2=cv2.getContext('2d');
|
||||
const z=cam2.zoom;
|
||||
// draw exactly like render.js does
|
||||
for(const ty of [5,6,7]){
|
||||
const pp=cam2.project(5,ty);
|
||||
WK.Sprites.drawFenceEdge(c2,pp.x,pp.y+WK.HH*z,pp.x+WK.HW*z,pp.y,1,0);
|
||||
}
|
||||
// sample along the rail from E-corner of (5,5) to S-corner of (5,7)
|
||||
const t55=cam2.project(5,5), t57=cam2.project(5,7);
|
||||
const sx=t55.x+WK.HW*z, sy=t55.y; // start (E corner, top)
|
||||
const ex=t57.x, ey=t57.y+WK.HH*z; // end (S corner, bottom)
|
||||
let gaps=0,samples=0;
|
||||
for(let tt=0.02;tt<=0.98;tt+=0.01){
|
||||
samples++;
|
||||
const xx=Math.round(sx+(ex-sx)*tt), yy=Math.round(sy+(ey-sy)*tt-5); // mid rail
|
||||
const d=c2.getImageData(xx,yy,1,1).data;
|
||||
if(d[3]<10) gaps++;
|
||||
}
|
||||
T('straight fence run continuous',gaps<=1,gaps+'/'+samples+' gaps');
|
||||
// L-corner joint: E-edge of (5,5) meets S-edge of (5,5) at the shared S corner
|
||||
const cv3=WK.mkCanvas(420,320), c3=cv3.getContext('2d');
|
||||
const p55=cam2.project(5,5);
|
||||
WK.Sprites.drawFenceEdge(c3,p55.x,p55.y+WK.HH*z,p55.x+WK.HW*z,p55.y,1,0);
|
||||
WK.Sprites.drawFenceEdge(c3,p55.x-WK.HW*z,p55.y,p55.x,p55.y+WK.HH*z,1,0);
|
||||
const jx=Math.round(p55.x), jy=Math.round(p55.y+WK.HH*z-8); // post zone at joint
|
||||
let post=0;
|
||||
for(let dx=-2;dx<=2;dx++)for(let dy=-2;dy<=2;dy++){
|
||||
const d=c3.getImageData(jx+dx,jy+dy,1,1).data;
|
||||
if(d[3]>60&&d[0]>90&&d[0]<200&&d[1]>60&&d[2]<130) post++;
|
||||
}
|
||||
T('corner joint has shared post',post>=8,String(post));
|
||||
}
|
||||
|
||||
}catch(e){
|
||||
out.errors.push(e.stack||String(e));
|
||||
}
|
||||
document.getElementById('out').textContent='QAJSON '+JSON.stringify(out);
|
||||
document.title='QADONE';
|
||||
})();
|
||||
</script>
|
||||
</body></html>
|
||||
Reference in New Issue
Block a user