@phalanx-engine/physics 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +437 -0
  2. package/dist/PhysicsWorld.d.ts +35 -0
  3. package/dist/PhysicsWorld.d.ts.map +1 -0
  4. package/dist/PhysicsWorld.js +112 -0
  5. package/dist/PhysicsWorldConfig.d.ts +21 -0
  6. package/dist/PhysicsWorldConfig.d.ts.map +1 -0
  7. package/dist/PhysicsWorldConfig.js +1 -0
  8. package/dist/collision/CollisionManifold.d.ts +9 -0
  9. package/dist/collision/CollisionManifold.d.ts.map +1 -0
  10. package/dist/collision/CollisionManifold.js +1 -0
  11. package/dist/collision/NarrowPhase.d.ts +8 -0
  12. package/dist/collision/NarrowPhase.d.ts.map +1 -0
  13. package/dist/collision/NarrowPhase.js +112 -0
  14. package/dist/collision/SpatialHashGrid.d.ts +19 -0
  15. package/dist/collision/SpatialHashGrid.d.ts.map +1 -0
  16. package/dist/collision/SpatialHashGrid.js +125 -0
  17. package/dist/collision/index.d.ts +4 -0
  18. package/dist/collision/index.d.ts.map +1 -0
  19. package/dist/collision/index.js +2 -0
  20. package/dist/components/InterpolationComponent.d.ts +15 -0
  21. package/dist/components/InterpolationComponent.d.ts.map +1 -0
  22. package/dist/components/InterpolationComponent.js +32 -0
  23. package/dist/components/PhysicsBodyComponent.d.ts +53 -0
  24. package/dist/components/PhysicsBodyComponent.d.ts.map +1 -0
  25. package/dist/components/PhysicsBodyComponent.js +157 -0
  26. package/dist/components/TransformComponent.d.ts +32 -0
  27. package/dist/components/TransformComponent.d.ts.map +1 -0
  28. package/dist/components/TransformComponent.js +75 -0
  29. package/dist/components/index.d.ts +4 -0
  30. package/dist/components/index.d.ts.map +1 -0
  31. package/dist/components/index.js +3 -0
  32. package/dist/events.d.ts +7 -0
  33. package/dist/events.d.ts.map +1 -0
  34. package/dist/events.js +6 -0
  35. package/dist/index.d.ts +17 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +8 -0
  38. package/dist/systems/CollisionSystem.d.ts +20 -0
  39. package/dist/systems/CollisionSystem.d.ts.map +1 -0
  40. package/dist/systems/CollisionSystem.js +150 -0
  41. package/dist/systems/InterpolationSystem.d.ts +28 -0
  42. package/dist/systems/InterpolationSystem.d.ts.map +1 -0
  43. package/dist/systems/InterpolationSystem.js +104 -0
  44. package/dist/systems/PhysicsSystem.d.ts +41 -0
  45. package/dist/systems/PhysicsSystem.d.ts.map +1 -0
  46. package/dist/systems/PhysicsSystem.js +316 -0
  47. package/dist/systems/index.d.ts +5 -0
  48. package/dist/systems/index.d.ts.map +1 -0
  49. package/dist/systems/index.js +3 -0
  50. package/dist/tick/AutonomousPhysicsTickProvider.d.ts +18 -0
  51. package/dist/tick/AutonomousPhysicsTickProvider.d.ts.map +1 -0
  52. package/dist/tick/AutonomousPhysicsTickProvider.js +39 -0
  53. package/dist/tick/ExternalPhysicsTickProvider.d.ts +8 -0
  54. package/dist/tick/ExternalPhysicsTickProvider.d.ts.map +1 -0
  55. package/dist/tick/ExternalPhysicsTickProvider.js +6 -0
  56. package/dist/tick/IPhysicsTickProvider.d.ts +5 -0
  57. package/dist/tick/IPhysicsTickProvider.d.ts.map +1 -0
  58. package/dist/tick/IPhysicsTickProvider.js +1 -0
  59. package/dist/tick/index.d.ts +5 -0
  60. package/dist/tick/index.d.ts.map +1 -0
  61. package/dist/tick/index.js +2 -0
  62. package/dist/types.d.ts +37 -0
  63. package/dist/types.d.ts.map +1 -0
  64. package/dist/types.js +1 -0
  65. package/package.json +55 -0
package/README.md ADDED
@@ -0,0 +1,437 @@
1
+ # Phalanx Physics
2
+
3
+ A deterministic, fixed-point physics engine for the [Phalanx Engine](../README.md). Designed for lockstep multiplayer games where every client must produce identical simulation results.
4
+
5
+ > Sibling packages: [phalanx-ecs](../phalanx-ecs/README.md) (ECS core), [phalanx-math](../phalanx-math/README.md) (fixed-point math), [phalanx-server](../phalanx-server/README.md), [phalanx-client](../phalanx-client/README.md).
6
+
7
+ ## Features
8
+
9
+ - **Deterministic by Design**: All math uses `FP.*` fixed-point operations — no floating-point non-determinism
10
+ - **SoA Storage**: Physics body data stored in contiguous typed arrays (`BigInt64Array` for fixed-point, `Uint8Array` for flags) via phalanx-ecs `SoAComponent`
11
+ - **Spatial Hash Grid**: O(n) broad-phase collision detection with configurable cell size
12
+ - **Narrow Phase**: Circle vs Circle, Circle vs AABB, and AABB vs AABB collision tests
13
+ - **Impulse Resolution**: Mass-weighted velocity impulse + positional separation for overlap correction
14
+ - **Sub-stepping**: Configurable physics sub-steps per tick for higher fidelity at the same tick rate
15
+ - **Collision Filtering**: Inject game-specific collision rules via callback — no coupling to game concepts
16
+ - **Tick Providers**: Pluggable `IPhysicsTickProvider` interface decouples tick scheduling from simulation logic — supports GameWorld-driven, autonomous (turn-based), and external (rAF) modes
17
+ - **Impulse API**: `applyImpulse()` sets body velocity for flick/strike mechanics; `isSettled()` queries whether all bodies are at rest
18
+ - **Bounds Exit Mode**: Optional `ejectOnBoundsExit` mode marks out-of-bounds bodies as ignored and emits `BOUNDS_EXIT` events instead of clamping
19
+ - **Event-Driven**: Collision, trigger enter, trigger exit, and bounds exit events emitted via phalanx-ecs `EventBus`
20
+ - **Built-in Transform & Interpolation**: `TransformComponent` (SoA fixed-point spatial state), `InterpolationComponent`, and `InterpolationSystem` for tick-to-frame render smoothing
21
+ - **PhysicsWorld Facade**: One-liner setup — wraps PhysicsSystem + InterpolationSystem, exposes event subscriptions, spatial queries, and interpolated transforms
22
+
23
+ ## Core Components
24
+
25
+ ### PhysicsWorld (Recommended Entry Point)
26
+ - **PhysicsWorld**: High-level facade — creates PhysicsSystem and InterpolationSystem, wires collision pipeline, exposes event subscriptions, spatial queries, and `getInterpolatedTransform()`
27
+
28
+ ### Transform & Interpolation
29
+ - **TransformComponent**: SoA-backed fixed-point position and rotation (`TransformSoASchema`, `TRANSFORM_COMPONENT_TYPE`)
30
+ - **InterpolationComponent**: Tick-to-tick transform samples for render smoothing
31
+ - **InterpolationSystem**: Snapshots/captures transform state each tick and interpolates each frame (implements `IBeforeTick`, `IAfterTick`, `IBeforeFrame`)
32
+
33
+ ### Physics Body
34
+ - **PhysicsBodyComponent**: SoA-backed component with velocity, radius, mass, restitution, friction, isStatic, and ignorePhysics fields
35
+ - **PhysicsSoASchema**: Schema definition for the SoA storage layout
36
+
37
+ ### Collision Detection
38
+ - **SpatialHashGrid**: Broad-phase spatial partitioning with `queryPairs()` and `queryRadius()`
39
+ - **NarrowPhase**: Static methods for precise collision geometry tests
40
+
41
+ ### Systems
42
+ - **PhysicsSystem**: Velocity integration with sub-stepping, world bounds handling, broad/narrow/resolve collision pipeline, `step()` / `applyImpulse()` / `isSettled()` / `setCollisionFilter()` API. Created and owned by `PhysicsWorld`; retrieve it via `physicsWorld.getSystems().physicsSystem` to register with `GameWorld`.
43
+ - **InterpolationSystem**: Tick/frame lifecycle hooks for transform interpolation. Register as a **frame system** via `physicsWorld.getSystems().interpolationSystem`.
44
+
45
+ ### Tick Providers
46
+ - **IPhysicsTickProvider**: Interface for custom tick scheduling strategies
47
+ - **AutonomousPhysicsTickProvider**: Runs physics loop via `setImmediate` (Node.js) or `setTimeout(0)` (browser) until settled or `maxSteps` reached — ideal for turn-based games
48
+ - **ExternalPhysicsTickProvider**: Delegates tick control to the caller (e.g. BabylonJS `onBeforeRenderObservable` or unit tests)
49
+
50
+ ### Events
51
+ - **PhysicsEvents.COLLISION**: Emitted when two bodies collide
52
+ - **PhysicsEvents.TRIGGER_ENTER**: Emitted when a trigger overlap starts
53
+ - **PhysicsEvents.TRIGGER_EXIT**: Emitted when a trigger overlap ends
54
+ - **PhysicsEvents.BOUNDS_EXIT**: Emitted when a body exits `worldBounds` and `ejectOnBoundsExit` is `true`
55
+
56
+ ## Installation
57
+
58
+ > ⚠️ **Not on npm yet** — clone the monorepo and install via pnpm.
59
+
60
+ ```bash
61
+ git clone https://github.com/phaeton-forge/phalanx-engine.git
62
+ cd phalanx-engine
63
+ pnpm install
64
+ pnpm --filter @phalanx-engine/physics build
65
+ ```
66
+
67
+ Peer dependencies: `@phalanx-engine/ecs` ^0.1.0, `@phalanx-engine/math` ^0.1.0
68
+
69
+ ## Imports
70
+
71
+ ```typescript
72
+ import {
73
+ // Facade & config
74
+ PhysicsWorld,
75
+ type PhysicsWorldConfig,
76
+
77
+ // Components
78
+ PhysicsBodyComponent,
79
+ PhysicsSoASchema,
80
+ PHYSICS_BODY_COMPONENT_TYPE,
81
+ TransformComponent,
82
+ TransformSoASchema,
83
+ TRANSFORM_COMPONENT_TYPE,
84
+ InterpolationComponent,
85
+ INTERPOLATION_COMPONENT_TYPE,
86
+ type PhysicsBodyConfig,
87
+
88
+ // Collision primitives
89
+ SpatialHashGrid,
90
+ NarrowPhase,
91
+ type CollisionManifold,
92
+
93
+ // Systems
94
+ PhysicsSystem,
95
+ InterpolationSystem,
96
+ type InterpolatedTransformSample,
97
+
98
+ // Events & event types
99
+ PhysicsEvents,
100
+ type CollisionEvent,
101
+ type BoundsExitEvent,
102
+
103
+ // Tick providers
104
+ type IPhysicsTickProvider,
105
+ AutonomousPhysicsTickProvider,
106
+ type AutonomousProviderOptions,
107
+ ExternalPhysicsTickProvider,
108
+
109
+ // Misc types
110
+ type CollisionFilter,
111
+ type PhysicsConfig,
112
+ } from '@phalanx-engine/physics';
113
+ ```
114
+
115
+ ## Quick Start
116
+
117
+ ```typescript
118
+ import { GameWorld, GameSystem, createComponentTypeRegistry } from '@phalanx-engine/ecs';
119
+ import {
120
+ PhysicsWorld,
121
+ PhysicsBodyComponent,
122
+ TransformComponent,
123
+ InterpolationComponent,
124
+ PHYSICS_BODY_COMPONENT_TYPE,
125
+ TRANSFORM_COMPONENT_TYPE,
126
+ INTERPOLATION_COMPONENT_TYPE,
127
+ } from '@phalanx-engine/physics';
128
+ import { FP, FPVector3 } from '@phalanx-engine/math';
129
+
130
+ // Register canonical component type symbols from phalanx-physics
131
+ export const ComponentType = createComponentTypeRegistry({
132
+ Transform: 'Transform',
133
+ Interpolation: 'Interpolation',
134
+ PhysicsBody: 'PhysicsBody',
135
+ });
136
+ (ComponentType as Record<string, symbol>).Transform = TRANSFORM_COMPONENT_TYPE;
137
+ (ComponentType as Record<string, symbol>).Interpolation = INTERPOLATION_COMPONENT_TYPE;
138
+ (ComponentType as Record<string, symbol>).PhysicsBody = PHYSICS_BODY_COMPONENT_TYPE;
139
+
140
+ // Minimal placeholder systems — replace with your real ones.
141
+ class MovementSystem extends GameSystem {
142
+ public override processTick(_tick: number): void { /* set velocities here */ }
143
+ }
144
+ class RenderSystem extends GameSystem {
145
+ public override update(_dt: number): void {
146
+ const sample = this.physics?.getInterpolatedTransform(entityId);
147
+ if (sample) mesh.position.set(sample.position.x, sample.position.y, sample.position.z);
148
+ }
149
+ }
150
+ const movementSystem = new MovementSystem();
151
+ const renderSystem = new RenderSystem();
152
+
153
+ // 1. Create GameWorld and the physics facade
154
+ const world = new GameWorld({ componentTypes: Object.values(ComponentType) });
155
+
156
+ const physicsWorld = new PhysicsWorld({
157
+ gridCellSize: FP.FromFloat(8),
158
+ subSteps: 3,
159
+ tickRate: 20,
160
+ maxVelocity: FP.FromFloat(15),
161
+ pushStrength: FP.FromFloat(15),
162
+ });
163
+
164
+ // 2. Wire physics into SystemContext so systems can access it
165
+ world.context.physics = physicsWorld;
166
+
167
+ // 3. Register systems with GameWorld (order matters)
168
+ const { physicsSystem, interpolationSystem } = physicsWorld.getSystems();
169
+ world.registerSystems(
170
+ [movementSystem, physicsSystem], // tick systems
171
+ [interpolationSystem, renderSystem], // frame systems — InterpolationSystem runs before render
172
+ );
173
+
174
+ world.start();
175
+
176
+ // 4. Add transform, interpolation, and physics body to entities
177
+ declare const entity: { id: number; addComponent(c: unknown): void };
178
+ const fpPosition = FPVector3.FromFloat(0, 0, 0);
179
+ entity.addComponent(new TransformComponent(entity.id, fpPosition));
180
+ entity.addComponent(new InterpolationComponent(fpPosition));
181
+ entity.addComponent(new PhysicsBodyComponent(entity.id, { radius: FP.FromFloat(1.0) }));
182
+ world.entityManager.addEntity(entity as any);
183
+
184
+ // 5. Subscribe to collision events (must be called after world.start())
185
+ physicsWorld.onCollision((event) => {
186
+ console.log(`Collision: ${event.entityA} ↔ ${event.entityB}`);
187
+ });
188
+ ```
189
+
190
+ > **Note:** `PhysicsSystem` reads and writes the built-in `TransformSoASchema` store directly — no `setTransformStore()` or consumer-defined transform schema is required. Register `Transform`, `Interpolation`, and `PhysicsBody` component types using the canonical symbols exported from phalanx-physics.
191
+
192
+ ## Turn-Based Physics (Tick Providers)
193
+
194
+ For turn-based games like Chapayev checkers, use a tick provider to decouple simulation from the server tick loop:
195
+
196
+ ```typescript
197
+ import {
198
+ PhysicsWorld,
199
+ AutonomousPhysicsTickProvider,
200
+ } from '@phalanx-engine/physics';
201
+ import { FP } from '@phalanx-engine/math';
202
+
203
+ // Game defines what "settled" means and what happens when it occurs
204
+ let physicsWorld: PhysicsWorld;
205
+
206
+ const provider = new AutonomousPhysicsTickProvider({
207
+ isSettled: () => physicsWorld.isSettled(),
208
+ onSettled: () => {
209
+ // Game-level logic: turn is over
210
+ sendTurnEnd(getCheckerPositions());
211
+ },
212
+ });
213
+
214
+ physicsWorld = new PhysicsWorld({
215
+ tickRate: 60,
216
+ subSteps: 3,
217
+ ejectOnBoundsExit: true,
218
+ worldBounds: {
219
+ minX: FP.FromFloat(-8), maxX: FP.FromFloat(8),
220
+ minZ: FP.FromFloat(-8), maxZ: FP.FromFloat(8),
221
+ },
222
+ tickProvider: provider,
223
+ });
224
+
225
+ // Player flicks a checker
226
+ function onFlick(entityId: number, dirX: number, dirZ: number, power: number) {
227
+ const speed = power * 12;
228
+ physicsWorld.applyImpulse(
229
+ entityId,
230
+ FP.FromFloat(dirX * speed),
231
+ FP.FromFloat(dirZ * speed),
232
+ );
233
+ }
234
+
235
+ // Checker exits the board
236
+ physicsWorld.onBoundsExit(({ entityId }) => {
237
+ removeChecker(entityId);
238
+ });
239
+ ```
240
+
241
+ ### Tick Provider Options
242
+
243
+ | Provider | Use Case |
244
+ |---|---|
245
+ | *(none / default)* | GameWorld `processTick()` drives simulation — real-time games |
246
+ | `AutonomousPhysicsTickProvider` | Runs until settled or `maxSteps` — turn-based physics |
247
+ | `ExternalPhysicsTickProvider` | Caller invokes `tick()` manually — BabylonJS rAF, unit tests |
248
+
249
+ ## API Reference
250
+
251
+ ### `PhysicsWorld`
252
+
253
+ ```typescript
254
+ class PhysicsWorld {
255
+ constructor(config?: PhysicsWorldConfig);
256
+
257
+ // System wiring
258
+ getSystems(): { physicsSystem: PhysicsSystem; interpolationSystem: InterpolationSystem };
259
+ setCollisionFilter(filter: (entityA: number, entityB: number) => boolean): void;
260
+
261
+ // Event subscriptions (must be called after GameWorld.start())
262
+ onCollision(callback: (event: CollisionEvent) => void): () => void;
263
+ onBoundsExit(callback: (event: BoundsExitEvent) => void): () => void;
264
+
265
+ // Planned — not yet emitted by PhysicsSystem.
266
+ onTriggerEnter(callback: (event: CollisionEvent) => void): () => void;
267
+ onTriggerExit(callback: (event: CollisionEvent) => void): () => void;
268
+
269
+ // Impulse / settle queries
270
+ applyImpulse(entityId: number, vx: FixedPoint, vz: FixedPoint): void;
271
+ isSettled(threshold?: FixedPoint): boolean;
272
+
273
+ // Spatial / transform queries
274
+ readonly spatialGrid: SpatialHashGrid;
275
+ getEntityPosition(entityId: number): { x: FixedPoint; z: FixedPoint } | undefined;
276
+ getInterpolatedTransform(entityId: number): InterpolatedTransformSample | undefined;
277
+
278
+ // Cleanup
279
+ dispose(): void;
280
+ }
281
+ ```
282
+
283
+ - **`getSystems()`** — Returns both `physicsSystem` (register as tick system) and `interpolationSystem` (register as frame system).
284
+ - **`getEntityPosition(entityId)`** — Fixed-point position for gameplay queries (e.g. ability targeting).
285
+ - **`getInterpolatedTransform(entityId)`** — Interpolated float position/rotation for rendering, populated after `InterpolationSystem` runs.
286
+
287
+ - **`applyImpulse(entityId, vx, vz)`** — Set body velocity (replaces, does not accumulate). Re-enables previously ejected bodies.
288
+ - **`isSettled(threshold?)`** — Pure query: `true` when all non-static, non-ignored bodies are below velocity threshold (default from config, falling back to `FP.FromFloat(0.01)`).
289
+ - **`onBoundsExit(callback)`** — Subscribe to `BOUNDS_EXIT` events (requires `ejectOnBoundsExit: true`).
290
+ - **`setCollisionFilter(filter)`** — Inject a per-pair predicate. Return `false` to skip collision resolution for that pair.
291
+
292
+ ### `PhysicsWorldConfig`
293
+
294
+ ```typescript
295
+ interface PhysicsWorldConfig {
296
+ gridCellSize?: FixedPoint; // default FP.FromFloat(4)
297
+ subSteps?: number; // default 3
298
+ tickRate?: number; // default 20 — used to compute tickDt
299
+ worldBounds?: { minX: FixedPoint; minZ: FixedPoint; maxX: FixedPoint; maxZ: FixedPoint };
300
+ defaultRestitution?: FixedPoint;
301
+ defaultFriction?: FixedPoint; // default FP.FromFloat(0.92)
302
+ maxVelocity?: FixedPoint; // default FP.FromFloat(15)
303
+ pushStrength?: FixedPoint; // default FP.FromFloat(15)
304
+ tickProvider?: IPhysicsTickProvider;
305
+ ejectOnBoundsExit?: boolean; // default false
306
+ settleThreshold?: FixedPoint; // default FP.FromFloat(0.01)
307
+ }
308
+ ```
309
+
310
+ ### `TransformComponent`
311
+
312
+ ```typescript
313
+ class TransformComponent extends SoAComponent<typeof TransformSoASchema.definition> {
314
+ static readonly soaSchema: typeof TransformSoASchema;
315
+ readonly type: symbol; // TRANSFORM_COMPONENT_TYPE
316
+
317
+ constructor(entityId: number, initialPosition?: FPVector3, initialRotation?: FPVector3);
318
+
319
+ fpPosition: FPVector3; // get/set — authoritative fixed-point position
320
+ fpRotation: FPVector3; // get/set — authoritative fixed-point rotation (radians)
321
+ fpRotationY: FixedPoint; // get/set — convenience for Y-axis rotation
322
+ }
323
+ ```
324
+
325
+ `TransformSoASchema` fields: `fpPositionX/Y/Z`, `fpRotationX/Y/Z` (all `i64`).
326
+
327
+ ### `InterpolationComponent`
328
+
329
+ ```typescript
330
+ class InterpolationComponent implements IComponent {
331
+ readonly type: symbol; // INTERPOLATION_COMPONENT_TYPE
332
+
333
+ constructor(initialPosition?: FPVector3, initialRotation?: FPVector3);
334
+
335
+ snapshot(): void; // copy current → previous (called by InterpolationSystem before tick)
336
+ capture(fpPosition: FPVector3, fpRotation: FPVector3): void; // capture authoritative state after tick
337
+ }
338
+ ```
339
+
340
+ Attach alongside `TransformComponent` on any entity that needs render interpolation.
341
+
342
+ ### `PhysicsBodyComponent`
343
+
344
+ ```typescript
345
+ class PhysicsBodyComponent extends SoAComponent<typeof PhysicsSoASchema.definition> {
346
+ static readonly soaSchema: typeof PhysicsSoASchema;
347
+ readonly type: symbol; // PHYSICS_BODY_COMPONENT_TYPE
348
+
349
+ constructor(entityId: number, config: PhysicsBodyConfig);
350
+
351
+ // Velocity
352
+ velocity: FPVector3; // get/set (returns cached object)
353
+ setVelocity(x: FixedPoint, y: FixedPoint, z: FixedPoint): void;
354
+ addVelocity(velocity: FPVector3): void;
355
+ stopVelocity(): void;
356
+
357
+ // Read-only attributes
358
+ readonly radius: FixedPoint;
359
+ readonly radiusFloat: number;
360
+ readonly mass: FixedPoint;
361
+ readonly restitution: FixedPoint;
362
+ readonly friction: FixedPoint;
363
+ readonly isStatic: boolean;
364
+ ignorePhysics: boolean; // get/set
365
+
366
+ // Spatial-grid bookkeeping
367
+ lastX: number;
368
+ lastZ: number;
369
+ }
370
+
371
+ interface PhysicsBodyConfig {
372
+ radius: FixedPoint;
373
+ mass?: FixedPoint; // default FP._1
374
+ isStatic?: boolean; // default false
375
+ restitution?: FixedPoint; // default FP.FromFloat(0.5)
376
+ friction?: FixedPoint; // default FP._0
377
+ }
378
+ ```
379
+
380
+ For hot-path access, prefer the SoA store directly:
381
+
382
+ ```typescript
383
+ const store = entityManager.getOrCreateSoAStore(PhysicsSoASchema);
384
+ const idx = store.indexOf(entityId);
385
+ store.arrays.velocityX[idx] = FP.ToRaw(newVx);
386
+ ```
387
+
388
+ ### Collision primitives
389
+
390
+ - **`SpatialHashGrid`** — broad-phase O(n) neighbor pairing. Methods: `clear()`, `insert(...)`, `queryPairs()`, `queryRadius(...)`. Access via `physicsWorld.spatialGrid` for ad-hoc range queries.
391
+ - **`NarrowPhase`** — static methods for circle/AABB intersection tests. Returns `CollisionManifold | null`.
392
+ - **`CollisionManifold`** — `{ entityA, entityB, normalX, normalZ, penetration }`.
393
+
394
+ ### Tick providers
395
+
396
+ ```typescript
397
+ interface IPhysicsTickProvider {
398
+ /** Start the provider; it calls `onStep` whenever physics should advance one step. */
399
+ start(onStep: () => void): void;
400
+ /** Stop the provider and release any timers/handles. */
401
+ stop(): void;
402
+ }
403
+
404
+ interface AutonomousProviderOptions {
405
+ /** Called every step to decide whether to stop (defined by the game). */
406
+ isSettled: () => boolean;
407
+ /** Called once when simulation settles or `maxSteps` is reached. */
408
+ onSettled: () => void;
409
+ /** Max simulation steps before forcing a stop. Default: 10000. */
410
+ maxSteps?: number;
411
+ }
412
+
413
+ class AutonomousPhysicsTickProvider implements IPhysicsTickProvider {
414
+ constructor(options: AutonomousProviderOptions);
415
+ // Schedules `onStep` via setImmediate (Node) or setTimeout(0) (browser)
416
+ // until isSettled() returns true or maxSteps is reached.
417
+ }
418
+
419
+ class ExternalPhysicsTickProvider implements IPhysicsTickProvider {
420
+ /** Manually advance one physics step from your render loop / test harness. */
421
+ tick(): void;
422
+ }
423
+ ```
424
+
425
+ ### Events
426
+
427
+ ```typescript
428
+ const PhysicsEvents = {
429
+ COLLISION: 'physics:collision',
430
+ TRIGGER_ENTER: 'physics:trigger:enter',
431
+ TRIGGER_EXIT: 'physics:trigger:exit',
432
+ BOUNDS_EXIT: 'physics:bounds:exit',
433
+ } as const;
434
+
435
+ interface CollisionEvent { entityA: number; entityB: number; manifold: CollisionManifold; }
436
+ interface BoundsExitEvent { entityId: number; }
437
+ ```
@@ -0,0 +1,35 @@
1
+ import { PhysicsSystem } from './systems/PhysicsSystem';
2
+ import { InterpolationSystem } from './systems/InterpolationSystem';
3
+ import type { InterpolatedTransformSample } from './systems/InterpolationSystem';
4
+ import { SpatialHashGrid } from './collision/SpatialHashGrid';
5
+ import type { PhysicsWorldConfig } from './PhysicsWorldConfig';
6
+ import type { FixedPoint } from '@phalanx-engine/math';
7
+ import type { CollisionEvent, BoundsExitEvent } from './types';
8
+ export declare class PhysicsWorld {
9
+ private readonly physicsSystem;
10
+ private readonly interpolationSystem;
11
+ private eventBusRef;
12
+ private readonly unsubscribers;
13
+ private readonly settleThreshold;
14
+ constructor(config?: PhysicsWorldConfig);
15
+ getSystems(): {
16
+ physicsSystem: PhysicsSystem;
17
+ interpolationSystem: InterpolationSystem;
18
+ };
19
+ setCollisionFilter(filter: (entityA: number, entityB: number) => boolean): void;
20
+ onCollision(callback: (event: CollisionEvent) => void): () => void;
21
+ onTriggerEnter(callback: (event: CollisionEvent) => void): () => void;
22
+ onTriggerExit(callback: (event: CollisionEvent) => void): () => void;
23
+ applyImpulse(entityId: number, vx: FixedPoint, vz: FixedPoint): void;
24
+ isSettled(threshold?: FixedPoint): boolean;
25
+ onBoundsExit(callback: (event: BoundsExitEvent) => void): () => void;
26
+ get spatialGrid(): SpatialHashGrid;
27
+ getEntityPosition(entityId: number): {
28
+ x: FixedPoint;
29
+ z: FixedPoint;
30
+ } | undefined;
31
+ getInterpolatedTransform(entityId: number): InterpolatedTransformSample | undefined;
32
+ dispose(): void;
33
+ private getEventBus;
34
+ }
35
+ //# sourceMappingURL=PhysicsWorld.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PhysicsWorld.d.ts","sourceRoot":"","sources":["../src/PhysicsWorld.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AACpE,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,+BAA+B,CAAC;AACjF,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAE9D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,KAAK,EAAE,cAAc,EAAiB,eAAe,EAAE,MAAM,SAAS,CAAC;AAS9E,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAgB;IAC9C,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAsB;IAC1D,OAAO,CAAC,WAAW,CAAyB;IAC5C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAsB;IACpD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAyB;gBAE7C,MAAM,CAAC,EAAE,kBAAkB;IAgChC,UAAU,IAAI;QACnB,aAAa,EAAE,aAAa,CAAC;QAC7B,mBAAmB,EAAE,mBAAmB,CAAC;KAC1C;IAWM,kBAAkB,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,GAAG,IAAI;IAS/E,WAAW,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,GAAG,MAAM,IAAI;IAalE,cAAc,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,GAAG,MAAM,IAAI;IAarE,aAAa,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,GAAG,MAAM,IAAI;IAWpE,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE,UAAU,GAAG,IAAI;IAQpE,SAAS,CAAC,SAAS,CAAC,EAAE,UAAU,GAAG,OAAO;IAK1C,YAAY,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,GAAG,MAAM,IAAI;IAS3E,IAAW,WAAW,IAAI,eAAe,CAExC;IAKM,iBAAiB,CACtB,QAAQ,EAAE,MAAM,GACf;QAAE,CAAC,EAAE,UAAU,CAAC;QAAC,CAAC,EAAE,UAAU,CAAA;KAAE,GAAG,SAAS;IAQxC,wBAAwB,CAAC,QAAQ,EAAE,MAAM,GAAG,2BAA2B,GAAG,SAAS;IAKnF,OAAO,IAAI,IAAI;IAOtB,OAAO,CAAC,WAAW;CASpB"}
@@ -0,0 +1,112 @@
1
+ import { FP } from '@phalanx-engine/math';
2
+ import { PhysicsSystem } from './systems/PhysicsSystem';
3
+ import { InterpolationSystem } from './systems/InterpolationSystem';
4
+ import { PhysicsEvents } from './events';
5
+ export class PhysicsWorld {
6
+ physicsSystem;
7
+ interpolationSystem;
8
+ eventBusRef = null;
9
+ unsubscribers = [];
10
+ settleThreshold;
11
+ constructor(config) {
12
+ const tickRate = config?.tickRate ?? 20;
13
+ const subSteps = config?.subSteps ?? 3;
14
+ const tickDt = FP.FromFloat(1 / tickRate);
15
+ const gridCellSize = config?.gridCellSize ?? FP.FromFloat(4);
16
+ const maxVelocity = config?.maxVelocity ?? FP.FromFloat(15.0);
17
+ const pushStrength = config?.pushStrength ?? FP.FromFloat(15.0);
18
+ const defaultFriction = config?.defaultFriction ?? FP.FromFloat(0.92);
19
+ const physicsConfig = {
20
+ tickDt,
21
+ subSteps,
22
+ maxVelocity,
23
+ defaultFriction,
24
+ pushStrength,
25
+ gridCellSize,
26
+ worldBounds: config?.worldBounds,
27
+ ejectOnBoundsExit: config?.ejectOnBoundsExit,
28
+ };
29
+ this.physicsSystem = new PhysicsSystem(physicsConfig);
30
+ this.interpolationSystem = new InterpolationSystem();
31
+ this.settleThreshold = config?.settleThreshold;
32
+ if (config?.tickProvider) {
33
+ this.physicsSystem.setTickProvider(config.tickProvider);
34
+ }
35
+ }
36
+ getSystems() {
37
+ return {
38
+ physicsSystem: this.physicsSystem,
39
+ interpolationSystem: this.interpolationSystem,
40
+ };
41
+ }
42
+ setCollisionFilter(filter) {
43
+ this.physicsSystem.setCollisionFilter(filter);
44
+ }
45
+ onCollision(callback) {
46
+ const eb = this.getEventBus();
47
+ if (!eb) {
48
+ throw new Error('PhysicsWorld: Cannot subscribe before systems are initialized');
49
+ }
50
+ const unsub = eb.on(PhysicsEvents.COLLISION, callback);
51
+ this.unsubscribers.push(unsub);
52
+ return unsub;
53
+ }
54
+ onTriggerEnter(callback) {
55
+ const eb = this.getEventBus();
56
+ if (!eb) {
57
+ throw new Error('PhysicsWorld: Cannot subscribe before systems are initialized');
58
+ }
59
+ const unsub = eb.on(PhysicsEvents.TRIGGER_ENTER, callback);
60
+ this.unsubscribers.push(unsub);
61
+ return unsub;
62
+ }
63
+ onTriggerExit(callback) {
64
+ const eb = this.getEventBus();
65
+ if (!eb) {
66
+ throw new Error('PhysicsWorld: Cannot subscribe before systems are initialized');
67
+ }
68
+ const unsub = eb.on(PhysicsEvents.TRIGGER_EXIT, callback);
69
+ this.unsubscribers.push(unsub);
70
+ return unsub;
71
+ }
72
+ applyImpulse(entityId, vx, vz) {
73
+ this.physicsSystem.applyImpulse(entityId, vx, vz);
74
+ }
75
+ isSettled(threshold) {
76
+ return this.physicsSystem.isSettled(threshold ?? this.settleThreshold);
77
+ }
78
+ onBoundsExit(callback) {
79
+ const eb = this.getEventBus();
80
+ if (!eb)
81
+ throw new Error('PhysicsWorld: Cannot subscribe before systems are initialized');
82
+ const unsub = eb.on(PhysicsEvents.BOUNDS_EXIT, callback);
83
+ this.unsubscribers.push(unsub);
84
+ return unsub;
85
+ }
86
+ get spatialGrid() {
87
+ return this.physicsSystem.getSpatialGrid();
88
+ }
89
+ getEntityPosition(entityId) {
90
+ return this.physicsSystem.getEntityPosition(entityId);
91
+ }
92
+ getInterpolatedTransform(entityId) {
93
+ return this.interpolationSystem.getInterpolatedTransform(entityId);
94
+ }
95
+ dispose() {
96
+ for (const unsub of this.unsubscribers) {
97
+ unsub();
98
+ }
99
+ this.unsubscribers.length = 0;
100
+ }
101
+ getEventBus() {
102
+ if (this.eventBusRef)
103
+ return this.eventBusRef;
104
+ try {
105
+ this.eventBusRef = this.physicsSystem.getEventBus() ?? null;
106
+ }
107
+ catch {
108
+ return null;
109
+ }
110
+ return this.eventBusRef;
111
+ }
112
+ }
@@ -0,0 +1,21 @@
1
+ import type { FixedPoint } from '@phalanx-engine/math';
2
+ import type { IPhysicsTickProvider } from './tick/IPhysicsTickProvider';
3
+ export interface PhysicsWorldConfig {
4
+ gridCellSize?: FixedPoint;
5
+ subSteps?: number;
6
+ tickRate?: number;
7
+ worldBounds?: {
8
+ minX: FixedPoint;
9
+ minZ: FixedPoint;
10
+ maxX: FixedPoint;
11
+ maxZ: FixedPoint;
12
+ };
13
+ defaultRestitution?: FixedPoint;
14
+ defaultFriction?: FixedPoint;
15
+ maxVelocity?: FixedPoint;
16
+ pushStrength?: FixedPoint;
17
+ tickProvider?: IPhysicsTickProvider;
18
+ ejectOnBoundsExit?: boolean;
19
+ settleThreshold?: FixedPoint;
20
+ }
21
+ //# sourceMappingURL=PhysicsWorldConfig.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PhysicsWorldConfig.d.ts","sourceRoot":"","sources":["../src/PhysicsWorldConfig.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAKxE,MAAM,WAAW,kBAAkB;IAEjC,YAAY,CAAC,EAAE,UAAU,CAAC;IAE1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,WAAW,CAAC,EAAE;QACZ,IAAI,EAAE,UAAU,CAAC;QACjB,IAAI,EAAE,UAAU,CAAC;QACjB,IAAI,EAAE,UAAU,CAAC;QACjB,IAAI,EAAE,UAAU,CAAC;KAClB,CAAC;IAEF,kBAAkB,CAAC,EAAE,UAAU,CAAC;IAEhC,eAAe,CAAC,EAAE,UAAU,CAAC;IAE7B,WAAW,CAAC,EAAE,UAAU,CAAC;IAEzB,YAAY,CAAC,EAAE,UAAU,CAAC;IAQ1B,YAAY,CAAC,EAAE,oBAAoB,CAAC;IAMpC,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAO5B,eAAe,CAAC,EAAE,UAAU,CAAC;CAC9B"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ import type { FixedPoint } from '@phalanx-engine/math';
2
+ export interface CollisionManifold {
3
+ entityA: number;
4
+ entityB: number;
5
+ normalX: FixedPoint;
6
+ normalZ: FixedPoint;
7
+ penetration: FixedPoint;
8
+ }
9
+ //# sourceMappingURL=CollisionManifold.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CollisionManifold.d.ts","sourceRoot":"","sources":["../../src/collision/CollisionManifold.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAKvD,MAAM,WAAW,iBAAiB;IAEhC,OAAO,EAAE,MAAM,CAAC;IAEhB,OAAO,EAAE,MAAM,CAAC;IAEhB,OAAO,EAAE,UAAU,CAAC;IAEpB,OAAO,EAAE,UAAU,CAAC;IAEpB,WAAW,EAAE,UAAU,CAAC;CACzB"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ import { type FixedPoint } from '@phalanx-engine/math';
2
+ import type { CollisionManifold } from './CollisionManifold';
3
+ export declare class NarrowPhase {
4
+ static circleVsCircle(posAX: FixedPoint, posAZ: FixedPoint, radiusA: FixedPoint, posBX: FixedPoint, posBZ: FixedPoint, radiusB: FixedPoint, entityA: number, entityB: number): CollisionManifold | null;
5
+ static circleVsAABB(circlePosX: FixedPoint, circlePosZ: FixedPoint, circleRadius: FixedPoint, aabbMinX: FixedPoint, aabbMinZ: FixedPoint, aabbMaxX: FixedPoint, aabbMaxZ: FixedPoint, entityCircle: number, entityAABB: number): CollisionManifold | null;
6
+ static aabbVsAABB(aMinX: FixedPoint, aMinZ: FixedPoint, aMaxX: FixedPoint, aMaxZ: FixedPoint, bMinX: FixedPoint, bMinZ: FixedPoint, bMaxX: FixedPoint, bMaxZ: FixedPoint, entityA: number, entityB: number): CollisionManifold | null;
7
+ }
8
+ //# sourceMappingURL=NarrowPhase.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NarrowPhase.d.ts","sourceRoot":"","sources":["../../src/collision/NarrowPhase.ts"],"names":[],"mappings":"AAAA,OAAO,EAAM,KAAK,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAW7D,qBAAa,WAAW;IAKtB,MAAM,CAAC,cAAc,CACnB,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU,EACzD,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU,EACzD,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAC/B,iBAAiB,GAAG,IAAI;IAwC3B,MAAM,CAAC,YAAY,CACjB,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,EAAE,UAAU,EACxE,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,UAAU,EAC1C,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,UAAU,EAC1C,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GACvC,iBAAiB,GAAG,IAAI;IAsE3B,MAAM,CAAC,UAAU,CACf,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAC1E,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAC1E,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAC/B,iBAAiB,GAAG,IAAI;CAuC5B"}