ecspresso 0.13.0 → 0.13.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/index.js +2 -2
  2. package/dist/index.js.map +1 -1
  3. package/dist/plugins/ai/detection.d.ts +118 -0
  4. package/dist/plugins/ai/detection.js +4 -0
  5. package/dist/plugins/ai/detection.js.map +10 -0
  6. package/dist/plugins/{audio.js → audio/audio.js} +1 -1
  7. package/dist/plugins/{audio.js.map → audio/audio.js.map} +2 -2
  8. package/dist/plugins/combat/health.d.ts +98 -0
  9. package/dist/plugins/combat/health.js +4 -0
  10. package/dist/plugins/combat/health.js.map +10 -0
  11. package/dist/plugins/combat/projectile.d.ts +115 -0
  12. package/dist/plugins/combat/projectile.js +4 -0
  13. package/dist/plugins/combat/projectile.js.map +10 -0
  14. package/dist/plugins/{diagnostics.js → debug/diagnostics.js} +1 -1
  15. package/dist/plugins/{diagnostics.js.map → debug/diagnostics.js.map} +2 -2
  16. package/dist/plugins/{input.js → input/input.js} +1 -1
  17. package/dist/plugins/{input.js.map → input/input.js.map} +2 -2
  18. package/dist/plugins/input/selection.d.ts +114 -0
  19. package/dist/plugins/input/selection.js +4 -0
  20. package/dist/plugins/input/selection.js.map +11 -0
  21. package/dist/plugins/isometric/depth-sort.d.ts +44 -0
  22. package/dist/plugins/isometric/depth-sort.js +4 -0
  23. package/dist/plugins/isometric/depth-sort.js.map +10 -0
  24. package/dist/plugins/isometric/projection.d.ts +102 -0
  25. package/dist/plugins/isometric/projection.js +4 -0
  26. package/dist/plugins/isometric/projection.js.map +10 -0
  27. package/dist/plugins/{collision.d.ts → physics/collision.d.ts} +1 -1
  28. package/dist/plugins/{collision.js → physics/collision.js} +1 -1
  29. package/dist/plugins/{collision.js.map → physics/collision.js.map} +3 -3
  30. package/dist/plugins/{physics2D.d.ts → physics/physics2D.d.ts} +1 -1
  31. package/dist/plugins/{physics2D.js → physics/physics2D.js} +1 -1
  32. package/dist/plugins/{physics2D.js.map → physics/physics2D.js.map} +3 -3
  33. package/dist/plugins/physics/steering.d.ts +102 -0
  34. package/dist/plugins/physics/steering.js +4 -0
  35. package/dist/plugins/physics/steering.js.map +10 -0
  36. package/dist/plugins/{particles.d.ts → rendering/particles.d.ts} +2 -2
  37. package/dist/plugins/{particles.js → rendering/particles.js} +1 -1
  38. package/dist/plugins/rendering/particles.js.map +10 -0
  39. package/dist/plugins/{renderers → rendering}/renderer2D.d.ts +9 -5
  40. package/dist/plugins/rendering/renderer2D.js +4 -0
  41. package/dist/plugins/rendering/renderer2D.js.map +10 -0
  42. package/dist/plugins/{sprite-animation.js → rendering/sprite-animation.js} +1 -1
  43. package/dist/plugins/{sprite-animation.js.map → rendering/sprite-animation.js.map} +2 -2
  44. package/dist/plugins/{coroutine.js → scripting/coroutine.js} +1 -1
  45. package/dist/plugins/{coroutine.js.map → scripting/coroutine.js.map} +2 -2
  46. package/dist/plugins/{state-machine.js → scripting/state-machine.js} +1 -1
  47. package/dist/plugins/{state-machine.js.map → scripting/state-machine.js.map} +2 -2
  48. package/dist/plugins/{timers.js → scripting/timers.js} +1 -1
  49. package/dist/plugins/{timers.js.map → scripting/timers.js.map} +2 -2
  50. package/dist/plugins/{tween.d.ts → scripting/tween.d.ts} +1 -1
  51. package/dist/plugins/{tween.js → scripting/tween.js} +1 -1
  52. package/dist/plugins/scripting/tween.js.map +11 -0
  53. package/dist/plugins/{bounds.js → spatial/bounds.js} +1 -1
  54. package/dist/plugins/{bounds.js.map → spatial/bounds.js.map} +2 -2
  55. package/dist/plugins/{camera.d.ts → spatial/camera.d.ts} +52 -12
  56. package/dist/plugins/spatial/camera.js +4 -0
  57. package/dist/plugins/spatial/camera.js.map +10 -0
  58. package/dist/plugins/{spatial-index.d.ts → spatial/spatial-index.d.ts} +2 -2
  59. package/dist/plugins/{spatial-index.js → spatial/spatial-index.js} +1 -1
  60. package/dist/plugins/{spatial-index.js.map → spatial/spatial-index.js.map} +3 -3
  61. package/dist/plugins/{transform.d.ts → spatial/transform.d.ts} +1 -1
  62. package/dist/plugins/{transform.js → spatial/transform.js} +1 -1
  63. package/dist/plugins/spatial/transform.js.map +10 -0
  64. package/package.json +78 -50
  65. package/dist/plugins/camera.js +0 -4
  66. package/dist/plugins/camera.js.map +0 -10
  67. package/dist/plugins/particles.js.map +0 -10
  68. package/dist/plugins/renderers/renderer2D.js +0 -4
  69. package/dist/plugins/renderers/renderer2D.js.map +0 -10
  70. package/dist/plugins/transform.js.map +0 -10
  71. package/dist/plugins/tween.js.map +0 -11
  72. /package/dist/plugins/{audio.d.ts → audio/audio.d.ts} +0 -0
  73. /package/dist/plugins/{diagnostics.d.ts → debug/diagnostics.d.ts} +0 -0
  74. /package/dist/plugins/{input.d.ts → input/input.d.ts} +0 -0
  75. /package/dist/plugins/{sprite-animation.d.ts → rendering/sprite-animation.d.ts} +0 -0
  76. /package/dist/plugins/{coroutine.d.ts → scripting/coroutine.d.ts} +0 -0
  77. /package/dist/plugins/{state-machine.d.ts → scripting/state-machine.d.ts} +0 -0
  78. /package/dist/plugins/{timers.d.ts → scripting/timers.d.ts} +0 -0
  79. /package/dist/plugins/{bounds.d.ts → spatial/bounds.d.ts} +0 -0
@@ -1,4 +1,4 @@
1
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=D1E817D3F2D57D1F64756E2164756E21
3
+ //# debugId=9EF329142BBA6E6864756E2164756E21
4
4
  //# sourceMappingURL=sprite-animation.js.map
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/plugins/sprite-animation.ts"],
3
+ "sources": ["../src/plugins/rendering/sprite-animation.ts"],
4
4
  "sourcesContent": [
5
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
7
  "mappings": "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",
8
- "debugId": "D1E817D3F2D57D1F64756E2164756E21",
8
+ "debugId": "9EF329142BBA6E6864756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -1,4 +1,4 @@
1
1
  var V=((j)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(j,{get:(z,x)=>(typeof require<"u"?require:z)[x]}):j)(function(j){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+j+'" is not supported')});import{definePlugin as R}from"ecspresso";function S(j,z){return{coroutine:{generator:j,onComplete:z?.onComplete}}}function*Y(j){if(j<=0)return;let z=0;while(z<j){let x=yield;z+=x}}function*Z(j){for(let z=0;z<j;z++)yield}function*_(j){while(!j())yield}function*$(...j){if(j.length===0)return;let z=j.map((x)=>{return x.next(0),{gen:x,done:!1}});while(z.some((x)=>!x.done)){let x=yield;for(let D of z){if(D.done)continue;if(D.gen.next(x).done)D.done=!0}}}function*A(...j){if(j.length===0)return;let z=j.map((x)=>{return x.next(0),{gen:x,done:!1}});try{while(!0){let x=yield;for(let D of z){if(D.done)continue;if(D.gen.next(x).done){D.done=!0;for(let K of z)if(!K.done)K.gen.return(),K.done=!0;return}}}}finally{for(let x of z)if(!x.done)x.gen.return(),x.done=!0}}function*U(j,z,x){let D=!1,H=j.subscribe(z,(K)=>{if(!x||x(K))D=!0});try{while(!D)yield}finally{H()}}function C(j,z){let x=j.getComponent(z,"coroutine");if(!x)return!1;return x.generator.return(),j.commands.removeComponent(z,"coroutine"),!0}function E(j){return{createCoroutine:S,waitForEvent:U}}function F(j){let{systemGroup:z="coroutines",priority:x=0,phase:D="update"}=j??{},H=new Set;return R("coroutines").withComponentTypes().withLabels().withGroups().install((K)=>{K.registerDispose("coroutine",({value:L,entityId:M})=>{L.generator.return(),H.delete(M)}),K.addSystem("coroutine-update").setPriority(x).inPhase(D).inGroup(z).addQuery("coroutines",{with:["coroutine"]}).setOnEntityEnter("coroutines",({entity:L})=>{L.components.coroutine.generator.next(0)}).setProcess(({queries:L,dt:M,ecs:N})=>{for(let J of L.coroutines){if(H.has(J.id)){H.delete(J.id);continue}let O=J.components.coroutine;try{if(O.generator.next(M).done)H.add(J.id),O.onComplete?.({entityId:J.id}),N.commands.removeComponent(J.id,"coroutine")}catch(Q){console.warn(`Coroutine error on entity ${J.id}:`,Q),H.add(J.id),N.commands.removeComponent(J.id,"coroutine")}}})})}export{_ as waitUntil,Y as waitSeconds,Z as waitFrames,U as waitForEvent,A as race,$ as parallel,F as createCoroutinePlugin,E as createCoroutineHelpers,S as createCoroutine,C as cancelCoroutine};
2
2
 
3
- //# debugId=0C9EF6AEE9641CFF64756E2164756E21
3
+ //# debugId=DF25F3CC4BAA535D64756E2164756E21
4
4
  //# sourceMappingURL=coroutine.js.map
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/plugins/coroutine.ts"],
3
+ "sources": ["../src/plugins/scripting/coroutine.ts"],
4
4
  "sourcesContent": [
5
5
  "/**\n * Coroutine Plugin for ECSpresso\n *\n * ES6 generator-based coroutines for multi-step, frame-spanning scripted sequences.\n * A `coroutine` component holds a live generator. A system ticks all generators each\n * frame via `.next(dt)`. Helper generators (`waitSeconds`, `waitFrames`, `waitUntil`,\n * `waitForEvent`, `parallel`, `race`) compose via `yield*`.\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type { EventsOfWorld, AnyECSpresso } from 'ecspresso';\n\n// ==================== Generator Protocol ====================\n\n/**\n * Yields void, returns void, receives deltaTime (number) via `.next(dt)`.\n * First `.next(dt)` initializes the generator (runs to first yield, dt discarded per JS spec).\n * Subsequent `.next(dt)` resume from yield with dt as the yield expression value.\n */\nexport type CoroutineGenerator = Generator<void, void, number>;\n\n// ==================== Event Types ====================\n\nexport interface CoroutineEventData {\n\tentityId: number;\n}\n\n\n// ==================== Component Types ====================\n\nexport interface CoroutineState {\n\tgenerator: CoroutineGenerator;\n\tonComplete?: (data: CoroutineEventData) => void;\n}\n\nexport interface CoroutineComponentTypes {\n\tcoroutine: CoroutineState;\n}\n\n// ==================== Plugin Options ====================\n\nexport interface CoroutinePluginOptions<G extends string = 'coroutines'> extends BasePluginOptions<G> {}\n\n// ==================== Component Factory ====================\n\nexport interface CoroutineOptions {\n\tonComplete?: (data: CoroutineEventData) => void;\n}\n\n/**\n * Create a coroutine component for spawning or adding to an entity.\n *\n * @param generator - The generator function to drive\n * @param options - Optional configuration (onComplete event)\n * @returns Component object suitable for spreading into spawn()\n */\nexport function createCoroutine(\n\tgenerator: CoroutineGenerator,\n\toptions?: CoroutineOptions,\n): Pick<CoroutineComponentTypes, 'coroutine'> {\n\treturn {\n\t\tcoroutine: {\n\t\t\tgenerator,\n\t\t\tonComplete: options?.onComplete,\n\t\t},\n\t};\n}\n\n// ==================== Helper Generators (standalone) ====================\n\n/**\n * Wait for a specified number of seconds. Accumulates dt until elapsed >= seconds.\n * If seconds <= 0, returns immediately.\n */\nexport function* waitSeconds(seconds: number): CoroutineGenerator {\n\tif (seconds <= 0) return;\n\tlet elapsed = 0;\n\twhile (elapsed < seconds) {\n\t\tconst dt: number = yield;\n\t\telapsed += dt;\n\t}\n}\n\n/**\n * Wait for a specified number of frames. Yields `frames` times.\n * If frames <= 0, returns immediately.\n */\nexport function* waitFrames(frames: number): CoroutineGenerator {\n\tfor (let i = 0; i < frames; i++) {\n\t\tyield;\n\t}\n}\n\n/**\n * Wait until a predicate returns true. Yields each frame until predicate is satisfied.\n * User closes over ecs if needed for state checks.\n */\nexport function* waitUntil(predicate: () => boolean): CoroutineGenerator {\n\twhile (!predicate()) {\n\t\tyield;\n\t}\n}\n\n/**\n * Run multiple coroutines in parallel. Completes when all finish.\n * Initializes all sub-generators, ticks all each frame.\n * Empty array = immediate return.\n */\nexport function* parallel(...coroutines: CoroutineGenerator[]): CoroutineGenerator {\n\tif (coroutines.length === 0) return;\n\n\t// Initialize all generators\n\tconst active = coroutines.map(gen => {\n\t\tgen.next(0);\n\t\treturn { gen, done: false };\n\t});\n\n\twhile (active.some(entry => !entry.done)) {\n\t\tconst dt: number = yield;\n\t\tfor (const entry of active) {\n\t\t\tif (entry.done) continue;\n\t\t\tconst result = entry.gen.next(dt);\n\t\t\tif (result.done) {\n\t\t\t\tentry.done = true;\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Run multiple coroutines, completing when the first one finishes.\n * Calls `.return()` on remaining generators (triggers finally blocks).\n * Empty array = immediate return.\n */\nexport function* race(...coroutines: CoroutineGenerator[]): CoroutineGenerator {\n\tif (coroutines.length === 0) return;\n\n\t// Initialize all generators\n\tconst entries = coroutines.map(gen => {\n\t\tgen.next(0);\n\t\treturn { gen, done: false };\n\t});\n\n\ttry {\n\t\twhile (true) {\n\t\t\tconst dt: number = yield;\n\t\t\tfor (const entry of entries) {\n\t\t\t\tif (entry.done) continue;\n\t\t\t\tconst result = entry.gen.next(dt);\n\t\t\t\tif (result.done) {\n\t\t\t\t\tentry.done = true;\n\t\t\t\t\t// Cancel all others\n\t\t\t\t\tfor (const other of entries) {\n\t\t\t\t\t\tif (!other.done) {\n\t\t\t\t\t\t\tother.gen.return();\n\t\t\t\t\t\t\tother.done = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} finally {\n\t\t// Clean up all on external cancellation\n\t\tfor (const entry of entries) {\n\t\t\tif (!entry.done) {\n\t\t\t\tentry.gen.return();\n\t\t\t\tentry.done = true;\n\t\t\t}\n\t\t}\n\t}\n}\n\n// ==================== Helper Generator (ECS-dependent) ====================\n\n/**\n * Wait until a matching event fires on the event bus.\n * Subscribes via eventBus.subscribe, yields until event received, unsubscribes in finally block.\n *\n * @param eventBus - Object with subscribe method (typically ecs.eventBus)\n * @param eventType - Event type name to listen for\n * @param filter - Optional predicate to filter events\n */\nexport function* waitForEvent<ET extends Record<string, any>, E extends keyof ET & string>(\n\teventBus: { subscribe(type: E, cb: (data: ET[E]) => void): () => void },\n\teventType: E,\n\tfilter?: (data: ET[E]) => boolean,\n): CoroutineGenerator {\n\tlet received = false;\n\tconst unsubscribe = eventBus.subscribe(eventType, (data: ET[E]) => {\n\t\tif (!filter || filter(data)) {\n\t\t\treceived = true;\n\t\t}\n\t});\n\ttry {\n\t\twhile (!received) {\n\t\t\tyield;\n\t\t}\n\t} finally {\n\t\tunsubscribe();\n\t}\n}\n\n// ==================== Cancellation ====================\n\n/**\n * Structural interface for ECS methods used by cancelCoroutine.\n */\nexport interface CoroutineWorld {\n\tgetComponent(entityId: number, componentName: string): unknown | undefined;\n\tcommands: {\n\t\tremoveComponent(entityId: number, componentName: string): void;\n\t};\n}\n\n/**\n * Cancel a running coroutine on an entity. Calls generator.return() (triggers finally blocks)\n * and queues component removal.\n *\n * @returns true if the entity had a coroutine that was cancelled, false otherwise\n */\nexport function cancelCoroutine(ecs: CoroutineWorld, entityId: number): boolean {\n\tconst state = ecs.getComponent(entityId, 'coroutine') as CoroutineState | undefined;\n\tif (!state) return false;\n\tstate.generator.return();\n\tecs.commands.removeComponent(entityId, 'coroutine');\n\treturn true;\n}\n\n// ==================== Typed Helpers (replaces Kit Pattern) ====================\n\n/**\n * Type-safe coroutine helpers that validate event names against a world's event types.\n * Use `createCoroutineHelpers<typeof ecs>()` to get compile-time validation.\n */\nexport interface CoroutineHelpers<W extends AnyECSpresso> {\n\tcreateCoroutine: (\n\t\tgenerator: CoroutineGenerator,\n\t\toptions?: { onComplete?: (data: CoroutineEventData) => void },\n\t) => Pick<CoroutineComponentTypes, 'coroutine'>;\n\twaitForEvent: <E extends keyof EventsOfWorld<W> & string>(\n\t\teventBus: { subscribe(type: E, cb: (data: EventsOfWorld<W>[E]) => void): () => void },\n\t\teventType: E,\n\t\tfilter?: (data: EventsOfWorld<W>[E]) => boolean,\n\t) => CoroutineGenerator;\n}\n\n/**\n * Create typed coroutine helpers that validate event names at compile time.\n *\n * @example\n * ```typescript\n * const { createCoroutine, waitForEvent } = createCoroutineHelpers<typeof ecs>();\n * ecs.spawn({ ...createCoroutine(myGen(), { onComplete: (data) => console.log(data.entityId) }) });\n * ```\n */\nexport function createCoroutineHelpers<W extends AnyECSpresso>(_world?: W): CoroutineHelpers<W> {\n\treturn {\n\t\tcreateCoroutine: createCoroutine as CoroutineHelpers<W>['createCoroutine'],\n\t\twaitForEvent: waitForEvent as CoroutineHelpers<W>['waitForEvent'],\n\t};\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a coroutine plugin for ECSpresso.\n *\n * This plugin provides:\n * - Coroutine system that ticks all generator-based coroutines each frame\n * - Automatic cleanup via dispose callback (triggers generator finally blocks)\n * - `onComplete` callback invocation\n * - Component removal on completion\n */\nexport function createCoroutinePlugin<G extends string = 'coroutines'>(\n\toptions?: CoroutinePluginOptions<G>,\n) {\n\tconst {\n\t\tsystemGroup = 'coroutines',\n\t\tpriority = 0,\n\t\tphase = 'update',\n\t} = options ?? {};\n\n\t// Tracks entities whose coroutine completed this frame to prevent re-ticking\n\t// before the command buffer removes the component.\n\tconst finished = new Set<number>();\n\n\treturn definePlugin('coroutines')\n\t\t.withComponentTypes<CoroutineComponentTypes>()\n\t\t.withLabels<'coroutine-update'>()\n\t\t.withGroups<G>()\n\t\t.install((world) => {\n\t\t\tworld.registerDispose('coroutine', ({ value, entityId }) => {\n\t\t\t\tvalue.generator.return();\n\t\t\t\tfinished.delete(entityId);\n\t\t\t});\n\n\t\t\tworld\n\t\t\t\t.addSystem('coroutine-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('coroutines', {\n\t\t\t\t\twith: ['coroutine'],\n\t\t\t\t})\n\t\t\t\t.setOnEntityEnter('coroutines', ({ entity }) => {\n\t\t\t\t\tentity.components.coroutine.generator.next(0);\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, dt, ecs }) => {\n\t\t\t\t\tfor (const entity of queries.coroutines) {\n\t\t\t\t\t\t// Already completed — skip until command buffer removes the component\n\t\t\t\t\t\tif (finished.has(entity.id)) {\n\t\t\t\t\t\t\tfinished.delete(entity.id);\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst state = entity.components.coroutine;\n\n\t\t\t\t\t\t// Tick the generator\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst result = state.generator.next(dt);\n\t\t\t\t\t\t\tif (result.done) {\n\t\t\t\t\t\t\t\tfinished.add(entity.id);\n\t\t\t\t\t\t\t\tstate.onComplete?.({ entityId: entity.id });\n\t\t\t\t\t\t\t\tecs.commands.removeComponent(entity.id, 'coroutine');\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\tconsole.warn(`Coroutine error on entity ${entity.id}:`, error);\n\t\t\t\t\t\t\tfinished.add(entity.id);\n\t\t\t\t\t\t\tecs.commands.removeComponent(entity.id, 'coroutine');\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
7
  "mappings": "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",
8
- "debugId": "0C9EF6AEE9641CFF64756E2164756E21",
8
+ "debugId": "DF25F3CC4BAA535D64756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -1,4 +1,4 @@
1
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=D219C3158E67789D64756E2164756E21
3
+ //# debugId=6ADF9D5235D78F5264756E2164756E21
4
4
  //# sourceMappingURL=state-machine.js.map
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/plugins/state-machine.ts"],
3
+ "sources": ["../src/plugins/scripting/state-machine.ts"],
4
4
  "sourcesContent": [
5
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
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",
8
+ "debugId": "6ADF9D5235D78F5264756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -1,4 +1,4 @@
1
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=690DFB44456C988D64756E2164756E21
3
+ //# debugId=7B8408B3E517C6C664756E2164756E21
4
4
  //# sourceMappingURL=timers.js.map
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/plugins/timers.ts"],
3
+ "sources": ["../src/plugins/scripting/timers.ts"],
4
4
  "sourcesContent": [
5
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
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",
8
+ "debugId": "7B8408B3E517C6C664756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import { type BasePluginOptions } from 'ecspresso';
8
8
  import type { ComponentsOfWorld, AnyECSpresso } from 'ecspresso';
9
- import { type EasingFn } from '../utils/easing';
9
+ import { type EasingFn } from '../../utils/easing';
10
10
  /**
11
11
  * Data structure published when a tween completes.
12
12
  * Use this type when defining tween completion events in your EventTypes interface.
@@ -1,4 +1,4 @@
1
1
  var B=((z)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(z,{get:(H,D)=>(typeof require<"u"?require:H)[D]}):z)(function(z){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+z+'" is not supported')});import{definePlugin as F}from"ecspresso";function $(z){return z}var V=1.70158,v=V*1.525,S=V+1;var f=2*Math.PI/3,g=2*Math.PI/4.5;function Q(z,H,D,J,M){let{from:N,easing:U=$,loop:W="once",loops:Y=1,onComplete:X}=M??{};return{tween:{steps:[{targets:[{component:z,path:H.split("."),from:N??null,to:D}],duration:J,easing:U}],currentStep:0,elapsed:0,loop:W,totalLoops:Y,completedLoops:0,direction:1,state:"pending",onComplete:X,justFinished:!1}}}function b(z,H){let{loop:D="once",loops:J=1,onComplete:M}=H??{};return{tween:{steps:z.map((N)=>({targets:N.targets.map((U)=>({component:U.component,path:U.field.split("."),from:U.from??null,to:U.to})),duration:N.duration,easing:N.easing??$})),currentStep:0,elapsed:0,loop:D,totalLoops:J,completedLoops:0,direction:1,state:"pending",onComplete:M,justFinished:!1}}}function d(z){return{createTween:Q,createTweenSequence:b}}var G={parent:{},key:""};function L(z,H){let D=H.length-1,J=z;for(let N=0;N<D;N++){let U=H[N];if(U===void 0)return null;let W=J[U];if(W===null||W===void 0||typeof W!=="object")return null;J=W}let M=H[D];if(M===void 0)return null;if(!(M in J))return null;return G.parent=J,G.key=M,G}function R(z,H){let D=L(z,H);if(!D)return null;let J=D.parent[D.key];return typeof J==="number"?J:null}function j(z,H,D){let J=L(z,H);if(!J)return!1;return J.parent[J.key]=D,!0}function h(z,H,D){return z<H?H:z>D?D:z}function E(z,H){for(let D of z.steps)for(let J of D.targets){if(J.from!==null)continue;let M=H[J.component];if(!M||typeof M!=="object")continue;let N=R(M,J.path);if(N!==null)J.from=N;else J.from=0}}function K(z,H,D,J,M){let N=z.easing(H);for(let U of z.targets){let W=D[U.component];if(!W||typeof W!=="object")continue;let Y=U.from??0,X=Y+(U.to-Y)*N;if(j(W,U.path,X))M.markChanged(J,U.component)}}function q(z,H,D,J){for(let M of z.targets){let N=H[M.component];if(!N||typeof N!=="object")continue;if(j(N,M.path,M.to))J.markChanged(D,M.component)}}function T(z){for(let H of z.steps)for(let D of H.targets){let J=D.from??0;D.from=D.to,D.to=J}}function P(z,H,D){z.state="complete",z.justFinished=!0,z.onComplete?.({entityId:H,stepCount:z.steps.length}),D.commands.removeComponent(H,"tween")}function A(z,H,D){if(z.completedLoops++,z.loop==="once")return P(z,H,D),!1;if(z.totalLoops>0&&z.completedLoops>=z.totalLoops)return P(z,H,D),!1;if(z.loop==="yoyo")z.direction=z.direction===1?-1:1,T(z);return z.currentStep=0,z.elapsed>0}function k(z,H,D,J){let M=z.currentStep+1;if(M<z.steps.length){z.currentStep=M;let N=z.steps[M];if(N)for(let U of N.targets){if(U.from!==null)continue;let W=H[U.component];if(!W||typeof W!=="object")continue;let Y=R(W,U.path);U.from=Y??0}return!0}return A(z,D,J)}function x(z,H,D,J){while(!0){let M=z.steps[z.currentStep];if(!M)return;if(M.duration<=0){if(q(M,H,D,J),z.elapsed=0,!k(z,H,D,J))return;continue}if(z.elapsed>=M.duration){q(M,H,D,J);let U=z.elapsed-M.duration;if(z.elapsed=U,!k(z,H,D,J))return;continue}let N=h(z.elapsed/M.duration,0,1);K(M,N,H,D,J);return}}function m(z){let{systemGroup:H="tweens",priority:D=0,phase:J="update"}=z??{};return F("tweens").withComponentTypes().withLabels().withGroups().install((M)=>{M.addSystem("tween-update").setPriority(D).inPhase(J).inGroup(H).addQuery("tweens",{with:["tween"]}).setProcess(({queries:N,dt:U,ecs:W})=>{for(let Y of N.tweens){let X=Y.components.tween,Z=Y.components;if(X.justFinished){X.justFinished=!1;continue}if(X.state==="complete")continue;if(X.state==="pending")E(X,Z),X.state="active";if(!X.steps[X.currentStep])continue;X.elapsed+=U,x(X,Z,Y.id,W)}})})}export{b as createTweenSequence,m as createTweenPlugin,d as createTweenHelpers,Q as createTween};
2
2
 
3
- //# debugId=5CFFC226CB1D9DD164756E2164756E21
3
+ //# debugId=52B5806729E5074564756E2164756E21
4
4
  //# sourceMappingURL=tween.js.map
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/plugins/scripting/tween.ts", "../src/utils/easing.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * Tween Plugin for ECSpresso\n *\n * Declarative property animation within the ECS. Tween any numeric component\n * field over time with standard easing functions, sequences, and completion events.\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type { ComponentsOfWorld, AnyECSpresso } from 'ecspresso';\nimport { linear, type EasingFn } from '../../utils/easing';\n\n// ==================== Event Types ====================\n\n/**\n * Data structure published when a tween completes.\n * Use this type when defining tween completion events in your EventTypes interface.\n */\nexport interface TweenEventData {\n\t/** The entity ID the tween belongs to */\n\tentityId: number;\n\t/** Number of steps in the tween */\n\tstepCount: number;\n}\n\n\n// ==================== Component Types ====================\n\nexport interface TweenTarget {\n\t/** Component name on the entity */\n\tcomponent: string;\n\t/** Pre-split field path (e.g., ['position', 'x']) */\n\tpath: readonly string[];\n\t/** Starting value. null = resolve from current value on first tick */\n\tfrom: number | null;\n\t/** Target value */\n\tto: number;\n}\n\nexport interface TweenStep {\n\ttargets: TweenTarget[];\n\tduration: number;\n\teasing: EasingFn;\n}\n\nexport interface Tween {\n\tsteps: TweenStep[];\n\tcurrentStep: number;\n\telapsed: number;\n\tloop: LoopMode;\n\ttotalLoops: number;\n\tcompletedLoops: number;\n\tdirection: 1 | -1;\n\tstate: 'pending' | 'active' | 'complete';\n\tonComplete?: (data: TweenEventData) => void;\n\tjustFinished: boolean;\n}\n\nexport type LoopMode = 'once' | 'loop' | 'yoyo';\n\n/**\n * Component types provided by the tween plugin.\n */\nexport interface TweenComponentTypes {\n\ttween: Tween;\n}\n\n// ==================== Plugin Options ====================\n\nexport interface TweenPluginOptions<G extends string = 'tweens'> extends BasePluginOptions<G> {}\n\n// ==================== Helper Functions ====================\n\nexport interface TweenOptions {\n\t/** Explicit starting value (default: captures current value on first tick) */\n\tfrom?: number;\n\t/** Easing function (default: linear) */\n\teasing?: EasingFn;\n\t/** Loop mode (default: 'once') */\n\tloop?: LoopMode;\n\t/** Number of loops. -1 = infinite (default: 1) */\n\tloops?: number;\n\t/** Callback invoked when tween completes */\n\tonComplete?: (data: TweenEventData) => void;\n}\n\n/**\n * Create a single-target tween component.\n *\n * @param component Component name on the entity\n * @param field Field path (dot-separated for nested, e.g. 'position.x')\n * @param to Target value\n * @param duration Duration in seconds\n * @param options Optional configuration\n * @returns Component object suitable for spreading into spawn()\n */\nexport function createTween(\n\tcomponent: string,\n\tfield: string,\n\tto: number,\n\tduration: number,\n\toptions?: TweenOptions,\n): Pick<TweenComponentTypes, 'tween'> {\n\tconst {\n\t\tfrom,\n\t\teasing = linear,\n\t\tloop = 'once',\n\t\tloops = 1,\n\t\tonComplete,\n\t} = options ?? {};\n\n\treturn {\n\t\ttween: {\n\t\t\tsteps: [{\n\t\t\t\ttargets: [{\n\t\t\t\t\tcomponent,\n\t\t\t\t\tpath: field.split('.'),\n\t\t\t\t\tfrom: from ?? null,\n\t\t\t\t\tto,\n\t\t\t\t}],\n\t\t\t\tduration,\n\t\t\t\teasing,\n\t\t\t}],\n\t\t\tcurrentStep: 0,\n\t\t\telapsed: 0,\n\t\t\tloop,\n\t\t\ttotalLoops: loops,\n\t\t\tcompletedLoops: 0,\n\t\t\tdirection: 1,\n\t\t\tstate: 'pending',\n\t\t\tonComplete,\n\t\t\tjustFinished: false,\n\t\t},\n\t};\n}\n\nexport interface TweenSequenceStepInput {\n\ttargets: ReadonlyArray<{\n\t\tcomponent: string;\n\t\tfield: string;\n\t\tto: number;\n\t\tfrom?: number;\n\t}>;\n\tduration: number;\n\teasing?: EasingFn;\n}\n\nexport interface TweenSequenceOptions {\n\t/** Loop mode (default: 'once') */\n\tloop?: LoopMode;\n\t/** Number of loops. -1 = infinite (default: 1) */\n\tloops?: number;\n\t/** Callback invoked when tween completes */\n\tonComplete?: (data: TweenEventData) => void;\n}\n\n/**\n * Create a multi-step tween sequence. Each step can have parallel targets.\n *\n * @param steps Array of step definitions\n * @param options Optional configuration\n * @returns Component object suitable for spreading into spawn()\n */\nexport function createTweenSequence(\n\tsteps: ReadonlyArray<TweenSequenceStepInput>,\n\toptions?: TweenSequenceOptions,\n): Pick<TweenComponentTypes, 'tween'> {\n\tconst {\n\t\tloop = 'once',\n\t\tloops = 1,\n\t\tonComplete,\n\t} = options ?? {};\n\n\treturn {\n\t\ttween: {\n\t\t\tsteps: steps.map((step) => ({\n\t\t\t\ttargets: step.targets.map((target) => ({\n\t\t\t\t\tcomponent: target.component,\n\t\t\t\t\tpath: target.field.split('.'),\n\t\t\t\t\tfrom: target.from ?? null,\n\t\t\t\t\tto: target.to,\n\t\t\t\t})),\n\t\t\t\tduration: step.duration,\n\t\t\t\teasing: step.easing ?? linear,\n\t\t\t})),\n\t\t\tcurrentStep: 0,\n\t\t\telapsed: 0,\n\t\t\tloop,\n\t\t\ttotalLoops: loops,\n\t\t\tcompletedLoops: 0,\n\t\t\tdirection: 1,\n\t\t\tstate: 'pending',\n\t\t\tonComplete,\n\t\t\tjustFinished: false,\n\t\t},\n\t};\n}\n\n// ==================== Kit Types ====================\n\n/**\n * Recursively produce a union of dot-separated paths that resolve to `number`\n * within type T. Depth-limited to 4 levels to prevent TS recursion errors.\n *\n * @example\n * NumericPaths<{ x: number; y: number }> // 'x' | 'y'\n * NumericPaths<{ position: { x: number }; rotation: number }> // 'position.x' | 'rotation'\n */\nexport type NumericPaths<T, Depth extends readonly unknown[] = []> =\n\tDepth['length'] extends 4 ? never :\n\tT extends readonly unknown[] ? never :\n\tT extends Record<string, unknown>\n\t\t? { [K in keyof T & string]:\n\t\t\tNonNullable<T[K]> extends number\n\t\t\t\t? K\n\t\t\t\t: NonNullable<T[K]> extends readonly unknown[]\n\t\t\t\t\t? never\n\t\t\t\t\t: NonNullable<T[K]> extends Record<string, unknown>\n\t\t\t\t\t\t? `${K}.${NumericPaths<NonNullable<T[K]>, [...Depth, unknown]>}`\n\t\t\t\t\t\t: never\n\t\t}[keyof T & string]\n\t\t: never;\n\n/**\n * Discriminated union over component names: each variant constrains `field`\n * to the numeric paths of that component. TS narrows inline object literals\n * by `component` discriminant — zero runtime overhead.\n */\nexport type TypedTweenTargetInput<C extends Record<string, any>> = {\n\t[K in keyof C & string]: {\n\t\tcomponent: K;\n\t\tfield: NumericPaths<C[K]>;\n\t\tto: number;\n\t\tfrom?: number;\n\t}\n}[keyof C & string];\n\nexport interface TypedTweenSequenceStepInput<C extends Record<string, any>> {\n\ttargets: ReadonlyArray<TypedTweenTargetInput<C>>;\n\tduration: number;\n\teasing?: EasingFn;\n}\n\nexport interface TweenHelpers<W extends AnyECSpresso> {\n\tcreateTween: <K extends keyof ComponentsOfWorld<W> & string>(\n\t\tcomponent: K,\n\t\tfield: NumericPaths<ComponentsOfWorld<W>[K]>,\n\t\tto: number,\n\t\tduration: number,\n\t\toptions?: {\n\t\t\tfrom?: number;\n\t\t\teasing?: EasingFn;\n\t\t\tloop?: LoopMode;\n\t\t\tloops?: number;\n\t\t\tonComplete?: (data: TweenEventData) => void;\n\t\t},\n\t) => Pick<TweenComponentTypes, 'tween'>;\n\tcreateTweenSequence: (\n\t\tsteps: ReadonlyArray<TypedTweenSequenceStepInput<ComponentsOfWorld<W>>>,\n\t\toptions?: {\n\t\t\tloop?: LoopMode;\n\t\t\tloops?: number;\n\t\t\tonComplete?: (data: TweenEventData) => void;\n\t\t},\n\t) => Pick<TweenComponentTypes, 'tween'>;\n}\n\nexport function createTweenHelpers<W extends AnyECSpresso>(_world?: W): TweenHelpers<W> {\n\treturn {\n\t\tcreateTween: createTween as TweenHelpers<W>['createTween'],\n\t\tcreateTweenSequence: createTweenSequence as TweenHelpers<W>['createTweenSequence'],\n\t};\n}\n\n// ==================== Field Path Resolution ====================\n\n/**\n * Module-scoped mutable result to avoid per-call allocation in hot path.\n */\nconst _fieldRef: { parent: Record<string, unknown>; key: string } = { parent: {} as Record<string, unknown>, key: '' };\n\n/**\n * Traverse an object by path segments. Returns the parent object and final key\n * for read/write, or null if any segment is missing.\n */\nfunction resolveField(obj: Record<string, unknown>, path: readonly string[]): typeof _fieldRef | null {\n\tconst lastIdx = path.length - 1;\n\tlet current: Record<string, unknown> = obj;\n\n\tfor (let i = 0; i < lastIdx; i++) {\n\t\tconst segment = path[i];\n\t\tif (segment === undefined) return null;\n\t\tconst next = current[segment];\n\t\tif (next === null || next === undefined || typeof next !== 'object') return null;\n\t\tcurrent = next as Record<string, unknown>;\n\t}\n\n\tconst finalKey = path[lastIdx];\n\tif (finalKey === undefined) return null;\n\tif (!(finalKey in current)) return null;\n\n\t_fieldRef.parent = current;\n\t_fieldRef.key = finalKey;\n\treturn _fieldRef;\n}\n\nfunction readField(obj: Record<string, unknown>, path: readonly string[]): number | null {\n\tconst ref = resolveField(obj, path);\n\tif (!ref) return null;\n\tconst val = ref.parent[ref.key];\n\treturn typeof val === 'number' ? val : null;\n}\n\nfunction writeField(obj: Record<string, unknown>, path: readonly string[], value: number): boolean {\n\tconst ref = resolveField(obj, path);\n\tif (!ref) return false;\n\tref.parent[ref.key] = value;\n\treturn true;\n}\n\n// ==================== System Logic ====================\n\nfunction clamp(value: number, min: number, max: number): number {\n\treturn value < min ? min : value > max ? max : value;\n}\n\n/**\n * Resolve all null `from` values by reading current component field values.\n */\nfunction resolveFromValues(\n\ttween: Tween,\n\tentityComponents: Record<string, unknown>,\n): void {\n\tfor (const step of tween.steps) {\n\t\tfor (const target of step.targets) {\n\t\t\tif (target.from !== null) continue;\n\t\t\tconst comp = entityComponents[target.component];\n\t\t\tif (!comp || typeof comp !== 'object') continue;\n\t\t\tconst val = readField(comp as Record<string, unknown>, target.path);\n\t\t\tif (val !== null) {\n\t\t\t\ttarget.from = val;\n\t\t\t} else {\n\t\t\t\ttarget.from = 0;\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Apply interpolation for a step's targets at a given progress.\n */\nfunction applyStep(\n\tstep: TweenStep,\n\tprogress: number,\n\tentityComponents: Record<string, unknown>,\n\tentityId: number,\n\tecs: { markChanged: (entityId: number, componentName: any) => void },\n): void {\n\tconst easedT = step.easing(progress);\n\n\tfor (const target of step.targets) {\n\t\tconst comp = entityComponents[target.component];\n\t\tif (!comp || typeof comp !== 'object') continue;\n\t\tconst from = target.from ?? 0;\n\t\tconst value = from + (target.to - from) * easedT;\n\t\tconst written = writeField(comp as Record<string, unknown>, target.path, value);\n\t\tif (written) {\n\t\t\tecs.markChanged(entityId, target.component);\n\t\t}\n\t}\n}\n\n/**\n * Snap all targets in a step to their final values (from or to depending on direction).\n */\nfunction snapStepToEnd(\n\tstep: TweenStep,\n\tentityComponents: Record<string, unknown>,\n\tentityId: number,\n\tecs: { markChanged: (entityId: number, componentName: any) => void },\n): void {\n\tfor (const target of step.targets) {\n\t\tconst comp = entityComponents[target.component];\n\t\tif (!comp || typeof comp !== 'object') continue;\n\t\tconst written = writeField(comp as Record<string, unknown>, target.path, target.to);\n\t\tif (written) {\n\t\t\tecs.markChanged(entityId, target.component);\n\t\t}\n\t}\n}\n\n/**\n * Reverse all from/to values in every step (for yoyo).\n */\nfunction reverseAllTargets(tween: Tween): void {\n\tfor (const step of tween.steps) {\n\t\tfor (const target of step.targets) {\n\t\t\tconst tmp = target.from ?? 0;\n\t\t\ttarget.from = target.to;\n\t\t\ttarget.to = tmp;\n\t\t}\n\t}\n}\n\n// ==================== Tween Processing Helpers ====================\n\ntype TweenEcs = { markChanged: (entityId: number, componentName: any) => void; commands: { removeComponent: (entityId: number, componentName: any) => void } };\n\nfunction completeTween(\n\ttween: Tween,\n\tentityId: number,\n\tecs: { commands: { removeComponent: (entityId: number, componentName: any) => void } },\n): void {\n\ttween.state = 'complete';\n\ttween.justFinished = true;\n\n\ttween.onComplete?.({ entityId, stepCount: tween.steps.length });\n\tecs.commands.removeComponent(entityId, 'tween');\n}\n\nfunction handleTweenEnd(\n\ttween: Tween,\n\tentityId: number,\n\tecs: TweenEcs,\n): boolean {\n\ttween.completedLoops++;\n\n\tif (tween.loop === 'once') {\n\t\tcompleteTween(tween, entityId, ecs);\n\t\treturn false;\n\t}\n\n\t// Check if finite loops exhausted\n\tif (tween.totalLoops > 0 && tween.completedLoops >= tween.totalLoops) {\n\t\tcompleteTween(tween, entityId, ecs);\n\t\treturn false;\n\t}\n\n\t// Loop continues\n\tif (tween.loop === 'yoyo') {\n\t\ttween.direction = tween.direction === 1 ? -1 : 1;\n\t\treverseAllTargets(tween);\n\t}\n\n\ttween.currentStep = 0;\n\n\t// For 'loop' mode, from values stay as-is so the animation replays identically.\n\t// For 'yoyo' mode, reverseAllTargets already swapped from/to.\n\n\treturn tween.elapsed > 0;\n}\n\n/**\n * Advance to next step. Returns true if there's more work to process,\n * false if the tween has completed or looped.\n */\nfunction advanceStep(\n\ttween: Tween,\n\tentityComponents: Record<string, unknown>,\n\tentityId: number,\n\tecs: TweenEcs,\n): boolean {\n\tconst nextStep = tween.currentStep + 1;\n\n\tif (nextStep < tween.steps.length) {\n\t\t// More steps — resolve from values for next step and continue\n\t\ttween.currentStep = nextStep;\n\t\tconst step = tween.steps[nextStep];\n\t\tif (step) {\n\t\t\tfor (const target of step.targets) {\n\t\t\t\tif (target.from !== null) continue;\n\t\t\t\tconst comp = entityComponents[target.component];\n\t\t\t\tif (!comp || typeof comp !== 'object') continue;\n\t\t\t\tconst val = readField(comp as Record<string, unknown>, target.path);\n\t\t\t\ttarget.from = val ?? 0;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\t// All steps done — handle loop/complete\n\treturn handleTweenEnd(tween, entityId, ecs);\n}\n\nfunction processTweenProgress(\n\ttween: Tween,\n\tentityComponents: Record<string, unknown>,\n\tentityId: number,\n\tecs: TweenEcs,\n): void {\n\t// eslint-disable-next-line no-constant-condition\n\twhile (true) {\n\t\tconst currentStep = tween.steps[tween.currentStep];\n\t\tif (!currentStep) return;\n\n\t\t// Zero-duration steps complete immediately\n\t\tif (currentStep.duration <= 0) {\n\t\t\tsnapStepToEnd(currentStep, entityComponents, entityId, ecs);\n\t\t\ttween.elapsed = 0;\n\n\t\t\tif (!advanceStep(tween, entityComponents, entityId, ecs)) return;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (tween.elapsed >= currentStep.duration) {\n\t\t\t// Step complete — snap to end and carry overflow\n\t\t\tsnapStepToEnd(currentStep, entityComponents, entityId, ecs);\n\t\t\tconst overflow = tween.elapsed - currentStep.duration;\n\t\t\ttween.elapsed = overflow;\n\n\t\t\tif (!advanceStep(tween, entityComponents, entityId, ecs)) return;\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Step in progress — interpolate\n\t\tconst progress = clamp(tween.elapsed / currentStep.duration, 0, 1);\n\t\tapplyStep(currentStep, progress, entityComponents, entityId, ecs);\n\t\treturn;\n\t}\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a tween plugin for ECSpresso.\n *\n * This plugin provides:\n * - Tween system that processes all tween components each frame\n * - Support for single-field, multi-target, and multi-step sequences\n * - 31 standard easing functions\n * - Loop modes: once, loop, yoyo\n * - `justFinished` flag for one-frame completion detection\n * - `onComplete` callback on completion\n * - Change detection via markChanged\n */\nexport function createTweenPlugin<G extends string = 'tweens'>(\n\toptions?: TweenPluginOptions<G>\n) {\n\tconst {\n\t\tsystemGroup = 'tweens',\n\t\tpriority = 0,\n\t\tphase = 'update',\n\t} = options ?? {};\n\n\treturn definePlugin('tweens')\n\t\t.withComponentTypes<TweenComponentTypes>()\n\t\t.withLabels<'tween-update'>()\n\t\t.withGroups<G>()\n\t\t.install((world) => {\n\t\t\tworld\n\t\t\t\t.addSystem('tween-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('tweens', {\n\t\t\t\t\twith: ['tween'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, dt, ecs }) => {\n\t\t\t\t\tfor (const entity of queries.tweens) {\n\t\t\t\t\t\tconst tween = entity.components.tween as Tween;\n\t\t\t\t\t\tconst entityComponents = entity.components as Record<string, unknown>;\n\n\t\t\t\t\t\t// Reset justFinished flag from previous frame\n\t\t\t\t\t\tif (tween.justFinished) {\n\t\t\t\t\t\t\ttween.justFinished = false;\n\t\t\t\t\t\t\t// Component removal was queued, skip processing\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Skip completed tweens\n\t\t\t\t\t\tif (tween.state === 'complete') continue;\n\n\t\t\t\t\t\t// Resolve pending state: capture null from values\n\t\t\t\t\t\tif (tween.state === 'pending') {\n\t\t\t\t\t\t\tresolveFromValues(tween, entityComponents);\n\t\t\t\t\t\t\ttween.state = 'active';\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Process active tween\n\t\t\t\t\t\tconst currentStep = tween.steps[tween.currentStep];\n\t\t\t\t\t\tif (!currentStep) continue;\n\n\t\t\t\t\t\ttween.elapsed += dt;\n\n\t\t\t\t\t\tprocessTweenProgress(tween, entityComponents, entity.id, ecs);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n}\n",
6
+ "/**\n * Easing Functions\n *\n * 31 standard easing functions for animation. Pure math, no dependencies.\n */\n\nexport type EasingFn = (t: number) => number;\n\nexport function linear(t: number): number {\n\treturn t;\n}\n\n// Quad\nexport function easeInQuad(t: number): number {\n\treturn t * t;\n}\n\nexport function easeOutQuad(t: number): number {\n\treturn t * (2 - t);\n}\n\nexport function easeInOutQuad(t: number): number {\n\treturn t < 0.5\n\t\t? 2 * t * t\n\t\t: -1 + (4 - 2 * t) * t;\n}\n\n// Cubic\nexport function easeInCubic(t: number): number {\n\treturn t * t * t;\n}\n\nexport function easeOutCubic(t: number): number {\n\tconst t1 = t - 1;\n\treturn t1 * t1 * t1 + 1;\n}\n\nexport function easeInOutCubic(t: number): number {\n\treturn t < 0.5\n\t\t? 4 * t * t * t\n\t\t: 1 + (t - 1) * (2 * t - 2) * (2 * t - 2);\n}\n\n// Quart\nexport function easeInQuart(t: number): number {\n\treturn t * t * t * t;\n}\n\nexport function easeOutQuart(t: number): number {\n\tconst t1 = t - 1;\n\treturn 1 - t1 * t1 * t1 * t1;\n}\n\nexport function easeInOutQuart(t: number): number {\n\treturn t < 0.5\n\t\t? 8 * t * t * t * t\n\t\t: 1 - 8 * (t - 1) * (t - 1) * (t - 1) * (t - 1);\n}\n\n// Quint\nexport function easeInQuint(t: number): number {\n\treturn t * t * t * t * t;\n}\n\nexport function easeOutQuint(t: number): number {\n\tconst t1 = t - 1;\n\treturn 1 + t1 * t1 * t1 * t1 * t1;\n}\n\nexport function easeInOutQuint(t: number): number {\n\treturn t < 0.5\n\t\t? 16 * t * t * t * t * t\n\t\t: 1 + 16 * (t - 1) * (t - 1) * (t - 1) * (t - 1) * (t - 1);\n}\n\n// Sine\nexport function easeInSine(t: number): number {\n\treturn 1 - Math.cos((t * Math.PI) / 2);\n}\n\nexport function easeOutSine(t: number): number {\n\treturn Math.sin((t * Math.PI) / 2);\n}\n\nexport function easeInOutSine(t: number): number {\n\treturn -(Math.cos(Math.PI * t) - 1) / 2;\n}\n\n// Expo\nexport function easeInExpo(t: number): number {\n\treturn t === 0 ? 0 : Math.pow(2, 10 * (t - 1));\n}\n\nexport function easeOutExpo(t: number): number {\n\treturn t === 1 ? 1 : 1 - Math.pow(2, -10 * t);\n}\n\nexport function easeInOutExpo(t: number): number {\n\tif (t === 0) return 0;\n\tif (t === 1) return 1;\n\treturn t < 0.5\n\t\t? Math.pow(2, 20 * t - 10) / 2\n\t\t: (2 - Math.pow(2, -20 * t + 10)) / 2;\n}\n\n// Circ\nexport function easeInCirc(t: number): number {\n\treturn 1 - Math.sqrt(1 - t * t);\n}\n\nexport function easeOutCirc(t: number): number {\n\tconst t1 = t - 1;\n\treturn Math.sqrt(1 - t1 * t1);\n}\n\nexport function easeInOutCirc(t: number): number {\n\treturn t < 0.5\n\t\t? (1 - Math.sqrt(1 - 4 * t * t)) / 2\n\t\t: (Math.sqrt(1 - (-2 * t + 2) * (-2 * t + 2)) + 1) / 2;\n}\n\n// Back\nconst BACK_C1 = 1.70158;\nconst BACK_C2 = BACK_C1 * 1.525;\nconst BACK_C3 = BACK_C1 + 1;\n\nexport function easeInBack(t: number): number {\n\treturn BACK_C3 * t * t * t - BACK_C1 * t * t;\n}\n\nexport function easeOutBack(t: number): number {\n\tconst t1 = t - 1;\n\treturn 1 + BACK_C3 * t1 * t1 * t1 + BACK_C1 * t1 * t1;\n}\n\nexport function easeInOutBack(t: number): number {\n\treturn t < 0.5\n\t\t? ((2 * t) * (2 * t) * ((BACK_C2 + 1) * 2 * t - BACK_C2)) / 2\n\t\t: ((2 * t - 2) * (2 * t - 2) * ((BACK_C2 + 1) * (t * 2 - 2) + BACK_C2) + 2) / 2;\n}\n\n// Elastic\nconst ELASTIC_C4 = (2 * Math.PI) / 3;\nconst ELASTIC_C5 = (2 * Math.PI) / 4.5;\n\nexport function easeInElastic(t: number): number {\n\tif (t === 0) return 0;\n\tif (t === 1) return 1;\n\treturn -Math.pow(2, 10 * t - 10) * Math.sin((10 * t - 10.75) * ELASTIC_C4);\n}\n\nexport function easeOutElastic(t: number): number {\n\tif (t === 0) return 0;\n\tif (t === 1) return 1;\n\treturn Math.pow(2, -10 * t) * Math.sin((10 * t - 0.75) * ELASTIC_C4) + 1;\n}\n\nexport function easeInOutElastic(t: number): number {\n\tif (t === 0) return 0;\n\tif (t === 1) return 1;\n\treturn t < 0.5\n\t\t? -(Math.pow(2, 20 * t - 10) * Math.sin((20 * t - 11.125) * ELASTIC_C5)) / 2\n\t\t: (Math.pow(2, -20 * t + 10) * Math.sin((20 * t - 11.125) * ELASTIC_C5)) / 2 + 1;\n}\n\n// Bounce\nexport function easeOutBounce(t: number): number {\n\tconst n1 = 7.5625;\n\tconst d1 = 2.75;\n\n\tif (t < 1 / d1) {\n\t\treturn n1 * t * t;\n\t} else if (t < 2 / d1) {\n\t\tconst t1 = t - 1.5 / d1;\n\t\treturn n1 * t1 * t1 + 0.75;\n\t} else if (t < 2.5 / d1) {\n\t\tconst t1 = t - 2.25 / d1;\n\t\treturn n1 * t1 * t1 + 0.9375;\n\t} else {\n\t\tconst t1 = t - 2.625 / d1;\n\t\treturn n1 * t1 * t1 + 0.984375;\n\t}\n}\n\nexport function easeInBounce(t: number): number {\n\treturn 1 - easeOutBounce(1 - t);\n}\n\nexport function easeInOutBounce(t: number): number {\n\treturn t < 0.5\n\t\t? (1 - easeOutBounce(1 - 2 * t)) / 2\n\t\t: (1 + easeOutBounce(2 * t - 1)) / 2;\n}\n\n/** Runtime lookup of all easing functions by name */\nexport const easings = {\n\tlinear,\n\teaseInQuad,\n\teaseOutQuad,\n\teaseInOutQuad,\n\teaseInCubic,\n\teaseOutCubic,\n\teaseInOutCubic,\n\teaseInQuart,\n\teaseOutQuart,\n\teaseInOutQuart,\n\teaseInQuint,\n\teaseOutQuint,\n\teaseInOutQuint,\n\teaseInSine,\n\teaseOutSine,\n\teaseInOutSine,\n\teaseInExpo,\n\teaseOutExpo,\n\teaseInOutExpo,\n\teaseInCirc,\n\teaseOutCirc,\n\teaseInOutCirc,\n\teaseInBack,\n\teaseOutBack,\n\teaseInOutBack,\n\teaseInElastic,\n\teaseOutElastic,\n\teaseInOutElastic,\n\teaseInBounce,\n\teaseOutBounce,\n\teaseInOutBounce,\n} as const;\n"
7
+ ],
8
+ "mappings": "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",
9
+ "debugId": "52B5806729E5074564756E2164756E21",
10
+ "names": []
11
+ }
@@ -1,4 +1,4 @@
1
1
  var R=((j)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(j,{get:(I,F)=>(typeof require<"u"?require:I)[F]}):j)(function(j){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+j+'" is not supported')});import{definePlugin as K}from"ecspresso";function h(j,I,F,S){let V={width:j,height:I};if(F!==void 0)V.x=F;if(S!==void 0)V.y=S;return V}function T(j){return{destroyOutOfBounds:j!==void 0?{padding:j}:{}}}function b(j){return{clampToBounds:j!==void 0?{margin:j}:{}}}function w(j){return{wrapAtBounds:j!==void 0?{padding:j}:{}}}function y(j){let{systemGroup:I="physics",priority:F=50,boundsResourceKey:S="bounds",autoRemove:V=!0,phase:U="postUpdate"}=j??{};return K("bounds").withComponentTypes().withEventTypes().withResourceTypes().withLabels().withGroups().requires().install((G)=>{G.addSystem("bounds-destroy").setPriority(F).inPhase(U).inGroup(I).addQuery("entities",{with:["worldTransform","destroyOutOfBounds"]}).setProcess(({queries:D,ecs:J})=>{let C=J.getResource(S),L=C.x??0,N=C.y??0,v=L+C.width,A=N+C.height;for(let Q of D.entities){let{worldTransform:Z,destroyOutOfBounds:z}=Q.components,P=z.padding??0,k=E(Z,L,N,v,A,P);if(!k)continue;if(J.eventBus.publish("entityOutOfBounds",{entityId:Q.id,exitEdge:k}),V)J.commands.removeEntity(Q.id)}}),G.addSystem("bounds-clamp").setPriority(F-1).inPhase(U).inGroup(I).addQuery("entities",{with:["localTransform","worldTransform","clampToBounds"]}).setProcess(({queries:D,ecs:J})=>{let C=J.getResource(S),L=C.x??0,N=C.y??0,v=L+C.width,A=N+C.height;for(let Q of D.entities){let{localTransform:Z,worldTransform:z,clampToBounds:P}=Q.components,k=P.margin??0,_=L+k,$=N+k,H=v-k,O=A-k,W=0,B=0;if(z.x<_)W=_-z.x;if(z.x>H)W=H-z.x;if(z.y<$)B=$-z.y;if(z.y>O)B=O-z.y;if(W!==0||B!==0)Z.x+=W,Z.y+=B,J.markChanged(Q.id,"localTransform")}}),G.addSystem("bounds-wrap").setPriority(F-2).inPhase(U).inGroup(I).addQuery("entities",{with:["localTransform","worldTransform","wrapAtBounds"]}).setProcess(({queries:D,ecs:J})=>{let C=J.getResource(S),L=C.x??0,N=C.y??0,v=L+C.width,A=N+C.height;for(let Q of D.entities){let{localTransform:Z,worldTransform:z,wrapAtBounds:P}=Q.components,k=P.padding??0,_=0,$=0,H=v-L,O=A-N;if(z.x>v+k)_=-(H+2*k);else if(z.x<L-k)_=H+2*k;if(z.y>A+k)$=-(O+2*k);else if(z.y<N-k)$=O+2*k;if(_!==0||$!==0)Z.x+=_,Z.y+=$,J.markChanged(Q.id,"localTransform")}})})}function E(j,I,F,S,V,U){if(j.x>S+U)return"right";if(j.x<I-U)return"left";if(j.y>V+U)return"bottom";if(j.y<F-U)return"top";return null}export{w as createWrapAtBounds,T as createDestroyOutOfBounds,b as createClampToBounds,y as createBoundsPlugin,h as createBounds};
2
2
 
3
- //# debugId=A9A3B2D9F55EA97664756E2164756E21
3
+ //# debugId=C4210D075EB1DBA264756E2164756E21
4
4
  //# sourceMappingURL=bounds.js.map
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/plugins/bounds.ts"],
3
+ "sources": ["../src/plugins/spatial/bounds.ts"],
4
4
  "sourcesContent": [
5
5
  "/**\n * Bounds Plugin for ECSpresso\n *\n * Provides screen bounds enforcement for entities with transforms.\n * Reads worldTransform for position checking; modifies localTransform for corrections.\n * Supports destroy, clamp, and wrap behaviors.\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type { TransformWorldConfig } from './transform';\n\n// ==================== Component Types ====================\n\n/**\n * Component that marks an entity for destruction when outside bounds.\n */\nexport interface DestroyOutOfBounds {\n\t/** Extra padding beyond bounds before destruction (default: 0) */\n\tpadding?: number;\n}\n\n/**\n * Component that clamps an entity's position to stay within bounds.\n */\nexport interface ClampToBounds {\n\t/** Margin to shrink the valid area (default: 0) */\n\tmargin?: number;\n}\n\n/**\n * Component that wraps an entity's position to the opposite edge.\n */\nexport interface WrapAtBounds {\n\t/** Padding beyond bounds before wrapping (default: 0) */\n\tpadding?: number;\n}\n\n/**\n * Component types provided by the bounds plugin.\n * Included automatically via `.withPlugin(createBoundsPlugin())`.\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createTransformPlugin())\n * .withPlugin(createBoundsPlugin({ width: 800, height: 600 }))\n * .withComponentTypes<{ sprite: Sprite }>()\n * .build();\n * ```\n */\nexport interface BoundsComponentTypes {\n\tdestroyOutOfBounds: DestroyOutOfBounds;\n\tclampToBounds: ClampToBounds;\n\twrapAtBounds: WrapAtBounds;\n}\n\n// ==================== Resource Types ====================\n\n/**\n * Bounds rectangle definition.\n */\nexport interface BoundsRect {\n\t/** Left edge x coordinate (default: 0) */\n\tx?: number;\n\t/** Top edge y coordinate (default: 0) */\n\ty?: number;\n\t/** Width of the bounds area */\n\twidth: number;\n\t/** Height of the bounds area */\n\theight: number;\n}\n\n/**\n * Resource types provided by the bounds plugin.\n */\nexport interface BoundsResourceTypes {\n\tbounds: BoundsRect;\n}\n\n// ==================== Event Types ====================\n\n/**\n * Event fired when an entity exits bounds.\n */\nexport interface EntityOutOfBoundsEvent {\n\t/** The entity that exited bounds */\n\tentityId: number;\n\t/** The edge the entity exited through */\n\texitEdge: 'top' | 'bottom' | 'left' | 'right';\n}\n\n/**\n * Event types provided by the bounds plugin.\n */\nexport interface BoundsEventTypes {\n\tentityOutOfBounds: EntityOutOfBoundsEvent;\n}\n\n// ==================== Plugin Options ====================\n\n/**\n * Configuration options for the bounds plugin.\n */\nexport interface BoundsPluginOptions<G extends string = 'physics'> extends BasePluginOptions<G> {\n\t/** Resource key for bounds rectangle (default: 'bounds') */\n\tboundsResourceKey?: string;\n\t/** Whether to auto-remove entities when out of bounds (default: true) */\n\tautoRemove?: boolean;\n}\n\n// ==================== Helper Functions ====================\n\n/**\n * Create a bounds rectangle resource.\n *\n * @param width The width of the bounds area\n * @param height The height of the bounds area\n * @param x The left edge x coordinate (default: 0)\n * @param y The top edge y coordinate (default: 0)\n * @returns Bounds rectangle suitable for use as a resource\n *\n * @example\n * ```typescript\n * ECSpresso.create()\n * .withResource('bounds', createBounds(800, 600))\n * .build();\n * ```\n */\nexport function createBounds(width: number, height: number, x?: number, y?: number): BoundsRect {\n\tconst bounds: BoundsRect = { width, height };\n\tif (x !== undefined) bounds.x = x;\n\tif (y !== undefined) bounds.y = y;\n\treturn bounds;\n}\n\n/**\n * Create a destroyOutOfBounds component.\n *\n * @param padding Extra padding beyond bounds before destruction\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createTransform(100, 200),\n * ...createDestroyOutOfBounds(20),\n * });\n * ```\n */\nexport function createDestroyOutOfBounds(padding?: number): Pick<BoundsComponentTypes, 'destroyOutOfBounds'> {\n\treturn {\n\t\tdestroyOutOfBounds: padding !== undefined ? { padding } : {},\n\t};\n}\n\n/**\n * Create a clampToBounds component.\n *\n * @param margin Margin to shrink the valid area\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createTransform(100, 200),\n * ...createClampToBounds(30),\n * });\n * ```\n */\nexport function createClampToBounds(margin?: number): Pick<BoundsComponentTypes, 'clampToBounds'> {\n\treturn {\n\t\tclampToBounds: margin !== undefined ? { margin } : {},\n\t};\n}\n\n/**\n * Create a wrapAtBounds component.\n *\n * @param padding Padding beyond bounds before wrapping\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createTransform(100, 200),\n * ...createWrapAtBounds(10),\n * });\n * ```\n */\nexport function createWrapAtBounds(padding?: number): Pick<BoundsComponentTypes, 'wrapAtBounds'> {\n\treturn {\n\t\twrapAtBounds: padding !== undefined ? { padding } : {},\n\t};\n}\n\n// ==================== Dependency Types ====================\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a bounds plugin for ECSpresso.\n *\n * This plugin provides:\n * - Destroy out of bounds system - removes entities that exit bounds\n * - Clamp to bounds system - constrains entities within bounds\n * - Wrap at bounds system - wraps entities to opposite edge\n *\n * Uses worldTransform for position checking (world-space) and modifies\n * localTransform for corrections. Works best with entities that don't\n * have parent transforms (orphan entities).\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso\n * .create<Components, Events, Resources>()\n * .withResource('bounds', createBounds(800, 600))\n * .withPlugin(createTransformPlugin())\n * .withPlugin(createBoundsPlugin())\n * .build();\n *\n * // Entity that gets destroyed when leaving screen\n * ecs.spawn({\n * ...createTransform(100, 200),\n * ...createDestroyOutOfBounds(),\n * });\n * ```\n */\nexport function createBoundsPlugin<ResourceTypes extends BoundsResourceTypes = BoundsResourceTypes, G extends string = 'physics'>(\n\toptions?: BoundsPluginOptions<G>\n) {\n\tconst {\n\t\tsystemGroup = 'physics',\n\t\tpriority = 50,\n\t\tboundsResourceKey = 'bounds',\n\t\tautoRemove = true,\n\t\tphase = 'postUpdate',\n\t} = options ?? {};\n\n\treturn definePlugin('bounds')\n\t\t.withComponentTypes<BoundsComponentTypes>()\n\t\t.withEventTypes<BoundsEventTypes>()\n\t\t.withResourceTypes<ResourceTypes>()\n\t\t.withLabels<'bounds-destroy' | 'bounds-clamp' | 'bounds-wrap'>()\n\t\t.withGroups<G>()\n\t\t.requires<TransformWorldConfig>()\n\t\t.install((world) => {\n\t\t\t// Destroy out of bounds system\n\t\t\tworld\n\t\t\t\t.addSystem('bounds-destroy')\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('entities', {\n\t\t\t\t\twith: ['worldTransform', 'destroyOutOfBounds'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, ecs }) => {\n\t\t\t\t\tconst bounds = ecs.getResource(boundsResourceKey as keyof ResourceTypes) as BoundsRect;\n\t\t\t\t\tconst minX = bounds.x ?? 0;\n\t\t\t\t\tconst minY = bounds.y ?? 0;\n\t\t\t\t\tconst maxX = minX + bounds.width;\n\t\t\t\t\tconst maxY = minY + bounds.height;\n\n\t\t\t\t\tfor (const entity of queries.entities) {\n\t\t\t\t\t\tconst { worldTransform, destroyOutOfBounds } = entity.components;\n\t\t\t\t\t\tconst padding = destroyOutOfBounds.padding ?? 0;\n\n\t\t\t\t\t\tconst exitEdge = getExitEdge(worldTransform, minX, minY, maxX, maxY, padding);\n\t\t\t\t\t\tif (!exitEdge) continue;\n\n\t\t\t\t\t\tecs.eventBus.publish('entityOutOfBounds', {\n\t\t\t\t\t\t\tentityId: entity.id,\n\t\t\t\t\t\t\texitEdge,\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\tif (autoRemove) {\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\n\t\t\t// Clamp to bounds system\n\t\t\tworld\n\t\t\t\t.addSystem('bounds-clamp')\n\t\t\t\t.setPriority(priority - 1)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('entities', {\n\t\t\t\t\twith: ['localTransform', 'worldTransform', 'clampToBounds'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, ecs }) => {\n\t\t\t\t\tconst bounds = ecs.getResource(boundsResourceKey as keyof ResourceTypes) as BoundsRect;\n\t\t\t\t\tconst minX = bounds.x ?? 0;\n\t\t\t\t\tconst minY = bounds.y ?? 0;\n\t\t\t\t\tconst maxX = minX + bounds.width;\n\t\t\t\t\tconst maxY = minY + bounds.height;\n\n\t\t\t\t\tfor (const entity of queries.entities) {\n\t\t\t\t\t\tconst { localTransform, worldTransform, clampToBounds } = entity.components;\n\t\t\t\t\t\tconst margin = clampToBounds.margin ?? 0;\n\n\t\t\t\t\t\tconst clampedMinX = minX + margin;\n\t\t\t\t\t\tconst clampedMinY = minY + margin;\n\t\t\t\t\t\tconst clampedMaxX = maxX - margin;\n\t\t\t\t\t\tconst clampedMaxY = maxY - margin;\n\n\t\t\t\t\t\t// Calculate world-space correction and apply to local transform\n\t\t\t\t\t\t// For entities without parents, this is equivalent to direct position clamping\n\t\t\t\t\t\tlet deltaX = 0;\n\t\t\t\t\t\tlet deltaY = 0;\n\n\t\t\t\t\t\tif (worldTransform.x < clampedMinX) deltaX = clampedMinX - worldTransform.x;\n\t\t\t\t\t\tif (worldTransform.x > clampedMaxX) deltaX = clampedMaxX - worldTransform.x;\n\t\t\t\t\t\tif (worldTransform.y < clampedMinY) deltaY = clampedMinY - worldTransform.y;\n\t\t\t\t\t\tif (worldTransform.y > clampedMaxY) deltaY = clampedMaxY - worldTransform.y;\n\n\t\t\t\t\t\tif (deltaX !== 0 || deltaY !== 0) {\n\t\t\t\t\t\t\tlocalTransform.x += deltaX;\n\t\t\t\t\t\t\tlocalTransform.y += deltaY;\n\t\t\t\t\t\t\tecs.markChanged(entity.id, 'localTransform');\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// Wrap at bounds system\n\t\t\tworld\n\t\t\t\t.addSystem('bounds-wrap')\n\t\t\t\t.setPriority(priority - 2)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('entities', {\n\t\t\t\t\twith: ['localTransform', 'worldTransform', 'wrapAtBounds'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, ecs }) => {\n\t\t\t\t\tconst bounds = ecs.getResource(boundsResourceKey as keyof ResourceTypes) as BoundsRect;\n\t\t\t\t\tconst minX = bounds.x ?? 0;\n\t\t\t\t\tconst minY = bounds.y ?? 0;\n\t\t\t\t\tconst maxX = minX + bounds.width;\n\t\t\t\t\tconst maxY = minY + bounds.height;\n\n\t\t\t\t\tfor (const entity of queries.entities) {\n\t\t\t\t\t\tconst { localTransform, worldTransform, wrapAtBounds } = entity.components;\n\t\t\t\t\t\tconst padding = wrapAtBounds.padding ?? 0;\n\n\t\t\t\t\t\tlet deltaX = 0;\n\t\t\t\t\t\tlet deltaY = 0;\n\t\t\t\t\t\tconst boundsWidth = maxX - minX;\n\t\t\t\t\t\tconst boundsHeight = maxY - minY;\n\n\t\t\t\t\t\t// Wrap horizontally\n\t\t\t\t\t\tif (worldTransform.x > maxX + padding) {\n\t\t\t\t\t\t\tdeltaX = -(boundsWidth + 2 * padding);\n\t\t\t\t\t\t} else if (worldTransform.x < minX - padding) {\n\t\t\t\t\t\t\tdeltaX = boundsWidth + 2 * padding;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Wrap vertically\n\t\t\t\t\t\tif (worldTransform.y > maxY + padding) {\n\t\t\t\t\t\t\tdeltaY = -(boundsHeight + 2 * padding);\n\t\t\t\t\t\t} else if (worldTransform.y < minY - padding) {\n\t\t\t\t\t\t\tdeltaY = boundsHeight + 2 * padding;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (deltaX !== 0 || deltaY !== 0) {\n\t\t\t\t\t\t\tlocalTransform.x += deltaX;\n\t\t\t\t\t\t\tlocalTransform.y += deltaY;\n\t\t\t\t\t\t\tecs.markChanged(entity.id, 'localTransform');\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n}\n\n/**\n * Determine which edge an entity has exited through, if any.\n */\nfunction getExitEdge(\n\ttransform: { x: number; y: number },\n\tminX: number,\n\tminY: number,\n\tmaxX: number,\n\tmaxY: number,\n\tpadding: number\n): 'top' | 'bottom' | 'left' | 'right' | null {\n\tif (transform.x > maxX + padding) return 'right';\n\tif (transform.x < minX - padding) return 'left';\n\tif (transform.y > maxY + padding) return 'bottom';\n\tif (transform.y < minY - padding) return 'top';\n\treturn null;\n}\n"
6
6
  ],
7
7
  "mappings": "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",
8
- "debugId": "A9A3B2D9F55EA97664756E2164756E21",
8
+ "debugId": "C4210D075EB1DBA264756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Camera / Viewport Plugin for ECSpresso
3
3
  *
4
- * Provides a camera entity with world/screen coordinate conversion, smooth follow,
5
- * trauma-based shake, bounds clamping, and logical viewport dimensions.
4
+ * Provides a declarative camera with world/screen coordinate conversion, smooth follow,
5
+ * trauma-based shake, bounds clamping, cursor-centered zoom, and logical viewport dimensions.
6
6
  *
7
7
  * This plugin is renderer-agnostic. PixiJS or other renderer integration (applying
8
8
  * cameraState to a container/stage transform) is the consumer's responsibility.
@@ -12,8 +12,6 @@
12
12
  * in the transform hierarchy itself.
13
13
  */
14
14
  import type { SystemPhase } from 'ecspresso';
15
- import type ECSpresso from 'ecspresso';
16
- import type { WorldConfigFrom } from '../type-utils';
17
15
  import type { TransformWorldConfig } from './transform';
18
16
  export interface Camera {
19
17
  x: number;
@@ -48,6 +46,16 @@ export interface CameraComponentTypes {
48
46
  cameraShake: CameraShake;
49
47
  cameraBounds: CameraBounds;
50
48
  }
49
+ export interface FollowOptions {
50
+ smoothing?: number;
51
+ deadzoneX?: number;
52
+ deadzoneY?: number;
53
+ offsetX?: number;
54
+ offsetY?: number;
55
+ }
56
+ export type EntityHandle = {
57
+ id: number;
58
+ };
51
59
  export interface CameraState {
52
60
  x: number;
53
61
  y: number;
@@ -58,6 +66,15 @@ export interface CameraState {
58
66
  shakeRotation: number;
59
67
  viewportWidth: number;
60
68
  viewportHeight: number;
69
+ entityId: number;
70
+ follow(target: number | EntityHandle, options?: FollowOptions): void;
71
+ unfollow(): void;
72
+ setPosition(x: number, y: number): void;
73
+ setZoom(zoom: number): void;
74
+ setRotation(rotation: number): void;
75
+ setBounds(minX: number, minY: number, maxX: number, maxY: number): void;
76
+ clearBounds(): void;
77
+ addTrauma(amount: number): void;
61
78
  }
62
79
  export interface CameraResourceTypes {
63
80
  cameraState: CameraState;
@@ -65,17 +82,38 @@ export interface CameraResourceTypes {
65
82
  export interface CameraPluginOptions<G extends string = 'camera'> {
66
83
  viewportWidth?: number;
67
84
  viewportHeight?: number;
85
+ initial?: {
86
+ x?: number;
87
+ y?: number;
88
+ zoom?: number;
89
+ rotation?: number;
90
+ };
91
+ follow?: FollowOptions;
92
+ shake?: boolean | Partial<Omit<CameraShake, 'trauma'>>;
93
+ bounds?: {
94
+ minX: number;
95
+ minY: number;
96
+ maxX: number;
97
+ maxY: number;
98
+ } | [number, number, number, number];
99
+ zoom?: {
100
+ zoomStep?: number;
101
+ minZoom?: number;
102
+ maxZoom?: number;
103
+ };
104
+ pan?: {
105
+ speed: number;
106
+ actions?: {
107
+ up?: string;
108
+ down?: string;
109
+ left?: string;
110
+ right?: string;
111
+ };
112
+ };
68
113
  systemGroup?: G;
69
114
  phase?: SystemPhase;
70
115
  randomFn?: () => number;
71
116
  }
72
- export declare const DEFAULT_CAMERA: Readonly<Camera>;
73
- export declare const DEFAULT_CAMERA_STATE: Readonly<CameraState>;
74
- export declare function createCamera(x?: number, y?: number, zoom?: number, rotation?: number): Pick<CameraComponentTypes, 'camera'>;
75
- export declare function createCameraFollow(target: number, options?: Partial<Omit<CameraFollow, 'target'>>): Pick<CameraComponentTypes, 'cameraFollow'>;
76
- export declare function createCameraShake(options?: Partial<CameraShake>): Pick<CameraComponentTypes, 'cameraShake'>;
77
- export declare function createCameraBounds(minX: number, minY: number, maxX: number, maxY: number): Pick<CameraComponentTypes, 'cameraBounds'>;
78
- export declare function addTrauma<Cfg extends WorldConfigFrom<CameraComponentTypes, {}, CameraResourceTypes>>(ecs: ECSpresso<Cfg>, entityId: number, amount: number): void;
79
117
  export declare function worldToScreen(worldX: number, worldY: number, state: CameraState): {
80
118
  x: number;
81
119
  y: number;
@@ -84,4 +122,6 @@ export declare function screenToWorld(screenX: number, screenY: number, state: C
84
122
  x: number;
85
123
  y: number;
86
124
  };
87
- export declare function createCameraPlugin<G extends string = 'camera'>(options?: CameraPluginOptions<G>): import("ecspresso").Plugin<import("ecspresso").WithResources<import("ecspresso").WithComponents<import("ecspresso").EmptyConfig, CameraComponentTypes>, CameraResourceTypes>, TransformWorldConfig, "camera-follow" | "camera-shake-update" | "camera-bounds" | "camera-state-sync", G, never, never>;
125
+ type CameraLabels = 'camera-init' | 'camera-follow' | 'camera-shake-update' | 'camera-bounds' | 'camera-state-sync' | 'camera-zoom' | 'camera-pan';
126
+ export declare function createCameraPlugin<G extends string = 'camera'>(options?: CameraPluginOptions<G>): import("ecspresso").Plugin<import("ecspresso").WithResources<import("ecspresso").WithComponents<import("ecspresso").EmptyConfig, CameraComponentTypes>, CameraResourceTypes>, TransformWorldConfig, CameraLabels, G, never, never>;
127
+ export {};
@@ -0,0 +1,4 @@
1
+ var t=(($)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy($,{get:(z,R)=>(typeof require<"u"?require:z)[R]}):$)(function($){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+$+'" is not supported')});import{definePlugin as r}from"ecspresso";var v={traumaDecay:1,maxOffsetX:10,maxOffsetY:10,maxRotation:0.05},X={smoothing:5,deadzoneX:0,deadzoneY:0,offsetX:0,offsetY:0};function jj($,z,R){let H=$-(R.x+R.shakeOffsetX),B=z-(R.y+R.shakeOffsetY),b=-(R.rotation+R.shakeRotation),E=Math.cos(b),W=Math.sin(b),L=H*E-B*W,U=H*W+B*E;return{x:L*R.zoom+R.viewportWidth/2,y:U*R.zoom+R.viewportHeight/2}}function c($,z,R){let H=($-R.viewportWidth/2)/R.zoom,B=(z-R.viewportHeight/2)/R.zoom,b=R.rotation+R.shakeRotation,E=Math.cos(b),W=Math.sin(b),L=H*E-B*W,U=H*W+B*E;return{x:L+R.x+R.shakeOffsetX,y:U+R.y+R.shakeOffsetY}}function o($){return typeof $==="number"?$:$.id}function w($){let z=$===!0?{}:$;return{trauma:0,traumaDecay:z.traumaDecay??v.traumaDecay,maxOffsetX:z.maxOffsetX??v.maxOffsetX,maxOffsetY:z.maxOffsetY??v.maxOffsetY,maxRotation:z.maxRotation??v.maxRotation}}function s($){if(Array.isArray($))return{minX:$[0],minY:$[1],maxX:$[2],maxY:$[3]};return{...$}}function u($){return{smoothing:$?.smoothing??X.smoothing,deadzoneX:$?.deadzoneX??X.deadzoneX,deadzoneY:$?.deadzoneY??X.deadzoneY,offsetX:$?.offsetX??X.offsetX,offsetY:$?.offsetY??X.offsetY}}function Jj($){let{viewportWidth:z=800,viewportHeight:R=600,initial:H,follow:B,shake:b,bounds:E,zoom:W,pan:L,systemGroup:U="camera",phase:Y="postUpdate",randomFn:F=Math.random}=$??{};return r("camera").withComponentTypes().withResourceTypes().withLabels().withGroups().requires().install((D)=>{let j={x:H?.x??0,y:H?.y??0,zoom:H?.zoom??1,rotation:H?.rotation??0,shakeOffsetX:0,shakeOffsetY:0,shakeRotation:0,viewportWidth:z,viewportHeight:R,entityId:-1,follow:()=>{},unfollow:()=>{},setPosition:()=>{},setZoom:()=>{},setRotation:()=>{},setBounds:()=>{},clearBounds:()=>{},addTrauma:()=>{}};if(D.addResource("cameraState",j),D.addSystem("camera-init").inGroup(U).setOnInitialize((V)=>{let I=V.spawn({camera:{x:H?.x??0,y:H?.y??0,zoom:H?.zoom??1,rotation:H?.rotation??0}});if(B)V.addComponent(I.id,"cameraFollow",{target:-1,...u(B)});if(b)V.addComponent(I.id,"cameraShake",w(b));if(E)V.addComponent(I.id,"cameraBounds",s(E));j.entityId=I.id,j.follow=(Q,J)=>{let P={target:o(Q),...u(J)},N=V.getComponent(j.entityId,"cameraFollow");if(N)N.target=P.target,N.smoothing=P.smoothing,N.deadzoneX=P.deadzoneX,N.deadzoneY=P.deadzoneY,N.offsetX=P.offsetX,N.offsetY=P.offsetY;else V.addComponent(j.entityId,"cameraFollow",P)},j.unfollow=()=>{if(V.getComponent(j.entityId,"cameraFollow"))V.removeComponent(j.entityId,"cameraFollow")},j.setPosition=(Q,J)=>{let G=V.getComponent(j.entityId,"camera");if(!G)return;G.x=Q,G.y=J},j.setZoom=(Q)=>{let J=V.getComponent(j.entityId,"camera");if(!J)return;J.zoom=Q},j.setRotation=(Q)=>{let J=V.getComponent(j.entityId,"camera");if(!J)return;J.rotation=Q},j.setBounds=(Q,J,G,P)=>{let N=V.getComponent(j.entityId,"cameraBounds");if(N)N.minX=Q,N.minY=J,N.maxX=G,N.maxY=P;else V.addComponent(j.entityId,"cameraBounds",{minX:Q,minY:J,maxX:G,maxY:P})},j.clearBounds=()=>{if(V.getComponent(j.entityId,"cameraBounds"))V.removeComponent(j.entityId,"cameraBounds")},j.addTrauma=(Q)=>{let J=V.getComponent(j.entityId,"cameraShake");if(J)J.trauma=Math.min(1,Math.max(0,J.trauma+Q));else V.addComponent(j.entityId,"cameraShake",{...w(!0),trauma:Math.min(1,Math.max(0,Q))})}}),D.addSystem("camera-follow").setPriority(400).inPhase(Y).inGroup(U).addQuery("cameras",{with:["camera","cameraFollow"]}).setProcess(({queries:V,dt:I,ecs:Q})=>{let J=Math.min(1,I);for(let G of V.cameras){let{camera:P,cameraFollow:N}=G.components;if(N.target<0)continue;let M;try{M=Q.getComponent(N.target,"worldTransform")}catch{continue}if(!M)continue;let q=M.x+N.offsetX,Z=M.y+N.offsetY,_=q-P.x,C=Z-P.y;if(Math.abs(_)>N.deadzoneX){let K=_>0?1:-1,T=_-K*N.deadzoneX,A=Math.min(1,N.smoothing*J);P.x+=T*A}if(Math.abs(C)>N.deadzoneY){let K=C>0?1:-1,T=C-K*N.deadzoneY,A=Math.min(1,N.smoothing*J);P.y+=T*A}}}),D.addSystem("camera-shake-update").setPriority(390).inPhase(Y).inGroup(U).addQuery("shakeCameras",{with:["camera","cameraShake"]}).setProcess(({queries:V,dt:I})=>{for(let Q of V.shakeCameras){let{cameraShake:J}=Q.components;J.trauma=Math.max(0,J.trauma-J.traumaDecay*I)}}),D.addSystem("camera-bounds").setPriority(380).inPhase(Y).inGroup(U).addQuery("boundedCameras",{with:["camera","cameraBounds"]}).setProcess(({queries:V})=>{for(let I of V.boundedCameras){let{camera:Q,cameraBounds:J}=I.components,G=j.viewportWidth/(2*Q.zoom),P=j.viewportHeight/(2*Q.zoom),N=J.minX+G,M=J.maxX-G,q=J.minY+P,Z=J.maxY-P;if(N>M)Q.x=(J.minX+J.maxX)/2;else Q.x=Math.max(N,Math.min(M,Q.x));if(q>Z)Q.y=(J.minY+J.maxY)/2;else Q.y=Math.max(q,Math.min(Z,Q.y))}}),D.addSystem("camera-state-sync").setPriority(370).inPhase(Y).inGroup(U).setProcess(({ecs:V})=>{let I=V.getComponent(j.entityId,"camera");if(!I){j.x=0,j.y=0,j.zoom=1,j.rotation=0,j.shakeOffsetX=0,j.shakeOffsetY=0,j.shakeRotation=0;return}j.x=I.x,j.y=I.y,j.zoom=I.zoom,j.rotation=I.rotation;let Q=V.getComponent(j.entityId,"cameraShake");if(Q&&Q.trauma>0){let J=Q.trauma*Q.trauma;j.shakeOffsetX=Q.maxOffsetX*J*(F()*2-1),j.shakeOffsetY=Q.maxOffsetY*J*(F()*2-1),j.shakeRotation=Q.maxRotation*J*(F()*2-1)}else j.shakeOffsetX=0,j.shakeOffsetY=0,j.shakeRotation=0}),W){let M=function(q){q.preventDefault(),J+=Math.sign(q.deltaY)},{zoomStep:V=0.1,minZoom:I=0.1,maxZoom:Q=10}=W,J=0,G=!1,P,N;D.addSystem("camera-zoom").setPriority(410).inPhase("preUpdate").inGroup(U).addQuery("cameras",{with:["camera"]}).setOnInitialize((q)=>{let Z=q.tryGetResource("inputState"),_=q.tryGetResource("pixiApp");if(!Z||!_){console.error("[camera] zoom requires the input plugin and renderer2D plugin. Zoom will be disabled.");return}P=_.canvas,P.addEventListener("wheel",M,{passive:!1}),N=q.tryGetResource("isoProjection"),G=!0}).setOnDetach(()=>{if(!G||!P)return;P.removeEventListener("wheel",M)}).setProcess(({queries:q,ecs:Z})=>{if(!G||J===0)return;let _=J;J=0;let[C]=q.cameras;if(!C)return;let K=C.components.camera,T=Z.tryGetResource("inputState");if(!T)return;let A=_>0?1-V:1+V,k=Math.max(I,Math.min(Q,K.zoom*Math.pow(A,Math.abs(_))));if(N&&P){let O=P.getBoundingClientRect(),y=T.pointer.position.x-(O.left+O.width/2),h=T.pointer.position.y-(O.top+O.height/2),f=N.tileWidth/2,p=N.tileHeight/2,d=(K.x-K.y)*f+N.originX,S=(K.x+K.y)*p+N.originY,l=d+y/K.zoom,m=S+h/K.zoom;K.zoom=k;let i=l-y/k,n=m-h/k,x=i-N.originX,g=n-N.originY;K.x=x/N.tileWidth+g/N.tileHeight,K.y=-x/N.tileWidth+g/N.tileHeight}else{let O=c(T.pointer.position.x,T.pointer.position.y,j);K.zoom=k,K.x=O.x-(T.pointer.position.x-j.viewportWidth/2)/k,K.y=O.y-(T.pointer.position.y-j.viewportHeight/2)/k}})}if(L){let{speed:V,actions:I}=L,Q=I?.up??"panUp",J=I?.down??"panDown",G=I?.left??"panLeft",P=I?.right??"panRight",N=!1;D.addSystem("camera-pan").setPriority(420).inPhase("preUpdate").inGroup(U).setOnInitialize((M)=>{if(!M.tryGetResource("inputState")){console.error("[camera] pan requires the input plugin. Pan will be disabled.");return}N=!0}).setProcess(({ecs:M,dt:q})=>{if(!N)return;let Z=M.tryGetResource("inputState");if(!Z)return;let _=V/j.zoom*q,C=(Z.actions.isActive(P)?1:0)-(Z.actions.isActive(G)?1:0),K=(Z.actions.isActive(J)?1:0)-(Z.actions.isActive(Q)?1:0);if(C!==0||K!==0)j.setPosition(j.x+C*_,j.y+K*_)})}})}export{jj as worldToScreen,c as screenToWorld,Jj as createCameraPlugin};
2
+
3
+ //# debugId=EEB187C9BEF5641464756E2164756E21
4
+ //# sourceMappingURL=camera.js.map