import { Canvas } from "@/canvas"; import { clamp } from "@/common/functions"; import { Vec } from "@/common/vec"; import { desiredDensity, particleSize } from "@/game/fluids/fluids"; import { Swarm } from "@/game/swarm/swarm"; import { fillCircle, strokeLine } from "@/graphics"; const padding = 50; const allowedSteer = 2 / 180; const idlingUrgency = 0.001; const idlingDistance = 20; const desiredDistanceToEveryone = 30; const flockPerceptionDistance = 150; const behaveLikeFlockDoesUrgency = 0.8; const epsilon = 1e-5; export type NavigationType = 'stayInBounds' | 'doNotBumpIntoFlock' | 'keepFlockClose' | 'behaveLikeFlockDoes' | 'idling'; export type NavigatingDesire = { pos: Vec; urgency: number; // [0,1] type: NavigationType; } const navigationColorMap = { stayInBounds: 'red', doNotBumpIntoFlock: 'blue', keepFlockClose: 'green', behaveLikeFlockDoes: 'yellow', idling: 'purple' }; export class Agent { dir: Vec; pos: Vec; swarm: Swarm; idlingGoal: Vec|null = null; lastNavigation: NavigatingDesire; constructor(pos: Vec, swarm: Swarm) { this.pos = pos; this.swarm = swarm; this.lastNavigation = { pos: pos, urgency: 0, type: 'idling' } this.dir = Vec.randomUnit(); } update(canvas: Canvas, delta: DOMHighResTimeStamp) { const nextPos = this.pos.add(this.dir); const navigations = []; /**if (nextPos.y >= canvas.height - padding || nextPos.y < padding) { this.vel.y *= -1; nextPos.y = clamp(nextPos.y, padding, canvas.height - padding); } if (nextPos.x >= canvas.width - padding || nextPos.x < padding) { this.vel.x *= -1; nextPos.x = clamp(nextPos.x, padding, canvas.width - padding); }*/ navigations.push(this.stayInBounds(nextPos)); navigations.push(this.doNotBumpIntoFlock(nextPos)); navigations.push(this.keepFlockClose(nextPos)); navigations.push(this.behaveLikeFlockDoes(nextPos)); navigations.push(this.idling(nextPos)); const navigation = this.mostUrgentNavigation(navigations); this.steerTowards(navigation.pos); this.lastNavigation = navigation; this.pos = nextPos; } steerTowards(target: Vec) { const targetDir = target.sub(this.pos); // no division by zero! if (targetDir.length() < epsilon) { return; } const alpha = this.dir.clockwiseAngleBetween(targetDir.normalize()); this.dir = this.dir.rotate(Math.min(allowedSteer, Math.abs(alpha)) * Math.sign(alpha)).normalize(); } mostUrgentNavigation(navigations: NavigatingDesire[]) { return navigations .reduce((a, b) => a.urgency > b.urgency ? a : b); } stayInBounds(nextPos: Vec): NavigatingDesire { const {rect} = this.swarm; if (rect.contains(this.pos.add(this.dir.scale(padding)))) { return { pos: nextPos, urgency: 0, type: 'stayInBounds' }; } return { pos: rect.center, urgency: 1, type: 'stayInBounds' }; } doNotBumpIntoFlock(nextPos: Vec): NavigatingDesire { const {agents} = this.swarm; const visibleAgentsNotMe = agents .filter(a => a.pos.distance(this.pos) < flockPerceptionDistance && a !== this); const visibleAgentsInBounds = visibleAgentsNotMe .filter(a => this.swarm.rect.contains(a.pos)); let closestAgent: Agent|null = null; for (const a of visibleAgentsInBounds) { if (!closestAgent || a.pos.distance(this.pos) < closestAgent.pos.distance(this.pos)) { closestAgent = a; } } if (!closestAgent) { return { pos: nextPos, urgency: 0, type: 'doNotBumpIntoFlock' }; } const awayFromClosestAgentDir = this.pos.sub(closestAgent.pos).normalize(); return { pos: this.pos.add(awayFromClosestAgentDir), urgency: (desiredDistanceToEveryone - closestAgent.pos.distance(this.pos)) / desiredDistanceToEveryone, type: 'doNotBumpIntoFlock' }; } keepFlockClose(nextPos: Vec): NavigatingDesire { const {agents} = this.swarm; const visibleAgents = agents .filter(a => a.pos.distance(this.pos) < flockPerceptionDistance); const visibleAgentsInBounds = visibleAgents .filter(a => this.swarm.rect.contains(a.pos)); const center = visibleAgentsInBounds .reduce((sum, a) => sum.add(a.pos), new Vec) .scale(1 / visibleAgentsInBounds.length); const distanceToCenter = this.pos.distance(center); return { pos: center, urgency: distanceToCenter / flockPerceptionDistance, type: 'keepFlockClose' }; } behaveLikeFlockDoes(nextPos: Vec): NavigatingDesire { const {agents} = this.swarm; const visibleAgents = agents .filter(a => a.pos.distance(this.pos) < flockPerceptionDistance); const visibleAgentsInBounds = visibleAgents .filter(a => this.swarm.rect.contains(a.pos)); const averageDir = visibleAgentsInBounds .reduce((sum, a) => sum.add(a.dir), new Vec) .scale(1 / visibleAgentsInBounds.length); const angleDiff = this.dir.clockwiseAngleBetween(averageDir); return { pos: nextPos.add(averageDir), urgency: behaveLikeFlockDoesUrgency * Math.abs(angleDiff) / Math.PI, type: 'behaveLikeFlockDoes' }; } idling(nextPos: Vec): NavigatingDesire { if (this.idlingGoal && this.idlingGoal.distance(nextPos) > idlingDistance) { return { pos: this.idlingGoal, urgency: idlingUrgency, type: 'idling' }; } this.idlingGoal = Vec.random(this.swarm.rect); return { pos: this.idlingGoal, urgency: idlingUrgency, type: 'idling' } } /** * @returns {Number} between 0 and 255 */ satisfaction(): number { return 255 - this.lastNavigation.urgency * 255; } /**render({ctx}: Canvas) { const satisfaction = this.satisfaction(); const dissatisfaction = 255 - satisfaction; fillCircle(ctx, this.pos, particleSize, `rgb(${dissatisfaction}, 0, ${satisfaction})`); strokeLine(ctx, this.pos, this.pos.add(this.dir.scale(30))); }*/ render({ctx}: Canvas) { const color = navigationColorMap[this.lastNavigation.type]; fillCircle(ctx, this.pos, particleSize, color); strokeLine(ctx, this.pos, this.pos.add(this.dir.scale(30))); } }