@codehz/ecs 0.4.1 → 0.4.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/README.md +36 -17
- package/index.d.mts +2 -2
- package/index.mjs +2 -2
- package/package.json +4 -1
- package/testing.d.mts +7 -54
- package/testing.mjs +8 -96
- package/testing.mjs.map +1 -1
- package/world.d.mts +50 -12
- package/world.mjs +160 -57
- package/world.mjs.map +1 -1
package/README.md
CHANGED
|
@@ -55,7 +55,11 @@ query.forEach([PositionId, VelocityId], (entity, position, velocity) => {
|
|
|
55
55
|
|
|
56
56
|
### 组件生命周期钩子
|
|
57
57
|
|
|
58
|
-
ECS
|
|
58
|
+
ECS 支持在组件添加或移除时执行回调函数。钩子回调函数的参数如下:
|
|
59
|
+
|
|
60
|
+
- `entityId`: 实体的 ID (number)
|
|
61
|
+
- `componentType`: 组件类型 ID (EntityId)
|
|
62
|
+
- `component`: 组件数据值 (T)
|
|
59
63
|
|
|
60
64
|
```typescript
|
|
61
65
|
// 注册组件生命周期钩子
|
|
@@ -130,15 +134,12 @@ ECS 支持 Exclusive Relations,确保实体对于指定的组件类型最多
|
|
|
130
134
|
```typescript
|
|
131
135
|
import { World, component, relation } from "@codehz/ecs";
|
|
132
136
|
|
|
133
|
-
// 定义组件ID
|
|
134
|
-
const ChildOf = component(); // 空组件,用于关系
|
|
137
|
+
// 定义组件ID,设置为独占关系
|
|
138
|
+
const ChildOf = component({ exclusive: true }); // 空组件,用于关系
|
|
135
139
|
|
|
136
140
|
// 创建世界
|
|
137
141
|
const world = new World();
|
|
138
142
|
|
|
139
|
-
// 设置 ChildOf 为独占关系
|
|
140
|
-
world.setExclusive(ChildOf);
|
|
141
|
-
|
|
142
143
|
// 创建实体
|
|
143
144
|
const child = world.new();
|
|
144
145
|
const parent1 = world.new();
|
|
@@ -173,22 +174,27 @@ bun run examples/simple/demo.ts
|
|
|
173
174
|
### World
|
|
174
175
|
|
|
175
176
|
- `new()`: 创建新实体
|
|
177
|
+
- `spawn()`: 创建 EntityBuilder 用于流式实体创建
|
|
178
|
+
- `spawnMany(count, configure)`: 批量创建多个实体
|
|
179
|
+
- `exists(entity)`: 检查实体是否存在
|
|
176
180
|
- `set(entity, componentId, data)`: 向实体添加组件
|
|
177
181
|
- `get(entity, componentId)`: 获取实体的组件数据(注意:只能获取已设置的组件,使用前请先用 `has()` 检查组件是否存在)
|
|
178
182
|
- `has(entity, componentId)`: 检查实体是否拥有指定组件
|
|
179
|
-
- `
|
|
180
|
-
- `
|
|
181
|
-
- `
|
|
183
|
+
- `remove(entity, componentId)`: 从实体移除组件
|
|
184
|
+
- `delete(entity)`: 销毁实体及其所有组件
|
|
185
|
+
- `query(componentIds)`: 快速查询具有指定组件的实体
|
|
186
|
+
- `createQuery(componentIds)`: 创建可重用的查询对象
|
|
182
187
|
- `hook(componentId, hook)`: 注册组件或通配符关系生命周期钩子
|
|
183
188
|
- `unhook(componentId, hook)`: 注销组件或通配符关系生命周期钩子
|
|
189
|
+
- `serialize()`: 序列化世界状态为快照对象
|
|
184
190
|
- `sync()`: 执行所有延迟命令
|
|
185
191
|
|
|
186
192
|
### 序列化(快照)
|
|
187
193
|
|
|
188
194
|
库提供了对世界状态的「内存快照」序列化接口,用于保存/恢复实体与组件的数据。注意关键点:
|
|
189
195
|
|
|
190
|
-
- `
|
|
191
|
-
- `World
|
|
196
|
+
- `world.serialize()` 返回一个内存中的快照对象(snapshot),快照会按引用保存组件的实际值;它不会对数据做 JSON.stringify 操作,也不会尝试把组件值转换为可序列化格式。
|
|
197
|
+
- `new World(snapshot)` 通过构造函数接受由 `world.serialize()` 生成的快照对象并重建世界状态。它期望一个内存对象(非 JSON 字符串)。
|
|
192
198
|
|
|
193
199
|
为什么采用这种设计?很多情况下组件值可能包含函数、类实例、循环引用或其他无法用 JSON 表示的值。库不对组件值强行进行序列化/字符串化,以避免数据丢失或不可信的自动转换。
|
|
194
200
|
|
|
@@ -199,7 +205,7 @@ bun run examples/simple/demo.ts
|
|
|
199
205
|
const snapshot = world.serialize();
|
|
200
206
|
|
|
201
207
|
// 在同一进程内直接恢复
|
|
202
|
-
const restored = World
|
|
208
|
+
const restored = new World(snapshot);
|
|
203
209
|
```
|
|
204
210
|
|
|
205
211
|
持久化到磁盘或跨进程传输
|
|
@@ -220,7 +226,7 @@ const text = JSON.stringify(snapshot);
|
|
|
220
226
|
|
|
221
227
|
// 恢复:parse -> deserialize
|
|
222
228
|
const parsed = JSON.parse(text);
|
|
223
|
-
const restored = World
|
|
229
|
+
const restored = new World(parsed);
|
|
224
230
|
```
|
|
225
231
|
|
|
226
232
|
示例:带自定义编码的持久化(伪代码)
|
|
@@ -249,7 +255,7 @@ const readySnapshot = {
|
|
|
249
255
|
})),
|
|
250
256
|
};
|
|
251
257
|
|
|
252
|
-
const restored = World
|
|
258
|
+
const restored = new World(readySnapshot);
|
|
253
259
|
```
|
|
254
260
|
|
|
255
261
|
注意事项
|
|
@@ -264,11 +270,24 @@ const restored = World.deserialize(readySnapshot);
|
|
|
264
270
|
|
|
265
271
|
### Query
|
|
266
272
|
|
|
267
|
-
- `forEach(componentIds, callback)`:
|
|
273
|
+
- `forEach(componentIds, callback)`: 遍历匹配的实体,为每个实体调用回调函数
|
|
268
274
|
- `getEntities()`: 获取所有匹配实体的ID列表
|
|
269
|
-
- `getEntitiesWithComponents(componentIds)`:
|
|
275
|
+
- `getEntitiesWithComponents(componentIds)`: 获取实体及其组件数据的对象数组
|
|
276
|
+
- `iterate(componentIds)`: 返回一个生成器,用于遍历匹配的实体及其组件数据
|
|
277
|
+
- `getComponentData(componentType)`: 获取指定组件类型的所有匹配实体的数据数组
|
|
278
|
+
- `dispose()`: 释放查询资源,停止接收世界更新通知
|
|
279
|
+
|
|
280
|
+
### EntityBuilder
|
|
270
281
|
|
|
271
|
-
|
|
282
|
+
EntityBuilder 提供流式 API 用于便捷的实体创建:
|
|
283
|
+
|
|
284
|
+
- `with(componentId, value)`: 添加组件到构建器
|
|
285
|
+
- `withTag(componentId)`: 添加标记组件(无值)到构建器
|
|
286
|
+
- `withRelation(componentId, targetEntity, value)`: 添加关系组件到构建器
|
|
287
|
+
- `withRelationTag(componentId, targetEntity)`: 添加关系标记(无值)到构建器
|
|
288
|
+
- `build()`: 创建实体并应用所有组件(需要手动调用 `world.sync()`)
|
|
289
|
+
|
|
290
|
+
### World
|
|
272
291
|
|
|
273
292
|
从 v0.4.0 开始,本库移除了内置的 `System` 和 `SystemScheduler` 功能。推荐使用 `@codehz/pipeline` 作为替代方案来组织游戏循环逻辑。
|
|
274
293
|
|
package/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { type ComponentId, type ComponentOptions, type ComponentRelationId, type ComponentTuple, type ComponentType, type EntityId, type EntityRelationId, type LifecycleHook, Query, type RelationId, type SerializedWorld, type WildcardRelationId, World, component, decodeRelationId, getComponentIdByName, getComponentNameById, isComponentId, isEntityId, isRelationId, isWildcardRelationId, relation };
|
|
1
|
+
import { C as isComponentId, D as relation, E as isWildcardRelationId, S as getComponentNameById, T as isRelationId, _ as RelationId, a as SerializedEntityId, b as decodeRelationId, c as Query, d as LifecycleHook, f as ComponentId, g as EntityRelationId, h as EntityId, i as SerializedEntity, l as ComponentTuple, m as ComponentRelationId, n as EntityBuilder, o as SerializedWorld, p as ComponentOptions, r as SerializedComponent, s as World, t as ComponentDef, u as ComponentType, v as WildcardRelationId, w as isEntityId, x as getComponentIdByName, y as component } from "./world.mjs";
|
|
2
|
+
export { type ComponentDef, type ComponentId, type ComponentOptions, type ComponentRelationId, type ComponentTuple, type ComponentType, EntityBuilder, type EntityId, type EntityRelationId, type LifecycleHook, Query, type RelationId, type SerializedComponent, type SerializedEntity, type SerializedEntityId, type SerializedWorld, type WildcardRelationId, World, component, decodeRelationId, getComponentIdByName, getComponentNameById, isComponentId, isEntityId, isRelationId, isWildcardRelationId, relation };
|
package/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as decodeRelationId, c as isComponentId, d as isWildcardRelationId, f as relation, i as component, l as isEntityId, n as World, o as getComponentIdByName, r as Query, s as getComponentNameById, t as EntityBuilder, u as isRelationId } from "./world.mjs";
|
|
2
2
|
|
|
3
|
-
export { Query, World, component, decodeRelationId, getComponentIdByName, getComponentNameById, isComponentId, isEntityId, isRelationId, isWildcardRelationId, relation };
|
|
3
|
+
export { EntityBuilder, Query, World, component, decodeRelationId, getComponentIdByName, getComponentNameById, isComponentId, isEntityId, isRelationId, isWildcardRelationId, relation };
|
package/package.json
CHANGED
package/testing.d.mts
CHANGED
|
@@ -1,20 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { D as relation, _ as RelationId, c as Query, d as LifecycleHook, f as ComponentId, h as EntityId, n as EntityBuilder, s as World, t as ComponentDef, v as WildcardRelationId, y as component } from "./world.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/testing.d.ts
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* A component definition for entity building, supporting both regular components and relations
|
|
7
|
-
*/
|
|
8
|
-
type ComponentDef<T = unknown> = {
|
|
9
|
-
type: "component";
|
|
10
|
-
id: EntityId<T>;
|
|
11
|
-
value: T;
|
|
12
|
-
} | {
|
|
13
|
-
type: "relation";
|
|
14
|
-
componentId: ComponentId<T>;
|
|
15
|
-
targetId: EntityId<any>;
|
|
16
|
-
value: T;
|
|
17
|
-
};
|
|
18
5
|
/**
|
|
19
6
|
* Snapshot of a single entity's component state
|
|
20
7
|
*/
|
|
@@ -119,16 +106,21 @@ declare class WorldFixture {
|
|
|
119
106
|
* @example
|
|
120
107
|
* ```typescript
|
|
121
108
|
* // Basic usage
|
|
109
|
+
* // Note: build() will enqueue component commands but will NOT call world.sync().
|
|
110
|
+
* // You must call world.sync() or fixture.sync() manually to apply commands.
|
|
122
111
|
* const entity = new EntityBuilder(world)
|
|
123
112
|
* .with(PositionId, { x: 10, y: 20 })
|
|
124
113
|
* .with(VelocityId, { x: 1, y: 0 })
|
|
125
114
|
* .build();
|
|
115
|
+
* // Apply pending changes
|
|
116
|
+
* world.sync();
|
|
126
117
|
*
|
|
127
118
|
* // With relations
|
|
128
119
|
* const child = new EntityBuilder(world)
|
|
129
120
|
* .with(PositionId, { x: 0, y: 0 })
|
|
130
121
|
* .withRelation(ParentId, parentEntity, { offset: { x: 5, y: 5 } })
|
|
131
122
|
* .build();
|
|
123
|
+
* world.sync();
|
|
132
124
|
*
|
|
133
125
|
* // Tag component (void type)
|
|
134
126
|
* const tagged = new EntityBuilder(world)
|
|
@@ -136,45 +128,6 @@ declare class WorldFixture {
|
|
|
136
128
|
* .build();
|
|
137
129
|
* ```
|
|
138
130
|
*/
|
|
139
|
-
declare class EntityBuilder {
|
|
140
|
-
private world;
|
|
141
|
-
private components;
|
|
142
|
-
constructor(world: World);
|
|
143
|
-
/**
|
|
144
|
-
* Add a component to the entity being built
|
|
145
|
-
* @param componentId The component ID
|
|
146
|
-
* @param value The component value
|
|
147
|
-
*/
|
|
148
|
-
with<T>(componentId: EntityId<T>, value: T): this;
|
|
149
|
-
/**
|
|
150
|
-
* Add a tag component (void type) to the entity being built
|
|
151
|
-
* @param componentId The component ID (must be void type)
|
|
152
|
-
*/
|
|
153
|
-
withTag(componentId: EntityId<void>): this;
|
|
154
|
-
/**
|
|
155
|
-
* Add a relation component targeting another entity
|
|
156
|
-
* @param componentId The base component ID for the relation
|
|
157
|
-
* @param targetEntity The target entity
|
|
158
|
-
* @param value The component value
|
|
159
|
-
*/
|
|
160
|
-
withRelation<T>(componentId: ComponentId<T>, targetEntity: EntityId<any>, value: T): this;
|
|
161
|
-
/**
|
|
162
|
-
* Add a relation tag (void type) targeting another entity
|
|
163
|
-
* @param componentId The base component ID for the relation
|
|
164
|
-
* @param targetEntity The target entity
|
|
165
|
-
*/
|
|
166
|
-
withRelationTag(componentId: ComponentId<void>, targetEntity: EntityId<any>): this;
|
|
167
|
-
/**
|
|
168
|
-
* Build the entity and return its ID.
|
|
169
|
-
* This creates the entity, sets all components, and calls sync().
|
|
170
|
-
*/
|
|
171
|
-
build(): EntityId;
|
|
172
|
-
/**
|
|
173
|
-
* Build the entity without calling sync().
|
|
174
|
-
* Useful when batching multiple entity creations.
|
|
175
|
-
*/
|
|
176
|
-
buildDeferred(): EntityId;
|
|
177
|
-
}
|
|
178
131
|
/**
|
|
179
132
|
* Test assertion utilities that return boolean values or throw descriptive errors.
|
|
180
133
|
* These work with any testing framework's expect() function.
|
|
@@ -301,5 +254,5 @@ declare class AssertionError extends Error {
|
|
|
301
254
|
constructor(message: string);
|
|
302
255
|
}
|
|
303
256
|
//#endregion
|
|
304
|
-
export { AssertionError, Assertions, ComponentDef, type ComponentId, EntityBuilder, type EntityId, EntitySnapshot, type LifecycleHook, 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 LifecycleHook, type Query, type RelationId, Snapshot, SnapshotDiff, type WildcardRelationId, World, WorldFixture, WorldSnapshot, component, relation };
|
|
305
258
|
//# sourceMappingURL=testing.d.mts.map
|
package/testing.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { d as relation,
|
|
1
|
+
import { d as isWildcardRelationId, f as relation, i as component, n as World, t as EntityBuilder } from "./world.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/testing.ts
|
|
4
4
|
/**
|
|
@@ -39,7 +39,7 @@ var WorldFixture = class {
|
|
|
39
39
|
* Create a new EntityBuilder for spawning an entity with components
|
|
40
40
|
*/
|
|
41
41
|
spawn() {
|
|
42
|
-
return
|
|
42
|
+
return this._world.spawn();
|
|
43
43
|
}
|
|
44
44
|
/**
|
|
45
45
|
* Spawn multiple entities with the same component configuration
|
|
@@ -48,12 +48,7 @@ var WorldFixture = class {
|
|
|
48
48
|
* @returns Array of created entity IDs
|
|
49
49
|
*/
|
|
50
50
|
spawnMany(count, configure) {
|
|
51
|
-
|
|
52
|
-
for (let i = 0; i < count; i++) {
|
|
53
|
-
const builder = new EntityBuilder(this._world);
|
|
54
|
-
entities.push(configure(builder, i).build());
|
|
55
|
-
}
|
|
56
|
-
return entities;
|
|
51
|
+
return this._world.spawnMany(count, configure);
|
|
57
52
|
}
|
|
58
53
|
/**
|
|
59
54
|
* Create a query and track it for automatic cleanup
|
|
@@ -102,16 +97,21 @@ var WorldFixture = class {
|
|
|
102
97
|
* @example
|
|
103
98
|
* ```typescript
|
|
104
99
|
* // Basic usage
|
|
100
|
+
* // Note: build() will enqueue component commands but will NOT call world.sync().
|
|
101
|
+
* // You must call world.sync() or fixture.sync() manually to apply commands.
|
|
105
102
|
* const entity = new EntityBuilder(world)
|
|
106
103
|
* .with(PositionId, { x: 10, y: 20 })
|
|
107
104
|
* .with(VelocityId, { x: 1, y: 0 })
|
|
108
105
|
* .build();
|
|
106
|
+
* // Apply pending changes
|
|
107
|
+
* world.sync();
|
|
109
108
|
*
|
|
110
109
|
* // With relations
|
|
111
110
|
* const child = new EntityBuilder(world)
|
|
112
111
|
* .with(PositionId, { x: 0, y: 0 })
|
|
113
112
|
* .withRelation(ParentId, parentEntity, { offset: { x: 5, y: 5 } })
|
|
114
113
|
* .build();
|
|
114
|
+
* world.sync();
|
|
115
115
|
*
|
|
116
116
|
* // Tag component (void type)
|
|
117
117
|
* const tagged = new EntityBuilder(world)
|
|
@@ -119,94 +119,6 @@ var WorldFixture = class {
|
|
|
119
119
|
* .build();
|
|
120
120
|
* ```
|
|
121
121
|
*/
|
|
122
|
-
var EntityBuilder = class {
|
|
123
|
-
world;
|
|
124
|
-
components = [];
|
|
125
|
-
constructor(world) {
|
|
126
|
-
this.world = world;
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* Add a component to the entity being built
|
|
130
|
-
* @param componentId The component ID
|
|
131
|
-
* @param value The component value
|
|
132
|
-
*/
|
|
133
|
-
with(componentId, value) {
|
|
134
|
-
this.components.push({
|
|
135
|
-
type: "component",
|
|
136
|
-
id: componentId,
|
|
137
|
-
value
|
|
138
|
-
});
|
|
139
|
-
return this;
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* Add a tag component (void type) to the entity being built
|
|
143
|
-
* @param componentId The component ID (must be void type)
|
|
144
|
-
*/
|
|
145
|
-
withTag(componentId) {
|
|
146
|
-
this.components.push({
|
|
147
|
-
type: "component",
|
|
148
|
-
id: componentId,
|
|
149
|
-
value: void 0
|
|
150
|
-
});
|
|
151
|
-
return this;
|
|
152
|
-
}
|
|
153
|
-
/**
|
|
154
|
-
* Add a relation component targeting another entity
|
|
155
|
-
* @param componentId The base component ID for the relation
|
|
156
|
-
* @param targetEntity The target entity
|
|
157
|
-
* @param value The component value
|
|
158
|
-
*/
|
|
159
|
-
withRelation(componentId, targetEntity, value) {
|
|
160
|
-
this.components.push({
|
|
161
|
-
type: "relation",
|
|
162
|
-
componentId,
|
|
163
|
-
targetId: targetEntity,
|
|
164
|
-
value
|
|
165
|
-
});
|
|
166
|
-
return this;
|
|
167
|
-
}
|
|
168
|
-
/**
|
|
169
|
-
* Add a relation tag (void type) targeting another entity
|
|
170
|
-
* @param componentId The base component ID for the relation
|
|
171
|
-
* @param targetEntity The target entity
|
|
172
|
-
*/
|
|
173
|
-
withRelationTag(componentId, targetEntity) {
|
|
174
|
-
this.components.push({
|
|
175
|
-
type: "relation",
|
|
176
|
-
componentId,
|
|
177
|
-
targetId: targetEntity,
|
|
178
|
-
value: void 0
|
|
179
|
-
});
|
|
180
|
-
return this;
|
|
181
|
-
}
|
|
182
|
-
/**
|
|
183
|
-
* Build the entity and return its ID.
|
|
184
|
-
* This creates the entity, sets all components, and calls sync().
|
|
185
|
-
*/
|
|
186
|
-
build() {
|
|
187
|
-
const entity = this.world.new();
|
|
188
|
-
for (const def of this.components) if (def.type === "component") this.world.set(entity, def.id, def.value);
|
|
189
|
-
else {
|
|
190
|
-
const relationId = relation(def.componentId, def.targetId);
|
|
191
|
-
this.world.set(entity, relationId, def.value);
|
|
192
|
-
}
|
|
193
|
-
this.world.sync();
|
|
194
|
-
return entity;
|
|
195
|
-
}
|
|
196
|
-
/**
|
|
197
|
-
* Build the entity without calling sync().
|
|
198
|
-
* Useful when batching multiple entity creations.
|
|
199
|
-
*/
|
|
200
|
-
buildDeferred() {
|
|
201
|
-
const entity = this.world.new();
|
|
202
|
-
for (const def of this.components) if (def.type === "component") this.world.set(entity, def.id, def.value);
|
|
203
|
-
else {
|
|
204
|
-
const relationId = relation(def.componentId, def.targetId);
|
|
205
|
-
this.world.set(entity, relationId, def.value);
|
|
206
|
-
}
|
|
207
|
-
return entity;
|
|
208
|
-
}
|
|
209
|
-
};
|
|
210
122
|
/**
|
|
211
123
|
* Test assertion utilities that return boolean values or throw descriptive errors.
|
|
212
124
|
* These work with any testing framework's expect() function.
|
package/testing.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.mjs","names":["entities: EntityId[]","entitySnapshots: EntitySnapshot[]","addedEntities: EntityId[]","removedEntities: EntityId[]","componentChanges: SnapshotDiff[\"componentChanges\"]","result: Record<string, unknown>"],"sources":["../src/testing.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 \"./entity\";\nimport { isWildcardRelationId, relation } from \"./entity\";\nimport type { Query } from \"./query\";\nimport { World } from \"./world\";\n\n// =============================================================================\n// Types\n// =============================================================================\n\n/**\n * A component definition for entity building, supporting both regular components and relations\n */\nexport type ComponentDef<T = unknown> =\n | { type: \"component\"; id: EntityId<T>; value: T }\n | { type: \"relation\"; componentId: ComponentId<T>; targetId: EntityId<any>; value: T };\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 new EntityBuilder(this._world);\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 const entities: EntityId[] = [];\n for (let i = 0; i < count; i++) {\n const builder = new EntityBuilder(this._world);\n entities.push(configure(builder, i).build());\n }\n return entities;\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 * const entity = new EntityBuilder(world)\n * .with(PositionId, { x: 10, y: 20 })\n * .with(VelocityId, { x: 1, y: 0 })\n * .build();\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 *\n * // Tag component (void type)\n * const tagged = new EntityBuilder(world)\n * .withTag(PlayerTagId)\n * .build();\n * ```\n */\nexport class EntityBuilder {\n private world: World;\n private components: ComponentDef[] = [];\n\n constructor(world: World) {\n this.world = world;\n }\n\n /**\n * Add a component to the entity being built\n * @param componentId The component ID\n * @param value The component value\n */\n with<T>(componentId: EntityId<T>, value: T): this {\n this.components.push({ type: \"component\", id: componentId, value });\n return this;\n }\n\n /**\n * Add a tag component (void type) to the entity being built\n * @param componentId The component ID (must be void type)\n */\n withTag(componentId: EntityId<void>): this {\n this.components.push({ type: \"component\", id: componentId, value: undefined as void });\n return this;\n }\n\n /**\n * Add a relation component targeting another entity\n * @param componentId The base component ID for the relation\n * @param targetEntity The target entity\n * @param value The component value\n */\n withRelation<T>(componentId: ComponentId<T>, targetEntity: EntityId<any>, value: T): this {\n this.components.push({ type: \"relation\", componentId, targetId: targetEntity, value });\n return this;\n }\n\n /**\n * Add a relation tag (void type) targeting another entity\n * @param componentId The base component ID for the relation\n * @param targetEntity The target entity\n */\n withRelationTag(componentId: ComponentId<void>, targetEntity: EntityId<any>): this {\n this.components.push({\n type: \"relation\",\n componentId,\n targetId: targetEntity,\n value: undefined as void,\n });\n return this;\n }\n\n /**\n * Build the entity and return its ID.\n * This creates the entity, sets all components, and calls sync().\n */\n build(): EntityId {\n const entity = this.world.new();\n\n for (const def of this.components) {\n if (def.type === \"component\") {\n this.world.set(entity, def.id, def.value);\n } else {\n const relationId = relation(def.componentId, def.targetId);\n this.world.set(entity, relationId, def.value);\n }\n }\n\n this.world.sync();\n return entity;\n }\n\n /**\n * Build the entity without calling sync().\n * Useful when batching multiple entity creations.\n */\n buildDeferred(): EntityId {\n const entity = this.world.new();\n\n for (const def of this.components) {\n if (def.type === \"component\") {\n this.world.set(entity, def.id, def.value);\n } else {\n const relationId = relation(def.componentId, def.targetId);\n this.world.set(entity, relationId, def.value);\n }\n }\n\n return entity;\n }\n}\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 \"./entity\";\nexport type { ComponentId, EntityId, RelationId, WildcardRelationId } from \"./entity\";\nexport type { Query } from \"./query\";\nexport type { LifecycleHook } from \"./types\";\nexport { World } from \"./world\";\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.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 \"./entity\";\nimport { isWildcardRelationId, relation } from \"./entity\";\nimport type { Query } from \"./query\";\nimport { World } from \"./world\";\nexport { EntityBuilder } from \"./world\";\nexport type { ComponentDef } from \"./world\";\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 \"./world\";\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 \"./entity\";\nexport type { ComponentId, EntityId, RelationId, WildcardRelationId } from \"./entity\";\nexport type { Query } from \"./query\";\nexport type { LifecycleHook } from \"./types\";\nexport { World } from \"./world\";\n"],"mappings":"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"}
|
package/world.d.mts
CHANGED
|
@@ -14,7 +14,7 @@ declare const __entityIdTypeTag: unique symbol;
|
|
|
14
14
|
* - Entity IDs: 1024+
|
|
15
15
|
* - Relation IDs: negative numbers encoding component and entity associations
|
|
16
16
|
*/
|
|
17
|
-
type EntityId<T =
|
|
17
|
+
type EntityId<T = unknown, U$1 = unknown> = number & {
|
|
18
18
|
readonly [__componentTypeMarker]: T;
|
|
19
19
|
readonly [__entityIdTypeTag]: U$1;
|
|
20
20
|
};
|
|
@@ -479,6 +479,10 @@ declare class Query {
|
|
|
479
479
|
}
|
|
480
480
|
//#endregion
|
|
481
481
|
//#region src/world.d.ts
|
|
482
|
+
type SerializedEntityId = number | string | {
|
|
483
|
+
component: string;
|
|
484
|
+
target: number | string | "*";
|
|
485
|
+
};
|
|
482
486
|
/**
|
|
483
487
|
* World class for ECS architecture
|
|
484
488
|
* Manages entities and components
|
|
@@ -521,7 +525,7 @@ declare class World {
|
|
|
521
525
|
* Create a new entity
|
|
522
526
|
* @returns The ID of the newly created entity
|
|
523
527
|
*/
|
|
524
|
-
new(): EntityId
|
|
528
|
+
new<T = void>(): EntityId<T>;
|
|
525
529
|
/**
|
|
526
530
|
* Destroy an entity and remove all its components (immediate execution)
|
|
527
531
|
*/
|
|
@@ -579,6 +583,18 @@ declare class World {
|
|
|
579
583
|
* @returns A Query object for the specified component types and filter
|
|
580
584
|
*/
|
|
581
585
|
createQuery(componentTypes: EntityId<any>[], filter?: QueryFilter): Query;
|
|
586
|
+
/**
|
|
587
|
+
* Create an EntityBuilder for convenient entity creation.
|
|
588
|
+
* @returns EntityBuilder
|
|
589
|
+
*/
|
|
590
|
+
spawn(): EntityBuilder;
|
|
591
|
+
/**
|
|
592
|
+
* Spawn multiple entities using an EntityBuilder configuration callback
|
|
593
|
+
* @param count number of entities
|
|
594
|
+
* @param configure builder configuration callback
|
|
595
|
+
* @returns Created entity IDs
|
|
596
|
+
*/
|
|
597
|
+
spawnMany(count: number, configure: (builder: EntityBuilder, index: number) => EntityBuilder): EntityId[];
|
|
582
598
|
/**
|
|
583
599
|
* @internal Register a query for archetype update notifications
|
|
584
600
|
*/
|
|
@@ -714,10 +730,6 @@ declare class World {
|
|
|
714
730
|
* Only removes archetypes whose component types reference the entity
|
|
715
731
|
*/
|
|
716
732
|
private cleanupArchetypesReferencingEntity;
|
|
717
|
-
/**
|
|
718
|
-
* Remove an empty archetype from all internal data structures
|
|
719
|
-
*/
|
|
720
|
-
private cleanupEmptyArchetype;
|
|
721
733
|
/**
|
|
722
734
|
* Remove archetype from the main archetypes list
|
|
723
735
|
*/
|
|
@@ -751,16 +763,42 @@ type SerializedWorld = {
|
|
|
751
763
|
entities: SerializedEntity[];
|
|
752
764
|
};
|
|
753
765
|
type SerializedEntity = {
|
|
754
|
-
id:
|
|
766
|
+
id: SerializedEntityId;
|
|
755
767
|
components: SerializedComponent[];
|
|
756
768
|
};
|
|
757
769
|
type SerializedComponent = {
|
|
758
|
-
type:
|
|
759
|
-
component: string;
|
|
760
|
-
target: number | string;
|
|
761
|
-
};
|
|
770
|
+
type: SerializedEntityId;
|
|
762
771
|
value: any;
|
|
763
772
|
};
|
|
773
|
+
/**
|
|
774
|
+
* A component definition for entity building, supporting both regular components and relations
|
|
775
|
+
*/
|
|
776
|
+
type ComponentDef<T = unknown> = {
|
|
777
|
+
type: "component";
|
|
778
|
+
id: EntityId<T>;
|
|
779
|
+
value: T;
|
|
780
|
+
} | {
|
|
781
|
+
type: "relation";
|
|
782
|
+
componentId: ComponentId<T>;
|
|
783
|
+
targetId: EntityId<any>;
|
|
784
|
+
value: T;
|
|
785
|
+
};
|
|
786
|
+
declare class EntityBuilder {
|
|
787
|
+
private world;
|
|
788
|
+
private components;
|
|
789
|
+
constructor(world: World);
|
|
790
|
+
with<T>(componentId: EntityId<T>, value: T): this;
|
|
791
|
+
withTag(componentId: EntityId<void>): this;
|
|
792
|
+
withRelation<T>(componentId: ComponentId<T>, targetEntity: EntityId<any>, value: T): this;
|
|
793
|
+
withRelationTag(componentId: ComponentId<void>, targetEntity: EntityId<any>): this;
|
|
794
|
+
/**
|
|
795
|
+
* Create an entity and enqueue components to be applied. This method
|
|
796
|
+
* does NOT call `world.sync()` automatically; callers must invoke
|
|
797
|
+
* `world.sync()` to apply deferred commands.
|
|
798
|
+
* (Previously auto-synced; now a breaking change — buildDeferred() removed.)
|
|
799
|
+
*/
|
|
800
|
+
build(): EntityId;
|
|
801
|
+
}
|
|
764
802
|
//#endregion
|
|
765
|
-
export { relation as
|
|
803
|
+
export { isComponentId as C, relation as D, isWildcardRelationId as E, getComponentNameById as S, isRelationId as T, RelationId as _, SerializedEntityId as a, decodeRelationId as b, Query as c, LifecycleHook as d, ComponentId as f, EntityRelationId as g, EntityId as h, SerializedEntity as i, ComponentTuple as l, ComponentRelationId as m, EntityBuilder as n, SerializedWorld as o, ComponentOptions as p, SerializedComponent as r, World as s, ComponentDef as t, ComponentType as u, WildcardRelationId as v, isEntityId as w, getComponentIdByName as x, component as y };
|
|
766
804
|
//# sourceMappingURL=world.d.mts.map
|