ecspresso 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +200 -148
- package/dist/asset-manager.d.ts +1 -1
- package/dist/asset-types.d.ts +2 -2
- package/dist/command-buffer.d.ts +34 -24
- package/dist/ecspresso-builder.d.ts +100 -72
- package/dist/ecspresso.d.ts +257 -122
- package/dist/entity-manager.d.ts +57 -47
- package/dist/index.d.ts +5 -4
- package/dist/plugin.d.ts +61 -0
- package/dist/{bundles → plugins}/audio.d.ts +27 -47
- package/dist/{bundles → plugins}/bounds.d.ts +17 -25
- package/dist/{bundles → plugins}/camera.d.ts +8 -9
- package/dist/{bundles → plugins}/collision.d.ts +22 -26
- package/dist/plugins/coroutine.d.ts +126 -0
- package/dist/{bundles → plugins}/diagnostics.d.ts +5 -4
- package/dist/{bundles → plugins}/input.d.ts +9 -15
- package/dist/plugins/particles.d.ts +225 -0
- package/dist/{bundles → plugins}/physics2D.d.ts +27 -23
- package/dist/{bundles → plugins}/renderers/renderer2D.d.ts +40 -39
- package/dist/{bundles → plugins}/spatial-index.d.ts +11 -10
- package/dist/plugins/sprite-animation.d.ts +150 -0
- package/dist/{bundles → plugins}/state-machine.d.ts +50 -104
- package/dist/plugins/timers.d.ts +151 -0
- package/dist/{bundles → plugins}/transform.d.ts +18 -19
- package/dist/{bundles → plugins}/tween.d.ts +36 -71
- package/dist/resource-manager.d.ts +32 -7
- package/dist/screen-manager.d.ts +17 -11
- package/dist/screen-types.d.ts +5 -2
- package/dist/src/index.js +2 -2
- package/dist/src/index.js.map +17 -17
- package/dist/src/plugins/audio.js +4 -0
- package/dist/src/plugins/audio.js.map +10 -0
- package/dist/src/plugins/bounds.js +4 -0
- package/dist/src/plugins/bounds.js.map +10 -0
- package/dist/src/plugins/camera.js +4 -0
- package/dist/src/plugins/camera.js.map +10 -0
- package/dist/src/plugins/collision.js +4 -0
- package/dist/src/plugins/collision.js.map +11 -0
- package/dist/src/plugins/coroutine.js +4 -0
- package/dist/src/plugins/coroutine.js.map +10 -0
- package/dist/src/plugins/diagnostics.js +5 -0
- package/dist/src/plugins/diagnostics.js.map +10 -0
- package/dist/src/plugins/input.js +4 -0
- package/dist/src/plugins/input.js.map +10 -0
- package/dist/src/plugins/particles.js +4 -0
- package/dist/src/plugins/particles.js.map +10 -0
- package/dist/src/plugins/physics2D.js +4 -0
- package/dist/src/plugins/physics2D.js.map +11 -0
- package/dist/src/plugins/renderers/renderer2D.js +4 -0
- package/dist/src/plugins/renderers/renderer2D.js.map +10 -0
- package/dist/src/plugins/spatial-index.js +4 -0
- package/dist/src/plugins/spatial-index.js.map +11 -0
- package/dist/src/plugins/sprite-animation.js +4 -0
- package/dist/src/plugins/sprite-animation.js.map +10 -0
- package/dist/src/plugins/state-machine.js +4 -0
- package/dist/src/plugins/state-machine.js.map +10 -0
- package/dist/src/plugins/timers.js +4 -0
- package/dist/src/plugins/timers.js.map +10 -0
- package/dist/src/plugins/transform.js +4 -0
- package/dist/src/plugins/transform.js.map +10 -0
- package/dist/src/plugins/tween.js +4 -0
- package/dist/src/plugins/tween.js.map +11 -0
- package/dist/system-builder.d.ts +66 -97
- package/dist/type-utils.d.ts +218 -27
- package/dist/types.d.ts +52 -24
- package/dist/utils/check-required-cycle.d.ts +1 -1
- package/dist/utils/narrowphase.d.ts +7 -7
- package/package.json +53 -45
- package/dist/bundle.d.ts +0 -173
- package/dist/bundles/timers.d.ts +0 -173
- package/dist/src/bundles/audio.js +0 -4
- package/dist/src/bundles/audio.js.map +0 -10
- package/dist/src/bundles/bounds.js +0 -4
- package/dist/src/bundles/bounds.js.map +0 -10
- package/dist/src/bundles/camera.js +0 -4
- package/dist/src/bundles/camera.js.map +0 -10
- package/dist/src/bundles/collision.js +0 -4
- package/dist/src/bundles/collision.js.map +0 -11
- package/dist/src/bundles/diagnostics.js +0 -5
- package/dist/src/bundles/diagnostics.js.map +0 -10
- package/dist/src/bundles/input.js +0 -4
- package/dist/src/bundles/input.js.map +0 -10
- package/dist/src/bundles/physics2D.js +0 -4
- package/dist/src/bundles/physics2D.js.map +0 -11
- package/dist/src/bundles/renderers/renderer2D.js +0 -4
- package/dist/src/bundles/renderers/renderer2D.js.map +0 -10
- package/dist/src/bundles/spatial-index.js +0 -4
- package/dist/src/bundles/spatial-index.js.map +0 -11
- package/dist/src/bundles/state-machine.js +0 -4
- package/dist/src/bundles/state-machine.js.map +0 -10
- package/dist/src/bundles/timers.js +0 -4
- package/dist/src/bundles/timers.js.map +0 -10
- package/dist/src/bundles/transform.js +0 -4
- package/dist/src/bundles/transform.js.map +0 -10
- package/dist/src/bundles/tween.js +0 -4
- package/dist/src/bundles/tween.js.map +0 -11
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ A type-safe, modular, and extensible Entity Component System (ECS) framework for
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
9
|
- **Type-Safe**: Full TypeScript support with component, event, and resource type inference
|
|
10
|
-
- **Modular**:
|
|
10
|
+
- **Modular**: Plugin-based architecture for organizing features
|
|
11
11
|
- **Developer-Friendly**: Clean, fluent API with method chaining
|
|
12
12
|
- **Event-Driven**: Integrated event system for decoupled communication
|
|
13
13
|
- **Resource Management**: Global state management with lazy loading
|
|
@@ -22,7 +22,7 @@ A type-safe, modular, and extensible Entity Component System (ECS) framework for
|
|
|
22
22
|
- **Component Lifecycle**: Callbacks for component add/remove with unsubscribe support
|
|
23
23
|
- **Required Components**: Auto-add dependent components on spawn/addComponent (e.g. `localTransform` implies `worldTransform`)
|
|
24
24
|
- **Command Buffer**: Deferred structural changes for safe entity/component operations during systems
|
|
25
|
-
- **Timer
|
|
25
|
+
- **Timer Plugin**: ECS-native timers with event-based completion notifications
|
|
26
26
|
|
|
27
27
|
## Installation
|
|
28
28
|
|
|
@@ -48,7 +48,7 @@ npm install ecspresso
|
|
|
48
48
|
- [Entity Hierarchy](#entity-hierarchy) -- [Traversal](#traversal), [Parent-First Traversal](#parent-first-traversal), [Cascade Deletion](#cascade-deletion)
|
|
49
49
|
- [Change Detection](#change-detection) -- [Marking Changes](#marking-changes), [Changed Query Filter](#changed-query-filter), [Sequence Timing](#sequence-timing)
|
|
50
50
|
- [Command Buffer](#command-buffer) -- [Available Commands](#available-commands)
|
|
51
|
-
- [
|
|
51
|
+
- [Plugins](#plugins) -- [Plugin Factory](#plugin-factory), [Required Components](#required-components), [Built-in Plugins](#built-in-plugins), [Timer Plugin](#timer-plugin)
|
|
52
52
|
- [Asset Management](#asset-management)
|
|
53
53
|
- [Screen Management](#screen-management) -- [Screen-Scoped Systems](#screen-scoped-systems), [Screen Resource](#screen-resource)
|
|
54
54
|
- [Type Safety](#type-safety)
|
|
@@ -67,8 +67,10 @@ interface Components {
|
|
|
67
67
|
health: { value: number };
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
// 2. Create a world
|
|
71
|
-
const world =
|
|
70
|
+
// 2. Create a world using the builder — types are inferred automatically
|
|
71
|
+
const world = ECSpresso.create()
|
|
72
|
+
.withComponentTypes<Components>()
|
|
73
|
+
.build();
|
|
72
74
|
|
|
73
75
|
// 3. Add a movement system
|
|
74
76
|
world.addSystem('movement')
|
|
@@ -78,8 +80,7 @@ world.addSystem('movement')
|
|
|
78
80
|
entity.components.position.x += entity.components.velocity.x * deltaTime;
|
|
79
81
|
entity.components.position.y += entity.components.velocity.y * deltaTime;
|
|
80
82
|
}
|
|
81
|
-
})
|
|
82
|
-
.build();
|
|
83
|
+
});
|
|
83
84
|
|
|
84
85
|
// 4. Create entities
|
|
85
86
|
const player = world.spawn({
|
|
@@ -187,8 +188,7 @@ world.addSystem('combat')
|
|
|
187
188
|
// Combat logic here
|
|
188
189
|
}
|
|
189
190
|
}
|
|
190
|
-
})
|
|
191
|
-
.build();
|
|
191
|
+
});
|
|
192
192
|
```
|
|
193
193
|
|
|
194
194
|
### Resources
|
|
@@ -201,10 +201,11 @@ interface Resources {
|
|
|
201
201
|
settings: { difficulty: 'easy' | 'hard' };
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
-
const world =
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
204
|
+
const world = ECSpresso.create()
|
|
205
|
+
.withComponentTypes<Components>()
|
|
206
|
+
.withResourceTypes<Resources>()
|
|
207
|
+
.withResource('score', { value: 0 })
|
|
208
|
+
.build();
|
|
208
209
|
|
|
209
210
|
// Sync or async factories (lazy initialization)
|
|
210
211
|
world.addResource('config', () => ({ difficulty: 'normal', soundEnabled: true }));
|
|
@@ -224,16 +225,14 @@ world.addSystem('scoring')
|
|
|
224
225
|
.setProcess((queries, deltaTime, ecs) => {
|
|
225
226
|
const score = ecs.getResource('score');
|
|
226
227
|
score.value += 10;
|
|
227
|
-
})
|
|
228
|
-
.build();
|
|
228
|
+
});
|
|
229
229
|
```
|
|
230
230
|
|
|
231
|
-
|
|
231
|
+
Resources also chain naturally with plugins in the builder:
|
|
232
232
|
|
|
233
233
|
```typescript
|
|
234
|
-
const world = ECSpresso
|
|
235
|
-
.
|
|
236
|
-
.withBundle(physicsBundle)
|
|
234
|
+
const world = ECSpresso.create()
|
|
235
|
+
.withPlugin(physicsPlugin)
|
|
237
236
|
.withResource('config', { debug: true, maxEntities: 1000 })
|
|
238
237
|
.withResource('score', () => ({ value: 0 }))
|
|
239
238
|
.withResource('cache', {
|
|
@@ -267,21 +266,20 @@ await world.disposeResources(); // All, in reverse dependency order
|
|
|
267
266
|
|
|
268
267
|
### Method Chaining
|
|
269
268
|
|
|
270
|
-
|
|
269
|
+
Systems use a fluent builder API: `world.addSystem().addQuery().setProcess()` -- systems are automatically registered via deferred finalization. No explicit termination call is needed.
|
|
271
270
|
|
|
272
271
|
```typescript
|
|
273
272
|
world.addSystem('physics')
|
|
274
273
|
.addQuery('moving', { with: ['position', 'velocity'] })
|
|
275
274
|
.setProcess((queries, deltaTime) => {
|
|
276
275
|
// Physics logic
|
|
277
|
-
})
|
|
278
|
-
|
|
279
|
-
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
world.addSystem('rendering')
|
|
280
279
|
.addQuery('visible', { with: ['position', 'sprite'] })
|
|
281
280
|
.setProcess((queries) => {
|
|
282
281
|
// Rendering logic
|
|
283
|
-
})
|
|
284
|
-
.build();
|
|
282
|
+
});
|
|
285
283
|
```
|
|
286
284
|
|
|
287
285
|
### Query Type Utilities
|
|
@@ -312,8 +310,7 @@ world.addSystem('movement')
|
|
|
312
310
|
for (const entity of queries.entities) {
|
|
313
311
|
updatePosition(entity, deltaTime);
|
|
314
312
|
}
|
|
315
|
-
})
|
|
316
|
-
.build();
|
|
313
|
+
});
|
|
317
314
|
```
|
|
318
315
|
|
|
319
316
|
### System Phases
|
|
@@ -329,33 +326,35 @@ Each phase's command buffer is played back before the next phase begins, so enti
|
|
|
329
326
|
```typescript
|
|
330
327
|
world.addSystem('input')
|
|
331
328
|
.inPhase('preUpdate')
|
|
332
|
-
.setProcess((queries, dt, ecs) => { /* Read input, update timers */ })
|
|
333
|
-
|
|
334
|
-
|
|
329
|
+
.setProcess((queries, dt, ecs) => { /* Read input, update timers */ });
|
|
330
|
+
|
|
331
|
+
world.addSystem('physics')
|
|
335
332
|
.inPhase('fixedUpdate')
|
|
336
333
|
.setProcess((queries, dt, ecs) => {
|
|
337
334
|
// dt is always fixedDt here (e.g. 1/60)
|
|
338
335
|
// Runs 0..N times per frame based on accumulated time
|
|
339
|
-
})
|
|
340
|
-
|
|
341
|
-
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
world.addSystem('gameplay')
|
|
342
339
|
.inPhase('update') // default phase
|
|
343
|
-
.setProcess((queries, dt, ecs) => { /* Game logic, AI */ })
|
|
344
|
-
|
|
345
|
-
|
|
340
|
+
.setProcess((queries, dt, ecs) => { /* Game logic, AI */ });
|
|
341
|
+
|
|
342
|
+
world.addSystem('transform-sync')
|
|
346
343
|
.inPhase('postUpdate')
|
|
347
|
-
.setProcess((queries, dt, ecs) => { /* Transform propagation */ })
|
|
348
|
-
|
|
349
|
-
|
|
344
|
+
.setProcess((queries, dt, ecs) => { /* Transform propagation */ });
|
|
345
|
+
|
|
346
|
+
world.addSystem('renderer')
|
|
350
347
|
.inPhase('render')
|
|
351
|
-
.setProcess((queries, dt, ecs) => { /* Visual output */ })
|
|
352
|
-
.build();
|
|
348
|
+
.setProcess((queries, dt, ecs) => { /* Visual output */ });
|
|
353
349
|
```
|
|
354
350
|
|
|
355
351
|
**Fixed Timestep** -- The `fixedUpdate` phase uses a time accumulator for deterministic simulation. A spiral-of-death cap (8 steps) prevents runaway accumulation.
|
|
356
352
|
|
|
357
353
|
```typescript
|
|
358
|
-
const world = ECSpresso.create
|
|
354
|
+
const world = ECSpresso.create()
|
|
355
|
+
.withComponentTypes<Components>()
|
|
356
|
+
.withEventTypes<Events>()
|
|
357
|
+
.withResourceTypes<Resources>()
|
|
359
358
|
.withFixedTimestep(1 / 60) // 60Hz physics (default)
|
|
360
359
|
.build();
|
|
361
360
|
```
|
|
@@ -372,13 +371,12 @@ Within each phase, systems execute in priority order (higher numbers first). Sys
|
|
|
372
371
|
world.addSystem('physics')
|
|
373
372
|
.inPhase('fixedUpdate')
|
|
374
373
|
.setPriority(100) // Runs first within fixedUpdate
|
|
375
|
-
.setProcess(() => { /* physics */ })
|
|
376
|
-
|
|
377
|
-
|
|
374
|
+
.setProcess(() => { /* physics */ });
|
|
375
|
+
|
|
376
|
+
world.addSystem('constraints')
|
|
378
377
|
.inPhase('fixedUpdate')
|
|
379
378
|
.setPriority(50) // Runs second within fixedUpdate
|
|
380
|
-
.setProcess(() => { /* constraints */ })
|
|
381
|
-
.build();
|
|
379
|
+
.setProcess(() => { /* constraints */ });
|
|
382
380
|
```
|
|
383
381
|
|
|
384
382
|
### System Groups
|
|
@@ -389,13 +387,12 @@ Organize systems into groups that can be enabled/disabled at runtime:
|
|
|
389
387
|
world.addSystem('renderSprites')
|
|
390
388
|
.inGroup('rendering')
|
|
391
389
|
.addQuery('sprites', { with: ['position', 'sprite'] })
|
|
392
|
-
.setProcess((queries) => { /* ... */ })
|
|
393
|
-
|
|
394
|
-
|
|
390
|
+
.setProcess((queries) => { /* ... */ });
|
|
391
|
+
|
|
392
|
+
world.addSystem('renderParticles')
|
|
395
393
|
.inGroup('rendering')
|
|
396
394
|
.inGroup('effects') // Systems can belong to multiple groups
|
|
397
|
-
.setProcess(() => { /* ... */ })
|
|
398
|
-
.build();
|
|
395
|
+
.setProcess(() => { /* ... */ });
|
|
399
396
|
|
|
400
397
|
world.disableSystemGroup('rendering'); // All rendering systems skip
|
|
401
398
|
world.enableSystemGroup('rendering'); // Resume rendering
|
|
@@ -416,8 +413,7 @@ world.addSystem('gameSystem')
|
|
|
416
413
|
})
|
|
417
414
|
.setOnDetach((ecs) => {
|
|
418
415
|
console.log('System shutting down...');
|
|
419
|
-
})
|
|
420
|
-
.build();
|
|
416
|
+
});
|
|
421
417
|
|
|
422
418
|
await world.initialize();
|
|
423
419
|
```
|
|
@@ -449,7 +445,10 @@ interface Events {
|
|
|
449
445
|
};
|
|
450
446
|
}
|
|
451
447
|
|
|
452
|
-
const world =
|
|
448
|
+
const world = ECSpresso.create()
|
|
449
|
+
.withComponentTypes<Components>()
|
|
450
|
+
.withEventTypes<Events>()
|
|
451
|
+
.build();
|
|
453
452
|
|
|
454
453
|
// Subscribe - returns unsubscribe function
|
|
455
454
|
const unsubscribe = world.on('playerDied', (data) => {
|
|
@@ -465,19 +464,16 @@ world.off('levelComplete', handler);
|
|
|
465
464
|
// Handle events in systems
|
|
466
465
|
world.addSystem('gameLogic')
|
|
467
466
|
.setEventHandlers({
|
|
468
|
-
playerDied: {
|
|
469
|
-
|
|
470
|
-
// Respawn logic
|
|
471
|
-
}
|
|
467
|
+
playerDied: (data, ecs) => {
|
|
468
|
+
// Respawn logic
|
|
472
469
|
}
|
|
473
|
-
})
|
|
474
|
-
.build();
|
|
470
|
+
});
|
|
475
471
|
|
|
476
472
|
// Publish events from anywhere
|
|
477
473
|
world.eventBus.publish('playerDied', { playerId: 1 });
|
|
478
474
|
```
|
|
479
475
|
|
|
480
|
-
**Built-in events**: `hierarchyChanged` (entity parent changes), `assetLoaded` / `assetFailed` / `assetGroupProgress` / `assetGroupLoaded` (asset loading), and timer `onComplete` events (see [
|
|
476
|
+
**Built-in events**: `hierarchyChanged` (entity parent changes), `assetLoaded` / `assetFailed` / `assetGroupProgress` / `assetGroupLoaded` (asset loading), and timer `onComplete` events (see [Plugins](#plugins)).
|
|
481
477
|
|
|
482
478
|
## Entity Hierarchy
|
|
483
479
|
|
|
@@ -546,7 +542,7 @@ world.removeEntity(parent.id, { cascade: false });
|
|
|
546
542
|
|
|
547
543
|
Hierarchy changes emit the `hierarchyChanged` event (see [Events](#events)).
|
|
548
544
|
|
|
549
|
-
**World position pattern**: `worldPos = localPos + parent.worldPos`. A parent's world position already includes all grandparents, so each entity only needs to combine its local position with its immediate parent's world position. The Transform
|
|
545
|
+
**World position pattern**: `worldPos = localPos + parent.worldPos`. A parent's world position already includes all grandparents, so each entity only needs to combine its local position with its immediate parent's world position. The Transform plugin implements this automatically.
|
|
550
546
|
|
|
551
547
|
## Change Detection
|
|
552
548
|
|
|
@@ -578,8 +574,7 @@ world.addSystem('render-sync')
|
|
|
578
574
|
for (const entity of queries.moved) {
|
|
579
575
|
syncSpritePosition(entity);
|
|
580
576
|
}
|
|
581
|
-
})
|
|
582
|
-
.build();
|
|
577
|
+
});
|
|
583
578
|
```
|
|
584
579
|
|
|
585
580
|
When multiple components are listed in `changed`, entities matching **any** of them are included (OR semantics).
|
|
@@ -603,7 +598,7 @@ if (em.getChangeSeq(entity.id, 'localTransform') > ecs.changeThreshold) {
|
|
|
603
598
|
|
|
604
599
|
**Deferred marking**: `ecs.commands.markChanged(entity.id, 'position')` queues a mark for command buffer playback.
|
|
605
600
|
|
|
606
|
-
**Built-in
|
|
601
|
+
**Built-in plugin usage**: Movement marks `localTransform` (fixedUpdate) → Transform propagation reads `localTransform` changed, writes+marks `worldTransform` (postUpdate) → Renderer reads `worldTransform` changed (render).
|
|
607
602
|
|
|
608
603
|
## Command Buffer
|
|
609
604
|
|
|
@@ -622,8 +617,7 @@ world.addSystem('combat')
|
|
|
622
617
|
});
|
|
623
618
|
}
|
|
624
619
|
}
|
|
625
|
-
})
|
|
626
|
-
.build();
|
|
620
|
+
});
|
|
627
621
|
```
|
|
628
622
|
|
|
629
623
|
### Available Commands
|
|
@@ -654,52 +648,106 @@ ecs.commands.clear(); // Discard all queued commands
|
|
|
654
648
|
|
|
655
649
|
Commands execute in FIFO order. If a command fails (e.g., entity doesn't exist), it logs a warning and continues with remaining commands.
|
|
656
650
|
|
|
657
|
-
##
|
|
651
|
+
## Plugins
|
|
658
652
|
|
|
659
|
-
Organize related systems and resources into reusable
|
|
653
|
+
Organize related systems and resources into reusable plugins:
|
|
660
654
|
|
|
661
655
|
```typescript
|
|
662
|
-
import {
|
|
656
|
+
import ECSpresso, { definePlugin, type WorldConfigFrom } from 'ecspresso';
|
|
663
657
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
for (const entity of queries.moving) {
|
|
669
|
-
entity.components.position.x += entity.components.velocity.x * deltaTime;
|
|
670
|
-
entity.components.position.y += entity.components.velocity.y * deltaTime;
|
|
671
|
-
}
|
|
672
|
-
})
|
|
673
|
-
.and()
|
|
674
|
-
.addSystem('applyGravity')
|
|
675
|
-
.addQuery('falling', { with: ['velocity'] })
|
|
676
|
-
.setProcess((queries, deltaTime, ecs) => {
|
|
677
|
-
const gravity = ecs.getResource('gravity');
|
|
678
|
-
for (const entity of queries.falling) {
|
|
679
|
-
entity.components.velocity.y += gravity.value * deltaTime;
|
|
680
|
-
}
|
|
681
|
-
})
|
|
682
|
-
.and()
|
|
683
|
-
.addResource('gravity', { value: 9.8 });
|
|
658
|
+
interface PhysicsComponents {
|
|
659
|
+
position: { x: number; y: number };
|
|
660
|
+
velocity: { x: number; y: number };
|
|
661
|
+
}
|
|
684
662
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
663
|
+
interface PhysicsResources {
|
|
664
|
+
gravity: { value: number };
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const physicsPlugin = definePlugin<WorldConfigFrom<PhysicsComponents, {}, PhysicsResources>>({
|
|
668
|
+
id: 'physics',
|
|
669
|
+
install(world) {
|
|
670
|
+
world.addSystem('applyVelocity')
|
|
671
|
+
.addQuery('moving', { with: ['position', 'velocity'] })
|
|
672
|
+
.setProcess((queries, deltaTime) => {
|
|
673
|
+
for (const entity of queries.moving) {
|
|
674
|
+
entity.components.position.x += entity.components.velocity.x * deltaTime;
|
|
675
|
+
entity.components.position.y += entity.components.velocity.y * deltaTime;
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
world.addSystem('applyGravity')
|
|
680
|
+
.addQuery('falling', { with: ['velocity'] })
|
|
681
|
+
.setProcess((queries, deltaTime, ecs) => {
|
|
682
|
+
const gravity = ecs.getResource('gravity');
|
|
683
|
+
for (const entity of queries.falling) {
|
|
684
|
+
entity.components.velocity.y += gravity.value * deltaTime;
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
world.addResource('gravity', { value: 9.8 });
|
|
689
|
+
},
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
// Register plugins with the world — types merge automatically
|
|
693
|
+
const game = ECSpresso.create()
|
|
694
|
+
.withPlugin(physicsPlugin)
|
|
688
695
|
.build();
|
|
689
696
|
```
|
|
690
697
|
|
|
698
|
+
### Plugin Factory
|
|
699
|
+
|
|
700
|
+
When multiple plugins share the same types (common in application code), use `pluginFactory()` on the builder or built world to capture types automatically:
|
|
701
|
+
|
|
702
|
+
```typescript
|
|
703
|
+
// types.ts — builder accumulates all types
|
|
704
|
+
export const builder = ECSpresso.create()
|
|
705
|
+
.withPlugin(createPhysicsPlugin())
|
|
706
|
+
.withComponentTypes<{ player: boolean; enemy: EnemyData }>()
|
|
707
|
+
.withResourceTypes<{ score: number }>();
|
|
708
|
+
|
|
709
|
+
// Types flow from the builder — no manual imports or extends chains
|
|
710
|
+
export const definePlugin = builder.pluginFactory();
|
|
711
|
+
|
|
712
|
+
// movement-plugin.ts — no type params needed
|
|
713
|
+
import { definePlugin } from './types';
|
|
714
|
+
|
|
715
|
+
export const movementPlugin = definePlugin({
|
|
716
|
+
id: 'movement',
|
|
717
|
+
install(world) {
|
|
718
|
+
world.addSystem('movement')
|
|
719
|
+
.addQuery('moving', { with: ['position', 'velocity'] })
|
|
720
|
+
.setProcess((queries, dt) => { /* ... */ });
|
|
721
|
+
},
|
|
722
|
+
});
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
You can also pass a world type directly to `definePlugin` as a one-off alternative:
|
|
726
|
+
|
|
727
|
+
```typescript
|
|
728
|
+
type MyWorld = typeof ecs; // derive from a built world
|
|
729
|
+
const plugin = definePlugin<MyWorld>({
|
|
730
|
+
id: 'my-plugin',
|
|
731
|
+
install(world) { /* world is fully typed */ },
|
|
732
|
+
});
|
|
733
|
+
```
|
|
734
|
+
|
|
691
735
|
### Required Components
|
|
692
736
|
|
|
693
|
-
|
|
737
|
+
Plugins can declare that certain components depend on others. When an entity gains a trigger component, any required components that aren't already present are auto-added with default values:
|
|
694
738
|
|
|
695
739
|
```typescript
|
|
696
|
-
const
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
740
|
+
const transformPlugin = definePlugin<WorldConfigFrom<TransformComponents>>({
|
|
741
|
+
id: 'transform',
|
|
742
|
+
install(world) {
|
|
743
|
+
world.registerRequired('localTransform', 'worldTransform', () => ({
|
|
744
|
+
x: 0, y: 0, rotation: 0, scaleX: 1, scaleY: 1,
|
|
745
|
+
}));
|
|
746
|
+
},
|
|
747
|
+
});
|
|
700
748
|
|
|
701
749
|
const world = ECSpresso.create()
|
|
702
|
-
.
|
|
750
|
+
.withPlugin(transformPlugin)
|
|
703
751
|
.build();
|
|
704
752
|
|
|
705
753
|
// worldTransform is auto-added with defaults
|
|
@@ -736,34 +784,34 @@ world.registerRequired('position', 'velocity', () => ({ x: 0, y: 0 }));
|
|
|
736
784
|
- Auto-added components are marked as changed and trigger reactive queries
|
|
737
785
|
- Component names and factory return types are fully type-checked
|
|
738
786
|
|
|
739
|
-
**Built-in requirements:** The Transform
|
|
787
|
+
**Built-in requirements:** The Transform plugin registers `localTransform` → `worldTransform`. The Physics 2D plugin registers `rigidBody` → `velocity` and `rigidBody` → `force`.
|
|
740
788
|
|
|
741
|
-
### Built-in
|
|
789
|
+
### Built-in Plugins
|
|
742
790
|
|
|
743
|
-
|
|
|
791
|
+
| Plugin | Import | Default Phase | Description |
|
|
744
792
|
|--------|--------|---------------|-------------|
|
|
745
|
-
| **Input** | `ecspresso/
|
|
746
|
-
| **Timers** | `ecspresso/
|
|
747
|
-
| **Movement** | `ecspresso/
|
|
748
|
-
| **Transform** | `ecspresso/
|
|
749
|
-
| **Bounds** | `ecspresso/
|
|
750
|
-
| **Collision** | `ecspresso/
|
|
751
|
-
| **2D Renderer** | `ecspresso/
|
|
793
|
+
| **Input** | `ecspresso/plugins/input` | `preUpdate` | Frame-accurate keyboard/pointer input with action mapping |
|
|
794
|
+
| **Timers** | `ecspresso/plugins/timers` | `preUpdate` | ECS-native timers with event-based completion |
|
|
795
|
+
| **Movement** | `ecspresso/plugins/movement` | `fixedUpdate` | Velocity-based movement integration |
|
|
796
|
+
| **Transform** | `ecspresso/plugins/transform` | `postUpdate` | Hierarchical transform propagation (local/world transforms) |
|
|
797
|
+
| **Bounds** | `ecspresso/plugins/bounds` | `postUpdate` | Screen bounds enforcement (destroy, clamp, wrap) |
|
|
798
|
+
| **Collision** | `ecspresso/plugins/collision` | `postUpdate` | Layer-based AABB/circle collision detection with events |
|
|
799
|
+
| **2D Renderer** | `ecspresso/plugins/renderers/renderer2D` | `render` | Automated PixiJS scene graph wiring |
|
|
752
800
|
|
|
753
|
-
Each
|
|
801
|
+
Each plugin accepts a `phase` option to override its default.
|
|
754
802
|
|
|
755
|
-
### Input
|
|
803
|
+
### Input Plugin
|
|
756
804
|
|
|
757
|
-
The input
|
|
805
|
+
The input plugin provides frame-accurate keyboard, pointer (mouse + touch via PointerEvent), and named action mapping. It's a resource-only plugin — input is polled via the `inputState` resource. DOM events are accumulated between frames and snapshotted once per frame, so all systems see consistent state.
|
|
758
806
|
|
|
759
807
|
```typescript
|
|
760
808
|
import {
|
|
761
|
-
|
|
809
|
+
createInputPlugin,
|
|
762
810
|
type InputResourceTypes, type KeyCode
|
|
763
|
-
} from 'ecspresso/
|
|
811
|
+
} from 'ecspresso/plugins/input';
|
|
764
812
|
|
|
765
813
|
const world = ECSpresso.create()
|
|
766
|
-
.
|
|
814
|
+
.withPlugin(createInputPlugin({
|
|
767
815
|
actions: {
|
|
768
816
|
jump: { keys: [' ', 'ArrowUp'] },
|
|
769
817
|
shoot: { keys: ['z'], buttons: [0] },
|
|
@@ -788,19 +836,19 @@ input.setActionMap({
|
|
|
788
836
|
});
|
|
789
837
|
```
|
|
790
838
|
|
|
791
|
-
Action names are type-safe — `isActive`, `justActivated`, `justDeactivated`, `setActionMap`, and `getActionMap` only accept action names from the config. The type parameter `A` is inferred from the `actions` object keys passed to `
|
|
839
|
+
Action names are type-safe — `isActive`, `justActivated`, `justDeactivated`, `setActionMap`, and `getActionMap` only accept action names from the config. The type parameter `A` is inferred from the `actions` object keys passed to `createInputPlugin`. Defaults to `string` when no actions are configured.
|
|
792
840
|
|
|
793
841
|
Key values use the `KeyCode` type — a union of all standard `KeyboardEvent.key` values — providing autocomplete and compile-time validation. Note that the space bar key is `' '` (a space character), not `'Space'`.
|
|
794
842
|
|
|
795
|
-
### Timer
|
|
843
|
+
### Timer Plugin
|
|
796
844
|
|
|
797
|
-
The timer
|
|
845
|
+
The timer plugin provides ECS-native timers that follow the "data, not callbacks" philosophy. Timers are components processed each frame, with optional event-based completion notifications.
|
|
798
846
|
|
|
799
847
|
```typescript
|
|
800
848
|
import {
|
|
801
|
-
|
|
849
|
+
createTimerPlugin, createTimer, createRepeatingTimer,
|
|
802
850
|
type TimerComponentTypes, type TimerEventData
|
|
803
|
-
} from 'ecspresso/
|
|
851
|
+
} from 'ecspresso/plugins/timers';
|
|
804
852
|
|
|
805
853
|
// Events used with onComplete must have TimerEventData payload
|
|
806
854
|
interface Events {
|
|
@@ -808,21 +856,19 @@ interface Events {
|
|
|
808
856
|
spawnWave: TimerEventData;
|
|
809
857
|
}
|
|
810
858
|
|
|
811
|
-
interface Components extends TimerComponentTypes<Events> {
|
|
812
|
-
position: { x: number; y: number };
|
|
813
|
-
}
|
|
814
|
-
|
|
815
859
|
const world = ECSpresso
|
|
816
|
-
.create
|
|
817
|
-
.
|
|
860
|
+
.create()
|
|
861
|
+
.withPlugin(createTimerPlugin())
|
|
862
|
+
.withComponentTypes<{ position: { x: number; y: number } }>()
|
|
863
|
+
.withEventTypes<Events>()
|
|
818
864
|
.build();
|
|
819
865
|
|
|
820
866
|
// One-shot timer (poll justFinished or use onComplete event)
|
|
821
|
-
world.spawn({ ...createTimer
|
|
822
|
-
world.spawn({ ...createTimer
|
|
867
|
+
world.spawn({ ...createTimer(2.0), position: { x: 0, y: 0 } });
|
|
868
|
+
world.spawn({ ...createTimer(1.5, { onComplete: 'hideMessage' }) });
|
|
823
869
|
|
|
824
870
|
// Repeating timer
|
|
825
|
-
world.spawn({ ...createRepeatingTimer
|
|
871
|
+
world.spawn({ ...createRepeatingTimer(5.0, { onComplete: 'spawnWave' }) });
|
|
826
872
|
```
|
|
827
873
|
|
|
828
874
|
Timer components expose `elapsed`, `duration`, `repeat`, `active`, `justFinished`, and optional `onComplete` for runtime control.
|
|
@@ -838,7 +884,10 @@ type Assets = {
|
|
|
838
884
|
level1Background: { data: ImageBitmap };
|
|
839
885
|
};
|
|
840
886
|
|
|
841
|
-
const game = ECSpresso.create
|
|
887
|
+
const game = ECSpresso.create()
|
|
888
|
+
.withComponentTypes<Components>()
|
|
889
|
+
.withEventTypes<Events>()
|
|
890
|
+
.withResourceTypes<Resources>()
|
|
842
891
|
.withAssets(assets => assets
|
|
843
892
|
// Eager assets - loaded automatically during initialize()
|
|
844
893
|
.add('playerTexture', async () => {
|
|
@@ -869,8 +918,7 @@ game.addSystem('gameplay')
|
|
|
869
918
|
.requiresAssets(['playerTexture'])
|
|
870
919
|
.setProcess((queries, dt, ecs) => {
|
|
871
920
|
const player = ecs.getAsset('playerTexture');
|
|
872
|
-
})
|
|
873
|
-
.build();
|
|
921
|
+
});
|
|
874
922
|
```
|
|
875
923
|
|
|
876
924
|
Asset events (`assetLoaded`, `assetFailed`, `assetGroupProgress`, `assetGroupLoaded`) are available through the event system -- see [Events](#events).
|
|
@@ -894,7 +942,10 @@ type Screens = {
|
|
|
894
942
|
pause: ScreenDefinition<Record<string, never>, Record<string, never>>;
|
|
895
943
|
};
|
|
896
944
|
|
|
897
|
-
const game = ECSpresso.create
|
|
945
|
+
const game = ECSpresso.create()
|
|
946
|
+
.withComponentTypes<Components>()
|
|
947
|
+
.withEventTypes<Events>()
|
|
948
|
+
.withResourceTypes<Resources>()
|
|
898
949
|
.withScreens(screens => screens
|
|
899
950
|
.add('menu', {
|
|
900
951
|
initialState: () => ({ selectedOption: 0 }),
|
|
@@ -932,13 +983,11 @@ game.addSystem('menuUI')
|
|
|
932
983
|
.inScreens(['menu']) // Only runs in 'menu'
|
|
933
984
|
.setProcess((queries, dt, ecs) => {
|
|
934
985
|
renderMenu(ecs.getScreenState().selectedOption);
|
|
935
|
-
})
|
|
936
|
-
.build();
|
|
986
|
+
});
|
|
937
987
|
|
|
938
988
|
game.addSystem('animations')
|
|
939
989
|
.excludeScreens(['pause']) // Runs in all screens except 'pause'
|
|
940
|
-
.setProcess(() => { /* ... */ })
|
|
941
|
-
.build();
|
|
990
|
+
.setProcess(() => { /* ... */ });
|
|
942
991
|
```
|
|
943
992
|
|
|
944
993
|
### Screen Resource
|
|
@@ -956,8 +1005,7 @@ game.addSystem('ui')
|
|
|
956
1005
|
screen.stackDepth; // Number of screens in stack
|
|
957
1006
|
screen.isCurrent('gameplay'); // Check current screen
|
|
958
1007
|
screen.isActive('menu'); // true if in current or stack
|
|
959
|
-
})
|
|
960
|
-
.build();
|
|
1008
|
+
});
|
|
961
1009
|
```
|
|
962
1010
|
|
|
963
1011
|
## Type Safety
|
|
@@ -982,15 +1030,19 @@ world.addSystem('example')
|
|
|
982
1030
|
entity.components.position.x; // ✅ guaranteed
|
|
983
1031
|
entity.components.health.value; // ❌ not in query
|
|
984
1032
|
}
|
|
985
|
-
})
|
|
986
|
-
.build();
|
|
1033
|
+
});
|
|
987
1034
|
|
|
988
|
-
//
|
|
989
|
-
const
|
|
990
|
-
|
|
1035
|
+
// Plugin type compatibility - conflicting types error at compile time
|
|
1036
|
+
const plugin1 = definePlugin<WorldConfigFrom<{ position: { x: number; y: number } }>>({
|
|
1037
|
+
id: 'p1', install() {},
|
|
1038
|
+
});
|
|
1039
|
+
const plugin2 = definePlugin<WorldConfigFrom<{ velocity: { x: number; y: number } }>>({
|
|
1040
|
+
id: 'p2', install() {},
|
|
1041
|
+
});
|
|
1042
|
+
// Builder merges plugin types automatically — no manual type params needed
|
|
991
1043
|
const world = ECSpresso.create()
|
|
992
|
-
.
|
|
993
|
-
.
|
|
1044
|
+
.withPlugin(plugin1)
|
|
1045
|
+
.withPlugin(plugin2)
|
|
994
1046
|
.build();
|
|
995
1047
|
```
|
|
996
1048
|
|
|
@@ -1014,7 +1066,7 @@ world.entityManager.getComponent(123, 'position'); // undefined
|
|
|
1014
1066
|
- Use `changed` query filters to skip unchanged entities in render sync, transform propagation, and similar systems
|
|
1015
1067
|
- Call `markChanged` after in-place mutations so downstream systems can detect the change
|
|
1016
1068
|
- Extract business logic into testable helper functions using query type utilities
|
|
1017
|
-
-
|
|
1069
|
+
- Group related systems into plugins for better organization and reusability
|
|
1018
1070
|
- Use system phases to separate concerns (physics in `fixedUpdate`, rendering in `render`) and priorities for ordering within a phase
|
|
1019
1071
|
- Use resource factories for expensive initialization (textures, audio, etc.)
|
|
1020
1072
|
- Consider component callbacks for immediate reactions to state changes
|
package/dist/asset-manager.d.ts
CHANGED
|
@@ -38,7 +38,7 @@ export default class AssetManager<AssetTypes extends Record<string, unknown> = R
|
|
|
38
38
|
/**
|
|
39
39
|
* Get a loaded asset or undefined
|
|
40
40
|
*/
|
|
41
|
-
|
|
41
|
+
tryGet<K extends keyof AssetTypes>(key: K): AssetTypes[K] | undefined;
|
|
42
42
|
/**
|
|
43
43
|
* Get a handle to an asset with status information
|
|
44
44
|
*/
|
package/dist/asset-types.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ export interface AssetHandle<T> {
|
|
|
26
26
|
/**
|
|
27
27
|
* Get the asset value if loaded, undefined otherwise.
|
|
28
28
|
*/
|
|
29
|
-
|
|
29
|
+
tryGet(): T | undefined;
|
|
30
30
|
}
|
|
31
31
|
/**
|
|
32
32
|
* Resource interface for accessing assets in systems
|
|
@@ -56,7 +56,7 @@ export interface AssetsResource<A extends Record<string, unknown>, G extends str
|
|
|
56
56
|
/**
|
|
57
57
|
* Get a loaded asset or undefined if not loaded
|
|
58
58
|
*/
|
|
59
|
-
|
|
59
|
+
tryGet<K extends keyof A>(key: K): A[K] | undefined;
|
|
60
60
|
/**
|
|
61
61
|
* Get a handle to an asset with status information
|
|
62
62
|
*/
|