ecspresso 0.12.9 → 0.13.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.
Files changed (54) hide show
  1. package/dist/index.js +2 -2
  2. package/dist/index.js.map +5 -5
  3. package/dist/plugin.d.ts +89 -22
  4. package/dist/plugins/audio.d.ts +2 -3
  5. package/dist/plugins/audio.js +2 -2
  6. package/dist/plugins/audio.js.map +3 -3
  7. package/dist/plugins/bounds.d.ts +2 -3
  8. package/dist/plugins/bounds.js +2 -2
  9. package/dist/plugins/bounds.js.map +3 -3
  10. package/dist/plugins/camera.d.ts +1 -2
  11. package/dist/plugins/camera.js +2 -2
  12. package/dist/plugins/camera.js.map +3 -3
  13. package/dist/plugins/collision.d.ts +9 -8
  14. package/dist/plugins/collision.js +2 -2
  15. package/dist/plugins/collision.js.map +4 -4
  16. package/dist/plugins/coroutine.d.ts +2 -3
  17. package/dist/plugins/coroutine.js +2 -2
  18. package/dist/plugins/coroutine.js.map +3 -3
  19. package/dist/plugins/diagnostics.d.ts +1 -3
  20. package/dist/plugins/diagnostics.js +2 -2
  21. package/dist/plugins/diagnostics.js.map +3 -3
  22. package/dist/plugins/input.d.ts +11 -3
  23. package/dist/plugins/input.js +2 -2
  24. package/dist/plugins/input.js.map +3 -3
  25. package/dist/plugins/particles.d.ts +2 -2
  26. package/dist/plugins/particles.js +2 -2
  27. package/dist/plugins/particles.js.map +3 -3
  28. package/dist/plugins/physics2D.d.ts +8 -5
  29. package/dist/plugins/physics2D.js +2 -2
  30. package/dist/plugins/physics2D.js.map +4 -4
  31. package/dist/plugins/renderers/renderer2D.d.ts +36 -9
  32. package/dist/plugins/renderers/renderer2D.js +2 -2
  33. package/dist/plugins/renderers/renderer2D.js.map +3 -3
  34. package/dist/plugins/spatial-index.d.ts +1 -4
  35. package/dist/plugins/spatial-index.js +2 -2
  36. package/dist/plugins/spatial-index.js.map +4 -4
  37. package/dist/plugins/sprite-animation.d.ts +2 -3
  38. package/dist/plugins/sprite-animation.js +2 -2
  39. package/dist/plugins/sprite-animation.js.map +3 -3
  40. package/dist/plugins/state-machine.d.ts +2 -3
  41. package/dist/plugins/state-machine.js +2 -2
  42. package/dist/plugins/state-machine.js.map +3 -3
  43. package/dist/plugins/timers.d.ts +2 -3
  44. package/dist/plugins/timers.js +2 -2
  45. package/dist/plugins/timers.js.map +3 -3
  46. package/dist/plugins/transform.d.ts +3 -3
  47. package/dist/plugins/transform.js +2 -2
  48. package/dist/plugins/transform.js.map +3 -3
  49. package/dist/plugins/tween.d.ts +2 -3
  50. package/dist/plugins/tween.js +2 -2
  51. package/dist/plugins/tween.js.map +3 -3
  52. package/dist/utils/narrowphase.d.ts +60 -19
  53. package/dist/utils/spatial-hash.d.ts +11 -1
  54. package/package.json +4 -1
@@ -9,8 +9,6 @@
9
9
  * Automatic acceleration: collision and physics2D plugins detect the
10
10
  * spatialIndex resource at runtime and use it for broadphase when present.
11
11
  */
12
- import { type Plugin } from 'ecspresso';
13
- import type { WorldConfigFrom, EmptyConfig } from '../type-utils';
14
12
  import type { TransformComponentTypes } from './transform';
15
13
  import type { CollisionComponentTypes } from './collision';
16
14
  import { type SpatialIndex } from '../utils/spatial-hash';
@@ -19,7 +17,6 @@ export interface SpatialIndexResourceTypes {
19
17
  }
20
18
  type SpatialIndexComponentTypes = TransformComponentTypes & Pick<CollisionComponentTypes<string>, 'aabbCollider' | 'circleCollider'>;
21
19
  export type SpatialIndexPhase = 'fixedUpdate' | 'postUpdate';
22
- type SpatialIndexLabel = `spatial-index-rebuild-${SpatialIndexPhase}`;
23
20
  export interface SpatialIndexPluginOptions<G extends string = 'spatialIndex'> {
24
21
  /** Cell size for the spatial hash grid (default: 64) */
25
22
  cellSize?: number;
@@ -54,5 +51,5 @@ export interface SpatialIndexPluginOptions<G extends string = 'spatialIndex'> {
54
51
  * const nearby = si.queryRadius(playerX, playerY, 200);
55
52
  * ```
56
53
  */
57
- export declare function createSpatialIndexPlugin<G extends string = 'spatialIndex'>(options?: SpatialIndexPluginOptions<G>): Plugin<WorldConfigFrom<SpatialIndexComponentTypes, {}, SpatialIndexResourceTypes>, EmptyConfig, SpatialIndexLabel, G>;
54
+ export declare function createSpatialIndexPlugin<G extends string = 'spatialIndex'>(options?: SpatialIndexPluginOptions<G>): import("ecspresso").Plugin<import("ecspresso").WithResources<import("ecspresso").WithComponents<import("ecspresso").EmptyConfig, SpatialIndexComponentTypes>, SpatialIndexResourceTypes>, import("ecspresso").EmptyConfig, "spatial-index-rebuild-fixedUpdate" | "spatial-index-rebuild-postUpdate", G, never, never>;
58
55
  export {};
@@ -1,4 +1,4 @@
1
- var S=((j)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(j,{get:(B,D)=>(typeof require<"u"?require:B)[D]}):j)(function(j){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+j+'" is not supported')});import{definePlugin as Q}from"ecspresso";function R(j,B){return j*73856093^B*19349663}function _(j){return{cellSize:j,invCellSize:1/j,cells:new Map,entries:new Map}}function v(j){j.cells.clear(),j.entries.clear()}function H(j,B,D,F,K,L){j.entries.set(B,{entityId:B,x:D,y:F,halfW:K,halfH:L});let N=j.invCellSize,U=Math.floor((D-K)*N),J=Math.floor((D+K)*N),$=Math.floor((F-L)*N),A=Math.floor((F+L)*N);for(let O=U;O<=J;O++)for(let M=$;M<=A;M++){let V=R(O,M),T=j.cells.get(V);if(T)T.push(B);else j.cells.set(V,[B])}}function G(j,B,D,F,K,L){let N=j.invCellSize,U=Math.floor(B*N),J=Math.floor(F*N),$=Math.floor(D*N),A=Math.floor(K*N);for(let O=U;O<=J;O++)for(let M=$;M<=A;M++){let V=j.cells.get(R(O,M));if(!V)continue;for(let T=0;T<V.length;T++){let Z=V[T];if(Z!==void 0)L.add(Z)}}}var W=new Set;function k(j,B,D,F,K){let L=W;L.clear(),G(j,B-F,D-F,B+F,D+F,L);let N=F*F;for(let U of L){let J=j.entries.get(U);if(!J)continue;let $=Math.max(J.x-J.halfW,Math.min(B,J.x+J.halfW)),A=Math.max(J.y-J.halfH,Math.min(D,J.y+J.halfH)),O=B-$,M=D-A;if(O*O+M*M<=N)K.add(U)}}var E=new Set;function p(j){return{grid:j,queryRect(B,D,F,K){return E.clear(),G(j,B,D,F,K,E),Array.from(E)},queryRectInto(B,D,F,K,L){G(j,B,D,F,K,L)},queryRadius(B,D,F){return E.clear(),k(j,B,D,F,E),Array.from(E)},queryRadiusInto(B,D,F,K){k(j,B,D,F,K)},getEntry(B){return j.entries.get(B)}}}function b(j){let{cellSize:B=64,systemGroup:D="spatialIndex",priority:F=2000,phases:K=["fixedUpdate","postUpdate"]}=j??{},L=_(B),N=p(L);return Q({id:"spatialIndex",install(U){U.addResource("spatialIndex",N);for(let J of K){let $=J==="fixedUpdate"?"localTransform":"worldTransform";U.addSystem(`spatial-index-rebuild-${J}`).setPriority(F).inPhase(J).inGroup(D).addQuery("transforms",{with:[$]}).setProcess(({queries:A,ecs:O})=>{v(L);for(let M of A.transforms){let V=M.components[$],T=O.getComponent(M.id,"aabbCollider"),Z=O.getComponent(M.id,"circleCollider");if(!T&&!Z)continue;let{x:w,y:z}=V,P=0,q=0;if(T)w+=T.offsetX??0,z+=T.offsetY??0,P=T.width/2,q=T.height/2;if(Z)w+=Z.offsetX??0,z+=Z.offsetY??0,P=Math.max(P,Z.radius),q=Math.max(q,Z.radius);H(L,M.id,w,z,P,q)}})}}})}export{b as createSpatialIndexPlugin};
1
+ var p=((j)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(j,{get:(B,D)=>(typeof require<"u"?require:B)[D]}):j)(function(j){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+j+'" is not supported')});import{definePlugin as W}from"ecspresso";function _(j,B){return j*73856093^B*19349663}function k(j){return{cellSize:j,invCellSize:1/j,cells:new Map,entries:new Map,_entriesPrev:new Map}}function v(j){j._entriesPrev.clear();let B=j.entries;j.entries=j._entriesPrev,j._entriesPrev=B;for(let D of j.cells.values())D.length=0}function Q(j,B,D,F,K,L){let M=j._entriesPrev.get(B);if(M)j._entriesPrev.delete(B),M.x=D,M.y=F,M.halfW=K,M.halfH=L,j.entries.set(B,M);else j.entries.set(B,{entityId:B,x:D,y:F,halfW:K,halfH:L});let T=j.invCellSize,J=Math.floor((D-K)*T),$=Math.floor((D+K)*T),A=Math.floor((F-L)*T),V=Math.floor((F+L)*T);for(let N=J;N<=$;N++)for(let Z=A;Z<=V;Z++){let O=_(N,Z),U=j.cells.get(O);if(U)U.push(B);else j.cells.set(O,[B])}}function G(j,B,D,F,K,L){let M=j.invCellSize,T=Math.floor(B*M),J=Math.floor(F*M),$=Math.floor(D*M),A=Math.floor(K*M);for(let V=T;V<=J;V++)for(let N=$;N<=A;N++){let Z=j.cells.get(_(V,N));if(!Z)continue;for(let O=0;O<Z.length;O++){let U=Z[O];if(U!==void 0)L.add(U)}}}var H=new Set;function R(j,B,D,F,K){let L=H;L.clear(),G(j,B-F,D-F,B+F,D+F,L);let M=F*F;for(let T of L){let J=j.entries.get(T);if(!J)continue;let $=Math.max(J.x-J.halfW,Math.min(B,J.x+J.halfW)),A=Math.max(J.y-J.halfH,Math.min(D,J.y+J.halfH)),V=B-$,N=D-A;if(V*V+N*N<=M)K.add(T)}}var E=new Set;function S(j){return{grid:j,queryRect(B,D,F,K){return E.clear(),G(j,B,D,F,K,E),Array.from(E)},queryRectInto(B,D,F,K,L){G(j,B,D,F,K,L)},queryRadius(B,D,F){return E.clear(),R(j,B,D,F,E),Array.from(E)},queryRadiusInto(B,D,F,K){R(j,B,D,F,K)},getEntry(B){return j.entries.get(B)}}}function b(j){let{cellSize:B=64,systemGroup:D="spatialIndex",priority:F=2000,phases:K=["fixedUpdate","postUpdate"]}=j??{},L=k(B),M=S(L);return W("spatialIndex").withComponentTypes().withResourceTypes().withLabels().withGroups().install((T)=>{T.addResource("spatialIndex",M);for(let J of K){let $=J==="fixedUpdate"?"localTransform":"worldTransform";T.addSystem(`spatial-index-rebuild-${J}`).setPriority(F).inPhase(J).inGroup(D).addQuery("transforms",{with:[$]}).setProcess(({queries:A,ecs:V})=>{v(L);for(let N of A.transforms){let Z=N.components[$],O=V.getComponent(N.id,"aabbCollider"),U=V.getComponent(N.id,"circleCollider");if(!O&&!U)continue;let{x:w,y:z}=Z,P=0,q=0;if(O)w+=O.offsetX??0,z+=O.offsetY??0,P=O.width/2,q=O.height/2;if(U)w+=U.offsetX??0,z+=U.offsetY??0,P=Math.max(P,U.radius),q=Math.max(q,U.radius);Q(L,N.id,w,z,P,q)}})}})}export{b as createSpatialIndexPlugin};
2
2
 
3
- //# debugId=926438F008C04F1E64756E2164756E21
3
+ //# debugId=670788C0A8BDB87E64756E2164756E21
4
4
  //# sourceMappingURL=spatial-index.js.map
@@ -2,10 +2,10 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/plugins/spatial-index.ts", "../src/utils/spatial-hash.ts"],
4
4
  "sourcesContent": [
5
- "/**\n * Spatial Index Plugin for ECSpresso\n *\n * Provides a uniform-grid spatial hash for broadphase collision detection\n * and proximity queries. Replaces O(n²) brute-force with O(n·d) where\n * d = local density.\n *\n * Standalone usage: queryRect / queryRadius for proximity queries.\n * Automatic acceleration: collision and physics2D plugins detect the\n * spatialIndex resource at runtime and use it for broadphase when present.\n */\n\nimport { definePlugin, type Plugin } from 'ecspresso';\nimport type { SystemPhase } from 'ecspresso';\nimport type { WorldConfigFrom, EmptyConfig } from '../type-utils';\nimport type { TransformComponentTypes } from './transform';\nimport type { CollisionComponentTypes } from './collision';\nimport {\n\ttype SpatialEntry,\n\ttype SpatialHashGrid,\n\ttype SpatialIndex,\n\tcreateGrid,\n\tclearGrid,\n\tinsertEntity,\n\tgridQueryRect,\n\tgridQueryRadius,\n} from '../utils/spatial-hash';\n\n// Module-scoped reusable set to reduce GC pressure\nconst _reusableQuerySet = new Set<number>();\n\n// ==================== Resource API ====================\n\nexport interface SpatialIndexResourceTypes {\n\tspatialIndex: SpatialIndex;\n}\n\nfunction createSpatialIndexResource(grid: SpatialHashGrid): SpatialIndex {\n\treturn {\n\t\tgrid,\n\t\tqueryRect(minX: number, minY: number, maxX: number, maxY: number): number[] {\n\t\t\t_reusableQuerySet.clear();\n\t\t\tgridQueryRect(grid, minX, minY, maxX, maxY, _reusableQuerySet);\n\t\t\treturn Array.from(_reusableQuerySet);\n\t\t},\n\t\tqueryRectInto(minX: number, minY: number, maxX: number, maxY: number, result: Set<number>): void {\n\t\t\tgridQueryRect(grid, minX, minY, maxX, maxY, result);\n\t\t},\n\t\tqueryRadius(cx: number, cy: number, radius: number): number[] {\n\t\t\t_reusableQuerySet.clear();\n\t\t\tgridQueryRadius(grid, cx, cy, radius, _reusableQuerySet);\n\t\t\treturn Array.from(_reusableQuerySet);\n\t\t},\n\t\tqueryRadiusInto(cx: number, cy: number, radius: number, result: Set<number>): void {\n\t\t\tgridQueryRadius(grid, cx, cy, radius, result);\n\t\t},\n\t\tgetEntry(entityId: number): SpatialEntry | undefined {\n\t\t\treturn grid.entries.get(entityId);\n\t\t},\n\t};\n}\n\n// ==================== Component Types ====================\n\ntype SpatialIndexComponentTypes =\n\tTransformComponentTypes & Pick<CollisionComponentTypes<string>, 'aabbCollider' | 'circleCollider'>;\n\n// ==================== Plugin Options ====================\n\nexport type SpatialIndexPhase = 'fixedUpdate' | 'postUpdate';\ntype SpatialIndexLabel = `spatial-index-rebuild-${SpatialIndexPhase}`;\n\nexport interface SpatialIndexPluginOptions<G extends string = 'spatialIndex'> {\n\t/** Cell size for the spatial hash grid (default: 64) */\n\tcellSize?: number;\n\t/** System group name (default: 'spatialIndex') */\n\tsystemGroup?: G;\n\t/** Priority for rebuild systems (default: 2000, before collision) */\n\tpriority?: number;\n\t/** Phases to register rebuild systems in (default: ['fixedUpdate', 'postUpdate']) */\n\tphases?: ReadonlyArray<SpatialIndexPhase>;\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a spatial index plugin for ECSpresso.\n *\n * Provides a uniform-grid spatial hash that accelerates collision detection.\n * When installed alongside the collision or physics2D plugins, they\n * automatically use the spatial index for broadphase instead of O(n²)\n * brute-force.\n *\n * Also provides proximity query methods for game logic (e.g. \"find all\n * enemies within 200 units\").\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createTransformPlugin())\n * .withPlugin(createCollisionPlugin({ layers }))\n * .withPlugin(createSpatialIndexPlugin({ cellSize: 128 }))\n * .build();\n *\n * // Proximity query in a system:\n * const si = ecs.getResource('spatialIndex');\n * const nearby = si.queryRadius(playerX, playerY, 200);\n * ```\n */\nexport function createSpatialIndexPlugin<G extends string = 'spatialIndex'>(\n\toptions?: SpatialIndexPluginOptions<G>,\n): Plugin<WorldConfigFrom<SpatialIndexComponentTypes, {}, SpatialIndexResourceTypes>, EmptyConfig, SpatialIndexLabel, G> {\n\tconst {\n\t\tcellSize = 64,\n\t\tsystemGroup = 'spatialIndex',\n\t\tpriority = 2000,\n\t\tphases = ['fixedUpdate', 'postUpdate'] as const,\n\t} = options ?? {};\n\n\tconst grid = createGrid(cellSize);\n\tconst resource = createSpatialIndexResource(grid);\n\n\treturn definePlugin<WorldConfigFrom<SpatialIndexComponentTypes, {}, SpatialIndexResourceTypes>, EmptyConfig, SpatialIndexLabel, G>({\n\t\tid: 'spatialIndex',\n\t\tinstall(world) {\n\t\t\tworld.addResource('spatialIndex', resource);\n\n\t\t\t// Register a rebuild system for each requested phase\n\t\t\tfor (const phase of phases) {\n\t\t\t\tconst transformComponent = phase === 'fixedUpdate' ? 'localTransform' : 'worldTransform';\n\n\t\t\t\tworld\n\t\t\t\t\t.addSystem(`spatial-index-rebuild-${phase}`)\n\t\t\t\t\t.setPriority(priority)\n\t\t\t\t\t.inPhase(phase as SystemPhase)\n\t\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t\t.addQuery('transforms', {\n\t\t\t\t\t\twith: [transformComponent],\n\t\t\t\t\t})\n\t\t\t\t\t.setProcess(({ queries, ecs }) => {\n\t\t\t\t\t\tclearGrid(grid);\n\n\t\t\t\t\t\tfor (const entity of queries.transforms) {\n\t\t\t\t\t\t\tconst transform = entity.components[transformComponent];\n\t\t\t\t\t\t\tconst aabb = ecs.getComponent(entity.id, 'aabbCollider');\n\t\t\t\t\t\t\tconst circle = ecs.getComponent(entity.id, 'circleCollider');\n\n\t\t\t\t\t\t\t// Only insert entities that have a collider\n\t\t\t\t\t\t\tif (!aabb && !circle) continue;\n\n\t\t\t\t\t\t\tlet x = transform.x;\n\t\t\t\t\t\t\tlet y = transform.y;\n\t\t\t\t\t\t\tlet halfW = 0;\n\t\t\t\t\t\t\tlet halfH = 0;\n\n\t\t\t\t\t\t\tif (aabb) {\n\t\t\t\t\t\t\t\tx += aabb.offsetX ?? 0;\n\t\t\t\t\t\t\t\ty += aabb.offsetY ?? 0;\n\t\t\t\t\t\t\t\thalfW = aabb.width / 2;\n\t\t\t\t\t\t\t\thalfH = aabb.height / 2;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (circle) {\n\t\t\t\t\t\t\t\tx += circle.offsetX ?? 0;\n\t\t\t\t\t\t\t\ty += circle.offsetY ?? 0;\n\t\t\t\t\t\t\t\t// Circle: use radius as half-extent in both dimensions\n\t\t\t\t\t\t\t\thalfW = Math.max(halfW, circle.radius);\n\t\t\t\t\t\t\t\thalfH = Math.max(halfH, circle.radius);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tinsertEntity(grid, entity.id, x, y, halfW, halfH);\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t}\n\t\t},\n\t});\n}\n",
6
- "/**\n * Spatial Hash Grid\n *\n * Uniform-grid spatial hash for broadphase collision detection and\n * proximity queries. Pure data structure, no ECS dependencies.\n */\n\n// ==================== Data Structure ====================\n\nexport interface SpatialEntry {\n\tentityId: number;\n\tx: number;\n\ty: number;\n\thalfW: number;\n\thalfH: number;\n}\n\nexport interface SpatialHashGrid {\n\tcellSize: number;\n\tinvCellSize: number;\n\tcells: Map<number, number[]>;\n\tentries: Map<number, SpatialEntry>;\n}\n\n// ==================== Pure Functions ====================\n\n/**\n * Hash a cell coordinate pair to a single integer key.\n * Uses large-prime XOR to distribute values.\n */\nexport function hashCell(cx: number, cy: number): number {\n\t// Large primes for spatial hashing distribution\n\treturn (cx * 73856093) ^ (cy * 19349663);\n}\n\n/**\n * Create a new empty spatial hash grid.\n */\nexport function createGrid(cellSize: number): SpatialHashGrid {\n\treturn {\n\t\tcellSize,\n\t\tinvCellSize: 1 / cellSize,\n\t\tcells: new Map(),\n\t\tentries: new Map(),\n\t};\n}\n\n/**\n * Clear all data from the grid without reallocating the Maps.\n */\nexport function clearGrid(grid: SpatialHashGrid): void {\n\tgrid.cells.clear();\n\tgrid.entries.clear();\n}\n\n/**\n * Insert an entity into all overlapping cells of the grid.\n */\nexport function insertEntity(\n\tgrid: SpatialHashGrid,\n\tentityId: number,\n\tx: number,\n\ty: number,\n\thalfW: number,\n\thalfH: number,\n): void {\n\tgrid.entries.set(entityId, { entityId, x, y, halfW, halfH });\n\n\tconst inv = grid.invCellSize;\n\tconst minCX = Math.floor((x - halfW) * inv);\n\tconst maxCX = Math.floor((x + halfW) * inv);\n\tconst minCY = Math.floor((y - halfH) * inv);\n\tconst maxCY = Math.floor((y + halfH) * inv);\n\n\tfor (let cx = minCX; cx <= maxCX; cx++) {\n\t\tfor (let cy = minCY; cy <= maxCY; cy++) {\n\t\t\tconst key = hashCell(cx, cy);\n\t\t\tconst bucket = grid.cells.get(key);\n\t\t\tif (bucket) {\n\t\t\t\tbucket.push(entityId);\n\t\t\t} else {\n\t\t\t\tgrid.cells.set(key, [entityId]);\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Collect entity IDs from all cells overlapping the given rectangle.\n */\nexport function gridQueryRect(\n\tgrid: SpatialHashGrid,\n\tminX: number,\n\tminY: number,\n\tmaxX: number,\n\tmaxY: number,\n\tresult: Set<number>,\n): void {\n\tconst inv = grid.invCellSize;\n\tconst minCX = Math.floor(minX * inv);\n\tconst maxCX = Math.floor(maxX * inv);\n\tconst minCY = Math.floor(minY * inv);\n\tconst maxCY = Math.floor(maxY * inv);\n\n\tfor (let cx = minCX; cx <= maxCX; cx++) {\n\t\tfor (let cy = minCY; cy <= maxCY; cy++) {\n\t\t\tconst bucket = grid.cells.get(hashCell(cx, cy));\n\t\t\tif (!bucket) continue;\n\t\t\tfor (let i = 0; i < bucket.length; i++) {\n\t\t\t\tconst entry = bucket[i];\n\t\t\t\tif (entry !== undefined) result.add(entry);\n\t\t\t}\n\t\t}\n\t}\n}\n\n// Module-scoped reusable set to reduce GC pressure\nconst _radiusCandidates = new Set<number>();\n\n/**\n * Collect entity IDs within a circle. Uses rect broadphase then\n * AABB-to-point distance filter.\n */\nexport function gridQueryRadius(\n\tgrid: SpatialHashGrid,\n\tcx: number,\n\tcy: number,\n\tradius: number,\n\tresult: Set<number>,\n): void {\n\t// Broadphase: rect query for bounding box of circle\n\tconst candidates = _radiusCandidates;\n\tcandidates.clear();\n\tgridQueryRect(grid, cx - radius, cy - radius, cx + radius, cy + radius, candidates);\n\n\tconst rSq = radius * radius;\n\n\tfor (const entityId of candidates) {\n\t\tconst entry = grid.entries.get(entityId);\n\t\tif (!entry) continue;\n\n\t\t// Closest point on entity AABB to query center\n\t\tconst closestX = Math.max(entry.x - entry.halfW, Math.min(cx, entry.x + entry.halfW));\n\t\tconst closestY = Math.max(entry.y - entry.halfH, Math.min(cy, entry.y + entry.halfH));\n\t\tconst dx = cx - closestX;\n\t\tconst dy = cy - closestY;\n\n\t\tif (dx * dx + dy * dy <= rSq) {\n\t\t\tresult.add(entityId);\n\t\t}\n\t}\n}\n\n// ==================== Resource API ====================\n\nexport interface SpatialIndex {\n\treadonly grid: SpatialHashGrid;\n\tqueryRect(minX: number, minY: number, maxX: number, maxY: number): number[];\n\tqueryRectInto(minX: number, minY: number, maxX: number, maxY: number, result: Set<number>): void;\n\tqueryRadius(cx: number, cy: number, radius: number): number[];\n\tqueryRadiusInto(cx: number, cy: number, radius: number, result: Set<number>): void;\n\tgetEntry(entityId: number): SpatialEntry | undefined;\n}\n"
5
+ "/**\n * Spatial Index Plugin for ECSpresso\n *\n * Provides a uniform-grid spatial hash for broadphase collision detection\n * and proximity queries. Replaces O(n²) brute-force with O(n·d) where\n * d = local density.\n *\n * Standalone usage: queryRect / queryRadius for proximity queries.\n * Automatic acceleration: collision and physics2D plugins detect the\n * spatialIndex resource at runtime and use it for broadphase when present.\n */\n\nimport { definePlugin } from 'ecspresso';\nimport type { SystemPhase } from 'ecspresso';\nimport type { TransformComponentTypes } from './transform';\nimport type { CollisionComponentTypes } from './collision';\nimport {\n\ttype SpatialEntry,\n\ttype SpatialHashGrid,\n\ttype SpatialIndex,\n\tcreateGrid,\n\tclearGrid,\n\tinsertEntity,\n\tgridQueryRect,\n\tgridQueryRadius,\n} from '../utils/spatial-hash';\n\n// Module-scoped reusable set to reduce GC pressure\nconst _reusableQuerySet = new Set<number>();\n\n// ==================== Resource API ====================\n\nexport interface SpatialIndexResourceTypes {\n\tspatialIndex: SpatialIndex;\n}\n\nfunction createSpatialIndexResource(grid: SpatialHashGrid): SpatialIndex {\n\treturn {\n\t\tgrid,\n\t\tqueryRect(minX: number, minY: number, maxX: number, maxY: number): number[] {\n\t\t\t_reusableQuerySet.clear();\n\t\t\tgridQueryRect(grid, minX, minY, maxX, maxY, _reusableQuerySet);\n\t\t\treturn Array.from(_reusableQuerySet);\n\t\t},\n\t\tqueryRectInto(minX: number, minY: number, maxX: number, maxY: number, result: Set<number>): void {\n\t\t\tgridQueryRect(grid, minX, minY, maxX, maxY, result);\n\t\t},\n\t\tqueryRadius(cx: number, cy: number, radius: number): number[] {\n\t\t\t_reusableQuerySet.clear();\n\t\t\tgridQueryRadius(grid, cx, cy, radius, _reusableQuerySet);\n\t\t\treturn Array.from(_reusableQuerySet);\n\t\t},\n\t\tqueryRadiusInto(cx: number, cy: number, radius: number, result: Set<number>): void {\n\t\t\tgridQueryRadius(grid, cx, cy, radius, result);\n\t\t},\n\t\tgetEntry(entityId: number): SpatialEntry | undefined {\n\t\t\treturn grid.entries.get(entityId);\n\t\t},\n\t};\n}\n\n// ==================== Component Types ====================\n\ntype SpatialIndexComponentTypes =\n\tTransformComponentTypes & Pick<CollisionComponentTypes<string>, 'aabbCollider' | 'circleCollider'>;\n\n// ==================== Plugin Options ====================\n\nexport type SpatialIndexPhase = 'fixedUpdate' | 'postUpdate';\ntype SpatialIndexLabel = `spatial-index-rebuild-${SpatialIndexPhase}`;\n\nexport interface SpatialIndexPluginOptions<G extends string = 'spatialIndex'> {\n\t/** Cell size for the spatial hash grid (default: 64) */\n\tcellSize?: number;\n\t/** System group name (default: 'spatialIndex') */\n\tsystemGroup?: G;\n\t/** Priority for rebuild systems (default: 2000, before collision) */\n\tpriority?: number;\n\t/** Phases to register rebuild systems in (default: ['fixedUpdate', 'postUpdate']) */\n\tphases?: ReadonlyArray<SpatialIndexPhase>;\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a spatial index plugin for ECSpresso.\n *\n * Provides a uniform-grid spatial hash that accelerates collision detection.\n * When installed alongside the collision or physics2D plugins, they\n * automatically use the spatial index for broadphase instead of O(n²)\n * brute-force.\n *\n * Also provides proximity query methods for game logic (e.g. \"find all\n * enemies within 200 units\").\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createTransformPlugin())\n * .withPlugin(createCollisionPlugin({ layers }))\n * .withPlugin(createSpatialIndexPlugin({ cellSize: 128 }))\n * .build();\n *\n * // Proximity query in a system:\n * const si = ecs.getResource('spatialIndex');\n * const nearby = si.queryRadius(playerX, playerY, 200);\n * ```\n */\nexport function createSpatialIndexPlugin<G extends string = 'spatialIndex'>(\n\toptions?: SpatialIndexPluginOptions<G>,\n) {\n\tconst {\n\t\tcellSize = 64,\n\t\tsystemGroup = 'spatialIndex',\n\t\tpriority = 2000,\n\t\tphases = ['fixedUpdate', 'postUpdate'] as const,\n\t} = options ?? {};\n\n\tconst grid = createGrid(cellSize);\n\tconst resource = createSpatialIndexResource(grid);\n\n\treturn definePlugin('spatialIndex')\n\t\t.withComponentTypes<SpatialIndexComponentTypes>()\n\t\t.withResourceTypes<SpatialIndexResourceTypes>()\n\t\t.withLabels<SpatialIndexLabel>()\n\t\t.withGroups<G>()\n\t\t.install((world) => {\n\t\t\tworld.addResource('spatialIndex', resource);\n\n\t\t\t// Register a rebuild system for each requested phase\n\t\t\tfor (const phase of phases) {\n\t\t\t\tconst transformComponent = phase === 'fixedUpdate' ? 'localTransform' : 'worldTransform';\n\n\t\t\t\tworld\n\t\t\t\t\t.addSystem(`spatial-index-rebuild-${phase}`)\n\t\t\t\t\t.setPriority(priority)\n\t\t\t\t\t.inPhase(phase as SystemPhase)\n\t\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t\t.addQuery('transforms', {\n\t\t\t\t\t\twith: [transformComponent],\n\t\t\t\t\t})\n\t\t\t\t\t.setProcess(({ queries, ecs }) => {\n\t\t\t\t\t\tclearGrid(grid);\n\n\t\t\t\t\t\tfor (const entity of queries.transforms) {\n\t\t\t\t\t\t\tconst transform = entity.components[transformComponent];\n\t\t\t\t\t\t\tconst aabb = ecs.getComponent(entity.id, 'aabbCollider');\n\t\t\t\t\t\t\tconst circle = ecs.getComponent(entity.id, 'circleCollider');\n\n\t\t\t\t\t\t\t// Only insert entities that have a collider\n\t\t\t\t\t\t\tif (!aabb && !circle) continue;\n\n\t\t\t\t\t\t\tlet x = transform.x;\n\t\t\t\t\t\t\tlet y = transform.y;\n\t\t\t\t\t\t\tlet halfW = 0;\n\t\t\t\t\t\t\tlet halfH = 0;\n\n\t\t\t\t\t\t\tif (aabb) {\n\t\t\t\t\t\t\t\tx += aabb.offsetX ?? 0;\n\t\t\t\t\t\t\t\ty += aabb.offsetY ?? 0;\n\t\t\t\t\t\t\t\thalfW = aabb.width / 2;\n\t\t\t\t\t\t\t\thalfH = aabb.height / 2;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (circle) {\n\t\t\t\t\t\t\t\tx += circle.offsetX ?? 0;\n\t\t\t\t\t\t\t\ty += circle.offsetY ?? 0;\n\t\t\t\t\t\t\t\t// Circle: use radius as half-extent in both dimensions\n\t\t\t\t\t\t\t\thalfW = Math.max(halfW, circle.radius);\n\t\t\t\t\t\t\t\thalfH = Math.max(halfH, circle.radius);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tinsertEntity(grid, entity.id, x, y, halfW, halfH);\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t}\n\t\t});\n}\n",
6
+ "/**\n * Spatial Hash Grid\n *\n * Uniform-grid spatial hash for broadphase collision detection and\n * proximity queries. Pure data structure, no ECS dependencies.\n */\n\n// ==================== Data Structure ====================\n\nexport interface SpatialEntry {\n\tentityId: number;\n\tx: number;\n\ty: number;\n\thalfW: number;\n\thalfH: number;\n}\n\nexport interface SpatialHashGrid {\n\tcellSize: number;\n\tinvCellSize: number;\n\tcells: Map<number, number[]>;\n\tentries: Map<number, SpatialEntry>;\n\t/** Previous-frame entries held for in-place reuse during rebuild. Internal. */\n\t_entriesPrev: Map<number, SpatialEntry>;\n}\n\n// ==================== Pure Functions ====================\n\n/**\n * Hash a cell coordinate pair to a single integer key.\n * Uses large-prime XOR to distribute values.\n */\nexport function hashCell(cx: number, cy: number): number {\n\t// Large primes for spatial hashing distribution\n\treturn (cx * 73856093) ^ (cy * 19349663);\n}\n\n/**\n * Create a new empty spatial hash grid.\n */\nexport function createGrid(cellSize: number): SpatialHashGrid {\n\treturn {\n\t\tcellSize,\n\t\tinvCellSize: 1 / cellSize,\n\t\tcells: new Map(),\n\t\tentries: new Map(),\n\t\t_entriesPrev: new Map(),\n\t};\n}\n\n/**\n * Prepare the grid for a rebuild.\n *\n * Swaps `entries` with `_entriesPrev` so `insertEntity` can reuse existing\n * `SpatialEntry` objects in place (steady-state rebuilds allocate zero\n * entries). Any stale entries left in `_entriesPrev` from the previous\n * rebuild are dropped here.\n *\n * Cell buckets are cleared in place — keys are retained so subsequent\n * inserts hit the existing array rather than allocating a fresh one.\n */\nexport function clearGrid(grid: SpatialHashGrid): void {\n\tgrid._entriesPrev.clear();\n\tconst tmp = grid.entries;\n\tgrid.entries = grid._entriesPrev;\n\tgrid._entriesPrev = tmp;\n\n\tfor (const bucket of grid.cells.values()) {\n\t\tbucket.length = 0;\n\t}\n}\n\n/**\n * Insert an entity into all overlapping cells of the grid.\n */\nexport function insertEntity(\n\tgrid: SpatialHashGrid,\n\tentityId: number,\n\tx: number,\n\ty: number,\n\thalfW: number,\n\thalfH: number,\n): void {\n\tconst recycled = grid._entriesPrev.get(entityId);\n\tif (recycled) {\n\t\tgrid._entriesPrev.delete(entityId);\n\t\trecycled.x = x;\n\t\trecycled.y = y;\n\t\trecycled.halfW = halfW;\n\t\trecycled.halfH = halfH;\n\t\tgrid.entries.set(entityId, recycled);\n\t} else {\n\t\tgrid.entries.set(entityId, { entityId, x, y, halfW, halfH });\n\t}\n\n\tconst inv = grid.invCellSize;\n\tconst minCX = Math.floor((x - halfW) * inv);\n\tconst maxCX = Math.floor((x + halfW) * inv);\n\tconst minCY = Math.floor((y - halfH) * inv);\n\tconst maxCY = Math.floor((y + halfH) * inv);\n\n\tfor (let cx = minCX; cx <= maxCX; cx++) {\n\t\tfor (let cy = minCY; cy <= maxCY; cy++) {\n\t\t\tconst key = hashCell(cx, cy);\n\t\t\tconst bucket = grid.cells.get(key);\n\t\t\tif (bucket) {\n\t\t\t\tbucket.push(entityId);\n\t\t\t} else {\n\t\t\t\tgrid.cells.set(key, [entityId]);\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Collect entity IDs from all cells overlapping the given rectangle.\n */\nexport function gridQueryRect(\n\tgrid: SpatialHashGrid,\n\tminX: number,\n\tminY: number,\n\tmaxX: number,\n\tmaxY: number,\n\tresult: Set<number>,\n): void {\n\tconst inv = grid.invCellSize;\n\tconst minCX = Math.floor(minX * inv);\n\tconst maxCX = Math.floor(maxX * inv);\n\tconst minCY = Math.floor(minY * inv);\n\tconst maxCY = Math.floor(maxY * inv);\n\n\tfor (let cx = minCX; cx <= maxCX; cx++) {\n\t\tfor (let cy = minCY; cy <= maxCY; cy++) {\n\t\t\tconst bucket = grid.cells.get(hashCell(cx, cy));\n\t\t\tif (!bucket) continue;\n\t\t\tfor (let i = 0; i < bucket.length; i++) {\n\t\t\t\tconst entry = bucket[i];\n\t\t\t\tif (entry !== undefined) result.add(entry);\n\t\t\t}\n\t\t}\n\t}\n}\n\n// Module-scoped reusable set to reduce GC pressure\nconst _radiusCandidates = new Set<number>();\n\n/**\n * Collect entity IDs within a circle. Uses rect broadphase then\n * AABB-to-point distance filter.\n */\nexport function gridQueryRadius(\n\tgrid: SpatialHashGrid,\n\tcx: number,\n\tcy: number,\n\tradius: number,\n\tresult: Set<number>,\n): void {\n\t// Broadphase: rect query for bounding box of circle\n\tconst candidates = _radiusCandidates;\n\tcandidates.clear();\n\tgridQueryRect(grid, cx - radius, cy - radius, cx + radius, cy + radius, candidates);\n\n\tconst rSq = radius * radius;\n\n\tfor (const entityId of candidates) {\n\t\tconst entry = grid.entries.get(entityId);\n\t\tif (!entry) continue;\n\n\t\t// Closest point on entity AABB to query center\n\t\tconst closestX = Math.max(entry.x - entry.halfW, Math.min(cx, entry.x + entry.halfW));\n\t\tconst closestY = Math.max(entry.y - entry.halfH, Math.min(cy, entry.y + entry.halfH));\n\t\tconst dx = cx - closestX;\n\t\tconst dy = cy - closestY;\n\n\t\tif (dx * dx + dy * dy <= rSq) {\n\t\t\tresult.add(entityId);\n\t\t}\n\t}\n}\n\n// ==================== Resource API ====================\n\nexport interface SpatialIndex {\n\treadonly grid: SpatialHashGrid;\n\tqueryRect(minX: number, minY: number, maxX: number, maxY: number): number[];\n\tqueryRectInto(minX: number, minY: number, maxX: number, maxY: number, result: Set<number>): void;\n\tqueryRadius(cx: number, cy: number, radius: number): number[];\n\tqueryRadiusInto(cx: number, cy: number, radius: number, result: Set<number>): void;\n\tgetEntry(entityId: number): SpatialEntry | undefined;\n}\n"
7
7
  ],
8
- "mappings": "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",
9
- "debugId": "926438F008C04F1E64756E2164756E21",
8
+ "mappings": "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",
9
+ "debugId": "670788C0A8BDB87E64756E2164756E21",
10
10
  "names": []
11
11
  }
@@ -8,9 +8,8 @@
8
8
  * Renderer2D is a required dependency — the `sprite` component comes from that plugin.
9
9
  * This plugin declares only `spriteAnimation` as its component type.
10
10
  */
11
- import { type Plugin, type BasePluginOptions } from 'ecspresso';
11
+ import { type BasePluginOptions } from 'ecspresso';
12
12
  import type { BaseWorld } from 'ecspresso';
13
- import type { WorldConfigFrom, EmptyConfig } from '../type-utils';
14
13
  /** BaseWorld narrowed to sprite-animation components for typed access in helpers. */
15
14
  type SpriteAnimationWorld = BaseWorld<SpriteAnimationComponentTypes>;
16
15
  export type AnimationLoopMode = 'once' | 'loop' | 'pingPong';
@@ -146,5 +145,5 @@ export declare function resumeAnimation(ecs: SpriteAnimationWorld, entityId: num
146
145
  * - Sprite texture sync via structural cross-plugin access
147
146
  * - Change detection via markChanged
148
147
  */
149
- export declare function createSpriteAnimationPlugin<G extends string = 'spriteAnimation'>(options?: SpriteAnimationPluginOptions<G>): Plugin<WorldConfigFrom<SpriteAnimationComponentTypes>, EmptyConfig, 'sprite-animation-update', G>;
148
+ export declare function createSpriteAnimationPlugin<G extends string = 'spriteAnimation'>(options?: SpriteAnimationPluginOptions<G>): import("ecspresso").Plugin<import("ecspresso").WithComponents<import("ecspresso").EmptyConfig, SpriteAnimationComponentTypes<string>>, import("ecspresso").EmptyConfig, "sprite-animation-update", G, never, never>;
150
149
  export {};
@@ -1,4 +1,4 @@
1
- var G=((j)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(j,{get:(z,E)=>(typeof require<"u"?require:z)[E]}):j)(function(j){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+j+'" is not supported')});import{definePlugin as Z}from"ecspresso";function Y(j){return Object.freeze({frames:Object.freeze([...j.frames]),frameDuration:j.frameDuration??0.1,frameDurations:j.frameDurations?Object.freeze([...j.frameDurations]):null,loop:j.loop??"loop"})}function U(j,z){return Object.freeze({id:j,clips:Object.freeze({default:Y(z)}),defaultClip:"default"})}function q(j,z,E){let J={},H=Object.keys(z);for(let N of H)J[N]=Y(z[N]);let M=H[0];if(!M)throw Error("defineSpriteAnimations: clips object must have at least one key");return Object.freeze({id:j,clips:Object.freeze(J),defaultClip:E?.defaultClip??M})}function D(j,z){let E=z?.initial??j.defaultClip;return{spriteAnimation:{set:j,current:E,currentFrame:0,elapsed:0,playing:!0,speed:z?.speed??1,direction:1,totalLoops:z?.totalLoops??-1,completedLoops:0,justFinished:!1,onComplete:z?.onComplete}}}function K(j,z,E,J){let H=j.getComponent(z,"spriteAnimation");if(!H)return!1;if(!(E in H.set.clips))return!1;if(E!==H.current||J?.restart===!0)H.current=E,H.currentFrame=0,H.elapsed=0,H.direction=1,H.completedLoops=0,H.justFinished=!1;if(H.playing=!0,J?.speed!==void 0)H.speed=J.speed;return j.markChanged(z,"spriteAnimation"),!0}function S(j,z){let E=j.getComponent(z,"spriteAnimation");if(!E)return!1;return E.playing=!1,!0}function A(j,z){let E=j.getComponent(z,"spriteAnimation");if(!E)return!1;return E.playing=!0,!0}function W(j,z,E){j.playing=!1,j.justFinished=!0,j.onComplete?.({entityId:z,animation:j.current}),E.commands.removeComponent(z,"spriteAnimation")}function _(j,z,E,J){if(j.completedLoops++,z.loop==="once")return W(j,E,J),!1;if(j.totalLoops>0&&j.completedLoops>=j.totalLoops)return W(j,E,J),!1;if(z.loop==="pingPong")return j.direction=j.direction===1?-1:1,j.currentFrame+=j.direction,j.elapsed>0;return j.currentFrame=0,j.elapsed>0}function X(j,z,E,J){let H=j.currentFrame+j.direction;if(H>=z.frames.length||H<0)return _(j,z,E,J);return j.currentFrame=H,!0}function $(j,z,E,J){while(!0){let H=z.frameDurations!==null?z.frameDurations[j.currentFrame]??z.frameDuration:z.frameDuration;if(H<=0){if(!X(j,z,E,J))return;continue}let M=H-j.elapsed;if(M>0.000001)return;if(j.elapsed=M<0?-M:0,!X(j,z,E,J))return}}function b(j){let{systemGroup:z="spriteAnimation",priority:E=0,phase:J="update"}=j??{};return Z({id:"spriteAnimation",install(H){H.addSystem("sprite-animation-update").setPriority(E).inPhase(J).inGroup(z).addQuery("animations",{with:["spriteAnimation"]}).setProcess(({queries:M,dt:N,ecs:V})=>{for(let O of M.animations){let L=O.components.spriteAnimation,Q=L.set.clips[L.current];if(!Q)continue;if(L.justFinished){L.justFinished=!1;continue}if(!L.playing)continue;if(Q.frames.length<=1)continue;let R=L.currentFrame;if(L.elapsed+=N*L.speed,$(L,Q,O.id,V),L.currentFrame!==R||R===0)B(O.components,L,Q);if(L.currentFrame!==R)V.markChanged(O.id,"spriteAnimation")}})}})}function B(j,z,E){let J=j.sprite;if(J&&typeof J==="object"&&"texture"in J)J.texture=E.frames[z.currentFrame]}export{S as stopAnimation,A as resumeAnimation,K as playAnimation,q as defineSpriteAnimations,U as defineSpriteAnimation,b as createSpriteAnimationPlugin,D as createSpriteAnimation};
1
+ var G=((j)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(j,{get:(z,E)=>(typeof require<"u"?require:z)[E]}):j)(function(j){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+j+'" is not supported')});import{definePlugin as Z}from"ecspresso";function Y(j){return Object.freeze({frames:Object.freeze([...j.frames]),frameDuration:j.frameDuration??0.1,frameDurations:j.frameDurations?Object.freeze([...j.frameDurations]):null,loop:j.loop??"loop"})}function U(j,z){return Object.freeze({id:j,clips:Object.freeze({default:Y(z)}),defaultClip:"default"})}function q(j,z,E){let J={},H=Object.keys(z);for(let N of H)J[N]=Y(z[N]);let M=H[0];if(!M)throw Error("defineSpriteAnimations: clips object must have at least one key");return Object.freeze({id:j,clips:Object.freeze(J),defaultClip:E?.defaultClip??M})}function D(j,z){let E=z?.initial??j.defaultClip;return{spriteAnimation:{set:j,current:E,currentFrame:0,elapsed:0,playing:!0,speed:z?.speed??1,direction:1,totalLoops:z?.totalLoops??-1,completedLoops:0,justFinished:!1,onComplete:z?.onComplete}}}function K(j,z,E,J){let H=j.getComponent(z,"spriteAnimation");if(!H)return!1;if(!(E in H.set.clips))return!1;if(E!==H.current||J?.restart===!0)H.current=E,H.currentFrame=0,H.elapsed=0,H.direction=1,H.completedLoops=0,H.justFinished=!1;if(H.playing=!0,J?.speed!==void 0)H.speed=J.speed;return j.markChanged(z,"spriteAnimation"),!0}function S(j,z){let E=j.getComponent(z,"spriteAnimation");if(!E)return!1;return E.playing=!1,!0}function A(j,z){let E=j.getComponent(z,"spriteAnimation");if(!E)return!1;return E.playing=!0,!0}function W(j,z,E){j.playing=!1,j.justFinished=!0,j.onComplete?.({entityId:z,animation:j.current}),E.commands.removeComponent(z,"spriteAnimation")}function _(j,z,E,J){if(j.completedLoops++,z.loop==="once")return W(j,E,J),!1;if(j.totalLoops>0&&j.completedLoops>=j.totalLoops)return W(j,E,J),!1;if(z.loop==="pingPong")return j.direction=j.direction===1?-1:1,j.currentFrame+=j.direction,j.elapsed>0;return j.currentFrame=0,j.elapsed>0}function X(j,z,E,J){let H=j.currentFrame+j.direction;if(H>=z.frames.length||H<0)return _(j,z,E,J);return j.currentFrame=H,!0}function $(j,z,E,J){while(!0){let H=z.frameDurations!==null?z.frameDurations[j.currentFrame]??z.frameDuration:z.frameDuration;if(H<=0){if(!X(j,z,E,J))return;continue}let M=H-j.elapsed;if(M>0.000001)return;if(j.elapsed=M<0?-M:0,!X(j,z,E,J))return}}function b(j){let{systemGroup:z="spriteAnimation",priority:E=0,phase:J="update"}=j??{};return Z("spriteAnimation").withComponentTypes().withLabels().withGroups().install((H)=>{H.addSystem("sprite-animation-update").setPriority(E).inPhase(J).inGroup(z).addQuery("animations",{with:["spriteAnimation"]}).setProcess(({queries:M,dt:N,ecs:V})=>{for(let O of M.animations){let L=O.components.spriteAnimation,Q=L.set.clips[L.current];if(!Q)continue;if(L.justFinished){L.justFinished=!1;continue}if(!L.playing)continue;if(Q.frames.length<=1)continue;let R=L.currentFrame;if(L.elapsed+=N*L.speed,$(L,Q,O.id,V),L.currentFrame!==R||R===0)B(O.components,L,Q);if(L.currentFrame!==R)V.markChanged(O.id,"spriteAnimation")}})})}function B(j,z,E){let J=j.sprite;if(J&&typeof J==="object"&&"texture"in J)J.texture=E.frames[z.currentFrame]}export{S as stopAnimation,A as resumeAnimation,K as playAnimation,q as defineSpriteAnimations,U as defineSpriteAnimation,b as createSpriteAnimationPlugin,D as createSpriteAnimation};
2
2
 
3
- //# debugId=C75605DB29C1814064756E2164756E21
3
+ //# debugId=D1E817D3F2D57D1F64756E2164756E21
4
4
  //# sourceMappingURL=sprite-animation.js.map
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/plugins/sprite-animation.ts"],
4
4
  "sourcesContent": [
5
- "/**\n * Sprite Animation Plugin for ECSpresso\n *\n * ECS-native frame-based sprite animation. Advances through spritesheet frames,\n * handles loop modes (once, loop, pingPong), publishes completion events, and\n * syncs the current frame's texture to the PixiJS Sprite via structural access.\n *\n * Renderer2D is a required dependency — the `sprite` component comes from that plugin.\n * This plugin declares only `spriteAnimation` as its component type.\n */\n\nimport { definePlugin, type Plugin, type BasePluginOptions } from 'ecspresso';\nimport type { BaseWorld } from 'ecspresso';\nimport type { WorldConfigFrom, EmptyConfig } from '../type-utils';\n\n/** BaseWorld narrowed to sprite-animation components for typed access in helpers. */\ntype SpriteAnimationWorld = BaseWorld<SpriteAnimationComponentTypes>;\n\n// ==================== Loop Mode ====================\n\nexport type AnimationLoopMode = 'once' | 'loop' | 'pingPong';\n\n// ==================== Clip Types ====================\n\n/**\n * A single animation clip: an ordered sequence of texture frames with timing.\n * Immutable and shared across entities.\n */\nexport interface SpriteAnimationClip {\n\treadonly frames: readonly unknown[];\n\treadonly frameDuration: number;\n\treadonly frameDurations: readonly number[] | null;\n\treadonly loop: AnimationLoopMode;\n}\n\n/**\n * Input format for defining a clip. Accepts either uniform or per-frame timing.\n */\nexport interface SpriteAnimationClipInput {\n\t/** Array of PixiJS Texture objects */\n\tframes: readonly unknown[];\n\t/** Uniform seconds-per-frame (used when frameDurations is not provided) */\n\tframeDuration?: number;\n\t/** Per-frame durations in seconds (overrides frameDuration) */\n\tframeDurations?: readonly number[];\n\t/** Loop mode (default: 'loop') */\n\tloop?: AnimationLoopMode;\n}\n\n// ==================== Animation Set ====================\n\n/**\n * A named collection of animation clips. Immutable and shared across entities.\n * Parameterized by A (animation name union) for compile-time validation.\n */\nexport interface SpriteAnimationSet<A extends string = string> {\n\treadonly id: string;\n\treadonly clips: { readonly [K in A]: SpriteAnimationClip };\n\treadonly defaultClip: A;\n}\n\n// ==================== Component ====================\n\n/**\n * Per-entity runtime animation state.\n */\nexport interface SpriteAnimation<A extends string = string> {\n\treadonly set: SpriteAnimationSet<A>;\n\tcurrent: A;\n\tcurrentFrame: number;\n\telapsed: number;\n\tplaying: boolean;\n\tspeed: number;\n\tdirection: 1 | -1;\n\ttotalLoops: number;\n\tcompletedLoops: number;\n\tjustFinished: boolean;\n\tonComplete?: (data: SpriteAnimationEventData) => void;\n}\n\n/**\n * Component types provided by the sprite animation plugin.\n */\nexport interface SpriteAnimationComponentTypes<A extends string = string> {\n\tspriteAnimation: SpriteAnimation<A>;\n}\n\n// ==================== Event Types ====================\n\n/**\n * Data published when an animation completes.\n */\nexport interface SpriteAnimationEventData {\n\tentityId: number;\n\tanimation: string;\n}\n\n// ==================== Plugin Options ====================\n\nexport interface SpriteAnimationPluginOptions<G extends string = 'spriteAnimation'> extends BasePluginOptions<G> {}\n\n// ==================== Helper Functions ====================\n\nfunction buildClip(input: SpriteAnimationClipInput): SpriteAnimationClip {\n\treturn Object.freeze({\n\t\tframes: Object.freeze([...input.frames]),\n\t\tframeDuration: input.frameDuration ?? (1 / 10),\n\t\tframeDurations: input.frameDurations\n\t\t\t? Object.freeze([...input.frameDurations])\n\t\t\t: null,\n\t\tloop: input.loop ?? 'loop',\n\t});\n}\n\n/**\n * Define a single-clip animation set named 'default'.\n * For simple use cases like spinning coins, pulsing effects, etc.\n *\n * @param id Unique identifier for this animation set\n * @param clip Clip definition\n * @returns A frozen SpriteAnimationSet with one clip named 'default'\n */\nexport function defineSpriteAnimation(\n\tid: string,\n\tclip: SpriteAnimationClipInput,\n): SpriteAnimationSet<'default'> {\n\treturn Object.freeze({\n\t\tid,\n\t\tclips: Object.freeze({ default: buildClip(clip) }),\n\t\tdefaultClip: 'default' as const,\n\t});\n}\n\n/**\n * Define a multi-clip animation set with named animations.\n * Animation names are inferred from the keys of the clips object.\n *\n * @param id Unique identifier for this animation set\n * @param clips Object mapping animation names to clip definitions\n * @param options Optional configuration (defaultClip)\n * @returns A frozen SpriteAnimationSet with inferred animation name union\n */\nexport function defineSpriteAnimations<A extends string>(\n\tid: string,\n\tclips: Record<A, SpriteAnimationClipInput>,\n\toptions?: { defaultClip?: NoInfer<A> },\n): SpriteAnimationSet<A> {\n\tconst builtClips = {} as Record<A, SpriteAnimationClip>;\n\tconst keys = Object.keys(clips) as A[];\n\n\tfor (const key of keys) {\n\t\tbuiltClips[key] = buildClip(clips[key]);\n\t}\n\n\tconst firstKey = keys[0];\n\tif (!firstKey) {\n\t\tthrow new Error(`defineSpriteAnimations: clips object must have at least one key`);\n\t}\n\n\treturn Object.freeze({\n\t\tid,\n\t\tclips: Object.freeze(builtClips),\n\t\tdefaultClip: options?.defaultClip ?? firstKey,\n\t});\n}\n\n/**\n * Create a spriteAnimation component from an animation set.\n *\n * @param set The animation set to use\n * @param options Optional configuration (initial clip, speed, onComplete event)\n * @returns Component object suitable for spreading into spawn()\n */\nexport function createSpriteAnimation<A extends string>(\n\tset: SpriteAnimationSet<A>,\n\toptions?: {\n\t\tinitial?: A;\n\t\tspeed?: number;\n\t\ttotalLoops?: number;\n\t\tonComplete?: (data: SpriteAnimationEventData) => void;\n\t},\n): Pick<SpriteAnimationComponentTypes<A>, 'spriteAnimation'> {\n\tconst initial = options?.initial ?? set.defaultClip;\n\treturn {\n\t\tspriteAnimation: {\n\t\t\tset,\n\t\t\tcurrent: initial,\n\t\t\tcurrentFrame: 0,\n\t\t\telapsed: 0,\n\t\t\tplaying: true,\n\t\t\tspeed: options?.speed ?? 1,\n\t\t\tdirection: 1,\n\t\t\ttotalLoops: options?.totalLoops ?? -1,\n\t\t\tcompletedLoops: 0,\n\t\t\tjustFinished: false,\n\t\t\tonComplete: options?.onComplete,\n\t\t},\n\t};\n}\n\n/**\n * Switch an entity's current animation at runtime.\n * Resets state if switching to a different animation (or restart=true).\n *\n * @returns false if entity has no spriteAnimation or animation name doesn't exist\n */\nexport function playAnimation(\n\tecs: SpriteAnimationWorld,\n\tentityId: number,\n\tanimation: string,\n\toptions?: { restart?: boolean; speed?: number },\n): boolean {\n\tconst anim = ecs.getComponent(entityId, 'spriteAnimation');\n\tif (!anim) return false;\n\tif (!(animation in anim.set.clips)) return false;\n\n\tconst shouldReset = animation !== anim.current || options?.restart === true;\n\n\tif (shouldReset) {\n\t\tanim.current = animation;\n\t\tanim.currentFrame = 0;\n\t\tanim.elapsed = 0;\n\t\tanim.direction = 1;\n\t\tanim.completedLoops = 0;\n\t\tanim.justFinished = false;\n\t}\n\n\tanim.playing = true;\n\n\tif (options?.speed !== undefined) {\n\t\tanim.speed = options.speed;\n\t}\n\n\tecs.markChanged(entityId, 'spriteAnimation');\n\treturn true;\n}\n\n/**\n * Pause an entity's animation.\n *\n * @returns false if entity has no spriteAnimation\n */\nexport function stopAnimation(\n\tecs: SpriteAnimationWorld,\n\tentityId: number,\n): boolean {\n\tconst anim = ecs.getComponent(entityId, 'spriteAnimation');\n\tif (!anim) return false;\n\n\tanim.playing = false;\n\treturn true;\n}\n\n/**\n * Resume a paused animation.\n *\n * @returns false if entity has no spriteAnimation\n */\nexport function resumeAnimation(\n\tecs: SpriteAnimationWorld,\n\tentityId: number,\n): boolean {\n\tconst anim = ecs.getComponent(entityId, 'spriteAnimation');\n\tif (!anim) return false;\n\n\tanim.playing = true;\n\treturn true;\n}\n\n// ==================== Animation Processing Helpers ====================\n\nfunction completeAnimation(\n\tanim: SpriteAnimation,\n\tentityId: number,\n\tecs: SpriteAnimationWorld,\n): void {\n\tanim.playing = false;\n\tanim.justFinished = true;\n\n\tanim.onComplete?.({ entityId, animation: anim.current });\n\n\tecs.commands.removeComponent(entityId, 'spriteAnimation');\n}\n\nfunction handleBoundary(\n\tanim: SpriteAnimation,\n\tclip: SpriteAnimationClip,\n\tentityId: number,\n\tecs: SpriteAnimationWorld,\n): boolean {\n\tanim.completedLoops++;\n\n\tif (clip.loop === 'once') {\n\t\tcompleteAnimation(anim, entityId, ecs);\n\t\treturn false;\n\t}\n\n\t// Check finite loop count\n\tif (anim.totalLoops > 0 && anim.completedLoops >= anim.totalLoops) {\n\t\tcompleteAnimation(anim, entityId, ecs);\n\t\treturn false;\n\t}\n\n\tif (clip.loop === 'pingPong') {\n\t\tanim.direction = anim.direction === 1 ? -1 : 1;\n\t\t// Step one frame in the new direction from the boundary\n\t\tanim.currentFrame += anim.direction;\n\t\treturn anim.elapsed > 0;\n\t}\n\n\t// loop mode: wrap to frame 0\n\tanim.currentFrame = 0;\n\treturn anim.elapsed > 0;\n}\n\n/**\n * Advance to next frame. Returns true if processing should continue (more overflow),\n * false if animation completed or reached a boundary.\n */\nfunction advanceFrame(\n\tanim: SpriteAnimation,\n\tclip: SpriteAnimationClip,\n\tentityId: number,\n\tecs: SpriteAnimationWorld,\n): boolean {\n\tconst nextFrame = anim.currentFrame + anim.direction;\n\n\t// Check boundary\n\tif (nextFrame >= clip.frames.length || nextFrame < 0) {\n\t\treturn handleBoundary(anim, clip, entityId, ecs);\n\t}\n\n\tanim.currentFrame = nextFrame;\n\treturn true;\n}\n\nfunction processFrameAdvancement(\n\tanim: SpriteAnimation,\n\tclip: SpriteAnimationClip,\n\tentityId: number,\n\tecs: SpriteAnimationWorld,\n): void {\n\t// Process frame overflow\n\t// eslint-disable-next-line no-constant-condition\n\twhile (true) {\n\t\tconst frameDuration = clip.frameDurations !== null\n\t\t\t? (clip.frameDurations[anim.currentFrame] ?? clip.frameDuration)\n\t\t\t: clip.frameDuration;\n\n\t\tif (frameDuration <= 0) {\n\t\t\t// Zero-duration frame: advance immediately\n\t\t\tif (!advanceFrame(anim, clip, entityId, ecs)) return;\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Floating-point-safe comparison: treat elapsed within 1μs of\n\t\t// frameDuration as having reached the boundary.\n\t\tconst remaining = frameDuration - anim.elapsed;\n\t\tif (remaining > 1e-6) return;\n\n\t\t// Frame complete — carry overflow (clamp negative remainders to 0)\n\t\tanim.elapsed = remaining < 0 ? -remaining : 0;\n\n\t\tif (!advanceFrame(anim, clip, entityId, ecs)) return;\n\t}\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a sprite animation plugin for ECSpresso.\n *\n * Provides:\n * - Frame-based animation system processing spriteAnimation components\n * - Loop modes: once, loop, pingPong\n * - justFinished one-frame flag for completion detection\n * - onComplete event publishing\n * - Sprite texture sync via structural cross-plugin access\n * - Change detection via markChanged\n */\nexport function createSpriteAnimationPlugin<\n\tG extends string = 'spriteAnimation',\n>(\n\toptions?: SpriteAnimationPluginOptions<G>,\n): Plugin<WorldConfigFrom<SpriteAnimationComponentTypes>, EmptyConfig, 'sprite-animation-update', G> {\n\tconst {\n\t\tsystemGroup = 'spriteAnimation',\n\t\tpriority = 0,\n\t\tphase = 'update',\n\t} = options ?? {};\n\n\treturn definePlugin<WorldConfigFrom<SpriteAnimationComponentTypes>, EmptyConfig, 'sprite-animation-update', G>({\n\t\tid: 'spriteAnimation',\n\t\tinstall(world) {\n\t\t\tworld\n\t\t\t\t.addSystem('sprite-animation-update')\n\t\t\t\t.setPriority(priority)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('animations', {\n\t\t\t\t\twith: ['spriteAnimation'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, dt, ecs }) => {\n\t\t\t\t\tfor (const entity of queries.animations) {\n\t\t\t\t\t\tconst anim = entity.components.spriteAnimation as SpriteAnimation;\n\t\t\t\t\t\tconst clip = anim.set.clips[anim.current];\n\t\t\t\t\t\tif (!clip) continue;\n\n\t\t\t\t\t\t// Clear justFinished from previous frame\n\t\t\t\t\t\tif (anim.justFinished) {\n\t\t\t\t\t\t\tanim.justFinished = false;\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Skip paused animations\n\t\t\t\t\t\tif (!anim.playing) continue;\n\n\t\t\t\t\t\t// Skip single-frame clips\n\t\t\t\t\t\tif (clip.frames.length <= 1) continue;\n\n\t\t\t\t\t\tconst previousFrame = anim.currentFrame;\n\t\t\t\t\t\tanim.elapsed += dt * anim.speed;\n\n\t\t\t\t\t\tprocessFrameAdvancement(anim, clip, entity.id, ecs);\n\n\t\t\t\t\t\t// Sync sprite texture if frame changed\n\t\t\t\t\t\tif (anim.currentFrame !== previousFrame || previousFrame === 0) {\n\t\t\t\t\t\t\tsyncSpriteTexture(entity.components as Record<string, unknown>, anim, clip);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (anim.currentFrame !== previousFrame) {\n\t\t\t\t\t\t\tecs.markChanged(entity.id, 'spriteAnimation');\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t},\n\t});\n}\n\n// ==================== Internal: Sprite Texture Sync ====================\n\n/**\n * Sync the sprite's texture to the current frame. Uses structural access\n * following the tween plugin's cross-component pattern.\n */\nfunction syncSpriteTexture(\n\tentityComponents: Record<string, unknown>,\n\tanim: SpriteAnimation,\n\tclip: SpriteAnimationClip,\n): void {\n\tconst sprite = entityComponents['sprite'];\n\tif (sprite && typeof sprite === 'object' && 'texture' in sprite) {\n\t\t(sprite as { texture: unknown }).texture = clip.frames[anim.currentFrame];\n\t}\n}\n"
5
+ "/**\n * Sprite Animation Plugin for ECSpresso\n *\n * ECS-native frame-based sprite animation. Advances through spritesheet frames,\n * handles loop modes (once, loop, pingPong), publishes completion events, and\n * syncs the current frame's texture to the PixiJS Sprite via structural access.\n *\n * Renderer2D is a required dependency — the `sprite` component comes from that plugin.\n * This plugin declares only `spriteAnimation` as its component type.\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type { BaseWorld } from 'ecspresso';\n\n/** BaseWorld narrowed to sprite-animation components for typed access in helpers. */\ntype SpriteAnimationWorld = BaseWorld<SpriteAnimationComponentTypes>;\n\n// ==================== Loop Mode ====================\n\nexport type AnimationLoopMode = 'once' | 'loop' | 'pingPong';\n\n// ==================== Clip Types ====================\n\n/**\n * A single animation clip: an ordered sequence of texture frames with timing.\n * Immutable and shared across entities.\n */\nexport interface SpriteAnimationClip {\n\treadonly frames: readonly unknown[];\n\treadonly frameDuration: number;\n\treadonly frameDurations: readonly number[] | null;\n\treadonly loop: AnimationLoopMode;\n}\n\n/**\n * Input format for defining a clip. Accepts either uniform or per-frame timing.\n */\nexport interface SpriteAnimationClipInput {\n\t/** Array of PixiJS Texture objects */\n\tframes: readonly unknown[];\n\t/** Uniform seconds-per-frame (used when frameDurations is not provided) */\n\tframeDuration?: number;\n\t/** Per-frame durations in seconds (overrides frameDuration) */\n\tframeDurations?: readonly number[];\n\t/** Loop mode (default: 'loop') */\n\tloop?: AnimationLoopMode;\n}\n\n// ==================== Animation Set ====================\n\n/**\n * A named collection of animation clips. Immutable and shared across entities.\n * Parameterized by A (animation name union) for compile-time validation.\n */\nexport interface SpriteAnimationSet<A extends string = string> {\n\treadonly id: string;\n\treadonly clips: { readonly [K in A]: SpriteAnimationClip };\n\treadonly defaultClip: A;\n}\n\n// ==================== Component ====================\n\n/**\n * Per-entity runtime animation state.\n */\nexport interface SpriteAnimation<A extends string = string> {\n\treadonly set: SpriteAnimationSet<A>;\n\tcurrent: A;\n\tcurrentFrame: number;\n\telapsed: number;\n\tplaying: boolean;\n\tspeed: number;\n\tdirection: 1 | -1;\n\ttotalLoops: number;\n\tcompletedLoops: number;\n\tjustFinished: boolean;\n\tonComplete?: (data: SpriteAnimationEventData) => void;\n}\n\n/**\n * Component types provided by the sprite animation plugin.\n */\nexport interface SpriteAnimationComponentTypes<A extends string = string> {\n\tspriteAnimation: SpriteAnimation<A>;\n}\n\n// ==================== Event Types ====================\n\n/**\n * Data published when an animation completes.\n */\nexport interface SpriteAnimationEventData {\n\tentityId: number;\n\tanimation: string;\n}\n\n// ==================== Plugin Options ====================\n\nexport interface SpriteAnimationPluginOptions<G extends string = 'spriteAnimation'> extends BasePluginOptions<G> {}\n\n// ==================== Helper Functions ====================\n\nfunction buildClip(input: SpriteAnimationClipInput): SpriteAnimationClip {\n\treturn Object.freeze({\n\t\tframes: Object.freeze([...input.frames]),\n\t\tframeDuration: input.frameDuration ?? (1 / 10),\n\t\tframeDurations: input.frameDurations\n\t\t\t? Object.freeze([...input.frameDurations])\n\t\t\t: null,\n\t\tloop: input.loop ?? 'loop',\n\t});\n}\n\n/**\n * Define a single-clip animation set named 'default'.\n * For simple use cases like spinning coins, pulsing effects, etc.\n *\n * @param id Unique identifier for this animation set\n * @param clip Clip definition\n * @returns A frozen SpriteAnimationSet with one clip named 'default'\n */\nexport function defineSpriteAnimation(\n\tid: string,\n\tclip: SpriteAnimationClipInput,\n): SpriteAnimationSet<'default'> {\n\treturn Object.freeze({\n\t\tid,\n\t\tclips: Object.freeze({ default: buildClip(clip) }),\n\t\tdefaultClip: 'default' as const,\n\t});\n}\n\n/**\n * Define a multi-clip animation set with named animations.\n * Animation names are inferred from the keys of the clips object.\n *\n * @param id Unique identifier for this animation set\n * @param clips Object mapping animation names to clip definitions\n * @param options Optional configuration (defaultClip)\n * @returns A frozen SpriteAnimationSet with inferred animation name union\n */\nexport function defineSpriteAnimations<A extends string>(\n\tid: string,\n\tclips: Record<A, SpriteAnimationClipInput>,\n\toptions?: { defaultClip?: NoInfer<A> },\n): SpriteAnimationSet<A> {\n\tconst builtClips = {} as Record<A, SpriteAnimationClip>;\n\tconst keys = Object.keys(clips) as A[];\n\n\tfor (const key of keys) {\n\t\tbuiltClips[key] = buildClip(clips[key]);\n\t}\n\n\tconst firstKey = keys[0];\n\tif (!firstKey) {\n\t\tthrow new Error(`defineSpriteAnimations: clips object must have at least one key`);\n\t}\n\n\treturn Object.freeze({\n\t\tid,\n\t\tclips: Object.freeze(builtClips),\n\t\tdefaultClip: options?.defaultClip ?? firstKey,\n\t});\n}\n\n/**\n * Create a spriteAnimation component from an animation set.\n *\n * @param set The animation set to use\n * @param options Optional configuration (initial clip, speed, onComplete event)\n * @returns Component object suitable for spreading into spawn()\n */\nexport function createSpriteAnimation<A extends string>(\n\tset: SpriteAnimationSet<A>,\n\toptions?: {\n\t\tinitial?: A;\n\t\tspeed?: number;\n\t\ttotalLoops?: number;\n\t\tonComplete?: (data: SpriteAnimationEventData) => void;\n\t},\n): Pick<SpriteAnimationComponentTypes<A>, 'spriteAnimation'> {\n\tconst initial = options?.initial ?? set.defaultClip;\n\treturn {\n\t\tspriteAnimation: {\n\t\t\tset,\n\t\t\tcurrent: initial,\n\t\t\tcurrentFrame: 0,\n\t\t\telapsed: 0,\n\t\t\tplaying: true,\n\t\t\tspeed: options?.speed ?? 1,\n\t\t\tdirection: 1,\n\t\t\ttotalLoops: options?.totalLoops ?? -1,\n\t\t\tcompletedLoops: 0,\n\t\t\tjustFinished: false,\n\t\t\tonComplete: options?.onComplete,\n\t\t},\n\t};\n}\n\n/**\n * Switch an entity's current animation at runtime.\n * Resets state if switching to a different animation (or restart=true).\n *\n * @returns false if entity has no spriteAnimation or animation name doesn't exist\n */\nexport function playAnimation(\n\tecs: SpriteAnimationWorld,\n\tentityId: number,\n\tanimation: string,\n\toptions?: { restart?: boolean; speed?: number },\n): boolean {\n\tconst anim = ecs.getComponent(entityId, 'spriteAnimation');\n\tif (!anim) return false;\n\tif (!(animation in anim.set.clips)) return false;\n\n\tconst shouldReset = animation !== anim.current || options?.restart === true;\n\n\tif (shouldReset) {\n\t\tanim.current = animation;\n\t\tanim.currentFrame = 0;\n\t\tanim.elapsed = 0;\n\t\tanim.direction = 1;\n\t\tanim.completedLoops = 0;\n\t\tanim.justFinished = false;\n\t}\n\n\tanim.playing = true;\n\n\tif (options?.speed !== undefined) {\n\t\tanim.speed = options.speed;\n\t}\n\n\tecs.markChanged(entityId, 'spriteAnimation');\n\treturn true;\n}\n\n/**\n * Pause an entity's animation.\n *\n * @returns false if entity has no spriteAnimation\n */\nexport function stopAnimation(\n\tecs: SpriteAnimationWorld,\n\tentityId: number,\n): boolean {\n\tconst anim = ecs.getComponent(entityId, 'spriteAnimation');\n\tif (!anim) return false;\n\n\tanim.playing = false;\n\treturn true;\n}\n\n/**\n * Resume a paused animation.\n *\n * @returns false if entity has no spriteAnimation\n */\nexport function resumeAnimation(\n\tecs: SpriteAnimationWorld,\n\tentityId: number,\n): boolean {\n\tconst anim = ecs.getComponent(entityId, 'spriteAnimation');\n\tif (!anim) return false;\n\n\tanim.playing = true;\n\treturn true;\n}\n\n// ==================== Animation Processing Helpers ====================\n\nfunction completeAnimation(\n\tanim: SpriteAnimation,\n\tentityId: number,\n\tecs: SpriteAnimationWorld,\n): void {\n\tanim.playing = false;\n\tanim.justFinished = true;\n\n\tanim.onComplete?.({ entityId, animation: anim.current });\n\n\tecs.commands.removeComponent(entityId, 'spriteAnimation');\n}\n\nfunction handleBoundary(\n\tanim: SpriteAnimation,\n\tclip: SpriteAnimationClip,\n\tentityId: number,\n\tecs: SpriteAnimationWorld,\n): boolean {\n\tanim.completedLoops++;\n\n\tif (clip.loop === 'once') {\n\t\tcompleteAnimation(anim, entityId, ecs);\n\t\treturn false;\n\t}\n\n\t// Check finite loop count\n\tif (anim.totalLoops > 0 && anim.completedLoops >= anim.totalLoops) {\n\t\tcompleteAnimation(anim, entityId, ecs);\n\t\treturn false;\n\t}\n\n\tif (clip.loop === 'pingPong') {\n\t\tanim.direction = anim.direction === 1 ? -1 : 1;\n\t\t// Step one frame in the new direction from the boundary\n\t\tanim.currentFrame += anim.direction;\n\t\treturn anim.elapsed > 0;\n\t}\n\n\t// loop mode: wrap to frame 0\n\tanim.currentFrame = 0;\n\treturn anim.elapsed > 0;\n}\n\n/**\n * Advance to next frame. Returns true if processing should continue (more overflow),\n * false if animation completed or reached a boundary.\n */\nfunction advanceFrame(\n\tanim: SpriteAnimation,\n\tclip: SpriteAnimationClip,\n\tentityId: number,\n\tecs: SpriteAnimationWorld,\n): boolean {\n\tconst nextFrame = anim.currentFrame + anim.direction;\n\n\t// Check boundary\n\tif (nextFrame >= clip.frames.length || nextFrame < 0) {\n\t\treturn handleBoundary(anim, clip, entityId, ecs);\n\t}\n\n\tanim.currentFrame = nextFrame;\n\treturn true;\n}\n\nfunction processFrameAdvancement(\n\tanim: SpriteAnimation,\n\tclip: SpriteAnimationClip,\n\tentityId: number,\n\tecs: SpriteAnimationWorld,\n): void {\n\t// Process frame overflow\n\t// eslint-disable-next-line no-constant-condition\n\twhile (true) {\n\t\tconst frameDuration = clip.frameDurations !== null\n\t\t\t? (clip.frameDurations[anim.currentFrame] ?? clip.frameDuration)\n\t\t\t: clip.frameDuration;\n\n\t\tif (frameDuration <= 0) {\n\t\t\t// Zero-duration frame: advance immediately\n\t\t\tif (!advanceFrame(anim, clip, entityId, ecs)) return;\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Floating-point-safe comparison: treat elapsed within 1μs of\n\t\t// frameDuration as having reached the boundary.\n\t\tconst remaining = frameDuration - anim.elapsed;\n\t\tif (remaining > 1e-6) return;\n\n\t\t// Frame complete — carry overflow (clamp negative remainders to 0)\n\t\tanim.elapsed = remaining < 0 ? -remaining : 0;\n\n\t\tif (!advanceFrame(anim, clip, entityId, ecs)) return;\n\t}\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a sprite animation plugin for ECSpresso.\n *\n * Provides:\n * - Frame-based animation system processing spriteAnimation components\n * - Loop modes: once, loop, pingPong\n * - justFinished one-frame flag for completion detection\n * - onComplete event publishing\n * - Sprite texture sync via structural cross-plugin access\n * - Change detection via markChanged\n */\nexport function createSpriteAnimationPlugin<\n\tG extends string = 'spriteAnimation',\n>(\n\toptions?: SpriteAnimationPluginOptions<G>,\n) {\n\tconst {\n\t\tsystemGroup = 'spriteAnimation',\n\t\tpriority = 0,\n\t\tphase = 'update',\n\t} = options ?? {};\n\n\treturn definePlugin('spriteAnimation')\n\t\t.withComponentTypes<SpriteAnimationComponentTypes>()\n\t\t.withLabels<'sprite-animation-update'>()\n\t\t.withGroups<G>()\n\t\t.install((world) => {\n\t\t\tworld\n\t\t\t\t.addSystem('sprite-animation-update')\n\t\t\t\t.setPriority(priority)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('animations', {\n\t\t\t\t\twith: ['spriteAnimation'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, dt, ecs }) => {\n\t\t\t\t\tfor (const entity of queries.animations) {\n\t\t\t\t\t\tconst anim = entity.components.spriteAnimation as SpriteAnimation;\n\t\t\t\t\t\tconst clip = anim.set.clips[anim.current];\n\t\t\t\t\t\tif (!clip) continue;\n\n\t\t\t\t\t\t// Clear justFinished from previous frame\n\t\t\t\t\t\tif (anim.justFinished) {\n\t\t\t\t\t\t\tanim.justFinished = false;\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Skip paused animations\n\t\t\t\t\t\tif (!anim.playing) continue;\n\n\t\t\t\t\t\t// Skip single-frame clips\n\t\t\t\t\t\tif (clip.frames.length <= 1) continue;\n\n\t\t\t\t\t\tconst previousFrame = anim.currentFrame;\n\t\t\t\t\t\tanim.elapsed += dt * anim.speed;\n\n\t\t\t\t\t\tprocessFrameAdvancement(anim, clip, entity.id, ecs);\n\n\t\t\t\t\t\t// Sync sprite texture if frame changed\n\t\t\t\t\t\tif (anim.currentFrame !== previousFrame || previousFrame === 0) {\n\t\t\t\t\t\t\tsyncSpriteTexture(entity.components as Record<string, unknown>, anim, clip);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (anim.currentFrame !== previousFrame) {\n\t\t\t\t\t\t\tecs.markChanged(entity.id, 'spriteAnimation');\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n}\n\n// ==================== Internal: Sprite Texture Sync ====================\n\n/**\n * Sync the sprite's texture to the current frame. Uses structural access\n * following the tween plugin's cross-component pattern.\n */\nfunction syncSpriteTexture(\n\tentityComponents: Record<string, unknown>,\n\tanim: SpriteAnimation,\n\tclip: SpriteAnimationClip,\n): void {\n\tconst sprite = entityComponents['sprite'];\n\tif (sprite && typeof sprite === 'object' && 'texture' in sprite) {\n\t\t(sprite as { texture: unknown }).texture = clip.frames[anim.currentFrame];\n\t}\n}\n"
6
6
  ],
7
- "mappings": "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",
8
- "debugId": "C75605DB29C1814064756E2164756E21",
7
+ "mappings": "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",
8
+ "debugId": "D1E817D3F2D57D1F64756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -7,9 +7,8 @@
7
7
  * Each entity gets a `stateMachine` component referencing a shared definition.
8
8
  * One system processes all state machine entities each tick.
9
9
  */
10
- import { type Plugin, type BasePluginOptions } from 'ecspresso';
10
+ import { type BasePluginOptions } from 'ecspresso';
11
11
  import type { BaseWorld } from 'ecspresso';
12
- import type { WorldConfigFrom, EmptyConfig } from '../type-utils';
13
12
  /** BaseWorld narrowed to state-machine components for typed access in helpers. */
14
13
  type StateMachineWorld = BaseWorld<StateMachineComponentTypes>;
15
14
  /**
@@ -240,5 +239,5 @@ export declare function createStateMachineHelpers<W extends BaseWorld<StateMachi
240
239
  * });
241
240
  * ```
242
241
  */
243
- export declare function createStateMachinePlugin<S extends string = string, G extends string = 'stateMachine'>(options?: StateMachinePluginOptions<G>): Plugin<WorldConfigFrom<StateMachineComponentTypes<S>, StateMachineEventTypes<S>>, EmptyConfig, 'state-machine-update', G>;
242
+ export declare function createStateMachinePlugin<S extends string = string, G extends string = 'stateMachine'>(options?: StateMachinePluginOptions<G>): import("ecspresso").Plugin<import("ecspresso").WithEvents<import("ecspresso").WithComponents<import("ecspresso").EmptyConfig, StateMachineComponentTypes<S>>, StateMachineEventTypes<S>>, import("ecspresso").EmptyConfig, "state-machine-update", G, never, never>;
244
243
  export {};
@@ -1,4 +1,4 @@
1
- var Y=((b)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(b,{get:(j,z)=>(typeof require<"u"?require:j)[z]}):b)(function(b){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+b+'" is not supported')});import{definePlugin as W}from"ecspresso";function X(b,j){return Object.freeze({id:b,initial:j.initial,states:Object.freeze(j.states)})}function $(b,j){let z=j?.initial??b.initial;return{stateMachine:{definition:b,current:z,previous:null,stateTime:0}}}function L(b,j,z,A){let H=z.definition.states,D=H[z.current],B=H[A];if(!B)return!1;return D?.onExit?.({ecs:b,entityId:j}),z.previous=z.current,z.current=A,z.stateTime=0,B.onEnter?.({ecs:b,entityId:j}),b.markChanged(j,"stateMachine"),b.eventBus.publish("stateTransition",{entityId:j,from:z.previous,to:z.current,definitionId:z.definition.id}),!0}function E(b,j,z){let A=b.getComponent(j,"stateMachine");if(!A)return!1;return L(b,j,A,z)}function G(b,j,z){let A=b.getComponent(j,"stateMachine");if(!A)return!1;let D=A.definition.states[A.current];if(!D?.on)return!1;let B=D.on[z];if(B===void 0)return!1;if(typeof B==="string")return L(b,j,A,B);if(!B.guard({ecs:b,entityId:j}))return!1;return L(b,j,A,B.target)}function M(b,j){return b.getComponent(j,"stateMachine")?.current}function P(b){return{defineStateMachine:X}}function q(b){let{systemGroup:j="stateMachine",priority:z=0,phase:A="update"}=b??{};return W({id:"stateMachine",install(H){H.addSystem("state-machine-update").setPriority(z).inPhase(A).inGroup(j).addQuery("machines",{with:["stateMachine"]}).setOnEntityEnter("machines",({entity:D,ecs:B})=>{let K=D.components.stateMachine,Q=K.definition.states,F=B;Q[K.current]?.onEnter?.({ecs:F,entityId:D.id})}).setProcess(({queries:D,dt:B,ecs:K})=>{let F={ecs:K,entityId:0,dt:0};for(let O of D.machines){let J=O.components.stateMachine,R=J.definition.states;F.entityId=O.id,F.dt=B,J.stateTime+=B,R[J.current]?.onUpdate?.(F);let U=R[J.current];if(U?.transitions){for(let V of U.transitions)if(V.guard(F)){L(F.ecs,O.id,J,V.target);break}}}})}})}export{E as transitionTo,G as sendEvent,M as getStateMachineState,X as defineStateMachine,q as createStateMachinePlugin,P as createStateMachineHelpers,$ as createStateMachine};
1
+ var Y=((b)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(b,{get:(j,z)=>(typeof require<"u"?require:j)[z]}):b)(function(b){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+b+'" is not supported')});import{definePlugin as W}from"ecspresso";function X(b,j){return Object.freeze({id:b,initial:j.initial,states:Object.freeze(j.states)})}function $(b,j){let z=j?.initial??b.initial;return{stateMachine:{definition:b,current:z,previous:null,stateTime:0}}}function L(b,j,z,A){let H=z.definition.states,D=H[z.current],B=H[A];if(!B)return!1;return D?.onExit?.({ecs:b,entityId:j}),z.previous=z.current,z.current=A,z.stateTime=0,B.onEnter?.({ecs:b,entityId:j}),b.markChanged(j,"stateMachine"),b.eventBus.publish("stateTransition",{entityId:j,from:z.previous,to:z.current,definitionId:z.definition.id}),!0}function E(b,j,z){let A=b.getComponent(j,"stateMachine");if(!A)return!1;return L(b,j,A,z)}function G(b,j,z){let A=b.getComponent(j,"stateMachine");if(!A)return!1;let D=A.definition.states[A.current];if(!D?.on)return!1;let B=D.on[z];if(B===void 0)return!1;if(typeof B==="string")return L(b,j,A,B);if(!B.guard({ecs:b,entityId:j}))return!1;return L(b,j,A,B.target)}function M(b,j){return b.getComponent(j,"stateMachine")?.current}function P(b){return{defineStateMachine:X}}function q(b){let{systemGroup:j="stateMachine",priority:z=0,phase:A="update"}=b??{};return W("stateMachine").withComponentTypes().withEventTypes().withLabels().withGroups().install((H)=>{H.addSystem("state-machine-update").setPriority(z).inPhase(A).inGroup(j).addQuery("machines",{with:["stateMachine"]}).setOnEntityEnter("machines",({entity:D,ecs:B})=>{let K=D.components.stateMachine,Q=K.definition.states,F=B;Q[K.current]?.onEnter?.({ecs:F,entityId:D.id})}).setProcess(({queries:D,dt:B,ecs:K})=>{let F={ecs:K,entityId:0,dt:0};for(let O of D.machines){let J=O.components.stateMachine,R=J.definition.states;F.entityId=O.id,F.dt=B,J.stateTime+=B,R[J.current]?.onUpdate?.(F);let U=R[J.current];if(U?.transitions){for(let V of U.transitions)if(V.guard(F)){L(F.ecs,O.id,J,V.target);break}}}})})}export{E as transitionTo,G as sendEvent,M as getStateMachineState,X as defineStateMachine,q as createStateMachinePlugin,P as createStateMachineHelpers,$ as createStateMachine};
2
2
 
3
- //# debugId=995F6042C9E1B1C064756E2164756E21
3
+ //# debugId=D219C3158E67789D64756E2164756E21
4
4
  //# sourceMappingURL=state-machine.js.map
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/plugins/state-machine.ts"],
4
4
  "sourcesContent": [
5
- "/**\n * State Machine Plugin for ECSpresso\n *\n * Provides ECS-native finite state machines with guard-based transitions,\n * event-driven transitions, and lifecycle hooks (onEnter, onExit, onUpdate).\n *\n * Each entity gets a `stateMachine` component referencing a shared definition.\n * One system processes all state machine entities each tick.\n */\n\nimport { definePlugin, type Plugin, type BasePluginOptions } from 'ecspresso';\nimport type { BaseWorld } from 'ecspresso';\nimport type { WorldConfigFrom, EmptyConfig } from '../type-utils';\n\n/** BaseWorld narrowed to state-machine components for typed access in helpers. */\ntype StateMachineWorld = BaseWorld<StateMachineComponentTypes>;\n\n// ==================== State Config ====================\n\n/**\n * Configuration for a single state in a state machine definition.\n *\n * @template S - Union of state name strings\n * @template W - World interface type for hooks/guards (default: StateMachineWorld)\n */\nexport interface StateConfig<S extends string, W extends BaseWorld<StateMachineComponentTypes> = StateMachineWorld> {\n\t/** Called when entering this state */\n\tonEnter?(ctx: { ecs: W; entityId: number }): void;\n\t/** Called when exiting this state */\n\tonExit?(ctx: { ecs: W; entityId: number }): void;\n\t/** Called each tick while in this state */\n\tonUpdate?(ctx: { ecs: W; entityId: number; dt: number }): void;\n\t/** Guard-based transitions evaluated each tick. First passing guard wins. */\n\ttransitions?: ReadonlyArray<{\n\t\ttarget: S;\n\t\tguard(ctx: { ecs: W; entityId: number }): boolean;\n\t}>;\n\t/** Event-based transition map: eventName → target state or guarded transition */\n\ton?: Record<string, S | { target: S; guard(ctx: { ecs: W; entityId: number }): boolean }>;\n}\n\n// ==================== State Machine Definition ====================\n\n/**\n * Immutable definition of a state machine. Shared across entities.\n *\n * @template S - Union of state name strings\n */\nexport interface StateMachineDefinition<S extends string> {\n\treadonly id: string;\n\treadonly initial: S;\n\treadonly states: { readonly [K in S]: StateConfig<S> };\n}\n\n// ==================== Component ====================\n\n/**\n * Runtime state machine component stored on each entity.\n *\n * @template S - Union of state name strings (default: string)\n */\nexport interface StateMachine<S extends string = string> {\n\treadonly definition: StateMachineDefinition<string>;\n\tcurrent: S;\n\tprevious: S | null;\n\tstateTime: number;\n}\n\n/**\n * Component types provided by the state machine plugin.\n *\n * @template S - Union of state name strings (default: string)\n */\nexport interface StateMachineComponentTypes<S extends string = string> {\n\tstateMachine: StateMachine<S>;\n}\n\n// ==================== Event Types ====================\n\n/**\n * Event published on every state transition.\n *\n * @template S - Union of state name strings (default: string)\n */\nexport interface StateTransitionEvent<S extends string = string> {\n\tentityId: number;\n\tfrom: S;\n\tto: S;\n\tdefinitionId: string;\n}\n\n/**\n * Event types provided by the state machine plugin.\n *\n * @template S - Union of state name strings (default: string)\n */\nexport interface StateMachineEventTypes<S extends string = string> {\n\tstateTransition: StateTransitionEvent<S>;\n}\n\n/**\n * Extract the state name union from a StateMachineDefinition.\n *\n * @example\n * ```typescript\n * const enemyFSM = defineStateMachine('enemy', { initial: 'idle', states: { idle: {}, chase: {} } });\n * type EnemyStates = StatesOf<typeof enemyFSM>; // 'idle' | 'chase'\n * type AllStates = StatesOf<typeof enemyFSM> | StatesOf<typeof playerFSM>;\n * ```\n */\nexport type StatesOf<D> = D extends StateMachineDefinition<infer S> ? S : never;\n\n// ==================== Plugin Options ====================\n\n/**\n * Configuration options for the state machine plugin.\n */\nexport interface StateMachinePluginOptions<G extends string = 'stateMachine'> extends BasePluginOptions<G> {}\n\n// ==================== Helper Functions ====================\n\n/**\n * Define a state machine with type-safe state names.\n *\n * @template S - Union of state name strings, inferred from `states` keys\n * @param id - Unique identifier for this definition\n * @param config - Initial state and state configurations\n * @returns A frozen StateMachineDefinition\n *\n * @example\n * ```typescript\n * const enemyFSM = defineStateMachine('enemy', {\n * initial: 'idle',\n * states: {\n * idle: {\n * onEnter: ({ ecs, entityId }) => { ... },\n * transitions: [{ target: 'chase', guard: ({ ecs, entityId }) => playerNearby(ecs, entityId) }],\n * },\n * chase: {\n * onUpdate: ({ ecs, entityId, dt }) => { ... },\n * on: { playerLost: 'idle' },\n * },\n * },\n * });\n * ```\n */\nexport function defineStateMachine<S extends string>(\n\tid: string,\n\tconfig: { initial: NoInfer<S>; states: Record<S, StateConfig<NoInfer<S>>> },\n): StateMachineDefinition<S> {\n\treturn Object.freeze({\n\t\tid,\n\t\tinitial: config.initial,\n\t\tstates: Object.freeze(config.states),\n\t}) as StateMachineDefinition<S>;\n}\n\n/**\n * Create a stateMachine component from a definition.\n *\n * @param definition - The state machine definition to use\n * @param options - Optional overrides (e.g., initial state)\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createStateMachine(enemyFSM),\n * position: { x: 100, y: 200 },\n * });\n * ```\n */\nexport function createStateMachine<S extends string>(\n\tdefinition: StateMachineDefinition<S>,\n\toptions?: { initial?: S },\n): Pick<StateMachineComponentTypes<S>, 'stateMachine'> {\n\tconst initial = options?.initial ?? definition.initial;\n\treturn {\n\t\tstateMachine: {\n\t\t\tdefinition,\n\t\t\tcurrent: initial,\n\t\t\tprevious: null,\n\t\t\tstateTime: 0,\n\t\t},\n\t};\n}\n\n// ==================== Internal: Shared Transition Logic ====================\n\n/**\n * Perform a state transition: onExit → update fields → onEnter → markChanged → publish event.\n * Returns true if the target state exists, false otherwise.\n */\nfunction performTransition(\n\tecs: StateMachineWorld,\n\tentityId: number,\n\tsm: StateMachine,\n\ttargetState: string,\n): boolean {\n\tconst states = sm.definition.states as Record<string, StateConfig<string>>;\n\tconst currentConfig = states[sm.current];\n\tconst targetConfig = states[targetState];\n\n\tif (!targetConfig) return false;\n\n\tcurrentConfig?.onExit?.({ ecs, entityId });\n\n\tsm.previous = sm.current;\n\tsm.current = targetState;\n\tsm.stateTime = 0;\n\n\ttargetConfig.onEnter?.({ ecs, entityId });\n\n\tecs.markChanged(entityId, 'stateMachine');\n\tecs.eventBus.publish('stateTransition', {\n\t\tentityId,\n\t\tfrom: sm.previous,\n\t\tto: sm.current,\n\t\tdefinitionId: sm.definition.id,\n\t} satisfies StateTransitionEvent);\n\n\treturn true;\n}\n\n// ==================== Utility Functions ====================\n\n/**\n * Directly transition an entity's state machine to a target state.\n * Fires onExit, onEnter hooks and publishes stateTransition event.\n *\n * @param ecs - ECS instance (structural typing)\n * @param entityId - Entity to transition\n * @param targetState - State to transition to\n * @returns true if transition succeeded, false if entity has no stateMachine or target state doesn't exist\n */\nexport function transitionTo(\n\tecs: StateMachineWorld,\n\tentityId: number,\n\ttargetState: string,\n): boolean {\n\tconst sm = ecs.getComponent(entityId, 'stateMachine');\n\tif (!sm) return false;\n\treturn performTransition(ecs, entityId, sm, targetState);\n}\n\n/**\n * Send a named event to an entity's state machine.\n * Checks the current state's `on` handlers for a matching event.\n *\n * @param ecs - ECS instance (structural typing)\n * @param entityId - Entity to send event to\n * @param eventName - Event name to match against `on` handlers\n * @returns true if a transition occurred, false otherwise\n */\nexport function sendEvent(\n\tecs: StateMachineWorld,\n\tentityId: number,\n\teventName: string,\n): boolean {\n\tconst sm = ecs.getComponent(entityId, 'stateMachine');\n\tif (!sm) return false;\n\n\tconst states = sm.definition.states as Record<string, StateConfig<string>>;\n\tconst currentConfig = states[sm.current];\n\tif (!currentConfig?.on) return false;\n\n\tconst handler = currentConfig.on[eventName];\n\tif (handler === undefined) return false;\n\n\tif (typeof handler === 'string') {\n\t\treturn performTransition(ecs, entityId, sm, handler);\n\t}\n\n\tif (!handler.guard({ ecs, entityId })) return false;\n\treturn performTransition(ecs, entityId, sm, handler.target);\n}\n\n/**\n * Get the current state of an entity's state machine.\n *\n * @param ecs - ECS instance (structural typing)\n * @param entityId - Entity to query\n * @returns The current state string, or undefined if entity has no stateMachine\n */\nexport function getStateMachineState(\n\tecs: StateMachineWorld,\n\tentityId: number,\n): string | undefined {\n\tconst sm = ecs.getComponent(entityId, 'stateMachine');\n\treturn sm?.current;\n}\n\n// ==================== State Machine Helpers ====================\n\n/**\n * Typed helpers for the state machine plugin.\n * Creates helpers that validate hook parameters against the world type W.\n * Call after .build() using typeof ecs.\n */\nexport interface StateMachineHelpers<W extends BaseWorld<StateMachineComponentTypes>> {\n\tdefineStateMachine: <S extends string>(\n\t\tid: string,\n\t\tconfig: { initial: NoInfer<S>; states: Record<S, StateConfig<NoInfer<S>, W>> },\n\t) => StateMachineDefinition<S>;\n}\n\nexport function createStateMachineHelpers<W extends BaseWorld<StateMachineComponentTypes> = StateMachineWorld>(_world?: W): StateMachineHelpers<W> {\n\treturn {\n\t\tdefineStateMachine: defineStateMachine as StateMachineHelpers<W>['defineStateMachine'],\n\t};\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a state machine plugin for ECSpresso.\n *\n * Provides:\n * - Lifecycle hooks (onEnter, onExit, onUpdate) per state\n * - Guard-based automatic transitions evaluated each tick\n * - Event-based transitions via `sendEvent()`\n * - Direct transitions via `transitionTo()`\n * - stateTransition events published on every transition\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createStateMachinePlugin())\n * .build();\n *\n * const fsm = defineStateMachine('enemy', {\n * initial: 'idle',\n * states: {\n * idle: {\n * transitions: [{ target: 'chase', guard: ({ ecs, entityId }) => playerNearby(ecs, entityId) }],\n * },\n * chase: {\n * onUpdate: ({ ecs, entityId, dt }) => moveTowardPlayer(ecs, entityId, dt),\n * on: { playerLost: 'idle' },\n * },\n * },\n * });\n *\n * ecs.spawn({\n * ...createStateMachine(fsm),\n * position: { x: 0, y: 0 },\n * });\n * ```\n */\nexport function createStateMachinePlugin<S extends string = string, G extends string = 'stateMachine'>(\n\toptions?: StateMachinePluginOptions<G>,\n): Plugin<WorldConfigFrom<StateMachineComponentTypes<S>, StateMachineEventTypes<S>>, EmptyConfig, 'state-machine-update', G> {\n\tconst {\n\t\tsystemGroup = 'stateMachine',\n\t\tpriority = 0,\n\t\tphase = 'update',\n\t} = options ?? {};\n\n\treturn definePlugin<WorldConfigFrom<StateMachineComponentTypes<S>, StateMachineEventTypes<S>>, EmptyConfig, 'state-machine-update', G>({\n\t\tid: 'stateMachine',\n\t\tinstall(world) {\n\t\t\tworld\n\t\t\t\t.addSystem('state-machine-update')\n\t\t\t\t.setPriority(priority)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('machines', {\n\t\t\t\t\twith: ['stateMachine'],\n\t\t\t\t})\n\t\t\t\t.setOnEntityEnter('machines', ({ entity, ecs }) => {\n\t\t\t\t\tconst sm = entity.components.stateMachine;\n\t\t\t\t\tconst states = sm.definition.states as Record<string, StateConfig<string>>;\n\t\t\t\t\tconst world: StateMachineWorld = ecs;\n\t\t\t\t\tstates[sm.current]?.onEnter?.({ ecs: world, entityId: entity.id });\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, dt, ecs }) => {\n\t\t\t\t\t// Pre-allocated context reused across entities to avoid per-entity-per-frame allocations\n\t\t\t\t\tconst world: StateMachineWorld = ecs;\n\t\t\t\t\tconst hookCtx = { ecs: world, entityId: 0, dt: 0 };\n\n\t\t\t\t\tfor (const entity of queries.machines) {\n\t\t\t\t\t\tconst sm = entity.components.stateMachine;\n\t\t\t\t\t\tconst states = sm.definition.states as Record<string, StateConfig<string>>;\n\n\t\t\t\t\t\thookCtx.entityId = entity.id;\n\t\t\t\t\t\thookCtx.dt = dt;\n\n\t\t\t\t\t\t// Accumulate state time\n\t\t\t\t\t\tsm.stateTime += dt;\n\n\t\t\t\t\t\t// onUpdate hook\n\t\t\t\t\t\tstates[sm.current]?.onUpdate?.(hookCtx);\n\n\t\t\t\t\t\t// Evaluate guard transitions (first passing guard wins)\n\t\t\t\t\t\tconst currentConfig = states[sm.current];\n\t\t\t\t\t\tif (currentConfig?.transitions) {\n\t\t\t\t\t\t\tfor (const transition of currentConfig.transitions) {\n\t\t\t\t\t\t\t\tif (transition.guard(hookCtx)) {\n\t\t\t\t\t\t\t\t\tperformTransition(hookCtx.ecs, entity.id, sm, transition.target);\n\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t},\n\t});\n}\n"
5
+ "/**\n * State Machine Plugin for ECSpresso\n *\n * Provides ECS-native finite state machines with guard-based transitions,\n * event-driven transitions, and lifecycle hooks (onEnter, onExit, onUpdate).\n *\n * Each entity gets a `stateMachine` component referencing a shared definition.\n * One system processes all state machine entities each tick.\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type { BaseWorld } from 'ecspresso';\n\n/** BaseWorld narrowed to state-machine components for typed access in helpers. */\ntype StateMachineWorld = BaseWorld<StateMachineComponentTypes>;\n\n// ==================== State Config ====================\n\n/**\n * Configuration for a single state in a state machine definition.\n *\n * @template S - Union of state name strings\n * @template W - World interface type for hooks/guards (default: StateMachineWorld)\n */\nexport interface StateConfig<S extends string, W extends BaseWorld<StateMachineComponentTypes> = StateMachineWorld> {\n\t/** Called when entering this state */\n\tonEnter?(ctx: { ecs: W; entityId: number }): void;\n\t/** Called when exiting this state */\n\tonExit?(ctx: { ecs: W; entityId: number }): void;\n\t/** Called each tick while in this state */\n\tonUpdate?(ctx: { ecs: W; entityId: number; dt: number }): void;\n\t/** Guard-based transitions evaluated each tick. First passing guard wins. */\n\ttransitions?: ReadonlyArray<{\n\t\ttarget: S;\n\t\tguard(ctx: { ecs: W; entityId: number }): boolean;\n\t}>;\n\t/** Event-based transition map: eventName → target state or guarded transition */\n\ton?: Record<string, S | { target: S; guard(ctx: { ecs: W; entityId: number }): boolean }>;\n}\n\n// ==================== State Machine Definition ====================\n\n/**\n * Immutable definition of a state machine. Shared across entities.\n *\n * @template S - Union of state name strings\n */\nexport interface StateMachineDefinition<S extends string> {\n\treadonly id: string;\n\treadonly initial: S;\n\treadonly states: { readonly [K in S]: StateConfig<S> };\n}\n\n// ==================== Component ====================\n\n/**\n * Runtime state machine component stored on each entity.\n *\n * @template S - Union of state name strings (default: string)\n */\nexport interface StateMachine<S extends string = string> {\n\treadonly definition: StateMachineDefinition<string>;\n\tcurrent: S;\n\tprevious: S | null;\n\tstateTime: number;\n}\n\n/**\n * Component types provided by the state machine plugin.\n *\n * @template S - Union of state name strings (default: string)\n */\nexport interface StateMachineComponentTypes<S extends string = string> {\n\tstateMachine: StateMachine<S>;\n}\n\n// ==================== Event Types ====================\n\n/**\n * Event published on every state transition.\n *\n * @template S - Union of state name strings (default: string)\n */\nexport interface StateTransitionEvent<S extends string = string> {\n\tentityId: number;\n\tfrom: S;\n\tto: S;\n\tdefinitionId: string;\n}\n\n/**\n * Event types provided by the state machine plugin.\n *\n * @template S - Union of state name strings (default: string)\n */\nexport interface StateMachineEventTypes<S extends string = string> {\n\tstateTransition: StateTransitionEvent<S>;\n}\n\n/**\n * Extract the state name union from a StateMachineDefinition.\n *\n * @example\n * ```typescript\n * const enemyFSM = defineStateMachine('enemy', { initial: 'idle', states: { idle: {}, chase: {} } });\n * type EnemyStates = StatesOf<typeof enemyFSM>; // 'idle' | 'chase'\n * type AllStates = StatesOf<typeof enemyFSM> | StatesOf<typeof playerFSM>;\n * ```\n */\nexport type StatesOf<D> = D extends StateMachineDefinition<infer S> ? S : never;\n\n// ==================== Plugin Options ====================\n\n/**\n * Configuration options for the state machine plugin.\n */\nexport interface StateMachinePluginOptions<G extends string = 'stateMachine'> extends BasePluginOptions<G> {}\n\n// ==================== Helper Functions ====================\n\n/**\n * Define a state machine with type-safe state names.\n *\n * @template S - Union of state name strings, inferred from `states` keys\n * @param id - Unique identifier for this definition\n * @param config - Initial state and state configurations\n * @returns A frozen StateMachineDefinition\n *\n * @example\n * ```typescript\n * const enemyFSM = defineStateMachine('enemy', {\n * initial: 'idle',\n * states: {\n * idle: {\n * onEnter: ({ ecs, entityId }) => { ... },\n * transitions: [{ target: 'chase', guard: ({ ecs, entityId }) => playerNearby(ecs, entityId) }],\n * },\n * chase: {\n * onUpdate: ({ ecs, entityId, dt }) => { ... },\n * on: { playerLost: 'idle' },\n * },\n * },\n * });\n * ```\n */\nexport function defineStateMachine<S extends string>(\n\tid: string,\n\tconfig: { initial: NoInfer<S>; states: Record<S, StateConfig<NoInfer<S>>> },\n): StateMachineDefinition<S> {\n\treturn Object.freeze({\n\t\tid,\n\t\tinitial: config.initial,\n\t\tstates: Object.freeze(config.states),\n\t}) as StateMachineDefinition<S>;\n}\n\n/**\n * Create a stateMachine component from a definition.\n *\n * @param definition - The state machine definition to use\n * @param options - Optional overrides (e.g., initial state)\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createStateMachine(enemyFSM),\n * position: { x: 100, y: 200 },\n * });\n * ```\n */\nexport function createStateMachine<S extends string>(\n\tdefinition: StateMachineDefinition<S>,\n\toptions?: { initial?: S },\n): Pick<StateMachineComponentTypes<S>, 'stateMachine'> {\n\tconst initial = options?.initial ?? definition.initial;\n\treturn {\n\t\tstateMachine: {\n\t\t\tdefinition,\n\t\t\tcurrent: initial,\n\t\t\tprevious: null,\n\t\t\tstateTime: 0,\n\t\t},\n\t};\n}\n\n// ==================== Internal: Shared Transition Logic ====================\n\n/**\n * Perform a state transition: onExit → update fields → onEnter → markChanged → publish event.\n * Returns true if the target state exists, false otherwise.\n */\nfunction performTransition(\n\tecs: StateMachineWorld,\n\tentityId: number,\n\tsm: StateMachine,\n\ttargetState: string,\n): boolean {\n\tconst states = sm.definition.states as Record<string, StateConfig<string>>;\n\tconst currentConfig = states[sm.current];\n\tconst targetConfig = states[targetState];\n\n\tif (!targetConfig) return false;\n\n\tcurrentConfig?.onExit?.({ ecs, entityId });\n\n\tsm.previous = sm.current;\n\tsm.current = targetState;\n\tsm.stateTime = 0;\n\n\ttargetConfig.onEnter?.({ ecs, entityId });\n\n\tecs.markChanged(entityId, 'stateMachine');\n\tecs.eventBus.publish('stateTransition', {\n\t\tentityId,\n\t\tfrom: sm.previous,\n\t\tto: sm.current,\n\t\tdefinitionId: sm.definition.id,\n\t} satisfies StateTransitionEvent);\n\n\treturn true;\n}\n\n// ==================== Utility Functions ====================\n\n/**\n * Directly transition an entity's state machine to a target state.\n * Fires onExit, onEnter hooks and publishes stateTransition event.\n *\n * @param ecs - ECS instance (structural typing)\n * @param entityId - Entity to transition\n * @param targetState - State to transition to\n * @returns true if transition succeeded, false if entity has no stateMachine or target state doesn't exist\n */\nexport function transitionTo(\n\tecs: StateMachineWorld,\n\tentityId: number,\n\ttargetState: string,\n): boolean {\n\tconst sm = ecs.getComponent(entityId, 'stateMachine');\n\tif (!sm) return false;\n\treturn performTransition(ecs, entityId, sm, targetState);\n}\n\n/**\n * Send a named event to an entity's state machine.\n * Checks the current state's `on` handlers for a matching event.\n *\n * @param ecs - ECS instance (structural typing)\n * @param entityId - Entity to send event to\n * @param eventName - Event name to match against `on` handlers\n * @returns true if a transition occurred, false otherwise\n */\nexport function sendEvent(\n\tecs: StateMachineWorld,\n\tentityId: number,\n\teventName: string,\n): boolean {\n\tconst sm = ecs.getComponent(entityId, 'stateMachine');\n\tif (!sm) return false;\n\n\tconst states = sm.definition.states as Record<string, StateConfig<string>>;\n\tconst currentConfig = states[sm.current];\n\tif (!currentConfig?.on) return false;\n\n\tconst handler = currentConfig.on[eventName];\n\tif (handler === undefined) return false;\n\n\tif (typeof handler === 'string') {\n\t\treturn performTransition(ecs, entityId, sm, handler);\n\t}\n\n\tif (!handler.guard({ ecs, entityId })) return false;\n\treturn performTransition(ecs, entityId, sm, handler.target);\n}\n\n/**\n * Get the current state of an entity's state machine.\n *\n * @param ecs - ECS instance (structural typing)\n * @param entityId - Entity to query\n * @returns The current state string, or undefined if entity has no stateMachine\n */\nexport function getStateMachineState(\n\tecs: StateMachineWorld,\n\tentityId: number,\n): string | undefined {\n\tconst sm = ecs.getComponent(entityId, 'stateMachine');\n\treturn sm?.current;\n}\n\n// ==================== State Machine Helpers ====================\n\n/**\n * Typed helpers for the state machine plugin.\n * Creates helpers that validate hook parameters against the world type W.\n * Call after .build() using typeof ecs.\n */\nexport interface StateMachineHelpers<W extends BaseWorld<StateMachineComponentTypes>> {\n\tdefineStateMachine: <S extends string>(\n\t\tid: string,\n\t\tconfig: { initial: NoInfer<S>; states: Record<S, StateConfig<NoInfer<S>, W>> },\n\t) => StateMachineDefinition<S>;\n}\n\nexport function createStateMachineHelpers<W extends BaseWorld<StateMachineComponentTypes> = StateMachineWorld>(_world?: W): StateMachineHelpers<W> {\n\treturn {\n\t\tdefineStateMachine: defineStateMachine as StateMachineHelpers<W>['defineStateMachine'],\n\t};\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a state machine plugin for ECSpresso.\n *\n * Provides:\n * - Lifecycle hooks (onEnter, onExit, onUpdate) per state\n * - Guard-based automatic transitions evaluated each tick\n * - Event-based transitions via `sendEvent()`\n * - Direct transitions via `transitionTo()`\n * - stateTransition events published on every transition\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createStateMachinePlugin())\n * .build();\n *\n * const fsm = defineStateMachine('enemy', {\n * initial: 'idle',\n * states: {\n * idle: {\n * transitions: [{ target: 'chase', guard: ({ ecs, entityId }) => playerNearby(ecs, entityId) }],\n * },\n * chase: {\n * onUpdate: ({ ecs, entityId, dt }) => moveTowardPlayer(ecs, entityId, dt),\n * on: { playerLost: 'idle' },\n * },\n * },\n * });\n *\n * ecs.spawn({\n * ...createStateMachine(fsm),\n * position: { x: 0, y: 0 },\n * });\n * ```\n */\nexport function createStateMachinePlugin<S extends string = string, G extends string = 'stateMachine'>(\n\toptions?: StateMachinePluginOptions<G>,\n) {\n\tconst {\n\t\tsystemGroup = 'stateMachine',\n\t\tpriority = 0,\n\t\tphase = 'update',\n\t} = options ?? {};\n\n\treturn definePlugin('stateMachine')\n\t\t.withComponentTypes<StateMachineComponentTypes<S>>()\n\t\t.withEventTypes<StateMachineEventTypes<S>>()\n\t\t.withLabels<'state-machine-update'>()\n\t\t.withGroups<G>()\n\t\t.install((world) => {\n\t\t\tworld\n\t\t\t\t.addSystem('state-machine-update')\n\t\t\t\t.setPriority(priority)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('machines', {\n\t\t\t\t\twith: ['stateMachine'],\n\t\t\t\t})\n\t\t\t\t.setOnEntityEnter('machines', ({ entity, ecs }) => {\n\t\t\t\t\tconst sm = entity.components.stateMachine;\n\t\t\t\t\tconst states = sm.definition.states as Record<string, StateConfig<string>>;\n\t\t\t\t\tconst world: StateMachineWorld = ecs;\n\t\t\t\t\tstates[sm.current]?.onEnter?.({ ecs: world, entityId: entity.id });\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, dt, ecs }) => {\n\t\t\t\t\t// Pre-allocated context reused across entities to avoid per-entity-per-frame allocations\n\t\t\t\t\tconst world: StateMachineWorld = ecs;\n\t\t\t\t\tconst hookCtx = { ecs: world, entityId: 0, dt: 0 };\n\n\t\t\t\t\tfor (const entity of queries.machines) {\n\t\t\t\t\t\tconst sm = entity.components.stateMachine;\n\t\t\t\t\t\tconst states = sm.definition.states as Record<string, StateConfig<string>>;\n\n\t\t\t\t\t\thookCtx.entityId = entity.id;\n\t\t\t\t\t\thookCtx.dt = dt;\n\n\t\t\t\t\t\t// Accumulate state time\n\t\t\t\t\t\tsm.stateTime += dt;\n\n\t\t\t\t\t\t// onUpdate hook\n\t\t\t\t\t\tstates[sm.current]?.onUpdate?.(hookCtx);\n\n\t\t\t\t\t\t// Evaluate guard transitions (first passing guard wins)\n\t\t\t\t\t\tconst currentConfig = states[sm.current];\n\t\t\t\t\t\tif (currentConfig?.transitions) {\n\t\t\t\t\t\t\tfor (const transition of currentConfig.transitions) {\n\t\t\t\t\t\t\t\tif (transition.guard(hookCtx)) {\n\t\t\t\t\t\t\t\t\tperformTransition(hookCtx.ecs, entity.id, sm, transition.target);\n\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n}\n"
6
6
  ],
7
- "mappings": "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",
8
- "debugId": "995F6042C9E1B1C064756E2164756E21",
7
+ "mappings": "2PAUA,uBAAS,kBAuIF,SAAS,CAAoC,CACnD,EACA,EAC4B,CAC5B,OAAO,OAAO,OAAO,CACpB,KACA,QAAS,EAAO,QAChB,OAAQ,OAAO,OAAO,EAAO,MAAM,CACpC,CAAC,EAkBK,SAAS,CAAoC,CACnD,EACA,EACsD,CACtD,IAAM,EAAU,GAAS,SAAW,EAAW,QAC/C,MAAO,CACN,aAAc,CACb,aACA,QAAS,EACT,SAAU,KACV,UAAW,CACZ,CACD,EASD,SAAS,CAAiB,CACzB,EACA,EACA,EACA,EACU,CACV,IAAM,EAAS,EAAG,WAAW,OACvB,EAAgB,EAAO,EAAG,SAC1B,EAAe,EAAO,GAE5B,GAAI,CAAC,EAAc,MAAO,GAkB1B,OAhBA,GAAe,SAAS,CAAE,MAAK,UAAS,CAAC,EAEzC,EAAG,SAAW,EAAG,QACjB,EAAG,QAAU,EACb,EAAG,UAAY,EAEf,EAAa,UAAU,CAAE,MAAK,UAAS,CAAC,EAExC,EAAI,YAAY,EAAU,cAAc,EACxC,EAAI,SAAS,QAAQ,kBAAmB,CACvC,WACA,KAAM,EAAG,SACT,GAAI,EAAG,QACP,aAAc,EAAG,WAAW,EAC7B,CAAgC,EAEzB,GAcD,SAAS,CAAY,CAC3B,EACA,EACA,EACU,CACV,IAAM,EAAK,EAAI,aAAa,EAAU,cAAc,EACpD,GAAI,CAAC,EAAI,MAAO,GAChB,OAAO,EAAkB,EAAK,EAAU,EAAI,CAAW,EAYjD,SAAS,CAAS,CACxB,EACA,EACA,EACU,CACV,IAAM,EAAK,EAAI,aAAa,EAAU,cAAc,EACpD,GAAI,CAAC,EAAI,MAAO,GAGhB,IAAM,EADS,EAAG,WAAW,OACA,EAAG,SAChC,GAAI,CAAC,GAAe,GAAI,MAAO,GAE/B,IAAM,EAAU,EAAc,GAAG,GACjC,GAAI,IAAY,OAAW,MAAO,GAElC,GAAI,OAAO,IAAY,SACtB,OAAO,EAAkB,EAAK,EAAU,EAAI,CAAO,EAGpD,GAAI,CAAC,EAAQ,MAAM,CAAE,MAAK,UAAS,CAAC,EAAG,MAAO,GAC9C,OAAO,EAAkB,EAAK,EAAU,EAAI,EAAQ,MAAM,EAUpD,SAAS,CAAoB,CACnC,EACA,EACqB,CAErB,OADW,EAAI,aAAa,EAAU,cAAc,GACzC,QAiBL,SAAS,CAA8F,CAAC,EAAoC,CAClJ,MAAO,CACN,mBAAoB,CACrB,EAwCM,SAAS,CAAsF,CACrG,EACC,CACD,IACC,cAAc,eACd,WAAW,EACX,QAAQ,UACL,GAAW,CAAC,EAEhB,OAAO,EAAa,cAAc,EAChC,mBAAkD,EAClD,eAA0C,EAC1C,WAAmC,EACnC,WAAc,EACd,QAAQ,CAAC,IAAU,CACnB,EACE,UAAU,sBAAsB,EAChC,YAAY,CAAQ,EACpB,QAAQ,CAAK,EACb,QAAQ,CAAW,EACnB,SAAS,WAAY,CACrB,KAAM,CAAC,cAAc,CACtB,CAAC,EACA,iBAAiB,WAAY,EAAG,SAAQ,SAAU,CAClD,IAAM,EAAK,EAAO,WAAW,aACvB,EAAS,EAAG,WAAW,OACvB,EAA2B,EACjC,EAAO,EAAG,UAAU,UAAU,CAAE,IAAK,EAAO,SAAU,EAAO,EAAG,CAAC,EACjE,EACA,WAAW,EAAG,UAAS,KAAI,SAAU,CAGrC,IAAM,EAAU,CAAE,IADe,EACH,SAAU,EAAG,GAAI,CAAE,EAEjD,QAAW,KAAU,EAAQ,SAAU,CACtC,IAAM,EAAK,EAAO,WAAW,aACvB,EAAS,EAAG,WAAW,OAE7B,EAAQ,SAAW,EAAO,GAC1B,EAAQ,GAAK,EAGb,EAAG,WAAa,EAGhB,EAAO,EAAG,UAAU,WAAW,CAAO,EAGtC,IAAM,EAAgB,EAAO,EAAG,SAChC,GAAI,GAAe,aAClB,QAAW,KAAc,EAAc,YACtC,GAAI,EAAW,MAAM,CAAO,EAAG,CAC9B,EAAkB,EAAQ,IAAK,EAAO,GAAI,EAAI,EAAW,MAAM,EAC/D,SAKJ,EACF",
8
+ "debugId": "D219C3158E67789D64756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -4,8 +4,7 @@
4
4
  * Provides ECS-native timers following the "data, not callbacks" philosophy.
5
5
  * Timers are components processed each frame, automatically cleaned up when entities are removed.
6
6
  */
7
- import { type Plugin, type BasePluginOptions } from 'ecspresso';
8
- import type { WorldConfigFrom, EmptyConfig } from '../type-utils';
7
+ import { type BasePluginOptions } from 'ecspresso';
9
8
  /**
10
9
  * Data structure passed to onComplete callbacks when a timer completes.
11
10
  *
@@ -148,4 +147,4 @@ export declare function createRepeatingTimer(duration: number, options?: TimerOp
148
147
  * });
149
148
  * ```
150
149
  */
151
- export declare function createTimerPlugin<G extends string = 'timers'>(options?: TimerPluginOptions<G>): Plugin<WorldConfigFrom<TimerComponentTypes>, EmptyConfig, 'timer-update', G>;
150
+ export declare function createTimerPlugin<G extends string = 'timers'>(options?: TimerPluginOptions<G>): import("ecspresso").Plugin<import("ecspresso").WithComponents<import("ecspresso").EmptyConfig, TimerComponentTypes>, import("ecspresso").EmptyConfig, "timer-update", G, never, never>;
@@ -1,4 +1,4 @@
1
- var J=((k)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(k,{get:(x,A)=>(typeof require<"u"?require:x)[A]}):k)(function(k){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+k+'" is not supported')});import{definePlugin as H}from"ecspresso";function M(k,x){return{timer:{elapsed:0,duration:k,repeat:!1,active:!0,justFinished:!1,onComplete:x?.onComplete}}}function N(k,x){return{timer:{elapsed:0,duration:k,repeat:!0,active:!0,justFinished:!1,onComplete:x?.onComplete}}}function Q(k){let{systemGroup:x="timers",priority:A=0,phase:B="preUpdate"}=k??{};return H({id:"timers",install(C){C.addSystem("timer-update").setPriority(A).inPhase(B).inGroup(x).addQuery("timers",{with:["timer"]}).setProcess(({queries:D,dt:E,ecs:F})=>{for(let z of D.timers){let{timer:j}=z.components;if(j.justFinished=!1,!j.active)continue;if(j.elapsed+=E,j.elapsed<j.duration)continue;if(j.repeat)while(j.elapsed>=j.duration)j.justFinished=!0,j.onComplete?.({entityId:z.id,duration:j.duration,elapsed:j.elapsed}),j.elapsed-=j.duration;else j.justFinished=!0,j.onComplete?.({entityId:z.id,duration:j.duration,elapsed:j.elapsed}),j.active=!1,F.commands.removeEntity(z.id)}})}})}export{Q as createTimerPlugin,M as createTimer,N as createRepeatingTimer};
1
+ var J=((k)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(k,{get:(x,A)=>(typeof require<"u"?require:x)[A]}):k)(function(k){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+k+'" is not supported')});import{definePlugin as H}from"ecspresso";function M(k,x){return{timer:{elapsed:0,duration:k,repeat:!1,active:!0,justFinished:!1,onComplete:x?.onComplete}}}function N(k,x){return{timer:{elapsed:0,duration:k,repeat:!0,active:!0,justFinished:!1,onComplete:x?.onComplete}}}function Q(k){let{systemGroup:x="timers",priority:A=0,phase:B="preUpdate"}=k??{};return H("timers").withComponentTypes().withLabels().withGroups().install((C)=>{C.addSystem("timer-update").setPriority(A).inPhase(B).inGroup(x).addQuery("timers",{with:["timer"]}).setProcess(({queries:D,dt:E,ecs:F})=>{for(let z of D.timers){let{timer:j}=z.components;if(j.justFinished=!1,!j.active)continue;if(j.elapsed+=E,j.elapsed<j.duration)continue;if(j.repeat)while(j.elapsed>=j.duration)j.justFinished=!0,j.onComplete?.({entityId:z.id,duration:j.duration,elapsed:j.elapsed}),j.elapsed-=j.duration;else j.justFinished=!0,j.onComplete?.({entityId:z.id,duration:j.duration,elapsed:j.elapsed}),j.active=!1,F.commands.removeEntity(z.id)}})})}export{Q as createTimerPlugin,M as createTimer,N as createRepeatingTimer};
2
2
 
3
- //# debugId=8D3E01379540788564756E2164756E21
3
+ //# debugId=690DFB44456C988D64756E2164756E21
4
4
  //# sourceMappingURL=timers.js.map
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/plugins/timers.ts"],
4
4
  "sourcesContent": [
5
- "/**\n * Timer Plugin for ECSpresso\n *\n * Provides ECS-native timers following the \"data, not callbacks\" philosophy.\n * Timers are components processed each frame, automatically cleaned up when entities are removed.\n */\n\nimport { definePlugin, type Plugin, type BasePluginOptions } from 'ecspresso';\nimport type { WorldConfigFrom, EmptyConfig } from '../type-utils';\n\n// ==================== Event Types ====================\n\n/**\n * Data structure passed to onComplete callbacks when a timer completes.\n *\n * @example\n * ```typescript\n * createTimer(1.5, {\n * onComplete: (data) => {\n * console.log(`Timer on entity ${data.entityId} finished after ${data.elapsed}s`);\n * }\n * });\n * ```\n */\nexport interface TimerEventData {\n\t/** The entity ID that the timer belongs to */\n\tentityId: number;\n\t/** The timer's configured duration in seconds */\n\tduration: number;\n\t/** The actual elapsed time (may exceed duration slightly) */\n\telapsed: number;\n}\n\n// ==================== Component Types ====================\n\n\n/**\n * Timer component data structure.\n * Use `justFinished` to detect timer completion in your systems.\n */\nexport interface Timer {\n\t/** Time accumulated so far (seconds) */\n\telapsed: number;\n\t/** Target duration (seconds) */\n\tduration: number;\n\t/** Whether timer repeats after completion */\n\trepeat: boolean;\n\t/** Whether timer is currently running */\n\tactive: boolean;\n\t/** True for one frame after timer completes */\n\tjustFinished: boolean;\n\t/** Optional callback invoked when timer completes */\n\tonComplete?: (data: TimerEventData) => void;\n}\n\n/**\n * Component types provided by the timer plugin.\n * Included automatically via `.withPlugin(createTimerPlugin())`.\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createTimerPlugin())\n * .withComponentTypes<{ velocity: { x: number; y: number }; player: true }>()\n * .build();\n * ```\n */\nexport interface TimerComponentTypes {\n\ttimer: Timer;\n}\n\n// ==================== Plugin Options ====================\n\n/**\n * Configuration options for the timer plugin.\n */\nexport interface TimerPluginOptions<G extends string = 'timers'> extends BasePluginOptions<G> {}\n\n// ==================== Helper Functions ====================\n\n/**\n * Options for timer creation\n */\nexport interface TimerOptions {\n\t/** Callback invoked when timer completes */\n\tonComplete?: (data: TimerEventData) => void;\n}\n\n/**\n * Create a one-shot timer that fires once after the specified duration.\n *\n * @param duration Duration in seconds until the timer completes\n * @param options Optional configuration including onComplete callback\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * // Timer without callback\n * ecs.spawn({\n * ...createTimer(2),\n * explosion: true,\n * });\n *\n * // Timer with onComplete callback\n * ecs.spawn({\n * ...createTimer(1.5, { onComplete: (data) => console.log('done', data.entityId) }),\n * });\n * ```\n */\nexport function createTimer(\n\tduration: number,\n\toptions?: TimerOptions\n): Pick<TimerComponentTypes, 'timer'> {\n\treturn {\n\t\ttimer: {\n\t\t\telapsed: 0,\n\t\t\tduration,\n\t\t\trepeat: false,\n\t\t\tactive: true,\n\t\t\tjustFinished: false,\n\t\t\tonComplete: options?.onComplete,\n\t\t},\n\t};\n}\n\n/**\n * Create a repeating timer that fires every `duration` seconds.\n *\n * @param duration Duration in seconds between each timer completion\n * @param options Optional configuration including onComplete callback\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * // Timer without callback\n * ecs.spawn({\n * ...createRepeatingTimer(5),\n * spawner: true,\n * });\n *\n * // Repeating timer with onComplete callback\n * ecs.spawn({\n * ...createRepeatingTimer(3, { onComplete: (data) => console.log('cycle', data.elapsed) }),\n * });\n * ```\n */\nexport function createRepeatingTimer(\n\tduration: number,\n\toptions?: TimerOptions\n): Pick<TimerComponentTypes, 'timer'> {\n\treturn {\n\t\ttimer: {\n\t\t\telapsed: 0,\n\t\t\tduration,\n\t\t\trepeat: true,\n\t\t\tactive: true,\n\t\t\tjustFinished: false,\n\t\t\tonComplete: options?.onComplete,\n\t\t},\n\t};\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a timer plugin for ECSpresso.\n *\n * This plugin provides:\n * - Timer update system that processes all timer components each frame\n * - `justFinished` flag pattern for one-frame completion detection\n * - Automatic cleanup when entities are removed\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso\n * .create<Components, Events, Resources>()\n * .withPlugin(createTimerPlugin())\n * .build();\n *\n * // Spawn entity with timer\n * ecs.spawn({\n * ...createRepeatingTimer(5),\n * spawner: true,\n * });\n *\n * // React to timer completion in a system\n * ecs.addSystem('spawn-on-timer')\n * .addQuery('spawners', { with: ['timer', 'spawner'] })\n * .setProcess((queries, _dt, ecs) => {\n * for (const { components } of queries.spawners) {\n * if (components.timer.justFinished) {\n * ecs.spawn({ enemy: true });\n * }\n * }\n * });\n * ```\n */\nexport function createTimerPlugin<G extends string = 'timers'>(\n\toptions?: TimerPluginOptions<G>\n): Plugin<WorldConfigFrom<TimerComponentTypes>, EmptyConfig, 'timer-update', G> {\n\tconst {\n\t\tsystemGroup = 'timers',\n\t\tpriority = 0,\n\t\tphase = 'preUpdate',\n\t} = options ?? {};\n\n\treturn definePlugin<WorldConfigFrom<TimerComponentTypes>, EmptyConfig, 'timer-update', G>({\n\t\tid: 'timers',\n\t\tinstall(world) {\n\t\t\tworld\n\t\t\t\t.addSystem('timer-update')\n\t\t\t\t.setPriority(priority)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('timers', {\n\t\t\t\t\twith: ['timer'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, dt, ecs }) => {\n\t\t\t\t\tfor (const entity of queries.timers) {\n\t\t\t\t\t\tconst { timer } = entity.components;\n\n\t\t\t\t\t\t// Reset justFinished flag from previous frame\n\t\t\t\t\t\ttimer.justFinished = false;\n\n\t\t\t\t\t\t// Skip inactive timers\n\t\t\t\t\t\tif (!timer.active) continue;\n\n\t\t\t\t\t\t// Accumulate time\n\t\t\t\t\t\ttimer.elapsed += dt;\n\n\t\t\t\t\t\t// Check if timer completed\n\t\t\t\t\t\tif (timer.elapsed < timer.duration) continue;\n\n\t\t\t\t\t\t// Timer completed - handle based on repeat mode\n\t\t\t\t\t\tif (timer.repeat) {\n\t\t\t\t\t\t\t// Handle multiple cycles in one frame\n\t\t\t\t\t\t\twhile (timer.elapsed >= timer.duration) {\n\t\t\t\t\t\t\t\ttimer.justFinished = true;\n\t\t\t\t\t\t\t\ttimer.onComplete?.({ entityId: entity.id, duration: timer.duration, elapsed: timer.elapsed });\n\t\t\t\t\t\t\t\ttimer.elapsed -= timer.duration;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// One-shot timer\n\t\t\t\t\t\t\ttimer.justFinished = true;\n\t\t\t\t\t\t\ttimer.onComplete?.({ entityId: entity.id, duration: timer.duration, elapsed: timer.elapsed });\n\t\t\t\t\t\t\ttimer.active = false;\n\t\t\t\t\t\t\t// Auto-remove one-shot timer entities after completion.\n\t\t\t\t\t\t\t// If configurability is needed in the future, add an autoRemove option to TimerOptions.\n\t\t\t\t\t\t\tecs.commands.removeEntity(entity.id);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t},\n\t});\n}\n"
5
+ "/**\n * Timer Plugin for ECSpresso\n *\n * Provides ECS-native timers following the \"data, not callbacks\" philosophy.\n * Timers are components processed each frame, automatically cleaned up when entities are removed.\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\n\n// ==================== Event Types ====================\n\n/**\n * Data structure passed to onComplete callbacks when a timer completes.\n *\n * @example\n * ```typescript\n * createTimer(1.5, {\n * onComplete: (data) => {\n * console.log(`Timer on entity ${data.entityId} finished after ${data.elapsed}s`);\n * }\n * });\n * ```\n */\nexport interface TimerEventData {\n\t/** The entity ID that the timer belongs to */\n\tentityId: number;\n\t/** The timer's configured duration in seconds */\n\tduration: number;\n\t/** The actual elapsed time (may exceed duration slightly) */\n\telapsed: number;\n}\n\n// ==================== Component Types ====================\n\n\n/**\n * Timer component data structure.\n * Use `justFinished` to detect timer completion in your systems.\n */\nexport interface Timer {\n\t/** Time accumulated so far (seconds) */\n\telapsed: number;\n\t/** Target duration (seconds) */\n\tduration: number;\n\t/** Whether timer repeats after completion */\n\trepeat: boolean;\n\t/** Whether timer is currently running */\n\tactive: boolean;\n\t/** True for one frame after timer completes */\n\tjustFinished: boolean;\n\t/** Optional callback invoked when timer completes */\n\tonComplete?: (data: TimerEventData) => void;\n}\n\n/**\n * Component types provided by the timer plugin.\n * Included automatically via `.withPlugin(createTimerPlugin())`.\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createTimerPlugin())\n * .withComponentTypes<{ velocity: { x: number; y: number }; player: true }>()\n * .build();\n * ```\n */\nexport interface TimerComponentTypes {\n\ttimer: Timer;\n}\n\n// ==================== Plugin Options ====================\n\n/**\n * Configuration options for the timer plugin.\n */\nexport interface TimerPluginOptions<G extends string = 'timers'> extends BasePluginOptions<G> {}\n\n// ==================== Helper Functions ====================\n\n/**\n * Options for timer creation\n */\nexport interface TimerOptions {\n\t/** Callback invoked when timer completes */\n\tonComplete?: (data: TimerEventData) => void;\n}\n\n/**\n * Create a one-shot timer that fires once after the specified duration.\n *\n * @param duration Duration in seconds until the timer completes\n * @param options Optional configuration including onComplete callback\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * // Timer without callback\n * ecs.spawn({\n * ...createTimer(2),\n * explosion: true,\n * });\n *\n * // Timer with onComplete callback\n * ecs.spawn({\n * ...createTimer(1.5, { onComplete: (data) => console.log('done', data.entityId) }),\n * });\n * ```\n */\nexport function createTimer(\n\tduration: number,\n\toptions?: TimerOptions\n): Pick<TimerComponentTypes, 'timer'> {\n\treturn {\n\t\ttimer: {\n\t\t\telapsed: 0,\n\t\t\tduration,\n\t\t\trepeat: false,\n\t\t\tactive: true,\n\t\t\tjustFinished: false,\n\t\t\tonComplete: options?.onComplete,\n\t\t},\n\t};\n}\n\n/**\n * Create a repeating timer that fires every `duration` seconds.\n *\n * @param duration Duration in seconds between each timer completion\n * @param options Optional configuration including onComplete callback\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * // Timer without callback\n * ecs.spawn({\n * ...createRepeatingTimer(5),\n * spawner: true,\n * });\n *\n * // Repeating timer with onComplete callback\n * ecs.spawn({\n * ...createRepeatingTimer(3, { onComplete: (data) => console.log('cycle', data.elapsed) }),\n * });\n * ```\n */\nexport function createRepeatingTimer(\n\tduration: number,\n\toptions?: TimerOptions\n): Pick<TimerComponentTypes, 'timer'> {\n\treturn {\n\t\ttimer: {\n\t\t\telapsed: 0,\n\t\t\tduration,\n\t\t\trepeat: true,\n\t\t\tactive: true,\n\t\t\tjustFinished: false,\n\t\t\tonComplete: options?.onComplete,\n\t\t},\n\t};\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a timer plugin for ECSpresso.\n *\n * This plugin provides:\n * - Timer update system that processes all timer components each frame\n * - `justFinished` flag pattern for one-frame completion detection\n * - Automatic cleanup when entities are removed\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso\n * .create<Components, Events, Resources>()\n * .withPlugin(createTimerPlugin())\n * .build();\n *\n * // Spawn entity with timer\n * ecs.spawn({\n * ...createRepeatingTimer(5),\n * spawner: true,\n * });\n *\n * // React to timer completion in a system\n * ecs.addSystem('spawn-on-timer')\n * .addQuery('spawners', { with: ['timer', 'spawner'] })\n * .setProcess((queries, _dt, ecs) => {\n * for (const { components } of queries.spawners) {\n * if (components.timer.justFinished) {\n * ecs.spawn({ enemy: true });\n * }\n * }\n * });\n * ```\n */\nexport function createTimerPlugin<G extends string = 'timers'>(\n\toptions?: TimerPluginOptions<G>\n) {\n\tconst {\n\t\tsystemGroup = 'timers',\n\t\tpriority = 0,\n\t\tphase = 'preUpdate',\n\t} = options ?? {};\n\n\treturn definePlugin('timers')\n\t\t.withComponentTypes<TimerComponentTypes>()\n\t\t.withLabels<'timer-update'>()\n\t\t.withGroups<G>()\n\t\t.install((world) => {\n\t\t\tworld\n\t\t\t\t.addSystem('timer-update')\n\t\t\t\t.setPriority(priority)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('timers', {\n\t\t\t\t\twith: ['timer'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, dt, ecs }) => {\n\t\t\t\t\tfor (const entity of queries.timers) {\n\t\t\t\t\t\tconst { timer } = entity.components;\n\n\t\t\t\t\t\t// Reset justFinished flag from previous frame\n\t\t\t\t\t\ttimer.justFinished = false;\n\n\t\t\t\t\t\t// Skip inactive timers\n\t\t\t\t\t\tif (!timer.active) continue;\n\n\t\t\t\t\t\t// Accumulate time\n\t\t\t\t\t\ttimer.elapsed += dt;\n\n\t\t\t\t\t\t// Check if timer completed\n\t\t\t\t\t\tif (timer.elapsed < timer.duration) continue;\n\n\t\t\t\t\t\t// Timer completed - handle based on repeat mode\n\t\t\t\t\t\tif (timer.repeat) {\n\t\t\t\t\t\t\t// Handle multiple cycles in one frame\n\t\t\t\t\t\t\twhile (timer.elapsed >= timer.duration) {\n\t\t\t\t\t\t\t\ttimer.justFinished = true;\n\t\t\t\t\t\t\t\ttimer.onComplete?.({ entityId: entity.id, duration: timer.duration, elapsed: timer.elapsed });\n\t\t\t\t\t\t\t\ttimer.elapsed -= timer.duration;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// One-shot timer\n\t\t\t\t\t\t\ttimer.justFinished = true;\n\t\t\t\t\t\t\ttimer.onComplete?.({ entityId: entity.id, duration: timer.duration, elapsed: timer.elapsed });\n\t\t\t\t\t\t\ttimer.active = false;\n\t\t\t\t\t\t\t// Auto-remove one-shot timer entities after completion.\n\t\t\t\t\t\t\t// If configurability is needed in the future, add an autoRemove option to TimerOptions.\n\t\t\t\t\t\t\tecs.commands.removeEntity(entity.id);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n}\n"
6
6
  ],
7
- "mappings": "2PAOA,uBAAS,kBAsGF,SAAS,CAAW,CAC1B,EACA,EACqC,CACrC,MAAO,CACN,MAAO,CACN,QAAS,EACT,WACA,OAAQ,GACR,OAAQ,GACR,aAAc,GACd,WAAY,GAAS,UACtB,CACD,EAwBM,SAAS,CAAoB,CACnC,EACA,EACqC,CACrC,MAAO,CACN,MAAO,CACN,QAAS,EACT,WACA,OAAQ,GACR,OAAQ,GACR,aAAc,GACd,WAAY,GAAS,UACtB,CACD,EAsCM,SAAS,CAA8C,CAC7D,EAC+E,CAC/E,IACC,cAAc,SACd,WAAW,EACX,QAAQ,aACL,GAAW,CAAC,EAEhB,OAAO,EAAmF,CACzF,GAAI,SACJ,OAAO,CAAC,EAAO,CACd,EACE,UAAU,cAAc,EACxB,YAAY,CAAQ,EACpB,QAAQ,CAAK,EACb,QAAQ,CAAW,EACnB,SAAS,SAAU,CACnB,KAAM,CAAC,OAAO,CACf,CAAC,EACA,WAAW,EAAG,UAAS,KAAI,SAAU,CACrC,QAAW,KAAU,EAAQ,OAAQ,CACpC,IAAQ,SAAU,EAAO,WAMzB,GAHA,EAAM,aAAe,GAGjB,CAAC,EAAM,OAAQ,SAMnB,GAHA,EAAM,SAAW,EAGb,EAAM,QAAU,EAAM,SAAU,SAGpC,GAAI,EAAM,OAET,MAAO,EAAM,SAAW,EAAM,SAC7B,EAAM,aAAe,GACrB,EAAM,aAAa,CAAE,SAAU,EAAO,GAAI,SAAU,EAAM,SAAU,QAAS,EAAM,OAAQ,CAAC,EAC5F,EAAM,SAAW,EAAM,SAIxB,OAAM,aAAe,GACrB,EAAM,aAAa,CAAE,SAAU,EAAO,GAAI,SAAU,EAAM,SAAU,QAAS,EAAM,OAAQ,CAAC,EAC5F,EAAM,OAAS,GAGf,EAAI,SAAS,aAAa,EAAO,EAAE,GAGrC,EAEJ,CAAC",
8
- "debugId": "8D3E01379540788564756E2164756E21",
7
+ "mappings": "2PAOA,uBAAS,kBAqGF,SAAS,CAAW,CAC1B,EACA,EACqC,CACrC,MAAO,CACN,MAAO,CACN,QAAS,EACT,WACA,OAAQ,GACR,OAAQ,GACR,aAAc,GACd,WAAY,GAAS,UACtB,CACD,EAwBM,SAAS,CAAoB,CACnC,EACA,EACqC,CACrC,MAAO,CACN,MAAO,CACN,QAAS,EACT,WACA,OAAQ,GACR,OAAQ,GACR,aAAc,GACd,WAAY,GAAS,UACtB,CACD,EAsCM,SAAS,CAA8C,CAC7D,EACC,CACD,IACC,cAAc,SACd,WAAW,EACX,QAAQ,aACL,GAAW,CAAC,EAEhB,OAAO,EAAa,QAAQ,EAC1B,mBAAwC,EACxC,WAA2B,EAC3B,WAAc,EACd,QAAQ,CAAC,IAAU,CACnB,EACE,UAAU,cAAc,EACxB,YAAY,CAAQ,EACpB,QAAQ,CAAK,EACb,QAAQ,CAAW,EACnB,SAAS,SAAU,CACnB,KAAM,CAAC,OAAO,CACf,CAAC,EACA,WAAW,EAAG,UAAS,KAAI,SAAU,CACrC,QAAW,KAAU,EAAQ,OAAQ,CACpC,IAAQ,SAAU,EAAO,WAMzB,GAHA,EAAM,aAAe,GAGjB,CAAC,EAAM,OAAQ,SAMnB,GAHA,EAAM,SAAW,EAGb,EAAM,QAAU,EAAM,SAAU,SAGpC,GAAI,EAAM,OAET,MAAO,EAAM,SAAW,EAAM,SAC7B,EAAM,aAAe,GACrB,EAAM,aAAa,CAAE,SAAU,EAAO,GAAI,SAAU,EAAM,SAAU,QAAS,EAAM,OAAQ,CAAC,EAC5F,EAAM,SAAW,EAAM,SAIxB,OAAM,aAAe,GACrB,EAAM,aAAa,CAAE,SAAU,EAAO,GAAI,SAAU,EAAM,SAAU,QAAS,EAAM,OAAQ,CAAC,EAC5F,EAAM,OAAS,GAGf,EAAI,SAAS,aAAa,EAAO,EAAE,GAGrC,EACF",
8
+ "debugId": "690DFB44456C988D64756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -6,8 +6,8 @@
6
6
  *
7
7
  * @see https://docs.rs/bevy/latest/bevy/transform/components/struct.GlobalTransform.html
8
8
  */
9
- import { type Plugin, type BasePluginOptions } from 'ecspresso';
10
- import type { WorldConfigFrom, EmptyConfig } from '../type-utils';
9
+ import { type BasePluginOptions } from 'ecspresso';
10
+ import type { WorldConfigFrom } from '../type-utils';
11
11
  /**
12
12
  * Local transform relative to parent (or world if no parent).
13
13
  * This is the transform you modify directly.
@@ -147,4 +147,4 @@ export declare function createTransform(x: number, y: number, options?: Transfor
147
147
  * });
148
148
  * ```
149
149
  */
150
- export declare function createTransformPlugin<G extends string = 'transform'>(options?: TransformPluginOptions<G>): Plugin<WorldConfigFrom<TransformComponentTypes>, EmptyConfig, 'transform-propagation', G>;
150
+ export declare function createTransformPlugin<G extends string = 'transform'>(options?: TransformPluginOptions<G>): import("ecspresso").Plugin<import("ecspresso").WithComponents<import("ecspresso").EmptyConfig, TransformComponentTypes>, import("ecspresso").EmptyConfig, "transform-propagation", G, never, never>;
@@ -1,4 +1,4 @@
1
- var N=((j)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(j,{get:(k,q)=>(typeof require<"u"?require:k)[q]}):j)(function(j){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+j+'" is not supported')});import{definePlugin as J}from"ecspresso";var S={x:0,y:0,rotation:0,scaleX:1,scaleY:1},U={x:0,y:0,rotation:0,scaleX:1,scaleY:1};function V(j,k){return{localTransform:{x:j,y:k,rotation:0,scaleX:1,scaleY:1}}}function Z(j,k){return{worldTransform:{x:j,y:k,rotation:0,scaleX:1,scaleY:1}}}function _(j,k,q){let z=q?.scale??q?.scaleX??1,D=q?.scale??q?.scaleY??1,C=q?.rotation??0,v={x:j,y:k,rotation:C,scaleX:z,scaleY:D};return{localTransform:{...v},worldTransform:{...v}}}function $(j){let{systemGroup:k="transform",priority:q=500,phase:z="postUpdate"}=j??{};return J({id:"transform",install(D){D.registerRequired("localTransform","worldTransform",(v)=>({x:v.x,y:v.y,rotation:v.rotation,scaleX:v.scaleX,scaleY:v.scaleY}));let C=[];D.addSystem("transform-propagation").setPriority(q).inPhase(z).inGroup(k).setProcess(({ecs:v})=>{K(v,C)})}})}function K(j,k){let q=j.entityManager;j.forEachInHierarchy((z,D)=>{let C=q.getComponent(z,"localTransform"),v=q.getComponent(z,"worldTransform");if(!C||!v)return;if(D===null)F(C,v);else{let E=q.getComponent(D,"worldTransform");if(E)M(E,C,v);else F(C,v)}j.markChanged(z,"worldTransform")}),q.getEntitiesWithQueryInto(k,["localTransform","worldTransform"]);for(let z of k)if(j.getParent(z.id)===null&&j.getChildren(z.id).length===0){let{localTransform:C,worldTransform:v}=z.components;F(C,v),j.markChanged(z.id,"worldTransform")}}function F(j,k){k.x=j.x,k.y=j.y,k.rotation=j.rotation,k.scaleX=j.scaleX,k.scaleY=j.scaleY}function M(j,k,q){let z=k.x*j.scaleX,D=k.y*j.scaleY,C=Math.cos(j.rotation),v=Math.sin(j.rotation),E=z*C-D*v,H=z*v+D*C;q.x=j.x+E,q.y=j.y+H,q.rotation=j.rotation+k.rotation,q.scaleX=j.scaleX*k.scaleX,q.scaleY=j.scaleY*k.scaleY}export{Z as createWorldTransform,$ as createTransformPlugin,_ as createTransform,V as createLocalTransform,U as DEFAULT_WORLD_TRANSFORM,S as DEFAULT_LOCAL_TRANSFORM};
1
+ var N=((j)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(j,{get:(k,q)=>(typeof require<"u"?require:k)[q]}):j)(function(j){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+j+'" is not supported')});import{definePlugin as J}from"ecspresso";var S={x:0,y:0,rotation:0,scaleX:1,scaleY:1},U={x:0,y:0,rotation:0,scaleX:1,scaleY:1};function V(j,k){return{localTransform:{x:j,y:k,rotation:0,scaleX:1,scaleY:1}}}function Z(j,k){return{worldTransform:{x:j,y:k,rotation:0,scaleX:1,scaleY:1}}}function _(j,k,q){let z=q?.scale??q?.scaleX??1,D=q?.scale??q?.scaleY??1,C=q?.rotation??0,v={x:j,y:k,rotation:C,scaleX:z,scaleY:D};return{localTransform:{...v},worldTransform:{...v}}}function $(j){let{systemGroup:k="transform",priority:q=500,phase:z="postUpdate"}=j??{};return J("transform").withComponentTypes().withLabels().withGroups().install((D)=>{D.registerRequired("localTransform","worldTransform",(v)=>({x:v.x,y:v.y,rotation:v.rotation,scaleX:v.scaleX,scaleY:v.scaleY}));let C=[];D.addSystem("transform-propagation").setPriority(q).inPhase(z).inGroup(k).setProcess(({ecs:v})=>{K(v,C)})})}function K(j,k){let q=j.entityManager;j.forEachInHierarchy((z,D)=>{let C=q.getComponent(z,"localTransform"),v=q.getComponent(z,"worldTransform");if(!C||!v)return;if(D===null)F(C,v);else{let E=q.getComponent(D,"worldTransform");if(E)M(E,C,v);else F(C,v)}j.markChanged(z,"worldTransform")}),q.getEntitiesWithQueryInto(k,["localTransform","worldTransform"]);for(let z of k)if(j.getParent(z.id)===null&&j.getChildren(z.id).length===0){let{localTransform:C,worldTransform:v}=z.components;F(C,v),j.markChanged(z.id,"worldTransform")}}function F(j,k){k.x=j.x,k.y=j.y,k.rotation=j.rotation,k.scaleX=j.scaleX,k.scaleY=j.scaleY}function M(j,k,q){let z=k.x*j.scaleX,D=k.y*j.scaleY,C=Math.cos(j.rotation),v=Math.sin(j.rotation),E=z*C-D*v,H=z*v+D*C;q.x=j.x+E,q.y=j.y+H,q.rotation=j.rotation+k.rotation,q.scaleX=j.scaleX*k.scaleX,q.scaleY=j.scaleY*k.scaleY}export{Z as createWorldTransform,$ as createTransformPlugin,_ as createTransform,V as createLocalTransform,U as DEFAULT_WORLD_TRANSFORM,S as DEFAULT_LOCAL_TRANSFORM};
2
2
 
3
- //# debugId=41A87FD60C5375A764756E2164756E21
3
+ //# debugId=7C2E80F6F657F59964756E2164756E21
4
4
  //# sourceMappingURL=transform.js.map