@aethervtt/shard-core 0.0.1

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 (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -0
  3. package/dist/ecs/commands.d.ts +35 -0
  4. package/dist/ecs/commands.js +96 -0
  5. package/dist/ecs/entity.d.ts +11 -0
  6. package/dist/ecs/entity.js +12 -0
  7. package/dist/ecs/events.d.ts +34 -0
  8. package/dist/ecs/events.js +65 -0
  9. package/dist/ecs/hierarchy.d.ts +7 -0
  10. package/dist/ecs/hierarchy.js +29 -0
  11. package/dist/ecs/index.d.ts +8 -0
  12. package/dist/ecs/index.js +38 -0
  13. package/dist/ecs/observers.d.ts +40 -0
  14. package/dist/ecs/observers.js +27 -0
  15. package/dist/ecs/query.d.ts +44 -0
  16. package/dist/ecs/query.js +67 -0
  17. package/dist/ecs/table.d.ts +91 -0
  18. package/dist/ecs/table.js +286 -0
  19. package/dist/ecs/world.d.ts +127 -0
  20. package/dist/ecs/world.js +513 -0
  21. package/dist/error.d.ts +26 -0
  22. package/dist/error.js +29 -0
  23. package/dist/index.d.ts +6 -0
  24. package/dist/index.js +9 -0
  25. package/dist/json.d.ts +15 -0
  26. package/dist/json.js +22 -0
  27. package/dist/math/aabb.d.ts +13 -0
  28. package/dist/math/aabb.js +78 -0
  29. package/dist/math/affine.d.ts +28 -0
  30. package/dist/math/affine.js +159 -0
  31. package/dist/math/affine64.d.ts +11 -0
  32. package/dist/math/affine64.js +58 -0
  33. package/dist/math/frustum.d.ts +11 -0
  34. package/dist/math/frustum.js +59 -0
  35. package/dist/math/hash.d.ts +14 -0
  36. package/dist/math/hash.js +38 -0
  37. package/dist/math/index.d.ts +14 -0
  38. package/dist/math/index.js +33 -0
  39. package/dist/math/mat3.d.ts +6 -0
  40. package/dist/math/mat3.js +37 -0
  41. package/dist/math/mat4.d.ts +23 -0
  42. package/dist/math/mat4.js +224 -0
  43. package/dist/math/quat.d.ts +22 -0
  44. package/dist/math/quat.js +233 -0
  45. package/dist/math/ray.d.ts +10 -0
  46. package/dist/math/ray.js +54 -0
  47. package/dist/math/rng.d.ts +41 -0
  48. package/dist/math/rng.js +102 -0
  49. package/dist/math/types.d.ts +8 -0
  50. package/dist/math/types.js +4 -0
  51. package/dist/math/vec2.d.ts +11 -0
  52. package/dist/math/vec2.js +52 -0
  53. package/dist/math/vec3.d.ts +23 -0
  54. package/dist/math/vec3.js +135 -0
  55. package/dist/math/vec4.d.ts +8 -0
  56. package/dist/math/vec4.js +47 -0
  57. package/dist/scheduler/index.d.ts +3 -0
  58. package/dist/scheduler/index.js +39 -0
  59. package/dist/scheduler/profiler.d.ts +19 -0
  60. package/dist/scheduler/profiler.js +44 -0
  61. package/dist/scheduler/schedule.d.ts +52 -0
  62. package/dist/scheduler/schedule.js +235 -0
  63. package/dist/scheduler/system.d.ts +74 -0
  64. package/dist/scheduler/system.js +106 -0
  65. package/dist/schema/color.d.ts +7 -0
  66. package/dist/schema/color.js +34 -0
  67. package/dist/schema/component.d.ts +71 -0
  68. package/dist/schema/component.js +120 -0
  69. package/dist/schema/field.d.ts +145 -0
  70. package/dist/schema/field.js +591 -0
  71. package/dist/schema/index.d.ts +6 -0
  72. package/dist/schema/index.js +40 -0
  73. package/dist/schema/names.d.ts +27 -0
  74. package/dist/schema/names.js +39 -0
  75. package/dist/schema/registry.d.ts +38 -0
  76. package/dist/schema/registry.js +58 -0
  77. package/dist/schema/resource.d.ts +48 -0
  78. package/dist/schema/resource.js +79 -0
  79. package/dist/test-env.d.ts +31 -0
  80. package/dist/test-env.js +37 -0
  81. package/package.json +38 -0
@@ -0,0 +1,513 @@
1
+ import { ShardError } from "../error.js";
2
+ import { Registry } from "../schema/registry.js";
3
+ import { formatEntity, MAX_ENTITIES, makeEntity } from "./entity.js";
4
+ import { EventQueue, EventReader } from "./events.js";
5
+ import { ChildOf, Children } from "./hierarchy.js";
6
+ import {
7
+ ObserverTable,
8
+ onAdd,
9
+ onRemove,
10
+ onSet
11
+ } from "./observers.js";
12
+ import { Query, queryKey } from "./query.js";
13
+ import { Table } from "./table.js";
14
+ const FREE = 4294967295;
15
+ const PENDING = 4294967294;
16
+ const MAX_GENERATION = 2147483647;
17
+ const byId = (a, b) => a.id - b.id;
18
+ class World {
19
+ /** Change-detection clock. The scheduler advances it once per system run. */
20
+ tick = 1;
21
+ registry = new Registry();
22
+ generations = new Uint32Array(1024);
23
+ tableOf = new Uint32Array(1024).fill(FREE);
24
+ rowOf = new Uint32Array(1024);
25
+ nextIndex = 0;
26
+ freeIndices = [];
27
+ aliveCount = 0;
28
+ tables = [];
29
+ tableByKey = /* @__PURE__ */ new Map();
30
+ emptyTable;
31
+ queries = /* @__PURE__ */ new Map();
32
+ resources = /* @__PURE__ */ new Map();
33
+ eventQueues = /* @__PURE__ */ new Map();
34
+ observers = new ObserverTable();
35
+ registered = [];
36
+ constructor() {
37
+ this.emptyTable = this.getOrCreateTable([]);
38
+ this.installHierarchy();
39
+ }
40
+ incrementTick() {
41
+ return ++this.tick;
42
+ }
43
+ // --- entities --------------------------------------------------------------
44
+ spawn(...inits) {
45
+ const entity = this.allocate();
46
+ this.insert(entity, inits);
47
+ return entity;
48
+ }
49
+ /** Allocates an id that comes alive later via `spawnReserved`. Used by `Commands.spawn`. */
50
+ reserveEntity() {
51
+ return this.allocate();
52
+ }
53
+ spawnReserved(entity, inits) {
54
+ const index = entity % MAX_ENTITIES;
55
+ if (index >= this.nextIndex || this.tableOf[index] !== PENDING || this.generations[index] !== Math.floor(entity / MAX_ENTITIES)) {
56
+ throw new ShardError("ecs/dead-entity", `Entity ${formatEntity(entity)} is not reserved`);
57
+ }
58
+ this.insert(entity, inits);
59
+ }
60
+ isAlive(entity) {
61
+ if (!Number.isInteger(entity) || entity < 0) return false;
62
+ const index = entity % MAX_ENTITIES;
63
+ if (index >= this.nextIndex || this.tableOf[index] >= PENDING) return false;
64
+ return this.generations[index] === Math.floor(entity / MAX_ENTITIES);
65
+ }
66
+ get entityCount() {
67
+ return this.aliveCount;
68
+ }
69
+ /** Despawns the entity and, if it has children, all of its descendants. */
70
+ despawn(entity) {
71
+ this.locate(entity);
72
+ const children = this.tryGet(entity, Children);
73
+ if (children) {
74
+ for (const child of children.entities) {
75
+ if (child !== null && this.isAlive(child)) this.despawn(child);
76
+ }
77
+ }
78
+ this.despawnSingle(entity);
79
+ }
80
+ /** Despawns only this entity. Its children lose their `ChildOf` and become roots. */
81
+ despawnSingle(entity) {
82
+ this.locate(entity);
83
+ const children = this.tryGet(entity, Children);
84
+ if (children) {
85
+ for (const child of children.entities) {
86
+ if (child !== null && this.isAlive(child)) this.remove(child, ChildOf);
87
+ }
88
+ }
89
+ let index = this.locate(entity);
90
+ const components = this.tables[this.tableOf[index]].components;
91
+ for (const def of components) {
92
+ this.fire("remove", entity, def, void 0);
93
+ if (!this.isAlive(entity)) return;
94
+ }
95
+ index = this.locate(entity);
96
+ const table = this.tables[this.tableOf[index]];
97
+ const moved = table.removeRow(this.rowOf[index]);
98
+ if (moved !== -1) this.rowOf[moved % MAX_ENTITIES] = this.rowOf[index];
99
+ this.generations[index] = this.generations[index] + 1 & MAX_GENERATION;
100
+ this.tableOf[index] = FREE;
101
+ this.freeIndices.push(index);
102
+ this.aliveCount--;
103
+ }
104
+ // --- components ------------------------------------------------------------
105
+ /** Adds a component (defaults for missing fields). If present, replaces it and fires `onSet`. */
106
+ add(entity, component, value) {
107
+ const def = component;
108
+ const index = this.locate(entity);
109
+ this.ensureRegistered(def);
110
+ const table = this.tables[this.tableOf[index]];
111
+ const row = this.rowOf[index];
112
+ const init = value;
113
+ if (table.has(def)) {
114
+ const previous = this.hasObservers("set", def) ? table.readComponent(def, row) : void 0;
115
+ const added2 = table.addedTicks(def)[row];
116
+ table.initComponent(def, row, init);
117
+ table.addedTicks(def)[row] = added2;
118
+ this.fire("set", entity, def, previous);
119
+ return;
120
+ }
121
+ const target = this.tableWith(table, def);
122
+ const newRow = this.moveEntity(index, table, row, target);
123
+ target.initComponent(def, newRow, init);
124
+ const added = target.components;
125
+ for (let i = 0; i < added.length; i++) {
126
+ const c = added[i];
127
+ if (c !== def && !table.has(c)) target.initComponent(c, newRow, void 0);
128
+ }
129
+ this.fire("add", entity, def, void 0);
130
+ for (let i = 0; i < added.length; i++) {
131
+ const c = added[i];
132
+ if (c !== def && !table.has(c)) this.fire("add", entity, c, void 0);
133
+ }
134
+ }
135
+ /** Removes a component. Returns false if the entity didn't have it. */
136
+ remove(entity, component) {
137
+ let index = this.locate(entity);
138
+ if (!this.tables[this.tableOf[index]].has(component)) return false;
139
+ this.fire("remove", entity, component, void 0);
140
+ index = this.locate(entity);
141
+ const table = this.tables[this.tableOf[index]];
142
+ if (!table.has(component)) return true;
143
+ this.moveEntity(index, table, this.rowOf[index], this.tableWithout(table, component));
144
+ return true;
145
+ }
146
+ has(entity, component) {
147
+ if (!this.isAlive(entity)) return false;
148
+ return this.tables[this.tableOf[entity % MAX_ENTITIES]].has(component);
149
+ }
150
+ /** A copy of the component's value. Cold path: allocates. */
151
+ get(entity, component) {
152
+ const index = this.locate(entity);
153
+ const table = this.tables[this.tableOf[index]];
154
+ if (!table.has(component)) throw missingComponent(entity, component.name);
155
+ return table.readComponent(component, this.rowOf[index]);
156
+ }
157
+ tryGet(entity, component) {
158
+ if (!this.has(entity, component)) return void 0;
159
+ const index = entity % MAX_ENTITIES;
160
+ return this.tables[this.tableOf[index]].readComponent(component, this.rowOf[index]);
161
+ }
162
+ /** Writes the given fields and marks the component changed. */
163
+ set(entity, component, values) {
164
+ const def = component;
165
+ const index = this.locate(entity);
166
+ const table = this.tables[this.tableOf[index]];
167
+ if (!table.has(def)) throw missingComponent(entity, def.name);
168
+ const row = this.rowOf[index];
169
+ const previous = this.hasObservers("set", def) ? table.readComponent(def, row) : void 0;
170
+ table.writeComponent(def, row, values);
171
+ this.fire("set", entity, def, previous);
172
+ }
173
+ /** The table holding the entity's components. For system code that walks relationships. */
174
+ entityTable(entity) {
175
+ return this.tables[this.tableOf[this.locate(entity)]];
176
+ }
177
+ /** The entity's row in its table. Valid until the next structural change. */
178
+ entityRow(entity) {
179
+ return this.rowOf[this.locate(entity)];
180
+ }
181
+ /**
182
+ * Like `entityTable` / `entityRow` but without the liveness check, for hot relationship walks
183
+ * where the caller already knows the entity is alive (e.g. entries of a `Children` list).
184
+ */
185
+ entityTableUnchecked(entity) {
186
+ return this.tables[this.tableOf[entity % MAX_ENTITIES]];
187
+ }
188
+ entityRowUnchecked(entity) {
189
+ return this.rowOf[entity % MAX_ENTITIES];
190
+ }
191
+ componentsOf(entity) {
192
+ return this.tables[this.tableOf[this.locate(entity)]].components;
193
+ }
194
+ // --- queries ---------------------------------------------------------------
195
+ /** Returns a cached query; the same descriptor always gives the same `Query`. */
196
+ query(desc) {
197
+ const key = queryKey(desc);
198
+ let query = this.queries.get(key);
199
+ if (!query) {
200
+ query = new Query(desc);
201
+ for (const def of query.with) this.ensureRegistered(def);
202
+ for (const table of this.tables) query.consider(table);
203
+ this.queries.set(key, query);
204
+ }
205
+ return query;
206
+ }
207
+ // --- resources -------------------------------------------------------------
208
+ insertResource(def, value) {
209
+ this.registry.register(def);
210
+ this.resources.set(def.id, value);
211
+ }
212
+ /** Inserts the resource from its `init` if it isn't present yet, and returns it. */
213
+ initResource(def) {
214
+ if (!this.resources.has(def.id)) {
215
+ if (!def.init) {
216
+ throw new ShardError(
217
+ "ecs/no-resource-init",
218
+ `Resource "${def.name}" has no init function`,
219
+ {
220
+ hint: "Use insertResource with a value instead."
221
+ }
222
+ );
223
+ }
224
+ this.insertResource(def, def.init());
225
+ }
226
+ return this.resources.get(def.id);
227
+ }
228
+ resource(def) {
229
+ if (!this.resources.has(def.id)) {
230
+ throw new ShardError("ecs/missing-resource", `Resource "${def.name}" is not in the world`, {
231
+ hint: "Insert it with insertResource, or add the plugin that provides it."
232
+ });
233
+ }
234
+ return this.resources.get(def.id);
235
+ }
236
+ tryResource(def) {
237
+ return this.resources.get(def.id);
238
+ }
239
+ hasResource(def) {
240
+ return this.resources.has(def.id);
241
+ }
242
+ removeResource(def) {
243
+ return this.resources.delete(def.id);
244
+ }
245
+ // --- events ----------------------------------------------------------------
246
+ send(def, ...data) {
247
+ this.eventQueue(def).send(data[0]);
248
+ }
249
+ reader(def) {
250
+ return new EventReader(this.eventQueue(def));
251
+ }
252
+ /** Advances every event queue by one frame. Called by the app at the start of each frame. */
253
+ updateEvents() {
254
+ for (const queue of this.eventQueues.values()) queue.update();
255
+ }
256
+ eventQueue(def) {
257
+ let queue = this.eventQueues.get(def.id);
258
+ if (!queue) {
259
+ this.registry.register(def);
260
+ queue = new EventQueue(def);
261
+ this.eventQueues.set(def.id, queue);
262
+ }
263
+ return queue;
264
+ }
265
+ observe(target, fn) {
266
+ let lists;
267
+ let id;
268
+ if (target.kind === "event") {
269
+ this.registry.register(target);
270
+ lists = this.observers.custom;
271
+ id = target.id;
272
+ } else {
273
+ this.ensureRegistered(target.component);
274
+ lists = this.observers.listFor(target.kind);
275
+ id = target.component.id;
276
+ }
277
+ let list = lists[id];
278
+ if (!list) {
279
+ list = [];
280
+ lists[id] = list;
281
+ }
282
+ list.push(fn);
283
+ return () => {
284
+ const i = list.indexOf(fn);
285
+ if (i !== -1) list.splice(i, 1);
286
+ };
287
+ }
288
+ /** Runs observers registered for `event`, synchronously. */
289
+ trigger(event, data, entity) {
290
+ const list = this.observers.custom[event.id];
291
+ if (!list || list.length === 0) return;
292
+ const payload = { event, data, entity, world: this };
293
+ for (const fn of list.slice()) fn(payload);
294
+ }
295
+ // --- introspection ---------------------------------------------------------
296
+ stats() {
297
+ return {
298
+ entities: this.aliveCount,
299
+ archetypes: this.tables.length,
300
+ tables: this.tables.map((t) => ({
301
+ components: t.components.map((c) => c.name),
302
+ count: t.count,
303
+ capacity: t.capacity,
304
+ bytes: t.bytes()
305
+ }))
306
+ };
307
+ }
308
+ /** All archetype tables, including empty ones. */
309
+ allTables() {
310
+ return this.tables;
311
+ }
312
+ // --- internals -------------------------------------------------------------
313
+ allocate() {
314
+ let index = this.freeIndices.pop();
315
+ if (index === void 0) {
316
+ if (this.nextIndex >= MAX_ENTITIES) {
317
+ throw new ShardError("ecs/entity-limit", `Cannot exceed ${MAX_ENTITIES} live entities`, {
318
+ hint: "High-count data (particles, foliage, tiles) belongs in buffers, not entities."
319
+ });
320
+ }
321
+ index = this.nextIndex++;
322
+ if (index >= this.generations.length) this.growIndex();
323
+ }
324
+ this.tableOf[index] = PENDING;
325
+ return makeEntity(index, this.generations[index]);
326
+ }
327
+ growIndex() {
328
+ const size = Math.min(this.generations.length * 2, MAX_ENTITIES);
329
+ const generations = new Uint32Array(size);
330
+ generations.set(this.generations);
331
+ const tableOf = new Uint32Array(size).fill(FREE);
332
+ tableOf.set(this.tableOf);
333
+ const rowOf = new Uint32Array(size);
334
+ rowOf.set(this.rowOf);
335
+ this.generations = generations;
336
+ this.tableOf = tableOf;
337
+ this.rowOf = rowOf;
338
+ }
339
+ /** Returns the entity's index, or throws if it isn't alive. */
340
+ locate(entity) {
341
+ if (!this.isAlive(entity)) {
342
+ throw new ShardError("ecs/dead-entity", `Entity ${formatEntity(entity)} is not alive`, {
343
+ hint: "It was despawned, or the id is stale. Check world.isAlive(entity) first."
344
+ });
345
+ }
346
+ return entity % MAX_ENTITIES;
347
+ }
348
+ insert(entity, inits) {
349
+ let table = this.emptyTable;
350
+ for (let i = 0; i < inits.length; i++) {
351
+ const init = inits[i];
352
+ const def = Array.isArray(init) ? init[0] : init;
353
+ this.ensureRegistered(def);
354
+ table = this.tableWith(table, def);
355
+ }
356
+ const row = table.pushRow(entity);
357
+ const index = entity % MAX_ENTITIES;
358
+ this.tableOf[index] = table.id;
359
+ this.rowOf[index] = row;
360
+ this.aliveCount++;
361
+ const components = table.components;
362
+ for (let c = 0; c < components.length; c++) {
363
+ if (!initsInclude(inits, components[c])) table.initComponent(components[c], row, void 0);
364
+ }
365
+ for (let i = 0; i < inits.length; i++) {
366
+ const init = inits[i];
367
+ if (Array.isArray(init)) table.initComponent(init[0], row, init[1]);
368
+ else table.initComponent(init, row, void 0);
369
+ }
370
+ for (let c = 0; c < components.length; c++) this.fire("add", entity, components[c], void 0);
371
+ }
372
+ moveEntity(index, from, row, to) {
373
+ const entity = from.entities[row];
374
+ const newRow = to.pushRow(entity);
375
+ from.copySharedTo(row, to, newRow);
376
+ const moved = from.removeRow(row);
377
+ if (moved !== -1) this.rowOf[moved % MAX_ENTITIES] = row;
378
+ this.tableOf[index] = to.id;
379
+ this.rowOf[index] = newRow;
380
+ return newRow;
381
+ }
382
+ /** The archetype with `def` and everything `def` requires (transitively) added. Cached. */
383
+ tableWith(table, def) {
384
+ let next = table.addEdges.get(def.id);
385
+ if (next) return next;
386
+ const components = [...table.components];
387
+ const stack = [def];
388
+ while (stack.length > 0) {
389
+ const c = stack.pop();
390
+ if (components.some((x) => x.id === c.id)) continue;
391
+ this.ensureRegistered(c);
392
+ components.push(c);
393
+ stack.push(...c.requires);
394
+ }
395
+ next = components.length === table.components.length ? table : this.getOrCreateTable(components.sort(byId));
396
+ table.addEdges.set(def.id, next);
397
+ return next;
398
+ }
399
+ tableWithout(table, def) {
400
+ let next = table.removeEdges.get(def.id);
401
+ if (next) return next;
402
+ next = table.has(def) ? this.getOrCreateTable(table.components.filter((c) => c.id !== def.id)) : table;
403
+ table.removeEdges.set(def.id, next);
404
+ return next;
405
+ }
406
+ /**
407
+ * Hot reload: replaces a definition with one that reuses its id. Components keep their storage
408
+ * when `migrate` is omitted (same layout); otherwise every row is rebuilt through `migrate`.
409
+ * Resources and events keep their values and queues.
410
+ */
411
+ redefine(def, migrate) {
412
+ if (this.registry.get(def.name) || this.registered[def.id]) this.registry.replace(def);
413
+ if (def.kind !== "component") return;
414
+ for (const table of this.tables) {
415
+ table.addEdges.clear();
416
+ if (!table.hasId(def.id)) continue;
417
+ table.redefine(
418
+ def,
419
+ migrate && ((value, row) => migrate(value, table.entities[row]))
420
+ );
421
+ }
422
+ }
423
+ getOrCreateTable(components) {
424
+ const key = components.map((c) => c.id).join(",");
425
+ let table = this.tableByKey.get(key);
426
+ if (!table) {
427
+ table = new Table(this.tables.length, components, this);
428
+ this.tables.push(table);
429
+ this.tableByKey.set(key, table);
430
+ for (const query of this.queries.values()) query.consider(table);
431
+ }
432
+ return table;
433
+ }
434
+ ensureRegistered(def) {
435
+ if (this.registered[def.id]) return;
436
+ this.registry.register(def);
437
+ this.registered[def.id] = true;
438
+ }
439
+ hasObservers(kind, def) {
440
+ const list = this.observers.listFor(kind)[def.id];
441
+ return list !== void 0 && list.length > 0;
442
+ }
443
+ fire(kind, entity, def, previous) {
444
+ const list = this.observers.listFor(kind)[def.id];
445
+ if (!list || list.length === 0) return;
446
+ const event = { kind, entity, component: def, world: this, previous };
447
+ for (const fn of list.slice()) fn(event);
448
+ }
449
+ /** Keeps `Children` in sync with `ChildOf`. */
450
+ installHierarchy() {
451
+ this.observe(onAdd(ChildOf), ({ entity }) => {
452
+ const { parent } = this.get(entity, ChildOf);
453
+ if (parent !== null) this.addChild(parent, entity);
454
+ });
455
+ this.observe(onSet(ChildOf), ({ entity, previous }) => {
456
+ const before = previous?.parent ?? null;
457
+ const { parent } = this.get(entity, ChildOf);
458
+ if (before === parent) return;
459
+ if (before !== null && this.isAlive(before)) this.removeChild(before, entity);
460
+ if (parent !== null) this.addChild(parent, entity);
461
+ });
462
+ this.observe(onRemove(ChildOf), ({ entity }) => {
463
+ const { parent } = this.get(entity, ChildOf);
464
+ if (parent !== null && this.isAlive(parent)) this.removeChild(parent, entity);
465
+ });
466
+ }
467
+ childList(parent) {
468
+ const index = this.locate(parent);
469
+ const table = this.tables[this.tableOf[index]];
470
+ if (!table.has(Children)) return void 0;
471
+ return table.column(Children, "entities")[this.rowOf[index]];
472
+ }
473
+ addChild(parent, child) {
474
+ const list = this.childList(parent);
475
+ if (!list) {
476
+ this.add(parent, Children, { entities: [child] });
477
+ return;
478
+ }
479
+ if (!list.includes(child)) list.push(child);
480
+ const index = parent % MAX_ENTITIES;
481
+ this.tables[this.tableOf[index]].markChanged(Children, this.rowOf[index]);
482
+ }
483
+ removeChild(parent, child) {
484
+ const list = this.childList(parent);
485
+ if (!list) return;
486
+ const i = list.indexOf(child);
487
+ if (i === -1) return;
488
+ list.splice(i, 1);
489
+ if (list.length === 0) {
490
+ this.remove(parent, Children);
491
+ return;
492
+ }
493
+ const index = parent % MAX_ENTITIES;
494
+ this.tables[this.tableOf[index]].markChanged(Children, this.rowOf[index]);
495
+ }
496
+ }
497
+ function initsInclude(inits, def) {
498
+ for (let i = 0; i < inits.length; i++) {
499
+ const init = inits[i];
500
+ if ((Array.isArray(init) ? init[0] : init) === def) return true;
501
+ }
502
+ return false;
503
+ }
504
+ function missingComponent(entity, name) {
505
+ return new ShardError(
506
+ "ecs/missing-component",
507
+ `Entity ${formatEntity(entity)} has no "${name}" component`,
508
+ { hint: "Check world.has(entity, component) first, or use tryGet." }
509
+ );
510
+ }
511
+ export {
512
+ World
513
+ };
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Structured engine error. Agents read `code` and `hint`; humans read `message`.
3
+ * Codes are namespaced by package, e.g. `gpu/unsupported`, `platform/fs-read-only`.
4
+ */
5
+ export declare class ShardError extends Error {
6
+ readonly name = "ShardError";
7
+ readonly code: string;
8
+ readonly hint: string | undefined;
9
+ /** Location of the problem: a file path, an entity path, a JSON pointer. */
10
+ readonly path: string | undefined;
11
+ /** Related errors, e.g. every problem found when validating a whole file. */
12
+ readonly details: readonly ShardError[] | undefined;
13
+ constructor(code: string, message: string, options?: {
14
+ hint?: string;
15
+ path?: string;
16
+ cause?: unknown;
17
+ details?: readonly ShardError[];
18
+ });
19
+ toJSON(): {
20
+ code: string;
21
+ message: string;
22
+ hint?: string;
23
+ path?: string;
24
+ details?: unknown[];
25
+ };
26
+ }
package/dist/error.js ADDED
@@ -0,0 +1,29 @@
1
+ class ShardError extends Error {
2
+ name = "ShardError";
3
+ code;
4
+ hint;
5
+ /** Location of the problem: a file path, an entity path, a JSON pointer. */
6
+ path;
7
+ /** Related errors, e.g. every problem found when validating a whole file. */
8
+ details;
9
+ constructor(code, message, options = {}) {
10
+ super(message, { cause: options.cause });
11
+ this.code = code;
12
+ this.hint = options.hint;
13
+ this.path = options.path;
14
+ this.details = options.details;
15
+ }
16
+ toJSON() {
17
+ const out = {
18
+ code: this.code,
19
+ message: this.message
20
+ };
21
+ if (this.hint !== void 0) out.hint = this.hint;
22
+ if (this.path !== void 0) out.path = this.path;
23
+ if (this.details) out.details = this.details.map((d) => d.toJSON());
24
+ return out;
25
+ }
26
+ }
27
+ export {
28
+ ShardError
29
+ };
@@ -0,0 +1,6 @@
1
+ export * from './ecs/index.js';
2
+ export { ShardError } from './error.js';
3
+ export * from './json.js';
4
+ export * from './math/index.js';
5
+ export * from './scheduler/index.js';
6
+ export * from './schema/index.js';
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ export * from "./ecs/index.js";
2
+ import { ShardError } from "./error.js";
3
+ export * from "./json.js";
4
+ export * from "./math/index.js";
5
+ export * from "./scheduler/index.js";
6
+ export * from "./schema/index.js";
7
+ export {
8
+ ShardError
9
+ };
package/dist/json.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ export type JsonValue = null | boolean | number | string | JsonValue[] | {
2
+ [key: string]: JsonValue;
3
+ };
4
+ export type JsonObject = {
5
+ [key: string]: JsonValue;
6
+ };
7
+ /** A JSON Schema document. Kept loose; we only produce these. */
8
+ export type JsonSchema = {
9
+ [key: string]: unknown;
10
+ };
11
+ /** Appends one segment to a JSON pointer, escaping `~` and `/`. */
12
+ export declare function pointer(base: string, key: string | number): string;
13
+ export declare function isPlainObject(value: unknown): value is Record<string, unknown>;
14
+ /** Deep copy of JSON-like data (arrays and plain objects). Other values are returned as-is. */
15
+ export declare function cloneData<T>(value: T): T;
package/dist/json.js ADDED
@@ -0,0 +1,22 @@
1
+ function pointer(base, key) {
2
+ return `${base}/${String(key).replace(/~/g, "~0").replace(/\//g, "~1")}`;
3
+ }
4
+ function isPlainObject(value) {
5
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
6
+ const proto = Object.getPrototypeOf(value);
7
+ return proto === Object.prototype || proto === null;
8
+ }
9
+ function cloneData(value) {
10
+ if (Array.isArray(value)) return value.map(cloneData);
11
+ if (isPlainObject(value)) {
12
+ const out = {};
13
+ for (const key in value) out[key] = cloneData(value[key]);
14
+ return out;
15
+ }
16
+ return value;
17
+ }
18
+ export {
19
+ cloneData,
20
+ isPlainObject,
21
+ pointer
22
+ };
@@ -0,0 +1,13 @@
1
+ import type { Readable, Writable } from './types.js';
2
+ /** Axis-aligned box as 6 floats: `[minX, minY, minZ, maxX, maxY, maxZ]`. */
3
+ export declare const create: () => Float32Array;
4
+ /** An inverted box that any point expands. */
5
+ export declare function empty<T extends Writable>(out: T): T;
6
+ export declare function set<T extends Writable>(out: T, min: Readable, max: Readable): T;
7
+ /** Bounds of packed xyz positions. */
8
+ export declare function fromPoints<T extends Writable>(out: T, positions: Readable, stride?: number): T;
9
+ /** Bounds of `box` after an affine transform (Arvo's method: exact for the transformed box). */
10
+ export declare function transformAffineAt<T extends Writable>(out: T, box: Readable, m: Readable, mo: number): T;
11
+ export declare function center<T extends Writable>(out: T, box: Readable): T;
12
+ export declare function intersects(a: Readable, b: Readable): boolean;
13
+ export declare function containsPoint(box: Readable, p: Readable): boolean;