@codehz/ecs 0.5.4 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/builder.d.mts CHANGED
@@ -175,7 +175,7 @@ interface Command {
175
175
  /**
176
176
  * Hook types for component lifecycle events
177
177
  */
178
- interface LifecycleHook<T = unknown> {
178
+ interface LegacyLifecycleHook<T = unknown> {
179
179
  /**
180
180
  * Called when a component is added to an entity
181
181
  */
@@ -189,21 +189,21 @@ interface LifecycleHook<T = unknown> {
189
189
  */
190
190
  on_remove?: (entityId: EntityId, componentType: EntityId<T>, component: T) => void;
191
191
  }
192
- interface MultiLifecycleHook<T extends readonly ComponentType<any>[]> {
193
- on_init?: (entityId: EntityId, componentTypes: T, components: ComponentTuple<T>) => void;
194
- on_set?: (entityId: EntityId, componentTypes: T, components: ComponentTuple<T>) => void;
195
- on_remove?: (entityId: EntityId, componentTypes: T, components: ComponentTuple<T>) => void;
192
+ interface LifecycleHook<T extends readonly ComponentType<any>[]> {
193
+ on_init?: (entityId: EntityId, ...components: ComponentTuple<T>) => void;
194
+ on_set?: (entityId: EntityId, ...components: ComponentTuple<T>) => void;
195
+ on_remove?: (entityId: EntityId, ...components: ComponentTuple<T>) => void;
196
196
  }
197
197
  /**
198
198
  * Convenience function type for single component lifecycle events
199
199
  * Combines on_init, on_set, and on_remove into a single callback
200
200
  */
201
- type LifecycleCallback<T = unknown> = (type: "init" | "set" | "remove", entityId: EntityId, componentType: EntityId<T>, component: T) => void;
201
+ type LegacyLifecycleCallback<T = unknown> = (type: "init" | "set" | "remove", entityId: EntityId, componentType: EntityId<T>, component: T) => void;
202
202
  /**
203
203
  * Convenience function type for multi-component lifecycle events
204
204
  * Combines on_init, on_set, and on_remove into a single callback
205
205
  */
206
- type MultiLifecycleCallback<T extends readonly ComponentType<any>[]> = (type: "init" | "set" | "remove", entityId: EntityId, componentTypes: T, components: ComponentTuple<T>) => void;
206
+ type LifecycleCallback<T extends readonly ComponentType<any>[]> = (type: "init" | "set" | "remove", entityId: EntityId, ...components: ComponentTuple<T>) => void;
207
207
  type ComponentType<T> = EntityId<T> | OptionalEntityId<T>;
208
208
  type OptionalEntityId<T> = {
209
209
  optional: EntityId<T>;
@@ -217,6 +217,12 @@ type ComponentTypeToData<T> = T extends {
217
217
  * Type helper for component tuples extracted from EntityId array
218
218
  */
219
219
  type ComponentTuple<T extends readonly ComponentType<any>[]> = { readonly [K in keyof T]: ComponentTypeToData<T[K]> };
220
+ interface LifecycleHookEntry {
221
+ componentTypes: readonly ComponentType<any>[];
222
+ requiredComponents: EntityId<any>[];
223
+ optionalComponents: EntityId<any>[];
224
+ hook: LifecycleHook<any>;
225
+ }
220
226
  //#endregion
221
227
  //#region src/core/archetype.d.ts
222
228
  /**
@@ -248,6 +254,13 @@ declare class Archetype {
248
254
  * Stored in World to avoid migration overhead when entities change archetypes
249
255
  */
250
256
  private dontFragmentRelations;
257
+ /**
258
+ * Cache for pre-computed component data sources to avoid repeated calculations
259
+ */
260
+ /**
261
+ * Multi-hooks that match this archetype
262
+ */
263
+ readonly matchingMultiHooks: Set<LifecycleHookEntry>;
251
264
  /**
252
265
  * Cache for pre-computed component data sources to avoid repeated calculations
253
266
  */
@@ -416,8 +429,8 @@ declare class World {
416
429
  private queries;
417
430
  private queryCache;
418
431
  private commandBuffer;
432
+ private legacyHooks;
419
433
  private hooks;
420
- private multiHooks;
421
434
  constructor(snapshot?: SerializedWorld);
422
435
  private deserializeSnapshot;
423
436
  private createArchetypeSignature;
@@ -434,10 +447,13 @@ declare class World {
434
447
  getOptional<T>(entityId: EntityId, componentType: EntityId<T>): {
435
448
  value: T;
436
449
  } | undefined;
437
- hook<T>(componentType: EntityId<T>, hook: LifecycleHook<T> | LifecycleCallback<T>): void;
438
- hook<const T extends readonly ComponentType<any>[]>(componentTypes: T, hook: MultiLifecycleHook<T> | MultiLifecycleCallback<T>): void;
439
- unhook<T>(componentType: EntityId<T>, hook: LifecycleHook<T>): void;
440
- unhook<const T extends readonly ComponentType<any>[]>(componentTypes: T, hook: MultiLifecycleHook<T>): void;
450
+ /**
451
+ * @deprecated use array overload with LifecycleCallback
452
+ */
453
+ hook<T>(componentType: EntityId<T>, hook: LegacyLifecycleHook<T> | LegacyLifecycleCallback<T>): void;
454
+ hook<const T extends readonly ComponentType<any>[]>(componentTypes: T, hook: LifecycleHook<T> | LifecycleCallback<T>): void;
455
+ unhook<T>(componentType: EntityId<T>, hook: LegacyLifecycleHook<T>): void;
456
+ unhook<const T extends readonly ComponentType<any>[]>(componentTypes: T, hook: LifecycleHook<T>): void;
441
457
  sync(): void;
442
458
  createQuery(componentTypes: EntityId<any>[], filter?: QueryFilter): Query;
443
459
  spawn(): EntityBuilder;
@@ -458,6 +474,8 @@ declare class World {
458
474
  private updateEntityReferences;
459
475
  private ensureArchetype;
460
476
  private createNewArchetype;
477
+ private updateArchetypeHookMatches;
478
+ private archetypeMatchesHook;
461
479
  private archetypeReferencesEntity;
462
480
  private cleanupArchetypesReferencingEntity;
463
481
  private removeArchetype;
@@ -495,5 +513,5 @@ declare class EntityBuilder {
495
513
  build(): EntityId;
496
514
  }
497
515
  //#endregion
498
- export { isRelationId as A, ComponentRelationId as C, WildcardRelationId as D, RelationId as E, isComponentId as O, ComponentId as S, EntityRelationId as T, getComponentIdByName as _, SerializedEntity as a, isWildcardRelationId as b, Query as c, LifecycleCallback as d, LifecycleHook as f, component as g, ComponentOptions as h, SerializedComponent as i, isEntityId as k, ComponentTuple as l, MultiLifecycleHook as m, EntityBuilder as n, SerializedEntityId as o, MultiLifecycleCallback as p, World as r, SerializedWorld as s, ComponentDef as t, ComponentType as u, getComponentNameById as v, EntityId as w, relation as x, decodeRelationId as y };
516
+ export { isRelationId as A, ComponentRelationId as C, WildcardRelationId as D, RelationId as E, isComponentId as O, ComponentId as S, EntityRelationId as T, getComponentIdByName as _, SerializedEntity as a, isWildcardRelationId as b, Query as c, LegacyLifecycleCallback as d, LegacyLifecycleHook as f, component as g, ComponentOptions as h, SerializedComponent as i, isEntityId as k, ComponentTuple as l, LifecycleHook as m, EntityBuilder as n, SerializedEntityId as o, LifecycleCallback as p, World as r, SerializedWorld as s, ComponentDef as t, ComponentType as u, getComponentNameById as v, EntityId as w, relation as x, decodeRelationId as y };
499
517
  //# sourceMappingURL=builder.d.mts.map
package/index.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { A as isRelationId, C as ComponentRelationId, D as WildcardRelationId, E as RelationId, O as isComponentId, S as ComponentId, T as EntityRelationId, _ as getComponentIdByName, a as SerializedEntity, b as isWildcardRelationId, c as Query, d as LifecycleCallback, f as LifecycleHook, g as component, h as ComponentOptions, i as SerializedComponent, k as isEntityId, l as ComponentTuple, m as MultiLifecycleHook, n as EntityBuilder, o as SerializedEntityId, p as MultiLifecycleCallback, r as World, s as SerializedWorld, t as ComponentDef, u as ComponentType, v as getComponentNameById, w as EntityId, x as relation, y as decodeRelationId } from "./builder.mjs";
2
- export { type ComponentDef, type ComponentId, type ComponentOptions, type ComponentRelationId, type ComponentTuple, type ComponentType, EntityBuilder, type EntityId, type EntityRelationId, type LifecycleCallback, type LifecycleHook, type MultiLifecycleCallback, type MultiLifecycleHook, Query, type RelationId, type SerializedComponent, type SerializedEntity, type SerializedEntityId, type SerializedWorld, type WildcardRelationId, World, component, decodeRelationId, getComponentIdByName, getComponentNameById, isComponentId, isEntityId, isRelationId, isWildcardRelationId, relation };
1
+ import { A as isRelationId, C as ComponentRelationId, D as WildcardRelationId, E as RelationId, O as isComponentId, S as ComponentId, T as EntityRelationId, _ as getComponentIdByName, a as SerializedEntity, b as isWildcardRelationId, c as Query, d as LegacyLifecycleCallback, f as LegacyLifecycleHook, g as component, h as ComponentOptions, i as SerializedComponent, k as isEntityId, l as ComponentTuple, m as LifecycleHook, n as EntityBuilder, o as SerializedEntityId, p as LifecycleCallback, r as World, s as SerializedWorld, t as ComponentDef, u as ComponentType, v as getComponentNameById, w as EntityId, x as relation, y as decodeRelationId } from "./builder.mjs";
2
+ export { type ComponentDef, type ComponentId, type ComponentOptions, type ComponentRelationId, type ComponentTuple, type ComponentType, EntityBuilder, type EntityId, type EntityRelationId, type LegacyLifecycleCallback as LifecycleCallback, type LegacyLifecycleHook as LifecycleHook, type LifecycleCallback as MultiLifecycleCallback, type LifecycleHook as MultiLifecycleHook, Query, type RelationId, type SerializedComponent, type SerializedEntity, type SerializedEntityId, type SerializedWorld, type WildcardRelationId, World, component, decodeRelationId, getComponentIdByName, getComponentNameById, isComponentId, isEntityId, isRelationId, isWildcardRelationId, relation };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codehz/ecs",
3
- "version": "0.5.4",
3
+ "version": "0.6.0",
4
4
  "repository": {
5
5
  "url": "https://github.com/codehz/ecs"
6
6
  },
package/testing.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { D as WildcardRelationId, E as RelationId, S as ComponentId, c as Query, d as LifecycleCallback, f as LifecycleHook, g as component, m as MultiLifecycleHook, n as EntityBuilder, p as MultiLifecycleCallback, r as World, t as ComponentDef, w as EntityId, x as relation } from "./builder.mjs";
1
+ import { D as WildcardRelationId, E as RelationId, S as ComponentId, c as Query, d as LegacyLifecycleCallback, f as LegacyLifecycleHook, g as component, m as LifecycleHook, n as EntityBuilder, p as LifecycleCallback, r as World, t as ComponentDef, w as EntityId, x as relation } from "./builder.mjs";
2
2
 
3
3
  //#region src/testing/index.d.ts
4
4
 
@@ -254,5 +254,5 @@ declare class AssertionError extends Error {
254
254
  constructor(message: string);
255
255
  }
256
256
  //#endregion
257
- export { AssertionError, Assertions, type ComponentDef, type ComponentId, EntityBuilder, type EntityId, EntitySnapshot, type LifecycleCallback, type LifecycleHook, type MultiLifecycleCallback, type MultiLifecycleHook, type Query, type RelationId, Snapshot, SnapshotDiff, type WildcardRelationId, World, WorldFixture, WorldSnapshot, component, relation };
257
+ export { AssertionError, Assertions, type ComponentDef, type ComponentId, EntityBuilder, type EntityId, EntitySnapshot, type LegacyLifecycleCallback as LifecycleCallback, type LegacyLifecycleHook as LifecycleHook, type LifecycleCallback as MultiLifecycleCallback, type LifecycleHook as MultiLifecycleHook, type Query, type RelationId, Snapshot, SnapshotDiff, type WildcardRelationId, World, WorldFixture, WorldSnapshot, component, relation };
258
258
  //# sourceMappingURL=testing.d.mts.map
package/testing.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"testing.mjs","names":["entitySnapshots: EntitySnapshot[]","addedEntities: EntityId[]","removedEntities: EntityId[]","componentChanges: SnapshotDiff[\"componentChanges\"]","result: Record<string, unknown>"],"sources":["../src/testing/index.ts"],"sourcesContent":["/**\n * @module testing\n * Testing utilities for ECS-based game logic\n *\n * This module provides framework-agnostic testing helpers that work with\n * bun:test, vitest, jest, or any other testing framework.\n *\n * @example\n * ```typescript\n * import { describe, expect, it } from \"bun:test\";\n * import { component } from \"@codehz/ecs\";\n * import { WorldFixture, EntityBuilder, Assertions } from \"@codehz/ecs/testing\";\n *\n * const PositionId = component<{ x: number; y: number }>();\n * const VelocityId = component<{ x: number; y: number }>();\n *\n * describe(\"Movement System\", () => {\n * it(\"should update position based on velocity\", () => {\n * const fixture = new WorldFixture();\n * const entity = fixture\n * .spawn()\n * .with(PositionId, { x: 0, y: 0 })\n * .with(VelocityId, { x: 1, y: 2 })\n * .build();\n *\n * // Run your game logic here\n * movementSystem(fixture.world, 1.0);\n *\n * expect(Assertions.hasComponent(fixture.world, entity, PositionId)).toBe(true);\n * expect(Assertions.getComponent(fixture.world, entity, PositionId)).toEqual({ x: 1, y: 2 });\n * });\n * });\n * ```\n */\n\nimport type { ComponentId, EntityId, WildcardRelationId } from \"../core/entity\";\nimport { isWildcardRelationId, relation } from \"../core/entity\";\nimport { World } from \"../core/world\";\nimport type { Query } from \"../query/query\";\nexport { EntityBuilder } from \"../core/builder\";\nexport type { ComponentDef } from \"../core/builder\";\n\n// =============================================================================\n// Types\n// =============================================================================\n\n/**\n * A component definition for entity building, supporting both regular components and relations\n */\nimport type { EntityBuilder } from \"../core/builder\";\n\n/**\n * Snapshot of a single entity's component state\n */\nexport interface EntitySnapshot {\n entity: EntityId;\n components: Map<EntityId<any>, unknown>;\n}\n\n/**\n * Snapshot of multiple entities' component state\n */\nexport interface WorldSnapshot {\n entities: EntitySnapshot[];\n}\n\n/**\n * Result of comparing two snapshots\n */\nexport interface SnapshotDiff {\n /** Entities that exist in 'after' but not in 'before' */\n addedEntities: EntityId[];\n /** Entities that exist in 'before' but not in 'after' */\n removedEntities: EntityId[];\n /** Changes to components on existing entities */\n componentChanges: Array<{\n entity: EntityId;\n componentId: EntityId<any>;\n before: unknown | undefined;\n after: unknown | undefined;\n changeType: \"added\" | \"removed\" | \"modified\";\n }>;\n}\n\n// =============================================================================\n// WorldFixture - Test World Factory\n// =============================================================================\n\n/**\n * A test fixture that manages a World instance and provides convenient\n * methods for setting up test scenarios.\n *\n * @example\n * ```typescript\n * const fixture = new WorldFixture();\n *\n * // Spawn entities with fluent API\n * const player = fixture\n * .spawn()\n * .with(PositionId, { x: 0, y: 0 })\n * .with(HealthId, { current: 100, max: 100 })\n * .build();\n *\n * // Access the world for running systems\n * movementSystem(fixture.world);\n *\n * // Clean up (optional - creates a fresh world)\n * fixture.reset();\n * ```\n */\nexport class WorldFixture {\n private _world: World;\n private _queries: Query[] = [];\n\n constructor() {\n this._world = new World();\n }\n\n /**\n * Get the underlying World instance\n */\n get world(): World {\n return this._world;\n }\n\n /**\n * Create a new EntityBuilder for spawning an entity with components\n */\n spawn(): EntityBuilder {\n return this._world.spawn();\n }\n\n /**\n * Spawn multiple entities with the same component configuration\n * @param count Number of entities to spawn\n * @param configure Function to configure each entity builder\n * @returns Array of created entity IDs\n */\n spawnMany(count: number, configure: (builder: EntityBuilder, index: number) => EntityBuilder): EntityId[] {\n return this._world.spawnMany(count, configure);\n }\n\n /**\n * Create a query and track it for automatic cleanup\n * @param componentTypes Component types to query for\n * @returns Query instance\n */\n createQuery(componentTypes: EntityId<any>[]): Query {\n const query = this._world.createQuery(componentTypes);\n this._queries.push(query);\n return query;\n }\n\n /**\n * Execute pending commands (alias for world.sync())\n */\n sync(): void {\n this._world.sync();\n }\n\n /**\n * Reset the fixture with a fresh World instance\n * Disposes all tracked queries\n */\n reset(): void {\n for (const query of this._queries) {\n query.dispose();\n }\n this._queries = [];\n this._world = new World();\n }\n\n /**\n * Capture a snapshot of specified entities and their components\n * @param entities Entities to capture\n * @param componentIds Components to include in the snapshot\n */\n captureSnapshot(entities: EntityId[], componentIds: EntityId<any>[]): WorldSnapshot {\n return Snapshot.capture(this._world, entities, componentIds);\n }\n\n /**\n * Symbol.dispose implementation for automatic resource management\n */\n [Symbol.dispose](): void {\n this.reset();\n }\n}\n\n// =============================================================================\n// EntityBuilder - Fluent Entity Creation\n// =============================================================================\n\n/**\n * Fluent builder for creating entities with components.\n * Supports both regular components and entity relations.\n *\n * @example\n * ```typescript\n * // Basic usage\n * // Note: build() will enqueue component commands but will NOT call world.sync().\n * // You must call world.sync() or fixture.sync() manually to apply commands.\n * const entity = new EntityBuilder(world)\n * .with(PositionId, { x: 10, y: 20 })\n * .with(VelocityId, { x: 1, y: 0 })\n * .build();\n * // Apply pending changes\n * world.sync();\n *\n * // With relations\n * const child = new EntityBuilder(world)\n * .with(PositionId, { x: 0, y: 0 })\n * .withRelation(ParentId, parentEntity, { offset: { x: 5, y: 5 } })\n * .build();\n * world.sync();\n *\n * // Tag component (void type)\n * const tagged = new EntityBuilder(world)\n * .withTag(PlayerTagId)\n * .build();\n * ```\n */\n// EntityBuilder is exported from world.ts; testing utilities will use world.spawn()\n\n// =============================================================================\n// Assertions - Test Assertion Helpers\n// =============================================================================\n\n/**\n * Test assertion utilities that return boolean values or throw descriptive errors.\n * These work with any testing framework's expect() function.\n *\n * @example\n * ```typescript\n * // With bun:test or vitest\n * expect(Assertions.hasComponent(world, entity, PositionId)).toBe(true);\n * expect(Assertions.getComponent(world, entity, PositionId)).toEqual({ x: 10, y: 20 });\n *\n * // Direct assertion (throws on failure)\n * Assertions.assertHasComponent(world, entity, PositionId);\n * Assertions.assertComponentEquals(world, entity, PositionId, { x: 10, y: 20 });\n * ```\n */\nexport const Assertions = {\n /**\n * Check if an entity has a specific component\n */\n hasComponent<T>(world: World, entity: EntityId, componentId: EntityId<T>): boolean {\n return world.exists(entity) && world.has(entity, componentId);\n },\n\n /**\n * Check if an entity does not have a specific component\n */\n lacksComponent<T>(world: World, entity: EntityId, componentId: EntityId<T>): boolean {\n return !world.exists(entity) || !world.has(entity, componentId);\n },\n\n /**\n * Get a component value (returns undefined if entity doesn't exist or doesn't have the component)\n */\n getComponent<T>(world: World, entity: EntityId, componentId: EntityId<T>): T | undefined {\n if (!world.exists(entity) || !world.has(entity, componentId)) {\n return undefined;\n }\n return world.get(entity, componentId);\n },\n\n /**\n * Get all relation instances for a wildcard relation\n */\n getRelations<T>(world: World, entity: EntityId, componentId: ComponentId<T>): [EntityId<unknown>, T][] | undefined {\n if (!world.exists(entity)) {\n return undefined;\n }\n const wildcardId = relation(componentId, \"*\");\n try {\n return world.get(entity, wildcardId);\n } catch {\n return [];\n }\n },\n\n /**\n * Check if an entity has a relation to a specific target\n */\n hasRelation<T>(world: World, entity: EntityId, componentId: ComponentId<T>, targetEntity: EntityId<any>): boolean {\n if (!world.exists(entity)) {\n return false;\n }\n const relationId = relation(componentId, targetEntity);\n return world.has(entity, relationId);\n },\n\n /**\n * Check if an entity exists in the world\n */\n entityExists(world: World, entity: EntityId): boolean {\n return world.exists(entity);\n },\n\n /**\n * Check if a query contains specific entities\n */\n queryContains(query: Query, ...entities: EntityId[]): boolean {\n const queryEntities = query.getEntities();\n return entities.every((e) => queryEntities.includes(e));\n },\n\n /**\n * Check if a query contains exactly the specified entities (no more, no less)\n */\n queryContainsExactly(query: Query, ...entities: EntityId[]): boolean {\n const queryEntities = query.getEntities();\n if (queryEntities.length !== entities.length) {\n return false;\n }\n return entities.every((e) => queryEntities.includes(e));\n },\n\n /**\n * Get the count of entities in a query\n */\n queryCount(query: Query): number {\n return query.getEntities().length;\n },\n\n // === Throwing assertions ===\n\n /**\n * Assert that an entity has a component (throws if not)\n */\n assertHasComponent<T>(world: World, entity: EntityId, componentId: EntityId<T>): void {\n if (!world.exists(entity)) {\n throw new AssertionError(`Entity ${entity} does not exist`);\n }\n if (!world.has(entity, componentId)) {\n throw new AssertionError(`Entity ${entity} does not have component ${componentId}`);\n }\n },\n\n /**\n * Assert that an entity lacks a component (throws if it has the component)\n */\n assertLacksComponent<T>(world: World, entity: EntityId, componentId: EntityId<T>): void {\n if (world.exists(entity) && world.has(entity, componentId)) {\n throw new AssertionError(`Entity ${entity} unexpectedly has component ${componentId}`);\n }\n },\n\n /**\n * Assert that a component equals an expected value (throws if not)\n */\n assertComponentEquals<T>(world: World, entity: EntityId, componentId: EntityId<T>, expected: T): void {\n this.assertHasComponent(world, entity, componentId);\n const actual = world.get(entity, componentId);\n if (!deepEquals(actual, expected)) {\n throw new AssertionError(\n `Component ${componentId} on entity ${entity} does not match expected value.\\n` +\n `Expected: ${JSON.stringify(expected)}\\n` +\n `Actual: ${JSON.stringify(actual)}`,\n );\n }\n },\n\n /**\n * Assert that an entity exists (throws if not)\n */\n assertEntityExists(world: World, entity: EntityId): void {\n if (!world.exists(entity)) {\n throw new AssertionError(`Entity ${entity} does not exist`);\n }\n },\n\n /**\n * Assert that an entity does not exist (throws if it exists)\n */\n assertEntityNotExists(world: World, entity: EntityId): void {\n if (world.exists(entity)) {\n throw new AssertionError(`Entity ${entity} unexpectedly exists`);\n }\n },\n\n /**\n * Assert that a query contains specific entities (throws if not)\n */\n assertQueryContains(query: Query, ...entities: EntityId[]): void {\n const queryEntities = query.getEntities();\n for (const entity of entities) {\n if (!queryEntities.includes(entity)) {\n throw new AssertionError(\n `Query does not contain entity ${entity}.\\n` + `Query entities: [${queryEntities.join(\", \")}]`,\n );\n }\n }\n },\n\n /**\n * Assert that a query does not contain specific entities (throws if it does)\n */\n assertQueryNotContains(query: Query, ...entities: EntityId[]): void {\n const queryEntities = query.getEntities();\n for (const entity of entities) {\n if (queryEntities.includes(entity)) {\n throw new AssertionError(\n `Query unexpectedly contains entity ${entity}.\\n` + `Query entities: [${queryEntities.join(\", \")}]`,\n );\n }\n }\n },\n};\n\n// =============================================================================\n// Snapshot - State Capture and Comparison\n// =============================================================================\n\n/**\n * Utilities for capturing and comparing world state snapshots.\n * Useful for testing that systems produce expected state changes.\n *\n * @example\n * ```typescript\n * const before = Snapshot.capture(world, [entity], [PositionId, VelocityId]);\n *\n * // Run game logic\n * movementSystem(world, deltaTime);\n * world.sync();\n *\n * const after = Snapshot.capture(world, [entity], [PositionId, VelocityId]);\n * const diff = Snapshot.compare(before, after);\n *\n * expect(diff.componentChanges).toHaveLength(1);\n * expect(diff.componentChanges[0].changeType).toBe(\"modified\");\n * ```\n */\nexport const Snapshot = {\n /**\n * Capture a snapshot of specified entities and their components\n * @param world The world to capture from\n * @param entities Entities to include in the snapshot\n * @param componentIds Components to capture for each entity\n */\n capture(world: World, entities: EntityId[], componentIds: EntityId<any>[]): WorldSnapshot {\n const entitySnapshots: EntitySnapshot[] = [];\n\n for (const entity of entities) {\n if (!world.exists(entity)) {\n continue;\n }\n\n const components = new Map<EntityId<any>, unknown>();\n\n for (const componentId of componentIds) {\n if (isWildcardRelationId(componentId)) {\n // For wildcard relations, capture all relation instances\n try {\n const relations = world.get(entity, componentId as WildcardRelationId<any>);\n if (relations && relations.length > 0) {\n components.set(componentId, deepClone(relations));\n }\n } catch {\n // Entity doesn't have this relation type\n }\n } else if (world.has(entity, componentId)) {\n components.set(componentId, deepClone(world.get(entity, componentId)));\n }\n }\n\n entitySnapshots.push({ entity, components });\n }\n\n return { entities: entitySnapshots };\n },\n\n /**\n * Compare two snapshots and return the differences\n * @param before The 'before' snapshot\n * @param after The 'after' snapshot\n */\n compare(before: WorldSnapshot, after: WorldSnapshot): SnapshotDiff {\n const beforeEntities = new Set(before.entities.map((e) => e.entity));\n const afterEntities = new Set(after.entities.map((e) => e.entity));\n\n const addedEntities: EntityId[] = [];\n const removedEntities: EntityId[] = [];\n const componentChanges: SnapshotDiff[\"componentChanges\"] = [];\n\n // Find added entities\n for (const entity of afterEntities) {\n if (!beforeEntities.has(entity)) {\n addedEntities.push(entity);\n }\n }\n\n // Find removed entities\n for (const entity of beforeEntities) {\n if (!afterEntities.has(entity)) {\n removedEntities.push(entity);\n }\n }\n\n // Find component changes on existing entities\n const beforeMap = new Map(before.entities.map((e) => [e.entity, e]));\n const afterMap = new Map(after.entities.map((e) => [e.entity, e]));\n\n for (const entity of beforeEntities) {\n if (!afterEntities.has(entity)) continue; // Skip removed entities\n\n const beforeEntity = beforeMap.get(entity)!;\n const afterEntity = afterMap.get(entity)!;\n\n // Check for component changes\n const allComponentIds = new Set([...beforeEntity.components.keys(), ...afterEntity.components.keys()]);\n\n for (const componentId of allComponentIds) {\n const beforeValue = beforeEntity.components.get(componentId);\n const afterValue = afterEntity.components.get(componentId);\n\n if (beforeValue === undefined && afterValue !== undefined) {\n componentChanges.push({\n entity,\n componentId,\n before: undefined,\n after: afterValue,\n changeType: \"added\",\n });\n } else if (beforeValue !== undefined && afterValue === undefined) {\n componentChanges.push({\n entity,\n componentId,\n before: beforeValue,\n after: undefined,\n changeType: \"removed\",\n });\n } else if (!deepEquals(beforeValue, afterValue)) {\n componentChanges.push({\n entity,\n componentId,\n before: beforeValue,\n after: afterValue,\n changeType: \"modified\",\n });\n }\n }\n }\n\n return { addedEntities, removedEntities, componentChanges };\n },\n\n /**\n * Check if two snapshots are equal\n */\n equals(a: WorldSnapshot, b: WorldSnapshot): boolean {\n const diff = this.compare(a, b);\n return diff.addedEntities.length === 0 && diff.removedEntities.length === 0 && diff.componentChanges.length === 0;\n },\n};\n\n// =============================================================================\n// Utilities\n// =============================================================================\n\n/**\n * Custom assertion error for testing utilities\n */\nexport class AssertionError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"AssertionError\";\n }\n}\n\n/**\n * Deep equality check for comparing component values\n */\nfunction deepEquals(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (a === null || b === null) return false;\n if (typeof a !== typeof b) return false;\n\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (!deepEquals(a[i], b[i])) return false;\n }\n return true;\n }\n\n if (typeof a === \"object\" && typeof b === \"object\") {\n const aObj = a as Record<string, unknown>;\n const bObj = b as Record<string, unknown>;\n const aKeys = Object.keys(aObj);\n const bKeys = Object.keys(bObj);\n\n if (aKeys.length !== bKeys.length) return false;\n\n for (const key of aKeys) {\n if (!Object.prototype.hasOwnProperty.call(bObj, key)) return false;\n if (!deepEquals(aObj[key], bObj[key])) return false;\n }\n return true;\n }\n\n return false;\n}\n\n/**\n * Deep clone a value for snapshot isolation\n */\nfunction deepClone<T>(value: T): T {\n if (value === null || typeof value !== \"object\") {\n return value;\n }\n\n if (Array.isArray(value)) {\n return value.map(deepClone) as T;\n }\n\n const result: Record<string, unknown> = {};\n for (const key of Object.keys(value)) {\n result[key] = deepClone((value as Record<string, unknown>)[key]);\n }\n return result as T;\n}\n\n// =============================================================================\n// Re-exports for convenience\n// =============================================================================\n\nexport { component, relation } from \"../core/entity\";\nexport type { ComponentId, EntityId, RelationId, WildcardRelationId } from \"../core/entity\";\nexport type { LifecycleCallback, LifecycleHook, MultiLifecycleCallback, MultiLifecycleHook } from \"../core/types\";\nexport { World } from \"../core/world\";\nexport type { Query } from \"../query/query\";\n"],"mappings":"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"}
1
+ {"version":3,"file":"testing.mjs","names":["entitySnapshots: EntitySnapshot[]","addedEntities: EntityId[]","removedEntities: EntityId[]","componentChanges: SnapshotDiff[\"componentChanges\"]","result: Record<string, unknown>"],"sources":["../src/testing/index.ts"],"sourcesContent":["/**\n * @module testing\n * Testing utilities for ECS-based game logic\n *\n * This module provides framework-agnostic testing helpers that work with\n * bun:test, vitest, jest, or any other testing framework.\n *\n * @example\n * ```typescript\n * import { describe, expect, it } from \"bun:test\";\n * import { component } from \"@codehz/ecs\";\n * import { WorldFixture, EntityBuilder, Assertions } from \"@codehz/ecs/testing\";\n *\n * const PositionId = component<{ x: number; y: number }>();\n * const VelocityId = component<{ x: number; y: number }>();\n *\n * describe(\"Movement System\", () => {\n * it(\"should update position based on velocity\", () => {\n * const fixture = new WorldFixture();\n * const entity = fixture\n * .spawn()\n * .with(PositionId, { x: 0, y: 0 })\n * .with(VelocityId, { x: 1, y: 2 })\n * .build();\n *\n * // Run your game logic here\n * movementSystem(fixture.world, 1.0);\n *\n * expect(Assertions.hasComponent(fixture.world, entity, PositionId)).toBe(true);\n * expect(Assertions.getComponent(fixture.world, entity, PositionId)).toEqual({ x: 1, y: 2 });\n * });\n * });\n * ```\n */\n\nimport type { ComponentId, EntityId, WildcardRelationId } from \"../core/entity\";\nimport { isWildcardRelationId, relation } from \"../core/entity\";\nimport { World } from \"../core/world\";\nimport type { Query } from \"../query/query\";\nexport { EntityBuilder } from \"../core/builder\";\nexport type { ComponentDef } from \"../core/builder\";\n\n// =============================================================================\n// Types\n// =============================================================================\n\n/**\n * A component definition for entity building, supporting both regular components and relations\n */\nimport type { EntityBuilder } from \"../core/builder\";\n\n/**\n * Snapshot of a single entity's component state\n */\nexport interface EntitySnapshot {\n entity: EntityId;\n components: Map<EntityId<any>, unknown>;\n}\n\n/**\n * Snapshot of multiple entities' component state\n */\nexport interface WorldSnapshot {\n entities: EntitySnapshot[];\n}\n\n/**\n * Result of comparing two snapshots\n */\nexport interface SnapshotDiff {\n /** Entities that exist in 'after' but not in 'before' */\n addedEntities: EntityId[];\n /** Entities that exist in 'before' but not in 'after' */\n removedEntities: EntityId[];\n /** Changes to components on existing entities */\n componentChanges: Array<{\n entity: EntityId;\n componentId: EntityId<any>;\n before: unknown | undefined;\n after: unknown | undefined;\n changeType: \"added\" | \"removed\" | \"modified\";\n }>;\n}\n\n// =============================================================================\n// WorldFixture - Test World Factory\n// =============================================================================\n\n/**\n * A test fixture that manages a World instance and provides convenient\n * methods for setting up test scenarios.\n *\n * @example\n * ```typescript\n * const fixture = new WorldFixture();\n *\n * // Spawn entities with fluent API\n * const player = fixture\n * .spawn()\n * .with(PositionId, { x: 0, y: 0 })\n * .with(HealthId, { current: 100, max: 100 })\n * .build();\n *\n * // Access the world for running systems\n * movementSystem(fixture.world);\n *\n * // Clean up (optional - creates a fresh world)\n * fixture.reset();\n * ```\n */\nexport class WorldFixture {\n private _world: World;\n private _queries: Query[] = [];\n\n constructor() {\n this._world = new World();\n }\n\n /**\n * Get the underlying World instance\n */\n get world(): World {\n return this._world;\n }\n\n /**\n * Create a new EntityBuilder for spawning an entity with components\n */\n spawn(): EntityBuilder {\n return this._world.spawn();\n }\n\n /**\n * Spawn multiple entities with the same component configuration\n * @param count Number of entities to spawn\n * @param configure Function to configure each entity builder\n * @returns Array of created entity IDs\n */\n spawnMany(count: number, configure: (builder: EntityBuilder, index: number) => EntityBuilder): EntityId[] {\n return this._world.spawnMany(count, configure);\n }\n\n /**\n * Create a query and track it for automatic cleanup\n * @param componentTypes Component types to query for\n * @returns Query instance\n */\n createQuery(componentTypes: EntityId<any>[]): Query {\n const query = this._world.createQuery(componentTypes);\n this._queries.push(query);\n return query;\n }\n\n /**\n * Execute pending commands (alias for world.sync())\n */\n sync(): void {\n this._world.sync();\n }\n\n /**\n * Reset the fixture with a fresh World instance\n * Disposes all tracked queries\n */\n reset(): void {\n for (const query of this._queries) {\n query.dispose();\n }\n this._queries = [];\n this._world = new World();\n }\n\n /**\n * Capture a snapshot of specified entities and their components\n * @param entities Entities to capture\n * @param componentIds Components to include in the snapshot\n */\n captureSnapshot(entities: EntityId[], componentIds: EntityId<any>[]): WorldSnapshot {\n return Snapshot.capture(this._world, entities, componentIds);\n }\n\n /**\n * Symbol.dispose implementation for automatic resource management\n */\n [Symbol.dispose](): void {\n this.reset();\n }\n}\n\n// =============================================================================\n// EntityBuilder - Fluent Entity Creation\n// =============================================================================\n\n/**\n * Fluent builder for creating entities with components.\n * Supports both regular components and entity relations.\n *\n * @example\n * ```typescript\n * // Basic usage\n * // Note: build() will enqueue component commands but will NOT call world.sync().\n * // You must call world.sync() or fixture.sync() manually to apply commands.\n * const entity = new EntityBuilder(world)\n * .with(PositionId, { x: 10, y: 20 })\n * .with(VelocityId, { x: 1, y: 0 })\n * .build();\n * // Apply pending changes\n * world.sync();\n *\n * // With relations\n * const child = new EntityBuilder(world)\n * .with(PositionId, { x: 0, y: 0 })\n * .withRelation(ParentId, parentEntity, { offset: { x: 5, y: 5 } })\n * .build();\n * world.sync();\n *\n * // Tag component (void type)\n * const tagged = new EntityBuilder(world)\n * .withTag(PlayerTagId)\n * .build();\n * ```\n */\n// EntityBuilder is exported from world.ts; testing utilities will use world.spawn()\n\n// =============================================================================\n// Assertions - Test Assertion Helpers\n// =============================================================================\n\n/**\n * Test assertion utilities that return boolean values or throw descriptive errors.\n * These work with any testing framework's expect() function.\n *\n * @example\n * ```typescript\n * // With bun:test or vitest\n * expect(Assertions.hasComponent(world, entity, PositionId)).toBe(true);\n * expect(Assertions.getComponent(world, entity, PositionId)).toEqual({ x: 10, y: 20 });\n *\n * // Direct assertion (throws on failure)\n * Assertions.assertHasComponent(world, entity, PositionId);\n * Assertions.assertComponentEquals(world, entity, PositionId, { x: 10, y: 20 });\n * ```\n */\nexport const Assertions = {\n /**\n * Check if an entity has a specific component\n */\n hasComponent<T>(world: World, entity: EntityId, componentId: EntityId<T>): boolean {\n return world.exists(entity) && world.has(entity, componentId);\n },\n\n /**\n * Check if an entity does not have a specific component\n */\n lacksComponent<T>(world: World, entity: EntityId, componentId: EntityId<T>): boolean {\n return !world.exists(entity) || !world.has(entity, componentId);\n },\n\n /**\n * Get a component value (returns undefined if entity doesn't exist or doesn't have the component)\n */\n getComponent<T>(world: World, entity: EntityId, componentId: EntityId<T>): T | undefined {\n if (!world.exists(entity) || !world.has(entity, componentId)) {\n return undefined;\n }\n return world.get(entity, componentId);\n },\n\n /**\n * Get all relation instances for a wildcard relation\n */\n getRelations<T>(world: World, entity: EntityId, componentId: ComponentId<T>): [EntityId<unknown>, T][] | undefined {\n if (!world.exists(entity)) {\n return undefined;\n }\n const wildcardId = relation(componentId, \"*\");\n try {\n return world.get(entity, wildcardId);\n } catch {\n return [];\n }\n },\n\n /**\n * Check if an entity has a relation to a specific target\n */\n hasRelation<T>(world: World, entity: EntityId, componentId: ComponentId<T>, targetEntity: EntityId<any>): boolean {\n if (!world.exists(entity)) {\n return false;\n }\n const relationId = relation(componentId, targetEntity);\n return world.has(entity, relationId);\n },\n\n /**\n * Check if an entity exists in the world\n */\n entityExists(world: World, entity: EntityId): boolean {\n return world.exists(entity);\n },\n\n /**\n * Check if a query contains specific entities\n */\n queryContains(query: Query, ...entities: EntityId[]): boolean {\n const queryEntities = query.getEntities();\n return entities.every((e) => queryEntities.includes(e));\n },\n\n /**\n * Check if a query contains exactly the specified entities (no more, no less)\n */\n queryContainsExactly(query: Query, ...entities: EntityId[]): boolean {\n const queryEntities = query.getEntities();\n if (queryEntities.length !== entities.length) {\n return false;\n }\n return entities.every((e) => queryEntities.includes(e));\n },\n\n /**\n * Get the count of entities in a query\n */\n queryCount(query: Query): number {\n return query.getEntities().length;\n },\n\n // === Throwing assertions ===\n\n /**\n * Assert that an entity has a component (throws if not)\n */\n assertHasComponent<T>(world: World, entity: EntityId, componentId: EntityId<T>): void {\n if (!world.exists(entity)) {\n throw new AssertionError(`Entity ${entity} does not exist`);\n }\n if (!world.has(entity, componentId)) {\n throw new AssertionError(`Entity ${entity} does not have component ${componentId}`);\n }\n },\n\n /**\n * Assert that an entity lacks a component (throws if it has the component)\n */\n assertLacksComponent<T>(world: World, entity: EntityId, componentId: EntityId<T>): void {\n if (world.exists(entity) && world.has(entity, componentId)) {\n throw new AssertionError(`Entity ${entity} unexpectedly has component ${componentId}`);\n }\n },\n\n /**\n * Assert that a component equals an expected value (throws if not)\n */\n assertComponentEquals<T>(world: World, entity: EntityId, componentId: EntityId<T>, expected: T): void {\n this.assertHasComponent(world, entity, componentId);\n const actual = world.get(entity, componentId);\n if (!deepEquals(actual, expected)) {\n throw new AssertionError(\n `Component ${componentId} on entity ${entity} does not match expected value.\\n` +\n `Expected: ${JSON.stringify(expected)}\\n` +\n `Actual: ${JSON.stringify(actual)}`,\n );\n }\n },\n\n /**\n * Assert that an entity exists (throws if not)\n */\n assertEntityExists(world: World, entity: EntityId): void {\n if (!world.exists(entity)) {\n throw new AssertionError(`Entity ${entity} does not exist`);\n }\n },\n\n /**\n * Assert that an entity does not exist (throws if it exists)\n */\n assertEntityNotExists(world: World, entity: EntityId): void {\n if (world.exists(entity)) {\n throw new AssertionError(`Entity ${entity} unexpectedly exists`);\n }\n },\n\n /**\n * Assert that a query contains specific entities (throws if not)\n */\n assertQueryContains(query: Query, ...entities: EntityId[]): void {\n const queryEntities = query.getEntities();\n for (const entity of entities) {\n if (!queryEntities.includes(entity)) {\n throw new AssertionError(\n `Query does not contain entity ${entity}.\\n` + `Query entities: [${queryEntities.join(\", \")}]`,\n );\n }\n }\n },\n\n /**\n * Assert that a query does not contain specific entities (throws if it does)\n */\n assertQueryNotContains(query: Query, ...entities: EntityId[]): void {\n const queryEntities = query.getEntities();\n for (const entity of entities) {\n if (queryEntities.includes(entity)) {\n throw new AssertionError(\n `Query unexpectedly contains entity ${entity}.\\n` + `Query entities: [${queryEntities.join(\", \")}]`,\n );\n }\n }\n },\n};\n\n// =============================================================================\n// Snapshot - State Capture and Comparison\n// =============================================================================\n\n/**\n * Utilities for capturing and comparing world state snapshots.\n * Useful for testing that systems produce expected state changes.\n *\n * @example\n * ```typescript\n * const before = Snapshot.capture(world, [entity], [PositionId, VelocityId]);\n *\n * // Run game logic\n * movementSystem(world, deltaTime);\n * world.sync();\n *\n * const after = Snapshot.capture(world, [entity], [PositionId, VelocityId]);\n * const diff = Snapshot.compare(before, after);\n *\n * expect(diff.componentChanges).toHaveLength(1);\n * expect(diff.componentChanges[0].changeType).toBe(\"modified\");\n * ```\n */\nexport const Snapshot = {\n /**\n * Capture a snapshot of specified entities and their components\n * @param world The world to capture from\n * @param entities Entities to include in the snapshot\n * @param componentIds Components to capture for each entity\n */\n capture(world: World, entities: EntityId[], componentIds: EntityId<any>[]): WorldSnapshot {\n const entitySnapshots: EntitySnapshot[] = [];\n\n for (const entity of entities) {\n if (!world.exists(entity)) {\n continue;\n }\n\n const components = new Map<EntityId<any>, unknown>();\n\n for (const componentId of componentIds) {\n if (isWildcardRelationId(componentId)) {\n // For wildcard relations, capture all relation instances\n try {\n const relations = world.get(entity, componentId as WildcardRelationId<any>);\n if (relations && relations.length > 0) {\n components.set(componentId, deepClone(relations));\n }\n } catch {\n // Entity doesn't have this relation type\n }\n } else if (world.has(entity, componentId)) {\n components.set(componentId, deepClone(world.get(entity, componentId)));\n }\n }\n\n entitySnapshots.push({ entity, components });\n }\n\n return { entities: entitySnapshots };\n },\n\n /**\n * Compare two snapshots and return the differences\n * @param before The 'before' snapshot\n * @param after The 'after' snapshot\n */\n compare(before: WorldSnapshot, after: WorldSnapshot): SnapshotDiff {\n const beforeEntities = new Set(before.entities.map((e) => e.entity));\n const afterEntities = new Set(after.entities.map((e) => e.entity));\n\n const addedEntities: EntityId[] = [];\n const removedEntities: EntityId[] = [];\n const componentChanges: SnapshotDiff[\"componentChanges\"] = [];\n\n // Find added entities\n for (const entity of afterEntities) {\n if (!beforeEntities.has(entity)) {\n addedEntities.push(entity);\n }\n }\n\n // Find removed entities\n for (const entity of beforeEntities) {\n if (!afterEntities.has(entity)) {\n removedEntities.push(entity);\n }\n }\n\n // Find component changes on existing entities\n const beforeMap = new Map(before.entities.map((e) => [e.entity, e]));\n const afterMap = new Map(after.entities.map((e) => [e.entity, e]));\n\n for (const entity of beforeEntities) {\n if (!afterEntities.has(entity)) continue; // Skip removed entities\n\n const beforeEntity = beforeMap.get(entity)!;\n const afterEntity = afterMap.get(entity)!;\n\n // Check for component changes\n const allComponentIds = new Set([...beforeEntity.components.keys(), ...afterEntity.components.keys()]);\n\n for (const componentId of allComponentIds) {\n const beforeValue = beforeEntity.components.get(componentId);\n const afterValue = afterEntity.components.get(componentId);\n\n if (beforeValue === undefined && afterValue !== undefined) {\n componentChanges.push({\n entity,\n componentId,\n before: undefined,\n after: afterValue,\n changeType: \"added\",\n });\n } else if (beforeValue !== undefined && afterValue === undefined) {\n componentChanges.push({\n entity,\n componentId,\n before: beforeValue,\n after: undefined,\n changeType: \"removed\",\n });\n } else if (!deepEquals(beforeValue, afterValue)) {\n componentChanges.push({\n entity,\n componentId,\n before: beforeValue,\n after: afterValue,\n changeType: \"modified\",\n });\n }\n }\n }\n\n return { addedEntities, removedEntities, componentChanges };\n },\n\n /**\n * Check if two snapshots are equal\n */\n equals(a: WorldSnapshot, b: WorldSnapshot): boolean {\n const diff = this.compare(a, b);\n return diff.addedEntities.length === 0 && diff.removedEntities.length === 0 && diff.componentChanges.length === 0;\n },\n};\n\n// =============================================================================\n// Utilities\n// =============================================================================\n\n/**\n * Custom assertion error for testing utilities\n */\nexport class AssertionError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"AssertionError\";\n }\n}\n\n/**\n * Deep equality check for comparing component values\n */\nfunction deepEquals(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (a === null || b === null) return false;\n if (typeof a !== typeof b) return false;\n\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (!deepEquals(a[i], b[i])) return false;\n }\n return true;\n }\n\n if (typeof a === \"object\" && typeof b === \"object\") {\n const aObj = a as Record<string, unknown>;\n const bObj = b as Record<string, unknown>;\n const aKeys = Object.keys(aObj);\n const bKeys = Object.keys(bObj);\n\n if (aKeys.length !== bKeys.length) return false;\n\n for (const key of aKeys) {\n if (!Object.prototype.hasOwnProperty.call(bObj, key)) return false;\n if (!deepEquals(aObj[key], bObj[key])) return false;\n }\n return true;\n }\n\n return false;\n}\n\n/**\n * Deep clone a value for snapshot isolation\n */\nfunction deepClone<T>(value: T): T {\n if (value === null || typeof value !== \"object\") {\n return value;\n }\n\n if (Array.isArray(value)) {\n return value.map(deepClone) as T;\n }\n\n const result: Record<string, unknown> = {};\n for (const key of Object.keys(value)) {\n result[key] = deepClone((value as Record<string, unknown>)[key]);\n }\n return result as T;\n}\n\n// =============================================================================\n// Re-exports for convenience\n// =============================================================================\n\nexport { component, relation } from \"../core/entity\";\nexport type { ComponentId, EntityId, RelationId, WildcardRelationId } from \"../core/entity\";\nexport type {\n LegacyLifecycleCallback as LifecycleCallback,\n LegacyLifecycleHook as LifecycleHook,\n LifecycleCallback as MultiLifecycleCallback,\n LifecycleHook as MultiLifecycleHook,\n} from \"../core/types\";\nexport { World } from \"../core/world\";\nexport type { Query } from \"../query/query\";\n"],"mappings":"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"}
package/world.mjs CHANGED
@@ -1039,6 +1039,13 @@ var Archetype = class {
1039
1039
  /**
1040
1040
  * Cache for pre-computed component data sources to avoid repeated calculations
1041
1041
  */
1042
+ /**
1043
+ * Multi-hooks that match this archetype
1044
+ */
1045
+ matchingMultiHooks = /* @__PURE__ */ new Set();
1046
+ /**
1047
+ * Cache for pre-computed component data sources to avoid repeated calculations
1048
+ */
1042
1049
  componentDataSourcesCache = /* @__PURE__ */ new Map();
1043
1050
  constructor(componentTypes, dontFragmentRelations) {
1044
1051
  this.componentTypes = [...componentTypes].sort((a, b) => a - b);
@@ -1406,10 +1413,16 @@ function applyChangeset(ctx, entityId, currentArchetype, changeset, entityToArch
1406
1413
  const allCurrentComponentTypes = currentEntityData ? Array.from(currentEntityData.keys()) : currentArchetype.componentTypes;
1407
1414
  const finalComponentTypes = changeset.getFinalComponentTypes(allCurrentComponentTypes);
1408
1415
  const removedComponents = /* @__PURE__ */ new Map();
1409
- if (finalComponentTypes) if (!areComponentTypesEqual(filterRegularComponentTypes(allCurrentComponentTypes), filterRegularComponentTypes(finalComponentTypes))) moveEntityToNewArchetype(ctx, entityId, currentArchetype, finalComponentTypes, changeset, removedComponents, entityToArchetype);
1416
+ if (finalComponentTypes) if (!areComponentTypesEqual(filterRegularComponentTypes(allCurrentComponentTypes), filterRegularComponentTypes(finalComponentTypes))) return {
1417
+ removedComponents,
1418
+ newArchetype: moveEntityToNewArchetype(ctx, entityId, currentArchetype, finalComponentTypes, changeset, removedComponents, entityToArchetype)
1419
+ };
1410
1420
  else updateEntityInSameArchetype(ctx, entityId, currentArchetype, changeset, removedComponents);
1411
1421
  else updateEntityInSameArchetype(ctx, entityId, currentArchetype, changeset, removedComponents);
1412
- return removedComponents;
1422
+ return {
1423
+ removedComponents,
1424
+ newArchetype: currentArchetype
1425
+ };
1413
1426
  }
1414
1427
  function moveEntityToNewArchetype(ctx, entityId, currentArchetype, finalComponentTypes, changeset, removedComponents, entityToArchetype) {
1415
1428
  const newArchetype = ctx.ensureArchetype(finalComponentTypes);
@@ -1417,6 +1430,7 @@ function moveEntityToNewArchetype(ctx, entityId, currentArchetype, finalComponen
1417
1430
  for (const componentType of changeset.removes) removedComponents.set(componentType, currentComponents.get(componentType));
1418
1431
  newArchetype.addEntity(entityId, changeset.applyTo(currentComponents));
1419
1432
  entityToArchetype.set(entityId, newArchetype);
1433
+ return newArchetype;
1420
1434
  }
1421
1435
  function updateEntityInSameArchetype(ctx, entityId, currentArchetype, changeset, removedComponents) {
1422
1436
  applyDontFragmentChanges(ctx.dontFragmentRelations, entityId, changeset, removedComponents);
@@ -1466,10 +1480,38 @@ function areComponentTypesEqual(types1, types2) {
1466
1480
 
1467
1481
  //#endregion
1468
1482
  //#region src/core/world-hooks.ts
1469
- function triggerLifecycleHooks(ctx, entityId, addedComponents, removedComponents) {
1483
+ /**
1484
+ * Check if a component change matches a hook component type.
1485
+ * Handles wildcard-relation matching: if hookComponent is a wildcard relation (e.g., relation(A, "*")),
1486
+ * it matches any concrete relation with the same component ID (e.g., relation(A, entity1)).
1487
+ */
1488
+ function componentMatchesHookType(changedComponent, hookComponent) {
1489
+ if (changedComponent === hookComponent) return true;
1490
+ if (isWildcardRelationId(hookComponent)) {
1491
+ const hookComponentId = getComponentIdFromRelationId(hookComponent);
1492
+ const changedComponentId = getComponentIdFromRelationId(changedComponent);
1493
+ if (hookComponentId !== void 0 && changedComponentId !== void 0) return hookComponentId === changedComponentId;
1494
+ }
1495
+ return false;
1496
+ }
1497
+ /**
1498
+ * Check if any component in the changes map matches a hook component type.
1499
+ */
1500
+ function anyComponentMatches(changes, hookComponent) {
1501
+ for (const changedComponent of changes.keys()) if (componentMatchesHookType(changedComponent, hookComponent)) return true;
1502
+ return false;
1503
+ }
1504
+ /**
1505
+ * Find a matching component in the changes map that matches the hook component type.
1506
+ * Returns [componentId, value] if found, undefined otherwise.
1507
+ */
1508
+ function findMatchingComponent(changes, hookComponent) {
1509
+ for (const [changedComponent, value] of changes.entries()) if (componentMatchesHookType(changedComponent, hookComponent)) return [changedComponent, value];
1510
+ }
1511
+ function triggerLifecycleHooks(ctx, entityId, addedComponents, removedComponents, oldArchetype, newArchetype) {
1470
1512
  invokeHooksForComponents(ctx.hooks, entityId, addedComponents, "on_set");
1471
1513
  invokeHooksForComponents(ctx.hooks, entityId, removedComponents, "on_remove");
1472
- triggerMultiComponentHooks(ctx, entityId, addedComponents, removedComponents);
1514
+ triggerMultiComponentHooks(ctx, entityId, addedComponents, removedComponents, oldArchetype, newArchetype);
1473
1515
  }
1474
1516
  function invokeHooksForComponents(hooks, entityId, components, hookType) {
1475
1517
  for (const [componentType, component$1] of components) {
@@ -1482,20 +1524,42 @@ function invokeHooksForComponents(hooks, entityId, components, hookType) {
1482
1524
  }
1483
1525
  }
1484
1526
  }
1485
- function triggerMultiComponentHooks(ctx, entityId, addedComponents, removedComponents) {
1486
- for (const { componentTypes, requiredComponents, optionalComponents, hook } of ctx.multiHooks) {
1487
- const anyRequiredAdded = requiredComponents.some((c) => addedComponents.has(c));
1488
- const anyOptionalAdded = optionalComponents.some((c) => addedComponents.has(c));
1489
- const anyRequiredRemoved = requiredComponents.some((c) => removedComponents.has(c));
1490
- if ((anyRequiredAdded || anyOptionalAdded) && hook.on_set && entityHasAllComponents(ctx, entityId, requiredComponents)) hook.on_set(entityId, componentTypes, collectMultiHookComponents(ctx, entityId, componentTypes));
1491
- if (anyRequiredRemoved && hook.on_remove && entityHadAllComponentsBefore(ctx, entityId, requiredComponents, removedComponents)) hook.on_remove(entityId, componentTypes, collectMultiHookComponentsWithRemoved(ctx, entityId, componentTypes, removedComponents));
1527
+ function triggerMultiComponentHooks(ctx, entityId, addedComponents, removedComponents, oldArchetype, newArchetype) {
1528
+ if (addedComponents.size > 0) for (const entry of newArchetype.matchingMultiHooks) {
1529
+ const { hook, requiredComponents, optionalComponents, componentTypes } = entry;
1530
+ if (!hook.on_set) continue;
1531
+ const anyRequiredAdded = requiredComponents.some((c) => anyComponentMatches(addedComponents, c));
1532
+ const anyOptionalAdded = optionalComponents.some((c) => anyComponentMatches(addedComponents, c));
1533
+ if ((anyRequiredAdded || anyOptionalAdded) && entityHasAllComponents(ctx, entityId, requiredComponents)) hook.on_set(entityId, ...collectMultiHookComponents(ctx, entityId, componentTypes));
1534
+ }
1535
+ if (removedComponents.size > 0) for (const entry of oldArchetype.matchingMultiHooks) {
1536
+ const { hook, requiredComponents, componentTypes } = entry;
1537
+ if (!hook.on_remove) continue;
1538
+ if (requiredComponents.some((c) => anyComponentMatches(removedComponents, c)) && entityHadAllComponentsBefore(ctx, entityId, requiredComponents, removedComponents)) hook.on_remove(entityId, ...collectMultiHookComponentsWithRemoved(ctx, entityId, componentTypes, removedComponents));
1492
1539
  }
1493
1540
  }
1494
1541
  function entityHasAllComponents(ctx, entityId, requiredComponents) {
1495
- return requiredComponents.every((c) => ctx.has(entityId, c));
1542
+ return requiredComponents.every((c) => {
1543
+ if (isWildcardRelationId(c)) try {
1544
+ const wildcardData = ctx.get(entityId, c);
1545
+ return Array.isArray(wildcardData) && wildcardData.length > 0;
1546
+ } catch {
1547
+ return false;
1548
+ }
1549
+ return ctx.has(entityId, c);
1550
+ });
1496
1551
  }
1497
1552
  function entityHadAllComponentsBefore(ctx, entityId, requiredComponents, removedComponents) {
1498
- return requiredComponents.every((c) => removedComponents.has(c) || ctx.has(entityId, c));
1553
+ return requiredComponents.every((c) => {
1554
+ if (anyComponentMatches(removedComponents, c)) return true;
1555
+ if (isWildcardRelationId(c)) try {
1556
+ const wildcardData = ctx.get(entityId, c);
1557
+ return Array.isArray(wildcardData) && wildcardData.length > 0;
1558
+ } catch {
1559
+ return false;
1560
+ }
1561
+ return ctx.has(entityId, c);
1562
+ });
1499
1563
  }
1500
1564
  function collectMultiHookComponents(ctx, entityId, componentTypes) {
1501
1565
  return componentTypes.map((ct) => isOptionalEntityId(ct) ? ctx.getOptional(entityId, ct.optional) : ctx.get(entityId, ct));
@@ -1504,10 +1568,12 @@ function collectMultiHookComponentsWithRemoved(ctx, entityId, componentTypes, re
1504
1568
  return componentTypes.map((ct) => {
1505
1569
  if (isOptionalEntityId(ct)) {
1506
1570
  const optionalId = ct.optional;
1507
- return removedComponents.has(optionalId) ? { value: removedComponents.get(optionalId) } : ctx.getOptional(entityId, optionalId);
1571
+ const match$1 = findMatchingComponent(removedComponents, optionalId);
1572
+ return match$1 ? { value: match$1[1] } : ctx.getOptional(entityId, optionalId);
1508
1573
  }
1509
1574
  const compId = ct;
1510
- return removedComponents.has(compId) ? removedComponents.get(compId) : ctx.get(entityId, compId);
1575
+ const match = findMatchingComponent(removedComponents, compId);
1576
+ return match ? match[1] : ctx.get(entityId, compId);
1511
1577
  });
1512
1578
  }
1513
1579
 
@@ -1614,8 +1680,8 @@ var World = class {
1614
1680
  queries = [];
1615
1681
  queryCache = /* @__PURE__ */ new Map();
1616
1682
  commandBuffer = new CommandBuffer((entityId, commands) => this.executeEntityCommands(entityId, commands));
1617
- hooks = /* @__PURE__ */ new Map();
1618
- multiHooks = /* @__PURE__ */ new Set();
1683
+ legacyHooks = /* @__PURE__ */ new Map();
1684
+ hooks = /* @__PURE__ */ new Set();
1619
1685
  constructor(snapshot) {
1620
1686
  if (snapshot && typeof snapshot === "object") this.deserializeSnapshot(snapshot);
1621
1687
  }
@@ -1714,16 +1780,20 @@ var World = class {
1714
1780
  getOptional(entityId, componentType) {
1715
1781
  const archetype = this.entityToArchetype.get(entityId);
1716
1782
  if (!archetype) throw new Error(`Entity ${entityId} does not exist`);
1717
- if (isWildcardRelationId(componentType)) return void 0;
1783
+ if (isWildcardRelationId(componentType)) {
1784
+ const wildcardData = archetype.get(entityId, componentType);
1785
+ if (Array.isArray(wildcardData) && wildcardData.length > 0) return { value: wildcardData };
1786
+ return;
1787
+ }
1718
1788
  return archetype.getOptional(entityId, componentType);
1719
1789
  }
1720
1790
  hook(componentTypesOrSingle, hook) {
1721
1791
  if (typeof hook === "function") if (Array.isArray(componentTypesOrSingle)) {
1722
1792
  const callback = hook;
1723
1793
  hook = {
1724
- on_init: (entityId, componentTypes, components) => callback("init", entityId, componentTypes, components),
1725
- on_set: (entityId, componentTypes, components) => callback("set", entityId, componentTypes, components),
1726
- on_remove: (entityId, componentTypes, components) => callback("remove", entityId, componentTypes, components)
1794
+ on_init: (entityId, ...components) => callback("init", entityId, ...components),
1795
+ on_set: (entityId, ...components) => callback("set", entityId, ...components),
1796
+ on_remove: (entityId, ...components) => callback("remove", entityId, ...components)
1727
1797
  };
1728
1798
  } else {
1729
1799
  const callback = hook;
@@ -1745,19 +1815,20 @@ var World = class {
1745
1815
  optionalComponents,
1746
1816
  hook
1747
1817
  };
1748
- this.multiHooks.add(entry);
1818
+ this.hooks.add(entry);
1819
+ for (const archetype of this.archetypes) if (this.archetypeMatchesHook(archetype, entry)) archetype.matchingMultiHooks.add(entry);
1749
1820
  const multiHook = hook;
1750
1821
  if (multiHook.on_init !== void 0) {
1751
1822
  const matchingArchetypes = this.getMatchingArchetypes(requiredComponents);
1752
1823
  for (const archetype of matchingArchetypes) for (const entityId of archetype.getEntities()) {
1753
1824
  const components = collectMultiHookComponents(this.createHooksContext(), entityId, componentTypes);
1754
- multiHook.on_init(entityId, componentTypes, components);
1825
+ multiHook.on_init(entityId, ...components);
1755
1826
  }
1756
1827
  }
1757
1828
  } else {
1758
1829
  const componentType = componentTypesOrSingle;
1759
- if (!this.hooks.has(componentType)) this.hooks.set(componentType, /* @__PURE__ */ new Set());
1760
- this.hooks.get(componentType).add(hook);
1830
+ if (!this.legacyHooks.has(componentType)) this.legacyHooks.set(componentType, /* @__PURE__ */ new Set());
1831
+ this.legacyHooks.get(componentType).add(hook);
1761
1832
  const singleHook = hook;
1762
1833
  if (singleHook.on_init !== void 0) this.archetypesByComponent.get(componentType)?.forEach((archetype) => {
1763
1834
  const entities = archetype.getEntityToIndexMap();
@@ -1772,16 +1843,17 @@ var World = class {
1772
1843
  }
1773
1844
  unhook(componentTypesOrSingle, hook) {
1774
1845
  if (Array.isArray(componentTypesOrSingle)) {
1775
- for (const entry of this.multiHooks) if (entry.hook === hook) {
1776
- this.multiHooks.delete(entry);
1846
+ for (const entry of this.hooks) if (entry.hook === hook) {
1847
+ this.hooks.delete(entry);
1848
+ for (const archetype of this.archetypes) archetype.matchingMultiHooks.delete(entry);
1777
1849
  break;
1778
1850
  }
1779
1851
  } else {
1780
1852
  const componentType = componentTypesOrSingle;
1781
- const hooks = this.hooks.get(componentType);
1853
+ const hooks = this.legacyHooks.get(componentType);
1782
1854
  if (hooks) {
1783
1855
  hooks.delete(hook);
1784
- if (hooks.size === 0) this.hooks.delete(componentType);
1856
+ if (hooks.size === 0) this.legacyHooks.delete(componentType);
1785
1857
  }
1786
1858
  }
1787
1859
  }
@@ -1880,18 +1952,18 @@ var World = class {
1880
1952
  processCommands(entityId, currentArchetype, commands, changeset, (eid, arch, compId) => {
1881
1953
  if (isExclusiveComponent(compId)) removeMatchingRelations(eid, arch, compId, changeset);
1882
1954
  });
1883
- const removedComponents = applyChangeset({
1955
+ const { removedComponents, newArchetype } = applyChangeset({
1884
1956
  dontFragmentRelations: this.dontFragmentRelations,
1885
1957
  ensureArchetype: (ct) => this.ensureArchetype(ct)
1886
1958
  }, entityId, currentArchetype, changeset, this.entityToArchetype);
1887
1959
  this.updateEntityReferences(entityId, changeset);
1888
- triggerLifecycleHooks(this.createHooksContext(), entityId, changeset.adds, removedComponents);
1960
+ triggerLifecycleHooks(this.createHooksContext(), entityId, changeset.adds, removedComponents, currentArchetype, newArchetype);
1889
1961
  return changeset;
1890
1962
  }
1891
1963
  createHooksContext() {
1892
1964
  return {
1893
- hooks: this.hooks,
1894
- multiHooks: this.multiHooks,
1965
+ hooks: this.legacyHooks,
1966
+ multiHooks: this.hooks,
1895
1967
  has: (eid, ct) => this.has(eid, ct),
1896
1968
  get: (eid, ct) => this.get(eid, ct),
1897
1969
  getOptional: (eid, ct) => this.getOptional(eid, ct)
@@ -1904,12 +1976,12 @@ var World = class {
1904
1976
  changeset.delete(componentType);
1905
1977
  maybeRemoveWildcardMarker(entityId, sourceArchetype, componentType, getComponentIdFromRelationId(componentType), changeset);
1906
1978
  const removedComponent = sourceArchetype.get(entityId, componentType);
1907
- applyChangeset({
1979
+ const { newArchetype } = applyChangeset({
1908
1980
  dontFragmentRelations: this.dontFragmentRelations,
1909
1981
  ensureArchetype: (ct) => this.ensureArchetype(ct)
1910
1982
  }, entityId, sourceArchetype, changeset, this.entityToArchetype);
1911
1983
  untrackEntityReference(this.entityReferences, entityId, componentType, targetEntityId);
1912
- triggerLifecycleHooks(this.createHooksContext(), entityId, /* @__PURE__ */ new Map(), new Map([[componentType, removedComponent]]));
1984
+ triggerLifecycleHooks(this.createHooksContext(), entityId, /* @__PURE__ */ new Map(), new Map([[componentType, removedComponent]]), sourceArchetype, newArchetype);
1913
1985
  }
1914
1986
  updateEntityReferences(entityId, changeset) {
1915
1987
  for (const componentType of changeset.removes) if (isEntityRelation(componentType)) {
@@ -1935,8 +2007,22 @@ var World = class {
1935
2007
  this.archetypesByComponent.set(componentType, archetypes);
1936
2008
  }
1937
2009
  for (const query of this.queries) query.checkNewArchetype(newArchetype);
2010
+ this.updateArchetypeHookMatches(newArchetype);
1938
2011
  return newArchetype;
1939
2012
  }
2013
+ updateArchetypeHookMatches(archetype) {
2014
+ for (const entry of this.hooks) if (this.archetypeMatchesHook(archetype, entry)) archetype.matchingMultiHooks.add(entry);
2015
+ }
2016
+ archetypeMatchesHook(archetype, entry) {
2017
+ return entry.requiredComponents.every((c) => {
2018
+ if (isWildcardRelationId(c)) {
2019
+ if (isDontFragmentWildcard(c)) return true;
2020
+ const componentId = getComponentIdFromRelationId(c);
2021
+ return componentId !== void 0 && archetype.hasRelationWithComponentId(componentId);
2022
+ }
2023
+ return archetype.componentTypes.includes(c) || isDontFragmentRelation(c);
2024
+ });
2025
+ }
1940
2026
  archetypeReferencesEntity(archetype, entityId) {
1941
2027
  return archetype.componentTypes.some((ct) => ct === entityId || isEntityRelation(ct) && getTargetIdFromRelationId(ct) === entityId);
1942
2028
  }