ecspresso 0.12.9 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/index.js +2 -2
  2. package/dist/index.js.map +5 -5
  3. package/dist/plugin.d.ts +89 -22
  4. package/dist/plugins/audio.d.ts +2 -3
  5. package/dist/plugins/audio.js +2 -2
  6. package/dist/plugins/audio.js.map +3 -3
  7. package/dist/plugins/bounds.d.ts +2 -3
  8. package/dist/plugins/bounds.js +2 -2
  9. package/dist/plugins/bounds.js.map +3 -3
  10. package/dist/plugins/camera.d.ts +1 -2
  11. package/dist/plugins/camera.js +2 -2
  12. package/dist/plugins/camera.js.map +3 -3
  13. package/dist/plugins/collision.d.ts +9 -8
  14. package/dist/plugins/collision.js +2 -2
  15. package/dist/plugins/collision.js.map +4 -4
  16. package/dist/plugins/coroutine.d.ts +2 -3
  17. package/dist/plugins/coroutine.js +2 -2
  18. package/dist/plugins/coroutine.js.map +3 -3
  19. package/dist/plugins/diagnostics.d.ts +1 -3
  20. package/dist/plugins/diagnostics.js +2 -2
  21. package/dist/plugins/diagnostics.js.map +3 -3
  22. package/dist/plugins/input.d.ts +11 -3
  23. package/dist/plugins/input.js +2 -2
  24. package/dist/plugins/input.js.map +3 -3
  25. package/dist/plugins/particles.d.ts +2 -2
  26. package/dist/plugins/particles.js +2 -2
  27. package/dist/plugins/particles.js.map +3 -3
  28. package/dist/plugins/physics2D.d.ts +8 -5
  29. package/dist/plugins/physics2D.js +2 -2
  30. package/dist/plugins/physics2D.js.map +4 -4
  31. package/dist/plugins/renderers/renderer2D.d.ts +36 -9
  32. package/dist/plugins/renderers/renderer2D.js +2 -2
  33. package/dist/plugins/renderers/renderer2D.js.map +3 -3
  34. package/dist/plugins/spatial-index.d.ts +1 -4
  35. package/dist/plugins/spatial-index.js +2 -2
  36. package/dist/plugins/spatial-index.js.map +4 -4
  37. package/dist/plugins/sprite-animation.d.ts +2 -3
  38. package/dist/plugins/sprite-animation.js +2 -2
  39. package/dist/plugins/sprite-animation.js.map +3 -3
  40. package/dist/plugins/state-machine.d.ts +2 -3
  41. package/dist/plugins/state-machine.js +2 -2
  42. package/dist/plugins/state-machine.js.map +3 -3
  43. package/dist/plugins/timers.d.ts +2 -3
  44. package/dist/plugins/timers.js +2 -2
  45. package/dist/plugins/timers.js.map +3 -3
  46. package/dist/plugins/transform.d.ts +3 -3
  47. package/dist/plugins/transform.js +2 -2
  48. package/dist/plugins/transform.js.map +3 -3
  49. package/dist/plugins/tween.d.ts +2 -3
  50. package/dist/plugins/tween.js +2 -2
  51. package/dist/plugins/tween.js.map +3 -3
  52. package/dist/utils/narrowphase.d.ts +60 -19
  53. package/dist/utils/spatial-hash.d.ts +11 -1
  54. package/package.json +4 -1
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/plugins/input.ts"],
4
4
  "sourcesContent": [
5
- "/**\n * Input Plugin for ECSpresso\n *\n * Provides frame-accurate keyboard, pointer (mouse + touch via PointerEvent),\n * and action mapping input. Resource-only plugin — input is polled via the\n * `inputState` resource. No ECS components or events.\n *\n * DOM events are accumulated between frames and snapshotted once per frame\n * in the system's process step, so all systems see consistent state.\n */\n\nimport { definePlugin, type Plugin, type BasePluginOptions } from 'ecspresso';\nimport type { WorldConfigFrom, EmptyConfig } from '../type-utils';\n\n// ==================== Public Types ====================\n\nexport interface Vec2 {\n\tx: number;\n\ty: number;\n}\n\n// Key codes per the UI Events spec (KeyboardEvent.key values)\n// https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values\n\ntype LowercaseLetter =\n\t| 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm'\n\t| 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z';\n\ntype Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';\n\ntype Punctuation =\n\t| '`' | '~' | '!' | '@' | '#' | '$' | '%' | '^' | '&' | '*' | '(' | ')'\n\t| '-' | '_' | '=' | '+' | '[' | '{' | ']' | '}' | '\\\\' | '|'\n\t| ';' | ':' | \"'\" | '\"' | ',' | '<' | '.' | '>' | '/' | '?';\n\ntype ModifierKey =\n\t| 'Alt' | 'AltGraph' | 'CapsLock' | 'Control' | 'Fn' | 'FnLock'\n\t| 'Hyper' | 'Meta' | 'NumLock' | 'ScrollLock' | 'Shift'\n\t| 'Super' | 'Symbol' | 'SymbolLock';\n\ntype WhitespaceKey = 'Enter' | 'Tab' | ' ';\n\ntype NavigationKey =\n\t| `Arrow${'Down' | 'Left' | 'Right' | 'Up'}`\n\t| 'End' | 'Home' | 'PageDown' | 'PageUp';\n\ntype EditingKey =\n\t| 'Backspace' | 'Clear' | 'Copy' | 'CrSel' | 'Cut' | 'Delete'\n\t| 'EraseEof' | 'ExSel' | 'Insert' | 'Paste' | 'Redo' | 'Undo';\n\ntype UIKey =\n\t| 'Accept' | 'Again' | 'Attn' | 'Cancel' | 'ContextMenu' | 'Escape'\n\t| 'Execute' | 'Find' | 'Finish' | 'Help' | 'Pause' | 'Play'\n\t| 'Props' | 'Select' | 'ZoomIn' | 'ZoomOut';\n\ntype DeviceKey =\n\t| 'BrightnessDown' | 'BrightnessUp' | 'Eject' | 'Hibernate'\n\t| 'LogOff' | 'Power' | 'PowerOff' | 'PrintScreen' | 'Standby' | 'WakeUp';\n\ntype IMEKey =\n\t| 'AllCandidates' | 'Alphanumeric' | 'CodeInput' | 'Compose' | 'Convert'\n\t| 'FinalMode' | 'GroupFirst' | 'GroupLast' | 'GroupNext' | 'GroupPrevious'\n\t| 'ModeChange' | 'NextCandidate' | 'NonConvert' | 'PreviousCandidate'\n\t| 'Process' | 'SingleCandidate'\n\t| 'HangulMode' | 'HanjaMode' | 'JunjaMode'\n\t| 'Eisu' | 'Hankaku' | 'Hiragana' | 'HiraganaKatakana' | 'KanaMode'\n\t| 'KanjiMode' | 'Katakana' | 'Romaji' | 'Zenkaku' | 'ZenkakuHankaku';\n\ntype FunctionKey =\n\t| `F${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24}`\n\t| 'Soft1' | 'Soft2' | 'Soft3' | 'Soft4';\n\ntype PhoneKey =\n\t| 'AppSwitch' | 'Call' | 'Camera' | 'CameraFocus' | 'EndCall'\n\t| 'GoBack' | 'GoHome' | 'HeadsetHook' | 'LastNumberRedial'\n\t| 'Notification' | 'MannerMode' | 'VoiceDial';\n\ntype MultimediaKey =\n\t| 'ChannelDown' | 'ChannelUp'\n\t| `Media${\n\t\t'FastForward' | 'Pause' | 'Play' | 'PlayPause'\n\t\t| 'Record' | 'Rewind' | 'Stop' | 'TrackNext' | 'TrackPrevious'\n\t}`;\n\ntype AudioKey =\n\t| `Audio${\n\t\t'BalanceLeft' | 'BalanceRight' | 'BassDown' | 'BassBoostDown'\n\t\t| 'BassBoostToggle' | 'BassBoostUp' | 'BassUp' | 'FaderFront' | 'FaderRear'\n\t\t| 'SurroundModeNext' | 'TrebleDown' | 'TrebleUp'\n\t\t| 'VolumeDown' | 'VolumeMute' | 'VolumeUp'\n\t}`\n\t| `Microphone${'Toggle' | 'VolumeDown' | 'VolumeMute' | 'VolumeUp'}`;\n\ntype TVKey =\n\t| 'TV'\n\t| `TV${\n\t\t'3DMode' | 'AntennaCable' | 'AudioDescription' | 'AudioDescriptionMixDown'\n\t\t| 'AudioDescriptionMixUp' | 'ContentsMenu' | 'DataService' | 'Input'\n\t\t| 'InputComponent1' | 'InputComponent2' | 'InputComposite1' | 'InputComposite2'\n\t\t| 'InputHDMI1' | 'InputHDMI2' | 'InputHDMI3' | 'InputHDMI4' | 'InputVGA1'\n\t\t| 'MediaContext' | 'Network' | 'NumberEntry' | 'Power' | 'RadioService'\n\t\t| 'Satellite' | 'SatelliteBS' | 'SatelliteCS' | 'SatelliteToggle'\n\t\t| 'TerrestrialAnalog' | 'TerrestrialDigital' | 'Timer'\n\t}`;\n\ntype MediaControllerKey =\n\t| 'AVRInput' | 'AVRPower'\n\t| `Color${'F0Red' | 'F1Green' | 'F2Yellow' | 'F3Blue' | 'F4Grey' | 'F5Brown'}`\n\t| 'ClosedCaptionToggle' | 'Dimmer' | 'DisplaySwap' | 'DVR' | 'Exit'\n\t| `Favorite${'Clear' | 'Recall' | 'Store'}${0 | 1 | 2 | 3}`\n\t| 'Guide' | 'GuideNextDay' | 'GuidePreviousDay' | 'Info' | 'InstantReplay'\n\t| 'Link' | 'ListProgram' | 'LiveContent' | 'Lock'\n\t| `Media${\n\t\t'Apps' | 'AudioTrack' | 'Last' | 'SkipBackward'\n\t\t| 'SkipForward' | 'StepBackward' | 'StepForward' | 'TopMenu'\n\t}`\n\t| `Navigate${'In' | 'Next' | 'Out' | 'Previous'}`\n\t| 'NextFavoriteChannel' | 'NextUserProfile' | 'OnDemand' | 'Pairing'\n\t| `PinP${'Down' | 'Move' | 'Toggle' | 'Up'}`\n\t| `PlaySpeed${'Down' | 'Reset' | 'Up'}`\n\t| 'RandomToggle' | 'RcLowBattery' | 'RecordSpeedNext' | 'RfBypass'\n\t| 'ScanChannelsToggle' | 'ScreenModeNext' | 'Settings' | 'SplitScreenToggle'\n\t| 'STBInput' | 'STBPower' | 'Subtitle' | 'Teletext'\n\t| 'VideoModeNext' | 'Wink' | 'ZoomToggle';\n\ntype SpeechKey = 'SpeechCorrectionList' | 'SpeechInputToggle';\n\ntype DocumentKey =\n\t| 'Close' | 'New' | 'Open' | 'Print' | 'Save' | 'SpellCheck'\n\t| 'MailForward' | 'MailReply' | 'MailSend';\n\ntype LaunchKey = `Launch${\n\t| 'Calculator' | 'Calendar' | 'Contacts' | 'Mail' | 'MediaPlayer'\n\t| 'MusicPlayer' | 'MyComputer' | 'Phone' | 'ScreenSaver' | 'Spreadsheet'\n\t| 'WebBrowser' | 'WebCam' | 'WordProcessor'\n\t| `Application${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16}`\n}`;\n\ntype BrowserKey = `Browser${'Back' | 'Favorites' | 'Forward' | 'Home' | 'Refresh' | 'Search' | 'Stop'}`;\n\ntype NumpadKey = 'Decimal' | 'Key11' | 'Key12' | 'Multiply' | 'Add' | 'Divide' | 'Subtract' | 'Separator';\n\nexport type KeyCode =\n\t| LowercaseLetter | Uppercase<LowercaseLetter> | Digit | Punctuation\n\t| ModifierKey | WhitespaceKey | NavigationKey | EditingKey | UIKey | DeviceKey\n\t| IMEKey | FunctionKey | PhoneKey | MultimediaKey | AudioKey | TVKey\n\t| MediaControllerKey | SpeechKey | DocumentKey | LaunchKey | BrowserKey | NumpadKey\n\t| 'Unidentified' | 'Dead';\n\nexport interface KeyboardState {\n\tisDown(key: KeyCode): boolean;\n\tjustPressed(key: KeyCode): boolean;\n\tjustReleased(key: KeyCode): boolean;\n}\n\nexport interface PointerState {\n\treadonly position: Readonly<Vec2>;\n\treadonly delta: Readonly<Vec2>;\n\tisDown(button: number): boolean;\n\tjustPressed(button: number): boolean;\n\tjustReleased(button: number): boolean;\n}\n\nexport interface ActionState<A extends string = string> {\n\tisActive(action: A): boolean;\n\tjustActivated(action: A): boolean;\n\tjustDeactivated(action: A): boolean;\n}\n\nexport interface InputState<A extends string = string> {\n\treadonly keyboard: KeyboardState;\n\treadonly pointer: PointerState;\n\treadonly actions: ActionState<A>;\n\tsetActionMap(actions: ActionMap<A>): void;\n\tgetActionMap(): Readonly<ActionMap<A>>;\n}\n\nexport interface ActionBinding {\n\tkeys?: KeyCode[];\n\tbuttons?: number[];\n}\n\nexport type ActionMap<A extends string = string> = Record<A, ActionBinding>;\n\nexport interface InputResourceTypes<A extends string = string> {\n\tinputState: InputState<A>;\n}\n\nexport interface InputPluginOptions<A extends string = string, G extends string = 'input'> extends BasePluginOptions<G> {\n\t/** Initial action mappings */\n\tactions?: ActionMap<A>;\n\t/** EventTarget to attach listeners to (default: globalThis). Pass a custom target for testability. */\n\ttarget?: EventTarget;\n}\n\n// ==================== Helper Functions ====================\n\n/**\n * Create a single action binding.\n *\n * @param binding The binding configuration\n * @returns The same binding object\n */\nexport function createActionBinding(binding: ActionBinding): ActionBinding {\n\treturn binding;\n}\n\n// ==================== Internal Types ====================\n\ninterface RawInputState {\n\tkeysDown: Set<string>;\n\tkeysPressed: string[];\n\tkeysReleased: string[];\n\tbuttonsDown: Set<number>;\n\tbuttonsPressed: number[];\n\tbuttonsReleased: number[];\n\tpointerX: number;\n\tpointerY: number;\n\tpointerDeltaX: number;\n\tpointerDeltaY: number;\n\tlastPointerX: number;\n\tlastPointerY: number;\n\tpointerMoved: boolean;\n}\n\ninterface FrameSnapshot {\n\tkeysDown: ReadonlySet<string>;\n\tkeysPressed: ReadonlySet<string>;\n\tkeysReleased: ReadonlySet<string>;\n\tbuttonsDown: ReadonlySet<number>;\n\tbuttonsPressed: ReadonlySet<number>;\n\tbuttonsReleased: ReadonlySet<number>;\n\tpointerX: number;\n\tpointerY: number;\n\tpointerDeltaX: number;\n\tpointerDeltaY: number;\n\tactionsActive: ReadonlySet<string>;\n\tprevActionsActive: ReadonlySet<string>;\n}\n\n// ==================== Plugin Factory ====================\n\nfunction createRawInputState(): RawInputState {\n\treturn {\n\t\tkeysDown: new Set(),\n\t\tkeysPressed: [],\n\t\tkeysReleased: [],\n\t\tbuttonsDown: new Set(),\n\t\tbuttonsPressed: [],\n\t\tbuttonsReleased: [],\n\t\tpointerX: 0,\n\t\tpointerY: 0,\n\t\tpointerDeltaX: 0,\n\t\tpointerDeltaY: 0,\n\t\tlastPointerX: 0,\n\t\tlastPointerY: 0,\n\t\tpointerMoved: false,\n\t};\n}\n\nconst EMPTY_SET_STRING: ReadonlySet<string> = new Set<string>();\nconst EMPTY_SET_NUMBER: ReadonlySet<number> = new Set<number>();\n\nfunction createEmptySnapshot(): FrameSnapshot {\n\treturn {\n\t\tkeysDown: EMPTY_SET_STRING,\n\t\tkeysPressed: EMPTY_SET_STRING,\n\t\tkeysReleased: EMPTY_SET_STRING,\n\t\tbuttonsDown: EMPTY_SET_NUMBER,\n\t\tbuttonsPressed: EMPTY_SET_NUMBER,\n\t\tbuttonsReleased: EMPTY_SET_NUMBER,\n\t\tpointerX: 0,\n\t\tpointerY: 0,\n\t\tpointerDeltaX: 0,\n\t\tpointerDeltaY: 0,\n\t\tactionsActive: EMPTY_SET_STRING,\n\t\tprevActionsActive: EMPTY_SET_STRING,\n\t};\n}\n\nfunction computeActiveActions(\n\tactionMap: ActionMap,\n\tkeysDown: ReadonlySet<string>,\n\tbuttonsDown: ReadonlySet<number>,\n): Set<string> {\n\tconst active = new Set<string>();\n\tfor (const [name, binding] of Object.entries(actionMap)) {\n\t\tconst keyActive = binding.keys?.some((k) => keysDown.has(k)) ?? false;\n\t\tconst buttonActive = binding.buttons?.some((b) => buttonsDown.has(b)) ?? false;\n\t\tif (keyActive || buttonActive) {\n\t\t\tactive.add(name);\n\t\t}\n\t}\n\treturn active;\n}\n\nfunction snapshotRaw(raw: RawInputState, prevActionsActive: ReadonlySet<string>, actionMap: ActionMap): FrameSnapshot {\n\tconst keysDown = new Set(raw.keysDown);\n\tconst keysPressed = new Set(raw.keysPressed);\n\tconst keysReleased = new Set(raw.keysReleased);\n\tconst buttonsDown = new Set(raw.buttonsDown);\n\tconst buttonsPressed = new Set(raw.buttonsPressed);\n\tconst buttonsReleased = new Set(raw.buttonsReleased);\n\n\tconst pointerDeltaX = raw.pointerMoved ? raw.pointerX - raw.lastPointerX : 0;\n\tconst pointerDeltaY = raw.pointerMoved ? raw.pointerY - raw.lastPointerY : 0;\n\n\tconst actionsActive = computeActiveActions(actionMap, keysDown, buttonsDown);\n\n\tconst snapshot: FrameSnapshot = {\n\t\tkeysDown,\n\t\tkeysPressed,\n\t\tkeysReleased,\n\t\tbuttonsDown,\n\t\tbuttonsPressed,\n\t\tbuttonsReleased,\n\t\tpointerX: raw.pointerX,\n\t\tpointerY: raw.pointerY,\n\t\tpointerDeltaX,\n\t\tpointerDeltaY,\n\t\tactionsActive,\n\t\tprevActionsActive,\n\t};\n\n\t// Clear accumulation buffers\n\traw.keysPressed = [];\n\traw.keysReleased = [];\n\traw.buttonsPressed = [];\n\traw.buttonsReleased = [];\n\traw.lastPointerX = raw.pointerX;\n\traw.lastPointerY = raw.pointerY;\n\traw.pointerMoved = false;\n\n\treturn snapshot;\n}\n\n/**\n * Create an input plugin for ECSpresso.\n *\n * This plugin provides:\n * - Frame-accurate keyboard state (isDown, justPressed, justReleased)\n * - Pointer position/delta and button state (mouse + touch via PointerEvent)\n * - Named action mapping with runtime remapping\n * - Automatic listener cleanup on detach\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createInputPlugin({\n * actions: {\n * jump: { keys: [' ', 'ArrowUp'] },\n * shoot: { keys: ['z'], buttons: [0] },\n * },\n * }))\n * .build();\n *\n * // In a system:\n * const input = ecs.getResource('inputState');\n * if (input.actions.justActivated('jump')) { ... }\n * if (input.keyboard.isDown('ArrowRight')) { ... }\n * ```\n */\nexport function createInputPlugin<A extends string = string, G extends string = 'input'>(\n\toptions?: InputPluginOptions<A, G>\n): Plugin<WorldConfigFrom<{}, {}, InputResourceTypes<A>>, EmptyConfig, 'input-state', G> {\n\tconst {\n\t\tsystemGroup = 'input',\n\t\tpriority = 100,\n\t\tphase = 'preUpdate',\n\t\tactions: initialActions = {},\n\t\ttarget = globalThis,\n\t} = options ?? {};\n\n\t// Closure state\n\tconst raw = createRawInputState();\n\tlet snapshot = createEmptySnapshot();\n\tlet actionMap: ActionMap = { ...initialActions };\n\tconst cleanupFns: Array<() => void> = [];\n\n\t// The position/delta objects exposed via the resource.\n\t// Updated in-place each frame to avoid allocations.\n\tconst position: Vec2 = { x: 0, y: 0 };\n\tconst delta: Vec2 = { x: 0, y: 0 };\n\n\t// Build the InputState resource that closes over snapshot\n\tconst keyboard: KeyboardState = {\n\t\tisDown: (key) => snapshot.keysDown.has(key),\n\t\tjustPressed: (key) => snapshot.keysPressed.has(key),\n\t\tjustReleased: (key) => snapshot.keysReleased.has(key),\n\t};\n\n\tconst pointer: PointerState = {\n\t\tposition,\n\t\tdelta,\n\t\tisDown: (button) => snapshot.buttonsDown.has(button),\n\t\tjustPressed: (button) => snapshot.buttonsPressed.has(button),\n\t\tjustReleased: (button) => snapshot.buttonsReleased.has(button),\n\t};\n\n\tconst actionState: ActionState<A> = {\n\t\tisActive: (action) => snapshot.actionsActive.has(action),\n\t\tjustActivated: (action) =>\n\t\t\tsnapshot.actionsActive.has(action) && !snapshot.prevActionsActive.has(action),\n\t\tjustDeactivated: (action) =>\n\t\t\t!snapshot.actionsActive.has(action) && snapshot.prevActionsActive.has(action),\n\t};\n\n\tconst inputState: InputState<A> = {\n\t\tkeyboard,\n\t\tpointer,\n\t\tactions: actionState,\n\t\tsetActionMap(newMap) {\n\t\t\tactionMap = { ...newMap };\n\t\t},\n\t\tgetActionMap() {\n\t\t\treturn { ...actionMap } as ActionMap<A>;\n\t\t},\n\t};\n\n\t// DOM event handlers\n\tfunction onKeyDown(e: Event) {\n\t\tconst ke = e as KeyboardEvent;\n\t\tif (ke.repeat) return;\n\t\traw.keysDown.add(ke.key);\n\t\traw.keysPressed.push(ke.key);\n\t}\n\n\tfunction onKeyUp(e: Event) {\n\t\tconst ke = e as KeyboardEvent;\n\t\traw.keysDown.delete(ke.key);\n\t\traw.keysReleased.push(ke.key);\n\t}\n\n\tfunction onPointerDown(e: Event) {\n\t\tconst pe = e as PointerEvent;\n\t\traw.buttonsDown.add(pe.button);\n\t\traw.buttonsPressed.push(pe.button);\n\t}\n\n\tfunction onPointerMove(e: Event) {\n\t\tconst pe = e as PointerEvent;\n\t\traw.pointerX = pe.clientX;\n\t\traw.pointerY = pe.clientY;\n\t\traw.pointerMoved = true;\n\t}\n\n\tfunction onPointerUp(e: Event) {\n\t\tconst pe = e as PointerEvent;\n\t\traw.buttonsDown.delete(pe.button);\n\t\traw.buttonsReleased.push(pe.button);\n\t}\n\n\tfunction addListener(type: string, handler: (e: Event) => void) {\n\t\ttarget.addEventListener(type, handler);\n\t\tcleanupFns.push(() => { target.removeEventListener(type, handler); });\n\t}\n\n\treturn definePlugin<WorldConfigFrom<{}, {}, InputResourceTypes<A>>, EmptyConfig, 'input-state', G>({\n\t\tid: 'input',\n\t\tinstall(world) {\n\t\t\tworld.addResource('inputState', inputState);\n\n\t\t\tworld\n\t\t\t\t.addSystem('input-state')\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.setOnInitialize(() => {\n\t\t\t\t\taddListener('keydown', onKeyDown);\n\t\t\t\t\taddListener('keyup', onKeyUp);\n\t\t\t\t\taddListener('pointerdown', onPointerDown);\n\t\t\t\t\taddListener('pointermove', onPointerMove);\n\t\t\t\t\taddListener('pointerup', onPointerUp);\n\t\t\t\t})\n\t\t\t\t.setOnDetach(() => {\n\t\t\t\t\tfor (const cleanup of cleanupFns) {\n\t\t\t\t\t\tcleanup();\n\t\t\t\t\t}\n\t\t\t\t\tcleanupFns.length = 0;\n\t\t\t\t})\n\t\t\t\t.setProcess(() => {\n\t\t\t\t\tconst prevActionsActive = snapshot.actionsActive;\n\t\t\t\t\tsnapshot = snapshotRaw(raw, prevActionsActive, actionMap);\n\n\t\t\t\t\t// Update the exposed position/delta objects in-place\n\t\t\t\t\tposition.x = snapshot.pointerX;\n\t\t\t\t\tposition.y = snapshot.pointerY;\n\t\t\t\t\tdelta.x = snapshot.pointerDeltaX;\n\t\t\t\t\tdelta.y = snapshot.pointerDeltaY;\n\t\t\t\t});\n\t\t},\n\t});\n}\n"
5
+ "/**\n * Input Plugin for ECSpresso\n *\n * Provides frame-accurate keyboard, pointer (mouse + touch via PointerEvent),\n * and action mapping input. Resource-only plugin — input is polled via the\n * `inputState` resource. No ECS components or events.\n *\n * DOM events are accumulated between frames and snapshotted once per frame\n * in the system's process step, so all systems see consistent state.\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\n\n// ==================== Public Types ====================\n\nexport interface Vec2 {\n\tx: number;\n\ty: number;\n}\n\n// Key codes per the UI Events spec (KeyboardEvent.key values)\n// https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values\n\ntype LowercaseLetter =\n\t| 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm'\n\t| 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z';\n\ntype Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';\n\ntype Punctuation =\n\t| '`' | '~' | '!' | '@' | '#' | '$' | '%' | '^' | '&' | '*' | '(' | ')'\n\t| '-' | '_' | '=' | '+' | '[' | '{' | ']' | '}' | '\\\\' | '|'\n\t| ';' | ':' | \"'\" | '\"' | ',' | '<' | '.' | '>' | '/' | '?';\n\ntype ModifierKey =\n\t| 'Alt' | 'AltGraph' | 'CapsLock' | 'Control' | 'Fn' | 'FnLock'\n\t| 'Hyper' | 'Meta' | 'NumLock' | 'ScrollLock' | 'Shift'\n\t| 'Super' | 'Symbol' | 'SymbolLock';\n\ntype WhitespaceKey = 'Enter' | 'Tab' | ' ';\n\ntype NavigationKey =\n\t| `Arrow${'Down' | 'Left' | 'Right' | 'Up'}`\n\t| 'End' | 'Home' | 'PageDown' | 'PageUp';\n\ntype EditingKey =\n\t| 'Backspace' | 'Clear' | 'Copy' | 'CrSel' | 'Cut' | 'Delete'\n\t| 'EraseEof' | 'ExSel' | 'Insert' | 'Paste' | 'Redo' | 'Undo';\n\ntype UIKey =\n\t| 'Accept' | 'Again' | 'Attn' | 'Cancel' | 'ContextMenu' | 'Escape'\n\t| 'Execute' | 'Find' | 'Finish' | 'Help' | 'Pause' | 'Play'\n\t| 'Props' | 'Select' | 'ZoomIn' | 'ZoomOut';\n\ntype DeviceKey =\n\t| 'BrightnessDown' | 'BrightnessUp' | 'Eject' | 'Hibernate'\n\t| 'LogOff' | 'Power' | 'PowerOff' | 'PrintScreen' | 'Standby' | 'WakeUp';\n\ntype IMEKey =\n\t| 'AllCandidates' | 'Alphanumeric' | 'CodeInput' | 'Compose' | 'Convert'\n\t| 'FinalMode' | 'GroupFirst' | 'GroupLast' | 'GroupNext' | 'GroupPrevious'\n\t| 'ModeChange' | 'NextCandidate' | 'NonConvert' | 'PreviousCandidate'\n\t| 'Process' | 'SingleCandidate'\n\t| 'HangulMode' | 'HanjaMode' | 'JunjaMode'\n\t| 'Eisu' | 'Hankaku' | 'Hiragana' | 'HiraganaKatakana' | 'KanaMode'\n\t| 'KanjiMode' | 'Katakana' | 'Romaji' | 'Zenkaku' | 'ZenkakuHankaku';\n\ntype FunctionKey =\n\t| `F${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24}`\n\t| 'Soft1' | 'Soft2' | 'Soft3' | 'Soft4';\n\ntype PhoneKey =\n\t| 'AppSwitch' | 'Call' | 'Camera' | 'CameraFocus' | 'EndCall'\n\t| 'GoBack' | 'GoHome' | 'HeadsetHook' | 'LastNumberRedial'\n\t| 'Notification' | 'MannerMode' | 'VoiceDial';\n\ntype MultimediaKey =\n\t| 'ChannelDown' | 'ChannelUp'\n\t| `Media${\n\t\t'FastForward' | 'Pause' | 'Play' | 'PlayPause'\n\t\t| 'Record' | 'Rewind' | 'Stop' | 'TrackNext' | 'TrackPrevious'\n\t}`;\n\ntype AudioKey =\n\t| `Audio${\n\t\t'BalanceLeft' | 'BalanceRight' | 'BassDown' | 'BassBoostDown'\n\t\t| 'BassBoostToggle' | 'BassBoostUp' | 'BassUp' | 'FaderFront' | 'FaderRear'\n\t\t| 'SurroundModeNext' | 'TrebleDown' | 'TrebleUp'\n\t\t| 'VolumeDown' | 'VolumeMute' | 'VolumeUp'\n\t}`\n\t| `Microphone${'Toggle' | 'VolumeDown' | 'VolumeMute' | 'VolumeUp'}`;\n\ntype TVKey =\n\t| 'TV'\n\t| `TV${\n\t\t'3DMode' | 'AntennaCable' | 'AudioDescription' | 'AudioDescriptionMixDown'\n\t\t| 'AudioDescriptionMixUp' | 'ContentsMenu' | 'DataService' | 'Input'\n\t\t| 'InputComponent1' | 'InputComponent2' | 'InputComposite1' | 'InputComposite2'\n\t\t| 'InputHDMI1' | 'InputHDMI2' | 'InputHDMI3' | 'InputHDMI4' | 'InputVGA1'\n\t\t| 'MediaContext' | 'Network' | 'NumberEntry' | 'Power' | 'RadioService'\n\t\t| 'Satellite' | 'SatelliteBS' | 'SatelliteCS' | 'SatelliteToggle'\n\t\t| 'TerrestrialAnalog' | 'TerrestrialDigital' | 'Timer'\n\t}`;\n\ntype MediaControllerKey =\n\t| 'AVRInput' | 'AVRPower'\n\t| `Color${'F0Red' | 'F1Green' | 'F2Yellow' | 'F3Blue' | 'F4Grey' | 'F5Brown'}`\n\t| 'ClosedCaptionToggle' | 'Dimmer' | 'DisplaySwap' | 'DVR' | 'Exit'\n\t| `Favorite${'Clear' | 'Recall' | 'Store'}${0 | 1 | 2 | 3}`\n\t| 'Guide' | 'GuideNextDay' | 'GuidePreviousDay' | 'Info' | 'InstantReplay'\n\t| 'Link' | 'ListProgram' | 'LiveContent' | 'Lock'\n\t| `Media${\n\t\t'Apps' | 'AudioTrack' | 'Last' | 'SkipBackward'\n\t\t| 'SkipForward' | 'StepBackward' | 'StepForward' | 'TopMenu'\n\t}`\n\t| `Navigate${'In' | 'Next' | 'Out' | 'Previous'}`\n\t| 'NextFavoriteChannel' | 'NextUserProfile' | 'OnDemand' | 'Pairing'\n\t| `PinP${'Down' | 'Move' | 'Toggle' | 'Up'}`\n\t| `PlaySpeed${'Down' | 'Reset' | 'Up'}`\n\t| 'RandomToggle' | 'RcLowBattery' | 'RecordSpeedNext' | 'RfBypass'\n\t| 'ScanChannelsToggle' | 'ScreenModeNext' | 'Settings' | 'SplitScreenToggle'\n\t| 'STBInput' | 'STBPower' | 'Subtitle' | 'Teletext'\n\t| 'VideoModeNext' | 'Wink' | 'ZoomToggle';\n\ntype SpeechKey = 'SpeechCorrectionList' | 'SpeechInputToggle';\n\ntype DocumentKey =\n\t| 'Close' | 'New' | 'Open' | 'Print' | 'Save' | 'SpellCheck'\n\t| 'MailForward' | 'MailReply' | 'MailSend';\n\ntype LaunchKey = `Launch${\n\t| 'Calculator' | 'Calendar' | 'Contacts' | 'Mail' | 'MediaPlayer'\n\t| 'MusicPlayer' | 'MyComputer' | 'Phone' | 'ScreenSaver' | 'Spreadsheet'\n\t| 'WebBrowser' | 'WebCam' | 'WordProcessor'\n\t| `Application${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16}`\n}`;\n\ntype BrowserKey = `Browser${'Back' | 'Favorites' | 'Forward' | 'Home' | 'Refresh' | 'Search' | 'Stop'}`;\n\ntype NumpadKey = 'Decimal' | 'Key11' | 'Key12' | 'Multiply' | 'Add' | 'Divide' | 'Subtract' | 'Separator';\n\nexport type KeyCode =\n\t| LowercaseLetter | Uppercase<LowercaseLetter> | Digit | Punctuation\n\t| ModifierKey | WhitespaceKey | NavigationKey | EditingKey | UIKey | DeviceKey\n\t| IMEKey | FunctionKey | PhoneKey | MultimediaKey | AudioKey | TVKey\n\t| MediaControllerKey | SpeechKey | DocumentKey | LaunchKey | BrowserKey | NumpadKey\n\t| 'Unidentified' | 'Dead';\n\nexport interface KeyboardState {\n\tisDown(key: KeyCode): boolean;\n\tjustPressed(key: KeyCode): boolean;\n\tjustReleased(key: KeyCode): boolean;\n}\n\nexport interface PointerState {\n\treadonly position: Readonly<Vec2>;\n\treadonly delta: Readonly<Vec2>;\n\tisDown(button: number): boolean;\n\tjustPressed(button: number): boolean;\n\tjustReleased(button: number): boolean;\n}\n\nexport interface ActionState<A extends string = string> {\n\tisActive(action: A): boolean;\n\tjustActivated(action: A): boolean;\n\tjustDeactivated(action: A): boolean;\n}\n\nexport interface InputState<A extends string = string> {\n\treadonly keyboard: KeyboardState;\n\treadonly pointer: PointerState;\n\treadonly actions: ActionState<A>;\n\tsetActionMap(actions: ActionMap<A>): void;\n\tgetActionMap(): Readonly<ActionMap<A>>;\n}\n\nexport interface ActionBinding {\n\tkeys?: KeyCode[];\n\tbuttons?: number[];\n}\n\nexport type ActionMap<A extends string = string> = Record<A, ActionBinding>;\n\nexport interface InputResourceTypes<A extends string = string> {\n\tinputState: InputState<A>;\n}\n\nexport interface InputPluginOptions<A extends string = string, G extends string = 'input'> extends BasePluginOptions<G> {\n\t/** Initial action mappings */\n\tactions?: ActionMap<A>;\n\t/** EventTarget to attach listeners to (default: globalThis). Pass a custom target for testability. */\n\ttarget?: EventTarget;\n\t/**\n\t * Optional conversion from raw DOM client coordinates to the space `inputState.pointer.position` should report.\n\t * Renderer-agnostic: wire to `clientToLogical(...)` from renderer2D when using `screenScale`, or to a renderer-specific helper.\n\t * When omitted, pointer coords remain raw `clientX`/`clientY` (not canvas-relative).\n\t */\n\tcoordinateTransform?: (clientX: number, clientY: number) => { x: number; y: number };\n}\n\n// ==================== Helper Functions ====================\n\n/**\n * Create a single action binding.\n *\n * @param binding The binding configuration\n * @returns The same binding object\n */\nexport function createActionBinding(binding: ActionBinding): ActionBinding {\n\treturn binding;\n}\n\n// ==================== Internal Types ====================\n\ninterface RawInputState {\n\tkeysDown: Set<string>;\n\tkeysPressed: string[];\n\tkeysReleased: string[];\n\tbuttonsDown: Set<number>;\n\tbuttonsPressed: number[];\n\tbuttonsReleased: number[];\n\tpointerX: number;\n\tpointerY: number;\n\tpointerDeltaX: number;\n\tpointerDeltaY: number;\n\tlastPointerX: number;\n\tlastPointerY: number;\n\tpointerMoved: boolean;\n}\n\ninterface FrameSnapshot {\n\tkeysDown: ReadonlySet<string>;\n\tkeysPressed: ReadonlySet<string>;\n\tkeysReleased: ReadonlySet<string>;\n\tbuttonsDown: ReadonlySet<number>;\n\tbuttonsPressed: ReadonlySet<number>;\n\tbuttonsReleased: ReadonlySet<number>;\n\tpointerX: number;\n\tpointerY: number;\n\tpointerDeltaX: number;\n\tpointerDeltaY: number;\n\tactionsActive: ReadonlySet<string>;\n\tprevActionsActive: ReadonlySet<string>;\n}\n\n// ==================== Plugin Factory ====================\n\nfunction createRawInputState(): RawInputState {\n\treturn {\n\t\tkeysDown: new Set(),\n\t\tkeysPressed: [],\n\t\tkeysReleased: [],\n\t\tbuttonsDown: new Set(),\n\t\tbuttonsPressed: [],\n\t\tbuttonsReleased: [],\n\t\tpointerX: 0,\n\t\tpointerY: 0,\n\t\tpointerDeltaX: 0,\n\t\tpointerDeltaY: 0,\n\t\tlastPointerX: 0,\n\t\tlastPointerY: 0,\n\t\tpointerMoved: false,\n\t};\n}\n\nconst EMPTY_SET_STRING: ReadonlySet<string> = new Set<string>();\nconst EMPTY_SET_NUMBER: ReadonlySet<number> = new Set<number>();\n\nfunction createEmptySnapshot(): FrameSnapshot {\n\treturn {\n\t\tkeysDown: EMPTY_SET_STRING,\n\t\tkeysPressed: EMPTY_SET_STRING,\n\t\tkeysReleased: EMPTY_SET_STRING,\n\t\tbuttonsDown: EMPTY_SET_NUMBER,\n\t\tbuttonsPressed: EMPTY_SET_NUMBER,\n\t\tbuttonsReleased: EMPTY_SET_NUMBER,\n\t\tpointerX: 0,\n\t\tpointerY: 0,\n\t\tpointerDeltaX: 0,\n\t\tpointerDeltaY: 0,\n\t\tactionsActive: EMPTY_SET_STRING,\n\t\tprevActionsActive: EMPTY_SET_STRING,\n\t};\n}\n\nfunction computeActiveActions(\n\tactionMap: ActionMap,\n\tkeysDown: ReadonlySet<string>,\n\tbuttonsDown: ReadonlySet<number>,\n): Set<string> {\n\tconst active = new Set<string>();\n\tfor (const [name, binding] of Object.entries(actionMap)) {\n\t\tconst keyActive = binding.keys?.some((k) => keysDown.has(k)) ?? false;\n\t\tconst buttonActive = binding.buttons?.some((b) => buttonsDown.has(b)) ?? false;\n\t\tif (keyActive || buttonActive) {\n\t\t\tactive.add(name);\n\t\t}\n\t}\n\treturn active;\n}\n\nfunction snapshotRaw(raw: RawInputState, prevActionsActive: ReadonlySet<string>, actionMap: ActionMap): FrameSnapshot {\n\tconst keysDown = new Set(raw.keysDown);\n\tconst keysPressed = new Set(raw.keysPressed);\n\tconst keysReleased = new Set(raw.keysReleased);\n\tconst buttonsDown = new Set(raw.buttonsDown);\n\tconst buttonsPressed = new Set(raw.buttonsPressed);\n\tconst buttonsReleased = new Set(raw.buttonsReleased);\n\n\tconst pointerDeltaX = raw.pointerMoved ? raw.pointerX - raw.lastPointerX : 0;\n\tconst pointerDeltaY = raw.pointerMoved ? raw.pointerY - raw.lastPointerY : 0;\n\n\tconst actionsActive = computeActiveActions(actionMap, keysDown, buttonsDown);\n\n\tconst snapshot: FrameSnapshot = {\n\t\tkeysDown,\n\t\tkeysPressed,\n\t\tkeysReleased,\n\t\tbuttonsDown,\n\t\tbuttonsPressed,\n\t\tbuttonsReleased,\n\t\tpointerX: raw.pointerX,\n\t\tpointerY: raw.pointerY,\n\t\tpointerDeltaX,\n\t\tpointerDeltaY,\n\t\tactionsActive,\n\t\tprevActionsActive,\n\t};\n\n\t// Clear accumulation buffers\n\traw.keysPressed = [];\n\traw.keysReleased = [];\n\traw.buttonsPressed = [];\n\traw.buttonsReleased = [];\n\traw.lastPointerX = raw.pointerX;\n\traw.lastPointerY = raw.pointerY;\n\traw.pointerMoved = false;\n\n\treturn snapshot;\n}\n\n/**\n * Create an input plugin for ECSpresso.\n *\n * This plugin provides:\n * - Frame-accurate keyboard state (isDown, justPressed, justReleased)\n * - Pointer position/delta and button state (mouse + touch via PointerEvent)\n * - Named action mapping with runtime remapping\n * - Automatic listener cleanup on detach\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createInputPlugin({\n * actions: {\n * jump: { keys: [' ', 'ArrowUp'] },\n * shoot: { keys: ['z'], buttons: [0] },\n * },\n * }))\n * .build();\n *\n * // In a system:\n * const input = ecs.getResource('inputState');\n * if (input.actions.justActivated('jump')) { ... }\n * if (input.keyboard.isDown('ArrowRight')) { ... }\n * ```\n */\nexport function createInputPlugin<A extends string = string, G extends string = 'input'>(\n\toptions?: InputPluginOptions<A, G>\n) {\n\tconst {\n\t\tsystemGroup = 'input',\n\t\tpriority = 100,\n\t\tphase = 'preUpdate',\n\t\tactions: initialActions = {},\n\t\ttarget = globalThis,\n\t\tcoordinateTransform,\n\t} = options ?? {};\n\n\t// Closure state\n\tconst raw = createRawInputState();\n\tlet snapshot = createEmptySnapshot();\n\tlet actionMap: ActionMap = { ...initialActions };\n\tconst cleanupFns: Array<() => void> = [];\n\n\t// The position/delta objects exposed via the resource.\n\t// Updated in-place each frame to avoid allocations.\n\tconst position: Vec2 = { x: 0, y: 0 };\n\tconst delta: Vec2 = { x: 0, y: 0 };\n\n\t// Build the InputState resource that closes over snapshot\n\tconst keyboard: KeyboardState = {\n\t\tisDown: (key) => snapshot.keysDown.has(key),\n\t\tjustPressed: (key) => snapshot.keysPressed.has(key),\n\t\tjustReleased: (key) => snapshot.keysReleased.has(key),\n\t};\n\n\tconst pointer: PointerState = {\n\t\tposition,\n\t\tdelta,\n\t\tisDown: (button) => snapshot.buttonsDown.has(button),\n\t\tjustPressed: (button) => snapshot.buttonsPressed.has(button),\n\t\tjustReleased: (button) => snapshot.buttonsReleased.has(button),\n\t};\n\n\tconst actionState: ActionState<A> = {\n\t\tisActive: (action) => snapshot.actionsActive.has(action),\n\t\tjustActivated: (action) =>\n\t\t\tsnapshot.actionsActive.has(action) && !snapshot.prevActionsActive.has(action),\n\t\tjustDeactivated: (action) =>\n\t\t\t!snapshot.actionsActive.has(action) && snapshot.prevActionsActive.has(action),\n\t};\n\n\tconst inputState: InputState<A> = {\n\t\tkeyboard,\n\t\tpointer,\n\t\tactions: actionState,\n\t\tsetActionMap(newMap) {\n\t\t\tactionMap = { ...newMap };\n\t\t},\n\t\tgetActionMap() {\n\t\t\treturn { ...actionMap } as ActionMap<A>;\n\t\t},\n\t};\n\n\t// DOM event handlers\n\tfunction onKeyDown(e: Event) {\n\t\tconst ke = e as KeyboardEvent;\n\t\tif (ke.repeat) return;\n\t\traw.keysDown.add(ke.key);\n\t\traw.keysPressed.push(ke.key);\n\t}\n\n\tfunction onKeyUp(e: Event) {\n\t\tconst ke = e as KeyboardEvent;\n\t\traw.keysDown.delete(ke.key);\n\t\traw.keysReleased.push(ke.key);\n\t}\n\n\tfunction onPointerDown(e: Event) {\n\t\tconst pe = e as PointerEvent;\n\t\traw.buttonsDown.add(pe.button);\n\t\traw.buttonsPressed.push(pe.button);\n\t}\n\n\tfunction onPointerMove(e: Event) {\n\t\tconst pe = e as PointerEvent;\n\t\tif (coordinateTransform) {\n\t\t\tconst { x, y } = coordinateTransform(pe.clientX, pe.clientY);\n\t\t\traw.pointerX = x;\n\t\t\traw.pointerY = y;\n\t\t} else {\n\t\t\traw.pointerX = pe.clientX;\n\t\t\traw.pointerY = pe.clientY;\n\t\t}\n\t\traw.pointerMoved = true;\n\t}\n\n\tfunction onPointerUp(e: Event) {\n\t\tconst pe = e as PointerEvent;\n\t\traw.buttonsDown.delete(pe.button);\n\t\traw.buttonsReleased.push(pe.button);\n\t}\n\n\tfunction addListener(type: string, handler: (e: Event) => void) {\n\t\ttarget.addEventListener(type, handler);\n\t\tcleanupFns.push(() => { target.removeEventListener(type, handler); });\n\t}\n\n\treturn definePlugin('input')\n\t\t.withResourceTypes<InputResourceTypes<A>>()\n\t\t.withLabels<'input-state'>()\n\t\t.withGroups<G>()\n\t\t.install((world) => {\n\t\t\tworld.addResource('inputState', inputState);\n\n\t\t\tworld\n\t\t\t\t.addSystem('input-state')\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.setOnInitialize(() => {\n\t\t\t\t\taddListener('keydown', onKeyDown);\n\t\t\t\t\taddListener('keyup', onKeyUp);\n\t\t\t\t\taddListener('pointerdown', onPointerDown);\n\t\t\t\t\taddListener('pointermove', onPointerMove);\n\t\t\t\t\taddListener('pointerup', onPointerUp);\n\t\t\t\t})\n\t\t\t\t.setOnDetach(() => {\n\t\t\t\t\tfor (const cleanup of cleanupFns) {\n\t\t\t\t\t\tcleanup();\n\t\t\t\t\t}\n\t\t\t\t\tcleanupFns.length = 0;\n\t\t\t\t})\n\t\t\t\t.setProcess(() => {\n\t\t\t\t\tconst prevActionsActive = snapshot.actionsActive;\n\t\t\t\t\tsnapshot = snapshotRaw(raw, prevActionsActive, actionMap);\n\n\t\t\t\t\t// Update the exposed position/delta objects in-place\n\t\t\t\t\tposition.x = snapshot.pointerX;\n\t\t\t\t\tposition.y = snapshot.pointerY;\n\t\t\t\t\tdelta.x = snapshot.pointerDeltaX;\n\t\t\t\t\tdelta.y = snapshot.pointerDeltaY;\n\t\t\t\t});\n\t\t});\n}\n"
6
6
  ],
7
- "mappings": "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",
8
- "debugId": "513EE3E4CA561E4664756E2164756E21",
7
+ "mappings": "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",
8
+ "debugId": "E7F2EB68AF3B74CD64756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -9,7 +9,7 @@
9
9
  * (ParticleEffectConfig) + mutable per-entity state (ParticleEmitter) component,
10
10
  * side-storage Map for PixiJS objects, kit pattern for typed helpers.
11
11
  */
12
- import { type Plugin, type BasePluginOptions } from 'ecspresso';
12
+ import { type BasePluginOptions } from 'ecspresso';
13
13
  import type { BaseWorld } from 'ecspresso';
14
14
  import type { WorldConfigFrom } from '../type-utils';
15
15
  import type { TransformComponentTypes } from 'ecspresso/plugins/transform';
@@ -215,7 +215,7 @@ type ParticleRequires = WorldConfigFrom<TransformComponentTypes & {
215
215
  *
216
216
  * Renderer2D is a required dependency.
217
217
  */
218
- export declare function createParticlePlugin<G extends string = 'particles'>(options?: ParticlePluginOptions<G>): Plugin<WorldConfigFrom<ParticleComponentTypes>, ParticleRequires, ParticleLabels, G, never, 'particle-emitters'>;
218
+ export declare function createParticlePlugin<G extends string = 'particles'>(options?: ParticlePluginOptions<G>): import("ecspresso").Plugin<import("ecspresso").WithComponents<import("ecspresso").EmptyConfig, ParticleComponentTypes>, ParticleRequires, ParticleLabels, G, never, "particle-emitters">;
219
219
  /**
220
220
  * Get the runtime data for an emitter entity.
221
221
  * Useful for tests and advanced usage.
@@ -1,4 +1,4 @@
1
- var b=((k)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(k,{get:(A,F)=>(typeof require<"u"?require:A)[F]}):k)(function(k){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+k+'" is not supported')});import{definePlugin as E}from"ecspresso";function M(k){if(typeof k==="number")return k;let[A,F]=k;return A+Math.random()*(F-A)}function D(k,A,F){if(k===A)return k;let L=k>>16&255,j=k>>8&255,q=k&255,H=A>>16&255,$=A>>8&255,Z=A&255,Q=L+(H-L)*F|0,V=j+($-j)*F|0,J=q+(Z-q)*F|0;return Q<<16|V<<8|J}var O=Math.PI*2;function G(k){let A=k.startSize??1,F=k.startTint??16777215;return Object.freeze({maxParticles:k.maxParticles,texture:k.texture,spawnRate:k.spawnRate??10,burstCount:k.burstCount??0,duration:k.duration??-1,lifetime:k.lifetime??1,speed:k.speed??100,angle:k.angle??[0,O],emissionShape:k.emissionShape??"point",emissionRadius:k.emissionRadius??0,gravity:Object.freeze(k.gravity??{x:0,y:0}),startSize:A,endSize:k.endSize??A,startAlpha:k.startAlpha??1,endAlpha:k.endAlpha??0,startTint:F,endTint:k.endTint??F,startRotation:k.startRotation??0,rotationSpeed:k.rotationSpeed??0,blendMode:k.blendMode??"normal",worldSpace:k.worldSpace??!0})}function C(k,A){return{particleEmitter:{config:k,activeCount:0,spawnAccumulator:0,elapsed:0,playing:A?.playing??!0,pendingBurst:0,finished:!1,onComplete:A?.onComplete}}}function R(k,A,F){let L=k.getComponent(A,"particleEmitter");if(!L)return!1;return L.pendingBurst+=F??L.config.burstCount,k.markChanged(A,"particleEmitter"),!0}function w(k,A){let F=k.getComponent(A,"particleEmitter");if(!F)return!1;return F.playing=!1,!0}function v(k,A){let F=k.getComponent(A,"particleEmitter");if(!F)return!1;return F.playing=!0,!0}function W(k,A,F,L,j){k.active=!0;let q=M(A.lifetime);if(k.life=q,k.maxLife=q,A.emissionShape==="circle"&&A.emissionRadius>0){let Z=Math.random()*O,Q=Math.random()*A.emissionRadius;k.x=F+Math.cos(Z)*Q,k.y=L+Math.sin(Z)*Q}else k.x=F,k.y=L;let H=M(A.speed),$=M(A.angle)+j;k.vx=Math.cos($)*H,k.vy=Math.sin($)*H,k.startSize=M(A.startSize),k.endSize=M(A.endSize),k.size=k.startSize,k.startAlpha=M(A.startAlpha),k.endAlpha=M(A.endAlpha),k.alpha=k.startAlpha,k.tint=A.startTint,k.rotation=M(A.startRotation),k.rotationSpeed=M(A.rotationSpeed)}function X(k,A,F,L,j,q){let H=k.config;k.elapsed+=F;let $=H.duration>=0&&k.elapsed>=H.duration;if(k.playing&&!$&&H.spawnRate>0){k.spawnAccumulator+=H.spawnRate*F;let B=Math.floor(k.spawnAccumulator);k.spawnAccumulator-=B;for(let U=0;U<B;U++){if(k.activeCount>=H.maxParticles)break;let z=A.particles[k.activeCount];if(!z)break;W(z,H,L,j,q),k.activeCount++}}if(k.pendingBurst>0){let B=Math.min(k.pendingBurst,H.maxParticles-k.activeCount);for(let U=0;U<B;U++){let z=A.particles[k.activeCount];if(!z)break;W(z,H,L,j,q),k.activeCount++}k.pendingBurst-=B}let Q=H.gravity.x,V=H.gravity.y,J=Q!==0||V!==0,N=H.startTint!==H.endTint,K=0;while(K<k.activeCount){let B=A.particles[K];if(!B)break;if(B.life-=F,B.life<=0){if(k.activeCount--,K<k.activeCount){let z=A.particles[k.activeCount];if(z){A.particles[K]=z,A.particles[k.activeCount]=B;let h=A.pixiParticles[K];A.pixiParticles[K]=A.pixiParticles[k.activeCount],A.pixiParticles[k.activeCount]=h}}B.active=!1;continue}if(J)B.vx+=Q*F,B.vy+=V*F;B.x+=B.vx*F,B.y+=B.vy*F;let U=1-B.life/B.maxLife;if(B.size=B.startSize+(B.endSize-B.startSize)*U,B.alpha=B.startAlpha+(B.endAlpha-B.startAlpha)*U,N)B.tint=D(H.startTint,H.endTint,U);B.rotation+=B.rotationSpeed*F,K++}}function _(k){let A=Array(k);for(let F=0;F<k;F++)A[F]={active:!1,x:0,y:0,vx:0,vy:0,life:0,maxLife:0,size:0,startSize:0,endSize:0,alpha:0,startAlpha:0,endAlpha:0,tint:16777215,rotation:0,rotationSpeed:0};return A}var I={explosion(k,A){return G({maxParticles:50,texture:k,spawnRate:0,burstCount:30,duration:1,lifetime:[0.3,0.8],speed:[100,300],angle:[0,O],startSize:[0.5,1.5],endSize:[0.1,0.3],startAlpha:1,endAlpha:0,...A})},smoke(k,A){return G({maxParticles:60,texture:k,spawnRate:15,duration:-1,lifetime:[1,3],speed:[20,60],angle:[-Math.PI/2-0.3,-Math.PI/2+0.3],startSize:[0.3,0.6],endSize:[1,2],startAlpha:0.4,endAlpha:0,...A})},fire(k,A){return G({maxParticles:80,texture:k,spawnRate:30,duration:-1,lifetime:[0.3,1],speed:[40,120],angle:[-Math.PI/2-0.5,-Math.PI/2+0.5],startSize:[0.5,1],endSize:[0.1,0.3],startAlpha:1,endAlpha:0,startTint:16746496,endTint:16720384,blendMode:"add",...A})},sparkle(k,A){return G({maxParticles:30,texture:k,spawnRate:10,duration:-1,lifetime:[0.5,1.5],speed:[10,40],angle:[0,O],startSize:[0.2,0.8],endSize:[0.1,0.4],startAlpha:[0.5,1],endAlpha:0,...A})},trail(k,A){return G({maxParticles:40,texture:k,spawnRate:20,duration:-1,lifetime:[0.3,0.8],speed:0,startSize:[0.5,1],endSize:[0.05,0.2],startAlpha:0.8,endAlpha:0,...A})}};function P(k){let{systemGroup:A="particles",priority:F=0,phase:L="update"}=k??{},j=new Map;return E({id:"particles",install(q){q.registerRequired("particleEmitter","localTransform",()=>({x:0,y:0,rotation:0,scaleX:1,scaleY:1})),q.registerDispose("particleEmitter",({entityId:H})=>{let $=j.get(H);if($){let Z=$.pixiContainer;if(Z)Z.removeFromParent?.(),Z.destroy?.();j.delete(H)}}),q.addSystem("particle-update").setPriority(F).inPhase(L).inGroup(A).addQuery("emitters",{with:["particleEmitter"]}).setProcess(({queries:H,dt:$,ecs:Z})=>{for(let Q of H.emitters){let V=Q.components.particleEmitter,J=j.get(Q.id);if(!J)J={particles:_(V.config.maxParticles),pixiContainer:null,pixiParticles:[]},j.set(Q.id,J);let N=Z.getComponent(Q.id,"worldTransform"),K=N?.x??0,B=N?.y??0,U=N?.rotation??0;X(V,J,$,K,B,U);let z=V.config;if(z.duration>=0&&V.elapsed>=z.duration&&V.activeCount===0&&!V.finished){if(V.finished=!0,V.onComplete)V.onComplete({entityId:Q.id});Z.commands.removeComponent(Q.id,"particleEmitter")}}}),q.addSystem("particle-render-sync").setPriority(400).inPhase("render").inGroup(A).setOnInitialize(async(H)=>{let $=await import("pixi.js"),Z=$.ParticleContainer,Q=$.Particle,V=H.tryGetResource("rootContainer");H.addReactiveQuery("particle-emitters",{with:["particleEmitter"],onEnter:(J)=>{let K=J.components.particleEmitter.config,B=new Z({dynamicProperties:{position:!0,rotation:!0,color:!0,vertex:!0}});B.blendMode=K.blendMode;let U=[];for(let h=0;h<K.maxParticles;h++){let S=new Q({texture:K.texture});S.alpha=0,U.push(S),B.addParticle(S)}let z=_(K.maxParticles);if(V)if(H.getComponent(J.id,"renderLayer"))V.addChild(B);else V.addChild(B);j.set(J.id,{particles:z,pixiContainer:B,pixiParticles:U})},onExit:(J)=>{let N=j.get(J);if(N){let K=N.pixiContainer;if(K)K.removeFromParent?.(),K.destroy?.();j.delete(J)}}})}).setProcess(({ecs:H})=>{for(let[$,Z]of j){let Q=H.getComponent($,"particleEmitter");if(!Q)continue;let V=Q.config;if(!V.worldSpace){let J=H.getComponent($,"worldTransform");if(J){let N=Z.pixiContainer;N.position.set(J.x,J.y),N.rotation=J.rotation,N.scale.set(J.scaleX,J.scaleY)}}for(let J=0;J<Q.activeCount;J++){let N=Z.particles[J],K=Z.pixiParticles[J];if(!N||!K)continue;K.x=N.x,K.y=N.y,K.scaleX=N.size,K.scaleY=N.size,K.rotation=N.rotation,K.tint=N.tint,K.alpha=N.alpha}for(let J=Q.activeCount;J<V.maxParticles;J++){let N=Z.pixiParticles[J];if(N)N.alpha=0}}})}})}function x(k,A){return k.get(A)}export{w as stopEmitter,M as sampleRange,v as resumeEmitter,I as particlePresets,D as lerpTint,x as getEmitterData,G as defineParticleEffect,P as createParticlePlugin,C as createParticleEmitter,R as burstParticles};
1
+ var b=((k)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(k,{get:(A,F)=>(typeof require<"u"?require:A)[F]}):k)(function(k){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+k+'" is not supported')});import{definePlugin as E}from"ecspresso";function M(k){if(typeof k==="number")return k;let[A,F]=k;return A+Math.random()*(F-A)}function D(k,A,F){if(k===A)return k;let L=k>>16&255,j=k>>8&255,q=k&255,H=A>>16&255,$=A>>8&255,Z=A&255,Q=L+(H-L)*F|0,V=j+($-j)*F|0,J=q+(Z-q)*F|0;return Q<<16|V<<8|J}var O=Math.PI*2;function G(k){let A=k.startSize??1,F=k.startTint??16777215;return Object.freeze({maxParticles:k.maxParticles,texture:k.texture,spawnRate:k.spawnRate??10,burstCount:k.burstCount??0,duration:k.duration??-1,lifetime:k.lifetime??1,speed:k.speed??100,angle:k.angle??[0,O],emissionShape:k.emissionShape??"point",emissionRadius:k.emissionRadius??0,gravity:Object.freeze(k.gravity??{x:0,y:0}),startSize:A,endSize:k.endSize??A,startAlpha:k.startAlpha??1,endAlpha:k.endAlpha??0,startTint:F,endTint:k.endTint??F,startRotation:k.startRotation??0,rotationSpeed:k.rotationSpeed??0,blendMode:k.blendMode??"normal",worldSpace:k.worldSpace??!0})}function C(k,A){return{particleEmitter:{config:k,activeCount:0,spawnAccumulator:0,elapsed:0,playing:A?.playing??!0,pendingBurst:0,finished:!1,onComplete:A?.onComplete}}}function R(k,A,F){let L=k.getComponent(A,"particleEmitter");if(!L)return!1;return L.pendingBurst+=F??L.config.burstCount,k.markChanged(A,"particleEmitter"),!0}function w(k,A){let F=k.getComponent(A,"particleEmitter");if(!F)return!1;return F.playing=!1,!0}function v(k,A){let F=k.getComponent(A,"particleEmitter");if(!F)return!1;return F.playing=!0,!0}function W(k,A,F,L,j){k.active=!0;let q=M(A.lifetime);if(k.life=q,k.maxLife=q,A.emissionShape==="circle"&&A.emissionRadius>0){let Z=Math.random()*O,Q=Math.random()*A.emissionRadius;k.x=F+Math.cos(Z)*Q,k.y=L+Math.sin(Z)*Q}else k.x=F,k.y=L;let H=M(A.speed),$=M(A.angle)+j;k.vx=Math.cos($)*H,k.vy=Math.sin($)*H,k.startSize=M(A.startSize),k.endSize=M(A.endSize),k.size=k.startSize,k.startAlpha=M(A.startAlpha),k.endAlpha=M(A.endAlpha),k.alpha=k.startAlpha,k.tint=A.startTint,k.rotation=M(A.startRotation),k.rotationSpeed=M(A.rotationSpeed)}function X(k,A,F,L,j,q){let H=k.config;k.elapsed+=F;let $=H.duration>=0&&k.elapsed>=H.duration;if(k.playing&&!$&&H.spawnRate>0){k.spawnAccumulator+=H.spawnRate*F;let B=Math.floor(k.spawnAccumulator);k.spawnAccumulator-=B;for(let U=0;U<B;U++){if(k.activeCount>=H.maxParticles)break;let z=A.particles[k.activeCount];if(!z)break;W(z,H,L,j,q),k.activeCount++}}if(k.pendingBurst>0){let B=Math.min(k.pendingBurst,H.maxParticles-k.activeCount);for(let U=0;U<B;U++){let z=A.particles[k.activeCount];if(!z)break;W(z,H,L,j,q),k.activeCount++}k.pendingBurst-=B}let Q=H.gravity.x,V=H.gravity.y,J=Q!==0||V!==0,N=H.startTint!==H.endTint,K=0;while(K<k.activeCount){let B=A.particles[K];if(!B)break;if(B.life-=F,B.life<=0){if(k.activeCount--,K<k.activeCount){let z=A.particles[k.activeCount];if(z){A.particles[K]=z,A.particles[k.activeCount]=B;let h=A.pixiParticles[K];A.pixiParticles[K]=A.pixiParticles[k.activeCount],A.pixiParticles[k.activeCount]=h}}B.active=!1;continue}if(J)B.vx+=Q*F,B.vy+=V*F;B.x+=B.vx*F,B.y+=B.vy*F;let U=1-B.life/B.maxLife;if(B.size=B.startSize+(B.endSize-B.startSize)*U,B.alpha=B.startAlpha+(B.endAlpha-B.startAlpha)*U,N)B.tint=D(H.startTint,H.endTint,U);B.rotation+=B.rotationSpeed*F,K++}}function _(k){let A=Array(k);for(let F=0;F<k;F++)A[F]={active:!1,x:0,y:0,vx:0,vy:0,life:0,maxLife:0,size:0,startSize:0,endSize:0,alpha:0,startAlpha:0,endAlpha:0,tint:16777215,rotation:0,rotationSpeed:0};return A}var I={explosion(k,A){return G({maxParticles:50,texture:k,spawnRate:0,burstCount:30,duration:1,lifetime:[0.3,0.8],speed:[100,300],angle:[0,O],startSize:[0.5,1.5],endSize:[0.1,0.3],startAlpha:1,endAlpha:0,...A})},smoke(k,A){return G({maxParticles:60,texture:k,spawnRate:15,duration:-1,lifetime:[1,3],speed:[20,60],angle:[-Math.PI/2-0.3,-Math.PI/2+0.3],startSize:[0.3,0.6],endSize:[1,2],startAlpha:0.4,endAlpha:0,...A})},fire(k,A){return G({maxParticles:80,texture:k,spawnRate:30,duration:-1,lifetime:[0.3,1],speed:[40,120],angle:[-Math.PI/2-0.5,-Math.PI/2+0.5],startSize:[0.5,1],endSize:[0.1,0.3],startAlpha:1,endAlpha:0,startTint:16746496,endTint:16720384,blendMode:"add",...A})},sparkle(k,A){return G({maxParticles:30,texture:k,spawnRate:10,duration:-1,lifetime:[0.5,1.5],speed:[10,40],angle:[0,O],startSize:[0.2,0.8],endSize:[0.1,0.4],startAlpha:[0.5,1],endAlpha:0,...A})},trail(k,A){return G({maxParticles:40,texture:k,spawnRate:20,duration:-1,lifetime:[0.3,0.8],speed:0,startSize:[0.5,1],endSize:[0.05,0.2],startAlpha:0.8,endAlpha:0,...A})}};function P(k){let{systemGroup:A="particles",priority:F=0,phase:L="update"}=k??{},j=new Map;return E("particles").withComponentTypes().withLabels().withGroups().withReactiveQueryNames().requires().install((q)=>{q.registerRequired("particleEmitter","localTransform",()=>({x:0,y:0,rotation:0,scaleX:1,scaleY:1})),q.registerDispose("particleEmitter",({entityId:H})=>{let $=j.get(H);if($){let Z=$.pixiContainer;if(Z)Z.removeFromParent?.(),Z.destroy?.();j.delete(H)}}),q.addSystem("particle-update").setPriority(F).inPhase(L).inGroup(A).addQuery("emitters",{with:["particleEmitter"]}).setProcess(({queries:H,dt:$,ecs:Z})=>{for(let Q of H.emitters){let V=Q.components.particleEmitter,J=j.get(Q.id);if(!J)J={particles:_(V.config.maxParticles),pixiContainer:null,pixiParticles:[]},j.set(Q.id,J);let N=Z.getComponent(Q.id,"worldTransform"),K=N?.x??0,B=N?.y??0,U=N?.rotation??0;X(V,J,$,K,B,U);let z=V.config;if(z.duration>=0&&V.elapsed>=z.duration&&V.activeCount===0&&!V.finished){if(V.finished=!0,V.onComplete)V.onComplete({entityId:Q.id});Z.commands.removeComponent(Q.id,"particleEmitter")}}}),q.addSystem("particle-render-sync").setPriority(400).inPhase("render").inGroup(A).setOnInitialize(async(H)=>{let $=await import("pixi.js"),Z=$.ParticleContainer,Q=$.Particle,V=H.tryGetResource("rootContainer");H.addReactiveQuery("particle-emitters",{with:["particleEmitter"],onEnter:(J)=>{let K=J.components.particleEmitter.config,B=new Z({dynamicProperties:{position:!0,rotation:!0,color:!0,vertex:!0}});B.blendMode=K.blendMode;let U=[];for(let h=0;h<K.maxParticles;h++){let S=new Q({texture:K.texture});S.alpha=0,U.push(S),B.addParticle(S)}let z=_(K.maxParticles);if(V)if(H.getComponent(J.id,"renderLayer"))V.addChild(B);else V.addChild(B);j.set(J.id,{particles:z,pixiContainer:B,pixiParticles:U})},onExit:(J)=>{let N=j.get(J);if(N){let K=N.pixiContainer;if(K)K.removeFromParent?.(),K.destroy?.();j.delete(J)}}})}).setProcess(({ecs:H})=>{for(let[$,Z]of j){let Q=H.getComponent($,"particleEmitter");if(!Q)continue;let V=Q.config;if(!V.worldSpace){let J=H.getComponent($,"worldTransform");if(J){let N=Z.pixiContainer;N.position.set(J.x,J.y),N.rotation=J.rotation,N.scale.set(J.scaleX,J.scaleY)}}for(let J=0;J<Q.activeCount;J++){let N=Z.particles[J],K=Z.pixiParticles[J];if(!N||!K)continue;K.x=N.x,K.y=N.y,K.scaleX=N.size,K.scaleY=N.size,K.rotation=N.rotation,K.tint=N.tint,K.alpha=N.alpha}for(let J=Q.activeCount;J<V.maxParticles;J++){let N=Z.pixiParticles[J];if(N)N.alpha=0}}})})}function x(k,A){return k.get(A)}export{w as stopEmitter,M as sampleRange,v as resumeEmitter,I as particlePresets,D as lerpTint,x as getEmitterData,G as defineParticleEffect,P as createParticlePlugin,C as createParticleEmitter,R as burstParticles};
2
2
 
3
- //# debugId=84EAAF5C730AFBAB64756E2164756E21
3
+ //# debugId=3E22E46E1B886D0664756E2164756E21
4
4
  //# sourceMappingURL=particles.js.map
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/plugins/particles.ts"],
4
4
  "sourcesContent": [
5
- "/**\n * Particle System Plugin for ECSpresso\n *\n * High-performance particle system where particles live outside the ECS in\n * pre-allocated pools. Renders via PixiJS v8's ParticleContainer + Particle API.\n * Renderer2D is a required dependency.\n *\n * Follows the established plugin pattern: immutable shared config\n * (ParticleEffectConfig) + mutable per-entity state (ParticleEmitter) component,\n * side-storage Map for PixiJS objects, kit pattern for typed helpers.\n */\n\nimport { definePlugin, type Plugin, type BasePluginOptions } from 'ecspresso';\nimport type { BaseWorld } from 'ecspresso';\nimport type { WorldConfigFrom } from '../type-utils';\nimport type { TransformComponentTypes, LocalTransform } from 'ecspresso/plugins/transform';\n\n/** BaseWorld narrowed to particle components for typed access in helpers. */\ntype ParticleWorld = BaseWorld<ParticleComponentTypes>;\n\n// ==================== Value Types ====================\n\n/** Fixed value or random range [min, max] */\nexport type ParticleValue = number | readonly [number, number];\n\n/** Emission geometry */\nexport type EmissionShape = 'point' | 'circle';\n\n/** Blend modes for particle rendering */\nexport type ParticleBlendMode = 'normal' | 'add' | 'multiply' | 'screen';\n\n// ==================== Config Types ====================\n\n/**\n * User-facing config input for defining a particle effect.\n * All properties optional except maxParticles and texture.\n */\nexport interface ParticleEffectInput {\n\t/** Pool size — maximum simultaneous particles */\n\tmaxParticles: number;\n\t/** PixiJS Texture for particles */\n\ttexture: unknown;\n\t/** Particles per second (0 = burst-only, default: 10) */\n\tspawnRate?: number;\n\t/** Particles per burst (default: 0) */\n\tburstCount?: number;\n\t/** Emitter lifetime in seconds (-1 = infinite, default: -1) */\n\tduration?: number;\n\t/** Per-particle lifetime in seconds (default: 1) */\n\tlifetime?: ParticleValue;\n\t/** Initial speed in pixels/second (default: 100) */\n\tspeed?: ParticleValue;\n\t/** Emission direction in radians (default: [0, 2*PI]) */\n\tangle?: ParticleValue;\n\t/** Spawn geometry (default: 'point') */\n\temissionShape?: EmissionShape;\n\t/** Radius for 'circle' shape (default: 0) */\n\temissionRadius?: number;\n\t/** Acceleration in pixels/second^2 (default: {x: 0, y: 0}) */\n\tgravity?: { readonly x: number; readonly y: number };\n\t/** Initial scale (default: 1) */\n\tstartSize?: ParticleValue;\n\t/** Final scale (default: same as startSize) */\n\tendSize?: ParticleValue;\n\t/** Initial opacity (default: 1) */\n\tstartAlpha?: ParticleValue;\n\t/** Final opacity (default: 0) */\n\tendAlpha?: ParticleValue;\n\t/** Initial hex color (default: 0xffffff) */\n\tstartTint?: number;\n\t/** Final hex color (default: same as startTint) */\n\tendTint?: number;\n\t/** Initial rotation in radians (default: 0) */\n\tstartRotation?: ParticleValue;\n\t/** Rotation velocity in rad/s (default: 0) */\n\trotationSpeed?: ParticleValue;\n\t/** Blend mode (default: 'normal') */\n\tblendMode?: ParticleBlendMode;\n\t/** Particles in world coordinates (default: true) */\n\tworldSpace?: boolean;\n}\n\n/**\n * Frozen, fully-resolved particle effect config.\n * Output of defineParticleEffect.\n */\nexport interface ParticleEffectConfig {\n\treadonly maxParticles: number;\n\treadonly texture: unknown;\n\treadonly spawnRate: number;\n\treadonly burstCount: number;\n\treadonly duration: number;\n\treadonly lifetime: ParticleValue;\n\treadonly speed: ParticleValue;\n\treadonly angle: ParticleValue;\n\treadonly emissionShape: EmissionShape;\n\treadonly emissionRadius: number;\n\treadonly gravity: { readonly x: number; readonly y: number };\n\treadonly startSize: ParticleValue;\n\treadonly endSize: ParticleValue;\n\treadonly startAlpha: ParticleValue;\n\treadonly endAlpha: ParticleValue;\n\treadonly startTint: number;\n\treadonly endTint: number;\n\treadonly startRotation: ParticleValue;\n\treadonly rotationSpeed: ParticleValue;\n\treadonly blendMode: ParticleBlendMode;\n\treadonly worldSpace: boolean;\n}\n\n// ==================== Per-Particle Pool Element ====================\n\n/**\n * Mutable per-particle state. Pre-allocated, never GC'd.\n */\nexport interface ParticleState {\n\tactive: boolean;\n\tx: number;\n\ty: number;\n\tvx: number;\n\tvy: number;\n\tlife: number;\n\tmaxLife: number;\n\tsize: number;\n\tstartSize: number;\n\tendSize: number;\n\talpha: number;\n\tstartAlpha: number;\n\tendAlpha: number;\n\ttint: number;\n\trotation: number;\n\trotationSpeed: number;\n}\n\n// ==================== ECS Component ====================\n\n/**\n * Per-entity emitter state stored as an ECS component.\n */\nexport interface ParticleEmitter {\n\treadonly config: ParticleEffectConfig;\n\tactiveCount: number;\n\tspawnAccumulator: number;\n\telapsed: number;\n\tplaying: boolean;\n\tpendingBurst: number;\n\tfinished: boolean;\n\tonComplete?: (data: ParticleEmitterEventData) => void;\n}\n\n/**\n * Component types provided by the particle plugin.\n */\nexport interface ParticleComponentTypes {\n\tparticleEmitter: ParticleEmitter;\n}\n\n// ==================== Event Types ====================\n\n/**\n * Data published when an emitter completes.\n */\nexport interface ParticleEmitterEventData {\n\tentityId: number;\n}\n\n// ==================== Plugin Options ====================\n\nexport interface ParticlePluginOptions<G extends string = 'particles'> extends BasePluginOptions<G> {}\n\n// ==================== Pure Functions (Simulation Engine) ====================\n\n/**\n * Sample a ParticleValue: returns fixed value or random within [min, max].\n */\nexport function sampleRange(value: ParticleValue): number {\n\tif (typeof value === 'number') return value;\n\tconst [min, max] = value;\n\treturn min + Math.random() * (max - min);\n}\n\n/**\n * Linear interpolation between two hex colors (RGB channels).\n */\nexport function lerpTint(start: number, end: number, t: number): number {\n\tif (start === end) return start;\n\tconst sr = (start >> 16) & 0xff;\n\tconst sg = (start >> 8) & 0xff;\n\tconst sb = start & 0xff;\n\tconst er = (end >> 16) & 0xff;\n\tconst eg = (end >> 8) & 0xff;\n\tconst eb = end & 0xff;\n\tconst r = (sr + (er - sr) * t) | 0;\n\tconst g = (sg + (eg - sg) * t) | 0;\n\tconst b = (sb + (eb - sb) * t) | 0;\n\treturn (r << 16) | (g << 8) | b;\n}\n\n// ==================== Config Builder ====================\n\nconst TWO_PI = Math.PI * 2;\n\n/**\n * Define a particle effect config with defaults applied and frozen.\n */\nexport function defineParticleEffect(input: ParticleEffectInput): ParticleEffectConfig {\n\tconst startSize = input.startSize ?? 1;\n\tconst startTint = input.startTint ?? 0xffffff;\n\treturn Object.freeze({\n\t\tmaxParticles: input.maxParticles,\n\t\ttexture: input.texture,\n\t\tspawnRate: input.spawnRate ?? 10,\n\t\tburstCount: input.burstCount ?? 0,\n\t\tduration: input.duration ?? -1,\n\t\tlifetime: input.lifetime ?? 1,\n\t\tspeed: input.speed ?? 100,\n\t\tangle: input.angle ?? [0, TWO_PI] as const,\n\t\temissionShape: input.emissionShape ?? 'point',\n\t\temissionRadius: input.emissionRadius ?? 0,\n\t\tgravity: Object.freeze(input.gravity ?? { x: 0, y: 0 }),\n\t\tstartSize,\n\t\tendSize: input.endSize ?? startSize,\n\t\tstartAlpha: input.startAlpha ?? 1,\n\t\tendAlpha: input.endAlpha ?? 0,\n\t\tstartTint,\n\t\tendTint: input.endTint ?? startTint,\n\t\tstartRotation: input.startRotation ?? 0,\n\t\trotationSpeed: input.rotationSpeed ?? 0,\n\t\tblendMode: input.blendMode ?? 'normal',\n\t\tworldSpace: input.worldSpace ?? true,\n\t});\n}\n\n// ==================== Component Factory ====================\n\n/**\n * Create a particleEmitter component suitable for spreading into spawn().\n */\nexport function createParticleEmitter(\n\tconfig: ParticleEffectConfig,\n\toptions?: {\n\t\tplaying?: boolean;\n\t\tonComplete?: (data: ParticleEmitterEventData) => void;\n\t},\n): Pick<ParticleComponentTypes, 'particleEmitter'> {\n\treturn {\n\t\tparticleEmitter: {\n\t\t\tconfig,\n\t\t\tactiveCount: 0,\n\t\t\tspawnAccumulator: 0,\n\t\t\telapsed: 0,\n\t\t\tplaying: options?.playing ?? true,\n\t\t\tpendingBurst: 0,\n\t\t\tfinished: false,\n\t\t\tonComplete: options?.onComplete,\n\t\t},\n\t};\n}\n\n// ==================== Helper Functions ====================\n\n/**\n * Queue a burst of particles on an emitter.\n * Returns false if entity has no particleEmitter component.\n */\nexport function burstParticles(\n\tecs: ParticleWorld,\n\tentityId: number,\n\tcount?: number,\n): boolean {\n\tconst emitter = ecs.getComponent(entityId, 'particleEmitter');\n\tif (!emitter) return false;\n\temitter.pendingBurst += count ?? emitter.config.burstCount;\n\tecs.markChanged(entityId, 'particleEmitter');\n\treturn true;\n}\n\n/**\n * Stop an emitter from spawning new particles.\n * Existing particles continue their lifecycle.\n */\nexport function stopEmitter(\n\tecs: ParticleWorld,\n\tentityId: number,\n): boolean {\n\tconst emitter = ecs.getComponent(entityId, 'particleEmitter');\n\tif (!emitter) return false;\n\temitter.playing = false;\n\treturn true;\n}\n\n/**\n * Resume a stopped emitter.\n */\nexport function resumeEmitter(\n\tecs: ParticleWorld,\n\tentityId: number,\n): boolean {\n\tconst emitter = ecs.getComponent(entityId, 'particleEmitter');\n\tif (!emitter) return false;\n\temitter.playing = true;\n\treturn true;\n}\n\n// ==================== Side Storage ====================\n\n/**\n * Runtime data stored outside the ECS, keyed by entity ID.\n */\nexport interface EmitterRuntimeData {\n\tparticles: ParticleState[];\n\tpixiContainer: unknown;\n\tpixiParticles: unknown[];\n}\n\n// ==================== Spawn Logic ====================\n\nfunction spawnParticle(\n\tparticle: ParticleState,\n\tconfig: ParticleEffectConfig,\n\temitterX: number,\n\temitterY: number,\n\temitterRotation: number,\n): void {\n\tparticle.active = true;\n\tconst life = sampleRange(config.lifetime);\n\tparticle.life = life;\n\tparticle.maxLife = life;\n\n\t// Position from emission shape\n\tif (config.emissionShape === 'circle' && config.emissionRadius > 0) {\n\t\tconst angle = Math.random() * TWO_PI;\n\t\tconst radius = Math.random() * config.emissionRadius;\n\t\tparticle.x = emitterX + Math.cos(angle) * radius;\n\t\tparticle.y = emitterY + Math.sin(angle) * radius;\n\t} else {\n\t\tparticle.x = emitterX;\n\t\tparticle.y = emitterY;\n\t}\n\n\t// Velocity from speed + angle + emitter rotation\n\tconst speed = sampleRange(config.speed);\n\tconst angle = sampleRange(config.angle) + emitterRotation;\n\tparticle.vx = Math.cos(angle) * speed;\n\tparticle.vy = Math.sin(angle) * speed;\n\n\t// Visual properties\n\tparticle.startSize = sampleRange(config.startSize);\n\tparticle.endSize = sampleRange(config.endSize);\n\tparticle.size = particle.startSize;\n\tparticle.startAlpha = sampleRange(config.startAlpha);\n\tparticle.endAlpha = sampleRange(config.endAlpha);\n\tparticle.alpha = particle.startAlpha;\n\tparticle.tint = config.startTint;\n\tparticle.rotation = sampleRange(config.startRotation);\n\tparticle.rotationSpeed = sampleRange(config.rotationSpeed);\n}\n\n// ==================== Update Logic ====================\n\nfunction updateParticles(\n\temitter: ParticleEmitter,\n\tdata: EmitterRuntimeData,\n\tdt: number,\n\temitterX: number,\n\temitterY: number,\n\temitterRotation: number,\n): void {\n\tconst config = emitter.config;\n\n\t// Update emitter elapsed time\n\temitter.elapsed += dt;\n\n\t// Determine if spawning is allowed\n\tconst durationExpired = config.duration >= 0 && emitter.elapsed >= config.duration;\n\tconst canSpawn = emitter.playing && !durationExpired;\n\n\t// Continuous spawning\n\tif (canSpawn && config.spawnRate > 0) {\n\t\temitter.spawnAccumulator += config.spawnRate * dt;\n\t\tconst toSpawn = Math.floor(emitter.spawnAccumulator);\n\t\temitter.spawnAccumulator -= toSpawn;\n\n\t\tfor (let i = 0; i < toSpawn; i++) {\n\t\t\tif (emitter.activeCount >= config.maxParticles) break;\n\t\t\tconst particle = data.particles[emitter.activeCount];\n\t\t\tif (!particle) break;\n\t\t\tspawnParticle(particle, config, emitterX, emitterY, emitterRotation);\n\t\t\temitter.activeCount++;\n\t\t}\n\t}\n\n\t// Burst spawning\n\tif (emitter.pendingBurst > 0) {\n\t\tconst burstCount = Math.min(\n\t\t\temitter.pendingBurst,\n\t\t\tconfig.maxParticles - emitter.activeCount,\n\t\t);\n\t\tfor (let i = 0; i < burstCount; i++) {\n\t\t\tconst particle = data.particles[emitter.activeCount];\n\t\t\tif (!particle) break;\n\t\t\tspawnParticle(particle, config, emitterX, emitterY, emitterRotation);\n\t\t\temitter.activeCount++;\n\t\t}\n\t\temitter.pendingBurst -= burstCount;\n\t}\n\n\t// Update active particles\n\tconst gravityX = config.gravity.x;\n\tconst gravityY = config.gravity.y;\n\tconst hasGravity = gravityX !== 0 || gravityY !== 0;\n\tconst hasTintLerp = config.startTint !== config.endTint;\n\n\tlet i = 0;\n\twhile (i < emitter.activeCount) {\n\t\tconst p = data.particles[i];\n\t\tif (!p) break;\n\n\t\tp.life -= dt;\n\n\t\tif (p.life <= 0) {\n\t\t\t// Swap-and-pop: move last active particle to this slot\n\t\t\temitter.activeCount--;\n\t\t\tif (i < emitter.activeCount) {\n\t\t\t\tconst last = data.particles[emitter.activeCount];\n\t\t\t\tif (last) {\n\t\t\t\t\t// Copy last particle data to current slot\n\t\t\t\t\tdata.particles[i] = last;\n\t\t\t\t\tdata.particles[emitter.activeCount] = p;\n\t\t\t\t\t// Also swap PixiJS particle refs\n\t\t\t\t\tconst tmpPixi = data.pixiParticles[i];\n\t\t\t\t\tdata.pixiParticles[i] = data.pixiParticles[emitter.activeCount];\n\t\t\t\t\tdata.pixiParticles[emitter.activeCount] = tmpPixi;\n\t\t\t\t}\n\t\t\t}\n\t\t\tp.active = false;\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Physics\n\t\tif (hasGravity) {\n\t\t\tp.vx += gravityX * dt;\n\t\t\tp.vy += gravityY * dt;\n\t\t}\n\t\tp.x += p.vx * dt;\n\t\tp.y += p.vy * dt;\n\n\t\t// Interpolation\n\t\tconst t = 1 - p.life / p.maxLife;\n\t\tp.size = p.startSize + (p.endSize - p.startSize) * t;\n\t\tp.alpha = p.startAlpha + (p.endAlpha - p.startAlpha) * t;\n\n\t\tif (hasTintLerp) {\n\t\t\tp.tint = lerpTint(config.startTint, config.endTint, t);\n\t\t}\n\n\t\t// Rotation\n\t\tp.rotation += p.rotationSpeed * dt;\n\n\t\ti++;\n\t}\n}\n\n// ==================== Pool Allocation ====================\n\nfunction createParticlePool(maxParticles: number): ParticleState[] {\n\tconst pool: ParticleState[] = new Array(maxParticles);\n\tfor (let i = 0; i < maxParticles; i++) {\n\t\tpool[i] = {\n\t\t\tactive: false,\n\t\t\tx: 0, y: 0,\n\t\t\tvx: 0, vy: 0,\n\t\t\tlife: 0, maxLife: 0,\n\t\t\tsize: 0,\n\t\t\tstartSize: 0, endSize: 0,\n\t\t\talpha: 0,\n\t\t\tstartAlpha: 0, endAlpha: 0,\n\t\t\ttint: 0xffffff,\n\t\t\trotation: 0,\n\t\t\trotationSpeed: 0,\n\t\t};\n\t}\n\treturn pool;\n}\n\n// ==================== Presets ====================\n\nexport const particlePresets = {\n\texplosion(texture: unknown, overrides?: Partial<ParticleEffectInput>): ParticleEffectConfig {\n\t\treturn defineParticleEffect({\n\t\t\tmaxParticles: 50,\n\t\t\ttexture,\n\t\t\tspawnRate: 0,\n\t\t\tburstCount: 30,\n\t\t\tduration: 1,\n\t\t\tlifetime: [0.3, 0.8],\n\t\t\tspeed: [100, 300],\n\t\t\tangle: [0, TWO_PI],\n\t\t\tstartSize: [0.5, 1.5],\n\t\t\tendSize: [0.1, 0.3],\n\t\t\tstartAlpha: 1,\n\t\t\tendAlpha: 0,\n\t\t\t...overrides,\n\t\t});\n\t},\n\n\tsmoke(texture: unknown, overrides?: Partial<ParticleEffectInput>): ParticleEffectConfig {\n\t\treturn defineParticleEffect({\n\t\t\tmaxParticles: 60,\n\t\t\ttexture,\n\t\t\tspawnRate: 15,\n\t\t\tduration: -1,\n\t\t\tlifetime: [1, 3],\n\t\t\tspeed: [20, 60],\n\t\t\tangle: [-Math.PI / 2 - 0.3, -Math.PI / 2 + 0.3],\n\t\t\tstartSize: [0.3, 0.6],\n\t\t\tendSize: [1, 2],\n\t\t\tstartAlpha: 0.4,\n\t\t\tendAlpha: 0,\n\t\t\t...overrides,\n\t\t});\n\t},\n\n\tfire(texture: unknown, overrides?: Partial<ParticleEffectInput>): ParticleEffectConfig {\n\t\treturn defineParticleEffect({\n\t\t\tmaxParticles: 80,\n\t\t\ttexture,\n\t\t\tspawnRate: 30,\n\t\t\tduration: -1,\n\t\t\tlifetime: [0.3, 1],\n\t\t\tspeed: [40, 120],\n\t\t\tangle: [-Math.PI / 2 - 0.5, -Math.PI / 2 + 0.5],\n\t\t\tstartSize: [0.5, 1],\n\t\t\tendSize: [0.1, 0.3],\n\t\t\tstartAlpha: 1,\n\t\t\tendAlpha: 0,\n\t\t\tstartTint: 0xff8800,\n\t\t\tendTint: 0xff2200,\n\t\t\tblendMode: 'add',\n\t\t\t...overrides,\n\t\t});\n\t},\n\n\tsparkle(texture: unknown, overrides?: Partial<ParticleEffectInput>): ParticleEffectConfig {\n\t\treturn defineParticleEffect({\n\t\t\tmaxParticles: 30,\n\t\t\ttexture,\n\t\t\tspawnRate: 10,\n\t\t\tduration: -1,\n\t\t\tlifetime: [0.5, 1.5],\n\t\t\tspeed: [10, 40],\n\t\t\tangle: [0, TWO_PI],\n\t\t\tstartSize: [0.2, 0.8],\n\t\t\tendSize: [0.1, 0.4],\n\t\t\tstartAlpha: [0.5, 1],\n\t\t\tendAlpha: 0,\n\t\t\t...overrides,\n\t\t});\n\t},\n\n\ttrail(texture: unknown, overrides?: Partial<ParticleEffectInput>): ParticleEffectConfig {\n\t\treturn defineParticleEffect({\n\t\t\tmaxParticles: 40,\n\t\t\ttexture,\n\t\t\tspawnRate: 20,\n\t\t\tduration: -1,\n\t\t\tlifetime: [0.3, 0.8],\n\t\t\tspeed: 0,\n\t\t\tstartSize: [0.5, 1],\n\t\t\tendSize: [0.05, 0.2],\n\t\t\tstartAlpha: 0.8,\n\t\t\tendAlpha: 0,\n\t\t\t...overrides,\n\t\t});\n\t},\n} as const;\n\n// ==================== Plugin Factory ====================\n\ntype ParticleLabels = 'particle-update' | 'particle-render-sync';\n\ntype ParticleRequires = WorldConfigFrom<TransformComponentTypes & { renderLayer: string }>;\n\n/**\n * Create a particle system plugin for ECSpresso.\n *\n * Provides:\n * - Pre-allocated particle pools outside the entity system\n * - Continuous and burst emission modes\n * - Velocity, gravity, lifetime, interpolation (size, alpha, tint, rotation)\n * - World-space and local-space particle emission\n * - PixiJS ParticleContainer rendering (via renderer2D dependency)\n * - Presets for common effects (explosion, smoke, fire, sparkle, trail)\n *\n * Renderer2D is a required dependency.\n */\nexport function createParticlePlugin<\n\tG extends string = 'particles',\n>(\n\toptions?: ParticlePluginOptions<G>,\n): Plugin<WorldConfigFrom<ParticleComponentTypes>, ParticleRequires, ParticleLabels, G, never, 'particle-emitters'> {\n\tconst {\n\t\tsystemGroup = 'particles',\n\t\tpriority = 0,\n\t\tphase = 'update',\n\t} = options ?? {};\n\n\t// Side storage for runtime particle data\n\tconst emitterData = new Map<number, EmitterRuntimeData>();\n\n\treturn definePlugin<WorldConfigFrom<ParticleComponentTypes>, ParticleRequires, ParticleLabels, G, never, 'particle-emitters'>({\n\t\tid: 'particles',\n\t\tinstall(world) {\n\t\t\t// Required component: particleEmitter needs localTransform\n\t\t\tworld.registerRequired('particleEmitter', 'localTransform', (): LocalTransform => ({\n\t\t\t\tx: 0, y: 0, rotation: 0, scaleX: 1, scaleY: 1,\n\t\t\t}));\n\n\t\t\t// Dispose: clean up side storage when particleEmitter removed\n\t\t\tworld.registerDispose('particleEmitter', ({ entityId }: { value: ParticleEmitter; entityId: number }) => {\n\t\t\t\tconst data = emitterData.get(entityId);\n\t\t\t\tif (data) {\n\t\t\t\t\t// Remove PixiJS container from scene graph\n\t\t\t\t\tconst container = data.pixiContainer as { removeFromParent?: () => void; destroy?: () => void } | null;\n\t\t\t\t\tif (container) {\n\t\t\t\t\t\tcontainer.removeFromParent?.();\n\t\t\t\t\t\tcontainer.destroy?.();\n\t\t\t\t\t}\n\t\t\t\t\temitterData.delete(entityId);\n\t\t\t\t}\n\t\t\t});\n\n\t\t\t// ==================== Particle Update System ====================\n\t\t\tworld\n\t\t\t\t.addSystem('particle-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('emitters', {\n\t\t\t\t\twith: ['particleEmitter'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, dt, ecs }) => {\n\t\t\t\t\tfor (const entity of queries.emitters) {\n\t\t\t\t\t\tconst emitter = entity.components.particleEmitter;\n\n\t\t\t\t\t\t// Lazily create particle pool on first encounter\n\t\t\t\t\t\tlet data = emitterData.get(entity.id);\n\t\t\t\t\t\tif (!data) {\n\t\t\t\t\t\t\tdata = {\n\t\t\t\t\t\t\t\tparticles: createParticlePool(emitter.config.maxParticles),\n\t\t\t\t\t\t\t\tpixiContainer: null,\n\t\t\t\t\t\t\t\tpixiParticles: [],\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\temitterData.set(entity.id, data);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst worldTransform = ecs.getComponent(entity.id, 'worldTransform');\n\t\t\t\t\t\tconst ex = worldTransform?.x ?? 0;\n\t\t\t\t\t\tconst ey = worldTransform?.y ?? 0;\n\t\t\t\t\t\tconst erot = worldTransform?.rotation ?? 0;\n\n\t\t\t\t\t\tupdateParticles(emitter, data, dt, ex, ey, erot);\n\n\t\t\t\t\t\t// Check completion\n\t\t\t\t\t\tconst config = emitter.config;\n\t\t\t\t\t\tconst durationExpired = config.duration >= 0 && emitter.elapsed >= config.duration;\n\t\t\t\t\t\tif (durationExpired && emitter.activeCount === 0 && !emitter.finished) {\n\t\t\t\t\t\t\temitter.finished = true;\n\n\t\t\t\t\t\t\tif (emitter.onComplete) {\n\t\t\t\t\t\t\t\temitter.onComplete({ entityId: entity.id });\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tecs.commands.removeComponent(entity.id, 'particleEmitter');\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// ==================== Particle Render Sync System ====================\n\t\t\tworld\n\t\t\t\t.addSystem('particle-render-sync')\n\t\t\t\t.setPriority(400)\n\t\t\t\t.inPhase('render')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setOnInitialize(async (ecs) => {\n\t\t\t\t\t// Dynamic import PixiJS\n\t\t\t\t\tconst pixi = await import('pixi.js');\n\t\t\t\t\tconst ParticleContainerClass = pixi.ParticleContainer;\n\t\t\t\t\tconst ParticleClass = pixi.Particle;\n\n\t\t\t\t\t// Get root container\n\t\t\t\t\tconst rootContainer = ecs.tryGetResource<{ addChild(child: unknown): void }>('rootContainer');\n\n\t\t\t\t\t// Reactive query for particleEmitter component\n\t\t\t\t\tecs.addReactiveQuery('particle-emitters', {\n\t\t\t\t\t\twith: ['particleEmitter'],\n\t\t\t\t\t\tonEnter: (entity) => {\n\t\t\t\t\t\t\tconst emitter = entity.components.particleEmitter;\n\t\t\t\t\t\t\tconst config = emitter.config;\n\n\t\t\t\t\t\t\t// Create PixiJS ParticleContainer\n\t\t\t\t\t\t\tconst pixiContainer = new ParticleContainerClass({\n\t\t\t\t\t\t\t\tdynamicProperties: {\n\t\t\t\t\t\t\t\t\tposition: true,\n\t\t\t\t\t\t\t\t\trotation: true,\n\t\t\t\t\t\t\t\t\tcolor: true,\n\t\t\t\t\t\t\t\t\tvertex: true,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t});\n\n\t\t\t\t\t\t\t// Set blend mode\n\t\t\t\t\t\t\tpixiContainer.blendMode = config.blendMode;\n\n\t\t\t\t\t\t\t// Pre-allocate Particle objects\n\t\t\t\t\t\t\tconst pixiParticles: InstanceType<typeof ParticleClass>[] = [];\n\t\t\t\t\t\t\tfor (let i = 0; i < config.maxParticles; i++) {\n\t\t\t\t\t\t\t\tconst p = new ParticleClass({\n\t\t\t\t\t\t\t\t\ttexture: config.texture,\n\t\t\t\t\t\t\t\t} as ConstructorParameters<typeof ParticleClass>[0]);\n\t\t\t\t\t\t\t\tp.alpha = 0;\n\t\t\t\t\t\t\t\tpixiParticles.push(p);\n\t\t\t\t\t\t\t\tpixiContainer.addParticle(p);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Create pre-allocated pool\n\t\t\t\t\t\t\tconst particles = createParticlePool(config.maxParticles);\n\n\t\t\t\t\t\t\t// Add to scene (cross-plugin structural access for renderLayer)\n\t\t\t\t\t\t\tif (rootContainer) {\n\t\t\t\t\t\t\t\tconst layerName = ecs.getComponent(entity.id, 'renderLayer');\n\t\t\t\t\t\t\t\tif (layerName) {\n\t\t\t\t\t\t\t\t\t(rootContainer as { addChild(child: unknown): void }).addChild(pixiContainer);\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t(rootContainer as { addChild(child: unknown): void }).addChild(pixiContainer);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Store in side storage\n\t\t\t\t\t\t\temitterData.set(entity.id, {\n\t\t\t\t\t\t\t\tparticles,\n\t\t\t\t\t\t\t\tpixiContainer,\n\t\t\t\t\t\t\t\tpixiParticles,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t},\n\t\t\t\t\t\tonExit: (entityId) => {\n\t\t\t\t\t\t\tconst data = emitterData.get(entityId);\n\t\t\t\t\t\t\tif (data) {\n\t\t\t\t\t\t\t\tconst container = data.pixiContainer as { removeFromParent?: () => void; destroy?: () => void } | null;\n\t\t\t\t\t\t\t\tif (container) {\n\t\t\t\t\t\t\t\t\tcontainer.removeFromParent?.();\n\t\t\t\t\t\t\t\t\tcontainer.destroy?.();\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\temitterData.delete(entityId);\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\t\t.setProcess(({ ecs }) => {\n\t\t\t\t\t// Sync ParticleState -> PixiJS Particle properties\n\t\t\t\t\tfor (const [entityId, data] of emitterData) {\n\t\t\t\t\t\tconst emitter = ecs.getComponent(entityId, 'particleEmitter');\n\t\t\t\t\t\tif (!emitter) continue;\n\n\t\t\t\t\t\tconst config = emitter.config;\n\n\t\t\t\t\t\t// Local-space: sync container position to emitter's worldTransform\n\t\t\t\t\t\tif (!config.worldSpace) {\n\t\t\t\t\t\t\tconst wt = ecs.getComponent(entityId, 'worldTransform');\n\t\t\t\t\t\t\tif (wt) {\n\t\t\t\t\t\t\t\tconst container = data.pixiContainer as {\n\t\t\t\t\t\t\t\t\tposition: { set(x: number, y: number): void };\n\t\t\t\t\t\t\t\t\trotation: number;\n\t\t\t\t\t\t\t\t\tscale: { set(x: number, y: number): void };\n\t\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\t\tcontainer.position.set(wt.x, wt.y);\n\t\t\t\t\t\t\t\tcontainer.rotation = wt.rotation;\n\t\t\t\t\t\t\t\tcontainer.scale.set(wt.scaleX, wt.scaleY);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Sync active particles\n\t\t\t\t\t\tfor (let i = 0; i < emitter.activeCount; i++) {\n\t\t\t\t\t\t\tconst ps = data.particles[i];\n\t\t\t\t\t\t\tconst pp = data.pixiParticles[i] as {\n\t\t\t\t\t\t\t\tx: number;\n\t\t\t\t\t\t\t\ty: number;\n\t\t\t\t\t\t\t\tscaleX: number;\n\t\t\t\t\t\t\t\tscaleY: number;\n\t\t\t\t\t\t\t\trotation: number;\n\t\t\t\t\t\t\t\ttint: number;\n\t\t\t\t\t\t\t\talpha: number;\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\tif (!ps || !pp) continue;\n\t\t\t\t\t\t\tpp.x = ps.x;\n\t\t\t\t\t\t\tpp.y = ps.y;\n\t\t\t\t\t\t\tpp.scaleX = ps.size;\n\t\t\t\t\t\t\tpp.scaleY = ps.size;\n\t\t\t\t\t\t\tpp.rotation = ps.rotation;\n\t\t\t\t\t\t\tpp.tint = ps.tint;\n\t\t\t\t\t\t\tpp.alpha = ps.alpha;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Hide inactive particles\n\t\t\t\t\t\tfor (let i = emitter.activeCount; i < config.maxParticles; i++) {\n\t\t\t\t\t\t\tconst pp = data.pixiParticles[i] as { alpha: number } | undefined;\n\t\t\t\t\t\t\tif (pp) {\n\t\t\t\t\t\t\t\tpp.alpha = 0;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t},\n\t});\n}\n\n/**\n * Get the runtime data for an emitter entity.\n * Useful for tests and advanced usage.\n * @internal Exported for testing only.\n */\nexport function getEmitterData(\n\temitterDataMap: Map<number, EmitterRuntimeData>,\n\tentityId: number,\n): EmitterRuntimeData | undefined {\n\treturn emitterDataMap.get(entityId);\n}\n"
5
+ "/**\n * Particle System Plugin for ECSpresso\n *\n * High-performance particle system where particles live outside the ECS in\n * pre-allocated pools. Renders via PixiJS v8's ParticleContainer + Particle API.\n * Renderer2D is a required dependency.\n *\n * Follows the established plugin pattern: immutable shared config\n * (ParticleEffectConfig) + mutable per-entity state (ParticleEmitter) component,\n * side-storage Map for PixiJS objects, kit pattern for typed helpers.\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type { BaseWorld } from 'ecspresso';\nimport type { WorldConfigFrom } from '../type-utils';\nimport type { TransformComponentTypes, LocalTransform } from 'ecspresso/plugins/transform';\n\n/** BaseWorld narrowed to particle components for typed access in helpers. */\ntype ParticleWorld = BaseWorld<ParticleComponentTypes>;\n\n// ==================== Value Types ====================\n\n/** Fixed value or random range [min, max] */\nexport type ParticleValue = number | readonly [number, number];\n\n/** Emission geometry */\nexport type EmissionShape = 'point' | 'circle';\n\n/** Blend modes for particle rendering */\nexport type ParticleBlendMode = 'normal' | 'add' | 'multiply' | 'screen';\n\n// ==================== Config Types ====================\n\n/**\n * User-facing config input for defining a particle effect.\n * All properties optional except maxParticles and texture.\n */\nexport interface ParticleEffectInput {\n\t/** Pool size — maximum simultaneous particles */\n\tmaxParticles: number;\n\t/** PixiJS Texture for particles */\n\ttexture: unknown;\n\t/** Particles per second (0 = burst-only, default: 10) */\n\tspawnRate?: number;\n\t/** Particles per burst (default: 0) */\n\tburstCount?: number;\n\t/** Emitter lifetime in seconds (-1 = infinite, default: -1) */\n\tduration?: number;\n\t/** Per-particle lifetime in seconds (default: 1) */\n\tlifetime?: ParticleValue;\n\t/** Initial speed in pixels/second (default: 100) */\n\tspeed?: ParticleValue;\n\t/** Emission direction in radians (default: [0, 2*PI]) */\n\tangle?: ParticleValue;\n\t/** Spawn geometry (default: 'point') */\n\temissionShape?: EmissionShape;\n\t/** Radius for 'circle' shape (default: 0) */\n\temissionRadius?: number;\n\t/** Acceleration in pixels/second^2 (default: {x: 0, y: 0}) */\n\tgravity?: { readonly x: number; readonly y: number };\n\t/** Initial scale (default: 1) */\n\tstartSize?: ParticleValue;\n\t/** Final scale (default: same as startSize) */\n\tendSize?: ParticleValue;\n\t/** Initial opacity (default: 1) */\n\tstartAlpha?: ParticleValue;\n\t/** Final opacity (default: 0) */\n\tendAlpha?: ParticleValue;\n\t/** Initial hex color (default: 0xffffff) */\n\tstartTint?: number;\n\t/** Final hex color (default: same as startTint) */\n\tendTint?: number;\n\t/** Initial rotation in radians (default: 0) */\n\tstartRotation?: ParticleValue;\n\t/** Rotation velocity in rad/s (default: 0) */\n\trotationSpeed?: ParticleValue;\n\t/** Blend mode (default: 'normal') */\n\tblendMode?: ParticleBlendMode;\n\t/** Particles in world coordinates (default: true) */\n\tworldSpace?: boolean;\n}\n\n/**\n * Frozen, fully-resolved particle effect config.\n * Output of defineParticleEffect.\n */\nexport interface ParticleEffectConfig {\n\treadonly maxParticles: number;\n\treadonly texture: unknown;\n\treadonly spawnRate: number;\n\treadonly burstCount: number;\n\treadonly duration: number;\n\treadonly lifetime: ParticleValue;\n\treadonly speed: ParticleValue;\n\treadonly angle: ParticleValue;\n\treadonly emissionShape: EmissionShape;\n\treadonly emissionRadius: number;\n\treadonly gravity: { readonly x: number; readonly y: number };\n\treadonly startSize: ParticleValue;\n\treadonly endSize: ParticleValue;\n\treadonly startAlpha: ParticleValue;\n\treadonly endAlpha: ParticleValue;\n\treadonly startTint: number;\n\treadonly endTint: number;\n\treadonly startRotation: ParticleValue;\n\treadonly rotationSpeed: ParticleValue;\n\treadonly blendMode: ParticleBlendMode;\n\treadonly worldSpace: boolean;\n}\n\n// ==================== Per-Particle Pool Element ====================\n\n/**\n * Mutable per-particle state. Pre-allocated, never GC'd.\n */\nexport interface ParticleState {\n\tactive: boolean;\n\tx: number;\n\ty: number;\n\tvx: number;\n\tvy: number;\n\tlife: number;\n\tmaxLife: number;\n\tsize: number;\n\tstartSize: number;\n\tendSize: number;\n\talpha: number;\n\tstartAlpha: number;\n\tendAlpha: number;\n\ttint: number;\n\trotation: number;\n\trotationSpeed: number;\n}\n\n// ==================== ECS Component ====================\n\n/**\n * Per-entity emitter state stored as an ECS component.\n */\nexport interface ParticleEmitter {\n\treadonly config: ParticleEffectConfig;\n\tactiveCount: number;\n\tspawnAccumulator: number;\n\telapsed: number;\n\tplaying: boolean;\n\tpendingBurst: number;\n\tfinished: boolean;\n\tonComplete?: (data: ParticleEmitterEventData) => void;\n}\n\n/**\n * Component types provided by the particle plugin.\n */\nexport interface ParticleComponentTypes {\n\tparticleEmitter: ParticleEmitter;\n}\n\n// ==================== Event Types ====================\n\n/**\n * Data published when an emitter completes.\n */\nexport interface ParticleEmitterEventData {\n\tentityId: number;\n}\n\n// ==================== Plugin Options ====================\n\nexport interface ParticlePluginOptions<G extends string = 'particles'> extends BasePluginOptions<G> {}\n\n// ==================== Pure Functions (Simulation Engine) ====================\n\n/**\n * Sample a ParticleValue: returns fixed value or random within [min, max].\n */\nexport function sampleRange(value: ParticleValue): number {\n\tif (typeof value === 'number') return value;\n\tconst [min, max] = value;\n\treturn min + Math.random() * (max - min);\n}\n\n/**\n * Linear interpolation between two hex colors (RGB channels).\n */\nexport function lerpTint(start: number, end: number, t: number): number {\n\tif (start === end) return start;\n\tconst sr = (start >> 16) & 0xff;\n\tconst sg = (start >> 8) & 0xff;\n\tconst sb = start & 0xff;\n\tconst er = (end >> 16) & 0xff;\n\tconst eg = (end >> 8) & 0xff;\n\tconst eb = end & 0xff;\n\tconst r = (sr + (er - sr) * t) | 0;\n\tconst g = (sg + (eg - sg) * t) | 0;\n\tconst b = (sb + (eb - sb) * t) | 0;\n\treturn (r << 16) | (g << 8) | b;\n}\n\n// ==================== Config Builder ====================\n\nconst TWO_PI = Math.PI * 2;\n\n/**\n * Define a particle effect config with defaults applied and frozen.\n */\nexport function defineParticleEffect(input: ParticleEffectInput): ParticleEffectConfig {\n\tconst startSize = input.startSize ?? 1;\n\tconst startTint = input.startTint ?? 0xffffff;\n\treturn Object.freeze({\n\t\tmaxParticles: input.maxParticles,\n\t\ttexture: input.texture,\n\t\tspawnRate: input.spawnRate ?? 10,\n\t\tburstCount: input.burstCount ?? 0,\n\t\tduration: input.duration ?? -1,\n\t\tlifetime: input.lifetime ?? 1,\n\t\tspeed: input.speed ?? 100,\n\t\tangle: input.angle ?? [0, TWO_PI] as const,\n\t\temissionShape: input.emissionShape ?? 'point',\n\t\temissionRadius: input.emissionRadius ?? 0,\n\t\tgravity: Object.freeze(input.gravity ?? { x: 0, y: 0 }),\n\t\tstartSize,\n\t\tendSize: input.endSize ?? startSize,\n\t\tstartAlpha: input.startAlpha ?? 1,\n\t\tendAlpha: input.endAlpha ?? 0,\n\t\tstartTint,\n\t\tendTint: input.endTint ?? startTint,\n\t\tstartRotation: input.startRotation ?? 0,\n\t\trotationSpeed: input.rotationSpeed ?? 0,\n\t\tblendMode: input.blendMode ?? 'normal',\n\t\tworldSpace: input.worldSpace ?? true,\n\t});\n}\n\n// ==================== Component Factory ====================\n\n/**\n * Create a particleEmitter component suitable for spreading into spawn().\n */\nexport function createParticleEmitter(\n\tconfig: ParticleEffectConfig,\n\toptions?: {\n\t\tplaying?: boolean;\n\t\tonComplete?: (data: ParticleEmitterEventData) => void;\n\t},\n): Pick<ParticleComponentTypes, 'particleEmitter'> {\n\treturn {\n\t\tparticleEmitter: {\n\t\t\tconfig,\n\t\t\tactiveCount: 0,\n\t\t\tspawnAccumulator: 0,\n\t\t\telapsed: 0,\n\t\t\tplaying: options?.playing ?? true,\n\t\t\tpendingBurst: 0,\n\t\t\tfinished: false,\n\t\t\tonComplete: options?.onComplete,\n\t\t},\n\t};\n}\n\n// ==================== Helper Functions ====================\n\n/**\n * Queue a burst of particles on an emitter.\n * Returns false if entity has no particleEmitter component.\n */\nexport function burstParticles(\n\tecs: ParticleWorld,\n\tentityId: number,\n\tcount?: number,\n): boolean {\n\tconst emitter = ecs.getComponent(entityId, 'particleEmitter');\n\tif (!emitter) return false;\n\temitter.pendingBurst += count ?? emitter.config.burstCount;\n\tecs.markChanged(entityId, 'particleEmitter');\n\treturn true;\n}\n\n/**\n * Stop an emitter from spawning new particles.\n * Existing particles continue their lifecycle.\n */\nexport function stopEmitter(\n\tecs: ParticleWorld,\n\tentityId: number,\n): boolean {\n\tconst emitter = ecs.getComponent(entityId, 'particleEmitter');\n\tif (!emitter) return false;\n\temitter.playing = false;\n\treturn true;\n}\n\n/**\n * Resume a stopped emitter.\n */\nexport function resumeEmitter(\n\tecs: ParticleWorld,\n\tentityId: number,\n): boolean {\n\tconst emitter = ecs.getComponent(entityId, 'particleEmitter');\n\tif (!emitter) return false;\n\temitter.playing = true;\n\treturn true;\n}\n\n// ==================== Side Storage ====================\n\n/**\n * Runtime data stored outside the ECS, keyed by entity ID.\n */\nexport interface EmitterRuntimeData {\n\tparticles: ParticleState[];\n\tpixiContainer: unknown;\n\tpixiParticles: unknown[];\n}\n\n// ==================== Spawn Logic ====================\n\nfunction spawnParticle(\n\tparticle: ParticleState,\n\tconfig: ParticleEffectConfig,\n\temitterX: number,\n\temitterY: number,\n\temitterRotation: number,\n): void {\n\tparticle.active = true;\n\tconst life = sampleRange(config.lifetime);\n\tparticle.life = life;\n\tparticle.maxLife = life;\n\n\t// Position from emission shape\n\tif (config.emissionShape === 'circle' && config.emissionRadius > 0) {\n\t\tconst angle = Math.random() * TWO_PI;\n\t\tconst radius = Math.random() * config.emissionRadius;\n\t\tparticle.x = emitterX + Math.cos(angle) * radius;\n\t\tparticle.y = emitterY + Math.sin(angle) * radius;\n\t} else {\n\t\tparticle.x = emitterX;\n\t\tparticle.y = emitterY;\n\t}\n\n\t// Velocity from speed + angle + emitter rotation\n\tconst speed = sampleRange(config.speed);\n\tconst angle = sampleRange(config.angle) + emitterRotation;\n\tparticle.vx = Math.cos(angle) * speed;\n\tparticle.vy = Math.sin(angle) * speed;\n\n\t// Visual properties\n\tparticle.startSize = sampleRange(config.startSize);\n\tparticle.endSize = sampleRange(config.endSize);\n\tparticle.size = particle.startSize;\n\tparticle.startAlpha = sampleRange(config.startAlpha);\n\tparticle.endAlpha = sampleRange(config.endAlpha);\n\tparticle.alpha = particle.startAlpha;\n\tparticle.tint = config.startTint;\n\tparticle.rotation = sampleRange(config.startRotation);\n\tparticle.rotationSpeed = sampleRange(config.rotationSpeed);\n}\n\n// ==================== Update Logic ====================\n\nfunction updateParticles(\n\temitter: ParticleEmitter,\n\tdata: EmitterRuntimeData,\n\tdt: number,\n\temitterX: number,\n\temitterY: number,\n\temitterRotation: number,\n): void {\n\tconst config = emitter.config;\n\n\t// Update emitter elapsed time\n\temitter.elapsed += dt;\n\n\t// Determine if spawning is allowed\n\tconst durationExpired = config.duration >= 0 && emitter.elapsed >= config.duration;\n\tconst canSpawn = emitter.playing && !durationExpired;\n\n\t// Continuous spawning\n\tif (canSpawn && config.spawnRate > 0) {\n\t\temitter.spawnAccumulator += config.spawnRate * dt;\n\t\tconst toSpawn = Math.floor(emitter.spawnAccumulator);\n\t\temitter.spawnAccumulator -= toSpawn;\n\n\t\tfor (let i = 0; i < toSpawn; i++) {\n\t\t\tif (emitter.activeCount >= config.maxParticles) break;\n\t\t\tconst particle = data.particles[emitter.activeCount];\n\t\t\tif (!particle) break;\n\t\t\tspawnParticle(particle, config, emitterX, emitterY, emitterRotation);\n\t\t\temitter.activeCount++;\n\t\t}\n\t}\n\n\t// Burst spawning\n\tif (emitter.pendingBurst > 0) {\n\t\tconst burstCount = Math.min(\n\t\t\temitter.pendingBurst,\n\t\t\tconfig.maxParticles - emitter.activeCount,\n\t\t);\n\t\tfor (let i = 0; i < burstCount; i++) {\n\t\t\tconst particle = data.particles[emitter.activeCount];\n\t\t\tif (!particle) break;\n\t\t\tspawnParticle(particle, config, emitterX, emitterY, emitterRotation);\n\t\t\temitter.activeCount++;\n\t\t}\n\t\temitter.pendingBurst -= burstCount;\n\t}\n\n\t// Update active particles\n\tconst gravityX = config.gravity.x;\n\tconst gravityY = config.gravity.y;\n\tconst hasGravity = gravityX !== 0 || gravityY !== 0;\n\tconst hasTintLerp = config.startTint !== config.endTint;\n\n\tlet i = 0;\n\twhile (i < emitter.activeCount) {\n\t\tconst p = data.particles[i];\n\t\tif (!p) break;\n\n\t\tp.life -= dt;\n\n\t\tif (p.life <= 0) {\n\t\t\t// Swap-and-pop: move last active particle to this slot\n\t\t\temitter.activeCount--;\n\t\t\tif (i < emitter.activeCount) {\n\t\t\t\tconst last = data.particles[emitter.activeCount];\n\t\t\t\tif (last) {\n\t\t\t\t\t// Copy last particle data to current slot\n\t\t\t\t\tdata.particles[i] = last;\n\t\t\t\t\tdata.particles[emitter.activeCount] = p;\n\t\t\t\t\t// Also swap PixiJS particle refs\n\t\t\t\t\tconst tmpPixi = data.pixiParticles[i];\n\t\t\t\t\tdata.pixiParticles[i] = data.pixiParticles[emitter.activeCount];\n\t\t\t\t\tdata.pixiParticles[emitter.activeCount] = tmpPixi;\n\t\t\t\t}\n\t\t\t}\n\t\t\tp.active = false;\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Physics\n\t\tif (hasGravity) {\n\t\t\tp.vx += gravityX * dt;\n\t\t\tp.vy += gravityY * dt;\n\t\t}\n\t\tp.x += p.vx * dt;\n\t\tp.y += p.vy * dt;\n\n\t\t// Interpolation\n\t\tconst t = 1 - p.life / p.maxLife;\n\t\tp.size = p.startSize + (p.endSize - p.startSize) * t;\n\t\tp.alpha = p.startAlpha + (p.endAlpha - p.startAlpha) * t;\n\n\t\tif (hasTintLerp) {\n\t\t\tp.tint = lerpTint(config.startTint, config.endTint, t);\n\t\t}\n\n\t\t// Rotation\n\t\tp.rotation += p.rotationSpeed * dt;\n\n\t\ti++;\n\t}\n}\n\n// ==================== Pool Allocation ====================\n\nfunction createParticlePool(maxParticles: number): ParticleState[] {\n\tconst pool: ParticleState[] = new Array(maxParticles);\n\tfor (let i = 0; i < maxParticles; i++) {\n\t\tpool[i] = {\n\t\t\tactive: false,\n\t\t\tx: 0, y: 0,\n\t\t\tvx: 0, vy: 0,\n\t\t\tlife: 0, maxLife: 0,\n\t\t\tsize: 0,\n\t\t\tstartSize: 0, endSize: 0,\n\t\t\talpha: 0,\n\t\t\tstartAlpha: 0, endAlpha: 0,\n\t\t\ttint: 0xffffff,\n\t\t\trotation: 0,\n\t\t\trotationSpeed: 0,\n\t\t};\n\t}\n\treturn pool;\n}\n\n// ==================== Presets ====================\n\nexport const particlePresets = {\n\texplosion(texture: unknown, overrides?: Partial<ParticleEffectInput>): ParticleEffectConfig {\n\t\treturn defineParticleEffect({\n\t\t\tmaxParticles: 50,\n\t\t\ttexture,\n\t\t\tspawnRate: 0,\n\t\t\tburstCount: 30,\n\t\t\tduration: 1,\n\t\t\tlifetime: [0.3, 0.8],\n\t\t\tspeed: [100, 300],\n\t\t\tangle: [0, TWO_PI],\n\t\t\tstartSize: [0.5, 1.5],\n\t\t\tendSize: [0.1, 0.3],\n\t\t\tstartAlpha: 1,\n\t\t\tendAlpha: 0,\n\t\t\t...overrides,\n\t\t});\n\t},\n\n\tsmoke(texture: unknown, overrides?: Partial<ParticleEffectInput>): ParticleEffectConfig {\n\t\treturn defineParticleEffect({\n\t\t\tmaxParticles: 60,\n\t\t\ttexture,\n\t\t\tspawnRate: 15,\n\t\t\tduration: -1,\n\t\t\tlifetime: [1, 3],\n\t\t\tspeed: [20, 60],\n\t\t\tangle: [-Math.PI / 2 - 0.3, -Math.PI / 2 + 0.3],\n\t\t\tstartSize: [0.3, 0.6],\n\t\t\tendSize: [1, 2],\n\t\t\tstartAlpha: 0.4,\n\t\t\tendAlpha: 0,\n\t\t\t...overrides,\n\t\t});\n\t},\n\n\tfire(texture: unknown, overrides?: Partial<ParticleEffectInput>): ParticleEffectConfig {\n\t\treturn defineParticleEffect({\n\t\t\tmaxParticles: 80,\n\t\t\ttexture,\n\t\t\tspawnRate: 30,\n\t\t\tduration: -1,\n\t\t\tlifetime: [0.3, 1],\n\t\t\tspeed: [40, 120],\n\t\t\tangle: [-Math.PI / 2 - 0.5, -Math.PI / 2 + 0.5],\n\t\t\tstartSize: [0.5, 1],\n\t\t\tendSize: [0.1, 0.3],\n\t\t\tstartAlpha: 1,\n\t\t\tendAlpha: 0,\n\t\t\tstartTint: 0xff8800,\n\t\t\tendTint: 0xff2200,\n\t\t\tblendMode: 'add',\n\t\t\t...overrides,\n\t\t});\n\t},\n\n\tsparkle(texture: unknown, overrides?: Partial<ParticleEffectInput>): ParticleEffectConfig {\n\t\treturn defineParticleEffect({\n\t\t\tmaxParticles: 30,\n\t\t\ttexture,\n\t\t\tspawnRate: 10,\n\t\t\tduration: -1,\n\t\t\tlifetime: [0.5, 1.5],\n\t\t\tspeed: [10, 40],\n\t\t\tangle: [0, TWO_PI],\n\t\t\tstartSize: [0.2, 0.8],\n\t\t\tendSize: [0.1, 0.4],\n\t\t\tstartAlpha: [0.5, 1],\n\t\t\tendAlpha: 0,\n\t\t\t...overrides,\n\t\t});\n\t},\n\n\ttrail(texture: unknown, overrides?: Partial<ParticleEffectInput>): ParticleEffectConfig {\n\t\treturn defineParticleEffect({\n\t\t\tmaxParticles: 40,\n\t\t\ttexture,\n\t\t\tspawnRate: 20,\n\t\t\tduration: -1,\n\t\t\tlifetime: [0.3, 0.8],\n\t\t\tspeed: 0,\n\t\t\tstartSize: [0.5, 1],\n\t\t\tendSize: [0.05, 0.2],\n\t\t\tstartAlpha: 0.8,\n\t\t\tendAlpha: 0,\n\t\t\t...overrides,\n\t\t});\n\t},\n} as const;\n\n// ==================== Plugin Factory ====================\n\ntype ParticleLabels = 'particle-update' | 'particle-render-sync';\n\ntype ParticleRequires = WorldConfigFrom<TransformComponentTypes & { renderLayer: string }>;\n\n/**\n * Create a particle system plugin for ECSpresso.\n *\n * Provides:\n * - Pre-allocated particle pools outside the entity system\n * - Continuous and burst emission modes\n * - Velocity, gravity, lifetime, interpolation (size, alpha, tint, rotation)\n * - World-space and local-space particle emission\n * - PixiJS ParticleContainer rendering (via renderer2D dependency)\n * - Presets for common effects (explosion, smoke, fire, sparkle, trail)\n *\n * Renderer2D is a required dependency.\n */\nexport function createParticlePlugin<\n\tG extends string = 'particles',\n>(\n\toptions?: ParticlePluginOptions<G>,\n) {\n\tconst {\n\t\tsystemGroup = 'particles',\n\t\tpriority = 0,\n\t\tphase = 'update',\n\t} = options ?? {};\n\n\t// Side storage for runtime particle data\n\tconst emitterData = new Map<number, EmitterRuntimeData>();\n\n\treturn definePlugin('particles')\n\t\t.withComponentTypes<ParticleComponentTypes>()\n\t\t.withLabels<ParticleLabels>()\n\t\t.withGroups<G>()\n\t\t.withReactiveQueryNames<'particle-emitters'>()\n\t\t.requires<ParticleRequires>()\n\t\t.install((world) => {\n\t\t\t// Required component: particleEmitter needs localTransform\n\t\t\tworld.registerRequired('particleEmitter', 'localTransform', (): LocalTransform => ({\n\t\t\t\tx: 0, y: 0, rotation: 0, scaleX: 1, scaleY: 1,\n\t\t\t}));\n\n\t\t\t// Dispose: clean up side storage when particleEmitter removed\n\t\t\tworld.registerDispose('particleEmitter', ({ entityId }: { value: ParticleEmitter; entityId: number }) => {\n\t\t\t\tconst data = emitterData.get(entityId);\n\t\t\t\tif (data) {\n\t\t\t\t\t// Remove PixiJS container from scene graph\n\t\t\t\t\tconst container = data.pixiContainer as { removeFromParent?: () => void; destroy?: () => void } | null;\n\t\t\t\t\tif (container) {\n\t\t\t\t\t\tcontainer.removeFromParent?.();\n\t\t\t\t\t\tcontainer.destroy?.();\n\t\t\t\t\t}\n\t\t\t\t\temitterData.delete(entityId);\n\t\t\t\t}\n\t\t\t});\n\n\t\t\t// ==================== Particle Update System ====================\n\t\t\tworld\n\t\t\t\t.addSystem('particle-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('emitters', {\n\t\t\t\t\twith: ['particleEmitter'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, dt, ecs }) => {\n\t\t\t\t\tfor (const entity of queries.emitters) {\n\t\t\t\t\t\tconst emitter = entity.components.particleEmitter;\n\n\t\t\t\t\t\t// Lazily create particle pool on first encounter\n\t\t\t\t\t\tlet data = emitterData.get(entity.id);\n\t\t\t\t\t\tif (!data) {\n\t\t\t\t\t\t\tdata = {\n\t\t\t\t\t\t\t\tparticles: createParticlePool(emitter.config.maxParticles),\n\t\t\t\t\t\t\t\tpixiContainer: null,\n\t\t\t\t\t\t\t\tpixiParticles: [],\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\temitterData.set(entity.id, data);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst worldTransform = ecs.getComponent(entity.id, 'worldTransform');\n\t\t\t\t\t\tconst ex = worldTransform?.x ?? 0;\n\t\t\t\t\t\tconst ey = worldTransform?.y ?? 0;\n\t\t\t\t\t\tconst erot = worldTransform?.rotation ?? 0;\n\n\t\t\t\t\t\tupdateParticles(emitter, data, dt, ex, ey, erot);\n\n\t\t\t\t\t\t// Check completion\n\t\t\t\t\t\tconst config = emitter.config;\n\t\t\t\t\t\tconst durationExpired = config.duration >= 0 && emitter.elapsed >= config.duration;\n\t\t\t\t\t\tif (durationExpired && emitter.activeCount === 0 && !emitter.finished) {\n\t\t\t\t\t\t\temitter.finished = true;\n\n\t\t\t\t\t\t\tif (emitter.onComplete) {\n\t\t\t\t\t\t\t\temitter.onComplete({ entityId: entity.id });\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tecs.commands.removeComponent(entity.id, 'particleEmitter');\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// ==================== Particle Render Sync System ====================\n\t\t\tworld\n\t\t\t\t.addSystem('particle-render-sync')\n\t\t\t\t.setPriority(400)\n\t\t\t\t.inPhase('render')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setOnInitialize(async (ecs) => {\n\t\t\t\t\t// Dynamic import PixiJS\n\t\t\t\t\tconst pixi = await import('pixi.js');\n\t\t\t\t\tconst ParticleContainerClass = pixi.ParticleContainer;\n\t\t\t\t\tconst ParticleClass = pixi.Particle;\n\n\t\t\t\t\t// Get root container\n\t\t\t\t\tconst rootContainer = ecs.tryGetResource<{ addChild(child: unknown): void }>('rootContainer');\n\n\t\t\t\t\t// Reactive query for particleEmitter component\n\t\t\t\t\tecs.addReactiveQuery('particle-emitters', {\n\t\t\t\t\t\twith: ['particleEmitter'],\n\t\t\t\t\t\tonEnter: (entity) => {\n\t\t\t\t\t\t\tconst emitter = entity.components.particleEmitter;\n\t\t\t\t\t\t\tconst config = emitter.config;\n\n\t\t\t\t\t\t\t// Create PixiJS ParticleContainer\n\t\t\t\t\t\t\tconst pixiContainer = new ParticleContainerClass({\n\t\t\t\t\t\t\t\tdynamicProperties: {\n\t\t\t\t\t\t\t\t\tposition: true,\n\t\t\t\t\t\t\t\t\trotation: true,\n\t\t\t\t\t\t\t\t\tcolor: true,\n\t\t\t\t\t\t\t\t\tvertex: true,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t});\n\n\t\t\t\t\t\t\t// Set blend mode\n\t\t\t\t\t\t\tpixiContainer.blendMode = config.blendMode;\n\n\t\t\t\t\t\t\t// Pre-allocate Particle objects\n\t\t\t\t\t\t\tconst pixiParticles: InstanceType<typeof ParticleClass>[] = [];\n\t\t\t\t\t\t\tfor (let i = 0; i < config.maxParticles; i++) {\n\t\t\t\t\t\t\t\tconst p = new ParticleClass({\n\t\t\t\t\t\t\t\t\ttexture: config.texture,\n\t\t\t\t\t\t\t\t} as ConstructorParameters<typeof ParticleClass>[0]);\n\t\t\t\t\t\t\t\tp.alpha = 0;\n\t\t\t\t\t\t\t\tpixiParticles.push(p);\n\t\t\t\t\t\t\t\tpixiContainer.addParticle(p);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Create pre-allocated pool\n\t\t\t\t\t\t\tconst particles = createParticlePool(config.maxParticles);\n\n\t\t\t\t\t\t\t// Add to scene (cross-plugin structural access for renderLayer)\n\t\t\t\t\t\t\tif (rootContainer) {\n\t\t\t\t\t\t\t\tconst layerName = ecs.getComponent(entity.id, 'renderLayer');\n\t\t\t\t\t\t\t\tif (layerName) {\n\t\t\t\t\t\t\t\t\t(rootContainer as { addChild(child: unknown): void }).addChild(pixiContainer);\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t(rootContainer as { addChild(child: unknown): void }).addChild(pixiContainer);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Store in side storage\n\t\t\t\t\t\t\temitterData.set(entity.id, {\n\t\t\t\t\t\t\t\tparticles,\n\t\t\t\t\t\t\t\tpixiContainer,\n\t\t\t\t\t\t\t\tpixiParticles,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t},\n\t\t\t\t\t\tonExit: (entityId) => {\n\t\t\t\t\t\t\tconst data = emitterData.get(entityId);\n\t\t\t\t\t\t\tif (data) {\n\t\t\t\t\t\t\t\tconst container = data.pixiContainer as { removeFromParent?: () => void; destroy?: () => void } | null;\n\t\t\t\t\t\t\t\tif (container) {\n\t\t\t\t\t\t\t\t\tcontainer.removeFromParent?.();\n\t\t\t\t\t\t\t\t\tcontainer.destroy?.();\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\temitterData.delete(entityId);\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\t\t.setProcess(({ ecs }) => {\n\t\t\t\t\t// Sync ParticleState -> PixiJS Particle properties\n\t\t\t\t\tfor (const [entityId, data] of emitterData) {\n\t\t\t\t\t\tconst emitter = ecs.getComponent(entityId, 'particleEmitter');\n\t\t\t\t\t\tif (!emitter) continue;\n\n\t\t\t\t\t\tconst config = emitter.config;\n\n\t\t\t\t\t\t// Local-space: sync container position to emitter's worldTransform\n\t\t\t\t\t\tif (!config.worldSpace) {\n\t\t\t\t\t\t\tconst wt = ecs.getComponent(entityId, 'worldTransform');\n\t\t\t\t\t\t\tif (wt) {\n\t\t\t\t\t\t\t\tconst container = data.pixiContainer as {\n\t\t\t\t\t\t\t\t\tposition: { set(x: number, y: number): void };\n\t\t\t\t\t\t\t\t\trotation: number;\n\t\t\t\t\t\t\t\t\tscale: { set(x: number, y: number): void };\n\t\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\t\tcontainer.position.set(wt.x, wt.y);\n\t\t\t\t\t\t\t\tcontainer.rotation = wt.rotation;\n\t\t\t\t\t\t\t\tcontainer.scale.set(wt.scaleX, wt.scaleY);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Sync active particles\n\t\t\t\t\t\tfor (let i = 0; i < emitter.activeCount; i++) {\n\t\t\t\t\t\t\tconst ps = data.particles[i];\n\t\t\t\t\t\t\tconst pp = data.pixiParticles[i] as {\n\t\t\t\t\t\t\t\tx: number;\n\t\t\t\t\t\t\t\ty: number;\n\t\t\t\t\t\t\t\tscaleX: number;\n\t\t\t\t\t\t\t\tscaleY: number;\n\t\t\t\t\t\t\t\trotation: number;\n\t\t\t\t\t\t\t\ttint: number;\n\t\t\t\t\t\t\t\talpha: number;\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\tif (!ps || !pp) continue;\n\t\t\t\t\t\t\tpp.x = ps.x;\n\t\t\t\t\t\t\tpp.y = ps.y;\n\t\t\t\t\t\t\tpp.scaleX = ps.size;\n\t\t\t\t\t\t\tpp.scaleY = ps.size;\n\t\t\t\t\t\t\tpp.rotation = ps.rotation;\n\t\t\t\t\t\t\tpp.tint = ps.tint;\n\t\t\t\t\t\t\tpp.alpha = ps.alpha;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Hide inactive particles\n\t\t\t\t\t\tfor (let i = emitter.activeCount; i < config.maxParticles; i++) {\n\t\t\t\t\t\t\tconst pp = data.pixiParticles[i] as { alpha: number } | undefined;\n\t\t\t\t\t\t\tif (pp) {\n\t\t\t\t\t\t\t\tpp.alpha = 0;\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\n/**\n * Get the runtime data for an emitter entity.\n * Useful for tests and advanced usage.\n * @internal Exported for testing only.\n */\nexport function getEmitterData(\n\temitterDataMap: Map<number, EmitterRuntimeData>,\n\tentityId: number,\n): EmitterRuntimeData | undefined {\n\treturn emitterDataMap.get(entityId);\n}\n"
6
6
  ],
7
- "mappings": "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",
8
- "debugId": "84EAAF5C730AFBAB64756E2164756E21",
7
+ "mappings": "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",
8
+ "debugId": "3E22E46E1B886D0664756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -8,9 +8,7 @@
8
8
  * Has its own collision detection in fixedUpdate for physics response;
9
9
  * the existing collision plugin can still run in postUpdate for game logic events.
10
10
  */
11
- import { type Plugin } from 'ecspresso';
12
11
  import type { SystemPhase } from 'ecspresso';
13
- import type { WorldConfigFrom } from '../type-utils';
14
12
  import type { TransformComponentTypes, TransformWorldConfig } from './transform';
15
13
  import type { CollisionComponentTypes, LayerFactories } from './collision';
16
14
  import type { Vector2D } from 'ecspresso';
@@ -63,12 +61,17 @@ export interface Physics2DResourceTypes {
63
61
  }
64
62
  /**
65
63
  * Event emitted for each physics collision pair.
64
+ *
65
+ * Normal components are flattened (`normalX`/`normalY`) rather than nested
66
+ * in a `Vector2D` to avoid a per-event allocation in the physics hot path.
66
67
  */
67
68
  export interface Physics2DCollisionEvent {
68
69
  entityA: number;
69
70
  entityB: number;
70
- /** Unit normal pointing from A toward B */
71
- normal: Vector2D;
71
+ /** Unit normal X, pointing from A toward B */
72
+ normalX: number;
73
+ /** Unit normal Y, pointing from A toward B */
74
+ normalY: number;
72
75
  /** Penetration depth (positive) */
73
76
  depth: number;
74
77
  }
@@ -159,5 +162,5 @@ export declare function setVelocity(ecs: {
159
162
  type Physics2DProvides<L extends string = never> = Physics2DOwnComponentTypes & CollisionComponentTypes<L>;
160
163
  export declare function createPhysics2DPlugin<L extends string = never, G extends string = 'physics2D', CG extends string = never>(options?: Physics2DPluginOptions<G, CG> & {
161
164
  layers?: LayerFactories<Record<L, readonly string[]>>;
162
- }): Plugin<WorldConfigFrom<Physics2DProvides<L>, Physics2DEventTypes, Physics2DResourceTypes>, TransformWorldConfig, 'physics2D-integration' | 'physics2D-collision', G | CG>;
165
+ }): import("ecspresso").Plugin<import("ecspresso").WithResources<import("ecspresso").WithEvents<import("ecspresso").WithComponents<import("ecspresso").EmptyConfig, Physics2DProvides<L>>, Physics2DEventTypes>, Physics2DResourceTypes>, TransformWorldConfig, "physics2D-integration" | "physics2D-collision", G | CG, never, never>;
163
166
  export {};
@@ -1,4 +1,4 @@
1
- var x=((z)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(z,{get:(E,K)=>(typeof require<"u"?require:E)[K]}):z)(function(z){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+z+'" is not supported')});import{definePlugin as I}from"ecspresso";function q(z,E,K,Z,Q,O,J){if(!O&&!J)return null;let k={entityId:z,x:E,y:K,layer:Z,collidesWith:Q};if(O)k.x+=O.offsetX??0,k.y+=O.offsetY??0,k.aabb={halfWidth:O.width/2,halfHeight:O.height/2};if(J)k.x+=J.offsetX??0,k.y+=J.offsetY??0,k.circle={radius:J.radius};return k}function C(z){return z("spatialIndex")}function A(z,E,K,Z,Q,O,J,k){let _=Q-z,j=O-E,$=K+J-Math.abs(_),F=Z+k-Math.abs(j);if($<=0||F<=0)return null;if($<F)return{normalX:_>=0?1:-1,normalY:0,depth:$};return{normalX:0,normalY:j>=0?1:-1,depth:F}}function X(z,E,K,Z,Q,O){let J=Z-z,k=Q-E,_=J*J+k*k,j=K+O;if(_>=j*j)return null;let $=Math.sqrt(_);if($===0)return{normalX:1,normalY:0,depth:j};return{normalX:J/$,normalY:k/$,depth:j-$}}function P(z,E,K,Z,Q,O,J){let k=Math.max(z-K,Math.min(Q,z+K)),_=Math.max(E-Z,Math.min(O,E+Z)),j=Q-k,$=O-_,F=j*j+$*$;if(F>=J*J)return null;if(F===0){let W=Q-(z-K),L=z+K-Q,R=O-(E-Z),V=E+Z-O,U=Math.min(W,L,R,V);if(U===L)return{normalX:1,normalY:0,depth:L+J};if(U===W)return{normalX:-1,normalY:0,depth:W+J};if(U===V)return{normalX:0,normalY:1,depth:V+J};return{normalX:0,normalY:-1,depth:R+J}}let T=Math.sqrt(F);return{normalX:j/T,normalY:$/T,depth:J-T}}function M(z,E){if(z.aabb&&E.aabb)return A(z.x,z.y,z.aabb.halfWidth,z.aabb.halfHeight,E.x,E.y,E.aabb.halfWidth,E.aabb.halfHeight);if(z.circle&&E.circle)return X(z.x,z.y,z.circle.radius,E.x,E.y,E.circle.radius);if(z.aabb&&E.circle)return P(z.x,z.y,z.aabb.halfWidth,z.aabb.halfHeight,E.x,E.y,E.circle.radius);if(z.circle&&E.aabb){let K=P(E.x,E.y,E.aabb.halfWidth,E.aabb.halfHeight,z.x,z.y,z.circle.radius);if(!K)return null;return{normalX:-K.normalX,normalY:-K.normalY,depth:K.depth}}return null}var N=new Set;function S(z,E,K,Z){if(E)w(z,E,K,Z);else Y(z,K,Z)}function Y(z,E,K){for(let Z=0;Z<z.length;Z++){let Q=z[Z];if(!Q)continue;for(let O=Z+1;O<z.length;O++){let J=z[O];if(!J)continue;if(!Q.collidesWith.includes(J.layer)&&!J.collidesWith.includes(Q.layer))continue;let k=M(Q,J);if(!k)continue;E(Q,J,k,K)}}}function w(z,E,K,Z){let Q=new Map;for(let O=0;O<z.length;O++){let J=z[O];if(!J)continue;Q.set(J.entityId,J)}for(let O=0;O<z.length;O++){let J=z[O];if(!J)continue;let k=J.aabb?J.aabb.halfWidth:J.circle?J.circle.radius:0,_=J.aabb?J.aabb.halfHeight:J.circle?J.circle.radius:0;N.clear(),E.queryRectInto(J.x-k,J.y-_,J.x+k,J.y+_,N);for(let j of N){if(j<=J.entityId)continue;let $=Q.get(j);if(!$)continue;if(!J.collidesWith.includes($.layer)&&!$.collidesWith.includes(J.layer))continue;let F=M(J,$);if(!F)continue;K(J,$,F,Z)}}}function u(z,E){return{rigidBody:{type:z,mass:z==="static"?1/0:E?.mass??1,drag:E?.drag??0,restitution:E?.restitution??0,friction:E?.friction??0,gravityScale:E?.gravityScale??1},force:{x:0,y:0}}}function p(z,E){return{force:{x:z,y:E}}}function y(z,E,K,Z){let Q=z.getComponent(E,"force");if(!Q)return;Q.x+=K,Q.y+=Z}function b(z,E,K,Z){let Q=z.getComponent(E,"velocity"),O=z.getComponent(E,"rigidBody");if(!Q||!O)return;if(O.mass===1/0||O.mass===0)return;Q.x+=K/O.mass,Q.y+=Z/O.mass}function d(z,E,K,Z){let Q=z.getComponent(E,"velocity");if(!Q)return;Q.x=K,Q.y=Z}function B(z,E,K,Z){let Q=z.rigidBody.type==="dynamic"&&z.rigidBody.mass>0&&z.rigidBody.mass!==1/0?1/z.rigidBody.mass:0,O=E.rigidBody.type==="dynamic"&&E.rigidBody.mass>0&&E.rigidBody.mass!==1/0?1/E.rigidBody.mass:0,J=Q+O;if(J>0){let k=K.depth/J;if(Q>0){let F=Z.getComponent(z.entityId,"localTransform");if(!F)return;F.x-=k*Q*K.normalX,F.y-=k*Q*K.normalY,z.x=F.x,Z.markChanged(z.entityId,"localTransform")}if(O>0){let F=Z.getComponent(E.entityId,"localTransform");if(!F)return;F.x+=k*O*K.normalX,F.y+=k*O*K.normalY,Z.markChanged(E.entityId,"localTransform")}let _=E.velocity.x-z.velocity.x,j=E.velocity.y-z.velocity.y,$=_*K.normalX+j*K.normalY;if($<0){let T=-(1+Math.min(z.rigidBody.restitution,E.rigidBody.restitution))*$/J;z.velocity.x-=T*Q*K.normalX,z.velocity.y-=T*Q*K.normalY,E.velocity.x+=T*O*K.normalX,E.velocity.y+=T*O*K.normalY;let W=_-$*K.normalX,L=j-$*K.normalY,R=Math.sqrt(W*W+L*L);if(R>0.000001){let V=W/R,U=L/R,G=Math.sqrt(z.rigidBody.friction*E.rigidBody.friction)*Math.abs(T),H=Math.min(R/J,G);z.velocity.x+=H*Q*V,z.velocity.y+=H*Q*U,E.velocity.x-=H*O*V,E.velocity.y-=H*O*U}}Z.markChanged(z.entityId,"velocity"),Z.markChanged(E.entityId,"velocity")}Z.eventBus.publish("physicsCollision",{entityA:z.entityId,entityB:E.entityId,normal:{x:K.normalX,y:K.normalY},depth:K.depth})}function f(z,E,K,Z){B(z,E,K,Z)}function r(z){let{gravity:E={x:0,y:0},systemGroup:K="physics2D",collisionSystemGroup:Z,integrationPriority:Q=1000,collisionPriority:O=900,phase:J="fixedUpdate"}=z??{};return I({id:"physics2D",install(k){k.registerRequired("rigidBody","velocity",()=>({x:0,y:0})),k.registerRequired("rigidBody","force",()=>({x:0,y:0})),k.addResource("physicsConfig",{gravity:{x:E.x,y:E.y}}),k.addSystem("physics2D-integration").setPriority(Q).inPhase(J).inGroup(K).addQuery("bodies",{with:["localTransform","velocity","rigidBody","force"]}).setProcess(({queries:j,dt:$,ecs:F})=>{let T=F.getResource("physicsConfig"),W=T.gravity.x,L=T.gravity.y;for(let R of j.bodies){let{localTransform:V,velocity:U,rigidBody:D,force:G}=R.components;if(D.type==="static")continue;if(D.type==="dynamic"){if(U.x+=W*D.gravityScale*$,U.y+=L*D.gravityScale*$,D.mass>0&&D.mass!==1/0)U.x+=G.x/D.mass*$,U.y+=G.y/D.mass*$;if(D.drag>0){let H=Math.max(0,1-D.drag*$);U.x*=H,U.y*=H}}V.x+=U.x*$,V.y+=U.y*$,G.x=0,G.y=0,F.markChanged(R.id,"localTransform")}});let _=k.addSystem("physics2D-collision").setPriority(O).inPhase(J).inGroup(K);if(Z)_.inGroup(Z);_.addQuery("collidables",{with:["localTransform","rigidBody","velocity","collisionLayer"]}).setProcess(({queries:j,ecs:$})=>{let F=[];for(let W of j.collidables){let{localTransform:L,rigidBody:R,velocity:V,collisionLayer:U}=W.components,D=q(W.id,L.x,L.y,U.layer,U.collidesWith,$.getComponent(W.id,"aabbCollider"),$.getComponent(W.id,"circleCollider"));if(!D)continue;F.push(Object.assign(D,{rigidBody:R,velocity:V}))}let T=C($.tryGetResource.bind($));S(F,T,f,$)})}})}export{d as setVelocity,u as createRigidBody,r as createPhysics2DPlugin,p as createForce,b as applyImpulse,y as applyForce};
1
+ var y=((z)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(z,{get:(J,Q)=>(typeof require<"u"?require:J)[Q]}):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";var _={normalX:0,normalY:0,depth:0},Y=0;function A(z,J,Q,j,Z,O,$,K){if(z.entityId=J,z.layer=Z,z.collidesWith=O,$)return z.x=Q+($.offsetX??0),z.y=j+($.offsetY??0),z.shape=0,z.halfWidth=$.width/2,z.halfHeight=$.height/2,z.radius=0,!0;if(K)return z.x=Q+(K.offsetX??0),z.y=j+(K.offsetY??0),z.shape=1,z.halfWidth=0,z.halfHeight=0,z.radius=K.radius,!0;return!1}function M(z){return z("spatialIndex")}function B(z,J,Q,j,Z,O,$,K,U){let D=Z-z,V=O-J,E=Q+$-Math.abs(D),F=j+K-Math.abs(V);if(E<=0||F<=0)return!1;if(E<F)return U.normalX=D>=0?1:-1,U.normalY=0,U.depth=E,!0;return U.normalX=0,U.normalY=V>=0?1:-1,U.depth=F,!0}function x(z,J,Q,j,Z,O,$){let K=j-z,U=Z-J,D=K*K+U*U,V=Q+O;if(D>=V*V)return!1;let E=Math.sqrt(D);if(E===0)return $.normalX=1,$.normalY=0,$.depth=V,!0;return $.normalX=K/E,$.normalY=U/E,$.depth=V-E,!0}function C(z,J,Q,j,Z,O,$,K){let U=Math.max(z-Q,Math.min(Z,z+Q)),D=Math.max(J-j,Math.min(O,J+j)),V=Z-U,E=O-D,F=V*V+E*E;if(F>=$*$)return!1;if(F===0){let H=Z-(z-Q),W=z+Q-Z,N=O-(J-j),T=J+j-O,q=Math.min(H,W,N,T);if(q===W)return K.normalX=1,K.normalY=0,K.depth=W+$,!0;if(q===H)return K.normalX=-1,K.normalY=0,K.depth=H+$,!0;if(q===T)return K.normalX=0,K.normalY=1,K.depth=T+$,!0;return K.normalX=0,K.normalY=-1,K.depth=N+$,!0}let G=Math.sqrt(F);return K.normalX=V/G,K.normalY=E/G,K.depth=$-G,!0}function w(z,J,Q){if(z.shape===0&&J.shape===0)return B(z.x,z.y,z.halfWidth,z.halfHeight,J.x,J.y,J.halfWidth,J.halfHeight,Q);if(z.shape===1&&J.shape===1)return x(z.x,z.y,z.radius,J.x,J.y,J.radius,Q);if(z.shape===0&&J.shape===1)return C(z.x,z.y,z.halfWidth,z.halfHeight,J.x,J.y,J.radius,Q);if(!C(J.x,J.y,J.halfWidth,J.halfHeight,z.x,z.y,z.radius,Q))return!1;return Q.normalX=-Q.normalX,Q.normalY=-Q.normalY,!0}var S=new Set,X=!1,m=50;function I(z,J,Q,j,Z,O){if(j)v(z,J,Q,j,Z,O);else g(z,J,Z,O)}function g(z,J,Q,j){if(!X&&J>=m)X=!0,console.warn(`[ecspresso] Collision detection is using O(n²) brute force with ${J} colliders. For better performance, install createSpatialIndexPlugin() alongside your collision or physics2D plugin.`);for(let Z=0;Z<J;Z++){let O=z[Z];if(!O)continue;for(let $=Z+1;$<J;$++){let K=z[$];if(!K)continue;if(!O.collidesWith.includes(K.layer)&&!K.collidesWith.includes(O.layer))continue;if(!w(O,K,_))continue;Q(O,K,_,j)}}}function v(z,J,Q,j,Z,O){Q.clear();for(let $=0;$<J;$++){let K=z[$];if(!K)continue;Q.set(K.entityId,K)}for(let $=0;$<J;$++){let K=z[$];if(!K)continue;let U=K.shape===0?K.halfWidth:K.radius,D=K.shape===0?K.halfHeight:K.radius;S.clear(),j.queryRectInto(K.x-U,K.y-D,K.x+U,K.y+D,S);for(let V of S){if(V<=K.entityId)continue;let E=Q.get(V);if(!E)continue;if(!K.collidesWith.includes(E.layer)&&!E.collidesWith.includes(K.layer))continue;if(!w(K,E,_))continue;Z(K,E,_,O)}}}function l(z,J){return{rigidBody:{type:z,mass:z==="static"?1/0:J?.mass??1,drag:J?.drag??0,restitution:J?.restitution??0,friction:J?.friction??0,gravityScale:J?.gravityScale??1},force:{x:0,y:0}}}function s(z,J){return{force:{x:z,y:J}}}function n(z,J,Q,j){let Z=z.getComponent(J,"force");if(!Z)return;Z.x+=Q,Z.y+=j}function i(z,J,Q,j){let Z=z.getComponent(J,"velocity"),O=z.getComponent(J,"rigidBody");if(!Z||!O)return;if(O.mass===1/0||O.mass===0)return;Z.x+=Q/O.mass,Z.y+=j/O.mass}function c(z,J,Q,j){let Z=z.getComponent(J,"velocity");if(!Z)return;Z.x=Q,Z.y=j}function h(z,J,Q,j){let Z=z.rigidBody.type==="dynamic"&&z.rigidBody.mass>0&&z.rigidBody.mass!==1/0?1/z.rigidBody.mass:0,O=J.rigidBody.type==="dynamic"&&J.rigidBody.mass>0&&J.rigidBody.mass!==1/0?1/J.rigidBody.mass:0,$=Z+O;if($>0){let K=Q.depth/$;if(Z>0){let E=j.getComponent(z.entityId,"localTransform");if(!E)return;E.x-=K*Z*Q.normalX,E.y-=K*Z*Q.normalY,z.x=E.x,j.markChanged(z.entityId,"localTransform")}if(O>0){let E=j.getComponent(J.entityId,"localTransform");if(!E)return;E.x+=K*O*Q.normalX,E.y+=K*O*Q.normalY,j.markChanged(J.entityId,"localTransform")}let U=J.velocity.x-z.velocity.x,D=J.velocity.y-z.velocity.y,V=U*Q.normalX+D*Q.normalY;if(V<0){let F=-(1+Math.min(z.rigidBody.restitution,J.rigidBody.restitution))*V/$;z.velocity.x-=F*Z*Q.normalX,z.velocity.y-=F*Z*Q.normalY,J.velocity.x+=F*O*Q.normalX,J.velocity.y+=F*O*Q.normalY;let G=U-V*Q.normalX,H=D-V*Q.normalY,W=Math.sqrt(G*G+H*H);if(W>0.000001){let N=G/W,T=H/W,L=Math.sqrt(z.rigidBody.friction*J.rigidBody.friction)*Math.abs(F),k=Math.min(W/$,L);z.velocity.x+=k*Z*N,z.velocity.y+=k*Z*T,J.velocity.x-=k*O*N,J.velocity.y-=k*O*T}}j.markChanged(z.entityId,"velocity"),j.markChanged(J.entityId,"velocity")}j.eventBus.publish("physicsCollision",{entityA:z.entityId,entityB:J.entityId,normalX:Q.normalX,normalY:Q.normalY,depth:Q.depth})}function p(z,J,Q,j){h(z,J,Q,j)}function e(z){let{gravity:J={x:0,y:0},systemGroup:Q="physics2D",collisionSystemGroup:j,integrationPriority:Z=1000,collisionPriority:O=900,phase:$="fixedUpdate"}=z??{};return f("physics2D").withComponentTypes().withEventTypes().withResourceTypes().withLabels().withGroups().requires().install((K)=>{K.registerRequired("rigidBody","velocity",()=>({x:0,y:0})),K.registerRequired("rigidBody","force",()=>({x:0,y:0})),K.addResource("physicsConfig",{gravity:{x:J.x,y:J.y}}),K.addSystem("physics2D-integration").setPriority(Z).inPhase($).inGroup(Q).addQuery("bodies",{with:["localTransform","velocity","rigidBody","force"]}).setProcess(({queries:E,dt:F,ecs:G})=>{let H=G.getResource("physicsConfig"),W=H.gravity.x,N=H.gravity.y;for(let T of E.bodies){let{localTransform:q,velocity:L,rigidBody:k,force:P}=T.components;if(k.type==="static")continue;if(k.type==="dynamic"){if(L.x+=W*k.gravityScale*F,L.y+=N*k.gravityScale*F,k.mass>0&&k.mass!==1/0)L.x+=P.x/k.mass*F,L.y+=P.y/k.mass*F;if(k.drag>0){let R=Math.max(0,1-k.drag*F);L.x*=R,L.y*=R}}q.x+=L.x*F,q.y+=L.y*F,P.x=0,P.y=0,G.markChanged(T.id,"localTransform")}});let U=K.addSystem("physics2D-collision").setPriority(O).inPhase($).inGroup(Q);if(j)U.inGroup(j);let D=[],V=new Map;U.addQuery("collidables",{with:["localTransform","rigidBody","velocity","collisionLayer"]}).setProcess(({queries:E,ecs:F})=>{let G=0;for(let W of E.collidables){let{localTransform:N,rigidBody:T,velocity:q,collisionLayer:L}=W.components,k=F.getComponent(W.id,"aabbCollider"),P=F.getComponent(W.id,"circleCollider");if(!k&&!P)continue;let R=D[G];if(!R)R={entityId:W.id,x:N.x,y:N.y,layer:L.layer,collidesWith:L.collidesWith,shape:Y,halfWidth:0,halfHeight:0,radius:0,rigidBody:T,velocity:q},D[G]=R;else R.rigidBody=T,R.velocity=q;if(!A(R,W.id,N.x,N.y,L.layer,L.collidesWith,k,P))continue;G++}let H=M(F.tryGetResource.bind(F));I(D,G,V,H,p,F)})})}export{c as setVelocity,l as createRigidBody,e as createPhysics2DPlugin,s as createForce,i as applyImpulse,n as applyForce};
2
2
 
3
- //# debugId=F2CFD17EA992232B64756E2164756E21
3
+ //# debugId=663A97FBA0B9DDC264756E2164756E21
4
4
  //# sourceMappingURL=physics2D.js.map