/* ============================================================ Wildlife Kingdom — js/render.js Isometric camera · layered renderer · cached terrain · water sparkle · day/night light · ghost previews · hero scene ============================================================ */ (function(){ 'use strict'; const WK = window.WK; const D = WK.Data; const HW = WK.HW, HH = WK.HH, TAU = WK.TAU; /* ---------------- Camera ---------------- */ class Camera{ constructor(){ this.x=0; this.y=0; this.zoom=1; this.w=800; this.h=600; } resize(w,h){ this.w=w; this.h=h; } centerOnTile(gx,gy){ const wx=(gx-gy)*HW, wy=(gx+gy)*HH; this.x=wx; this.y=wy; } /* tile coords → screen px */ project(gx,gy){ const wx=(gx-gy)*HW, wy=(gx+gy)*HH; return { x:(wx-this.x)*this.zoom + this.w/2, y:(wy-this.y)*this.zoom + this.h/2, }; } worldToScreen(wx,wy){ return { x:(wx-this.x)*this.zoom+this.w/2, y:(wy-this.y)*this.zoom+this.h/2 }; } screenToWorld(sx,sy){ return { x:(sx-this.w/2)/this.zoom + this.x, y:(sy-this.h/2)/this.zoom + this.y, }; } screenToTile(sx,sy){ const w=this.screenToWorld(sx,sy); return { x:Math.floor((w.x/HW + w.y/HH)/2), y:Math.floor((w.y/HH - w.x/HW)/2), }; } panBy(dx,dy){ this.x+=dx/this.zoom; this.y+=dy/this.zoom; } zoomAt(sx,sy,factor){ const before=this.screenToWorld(sx,sy); this.zoom=WK.clamp(this.zoom*factor,0.45,2.4); const after=this.screenToWorld(sx,sy); this.x+=before.x-after.x; this.y+=before.y-after.y; } clampToWorld(world){ const maxX=((world.cols))*HW, maxY=((world.cols+world.rows))*HH; this.x=WK.clamp(this.x,-maxX,maxX); this.y=WK.clamp(this.y,-200,maxY+200); } isVisible(sx,sy,margin){ const m=(margin||40)+60; return sx>-m && sx-m && syr.id===opt.tintRegionId); if(reg){ ctx.fillStyle='rgba(120,220,140,.16)'; for(const k of reg.tiles){ const x=k%world.cols, y=(k/world.cols)|0; if(xmaxX||ymaxY) continue; const p=cam.project(x,y); WK.Sprites.diamondPath(ctx,p.x,p.y,0.5); ctx.fill(); } } } /* ---------- collect drawables (painter's algorithm) ---------- */ const draws=[]; const z=cam.zoom; const push=(d,fn)=>draws.push({d,fn}); for(let y=minY;y<=maxY;y++)for(let x=minX;x<=maxX;x++){ const k=world.idx(x,y); const p=cam.project(x,y); // fences — exact diamond-corner geometry: // W corner = C+(-HW,0) · S corner = C+(0,+HH) · E corner = C+(+HW,0) // so consecutive collinear edges share endpoints perfectly. const fs=world.fenceS[k], fe=world.fenceE[k]; if(fs){ push(x+y+0.32,()=>{ WK.Sprites.drawFenceEdge(ctx,p.x-HW*z,p.y,p.x,p.y+HH*z,fs,t); }); } if(fe){ push(x+y+0.34,()=>{ WK.Sprites.drawFenceEdge(ctx,p.x,p.y+HH*z,p.x+HW*z,p.y,fe,t); }); } // nature objects const oid=world.oid(x,y); if(oid){ const spr=WK.Sprites.getObject(oid,world.objVar[k]); const scale=(WK.Sprites.objectRealSize[oid]||1); push(x+y+0.5+(x-y)*0.001,()=>{ ctx.save(); ctx.translate(p.x,p.y); const sway=Math.sin(t*1.1+x*1.7+y)*0.02; if(scale!==1) ctx.scale(scale,scale); ctx.rotate(sway); ctx.drawImage(spr.cv,-spr.ax,-spr.ay); ctx.restore(); }); } // buildings const b=world.buildingAt(x,y); if(b && b.x===x && b.y===y){ // draw once at NW corner tile const spr=WK.Sprites.getBuilding(b.type); const def=D.BUILDING[b.type]; const cxT=b.x+def.w/2, cyT=b.y+def.h/2; const pc=cam.project(cxT,cyT); push(b.x+b.y+def.w+def.h-1.2,()=>{ ctx.drawImage(spr.cv,pc.x-spr.ax,pc.y-spr.ay); }); } } // entities for(const a of game.animals){ push(a.x+a.y+0.55,()=>a.draw(ctx,cam,t,game.world.gid(Math.round(a.x),Math.round(a.y)))); } for(const g of game.guests){ push(g.x+g.y+0.56,()=>g.draw(ctx,cam,t)); } for(const s of game.staff){ push(s.x+s.y+0.57,()=>s.draw(ctx,cam,t)); } draws.sort((A,B)=>A.d-B.d); for(const it of draws) it.fn(); // particles above world game.particles.draw(ctx,cam,t); // ghost preview & hover handled by input via callbacks if(opt.overlay) opt.overlay(ctx,cam,{minX,maxX,minY,maxY}); // selection highlight if(game.selected&&game.selected.alive!==false&&game.selected.kind==='animal'){ const s=game.selected; const p=cam.project(s.x,s.y); const rr=(18*s.size)*(1+Math.sin(t*5)*0.08); ctx.strokeStyle='rgba(255,201,60,.95)'; ctx.lineWidth=2.5; ctx.beginPath(); ctx.ellipse(p.x,p.y+1,rr,rr*0.42,0,0,TAU); ctx.stroke(); } // lamps glow + night tint const dark=1-dl; if(dark>0.05){ const lamps=this.lampsFor(world); ctx.save(); ctx.globalCompositeOperation='screen'; for(const [lx,ly] of lamps){ const p=cam.project(lx,ly); if(!cam.isVisible(p.x,p.y,80)) continue; const rr=46*cam.zoom; const gg=ctx.createRadialGradient(p.x,p.y-52*cam.zoom,2,p.x,p.y-52*cam.zoom,rr); gg.addColorStop(0,'rgba(255,220,130,'+(0.5*dark)+')'); gg.addColorStop(1,'rgba(255,220,130,0)'); ctx.fillStyle=gg; ctx.beginPath(); ctx.arc(p.x,p.y-52*cam.zoom,rr,0,TAU); ctx.fill(); } ctx.restore(); ctx.fillStyle='rgba(24,34,72,'+(dark*0.42)+')'; ctx.fillRect(0,0,this.cssW,this.cssH); } // gentle vignette const vg=ctx.createRadialGradient(this.cssW/2,this.cssH/2,Math.min(this.cssW,this.cssH)*0.62,this.cssW/2,this.cssH/2,Math.max(this.cssW,this.cssH)*0.78); vg.addColorStop(0,'rgba(30,25,10,0)'); vg.addColorStop(1,'rgba(30,25,10,.18)'); ctx.fillStyle=vg; ctx.fillRect(0,0,this.cssW,this.cssH); } daylight(minutes){ // 0 at midnight → 1 at noon, warm golden hours const m=minutes/1440; const sun=Math.sin((m-0.25)*TAU)*0.5+0.5; // peak at noon return Math.pow(WK.clamp(sun*1.15,0,1),0.8); } /* ---- ghost preview ---- */ drawGhost(ctx,cam,g){ if(!g||g.x==null) return; const ok=g.valid; const col= ok?'rgba(110,220,120,.85)':'rgba(230,80,70,.9)'; const fill= ok?'rgba(140,230,150,.28)':'rgba(230,90,80,.3)'; if(g.mode==='tile'){ const p=cam.project(g.x,g.y); WK.Sprites.diamondPath(ctx,p.x,p.y,1); ctx.fillStyle=fill; ctx.fill(); ctx.strokeStyle=col; ctx.lineWidth=2; ctx.stroke(); if(g.sprite){ ctx.save(); ctx.globalAlpha=0.75; ctx.drawImage(g.sprite.cv,p.x-g.sprite.ax,p.y-g.sprite.ay); ctx.restore(); } }else if(g.mode==='foot'){ const def=D.BUILDING[g.item]; for(let j=0;j