196 lines
6.8 KiB
TypeScript
196 lines
6.8 KiB
TypeScript
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)));
|
|
}
|
|
}
|