@codehz/ecs 0.7.2 → 0.7.4
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/examples/advanced-scheduling.ts +96 -0
- package/examples/collision-detection.ts +229 -0
- package/examples/inventory-system-relations.ts +108 -0
- package/examples/parent-child-hierarchy.ts +206 -0
- package/examples/serialization.ts +337 -0
- package/examples/simple.ts +96 -0
- package/examples/spatial-grid.ts +276 -0
- package/examples/state-machine.ts +273 -0
- package/examples/tag-filtering.ts +266 -0
- package/package.json +60 -12
- package/src/__tests__/commands/buffer-limits.test.ts +72 -0
- package/src/__tests__/commands/buffer.test.ts +195 -0
- package/src/__tests__/component/singleton.test.ts +148 -0
- package/src/__tests__/core/archetype.test.ts +247 -0
- package/src/__tests__/core/bitset.test.ts +171 -0
- package/src/__tests__/core/changeset.test.ts +254 -0
- package/src/__tests__/core/multi-map.test.ts +74 -0
- package/src/__tests__/entity/component-registry.test.ts +66 -0
- package/src/__tests__/entity/entity.test.ts +520 -0
- package/src/__tests__/entity/id-manager.test.ts +157 -0
- package/src/__tests__/entity/id-system.test.ts +260 -0
- package/src/__tests__/perf/comprehensive.perf.test.ts +300 -0
- package/src/__tests__/perf/sync-hotpath.perf.test.ts +79 -0
- package/src/__tests__/query/basic.test.ts +341 -0
- package/src/__tests__/query/caching.test.ts +112 -0
- package/src/__tests__/query/filter.test.ts +111 -0
- package/src/__tests__/query/optional.test.ts +231 -0
- package/src/__tests__/query/perf.test.ts +99 -0
- package/src/__tests__/relations/dont-fragment/basic.test.ts +496 -0
- package/src/__tests__/relations/dont-fragment/query-notification.test.ts +125 -0
- package/src/__tests__/relations/wildcard.test.ts +179 -0
- package/src/__tests__/serialization/bounds.test.ts +237 -0
- package/src/__tests__/testing/assertions.test.ts +224 -0
- package/src/__tests__/testing/entity-builder.test.ts +84 -0
- package/src/__tests__/testing/snapshot.test.ts +150 -0
- package/src/__tests__/testing/world-fixture.test.ts +73 -0
- package/src/__tests__/world/component-hooks.test.ts +185 -0
- package/src/__tests__/world/component-management.test.ts +447 -0
- package/src/__tests__/world/entity-management.test.ts +86 -0
- package/src/__tests__/world/get-optional.test.ts +96 -0
- package/src/__tests__/world/multi-component-hooks.test.ts +502 -0
- package/src/__tests__/world/perf.test.ts +93 -0
- package/src/__tests__/world/query.test.ts +223 -0
- package/src/__tests__/world/serialize.test.ts +83 -0
- package/src/__tests__/world/wildcard-relation-hooks.test.ts +332 -0
- package/src/archetype/archetype.ts +472 -0
- package/src/archetype/helpers.ts +186 -0
- package/src/archetype/store.ts +33 -0
- package/src/commands/buffer.ts +110 -0
- package/src/commands/changeset.ts +104 -0
- package/src/component/entity-store.ts +223 -0
- package/src/component/registry.ts +657 -0
- package/src/component/type-utils.ts +9 -0
- package/src/entity/index.ts +63 -0
- package/src/entity/manager.ts +115 -0
- package/src/entity/relation.ts +319 -0
- package/src/entity/types.ts +135 -0
- package/src/index.ts +41 -0
- package/src/query/filter.ts +75 -0
- package/src/query/query.ts +313 -0
- package/src/query/registry.ts +101 -0
- package/src/storage/serialization.ts +130 -0
- package/src/testing/index.ts +634 -0
- package/src/types/index.ts +99 -0
- package/src/utils/bit-set.ts +133 -0
- package/src/utils/multi-map.ts +96 -0
- package/src/utils/utils.ts +19 -0
- package/src/world/builder.ts +100 -0
- package/src/world/commands.ts +378 -0
- package/src/world/hooks.ts +358 -0
- package/src/world/references.ts +38 -0
- package/src/world/serialization.ts +122 -0
- package/src/world/world.ts +1201 -0
- /package/{builder.d.mts → dist/builder.d.mts} +0 -0
- /package/{index.d.mts → dist/index.d.mts} +0 -0
- /package/{index.mjs → dist/index.mjs} +0 -0
- /package/{testing.d.mts → dist/testing.d.mts} +0 -0
- /package/{testing.mjs → dist/testing.mjs} +0 -0
- /package/{testing.mjs.map → dist/testing.mjs.map} +0 -0
- /package/{world.mjs → dist/world.mjs} +0 -0
- /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
|
+
}
|