@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 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
- - `delete(entity, componentId)`: 从实体移除组件
180
- - `setExclusive(componentId)`: 将组件标记为独占关系
181
- - `createQuery(componentIds)`: 创建查询
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
- - `World.serialize()` 返回一个内存中的快照对象(snapshot),快照会按引用保存组件的实际值;它不会对数据做 JSON.stringify 操作,也不会尝试把组件值转换为可序列化格式。
191
- - `World.deserialize(snapshot)` 接受由 `World.serialize()` 生成的快照对象并重建世界状态。它期望一个内存对象(非 JSON 字符串)。
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.deserialize(snapshot);
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.deserialize(parsed);
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.deserialize(readySnapshot);
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
- ## System 迁移到 Pipeline
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 { S as relation, _ as getComponentNameById, a as ComponentType, b as isRelationId, c as ComponentOptions, d as EntityRelationId, f as RelationId, g as getComponentIdByName, h as decodeRelationId, i as ComponentTuple, l as ComponentRelationId, m as component, n as World, o as LifecycleHook, p as WildcardRelationId, r as Query, s as ComponentId, t as SerializedWorld, u as EntityId, v as isComponentId, x as isWildcardRelationId, y as isEntityId } from "./world.mjs";
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 getComponentIdByName, c as isEntityId, d as relation, i as decodeRelationId, l as isRelationId, n as Query, o as getComponentNameById, r as component, s as isComponentId, t as World, u as isWildcardRelationId } from "./world.mjs";
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
@@ -1,6 +1,9 @@
1
1
  {
2
2
  "name": "@codehz/ecs",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
+ "repository": {
5
+ "url": "https://github.com/codehz/ecs"
6
+ },
4
7
  "type": "module",
5
8
  "main": "./index.mjs",
6
9
  "types": "./index.d.mts",
package/testing.d.mts CHANGED
@@ -1,20 +1,7 @@
1
- import { S as relation, f as RelationId, m as component, n as World, o as LifecycleHook, p as WildcardRelationId, r as Query, s as ComponentId, u as EntityId } from "./world.mjs";
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, r as component, t as World, u as isWildcardRelationId } from "./world.mjs";
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 new EntityBuilder(this._world);
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
- const entities = [];
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 = void, U$1 = unknown> = number & {
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: number;
766
+ id: SerializedEntityId;
755
767
  components: SerializedComponent[];
756
768
  };
757
769
  type SerializedComponent = {
758
- type: number | string | {
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 S, getComponentNameById as _, ComponentType as a, isRelationId as b, ComponentOptions as c, EntityRelationId as d, RelationId as f, getComponentIdByName as g, decodeRelationId as h, ComponentTuple as i, ComponentRelationId as l, component as m, World as n, LifecycleHook as o, WildcardRelationId as p, Query as r, ComponentId as s, SerializedWorld as t, EntityId as u, isComponentId as v, isWildcardRelationId as x, isEntityId as y };
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