Files
wildlife-kingdom/js/util.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

151 lines
5.3 KiB
JavaScript

/* ============================================================
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];
};
})();