ecspresso 0.13.0 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/plugins/ai/detection.d.ts +118 -0
- package/dist/plugins/ai/detection.js +4 -0
- package/dist/plugins/ai/detection.js.map +10 -0
- package/dist/plugins/{audio.js → audio/audio.js} +1 -1
- package/dist/plugins/{audio.js.map → audio/audio.js.map} +2 -2
- package/dist/plugins/combat/health.d.ts +98 -0
- package/dist/plugins/combat/health.js +4 -0
- package/dist/plugins/combat/health.js.map +10 -0
- package/dist/plugins/combat/projectile.d.ts +115 -0
- package/dist/plugins/combat/projectile.js +4 -0
- package/dist/plugins/combat/projectile.js.map +10 -0
- package/dist/plugins/{diagnostics.js → debug/diagnostics.js} +1 -1
- package/dist/plugins/{diagnostics.js.map → debug/diagnostics.js.map} +2 -2
- package/dist/plugins/{input.js → input/input.js} +1 -1
- package/dist/plugins/{input.js.map → input/input.js.map} +2 -2
- package/dist/plugins/input/selection.d.ts +114 -0
- package/dist/plugins/input/selection.js +4 -0
- package/dist/plugins/input/selection.js.map +11 -0
- package/dist/plugins/isometric/depth-sort.d.ts +44 -0
- package/dist/plugins/isometric/depth-sort.js +4 -0
- package/dist/plugins/isometric/depth-sort.js.map +10 -0
- package/dist/plugins/isometric/projection.d.ts +83 -0
- package/dist/plugins/isometric/projection.js +4 -0
- package/dist/plugins/isometric/projection.js.map +10 -0
- package/dist/plugins/{collision.d.ts → physics/collision.d.ts} +1 -1
- package/dist/plugins/{collision.js → physics/collision.js} +1 -1
- package/dist/plugins/{collision.js.map → physics/collision.js.map} +3 -3
- package/dist/plugins/{physics2D.d.ts → physics/physics2D.d.ts} +1 -1
- package/dist/plugins/{physics2D.js → physics/physics2D.js} +1 -1
- package/dist/plugins/{physics2D.js.map → physics/physics2D.js.map} +3 -3
- package/dist/plugins/physics/steering.d.ts +102 -0
- package/dist/plugins/physics/steering.js +4 -0
- package/dist/plugins/physics/steering.js.map +10 -0
- package/dist/plugins/{particles.d.ts → rendering/particles.d.ts} +2 -2
- package/dist/plugins/{particles.js → rendering/particles.js} +1 -1
- package/dist/plugins/rendering/particles.js.map +10 -0
- package/dist/plugins/{renderers → rendering}/renderer2D.d.ts +9 -5
- package/dist/plugins/rendering/renderer2D.js +4 -0
- package/dist/plugins/rendering/renderer2D.js.map +10 -0
- package/dist/plugins/{sprite-animation.js → rendering/sprite-animation.js} +1 -1
- package/dist/plugins/{sprite-animation.js.map → rendering/sprite-animation.js.map} +2 -2
- package/dist/plugins/{coroutine.js → scripting/coroutine.js} +1 -1
- package/dist/plugins/{coroutine.js.map → scripting/coroutine.js.map} +2 -2
- package/dist/plugins/{state-machine.js → scripting/state-machine.js} +1 -1
- package/dist/plugins/{state-machine.js.map → scripting/state-machine.js.map} +2 -2
- package/dist/plugins/{timers.js → scripting/timers.js} +1 -1
- package/dist/plugins/{timers.js.map → scripting/timers.js.map} +2 -2
- package/dist/plugins/{tween.d.ts → scripting/tween.d.ts} +1 -1
- package/dist/plugins/{tween.js → scripting/tween.js} +1 -1
- package/dist/plugins/scripting/tween.js.map +11 -0
- package/dist/plugins/{bounds.js → spatial/bounds.js} +1 -1
- package/dist/plugins/{bounds.js.map → spatial/bounds.js.map} +2 -2
- package/dist/plugins/{camera.d.ts → spatial/camera.d.ts} +43 -12
- package/dist/plugins/spatial/camera.js +4 -0
- package/dist/plugins/spatial/camera.js.map +10 -0
- package/dist/plugins/{spatial-index.d.ts → spatial/spatial-index.d.ts} +2 -2
- package/dist/plugins/{spatial-index.js → spatial/spatial-index.js} +1 -1
- package/dist/plugins/{spatial-index.js.map → spatial/spatial-index.js.map} +3 -3
- package/dist/plugins/{transform.d.ts → spatial/transform.d.ts} +1 -1
- package/dist/plugins/{transform.js → spatial/transform.js} +1 -1
- package/dist/plugins/spatial/transform.js.map +10 -0
- package/package.json +77 -49
- package/dist/plugins/camera.js +0 -4
- package/dist/plugins/camera.js.map +0 -10
- package/dist/plugins/particles.js.map +0 -10
- package/dist/plugins/renderers/renderer2D.js +0 -4
- package/dist/plugins/renderers/renderer2D.js.map +0 -10
- package/dist/plugins/transform.js.map +0 -10
- package/dist/plugins/tween.js.map +0 -11
- /package/dist/plugins/{audio.d.ts → audio/audio.d.ts} +0 -0
- /package/dist/plugins/{diagnostics.d.ts → debug/diagnostics.d.ts} +0 -0
- /package/dist/plugins/{input.d.ts → input/input.d.ts} +0 -0
- /package/dist/plugins/{sprite-animation.d.ts → rendering/sprite-animation.d.ts} +0 -0
- /package/dist/plugins/{coroutine.d.ts → scripting/coroutine.d.ts} +0 -0
- /package/dist/plugins/{state-machine.d.ts → scripting/state-machine.d.ts} +0 -0
- /package/dist/plugins/{timers.d.ts → scripting/timers.d.ts} +0 -0
- /package/dist/plugins/{bounds.d.ts → spatial/bounds.d.ts} +0 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/plugins/spatial/camera.ts", "../src/plugins/input/selection.ts"],
|
|
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\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\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\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\tlet targetWorld;\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\ttargetWorld = ecs.getComponent(cameraFollow.target, 'worldTransform');\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\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\tlet pendingSteps = 0;\n\t\t\t\tlet zoomActive = false;\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\ttype InputState = { pointer: { position: { x: number; y: number } } };\n\t\t\t\t\t\tconst inputState = ecs.tryGetResource<InputState>('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\tpixiApp.canvas.addEventListener('wheel', onWheel as EventListener, { passive: false });\n\t\t\t\t\t\tzoomActive = true;\n\t\t\t\t\t})\n\t\t\t\t\t.setOnDetach((ecs) => {\n\t\t\t\t\t\tif (!zoomActive) return;\n\t\t\t\t\t\tconst pixiApp = ecs.tryGetResource('pixiApp') as { canvas: HTMLCanvasElement } | undefined;\n\t\t\t\t\t\tif (pixiApp) {\n\t\t\t\t\t\t\tpixiApp.canvas.removeEventListener('wheel', onWheel as EventListener);\n\t\t\t\t\t\t}\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\ttype InputState = { pointer: { position: { x: number; y: number } } };\n\t\t\t\t\t\tconst inputState = ecs.tryGetResource<InputState>('inputState');\n\t\t\t\t\t\tif (!inputState) return;\n\n\t\t\t\t\t\t// World point under cursor before zoom\n\t\t\t\t\t\tconst worldBefore = screenToWorld(\n\t\t\t\t\t\t\tinputState.pointer.position.x,\n\t\t\t\t\t\t\tinputState.pointer.position.y,\n\t\t\t\t\t\t\tcameraState,\n\t\t\t\t\t\t);\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\tcam.zoom = Math.max(minZoom, Math.min(maxZoom, cam.zoom * Math.pow(direction, Math.abs(steps))));\n\n\t\t\t\t\t\t// Adjust camera position so the world point under cursor stays fixed\n\t\t\t\t\t\tcam.x = worldBefore.x - (inputState.pointer.position.x - cameraState.viewportWidth / 2) / cam.zoom;\n\t\t\t\t\t\tcam.y = worldBefore.y - (inputState.pointer.position.y - cameraState.viewportHeight / 2) / cam.zoom;\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"
|
|
7
|
+
],
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "206042F83F56CEF464756E2164756E21",
|
|
10
|
+
"names": []
|
|
11
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Isometric Depth Sort Plugin for ECSpresso
|
|
3
|
+
*
|
|
4
|
+
* Sets PixiJS `zIndex` on entities based on their world-space position,
|
|
5
|
+
* ensuring correct visual overlap in isometric rendering. Entities with
|
|
6
|
+
* higher world X + Y values render in front.
|
|
7
|
+
*
|
|
8
|
+
* Requires `rootContainer` from the renderer2D plugin.
|
|
9
|
+
* Enables `sortableChildren` on the root container at initialization.
|
|
10
|
+
*/
|
|
11
|
+
import type { BasePluginOptions } from 'ecspresso';
|
|
12
|
+
import type { WorldConfigFrom } from '../../type-utils';
|
|
13
|
+
import type { TransformComponentTypes } from '../spatial/transform';
|
|
14
|
+
import type { Renderer2DComponentTypes, Renderer2DResourceTypes } from '../rendering/renderer2D';
|
|
15
|
+
/**
|
|
16
|
+
* Optional component that offsets an entity's depth sort value.
|
|
17
|
+
* Entities with a positive depthOffset render in front of entities
|
|
18
|
+
* at the same world position (e.g., a player on top of a ground tile).
|
|
19
|
+
*/
|
|
20
|
+
export interface IsoDepthSortComponentTypes {
|
|
21
|
+
depthOffset: number;
|
|
22
|
+
}
|
|
23
|
+
type IsoDepthSortRequires = WorldConfigFrom<TransformComponentTypes & Pick<Renderer2DComponentTypes, 'sprite' | 'graphics' | 'container'>, {}, Renderer2DResourceTypes>;
|
|
24
|
+
export interface IsoDepthSortPluginOptions<G extends string = 'isometric'> extends BasePluginOptions<G> {
|
|
25
|
+
/** Custom depth function. Receives world-space x/y, returns a sort key.
|
|
26
|
+
* Default: `(x, y) => x + y` */
|
|
27
|
+
depthFn?: (worldX: number, worldY: number) => number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Create an isometric depth sort plugin.
|
|
31
|
+
*
|
|
32
|
+
* Adds a render-phase system that sets PixiJS `zIndex` based on world-space
|
|
33
|
+
* position, enabling correct front-to-back ordering in isometric views.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* const ecs = ECSpresso.create()
|
|
38
|
+
* .withPlugin(createRenderer2DPlugin({ ... }))
|
|
39
|
+
* .withPlugin(createIsoDepthSortPlugin())
|
|
40
|
+
* .build();
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export declare function createIsoDepthSortPlugin<G extends string = 'isometric'>(options?: IsoDepthSortPluginOptions<G>): import("ecspresso").Plugin<import("ecspresso").WithComponents<import("ecspresso").EmptyConfig, IsoDepthSortComponentTypes>, IsoDepthSortRequires, never, G, never, never>;
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
var I=((b)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(b,{get:(k,B)=>(typeof require<"u"?require:k)[B]}):b)(function(b){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+b+'" is not supported')});import{definePlugin as E}from"ecspresso";function H(b,k){return b+k}function L(b){let{depthFn:k=H,systemGroup:B="isometric"}=b??{};return E("isometric-depth-sort").withComponentTypes().requires().withGroups().install((C)=>{C.addSystem("isometric-depth-sort-init").inGroup(B).setOnInitialize((x)=>{let v=x.getResource("rootContainer");v.sortableChildren=!0}),C.addSystem("isometric-depth-sort").setPriority(350).inPhase("render").inGroup(B).addQuery("sprites",{with:["sprite","worldTransform"],changed:["worldTransform"],optional:["depthOffset"]}).addQuery("graphics",{with:["graphics","worldTransform"],changed:["worldTransform"],optional:["depthOffset"]}).addQuery("containers",{with:["container","worldTransform"],changed:["worldTransform"],optional:["depthOffset"]}).setProcess(({queries:x})=>{for(let v of x.sprites){let{sprite:z,worldTransform:j,depthOffset:A}=v.components;z.zIndex=k(j.x,j.y)+(A??0)}for(let v of x.graphics){let{graphics:z,worldTransform:j,depthOffset:A}=v.components;z.zIndex=k(j.x,j.y)+(A??0)}for(let v of x.containers){let{container:z,worldTransform:j,depthOffset:A}=v.components;z.zIndex=k(j.x,j.y)+(A??0)}})})}export{L as createIsoDepthSortPlugin};
|
|
2
|
+
|
|
3
|
+
//# debugId=E8C9DCC3EA4239A864756E2164756E21
|
|
4
|
+
//# sourceMappingURL=depth-sort.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/plugins/isometric/depth-sort.ts"],
|
|
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"
|
|
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",
|
|
8
|
+
"debugId": "E8C9DCC3EA4239A864756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Isometric Projection Plugin for ECSpresso
|
|
3
|
+
*
|
|
4
|
+
* Converts Cartesian world-space positions to isometric screen positions
|
|
5
|
+
* in the render phase. All ECS-level logic (physics, collision, camera follow)
|
|
6
|
+
* continues to operate in Cartesian world space — only PixiJS display object
|
|
7
|
+
* positions are projected.
|
|
8
|
+
*
|
|
9
|
+
* Optionally provides an isometric-aware camera sync system that projects
|
|
10
|
+
* the camera position before applying it to the root container.
|
|
11
|
+
*/
|
|
12
|
+
import type { BasePluginOptions } from 'ecspresso';
|
|
13
|
+
import type { WorldConfigFrom } from '../../type-utils';
|
|
14
|
+
import type { TransformComponentTypes } from '../spatial/transform';
|
|
15
|
+
import type { Renderer2DComponentTypes, Renderer2DResourceTypes } from '../rendering/renderer2D';
|
|
16
|
+
import type { CameraResourceTypes } from '../spatial/camera';
|
|
17
|
+
/**
|
|
18
|
+
* Isometric projection configuration.
|
|
19
|
+
*/
|
|
20
|
+
export interface IsoProjectionState {
|
|
21
|
+
readonly tileWidth: number;
|
|
22
|
+
readonly tileHeight: number;
|
|
23
|
+
readonly originX: number;
|
|
24
|
+
readonly originY: number;
|
|
25
|
+
}
|
|
26
|
+
export interface IsoProjectionResourceTypes {
|
|
27
|
+
isoProjection: IsoProjectionState;
|
|
28
|
+
}
|
|
29
|
+
type IsoProjectionRequires = WorldConfigFrom<TransformComponentTypes & Pick<Renderer2DComponentTypes, 'sprite' | 'graphics' | 'container'>, {}, Renderer2DResourceTypes & CameraResourceTypes>;
|
|
30
|
+
export interface IsoProjectionPluginOptions<G extends string = 'isometric'> extends BasePluginOptions<G> {
|
|
31
|
+
/** Tile width in pixels (default: 64) */
|
|
32
|
+
tileWidth?: number;
|
|
33
|
+
/** Tile height in pixels (default: 32) */
|
|
34
|
+
tileHeight?: number;
|
|
35
|
+
/** Screen-space X origin offset (default: 0) */
|
|
36
|
+
originX?: number;
|
|
37
|
+
/** Screen-space Y origin offset (default: 0) */
|
|
38
|
+
originY?: number;
|
|
39
|
+
/** Register an isometric-aware camera sync system (default: false).
|
|
40
|
+
* When true, set `camera: false` on createRenderer2DPlugin to avoid conflicts. */
|
|
41
|
+
camera?: boolean;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Convert Cartesian world coordinates to isometric screen coordinates.
|
|
45
|
+
*
|
|
46
|
+
* @param worldX World-space X coordinate
|
|
47
|
+
* @param worldY World-space Y coordinate
|
|
48
|
+
* @param state Isometric projection state
|
|
49
|
+
* @returns New object with projected { x, y }
|
|
50
|
+
*/
|
|
51
|
+
export declare function worldToIso(worldX: number, worldY: number, state: IsoProjectionState): {
|
|
52
|
+
x: number;
|
|
53
|
+
y: number;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Convert isometric screen coordinates back to Cartesian world coordinates.
|
|
57
|
+
*
|
|
58
|
+
* @param isoX Isometric screen-space X coordinate
|
|
59
|
+
* @param isoY Isometric screen-space Y coordinate
|
|
60
|
+
* @param state Isometric projection state
|
|
61
|
+
* @returns New object with world-space { x, y }
|
|
62
|
+
*/
|
|
63
|
+
export declare function isoToWorld(isoX: number, isoY: number, state: IsoProjectionState): {
|
|
64
|
+
x: number;
|
|
65
|
+
y: number;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Create an isometric projection plugin.
|
|
69
|
+
*
|
|
70
|
+
* Adds a render-phase system that overwrites PixiJS display object positions
|
|
71
|
+
* with isometric projections of their `worldTransform` coordinates.
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```typescript
|
|
75
|
+
* const ecs = ECSpresso.create()
|
|
76
|
+
* .withPlugin(createRenderer2DPlugin({ camera: false, ... }))
|
|
77
|
+
* .withPlugin(createCameraPlugin({ ... }))
|
|
78
|
+
* .withPlugin(createIsoProjectionPlugin({ tileWidth: 64, tileHeight: 32, camera: true }))
|
|
79
|
+
* .build();
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export declare function createIsoProjectionPlugin<G extends string = 'isometric'>(options?: IsoProjectionPluginOptions<G>): import("ecspresso").Plugin<import("ecspresso").WithResources<import("ecspresso").EmptyConfig, IsoProjectionResourceTypes>, IsoProjectionRequires, never, G, never, never>;
|
|
83
|
+
export {};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
var V=((k)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(k,{get:(x,b)=>(typeof require<"u"?require:x)[b]}):k)(function(k){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+k+'" is not supported')});import{definePlugin as U}from"ecspresso";var O={x:0,y:0};function L(k,x,b,z,A,M){return O.x=(k-x)*b+A,O.y=(k+x)*z+M,O}function C(k,x,b){return{x:(k-x)*(b.tileWidth/2)+b.originX,y:(k+x)*(b.tileHeight/2)+b.originY}}function S(k,x,b){let z=k-b.originX,A=x-b.originY;return{x:z/b.tileWidth+A/b.tileHeight,y:-z/b.tileWidth+A/b.tileHeight}}function G(k){let{tileWidth:x=64,tileHeight:b=32,originX:z=0,originY:A=0,camera:M=!1,systemGroup:Q="isometric"}=k??{};return U("isometric-projection").withResourceTypes().requires().withGroups().install((N)=>{let J=x/2,K=b/2;if(N.addResource("isoProjection",{tileWidth:x,tileHeight:b,originX:z,originY:A}),N.addSystem("isometric-projection").setPriority(400).inPhase("render").inGroup(Q).addQuery("sprites",{with:["sprite","worldTransform"],changed:["worldTransform"]}).addQuery("graphics",{with:["graphics","worldTransform"],changed:["worldTransform"]}).addQuery("containers",{with:["container","worldTransform"],changed:["worldTransform"]}).setProcess(({queries:v})=>{for(let E of v.sprites){let{sprite:F,worldTransform:B}=E.components,D=L(B.x,B.y,J,K,z,A);F.position.set(D.x,D.y)}for(let E of v.graphics){let{graphics:F,worldTransform:B}=E.components,D=L(B.x,B.y,J,K,z,A);F.position.set(D.x,D.y)}for(let E of v.containers){let{container:F,worldTransform:B}=E.components,D=L(B.x,B.y,J,K,z,A);F.position.set(D.x,D.y)}}),M)N.addSystem("isometric-camera-sync").setPriority(900).inPhase("render").inGroup(Q).withResources(["cameraState","rootContainer","pixiApp"]).setProcess(({resources:{cameraState:v,rootContainer:E,pixiApp:F}})=>{let B=F.screen.width,D=F.screen.height,R=L(v.x+v.shakeOffsetX,v.y+v.shakeOffsetY,J,K,z,A);E.position.set(B/2-R.x*v.zoom,D/2-R.y*v.zoom),E.scale.set(v.zoom),E.rotation=-(v.rotation+v.shakeRotation)})})}export{C as worldToIso,S as isoToWorld,G as createIsoProjectionPlugin};
|
|
2
|
+
|
|
3
|
+
//# debugId=D8E00FC32D5B431064756E2164756E21
|
|
4
|
+
//# sourceMappingURL=projection.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/plugins/isometric/projection.ts"],
|
|
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// ==================== 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
|
+
],
|
|
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,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",
|
|
8
|
+
"debugId": "D8E00FC32D5B431064756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Supports AABB and circle colliders.
|
|
7
7
|
*/
|
|
8
8
|
import { type BasePluginOptions } from 'ecspresso';
|
|
9
|
-
import type { TransformWorldConfig } from '
|
|
9
|
+
import type { TransformWorldConfig } from '../spatial/transform';
|
|
10
10
|
/**
|
|
11
11
|
* Axis-Aligned Bounding Box collider.
|
|
12
12
|
*/
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
var p=((z)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(z,{get:(J,Z)=>(typeof require<"u"?require:J)[Z]}):z)(function(z){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+z+'" is not supported')});import{definePlugin as v}from"ecspresso";var H={normalX:0,normalY:0,depth:0},S=0;function g(z,J,Z,N,V,O,$,Q){if(z.entityId=J,z.layer=V,z.collidesWith=O,$)return z.x=Z+($.offsetX??0),z.y=N+($.offsetY??0),z.shape=0,z.halfWidth=$.width/2,z.halfHeight=$.height/2,z.radius=0,!0;if(Q)return z.x=Z+(Q.offsetX??0),z.y=N+(Q.offsetY??0),z.shape=1,z.halfWidth=0,z.halfHeight=0,z.radius=Q.radius,!0;return!1}function _(z){return z("spatialIndex")}function m(z,J,Z,N,V,O,$,Q,E){let G=V-z,F=O-J,j=Z+$-Math.abs(G),U=N+Q-Math.abs(F);if(j<=0||U<=0)return!1;if(j<U)return E.normalX=G>=0?1:-1,E.normalY=0,E.depth=j,!0;return E.normalX=0,E.normalY=F>=0?1:-1,E.depth=U,!0}function C(z,J,Z,N,V,O,$){let Q=N-z,E=V-J,G=Q*Q+E*E,F=Z+O;if(G>=F*F)return!1;let j=Math.sqrt(G);if(j===0)return $.normalX=1,$.normalY=0,$.depth=F,!0;return $.normalX=Q/j,$.normalY=E/j,$.depth=F-j,!0}function W(z,J,Z,N,V,O,$,Q){let E=Math.max(z-Z,Math.min(V,z+Z)),G=Math.max(J-N,Math.min(O,J+N)),F=V-E,j=O-G,U=F*F+j*j;if(U>=$*$)return!1;if(U===0){let T=V-(z-Z),q=z+Z-V,K=O-(J-N),L=J+N-O,P=Math.min(T,q,K,L);if(P===q)return Q.normalX=1,Q.normalY=0,Q.depth=q+$,!0;if(P===T)return Q.normalX=-1,Q.normalY=0,Q.depth=T+$,!0;if(P===L)return Q.normalX=0,Q.normalY=1,Q.depth=L+$,!0;return Q.normalX=0,Q.normalY=-1,Q.depth=K+$,!0}let D=Math.sqrt(U);return Q.normalX=F/D,Q.normalY=j/D,Q.depth=$-D,!0}function A(z,J,Z){if(z.shape===0&&J.shape===0)return m(z.x,z.y,z.halfWidth,z.halfHeight,J.x,J.y,J.halfWidth,J.halfHeight,Z);if(z.shape===1&&J.shape===1)return C(z.x,z.y,z.radius,J.x,J.y,J.radius,Z);if(z.shape===0&&J.shape===1)return W(z.x,z.y,z.halfWidth,z.halfHeight,J.x,J.y,J.radius,Z);if(!W(J.x,J.y,J.halfWidth,J.halfHeight,z.x,z.y,z.radius,Z))return!1;return Z.normalX=-Z.normalX,Z.normalY=-Z.normalY,!0}var R=new Set,M=!1,k=50;function B(z,J,Z,N,V,O){if(N)Y(z,J,Z,N,V,O);else X(z,J,V,O)}function X(z,J,Z,N){if(!M&&J>=k)M=!0,console.warn(`[ecspresso] Collision detection is using O(n²) brute force with ${J} colliders. For better performance, install createSpatialIndexPlugin() alongside your collision or physics2D plugin.`);for(let V=0;V<J;V++){let O=z[V];if(!O)continue;for(let $=V+1;$<J;$++){let Q=z[$];if(!Q)continue;if(!O.collidesWith.includes(Q.layer)&&!Q.collidesWith.includes(O.layer))continue;if(!A(O,Q,H))continue;Z(O,Q,H,N)}}}function Y(z,J,Z,N,V,O){Z.clear();for(let $=0;$<J;$++){let Q=z[$];if(!Q)continue;Z.set(Q.entityId,Q)}for(let $=0;$<J;$++){let Q=z[$];if(!Q)continue;let E=Q.shape===0?Q.halfWidth:Q.radius,G=Q.shape===0?Q.halfHeight:Q.radius;R.clear(),N.queryRectInto(Q.x-E,Q.y-G,Q.x+E,Q.y+G,R);for(let F of R){if(F<=Q.entityId)continue;let j=Z.get(F);if(!j)continue;if(!Q.collidesWith.includes(j.layer)&&!j.collidesWith.includes(Q.layer))continue;if(!A(Q,j,H))continue;V(Q,j,H,O)}}}function d(z,J,Z,N){let V={width:z,height:J};if(Z!==void 0)V.offsetX=Z;if(N!==void 0)V.offsetY=N;return{aabbCollider:V}}function u(z,J,Z){let N={radius:z};if(J!==void 0)N.offsetX=J;if(Z!==void 0)N.offsetY=Z;return{circleCollider:N}}function w(z,J){return{collisionLayer:{layer:z,collidesWith:J}}}function n(z){let J={};for(let Z of Object.keys(z)){let N=z[Z];J[Z]=()=>w(Z,N)}return J}function I(z){let J=z.indexOf(":");if(J===-1)throw Error(`Invalid collision pair key "${z}": must contain a colon separator (e.g. "player:enemy")`);let Z=z.slice(0,J),N=z.slice(J+1);if(Z===""||N==="")throw Error(`Invalid collision pair key "${z}": layer names must not be empty`);return[Z,N]}function s(z){let J=new Map,Z=new Set;for(let N of Object.keys(z))I(N),Z.add(N);for(let N of Object.keys(z)){let[V,O]=I(N),$=z[N];if(!$)continue;J.set(N,{callback:$,swapped:!1});let Q=`${O}:${V}`;if(Q!==N&&!Z.has(Q))J.set(Q,{callback:$,swapped:!0})}return function({data:V,ecs:O}){let $=J.get(V.layerA+":"+V.layerB);if(!$)return;if($.swapped)$.callback(V.entityB,V.entityA,O);else $.callback(V.entityA,V.entityB,O)}}function x(z,J,Z,N){N.publish("collision",{entityA:z.entityId,entityB:J.entityId,layerA:z.layer,layerB:J.layer,normalX:Z.normalX,normalY:Z.normalY,depth:Z.depth})}function l(z){let{systemGroup:J="physics",priority:Z=0,phase:N="postUpdate"}=z;return v("collision").withComponentTypes().withEventTypes().withLabels().withGroups().requires().install((V)=>{let O=[],$=new Map;V.addSystem("collision-detection").setPriority(Z).inPhase(N).inGroup(J).addQuery("collidables",{with:["worldTransform","collisionLayer"]}).setProcess(({queries:Q,ecs:E})=>{let G=0;for(let j of Q.collidables){let{worldTransform:U,collisionLayer:D}=j.components,T=E.getComponent(j.id,"aabbCollider"),q=E.getComponent(j.id,"circleCollider");if(!T&&!q)continue;let K=O[G];if(!K)K={entityId:j.id,x:U.x,y:U.y,layer:D.layer,collidesWith:D.collidesWith,shape:S,halfWidth:0,halfHeight:0,radius:0},O[G]=K;if(!g(K,j.id,U.x,U.y,D.layer,D.collidesWith,T,q))continue;G++}let F=_(E.tryGetResource.bind(E));B(O,G,$,F,x,E.eventBus)})})}export{n as defineCollisionLayers,l as createCollisionPlugin,s as createCollisionPairHandler,w as createCollisionLayer,u as createCircleCollider,d as createAABBCollider};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=F04221DD4AA45FC664756E2164756E21
|
|
4
4
|
//# sourceMappingURL=collision.js.map
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/plugins/collision.ts", "../src/utils/narrowphase.ts"],
|
|
3
|
+
"sources": ["../src/plugins/physics/collision.ts", "../src/utils/narrowphase.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * Collision Plugin for ECSpresso\n *\n * Provides layer-based collision detection with events.\n * Uses worldTransform for position (world-space collision).\n * Supports AABB and circle colliders.\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type { TransformWorldConfig } from './transform';\nimport { fillBaseColliderInfo, detectCollisions, tryGetSpatialIndex, AABB_SHAPE, type Contact, type BaseColliderInfo } from '../utils/narrowphase';\n\n// ==================== Component Types ====================\n\n/**\n * Axis-Aligned Bounding Box collider.\n */\nexport interface AABBCollider {\n\t/** Width of the bounding box */\n\twidth: number;\n\t/** Height of the bounding box */\n\theight: number;\n\t/** X offset from entity position (default: 0) */\n\toffsetX?: number;\n\t/** Y offset from entity position (default: 0) */\n\toffsetY?: number;\n}\n\n/**\n * Circle collider.\n */\nexport interface CircleCollider {\n\t/** Radius of the circle */\n\tradius: number;\n\t/** X offset from entity position (default: 0) */\n\toffsetX?: number;\n\t/** Y offset from entity position (default: 0) */\n\toffsetY?: number;\n}\n\n/**\n * Collision layer configuration.\n */\nexport interface CollisionLayer<L extends string = never> {\n\t/** The layer this entity belongs to */\n\tlayer: L;\n\t/** Layers this entity can collide with */\n\tcollidesWith: readonly L[];\n}\n\n/**\n * Component types provided by the collision plugin.\n * Included automatically via `.withPlugin(createCollisionPlugin())`.\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createCollisionPlugin())\n * .withComponentTypes<{ sprite: Sprite; enemy: boolean }>()\n * .build();\n * ```\n */\nexport interface CollisionComponentTypes<L extends string = never> {\n\taabbCollider: AABBCollider;\n\tcircleCollider: CircleCollider;\n\tcollisionLayer: CollisionLayer<L>;\n}\n\n// ==================== Event Types ====================\n\n/**\n * Event fired when two entities collide.\n *\n * Normal components are flattened (`normalX`/`normalY`) rather than nested\n * in a sub-object to avoid a per-event allocation in the collision hot path.\n */\nexport interface CollisionEvent<L extends string = never> {\n\t/** First entity in the collision */\n\tentityA: number;\n\t/** Second entity in the collision */\n\tentityB: number;\n\t/** Layer of the first entity */\n\tlayerA: L;\n\t/** Layer of the second entity */\n\tlayerB: L;\n\t/** Contact normal X, pointing from entityA toward entityB */\n\tnormalX: number;\n\t/** Contact normal Y, pointing from entityA toward entityB */\n\tnormalY: number;\n\t/** Penetration depth (positive = overlapping) */\n\tdepth: number;\n}\n\n/**\n * Event types provided by the collision plugin.\n */\nexport interface CollisionEventTypes<L extends string = never> {\n\tcollision: CollisionEvent<L>;\n}\n\n// ==================== Plugin Options ====================\n\n/**\n * Configuration options for the collision plugin.\n */\nexport interface CollisionPluginOptions<G extends string = 'physics'> extends BasePluginOptions<G> {\n\t/** Name of the collision event (default: 'collision') */\n\tcollisionEventName?: string;\n}\n\n// ==================== Helper Functions ====================\n\n/**\n * Create an AABB collider component.\n *\n * @param width Width of the bounding box\n * @param height Height of the bounding box\n * @param offsetX X offset from entity position\n * @param offsetY Y offset from entity position\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createTransform(100, 200),\n * ...createAABBCollider(50, 30),\n * });\n * ```\n */\nexport function createAABBCollider(\n\twidth: number,\n\theight: number,\n\toffsetX?: number,\n\toffsetY?: number\n): { aabbCollider: AABBCollider } {\n\tconst collider: AABBCollider = { width, height };\n\tif (offsetX !== undefined) collider.offsetX = offsetX;\n\tif (offsetY !== undefined) collider.offsetY = offsetY;\n\treturn { aabbCollider: collider };\n}\n\n/**\n * Create a circle collider component.\n *\n * @param radius Radius of the circle\n * @param offsetX X offset from entity position\n * @param offsetY Y offset from entity position\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createTransform(100, 200),\n * ...createCircleCollider(25),\n * });\n * ```\n */\nexport function createCircleCollider(\n\tradius: number,\n\toffsetX?: number,\n\toffsetY?: number\n): { circleCollider: CircleCollider } {\n\tconst collider: CircleCollider = { radius };\n\tif (offsetX !== undefined) collider.offsetX = offsetX;\n\tif (offsetY !== undefined) collider.offsetY = offsetY;\n\treturn { circleCollider: collider };\n}\n\n/**\n * Create a collision layer component.\n *\n * @param layer The layer this entity belongs to\n * @param collidesWith Layers this entity can collide with\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createTransform(100, 200),\n * ...createAABBCollider(50, 30),\n * ...createCollisionLayer('player', ['enemy', 'obstacle']),\n * });\n * ```\n */\nexport function createCollisionLayer<L extends string>(\n\tlayer: L,\n\tcollidesWith: readonly L[]\n): Pick<CollisionComponentTypes<L>, 'collisionLayer'> {\n\treturn {\n\t\tcollisionLayer: { layer, collidesWith },\n\t};\n}\n\n/**\n * Layer factory result from defineCollisionLayers.\n */\nexport type LayerFactories<T extends Record<string, readonly string[]>> = {\n\t[K in keyof T]: () => Pick<CollisionComponentTypes<Extract<keyof T, string>>, 'collisionLayer'>;\n};\n\n/**\n * Extract layer names from a `defineCollisionLayers` result for use with\n * `createCollisionPairHandler`'s `L` type parameter.\n *\n * @example\n * ```typescript\n * const layers = defineCollisionLayers({ player: ['enemy'], enemy: ['player'] });\n * type Layer = LayersOf<typeof layers>;\n * const handler = createCollisionPairHandler<ECS, Layer>({\n * 'player:enemy': (playerId, enemyId, ecs) => { ... },\n * });\n * ```\n */\nexport type LayersOf<T> = Extract<keyof T, string>;\n\n/**\n * Define collision layer relationships and get factory functions.\n *\n * @param rules Object mapping layer names to arrays of layers they collide with\n * @returns Object with factory functions for each layer\n *\n * @example\n * ```typescript\n * const layers = defineCollisionLayers({\n * player: ['enemy', 'enemyProjectile'],\n * playerProjectile: ['enemy'],\n * enemy: ['playerProjectile'],\n * enemyProjectile: ['player'],\n * });\n *\n * // Usage\n * ecs.spawn({\n * ...createTransform(100, 200),\n * ...createAABBCollider(50, 30),\n * ...layers.player(),\n * });\n * ```\n */\n/**\n * Validates that all `collidesWith` values reference actual layer keys.\n * Catches typos at compile time.\n */\ntype ValidateCollidesWith<T> = {\n\t[K in keyof T]: T[K] extends readonly (infer V)[]\n\t\t? [V] extends [Extract<keyof T, string>] ? T[K] : readonly Extract<keyof T, string>[]\n\t\t: never;\n};\n\nexport function defineCollisionLayers<const T extends Record<string, readonly string[]>>(\n\trules: T & ValidateCollidesWith<T>\n): LayerFactories<T> {\n\ttype L = Extract<keyof T, string>;\n\tconst factories = {} as LayerFactories<T>;\n\n\tfor (const layer of Object.keys(rules) as Array<L>) {\n\t\tconst collidesWith = rules[layer] as readonly L[];\n\t\tfactories[layer] = () => createCollisionLayer<L>(layer, collidesWith);\n\t}\n\n\treturn factories;\n}\n\n// ==================== Collision Pair Handler ====================\n\n/**\n * Callback for a collision pair handler.\n *\n * @param firstEntityId Entity belonging to the first layer in the pair key\n * @param secondEntityId Entity belonging to the second layer in the pair key\n * @param ecs The ECS world instance (passed through from the subscriber)\n */\nexport type CollisionPairCallback<W = unknown> = (\n\tfirstEntityId: number,\n\tsecondEntityId: number,\n\tecs: W,\n) => void;\n\ninterface PairEntry<W> {\n\tcallback: CollisionPairCallback<W>;\n\tswapped: boolean;\n}\n\nfunction parsePairKey(key: string): [string, string] {\n\tconst colonIndex = key.indexOf(':');\n\tif (colonIndex === -1) {\n\t\tthrow new Error(`Invalid collision pair key \"${key}\": must contain a colon separator (e.g. \"player:enemy\")`);\n\t}\n\tconst layerA = key.slice(0, colonIndex);\n\tconst layerB = key.slice(colonIndex + 1);\n\tif (layerA === '' || layerB === '') {\n\t\tthrow new Error(`Invalid collision pair key \"${key}\": layer names must not be empty`);\n\t}\n\treturn [layerA, layerB];\n}\n\n/**\n * Create a collision pair handler that routes collision events to\n * layer-pair-specific callbacks.\n *\n * Registering `\"a:b\"` automatically handles both `(layerA=a, layerB=b)` and\n * `(layerA=b, layerB=a)`. Entity arguments are swapped to match the declared\n * key order. If both `\"a:b\"` and `\"b:a\"` are explicitly registered, each gets\n * its own handler with no implicit reverse.\n *\n * @typeParam W - The ECS world type (e.g. `ECSpresso<C, E, R>`). Defaults to `unknown`.\n * @typeParam L - Union of valid layer names. Defaults to `string`.\n * Provide specific layer names for compile-time key validation:\n * `createCollisionPairHandler<ECS, keyof typeof layers>({...})`\n *\n * @param pairs Object mapping `\"layerA:layerB\"` keys to callbacks\n * @returns A dispatch function to call with collision event data and ECS instance\n *\n * @example\n * ```typescript\n * // Basic usage:\n * const handler = createCollisionPairHandler<ECS>({\n * 'playerProjectile:enemy': (projectileId, enemyId, ecs) => {\n * ecs.commands.removeEntity(projectileId);\n * },\n * });\n *\n * // With layer name validation:\n * const layers = defineCollisionLayers({ player: ['enemy'], enemy: ['player'] });\n * type Layer = LayersOf<typeof layers>;\n * const handler = createCollisionPairHandler<ECS, Layer>({\n * 'player:enemy': (playerId, enemyId, ecs) => { ... },\n * });\n *\n * ecs.eventBus.subscribe('collision', (data) => handler({ data, ecs }));\n * ```\n */\nexport function createCollisionPairHandler<W = unknown, L extends string = string>(\n\tpairs: { [K in `${L}:${L}`]?: CollisionPairCallback<W> }\n): (ctx: { data: CollisionEvent<L>; ecs: W }) => void;\nexport function createCollisionPairHandler<W = unknown>(\n\tpairs: Record<string, CollisionPairCallback<W> | undefined>\n): (ctx: { data: CollisionEvent<string>; ecs: W }) => void {\n\tconst lookup = new Map<string, PairEntry<W>>();\n\tconst explicitKeys = new Set<string>();\n\n\t// First pass: collect all explicit keys\n\tfor (const key of Object.keys(pairs)) {\n\t\tparsePairKey(key); // validate\n\t\texplicitKeys.add(key);\n\t}\n\n\t// Second pass: build lookup with forward + conditional reverse entries\n\tfor (const key of Object.keys(pairs)) {\n\t\tconst [layerA, layerB] = parsePairKey(key);\n\t\tconst callback = pairs[key];\n\t\tif (!callback) continue;\n\n\t\t// Forward entry\n\t\tlookup.set(key, { callback, swapped: false });\n\n\t\t// Reverse entry (only if the reverse key wasn't explicitly registered\n\t\t// and it's not a self-collision where forward === reverse)\n\t\tconst reverseKey = `${layerB}:${layerA}`;\n\t\tif (reverseKey !== key && !explicitKeys.has(reverseKey)) {\n\t\t\tlookup.set(reverseKey, { callback, swapped: true });\n\t\t}\n\t}\n\n\treturn function collisionPairDispatch({ data: event, ecs }: { data: CollisionEvent<string>; ecs: W }): void {\n\t\tconst entry = lookup.get(event.layerA + ':' + event.layerB);\n\t\tif (!entry) return;\n\n\t\tif (entry.swapped) {\n\t\t\tentry.callback(event.entityB, event.entityA, ecs);\n\t\t} else {\n\t\t\tentry.callback(event.entityA, event.entityB, ecs);\n\t\t}\n\t};\n}\n\n// ==================== Dependency Types ====================\n\n// ==================== Module-level Collision Callback ====================\n\ninterface CollisionEventBus<L extends string> {\n\tpublish(event: 'collision', data: CollisionEvent<L>): void;\n}\n\nfunction onCollisionDetected<L extends string>(\n\ta: BaseColliderInfo<L>,\n\tb: BaseColliderInfo<L>,\n\tcontact: Contact,\n\teventBus: CollisionEventBus<L>,\n): void {\n\teventBus.publish('collision', {\n\t\tentityA: a.entityId,\n\t\tentityB: b.entityId,\n\t\tlayerA: a.layer,\n\t\tlayerB: b.layer,\n\t\tnormalX: contact.normalX,\n\t\tnormalY: contact.normalY,\n\t\tdepth: contact.depth,\n\t});\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a collision plugin for ECSpresso.\n *\n * This plugin provides:\n * - Collision detection between entities with colliders\n * - AABB-AABB, circle-circle, and AABB-circle collision\n * - Layer-based filtering for collision pairs\n * - Deduplication of A-B / B-A collisions\n * - Automatic broadphase acceleration when spatialIndex resource is present\n *\n * Uses worldTransform for position (world-space collision detection).\n * The `layers` parameter is required for type inference — at runtime the\n * plugin does not consume it.\n *\n * @example\n * ```typescript\n * const layers = defineCollisionLayers({ player: ['enemy'], enemy: ['player'] });\n * const ecs = ECSpresso\n * .create()\n * .withPlugin(createTransformPlugin())\n * .withPlugin(createCollisionPlugin({ layers }))\n * .build();\n *\n * // Entity with collision\n * ecs.spawn({\n * ...createTransform(100, 200),\n * ...createAABBCollider(50, 30),\n * ...layers.player(),\n * });\n * ```\n */\nexport function createCollisionPlugin<L extends string, G extends string = 'physics'>(\n\toptions: CollisionPluginOptions<G> & { layers: LayerFactories<Record<L, readonly string[]>> }\n) {\n\tconst {\n\t\tsystemGroup = 'physics',\n\t\tpriority = 0,\n\t\tphase = 'postUpdate',\n\t} = options;\n\n\treturn definePlugin('collision')\n\t\t.withComponentTypes<CollisionComponentTypes<L>>()\n\t\t.withEventTypes<CollisionEventTypes<L>>()\n\t\t.withLabels<'collision-detection'>()\n\t\t.withGroups<G>()\n\t\t.requires<TransformWorldConfig>()\n\t\t.install((world) => {\n\t\t\t// Grow-only pool of BaseColliderInfo slots reused across frames.\n\t\t\t// Steady-state: zero allocations per frame once the pool is warm.\n\t\t\tconst colliderPool: BaseColliderInfo<L>[] = [];\n\t\t\t// Reusable entityId → collider lookup for the broadphase path.\n\t\t\tconst broadphaseMap = new Map<number, BaseColliderInfo<L>>();\n\n\t\t\tworld\n\t\t\t\t.addSystem('collision-detection')\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('collidables', {\n\t\t\t\t\twith: ['worldTransform', 'collisionLayer'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, ecs }) => {\n\t\t\t\t\tlet count = 0;\n\n\t\t\t\t\tfor (const entity of queries.collidables) {\n\t\t\t\t\t\tconst { worldTransform, collisionLayer } = entity.components;\n\t\t\t\t\t\tconst aabb = ecs.getComponent(entity.id, 'aabbCollider');\n\t\t\t\t\t\tconst circle = ecs.getComponent(entity.id, 'circleCollider');\n\t\t\t\t\t\tif (!aabb && !circle) continue;\n\n\t\t\t\t\t\tlet slot = colliderPool[count];\n\t\t\t\t\t\tif (!slot) {\n\t\t\t\t\t\t\tslot = {\n\t\t\t\t\t\t\t\tentityId: entity.id,\n\t\t\t\t\t\t\t\tx: worldTransform.x,\n\t\t\t\t\t\t\t\ty: worldTransform.y,\n\t\t\t\t\t\t\t\tlayer: collisionLayer.layer,\n\t\t\t\t\t\t\t\tcollidesWith: collisionLayer.collidesWith,\n\t\t\t\t\t\t\t\tshape: AABB_SHAPE,\n\t\t\t\t\t\t\t\thalfWidth: 0,\n\t\t\t\t\t\t\t\thalfHeight: 0,\n\t\t\t\t\t\t\t\tradius: 0,\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\tcolliderPool[count] = slot;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (!fillBaseColliderInfo(\n\t\t\t\t\t\t\tslot,\n\t\t\t\t\t\t\tentity.id, worldTransform.x, worldTransform.y,\n\t\t\t\t\t\t\tcollisionLayer.layer, collisionLayer.collidesWith,\n\t\t\t\t\t\t\taabb, circle,\n\t\t\t\t\t\t)) continue;\n\n\t\t\t\t\t\tcount++;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst si = tryGetSpatialIndex(ecs.tryGetResource.bind(ecs));\n\t\t\t\t\tdetectCollisions(colliderPool, count, broadphaseMap, si, onCollisionDetected<L>, ecs.eventBus);\n\t\t\t\t});\n\t\t});\n}\n\n",
|
|
5
|
+
"/**\n * Collision Plugin for ECSpresso\n *\n * Provides layer-based collision detection with events.\n * Uses worldTransform for position (world-space collision).\n * Supports AABB and circle colliders.\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type { TransformWorldConfig } from '../spatial/transform';\nimport { fillBaseColliderInfo, detectCollisions, tryGetSpatialIndex, AABB_SHAPE, type Contact, type BaseColliderInfo } from '../../utils/narrowphase';\n\n// ==================== Component Types ====================\n\n/**\n * Axis-Aligned Bounding Box collider.\n */\nexport interface AABBCollider {\n\t/** Width of the bounding box */\n\twidth: number;\n\t/** Height of the bounding box */\n\theight: number;\n\t/** X offset from entity position (default: 0) */\n\toffsetX?: number;\n\t/** Y offset from entity position (default: 0) */\n\toffsetY?: number;\n}\n\n/**\n * Circle collider.\n */\nexport interface CircleCollider {\n\t/** Radius of the circle */\n\tradius: number;\n\t/** X offset from entity position (default: 0) */\n\toffsetX?: number;\n\t/** Y offset from entity position (default: 0) */\n\toffsetY?: number;\n}\n\n/**\n * Collision layer configuration.\n */\nexport interface CollisionLayer<L extends string = never> {\n\t/** The layer this entity belongs to */\n\tlayer: L;\n\t/** Layers this entity can collide with */\n\tcollidesWith: readonly L[];\n}\n\n/**\n * Component types provided by the collision plugin.\n * Included automatically via `.withPlugin(createCollisionPlugin())`.\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createCollisionPlugin())\n * .withComponentTypes<{ sprite: Sprite; enemy: boolean }>()\n * .build();\n * ```\n */\nexport interface CollisionComponentTypes<L extends string = never> {\n\taabbCollider: AABBCollider;\n\tcircleCollider: CircleCollider;\n\tcollisionLayer: CollisionLayer<L>;\n}\n\n// ==================== Event Types ====================\n\n/**\n * Event fired when two entities collide.\n *\n * Normal components are flattened (`normalX`/`normalY`) rather than nested\n * in a sub-object to avoid a per-event allocation in the collision hot path.\n */\nexport interface CollisionEvent<L extends string = never> {\n\t/** First entity in the collision */\n\tentityA: number;\n\t/** Second entity in the collision */\n\tentityB: number;\n\t/** Layer of the first entity */\n\tlayerA: L;\n\t/** Layer of the second entity */\n\tlayerB: L;\n\t/** Contact normal X, pointing from entityA toward entityB */\n\tnormalX: number;\n\t/** Contact normal Y, pointing from entityA toward entityB */\n\tnormalY: number;\n\t/** Penetration depth (positive = overlapping) */\n\tdepth: number;\n}\n\n/**\n * Event types provided by the collision plugin.\n */\nexport interface CollisionEventTypes<L extends string = never> {\n\tcollision: CollisionEvent<L>;\n}\n\n// ==================== Plugin Options ====================\n\n/**\n * Configuration options for the collision plugin.\n */\nexport interface CollisionPluginOptions<G extends string = 'physics'> extends BasePluginOptions<G> {\n\t/** Name of the collision event (default: 'collision') */\n\tcollisionEventName?: string;\n}\n\n// ==================== Helper Functions ====================\n\n/**\n * Create an AABB collider component.\n *\n * @param width Width of the bounding box\n * @param height Height of the bounding box\n * @param offsetX X offset from entity position\n * @param offsetY Y offset from entity position\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createTransform(100, 200),\n * ...createAABBCollider(50, 30),\n * });\n * ```\n */\nexport function createAABBCollider(\n\twidth: number,\n\theight: number,\n\toffsetX?: number,\n\toffsetY?: number\n): { aabbCollider: AABBCollider } {\n\tconst collider: AABBCollider = { width, height };\n\tif (offsetX !== undefined) collider.offsetX = offsetX;\n\tif (offsetY !== undefined) collider.offsetY = offsetY;\n\treturn { aabbCollider: collider };\n}\n\n/**\n * Create a circle collider component.\n *\n * @param radius Radius of the circle\n * @param offsetX X offset from entity position\n * @param offsetY Y offset from entity position\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createTransform(100, 200),\n * ...createCircleCollider(25),\n * });\n * ```\n */\nexport function createCircleCollider(\n\tradius: number,\n\toffsetX?: number,\n\toffsetY?: number\n): { circleCollider: CircleCollider } {\n\tconst collider: CircleCollider = { radius };\n\tif (offsetX !== undefined) collider.offsetX = offsetX;\n\tif (offsetY !== undefined) collider.offsetY = offsetY;\n\treturn { circleCollider: collider };\n}\n\n/**\n * Create a collision layer component.\n *\n * @param layer The layer this entity belongs to\n * @param collidesWith Layers this entity can collide with\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createTransform(100, 200),\n * ...createAABBCollider(50, 30),\n * ...createCollisionLayer('player', ['enemy', 'obstacle']),\n * });\n * ```\n */\nexport function createCollisionLayer<L extends string>(\n\tlayer: L,\n\tcollidesWith: readonly L[]\n): Pick<CollisionComponentTypes<L>, 'collisionLayer'> {\n\treturn {\n\t\tcollisionLayer: { layer, collidesWith },\n\t};\n}\n\n/**\n * Layer factory result from defineCollisionLayers.\n */\nexport type LayerFactories<T extends Record<string, readonly string[]>> = {\n\t[K in keyof T]: () => Pick<CollisionComponentTypes<Extract<keyof T, string>>, 'collisionLayer'>;\n};\n\n/**\n * Extract layer names from a `defineCollisionLayers` result for use with\n * `createCollisionPairHandler`'s `L` type parameter.\n *\n * @example\n * ```typescript\n * const layers = defineCollisionLayers({ player: ['enemy'], enemy: ['player'] });\n * type Layer = LayersOf<typeof layers>;\n * const handler = createCollisionPairHandler<ECS, Layer>({\n * 'player:enemy': (playerId, enemyId, ecs) => { ... },\n * });\n * ```\n */\nexport type LayersOf<T> = Extract<keyof T, string>;\n\n/**\n * Define collision layer relationships and get factory functions.\n *\n * @param rules Object mapping layer names to arrays of layers they collide with\n * @returns Object with factory functions for each layer\n *\n * @example\n * ```typescript\n * const layers = defineCollisionLayers({\n * player: ['enemy', 'enemyProjectile'],\n * playerProjectile: ['enemy'],\n * enemy: ['playerProjectile'],\n * enemyProjectile: ['player'],\n * });\n *\n * // Usage\n * ecs.spawn({\n * ...createTransform(100, 200),\n * ...createAABBCollider(50, 30),\n * ...layers.player(),\n * });\n * ```\n */\n/**\n * Validates that all `collidesWith` values reference actual layer keys.\n * Catches typos at compile time.\n */\ntype ValidateCollidesWith<T> = {\n\t[K in keyof T]: T[K] extends readonly (infer V)[]\n\t\t? [V] extends [Extract<keyof T, string>] ? T[K] : readonly Extract<keyof T, string>[]\n\t\t: never;\n};\n\nexport function defineCollisionLayers<const T extends Record<string, readonly string[]>>(\n\trules: T & ValidateCollidesWith<T>\n): LayerFactories<T> {\n\ttype L = Extract<keyof T, string>;\n\tconst factories = {} as LayerFactories<T>;\n\n\tfor (const layer of Object.keys(rules) as Array<L>) {\n\t\tconst collidesWith = rules[layer] as readonly L[];\n\t\tfactories[layer] = () => createCollisionLayer<L>(layer, collidesWith);\n\t}\n\n\treturn factories;\n}\n\n// ==================== Collision Pair Handler ====================\n\n/**\n * Callback for a collision pair handler.\n *\n * @param firstEntityId Entity belonging to the first layer in the pair key\n * @param secondEntityId Entity belonging to the second layer in the pair key\n * @param ecs The ECS world instance (passed through from the subscriber)\n */\nexport type CollisionPairCallback<W = unknown> = (\n\tfirstEntityId: number,\n\tsecondEntityId: number,\n\tecs: W,\n) => void;\n\ninterface PairEntry<W> {\n\tcallback: CollisionPairCallback<W>;\n\tswapped: boolean;\n}\n\nfunction parsePairKey(key: string): [string, string] {\n\tconst colonIndex = key.indexOf(':');\n\tif (colonIndex === -1) {\n\t\tthrow new Error(`Invalid collision pair key \"${key}\": must contain a colon separator (e.g. \"player:enemy\")`);\n\t}\n\tconst layerA = key.slice(0, colonIndex);\n\tconst layerB = key.slice(colonIndex + 1);\n\tif (layerA === '' || layerB === '') {\n\t\tthrow new Error(`Invalid collision pair key \"${key}\": layer names must not be empty`);\n\t}\n\treturn [layerA, layerB];\n}\n\n/**\n * Create a collision pair handler that routes collision events to\n * layer-pair-specific callbacks.\n *\n * Registering `\"a:b\"` automatically handles both `(layerA=a, layerB=b)` and\n * `(layerA=b, layerB=a)`. Entity arguments are swapped to match the declared\n * key order. If both `\"a:b\"` and `\"b:a\"` are explicitly registered, each gets\n * its own handler with no implicit reverse.\n *\n * @typeParam W - The ECS world type (e.g. `ECSpresso<C, E, R>`). Defaults to `unknown`.\n * @typeParam L - Union of valid layer names. Defaults to `string`.\n * Provide specific layer names for compile-time key validation:\n * `createCollisionPairHandler<ECS, keyof typeof layers>({...})`\n *\n * @param pairs Object mapping `\"layerA:layerB\"` keys to callbacks\n * @returns A dispatch function to call with collision event data and ECS instance\n *\n * @example\n * ```typescript\n * // Basic usage:\n * const handler = createCollisionPairHandler<ECS>({\n * 'playerProjectile:enemy': (projectileId, enemyId, ecs) => {\n * ecs.commands.removeEntity(projectileId);\n * },\n * });\n *\n * // With layer name validation:\n * const layers = defineCollisionLayers({ player: ['enemy'], enemy: ['player'] });\n * type Layer = LayersOf<typeof layers>;\n * const handler = createCollisionPairHandler<ECS, Layer>({\n * 'player:enemy': (playerId, enemyId, ecs) => { ... },\n * });\n *\n * ecs.eventBus.subscribe('collision', (data) => handler({ data, ecs }));\n * ```\n */\nexport function createCollisionPairHandler<W = unknown, L extends string = string>(\n\tpairs: { [K in `${L}:${L}`]?: CollisionPairCallback<W> }\n): (ctx: { data: CollisionEvent<L>; ecs: W }) => void;\nexport function createCollisionPairHandler<W = unknown>(\n\tpairs: Record<string, CollisionPairCallback<W> | undefined>\n): (ctx: { data: CollisionEvent<string>; ecs: W }) => void {\n\tconst lookup = new Map<string, PairEntry<W>>();\n\tconst explicitKeys = new Set<string>();\n\n\t// First pass: collect all explicit keys\n\tfor (const key of Object.keys(pairs)) {\n\t\tparsePairKey(key); // validate\n\t\texplicitKeys.add(key);\n\t}\n\n\t// Second pass: build lookup with forward + conditional reverse entries\n\tfor (const key of Object.keys(pairs)) {\n\t\tconst [layerA, layerB] = parsePairKey(key);\n\t\tconst callback = pairs[key];\n\t\tif (!callback) continue;\n\n\t\t// Forward entry\n\t\tlookup.set(key, { callback, swapped: false });\n\n\t\t// Reverse entry (only if the reverse key wasn't explicitly registered\n\t\t// and it's not a self-collision where forward === reverse)\n\t\tconst reverseKey = `${layerB}:${layerA}`;\n\t\tif (reverseKey !== key && !explicitKeys.has(reverseKey)) {\n\t\t\tlookup.set(reverseKey, { callback, swapped: true });\n\t\t}\n\t}\n\n\treturn function collisionPairDispatch({ data: event, ecs }: { data: CollisionEvent<string>; ecs: W }): void {\n\t\tconst entry = lookup.get(event.layerA + ':' + event.layerB);\n\t\tif (!entry) return;\n\n\t\tif (entry.swapped) {\n\t\t\tentry.callback(event.entityB, event.entityA, ecs);\n\t\t} else {\n\t\t\tentry.callback(event.entityA, event.entityB, ecs);\n\t\t}\n\t};\n}\n\n// ==================== Dependency Types ====================\n\n// ==================== Module-level Collision Callback ====================\n\ninterface CollisionEventBus<L extends string> {\n\tpublish(event: 'collision', data: CollisionEvent<L>): void;\n}\n\nfunction onCollisionDetected<L extends string>(\n\ta: BaseColliderInfo<L>,\n\tb: BaseColliderInfo<L>,\n\tcontact: Contact,\n\teventBus: CollisionEventBus<L>,\n): void {\n\teventBus.publish('collision', {\n\t\tentityA: a.entityId,\n\t\tentityB: b.entityId,\n\t\tlayerA: a.layer,\n\t\tlayerB: b.layer,\n\t\tnormalX: contact.normalX,\n\t\tnormalY: contact.normalY,\n\t\tdepth: contact.depth,\n\t});\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a collision plugin for ECSpresso.\n *\n * This plugin provides:\n * - Collision detection between entities with colliders\n * - AABB-AABB, circle-circle, and AABB-circle collision\n * - Layer-based filtering for collision pairs\n * - Deduplication of A-B / B-A collisions\n * - Automatic broadphase acceleration when spatialIndex resource is present\n *\n * Uses worldTransform for position (world-space collision detection).\n * The `layers` parameter is required for type inference — at runtime the\n * plugin does not consume it.\n *\n * @example\n * ```typescript\n * const layers = defineCollisionLayers({ player: ['enemy'], enemy: ['player'] });\n * const ecs = ECSpresso\n * .create()\n * .withPlugin(createTransformPlugin())\n * .withPlugin(createCollisionPlugin({ layers }))\n * .build();\n *\n * // Entity with collision\n * ecs.spawn({\n * ...createTransform(100, 200),\n * ...createAABBCollider(50, 30),\n * ...layers.player(),\n * });\n * ```\n */\nexport function createCollisionPlugin<L extends string, G extends string = 'physics'>(\n\toptions: CollisionPluginOptions<G> & { layers: LayerFactories<Record<L, readonly string[]>> }\n) {\n\tconst {\n\t\tsystemGroup = 'physics',\n\t\tpriority = 0,\n\t\tphase = 'postUpdate',\n\t} = options;\n\n\treturn definePlugin('collision')\n\t\t.withComponentTypes<CollisionComponentTypes<L>>()\n\t\t.withEventTypes<CollisionEventTypes<L>>()\n\t\t.withLabels<'collision-detection'>()\n\t\t.withGroups<G>()\n\t\t.requires<TransformWorldConfig>()\n\t\t.install((world) => {\n\t\t\t// Grow-only pool of BaseColliderInfo slots reused across frames.\n\t\t\t// Steady-state: zero allocations per frame once the pool is warm.\n\t\t\tconst colliderPool: BaseColliderInfo<L>[] = [];\n\t\t\t// Reusable entityId → collider lookup for the broadphase path.\n\t\t\tconst broadphaseMap = new Map<number, BaseColliderInfo<L>>();\n\n\t\t\tworld\n\t\t\t\t.addSystem('collision-detection')\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('collidables', {\n\t\t\t\t\twith: ['worldTransform', 'collisionLayer'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, ecs }) => {\n\t\t\t\t\tlet count = 0;\n\n\t\t\t\t\tfor (const entity of queries.collidables) {\n\t\t\t\t\t\tconst { worldTransform, collisionLayer } = entity.components;\n\t\t\t\t\t\tconst aabb = ecs.getComponent(entity.id, 'aabbCollider');\n\t\t\t\t\t\tconst circle = ecs.getComponent(entity.id, 'circleCollider');\n\t\t\t\t\t\tif (!aabb && !circle) continue;\n\n\t\t\t\t\t\tlet slot = colliderPool[count];\n\t\t\t\t\t\tif (!slot) {\n\t\t\t\t\t\t\tslot = {\n\t\t\t\t\t\t\t\tentityId: entity.id,\n\t\t\t\t\t\t\t\tx: worldTransform.x,\n\t\t\t\t\t\t\t\ty: worldTransform.y,\n\t\t\t\t\t\t\t\tlayer: collisionLayer.layer,\n\t\t\t\t\t\t\t\tcollidesWith: collisionLayer.collidesWith,\n\t\t\t\t\t\t\t\tshape: AABB_SHAPE,\n\t\t\t\t\t\t\t\thalfWidth: 0,\n\t\t\t\t\t\t\t\thalfHeight: 0,\n\t\t\t\t\t\t\t\tradius: 0,\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\tcolliderPool[count] = slot;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (!fillBaseColliderInfo(\n\t\t\t\t\t\t\tslot,\n\t\t\t\t\t\t\tentity.id, worldTransform.x, worldTransform.y,\n\t\t\t\t\t\t\tcollisionLayer.layer, collisionLayer.collidesWith,\n\t\t\t\t\t\t\taabb, circle,\n\t\t\t\t\t\t)) continue;\n\n\t\t\t\t\t\tcount++;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst si = tryGetSpatialIndex(ecs.tryGetResource.bind(ecs));\n\t\t\t\t\tdetectCollisions(colliderPool, count, broadphaseMap, si, onCollisionDetected<L>, ecs.eventBus);\n\t\t\t\t});\n\t\t});\n}\n\n",
|
|
6
6
|
"/**\n * Shared Narrowphase Module\n *\n * Provides contact-computing narrowphase tests and a generic collision\n * iteration pipeline used by both the collision plugin (event-only) and\n * the physics2D plugin (impulse response).\n */\n\nimport type { SpatialIndex } from './spatial-hash';\n\n// ==================== Contact ====================\n\n/**\n * Contact result from a narrowphase test. Normal points from A toward B.\n *\n * Narrowphase functions use this as an out-parameter: the caller owns the\n * struct, the function writes fields in place and returns `true` on hit.\n * The `onContact` callback in `detectCollisions` receives a shared module-\n * level instance — **subscribers must consume it synchronously and must not\n * retain the reference across frames**.\n */\nexport interface Contact {\n\tnormalX: number;\n\tnormalY: number;\n\t/** Penetration depth (positive = overlapping) */\n\tdepth: number;\n}\n\n/**\n * Module-level reusable Contact passed down from `detectCollisions` into\n * narrowphase tests and forwarded to the `onContact` callback. Reused across\n * every pair in every frame — zero allocation in the narrowphase hot path.\n */\nconst _sharedContact: Contact = { normalX: 0, normalY: 0, depth: 0 };\n\n// ==================== BaseColliderInfo ====================\n\n/** Collider shape discriminator for the flattened BaseColliderInfo layout. */\nexport const AABB_SHAPE = 0;\nexport const CIRCLE_SHAPE = 1;\nexport type ColliderShape = typeof AABB_SHAPE | typeof CIRCLE_SHAPE;\n\n/**\n * Minimum collider data shared by collision and physics bundles.\n *\n * Flat layout (no nested `aabb` / `circle` sub-objects): the `shape`\n * discriminator tells you whether to read `halfWidth`/`halfHeight`\n * (AABB) or `radius` (Circle). Unused fields are set to 0.\n *\n * This shape is pool-friendly — all fields are assigned in place each\n * frame without allocating nested objects.\n */\nexport interface BaseColliderInfo<L extends string = string> {\n\tentityId: number;\n\tx: number;\n\ty: number;\n\tlayer: L;\n\tcollidesWith: readonly L[];\n\tshape: ColliderShape;\n\thalfWidth: number;\n\thalfHeight: number;\n\tradius: number;\n}\n\n// ==================== Collider Construction ====================\n\n/**\n * Populate a `BaseColliderInfo` slot in place from raw component data.\n * Returns `true` if the slot was filled, `false` if the entity has no\n * collider (caller should skip it).\n *\n * If an entity has both AABB and circle colliders, AABB wins and only\n * the AABB offset is applied. This matches the dispatch precedence in\n * `computeContact`; the previous implementation stacked both offsets,\n * which was a bug.\n */\nexport function fillBaseColliderInfo<L extends string>(\n\tinfo: BaseColliderInfo<L>,\n\tentityId: number,\n\tx: number,\n\ty: number,\n\tlayer: L,\n\tcollidesWith: readonly L[],\n\taabb: { width: number; height: number; offsetX?: number; offsetY?: number } | undefined,\n\tcircle: { radius: number; offsetX?: number; offsetY?: number } | undefined,\n): boolean {\n\tinfo.entityId = entityId;\n\tinfo.layer = layer;\n\tinfo.collidesWith = collidesWith;\n\n\tif (aabb) {\n\t\tinfo.x = x + (aabb.offsetX ?? 0);\n\t\tinfo.y = y + (aabb.offsetY ?? 0);\n\t\tinfo.shape = AABB_SHAPE;\n\t\tinfo.halfWidth = aabb.width / 2;\n\t\tinfo.halfHeight = aabb.height / 2;\n\t\tinfo.radius = 0;\n\t\treturn true;\n\t}\n\n\tif (circle) {\n\t\tinfo.x = x + (circle.offsetX ?? 0);\n\t\tinfo.y = y + (circle.offsetY ?? 0);\n\t\tinfo.shape = CIRCLE_SHAPE;\n\t\tinfo.halfWidth = 0;\n\t\tinfo.halfHeight = 0;\n\t\tinfo.radius = circle.radius;\n\t\treturn true;\n\t}\n\n\treturn false;\n}\n\n// ==================== Spatial Index Lookup ====================\n\n/**\n * Retrieve the optional spatialIndex resource, returning undefined when absent.\n * Centralizes the cross-plugin typed lookup so individual plugins don't each\n * need to import SpatialIndex or repeat the tryGetResource pattern.\n */\nexport function tryGetSpatialIndex(\n\ttryGetResource: <T>(key: string) => T | undefined,\n): SpatialIndex | undefined {\n\treturn tryGetResource<SpatialIndex>('spatialIndex');\n}\n\n// ==================== Narrowphase Tests ====================\n\n/**\n * Write an AABB-AABB contact into `out`. Returns `true` if the shapes\n * overlap (out was filled), `false` otherwise.\n */\nexport function computeAABBvsAABB(\n\tax: number, ay: number, ahw: number, ahh: number,\n\tbx: number, by: number, bhw: number, bhh: number,\n\tout: Contact,\n): boolean {\n\tconst dx = bx - ax;\n\tconst dy = by - ay;\n\tconst overlapX = (ahw + bhw) - Math.abs(dx);\n\tconst overlapY = (ahh + bhh) - Math.abs(dy);\n\n\tif (overlapX <= 0 || overlapY <= 0) return false;\n\n\tif (overlapX < overlapY) {\n\t\tout.normalX = dx >= 0 ? 1 : -1;\n\t\tout.normalY = 0;\n\t\tout.depth = overlapX;\n\t\treturn true;\n\t}\n\tout.normalX = 0;\n\tout.normalY = dy >= 0 ? 1 : -1;\n\tout.depth = overlapY;\n\treturn true;\n}\n\nexport function computeCircleVsCircle(\n\tax: number, ay: number, ar: number,\n\tbx: number, by: number, br: number,\n\tout: Contact,\n): boolean {\n\tconst dx = bx - ax;\n\tconst dy = by - ay;\n\tconst distSq = dx * dx + dy * dy;\n\tconst radiusSum = ar + br;\n\n\tif (distSq >= radiusSum * radiusSum) return false;\n\n\tconst dist = Math.sqrt(distSq);\n\tif (dist === 0) {\n\t\tout.normalX = 1;\n\t\tout.normalY = 0;\n\t\tout.depth = radiusSum;\n\t\treturn true;\n\t}\n\tout.normalX = dx / dist;\n\tout.normalY = dy / dist;\n\tout.depth = radiusSum - dist;\n\treturn true;\n}\n\nexport function computeAABBvsCircle(\n\taabbX: number, aabbY: number, ahw: number, ahh: number,\n\tcircleX: number, circleY: number, radius: number,\n\tout: Contact,\n): boolean {\n\tconst closestX = Math.max(aabbX - ahw, Math.min(circleX, aabbX + ahw));\n\tconst closestY = Math.max(aabbY - ahh, Math.min(circleY, aabbY + ahh));\n\n\tconst dx = circleX - closestX;\n\tconst dy = circleY - closestY;\n\tconst distSq = dx * dx + dy * dy;\n\n\tif (distSq >= radius * radius) return false;\n\n\t// Circle center inside AABB\n\tif (distSq === 0) {\n\t\tconst pushLeft = (circleX - (aabbX - ahw));\n\t\tconst pushRight = ((aabbX + ahw) - circleX);\n\t\tconst pushUp = (circleY - (aabbY - ahh));\n\t\tconst pushDown = ((aabbY + ahh) - circleY);\n\t\tconst minPush = Math.min(pushLeft, pushRight, pushUp, pushDown);\n\n\t\tif (minPush === pushRight) {\n\t\t\tout.normalX = 1; out.normalY = 0; out.depth = pushRight + radius;\n\t\t\treturn true;\n\t\t}\n\t\tif (minPush === pushLeft) {\n\t\t\tout.normalX = -1; out.normalY = 0; out.depth = pushLeft + radius;\n\t\t\treturn true;\n\t\t}\n\t\tif (minPush === pushDown) {\n\t\t\tout.normalX = 0; out.normalY = 1; out.depth = pushDown + radius;\n\t\t\treturn true;\n\t\t}\n\t\tout.normalX = 0; out.normalY = -1; out.depth = pushUp + radius;\n\t\treturn true;\n\t}\n\n\tconst dist = Math.sqrt(distSq);\n\tout.normalX = dx / dist;\n\tout.normalY = dy / dist;\n\tout.depth = radius - dist;\n\treturn true;\n}\n\n// ==================== Contact Dispatcher ====================\n\n/**\n * Dispatch to the correct narrowphase function for the given pair and\n * write the contact into `out`. Returns `true` if the pair overlaps.\n */\nexport function computeContact(a: BaseColliderInfo, b: BaseColliderInfo, out: Contact): boolean {\n\tif (a.shape === AABB_SHAPE && b.shape === AABB_SHAPE) {\n\t\treturn computeAABBvsAABB(\n\t\t\ta.x, a.y, a.halfWidth, a.halfHeight,\n\t\t\tb.x, b.y, b.halfWidth, b.halfHeight,\n\t\t\tout,\n\t\t);\n\t}\n\n\tif (a.shape === CIRCLE_SHAPE && b.shape === CIRCLE_SHAPE) {\n\t\treturn computeCircleVsCircle(\n\t\t\ta.x, a.y, a.radius,\n\t\t\tb.x, b.y, b.radius,\n\t\t\tout,\n\t\t);\n\t}\n\n\tif (a.shape === AABB_SHAPE && b.shape === CIRCLE_SHAPE) {\n\t\treturn computeAABBvsCircle(\n\t\t\ta.x, a.y, a.halfWidth, a.halfHeight,\n\t\t\tb.x, b.y, b.radius,\n\t\t\tout,\n\t\t);\n\t}\n\n\t// a is Circle, b is AABB — compute as AABB-vs-Circle, then flip normal in place\n\tif (!computeAABBvsCircle(\n\t\tb.x, b.y, b.halfWidth, b.halfHeight,\n\t\ta.x, a.y, a.radius,\n\t\tout,\n\t)) return false;\n\tout.normalX = -out.normalX;\n\tout.normalY = -out.normalY;\n\treturn true;\n}\n\n// ==================== Collision Iteration ====================\n\n/** Module-level reusable set for broadphase candidates. */\nconst _broadphaseCandidates = new Set<number>();\n\nlet _bruteForceWarned = false;\nconst BRUTE_FORCE_WARN_THRESHOLD = 50;\n\n/**\n * Generic collision detection pipeline: brute-force or broadphase,\n * with layer filtering and contact computation.\n *\n * `count` is the number of live entries at the front of `colliders`.\n * The array itself may be a grow-only pool — only indices `[0, count)`\n * are iterated, so trailing pool slots are ignored.\n *\n * `workingMap` is a caller-owned `Map<number, I>` used by the broadphase\n * path as an entityId → collider lookup. It is cleared and repopulated on\n * each call; callers should allocate it once and pass the same instance\n * every frame. Unused by the brute-force path but still required so that\n * typed reuse is the default, not an opt-in.\n *\n * Uses a context parameter forwarded to the callback to avoid\n * per-frame closure allocation.\n */\nexport function detectCollisions<I extends BaseColliderInfo, C>(\n\tcolliders: I[],\n\tcount: number,\n\tworkingMap: Map<number, I>,\n\tspatialIndex: SpatialIndex | undefined,\n\tonContact: (a: I, b: I, contact: Contact, context: C) => void,\n\tcontext: C,\n): void {\n\tif (spatialIndex) {\n\t\tbroadphaseDetect(colliders, count, workingMap, spatialIndex, onContact, context);\n\t} else {\n\t\tbruteForceDetect(colliders, count, onContact, context);\n\t}\n}\n\nfunction bruteForceDetect<I extends BaseColliderInfo, C>(\n\tcolliders: I[],\n\tcount: number,\n\tonContact: (a: I, b: I, contact: Contact, context: C) => void,\n\tcontext: C,\n): void {\n\tif (!_bruteForceWarned && count >= BRUTE_FORCE_WARN_THRESHOLD) {\n\t\t_bruteForceWarned = true;\n\t\tconsole.warn(\n\t\t\t`[ecspresso] Collision detection is using O(n²) brute force with ${count} colliders. ` +\n\t\t\t`For better performance, install createSpatialIndexPlugin() alongside your collision or physics2D plugin.`,\n\t\t);\n\t}\n\n\tfor (let i = 0; i < count; i++) {\n\t\tconst a = colliders[i];\n\t\tif (!a) continue;\n\n\t\tfor (let j = i + 1; j < count; j++) {\n\t\t\tconst b = colliders[j];\n\t\t\tif (!b) continue;\n\n\t\t\tif (!a.collidesWith.includes(b.layer) && !b.collidesWith.includes(a.layer)) continue;\n\n\t\t\tif (!computeContact(a, b, _sharedContact)) continue;\n\n\t\t\tonContact(a, b, _sharedContact, context);\n\t\t}\n\t}\n}\n\nfunction broadphaseDetect<I extends BaseColliderInfo, C>(\n\tcolliders: I[],\n\tcount: number,\n\tcolliderMap: Map<number, I>,\n\tspatialIndex: SpatialIndex,\n\tonContact: (a: I, b: I, contact: Contact, context: C) => void,\n\tcontext: C,\n): void {\n\tcolliderMap.clear();\n\tfor (let i = 0; i < count; i++) {\n\t\tconst c = colliders[i];\n\t\tif (!c) continue;\n\t\tcolliderMap.set(c.entityId, c);\n\t}\n\n\tfor (let i = 0; i < count; i++) {\n\t\tconst a = colliders[i];\n\t\tif (!a) continue;\n\n\t\tconst aHalfW = a.shape === AABB_SHAPE ? a.halfWidth : a.radius;\n\t\tconst aHalfH = a.shape === AABB_SHAPE ? a.halfHeight : a.radius;\n\n\t\t_broadphaseCandidates.clear();\n\t\tspatialIndex.queryRectInto(\n\t\t\ta.x - aHalfW, a.y - aHalfH,\n\t\t\ta.x + aHalfW, a.y + aHalfH,\n\t\t\t_broadphaseCandidates,\n\t\t);\n\n\t\tfor (const bId of _broadphaseCandidates) {\n\t\t\tif (bId <= a.entityId) continue;\n\n\t\t\tconst b = colliderMap.get(bId);\n\t\t\tif (!b) continue;\n\n\t\t\tif (!a.collidesWith.includes(b.layer) && !b.collidesWith.includes(a.layer)) continue;\n\n\t\t\tif (!computeContact(a, b, _sharedContact)) continue;\n\n\t\t\tonContact(a, b, _sharedContact, context);\n\t\t}\n\t}\n}\n"
|
|
7
7
|
],
|
|
8
8
|
"mappings": "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",
|
|
9
|
-
"debugId": "
|
|
9
|
+
"debugId": "F04221DD4AA45FC664756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* the existing collision plugin can still run in postUpdate for game logic events.
|
|
10
10
|
*/
|
|
11
11
|
import type { SystemPhase } from 'ecspresso';
|
|
12
|
-
import type { TransformComponentTypes, TransformWorldConfig } from '
|
|
12
|
+
import type { TransformComponentTypes, TransformWorldConfig } from '../spatial/transform';
|
|
13
13
|
import type { CollisionComponentTypes, LayerFactories } from './collision';
|
|
14
14
|
import type { Vector2D } from 'ecspresso';
|
|
15
15
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
var y=((z)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(z,{get:(J,Q)=>(typeof require<"u"?require:J)[Q]}):z)(function(z){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+z+'" is not supported')});import{definePlugin as f}from"ecspresso";var _={normalX:0,normalY:0,depth:0},Y=0;function A(z,J,Q,j,Z,O,$,K){if(z.entityId=J,z.layer=Z,z.collidesWith=O,$)return z.x=Q+($.offsetX??0),z.y=j+($.offsetY??0),z.shape=0,z.halfWidth=$.width/2,z.halfHeight=$.height/2,z.radius=0,!0;if(K)return z.x=Q+(K.offsetX??0),z.y=j+(K.offsetY??0),z.shape=1,z.halfWidth=0,z.halfHeight=0,z.radius=K.radius,!0;return!1}function M(z){return z("spatialIndex")}function B(z,J,Q,j,Z,O,$,K,U){let D=Z-z,V=O-J,E=Q+$-Math.abs(D),F=j+K-Math.abs(V);if(E<=0||F<=0)return!1;if(E<F)return U.normalX=D>=0?1:-1,U.normalY=0,U.depth=E,!0;return U.normalX=0,U.normalY=V>=0?1:-1,U.depth=F,!0}function x(z,J,Q,j,Z,O,$){let K=j-z,U=Z-J,D=K*K+U*U,V=Q+O;if(D>=V*V)return!1;let E=Math.sqrt(D);if(E===0)return $.normalX=1,$.normalY=0,$.depth=V,!0;return $.normalX=K/E,$.normalY=U/E,$.depth=V-E,!0}function C(z,J,Q,j,Z,O,$,K){let U=Math.max(z-Q,Math.min(Z,z+Q)),D=Math.max(J-j,Math.min(O,J+j)),V=Z-U,E=O-D,F=V*V+E*E;if(F>=$*$)return!1;if(F===0){let H=Z-(z-Q),W=z+Q-Z,N=O-(J-j),T=J+j-O,q=Math.min(H,W,N,T);if(q===W)return K.normalX=1,K.normalY=0,K.depth=W+$,!0;if(q===H)return K.normalX=-1,K.normalY=0,K.depth=H+$,!0;if(q===T)return K.normalX=0,K.normalY=1,K.depth=T+$,!0;return K.normalX=0,K.normalY=-1,K.depth=N+$,!0}let G=Math.sqrt(F);return K.normalX=V/G,K.normalY=E/G,K.depth=$-G,!0}function w(z,J,Q){if(z.shape===0&&J.shape===0)return B(z.x,z.y,z.halfWidth,z.halfHeight,J.x,J.y,J.halfWidth,J.halfHeight,Q);if(z.shape===1&&J.shape===1)return x(z.x,z.y,z.radius,J.x,J.y,J.radius,Q);if(z.shape===0&&J.shape===1)return C(z.x,z.y,z.halfWidth,z.halfHeight,J.x,J.y,J.radius,Q);if(!C(J.x,J.y,J.halfWidth,J.halfHeight,z.x,z.y,z.radius,Q))return!1;return Q.normalX=-Q.normalX,Q.normalY=-Q.normalY,!0}var S=new Set,X=!1,m=50;function I(z,J,Q,j,Z,O){if(j)v(z,J,Q,j,Z,O);else g(z,J,Z,O)}function g(z,J,Q,j){if(!X&&J>=m)X=!0,console.warn(`[ecspresso] Collision detection is using O(n²) brute force with ${J} colliders. For better performance, install createSpatialIndexPlugin() alongside your collision or physics2D plugin.`);for(let Z=0;Z<J;Z++){let O=z[Z];if(!O)continue;for(let $=Z+1;$<J;$++){let K=z[$];if(!K)continue;if(!O.collidesWith.includes(K.layer)&&!K.collidesWith.includes(O.layer))continue;if(!w(O,K,_))continue;Q(O,K,_,j)}}}function v(z,J,Q,j,Z,O){Q.clear();for(let $=0;$<J;$++){let K=z[$];if(!K)continue;Q.set(K.entityId,K)}for(let $=0;$<J;$++){let K=z[$];if(!K)continue;let U=K.shape===0?K.halfWidth:K.radius,D=K.shape===0?K.halfHeight:K.radius;S.clear(),j.queryRectInto(K.x-U,K.y-D,K.x+U,K.y+D,S);for(let V of S){if(V<=K.entityId)continue;let E=Q.get(V);if(!E)continue;if(!K.collidesWith.includes(E.layer)&&!E.collidesWith.includes(K.layer))continue;if(!w(K,E,_))continue;Z(K,E,_,O)}}}function l(z,J){return{rigidBody:{type:z,mass:z==="static"?1/0:J?.mass??1,drag:J?.drag??0,restitution:J?.restitution??0,friction:J?.friction??0,gravityScale:J?.gravityScale??1},force:{x:0,y:0}}}function s(z,J){return{force:{x:z,y:J}}}function n(z,J,Q,j){let Z=z.getComponent(J,"force");if(!Z)return;Z.x+=Q,Z.y+=j}function i(z,J,Q,j){let Z=z.getComponent(J,"velocity"),O=z.getComponent(J,"rigidBody");if(!Z||!O)return;if(O.mass===1/0||O.mass===0)return;Z.x+=Q/O.mass,Z.y+=j/O.mass}function c(z,J,Q,j){let Z=z.getComponent(J,"velocity");if(!Z)return;Z.x=Q,Z.y=j}function h(z,J,Q,j){let Z=z.rigidBody.type==="dynamic"&&z.rigidBody.mass>0&&z.rigidBody.mass!==1/0?1/z.rigidBody.mass:0,O=J.rigidBody.type==="dynamic"&&J.rigidBody.mass>0&&J.rigidBody.mass!==1/0?1/J.rigidBody.mass:0,$=Z+O;if($>0){let K=Q.depth/$;if(Z>0){let E=j.getComponent(z.entityId,"localTransform");if(!E)return;E.x-=K*Z*Q.normalX,E.y-=K*Z*Q.normalY,z.x=E.x,j.markChanged(z.entityId,"localTransform")}if(O>0){let E=j.getComponent(J.entityId,"localTransform");if(!E)return;E.x+=K*O*Q.normalX,E.y+=K*O*Q.normalY,j.markChanged(J.entityId,"localTransform")}let U=J.velocity.x-z.velocity.x,D=J.velocity.y-z.velocity.y,V=U*Q.normalX+D*Q.normalY;if(V<0){let F=-(1+Math.min(z.rigidBody.restitution,J.rigidBody.restitution))*V/$;z.velocity.x-=F*Z*Q.normalX,z.velocity.y-=F*Z*Q.normalY,J.velocity.x+=F*O*Q.normalX,J.velocity.y+=F*O*Q.normalY;let G=U-V*Q.normalX,H=D-V*Q.normalY,W=Math.sqrt(G*G+H*H);if(W>0.000001){let N=G/W,T=H/W,L=Math.sqrt(z.rigidBody.friction*J.rigidBody.friction)*Math.abs(F),k=Math.min(W/$,L);z.velocity.x+=k*Z*N,z.velocity.y+=k*Z*T,J.velocity.x-=k*O*N,J.velocity.y-=k*O*T}}j.markChanged(z.entityId,"velocity"),j.markChanged(J.entityId,"velocity")}j.eventBus.publish("physicsCollision",{entityA:z.entityId,entityB:J.entityId,normalX:Q.normalX,normalY:Q.normalY,depth:Q.depth})}function p(z,J,Q,j){h(z,J,Q,j)}function e(z){let{gravity:J={x:0,y:0},systemGroup:Q="physics2D",collisionSystemGroup:j,integrationPriority:Z=1000,collisionPriority:O=900,phase:$="fixedUpdate"}=z??{};return f("physics2D").withComponentTypes().withEventTypes().withResourceTypes().withLabels().withGroups().requires().install((K)=>{K.registerRequired("rigidBody","velocity",()=>({x:0,y:0})),K.registerRequired("rigidBody","force",()=>({x:0,y:0})),K.addResource("physicsConfig",{gravity:{x:J.x,y:J.y}}),K.addSystem("physics2D-integration").setPriority(Z).inPhase($).inGroup(Q).addQuery("bodies",{with:["localTransform","velocity","rigidBody","force"]}).setProcess(({queries:E,dt:F,ecs:G})=>{let H=G.getResource("physicsConfig"),W=H.gravity.x,N=H.gravity.y;for(let T of E.bodies){let{localTransform:q,velocity:L,rigidBody:k,force:P}=T.components;if(k.type==="static")continue;if(k.type==="dynamic"){if(L.x+=W*k.gravityScale*F,L.y+=N*k.gravityScale*F,k.mass>0&&k.mass!==1/0)L.x+=P.x/k.mass*F,L.y+=P.y/k.mass*F;if(k.drag>0){let R=Math.max(0,1-k.drag*F);L.x*=R,L.y*=R}}q.x+=L.x*F,q.y+=L.y*F,P.x=0,P.y=0,G.markChanged(T.id,"localTransform")}});let U=K.addSystem("physics2D-collision").setPriority(O).inPhase($).inGroup(Q);if(j)U.inGroup(j);let D=[],V=new Map;U.addQuery("collidables",{with:["localTransform","rigidBody","velocity","collisionLayer"]}).setProcess(({queries:E,ecs:F})=>{let G=0;for(let W of E.collidables){let{localTransform:N,rigidBody:T,velocity:q,collisionLayer:L}=W.components,k=F.getComponent(W.id,"aabbCollider"),P=F.getComponent(W.id,"circleCollider");if(!k&&!P)continue;let R=D[G];if(!R)R={entityId:W.id,x:N.x,y:N.y,layer:L.layer,collidesWith:L.collidesWith,shape:Y,halfWidth:0,halfHeight:0,radius:0,rigidBody:T,velocity:q},D[G]=R;else R.rigidBody=T,R.velocity=q;if(!A(R,W.id,N.x,N.y,L.layer,L.collidesWith,k,P))continue;G++}let H=M(F.tryGetResource.bind(F));I(D,G,V,H,p,F)})})}export{c as setVelocity,l as createRigidBody,e as createPhysics2DPlugin,s as createForce,i as applyImpulse,n as applyForce};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=B8850A0A0D8A257364756E2164756E21
|
|
4
4
|
//# sourceMappingURL=physics2D.js.map
|