@daneren2005/shared-memory-physics 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Scott Jackson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,365 @@
1
+ # @daneren2005/shared-memory-physics
2
+
3
+ 2D physics components and systems for
4
+ [`@daneren2005/shared-memory-ecs`](https://www.npmjs.com/package/@daneren2005/shared-memory-ecs). Everything
5
+ lives in the ECS's shared-memory blocks, so the simulation can run on a worker thread while the main thread
6
+ reads positions straight off its entities.
7
+
8
+ The ECS (`>=1.1.1`) and the shared-memory primitives it is built on are **peer dependencies**: a game must
9
+ register these components against the same copy of the ECS it builds its world with.
10
+
11
+ ```sh
12
+ npm install @daneren2005/shared-memory-physics @daneren2005/shared-memory-ecs @daneren2005/shared-memory-objects
13
+ ```
14
+
15
+ ## Components
16
+
17
+ | Component | Config | Serialization | Block |
18
+ | ----------- | -------------------------------------------- | -------------------------- | ---------------------------------------------- |
19
+ | `transform` | `width`, `height`, `angle?` / `radius` | `x`, `y` | `Float32Array` – where, how big, facing |
20
+ | `velocity` | – | `velocityX?`, `velocityY?` | `Float32Array` – world units per second |
21
+ | `body` | `shape?`, `collideCategory?`, `collideMask?` | – | `Uint32Array` – what shape, what collides with |
22
+
23
+ Entity configs are flat and shared across every component, so velocity is keyed as `velocityX` / `velocityY`
24
+ rather than `x` / `y` (which already belong to the transform). Either velocity axis on its own is enough to
25
+ load one, and the missing one defaults to `0`.
26
+
27
+ The transform is split in two. **Config** is what your entity template says - `width` and `height` are
28
+ required, `angle` defaults to `0` - and a size is what loads the component at all: a config with neither a
29
+ width nor a height does not get a transform. **Serialization** is the live state that changes as the world
30
+ runs, so `save()` returns only `x` / `y` and a reloaded entity gets its size and starting facing back from
31
+ your own template rather than the save.
32
+
33
+ `x` / `y` are the **centre** of the box, not a corner: the box is rotated about that point and collision
34
+ projects out from it in both directions. `angle` is in **radians** counter-clockwise from the +x axis, and
35
+ nothing here writes it - it is yours to set, usually from the heading, and collision reads it. `width` /
36
+ `height` are the unrotated size; a box with no area never overlaps anything, not even another box sharing its
37
+ exact position.
38
+
39
+ The `body` is what makes an entity collidable, and a size loads one just as it loads a transform - so
40
+ everything with a place in the world collides with everything else until you say otherwise. All of it is
41
+ defining config out of your own template, which is why `save` returns nothing for it: a reloaded entity gets
42
+ its shape and categories back from the template rather than from the save. See
43
+ [Body shapes](#body-shapes) for `shape` and [Collision filtering](#collision-filtering) for the other two.
44
+
45
+ ## Body shapes
46
+
47
+ An entity is one of three shapes, and all three live in the same transform - so which one it is changes what
48
+ that width and height *mean* rather than where they are kept:
49
+
50
+ | `shape` | Config | What it is |
51
+ | ----------------- | ------------------------------------- | ------------------------------------------------------------- |
52
+ | `SHAPE_RECTANGLE` | `width`, `height` | a `width` x `height` box turned to `angle` – the default |
53
+ | `SHAPE_CIRCLE` | `radius` | a circle of diameter `width`, which `angle` does not affect |
54
+ | `SHAPE_CAPSULE` | `width`, `height`, `shape` | `width` from end to end, `height` thick, lying along `angle` |
55
+
56
+ ```ts
57
+ import { SHAPE_CAPSULE } from '@daneren2005/shared-memory-physics';
58
+
59
+ world.loadEntity({ x: 0, y: 0, width: 10, height: 4 }); // a rectangle
60
+ world.loadEntity({ x: 0, y: 0, radius: 5 }); // a circle
61
+ world.loadEntity({ x: 0, y: 0, width: 40, height: 10, shape: SHAPE_CAPSULE }); // a capsule
62
+ ```
63
+
64
+ `radius` is the same size said differently: it loads as a `width` and a `height` of the diameter, so there is
65
+ still only one size in the block however the config spelled it. Giving both a `radius` and a `width` throws
66
+ rather than one quietly winning. A config that says nothing about its `shape` is read off the size it gave - a
67
+ `radius` describes a circle, anything else a rectangle - so **only capsules have to name their shape**, and an
68
+ unrecognised one throws rather than quietly becoming a rectangle.
69
+
70
+ A capsule's `width` is its **whole** length, caps included, and it lies along the way the entity faces: a 40x10
71
+ capsule is a 30-long core with a 5 radius cap at each end, and it is the same 10 thick all the way down. One
72
+ told it is no longer than it is wide collapses to a circle rather than turning itself inside out.
73
+
74
+ Because a circle is a capsule whose core has no length, all three shapes are *an oriented core grown by a
75
+ radius* - a box with no radius, a point, or a segment - which leaves only three overlap tests rather than one
76
+ per pairing: box against box (the separating axis test), core against core, and core against box. Touching
77
+ exactly still counts as **not** overlapping, whichever pair it is, and a shape with no area never overlaps
78
+ anything. Which of the two sizes decides that depends on the shape: a circle with no height is a perfectly
79
+ good circle, while a rectangle with no height is nothing at all.
80
+
81
+ `shapesOverlap` is exported, along with `shapeHalfWidth` / `shapeHalfHeight` (the axis-aligned box a shape is
82
+ indexed under), `shapeRadius`, `shapeIsEmpty`, `capsuleHalfLength`, and the distance primitives underneath -
83
+ `segmentSegmentDistanceSquared`, `segmentBoxDistanceSquared` and `pointSegmentDistanceSquared`.
84
+ `orientedBoxesOverlap`, `boundsHalfWidth` and `boundsHalfHeight` are still there and still rectangle-only.
85
+
86
+ ## Registering
87
+
88
+ Spread `physicsRegistry` into your own registry so the world allocates the memory pools for these components
89
+ alongside your game specific ones. The keys are what the systems query on, so keep them as they are:
90
+
91
+ ```ts
92
+ import { BaseWorld } from '@daneren2005/shared-memory-ecs';
93
+ import { physicsRegistry } from '@daneren2005/shared-memory-physics';
94
+
95
+ const registry = {
96
+ ...physicsRegistry,
97
+ health: healthDefinition,
98
+ };
99
+ const world = new BaseWorld(registry);
100
+
101
+ const entity = world.loadEntity({ x: 0, y: 0, width: 10, height: 5, velocityX: 10, velocityY: 0, maxHealth: 100 });
102
+ console.log(entity.components.transform!.x); // 0
103
+ ```
104
+
105
+ ## PhysicsSystem
106
+
107
+ `PhysicsSystem` moves every entity that has both a `transform` and a `velocity`, integrating velocity into
108
+ position once per run - and, once you give it an update built by [`createPhysicsUpdate`](#collisions), tells
109
+ you which entities have run into each other. It is generic over your full component map, so your world passes
110
+ straight in:
111
+
112
+ ```ts
113
+ import { PhysicsSystem } from '@daneren2005/shared-memory-physics';
114
+
115
+ world.addSystem(new PhysicsSystem(world));
116
+
117
+ await world.init();
118
+ world.update(1000); // one second: the entity above is now at x = 10
119
+ ```
120
+
121
+ `elapsedTime` is in milliseconds (what `BaseWorld#update` is driven with) and velocity is in world units per
122
+ **second**, so a run covering 16ms moves an entity 16/1000ths of its velocity.
123
+
124
+ Each axis an entity actually moved along is reported back to the main thread as a `component-property-updated`
125
+ event, so anything you keep keyed off position can follow a move rather than having to poll the block:
126
+
127
+ ```ts
128
+ entity.on('component-property-updated', (componentName, prop, value) => {
129
+ // 'transform', 'x', 12.5
130
+ });
131
+ ```
132
+
133
+ An axis that did not move says nothing, so a world of still entities stays quiet - and so does an entity held
134
+ against something it cannot move past.
135
+
136
+ The move is written with `addAtomicFloat32` rather than a plain `+=`, so a game system on another thread can
137
+ add to the same position in the same instant without either move being lost.
138
+
139
+ ### Running off the main thread
140
+
141
+ Bundlers need the `new Worker(new URL(...))` call written out in your own source, so the worker entry file
142
+ lives in your game and is handed to the system through `getWorker`:
143
+
144
+ ```ts
145
+ // physics.worker.ts
146
+ import { createComponentWorker } from '@daneren2005/shared-memory-ecs';
147
+ import { physicsUpdate } from '@daneren2005/shared-memory-physics';
148
+
149
+ createComponentWorker(self, physicsUpdate);
150
+ ```
151
+
152
+ ```ts
153
+ world.addSystem(new PhysicsSystem(world, {
154
+ getWorker: () => new Worker(new URL('./physics.worker.ts', import.meta.url), { type: 'module' }),
155
+ // Optional: run at a fixed timestep instead of every frame.
156
+ deltaBetweenRuns: 1000 / 60,
157
+ }));
158
+ ```
159
+
160
+ Without a `getWorker` (or where Web Workers / `SharedArrayBuffer` are unavailable) the exact same
161
+ `physicsUpdate` runs in-process on the main thread instead.
162
+
163
+ ## Collisions
164
+
165
+ `createPhysicsUpdate` builds an update that, right after moving each entity, tells a callback of yours what
166
+ that entity has moved into. The callback runs wherever the update runs - on the worker thread, in the same
167
+ pass as the movement - so it cannot be handed to `PhysicsSystem` at construction time: functions do not
168
+ survive `postMessage`. It reaches the worker by being baked into a module that both the worker file and the
169
+ main thread import.
170
+
171
+ ```ts
172
+ // game-physics-update.ts - imported by *both* the worker file and the system below
173
+ import {
174
+ createPhysicsUpdate,
175
+ VELOCITY_X_INDEX,
176
+ VELOCITY_Y_INDEX,
177
+ type PhysicsUpdateComponents,
178
+ } from '@daneren2005/shared-memory-physics';
179
+ import { HEALTH_INDEX } from './health-component';
180
+
181
+ // The blocks the update touches: the two physics needs, plus your own.
182
+ export type GameUpdateComponents = PhysicsUpdateComponents & { health?: Float32Array };
183
+
184
+ export const gamePhysicsUpdate = createPhysicsUpdate<Components, GameUpdateComponents>({
185
+ // Extra components to send along to the callback, on top of the transform and velocity.
186
+ optional: ['health'],
187
+ onCollision(world, self, other, queries, callbacks) {
188
+ // `self` just moved into `other`. Bounce it back off whatever it hit...
189
+ self.components.velocity[VELOCITY_X_INDEX] *= -1;
190
+ self.components.velocity[VELOCITY_Y_INDEX] *= -1;
191
+
192
+ // ...and take a bite out of the thing it ran into, if that is something with health.
193
+ const health = other.components.health;
194
+ if(health && (health[HEALTH_INDEX] -= 1) <= 0) {
195
+ // The worker has blocks, not entities, so a death goes back to the main thread as an event.
196
+ callbacks.entityDied(other.entityId);
197
+ }
198
+ },
199
+ });
200
+ ```
201
+
202
+ ```ts
203
+ // physics.worker.ts
204
+ import { createComponentWorker } from '@daneren2005/shared-memory-ecs';
205
+ import { gamePhysicsUpdate } from './game-physics-update';
206
+
207
+ createComponentWorker(self, gamePhysicsUpdate);
208
+ ```
209
+
210
+ ```ts
211
+ world.addSystem(new PhysicsSystem<Components, GameUpdateComponents>(world, {
212
+ updateFunction: gamePhysicsUpdate,
213
+ getWorker: () => new Worker(new URL('./physics.worker.ts', import.meta.url), { type: 'module' }),
214
+ }));
215
+ ```
216
+
217
+ `createPhysicsUpdate` stamps what it needs onto the function it returns, so `optional` and the collidable
218
+ query come across with `updateFunction` and never have to be repeated on the system.
219
+
220
+ **Per entity, not per pair.** The callback fires for the entity that *moved*, once for each thing it ended up
221
+ on top of. `self` is that entity, with every block the system asked for; `other` is what it hit, where only
222
+ `transform` is guaranteed. Two moving entities that run into each other therefore get one call each with the
223
+ roles swapped - which is what lets a callback act on itself and leave the other side to its own call. An
224
+ entity with no velocity is never `self`, because the system never moves it, but it is still found as `other`.
225
+
226
+ Writes go straight into shared memory, so a callback can bounce an entity by flipping its velocity, or push a
227
+ value another thread also touches with the atomics from `@daneren2005/shared-memory-objects`. To reach past
228
+ the two entities in front of it - to credit a third for a kill, say - a callback can walk
229
+ `queries[COLLIDABLE_QUERY]`, the full collidable list. Anything that has to happen on the main thread goes
230
+ through `callbacks`: `entityDied`, `entityComponentChanged`, `createEntity`.
231
+
232
+ **What collides.** Everything with a transform and a `body`, not only the entities the system moves, so a
233
+ ship can run into a station that has no velocity of its own. Shapes that only just touch do not count as
234
+ overlapping, and one with no area never collides at all.
235
+
236
+ ### Stopping at the edge
237
+
238
+ An entity is never moved into another one. Before anything is written to the transform, the move is swept
239
+ against everything along its path, and the entity is put down on the edge of whatever is in the way - then
240
+ `onCollision` runs for it, exactly as it would if the two had ended up overlapping.
241
+
242
+ ```ts
243
+ // A ship at x = 0 and a station at x = 30, both 10 wide, with a velocity that asks for 100 in this run.
244
+ world.update(1000);
245
+ console.log(ship.components.transform!.x); // ~20, their edges touching - not 100, and never 25
246
+ ```
247
+
248
+ The whole path is one decision rather than one per entity found along it, so a mover closing on two units in a
249
+ line comes to rest against the *near* one however the two happened to come up, and only the near one is
250
+ reported as a collision - the far one was never reached. An entity wedged between two at the same moment gets a
251
+ call for each.
252
+
253
+ Two details worth knowing:
254
+
255
+ - **An entity that was already inside another one is let through.** Something you spawned on top of it, or
256
+ another system pushed it into, cannot be what *this* move ran into, and blocking on it would pin the entity
257
+ there for good. It still gets its `onCollision` call for the overlap.
258
+ - **An entity with nowhere left to go stays exactly where it is** rather than creeping the last fraction
259
+ forwards each run, so it reports no position change while it pushes - but it does keep reporting the
260
+ collision for as long as it keeps pushing.
261
+
262
+ Contact is found by walking the path and then halving in on it, rather than solved: three shapes at any
263
+ rotation to one another have no single formula for when a pair first touches, while the overlap test the
264
+ 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` of world units of touching. A
266
+ move long enough to cross many times the size of what is in the way runs out of that walk's step budget, and
267
+ can still pass clean through something very thin; a fixed `deltaBetweenRuns` is the cure if that is ever a real
268
+ prospect in your game.
269
+
270
+ ### Collision filtering
271
+
272
+ Every body collides **as** a `collideCategory` and **with** a `collideMask`, both plain 32-bit fields. The
273
+ categories are yours to name - this library only ever reads them as bits - and a pair collides when each
274
+ side's mask accepts the other's category:
275
+
276
+ ```ts
277
+ // Your own categories, in your own game.
278
+ const GROUND = 1 << 0;
279
+ const AIR = 1 << 1;
280
+ const PROJECTILE = 1 << 2;
281
+
282
+ world.loadEntity({ x: 0, y: 0, width: 10, height: 10, collideCategory: GROUND, collideMask: GROUND | PROJECTILE });
283
+ world.loadEntity({ x: 0, y: 0, width: 10, height: 10, collideCategory: AIR, collideMask: AIR | PROJECTILE });
284
+ // A ranged attack the ground unit fires, which can hit either of them.
285
+ world.loadEntity({ x: 0, y: 0, width: 2, height: 2, collideCategory: PROJECTILE, collideMask: GROUND | AIR });
286
+ ```
287
+
288
+ The ground unit and the air unit pass straight through each other; the attack hits both.
289
+
290
+ The test is **symmetric** - both sides have to accept each other - so a pair either collides or does not
291
+ regardless of which one moved into which. That matters because collisions are reported per entity as each one
292
+ moves: a rule that depended on the direction would make the outcome depend on the order the system happened
293
+ to move them in. The thing to remember is that the unit above has to name `PROJECTILE` in its own mask; a
294
+ projectile naming `GROUND` is not on its own enough. A one-sided *response* - an attack that hurts a unit
295
+ without the unit hurting it back - is something your `onCollision` decides by looking at `other`, not
296
+ something the two entities disagree about having touched.
297
+
298
+ `collideCategory` defaults to `1` and `collideMask` to every bit, so a world that sets neither behaves as it
299
+ did before categories existed. Either `collideCategory: 0` or `collideMask: 0` opts an entity out of
300
+ collisions entirely, in both directions. `canCollide(categoryA, maskA, categoryB, maskB)` is exported if you
301
+ want to run the same test yourself.
302
+
303
+ Both fields live in the shared block, so a system on any thread can write them and the next run picks the
304
+ change up - a unit that takes off becomes an air unit without anything being re-sent to the worker.
305
+
306
+ The broadphase is bucketed by category rather than being one tree over everything, so a projectile that can
307
+ only hit units never walks the tree the terrain is in.
308
+
309
+ **How it is found.** `preRun` builds a [Flatbush](https://github.com/mourner/flatbush) R-tree over every
310
+ collidable entity as it stands at the top of the run. That is the only moment they all agree on, so each
311
+ entity's box is grown by however far its velocity could carry it before the run is out, keeping the tree a
312
+ true superset of what can really collide - grown both ways rather than along the heading, since a callback
313
+ can turn an entity around before it has had its own move. Then each entity, as it moves, searches the trees its
314
+ mask accepts with where it has actually ended up, and every candidate that gets past the masks goes through the
315
+ full [shape](#body-shapes) test. `CollisionBroadphase` is exported if you want the tree itself, and the
316
+ geometry is all reachable on its own - see the end of [Body shapes](#body-shapes).
317
+
318
+ **One thing to know about the ordering.** Entities move one at a time, and the narrowphase reads live
319
+ positions - so an entity that has not had its own move yet is still tested where it started. Two entities
320
+ closing head on will usually collide off the *second* one's move, when both are in their new places, rather
321
+ than off both. The pair is never missed, just attributed to the side that moved last. The same ordering is why
322
+ the entity that moves first gets the clear road: it takes its whole move, and the second one is the one that
323
+ comes to rest against it.
324
+
325
+ ## Using physicsUpdate in your own system
326
+
327
+ `physicsUpdate` is a plain `EntityUpdateFunction`, so a game that already has its own movement system can call
328
+ it rather than adding a second system - the block offsets it reads are exported too:
329
+
330
+ ```ts
331
+ import { physicsUpdate, TRANSFORM_X_INDEX, VELOCITY_X_INDEX } from '@daneren2005/shared-memory-physics';
332
+ ```
333
+
334
+ `CollisionBroadphase` is available the same way, for a system that wants the collision detection without the
335
+ movement. Build one per run from a list of `{ entityId, components }` and how many **seconds** the run covers
336
+ (that is what sizes the room left for movement), then ask it what any given entity is overlapping:
337
+
338
+ ```ts
339
+ import { CollisionBroadphase, COLLIDABLE_QUERY } from '@daneren2005/shared-memory-physics';
340
+
341
+ const broadphase = new CollisionBroadphase(queries[COLLIDABLE_QUERY], world.elapsedTime / 1000);
342
+ broadphase.forEachOverlapping({ entityId, components }, other => { /* ... */ });
343
+ ```
344
+
345
+ `sweep` is the other half, for a system that moves things itself and wants the same
346
+ [stopping at the edge](#stopping-at-the-edge). Hand it the move you were about to make and it says how much of
347
+ it is available, along with whatever the rest of it would have run into:
348
+
349
+ ```ts
350
+ const { fraction, blocking } = broadphase.sweep({ entityId, components }, moveX, moveY);
351
+ transform[TRANSFORM_X_INDEX] += moveX * fraction;
352
+ transform[TRANSFORM_Y_INDEX] += moveY * fraction;
353
+ ```
354
+
355
+ `blocking` is what the entity came to rest *against*, which `forEachOverlapping` will not report from that
356
+ resting place: the two are touching rather than through each other.
357
+
358
+ ## Building
359
+
360
+ ```sh
361
+ npm install
362
+ npm run build # emits dist/ (js + d.ts)
363
+ npm run type-check
364
+ npm test
365
+ ```
@@ -0,0 +1,27 @@
1
+ import type { ComponentDefinition } from '@daneren2005/shared-memory-ecs';
2
+ export interface BodyComponent {
3
+ index: number;
4
+ shape: number;
5
+ collideCategory: number;
6
+ collideMask: number;
7
+ }
8
+ export declare const SHAPE_RECTANGLE = 1;
9
+ export declare const SHAPE_CIRCLE = 2;
10
+ export declare const SHAPE_CAPSULE = 3;
11
+ export declare const DEFAULT_COLLIDE_CATEGORY = 1;
12
+ export declare const DEFAULT_COLLIDE_MASK = 4294967295;
13
+ export declare function canCollide(categoryA: number, maskA: number, categoryB: number, maskB: number): boolean;
14
+ export interface BodyConfig {
15
+ width?: number;
16
+ height?: number;
17
+ shape?: number;
18
+ radius?: number;
19
+ collideCategory?: number;
20
+ collideMask?: number;
21
+ }
22
+ export declare const BODY_SHAPE_INDEX = 0;
23
+ export declare const BODY_CATEGORY_INDEX = 1;
24
+ export declare const BODY_MASK_INDEX = 2;
25
+ export declare const BODY_SIZE = 3;
26
+ export declare const bodyDefinition: ComponentDefinition<BodyComponent, Uint32Array, BodyConfig>;
27
+ export default bodyDefinition;
@@ -0,0 +1,18 @@
1
+ import { type BodyComponent } from './body-component';
2
+ import { type TransformComponent } from './transform-component';
3
+ import { type VelocityComponent } from './velocity-component';
4
+ export declare const physicsRegistry: {
5
+ transform: import("@daneren2005/shared-memory-ecs").ComponentDefinition<TransformComponent, Float32Array<ArrayBufferLike>, import("./transform-component").TransformConfig, import("./transform-component").TransformSerialization>;
6
+ velocity: import("@daneren2005/shared-memory-ecs").ComponentDefinition<VelocityComponent, Float32Array<ArrayBufferLike>, import("./velocity-component").VelocityConfig, import("./velocity-component").VelocityConfig>;
7
+ body: import("@daneren2005/shared-memory-ecs").ComponentDefinition<BodyComponent, Uint32Array<ArrayBufferLike>, import("./body-component").BodyConfig, object>;
8
+ };
9
+ export type PhysicsComponents = {
10
+ transform: TransformComponent;
11
+ velocity: VelocityComponent;
12
+ body: BodyComponent;
13
+ };
14
+ export type PhysicsUpdateComponents = {
15
+ transform: Float32Array;
16
+ velocity: Float32Array;
17
+ body?: Uint32Array;
18
+ };
@@ -0,0 +1,27 @@
1
+ import type { ComponentDefinition } from '@daneren2005/shared-memory-ecs';
2
+ export interface TransformComponent {
3
+ index: number;
4
+ x: number;
5
+ y: number;
6
+ width: number;
7
+ height: number;
8
+ angle: number;
9
+ }
10
+ export interface TransformConfig {
11
+ width: number;
12
+ height: number;
13
+ angle?: number;
14
+ radius?: number;
15
+ }
16
+ export interface TransformSerialization {
17
+ x: number;
18
+ y: number;
19
+ }
20
+ export declare const TRANSFORM_X_INDEX = 0;
21
+ export declare const TRANSFORM_Y_INDEX = 1;
22
+ export declare const TRANSFORM_WIDTH_INDEX = 2;
23
+ export declare const TRANSFORM_HEIGHT_INDEX = 3;
24
+ export declare const TRANSFORM_ANGLE_INDEX = 4;
25
+ export declare const TRANSFORM_SIZE = 5;
26
+ export declare const transformDefinition: ComponentDefinition<TransformComponent, Float32Array, TransformConfig, TransformSerialization>;
27
+ export default transformDefinition;
@@ -0,0 +1,16 @@
1
+ import type { ComponentDefinition } from '@daneren2005/shared-memory-ecs';
2
+ export interface VelocityComponent {
3
+ index: number;
4
+ velocityX: number;
5
+ velocityY: number;
6
+ }
7
+ export interface VelocityConfig {
8
+ velocityX?: number;
9
+ velocityY?: number;
10
+ }
11
+ export type VelocitySerialization = VelocityConfig;
12
+ export declare const VELOCITY_X_INDEX = 0;
13
+ export declare const VELOCITY_Y_INDEX = 1;
14
+ export declare const VELOCITY_SIZE = 2;
15
+ export declare const velocityDefinition: ComponentDefinition<VelocityComponent, Float32Array, VelocityConfig, VelocitySerialization>;
16
+ export default velocityDefinition;
@@ -0,0 +1,18 @@
1
+ export { default as transformDefinition } from './components/transform-component';
2
+ export { TRANSFORM_X_INDEX, TRANSFORM_Y_INDEX, TRANSFORM_WIDTH_INDEX, TRANSFORM_HEIGHT_INDEX, TRANSFORM_ANGLE_INDEX, TRANSFORM_SIZE, } from './components/transform-component';
3
+ export type { TransformComponent, TransformConfig, TransformSerialization, } from './components/transform-component';
4
+ export { default as velocityDefinition } from './components/velocity-component';
5
+ export { VELOCITY_X_INDEX, VELOCITY_Y_INDEX, VELOCITY_SIZE, } from './components/velocity-component';
6
+ export type { VelocityComponent, VelocityConfig, VelocitySerialization, } from './components/velocity-component';
7
+ export { default as bodyDefinition, canCollide } from './components/body-component';
8
+ export { SHAPE_RECTANGLE, SHAPE_CIRCLE, SHAPE_CAPSULE, DEFAULT_COLLIDE_CATEGORY, DEFAULT_COLLIDE_MASK, BODY_SHAPE_INDEX, BODY_CATEGORY_INDEX, BODY_MASK_INDEX, BODY_SIZE, } from './components/body-component';
9
+ export type { BodyComponent, BodyConfig, } from './components/body-component';
10
+ export { physicsRegistry } from './components/registry';
11
+ export type { PhysicsComponents, PhysicsUpdateComponents } from './components/registry';
12
+ export { shapesOverlap, shapeHalfWidth, shapeHalfHeight, shapeIsEmpty, shapeMinHalfExtent, shapeRadius, capsuleHalfLength, boundsHalfWidth, boundsHalfHeight, orientedBoxesOverlap, segmentSegmentDistanceSquared, segmentBoxDistanceSquared, pointSegmentDistanceSquared, } from './math/shapes';
13
+ export { default as CollisionBroadphase, COLLIDABLE_QUERY } from './systems/collision';
14
+ export type { CollisionComponents, CollisionEntity, CollisionFunction, MovingEntity, SweepResult } from './systems/collision';
15
+ export { default as physicsUpdate, createPhysicsUpdate } from './systems/physics-update';
16
+ export type { PhysicsUpdateFunction, PhysicsUpdateMetadata, PhysicsUpdateOptions } from './systems/physics-update';
17
+ export { default as PhysicsSystem } from './systems/physics-system';
18
+ export type { PhysicsSystemConfig } from './systems/physics-system';
package/dist/index.js ADDED
@@ -0,0 +1,413 @@
1
+ import e from "flatbush";
2
+ import { addAtomicFloat32 as t } from "@daneren2005/shared-memory-objects";
3
+ import { ComponentSystem as n } from "@daneren2005/shared-memory-ecs";
4
+ //#region src/components/transform-component.ts
5
+ var r = 0, i = 1, a = 2, o = 3, s = 4, c = 5, l = {
6
+ type: Float32Array,
7
+ size: 5,
8
+ loadProperties: [
9
+ "width",
10
+ "height",
11
+ "radius"
12
+ ],
13
+ load(e, t, n) {
14
+ let r = n.radius, i = n.width, a = n.height;
15
+ if (r !== void 0) {
16
+ if (i !== void 0 || a !== void 0) throw Error("A transform takes either a radius or a width and height, not both");
17
+ i = a = r * 2;
18
+ }
19
+ let o = t.create([
20
+ n.x,
21
+ n.y,
22
+ i,
23
+ a,
24
+ n.angle ?? 0
25
+ ]), s = t.getBlock(o);
26
+ return {
27
+ index: o,
28
+ get x() {
29
+ return s[0];
30
+ },
31
+ set x(e) {
32
+ s[0] = e;
33
+ },
34
+ get y() {
35
+ return s[1];
36
+ },
37
+ set y(e) {
38
+ s[1] = e;
39
+ },
40
+ get width() {
41
+ return s[2];
42
+ },
43
+ set width(e) {
44
+ s[2] = e;
45
+ },
46
+ get height() {
47
+ return s[3];
48
+ },
49
+ set height(e) {
50
+ s[3] = e;
51
+ },
52
+ get angle() {
53
+ return s[4];
54
+ },
55
+ set angle(e) {
56
+ s[4] = e;
57
+ }
58
+ };
59
+ },
60
+ save(e) {
61
+ return {
62
+ x: e.x,
63
+ y: e.y
64
+ };
65
+ }
66
+ }, u = 0, d = 1, f = 2, p = {
67
+ type: Float32Array,
68
+ size: 2,
69
+ loadProperties: ["velocityX", "velocityY"],
70
+ load(e, t, n) {
71
+ let r = t.create([n.velocityX ?? 0, n.velocityY ?? 0]), i = t.getBlock(r);
72
+ return {
73
+ index: r,
74
+ get velocityX() {
75
+ return i[0];
76
+ },
77
+ set velocityX(e) {
78
+ i[0] = e;
79
+ },
80
+ get velocityY() {
81
+ return i[1];
82
+ },
83
+ set velocityY(e) {
84
+ i[1] = e;
85
+ }
86
+ };
87
+ },
88
+ save(e) {
89
+ return {
90
+ velocityX: e.velocityX,
91
+ velocityY: e.velocityY
92
+ };
93
+ }
94
+ }, m = 1, h = 2, g = 3, _ = [
95
+ 1,
96
+ 2,
97
+ 3
98
+ ], v = 1, y = 4294967295;
99
+ function b(e, t, n, r) {
100
+ return (t & n) !== 0 && (r & e) !== 0;
101
+ }
102
+ var x = 0, ee = 1, S = 2, C = 3, w = {
103
+ type: Uint32Array,
104
+ size: 3,
105
+ loadProperties: [
106
+ "width",
107
+ "height",
108
+ "radius",
109
+ "shape",
110
+ "collideCategory",
111
+ "collideMask"
112
+ ],
113
+ load(e, t, n) {
114
+ let r = t.create([
115
+ T(n),
116
+ n.collideCategory ?? 1,
117
+ n.collideMask ?? 4294967295
118
+ ]), i = t.getBlock(r);
119
+ return {
120
+ index: r,
121
+ get shape() {
122
+ return i[0];
123
+ },
124
+ set shape(e) {
125
+ i[0] = e;
126
+ },
127
+ get collideCategory() {
128
+ return i[1];
129
+ },
130
+ set collideCategory(e) {
131
+ i[1] = e;
132
+ },
133
+ get collideMask() {
134
+ return i[2];
135
+ },
136
+ set collideMask(e) {
137
+ i[2] = e;
138
+ }
139
+ };
140
+ }
141
+ };
142
+ function T(e) {
143
+ let t = e.shape;
144
+ if (t === void 0) return e.radius === void 0 ? 1 : 2;
145
+ if (!_.includes(t)) throw Error(`Unknown body shape: ${t}`);
146
+ return t;
147
+ }
148
+ //#endregion
149
+ //#region src/components/registry.ts
150
+ var E = {
151
+ transform: l,
152
+ velocity: p,
153
+ body: w
154
+ };
155
+ //#endregion
156
+ //#region src/math/shapes.ts
157
+ function D(e, t) {
158
+ return Math.max(0, (e - t) / 2);
159
+ }
160
+ function O(e, t, n) {
161
+ return e === 2 ? t / 2 : e === 3 ? n / 2 : 0;
162
+ }
163
+ function k(e, t, n) {
164
+ return e === 2 ? t <= 0 : e === 3 ? n <= 0 : t <= 0 || n <= 0;
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
+ function j(e, t, n, r) {
170
+ return e === 2 ? t / 2 : e === 3 ? Math.abs(Math.sin(r)) * D(t, n) + n / 2 : I(t, n, r);
171
+ }
172
+ function M(e, t, n) {
173
+ return e === 2 ? t / 2 : e === 3 ? n / 2 : Math.min(t, n) / 2;
174
+ }
175
+ function N(e, t, n, r, i, a, o, s, c, l, u, d) {
176
+ if (k(e, r, i) || k(o, l, u)) return !1;
177
+ let f = e !== 1, p = o !== 1;
178
+ if (!f && !p) return L(t, n, r, i, a, s, c, l, u, d);
179
+ 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, _ = O(o, l, u);
181
+ 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 = O(e, r, i) + _;
183
+ return z(t - y, n - b, t + y, n + b, s - h, c - g, s + h, c + g) < x * x;
184
+ }
185
+ function P(e, t, n) {
186
+ return e === 3 ? D(t, n) : 0;
187
+ }
188
+ function F(e, t, n) {
189
+ return (Math.abs(Math.cos(n)) * e + Math.abs(Math.sin(n)) * t) / 2;
190
+ }
191
+ function I(e, t, n) {
192
+ return (Math.abs(Math.sin(n)) * e + Math.abs(Math.cos(n)) * t) / 2;
193
+ }
194
+ function L(e, t, n, r, i, a, o, s, c, l) {
195
+ if (n <= 0 || r <= 0 || s <= 0 || c <= 0) return !1;
196
+ let u = Math.cos(i), d = Math.sin(i), f = Math.cos(l), p = Math.sin(l), m = n / 2, h = r / 2, g = s / 2, _ = c / 2, v = a - e, y = o - t;
197
+ return !R(u, d, v, y, m, h, u, d, g, _, f, p) && !R(-d, u, v, y, m, h, u, d, g, _, f, p) && !R(f, p, v, y, m, h, u, d, g, _, f, p) && !R(-p, f, v, y, m, h, u, d, g, _, f, p);
198
+ }
199
+ function R(e, t, n, r, i, a, o, s, c, l, u, d) {
200
+ let f = i * Math.abs(o * e + s * t) + a * Math.abs(-s * e + o * t), p = c * Math.abs(u * e + d * t) + l * Math.abs(-d * e + u * t);
201
+ return Math.abs(n * e + r * t) >= f + p;
202
+ }
203
+ function z(e, t, n, r, i, a, o, s) {
204
+ let c = n - e, l = r - t, u = o - i, d = s - a, f = e - i, p = t - a, m = c * c + l * l, h = u * u + d * d, g = u * f + d * p, _ = 0, v = 0;
205
+ if (m <= 0 && h <= 0) return f * f + p * p;
206
+ if (m <= 0) v = W(g / h);
207
+ else {
208
+ let e = c * f + l * p;
209
+ if (h <= 0) _ = W(-e / m);
210
+ else {
211
+ let t = c * u + l * d, n = m * h - t * t;
212
+ n !== 0 && (_ = W((t * g - e * h) / n)), v = (t * _ + g) / h, v < 0 ? (v = 0, _ = W(-e / m)) : v > 1 && (v = 1, _ = W((t - e) / m));
213
+ }
214
+ }
215
+ let y = e + c * _ - (i + u * v), b = t + l * _ - (a + d * v);
216
+ return y * y + b * b;
217
+ }
218
+ function B(e, t, n, r, i, a, o, s, c) {
219
+ let l = Math.cos(c), u = Math.sin(c), d = o / 2, f = s / 2, p = e - i, m = t - a, h = n - i, g = r - a, _ = p * l + m * u, v = -p * u + m * l, y = h * l + g * u, b = -h * u + g * l;
220
+ return H(_, v, y, b, d, f) ? Math.min(U(_, v, d, f), U(y, b, d, f), V(-d, -f, _, v, y, b), V(d, -f, _, v, y, b), V(d, f, _, v, y, b), V(-d, f, _, v, y, b)) : 0;
221
+ }
222
+ function V(e, t, n, r, i, a) {
223
+ let o = i - n, s = a - r, c = o * o + s * s, l = c > 0 ? W(((e - n) * o + (t - r) * s) / c) : 0, u = e - (n + o * l), d = t - (r + s * l);
224
+ return u * u + d * d;
225
+ }
226
+ function H(e, t, n, r, i, a) {
227
+ if (Math.max(e, n) < -i || Math.min(e, n) > i || Math.max(t, r) < -a || Math.min(t, r) > a) return !0;
228
+ let o = -(r - t), s = n - e;
229
+ return Math.abs(o * e + s * t) > i * Math.abs(o) + a * Math.abs(s);
230
+ }
231
+ function U(e, t, n, r) {
232
+ let i = Math.abs(e) - n, a = Math.abs(t) - r, o = i > 0 ? i : 0, s = a > 0 ? a : 0;
233
+ return o * o + s * s;
234
+ }
235
+ function W(e) {
236
+ return e < 0 ? 0 : e > 1 ? 1 : e;
237
+ }
238
+ //#endregion
239
+ //#region src/systems/collision.ts
240
+ var G = 1e-4, K = 20, te = 64, q = "collidable", J = [], Y = class {
241
+ buckets = [];
242
+ constructor(t, n) {
243
+ let r = /* @__PURE__ */ new Map();
244
+ for (let e of t) {
245
+ let t = e.components, i = t.transform, a = t.body;
246
+ if (!i || !a) continue;
247
+ let o = a[1];
248
+ if (o === 0) continue;
249
+ let s = a[0], c = i[2], l = i[3];
250
+ if (k(s, c, l)) continue;
251
+ let u = i[4], d = A(s, c, l, u), f = j(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
+ v || (v = {
253
+ entries: [],
254
+ bounds: []
255
+ }, r.set(o, v)), v.entries.push({
256
+ entityId: e.entityId,
257
+ components: t
258
+ }), v.bounds.push(g - d - m, _ - f - h, g + d + m, _ + f + h);
259
+ }
260
+ for (let [t, { entries: n, bounds: i }] of r) {
261
+ let r = new e(n.length);
262
+ for (let e = 0; e < n.length; e++) r.add(i[e * 4], i[e * 4 + 1], i[e * 4 + 2], i[e * 4 + 3]);
263
+ r.finish(), this.buckets.push({
264
+ category: t,
265
+ entries: n,
266
+ index: r
267
+ });
268
+ }
269
+ }
270
+ forEachOverlapping(e, t) {
271
+ let n = X(e);
272
+ if (!n) return;
273
+ let { x: r, y: i, halfWidth: a, halfHeight: o } = n;
274
+ this.forEachCandidate(n, r - a, i - o, r + a, i + o, (e) => {
275
+ Z(n, r, i, e) && t(e);
276
+ });
277
+ }
278
+ sweep(e, t, n) {
279
+ let r = Math.sqrt(t * t + n * n), i = X(e);
280
+ if (!i || r === 0) return {
281
+ fraction: 1,
282
+ blocking: J
283
+ };
284
+ let { x: a, y: o, halfWidth: s, halfHeight: c } = i, l = a + t, u = o + n, d = [], f = M(i.shape, i.width, i.height);
285
+ if (this.forEachCandidate(i, Math.min(a, l) - s, Math.min(o, u) - c, Math.max(a, l) + s, Math.max(o, u) + c, (e) => {
286
+ if (Z(i, a, o, e)) return;
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]));
290
+ }), d.length === 0) return {
291
+ fraction: 1,
292
+ blocking: J
293
+ };
294
+ let p = 0, m = -1, h = Math.min(te, Math.max(1, Math.ceil(r / f)));
295
+ for (let e = 1; e <= h; e++) {
296
+ let r = e / h;
297
+ if (this.blockedAt(i, d, t, n, r)) {
298
+ m = r;
299
+ break;
300
+ }
301
+ p = r;
302
+ }
303
+ if (m < 0) return {
304
+ fraction: 1,
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);
314
+ return {
315
+ fraction: p * r > G ? p : 0,
316
+ blocking: _
317
+ };
318
+ }
319
+ blockedAt(e, t, n, r, i) {
320
+ let a = e.x + n * i, o = e.y + r * i;
321
+ for (let n of t) if (Z(e, a, o, n)) return !0;
322
+ return !1;
323
+ }
324
+ forEachCandidate(e, t, n, r, i, a) {
325
+ for (let o of this.buckets) if ((e.mask & o.category) !== 0) for (let s of o.index.search(t, n, r, i)) {
326
+ let t = o.entries[s];
327
+ t.entityId !== e.entityId && (t.components.body[2] & e.category) !== 0 && a(t);
328
+ }
329
+ }
330
+ };
331
+ function X(e) {
332
+ let t = e.components.body;
333
+ if (!t) return;
334
+ let n = t[2];
335
+ if (n === 0) return;
336
+ let r = e.components.transform, i = t[0], a = r[2], o = r[3];
337
+ if (k(i, a, o)) return;
338
+ let s = r[4];
339
+ return {
340
+ entityId: e.entityId,
341
+ shape: i,
342
+ x: r[0],
343
+ y: r[1],
344
+ width: a,
345
+ height: o,
346
+ angle: s,
347
+ halfWidth: A(i, a, o, s),
348
+ halfHeight: j(i, a, o, s),
349
+ category: t[1],
350
+ mask: n
351
+ };
352
+ }
353
+ function Z(e, t, n, r) {
354
+ let i = r.components.transform;
355
+ 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
+ }
357
+ //#endregion
358
+ //#region src/systems/physics-update.ts
359
+ function ne(e = {}) {
360
+ let { onCollision: t, optional: n = [] } = e, r, i = Object.assign((e, n, i, a, o) => {
361
+ let s = {
362
+ entityId: n,
363
+ components: i
364
+ }, c = e.elapsedTime / 1e3, l = i.velocity[0] * c, u = i.velocity[1] * c, d;
365
+ if (r) {
366
+ let e = r.sweep(s, l, u);
367
+ l *= e.fraction, u *= e.fraction, d = e.blocking;
368
+ }
369
+ if ($(n, i.transform, l, u, o), !(!t || !r) && (r.forEachOverlapping(s, (n) => {
370
+ t(e, s, n, a, o);
371
+ }), d)) for (let n of d) t(e, s, n, a, o);
372
+ }, { physics: {
373
+ collision: t !== void 0,
374
+ optional: n
375
+ } });
376
+ return t && (i.preRun = (e, t, n) => {
377
+ r = new Y(n.collidable ?? [], e.elapsedTime / 1e3);
378
+ }), i;
379
+ }
380
+ var Q = (e, t, n, r, i) => {
381
+ let a = e.elapsedTime / 1e3;
382
+ $(t, n.transform, n.velocity[0] * a, n.velocity[1] * a, i);
383
+ };
384
+ function $(e, n, r, i, a) {
385
+ r !== 0 && (t(n, 0, r, Infinity), a.entityComponentChanged(e, "transform", "x", n[0])), i !== 0 && (t(n, 1, i, Infinity), a.entityComponentChanged(e, "transform", "y", n[1]));
386
+ }
387
+ //#endregion
388
+ //#region src/systems/physics-system.ts
389
+ var re = class extends n {
390
+ constructor(e, t = {}) {
391
+ let n = t.updateFunction ?? Q, r = t.optional ?? n.physics?.optional ?? [], i = t.collision ?? n.physics?.collision ?? !1, a = i && !r.includes("body") ? ["body", ...r] : r;
392
+ super(e, {
393
+ name: t.name ?? "PhysicsSystem",
394
+ deltaBetweenRuns: t.deltaBetweenRuns,
395
+ firstRun: t.firstRun,
396
+ required: ["transform", "velocity"],
397
+ optional: a,
398
+ updateFunction: n,
399
+ queries: i ? { [q]: {
400
+ required: ["transform", "body"],
401
+ optional: ["velocity", ...r]
402
+ } } : void 0,
403
+ forceMainThread: t.forceMainThread ?? !t.getWorker,
404
+ getWorker: t.getWorker ?? (() => {
405
+ throw Error("PhysicsSystem cannot start a worker: no getWorker was supplied");
406
+ })
407
+ });
408
+ }
409
+ };
410
+ //#endregion
411
+ export { ee as BODY_CATEGORY_INDEX, S as BODY_MASK_INDEX, x as BODY_SHAPE_INDEX, C as BODY_SIZE, q as COLLIDABLE_QUERY, Y as CollisionBroadphase, v as DEFAULT_COLLIDE_CATEGORY, y as DEFAULT_COLLIDE_MASK, re as PhysicsSystem, g as SHAPE_CAPSULE, h as SHAPE_CIRCLE, m as SHAPE_RECTANGLE, 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, w as bodyDefinition, I as boundsHalfHeight, F as boundsHalfWidth, b as canCollide, D as capsuleHalfLength, ne as createPhysicsUpdate, L as orientedBoxesOverlap, E as physicsRegistry, Q as physicsUpdate, V as pointSegmentDistanceSquared, B as segmentBoxDistanceSquared, z as segmentSegmentDistanceSquared, j as shapeHalfHeight, A as shapeHalfWidth, k as shapeIsEmpty, M as shapeMinHalfExtent, O as shapeRadius, N as shapesOverlap, l as transformDefinition, p as velocityDefinition };
412
+
413
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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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"}
@@ -0,0 +1,13 @@
1
+ export declare function capsuleHalfLength(width: number, height: number): number;
2
+ export declare function shapeRadius(shape: number, width: number, height: number): number;
3
+ export declare function shapeIsEmpty(shape: number, width: number, height: number): boolean;
4
+ export declare function shapeHalfWidth(shape: number, width: number, height: number, angle: number): number;
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
+ 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
+ export declare function boundsHalfWidth(width: number, height: number, angle: number): number;
9
+ export declare function boundsHalfHeight(width: number, height: number, angle: number): number;
10
+ export declare function orientedBoxesOverlap(aX: number, aY: number, aWidth: number, aHeight: number, aAngle: number, bX: number, bY: number, bWidth: number, bHeight: number, bAngle: number): boolean;
11
+ export declare function segmentSegmentDistanceSquared(aX1: number, aY1: number, aX2: number, aY2: number, bX1: number, bY1: number, bX2: number, bY2: number): number;
12
+ export declare function segmentBoxDistanceSquared(x1: number, y1: number, x2: number, y2: number, boxX: number, boxY: number, boxWidth: number, boxHeight: number, boxAngle: number): number;
13
+ export declare function pointSegmentDistanceSquared(x: number, y: number, x1: number, y1: number, x2: number, y2: number): number;
@@ -0,0 +1,31 @@
1
+ import type { ComponentMap, ComponentSystemCallbacks, ComponentSystemWorld, EntityQueryComponents, EntityUpdateComponents } from '@daneren2005/shared-memory-ecs';
2
+ import type { PhysicsUpdateComponents } from '../components/registry';
3
+ export declare const COLLIDABLE_QUERY = "collidable";
4
+ export type CollisionComponents<T extends PhysicsUpdateComponents> = Partial<T> & {
5
+ transform: Float32Array;
6
+ body: Uint32Array;
7
+ };
8
+ export interface MovingEntity<T extends PhysicsUpdateComponents> {
9
+ entityId: number;
10
+ components: T;
11
+ }
12
+ export interface CollisionEntity<T extends PhysicsUpdateComponents> {
13
+ entityId: number;
14
+ components: CollisionComponents<T>;
15
+ }
16
+ export type CollisionFunction<C extends ComponentMap, T extends PhysicsUpdateComponents & EntityUpdateComponents<C>, W extends ComponentSystemWorld = ComponentSystemWorld> = (world: W, self: MovingEntity<T>, other: CollisionEntity<T>, queries: EntityQueryComponents<C>, callbacks: ComponentSystemCallbacks<C>) => void;
17
+ export interface SweepResult<T extends PhysicsUpdateComponents> {
18
+ fraction: number;
19
+ blocking: Array<CollisionEntity<T>>;
20
+ }
21
+ export default class CollisionBroadphase<T extends PhysicsUpdateComponents> {
22
+ private buckets;
23
+ constructor(entities: Array<{
24
+ entityId: number;
25
+ components: EntityUpdateComponents;
26
+ }>, seconds: number);
27
+ forEachOverlapping(self: MovingEntity<T>, handle: (other: CollisionEntity<T>) => void): void;
28
+ sweep(self: MovingEntity<T>, moveX: number, moveY: number): SweepResult<T>;
29
+ private blockedAt;
30
+ private forEachCandidate;
31
+ }
@@ -0,0 +1,16 @@
1
+ import { ComponentSystem } from '@daneren2005/shared-memory-ecs';
2
+ import type { BaseWorld, ComponentDefinitionMap, ComponentMap, ComponentSystemWorld, EntityUpdateComponents, EntityUpdateFunction, SystemConfig } from '@daneren2005/shared-memory-ecs';
3
+ import type { PhysicsComponents, PhysicsUpdateComponents } from '../components/registry';
4
+ import { type PhysicsUpdateMetadata } from './physics-update';
5
+ export interface PhysicsSystemConfig<C extends ComponentMap & PhysicsComponents, T extends PhysicsUpdateComponents & EntityUpdateComponents<C>, W extends ComponentSystemWorld = ComponentSystemWorld> extends Partial<SystemConfig> {
6
+ getWorker?: () => Worker;
7
+ forceMainThread?: boolean;
8
+ updateFunction?: EntityUpdateFunction<C, T, W> & {
9
+ physics?: PhysicsUpdateMetadata<C>;
10
+ };
11
+ optional?: Array<keyof C & string>;
12
+ collision?: boolean;
13
+ }
14
+ export default class PhysicsSystem<C extends ComponentMap & PhysicsComponents, T extends PhysicsUpdateComponents & EntityUpdateComponents<C> = PhysicsUpdateComponents & EntityUpdateComponents<C>, W extends ComponentSystemWorld = ComponentSystemWorld> extends ComponentSystem<C, T, W> {
15
+ constructor(world: BaseWorld<ComponentDefinitionMap, C>, options?: PhysicsSystemConfig<C, T, W>);
16
+ }
@@ -0,0 +1,17 @@
1
+ import type { ComponentMap, ComponentSystemWorld, EntityUpdateComponents, EntityUpdateFunction } from '@daneren2005/shared-memory-ecs';
2
+ import type { PhysicsComponents, PhysicsUpdateComponents } from '../components/registry';
3
+ import { type CollisionFunction } from './collision';
4
+ export interface PhysicsUpdateOptions<C extends ComponentMap, T extends PhysicsUpdateComponents & EntityUpdateComponents<C>, W extends ComponentSystemWorld = ComponentSystemWorld> {
5
+ onCollision?: CollisionFunction<C, T, W>;
6
+ optional?: Array<keyof C & string>;
7
+ }
8
+ export interface PhysicsUpdateMetadata<C extends ComponentMap> {
9
+ collision: boolean;
10
+ optional: Array<keyof C & string>;
11
+ }
12
+ export type PhysicsUpdateFunction<C extends ComponentMap, T extends PhysicsUpdateComponents & EntityUpdateComponents<C>, W extends ComponentSystemWorld = ComponentSystemWorld> = EntityUpdateFunction<C, T, W> & {
13
+ physics: PhysicsUpdateMetadata<C>;
14
+ };
15
+ export declare function createPhysicsUpdate<C extends ComponentMap & PhysicsComponents = PhysicsComponents, T extends PhysicsUpdateComponents & EntityUpdateComponents<C> = PhysicsUpdateComponents & EntityUpdateComponents<C>, W extends ComponentSystemWorld = ComponentSystemWorld>(options?: PhysicsUpdateOptions<C, T, W>): PhysicsUpdateFunction<C, T, W>;
16
+ export declare const physicsUpdate: EntityUpdateFunction<PhysicsComponents, PhysicsUpdateComponents>;
17
+ export default physicsUpdate;
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@daneren2005/shared-memory-physics",
3
+ "description": "2D physics components and systems built on the shared-memory ECS.",
4
+ "author": "Scott Jackson",
5
+ "version": "0.0.1",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/daneren2005/shared-memory-physics.git"
10
+ },
11
+ "homepage": "https://github.com/daneren2005/shared-memory-physics#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/daneren2005/shared-memory-physics/issues"
14
+ },
15
+ "main": "./dist/index.js",
16
+ "types": "./dist/index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "import": {
20
+ "types": "./dist/index.d.ts",
21
+ "default": "./dist/index.js"
22
+ }
23
+ }
24
+ },
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "scripts": {
29
+ "build": "vite build && tsc -p tsconfig.build.json --emitDeclarationOnly",
30
+ "type-check": "tsc --noEmit",
31
+ "lint": "cross-env NODE_ENV=production oxlint .",
32
+ "lint:fix": "cross-env NODE_ENV=production oxlint --fix .",
33
+ "test": "vitest run",
34
+ "release": "semantic-release",
35
+ "prepare": "husky"
36
+ },
37
+ "devDependencies": {
38
+ "@daneren2005/shared-memory-ecs": "^1.1.1",
39
+ "@daneren2005/shared-memory-objects": "^1.0.0",
40
+ "@semantic-release/git": "^10.0.1",
41
+ "@stylistic/eslint-plugin": "^5.10.0",
42
+ "@types/node": "^26.1.1",
43
+ "@vitest/web-worker": "^4.1.10",
44
+ "cross-env": "^10.1.0",
45
+ "husky": "^9.1.7",
46
+ "lint-staged": "^17.0.8",
47
+ "oxlint": "^1.76.0",
48
+ "oxlint-tsgolint": "^7.0.2001",
49
+ "semantic-release": "^25.0.6",
50
+ "typescript": "^7.0.2",
51
+ "vite": "^8.1.4",
52
+ "vitest": "^4.1.10"
53
+ },
54
+ "peerDependencies": {
55
+ "@daneren2005/shared-memory-ecs": "^1.1.1",
56
+ "@daneren2005/shared-memory-objects": "^1.0.0"
57
+ },
58
+ "dependencies": {
59
+ "flatbush": "^4.6.2"
60
+ }
61
+ }