ecspresso 0.17.1 → 0.19.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 (57) hide show
  1. package/CHANGELOG.md +44 -1
  2. package/dist/asset-manager.d.ts +8 -8
  3. package/dist/asset-types.d.ts +9 -5
  4. package/dist/ecspresso-builder.d.ts +5 -5
  5. package/dist/ecspresso.d.ts +13 -1
  6. package/dist/index.d.ts +3 -3
  7. package/dist/index.js +2 -2
  8. package/dist/index.js.map +10 -10
  9. package/dist/plugins/ai/behavior-tree.d.ts +2 -2
  10. package/dist/plugins/ai/behavior-tree.js.map +2 -2
  11. package/dist/plugins/ai/detection.d.ts +3 -3
  12. package/dist/plugins/ai/detection.js.map +2 -2
  13. package/dist/plugins/ai/flocking.d.ts +3 -3
  14. package/dist/plugins/ai/flocking.js.map +1 -1
  15. package/dist/plugins/ai/pathfinding.d.ts +3 -4
  16. package/dist/plugins/ai/pathfinding.js.map +2 -2
  17. package/dist/plugins/audio/audio.js +2 -2
  18. package/dist/plugins/audio/audio.js.map +3 -3
  19. package/dist/plugins/combat/health.d.ts +2 -2
  20. package/dist/plugins/combat/health.js.map +2 -2
  21. package/dist/plugins/combat/projectile.d.ts +3 -3
  22. package/dist/plugins/combat/projectile.js.map +2 -2
  23. package/dist/plugins/input/selection.d.ts +3 -3
  24. package/dist/plugins/input/selection.js.map +3 -3
  25. package/dist/plugins/isometric/depth-sort.d.ts +2 -2
  26. package/dist/plugins/isometric/depth-sort.js.map +2 -2
  27. package/dist/plugins/isometric/projection.d.ts +2 -2
  28. package/dist/plugins/isometric/projection.js.map +2 -2
  29. package/dist/plugins/physics/steering.d.ts +2 -2
  30. package/dist/plugins/physics/steering.js.map +2 -2
  31. package/dist/plugins/rendering/particles.d.ts +2 -2
  32. package/dist/plugins/rendering/particles.js +2 -2
  33. package/dist/plugins/rendering/particles.js.map +3 -3
  34. package/dist/plugins/rendering/renderer2D.d.ts +6 -5
  35. package/dist/plugins/rendering/renderer2D.js +2 -2
  36. package/dist/plugins/rendering/renderer2D.js.map +3 -3
  37. package/dist/plugins/rendering/renderer3D.d.ts +3 -2
  38. package/dist/plugins/rendering/renderer3D.js +2 -2
  39. package/dist/plugins/rendering/renderer3D.js.map +3 -3
  40. package/dist/plugins/rendering/tilemap.d.ts +3 -3
  41. package/dist/plugins/rendering/tilemap.js.map +3 -3
  42. package/dist/plugins/spatial/camera.js.map +2 -2
  43. package/dist/plugins/spatial/camera3D.d.ts +3 -3
  44. package/dist/plugins/spatial/camera3D.js.map +2 -2
  45. package/dist/plugins/spatial/transform.d.ts +2 -2
  46. package/dist/plugins/spatial/transform.js.map +1 -1
  47. package/dist/plugins/spatial/transform3D.d.ts +2 -2
  48. package/dist/plugins/spatial/transform3D.js.map +1 -1
  49. package/dist/plugins/ui/ui.d.ts +2 -2
  50. package/dist/plugins/ui/ui.js +2 -2
  51. package/dist/plugins/ui/ui.js.map +4 -4
  52. package/dist/reactive-query-manager.d.ts +26 -11
  53. package/dist/screen-types.d.ts +21 -3
  54. package/dist/system-builder.d.ts +5 -5
  55. package/dist/type-utils.d.ts +20 -0
  56. package/dist/types.d.ts +1 -1
  57. package/package.json +2 -2
@@ -2,10 +2,10 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/plugins/spatial/camera.ts", "../src/plugins/input/selection.ts"],
4
4
  "sourcesContent": [
5
- "/**\n * Camera / Viewport Plugin for ECSpresso\n *\n * Provides a declarative camera with world/screen coordinate conversion, smooth follow,\n * trauma-based shake, bounds clamping, cursor-centered zoom, and logical viewport dimensions.\n *\n * This plugin is renderer-agnostic. PixiJS or other renderer integration (applying\n * cameraState to a container/stage transform) is the consumer's responsibility.\n *\n * Camera uses its own x/y/zoom/rotation rather than localTransform/worldTransform.\n * It reads the target entity's worldTransform for follow, but doesn't participate\n * in the transform hierarchy itself.\n */\n\nimport { definePlugin } from 'ecspresso';\nimport type { SystemPhase } from 'ecspresso';\nimport type ECSpresso from 'ecspresso';\nimport type { WorldConfigFrom } from '../../type-utils';\nimport type { TransformWorldConfig } from './transform';\n\n// ==================== Component Types ====================\n\nexport interface Camera {\n\tx: number;\n\ty: number;\n\tzoom: number;\n\trotation: number;\n}\n\nexport interface CameraFollow {\n\ttarget: number;\n\tsmoothing: number;\n\tdeadzoneX: number;\n\tdeadzoneY: number;\n\toffsetX: number;\n\toffsetY: number;\n}\n\nexport interface CameraShake {\n\ttrauma: number;\n\ttraumaDecay: number;\n\tmaxOffsetX: number;\n\tmaxOffsetY: number;\n\tmaxRotation: number;\n}\n\nexport interface CameraBounds {\n\tminX: number;\n\tminY: number;\n\tmaxX: number;\n\tmaxY: number;\n}\n\nexport interface CameraComponentTypes {\n\tcamera: Camera;\n\tcameraFollow: CameraFollow;\n\tcameraShake: CameraShake;\n\tcameraBounds: CameraBounds;\n}\n\n// ==================== Resource Types ====================\n\nexport interface FollowOptions {\n\tsmoothing?: number;\n\tdeadzoneX?: number;\n\tdeadzoneY?: number;\n\toffsetX?: number;\n\toffsetY?: number;\n}\n\nexport type EntityHandle = { id: number };\n\nexport interface CameraState {\n\t// Read-only data (synced from camera entity each frame)\n\tx: number;\n\ty: number;\n\tzoom: number;\n\trotation: number;\n\tshakeOffsetX: number;\n\tshakeOffsetY: number;\n\tshakeRotation: number;\n\tviewportWidth: number;\n\tviewportHeight: number;\n\tentityId: number;\n\n\t// Mutation methods\n\tfollow(target: number | EntityHandle, options?: FollowOptions): void;\n\tunfollow(): void;\n\tsetPosition(x: number, y: number): void;\n\tsetZoom(zoom: number): void;\n\tsetRotation(rotation: number): void;\n\tsetBounds(minX: number, minY: number, maxX: number, maxY: number): void;\n\tclearBounds(): void;\n\taddTrauma(amount: number): void;\n}\n\nexport interface CameraResourceTypes {\n\tcameraState: CameraState;\n}\n\n// ==================== Plugin Options ====================\n\nexport interface CameraPluginOptions<G extends string = 'camera'> {\n\tviewportWidth?: number;\n\tviewportHeight?: number;\n\tinitial?: {\n\t\tx?: number;\n\t\ty?: number;\n\t\tzoom?: number;\n\t\trotation?: number;\n\t};\n\tfollow?: FollowOptions;\n\tshake?: boolean | Partial<Omit<CameraShake, 'trauma'>>;\n\tbounds?:\n\t\t| { minX: number; minY: number; maxX: number; maxY: number }\n\t\t| [number, number, number, number];\n\tzoom?: {\n\t\tzoomStep?: number;\n\t\tminZoom?: number;\n\t\tmaxZoom?: number;\n\t};\n\tpan?: {\n\t\tspeed: number;\n\t\tactions?: {\n\t\t\tup?: string;\n\t\t\tdown?: string;\n\t\t\tleft?: string;\n\t\t\tright?: string;\n\t\t};\n\t};\n\tsystemGroup?: G;\n\tphase?: SystemPhase;\n\trandomFn?: () => number;\n}\n\n// ==================== Default Values ====================\n\nconst DEFAULT_SHAKE: Readonly<Omit<CameraShake, 'trauma'>> = {\n\ttraumaDecay: 1,\n\tmaxOffsetX: 10,\n\tmaxOffsetY: 10,\n\tmaxRotation: 0.05,\n};\n\nconst DEFAULT_FOLLOW: Readonly<Omit<CameraFollow, 'target'>> = {\n\tsmoothing: 5,\n\tdeadzoneX: 0,\n\tdeadzoneY: 0,\n\toffsetX: 0,\n\toffsetY: 0,\n};\n\n// ==================== Coordinate Conversion ====================\n\nexport function worldToScreen(\n\tworldX: number,\n\tworldY: number,\n\tstate: CameraState,\n): { x: number; y: number } {\n\tconst dx = worldX - (state.x + state.shakeOffsetX);\n\tconst dy = worldY - (state.y + state.shakeOffsetY);\n\n\tconst angle = -(state.rotation + state.shakeRotation);\n\tconst cos = Math.cos(angle);\n\tconst sin = Math.sin(angle);\n\tconst rx = dx * cos - dy * sin;\n\tconst ry = dx * sin + dy * cos;\n\n\treturn {\n\t\tx: rx * state.zoom + state.viewportWidth / 2,\n\t\ty: ry * state.zoom + state.viewportHeight / 2,\n\t};\n}\n\nexport function screenToWorld(\n\tscreenX: number,\n\tscreenY: number,\n\tstate: CameraState,\n): { x: number; y: number } {\n\tconst cx = (screenX - state.viewportWidth / 2) / state.zoom;\n\tconst cy = (screenY - state.viewportHeight / 2) / state.zoom;\n\n\tconst angle = state.rotation + state.shakeRotation;\n\tconst cos = Math.cos(angle);\n\tconst sin = Math.sin(angle);\n\tconst rx = cx * cos - cy * sin;\n\tconst ry = cx * sin + cy * cos;\n\n\treturn {\n\t\tx: rx + state.x + state.shakeOffsetX,\n\t\ty: ry + state.y + state.shakeOffsetY,\n\t};\n}\n\n// ==================== Internal Helpers ====================\n\nfunction resolveTarget(target: number | EntityHandle): number {\n\treturn typeof target === 'number' ? target : target.id;\n}\n\nfunction resolveShakeOptions(shake: true | Partial<Omit<CameraShake, 'trauma'>>): CameraShake {\n\tconst opts = shake === true ? {} : shake;\n\treturn {\n\t\ttrauma: 0,\n\t\ttraumaDecay: opts.traumaDecay ?? DEFAULT_SHAKE.traumaDecay,\n\t\tmaxOffsetX: opts.maxOffsetX ?? DEFAULT_SHAKE.maxOffsetX,\n\t\tmaxOffsetY: opts.maxOffsetY ?? DEFAULT_SHAKE.maxOffsetY,\n\t\tmaxRotation: opts.maxRotation ?? DEFAULT_SHAKE.maxRotation,\n\t};\n}\n\nfunction resolveBounds(\n\tbounds: { minX: number; minY: number; maxX: number; maxY: number } | [number, number, number, number],\n): CameraBounds {\n\tif (Array.isArray(bounds)) {\n\t\treturn { minX: bounds[0], minY: bounds[1], maxX: bounds[2], maxY: bounds[3] };\n\t}\n\treturn { ...bounds };\n}\n\nfunction resolveFollowOptions(options?: FollowOptions): Omit<CameraFollow, 'target'> {\n\treturn {\n\t\tsmoothing: options?.smoothing ?? DEFAULT_FOLLOW.smoothing,\n\t\tdeadzoneX: options?.deadzoneX ?? DEFAULT_FOLLOW.deadzoneX,\n\t\tdeadzoneY: options?.deadzoneY ?? DEFAULT_FOLLOW.deadzoneY,\n\t\toffsetX: options?.offsetX ?? DEFAULT_FOLLOW.offsetX,\n\t\toffsetY: options?.offsetY ?? DEFAULT_FOLLOW.offsetY,\n\t};\n}\n\n// ==================== Plugin Factory ====================\n\ntype CameraWorldConfig = WorldConfigFrom<CameraComponentTypes, {}, CameraResourceTypes>;\n\ntype CameraLabels =\n\t| 'camera-init'\n\t| 'camera-follow'\n\t| 'camera-shake-update'\n\t| 'camera-bounds'\n\t| 'camera-state-sync'\n\t| 'camera-zoom'\n\t| 'camera-pan';\n\nexport function createCameraPlugin<G extends string = 'camera'>(\n\toptions?: CameraPluginOptions<G>,\n) {\n\tconst {\n\t\tviewportWidth = 800,\n\t\tviewportHeight = 600,\n\t\tinitial,\n\t\tfollow: followConfig,\n\t\tshake: shakeConfig,\n\t\tbounds: boundsConfig,\n\t\tzoom: zoomConfig,\n\t\tpan: panConfig,\n\t\tsystemGroup = 'camera',\n\t\tphase = 'postUpdate',\n\t\trandomFn = Math.random,\n\t} = options ?? {};\n\n\treturn definePlugin('camera')\n\t\t.withComponentTypes<CameraComponentTypes>()\n\t\t.withResourceTypes<CameraResourceTypes>()\n\t\t.withLabels<CameraLabels>()\n\t\t.withGroups<G>()\n\t\t.requires<TransformWorldConfig>()\n\t\t.install((world) => {\n\t\t\t// Build mutation methods as closures over the world reference.\n\t\t\t// The cameraState resource is created immediately with placeholder methods,\n\t\t\t// then the init system populates entityId and wires up real methods.\n\n\t\t\tconst cameraState: CameraState = {\n\t\t\t\tx: initial?.x ?? 0,\n\t\t\t\ty: initial?.y ?? 0,\n\t\t\t\tzoom: initial?.zoom ?? 1,\n\t\t\t\trotation: initial?.rotation ?? 0,\n\t\t\t\tshakeOffsetX: 0,\n\t\t\t\tshakeOffsetY: 0,\n\t\t\t\tshakeRotation: 0,\n\t\t\t\tviewportWidth,\n\t\t\t\tviewportHeight,\n\t\t\t\tentityId: -1,\n\n\t\t\t\t// Mutation methods — wired up after camera entity is spawned\n\t\t\t\tfollow: () => {},\n\t\t\t\tunfollow: () => {},\n\t\t\t\tsetPosition: () => {},\n\t\t\t\tsetZoom: () => {},\n\t\t\t\tsetRotation: () => {},\n\t\t\t\tsetBounds: () => {},\n\t\t\t\tclearBounds: () => {},\n\t\t\t\taddTrauma: () => {},\n\t\t\t};\n\n\t\t\tworld.addResource('cameraState', cameraState);\n\n\t\t\t// camera-init: spawns camera entity and wires up mutation closures\n\t\t\tworld\n\t\t\t\t.addSystem('camera-init')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setOnInitialize((ecs: ECSpresso<CameraWorldConfig & TransformWorldConfig>) => {\n\t\t\t\t\t// Spawn with required camera component\n\t\t\t\t\tconst entity = ecs.spawn({\n\t\t\t\t\t\tcamera: {\n\t\t\t\t\t\t\tx: initial?.x ?? 0,\n\t\t\t\t\t\t\ty: initial?.y ?? 0,\n\t\t\t\t\t\t\tzoom: initial?.zoom ?? 1,\n\t\t\t\t\t\t\trotation: initial?.rotation ?? 0,\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\n\t\t\t\t\t// Conditionally add optional components\n\t\t\t\t\tif (followConfig) {\n\t\t\t\t\t\tecs.addComponent(entity.id, 'cameraFollow', {\n\t\t\t\t\t\t\ttarget: -1,\n\t\t\t\t\t\t\t...resolveFollowOptions(followConfig),\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\tif (shakeConfig) {\n\t\t\t\t\t\tecs.addComponent(entity.id, 'cameraShake', resolveShakeOptions(shakeConfig));\n\t\t\t\t\t}\n\n\t\t\t\t\tif (boundsConfig) {\n\t\t\t\t\t\tecs.addComponent(entity.id, 'cameraBounds', resolveBounds(boundsConfig));\n\t\t\t\t\t}\n\t\t\t\t\tcameraState.entityId = entity.id;\n\n\t\t\t\t\t// Wire up mutation methods\n\t\t\t\t\tcameraState.follow = (target: number | EntityHandle, opts?: FollowOptions) => {\n\t\t\t\t\t\tconst targetId = resolveTarget(target);\n\t\t\t\t\t\tconst followData: CameraFollow = {\n\t\t\t\t\t\t\ttarget: targetId,\n\t\t\t\t\t\t\t...resolveFollowOptions(opts),\n\t\t\t\t\t\t};\n\t\t\t\t\t\tconst existing = ecs.getComponent(cameraState.entityId, 'cameraFollow');\n\t\t\t\t\t\tif (existing) {\n\t\t\t\t\t\t\texisting.target = followData.target;\n\t\t\t\t\t\t\texisting.smoothing = followData.smoothing;\n\t\t\t\t\t\t\texisting.deadzoneX = followData.deadzoneX;\n\t\t\t\t\t\t\texisting.deadzoneY = followData.deadzoneY;\n\t\t\t\t\t\t\texisting.offsetX = followData.offsetX;\n\t\t\t\t\t\t\texisting.offsetY = followData.offsetY;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tecs.addComponent(cameraState.entityId, 'cameraFollow', followData);\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\n\t\t\t\t\tcameraState.unfollow = () => {\n\t\t\t\t\t\tconst existing = ecs.getComponent(cameraState.entityId, 'cameraFollow');\n\t\t\t\t\t\tif (existing) {\n\t\t\t\t\t\t\tecs.removeComponent(cameraState.entityId, 'cameraFollow');\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\n\t\t\t\t\tcameraState.setPosition = (x: number, y: number) => {\n\t\t\t\t\t\tconst camera = ecs.getComponent(cameraState.entityId, 'camera');\n\t\t\t\t\t\tif (!camera) return;\n\t\t\t\t\t\tcamera.x = x;\n\t\t\t\t\t\tcamera.y = y;\n\t\t\t\t\t};\n\n\t\t\t\t\tcameraState.setZoom = (zoom: number) => {\n\t\t\t\t\t\tconst camera = ecs.getComponent(cameraState.entityId, 'camera');\n\t\t\t\t\t\tif (!camera) return;\n\t\t\t\t\t\tcamera.zoom = zoom;\n\t\t\t\t\t};\n\n\t\t\t\t\tcameraState.setRotation = (rotation: number) => {\n\t\t\t\t\t\tconst camera = ecs.getComponent(cameraState.entityId, 'camera');\n\t\t\t\t\t\tif (!camera) return;\n\t\t\t\t\t\tcamera.rotation = rotation;\n\t\t\t\t\t};\n\n\t\t\t\t\tcameraState.setBounds = (minX: number, minY: number, maxX: number, maxY: number) => {\n\t\t\t\t\t\tconst existing = ecs.getComponent(cameraState.entityId, 'cameraBounds');\n\t\t\t\t\t\tif (existing) {\n\t\t\t\t\t\t\texisting.minX = minX;\n\t\t\t\t\t\t\texisting.minY = minY;\n\t\t\t\t\t\t\texisting.maxX = maxX;\n\t\t\t\t\t\t\texisting.maxY = maxY;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tecs.addComponent(cameraState.entityId, 'cameraBounds', { minX, minY, maxX, maxY });\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\n\t\t\t\t\tcameraState.clearBounds = () => {\n\t\t\t\t\t\tconst existing = ecs.getComponent(cameraState.entityId, 'cameraBounds');\n\t\t\t\t\t\tif (existing) {\n\t\t\t\t\t\t\tecs.removeComponent(cameraState.entityId, 'cameraBounds');\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\n\t\t\t\t\tcameraState.addTrauma = (amount: number) => {\n\t\t\t\t\t\tconst shake = ecs.getComponent(cameraState.entityId, 'cameraShake');\n\t\t\t\t\t\tif (shake) {\n\t\t\t\t\t\t\tshake.trauma = Math.min(1, Math.max(0, shake.trauma + amount));\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tecs.addComponent(cameraState.entityId, 'cameraShake', {\n\t\t\t\t\t\t\t\t...resolveShakeOptions(true),\n\t\t\t\t\t\t\t\ttrauma: Math.min(1, Math.max(0, amount)),\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\n\t\t\t// camera-follow: priority 400 (after transform propagation at 500)\n\t\t\tworld\n\t\t\t\t.addSystem('camera-follow')\n\t\t\t\t.setPriority(400)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('cameras', {\n\t\t\t\t\twith: ['camera', 'cameraFollow'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, dt, ecs }) => {\n\t\t\t\t\tconst t = Math.min(1, dt);\n\t\t\t\t\tfor (const entity of queries.cameras) {\n\t\t\t\t\t\tconst { camera, cameraFollow } = entity.components;\n\t\t\t\t\t\tif (cameraFollow.target < 0) continue;\n\n\t\t\t\t\t\tconst targetWorld = ecs.getComponent(cameraFollow.target, 'worldTransform');\n\t\t\t\t\t\tif (!targetWorld) continue;\n\n\t\t\t\t\t\tconst goalX = targetWorld.x + cameraFollow.offsetX;\n\t\t\t\t\t\tconst goalY = targetWorld.y + cameraFollow.offsetY;\n\t\t\t\t\t\tconst dx = goalX - camera.x;\n\t\t\t\t\t\tconst dy = goalY - camera.y;\n\n\t\t\t\t\t\tif (Math.abs(dx) > cameraFollow.deadzoneX) {\n\t\t\t\t\t\t\tconst sign = dx > 0 ? 1 : -1;\n\t\t\t\t\t\t\tconst excessX = dx - sign * cameraFollow.deadzoneX;\n\t\t\t\t\t\t\tconst factor = Math.min(1, cameraFollow.smoothing * t);\n\t\t\t\t\t\t\tcamera.x += excessX * factor;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (Math.abs(dy) > cameraFollow.deadzoneY) {\n\t\t\t\t\t\t\tconst sign = dy > 0 ? 1 : -1;\n\t\t\t\t\t\t\tconst excessY = dy - sign * cameraFollow.deadzoneY;\n\t\t\t\t\t\t\tconst factor = Math.min(1, cameraFollow.smoothing * t);\n\t\t\t\t\t\t\tcamera.y += excessY * factor;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// camera-shake-update: priority 390\n\t\t\tworld\n\t\t\t\t.addSystem('camera-shake-update')\n\t\t\t\t.setPriority(390)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('shakeCameras', {\n\t\t\t\t\twith: ['camera', 'cameraShake'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, dt }) => {\n\t\t\t\t\tfor (const entity of queries.shakeCameras) {\n\t\t\t\t\t\tconst { cameraShake } = entity.components;\n\t\t\t\t\t\tcameraShake.trauma = Math.max(0, cameraShake.trauma - cameraShake.traumaDecay * dt);\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// camera-bounds: priority 380\n\t\t\tworld\n\t\t\t\t.addSystem('camera-bounds')\n\t\t\t\t.setPriority(380)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('boundedCameras', {\n\t\t\t\t\twith: ['camera', 'cameraBounds'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries }) => {\n\t\t\t\t\tfor (const entity of queries.boundedCameras) {\n\t\t\t\t\t\tconst { camera, cameraBounds } = entity.components;\n\t\t\t\t\t\tconst halfW = cameraState.viewportWidth / (2 * camera.zoom);\n\t\t\t\t\t\tconst halfH = cameraState.viewportHeight / (2 * camera.zoom);\n\n\t\t\t\t\t\tconst effectiveMinX = cameraBounds.minX + halfW;\n\t\t\t\t\t\tconst effectiveMaxX = cameraBounds.maxX - halfW;\n\t\t\t\t\t\tconst effectiveMinY = cameraBounds.minY + halfH;\n\t\t\t\t\t\tconst effectiveMaxY = cameraBounds.maxY - halfH;\n\n\t\t\t\t\t\tif (effectiveMinX > effectiveMaxX) {\n\t\t\t\t\t\t\tcamera.x = (cameraBounds.minX + cameraBounds.maxX) / 2;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tcamera.x = Math.max(effectiveMinX, Math.min(effectiveMaxX, camera.x));\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (effectiveMinY > effectiveMaxY) {\n\t\t\t\t\t\t\tcamera.y = (cameraBounds.minY + cameraBounds.maxY) / 2;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tcamera.y = Math.max(effectiveMinY, Math.min(effectiveMaxY, camera.y));\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// camera-state-sync: priority 370\n\t\t\tworld\n\t\t\t\t.addSystem('camera-state-sync')\n\t\t\t\t.setPriority(370)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setProcess(({ ecs }) => {\n\t\t\t\t\tconst camera = ecs.getComponent(cameraState.entityId, 'camera');\n\t\t\t\t\tif (!camera) {\n\t\t\t\t\t\tcameraState.x = 0;\n\t\t\t\t\t\tcameraState.y = 0;\n\t\t\t\t\t\tcameraState.zoom = 1;\n\t\t\t\t\t\tcameraState.rotation = 0;\n\t\t\t\t\t\tcameraState.shakeOffsetX = 0;\n\t\t\t\t\t\tcameraState.shakeOffsetY = 0;\n\t\t\t\t\t\tcameraState.shakeRotation = 0;\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tcameraState.x = camera.x;\n\t\t\t\t\tcameraState.y = camera.y;\n\t\t\t\t\tcameraState.zoom = camera.zoom;\n\t\t\t\t\tcameraState.rotation = camera.rotation;\n\n\t\t\t\t\tconst shake = ecs.getComponent(cameraState.entityId, 'cameraShake');\n\t\t\t\t\tif (shake && shake.trauma > 0) {\n\t\t\t\t\t\tconst intensity = shake.trauma * shake.trauma;\n\t\t\t\t\t\tcameraState.shakeOffsetX = shake.maxOffsetX * intensity * (randomFn() * 2 - 1);\n\t\t\t\t\t\tcameraState.shakeOffsetY = shake.maxOffsetY * intensity * (randomFn() * 2 - 1);\n\t\t\t\t\t\tcameraState.shakeRotation = shake.maxRotation * intensity * (randomFn() * 2 - 1);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tcameraState.shakeOffsetX = 0;\n\t\t\t\t\t\tcameraState.shakeOffsetY = 0;\n\t\t\t\t\t\tcameraState.shakeRotation = 0;\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// camera-zoom: conditionally registered when zoom option is provided\n\t\t\tif (zoomConfig) {\n\t\t\t\tconst {\n\t\t\t\t\tzoomStep = 0.1,\n\t\t\t\t\tminZoom = 0.1,\n\t\t\t\t\tmaxZoom = 10,\n\t\t\t\t} = zoomConfig;\n\n\t\t\t\ttype ZoomInputState = { pointer: { position: { x: number; y: number } } };\n\n\t\t\t\tlet pendingSteps = 0;\n\t\t\t\tlet zoomActive = false;\n\t\t\t\tlet canvas: HTMLCanvasElement | undefined;\n\t\t\t\tlet isoState: { tileWidth: number; tileHeight: number; originX: number; originY: number } | undefined;\n\n\t\t\t\tfunction onWheel(e: WheelEvent) {\n\t\t\t\t\te.preventDefault();\n\t\t\t\t\tpendingSteps += Math.sign(e.deltaY);\n\t\t\t\t}\n\n\t\t\t\tworld\n\t\t\t\t\t.addSystem('camera-zoom')\n\t\t\t\t\t.setPriority(410)\n\t\t\t\t\t.inPhase('preUpdate')\n\t\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t\t.addQuery('cameras', {\n\t\t\t\t\t\twith: ['camera'],\n\t\t\t\t\t})\n\t\t\t\t\t.setOnInitialize((ecs) => {\n\t\t\t\t\t\t// Check for required dependencies\n\t\t\t\t\t\tconst inputState = ecs.tryGetResource<ZoomInputState>('inputState');\n\t\t\t\t\t\tconst pixiApp = ecs.tryGetResource<{ canvas: HTMLCanvasElement }>('pixiApp');\n\n\t\t\t\t\t\tif (!inputState || !pixiApp) {\n\t\t\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t\t\t'[camera] zoom requires the input plugin and renderer2D plugin. ' +\n\t\t\t\t\t\t\t\t'Zoom will be disabled.',\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tcanvas = pixiApp.canvas;\n\t\t\t\t\t\tcanvas.addEventListener('wheel', onWheel as EventListener, { passive: false });\n\n\t\t\t\t\t\t// Detect isometric projection for iso-aware cursor-centered zoom\n\t\t\t\t\t\tisoState = ecs.tryGetResource<{\n\t\t\t\t\t\t\ttileWidth: number; tileHeight: number;\n\t\t\t\t\t\t\toriginX: number; originY: number;\n\t\t\t\t\t\t}>('isoProjection');\n\n\t\t\t\t\t\tzoomActive = true;\n\t\t\t\t\t})\n\t\t\t\t\t.setOnDetach(() => {\n\t\t\t\t\t\tif (!zoomActive || !canvas) return;\n\t\t\t\t\t\tcanvas.removeEventListener('wheel', onWheel as EventListener);\n\t\t\t\t\t})\n\t\t\t\t\t.setProcess(({ queries, ecs }) => {\n\t\t\t\t\t\tif (!zoomActive || pendingSteps === 0) return;\n\n\t\t\t\t\t\tconst steps = pendingSteps;\n\t\t\t\t\t\tpendingSteps = 0;\n\n\t\t\t\t\t\tconst [cameraEntity] = queries.cameras;\n\t\t\t\t\t\tif (!cameraEntity) return;\n\n\t\t\t\t\t\tconst cam = cameraEntity.components.camera;\n\t\t\t\t\t\tconst inputState = ecs.tryGetResource<ZoomInputState>('inputState');\n\t\t\t\t\t\tif (!inputState) return;\n\n\t\t\t\t\t\t// Apply zoom — proportional to number of wheel steps\n\t\t\t\t\t\tconst direction = steps > 0 ? (1 - zoomStep) : (1 + zoomStep);\n\t\t\t\t\t\tconst newZoom = Math.max(minZoom, Math.min(maxZoom, cam.zoom * Math.pow(direction, Math.abs(steps))));\n\n\t\t\t\t\t\tif (isoState && canvas) {\n\t\t\t\t\t\t\t// Iso-aware cursor-centered zoom: work in iso-screen space\n\t\t\t\t\t\t\tconst rect = canvas.getBoundingClientRect();\n\t\t\t\t\t\t\tconst screenOffX = inputState.pointer.position.x - (rect.left + rect.width / 2);\n\t\t\t\t\t\t\tconst screenOffY = inputState.pointer.position.y - (rect.top + rect.height / 2);\n\n\t\t\t\t\t\t\t// Inlined worldToIso — avoids cross-plugin import\n\t\t\t\t\t\t\tconst halfW = isoState.tileWidth / 2;\n\t\t\t\t\t\t\tconst halfH = isoState.tileHeight / 2;\n\t\t\t\t\t\t\tconst camIsoX = (cam.x - cam.y) * halfW + isoState.originX;\n\t\t\t\t\t\t\tconst camIsoY = (cam.x + cam.y) * halfH + isoState.originY;\n\t\t\t\t\t\t\tconst isoBeforeX = camIsoX + screenOffX / cam.zoom;\n\t\t\t\t\t\t\tconst isoBeforeY = camIsoY + screenOffY / cam.zoom;\n\n\t\t\t\t\t\t\tcam.zoom = newZoom;\n\n\t\t\t\t\t\t\t// New camera iso position so the same point stays under cursor\n\t\t\t\t\t\t\tconst newCamIsoX = isoBeforeX - screenOffX / newZoom;\n\t\t\t\t\t\t\tconst newCamIsoY = isoBeforeY - screenOffY / newZoom;\n\n\t\t\t\t\t\t\t// Inlined isoToWorld\n\t\t\t\t\t\t\tconst relX = newCamIsoX - isoState.originX;\n\t\t\t\t\t\t\tconst relY = newCamIsoY - isoState.originY;\n\t\t\t\t\t\t\tcam.x = relX / isoState.tileWidth + relY / isoState.tileHeight;\n\t\t\t\t\t\t\tcam.y = -relX / isoState.tileWidth + relY / isoState.tileHeight;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Pixel-space cursor-centered zoom\n\t\t\t\t\t\t\tconst worldBefore = screenToWorld(\n\t\t\t\t\t\t\t\tinputState.pointer.position.x,\n\t\t\t\t\t\t\t\tinputState.pointer.position.y,\n\t\t\t\t\t\t\t\tcameraState,\n\t\t\t\t\t\t\t);\n\n\t\t\t\t\t\t\tcam.zoom = newZoom;\n\n\t\t\t\t\t\t\tcam.x = worldBefore.x - (inputState.pointer.position.x - cameraState.viewportWidth / 2) / newZoom;\n\t\t\t\t\t\t\tcam.y = worldBefore.y - (inputState.pointer.position.y - cameraState.viewportHeight / 2) / newZoom;\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t}\n\n\t\t\t// camera-pan: conditionally registered when pan option is provided\n\t\t\tif (panConfig) {\n\t\t\t\ttype PanInputState = { actions: { isActive(action: string): boolean } };\n\n\t\t\t\tconst {\n\t\t\t\t\tspeed,\n\t\t\t\t\tactions: panActions,\n\t\t\t\t} = panConfig;\n\n\t\t\t\tconst actionUp = panActions?.up ?? 'panUp';\n\t\t\t\tconst actionDown = panActions?.down ?? 'panDown';\n\t\t\t\tconst actionLeft = panActions?.left ?? 'panLeft';\n\t\t\t\tconst actionRight = panActions?.right ?? 'panRight';\n\n\t\t\t\tlet panActive = false;\n\n\t\t\t\tworld\n\t\t\t\t\t.addSystem('camera-pan')\n\t\t\t\t\t.setPriority(420)\n\t\t\t\t\t.inPhase('preUpdate')\n\t\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t\t.setOnInitialize((ecs) => {\n\t\t\t\t\t\tconst inputState = ecs.tryGetResource<PanInputState>('inputState');\n\t\t\t\t\t\tif (!inputState) {\n\t\t\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t\t\t'[camera] pan requires the input plugin. Pan will be disabled.',\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tpanActive = true;\n\t\t\t\t\t})\n\t\t\t\t\t.setProcess(({ ecs, dt }) => {\n\t\t\t\t\t\tif (!panActive) return;\n\n\t\t\t\t\t\tconst inputState = ecs.tryGetResource<PanInputState>('inputState');\n\t\t\t\t\t\tif (!inputState) return;\n\n\t\t\t\t\t\tconst delta = (speed / cameraState.zoom) * dt;\n\t\t\t\t\t\tconst dx = (inputState.actions.isActive(actionRight) ? 1 : 0)\n\t\t\t\t\t\t\t- (inputState.actions.isActive(actionLeft) ? 1 : 0);\n\t\t\t\t\t\tconst dy = (inputState.actions.isActive(actionDown) ? 1 : 0)\n\t\t\t\t\t\t\t- (inputState.actions.isActive(actionUp) ? 1 : 0);\n\n\t\t\t\t\t\tif (dx !== 0 || dy !== 0) {\n\t\t\t\t\t\t\tcameraState.setPosition(\n\t\t\t\t\t\t\t\tcameraState.x + dx * delta,\n\t\t\t\t\t\t\t\tcameraState.y + dy * delta,\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}\n\t\t});\n}\n",
6
- "/**\n * Selection Plugin for ECSpresso\n *\n * Provides pointer-driven entity selection via box-drag and click.\n * Entities with a `selectable` component can be selected by the user.\n * Selected entities receive a `selected` component that other systems\n * can query for.\n *\n * Requires the input plugin (for pointer state) and the renderer2D plugin\n * (for graphics rendering of the selection box).\n *\n * Camera-aware: when a `cameraState` resource is present (from the camera\n * plugin), pointer coordinates are automatically converted to world space\n * for hit-testing. The selection box overlay remains in screen space.\n */\n\nimport { Graphics } from 'pixi.js';\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type { WorldConfigFrom } from 'ecspresso';\nimport type { InputResourceTypes } from './input';\nimport type { Renderer2DComponentTypes, Renderer2DResourceTypes } from '../rendering/renderer2D';\nimport type { CameraState } from '../spatial/camera';\nimport { screenToWorld } from '../spatial/camera';\n\n// ==================== Component Types ====================\n\n/**\n * Component types provided by the selection plugin.\n */\nexport interface SelectionComponentTypes {\n\t/** Tag marking an entity as eligible for selection */\n\tselectable: true;\n\t/** Tag marking an entity as currently selected (added/removed dynamically) */\n\tselected: true;\n}\n\n// ==================== Resource Types ====================\n\n/**\n * Internal state tracking the current drag selection.\n */\nexport interface SelectionState {\n\tdragStart: { x: number; y: number };\n\tboxEntityId: number | null;\n}\n\n/**\n * Resource types provided by the selection plugin.\n */\nexport interface SelectionResourceTypes {\n\tselectionState: SelectionState;\n}\n\n// ==================== WorldConfig ====================\n\n/**\n * WorldConfig representing the selection plugin's provided types.\n */\nexport type SelectionWorldConfig = WorldConfigFrom<SelectionComponentTypes, {}, SelectionResourceTypes>;\n\n// ==================== Dependency Types ====================\n\ntype SelectionRequires = WorldConfigFrom<Renderer2DComponentTypes, {}, InputResourceTypes & Renderer2DResourceTypes>;\n\n// ==================== Plugin Options ====================\n\n/**\n * Configuration options for the selection plugin.\n */\nexport interface SelectionPluginOptions<G extends string = 'selection'> extends BasePluginOptions<G> {\n\t/** Minimum drag distance (px) to trigger box select vs click select (default: 5) */\n\tclickThreshold?: number;\n\t/** Selection box fill color (default: 0x00FF00) */\n\tboxFillColor?: number;\n\t/** Selection box fill alpha (default: 0.15) */\n\tboxFillAlpha?: number;\n\t/** Selection box stroke color (default: 0x00FF00) */\n\tboxStrokeColor?: number;\n\t/** Selection box stroke alpha (default: 0.8) */\n\tboxStrokeAlpha?: number;\n\t/** Tint applied to selected entities' sprites (default: 0x44FF44) */\n\tselectedTint?: number;\n\t/** Render layer for the selection box entity (default: undefined) */\n\trenderLayer?: string;\n}\n\n// ==================== Helper Functions ====================\n\n/**\n * Create a selectable component.\n *\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createTransform(100, 200),\n * sprite,\n * ...createSelectable(),\n * });\n * ```\n */\nexport function createSelectable(): Pick<SelectionComponentTypes, 'selectable'> {\n\treturn { selectable: true };\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a selection plugin for ECSpresso.\n *\n * Provides:\n * - Box-drag selection (left-click drag to select multiple entities)\n * - Click selection (left-click to select a single entity)\n * - Visual feedback (configurable sprite tint for selected entities)\n * - Selection box overlay (rendered as a PixiJS Graphics entity)\n * - Automatic camera-awareness when cameraState resource is present\n *\n * Requires the input plugin and renderer2D plugin to be installed.\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createRenderer2DPlugin({ renderLayers: ['game', 'ui'] }))\n * .withPlugin(createInputPlugin())\n * .withPlugin(createSelectionPlugin({ renderLayer: 'ui' }))\n * .build();\n *\n * await ecs.initialize();\n *\n * ecs.spawn({\n * sprite,\n * ...createTransform(100, 200),\n * ...createSelectable(),\n * });\n * ```\n */\nexport function createSelectionPlugin<G extends string = 'selection'>(\n\toptions?: SelectionPluginOptions<G>\n) {\n\tconst {\n\t\tsystemGroup = 'selection',\n\t\tpriority = 100,\n\t\tphase = 'preUpdate',\n\t\tclickThreshold = 5,\n\t\tboxFillColor = 0x00FF00,\n\t\tboxFillAlpha = 0.15,\n\t\tboxStrokeColor = 0x00FF00,\n\t\tboxStrokeAlpha = 0.8,\n\t\tselectedTint = 0x44FF44,\n\t\trenderLayer,\n\t} = options ?? {};\n\n\t// Pre-allocate draw options to avoid per-frame allocations during drag\n\tconst fillOptions = { color: boxFillColor, alpha: boxFillAlpha };\n\tconst strokeOptions = { color: boxStrokeColor, width: 1.5, alpha: boxStrokeAlpha };\n\n\treturn definePlugin('selection')\n\t\t.withComponentTypes<SelectionComponentTypes>()\n\t\t.withResourceTypes<SelectionResourceTypes>()\n\t\t.withLabels<'selection-input' | 'selection-visual'>()\n\t\t.withGroups<G>()\n\t\t.requires<SelectionRequires>()\n\t\t.install((world) => {\n\t\t\tworld.addResource('selectionState', {\n\t\t\t\tdragStart: { x: 0, y: 0 },\n\t\t\t\tboxEntityId: null,\n\t\t\t});\n\n\t\t\tlet preventContextMenu: ((e: Event) => void) | null = null;\n\n\t\t\tworld\n\t\t\t\t.addSystem('selection-input')\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('selectables', {\n\t\t\t\t\twith: ['selectable', 'worldTransform'],\n\t\t\t\t})\n\t\t\t\t.addQuery('currentlySelected', {\n\t\t\t\t\twith: ['selected'],\n\t\t\t\t})\n\t\t\t\t.withResources(['inputState', 'selectionState', 'pixiApp'])\n\t\t\t\t.setOnInitialize((ecs) => {\n\t\t\t\t\tconst pixiApp = ecs.getResource('pixiApp');\n\t\t\t\t\tpreventContextMenu = (e: Event) => e.preventDefault();\n\t\t\t\t\tpixiApp.canvas.addEventListener('contextmenu', preventContextMenu);\n\t\t\t\t})\n\t\t\t\t.setOnDetach((ecs) => {\n\t\t\t\t\tif (!preventContextMenu) return;\n\t\t\t\t\tconst pixiApp = ecs.getResource('pixiApp');\n\t\t\t\t\tpixiApp.canvas.removeEventListener('contextmenu', preventContextMenu);\n\t\t\t\t\tpreventContextMenu = null;\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, ecs, resources }) => {\n\t\t\t\t\tconst { inputState: input, selectionState } = resources;\n\t\t\t\t\tconst pointer = input.pointer;\n\n\t\t\t\t\t// Start drag\n\t\t\t\t\tif (pointer.justPressed(0)) {\n\t\t\t\t\t\t// Clean up any orphaned box entity from an interrupted drag\n\t\t\t\t\t\tif (selectionState.boxEntityId !== null) {\n\t\t\t\t\t\t\tecs.commands.removeEntity(selectionState.boxEntityId);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tselectionState.dragStart.x = pointer.position.x;\n\t\t\t\t\t\tselectionState.dragStart.y = pointer.position.y;\n\n\t\t\t\t\t\tconst boxEntity = ecs.spawn({\n\t\t\t\t\t\t\tgraphics: new Graphics(),\n\t\t\t\t\t\t});\n\t\t\t\t\t\tif (renderLayer) {\n\t\t\t\t\t\t\tecs.addComponent(boxEntity.id, 'renderLayer', renderLayer);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tselectionState.boxEntityId = boxEntity.id;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Update drag visual (screen-space — no camera conversion)\n\t\t\t\t\tif (pointer.isDown(0) && selectionState.boxEntityId !== null) {\n\t\t\t\t\t\tconst g = ecs.getComponent(selectionState.boxEntityId, 'graphics');\n\t\t\t\t\t\tif (!g) return;\n\n\t\t\t\t\t\tconst startX = selectionState.dragStart.x;\n\t\t\t\t\t\tconst startY = selectionState.dragStart.y;\n\t\t\t\t\t\tconst curX = pointer.position.x;\n\t\t\t\t\t\tconst curY = pointer.position.y;\n\t\t\t\t\t\tconst minX = Math.min(startX, curX);\n\t\t\t\t\t\tconst minY = Math.min(startY, curY);\n\t\t\t\t\t\tconst w = Math.abs(curX - startX);\n\t\t\t\t\t\tconst h = Math.abs(curY - startY);\n\n\t\t\t\t\t\tg.clear();\n\t\t\t\t\t\tg.rect(minX, minY, w, h);\n\t\t\t\t\t\tg.fill(fillOptions);\n\t\t\t\t\t\tg.stroke(strokeOptions);\n\t\t\t\t\t}\n\n\t\t\t\t\t// End drag — perform selection\n\t\t\t\t\tif (!pointer.justReleased(0) || selectionState.boxEntityId === null) return;\n\n\t\t\t\t\tconst startX = selectionState.dragStart.x;\n\t\t\t\t\tconst startY = selectionState.dragStart.y;\n\t\t\t\t\tconst endX = pointer.position.x;\n\t\t\t\t\tconst endY = pointer.position.y;\n\n\t\t\t\t\tconst w = Math.abs(endX - startX);\n\t\t\t\t\tconst h = Math.abs(endY - startY);\n\n\t\t\t\t\t// Clear current selection\n\t\t\t\t\tfor (const entity of queries.currentlySelected) {\n\t\t\t\t\t\tecs.removeComponent(entity.id, 'selected');\n\t\t\t\t\t}\n\n\t\t\t\t\tconst isClick = w < clickThreshold && h < clickThreshold;\n\n\t\t\t\t\t// Convert screen coords to world space for hit-testing\n\t\t\t\t\tconst camState = ecs.tryGetResource('cameraState') as CameraState | undefined;\n\t\t\t\t\tconst worldEnd = camState\n\t\t\t\t\t\t? screenToWorld(endX, endY, camState)\n\t\t\t\t\t\t: { x: endX, y: endY };\n\n\t\t\t\t\tif (isClick) {\n\t\t\t\t\t\tconst clickRadiusSq = 400; // 20px radius in world space\n\t\t\t\t\t\tlet nearestId: number | null = null;\n\t\t\t\t\t\tlet nearestDistSq = Infinity;\n\n\t\t\t\t\t\tfor (const entity of queries.selectables) {\n\t\t\t\t\t\t\tconst { worldTransform } = entity.components;\n\t\t\t\t\t\t\tconst dx = worldTransform.x - worldEnd.x;\n\t\t\t\t\t\t\tconst dy = worldTransform.y - worldEnd.y;\n\t\t\t\t\t\t\tconst distSq = dx * dx + dy * dy;\n\t\t\t\t\t\t\tif (distSq < clickRadiusSq && distSq < nearestDistSq) {\n\t\t\t\t\t\t\t\tnearestDistSq = distSq;\n\t\t\t\t\t\t\t\tnearestId = entity.id;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (nearestId !== null) {\n\t\t\t\t\t\t\tecs.addComponent(nearestId, 'selected', true);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst worldStart = camState\n\t\t\t\t\t\t\t? screenToWorld(startX, startY, camState)\n\t\t\t\t\t\t\t: { x: startX, y: startY };\n\t\t\t\t\t\tconst minWX = Math.min(worldStart.x, worldEnd.x);\n\t\t\t\t\t\tconst maxWX = Math.max(worldStart.x, worldEnd.x);\n\t\t\t\t\t\tconst minWY = Math.min(worldStart.y, worldEnd.y);\n\t\t\t\t\t\tconst maxWY = Math.max(worldStart.y, worldEnd.y);\n\n\t\t\t\t\t\tfor (const entity of queries.selectables) {\n\t\t\t\t\t\t\tconst { worldTransform } = entity.components;\n\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\tworldTransform.x >= minWX &&\n\t\t\t\t\t\t\t\tworldTransform.x <= maxWX &&\n\t\t\t\t\t\t\t\tworldTransform.y >= minWY &&\n\t\t\t\t\t\t\t\tworldTransform.y <= maxWY\n\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\tecs.addComponent(entity.id, 'selected', true);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tecs.commands.removeEntity(selectionState.boxEntityId);\n\t\t\t\t\tselectionState.boxEntityId = null;\n\t\t\t\t});\n\n\t\t\t// Visual feedback via enter/exit callbacks — only fires on selection change\n\t\t\tworld\n\t\t\t\t.addSystem('selection-visual')\n\t\t\t\t.setPriority(priority)\n\t\t\t\t.inPhase('render')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('selectedUnits', {\n\t\t\t\t\twith: ['selected', 'sprite'],\n\t\t\t\t})\n\t\t\t\t.setOnEntityEnter('selectedUnits', ({ entity }) => {\n\t\t\t\t\tentity.components.sprite.tint = selectedTint;\n\t\t\t\t})\n\t\t\t\t.addQuery('deselectedUnits', {\n\t\t\t\t\twith: ['selectable', 'sprite'],\n\t\t\t\t\twithout: ['selected'],\n\t\t\t\t})\n\t\t\t\t.setOnEntityEnter('deselectedUnits', ({ entity }) => {\n\t\t\t\t\tentity.components.sprite.tint = 0xFFFFFF;\n\t\t\t\t});\n\t\t});\n}\n"
5
+ "/**\n * Camera / Viewport Plugin for ECSpresso\n *\n * Provides a declarative camera with world/screen coordinate conversion, smooth follow,\n * trauma-based shake, bounds clamping, cursor-centered zoom, and logical viewport dimensions.\n *\n * This plugin is renderer-agnostic. PixiJS or other renderer integration (applying\n * cameraState to a container/stage transform) is the consumer's responsibility.\n *\n * Camera uses its own x/y/zoom/rotation rather than localTransform/worldTransform.\n * It reads the target entity's worldTransform for follow, but doesn't participate\n * in the transform hierarchy itself.\n */\n\nimport { definePlugin } from 'ecspresso';\nimport type { SystemPhase } from 'ecspresso';\nimport type ECSpresso from 'ecspresso';\nimport type { ComponentsConfig, ResourcesConfig } from '../../type-utils';\nimport type { TransformWorldConfig } from './transform';\n\n// ==================== Component Types ====================\n\nexport interface Camera {\n\tx: number;\n\ty: number;\n\tzoom: number;\n\trotation: number;\n}\n\nexport interface CameraFollow {\n\ttarget: number;\n\tsmoothing: number;\n\tdeadzoneX: number;\n\tdeadzoneY: number;\n\toffsetX: number;\n\toffsetY: number;\n}\n\nexport interface CameraShake {\n\ttrauma: number;\n\ttraumaDecay: number;\n\tmaxOffsetX: number;\n\tmaxOffsetY: number;\n\tmaxRotation: number;\n}\n\nexport interface CameraBounds {\n\tminX: number;\n\tminY: number;\n\tmaxX: number;\n\tmaxY: number;\n}\n\nexport interface CameraComponentTypes {\n\tcamera: Camera;\n\tcameraFollow: CameraFollow;\n\tcameraShake: CameraShake;\n\tcameraBounds: CameraBounds;\n}\n\n// ==================== Resource Types ====================\n\nexport interface FollowOptions {\n\tsmoothing?: number;\n\tdeadzoneX?: number;\n\tdeadzoneY?: number;\n\toffsetX?: number;\n\toffsetY?: number;\n}\n\nexport type EntityHandle = { id: number };\n\nexport interface CameraState {\n\t// Read-only data (synced from camera entity each frame)\n\tx: number;\n\ty: number;\n\tzoom: number;\n\trotation: number;\n\tshakeOffsetX: number;\n\tshakeOffsetY: number;\n\tshakeRotation: number;\n\tviewportWidth: number;\n\tviewportHeight: number;\n\tentityId: number;\n\n\t// Mutation methods\n\tfollow(target: number | EntityHandle, options?: FollowOptions): void;\n\tunfollow(): void;\n\tsetPosition(x: number, y: number): void;\n\tsetZoom(zoom: number): void;\n\tsetRotation(rotation: number): void;\n\tsetBounds(minX: number, minY: number, maxX: number, maxY: number): void;\n\tclearBounds(): void;\n\taddTrauma(amount: number): void;\n}\n\nexport interface CameraResourceTypes {\n\tcameraState: CameraState;\n}\n\n// ==================== Plugin Options ====================\n\nexport interface CameraPluginOptions<G extends string = 'camera'> {\n\tviewportWidth?: number;\n\tviewportHeight?: number;\n\tinitial?: {\n\t\tx?: number;\n\t\ty?: number;\n\t\tzoom?: number;\n\t\trotation?: number;\n\t};\n\tfollow?: FollowOptions;\n\tshake?: boolean | Partial<Omit<CameraShake, 'trauma'>>;\n\tbounds?:\n\t\t| { minX: number; minY: number; maxX: number; maxY: number }\n\t\t| [number, number, number, number];\n\tzoom?: {\n\t\tzoomStep?: number;\n\t\tminZoom?: number;\n\t\tmaxZoom?: number;\n\t};\n\tpan?: {\n\t\tspeed: number;\n\t\tactions?: {\n\t\t\tup?: string;\n\t\t\tdown?: string;\n\t\t\tleft?: string;\n\t\t\tright?: string;\n\t\t};\n\t};\n\tsystemGroup?: G;\n\tphase?: SystemPhase;\n\trandomFn?: () => number;\n}\n\n// ==================== Default Values ====================\n\nconst DEFAULT_SHAKE: Readonly<Omit<CameraShake, 'trauma'>> = {\n\ttraumaDecay: 1,\n\tmaxOffsetX: 10,\n\tmaxOffsetY: 10,\n\tmaxRotation: 0.05,\n};\n\nconst DEFAULT_FOLLOW: Readonly<Omit<CameraFollow, 'target'>> = {\n\tsmoothing: 5,\n\tdeadzoneX: 0,\n\tdeadzoneY: 0,\n\toffsetX: 0,\n\toffsetY: 0,\n};\n\n// ==================== Coordinate Conversion ====================\n\nexport function worldToScreen(\n\tworldX: number,\n\tworldY: number,\n\tstate: CameraState,\n): { x: number; y: number } {\n\tconst dx = worldX - (state.x + state.shakeOffsetX);\n\tconst dy = worldY - (state.y + state.shakeOffsetY);\n\n\tconst angle = -(state.rotation + state.shakeRotation);\n\tconst cos = Math.cos(angle);\n\tconst sin = Math.sin(angle);\n\tconst rx = dx * cos - dy * sin;\n\tconst ry = dx * sin + dy * cos;\n\n\treturn {\n\t\tx: rx * state.zoom + state.viewportWidth / 2,\n\t\ty: ry * state.zoom + state.viewportHeight / 2,\n\t};\n}\n\nexport function screenToWorld(\n\tscreenX: number,\n\tscreenY: number,\n\tstate: CameraState,\n): { x: number; y: number } {\n\tconst cx = (screenX - state.viewportWidth / 2) / state.zoom;\n\tconst cy = (screenY - state.viewportHeight / 2) / state.zoom;\n\n\tconst angle = state.rotation + state.shakeRotation;\n\tconst cos = Math.cos(angle);\n\tconst sin = Math.sin(angle);\n\tconst rx = cx * cos - cy * sin;\n\tconst ry = cx * sin + cy * cos;\n\n\treturn {\n\t\tx: rx + state.x + state.shakeOffsetX,\n\t\ty: ry + state.y + state.shakeOffsetY,\n\t};\n}\n\n// ==================== Internal Helpers ====================\n\nfunction resolveTarget(target: number | EntityHandle): number {\n\treturn typeof target === 'number' ? target : target.id;\n}\n\nfunction resolveShakeOptions(shake: true | Partial<Omit<CameraShake, 'trauma'>>): CameraShake {\n\tconst opts = shake === true ? {} : shake;\n\treturn {\n\t\ttrauma: 0,\n\t\ttraumaDecay: opts.traumaDecay ?? DEFAULT_SHAKE.traumaDecay,\n\t\tmaxOffsetX: opts.maxOffsetX ?? DEFAULT_SHAKE.maxOffsetX,\n\t\tmaxOffsetY: opts.maxOffsetY ?? DEFAULT_SHAKE.maxOffsetY,\n\t\tmaxRotation: opts.maxRotation ?? DEFAULT_SHAKE.maxRotation,\n\t};\n}\n\nfunction resolveBounds(\n\tbounds: { minX: number; minY: number; maxX: number; maxY: number } | [number, number, number, number],\n): CameraBounds {\n\tif (Array.isArray(bounds)) {\n\t\treturn { minX: bounds[0], minY: bounds[1], maxX: bounds[2], maxY: bounds[3] };\n\t}\n\treturn { ...bounds };\n}\n\nfunction resolveFollowOptions(options?: FollowOptions): Omit<CameraFollow, 'target'> {\n\treturn {\n\t\tsmoothing: options?.smoothing ?? DEFAULT_FOLLOW.smoothing,\n\t\tdeadzoneX: options?.deadzoneX ?? DEFAULT_FOLLOW.deadzoneX,\n\t\tdeadzoneY: options?.deadzoneY ?? DEFAULT_FOLLOW.deadzoneY,\n\t\toffsetX: options?.offsetX ?? DEFAULT_FOLLOW.offsetX,\n\t\toffsetY: options?.offsetY ?? DEFAULT_FOLLOW.offsetY,\n\t};\n}\n\n// ==================== Plugin Factory ====================\n\ntype CameraWorldConfig =\n\tComponentsConfig<CameraComponentTypes>\n\t& ResourcesConfig<CameraResourceTypes>;\n\ntype CameraLabels =\n\t| 'camera-init'\n\t| 'camera-follow'\n\t| 'camera-shake-update'\n\t| 'camera-bounds'\n\t| 'camera-state-sync'\n\t| 'camera-zoom'\n\t| 'camera-pan';\n\nexport function createCameraPlugin<G extends string = 'camera'>(\n\toptions?: CameraPluginOptions<G>,\n) {\n\tconst {\n\t\tviewportWidth = 800,\n\t\tviewportHeight = 600,\n\t\tinitial,\n\t\tfollow: followConfig,\n\t\tshake: shakeConfig,\n\t\tbounds: boundsConfig,\n\t\tzoom: zoomConfig,\n\t\tpan: panConfig,\n\t\tsystemGroup = 'camera',\n\t\tphase = 'postUpdate',\n\t\trandomFn = Math.random,\n\t} = options ?? {};\n\n\treturn definePlugin('camera')\n\t\t.withComponentTypes<CameraComponentTypes>()\n\t\t.withResourceTypes<CameraResourceTypes>()\n\t\t.withLabels<CameraLabels>()\n\t\t.withGroups<G>()\n\t\t.requires<TransformWorldConfig>()\n\t\t.install((world) => {\n\t\t\t// Build mutation methods as closures over the world reference.\n\t\t\t// The cameraState resource is created immediately with placeholder methods,\n\t\t\t// then the init system populates entityId and wires up real methods.\n\n\t\t\tconst cameraState: CameraState = {\n\t\t\t\tx: initial?.x ?? 0,\n\t\t\t\ty: initial?.y ?? 0,\n\t\t\t\tzoom: initial?.zoom ?? 1,\n\t\t\t\trotation: initial?.rotation ?? 0,\n\t\t\t\tshakeOffsetX: 0,\n\t\t\t\tshakeOffsetY: 0,\n\t\t\t\tshakeRotation: 0,\n\t\t\t\tviewportWidth,\n\t\t\t\tviewportHeight,\n\t\t\t\tentityId: -1,\n\n\t\t\t\t// Mutation methods — wired up after camera entity is spawned\n\t\t\t\tfollow: () => {},\n\t\t\t\tunfollow: () => {},\n\t\t\t\tsetPosition: () => {},\n\t\t\t\tsetZoom: () => {},\n\t\t\t\tsetRotation: () => {},\n\t\t\t\tsetBounds: () => {},\n\t\t\t\tclearBounds: () => {},\n\t\t\t\taddTrauma: () => {},\n\t\t\t};\n\n\t\t\tworld.addResource('cameraState', cameraState);\n\n\t\t\t// camera-init: spawns camera entity and wires up mutation closures\n\t\t\tworld\n\t\t\t\t.addSystem('camera-init')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setOnInitialize((ecs: ECSpresso<CameraWorldConfig & TransformWorldConfig>) => {\n\t\t\t\t\t// Spawn with required camera component\n\t\t\t\t\tconst entity = ecs.spawn({\n\t\t\t\t\t\tcamera: {\n\t\t\t\t\t\t\tx: initial?.x ?? 0,\n\t\t\t\t\t\t\ty: initial?.y ?? 0,\n\t\t\t\t\t\t\tzoom: initial?.zoom ?? 1,\n\t\t\t\t\t\t\trotation: initial?.rotation ?? 0,\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\n\t\t\t\t\t// Conditionally add optional components\n\t\t\t\t\tif (followConfig) {\n\t\t\t\t\t\tecs.addComponent(entity.id, 'cameraFollow', {\n\t\t\t\t\t\t\ttarget: -1,\n\t\t\t\t\t\t\t...resolveFollowOptions(followConfig),\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\tif (shakeConfig) {\n\t\t\t\t\t\tecs.addComponent(entity.id, 'cameraShake', resolveShakeOptions(shakeConfig));\n\t\t\t\t\t}\n\n\t\t\t\t\tif (boundsConfig) {\n\t\t\t\t\t\tecs.addComponent(entity.id, 'cameraBounds', resolveBounds(boundsConfig));\n\t\t\t\t\t}\n\t\t\t\t\tcameraState.entityId = entity.id;\n\n\t\t\t\t\t// Wire up mutation methods\n\t\t\t\t\tcameraState.follow = (target: number | EntityHandle, opts?: FollowOptions) => {\n\t\t\t\t\t\tconst targetId = resolveTarget(target);\n\t\t\t\t\t\tconst followData: CameraFollow = {\n\t\t\t\t\t\t\ttarget: targetId,\n\t\t\t\t\t\t\t...resolveFollowOptions(opts),\n\t\t\t\t\t\t};\n\t\t\t\t\t\tconst existing = ecs.getComponent(cameraState.entityId, 'cameraFollow');\n\t\t\t\t\t\tif (existing) {\n\t\t\t\t\t\t\texisting.target = followData.target;\n\t\t\t\t\t\t\texisting.smoothing = followData.smoothing;\n\t\t\t\t\t\t\texisting.deadzoneX = followData.deadzoneX;\n\t\t\t\t\t\t\texisting.deadzoneY = followData.deadzoneY;\n\t\t\t\t\t\t\texisting.offsetX = followData.offsetX;\n\t\t\t\t\t\t\texisting.offsetY = followData.offsetY;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tecs.addComponent(cameraState.entityId, 'cameraFollow', followData);\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\n\t\t\t\t\tcameraState.unfollow = () => {\n\t\t\t\t\t\tconst existing = ecs.getComponent(cameraState.entityId, 'cameraFollow');\n\t\t\t\t\t\tif (existing) {\n\t\t\t\t\t\t\tecs.removeComponent(cameraState.entityId, 'cameraFollow');\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\n\t\t\t\t\tcameraState.setPosition = (x: number, y: number) => {\n\t\t\t\t\t\tconst camera = ecs.getComponent(cameraState.entityId, 'camera');\n\t\t\t\t\t\tif (!camera) return;\n\t\t\t\t\t\tcamera.x = x;\n\t\t\t\t\t\tcamera.y = y;\n\t\t\t\t\t};\n\n\t\t\t\t\tcameraState.setZoom = (zoom: number) => {\n\t\t\t\t\t\tconst camera = ecs.getComponent(cameraState.entityId, 'camera');\n\t\t\t\t\t\tif (!camera) return;\n\t\t\t\t\t\tcamera.zoom = zoom;\n\t\t\t\t\t};\n\n\t\t\t\t\tcameraState.setRotation = (rotation: number) => {\n\t\t\t\t\t\tconst camera = ecs.getComponent(cameraState.entityId, 'camera');\n\t\t\t\t\t\tif (!camera) return;\n\t\t\t\t\t\tcamera.rotation = rotation;\n\t\t\t\t\t};\n\n\t\t\t\t\tcameraState.setBounds = (minX: number, minY: number, maxX: number, maxY: number) => {\n\t\t\t\t\t\tconst existing = ecs.getComponent(cameraState.entityId, 'cameraBounds');\n\t\t\t\t\t\tif (existing) {\n\t\t\t\t\t\t\texisting.minX = minX;\n\t\t\t\t\t\t\texisting.minY = minY;\n\t\t\t\t\t\t\texisting.maxX = maxX;\n\t\t\t\t\t\t\texisting.maxY = maxY;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tecs.addComponent(cameraState.entityId, 'cameraBounds', { minX, minY, maxX, maxY });\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\n\t\t\t\t\tcameraState.clearBounds = () => {\n\t\t\t\t\t\tconst existing = ecs.getComponent(cameraState.entityId, 'cameraBounds');\n\t\t\t\t\t\tif (existing) {\n\t\t\t\t\t\t\tecs.removeComponent(cameraState.entityId, 'cameraBounds');\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\n\t\t\t\t\tcameraState.addTrauma = (amount: number) => {\n\t\t\t\t\t\tconst shake = ecs.getComponent(cameraState.entityId, 'cameraShake');\n\t\t\t\t\t\tif (shake) {\n\t\t\t\t\t\t\tshake.trauma = Math.min(1, Math.max(0, shake.trauma + amount));\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tecs.addComponent(cameraState.entityId, 'cameraShake', {\n\t\t\t\t\t\t\t\t...resolveShakeOptions(true),\n\t\t\t\t\t\t\t\ttrauma: Math.min(1, Math.max(0, amount)),\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\n\t\t\t// camera-follow: priority 400 (after transform propagation at 500)\n\t\t\tworld\n\t\t\t\t.addSystem('camera-follow')\n\t\t\t\t.setPriority(400)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('cameras', {\n\t\t\t\t\twith: ['camera', 'cameraFollow'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, dt, ecs }) => {\n\t\t\t\t\tconst t = Math.min(1, dt);\n\t\t\t\t\tfor (const entity of queries.cameras) {\n\t\t\t\t\t\tconst { camera, cameraFollow } = entity.components;\n\t\t\t\t\t\tif (cameraFollow.target < 0) continue;\n\n\t\t\t\t\t\tconst targetWorld = ecs.getComponent(cameraFollow.target, 'worldTransform');\n\t\t\t\t\t\tif (!targetWorld) continue;\n\n\t\t\t\t\t\tconst goalX = targetWorld.x + cameraFollow.offsetX;\n\t\t\t\t\t\tconst goalY = targetWorld.y + cameraFollow.offsetY;\n\t\t\t\t\t\tconst dx = goalX - camera.x;\n\t\t\t\t\t\tconst dy = goalY - camera.y;\n\n\t\t\t\t\t\tif (Math.abs(dx) > cameraFollow.deadzoneX) {\n\t\t\t\t\t\t\tconst sign = dx > 0 ? 1 : -1;\n\t\t\t\t\t\t\tconst excessX = dx - sign * cameraFollow.deadzoneX;\n\t\t\t\t\t\t\tconst factor = Math.min(1, cameraFollow.smoothing * t);\n\t\t\t\t\t\t\tcamera.x += excessX * factor;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (Math.abs(dy) > cameraFollow.deadzoneY) {\n\t\t\t\t\t\t\tconst sign = dy > 0 ? 1 : -1;\n\t\t\t\t\t\t\tconst excessY = dy - sign * cameraFollow.deadzoneY;\n\t\t\t\t\t\t\tconst factor = Math.min(1, cameraFollow.smoothing * t);\n\t\t\t\t\t\t\tcamera.y += excessY * factor;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// camera-shake-update: priority 390\n\t\t\tworld\n\t\t\t\t.addSystem('camera-shake-update')\n\t\t\t\t.setPriority(390)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('shakeCameras', {\n\t\t\t\t\twith: ['camera', 'cameraShake'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, dt }) => {\n\t\t\t\t\tfor (const entity of queries.shakeCameras) {\n\t\t\t\t\t\tconst { cameraShake } = entity.components;\n\t\t\t\t\t\tcameraShake.trauma = Math.max(0, cameraShake.trauma - cameraShake.traumaDecay * dt);\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// camera-bounds: priority 380\n\t\t\tworld\n\t\t\t\t.addSystem('camera-bounds')\n\t\t\t\t.setPriority(380)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('boundedCameras', {\n\t\t\t\t\twith: ['camera', 'cameraBounds'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries }) => {\n\t\t\t\t\tfor (const entity of queries.boundedCameras) {\n\t\t\t\t\t\tconst { camera, cameraBounds } = entity.components;\n\t\t\t\t\t\tconst halfW = cameraState.viewportWidth / (2 * camera.zoom);\n\t\t\t\t\t\tconst halfH = cameraState.viewportHeight / (2 * camera.zoom);\n\n\t\t\t\t\t\tconst effectiveMinX = cameraBounds.minX + halfW;\n\t\t\t\t\t\tconst effectiveMaxX = cameraBounds.maxX - halfW;\n\t\t\t\t\t\tconst effectiveMinY = cameraBounds.minY + halfH;\n\t\t\t\t\t\tconst effectiveMaxY = cameraBounds.maxY - halfH;\n\n\t\t\t\t\t\tif (effectiveMinX > effectiveMaxX) {\n\t\t\t\t\t\t\tcamera.x = (cameraBounds.minX + cameraBounds.maxX) / 2;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tcamera.x = Math.max(effectiveMinX, Math.min(effectiveMaxX, camera.x));\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (effectiveMinY > effectiveMaxY) {\n\t\t\t\t\t\t\tcamera.y = (cameraBounds.minY + cameraBounds.maxY) / 2;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tcamera.y = Math.max(effectiveMinY, Math.min(effectiveMaxY, camera.y));\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// camera-state-sync: priority 370\n\t\t\tworld\n\t\t\t\t.addSystem('camera-state-sync')\n\t\t\t\t.setPriority(370)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setProcess(({ ecs }) => {\n\t\t\t\t\tconst camera = ecs.getComponent(cameraState.entityId, 'camera');\n\t\t\t\t\tif (!camera) {\n\t\t\t\t\t\tcameraState.x = 0;\n\t\t\t\t\t\tcameraState.y = 0;\n\t\t\t\t\t\tcameraState.zoom = 1;\n\t\t\t\t\t\tcameraState.rotation = 0;\n\t\t\t\t\t\tcameraState.shakeOffsetX = 0;\n\t\t\t\t\t\tcameraState.shakeOffsetY = 0;\n\t\t\t\t\t\tcameraState.shakeRotation = 0;\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tcameraState.x = camera.x;\n\t\t\t\t\tcameraState.y = camera.y;\n\t\t\t\t\tcameraState.zoom = camera.zoom;\n\t\t\t\t\tcameraState.rotation = camera.rotation;\n\n\t\t\t\t\tconst shake = ecs.getComponent(cameraState.entityId, 'cameraShake');\n\t\t\t\t\tif (shake && shake.trauma > 0) {\n\t\t\t\t\t\tconst intensity = shake.trauma * shake.trauma;\n\t\t\t\t\t\tcameraState.shakeOffsetX = shake.maxOffsetX * intensity * (randomFn() * 2 - 1);\n\t\t\t\t\t\tcameraState.shakeOffsetY = shake.maxOffsetY * intensity * (randomFn() * 2 - 1);\n\t\t\t\t\t\tcameraState.shakeRotation = shake.maxRotation * intensity * (randomFn() * 2 - 1);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tcameraState.shakeOffsetX = 0;\n\t\t\t\t\t\tcameraState.shakeOffsetY = 0;\n\t\t\t\t\t\tcameraState.shakeRotation = 0;\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// camera-zoom: conditionally registered when zoom option is provided\n\t\t\tif (zoomConfig) {\n\t\t\t\tconst {\n\t\t\t\t\tzoomStep = 0.1,\n\t\t\t\t\tminZoom = 0.1,\n\t\t\t\t\tmaxZoom = 10,\n\t\t\t\t} = zoomConfig;\n\n\t\t\t\ttype ZoomInputState = { pointer: { position: { x: number; y: number } } };\n\n\t\t\t\tlet pendingSteps = 0;\n\t\t\t\tlet zoomActive = false;\n\t\t\t\tlet canvas: HTMLCanvasElement | undefined;\n\t\t\t\tlet isoState: { tileWidth: number; tileHeight: number; originX: number; originY: number } | undefined;\n\n\t\t\t\tfunction onWheel(e: WheelEvent) {\n\t\t\t\t\te.preventDefault();\n\t\t\t\t\tpendingSteps += Math.sign(e.deltaY);\n\t\t\t\t}\n\n\t\t\t\tworld\n\t\t\t\t\t.addSystem('camera-zoom')\n\t\t\t\t\t.setPriority(410)\n\t\t\t\t\t.inPhase('preUpdate')\n\t\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t\t.addQuery('cameras', {\n\t\t\t\t\t\twith: ['camera'],\n\t\t\t\t\t})\n\t\t\t\t\t.setOnInitialize((ecs) => {\n\t\t\t\t\t\t// Check for required dependencies\n\t\t\t\t\t\tconst inputState = ecs.tryGetResource<ZoomInputState>('inputState');\n\t\t\t\t\t\tconst pixiApp = ecs.tryGetResource<{ canvas: HTMLCanvasElement }>('pixiApp');\n\n\t\t\t\t\t\tif (!inputState || !pixiApp) {\n\t\t\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t\t\t'[camera] zoom requires the input plugin and renderer2D plugin. ' +\n\t\t\t\t\t\t\t\t'Zoom will be disabled.',\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tcanvas = pixiApp.canvas;\n\t\t\t\t\t\tcanvas.addEventListener('wheel', onWheel as EventListener, { passive: false });\n\n\t\t\t\t\t\t// Detect isometric projection for iso-aware cursor-centered zoom\n\t\t\t\t\t\tisoState = ecs.tryGetResource<{\n\t\t\t\t\t\t\ttileWidth: number; tileHeight: number;\n\t\t\t\t\t\t\toriginX: number; originY: number;\n\t\t\t\t\t\t}>('isoProjection');\n\n\t\t\t\t\t\tzoomActive = true;\n\t\t\t\t\t})\n\t\t\t\t\t.setOnDetach(() => {\n\t\t\t\t\t\tif (!zoomActive || !canvas) return;\n\t\t\t\t\t\tcanvas.removeEventListener('wheel', onWheel as EventListener);\n\t\t\t\t\t})\n\t\t\t\t\t.setProcess(({ queries, ecs }) => {\n\t\t\t\t\t\tif (!zoomActive || pendingSteps === 0) return;\n\n\t\t\t\t\t\tconst steps = pendingSteps;\n\t\t\t\t\t\tpendingSteps = 0;\n\n\t\t\t\t\t\tconst [cameraEntity] = queries.cameras;\n\t\t\t\t\t\tif (!cameraEntity) return;\n\n\t\t\t\t\t\tconst cam = cameraEntity.components.camera;\n\t\t\t\t\t\tconst inputState = ecs.tryGetResource<ZoomInputState>('inputState');\n\t\t\t\t\t\tif (!inputState) return;\n\n\t\t\t\t\t\t// Apply zoom — proportional to number of wheel steps\n\t\t\t\t\t\tconst direction = steps > 0 ? (1 - zoomStep) : (1 + zoomStep);\n\t\t\t\t\t\tconst newZoom = Math.max(minZoom, Math.min(maxZoom, cam.zoom * Math.pow(direction, Math.abs(steps))));\n\n\t\t\t\t\t\tif (isoState && canvas) {\n\t\t\t\t\t\t\t// Iso-aware cursor-centered zoom: work in iso-screen space\n\t\t\t\t\t\t\tconst rect = canvas.getBoundingClientRect();\n\t\t\t\t\t\t\tconst screenOffX = inputState.pointer.position.x - (rect.left + rect.width / 2);\n\t\t\t\t\t\t\tconst screenOffY = inputState.pointer.position.y - (rect.top + rect.height / 2);\n\n\t\t\t\t\t\t\t// Inlined worldToIso — avoids cross-plugin import\n\t\t\t\t\t\t\tconst halfW = isoState.tileWidth / 2;\n\t\t\t\t\t\t\tconst halfH = isoState.tileHeight / 2;\n\t\t\t\t\t\t\tconst camIsoX = (cam.x - cam.y) * halfW + isoState.originX;\n\t\t\t\t\t\t\tconst camIsoY = (cam.x + cam.y) * halfH + isoState.originY;\n\t\t\t\t\t\t\tconst isoBeforeX = camIsoX + screenOffX / cam.zoom;\n\t\t\t\t\t\t\tconst isoBeforeY = camIsoY + screenOffY / cam.zoom;\n\n\t\t\t\t\t\t\tcam.zoom = newZoom;\n\n\t\t\t\t\t\t\t// New camera iso position so the same point stays under cursor\n\t\t\t\t\t\t\tconst newCamIsoX = isoBeforeX - screenOffX / newZoom;\n\t\t\t\t\t\t\tconst newCamIsoY = isoBeforeY - screenOffY / newZoom;\n\n\t\t\t\t\t\t\t// Inlined isoToWorld\n\t\t\t\t\t\t\tconst relX = newCamIsoX - isoState.originX;\n\t\t\t\t\t\t\tconst relY = newCamIsoY - isoState.originY;\n\t\t\t\t\t\t\tcam.x = relX / isoState.tileWidth + relY / isoState.tileHeight;\n\t\t\t\t\t\t\tcam.y = -relX / isoState.tileWidth + relY / isoState.tileHeight;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Pixel-space cursor-centered zoom\n\t\t\t\t\t\t\tconst worldBefore = screenToWorld(\n\t\t\t\t\t\t\t\tinputState.pointer.position.x,\n\t\t\t\t\t\t\t\tinputState.pointer.position.y,\n\t\t\t\t\t\t\t\tcameraState,\n\t\t\t\t\t\t\t);\n\n\t\t\t\t\t\t\tcam.zoom = newZoom;\n\n\t\t\t\t\t\t\tcam.x = worldBefore.x - (inputState.pointer.position.x - cameraState.viewportWidth / 2) / newZoom;\n\t\t\t\t\t\t\tcam.y = worldBefore.y - (inputState.pointer.position.y - cameraState.viewportHeight / 2) / newZoom;\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t}\n\n\t\t\t// camera-pan: conditionally registered when pan option is provided\n\t\t\tif (panConfig) {\n\t\t\t\ttype PanInputState = { actions: { isActive(action: string): boolean } };\n\n\t\t\t\tconst {\n\t\t\t\t\tspeed,\n\t\t\t\t\tactions: panActions,\n\t\t\t\t} = panConfig;\n\n\t\t\t\tconst actionUp = panActions?.up ?? 'panUp';\n\t\t\t\tconst actionDown = panActions?.down ?? 'panDown';\n\t\t\t\tconst actionLeft = panActions?.left ?? 'panLeft';\n\t\t\t\tconst actionRight = panActions?.right ?? 'panRight';\n\n\t\t\t\tlet panActive = false;\n\n\t\t\t\tworld\n\t\t\t\t\t.addSystem('camera-pan')\n\t\t\t\t\t.setPriority(420)\n\t\t\t\t\t.inPhase('preUpdate')\n\t\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t\t.setOnInitialize((ecs) => {\n\t\t\t\t\t\tconst inputState = ecs.tryGetResource<PanInputState>('inputState');\n\t\t\t\t\t\tif (!inputState) {\n\t\t\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t\t\t'[camera] pan requires the input plugin. Pan will be disabled.',\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tpanActive = true;\n\t\t\t\t\t})\n\t\t\t\t\t.setProcess(({ ecs, dt }) => {\n\t\t\t\t\t\tif (!panActive) return;\n\n\t\t\t\t\t\tconst inputState = ecs.tryGetResource<PanInputState>('inputState');\n\t\t\t\t\t\tif (!inputState) return;\n\n\t\t\t\t\t\tconst delta = (speed / cameraState.zoom) * dt;\n\t\t\t\t\t\tconst dx = (inputState.actions.isActive(actionRight) ? 1 : 0)\n\t\t\t\t\t\t\t- (inputState.actions.isActive(actionLeft) ? 1 : 0);\n\t\t\t\t\t\tconst dy = (inputState.actions.isActive(actionDown) ? 1 : 0)\n\t\t\t\t\t\t\t- (inputState.actions.isActive(actionUp) ? 1 : 0);\n\n\t\t\t\t\t\tif (dx !== 0 || dy !== 0) {\n\t\t\t\t\t\t\tcameraState.setPosition(\n\t\t\t\t\t\t\t\tcameraState.x + dx * delta,\n\t\t\t\t\t\t\t\tcameraState.y + dy * delta,\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}\n\t\t});\n}\n",
6
+ "/**\n * Selection Plugin for ECSpresso\n *\n * Provides pointer-driven entity selection via box-drag and click.\n * Entities with a `selectable` component can be selected by the user.\n * Selected entities receive a `selected` component that other systems\n * can query for.\n *\n * Requires the input plugin (for pointer state) and the renderer2D plugin\n * (for graphics rendering of the selection box).\n *\n * Camera-aware: when a `cameraState` resource is present (from the camera\n * plugin), pointer coordinates are automatically converted to world space\n * for hit-testing. The selection box overlay remains in screen space.\n */\n\nimport { Graphics } from 'pixi.js';\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type { ComponentsConfig, ResourcesConfig } from 'ecspresso';\nimport type { InputResourceTypes } from './input';\nimport type { Renderer2DComponentTypes, Renderer2DResourceTypes } from '../rendering/renderer2D';\nimport type { CameraState } from '../spatial/camera';\nimport { screenToWorld } from '../spatial/camera';\n\n// ==================== Component Types ====================\n\n/**\n * Component types provided by the selection plugin.\n */\nexport interface SelectionComponentTypes {\n\t/** Tag marking an entity as eligible for selection */\n\tselectable: true;\n\t/** Tag marking an entity as currently selected (added/removed dynamically) */\n\tselected: true;\n}\n\n// ==================== Resource Types ====================\n\n/**\n * Internal state tracking the current drag selection.\n */\nexport interface SelectionState {\n\tdragStart: { x: number; y: number };\n\tboxEntityId: number | null;\n}\n\n/**\n * Resource types provided by the selection plugin.\n */\nexport interface SelectionResourceTypes {\n\tselectionState: SelectionState;\n}\n\n// ==================== WorldConfig ====================\n\n/**\n * WorldConfig representing the selection plugin's provided types.\n */\nexport type SelectionWorldConfig =\n\tComponentsConfig<SelectionComponentTypes>\n\t& ResourcesConfig<SelectionResourceTypes>;\n\n// ==================== Dependency Types ====================\n\ntype SelectionRequires =\n\tComponentsConfig<Renderer2DComponentTypes>\n\t& ResourcesConfig<InputResourceTypes & Renderer2DResourceTypes>;\n\n// ==================== Plugin Options ====================\n\n/**\n * Configuration options for the selection plugin.\n */\nexport interface SelectionPluginOptions<G extends string = 'selection'> extends BasePluginOptions<G> {\n\t/** Minimum drag distance (px) to trigger box select vs click select (default: 5) */\n\tclickThreshold?: number;\n\t/** Selection box fill color (default: 0x00FF00) */\n\tboxFillColor?: number;\n\t/** Selection box fill alpha (default: 0.15) */\n\tboxFillAlpha?: number;\n\t/** Selection box stroke color (default: 0x00FF00) */\n\tboxStrokeColor?: number;\n\t/** Selection box stroke alpha (default: 0.8) */\n\tboxStrokeAlpha?: number;\n\t/** Tint applied to selected entities' sprites (default: 0x44FF44) */\n\tselectedTint?: number;\n\t/** Render layer for the selection box entity (default: undefined) */\n\trenderLayer?: string;\n}\n\n// ==================== Helper Functions ====================\n\n/**\n * Create a selectable component.\n *\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createTransform(100, 200),\n * sprite,\n * ...createSelectable(),\n * });\n * ```\n */\nexport function createSelectable(): Pick<SelectionComponentTypes, 'selectable'> {\n\treturn { selectable: true };\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a selection plugin for ECSpresso.\n *\n * Provides:\n * - Box-drag selection (left-click drag to select multiple entities)\n * - Click selection (left-click to select a single entity)\n * - Visual feedback (configurable sprite tint for selected entities)\n * - Selection box overlay (rendered as a PixiJS Graphics entity)\n * - Automatic camera-awareness when cameraState resource is present\n *\n * Requires the input plugin and renderer2D plugin to be installed.\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createRenderer2DPlugin({ renderLayers: ['game', 'ui'] }))\n * .withPlugin(createInputPlugin())\n * .withPlugin(createSelectionPlugin({ renderLayer: 'ui' }))\n * .build();\n *\n * await ecs.initialize();\n *\n * ecs.spawn({\n * sprite,\n * ...createTransform(100, 200),\n * ...createSelectable(),\n * });\n * ```\n */\nexport function createSelectionPlugin<G extends string = 'selection'>(\n\toptions?: SelectionPluginOptions<G>\n) {\n\tconst {\n\t\tsystemGroup = 'selection',\n\t\tpriority = 100,\n\t\tphase = 'preUpdate',\n\t\tclickThreshold = 5,\n\t\tboxFillColor = 0x00FF00,\n\t\tboxFillAlpha = 0.15,\n\t\tboxStrokeColor = 0x00FF00,\n\t\tboxStrokeAlpha = 0.8,\n\t\tselectedTint = 0x44FF44,\n\t\trenderLayer,\n\t} = options ?? {};\n\n\t// Pre-allocate draw options to avoid per-frame allocations during drag\n\tconst fillOptions = { color: boxFillColor, alpha: boxFillAlpha };\n\tconst strokeOptions = { color: boxStrokeColor, width: 1.5, alpha: boxStrokeAlpha };\n\n\treturn definePlugin('selection')\n\t\t.withComponentTypes<SelectionComponentTypes>()\n\t\t.withResourceTypes<SelectionResourceTypes>()\n\t\t.withLabels<'selection-input' | 'selection-visual'>()\n\t\t.withGroups<G>()\n\t\t.requires<SelectionRequires>()\n\t\t.install((world) => {\n\t\t\tworld.addResource('selectionState', {\n\t\t\t\tdragStart: { x: 0, y: 0 },\n\t\t\t\tboxEntityId: null,\n\t\t\t});\n\n\t\t\tlet preventContextMenu: ((e: Event) => void) | null = null;\n\n\t\t\tworld\n\t\t\t\t.addSystem('selection-input')\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('selectables', {\n\t\t\t\t\twith: ['selectable', 'worldTransform'],\n\t\t\t\t})\n\t\t\t\t.addQuery('currentlySelected', {\n\t\t\t\t\twith: ['selected'],\n\t\t\t\t})\n\t\t\t\t.withResources(['inputState', 'selectionState', 'pixiApp'])\n\t\t\t\t.setOnInitialize((ecs) => {\n\t\t\t\t\tconst pixiApp = ecs.getResource('pixiApp');\n\t\t\t\t\tpreventContextMenu = (e: Event) => e.preventDefault();\n\t\t\t\t\tpixiApp.canvas.addEventListener('contextmenu', preventContextMenu);\n\t\t\t\t})\n\t\t\t\t.setOnDetach((ecs) => {\n\t\t\t\t\tif (!preventContextMenu) return;\n\t\t\t\t\tconst pixiApp = ecs.getResource('pixiApp');\n\t\t\t\t\tpixiApp.canvas.removeEventListener('contextmenu', preventContextMenu);\n\t\t\t\t\tpreventContextMenu = null;\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, ecs, resources }) => {\n\t\t\t\t\tconst { inputState: input, selectionState } = resources;\n\t\t\t\t\tconst pointer = input.pointer;\n\n\t\t\t\t\t// Start drag\n\t\t\t\t\tif (pointer.justPressed(0)) {\n\t\t\t\t\t\t// Clean up any orphaned box entity from an interrupted drag\n\t\t\t\t\t\tif (selectionState.boxEntityId !== null) {\n\t\t\t\t\t\t\tecs.commands.removeEntity(selectionState.boxEntityId);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tselectionState.dragStart.x = pointer.position.x;\n\t\t\t\t\t\tselectionState.dragStart.y = pointer.position.y;\n\n\t\t\t\t\t\tconst boxEntity = ecs.spawn({\n\t\t\t\t\t\t\tgraphics: new Graphics(),\n\t\t\t\t\t\t});\n\t\t\t\t\t\tif (renderLayer) {\n\t\t\t\t\t\t\tecs.addComponent(boxEntity.id, 'renderLayer', renderLayer);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tselectionState.boxEntityId = boxEntity.id;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Update drag visual (screen-space — no camera conversion)\n\t\t\t\t\tif (pointer.isDown(0) && selectionState.boxEntityId !== null) {\n\t\t\t\t\t\tconst g = ecs.getComponent(selectionState.boxEntityId, 'graphics');\n\t\t\t\t\t\tif (!g) return;\n\n\t\t\t\t\t\tconst startX = selectionState.dragStart.x;\n\t\t\t\t\t\tconst startY = selectionState.dragStart.y;\n\t\t\t\t\t\tconst curX = pointer.position.x;\n\t\t\t\t\t\tconst curY = pointer.position.y;\n\t\t\t\t\t\tconst minX = Math.min(startX, curX);\n\t\t\t\t\t\tconst minY = Math.min(startY, curY);\n\t\t\t\t\t\tconst w = Math.abs(curX - startX);\n\t\t\t\t\t\tconst h = Math.abs(curY - startY);\n\n\t\t\t\t\t\tg.clear();\n\t\t\t\t\t\tg.rect(minX, minY, w, h);\n\t\t\t\t\t\tg.fill(fillOptions);\n\t\t\t\t\t\tg.stroke(strokeOptions);\n\t\t\t\t\t}\n\n\t\t\t\t\t// End drag — perform selection\n\t\t\t\t\tif (!pointer.justReleased(0) || selectionState.boxEntityId === null) return;\n\n\t\t\t\t\tconst startX = selectionState.dragStart.x;\n\t\t\t\t\tconst startY = selectionState.dragStart.y;\n\t\t\t\t\tconst endX = pointer.position.x;\n\t\t\t\t\tconst endY = pointer.position.y;\n\n\t\t\t\t\tconst w = Math.abs(endX - startX);\n\t\t\t\t\tconst h = Math.abs(endY - startY);\n\n\t\t\t\t\t// Clear current selection\n\t\t\t\t\tfor (const entity of queries.currentlySelected) {\n\t\t\t\t\t\tecs.removeComponent(entity.id, 'selected');\n\t\t\t\t\t}\n\n\t\t\t\t\tconst isClick = w < clickThreshold && h < clickThreshold;\n\n\t\t\t\t\t// Convert screen coords to world space for hit-testing\n\t\t\t\t\tconst camState = ecs.tryGetResource('cameraState') as CameraState | undefined;\n\t\t\t\t\tconst worldEnd = camState\n\t\t\t\t\t\t? screenToWorld(endX, endY, camState)\n\t\t\t\t\t\t: { x: endX, y: endY };\n\n\t\t\t\t\tif (isClick) {\n\t\t\t\t\t\tconst clickRadiusSq = 400; // 20px radius in world space\n\t\t\t\t\t\tlet nearestId: number | null = null;\n\t\t\t\t\t\tlet nearestDistSq = Infinity;\n\n\t\t\t\t\t\tfor (const entity of queries.selectables) {\n\t\t\t\t\t\t\tconst { worldTransform } = entity.components;\n\t\t\t\t\t\t\tconst dx = worldTransform.x - worldEnd.x;\n\t\t\t\t\t\t\tconst dy = worldTransform.y - worldEnd.y;\n\t\t\t\t\t\t\tconst distSq = dx * dx + dy * dy;\n\t\t\t\t\t\t\tif (distSq < clickRadiusSq && distSq < nearestDistSq) {\n\t\t\t\t\t\t\t\tnearestDistSq = distSq;\n\t\t\t\t\t\t\t\tnearestId = entity.id;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (nearestId !== null) {\n\t\t\t\t\t\t\tecs.addComponent(nearestId, 'selected', true);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst worldStart = camState\n\t\t\t\t\t\t\t? screenToWorld(startX, startY, camState)\n\t\t\t\t\t\t\t: { x: startX, y: startY };\n\t\t\t\t\t\tconst minWX = Math.min(worldStart.x, worldEnd.x);\n\t\t\t\t\t\tconst maxWX = Math.max(worldStart.x, worldEnd.x);\n\t\t\t\t\t\tconst minWY = Math.min(worldStart.y, worldEnd.y);\n\t\t\t\t\t\tconst maxWY = Math.max(worldStart.y, worldEnd.y);\n\n\t\t\t\t\t\tfor (const entity of queries.selectables) {\n\t\t\t\t\t\t\tconst { worldTransform } = entity.components;\n\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\tworldTransform.x >= minWX &&\n\t\t\t\t\t\t\t\tworldTransform.x <= maxWX &&\n\t\t\t\t\t\t\t\tworldTransform.y >= minWY &&\n\t\t\t\t\t\t\t\tworldTransform.y <= maxWY\n\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\tecs.addComponent(entity.id, 'selected', true);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tecs.commands.removeEntity(selectionState.boxEntityId);\n\t\t\t\t\tselectionState.boxEntityId = null;\n\t\t\t\t});\n\n\t\t\t// Visual feedback via enter/exit callbacks — only fires on selection change\n\t\t\tworld\n\t\t\t\t.addSystem('selection-visual')\n\t\t\t\t.setPriority(priority)\n\t\t\t\t.inPhase('render')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('selectedUnits', {\n\t\t\t\t\twith: ['selected', 'sprite'],\n\t\t\t\t})\n\t\t\t\t.setOnEntityEnter('selectedUnits', ({ entity }) => {\n\t\t\t\t\tentity.components.sprite.tint = selectedTint;\n\t\t\t\t})\n\t\t\t\t.addQuery('deselectedUnits', {\n\t\t\t\t\twith: ['selectable', 'sprite'],\n\t\t\t\t\twithout: ['selected'],\n\t\t\t\t})\n\t\t\t\t.setOnEntityEnter('deselectedUnits', ({ entity }) => {\n\t\t\t\t\tentity.components.sprite.tint = 0xFFFFFF;\n\t\t\t\t});\n\t\t});\n}\n"
7
7
  ],
8
- "mappings": "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",
8
+ "mappings": "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",
9
9
  "debugId": "A100A9606EA8CF3164756E2164756E21",
10
10
  "names": []
11
11
  }
@@ -9,7 +9,7 @@
9
9
  * Enables `sortableChildren` on the root container at initialization.
10
10
  */
11
11
  import type { BasePluginOptions } from 'ecspresso';
12
- import type { WorldConfigFrom } from '../../type-utils';
12
+ import type { ComponentsConfig, ResourcesConfig } from '../../type-utils';
13
13
  import type { TransformComponentTypes } from '../spatial/transform';
14
14
  import type { Renderer2DComponentTypes, Renderer2DResourceTypes } from '../rendering/renderer2D';
15
15
  /**
@@ -20,7 +20,7 @@ import type { Renderer2DComponentTypes, Renderer2DResourceTypes } from '../rende
20
20
  export interface IsoDepthSortComponentTypes {
21
21
  depthOffset: number;
22
22
  }
23
- type IsoDepthSortRequires = WorldConfigFrom<TransformComponentTypes & Pick<Renderer2DComponentTypes, 'sprite' | 'graphics' | 'container'>, {}, Renderer2DResourceTypes>;
23
+ type IsoDepthSortRequires = ComponentsConfig<TransformComponentTypes & Pick<Renderer2DComponentTypes, 'sprite' | 'graphics' | 'container'>> & ResourcesConfig<Renderer2DResourceTypes>;
24
24
  export interface IsoDepthSortPluginOptions<G extends string = 'isometric'> extends BasePluginOptions<G> {
25
25
  /** Custom depth function. Receives world-space x/y, returns a sort key.
26
26
  * Default: `(x, y) => x + y` */
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/plugins/isometric/depth-sort.ts"],
4
4
  "sourcesContent": [
5
- "/**\n * Isometric Depth Sort Plugin for ECSpresso\n *\n * Sets PixiJS `zIndex` on entities based on their world-space position,\n * ensuring correct visual overlap in isometric rendering. Entities with\n * higher world X + Y values render in front.\n *\n * Requires `rootContainer` from the renderer2D plugin.\n * Enables `sortableChildren` on the root container at initialization.\n */\n\nimport { definePlugin } from 'ecspresso';\nimport type { BasePluginOptions } from 'ecspresso';\nimport type { WorldConfigFrom } from '../../type-utils';\nimport type { TransformComponentTypes } from '../spatial/transform';\nimport type { Renderer2DComponentTypes, Renderer2DResourceTypes } from '../rendering/renderer2D';\n\n// ==================== Component Types ====================\n\n/**\n * Optional component that offsets an entity's depth sort value.\n * Entities with a positive depthOffset render in front of entities\n * at the same world position (e.g., a player on top of a ground tile).\n */\nexport interface IsoDepthSortComponentTypes {\n\tdepthOffset: number;\n}\n\ntype IsoDepthSortRequires = WorldConfigFrom<\n\tTransformComponentTypes & Pick<Renderer2DComponentTypes, 'sprite' | 'graphics' | 'container'>,\n\t{},\n\tRenderer2DResourceTypes\n>;\n\n// ==================== Plugin Options ====================\n\nexport interface IsoDepthSortPluginOptions<G extends string = 'isometric'> extends BasePluginOptions<G> {\n\t/** Custom depth function. Receives world-space x/y, returns a sort key.\n\t * Default: `(x, y) => x + y` */\n\tdepthFn?: (worldX: number, worldY: number) => number;\n}\n\n// ==================== Default Depth Function ====================\n\nfunction defaultDepthFn(worldX: number, worldY: number): number {\n\treturn worldX + worldY;\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create an isometric depth sort plugin.\n *\n * Adds a render-phase system that sets PixiJS `zIndex` based on world-space\n * position, enabling correct front-to-back ordering in isometric views.\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createRenderer2DPlugin({ ... }))\n * .withPlugin(createIsoDepthSortPlugin())\n * .build();\n * ```\n */\nexport function createIsoDepthSortPlugin<G extends string = 'isometric'>(\n\toptions?: IsoDepthSortPluginOptions<G>,\n) {\n\tconst {\n\t\tdepthFn = defaultDepthFn,\n\t\tsystemGroup = 'isometric',\n\t} = options ?? {};\n\n\treturn definePlugin('isometric-depth-sort')\n\t\t.withComponentTypes<IsoDepthSortComponentTypes>()\n\t\t.requires<IsoDepthSortRequires>()\n\t\t.withGroups<G>()\n\t\t.install((world) => {\n\t\t\t// ==================== Init: Enable Sorting ====================\n\n\t\t\tworld\n\t\t\t\t.addSystem('isometric-depth-sort-init')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setOnInitialize((ecs) => {\n\t\t\t\t\tconst root = ecs.getResource('rootContainer');\n\t\t\t\t\troot.sortableChildren = true;\n\t\t\t\t});\n\n\t\t\t// ==================== Depth Sort System ====================\n\n\t\t\tworld\n\t\t\t\t.addSystem('isometric-depth-sort')\n\t\t\t\t.setPriority(350)\n\t\t\t\t.inPhase('render')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('sprites', {\n\t\t\t\t\twith: ['sprite', 'worldTransform'],\n\t\t\t\t\tchanged: ['worldTransform'],\n\t\t\t\t\toptional: ['depthOffset'],\n\t\t\t\t})\n\t\t\t\t.addQuery('graphics', {\n\t\t\t\t\twith: ['graphics', 'worldTransform'],\n\t\t\t\t\tchanged: ['worldTransform'],\n\t\t\t\t\toptional: ['depthOffset'],\n\t\t\t\t})\n\t\t\t\t.addQuery('containers', {\n\t\t\t\t\twith: ['container', 'worldTransform'],\n\t\t\t\t\tchanged: ['worldTransform'],\n\t\t\t\t\toptional: ['depthOffset'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries }) => {\n\t\t\t\t\tfor (const entity of queries.sprites) {\n\t\t\t\t\t\tconst { sprite, worldTransform, depthOffset } = entity.components;\n\t\t\t\t\t\tsprite.zIndex = depthFn(worldTransform.x, worldTransform.y) + (depthOffset ?? 0);\n\t\t\t\t\t}\n\n\t\t\t\t\tfor (const entity of queries.graphics) {\n\t\t\t\t\t\tconst { graphics, worldTransform, depthOffset } = entity.components;\n\t\t\t\t\t\tgraphics.zIndex = depthFn(worldTransform.x, worldTransform.y) + (depthOffset ?? 0);\n\t\t\t\t\t}\n\n\t\t\t\t\tfor (const entity of queries.containers) {\n\t\t\t\t\t\tconst { container, worldTransform, depthOffset } = entity.components;\n\t\t\t\t\t\tcontainer.zIndex = depthFn(worldTransform.x, worldTransform.y) + (depthOffset ?? 0);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n}\n"
5
+ "/**\n * Isometric Depth Sort Plugin for ECSpresso\n *\n * Sets PixiJS `zIndex` on entities based on their world-space position,\n * ensuring correct visual overlap in isometric rendering. Entities with\n * higher world X + Y values render in front.\n *\n * Requires `rootContainer` from the renderer2D plugin.\n * Enables `sortableChildren` on the root container at initialization.\n */\n\nimport { definePlugin } from 'ecspresso';\nimport type { BasePluginOptions } from 'ecspresso';\nimport type { ComponentsConfig, ResourcesConfig } from '../../type-utils';\nimport type { TransformComponentTypes } from '../spatial/transform';\nimport type { Renderer2DComponentTypes, Renderer2DResourceTypes } from '../rendering/renderer2D';\n\n// ==================== Component Types ====================\n\n/**\n * Optional component that offsets an entity's depth sort value.\n * Entities with a positive depthOffset render in front of entities\n * at the same world position (e.g., a player on top of a ground tile).\n */\nexport interface IsoDepthSortComponentTypes {\n\tdepthOffset: number;\n}\n\ntype IsoDepthSortRequires =\n\tComponentsConfig<TransformComponentTypes & Pick<Renderer2DComponentTypes, 'sprite' | 'graphics' | 'container'>>\n\t& ResourcesConfig<Renderer2DResourceTypes>;\n\n// ==================== Plugin Options ====================\n\nexport interface IsoDepthSortPluginOptions<G extends string = 'isometric'> extends BasePluginOptions<G> {\n\t/** Custom depth function. Receives world-space x/y, returns a sort key.\n\t * Default: `(x, y) => x + y` */\n\tdepthFn?: (worldX: number, worldY: number) => number;\n}\n\n// ==================== Default Depth Function ====================\n\nfunction defaultDepthFn(worldX: number, worldY: number): number {\n\treturn worldX + worldY;\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create an isometric depth sort plugin.\n *\n * Adds a render-phase system that sets PixiJS `zIndex` based on world-space\n * position, enabling correct front-to-back ordering in isometric views.\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createRenderer2DPlugin({ ... }))\n * .withPlugin(createIsoDepthSortPlugin())\n * .build();\n * ```\n */\nexport function createIsoDepthSortPlugin<G extends string = 'isometric'>(\n\toptions?: IsoDepthSortPluginOptions<G>,\n) {\n\tconst {\n\t\tdepthFn = defaultDepthFn,\n\t\tsystemGroup = 'isometric',\n\t} = options ?? {};\n\n\treturn definePlugin('isometric-depth-sort')\n\t\t.withComponentTypes<IsoDepthSortComponentTypes>()\n\t\t.requires<IsoDepthSortRequires>()\n\t\t.withGroups<G>()\n\t\t.install((world) => {\n\t\t\t// ==================== Init: Enable Sorting ====================\n\n\t\t\tworld\n\t\t\t\t.addSystem('isometric-depth-sort-init')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setOnInitialize((ecs) => {\n\t\t\t\t\tconst root = ecs.getResource('rootContainer');\n\t\t\t\t\troot.sortableChildren = true;\n\t\t\t\t});\n\n\t\t\t// ==================== Depth Sort System ====================\n\n\t\t\tworld\n\t\t\t\t.addSystem('isometric-depth-sort')\n\t\t\t\t.setPriority(350)\n\t\t\t\t.inPhase('render')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('sprites', {\n\t\t\t\t\twith: ['sprite', 'worldTransform'],\n\t\t\t\t\tchanged: ['worldTransform'],\n\t\t\t\t\toptional: ['depthOffset'],\n\t\t\t\t})\n\t\t\t\t.addQuery('graphics', {\n\t\t\t\t\twith: ['graphics', 'worldTransform'],\n\t\t\t\t\tchanged: ['worldTransform'],\n\t\t\t\t\toptional: ['depthOffset'],\n\t\t\t\t})\n\t\t\t\t.addQuery('containers', {\n\t\t\t\t\twith: ['container', 'worldTransform'],\n\t\t\t\t\tchanged: ['worldTransform'],\n\t\t\t\t\toptional: ['depthOffset'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries }) => {\n\t\t\t\t\tfor (const entity of queries.sprites) {\n\t\t\t\t\t\tconst { sprite, worldTransform, depthOffset } = entity.components;\n\t\t\t\t\t\tsprite.zIndex = depthFn(worldTransform.x, worldTransform.y) + (depthOffset ?? 0);\n\t\t\t\t\t}\n\n\t\t\t\t\tfor (const entity of queries.graphics) {\n\t\t\t\t\t\tconst { graphics, worldTransform, depthOffset } = entity.components;\n\t\t\t\t\t\tgraphics.zIndex = depthFn(worldTransform.x, worldTransform.y) + (depthOffset ?? 0);\n\t\t\t\t\t}\n\n\t\t\t\t\tfor (const entity of queries.containers) {\n\t\t\t\t\t\tconst { container, worldTransform, depthOffset } = entity.components;\n\t\t\t\t\t\tcontainer.zIndex = depthFn(worldTransform.x, worldTransform.y) + (depthOffset ?? 0);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n}\n"
6
6
  ],
7
- "mappings": "2PAWA,uBAAS,kBAiCT,SAAS,CAAc,CAAC,EAAgB,EAAwB,CAC/D,OAAO,EAAS,EAmBV,SAAS,CAAwD,CACvE,EACC,CACD,IACC,UAAU,EACV,cAAc,aACX,GAAW,CAAC,EAEhB,OAAO,EAAa,sBAAsB,EACxC,mBAA+C,EAC/C,SAA+B,EAC/B,WAAc,EACd,QAAQ,CAAC,IAAU,CAGnB,EACE,UAAU,2BAA2B,EACrC,QAAQ,CAAW,EACnB,gBAAgB,CAAC,IAAQ,CACzB,IAAM,EAAO,EAAI,YAAY,eAAe,EAC5C,EAAK,iBAAmB,GACxB,EAIF,EACE,UAAU,sBAAsB,EAChC,YAAY,GAAG,EACf,QAAQ,QAAQ,EAChB,QAAQ,CAAW,EACnB,SAAS,UAAW,CACpB,KAAM,CAAC,SAAU,gBAAgB,EACjC,QAAS,CAAC,gBAAgB,EAC1B,SAAU,CAAC,aAAa,CACzB,CAAC,EACA,SAAS,WAAY,CACrB,KAAM,CAAC,WAAY,gBAAgB,EACnC,QAAS,CAAC,gBAAgB,EAC1B,SAAU,CAAC,aAAa,CACzB,CAAC,EACA,SAAS,aAAc,CACvB,KAAM,CAAC,YAAa,gBAAgB,EACpC,QAAS,CAAC,gBAAgB,EAC1B,SAAU,CAAC,aAAa,CACzB,CAAC,EACA,WAAW,EAAG,aAAc,CAC5B,QAAW,KAAU,EAAQ,QAAS,CACrC,IAAQ,SAAQ,iBAAgB,eAAgB,EAAO,WACvD,EAAO,OAAS,EAAQ,EAAe,EAAG,EAAe,CAAC,GAAK,GAAe,GAG/E,QAAW,KAAU,EAAQ,SAAU,CACtC,IAAQ,WAAU,iBAAgB,eAAgB,EAAO,WACzD,EAAS,OAAS,EAAQ,EAAe,EAAG,EAAe,CAAC,GAAK,GAAe,GAGjF,QAAW,KAAU,EAAQ,WAAY,CACxC,IAAQ,YAAW,iBAAgB,eAAgB,EAAO,WAC1D,EAAU,OAAS,EAAQ,EAAe,EAAG,EAAe,CAAC,GAAK,GAAe,IAElF,EACF",
7
+ "mappings": "2PAWA,uBAAS,kBA+BT,SAAS,CAAc,CAAC,EAAgB,EAAwB,CAC/D,OAAO,EAAS,EAmBV,SAAS,CAAwD,CACvE,EACC,CACD,IACC,UAAU,EACV,cAAc,aACX,GAAW,CAAC,EAEhB,OAAO,EAAa,sBAAsB,EACxC,mBAA+C,EAC/C,SAA+B,EAC/B,WAAc,EACd,QAAQ,CAAC,IAAU,CAGnB,EACE,UAAU,2BAA2B,EACrC,QAAQ,CAAW,EACnB,gBAAgB,CAAC,IAAQ,CACzB,IAAM,EAAO,EAAI,YAAY,eAAe,EAC5C,EAAK,iBAAmB,GACxB,EAIF,EACE,UAAU,sBAAsB,EAChC,YAAY,GAAG,EACf,QAAQ,QAAQ,EAChB,QAAQ,CAAW,EACnB,SAAS,UAAW,CACpB,KAAM,CAAC,SAAU,gBAAgB,EACjC,QAAS,CAAC,gBAAgB,EAC1B,SAAU,CAAC,aAAa,CACzB,CAAC,EACA,SAAS,WAAY,CACrB,KAAM,CAAC,WAAY,gBAAgB,EACnC,QAAS,CAAC,gBAAgB,EAC1B,SAAU,CAAC,aAAa,CACzB,CAAC,EACA,SAAS,aAAc,CACvB,KAAM,CAAC,YAAa,gBAAgB,EACpC,QAAS,CAAC,gBAAgB,EAC1B,SAAU,CAAC,aAAa,CACzB,CAAC,EACA,WAAW,EAAG,aAAc,CAC5B,QAAW,KAAU,EAAQ,QAAS,CACrC,IAAQ,SAAQ,iBAAgB,eAAgB,EAAO,WACvD,EAAO,OAAS,EAAQ,EAAe,EAAG,EAAe,CAAC,GAAK,GAAe,GAG/E,QAAW,KAAU,EAAQ,SAAU,CACtC,IAAQ,WAAU,iBAAgB,eAAgB,EAAO,WACzD,EAAS,OAAS,EAAQ,EAAe,EAAG,EAAe,CAAC,GAAK,GAAe,GAGjF,QAAW,KAAU,EAAQ,WAAY,CACxC,IAAQ,YAAW,iBAAgB,eAAgB,EAAO,WAC1D,EAAU,OAAS,EAAQ,EAAe,EAAG,EAAe,CAAC,GAAK,GAAe,IAElF,EACF",
8
8
  "debugId": "E8C9DCC3EA4239A864756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -10,7 +10,7 @@
10
10
  * the camera position before applying it to the root container.
11
11
  */
12
12
  import type { BasePluginOptions } from 'ecspresso';
13
- import type { WorldConfigFrom } from '../../type-utils';
13
+ import type { ComponentsConfig, ResourcesConfig } from '../../type-utils';
14
14
  import type { TransformComponentTypes } from '../spatial/transform';
15
15
  import type { Renderer2DComponentTypes, Renderer2DResourceTypes } from '../rendering/renderer2D';
16
16
  import type { CameraResourceTypes } from '../spatial/camera';
@@ -26,7 +26,7 @@ export interface IsoProjectionState {
26
26
  export interface IsoProjectionResourceTypes {
27
27
  isoProjection: IsoProjectionState;
28
28
  }
29
- type IsoProjectionRequires = WorldConfigFrom<TransformComponentTypes & Pick<Renderer2DComponentTypes, 'sprite' | 'graphics' | 'container'>, {}, Renderer2DResourceTypes & CameraResourceTypes>;
29
+ type IsoProjectionRequires = ComponentsConfig<TransformComponentTypes & Pick<Renderer2DComponentTypes, 'sprite' | 'graphics' | 'container'>> & ResourcesConfig<Renderer2DResourceTypes & CameraResourceTypes>;
30
30
  export interface IsoProjectionPluginOptions<G extends string = 'isometric'> extends BasePluginOptions<G> {
31
31
  /** Tile width in pixels (default: 64) */
32
32
  tileWidth?: number;
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/plugins/isometric/projection.ts"],
4
4
  "sourcesContent": [
5
- "/**\n * Isometric Projection Plugin for ECSpresso\n *\n * Converts Cartesian world-space positions to isometric screen positions\n * in the render phase. All ECS-level logic (physics, collision, camera follow)\n * continues to operate in Cartesian world space — only PixiJS display object\n * positions are projected.\n *\n * Optionally provides an isometric-aware camera sync system that projects\n * the camera position before applying it to the root container.\n */\n\nimport { definePlugin } from 'ecspresso';\nimport type { BasePluginOptions } from 'ecspresso';\nimport type { WorldConfigFrom } from '../../type-utils';\nimport type { TransformComponentTypes } from '../spatial/transform';\nimport type { Renderer2DComponentTypes, Renderer2DResourceTypes } from '../rendering/renderer2D';\nimport type { CameraResourceTypes } from '../spatial/camera';\n\n// ==================== Types ====================\n\n/**\n * Isometric projection configuration.\n */\nexport interface IsoProjectionState {\n\treadonly tileWidth: number;\n\treadonly tileHeight: number;\n\treadonly originX: number;\n\treadonly originY: number;\n}\n\nexport interface IsoProjectionResourceTypes {\n\tisoProjection: IsoProjectionState;\n}\n\ntype IsoProjectionRequires = WorldConfigFrom<\n\tTransformComponentTypes & Pick<Renderer2DComponentTypes, 'sprite' | 'graphics' | 'container'>,\n\t{},\n\tRenderer2DResourceTypes & CameraResourceTypes\n>;\n\n// ==================== Plugin Options ====================\n\nexport interface IsoProjectionPluginOptions<G extends string = 'isometric'> extends BasePluginOptions<G> {\n\t/** Tile width in pixels (default: 64) */\n\ttileWidth?: number;\n\t/** Tile height in pixels (default: 32) */\n\ttileHeight?: number;\n\t/** Screen-space X origin offset (default: 0) */\n\toriginX?: number;\n\t/** Screen-space Y origin offset (default: 0) */\n\toriginY?: number;\n\t/** Register an isometric-aware camera sync system (default: false).\n\t * When true, set `camera: false` on createRenderer2DPlugin to avoid conflicts. */\n\tcamera?: boolean;\n}\n\n// ==================== Coordinate Conversion ====================\n\n// Pre-allocated point for hot-path use — returned by reference, consumed immediately by callers\nconst _tempPoint = { x: 0, y: 0 };\n\nfunction worldToIsoInto(\n\tworldX: number,\n\tworldY: number,\n\thalfW: number,\n\thalfH: number,\n\toriginX: number,\n\toriginY: number,\n): { x: number; y: number } {\n\t_tempPoint.x = (worldX - worldY) * halfW + originX;\n\t_tempPoint.y = (worldX + worldY) * halfH + originY;\n\treturn _tempPoint;\n}\n\n/**\n * Convert Cartesian world coordinates to isometric screen coordinates.\n *\n * @param worldX World-space X coordinate\n * @param worldY World-space Y coordinate\n * @param state Isometric projection state\n * @returns New object with projected { x, y }\n */\nexport function worldToIso(\n\tworldX: number,\n\tworldY: number,\n\tstate: IsoProjectionState,\n): { x: number; y: number } {\n\treturn {\n\t\tx: (worldX - worldY) * (state.tileWidth / 2) + state.originX,\n\t\ty: (worldX + worldY) * (state.tileHeight / 2) + state.originY,\n\t};\n}\n\n/**\n * Convert isometric screen coordinates back to Cartesian world coordinates.\n *\n * @param isoX Isometric screen-space X coordinate\n * @param isoY Isometric screen-space Y coordinate\n * @param state Isometric projection state\n * @returns New object with world-space { x, y }\n */\nexport function isoToWorld(\n\tisoX: number,\n\tisoY: number,\n\tstate: IsoProjectionState,\n): { x: number; y: number } {\n\tconst relX = isoX - state.originX;\n\tconst relY = isoY - state.originY;\n\treturn {\n\t\tx: relX / state.tileWidth + relY / state.tileHeight,\n\t\ty: -relX / state.tileWidth + relY / state.tileHeight,\n\t};\n}\n\n/**\n * Convert screen coordinates (e.g. clientX/clientY) to isometric world (tile) coordinates,\n * accounting for camera position and zoom.\n *\n * @param screenX Screen-space X coordinate (e.g. clientX from a pointer event)\n * @param screenY Screen-space Y coordinate (e.g. clientY from a pointer event)\n * @param cameraState Camera state with position and zoom\n * @param isoState Isometric projection state\n * @param canvas The HTMLCanvasElement used for rendering\n * @returns World-space { x, y } in tile coordinates\n */\nexport function screenToIsoWorld(\n\tscreenX: number,\n\tscreenY: number,\n\tcameraState: { x: number; y: number; zoom: number },\n\tisoState: IsoProjectionState,\n\tcanvas: HTMLCanvasElement,\n): { x: number; y: number } {\n\tconst rect = canvas.getBoundingClientRect();\n\tconst screenOffX = screenX - (rect.left + rect.width / 2);\n\tconst screenOffY = screenY - (rect.top + rect.height / 2);\n\tconst camIso = worldToIso(cameraState.x, cameraState.y, isoState);\n\treturn isoToWorld(\n\t\tcamIso.x + screenOffX / cameraState.zoom,\n\t\tcamIso.y + screenOffY / cameraState.zoom,\n\t\tisoState,\n\t);\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create an isometric projection plugin.\n *\n * Adds a render-phase system that overwrites PixiJS display object positions\n * with isometric projections of their `worldTransform` coordinates.\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createRenderer2DPlugin({ camera: false, ... }))\n * .withPlugin(createCameraPlugin({ ... }))\n * .withPlugin(createIsoProjectionPlugin({ tileWidth: 64, tileHeight: 32, camera: true }))\n * .build();\n * ```\n */\nexport function createIsoProjectionPlugin<G extends string = 'isometric'>(\n\toptions?: IsoProjectionPluginOptions<G>,\n) {\n\tconst {\n\t\ttileWidth = 64,\n\t\ttileHeight = 32,\n\t\toriginX = 0,\n\t\toriginY = 0,\n\t\tcamera = false,\n\t\tsystemGroup = 'isometric',\n\t} = options ?? {};\n\n\treturn definePlugin('isometric-projection')\n\t\t.withResourceTypes<IsoProjectionResourceTypes>()\n\t\t.requires<IsoProjectionRequires>()\n\t\t.withGroups<G>()\n\t\t.install((world) => {\n\t\t\tconst halfW = tileWidth / 2;\n\t\t\tconst halfH = tileHeight / 2;\n\n\t\t\tworld.addResource('isoProjection', {\n\t\t\t\ttileWidth,\n\t\t\t\ttileHeight,\n\t\t\t\toriginX,\n\t\t\t\toriginY,\n\t\t\t});\n\n\t\t\t// ==================== Projection System ====================\n\n\t\t\tworld\n\t\t\t\t.addSystem('isometric-projection')\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.addQuery('sprites', {\n\t\t\t\t\twith: ['sprite', 'worldTransform'],\n\t\t\t\t\tchanged: ['worldTransform'],\n\t\t\t\t})\n\t\t\t\t.addQuery('graphics', {\n\t\t\t\t\twith: ['graphics', 'worldTransform'],\n\t\t\t\t\tchanged: ['worldTransform'],\n\t\t\t\t})\n\t\t\t\t.addQuery('containers', {\n\t\t\t\t\twith: ['container', 'worldTransform'],\n\t\t\t\t\tchanged: ['worldTransform'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries }) => {\n\t\t\t\t\tfor (const entity of queries.sprites) {\n\t\t\t\t\t\tconst { sprite, worldTransform } = entity.components;\n\t\t\t\t\t\tconst projected = worldToIsoInto(worldTransform.x, worldTransform.y, halfW, halfH, originX, originY);\n\t\t\t\t\t\tsprite.position.set(projected.x, projected.y);\n\t\t\t\t\t}\n\n\t\t\t\t\tfor (const entity of queries.graphics) {\n\t\t\t\t\t\tconst { graphics, worldTransform } = entity.components;\n\t\t\t\t\t\tconst projected = worldToIsoInto(worldTransform.x, worldTransform.y, halfW, halfH, originX, originY);\n\t\t\t\t\t\tgraphics.position.set(projected.x, projected.y);\n\t\t\t\t\t}\n\n\t\t\t\t\tfor (const entity of queries.containers) {\n\t\t\t\t\t\tconst { container, worldTransform } = entity.components;\n\t\t\t\t\t\tconst projected = worldToIsoInto(worldTransform.x, worldTransform.y, halfW, halfH, originX, originY);\n\t\t\t\t\t\tcontainer.position.set(projected.x, projected.y);\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// ==================== Isometric Camera Sync (opt-in) ====================\n\n\t\t\tif (camera) {\n\t\t\t\tworld\n\t\t\t\t\t.addSystem('isometric-camera-sync')\n\t\t\t\t\t.setPriority(900)\n\t\t\t\t\t.inPhase('render')\n\t\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t\t.withResources(['cameraState', 'rootContainer', 'pixiApp'])\n\t\t\t\t\t.setProcess(({ resources: { cameraState: state, rootContainer: root, pixiApp: app } }) => {\n\t\t\t\t\t\tconst centerW = app.screen.width;\n\t\t\t\t\t\tconst centerH = app.screen.height;\n\n\t\t\t\t\t\tconst camIso = worldToIsoInto(\n\t\t\t\t\t\t\tstate.x + state.shakeOffsetX,\n\t\t\t\t\t\t\tstate.y + state.shakeOffsetY,\n\t\t\t\t\t\t\thalfW, halfH, originX, originY,\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\troot.position.set(\n\t\t\t\t\t\t\tcenterW / 2 - camIso.x * state.zoom,\n\t\t\t\t\t\t\tcenterH / 2 - camIso.y * state.zoom,\n\t\t\t\t\t\t);\n\t\t\t\t\t\troot.scale.set(state.zoom);\n\t\t\t\t\t\troot.rotation = -(state.rotation + state.shakeRotation);\n\t\t\t\t\t});\n\t\t\t}\n\t\t});\n}\n"
5
+ "/**\n * Isometric Projection Plugin for ECSpresso\n *\n * Converts Cartesian world-space positions to isometric screen positions\n * in the render phase. All ECS-level logic (physics, collision, camera follow)\n * continues to operate in Cartesian world space — only PixiJS display object\n * positions are projected.\n *\n * Optionally provides an isometric-aware camera sync system that projects\n * the camera position before applying it to the root container.\n */\n\nimport { definePlugin } from 'ecspresso';\nimport type { BasePluginOptions } from 'ecspresso';\nimport type { ComponentsConfig, ResourcesConfig } from '../../type-utils';\nimport type { TransformComponentTypes } from '../spatial/transform';\nimport type { Renderer2DComponentTypes, Renderer2DResourceTypes } from '../rendering/renderer2D';\nimport type { CameraResourceTypes } from '../spatial/camera';\n\n// ==================== Types ====================\n\n/**\n * Isometric projection configuration.\n */\nexport interface IsoProjectionState {\n\treadonly tileWidth: number;\n\treadonly tileHeight: number;\n\treadonly originX: number;\n\treadonly originY: number;\n}\n\nexport interface IsoProjectionResourceTypes {\n\tisoProjection: IsoProjectionState;\n}\n\ntype IsoProjectionRequires =\n\tComponentsConfig<TransformComponentTypes & Pick<Renderer2DComponentTypes, 'sprite' | 'graphics' | 'container'>>\n\t& ResourcesConfig<Renderer2DResourceTypes & CameraResourceTypes>;\n\n// ==================== Plugin Options ====================\n\nexport interface IsoProjectionPluginOptions<G extends string = 'isometric'> extends BasePluginOptions<G> {\n\t/** Tile width in pixels (default: 64) */\n\ttileWidth?: number;\n\t/** Tile height in pixels (default: 32) */\n\ttileHeight?: number;\n\t/** Screen-space X origin offset (default: 0) */\n\toriginX?: number;\n\t/** Screen-space Y origin offset (default: 0) */\n\toriginY?: number;\n\t/** Register an isometric-aware camera sync system (default: false).\n\t * When true, set `camera: false` on createRenderer2DPlugin to avoid conflicts. */\n\tcamera?: boolean;\n}\n\n// ==================== Coordinate Conversion ====================\n\n// Pre-allocated point for hot-path use — returned by reference, consumed immediately by callers\nconst _tempPoint = { x: 0, y: 0 };\n\nfunction worldToIsoInto(\n\tworldX: number,\n\tworldY: number,\n\thalfW: number,\n\thalfH: number,\n\toriginX: number,\n\toriginY: number,\n): { x: number; y: number } {\n\t_tempPoint.x = (worldX - worldY) * halfW + originX;\n\t_tempPoint.y = (worldX + worldY) * halfH + originY;\n\treturn _tempPoint;\n}\n\n/**\n * Convert Cartesian world coordinates to isometric screen coordinates.\n *\n * @param worldX World-space X coordinate\n * @param worldY World-space Y coordinate\n * @param state Isometric projection state\n * @returns New object with projected { x, y }\n */\nexport function worldToIso(\n\tworldX: number,\n\tworldY: number,\n\tstate: IsoProjectionState,\n): { x: number; y: number } {\n\treturn {\n\t\tx: (worldX - worldY) * (state.tileWidth / 2) + state.originX,\n\t\ty: (worldX + worldY) * (state.tileHeight / 2) + state.originY,\n\t};\n}\n\n/**\n * Convert isometric screen coordinates back to Cartesian world coordinates.\n *\n * @param isoX Isometric screen-space X coordinate\n * @param isoY Isometric screen-space Y coordinate\n * @param state Isometric projection state\n * @returns New object with world-space { x, y }\n */\nexport function isoToWorld(\n\tisoX: number,\n\tisoY: number,\n\tstate: IsoProjectionState,\n): { x: number; y: number } {\n\tconst relX = isoX - state.originX;\n\tconst relY = isoY - state.originY;\n\treturn {\n\t\tx: relX / state.tileWidth + relY / state.tileHeight,\n\t\ty: -relX / state.tileWidth + relY / state.tileHeight,\n\t};\n}\n\n/**\n * Convert screen coordinates (e.g. clientX/clientY) to isometric world (tile) coordinates,\n * accounting for camera position and zoom.\n *\n * @param screenX Screen-space X coordinate (e.g. clientX from a pointer event)\n * @param screenY Screen-space Y coordinate (e.g. clientY from a pointer event)\n * @param cameraState Camera state with position and zoom\n * @param isoState Isometric projection state\n * @param canvas The HTMLCanvasElement used for rendering\n * @returns World-space { x, y } in tile coordinates\n */\nexport function screenToIsoWorld(\n\tscreenX: number,\n\tscreenY: number,\n\tcameraState: { x: number; y: number; zoom: number },\n\tisoState: IsoProjectionState,\n\tcanvas: HTMLCanvasElement,\n): { x: number; y: number } {\n\tconst rect = canvas.getBoundingClientRect();\n\tconst screenOffX = screenX - (rect.left + rect.width / 2);\n\tconst screenOffY = screenY - (rect.top + rect.height / 2);\n\tconst camIso = worldToIso(cameraState.x, cameraState.y, isoState);\n\treturn isoToWorld(\n\t\tcamIso.x + screenOffX / cameraState.zoom,\n\t\tcamIso.y + screenOffY / cameraState.zoom,\n\t\tisoState,\n\t);\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create an isometric projection plugin.\n *\n * Adds a render-phase system that overwrites PixiJS display object positions\n * with isometric projections of their `worldTransform` coordinates.\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createRenderer2DPlugin({ camera: false, ... }))\n * .withPlugin(createCameraPlugin({ ... }))\n * .withPlugin(createIsoProjectionPlugin({ tileWidth: 64, tileHeight: 32, camera: true }))\n * .build();\n * ```\n */\nexport function createIsoProjectionPlugin<G extends string = 'isometric'>(\n\toptions?: IsoProjectionPluginOptions<G>,\n) {\n\tconst {\n\t\ttileWidth = 64,\n\t\ttileHeight = 32,\n\t\toriginX = 0,\n\t\toriginY = 0,\n\t\tcamera = false,\n\t\tsystemGroup = 'isometric',\n\t} = options ?? {};\n\n\treturn definePlugin('isometric-projection')\n\t\t.withResourceTypes<IsoProjectionResourceTypes>()\n\t\t.requires<IsoProjectionRequires>()\n\t\t.withGroups<G>()\n\t\t.install((world) => {\n\t\t\tconst halfW = tileWidth / 2;\n\t\t\tconst halfH = tileHeight / 2;\n\n\t\t\tworld.addResource('isoProjection', {\n\t\t\t\ttileWidth,\n\t\t\t\ttileHeight,\n\t\t\t\toriginX,\n\t\t\t\toriginY,\n\t\t\t});\n\n\t\t\t// ==================== Projection System ====================\n\n\t\t\tworld\n\t\t\t\t.addSystem('isometric-projection')\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.addQuery('sprites', {\n\t\t\t\t\twith: ['sprite', 'worldTransform'],\n\t\t\t\t\tchanged: ['worldTransform'],\n\t\t\t\t})\n\t\t\t\t.addQuery('graphics', {\n\t\t\t\t\twith: ['graphics', 'worldTransform'],\n\t\t\t\t\tchanged: ['worldTransform'],\n\t\t\t\t})\n\t\t\t\t.addQuery('containers', {\n\t\t\t\t\twith: ['container', 'worldTransform'],\n\t\t\t\t\tchanged: ['worldTransform'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries }) => {\n\t\t\t\t\tfor (const entity of queries.sprites) {\n\t\t\t\t\t\tconst { sprite, worldTransform } = entity.components;\n\t\t\t\t\t\tconst projected = worldToIsoInto(worldTransform.x, worldTransform.y, halfW, halfH, originX, originY);\n\t\t\t\t\t\tsprite.position.set(projected.x, projected.y);\n\t\t\t\t\t}\n\n\t\t\t\t\tfor (const entity of queries.graphics) {\n\t\t\t\t\t\tconst { graphics, worldTransform } = entity.components;\n\t\t\t\t\t\tconst projected = worldToIsoInto(worldTransform.x, worldTransform.y, halfW, halfH, originX, originY);\n\t\t\t\t\t\tgraphics.position.set(projected.x, projected.y);\n\t\t\t\t\t}\n\n\t\t\t\t\tfor (const entity of queries.containers) {\n\t\t\t\t\t\tconst { container, worldTransform } = entity.components;\n\t\t\t\t\t\tconst projected = worldToIsoInto(worldTransform.x, worldTransform.y, halfW, halfH, originX, originY);\n\t\t\t\t\t\tcontainer.position.set(projected.x, projected.y);\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// ==================== Isometric Camera Sync (opt-in) ====================\n\n\t\t\tif (camera) {\n\t\t\t\tworld\n\t\t\t\t\t.addSystem('isometric-camera-sync')\n\t\t\t\t\t.setPriority(900)\n\t\t\t\t\t.inPhase('render')\n\t\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t\t.withResources(['cameraState', 'rootContainer', 'pixiApp'])\n\t\t\t\t\t.setProcess(({ resources: { cameraState: state, rootContainer: root, pixiApp: app } }) => {\n\t\t\t\t\t\tconst centerW = app.screen.width;\n\t\t\t\t\t\tconst centerH = app.screen.height;\n\n\t\t\t\t\t\tconst camIso = worldToIsoInto(\n\t\t\t\t\t\t\tstate.x + state.shakeOffsetX,\n\t\t\t\t\t\t\tstate.y + state.shakeOffsetY,\n\t\t\t\t\t\t\thalfW, halfH, originX, originY,\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\troot.position.set(\n\t\t\t\t\t\t\tcenterW / 2 - camIso.x * state.zoom,\n\t\t\t\t\t\t\tcenterH / 2 - camIso.y * state.zoom,\n\t\t\t\t\t\t);\n\t\t\t\t\t\troot.scale.set(state.zoom);\n\t\t\t\t\t\troot.rotation = -(state.rotation + state.shakeRotation);\n\t\t\t\t\t});\n\t\t\t}\n\t\t});\n}\n"
6
6
  ],
7
- "mappings": "2PAYA,uBAAS,kBAgDT,IAAM,EAAa,CAAE,EAAG,EAAG,EAAG,CAAE,EAEhC,SAAS,CAAc,CACtB,EACA,EACA,EACA,EACA,EACA,EAC2B,CAG3B,OAFA,EAAW,GAAK,EAAS,GAAU,EAAQ,EAC3C,EAAW,GAAK,EAAS,GAAU,EAAQ,EACpC,EAWD,SAAS,CAAU,CACzB,EACA,EACA,EAC2B,CAC3B,MAAO,CACN,GAAI,EAAS,IAAW,EAAM,UAAY,GAAK,EAAM,QACrD,GAAI,EAAS,IAAW,EAAM,WAAa,GAAK,EAAM,OACvD,EAWM,SAAS,CAAU,CACzB,EACA,EACA,EAC2B,CAC3B,IAAM,EAAO,EAAO,EAAM,QACpB,EAAO,EAAO,EAAM,QAC1B,MAAO,CACN,EAAG,EAAO,EAAM,UAAY,EAAO,EAAM,WACzC,EAAG,CAAC,EAAO,EAAM,UAAY,EAAO,EAAM,UAC3C,EAcM,SAAS,CAAgB,CAC/B,EACA,EACA,EACA,EACA,EAC2B,CAC3B,IAAM,EAAO,EAAO,sBAAsB,EACpC,EAAa,GAAW,EAAK,KAAO,EAAK,MAAQ,GACjD,EAAa,GAAW,EAAK,IAAM,EAAK,OAAS,GACjD,EAAS,EAAW,EAAY,EAAG,EAAY,EAAG,CAAQ,EAChE,OAAO,EACN,EAAO,EAAI,EAAa,EAAY,KACpC,EAAO,EAAI,EAAa,EAAY,KACpC,CACD,EAoBM,SAAS,CAAyD,CACxE,EACC,CACD,IACC,YAAY,GACZ,aAAa,GACb,UAAU,EACV,UAAU,EACV,SAAS,GACT,cAAc,aACX,GAAW,CAAC,EAEhB,OAAO,EAAa,sBAAsB,EACxC,kBAA8C,EAC9C,SAAgC,EAChC,WAAc,EACd,QAAQ,CAAC,IAAU,CACnB,IAAM,EAAQ,EAAY,EACpB,EAAQ,EAAa,EAkD3B,GAhDA,EAAM,YAAY,gBAAiB,CAClC,YACA,aACA,UACA,SACD,CAAC,EAID,EACE,UAAU,sBAAsB,EAChC,YAAY,GAAG,EACf,QAAQ,QAAQ,EAChB,QAAQ,CAAW,EACnB,SAAS,UAAW,CACpB,KAAM,CAAC,SAAU,gBAAgB,EACjC,QAAS,CAAC,gBAAgB,CAC3B,CAAC,EACA,SAAS,WAAY,CACrB,KAAM,CAAC,WAAY,gBAAgB,EACnC,QAAS,CAAC,gBAAgB,CAC3B,CAAC,EACA,SAAS,aAAc,CACvB,KAAM,CAAC,YAAa,gBAAgB,EACpC,QAAS,CAAC,gBAAgB,CAC3B,CAAC,EACA,WAAW,EAAG,aAAc,CAC5B,QAAW,KAAU,EAAQ,QAAS,CACrC,IAAQ,SAAQ,kBAAmB,EAAO,WACpC,EAAY,EAAe,EAAe,EAAG,EAAe,EAAG,EAAO,EAAO,EAAS,CAAO,EACnG,EAAO,SAAS,IAAI,EAAU,EAAG,EAAU,CAAC,EAG7C,QAAW,KAAU,EAAQ,SAAU,CACtC,IAAQ,WAAU,kBAAmB,EAAO,WACtC,EAAY,EAAe,EAAe,EAAG,EAAe,EAAG,EAAO,EAAO,EAAS,CAAO,EACnG,EAAS,SAAS,IAAI,EAAU,EAAG,EAAU,CAAC,EAG/C,QAAW,KAAU,EAAQ,WAAY,CACxC,IAAQ,YAAW,kBAAmB,EAAO,WACvC,EAAY,EAAe,EAAe,EAAG,EAAe,EAAG,EAAO,EAAO,EAAS,CAAO,EACnG,EAAU,SAAS,IAAI,EAAU,EAAG,EAAU,CAAC,GAEhD,EAIE,EACH,EACE,UAAU,uBAAuB,EACjC,YAAY,GAAG,EACf,QAAQ,QAAQ,EAChB,QAAQ,CAAW,EACnB,cAAc,CAAC,cAAe,gBAAiB,SAAS,CAAC,EACzD,WAAW,EAAG,WAAa,YAAa,EAAO,cAAe,EAAM,QAAS,MAAY,CACzF,IAAM,EAAU,EAAI,OAAO,MACrB,EAAU,EAAI,OAAO,OAErB,EAAS,EACd,EAAM,EAAI,EAAM,aAChB,EAAM,EAAI,EAAM,aAChB,EAAO,EAAO,EAAS,CACxB,EAEA,EAAK,SAAS,IACb,EAAU,EAAI,EAAO,EAAI,EAAM,KAC/B,EAAU,EAAI,EAAO,EAAI,EAAM,IAChC,EACA,EAAK,MAAM,IAAI,EAAM,IAAI,EACzB,EAAK,SAAW,EAAE,EAAM,SAAW,EAAM,eACzC,EAEH",
7
+ "mappings": "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",
8
8
  "debugId": "07D2C07CA0A331EE64756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -7,7 +7,7 @@
7
7
  * on arrival and an `arriveAtTarget` event is published.
8
8
  */
9
9
  import { type BasePluginOptions } from 'ecspresso';
10
- import type { WorldConfigFrom } from 'ecspresso';
10
+ import type { ComponentsConfig, EventsConfig } from 'ecspresso';
11
11
  import type { TransformWorldConfig } from '../spatial/transform';
12
12
  /**
13
13
  * Target position for an entity to move toward.
@@ -40,7 +40,7 @@ export interface SteeringEventTypes {
40
40
  * WorldConfig representing the steering plugin's provided types.
41
41
  * Used as the `Requires` type parameter by plugins that depend on steering.
42
42
  */
43
- export type SteeringWorldConfig = WorldConfigFrom<SteeringComponentTypes, SteeringEventTypes>;
43
+ export type SteeringWorldConfig = ComponentsConfig<SteeringComponentTypes> & EventsConfig<SteeringEventTypes>;
44
44
  /**
45
45
  * Configuration options for the steering plugin.
46
46
  */
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/plugins/physics/steering.ts"],
4
4
  "sourcesContent": [
5
- "/**\n * Steering Plugin for ECSpresso\n *\n * Provides simple move-to-target behavior with arrival detection.\n * Entities with a `moveTarget` component move toward the target position\n * at their configured `moveSpeed`. The `moveTarget` component is removed\n * on arrival and an `arriveAtTarget` event is published.\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type { WorldConfigFrom } from 'ecspresso';\nimport type { TransformWorldConfig } from '../spatial/transform';\n\n// ==================== Component Types ====================\n\n/**\n * Target position for an entity to move toward.\n * Removed automatically when the entity arrives.\n */\nexport interface MoveTarget {\n\tx: number;\n\ty: number;\n}\n\n/**\n * Component types provided by the steering plugin.\n */\nexport interface SteeringComponentTypes {\n\tmoveTarget: MoveTarget;\n\tmoveSpeed: number;\n}\n\n// ==================== Event Types ====================\n\n/**\n * Event fired when an entity arrives at its move target.\n */\nexport interface ArriveAtTargetEvent {\n\tentityId: number;\n}\n\n/**\n * Event types provided by the steering plugin.\n */\nexport interface SteeringEventTypes {\n\tarriveAtTarget: ArriveAtTargetEvent;\n}\n\n// ==================== WorldConfig ====================\n\n/**\n * WorldConfig representing the steering plugin's provided types.\n * Used as the `Requires` type parameter by plugins that depend on steering.\n */\nexport type SteeringWorldConfig = WorldConfigFrom<SteeringComponentTypes, SteeringEventTypes>;\n\n// ==================== Plugin Options ====================\n\n/**\n * Configuration options for the steering plugin.\n */\nexport interface SteeringPluginOptions<G extends string = 'steering'> extends BasePluginOptions<G> {\n\t/** Distance threshold to consider arrival (default: 2) */\n\tarrivalThreshold?: number;\n}\n\n// ==================== Helper Functions ====================\n\n/**\n * Create a moveTarget component.\n *\n * @param x Target x position\n * @param y Target y position\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.addComponent(entityId, 'moveTarget', createMoveTarget(200, 300).moveTarget);\n * ```\n */\nexport function createMoveTarget(x: number, y: number): Pick<SteeringComponentTypes, 'moveTarget'> {\n\treturn { moveTarget: { x, y } };\n}\n\n/**\n * Create a moveSpeed component.\n *\n * @param speed Movement speed in pixels per second\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createTransform(100, 200),\n * ...createMoveSpeed(150),\n * });\n * ```\n */\nexport function createMoveSpeed(speed: number): Pick<SteeringComponentTypes, 'moveSpeed'> {\n\treturn { moveSpeed: speed };\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a steering plugin for ECSpresso.\n *\n * Provides a `move-to-target` system that moves entities with `moveTarget`\n * and `moveSpeed` components toward their target position. On arrival,\n * the `moveTarget` component is removed and an `arriveAtTarget` event is published.\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createTransformPlugin())\n * .withPlugin(createSteeringPlugin())\n * .build();\n *\n * await ecs.initialize();\n *\n * ecs.spawn({\n * ...createTransform(0, 0),\n * ...createMoveSpeed(100),\n * ...createMoveTarget(200, 200),\n * });\n * ```\n */\nexport function createSteeringPlugin<G extends string = 'steering'>(\n\toptions?: SteeringPluginOptions<G>\n) {\n\tconst {\n\t\tsystemGroup = 'steering',\n\t\tpriority = 100,\n\t\tphase = 'update',\n\t\tarrivalThreshold = 2,\n\t} = options ?? {};\n\n\tconst arrivalThresholdSq = arrivalThreshold * arrivalThreshold;\n\n\treturn definePlugin('steering')\n\t\t.withComponentTypes<SteeringComponentTypes>()\n\t\t.withEventTypes<SteeringEventTypes>()\n\t\t.withLabels<'move-to-target'>()\n\t\t.withGroups<G>()\n\t\t.requires<TransformWorldConfig>()\n\t\t.install((world) => {\n\t\t\tworld\n\t\t\t\t.addSystem('move-to-target')\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('movers', {\n\t\t\t\t\twith: ['localTransform', 'moveTarget', 'moveSpeed'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, ecs, dt }) => {\n\t\t\t\t\tfor (const entity of queries.movers) {\n\t\t\t\t\t\tconst { localTransform, moveTarget, moveSpeed } = entity.components;\n\n\t\t\t\t\t\tconst dx = moveTarget.x - localTransform.x;\n\t\t\t\t\t\tconst dy = moveTarget.y - localTransform.y;\n\t\t\t\t\t\tconst distSq = dx * dx + dy * dy;\n\n\t\t\t\t\t\tif (distSq <= arrivalThresholdSq) {\n\t\t\t\t\t\t\tlocalTransform.x = moveTarget.x;\n\t\t\t\t\t\t\tlocalTransform.y = moveTarget.y;\n\t\t\t\t\t\t\tecs.markChanged(entity.id, 'localTransform');\n\t\t\t\t\t\t\tecs.commands.removeComponent(entity.id, 'moveTarget');\n\t\t\t\t\t\t\tecs.eventBus.publish('arriveAtTarget', { entityId: entity.id });\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst dist = Math.sqrt(distSq);\n\t\t\t\t\t\tconst step = Math.min(moveSpeed * dt, dist);\n\t\t\t\t\t\tlocalTransform.x += (dx / dist) * step;\n\t\t\t\t\t\tlocalTransform.y += (dy / dist) * step;\n\t\t\t\t\t\tecs.markChanged(entity.id, 'localTransform');\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n}\n"
5
+ "/**\n * Steering Plugin for ECSpresso\n *\n * Provides simple move-to-target behavior with arrival detection.\n * Entities with a `moveTarget` component move toward the target position\n * at their configured `moveSpeed`. The `moveTarget` component is removed\n * on arrival and an `arriveAtTarget` event is published.\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type { ComponentsConfig, EventsConfig } from 'ecspresso';\nimport type { TransformWorldConfig } from '../spatial/transform';\n\n// ==================== Component Types ====================\n\n/**\n * Target position for an entity to move toward.\n * Removed automatically when the entity arrives.\n */\nexport interface MoveTarget {\n\tx: number;\n\ty: number;\n}\n\n/**\n * Component types provided by the steering plugin.\n */\nexport interface SteeringComponentTypes {\n\tmoveTarget: MoveTarget;\n\tmoveSpeed: number;\n}\n\n// ==================== Event Types ====================\n\n/**\n * Event fired when an entity arrives at its move target.\n */\nexport interface ArriveAtTargetEvent {\n\tentityId: number;\n}\n\n/**\n * Event types provided by the steering plugin.\n */\nexport interface SteeringEventTypes {\n\tarriveAtTarget: ArriveAtTargetEvent;\n}\n\n// ==================== WorldConfig ====================\n\n/**\n * WorldConfig representing the steering plugin's provided types.\n * Used as the `Requires` type parameter by plugins that depend on steering.\n */\nexport type SteeringWorldConfig =\n\tComponentsConfig<SteeringComponentTypes>\n\t& EventsConfig<SteeringEventTypes>;\n\n// ==================== Plugin Options ====================\n\n/**\n * Configuration options for the steering plugin.\n */\nexport interface SteeringPluginOptions<G extends string = 'steering'> extends BasePluginOptions<G> {\n\t/** Distance threshold to consider arrival (default: 2) */\n\tarrivalThreshold?: number;\n}\n\n// ==================== Helper Functions ====================\n\n/**\n * Create a moveTarget component.\n *\n * @param x Target x position\n * @param y Target y position\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.addComponent(entityId, 'moveTarget', createMoveTarget(200, 300).moveTarget);\n * ```\n */\nexport function createMoveTarget(x: number, y: number): Pick<SteeringComponentTypes, 'moveTarget'> {\n\treturn { moveTarget: { x, y } };\n}\n\n/**\n * Create a moveSpeed component.\n *\n * @param speed Movement speed in pixels per second\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createTransform(100, 200),\n * ...createMoveSpeed(150),\n * });\n * ```\n */\nexport function createMoveSpeed(speed: number): Pick<SteeringComponentTypes, 'moveSpeed'> {\n\treturn { moveSpeed: speed };\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a steering plugin for ECSpresso.\n *\n * Provides a `move-to-target` system that moves entities with `moveTarget`\n * and `moveSpeed` components toward their target position. On arrival,\n * the `moveTarget` component is removed and an `arriveAtTarget` event is published.\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createTransformPlugin())\n * .withPlugin(createSteeringPlugin())\n * .build();\n *\n * await ecs.initialize();\n *\n * ecs.spawn({\n * ...createTransform(0, 0),\n * ...createMoveSpeed(100),\n * ...createMoveTarget(200, 200),\n * });\n * ```\n */\nexport function createSteeringPlugin<G extends string = 'steering'>(\n\toptions?: SteeringPluginOptions<G>\n) {\n\tconst {\n\t\tsystemGroup = 'steering',\n\t\tpriority = 100,\n\t\tphase = 'update',\n\t\tarrivalThreshold = 2,\n\t} = options ?? {};\n\n\tconst arrivalThresholdSq = arrivalThreshold * arrivalThreshold;\n\n\treturn definePlugin('steering')\n\t\t.withComponentTypes<SteeringComponentTypes>()\n\t\t.withEventTypes<SteeringEventTypes>()\n\t\t.withLabels<'move-to-target'>()\n\t\t.withGroups<G>()\n\t\t.requires<TransformWorldConfig>()\n\t\t.install((world) => {\n\t\t\tworld\n\t\t\t\t.addSystem('move-to-target')\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('movers', {\n\t\t\t\t\twith: ['localTransform', 'moveTarget', 'moveSpeed'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, ecs, dt }) => {\n\t\t\t\t\tfor (const entity of queries.movers) {\n\t\t\t\t\t\tconst { localTransform, moveTarget, moveSpeed } = entity.components;\n\n\t\t\t\t\t\tconst dx = moveTarget.x - localTransform.x;\n\t\t\t\t\t\tconst dy = moveTarget.y - localTransform.y;\n\t\t\t\t\t\tconst distSq = dx * dx + dy * dy;\n\n\t\t\t\t\t\tif (distSq <= arrivalThresholdSq) {\n\t\t\t\t\t\t\tlocalTransform.x = moveTarget.x;\n\t\t\t\t\t\t\tlocalTransform.y = moveTarget.y;\n\t\t\t\t\t\t\tecs.markChanged(entity.id, 'localTransform');\n\t\t\t\t\t\t\tecs.commands.removeComponent(entity.id, 'moveTarget');\n\t\t\t\t\t\t\tecs.eventBus.publish('arriveAtTarget', { entityId: entity.id });\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst dist = Math.sqrt(distSq);\n\t\t\t\t\t\tconst step = Math.min(moveSpeed * dt, dist);\n\t\t\t\t\t\tlocalTransform.x += (dx / dist) * step;\n\t\t\t\t\t\tlocalTransform.y += (dy / dist) * step;\n\t\t\t\t\t\tecs.markChanged(entity.id, 'localTransform');\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n}\n"
6
6
  ],
7
- "mappings": "2PASA,uBAAS,kBAuEF,SAAS,CAAgB,CAAC,EAAW,EAAuD,CAClG,MAAO,CAAE,WAAY,CAAE,IAAG,GAAE,CAAE,EAiBxB,SAAS,CAAe,CAAC,EAA0D,CACzF,MAAO,CAAE,UAAW,CAAM,EA4BpB,SAAS,CAAmD,CAClE,EACC,CACD,IACC,cAAc,WACd,WAAW,IACX,QAAQ,SACR,mBAAmB,GAChB,GAAW,CAAC,EAEV,EAAqB,EAAmB,EAE9C,OAAO,EAAa,UAAU,EAC5B,mBAA2C,EAC3C,eAAmC,EACnC,WAA6B,EAC7B,WAAc,EACd,SAA+B,EAC/B,QAAQ,CAAC,IAAU,CACnB,EACE,UAAU,gBAAgB,EAC1B,YAAY,CAAQ,EACpB,QAAQ,CAAK,EACb,QAAQ,CAAW,EACnB,SAAS,SAAU,CACnB,KAAM,CAAC,iBAAkB,aAAc,WAAW,CACnD,CAAC,EACA,WAAW,EAAG,UAAS,MAAK,QAAS,CACrC,QAAW,KAAU,EAAQ,OAAQ,CACpC,IAAQ,iBAAgB,aAAY,aAAc,EAAO,WAEnD,EAAK,EAAW,EAAI,EAAe,EACnC,EAAK,EAAW,EAAI,EAAe,EACnC,EAAS,EAAK,EAAK,EAAK,EAE9B,GAAI,GAAU,EAAoB,CACjC,EAAe,EAAI,EAAW,EAC9B,EAAe,EAAI,EAAW,EAC9B,EAAI,YAAY,EAAO,GAAI,gBAAgB,EAC3C,EAAI,SAAS,gBAAgB,EAAO,GAAI,YAAY,EACpD,EAAI,SAAS,QAAQ,iBAAkB,CAAE,SAAU,EAAO,EAAG,CAAC,EAC9D,SAGD,IAAM,EAAO,KAAK,KAAK,CAAM,EACvB,EAAO,KAAK,IAAI,EAAY,EAAI,CAAI,EAC1C,EAAe,GAAM,EAAK,EAAQ,EAClC,EAAe,GAAM,EAAK,EAAQ,EAClC,EAAI,YAAY,EAAO,GAAI,gBAAgB,GAE5C,EACF",
7
+ "mappings": "2PASA,uBAAS,kBAyEF,SAAS,CAAgB,CAAC,EAAW,EAAuD,CAClG,MAAO,CAAE,WAAY,CAAE,IAAG,GAAE,CAAE,EAiBxB,SAAS,CAAe,CAAC,EAA0D,CACzF,MAAO,CAAE,UAAW,CAAM,EA4BpB,SAAS,CAAmD,CAClE,EACC,CACD,IACC,cAAc,WACd,WAAW,IACX,QAAQ,SACR,mBAAmB,GAChB,GAAW,CAAC,EAEV,EAAqB,EAAmB,EAE9C,OAAO,EAAa,UAAU,EAC5B,mBAA2C,EAC3C,eAAmC,EACnC,WAA6B,EAC7B,WAAc,EACd,SAA+B,EAC/B,QAAQ,CAAC,IAAU,CACnB,EACE,UAAU,gBAAgB,EAC1B,YAAY,CAAQ,EACpB,QAAQ,CAAK,EACb,QAAQ,CAAW,EACnB,SAAS,SAAU,CACnB,KAAM,CAAC,iBAAkB,aAAc,WAAW,CACnD,CAAC,EACA,WAAW,EAAG,UAAS,MAAK,QAAS,CACrC,QAAW,KAAU,EAAQ,OAAQ,CACpC,IAAQ,iBAAgB,aAAY,aAAc,EAAO,WAEnD,EAAK,EAAW,EAAI,EAAe,EACnC,EAAK,EAAW,EAAI,EAAe,EACnC,EAAS,EAAK,EAAK,EAAK,EAE9B,GAAI,GAAU,EAAoB,CACjC,EAAe,EAAI,EAAW,EAC9B,EAAe,EAAI,EAAW,EAC9B,EAAI,YAAY,EAAO,GAAI,gBAAgB,EAC3C,EAAI,SAAS,gBAAgB,EAAO,GAAI,YAAY,EACpD,EAAI,SAAS,QAAQ,iBAAkB,CAAE,SAAU,EAAO,EAAG,CAAC,EAC9D,SAGD,IAAM,EAAO,KAAK,KAAK,CAAM,EACvB,EAAO,KAAK,IAAI,EAAY,EAAI,CAAI,EAC1C,EAAe,GAAM,EAAK,EAAQ,EAClC,EAAe,GAAM,EAAK,EAAQ,EAClC,EAAI,YAAY,EAAO,GAAI,gBAAgB,GAE5C,EACF",
8
8
  "debugId": "A245A409F4202B3864756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -11,7 +11,7 @@
11
11
  */
12
12
  import { type BasePluginOptions } from 'ecspresso';
13
13
  import type { BaseWorld } from 'ecspresso';
14
- import type { WorldConfigFrom } from '../../type-utils';
14
+ import type { ComponentsConfig } from '../../type-utils';
15
15
  import type { TransformComponentTypes } from 'ecspresso/plugins/spatial/transform';
16
16
  /** BaseWorld narrowed to particle components for typed access in helpers. */
17
17
  type ParticleWorld = BaseWorld<ParticleComponentTypes>;
@@ -199,7 +199,7 @@ export declare const particlePresets: {
199
199
  readonly trail: (texture: unknown, overrides?: Partial<ParticleEffectInput>) => ParticleEffectConfig;
200
200
  };
201
201
  type ParticleLabels = 'particle-update' | 'particle-render-sync';
202
- type ParticleRequires = WorldConfigFrom<TransformComponentTypes & {
202
+ type ParticleRequires = ComponentsConfig<TransformComponentTypes & {
203
203
  renderLayer: string;
204
204
  }>;
205
205
  /**
@@ -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("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};
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:({entity: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:({entityId: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=60D36D4494C6248064756E2164756E21
3
+ //# debugId=E04EC3E8DCC5C25764756E2164756E21
4
4
  //# sourceMappingURL=particles.js.map