@aethervtt/shard-nav 0.0.1

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/dist/agents.js ADDED
@@ -0,0 +1,703 @@
1
+ import { defineSystem } from "@aethervtt/shard-core";
2
+ import { CharacterController, CharacterIntent, Physics, Velocity } from "@aethervtt/shard-physics";
3
+ import { FixedTime } from "@aethervtt/shard-runtime";
4
+ import { GlobalTransform, Transform } from "@aethervtt/shard-transform";
5
+ import { NavAgent, NavAgentState, NavAreas, NavArrived, NavUnreachable } from "./components.js";
6
+ import { traceSegment } from "./grid.js";
7
+ import { createNavPath, findPath, meshPathLocal } from "./query.js";
8
+ import { Nav } from "./state.js";
9
+ const IDLE = 0;
10
+ const MOVING = 1;
11
+ const ARRIVED = 2;
12
+ const UNREACHABLE = 3;
13
+ const DRIVE_CHARACTER = 0;
14
+ const DRIVE_TRANSFORM = 1;
15
+ const DRIVE_VELOCITY = 2;
16
+ const ANTICIPATE_TURNS = 1;
17
+ const OBSTACLE_AVOIDANCE = 2;
18
+ const OPTIMIZE_VIS = 8;
19
+ const OPTIMIZE_TOPO = 16;
20
+ const CROWD_OFFMESH = 2;
21
+ const STRAIGHTPATH_END = 2;
22
+ const CORNERS = 128;
23
+ function columns(table, out) {
24
+ out.destination = table.column(NavAgent, "destination");
25
+ out.target = table.column(NavAgent, "target");
26
+ out.speed = table.column(NavAgent, "speed");
27
+ out.acceleration = table.column(NavAgent, "acceleration");
28
+ out.radius = table.column(NavAgent, "radius");
29
+ out.stopping = table.column(NavAgent, "stoppingDistance");
30
+ out.avoidance = table.column(NavAgent, "avoidance");
31
+ out.repath = table.column(NavAgent, "repathInterval");
32
+ out.drive = table.column(NavAgent, "drive");
33
+ out.stopped = table.column(NavAgent, "stopped");
34
+ out.nav = table.column(NavAgent, "nav");
35
+ out.agentTicks = table.changedTicks(NavAgent);
36
+ out.status = table.column(NavAgentState, "status");
37
+ out.remaining = table.column(NavAgentState, "remaining");
38
+ out.corners = table.column(NavAgentState, "corners");
39
+ out.velocity = table.column(NavAgentState, "velocity");
40
+ out.translation = table.column(Transform, "translation");
41
+ out.rotation = table.column(Transform, "rotation");
42
+ out.global = table.column(GlobalTransform, "matrix");
43
+ }
44
+ function newRecord(entity) {
45
+ return {
46
+ entity,
47
+ nav: -1,
48
+ kind: "none",
49
+ crowdIndex: -1,
50
+ corners: new Float32Array(CORNERS * 3),
51
+ count: 0,
52
+ next: 1,
53
+ goal: new Float64Array(3),
54
+ navVersion: -1,
55
+ sinceRepath: 0,
56
+ reachable: true,
57
+ status: IDLE,
58
+ agentTick: 0,
59
+ drive: DRIVE_TRANSFORM,
60
+ framed: false,
61
+ lpos: new Float64Array(3),
62
+ link: { active: false, lift: 0, x0: 0, z0: 0, y0: 0, x1: 0, z1: 0, y1: 0, arc: 0 }
63
+ };
64
+ }
65
+ const pos = new Float64Array(3);
66
+ const goal = new Float64Array(3);
67
+ const seg = new Float64Array(4);
68
+ const vec = { x: 0, y: 0, z: 0 };
69
+ const navAgents = defineSystem({
70
+ name: "nav/agents",
71
+ description: "Steers NavAgents along paths on grids (seek, arrive, separation) and navmeshes (Detour crowd with avoidance), repaths for moving targets, writes CharacterIntent / Velocity / Transform and NavAgentState, sends NavArrived and NavUnreachable.",
72
+ setup: (world) => ({
73
+ query: world.query({ with: [NavAgent, NavAgentState, Transform] }),
74
+ cols: {},
75
+ path: createNavPath(CORNERS),
76
+ find: { out: void 0, nav: null },
77
+ // Grid separation: agent positions and radii this step, bucketed in a spatial hash.
78
+ gx: new Float64Array(64),
79
+ gy: new Float64Array(64),
80
+ gr: new Float64Array(64),
81
+ gnav: new Float64Array(64),
82
+ heads: new Int32Array(1024),
83
+ next: new Int32Array(64),
84
+ gridCount: 0,
85
+ cellSize: 1
86
+ }),
87
+ run: (s, world, ctx) => {
88
+ const nav = world.tryResource(Nav);
89
+ if (!nav) return;
90
+ const dt = world.resource(FixedTime).step;
91
+ const since = ctx.lastRunTick;
92
+ const physicsDim = world.tryResource(Physics)?.dim ?? 3;
93
+ buildHash(s, nav);
94
+ let crowds = false;
95
+ for (let t = 0; t < s.query.tables.length; t++) {
96
+ const table = s.query.tables[t];
97
+ columns(table, s.cols);
98
+ for (let row = 0; row < table.count; row++) {
99
+ if (stepAgent(s, world, nav, table, row, dt, since, physicsDim)) crowds = true;
100
+ }
101
+ table.markChanged(NavAgentState);
102
+ }
103
+ if (!crowds) return;
104
+ for (let i = 0; i < nav.meshList.length; i++) nav.meshList[i].crowd?.update(dt);
105
+ for (let t = 0; t < s.query.tables.length; t++) {
106
+ const table = s.query.tables[t];
107
+ columns(table, s.cols);
108
+ for (let row = 0; row < table.count; row++)
109
+ readCrowd(s, world, nav, table, row, dt, physicsDim);
110
+ }
111
+ }
112
+ });
113
+ function buildHash(s, nav) {
114
+ let n = 0;
115
+ let maxR = 0.1;
116
+ for (let t = 0; t < s.query.tables.length; t++) {
117
+ const table = s.query.tables[t];
118
+ const tr = table.column(Transform, "translation");
119
+ const radius = table.column(NavAgent, "radius");
120
+ for (let row = 0; row < table.count; row++) {
121
+ const rec = nav.agents.get(table.entities[row]);
122
+ if (rec?.kind !== "grid") continue;
123
+ if (n === s.gx.length) {
124
+ s.gx = grow(s.gx);
125
+ s.gy = grow(s.gy);
126
+ s.gr = grow(s.gr);
127
+ s.gnav = grow(s.gnav);
128
+ const next = new Int32Array(s.next.length * 2);
129
+ next.set(s.next);
130
+ s.next = next;
131
+ }
132
+ s.gx[n] = tr[row * 3];
133
+ s.gy[n] = tr[row * 3 + 1];
134
+ s.gr[n] = radius[row];
135
+ s.gnav[n] = rec.nav;
136
+ if (radius[row] > maxR) maxR = radius[row];
137
+ n++;
138
+ }
139
+ }
140
+ s.gridCount = n;
141
+ s.cellSize = maxR * 4;
142
+ s.heads.fill(-1);
143
+ for (let i = 0; i < n; i++) {
144
+ const h = hashCell(Math.floor(s.gx[i] / s.cellSize), Math.floor(s.gy[i] / s.cellSize));
145
+ s.next[i] = s.heads[h];
146
+ s.heads[h] = i;
147
+ }
148
+ }
149
+ function grow(a) {
150
+ const b = new Float64Array(a.length * 2);
151
+ b.set(a);
152
+ return b;
153
+ }
154
+ const hashCell = (x, y) => (Math.imul(x, 73856093) ^ Math.imul(y, 19349663)) & 1023;
155
+ const framePos = new Float64Array(3);
156
+ function locate(nav, preferred, x, y, z, wx, wy, wz) {
157
+ if (preferred >= 0) return nav.grids.has(preferred) || nav.meshes.has(preferred) ? preferred : -1;
158
+ for (let i = 0; i < nav.gridList.length; i++) {
159
+ const g = nav.gridList[i];
160
+ if (!g.grid) continue;
161
+ const cx = (x - g.ox) / g.csx;
162
+ const cy = (y - g.oy) / g.csy;
163
+ if (cx >= 0 && cy >= 0 && cx < g.grid.width && cy < g.grid.height) return g.entity;
164
+ }
165
+ for (let i = 0; i < nav.meshList.length; i++) {
166
+ const m = nav.meshList[i];
167
+ const pad = m.settings.agentRadius + m.settings.cellSize * 2;
168
+ let px = x;
169
+ let py = y;
170
+ let pz = z;
171
+ if (m.frame !== null) {
172
+ m.pointIn(wx, wy, wz, framePos);
173
+ px = framePos[0];
174
+ py = framePos[1];
175
+ pz = framePos[2];
176
+ }
177
+ if (px >= m.min[0] - pad && px <= m.max[0] + pad && pz >= m.min[2] - pad && pz <= m.max[2] + pad && py >= m.min[1] - m.settings.agentHeight && py <= m.max[1] + m.settings.agentHeight)
178
+ return m.entity;
179
+ }
180
+ return -1;
181
+ }
182
+ function removeFromCrowd(nav, rec) {
183
+ if (rec.crowdIndex >= 0) {
184
+ const rt = nav.meshes.get(rec.nav);
185
+ rt?.crowd?.removeAgent(rec.crowdIndex);
186
+ rec.crowdIndex = -1;
187
+ }
188
+ }
189
+ function stepAgent(s, world, nav, table, row, dt, since, dim) {
190
+ const c = s.cols;
191
+ const entity = table.entities[row];
192
+ let rec = nav.agents.get(entity);
193
+ if (!rec) {
194
+ rec = newRecord(entity);
195
+ nav.agents.set(entity, rec);
196
+ }
197
+ const o3 = row * 3;
198
+ pos[0] = c.translation[o3];
199
+ pos[1] = c.translation[o3 + 1];
200
+ pos[2] = c.translation[o3 + 2];
201
+ const drive = c.drive[row];
202
+ rec.drive = drive === DRIVE_CHARACTER && !table.has(CharacterController) ? DRIVE_TRANSFORM : drive === DRIVE_VELOCITY && !table.has(Velocity) ? DRIVE_TRANSFORM : drive;
203
+ const g12 = row * 12;
204
+ const navEntity = locate(
205
+ nav,
206
+ c.nav[row],
207
+ pos[0],
208
+ pos[1],
209
+ pos[2],
210
+ c.global[g12 + 3],
211
+ c.global[g12 + 7],
212
+ c.global[g12 + 11]
213
+ );
214
+ if (navEntity !== rec.nav) {
215
+ removeFromCrowd(nav, rec);
216
+ rec.nav = navEntity;
217
+ rec.kind = navEntity < 0 ? "none" : nav.grids.has(navEntity) ? "grid" : "mesh";
218
+ rec.navVersion = -1;
219
+ rec.count = 0;
220
+ }
221
+ if (rec.kind === "none" || c.stopped[row] === 1) {
222
+ removeFromCrowd(nav, rec);
223
+ halt(s, world, table, row, rec, IDLE, dim);
224
+ return false;
225
+ }
226
+ const framedMesh = rec.kind === "mesh" ? nav.meshes.get(rec.nav) : void 0;
227
+ rec.framed = framedMesh !== void 0 && framedMesh.frame !== null;
228
+ if (rec.framed) {
229
+ framedMesh.pointIn(c.global[g12 + 3], c.global[g12 + 7], c.global[g12 + 11], pos);
230
+ rec.lpos[0] = pos[0];
231
+ rec.lpos[1] = pos[1];
232
+ rec.lpos[2] = pos[2];
233
+ }
234
+ const target = c.target[row];
235
+ if (target >= 0 && world.isAlive(target) && world.has(target, GlobalTransform)) {
236
+ const tt = world.entityTable(target);
237
+ const tm = tt.column(GlobalTransform, "matrix");
238
+ const tr = world.entityRow(target);
239
+ goal[0] = tm[tr * 12 + 3];
240
+ goal[1] = tm[tr * 12 + 7];
241
+ goal[2] = tm[tr * 12 + 11];
242
+ } else {
243
+ goal[0] = c.destination[o3];
244
+ goal[1] = c.destination[o3 + 1];
245
+ goal[2] = c.destination[o3 + 2];
246
+ }
247
+ if (rec.framed) framedMesh.pointIn(goal[0], goal[1], goal[2], goal);
248
+ rec.sinceRepath += dt;
249
+ const grid = rec.kind === "grid" ? nav.grids.get(rec.nav) : void 0;
250
+ const mesh = rec.kind === "mesh" ? nav.meshes.get(rec.nav) : void 0;
251
+ const version = grid ? grid.version : mesh.version;
252
+ const cell = grid ? Math.max(grid.csx, grid.csy) : Math.max(c.radius[row], mesh.settings.cellSize);
253
+ const gdx = goal[0] - rec.goal[0];
254
+ const gdy = goal[1] - rec.goal[1];
255
+ const gdz = goal[2] - rec.goal[2];
256
+ const moved = Math.sqrt(gdx * gdx + gdy * gdy + gdz * gdz);
257
+ const edited = c.agentTicks[row] > since && c.agentTicks[row] !== rec.agentTick;
258
+ const repath = rec.navVersion !== version || edited || moved > cell || target >= 0 && moved > 1e-4 && rec.sinceRepath >= c.repath[row];
259
+ if (repath) {
260
+ rec.agentTick = c.agentTicks[row];
261
+ rec.goal[0] = goal[0];
262
+ rec.goal[1] = goal[1];
263
+ rec.goal[2] = goal[2];
264
+ rec.navVersion = version;
265
+ rec.sinceRepath = 0;
266
+ plan(s, world, nav, rec);
267
+ if (rec.status === ARRIVED || rec.status === IDLE) rec.status = MOVING;
268
+ }
269
+ if (grid) {
270
+ steerGrid(s, world, table, row, rec, grid, dt, dim);
271
+ return false;
272
+ }
273
+ return requestCrowd(s, world, nav, table, row, rec, mesh, repath, dim);
274
+ }
275
+ function plan(s, world, nav, rec) {
276
+ const path = s.path;
277
+ if (rec.kind === "grid" && !nav.grids.get(rec.nav)?.grid) {
278
+ rec.count = 0;
279
+ return;
280
+ }
281
+ s.find.out = path;
282
+ s.find.nav = rec.nav;
283
+ const mesh = rec.kind === "mesh" ? nav.meshes.get(rec.nav) : void 0;
284
+ if (mesh && rec.framed) meshPathLocal(mesh, pos, rec.goal, path);
285
+ else findPath(world, pos, rec.goal, s.find);
286
+ rec.corners.set(path.corners.subarray(0, path.count * 3));
287
+ rec.count = path.count;
288
+ rec.next = 1;
289
+ rec.reachable = path.status === "complete";
290
+ }
291
+ function halt(s, world, table, row, rec, status, dim) {
292
+ const c = s.cols;
293
+ c.velocity[row * 3] = 0;
294
+ c.velocity[row * 3 + 1] = 0;
295
+ c.velocity[row * 3 + 2] = 0;
296
+ applyDrive(table, row, rec, 0, 0, 0, 0, dim);
297
+ if (status !== ARRIVED) c.remaining[row] = 0;
298
+ c.corners[row] = 0;
299
+ setStatus(world, s, row, rec, status);
300
+ }
301
+ function setStatus(world, s, row, rec, status) {
302
+ s.cols.status[row] = status;
303
+ if (status === rec.status) return;
304
+ rec.status = status;
305
+ if (status === ARRIVED) world.send(NavArrived, { entity: rec.entity });
306
+ else if (status === UNREACHABLE) world.send(NavUnreachable, { entity: rec.entity });
307
+ }
308
+ const worldVel = new Float64Array(3);
309
+ function applyDrive(table, row, rec, vx, vy, vz, dt, dim, mesh) {
310
+ if (rec.framed && mesh) {
311
+ mesh.vectorOut(vx, vy, vz, worldVel);
312
+ driveFramed(table, row, rec, worldVel[0], worldVel[1], worldVel[2], dt);
313
+ return;
314
+ }
315
+ if (rec.drive === DRIVE_CHARACTER) {
316
+ const move = table.column(CharacterIntent, "move");
317
+ if (dim === 2) {
318
+ move[row * 3] = vx;
319
+ move[row * 3 + 1] = 0;
320
+ move[row * 3 + 2] = 0;
321
+ } else {
322
+ const q = table.column(Transform, "rotation");
323
+ const qx = -q[row * 4];
324
+ const qy = -q[row * 4 + 1];
325
+ const qz = -q[row * 4 + 2];
326
+ const qw = q[row * 4 + 3];
327
+ const tx = 2 * (qy * vz - qz * vy);
328
+ const ty = 2 * (qz * vx - qx * vz);
329
+ const tz = 2 * (qx * vy - qy * vx);
330
+ move[row * 3] = vx + qw * tx + (qy * tz - qz * ty);
331
+ move[row * 3 + 1] = 0;
332
+ move[row * 3 + 2] = vz + qw * tz + (qx * ty - qy * tx);
333
+ }
334
+ table.markChanged(CharacterIntent, row);
335
+ return;
336
+ }
337
+ if (rec.drive === DRIVE_VELOCITY) {
338
+ const lin = table.column(Velocity, "linear");
339
+ lin[row * 3] = vx;
340
+ if (dim === 2 || rec.kind === "grid") lin[row * 3 + 1] = vy;
341
+ else lin[row * 3 + 2] = vz;
342
+ table.markChanged(Velocity, row);
343
+ return;
344
+ }
345
+ if (dt > 0) {
346
+ const tr = table.column(Transform, "translation");
347
+ tr[row * 3] = tr[row * 3] + vx * dt;
348
+ tr[row * 3 + 1] = tr[row * 3 + 1] + vy * dt;
349
+ tr[row * 3 + 2] = tr[row * 3 + 2] + vz * dt;
350
+ table.markChanged(Transform, row);
351
+ }
352
+ }
353
+ function driveFramed(table, row, rec, vx, vy, vz, dt) {
354
+ const m = table.column(GlobalTransform, "matrix");
355
+ const o = row * 12;
356
+ const ax = axis(m, o, 0);
357
+ const ay = axis(m, o, 1);
358
+ const az = axis(m, o, 2);
359
+ const lx = (vx * m[o] + vy * m[o + 4] + vz * m[o + 8]) / ax;
360
+ const ly = (vx * m[o + 1] + vy * m[o + 5] + vz * m[o + 9]) / ay;
361
+ const lz = (vx * m[o + 2] + vy * m[o + 6] + vz * m[o + 10]) / az;
362
+ if (rec.drive === DRIVE_CHARACTER) {
363
+ const move = table.column(CharacterIntent, "move");
364
+ move[row * 3] = lx;
365
+ move[row * 3 + 1] = 0;
366
+ move[row * 3 + 2] = lz;
367
+ table.markChanged(CharacterIntent, row);
368
+ return;
369
+ }
370
+ if (rec.drive === DRIVE_VELOCITY) {
371
+ const lin = table.column(Velocity, "linear");
372
+ lin[row * 3] = vx;
373
+ lin[row * 3 + 1] = vy;
374
+ lin[row * 3 + 2] = vz;
375
+ table.markChanged(Velocity, row);
376
+ return;
377
+ }
378
+ if (dt > 0) {
379
+ const q = table.column(Transform, "rotation");
380
+ const qx = q[row * 4];
381
+ const qy = q[row * 4 + 1];
382
+ const qz = q[row * 4 + 2];
383
+ const qw = q[row * 4 + 3];
384
+ const tx = 2 * (qy * lz - qz * ly);
385
+ const ty = 2 * (qz * lx - qx * lz);
386
+ const tz = 2 * (qx * ly - qy * lx);
387
+ const tr = table.column(Transform, "translation");
388
+ tr[row * 3] = tr[row * 3] + (lx + qw * tx + (qy * tz - qz * ty)) * dt;
389
+ tr[row * 3 + 1] = tr[row * 3 + 1] + (ly + qw * ty + (qz * tx - qx * tz)) * dt;
390
+ tr[row * 3 + 2] = tr[row * 3 + 2] + (lz + qw * tz + (qx * ty - qy * tx)) * dt;
391
+ table.markChanged(Transform, row);
392
+ }
393
+ }
394
+ function axis(m, o, c) {
395
+ return Math.sqrt(m[o + c] ** 2 + m[o + 4 + c] ** 2 + m[o + 8 + c] ** 2) || 1;
396
+ }
397
+ function steerGrid(s, world, table, row, rec, g, dt, dim) {
398
+ const c = s.cols;
399
+ const o3 = row * 3;
400
+ const speed = c.speed[row];
401
+ const accel = c.acceleration[row];
402
+ const radius = c.radius[row];
403
+ const stopping = c.stopping[row];
404
+ const corners = rec.corners;
405
+ if (rec.count === 0) {
406
+ halt(s, world, table, row, rec, UNREACHABLE, dim);
407
+ return;
408
+ }
409
+ const reach = Math.max(stopping, radius * 0.5, 1e-3);
410
+ while (rec.next < rec.count - 1) {
411
+ const dx = corners[rec.next * 3] - pos[0];
412
+ const dy = corners[rec.next * 3 + 1] - pos[1];
413
+ if (dx * dx + dy * dy > reach * reach) break;
414
+ rec.next++;
415
+ }
416
+ if (rec.next >= rec.count) rec.next = rec.count - 1;
417
+ let remaining = 0;
418
+ {
419
+ let px = pos[0];
420
+ let py = pos[1];
421
+ for (let i = rec.next; i < rec.count; i++) {
422
+ const dx = corners[i * 3] - px;
423
+ const dy = corners[i * 3 + 1] - py;
424
+ remaining += Math.sqrt(dx * dx + dy * dy);
425
+ px = corners[i * 3];
426
+ py = corners[i * 3 + 1];
427
+ }
428
+ }
429
+ c.remaining[row] = remaining;
430
+ c.corners[row] = rec.count - rec.next;
431
+ const end = remaining <= stopping;
432
+ if (end && rec.next === rec.count - 1) {
433
+ halt(s, world, table, row, rec, rec.reachable ? ARRIVED : UNREACHABLE, dim);
434
+ c.remaining[row] = remaining;
435
+ return;
436
+ }
437
+ const tx = corners[rec.next * 3] - pos[0];
438
+ const ty = corners[rec.next * 3 + 1] - pos[1];
439
+ const d = Math.sqrt(tx * tx + ty * ty) || 1;
440
+ let want = speed;
441
+ const brake = Math.sqrt(2 * accel * Math.max(0, remaining - stopping * 0.5));
442
+ if (brake < want) want = brake;
443
+ let dvx = tx / d * want;
444
+ let dvy = ty / d * want;
445
+ const cs = s.cellSize;
446
+ const hx = Math.floor(pos[0] / cs);
447
+ const hy = Math.floor(pos[1] / cs);
448
+ for (let oy = -1; oy <= 1; oy++) {
449
+ for (let ox = -1; ox <= 1; ox++) {
450
+ for (let j = s.heads[hashCell(hx + ox, hy + oy)]; j !== -1; j = s.next[j]) {
451
+ if (s.gnav[j] !== rec.nav) continue;
452
+ const ax = pos[0] - s.gx[j];
453
+ const ay = pos[1] - s.gy[j];
454
+ const min = radius + s.gr[j];
455
+ const dd = ax * ax + ay * ay;
456
+ if (dd >= min * min || dd < 1e-12) continue;
457
+ const dist = Math.sqrt(dd);
458
+ const push = (min - dist) / min * speed * 2;
459
+ dvx += ax / dist * push;
460
+ dvy += ay / dist * push;
461
+ }
462
+ }
463
+ }
464
+ const dl = Math.sqrt(dvx * dvx + dvy * dvy);
465
+ if (dl > speed) {
466
+ dvx = dvx / dl * speed;
467
+ dvy = dvy / dl * speed;
468
+ }
469
+ let vx = c.velocity[o3];
470
+ let vy = c.velocity[o3 + 1];
471
+ const ex = dvx - vx;
472
+ const ey = dvy - vy;
473
+ const el = Math.sqrt(ex * ex + ey * ey);
474
+ const maxDv = accel * dt;
475
+ if (el > maxDv) {
476
+ vx += ex / el * maxDv;
477
+ vy += ey / el * maxDv;
478
+ } else {
479
+ vx = dvx;
480
+ vy = dvy;
481
+ }
482
+ if (rec.drive === DRIVE_TRANSFORM && g.grid) {
483
+ const grid = g.grid;
484
+ seg[0] = (pos[0] - g.ox) / g.csx;
485
+ seg[1] = (pos[1] - g.oy) / g.csy;
486
+ seg[2] = (pos[0] + vx * dt - g.ox) / g.csx;
487
+ seg[3] = (pos[1] + vy * dt - g.oy) / g.csy;
488
+ if (!traceSegment(grid, seg, 2, 255)) {
489
+ seg[3] = seg[1];
490
+ if (traceSegment(grid, seg, 2, 255)) vy = 0;
491
+ else {
492
+ seg[2] = seg[0];
493
+ seg[3] = (pos[1] + vy * dt - g.oy) / g.csy;
494
+ if (traceSegment(grid, seg, 2, 255)) vx = 0;
495
+ else {
496
+ vx = 0;
497
+ vy = 0;
498
+ }
499
+ }
500
+ }
501
+ }
502
+ c.velocity[o3] = vx;
503
+ c.velocity[o3 + 1] = vy;
504
+ c.velocity[o3 + 2] = 0;
505
+ applyDrive(table, row, rec, vx, vy, 0, dt, dim);
506
+ setStatus(world, s, row, rec, rec.reachable ? MOVING : UNREACHABLE);
507
+ }
508
+ function crowdParams(s, row, rt) {
509
+ const c = s.cols;
510
+ const r = c.radius[row];
511
+ const avoid = c.avoidance[row] === 1;
512
+ return {
513
+ radius: r,
514
+ height: rt.settings.agentHeight,
515
+ maxAcceleration: c.acceleration[row],
516
+ maxSpeed: c.speed[row],
517
+ collisionQueryRange: r * 12,
518
+ pathOptimizationRange: r * 30,
519
+ // Separation pushes across the whole query range (12 radii): agents at the edge of a group
520
+ // settle short of their goals. Velocity-obstacle avoidance alone keeps them apart.
521
+ separationWeight: 0,
522
+ updateFlags: ANTICIPATE_TURNS | OPTIMIZE_VIS | OPTIMIZE_TOPO | (avoid ? OBSTACLE_AVOIDANCE : 0),
523
+ obstacleAvoidanceType: 3,
524
+ queryFilterType: 0
525
+ };
526
+ }
527
+ function ensureCrowd(world, nav, rt, radius) {
528
+ let agents = 0;
529
+ for (const r of nav.agents.values()) if (r.nav === rt.entity) agents++;
530
+ if (rt.crowd && rt.crowdCapacity >= agents && rt.crowdRadius >= radius) return;
531
+ const capacity = Math.max(32, rt.crowdCapacity * 2, agents * 2);
532
+ const maxRadius = Math.max(radius, rt.crowdRadius, rt.settings.agentRadius);
533
+ rt.destroyCrowd();
534
+ rt.crowd = new rt.R.Crowd(rt.navMesh, { maxAgents: capacity, maxAgentRadius: maxRadius });
535
+ rt.crowdCapacity = capacity;
536
+ rt.crowdRadius = maxRadius;
537
+ rt.applyAreas(world.tryResource(NavAreas) ?? {});
538
+ for (const r of nav.agents.values()) if (r.nav === rt.entity) r.crowdIndex = -1;
539
+ }
540
+ function requestCrowd(s, world, nav, table, row, rec, rt, repath, dim) {
541
+ const c = s.cols;
542
+ if (!rt.crowd || rec.crowdIndex < 0) ensureCrowd(world, nav, rt, c.radius[row]);
543
+ const crowd = rt.crowd;
544
+ vec.x = pos[0];
545
+ vec.y = pos[1];
546
+ vec.z = pos[2];
547
+ let agent = rec.crowdIndex >= 0 ? crowd.agents[rec.crowdIndex] : void 0;
548
+ if (!agent) {
549
+ agent = crowd.addAgent(vec, crowdParams(s, row, rt));
550
+ rec.crowdIndex = agent.agentIndex;
551
+ repath = true;
552
+ } else if (c.agentTicks[row] > rec.agentTick || repath) {
553
+ agent.updateParameters(crowdParams(s, row, rt));
554
+ }
555
+ if (rec.count === 0) {
556
+ halt(s, world, table, row, rec, UNREACHABLE, dim);
557
+ agent.resetMoveTarget();
558
+ return true;
559
+ }
560
+ if (repath) {
561
+ const e = (rec.count - 1) * 3;
562
+ vec.x = rec.corners[e];
563
+ vec.y = rec.corners[e + 1];
564
+ vec.z = rec.corners[e + 2];
565
+ agent.requestMoveTarget(vec);
566
+ }
567
+ if (rec.drive !== DRIVE_TRANSFORM && agent.raw.get_state() !== CROWD_OFFMESH) {
568
+ const raw = agent.raw;
569
+ raw.set_npos(0, pos[0]);
570
+ raw.set_npos(1, pos[1]);
571
+ raw.set_npos(2, pos[2]);
572
+ }
573
+ return true;
574
+ }
575
+ function readCrowd(s, world, nav, table, row, dt, dim) {
576
+ const c = s.cols;
577
+ const rec = nav.agents.get(table.entities[row]);
578
+ if (rec?.kind !== "mesh" || rec.crowdIndex < 0 || c.stopped[row] === 1) return;
579
+ const rt = nav.meshes.get(rec.nav);
580
+ const agent = rt?.crowd?.agents[rec.crowdIndex];
581
+ if (!agent || rec.count === 0) return;
582
+ const raw = agent.raw;
583
+ const o3 = row * 3;
584
+ const px = raw.get_npos(0);
585
+ const py = raw.get_npos(1);
586
+ const pz = raw.get_npos(2);
587
+ let vx = raw.get_vel(0);
588
+ let vy = raw.get_vel(1);
589
+ let vz = raw.get_vel(2);
590
+ const hx = rec.framed ? rec.lpos[0] : c.translation[o3];
591
+ const hy = rec.framed ? rec.lpos[1] : c.translation[o3 + 1];
592
+ const hz = rec.framed ? rec.lpos[2] : c.translation[o3 + 2];
593
+ const e = (rec.count - 1) * 3;
594
+ const ex = rec.corners[e] - hx;
595
+ const ez = rec.corners[e + 2] - hz;
596
+ const ey = rec.corners[e + 1] - hy;
597
+ let remaining = 0;
598
+ let lx = hx;
599
+ let ly = hy;
600
+ let lz = hz;
601
+ const n = raw.get_ncorners();
602
+ let ended = false;
603
+ for (let i = 0; i < n; i++) {
604
+ const cx = raw.get_cornerVerts(i * 3);
605
+ const cy = raw.get_cornerVerts(i * 3 + 1);
606
+ const cz = raw.get_cornerVerts(i * 3 + 2);
607
+ remaining += Math.sqrt((cx - lx) ** 2 + (cy - ly) ** 2 + (cz - lz) ** 2);
608
+ lx = cx;
609
+ ly = cy;
610
+ lz = cz;
611
+ if ((raw.get_cornerFlags(i) & STRAIGHTPATH_END) !== 0) ended = true;
612
+ }
613
+ if (!ended) {
614
+ remaining += Math.sqrt(
615
+ (rec.corners[e] - lx) ** 2 + (rec.corners[e + 1] - ly) ** 2 + (rec.corners[e + 2] - lz) ** 2
616
+ );
617
+ }
618
+ const flat = Math.sqrt(ex * ex + ez * ez);
619
+ c.remaining[row] = n === 0 ? flat : remaining;
620
+ c.corners[row] = n;
621
+ if (flat <= c.stopping[row] && Math.abs(ey) < rt.settings.agentHeight) {
622
+ agent.resetMoveTarget();
623
+ halt(s, world, table, row, rec, rec.reachable ? ARRIVED : UNREACHABLE, dim);
624
+ c.remaining[row] = flat;
625
+ raw.set_npos(0, hx);
626
+ raw.set_npos(1, hy);
627
+ raw.set_npos(2, hz);
628
+ return;
629
+ }
630
+ const offmesh = raw.get_state() === CROWD_OFFMESH;
631
+ const link = rec.link;
632
+ if (offmesh && !link.active) startLink(nav, rec, hy, px, py, pz);
633
+ link.active = offmesh;
634
+ if (rec.drive === DRIVE_TRANSFORM || offmesh) {
635
+ let y = py;
636
+ if (offmesh) {
637
+ const total = Math.sqrt((link.x1 - link.x0) ** 2 + (link.z1 - link.z0) ** 2);
638
+ const done = Math.sqrt((px - link.x0) ** 2 + (pz - link.z0) ** 2);
639
+ const t = total > 1e-6 ? Math.min(1, done / total) : 1;
640
+ y = link.y0 + (link.y1 - link.y0) * t + link.lift + link.arc * 4 * t * (1 - t);
641
+ }
642
+ vx = (px - hx) / dt;
643
+ vy = (y - hy) / dt;
644
+ vz = (pz - hz) / dt;
645
+ if (rec.framed) {
646
+ const drive = rec.drive;
647
+ rec.drive = DRIVE_TRANSFORM;
648
+ applyDrive(table, row, rec, vx, vy, vz, dt, dim, rt);
649
+ rec.drive = drive;
650
+ } else {
651
+ const tr = c.translation;
652
+ tr[o3] = px;
653
+ tr[o3 + 1] = y;
654
+ tr[o3 + 2] = pz;
655
+ table.markChanged(Transform, row);
656
+ }
657
+ if (rec.drive !== DRIVE_TRANSFORM) applyDrive(table, row, rec, 0, 0, 0, 0, dim, rt);
658
+ } else {
659
+ applyDrive(table, row, rec, vx, vy, vz, dt, dim, rt);
660
+ }
661
+ c.velocity[o3] = vx;
662
+ c.velocity[o3 + 1] = vy;
663
+ c.velocity[o3 + 2] = vz;
664
+ setStatus(world, s, row, rec, rec.reachable ? MOVING : UNREACHABLE);
665
+ }
666
+ function startLink(nav, rec, y, x, surface, z) {
667
+ const link = rec.link;
668
+ link.x0 = x;
669
+ link.z0 = z;
670
+ link.y0 = surface;
671
+ link.x1 = x;
672
+ link.z1 = z;
673
+ link.y1 = surface;
674
+ link.arc = 0.5;
675
+ let best = Infinity;
676
+ for (let i = 0; i < nav.links.length; i++) {
677
+ const l = nav.links[i];
678
+ const ds = (l.start[0] - x) ** 2 + (l.start[2] - z) ** 2;
679
+ const de = l.bidirectional ? (l.end[0] - x) ** 2 + (l.end[2] - z) ** 2 : Infinity;
680
+ const d = Math.min(ds, de);
681
+ if (d >= best) continue;
682
+ best = d;
683
+ const near = ds <= de ? l.start : l.end;
684
+ const far = ds <= de ? l.end : l.start;
685
+ link.y0 = near[1];
686
+ link.x1 = far[0];
687
+ link.z1 = far[2];
688
+ link.y1 = far[1];
689
+ link.arc = 0.4 + Math.abs(far[1] - near[1]);
690
+ }
691
+ link.lift = rec.drive === DRIVE_TRANSFORM ? 0 : Math.max(0, y - link.y0);
692
+ }
693
+ function dropAgent(world, entity) {
694
+ const nav = world.tryResource(Nav);
695
+ const rec = nav?.agents.get(entity);
696
+ if (!nav || !rec) return;
697
+ removeFromCrowd(nav, rec);
698
+ nav.agents.delete(entity);
699
+ }
700
+ export {
701
+ dropAgent,
702
+ navAgents
703
+ };