iris-ecs 0.0.8 → 0.0.10

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 (51) hide show
  1. package/README.md +250 -61
  2. package/dist/archetype.d.ts +18 -1
  3. package/dist/archetype.d.ts.map +1 -1
  4. package/dist/archetype.js +60 -15
  5. package/dist/archetype.js.map +1 -1
  6. package/dist/component.d.ts +106 -7
  7. package/dist/component.d.ts.map +1 -1
  8. package/dist/component.js +88 -6
  9. package/dist/component.js.map +1 -1
  10. package/dist/directed-acyclic-graph.d.ts +123 -0
  11. package/dist/directed-acyclic-graph.d.ts.map +1 -0
  12. package/dist/directed-acyclic-graph.js +235 -0
  13. package/dist/directed-acyclic-graph.js.map +1 -0
  14. package/dist/entity.d.ts +18 -0
  15. package/dist/entity.d.ts.map +1 -1
  16. package/dist/entity.js +5 -15
  17. package/dist/entity.js.map +1 -1
  18. package/dist/error.d.ts +2 -2
  19. package/dist/error.js +2 -2
  20. package/dist/index.d.ts +10 -8
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +9 -5
  23. package/dist/index.js.map +1 -1
  24. package/dist/name.d.ts +2 -2
  25. package/dist/name.d.ts.map +1 -1
  26. package/dist/name.js +3 -3
  27. package/dist/name.js.map +1 -1
  28. package/dist/observer.d.ts +5 -0
  29. package/dist/observer.d.ts.map +1 -1
  30. package/dist/observer.js.map +1 -1
  31. package/dist/query.d.ts +65 -5
  32. package/dist/query.d.ts.map +1 -1
  33. package/dist/query.js +34 -3
  34. package/dist/query.js.map +1 -1
  35. package/dist/resource.d.ts +62 -4
  36. package/dist/resource.d.ts.map +1 -1
  37. package/dist/resource.js +10 -1
  38. package/dist/resource.js.map +1 -1
  39. package/dist/scheduler.d.ts +119 -11
  40. package/dist/scheduler.d.ts.map +1 -1
  41. package/dist/scheduler.js +175 -68
  42. package/dist/scheduler.js.map +1 -1
  43. package/dist/schema.d.ts +97 -40
  44. package/dist/schema.d.ts.map +1 -1
  45. package/dist/schema.js +31 -69
  46. package/dist/schema.js.map +1 -1
  47. package/dist/world.d.ts +10 -1
  48. package/dist/world.d.ts.map +1 -1
  49. package/dist/world.js +7 -0
  50. package/dist/world.js.map +1 -1
  51. package/package.json +1 -1
package/README.md CHANGED
@@ -30,6 +30,14 @@ ECS is not a good fit for everything. Simple CRUD applications, form-heavy UIs,
30
30
  npm install iris-ecs
31
31
  ```
32
32
 
33
+ ## AI Skills
34
+
35
+ Install the [iris-ecs skill](https://skills.sh) so AI coding agents (Claude Code, Cursor, etc.) understand the iris-ecs API:
36
+
37
+ ```bash
38
+ npx skills add https://github.com/r04423/iris --skill iris-ecs
39
+ ```
40
+
33
41
  ## Quick Start
34
42
 
35
43
  ```typescript
@@ -40,43 +48,40 @@ import {
40
48
  defineSystem,
41
49
  defineTag,
42
50
  cacheQuery,
43
- addComponent,
44
- getComponentValue,
45
- setComponentValue,
51
+ getComponentVectorView,
46
52
  queryEntities,
47
53
  addSystem,
48
54
  runOnce,
49
55
  Type,
50
56
  } from "iris-ecs";
51
57
 
52
- // Define components
53
- const Position = defineComponent("Position", { x: Type.f32(), y: Type.f32() });
54
- const Velocity = defineComponent("Velocity", { x: Type.f32(), y: Type.f32() });
58
+ // Define components -- vector fields store x,y interleaved in one TypedArray
59
+ const Position = defineComponent("Position", { value: Type.f32(2) });
60
+ const Velocity = defineComponent("Velocity", { value: Type.f32(2) });
55
61
  const Player = defineTag("Player");
56
62
 
57
63
  // Create world and entities
58
64
  const world = createWorld();
59
65
 
60
- const player = createEntity(world);
61
- addComponent(world, player, Position, { x: 0, y: 0 });
62
- addComponent(world, player, Velocity, { x: 1, y: 0 });
63
- addComponent(world, player, Player);
66
+ const player = createEntity(world, [
67
+ [Position, { value: [0, 0] }],
68
+ [Velocity, { value: [1, 0] }],
69
+ Player,
70
+ ]);
64
71
 
65
72
  // Define a system -- init runs once, tick runs every frame
66
73
  const movementSystem = defineSystem("movementSystem", (world) => {
67
74
  // Init: cache queries once
68
- const movers = cacheQuery(world, Position, Velocity);
75
+ const movers = cacheQuery(world, [Position, Velocity]);
69
76
 
70
77
  return () => {
71
78
  // Tick: runs every frame
72
79
  queryEntities(world, movers, (e) => {
73
- const px = getComponentValue(world, e, Position, "x");
74
- const py = getComponentValue(world, e, Position, "y");
75
- const vx = getComponentValue(world, e, Velocity, "x");
76
- const vy = getComponentValue(world, e, Velocity, "y");
80
+ const pos = getComponentVectorView(world, e, Position, "value");
81
+ const vel = getComponentVectorView(world, e, Velocity, "value");
77
82
 
78
- setComponentValue(world, e, Position, "x", px + vx);
79
- setComponentValue(world, e, Position, "y", py + vy);
83
+ pos[0] += vel[0];
84
+ pos[1] += vel[1];
80
85
  });
81
86
  };
82
87
  });
@@ -85,7 +90,7 @@ const movementSystem = defineSystem("movementSystem", (world) => {
85
90
  addSystem(world, movementSystem);
86
91
  await runOnce(world);
87
92
 
88
- // Position is now { x: 1, y: 0 }
93
+ // Position is now [1, 0]
89
94
  ```
90
95
 
91
96
  ## Core Concepts
@@ -108,6 +113,12 @@ const world = createWorld();
108
113
  const player = createEntity(world);
109
114
  const enemy = createEntity(world);
110
115
 
116
+ // Create with initial components
117
+ const npc = createEntity(world, [
118
+ [Position, { value: [10, 20] }],
119
+ Enemy,
120
+ ]);
121
+
111
122
  destroyEntity(world, enemy);
112
123
  isEntityAlive(world, enemy); // false
113
124
  isEntityAlive(world, player); // true
@@ -116,7 +127,7 @@ isEntityAlive(world, player); // true
116
127
  resetWorld(world);
117
128
  ```
118
129
 
119
- Create entities with `createEntity()`, destroy them with `destroyEntity()`. Use `isEntityAlive()` to check if an entity reference is still valid. Call `resetWorld()` to clear all entities and state while preserving definitions -- useful for level reloads or testing.
130
+ Create entities with `createEntity()`, optionally passing an array of component entries to attach in one call. Destroy them with `destroyEntity()`. Use `isEntityAlive()` to check if an entity reference is still valid. Call `resetWorld()` to clear all entities and state while preserving definitions -- useful for level reloads or testing.
120
131
 
121
132
  ⚠️ **Entity IDs are recycled.** After destroying an entity, its ID may be reused for a new entity. Never store entity IDs long-term without checking `isEntityAlive()` first -- your old reference might now point to a different entity.
122
133
 
@@ -138,7 +149,7 @@ getName(world, player); // "player-1"
138
149
  lookupByName(world, "player-1"); // player entity
139
150
 
140
151
  // Validate components during lookup -- returns entity only if it has both
141
- lookupByName(world, "player-1", Position, Health);
152
+ lookupByName(world, "player-1", [Position, Health]);
142
153
 
143
154
  removeName(world, player);
144
155
  lookupByName(world, "player-1"); // undefined
@@ -177,18 +188,26 @@ import {
177
188
  defineComponent,
178
189
  Type,
179
190
  addComponent,
191
+ addComponents,
180
192
  getComponentValue,
181
193
  setComponentValue,
194
+ getComponentVectorValue,
195
+ setComponentVectorValue,
196
+ getComponentVectorView,
182
197
  } from "iris-ecs";
183
198
 
184
- const Position = defineComponent("Position", { x: Type.f32(), y: Type.f32() });
199
+ const Position = defineComponent("Position", { value: Type.f32(2) });
185
200
  const Health = defineComponent("Health", { current: Type.i32(), max: Type.i32() });
186
201
 
187
- addComponent(world, entity, Position, { x: 0, y: 0 });
202
+ addComponent(world, entity, Position, { value: [0, 0] });
188
203
  addComponent(world, entity, Health, { current: 100, max: 100 });
189
204
 
190
- const x = getComponentValue(world, entity, Position, "x"); // 0
191
- setComponentValue(world, entity, Position, "x", 10);
205
+ // Scalar fields use getComponentValue / setComponentValue
206
+ const hp = getComponentValue(world, entity, Health, "current"); // 100
207
+ setComponentValue(world, entity, Health, "current", 80);
208
+
209
+ // Vector fields use dedicated access functions
210
+ const pos = getComponentVectorView(world, entity, Position, "value"); // Float32Array [0, 0]
192
211
  ```
193
212
 
194
213
  #### Schema Types
@@ -207,6 +226,8 @@ The `Type` namespace provides storage-optimized types:
207
226
  | `Type.string()` | Array | Text data |
208
227
  | `Type.object<T>()` | Array | Complex nested objects |
209
228
 
229
+ All numeric type factories accept an optional size parameter (2-16) to create **vector fields** -- see [Vector Fields](#vector-fields) below.
230
+
210
231
  Numeric types use TypedArrays for cache-friendly memory layout. Use the smallest type that fits your data.
211
232
 
212
233
  #### Adding Components is Idempotent
@@ -214,14 +235,82 @@ Numeric types use TypedArrays for cache-friendly memory layout. Use the smallest
214
235
  Adding a component that already exists does nothing -- the existing data is preserved.
215
236
 
216
237
  ```typescript
217
- addComponent(world, entity, Position, { x: 0, y: 0 });
218
- addComponent(world, entity, Position, { x: 99, y: 99 }); // ignored
238
+ addComponent(world, entity, Health, { current: 100, max: 100 });
239
+ addComponent(world, entity, Health, { current: 50, max: 50 }); // ignored
219
240
 
220
- getComponentValue(world, entity, Position, "x"); // still 0
241
+ getComponentValue(world, entity, Health, "current"); // still 100
221
242
  ```
222
243
 
223
244
  💡 **Tip:** Use `hasComponent()` to check first if you need conditional addition, or `setComponentValue()` to update existing data.
224
245
 
246
+ #### Batch Adding Components
247
+
248
+ Use `addComponents()` to attach multiple components in one call:
249
+
250
+ ```typescript
251
+ addComponents(world, entity, [
252
+ [Position, { value: [0, 0] }],
253
+ [Velocity, { value: [1, 0] }],
254
+ Player,
255
+ ]);
256
+ ```
257
+
258
+ Each entry is either a standalone ID (tag, entity, schema-less pair) or a `[component, data]` tuple for data components.
259
+
260
+ #### Vector Fields
261
+
262
+ When component fields represent logically grouped numbers (positions, colors, directions), use **vector fields** to store them interleaved in a single TypedArray column. Pass a size (2-16) to any numeric type factory:
263
+
264
+ ```typescript
265
+ import {
266
+ defineComponent,
267
+ addComponent,
268
+ getComponentVectorValue,
269
+ setComponentVectorValue,
270
+ getComponentVectorView,
271
+ Type,
272
+ } from "iris-ecs";
273
+
274
+ const Position = defineComponent("Position", { value: Type.f32(2) });
275
+ const Color = defineComponent("Color", { value: Type.u32(4) });
276
+
277
+ const entity = createEntity(world);
278
+ addComponent(world, entity, Position, { value: [10, 20] });
279
+ addComponent(world, entity, Color, { value: [255, 128, 0, 255] });
280
+ ```
281
+
282
+ Vector fields use dedicated access functions instead of the scalar `getComponentValue` / `setComponentValue`:
283
+
284
+ ```typescript
285
+ // Copy-based read -- returns a tuple (e.g., [number, number])
286
+ const pos = getComponentVectorValue(world, entity, Position, "value");
287
+
288
+ // Copy-based write
289
+ setComponentVectorValue(world, entity, Position, "value", [30, 40]);
290
+
291
+ // Zero-copy view -- returns a TypedArray subarray backed by the column buffer
292
+ const view = getComponentVectorView(world, entity, Position, "value");
293
+ view[0] += 1.0; // direct mutation, no copy
294
+ ```
295
+
296
+ The zero-copy view shares the underlying buffer -- mutations are immediate. Views are invalidated if the archetype resizes (when new entities are added and capacity grows). Use views within a system tick; do not cache across frames.
297
+
298
+ Components can mix scalar and vector fields:
299
+
300
+ ```typescript
301
+ const Particle = defineComponent("Particle", {
302
+ position: Type.f32(3),
303
+ mass: Type.f32(),
304
+ });
305
+
306
+ addComponent(world, entity, Particle, { position: [0, 0, 0], mass: 1.0 });
307
+
308
+ const mass = getComponentValue(world, entity, Particle, "mass"); // number
309
+ const pos = getComponentVectorValue(world, entity, Particle, "position"); // [number, number, number]
310
+ ```
311
+
312
+ TypeScript enforces the scalar/vector boundary at the type level -- `getComponentValue` rejects vector fields, and `getComponentVectorValue` rejects scalar fields.
313
+
225
314
  ### Resources
226
315
 
227
316
  A **Resource** is a global singleton -- world-level data that isn't attached to any specific entity. Define resources using regular components and store them with `addResource()`.
@@ -261,6 +350,32 @@ queryEntities(world, [Time], (entity) => {
261
350
 
262
351
  Use resources for frame timing, configuration, asset registry, input state, physics settings, or any global data that systems need but doesn't belong to a specific entity.
263
352
 
353
+ Resources with vector fields use dedicated access functions, mirroring the component vector API:
354
+
355
+ ```typescript
356
+ import {
357
+ defineComponent,
358
+ addResource,
359
+ getResourceVectorValue,
360
+ setResourceVectorValue,
361
+ getResourceVectorView,
362
+ Type,
363
+ } from "iris-ecs";
364
+
365
+ const Gravity = defineComponent("Gravity", { value: Type.f64(3) });
366
+ addResource(world, Gravity, { value: [0, -9.81, 0] });
367
+
368
+ // Copy-based read
369
+ const g = getResourceVectorValue(world, Gravity, "value"); // [number, number, number]
370
+
371
+ // Copy-based write
372
+ setResourceVectorValue(world, Gravity, "value", [0, -20, 0]);
373
+
374
+ // Zero-copy view
375
+ const view = getResourceVectorView(world, Gravity, "value"); // Float64Array
376
+ view[1] = -15; // direct mutation
377
+ ```
378
+
264
379
  ### Relations
265
380
 
266
381
  A **Relation** describes a directed connection between two entities. Combine a relation with a target using `pair()` to create a pair -- pairs are added to entities like components.
@@ -358,22 +473,24 @@ An **Archetype** groups entities that share the same component set. All entities
358
473
 
359
474
  ```
360
475
  Archetype [Position, Velocity]
361
- ┌─────────┬─────────┬─────────┐
362
- │ Entity │ Pos.x/yVel.x/y
363
- ├─────────┼─────────┼─────────┤
364
- │ bullet1 │ 10, 5 │ 1, 0
365
- │ bullet2 │ 15, 8 │ 1, 0
366
- └─────────┴─────────┴─────────┘
476
+ ┌─────────┬──────────────────┬──────────────────┐
477
+ │ Entity │ Position (vec2) Velocity (vec2)
478
+ ├─────────┼──────────────────┼──────────────────┤
479
+ │ bullet1 │ [10, 5] [1, 0]
480
+ │ bullet2 │ [15, 8] [1, 0]
481
+ └─────────┴──────────────────┴──────────────────┘
367
482
 
368
483
  Archetype [Position, Velocity, Health]
369
- ┌─────────┬─────────┬─────────┬─────────┐
370
- │ Entity │ Pos.x/yVel.x/y │ Health │
371
- ├─────────┼─────────┼─────────┼─────────┤
372
- │ player │ 0, 0 1, 0 │ 100 │
373
- │ enemy │ 50, 20 │ -1, 0 │ 50 │
374
- └─────────┴─────────┴─────────┴─────────┘
484
+ ┌─────────┬──────────────────┬──────────────────┬─────────┐
485
+ │ Entity │ Position (vec2) Velocity (vec2) │ Health │
486
+ ├─────────┼──────────────────┼──────────────────┼─────────┤
487
+ │ player │ [0, 0] │ [1, 0] │ 100 │
488
+ │ enemy │ [50, 20] [-1, 0] │ 50 │
489
+ └─────────┴──────────────────┴──────────────────┴─────────┘
375
490
  ```
376
491
 
492
+ Vector fields like Position store all elements interleaved in a single TypedArray column: `[x0, y0, x1, y1, ...]`. This keeps each entity's vector contiguous in memory for cache-friendly access.
493
+
377
494
  Within an archetype, component data is stored in **columns** (TypedArrays for numeric types). When a query iterates entities with `Position` and `Velocity`, it walks through archetypes that contain both components. This columnar layout keeps data contiguous rather than scattered across objects, reducing memory overhead and enabling efficient iteration.
378
495
 
379
496
  Adding or removing a component moves an entity to a different archetype. This is more expensive than reading or writing component values, so prefer stable component sets for entities that update frequently.
@@ -389,7 +506,7 @@ import { queryEntities, queryFirstEntity, cacheQuery, not } from "iris-ecs";
389
506
 
390
507
  // Iterate all entities with Position and Velocity
391
508
  queryEntities(world, [Position, Velocity], (entity) => {
392
- const x = getComponentValue(world, entity, Position, "x");
509
+ const pos = getComponentVectorView(world, entity, Position, "value")!;
393
510
  // ...
394
511
  });
395
512
 
@@ -401,7 +518,7 @@ Inline terms (arrays) work anywhere, but in systems it's advised to pre-build qu
401
518
 
402
519
  ```typescript
403
520
  const mySystem = defineSystem("mySystem", (world) => {
404
- const movers = cacheQuery(world, Position, Velocity);
521
+ const movers = cacheQuery(world, [Position, Velocity]);
405
522
 
406
523
  return () => {
407
524
  queryEntities(world, movers, (entity) => {
@@ -433,6 +550,50 @@ queryEntities(world, [Position, Velocity, not(Frozen), not(Disabled)], (entity)
433
550
 
434
551
  Queries match archetypes where all required components are present and no excluded components exist. Matched archetypes are cached and auto-update when archetypes are created or destroyed.
435
552
 
553
+ #### Column Iteration
554
+
555
+ For performance-critical systems, `queryColumns()` provides direct access to the underlying TypedArray columns instead of iterating entity-by-entity. The callback receives the entity array and a columns tuple for each matching archetype. Each element in the tuple corresponds to a data-bearing component in query term order:
556
+
557
+ ```typescript
558
+ import { queryColumns, cacheQuery, not } from "iris-ecs";
559
+
560
+ queryColumns(world, [Player, Position, Velocity, not(Dead)], (entities, [pos, vel]) => {
561
+ // pos.value and vel.value are raw TypedArrays (e.g. Float32Array)
562
+ // entities.length tells you how many entities are in this archetype
563
+ for (let i = 0; i < entities.length; i++) {
564
+ const offset = i * 2; // vec2 stride
565
+ pos.value[offset] += vel.value[offset];
566
+ pos.value[offset + 1] += vel.value[offset + 1];
567
+ }
568
+ });
569
+ ```
570
+
571
+ Tags and data-less pairs are omitted from the columns tuple -- only data-bearing components and relation pairs appear as elements. The element order matches the order of data-bearing terms in the query.
572
+
573
+ Pre-cached queries work the same way:
574
+
575
+ ```typescript
576
+ const movementSystem = defineSystem("movementSystem", (world) => {
577
+ const movers = cacheQuery(world, [Position, Enemy, Velocity, not(Frozen)]);
578
+
579
+ return () => {
580
+ queryColumns(world, movers, (entities, [pos, vel]) => {
581
+ for (let i = 0; i < entities.length; i++) {
582
+ const offset = i * 2;
583
+ pos.value[offset] += vel.value[offset];
584
+ pos.value[offset + 1] += vel.value[offset + 1];
585
+ }
586
+ });
587
+ };
588
+ });
589
+ ```
590
+
591
+ Return `false` from the callback to stop iteration early (same as `queryEntities`).
592
+
593
+ `queryColumns` does not support `added()` or `changed()` modifiers -- use `queryEntities` for change detection.
594
+
595
+ 💡 **Tip:** Use `queryColumns` when you need to process large numbers of entities with tight loops over TypedArray data. Use `queryEntities` for entity-level logic, change detection, or when you need per-entity API calls like `getComponentValue`.
596
+
436
597
  ### Systems
437
598
 
438
599
  A **System** is a function that operates on the world. Systems query entities, read and write components, emit events, and implement game logic.
@@ -449,24 +610,24 @@ import {
449
610
  run,
450
611
  stop,
451
612
  queryEntities,
452
- getComponentValue,
453
- setComponentValue,
613
+ getComponentVectorView,
454
614
  getResourceValue,
455
615
  } from "iris-ecs";
456
616
 
457
617
  const movementSystem = defineSystem("movementSystem", (world) => {
458
618
  // Init: cache queries and action getters once
459
- const movers = cacheQuery(world, Position, Velocity);
619
+ const movers = cacheQuery(world, [Position, Velocity]);
460
620
 
461
621
  return () => {
462
622
  // Tick: runs every frame
463
623
  const dt = getResourceValue(world, Time, "delta") ?? 0;
464
624
 
465
625
  queryEntities(world, movers, (e) => {
466
- const x = getComponentValue(world, e, Position, "x");
467
- const vx = getComponentValue(world, e, Velocity, "x");
626
+ const pos = getComponentVectorView(world, e, Position, "value");
627
+ const vel = getComponentVectorView(world, e, Velocity, "value");
468
628
 
469
- setComponentValue(world, e, Position, "x", x + vx * dt);
629
+ pos[0] += vel[0] * dt;
630
+ pos[1] += vel[1] * dt;
470
631
  });
471
632
  };
472
633
  });
@@ -484,8 +645,8 @@ For simple systems that don't need one-time setup, plain functions also work, an
484
645
 
485
646
  ```typescript
486
647
  function debugSystem(world) {
487
- queryEntities(world, [Position], (e) => {
488
- console.log(getComponentValue(world, e, Position, "x"));
648
+ queryEntities(world, [Health], (e) => {
649
+ console.log(getComponentValue(world, e, Health, "current"));
489
650
  });
490
651
  }
491
652
 
@@ -517,6 +678,34 @@ Without constraints, systems run in registration order. Use arrays for multiple
517
678
 
518
679
  `defineSystem` factory can be registered multiple times with different names via the `name` option: `addSystem(world, movementSystem, { name: "lateMovement" })`.
519
680
 
681
+ #### System Sets
682
+
683
+ **System sets** are named groups for ordering entire groups of systems relative to each other. Instead of wiring individual `before`/`after` between every physics and render system, declare the group-level constraint once:
684
+
685
+ ```typescript
686
+ import { defineSystemSet, addSystemSet, addSystem } from "iris-ecs";
687
+
688
+ const PhysicsSystems = defineSystemSet("PhysicsSystems");
689
+ const RenderSystems = defineSystemSet("RenderSystems");
690
+
691
+ addSystemSet(world, PhysicsSystems, { before: RenderSystems });
692
+ addSystemSet(world, RenderSystems);
693
+
694
+ addSystem(world, applyGravity, { set: PhysicsSystems });
695
+ addSystem(world, detectCollisions, { set: PhysicsSystems, after: applyGravity });
696
+ addSystem(world, drawSprites, { set: RenderSystems });
697
+ addSystem(world, drawParticles, { set: RenderSystems });
698
+ // All physics systems run before all render systems
699
+ ```
700
+
701
+ Systems within a set still respect their own `before`/`after` constraints. A system can also order itself relative to a set without joining it:
702
+
703
+ ```typescript
704
+ addSystem(world, debugOverlay, { after: PhysicsSystems, before: RenderSystems });
705
+ ```
706
+
707
+ A system uses either `schedule` or `set`, not both -- the set inherits its schedule from `addSystemSet`.
708
+
520
709
  #### Schedules
521
710
 
522
711
  Systems are grouped into **schedules** -- named execution phases. The default pipeline runs these schedules every frame:
@@ -600,20 +789,20 @@ addSystem(world, loadAssetsSystem, { schedule: Startup });
600
789
  **Actions** bundle reusable operations with a world captured in closure. Define actions once, then call them without repeatedly passing the world.
601
790
 
602
791
  ```typescript
603
- import { defineActions, createEntity, addComponent } from "iris-ecs";
792
+ import { defineActions, createEntity } from "iris-ecs";
604
793
 
605
794
  const spawnActions = defineActions((world) => ({
606
795
  player(x: number, y: number) {
607
- const entity = createEntity(world);
608
- addComponent(world, entity, Position, { x, y });
609
- addComponent(world, entity, Player);
610
- return entity;
796
+ return createEntity(world, [
797
+ [Position, { value: [x, y] }],
798
+ Player,
799
+ ]);
611
800
  },
612
801
  enemy(x: number, y: number) {
613
- const entity = createEntity(world);
614
- addComponent(world, entity, Position, { x, y });
615
- addComponent(world, entity, Enemy);
616
- return entity;
802
+ return createEntity(world, [
803
+ [Position, { value: [x, y] }],
804
+ Enemy,
805
+ ]);
617
806
  },
618
807
  }));
619
808
 
@@ -729,7 +918,7 @@ import {
729
918
  } from "iris-ecs";
730
919
 
731
920
  const physicsSetupSystem = defineSystem("physicsSetupSystem", (world) => {
732
- const newBodies = cacheQuery(world, added(Position));
921
+ const newBodies = cacheQuery(world, [added(Position)]);
733
922
 
734
923
  return () => {
735
924
  // Entities where Position was added since this system's last run
@@ -740,7 +929,7 @@ const physicsSetupSystem = defineSystem("physicsSetupSystem", (world) => {
740
929
  });
741
930
 
742
931
  const healthBarSystem = defineSystem("healthBarSystem", (world) => {
743
- const damaged = cacheQuery(world, changed(Health));
932
+ const damaged = cacheQuery(world, [changed(Health)]);
744
933
 
745
934
  return () => {
746
935
  // Entities where Health was modified (added OR value changed)
@@ -752,7 +941,7 @@ const healthBarSystem = defineSystem("healthBarSystem", (world) => {
752
941
 
753
942
  const minimapSystem = defineSystem("minimapSystem", (world) => {
754
943
  // Combine change detection with regular filters
755
- const movedPlayers = cacheQuery(world, Player, changed(Position), not(Dead));
944
+ const movedPlayers = cacheQuery(world, [Player, changed(Position), not(Dead)]);
756
945
 
757
946
  return () => {
758
947
  queryEntities(world, movedPlayers, (e) => {
@@ -1,5 +1,5 @@
1
1
  import type { EntityId } from "./encoding.js";
2
- import type { SchemaRecord } from "./schema.js";
2
+ import type { Schema, SchemaRecord, TypedArrayInstance } from "./schema.js";
3
3
  import type { World } from "./world.js";
4
4
  /**
5
5
  * Column storage type.
@@ -15,6 +15,16 @@ export type Column = Int8Array | Int16Array | Int32Array | Uint32Array | Float32
15
15
  export type FieldColumns = {
16
16
  [fieldName: string]: Column;
17
17
  };
18
+ /**
19
+ * Map a schema record to its column types.
20
+ *
21
+ * Each field maps to the runtime array type used for storage.
22
+ *
23
+ * @internal
24
+ */
25
+ export type FieldColumnsOf<S extends SchemaRecord> = {
26
+ [K in keyof S]: S[K] extends Schema<number> ? TypedArrayInstance : S[K] extends Schema<number[]> ? TypedArrayInstance : S[K] extends Schema<boolean> ? boolean[] : S[K] extends Schema<string> ? string[] : unknown[];
27
+ };
18
28
  /**
19
29
  * Component tick storage for change detection.
20
30
  *
@@ -40,6 +50,13 @@ export type Archetype = {
40
50
  capacity: number;
41
51
  ticks: Map<EntityId, ComponentTicks>;
42
52
  };
53
+ /**
54
+ * Derives the stride of a column from its length and the archetype capacity.
55
+ * Scalar columns return 1, vector columns return their stride (e.g., 2 for vec2).
56
+ *
57
+ * @internal
58
+ */
59
+ export declare function getColumnStride(column: Column, capacity: number): number;
43
60
  /**
44
61
  * Hashes a sorted array of type IDs into a unique archetype key.
45
62
  *
@@ -1 +1 @@
1
- {"version":3,"file":"archetype.d.ts","sourceRoot":"","sources":["../src/archetype.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"archetype.d.ts","sourceRoot":"","sources":["../src/archetype.ts"],"names":[],"mappings":"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"}