archetype-ecs 1.4.0 → 1.4.2

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 CHANGED
@@ -130,7 +130,7 @@ Two ways to work with entities in bulk. Pick the right one for the job:
130
130
  Iterates over matching archetypes. You get the backing TypedArrays directly.
131
131
 
132
132
  ```ts
133
- function movementSystem(dt) {
133
+ function movementSystem(dt: number) {
134
134
  em.forEach([Position, Velocity], (arch) => {
135
135
  const px = arch.field(Position.x) // Float32Array
136
136
  const py = arch.field(Position.y)
@@ -183,7 +183,7 @@ const total = em.count([Position])
183
183
  Class-based systems with decorators for component lifecycle hooks:
184
184
 
185
185
  ```ts
186
- import { System, OnAdded, OnRemoved, createSystems } from 'archetype-ecs'
186
+ import { System, OnAdded, OnRemoved, createSystems, type EntityId } from 'archetype-ecs'
187
187
 
188
188
  class MovementSystem extends System {
189
189
  tick() {
@@ -202,12 +202,12 @@ class MovementSystem extends System {
202
202
 
203
203
  class DeathSystem extends System {
204
204
  @OnAdded(Health)
205
- onSpawn(id) {
205
+ onSpawn(id: EntityId) {
206
206
  console.log(`Entity ${id} spawned with ${this.em.get(id, Health.hp)} HP`)
207
207
  }
208
208
 
209
209
  @OnRemoved(Health)
210
- onDeath(id) {
210
+ onDeath(id: EntityId) {
211
211
  this.em.addComponent(id, Dead)
212
212
  }
213
213
  }
@@ -228,8 +228,8 @@ A functional API is also available:
228
228
  import { createSystem } from 'archetype-ecs'
229
229
 
230
230
  const deathSystem = createSystem(em, (sys) => {
231
- sys.onAdded(Health, (id) => console.log(`${id} spawned`))
232
- sys.onRemoved(Health, (id) => console.log(`${id} died`))
231
+ sys.onAdded(Health, (id: EntityId) => console.log(`${id} spawned`))
232
+ sys.onRemoved(Health, (id: EntityId) => console.log(`${id} died`))
233
233
  })
234
234
 
235
235
  em.flushHooks()
@@ -261,32 +261,16 @@ Supports stripping components, skipping entities, and custom serializers.
261
261
  Component types are inferred from their definition. Field names autocomplete, wrong fields are compile errors.
262
262
 
263
263
  ```ts
264
- // Schema is inferred — Position becomes ComponentDef<{ x: number; y: number }>
264
+ // Fields are typed as FieldRef — Position becomes ComponentDef & { x: FieldRef; y: FieldRef }
265
265
  const Position = component('Position', 'f32', ['x', 'y'])
266
266
 
267
- Position.x // autocompletes to .x and .y
268
- Position.z // Property 'z' does not exist
267
+ Position.x // FieldRef — autocompletes to .x and .y
268
+ Position.z // compile error: Property 'z' does not exist
269
269
 
270
- em.get(id, Position.x) // number | undefined
271
- em.set(id, Position.z, 5) // Property 'z' does not exist
270
+ em.get(id, Position.x) // zero-alloc field access
271
+ em.set(id, Position.x, 5) // zero-alloc field write
272
272
 
273
- em.addComponent(id, Position, { x: 1, y: 2 }) // ok
274
- em.addComponent(id, Position, { x: 1 }) // Property 'y' is missing
275
-
276
- em.getComponent(id, Position) // { x: number; y: number } | undefined
277
- ```
278
-
279
- String fields are fully typed too:
280
-
281
- ```ts
282
- const Name = component('Name', 'string', ['name', 'title'])
283
-
284
- em.get(id, Name.name) // string | undefined
285
- em.set(id, Name.name, 'Hero') // ok
286
- em.set(id, Name.name, 42) // number not assignable to string
287
-
288
- em.addComponent(id, Name, { name: 'Hero', title: 'Sir' }) // ok
289
- em.addComponent(id, Name, { foo: 'bar' }) // type error
273
+ arch.field(Position.x) // Float32Array direct TypedArray access
290
274
  ```
291
275
 
292
276
  ---
@@ -1,7 +1,7 @@
1
1
  import type { ComponentDef } from './ComponentRegistry.js';
2
2
  import type { EntityId, EntityManager, ArchetypeView } from './EntityManager.js';
3
- export declare function OnAdded(...types: ComponentDef[]): (_method: Function, context: ClassMethodDecoratorContext) => void;
4
- export declare function OnRemoved(...types: ComponentDef[]): (_method: Function, context: ClassMethodDecoratorContext) => void;
3
+ export declare function OnAdded(...types: ComponentDef[]): (method: (id: EntityId) => void, _context: ClassMethodDecoratorContext) => void;
4
+ export declare function OnRemoved(...types: ComponentDef[]): (method: (id: EntityId) => void, _context: ClassMethodDecoratorContext) => void;
5
5
  interface Hook {
6
6
  buffer: Set<EntityId>;
7
7
  handler: (id: EntityId) => void;
@@ -1,17 +1,17 @@
1
1
  // ── Decorators (TC39 Stage 3) ────────────────────────────
2
2
  export function OnAdded(...types) {
3
- return function (_method, context) {
4
- context.addInitializer(function () {
3
+ return function (method, _context) {
4
+ _context.addInitializer(function () {
5
5
  const self = this;
6
- self._registerHook('add', types, self[context.name].bind(self));
6
+ self._registerHook('add', types, method.bind(self));
7
7
  });
8
8
  };
9
9
  }
10
10
  export function OnRemoved(...types) {
11
- return function (_method, context) {
12
- context.addInitializer(function () {
11
+ return function (method, _context) {
12
+ _context.addInitializer(function () {
13
13
  const self = this;
14
- self._registerHook('remove', types, self[context.name].bind(self));
14
+ self._registerHook('remove', types, method.bind(self));
15
15
  });
16
16
  };
17
17
  }
@@ -121,7 +121,7 @@ export function createSystem(em, constructor) {
121
121
  }
122
122
  export function createSystems(em, entries) {
123
123
  const systems = entries.map(Entry => {
124
- if (Entry.prototype instanceof System) {
124
+ if ('prototype' in Entry && Entry.prototype instanceof System) {
125
125
  return new Entry(em);
126
126
  }
127
127
  const sys = createSystem(em, Entry);
@@ -7,4 +7,11 @@ export type { Profiler, ProfilerEntry } from './Profiler.js';
7
7
  export { TYPED, componentSchemas, parseTypeSpec } from './ComponentRegistry.js';
8
8
  export type { ComponentDef, TypeSpec } from './ComponentRegistry.js';
9
9
  import { type ComponentDef } from './ComponentRegistry.js';
10
- export declare function component(name: string, typeOrSchema?: string | Record<string, string>, fields?: string[]): ComponentDef;
10
+ import type { FieldRef } from './EntityManager.js';
11
+ export declare function component(name: string): ComponentDef;
12
+ export declare function component<F extends readonly string[]>(name: string, type: string, fields: F): ComponentDef & {
13
+ readonly [K in F[number]]: FieldRef;
14
+ };
15
+ export declare function component<S extends Record<string, string>>(name: string, schema: S): ComponentDef & {
16
+ readonly [K in keyof S]: FieldRef;
17
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archetype-ecs",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
4
4
  "description": "Lightweight archetype-based Entity Component System",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",
package/src/System.ts CHANGED
@@ -4,19 +4,19 @@ import type { EntityId, EntityManager, ArchetypeView } from './EntityManager.js'
4
4
  // ── Decorators (TC39 Stage 3) ────────────────────────────
5
5
 
6
6
  export function OnAdded(...types: ComponentDef[]) {
7
- return function (_method: Function, context: ClassMethodDecoratorContext) {
8
- context.addInitializer(function () {
7
+ return function (method: (id: EntityId) => void, _context: ClassMethodDecoratorContext) {
8
+ _context.addInitializer(function () {
9
9
  const self = this as unknown as System;
10
- self._registerHook('add', types, (self as any)[context.name].bind(self));
10
+ self._registerHook('add', types, method.bind(self));
11
11
  });
12
12
  };
13
13
  }
14
14
 
15
15
  export function OnRemoved(...types: ComponentDef[]) {
16
- return function (_method: Function, context: ClassMethodDecoratorContext) {
17
- context.addInitializer(function () {
16
+ return function (method: (id: EntityId) => void, _context: ClassMethodDecoratorContext) {
17
+ _context.addInitializer(function () {
18
18
  const self = this as unknown as System;
19
- self._registerHook('remove', types, (self as any)[context.name].bind(self));
19
+ self._registerHook('remove', types, method.bind(self));
20
20
  });
21
21
  };
22
22
  }
@@ -182,7 +182,7 @@ export interface Pipeline {
182
182
 
183
183
  export function createSystems(em: EntityManager, entries: (FunctionalSystemConstructor | (new (em: EntityManager) => System))[]): Pipeline {
184
184
  const systems: Runnable[] = entries.map(Entry => {
185
- if ((Entry as any).prototype instanceof System) {
185
+ if ('prototype' in Entry && Entry.prototype instanceof System) {
186
186
  return new (Entry as new (em: EntityManager) => System)(em);
187
187
  }
188
188
  const sys = createSystem(em, Entry as FunctionalSystemConstructor);
package/src/index.ts CHANGED
@@ -8,7 +8,11 @@ export { TYPED, componentSchemas, parseTypeSpec } from './ComponentRegistry.js';
8
8
  export type { ComponentDef, TypeSpec } from './ComponentRegistry.js';
9
9
 
10
10
  import { parseTypeSpec, componentSchemas, type ComponentDef, type TypeSpec } from './ComponentRegistry.js';
11
+ import type { FieldRef } from './EntityManager.js';
11
12
 
13
+ export function component(name: string): ComponentDef;
14
+ export function component<F extends readonly string[]>(name: string, type: string, fields: F): ComponentDef & { readonly [K in F[number]]: FieldRef };
15
+ export function component<S extends Record<string, string>>(name: string, schema: S): ComponentDef & { readonly [K in keyof S]: FieldRef };
12
16
  export function component(name: string, typeOrSchema?: string | Record<string, string>, fields?: string[]): ComponentDef {
13
17
  const sym = Symbol(name);
14
18
  const comp: any = { _sym: sym, _name: name };
@@ -300,10 +300,10 @@ describe('Typed Components (SoA)', () => {
300
300
  }
301
301
 
302
302
  em.forEach([Pos, Vel], (arch) => {
303
- const px = arch.field(Pos.x as any);
304
- const py = arch.field(Pos.y as any);
305
- const vx = arch.field(Vel.vx as any);
306
- const vy = arch.field(Vel.vy as any);
303
+ const px = arch.field(Pos.x);
304
+ const py = arch.field(Pos.y);
305
+ const vx = arch.field(Vel.vx);
306
+ const vy = arch.field(Vel.vy);
307
307
  for (let i = 0; i < arch.count; i++) {
308
308
  px[i] += vx[i];
309
309
  py[i] += vy[i];
@@ -367,16 +367,16 @@ describe('Typed Components (SoA)', () => {
367
367
  const Pos = component('PosGS', 'f32', ['x', 'y']);
368
368
  const id = em.createEntity();
369
369
  em.addComponent(id, Pos, { x: 3.5, y: 7.5 });
370
- assert.ok(Math.abs(em.get(id, Pos.x as any) - 3.5) < 0.001);
371
- em.set(id, Pos.x as any, 42);
372
- assert.ok(Math.abs(em.get(id, Pos.x as any) - 42) < 0.001);
370
+ assert.ok(Math.abs(em.get(id, Pos.x) - 3.5) < 0.001);
371
+ em.set(id, Pos.x, 42);
372
+ assert.ok(Math.abs(em.get(id, Pos.x) - 42) < 0.001);
373
373
  });
374
374
 
375
375
  it('get returns undefined for missing entity/component', () => {
376
376
  const Pos = component('PosGFM', 'f32', ['x', 'y']);
377
- assert.equal(em.get(999, Pos.x as any), undefined);
377
+ assert.equal(em.get(999, Pos.x), undefined);
378
378
  const id = em.createEntity();
379
- assert.equal(em.get(id, Pos.x as any), undefined);
379
+ assert.equal(em.get(id, Pos.x), undefined);
380
380
  });
381
381
 
382
382
  it('forEach field returns undefined for tag component', () => {
@@ -387,7 +387,7 @@ describe('Typed Components (SoA)', () => {
387
387
  em.addComponent(id, Tag, {});
388
388
 
389
389
  em.forEach([Pos, Tag], (arch) => {
390
- assert.ok(arch.field(Pos.x as any) instanceof Float32Array);
390
+ assert.ok(arch.field(Pos.x) instanceof Float32Array);
391
391
  });
392
392
  });
393
393
 
@@ -396,11 +396,11 @@ describe('Typed Components (SoA)', () => {
396
396
  const id = em.createEntity();
397
397
  em.addComponent(id, Name, { name: 'Hero', title: 'Sir' });
398
398
 
399
- assert.equal(em.get(id, Name.name as any), 'Hero');
400
- assert.equal(em.get(id, Name.title as any), 'Sir');
399
+ assert.equal(em.get(id, Name.name), 'Hero');
400
+ assert.equal(em.get(id, Name.title), 'Sir');
401
401
 
402
- em.set(id, Name.name as any, 'Villain');
403
- assert.equal(em.get(id, Name.name as any), 'Villain');
402
+ em.set(id, Name.name, 'Villain');
403
+ assert.equal(em.get(id, Name.name), 'Villain');
404
404
 
405
405
  const obj = em.getComponent(id, Name);
406
406
  assert.deepEqual(obj, { name: 'Villain', title: 'Sir' });
@@ -411,8 +411,8 @@ describe('Typed Components (SoA)', () => {
411
411
  const id = em.createEntity();
412
412
  em.addComponent(id, Label, { text: 'hello', color: 'red' });
413
413
 
414
- assert.equal(em.get(id, Label.text as any), 'hello');
415
- assert.equal(em.get(id, Label.color as any), 'red');
414
+ assert.equal(em.get(id, Label.text), 'hello');
415
+ assert.equal(em.get(id, Label.color), 'red');
416
416
  });
417
417
 
418
418
  it('string component growth past capacity', () => {
@@ -423,8 +423,8 @@ describe('Typed Components (SoA)', () => {
423
423
  }
424
424
  const ids = em.query([Name]);
425
425
  assert.equal(ids.length, 100);
426
- assert.equal(em.get(ids[0], Name.value as any), 'entity_0');
427
- assert.equal(em.get(ids[99], Name.value as any), 'entity_99');
426
+ assert.equal(em.get(ids[0], Name.value), 'entity_0');
427
+ assert.equal(em.get(ids[99], Name.value), 'entity_99');
428
428
  });
429
429
 
430
430
  it('string component swap-remove preserves data', () => {
@@ -437,8 +437,8 @@ describe('Typed Components (SoA)', () => {
437
437
  em.addComponent(c, Name, { value: 'ccc' });
438
438
 
439
439
  em.destroyEntity(a);
440
- assert.equal(em.get(b, Name.value as any), 'bbb');
441
- assert.equal(em.get(c, Name.value as any), 'ccc');
440
+ assert.equal(em.get(b, Name.value), 'bbb');
441
+ assert.equal(em.get(c, Name.value), 'ccc');
442
442
  });
443
443
 
444
444
  it('mixed string + numeric fields in one component', () => {
@@ -446,8 +446,8 @@ describe('Typed Components (SoA)', () => {
446
446
  const id = em.createEntity();
447
447
  em.addComponent(id, Item, { name: 'Sword', weight: 3.5 });
448
448
 
449
- assert.equal(em.get(id, Item.name as any), 'Sword');
450
- assert.ok(Math.abs(em.get(id, Item.weight as any) - 3.5) < 0.01);
449
+ assert.equal(em.get(id, Item.name), 'Sword');
450
+ assert.ok(Math.abs(em.get(id, Item.weight) - 3.5) < 0.01);
451
451
  });
452
452
 
453
453
  it('string component forEach field access', () => {
@@ -458,7 +458,7 @@ describe('Typed Components (SoA)', () => {
458
458
  }
459
459
 
460
460
  em.forEach([Name], (arch) => {
461
- const values = arch.field(Name.value as any);
461
+ const values = arch.field(Name.value);
462
462
  assert.ok(Array.isArray(values));
463
463
  assert.equal(values[0], 'e0');
464
464
  assert.equal(values[4], 'e4');
@@ -539,7 +539,7 @@ describe('Deferred Structural Changes during forEach', () => {
539
539
  em.addComponent(a, Pos, { x: 1, y: 2 });
540
540
 
541
541
  em.forEach([Pos], (arch) => {
542
- const px = arch.field(Pos.x as any);
542
+ const px = arch.field(Pos.x);
543
543
  // Overwrite via addComponent — same archetype, should be immediate
544
544
  em.addComponent(a, Pos, { x: 99, y: 88 });
545
545
  // The array should reflect the change immediately
@@ -615,9 +615,9 @@ describe('Deferred Structural Changes during forEach', () => {
615
615
  em.addComponent(a, Pos, { x: 1, y: 2 });
616
616
 
617
617
  em.forEach([Pos], (arch) => {
618
- em.set(a, Pos.x as any, 42);
618
+ em.set(a, Pos.x, 42);
619
619
  // Should be immediately visible
620
- const px = arch.field(Pos.x as any);
620
+ const px = arch.field(Pos.x);
621
621
  assert.ok(Math.abs(px[0] - 42) < 0.001);
622
622
  });
623
623
  });
package/tests/types.ts CHANGED
@@ -33,14 +33,14 @@ em.addComponent(id, Name, { name: 'Hero', title: 'Sir' });
33
33
  const pos = em.getComponent(id, Position);
34
34
 
35
35
  // get/set: field descriptor access
36
- const px: any = em.get(id, Position.x as any);
37
- em.set(id, Position.x as any, 10);
36
+ const px: any = em.get(id, Position.x);
37
+ em.set(id, Position.x, 10);
38
38
 
39
39
  // forEach + field: accepts FieldRef
40
40
  em.forEach([Position, Velocity], (arch) => {
41
41
  const count: number = arch.count;
42
42
  const ids: EntityId[] = arch.entityIds;
43
- const arrX = arch.field(Position.x as any);
43
+ const arrX = arch.field(Position.x);
44
44
  });
45
45
 
46
46
  // createEntityWith: alternating type, data