ecspresso 0.13.1 → 0.13.4
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/bindings/react/index.d.ts +29 -0
- package/dist/ecspresso.d.ts +5 -0
- package/dist/entity-manager.d.ts +4 -0
- package/dist/hierarchy-manager.d.ts +7 -0
- package/dist/index.js +2 -2
- package/dist/index.js.map +7 -7
- package/dist/plugins/ai/detection.d.ts +17 -0
- package/dist/plugins/ai/detection.js +2 -2
- package/dist/plugins/ai/detection.js.map +3 -3
- package/dist/plugins/ai/flocking.d.ts +97 -0
- package/dist/plugins/ai/flocking.js +4 -0
- package/dist/plugins/ai/flocking.js.map +12 -0
- package/dist/plugins/input/selection.js +2 -2
- package/dist/plugins/input/selection.js.map +3 -3
- package/dist/plugins/isometric/projection.d.ts +19 -0
- package/dist/plugins/isometric/projection.js +2 -2
- package/dist/plugins/isometric/projection.js.map +3 -3
- package/dist/plugins/physics/collision.js +2 -2
- package/dist/plugins/physics/collision.js.map +3 -3
- package/dist/plugins/physics/physics2D.js +2 -2
- package/dist/plugins/physics/physics2D.js.map +3 -3
- package/dist/plugins/rendering/renderer2D.js +2 -2
- package/dist/plugins/rendering/renderer2D.js.map +3 -3
- package/dist/plugins/spatial/camera.d.ts +10 -1
- package/dist/plugins/spatial/camera.js +2 -2
- package/dist/plugins/spatial/camera.js.map +3 -3
- package/dist/plugins/spatial/transform.js +2 -2
- package/dist/plugins/spatial/transform.js.map +3 -3
- package/dist/resource-manager.d.ts +24 -0
- package/package.json +22 -10
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/plugins/spatial/camera.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * Camera / Viewport Plugin for ECSpresso\n *\n * Provides a declarative camera with world/screen coordinate conversion, smooth follow,\n * trauma-based shake, bounds clamping, cursor-centered zoom, and logical viewport dimensions.\n *\n * This plugin is renderer-agnostic. PixiJS or other renderer integration (applying\n * cameraState to a container/stage transform) is the consumer's responsibility.\n *\n * Camera uses its own x/y/zoom/rotation rather than localTransform/worldTransform.\n * It reads the target entity's worldTransform for follow, but doesn't participate\n * in the transform hierarchy itself.\n */\n\nimport { definePlugin } from 'ecspresso';\nimport type { SystemPhase } from 'ecspresso';\nimport type ECSpresso from 'ecspresso';\nimport type { WorldConfigFrom } from '../../type-utils';\nimport type { TransformWorldConfig } from './transform';\n\n// ==================== Component Types ====================\n\nexport interface Camera {\n\tx: number;\n\ty: number;\n\tzoom: number;\n\trotation: number;\n}\n\nexport interface CameraFollow {\n\ttarget: number;\n\tsmoothing: number;\n\tdeadzoneX: number;\n\tdeadzoneY: number;\n\toffsetX: number;\n\toffsetY: number;\n}\n\nexport interface CameraShake {\n\ttrauma: number;\n\ttraumaDecay: number;\n\tmaxOffsetX: number;\n\tmaxOffsetY: number;\n\tmaxRotation: number;\n}\n\nexport interface CameraBounds {\n\tminX: number;\n\tminY: number;\n\tmaxX: number;\n\tmaxY: number;\n}\n\nexport interface CameraComponentTypes {\n\tcamera: Camera;\n\tcameraFollow: CameraFollow;\n\tcameraShake: CameraShake;\n\tcameraBounds: CameraBounds;\n}\n\n// ==================== Resource Types ====================\n\nexport interface FollowOptions {\n\tsmoothing?: number;\n\tdeadzoneX?: number;\n\tdeadzoneY?: number;\n\toffsetX?: number;\n\toffsetY?: number;\n}\n\nexport type EntityHandle = { id: number };\n\nexport interface CameraState {\n\t// Read-only data (synced from camera entity each frame)\n\tx: number;\n\ty: number;\n\tzoom: number;\n\trotation: number;\n\tshakeOffsetX: number;\n\tshakeOffsetY: number;\n\tshakeRotation: number;\n\tviewportWidth: number;\n\tviewportHeight: number;\n\tentityId: number;\n\n\t// Mutation methods\n\tfollow(target: number | EntityHandle, options?: FollowOptions): void;\n\tunfollow(): void;\n\tsetPosition(x: number, y: number): void;\n\tsetZoom(zoom: number): void;\n\tsetRotation(rotation: number): void;\n\tsetBounds(minX: number, minY: number, maxX: number, maxY: number): void;\n\tclearBounds(): void;\n\taddTrauma(amount: number): void;\n}\n\nexport interface CameraResourceTypes {\n\tcameraState: CameraState;\n}\n\n// ==================== Plugin Options ====================\n\nexport interface CameraPluginOptions<G extends string = 'camera'> {\n\tviewportWidth?: number;\n\tviewportHeight?: number;\n\tinitial?: {\n\t\tx?: number;\n\t\ty?: number;\n\t\tzoom?: number;\n\t\trotation?: number;\n\t};\n\tfollow?: FollowOptions;\n\tshake?: boolean | Partial<Omit<CameraShake, 'trauma'>>;\n\tbounds?:\n\t\t| { minX: number; minY: number; maxX: number; maxY: number }\n\t\t| [number, number, number, number];\n\tzoom?: {\n\t\tzoomStep?: number;\n\t\tminZoom?: number;\n\t\tmaxZoom?: number;\n\t};\n\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"
|
|
5
|
+
"/**\n * Camera / Viewport Plugin for ECSpresso\n *\n * Provides a declarative camera with world/screen coordinate conversion, smooth follow,\n * trauma-based shake, bounds clamping, cursor-centered zoom, and logical viewport dimensions.\n *\n * This plugin is renderer-agnostic. PixiJS or other renderer integration (applying\n * cameraState to a container/stage transform) is the consumer's responsibility.\n *\n * Camera uses its own x/y/zoom/rotation rather than localTransform/worldTransform.\n * It reads the target entity's worldTransform for follow, but doesn't participate\n * in the transform hierarchy itself.\n */\n\nimport { definePlugin } from 'ecspresso';\nimport type { SystemPhase } from 'ecspresso';\nimport type ECSpresso from 'ecspresso';\nimport type { WorldConfigFrom } from '../../type-utils';\nimport type { TransformWorldConfig } from './transform';\n\n// ==================== Component Types ====================\n\nexport interface Camera {\n\tx: number;\n\ty: number;\n\tzoom: number;\n\trotation: number;\n}\n\nexport interface CameraFollow {\n\ttarget: number;\n\tsmoothing: number;\n\tdeadzoneX: number;\n\tdeadzoneY: number;\n\toffsetX: number;\n\toffsetY: number;\n}\n\nexport interface CameraShake {\n\ttrauma: number;\n\ttraumaDecay: number;\n\tmaxOffsetX: number;\n\tmaxOffsetY: number;\n\tmaxRotation: number;\n}\n\nexport interface CameraBounds {\n\tminX: number;\n\tminY: number;\n\tmaxX: number;\n\tmaxY: number;\n}\n\nexport interface CameraComponentTypes {\n\tcamera: Camera;\n\tcameraFollow: CameraFollow;\n\tcameraShake: CameraShake;\n\tcameraBounds: CameraBounds;\n}\n\n// ==================== Resource Types ====================\n\nexport interface FollowOptions {\n\tsmoothing?: number;\n\tdeadzoneX?: number;\n\tdeadzoneY?: number;\n\toffsetX?: number;\n\toffsetY?: number;\n}\n\nexport type EntityHandle = { id: number };\n\nexport interface CameraState {\n\t// Read-only data (synced from camera entity each frame)\n\tx: number;\n\ty: number;\n\tzoom: number;\n\trotation: number;\n\tshakeOffsetX: number;\n\tshakeOffsetY: number;\n\tshakeRotation: number;\n\tviewportWidth: number;\n\tviewportHeight: number;\n\tentityId: number;\n\n\t// Mutation methods\n\tfollow(target: number | EntityHandle, options?: FollowOptions): void;\n\tunfollow(): void;\n\tsetPosition(x: number, y: number): void;\n\tsetZoom(zoom: number): void;\n\tsetRotation(rotation: number): void;\n\tsetBounds(minX: number, minY: number, maxX: number, maxY: number): void;\n\tclearBounds(): void;\n\taddTrauma(amount: number): void;\n}\n\nexport interface CameraResourceTypes {\n\tcameraState: CameraState;\n}\n\n// ==================== Plugin Options ====================\n\nexport interface CameraPluginOptions<G extends string = 'camera'> {\n\tviewportWidth?: number;\n\tviewportHeight?: number;\n\tinitial?: {\n\t\tx?: number;\n\t\ty?: number;\n\t\tzoom?: number;\n\t\trotation?: number;\n\t};\n\tfollow?: FollowOptions;\n\tshake?: boolean | Partial<Omit<CameraShake, 'trauma'>>;\n\tbounds?:\n\t\t| { minX: number; minY: number; maxX: number; maxY: number }\n\t\t| [number, number, number, number];\n\tzoom?: {\n\t\tzoomStep?: number;\n\t\tminZoom?: number;\n\t\tmaxZoom?: number;\n\t};\n\tpan?: {\n\t\tspeed: number;\n\t\tactions?: {\n\t\t\tup?: string;\n\t\t\tdown?: string;\n\t\t\tleft?: string;\n\t\t\tright?: string;\n\t\t};\n\t};\n\tsystemGroup?: G;\n\tphase?: SystemPhase;\n\trandomFn?: () => number;\n}\n\n// ==================== Default Values ====================\n\nconst DEFAULT_SHAKE: Readonly<Omit<CameraShake, 'trauma'>> = {\n\ttraumaDecay: 1,\n\tmaxOffsetX: 10,\n\tmaxOffsetY: 10,\n\tmaxRotation: 0.05,\n};\n\nconst DEFAULT_FOLLOW: Readonly<Omit<CameraFollow, 'target'>> = {\n\tsmoothing: 5,\n\tdeadzoneX: 0,\n\tdeadzoneY: 0,\n\toffsetX: 0,\n\toffsetY: 0,\n};\n\n// ==================== Coordinate Conversion ====================\n\nexport function worldToScreen(\n\tworldX: number,\n\tworldY: number,\n\tstate: CameraState,\n): { x: number; y: number } {\n\tconst dx = worldX - (state.x + state.shakeOffsetX);\n\tconst dy = worldY - (state.y + state.shakeOffsetY);\n\n\tconst angle = -(state.rotation + state.shakeRotation);\n\tconst cos = Math.cos(angle);\n\tconst sin = Math.sin(angle);\n\tconst rx = dx * cos - dy * sin;\n\tconst ry = dx * sin + dy * cos;\n\n\treturn {\n\t\tx: rx * state.zoom + state.viewportWidth / 2,\n\t\ty: ry * state.zoom + state.viewportHeight / 2,\n\t};\n}\n\nexport function screenToWorld(\n\tscreenX: number,\n\tscreenY: number,\n\tstate: CameraState,\n): { x: number; y: number } {\n\tconst cx = (screenX - state.viewportWidth / 2) / state.zoom;\n\tconst cy = (screenY - state.viewportHeight / 2) / state.zoom;\n\n\tconst angle = state.rotation + state.shakeRotation;\n\tconst cos = Math.cos(angle);\n\tconst sin = Math.sin(angle);\n\tconst rx = cx * cos - cy * sin;\n\tconst ry = cx * sin + cy * cos;\n\n\treturn {\n\t\tx: rx + state.x + state.shakeOffsetX,\n\t\ty: ry + state.y + state.shakeOffsetY,\n\t};\n}\n\n// ==================== Internal Helpers ====================\n\nfunction resolveTarget(target: number | EntityHandle): number {\n\treturn typeof target === 'number' ? target : target.id;\n}\n\nfunction resolveShakeOptions(shake: true | Partial<Omit<CameraShake, 'trauma'>>): CameraShake {\n\tconst opts = shake === true ? {} : shake;\n\treturn {\n\t\ttrauma: 0,\n\t\ttraumaDecay: opts.traumaDecay ?? DEFAULT_SHAKE.traumaDecay,\n\t\tmaxOffsetX: opts.maxOffsetX ?? DEFAULT_SHAKE.maxOffsetX,\n\t\tmaxOffsetY: opts.maxOffsetY ?? DEFAULT_SHAKE.maxOffsetY,\n\t\tmaxRotation: opts.maxRotation ?? DEFAULT_SHAKE.maxRotation,\n\t};\n}\n\nfunction resolveBounds(\n\tbounds: { minX: number; minY: number; maxX: number; maxY: number } | [number, number, number, number],\n): CameraBounds {\n\tif (Array.isArray(bounds)) {\n\t\treturn { minX: bounds[0], minY: bounds[1], maxX: bounds[2], maxY: bounds[3] };\n\t}\n\treturn { ...bounds };\n}\n\nfunction resolveFollowOptions(options?: FollowOptions): Omit<CameraFollow, 'target'> {\n\treturn {\n\t\tsmoothing: options?.smoothing ?? DEFAULT_FOLLOW.smoothing,\n\t\tdeadzoneX: options?.deadzoneX ?? DEFAULT_FOLLOW.deadzoneX,\n\t\tdeadzoneY: options?.deadzoneY ?? DEFAULT_FOLLOW.deadzoneY,\n\t\toffsetX: options?.offsetX ?? DEFAULT_FOLLOW.offsetX,\n\t\toffsetY: options?.offsetY ?? DEFAULT_FOLLOW.offsetY,\n\t};\n}\n\n// ==================== Plugin Factory ====================\n\ntype CameraWorldConfig = WorldConfigFrom<CameraComponentTypes, {}, CameraResourceTypes>;\n\ntype CameraLabels =\n\t| 'camera-init'\n\t| 'camera-follow'\n\t| 'camera-shake-update'\n\t| 'camera-bounds'\n\t| 'camera-state-sync'\n\t| 'camera-zoom'\n\t| 'camera-pan';\n\nexport function createCameraPlugin<G extends string = 'camera'>(\n\toptions?: CameraPluginOptions<G>,\n) {\n\tconst {\n\t\tviewportWidth = 800,\n\t\tviewportHeight = 600,\n\t\tinitial,\n\t\tfollow: followConfig,\n\t\tshake: shakeConfig,\n\t\tbounds: boundsConfig,\n\t\tzoom: zoomConfig,\n\t\tpan: panConfig,\n\t\tsystemGroup = 'camera',\n\t\tphase = 'postUpdate',\n\t\trandomFn = Math.random,\n\t} = options ?? {};\n\n\treturn definePlugin('camera')\n\t\t.withComponentTypes<CameraComponentTypes>()\n\t\t.withResourceTypes<CameraResourceTypes>()\n\t\t.withLabels<CameraLabels>()\n\t\t.withGroups<G>()\n\t\t.requires<TransformWorldConfig>()\n\t\t.install((world) => {\n\t\t\t// Build mutation methods as closures over the world reference.\n\t\t\t// The cameraState resource is created immediately with placeholder methods,\n\t\t\t// then the init system populates entityId and wires up real methods.\n\n\t\t\tconst cameraState: CameraState = {\n\t\t\t\tx: initial?.x ?? 0,\n\t\t\t\ty: initial?.y ?? 0,\n\t\t\t\tzoom: initial?.zoom ?? 1,\n\t\t\t\trotation: initial?.rotation ?? 0,\n\t\t\t\tshakeOffsetX: 0,\n\t\t\t\tshakeOffsetY: 0,\n\t\t\t\tshakeRotation: 0,\n\t\t\t\tviewportWidth,\n\t\t\t\tviewportHeight,\n\t\t\t\tentityId: -1,\n\n\t\t\t\t// Mutation methods — wired up after camera entity is spawned\n\t\t\t\tfollow: () => {},\n\t\t\t\tunfollow: () => {},\n\t\t\t\tsetPosition: () => {},\n\t\t\t\tsetZoom: () => {},\n\t\t\t\tsetRotation: () => {},\n\t\t\t\tsetBounds: () => {},\n\t\t\t\tclearBounds: () => {},\n\t\t\t\taddTrauma: () => {},\n\t\t\t};\n\n\t\t\tworld.addResource('cameraState', cameraState);\n\n\t\t\t// camera-init: spawns camera entity and wires up mutation closures\n\t\t\tworld\n\t\t\t\t.addSystem('camera-init')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setOnInitialize((ecs: ECSpresso<CameraWorldConfig & TransformWorldConfig>) => {\n\t\t\t\t\t// Spawn with required camera component\n\t\t\t\t\tconst entity = ecs.spawn({\n\t\t\t\t\t\tcamera: {\n\t\t\t\t\t\t\tx: initial?.x ?? 0,\n\t\t\t\t\t\t\ty: initial?.y ?? 0,\n\t\t\t\t\t\t\tzoom: initial?.zoom ?? 1,\n\t\t\t\t\t\t\trotation: initial?.rotation ?? 0,\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\n\t\t\t\t\t// Conditionally add optional components\n\t\t\t\t\tif (followConfig) {\n\t\t\t\t\t\tecs.addComponent(entity.id, 'cameraFollow', {\n\t\t\t\t\t\t\ttarget: -1,\n\t\t\t\t\t\t\t...resolveFollowOptions(followConfig),\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\tif (shakeConfig) {\n\t\t\t\t\t\tecs.addComponent(entity.id, 'cameraShake', resolveShakeOptions(shakeConfig));\n\t\t\t\t\t}\n\n\t\t\t\t\tif (boundsConfig) {\n\t\t\t\t\t\tecs.addComponent(entity.id, 'cameraBounds', resolveBounds(boundsConfig));\n\t\t\t\t\t}\n\t\t\t\t\tcameraState.entityId = entity.id;\n\n\t\t\t\t\t// Wire up mutation methods\n\t\t\t\t\tcameraState.follow = (target: number | EntityHandle, opts?: FollowOptions) => {\n\t\t\t\t\t\tconst targetId = resolveTarget(target);\n\t\t\t\t\t\tconst followData: CameraFollow = {\n\t\t\t\t\t\t\ttarget: targetId,\n\t\t\t\t\t\t\t...resolveFollowOptions(opts),\n\t\t\t\t\t\t};\n\t\t\t\t\t\tconst existing = ecs.getComponent(cameraState.entityId, 'cameraFollow');\n\t\t\t\t\t\tif (existing) {\n\t\t\t\t\t\t\texisting.target = followData.target;\n\t\t\t\t\t\t\texisting.smoothing = followData.smoothing;\n\t\t\t\t\t\t\texisting.deadzoneX = followData.deadzoneX;\n\t\t\t\t\t\t\texisting.deadzoneY = followData.deadzoneY;\n\t\t\t\t\t\t\texisting.offsetX = followData.offsetX;\n\t\t\t\t\t\t\texisting.offsetY = followData.offsetY;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tecs.addComponent(cameraState.entityId, 'cameraFollow', followData);\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\n\t\t\t\t\tcameraState.unfollow = () => {\n\t\t\t\t\t\tconst existing = ecs.getComponent(cameraState.entityId, 'cameraFollow');\n\t\t\t\t\t\tif (existing) {\n\t\t\t\t\t\t\tecs.removeComponent(cameraState.entityId, 'cameraFollow');\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\n\t\t\t\t\tcameraState.setPosition = (x: number, y: number) => {\n\t\t\t\t\t\tconst camera = ecs.getComponent(cameraState.entityId, 'camera');\n\t\t\t\t\t\tif (!camera) return;\n\t\t\t\t\t\tcamera.x = x;\n\t\t\t\t\t\tcamera.y = y;\n\t\t\t\t\t};\n\n\t\t\t\t\tcameraState.setZoom = (zoom: number) => {\n\t\t\t\t\t\tconst camera = ecs.getComponent(cameraState.entityId, 'camera');\n\t\t\t\t\t\tif (!camera) return;\n\t\t\t\t\t\tcamera.zoom = zoom;\n\t\t\t\t\t};\n\n\t\t\t\t\tcameraState.setRotation = (rotation: number) => {\n\t\t\t\t\t\tconst camera = ecs.getComponent(cameraState.entityId, 'camera');\n\t\t\t\t\t\tif (!camera) return;\n\t\t\t\t\t\tcamera.rotation = rotation;\n\t\t\t\t\t};\n\n\t\t\t\t\tcameraState.setBounds = (minX: number, minY: number, maxX: number, maxY: number) => {\n\t\t\t\t\t\tconst existing = ecs.getComponent(cameraState.entityId, 'cameraBounds');\n\t\t\t\t\t\tif (existing) {\n\t\t\t\t\t\t\texisting.minX = minX;\n\t\t\t\t\t\t\texisting.minY = minY;\n\t\t\t\t\t\t\texisting.maxX = maxX;\n\t\t\t\t\t\t\texisting.maxY = maxY;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tecs.addComponent(cameraState.entityId, 'cameraBounds', { minX, minY, maxX, maxY });\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\n\t\t\t\t\tcameraState.clearBounds = () => {\n\t\t\t\t\t\tconst existing = ecs.getComponent(cameraState.entityId, 'cameraBounds');\n\t\t\t\t\t\tif (existing) {\n\t\t\t\t\t\t\tecs.removeComponent(cameraState.entityId, 'cameraBounds');\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\n\t\t\t\t\tcameraState.addTrauma = (amount: number) => {\n\t\t\t\t\t\tconst shake = ecs.getComponent(cameraState.entityId, 'cameraShake');\n\t\t\t\t\t\tif (shake) {\n\t\t\t\t\t\t\tshake.trauma = Math.min(1, Math.max(0, shake.trauma + amount));\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tecs.addComponent(cameraState.entityId, 'cameraShake', {\n\t\t\t\t\t\t\t\t...resolveShakeOptions(true),\n\t\t\t\t\t\t\t\ttrauma: Math.min(1, Math.max(0, amount)),\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\t\t\t\t});\n\n\t\t\t// camera-follow: priority 400 (after transform propagation at 500)\n\t\t\tworld\n\t\t\t\t.addSystem('camera-follow')\n\t\t\t\t.setPriority(400)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('cameras', {\n\t\t\t\t\twith: ['camera', 'cameraFollow'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, dt, ecs }) => {\n\t\t\t\t\tconst t = Math.min(1, dt);\n\t\t\t\t\tfor (const entity of queries.cameras) {\n\t\t\t\t\t\tconst { camera, cameraFollow } = entity.components;\n\t\t\t\t\t\tif (cameraFollow.target < 0) continue;\n\n\t\t\t\t\t\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\ttype ZoomInputState = { pointer: { position: { x: number; y: number } } };\n\n\t\t\t\tlet pendingSteps = 0;\n\t\t\t\tlet zoomActive = false;\n\t\t\t\tlet canvas: HTMLCanvasElement | undefined;\n\t\t\t\tlet isoState: { tileWidth: number; tileHeight: number; originX: number; originY: number } | undefined;\n\n\t\t\t\tfunction onWheel(e: WheelEvent) {\n\t\t\t\t\te.preventDefault();\n\t\t\t\t\tpendingSteps += Math.sign(e.deltaY);\n\t\t\t\t}\n\n\t\t\t\tworld\n\t\t\t\t\t.addSystem('camera-zoom')\n\t\t\t\t\t.setPriority(410)\n\t\t\t\t\t.inPhase('preUpdate')\n\t\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t\t.addQuery('cameras', {\n\t\t\t\t\t\twith: ['camera'],\n\t\t\t\t\t})\n\t\t\t\t\t.setOnInitialize((ecs) => {\n\t\t\t\t\t\t// Check for required dependencies\n\t\t\t\t\t\tconst inputState = ecs.tryGetResource<ZoomInputState>('inputState');\n\t\t\t\t\t\tconst pixiApp = ecs.tryGetResource<{ canvas: HTMLCanvasElement }>('pixiApp');\n\n\t\t\t\t\t\tif (!inputState || !pixiApp) {\n\t\t\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t\t\t'[camera] zoom requires the input plugin and renderer2D plugin. ' +\n\t\t\t\t\t\t\t\t'Zoom will be disabled.',\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tcanvas = pixiApp.canvas;\n\t\t\t\t\t\tcanvas.addEventListener('wheel', onWheel as EventListener, { passive: false });\n\n\t\t\t\t\t\t// Detect isometric projection for iso-aware cursor-centered zoom\n\t\t\t\t\t\tisoState = ecs.tryGetResource<{\n\t\t\t\t\t\t\ttileWidth: number; tileHeight: number;\n\t\t\t\t\t\t\toriginX: number; originY: number;\n\t\t\t\t\t\t}>('isoProjection');\n\n\t\t\t\t\t\tzoomActive = true;\n\t\t\t\t\t})\n\t\t\t\t\t.setOnDetach(() => {\n\t\t\t\t\t\tif (!zoomActive || !canvas) return;\n\t\t\t\t\t\tcanvas.removeEventListener('wheel', onWheel as EventListener);\n\t\t\t\t\t})\n\t\t\t\t\t.setProcess(({ queries, ecs }) => {\n\t\t\t\t\t\tif (!zoomActive || pendingSteps === 0) return;\n\n\t\t\t\t\t\tconst steps = pendingSteps;\n\t\t\t\t\t\tpendingSteps = 0;\n\n\t\t\t\t\t\tconst [cameraEntity] = queries.cameras;\n\t\t\t\t\t\tif (!cameraEntity) return;\n\n\t\t\t\t\t\tconst cam = cameraEntity.components.camera;\n\t\t\t\t\t\tconst inputState = ecs.tryGetResource<ZoomInputState>('inputState');\n\t\t\t\t\t\tif (!inputState) return;\n\n\t\t\t\t\t\t// Apply zoom — proportional to number of wheel steps\n\t\t\t\t\t\tconst direction = steps > 0 ? (1 - zoomStep) : (1 + zoomStep);\n\t\t\t\t\t\tconst newZoom = Math.max(minZoom, Math.min(maxZoom, cam.zoom * Math.pow(direction, Math.abs(steps))));\n\n\t\t\t\t\t\tif (isoState && canvas) {\n\t\t\t\t\t\t\t// Iso-aware cursor-centered zoom: work in iso-screen space\n\t\t\t\t\t\t\tconst rect = canvas.getBoundingClientRect();\n\t\t\t\t\t\t\tconst screenOffX = inputState.pointer.position.x - (rect.left + rect.width / 2);\n\t\t\t\t\t\t\tconst screenOffY = inputState.pointer.position.y - (rect.top + rect.height / 2);\n\n\t\t\t\t\t\t\t// Inlined worldToIso — avoids cross-plugin import\n\t\t\t\t\t\t\tconst halfW = isoState.tileWidth / 2;\n\t\t\t\t\t\t\tconst halfH = isoState.tileHeight / 2;\n\t\t\t\t\t\t\tconst camIsoX = (cam.x - cam.y) * halfW + isoState.originX;\n\t\t\t\t\t\t\tconst camIsoY = (cam.x + cam.y) * halfH + isoState.originY;\n\t\t\t\t\t\t\tconst isoBeforeX = camIsoX + screenOffX / cam.zoom;\n\t\t\t\t\t\t\tconst isoBeforeY = camIsoY + screenOffY / cam.zoom;\n\n\t\t\t\t\t\t\tcam.zoom = newZoom;\n\n\t\t\t\t\t\t\t// New camera iso position so the same point stays under cursor\n\t\t\t\t\t\t\tconst newCamIsoX = isoBeforeX - screenOffX / newZoom;\n\t\t\t\t\t\t\tconst newCamIsoY = isoBeforeY - screenOffY / newZoom;\n\n\t\t\t\t\t\t\t// Inlined isoToWorld\n\t\t\t\t\t\t\tconst relX = newCamIsoX - isoState.originX;\n\t\t\t\t\t\t\tconst relY = newCamIsoY - isoState.originY;\n\t\t\t\t\t\t\tcam.x = relX / isoState.tileWidth + relY / isoState.tileHeight;\n\t\t\t\t\t\t\tcam.y = -relX / isoState.tileWidth + relY / isoState.tileHeight;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Pixel-space cursor-centered zoom\n\t\t\t\t\t\t\tconst worldBefore = screenToWorld(\n\t\t\t\t\t\t\t\tinputState.pointer.position.x,\n\t\t\t\t\t\t\t\tinputState.pointer.position.y,\n\t\t\t\t\t\t\t\tcameraState,\n\t\t\t\t\t\t\t);\n\n\t\t\t\t\t\t\tcam.zoom = newZoom;\n\n\t\t\t\t\t\t\tcam.x = worldBefore.x - (inputState.pointer.position.x - cameraState.viewportWidth / 2) / newZoom;\n\t\t\t\t\t\t\tcam.y = worldBefore.y - (inputState.pointer.position.y - cameraState.viewportHeight / 2) / newZoom;\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t}\n\n\t\t\t// camera-pan: conditionally registered when pan option is provided\n\t\t\tif (panConfig) {\n\t\t\t\ttype PanInputState = { actions: { isActive(action: string): boolean } };\n\n\t\t\t\tconst {\n\t\t\t\t\tspeed,\n\t\t\t\t\tactions: panActions,\n\t\t\t\t} = panConfig;\n\n\t\t\t\tconst actionUp = panActions?.up ?? 'panUp';\n\t\t\t\tconst actionDown = panActions?.down ?? 'panDown';\n\t\t\t\tconst actionLeft = panActions?.left ?? 'panLeft';\n\t\t\t\tconst actionRight = panActions?.right ?? 'panRight';\n\n\t\t\t\tlet panActive = false;\n\n\t\t\t\tworld\n\t\t\t\t\t.addSystem('camera-pan')\n\t\t\t\t\t.setPriority(420)\n\t\t\t\t\t.inPhase('preUpdate')\n\t\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t\t.setOnInitialize((ecs) => {\n\t\t\t\t\t\tconst inputState = ecs.tryGetResource<PanInputState>('inputState');\n\t\t\t\t\t\tif (!inputState) {\n\t\t\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t\t\t'[camera] pan requires the input plugin. Pan will be disabled.',\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tpanActive = true;\n\t\t\t\t\t})\n\t\t\t\t\t.setProcess(({ ecs, dt }) => {\n\t\t\t\t\t\tif (!panActive) return;\n\n\t\t\t\t\t\tconst inputState = ecs.tryGetResource<PanInputState>('inputState');\n\t\t\t\t\t\tif (!inputState) return;\n\n\t\t\t\t\t\tconst delta = (speed / cameraState.zoom) * dt;\n\t\t\t\t\t\tconst dx = (inputState.actions.isActive(actionRight) ? 1 : 0)\n\t\t\t\t\t\t\t- (inputState.actions.isActive(actionLeft) ? 1 : 0);\n\t\t\t\t\t\tconst dy = (inputState.actions.isActive(actionDown) ? 1 : 0)\n\t\t\t\t\t\t\t- (inputState.actions.isActive(actionUp) ? 1 : 0);\n\n\t\t\t\t\t\tif (dx !== 0 || dy !== 0) {\n\t\t\t\t\t\t\tcameraState.setPosition(\n\t\t\t\t\t\t\t\tcameraState.x + dx * delta,\n\t\t\t\t\t\t\t\tcameraState.y + dy * delta,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t}\n\t\t});\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": "2PAcA,uBAAS,
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "EEB187C9BEF5641464756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var
|
|
1
|
+
var _=((j)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(j,{get:(k,q)=>(typeof require<"u"?require:k)[q]}):j)(function(j){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+j+'" is not supported')});import{definePlugin as Q}from"ecspresso";var G={x:0,y:0,rotation:0,scaleX:1,scaleY:1},P={x:0,y:0,rotation:0,scaleX:1,scaleY:1};function b(j,k){return{localTransform:{x:j,y:k,rotation:0,scaleX:1,scaleY:1}}}function g(j,k){return{worldTransform:{x:j,y:k,rotation:0,scaleX:1,scaleY:1}}}function W(j,k,q){let C=q?.scale??q?.scaleX??1,v=q?.scale??q?.scaleY??1,D=q?.rotation??0,z={x:j,y:k,rotation:D,scaleX:C,scaleY:v};return{localTransform:{...z},worldTransform:{...z}}}function R(j){let{systemGroup:k="transform",priority:q=500,phase:C="postUpdate"}=j??{};return Q("transform").withComponentTypes().withLabels().withGroups().install((v)=>{v.registerRequired("localTransform","worldTransform",(E)=>({x:E.x,y:E.y,rotation:E.rotation,scaleX:E.scaleX,scaleY:E.scaleY}));let D=[],z=new Set;v.addSystem("transform-propagation").setPriority(q).inPhase(C).inGroup(k).setProcess(({ecs:E})=>{U(E,D,z)})})}function U(j,k,q){let C=j.entityManager;if(!C.hasHierarchy){C.getEntitiesWithQueryInto(k,["localTransform","worldTransform"]);for(let v of k){let{localTransform:D,worldTransform:z}=v.components;if(J(D,z))j.markChanged(v.id,"worldTransform")}return}q.clear(),j.forEachInHierarchy((v,D)=>{q.add(v);let z=C.getComponent(v,"localTransform"),E=C.getComponent(v,"worldTransform");if(!z||!E)return;let F=D!==null?C.getComponent(D,"worldTransform"):null;if(F?Z(F,z,E):J(z,E))j.markChanged(v,"worldTransform")}),C.getEntitiesWithQueryInto(k,["localTransform","worldTransform"]);for(let v of k){if(q.has(v.id))continue;let{localTransform:D,worldTransform:z}=v.components;if(J(D,z))j.markChanged(v.id,"worldTransform")}}function J(j,k){if(k.x===j.x&&k.y===j.y&&k.rotation===j.rotation&&k.scaleX===j.scaleX&&k.scaleY===j.scaleY)return!1;return k.x=j.x,k.y=j.y,k.rotation=j.rotation,k.scaleX=j.scaleX,k.scaleY=j.scaleY,!0}function Z(j,k,q){let C=k.x*j.scaleX,v=k.y*j.scaleY,D=Math.cos(j.rotation),z=Math.sin(j.rotation),E=C*D-v*z,F=C*z+v*D,H=j.x+E,K=j.y+F,M=j.rotation+k.rotation,N=j.scaleX*k.scaleX,O=j.scaleY*k.scaleY;if(q.x===H&&q.y===K&&q.rotation===M&&q.scaleX===N&&q.scaleY===O)return!1;return q.x=H,q.y=K,q.rotation=M,q.scaleX=N,q.scaleY=O,!0}export{g as createWorldTransform,R as createTransformPlugin,W as createTransform,b as createLocalTransform,P as DEFAULT_WORLD_TRANSFORM,G as DEFAULT_LOCAL_TRANSFORM};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=DD4C20CA5FEDDFB464756E2164756E21
|
|
4
4
|
//# sourceMappingURL=transform.js.map
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/plugins/spatial/transform.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * Transform Plugin for ECSpresso\n *\n * Provides hierarchical transform propagation following Bevy's Transform/GlobalTransform pattern.\n * LocalTransform is modified by user code; WorldTransform is computed automatically.\n *\n * @see https://docs.rs/bevy/latest/bevy/transform/components/struct.GlobalTransform.html\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type ECSpresso from 'ecspresso';\nimport type { WorldConfigFrom } from '../../type-utils';\n\n// ==================== Component Types ====================\n\n/**\n * Local transform relative to parent (or world if no parent).\n * This is the transform you modify directly.\n */\nexport interface LocalTransform {\n\tx: number;\n\ty: number;\n\trotation: number;\n\tscaleX: number;\n\tscaleY: number;\n}\n\n/**\n * Computed world transform (accumulated from parent chain).\n * Read-only - managed by the transform propagation system.\n */\nexport interface WorldTransform {\n\tx: number;\n\ty: number;\n\trotation: number;\n\tscaleX: number;\n\tscaleY: number;\n}\n\n/**\n * Component types provided by the transform plugin.\n * Included automatically via `.withPlugin(createTransformPlugin())`.\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createTransformPlugin())\n * .withComponentTypes<{ sprite: Sprite; velocity: { x: number; y: number } }>()\n * .build();\n * ```\n */\nexport interface TransformComponentTypes {\n\tlocalTransform: LocalTransform;\n\tworldTransform: WorldTransform;\n}\n\n/**\n * WorldConfig representing the transform plugin's provided components.\n * Used as the `Requires` type parameter by plugins that depend on transform.\n */\nexport type TransformWorldConfig = WorldConfigFrom<TransformComponentTypes>;\n\n// ==================== Plugin Options ====================\n\n/**\n * Configuration options for the transform plugin.\n */\nexport interface TransformPluginOptions<G extends string = 'transform'> extends BasePluginOptions<G> {}\n\n// ==================== Default Values ====================\n\n/**\n * Default local transform values.\n */\nexport const DEFAULT_LOCAL_TRANSFORM: Readonly<LocalTransform> = {\n\tx: 0,\n\ty: 0,\n\trotation: 0,\n\tscaleX: 1,\n\tscaleY: 1,\n};\n\n/**\n * Default world transform values.\n */\nexport const DEFAULT_WORLD_TRANSFORM: Readonly<WorldTransform> = {\n\tx: 0,\n\ty: 0,\n\trotation: 0,\n\tscaleX: 1,\n\tscaleY: 1,\n};\n\n// ==================== Helper Functions ====================\n\n/**\n * Create a local transform component with position only.\n * Uses default rotation (0) and scale (1, 1).\n *\n * @param x The x coordinate\n * @param y The y coordinate\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createLocalTransform(100, 200),\n * sprite,\n * });\n * ```\n */\nexport function createLocalTransform(x: number, y: number): Pick<TransformComponentTypes, 'localTransform'> {\n\treturn {\n\t\tlocalTransform: {\n\t\t\tx,\n\t\t\ty,\n\t\t\trotation: 0,\n\t\t\tscaleX: 1,\n\t\t\tscaleY: 1,\n\t\t},\n\t};\n}\n\n/**\n * Create a world transform component with position only.\n * Typically used alongside createLocalTransform for initial state.\n *\n * @param x The x coordinate\n * @param y The y coordinate\n * @returns Component object suitable for spreading into spawn()\n */\nexport function createWorldTransform(x: number, y: number): Pick<TransformComponentTypes, 'worldTransform'> {\n\treturn {\n\t\tworldTransform: {\n\t\t\tx,\n\t\t\ty,\n\t\t\trotation: 0,\n\t\t\tscaleX: 1,\n\t\t\tscaleY: 1,\n\t\t},\n\t};\n}\n\n/**\n * Options for creating a full transform.\n */\nexport interface TransformOptions {\n\trotation?: number;\n\tscaleX?: number;\n\tscaleY?: number;\n\t/** Uniform scale (overrides scaleX/scaleY if provided) */\n\tscale?: number;\n}\n\n/**\n * Create both local and world transform components.\n * World transform is initialized to match local transform.\n *\n * @param x The x coordinate\n * @param y The y coordinate\n * @param options Optional rotation and scale\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createTransform(100, 200),\n * sprite,\n * });\n *\n * // With rotation and scale\n * ecs.spawn({\n * ...createTransform(100, 200, { rotation: Math.PI / 4, scale: 2 }),\n * sprite,\n * });\n * ```\n */\nexport function createTransform(\n\tx: number,\n\ty: number,\n\toptions?: TransformOptions\n): TransformComponentTypes {\n\tconst scaleX = options?.scale ?? options?.scaleX ?? 1;\n\tconst scaleY = options?.scale ?? options?.scaleY ?? 1;\n\tconst rotation = options?.rotation ?? 0;\n\n\tconst transform = {\n\t\tx,\n\t\ty,\n\t\trotation,\n\t\tscaleX,\n\t\tscaleY,\n\t};\n\n\treturn {\n\t\tlocalTransform: { ...transform },\n\t\tworldTransform: { ...transform },\n\t};\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a transform plugin for ECSpresso.\n *\n * This plugin provides:\n * - Transform propagation system that computes world transforms from local transforms\n * - Parent-first traversal ensures parents are processed before children\n * - Supports full transform hierarchy (position, rotation, scale)\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso\n * .create<Components, Events, Resources>()\n * .withPlugin(createTransformPlugin())\n * .withPlugin(createPhysics2DPlugin())\n * .build();\n *\n * // Spawn entity with transform\n * ecs.spawn({\n * ...createTransform(100, 200),\n * velocity: { x: 50, y: 0 },\n * });\n * ```\n */\nexport function createTransformPlugin<G extends string = 'transform'>(\n\toptions?: TransformPluginOptions<G>\n) {\n\tconst {\n\t\tsystemGroup = 'transform',\n\t\tpriority = 500,\n\t\tphase = 'postUpdate',\n\t} = options ?? {};\n\n\treturn definePlugin('transform')\n\t\t.withComponentTypes<TransformComponentTypes>()\n\t\t.withLabels<'transform-propagation'>()\n\t\t.withGroups<G>()\n\t\t.install((world) => {\n\t\t\t// localTransform requires worldTransform — initialize from localTransform values\n\t\t\tworld.registerRequired('localTransform', 'worldTransform', (lt) => ({\n\t\t\t\tx: lt.x, y: lt.y, rotation: lt.rotation, scaleX: lt.scaleX, scaleY: lt.scaleY,\n\t\t\t}));\n\n\t\t\tconst orphanBuffer: Array<import('../../types').FilteredEntity<TransformComponentTypes, 'localTransform' | 'worldTransform'>> = [];\n\n\t\t\tworld\n\t\t\t\t.addSystem('transform-propagation')\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.setProcess(({ ecs }) => {\n\t\t\t\t\tpropagateTransforms(ecs, orphanBuffer);\n\t\t\t\t});\n\t\t});\n}\n\n/**\n * Propagate transforms through the hierarchy.\n * Parent-first traversal ensures parents are computed before children.\n *\n * Runs unconditionally for all entities with transforms — user code can\n * freely mutate localTransform without needing to call markChanged.\n * Marks worldTransform as changed so downstream systems (e.g. renderer\n * sync) pick up the updated values.\n */\nfunction propagateTransforms(\n\tecs: ECSpresso<WorldConfigFrom<TransformComponentTypes>>,\n\torphanBuffer: Array<import('../../types').FilteredEntity<TransformComponentTypes, 'localTransform' | 'worldTransform'>>,\n): void {\n\tconst em = ecs.entityManager;\n\n\t// Use parent-first traversal for entities in hierarchy\n\tecs.forEachInHierarchy((entityId, parentId) => {\n\t\tconst localTransform = em.getComponent(entityId, 'localTransform');\n\t\tconst worldTransform = em.getComponent(entityId, 'worldTransform');\n\n\t\tif (!localTransform || !worldTransform) return;\n\n\t\tif (parentId === null) {\n\t\t\t// Root entity: world transform equals local transform\n\t\t\tcopyTransform(localTransform, worldTransform);\n\t\t} else {\n\t\t\t// Child entity: combine with parent's world transform\n\t\t\tconst parentWorld = em.getComponent(parentId, 'worldTransform');\n\t\t\tif (parentWorld) {\n\t\t\t\tcombineTransforms(parentWorld, localTransform, worldTransform);\n\t\t\t} else {\n\t\t\t\t// Parent has no world transform, treat as root\n\t\t\t\tcopyTransform(localTransform, worldTransform);\n\t\t\t}\n\t\t}\n\n\t\tecs.markChanged(entityId, 'worldTransform');\n\t});\n\n\t// Process orphaned entities (not in hierarchy but have transforms)\n\tem.getEntitiesWithQueryInto(orphanBuffer, ['localTransform', 'worldTransform']);\n\tfor (const entity of orphanBuffer) {\n\t\tconst parentId = ecs.getParent(entity.id);\n\t\t// Only process if truly orphaned (no parent and not a root with children)\n\t\tif (parentId === null && ecs.getChildren(entity.id).length === 0) {\n\t\t\tconst { localTransform, worldTransform } = entity.components;\n\t\t\tcopyTransform(localTransform, worldTransform);\n\t\t\tecs.markChanged(entity.id, 'worldTransform');\n\t\t}\n\t}\n}\n\n/**\n * Copy transform values from source to destination.\n */\nfunction copyTransform(src: LocalTransform, dest: WorldTransform): void {\n\tdest.x = src.x;\n\tdest.y = src.y;\n\tdest.rotation = src.rotation;\n\tdest.scaleX = src.scaleX;\n\tdest.scaleY = src.scaleY;\n}\n\n/**\n * Combine parent world transform with child local transform into child world transform.\n */\nfunction combineTransforms(\n\tparent: WorldTransform,\n\tlocal: LocalTransform,\n\tworld: WorldTransform\n): void {\n\t// Apply parent's scale to local position\n\tconst scaledLocalX = local.x * parent.scaleX;\n\tconst scaledLocalY = local.y * parent.scaleY;\n\n\t// Rotate local position by parent's rotation\n\tconst cos = Math.cos(parent.rotation);\n\tconst sin = Math.sin(parent.rotation);\n\tconst rotatedX = scaledLocalX * cos - scaledLocalY * sin;\n\tconst rotatedY = scaledLocalX * sin + scaledLocalY * cos;\n\n\t// Add to parent's position\n\tworld.x = parent.x + rotatedX;\n\tworld.y = parent.y + rotatedY;\n\tworld.rotation = parent.rotation + local.rotation;\n\tworld.scaleX = parent.scaleX * local.scaleX;\n\tworld.scaleY = parent.scaleY * local.scaleY;\n}\n"
|
|
5
|
+
"/**\n * Transform Plugin for ECSpresso\n *\n * Provides hierarchical transform propagation following Bevy's Transform/GlobalTransform pattern.\n * LocalTransform is modified by user code; WorldTransform is computed automatically.\n *\n * @see https://docs.rs/bevy/latest/bevy/transform/components/struct.GlobalTransform.html\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type ECSpresso from 'ecspresso';\nimport type { WorldConfigFrom } from '../../type-utils';\n\n// ==================== Component Types ====================\n\n/**\n * Local transform relative to parent (or world if no parent).\n * This is the transform you modify directly.\n */\nexport interface LocalTransform {\n\tx: number;\n\ty: number;\n\trotation: number;\n\tscaleX: number;\n\tscaleY: number;\n}\n\n/**\n * Computed world transform (accumulated from parent chain).\n * Read-only - managed by the transform propagation system.\n */\nexport interface WorldTransform {\n\tx: number;\n\ty: number;\n\trotation: number;\n\tscaleX: number;\n\tscaleY: number;\n}\n\n/**\n * Component types provided by the transform plugin.\n * Included automatically via `.withPlugin(createTransformPlugin())`.\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createTransformPlugin())\n * .withComponentTypes<{ sprite: Sprite; velocity: { x: number; y: number } }>()\n * .build();\n * ```\n */\nexport interface TransformComponentTypes {\n\tlocalTransform: LocalTransform;\n\tworldTransform: WorldTransform;\n}\n\n/**\n * WorldConfig representing the transform plugin's provided components.\n * Used as the `Requires` type parameter by plugins that depend on transform.\n */\nexport type TransformWorldConfig = WorldConfigFrom<TransformComponentTypes>;\n\n// ==================== Plugin Options ====================\n\n/**\n * Configuration options for the transform plugin.\n */\nexport interface TransformPluginOptions<G extends string = 'transform'> extends BasePluginOptions<G> {}\n\n// ==================== Default Values ====================\n\n/**\n * Default local transform values.\n */\nexport const DEFAULT_LOCAL_TRANSFORM: Readonly<LocalTransform> = {\n\tx: 0,\n\ty: 0,\n\trotation: 0,\n\tscaleX: 1,\n\tscaleY: 1,\n};\n\n/**\n * Default world transform values.\n */\nexport const DEFAULT_WORLD_TRANSFORM: Readonly<WorldTransform> = {\n\tx: 0,\n\ty: 0,\n\trotation: 0,\n\tscaleX: 1,\n\tscaleY: 1,\n};\n\n// ==================== Helper Functions ====================\n\n/**\n * Create a local transform component with position only.\n * Uses default rotation (0) and scale (1, 1).\n *\n * @param x The x coordinate\n * @param y The y coordinate\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createLocalTransform(100, 200),\n * sprite,\n * });\n * ```\n */\nexport function createLocalTransform(x: number, y: number): Pick<TransformComponentTypes, 'localTransform'> {\n\treturn {\n\t\tlocalTransform: {\n\t\t\tx,\n\t\t\ty,\n\t\t\trotation: 0,\n\t\t\tscaleX: 1,\n\t\t\tscaleY: 1,\n\t\t},\n\t};\n}\n\n/**\n * Create a world transform component with position only.\n * Typically used alongside createLocalTransform for initial state.\n *\n * @param x The x coordinate\n * @param y The y coordinate\n * @returns Component object suitable for spreading into spawn()\n */\nexport function createWorldTransform(x: number, y: number): Pick<TransformComponentTypes, 'worldTransform'> {\n\treturn {\n\t\tworldTransform: {\n\t\t\tx,\n\t\t\ty,\n\t\t\trotation: 0,\n\t\t\tscaleX: 1,\n\t\t\tscaleY: 1,\n\t\t},\n\t};\n}\n\n/**\n * Options for creating a full transform.\n */\nexport interface TransformOptions {\n\trotation?: number;\n\tscaleX?: number;\n\tscaleY?: number;\n\t/** Uniform scale (overrides scaleX/scaleY if provided) */\n\tscale?: number;\n}\n\n/**\n * Create both local and world transform components.\n * World transform is initialized to match local transform.\n *\n * @param x The x coordinate\n * @param y The y coordinate\n * @param options Optional rotation and scale\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createTransform(100, 200),\n * sprite,\n * });\n *\n * // With rotation and scale\n * ecs.spawn({\n * ...createTransform(100, 200, { rotation: Math.PI / 4, scale: 2 }),\n * sprite,\n * });\n * ```\n */\nexport function createTransform(\n\tx: number,\n\ty: number,\n\toptions?: TransformOptions\n): TransformComponentTypes {\n\tconst scaleX = options?.scale ?? options?.scaleX ?? 1;\n\tconst scaleY = options?.scale ?? options?.scaleY ?? 1;\n\tconst rotation = options?.rotation ?? 0;\n\n\tconst transform = {\n\t\tx,\n\t\ty,\n\t\trotation,\n\t\tscaleX,\n\t\tscaleY,\n\t};\n\n\treturn {\n\t\tlocalTransform: { ...transform },\n\t\tworldTransform: { ...transform },\n\t};\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a transform plugin for ECSpresso.\n *\n * This plugin provides:\n * - Transform propagation system that computes world transforms from local transforms\n * - Parent-first traversal ensures parents are processed before children\n * - Supports full transform hierarchy (position, rotation, scale)\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso\n * .create<Components, Events, Resources>()\n * .withPlugin(createTransformPlugin())\n * .withPlugin(createPhysics2DPlugin())\n * .build();\n *\n * // Spawn entity with transform\n * ecs.spawn({\n * ...createTransform(100, 200),\n * velocity: { x: 50, y: 0 },\n * });\n * ```\n */\nexport function createTransformPlugin<G extends string = 'transform'>(\n\toptions?: TransformPluginOptions<G>\n) {\n\tconst {\n\t\tsystemGroup = 'transform',\n\t\tpriority = 500,\n\t\tphase = 'postUpdate',\n\t} = options ?? {};\n\n\treturn definePlugin('transform')\n\t\t.withComponentTypes<TransformComponentTypes>()\n\t\t.withLabels<'transform-propagation'>()\n\t\t.withGroups<G>()\n\t\t.install((world) => {\n\t\t\t// localTransform requires worldTransform — initialize from localTransform values\n\t\t\tworld.registerRequired('localTransform', 'worldTransform', (lt) => ({\n\t\t\t\tx: lt.x, y: lt.y, rotation: lt.rotation, scaleX: lt.scaleX, scaleY: lt.scaleY,\n\t\t\t}));\n\n\t\t\tconst orphanBuffer: Array<import('../../types').FilteredEntity<TransformComponentTypes, 'localTransform' | 'worldTransform'>> = [];\n\t\t\tconst hierarchyVisited = new Set<number>();\n\n\t\t\tworld\n\t\t\t\t.addSystem('transform-propagation')\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.setProcess(({ ecs }) => {\n\t\t\t\t\tpropagateTransforms(ecs, orphanBuffer, hierarchyVisited);\n\t\t\t\t});\n\t\t});\n}\n\n/**\n * Propagate transforms through the hierarchy.\n * Parent-first traversal ensures parents are computed before children.\n *\n * Runs unconditionally for all entities with transforms — user code can\n * freely mutate localTransform without needing to call markChanged.\n * Only marks worldTransform as changed when values actually differ,\n * so downstream systems (e.g. renderer sync) can skip static entities.\n */\nfunction propagateTransforms(\n\tecs: ECSpresso<WorldConfigFrom<TransformComponentTypes>>,\n\torphanBuffer: Array<import('../../types').FilteredEntity<TransformComponentTypes, 'localTransform' | 'worldTransform'>>,\n\thierarchyVisited: Set<number>,\n): void {\n\tconst em = ecs.entityManager;\n\n\t// Fast path: no hierarchy relationships exist — all entities are flat\n\tif (!em.hasHierarchy) {\n\t\tem.getEntitiesWithQueryInto(orphanBuffer, ['localTransform', 'worldTransform']);\n\t\tfor (const entity of orphanBuffer) {\n\t\t\tconst { localTransform, worldTransform } = entity.components;\n\t\t\tif (copyTransform(localTransform, worldTransform)) {\n\t\t\t\tecs.markChanged(entity.id, 'worldTransform');\n\t\t\t}\n\t\t}\n\t\treturn;\n\t}\n\n\t// Hierarchy exists — use parent-first traversal then process remaining orphans\n\thierarchyVisited.clear();\n\n\tecs.forEachInHierarchy((entityId, parentId) => {\n\t\thierarchyVisited.add(entityId);\n\t\tconst localTransform = em.getComponent(entityId, 'localTransform');\n\t\tconst worldTransform = em.getComponent(entityId, 'worldTransform');\n\n\t\tif (!localTransform || !worldTransform) return;\n\n\t\tconst parentWorld = parentId !== null\n\t\t\t? em.getComponent(parentId, 'worldTransform')\n\t\t\t: null;\n\n\t\tconst changed = parentWorld\n\t\t\t? combineTransforms(parentWorld, localTransform, worldTransform)\n\t\t\t: copyTransform(localTransform, worldTransform);\n\n\t\tif (changed) ecs.markChanged(entityId, 'worldTransform');\n\t});\n\n\tem.getEntitiesWithQueryInto(orphanBuffer, ['localTransform', 'worldTransform']);\n\tfor (const entity of orphanBuffer) {\n\t\tif (hierarchyVisited.has(entity.id)) continue;\n\t\tconst { localTransform, worldTransform } = entity.components;\n\t\tif (copyTransform(localTransform, worldTransform)) {\n\t\t\tecs.markChanged(entity.id, 'worldTransform');\n\t\t}\n\t}\n}\n\n/**\n * Copy transform values from source to destination.\n * Returns true if the destination was actually modified.\n */\nfunction copyTransform(src: LocalTransform, dest: WorldTransform): boolean {\n\tif (dest.x === src.x && dest.y === src.y &&\n\t\tdest.rotation === src.rotation &&\n\t\tdest.scaleX === src.scaleX && dest.scaleY === src.scaleY) {\n\t\treturn false;\n\t}\n\tdest.x = src.x;\n\tdest.y = src.y;\n\tdest.rotation = src.rotation;\n\tdest.scaleX = src.scaleX;\n\tdest.scaleY = src.scaleY;\n\treturn true;\n}\n\n/**\n * Combine parent world transform with child local transform into child world transform.\n * Returns true if the destination was actually modified.\n */\nfunction combineTransforms(\n\tparent: WorldTransform,\n\tlocal: LocalTransform,\n\tworld: WorldTransform\n): boolean {\n\t// Apply parent's scale to local position\n\tconst scaledLocalX = local.x * parent.scaleX;\n\tconst scaledLocalY = local.y * parent.scaleY;\n\n\t// Rotate local position by parent's rotation\n\tconst cos = Math.cos(parent.rotation);\n\tconst sin = Math.sin(parent.rotation);\n\tconst rotatedX = scaledLocalX * cos - scaledLocalY * sin;\n\tconst rotatedY = scaledLocalX * sin + scaledLocalY * cos;\n\n\t// Add to parent's position\n\tconst newX = parent.x + rotatedX;\n\tconst newY = parent.y + rotatedY;\n\tconst newRotation = parent.rotation + local.rotation;\n\tconst newScaleX = parent.scaleX * local.scaleX;\n\tconst newScaleY = parent.scaleY * local.scaleY;\n\n\tif (world.x === newX && world.y === newY &&\n\t\tworld.rotation === newRotation &&\n\t\tworld.scaleX === newScaleX && world.scaleY === newScaleY) {\n\t\treturn false;\n\t}\n\n\tworld.x = newX;\n\tworld.y = newY;\n\tworld.rotation = newRotation;\n\tworld.scaleX = newScaleX;\n\tworld.scaleY = newScaleY;\n\treturn true;\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": "2PASA,uBAAS,kBAiEF,IAAM,EAAoD,CAChE,EAAG,EACH,EAAG,EACH,SAAU,EACV,OAAQ,EACR,OAAQ,CACT,EAKa,EAAoD,CAChE,EAAG,EACH,EAAG,EACH,SAAU,EACV,OAAQ,EACR,OAAQ,CACT,EAoBO,SAAS,CAAoB,CAAC,EAAW,EAA4D,CAC3G,MAAO,CACN,eAAgB,CACf,IACA,IACA,SAAU,EACV,OAAQ,EACR,OAAQ,CACT,CACD,EAWM,SAAS,CAAoB,CAAC,EAAW,EAA4D,CAC3G,MAAO,CACN,eAAgB,CACf,IACA,IACA,SAAU,EACV,OAAQ,EACR,OAAQ,CACT,CACD,EAqCM,SAAS,CAAe,CAC9B,EACA,EACA,EAC0B,CAC1B,IAAM,EAAS,GAAS,OAAS,GAAS,QAAU,EAC9C,EAAS,GAAS,OAAS,GAAS,QAAU,EAC9C,EAAW,GAAS,UAAY,EAEhC,EAAY,CACjB,IACA,IACA,WACA,SACA,QACD,EAEA,MAAO,CACN,eAAgB,IAAK,CAAU,EAC/B,eAAgB,IAAK,CAAU,CAChC,EA4BM,SAAS,CAAqD,CACpE,EACC,CACD,IACC,cAAc,YACd,WAAW,IACX,QAAQ,cACL,GAAW,CAAC,EAEhB,OAAO,EAAa,WAAW,EAC7B,mBAA4C,EAC5C,WAAoC,EACpC,WAAc,EACd,QAAQ,CAAC,IAAU,CAEnB,EAAM,iBAAiB,iBAAkB,iBAAkB,CAAC,KAAQ,CACnE,EAAG,EAAG,EAAG,EAAG,EAAG,EAAG,SAAU,EAAG,SAAU,OAAQ,EAAG,OAAQ,OAAQ,EAAG,MACxE,EAAE,EAEF,IAAM,EAA0H,CAAC,
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "2PASA,uBAAS,kBAiEF,IAAM,EAAoD,CAChE,EAAG,EACH,EAAG,EACH,SAAU,EACV,OAAQ,EACR,OAAQ,CACT,EAKa,EAAoD,CAChE,EAAG,EACH,EAAG,EACH,SAAU,EACV,OAAQ,EACR,OAAQ,CACT,EAoBO,SAAS,CAAoB,CAAC,EAAW,EAA4D,CAC3G,MAAO,CACN,eAAgB,CACf,IACA,IACA,SAAU,EACV,OAAQ,EACR,OAAQ,CACT,CACD,EAWM,SAAS,CAAoB,CAAC,EAAW,EAA4D,CAC3G,MAAO,CACN,eAAgB,CACf,IACA,IACA,SAAU,EACV,OAAQ,EACR,OAAQ,CACT,CACD,EAqCM,SAAS,CAAe,CAC9B,EACA,EACA,EAC0B,CAC1B,IAAM,EAAS,GAAS,OAAS,GAAS,QAAU,EAC9C,EAAS,GAAS,OAAS,GAAS,QAAU,EAC9C,EAAW,GAAS,UAAY,EAEhC,EAAY,CACjB,IACA,IACA,WACA,SACA,QACD,EAEA,MAAO,CACN,eAAgB,IAAK,CAAU,EAC/B,eAAgB,IAAK,CAAU,CAChC,EA4BM,SAAS,CAAqD,CACpE,EACC,CACD,IACC,cAAc,YACd,WAAW,IACX,QAAQ,cACL,GAAW,CAAC,EAEhB,OAAO,EAAa,WAAW,EAC7B,mBAA4C,EAC5C,WAAoC,EACpC,WAAc,EACd,QAAQ,CAAC,IAAU,CAEnB,EAAM,iBAAiB,iBAAkB,iBAAkB,CAAC,KAAQ,CACnE,EAAG,EAAG,EAAG,EAAG,EAAG,EAAG,SAAU,EAAG,SAAU,OAAQ,EAAG,OAAQ,OAAQ,EAAG,MACxE,EAAE,EAEF,IAAM,EAA0H,CAAC,EAC3H,EAAmB,IAAI,IAE7B,EACE,UAAU,uBAAuB,EACjC,YAAY,CAAQ,EACpB,QAAQ,CAAK,EACb,QAAQ,CAAW,EACnB,WAAW,EAAG,SAAU,CACxB,EAAoB,EAAK,EAAc,CAAgB,EACvD,EACF,EAYH,SAAS,CAAmB,CAC3B,EACA,EACA,EACO,CACP,IAAM,EAAK,EAAI,cAGf,GAAI,CAAC,EAAG,aAAc,CACrB,EAAG,yBAAyB,EAAc,CAAC,iBAAkB,gBAAgB,CAAC,EAC9E,QAAW,KAAU,EAAc,CAClC,IAAQ,iBAAgB,kBAAmB,EAAO,WAClD,GAAI,EAAc,EAAgB,CAAc,EAC/C,EAAI,YAAY,EAAO,GAAI,gBAAgB,EAG7C,OAID,EAAiB,MAAM,EAEvB,EAAI,mBAAmB,CAAC,EAAU,IAAa,CAC9C,EAAiB,IAAI,CAAQ,EAC7B,IAAM,EAAiB,EAAG,aAAa,EAAU,gBAAgB,EAC3D,EAAiB,EAAG,aAAa,EAAU,gBAAgB,EAEjE,GAAI,CAAC,GAAkB,CAAC,EAAgB,OAExC,IAAM,EAAc,IAAa,KAC9B,EAAG,aAAa,EAAU,gBAAgB,EAC1C,KAMH,GAJgB,EACb,EAAkB,EAAa,EAAgB,CAAc,EAC7D,EAAc,EAAgB,CAAc,EAElC,EAAI,YAAY,EAAU,gBAAgB,EACvD,EAED,EAAG,yBAAyB,EAAc,CAAC,iBAAkB,gBAAgB,CAAC,EAC9E,QAAW,KAAU,EAAc,CAClC,GAAI,EAAiB,IAAI,EAAO,EAAE,EAAG,SACrC,IAAQ,iBAAgB,kBAAmB,EAAO,WAClD,GAAI,EAAc,EAAgB,CAAc,EAC/C,EAAI,YAAY,EAAO,GAAI,gBAAgB,GAS9C,SAAS,CAAa,CAAC,EAAqB,EAA+B,CAC1E,GAAI,EAAK,IAAM,EAAI,GAAK,EAAK,IAAM,EAAI,GACtC,EAAK,WAAa,EAAI,UACtB,EAAK,SAAW,EAAI,QAAU,EAAK,SAAW,EAAI,OAClD,MAAO,GAOR,OALA,EAAK,EAAI,EAAI,EACb,EAAK,EAAI,EAAI,EACb,EAAK,SAAW,EAAI,SACpB,EAAK,OAAS,EAAI,OAClB,EAAK,OAAS,EAAI,OACX,GAOR,SAAS,CAAiB,CACzB,EACA,EACA,EACU,CAEV,IAAM,EAAe,EAAM,EAAI,EAAO,OAChC,EAAe,EAAM,EAAI,EAAO,OAGhC,EAAM,KAAK,IAAI,EAAO,QAAQ,EAC9B,EAAM,KAAK,IAAI,EAAO,QAAQ,EAC9B,EAAW,EAAe,EAAM,EAAe,EAC/C,EAAW,EAAe,EAAM,EAAe,EAG/C,EAAO,EAAO,EAAI,EAClB,EAAO,EAAO,EAAI,EAClB,EAAc,EAAO,SAAW,EAAM,SACtC,EAAY,EAAO,OAAS,EAAM,OAClC,EAAY,EAAO,OAAS,EAAM,OAExC,GAAI,EAAM,IAAM,GAAQ,EAAM,IAAM,GACnC,EAAM,WAAa,GACnB,EAAM,SAAW,GAAa,EAAM,SAAW,EAC/C,MAAO,GAQR,OALA,EAAM,EAAI,EACV,EAAM,EAAI,EACV,EAAM,SAAW,EACjB,EAAM,OAAS,EACf,EAAM,OAAS,EACR",
|
|
8
|
+
"debugId": "DD4C20CA5FEDDFB464756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -41,6 +41,8 @@ export default class ResourceManager<ResourceTypes extends Record<string, any> =
|
|
|
41
41
|
private resourceDisposers;
|
|
42
42
|
private initializedResourceKeys;
|
|
43
43
|
private _changeSubscribers;
|
|
44
|
+
/** Shallow snapshots of observed resources, keyed by resource key */
|
|
45
|
+
private _observedSnapshots;
|
|
44
46
|
/**
|
|
45
47
|
* Add a resource to the manager.
|
|
46
48
|
*
|
|
@@ -132,6 +134,15 @@ export default class ResourceManager<ResourceTypes extends Record<string, any> =
|
|
|
132
134
|
disposeResource<K extends keyof ResourceTypes>(label: K, ...args: ContextArgs<Context>): Promise<boolean>;
|
|
133
135
|
/**
|
|
134
136
|
* Subscribe to changes for a specific resource key.
|
|
137
|
+
*
|
|
138
|
+
* Subscribing marks the resource as "observed." Observed resources:
|
|
139
|
+
* - Are re-resolved each frame by `withResources` (no stale cache)
|
|
140
|
+
* - Are shallow-diffed at the end of each frame via `flushObserved()`,
|
|
141
|
+
* so in-place mutations are detected and subscribers notified
|
|
142
|
+
*
|
|
143
|
+
* When the last subscriber unsubscribes, the resource reverts to
|
|
144
|
+
* normal (cached, no per-frame diff).
|
|
145
|
+
*
|
|
135
146
|
* @param key The resource key to watch
|
|
136
147
|
* @param callback Function called with (newValue, oldValue) when the resource changes
|
|
137
148
|
* @returns Unsubscribe function
|
|
@@ -145,6 +156,19 @@ export default class ResourceManager<ResourceTypes extends Record<string, any> =
|
|
|
145
156
|
* @param oldValue The previous resource value
|
|
146
157
|
*/
|
|
147
158
|
notifyChange<K extends keyof ResourceTypes>(key: K, newValue: ResourceTypes[K], oldValue: ResourceTypes[K]): void;
|
|
159
|
+
/**
|
|
160
|
+
* Whether a resource has active change subscribers.
|
|
161
|
+
* Observed resources should not be cached by systems — they need
|
|
162
|
+
* to be re-resolved each frame so external mutations are visible.
|
|
163
|
+
*/
|
|
164
|
+
isObserved<K extends keyof ResourceTypes>(key: K): boolean;
|
|
165
|
+
/**
|
|
166
|
+
* Diff all observed resources against their snapshots.
|
|
167
|
+
* Fires subscribers for any resource whose shallow properties changed
|
|
168
|
+
* since the last snapshot, then updates the snapshot.
|
|
169
|
+
* Call once per frame after all systems have run.
|
|
170
|
+
*/
|
|
171
|
+
flushObserved(): void;
|
|
148
172
|
/**
|
|
149
173
|
* Dispose all initialized resources in reverse dependency order.
|
|
150
174
|
* Resources that depend on others are disposed first.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ecspresso",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.4",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"module": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -24,6 +24,14 @@
|
|
|
24
24
|
"import": "./dist/plugins/spatial/camera.js",
|
|
25
25
|
"types": "./dist/plugins/spatial/camera.d.ts"
|
|
26
26
|
},
|
|
27
|
+
"./plugins/isometric/projection": {
|
|
28
|
+
"import": "./dist/plugins/isometric/projection.js",
|
|
29
|
+
"types": "./dist/plugins/isometric/projection.d.ts"
|
|
30
|
+
},
|
|
31
|
+
"./plugins/isometric/depth-sort": {
|
|
32
|
+
"import": "./dist/plugins/isometric/depth-sort.js",
|
|
33
|
+
"types": "./dist/plugins/isometric/depth-sort.d.ts"
|
|
34
|
+
},
|
|
27
35
|
"./plugins/spatial/spatial-index": {
|
|
28
36
|
"import": "./dist/plugins/spatial/spatial-index.js",
|
|
29
37
|
"types": "./dist/plugins/spatial/spatial-index.d.ts"
|
|
@@ -88,6 +96,10 @@
|
|
|
88
96
|
"import": "./dist/plugins/ai/detection.js",
|
|
89
97
|
"types": "./dist/plugins/ai/detection.d.ts"
|
|
90
98
|
},
|
|
99
|
+
"./plugins/ai/flocking": {
|
|
100
|
+
"import": "./dist/plugins/ai/flocking.js",
|
|
101
|
+
"types": "./dist/plugins/ai/flocking.d.ts"
|
|
102
|
+
},
|
|
91
103
|
"./plugins/combat/health": {
|
|
92
104
|
"import": "./dist/plugins/combat/health.js",
|
|
93
105
|
"types": "./dist/plugins/combat/health.d.ts"
|
|
@@ -96,13 +108,9 @@
|
|
|
96
108
|
"import": "./dist/plugins/combat/projectile.js",
|
|
97
109
|
"types": "./dist/plugins/combat/projectile.d.ts"
|
|
98
110
|
},
|
|
99
|
-
"./
|
|
100
|
-
"import": "./dist/
|
|
101
|
-
"types": "./dist/
|
|
102
|
-
},
|
|
103
|
-
"./plugins/isometric/depth-sort": {
|
|
104
|
-
"import": "./dist/plugins/isometric/depth-sort.js",
|
|
105
|
-
"types": "./dist/plugins/isometric/depth-sort.d.ts"
|
|
111
|
+
"./bindings/react": {
|
|
112
|
+
"import": "./dist/bindings/react/index.js",
|
|
113
|
+
"types": "./dist/bindings/react/index.d.ts"
|
|
106
114
|
}
|
|
107
115
|
},
|
|
108
116
|
"publishConfig": {
|
|
@@ -125,11 +133,15 @@
|
|
|
125
133
|
"devDependencies": {
|
|
126
134
|
"@types/bun": "latest",
|
|
127
135
|
"@types/howler": "^2.2.12",
|
|
136
|
+
"@types/react": "^19.2.14",
|
|
137
|
+
"@types/react-dom": "^19.2.3",
|
|
128
138
|
"@types/three": "^0.183.1",
|
|
129
139
|
"howler": "^2.2.4",
|
|
130
|
-
"pixi.js": "^8.
|
|
140
|
+
"pixi.js": "^8.18.1",
|
|
141
|
+
"react": "^19.2.5",
|
|
142
|
+
"react-dom": "^19.2.5",
|
|
131
143
|
"three": "^0.183.2",
|
|
132
|
-
"typedoc": "^0.28.
|
|
144
|
+
"typedoc": "^0.28.19"
|
|
133
145
|
},
|
|
134
146
|
"peerDependencies": {
|
|
135
147
|
"typescript": "^6.0.2",
|