@codehz/ecs 0.7.2 → 0.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/examples/advanced-scheduling.ts +96 -0
  2. package/examples/collision-detection.ts +229 -0
  3. package/examples/inventory-system-relations.ts +108 -0
  4. package/examples/parent-child-hierarchy.ts +206 -0
  5. package/examples/serialization.ts +337 -0
  6. package/examples/simple.ts +96 -0
  7. package/examples/spatial-grid.ts +276 -0
  8. package/examples/state-machine.ts +273 -0
  9. package/examples/tag-filtering.ts +266 -0
  10. package/package.json +60 -12
  11. package/src/__tests__/commands/buffer-limits.test.ts +72 -0
  12. package/src/__tests__/commands/buffer.test.ts +195 -0
  13. package/src/__tests__/component/singleton.test.ts +148 -0
  14. package/src/__tests__/core/archetype.test.ts +247 -0
  15. package/src/__tests__/core/bitset.test.ts +171 -0
  16. package/src/__tests__/core/changeset.test.ts +254 -0
  17. package/src/__tests__/core/multi-map.test.ts +74 -0
  18. package/src/__tests__/entity/component-registry.test.ts +66 -0
  19. package/src/__tests__/entity/entity.test.ts +520 -0
  20. package/src/__tests__/entity/id-manager.test.ts +157 -0
  21. package/src/__tests__/entity/id-system.test.ts +260 -0
  22. package/src/__tests__/perf/comprehensive.perf.test.ts +300 -0
  23. package/src/__tests__/perf/sync-hotpath.perf.test.ts +79 -0
  24. package/src/__tests__/query/basic.test.ts +341 -0
  25. package/src/__tests__/query/caching.test.ts +112 -0
  26. package/src/__tests__/query/filter.test.ts +111 -0
  27. package/src/__tests__/query/optional.test.ts +231 -0
  28. package/src/__tests__/query/perf.test.ts +99 -0
  29. package/src/__tests__/relations/dont-fragment/basic.test.ts +496 -0
  30. package/src/__tests__/relations/dont-fragment/query-notification.test.ts +125 -0
  31. package/src/__tests__/relations/wildcard.test.ts +179 -0
  32. package/src/__tests__/serialization/bounds.test.ts +237 -0
  33. package/src/__tests__/testing/assertions.test.ts +224 -0
  34. package/src/__tests__/testing/entity-builder.test.ts +84 -0
  35. package/src/__tests__/testing/snapshot.test.ts +150 -0
  36. package/src/__tests__/testing/world-fixture.test.ts +73 -0
  37. package/src/__tests__/world/component-hooks.test.ts +185 -0
  38. package/src/__tests__/world/component-management.test.ts +447 -0
  39. package/src/__tests__/world/entity-management.test.ts +86 -0
  40. package/src/__tests__/world/get-optional.test.ts +96 -0
  41. package/src/__tests__/world/multi-component-hooks.test.ts +502 -0
  42. package/src/__tests__/world/perf.test.ts +93 -0
  43. package/src/__tests__/world/query.test.ts +223 -0
  44. package/src/__tests__/world/serialize.test.ts +83 -0
  45. package/src/__tests__/world/wildcard-relation-hooks.test.ts +332 -0
  46. package/src/archetype/archetype.ts +472 -0
  47. package/src/archetype/helpers.ts +186 -0
  48. package/src/archetype/store.ts +33 -0
  49. package/src/commands/buffer.ts +110 -0
  50. package/src/commands/changeset.ts +104 -0
  51. package/src/component/entity-store.ts +223 -0
  52. package/src/component/registry.ts +657 -0
  53. package/src/component/type-utils.ts +9 -0
  54. package/src/entity/index.ts +63 -0
  55. package/src/entity/manager.ts +115 -0
  56. package/src/entity/relation.ts +319 -0
  57. package/src/entity/types.ts +135 -0
  58. package/src/index.ts +41 -0
  59. package/src/query/filter.ts +75 -0
  60. package/src/query/query.ts +313 -0
  61. package/src/query/registry.ts +101 -0
  62. package/src/storage/serialization.ts +130 -0
  63. package/src/testing/index.ts +634 -0
  64. package/src/types/index.ts +99 -0
  65. package/src/utils/bit-set.ts +133 -0
  66. package/src/utils/multi-map.ts +96 -0
  67. package/src/utils/utils.ts +19 -0
  68. package/src/world/builder.ts +100 -0
  69. package/src/world/commands.ts +378 -0
  70. package/src/world/hooks.ts +358 -0
  71. package/src/world/references.ts +38 -0
  72. package/src/world/serialization.ts +122 -0
  73. package/src/world/world.ts +1201 -0
  74. /package/{builder.d.mts → dist/builder.d.mts} +0 -0
  75. /package/{index.d.mts → dist/index.d.mts} +0 -0
  76. /package/{index.mjs → dist/index.mjs} +0 -0
  77. /package/{testing.d.mts → dist/testing.d.mts} +0 -0
  78. /package/{testing.mjs → dist/testing.mjs} +0 -0
  79. /package/{testing.mjs.map → dist/testing.mjs.map} +0 -0
  80. /package/{world.mjs → dist/world.mjs} +0 -0
  81. /package/{world.mjs.map → dist/world.mjs.map} +0 -0
@@ -0,0 +1,63 @@
1
+ // Re-export all types and functions from split modules for backwards compatibility
2
+
3
+ // Entity types and constants
4
+ export type {
5
+ ComponentId,
6
+ ComponentRelationId,
7
+ EntityId,
8
+ EntityRelationId,
9
+ RelationId,
10
+ WildcardRelationId,
11
+ } from "./types";
12
+
13
+ export {
14
+ COMPONENT_ID_MAX,
15
+ ENTITY_ID_START,
16
+ INVALID_COMPONENT_ID,
17
+ RELATION_SHIFT,
18
+ WILDCARD_TARGET_ID,
19
+ createComponentId,
20
+ createEntityId,
21
+ isComponentId,
22
+ isEntityId,
23
+ isRelationId,
24
+ isValidComponentId,
25
+ } from "./types";
26
+
27
+ // Relation functions
28
+ export {
29
+ decodeRelationId,
30
+ decodeRelationRaw,
31
+ getComponentIdFromRelationId,
32
+ getDetailedIdType,
33
+ getIdType,
34
+ getTargetIdFromRelationId,
35
+ inspectEntityId,
36
+ isAnyRelation,
37
+ isComponentRelation,
38
+ isEntityRelation,
39
+ isWildcardRelationId,
40
+ relation,
41
+ } from "./relation";
42
+
43
+ // Entity and component managers
44
+ export { ComponentIdAllocator, EntityIdManager } from "./manager";
45
+
46
+ // Component registry
47
+ export type { ComponentOptions } from "../component/registry";
48
+
49
+ export {
50
+ component,
51
+ getComponentIdByName,
52
+ getComponentMerge,
53
+ getComponentNameById,
54
+ getComponentOptions,
55
+ isCascadeDeleteComponent,
56
+ isCascadeDeleteRelation,
57
+ isDontFragmentComponent,
58
+ isDontFragmentRelation,
59
+ isDontFragmentWildcard,
60
+ isExclusiveComponent,
61
+ isExclusiveRelation,
62
+ isExclusiveWildcard,
63
+ } from "../component/registry";
@@ -0,0 +1,115 @@
1
+ import type { ComponentId, EntityId } from "./types";
2
+ import { COMPONENT_ID_MAX, ENTITY_ID_START, isEntityId } from "./types";
3
+
4
+ /**
5
+ * Entity ID Manager for automatic allocation and freelist recycling
6
+ */
7
+ export class EntityIdManager {
8
+ private nextId: number = ENTITY_ID_START;
9
+ /**
10
+ * Free list uses a stack (LIFO) for better memory locality when reusing IDs.
11
+ * We use an array instead of a Set for significantly better performance.
12
+ */
13
+ private freelist: EntityId[] = [];
14
+
15
+ /**
16
+ * Allocate a new entity ID
17
+ * Uses freelist if available, otherwise increments counter
18
+ */
19
+ allocate(): EntityId {
20
+ if (this.freelist.length > 0) {
21
+ return this.freelist.pop()!;
22
+ } else {
23
+ const id = this.nextId;
24
+ this.nextId++;
25
+ // Check for overflow (though unlikely in practice)
26
+ if (this.nextId >= Number.MAX_SAFE_INTEGER) {
27
+ throw new Error("Entity ID overflow: reached maximum safe integer");
28
+ }
29
+ return id as EntityId;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Deallocate an entity ID, adding it to the freelist for reuse
35
+ * @param id The entity ID to deallocate
36
+ */
37
+ deallocate(id: EntityId<any>): void {
38
+ if (!isEntityId(id)) {
39
+ throw new Error("Can only deallocate valid entity IDs");
40
+ }
41
+ if (id >= this.nextId) {
42
+ throw new Error("Cannot deallocate an ID that was never allocated");
43
+ }
44
+ this.freelist.push(id);
45
+ }
46
+
47
+ /**
48
+ * Get the current freelist size (for debugging/monitoring)
49
+ */
50
+ getFreelistSize(): number {
51
+ return this.freelist.length;
52
+ }
53
+
54
+ /**
55
+ * Get the next ID that would be allocated (for debugging)
56
+ */
57
+ getNextId(): number {
58
+ return this.nextId;
59
+ }
60
+
61
+ /**
62
+ * Serialize internal state for persistence.
63
+ * Returns a plain object representing allocator state. Values may be non-JSON-serializable.
64
+ */
65
+ serializeState(): { nextId: number; freelist: number[] } {
66
+ return { nextId: this.nextId, freelist: Array.from(this.freelist) };
67
+ }
68
+
69
+ /**
70
+ * Restore internal state from a previously-serialized object.
71
+ * Overwrites the current nextId and freelist.
72
+ */
73
+ deserializeState(state: { nextId: number; freelist?: number[] }): void {
74
+ if (typeof state.nextId !== "number") {
75
+ throw new Error("Invalid state for EntityIdManager.deserializeState");
76
+ }
77
+ this.nextId = state.nextId;
78
+ this.freelist = (state.freelist || []) as EntityId[];
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Component ID Manager for automatic allocation
84
+ * Components are typically registered once and not recycled
85
+ */
86
+ export class ComponentIdAllocator {
87
+ private nextId: number = 1;
88
+
89
+ /**
90
+ * Allocate a new component ID
91
+ * Increments counter sequentially from 1
92
+ */
93
+ allocate<T = void>(): ComponentId<T> {
94
+ if (this.nextId > COMPONENT_ID_MAX) {
95
+ throw new Error(`Component ID overflow: maximum ${COMPONENT_ID_MAX} components allowed`);
96
+ }
97
+ const id = this.nextId;
98
+ this.nextId++;
99
+ return id as ComponentId<T>;
100
+ }
101
+
102
+ /**
103
+ * Get the next ID that would be allocated (for debugging)
104
+ */
105
+ getNextId(): number {
106
+ return this.nextId;
107
+ }
108
+
109
+ /**
110
+ * Check if more component IDs are available
111
+ */
112
+ hasAvailableIds(): boolean {
113
+ return this.nextId <= COMPONENT_ID_MAX;
114
+ }
115
+ }
@@ -0,0 +1,319 @@
1
+ import type {
2
+ ComponentId,
3
+ ComponentRelationId,
4
+ EntityId,
5
+ EntityRelationId,
6
+ RelationId,
7
+ WildcardRelationId,
8
+ } from "./types";
9
+ import {
10
+ ENTITY_ID_START,
11
+ isComponentId,
12
+ isEntityId,
13
+ isValidComponentId,
14
+ RELATION_SHIFT,
15
+ WILDCARD_TARGET_ID,
16
+ } from "./types";
17
+
18
+ /**
19
+ * Internal function to decode a relation ID into raw component and target IDs
20
+ * @param id The EntityId to decode
21
+ * @returns Object with componentId and targetId, or null if not a relation
22
+ */
23
+ export function decodeRelationRaw(id: EntityId<any>): { componentId: number; targetId: number } | null {
24
+ if (id >= 0) return null;
25
+ const absId = -id;
26
+ const componentId = Math.floor(absId / RELATION_SHIFT);
27
+ const targetId = absId % RELATION_SHIFT;
28
+ return { componentId, targetId };
29
+ }
30
+
31
+ /**
32
+ * Type for relation ID based on component and target types
33
+ */
34
+ type RelationIdType<T, R> =
35
+ R extends ComponentId<infer U>
36
+ ? U extends void
37
+ ? ComponentRelationId<T>
38
+ : ComponentRelationId<T extends void ? U : T>
39
+ : R extends EntityId<any>
40
+ ? EntityRelationId<T>
41
+ : never;
42
+
43
+ /**
44
+ * Create a relation ID by associating a component with a target entity, component, or wildcard.
45
+ *
46
+ * Relations are encoded as negative numbers and can be used anywhere a regular component ID is accepted.
47
+ * Use `"*"` as the target to create a wildcard relation for querying all targets of a given relation type.
48
+ *
49
+ * @param componentId - The base component ID (must be a valid component)
50
+ * @param targetId - The target entity ID, component ID, or `"*"` for wildcard
51
+ * @returns A relation ID that encodes both the component and target
52
+ *
53
+ * @throws {Error} If `componentId` is not a valid component ID
54
+ * @throws {Error} If `targetId` is not a valid entity, component, or `"*"`
55
+ *
56
+ * @example
57
+ * const ChildOf = component();
58
+ * const parent = world.new();
59
+ *
60
+ * // Entity relation
61
+ * const childRelation = relation(ChildOf, parent);
62
+ * world.set(child, childRelation);
63
+ *
64
+ * // Wildcard relation (queries all targets)
65
+ * const allChildren = world.createQuery([relation(ChildOf, "*")]);
66
+ */
67
+ export function relation<T>(componentId: ComponentId<T>, targetId: "*"): WildcardRelationId<T>;
68
+ export function relation<T, R extends EntityId<any>>(componentId: ComponentId<T>, targetId: R): RelationIdType<T, R>;
69
+ export function relation<T>(componentId: ComponentId<T>, targetId: EntityId<any> | "*"): EntityId<any> {
70
+ if (!isComponentId(componentId)) {
71
+ throw new Error("First argument must be a valid component ID");
72
+ }
73
+
74
+ let actualTargetId: number;
75
+ if (targetId === "*") {
76
+ actualTargetId = WILDCARD_TARGET_ID;
77
+ } else {
78
+ if (!isEntityId(targetId) && !isComponentId(targetId)) {
79
+ throw new Error("Second argument must be a valid entity ID, component ID, or '*'");
80
+ }
81
+ actualTargetId = targetId;
82
+ }
83
+
84
+ // Encode: negative number with component_id * 2^42 + target_id
85
+ return -(componentId * RELATION_SHIFT + actualTargetId) as EntityId<any>;
86
+ }
87
+
88
+ /**
89
+ * Check if an ID is a wildcard relation (created with `relation(componentId, "*")`).
90
+ *
91
+ * @param id - The ID to check
92
+ * @returns `true` if the ID is a wildcard relation, `false` otherwise
93
+ */
94
+ export function isWildcardRelationId<T>(id: EntityId<T>): id is WildcardRelationId<T> {
95
+ const decoded = decodeRelationRaw(id);
96
+ return decoded !== null && decoded.targetId === WILDCARD_TARGET_ID;
97
+ }
98
+
99
+ /**
100
+ * Decode a relation ID into component and target IDs
101
+ * @param relationId The relation ID (must be negative)
102
+ * @returns Object with componentId, targetId, and relation type
103
+ */
104
+ export function decodeRelationId(relationId: RelationId<any>): {
105
+ componentId: ComponentId<any>;
106
+ targetId: EntityId<any>;
107
+ type: "entity" | "component" | "wildcard";
108
+ } {
109
+ const decoded = decodeRelationRaw(relationId);
110
+ if (decoded === null) {
111
+ throw new Error("ID is not a relation ID");
112
+ }
113
+
114
+ const { componentId: rawComponentId, targetId: rawTargetId } = decoded;
115
+ if (!isValidComponentId(rawComponentId)) {
116
+ throw new Error("Invalid component ID in relation");
117
+ }
118
+
119
+ const componentId = rawComponentId as ComponentId<any>;
120
+ const targetId = rawTargetId as EntityId<any>;
121
+
122
+ // Determine type based on targetId range
123
+ if (targetId === WILDCARD_TARGET_ID) {
124
+ return { componentId, targetId, type: "wildcard" };
125
+ } else if (isEntityId(targetId)) {
126
+ return { componentId, targetId, type: "entity" };
127
+ } else if (isComponentId(targetId)) {
128
+ return { componentId, targetId, type: "component" };
129
+ } else {
130
+ throw new Error("Invalid target ID in relation");
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Get the string representation of an ID type
136
+ */
137
+ export function getIdType(
138
+ id: EntityId<any>,
139
+ ): "component" | "entity" | "entity-relation" | "component-relation" | "wildcard-relation" | "invalid" {
140
+ if (isComponentId(id)) return "component";
141
+ if (isEntityId(id)) return "entity";
142
+
143
+ if (id < 0) {
144
+ const decoded = decodeRelationRaw(id as RelationId<any>);
145
+ if (decoded === null) return "invalid";
146
+
147
+ const { componentId: rawComponentId, targetId: rawTargetId } = decoded;
148
+
149
+ // Validate component ID
150
+ if (!isValidComponentId(rawComponentId)) return "invalid";
151
+
152
+ // Determine type based on targetId range
153
+ if (rawTargetId === WILDCARD_TARGET_ID) {
154
+ return "wildcard-relation";
155
+ } else if (isEntityId(rawTargetId as EntityId<any>)) {
156
+ return "entity-relation";
157
+ } else if (isComponentId(rawTargetId as ComponentId<any>)) {
158
+ return "component-relation";
159
+ } else {
160
+ return "invalid";
161
+ }
162
+ }
163
+
164
+ return "invalid"; // fallback for unknown/invalid IDs
165
+ }
166
+
167
+ /**
168
+ * Get detailed type information for an EntityId
169
+ * @param id The EntityId to analyze
170
+ * @returns Detailed type information including relation subtypes
171
+ */
172
+ export function getDetailedIdType(id: EntityId<any>):
173
+ | {
174
+ type: "component" | "entity" | "invalid";
175
+ componentId?: never;
176
+ targetId?: never;
177
+ }
178
+ | {
179
+ type: "entity-relation" | "wildcard-relation";
180
+ componentId: ComponentId<any>;
181
+ targetId: EntityId<any>;
182
+ }
183
+ | {
184
+ type: "component-relation";
185
+ componentId: ComponentId<any>;
186
+ targetId: ComponentId<any>;
187
+ } {
188
+ if (isComponentId(id)) {
189
+ return { type: "component" };
190
+ }
191
+
192
+ if (isEntityId(id)) {
193
+ return { type: "entity" };
194
+ }
195
+
196
+ if (id < 0) {
197
+ const decoded = decodeRelationRaw(id as RelationId<any>);
198
+ if (decoded === null) {
199
+ return { type: "invalid" };
200
+ }
201
+
202
+ const { componentId: rawComponentId, targetId: rawTargetId } = decoded;
203
+
204
+ // Validate component ID
205
+ if (!isValidComponentId(rawComponentId)) {
206
+ return { type: "invalid" };
207
+ }
208
+
209
+ const componentId = rawComponentId as ComponentId<any>;
210
+ const targetId = rawTargetId as EntityId<any>;
211
+
212
+ // Determine type based on targetId range
213
+ if (targetId === WILDCARD_TARGET_ID) {
214
+ return { type: "wildcard-relation", componentId, targetId };
215
+ } else if (isEntityId(targetId)) {
216
+ return { type: "entity-relation", componentId, targetId };
217
+ } else if (isComponentId(targetId as any)) {
218
+ return { type: "component-relation", componentId, targetId: targetId as ComponentId<any> };
219
+ } else {
220
+ return { type: "invalid" };
221
+ }
222
+ }
223
+
224
+ // Unknown/invalid ID
225
+ return { type: "invalid" };
226
+ }
227
+
228
+ /**
229
+ * Inspect an EntityId and return a human-readable string representation
230
+ * @param id The EntityId to inspect
231
+ * @returns A friendly string representation of the ID
232
+ */
233
+ export function inspectEntityId(id: EntityId<any>): string {
234
+ if (id === 0) {
235
+ return "Invalid Component ID (0)";
236
+ }
237
+
238
+ if (isComponentId(id)) {
239
+ return `Component ID (${id})`;
240
+ }
241
+
242
+ if (isEntityId(id)) {
243
+ return `Entity ID (${id})`;
244
+ }
245
+
246
+ if (id < 0) {
247
+ const decoded = decodeRelationRaw(id as RelationId<any>);
248
+ if (decoded === null) {
249
+ return `Invalid Relation ID (${id})`;
250
+ }
251
+
252
+ const { componentId: rawComponentId, targetId: rawTargetId } = decoded;
253
+
254
+ // Validate component ID
255
+ if (!isValidComponentId(rawComponentId)) {
256
+ return `Invalid Relation ID (${id})`;
257
+ }
258
+
259
+ // Determine target type and format output
260
+ const componentStr = `Component ID (${rawComponentId})`;
261
+ let targetStr: string;
262
+
263
+ if (rawTargetId === WILDCARD_TARGET_ID) {
264
+ targetStr = "Wildcard (*)";
265
+ } else if (isEntityId(rawTargetId as EntityId<any>)) {
266
+ targetStr = `Entity ID (${rawTargetId})`;
267
+ } else if (isComponentId(rawTargetId as ComponentId<any>)) {
268
+ targetStr = `Component ID (${rawTargetId})`;
269
+ } else {
270
+ return `Invalid Relation ID (${id})`;
271
+ }
272
+
273
+ return `Relation ID: ${componentStr} -> ${targetStr}`;
274
+ }
275
+
276
+ return `Unknown ID (${id})`;
277
+ }
278
+
279
+ /**
280
+ * Get the componentId from a relation ID without fully decoding the relation.
281
+ * Returns undefined for non-relation IDs or invalid component IDs.
282
+ */
283
+ export function getComponentIdFromRelationId<T>(id: EntityId<T>): ComponentId<T> | undefined {
284
+ const decoded = decodeRelationRaw(id);
285
+ if (decoded === null || !isValidComponentId(decoded.componentId)) return undefined;
286
+ return decoded.componentId as ComponentId<T>;
287
+ }
288
+
289
+ /**
290
+ * Get the targetId from a relation ID without fully decoding the relation.
291
+ * Returns undefined for non-relation IDs.
292
+ */
293
+ export function getTargetIdFromRelationId(id: EntityId<any>): EntityId<any> | undefined {
294
+ const decoded = decodeRelationRaw(id);
295
+ return decoded?.targetId as EntityId<any>;
296
+ }
297
+
298
+ /**
299
+ * Check if an ID is an entity-relation (relation targeting an entity, not a component or wildcard)
300
+ */
301
+ export function isEntityRelation(id: EntityId<any>): boolean {
302
+ const decoded = decodeRelationRaw(id);
303
+ return decoded !== null && decoded.targetId >= ENTITY_ID_START;
304
+ }
305
+
306
+ /**
307
+ * Check if an ID is a component-relation (relation targeting a component)
308
+ */
309
+ export function isComponentRelation(id: EntityId<any>): boolean {
310
+ const decoded = decodeRelationRaw(id);
311
+ return decoded !== null && isValidComponentId(decoded.targetId);
312
+ }
313
+
314
+ /**
315
+ * Check if an ID is any type of relation (entity, component, or wildcard)
316
+ */
317
+ export function isAnyRelation(id: EntityId<any>): boolean {
318
+ return id < 0;
319
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Unique symbol brand for associating component type information with EntityId
3
+ */
4
+ declare const __componentTypeMarker: unique symbol;
5
+
6
+ /**
7
+ * Unique symbol brand for tagging the kind of EntityId (e.g., 'component', 'entity-relation')
8
+ */
9
+ declare const __entityIdTypeTag: unique symbol;
10
+
11
+ /**
12
+ * Entity ID type for ECS architecture
13
+ * Based on 52-bit integers within safe integer range
14
+ * - Component IDs: 1-1023
15
+ * - Entity IDs: 1024+
16
+ * - Relation IDs: negative numbers encoding component and entity associations
17
+ */
18
+ /**
19
+ * Branded numeric type representing an ECS identifier.
20
+ *
21
+ * - {@link ComponentId}: positive values in range `1–1023`
22
+ * - Entity IDs: values `1024+`
23
+ * - {@link RelationId}: negative values encoding `(componentId, targetId)`
24
+ *
25
+ * @template T - The data type associated with this ID
26
+ * @template U - Discriminant for the ID kind (e.g. `"component"`, `"entity-relation"`)
27
+ */
28
+ export type EntityId<T = unknown, U = unknown> = number & {
29
+ readonly [__componentTypeMarker]: T;
30
+ readonly [__entityIdTypeTag]: U;
31
+ };
32
+
33
+ /**
34
+ * Component identifier. Valid values are `1` through `1023`.
35
+ * Created with {@link component}.
36
+ *
37
+ * @template T - The data type stored by this component (`void` for tag components)
38
+ */
39
+ export type ComponentId<T = void> = EntityId<T, "component">;
40
+
41
+ /**
42
+ * Relation identifier targeting an entity.
43
+ * Created with {@link relation}.
44
+ *
45
+ * @template T - The data type stored by this relation
46
+ */
47
+ export type EntityRelationId<T = void> = EntityId<T, "entity-relation">;
48
+
49
+ /**
50
+ * Relation identifier targeting another component (singleton relation).
51
+ * Created with {@link relation}.
52
+ *
53
+ * @template T - The data type stored by this relation
54
+ */
55
+ export type ComponentRelationId<T = void> = EntityId<T, "component-relation">;
56
+
57
+ /**
58
+ * Wildcard relation identifier used to query all targets of a given relation component.
59
+ * Created with `relation(componentId, "*")`.
60
+ *
61
+ * @template T - The data type stored by the relation
62
+ */
63
+ export type WildcardRelationId<T = void> = EntityId<T, "wildcard-relation">;
64
+
65
+ /**
66
+ * Union of all relation identifier kinds.
67
+ *
68
+ * @template T - The data type stored by the relation
69
+ */
70
+ export type RelationId<T = void> = EntityRelationId<T> | ComponentRelationId<T> | WildcardRelationId<T>;
71
+
72
+ /**
73
+ * Constants for ID ranges
74
+ */
75
+ export const INVALID_COMPONENT_ID = 0;
76
+ export const COMPONENT_ID_MAX = 1023;
77
+ export const ENTITY_ID_START = 1024;
78
+
79
+ /**
80
+ * Constants for relation ID encoding
81
+ */
82
+ export const RELATION_SHIFT = 2 ** 42;
83
+ export const WILDCARD_TARGET_ID = 0;
84
+
85
+ /**
86
+ * Check if a component ID is valid (1-1023)
87
+ */
88
+ export function isValidComponentId(componentId: number): boolean {
89
+ return componentId >= 1 && componentId <= COMPONENT_ID_MAX;
90
+ }
91
+
92
+ /**
93
+ * Check if an ID is a component ID
94
+ */
95
+ export function isComponentId<T>(id: EntityId<T>): id is ComponentId<T> {
96
+ return id >= 1 && id <= COMPONENT_ID_MAX;
97
+ }
98
+
99
+ /**
100
+ * Check if an ID is an entity ID
101
+ */
102
+ export function isEntityId<T>(id: EntityId<T>): id is EntityId<T> {
103
+ return id >= ENTITY_ID_START;
104
+ }
105
+
106
+ /**
107
+ * Check if an ID is a relation ID
108
+ */
109
+ export function isRelationId<T>(id: EntityId<T>): id is RelationId<T> {
110
+ return id < 0;
111
+ }
112
+
113
+ /**
114
+ * Create a component ID
115
+ * @param id Component identifier (1-1023)
116
+ * @internal This function is for internal use and testing only. Use `component()` to create components.
117
+ * @see component
118
+ */
119
+ export function createComponentId<T = void>(id: number): ComponentId<T> {
120
+ if (id < 1 || id > COMPONENT_ID_MAX) {
121
+ throw new Error(`Component ID must be between 1 and ${COMPONENT_ID_MAX}`);
122
+ }
123
+ return id as ComponentId<T>;
124
+ }
125
+
126
+ /**
127
+ * Create an entity ID
128
+ * @param id Entity identifier (starting from 1024)
129
+ */
130
+ export function createEntityId(id: number): EntityId {
131
+ if (id < ENTITY_ID_START) {
132
+ throw new Error(`Entity ID must be ${ENTITY_ID_START} or greater`);
133
+ }
134
+ return id as EntityId;
135
+ }
package/src/index.ts ADDED
@@ -0,0 +1,41 @@
1
+ // ECS Library Entry Point - Public API
2
+
3
+ // Entity ID types and utilities
4
+ export type {
5
+ ComponentId,
6
+ ComponentOptions,
7
+ ComponentRelationId,
8
+ EntityId,
9
+ EntityRelationId,
10
+ RelationId,
11
+ WildcardRelationId,
12
+ } from "./entity";
13
+
14
+ export {
15
+ component,
16
+ decodeRelationId,
17
+ getComponentIdByName,
18
+ getComponentNameById,
19
+ isComponentId,
20
+ isEntityId,
21
+ isRelationId,
22
+ isWildcardRelationId,
23
+ relation,
24
+ } from "./entity";
25
+
26
+ // World class
27
+ export type {
28
+ SerializedComponent,
29
+ SerializedEntity,
30
+ SerializedEntityId,
31
+ SerializedWorld,
32
+ } from "./storage/serialization";
33
+ export { EntityBuilder } from "./world/builder";
34
+ export type { ComponentDef } from "./world/builder";
35
+ export { World } from "./world/world";
36
+
37
+ // Query class
38
+ export { Query } from "./query/query";
39
+
40
+ // Type utilities
41
+ export type { ComponentTuple, ComponentType, LifecycleCallback, LifecycleHook } from "./types";