@daneren2005/shared-memory-physics 0.0.1 → 1.0.0
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 +70 -15
- package/dist/index.d.ts +4 -2
- package/dist/index.js +86 -64
- package/dist/index.js.map +1 -1
- package/dist/math/shapes.d.ts +0 -1
- package/dist/systems/physics-update.d.ts +1 -0
- package/dist/systems/spatial-index.d.ts +23 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -121,16 +121,23 @@ world.update(1000); // one second: the entity above is now at x = 10
|
|
|
121
121
|
`elapsedTime` is in milliseconds (what `BaseWorld#update` is driven with) and velocity is in world units per
|
|
122
122
|
**second**, so a run covering 16ms moves an entity 16/1000ths of its velocity.
|
|
123
123
|
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
An entity that moved is reported back to the main thread as a single `position-updated` event carrying where
|
|
125
|
+
it ended up, so anything you keep keyed off position can follow a move rather than having to poll the block:
|
|
126
126
|
|
|
127
127
|
```ts
|
|
128
|
-
|
|
129
|
-
|
|
128
|
+
import { POSITION_UPDATED_EVENT } from '@daneren2005/shared-memory-physics';
|
|
129
|
+
|
|
130
|
+
entity.on(POSITION_UPDATED_EVENT, (x, y) => {
|
|
131
|
+
// 12.5, -3
|
|
130
132
|
});
|
|
131
133
|
```
|
|
132
134
|
|
|
133
|
-
|
|
135
|
+
Both axes come with it even when only one of them moved, so a listener always has the whole position without
|
|
136
|
+
having to remember the last one it was told - and a diagonal move costs one event across the worker boundary
|
|
137
|
+
rather than the two a `component-property-updated` per property would. The move is not reported as a property
|
|
138
|
+
change as well, so a listener on `component-property-updated` no longer hears about positions at all.
|
|
139
|
+
|
|
140
|
+
An entity that did not move says nothing, so a world of still entities stays quiet - and so does one held
|
|
134
141
|
against something it cannot move past.
|
|
135
142
|
|
|
136
143
|
The move is written with `addAtomicFloat32` rather than a plain `+=`, so a game system on another thread can
|
|
@@ -227,7 +234,8 @@ Writes go straight into shared memory, so a callback can bounce an entity by fli
|
|
|
227
234
|
value another thread also touches with the atomics from `@daneren2005/shared-memory-objects`. To reach past
|
|
228
235
|
the two entities in front of it - to credit a third for a kill, say - a callback can walk
|
|
229
236
|
`queries[COLLIDABLE_QUERY]`, the full collidable list. Anything that has to happen on the main thread goes
|
|
230
|
-
through `callbacks`: `entityDied`, `entityComponentChanged`, `createEntity
|
|
237
|
+
through `callbacks`: `entityDied`, `entityComponentChanged`, `createEntity`, and `emitEntityEvent` for an
|
|
238
|
+
event of your own - the same one the move above reports itself through.
|
|
231
239
|
|
|
232
240
|
**What collides.** Everything with a transform and a `body`, not only the entities the system moves, so a
|
|
233
241
|
ship can run into a station that has no velocity of its own. Shapes that only just touch do not count as
|
|
@@ -240,13 +248,13 @@ against everything along its path, and the entity is put down on the edge of wha
|
|
|
240
248
|
`onCollision` runs for it, exactly as it would if the two had ended up overlapping.
|
|
241
249
|
|
|
242
250
|
```ts
|
|
243
|
-
// A ship at x = 0 and a station at x = 30, both 10 wide, with a velocity that asks for
|
|
251
|
+
// A ship at x = 0 and a station at x = 30, both 10 wide, with a velocity that asks for 25 in this run.
|
|
244
252
|
world.update(1000);
|
|
245
|
-
console.log(ship.components.transform!.x); // ~20, their edges touching - not
|
|
253
|
+
console.log(ship.components.transform!.x); // ~20, their edges touching - not 25, and never inside it
|
|
246
254
|
```
|
|
247
255
|
|
|
248
|
-
The
|
|
249
|
-
|
|
256
|
+
The move is one decision over everything it lands on rather than one per entity, so a mover ending up on top of
|
|
257
|
+
two units comes to rest against the *near* one however the two happened to come up, and only the near one is
|
|
250
258
|
reported as a collision - the far one was never reached. An entity wedged between two at the same moment gets a
|
|
251
259
|
call for each.
|
|
252
260
|
|
|
@@ -259,13 +267,18 @@ Two details worth knowing:
|
|
|
259
267
|
forwards each run, so it reports no position change while it pushes - but it does keep reporting the
|
|
260
268
|
collision for as long as it keeps pushing.
|
|
261
269
|
|
|
262
|
-
|
|
270
|
+
**What it costs.** A move is checked where it *ends*, so a clear one - which is nearly every move - is a single
|
|
271
|
+
shape test against each thing near where it lands, and nothing more. Only a move that is genuinely stopping
|
|
272
|
+
somewhere pays for working out where: contact is then halved in on rather than solved, since three shapes at any
|
|
263
273
|
rotation to one another have no single formula for when a pair first touches, while the overlap test the
|
|
264
274
|
narrowphase already has answers it for any pair anywhere. The position an entity is put down at is always one
|
|
265
|
-
that tested clear, so it is genuinely outside everything - within about `1e-4`
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
275
|
+
that tested clear, so it is genuinely outside everything - within about `1e-4` world units of touching.
|
|
276
|
+
|
|
277
|
+
The trade for that is **a move long enough to carry an entity clean past something is not stopped by it**: by
|
|
278
|
+
the time the move is out there is nothing left to land on. That takes a single run covering more ground than the
|
|
279
|
+
thing in the way is thick, so at any normal frame rate it does not arise - a ship crossing 100 units a second
|
|
280
|
+
moves 1.6 of them per frame at 60fps. A fixed `deltaBetweenRuns` puts a ceiling on it if your game has anything
|
|
281
|
+
fast enough to care.
|
|
269
282
|
|
|
270
283
|
### Collision filtering
|
|
271
284
|
|
|
@@ -355,6 +368,48 @@ transform[TRANSFORM_Y_INDEX] += moveY * fraction;
|
|
|
355
368
|
`blocking` is what the entity came to rest *against*, which `forEachOverlapping` will not report from that
|
|
356
369
|
resting place: the two are touching rather than through each other.
|
|
357
370
|
|
|
371
|
+
## Spatial queries
|
|
372
|
+
|
|
373
|
+
`SpatialIndex` answers the other kind of question about where things are - who is in this area, and what is
|
|
374
|
+
the closest thing to this point - for the systems that are not about collisions at all: targeting, aggro
|
|
375
|
+
range, spawning somewhere clear, area effects. It is the same R-tree the broadphase is built on, without the
|
|
376
|
+
collide categories or the shape tests.
|
|
377
|
+
|
|
378
|
+
Build one from anything shaped like a query result and it reads the transform (and the body, to know which
|
|
379
|
+
outline that transform describes) off each entity. Only the transform is needed, so it indexes anything with
|
|
380
|
+
a place in the world rather than only what collides:
|
|
381
|
+
|
|
382
|
+
```ts
|
|
383
|
+
import { SpatialIndex } from '@daneren2005/shared-memory-physics';
|
|
384
|
+
|
|
385
|
+
const index = new SpatialIndex(queries.collidable);
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
Nothing reads the blocks after that, so the index is a **snapshot** of where everything was when it was
|
|
389
|
+
built - build one per run, in `preRun`, and let every entity in that run search it.
|
|
390
|
+
|
|
391
|
+
| Query | Answers |
|
|
392
|
+
| -------------------------------------------------- | ---------------------------------------------------- |
|
|
393
|
+
| `search(minX, minY, maxX, maxY, filter?)` | everything whose box reaches into that one |
|
|
394
|
+
| `searchAround(x, y, reachX, reachY, filter?)` | the same, written from the middle out |
|
|
395
|
+
| `findNearest(x, y, maxDistance?, filter?)` | the closest one, or `undefined` |
|
|
396
|
+
| `findNearby(x, y, maxResults, maxDistance?, filter?)` | the closest `maxResults`, nearest first |
|
|
397
|
+
|
|
398
|
+
Every one of them takes a `filter`, run per candidate the tree turns up, and hands it the whole entity -
|
|
399
|
+
`{ entityId, components }` - so it can read back any block that came along with the query. That is where the
|
|
400
|
+
things the index cannot know go: the searcher's own id, which side something is on, whether it is worth
|
|
401
|
+
shooting at.
|
|
402
|
+
|
|
403
|
+
```ts
|
|
404
|
+
const target = index.findNearest(x, y, 150, other => other.entityId !== entityId && isEnemy(other));
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
`findNearest` walks the tree in distance order rather than searching a box and sorting what comes back, so
|
|
408
|
+
ask it directly rather than building the sort yourself - it settles as soon as the nearest box is reached, and
|
|
409
|
+
`maxDistance` stops it walking the rest of the world when the answer is that there is nobody. Distance is
|
|
410
|
+
measured to an entity's **box**, not to its centre, so a large target is as near as its nearest edge and
|
|
411
|
+
`maxDistance` reads as a range past the hull.
|
|
412
|
+
|
|
358
413
|
## Building
|
|
359
414
|
|
|
360
415
|
```sh
|
package/dist/index.d.ts
CHANGED
|
@@ -9,10 +9,12 @@ export { SHAPE_RECTANGLE, SHAPE_CIRCLE, SHAPE_CAPSULE, DEFAULT_COLLIDE_CATEGORY,
|
|
|
9
9
|
export type { BodyComponent, BodyConfig, } from './components/body-component';
|
|
10
10
|
export { physicsRegistry } from './components/registry';
|
|
11
11
|
export type { PhysicsComponents, PhysicsUpdateComponents } from './components/registry';
|
|
12
|
-
export { shapesOverlap, shapeHalfWidth, shapeHalfHeight, shapeIsEmpty,
|
|
12
|
+
export { shapesOverlap, shapeHalfWidth, shapeHalfHeight, shapeIsEmpty, shapeRadius, capsuleHalfLength, boundsHalfWidth, boundsHalfHeight, orientedBoxesOverlap, segmentSegmentDistanceSquared, segmentBoxDistanceSquared, pointSegmentDistanceSquared, } from './math/shapes';
|
|
13
13
|
export { default as CollisionBroadphase, COLLIDABLE_QUERY } from './systems/collision';
|
|
14
14
|
export type { CollisionComponents, CollisionEntity, CollisionFunction, MovingEntity, SweepResult } from './systems/collision';
|
|
15
|
-
export { default as
|
|
15
|
+
export { default as SpatialIndex } from './systems/spatial-index';
|
|
16
|
+
export type { SpatialComponents, SpatialEntity, SpatialFilter } from './systems/spatial-index';
|
|
17
|
+
export { default as physicsUpdate, createPhysicsUpdate, POSITION_UPDATED_EVENT } from './systems/physics-update';
|
|
16
18
|
export type { PhysicsUpdateFunction, PhysicsUpdateMetadata, PhysicsUpdateOptions } from './systems/physics-update';
|
|
17
19
|
export { default as PhysicsSystem } from './systems/physics-system';
|
|
18
20
|
export type { PhysicsSystemConfig } from './systems/physics-system';
|
package/dist/index.js
CHANGED
|
@@ -99,7 +99,7 @@ var r = 0, i = 1, a = 2, o = 3, s = 4, c = 5, l = {
|
|
|
99
99
|
function b(e, t, n, r) {
|
|
100
100
|
return (t & n) !== 0 && (r & e) !== 0;
|
|
101
101
|
}
|
|
102
|
-
var x = 0,
|
|
102
|
+
var x = 0, S = 1, C = 2, w = 3, T = {
|
|
103
103
|
type: Uint32Array,
|
|
104
104
|
size: 3,
|
|
105
105
|
loadProperties: [
|
|
@@ -112,7 +112,7 @@ var x = 0, ee = 1, S = 2, C = 3, w = {
|
|
|
112
112
|
],
|
|
113
113
|
load(e, t, n) {
|
|
114
114
|
let r = t.create([
|
|
115
|
-
|
|
115
|
+
E(n),
|
|
116
116
|
n.collideCategory ?? 1,
|
|
117
117
|
n.collideMask ?? 4294967295
|
|
118
118
|
]), i = t.getBlock(r);
|
|
@@ -139,7 +139,7 @@ var x = 0, ee = 1, S = 2, C = 3, w = {
|
|
|
139
139
|
};
|
|
140
140
|
}
|
|
141
141
|
};
|
|
142
|
-
function
|
|
142
|
+
function E(e) {
|
|
143
143
|
let t = e.shape;
|
|
144
144
|
if (t === void 0) return e.radius === void 0 ? 1 : 2;
|
|
145
145
|
if (!_.includes(t)) throw Error(`Unknown body shape: ${t}`);
|
|
@@ -147,43 +147,40 @@ function T(e) {
|
|
|
147
147
|
}
|
|
148
148
|
//#endregion
|
|
149
149
|
//#region src/components/registry.ts
|
|
150
|
-
var
|
|
150
|
+
var D = {
|
|
151
151
|
transform: l,
|
|
152
152
|
velocity: p,
|
|
153
|
-
body:
|
|
153
|
+
body: T
|
|
154
154
|
};
|
|
155
155
|
//#endregion
|
|
156
156
|
//#region src/math/shapes.ts
|
|
157
|
-
function
|
|
157
|
+
function O(e, t) {
|
|
158
158
|
return Math.max(0, (e - t) / 2);
|
|
159
159
|
}
|
|
160
|
-
function
|
|
160
|
+
function k(e, t, n) {
|
|
161
161
|
return e === 2 ? t / 2 : e === 3 ? n / 2 : 0;
|
|
162
162
|
}
|
|
163
|
-
function
|
|
163
|
+
function A(e, t, n) {
|
|
164
164
|
return e === 2 ? t <= 0 : e === 3 ? n <= 0 : t <= 0 || n <= 0;
|
|
165
165
|
}
|
|
166
|
-
function A(e, t, n, r) {
|
|
167
|
-
return e === 2 ? t / 2 : e === 3 ? Math.abs(Math.cos(r)) * D(t, n) + n / 2 : F(t, n, r);
|
|
168
|
-
}
|
|
169
166
|
function j(e, t, n, r) {
|
|
170
|
-
return e === 2 ? t / 2 : e === 3 ? Math.abs(Math.
|
|
167
|
+
return e === 2 ? t / 2 : e === 3 ? Math.abs(Math.cos(r)) * O(t, n) + n / 2 : F(t, n, r);
|
|
171
168
|
}
|
|
172
|
-
function M(e, t, n) {
|
|
173
|
-
return e === 2 ? t / 2 : e === 3 ? n / 2 :
|
|
169
|
+
function M(e, t, n, r) {
|
|
170
|
+
return e === 2 ? t / 2 : e === 3 ? Math.abs(Math.sin(r)) * O(t, n) + n / 2 : I(t, n, r);
|
|
174
171
|
}
|
|
175
172
|
function N(e, t, n, r, i, a, o, s, c, l, u, d) {
|
|
176
|
-
if (
|
|
173
|
+
if (A(e, r, i) || A(o, l, u)) return !1;
|
|
177
174
|
let f = e !== 1, p = o !== 1;
|
|
178
175
|
if (!f && !p) return L(t, n, r, i, a, s, c, l, u, d);
|
|
179
176
|
if (f && !p) return N(o, s, c, l, u, d, e, t, n, r, i, a);
|
|
180
|
-
let m = P(o, l, u), h = Math.cos(d) * m, g = Math.sin(d) * m, _ =
|
|
177
|
+
let m = P(o, l, u), h = Math.cos(d) * m, g = Math.sin(d) * m, _ = k(o, l, u);
|
|
181
178
|
if (!f) return B(s - h, c - g, s + h, c + g, t, n, r, i, a) < _ * _;
|
|
182
|
-
let v = P(e, r, i), y = Math.cos(a) * v, b = Math.sin(a) * v, x =
|
|
179
|
+
let v = P(e, r, i), y = Math.cos(a) * v, b = Math.sin(a) * v, x = k(e, r, i) + _;
|
|
183
180
|
return z(t - y, n - b, t + y, n + b, s - h, c - g, s + h, c + g) < x * x;
|
|
184
181
|
}
|
|
185
182
|
function P(e, t, n) {
|
|
186
|
-
return e === 3 ?
|
|
183
|
+
return e === 3 ? O(t, n) : 0;
|
|
187
184
|
}
|
|
188
185
|
function F(e, t, n) {
|
|
189
186
|
return (Math.abs(Math.cos(n)) * e + Math.abs(Math.sin(n)) * t) / 2;
|
|
@@ -237,7 +234,7 @@ function W(e) {
|
|
|
237
234
|
}
|
|
238
235
|
//#endregion
|
|
239
236
|
//#region src/systems/collision.ts
|
|
240
|
-
var G = 1e-4,
|
|
237
|
+
var G = 1e-4, ee = 24, K = "collidable", q = [], J = class {
|
|
241
238
|
buckets = [];
|
|
242
239
|
constructor(t, n) {
|
|
243
240
|
let r = /* @__PURE__ */ new Map();
|
|
@@ -247,8 +244,8 @@ var G = 1e-4, K = 20, te = 64, q = "collidable", J = [], Y = class {
|
|
|
247
244
|
let o = a[1];
|
|
248
245
|
if (o === 0) continue;
|
|
249
246
|
let s = a[0], c = i[2], l = i[3];
|
|
250
|
-
if (
|
|
251
|
-
let u = i[4], d =
|
|
247
|
+
if (A(s, c, l)) continue;
|
|
248
|
+
let u = i[4], d = j(s, c, l, u), f = M(s, c, l, u), p = t.velocity, m = p ? Math.abs(p[0]) * n : 0, h = p ? Math.abs(p[1]) * n : 0, g = i[0], _ = i[1], v = r.get(o);
|
|
252
249
|
v || (v = {
|
|
253
250
|
entries: [],
|
|
254
251
|
bounds: []
|
|
@@ -268,57 +265,41 @@ var G = 1e-4, K = 20, te = 64, q = "collidable", J = [], Y = class {
|
|
|
268
265
|
}
|
|
269
266
|
}
|
|
270
267
|
forEachOverlapping(e, t) {
|
|
271
|
-
let n =
|
|
268
|
+
let n = Y(e);
|
|
272
269
|
if (!n) return;
|
|
273
270
|
let { x: r, y: i, halfWidth: a, halfHeight: o } = n;
|
|
274
271
|
this.forEachCandidate(n, r - a, i - o, r + a, i + o, (e) => {
|
|
275
|
-
|
|
272
|
+
X(n, r, i, e) && t(e);
|
|
276
273
|
});
|
|
277
274
|
}
|
|
278
275
|
sweep(e, t, n) {
|
|
279
|
-
let r = Math.sqrt(t * t + n * n), i =
|
|
276
|
+
let r = Math.sqrt(t * t + n * n), i = Y(e);
|
|
280
277
|
if (!i || r === 0) return {
|
|
281
278
|
fraction: 1,
|
|
282
|
-
blocking:
|
|
279
|
+
blocking: q
|
|
283
280
|
};
|
|
284
|
-
let { x: a, y: o, halfWidth: s, halfHeight: c } = i, l = a + t, u = o + n, d = []
|
|
285
|
-
if (this.forEachCandidate(i,
|
|
286
|
-
|
|
287
|
-
d.push(e);
|
|
288
|
-
let t = e.components.body, n = e.components.transform;
|
|
289
|
-
f = Math.min(f, M(t[0], n[2], n[3]));
|
|
281
|
+
let { x: a, y: o, halfWidth: s, halfHeight: c } = i, l = a + t, u = o + n, d = [];
|
|
282
|
+
if (this.forEachCandidate(i, l - s, u - c, l + s, u + c, (e) => {
|
|
283
|
+
X(i, l, u, e) && (X(i, a, o, e) || d.push(e));
|
|
290
284
|
}), d.length === 0) return {
|
|
291
285
|
fraction: 1,
|
|
292
|
-
blocking:
|
|
286
|
+
blocking: q
|
|
293
287
|
};
|
|
294
|
-
let
|
|
295
|
-
for (let e =
|
|
296
|
-
let
|
|
297
|
-
|
|
298
|
-
m = r;
|
|
299
|
-
break;
|
|
300
|
-
}
|
|
301
|
-
p = r;
|
|
288
|
+
let f = 0, p = 1, m = G / r;
|
|
289
|
+
for (let e = 0; e < ee && p - f > m; e++) {
|
|
290
|
+
let e = (f + p) / 2;
|
|
291
|
+
this.blockedAt(i, d, t, n, e) ? p = e : f = e;
|
|
302
292
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
blocking: J
|
|
306
|
-
};
|
|
307
|
-
let g = G / r;
|
|
308
|
-
for (let e = 0; e < K && m - p > g; e++) {
|
|
309
|
-
let e = (p + m) / 2;
|
|
310
|
-
this.blockedAt(i, d, t, n, e) ? m = e : p = e;
|
|
311
|
-
}
|
|
312
|
-
let _ = [], v = a + t * m, y = o + n * m;
|
|
313
|
-
for (let e of d) Z(i, v, y, e) && _.push(e);
|
|
293
|
+
let h = [], g = a + t * p, _ = o + n * p;
|
|
294
|
+
for (let e of d) X(i, g, _, e) && h.push(e);
|
|
314
295
|
return {
|
|
315
|
-
fraction:
|
|
316
|
-
blocking:
|
|
296
|
+
fraction: f * r > G ? f : 0,
|
|
297
|
+
blocking: h
|
|
317
298
|
};
|
|
318
299
|
}
|
|
319
300
|
blockedAt(e, t, n, r, i) {
|
|
320
301
|
let a = e.x + n * i, o = e.y + r * i;
|
|
321
|
-
for (let n of t) if (
|
|
302
|
+
for (let n of t) if (X(e, a, o, n)) return !0;
|
|
322
303
|
return !1;
|
|
323
304
|
}
|
|
324
305
|
forEachCandidate(e, t, n, r, i, a) {
|
|
@@ -328,13 +309,13 @@ var G = 1e-4, K = 20, te = 64, q = "collidable", J = [], Y = class {
|
|
|
328
309
|
}
|
|
329
310
|
}
|
|
330
311
|
};
|
|
331
|
-
function
|
|
312
|
+
function Y(e) {
|
|
332
313
|
let t = e.components.body;
|
|
333
314
|
if (!t) return;
|
|
334
315
|
let n = t[2];
|
|
335
316
|
if (n === 0) return;
|
|
336
317
|
let r = e.components.transform, i = t[0], a = r[2], o = r[3];
|
|
337
|
-
if (
|
|
318
|
+
if (A(i, a, o)) return;
|
|
338
319
|
let s = r[4];
|
|
339
320
|
return {
|
|
340
321
|
entityId: e.entityId,
|
|
@@ -344,18 +325,59 @@ function X(e) {
|
|
|
344
325
|
width: a,
|
|
345
326
|
height: o,
|
|
346
327
|
angle: s,
|
|
347
|
-
halfWidth:
|
|
348
|
-
halfHeight:
|
|
328
|
+
halfWidth: j(i, a, o, s),
|
|
329
|
+
halfHeight: M(i, a, o, s),
|
|
349
330
|
category: t[1],
|
|
350
331
|
mask: n
|
|
351
332
|
};
|
|
352
333
|
}
|
|
353
|
-
function
|
|
334
|
+
function X(e, t, n, r) {
|
|
354
335
|
let i = r.components.transform;
|
|
355
336
|
return N(e.shape, t, n, e.width, e.height, e.angle, r.components.body[0], i[0], i[1], i[2], i[3], i[4]);
|
|
356
337
|
}
|
|
357
338
|
//#endregion
|
|
358
|
-
//#region src/systems/
|
|
339
|
+
//#region src/systems/spatial-index.ts
|
|
340
|
+
var te = class {
|
|
341
|
+
entries = [];
|
|
342
|
+
index;
|
|
343
|
+
constructor(t) {
|
|
344
|
+
let n = [];
|
|
345
|
+
for (let e of t) {
|
|
346
|
+
let t = e.components, r = t.transform;
|
|
347
|
+
if (!r) continue;
|
|
348
|
+
let i = t.body, a = i ? i[0] : 1, o = r[2], s = r[3], c = r[4], l = r[0], u = r[1], d = j(a, o, s, c), f = M(a, o, s, c);
|
|
349
|
+
this.entries.push({
|
|
350
|
+
entityId: e.entityId,
|
|
351
|
+
components: t
|
|
352
|
+
}), n.push(l - d, u - f, l + d, u + f);
|
|
353
|
+
}
|
|
354
|
+
if (this.entries.length === 0) return;
|
|
355
|
+
let r = new e(this.entries.length);
|
|
356
|
+
for (let e = 0; e < this.entries.length; e++) r.add(n[e * 4], n[e * 4 + 1], n[e * 4 + 2], n[e * 4 + 3]);
|
|
357
|
+
r.finish(), this.index = r;
|
|
358
|
+
}
|
|
359
|
+
get size() {
|
|
360
|
+
return this.entries.length;
|
|
361
|
+
}
|
|
362
|
+
search(e, t, n, r, i) {
|
|
363
|
+
let a = this.index;
|
|
364
|
+
if (!a) return [];
|
|
365
|
+
let o = this.entries;
|
|
366
|
+
return (i ? a.search(e, t, n, r, (e) => i(o[e])) : a.search(e, t, n, r)).map((e) => o[e]);
|
|
367
|
+
}
|
|
368
|
+
searchAround(e, t, n, r, i) {
|
|
369
|
+
return this.search(e - n, t - r, e + n, t + r, i);
|
|
370
|
+
}
|
|
371
|
+
findNearest(e, t, n = Infinity, r) {
|
|
372
|
+
return this.findNearby(e, t, 1, n, r)[0];
|
|
373
|
+
}
|
|
374
|
+
findNearby(e, t, n, r = Infinity, i) {
|
|
375
|
+
let a = this.index;
|
|
376
|
+
if (!a) return [];
|
|
377
|
+
let o = this.entries;
|
|
378
|
+
return (i ? a.neighbors(e, t, n, r, (e) => i(o[e])) : a.neighbors(e, t, n, r)).map((e) => o[e]);
|
|
379
|
+
}
|
|
380
|
+
}, Z = "position-updated";
|
|
359
381
|
function ne(e = {}) {
|
|
360
382
|
let { onCollision: t, optional: n = [] } = e, r, i = Object.assign((e, n, i, a, o) => {
|
|
361
383
|
let s = {
|
|
@@ -374,7 +396,7 @@ function ne(e = {}) {
|
|
|
374
396
|
optional: n
|
|
375
397
|
} });
|
|
376
398
|
return t && (i.preRun = (e, t, n) => {
|
|
377
|
-
r = new
|
|
399
|
+
r = new J(n.collidable ?? [], e.elapsedTime / 1e3);
|
|
378
400
|
}), i;
|
|
379
401
|
}
|
|
380
402
|
var Q = (e, t, n, r, i) => {
|
|
@@ -382,7 +404,7 @@ var Q = (e, t, n, r, i) => {
|
|
|
382
404
|
$(t, n.transform, n.velocity[0] * a, n.velocity[1] * a, i);
|
|
383
405
|
};
|
|
384
406
|
function $(e, n, r, i, a) {
|
|
385
|
-
r !== 0 &&
|
|
407
|
+
r === 0 && i === 0 || (r !== 0 && t(n, 0, r, Infinity), i !== 0 && t(n, 1, i, Infinity), a.emitEntityEvent(e, Z, n[0], n[1]));
|
|
386
408
|
}
|
|
387
409
|
//#endregion
|
|
388
410
|
//#region src/systems/physics-system.ts
|
|
@@ -396,7 +418,7 @@ var re = class extends n {
|
|
|
396
418
|
required: ["transform", "velocity"],
|
|
397
419
|
optional: a,
|
|
398
420
|
updateFunction: n,
|
|
399
|
-
queries: i ? { [
|
|
421
|
+
queries: i ? { [K]: {
|
|
400
422
|
required: ["transform", "body"],
|
|
401
423
|
optional: ["velocity", ...r]
|
|
402
424
|
} } : void 0,
|
|
@@ -408,6 +430,6 @@ var re = class extends n {
|
|
|
408
430
|
}
|
|
409
431
|
};
|
|
410
432
|
//#endregion
|
|
411
|
-
export {
|
|
433
|
+
export { S as BODY_CATEGORY_INDEX, C as BODY_MASK_INDEX, x as BODY_SHAPE_INDEX, w as BODY_SIZE, K as COLLIDABLE_QUERY, J as CollisionBroadphase, v as DEFAULT_COLLIDE_CATEGORY, y as DEFAULT_COLLIDE_MASK, Z as POSITION_UPDATED_EVENT, re as PhysicsSystem, g as SHAPE_CAPSULE, h as SHAPE_CIRCLE, m as SHAPE_RECTANGLE, te as SpatialIndex, s as TRANSFORM_ANGLE_INDEX, o as TRANSFORM_HEIGHT_INDEX, c as TRANSFORM_SIZE, a as TRANSFORM_WIDTH_INDEX, r as TRANSFORM_X_INDEX, i as TRANSFORM_Y_INDEX, f as VELOCITY_SIZE, u as VELOCITY_X_INDEX, d as VELOCITY_Y_INDEX, T as bodyDefinition, I as boundsHalfHeight, F as boundsHalfWidth, b as canCollide, O as capsuleHalfLength, ne as createPhysicsUpdate, L as orientedBoxesOverlap, D as physicsRegistry, Q as physicsUpdate, V as pointSegmentDistanceSquared, B as segmentBoxDistanceSquared, z as segmentSegmentDistanceSquared, M as shapeHalfHeight, j as shapeHalfWidth, A as shapeIsEmpty, k as shapeRadius, N as shapesOverlap, l as transformDefinition, p as velocityDefinition };
|
|
412
434
|
|
|
413
435
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/components/transform-component.ts","../src/components/velocity-component.ts","../src/components/body-component.ts","../src/components/registry.ts","../src/math/shapes.ts","../src/systems/collision.ts","../src/systems/physics-update.ts","../src/systems/physics-system.ts"],"sourcesContent":["import type { ComponentDefinition } from '@daneren2005/shared-memory-ecs';\n\n// Where an entity is in the world, how big it is, and which way it is facing. This is 2D physics, so the\n// transform is a position, the box that sits around it, and the angle that box is rotated to.\nexport interface TransformComponent {\n\tindex: number\n\tx: number\n\ty: number\n\twidth: number\n\theight: number\n\tangle: number\n}\n\n// How big an entity is and which way it starts out facing. This is the defining half of a transform: it is\n// what a game's entity template says, the same for every ship of a type, and it is what gives an entity a\n// transform at all - a config with neither a width nor a height does not load one.\n//\n// `width`/`height` are the *unrotated* size of the box, and are required: a box with no area never overlaps\n// anything, so an entity without a real size could never collide.\n//\n// `angle` is in radians, measured counter-clockwise from the +x axis, so it feeds Math.cos / Math.sin\n// directly. Nothing in this library writes it - it is the game's to set, usually from its heading - which is\n// why it is a starting facing here rather than saved state.\n//\n// A round entity can give a `radius` **instead of** a width and a height, which loads as a width and a height\n// of its diameter - so there is still only one size in the block however the config spelled it. Giving both\n// throws rather than picking a winner.\nexport interface TransformConfig {\n\twidth: number\n\theight: number\n\tangle?: number\n\tradius?: number\n}\n// Where the entity is, which is the half that is live runtime state: the physics system moves it every run,\n// so it is what has to round-trip through `save` for a saved world to resume where it left off. The size and\n// facing come back from the game's own template rather than the save.\n//\n// `x`/`y` are the *centre* of the box, not a corner: the box is rotated about that point and collision\n// projects out from it in both directions.\nexport interface TransformSerialization {\n\tx: number\n\ty: number\n}\n\n// Indexes into the backing Float32Array block. The physics update reads the same offsets off the raw shared\n// block, so they are exported for it (and for any game system that touches the block directly).\nexport const TRANSFORM_X_INDEX = 0;\nexport const TRANSFORM_Y_INDEX = 1;\nexport const TRANSFORM_WIDTH_INDEX = 2;\nexport const TRANSFORM_HEIGHT_INDEX = 3;\nexport const TRANSFORM_ANGLE_INDEX = 4;\nexport const TRANSFORM_SIZE = 5;\n\nexport const transformDefinition: ComponentDefinition<TransformComponent, Float32Array, TransformConfig, TransformSerialization> = {\n\ttype: Float32Array,\n\tsize: TRANSFORM_SIZE,\n\t// A size is what marks a config as having a transform at all. x/y are not in the list: they come back off\n\t// a save rather than out of a game's entity template, so on their own they do not describe an entity that\n\t// has a place in the world.\n\tloadProperties: ['width', 'height', 'radius'],\n\tload(entity, memory, config) {\n\t\t// A radius is the same size said differently, so it is turned into a width and a height here and nothing\n\t\t// downstream has to know which way the config was written. Both at once is a config that cannot be\n\t\t// honoured either way round, so it is refused rather than silently resolved.\n\t\tconst radius = config.radius;\n\t\tlet width = config.width;\n\t\tlet height = config.height;\n\t\tif(radius !== undefined) {\n\t\t\tif(width !== undefined || height !== undefined) {\n\t\t\t\tthrow new Error('A transform takes either a radius or a width and height, not both');\n\t\t\t}\n\n\t\t\twidth = height = radius * 2;\n\t\t}\n\n\t\tconst index = memory.create([config.x, config.y, width, height, config.angle ?? 0]);\n\t\tconst block = memory.getBlock(index);\n\n\t\treturn {\n\t\t\tindex,\n\t\t\tget x() {\n\t\t\t\treturn block[TRANSFORM_X_INDEX];\n\t\t\t},\n\t\t\tset x(value: number) {\n\t\t\t\tblock[TRANSFORM_X_INDEX] = value;\n\t\t\t},\n\t\t\tget y() {\n\t\t\t\treturn block[TRANSFORM_Y_INDEX];\n\t\t\t},\n\t\t\tset y(value: number) {\n\t\t\t\tblock[TRANSFORM_Y_INDEX] = value;\n\t\t\t},\n\t\t\tget width() {\n\t\t\t\treturn block[TRANSFORM_WIDTH_INDEX];\n\t\t\t},\n\t\t\tset width(value: number) {\n\t\t\t\tblock[TRANSFORM_WIDTH_INDEX] = value;\n\t\t\t},\n\t\t\tget height() {\n\t\t\t\treturn block[TRANSFORM_HEIGHT_INDEX];\n\t\t\t},\n\t\t\tset height(value: number) {\n\t\t\t\tblock[TRANSFORM_HEIGHT_INDEX] = value;\n\t\t\t},\n\t\t\tget angle() {\n\t\t\t\treturn block[TRANSFORM_ANGLE_INDEX];\n\t\t\t},\n\t\t\tset angle(value: number) {\n\t\t\t\tblock[TRANSFORM_ANGLE_INDEX] = value;\n\t\t\t},\n\t\t};\n\t},\n\tsave(component) {\n\t\treturn {\n\t\t\tx: component.x,\n\t\t\ty: component.y,\n\t\t};\n\t},\n};\n\nexport default transformDefinition;\n","import type { ComponentDefinition } from '@daneren2005/shared-memory-ecs';\n\n// How fast an entity is moving, in world units per second, along each axis. An entity with both a\n// transform and a velocity is moved by the physics system every run.\nexport interface VelocityComponent {\n\tindex: number\n\tvelocityX: number\n\tvelocityY: number\n}\n\n// Velocity is named per-axis rather than plain `x`/`y` because entity configs are flat and shared across\n// every component - `x`/`y` already belong to the transform. Either axis on its own is enough to give an\n// entity a velocity, so both are optional and a missing one starts at 0.\nexport interface VelocityConfig {\n\tvelocityX?: number\n\tvelocityY?: number\n}\n// Velocity is both the config an entity spawns with and live runtime state (a game's own systems apply\n// forces to it), so the whole thing round-trips back out through `save`.\nexport type VelocitySerialization = VelocityConfig;\n\n// Indexes into the backing Float32Array block. The physics update reads the same offsets off the raw shared\n// block, so they are exported for it (and for any game system that touches the block directly).\nexport const VELOCITY_X_INDEX = 0;\nexport const VELOCITY_Y_INDEX = 1;\nexport const VELOCITY_SIZE = 2;\n\nexport const velocityDefinition: ComponentDefinition<VelocityComponent, Float32Array, VelocityConfig, VelocitySerialization> = {\n\ttype: Float32Array,\n\tsize: VELOCITY_SIZE,\n\tloadProperties: ['velocityX', 'velocityY'],\n\tload(entity, memory, config) {\n\t\tconst index = memory.create([config.velocityX ?? 0, config.velocityY ?? 0]);\n\t\tconst block = memory.getBlock(index);\n\n\t\treturn {\n\t\t\tindex,\n\t\t\tget velocityX() {\n\t\t\t\treturn block[VELOCITY_X_INDEX];\n\t\t\t},\n\t\t\tset velocityX(value: number) {\n\t\t\t\tblock[VELOCITY_X_INDEX] = value;\n\t\t\t},\n\t\t\tget velocityY() {\n\t\t\t\treturn block[VELOCITY_Y_INDEX];\n\t\t\t},\n\t\t\tset velocityY(value: number) {\n\t\t\t\tblock[VELOCITY_Y_INDEX] = value;\n\t\t\t},\n\t\t};\n\t},\n\tsave(component) {\n\t\treturn {\n\t\t\tvelocityX: component.velocityX,\n\t\t\tvelocityY: component.velocityY,\n\t\t};\n\t},\n};\n\nexport default velocityDefinition;\n","import type { ComponentDefinition } from '@daneren2005/shared-memory-ecs';\n\n// What an entity collides *as*, and what it is willing to collide *with*. Only entities with a body take part\n// in collisions at all - but a body is loaded for anything with a size, so in practice the way to opt an entity\n// out is `collideCategory: 0` rather than leaving the component off.\n//\n// The categories themselves are the game's to name. This library only ever reads them as bits, so a game\n// declares whatever set it needs (ground, air, projectile, terrain, ...) and hands the numbers in through the\n// config.\nexport interface BodyComponent {\n\tindex: number\n\tshape: number\n\tcollideCategory: number\n\tcollideMask: number\n}\n\n// The shapes a body can be. All three are held in the same transform - a position, a width, a height and an\n// angle - so which one an entity is changes what that data means rather than where it lives:\n//\n// SHAPE_RECTANGLE - a width x height box turned to `angle`\n// SHAPE_CIRCLE - a circle of diameter `width`, which `angle` does not affect\n// SHAPE_CAPSULE - a capsule `width` from end to end and `height` thick, lying along the way it faces\n//\n// See src/math/shapes.ts for what each one means geometrically.\nexport const SHAPE_RECTANGLE = 1;\nexport const SHAPE_CIRCLE = 2;\nexport const SHAPE_CAPSULE = 3;\n\nconst KNOWN_SHAPES = [SHAPE_RECTANGLE, SHAPE_CIRCLE, SHAPE_CAPSULE];\n\n// What a body collides as when the game does not say: a single bit, so a world where nothing sets a category\n// behaves exactly as it did before categories existed.\nexport const DEFAULT_COLLIDE_CATEGORY = 1;\n// ...and what it is willing to collide with: every bit, so a default body meets anything whose own mask lets\n// it. 0xFFFFFFFF does not fit in a signed int, but it is stored in a Uint32Array and JS bitwise operators\n// work on the same 32 bits, so it ANDs correctly against any category a game can supply.\nexport const DEFAULT_COLLIDE_MASK = 0xFFFFFFFF;\n\n// Whether two bodies collide: each side's mask has to accept the other's category.\n//\n// The test is deliberately **symmetric**, so a pair either collides or does not regardless of which one moved\n// into which. That matters because collisions are reported per entity as each one moves: a rule that depended\n// on the direction would make the outcome depend on the order the system happened to move them in. A game\n// that wants a one-sided *response* - a projectile that hurts a unit without the unit hurting it back - gets\n// that by checking `other` inside its own onCollision, not by the two entities disagreeing about whether they\n// touched.\n//\n// So a ground unit that should be hit by projectiles has to name projectiles in its own mask, not just the\n// other way round. `collideCategory: 0` collides with nothing at all, since every mask ANDs to 0 against it.\n//\n// `&` yields a signed 32 bit result, so a mask with the top bit set comes back negative - compare against 0\n// rather than testing for a positive number.\nexport function canCollide(categoryA: number, maskA: number, categoryB: number, maskB: number): boolean {\n\treturn (maskA & categoryB) !== 0 && (maskB & categoryA) !== 0;\n}\n\n// A size is what loads a body, the same way it is what loads a transform: anything big enough to be in the\n// world is collidable by default, and the collide properties are there to narrow that rather than to grant it.\n// They are all *defining* config out of a game's entity template, which is why there is no `save` below - a\n// reloaded entity gets its category and mask back from the template, not from the save.\nexport interface BodyConfig {\n\t// Not read by the loader, only listed in `loadProperties`: they are what marks a config as describing\n\t// something that has a place in the world, and so something that can be run into.\n\twidth?: number\n\theight?: number\n\n\t// One of the SHAPE_ constants above. Defaults to a circle for a config given a `radius` and a rectangle\n\t// otherwise, so only capsules have to name their shape.\n\tshape?: number\n\t// Not read here either - the transform is what turns it into a width and a height - but a config that gives\n\t// a radius is describing a circle, and that is what decides the default shape above.\n\tradius?: number\n\t// The bit(s) this entity collides as, and the bits it is willing to collide with. See canCollide.\n\tcollideCategory?: number\n\tcollideMask?: number\n}\n\n// Indexes into the backing Uint32Array block. The collision broadphase reads the same offsets off the raw\n// shared block, so they are exported for it (and for any game system that touches the block directly).\nexport const BODY_SHAPE_INDEX = 0;\nexport const BODY_CATEGORY_INDEX = 1;\nexport const BODY_MASK_INDEX = 2;\nexport const BODY_SIZE = 3;\n\nexport const bodyDefinition: ComponentDefinition<BodyComponent, Uint32Array, BodyConfig> = {\n\ttype: Uint32Array,\n\tsize: BODY_SIZE,\n\tloadProperties: ['width', 'height', 'radius', 'shape', 'collideCategory', 'collideMask'],\n\tload(entity, memory, config) {\n\t\tconst index = memory.create([\n\t\t\ttoShape(config),\n\t\t\tconfig.collideCategory ?? DEFAULT_COLLIDE_CATEGORY,\n\t\t\tconfig.collideMask ?? DEFAULT_COLLIDE_MASK,\n\t\t]);\n\t\tconst block = memory.getBlock(index);\n\n\t\treturn {\n\t\t\tindex,\n\t\t\tget shape() {\n\t\t\t\treturn block[BODY_SHAPE_INDEX];\n\t\t\t},\n\t\t\tset shape(value: number) {\n\t\t\t\tblock[BODY_SHAPE_INDEX] = value;\n\t\t\t},\n\t\t\tget collideCategory() {\n\t\t\t\treturn block[BODY_CATEGORY_INDEX];\n\t\t\t},\n\t\t\tset collideCategory(value: number) {\n\t\t\t\tblock[BODY_CATEGORY_INDEX] = value;\n\t\t\t},\n\t\t\tget collideMask() {\n\t\t\t\treturn block[BODY_MASK_INDEX];\n\t\t\t},\n\t\t\tset collideMask(value: number) {\n\t\t\t\tblock[BODY_MASK_INDEX] = value;\n\t\t\t},\n\t\t};\n\t},\n};\n\n// An unknown shape is thrown on rather than defaulted: silently treating it as a rectangle would leave an\n// entity colliding with the wrong outline, which is far harder to spot than a config that fails to load.\n//\n// A config that says nothing is read off the size it gave instead - a `radius` describes a circle, and anything\n// else is a rectangle - so a game only has to name a shape for the one case its size cannot imply.\nfunction toShape(config: BodyConfig): number {\n\tconst shape = config.shape;\n\tif(shape === undefined) {\n\t\treturn config.radius !== undefined ? SHAPE_CIRCLE : SHAPE_RECTANGLE;\n\t} else if(!KNOWN_SHAPES.includes(shape)) {\n\t\tthrow new Error(`Unknown body shape: ${shape}`);\n\t}\n\n\treturn shape;\n}\n\nexport default bodyDefinition;\n","import bodyDefinition, { type BodyComponent } from './body-component';\nimport transformDefinition, { type TransformComponent } from './transform-component';\nimport velocityDefinition, { type VelocityComponent } from './velocity-component';\n\n// The component definitions this library supplies. A game spreads them into its own registry so its world\n// allocates their memory pools alongside its game specific components:\n//\n// const registry = { ...physicsRegistry, health: healthDefinition };\n// const world = new BaseWorld(registry);\n//\n// The names here are what the physics systems query on, so they must be kept as the registry keys.\nexport const physicsRegistry = {\n\ttransform: transformDefinition,\n\tvelocity: velocityDefinition,\n\tbody: bodyDefinition,\n};\n\n// The slice of a game's component map that the physics systems need. Declared as a type alias (not an\n// interface) so it satisfies the ECS's `ComponentMap` index signature. Systems here are generic over the\n// game's full component map and only require that it extends this.\nexport type PhysicsComponents = {\n\ttransform: TransformComponent\n\tvelocity: VelocityComponent\n\tbody: BodyComponent\n};\n\n// The same components as their concrete backing arrays, which is the form a system's update function sees them\n// in on the worker thread. `body` is optional here because it is optional on the query that moves entities -\n// movement does not need it, and it is only sent along when something is going to collide. The collidable\n// query requires it, so CollisionComponents narrows it back to guaranteed for the entities found through that.\n//\n// A game that hands extra components to its collision callback widens this with its own:\n//\n// type GameUpdateComponents = PhysicsUpdateComponents & { health?: Float32Array };\nexport type PhysicsUpdateComponents = {\n\ttransform: Float32Array\n\tvelocity: Float32Array\n\tbody?: Uint32Array\n};\n","// Geometry for the shapes a transform plus a body describe, all of them centred on (x, y) and turned `angle`\n// radians counter-clockwise. Everything here is pure number math on loose arguments rather than objects, so\n// the collision sweep can call it per pair without allocating.\n//\n// The three shapes are one idea in three settings: **an oriented core, grown by a radius**.\n//\n// rectangle - a width x height box, radius 0\n// circle - a point, radius width / 2\n// capsule - a segment along its facing, radius height / 2\n//\n// A circle is just a capsule whose segment has no length, so there are only two kinds of core - a box and a\n// segment - and therefore only three pair tests rather than one per combination of shapes. See shapesOverlap.\n\nimport { SHAPE_CAPSULE, SHAPE_CIRCLE, SHAPE_RECTANGLE } from '../components/body-component';\n\n// How far a capsule's core segment reaches from the centre, along the direction it faces. The caps are part\n// of the length, so a 40 x 10 capsule is 40 from end to end: 15 of segment either way plus a 5 radius cap.\n//\n// Clamped at 0, so a capsule told to be shorter than it is wide collapses to a circle of its own width rather\n// than turning itself inside out.\nexport function capsuleHalfLength(width: number, height: number): number {\n\treturn Math.max(0, (width - height) / 2);\n}\n\n// The radius of a shape's cap: half the diameter for a circle, half the thickness for a capsule, and nothing\n// at all for a rectangle, whose outline is its core.\n//\n// A circle takes its diameter from `width` and ignores `height` - loading one from a `radius` config sets both\n// to the same thing, so the two only ever disagree if a game writes the block itself.\nexport function shapeRadius(shape: number, width: number, height: number): number {\n\tif(shape === SHAPE_CIRCLE) {\n\t\treturn width / 2;\n\t} else if(shape === SHAPE_CAPSULE) {\n\t\treturn height / 2;\n\t}\n\n\treturn 0;\n}\n\n// Whether a shape encloses no area at all, and so can never overlap anything - not even something sitting\n// exactly on top of it. This is what keeps an entity the game never gave a size out of collisions, and which\n// of the two sizes matters depends on the shape: a circle is nothing without its width, while a capsule with\n// no thickness is a bare line however long it is.\nexport function shapeIsEmpty(shape: number, width: number, height: number): boolean {\n\tif(shape === SHAPE_CIRCLE) {\n\t\treturn width <= 0;\n\t} else if(shape === SHAPE_CAPSULE) {\n\t\treturn height <= 0;\n\t}\n\n\treturn width <= 0 || height <= 0;\n}\n\n// Half the width of the *axis aligned* box that fully contains a shape, whatever it is turned to - the box the\n// broadphase indexes it under, so it has to hold the whole shape or the tree would rule out its own candidates.\n//\n// A capsule always fits inside the rectangle its width and height describe, being that rectangle with its ends\n// rounded off, and so does a circle loaded from a `radius` (whose width and height agree). A circle is its\n// *width* alone though, so one whose height was written smaller by hand is taller than that rectangle - which is\n// why this is asked per shape rather than measured off the rectangle and trusted for everything.\nexport function shapeHalfWidth(shape: number, width: number, height: number, angle: number): number {\n\tif(shape === SHAPE_CIRCLE) {\n\t\t// A circle covers the same ground whichever way it is turned.\n\t\treturn width / 2;\n\t} else if(shape === SHAPE_CAPSULE) {\n\t\treturn Math.abs(Math.cos(angle)) * capsuleHalfLength(width, height) + height / 2;\n\t}\n\n\treturn boundsHalfWidth(width, height, angle);\n}\n// The same for the y axis: the capsule's segment leans the other way, and the circle is still a circle.\nexport function shapeHalfHeight(shape: number, width: number, height: number, angle: number): number {\n\tif(shape === SHAPE_CIRCLE) {\n\t\treturn width / 2;\n\t} else if(shape === SHAPE_CAPSULE) {\n\t\treturn Math.abs(Math.sin(angle)) * capsuleHalfLength(width, height) + height / 2;\n\t}\n\n\treturn boundsHalfHeight(width, height, angle);\n}\n\n// The least a shape reaches from its centre, whichever direction is asked - so a bar's thickness rather than\n// its length, and a circle's radius either way. Rotation cannot bring it any lower, which is what makes it\n// safe to take without an angle.\n//\n// This is what a sweep steps the path by: the stretch of a move a pair spends overlapping is at least their two\n// smallest reaches added together, so a step no bigger than one of them can never skip clean over it.\nexport function shapeMinHalfExtent(shape: number, width: number, height: number): number {\n\tif(shape === SHAPE_CIRCLE) {\n\t\treturn width / 2;\n\t} else if(shape === SHAPE_CAPSULE) {\n\t\treturn height / 2;\n\t}\n\n\treturn Math.min(width, height) / 2;\n}\n\n// Whether two shapes of any kind overlap.\n//\n// Because a circle is a capsule with no length, every pair falls into one of three cases, picked by a switch\n// over the two shape constants with no indirection in the way:\n//\n// rectangle + rectangle - two boxes with no radius between them, which is the separating axis test below\n// round + round - the gap between the two core segments has to be under the two radii added together\n// rectangle + round - the gap between the core segment and the box has to be under the one radius\n//\n// Touching exactly counts as *not* overlapping in all three, matching orientedBoxesOverlap: the comparisons\n// are strict, so a capsule whose cap grazes a wall has not hit it.\nexport function shapesOverlap(\n\taShape: number, aX: number, aY: number, aWidth: number, aHeight: number, aAngle: number,\n\tbShape: number, bX: number, bY: number, bWidth: number, bHeight: number, bAngle: number,\n): boolean {\n\tif(shapeIsEmpty(aShape, aWidth, aHeight) || shapeIsEmpty(bShape, bWidth, bHeight)) {\n\t\treturn false;\n\t}\n\n\tconst aIsRound = aShape !== SHAPE_RECTANGLE;\n\tconst bIsRound = bShape !== SHAPE_RECTANGLE;\n\n\tif(!aIsRound && !bIsRound) {\n\t\treturn orientedBoxesOverlap(aX, aY, aWidth, aHeight, aAngle, bX, bY, bWidth, bHeight, bAngle);\n\t} else if(aIsRound && !bIsRound) {\n\t\t// Overlapping is the same relation whichever way round it is asked, so the one mixed case is written once\n\t\t// with the box first and reached from the other side by swapping.\n\t\treturn shapesOverlap(\n\t\t\tbShape, bX, bY, bWidth, bHeight, bAngle,\n\t\t\taShape, aX, aY, aWidth, aHeight, aAngle,\n\t\t);\n\t}\n\n\t// b is round either way from here, so take its core segment now.\n\tconst bHalf = coreHalfLength(bShape, bWidth, bHeight);\n\tconst bCos = Math.cos(bAngle) * bHalf;\n\tconst bSin = Math.sin(bAngle) * bHalf;\n\tconst bRadius = shapeRadius(bShape, bWidth, bHeight);\n\n\tif(!aIsRound) {\n\t\treturn segmentBoxDistanceSquared(\n\t\t\tbX - bCos, bY - bSin, bX + bCos, bY + bSin,\n\t\t\taX, aY, aWidth, aHeight, aAngle,\n\t\t) < bRadius * bRadius;\n\t}\n\n\tconst aHalf = coreHalfLength(aShape, aWidth, aHeight);\n\tconst aCos = Math.cos(aAngle) * aHalf;\n\tconst aSin = Math.sin(aAngle) * aHalf;\n\tconst reach = shapeRadius(aShape, aWidth, aHeight) + bRadius;\n\n\treturn segmentSegmentDistanceSquared(\n\t\taX - aCos, aY - aSin, aX + aCos, aY + aSin,\n\t\tbX - bCos, bY - bSin, bX + bCos, bY + bSin,\n\t) < reach * reach;\n}\n\n// How far a shape's core reaches from its centre along the direction it faces: a capsule's segment, and nothing\n// for a circle (a point) or a rectangle (whose core is the whole box, handled separately).\nfunction coreHalfLength(shape: number, width: number, height: number): number {\n\treturn shape === SHAPE_CAPSULE ? capsuleHalfLength(width, height) : 0;\n}\n\n// Half the width of the *axis aligned* box that fully contains a rotated one. Turning a rectangle widens the\n// footprint it needs: each of its two sides contributes however much of itself now points along the x axis.\n// At angle 0 this is just width / 2, and at a quarter turn it is height / 2.\nexport function boundsHalfWidth(width: number, height: number, angle: number): number {\n\treturn (Math.abs(Math.cos(angle)) * width + Math.abs(Math.sin(angle)) * height) / 2;\n}\n// The same for the y axis, with the two sides' contributions swapped.\nexport function boundsHalfHeight(width: number, height: number, angle: number): number {\n\treturn (Math.abs(Math.sin(angle)) * width + Math.abs(Math.cos(angle)) * height) / 2;\n}\n\n// Whether two rotated boxes overlap, by the separating axis theorem: two convex shapes miss each other if and\n// only if some axis exists that their projections do not both cover. For a pair of boxes only four axes can\n// ever be that axis - the two each box's own sides face along - so testing those four settles it.\n//\n// Touching exactly counts as *not* overlapping: the projections meet but do not cross. A box with no area\n// never overlaps anything either, even sitting inside another one - it collapses to a point or a line, and\n// the theorem has nothing to separate. Together those are what keeps an entity the game never gave a size\n// out of collisions entirely.\nexport function orientedBoxesOverlap(\n\taX: number, aY: number, aWidth: number, aHeight: number, aAngle: number,\n\tbX: number, bY: number, bWidth: number, bHeight: number, bAngle: number,\n): boolean {\n\tif(aWidth <= 0 || aHeight <= 0 || bWidth <= 0 || bHeight <= 0) {\n\t\treturn false;\n\t}\n\n\tconst aCos = Math.cos(aAngle);\n\tconst aSin = Math.sin(aAngle);\n\tconst bCos = Math.cos(bAngle);\n\tconst bSin = Math.sin(bAngle);\n\n\tconst aHalfWidth = aWidth / 2;\n\tconst aHalfHeight = aHeight / 2;\n\tconst bHalfWidth = bWidth / 2;\n\tconst bHalfHeight = bHeight / 2;\n\n\t// Everything is measured relative to a's centre, so only the offset between the two centres matters.\n\tconst offsetX = bX - aX;\n\tconst offsetY = bY - aY;\n\n\t// a's own two axes, then b's. Each box's sides face along (cos, sin) and (-sin, cos) of its angle.\n\treturn !separatedAlong(aCos, aSin, offsetX, offsetY, aHalfWidth, aHalfHeight, aCos, aSin, bHalfWidth, bHalfHeight, bCos, bSin)\n\t\t&& !separatedAlong(-aSin, aCos, offsetX, offsetY, aHalfWidth, aHalfHeight, aCos, aSin, bHalfWidth, bHalfHeight, bCos, bSin)\n\t\t&& !separatedAlong(bCos, bSin, offsetX, offsetY, aHalfWidth, aHalfHeight, aCos, aSin, bHalfWidth, bHalfHeight, bCos, bSin)\n\t\t&& !separatedAlong(-bSin, bCos, offsetX, offsetY, aHalfWidth, aHalfHeight, aCos, aSin, bHalfWidth, bHalfHeight, bCos, bSin);\n}\n\n// Whether the two boxes' shadows on the axis (axisX, axisY) fail to reach each other. A box's shadow reaches\n// out from its centre by however far its own two half-sides project onto the axis, so the pair is separated\n// when the gap between the centres along that axis is at least the two reaches added together.\n//\n// The axes passed in are always a box's own sides, which are unit vectors, so the dot products below are\n// already true distances and nothing needs normalising.\nfunction separatedAlong(\n\taxisX: number, axisY: number,\n\toffsetX: number, offsetY: number,\n\taHalfWidth: number, aHalfHeight: number, aCos: number, aSin: number,\n\tbHalfWidth: number, bHalfHeight: number, bCos: number, bSin: number,\n): boolean {\n\tconst aReach = aHalfWidth * Math.abs(aCos * axisX + aSin * axisY) + aHalfHeight * Math.abs(-aSin * axisX + aCos * axisY);\n\tconst bReach = bHalfWidth * Math.abs(bCos * axisX + bSin * axisY) + bHalfHeight * Math.abs(-bSin * axisX + bCos * axisY);\n\n\treturn Math.abs(offsetX * axisX + offsetY * axisY) >= aReach + bReach;\n}\n\n// The squared distance between two line segments, each given as its two end points. Squared because that is\n// all the callers need - comparing it against a squared radius avoids a square root per pair.\n//\n// This is what settles every pair of round shapes: two capsules are their two segments grown by a radius each,\n// so they meet exactly when the segments come within the two radii of one another. A circle is a segment whose\n// ends are the same point, which this handles as the degenerate case rather than as a shape of its own.\n//\n// Walks the usual closest-point-between-segments argument: solve for the closest points on the two infinite\n// lines, then clamp back into the segments, re-solving the other parameter each time a clamp moves one. The\n// parallel case has no single solution, so it pins one end and lets the clamping find the rest.\nexport function segmentSegmentDistanceSquared(\n\taX1: number, aY1: number, aX2: number, aY2: number,\n\tbX1: number, bY1: number, bX2: number, bY2: number,\n): number {\n\tconst aDeltaX = aX2 - aX1;\n\tconst aDeltaY = aY2 - aY1;\n\tconst bDeltaX = bX2 - bX1;\n\tconst bDeltaY = bY2 - bY1;\n\tconst offsetX = aX1 - bX1;\n\tconst offsetY = aY1 - bY1;\n\n\tconst aLength = aDeltaX * aDeltaX + aDeltaY * aDeltaY;\n\tconst bLength = bDeltaX * bDeltaX + bDeltaY * bDeltaY;\n\tconst bOffset = bDeltaX * offsetX + bDeltaY * offsetY;\n\n\tlet aParam = 0;\n\tlet bParam = 0;\n\tif(aLength <= 0 && bLength <= 0) {\n\t\t// Two points - which is two circles, the most common pair of all.\n\t\treturn offsetX * offsetX + offsetY * offsetY;\n\t} else if(aLength <= 0) {\n\t\t// A circle against a capsule: only where it lands along the capsule's segment matters.\n\t\tbParam = clamp(bOffset / bLength);\n\t} else {\n\t\tconst aOffset = aDeltaX * offsetX + aDeltaY * offsetY;\n\t\tif(bLength <= 0) {\n\t\t\taParam = clamp(-aOffset / aLength);\n\t\t} else {\n\t\t\tconst along = aDeltaX * bDeltaX + aDeltaY * bDeltaY;\n\t\t\t// Zero exactly when the two segments are parallel, which is where pinning aParam at 0 comes in: every\n\t\t\t// point along one is the same distance from the other, so any of them is as good an answer.\n\t\t\tconst denominator = aLength * bLength - along * along;\n\t\t\tif(denominator !== 0) {\n\t\t\t\taParam = clamp((along * bOffset - aOffset * bLength) / denominator);\n\t\t\t}\n\n\t\t\tbParam = (along * aParam + bOffset) / bLength;\n\t\t\t// Clamping bParam moves the closest point on b, so where it sits along a has to be solved again.\n\t\t\tif(bParam < 0) {\n\t\t\t\tbParam = 0;\n\t\t\t\taParam = clamp(-aOffset / aLength);\n\t\t\t} else if(bParam > 1) {\n\t\t\t\tbParam = 1;\n\t\t\t\taParam = clamp((along - aOffset) / aLength);\n\t\t\t}\n\t\t}\n\t}\n\n\tconst gapX = (aX1 + aDeltaX * aParam) - (bX1 + bDeltaX * bParam);\n\tconst gapY = (aY1 + aDeltaY * aParam) - (bY1 + bDeltaY * bParam);\n\n\treturn gapX * gapX + gapY * gapY;\n}\n\n// The squared distance between a line segment and a rotated box. This is what settles a rectangle against a\n// circle or a capsule: the round shape is its segment grown by a radius, so it reaches the box exactly when the\n// segment comes within that radius of it.\n//\n// Measured in the box's own frame, where it is axis aligned and the whole thing reduces to a segment against a\n// rectangle sitting at the origin. Two disjoint convex shapes always have their closest pair at a corner of\n// one against the other, so once the crossing case is out of the way the answer is the nearest of: each end of\n// the segment to the box, and each corner of the box to the segment.\nexport function segmentBoxDistanceSquared(\n\tx1: number, y1: number, x2: number, y2: number,\n\tboxX: number, boxY: number, boxWidth: number, boxHeight: number, boxAngle: number,\n): number {\n\tconst cos = Math.cos(boxAngle);\n\tconst sin = Math.sin(boxAngle);\n\tconst halfWidth = boxWidth / 2;\n\tconst halfHeight = boxHeight / 2;\n\n\t// Both ends into the box's frame, so the box becomes the rectangle from (-halfWidth, -halfHeight) to\n\t// (halfWidth, halfHeight).\n\tconst firstOffsetX = x1 - boxX;\n\tconst firstOffsetY = y1 - boxY;\n\tconst secondOffsetX = x2 - boxX;\n\tconst secondOffsetY = y2 - boxY;\n\tconst localX1 = firstOffsetX * cos + firstOffsetY * sin;\n\tconst localY1 = -firstOffsetX * sin + firstOffsetY * cos;\n\tconst localX2 = secondOffsetX * cos + secondOffsetY * sin;\n\tconst localY2 = -secondOffsetX * sin + secondOffsetY * cos;\n\n\t// The crossing case has to be caught on its own: a segment straight through the middle of the box has both\n\t// its ends outside it and every corner some way off, so the nearest-pair search below would report a real\n\t// distance for two shapes that are already through each other.\n\tif(!segmentMissesBox(localX1, localY1, localX2, localY2, halfWidth, halfHeight)) {\n\t\treturn 0;\n\t}\n\n\treturn Math.min(\n\t\t// Each end of the segment against the box...\n\t\tpointBoxDistanceSquared(localX1, localY1, halfWidth, halfHeight),\n\t\tpointBoxDistanceSquared(localX2, localY2, halfWidth, halfHeight),\n\t\t// ...and each corner of the box against the segment, which is where a segment running past a box at an\n\t\t// angle comes closest to it.\n\t\tpointSegmentDistanceSquared(-halfWidth, -halfHeight, localX1, localY1, localX2, localY2),\n\t\tpointSegmentDistanceSquared(halfWidth, -halfHeight, localX1, localY1, localX2, localY2),\n\t\tpointSegmentDistanceSquared(halfWidth, halfHeight, localX1, localY1, localX2, localY2),\n\t\tpointSegmentDistanceSquared(-halfWidth, halfHeight, localX1, localY1, localX2, localY2),\n\t);\n}\n\n// The squared distance from a point to a line segment: how far along the segment the point falls, clamped to\n// its ends, and then the gap from there.\nexport function pointSegmentDistanceSquared(x: number, y: number, x1: number, y1: number, x2: number, y2: number): number {\n\tconst deltaX = x2 - x1;\n\tconst deltaY = y2 - y1;\n\tconst length = deltaX * deltaX + deltaY * deltaY;\n\n\t// A segment with no length is just its own start point.\n\tconst along = length > 0 ? clamp(((x - x1) * deltaX + (y - y1) * deltaY) / length) : 0;\n\tconst gapX = x - (x1 + deltaX * along);\n\tconst gapY = y - (y1 + deltaY * along);\n\n\treturn gapX * gapX + gapY * gapY;\n}\n\n// Whether an axis aligned box at the origin and a segment fail to reach each other, by the same separating axis\n// argument as orientedBoxesOverlap: the box's own two axes, plus the one the segment faces along. Touching\n// exactly counts as reaching, so a grazing segment is measured as 0 away rather than as a near miss.\nfunction segmentMissesBox(x1: number, y1: number, x2: number, y2: number, halfWidth: number, halfHeight: number): boolean {\n\tif(Math.max(x1, x2) < -halfWidth || Math.min(x1, x2) > halfWidth) {\n\t\treturn true;\n\t} else if(Math.max(y1, y2) < -halfHeight || Math.min(y1, y2) > halfHeight) {\n\t\treturn true;\n\t}\n\n\t// The segment's own axis. Both of its ends project to the same place on the normal, so one is enough - and\n\t// for a segment with no length the normal is (0, 0), which never separates and correctly leaves the two box\n\t// axes above to decide whether the point is inside.\n\tconst normalX = -(y2 - y1);\n\tconst normalY = x2 - x1;\n\n\treturn Math.abs(normalX * x1 + normalY * y1) > halfWidth * Math.abs(normalX) + halfHeight * Math.abs(normalY);\n}\n\n// The squared distance from a point to an axis aligned box at the origin: clamp the point onto the box and\n// measure what is left over. 0 for a point inside it.\nfunction pointBoxDistanceSquared(x: number, y: number, halfWidth: number, halfHeight: number): number {\n\tconst gapX = Math.abs(x) - halfWidth;\n\tconst gapY = Math.abs(y) - halfHeight;\n\tconst outsideX = gapX > 0 ? gapX : 0;\n\tconst outsideY = gapY > 0 ? gapY : 0;\n\n\treturn outsideX * outsideX + outsideY * outsideY;\n}\n\nfunction clamp(value: number): number {\n\tif(value < 0) {\n\t\treturn 0;\n\t} else if(value > 1) {\n\t\treturn 1;\n\t}\n\n\treturn value;\n}\n","import Flatbush from 'flatbush';\nimport type { ComponentMap, ComponentSystemCallbacks, ComponentSystemWorld, EntityQueryComponents, EntityUpdateComponents } from '@daneren2005/shared-memory-ecs';\nimport type { PhysicsUpdateComponents } from '../components/registry';\nimport { BODY_CATEGORY_INDEX, BODY_MASK_INDEX, BODY_SHAPE_INDEX } from '../components/body-component';\nimport { TRANSFORM_ANGLE_INDEX, TRANSFORM_HEIGHT_INDEX, TRANSFORM_WIDTH_INDEX, TRANSFORM_X_INDEX, TRANSFORM_Y_INDEX } from '../components/transform-component';\nimport { VELOCITY_X_INDEX, VELOCITY_Y_INDEX } from '../components/velocity-component';\nimport { shapeHalfHeight, shapeHalfWidth, shapeIsEmpty, shapeMinHalfExtent, shapesOverlap } from '../math/shapes';\n\n// How close to the moment of contact a sweep has to get before it settles for it, in world units. Small enough\n// that the gap it leaves an entity resting against another is far below anything a game would draw, and large\n// enough that the halving below reaches it in a handful of steps.\nconst CONTACT_TOLERANCE = 1e-4;\n// A cap on that halving, so a move over a huge distance cannot turn into an unbounded number of overlap tests.\n// Twenty of them narrow the coarse step they start from by a millionth, which reaches the tolerance above from\n// any step an entity up to a hundred units across could take - and only an entity that is actually blocked ever\n// spends them.\nconst MAX_REFINEMENTS = 20;\n// ...and on the coarse walk along the path that finds which step contact falls in. Below the cap the step is\n// fine enough that nothing can be stepped clean over; past it - an entity covering many times its own size in\n// one run - something very thin can still be gone straight through, which a fixed `deltaBetweenRuns` is the\n// answer to rather than a budget no run can afford.\nconst MAX_STEPS = 64;\n\n// The name PhysicsSystem registers its extra query under, and the key the broadphase reads it back out of.\n// It holds everything with a transform and a body rather than only the entities the system moves, so a moving\n// entity can still run into a station, a wall or anything else that has no velocity of its own.\nexport const COLLIDABLE_QUERY = 'collidable';\n\n// The blocks a collision callback is handed for the entity that was run into. `transform` and `body` are the\n// guarantees - they are what the collidable query requires - while `velocity` and everything the game listed\n// in `optional` are there only if that entity has them.\nexport type CollisionComponents<T extends PhysicsUpdateComponents> = Partial<T> & {\n\ttransform: Float32Array\n\tbody: Uint32Array\n};\n\n// The entity that did the running into: the one being updated, so everything the system asked for is there.\nexport interface MovingEntity<T extends PhysicsUpdateComponents> {\n\tentityId: number\n\tcomponents: T\n}\n\n// The entity on the other side of a collision, which may be something the system never moves at all.\nexport interface CollisionEntity<T extends PhysicsUpdateComponents> {\n\tentityId: number\n\tcomponents: CollisionComponents<T>\n}\n\n// What a game runs when one entity moves into another. It is called on the same thread the physics update\n// runs on, so it must be a plain function over the raw blocks - no entities, no world - and it reaches the\n// main thread only through `callbacks` (which report a changed property, a death, or an entity to spawn).\n//\n// It is called *per entity*, right after that entity has moved: `self` is the one that just moved and `other`\n// is what it has ended up on top of. Two moving entities that run into each other therefore get one call\n// each, with the roles swapped, which is what lets a callback act on itself - bouncing, taking damage - and\n// leave the other side to its own call. An entity with no velocity is never `self`, since the system never\n// moves it, but it is still found as `other`.\n//\n// `queries` is passed straight through so a callback that needs more than the two entities in front of it can\n// still walk the full collidable list, e.g. to credit a third entity for the kill.\nexport type CollisionFunction<C extends ComponentMap, T extends PhysicsUpdateComponents & EntityUpdateComponents<C>, W extends ComponentSystemWorld = ComponentSystemWorld> = (\n\tworld: W,\n\tself: MovingEntity<T>,\n\tother: CollisionEntity<T>,\n\tqueries: EntityQueryComponents<C>,\n\tcallbacks: ComponentSystemCallbacks<C>,\n) => void;\n\n// How much of a move an entity is actually allowed to make, and what stopped it there.\n//\n// `fraction` is how far along the requested move it got: 1 for a clear path, 0 for an entity already pressed up\n// against something, and whatever is in between for one that came to rest part way.\n//\n// `blocking` is what it came to rest *against*, which the overlap test at that resting place will not report -\n// the whole point of stopping there is that the two are touching rather than through each other. More than one\n// entry means it wedged between them at the same moment, not that it should have stopped at the first.\nexport interface SweepResult<T extends PhysicsUpdateComponents> {\n\tfraction: number\n\tblocking: Array<CollisionEntity<T>>\n}\n\n// What `blocking` is for the sweeps that find nothing in the way, which is most of them: one shared empty array\n// rather than one per entity per run. Read-only to a caller by nature - there is nothing in it to iterate.\nconst NOTHING_BLOCKING: Array<never> = [];\n\n// Everything about the entity doing the searching, read out of its blocks once so that a sweep testing the same\n// pair a dozen times over does not read them a dozen times over. x/y are where it started the move.\ninterface Searcher {\n\tentityId: number\n\tshape: number\n\tx: number\n\ty: number\n\twidth: number\n\theight: number\n\tangle: number\n\thalfWidth: number\n\thalfHeight: number\n\tcategory: number\n\tmask: number\n}\n\n// One category's worth of the broadphase: every collidable entity that collides *as* that category, and an\n// R-tree over just those. A searching entity only looks in the buckets its own mask accepts, so a projectile\n// that can only hit units never walks the tree the terrain is in.\n//\n// Bucketed on the whole category *value* rather than a bit at a time, so an entity that collides as two things\n// at once still lives in exactly one bucket and can never be found twice by one search.\ninterface CategoryBucket<T extends PhysicsUpdateComponents> {\n\tcategory: number\n\tentries: Array<CollisionEntity<T>>\n\tindex: Flatbush\n}\n\n// An R-tree over every collidable entity, built once at the top of a run and then asked, entity by entity as\n// each one moves, what it might have hit.\n//\n// Everything is indexed where it stood at the *start* of the run, because that is the only moment all of the\n// entities agree on - by the time the third entity moves, the first two have already gone somewhere else. To\n// stay a true superset of what can actually collide, each box is grown by however far that entity could\n// travel before the run is out. The growth goes both ways rather than along the heading: a collision\n// callback is free to turn an entity around before it has had its own move, and a box grown only forwards\n// would then be pointing the wrong way.\n//\n// The tree is split by collide category rather than being one index over everything, so most of what an entity\n// cannot hit is ruled out a whole subtree at a time instead of one candidate at a time. Nothing is left empty:\n// a category with no collidable entities has no bucket at all, which also means Flatbush is never asked to\n// build an index over zero items.\nexport default class CollisionBroadphase<T extends PhysicsUpdateComponents> {\n\tprivate buckets: Array<CategoryBucket<T>> = [];\n\n\tconstructor(entities: Array<{ entityId: number, components: EntityUpdateComponents }>, seconds: number) {\n\t\t// Boxes are collected per category first because Flatbush has to be told how many items it will hold up\n\t\t// front, and how many that is only becomes clear once everything that cannot collide has been dropped.\n\t\tconst pending = new Map<number, { entries: Array<CollisionEntity<T>>, bounds: Array<number> }>();\n\n\t\tfor(const entity of entities) {\n\t\t\t// The ECS types query blocks generically as ComponentTypedArray since it cannot know what any given\n\t\t\t// game registered; narrow them to the concrete arrays the definitions allocate here, once, so neither\n\t\t\t// the search below nor the game's callback needs a cast of its own.\n\t\t\tconst components = entity.components as CollisionComponents<T>;\n\t\t\tconst transform = components.transform;\n\t\t\tconst body = components.body;\n\t\t\tif(!transform || !body) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// No mask can ever name category 0, so an entity that collides as nothing can never be the other half\n\t\t\t// of a collision either - dropping it here is exact rather than an approximation, and it is what makes\n\t\t\t// `collideCategory: 0` a way to opt an entity out of collisions entirely.\n\t\t\tconst category = body[BODY_CATEGORY_INDEX];\n\t\t\tif(category === 0) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst shape = body[BODY_SHAPE_INDEX];\n\t\t\tconst width = transform[TRANSFORM_WIDTH_INDEX];\n\t\t\tconst height = transform[TRANSFORM_HEIGHT_INDEX];\n\t\t\t// A shape with no area can never overlap anything, so dropping it here is exact rather than an\n\t\t\t// approximation - and it keeps an entity the game never gave a size out of the tree entirely.\n\t\t\tif(shapeIsEmpty(shape, width, height)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst angle = transform[TRANSFORM_ANGLE_INDEX];\n\t\t\tconst halfWidth = shapeHalfWidth(shape, width, height, angle);\n\t\t\tconst halfHeight = shapeHalfHeight(shape, width, height, angle);\n\n\t\t\tconst velocity = components.velocity;\n\t\t\tconst travelX = velocity ? Math.abs(velocity[VELOCITY_X_INDEX]) * seconds : 0;\n\t\t\tconst travelY = velocity ? Math.abs(velocity[VELOCITY_Y_INDEX]) * seconds : 0;\n\n\t\t\tconst x = transform[TRANSFORM_X_INDEX];\n\t\t\tconst y = transform[TRANSFORM_Y_INDEX];\n\n\t\t\tlet bucket = pending.get(category);\n\t\t\tif(!bucket) {\n\t\t\t\tbucket = { entries: [], bounds: [] };\n\t\t\t\tpending.set(category, bucket);\n\t\t\t}\n\n\t\t\tbucket.entries.push({ entityId: entity.entityId, components });\n\t\t\tbucket.bounds.push(\n\t\t\t\tx - halfWidth - travelX,\n\t\t\t\ty - halfHeight - travelY,\n\t\t\t\tx + halfWidth + travelX,\n\t\t\t\ty + halfHeight + travelY,\n\t\t\t);\n\t\t}\n\n\t\tfor(const [category, { entries, bounds }] of pending) {\n\t\t\tconst index = new Flatbush(entries.length);\n\t\t\tfor(let i = 0; i < entries.length; i++) {\n\t\t\t\tindex.add(bounds[i * 4], bounds[i * 4 + 1], bounds[i * 4 + 2], bounds[i * 4 + 3]);\n\t\t\t}\n\t\t\tindex.finish();\n\n\t\t\tthis.buckets.push({ category, entries, index });\n\t\t}\n\t}\n\n\t// Runs `handle` for everything `self` is really overlapping, having just moved.\n\t//\n\t// The tree narrows the field to whatever is anywhere near, then each of those is put through the full\n\t// rotated box test against where the two entities are *now* - which for an entity that has not had its own\n\t// move yet is still where it started. That is the price of collisions happening as each entity moves\n\t// rather than all at the end, and it evens out: the pair is looked at again from the other side once that\n\t// entity moves too.\n\tforEachOverlapping(self: MovingEntity<T>, handle: (other: CollisionEntity<T>) => void): void {\n\t\tconst searcher = toSearcher(self);\n\t\tif(!searcher) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst { x, y, halfWidth, halfHeight } = searcher;\n\t\t// Searched with where this entity has actually ended up, ungrown: every box in the tree already allows\n\t\t// for its own entity's move, so allowing for this one's a second time would only widen the net.\n\t\tthis.forEachCandidate(searcher, x - halfWidth, y - halfHeight, x + halfWidth, y + halfHeight, other => {\n\t\t\tif(overlapsAt(searcher, x, y, other)) {\n\t\t\t\thandle(other);\n\t\t\t}\n\t\t});\n\t}\n\n\t// How much of the move (moveX, moveY) `self` may take before it runs into something, so that an entity comes\n\t// to rest against what is in its way instead of ending up inside it.\n\t//\n\t// Every candidate anywhere along the path is collected first and then they are all asked together, which is\n\t// what keeps an entity from stopping inside the near one because the far one happened to be looked at first:\n\t// there is one answer for the whole move, and it is the earliest contact of the lot.\n\t//\n\t// Contact is found by walking the path and then halving, rather than solved: three shapes at any rotation to\n\t// one another have no single formula for when a pair first touches, while the overlap test the narrowphase\n\t// already has answers it for any pair anywhere. The result is always taken from a position that tested\n\t// clear, so wherever the entity is put down it is genuinely not inside anything.\n\tsweep(self: MovingEntity<T>, moveX: number, moveY: number): SweepResult<T> {\n\t\tconst distance = Math.sqrt(moveX * moveX + moveY * moveY);\n\t\tconst searcher = toSearcher(self);\n\t\t// Nothing to work out for an entity that is not going anywhere, or that does not collide at all.\n\t\tif(!searcher || distance === 0) {\n\t\t\treturn { fraction: 1, blocking: NOTHING_BLOCKING };\n\t\t}\n\n\t\tconst { x, y, halfWidth, halfHeight } = searcher;\n\t\tconst endX = x + moveX;\n\t\tconst endY = y + moveY;\n\n\t\t// Searched over the whole path rather than only where the move ends, because something can be run\n\t\t// straight through: an entity that moves further in a run than the thing in front of it is thick would\n\t\t// have left it behind by the time the move is out, and it still has to be stopped by it.\n\t\tconst candidates: Array<CollisionEntity<T>> = [];\n\t\t// How finely that path has to be walked, set by the thinnest thing on either side of the pair.\n\t\tlet thinnest = shapeMinHalfExtent(searcher.shape, searcher.width, searcher.height);\n\t\tthis.forEachCandidate(\n\t\t\tsearcher,\n\t\t\tMath.min(x, endX) - halfWidth, Math.min(y, endY) - halfHeight,\n\t\t\tMath.max(x, endX) + halfWidth, Math.max(y, endY) + halfHeight,\n\t\t\tother => {\n\t\t\t\t// Already inside it before the move began - something the game put there, or another system pushed\n\t\t\t\t// it into. It cannot be what *this* move ran into, and blocking on it would pin the entity inside\n\t\t\t\t// it for good with no way back out, so the move is let through and the pair is left to the overlap\n\t\t\t\t// callback that follows it.\n\t\t\t\tif(overlapsAt(searcher, x, y, other)) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tcandidates.push(other);\n\n\t\t\t\tconst otherBody = other.components.body;\n\t\t\t\tconst otherTransform = other.components.transform;\n\t\t\t\tthinnest = Math.min(thinnest, shapeMinHalfExtent(\n\t\t\t\t\totherBody[BODY_SHAPE_INDEX],\n\t\t\t\t\totherTransform[TRANSFORM_WIDTH_INDEX],\n\t\t\t\t\totherTransform[TRANSFORM_HEIGHT_INDEX],\n\t\t\t\t));\n\t\t\t},\n\t\t);\n\t\tif(candidates.length === 0) {\n\t\t\treturn { fraction: 1, blocking: NOTHING_BLOCKING };\n\t\t}\n\n\t\t// `clear` is always a fraction the entity is known to fit at, and `blocked` the earliest one it is known\n\t\t// not to. The walk below moves them towards each other until they are close enough to settle for.\n\t\tlet clear = 0;\n\t\tlet blocked = -1;\n\t\tconst steps = Math.min(MAX_STEPS, Math.max(1, Math.ceil(distance / thinnest)));\n\t\tfor(let step = 1; step <= steps; step++) {\n\t\t\tconst fraction = step / steps;\n\t\t\tif(this.blockedAt(searcher, candidates, moveX, moveY, fraction)) {\n\t\t\t\tblocked = fraction;\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tclear = fraction;\n\t\t}\n\t\t// The whole path tested clear, so nothing along it is in the way after all.\n\t\tif(blocked < 0) {\n\t\t\treturn { fraction: 1, blocking: NOTHING_BLOCKING };\n\t\t}\n\n\t\tconst tolerance = CONTACT_TOLERANCE / distance;\n\t\tfor(let i = 0; i < MAX_REFINEMENTS && blocked - clear > tolerance; i++) {\n\t\t\tconst middle = (clear + blocked) / 2;\n\t\t\tif(this.blockedAt(searcher, candidates, moveX, moveY, middle)) {\n\t\t\t\tblocked = middle;\n\t\t\t} else {\n\t\t\t\tclear = middle;\n\t\t\t}\n\t\t}\n\n\t\t// Whatever is in the way at the first position that was not clear: the entity has come to rest a hair\n\t\t// short of exactly there, so this is what it is up against.\n\t\tconst blocking: Array<CollisionEntity<T>> = [];\n\t\tconst blockedX = x + moveX * blocked;\n\t\tconst blockedY = y + moveY * blocked;\n\t\tfor(const other of candidates) {\n\t\t\tif(overlapsAt(searcher, blockedX, blockedY, other)) {\n\t\t\t\tblocking.push(other);\n\t\t\t}\n\t\t}\n\n\t\t// An entity already up against something is left exactly where it is rather than crawling the last\n\t\t// fraction of a tolerance forwards every run, which would report a position change for no visible move.\n\t\treturn { fraction: clear * distance > CONTACT_TOLERANCE ? clear : 0, blocking };\n\t}\n\n\t// Whether `self` would be inside any of `candidates` that far along its move.\n\tprivate blockedAt(searcher: Searcher, candidates: Array<CollisionEntity<T>>, moveX: number, moveY: number, fraction: number): boolean {\n\t\tconst x = searcher.x + moveX * fraction;\n\t\tconst y = searcher.y + moveY * fraction;\n\t\tfor(const other of candidates) {\n\t\t\tif(overlapsAt(searcher, x, y, other)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t// Runs `handle` for every collidable entity in the box that `searcher` is allowed to collide with at all -\n\t// the tree search and the category rules, with no geometry beyond the boxes themselves.\n\t//\n\t// The collide masks are applied before any shape test rather than after: a pair that can never collide is\n\t// settled by two ANDs instead of a full separating-axis test.\n\tprivate forEachCandidate(searcher: Searcher, minX: number, minY: number, maxX: number, maxY: number, handle: (other: CollisionEntity<T>) => void): void {\n\t\tfor(const bucket of this.buckets) {\n\t\t\t// Half of canCollide, settled once for everything in the bucket rather than once per candidate:\n\t\t\t// they all collide as the same category, so either this entity's mask accepts the lot of them or it\n\t\t\t// accepts none of them.\n\t\t\tif((searcher.mask & bucket.category) === 0) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tfor(const found of bucket.index.search(minX, minY, maxX, maxY)) {\n\t\t\t\tconst other = bucket.entries[found];\n\t\t\t\t// Every collidable entity is in some bucket, including this one.\n\t\t\t\tif(other.entityId === searcher.entityId) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// The other half of canCollide, which does have to be per candidate: a bucket shares one category\n\t\t\t\t// but every entity in it carries its own mask.\n\t\t\t\tif((other.components.body[BODY_MASK_INDEX] & searcher.category) === 0) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\thandle(other);\n\t\t\t}\n\t\t}\n\t}\n}\n\n// Reads out what a searching entity needs to be measured by, or undefined for one that cannot collide at all.\nfunction toSearcher<T extends PhysicsUpdateComponents>(self: MovingEntity<T>): Searcher | undefined {\n\t// An entity with no body does not collide at all. It can still be moved by the system - the query that\n\t// moves entities only asks for the body, it does not require it.\n\tconst body = self.components.body;\n\tif(!body) {\n\t\treturn undefined;\n\t}\n\n\t// Willing to run into nothing at all, so there is no bucket worth searching. Note this only stops it being\n\t// the entity that *finds* a collision: something whose mask accepts it can still find this one, and that is\n\t// the asymmetry the symmetric canCollide rule then rules out.\n\tconst mask = body[BODY_MASK_INDEX];\n\tif(mask === 0) {\n\t\treturn undefined;\n\t}\n\n\tconst transform = self.components.transform;\n\tconst shape = body[BODY_SHAPE_INDEX];\n\tconst width = transform[TRANSFORM_WIDTH_INDEX];\n\tconst height = transform[TRANSFORM_HEIGHT_INDEX];\n\tif(shapeIsEmpty(shape, width, height)) {\n\t\treturn undefined;\n\t}\n\n\tconst angle = transform[TRANSFORM_ANGLE_INDEX];\n\n\treturn {\n\t\tentityId: self.entityId,\n\t\tshape,\n\t\tx: transform[TRANSFORM_X_INDEX],\n\t\ty: transform[TRANSFORM_Y_INDEX],\n\t\twidth,\n\t\theight,\n\t\tangle,\n\t\thalfWidth: shapeHalfWidth(shape, width, height, angle),\n\t\thalfHeight: shapeHalfHeight(shape, width, height, angle),\n\t\tcategory: body[BODY_CATEGORY_INDEX],\n\t\tmask,\n\t};\n}\n\n// Whether the searching entity, put down at (x, y) rather than wherever its own block says it is, overlaps\n// `other`. Taking the position as arguments is what lets a sweep try a whole path without writing any of it.\nfunction overlapsAt<T extends PhysicsUpdateComponents>(searcher: Searcher, x: number, y: number, other: CollisionEntity<T>): boolean {\n\tconst transform = other.components.transform;\n\n\treturn shapesOverlap(\n\t\tsearcher.shape, x, y, searcher.width, searcher.height, searcher.angle,\n\t\tother.components.body[BODY_SHAPE_INDEX],\n\t\ttransform[TRANSFORM_X_INDEX], transform[TRANSFORM_Y_INDEX],\n\t\ttransform[TRANSFORM_WIDTH_INDEX], transform[TRANSFORM_HEIGHT_INDEX], transform[TRANSFORM_ANGLE_INDEX],\n\t);\n}\n","import { addAtomicFloat32 } from '@daneren2005/shared-memory-objects';\nimport type { ComponentMap, ComponentSystemCallbacks, ComponentSystemWorld, EntityQueryComponents, EntityUpdateComponents, EntityUpdateFunction } from '@daneren2005/shared-memory-ecs';\nimport type { PhysicsComponents, PhysicsUpdateComponents } from '../components/registry';\nimport { TRANSFORM_X_INDEX, TRANSFORM_Y_INDEX } from '../components/transform-component';\nimport { VELOCITY_X_INDEX, VELOCITY_Y_INDEX } from '../components/velocity-component';\nimport CollisionBroadphase, { COLLIDABLE_QUERY, type CollisionEntity, type CollisionFunction, type MovingEntity } from './collision';\n\n// What a game asks createPhysicsUpdate for on top of plain movement.\nexport interface PhysicsUpdateOptions<\n\tC extends ComponentMap,\n\tT extends PhysicsUpdateComponents & EntityUpdateComponents<C>,\n\tW extends ComponentSystemWorld = ComponentSystemWorld,\n> {\n\t// Run for each entity that has moved into another one, straight after its own move.\n\tonCollision?: CollisionFunction<C, T, W>\n\t// Extra components to hand the update - and so the collision callback - beyond the transform and velocity\n\t// physics itself needs. A game that wants to take damage on collision lists its health component here so\n\t// that block travels to the worker alongside the transform. They are optional per entity: an entity that\n\t// does not have one is still moved, and still collides, it just arrives without that block.\n\toptional?: Array<keyof C & string>\n}\n\n// What PhysicsSystem needs to know about an update function to set up the same components and queries the\n// function expects. Stamped on by createPhysicsUpdate so a game declares its collision setup in the one\n// module both its worker file and its main thread system import, with no chance of the two drifting apart.\nexport interface PhysicsUpdateMetadata<C extends ComponentMap> {\n\tcollision: boolean\n\toptional: Array<keyof C & string>\n}\n\nexport type PhysicsUpdateFunction<\n\tC extends ComponentMap,\n\tT extends PhysicsUpdateComponents & EntityUpdateComponents<C>,\n\tW extends ComponentSystemWorld = ComponentSystemWorld,\n> = EntityUpdateFunction<C, T, W> & {\n\tphysics: PhysicsUpdateMetadata<C>\n};\n\n// Builds the update function a game runs physics through when it wants collision callbacks as well as\n// movement. The result is a plain EntityUpdateFunction, so it goes to both backends the same way the bare\n// physicsUpdate does - handed to createComponentWorker in the game's worker file, and to PhysicsSystem as its\n// `updateFunction` for the in-process fallback:\n//\n// // game-physics-update.ts - imported by both the worker file and the main thread system\n// export const gamePhysicsUpdate = createPhysicsUpdate<Components, GameUpdateComponents>({\n// optional: ['health'],\n// onCollision(world, a, b, queries, callbacks) { ... },\n// });\n//\n// A callback cannot be sent to a worker (functions do not survive postMessage), which is why it is baked in\n// here rather than passed to PhysicsSystem: the worker file imports this module and gets the callback with it.\nexport function createPhysicsUpdate<\n\t// Constrained to a map that has the physics components in it rather than to any map at all, because the move\n\t// reports the position it lands on back through `entityComponentChanged` - which types the value it takes\n\t// against the game's own component map, and so has to be able to see the transform in there. Every game\n\t// already spreads physicsRegistry into its own, which is the same requirement PhysicsSystem makes.\n\tC extends ComponentMap & PhysicsComponents = PhysicsComponents,\n\tT extends PhysicsUpdateComponents & EntityUpdateComponents<C> = PhysicsUpdateComponents & EntityUpdateComponents<C>,\n\tW extends ComponentSystemWorld = ComponentSystemWorld,\n>(options: PhysicsUpdateOptions<C, T, W> = {}): PhysicsUpdateFunction<C, T, W> {\n\tconst { onCollision, optional = [] } = options;\n\n\t// Built by preRun and read back by each entity update that follows it, which is the whole reason it can be\n\t// a plain closure variable: a run is one unbroken pass - preRun, then every entity, then done - so there is\n\t// never a second run part way through this one to overwrite it.\n\tlet broadphase: CollisionBroadphase<T> | undefined;\n\n\tconst update: PhysicsUpdateFunction<C, T, W> = Object.assign(\n\t\t(world: W, entityId: number, components: T, queries: EntityQueryComponents<C>, callbacks: ComponentSystemCallbacks<C>) => {\n\t\t\tconst self: MovingEntity<T> = { entityId, components };\n\t\t\tconst seconds = world.elapsedTime / 1000;\n\t\t\tlet moveX = components.velocity[VELOCITY_X_INDEX] * seconds;\n\t\t\tlet moveY = components.velocity[VELOCITY_Y_INDEX] * seconds;\n\n\t\t\t// Where the entity may actually get to is worked out here, off the blocks rather than in them: the\n\t\t\t// position is only written once the answer is known, so an entity is never inside another one - not\n\t\t\t// even for the moment it would take to push it back out.\n\t\t\tlet blocking: Array<CollisionEntity<T>> | undefined;\n\t\t\tif(broadphase) {\n\t\t\t\tconst sweep = broadphase.sweep(self, moveX, moveY);\n\t\t\t\tmoveX *= sweep.fraction;\n\t\t\t\tmoveY *= sweep.fraction;\n\t\t\t\tblocking = sweep.blocking;\n\t\t\t}\n\n\t\t\tmove(entityId, components.transform, moveX, moveY, callbacks);\n\n\t\t\tif(!onCollision || !broadphase) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Collisions are reported here, straight after the entity has been put down, so a callback acts on\n\t\t\t// where it has actually ended up rather than where it was - and a bounce that flips its velocity is\n\t\t\t// applied by the next run's move. Only the entity that moved gets the call; whatever it hit gets its\n\t\t\t// own when its turn comes, or never, if it is something the system does not move.\n\t\t\tbroadphase.forEachOverlapping(self, other => {\n\t\t\t\tonCollision(world, self, other, queries, callbacks);\n\t\t\t});\n\n\t\t\t// Then whatever the move was stopped *against*, which the overlap check above cannot find by design:\n\t\t\t// the entity was put down touching it rather than through it. The two lists never share an entry, so\n\t\t\t// nothing here has already had its callback.\n\t\t\tif(blocking) {\n\t\t\t\tfor(const other of blocking) {\n\t\t\t\t\tonCollision(world, self, other, queries, callbacks);\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\tphysics: {\n\t\t\t\tcollision: onCollision !== undefined,\n\t\t\t\toptional,\n\t\t\t},\n\t\t},\n\t);\n\n\tif(onCollision) {\n\t\t// preRun is the one hook that sees every entity at once, which makes it the only place the tree can be\n\t\t// built from a single consistent moment - before any of this run's movement has happened.\n\t\tupdate.preRun = (world, entities, queries) => {\n\t\t\tbroadphase = new CollisionBroadphase<T>(queries[COLLIDABLE_QUERY] ?? [], world.elapsedTime / 1000);\n\t\t};\n\t}\n\n\treturn update;\n}\n\n// Shared update logic used by both ComponentSystem backends: the main-thread ComponentWebWorker runs it\n// directly (PhysicsSystem passes it as `updateFunction`), while a game's worker file imports it and hands it\n// to createComponentWorker. Keeping it in one place means both paths run identical logic.\n//\n// This is movement only. A game that also wants collision callbacks builds its update with\n// createPhysicsUpdate instead, which adds them around this same move.\nexport const physicsUpdate: EntityUpdateFunction<PhysicsComponents, PhysicsUpdateComponents> = (world, entityId, components, queries, callbacks) => {\n\t// elapsedTime is in milliseconds (what BaseWorld#update is driven with), while velocity is in world units\n\t// per second - so convert before integrating.\n\tconst seconds = world.elapsedTime / 1000;\n\n\tmove(\n\t\tentityId,\n\t\tcomponents.transform,\n\t\tcomponents.velocity[VELOCITY_X_INDEX] * seconds,\n\t\tcomponents.velocity[VELOCITY_Y_INDEX] * seconds,\n\t\tcallbacks,\n\t);\n};\n\n// Applies a move that has already been decided on - by the integration above, or by what a sweep allowed of it -\n// and reports where the entity ended up. Because the blocks live in a SharedArrayBuffer, writing them here is\n// immediately visible through `entity.components.transform` on the main thread.\n//\n// This is the one place the position is written, and it is only reached once the final position is known: the\n// distance is worked out on plain numbers first so that nothing ever reads a transform mid-decision.\n//\n// The move is applied with addAtomicFloat32 rather than `+=`: the position is shared memory that another\n// thread (a game's own system, or a later physics system) may be adding to at the same moment, and a plain\n// read-modify-write would silently drop one of the two moves. Its `max` cap is not wanted here - physics\n// does not bound how far something may travel - so it is passed Infinity, leaving a plain atomic add. It is\n// also why the value reported back is read out of the block afterwards rather than being worked out here: with\n// another thread adding to the same position, the block is the only thing that knows the total.\n//\n// An axis that did not move is not reported, which is what keeps a world of still entities quiet - and an entity\n// pressed up against something it cannot move past reports nothing at all rather than a change per run.\nfunction move(entityId: number, transform: Float32Array, moveX: number, moveY: number, callbacks: TransformCallbacks): void {\n\tif(moveX !== 0) {\n\t\taddAtomicFloat32(transform, TRANSFORM_X_INDEX, moveX, Infinity);\n\t\tcallbacks.entityComponentChanged(entityId, 'transform', 'x', transform[TRANSFORM_X_INDEX]);\n\t}\n\n\tif(moveY !== 0) {\n\t\taddAtomicFloat32(transform, TRANSFORM_Y_INDEX, moveY, Infinity);\n\t\tcallbacks.entityComponentChanged(entityId, 'transform', 'y', transform[TRANSFORM_Y_INDEX]);\n\t}\n}\n\n// The one callback move reaches for, narrowed to the transform's own x and y. ComponentSystemCallbacks types\n// the value it takes against the game's own component map, which is only known here as some map that has a\n// transform in it - so the plain numbers coming out of the block are handed over through this instead.\ninterface TransformCallbacks {\n\tentityComponentChanged(entityId: number, componentName: 'transform', prop: 'x' | 'y', value: number): void\n}\n\nexport default physicsUpdate;\n","import { ComponentSystem } from '@daneren2005/shared-memory-ecs';\nimport type { BaseWorld, ComponentDefinitionMap, ComponentMap, ComponentSystemWorld, EntityUpdateComponents, EntityUpdateFunction, SystemConfig } from '@daneren2005/shared-memory-ecs';\nimport type { PhysicsComponents, PhysicsUpdateComponents } from '../components/registry';\nimport physicsUpdate, { type PhysicsUpdateMetadata } from './physics-update';\nimport { COLLIDABLE_QUERY } from './collision';\n\nexport interface PhysicsSystemConfig<\n\tC extends ComponentMap & PhysicsComponents,\n\tT extends PhysicsUpdateComponents & EntityUpdateComponents<C>,\n\tW extends ComponentSystemWorld = ComponentSystemWorld,\n> extends Partial<SystemConfig> {\n\t// Worker entry point that runs the update off the main thread. Bundlers need the `new Worker(new\n\t// URL(...))` call to be written out in the game's own source, so the game supplies the file:\n\t//\n\t// // physics.worker.ts\n\t// import { createComponentWorker } from '@daneren2005/shared-memory-ecs';\n\t// import { physicsUpdate } from '@daneren2005/shared-memory-physics';\n\t// createComponentWorker(self, physicsUpdate);\n\t//\n\t// getWorker: () => new Worker(new URL('./physics.worker.ts', import.meta.url), { type: 'module' })\n\t//\n\t// Left off, the simulation runs in-process on the main thread instead.\n\tgetWorker?: () => Worker\n\t// Forces the in-process ComponentWebWorker even when a getWorker is supplied. Defaults to true when no\n\t// getWorker was given, since there is then nothing to run the update on.\n\tforceMainThread?: boolean\n\t// The update to run, defaulting to plain movement. A game that wants collision callbacks builds one with\n\t// createPhysicsUpdate and passes the *same* function its worker file hands to createComponentWorker, so\n\t// both backends behave identically. createPhysicsUpdate stamps what it needs onto the function, so\n\t// `optional` and the collidable query below come across with it and do not have to be repeated here.\n\tupdateFunction?: EntityUpdateFunction<C, T, W> & { physics?: PhysicsUpdateMetadata<C> }\n\t// Extra components to send along with each entity's transform and velocity, for an update function built\n\t// by hand rather than through createPhysicsUpdate. Overrides what the update function declared.\n\toptional?: Array<keyof C & string>\n\t// Whether to gather the collidable query - every entity with a transform, whether it moves or not - and\n\t// send it along for the update's collision detection. Defaults to whatever the update function declared,\n\t// so it is on exactly when a createPhysicsUpdate was given an onCollision.\n\tcollision?: boolean\n}\n\n// Moves every entity that has both a transform and a velocity, and (when the update function detects\n// collisions) reports the entities that overlap to the game's callback. Generic over `C`, the game's full\n// component map, so a game's own world passes straight in; over `T`, the blocks its update function touches,\n// which is wider than transform + velocity once a game adds `optional` components of its own; and over `W`,\n// the per-run data object, for a game that subclasses this to add more through `addDataToWorld`.\nexport default class PhysicsSystem<\n\tC extends ComponentMap & PhysicsComponents,\n\tT extends PhysicsUpdateComponents & EntityUpdateComponents<C> = PhysicsUpdateComponents & EntityUpdateComponents<C>,\n\tW extends ComponentSystemWorld = ComponentSystemWorld,\n> extends ComponentSystem<C, T, W> {\n\tconstructor(world: BaseWorld<ComponentDefinitionMap, C>, options: PhysicsSystemConfig<C, T, W> = {}) {\n\t\tconst updateFunction: EntityUpdateFunction<C, T, W> & { physics?: PhysicsUpdateMetadata<C> } = options.updateFunction ?? physicsUpdate;\n\t\tconst optional = options.optional ?? updateFunction.physics?.optional ?? [];\n\t\tconst collision = options.collision ?? updateFunction.physics?.collision ?? false;\n\t\t// An entity reads its own collide category and mask before it searches, so the body block has to travel\n\t\t// with the entities this system moves as well as with the ones they can run into - but only when there is\n\t\t// collision detection to read it, since otherwise it is a block per moving entity for nothing.\n\t\tconst movingOptional = collision && !optional.includes('body') ? ['body' as keyof C & string, ...optional] : optional;\n\n\t\tsuper(world, {\n\t\t\tname: options.name ?? 'PhysicsSystem',\n\t\t\tdeltaBetweenRuns: options.deltaBetweenRuns,\n\t\t\tfirstRun: options.firstRun,\n\n\t\t\t// Movement needs both, so an entity with only one of them is not in the system at all - though it can\n\t\t\t// still be collided with through the query below.\n\t\t\trequired: ['transform', 'velocity'],\n\t\t\toptional: movingOptional,\n\t\t\tupdateFunction,\n\t\t\t// Collision is between anything with a transform and a body, not only the entities this system moves,\n\t\t\t// so that a ship can hit a station that has no velocity of its own. It is only gathered when something\n\t\t\t// is actually going to look at it, since it means shipping a block for every entity in the world.\n\t\t\tqueries: collision ? {\n\t\t\t\t[COLLIDABLE_QUERY]: {\n\t\t\t\t\trequired: ['transform', 'body'],\n\t\t\t\t\toptional: ['velocity', ...optional],\n\t\t\t\t},\n\t\t\t} : undefined,\n\n\t\t\tforceMainThread: options.forceMainThread ?? !options.getWorker,\n\t\t\t// Only ever reached when a getWorker was supplied: without one forceMainThread defaults to true, so\n\t\t\t// the base class never asks for a worker.\n\t\t\tgetWorker: options.getWorker ?? (() => {\n\t\t\t\tthrow new Error('PhysicsSystem cannot start a worker: no getWorker was supplied');\n\t\t\t}),\n\t\t});\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/components/transform-component.ts","../src/components/velocity-component.ts","../src/components/body-component.ts","../src/components/registry.ts","../src/math/shapes.ts","../src/systems/collision.ts","../src/systems/spatial-index.ts","../src/systems/physics-update.ts","../src/systems/physics-system.ts"],"sourcesContent":["import type { ComponentDefinition } from '@daneren2005/shared-memory-ecs';\n\n// Where an entity is in the world, how big it is, and which way it is facing. This is 2D physics, so the\n// transform is a position, the box that sits around it, and the angle that box is rotated to.\nexport interface TransformComponent {\n\tindex: number\n\tx: number\n\ty: number\n\twidth: number\n\theight: number\n\tangle: number\n}\n\n// How big an entity is and which way it starts out facing. This is the defining half of a transform: it is\n// what a game's entity template says, the same for every ship of a type, and it is what gives an entity a\n// transform at all - a config with neither a width nor a height does not load one.\n//\n// `width`/`height` are the *unrotated* size of the box, and are required: a box with no area never overlaps\n// anything, so an entity without a real size could never collide.\n//\n// `angle` is in radians, measured counter-clockwise from the +x axis, so it feeds Math.cos / Math.sin\n// directly. Nothing in this library writes it - it is the game's to set, usually from its heading - which is\n// why it is a starting facing here rather than saved state.\n//\n// A round entity can give a `radius` **instead of** a width and a height, which loads as a width and a height\n// of its diameter - so there is still only one size in the block however the config spelled it. Giving both\n// throws rather than picking a winner.\nexport interface TransformConfig {\n\twidth: number\n\theight: number\n\tangle?: number\n\tradius?: number\n}\n// Where the entity is, which is the half that is live runtime state: the physics system moves it every run,\n// so it is what has to round-trip through `save` for a saved world to resume where it left off. The size and\n// facing come back from the game's own template rather than the save.\n//\n// `x`/`y` are the *centre* of the box, not a corner: the box is rotated about that point and collision\n// projects out from it in both directions.\nexport interface TransformSerialization {\n\tx: number\n\ty: number\n}\n\n// Indexes into the backing Float32Array block. The physics update reads the same offsets off the raw shared\n// block, so they are exported for it (and for any game system that touches the block directly).\nexport const TRANSFORM_X_INDEX = 0;\nexport const TRANSFORM_Y_INDEX = 1;\nexport const TRANSFORM_WIDTH_INDEX = 2;\nexport const TRANSFORM_HEIGHT_INDEX = 3;\nexport const TRANSFORM_ANGLE_INDEX = 4;\nexport const TRANSFORM_SIZE = 5;\n\nexport const transformDefinition: ComponentDefinition<TransformComponent, Float32Array, TransformConfig, TransformSerialization> = {\n\ttype: Float32Array,\n\tsize: TRANSFORM_SIZE,\n\t// A size is what marks a config as having a transform at all. x/y are not in the list: they come back off\n\t// a save rather than out of a game's entity template, so on their own they do not describe an entity that\n\t// has a place in the world.\n\tloadProperties: ['width', 'height', 'radius'],\n\tload(entity, memory, config) {\n\t\t// A radius is the same size said differently, so it is turned into a width and a height here and nothing\n\t\t// downstream has to know which way the config was written. Both at once is a config that cannot be\n\t\t// honoured either way round, so it is refused rather than silently resolved.\n\t\tconst radius = config.radius;\n\t\tlet width = config.width;\n\t\tlet height = config.height;\n\t\tif(radius !== undefined) {\n\t\t\tif(width !== undefined || height !== undefined) {\n\t\t\t\tthrow new Error('A transform takes either a radius or a width and height, not both');\n\t\t\t}\n\n\t\t\twidth = height = radius * 2;\n\t\t}\n\n\t\tconst index = memory.create([config.x, config.y, width, height, config.angle ?? 0]);\n\t\tconst block = memory.getBlock(index);\n\n\t\treturn {\n\t\t\tindex,\n\t\t\tget x() {\n\t\t\t\treturn block[TRANSFORM_X_INDEX];\n\t\t\t},\n\t\t\tset x(value: number) {\n\t\t\t\tblock[TRANSFORM_X_INDEX] = value;\n\t\t\t},\n\t\t\tget y() {\n\t\t\t\treturn block[TRANSFORM_Y_INDEX];\n\t\t\t},\n\t\t\tset y(value: number) {\n\t\t\t\tblock[TRANSFORM_Y_INDEX] = value;\n\t\t\t},\n\t\t\tget width() {\n\t\t\t\treturn block[TRANSFORM_WIDTH_INDEX];\n\t\t\t},\n\t\t\tset width(value: number) {\n\t\t\t\tblock[TRANSFORM_WIDTH_INDEX] = value;\n\t\t\t},\n\t\t\tget height() {\n\t\t\t\treturn block[TRANSFORM_HEIGHT_INDEX];\n\t\t\t},\n\t\t\tset height(value: number) {\n\t\t\t\tblock[TRANSFORM_HEIGHT_INDEX] = value;\n\t\t\t},\n\t\t\tget angle() {\n\t\t\t\treturn block[TRANSFORM_ANGLE_INDEX];\n\t\t\t},\n\t\t\tset angle(value: number) {\n\t\t\t\tblock[TRANSFORM_ANGLE_INDEX] = value;\n\t\t\t},\n\t\t};\n\t},\n\tsave(component) {\n\t\treturn {\n\t\t\tx: component.x,\n\t\t\ty: component.y,\n\t\t};\n\t},\n};\n\nexport default transformDefinition;\n","import type { ComponentDefinition } from '@daneren2005/shared-memory-ecs';\n\n// How fast an entity is moving, in world units per second, along each axis. An entity with both a\n// transform and a velocity is moved by the physics system every run.\nexport interface VelocityComponent {\n\tindex: number\n\tvelocityX: number\n\tvelocityY: number\n}\n\n// Velocity is named per-axis rather than plain `x`/`y` because entity configs are flat and shared across\n// every component - `x`/`y` already belong to the transform. Either axis on its own is enough to give an\n// entity a velocity, so both are optional and a missing one starts at 0.\nexport interface VelocityConfig {\n\tvelocityX?: number\n\tvelocityY?: number\n}\n// Velocity is both the config an entity spawns with and live runtime state (a game's own systems apply\n// forces to it), so the whole thing round-trips back out through `save`.\nexport type VelocitySerialization = VelocityConfig;\n\n// Indexes into the backing Float32Array block. The physics update reads the same offsets off the raw shared\n// block, so they are exported for it (and for any game system that touches the block directly).\nexport const VELOCITY_X_INDEX = 0;\nexport const VELOCITY_Y_INDEX = 1;\nexport const VELOCITY_SIZE = 2;\n\nexport const velocityDefinition: ComponentDefinition<VelocityComponent, Float32Array, VelocityConfig, VelocitySerialization> = {\n\ttype: Float32Array,\n\tsize: VELOCITY_SIZE,\n\tloadProperties: ['velocityX', 'velocityY'],\n\tload(entity, memory, config) {\n\t\tconst index = memory.create([config.velocityX ?? 0, config.velocityY ?? 0]);\n\t\tconst block = memory.getBlock(index);\n\n\t\treturn {\n\t\t\tindex,\n\t\t\tget velocityX() {\n\t\t\t\treturn block[VELOCITY_X_INDEX];\n\t\t\t},\n\t\t\tset velocityX(value: number) {\n\t\t\t\tblock[VELOCITY_X_INDEX] = value;\n\t\t\t},\n\t\t\tget velocityY() {\n\t\t\t\treturn block[VELOCITY_Y_INDEX];\n\t\t\t},\n\t\t\tset velocityY(value: number) {\n\t\t\t\tblock[VELOCITY_Y_INDEX] = value;\n\t\t\t},\n\t\t};\n\t},\n\tsave(component) {\n\t\treturn {\n\t\t\tvelocityX: component.velocityX,\n\t\t\tvelocityY: component.velocityY,\n\t\t};\n\t},\n};\n\nexport default velocityDefinition;\n","import type { ComponentDefinition } from '@daneren2005/shared-memory-ecs';\n\n// What an entity collides *as*, and what it is willing to collide *with*. Only entities with a body take part\n// in collisions at all - but a body is loaded for anything with a size, so in practice the way to opt an entity\n// out is `collideCategory: 0` rather than leaving the component off.\n//\n// The categories themselves are the game's to name. This library only ever reads them as bits, so a game\n// declares whatever set it needs (ground, air, projectile, terrain, ...) and hands the numbers in through the\n// config.\nexport interface BodyComponent {\n\tindex: number\n\tshape: number\n\tcollideCategory: number\n\tcollideMask: number\n}\n\n// The shapes a body can be. All three are held in the same transform - a position, a width, a height and an\n// angle - so which one an entity is changes what that data means rather than where it lives:\n//\n// SHAPE_RECTANGLE - a width x height box turned to `angle`\n// SHAPE_CIRCLE - a circle of diameter `width`, which `angle` does not affect\n// SHAPE_CAPSULE - a capsule `width` from end to end and `height` thick, lying along the way it faces\n//\n// See src/math/shapes.ts for what each one means geometrically.\nexport const SHAPE_RECTANGLE = 1;\nexport const SHAPE_CIRCLE = 2;\nexport const SHAPE_CAPSULE = 3;\n\nconst KNOWN_SHAPES = [SHAPE_RECTANGLE, SHAPE_CIRCLE, SHAPE_CAPSULE];\n\n// What a body collides as when the game does not say: a single bit, so a world where nothing sets a category\n// behaves exactly as it did before categories existed.\nexport const DEFAULT_COLLIDE_CATEGORY = 1;\n// ...and what it is willing to collide with: every bit, so a default body meets anything whose own mask lets\n// it. 0xFFFFFFFF does not fit in a signed int, but it is stored in a Uint32Array and JS bitwise operators\n// work on the same 32 bits, so it ANDs correctly against any category a game can supply.\nexport const DEFAULT_COLLIDE_MASK = 0xFFFFFFFF;\n\n// Whether two bodies collide: each side's mask has to accept the other's category.\n//\n// The test is deliberately **symmetric**, so a pair either collides or does not regardless of which one moved\n// into which. That matters because collisions are reported per entity as each one moves: a rule that depended\n// on the direction would make the outcome depend on the order the system happened to move them in. A game\n// that wants a one-sided *response* - a projectile that hurts a unit without the unit hurting it back - gets\n// that by checking `other` inside its own onCollision, not by the two entities disagreeing about whether they\n// touched.\n//\n// So a ground unit that should be hit by projectiles has to name projectiles in its own mask, not just the\n// other way round. `collideCategory: 0` collides with nothing at all, since every mask ANDs to 0 against it.\n//\n// `&` yields a signed 32 bit result, so a mask with the top bit set comes back negative - compare against 0\n// rather than testing for a positive number.\nexport function canCollide(categoryA: number, maskA: number, categoryB: number, maskB: number): boolean {\n\treturn (maskA & categoryB) !== 0 && (maskB & categoryA) !== 0;\n}\n\n// A size is what loads a body, the same way it is what loads a transform: anything big enough to be in the\n// world is collidable by default, and the collide properties are there to narrow that rather than to grant it.\n// They are all *defining* config out of a game's entity template, which is why there is no `save` below - a\n// reloaded entity gets its category and mask back from the template, not from the save.\nexport interface BodyConfig {\n\t// Not read by the loader, only listed in `loadProperties`: they are what marks a config as describing\n\t// something that has a place in the world, and so something that can be run into.\n\twidth?: number\n\theight?: number\n\n\t// One of the SHAPE_ constants above. Defaults to a circle for a config given a `radius` and a rectangle\n\t// otherwise, so only capsules have to name their shape.\n\tshape?: number\n\t// Not read here either - the transform is what turns it into a width and a height - but a config that gives\n\t// a radius is describing a circle, and that is what decides the default shape above.\n\tradius?: number\n\t// The bit(s) this entity collides as, and the bits it is willing to collide with. See canCollide.\n\tcollideCategory?: number\n\tcollideMask?: number\n}\n\n// Indexes into the backing Uint32Array block. The collision broadphase reads the same offsets off the raw\n// shared block, so they are exported for it (and for any game system that touches the block directly).\nexport const BODY_SHAPE_INDEX = 0;\nexport const BODY_CATEGORY_INDEX = 1;\nexport const BODY_MASK_INDEX = 2;\nexport const BODY_SIZE = 3;\n\nexport const bodyDefinition: ComponentDefinition<BodyComponent, Uint32Array, BodyConfig> = {\n\ttype: Uint32Array,\n\tsize: BODY_SIZE,\n\tloadProperties: ['width', 'height', 'radius', 'shape', 'collideCategory', 'collideMask'],\n\tload(entity, memory, config) {\n\t\tconst index = memory.create([\n\t\t\ttoShape(config),\n\t\t\tconfig.collideCategory ?? DEFAULT_COLLIDE_CATEGORY,\n\t\t\tconfig.collideMask ?? DEFAULT_COLLIDE_MASK,\n\t\t]);\n\t\tconst block = memory.getBlock(index);\n\n\t\treturn {\n\t\t\tindex,\n\t\t\tget shape() {\n\t\t\t\treturn block[BODY_SHAPE_INDEX];\n\t\t\t},\n\t\t\tset shape(value: number) {\n\t\t\t\tblock[BODY_SHAPE_INDEX] = value;\n\t\t\t},\n\t\t\tget collideCategory() {\n\t\t\t\treturn block[BODY_CATEGORY_INDEX];\n\t\t\t},\n\t\t\tset collideCategory(value: number) {\n\t\t\t\tblock[BODY_CATEGORY_INDEX] = value;\n\t\t\t},\n\t\t\tget collideMask() {\n\t\t\t\treturn block[BODY_MASK_INDEX];\n\t\t\t},\n\t\t\tset collideMask(value: number) {\n\t\t\t\tblock[BODY_MASK_INDEX] = value;\n\t\t\t},\n\t\t};\n\t},\n};\n\n// An unknown shape is thrown on rather than defaulted: silently treating it as a rectangle would leave an\n// entity colliding with the wrong outline, which is far harder to spot than a config that fails to load.\n//\n// A config that says nothing is read off the size it gave instead - a `radius` describes a circle, and anything\n// else is a rectangle - so a game only has to name a shape for the one case its size cannot imply.\nfunction toShape(config: BodyConfig): number {\n\tconst shape = config.shape;\n\tif(shape === undefined) {\n\t\treturn config.radius !== undefined ? SHAPE_CIRCLE : SHAPE_RECTANGLE;\n\t} else if(!KNOWN_SHAPES.includes(shape)) {\n\t\tthrow new Error(`Unknown body shape: ${shape}`);\n\t}\n\n\treturn shape;\n}\n\nexport default bodyDefinition;\n","import bodyDefinition, { type BodyComponent } from './body-component';\nimport transformDefinition, { type TransformComponent } from './transform-component';\nimport velocityDefinition, { type VelocityComponent } from './velocity-component';\n\n// The component definitions this library supplies. A game spreads them into its own registry so its world\n// allocates their memory pools alongside its game specific components:\n//\n// const registry = { ...physicsRegistry, health: healthDefinition };\n// const world = new BaseWorld(registry);\n//\n// The names here are what the physics systems query on, so they must be kept as the registry keys.\nexport const physicsRegistry = {\n\ttransform: transformDefinition,\n\tvelocity: velocityDefinition,\n\tbody: bodyDefinition,\n};\n\n// The slice of a game's component map that the physics systems need. Declared as a type alias (not an\n// interface) so it satisfies the ECS's `ComponentMap` index signature. Systems here are generic over the\n// game's full component map and only require that it extends this.\nexport type PhysicsComponents = {\n\ttransform: TransformComponent\n\tvelocity: VelocityComponent\n\tbody: BodyComponent\n};\n\n// The same components as their concrete backing arrays, which is the form a system's update function sees them\n// in on the worker thread. `body` is optional here because it is optional on the query that moves entities -\n// movement does not need it, and it is only sent along when something is going to collide. The collidable\n// query requires it, so CollisionComponents narrows it back to guaranteed for the entities found through that.\n//\n// A game that hands extra components to its collision callback widens this with its own:\n//\n// type GameUpdateComponents = PhysicsUpdateComponents & { health?: Float32Array };\nexport type PhysicsUpdateComponents = {\n\ttransform: Float32Array\n\tvelocity: Float32Array\n\tbody?: Uint32Array\n};\n","// Geometry for the shapes a transform plus a body describe, all of them centred on (x, y) and turned `angle`\n// radians counter-clockwise. Everything here is pure number math on loose arguments rather than objects, so\n// the collision sweep can call it per pair without allocating.\n//\n// The three shapes are one idea in three settings: **an oriented core, grown by a radius**.\n//\n// rectangle - a width x height box, radius 0\n// circle - a point, radius width / 2\n// capsule - a segment along its facing, radius height / 2\n//\n// A circle is just a capsule whose segment has no length, so there are only two kinds of core - a box and a\n// segment - and therefore only three pair tests rather than one per combination of shapes. See shapesOverlap.\n\nimport { SHAPE_CAPSULE, SHAPE_CIRCLE, SHAPE_RECTANGLE } from '../components/body-component';\n\n// How far a capsule's core segment reaches from the centre, along the direction it faces. The caps are part\n// of the length, so a 40 x 10 capsule is 40 from end to end: 15 of segment either way plus a 5 radius cap.\n//\n// Clamped at 0, so a capsule told to be shorter than it is wide collapses to a circle of its own width rather\n// than turning itself inside out.\nexport function capsuleHalfLength(width: number, height: number): number {\n\treturn Math.max(0, (width - height) / 2);\n}\n\n// The radius of a shape's cap: half the diameter for a circle, half the thickness for a capsule, and nothing\n// at all for a rectangle, whose outline is its core.\n//\n// A circle takes its diameter from `width` and ignores `height` - loading one from a `radius` config sets both\n// to the same thing, so the two only ever disagree if a game writes the block itself.\nexport function shapeRadius(shape: number, width: number, height: number): number {\n\tif(shape === SHAPE_CIRCLE) {\n\t\treturn width / 2;\n\t} else if(shape === SHAPE_CAPSULE) {\n\t\treturn height / 2;\n\t}\n\n\treturn 0;\n}\n\n// Whether a shape encloses no area at all, and so can never overlap anything - not even something sitting\n// exactly on top of it. This is what keeps an entity the game never gave a size out of collisions, and which\n// of the two sizes matters depends on the shape: a circle is nothing without its width, while a capsule with\n// no thickness is a bare line however long it is.\nexport function shapeIsEmpty(shape: number, width: number, height: number): boolean {\n\tif(shape === SHAPE_CIRCLE) {\n\t\treturn width <= 0;\n\t} else if(shape === SHAPE_CAPSULE) {\n\t\treturn height <= 0;\n\t}\n\n\treturn width <= 0 || height <= 0;\n}\n\n// Half the width of the *axis aligned* box that fully contains a shape, whatever it is turned to - the box the\n// broadphase indexes it under, so it has to hold the whole shape or the tree would rule out its own candidates.\n//\n// A capsule always fits inside the rectangle its width and height describe, being that rectangle with its ends\n// rounded off, and so does a circle loaded from a `radius` (whose width and height agree). A circle is its\n// *width* alone though, so one whose height was written smaller by hand is taller than that rectangle - which is\n// why this is asked per shape rather than measured off the rectangle and trusted for everything.\nexport function shapeHalfWidth(shape: number, width: number, height: number, angle: number): number {\n\tif(shape === SHAPE_CIRCLE) {\n\t\t// A circle covers the same ground whichever way it is turned.\n\t\treturn width / 2;\n\t} else if(shape === SHAPE_CAPSULE) {\n\t\treturn Math.abs(Math.cos(angle)) * capsuleHalfLength(width, height) + height / 2;\n\t}\n\n\treturn boundsHalfWidth(width, height, angle);\n}\n// The same for the y axis: the capsule's segment leans the other way, and the circle is still a circle.\nexport function shapeHalfHeight(shape: number, width: number, height: number, angle: number): number {\n\tif(shape === SHAPE_CIRCLE) {\n\t\treturn width / 2;\n\t} else if(shape === SHAPE_CAPSULE) {\n\t\treturn Math.abs(Math.sin(angle)) * capsuleHalfLength(width, height) + height / 2;\n\t}\n\n\treturn boundsHalfHeight(width, height, angle);\n}\n\n// Whether two shapes of any kind overlap.\n//\n// Because a circle is a capsule with no length, every pair falls into one of three cases, picked by a switch\n// over the two shape constants with no indirection in the way:\n//\n// rectangle + rectangle - two boxes with no radius between them, which is the separating axis test below\n// round + round - the gap between the two core segments has to be under the two radii added together\n// rectangle + round - the gap between the core segment and the box has to be under the one radius\n//\n// Touching exactly counts as *not* overlapping in all three, matching orientedBoxesOverlap: the comparisons\n// are strict, so a capsule whose cap grazes a wall has not hit it.\nexport function shapesOverlap(\n\taShape: number, aX: number, aY: number, aWidth: number, aHeight: number, aAngle: number,\n\tbShape: number, bX: number, bY: number, bWidth: number, bHeight: number, bAngle: number,\n): boolean {\n\tif(shapeIsEmpty(aShape, aWidth, aHeight) || shapeIsEmpty(bShape, bWidth, bHeight)) {\n\t\treturn false;\n\t}\n\n\tconst aIsRound = aShape !== SHAPE_RECTANGLE;\n\tconst bIsRound = bShape !== SHAPE_RECTANGLE;\n\n\tif(!aIsRound && !bIsRound) {\n\t\treturn orientedBoxesOverlap(aX, aY, aWidth, aHeight, aAngle, bX, bY, bWidth, bHeight, bAngle);\n\t} else if(aIsRound && !bIsRound) {\n\t\t// Overlapping is the same relation whichever way round it is asked, so the one mixed case is written once\n\t\t// with the box first and reached from the other side by swapping.\n\t\treturn shapesOverlap(\n\t\t\tbShape, bX, bY, bWidth, bHeight, bAngle,\n\t\t\taShape, aX, aY, aWidth, aHeight, aAngle,\n\t\t);\n\t}\n\n\t// b is round either way from here, so take its core segment now.\n\tconst bHalf = coreHalfLength(bShape, bWidth, bHeight);\n\tconst bCos = Math.cos(bAngle) * bHalf;\n\tconst bSin = Math.sin(bAngle) * bHalf;\n\tconst bRadius = shapeRadius(bShape, bWidth, bHeight);\n\n\tif(!aIsRound) {\n\t\treturn segmentBoxDistanceSquared(\n\t\t\tbX - bCos, bY - bSin, bX + bCos, bY + bSin,\n\t\t\taX, aY, aWidth, aHeight, aAngle,\n\t\t) < bRadius * bRadius;\n\t}\n\n\tconst aHalf = coreHalfLength(aShape, aWidth, aHeight);\n\tconst aCos = Math.cos(aAngle) * aHalf;\n\tconst aSin = Math.sin(aAngle) * aHalf;\n\tconst reach = shapeRadius(aShape, aWidth, aHeight) + bRadius;\n\n\treturn segmentSegmentDistanceSquared(\n\t\taX - aCos, aY - aSin, aX + aCos, aY + aSin,\n\t\tbX - bCos, bY - bSin, bX + bCos, bY + bSin,\n\t) < reach * reach;\n}\n\n// How far a shape's core reaches from its centre along the direction it faces: a capsule's segment, and nothing\n// for a circle (a point) or a rectangle (whose core is the whole box, handled separately).\nfunction coreHalfLength(shape: number, width: number, height: number): number {\n\treturn shape === SHAPE_CAPSULE ? capsuleHalfLength(width, height) : 0;\n}\n\n// Half the width of the *axis aligned* box that fully contains a rotated one. Turning a rectangle widens the\n// footprint it needs: each of its two sides contributes however much of itself now points along the x axis.\n// At angle 0 this is just width / 2, and at a quarter turn it is height / 2.\nexport function boundsHalfWidth(width: number, height: number, angle: number): number {\n\treturn (Math.abs(Math.cos(angle)) * width + Math.abs(Math.sin(angle)) * height) / 2;\n}\n// The same for the y axis, with the two sides' contributions swapped.\nexport function boundsHalfHeight(width: number, height: number, angle: number): number {\n\treturn (Math.abs(Math.sin(angle)) * width + Math.abs(Math.cos(angle)) * height) / 2;\n}\n\n// Whether two rotated boxes overlap, by the separating axis theorem: two convex shapes miss each other if and\n// only if some axis exists that their projections do not both cover. For a pair of boxes only four axes can\n// ever be that axis - the two each box's own sides face along - so testing those four settles it.\n//\n// Touching exactly counts as *not* overlapping: the projections meet but do not cross. A box with no area\n// never overlaps anything either, even sitting inside another one - it collapses to a point or a line, and\n// the theorem has nothing to separate. Together those are what keeps an entity the game never gave a size\n// out of collisions entirely.\nexport function orientedBoxesOverlap(\n\taX: number, aY: number, aWidth: number, aHeight: number, aAngle: number,\n\tbX: number, bY: number, bWidth: number, bHeight: number, bAngle: number,\n): boolean {\n\tif(aWidth <= 0 || aHeight <= 0 || bWidth <= 0 || bHeight <= 0) {\n\t\treturn false;\n\t}\n\n\tconst aCos = Math.cos(aAngle);\n\tconst aSin = Math.sin(aAngle);\n\tconst bCos = Math.cos(bAngle);\n\tconst bSin = Math.sin(bAngle);\n\n\tconst aHalfWidth = aWidth / 2;\n\tconst aHalfHeight = aHeight / 2;\n\tconst bHalfWidth = bWidth / 2;\n\tconst bHalfHeight = bHeight / 2;\n\n\t// Everything is measured relative to a's centre, so only the offset between the two centres matters.\n\tconst offsetX = bX - aX;\n\tconst offsetY = bY - aY;\n\n\t// a's own two axes, then b's. Each box's sides face along (cos, sin) and (-sin, cos) of its angle.\n\treturn !separatedAlong(aCos, aSin, offsetX, offsetY, aHalfWidth, aHalfHeight, aCos, aSin, bHalfWidth, bHalfHeight, bCos, bSin)\n\t\t&& !separatedAlong(-aSin, aCos, offsetX, offsetY, aHalfWidth, aHalfHeight, aCos, aSin, bHalfWidth, bHalfHeight, bCos, bSin)\n\t\t&& !separatedAlong(bCos, bSin, offsetX, offsetY, aHalfWidth, aHalfHeight, aCos, aSin, bHalfWidth, bHalfHeight, bCos, bSin)\n\t\t&& !separatedAlong(-bSin, bCos, offsetX, offsetY, aHalfWidth, aHalfHeight, aCos, aSin, bHalfWidth, bHalfHeight, bCos, bSin);\n}\n\n// Whether the two boxes' shadows on the axis (axisX, axisY) fail to reach each other. A box's shadow reaches\n// out from its centre by however far its own two half-sides project onto the axis, so the pair is separated\n// when the gap between the centres along that axis is at least the two reaches added together.\n//\n// The axes passed in are always a box's own sides, which are unit vectors, so the dot products below are\n// already true distances and nothing needs normalising.\nfunction separatedAlong(\n\taxisX: number, axisY: number,\n\toffsetX: number, offsetY: number,\n\taHalfWidth: number, aHalfHeight: number, aCos: number, aSin: number,\n\tbHalfWidth: number, bHalfHeight: number, bCos: number, bSin: number,\n): boolean {\n\tconst aReach = aHalfWidth * Math.abs(aCos * axisX + aSin * axisY) + aHalfHeight * Math.abs(-aSin * axisX + aCos * axisY);\n\tconst bReach = bHalfWidth * Math.abs(bCos * axisX + bSin * axisY) + bHalfHeight * Math.abs(-bSin * axisX + bCos * axisY);\n\n\treturn Math.abs(offsetX * axisX + offsetY * axisY) >= aReach + bReach;\n}\n\n// The squared distance between two line segments, each given as its two end points. Squared because that is\n// all the callers need - comparing it against a squared radius avoids a square root per pair.\n//\n// This is what settles every pair of round shapes: two capsules are their two segments grown by a radius each,\n// so they meet exactly when the segments come within the two radii of one another. A circle is a segment whose\n// ends are the same point, which this handles as the degenerate case rather than as a shape of its own.\n//\n// Walks the usual closest-point-between-segments argument: solve for the closest points on the two infinite\n// lines, then clamp back into the segments, re-solving the other parameter each time a clamp moves one. The\n// parallel case has no single solution, so it pins one end and lets the clamping find the rest.\nexport function segmentSegmentDistanceSquared(\n\taX1: number, aY1: number, aX2: number, aY2: number,\n\tbX1: number, bY1: number, bX2: number, bY2: number,\n): number {\n\tconst aDeltaX = aX2 - aX1;\n\tconst aDeltaY = aY2 - aY1;\n\tconst bDeltaX = bX2 - bX1;\n\tconst bDeltaY = bY2 - bY1;\n\tconst offsetX = aX1 - bX1;\n\tconst offsetY = aY1 - bY1;\n\n\tconst aLength = aDeltaX * aDeltaX + aDeltaY * aDeltaY;\n\tconst bLength = bDeltaX * bDeltaX + bDeltaY * bDeltaY;\n\tconst bOffset = bDeltaX * offsetX + bDeltaY * offsetY;\n\n\tlet aParam = 0;\n\tlet bParam = 0;\n\tif(aLength <= 0 && bLength <= 0) {\n\t\t// Two points - which is two circles, the most common pair of all.\n\t\treturn offsetX * offsetX + offsetY * offsetY;\n\t} else if(aLength <= 0) {\n\t\t// A circle against a capsule: only where it lands along the capsule's segment matters.\n\t\tbParam = clamp(bOffset / bLength);\n\t} else {\n\t\tconst aOffset = aDeltaX * offsetX + aDeltaY * offsetY;\n\t\tif(bLength <= 0) {\n\t\t\taParam = clamp(-aOffset / aLength);\n\t\t} else {\n\t\t\tconst along = aDeltaX * bDeltaX + aDeltaY * bDeltaY;\n\t\t\t// Zero exactly when the two segments are parallel, which is where pinning aParam at 0 comes in: every\n\t\t\t// point along one is the same distance from the other, so any of them is as good an answer.\n\t\t\tconst denominator = aLength * bLength - along * along;\n\t\t\tif(denominator !== 0) {\n\t\t\t\taParam = clamp((along * bOffset - aOffset * bLength) / denominator);\n\t\t\t}\n\n\t\t\tbParam = (along * aParam + bOffset) / bLength;\n\t\t\t// Clamping bParam moves the closest point on b, so where it sits along a has to be solved again.\n\t\t\tif(bParam < 0) {\n\t\t\t\tbParam = 0;\n\t\t\t\taParam = clamp(-aOffset / aLength);\n\t\t\t} else if(bParam > 1) {\n\t\t\t\tbParam = 1;\n\t\t\t\taParam = clamp((along - aOffset) / aLength);\n\t\t\t}\n\t\t}\n\t}\n\n\tconst gapX = (aX1 + aDeltaX * aParam) - (bX1 + bDeltaX * bParam);\n\tconst gapY = (aY1 + aDeltaY * aParam) - (bY1 + bDeltaY * bParam);\n\n\treturn gapX * gapX + gapY * gapY;\n}\n\n// The squared distance between a line segment and a rotated box. This is what settles a rectangle against a\n// circle or a capsule: the round shape is its segment grown by a radius, so it reaches the box exactly when the\n// segment comes within that radius of it.\n//\n// Measured in the box's own frame, where it is axis aligned and the whole thing reduces to a segment against a\n// rectangle sitting at the origin. Two disjoint convex shapes always have their closest pair at a corner of\n// one against the other, so once the crossing case is out of the way the answer is the nearest of: each end of\n// the segment to the box, and each corner of the box to the segment.\nexport function segmentBoxDistanceSquared(\n\tx1: number, y1: number, x2: number, y2: number,\n\tboxX: number, boxY: number, boxWidth: number, boxHeight: number, boxAngle: number,\n): number {\n\tconst cos = Math.cos(boxAngle);\n\tconst sin = Math.sin(boxAngle);\n\tconst halfWidth = boxWidth / 2;\n\tconst halfHeight = boxHeight / 2;\n\n\t// Both ends into the box's frame, so the box becomes the rectangle from (-halfWidth, -halfHeight) to\n\t// (halfWidth, halfHeight).\n\tconst firstOffsetX = x1 - boxX;\n\tconst firstOffsetY = y1 - boxY;\n\tconst secondOffsetX = x2 - boxX;\n\tconst secondOffsetY = y2 - boxY;\n\tconst localX1 = firstOffsetX * cos + firstOffsetY * sin;\n\tconst localY1 = -firstOffsetX * sin + firstOffsetY * cos;\n\tconst localX2 = secondOffsetX * cos + secondOffsetY * sin;\n\tconst localY2 = -secondOffsetX * sin + secondOffsetY * cos;\n\n\t// The crossing case has to be caught on its own: a segment straight through the middle of the box has both\n\t// its ends outside it and every corner some way off, so the nearest-pair search below would report a real\n\t// distance for two shapes that are already through each other.\n\tif(!segmentMissesBox(localX1, localY1, localX2, localY2, halfWidth, halfHeight)) {\n\t\treturn 0;\n\t}\n\n\treturn Math.min(\n\t\t// Each end of the segment against the box...\n\t\tpointBoxDistanceSquared(localX1, localY1, halfWidth, halfHeight),\n\t\tpointBoxDistanceSquared(localX2, localY2, halfWidth, halfHeight),\n\t\t// ...and each corner of the box against the segment, which is where a segment running past a box at an\n\t\t// angle comes closest to it.\n\t\tpointSegmentDistanceSquared(-halfWidth, -halfHeight, localX1, localY1, localX2, localY2),\n\t\tpointSegmentDistanceSquared(halfWidth, -halfHeight, localX1, localY1, localX2, localY2),\n\t\tpointSegmentDistanceSquared(halfWidth, halfHeight, localX1, localY1, localX2, localY2),\n\t\tpointSegmentDistanceSquared(-halfWidth, halfHeight, localX1, localY1, localX2, localY2),\n\t);\n}\n\n// The squared distance from a point to a line segment: how far along the segment the point falls, clamped to\n// its ends, and then the gap from there.\nexport function pointSegmentDistanceSquared(x: number, y: number, x1: number, y1: number, x2: number, y2: number): number {\n\tconst deltaX = x2 - x1;\n\tconst deltaY = y2 - y1;\n\tconst length = deltaX * deltaX + deltaY * deltaY;\n\n\t// A segment with no length is just its own start point.\n\tconst along = length > 0 ? clamp(((x - x1) * deltaX + (y - y1) * deltaY) / length) : 0;\n\tconst gapX = x - (x1 + deltaX * along);\n\tconst gapY = y - (y1 + deltaY * along);\n\n\treturn gapX * gapX + gapY * gapY;\n}\n\n// Whether an axis aligned box at the origin and a segment fail to reach each other, by the same separating axis\n// argument as orientedBoxesOverlap: the box's own two axes, plus the one the segment faces along. Touching\n// exactly counts as reaching, so a grazing segment is measured as 0 away rather than as a near miss.\nfunction segmentMissesBox(x1: number, y1: number, x2: number, y2: number, halfWidth: number, halfHeight: number): boolean {\n\tif(Math.max(x1, x2) < -halfWidth || Math.min(x1, x2) > halfWidth) {\n\t\treturn true;\n\t} else if(Math.max(y1, y2) < -halfHeight || Math.min(y1, y2) > halfHeight) {\n\t\treturn true;\n\t}\n\n\t// The segment's own axis. Both of its ends project to the same place on the normal, so one is enough - and\n\t// for a segment with no length the normal is (0, 0), which never separates and correctly leaves the two box\n\t// axes above to decide whether the point is inside.\n\tconst normalX = -(y2 - y1);\n\tconst normalY = x2 - x1;\n\n\treturn Math.abs(normalX * x1 + normalY * y1) > halfWidth * Math.abs(normalX) + halfHeight * Math.abs(normalY);\n}\n\n// The squared distance from a point to an axis aligned box at the origin: clamp the point onto the box and\n// measure what is left over. 0 for a point inside it.\nfunction pointBoxDistanceSquared(x: number, y: number, halfWidth: number, halfHeight: number): number {\n\tconst gapX = Math.abs(x) - halfWidth;\n\tconst gapY = Math.abs(y) - halfHeight;\n\tconst outsideX = gapX > 0 ? gapX : 0;\n\tconst outsideY = gapY > 0 ? gapY : 0;\n\n\treturn outsideX * outsideX + outsideY * outsideY;\n}\n\nfunction clamp(value: number): number {\n\tif(value < 0) {\n\t\treturn 0;\n\t} else if(value > 1) {\n\t\treturn 1;\n\t}\n\n\treturn value;\n}\n","import Flatbush from 'flatbush';\nimport type { ComponentMap, ComponentSystemCallbacks, ComponentSystemWorld, EntityQueryComponents, EntityUpdateComponents } from '@daneren2005/shared-memory-ecs';\nimport type { PhysicsUpdateComponents } from '../components/registry';\nimport { BODY_CATEGORY_INDEX, BODY_MASK_INDEX, BODY_SHAPE_INDEX } from '../components/body-component';\nimport { TRANSFORM_ANGLE_INDEX, TRANSFORM_HEIGHT_INDEX, TRANSFORM_WIDTH_INDEX, TRANSFORM_X_INDEX, TRANSFORM_Y_INDEX } from '../components/transform-component';\nimport { VELOCITY_X_INDEX, VELOCITY_Y_INDEX } from '../components/velocity-component';\nimport { shapeHalfHeight, shapeHalfWidth, shapeIsEmpty, shapesOverlap } from '../math/shapes';\n\n// How close to the moment of contact a sweep has to get before it settles for it, in world units. Small enough\n// that the gap it leaves an entity resting against another is far below anything a game would draw, and large\n// enough that the halving below reaches it in a handful of steps.\nconst CONTACT_TOLERANCE = 1e-4;\n// A cap on that halving, so a move over a huge distance cannot turn into an unbounded number of overlap tests.\n// Each one narrows the answer by half, so this reaches the tolerance above for any move up to about sixteen\n// hundred units - and only an entity that is actually stopping somewhere ever spends a single one of them.\nconst MAX_REFINEMENTS = 24;\n\n// The name PhysicsSystem registers its extra query under, and the key the broadphase reads it back out of.\n// It holds everything with a transform and a body rather than only the entities the system moves, so a moving\n// entity can still run into a station, a wall or anything else that has no velocity of its own.\nexport const COLLIDABLE_QUERY = 'collidable';\n\n// The blocks a collision callback is handed for the entity that was run into. `transform` and `body` are the\n// guarantees - they are what the collidable query requires - while `velocity` and everything the game listed\n// in `optional` are there only if that entity has them.\nexport type CollisionComponents<T extends PhysicsUpdateComponents> = Partial<T> & {\n\ttransform: Float32Array\n\tbody: Uint32Array\n};\n\n// The entity that did the running into: the one being updated, so everything the system asked for is there.\nexport interface MovingEntity<T extends PhysicsUpdateComponents> {\n\tentityId: number\n\tcomponents: T\n}\n\n// The entity on the other side of a collision, which may be something the system never moves at all.\nexport interface CollisionEntity<T extends PhysicsUpdateComponents> {\n\tentityId: number\n\tcomponents: CollisionComponents<T>\n}\n\n// What a game runs when one entity moves into another. It is called on the same thread the physics update\n// runs on, so it must be a plain function over the raw blocks - no entities, no world - and it reaches the\n// main thread only through `callbacks` (which report a changed property, a death, or an entity to spawn).\n//\n// It is called *per entity*, right after that entity has moved: `self` is the one that just moved and `other`\n// is what it has ended up on top of. Two moving entities that run into each other therefore get one call\n// each, with the roles swapped, which is what lets a callback act on itself - bouncing, taking damage - and\n// leave the other side to its own call. An entity with no velocity is never `self`, since the system never\n// moves it, but it is still found as `other`.\n//\n// `queries` is passed straight through so a callback that needs more than the two entities in front of it can\n// still walk the full collidable list, e.g. to credit a third entity for the kill.\nexport type CollisionFunction<C extends ComponentMap, T extends PhysicsUpdateComponents & EntityUpdateComponents<C>, W extends ComponentSystemWorld = ComponentSystemWorld> = (\n\tworld: W,\n\tself: MovingEntity<T>,\n\tother: CollisionEntity<T>,\n\tqueries: EntityQueryComponents<C>,\n\tcallbacks: ComponentSystemCallbacks<C>,\n) => void;\n\n// How much of a move an entity is actually allowed to make, and what stopped it there.\n//\n// `fraction` is how far along the requested move it got: 1 for a clear path, 0 for an entity already pressed up\n// against something, and whatever is in between for one that came to rest part way.\n//\n// `blocking` is what it came to rest *against*, which the overlap test at that resting place will not report -\n// the whole point of stopping there is that the two are touching rather than through each other. More than one\n// entry means it wedged between them at the same moment, not that it should have stopped at the first.\nexport interface SweepResult<T extends PhysicsUpdateComponents> {\n\tfraction: number\n\tblocking: Array<CollisionEntity<T>>\n}\n\n// What `blocking` is for the sweeps that find nothing in the way, which is most of them: one shared empty array\n// rather than one per entity per run. Read-only to a caller by nature - there is nothing in it to iterate.\nconst NOTHING_BLOCKING: Array<never> = [];\n\n// Everything about the entity doing the searching, read out of its blocks once so that a sweep testing the same\n// pair a dozen times over does not read them a dozen times over. x/y are where it started the move.\ninterface Searcher {\n\tentityId: number\n\tshape: number\n\tx: number\n\ty: number\n\twidth: number\n\theight: number\n\tangle: number\n\thalfWidth: number\n\thalfHeight: number\n\tcategory: number\n\tmask: number\n}\n\n// One category's worth of the broadphase: every collidable entity that collides *as* that category, and an\n// R-tree over just those. A searching entity only looks in the buckets its own mask accepts, so a projectile\n// that can only hit units never walks the tree the terrain is in.\n//\n// Bucketed on the whole category *value* rather than a bit at a time, so an entity that collides as two things\n// at once still lives in exactly one bucket and can never be found twice by one search.\ninterface CategoryBucket<T extends PhysicsUpdateComponents> {\n\tcategory: number\n\tentries: Array<CollisionEntity<T>>\n\tindex: Flatbush\n}\n\n// An R-tree over every collidable entity, built once at the top of a run and then asked, entity by entity as\n// each one moves, what it might have hit.\n//\n// Everything is indexed where it stood at the *start* of the run, because that is the only moment all of the\n// entities agree on - by the time the third entity moves, the first two have already gone somewhere else. To\n// stay a true superset of what can actually collide, each box is grown by however far that entity could\n// travel before the run is out. The growth goes both ways rather than along the heading: a collision\n// callback is free to turn an entity around before it has had its own move, and a box grown only forwards\n// would then be pointing the wrong way.\n//\n// The tree is split by collide category rather than being one index over everything, so most of what an entity\n// cannot hit is ruled out a whole subtree at a time instead of one candidate at a time. Nothing is left empty:\n// a category with no collidable entities has no bucket at all, which also means Flatbush is never asked to\n// build an index over zero items.\nexport default class CollisionBroadphase<T extends PhysicsUpdateComponents> {\n\tprivate buckets: Array<CategoryBucket<T>> = [];\n\n\tconstructor(entities: Array<{ entityId: number, components: EntityUpdateComponents }>, seconds: number) {\n\t\t// Boxes are collected per category first because Flatbush has to be told how many items it will hold up\n\t\t// front, and how many that is only becomes clear once everything that cannot collide has been dropped.\n\t\tconst pending = new Map<number, { entries: Array<CollisionEntity<T>>, bounds: Array<number> }>();\n\n\t\tfor(const entity of entities) {\n\t\t\t// The ECS types query blocks generically as ComponentTypedArray since it cannot know what any given\n\t\t\t// game registered; narrow them to the concrete arrays the definitions allocate here, once, so neither\n\t\t\t// the search below nor the game's callback needs a cast of its own.\n\t\t\tconst components = entity.components as CollisionComponents<T>;\n\t\t\tconst transform = components.transform;\n\t\t\tconst body = components.body;\n\t\t\tif(!transform || !body) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// No mask can ever name category 0, so an entity that collides as nothing can never be the other half\n\t\t\t// of a collision either - dropping it here is exact rather than an approximation, and it is what makes\n\t\t\t// `collideCategory: 0` a way to opt an entity out of collisions entirely.\n\t\t\tconst category = body[BODY_CATEGORY_INDEX];\n\t\t\tif(category === 0) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst shape = body[BODY_SHAPE_INDEX];\n\t\t\tconst width = transform[TRANSFORM_WIDTH_INDEX];\n\t\t\tconst height = transform[TRANSFORM_HEIGHT_INDEX];\n\t\t\t// A shape with no area can never overlap anything, so dropping it here is exact rather than an\n\t\t\t// approximation - and it keeps an entity the game never gave a size out of the tree entirely.\n\t\t\tif(shapeIsEmpty(shape, width, height)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst angle = transform[TRANSFORM_ANGLE_INDEX];\n\t\t\tconst halfWidth = shapeHalfWidth(shape, width, height, angle);\n\t\t\tconst halfHeight = shapeHalfHeight(shape, width, height, angle);\n\n\t\t\tconst velocity = components.velocity;\n\t\t\tconst travelX = velocity ? Math.abs(velocity[VELOCITY_X_INDEX]) * seconds : 0;\n\t\t\tconst travelY = velocity ? Math.abs(velocity[VELOCITY_Y_INDEX]) * seconds : 0;\n\n\t\t\tconst x = transform[TRANSFORM_X_INDEX];\n\t\t\tconst y = transform[TRANSFORM_Y_INDEX];\n\n\t\t\tlet bucket = pending.get(category);\n\t\t\tif(!bucket) {\n\t\t\t\tbucket = { entries: [], bounds: [] };\n\t\t\t\tpending.set(category, bucket);\n\t\t\t}\n\n\t\t\tbucket.entries.push({ entityId: entity.entityId, components });\n\t\t\tbucket.bounds.push(\n\t\t\t\tx - halfWidth - travelX,\n\t\t\t\ty - halfHeight - travelY,\n\t\t\t\tx + halfWidth + travelX,\n\t\t\t\ty + halfHeight + travelY,\n\t\t\t);\n\t\t}\n\n\t\tfor(const [category, { entries, bounds }] of pending) {\n\t\t\tconst index = new Flatbush(entries.length);\n\t\t\tfor(let i = 0; i < entries.length; i++) {\n\t\t\t\tindex.add(bounds[i * 4], bounds[i * 4 + 1], bounds[i * 4 + 2], bounds[i * 4 + 3]);\n\t\t\t}\n\t\t\tindex.finish();\n\n\t\t\tthis.buckets.push({ category, entries, index });\n\t\t}\n\t}\n\n\t// Runs `handle` for everything `self` is really overlapping, having just moved.\n\t//\n\t// The tree narrows the field to whatever is anywhere near, then each of those is put through the full\n\t// rotated box test against where the two entities are *now* - which for an entity that has not had its own\n\t// move yet is still where it started. That is the price of collisions happening as each entity moves\n\t// rather than all at the end, and it evens out: the pair is looked at again from the other side once that\n\t// entity moves too.\n\tforEachOverlapping(self: MovingEntity<T>, handle: (other: CollisionEntity<T>) => void): void {\n\t\tconst searcher = toSearcher(self);\n\t\tif(!searcher) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst { x, y, halfWidth, halfHeight } = searcher;\n\t\t// Searched with where this entity has actually ended up, ungrown: every box in the tree already allows\n\t\t// for its own entity's move, so allowing for this one's a second time would only widen the net.\n\t\tthis.forEachCandidate(searcher, x - halfWidth, y - halfHeight, x + halfWidth, y + halfHeight, other => {\n\t\t\tif(overlapsAt(searcher, x, y, other)) {\n\t\t\t\thandle(other);\n\t\t\t}\n\t\t});\n\t}\n\n\t// How much of the move (moveX, moveY) `self` may take before it runs into something, so that an entity comes\n\t// to rest against what is in its way instead of ending up inside it.\n\t//\n\t// Everything the move lands on is collected first and then they are all asked together, which is what keeps\n\t// an entity from stopping inside the near one because the far one happened to be looked at first: there is\n\t// one answer for the whole move, and it is the earliest contact of the lot.\n\t//\n\t// Contact is halved in on rather than solved: three shapes at any rotation to one another have no single\n\t// formula for when a pair first touches, while the overlap test the narrowphase already has answers it for\n\t// any pair anywhere. The result is always taken from a position that tested clear, so wherever the entity\n\t// is put down it is genuinely not inside any of them.\n\tsweep(self: MovingEntity<T>, moveX: number, moveY: number): SweepResult<T> {\n\t\tconst distance = Math.sqrt(moveX * moveX + moveY * moveY);\n\t\tconst searcher = toSearcher(self);\n\t\t// Nothing to work out for an entity that is not going anywhere, or that does not collide at all.\n\t\tif(!searcher || distance === 0) {\n\t\t\treturn { fraction: 1, blocking: NOTHING_BLOCKING };\n\t\t}\n\n\t\tconst { x, y, halfWidth, halfHeight } = searcher;\n\t\tconst endX = x + moveX;\n\t\tconst endY = y + moveY;\n\n\t\t// What the move would land on top of, asked where it *ends* rather than anywhere along the way. That is\n\t\t// what keeps a clear move - which is nearly every move - down to a single shape test per candidate and\n\t\t// nothing else: an entity only ever walks its own path once something is genuinely in the way of it.\n\t\t//\n\t\t// The trade is that a move long enough to carry an entity clean past something is not stopped by it: by\n\t\t// the time the move is out there is nothing left to land on. That takes a single run covering more\n\t\t// ground than the thing in the way is thick, which a fixed `deltaBetweenRuns` rules out.\n\t\tconst candidates: Array<CollisionEntity<T>> = [];\n\t\tthis.forEachCandidate(searcher, endX - halfWidth, endY - halfHeight, endX + halfWidth, endY + halfHeight, other => {\n\t\t\t// Not where the move ends up, so nothing this move has to stop for.\n\t\t\tif(!overlapsAt(searcher, endX, endY, other)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Already inside it before the move began - something the game put there, or another system pushed\n\t\t\t// it into. It cannot be what *this* move ran into, and blocking on it would pin the entity inside\n\t\t\t// it for good with no way back out, so the move is let through and the pair is left to the overlap\n\t\t\t// callback that follows it.\n\t\t\tif(overlapsAt(searcher, x, y, other)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tcandidates.push(other);\n\t\t});\n\t\tif(candidates.length === 0) {\n\t\t\treturn { fraction: 1, blocking: NOTHING_BLOCKING };\n\t\t}\n\n\t\t// From here the entity is definitely stopping short of where it asked to go, and all that is left is\n\t\t// where. `clear` is a fraction of the move it is known to fit at and `blocked` one it is known not to -\n\t\t// the whole move to begin with, since landing there is what put these candidates in the list at all -\n\t\t// and halving closes the gap between the two.\n\t\tlet clear = 0;\n\t\tlet blocked = 1;\n\t\tconst tolerance = CONTACT_TOLERANCE / distance;\n\t\tfor(let i = 0; i < MAX_REFINEMENTS && blocked - clear > tolerance; i++) {\n\t\t\tconst middle = (clear + blocked) / 2;\n\t\t\tif(this.blockedAt(searcher, candidates, moveX, moveY, middle)) {\n\t\t\t\tblocked = middle;\n\t\t\t} else {\n\t\t\t\tclear = middle;\n\t\t\t}\n\t\t}\n\n\t\t// Whatever is in the way at the first position that was not clear: the entity has come to rest a hair\n\t\t// short of exactly there, so this is what it is up against.\n\t\tconst blocking: Array<CollisionEntity<T>> = [];\n\t\tconst blockedX = x + moveX * blocked;\n\t\tconst blockedY = y + moveY * blocked;\n\t\tfor(const other of candidates) {\n\t\t\tif(overlapsAt(searcher, blockedX, blockedY, other)) {\n\t\t\t\tblocking.push(other);\n\t\t\t}\n\t\t}\n\n\t\t// An entity already up against something is left exactly where it is rather than crawling the last\n\t\t// fraction of a tolerance forwards every run, which would report a position change for no visible move.\n\t\treturn { fraction: clear * distance > CONTACT_TOLERANCE ? clear : 0, blocking };\n\t}\n\n\t// Whether `self` would be inside any of `candidates` that far along its move.\n\tprivate blockedAt(searcher: Searcher, candidates: Array<CollisionEntity<T>>, moveX: number, moveY: number, fraction: number): boolean {\n\t\tconst x = searcher.x + moveX * fraction;\n\t\tconst y = searcher.y + moveY * fraction;\n\t\tfor(const other of candidates) {\n\t\t\tif(overlapsAt(searcher, x, y, other)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t// Runs `handle` for every collidable entity in the box that `searcher` is allowed to collide with at all -\n\t// the tree search and the category rules, with no geometry beyond the boxes themselves.\n\t//\n\t// The collide masks are applied before any shape test rather than after: a pair that can never collide is\n\t// settled by two ANDs instead of a full separating-axis test.\n\tprivate forEachCandidate(searcher: Searcher, minX: number, minY: number, maxX: number, maxY: number, handle: (other: CollisionEntity<T>) => void): void {\n\t\tfor(const bucket of this.buckets) {\n\t\t\t// Half of canCollide, settled once for everything in the bucket rather than once per candidate:\n\t\t\t// they all collide as the same category, so either this entity's mask accepts the lot of them or it\n\t\t\t// accepts none of them.\n\t\t\tif((searcher.mask & bucket.category) === 0) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tfor(const found of bucket.index.search(minX, minY, maxX, maxY)) {\n\t\t\t\tconst other = bucket.entries[found];\n\t\t\t\t// Every collidable entity is in some bucket, including this one.\n\t\t\t\tif(other.entityId === searcher.entityId) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// The other half of canCollide, which does have to be per candidate: a bucket shares one category\n\t\t\t\t// but every entity in it carries its own mask.\n\t\t\t\tif((other.components.body[BODY_MASK_INDEX] & searcher.category) === 0) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\thandle(other);\n\t\t\t}\n\t\t}\n\t}\n}\n\n// Reads out what a searching entity needs to be measured by, or undefined for one that cannot collide at all.\nfunction toSearcher<T extends PhysicsUpdateComponents>(self: MovingEntity<T>): Searcher | undefined {\n\t// An entity with no body does not collide at all. It can still be moved by the system - the query that\n\t// moves entities only asks for the body, it does not require it.\n\tconst body = self.components.body;\n\tif(!body) {\n\t\treturn undefined;\n\t}\n\n\t// Willing to run into nothing at all, so there is no bucket worth searching. Note this only stops it being\n\t// the entity that *finds* a collision: something whose mask accepts it can still find this one, and that is\n\t// the asymmetry the symmetric canCollide rule then rules out.\n\tconst mask = body[BODY_MASK_INDEX];\n\tif(mask === 0) {\n\t\treturn undefined;\n\t}\n\n\tconst transform = self.components.transform;\n\tconst shape = body[BODY_SHAPE_INDEX];\n\tconst width = transform[TRANSFORM_WIDTH_INDEX];\n\tconst height = transform[TRANSFORM_HEIGHT_INDEX];\n\tif(shapeIsEmpty(shape, width, height)) {\n\t\treturn undefined;\n\t}\n\n\tconst angle = transform[TRANSFORM_ANGLE_INDEX];\n\n\treturn {\n\t\tentityId: self.entityId,\n\t\tshape,\n\t\tx: transform[TRANSFORM_X_INDEX],\n\t\ty: transform[TRANSFORM_Y_INDEX],\n\t\twidth,\n\t\theight,\n\t\tangle,\n\t\thalfWidth: shapeHalfWidth(shape, width, height, angle),\n\t\thalfHeight: shapeHalfHeight(shape, width, height, angle),\n\t\tcategory: body[BODY_CATEGORY_INDEX],\n\t\tmask,\n\t};\n}\n\n// Whether the searching entity, put down at (x, y) rather than wherever its own block says it is, overlaps\n// `other`. Taking the position as arguments is what lets a sweep try a whole path without writing any of it.\nfunction overlapsAt<T extends PhysicsUpdateComponents>(searcher: Searcher, x: number, y: number, other: CollisionEntity<T>): boolean {\n\tconst transform = other.components.transform;\n\n\treturn shapesOverlap(\n\t\tsearcher.shape, x, y, searcher.width, searcher.height, searcher.angle,\n\t\tother.components.body[BODY_SHAPE_INDEX],\n\t\ttransform[TRANSFORM_X_INDEX], transform[TRANSFORM_Y_INDEX],\n\t\ttransform[TRANSFORM_WIDTH_INDEX], transform[TRANSFORM_HEIGHT_INDEX], transform[TRANSFORM_ANGLE_INDEX],\n\t);\n}\n","import Flatbush from 'flatbush';\nimport type { EntityUpdateComponents } from '@daneren2005/shared-memory-ecs';\nimport { BODY_SHAPE_INDEX, SHAPE_RECTANGLE } from '../components/body-component';\nimport { TRANSFORM_ANGLE_INDEX, TRANSFORM_HEIGHT_INDEX, TRANSFORM_WIDTH_INDEX, TRANSFORM_X_INDEX, TRANSFORM_Y_INDEX } from '../components/transform-component';\nimport { shapeHalfHeight, shapeHalfWidth } from '../math/shapes';\n\n// The blocks the index reads off each entity. Only the transform is needed - that is what says where something\n// is and how big it is - while the body refines the box it is filed under to the shape it actually is. Anything\n// else a game queried travels along untouched, so a filter can read it back out.\nexport type SpatialComponents = {\n\ttransform: Float32Array\n\tbody?: Uint32Array\n};\n\n// One entity as the index hands it back: the id it was given plus whatever blocks came with it.\nexport interface SpatialEntity<T extends SpatialComponents = SpatialComponents> {\n\tentityId: number\n\tcomponents: T\n}\n\n// Which of the entities a query found are wanted. Run per candidate the tree turns up, so it is the place to\n// put whatever the index itself cannot know - the searcher's own id, which team something is on, whether it is\n// already dead - and everything it rejects is as if it were never in the box at all.\nexport type SpatialFilter<T extends SpatialComponents = SpatialComponents> = (entity: SpatialEntity<T>) => boolean;\n\n// An R-tree over a set of entities, for the questions a game asks about *where things are* rather than about\n// what is running into what: who is in this area, and what is the closest thing to this point.\n//\n// This is the same machinery the collision broadphase is built on, minus everything collision specific - there\n// are no collide categories, no room left for a move, and no shape tests. Every answer is in terms of the\n// axis aligned box each entity occupies, so an entity is \"found\" as soon as that box is reached, and picking\n// out the ones that really matter is the caller's to do with a `filter` (see SpatialFilter).\n//\n// Nothing here reads the blocks after the constructor, so the index is a snapshot: it describes where everything\n// was at the moment it was built. Build one per run - the entities it holds are moving, and a tree built last\n// run is a tree of where they all used to be.\n//\n// const index = new SpatialIndex(queries.collidable);\n// const nearest = index.findNearest(x, y, 150, other => other.entityId !== entityId);\nexport default class SpatialIndex<T extends SpatialComponents = SpatialComponents> {\n\tprivate entries: Array<SpatialEntity<T>> = [];\n\t// Left off for an index over nothing at all: Flatbush cannot be built to hold zero items, and there is no\n\t// answer to give from an empty index anyway.\n\tprivate index: Flatbush | undefined;\n\n\t// `entities` is anything shaped like a query result - `{ entityId, components }` - so a system's own\n\t// `queries.whatever` goes straight in. Entities without a transform are skipped rather than rejected: a\n\t// query that only *optionally* asks for one is a perfectly reasonable thing to hand over, and something with\n\t// no place in the world cannot be found by a search of the world.\n\tconstructor(entities: Iterable<{ entityId: number, components: EntityUpdateComponents }>) {\n\t\t// Boxes are collected up front because Flatbush has to be told how many items it will hold before any of\n\t\t// them are added, and how many that is only becomes clear once everything without a transform is dropped.\n\t\tconst bounds: Array<number> = [];\n\n\t\tfor(const entity of entities) {\n\t\t\t// The ECS types query blocks generically as ComponentTypedArray since it cannot know what any given\n\t\t\t// game registered; narrow them to the concrete arrays the definitions allocate here, once, so neither\n\t\t\t// the searches below nor the game's filter needs a cast of its own.\n\t\t\tconst components = entity.components as T;\n\t\t\tconst transform = components.transform;\n\t\t\tif(!transform) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// A body only says which outline the transform's width and height describe, so an entity without one is\n\t\t\t// taken as the plain rectangle they already are. That keeps the index usable for anything with a place\n\t\t\t// in the world rather than only for things that collide.\n\t\t\tconst body = components.body;\n\t\t\tconst shape = body ? body[BODY_SHAPE_INDEX] : SHAPE_RECTANGLE;\n\t\t\tconst width = transform[TRANSFORM_WIDTH_INDEX];\n\t\t\tconst height = transform[TRANSFORM_HEIGHT_INDEX];\n\t\t\tconst angle = transform[TRANSFORM_ANGLE_INDEX];\n\t\t\tconst x = transform[TRANSFORM_X_INDEX];\n\t\t\tconst y = transform[TRANSFORM_Y_INDEX];\n\n\t\t\t// Unlike the collision broadphase, an entity with no area is kept: a search is a question about where\n\t\t\t// something is, and a thing with a position but no size still has an answer to it.\n\t\t\tconst halfWidth = shapeHalfWidth(shape, width, height, angle);\n\t\t\tconst halfHeight = shapeHalfHeight(shape, width, height, angle);\n\n\t\t\tthis.entries.push({ entityId: entity.entityId, components });\n\t\t\tbounds.push(x - halfWidth, y - halfHeight, x + halfWidth, y + halfHeight);\n\t\t}\n\n\t\tif(this.entries.length === 0) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst index = new Flatbush(this.entries.length);\n\t\tfor(let i = 0; i < this.entries.length; i++) {\n\t\t\tindex.add(bounds[i * 4], bounds[i * 4 + 1], bounds[i * 4 + 2], bounds[i * 4 + 3]);\n\t\t}\n\t\tindex.finish();\n\n\t\tthis.index = index;\n\t}\n\n\t// How many entities are actually in the index, which is not how many were handed to it: anything without a\n\t// transform never made it in.\n\tget size(): number {\n\t\treturn this.entries.length;\n\t}\n\n\t// Everything whose box reaches into the one given, in no particular order. Touching counts - two boxes that\n\t// share nothing but an edge are still found - so an exact question about where the edges fall is one for the\n\t// caller to settle on the results.\n\tsearch(minX: number, minY: number, maxX: number, maxY: number, filter?: SpatialFilter<T>): Array<SpatialEntity<T>> {\n\t\tconst index = this.index;\n\t\tif(!index) {\n\t\t\treturn [];\n\t\t}\n\n\t\tconst entries = this.entries;\n\t\t// Filtering inside the search rather than over its results is what keeps a rejected candidate from ever\n\t\t// reaching the results array in the first place.\n\t\tconst found = filter ? index.search(minX, minY, maxX, maxY, i => filter(entries[i]))\n\t\t\t: index.search(minX, minY, maxX, maxY);\n\n\t\treturn found.map(i => entries[i]);\n\t}\n\n\t// The same search written from the middle out: everything within `reachX` / `reachY` of (x, y) on each axis.\n\t// This is the form most game searches are actually in - something looking a certain distance around itself -\n\t// and it saves every caller writing the same four sums.\n\tsearchAround(x: number, y: number, reachX: number, reachY: number, filter?: SpatialFilter<T>): Array<SpatialEntity<T>> {\n\t\treturn this.search(x - reachX, y - reachY, x + reachX, y + reachY, filter);\n\t}\n\n\t// The closest entity to (x, y) that `filter` accepts, or undefined when there is none within `maxDistance`.\n\t//\n\t// This is a walk of the tree in distance order rather than a search followed by a sort: it settles on an\n\t// answer as soon as the nearest box is reached, and never measures the far side of the world at all. So a\n\t// game asking \"who is nearest\" should ask it this way rather than searching a box and sorting what comes\n\t// back - and `maxDistance` is what keeps it from walking the whole tree when the answer is that nobody is.\n\t//\n\t// Distance is to the entity's **box**, not to its centre, so a big thing is as near as its nearest edge -\n\t// which is what a ship closing on a station wants, and it makes `maxDistance` a range past the hull rather\n\t// than a range that a large enough target could sit outside of while overlapping the searcher.\n\tfindNearest(x: number, y: number, maxDistance = Infinity, filter?: SpatialFilter<T>): SpatialEntity<T> | undefined {\n\t\treturn this.findNearby(x, y, 1, maxDistance, filter)[0];\n\t}\n\n\t// The `maxResults` closest entities to (x, y), nearest first, on the same terms as findNearest - for a game\n\t// that wants a few to choose between rather than only the one.\n\tfindNearby(x: number, y: number, maxResults: number, maxDistance = Infinity, filter?: SpatialFilter<T>): Array<SpatialEntity<T>> {\n\t\tconst index = this.index;\n\t\tif(!index) {\n\t\t\treturn [];\n\t\t}\n\n\t\tconst entries = this.entries;\n\t\t// Handed to Flatbush rather than applied afterwards so that `maxResults` counts entities the game actually\n\t\t// wants: filtering after the fact would let a handful of rejects use up the whole allowance and return\n\t\t// nothing while a valid target sat just behind them.\n\t\tconst found = filter ? index.neighbors(x, y, maxResults, maxDistance, i => filter(entries[i]))\n\t\t\t: index.neighbors(x, y, maxResults, maxDistance);\n\n\t\treturn found.map(i => entries[i]);\n\t}\n}\n","import { addAtomicFloat32 } from '@daneren2005/shared-memory-objects';\nimport type { ComponentMap, ComponentSystemCallbacks, ComponentSystemWorld, EntityQueryComponents, EntityUpdateComponents, EntityUpdateFunction } from '@daneren2005/shared-memory-ecs';\nimport type { PhysicsComponents, PhysicsUpdateComponents } from '../components/registry';\nimport { TRANSFORM_X_INDEX, TRANSFORM_Y_INDEX } from '../components/transform-component';\nimport { VELOCITY_X_INDEX, VELOCITY_Y_INDEX } from '../components/velocity-component';\nimport CollisionBroadphase, { COLLIDABLE_QUERY, type CollisionEntity, type CollisionFunction, type MovingEntity } from './collision';\n\n// The event a move is reported through, emitted on the entity itself on the main thread once the run that\n// moved it completes:\n//\n// entity.on(POSITION_UPDATED_EVENT, (x: number, y: number) => { ... });\n//\n// It is deliberately not the ECS's own `component-property-updated`: that one is a property at a time, so a\n// diagonal move costs two events across the worker boundary - and every listener keyed off position pays for\n// every other property change on the way past.\nexport const POSITION_UPDATED_EVENT = 'position-updated';\n\n// What a game asks createPhysicsUpdate for on top of plain movement.\nexport interface PhysicsUpdateOptions<\n\tC extends ComponentMap,\n\tT extends PhysicsUpdateComponents & EntityUpdateComponents<C>,\n\tW extends ComponentSystemWorld = ComponentSystemWorld,\n> {\n\t// Run for each entity that has moved into another one, straight after its own move.\n\tonCollision?: CollisionFunction<C, T, W>\n\t// Extra components to hand the update - and so the collision callback - beyond the transform and velocity\n\t// physics itself needs. A game that wants to take damage on collision lists its health component here so\n\t// that block travels to the worker alongside the transform. They are optional per entity: an entity that\n\t// does not have one is still moved, and still collides, it just arrives without that block.\n\toptional?: Array<keyof C & string>\n}\n\n// What PhysicsSystem needs to know about an update function to set up the same components and queries the\n// function expects. Stamped on by createPhysicsUpdate so a game declares its collision setup in the one\n// module both its worker file and its main thread system import, with no chance of the two drifting apart.\nexport interface PhysicsUpdateMetadata<C extends ComponentMap> {\n\tcollision: boolean\n\toptional: Array<keyof C & string>\n}\n\nexport type PhysicsUpdateFunction<\n\tC extends ComponentMap,\n\tT extends PhysicsUpdateComponents & EntityUpdateComponents<C>,\n\tW extends ComponentSystemWorld = ComponentSystemWorld,\n> = EntityUpdateFunction<C, T, W> & {\n\tphysics: PhysicsUpdateMetadata<C>\n};\n\n// Builds the update function a game runs physics through when it wants collision callbacks as well as\n// movement. The result is a plain EntityUpdateFunction, so it goes to both backends the same way the bare\n// physicsUpdate does - handed to createComponentWorker in the game's worker file, and to PhysicsSystem as its\n// `updateFunction` for the in-process fallback:\n//\n// // game-physics-update.ts - imported by both the worker file and the main thread system\n// export const gamePhysicsUpdate = createPhysicsUpdate<Components, GameUpdateComponents>({\n// optional: ['health'],\n// onCollision(world, a, b, queries, callbacks) { ... },\n// });\n//\n// A callback cannot be sent to a worker (functions do not survive postMessage), which is why it is baked in\n// here rather than passed to PhysicsSystem: the worker file imports this module and gets the callback with it.\nexport function createPhysicsUpdate<\n\t// Constrained to a map that has the physics components in it rather than to any map at all, because the\n\t// blocks the update works on - `T` below - are the game's own components narrowed to the transform and\n\t// velocity it moves, so the map has to be able to see those two in there. Every game already spreads\n\t// physicsRegistry into its own, which is the same requirement PhysicsSystem makes.\n\tC extends ComponentMap & PhysicsComponents = PhysicsComponents,\n\tT extends PhysicsUpdateComponents & EntityUpdateComponents<C> = PhysicsUpdateComponents & EntityUpdateComponents<C>,\n\tW extends ComponentSystemWorld = ComponentSystemWorld,\n>(options: PhysicsUpdateOptions<C, T, W> = {}): PhysicsUpdateFunction<C, T, W> {\n\tconst { onCollision, optional = [] } = options;\n\n\t// Built by preRun and read back by each entity update that follows it, which is the whole reason it can be\n\t// a plain closure variable: a run is one unbroken pass - preRun, then every entity, then done - so there is\n\t// never a second run part way through this one to overwrite it.\n\tlet broadphase: CollisionBroadphase<T> | undefined;\n\n\tconst update: PhysicsUpdateFunction<C, T, W> = Object.assign(\n\t\t(world: W, entityId: number, components: T, queries: EntityQueryComponents<C>, callbacks: ComponentSystemCallbacks<C>) => {\n\t\t\tconst self: MovingEntity<T> = { entityId, components };\n\t\t\tconst seconds = world.elapsedTime / 1000;\n\t\t\tlet moveX = components.velocity[VELOCITY_X_INDEX] * seconds;\n\t\t\tlet moveY = components.velocity[VELOCITY_Y_INDEX] * seconds;\n\n\t\t\t// Where the entity may actually get to is worked out here, off the blocks rather than in them: the\n\t\t\t// position is only written once the answer is known, so an entity is never inside another one - not\n\t\t\t// even for the moment it would take to push it back out.\n\t\t\tlet blocking: Array<CollisionEntity<T>> | undefined;\n\t\t\tif(broadphase) {\n\t\t\t\tconst sweep = broadphase.sweep(self, moveX, moveY);\n\t\t\t\tmoveX *= sweep.fraction;\n\t\t\t\tmoveY *= sweep.fraction;\n\t\t\t\tblocking = sweep.blocking;\n\t\t\t}\n\n\t\t\tmove(entityId, components.transform, moveX, moveY, callbacks);\n\n\t\t\tif(!onCollision || !broadphase) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Collisions are reported here, straight after the entity has been put down, so a callback acts on\n\t\t\t// where it has actually ended up rather than where it was - and a bounce that flips its velocity is\n\t\t\t// applied by the next run's move. Only the entity that moved gets the call; whatever it hit gets its\n\t\t\t// own when its turn comes, or never, if it is something the system does not move.\n\t\t\tbroadphase.forEachOverlapping(self, other => {\n\t\t\t\tonCollision(world, self, other, queries, callbacks);\n\t\t\t});\n\n\t\t\t// Then whatever the move was stopped *against*, which the overlap check above cannot find by design:\n\t\t\t// the entity was put down touching it rather than through it. The two lists never share an entry, so\n\t\t\t// nothing here has already had its callback.\n\t\t\tif(blocking) {\n\t\t\t\tfor(const other of blocking) {\n\t\t\t\t\tonCollision(world, self, other, queries, callbacks);\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\tphysics: {\n\t\t\t\tcollision: onCollision !== undefined,\n\t\t\t\toptional,\n\t\t\t},\n\t\t},\n\t);\n\n\tif(onCollision) {\n\t\t// preRun is the one hook that sees every entity at once, which makes it the only place the tree can be\n\t\t// built from a single consistent moment - before any of this run's movement has happened.\n\t\tupdate.preRun = (world, entities, queries) => {\n\t\t\tbroadphase = new CollisionBroadphase<T>(queries[COLLIDABLE_QUERY] ?? [], world.elapsedTime / 1000);\n\t\t};\n\t}\n\n\treturn update;\n}\n\n// Shared update logic used by both ComponentSystem backends: the main-thread ComponentWebWorker runs it\n// directly (PhysicsSystem passes it as `updateFunction`), while a game's worker file imports it and hands it\n// to createComponentWorker. Keeping it in one place means both paths run identical logic.\n//\n// This is movement only. A game that also wants collision callbacks builds its update with\n// createPhysicsUpdate instead, which adds them around this same move.\nexport const physicsUpdate: EntityUpdateFunction<PhysicsComponents, PhysicsUpdateComponents> = (world, entityId, components, queries, callbacks) => {\n\t// elapsedTime is in milliseconds (what BaseWorld#update is driven with), while velocity is in world units\n\t// per second - so convert before integrating.\n\tconst seconds = world.elapsedTime / 1000;\n\n\tmove(\n\t\tentityId,\n\t\tcomponents.transform,\n\t\tcomponents.velocity[VELOCITY_X_INDEX] * seconds,\n\t\tcomponents.velocity[VELOCITY_Y_INDEX] * seconds,\n\t\tcallbacks,\n\t);\n};\n\n// Applies a move that has already been decided on - by the integration above, or by what a sweep allowed of it -\n// and reports where the entity ended up. Because the blocks live in a SharedArrayBuffer, writing them here is\n// immediately visible through `entity.components.transform` on the main thread.\n//\n// This is the one place the position is written, and it is only reached once the final position is known: the\n// distance is worked out on plain numbers first so that nothing ever reads a transform mid-decision.\n//\n// The move is applied with addAtomicFloat32 rather than `+=`: the position is shared memory that another\n// thread (a game's own system, or a later physics system) may be adding to at the same moment, and a plain\n// read-modify-write would silently drop one of the two moves. Its `max` cap is not wanted here - physics\n// does not bound how far something may travel - so it is passed Infinity, leaving a plain atomic add. It is\n// also why the value reported back is read out of the block afterwards rather than being worked out here: with\n// another thread adding to the same position, the block is the only thing that knows the total.\n//\n// A move that did not go anywhere is not reported, which is what keeps a world of still entities quiet - and an\n// entity pressed up against something it cannot move past reports nothing at all rather than a change per run.\n// An entity that did move reports both axes in the one event, even when only one of them changed: a listener\n// keyed off position - a spatial index, a minimap, a sprite - wants the place rather than the axis, and a\n// second event to tell it the other half of a diagonal move is one it would only have to put back together.\nfunction move(entityId: number, transform: Float32Array, moveX: number, moveY: number, callbacks: PositionCallbacks): void {\n\tif(moveX === 0 && moveY === 0) {\n\t\treturn;\n\t}\n\n\tif(moveX !== 0) {\n\t\taddAtomicFloat32(transform, TRANSFORM_X_INDEX, moveX, Infinity);\n\t}\n\tif(moveY !== 0) {\n\t\taddAtomicFloat32(transform, TRANSFORM_Y_INDEX, moveY, Infinity);\n\t}\n\n\t// Both read back out of the block after every write, so the axis that did not move this run still arrives as\n\t// where the entity actually is - which is not necessarily where it was, since another thread may have moved\n\t// it along that axis in the meantime.\n\tcallbacks.emitEntityEvent(entityId, POSITION_UPDATED_EVENT, transform[TRANSFORM_X_INDEX], transform[TRANSFORM_Y_INDEX]);\n}\n\n// The one callback move reaches for, narrowed to the event physics emits and the two numbers it carries.\n// ComponentSystemCallbacks takes any event with any args, which says nothing about what to listen for or what\n// arrives with it - so the positions coming out of the block are handed over through this instead.\ninterface PositionCallbacks {\n\temitEntityEvent(entityId: number, event: typeof POSITION_UPDATED_EVENT, x: number, y: number): void\n}\n\nexport default physicsUpdate;\n","import { ComponentSystem } from '@daneren2005/shared-memory-ecs';\nimport type { BaseWorld, ComponentDefinitionMap, ComponentMap, ComponentSystemWorld, EntityUpdateComponents, EntityUpdateFunction, SystemConfig } from '@daneren2005/shared-memory-ecs';\nimport type { PhysicsComponents, PhysicsUpdateComponents } from '../components/registry';\nimport physicsUpdate, { type PhysicsUpdateMetadata } from './physics-update';\nimport { COLLIDABLE_QUERY } from './collision';\n\nexport interface PhysicsSystemConfig<\n\tC extends ComponentMap & PhysicsComponents,\n\tT extends PhysicsUpdateComponents & EntityUpdateComponents<C>,\n\tW extends ComponentSystemWorld = ComponentSystemWorld,\n> extends Partial<SystemConfig> {\n\t// Worker entry point that runs the update off the main thread. Bundlers need the `new Worker(new\n\t// URL(...))` call to be written out in the game's own source, so the game supplies the file:\n\t//\n\t// // physics.worker.ts\n\t// import { createComponentWorker } from '@daneren2005/shared-memory-ecs';\n\t// import { physicsUpdate } from '@daneren2005/shared-memory-physics';\n\t// createComponentWorker(self, physicsUpdate);\n\t//\n\t// getWorker: () => new Worker(new URL('./physics.worker.ts', import.meta.url), { type: 'module' })\n\t//\n\t// Left off, the simulation runs in-process on the main thread instead.\n\tgetWorker?: () => Worker\n\t// Forces the in-process ComponentWebWorker even when a getWorker is supplied. Defaults to true when no\n\t// getWorker was given, since there is then nothing to run the update on.\n\tforceMainThread?: boolean\n\t// The update to run, defaulting to plain movement. A game that wants collision callbacks builds one with\n\t// createPhysicsUpdate and passes the *same* function its worker file hands to createComponentWorker, so\n\t// both backends behave identically. createPhysicsUpdate stamps what it needs onto the function, so\n\t// `optional` and the collidable query below come across with it and do not have to be repeated here.\n\tupdateFunction?: EntityUpdateFunction<C, T, W> & { physics?: PhysicsUpdateMetadata<C> }\n\t// Extra components to send along with each entity's transform and velocity, for an update function built\n\t// by hand rather than through createPhysicsUpdate. Overrides what the update function declared.\n\toptional?: Array<keyof C & string>\n\t// Whether to gather the collidable query - every entity with a transform, whether it moves or not - and\n\t// send it along for the update's collision detection. Defaults to whatever the update function declared,\n\t// so it is on exactly when a createPhysicsUpdate was given an onCollision.\n\tcollision?: boolean\n}\n\n// Moves every entity that has both a transform and a velocity, and (when the update function detects\n// collisions) reports the entities that overlap to the game's callback. Generic over `C`, the game's full\n// component map, so a game's own world passes straight in; over `T`, the blocks its update function touches,\n// which is wider than transform + velocity once a game adds `optional` components of its own; and over `W`,\n// the per-run data object, for a game that subclasses this to add more through `addDataToWorld`.\nexport default class PhysicsSystem<\n\tC extends ComponentMap & PhysicsComponents,\n\tT extends PhysicsUpdateComponents & EntityUpdateComponents<C> = PhysicsUpdateComponents & EntityUpdateComponents<C>,\n\tW extends ComponentSystemWorld = ComponentSystemWorld,\n> extends ComponentSystem<C, T, W> {\n\tconstructor(world: BaseWorld<ComponentDefinitionMap, C>, options: PhysicsSystemConfig<C, T, W> = {}) {\n\t\tconst updateFunction: EntityUpdateFunction<C, T, W> & { physics?: PhysicsUpdateMetadata<C> } = options.updateFunction ?? physicsUpdate;\n\t\tconst optional = options.optional ?? updateFunction.physics?.optional ?? [];\n\t\tconst collision = options.collision ?? updateFunction.physics?.collision ?? false;\n\t\t// An entity reads its own collide category and mask before it searches, so the body block has to travel\n\t\t// with the entities this system moves as well as with the ones they can run into - but only when there is\n\t\t// collision detection to read it, since otherwise it is a block per moving entity for nothing.\n\t\tconst movingOptional = collision && !optional.includes('body') ? ['body' as keyof C & string, ...optional] : optional;\n\n\t\tsuper(world, {\n\t\t\tname: options.name ?? 'PhysicsSystem',\n\t\t\tdeltaBetweenRuns: options.deltaBetweenRuns,\n\t\t\tfirstRun: options.firstRun,\n\n\t\t\t// Movement needs both, so an entity with only one of them is not in the system at all - though it can\n\t\t\t// still be collided with through the query below.\n\t\t\trequired: ['transform', 'velocity'],\n\t\t\toptional: movingOptional,\n\t\t\tupdateFunction,\n\t\t\t// Collision is between anything with a transform and a body, not only the entities this system moves,\n\t\t\t// so that a ship can hit a station that has no velocity of its own. It is only gathered when something\n\t\t\t// is actually going to look at it, since it means shipping a block for every entity in the world.\n\t\t\tqueries: collision ? {\n\t\t\t\t[COLLIDABLE_QUERY]: {\n\t\t\t\t\trequired: ['transform', 'body'],\n\t\t\t\t\toptional: ['velocity', ...optional],\n\t\t\t\t},\n\t\t\t} : undefined,\n\n\t\t\tforceMainThread: options.forceMainThread ?? !options.getWorker,\n\t\t\t// Only ever reached when a getWorker was supplied: without one forceMainThread defaults to true, so\n\t\t\t// the base class never asks for a worker.\n\t\t\tgetWorker: options.getWorker ?? (() => {\n\t\t\t\tthrow new Error('PhysicsSystem cannot start a worker: no getWorker was supplied');\n\t\t\t}),\n\t\t});\n\t}\n}\n"],"mappings":"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"}
|
package/dist/math/shapes.d.ts
CHANGED
|
@@ -3,7 +3,6 @@ export declare function shapeRadius(shape: number, width: number, height: number
|
|
|
3
3
|
export declare function shapeIsEmpty(shape: number, width: number, height: number): boolean;
|
|
4
4
|
export declare function shapeHalfWidth(shape: number, width: number, height: number, angle: number): number;
|
|
5
5
|
export declare function shapeHalfHeight(shape: number, width: number, height: number, angle: number): number;
|
|
6
|
-
export declare function shapeMinHalfExtent(shape: number, width: number, height: number): number;
|
|
7
6
|
export declare function shapesOverlap(aShape: number, aX: number, aY: number, aWidth: number, aHeight: number, aAngle: number, bShape: number, bX: number, bY: number, bWidth: number, bHeight: number, bAngle: number): boolean;
|
|
8
7
|
export declare function boundsHalfWidth(width: number, height: number, angle: number): number;
|
|
9
8
|
export declare function boundsHalfHeight(width: number, height: number, angle: number): number;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ComponentMap, ComponentSystemWorld, EntityUpdateComponents, EntityUpdateFunction } from '@daneren2005/shared-memory-ecs';
|
|
2
2
|
import type { PhysicsComponents, PhysicsUpdateComponents } from '../components/registry';
|
|
3
3
|
import { type CollisionFunction } from './collision';
|
|
4
|
+
export declare const POSITION_UPDATED_EVENT = "position-updated";
|
|
4
5
|
export interface PhysicsUpdateOptions<C extends ComponentMap, T extends PhysicsUpdateComponents & EntityUpdateComponents<C>, W extends ComponentSystemWorld = ComponentSystemWorld> {
|
|
5
6
|
onCollision?: CollisionFunction<C, T, W>;
|
|
6
7
|
optional?: Array<keyof C & string>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { EntityUpdateComponents } from '@daneren2005/shared-memory-ecs';
|
|
2
|
+
export type SpatialComponents = {
|
|
3
|
+
transform: Float32Array;
|
|
4
|
+
body?: Uint32Array;
|
|
5
|
+
};
|
|
6
|
+
export interface SpatialEntity<T extends SpatialComponents = SpatialComponents> {
|
|
7
|
+
entityId: number;
|
|
8
|
+
components: T;
|
|
9
|
+
}
|
|
10
|
+
export type SpatialFilter<T extends SpatialComponents = SpatialComponents> = (entity: SpatialEntity<T>) => boolean;
|
|
11
|
+
export default class SpatialIndex<T extends SpatialComponents = SpatialComponents> {
|
|
12
|
+
private entries;
|
|
13
|
+
private index;
|
|
14
|
+
constructor(entities: Iterable<{
|
|
15
|
+
entityId: number;
|
|
16
|
+
components: EntityUpdateComponents;
|
|
17
|
+
}>);
|
|
18
|
+
get size(): number;
|
|
19
|
+
search(minX: number, minY: number, maxX: number, maxY: number, filter?: SpatialFilter<T>): Array<SpatialEntity<T>>;
|
|
20
|
+
searchAround(x: number, y: number, reachX: number, reachY: number, filter?: SpatialFilter<T>): Array<SpatialEntity<T>>;
|
|
21
|
+
findNearest(x: number, y: number, maxDistance?: number, filter?: SpatialFilter<T>): SpatialEntity<T> | undefined;
|
|
22
|
+
findNearby(x: number, y: number, maxResults: number, maxDistance?: number, filter?: SpatialFilter<T>): Array<SpatialEntity<T>>;
|
|
23
|
+
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@daneren2005/shared-memory-physics",
|
|
3
3
|
"description": "2D physics components and systems built on the shared-memory ECS.",
|
|
4
4
|
"author": "Scott Jackson",
|
|
5
|
-
"version": "0.0
|
|
5
|
+
"version": "1.0.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"prepare": "husky"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@daneren2005/shared-memory-ecs": "^1.
|
|
38
|
+
"@daneren2005/shared-memory-ecs": "^1.2.0",
|
|
39
39
|
"@daneren2005/shared-memory-objects": "^1.0.0",
|
|
40
40
|
"@semantic-release/git": "^10.0.1",
|
|
41
41
|
"@stylistic/eslint-plugin": "^5.10.0",
|