@codehz/ecs 0.7.1 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{builder.d.mts → dist/builder.d.mts} +4 -2
- package/{world.mjs → dist/world.mjs} +9 -30
- package/dist/world.mjs.map +1 -0
- package/examples/advanced-scheduling.ts +96 -0
- package/examples/collision-detection.ts +229 -0
- package/examples/inventory-system-relations.ts +108 -0
- package/examples/parent-child-hierarchy.ts +206 -0
- package/examples/serialization.ts +337 -0
- package/examples/simple.ts +96 -0
- package/examples/spatial-grid.ts +276 -0
- package/examples/state-machine.ts +273 -0
- package/examples/tag-filtering.ts +266 -0
- package/package.json +58 -12
- package/src/__tests__/commands/buffer-limits.test.ts +72 -0
- package/src/__tests__/commands/buffer.test.ts +195 -0
- package/src/__tests__/component/singleton.test.ts +148 -0
- package/src/__tests__/core/archetype.test.ts +247 -0
- package/src/__tests__/core/bitset.test.ts +171 -0
- package/src/__tests__/core/changeset.test.ts +254 -0
- package/src/__tests__/core/multi-map.test.ts +74 -0
- package/src/__tests__/entity/component-registry.test.ts +66 -0
- package/src/__tests__/entity/entity.test.ts +520 -0
- package/src/__tests__/entity/id-manager.test.ts +157 -0
- package/src/__tests__/entity/id-system.test.ts +260 -0
- package/src/__tests__/perf/comprehensive.perf.test.ts +300 -0
- package/src/__tests__/perf/sync-hotpath.perf.test.ts +79 -0
- package/src/__tests__/query/basic.test.ts +341 -0
- package/src/__tests__/query/caching.test.ts +112 -0
- package/src/__tests__/query/filter.test.ts +111 -0
- package/src/__tests__/query/optional.test.ts +231 -0
- package/src/__tests__/query/perf.test.ts +99 -0
- package/src/__tests__/relations/dont-fragment/basic.test.ts +496 -0
- package/src/__tests__/relations/dont-fragment/query-notification.test.ts +125 -0
- package/src/__tests__/relations/wildcard.test.ts +179 -0
- package/src/__tests__/serialization/bounds.test.ts +237 -0
- package/src/__tests__/testing/assertions.test.ts +224 -0
- package/src/__tests__/testing/entity-builder.test.ts +84 -0
- package/src/__tests__/testing/snapshot.test.ts +150 -0
- package/src/__tests__/testing/world-fixture.test.ts +73 -0
- package/src/__tests__/world/component-hooks.test.ts +185 -0
- package/src/__tests__/world/component-management.test.ts +447 -0
- package/src/__tests__/world/entity-management.test.ts +86 -0
- package/src/__tests__/world/get-optional.test.ts +96 -0
- package/src/__tests__/world/multi-component-hooks.test.ts +502 -0
- package/src/__tests__/world/perf.test.ts +93 -0
- package/src/__tests__/world/query.test.ts +223 -0
- package/src/__tests__/world/serialize.test.ts +83 -0
- package/src/__tests__/world/wildcard-relation-hooks.test.ts +332 -0
- package/src/archetype/archetype.ts +472 -0
- package/src/archetype/helpers.ts +186 -0
- package/src/archetype/store.ts +33 -0
- package/src/commands/buffer.ts +110 -0
- package/src/commands/changeset.ts +104 -0
- package/src/component/entity-store.ts +223 -0
- package/src/component/registry.ts +657 -0
- package/src/component/type-utils.ts +9 -0
- package/src/entity/index.ts +63 -0
- package/src/entity/manager.ts +115 -0
- package/src/entity/relation.ts +319 -0
- package/src/entity/types.ts +135 -0
- package/src/index.ts +41 -0
- package/src/query/filter.ts +75 -0
- package/src/query/query.ts +313 -0
- package/src/query/registry.ts +101 -0
- package/src/storage/serialization.ts +130 -0
- package/src/testing/index.ts +634 -0
- package/src/types/index.ts +99 -0
- package/src/utils/bit-set.ts +133 -0
- package/src/utils/multi-map.ts +96 -0
- package/src/utils/utils.ts +19 -0
- package/src/world/builder.ts +100 -0
- package/src/world/commands.ts +378 -0
- package/src/world/hooks.ts +358 -0
- package/src/world/references.ts +38 -0
- package/src/world/serialization.ts +122 -0
- package/src/world/world.ts +1201 -0
- package/world.mjs.map +0 -1
- /package/{index.d.mts → dist/index.d.mts} +0 -0
- /package/{index.mjs → dist/index.mjs} +0 -0
- /package/{testing.d.mts → dist/testing.d.mts} +0 -0
- /package/{testing.mjs → dist/testing.mjs} +0 -0
- /package/{testing.mjs.map → dist/testing.mjs.map} +0 -0
|
@@ -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";
|