@vworlds/vecs 1.0.10 → 1.0.11

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 (43) hide show
  1. package/README.md +218 -229
  2. package/dist/command.d.ts +1 -46
  3. package/dist/component.d.ts +51 -59
  4. package/dist/component.js +31 -25
  5. package/dist/component.js.map +1 -1
  6. package/dist/dsl.d.ts +34 -26
  7. package/dist/dsl.js +46 -20
  8. package/dist/dsl.js.map +1 -1
  9. package/dist/entity.d.ts +96 -106
  10. package/dist/entity.js +261 -190
  11. package/dist/entity.js.map +1 -1
  12. package/dist/filter.d.ts +31 -23
  13. package/dist/filter.js +24 -17
  14. package/dist/filter.js.map +1 -1
  15. package/dist/index.d.ts +1 -1
  16. package/dist/package.json +3 -1
  17. package/dist/phase.d.ts +5 -28
  18. package/dist/phase.js +11 -10
  19. package/dist/phase.js.map +1 -1
  20. package/dist/query.d.ts +107 -144
  21. package/dist/query.js +200 -169
  22. package/dist/query.js.map +1 -1
  23. package/dist/system.d.ts +59 -87
  24. package/dist/system.js +114 -114
  25. package/dist/system.js.map +1 -1
  26. package/dist/util/array_map.d.ts +4 -55
  27. package/dist/util/array_map.js +35 -37
  28. package/dist/util/array_map.js.map +1 -1
  29. package/dist/util/bitset.d.ts +40 -50
  30. package/dist/util/bitset.js +76 -62
  31. package/dist/util/bitset.js.map +1 -1
  32. package/dist/util/events.d.ts +14 -18
  33. package/dist/util/events.js +24 -3
  34. package/dist/util/events.js.map +1 -1
  35. package/dist/util/ordered_set.d.ts +1 -17
  36. package/dist/util/ordered_set.js +74 -25
  37. package/dist/util/ordered_set.js.map +1 -1
  38. package/dist/world.d.ts +212 -224
  39. package/dist/world.js +368 -330
  40. package/dist/world.js.map +1 -1
  41. package/eslint-rules/internal-underscore.js +60 -0
  42. package/eslint.config.js +5 -0
  43. package/package.json +3 -1
package/dist/world.js CHANGED
@@ -5,20 +5,26 @@ import { System } from "./system.js";
5
5
  import { Filter } from "./filter.js";
6
6
  import { ArrayMap } from "./util/array_map.js";
7
7
  import { Phase } from "./phase.js";
8
+ /**
9
+ * Numeric type ids below this value are reserved for components whose id was
10
+ * pre-registered via {@link World.registerComponentType} (typically server
11
+ * assigned). Auto-assigned ids start here.
12
+ */
8
13
  const LOCAL_COMPONENT_MIN = 256;
9
14
  /**
10
- * The central ECS container.
15
+ * The central ECS container. One world per game session.
11
16
  *
12
- * A `World` owns all entities, components, systems, queries, and the update
13
- * pipeline. Typical lifecycle:
17
+ * A `World` owns every entity, every registered component class, every
18
+ * registered query / system, and the update pipeline. The typical lifecycle:
14
19
  *
15
- * 1. **Register components** — call {@link registerComponent} (and optionally
16
- * {@link registerComponentType}) for every component class.
17
- * 2. **Register systems and queries** — call {@link system} and {@link query}
18
- * to create and configure them.
20
+ * 1. **Register components** — {@link registerComponent} (and optionally
21
+ * {@link registerComponentType}) for every component class you plan to use.
22
+ * 2. **Build the pipeline** — {@link addPhase} for every named phase, then
23
+ * {@link system} / {@link query} for each processor.
19
24
  * 3. **Start** — call {@link start} to freeze component registration and
20
25
  * distribute systems into their phases.
21
- * 4. **Run loop** — call {@link runPhase} once per frame for each phase.
26
+ * 4. **Run loop** — call {@link runPhase} per phase or {@link progress} for
27
+ * every phase, once per frame.
22
28
  *
23
29
  * ```ts
24
30
  * const world = new World();
@@ -28,161 +34,190 @@ const LOCAL_COMPONENT_MIN = 256;
28
34
  *
29
35
  * world.system("Move")
30
36
  * .requires(Position, Velocity)
31
- * .update(Position, (pos) => { pos.x += vel.x; });
37
+ * .each([Position, Velocity], (e, [pos, vel]) => {
38
+ * pos.x += vel.vx;
39
+ * });
32
40
  *
33
41
  * world.start();
34
42
  *
35
43
  * // game loop:
36
- * world.runPhase(updatePhase, Date.now(), 16);
44
+ * world.progress(now, delta);
37
45
  * ```
46
+ *
47
+ * ## Deferred mode
48
+ *
49
+ * The world can be in **deferred mode**, in which case entity mutations
50
+ * (`add` / `set` / `remove` / `destroy` / `setParent` / `modified`) are
51
+ * queued instead of applied inline. Systems run inside an automatically
52
+ * deferred scope; user code can wrap arbitrary blocks with
53
+ * {@link beginDefer} / {@link endDefer} or {@link defer}. {@link flush}
54
+ * drains the queue at top level.
38
55
  */
39
56
  export class World {
40
- /** `true` when the world is in deferred mode — mutations are queued rather than applied immediately. */
41
- get deferred() {
42
- return this.deferredDepth > 0 || this.draining;
43
- }
44
57
  constructor() {
45
- this._entities = new Map(); // maps entity Id to Entity
46
- this.componentNameTypeMap = new Map();
58
+ /** @internal Entity id entity. Owns every live entity. */
59
+ this._entities = new Map();
60
+ /** @internal All registered queries, including systems (which extend `Query`). */
47
61
  this._queries = [];
48
- this.Class2Meta = new Map();
49
- this.Type2Meta = new ArrayMap();
50
- this.localComponentCounter = LOCAL_COMPONENT_MIN;
51
- this.componentRegistrationDisabled = false;
62
+ /** @internal Component class → meta record. */
63
+ this._Class2Meta = new Map();
64
+ /** @internal Component type id → meta record. */
65
+ this._Type2Meta = new ArrayMap();
66
+ /** @internal Pre-registered name → type id mappings (server-assigned ids). */
67
+ this._componentNameTypeMap = new Map();
68
+ /** @internal Counter used to auto-assign type ids for "local" components (≥ 256). */
69
+ this._localComponentCounter = LOCAL_COMPONENT_MIN;
70
+ /** @internal `true` once {@link start} (or {@link disableComponentRegistration}) has been called. */
71
+ this._componentRegistrationDisabled = false;
72
+ /** @internal Auto-incrementing entity id counter, seeded by {@link setEntityIdRange}. */
73
+ this._eidCounter = 0;
52
74
  /** @internal Single ordered command queue used in deferred mode. */
53
- this.commandQueue = [];
54
- /** @internal Nested `beginDefer` / `endDefer` count. */
55
- this.deferredDepth = 0;
56
- /** @internal True while `processCommandQueue` is iterating, to avoid re-entrant drains. */
57
- this.draining = false;
58
- /** @internal */
75
+ this._commandQueue = [];
76
+ /** @internal Nested {@link beginDefer} / {@link endDefer} count. */
77
+ this._deferredDepth = 0;
78
+ /** @internal `true` while {@link _processCommandQueue} is iterating, to avoid re-entrant drains. */
79
+ this._draining = false;
80
+ /** @internal Phase name → phase. Insertion-ordered, matches pipeline execution order. */
59
81
  this._pipeline = new Map();
60
- this.eidCounter = 0;
61
- }
62
- /** @readonly */
63
- get entities() {
64
- return this._entities;
65
- }
66
- /** @readonly */
67
- get queries() {
68
- return this._queries;
69
82
  }
70
83
  /**
71
- * Return the entity with id `eid`, creating it if it does not yet exist.
72
- *
73
- * Used by networking code to materialise server-assigned entities:
74
- *
75
- * ```ts
76
- * const e = world.getOrCreateEntity(snapshot.eid, (e) => {
77
- * networkEntities.add(e);
78
- * });
79
- * e.add(snapshot.type, false);
80
- * ```
81
- *
82
- * @param eid - The entity id to look up or create.
83
- * @param onCreateCallback - Optional callback invoked only when a **new**
84
- * entity is created, before it is returned. Use this to initialise
85
- * bookkeeping (e.g. tracking it in a local set).
86
- * @returns The existing or newly created entity.
84
+ * @internal Drain the top-level command queue: walk it in arrival order,
85
+ * executing each command. Callbacks may push more commands; they are picked
86
+ * up by index iteration in the same pass.
87
87
  */
88
- getOrCreateEntity(eid, onCreateCallback) {
89
- let e = this._entities.get(eid);
90
- if (!e) {
91
- e = new Entity(this, eid);
92
- this._entities.set(eid, e);
93
- if (onCreateCallback) {
94
- onCreateCallback(e);
95
- }
88
+ _processCommandQueue() {
89
+ if (this._draining) {
90
+ return;
96
91
  }
97
- return e;
98
- }
99
- entity(id) {
100
- if (id === undefined) {
101
- const eid = this.eidCounter++;
102
- const e = new Entity(this, eid);
103
- if (this.deferred) {
104
- this._enqueue({ kind: 0 /* CommandKind.CreateEntity */, entity: e });
105
- }
106
- else {
107
- this._entities.set(eid, e);
92
+ if (this._commandQueue.length === 0) {
93
+ return;
94
+ }
95
+ this._draining = true;
96
+ try {
97
+ for (let i = 0; i < this._commandQueue.length; i++) {
98
+ this._executeCommand(this._commandQueue[i]);
108
99
  }
109
- return e;
100
+ this._commandQueue.length = 0;
101
+ }
102
+ finally {
103
+ this._draining = false;
110
104
  }
111
- return this._entities.get(id);
112
105
  }
113
106
  /**
114
- * Set the starting value for the auto-incrementing entity id counter.
115
- *
116
- * Must be called **before** {@link start} (or
117
- * {@link disableComponentRegistration}). Useful when the world runs alongside
118
- * a server that owns a different id range — for example, locally-created
119
- * client entities can start at a high offset to avoid collisions with
120
- * server-assigned ids.
121
- *
122
- * @param min - The first id that will be assigned by {@link entity}.
123
- * @throws If called after registration has been disabled.
107
+ * @internal Run one command's side effects: data-layer mutation, hook
108
+ * firing, and routing to every registered query / system.
124
109
  */
125
- setEntityIdRange(min) {
126
- if (this.componentRegistrationDisabled) {
127
- throw "setEntityIdRange must be called before component registration is disabled";
110
+ _executeCommand(cmd) {
111
+ switch (cmd.kind) {
112
+ case 0 /* CommandKind.CreateEntity */:
113
+ this._entities.set(cmd.entity.eid, cmd.entity);
114
+ return;
115
+ case 1 /* CommandKind.Set */:
116
+ cmd.entity._set(cmd.type, cmd.props);
117
+ return;
118
+ case 2 /* CommandKind.Modified */:
119
+ cmd.entity._modified(cmd.type);
120
+ return;
121
+ case 3 /* CommandKind.Remove */:
122
+ cmd.entity._remove(cmd.type);
123
+ return;
124
+ case 4 /* CommandKind.Destroy */:
125
+ cmd.entity._destroy();
126
+ return;
127
+ case 5 /* CommandKind.SetParent */:
128
+ cmd.entity._setParent(cmd.parent);
129
+ return;
128
130
  }
129
- this.eidCounter = min;
130
131
  }
131
132
  /**
132
- * Retrieve the {@link ComponentMeta} record for a registered component.
133
- *
134
- * @param typeOrClass - A component class constructor or a numeric type id.
135
- * @returns The corresponding `ComponentMeta`.
136
- * @throws If no component with that class or type id has been registered.
133
+ * @internal Distribute every registered system into its phase's `systems`
134
+ * list. Called by {@link start}; idempotent so it can be re-run if the
135
+ * pipeline is rebuilt.
137
136
  */
138
- getComponentMeta(typeOrClass) {
139
- let meta;
140
- if (typeof typeOrClass === "function") {
141
- meta = this.Class2Meta.get(typeOrClass);
142
- }
143
- else {
144
- meta = this.Type2Meta.get(typeOrClass);
145
- }
146
- if (!meta) {
147
- throw `unregistered component meta for component type or class '${typeOrClass}'`;
137
+ _reindexSystems() {
138
+ let _defaultPhase = this._pipeline.get("update");
139
+ if (!_defaultPhase) {
140
+ _defaultPhase = new Phase("update", this);
141
+ this._pipeline.set(_defaultPhase.name, _defaultPhase);
148
142
  }
149
- return meta;
143
+ const defaultPhase = _defaultPhase;
144
+ this._queries.forEach((q) => {
145
+ if (!(q instanceof System)) {
146
+ return;
147
+ }
148
+ let phase = q._phase;
149
+ if (typeof phase === "string") {
150
+ phase = this._pipeline.get(phase);
151
+ }
152
+ phase = phase || defaultPhase;
153
+ phase.systems.push(q);
154
+ });
155
+ this._pipeline.forEach((phase) => {
156
+ console.log("Phase %s : %s", phase.name, phase.systems.map((s) => s.name).join(" -> "));
157
+ });
158
+ }
159
+ /** @internal Append a command to the deferred-mode queue. */
160
+ _enqueue(cmd) {
161
+ this._commandQueue.push(cmd);
162
+ }
163
+ /** @internal Register a freshly created {@link Query} (called from its constructor). */
164
+ _addQuery(q) {
165
+ this._queries.push(q);
150
166
  }
151
167
  /**
152
- * Resolve a component class or type id to its numeric type id.
153
- *
154
- * @param typeOrClass - A component class constructor or a numeric type id.
155
- * @returns The numeric type id.
168
+ * @internal Unregister a query and purge its membership from every entity.
169
+ * Called by {@link Query.destroy}.
156
170
  */
157
- getComponentType(typeOrClass) {
158
- if (typeof typeOrClass === "function") {
159
- return this.getComponentMeta(typeOrClass).type;
171
+ _removeQuery(q) {
172
+ const idx = this._queries.indexOf(q);
173
+ if (idx !== -1) {
174
+ this._queries.splice(idx, 1);
160
175
  }
161
- return typeOrClass;
176
+ this._entities.forEach((e) => e._purgeQuery(q));
177
+ }
178
+ /** @internal Remove an entity from the world's entity map (called by `Entity._destroy`). */
179
+ _unregisterEntity(entity) {
180
+ this._entities.delete(entity.eid);
181
+ }
182
+ /** Read-only view of the live entities, keyed by entity id. */
183
+ get entities() {
184
+ return this._entities;
185
+ }
186
+ /** Read-only view of every registered query (includes systems). */
187
+ get queries() {
188
+ return this._queries;
189
+ }
190
+ /**
191
+ * `true` while the world is in deferred mode — entity mutations are queued
192
+ * rather than applied inline. Equivalent to "the queue depth is non-zero or
193
+ * the world is currently draining".
194
+ */
195
+ get deferred() {
196
+ return this._deferredDepth > 0 || this._draining;
162
197
  }
163
198
  /**
164
199
  * Enter deferred mode. Mutations made until the matching {@link endDefer}
165
200
  * are queued instead of executing inline.
166
201
  *
167
- * Nested begin/end pairs are allowed; only the outermost `endDefer`
168
- * triggers a drain.
169
- *
202
+ * Nested `beginDefer` / `endDefer` pairs are allowed; only the outermost
203
+ * `endDefer` triggers a queue drain.
170
204
  */
171
205
  beginDefer() {
172
- this.deferredDepth++;
206
+ this._deferredDepth++;
173
207
  }
174
208
  /**
175
- * Leave deferred mode. When the depth returns to zero, the world processes
176
- * the command queue (firing hooks and routing enter / exit / update events).
177
- *
209
+ * Leave deferred mode. When the depth returns to zero the world drains the
210
+ * command queue (firing hooks and routing enter / exit / update events).
178
211
  */
179
212
  endDefer() {
180
- this.deferredDepth--;
213
+ this._deferredDepth--;
181
214
  this.flush();
182
215
  }
183
216
  /**
217
+ * Run `fn` inside a deferred scope. Equivalent to
218
+ * `beginDefer(); try { fn(); } finally { endDefer(); }`.
184
219
  *
185
- * @param fn callback to invoke in deferred mode.
220
+ * @param fn - Callback executed in deferred mode.
186
221
  */
187
222
  defer(fn) {
188
223
  this.beginDefer();
@@ -194,80 +229,36 @@ export class World {
194
229
  }
195
230
  }
196
231
  /**
197
- * Drain any pending commands queued at the top level (depth 0).
232
+ * Drain any commands queued at the top level (depth 0).
198
233
  *
199
- * Useful between phases or after batch-loading network snapshots, to make
200
- * accumulated mutations visible (fire hooks, route enter/exit/update) before
201
- * the next read or system run.
234
+ * Call between phases or after batch-loading network snapshots to surface
235
+ * accumulated mutations (firing hooks and routing enter / exit / update)
236
+ * before the next read or system run.
202
237
  */
203
238
  flush() {
204
- if (this.deferredDepth === 0) {
205
- this.processCommandQueue();
239
+ if (this._deferredDepth === 0) {
240
+ this._processCommandQueue();
206
241
  }
207
242
  }
208
- /** @internal Append a command to the queue. */
209
- _enqueue(cmd) {
210
- this.commandQueue.push(cmd);
211
- }
212
243
  /**
213
- * @internal Walk the command queue in insertion order, executing each
214
- * command. Callbacks may push more commands, which are processed in the
215
- * same pass via index iteration.
216
- */
217
- processCommandQueue() {
218
- if (this.draining) {
219
- return;
220
- }
221
- if (this.commandQueue.length === 0) {
222
- return;
223
- }
224
- this.draining = true;
225
- try {
226
- for (let i = 0; i < this.commandQueue.length; i++) {
227
- this.executeCommand(this.commandQueue[i]);
228
- }
229
- this.commandQueue.length = 0;
230
- }
231
- finally {
232
- this.draining = false;
233
- }
234
- }
235
- /**
236
- * @internal Run a single command's side effects: data-layer mutation, hook
237
- * firing, and routing to all registered queries / systems.
244
+ * Pre-register a `componentName typeId` mapping without binding a class.
245
+ *
246
+ * Useful when network messages refer to components by type id and the
247
+ * corresponding class may be registered later. Call this **before**
248
+ * {@link registerComponent} so the class picks up the server-assigned id
249
+ * rather than a locally generated one.
250
+ *
251
+ * @param componentName - String name used in network payloads.
252
+ * @param type - Numeric type id assigned by the server.
238
253
  */
239
- executeCommand(cmd) {
240
- switch (cmd.kind) {
241
- case 0 /* CommandKind.CreateEntity */:
242
- this._entities.set(cmd.entity.eid, cmd.entity);
243
- return;
244
- case 1 /* CommandKind.Set */:
245
- cmd.entity._set(cmd.type, cmd.props);
246
- return;
247
- case 2 /* CommandKind.Modified */:
248
- cmd.entity._modified(cmd.type);
249
- return;
250
- case 3 /* CommandKind.Remove */:
251
- cmd.entity._remove(cmd.type);
252
- return;
253
- case 4 /* CommandKind.Destroy */:
254
- cmd.entity._destroy();
255
- return;
256
- case 5 /* CommandKind.SetParent */:
257
- cmd.entity._setParent(cmd.parent);
258
- return;
259
- }
260
- }
261
- /** @internal Remove an entity from the world's entity map. Called by Entity._destroy. */
262
- _unregisterEntity(entity) {
263
- this._entities.delete(entity.eid);
254
+ registerComponentType(componentName, type) {
255
+ this._componentNameTypeMap.set(componentName, type);
264
256
  }
265
257
  registerComponent(ComponentClass, typeOrComponentName, componentName) {
266
- if (this.componentRegistrationDisabled) {
258
+ if (this._componentRegistrationDisabled) {
267
259
  throw "World component registartion is disabled";
268
260
  }
269
261
  let type = undefined;
270
- // Determine if the second argument is type or componentName based on its type
271
262
  if (typeof typeOrComponentName === "number") {
272
263
  type = typeOrComponentName;
273
264
  }
@@ -277,167 +268,226 @@ export class World {
277
268
  componentName = componentName || ComponentClass.name;
278
269
  let local = false;
279
270
  if (type === undefined) {
280
- // attempt to get type id from name->type map
281
- type = this.componentNameTypeMap.get(componentName);
271
+ type = this._componentNameTypeMap.get(componentName);
282
272
  if (type === undefined) {
283
- type = this.localComponentCounter++;
273
+ type = this._localComponentCounter++;
284
274
  local = true;
285
275
  }
286
276
  }
287
- let meta = this.Class2Meta.get(ComponentClass);
277
+ let meta = this._Class2Meta.get(ComponentClass);
288
278
  if (meta) {
289
279
  if (local) {
290
- this.localComponentCounter--;
280
+ this._localComponentCounter--;
291
281
  }
292
282
  throw `Trying to register ${componentName} with type=${type} which is already registered to ${meta.componentName}`;
293
283
  }
294
284
  this.registerComponentType(componentName, type);
295
285
  meta = new ComponentMeta(ComponentClass, type, componentName);
296
- this.Class2Meta.set(ComponentClass, meta);
297
- this.Type2Meta.set(type, meta);
286
+ this._Class2Meta.set(ComponentClass, meta);
287
+ this._Type2Meta.set(type, meta);
298
288
  console.log("Registered component %s with type=%d as %s component", componentName, type, local ? "local" : "networked");
299
289
  }
300
290
  /**
301
- * Pre-register a component name type id mapping without associating a
302
- * class.
303
- *
304
- * Useful when network messages refer to components by type id and the
305
- * corresponding class may be registered later. Call this before
306
- * {@link registerComponent} to ensure the class picks up the server-assigned
307
- * id rather than a locally generated one.
291
+ * Look up the {@link ComponentMeta} for a registered component.
308
292
  *
309
- * @param componentName - The string name used in network payloads.
310
- * @param type - The numeric type id assigned by the server.
293
+ * @param typeOrClass - Component class or numeric type id.
294
+ * @returns The corresponding meta record.
295
+ * @throws When no component with that class or type id has been registered.
311
296
  */
312
- registerComponentType(componentName, type) {
313
- this.componentNameTypeMap.set(componentName, type);
314
- }
315
- /** @internal Called by the {@link Query} constructor to register itself. */
316
- _addQuery(q) {
317
- this._queries.push(q);
297
+ getComponentMeta(typeOrClass) {
298
+ let meta;
299
+ if (typeof typeOrClass === "function") {
300
+ meta = this._Class2Meta.get(typeOrClass);
301
+ }
302
+ else {
303
+ meta = this._Type2Meta.get(typeOrClass);
304
+ }
305
+ if (!meta) {
306
+ throw `unregistered component meta for component type or class '${typeOrClass}'`;
307
+ }
308
+ return meta;
318
309
  }
319
- /** @internal Called by {@link Query.destroy} to unregister a query and remove it from all entities. */
320
- _removeQuery(q) {
321
- const idx = this._queries.indexOf(q);
322
- if (idx !== -1) {
323
- this._queries.splice(idx, 1);
310
+ /**
311
+ * Resolve a component class or type id to its numeric type id.
312
+ *
313
+ * @param typeOrClass - Component class or numeric type id.
314
+ * @returns The numeric type id.
315
+ */
316
+ getComponentType(typeOrClass) {
317
+ if (typeof typeOrClass === "function") {
318
+ return this.getComponentMeta(typeOrClass).type;
324
319
  }
325
- this._entities.forEach((e) => e._purgeQuery(q));
320
+ return typeOrClass;
326
321
  }
327
322
  /**
328
- * Create a new {@link System}, register it, and return it for configuration.
323
+ * Return the {@link Hook} for a component class.
324
+ *
325
+ * Hooks let you react to component lifecycle events (add / remove / set)
326
+ * without building a full {@link System}. The same hook is returned on every
327
+ * call — handlers stack on the underlying meta record.
329
328
  *
330
329
  * ```ts
331
- * world.system("Render")
332
- * .phase("update")
333
- * .requires(Position, Sprite)
334
- * .enter([Sprite], (e, [sprite]) => sprite.initialize(scene))
335
- * .update(Position, (pos) => { ... });
330
+ * world.hook(Sprite)
331
+ * .onAdd(c => c.initialize(scene))
332
+ * .onRemove(c => c.destroy());
336
333
  * ```
337
334
  *
338
- * @param name - A unique display name for the system.
339
- * @returns The new `System` instance.
335
+ * @param C - Component class.
336
+ * @returns The hook bound to that component type.
340
337
  */
341
- system(name) {
342
- return new System(name, this);
338
+ hook(C) {
339
+ return this.getComponentMeta(C);
343
340
  }
344
341
  /**
345
- * Create a standalone {@link Query}, register it, and return it for
346
- * configuration.
342
+ * Declare a group of mutually exclusive components.
347
343
  *
348
- * Unlike a {@link System}, a standalone query has no phase and no per-tick
349
- * callbacks it is a reactive, always-updated entity set that can be read
350
- * at any time after {@link start}. Standalone queries can also be created
351
- * after {@link start}; existing matched entities are backfilled immediately.
344
+ * Adding any component in the group to an entity that already has another
345
+ * member of the group automatically removes the previous member. Members
346
+ * not in the group are unaffected.
352
347
  *
353
348
  * ```ts
354
- * const enemies = world.query("Enemies")
355
- * .requires(Enemy, Health)
356
- * .enter((e) => console.log("enemy spawned", e.eid));
357
- *
358
- * world.start();
359
- * // enemies.entities is kept up-to-date automatically
349
+ * world.setExclusiveComponents(Walking, Running, Idle);
350
+ * entity.add(Walking);
351
+ * entity.add(Running); // Walking is removed automatically
360
352
  * ```
361
353
  *
362
- * @param name - A unique display name for the query.
363
- * @returns The new `Query` instance.
354
+ * Each call defines one independent group. A component may belong to at
355
+ * most one group at a time; calling {@link setExclusiveComponents} with the
356
+ * same class again overwrites its group. Safe to call before or after
357
+ * {@link start}.
358
+ *
359
+ * @param components - Two or more component classes that cannot coexist.
360
+ * @throws When any class has not been registered.
364
361
  */
365
- query(name) {
366
- return new Query(name, this);
367
- }
368
- filter(q, _guaranteed) {
369
- return new Filter(this, q);
362
+ setExclusiveComponents(...components) {
363
+ const types = components.map((C) => this.getComponentType(C));
364
+ for (let i = 0; i < components.length; i++) {
365
+ this.getComponentMeta(components[i]).exclusive = types.filter((_, j) => j !== i);
366
+ }
370
367
  }
371
368
  /**
372
- * Prevent any further calls to {@link registerComponent}.
369
+ * Set the starting value of the auto-incrementing entity id counter.
370
+ *
371
+ * Must be called **before** {@link start} (or
372
+ * {@link disableComponentRegistration}). Useful when the world runs
373
+ * alongside a server that owns a different id range — locally created
374
+ * client entities can start at a high offset to avoid collisions with
375
+ * server-assigned ids.
373
376
  *
374
- * Called automatically by {@link start}. Can be called early if you want to
375
- * lock component registration before systems are fully configured.
377
+ * @param min - First id assigned by {@link entity}.
378
+ * @throws When called after registration has been disabled.
376
379
  */
377
- disableComponentRegistration() {
378
- this.componentRegistrationDisabled = true;
380
+ setEntityIdRange(min) {
381
+ if (this._componentRegistrationDisabled) {
382
+ throw "setEntityIdRange must be called before component registration is disabled";
383
+ }
384
+ this._eidCounter = min;
379
385
  }
380
386
  /**
381
- * Freeze component registration and prepare the world for running.
387
+ * Return the entity with id `eid`, creating it if it does not yet exist.
388
+ *
389
+ * Used by networking code to materialise server-assigned entities:
382
390
  *
383
- * Distributes all systems registered so far into their pipeline phases
384
- * (defaulting to `"update"`) and logs the phase → system order to the
385
- * console. Systems and queries can still be created after this call —
386
- * standalone queries will immediately backfill existing matched entities.
391
+ * ```ts
392
+ * const e = world.getOrCreateEntity(snapshot.eid, (e) => {
393
+ * networkEntities.add(e);
394
+ * });
395
+ * e.add(snapshot.type);
396
+ * ```
387
397
  *
388
- * Call this once before the first {@link runPhase} call.
398
+ * @param eid - Entity id to look up or create.
399
+ * @param onCreateCallback - Optional callback invoked only when a new
400
+ * entity is created, before it is returned. Use it to initialise
401
+ * bookkeeping (e.g. tracking it in a local set).
402
+ * @returns The existing or newly created entity.
389
403
  */
390
- start() {
391
- this.componentRegistrationDisabled = true;
392
- this.reindexSystems();
393
- }
394
- reindexSystems() {
395
- let _defaultPhase = this._pipeline.get("update");
396
- if (!_defaultPhase) {
397
- _defaultPhase = new Phase("update", this);
398
- this._pipeline.set(_defaultPhase.name, _defaultPhase);
404
+ getOrCreateEntity(eid, onCreateCallback) {
405
+ let e = this._entities.get(eid);
406
+ if (!e) {
407
+ e = new Entity(this, eid);
408
+ this._entities.set(eid, e);
409
+ if (onCreateCallback) {
410
+ onCreateCallback(e);
411
+ }
399
412
  }
400
- const defaultPhase = _defaultPhase;
401
- this._queries.forEach((q) => {
402
- if (!(q instanceof System)) {
403
- return;
413
+ return e;
414
+ }
415
+ entity(id) {
416
+ if (id === undefined) {
417
+ const eid = this._eidCounter++;
418
+ const e = new Entity(this, eid);
419
+ if (this.deferred) {
420
+ this._enqueue({ kind: 0 /* CommandKind.CreateEntity */, entity: e });
404
421
  }
405
- let phase = q._phase;
406
- if (typeof phase === "string") {
407
- phase = this._pipeline.get(phase);
422
+ else {
423
+ this._entities.set(eid, e);
408
424
  }
409
- phase = phase || defaultPhase;
410
- phase.systems.push(q);
411
- });
412
- this._pipeline.forEach((phase) => {
413
- console.log("Phase %s : %s", phase.name, phase.systems.map((s) => s.name).join(" -> "));
425
+ return e;
426
+ }
427
+ return this._entities.get(id);
428
+ }
429
+ /**
430
+ * Destroy every entity currently tracked by the world.
431
+ *
432
+ * Triggers all `onRemove` hooks and `exit` callbacks. Useful when
433
+ * transitioning between game sessions or resetting to a clean state.
434
+ */
435
+ clearAllEntities() {
436
+ this._entities.forEach((e) => {
437
+ e.destroy();
414
438
  });
439
+ this.flush();
415
440
  }
416
441
  /**
417
- * Return the {@link Hook} for a component class.
442
+ * Create, register, and return a new {@link System}, ready for fluent
443
+ * configuration.
418
444
  *
419
- * Hooks let you react to component lifecycle events (add / remove / set)
420
- * without building a full {@link System}. The hook is backed by the
421
- * component's {@link ComponentMeta} and the same object is returned on every
422
- * call.
445
+ * ```ts
446
+ * world.system("Render")
447
+ * .phase("update")
448
+ * .requires(Position, Sprite)
449
+ * .enter([Sprite], (e, [sprite]) => sprite.initialize(scene))
450
+ * .each([Position, Sprite], (e, [pos, sprite]) => sprite.draw(pos.x, pos.y));
451
+ * ```
452
+ *
453
+ * @param name - Unique display name for the system.
454
+ * @returns The new system.
455
+ */
456
+ system(name) {
457
+ return new System(name, this);
458
+ }
459
+ /**
460
+ * Create, register, and return a standalone {@link Query}, ready for fluent
461
+ * configuration.
462
+ *
463
+ * Unlike a {@link System}, a standalone query has no phase and no per-tick
464
+ * callbacks — it is a reactive entity set that can be read at any time. It
465
+ * can also be created **after** {@link start}; existing matched entities
466
+ * are backfilled immediately.
423
467
  *
424
468
  * ```ts
425
- * world.hook(Sprite)
426
- * .onAdd(c => c.initialize(scene))
427
- * .onRemove(c => c.destroy());
469
+ * const enemies = world.query("Enemies")
470
+ * .requires(Enemy, Health)
471
+ * .enter((e) => console.log("enemy spawned", e.eid));
472
+ *
473
+ * world.start();
474
+ * // enemies.entities is kept up-to-date automatically
428
475
  * ```
429
476
  *
430
- * @param C - The component class.
431
- * @returns The `Hook` for that component type.
477
+ * @param name - Unique display name for the query.
478
+ * @returns The new query.
432
479
  */
433
- hook(C) {
434
- return this.getComponentMeta(C);
480
+ query(name) {
481
+ return new Query(name, this);
482
+ }
483
+ filter(q, _guaranteed) {
484
+ return new Filter(this, q);
435
485
  }
436
486
  /**
437
- * Add a named phase to the update pipeline and return it.
487
+ * Add a named phase to the update pipeline.
438
488
  *
439
- * Phases are executed in insertion order when you call {@link runPhase} for
440
- * each one. Systems are assigned to a phase via {@link System.phase}.
489
+ * Phases are executed in insertion order when {@link runPhase} or
490
+ * {@link progress} is called. Systems join a phase via {@link System.phase}.
441
491
  *
442
492
  * ```ts
443
493
  * const preUpdate = world.addPhase("preupdate");
@@ -446,7 +496,7 @@ export class World {
446
496
  * ```
447
497
  *
448
498
  * @param name - Unique phase name. Systems can reference it by this string.
449
- * @returns The new {@link IPhase}.
499
+ * @returns The new phase.
450
500
  */
451
501
  addPhase(name) {
452
502
  const phase = new Phase(name, this);
@@ -454,15 +504,37 @@ export class World {
454
504
  return phase;
455
505
  }
456
506
  /**
457
- * Execute all systems in the given phase for one tick.
507
+ * Prevent any further calls to {@link registerComponent}.
508
+ *
509
+ * Called automatically by {@link start}. Call directly if you want to lock
510
+ * registration before the rest of the systems are wired up.
511
+ */
512
+ disableComponentRegistration() {
513
+ this._componentRegistrationDisabled = true;
514
+ }
515
+ /**
516
+ * Freeze component registration and prepare the world for running.
517
+ *
518
+ * Distributes every system registered so far into its phase (defaulting to
519
+ * `"update"`) and logs the phase → system order to the console. Systems
520
+ * and queries can still be created after this call — standalone queries
521
+ * backfill existing matched entities immediately.
522
+ *
523
+ * Call once before the first {@link runPhase} / {@link progress}.
524
+ */
525
+ start() {
526
+ this._componentRegistrationDisabled = true;
527
+ this._reindexSystems();
528
+ }
529
+ /**
530
+ * Execute every system in `phase` for one tick.
458
531
  *
459
- * Pending top-level mutations are drained at the start of the phase so the
460
- * first system observes a consistent world. Each system's body runs in a
461
- * deferred scope; mutations made by callbacks are appended to the world
462
- * queue and processed by the world after the system returns, before the
463
- * next system runs.
532
+ * Pending top-level mutations are drained before the first system runs so
533
+ * each system observes a consistent world. Each system body executes in a
534
+ * deferred scope; mutations made by callbacks land in the world queue and
535
+ * are processed before the next system runs.
464
536
  *
465
- * @param phase - The {@link IPhase} to run (returned by {@link addPhase}).
537
+ * @param phase - Phase reference returned from {@link addPhase}.
466
538
  * @param now - Absolute timestamp in milliseconds (e.g. `Date.now()`).
467
539
  * @param delta - Milliseconds elapsed since the previous tick.
468
540
  */
@@ -470,14 +542,12 @@ export class World {
470
542
  this.flush();
471
543
  phase.systems.forEach((s) => {
472
544
  s._run(now, delta);
473
- // System._run wraps in begin/end which drains on return; nothing more
474
- // to do here.
475
545
  });
476
546
  }
477
547
  /**
478
- * Run every phase in the pipeline in insertion order (the order phases were
479
- * registered via {@link addPhase}). Equivalent to calling
480
- * {@link runPhase} for each phase manually.
548
+ * Run every phase in the pipeline in registration order.
549
+ *
550
+ * Equivalent to calling {@link runPhase} for each phase manually.
481
551
  *
482
552
  * @param now - Absolute timestamp in milliseconds (e.g. `Date.now()`).
483
553
  * @param delta - Milliseconds elapsed since the previous tick.
@@ -488,37 +558,5 @@ export class World {
488
558
  this.runPhase(phase, now, delta);
489
559
  });
490
560
  }
491
- /**
492
- * Declare a group of mutually exclusive components.
493
- *
494
- * After this call, adding any component in the group to an entity that
495
- * already has another component from the same group will remove the other component
496
- *
497
- * ```ts
498
- * world.setExclusiveComponents(Walking, Running, Idle);
499
- * // entity.add(Running) throws if entity already has Walking or Idle
500
- * ```
501
- *
502
- * @param components - Two or more component classes that cannot coexist.
503
- * @throws If any class has not been registered.
504
- */
505
- setExclusiveComponents(...components) {
506
- const types = components.map((C) => this.getComponentType(C));
507
- for (let i = 0; i < components.length; i++) {
508
- this.getComponentMeta(components[i]).exclusive = types.filter((_, j) => j !== i);
509
- }
510
- }
511
- /**
512
- * Destroy every entity currently tracked by the world.
513
- *
514
- * Triggers all `onRemove` hooks and `exit` callbacks. Useful when
515
- * transitioning between game sessions or resetting to a clean state.
516
- */
517
- clearAllEntities() {
518
- this._entities.forEach((e) => {
519
- e.destroy();
520
- });
521
- this.flush();
522
- }
523
561
  }
524
562
  //# sourceMappingURL=world.js.map