@aura3d/physics 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/dist/CharacterController.d.ts +52 -0
- package/dist/CharacterController.d.ts.map +1 -0
- package/dist/CharacterController.js +146 -0
- package/dist/CharacterController.js.map +1 -0
- package/dist/ClothFixtures.d.ts +87 -0
- package/dist/ClothFixtures.d.ts.map +1 -0
- package/dist/ClothFixtures.js +264 -0
- package/dist/ClothFixtures.js.map +1 -0
- package/dist/Collider.d.ts +27 -0
- package/dist/Collider.d.ts.map +1 -0
- package/dist/Collider.js +45 -0
- package/dist/Collider.js.map +1 -0
- package/dist/CollisionEvents.d.ts +26 -0
- package/dist/CollisionEvents.d.ts.map +1 -0
- package/dist/CollisionEvents.js +46 -0
- package/dist/CollisionEvents.js.map +1 -0
- package/dist/CollisionVolumes.d.ts +57 -0
- package/dist/CollisionVolumes.d.ts.map +1 -0
- package/dist/CollisionVolumes.js +159 -0
- package/dist/CollisionVolumes.js.map +1 -0
- package/dist/Constraint.d.ts +31 -0
- package/dist/Constraint.d.ts.map +1 -0
- package/dist/Constraint.js +103 -0
- package/dist/Constraint.js.map +1 -0
- package/dist/Constraints.d.ts +2 -0
- package/dist/Constraints.d.ts.map +1 -0
- package/dist/Constraints.js +2 -0
- package/dist/Constraints.js.map +1 -0
- package/dist/Crowd.d.ts +64 -0
- package/dist/Crowd.d.ts.map +1 -0
- package/dist/Crowd.js +209 -0
- package/dist/Crowd.js.map +1 -0
- package/dist/ECSPhysicsBridge.d.ts +17 -0
- package/dist/ECSPhysicsBridge.d.ts.map +1 -0
- package/dist/ECSPhysicsBridge.js +37 -0
- package/dist/ECSPhysicsBridge.js.map +1 -0
- package/dist/FireSmokeFixtures.d.ts +103 -0
- package/dist/FireSmokeFixtures.d.ts.map +1 -0
- package/dist/FireSmokeFixtures.js +271 -0
- package/dist/FireSmokeFixtures.js.map +1 -0
- package/dist/FluidFixtures.d.ts +75 -0
- package/dist/FluidFixtures.d.ts.map +1 -0
- package/dist/FluidFixtures.js +219 -0
- package/dist/FluidFixtures.js.map +1 -0
- package/dist/FractureFixtures.d.ts +68 -0
- package/dist/FractureFixtures.d.ts.map +1 -0
- package/dist/FractureFixtures.js +199 -0
- package/dist/FractureFixtures.js.map +1 -0
- package/dist/HitboxWorld.d.ts +250 -0
- package/dist/HitboxWorld.d.ts.map +1 -0
- package/dist/HitboxWorld.js +771 -0
- package/dist/HitboxWorld.js.map +1 -0
- package/dist/KinematicBody.d.ts +157 -0
- package/dist/KinematicBody.d.ts.map +1 -0
- package/dist/KinematicBody.js +405 -0
- package/dist/KinematicBody.js.map +1 -0
- package/dist/KinematicWorld.d.ts +58 -0
- package/dist/KinematicWorld.d.ts.map +1 -0
- package/dist/KinematicWorld.js +246 -0
- package/dist/KinematicWorld.js.map +1 -0
- package/dist/Navigation.d.ts +67 -0
- package/dist/Navigation.d.ts.map +1 -0
- package/dist/Navigation.js +261 -0
- package/dist/Navigation.js.map +1 -0
- package/dist/PhysicsDebugDraw.d.ts +11 -0
- package/dist/PhysicsDebugDraw.d.ts.map +1 -0
- package/dist/PhysicsDebugDraw.js +64 -0
- package/dist/PhysicsDebugDraw.js.map +1 -0
- package/dist/PhysicsSandboxFixtures.d.ts +53 -0
- package/dist/PhysicsSandboxFixtures.d.ts.map +1 -0
- package/dist/PhysicsSandboxFixtures.js +280 -0
- package/dist/PhysicsSandboxFixtures.js.map +1 -0
- package/dist/PhysicsStepper.d.ts +15 -0
- package/dist/PhysicsStepper.d.ts.map +1 -0
- package/dist/PhysicsStepper.js +37 -0
- package/dist/PhysicsStepper.js.map +1 -0
- package/dist/PhysicsWorld.d.ts +101 -0
- package/dist/PhysicsWorld.d.ts.map +1 -0
- package/dist/PhysicsWorld.js +868 -0
- package/dist/PhysicsWorld.js.map +1 -0
- package/dist/PlatformerFixtures.d.ts +70 -0
- package/dist/PlatformerFixtures.d.ts.map +1 -0
- package/dist/PlatformerFixtures.js +120 -0
- package/dist/PlatformerFixtures.js.map +1 -0
- package/dist/Raycast.d.ts +23 -0
- package/dist/Raycast.d.ts.map +1 -0
- package/dist/Raycast.js +271 -0
- package/dist/Raycast.js.map +1 -0
- package/dist/RigidBody.d.ts +69 -0
- package/dist/RigidBody.d.ts.map +1 -0
- package/dist/RigidBody.js +255 -0
- package/dist/RigidBody.js.map +1 -0
- package/dist/ScenePhysicsBridge.d.ts +42 -0
- package/dist/ScenePhysicsBridge.d.ts.map +1 -0
- package/dist/ScenePhysicsBridge.js +168 -0
- package/dist/ScenePhysicsBridge.js.map +1 -0
- package/dist/Shape.d.ts +50 -0
- package/dist/Shape.d.ts.map +1 -0
- package/dist/Shape.js +128 -0
- package/dist/Shape.js.map +1 -0
- package/dist/SoftBodyFixtures.d.ts +70 -0
- package/dist/SoftBodyFixtures.d.ts.map +1 -0
- package/dist/SoftBodyFixtures.js +272 -0
- package/dist/SoftBodyFixtures.js.map +1 -0
- package/dist/Steering.d.ts +161 -0
- package/dist/Steering.d.ts.map +1 -0
- package/dist/Steering.js +347 -0
- package/dist/Steering.js.map +1 -0
- package/dist/VehicleDynamics.d.ts +152 -0
- package/dist/VehicleDynamics.d.ts.map +1 -0
- package/dist/VehicleDynamics.js +335 -0
- package/dist/VehicleDynamics.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/package.json +17 -0
- package/src/CharacterController.ts +184 -0
- package/src/ClothFixtures.ts +359 -0
- package/src/Collider.ts +67 -0
- package/src/CollisionEvents.ts +70 -0
- package/src/CollisionVolumes.ts +218 -0
- package/src/Constraint.ts +120 -0
- package/src/Constraints.ts +1 -0
- package/src/Crowd.ts +283 -0
- package/src/ECSPhysicsBridge.ts +53 -0
- package/src/FireSmokeFixtures.ts +382 -0
- package/src/FluidFixtures.ts +301 -0
- package/src/FractureFixtures.ts +277 -0
- package/src/HitboxWorld.ts +1179 -0
- package/src/KinematicBody.ts +553 -0
- package/src/KinematicWorld.ts +310 -0
- package/src/Navigation.ts +321 -0
- package/src/PhysicsDebugDraw.ts +71 -0
- package/src/PhysicsSandboxFixtures.ts +394 -0
- package/src/PhysicsStepper.ts +47 -0
- package/src/PhysicsWorld.ts +986 -0
- package/src/PlatformerFixtures.ts +205 -0
- package/src/Raycast.ts +315 -0
- package/src/RigidBody.ts +311 -0
- package/src/ScenePhysicsBridge.ts +207 -0
- package/src/Shape.ts +182 -0
- package/src/SoftBodyFixtures.ts +363 -0
- package/src/Steering.ts +531 -0
- package/src/VehicleDynamics.ts +524 -0
- package/src/index.ts +28 -0
package/src/Steering.ts
ADDED
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
export type SteeringPoint = readonly [number, number];
|
|
2
|
+
export type SteeringVector = readonly [number, number];
|
|
3
|
+
|
|
4
|
+
export interface SteeringForce {
|
|
5
|
+
readonly force: SteeringVector;
|
|
6
|
+
readonly desiredVelocity: SteeringVector;
|
|
7
|
+
readonly distance: number;
|
|
8
|
+
readonly arrived: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface PredictiveSteeringForce extends SteeringForce {
|
|
12
|
+
readonly predictedTarget: SteeringPoint;
|
|
13
|
+
readonly predictionTime: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface WanderSteeringForce extends SteeringForce {
|
|
17
|
+
readonly wanderTarget: SteeringPoint;
|
|
18
|
+
readonly seed: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface FlockingNeighbor {
|
|
22
|
+
readonly position: SteeringPoint;
|
|
23
|
+
readonly velocity: SteeringVector;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface FlockingSteeringForce extends SteeringForce {
|
|
27
|
+
readonly separation: SteeringVector;
|
|
28
|
+
readonly alignment: SteeringVector;
|
|
29
|
+
readonly cohesion: SteeringVector;
|
|
30
|
+
readonly neighborCount: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface SteeringObstacle {
|
|
34
|
+
readonly position: SteeringPoint;
|
|
35
|
+
readonly radius: number;
|
|
36
|
+
readonly id?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ObstacleAvoidanceSteeringForce extends SteeringForce {
|
|
40
|
+
readonly obstacleDetected: boolean;
|
|
41
|
+
readonly obstacleId?: string;
|
|
42
|
+
readonly closestDistance: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface SteeringWall {
|
|
46
|
+
readonly start: SteeringPoint;
|
|
47
|
+
readonly end: SteeringPoint;
|
|
48
|
+
readonly normal: SteeringVector;
|
|
49
|
+
readonly id?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface WallAvoidanceSteeringForce extends SteeringForce {
|
|
53
|
+
readonly wallDetected: boolean;
|
|
54
|
+
readonly wallId?: string;
|
|
55
|
+
readonly hitDistance: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type SteeringBlendMode = "weighted-sum" | "priority";
|
|
59
|
+
|
|
60
|
+
export interface SteeringPipelineEntry {
|
|
61
|
+
readonly id: string;
|
|
62
|
+
readonly force: SteeringVector;
|
|
63
|
+
readonly weight?: number;
|
|
64
|
+
readonly priority?: number;
|
|
65
|
+
readonly enabled?: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface SteeringPipelineResult {
|
|
69
|
+
readonly force: SteeringVector;
|
|
70
|
+
readonly selectedIds: readonly string[];
|
|
71
|
+
readonly mode: SteeringBlendMode;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface SteeringAgentOptions {
|
|
75
|
+
readonly position: SteeringPoint;
|
|
76
|
+
readonly velocity?: SteeringVector;
|
|
77
|
+
readonly maxSpeed?: number;
|
|
78
|
+
readonly maxForce?: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface SteeringAgentSnapshot {
|
|
82
|
+
readonly position: SteeringPoint;
|
|
83
|
+
readonly velocity: SteeringVector;
|
|
84
|
+
readonly speed: number;
|
|
85
|
+
readonly distanceTraveled: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function seekSteering(position: SteeringPoint, velocity: SteeringVector, target: SteeringPoint, maxSpeed: number): SteeringForce {
|
|
89
|
+
const toTarget = subtract(target, position);
|
|
90
|
+
const distance = length(toTarget);
|
|
91
|
+
if (distance === 0) {
|
|
92
|
+
return { force: [-velocity[0], -velocity[1]], desiredVelocity: [0, 0], distance: 0, arrived: true };
|
|
93
|
+
}
|
|
94
|
+
const desiredVelocity = scale(toTarget, maxSpeed / distance);
|
|
95
|
+
return {
|
|
96
|
+
force: subtract(desiredVelocity, velocity),
|
|
97
|
+
desiredVelocity,
|
|
98
|
+
distance: round3(distance),
|
|
99
|
+
arrived: false
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function arriveSteering(options: {
|
|
104
|
+
readonly position: SteeringPoint;
|
|
105
|
+
readonly velocity: SteeringVector;
|
|
106
|
+
readonly target: SteeringPoint;
|
|
107
|
+
readonly maxSpeed: number;
|
|
108
|
+
readonly slowingRadius: number;
|
|
109
|
+
readonly tolerance?: number;
|
|
110
|
+
}): SteeringForce {
|
|
111
|
+
const tolerance = options.tolerance ?? 0.02;
|
|
112
|
+
const toTarget = subtract(options.target, options.position);
|
|
113
|
+
const distance = length(toTarget);
|
|
114
|
+
if (distance <= tolerance) {
|
|
115
|
+
return {
|
|
116
|
+
force: [-options.velocity[0], -options.velocity[1]],
|
|
117
|
+
desiredVelocity: [0, 0],
|
|
118
|
+
distance: round3(distance),
|
|
119
|
+
arrived: true
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const desiredSpeed = distance > options.slowingRadius
|
|
123
|
+
? options.maxSpeed
|
|
124
|
+
: options.maxSpeed * (distance / Math.max(options.slowingRadius, tolerance));
|
|
125
|
+
const desiredVelocity = scale(toTarget, desiredSpeed / distance);
|
|
126
|
+
return {
|
|
127
|
+
force: subtract(desiredVelocity, options.velocity),
|
|
128
|
+
desiredVelocity,
|
|
129
|
+
distance: round3(distance),
|
|
130
|
+
arrived: false
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function fleeSteering(options: {
|
|
135
|
+
readonly position: SteeringPoint;
|
|
136
|
+
readonly velocity: SteeringVector;
|
|
137
|
+
readonly threat: SteeringPoint;
|
|
138
|
+
readonly maxSpeed: number;
|
|
139
|
+
readonly panicDistance?: number;
|
|
140
|
+
}): SteeringForce {
|
|
141
|
+
const panicDistance = options.panicDistance ?? Number.POSITIVE_INFINITY;
|
|
142
|
+
const away = subtract(options.position, options.threat);
|
|
143
|
+
const distance = length(away);
|
|
144
|
+
if (distance > panicDistance || distance === 0) {
|
|
145
|
+
return { force: [0, 0], desiredVelocity: [0, 0], distance: round3(distance), arrived: distance > panicDistance };
|
|
146
|
+
}
|
|
147
|
+
const desiredVelocity = scale(away, options.maxSpeed / distance);
|
|
148
|
+
return {
|
|
149
|
+
force: subtract(desiredVelocity, options.velocity),
|
|
150
|
+
desiredVelocity,
|
|
151
|
+
distance: round3(distance),
|
|
152
|
+
arrived: false
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function pursuitSteering(options: {
|
|
157
|
+
readonly position: SteeringPoint;
|
|
158
|
+
readonly velocity: SteeringVector;
|
|
159
|
+
readonly targetPosition: SteeringPoint;
|
|
160
|
+
readonly targetVelocity: SteeringVector;
|
|
161
|
+
readonly maxSpeed: number;
|
|
162
|
+
readonly maxPredictionTime?: number;
|
|
163
|
+
}): PredictiveSteeringForce {
|
|
164
|
+
const prediction = predictedPoint(options.position, options.velocity, options.targetPosition, options.targetVelocity, options.maxPredictionTime ?? 1);
|
|
165
|
+
const steering = seekSteering(options.position, options.velocity, prediction.point, options.maxSpeed);
|
|
166
|
+
return { ...steering, predictedTarget: prediction.point, predictionTime: prediction.time };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function evadeSteering(options: {
|
|
170
|
+
readonly position: SteeringPoint;
|
|
171
|
+
readonly velocity: SteeringVector;
|
|
172
|
+
readonly threatPosition: SteeringPoint;
|
|
173
|
+
readonly threatVelocity: SteeringVector;
|
|
174
|
+
readonly maxSpeed: number;
|
|
175
|
+
readonly maxPredictionTime?: number;
|
|
176
|
+
readonly panicDistance?: number;
|
|
177
|
+
}): PredictiveSteeringForce {
|
|
178
|
+
const prediction = predictedPoint(options.position, options.velocity, options.threatPosition, options.threatVelocity, options.maxPredictionTime ?? 1);
|
|
179
|
+
const steering = fleeSteering({
|
|
180
|
+
position: options.position,
|
|
181
|
+
velocity: options.velocity,
|
|
182
|
+
threat: prediction.point,
|
|
183
|
+
maxSpeed: options.maxSpeed,
|
|
184
|
+
panicDistance: options.panicDistance
|
|
185
|
+
});
|
|
186
|
+
return { ...steering, predictedTarget: prediction.point, predictionTime: prediction.time };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function wanderSteering(options: {
|
|
190
|
+
readonly position: SteeringPoint;
|
|
191
|
+
readonly velocity: SteeringVector;
|
|
192
|
+
readonly maxSpeed: number;
|
|
193
|
+
readonly seed: number;
|
|
194
|
+
readonly radius?: number;
|
|
195
|
+
readonly distance?: number;
|
|
196
|
+
readonly jitter?: number;
|
|
197
|
+
}): WanderSteeringForce {
|
|
198
|
+
const radius = options.radius ?? 0.35;
|
|
199
|
+
const forward = length(options.velocity) > 0.001 ? normalize(options.velocity) : [1, 0] as const;
|
|
200
|
+
const circleCenter = [options.position[0] + forward[0] * (options.distance ?? 0.55), options.position[1] + forward[1] * (options.distance ?? 0.55)] as const;
|
|
201
|
+
const angle = seededUnit(options.seed) * Math.PI * 2;
|
|
202
|
+
const jitter = (options.jitter ?? 0.12) * (seededUnit(options.seed + 1) * 2 - 1);
|
|
203
|
+
const wanderTarget = [
|
|
204
|
+
circleCenter[0] + Math.cos(angle + jitter) * radius,
|
|
205
|
+
circleCenter[1] + Math.sin(angle + jitter) * radius
|
|
206
|
+
] as const;
|
|
207
|
+
const steering = seekSteering(options.position, options.velocity, wanderTarget, options.maxSpeed);
|
|
208
|
+
return { ...steering, wanderTarget: [round3(wanderTarget[0]), round3(wanderTarget[1])], seed: options.seed };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function flockingSteering(options: {
|
|
212
|
+
readonly position: SteeringPoint;
|
|
213
|
+
readonly velocity: SteeringVector;
|
|
214
|
+
readonly neighbors: readonly FlockingNeighbor[];
|
|
215
|
+
readonly maxSpeed: number;
|
|
216
|
+
readonly separationRadius?: number;
|
|
217
|
+
readonly alignmentRadius?: number;
|
|
218
|
+
readonly cohesionRadius?: number;
|
|
219
|
+
readonly separationWeight?: number;
|
|
220
|
+
readonly alignmentWeight?: number;
|
|
221
|
+
readonly cohesionWeight?: number;
|
|
222
|
+
}): FlockingSteeringForce {
|
|
223
|
+
const separation = separationSteering(options.position, options.neighbors, options.maxSpeed, options.separationRadius ?? 0.24);
|
|
224
|
+
const alignment = alignmentSteering(options.position, options.velocity, options.neighbors, options.maxSpeed, options.alignmentRadius ?? 0.45);
|
|
225
|
+
const cohesion = cohesionSteering(options.position, options.velocity, options.neighbors, options.maxSpeed, options.cohesionRadius ?? 0.7);
|
|
226
|
+
const force = add(
|
|
227
|
+
scale(separation, options.separationWeight ?? 1.7),
|
|
228
|
+
add(scale(alignment, options.alignmentWeight ?? 0.8), scale(cohesion, options.cohesionWeight ?? 0.9))
|
|
229
|
+
);
|
|
230
|
+
return {
|
|
231
|
+
force: roundVector(force),
|
|
232
|
+
desiredVelocity: roundVector(add(options.velocity, force)),
|
|
233
|
+
distance: round3(options.neighbors.length > 0 ? averageNeighborDistance(options.position, options.neighbors) : 0),
|
|
234
|
+
arrived: options.neighbors.length === 0,
|
|
235
|
+
separation: roundVector(separation),
|
|
236
|
+
alignment: roundVector(alignment),
|
|
237
|
+
cohesion: roundVector(cohesion),
|
|
238
|
+
neighborCount: options.neighbors.length
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function obstacleAvoidanceSteering(options: {
|
|
243
|
+
readonly position: SteeringPoint;
|
|
244
|
+
readonly velocity: SteeringVector;
|
|
245
|
+
readonly obstacles: readonly SteeringObstacle[];
|
|
246
|
+
readonly maxSpeed: number;
|
|
247
|
+
readonly detectionDistance?: number;
|
|
248
|
+
readonly agentRadius?: number;
|
|
249
|
+
readonly avoidanceForce?: number;
|
|
250
|
+
}): ObstacleAvoidanceSteeringForce {
|
|
251
|
+
const speed = length(options.velocity);
|
|
252
|
+
if (speed <= 0.001 || options.obstacles.length === 0) {
|
|
253
|
+
return { force: [0, 0], desiredVelocity: [0, 0], distance: 0, arrived: true, obstacleDetected: false, closestDistance: 0 };
|
|
254
|
+
}
|
|
255
|
+
const forward = normalize(options.velocity);
|
|
256
|
+
const right = perpendicular(forward);
|
|
257
|
+
const detectionDistance = (options.detectionDistance ?? 0.8) * Math.min(1, speed / options.maxSpeed);
|
|
258
|
+
const agentRadius = options.agentRadius ?? 0.08;
|
|
259
|
+
let closest: { readonly obstacle: SteeringObstacle; readonly forwardDistance: number; readonly lateralDistance: number } | undefined;
|
|
260
|
+
for (const obstacle of options.obstacles) {
|
|
261
|
+
const offset = subtract(obstacle.position, options.position);
|
|
262
|
+
const forwardDistance = dot(offset, forward);
|
|
263
|
+
if (forwardDistance < 0 || forwardDistance > detectionDistance) continue;
|
|
264
|
+
const lateralDistance = dot(offset, right);
|
|
265
|
+
const expandedRadius = obstacle.radius + agentRadius;
|
|
266
|
+
if (Math.abs(lateralDistance) > expandedRadius) continue;
|
|
267
|
+
if (!closest || forwardDistance < closest.forwardDistance) {
|
|
268
|
+
closest = { obstacle, forwardDistance, lateralDistance };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (!closest) {
|
|
272
|
+
return { force: [0, 0], desiredVelocity: [0, 0], distance: round3(detectionDistance), arrived: true, obstacleDetected: false, closestDistance: round3(detectionDistance) };
|
|
273
|
+
}
|
|
274
|
+
const expandedRadius = closest.obstacle.radius + agentRadius;
|
|
275
|
+
const lateralSign = closest.lateralDistance >= 0 ? -1 : 1;
|
|
276
|
+
const lateralStrength = (expandedRadius - Math.abs(closest.lateralDistance)) / Math.max(expandedRadius, 0.001);
|
|
277
|
+
const brakeStrength = 1 - closest.forwardDistance / Math.max(detectionDistance, 0.001);
|
|
278
|
+
const force = add(
|
|
279
|
+
scale(right, lateralSign * lateralStrength * (options.avoidanceForce ?? options.maxSpeed)),
|
|
280
|
+
scale(forward, -brakeStrength * (options.avoidanceForce ?? options.maxSpeed) * 0.35)
|
|
281
|
+
);
|
|
282
|
+
return {
|
|
283
|
+
force: roundVector(force),
|
|
284
|
+
desiredVelocity: roundVector(add(options.velocity, force)),
|
|
285
|
+
distance: round3(closest.forwardDistance),
|
|
286
|
+
arrived: false,
|
|
287
|
+
obstacleDetected: true,
|
|
288
|
+
obstacleId: closest.obstacle.id,
|
|
289
|
+
closestDistance: round3(closest.forwardDistance)
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function wallAvoidanceSteering(options: {
|
|
294
|
+
readonly position: SteeringPoint;
|
|
295
|
+
readonly velocity: SteeringVector;
|
|
296
|
+
readonly walls: readonly SteeringWall[];
|
|
297
|
+
readonly maxSpeed: number;
|
|
298
|
+
readonly whiskerLength?: number;
|
|
299
|
+
readonly whiskerSpread?: number;
|
|
300
|
+
readonly avoidanceForce?: number;
|
|
301
|
+
}): WallAvoidanceSteeringForce {
|
|
302
|
+
if (length(options.velocity) <= 0.001 || options.walls.length === 0) {
|
|
303
|
+
return { force: [0, 0], desiredVelocity: [0, 0], distance: 0, arrived: true, wallDetected: false, hitDistance: 0 };
|
|
304
|
+
}
|
|
305
|
+
const forward = normalize(options.velocity);
|
|
306
|
+
const whiskerLength = options.whiskerLength ?? 0.8;
|
|
307
|
+
const spread = options.whiskerSpread ?? Math.PI / 5;
|
|
308
|
+
const whiskers = [
|
|
309
|
+
{ direction: forward, lengthScale: 1 },
|
|
310
|
+
{ direction: rotate(forward, spread), lengthScale: 0.78 },
|
|
311
|
+
{ direction: rotate(forward, -spread), lengthScale: 0.78 }
|
|
312
|
+
] as const;
|
|
313
|
+
let closest: { readonly wall: SteeringWall; readonly distance: number; readonly normal: SteeringVector; readonly length: number } | undefined;
|
|
314
|
+
for (const whisker of whiskers) {
|
|
315
|
+
const end = add(options.position, scale(whisker.direction, whiskerLength * whisker.lengthScale));
|
|
316
|
+
for (const wall of options.walls) {
|
|
317
|
+
const hit = segmentIntersection(options.position, end, wall.start, wall.end);
|
|
318
|
+
if (!hit) continue;
|
|
319
|
+
const distance = length(subtract(hit, options.position));
|
|
320
|
+
if (!closest || distance < closest.distance) {
|
|
321
|
+
closest = { wall, distance, normal: normalize(wall.normal), length: whiskerLength * whisker.lengthScale };
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (!closest) {
|
|
326
|
+
return { force: [0, 0], desiredVelocity: [0, 0], distance: round3(whiskerLength), arrived: true, wallDetected: false, hitDistance: round3(whiskerLength) };
|
|
327
|
+
}
|
|
328
|
+
const strength = 1 - closest.distance / Math.max(closest.length, 0.001);
|
|
329
|
+
const force = scale(closest.normal, strength * (options.avoidanceForce ?? options.maxSpeed));
|
|
330
|
+
return {
|
|
331
|
+
force: roundVector(force),
|
|
332
|
+
desiredVelocity: roundVector(add(options.velocity, force)),
|
|
333
|
+
distance: round3(closest.distance),
|
|
334
|
+
arrived: false,
|
|
335
|
+
wallDetected: true,
|
|
336
|
+
wallId: closest.wall.id,
|
|
337
|
+
hitDistance: round3(closest.distance)
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export function blendSteeringForces(options: {
|
|
342
|
+
readonly entries: readonly SteeringPipelineEntry[];
|
|
343
|
+
readonly mode?: SteeringBlendMode;
|
|
344
|
+
readonly maxForce?: number;
|
|
345
|
+
readonly priorityThreshold?: number;
|
|
346
|
+
}): SteeringPipelineResult {
|
|
347
|
+
const mode = options.mode ?? "weighted-sum";
|
|
348
|
+
const enabled = options.entries.filter((entry) => entry.enabled !== false);
|
|
349
|
+
if (mode === "priority") {
|
|
350
|
+
const threshold = options.priorityThreshold ?? 0.001;
|
|
351
|
+
const sorted = [...enabled].sort((left, right) => (right.priority ?? 0) - (left.priority ?? 0));
|
|
352
|
+
const selected = sorted.find((entry) => length(scale(entry.force, entry.weight ?? 1)) > threshold);
|
|
353
|
+
const force = selected ? clampMagnitude(scale(selected.force, selected.weight ?? 1), options.maxForce ?? Number.POSITIVE_INFINITY) : [0, 0] as const;
|
|
354
|
+
return { force: roundVector(force), selectedIds: selected ? [selected.id] : [], mode };
|
|
355
|
+
}
|
|
356
|
+
const force = enabled.reduce<SteeringVector>((sum, entry) => add(sum, scale(entry.force, entry.weight ?? 1)), [0, 0]);
|
|
357
|
+
return {
|
|
358
|
+
force: roundVector(clampMagnitude(force, options.maxForce ?? Number.POSITIVE_INFINITY)),
|
|
359
|
+
selectedIds: enabled.map((entry) => entry.id),
|
|
360
|
+
mode
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export class SteeringAgent {
|
|
365
|
+
private position: SteeringPoint;
|
|
366
|
+
private velocity: SteeringVector;
|
|
367
|
+
private distanceTraveled = 0;
|
|
368
|
+
private readonly maxSpeed: number;
|
|
369
|
+
private readonly maxForce: number;
|
|
370
|
+
|
|
371
|
+
constructor(options: SteeringAgentOptions) {
|
|
372
|
+
this.position = options.position;
|
|
373
|
+
this.velocity = options.velocity ?? [0, 0];
|
|
374
|
+
this.maxSpeed = options.maxSpeed ?? 1;
|
|
375
|
+
this.maxForce = options.maxForce ?? 1;
|
|
376
|
+
if (!Number.isFinite(this.maxSpeed) || this.maxSpeed <= 0 || !Number.isFinite(this.maxForce) || this.maxForce <= 0) {
|
|
377
|
+
throw new RangeError("SteeringAgent maxSpeed and maxForce must be finite positive numbers.");
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
apply(force: SteeringVector, deltaSeconds: number): SteeringAgentSnapshot {
|
|
382
|
+
const dt = Math.max(0, deltaSeconds);
|
|
383
|
+
const clampedForce = clampMagnitude(force, this.maxForce);
|
|
384
|
+
this.velocity = clampMagnitude([
|
|
385
|
+
this.velocity[0] + clampedForce[0] * dt,
|
|
386
|
+
this.velocity[1] + clampedForce[1] * dt
|
|
387
|
+
], this.maxSpeed);
|
|
388
|
+
const step: SteeringVector = [this.velocity[0] * dt, this.velocity[1] * dt];
|
|
389
|
+
this.position = [this.position[0] + step[0], this.position[1] + step[1]];
|
|
390
|
+
this.distanceTraveled += length(step);
|
|
391
|
+
return this.snapshot();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
snapshot(): SteeringAgentSnapshot {
|
|
395
|
+
return {
|
|
396
|
+
position: [round3(this.position[0]), round3(this.position[1])],
|
|
397
|
+
velocity: [round3(this.velocity[0]), round3(this.velocity[1])],
|
|
398
|
+
speed: round3(length(this.velocity)),
|
|
399
|
+
distanceTraveled: round3(this.distanceTraveled)
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function subtract(left: SteeringPoint, right: SteeringPoint): SteeringVector {
|
|
405
|
+
return [left[0] - right[0], left[1] - right[1]];
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function add(left: SteeringVector, right: SteeringVector): SteeringVector {
|
|
409
|
+
return [left[0] + right[0], left[1] + right[1]];
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function scale(value: SteeringVector, scalar: number): SteeringVector {
|
|
413
|
+
return [value[0] * scalar, value[1] * scalar];
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function dot(left: SteeringVector, right: SteeringVector): number {
|
|
417
|
+
return left[0] * right[0] + left[1] * right[1];
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function length(value: SteeringVector): number {
|
|
421
|
+
return Math.hypot(value[0], value[1]);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function normalize(value: SteeringVector): SteeringVector {
|
|
425
|
+
const magnitude = length(value);
|
|
426
|
+
return magnitude === 0 ? [0, 0] : [value[0] / magnitude, value[1] / magnitude];
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function clampMagnitude(value: SteeringVector, maxMagnitude: number): SteeringVector {
|
|
430
|
+
const magnitude = length(value);
|
|
431
|
+
if (magnitude <= maxMagnitude || magnitude === 0) return value;
|
|
432
|
+
return scale(value, maxMagnitude / magnitude);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function perpendicular(value: SteeringVector): SteeringVector {
|
|
436
|
+
return [-value[1], value[0]];
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function rotate(value: SteeringVector, radians: number): SteeringVector {
|
|
440
|
+
const cos = Math.cos(radians);
|
|
441
|
+
const sin = Math.sin(radians);
|
|
442
|
+
return [value[0] * cos - value[1] * sin, value[0] * sin + value[1] * cos];
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function roundVector(value: SteeringVector): SteeringVector {
|
|
446
|
+
return [round3(value[0]), round3(value[1])];
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function separationSteering(position: SteeringPoint, neighbors: readonly FlockingNeighbor[], maxSpeed: number, radius: number): SteeringVector {
|
|
450
|
+
let force: SteeringVector = [0, 0];
|
|
451
|
+
let count = 0;
|
|
452
|
+
for (const neighbor of neighbors) {
|
|
453
|
+
const away = subtract(position, neighbor.position);
|
|
454
|
+
const distance = length(away);
|
|
455
|
+
if (distance > 0.001 && distance <= radius) {
|
|
456
|
+
force = add(force, scale(normalize(away), 1 / distance));
|
|
457
|
+
count += 1;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return count > 0 ? scale(normalize(scale(force, 1 / count)), maxSpeed) : [0, 0];
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function alignmentSteering(position: SteeringPoint, velocity: SteeringVector, neighbors: readonly FlockingNeighbor[], maxSpeed: number, radius: number): SteeringVector {
|
|
464
|
+
let average: SteeringVector = [0, 0];
|
|
465
|
+
let count = 0;
|
|
466
|
+
for (const neighbor of neighbors) {
|
|
467
|
+
if (length(subtract(neighbor.position, position)) <= radius) {
|
|
468
|
+
average = add(average, neighbor.velocity);
|
|
469
|
+
count += 1;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (count === 0 || length(average) === 0) return [0, 0];
|
|
473
|
+
return subtract(scale(normalize(scale(average, 1 / count)), maxSpeed), velocity);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function cohesionSteering(position: SteeringPoint, velocity: SteeringVector, neighbors: readonly FlockingNeighbor[], maxSpeed: number, radius: number): SteeringVector {
|
|
477
|
+
let center: SteeringVector = [0, 0];
|
|
478
|
+
let count = 0;
|
|
479
|
+
for (const neighbor of neighbors) {
|
|
480
|
+
const toNeighbor = subtract(neighbor.position, position);
|
|
481
|
+
if (length(toNeighbor) <= radius) {
|
|
482
|
+
center = add(center, neighbor.position);
|
|
483
|
+
count += 1;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
if (count === 0) return [0, 0];
|
|
487
|
+
const target = scale(center, 1 / count);
|
|
488
|
+
return seekSteering(position, velocity, target, maxSpeed).force;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function averageNeighborDistance(position: SteeringPoint, neighbors: readonly FlockingNeighbor[]): number {
|
|
492
|
+
return neighbors.reduce((sum, neighbor) => sum + length(subtract(neighbor.position, position)), 0) / Math.max(1, neighbors.length);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function segmentIntersection(a: SteeringPoint, b: SteeringPoint, c: SteeringPoint, d: SteeringPoint): SteeringPoint | null {
|
|
496
|
+
const r = subtract(b, a);
|
|
497
|
+
const s = subtract(d, c);
|
|
498
|
+
const denominator = cross(r, s);
|
|
499
|
+
if (Math.abs(denominator) < 0.000001) return null;
|
|
500
|
+
const cMinusA = subtract(c, a);
|
|
501
|
+
const t = cross(cMinusA, s) / denominator;
|
|
502
|
+
const u = cross(cMinusA, r) / denominator;
|
|
503
|
+
if (t < 0 || t > 1 || u < 0 || u > 1) return null;
|
|
504
|
+
return [a[0] + r[0] * t, a[1] + r[1] * t];
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function cross(left: SteeringVector, right: SteeringVector): number {
|
|
508
|
+
return left[0] * right[1] - left[1] * right[0];
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function predictedPoint(position: SteeringPoint, velocity: SteeringVector, targetPosition: SteeringPoint, targetVelocity: SteeringVector, maxPredictionTime: number): { readonly point: SteeringPoint; readonly time: number } {
|
|
512
|
+
const toTarget = subtract(targetPosition, position);
|
|
513
|
+
const distance = length(toTarget);
|
|
514
|
+
const relativeSpeed = length(velocity) + length(targetVelocity);
|
|
515
|
+
const predictionTime = Math.min(maxPredictionTime, relativeSpeed > 0.001 ? distance / relativeSpeed : maxPredictionTime);
|
|
516
|
+
return {
|
|
517
|
+
point: [round3(targetPosition[0] + targetVelocity[0] * predictionTime), round3(targetPosition[1] + targetVelocity[1] * predictionTime)],
|
|
518
|
+
time: round3(predictionTime)
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function seededUnit(seed: number): number {
|
|
523
|
+
let value = (Math.trunc(seed) >>> 0) + 0x6d2b79f5;
|
|
524
|
+
value = Math.imul(value ^ (value >>> 15), value | 1);
|
|
525
|
+
value ^= value + Math.imul(value ^ (value >>> 7), value | 61);
|
|
526
|
+
return ((value ^ (value >>> 14)) >>> 0) / 4_294_967_296;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function round3(value: number): number {
|
|
530
|
+
return Number(value.toFixed(3));
|
|
531
|
+
}
|