@codehz/ecs 0.3.15 → 0.4.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.
- package/README.md +65 -44
- package/index.d.mts +2 -793
- package/index.mjs +2 -2164
- package/package.json +5 -1
- package/testing.d.mts +305 -0
- package/testing.mjs +429 -0
- package/testing.mjs.map +1 -0
- package/world.d.mts +766 -0
- package/world.mjs +2081 -0
- package/world.mjs.map +1 -0
- package/index.mjs.map +0 -1
package/world.mjs
ADDED
|
@@ -0,0 +1,2081 @@
|
|
|
1
|
+
//#region src/bit-set.ts
|
|
2
|
+
var BitSet = class {
|
|
3
|
+
data;
|
|
4
|
+
_length;
|
|
5
|
+
constructor(length) {
|
|
6
|
+
this._length = length;
|
|
7
|
+
const numWords = Math.ceil(length / 32);
|
|
8
|
+
this.data = new Uint32Array(numWords);
|
|
9
|
+
}
|
|
10
|
+
get length() {
|
|
11
|
+
return this._length;
|
|
12
|
+
}
|
|
13
|
+
has(index) {
|
|
14
|
+
if (index < 0 || index >= this._length) return false;
|
|
15
|
+
const word = index >>> 5;
|
|
16
|
+
const bit = index & 31;
|
|
17
|
+
return (this.data[word] >>> bit & 1) !== 0;
|
|
18
|
+
}
|
|
19
|
+
set(index) {
|
|
20
|
+
if (index < 0 || index >= this._length) return;
|
|
21
|
+
const word = index >>> 5;
|
|
22
|
+
const bit = index & 31;
|
|
23
|
+
this.data[word] |= 1 << bit;
|
|
24
|
+
}
|
|
25
|
+
clear(index) {
|
|
26
|
+
if (index < 0 || index >= this._length) return;
|
|
27
|
+
const word = index >>> 5;
|
|
28
|
+
const bit = index & 31;
|
|
29
|
+
this.data[word] &= ~(1 << bit);
|
|
30
|
+
}
|
|
31
|
+
setRange(lo, hi) {
|
|
32
|
+
if (lo > hi) return;
|
|
33
|
+
if (lo < 0) lo = 0;
|
|
34
|
+
if (hi >= this._length) hi = this._length - 1;
|
|
35
|
+
const firstWord = lo >>> 5;
|
|
36
|
+
const lastWord = hi >>> 5;
|
|
37
|
+
const loBit = lo & 31;
|
|
38
|
+
const hiBit = hi & 31;
|
|
39
|
+
const maskFor = (a, b) => {
|
|
40
|
+
const width = b - a + 1;
|
|
41
|
+
if (width <= 0) return 0;
|
|
42
|
+
if (width >= 32) return 4294967295;
|
|
43
|
+
return (1 << width) - 1 << a >>> 0;
|
|
44
|
+
};
|
|
45
|
+
if (firstWord === lastWord) {
|
|
46
|
+
const mask = maskFor(loBit, hiBit);
|
|
47
|
+
this.data[firstWord] = (this.data[firstWord] | mask) >>> 0;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const firstMask = maskFor(loBit, 31);
|
|
51
|
+
this.data[firstWord] = (this.data[firstWord] | firstMask) >>> 0;
|
|
52
|
+
for (let w = firstWord + 1; w <= lastWord - 1; w++) this.data[w] = 4294967295;
|
|
53
|
+
const lastMask = maskFor(0, hiBit);
|
|
54
|
+
this.data[lastWord] = (this.data[lastWord] | lastMask) >>> 0;
|
|
55
|
+
}
|
|
56
|
+
anyClearInRange(lo, hi) {
|
|
57
|
+
if (lo > hi) return false;
|
|
58
|
+
if (lo < 0) lo = 0;
|
|
59
|
+
if (hi >= this._length) hi = this._length - 1;
|
|
60
|
+
const firstWord = lo >>> 5;
|
|
61
|
+
const lastWord = hi >>> 5;
|
|
62
|
+
const loBit = lo & 31;
|
|
63
|
+
const hiBit = hi & 31;
|
|
64
|
+
const maskFor = (a, b) => {
|
|
65
|
+
const width = b - a + 1;
|
|
66
|
+
if (width <= 0) return 0;
|
|
67
|
+
if (width >= 32) return 4294967295;
|
|
68
|
+
return (1 << width) - 1 << a >>> 0;
|
|
69
|
+
};
|
|
70
|
+
if (firstWord === lastWord) {
|
|
71
|
+
const mask = maskFor(loBit, hiBit);
|
|
72
|
+
return (this.data[firstWord] & mask) >>> 0 !== mask >>> 0;
|
|
73
|
+
}
|
|
74
|
+
const firstMask = maskFor(loBit, 31);
|
|
75
|
+
if ((this.data[firstWord] & firstMask) >>> 0 !== firstMask >>> 0) return true;
|
|
76
|
+
for (let w = firstWord + 1; w <= lastWord - 1; w++) if (this.data[w] !== 4294967295) return true;
|
|
77
|
+
const lastMask = maskFor(0, hiBit);
|
|
78
|
+
if ((this.data[lastWord] & lastMask) >>> 0 !== lastMask >>> 0) return true;
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
reset() {
|
|
82
|
+
this.data.fill(0);
|
|
83
|
+
}
|
|
84
|
+
*[Symbol.iterator]() {
|
|
85
|
+
for (let wordIndex = 0; wordIndex < this.data.length; wordIndex++) {
|
|
86
|
+
let word = this.data[wordIndex];
|
|
87
|
+
if (word === 0) continue;
|
|
88
|
+
const baseIndex = wordIndex * 32;
|
|
89
|
+
for (let bit = 0; bit < 32 && baseIndex + bit < this._length; bit++) {
|
|
90
|
+
if (word & 1) yield baseIndex + bit;
|
|
91
|
+
word >>>= 1;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
//#endregion
|
|
98
|
+
//#region src/entity.ts
|
|
99
|
+
const COMPONENT_ID_MAX = 1023;
|
|
100
|
+
const ENTITY_ID_START = 1024;
|
|
101
|
+
/**
|
|
102
|
+
* Constants for relation ID encoding
|
|
103
|
+
*/
|
|
104
|
+
const RELATION_SHIFT = 2 ** 42;
|
|
105
|
+
const WILDCARD_TARGET_ID = 0;
|
|
106
|
+
/**
|
|
107
|
+
* Internal function to decode a relation ID into raw component and target IDs
|
|
108
|
+
* @param id The EntityId to decode
|
|
109
|
+
* @returns Object with componentId and targetId, or null if not a relation
|
|
110
|
+
*/
|
|
111
|
+
function decodeRelationRaw(id) {
|
|
112
|
+
if (id >= 0) return null;
|
|
113
|
+
const absId = -id;
|
|
114
|
+
return {
|
|
115
|
+
componentId: Math.floor(absId / RELATION_SHIFT),
|
|
116
|
+
targetId: absId % RELATION_SHIFT
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Check if a component ID is valid (1-1023)
|
|
121
|
+
*/
|
|
122
|
+
function isValidComponentId(componentId) {
|
|
123
|
+
return componentId >= 1 && componentId <= COMPONENT_ID_MAX;
|
|
124
|
+
}
|
|
125
|
+
function relation(componentId, targetId) {
|
|
126
|
+
if (!isComponentId(componentId)) throw new Error("First argument must be a valid component ID");
|
|
127
|
+
let actualTargetId;
|
|
128
|
+
if (targetId === "*") actualTargetId = WILDCARD_TARGET_ID;
|
|
129
|
+
else {
|
|
130
|
+
if (!isEntityId(targetId) && !isComponentId(targetId)) throw new Error("Second argument must be a valid entity ID, component ID, or '*'");
|
|
131
|
+
actualTargetId = targetId;
|
|
132
|
+
}
|
|
133
|
+
return -(componentId * RELATION_SHIFT + actualTargetId);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Check if an ID is a component ID
|
|
137
|
+
*/
|
|
138
|
+
function isComponentId(id) {
|
|
139
|
+
return id >= 1 && id <= COMPONENT_ID_MAX;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Check if an ID is an entity ID
|
|
143
|
+
*/
|
|
144
|
+
function isEntityId(id) {
|
|
145
|
+
return id >= ENTITY_ID_START;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Check if an ID is a relation ID
|
|
149
|
+
*/
|
|
150
|
+
function isRelationId(id) {
|
|
151
|
+
return id < 0;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Check if an ID is a wildcard relation id
|
|
155
|
+
*/
|
|
156
|
+
function isWildcardRelationId(id) {
|
|
157
|
+
const decoded = decodeRelationRaw(id);
|
|
158
|
+
return decoded !== null && decoded.targetId === WILDCARD_TARGET_ID;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Decode a relation ID into component and target IDs
|
|
162
|
+
* @param relationId The relation ID (must be negative)
|
|
163
|
+
* @returns Object with componentId, targetId, and relation type
|
|
164
|
+
*/
|
|
165
|
+
function decodeRelationId(relationId) {
|
|
166
|
+
const decoded = decodeRelationRaw(relationId);
|
|
167
|
+
if (decoded === null) throw new Error("ID is not a relation ID");
|
|
168
|
+
const { componentId: rawComponentId, targetId: rawTargetId } = decoded;
|
|
169
|
+
if (!isValidComponentId(rawComponentId)) throw new Error("Invalid component ID in relation");
|
|
170
|
+
const componentId = rawComponentId;
|
|
171
|
+
const targetId = rawTargetId;
|
|
172
|
+
if (targetId === WILDCARD_TARGET_ID) return {
|
|
173
|
+
componentId,
|
|
174
|
+
targetId,
|
|
175
|
+
type: "wildcard"
|
|
176
|
+
};
|
|
177
|
+
else if (isEntityId(targetId)) return {
|
|
178
|
+
componentId,
|
|
179
|
+
targetId,
|
|
180
|
+
type: "entity"
|
|
181
|
+
};
|
|
182
|
+
else if (isComponentId(targetId)) return {
|
|
183
|
+
componentId,
|
|
184
|
+
targetId,
|
|
185
|
+
type: "component"
|
|
186
|
+
};
|
|
187
|
+
else throw new Error("Invalid target ID in relation");
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Get the string representation of an ID type
|
|
191
|
+
*/
|
|
192
|
+
function getIdType(id) {
|
|
193
|
+
if (isComponentId(id)) return "component";
|
|
194
|
+
if (isEntityId(id)) return "entity";
|
|
195
|
+
if (isRelationId(id)) try {
|
|
196
|
+
const decoded = decodeRelationId(id);
|
|
197
|
+
if (decoded.type !== "wildcard" && !isEntityId(decoded.targetId) && !isComponentId(decoded.targetId)) return "invalid";
|
|
198
|
+
switch (decoded.type) {
|
|
199
|
+
case "entity": return "entity-relation";
|
|
200
|
+
case "component": return "component-relation";
|
|
201
|
+
case "wildcard": return "wildcard-relation";
|
|
202
|
+
}
|
|
203
|
+
} catch (_error) {
|
|
204
|
+
return "invalid";
|
|
205
|
+
}
|
|
206
|
+
return "invalid";
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Get detailed type information for an EntityId
|
|
210
|
+
* @param id The EntityId to analyze
|
|
211
|
+
* @returns Detailed type information including relation subtypes
|
|
212
|
+
*/
|
|
213
|
+
function getDetailedIdType(id) {
|
|
214
|
+
if (isComponentId(id)) return { type: "component" };
|
|
215
|
+
if (isEntityId(id)) return { type: "entity" };
|
|
216
|
+
if (isRelationId(id)) try {
|
|
217
|
+
const decoded = decodeRelationId(id);
|
|
218
|
+
if (decoded.type !== "wildcard" && !isEntityId(decoded.targetId) && !isComponentId(decoded.targetId)) return { type: "invalid" };
|
|
219
|
+
let type;
|
|
220
|
+
switch (decoded.type) {
|
|
221
|
+
case "entity":
|
|
222
|
+
type = "entity-relation";
|
|
223
|
+
break;
|
|
224
|
+
case "component":
|
|
225
|
+
type = "component-relation";
|
|
226
|
+
break;
|
|
227
|
+
case "wildcard":
|
|
228
|
+
type = "wildcard-relation";
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
type,
|
|
233
|
+
componentId: decoded.componentId,
|
|
234
|
+
targetId: decoded.targetId
|
|
235
|
+
};
|
|
236
|
+
} catch (_error) {
|
|
237
|
+
return { type: "invalid" };
|
|
238
|
+
}
|
|
239
|
+
return { type: "invalid" };
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Entity ID Manager for automatic allocation and freelist recycling
|
|
243
|
+
*/
|
|
244
|
+
var EntityIdManager = class {
|
|
245
|
+
nextId = ENTITY_ID_START;
|
|
246
|
+
freelist = /* @__PURE__ */ new Set();
|
|
247
|
+
/**
|
|
248
|
+
* Allocate a new entity ID
|
|
249
|
+
* Uses freelist if available, otherwise increments counter
|
|
250
|
+
*/
|
|
251
|
+
allocate() {
|
|
252
|
+
if (this.freelist.size > 0) {
|
|
253
|
+
const id = this.freelist.values().next().value;
|
|
254
|
+
this.freelist.delete(id);
|
|
255
|
+
return id;
|
|
256
|
+
} else {
|
|
257
|
+
const id = this.nextId;
|
|
258
|
+
this.nextId++;
|
|
259
|
+
if (this.nextId >= Number.MAX_SAFE_INTEGER) throw new Error("Entity ID overflow: reached maximum safe integer");
|
|
260
|
+
return id;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Deallocate an entity ID, adding it to the freelist for reuse
|
|
265
|
+
* @param id The entity ID to deallocate
|
|
266
|
+
*/
|
|
267
|
+
deallocate(id) {
|
|
268
|
+
if (!isEntityId(id)) throw new Error("Can only deallocate valid entity IDs");
|
|
269
|
+
if (id >= this.nextId) throw new Error("Cannot deallocate an ID that was never allocated");
|
|
270
|
+
this.freelist.add(id);
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Get the current freelist size (for debugging/monitoring)
|
|
274
|
+
*/
|
|
275
|
+
getFreelistSize() {
|
|
276
|
+
return this.freelist.size;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Get the next ID that would be allocated (for debugging)
|
|
280
|
+
*/
|
|
281
|
+
getNextId() {
|
|
282
|
+
return this.nextId;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Serialize internal state for persistence.
|
|
286
|
+
* Returns a plain object representing allocator state. Values may be non-JSON-serializable.
|
|
287
|
+
*/
|
|
288
|
+
serializeState() {
|
|
289
|
+
return {
|
|
290
|
+
nextId: this.nextId,
|
|
291
|
+
freelist: Array.from(this.freelist)
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Restore internal state from a previously-serialized object.
|
|
296
|
+
* Overwrites the current nextId and freelist.
|
|
297
|
+
*/
|
|
298
|
+
deserializeState(state) {
|
|
299
|
+
if (typeof state.nextId !== "number") throw new Error("Invalid state for EntityIdManager.deserializeState");
|
|
300
|
+
this.nextId = state.nextId;
|
|
301
|
+
this.freelist = new Set(state.freelist || []);
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
/**
|
|
305
|
+
* Component ID Manager for automatic allocation
|
|
306
|
+
* Components are typically registered once and not recycled
|
|
307
|
+
*/
|
|
308
|
+
var ComponentIdAllocator = class {
|
|
309
|
+
nextId = 1;
|
|
310
|
+
/**
|
|
311
|
+
* Allocate a new component ID
|
|
312
|
+
* Increments counter sequentially from 1
|
|
313
|
+
*/
|
|
314
|
+
allocate() {
|
|
315
|
+
if (this.nextId > COMPONENT_ID_MAX) throw new Error(`Component ID overflow: maximum ${COMPONENT_ID_MAX} components allowed`);
|
|
316
|
+
const id = this.nextId;
|
|
317
|
+
this.nextId++;
|
|
318
|
+
return id;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Get the next ID that would be allocated (for debugging)
|
|
322
|
+
*/
|
|
323
|
+
getNextId() {
|
|
324
|
+
return this.nextId;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Check if more component IDs are available
|
|
328
|
+
*/
|
|
329
|
+
hasAvailableIds() {
|
|
330
|
+
return this.nextId <= COMPONENT_ID_MAX;
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
const globalComponentIdAllocator = new ComponentIdAllocator();
|
|
334
|
+
const ComponentIdForNames = /* @__PURE__ */ new Map();
|
|
335
|
+
const componentNames = new Array(COMPONENT_ID_MAX + 1);
|
|
336
|
+
const exclusiveFlags = new BitSet(COMPONENT_ID_MAX + 1);
|
|
337
|
+
const cascadeDeleteFlags = new BitSet(COMPONENT_ID_MAX + 1);
|
|
338
|
+
const dontFragmentFlags = new BitSet(COMPONENT_ID_MAX + 1);
|
|
339
|
+
/**
|
|
340
|
+
* Allocate a new component ID from the global allocator.
|
|
341
|
+
* @param nameOrOptions Optional name for the component (for serialization/debugging) or options object
|
|
342
|
+
* @returns The allocated component ID
|
|
343
|
+
* @example
|
|
344
|
+
* // Just a name
|
|
345
|
+
* const Position = component<Position>("Position");
|
|
346
|
+
*
|
|
347
|
+
* // With options
|
|
348
|
+
* const ChildOf = component({ exclusive: true, cascadeDelete: true });
|
|
349
|
+
*
|
|
350
|
+
* // With name and options
|
|
351
|
+
* const ChildOf = component({ name: "ChildOf", exclusive: true });
|
|
352
|
+
*/
|
|
353
|
+
function component(nameOrOptions) {
|
|
354
|
+
const id = globalComponentIdAllocator.allocate();
|
|
355
|
+
let name;
|
|
356
|
+
let options;
|
|
357
|
+
if (typeof nameOrOptions === "string") name = nameOrOptions;
|
|
358
|
+
else if (typeof nameOrOptions === "object" && nameOrOptions !== null) {
|
|
359
|
+
options = nameOrOptions;
|
|
360
|
+
name = options.name;
|
|
361
|
+
}
|
|
362
|
+
if (name) {
|
|
363
|
+
if (ComponentIdForNames.has(name)) throw new Error(`Component name "${name}" is already registered`);
|
|
364
|
+
componentNames[id] = name;
|
|
365
|
+
ComponentIdForNames.set(name, id);
|
|
366
|
+
}
|
|
367
|
+
if (options) {
|
|
368
|
+
if (options.exclusive) exclusiveFlags.set(id);
|
|
369
|
+
if (options.cascadeDelete) cascadeDeleteFlags.set(id);
|
|
370
|
+
if (options.dontFragment) dontFragmentFlags.set(id);
|
|
371
|
+
}
|
|
372
|
+
return id;
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Get a component ID by its registered name
|
|
376
|
+
* @param name The component name
|
|
377
|
+
* @returns The component ID if found, undefined otherwise
|
|
378
|
+
*/
|
|
379
|
+
function getComponentIdByName(name) {
|
|
380
|
+
return ComponentIdForNames.get(name);
|
|
381
|
+
}
|
|
382
|
+
/** Get a component name by its ID
|
|
383
|
+
* @param id The component ID
|
|
384
|
+
* @returns The component name if found, undefined otherwise
|
|
385
|
+
*/
|
|
386
|
+
function getComponentNameById(id) {
|
|
387
|
+
return componentNames[id];
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Check if a component is marked as exclusive
|
|
391
|
+
* @param id The component ID
|
|
392
|
+
* @returns true if the component is exclusive, false otherwise
|
|
393
|
+
*/
|
|
394
|
+
function isExclusiveComponent(id) {
|
|
395
|
+
return exclusiveFlags.has(id);
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Check if a component is marked as dontFragment
|
|
399
|
+
* @param id The component ID
|
|
400
|
+
* @returns true if the component is dontFragment, false otherwise
|
|
401
|
+
*/
|
|
402
|
+
function isDontFragmentComponent(id) {
|
|
403
|
+
return dontFragmentFlags.has(id);
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Generic function to check relation flags with specific target conditions
|
|
407
|
+
* @param id The entity/relation ID to check
|
|
408
|
+
* @param flagBitSet The bitset for the flag
|
|
409
|
+
* @param targetCondition Function to check target ID condition
|
|
410
|
+
* @returns true if the condition is met, false otherwise
|
|
411
|
+
*/
|
|
412
|
+
function checkRelationFlag(id, flagBitSet, targetCondition) {
|
|
413
|
+
const decoded = decodeRelationRaw(id);
|
|
414
|
+
if (decoded === null) return false;
|
|
415
|
+
const { componentId, targetId } = decoded;
|
|
416
|
+
return isValidComponentId(componentId) && targetCondition(targetId) && flagBitSet.has(componentId);
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Check if a relation ID is a dontFragment relation (entity-relation or component-relation with dontFragment component)
|
|
420
|
+
* This is an optimized function that avoids the overhead of getDetailedIdType
|
|
421
|
+
* @param id The entity/relation ID to check
|
|
422
|
+
* @returns true if this is a dontFragment relation, false otherwise
|
|
423
|
+
*/
|
|
424
|
+
function isDontFragmentRelation(id) {
|
|
425
|
+
return checkRelationFlag(id, dontFragmentFlags, (targetId) => targetId !== WILDCARD_TARGET_ID);
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Check if an ID is a wildcard relation with dontFragment component
|
|
429
|
+
* This is an optimized function for filtering archetype component types
|
|
430
|
+
* @param id The entity/relation ID to check
|
|
431
|
+
* @returns true if this is a wildcard relation with dontFragment component, false otherwise
|
|
432
|
+
*/
|
|
433
|
+
function isDontFragmentWildcard(id) {
|
|
434
|
+
return checkRelationFlag(id, dontFragmentFlags, (targetId) => targetId === WILDCARD_TARGET_ID);
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Check if a relation ID is a cascade delete entity-relation
|
|
438
|
+
* This is an optimized function that avoids the overhead of getDetailedIdType
|
|
439
|
+
* Note: Cascade delete only applies to entity-relations (not component-relations or wildcards)
|
|
440
|
+
* @param id The entity/relation ID to check
|
|
441
|
+
* @returns true if this is an entity-relation with cascade delete, false otherwise
|
|
442
|
+
*/
|
|
443
|
+
function isCascadeDeleteRelation(id) {
|
|
444
|
+
return checkRelationFlag(id, cascadeDeleteFlags, (targetId) => targetId !== WILDCARD_TARGET_ID && targetId >= ENTITY_ID_START);
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Get the componentId from a relation ID without fully decoding the relation.
|
|
448
|
+
* Returns undefined for non-relation IDs or invalid component IDs.
|
|
449
|
+
*/
|
|
450
|
+
function getComponentIdFromRelationId(id) {
|
|
451
|
+
const decoded = decodeRelationRaw(id);
|
|
452
|
+
if (decoded === null || !isValidComponentId(decoded.componentId)) return void 0;
|
|
453
|
+
return decoded.componentId;
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Get the targetId from a relation ID without fully decoding the relation.
|
|
457
|
+
* Returns undefined for non-relation IDs.
|
|
458
|
+
*/
|
|
459
|
+
function getTargetIdFromRelationId(id) {
|
|
460
|
+
return decodeRelationRaw(id)?.targetId;
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Check if an ID is an entity-relation (relation targeting an entity, not a component or wildcard)
|
|
464
|
+
*/
|
|
465
|
+
function isEntityRelation(id) {
|
|
466
|
+
const decoded = decodeRelationRaw(id);
|
|
467
|
+
return decoded !== null && decoded.targetId >= ENTITY_ID_START;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
//#endregion
|
|
471
|
+
//#region src/types.ts
|
|
472
|
+
function isOptionalEntityId(type) {
|
|
473
|
+
return typeof type === "object" && type !== null && "optional" in type;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
//#endregion
|
|
477
|
+
//#region src/utils.ts
|
|
478
|
+
/**
|
|
479
|
+
* Utility functions for ECS library
|
|
480
|
+
*/
|
|
481
|
+
/**
|
|
482
|
+
* Get a value from cache or compute and cache it if not present
|
|
483
|
+
* @param cache The cache map
|
|
484
|
+
* @param key The cache key
|
|
485
|
+
* @param compute Function to compute the value if not cached
|
|
486
|
+
* @returns The cached or computed value
|
|
487
|
+
*/
|
|
488
|
+
function getOrComputeCache(cache, key, compute) {
|
|
489
|
+
let value = cache.get(key);
|
|
490
|
+
if (value === void 0) {
|
|
491
|
+
value = compute();
|
|
492
|
+
cache.set(key, value);
|
|
493
|
+
}
|
|
494
|
+
return value;
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Get a value from cache or create and cache it if not present, allowing side effects during creation
|
|
498
|
+
* @param cache The cache map
|
|
499
|
+
* @param key The cache key
|
|
500
|
+
* @param create Function to create the value if not cached (can have side effects)
|
|
501
|
+
* @returns The cached or created value
|
|
502
|
+
*/
|
|
503
|
+
function getOrCreateWithSideEffect(cache, key, create) {
|
|
504
|
+
let value = cache.get(key);
|
|
505
|
+
if (value === void 0) {
|
|
506
|
+
value = create();
|
|
507
|
+
cache.set(key, value);
|
|
508
|
+
}
|
|
509
|
+
return value;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
//#endregion
|
|
513
|
+
//#region src/archetype.ts
|
|
514
|
+
/**
|
|
515
|
+
* Special value to represent missing component data
|
|
516
|
+
*/
|
|
517
|
+
const MISSING_COMPONENT = Symbol("missing component");
|
|
518
|
+
/**
|
|
519
|
+
* Archetype class for ECS architecture
|
|
520
|
+
* Represents a group of entities that share the same set of components
|
|
521
|
+
* Optimized for fast iteration and component access
|
|
522
|
+
*/
|
|
523
|
+
var Archetype = class {
|
|
524
|
+
/**
|
|
525
|
+
* The component types that define this archetype
|
|
526
|
+
*/
|
|
527
|
+
componentTypes;
|
|
528
|
+
/**
|
|
529
|
+
* List of entities in this archetype
|
|
530
|
+
*/
|
|
531
|
+
entities = [];
|
|
532
|
+
/**
|
|
533
|
+
* Component data storage - maps component type to array of component data
|
|
534
|
+
* Each array index corresponds to the entity index in the entities array
|
|
535
|
+
*/
|
|
536
|
+
componentData = /* @__PURE__ */ new Map();
|
|
537
|
+
/**
|
|
538
|
+
* Reverse mapping from entity to its index in this archetype
|
|
539
|
+
*/
|
|
540
|
+
entityToIndex = /* @__PURE__ */ new Map();
|
|
541
|
+
/**
|
|
542
|
+
* Reference to dontFragment relations storage from World
|
|
543
|
+
* This allows entities with different relation targets to share the same archetype
|
|
544
|
+
* Stored in World to avoid migration overhead when entities change archetypes
|
|
545
|
+
*/
|
|
546
|
+
dontFragmentRelations;
|
|
547
|
+
/**
|
|
548
|
+
* Cache for pre-computed component data sources to avoid repeated calculations
|
|
549
|
+
* For regular components: data array
|
|
550
|
+
* For wildcards: matching relation types array
|
|
551
|
+
*/
|
|
552
|
+
componentDataSourcesCache = /* @__PURE__ */ new Map();
|
|
553
|
+
/**
|
|
554
|
+
* Create a new archetype with the specified component types
|
|
555
|
+
* @param componentTypes The component types that define this archetype
|
|
556
|
+
* @param dontFragmentRelations Reference to the World's dontFragmentRelations storage
|
|
557
|
+
*/
|
|
558
|
+
constructor(componentTypes, dontFragmentRelations) {
|
|
559
|
+
this.componentTypes = [...componentTypes].sort((a, b) => a - b);
|
|
560
|
+
this.dontFragmentRelations = dontFragmentRelations;
|
|
561
|
+
for (const componentType of this.componentTypes) this.componentData.set(componentType, []);
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Get the number of entities in this archetype
|
|
565
|
+
*/
|
|
566
|
+
get size() {
|
|
567
|
+
return this.entities.length;
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Check if this archetype matches the given component types
|
|
571
|
+
* @param componentTypes The component types to check
|
|
572
|
+
*/
|
|
573
|
+
matches(componentTypes) {
|
|
574
|
+
if (this.componentTypes.length !== componentTypes.length) return false;
|
|
575
|
+
const sortedTypes = [...componentTypes].sort((a, b) => a - b);
|
|
576
|
+
return this.componentTypes.every((type, index) => type === sortedTypes[index]);
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Add an entity to this archetype with initial component data
|
|
580
|
+
* @param entityId The entity to add
|
|
581
|
+
* @param componentData Map of component type to component data (includes both regular and dontFragment components)
|
|
582
|
+
*/
|
|
583
|
+
addEntity(entityId, componentData) {
|
|
584
|
+
if (this.entityToIndex.has(entityId)) throw new Error(`Entity ${entityId} is already in this archetype`);
|
|
585
|
+
const index = this.entities.length;
|
|
586
|
+
this.entities.push(entityId);
|
|
587
|
+
this.entityToIndex.set(entityId, index);
|
|
588
|
+
for (const componentType of this.componentTypes) {
|
|
589
|
+
const data = componentData.get(componentType);
|
|
590
|
+
this.getComponentData(componentType).push(data === void 0 ? MISSING_COMPONENT : data);
|
|
591
|
+
}
|
|
592
|
+
const dontFragmentData = /* @__PURE__ */ new Map();
|
|
593
|
+
for (const [componentType, data] of componentData) {
|
|
594
|
+
if (this.componentTypes.includes(componentType)) continue;
|
|
595
|
+
const detailedType = getDetailedIdType(componentType);
|
|
596
|
+
if ((detailedType.type === "entity-relation" || detailedType.type === "component-relation") && isDontFragmentComponent(detailedType.componentId)) dontFragmentData.set(componentType, data);
|
|
597
|
+
}
|
|
598
|
+
if (dontFragmentData.size > 0) this.dontFragmentRelations.set(entityId, dontFragmentData);
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Get all component data for a specific entity
|
|
602
|
+
* @param entityId The entity to get data for
|
|
603
|
+
* @returns Map of component type to component data (includes both regular and dontFragment components)
|
|
604
|
+
*/
|
|
605
|
+
getEntity(entityId) {
|
|
606
|
+
const index = this.entityToIndex.get(entityId);
|
|
607
|
+
if (index === void 0) return;
|
|
608
|
+
const entityData = /* @__PURE__ */ new Map();
|
|
609
|
+
for (const componentType of this.componentTypes) {
|
|
610
|
+
const data = this.getComponentData(componentType)[index];
|
|
611
|
+
entityData.set(componentType, data === MISSING_COMPONENT ? void 0 : data);
|
|
612
|
+
}
|
|
613
|
+
const dontFragmentData = this.dontFragmentRelations.get(entityId);
|
|
614
|
+
if (dontFragmentData) for (const [componentType, data] of dontFragmentData) entityData.set(componentType, data);
|
|
615
|
+
return entityData;
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Dump all entities and their component data in this archetype
|
|
619
|
+
* @returns Array of objects with entity and component data (includes both regular and dontFragment components)
|
|
620
|
+
*/
|
|
621
|
+
dump() {
|
|
622
|
+
const result = [];
|
|
623
|
+
for (let i = 0; i < this.entities.length; i++) {
|
|
624
|
+
const entity = this.entities[i];
|
|
625
|
+
const components = /* @__PURE__ */ new Map();
|
|
626
|
+
for (const componentType of this.componentTypes) {
|
|
627
|
+
const data = this.getComponentData(componentType)[i];
|
|
628
|
+
components.set(componentType, data === MISSING_COMPONENT ? void 0 : data);
|
|
629
|
+
}
|
|
630
|
+
const dontFragmentData = this.dontFragmentRelations.get(entity);
|
|
631
|
+
if (dontFragmentData) for (const [componentType, data] of dontFragmentData) components.set(componentType, data);
|
|
632
|
+
result.push({
|
|
633
|
+
entity,
|
|
634
|
+
components
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
return result;
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Remove an entity from this archetype
|
|
641
|
+
* @param entityId The entity to remove
|
|
642
|
+
* @returns The component data of the removed entity (includes both regular and dontFragment components)
|
|
643
|
+
*/
|
|
644
|
+
removeEntity(entityId) {
|
|
645
|
+
const index = this.entityToIndex.get(entityId);
|
|
646
|
+
if (index === void 0) return;
|
|
647
|
+
const removedData = /* @__PURE__ */ new Map();
|
|
648
|
+
for (const componentType of this.componentTypes) {
|
|
649
|
+
const dataArray = this.getComponentData(componentType);
|
|
650
|
+
removedData.set(componentType, dataArray[index]);
|
|
651
|
+
}
|
|
652
|
+
const dontFragmentData = this.dontFragmentRelations.get(entityId);
|
|
653
|
+
if (dontFragmentData) {
|
|
654
|
+
for (const [componentType, data] of dontFragmentData) removedData.set(componentType, data);
|
|
655
|
+
this.dontFragmentRelations.delete(entityId);
|
|
656
|
+
}
|
|
657
|
+
this.entityToIndex.delete(entityId);
|
|
658
|
+
const lastIndex = this.entities.length - 1;
|
|
659
|
+
if (index !== lastIndex) {
|
|
660
|
+
const lastEntity = this.entities[lastIndex];
|
|
661
|
+
this.entities[index] = lastEntity;
|
|
662
|
+
this.entityToIndex.set(lastEntity, index);
|
|
663
|
+
for (const componentType of this.componentTypes) {
|
|
664
|
+
const dataArray = this.getComponentData(componentType);
|
|
665
|
+
dataArray[index] = dataArray[lastIndex];
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
this.entities.pop();
|
|
669
|
+
for (const componentType of this.componentTypes) this.getComponentData(componentType).pop();
|
|
670
|
+
return removedData;
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Check if an entity is in this archetype
|
|
674
|
+
* @param entityId The entity to check
|
|
675
|
+
*/
|
|
676
|
+
exists(entityId) {
|
|
677
|
+
return this.entityToIndex.has(entityId);
|
|
678
|
+
}
|
|
679
|
+
get(entityId, componentType) {
|
|
680
|
+
const index = this.entityToIndex.get(entityId);
|
|
681
|
+
if (index === void 0) throw new Error(`Entity ${entityId} is not in this archetype`);
|
|
682
|
+
if (isWildcardRelationId(componentType)) {
|
|
683
|
+
const componentId = getComponentIdFromRelationId(componentType);
|
|
684
|
+
const relations = [];
|
|
685
|
+
for (const relType of this.componentTypes) {
|
|
686
|
+
const relDetailed = getDetailedIdType(relType);
|
|
687
|
+
if ((relDetailed.type === "entity-relation" || relDetailed.type === "component-relation") && relDetailed.componentId === componentId) {
|
|
688
|
+
const dataArray = this.getComponentData(relType);
|
|
689
|
+
if (dataArray && dataArray[index] !== void 0) {
|
|
690
|
+
const data = dataArray[index];
|
|
691
|
+
relations.push([relDetailed.targetId, data === MISSING_COMPONENT ? void 0 : data]);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
const dontFragmentData = this.dontFragmentRelations.get(entityId);
|
|
696
|
+
if (dontFragmentData) for (const [relType, data] of dontFragmentData) {
|
|
697
|
+
const relDetailed = getDetailedIdType(relType);
|
|
698
|
+
if ((relDetailed.type === "entity-relation" || relDetailed.type === "component-relation") && relDetailed.componentId === componentId) relations.push([relDetailed.targetId, data]);
|
|
699
|
+
}
|
|
700
|
+
return relations;
|
|
701
|
+
} else {
|
|
702
|
+
if (this.componentTypes.includes(componentType)) {
|
|
703
|
+
const data = this.getComponentData(componentType)[index];
|
|
704
|
+
return data === MISSING_COMPONENT ? void 0 : data;
|
|
705
|
+
}
|
|
706
|
+
const dontFragmentData = this.dontFragmentRelations.get(entityId);
|
|
707
|
+
if (dontFragmentData && dontFragmentData.has(componentType)) return dontFragmentData.get(componentType);
|
|
708
|
+
throw new Error(`Component type ${componentType} not found for entity ${entityId}`);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Set component data for a specific entity and component type
|
|
713
|
+
* @param entityId The entity
|
|
714
|
+
* @param componentType The component type
|
|
715
|
+
* @param data The component data
|
|
716
|
+
*/
|
|
717
|
+
set(entityId, componentType, data) {
|
|
718
|
+
const index = this.entityToIndex.get(entityId);
|
|
719
|
+
if (index === void 0) throw new Error(`Entity ${entityId} is not in this archetype`);
|
|
720
|
+
if (this.componentData.has(componentType)) {
|
|
721
|
+
const dataArray = this.getComponentData(componentType);
|
|
722
|
+
dataArray[index] = data;
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
const detailedType = getDetailedIdType(componentType);
|
|
726
|
+
if ((detailedType.type === "entity-relation" || detailedType.type === "component-relation") && isDontFragmentComponent(detailedType.componentId)) {
|
|
727
|
+
let dontFragmentData = this.dontFragmentRelations.get(entityId);
|
|
728
|
+
if (!dontFragmentData) {
|
|
729
|
+
dontFragmentData = /* @__PURE__ */ new Map();
|
|
730
|
+
this.dontFragmentRelations.set(entityId, dontFragmentData);
|
|
731
|
+
}
|
|
732
|
+
dontFragmentData.set(componentType, data);
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
throw new Error(`Component type ${componentType} is not in this archetype`);
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Get all entities in this archetype
|
|
739
|
+
*/
|
|
740
|
+
getEntities() {
|
|
741
|
+
return this.entities;
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Get the mapping of entities to their indices in this archetype
|
|
745
|
+
*/
|
|
746
|
+
getEntityToIndexMap() {
|
|
747
|
+
return this.entityToIndex;
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Get component data for all entities of a specific component type
|
|
751
|
+
* @param componentType The component type
|
|
752
|
+
*/
|
|
753
|
+
getComponentData(componentType) {
|
|
754
|
+
const data = this.componentData.get(componentType);
|
|
755
|
+
if (!data) throw new Error(`Component type ${componentType} is not in this archetype`);
|
|
756
|
+
return data;
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Get optional component data for all entities of a specific component type
|
|
760
|
+
* @param componentType The component type
|
|
761
|
+
* @returns An array of component data or undefined if not present
|
|
762
|
+
*/
|
|
763
|
+
getOptionalComponentData(componentType) {
|
|
764
|
+
return this.componentData.get(componentType);
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Helper: compute or return cached data sources for provided componentTypes
|
|
768
|
+
*/
|
|
769
|
+
getCachedComponentDataSources(componentTypes) {
|
|
770
|
+
const cacheKey = this.buildCacheKey(componentTypes);
|
|
771
|
+
return getOrComputeCache(this.componentDataSourcesCache, cacheKey, () => componentTypes.map((compType) => this.getComponentDataSource(compType)));
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Build cache key for component types
|
|
775
|
+
*/
|
|
776
|
+
buildCacheKey(componentTypes) {
|
|
777
|
+
return componentTypes.map((id) => isOptionalEntityId(id) ? `opt(${id.optional})` : `${id}`).join(",");
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Get data source for a single component type
|
|
781
|
+
*/
|
|
782
|
+
getComponentDataSource(compType) {
|
|
783
|
+
const optional = isOptionalEntityId(compType);
|
|
784
|
+
const actualType = optional ? compType.optional : compType;
|
|
785
|
+
const detailedType = getDetailedIdType(actualType);
|
|
786
|
+
if (detailedType.type === "wildcard-relation") return this.getWildcardRelationDataSource(detailedType.componentId, optional);
|
|
787
|
+
else return optional ? this.getOptionalComponentData(actualType) : this.getComponentData(actualType);
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Get data source for wildcard relations
|
|
791
|
+
*/
|
|
792
|
+
getWildcardRelationDataSource(componentId, optional) {
|
|
793
|
+
const matchingRelations = this.componentTypes.filter((ct) => {
|
|
794
|
+
const detailedCt = getDetailedIdType(ct);
|
|
795
|
+
return (detailedCt.type === "entity-relation" || detailedCt.type === "component-relation") && detailedCt.componentId === componentId;
|
|
796
|
+
});
|
|
797
|
+
return optional ? matchingRelations.length > 0 ? matchingRelations : void 0 : matchingRelations;
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Helper: build component tuples for a specific entity index using precomputed data sources
|
|
801
|
+
*/
|
|
802
|
+
buildComponentsForIndex(componentTypes, componentDataSources, entityIndex, entityId) {
|
|
803
|
+
return componentDataSources.map((dataSource, i) => {
|
|
804
|
+
const compType = componentTypes[i];
|
|
805
|
+
return this.buildSingleComponent(compType, dataSource, entityIndex, entityId);
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Build a single component value from its data source
|
|
810
|
+
*/
|
|
811
|
+
buildSingleComponent(compType, dataSource, entityIndex, entityId) {
|
|
812
|
+
const optional = isOptionalEntityId(compType);
|
|
813
|
+
const actualType = optional ? compType.optional : compType;
|
|
814
|
+
if (getIdType(actualType) === "wildcard-relation") return this.buildWildcardRelationValue(actualType, dataSource, entityIndex, entityId, optional);
|
|
815
|
+
else return this.buildRegularComponentValue(dataSource, entityIndex, optional);
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* Build wildcard relation value from matching relations
|
|
819
|
+
*/
|
|
820
|
+
buildWildcardRelationValue(wildcardRelationType, dataSource, entityIndex, entityId, optional) {
|
|
821
|
+
const matchingRelations = dataSource || [];
|
|
822
|
+
const relations = [];
|
|
823
|
+
for (const relType of matchingRelations) {
|
|
824
|
+
const data = this.getComponentData(relType)[entityIndex];
|
|
825
|
+
const targetId = getTargetIdFromRelationId(relType);
|
|
826
|
+
relations.push([targetId, data === MISSING_COMPONENT ? void 0 : data]);
|
|
827
|
+
}
|
|
828
|
+
const targetComponentId = getComponentIdFromRelationId(wildcardRelationType);
|
|
829
|
+
const dontFragmentData = this.dontFragmentRelations.get(entityId);
|
|
830
|
+
if (dontFragmentData) for (const [relType, data] of dontFragmentData) {
|
|
831
|
+
const relDetailed = getDetailedIdType(relType);
|
|
832
|
+
if ((relDetailed.type === "entity-relation" || relDetailed.type === "component-relation") && relDetailed.componentId === targetComponentId) relations.push([relDetailed.targetId, data]);
|
|
833
|
+
}
|
|
834
|
+
if (relations.length === 0) {
|
|
835
|
+
if (!optional) {
|
|
836
|
+
const componentId = getComponentIdFromRelationId(wildcardRelationType);
|
|
837
|
+
throw new Error(`No matching relations found for mandatory wildcard relation component ${componentId} on entity ${entityId}`);
|
|
838
|
+
}
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
return optional ? { value: relations } : relations;
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* Build regular component value from data source
|
|
845
|
+
*/
|
|
846
|
+
buildRegularComponentValue(dataSource, entityIndex, optional) {
|
|
847
|
+
if (dataSource === void 0) {
|
|
848
|
+
if (optional) return;
|
|
849
|
+
throw new Error(`Component data not found for mandatory component type`);
|
|
850
|
+
}
|
|
851
|
+
const data = dataSource[entityIndex];
|
|
852
|
+
const result = data === MISSING_COMPONENT ? void 0 : data;
|
|
853
|
+
return optional ? { value: result } : result;
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Get entities with their component data for specified component types
|
|
857
|
+
* Optimized for bulk component access with pre-computed indices
|
|
858
|
+
* @param componentTypes Array of component types to retrieve
|
|
859
|
+
* @returns Array of objects with entity and component data
|
|
860
|
+
*/
|
|
861
|
+
getEntitiesWithComponents(componentTypes) {
|
|
862
|
+
const result = [];
|
|
863
|
+
this.forEachWithComponents(componentTypes, (entity, ...components) => {
|
|
864
|
+
result.push({
|
|
865
|
+
entity,
|
|
866
|
+
components
|
|
867
|
+
});
|
|
868
|
+
});
|
|
869
|
+
return result;
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* Iterate over entities with their component data for specified component types
|
|
873
|
+
* implemented as a generator returning each entity/components pair lazily
|
|
874
|
+
* @param componentTypes Array of component types to retrieve
|
|
875
|
+
*/
|
|
876
|
+
*iterateWithComponents(componentTypes) {
|
|
877
|
+
const componentDataSources = this.getCachedComponentDataSources(componentTypes);
|
|
878
|
+
for (let entityIndex = 0; entityIndex < this.entities.length; entityIndex++) {
|
|
879
|
+
const entity = this.entities[entityIndex];
|
|
880
|
+
yield [entity, ...this.buildComponentsForIndex(componentTypes, componentDataSources, entityIndex, entity)];
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Iterate over entities with their component data for specified component types
|
|
885
|
+
* Optimized for bulk component access
|
|
886
|
+
* @param componentTypes Array of component types to retrieve
|
|
887
|
+
* @param callback Function called for each entity with its components
|
|
888
|
+
*/
|
|
889
|
+
forEachWithComponents(componentTypes, callback) {
|
|
890
|
+
const componentDataSources = this.getCachedComponentDataSources(componentTypes);
|
|
891
|
+
for (let entityIndex = 0; entityIndex < this.entities.length; entityIndex++) {
|
|
892
|
+
const entity = this.entities[entityIndex];
|
|
893
|
+
callback(entity, ...this.buildComponentsForIndex(componentTypes, componentDataSources, entityIndex, entity));
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Iterate over all entities with their component data
|
|
898
|
+
* @param callback Function called for each entity with its component data
|
|
899
|
+
*/
|
|
900
|
+
forEach(callback) {
|
|
901
|
+
for (let i = 0; i < this.entities.length; i++) {
|
|
902
|
+
const components = /* @__PURE__ */ new Map();
|
|
903
|
+
for (const componentType of this.componentTypes) {
|
|
904
|
+
const data = this.getComponentData(componentType)[i];
|
|
905
|
+
components.set(componentType, data === MISSING_COMPONENT ? void 0 : data);
|
|
906
|
+
}
|
|
907
|
+
callback(this.entities[i], components);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* Check if any entity in this archetype has a relation matching the given component ID
|
|
912
|
+
* This includes both regular relations in componentTypes and dontFragment relations
|
|
913
|
+
* @param componentId The component ID to match
|
|
914
|
+
* @returns true if any entity has a matching relation
|
|
915
|
+
*/
|
|
916
|
+
hasRelationWithComponentId(componentId) {
|
|
917
|
+
for (const componentType of this.componentTypes) {
|
|
918
|
+
const detailedType = getDetailedIdType(componentType);
|
|
919
|
+
if ((detailedType.type === "entity-relation" || detailedType.type === "component-relation") && detailedType.componentId === componentId) return true;
|
|
920
|
+
}
|
|
921
|
+
for (const entityId of this.entities) {
|
|
922
|
+
const entityDontFragmentRelations = this.dontFragmentRelations.get(entityId);
|
|
923
|
+
if (entityDontFragmentRelations) for (const relationType of entityDontFragmentRelations.keys()) {
|
|
924
|
+
const detailedType = getDetailedIdType(relationType);
|
|
925
|
+
if ((detailedType.type === "entity-relation" || detailedType.type === "component-relation") && detailedType.componentId === componentId) return true;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
return false;
|
|
929
|
+
}
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
//#endregion
|
|
933
|
+
//#region src/changeset.ts
|
|
934
|
+
/**
|
|
935
|
+
* @internal Represents a set of component changes to be applied to an entity
|
|
936
|
+
*/
|
|
937
|
+
var ComponentChangeset = class {
|
|
938
|
+
adds = /* @__PURE__ */ new Map();
|
|
939
|
+
removes = /* @__PURE__ */ new Set();
|
|
940
|
+
/**
|
|
941
|
+
* Add a component to the changeset
|
|
942
|
+
*/
|
|
943
|
+
set(componentType, component$1) {
|
|
944
|
+
this.adds.set(componentType, component$1);
|
|
945
|
+
this.removes.delete(componentType);
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* Remove a component from the changeset
|
|
949
|
+
*/
|
|
950
|
+
delete(componentType) {
|
|
951
|
+
this.removes.add(componentType);
|
|
952
|
+
this.adds.delete(componentType);
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Check if the changeset has any changes
|
|
956
|
+
*/
|
|
957
|
+
hasChanges() {
|
|
958
|
+
return this.adds.size > 0 || this.removes.size > 0;
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* Clear all changes
|
|
962
|
+
*/
|
|
963
|
+
clear() {
|
|
964
|
+
this.adds.clear();
|
|
965
|
+
this.removes.clear();
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* Merge another changeset into this one
|
|
969
|
+
*/
|
|
970
|
+
merge(other) {
|
|
971
|
+
for (const [componentType, component$1] of other.adds) {
|
|
972
|
+
this.adds.set(componentType, component$1);
|
|
973
|
+
this.removes.delete(componentType);
|
|
974
|
+
}
|
|
975
|
+
for (const componentType of other.removes) {
|
|
976
|
+
this.removes.add(componentType);
|
|
977
|
+
this.adds.delete(componentType);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Apply the changeset to existing components and return the final state
|
|
982
|
+
*/
|
|
983
|
+
applyTo(existingComponents) {
|
|
984
|
+
for (const componentType of this.removes) existingComponents.delete(componentType);
|
|
985
|
+
for (const [componentType, component$1] of this.adds) existingComponents.set(componentType, component$1);
|
|
986
|
+
return existingComponents;
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Get the final component types after applying the changeset
|
|
990
|
+
* @param existingComponentTypes - The current component types on the entity
|
|
991
|
+
* @returns The final component types or undefined if no changes
|
|
992
|
+
*/
|
|
993
|
+
getFinalComponentTypes(existingComponentTypes) {
|
|
994
|
+
const finalComponentTypes = new Set(existingComponentTypes);
|
|
995
|
+
let changed = false;
|
|
996
|
+
for (const componentType of this.removes) {
|
|
997
|
+
if (!finalComponentTypes.has(componentType)) {
|
|
998
|
+
this.removes.delete(componentType);
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
changed = true;
|
|
1002
|
+
finalComponentTypes.delete(componentType);
|
|
1003
|
+
}
|
|
1004
|
+
for (const componentType of this.adds.keys()) {
|
|
1005
|
+
if (finalComponentTypes.has(componentType)) continue;
|
|
1006
|
+
changed = true;
|
|
1007
|
+
finalComponentTypes.add(componentType);
|
|
1008
|
+
}
|
|
1009
|
+
return changed ? Array.from(finalComponentTypes) : void 0;
|
|
1010
|
+
}
|
|
1011
|
+
};
|
|
1012
|
+
|
|
1013
|
+
//#endregion
|
|
1014
|
+
//#region src/command-buffer.ts
|
|
1015
|
+
/**
|
|
1016
|
+
* Command buffer for deferred structural changes
|
|
1017
|
+
*/
|
|
1018
|
+
var CommandBuffer = class {
|
|
1019
|
+
commands = [];
|
|
1020
|
+
executeEntityCommands;
|
|
1021
|
+
/**
|
|
1022
|
+
* Create a command buffer with an executor function
|
|
1023
|
+
*/
|
|
1024
|
+
constructor(executeEntityCommands) {
|
|
1025
|
+
this.executeEntityCommands = executeEntityCommands;
|
|
1026
|
+
}
|
|
1027
|
+
set(entityId, componentType, component$1) {
|
|
1028
|
+
this.commands.push({
|
|
1029
|
+
type: "set",
|
|
1030
|
+
entityId,
|
|
1031
|
+
componentType,
|
|
1032
|
+
component: component$1
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* Remove a component from an entity (deferred)
|
|
1037
|
+
*/
|
|
1038
|
+
remove(entityId, componentType) {
|
|
1039
|
+
this.commands.push({
|
|
1040
|
+
type: "delete",
|
|
1041
|
+
entityId,
|
|
1042
|
+
componentType
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Destroy an entity (deferred)
|
|
1047
|
+
*/
|
|
1048
|
+
delete(entityId) {
|
|
1049
|
+
this.commands.push({
|
|
1050
|
+
type: "destroy",
|
|
1051
|
+
entityId
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Execute all commands and clear the buffer
|
|
1056
|
+
*/
|
|
1057
|
+
execute() {
|
|
1058
|
+
const MAX_ITERATIONS = 100;
|
|
1059
|
+
let iterations = 0;
|
|
1060
|
+
while (this.commands.length > 0) {
|
|
1061
|
+
if (iterations >= MAX_ITERATIONS) throw new Error("Command execution exceeded maximum iterations, possible infinite loop");
|
|
1062
|
+
iterations++;
|
|
1063
|
+
const currentCommands = [...this.commands];
|
|
1064
|
+
this.commands = [];
|
|
1065
|
+
const entityCommands = /* @__PURE__ */ new Map();
|
|
1066
|
+
for (const cmd of currentCommands) {
|
|
1067
|
+
if (!entityCommands.has(cmd.entityId)) entityCommands.set(cmd.entityId, []);
|
|
1068
|
+
entityCommands.get(cmd.entityId).push(cmd);
|
|
1069
|
+
}
|
|
1070
|
+
for (const [entityId, commands] of entityCommands) this.executeEntityCommands(entityId, commands);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Get current commands (for testing)
|
|
1075
|
+
*/
|
|
1076
|
+
getCommands() {
|
|
1077
|
+
return [...this.commands];
|
|
1078
|
+
}
|
|
1079
|
+
/**
|
|
1080
|
+
* Clear all commands
|
|
1081
|
+
*/
|
|
1082
|
+
clear() {
|
|
1083
|
+
this.commands = [];
|
|
1084
|
+
}
|
|
1085
|
+
};
|
|
1086
|
+
|
|
1087
|
+
//#endregion
|
|
1088
|
+
//#region src/multi-map.ts
|
|
1089
|
+
var MultiMap = class {
|
|
1090
|
+
map = /* @__PURE__ */ new Map();
|
|
1091
|
+
_valueCount = 0;
|
|
1092
|
+
get valueCount() {
|
|
1093
|
+
return this._valueCount;
|
|
1094
|
+
}
|
|
1095
|
+
get keyCount() {
|
|
1096
|
+
return this.map.size;
|
|
1097
|
+
}
|
|
1098
|
+
hasKey(key) {
|
|
1099
|
+
return this.map.has(key);
|
|
1100
|
+
}
|
|
1101
|
+
has(key, value) {
|
|
1102
|
+
const set = this.map.get(key);
|
|
1103
|
+
if (!set) return false;
|
|
1104
|
+
if (arguments.length === 1) return true;
|
|
1105
|
+
return set.has(value);
|
|
1106
|
+
}
|
|
1107
|
+
add(key, value) {
|
|
1108
|
+
let set = this.map.get(key);
|
|
1109
|
+
if (!set) {
|
|
1110
|
+
set = /* @__PURE__ */ new Set();
|
|
1111
|
+
this.map.set(key, set);
|
|
1112
|
+
}
|
|
1113
|
+
if (!set.has(value)) {
|
|
1114
|
+
set.add(value);
|
|
1115
|
+
this._valueCount++;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
remove(key, value) {
|
|
1119
|
+
const set = this.map.get(key);
|
|
1120
|
+
if (!set) return false;
|
|
1121
|
+
if (!set.has(value)) return false;
|
|
1122
|
+
set.delete(value);
|
|
1123
|
+
this._valueCount--;
|
|
1124
|
+
if (set.size === 0) this.map.delete(key);
|
|
1125
|
+
return true;
|
|
1126
|
+
}
|
|
1127
|
+
deleteKey(key) {
|
|
1128
|
+
const set = this.map.get(key);
|
|
1129
|
+
if (!set) return false;
|
|
1130
|
+
this._valueCount -= set.size;
|
|
1131
|
+
this.map.delete(key);
|
|
1132
|
+
return true;
|
|
1133
|
+
}
|
|
1134
|
+
get(key) {
|
|
1135
|
+
const set = this.map.get(key);
|
|
1136
|
+
return set ? new Set(set) : /* @__PURE__ */ new Set();
|
|
1137
|
+
}
|
|
1138
|
+
*keys() {
|
|
1139
|
+
yield* this.map.keys();
|
|
1140
|
+
}
|
|
1141
|
+
*values() {
|
|
1142
|
+
for (const set of this.map.values()) for (const v of set) yield v;
|
|
1143
|
+
}
|
|
1144
|
+
[Symbol.iterator]() {
|
|
1145
|
+
return this.entries();
|
|
1146
|
+
}
|
|
1147
|
+
*entries() {
|
|
1148
|
+
for (const [k, set] of this.map.entries()) for (const v of set) yield [k, v];
|
|
1149
|
+
}
|
|
1150
|
+
clear() {
|
|
1151
|
+
this.map.clear();
|
|
1152
|
+
this._valueCount = 0;
|
|
1153
|
+
}
|
|
1154
|
+
};
|
|
1155
|
+
|
|
1156
|
+
//#endregion
|
|
1157
|
+
//#region src/query-filter.ts
|
|
1158
|
+
/**
|
|
1159
|
+
* Serialize a QueryFilter into a deterministic string suitable for cache keys.
|
|
1160
|
+
* Currently only serializes `negativeComponentTypes`.
|
|
1161
|
+
*/
|
|
1162
|
+
function serializeQueryFilter(filter = {}) {
|
|
1163
|
+
const negative = (filter.negativeComponentTypes || []).slice().sort((a, b) => a - b);
|
|
1164
|
+
if (negative.length === 0) return "";
|
|
1165
|
+
return `neg:${negative.join(",")}`;
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* Check if an archetype matches the given component types
|
|
1169
|
+
*/
|
|
1170
|
+
function matchesComponentTypes(archetype, componentTypes) {
|
|
1171
|
+
return componentTypes.every((type) => {
|
|
1172
|
+
const detailedType = getDetailedIdType(type);
|
|
1173
|
+
if (detailedType.type === "wildcard-relation") return archetype.componentTypes.some((archetypeType) => {
|
|
1174
|
+
if (!isRelationId(archetypeType)) return false;
|
|
1175
|
+
return getComponentIdFromRelationId(archetypeType) === detailedType.componentId;
|
|
1176
|
+
});
|
|
1177
|
+
else return archetype.componentTypes.includes(type);
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
/**
|
|
1181
|
+
* Check if an archetype matches the filter conditions (only filtering logic)
|
|
1182
|
+
*/
|
|
1183
|
+
function matchesFilter(archetype, filter) {
|
|
1184
|
+
return (filter.negativeComponentTypes || []).every((type) => {
|
|
1185
|
+
const detailedType = getDetailedIdType(type);
|
|
1186
|
+
if (detailedType.type === "wildcard-relation") return !archetype.componentTypes.some((archetypeType) => {
|
|
1187
|
+
if (!isRelationId(archetypeType)) return false;
|
|
1188
|
+
return getComponentIdFromRelationId(archetypeType) === detailedType.componentId;
|
|
1189
|
+
});
|
|
1190
|
+
else return !archetype.componentTypes.includes(type);
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
//#endregion
|
|
1195
|
+
//#region src/query.ts
|
|
1196
|
+
/**
|
|
1197
|
+
* Query class for efficient entity queries with cached archetypes
|
|
1198
|
+
*/
|
|
1199
|
+
var Query = class {
|
|
1200
|
+
world;
|
|
1201
|
+
componentTypes;
|
|
1202
|
+
filter;
|
|
1203
|
+
cachedArchetypes = [];
|
|
1204
|
+
isDisposed = false;
|
|
1205
|
+
constructor(world, componentTypes, filter = {}) {
|
|
1206
|
+
this.world = world;
|
|
1207
|
+
this.componentTypes = [...componentTypes].sort((a, b) => a - b);
|
|
1208
|
+
this.filter = filter;
|
|
1209
|
+
this.updateCache();
|
|
1210
|
+
world._registerQuery(this);
|
|
1211
|
+
}
|
|
1212
|
+
/**
|
|
1213
|
+
* Check if query is disposed and throw error if so
|
|
1214
|
+
*/
|
|
1215
|
+
ensureNotDisposed() {
|
|
1216
|
+
if (this.isDisposed) throw new Error("Query has been disposed");
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* Get all entities matching the query
|
|
1220
|
+
*/
|
|
1221
|
+
getEntities() {
|
|
1222
|
+
this.ensureNotDisposed();
|
|
1223
|
+
const result = [];
|
|
1224
|
+
if (this.componentTypes.some((ct) => {
|
|
1225
|
+
return getDetailedIdType(ct).type === "wildcard-relation";
|
|
1226
|
+
})) for (const archetype of this.cachedArchetypes) for (const entity of archetype.getEntities()) {
|
|
1227
|
+
let hasAllRelations = true;
|
|
1228
|
+
for (const componentType of this.componentTypes) if (getDetailedIdType(componentType).type === "wildcard-relation") {
|
|
1229
|
+
const relations = archetype.get(entity, componentType);
|
|
1230
|
+
if (!relations || relations.length === 0) {
|
|
1231
|
+
hasAllRelations = false;
|
|
1232
|
+
break;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
if (hasAllRelations) result.push(entity);
|
|
1236
|
+
}
|
|
1237
|
+
else for (const archetype of this.cachedArchetypes) result.push(...archetype.getEntities());
|
|
1238
|
+
return result;
|
|
1239
|
+
}
|
|
1240
|
+
/**
|
|
1241
|
+
* Get entities with their component data
|
|
1242
|
+
* @param componentTypes Array of component types to retrieve
|
|
1243
|
+
* @returns Array of objects with entity and component data
|
|
1244
|
+
*/
|
|
1245
|
+
getEntitiesWithComponents(componentTypes) {
|
|
1246
|
+
this.ensureNotDisposed();
|
|
1247
|
+
const result = [];
|
|
1248
|
+
for (const archetype of this.cachedArchetypes) {
|
|
1249
|
+
const entitiesWithData = archetype.getEntitiesWithComponents(componentTypes);
|
|
1250
|
+
result.push(...entitiesWithData);
|
|
1251
|
+
}
|
|
1252
|
+
return result;
|
|
1253
|
+
}
|
|
1254
|
+
/**
|
|
1255
|
+
* Iterate over entities with their component data
|
|
1256
|
+
* @param componentTypes Array of component types to retrieve
|
|
1257
|
+
* @param callback Function called for each entity with its components
|
|
1258
|
+
*/
|
|
1259
|
+
forEach(componentTypes, callback) {
|
|
1260
|
+
this.ensureNotDisposed();
|
|
1261
|
+
for (const archetype of this.cachedArchetypes) archetype.forEachWithComponents(componentTypes, callback);
|
|
1262
|
+
}
|
|
1263
|
+
/**
|
|
1264
|
+
* Iterate over entities with their component data (generator)
|
|
1265
|
+
* @param componentTypes Array of component types to retrieve
|
|
1266
|
+
*/
|
|
1267
|
+
*iterate(componentTypes) {
|
|
1268
|
+
this.ensureNotDisposed();
|
|
1269
|
+
for (const archetype of this.cachedArchetypes) yield* archetype.iterateWithComponents(componentTypes);
|
|
1270
|
+
}
|
|
1271
|
+
/**
|
|
1272
|
+
* Get component data arrays for all matching entities
|
|
1273
|
+
* @param componentType The component type to retrieve
|
|
1274
|
+
* @returns Array of component data for all matching entities
|
|
1275
|
+
*/
|
|
1276
|
+
getComponentData(componentType) {
|
|
1277
|
+
this.ensureNotDisposed();
|
|
1278
|
+
const result = [];
|
|
1279
|
+
for (const archetype of this.cachedArchetypes) result.push(...archetype.getComponentData(componentType));
|
|
1280
|
+
return result;
|
|
1281
|
+
}
|
|
1282
|
+
/**
|
|
1283
|
+
* Update the cached archetypes
|
|
1284
|
+
* Called when new archetypes are created
|
|
1285
|
+
*/
|
|
1286
|
+
updateCache() {
|
|
1287
|
+
if (this.isDisposed) return;
|
|
1288
|
+
this.cachedArchetypes = this.world.getMatchingArchetypes(this.componentTypes).filter((archetype) => matchesFilter(archetype, this.filter));
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* Check if a new archetype matches this query and add to cache if it does
|
|
1292
|
+
*/
|
|
1293
|
+
checkNewArchetype(archetype) {
|
|
1294
|
+
if (this.isDisposed) return;
|
|
1295
|
+
if (matchesComponentTypes(archetype, this.componentTypes) && matchesFilter(archetype, this.filter) && !this.cachedArchetypes.includes(archetype)) this.cachedArchetypes.push(archetype);
|
|
1296
|
+
}
|
|
1297
|
+
/**
|
|
1298
|
+
* Remove an archetype from the cached archetypes
|
|
1299
|
+
*/
|
|
1300
|
+
removeArchetype(archetype) {
|
|
1301
|
+
if (this.isDisposed) return;
|
|
1302
|
+
const index = this.cachedArchetypes.indexOf(archetype);
|
|
1303
|
+
if (index !== -1) this.cachedArchetypes.splice(index, 1);
|
|
1304
|
+
}
|
|
1305
|
+
/**
|
|
1306
|
+
* Dispose the query and disconnect from world
|
|
1307
|
+
*/
|
|
1308
|
+
/**
|
|
1309
|
+
* Request disposal of this query.
|
|
1310
|
+
* This will decrement the world's reference count for the query.
|
|
1311
|
+
* The query will only be fully disposed when the ref count reaches zero.
|
|
1312
|
+
*/
|
|
1313
|
+
dispose() {
|
|
1314
|
+
this.world.releaseQuery(this);
|
|
1315
|
+
}
|
|
1316
|
+
/**
|
|
1317
|
+
* Internal full dispose called by World when refCount reaches zero.
|
|
1318
|
+
*/
|
|
1319
|
+
_disposeInternal() {
|
|
1320
|
+
if (!this.isDisposed) {
|
|
1321
|
+
this.world._unregisterQuery(this);
|
|
1322
|
+
this.cachedArchetypes = [];
|
|
1323
|
+
this.isDisposed = true;
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
/**
|
|
1327
|
+
* Symbol.dispose implementation for automatic resource management
|
|
1328
|
+
*/
|
|
1329
|
+
[Symbol.dispose]() {
|
|
1330
|
+
this.dispose();
|
|
1331
|
+
}
|
|
1332
|
+
/**
|
|
1333
|
+
* Check if the query has been disposed
|
|
1334
|
+
*/
|
|
1335
|
+
get disposed() {
|
|
1336
|
+
return this.isDisposed;
|
|
1337
|
+
}
|
|
1338
|
+
};
|
|
1339
|
+
|
|
1340
|
+
//#endregion
|
|
1341
|
+
//#region src/world.ts
|
|
1342
|
+
/**
|
|
1343
|
+
* World class for ECS architecture
|
|
1344
|
+
* Manages entities and components
|
|
1345
|
+
*/
|
|
1346
|
+
var World = class {
|
|
1347
|
+
/** Manages allocation and deallocation of entity IDs */
|
|
1348
|
+
entityIdManager = new EntityIdManager();
|
|
1349
|
+
/** Array of all archetypes in the world */
|
|
1350
|
+
archetypes = [];
|
|
1351
|
+
/** Maps archetype signatures (component type signatures) to archetype instances */
|
|
1352
|
+
archetypeBySignature = /* @__PURE__ */ new Map();
|
|
1353
|
+
/** Maps entity IDs to their current archetype */
|
|
1354
|
+
entityToArchetype = /* @__PURE__ */ new Map();
|
|
1355
|
+
/** Maps component types to arrays of archetypes that contain them */
|
|
1356
|
+
archetypesByComponent = /* @__PURE__ */ new Map();
|
|
1357
|
+
/** Tracks which entities reference each entity as a component type */
|
|
1358
|
+
entityReferences = /* @__PURE__ */ new Map();
|
|
1359
|
+
/** Storage for dontFragment relations - maps entity ID to a map of relation type to component data */
|
|
1360
|
+
dontFragmentRelations = /* @__PURE__ */ new Map();
|
|
1361
|
+
/** Array of all active queries for archetype change notifications */
|
|
1362
|
+
queries = [];
|
|
1363
|
+
/** Cache for queries keyed by component types and filter signatures */
|
|
1364
|
+
queryCache = /* @__PURE__ */ new Map();
|
|
1365
|
+
/** Buffers structural changes for deferred execution */
|
|
1366
|
+
commandBuffer = new CommandBuffer((entityId, commands) => this.executeEntityCommands(entityId, commands));
|
|
1367
|
+
/** Stores lifecycle hooks for component and relation events */
|
|
1368
|
+
hooks = /* @__PURE__ */ new Map();
|
|
1369
|
+
/**
|
|
1370
|
+
* Create a new World.
|
|
1371
|
+
* If an optional snapshot object is provided (previously produced by `world.serialize()`),
|
|
1372
|
+
* the world will be restored from that snapshot. The snapshot may contain non-JSON values.
|
|
1373
|
+
*/
|
|
1374
|
+
constructor(snapshot) {
|
|
1375
|
+
if (snapshot && typeof snapshot === "object") {
|
|
1376
|
+
if (snapshot.entityManager) this.entityIdManager.deserializeState(snapshot.entityManager);
|
|
1377
|
+
if (Array.isArray(snapshot.entities)) for (const entry of snapshot.entities) {
|
|
1378
|
+
const entityId = entry.id;
|
|
1379
|
+
const componentsArray = entry.components || [];
|
|
1380
|
+
const componentMap = /* @__PURE__ */ new Map();
|
|
1381
|
+
const componentTypes = [];
|
|
1382
|
+
for (const componentEntry of componentsArray) {
|
|
1383
|
+
const componentTypeRaw = componentEntry.type;
|
|
1384
|
+
let componentType;
|
|
1385
|
+
if (typeof componentTypeRaw === "number") componentType = componentTypeRaw;
|
|
1386
|
+
else if (typeof componentTypeRaw === "string") {
|
|
1387
|
+
const compId = getComponentIdByName(componentTypeRaw);
|
|
1388
|
+
if (compId === void 0) throw new Error(`Unknown component name in snapshot: ${componentTypeRaw}`);
|
|
1389
|
+
componentType = compId;
|
|
1390
|
+
} else if (typeof componentTypeRaw === "object" && componentTypeRaw !== null && typeof componentTypeRaw.component === "string") {
|
|
1391
|
+
const compId = getComponentIdByName(componentTypeRaw.component);
|
|
1392
|
+
if (compId === void 0) throw new Error(`Unknown component name in snapshot: ${componentTypeRaw.component}`);
|
|
1393
|
+
if (typeof componentTypeRaw.target === "string") {
|
|
1394
|
+
const targetCompId = getComponentIdByName(componentTypeRaw.target);
|
|
1395
|
+
if (targetCompId === void 0) throw new Error(`Unknown target component name in snapshot: ${componentTypeRaw.target}`);
|
|
1396
|
+
componentType = relation(compId, targetCompId);
|
|
1397
|
+
} else componentType = relation(compId, componentTypeRaw.target);
|
|
1398
|
+
} else throw new Error(`Invalid component type in snapshot: ${JSON.stringify(componentTypeRaw)}`);
|
|
1399
|
+
componentMap.set(componentType, componentEntry.value);
|
|
1400
|
+
componentTypes.push(componentType);
|
|
1401
|
+
}
|
|
1402
|
+
const archetype = this.ensureArchetype(componentTypes);
|
|
1403
|
+
archetype.addEntity(entityId, componentMap);
|
|
1404
|
+
this.entityToArchetype.set(entityId, archetype);
|
|
1405
|
+
for (const compType of componentTypes) {
|
|
1406
|
+
const detailedType = getDetailedIdType(compType);
|
|
1407
|
+
if (detailedType.type === "entity-relation") {
|
|
1408
|
+
const targetEntityId = detailedType.targetId;
|
|
1409
|
+
this.trackEntityReference(entityId, compType, targetEntityId);
|
|
1410
|
+
} else if (detailedType.type === "entity") this.trackEntityReference(entityId, compType, compType);
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
/**
|
|
1416
|
+
* Generate a signature string for component types array
|
|
1417
|
+
* @returns A string signature for the component types
|
|
1418
|
+
*/
|
|
1419
|
+
createArchetypeSignature(componentTypes) {
|
|
1420
|
+
return componentTypes.join(",");
|
|
1421
|
+
}
|
|
1422
|
+
/**
|
|
1423
|
+
* Create a new entity
|
|
1424
|
+
* @returns The ID of the newly created entity
|
|
1425
|
+
*/
|
|
1426
|
+
new() {
|
|
1427
|
+
const entityId = this.entityIdManager.allocate();
|
|
1428
|
+
let emptyArchetype = this.ensureArchetype([]);
|
|
1429
|
+
emptyArchetype.addEntity(entityId, /* @__PURE__ */ new Map());
|
|
1430
|
+
this.entityToArchetype.set(entityId, emptyArchetype);
|
|
1431
|
+
return entityId;
|
|
1432
|
+
}
|
|
1433
|
+
/**
|
|
1434
|
+
* Destroy an entity and remove all its components (immediate execution)
|
|
1435
|
+
*/
|
|
1436
|
+
destroyEntityImmediate(entityId) {
|
|
1437
|
+
const queue = [entityId];
|
|
1438
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1439
|
+
while (queue.length > 0) {
|
|
1440
|
+
const cur = queue.shift();
|
|
1441
|
+
if (visited.has(cur)) continue;
|
|
1442
|
+
visited.add(cur);
|
|
1443
|
+
const archetype = this.entityToArchetype.get(cur);
|
|
1444
|
+
if (!archetype) continue;
|
|
1445
|
+
const componentReferences = Array.from(this.getEntityReferences(cur));
|
|
1446
|
+
for (const [sourceEntityId, componentType] of componentReferences) {
|
|
1447
|
+
if (!this.entityToArchetype.get(sourceEntityId)) continue;
|
|
1448
|
+
if (isCascadeDeleteRelation(componentType)) {
|
|
1449
|
+
if (!visited.has(sourceEntityId)) queue.push(sourceEntityId);
|
|
1450
|
+
continue;
|
|
1451
|
+
}
|
|
1452
|
+
this.removeComponentImmediate(sourceEntityId, componentType, cur);
|
|
1453
|
+
}
|
|
1454
|
+
this.entityReferences.delete(cur);
|
|
1455
|
+
archetype.removeEntity(cur);
|
|
1456
|
+
this.entityToArchetype.delete(cur);
|
|
1457
|
+
this.cleanupArchetypesReferencingEntity(cur);
|
|
1458
|
+
this.entityIdManager.deallocate(cur);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
/**
|
|
1462
|
+
* Check if an entity exists
|
|
1463
|
+
*/
|
|
1464
|
+
exists(entityId) {
|
|
1465
|
+
return this.entityToArchetype.has(entityId);
|
|
1466
|
+
}
|
|
1467
|
+
set(entityId, componentType, component$1) {
|
|
1468
|
+
if (!this.exists(entityId)) throw new Error(`Entity ${entityId} does not exist`);
|
|
1469
|
+
const detailedType = getDetailedIdType(componentType);
|
|
1470
|
+
if (detailedType.type === "invalid") throw new Error(`Invalid component type: ${componentType}`);
|
|
1471
|
+
if (detailedType.type === "wildcard-relation") throw new Error(`Cannot directly add wildcard relation components: ${componentType}`);
|
|
1472
|
+
this.commandBuffer.set(entityId, componentType, component$1);
|
|
1473
|
+
}
|
|
1474
|
+
/**
|
|
1475
|
+
* Remove a component from an entity (deferred)
|
|
1476
|
+
*/
|
|
1477
|
+
remove(entityId, componentType) {
|
|
1478
|
+
if (!this.exists(entityId)) throw new Error(`Entity ${entityId} does not exist`);
|
|
1479
|
+
if (getDetailedIdType(componentType).type === "invalid") throw new Error(`Invalid component type: ${componentType}`);
|
|
1480
|
+
this.commandBuffer.remove(entityId, componentType);
|
|
1481
|
+
}
|
|
1482
|
+
/**
|
|
1483
|
+
* Destroy an entity and remove all its components (deferred)
|
|
1484
|
+
*/
|
|
1485
|
+
delete(entityId) {
|
|
1486
|
+
this.commandBuffer.delete(entityId);
|
|
1487
|
+
}
|
|
1488
|
+
/**
|
|
1489
|
+
* Check if an entity has a specific component
|
|
1490
|
+
*/
|
|
1491
|
+
has(entityId, componentType) {
|
|
1492
|
+
const archetype = this.entityToArchetype.get(entityId);
|
|
1493
|
+
if (!archetype) return false;
|
|
1494
|
+
if (archetype.componentTypes.includes(componentType)) return true;
|
|
1495
|
+
if (isDontFragmentRelation(componentType)) return this.dontFragmentRelations.get(entityId)?.has(componentType) ?? false;
|
|
1496
|
+
return false;
|
|
1497
|
+
}
|
|
1498
|
+
get(entityId, componentType) {
|
|
1499
|
+
const archetype = this.entityToArchetype.get(entityId);
|
|
1500
|
+
if (!archetype) throw new Error(`Entity ${entityId} does not exist`);
|
|
1501
|
+
if (componentType >= 0 || componentType % 2 ** 42 !== 0) {
|
|
1502
|
+
const inArchetype = archetype.componentTypes.includes(componentType);
|
|
1503
|
+
const hasDontFragment = isDontFragmentRelation(componentType);
|
|
1504
|
+
if (!(inArchetype || hasDontFragment && this.dontFragmentRelations.get(entityId)?.has(componentType))) throw new Error(`Entity ${entityId} does not have component ${componentType}. Use has() to check component existence before calling get().`);
|
|
1505
|
+
}
|
|
1506
|
+
return archetype.get(entityId, componentType);
|
|
1507
|
+
}
|
|
1508
|
+
/**
|
|
1509
|
+
* Register a lifecycle hook for component or wildcard relation events
|
|
1510
|
+
*/
|
|
1511
|
+
hook(componentType, hook) {
|
|
1512
|
+
if (!this.hooks.has(componentType)) this.hooks.set(componentType, /* @__PURE__ */ new Set());
|
|
1513
|
+
this.hooks.get(componentType).add(hook);
|
|
1514
|
+
if (hook.on_init !== void 0) this.archetypesByComponent.get(componentType)?.forEach((archetype) => {
|
|
1515
|
+
const entities = archetype.getEntityToIndexMap();
|
|
1516
|
+
const componentData = archetype.getComponentData(componentType);
|
|
1517
|
+
for (const [entity, index] of entities) {
|
|
1518
|
+
const data = componentData[index];
|
|
1519
|
+
const value = data === MISSING_COMPONENT ? void 0 : data;
|
|
1520
|
+
hook.on_init?.(entity, componentType, value);
|
|
1521
|
+
}
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1524
|
+
/**
|
|
1525
|
+
* Unregister a lifecycle hook for component or wildcard relation events
|
|
1526
|
+
*/
|
|
1527
|
+
unhook(componentType, hook) {
|
|
1528
|
+
const hooks = this.hooks.get(componentType);
|
|
1529
|
+
if (hooks) {
|
|
1530
|
+
hooks.delete(hook);
|
|
1531
|
+
if (hooks.size === 0) this.hooks.delete(componentType);
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
/**
|
|
1535
|
+
* Execute all deferred commands immediately
|
|
1536
|
+
*/
|
|
1537
|
+
sync() {
|
|
1538
|
+
this.commandBuffer.execute();
|
|
1539
|
+
}
|
|
1540
|
+
/**
|
|
1541
|
+
* Create a cached query for efficient entity lookups
|
|
1542
|
+
* @returns A Query object for the specified component types and filter
|
|
1543
|
+
*/
|
|
1544
|
+
createQuery(componentTypes, filter = {}) {
|
|
1545
|
+
const sortedTypes = [...componentTypes].sort((a, b) => a - b);
|
|
1546
|
+
const filterKey = serializeQueryFilter(filter);
|
|
1547
|
+
const key = `${this.createArchetypeSignature(sortedTypes)}${filterKey ? `|${filterKey}` : ""}`;
|
|
1548
|
+
const cached = this.queryCache.get(key);
|
|
1549
|
+
if (cached) {
|
|
1550
|
+
cached.refCount++;
|
|
1551
|
+
return cached.query;
|
|
1552
|
+
}
|
|
1553
|
+
const query = new Query(this, sortedTypes, filter);
|
|
1554
|
+
this.queryCache.set(key, {
|
|
1555
|
+
query,
|
|
1556
|
+
refCount: 1
|
|
1557
|
+
});
|
|
1558
|
+
return query;
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1561
|
+
* @internal Register a query for archetype update notifications
|
|
1562
|
+
*/
|
|
1563
|
+
_registerQuery(query) {
|
|
1564
|
+
this.queries.push(query);
|
|
1565
|
+
}
|
|
1566
|
+
/**
|
|
1567
|
+
* @internal Unregister a query
|
|
1568
|
+
*/
|
|
1569
|
+
_unregisterQuery(query) {
|
|
1570
|
+
const index = this.queries.indexOf(query);
|
|
1571
|
+
if (index !== -1) this.queries.splice(index, 1);
|
|
1572
|
+
}
|
|
1573
|
+
/**
|
|
1574
|
+
* Release a query reference obtained from createQuery.
|
|
1575
|
+
* Decrements the refCount and fully disposes the query when it reaches zero.
|
|
1576
|
+
*/
|
|
1577
|
+
releaseQuery(query) {
|
|
1578
|
+
for (const [k, v] of this.queryCache.entries()) if (v.query === query) {
|
|
1579
|
+
v.refCount--;
|
|
1580
|
+
if (v.refCount <= 0) {
|
|
1581
|
+
this.queryCache.delete(k);
|
|
1582
|
+
this._unregisterQuery(query);
|
|
1583
|
+
v.query._disposeInternal();
|
|
1584
|
+
}
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
/**
|
|
1589
|
+
* @internal Get archetypes that match specific component types (for internal use by queries)
|
|
1590
|
+
*/
|
|
1591
|
+
getMatchingArchetypes(componentTypes) {
|
|
1592
|
+
if (componentTypes.length === 0) return [...this.archetypes];
|
|
1593
|
+
const regularComponents = [];
|
|
1594
|
+
const wildcardRelations = [];
|
|
1595
|
+
for (const componentType of componentTypes) if (isWildcardRelationId(componentType)) {
|
|
1596
|
+
const componentId = getComponentIdFromRelationId(componentType);
|
|
1597
|
+
if (componentId !== void 0) wildcardRelations.push({
|
|
1598
|
+
componentId,
|
|
1599
|
+
relationId: componentType
|
|
1600
|
+
});
|
|
1601
|
+
} else regularComponents.push(componentType);
|
|
1602
|
+
let matchingArchetypes = [];
|
|
1603
|
+
if (regularComponents.length > 0) {
|
|
1604
|
+
const sortedRegularTypes = [...regularComponents].sort((a, b) => a - b);
|
|
1605
|
+
if (sortedRegularTypes.length === 1) {
|
|
1606
|
+
const componentType = sortedRegularTypes[0];
|
|
1607
|
+
matchingArchetypes = this.archetypesByComponent.get(componentType) || [];
|
|
1608
|
+
} else {
|
|
1609
|
+
const archetypeLists = sortedRegularTypes.map((type) => this.archetypesByComponent.get(type) || []);
|
|
1610
|
+
const firstList = archetypeLists[0] || [];
|
|
1611
|
+
const intersection = /* @__PURE__ */ new Set();
|
|
1612
|
+
for (const archetype of firstList) {
|
|
1613
|
+
let hasAllComponents = true;
|
|
1614
|
+
for (let listIndex = 1; listIndex < archetypeLists.length; listIndex++) if (!archetypeLists[listIndex].includes(archetype)) {
|
|
1615
|
+
hasAllComponents = false;
|
|
1616
|
+
break;
|
|
1617
|
+
}
|
|
1618
|
+
if (hasAllComponents) intersection.add(archetype);
|
|
1619
|
+
}
|
|
1620
|
+
matchingArchetypes = Array.from(intersection);
|
|
1621
|
+
}
|
|
1622
|
+
} else matchingArchetypes = [...this.archetypes];
|
|
1623
|
+
for (const wildcard of wildcardRelations) if (isDontFragmentComponent(wildcard.componentId)) {
|
|
1624
|
+
const archetypesWithMarker = this.archetypesByComponent.get(wildcard.relationId) || [];
|
|
1625
|
+
if (matchingArchetypes.length === 0) matchingArchetypes = archetypesWithMarker;
|
|
1626
|
+
else matchingArchetypes = matchingArchetypes.filter((archetype) => archetypesWithMarker.includes(archetype));
|
|
1627
|
+
} else matchingArchetypes = matchingArchetypes.filter((archetype) => archetype.hasRelationWithComponentId(wildcard.componentId));
|
|
1628
|
+
return matchingArchetypes;
|
|
1629
|
+
}
|
|
1630
|
+
query(componentTypes, includeComponents) {
|
|
1631
|
+
const matchingArchetypes = this.getMatchingArchetypes(componentTypes);
|
|
1632
|
+
if (includeComponents) {
|
|
1633
|
+
const result = [];
|
|
1634
|
+
for (const archetype of matchingArchetypes) {
|
|
1635
|
+
const entitiesWithData = archetype.getEntitiesWithComponents(componentTypes);
|
|
1636
|
+
result.push(...entitiesWithData);
|
|
1637
|
+
}
|
|
1638
|
+
return result;
|
|
1639
|
+
} else {
|
|
1640
|
+
const result = [];
|
|
1641
|
+
for (const archetype of matchingArchetypes) result.push(...archetype.getEntities());
|
|
1642
|
+
return result;
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
/**
|
|
1646
|
+
* @internal Execute commands for a single entity (for internal use by CommandBuffer)
|
|
1647
|
+
* @returns ComponentChangeset describing the changes made
|
|
1648
|
+
*/
|
|
1649
|
+
executeEntityCommands(entityId, commands) {
|
|
1650
|
+
const changeset = new ComponentChangeset();
|
|
1651
|
+
if (commands.some((cmd) => cmd.type === "destroy")) {
|
|
1652
|
+
this.destroyEntityImmediate(entityId);
|
|
1653
|
+
return changeset;
|
|
1654
|
+
}
|
|
1655
|
+
const currentArchetype = this.entityToArchetype.get(entityId);
|
|
1656
|
+
if (!currentArchetype) return changeset;
|
|
1657
|
+
this.processCommands(entityId, currentArchetype, commands, changeset);
|
|
1658
|
+
const removedComponents = this.applyChangeset(entityId, currentArchetype, changeset);
|
|
1659
|
+
this.updateEntityReferences(entityId, changeset);
|
|
1660
|
+
this.triggerLifecycleHooks(entityId, changeset.adds, removedComponents);
|
|
1661
|
+
return changeset;
|
|
1662
|
+
}
|
|
1663
|
+
/**
|
|
1664
|
+
* Process commands and populate the changeset
|
|
1665
|
+
*/
|
|
1666
|
+
processCommands(entityId, currentArchetype, commands, changeset) {
|
|
1667
|
+
for (const command of commands) if (command.type === "set" && command.componentType) this.processSetCommand(entityId, currentArchetype, command.componentType, command.component, changeset);
|
|
1668
|
+
else if (command.type === "delete" && command.componentType) this.processDeleteCommand(entityId, currentArchetype, command.componentType, changeset);
|
|
1669
|
+
}
|
|
1670
|
+
/**
|
|
1671
|
+
* Process a set command, handling exclusive relations
|
|
1672
|
+
*/
|
|
1673
|
+
processSetCommand(entityId, currentArchetype, componentType, component$1, changeset) {
|
|
1674
|
+
const componentId = getComponentIdFromRelationId(componentType);
|
|
1675
|
+
if (componentId !== void 0) {
|
|
1676
|
+
if (componentId !== void 0 && isExclusiveComponent(componentId)) this.removeExclusiveRelations(entityId, currentArchetype, componentId, changeset);
|
|
1677
|
+
if (componentId !== void 0 && isDontFragmentComponent(componentId)) {
|
|
1678
|
+
const wildcardMarker = relation(componentId, "*");
|
|
1679
|
+
if (!currentArchetype.componentTypes.includes(wildcardMarker)) changeset.set(wildcardMarker, void 0);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
changeset.set(componentType, component$1);
|
|
1683
|
+
}
|
|
1684
|
+
/**
|
|
1685
|
+
* Remove all relations with the same base component (for exclusive relations)
|
|
1686
|
+
*/
|
|
1687
|
+
removeExclusiveRelations(entityId, currentArchetype, baseComponentId, changeset) {
|
|
1688
|
+
for (const componentType of currentArchetype.componentTypes) if (this.isRelationWithComponent(componentType, baseComponentId)) changeset.delete(componentType);
|
|
1689
|
+
const entityData = currentArchetype.getEntity(entityId);
|
|
1690
|
+
if (entityData) for (const [componentType] of entityData) {
|
|
1691
|
+
if (currentArchetype.componentTypes.includes(componentType)) continue;
|
|
1692
|
+
if (this.isRelationWithComponent(componentType, baseComponentId)) changeset.delete(componentType);
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
isRelationWithComponent(componentType, baseComponentId) {
|
|
1696
|
+
return getComponentIdFromRelationId(componentType) === baseComponentId;
|
|
1697
|
+
}
|
|
1698
|
+
/**
|
|
1699
|
+
* Process a delete command, handling wildcard relations
|
|
1700
|
+
*/
|
|
1701
|
+
processDeleteCommand(entityId, currentArchetype, componentType, changeset) {
|
|
1702
|
+
const componentId = getComponentIdFromRelationId(componentType);
|
|
1703
|
+
if (isWildcardRelationId(componentType) && componentId !== void 0) this.removeWildcardRelations(entityId, currentArchetype, componentId, changeset);
|
|
1704
|
+
else {
|
|
1705
|
+
changeset.delete(componentType);
|
|
1706
|
+
if (componentId !== void 0 && isDontFragmentComponent(componentId)) {
|
|
1707
|
+
const wildcardMarker = relation(componentId, "*");
|
|
1708
|
+
const entityData = currentArchetype.getEntity(entityId);
|
|
1709
|
+
let hasOtherRelations = false;
|
|
1710
|
+
if (entityData) for (const [otherComponentType] of entityData) {
|
|
1711
|
+
if (otherComponentType === componentType) continue;
|
|
1712
|
+
if (otherComponentType === wildcardMarker) continue;
|
|
1713
|
+
if (changeset.removes.has(otherComponentType)) continue;
|
|
1714
|
+
if (getComponentIdFromRelationId(otherComponentType) === componentId) {
|
|
1715
|
+
hasOtherRelations = true;
|
|
1716
|
+
break;
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
if (!hasOtherRelations) changeset.delete(wildcardMarker);
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
/**
|
|
1724
|
+
* Remove all relations matching a wildcard component ID
|
|
1725
|
+
*/
|
|
1726
|
+
removeWildcardRelations(entityId, currentArchetype, baseComponentId, changeset) {
|
|
1727
|
+
for (const componentType of currentArchetype.componentTypes) if (this.isRelationWithComponent(componentType, baseComponentId)) changeset.delete(componentType);
|
|
1728
|
+
const entityData = currentArchetype.getEntity(entityId);
|
|
1729
|
+
if (entityData) for (const [componentType] of entityData) {
|
|
1730
|
+
if (currentArchetype.componentTypes.includes(componentType)) continue;
|
|
1731
|
+
if (this.isRelationWithComponent(componentType, baseComponentId)) changeset.delete(componentType);
|
|
1732
|
+
}
|
|
1733
|
+
if (isDontFragmentComponent(baseComponentId)) {
|
|
1734
|
+
const wildcardMarker = relation(baseComponentId, "*");
|
|
1735
|
+
changeset.delete(wildcardMarker);
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
/**
|
|
1739
|
+
* Remove a single component from an entity immediately, handling dontFragment relations correctly.
|
|
1740
|
+
* Used by destroyEntityImmediate for non-cascade relation cleanup.
|
|
1741
|
+
*/
|
|
1742
|
+
removeComponentImmediate(entityId, componentType, targetEntityId) {
|
|
1743
|
+
const sourceArchetype = this.entityToArchetype.get(entityId);
|
|
1744
|
+
if (!sourceArchetype) return;
|
|
1745
|
+
const changeset = new ComponentChangeset();
|
|
1746
|
+
const componentId = getComponentIdFromRelationId(componentType);
|
|
1747
|
+
changeset.delete(componentType);
|
|
1748
|
+
if (componentId !== void 0 && isDontFragmentComponent(componentId)) {
|
|
1749
|
+
const wildcardMarker = relation(componentId, "*");
|
|
1750
|
+
const entityData = sourceArchetype.getEntity(entityId);
|
|
1751
|
+
let hasOtherRelations = false;
|
|
1752
|
+
if (entityData) for (const [otherComponentType] of entityData) {
|
|
1753
|
+
if (otherComponentType === componentType) continue;
|
|
1754
|
+
if (otherComponentType === wildcardMarker) continue;
|
|
1755
|
+
if (getComponentIdFromRelationId(otherComponentType) === componentId) {
|
|
1756
|
+
hasOtherRelations = true;
|
|
1757
|
+
break;
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
if (!hasOtherRelations) changeset.delete(wildcardMarker);
|
|
1761
|
+
}
|
|
1762
|
+
const removedComponent = sourceArchetype.get(entityId, componentType);
|
|
1763
|
+
this.applyChangeset(entityId, sourceArchetype, changeset);
|
|
1764
|
+
this.untrackEntityReference(entityId, componentType, targetEntityId);
|
|
1765
|
+
this.triggerLifecycleHooks(entityId, /* @__PURE__ */ new Map(), new Map([[componentType, removedComponent]]));
|
|
1766
|
+
}
|
|
1767
|
+
/**
|
|
1768
|
+
* Apply changeset to entity, moving to new archetype if needed
|
|
1769
|
+
* @returns Map of removed components with their data
|
|
1770
|
+
*/
|
|
1771
|
+
applyChangeset(entityId, currentArchetype, changeset) {
|
|
1772
|
+
const currentEntityData = currentArchetype.getEntity(entityId);
|
|
1773
|
+
const allCurrentComponentTypes = currentEntityData ? Array.from(currentEntityData.keys()) : currentArchetype.componentTypes;
|
|
1774
|
+
const finalComponentTypes = changeset.getFinalComponentTypes(allCurrentComponentTypes);
|
|
1775
|
+
const removedComponents = /* @__PURE__ */ new Map();
|
|
1776
|
+
if (finalComponentTypes) {
|
|
1777
|
+
const currentRegularTypes = this.filterRegularComponentTypes(allCurrentComponentTypes);
|
|
1778
|
+
const finalRegularTypes = this.filterRegularComponentTypes(finalComponentTypes);
|
|
1779
|
+
if (!this.areComponentTypesEqual(currentRegularTypes, finalRegularTypes)) this.moveEntityToNewArchetype(entityId, currentArchetype, finalComponentTypes, changeset, removedComponents);
|
|
1780
|
+
else this.updateEntityInSameArchetype(entityId, currentArchetype, changeset, removedComponents);
|
|
1781
|
+
} else this.updateEntityInSameArchetype(entityId, currentArchetype, changeset, removedComponents);
|
|
1782
|
+
return removedComponents;
|
|
1783
|
+
}
|
|
1784
|
+
/**
|
|
1785
|
+
* Move entity to a new archetype with updated components
|
|
1786
|
+
*/
|
|
1787
|
+
moveEntityToNewArchetype(entityId, currentArchetype, finalComponentTypes, changeset, removedComponents) {
|
|
1788
|
+
const newArchetype = this.ensureArchetype(finalComponentTypes);
|
|
1789
|
+
const currentComponents = currentArchetype.removeEntity(entityId);
|
|
1790
|
+
for (const componentType of changeset.removes) removedComponents.set(componentType, currentComponents.get(componentType));
|
|
1791
|
+
newArchetype.addEntity(entityId, changeset.applyTo(currentComponents));
|
|
1792
|
+
this.entityToArchetype.set(entityId, newArchetype);
|
|
1793
|
+
}
|
|
1794
|
+
/**
|
|
1795
|
+
* Update entity in same archetype (no archetype change needed)
|
|
1796
|
+
*/
|
|
1797
|
+
updateEntityInSameArchetype(entityId, currentArchetype, changeset, removedComponents) {
|
|
1798
|
+
this.applyDontFragmentChanges(entityId, changeset, removedComponents);
|
|
1799
|
+
for (const [componentType, component$1] of changeset.adds) {
|
|
1800
|
+
if (isDontFragmentRelation(componentType)) continue;
|
|
1801
|
+
currentArchetype.set(entityId, componentType, component$1);
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
/**
|
|
1805
|
+
* Apply dontFragment relation changes directly to World's storage
|
|
1806
|
+
* This is much more efficient than the removeEntity + addEntity approach
|
|
1807
|
+
*/
|
|
1808
|
+
applyDontFragmentChanges(entityId, changeset, removedComponents) {
|
|
1809
|
+
let entityRelations = this.dontFragmentRelations.get(entityId);
|
|
1810
|
+
for (const componentType of changeset.removes) if (isDontFragmentRelation(componentType)) {
|
|
1811
|
+
if (entityRelations) {
|
|
1812
|
+
const removedValue = entityRelations.get(componentType);
|
|
1813
|
+
if (removedValue !== void 0 || entityRelations.has(componentType)) {
|
|
1814
|
+
removedComponents.set(componentType, removedValue);
|
|
1815
|
+
entityRelations.delete(componentType);
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
for (const [componentType, component$1] of changeset.adds) if (isDontFragmentRelation(componentType)) {
|
|
1820
|
+
if (!entityRelations) {
|
|
1821
|
+
entityRelations = /* @__PURE__ */ new Map();
|
|
1822
|
+
this.dontFragmentRelations.set(entityId, entityRelations);
|
|
1823
|
+
}
|
|
1824
|
+
entityRelations.set(componentType, component$1);
|
|
1825
|
+
}
|
|
1826
|
+
if (entityRelations && entityRelations.size === 0) this.dontFragmentRelations.delete(entityId);
|
|
1827
|
+
}
|
|
1828
|
+
/**
|
|
1829
|
+
* Update entity reference tracking based on changeset
|
|
1830
|
+
*/
|
|
1831
|
+
updateEntityReferences(entityId, changeset) {
|
|
1832
|
+
for (const componentType of changeset.removes) if (isEntityRelation(componentType)) {
|
|
1833
|
+
const targetId = getTargetIdFromRelationId(componentType);
|
|
1834
|
+
this.untrackEntityReference(entityId, componentType, targetId);
|
|
1835
|
+
} else if (componentType >= 1024) this.untrackEntityReference(entityId, componentType, componentType);
|
|
1836
|
+
for (const [componentType] of changeset.adds) if (isEntityRelation(componentType)) {
|
|
1837
|
+
const targetId = getTargetIdFromRelationId(componentType);
|
|
1838
|
+
this.trackEntityReference(entityId, componentType, targetId);
|
|
1839
|
+
} else if (componentType >= 1024) this.trackEntityReference(entityId, componentType, componentType);
|
|
1840
|
+
}
|
|
1841
|
+
/**
|
|
1842
|
+
* Get or create an archetype for the given component types
|
|
1843
|
+
* Filters out dontFragment relations from the archetype signature
|
|
1844
|
+
* @returns The archetype for the given component types (excluding dontFragment relations)
|
|
1845
|
+
*/
|
|
1846
|
+
ensureArchetype(componentTypes) {
|
|
1847
|
+
const sortedTypes = this.filterRegularComponentTypes(componentTypes).sort((a, b) => a - b);
|
|
1848
|
+
const hashKey = this.createArchetypeSignature(sortedTypes);
|
|
1849
|
+
return getOrCreateWithSideEffect(this.archetypeBySignature, hashKey, () => this.createNewArchetype(sortedTypes));
|
|
1850
|
+
}
|
|
1851
|
+
/**
|
|
1852
|
+
* Compare two arrays of component types for equality (order-independent)
|
|
1853
|
+
*/
|
|
1854
|
+
areComponentTypesEqual(types1, types2) {
|
|
1855
|
+
if (types1.length !== types2.length) return false;
|
|
1856
|
+
const sorted1 = [...types1].sort((a, b) => a - b);
|
|
1857
|
+
const sorted2 = [...types2].sort((a, b) => a - b);
|
|
1858
|
+
return sorted1.every((v, i) => v === sorted2[i]);
|
|
1859
|
+
}
|
|
1860
|
+
/**
|
|
1861
|
+
* Filter out dontFragment relations from component types, but keep wildcard markers
|
|
1862
|
+
*/
|
|
1863
|
+
filterRegularComponentTypes(componentTypes) {
|
|
1864
|
+
const regularTypes = [];
|
|
1865
|
+
for (const componentType of componentTypes) {
|
|
1866
|
+
if (isDontFragmentWildcard(componentType)) {
|
|
1867
|
+
regularTypes.push(componentType);
|
|
1868
|
+
continue;
|
|
1869
|
+
}
|
|
1870
|
+
if (isDontFragmentRelation(componentType)) continue;
|
|
1871
|
+
regularTypes.push(componentType);
|
|
1872
|
+
}
|
|
1873
|
+
return regularTypes;
|
|
1874
|
+
}
|
|
1875
|
+
/**
|
|
1876
|
+
* Create a new archetype and register it with all tracking structures
|
|
1877
|
+
*/
|
|
1878
|
+
createNewArchetype(componentTypes) {
|
|
1879
|
+
const newArchetype = new Archetype(componentTypes, this.dontFragmentRelations);
|
|
1880
|
+
this.archetypes.push(newArchetype);
|
|
1881
|
+
this.registerArchetypeInComponentIndex(newArchetype, componentTypes);
|
|
1882
|
+
this.notifyQueriesOfNewArchetype(newArchetype);
|
|
1883
|
+
return newArchetype;
|
|
1884
|
+
}
|
|
1885
|
+
/**
|
|
1886
|
+
* Register archetype in the component-to-archetype index
|
|
1887
|
+
*/
|
|
1888
|
+
registerArchetypeInComponentIndex(archetype, componentTypes) {
|
|
1889
|
+
for (const componentType of componentTypes) {
|
|
1890
|
+
const archetypes = this.archetypesByComponent.get(componentType) || [];
|
|
1891
|
+
archetypes.push(archetype);
|
|
1892
|
+
this.archetypesByComponent.set(componentType, archetypes);
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
/**
|
|
1896
|
+
* Notify all queries to check the new archetype
|
|
1897
|
+
*/
|
|
1898
|
+
notifyQueriesOfNewArchetype(archetype) {
|
|
1899
|
+
for (const query of this.queries) query.checkNewArchetype(archetype);
|
|
1900
|
+
}
|
|
1901
|
+
/**
|
|
1902
|
+
* Add a component reference to the reverse index when an entity is used as a component type
|
|
1903
|
+
* @param sourceEntityId The entity that has the component
|
|
1904
|
+
* @param componentType The component type (which may be an entity ID used as component type)
|
|
1905
|
+
* @param targetEntityId The entity being used as component type
|
|
1906
|
+
*/
|
|
1907
|
+
trackEntityReference(sourceEntityId, componentType, targetEntityId) {
|
|
1908
|
+
if (!this.entityReferences.has(targetEntityId)) this.entityReferences.set(targetEntityId, new MultiMap());
|
|
1909
|
+
this.entityReferences.get(targetEntityId).add(sourceEntityId, componentType);
|
|
1910
|
+
}
|
|
1911
|
+
/**
|
|
1912
|
+
* Remove a component reference from the reverse index
|
|
1913
|
+
* @param sourceEntityId The entity that has the component
|
|
1914
|
+
* @param componentType The component type
|
|
1915
|
+
* @param targetEntityId The entity being used as component type
|
|
1916
|
+
*/
|
|
1917
|
+
untrackEntityReference(sourceEntityId, componentType, targetEntityId) {
|
|
1918
|
+
const references = this.entityReferences.get(targetEntityId);
|
|
1919
|
+
if (references) {
|
|
1920
|
+
references.remove(sourceEntityId, componentType);
|
|
1921
|
+
if (references.keyCount === 0) this.entityReferences.delete(targetEntityId);
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
/**
|
|
1925
|
+
* Get all component references where a target entity is used as a component type
|
|
1926
|
+
* @param targetEntityId The target entity
|
|
1927
|
+
* @returns A MultiMap of sourceEntityId to componentTypes that reference the target entity
|
|
1928
|
+
*/
|
|
1929
|
+
getEntityReferences(targetEntityId) {
|
|
1930
|
+
return this.entityReferences.get(targetEntityId) ?? new MultiMap();
|
|
1931
|
+
}
|
|
1932
|
+
/**
|
|
1933
|
+
* Check if an archetype's signature references a specific entity
|
|
1934
|
+
* (via entity-relation targeting the entity, or using entity as component type)
|
|
1935
|
+
*/
|
|
1936
|
+
archetypeReferencesEntity(archetype, entityId) {
|
|
1937
|
+
for (const componentType of archetype.componentTypes) {
|
|
1938
|
+
if (componentType === entityId) return true;
|
|
1939
|
+
if (isEntityRelation(componentType)) {
|
|
1940
|
+
if (getTargetIdFromRelationId(componentType) === entityId) return true;
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
return false;
|
|
1944
|
+
}
|
|
1945
|
+
/**
|
|
1946
|
+
* Cleanup empty archetypes that reference a specific deleted entity
|
|
1947
|
+
* Only removes archetypes whose component types reference the entity
|
|
1948
|
+
*/
|
|
1949
|
+
cleanupArchetypesReferencingEntity(entityId) {
|
|
1950
|
+
for (let i = this.archetypes.length - 1; i >= 0; i--) {
|
|
1951
|
+
const archetype = this.archetypes[i];
|
|
1952
|
+
if (archetype.getEntities().length === 0 && this.archetypeReferencesEntity(archetype, entityId)) {
|
|
1953
|
+
this.removeArchetypeFromList(archetype);
|
|
1954
|
+
this.removeArchetypeFromSignatureMap(archetype);
|
|
1955
|
+
this.removeArchetypeFromComponentIndex(archetype);
|
|
1956
|
+
this.removeArchetypeFromQueries(archetype);
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
/**
|
|
1961
|
+
* Remove an empty archetype from all internal data structures
|
|
1962
|
+
*/
|
|
1963
|
+
cleanupEmptyArchetype(archetype) {
|
|
1964
|
+
if (archetype.getEntities().length > 0) return;
|
|
1965
|
+
this.removeArchetypeFromList(archetype);
|
|
1966
|
+
this.removeArchetypeFromSignatureMap(archetype);
|
|
1967
|
+
this.removeArchetypeFromComponentIndex(archetype);
|
|
1968
|
+
this.removeArchetypeFromQueries(archetype);
|
|
1969
|
+
}
|
|
1970
|
+
/**
|
|
1971
|
+
* Remove archetype from the main archetypes list
|
|
1972
|
+
*/
|
|
1973
|
+
removeArchetypeFromList(archetype) {
|
|
1974
|
+
const index = this.archetypes.indexOf(archetype);
|
|
1975
|
+
if (index !== -1) this.archetypes.splice(index, 1);
|
|
1976
|
+
}
|
|
1977
|
+
/**
|
|
1978
|
+
* Remove archetype from the signature-to-archetype map
|
|
1979
|
+
*/
|
|
1980
|
+
removeArchetypeFromSignatureMap(archetype) {
|
|
1981
|
+
const hashKey = this.createArchetypeSignature(archetype.componentTypes);
|
|
1982
|
+
this.archetypeBySignature.delete(hashKey);
|
|
1983
|
+
}
|
|
1984
|
+
/**
|
|
1985
|
+
* Remove archetype from the component-to-archetypes index
|
|
1986
|
+
*/
|
|
1987
|
+
removeArchetypeFromComponentIndex(archetype) {
|
|
1988
|
+
for (const componentType of archetype.componentTypes) {
|
|
1989
|
+
const archetypes = this.archetypesByComponent.get(componentType);
|
|
1990
|
+
if (archetypes) {
|
|
1991
|
+
const compIndex = archetypes.indexOf(archetype);
|
|
1992
|
+
if (compIndex !== -1) {
|
|
1993
|
+
archetypes.splice(compIndex, 1);
|
|
1994
|
+
if (archetypes.length === 0) this.archetypesByComponent.delete(componentType);
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
/**
|
|
2000
|
+
* Remove archetype from all queries
|
|
2001
|
+
*/
|
|
2002
|
+
removeArchetypeFromQueries(archetype) {
|
|
2003
|
+
for (const query of this.queries) query.removeArchetype(archetype);
|
|
2004
|
+
}
|
|
2005
|
+
/**
|
|
2006
|
+
* Execute component lifecycle hooks for added and removed components
|
|
2007
|
+
*/
|
|
2008
|
+
triggerLifecycleHooks(entityId, addedComponents, removedComponents) {
|
|
2009
|
+
for (const [componentType, component$1] of addedComponents) {
|
|
2010
|
+
const directHooks = this.hooks.get(componentType);
|
|
2011
|
+
if (directHooks) for (const lifecycleHook of directHooks) lifecycleHook.on_set?.(entityId, componentType, component$1);
|
|
2012
|
+
const componentId = getComponentIdFromRelationId(componentType);
|
|
2013
|
+
if (componentId !== void 0) {
|
|
2014
|
+
const wildcardRelationId = relation(componentId, "*");
|
|
2015
|
+
const wildcardHooks = this.hooks.get(wildcardRelationId);
|
|
2016
|
+
if (wildcardHooks) for (const lifecycleHook of wildcardHooks) lifecycleHook.on_set?.(entityId, componentType, component$1);
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
for (const [componentType, component$1] of removedComponents) {
|
|
2020
|
+
const directHooks = this.hooks.get(componentType);
|
|
2021
|
+
if (directHooks) for (const lifecycleHook of directHooks) lifecycleHook.on_remove?.(entityId, componentType, component$1);
|
|
2022
|
+
const componentId = getComponentIdFromRelationId(componentType);
|
|
2023
|
+
if (componentId !== void 0) {
|
|
2024
|
+
const wildcardRelationId = relation(componentId, "*");
|
|
2025
|
+
const wildcardHooks = this.hooks.get(wildcardRelationId);
|
|
2026
|
+
if (wildcardHooks) for (const hook of wildcardHooks) hook.on_remove?.(entityId, componentType, component$1);
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
/**
|
|
2031
|
+
* Convert the world into a plain snapshot object.
|
|
2032
|
+
* This returns an in-memory structure and does not perform JSON stringification.
|
|
2033
|
+
* Component values are stored as-is (they may be non-JSON-serializable).
|
|
2034
|
+
*/
|
|
2035
|
+
serialize() {
|
|
2036
|
+
const entities = [];
|
|
2037
|
+
for (const archetype of this.archetypes) {
|
|
2038
|
+
const dumpedEntities = archetype.dump();
|
|
2039
|
+
for (const { entity, components } of dumpedEntities) entities.push({
|
|
2040
|
+
id: entity,
|
|
2041
|
+
components: Array.from(components.entries()).map(([rawType, value]) => {
|
|
2042
|
+
const detailedType = getDetailedIdType(rawType);
|
|
2043
|
+
let type = rawType;
|
|
2044
|
+
let componentName;
|
|
2045
|
+
switch (detailedType.type) {
|
|
2046
|
+
case "component":
|
|
2047
|
+
type = getComponentNameById(rawType) || rawType;
|
|
2048
|
+
break;
|
|
2049
|
+
case "entity-relation":
|
|
2050
|
+
componentName = getComponentNameById(detailedType.componentId);
|
|
2051
|
+
if (componentName) type = {
|
|
2052
|
+
component: componentName,
|
|
2053
|
+
target: detailedType.targetId
|
|
2054
|
+
};
|
|
2055
|
+
break;
|
|
2056
|
+
case "component-relation":
|
|
2057
|
+
componentName = getComponentNameById(detailedType.componentId);
|
|
2058
|
+
if (componentName) type = {
|
|
2059
|
+
component: componentName,
|
|
2060
|
+
target: getComponentNameById(detailedType.targetId) || detailedType.targetId
|
|
2061
|
+
};
|
|
2062
|
+
break;
|
|
2063
|
+
}
|
|
2064
|
+
return {
|
|
2065
|
+
type,
|
|
2066
|
+
value: value === MISSING_COMPONENT ? void 0 : value
|
|
2067
|
+
};
|
|
2068
|
+
})
|
|
2069
|
+
});
|
|
2070
|
+
}
|
|
2071
|
+
return {
|
|
2072
|
+
version: 1,
|
|
2073
|
+
entityManager: this.entityIdManager.serializeState(),
|
|
2074
|
+
entities
|
|
2075
|
+
};
|
|
2076
|
+
}
|
|
2077
|
+
};
|
|
2078
|
+
|
|
2079
|
+
//#endregion
|
|
2080
|
+
export { getComponentIdByName as a, isEntityId as c, relation as d, decodeRelationId as i, isRelationId as l, Query as n, getComponentNameById as o, component as r, isComponentId as s, World as t, isWildcardRelationId as u };
|
|
2081
|
+
//# sourceMappingURL=world.mjs.map
|