food_chain/scene.js

47 lines
7.2 KiB
JavaScript
Raw Normal View History

2026-09-29 19:10:03 +09:00
import {waterPath,shoreRadius,withinWater} from './water.js';
import {W,H} from './engine.js';
import {bodyPath} from './body-shapes.js';
const BIN=128,NY=Math.ceil(H/BIN),EMPTY=[];
let indexedTrees=null,treeBins=null;
let indexedWater=null,coastline=null;
function outerCoast(waters,editing=false){if(!editing&&indexedWater===waters)return coastline;if(!editing)indexedWater=waters;const lines=[];for(const [i,w] of waters.entries()){let px=0,py=0;for(let k=0;k<=72;k++){const angle=k*Math.PI/36,r=shoreRadius(w,angle),x=w.x+Math.cos(angle)*r,y=w.y+Math.sin(angle)*r;if(k&& !waters.some((other,j)=>j!==i&&withinWater(other,(px+x)/2,(py+y)/2)))lines.push([px,py,x,y]);px=x;py=y}}if(!editing)coastline=lines;return lines}
function nearbyTrees(trees,x,y){
if(indexedTrees!==trees){indexedTrees=trees;treeBins=new Map();for(const t of trees){const r=t.r*(.32+.68*Math.sqrt(Math.max(0,t.leaves/t.maxLeaves)))+16;
for(let i=Math.max(0,Math.floor((t.x-r)/BIN));i<=Math.floor((t.x+r)/BIN);i++)for(let j=Math.max(0,Math.floor((t.y-r)/BIN));j<=Math.floor((t.y+r)/BIN);j++){
const key=i*NY+j,b=treeBins.get(key);if(b)b.push(t);else treeBins.set(key,[t]);
}
}}return treeBins.get(Math.floor(x/BIN)*NY+Math.floor(y/BIN))||EMPTY;
}
export function drawScene(ctx,{state,view,zoom=1,panX=0,panY=0,terrain,tool='observe',drag=null,water=[],treeSprites,selected=null,hover=null,waterMode='add',layer='plants'}){
const shapeTool=()=>tool==='water'||tool==='rocks';
const d=view.d,scale=view.base*zoom,t={s:scale,x:(view.width-W*scale)/2+panX,y:(view.height-H*scale)/2+panY};ctx.setTransform(d,0,0,d,0,0);ctx.fillStyle='#172723';ctx.fillRect(0,0,view.width,view.height);ctx.translate(t.x,t.y);ctx.scale(t.s,t.s);ctx.imageSmoothingEnabled=true;ctx.drawImage(terrain,0,0,W,H);
const waters=tool==='water'&&drag?.water!==undefined?water:state.water;ctx.save();ctx.beginPath();ctx.rect(0,0,W,H);ctx.clip();ctx.beginPath();for(const w of waters)waterPath(ctx,w,true);ctx.fillStyle='#315b608c';ctx.fill();ctx.beginPath();for(const [x,y,xx,yy] of outerCoast(waters,tool==='water'&&drag?.water!==undefined)){ctx.moveTo(x,y);ctx.lineTo(xx,yy)}ctx.strokeStyle='#a7baa063';ctx.lineWidth=8;ctx.stroke();const rocks=tool==='rocks'&&drag?.water!==undefined?water:state.rocks;
for(const rock of rocks){ctx.fillStyle='#6c7472';ctx.strokeStyle='#a1aaa0';ctx.lineWidth=1/t.s;ctx.beginPath();ctx.arc(rock.x,rock.y,rock.r,0,Math.PI*2);ctx.fill();ctx.stroke();ctx.fillStyle='#ffffff0b';ctx.beginPath();ctx.arc(rock.x-rock.r*.16,rock.y-rock.r*.18,rock.r*.64,0,Math.PI*2);ctx.fill()}
if(shapeTool()){ctx.lineWidth=1/t.s;ctx.setLineDash([5/t.s,5/t.s]);for(const w of tool==='rocks'?rocks:waters){ctx.strokeStyle='#bfded3aa';if(tool==='water')waterPath(ctx,w);else{ctx.beginPath();ctx.arc(w.x,w.y,w.r,0,Math.PI*2)}ctx.stroke();if(waterMode==='edit'){ctx.fillStyle='#d5e697';ctx.fillRect(w.x-3/t.s,w.y-3/t.s,6/t.s,6/t.s)}}ctx.setLineDash([])}
for(const log of state.fallen||[]){ctx.save();ctx.translate(log.x,log.y);ctx.rotate(log.angle||0);const w=log.w,h=log.h;ctx.fillStyle='#17201880';ctx.fillRect(-w/2+3,-h/2+4,w,h);const bark=ctx.createLinearGradient(0,-h/2,0,h/2);bark.addColorStop(0,'#a48655');bark.addColorStop(.22,'#705331');bark.addColorStop(.65,'#64462a');bark.addColorStop(1,'#33291e');ctx.fillStyle=bark;ctx.strokeStyle='#291f18';ctx.lineWidth=Math.min(h/2,Math.max(.7,1/t.s));ctx.fillRect(-w/2,-h/2,w,h);const edge=ctx.lineWidth;ctx.strokeRect(-w/2+edge/2,-h/2+edge/2,w-edge,h-edge);for(let j=0;j<7;j++){const y=-h*.36+j*h*.12,shift=Math.sin(log.x*.13+log.y*.17+j*3.1)*h*.035;ctx.strokeStyle=j%2?'#b4945b80':'#2d261e95';ctx.lineWidth=Math.max(.45,.7/t.s);ctx.beginPath();ctx.moveTo(-w*.46,y);ctx.bezierCurveTo(-w*.2,y+shift,w*.17,y-shift,w*.45,y+shift*.5);ctx.stroke()}for(const end of [-1,1]){const x=end*(w/2-h*.17);ctx.fillStyle=end<0?'#a98c5e':'#92734c';ctx.beginPath();ctx.ellipse(x,0,h*.12,h*.42,0,0,Math.PI*2);ctx.fill();ctx.strokeStyle='#52402b';for(let j=0;j<3;j++){ctx.beginPath();ctx.ellipse(x,0,h*(.04+j*.023),h*(.13+j*.1),0,0,Math.PI*2);ctx.stroke()}}ctx.restore()}
for(const tree of state.trees){const radius=tree.r*(.32+.68*Math.sqrt(Math.max(0,tree.leaves/tree.maxLeaves)));ctx.globalAlpha=.5+.5*tree.leaves/tree.maxLeaves;ctx.drawImage(treeSprites[tree.variant],tree.x-radius,tree.y-radius,radius*2,radius*2);ctx.globalAlpha=1;ctx.fillStyle='#57442b';ctx.strokeStyle='#ad9768';ctx.lineWidth=.8/t.s;ctx.beginPath();ctx.arc(tree.x,tree.y,tree.trunk,0,Math.PI*2);ctx.fill();ctx.stroke()}
ctx.fillStyle='#958670aa';for(const c of state.carcasses){const radius=2.3*Math.sqrt(Math.max(0,c.remainingBiomass));if(radius*t.s<2){const dot=Math.max(radius*1.5,1.5/t.s);ctx.fillRect(c.x-dot/2,c.y-dot/2,dot,dot)}else{ctx.beginPath();ctx.arc(c.x,c.y,radius,0,6.29);ctx.fill()}}
const species=new Map(state.stats.species.map(s=>[s.id,s])),left=-t.x/t.s,right=(view.width-t.x)/t.s,top=-t.y/t.s,bottom=(view.height-t.y)/t.s,noCull=left<=0&&top<=0&&right>=W&&bottom>=H;
const packed=state.animalData,count=packed?state.animalIds.length:state.animals.length;
for(let i=0;i<count;i++){
const a=packed?null:state.animals[i],j=i*7,id=packed?state.animalIds[i]:a.id,sid=packed?state.animalSids[i]:a.sid,ax=packed?packed[j]:a.x,ay=packed?packed[j+1]:a.y,ar=packed?packed[j+2]:a.r,vx=packed?packed[j+3]:a.vx,vy=packed?packed[j+4]:a.vy,role=packed?packed[j+5]:a.role,affinity=packed?packed[j+6]:a.affinity;
if(!noCull){const extent=Math.max(ar*1.65+1/t.s,ar+8/t.s);if(ax+extent<left||ax-extent>right||ay+extent<top||ay-extent>bottom)continue}
const sp=species.get(sid),col=layer==='trophic'?(role<.5?'#a7d46d':role<1.5?'#e9c976':role<2.5?'#d88979':'#aa8bc7'):layer==='habitat'?(affinity<1/3?'#d5bc88':affinity<2/3?'#86c0a1':'#78b8da'):sp?.color||'#ddd',morph=sp?.morph||0,angle=Math.atan2(vy,vx),r=Math.max(ar,1.9/t.s);
if(r*t.s<2){let covered=false;for(const tree of nearbyTrees(state.trees,ax,ay)){const radius=tree.r*(.32+.68*Math.sqrt(Math.max(0,tree.leaves/tree.maxLeaves)));if((ax-tree.x)**2+(ay-tree.y)**2<radius*radius){covered=true;break}}
ctx.globalAlpha=covered?.66:1;ctx.fillStyle=col;const dot=Math.max(ar*1.6,3/t.s);ctx.fillRect(ax-dot/2,ay-dot/2,dot,dot);ctx.globalAlpha=1;
if(id===selected){ctx.strokeStyle='#f4f3cc';ctx.lineWidth=1.3/t.s;ctx.beginPath();ctx.arc(ax,ay,ar+6/t.s,0,6.29);ctx.stroke()}continue}
ctx.save();ctx.translate(ax,ay);ctx.rotate(angle);ctx.fillStyle=col;bodyPath(ctx,r,morph);ctx.fill();
if(zoom>1.7||ar>7){ctx.strokeStyle=col;ctx.lineWidth=1/t.s;ctx.beginPath();ctx.moveTo(-ar,0);ctx.lineTo(-ar*1.5,0);ctx.stroke()}ctx.restore();
for(const tree of nearbyTrees(state.trees,ax,ay)){
const radius=tree.r*(.32+.68*Math.sqrt(Math.max(0,tree.leaves/tree.maxLeaves)));
if((ax-tree.x)**2+(ay-tree.y)**2>(radius+r*1.13)**2)continue;
ctx.save();ctx.translate(ax,ay);ctx.rotate(angle);bodyPath(ctx,r,morph);ctx.clip();ctx.rotate(-angle);ctx.translate(-ax,-ay);
ctx.globalAlpha=.35+.4*tree.leaves/tree.maxLeaves;ctx.drawImage(treeSprites[tree.variant],tree.x-radius,tree.y-radius,radius*2,radius*2);ctx.restore();
}
if(id===selected){ctx.strokeStyle='#f4f3cc';ctx.lineWidth=1.3/t.s;ctx.beginPath();ctx.arc(ax,ay,ar+6/t.s,0,6.29);ctx.stroke()}
}
if(tool==='introduce'&&hover){ctx.strokeStyle='#d5e697';ctx.lineWidth=1/t.s;ctx.beginPath();ctx.arc(hover.x,hover.y,45,0,6.29);ctx.stroke()}ctx.restore();return t;
}