@codehz/ecs 0.7.2 → 0.7.3

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/examples/advanced-scheduling.ts +96 -0
  2. package/examples/collision-detection.ts +229 -0
  3. package/examples/inventory-system-relations.ts +108 -0
  4. package/examples/parent-child-hierarchy.ts +206 -0
  5. package/examples/serialization.ts +337 -0
  6. package/examples/simple.ts +96 -0
  7. package/examples/spatial-grid.ts +276 -0
  8. package/examples/state-machine.ts +273 -0
  9. package/examples/tag-filtering.ts +266 -0
  10. package/package.json +58 -12
  11. package/src/__tests__/commands/buffer-limits.test.ts +72 -0
  12. package/src/__tests__/commands/buffer.test.ts +195 -0
  13. package/src/__tests__/component/singleton.test.ts +148 -0
  14. package/src/__tests__/core/archetype.test.ts +247 -0
  15. package/src/__tests__/core/bitset.test.ts +171 -0
  16. package/src/__tests__/core/changeset.test.ts +254 -0
  17. package/src/__tests__/core/multi-map.test.ts +74 -0
  18. package/src/__tests__/entity/component-registry.test.ts +66 -0
  19. package/src/__tests__/entity/entity.test.ts +520 -0
  20. package/src/__tests__/entity/id-manager.test.ts +157 -0
  21. package/src/__tests__/entity/id-system.test.ts +260 -0
  22. package/src/__tests__/perf/comprehensive.perf.test.ts +300 -0
  23. package/src/__tests__/perf/sync-hotpath.perf.test.ts +79 -0
  24. package/src/__tests__/query/basic.test.ts +341 -0
  25. package/src/__tests__/query/caching.test.ts +112 -0
  26. package/src/__tests__/query/filter.test.ts +111 -0
  27. package/src/__tests__/query/optional.test.ts +231 -0
  28. package/src/__tests__/query/perf.test.ts +99 -0
  29. package/src/__tests__/relations/dont-fragment/basic.test.ts +496 -0
  30. package/src/__tests__/relations/dont-fragment/query-notification.test.ts +125 -0
  31. package/src/__tests__/relations/wildcard.test.ts +179 -0
  32. package/src/__tests__/serialization/bounds.test.ts +237 -0
  33. package/src/__tests__/testing/assertions.test.ts +224 -0
  34. package/src/__tests__/testing/entity-builder.test.ts +84 -0
  35. package/src/__tests__/testing/snapshot.test.ts +150 -0
  36. package/src/__tests__/testing/world-fixture.test.ts +73 -0
  37. package/src/__tests__/world/component-hooks.test.ts +185 -0
  38. package/src/__tests__/world/component-management.test.ts +447 -0
  39. package/src/__tests__/world/entity-management.test.ts +86 -0
  40. package/src/__tests__/world/get-optional.test.ts +96 -0
  41. package/src/__tests__/world/multi-component-hooks.test.ts +502 -0
  42. package/src/__tests__/world/perf.test.ts +93 -0
  43. package/src/__tests__/world/query.test.ts +223 -0
  44. package/src/__tests__/world/serialize.test.ts +83 -0
  45. package/src/__tests__/world/wildcard-relation-hooks.test.ts +332 -0
  46. package/src/archetype/archetype.ts +472 -0
  47. package/src/archetype/helpers.ts +186 -0
  48. package/src/archetype/store.ts +33 -0
  49. package/src/commands/buffer.ts +110 -0
  50. package/src/commands/changeset.ts +104 -0
  51. package/src/component/entity-store.ts +223 -0
  52. package/src/component/registry.ts +657 -0
  53. package/src/component/type-utils.ts +9 -0
  54. package/src/entity/index.ts +63 -0
  55. package/src/entity/manager.ts +115 -0
  56. package/src/entity/relation.ts +319 -0
  57. package/src/entity/types.ts +135 -0
  58. package/src/index.ts +41 -0
  59. package/src/query/filter.ts +75 -0
  60. package/src/query/query.ts +313 -0
  61. package/src/query/registry.ts +101 -0
  62. package/src/storage/serialization.ts +130 -0
  63. package/src/testing/index.ts +634 -0
  64. package/src/types/index.ts +99 -0
  65. package/src/utils/bit-set.ts +133 -0
  66. package/src/utils/multi-map.ts +96 -0
  67. package/src/utils/utils.ts +19 -0
  68. package/src/world/builder.ts +100 -0
  69. package/src/world/commands.ts +378 -0
  70. package/src/world/hooks.ts +358 -0
  71. package/src/world/references.ts +38 -0
  72. package/src/world/serialization.ts +122 -0
  73. package/src/world/world.ts +1201 -0
  74. /package/{builder.d.mts → dist/builder.d.mts} +0 -0
  75. /package/{index.d.mts → dist/index.d.mts} +0 -0
  76. /package/{index.mjs → dist/index.mjs} +0 -0
  77. /package/{testing.d.mts → dist/testing.d.mts} +0 -0
  78. /package/{testing.mjs → dist/testing.mjs} +0 -0
  79. /package/{testing.mjs.map → dist/testing.mjs.map} +0 -0
  80. /package/{world.mjs → dist/world.mjs} +0 -0
  81. /package/{world.mjs.map → dist/world.mjs.map} +0 -0
@@ -0,0 +1,99 @@
1
+ import type { EntityId, WildcardRelationId } from "../entity";
2
+ import type { QueryFilter } from "../query/filter";
3
+
4
+ /**
5
+ * Type-erased component ID, used for runtime container storage
6
+ * @internal
7
+ */
8
+ export type AnyComponentId = EntityId<any>;
9
+
10
+ /**
11
+ * Type-erased entity ID, used for runtime container storage
12
+ * @internal
13
+ */
14
+ export type AnyEntityId = EntityId<any>;
15
+
16
+ /**
17
+ * Lifecycle hook definition for reacting to component additions, updates, and removals.
18
+ * Register hooks with {@link World.hook}.
19
+ */
20
+ export interface LifecycleHook<T extends readonly ComponentType<any>[]> {
21
+ /**
22
+ * Called once for each entity that already matches the hook's component types
23
+ * when the hook is first registered, and then for every new matching entity.
24
+ */
25
+ on_init?: (entityId: EntityId, ...components: ComponentTuple<T>) => void;
26
+ /**
27
+ * Called whenever a matching entity's component data is updated via `set()`.
28
+ */
29
+ on_set?: (entityId: EntityId, ...components: ComponentTuple<T>) => void;
30
+ /**
31
+ * Called whenever a matching entity loses one of the required components
32
+ * or is deleted.
33
+ */
34
+ on_remove?: (entityId: EntityId, ...components: ComponentTuple<T>) => void;
35
+ }
36
+
37
+ /**
38
+ * Shorthand callback style for multi-component lifecycle hooks.
39
+ * The same function receives all three events distinguished by the `type` parameter.
40
+ *
41
+ * @example
42
+ * world.hook([Position, Velocity], (type, entityId, position, velocity) => {
43
+ * if (type === "init") console.log("spawned");
44
+ * if (type === "set") console.log("updated");
45
+ * if (type === "remove") console.log("despawned");
46
+ * });
47
+ */
48
+ export type LifecycleCallback<T extends readonly ComponentType<any>[]> = (
49
+ type: "init" | "set" | "remove",
50
+ entityId: EntityId,
51
+ ...components: ComponentTuple<T>
52
+ ) => void;
53
+
54
+ /**
55
+ * A component type used in queries and hooks.
56
+ * Can be a plain {@link EntityId} or an {@link OptionalEntityId} wrapped with `.optional`.
57
+ */
58
+ export type ComponentType<T> = EntityId<T> | OptionalEntityId<T>;
59
+
60
+ /**
61
+ * Wrapper that marks a component as optional in queries and hooks.
62
+ * When a component is optional, entities missing it are still included in results.
63
+ *
64
+ * @example
65
+ * world.createQuery([Position, { optional: Velocity }]);
66
+ */
67
+ export type OptionalEntityId<T> = { optional: EntityId<T> };
68
+
69
+ export function isOptionalEntityId<T>(type: ComponentType<T>): type is OptionalEntityId<T> {
70
+ return typeof type === "object" && type !== null && "optional" in type;
71
+ }
72
+
73
+ export type ComponentTypeToData<T> = T extends { optional: infer U }
74
+ ? { value: ComponentTypeToData<U> } | undefined
75
+ : T extends WildcardRelationId<infer U>
76
+ ? [EntityId<unknown>, U][]
77
+ : T extends EntityId<infer U>
78
+ ? U
79
+ : never;
80
+
81
+ /**
82
+ * Maps an array of {@link ComponentType} to their corresponding data tuples.
83
+ * Used by {@link World.query} and {@link Query.forEach} to type component results.
84
+ */
85
+ export type ComponentTuple<T extends readonly ComponentType<any>[]> = {
86
+ readonly [K in keyof T]: ComponentTypeToData<T[K]>;
87
+ };
88
+
89
+ export interface LifecycleHookEntry {
90
+ componentTypes: readonly ComponentType<any>[];
91
+ requiredComponents: EntityId<any>[];
92
+ optionalComponents: EntityId<any>[];
93
+ filter: QueryFilter;
94
+ hook: LifecycleHook<any>;
95
+ /** Raw callback function; takes precedence over hook.on_* when present */
96
+ callback?: LifecycleCallback<any>;
97
+ /** Archetypes that match this hook, used for precise cleanup on unsubscription */
98
+ matchedArchetypes?: Set<any>;
99
+ }
@@ -0,0 +1,133 @@
1
+ export class BitSet {
2
+ private data: Uint32Array;
3
+ private _length: number;
4
+
5
+ constructor(length: number) {
6
+ this._length = length;
7
+ const numWords = Math.ceil(length / 32);
8
+ this.data = new Uint32Array(numWords);
9
+ }
10
+
11
+ get length(): number {
12
+ return this._length;
13
+ }
14
+
15
+ has(index: number): boolean {
16
+ if (index < 0 || index >= this._length) return false;
17
+ const word = index >>> 5; // divide by 32
18
+ const bit = index & 31;
19
+ return ((this.data[word]! >>> bit) & 1) !== 0;
20
+ }
21
+
22
+ set(index: number): void {
23
+ if (index < 0 || index >= this._length) return;
24
+ const word = index >>> 5;
25
+ const bit = index & 31;
26
+ this.data[word]! |= 1 << bit;
27
+ }
28
+
29
+ clear(index: number): void {
30
+ if (index < 0 || index >= this._length) return;
31
+ const word = index >>> 5;
32
+ const bit = index & 31;
33
+ this.data[word]! &= ~(1 << bit);
34
+ }
35
+
36
+ // set a range [lo, hi] inclusive to 1
37
+ setRange(lo: number, hi: number): void {
38
+ if (lo > hi) return;
39
+ if (lo < 0) lo = 0;
40
+ if (hi >= this._length) hi = this._length - 1;
41
+
42
+ const firstWord = lo >>> 5;
43
+ const lastWord = hi >>> 5;
44
+ const loBit = lo & 31;
45
+ const hiBit = hi & 31;
46
+
47
+ // helper to produce mask for [a..b] within a single 32-bit word
48
+ const maskFor = (a: number, b: number) => {
49
+ const width = b - a + 1;
50
+ if (width <= 0) return 0 >>> 0;
51
+ if (width >= 32) return 0xffffffff >>> 0;
52
+ return (((1 << width) - 1) << a) >>> 0;
53
+ };
54
+
55
+ if (firstWord === lastWord) {
56
+ const mask = maskFor(loBit, hiBit);
57
+ this.data[firstWord]! = (this.data[firstWord]! | mask) >>> 0;
58
+ return;
59
+ }
60
+
61
+ // first partial word
62
+ const firstMask = maskFor(loBit, 31);
63
+ this.data[firstWord]! = (this.data[firstWord]! | firstMask) >>> 0;
64
+
65
+ // middle full words
66
+ for (let w = firstWord + 1; w <= lastWord - 1; w++) {
67
+ this.data[w] = 0xffffffff >>> 0;
68
+ }
69
+
70
+ // last partial word
71
+ const lastMask = maskFor(0, hiBit);
72
+ this.data[lastWord]! = (this.data[lastWord]! | lastMask) >>> 0;
73
+ }
74
+
75
+ // check whether any bit in [lo, hi] is zero (i.e. not set)
76
+ anyClearInRange(lo: number, hi: number): boolean {
77
+ if (lo > hi) return false;
78
+ if (lo < 0) lo = 0;
79
+ if (hi >= this._length) hi = this._length - 1;
80
+
81
+ const firstWord = lo >>> 5;
82
+ const lastWord = hi >>> 5;
83
+ const loBit = lo & 31;
84
+ const hiBit = hi & 31;
85
+
86
+ const maskFor = (a: number, b: number) => {
87
+ const width = b - a + 1;
88
+ if (width <= 0) return 0 >>> 0;
89
+ if (width >= 32) return 0xffffffff >>> 0;
90
+ return (((1 << width) - 1) << a) >>> 0;
91
+ };
92
+
93
+ if (firstWord === lastWord) {
94
+ const mask = maskFor(loBit, hiBit);
95
+ const bits = (this.data[firstWord]! & mask) >>> 0;
96
+ return bits !== mask >>> 0;
97
+ }
98
+
99
+ // first partial word: if any bit in the mask is clear -> return true
100
+ const firstMask = maskFor(loBit, 31);
101
+ if ((this.data[firstWord]! & firstMask) >>> 0 !== firstMask >>> 0) return true;
102
+
103
+ // middle full words
104
+ for (let w = firstWord + 1; w <= lastWord - 1; w++) {
105
+ if (this.data[w] !== 0xffffffff >>> 0) return true;
106
+ }
107
+
108
+ // last partial word
109
+ const lastMask = maskFor(0, hiBit);
110
+ if ((this.data[lastWord]! & lastMask) >>> 0 !== lastMask >>> 0) return true;
111
+
112
+ return false;
113
+ }
114
+
115
+ // reset all bits to zero
116
+ reset(): void {
117
+ this.data.fill(0);
118
+ }
119
+
120
+ *[Symbol.iterator](): IterableIterator<number> {
121
+ for (let wordIndex = 0; wordIndex < this.data.length; wordIndex++) {
122
+ let word = this.data[wordIndex]!;
123
+ if (word === 0) continue;
124
+ const baseIndex = wordIndex * 32;
125
+ for (let bit = 0; bit < 32 && baseIndex + bit < this._length; bit++) {
126
+ if (word & 1) {
127
+ yield baseIndex + bit;
128
+ }
129
+ word >>>= 1;
130
+ }
131
+ }
132
+ }
133
+ }
@@ -0,0 +1,96 @@
1
+ // A lightweight generic MultiMap implementation backed by Map<K, Set<V>>.
2
+ // Provides usual operations: add, remove, get, has, keys, values, entries,
3
+ // clear, deleteKey and size accessors.
4
+
5
+ const _MISSING = Symbol("missing");
6
+
7
+ class MultiMap<K, V> {
8
+ private map: Map<K, Set<V>> = new Map();
9
+
10
+ // Number of value entries across all keys (not number of keys).
11
+ private _valueCount = 0;
12
+
13
+ get valueCount(): number {
14
+ return this._valueCount;
15
+ }
16
+
17
+ get keyCount(): number {
18
+ return this.map.size;
19
+ }
20
+
21
+ hasKey(key: K): boolean {
22
+ return this.map.has(key);
23
+ }
24
+
25
+ has(key: K, value: V | typeof _MISSING = _MISSING): boolean {
26
+ const set = this.map.get(key);
27
+ if (!set) return false;
28
+ if (value === _MISSING) return true;
29
+ return set.has(value);
30
+ }
31
+
32
+ add(key: K, value: V): void {
33
+ let set = this.map.get(key);
34
+ if (!set) {
35
+ set = new Set();
36
+ this.map.set(key, set);
37
+ }
38
+ if (!set.has(value)) {
39
+ set.add(value);
40
+ this._valueCount++;
41
+ }
42
+ }
43
+
44
+ // Remove a specific value for a key. Returns true if removed.
45
+ remove(key: K, value: V): boolean {
46
+ const set = this.map.get(key);
47
+ if (!set) return false;
48
+ if (!set.has(value)) return false;
49
+ set.delete(value);
50
+ this._valueCount--;
51
+ if (set.size === 0) this.map.delete(key);
52
+ return true;
53
+ }
54
+
55
+ // Delete entire key and all its values. Returns true if key existed.
56
+ deleteKey(key: K): boolean {
57
+ const set = this.map.get(key);
58
+ if (!set) return false;
59
+ this._valueCount -= set.size;
60
+ this.map.delete(key);
61
+ return true;
62
+ }
63
+
64
+ get(key: K): Set<V> {
65
+ const set = this.map.get(key);
66
+ return set ? new Set(set) : new Set();
67
+ }
68
+
69
+ // iterate keys, values and entries (key -> Set copy)
70
+ *keys(): IterableIterator<K> {
71
+ yield* this.map.keys();
72
+ }
73
+
74
+ *values(): IterableIterator<V> {
75
+ for (const set of this.map.values()) {
76
+ for (const v of set) yield v;
77
+ }
78
+ }
79
+
80
+ [Symbol.iterator](): IterableIterator<[K, V]> {
81
+ return this.entries();
82
+ }
83
+
84
+ *entries(): IterableIterator<[K, V]> {
85
+ for (const [k, set] of this.map.entries()) {
86
+ for (const v of set) yield [k, v];
87
+ }
88
+ }
89
+
90
+ clear(): void {
91
+ this.map.clear();
92
+ this._valueCount = 0;
93
+ }
94
+ }
95
+
96
+ export { MultiMap };
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Utility functions for ECS library
3
+ */
4
+
5
+ /**
6
+ * Get a value from cache or compute and cache it if not present
7
+ * @param cache The cache map
8
+ * @param key The cache key
9
+ * @param compute Function to compute the value if not cached (may have side effects)
10
+ * @returns The cached or computed value
11
+ */
12
+ export function getOrCompute<K, V>(cache: Map<K, V>, key: K, compute: () => V): V {
13
+ let value = cache.get(key);
14
+ if (value === undefined) {
15
+ value = compute();
16
+ cache.set(key, value);
17
+ }
18
+ return value;
19
+ }
@@ -0,0 +1,100 @@
1
+ import type { ComponentId, EntityId } from "../entity";
2
+ import { relation } from "../entity";
3
+ import type { World } from "./world";
4
+
5
+ // =============================================================================
6
+ // EntityBuilder - Fluent Entity Creation (moved from testing utilities)
7
+ // =============================================================================
8
+
9
+ /**
10
+ * A component definition for entity building, supporting both regular components and relations
11
+ */
12
+ export type ComponentDef<T = unknown> =
13
+ | { type: "component"; id: EntityId<T>; value: T }
14
+ | { type: "relation"; componentId: ComponentId<T>; targetId: EntityId<any>; value: T };
15
+
16
+ /**
17
+ * Fluent API for constructing entities with multiple components.
18
+ * Create instances via {@link World.spawn}.
19
+ *
20
+ * @example
21
+ * const entity = world.spawn()
22
+ * .with(Position, { x: 0, y: 0 })
23
+ * .withRelation(Parent, parentEntity)
24
+ * .build();
25
+ * world.sync();
26
+ */
27
+ export class EntityBuilder {
28
+ private world: World;
29
+ private components: ComponentDef[] = [];
30
+
31
+ constructor(world: World) {
32
+ this.world = world;
33
+ }
34
+
35
+ /**
36
+ * Add a regular component to the entity under construction.
37
+ *
38
+ * @template T - The component data type
39
+ * @param componentId - The component type to add
40
+ * @param args - Component data (omit for void components)
41
+ * @returns This builder for chaining
42
+ *
43
+ * @example
44
+ * builder.with(Position, { x: 10, y: 20 });
45
+ * builder.with(Marker); // void component
46
+ */
47
+ with<T extends void>(componentId: EntityId<T>): this;
48
+ with<T>(componentId: EntityId<T>, value: T): this;
49
+ with<T>(componentId: EntityId<T>, value?: T): this {
50
+ this.components.push({ type: "component", id: componentId, value: value as T });
51
+ return this;
52
+ }
53
+
54
+ /**
55
+ * Add a relation component to the entity under construction.
56
+ *
57
+ * @template T - The relation data type
58
+ * @param componentId - The base component type for the relation
59
+ * @param targetEntity - The target entity or component for the relation
60
+ * @param args - Relation data (omit for void relations)
61
+ * @returns This builder for chaining
62
+ *
63
+ * @example
64
+ * builder.withRelation(Parent, parentEntity);
65
+ * builder.withRelation(ChildOf, childEntity, { order: 1 });
66
+ */
67
+ withRelation<T extends void>(componentId: ComponentId<T>, targetEntity: EntityId<any>): this;
68
+ withRelation<T>(componentId: ComponentId<T>, targetEntity: EntityId<any>, value: T): this;
69
+ withRelation<T>(componentId: ComponentId<T>, targetEntity: EntityId<any>, value?: T): this {
70
+ this.components.push({ type: "relation", componentId, targetId: targetEntity, value: value as T });
71
+ return this;
72
+ }
73
+
74
+ /**
75
+ * Create the entity and enqueue all configured components.
76
+ * The entity and components are only materialised after {@link World.sync} is called.
77
+ *
78
+ * @returns The newly created entity ID
79
+ *
80
+ * @example
81
+ * const entity = world.spawn()
82
+ * .with(Position, { x: 0, y: 0 })
83
+ * .build();
84
+ * world.sync(); // Apply changes
85
+ */
86
+ build(): EntityId {
87
+ const entity = this.world.new();
88
+
89
+ for (const def of this.components) {
90
+ if (def.type === "component") {
91
+ this.world.set(entity, def.id, def.value as any);
92
+ } else {
93
+ const relationId = relation(def.componentId, def.targetId);
94
+ this.world.set(entity, relationId, def.value as any);
95
+ }
96
+ }
97
+
98
+ return entity;
99
+ }
100
+ }