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:
+150
@@ -0,0 +1,150 @@
|
||||
/* ============================================================
|
||||
Wildlife Kingdom — js/util.js
|
||||
Math, RNG, color & canvas helpers. Exposes global WK.
|
||||
============================================================ */
|
||||
(function(){
|
||||
'use strict';
|
||||
const WK = (window.WK = window.WK || {});
|
||||
|
||||
WK.TILE_W = 64; // iso diamond width
|
||||
WK.TILE_H = 32; // iso diamond height
|
||||
WK.HW = WK.TILE_W / 2;
|
||||
WK.HH = WK.TILE_H / 2;
|
||||
|
||||
/* ---- math ---- */
|
||||
WK.clamp = (v,a,b)=> v<a?a:(v>b?b:v);
|
||||
WK.lerp = (a,b,t)=> a+(b-a)*t;
|
||||
WK.dist2 = (ax,ay,bx,by)=>{const dx=ax-bx,dy=ay-by;return dx*dx+dy*dy;};
|
||||
WK.dist = (ax,ay,bx,by)=> Math.hypot(ax-bx,ay-by);
|
||||
WK.TAU = Math.PI*2;
|
||||
WK.easeOutCubic = t=>1-Math.pow(1-t,3);
|
||||
WK.easeInOut = t=> t<0.5 ? 2*t*t : 1-Math.pow(-2*t+2,2)/2;
|
||||
|
||||
/* deterministic RNG */
|
||||
WK.mulberry32 = function(seed){
|
||||
let a = seed>>>0;
|
||||
return function(){
|
||||
a |= 0; a = (a + 0x6D2B79F5)|0;
|
||||
let t = Math.imul(a ^ (a>>>15), 1|a);
|
||||
t = (t + Math.imul(t ^ (t>>>7), 61|t)) ^ t;
|
||||
return ((t ^ (t>>>14)) >>> 0) / 4294967296;
|
||||
};
|
||||
};
|
||||
WK.hash2 = function(x,y,s){
|
||||
let h = (x*374761393 + y*668265263) ^ (s|0)*1442695041;
|
||||
h = (h ^ (h>>>13)) >>> 0;
|
||||
h = Math.imul(h, 1274126177) >>> 0;
|
||||
return ((h ^ (h>>>16)) % 100000)/100000; // 0..1
|
||||
};
|
||||
|
||||
/* ---- formatting ---- */
|
||||
WK.fmtMoney = n=>{
|
||||
const neg = n<0; n = Math.round(Math.abs(n));
|
||||
let s = String(n).replace(/\B(?=(\d{3})+(?!\d))/g,',');
|
||||
return (neg?'-$':'$')+s;
|
||||
};
|
||||
WK.fmtClock = min=>{ // minutes in day
|
||||
min = ((min%1440)+1440)%1440;
|
||||
const h=Math.floor(min/60), m=Math.floor(min%60);
|
||||
return String(h).padStart(2,'0')+':'+String(m).padStart(2,'0');
|
||||
};
|
||||
|
||||
/* ---- colors ---- */
|
||||
WK.hexToRgb = hex=>{
|
||||
hex = hex.replace('#','');
|
||||
if(hex.length===3) hex = hex.split('').map(c=>c+c).join('');
|
||||
const n=parseInt(hex,16);
|
||||
return [(n>>16)&255,(n>>8)&255,n&255];
|
||||
};
|
||||
WK.shade = (hex,amt)=>{ // amt -1..1 (negative darker)
|
||||
const [r,g,b]=WK.hexToRgb(hex);
|
||||
const f=c=> WK.clamp(Math.round(amt>=0 ? c+(255-c)*amt : c*(1+amt)),0,255);
|
||||
return `rgb(${f(r)},${f(g)},${f(b)})`;
|
||||
};
|
||||
WK.rgba = (hex,a)=>{
|
||||
const [r,g,b]=WK.hexToRgb(hex); return `rgba(${r},${g},${b},${a})`;
|
||||
};
|
||||
WK.mix = (h1,h2,t)=>{
|
||||
const a=WK.hexToRgb(h1), b=WK.hexToRgb(h2);
|
||||
return `rgb(${Math.round(WK.lerp(a[0],b[0],t))},${Math.round(WK.lerp(a[1],b[1],t))},${Math.round(WK.lerp(a[2],b[2],t))})`;
|
||||
};
|
||||
|
||||
/* ---- canvas helpers ---- */
|
||||
WK.mkCanvas = (w,h)=>{ const c=document.createElement('canvas'); c.width=Math.max(1,Math.ceil(w)); c.height=Math.max(1,Math.ceil(h)); return c; };
|
||||
|
||||
WK.rr = function(ctx,x,y,w,h,r){ // rounded rect path
|
||||
if(typeof r==='number') r={tl:r,tr:r,br:r,bl:r};
|
||||
else r = Object.assign({tl:0,tr:0,br:0,bl:0},r);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x+r.tl,y);
|
||||
ctx.lineTo(x+w-r.tr,y); ctx.arcTo(x+w,y,x+w,y+r.tr,r.tr);
|
||||
ctx.lineTo(x+w,y+h-r.br); ctx.arcTo(x+w,y+h,x+w-r.br,y+h,r.br);
|
||||
ctx.lineTo(x+r.bl,y+h); ctx.arcTo(x,y+h,x,y+h-r.bl,r.bl);
|
||||
ctx.lineTo(x,y+r.tl); ctx.arcTo(x,y,x+r.tl,y,r.tl);
|
||||
ctx.closePath();
|
||||
};
|
||||
|
||||
WK.softShadow = function(ctx, blur, color, offY){
|
||||
ctx.shadowBlur=blur; ctx.shadowColor=color||'rgba(70,45,10,.35)';
|
||||
ctx.shadowOffsetY = offY==null?2:offY;
|
||||
};
|
||||
WK.clearShadow = ctx=>{ ctx.shadowBlur=0; ctx.shadowColor='transparent'; ctx.shadowOffsetY=0; };
|
||||
|
||||
/* ellipse blob with vertical gradient for cute volume */
|
||||
WK.blob = function(ctx,x,y,rx,ry,color,lighten){
|
||||
const g = ctx.createRadialGradient(x-rx*0.35,y-ry*0.55,ry*0.15,x,y,Math.max(rx,ry));
|
||||
g.addColorStop(0, lighten || WK.shade(color,0.28));
|
||||
g.addColorStop(1, color);
|
||||
ctx.fillStyle=g;
|
||||
ctx.beginPath(); ctx.ellipse(x,y,rx,ry,0,0,WK.TAU); ctx.fill();
|
||||
};
|
||||
|
||||
/* simple eye: white + pupil + shine */
|
||||
WK.eye = function(ctx,x,y,r,opt){
|
||||
opt=opt||{};
|
||||
if(opt.closed){
|
||||
ctx.strokeStyle=opt.lidColor||'#3a2c20'; ctx.lineWidth=Math.max(1.5,r*0.5);
|
||||
ctx.beginPath(); ctx.moveTo(x-r,y*0.98); ctx.quadraticCurveTo(x,y+r*0.9,x+r,y*0.98); ctx.stroke();
|
||||
return;
|
||||
}
|
||||
ctx.fillStyle='#fff';
|
||||
ctx.beginPath(); ctx.ellipse(x,y,r,r*(opt.tall||1),0,0,WK.TAU); ctx.fill();
|
||||
const px = x + (opt.lookX||0)*r*0.28, py = y + (opt.lookY||0.08)*r*0.4;
|
||||
ctx.fillStyle=opt.pupil||'#33261a';
|
||||
ctx.beginPath(); ctx.ellipse(px,py,r*0.52,r*0.62*(opt.tall||1),0,0,WK.TAU); ctx.fill();
|
||||
ctx.fillStyle='#fff';
|
||||
ctx.beginPath(); ctx.arc(px-r*0.18,py-r*0.24,r*0.2,0,WK.TAU); ctx.fill();
|
||||
};
|
||||
|
||||
/* tiny smile / open mouth */
|
||||
WK.mouth = function(ctx,x,y,w,open,color){
|
||||
ctx.fillStyle = color||'#7c4a3a';
|
||||
if(open>0.05){
|
||||
ctx.beginPath(); ctx.ellipse(x,y,w,w*(0.5+open),0,0,Math.PI); ctx.fill();
|
||||
ctx.fillStyle='#e8756f';
|
||||
ctx.beginPath(); ctx.ellipse(x,y+w*(0.35+open*0.5),w*0.55,w*0.4*open,0,0,Math.PI); ctx.fill();
|
||||
}else{
|
||||
ctx.strokeStyle=color||'#7c4a3a'; ctx.lineWidth=Math.max(1.2,w*0.22); ctx.lineCap='round';
|
||||
ctx.beginPath(); ctx.moveTo(x-w,y-w*0.25); ctx.quadraticCurveTo(x,y+w*0.65,x+w,y-w*0.25); ctx.stroke();
|
||||
}
|
||||
};
|
||||
|
||||
WK.blush = function(ctx,x,y,r,color,alpha){
|
||||
ctx.globalAlpha = alpha==null?0.35:alpha;
|
||||
ctx.fillStyle=color||'#ff9d8a';
|
||||
ctx.beginPath(); ctx.ellipse(x,y,r,r*0.62,0,0,WK.TAU); ctx.fill();
|
||||
ctx.globalAlpha=1;
|
||||
};
|
||||
|
||||
/* object pool-ish array clear */
|
||||
WK.clearArr = a=>{ a.length=0; return a; };
|
||||
|
||||
/* pick weighted {id:w} map or [ [item,w],... ] */
|
||||
WK.weightedPick = function(entries,rnd){
|
||||
rnd = rnd||Math.random;
|
||||
let tot=0; for(const e of entries) tot+=e.w;
|
||||
let r=rnd()*tot;
|
||||
for(const e of entries){ r-=e.w; if(r<=0) return e; }
|
||||
return entries[entries.length-1];
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user