@phalanx-engine/ecs 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 (89) hide show
  1. package/README.md +666 -0
  2. package/dist/Component.d.ts +7 -0
  3. package/dist/Component.d.ts.map +1 -0
  4. package/dist/Component.js +7 -0
  5. package/dist/Entity.d.ts +25 -0
  6. package/dist/Entity.d.ts.map +1 -0
  7. package/dist/Entity.js +58 -0
  8. package/dist/EntityManager.d.ts +36 -0
  9. package/dist/EntityManager.d.ts.map +1 -0
  10. package/dist/EntityManager.js +218 -0
  11. package/dist/EventBus.d.ts +15 -0
  12. package/dist/EventBus.d.ts.map +1 -0
  13. package/dist/EventBus.js +69 -0
  14. package/dist/GameSystem.d.ts +23 -0
  15. package/dist/GameSystem.d.ts.map +1 -0
  16. package/dist/GameSystem.js +43 -0
  17. package/dist/GameWorld.d.ts +69 -0
  18. package/dist/GameWorld.d.ts.map +1 -0
  19. package/dist/GameWorld.js +237 -0
  20. package/dist/IAbilitySystem.d.ts +18 -0
  21. package/dist/IAbilitySystem.d.ts.map +1 -0
  22. package/dist/IAbilitySystem.js +1 -0
  23. package/dist/IPhysicsWorld.d.ts +21 -0
  24. package/dist/IPhysicsWorld.d.ts.map +1 -0
  25. package/dist/IPhysicsWorld.js +1 -0
  26. package/dist/ISystemLifecycleHooks.d.ts +18 -0
  27. package/dist/ISystemLifecycleHooks.d.ts.map +1 -0
  28. package/dist/ISystemLifecycleHooks.js +12 -0
  29. package/dist/ITickFrameProvider.d.ts +24 -0
  30. package/dist/ITickFrameProvider.d.ts.map +1 -0
  31. package/dist/ITickFrameProvider.js +1 -0
  32. package/dist/SoAComponent.d.ts +22 -0
  33. package/dist/SoAComponent.d.ts.map +1 -0
  34. package/dist/SoAComponent.js +59 -0
  35. package/dist/SoAComponentStore.d.ts +41 -0
  36. package/dist/SoAComponentStore.d.ts.map +1 -0
  37. package/dist/SoAComponentStore.js +253 -0
  38. package/dist/SoASchema.d.ts +22 -0
  39. package/dist/SoASchema.d.ts.map +1 -0
  40. package/dist/SoASchema.js +33 -0
  41. package/dist/SystemContext.d.ts +18 -0
  42. package/dist/SystemContext.d.ts.map +1 -0
  43. package/dist/SystemContext.js +18 -0
  44. package/dist/SystemRegistry.d.ts +20 -0
  45. package/dist/SystemRegistry.d.ts.map +1 -0
  46. package/dist/SystemRegistry.js +73 -0
  47. package/dist/TickFrameManager.d.ts +35 -0
  48. package/dist/TickFrameManager.d.ts.map +1 -0
  49. package/dist/TickFrameManager.js +130 -0
  50. package/dist/debug/DebugDataProvider.d.ts +26 -0
  51. package/dist/debug/DebugDataProvider.d.ts.map +1 -0
  52. package/dist/debug/DebugDataProvider.js +128 -0
  53. package/dist/debug/DebugPanel.d.ts +57 -0
  54. package/dist/debug/DebugPanel.d.ts.map +1 -0
  55. package/dist/debug/DebugPanel.js +482 -0
  56. package/dist/debug/index.d.ts +4 -0
  57. package/dist/debug/index.d.ts.map +1 -0
  58. package/dist/debug/index.js +2 -0
  59. package/dist/debug/types.d.ts +47 -0
  60. package/dist/debug/types.d.ts.map +1 -0
  61. package/dist/debug/types.js +1 -0
  62. package/dist/index.d.ts +27 -0
  63. package/dist/index.d.ts.map +1 -0
  64. package/dist/index.js +15 -0
  65. package/dist/pool/EntityPool.d.ts +21 -0
  66. package/dist/pool/EntityPool.d.ts.map +1 -0
  67. package/dist/pool/EntityPool.js +86 -0
  68. package/dist/pool/IPoolable.d.ts +4 -0
  69. package/dist/pool/IPoolable.d.ts.map +1 -0
  70. package/dist/pool/IPoolable.js +1 -0
  71. package/dist/pool/IPoolableComponent.d.ts +7 -0
  72. package/dist/pool/IPoolableComponent.d.ts.map +1 -0
  73. package/dist/pool/IPoolableComponent.js +4 -0
  74. package/dist/pool/IPoolableEntity.d.ts +6 -0
  75. package/dist/pool/IPoolableEntity.d.ts.map +1 -0
  76. package/dist/pool/IPoolableEntity.js +1 -0
  77. package/dist/pool/ObjectPool.d.ts +20 -0
  78. package/dist/pool/ObjectPool.d.ts.map +1 -0
  79. package/dist/pool/ObjectPool.js +76 -0
  80. package/dist/pool/PoolManager.d.ts +20 -0
  81. package/dist/pool/PoolManager.d.ts.map +1 -0
  82. package/dist/pool/PoolManager.js +92 -0
  83. package/dist/pool/index.d.ts +10 -0
  84. package/dist/pool/index.d.ts.map +1 -0
  85. package/dist/pool/index.js +5 -0
  86. package/dist/pool/types.d.ts +31 -0
  87. package/dist/pool/types.d.ts.map +1 -0
  88. package/dist/pool/types.js +8 -0
  89. package/package.json +46 -0
package/README.md ADDED
@@ -0,0 +1,666 @@
1
+ # Phalanx ECS
2
+
3
+ A lightweight, renderer-agnostic Entity-Component-System (ECS) library with optional multiplayer support via Phalanx Engine.
4
+
5
+ ## Features
6
+
7
+ - **GameWorld Facade**: One-liner setup — construct, register systems, start
8
+ - **Pure ECS Architecture**: EntityManager, GameSystem, EventBus
9
+ - **Renderer Agnostic**: No rendering dependencies — bring your own renderer (Babylon.js, Three.js, etc.)
10
+ - **Flexible Integration**: Use standalone or with Phalanx Client for multiplayer
11
+ - **TypeScript First**: Full type safety and excellent IDE support
12
+ - **Deterministic Tick/Frame**: Separate tick-based simulation from frame-based rendering
13
+ - **SoA Storage**: Optional Structure-of-Arrays component storage for cache-friendly hot-path iteration
14
+ - **Object Pooling**: Built-in entity and object pools to minimize GC pressure in hot loops
15
+
16
+ ## Core Components
17
+
18
+ ### GameWorld (Recommended Entry Point)
19
+ - **GameWorld**: High-level facade — creates all core dependencies, wires tick/frame loops, provides convenience accessors
20
+
21
+ ### Entity Management
22
+ - **Entity**: Base class for all game objects (id + components only, no rendering)
23
+ - **EntityManager**: Central registry with efficient component-based queries
24
+ - **IComponent**: Interface for all components
25
+
26
+ ### System Architecture
27
+ - **GameSystem**: Base class for all game systems (convenience accessors for `eventBus`, `entityManager`, `abilities`, `physics`, `pools`)
28
+ - **SystemRegistry**: Low-level system lifecycle and execution order (used internally by GameWorld)
29
+ - **SystemContext**: Dependency injection container (`eventBus`, `entityManager`, optional `abilities` / `physics`, `pools`)
30
+ - **ISystemLifecycleHooks**: Optional `IBeforeTick`, `IAfterTick`, `IBeforeFrame`, `IAfterFrame` interfaces — GameWorld invokes them automatically
31
+
32
+ ### Event System
33
+ - **EventBus**: Decoupled communication between systems
34
+
35
+ ### Tick/Frame Management
36
+ - **TickFrameManager**: Built-in no-op client for single-player games
37
+ - Compatible with PhalanxClient for multiplayer
38
+
39
+ ### SoA (Structure-of-Arrays) Storage
40
+ - **SoAComponent**: Base class for components backed by contiguous typed arrays
41
+ - **SoAComponentStore**: Dense, cache-friendly storage with O(1) entity lookup
42
+ - **defineSoASchema**: Type-safe schema definition for SoA field layout
43
+
44
+ ### Object Pooling
45
+ - **ObjectPool**: Generic LIFO pool for any `IPoolable` object
46
+ - **EntityPool**: Low-level entity storage with stable IDs and growth stats
47
+ - **PoolManager**: Orchestrates spawn/despawn lifecycle and EntityManager registration
48
+ - **IPoolableEntity**: Per-entity `onSpawn(args)` / `onDespawn()` hooks for typed value assignment
49
+ - **IPoolableComponent**: Engine-driven `onSpawn()` / `onDespawn()` hooks for backing storage (SoA rows, visibility)
50
+
51
+ ## Installation
52
+
53
+ ```bash
54
+ npm install @phalanx-engine/ecs
55
+ ```
56
+
57
+ ## Usage
58
+
59
+ ### Single-player Mode
60
+
61
+ ```typescript
62
+ import { GameWorld } from '@phalanx-engine/ecs';
63
+
64
+ // Create GameWorld (no rendering dependencies)
65
+ const world = new GameWorld({
66
+ tickRate: 60, // optional, default 60
67
+ maxFrameTime: 0.25, // optional, default 0.25
68
+ });
69
+
70
+ // Create and register your systems
71
+ const movementSystem = new MovementSystem();
72
+ const renderSystem = new RenderSystem();
73
+
74
+ world.registerSystems(
75
+ [movementSystem], // Tick systems (deterministic)
76
+ [renderSystem] // Frame systems (visual)
77
+ );
78
+
79
+ // Start the game loop
80
+ // Automatically runs: processAllTicks(tick), updateAll(dt)
81
+ // Note: scene.render() is NOT called automatically — call it in afterFrame hook if needed
82
+ world.start();
83
+ ```
84
+
85
+ ### Multiplayer Mode (with Phalanx Client)
86
+
87
+ ```typescript
88
+ import { PhalanxClient } from '@phalanx-engine/client';
89
+ import { GameWorld } from '@phalanx-engine/ecs';
90
+
91
+ // Initialize Phalanx Client
92
+ const client = new PhalanxClient({
93
+ serverUrl: 'wss://your-server.com',
94
+ // ... other config
95
+ });
96
+
97
+ // Create GameWorld with external tick/frame provider
98
+ const world = new GameWorld({
99
+ tickFrameProvider: client,
100
+ });
101
+
102
+ world.registerSystems(tickSystems, frameSystems);
103
+
104
+ // Start with lifecycle hooks (tick systems and frame systems run automatically)
105
+ world.start({
106
+ beforeTick(tick, commandsBatch) {
107
+ // Execute network commands before tick systems run
108
+ lockstepManager.processTick(tick, commandsBatch);
109
+ },
110
+ afterTick(tick) {
111
+ // Cleanup after tick systems have run
112
+ cleanupDestroyedEntities();
113
+ },
114
+ beforeFrame(alpha, dt) {
115
+ // Update camera before frame systems
116
+ cameraController.update(dt);
117
+ },
118
+ afterFrame(alpha, dt) {
119
+ // Render the scene (must be called manually)
120
+ // Interpolation is handled by phalanx-physics InterpolationSystem when registered
121
+ scene.render();
122
+ },
123
+ });
124
+
125
+ // Connect to match
126
+ await client.connect();
127
+ ```
128
+
129
+ ### Low-level API (SystemRegistry + ITickFrameProvider)
130
+
131
+ For advanced use-cases you can still use `SystemRegistry` and `ITickFrameProvider` directly:
132
+
133
+ ```typescript
134
+ import { SystemRegistry, TickFrameManager } from '@phalanx-engine/ecs';
135
+
136
+ const registry = new SystemRegistry(componentTypes);
137
+ registry.registerSystems(tickSystems, frameSystems);
138
+
139
+ const tickManager = new TickFrameManager({ tickRate: 60 });
140
+ tickManager.onTick((tick) => registry.processAllTicks(tick));
141
+ tickManager.onFrame((alpha, dt) => { registry.updateAll(dt); });
142
+ tickManager.start();
143
+ ```
144
+
145
+ ## API Reference
146
+
147
+ ### GameWorld
148
+
149
+ ```typescript
150
+ class GameWorld {
151
+ constructor(config: GameWorldConfig)
152
+
153
+ // Convenience accessors
154
+ get eventBus(): EventBus
155
+ get entityManager(): EntityManager
156
+ get context(): SystemContext
157
+ get pools(): PoolManager | null
158
+ get paused(): boolean
159
+ getSystem<T extends GameSystem>(systemClass: new (...args: any[]) => T): T | undefined
160
+
161
+ // System registration
162
+ registerSystems(tickSystems: GameSystem[], frameSystems: GameSystem[]): void
163
+ addFrameSystem(system: GameSystem): void
164
+
165
+ // Tick / Frame delegation
166
+ processAllTicks(tick: number): void
167
+ updateAll(dt: number): void
168
+
169
+ // Pause / Resume (delegates to tick/frame provider if available)
170
+ pause(): void
171
+ resume(): void
172
+
173
+ // Lifecycle
174
+ start(hooks?: GameWorldHooks): void
175
+ stop(): void
176
+ dispose(): void
177
+ }
178
+
179
+ interface GameWorldConfig {
180
+ componentTypes?: symbol[]
181
+ tickRate?: number // default 60
182
+ maxFrameTime?: number // default 0.25
183
+ tickFrameProvider?: ITickFrameProvider // e.g. PhalanxClient
184
+ pooling?: PoolingConfig // Object pooling configuration
185
+ debug?: boolean // Enable debug features (default: false)
186
+ debugConfig?: DebugDataProviderConfig
187
+ debugPanelConfig?: DebugPanelConfig
188
+ }
189
+
190
+ interface GameWorldHooks {
191
+ beforeTick?(tick: number, commands: CommandsBatch): void
192
+ afterTick?(tick: number): void
193
+ beforeFrame?(alpha: number, dt: number): void
194
+ afterFrame?(alpha: number, dt: number): void
195
+ }
196
+ ```
197
+
198
+ ### EntityManager
199
+
200
+ ```typescript
201
+ class EntityManager {
202
+ addEntity(entity: Entity): void
203
+ removeEntity(entity: Entity): void
204
+ getEntity(id: number): Entity | undefined
205
+ getAllEntities(): Entity[]
206
+ queryEntities(...componentTypes: symbol[]): Entity[]
207
+ queryEntitiesAny(...componentTypes: symbol[]): Entity[]
208
+ cleanupDestroyed(): Entity[]
209
+ count: number
210
+
211
+ // SoA store management
212
+ getSoAStore<S>(schema: SoASchema<S>): SoAComponentStore<S> | undefined
213
+ getOrCreateSoAStore<S>(schema: SoASchema<S>, capacity?: number): SoAComponentStore<S>
214
+ hasSoAStore(schema: SoASchema): boolean
215
+ }
216
+ ```
217
+
218
+ ### SystemContext
219
+
220
+ ```typescript
221
+ class SystemContext {
222
+ readonly eventBus: EventBus
223
+ readonly entityManager: EntityManager
224
+ abilities: IAbilitySystem | undefined // set by createAbilitySystem() before registerSystems()
225
+ physics: IPhysicsWorld | undefined // set by game bootstrap before registerSystems()
226
+ pools: PoolManager | null // wired automatically when pooling is configured
227
+
228
+ getSystem<T extends GameSystem>(systemClass: new (...args: any[]) => T): T | undefined
229
+ }
230
+ ```
231
+
232
+ Set optional services on `world.context` before calling `registerSystems()`:
233
+
234
+ ```typescript
235
+ import { PhysicsWorld } from '@phalanx-engine/physics';
236
+
237
+ const physicsWorld = new PhysicsWorld({ tickRate: 20 });
238
+ world.context.physics = physicsWorld;
239
+
240
+ // Systems access it via the protected getter:
241
+ class RenderSystem extends GameSystem {
242
+ update(_dt: number): void {
243
+ const sample = this.physics?.getInterpolatedTransform(entityId);
244
+ }
245
+ }
246
+ ```
247
+
248
+ ### IPhysicsWorld (optional contract)
249
+
250
+ Implemented by `PhysicsWorld` from phalanx-physics. Keeps phalanx-ecs dependency-free via `unknown` for fixed-point types.
251
+
252
+ ```typescript
253
+ interface IPhysicsWorld {
254
+ getInterpolatedTransform(entityId: number): InterpolatedTransformSample | undefined
255
+ getEntityPosition(entityId: number): { x: unknown; z: unknown } | undefined
256
+ applyImpulse(entityId: number, vx: unknown, vz: unknown): void
257
+ }
258
+
259
+ interface InterpolatedTransformSample {
260
+ position: { x: number; y: number; z: number }
261
+ rotation: { x: number; y: number; z: number }
262
+ }
263
+ ```
264
+
265
+ ### System Lifecycle Hooks
266
+
267
+ Systems can implement optional hook interfaces. GameWorld calls them automatically at the correct pipeline phase (before user-supplied `GameWorldHooks` for "before" variants, after tick/frame systems for "after" variants):
268
+
269
+ ```
270
+ Tick: IBeforeTick systems → beforeTick hook → tick systems → IAfterTick systems → afterTick hook
271
+ Frame: IBeforeFrame systems → beforeFrame hook → frame systems → IAfterFrame systems → afterFrame hook
272
+ ```
273
+
274
+ ```typescript
275
+ import { GameSystem, type IBeforeTick, type IAfterTick, type IBeforeFrame } from '@phalanx-engine/ecs';
276
+
277
+ class MySystem extends GameSystem implements IBeforeTick, IAfterTick {
278
+ beforeTick(tick: number, commands: CommandsBatch): void { /* snapshot state */ }
279
+ afterTick(tick: number): void { /* capture state */ }
280
+ }
281
+ ```
282
+
283
+ Type guards are also exported: `isBeforeTick`, `isAfterTick`, `isBeforeFrame`, `isAfterFrame`.
284
+
285
+ ### SystemRegistry (Low-level)
286
+
287
+ ```typescript
288
+ class SystemRegistry {
289
+ constructor(componentTypes?: symbol[])
290
+ registerSystems(tickSystems: GameSystem[], frameSystems: GameSystem[]): void
291
+ processAllTicks(tick: number): void
292
+ updateAll(deltaTime: number): void
293
+ getContext(): SystemContext
294
+ dispose(): void
295
+ }
296
+ ```
297
+
298
+ ### EventBus
299
+
300
+ ```typescript
301
+ class EventBus {
302
+ on<T>(eventType: string, callback: (data: T) => void): UnsubscribeFunction
303
+ once<T>(eventType: string, callback: (data: T) => void): UnsubscribeFunction
304
+ off<T>(eventType: string, callback: (data: T) => void): void
305
+ emit<T>(eventType: string, data: T): void
306
+ clear(eventType: string): void
307
+ clearAll(): void
308
+ listenerCount(eventType: string): number
309
+ }
310
+ ```
311
+
312
+ ### TickFrameManager
313
+
314
+ ```typescript
315
+ class TickFrameManager implements ITickFrameProvider {
316
+ constructor(config?: { tickRate?: number; maxFrameTime?: number })
317
+ onTick(callback: (tick: number, commands: CommandsBatch) => void): Unsubscribe
318
+ onFrame(callback: (alpha: number, deltaTime: number) => void): Unsubscribe
319
+ onPause(handler: PauseHandler): Unsubscribe
320
+ onResume(handler: PauseHandler): Unsubscribe
321
+ start(): void
322
+ stop(): void
323
+ requestPause(): void
324
+ requestResume(): void
325
+ dispose(): void
326
+ getCurrentTick(): number
327
+ getTickRate(): number
328
+ isActive(): boolean
329
+ }
330
+ ```
331
+
332
+ ### ITickFrameProvider
333
+
334
+ The shared interface that both `TickFrameManager` and `PhalanxClient` satisfy.
335
+ Game code should depend on this interface to allow easy switching between
336
+ single-player and multiplayer modes.
337
+
338
+ ```typescript
339
+ interface ITickFrameProvider {
340
+ onTick(handler: TickHandler): Unsubscribe;
341
+ onFrame(handler: FrameHandler): Unsubscribe;
342
+
343
+ // Optional pause/resume support
344
+ requestPause?(): void;
345
+ requestResume?(): void;
346
+ onPause?(handler: PauseHandler): Unsubscribe;
347
+ onResume?(handler: PauseHandler): Unsubscribe;
348
+ }
349
+
350
+ type TickHandler = (tick: number, commands: CommandsBatch) => void;
351
+ type FrameHandler = (alpha: number, dt: number) => void;
352
+ type PauseHandler = () => void;
353
+ ```
354
+
355
+ ## Component Types: IComponent vs SoAComponent
356
+
357
+ Phalanx ECS offers two component types. Choose based on your performance and data layout needs.
358
+
359
+ ### IComponent (Standard Components)
360
+
361
+ Simple class-based components that store data in regular object properties.
362
+
363
+ **Use when:**
364
+ - The component is accessed infrequently (e.g., flags, config, UI state)
365
+ - There are few instances (e.g., a single `ResourceComponent` per player)
366
+ - The data is complex or polymorphic (nested objects, arrays, callbacks)
367
+ - You want maximum simplicity
368
+
369
+ ```typescript
370
+ import type { IComponent } from '@phalanx-engine/ecs';
371
+
372
+ class ArmorComponent implements IComponent {
373
+ public readonly type = ComponentType.Armor;
374
+ constructor(public armor: number = 10) {}
375
+ }
376
+
377
+ // Usage: attach to entity as usual
378
+ entity.addComponent(new ArmorComponent(15));
379
+ ```
380
+
381
+ ### SoAComponent (Structure-of-Arrays Components)
382
+
383
+ Components backed by contiguous typed arrays (`Float64Array`, `BigInt64Array`, etc.) for cache-friendly memory layout.
384
+
385
+ **Use when:**
386
+ - The component is iterated every tick in a hot loop (physics, transforms, velocities)
387
+ - There are many instances (hundreds/thousands of entities)
388
+ - The data is flat numeric fields (positions, velocities, radii)
389
+ - You need deterministic fixed-point storage via `BigInt64Array` (`'i64'` fields)
390
+
391
+ **Avoid when:**
392
+ - The data is complex (nested objects, strings, variable-length arrays)
393
+ - There are very few instances — the typed-array overhead isn't worth it
394
+ - The component is rarely queried
395
+
396
+ ```typescript
397
+ import { SoAComponent, defineSoASchema } from '@phalanx-engine/ecs';
398
+
399
+ // 1. Define a schema — maps field names to typed-array element types
400
+ const PhysicsSoASchema = defineSoASchema({
401
+ velocityX: 'i64', // BigInt64Array — deterministic fixed-point
402
+ velocityY: 'i64',
403
+ velocityZ: 'i64',
404
+ radius: 'i64',
405
+ isStatic: 'u8', // Uint8Array — boolean flag
406
+ }, 'PhysicsBody');
407
+
408
+ // 2. Extend SoAComponent
409
+ class PhysicsBodyComponent extends SoAComponent<typeof PhysicsSoASchema.definition> {
410
+ public readonly type = ComponentType.PhysicsBody;
411
+ static readonly soaSchema = PhysicsSoASchema;
412
+
413
+ constructor(entityId: number, radius: bigint) {
414
+ super(PhysicsSoASchema, entityId, {
415
+ velocityX: 0n,
416
+ velocityY: 0n,
417
+ velocityZ: 0n,
418
+ radius: radius,
419
+ isStatic: 0,
420
+ });
421
+ }
422
+
423
+ // Getters/setters provide a clean API over raw array access
424
+ get radiusRaw(): bigint {
425
+ return this.store.arrays.radius[this.getIndex()];
426
+ }
427
+ }
428
+
429
+ // 3. Hot-path systems should bypass the component facade and access arrays directly
430
+ // Cache store references in init(), iterate entityIds() in tick methods
431
+ const store = entityManager.getSoAStore(PhysicsSoASchema)!;
432
+
433
+ // Single-store loop — ideal case, zero cross-store overhead
434
+ for (const entityId of store.entityIds()) {
435
+ const idx = store.indexOf(entityId);
436
+ store.arrays.velocityX[idx] += accelerationX;
437
+ }
438
+
439
+ // Cross-store loop — needed when correlating two SoA stores
440
+ const txStore = entityManager.getSoAStore(TransformSoASchema)!;
441
+ const velocityX = store.arrays.velocityX;
442
+ const fpPositionX = txStore.arrays.fpPositionX;
443
+ for (const entityId of store.entityIds()) {
444
+ const physIdx = store.indexOf(entityId);
445
+ const txIdx = txStore.indexOf(entityId); // one Map.get() per entity
446
+ if (txIdx === -1) continue;
447
+ fpPositionX[txIdx] += velocityX[physIdx];
448
+ }
449
+ ```
450
+
451
+ **Important:** The facade (getters/setters on `SoAComponent`) is convenient for infrequent
452
+ access (spawning, event handlers) but adds overhead in hot loops — each field access calls
453
+ `getIndex()` (a `Map.get()` + stale check). Direct store access removes this overhead.
454
+
455
+ ### SoA Field Types
456
+
457
+ | Type | TypedArray | JS Value | Use Case |
458
+ | ------ | ---------------- | --------- | --------------------------------------- |
459
+ | `f64` | `Float64Array` | `number` | Floating-point values, visual positions |
460
+ | `f32` | `Float32Array` | `number` | Lower-precision floats |
461
+ | `i32` | `Int32Array` | `number` | Signed integers |
462
+ | `u32` | `Uint32Array` | `number` | Unsigned integers |
463
+ | `u8` | `Uint8Array` | `number` | Flags, booleans (0/1) |
464
+ | `i64` | `BigInt64Array` | `bigint` | Fixed-point raw values (deterministic) |
465
+
466
+ ### SoA Store Lifecycle
467
+
468
+ Stores are **lazily created** when the first `SoAComponent` of a given schema is instantiated. `GameWorld` sets the `EntityManager` context automatically — no manual store registration needed.
469
+
470
+ ```
471
+ GameWorld created → SoAComponent.useEntityManager(em)
472
+ First PhysicsBodyComponent constructed → store created in EntityManager
473
+ Subsequent PhysicsBodyComponents → share the same store
474
+ GameWorld disposed → SoAComponent.resetContext()
475
+ ```
476
+
477
+ ## Object Pooling
478
+
479
+ Phalanx ECS includes a built-in pooling system to avoid garbage collection spikes in hot loops. This is critical for deterministic lockstep games where GC pauses can cause missed ticks.
480
+
481
+ ### ObjectPool
482
+
483
+ Generic pool for any object implementing `IPoolable`:
484
+
485
+ ```typescript
486
+ import { ObjectPool } from '@phalanx-engine/ecs';
487
+ import type { IPoolable } from '@phalanx-engine/ecs';
488
+
489
+ class Particle implements IPoolable {
490
+ x = 0; y = 0; life = 0;
491
+ reset(): void { this.x = 0; this.y = 0; this.life = 0; }
492
+ }
493
+
494
+ const pool = new ObjectPool(() => new Particle(), { initialSize: 100 });
495
+ pool.prewarm(100);
496
+
497
+ const p = pool.acquire(); // reuses an existing object or creates new
498
+ p.x = 10; p.life = 60;
499
+ pool.release(p); // returns to pool, calls reset()
500
+ ```
501
+
502
+ ### Entity pooling with GameWorld
503
+
504
+ Attach all components once in the entity constructor. Use `IPoolableEntity` for per-spawn values and let the engine handle SoA row lifecycle automatically:
505
+
506
+ ```typescript
507
+ import { GameWorld, Entity, type IPoolableEntity } from '@phalanx-engine/ecs';
508
+
509
+ export interface ProjectileSpawnArgs {
510
+ fpPosition: FPVector3;
511
+ fpDirection2: FPVector2;
512
+ teamId: number;
513
+ }
514
+
515
+ export class ProjectileEntity extends Entity implements IPoolableEntity<ProjectileSpawnArgs> {
516
+ private readonly transform: TransformComponent;
517
+ private readonly projectile: ProjectileComponent;
518
+
519
+ constructor() {
520
+ super();
521
+ this.addComponent(MeshComponent.createProjectile(radius));
522
+ this.projectile = this.addComponent(new ProjectileComponent());
523
+ // SoA rows are auto-managed — attach wrappers once, never call reattach/detach
524
+ this.transform = this.addComponent(new TransformComponent(this.id));
525
+ this.addComponent(new PhysicsBodyComponent(this.id, { radius }));
526
+ }
527
+
528
+ onSpawn(args: ProjectileSpawnArgs): void {
529
+ this.transform.fpPosition = args.fpPosition;
530
+ this.projectile.fpDirection2 = args.fpDirection2;
531
+ this.teamId = args.teamId;
532
+ }
533
+
534
+ onDespawn(): void {
535
+ this.active = false;
536
+ }
537
+ }
538
+
539
+ const world = new GameWorld({
540
+ componentTypes: Object.values(ComponentType),
541
+ pooling: {
542
+ autoPrewarm: true,
543
+ entityTypes: {
544
+ projectile: {
545
+ factory: () => new ProjectileEntity(),
546
+ pool: { initialSize: 50, maxSize: 200 },
547
+ },
548
+ },
549
+ },
550
+ });
551
+
552
+ // Spawn: component onSpawn() → entity.onSpawn(args) → EntityManager.addEntity()
553
+ const projectile = world.pools!.spawn<ProjectileEntity>('projectile', {
554
+ fpPosition: spawnPosition,
555
+ fpDirection2: direction2,
556
+ teamId: caster.teamId,
557
+ });
558
+
559
+ // Despawn: EntityManager.removeEntity() → entity.onDespawn() → component onDespawn() → pool
560
+ world.pools!.despawn(projectile);
561
+ ```
562
+
563
+ ### PoolManager
564
+
565
+ `PoolManager` is constructed with an `EntityManager` and wired automatically when `pooling` is configured on `GameWorld`. Game code uses `spawn()` / `despawn()` — not low-level `acquire()` / `release()`:
566
+
567
+ ```typescript
568
+ // Diagnostics
569
+ const stats = world.pools!.getStats(); // Map<string, PoolStats>
570
+ ```
571
+
572
+ ### IPoolableComponent
573
+
574
+ Components that manage backing storage (SoA rows, mesh visibility) implement `IPoolableComponent`. The engine calls these hooks automatically — game code never does:
575
+
576
+ ```typescript
577
+ import type { IPoolableComponent } from '@phalanx-engine/ecs';
578
+
579
+ // SoAComponent implements IPoolableComponent generically — subclasses need no changes.
580
+ // Custom render components can toggle visibility in onSpawn/onDespawn:
581
+
582
+ class MeshComponent implements IPoolableComponent {
583
+ onSpawn(): void { this.mesh.isVisible = true; }
584
+ onDespawn(): void { this.mesh.isVisible = false; }
585
+ }
586
+ ```
587
+
588
+ `EntityPool` remains available as a low-level storage primitive; prefer `PoolManager.spawn()` / `despawn()` for gameplay entities.
589
+
590
+ ## Creating Custom Systems
591
+
592
+ ```typescript
593
+ import { GameSystem, SystemContext } from '@phalanx-engine/ecs';
594
+
595
+ class MySystem extends GameSystem {
596
+ init(context: SystemContext): void {
597
+ super.init(context);
598
+
599
+ // Optional: resolve other systems or optional services
600
+ const movement = context.getSystem(MovementSystem);
601
+ const physics = this.physics; // IPhysicsWorld | undefined
602
+ const abilities = this.abilities; // IAbilitySystem | undefined
603
+
604
+ // Subscribe to events
605
+ this.subscribe('MY_EVENT', (data) => {
606
+ console.log('Event received:', data);
607
+ });
608
+ }
609
+
610
+ processTick(tick: number): void {
611
+ // Deterministic simulation logic
612
+ const entities = this.entityManager.queryEntities(ComponentType.Movement);
613
+ for (const entity of entities) {
614
+ // Update entity
615
+ }
616
+ }
617
+
618
+ update(deltaTime: number): void {
619
+ // Visual updates, animations, interpolation
620
+ }
621
+
622
+ dispose(): void {
623
+ super.dispose();
624
+ // Clean up resources
625
+ }
626
+ }
627
+ ```
628
+
629
+ ## EventBus vs Direct System Calls
630
+
631
+ In a deterministic lockstep game, not every action needs to go through the network. Use this guideline to decide:
632
+
633
+ | Scenario | Mechanism | Why |
634
+ | --- | --- | --- |
635
+ | **Player commands** (move, attack, place unit) | Network → EventBus | Only player intent crosses the wire; all clients execute the same commands |
636
+ | **Simulation-internal decisions** (combat chase, resume movement after target dies, AI pathing) | Direct system call via `context.getSystem()` | These are deterministic outcomes computed identically on every client during tick processing — sending them through the network would be redundant and add latency |
637
+ | **Cross-system notifications** (damage dealt, entity spawned, animation trigger) | EventBus (local) | Keeps systems decoupled without network overhead |
638
+
639
+ ### Example: Direct System Call for Combat Movement
640
+
641
+ When CombatSystem decides a unit should chase a target or resume its original waypoint after killing an enemy, it calls `MovementSystem.moveEntityTo()` directly instead of emitting a network event:
642
+
643
+ ```typescript
644
+ export class CombatSystem extends GameSystem {
645
+ private movementSystem!: MovementSystem;
646
+
647
+ init(context: SystemContext): void {
648
+ super.init(context);
649
+ // Resolve at init — guaranteed to be available during tick processing
650
+ const ms = context.getSystem(MovementSystem);
651
+ if (!ms) throw new Error('CombatSystem requires MovementSystem');
652
+ this.movementSystem = ms;
653
+ }
654
+
655
+ private requestMove(entityId: number, target: Vector3): void {
656
+ // Direct call — deterministic, no network round-trip
657
+ this.movementSystem.moveEntityTo(entityId, target);
658
+ }
659
+ }
660
+ ```
661
+
662
+ **Rule of thumb:** if every client will compute the same result from the same game state, call the system directly. If the action originates from a specific player's input, send it through the network.
663
+
664
+ ## License
665
+
666
+ MIT
@@ -0,0 +1,7 @@
1
+ export interface IComponent {
2
+ readonly type: symbol;
3
+ }
4
+ export declare function createComponentTypeRegistry<T extends Record<string, string>>(types: T): {
5
+ [K in keyof T]: symbol;
6
+ };
7
+ //# sourceMappingURL=Component.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Component.d.ts","sourceRoot":"","sources":["../src/Component.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAeD,wBAAgB,2BAA2B,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC1E,KAAK,EAAE,CAAC,GACP;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,MAAM;CAAE,CAM5B"}
@@ -0,0 +1,7 @@
1
+ export function createComponentTypeRegistry(types) {
2
+ const registry = {};
3
+ for (const key in types) {
4
+ registry[key] = Symbol(types[key]);
5
+ }
6
+ return registry;
7
+ }