function update(dt){ const vision=val("vision"); const speed=val("speed"); const maxForce=.11; const old=birds.map(b=>({...b})); birds.forEach((b,i)=>{ if(!visible[b.type])return; let sx=0,sy=0; let ax=0,ay=0; let cx=0,cy=0; let same=0; const own=species[b.type]; const range=vision*own.vision; for(let j=0;j0){ // 分離 let s=limit(sx,sy,speed); fx+=s[0]*val("sep")*.10; fy+=s[1]*val("sep")*.10; // 整列:同じ色だけ let a=limit(ax/same,ay/same,speed); fx+=(a[0]-b.vx)*val("ali")*.075; fy+=(a[1]-b.vy)*val("ali")*.075; // 結合:同じ色だけ let c=limit(cx/same,cy/same,speed); fx+=(c[0]-b.vx)*val("coh")*.12; fy+=(c[1]-b.vy)*val("coh")*.12; } // エサを探す let nearest=null,nearestD=Infinity; for(const f of foods){ const [dx,dy]=wrapDistance(f.x-b.x,f.y-b.y); const d=Math.hypot(dx,dy); if(d1){ fx+=nearest.dx/nearestD*val("food")*.055*own.food; fy+=nearest.dy/nearestD*val("food")*.055*own.food; } // マウス誘導 if(mouseMode && mouse.active && val("mousePower")>0){ const [dx,dy]=wrapDistance(mouse.x-b.x,mouse.y-b.y); const d=Math.hypot(dx,dy); if(d<200 && d>1){ fx+=dx/d*val("mousePower")*.06; fy+=dy/d*val("mousePower")*.06; } } // 滑らかな方向転換 const f=limit(fx,fy,maxForce); b.vx+=f[0]*dt*60; b.vy+=f[1]*dt*60; const v=limit(b.vx,b.vy,speed*own.speed); b.vx=v[0]; b.vy=v[1]; const len=Math.hypot(b.vx,b.vy); if(len>.01){ const newAngle=Math.atan2(b.vy,b.vx); let diff=Math.atan2( Math.sin(newAngle-b.angle), Math.cos(newAngle-b.angle) ); // 大きな方向転換では古い軌跡を消す if(Math.abs(diff)>2.0){ b.trail=[]; } b.angle=newAngle; } b.x=(b.x+b.vx*dt*60+W)%W; b.y=(b.y+b.vy*dt*60+H)%H; // 軌跡 if(trailMode && val("trail")>0){ b.trail.push({x:b.x,y:b.y}); while(b.trail.length>val("trail"))b.trail.shift(); }else{ b.trail=[]; } }); // エサの消費 for(let i=foods.length-1;i>=0;i--){ const f=foods[i]; let eaten=false; for(const b of birds){ if(!visible[b.type])continue; const [dx,dy]=wrapDistance(f.x-b.x,f.y-b.y); if(Math.hypot(dx,dy)<9){ eaten=true; break; } } if(eaten)foods.splice(i,1); } // エサの再出現 const target=val("foodNum"); if(foods.length