@codehz/ecs 0.5.5 → 0.6.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 +42 -58
- package/builder.d.mts +21 -16
- package/index.d.mts +2 -2
- package/package.json +1 -1
- package/testing.d.mts +2 -2
- package/testing.mjs.map +1 -1
- package/world.mjs +51 -21
- package/world.mjs.map +1 -1
package/README.md
CHANGED
|
@@ -55,42 +55,7 @@ query.forEach([PositionId, VelocityId], (entity, position, velocity) => {
|
|
|
55
55
|
|
|
56
56
|
### 组件生命周期钩子
|
|
57
57
|
|
|
58
|
-
ECS
|
|
59
|
-
|
|
60
|
-
- `entityId`: 实体的 ID (number)
|
|
61
|
-
- `componentType`: 组件类型 ID (EntityId)
|
|
62
|
-
- `component`: 组件数据值 (T)
|
|
63
|
-
|
|
64
|
-
```typescript
|
|
65
|
-
// 注册组件生命周期钩子
|
|
66
|
-
world.hook(PositionId, {
|
|
67
|
-
on_init: (entityId, componentType, component) => {
|
|
68
|
-
// 当钩子注册时,为现有实体上的组件调用
|
|
69
|
-
console.log(`现有组件 ${componentType} 在实体 ${entityId}`);
|
|
70
|
-
},
|
|
71
|
-
on_set: (entityId, componentType, component) => {
|
|
72
|
-
console.log(`组件 ${componentType} 被添加到实体 ${entityId}`);
|
|
73
|
-
},
|
|
74
|
-
on_remove: (entityId, componentType, component) => {
|
|
75
|
-
console.log(`组件 ${componentType} 被从实体 ${entityId} 移除`);
|
|
76
|
-
},
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
// 你也可以只注册其中一个钩子
|
|
80
|
-
world.hook(VelocityId, {
|
|
81
|
-
on_remove: (entityId, componentType, component) => {
|
|
82
|
-
console.log(`组件 ${componentType} 被从实体 ${entityId} 移除`);
|
|
83
|
-
},
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
// 添加组件时会触发钩子
|
|
87
|
-
world.set(entity, PositionId, { x: 0, y: 0 });
|
|
88
|
-
world.sync(); // 钩子在这里被调用
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
### 多组件生命周期钩子
|
|
92
|
-
|
|
93
|
-
ECS 还支持多组件生命周期钩子,可以监听多个组件同时存在于实体时的事件。只有当所有必需组件都存在时才会触发回调。
|
|
58
|
+
ECS 支持监听组件的生命周期事件。可以监听单个组件或多个组件同时存在于实体时的事件。
|
|
94
59
|
|
|
95
60
|
```typescript
|
|
96
61
|
// 定义组件类型
|
|
@@ -101,22 +66,20 @@ type Velocity = { x: number; y: number };
|
|
|
101
66
|
const PositionId = component<Position>();
|
|
102
67
|
const VelocityId = component<Velocity>();
|
|
103
68
|
|
|
104
|
-
//
|
|
105
|
-
world.hook([PositionId, VelocityId], {
|
|
106
|
-
on_init: (entityId,
|
|
69
|
+
// 注册生命周期钩子,返回卸载函数
|
|
70
|
+
const unhook = world.hook([PositionId, VelocityId], {
|
|
71
|
+
on_init: (entityId, position, velocity) => {
|
|
107
72
|
// 当钩子注册时,为已同时拥有 Position 和 Velocity 组件的实体调用
|
|
108
73
|
console.log(`实体 ${entityId} 同时拥有 Position 和 Velocity 组件`);
|
|
109
74
|
},
|
|
110
|
-
on_set: (entityId,
|
|
75
|
+
on_set: (entityId, position, velocity) => {
|
|
111
76
|
// 当实体同时拥有 Position 和 Velocity 组件时调用
|
|
112
|
-
const [position, velocity] = components;
|
|
113
77
|
console.log(
|
|
114
78
|
`实体 ${entityId} 现在同时拥有 Position (${position.x}, ${position.y}) 和 Velocity (${velocity.x}, ${velocity.y})`,
|
|
115
79
|
);
|
|
116
80
|
},
|
|
117
|
-
on_remove: (entityId,
|
|
81
|
+
on_remove: (entityId, position, velocity) => {
|
|
118
82
|
// 当实体失去 Position 或 Velocity 组件之一时调用(如果之前同时拥有两者)
|
|
119
|
-
const [position, velocity] = components; // 移除前的组件值快照
|
|
120
83
|
console.log(`实体 ${entityId} 失去了 Position 或 Velocity 组件`);
|
|
121
84
|
},
|
|
122
85
|
});
|
|
@@ -125,17 +88,33 @@ world.hook([PositionId, VelocityId], {
|
|
|
125
88
|
const entity = world.new();
|
|
126
89
|
world.set(entity, PositionId, { x: 0, y: 0 });
|
|
127
90
|
world.set(entity, VelocityId, { x: 1, y: 0.5 });
|
|
128
|
-
world.sync(); //
|
|
91
|
+
world.sync(); // 钩子在这里被调用
|
|
92
|
+
|
|
93
|
+
// 不再需要时,调用卸载函数移除钩子
|
|
94
|
+
unhook();
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`hook()` 也支持只监听单个组件:
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
// 监听单个组件
|
|
101
|
+
const unhook = world.hook([PositionId], {
|
|
102
|
+
on_set: (entityId, position) => {
|
|
103
|
+
console.log(`组件 Position 被添加到实体 ${entityId}`);
|
|
104
|
+
},
|
|
105
|
+
on_remove: (entityId, position) => {
|
|
106
|
+
console.log(`组件 Position 被从实体 ${entityId} 移除`);
|
|
107
|
+
},
|
|
108
|
+
});
|
|
129
109
|
```
|
|
130
110
|
|
|
131
111
|
还可以使用可选组件,这样即使某些组件不存在也会触发钩子:
|
|
132
112
|
|
|
133
113
|
```typescript
|
|
134
|
-
//
|
|
135
|
-
world.hook([PositionId, { optional: VelocityId }], {
|
|
136
|
-
on_set: (entityId,
|
|
114
|
+
// 注册包含可选组件的生命周期钩子
|
|
115
|
+
const unhook = world.hook([PositionId, { optional: VelocityId }], {
|
|
116
|
+
on_set: (entityId, position, velocity) => {
|
|
137
117
|
// 当实体拥有 Position 组件时调用,Velocity 组件可选
|
|
138
|
-
const [position, velocity] = components;
|
|
139
118
|
if (velocity !== undefined) {
|
|
140
119
|
console.log(`实体 ${entityId} 拥有 Position 和 Velocity 组件`);
|
|
141
120
|
} else {
|
|
@@ -145,9 +124,9 @@ world.hook([PositionId, { optional: VelocityId }], {
|
|
|
145
124
|
});
|
|
146
125
|
```
|
|
147
126
|
|
|
148
|
-
###
|
|
127
|
+
### 通配符关系钩子
|
|
149
128
|
|
|
150
|
-
ECS
|
|
129
|
+
ECS 支持通配符关系钩子,可以监听特定组件的所有关系变化:
|
|
151
130
|
|
|
152
131
|
```typescript
|
|
153
132
|
import { World, component, relation } from "@codehz/ecs";
|
|
@@ -167,13 +146,16 @@ const entity = world.new();
|
|
|
167
146
|
// 创建通配符关系ID,用于监听所有 Position 相关的关系
|
|
168
147
|
const wildcardPositionRelation = relation(PositionId, "*");
|
|
169
148
|
|
|
170
|
-
//
|
|
171
|
-
world.hook(wildcardPositionRelation, {
|
|
172
|
-
on_set: (entityId,
|
|
173
|
-
console.log(
|
|
149
|
+
// 注册通配符关系钩子,返回卸载函数
|
|
150
|
+
const unhook = world.hook([wildcardPositionRelation], {
|
|
151
|
+
on_set: (entityId, relations) => {
|
|
152
|
+
console.log(`实体 ${entityId} 添加了 Position 关系`);
|
|
153
|
+
for (const [targetId, position] of relations) {
|
|
154
|
+
console.log(` -> 目标实体 ${targetId}:`, position);
|
|
155
|
+
}
|
|
174
156
|
},
|
|
175
|
-
on_remove: (entityId,
|
|
176
|
-
console.log(
|
|
157
|
+
on_remove: (entityId, relations) => {
|
|
158
|
+
console.log(`实体 ${entityId} 移除了 Position 关系`);
|
|
177
159
|
},
|
|
178
160
|
});
|
|
179
161
|
|
|
@@ -182,6 +164,9 @@ const entity2 = world.new();
|
|
|
182
164
|
const positionRelation = relation(PositionId, entity2);
|
|
183
165
|
world.set(entity, positionRelation, { x: 10, y: 20 });
|
|
184
166
|
world.sync(); // 通配符钩子会被触发
|
|
167
|
+
|
|
168
|
+
// 不再需要时移除钩子
|
|
169
|
+
unhook();
|
|
185
170
|
```
|
|
186
171
|
|
|
187
172
|
### Exclusive Relations
|
|
@@ -241,8 +226,7 @@ bun run examples/simple/demo.ts
|
|
|
241
226
|
- `delete(entity)`: 销毁实体及其所有组件
|
|
242
227
|
- `query(componentIds)`: 快速查询具有指定组件的实体
|
|
243
228
|
- `createQuery(componentIds)`: 创建可重用的查询对象
|
|
244
|
-
- `hook(
|
|
245
|
-
- `unhook(componentId, hook)`: 注销组件或通配符关系生命周期钩子
|
|
229
|
+
- `hook(componentIds, hook)`: 注册生命周期钩子,返回卸载函数
|
|
246
230
|
- `serialize()`: 序列化世界状态为快照对象
|
|
247
231
|
- `sync()`: 执行所有延迟命令
|
|
248
232
|
|
package/builder.d.mts
CHANGED
|
@@ -175,7 +175,7 @@ interface Command {
|
|
|
175
175
|
/**
|
|
176
176
|
* Hook types for component lifecycle events
|
|
177
177
|
*/
|
|
178
|
-
interface
|
|
178
|
+
interface LegacyLifecycleHook<T = unknown> {
|
|
179
179
|
/**
|
|
180
180
|
* Called when a component is added to an entity
|
|
181
181
|
*/
|
|
@@ -189,21 +189,21 @@ interface LifecycleHook<T = unknown> {
|
|
|
189
189
|
*/
|
|
190
190
|
on_remove?: (entityId: EntityId, componentType: EntityId<T>, component: T) => void;
|
|
191
191
|
}
|
|
192
|
-
interface
|
|
193
|
-
on_init?: (entityId: EntityId,
|
|
194
|
-
on_set?: (entityId: EntityId,
|
|
195
|
-
on_remove?: (entityId: EntityId,
|
|
192
|
+
interface LifecycleHook<T extends readonly ComponentType<any>[]> {
|
|
193
|
+
on_init?: (entityId: EntityId, ...components: ComponentTuple<T>) => void;
|
|
194
|
+
on_set?: (entityId: EntityId, ...components: ComponentTuple<T>) => void;
|
|
195
|
+
on_remove?: (entityId: EntityId, ...components: ComponentTuple<T>) => void;
|
|
196
196
|
}
|
|
197
197
|
/**
|
|
198
198
|
* Convenience function type for single component lifecycle events
|
|
199
199
|
* Combines on_init, on_set, and on_remove into a single callback
|
|
200
200
|
*/
|
|
201
|
-
type
|
|
201
|
+
type LegacyLifecycleCallback<T = unknown> = (type: "init" | "set" | "remove", entityId: EntityId, componentType: EntityId<T>, component: T) => void;
|
|
202
202
|
/**
|
|
203
203
|
* Convenience function type for multi-component lifecycle events
|
|
204
204
|
* Combines on_init, on_set, and on_remove into a single callback
|
|
205
205
|
*/
|
|
206
|
-
type
|
|
206
|
+
type LifecycleCallback<T extends readonly ComponentType<any>[]> = (type: "init" | "set" | "remove", entityId: EntityId, ...components: ComponentTuple<T>) => void;
|
|
207
207
|
type ComponentType<T> = EntityId<T> | OptionalEntityId<T>;
|
|
208
208
|
type OptionalEntityId<T> = {
|
|
209
209
|
optional: EntityId<T>;
|
|
@@ -217,11 +217,11 @@ type ComponentTypeToData<T> = T extends {
|
|
|
217
217
|
* Type helper for component tuples extracted from EntityId array
|
|
218
218
|
*/
|
|
219
219
|
type ComponentTuple<T extends readonly ComponentType<any>[]> = { readonly [K in keyof T]: ComponentTypeToData<T[K]> };
|
|
220
|
-
interface
|
|
220
|
+
interface LifecycleHookEntry {
|
|
221
221
|
componentTypes: readonly ComponentType<any>[];
|
|
222
222
|
requiredComponents: EntityId<any>[];
|
|
223
223
|
optionalComponents: EntityId<any>[];
|
|
224
|
-
hook:
|
|
224
|
+
hook: LifecycleHook<any>;
|
|
225
225
|
}
|
|
226
226
|
//#endregion
|
|
227
227
|
//#region src/core/archetype.d.ts
|
|
@@ -260,7 +260,7 @@ declare class Archetype {
|
|
|
260
260
|
/**
|
|
261
261
|
* Multi-hooks that match this archetype
|
|
262
262
|
*/
|
|
263
|
-
readonly matchingMultiHooks: Set<
|
|
263
|
+
readonly matchingMultiHooks: Set<LifecycleHookEntry>;
|
|
264
264
|
/**
|
|
265
265
|
* Cache for pre-computed component data sources to avoid repeated calculations
|
|
266
266
|
*/
|
|
@@ -429,8 +429,8 @@ declare class World {
|
|
|
429
429
|
private queries;
|
|
430
430
|
private queryCache;
|
|
431
431
|
private commandBuffer;
|
|
432
|
+
private legacyHooks;
|
|
432
433
|
private hooks;
|
|
433
|
-
private multiHooks;
|
|
434
434
|
constructor(snapshot?: SerializedWorld);
|
|
435
435
|
private deserializeSnapshot;
|
|
436
436
|
private createArchetypeSignature;
|
|
@@ -447,10 +447,15 @@ declare class World {
|
|
|
447
447
|
getOptional<T>(entityId: EntityId, componentType: EntityId<T>): {
|
|
448
448
|
value: T;
|
|
449
449
|
} | undefined;
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
450
|
+
/**
|
|
451
|
+
* @deprecated use array overload with LifecycleCallback
|
|
452
|
+
*/
|
|
453
|
+
hook<T>(componentType: EntityId<T>, hook: LegacyLifecycleHook<T> | LegacyLifecycleCallback<T>): () => void;
|
|
454
|
+
hook<const T extends readonly ComponentType<any>[]>(componentTypes: T, hook: LifecycleHook<T> | LifecycleCallback<T>): () => void;
|
|
455
|
+
/** @deprecated use the unsubscribe function returned by hook() instead */
|
|
456
|
+
unhook<T>(componentType: EntityId<T>, hook: LegacyLifecycleHook<T>): void;
|
|
457
|
+
/** @deprecated use the unsubscribe function returned by hook() instead */
|
|
458
|
+
unhook<const T extends readonly ComponentType<any>[]>(componentTypes: T, hook: LifecycleHook<T>): void;
|
|
454
459
|
sync(): void;
|
|
455
460
|
createQuery(componentTypes: EntityId<any>[], filter?: QueryFilter): Query;
|
|
456
461
|
spawn(): EntityBuilder;
|
|
@@ -510,5 +515,5 @@ declare class EntityBuilder {
|
|
|
510
515
|
build(): EntityId;
|
|
511
516
|
}
|
|
512
517
|
//#endregion
|
|
513
|
-
export { isRelationId as A, ComponentRelationId as C, WildcardRelationId as D, RelationId as E, isComponentId as O, ComponentId as S, EntityRelationId as T, getComponentIdByName as _, SerializedEntity as a, isWildcardRelationId as b, Query as c,
|
|
518
|
+
export { isRelationId as A, ComponentRelationId as C, WildcardRelationId as D, RelationId as E, isComponentId as O, ComponentId as S, EntityRelationId as T, getComponentIdByName as _, SerializedEntity as a, isWildcardRelationId as b, Query as c, LegacyLifecycleCallback as d, LegacyLifecycleHook as f, component as g, ComponentOptions as h, SerializedComponent as i, isEntityId as k, ComponentTuple as l, LifecycleHook as m, EntityBuilder as n, SerializedEntityId as o, LifecycleCallback as p, World as r, SerializedWorld as s, ComponentDef as t, ComponentType as u, getComponentNameById as v, EntityId as w, relation as x, decodeRelationId as y };
|
|
514
519
|
//# sourceMappingURL=builder.d.mts.map
|
package/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { A as isRelationId, C as ComponentRelationId, D as WildcardRelationId, E as RelationId, O as isComponentId, S as ComponentId, T as EntityRelationId, _ as getComponentIdByName, a as SerializedEntity, b as isWildcardRelationId, c as Query, d as
|
|
2
|
-
export { type ComponentDef, type ComponentId, type ComponentOptions, type ComponentRelationId, type ComponentTuple, type ComponentType, EntityBuilder, type EntityId, type EntityRelationId, type LifecycleCallback, type LifecycleHook, type MultiLifecycleCallback, type MultiLifecycleHook, Query, type RelationId, type SerializedComponent, type SerializedEntity, type SerializedEntityId, type SerializedWorld, type WildcardRelationId, World, component, decodeRelationId, getComponentIdByName, getComponentNameById, isComponentId, isEntityId, isRelationId, isWildcardRelationId, relation };
|
|
1
|
+
import { A as isRelationId, C as ComponentRelationId, D as WildcardRelationId, E as RelationId, O as isComponentId, S as ComponentId, T as EntityRelationId, _ as getComponentIdByName, a as SerializedEntity, b as isWildcardRelationId, c as Query, d as LegacyLifecycleCallback, f as LegacyLifecycleHook, g as component, h as ComponentOptions, i as SerializedComponent, k as isEntityId, l as ComponentTuple, m as LifecycleHook, n as EntityBuilder, o as SerializedEntityId, p as LifecycleCallback, r as World, s as SerializedWorld, t as ComponentDef, u as ComponentType, v as getComponentNameById, w as EntityId, x as relation, y as decodeRelationId } from "./builder.mjs";
|
|
2
|
+
export { type ComponentDef, type ComponentId, type ComponentOptions, type ComponentRelationId, type ComponentTuple, type ComponentType, EntityBuilder, type EntityId, type EntityRelationId, type LegacyLifecycleCallback as LifecycleCallback, type LegacyLifecycleHook as LifecycleHook, type LifecycleCallback as MultiLifecycleCallback, type LifecycleHook as MultiLifecycleHook, Query, type RelationId, type SerializedComponent, type SerializedEntity, type SerializedEntityId, type SerializedWorld, type WildcardRelationId, World, component, decodeRelationId, getComponentIdByName, getComponentNameById, isComponentId, isEntityId, isRelationId, isWildcardRelationId, relation };
|
package/package.json
CHANGED
package/testing.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { D as WildcardRelationId, E as RelationId, S as ComponentId, c as Query, d as
|
|
1
|
+
import { D as WildcardRelationId, E as RelationId, S as ComponentId, c as Query, d as LegacyLifecycleCallback, f as LegacyLifecycleHook, g as component, m as LifecycleHook, n as EntityBuilder, p as LifecycleCallback, r as World, t as ComponentDef, w as EntityId, x as relation } from "./builder.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/testing/index.d.ts
|
|
4
4
|
|
|
@@ -254,5 +254,5 @@ declare class AssertionError extends Error {
|
|
|
254
254
|
constructor(message: string);
|
|
255
255
|
}
|
|
256
256
|
//#endregion
|
|
257
|
-
export { AssertionError, Assertions, type ComponentDef, type ComponentId, EntityBuilder, type EntityId, EntitySnapshot, type LifecycleCallback, type LifecycleHook, type MultiLifecycleCallback, type MultiLifecycleHook, type Query, type RelationId, Snapshot, SnapshotDiff, type WildcardRelationId, World, WorldFixture, WorldSnapshot, component, relation };
|
|
257
|
+
export { AssertionError, Assertions, type ComponentDef, type ComponentId, EntityBuilder, type EntityId, EntitySnapshot, type LegacyLifecycleCallback as LifecycleCallback, type LegacyLifecycleHook as LifecycleHook, type LifecycleCallback as MultiLifecycleCallback, type LifecycleHook as MultiLifecycleHook, type Query, type RelationId, Snapshot, SnapshotDiff, type WildcardRelationId, World, WorldFixture, WorldSnapshot, component, relation };
|
|
258
258
|
//# sourceMappingURL=testing.d.mts.map
|
package/testing.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.mjs","names":["entitySnapshots: EntitySnapshot[]","addedEntities: EntityId[]","removedEntities: EntityId[]","componentChanges: SnapshotDiff[\"componentChanges\"]","result: Record<string, unknown>"],"sources":["../src/testing/index.ts"],"sourcesContent":["/**\n * @module testing\n * Testing utilities for ECS-based game logic\n *\n * This module provides framework-agnostic testing helpers that work with\n * bun:test, vitest, jest, or any other testing framework.\n *\n * @example\n * ```typescript\n * import { describe, expect, it } from \"bun:test\";\n * import { component } from \"@codehz/ecs\";\n * import { WorldFixture, EntityBuilder, Assertions } from \"@codehz/ecs/testing\";\n *\n * const PositionId = component<{ x: number; y: number }>();\n * const VelocityId = component<{ x: number; y: number }>();\n *\n * describe(\"Movement System\", () => {\n * it(\"should update position based on velocity\", () => {\n * const fixture = new WorldFixture();\n * const entity = fixture\n * .spawn()\n * .with(PositionId, { x: 0, y: 0 })\n * .with(VelocityId, { x: 1, y: 2 })\n * .build();\n *\n * // Run your game logic here\n * movementSystem(fixture.world, 1.0);\n *\n * expect(Assertions.hasComponent(fixture.world, entity, PositionId)).toBe(true);\n * expect(Assertions.getComponent(fixture.world, entity, PositionId)).toEqual({ x: 1, y: 2 });\n * });\n * });\n * ```\n */\n\nimport type { ComponentId, EntityId, WildcardRelationId } from \"../core/entity\";\nimport { isWildcardRelationId, relation } from \"../core/entity\";\nimport { World } from \"../core/world\";\nimport type { Query } from \"../query/query\";\nexport { EntityBuilder } from \"../core/builder\";\nexport type { ComponentDef } from \"../core/builder\";\n\n// =============================================================================\n// Types\n// =============================================================================\n\n/**\n * A component definition for entity building, supporting both regular components and relations\n */\nimport type { EntityBuilder } from \"../core/builder\";\n\n/**\n * Snapshot of a single entity's component state\n */\nexport interface EntitySnapshot {\n entity: EntityId;\n components: Map<EntityId<any>, unknown>;\n}\n\n/**\n * Snapshot of multiple entities' component state\n */\nexport interface WorldSnapshot {\n entities: EntitySnapshot[];\n}\n\n/**\n * Result of comparing two snapshots\n */\nexport interface SnapshotDiff {\n /** Entities that exist in 'after' but not in 'before' */\n addedEntities: EntityId[];\n /** Entities that exist in 'before' but not in 'after' */\n removedEntities: EntityId[];\n /** Changes to components on existing entities */\n componentChanges: Array<{\n entity: EntityId;\n componentId: EntityId<any>;\n before: unknown | undefined;\n after: unknown | undefined;\n changeType: \"added\" | \"removed\" | \"modified\";\n }>;\n}\n\n// =============================================================================\n// WorldFixture - Test World Factory\n// =============================================================================\n\n/**\n * A test fixture that manages a World instance and provides convenient\n * methods for setting up test scenarios.\n *\n * @example\n * ```typescript\n * const fixture = new WorldFixture();\n *\n * // Spawn entities with fluent API\n * const player = fixture\n * .spawn()\n * .with(PositionId, { x: 0, y: 0 })\n * .with(HealthId, { current: 100, max: 100 })\n * .build();\n *\n * // Access the world for running systems\n * movementSystem(fixture.world);\n *\n * // Clean up (optional - creates a fresh world)\n * fixture.reset();\n * ```\n */\nexport class WorldFixture {\n private _world: World;\n private _queries: Query[] = [];\n\n constructor() {\n this._world = new World();\n }\n\n /**\n * Get the underlying World instance\n */\n get world(): World {\n return this._world;\n }\n\n /**\n * Create a new EntityBuilder for spawning an entity with components\n */\n spawn(): EntityBuilder {\n return this._world.spawn();\n }\n\n /**\n * Spawn multiple entities with the same component configuration\n * @param count Number of entities to spawn\n * @param configure Function to configure each entity builder\n * @returns Array of created entity IDs\n */\n spawnMany(count: number, configure: (builder: EntityBuilder, index: number) => EntityBuilder): EntityId[] {\n return this._world.spawnMany(count, configure);\n }\n\n /**\n * Create a query and track it for automatic cleanup\n * @param componentTypes Component types to query for\n * @returns Query instance\n */\n createQuery(componentTypes: EntityId<any>[]): Query {\n const query = this._world.createQuery(componentTypes);\n this._queries.push(query);\n return query;\n }\n\n /**\n * Execute pending commands (alias for world.sync())\n */\n sync(): void {\n this._world.sync();\n }\n\n /**\n * Reset the fixture with a fresh World instance\n * Disposes all tracked queries\n */\n reset(): void {\n for (const query of this._queries) {\n query.dispose();\n }\n this._queries = [];\n this._world = new World();\n }\n\n /**\n * Capture a snapshot of specified entities and their components\n * @param entities Entities to capture\n * @param componentIds Components to include in the snapshot\n */\n captureSnapshot(entities: EntityId[], componentIds: EntityId<any>[]): WorldSnapshot {\n return Snapshot.capture(this._world, entities, componentIds);\n }\n\n /**\n * Symbol.dispose implementation for automatic resource management\n */\n [Symbol.dispose](): void {\n this.reset();\n }\n}\n\n// =============================================================================\n// EntityBuilder - Fluent Entity Creation\n// =============================================================================\n\n/**\n * Fluent builder for creating entities with components.\n * Supports both regular components and entity relations.\n *\n * @example\n * ```typescript\n * // Basic usage\n * // Note: build() will enqueue component commands but will NOT call world.sync().\n * // You must call world.sync() or fixture.sync() manually to apply commands.\n * const entity = new EntityBuilder(world)\n * .with(PositionId, { x: 10, y: 20 })\n * .with(VelocityId, { x: 1, y: 0 })\n * .build();\n * // Apply pending changes\n * world.sync();\n *\n * // With relations\n * const child = new EntityBuilder(world)\n * .with(PositionId, { x: 0, y: 0 })\n * .withRelation(ParentId, parentEntity, { offset: { x: 5, y: 5 } })\n * .build();\n * world.sync();\n *\n * // Tag component (void type)\n * const tagged = new EntityBuilder(world)\n * .withTag(PlayerTagId)\n * .build();\n * ```\n */\n// EntityBuilder is exported from world.ts; testing utilities will use world.spawn()\n\n// =============================================================================\n// Assertions - Test Assertion Helpers\n// =============================================================================\n\n/**\n * Test assertion utilities that return boolean values or throw descriptive errors.\n * These work with any testing framework's expect() function.\n *\n * @example\n * ```typescript\n * // With bun:test or vitest\n * expect(Assertions.hasComponent(world, entity, PositionId)).toBe(true);\n * expect(Assertions.getComponent(world, entity, PositionId)).toEqual({ x: 10, y: 20 });\n *\n * // Direct assertion (throws on failure)\n * Assertions.assertHasComponent(world, entity, PositionId);\n * Assertions.assertComponentEquals(world, entity, PositionId, { x: 10, y: 20 });\n * ```\n */\nexport const Assertions = {\n /**\n * Check if an entity has a specific component\n */\n hasComponent<T>(world: World, entity: EntityId, componentId: EntityId<T>): boolean {\n return world.exists(entity) && world.has(entity, componentId);\n },\n\n /**\n * Check if an entity does not have a specific component\n */\n lacksComponent<T>(world: World, entity: EntityId, componentId: EntityId<T>): boolean {\n return !world.exists(entity) || !world.has(entity, componentId);\n },\n\n /**\n * Get a component value (returns undefined if entity doesn't exist or doesn't have the component)\n */\n getComponent<T>(world: World, entity: EntityId, componentId: EntityId<T>): T | undefined {\n if (!world.exists(entity) || !world.has(entity, componentId)) {\n return undefined;\n }\n return world.get(entity, componentId);\n },\n\n /**\n * Get all relation instances for a wildcard relation\n */\n getRelations<T>(world: World, entity: EntityId, componentId: ComponentId<T>): [EntityId<unknown>, T][] | undefined {\n if (!world.exists(entity)) {\n return undefined;\n }\n const wildcardId = relation(componentId, \"*\");\n try {\n return world.get(entity, wildcardId);\n } catch {\n return [];\n }\n },\n\n /**\n * Check if an entity has a relation to a specific target\n */\n hasRelation<T>(world: World, entity: EntityId, componentId: ComponentId<T>, targetEntity: EntityId<any>): boolean {\n if (!world.exists(entity)) {\n return false;\n }\n const relationId = relation(componentId, targetEntity);\n return world.has(entity, relationId);\n },\n\n /**\n * Check if an entity exists in the world\n */\n entityExists(world: World, entity: EntityId): boolean {\n return world.exists(entity);\n },\n\n /**\n * Check if a query contains specific entities\n */\n queryContains(query: Query, ...entities: EntityId[]): boolean {\n const queryEntities = query.getEntities();\n return entities.every((e) => queryEntities.includes(e));\n },\n\n /**\n * Check if a query contains exactly the specified entities (no more, no less)\n */\n queryContainsExactly(query: Query, ...entities: EntityId[]): boolean {\n const queryEntities = query.getEntities();\n if (queryEntities.length !== entities.length) {\n return false;\n }\n return entities.every((e) => queryEntities.includes(e));\n },\n\n /**\n * Get the count of entities in a query\n */\n queryCount(query: Query): number {\n return query.getEntities().length;\n },\n\n // === Throwing assertions ===\n\n /**\n * Assert that an entity has a component (throws if not)\n */\n assertHasComponent<T>(world: World, entity: EntityId, componentId: EntityId<T>): void {\n if (!world.exists(entity)) {\n throw new AssertionError(`Entity ${entity} does not exist`);\n }\n if (!world.has(entity, componentId)) {\n throw new AssertionError(`Entity ${entity} does not have component ${componentId}`);\n }\n },\n\n /**\n * Assert that an entity lacks a component (throws if it has the component)\n */\n assertLacksComponent<T>(world: World, entity: EntityId, componentId: EntityId<T>): void {\n if (world.exists(entity) && world.has(entity, componentId)) {\n throw new AssertionError(`Entity ${entity} unexpectedly has component ${componentId}`);\n }\n },\n\n /**\n * Assert that a component equals an expected value (throws if not)\n */\n assertComponentEquals<T>(world: World, entity: EntityId, componentId: EntityId<T>, expected: T): void {\n this.assertHasComponent(world, entity, componentId);\n const actual = world.get(entity, componentId);\n if (!deepEquals(actual, expected)) {\n throw new AssertionError(\n `Component ${componentId} on entity ${entity} does not match expected value.\\n` +\n `Expected: ${JSON.stringify(expected)}\\n` +\n `Actual: ${JSON.stringify(actual)}`,\n );\n }\n },\n\n /**\n * Assert that an entity exists (throws if not)\n */\n assertEntityExists(world: World, entity: EntityId): void {\n if (!world.exists(entity)) {\n throw new AssertionError(`Entity ${entity} does not exist`);\n }\n },\n\n /**\n * Assert that an entity does not exist (throws if it exists)\n */\n assertEntityNotExists(world: World, entity: EntityId): void {\n if (world.exists(entity)) {\n throw new AssertionError(`Entity ${entity} unexpectedly exists`);\n }\n },\n\n /**\n * Assert that a query contains specific entities (throws if not)\n */\n assertQueryContains(query: Query, ...entities: EntityId[]): void {\n const queryEntities = query.getEntities();\n for (const entity of entities) {\n if (!queryEntities.includes(entity)) {\n throw new AssertionError(\n `Query does not contain entity ${entity}.\\n` + `Query entities: [${queryEntities.join(\", \")}]`,\n );\n }\n }\n },\n\n /**\n * Assert that a query does not contain specific entities (throws if it does)\n */\n assertQueryNotContains(query: Query, ...entities: EntityId[]): void {\n const queryEntities = query.getEntities();\n for (const entity of entities) {\n if (queryEntities.includes(entity)) {\n throw new AssertionError(\n `Query unexpectedly contains entity ${entity}.\\n` + `Query entities: [${queryEntities.join(\", \")}]`,\n );\n }\n }\n },\n};\n\n// =============================================================================\n// Snapshot - State Capture and Comparison\n// =============================================================================\n\n/**\n * Utilities for capturing and comparing world state snapshots.\n * Useful for testing that systems produce expected state changes.\n *\n * @example\n * ```typescript\n * const before = Snapshot.capture(world, [entity], [PositionId, VelocityId]);\n *\n * // Run game logic\n * movementSystem(world, deltaTime);\n * world.sync();\n *\n * const after = Snapshot.capture(world, [entity], [PositionId, VelocityId]);\n * const diff = Snapshot.compare(before, after);\n *\n * expect(diff.componentChanges).toHaveLength(1);\n * expect(diff.componentChanges[0].changeType).toBe(\"modified\");\n * ```\n */\nexport const Snapshot = {\n /**\n * Capture a snapshot of specified entities and their components\n * @param world The world to capture from\n * @param entities Entities to include in the snapshot\n * @param componentIds Components to capture for each entity\n */\n capture(world: World, entities: EntityId[], componentIds: EntityId<any>[]): WorldSnapshot {\n const entitySnapshots: EntitySnapshot[] = [];\n\n for (const entity of entities) {\n if (!world.exists(entity)) {\n continue;\n }\n\n const components = new Map<EntityId<any>, unknown>();\n\n for (const componentId of componentIds) {\n if (isWildcardRelationId(componentId)) {\n // For wildcard relations, capture all relation instances\n try {\n const relations = world.get(entity, componentId as WildcardRelationId<any>);\n if (relations && relations.length > 0) {\n components.set(componentId, deepClone(relations));\n }\n } catch {\n // Entity doesn't have this relation type\n }\n } else if (world.has(entity, componentId)) {\n components.set(componentId, deepClone(world.get(entity, componentId)));\n }\n }\n\n entitySnapshots.push({ entity, components });\n }\n\n return { entities: entitySnapshots };\n },\n\n /**\n * Compare two snapshots and return the differences\n * @param before The 'before' snapshot\n * @param after The 'after' snapshot\n */\n compare(before: WorldSnapshot, after: WorldSnapshot): SnapshotDiff {\n const beforeEntities = new Set(before.entities.map((e) => e.entity));\n const afterEntities = new Set(after.entities.map((e) => e.entity));\n\n const addedEntities: EntityId[] = [];\n const removedEntities: EntityId[] = [];\n const componentChanges: SnapshotDiff[\"componentChanges\"] = [];\n\n // Find added entities\n for (const entity of afterEntities) {\n if (!beforeEntities.has(entity)) {\n addedEntities.push(entity);\n }\n }\n\n // Find removed entities\n for (const entity of beforeEntities) {\n if (!afterEntities.has(entity)) {\n removedEntities.push(entity);\n }\n }\n\n // Find component changes on existing entities\n const beforeMap = new Map(before.entities.map((e) => [e.entity, e]));\n const afterMap = new Map(after.entities.map((e) => [e.entity, e]));\n\n for (const entity of beforeEntities) {\n if (!afterEntities.has(entity)) continue; // Skip removed entities\n\n const beforeEntity = beforeMap.get(entity)!;\n const afterEntity = afterMap.get(entity)!;\n\n // Check for component changes\n const allComponentIds = new Set([...beforeEntity.components.keys(), ...afterEntity.components.keys()]);\n\n for (const componentId of allComponentIds) {\n const beforeValue = beforeEntity.components.get(componentId);\n const afterValue = afterEntity.components.get(componentId);\n\n if (beforeValue === undefined && afterValue !== undefined) {\n componentChanges.push({\n entity,\n componentId,\n before: undefined,\n after: afterValue,\n changeType: \"added\",\n });\n } else if (beforeValue !== undefined && afterValue === undefined) {\n componentChanges.push({\n entity,\n componentId,\n before: beforeValue,\n after: undefined,\n changeType: \"removed\",\n });\n } else if (!deepEquals(beforeValue, afterValue)) {\n componentChanges.push({\n entity,\n componentId,\n before: beforeValue,\n after: afterValue,\n changeType: \"modified\",\n });\n }\n }\n }\n\n return { addedEntities, removedEntities, componentChanges };\n },\n\n /**\n * Check if two snapshots are equal\n */\n equals(a: WorldSnapshot, b: WorldSnapshot): boolean {\n const diff = this.compare(a, b);\n return diff.addedEntities.length === 0 && diff.removedEntities.length === 0 && diff.componentChanges.length === 0;\n },\n};\n\n// =============================================================================\n// Utilities\n// =============================================================================\n\n/**\n * Custom assertion error for testing utilities\n */\nexport class AssertionError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"AssertionError\";\n }\n}\n\n/**\n * Deep equality check for comparing component values\n */\nfunction deepEquals(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (a === null || b === null) return false;\n if (typeof a !== typeof b) return false;\n\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (!deepEquals(a[i], b[i])) return false;\n }\n return true;\n }\n\n if (typeof a === \"object\" && typeof b === \"object\") {\n const aObj = a as Record<string, unknown>;\n const bObj = b as Record<string, unknown>;\n const aKeys = Object.keys(aObj);\n const bKeys = Object.keys(bObj);\n\n if (aKeys.length !== bKeys.length) return false;\n\n for (const key of aKeys) {\n if (!Object.prototype.hasOwnProperty.call(bObj, key)) return false;\n if (!deepEquals(aObj[key], bObj[key])) return false;\n }\n return true;\n }\n\n return false;\n}\n\n/**\n * Deep clone a value for snapshot isolation\n */\nfunction deepClone<T>(value: T): T {\n if (value === null || typeof value !== \"object\") {\n return value;\n }\n\n if (Array.isArray(value)) {\n return value.map(deepClone) as T;\n }\n\n const result: Record<string, unknown> = {};\n for (const key of Object.keys(value)) {\n result[key] = deepClone((value as Record<string, unknown>)[key]);\n }\n return result as T;\n}\n\n// =============================================================================\n// Re-exports for convenience\n// =============================================================================\n\nexport { component, relation } from \"../core/entity\";\nexport type { ComponentId, EntityId, RelationId, WildcardRelationId } from \"../core/entity\";\nexport type { LifecycleCallback, LifecycleHook, MultiLifecycleCallback, MultiLifecycleHook } from \"../core/types\";\nexport { World } from \"../core/world\";\nexport type { Query } from \"../query/query\";\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"testing.mjs","names":["entitySnapshots: EntitySnapshot[]","addedEntities: EntityId[]","removedEntities: EntityId[]","componentChanges: SnapshotDiff[\"componentChanges\"]","result: Record<string, unknown>"],"sources":["../src/testing/index.ts"],"sourcesContent":["/**\n * @module testing\n * Testing utilities for ECS-based game logic\n *\n * This module provides framework-agnostic testing helpers that work with\n * bun:test, vitest, jest, or any other testing framework.\n *\n * @example\n * ```typescript\n * import { describe, expect, it } from \"bun:test\";\n * import { component } from \"@codehz/ecs\";\n * import { WorldFixture, EntityBuilder, Assertions } from \"@codehz/ecs/testing\";\n *\n * const PositionId = component<{ x: number; y: number }>();\n * const VelocityId = component<{ x: number; y: number }>();\n *\n * describe(\"Movement System\", () => {\n * it(\"should update position based on velocity\", () => {\n * const fixture = new WorldFixture();\n * const entity = fixture\n * .spawn()\n * .with(PositionId, { x: 0, y: 0 })\n * .with(VelocityId, { x: 1, y: 2 })\n * .build();\n *\n * // Run your game logic here\n * movementSystem(fixture.world, 1.0);\n *\n * expect(Assertions.hasComponent(fixture.world, entity, PositionId)).toBe(true);\n * expect(Assertions.getComponent(fixture.world, entity, PositionId)).toEqual({ x: 1, y: 2 });\n * });\n * });\n * ```\n */\n\nimport type { ComponentId, EntityId, WildcardRelationId } from \"../core/entity\";\nimport { isWildcardRelationId, relation } from \"../core/entity\";\nimport { World } from \"../core/world\";\nimport type { Query } from \"../query/query\";\nexport { EntityBuilder } from \"../core/builder\";\nexport type { ComponentDef } from \"../core/builder\";\n\n// =============================================================================\n// Types\n// =============================================================================\n\n/**\n * A component definition for entity building, supporting both regular components and relations\n */\nimport type { EntityBuilder } from \"../core/builder\";\n\n/**\n * Snapshot of a single entity's component state\n */\nexport interface EntitySnapshot {\n entity: EntityId;\n components: Map<EntityId<any>, unknown>;\n}\n\n/**\n * Snapshot of multiple entities' component state\n */\nexport interface WorldSnapshot {\n entities: EntitySnapshot[];\n}\n\n/**\n * Result of comparing two snapshots\n */\nexport interface SnapshotDiff {\n /** Entities that exist in 'after' but not in 'before' */\n addedEntities: EntityId[];\n /** Entities that exist in 'before' but not in 'after' */\n removedEntities: EntityId[];\n /** Changes to components on existing entities */\n componentChanges: Array<{\n entity: EntityId;\n componentId: EntityId<any>;\n before: unknown | undefined;\n after: unknown | undefined;\n changeType: \"added\" | \"removed\" | \"modified\";\n }>;\n}\n\n// =============================================================================\n// WorldFixture - Test World Factory\n// =============================================================================\n\n/**\n * A test fixture that manages a World instance and provides convenient\n * methods for setting up test scenarios.\n *\n * @example\n * ```typescript\n * const fixture = new WorldFixture();\n *\n * // Spawn entities with fluent API\n * const player = fixture\n * .spawn()\n * .with(PositionId, { x: 0, y: 0 })\n * .with(HealthId, { current: 100, max: 100 })\n * .build();\n *\n * // Access the world for running systems\n * movementSystem(fixture.world);\n *\n * // Clean up (optional - creates a fresh world)\n * fixture.reset();\n * ```\n */\nexport class WorldFixture {\n private _world: World;\n private _queries: Query[] = [];\n\n constructor() {\n this._world = new World();\n }\n\n /**\n * Get the underlying World instance\n */\n get world(): World {\n return this._world;\n }\n\n /**\n * Create a new EntityBuilder for spawning an entity with components\n */\n spawn(): EntityBuilder {\n return this._world.spawn();\n }\n\n /**\n * Spawn multiple entities with the same component configuration\n * @param count Number of entities to spawn\n * @param configure Function to configure each entity builder\n * @returns Array of created entity IDs\n */\n spawnMany(count: number, configure: (builder: EntityBuilder, index: number) => EntityBuilder): EntityId[] {\n return this._world.spawnMany(count, configure);\n }\n\n /**\n * Create a query and track it for automatic cleanup\n * @param componentTypes Component types to query for\n * @returns Query instance\n */\n createQuery(componentTypes: EntityId<any>[]): Query {\n const query = this._world.createQuery(componentTypes);\n this._queries.push(query);\n return query;\n }\n\n /**\n * Execute pending commands (alias for world.sync())\n */\n sync(): void {\n this._world.sync();\n }\n\n /**\n * Reset the fixture with a fresh World instance\n * Disposes all tracked queries\n */\n reset(): void {\n for (const query of this._queries) {\n query.dispose();\n }\n this._queries = [];\n this._world = new World();\n }\n\n /**\n * Capture a snapshot of specified entities and their components\n * @param entities Entities to capture\n * @param componentIds Components to include in the snapshot\n */\n captureSnapshot(entities: EntityId[], componentIds: EntityId<any>[]): WorldSnapshot {\n return Snapshot.capture(this._world, entities, componentIds);\n }\n\n /**\n * Symbol.dispose implementation for automatic resource management\n */\n [Symbol.dispose](): void {\n this.reset();\n }\n}\n\n// =============================================================================\n// EntityBuilder - Fluent Entity Creation\n// =============================================================================\n\n/**\n * Fluent builder for creating entities with components.\n * Supports both regular components and entity relations.\n *\n * @example\n * ```typescript\n * // Basic usage\n * // Note: build() will enqueue component commands but will NOT call world.sync().\n * // You must call world.sync() or fixture.sync() manually to apply commands.\n * const entity = new EntityBuilder(world)\n * .with(PositionId, { x: 10, y: 20 })\n * .with(VelocityId, { x: 1, y: 0 })\n * .build();\n * // Apply pending changes\n * world.sync();\n *\n * // With relations\n * const child = new EntityBuilder(world)\n * .with(PositionId, { x: 0, y: 0 })\n * .withRelation(ParentId, parentEntity, { offset: { x: 5, y: 5 } })\n * .build();\n * world.sync();\n *\n * // Tag component (void type)\n * const tagged = new EntityBuilder(world)\n * .withTag(PlayerTagId)\n * .build();\n * ```\n */\n// EntityBuilder is exported from world.ts; testing utilities will use world.spawn()\n\n// =============================================================================\n// Assertions - Test Assertion Helpers\n// =============================================================================\n\n/**\n * Test assertion utilities that return boolean values or throw descriptive errors.\n * These work with any testing framework's expect() function.\n *\n * @example\n * ```typescript\n * // With bun:test or vitest\n * expect(Assertions.hasComponent(world, entity, PositionId)).toBe(true);\n * expect(Assertions.getComponent(world, entity, PositionId)).toEqual({ x: 10, y: 20 });\n *\n * // Direct assertion (throws on failure)\n * Assertions.assertHasComponent(world, entity, PositionId);\n * Assertions.assertComponentEquals(world, entity, PositionId, { x: 10, y: 20 });\n * ```\n */\nexport const Assertions = {\n /**\n * Check if an entity has a specific component\n */\n hasComponent<T>(world: World, entity: EntityId, componentId: EntityId<T>): boolean {\n return world.exists(entity) && world.has(entity, componentId);\n },\n\n /**\n * Check if an entity does not have a specific component\n */\n lacksComponent<T>(world: World, entity: EntityId, componentId: EntityId<T>): boolean {\n return !world.exists(entity) || !world.has(entity, componentId);\n },\n\n /**\n * Get a component value (returns undefined if entity doesn't exist or doesn't have the component)\n */\n getComponent<T>(world: World, entity: EntityId, componentId: EntityId<T>): T | undefined {\n if (!world.exists(entity) || !world.has(entity, componentId)) {\n return undefined;\n }\n return world.get(entity, componentId);\n },\n\n /**\n * Get all relation instances for a wildcard relation\n */\n getRelations<T>(world: World, entity: EntityId, componentId: ComponentId<T>): [EntityId<unknown>, T][] | undefined {\n if (!world.exists(entity)) {\n return undefined;\n }\n const wildcardId = relation(componentId, \"*\");\n try {\n return world.get(entity, wildcardId);\n } catch {\n return [];\n }\n },\n\n /**\n * Check if an entity has a relation to a specific target\n */\n hasRelation<T>(world: World, entity: EntityId, componentId: ComponentId<T>, targetEntity: EntityId<any>): boolean {\n if (!world.exists(entity)) {\n return false;\n }\n const relationId = relation(componentId, targetEntity);\n return world.has(entity, relationId);\n },\n\n /**\n * Check if an entity exists in the world\n */\n entityExists(world: World, entity: EntityId): boolean {\n return world.exists(entity);\n },\n\n /**\n * Check if a query contains specific entities\n */\n queryContains(query: Query, ...entities: EntityId[]): boolean {\n const queryEntities = query.getEntities();\n return entities.every((e) => queryEntities.includes(e));\n },\n\n /**\n * Check if a query contains exactly the specified entities (no more, no less)\n */\n queryContainsExactly(query: Query, ...entities: EntityId[]): boolean {\n const queryEntities = query.getEntities();\n if (queryEntities.length !== entities.length) {\n return false;\n }\n return entities.every((e) => queryEntities.includes(e));\n },\n\n /**\n * Get the count of entities in a query\n */\n queryCount(query: Query): number {\n return query.getEntities().length;\n },\n\n // === Throwing assertions ===\n\n /**\n * Assert that an entity has a component (throws if not)\n */\n assertHasComponent<T>(world: World, entity: EntityId, componentId: EntityId<T>): void {\n if (!world.exists(entity)) {\n throw new AssertionError(`Entity ${entity} does not exist`);\n }\n if (!world.has(entity, componentId)) {\n throw new AssertionError(`Entity ${entity} does not have component ${componentId}`);\n }\n },\n\n /**\n * Assert that an entity lacks a component (throws if it has the component)\n */\n assertLacksComponent<T>(world: World, entity: EntityId, componentId: EntityId<T>): void {\n if (world.exists(entity) && world.has(entity, componentId)) {\n throw new AssertionError(`Entity ${entity} unexpectedly has component ${componentId}`);\n }\n },\n\n /**\n * Assert that a component equals an expected value (throws if not)\n */\n assertComponentEquals<T>(world: World, entity: EntityId, componentId: EntityId<T>, expected: T): void {\n this.assertHasComponent(world, entity, componentId);\n const actual = world.get(entity, componentId);\n if (!deepEquals(actual, expected)) {\n throw new AssertionError(\n `Component ${componentId} on entity ${entity} does not match expected value.\\n` +\n `Expected: ${JSON.stringify(expected)}\\n` +\n `Actual: ${JSON.stringify(actual)}`,\n );\n }\n },\n\n /**\n * Assert that an entity exists (throws if not)\n */\n assertEntityExists(world: World, entity: EntityId): void {\n if (!world.exists(entity)) {\n throw new AssertionError(`Entity ${entity} does not exist`);\n }\n },\n\n /**\n * Assert that an entity does not exist (throws if it exists)\n */\n assertEntityNotExists(world: World, entity: EntityId): void {\n if (world.exists(entity)) {\n throw new AssertionError(`Entity ${entity} unexpectedly exists`);\n }\n },\n\n /**\n * Assert that a query contains specific entities (throws if not)\n */\n assertQueryContains(query: Query, ...entities: EntityId[]): void {\n const queryEntities = query.getEntities();\n for (const entity of entities) {\n if (!queryEntities.includes(entity)) {\n throw new AssertionError(\n `Query does not contain entity ${entity}.\\n` + `Query entities: [${queryEntities.join(\", \")}]`,\n );\n }\n }\n },\n\n /**\n * Assert that a query does not contain specific entities (throws if it does)\n */\n assertQueryNotContains(query: Query, ...entities: EntityId[]): void {\n const queryEntities = query.getEntities();\n for (const entity of entities) {\n if (queryEntities.includes(entity)) {\n throw new AssertionError(\n `Query unexpectedly contains entity ${entity}.\\n` + `Query entities: [${queryEntities.join(\", \")}]`,\n );\n }\n }\n },\n};\n\n// =============================================================================\n// Snapshot - State Capture and Comparison\n// =============================================================================\n\n/**\n * Utilities for capturing and comparing world state snapshots.\n * Useful for testing that systems produce expected state changes.\n *\n * @example\n * ```typescript\n * const before = Snapshot.capture(world, [entity], [PositionId, VelocityId]);\n *\n * // Run game logic\n * movementSystem(world, deltaTime);\n * world.sync();\n *\n * const after = Snapshot.capture(world, [entity], [PositionId, VelocityId]);\n * const diff = Snapshot.compare(before, after);\n *\n * expect(diff.componentChanges).toHaveLength(1);\n * expect(diff.componentChanges[0].changeType).toBe(\"modified\");\n * ```\n */\nexport const Snapshot = {\n /**\n * Capture a snapshot of specified entities and their components\n * @param world The world to capture from\n * @param entities Entities to include in the snapshot\n * @param componentIds Components to capture for each entity\n */\n capture(world: World, entities: EntityId[], componentIds: EntityId<any>[]): WorldSnapshot {\n const entitySnapshots: EntitySnapshot[] = [];\n\n for (const entity of entities) {\n if (!world.exists(entity)) {\n continue;\n }\n\n const components = new Map<EntityId<any>, unknown>();\n\n for (const componentId of componentIds) {\n if (isWildcardRelationId(componentId)) {\n // For wildcard relations, capture all relation instances\n try {\n const relations = world.get(entity, componentId as WildcardRelationId<any>);\n if (relations && relations.length > 0) {\n components.set(componentId, deepClone(relations));\n }\n } catch {\n // Entity doesn't have this relation type\n }\n } else if (world.has(entity, componentId)) {\n components.set(componentId, deepClone(world.get(entity, componentId)));\n }\n }\n\n entitySnapshots.push({ entity, components });\n }\n\n return { entities: entitySnapshots };\n },\n\n /**\n * Compare two snapshots and return the differences\n * @param before The 'before' snapshot\n * @param after The 'after' snapshot\n */\n compare(before: WorldSnapshot, after: WorldSnapshot): SnapshotDiff {\n const beforeEntities = new Set(before.entities.map((e) => e.entity));\n const afterEntities = new Set(after.entities.map((e) => e.entity));\n\n const addedEntities: EntityId[] = [];\n const removedEntities: EntityId[] = [];\n const componentChanges: SnapshotDiff[\"componentChanges\"] = [];\n\n // Find added entities\n for (const entity of afterEntities) {\n if (!beforeEntities.has(entity)) {\n addedEntities.push(entity);\n }\n }\n\n // Find removed entities\n for (const entity of beforeEntities) {\n if (!afterEntities.has(entity)) {\n removedEntities.push(entity);\n }\n }\n\n // Find component changes on existing entities\n const beforeMap = new Map(before.entities.map((e) => [e.entity, e]));\n const afterMap = new Map(after.entities.map((e) => [e.entity, e]));\n\n for (const entity of beforeEntities) {\n if (!afterEntities.has(entity)) continue; // Skip removed entities\n\n const beforeEntity = beforeMap.get(entity)!;\n const afterEntity = afterMap.get(entity)!;\n\n // Check for component changes\n const allComponentIds = new Set([...beforeEntity.components.keys(), ...afterEntity.components.keys()]);\n\n for (const componentId of allComponentIds) {\n const beforeValue = beforeEntity.components.get(componentId);\n const afterValue = afterEntity.components.get(componentId);\n\n if (beforeValue === undefined && afterValue !== undefined) {\n componentChanges.push({\n entity,\n componentId,\n before: undefined,\n after: afterValue,\n changeType: \"added\",\n });\n } else if (beforeValue !== undefined && afterValue === undefined) {\n componentChanges.push({\n entity,\n componentId,\n before: beforeValue,\n after: undefined,\n changeType: \"removed\",\n });\n } else if (!deepEquals(beforeValue, afterValue)) {\n componentChanges.push({\n entity,\n componentId,\n before: beforeValue,\n after: afterValue,\n changeType: \"modified\",\n });\n }\n }\n }\n\n return { addedEntities, removedEntities, componentChanges };\n },\n\n /**\n * Check if two snapshots are equal\n */\n equals(a: WorldSnapshot, b: WorldSnapshot): boolean {\n const diff = this.compare(a, b);\n return diff.addedEntities.length === 0 && diff.removedEntities.length === 0 && diff.componentChanges.length === 0;\n },\n};\n\n// =============================================================================\n// Utilities\n// =============================================================================\n\n/**\n * Custom assertion error for testing utilities\n */\nexport class AssertionError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"AssertionError\";\n }\n}\n\n/**\n * Deep equality check for comparing component values\n */\nfunction deepEquals(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (a === null || b === null) return false;\n if (typeof a !== typeof b) return false;\n\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (!deepEquals(a[i], b[i])) return false;\n }\n return true;\n }\n\n if (typeof a === \"object\" && typeof b === \"object\") {\n const aObj = a as Record<string, unknown>;\n const bObj = b as Record<string, unknown>;\n const aKeys = Object.keys(aObj);\n const bKeys = Object.keys(bObj);\n\n if (aKeys.length !== bKeys.length) return false;\n\n for (const key of aKeys) {\n if (!Object.prototype.hasOwnProperty.call(bObj, key)) return false;\n if (!deepEquals(aObj[key], bObj[key])) return false;\n }\n return true;\n }\n\n return false;\n}\n\n/**\n * Deep clone a value for snapshot isolation\n */\nfunction deepClone<T>(value: T): T {\n if (value === null || typeof value !== \"object\") {\n return value;\n }\n\n if (Array.isArray(value)) {\n return value.map(deepClone) as T;\n }\n\n const result: Record<string, unknown> = {};\n for (const key of Object.keys(value)) {\n result[key] = deepClone((value as Record<string, unknown>)[key]);\n }\n return result as T;\n}\n\n// =============================================================================\n// Re-exports for convenience\n// =============================================================================\n\nexport { component, relation } from \"../core/entity\";\nexport type { ComponentId, EntityId, RelationId, WildcardRelationId } from \"../core/entity\";\nexport type {\n LegacyLifecycleCallback as LifecycleCallback,\n LegacyLifecycleHook as LifecycleHook,\n LifecycleCallback as MultiLifecycleCallback,\n LifecycleHook as MultiLifecycleHook,\n} from \"../core/types\";\nexport { World } from \"../core/world\";\nexport type { Query } from \"../query/query\";\n"],"mappings":"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"}
|
package/world.mjs
CHANGED
|
@@ -1530,12 +1530,12 @@ function triggerMultiComponentHooks(ctx, entityId, addedComponents, removedCompo
|
|
|
1530
1530
|
if (!hook.on_set) continue;
|
|
1531
1531
|
const anyRequiredAdded = requiredComponents.some((c) => anyComponentMatches(addedComponents, c));
|
|
1532
1532
|
const anyOptionalAdded = optionalComponents.some((c) => anyComponentMatches(addedComponents, c));
|
|
1533
|
-
if ((anyRequiredAdded || anyOptionalAdded) && entityHasAllComponents(ctx, entityId, requiredComponents)) hook.on_set(entityId,
|
|
1533
|
+
if ((anyRequiredAdded || anyOptionalAdded) && entityHasAllComponents(ctx, entityId, requiredComponents)) hook.on_set(entityId, ...collectMultiHookComponents(ctx, entityId, componentTypes));
|
|
1534
1534
|
}
|
|
1535
1535
|
if (removedComponents.size > 0) for (const entry of oldArchetype.matchingMultiHooks) {
|
|
1536
1536
|
const { hook, requiredComponents, componentTypes } = entry;
|
|
1537
1537
|
if (!hook.on_remove) continue;
|
|
1538
|
-
if (requiredComponents.some((c) => anyComponentMatches(removedComponents, c)) && entityHadAllComponentsBefore(ctx, entityId, requiredComponents, removedComponents)) hook.on_remove(entityId,
|
|
1538
|
+
if (requiredComponents.some((c) => anyComponentMatches(removedComponents, c)) && entityHadAllComponentsBefore(ctx, entityId, requiredComponents, removedComponents) && !entityHasAllComponents(ctx, entityId, requiredComponents)) hook.on_remove(entityId, ...collectMultiHookComponentsWithRemoved(ctx, entityId, componentTypes, removedComponents));
|
|
1539
1539
|
}
|
|
1540
1540
|
}
|
|
1541
1541
|
function entityHasAllComponents(ctx, entityId, requiredComponents) {
|
|
@@ -1564,14 +1564,32 @@ function entityHadAllComponentsBefore(ctx, entityId, requiredComponents, removed
|
|
|
1564
1564
|
function collectMultiHookComponents(ctx, entityId, componentTypes) {
|
|
1565
1565
|
return componentTypes.map((ct) => isOptionalEntityId(ct) ? ctx.getOptional(entityId, ct.optional) : ctx.get(entityId, ct));
|
|
1566
1566
|
}
|
|
1567
|
+
/**
|
|
1568
|
+
* Reconstructs wildcard relation data by merging current data with removed components.
|
|
1569
|
+
* Returns an array of [targetId, value] tuples for the wildcard relation.
|
|
1570
|
+
*/
|
|
1571
|
+
function reconstructWildcardWithRemoved(ctx, entityId, wildcardId, removedComponents) {
|
|
1572
|
+
const currentData = ctx.get(entityId, wildcardId);
|
|
1573
|
+
const result = Array.isArray(currentData) ? [...currentData] : [];
|
|
1574
|
+
for (const [removedCompId, removedValue] of removedComponents.entries()) if (componentMatchesHookType(removedCompId, wildcardId)) {
|
|
1575
|
+
const targetId = getTargetIdFromRelationId(removedCompId);
|
|
1576
|
+
if (targetId !== void 0) result.push([targetId, removedValue]);
|
|
1577
|
+
}
|
|
1578
|
+
return result;
|
|
1579
|
+
}
|
|
1567
1580
|
function collectMultiHookComponentsWithRemoved(ctx, entityId, componentTypes, removedComponents) {
|
|
1568
1581
|
return componentTypes.map((ct) => {
|
|
1569
1582
|
if (isOptionalEntityId(ct)) {
|
|
1570
1583
|
const optionalId = ct.optional;
|
|
1584
|
+
if (isWildcardRelationId(optionalId)) {
|
|
1585
|
+
const result = reconstructWildcardWithRemoved(ctx, entityId, optionalId, removedComponents);
|
|
1586
|
+
return result.length > 0 ? { value: result } : void 0;
|
|
1587
|
+
}
|
|
1571
1588
|
const match$1 = findMatchingComponent(removedComponents, optionalId);
|
|
1572
1589
|
return match$1 ? { value: match$1[1] } : ctx.getOptional(entityId, optionalId);
|
|
1573
1590
|
}
|
|
1574
1591
|
const compId = ct;
|
|
1592
|
+
if (isWildcardRelationId(compId)) return reconstructWildcardWithRemoved(ctx, entityId, compId, removedComponents);
|
|
1575
1593
|
const match = findMatchingComponent(removedComponents, compId);
|
|
1576
1594
|
return match ? match[1] : ctx.get(entityId, compId);
|
|
1577
1595
|
});
|
|
@@ -1680,8 +1698,8 @@ var World = class {
|
|
|
1680
1698
|
queries = [];
|
|
1681
1699
|
queryCache = /* @__PURE__ */ new Map();
|
|
1682
1700
|
commandBuffer = new CommandBuffer((entityId, commands) => this.executeEntityCommands(entityId, commands));
|
|
1683
|
-
|
|
1684
|
-
|
|
1701
|
+
legacyHooks = /* @__PURE__ */ new Map();
|
|
1702
|
+
hooks = /* @__PURE__ */ new Set();
|
|
1685
1703
|
constructor(snapshot) {
|
|
1686
1704
|
if (snapshot && typeof snapshot === "object") this.deserializeSnapshot(snapshot);
|
|
1687
1705
|
}
|
|
@@ -1791,9 +1809,9 @@ var World = class {
|
|
|
1791
1809
|
if (typeof hook === "function") if (Array.isArray(componentTypesOrSingle)) {
|
|
1792
1810
|
const callback = hook;
|
|
1793
1811
|
hook = {
|
|
1794
|
-
on_init: (entityId,
|
|
1795
|
-
on_set: (entityId,
|
|
1796
|
-
on_remove: (entityId,
|
|
1812
|
+
on_init: (entityId, ...components) => callback("init", entityId, ...components),
|
|
1813
|
+
on_set: (entityId, ...components) => callback("set", entityId, ...components),
|
|
1814
|
+
on_remove: (entityId, ...components) => callback("remove", entityId, ...components)
|
|
1797
1815
|
};
|
|
1798
1816
|
} else {
|
|
1799
1817
|
const callback = hook;
|
|
@@ -1815,45 +1833,57 @@ var World = class {
|
|
|
1815
1833
|
optionalComponents,
|
|
1816
1834
|
hook
|
|
1817
1835
|
};
|
|
1818
|
-
this.
|
|
1836
|
+
this.hooks.add(entry);
|
|
1819
1837
|
for (const archetype of this.archetypes) if (this.archetypeMatchesHook(archetype, entry)) archetype.matchingMultiHooks.add(entry);
|
|
1820
1838
|
const multiHook = hook;
|
|
1821
1839
|
if (multiHook.on_init !== void 0) {
|
|
1822
1840
|
const matchingArchetypes = this.getMatchingArchetypes(requiredComponents);
|
|
1823
1841
|
for (const archetype of matchingArchetypes) for (const entityId of archetype.getEntities()) {
|
|
1824
1842
|
const components = collectMultiHookComponents(this.createHooksContext(), entityId, componentTypes);
|
|
1825
|
-
multiHook.on_init(entityId,
|
|
1843
|
+
multiHook.on_init(entityId, ...components);
|
|
1826
1844
|
}
|
|
1827
1845
|
}
|
|
1846
|
+
return () => {
|
|
1847
|
+
this.hooks.delete(entry);
|
|
1848
|
+
for (const archetype of this.archetypes) archetype.matchingMultiHooks.delete(entry);
|
|
1849
|
+
};
|
|
1828
1850
|
} else {
|
|
1829
1851
|
const componentType = componentTypesOrSingle;
|
|
1830
|
-
if (!this.
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
if (
|
|
1852
|
+
if (!this.legacyHooks.has(componentType)) this.legacyHooks.set(componentType, /* @__PURE__ */ new Set());
|
|
1853
|
+
const legacyHook = hook;
|
|
1854
|
+
this.legacyHooks.get(componentType).add(legacyHook);
|
|
1855
|
+
if (legacyHook.on_init !== void 0) this.archetypesByComponent.get(componentType)?.forEach((archetype) => {
|
|
1834
1856
|
const entities = archetype.getEntityToIndexMap();
|
|
1835
1857
|
const componentData = archetype.getComponentData(componentType);
|
|
1836
1858
|
for (const [entity, index] of entities) {
|
|
1837
1859
|
const data = componentData[index];
|
|
1838
1860
|
const value = data === MISSING_COMPONENT ? void 0 : data;
|
|
1839
|
-
|
|
1861
|
+
legacyHook.on_init?.(entity, componentType, value);
|
|
1840
1862
|
}
|
|
1841
1863
|
});
|
|
1864
|
+
return () => {
|
|
1865
|
+
const hooks = this.legacyHooks.get(componentType);
|
|
1866
|
+
if (hooks) {
|
|
1867
|
+
hooks.delete(legacyHook);
|
|
1868
|
+
if (hooks.size === 0) this.legacyHooks.delete(componentType);
|
|
1869
|
+
}
|
|
1870
|
+
};
|
|
1842
1871
|
}
|
|
1843
1872
|
}
|
|
1873
|
+
/** @deprecated use the unsubscribe function returned by hook() instead */
|
|
1844
1874
|
unhook(componentTypesOrSingle, hook) {
|
|
1845
1875
|
if (Array.isArray(componentTypesOrSingle)) {
|
|
1846
|
-
for (const entry of this.
|
|
1847
|
-
this.
|
|
1876
|
+
for (const entry of this.hooks) if (entry.hook === hook) {
|
|
1877
|
+
this.hooks.delete(entry);
|
|
1848
1878
|
for (const archetype of this.archetypes) archetype.matchingMultiHooks.delete(entry);
|
|
1849
1879
|
break;
|
|
1850
1880
|
}
|
|
1851
1881
|
} else {
|
|
1852
1882
|
const componentType = componentTypesOrSingle;
|
|
1853
|
-
const hooks = this.
|
|
1883
|
+
const hooks = this.legacyHooks.get(componentType);
|
|
1854
1884
|
if (hooks) {
|
|
1855
1885
|
hooks.delete(hook);
|
|
1856
|
-
if (hooks.size === 0) this.
|
|
1886
|
+
if (hooks.size === 0) this.legacyHooks.delete(componentType);
|
|
1857
1887
|
}
|
|
1858
1888
|
}
|
|
1859
1889
|
}
|
|
@@ -1962,8 +1992,8 @@ var World = class {
|
|
|
1962
1992
|
}
|
|
1963
1993
|
createHooksContext() {
|
|
1964
1994
|
return {
|
|
1965
|
-
hooks: this.
|
|
1966
|
-
multiHooks: this.
|
|
1995
|
+
hooks: this.legacyHooks,
|
|
1996
|
+
multiHooks: this.hooks,
|
|
1967
1997
|
has: (eid, ct) => this.has(eid, ct),
|
|
1968
1998
|
get: (eid, ct) => this.get(eid, ct),
|
|
1969
1999
|
getOptional: (eid, ct) => this.getOptional(eid, ct)
|
|
@@ -2011,7 +2041,7 @@ var World = class {
|
|
|
2011
2041
|
return newArchetype;
|
|
2012
2042
|
}
|
|
2013
2043
|
updateArchetypeHookMatches(archetype) {
|
|
2014
|
-
for (const entry of this.
|
|
2044
|
+
for (const entry of this.hooks) if (this.archetypeMatchesHook(archetype, entry)) archetype.matchingMultiHooks.add(entry);
|
|
2015
2045
|
}
|
|
2016
2046
|
archetypeMatchesHook(archetype, entry) {
|
|
2017
2047
|
return entry.requiredComponents.every((c) => {
|