ecspresso 0.17.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +64 -0
- package/README.md +2 -0
- package/dist/asset-manager.d.ts +8 -8
- package/dist/asset-types.d.ts +9 -5
- package/dist/ecspresso-builder.d.ts +5 -5
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/index.js.map +7 -7
- package/dist/plugins/ai/behavior-tree.d.ts +2 -2
- package/dist/plugins/ai/behavior-tree.js.map +2 -2
- package/dist/plugins/ai/detection.d.ts +3 -3
- package/dist/plugins/ai/detection.js.map +2 -2
- package/dist/plugins/ai/flocking.d.ts +3 -3
- package/dist/plugins/ai/flocking.js.map +1 -1
- package/dist/plugins/ai/pathfinding.d.ts +3 -4
- package/dist/plugins/ai/pathfinding.js.map +2 -2
- package/dist/plugins/combat/health.d.ts +2 -2
- package/dist/plugins/combat/health.js.map +2 -2
- package/dist/plugins/combat/projectile.d.ts +3 -3
- package/dist/plugins/combat/projectile.js.map +2 -2
- package/dist/plugins/input/selection.d.ts +3 -3
- package/dist/plugins/input/selection.js.map +3 -3
- package/dist/plugins/isometric/depth-sort.d.ts +2 -2
- package/dist/plugins/isometric/depth-sort.js.map +2 -2
- package/dist/plugins/isometric/projection.d.ts +2 -2
- package/dist/plugins/isometric/projection.js.map +2 -2
- package/dist/plugins/physics/steering.d.ts +2 -2
- package/dist/plugins/physics/steering.js.map +2 -2
- package/dist/plugins/rendering/particles.d.ts +2 -2
- package/dist/plugins/rendering/particles.js.map +1 -1
- package/dist/plugins/rendering/renderer2D.d.ts +6 -5
- package/dist/plugins/rendering/renderer2D.js.map +2 -2
- package/dist/plugins/rendering/renderer3D.d.ts +3 -2
- package/dist/plugins/rendering/renderer3D.js.map +2 -2
- package/dist/plugins/rendering/sprite-animation.d.ts +110 -0
- package/dist/plugins/rendering/sprite-animation.js +2 -2
- package/dist/plugins/rendering/sprite-animation.js.map +3 -3
- package/dist/plugins/rendering/tilemap.d.ts +3 -3
- package/dist/plugins/rendering/tilemap.js.map +3 -3
- package/dist/plugins/spatial/camera.js.map +2 -2
- package/dist/plugins/spatial/camera3D.d.ts +3 -3
- package/dist/plugins/spatial/camera3D.js.map +2 -2
- package/dist/plugins/spatial/transform.d.ts +2 -2
- package/dist/plugins/spatial/transform.js.map +1 -1
- package/dist/plugins/spatial/transform3D.d.ts +2 -2
- package/dist/plugins/spatial/transform3D.js.map +1 -1
- package/dist/plugins/ui/ui.d.ts +2 -2
- package/dist/plugins/ui/ui.js.map +3 -3
- package/dist/screen-types.d.ts +7 -3
- package/dist/system-builder.d.ts +5 -5
- package/dist/type-utils.d.ts +20 -0
- package/dist/types.d.ts +1 -1
- package/package.json +5 -4
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/plugins/ai/pathfinding.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * Pathfinding Plugin for ECSpresso\n *\n * A* pathfinding on a weighted grid. Produces waypoint lists consumed by the\n * steering plugin — the pathfinding system writes the `path` component and\n * sets `moveTarget` to the first waypoint; the waypoint advancement handler\n * listens for `arriveAtTarget` and advances to the next waypoint.\n *\n * Exports the pure `findPath(grid, start, goal, options?)` function for\n * turn-based / non-realtime consumers that don't need the component dance.\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type { WorldConfigFrom } from 'ecspresso';\nimport type { Vector2D } from '../../utils/math';\nimport type { TransformWorldConfig } from '../spatial/transform';\nimport type { SteeringWorldConfig } from '../physics/steering';\n\n// ==================== Topology / Grid Types ====================\n\n/** Flat-indexed cell position in a `NavGrid`. Transparent alias, not branded. */\nexport type CellIndex = number;\n\n/**\n * Grid topology. v1 ships `square4`; other values are accepted at construction\n * but throw at `findPath` time.\n */\nexport type NavGridTopology = 'square4' | 'square8' | 'hex-pointy' | 'hex-flat';\n\n/**\n * Weighted navigation grid. Row-major storage (`idx = row * width + col`).\n * Cell value `0` = impassable, `1`–`255` = traversal cost into that cell.\n */\nexport interface NavGrid {\n\treadonly topology: NavGridTopology;\n\treadonly width: number;\n\treadonly height: number;\n\treadonly cellSize: number;\n\treadonly originX: number;\n\treadonly originY: number;\n\treadonly cells: Uint8Array;\n\tworldToCell(wx: number, wy: number): CellIndex;\n\tcellToWorld(idx: CellIndex): Vector2D;\n\tcellFromXY(x: number, y: number): CellIndex;\n\tcellToXY(idx: CellIndex): { x: number; y: number };\n}\n\n/** Options accepted by `createNavGrid`. */\nexport interface CreateNavGridOptions {\n\ttopology?: NavGridTopology;\n\twidth: number;\n\theight: number;\n\tcellSize?: number;\n\toriginX?: number;\n\toriginY?: number;\n\tcells?: Uint8Array;\n\tdefaultCost?: number;\n}\n\n// ==================== Component Types ====================\n\n/** Signals the pathfinding system to compute a route to `target`. */\nexport interface PathRequest {\n\ttarget: Vector2D;\n}\n\n/** Active route; waypoints are in world-space, advanced by `currentIndex`. */\nexport interface Path {\n\twaypoints: Vector2D[];\n\tcurrentIndex: number;\n}\n\n/** Component types provided by the pathfinding plugin. */\nexport interface PathfindingComponentTypes {\n\tpathRequest: PathRequest;\n\tpath: Path;\n}\n\n// ==================== Event Types ====================\n\n/** Fired when A* produces a route. `path` is empty when start is already at the goal. */\nexport interface PathFoundEvent {\n\tentityId: number;\n\tpath: Vector2D[];\n}\n\n/** Fired when no path exists to the target. */\nexport interface PathBlockedEvent {\n\tentityId: number;\n}\n\n/** Event types provided by the pathfinding plugin. */\nexport interface PathfindingEventTypes {\n\tpathFound: PathFoundEvent;\n\tpathBlocked: PathBlockedEvent;\n}\n\n// ==================== Resource Types ====================\n\n/** Resource types provided by the pathfinding plugin. */\nexport interface PathfindingResourceTypes {\n\tnavGrid: NavGrid;\n}\n\n// ==================== WorldConfig ====================\n\n/** WorldConfig representing the pathfinding plugin's provided types. */\nexport type PathfindingWorldConfig = WorldConfigFrom<\n\tPathfindingComponentTypes,\n\tPathfindingEventTypes,\n\tPathfindingResourceTypes\n>;\n\n// ==================== Plugin Options ====================\n\nexport interface PathfindingPluginOptions<G extends string = 'ai'> extends BasePluginOptions<G> {\n\t/** The navigation grid. Construct via `createNavGrid`. */\n\tgrid: NavGrid;\n\t/** Max path requests processed per frame (default 4). */\n\tmaxRequestsPerFrame?: number;\n\t/** Default `maxNodesExpanded` passed to A* per request (default 10_000). */\n\tmaxNodesExpanded?: number;\n}\n\n// ==================== NavGrid Construction ====================\n\ninterface TopologyOps {\n\tneighbors(grid: NavGrid, idx: CellIndex, out: number[]): number;\n\tstepCost(grid: NavGrid, from: CellIndex, to: CellIndex): number;\n\theuristic(grid: NavGrid, a: CellIndex, b: CellIndex): number;\n}\n\nconst square4Ops: TopologyOps = {\n\tneighbors(grid, idx, out) {\n\t\tconst col = idx % grid.width;\n\t\tconst row = (idx - col) / grid.width;\n\t\tlet count = 0;\n\t\tif (col > 0) out[count++] = idx - 1;\n\t\tif (col < grid.width - 1) out[count++] = idx + 1;\n\t\tif (row > 0) out[count++] = idx - grid.width;\n\t\tif (row < grid.height - 1) out[count++] = idx + grid.width;\n\t\treturn count;\n\t},\n\tstepCost(grid, _from, to) {\n\t\treturn grid.cells[to] ?? 0;\n\t},\n\theuristic(grid, a, b) {\n\t\tconst ax = a % grid.width;\n\t\tconst ay = (a - ax) / grid.width;\n\t\tconst bx = b % grid.width;\n\t\tconst by = (b - bx) / grid.width;\n\t\treturn Math.abs(ax - bx) + Math.abs(ay - by);\n\t},\n};\n\nconst unimplementedOps = (topology: NavGridTopology): TopologyOps => {\n\tconst err = (): never => {\n\t\tthrow new Error(`pathfinding: topology '${topology}' is not implemented in v1`);\n\t};\n\treturn {\n\t\tneighbors: err,\n\t\tstepCost: err,\n\t\theuristic: err,\n\t};\n};\n\nconst topologyOps: Readonly<Record<NavGridTopology, TopologyOps>> = Object.freeze({\n\t'square4': square4Ops,\n\t'square8': unimplementedOps('square8'),\n\t'hex-pointy': unimplementedOps('hex-pointy'),\n\t'hex-flat': unimplementedOps('hex-flat'),\n});\n\n/**\n * Create a weighted navigation grid.\n *\n * @example\n * ```typescript\n * const grid = createNavGrid({ width: 32, height: 32, cellSize: 16 });\n * grid.cells[grid.cellFromXY(5, 5)] = 0; // block a cell\n * ```\n */\nexport function createNavGrid(options: CreateNavGridOptions): NavGrid {\n\tconst topology = options.topology ?? 'square4';\n\tconst cellSize = options.cellSize ?? 32;\n\tconst originX = options.originX ?? 0;\n\tconst originY = options.originY ?? 0;\n\tconst { width, height } = options;\n\tconst defaultCost = options.defaultCost ?? 1;\n\n\tif (!Number.isInteger(width) || width <= 0) {\n\t\tthrow new Error(`pathfinding: width must be a positive integer, got ${width}`);\n\t}\n\tif (!Number.isInteger(height) || height <= 0) {\n\t\tthrow new Error(`pathfinding: height must be a positive integer, got ${height}`);\n\t}\n\tif (cellSize <= 0) {\n\t\tthrow new Error(`pathfinding: cellSize must be > 0, got ${cellSize}`);\n\t}\n\tif (defaultCost < 0 || defaultCost > 255) {\n\t\tthrow new Error(`pathfinding: defaultCost must be in 0–255, got ${defaultCost}`);\n\t}\n\n\tconst expectedLen = width * height;\n\tconst cells = options.cells ?? new Uint8Array(expectedLen).fill(defaultCost);\n\tif (cells.length !== expectedLen) {\n\t\tthrow new Error(\n\t\t\t`pathfinding: cells length ${cells.length} does not match width*height ${expectedLen}`,\n\t\t);\n\t}\n\n\tconst invCellSize = 1 / cellSize;\n\n\tconst worldToCell = (wx: number, wy: number): CellIndex => {\n\t\tconst col = Math.floor((wx - originX) * invCellSize);\n\t\tconst row = Math.floor((wy - originY) * invCellSize);\n\t\tconst cCol = col < 0 ? 0 : col >= width ? width - 1 : col;\n\t\tconst cRow = row < 0 ? 0 : row >= height ? height - 1 : row;\n\t\treturn cRow * width + cCol;\n\t};\n\n\tconst cellToWorld = (idx: CellIndex): Vector2D => {\n\t\tconst col = idx % width;\n\t\tconst row = (idx - col) / width;\n\t\treturn {\n\t\t\tx: originX + (col + 0.5) * cellSize,\n\t\t\ty: originY + (row + 0.5) * cellSize,\n\t\t};\n\t};\n\n\tconst cellFromXY = (x: number, y: number): CellIndex => y * width + x;\n\n\tconst cellToXY = (idx: CellIndex): { x: number; y: number } => {\n\t\tconst x = idx % width;\n\t\treturn { x, y: (idx - x) / width };\n\t};\n\n\treturn {\n\t\ttopology, width, height, cellSize, originX, originY, cells,\n\t\tworldToCell, cellToWorld, cellFromXY, cellToXY,\n\t};\n}\n\n// ==================== Helper Functions ====================\n\n/**\n * Create a `pathRequest` component for spreading into `spawn()` / `addComponent()`.\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createTransform(0, 0),\n * ...createMoveSpeed(100),\n * ...createPathRequest({ x: 200, y: 300 }),\n * });\n * ```\n */\nexport function createPathRequest(target: Vector2D): Pick<PathfindingComponentTypes, 'pathRequest'> {\n\treturn { pathRequest: { target: { x: target.x, y: target.y } } };\n}\n\n// ==================== Pure A* API ====================\n\nexport interface FindPathOptions {\n\t/** Cap on A* node expansions; returns `null` if exceeded. Default 10_000. */\n\tmaxNodesExpanded?: number;\n\t/** Dynamic per-call obstacles layered on top of the static grid. */\n\tblockedCells?: Set<CellIndex>;\n\t/** Accept arrival within N cells of goal (topology-aware distance). Default 0. */\n\tgoalTolerance?: number;\n}\n\ninterface PathHeap {\n\tids: Int32Array;\n\tpriorities: Float32Array;\n\tsize: number;\n}\n\n// Why: parallel-typed-array heap keeps cells & priorities in cache without per-node allocations.\nfunction heapPush(heap: PathHeap, id: number, priority: number): void {\n\tlet i = heap.size;\n\theap.size = i + 1;\n\twhile (i > 0) {\n\t\tconst parent = (i - 1) >> 1;\n\t\tif ((heap.priorities[parent] ?? 0) <= priority) break;\n\t\theap.ids[i] = heap.ids[parent] ?? 0;\n\t\theap.priorities[i] = heap.priorities[parent] ?? 0;\n\t\ti = parent;\n\t}\n\theap.ids[i] = id;\n\theap.priorities[i] = priority;\n}\n\nfunction heapPop(heap: PathHeap): number {\n\tconst top = heap.ids[0] ?? -1;\n\tconst last = heap.size - 1;\n\theap.size = last;\n\tif (last <= 0) return top;\n\tconst movedId = heap.ids[last] ?? 0;\n\tconst movedPri = heap.priorities[last] ?? 0;\n\tlet i = 0;\n\tconst half = last >> 1;\n\twhile (i < half) {\n\t\tlet child = (i << 1) + 1;\n\t\tconst right = child + 1;\n\t\tif (right < last && (heap.priorities[right] ?? 0) < (heap.priorities[child] ?? 0)) child = right;\n\t\tif ((heap.priorities[child] ?? 0) >= movedPri) break;\n\t\theap.ids[i] = heap.ids[child] ?? 0;\n\t\theap.priorities[i] = heap.priorities[child] ?? 0;\n\t\ti = child;\n\t}\n\theap.ids[i] = movedId;\n\theap.priorities[i] = movedPri;\n\treturn top;\n}\n\nfunction reconstructPath(cameFrom: Int32Array, end: CellIndex): CellIndex[] {\n\t// Why: two-pass (count then fill) avoids unshift/reverse allocation.\n\tlet count = 1;\n\tlet cur = end;\n\twhile ((cameFrom[cur] ?? -1) !== -1) {\n\t\tcount++;\n\t\tcur = cameFrom[cur] ?? -1;\n\t}\n\tconst path = new Array<CellIndex>(count);\n\tcur = end;\n\tfor (let i = count - 1; i >= 0; i--) {\n\t\tpath[i] = cur;\n\t\tif (i > 0) cur = cameFrom[cur] ?? -1;\n\t}\n\treturn path;\n}\n\n/**\n * Compute a path through `grid` from `start` to `goal`.\n *\n * Returns a list of cell indices starting with `start` and ending at a cell\n * within `goalTolerance` of `goal`, or `null` if no such path exists within\n * `maxNodesExpanded` expansions.\n *\n * `start` is always treated as passable (even if its grid cell is 0 or the\n * cell is in `blockedCells`) — actors physics-pushed onto a wall still get a\n * valid origin.\n */\nexport function findPath(\n\tgrid: NavGrid,\n\tstart: CellIndex,\n\tgoal: CellIndex,\n\toptions?: FindPathOptions,\n): CellIndex[] | null {\n\tconst n = grid.cells.length;\n\tif (start < 0 || start >= n) return null;\n\tif (goal < 0 || goal >= n) return null;\n\n\tconst maxNodesExpanded = options?.maxNodesExpanded ?? 10_000;\n\tconst blockedCells = options?.blockedCells;\n\tconst goalTolerance = options?.goalTolerance ?? 0;\n\tconst ops = topologyOps[grid.topology];\n\n\t// Per-call allocations: ~n bytes × 5 (gScore, cameFrom, closed, heap ids, heap priorities).\n\t// For a 100×100 grid that's ~120 KB per search. Acceptable for v1 game-grid scales.\n\t// Deferred optimization: closure-scoped reusable pool keyed by `n`, reset per call.\n\tconst gScore = new Float32Array(n);\n\tgScore.fill(Number.POSITIVE_INFINITY);\n\tconst cameFrom = new Int32Array(n);\n\tcameFrom.fill(-1);\n\tconst closed = new Uint8Array(n);\n\tconst heap: PathHeap = {\n\t\tids: new Int32Array(n),\n\t\tpriorities: new Float32Array(n),\n\t\tsize: 0,\n\t};\n\tconst neighborBuf: number[] = [];\n\n\tgScore[start] = 0;\n\theapPush(heap, start, ops.heuristic(grid, start, goal));\n\n\tlet expanded = 0;\n\twhile (heap.size > 0) {\n\t\tif (expanded >= maxNodesExpanded) return null;\n\t\tconst current = heapPop(heap);\n\t\tif (closed[current]) continue;\n\t\tclosed[current] = 1;\n\t\texpanded++;\n\n\t\tif (ops.heuristic(grid, current, goal) <= goalTolerance) {\n\t\t\treturn reconstructPath(cameFrom, current);\n\t\t}\n\n\t\tneighborBuf.length = 0;\n\t\tconst count = ops.neighbors(grid, current, neighborBuf);\n\t\tfor (let k = 0; k < count; k++) {\n\t\t\tconst next = neighborBuf[k] ?? -1;\n\t\t\tif (next < 0 || closed[next]) continue;\n\t\t\tconst cellCost = grid.cells[next] ?? 0;\n\t\t\tif (cellCost === 0) continue;\n\t\t\tif (blockedCells && blockedCells.has(next)) continue;\n\n\t\t\tconst tentative = (gScore[current] ?? Number.POSITIVE_INFINITY) + ops.stepCost(grid, current, next);\n\t\t\tif (tentative < (gScore[next] ?? Number.POSITIVE_INFINITY)) {\n\t\t\t\tgScore[next] = tentative;\n\t\t\t\tcameFrom[next] = current;\n\t\t\t\theapPush(heap, next, tentative + ops.heuristic(grid, next, goal));\n\t\t\t}\n\t\t}\n\t}\n\treturn null;\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a pathfinding plugin for ECSpresso.\n *\n * Requires the transform and steering plugins to be installed (entities need\n * `worldTransform` for start-cell detection and `moveTarget`/`moveSpeed` for\n * waypoint traversal).\n *\n * @example\n * ```typescript\n * const grid = createNavGrid({ width: 32, height: 32, cellSize: 16 });\n * const ecs = ECSpresso.create()\n * .withPlugin(createTransformPlugin())\n * .withPlugin(createSteeringPlugin())\n * .withPlugin(createPathfindingPlugin({ grid }))\n * .build();\n *\n * ecs.spawn({\n * ...createTransform(0, 0),\n * ...createMoveSpeed(100),\n * ...createPathRequest({ x: 500, y: 300 }),\n * });\n * ```\n */\nexport function createPathfindingPlugin<G extends string = 'ai'>(\n\toptions: PathfindingPluginOptions<G>,\n) {\n\tconst {\n\t\tgrid,\n\t\tsystemGroup = 'ai' as G,\n\t\tpriority = 150,\n\t\tphase = 'update',\n\t\tmaxRequestsPerFrame = 4,\n\t\tmaxNodesExpanded = 10_000,\n\t} = options;\n\n\treturn definePlugin('pathfinding')\n\t\t.withComponentTypes<PathfindingComponentTypes>()\n\t\t.withEventTypes<PathfindingEventTypes>()\n\t\t.withResourceTypes<PathfindingResourceTypes>()\n\t\t.withLabels<'pathfinding-request' | 'pathfinding-waypoint-advance'>()\n\t\t.withGroups<G>()\n\t\t.requires<TransformWorldConfig & SteeringWorldConfig>()\n\t\t.install((world) => {\n\t\t\tworld.addResource('navGrid', grid);\n\n\t\t\tworld\n\t\t\t\t.addSystem('pathfinding-request')\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('requests', {\n\t\t\t\t\twith: ['pathRequest', 'worldTransform'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, ecs }) => {\n\t\t\t\t\tconst navGrid = ecs.getResource('navGrid');\n\t\t\t\t\tlet processed = 0;\n\t\t\t\t\tfor (const entity of queries.requests) {\n\t\t\t\t\t\tif (processed >= maxRequestsPerFrame) break;\n\t\t\t\t\t\tprocessed++;\n\t\t\t\t\t\tconst { pathRequest, worldTransform } = entity.components;\n\t\t\t\t\t\tconst startIdx = navGrid.worldToCell(worldTransform.x, worldTransform.y);\n\t\t\t\t\t\tconst goalIdx = navGrid.worldToCell(pathRequest.target.x, pathRequest.target.y);\n\t\t\t\t\t\tconst result = findPath(navGrid, startIdx, goalIdx, { maxNodesExpanded });\n\t\t\t\t\t\tecs.commands.removeComponent(entity.id, 'pathRequest');\n\n\t\t\t\t\t\tif (result === null) {\n\t\t\t\t\t\t\tecs.eventBus.publish('pathBlocked', { entityId: entity.id });\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst waypoints = result.slice(1).map(idx => navGrid.cellToWorld(idx));\n\t\t\t\t\t\tecs.eventBus.publish('pathFound', { entityId: entity.id, path: waypoints });\n\t\t\t\t\t\tif (waypoints.length === 0) continue;\n\n\t\t\t\t\t\tconst existingPath = ecs.getComponent(entity.id, 'path');\n\t\t\t\t\t\tif (existingPath) {\n\t\t\t\t\t\t\texistingPath.waypoints = waypoints;\n\t\t\t\t\t\t\texistingPath.currentIndex = 0;\n\t\t\t\t\t\t\tecs.markChanged(entity.id, 'path');\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tecs.addComponent(entity.id, 'path', { waypoints, currentIndex: 0 });\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst first = waypoints[0];\n\t\t\t\t\t\tif (!first) continue;\n\t\t\t\t\t\tconst existingMT = ecs.getComponent(entity.id, 'moveTarget');\n\t\t\t\t\t\tif (existingMT) {\n\t\t\t\t\t\t\texistingMT.x = first.x;\n\t\t\t\t\t\t\texistingMT.y = first.y;\n\t\t\t\t\t\t\tecs.markChanged(entity.id, 'moveTarget');\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tecs.addComponent(entity.id, 'moveTarget', { x: first.x, y: first.y });\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\tworld\n\t\t\t\t.addSystem('pathfinding-waypoint-advance')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setEventHandlers({\n\t\t\t\t\tarriveAtTarget({ data, ecs }) {\n\t\t\t\t\t\tconst path = ecs.getComponent(data.entityId, 'path');\n\t\t\t\t\t\tif (!path) return;\n\t\t\t\t\t\tconst next = path.currentIndex + 1;\n\t\t\t\t\t\tif (next >= path.waypoints.length) {\n\t\t\t\t\t\t\tecs.commands.removeComponent(data.entityId, 'path');\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tpath.currentIndex = next;\n\t\t\t\t\t\tecs.markChanged(data.entityId, 'path');\n\t\t\t\t\t\tconst wp = path.waypoints[next];\n\t\t\t\t\t\tif (!wp) return;\n\t\t\t\t\t\t// Why: use command buffer so the add is queued AFTER steering's\n\t\t\t\t\t\t// queued `removeComponent('moveTarget')` in the same phase.\n\t\t\t\t\t\tecs.commands.addComponent(data.entityId, 'moveTarget', { x: wp.x, y: wp.y });\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t});\n}\n"
|
|
5
|
+
"/**\n * Pathfinding Plugin for ECSpresso\n *\n * A* pathfinding on a weighted grid. Produces waypoint lists consumed by the\n * steering plugin — the pathfinding system writes the `path` component and\n * sets `moveTarget` to the first waypoint; the waypoint advancement handler\n * listens for `arriveAtTarget` and advances to the next waypoint.\n *\n * Exports the pure `findPath(grid, start, goal, options?)` function for\n * turn-based / non-realtime consumers that don't need the component dance.\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type { ComponentsConfig, EventsConfig, ResourcesConfig } from 'ecspresso';\nimport type { Vector2D } from '../../utils/math';\nimport type { TransformWorldConfig } from '../spatial/transform';\nimport type { SteeringWorldConfig } from '../physics/steering';\n\n// ==================== Topology / Grid Types ====================\n\n/** Flat-indexed cell position in a `NavGrid`. Transparent alias, not branded. */\nexport type CellIndex = number;\n\n/**\n * Grid topology. v1 ships `square4`; other values are accepted at construction\n * but throw at `findPath` time.\n */\nexport type NavGridTopology = 'square4' | 'square8' | 'hex-pointy' | 'hex-flat';\n\n/**\n * Weighted navigation grid. Row-major storage (`idx = row * width + col`).\n * Cell value `0` = impassable, `1`–`255` = traversal cost into that cell.\n */\nexport interface NavGrid {\n\treadonly topology: NavGridTopology;\n\treadonly width: number;\n\treadonly height: number;\n\treadonly cellSize: number;\n\treadonly originX: number;\n\treadonly originY: number;\n\treadonly cells: Uint8Array;\n\tworldToCell(wx: number, wy: number): CellIndex;\n\tcellToWorld(idx: CellIndex): Vector2D;\n\tcellFromXY(x: number, y: number): CellIndex;\n\tcellToXY(idx: CellIndex): { x: number; y: number };\n}\n\n/** Options accepted by `createNavGrid`. */\nexport interface CreateNavGridOptions {\n\ttopology?: NavGridTopology;\n\twidth: number;\n\theight: number;\n\tcellSize?: number;\n\toriginX?: number;\n\toriginY?: number;\n\tcells?: Uint8Array;\n\tdefaultCost?: number;\n}\n\n// ==================== Component Types ====================\n\n/** Signals the pathfinding system to compute a route to `target`. */\nexport interface PathRequest {\n\ttarget: Vector2D;\n}\n\n/** Active route; waypoints are in world-space, advanced by `currentIndex`. */\nexport interface Path {\n\twaypoints: Vector2D[];\n\tcurrentIndex: number;\n}\n\n/** Component types provided by the pathfinding plugin. */\nexport interface PathfindingComponentTypes {\n\tpathRequest: PathRequest;\n\tpath: Path;\n}\n\n// ==================== Event Types ====================\n\n/** Fired when A* produces a route. `path` is empty when start is already at the goal. */\nexport interface PathFoundEvent {\n\tentityId: number;\n\tpath: Vector2D[];\n}\n\n/** Fired when no path exists to the target. */\nexport interface PathBlockedEvent {\n\tentityId: number;\n}\n\n/** Event types provided by the pathfinding plugin. */\nexport interface PathfindingEventTypes {\n\tpathFound: PathFoundEvent;\n\tpathBlocked: PathBlockedEvent;\n}\n\n// ==================== Resource Types ====================\n\n/** Resource types provided by the pathfinding plugin. */\nexport interface PathfindingResourceTypes {\n\tnavGrid: NavGrid;\n}\n\n// ==================== WorldConfig ====================\n\n/** WorldConfig representing the pathfinding plugin's provided types. */\nexport type PathfindingWorldConfig =\n\tComponentsConfig<PathfindingComponentTypes>\n\t& EventsConfig<PathfindingEventTypes>\n\t& ResourcesConfig<PathfindingResourceTypes>;\n\n// ==================== Plugin Options ====================\n\nexport interface PathfindingPluginOptions<G extends string = 'ai'> extends BasePluginOptions<G> {\n\t/** The navigation grid. Construct via `createNavGrid`. */\n\tgrid: NavGrid;\n\t/** Max path requests processed per frame (default 4). */\n\tmaxRequestsPerFrame?: number;\n\t/** Default `maxNodesExpanded` passed to A* per request (default 10_000). */\n\tmaxNodesExpanded?: number;\n}\n\n// ==================== NavGrid Construction ====================\n\ninterface TopologyOps {\n\tneighbors(grid: NavGrid, idx: CellIndex, out: number[]): number;\n\tstepCost(grid: NavGrid, from: CellIndex, to: CellIndex): number;\n\theuristic(grid: NavGrid, a: CellIndex, b: CellIndex): number;\n}\n\nconst square4Ops: TopologyOps = {\n\tneighbors(grid, idx, out) {\n\t\tconst col = idx % grid.width;\n\t\tconst row = (idx - col) / grid.width;\n\t\tlet count = 0;\n\t\tif (col > 0) out[count++] = idx - 1;\n\t\tif (col < grid.width - 1) out[count++] = idx + 1;\n\t\tif (row > 0) out[count++] = idx - grid.width;\n\t\tif (row < grid.height - 1) out[count++] = idx + grid.width;\n\t\treturn count;\n\t},\n\tstepCost(grid, _from, to) {\n\t\treturn grid.cells[to] ?? 0;\n\t},\n\theuristic(grid, a, b) {\n\t\tconst ax = a % grid.width;\n\t\tconst ay = (a - ax) / grid.width;\n\t\tconst bx = b % grid.width;\n\t\tconst by = (b - bx) / grid.width;\n\t\treturn Math.abs(ax - bx) + Math.abs(ay - by);\n\t},\n};\n\nconst unimplementedOps = (topology: NavGridTopology): TopologyOps => {\n\tconst err = (): never => {\n\t\tthrow new Error(`pathfinding: topology '${topology}' is not implemented in v1`);\n\t};\n\treturn {\n\t\tneighbors: err,\n\t\tstepCost: err,\n\t\theuristic: err,\n\t};\n};\n\nconst topologyOps: Readonly<Record<NavGridTopology, TopologyOps>> = Object.freeze({\n\t'square4': square4Ops,\n\t'square8': unimplementedOps('square8'),\n\t'hex-pointy': unimplementedOps('hex-pointy'),\n\t'hex-flat': unimplementedOps('hex-flat'),\n});\n\n/**\n * Create a weighted navigation grid.\n *\n * @example\n * ```typescript\n * const grid = createNavGrid({ width: 32, height: 32, cellSize: 16 });\n * grid.cells[grid.cellFromXY(5, 5)] = 0; // block a cell\n * ```\n */\nexport function createNavGrid(options: CreateNavGridOptions): NavGrid {\n\tconst topology = options.topology ?? 'square4';\n\tconst cellSize = options.cellSize ?? 32;\n\tconst originX = options.originX ?? 0;\n\tconst originY = options.originY ?? 0;\n\tconst { width, height } = options;\n\tconst defaultCost = options.defaultCost ?? 1;\n\n\tif (!Number.isInteger(width) || width <= 0) {\n\t\tthrow new Error(`pathfinding: width must be a positive integer, got ${width}`);\n\t}\n\tif (!Number.isInteger(height) || height <= 0) {\n\t\tthrow new Error(`pathfinding: height must be a positive integer, got ${height}`);\n\t}\n\tif (cellSize <= 0) {\n\t\tthrow new Error(`pathfinding: cellSize must be > 0, got ${cellSize}`);\n\t}\n\tif (defaultCost < 0 || defaultCost > 255) {\n\t\tthrow new Error(`pathfinding: defaultCost must be in 0–255, got ${defaultCost}`);\n\t}\n\n\tconst expectedLen = width * height;\n\tconst cells = options.cells ?? new Uint8Array(expectedLen).fill(defaultCost);\n\tif (cells.length !== expectedLen) {\n\t\tthrow new Error(\n\t\t\t`pathfinding: cells length ${cells.length} does not match width*height ${expectedLen}`,\n\t\t);\n\t}\n\n\tconst invCellSize = 1 / cellSize;\n\n\tconst worldToCell = (wx: number, wy: number): CellIndex => {\n\t\tconst col = Math.floor((wx - originX) * invCellSize);\n\t\tconst row = Math.floor((wy - originY) * invCellSize);\n\t\tconst cCol = col < 0 ? 0 : col >= width ? width - 1 : col;\n\t\tconst cRow = row < 0 ? 0 : row >= height ? height - 1 : row;\n\t\treturn cRow * width + cCol;\n\t};\n\n\tconst cellToWorld = (idx: CellIndex): Vector2D => {\n\t\tconst col = idx % width;\n\t\tconst row = (idx - col) / width;\n\t\treturn {\n\t\t\tx: originX + (col + 0.5) * cellSize,\n\t\t\ty: originY + (row + 0.5) * cellSize,\n\t\t};\n\t};\n\n\tconst cellFromXY = (x: number, y: number): CellIndex => y * width + x;\n\n\tconst cellToXY = (idx: CellIndex): { x: number; y: number } => {\n\t\tconst x = idx % width;\n\t\treturn { x, y: (idx - x) / width };\n\t};\n\n\treturn {\n\t\ttopology, width, height, cellSize, originX, originY, cells,\n\t\tworldToCell, cellToWorld, cellFromXY, cellToXY,\n\t};\n}\n\n// ==================== Helper Functions ====================\n\n/**\n * Create a `pathRequest` component for spreading into `spawn()` / `addComponent()`.\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createTransform(0, 0),\n * ...createMoveSpeed(100),\n * ...createPathRequest({ x: 200, y: 300 }),\n * });\n * ```\n */\nexport function createPathRequest(target: Vector2D): Pick<PathfindingComponentTypes, 'pathRequest'> {\n\treturn { pathRequest: { target: { x: target.x, y: target.y } } };\n}\n\n// ==================== Pure A* API ====================\n\nexport interface FindPathOptions {\n\t/** Cap on A* node expansions; returns `null` if exceeded. Default 10_000. */\n\tmaxNodesExpanded?: number;\n\t/** Dynamic per-call obstacles layered on top of the static grid. */\n\tblockedCells?: Set<CellIndex>;\n\t/** Accept arrival within N cells of goal (topology-aware distance). Default 0. */\n\tgoalTolerance?: number;\n}\n\ninterface PathHeap {\n\tids: Int32Array;\n\tpriorities: Float32Array;\n\tsize: number;\n}\n\n// Why: parallel-typed-array heap keeps cells & priorities in cache without per-node allocations.\nfunction heapPush(heap: PathHeap, id: number, priority: number): void {\n\tlet i = heap.size;\n\theap.size = i + 1;\n\twhile (i > 0) {\n\t\tconst parent = (i - 1) >> 1;\n\t\tif ((heap.priorities[parent] ?? 0) <= priority) break;\n\t\theap.ids[i] = heap.ids[parent] ?? 0;\n\t\theap.priorities[i] = heap.priorities[parent] ?? 0;\n\t\ti = parent;\n\t}\n\theap.ids[i] = id;\n\theap.priorities[i] = priority;\n}\n\nfunction heapPop(heap: PathHeap): number {\n\tconst top = heap.ids[0] ?? -1;\n\tconst last = heap.size - 1;\n\theap.size = last;\n\tif (last <= 0) return top;\n\tconst movedId = heap.ids[last] ?? 0;\n\tconst movedPri = heap.priorities[last] ?? 0;\n\tlet i = 0;\n\tconst half = last >> 1;\n\twhile (i < half) {\n\t\tlet child = (i << 1) + 1;\n\t\tconst right = child + 1;\n\t\tif (right < last && (heap.priorities[right] ?? 0) < (heap.priorities[child] ?? 0)) child = right;\n\t\tif ((heap.priorities[child] ?? 0) >= movedPri) break;\n\t\theap.ids[i] = heap.ids[child] ?? 0;\n\t\theap.priorities[i] = heap.priorities[child] ?? 0;\n\t\ti = child;\n\t}\n\theap.ids[i] = movedId;\n\theap.priorities[i] = movedPri;\n\treturn top;\n}\n\nfunction reconstructPath(cameFrom: Int32Array, end: CellIndex): CellIndex[] {\n\t// Why: two-pass (count then fill) avoids unshift/reverse allocation.\n\tlet count = 1;\n\tlet cur = end;\n\twhile ((cameFrom[cur] ?? -1) !== -1) {\n\t\tcount++;\n\t\tcur = cameFrom[cur] ?? -1;\n\t}\n\tconst path = new Array<CellIndex>(count);\n\tcur = end;\n\tfor (let i = count - 1; i >= 0; i--) {\n\t\tpath[i] = cur;\n\t\tif (i > 0) cur = cameFrom[cur] ?? -1;\n\t}\n\treturn path;\n}\n\n/**\n * Compute a path through `grid` from `start` to `goal`.\n *\n * Returns a list of cell indices starting with `start` and ending at a cell\n * within `goalTolerance` of `goal`, or `null` if no such path exists within\n * `maxNodesExpanded` expansions.\n *\n * `start` is always treated as passable (even if its grid cell is 0 or the\n * cell is in `blockedCells`) — actors physics-pushed onto a wall still get a\n * valid origin.\n */\nexport function findPath(\n\tgrid: NavGrid,\n\tstart: CellIndex,\n\tgoal: CellIndex,\n\toptions?: FindPathOptions,\n): CellIndex[] | null {\n\tconst n = grid.cells.length;\n\tif (start < 0 || start >= n) return null;\n\tif (goal < 0 || goal >= n) return null;\n\n\tconst maxNodesExpanded = options?.maxNodesExpanded ?? 10_000;\n\tconst blockedCells = options?.blockedCells;\n\tconst goalTolerance = options?.goalTolerance ?? 0;\n\tconst ops = topologyOps[grid.topology];\n\n\t// Per-call allocations: ~n bytes × 5 (gScore, cameFrom, closed, heap ids, heap priorities).\n\t// For a 100×100 grid that's ~120 KB per search. Acceptable for v1 game-grid scales.\n\t// Deferred optimization: closure-scoped reusable pool keyed by `n`, reset per call.\n\tconst gScore = new Float32Array(n);\n\tgScore.fill(Number.POSITIVE_INFINITY);\n\tconst cameFrom = new Int32Array(n);\n\tcameFrom.fill(-1);\n\tconst closed = new Uint8Array(n);\n\tconst heap: PathHeap = {\n\t\tids: new Int32Array(n),\n\t\tpriorities: new Float32Array(n),\n\t\tsize: 0,\n\t};\n\tconst neighborBuf: number[] = [];\n\n\tgScore[start] = 0;\n\theapPush(heap, start, ops.heuristic(grid, start, goal));\n\n\tlet expanded = 0;\n\twhile (heap.size > 0) {\n\t\tif (expanded >= maxNodesExpanded) return null;\n\t\tconst current = heapPop(heap);\n\t\tif (closed[current]) continue;\n\t\tclosed[current] = 1;\n\t\texpanded++;\n\n\t\tif (ops.heuristic(grid, current, goal) <= goalTolerance) {\n\t\t\treturn reconstructPath(cameFrom, current);\n\t\t}\n\n\t\tneighborBuf.length = 0;\n\t\tconst count = ops.neighbors(grid, current, neighborBuf);\n\t\tfor (let k = 0; k < count; k++) {\n\t\t\tconst next = neighborBuf[k] ?? -1;\n\t\t\tif (next < 0 || closed[next]) continue;\n\t\t\tconst cellCost = grid.cells[next] ?? 0;\n\t\t\tif (cellCost === 0) continue;\n\t\t\tif (blockedCells && blockedCells.has(next)) continue;\n\n\t\t\tconst tentative = (gScore[current] ?? Number.POSITIVE_INFINITY) + ops.stepCost(grid, current, next);\n\t\t\tif (tentative < (gScore[next] ?? Number.POSITIVE_INFINITY)) {\n\t\t\t\tgScore[next] = tentative;\n\t\t\t\tcameFrom[next] = current;\n\t\t\t\theapPush(heap, next, tentative + ops.heuristic(grid, next, goal));\n\t\t\t}\n\t\t}\n\t}\n\treturn null;\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a pathfinding plugin for ECSpresso.\n *\n * Requires the transform and steering plugins to be installed (entities need\n * `worldTransform` for start-cell detection and `moveTarget`/`moveSpeed` for\n * waypoint traversal).\n *\n * @example\n * ```typescript\n * const grid = createNavGrid({ width: 32, height: 32, cellSize: 16 });\n * const ecs = ECSpresso.create()\n * .withPlugin(createTransformPlugin())\n * .withPlugin(createSteeringPlugin())\n * .withPlugin(createPathfindingPlugin({ grid }))\n * .build();\n *\n * ecs.spawn({\n * ...createTransform(0, 0),\n * ...createMoveSpeed(100),\n * ...createPathRequest({ x: 500, y: 300 }),\n * });\n * ```\n */\nexport function createPathfindingPlugin<G extends string = 'ai'>(\n\toptions: PathfindingPluginOptions<G>,\n) {\n\tconst {\n\t\tgrid,\n\t\tsystemGroup = 'ai' as G,\n\t\tpriority = 150,\n\t\tphase = 'update',\n\t\tmaxRequestsPerFrame = 4,\n\t\tmaxNodesExpanded = 10_000,\n\t} = options;\n\n\treturn definePlugin('pathfinding')\n\t\t.withComponentTypes<PathfindingComponentTypes>()\n\t\t.withEventTypes<PathfindingEventTypes>()\n\t\t.withResourceTypes<PathfindingResourceTypes>()\n\t\t.withLabels<'pathfinding-request' | 'pathfinding-waypoint-advance'>()\n\t\t.withGroups<G>()\n\t\t.requires<TransformWorldConfig & SteeringWorldConfig>()\n\t\t.install((world) => {\n\t\t\tworld.addResource('navGrid', grid);\n\n\t\t\tworld\n\t\t\t\t.addSystem('pathfinding-request')\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('requests', {\n\t\t\t\t\twith: ['pathRequest', 'worldTransform'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, ecs }) => {\n\t\t\t\t\tconst navGrid = ecs.getResource('navGrid');\n\t\t\t\t\tlet processed = 0;\n\t\t\t\t\tfor (const entity of queries.requests) {\n\t\t\t\t\t\tif (processed >= maxRequestsPerFrame) break;\n\t\t\t\t\t\tprocessed++;\n\t\t\t\t\t\tconst { pathRequest, worldTransform } = entity.components;\n\t\t\t\t\t\tconst startIdx = navGrid.worldToCell(worldTransform.x, worldTransform.y);\n\t\t\t\t\t\tconst goalIdx = navGrid.worldToCell(pathRequest.target.x, pathRequest.target.y);\n\t\t\t\t\t\tconst result = findPath(navGrid, startIdx, goalIdx, { maxNodesExpanded });\n\t\t\t\t\t\tecs.commands.removeComponent(entity.id, 'pathRequest');\n\n\t\t\t\t\t\tif (result === null) {\n\t\t\t\t\t\t\tecs.eventBus.publish('pathBlocked', { entityId: entity.id });\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst waypoints = result.slice(1).map(idx => navGrid.cellToWorld(idx));\n\t\t\t\t\t\tecs.eventBus.publish('pathFound', { entityId: entity.id, path: waypoints });\n\t\t\t\t\t\tif (waypoints.length === 0) continue;\n\n\t\t\t\t\t\tconst existingPath = ecs.getComponent(entity.id, 'path');\n\t\t\t\t\t\tif (existingPath) {\n\t\t\t\t\t\t\texistingPath.waypoints = waypoints;\n\t\t\t\t\t\t\texistingPath.currentIndex = 0;\n\t\t\t\t\t\t\tecs.markChanged(entity.id, 'path');\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tecs.addComponent(entity.id, 'path', { waypoints, currentIndex: 0 });\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst first = waypoints[0];\n\t\t\t\t\t\tif (!first) continue;\n\t\t\t\t\t\tconst existingMT = ecs.getComponent(entity.id, 'moveTarget');\n\t\t\t\t\t\tif (existingMT) {\n\t\t\t\t\t\t\texistingMT.x = first.x;\n\t\t\t\t\t\t\texistingMT.y = first.y;\n\t\t\t\t\t\t\tecs.markChanged(entity.id, 'moveTarget');\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tecs.addComponent(entity.id, 'moveTarget', { x: first.x, y: first.y });\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\tworld\n\t\t\t\t.addSystem('pathfinding-waypoint-advance')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setEventHandlers({\n\t\t\t\t\tarriveAtTarget({ data, ecs }) {\n\t\t\t\t\t\tconst path = ecs.getComponent(data.entityId, 'path');\n\t\t\t\t\t\tif (!path) return;\n\t\t\t\t\t\tconst next = path.currentIndex + 1;\n\t\t\t\t\t\tif (next >= path.waypoints.length) {\n\t\t\t\t\t\t\tecs.commands.removeComponent(data.entityId, 'path');\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tpath.currentIndex = next;\n\t\t\t\t\t\tecs.markChanged(data.entityId, 'path');\n\t\t\t\t\t\tconst wp = path.waypoints[next];\n\t\t\t\t\t\tif (!wp) return;\n\t\t\t\t\t\t// Why: use command buffer so the add is queued AFTER steering's\n\t\t\t\t\t\t// queued `removeComponent('moveTarget')` in the same phase.\n\t\t\t\t\t\tecs.commands.addComponent(data.entityId, 'moveTarget', { x: wp.x, y: wp.y });\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t});\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": "2PAYA,uBAAS,
|
|
7
|
+
"mappings": "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",
|
|
8
8
|
"debugId": "72F70AA348AB2CF664756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* decides when and how to handle death (animations, loot, etc).
|
|
9
9
|
*/
|
|
10
10
|
import { type BasePluginOptions } from 'ecspresso';
|
|
11
|
-
import type {
|
|
11
|
+
import type { ComponentsConfig, EventsConfig } from 'ecspresso';
|
|
12
12
|
/**
|
|
13
13
|
* Health state for an entity.
|
|
14
14
|
*/
|
|
@@ -48,7 +48,7 @@ export interface HealthEventTypes {
|
|
|
48
48
|
* WorldConfig representing the health plugin's provided types.
|
|
49
49
|
* Used as the `Requires` type parameter by plugins that depend on health.
|
|
50
50
|
*/
|
|
51
|
-
export type HealthWorldConfig =
|
|
51
|
+
export type HealthWorldConfig = ComponentsConfig<HealthComponentTypes> & EventsConfig<HealthEventTypes>;
|
|
52
52
|
export interface HealthPluginOptions<G extends string = 'combat'> extends BasePluginOptions<G> {
|
|
53
53
|
}
|
|
54
54
|
/**
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/plugins/combat/health.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * Health Plugin for ECSpresso\n *\n * Provides a standard health/damage/death lifecycle.\n * Entities with a `health` component can receive `damage` events.\n * When health reaches zero, an `entityDied` event is published.\n * The plugin does NOT remove dead entities — game-specific logic\n * decides when and how to handle death (animations, loot, etc).\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type {
|
|
5
|
+
"/**\n * Health Plugin for ECSpresso\n *\n * Provides a standard health/damage/death lifecycle.\n * Entities with a `health` component can receive `damage` events.\n * When health reaches zero, an `entityDied` event is published.\n * The plugin does NOT remove dead entities — game-specific logic\n * decides when and how to handle death (animations, loot, etc).\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type { ComponentsConfig, EventsConfig } from 'ecspresso';\n\n// ==================== Component Types ====================\n\n/**\n * Health state for an entity.\n */\nexport interface Health {\n\tcurrent: number;\n\tmax: number;\n}\n\n/**\n * Component types provided by the health plugin.\n */\nexport interface HealthComponentTypes {\n\thealth: Health;\n}\n\n// ==================== Event Types ====================\n\n/**\n * Event requesting damage to an entity.\n */\nexport interface DamageEvent {\n\tentityId: number;\n\tamount: number;\n\tsourceId?: number;\n}\n\n/**\n * Event fired when an entity's health reaches zero.\n */\nexport interface EntityDiedEvent {\n\tentityId: number;\n\tkillerId?: number;\n}\n\n/**\n * Event types provided by the health plugin.\n */\nexport interface HealthEventTypes {\n\tdamage: DamageEvent;\n\tentityDied: EntityDiedEvent;\n}\n\n// ==================== WorldConfig ====================\n\n/**\n * WorldConfig representing the health plugin's provided types.\n * Used as the `Requires` type parameter by plugins that depend on health.\n */\nexport type HealthWorldConfig =\n\tComponentsConfig<HealthComponentTypes>\n\t& EventsConfig<HealthEventTypes>;\n\n// ==================== Plugin Options ====================\n\nexport interface HealthPluginOptions<G extends string = 'combat'> extends BasePluginOptions<G> {}\n\n// ==================== Helper Functions ====================\n\n/**\n * Create a health component at full HP.\n *\n * @param max Maximum (and initial) health\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createHealth(100),\n * ...createLocalTransform(200, 300),\n * });\n * ```\n */\nexport function createHealth(max: number): Pick<HealthComponentTypes, 'health'> {\n\treturn { health: { current: max, max } };\n}\n\n/**\n * Create a health component with a specific current value.\n *\n * @param current Current health\n * @param max Maximum health\n * @returns Component object suitable for spreading into spawn()\n */\nexport function createHealthWith(current: number, max: number): Pick<HealthComponentTypes, 'health'> {\n\treturn { health: { current, max } };\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a health plugin for ECSpresso.\n *\n * Provides event-driven damage processing. Subscribe to `damage` events\n * to deal damage, and listen to `entityDied` events to react to deaths.\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createHealthPlugin())\n * .build();\n *\n * // Deal damage:\n * ecs.eventBus.publish('damage', { entityId: targetId, amount: 25 });\n *\n * // React to death:\n * ecs.on('entityDied', ({ entityId }) => {\n * ecs.commands.removeEntity(entityId);\n * });\n * ```\n */\nexport function createHealthPlugin<G extends string = 'combat'>(\n\toptions?: HealthPluginOptions<G>,\n) {\n\tconst {\n\t\tsystemGroup = 'combat',\n\t} = options ?? {};\n\n\treturn definePlugin('health')\n\t\t.withComponentTypes<HealthComponentTypes>()\n\t\t.withEventTypes<HealthEventTypes>()\n\t\t.withLabels<'health-damage'>()\n\t\t.withGroups<G>()\n\t\t.install((world) => {\n\t\t\tworld\n\t\t\t\t.addSystem('health-damage')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setEventHandlers({\n\t\t\t\t\tdamage({ data, ecs }) {\n\t\t\t\t\t\tconst health = ecs.getComponent(data.entityId, 'health');\n\t\t\t\t\t\tif (!health) return;\n\t\t\t\t\t\tif (health.current <= 0) return;\n\n\t\t\t\t\t\thealth.current = Math.max(0, health.current - data.amount);\n\t\t\t\t\t\tecs.markChanged(data.entityId, 'health');\n\n\t\t\t\t\t\tif (health.current <= 0) {\n\t\t\t\t\t\t\tecs.eventBus.publish('entityDied', {\n\t\t\t\t\t\t\t\tentityId: data.entityId,\n\t\t\t\t\t\t\t\tkillerId: data.sourceId,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t});\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": "2PAUA,uBAAS,
|
|
7
|
+
"mappings": "2PAUA,uBAAS,kBA6EF,SAAS,CAAY,CAAC,EAAmD,CAC/E,MAAO,CAAE,OAAQ,CAAE,QAAS,EAAK,KAAI,CAAE,EAUjC,SAAS,CAAgB,CAAC,EAAiB,EAAmD,CACpG,MAAO,CAAE,OAAQ,CAAE,UAAS,KAAI,CAAE,EA0B5B,SAAS,CAA+C,CAC9D,EACC,CACD,IACC,cAAc,UACX,GAAW,CAAC,EAEhB,OAAO,EAAa,QAAQ,EAC1B,mBAAyC,EACzC,eAAiC,EACjC,WAA4B,EAC5B,WAAc,EACd,QAAQ,CAAC,IAAU,CACnB,EACE,UAAU,eAAe,EACzB,QAAQ,CAAW,EACnB,iBAAiB,CACjB,MAAM,EAAG,OAAM,OAAO,CACrB,IAAM,EAAS,EAAI,aAAa,EAAK,SAAU,QAAQ,EACvD,GAAI,CAAC,EAAQ,OACb,GAAI,EAAO,SAAW,EAAG,OAKzB,GAHA,EAAO,QAAU,KAAK,IAAI,EAAG,EAAO,QAAU,EAAK,MAAM,EACzD,EAAI,YAAY,EAAK,SAAU,QAAQ,EAEnC,EAAO,SAAW,EACrB,EAAI,SAAS,QAAQ,aAAc,CAClC,SAAU,EAAK,SACf,SAAU,EAAK,QAChB,CAAC,EAGJ,CAAC,EACF",
|
|
8
8
|
"debugId": "F64127187EB3461D64756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* and a `damage` event is forwarded to the target (if the health plugin is present).
|
|
9
9
|
*/
|
|
10
10
|
import { type BasePluginOptions } from 'ecspresso';
|
|
11
|
-
import type {
|
|
11
|
+
import type { ComponentsConfig, EventsConfig } from 'ecspresso';
|
|
12
12
|
import type { TransformWorldConfig } from '../spatial/transform';
|
|
13
13
|
import type { CollisionEventTypes } from '../physics/collision';
|
|
14
14
|
import type { DamageEvent } from './health';
|
|
@@ -60,7 +60,7 @@ export interface ProjectileEventTypes {
|
|
|
60
60
|
/**
|
|
61
61
|
* WorldConfig representing the projectile plugin's provided types.
|
|
62
62
|
*/
|
|
63
|
-
export type ProjectileWorldConfig =
|
|
63
|
+
export type ProjectileWorldConfig = ComponentsConfig<ProjectileComponentTypes> & EventsConfig<ProjectileEventTypes>;
|
|
64
64
|
export interface ProjectilePluginOptions<G extends string = 'combat'> extends BasePluginOptions<G> {
|
|
65
65
|
/**
|
|
66
66
|
* Whether to auto-publish `damage` events on hit.
|
|
@@ -112,4 +112,4 @@ export declare function createProjectileDirection(x: number, y: number): Pick<Pr
|
|
|
112
112
|
* });
|
|
113
113
|
* ```
|
|
114
114
|
*/
|
|
115
|
-
export declare function createProjectilePlugin<G extends string = 'combat'>(options?: ProjectilePluginOptions<G>): import("ecspresso").Plugin<import("ecspresso").WithEvents<import("ecspresso").WithComponents<import("ecspresso").EmptyConfig, ProjectileComponentTypes>, ProjectileEventTypes>, TransformWorldConfig &
|
|
115
|
+
export declare function createProjectilePlugin<G extends string = 'combat'>(options?: ProjectilePluginOptions<G>): import("ecspresso").Plugin<import("ecspresso").WithEvents<import("ecspresso").WithComponents<import("ecspresso").EmptyConfig, ProjectileComponentTypes>, ProjectileEventTypes>, TransformWorldConfig & EventsConfig<CollisionEventTypes<string>>, "projectile-homing" | "projectile-linear" | "projectile-collision", G, never, never>;
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/plugins/combat/projectile.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * Projectile Plugin for ECSpresso\n *\n * Provides projectile movement (homing and linear) and collision integration.\n * Homing projectiles track a target entity's position each frame.\n * Linear projectiles move in a fixed direction.\n * When a collision involves a projectile, a `projectileHit` event is published\n * and a `damage` event is forwarded to the target (if the health plugin is present).\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type {
|
|
5
|
+
"/**\n * Projectile Plugin for ECSpresso\n *\n * Provides projectile movement (homing and linear) and collision integration.\n * Homing projectiles track a target entity's position each frame.\n * Linear projectiles move in a fixed direction.\n * When a collision involves a projectile, a `projectileHit` event is published\n * and a `damage` event is forwarded to the target (if the health plugin is present).\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type { ComponentsConfig, EventsConfig } from 'ecspresso';\nimport type { TransformWorldConfig } from '../spatial/transform';\nimport type { CollisionEventTypes } from '../physics/collision';\nimport type { DamageEvent } from './health';\n\n// ==================== Component Types ====================\n\n/**\n * Core projectile data.\n */\nexport interface Projectile {\n\tdamage: number;\n\tspeed: number;\n\t/** Entity that fired this projectile */\n\tsourceId: number;\n}\n\n/**\n * Homing target — projectile tracks this entity's position each frame.\n */\nexport interface ProjectileTarget {\n\tentityId: number;\n}\n\n/**\n * Fixed direction for non-homing projectiles (normalized).\n */\nexport interface ProjectileDirection {\n\tx: number;\n\ty: number;\n}\n\n/**\n * Component types provided by the projectile plugin.\n */\nexport interface ProjectileComponentTypes {\n\tprojectile: Projectile;\n\tprojectileTarget: ProjectileTarget;\n\tprojectileDirection: ProjectileDirection;\n}\n\n// ==================== Event Types ====================\n\n/**\n * Event fired when a projectile hits a target via collision.\n */\nexport interface ProjectileHitEvent {\n\tprojectileId: number;\n\ttargetId: number;\n\tdamage: number;\n}\n\n/**\n * Event types provided by the projectile plugin.\n */\nexport interface ProjectileEventTypes {\n\tprojectileHit: ProjectileHitEvent;\n\tdamage: DamageEvent;\n}\n\n// ==================== WorldConfig ====================\n\n/**\n * WorldConfig representing the projectile plugin's provided types.\n */\nexport type ProjectileWorldConfig =\n\tComponentsConfig<ProjectileComponentTypes>\n\t& EventsConfig<ProjectileEventTypes>;\n\n// ==================== Plugin Options ====================\n\nexport interface ProjectilePluginOptions<G extends string = 'combat'> extends BasePluginOptions<G> {\n\t/**\n\t * Whether to auto-publish `damage` events on hit.\n\t * Requires the health plugin to be installed. (default: true)\n\t */\n\tpublishDamage?: boolean;\n}\n\n// ==================== Helper Functions ====================\n\n/**\n * Create a projectile component.\n *\n * @param damage Damage dealt on hit\n * @param speed Movement speed in pixels per second\n * @param sourceId Entity that fired this projectile\n * @returns Component object suitable for spreading into spawn()\n */\nexport function createProjectile(\n\tdamage: number,\n\tspeed: number,\n\tsourceId: number,\n): Pick<ProjectileComponentTypes, 'projectile'> {\n\treturn { projectile: { damage, speed, sourceId } };\n}\n\n/**\n * Create a homing projectile target component.\n *\n * @param entityId Target entity to track\n * @returns Component object suitable for spreading into spawn()\n */\nexport function createProjectileTarget(entityId: number): Pick<ProjectileComponentTypes, 'projectileTarget'> {\n\treturn { projectileTarget: { entityId } };\n}\n\n/**\n * Create a fixed-direction projectile component (auto-normalizes).\n *\n * @param x Direction x\n * @param y Direction y\n * @returns Component object suitable for spreading into spawn()\n */\nexport function createProjectileDirection(x: number, y: number): Pick<ProjectileComponentTypes, 'projectileDirection'> {\n\tconst len = Math.sqrt(x * x + y * y);\n\tif (len === 0) return { projectileDirection: { x: 0, y: -1 } };\n\treturn { projectileDirection: { x: x / len, y: y / len } };\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a projectile plugin for ECSpresso.\n *\n * Provides homing and linear projectile movement systems, plus\n * automatic collision-to-damage integration.\n *\n * @example\n * ```typescript\n * // Spawn a homing projectile:\n * ecs.spawn({\n * ...createProjectile(10, 400, turretId),\n * ...createProjectileTarget(enemyId),\n * ...createLocalTransform(x, y),\n * ...createCircleCollider(4),\n * ...collisionLayers.turretProjectile(),\n * sprite: bulletSprite,\n * renderLayer: 'projectiles',\n * });\n * ```\n */\nexport function createProjectilePlugin<G extends string = 'combat'>(\n\toptions?: ProjectilePluginOptions<G>,\n) {\n\tconst {\n\t\tsystemGroup = 'combat',\n\t\tpriority = 300,\n\t\tphase = 'update',\n\t\tpublishDamage = true,\n\t} = options ?? {};\n\n\treturn definePlugin('projectile')\n\t\t.withComponentTypes<ProjectileComponentTypes>()\n\t\t.withEventTypes<ProjectileEventTypes>()\n\t\t.withLabels<'projectile-homing' | 'projectile-linear' | 'projectile-collision'>()\n\t\t.withGroups<G>()\n\t\t.requires<\n\t\t\tTransformWorldConfig &\n\t\t\tEventsConfig<CollisionEventTypes<string>>\n\t\t>()\n\t\t.install((world) => {\n\t\t\t// Homing projectiles — track target position each frame\n\t\t\tworld\n\t\t\t\t.addSystem('projectile-homing')\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('homing', {\n\t\t\t\t\twith: ['projectile', 'projectileTarget', 'localTransform'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, ecs, dt }) => {\n\t\t\t\t\tfor (const entity of queries.homing) {\n\t\t\t\t\t\tconst { projectile, projectileTarget, localTransform } = entity.components;\n\n\t\t\t\t\t\t// Target no longer exists — remove projectile\n\t\t\t\t\t\tif (!ecs.getEntity(projectileTarget.entityId)) {\n\t\t\t\t\t\t\tecs.commands.removeEntity(entity.id);\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst targetTransform = ecs.getComponent(projectileTarget.entityId, 'worldTransform');\n\t\t\t\t\t\tif (!targetTransform) {\n\t\t\t\t\t\t\tecs.commands.removeEntity(entity.id);\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst dx = targetTransform.x - localTransform.x;\n\t\t\t\t\t\tconst dy = targetTransform.y - localTransform.y;\n\t\t\t\t\t\tconst distSq = dx * dx + dy * dy;\n\t\t\t\t\t\tconst step = projectile.speed * dt;\n\n\t\t\t\t\t\tif (distSq <= step * step) {\n\t\t\t\t\t\t\tlocalTransform.x = targetTransform.x;\n\t\t\t\t\t\t\tlocalTransform.y = targetTransform.y;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tconst dist = Math.sqrt(distSq);\n\t\t\t\t\t\t\tlocalTransform.x += (dx / dist) * step;\n\t\t\t\t\t\t\tlocalTransform.y += (dy / dist) * step;\n\t\t\t\t\t\t\tlocalTransform.rotation = Math.atan2(dy, dx);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tecs.markChanged(entity.id, 'localTransform');\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// Linear projectiles — move in fixed direction\n\t\t\tworld\n\t\t\t\t.addSystem('projectile-linear')\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('linear', {\n\t\t\t\t\twith: ['projectile', 'projectileDirection', 'localTransform'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, dt }) => {\n\t\t\t\t\tfor (const entity of queries.linear) {\n\t\t\t\t\t\tconst { projectile, projectileDirection, localTransform } = entity.components;\n\t\t\t\t\t\tconst step = projectile.speed * dt;\n\t\t\t\t\t\tlocalTransform.x += projectileDirection.x * step;\n\t\t\t\t\t\tlocalTransform.y += projectileDirection.y * step;\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// Collision integration — route collision events to projectileHit + damage\n\t\t\tworld\n\t\t\t\t.addSystem('projectile-collision')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setEventHandlers({\n\t\t\t\t\tcollision({ data, ecs }) {\n\t\t\t\t\t\tif (!ecs.getEntity(data.entityA) || !ecs.getEntity(data.entityB)) return;\n\n\t\t\t\t\t\tconst projectileA = ecs.getComponent(data.entityA, 'projectile');\n\t\t\t\t\t\tconst projectileB = ecs.getComponent(data.entityB, 'projectile');\n\n\t\t\t\t\t\tconst isAProjectile = projectileA !== undefined;\n\t\t\t\t\t\tconst projectileData = isAProjectile ? projectileA : projectileB;\n\t\t\t\t\t\tif (!projectileData) return;\n\n\t\t\t\t\t\tconst projectileId = isAProjectile ? data.entityA : data.entityB;\n\t\t\t\t\t\tconst targetId = isAProjectile ? data.entityB : data.entityA;\n\n\t\t\t\t\t\t// Don't hit the entity that fired this projectile\n\t\t\t\t\t\tif (targetId === projectileData.sourceId) return;\n\n\t\t\t\t\t\tecs.eventBus.publish('projectileHit', {\n\t\t\t\t\t\t\tprojectileId,\n\t\t\t\t\t\t\ttargetId,\n\t\t\t\t\t\t\tdamage: projectileData.damage,\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\tif (publishDamage) {\n\t\t\t\t\t\t\tecs.eventBus.publish('damage', {\n\t\t\t\t\t\t\t\tentityId: targetId,\n\t\t\t\t\t\t\t\tamount: projectileData.damage,\n\t\t\t\t\t\t\t\tsourceId: projectileData.sourceId,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tecs.commands.removeEntity(projectileId);\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t});\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": "2PAUA,uBAAS,
|
|
7
|
+
"mappings": "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",
|
|
8
8
|
"debugId": "D0756C0A27AA8CAC64756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* for hit-testing. The selection box overlay remains in screen space.
|
|
15
15
|
*/
|
|
16
16
|
import { type BasePluginOptions } from 'ecspresso';
|
|
17
|
-
import type {
|
|
17
|
+
import type { ComponentsConfig, ResourcesConfig } from 'ecspresso';
|
|
18
18
|
import type { InputResourceTypes } from './input';
|
|
19
19
|
import type { Renderer2DComponentTypes, Renderer2DResourceTypes } from '../rendering/renderer2D';
|
|
20
20
|
/**
|
|
@@ -45,8 +45,8 @@ export interface SelectionResourceTypes {
|
|
|
45
45
|
/**
|
|
46
46
|
* WorldConfig representing the selection plugin's provided types.
|
|
47
47
|
*/
|
|
48
|
-
export type SelectionWorldConfig =
|
|
49
|
-
type SelectionRequires =
|
|
48
|
+
export type SelectionWorldConfig = ComponentsConfig<SelectionComponentTypes> & ResourcesConfig<SelectionResourceTypes>;
|
|
49
|
+
type SelectionRequires = ComponentsConfig<Renderer2DComponentTypes> & ResourcesConfig<InputResourceTypes & Renderer2DResourceTypes>;
|
|
50
50
|
/**
|
|
51
51
|
* Configuration options for the selection plugin.
|
|
52
52
|
*/
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/plugins/spatial/camera.ts", "../src/plugins/input/selection.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * Camera / Viewport Plugin for ECSpresso\n *\n * Provides a declarative camera with world/screen coordinate conversion, smooth follow,\n * trauma-based shake, bounds clamping, cursor-centered zoom, and logical viewport dimensions.\n *\n * This plugin is renderer-agnostic. PixiJS or other renderer integration (applying\n * cameraState to a container/stage transform) is the consumer's responsibility.\n *\n * Camera uses its own x/y/zoom/rotation rather than localTransform/worldTransform.\n * It reads the target entity's worldTransform for follow, but doesn't participate\n * in the transform hierarchy itself.\n */\n\nimport { definePlugin } from 'ecspresso';\nimport type { SystemPhase } from 'ecspresso';\nimport type ECSpresso from 'ecspresso';\nimport type { WorldConfigFrom } from '../../type-utils';\nimport type { TransformWorldConfig } from './transform';\n\n// ==================== Component Types ====================\n\nexport interface Camera {\n\tx: number;\n\ty: number;\n\tzoom: number;\n\trotation: number;\n}\n\nexport interface CameraFollow {\n\ttarget: number;\n\tsmoothing: number;\n\tdeadzoneX: number;\n\tdeadzoneY: number;\n\toffsetX: number;\n\toffsetY: number;\n}\n\nexport interface CameraShake {\n\ttrauma: number;\n\ttraumaDecay: number;\n\tmaxOffsetX: number;\n\tmaxOffsetY: number;\n\tmaxRotation: number;\n}\n\nexport interface CameraBounds {\n\tminX: number;\n\tminY: number;\n\tmaxX: number;\n\tmaxY: number;\n}\n\nexport interface CameraComponentTypes {\n\tcamera: Camera;\n\tcameraFollow: CameraFollow;\n\tcameraShake: CameraShake;\n\tcameraBounds: CameraBounds;\n}\n\n// ==================== Resource Types ====================\n\nexport interface FollowOptions {\n\tsmoothing?: number;\n\tdeadzoneX?: number;\n\tdeadzoneY?: number;\n\toffsetX?: number;\n\toffsetY?: number;\n}\n\nexport type EntityHandle = { id: number };\n\nexport interface CameraState {\n\t// Read-only data (synced from camera entity each frame)\n\tx: number;\n\ty: number;\n\tzoom: number;\n\trotation: number;\n\tshakeOffsetX: number;\n\tshakeOffsetY: number;\n\tshakeRotation: number;\n\tviewportWidth: number;\n\tviewportHeight: number;\n\tentityId: number;\n\n\t// Mutation methods\n\tfollow(target: number | EntityHandle, options?: FollowOptions): void;\n\tunfollow(): void;\n\tsetPosition(x: number, y: number): void;\n\tsetZoom(zoom: number): void;\n\tsetRotation(rotation: number): void;\n\tsetBounds(minX: number, minY: number, maxX: number, maxY: number): void;\n\tclearBounds(): void;\n\taddTrauma(amount: number): void;\n}\n\nexport interface CameraResourceTypes {\n\tcameraState: CameraState;\n}\n\n// ==================== Plugin Options ====================\n\nexport interface CameraPluginOptions<G extends string = 'camera'> {\n\tviewportWidth?: number;\n\tviewportHeight?: number;\n\tinitial?: {\n\t\tx?: number;\n\t\ty?: number;\n\t\tzoom?: number;\n\t\trotation?: number;\n\t};\n\tfollow?: FollowOptions;\n\tshake?: boolean | Partial<Omit<CameraShake, 'trauma'>>;\n\tbounds?:\n\t\t| { minX: number; minY: number; maxX: number; maxY: number }\n\t\t| [number, number, number, number];\n\tzoom?: {\n\t\tzoomStep?: number;\n\t\tminZoom?: number;\n\t\tmaxZoom?: number;\n\t};\n\tpan?: {\n\t\tspeed: number;\n\t\tactions?: {\n\t\t\tup?: string;\n\t\t\tdown?: string;\n\t\t\tleft?: string;\n\t\t\tright?: string;\n\t\t};\n\t};\n\tsystemGroup?: G;\n\tphase?: SystemPhase;\n\trandomFn?: () => number;\n}\n\n// ==================== Default Values ====================\n\nconst DEFAULT_SHAKE: Readonly<Omit<CameraShake, 'trauma'>> = {\n\ttraumaDecay: 1,\n\tmaxOffsetX: 10,\n\tmaxOffsetY: 10,\n\tmaxRotation: 0.05,\n};\n\nconst DEFAULT_FOLLOW: Readonly<Omit<CameraFollow, 'target'>> = {\n\tsmoothing: 5,\n\tdeadzoneX: 0,\n\tdeadzoneY: 0,\n\toffsetX: 0,\n\toffsetY: 0,\n};\n\n// ==================== Coordinate Conversion ====================\n\nexport function worldToScreen(\n\tworldX: number,\n\tworldY: number,\n\tstate: CameraState,\n): { x: number; y: number } {\n\tconst dx = worldX - (state.x + state.shakeOffsetX);\n\tconst dy = worldY - (state.y + state.shakeOffsetY);\n\n\tconst angle = -(state.rotation + state.shakeRotation);\n\tconst cos = Math.cos(angle);\n\tconst sin = Math.sin(angle);\n\tconst rx = dx * cos - dy * sin;\n\tconst ry = dx * sin + dy * cos;\n\n\treturn {\n\t\tx: rx * state.zoom + state.viewportWidth / 2,\n\t\ty: ry * state.zoom + state.viewportHeight / 2,\n\t};\n}\n\nexport function screenToWorld(\n\tscreenX: number,\n\tscreenY: number,\n\tstate: CameraState,\n): { x: number; y: number } {\n\tconst cx = (screenX - state.viewportWidth / 2) / state.zoom;\n\tconst cy = (screenY - state.viewportHeight / 2) / state.zoom;\n\n\tconst angle = state.rotation + state.shakeRotation;\n\tconst cos = Math.cos(angle);\n\tconst sin = Math.sin(angle);\n\tconst rx = cx * cos - cy * sin;\n\tconst ry = cx * sin + cy * cos;\n\n\treturn {\n\t\tx: rx + state.x + state.shakeOffsetX,\n\t\ty: ry + state.y + state.shakeOffsetY,\n\t};\n}\n\n// ==================== Internal Helpers ====================\n\nfunction resolveTarget(target: number | EntityHandle): number {\n\treturn typeof target === 'number' ? target : target.id;\n}\n\nfunction resolveShakeOptions(shake: true | Partial<Omit<CameraShake, 'trauma'>>): CameraShake {\n\tconst opts = shake === true ? {} : shake;\n\treturn {\n\t\ttrauma: 0,\n\t\ttraumaDecay: opts.traumaDecay ?? DEFAULT_SHAKE.traumaDecay,\n\t\tmaxOffsetX: opts.maxOffsetX ?? DEFAULT_SHAKE.maxOffsetX,\n\t\tmaxOffsetY: opts.maxOffsetY ?? DEFAULT_SHAKE.maxOffsetY,\n\t\tmaxRotation: opts.maxRotation ?? DEFAULT_SHAKE.maxRotation,\n\t};\n}\n\nfunction resolveBounds(\n\tbounds: { minX: number; minY: number; maxX: number; maxY: number } | [number, number, number, number],\n): CameraBounds {\n\tif (Array.isArray(bounds)) {\n\t\treturn { minX: bounds[0], minY: bounds[1], maxX: bounds[2], maxY: bounds[3] };\n\t}\n\treturn { ...bounds };\n}\n\nfunction resolveFollowOptions(options?: FollowOptions): Omit<CameraFollow, 'target'> {\n\treturn {\n\t\tsmoothing: options?.smoothing ?? DEFAULT_FOLLOW.smoothing,\n\t\tdeadzoneX: options?.deadzoneX ?? DEFAULT_FOLLOW.deadzoneX,\n\t\tdeadzoneY: options?.deadzoneY ?? DEFAULT_FOLLOW.deadzoneY,\n\t\toffsetX: options?.offsetX ?? DEFAULT_FOLLOW.offsetX,\n\t\toffsetY: options?.offsetY ?? DEFAULT_FOLLOW.offsetY,\n\t};\n}\n\n// ==================== Plugin Factory ====================\n\ntype CameraWorldConfig = WorldConfigFrom<CameraComponentTypes, {}, CameraResourceTypes>;\n\ntype CameraLabels =\n\t| 'camera-init'\n\t| 'camera-follow'\n\t| 'camera-shake-update'\n\t| 'camera-bounds'\n\t| 'camera-state-sync'\n\t| 'camera-zoom'\n\t| 'camera-pan';\n\nexport function createCameraPlugin<G extends string = 'camera'>(\n\toptions?: CameraPluginOptions<G>,\n) {\n\tconst {\n\t\tviewportWidth = 800,\n\t\tviewportHeight = 600,\n\t\tinitial,\n\t\tfollow: followConfig,\n\t\tshake: shakeConfig,\n\t\tbounds: boundsConfig,\n\t\tzoom: zoomConfig,\n\t\tpan: panConfig,\n\t\tsystemGroup = 'camera',\n\t\tphase = 'postUpdate',\n\t\trandomFn = Math.random,\n\t} = options ?? {};\n\n\treturn definePlugin('camera')\n\t\t.withComponentTypes<CameraComponentTypes>()\n\t\t.withResourceTypes<CameraResourceTypes>()\n\t\t.withLabels<CameraLabels>()\n\t\t.withGroups<G>()\n\t\t.requires<TransformWorldConfig>()\n\t\t.install((world) => {\n\t\t\t// Build mutation methods as closures over the world reference.\n\t\t\t// The cameraState resource is created immediately with placeholder methods,\n\t\t\t// then the init system populates entityId and wires up real methods.\n\n\t\t\tconst cameraState: CameraState = {\n\t\t\t\tx: initial?.x ?? 0,\n\t\t\t\ty: initial?.y ?? 0,\n\t\t\t\tzoom: initial?.zoom ?? 1,\n\t\t\t\trotation: initial?.rotation ?? 0,\n\t\t\t\tshakeOffsetX: 0,\n\t\t\t\tshakeOffsetY: 0,\n\t\t\t\tshakeRotation: 0,\n\t\t\t\tviewportWidth,\n\t\t\t\tviewportHeight,\n\t\t\t\tentityId: -1,\n\n\t\t\t\t// Mutation methods — wired up after camera entity is spawned\n\t\t\t\tfollow: () => {},\n\t\t\t\tunfollow: () => {},\n\t\t\t\tsetPosition: () => {},\n\t\t\t\tsetZoom: () => {},\n\t\t\t\tsetRotation: () => {},\n\t\t\t\tsetBounds: () => {},\n\t\t\t\tclearBounds: () => {},\n\t\t\t\taddTrauma: () => {},\n\t\t\t};\n\n\t\t\tworld.addResource('cameraState', cameraState);\n\n\t\t\t// camera-init: spawns camera entity and wires up mutation closures\n\t\t\tworld\n\t\t\t\t.addSystem('camera-init')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setOnInitialize((ecs: ECSpresso<CameraWorldConfig & TransformWorldConfig>) => {\n\t\t\t\t\t// Spawn with required camera component\n\t\t\t\t\tconst entity = ecs.spawn({\n\t\t\t\t\t\tcamera: {\n\t\t\t\t\t\t\tx: initial?.x ?? 0,\n\t\t\t\t\t\t\ty: initial?.y ?? 0,\n\t\t\t\t\t\t\tzoom: initial?.zoom ?? 1,\n\t\t\t\t\t\t\trotation: initial?.rotation ?? 0,\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\n\t\t\t\t\t// Conditionally add optional components\n\t\t\t\t\tif (followConfig) {\n\t\t\t\t\t\tecs.addComponent(entity.id, 'cameraFollow', {\n\t\t\t\t\t\t\ttarget: -1,\n\t\t\t\t\t\t\t...resolveFollowOptions(followConfig),\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\tif (shakeConfig) {\n\t\t\t\t\t\tecs.addComponent(entity.id, 'cameraShake', resolveShakeOptions(shakeConfig));\n\t\t\t\t\t}\n\n\t\t\t\t\tif (boundsConfig) {\n\t\t\t\t\t\tecs.addComponent(entity.id, 'cameraBounds', resolveBounds(boundsConfig));\n\t\t\t\t\t}\n\t\t\t\t\tcameraState.entityId = entity.id;\n\n\t\t\t\t\t// Wire up mutation methods\n\t\t\t\t\tcameraState.follow = (target: number | EntityHandle, opts?: FollowOptions) => {\n\t\t\t\t\t\tconst targetId = resolveTarget(target);\n\t\t\t\t\t\tconst followData: CameraFollow = {\n\t\t\t\t\t\t\ttarget: targetId,\n\t\t\t\t\t\t\t...resolveFollowOptions(opts),\n\t\t\t\t\t\t};\n\t\t\t\t\t\tconst existing = ecs.getComponent(cameraState.entityId, 'cameraFollow');\n\t\t\t\t\t\tif (existing) {\n\t\t\t\t\t\t\texisting.target = followData.target;\n\t\t\t\t\t\t\texisting.smoothing = followData.smoothing;\n\t\t\t\t\t\t\texisting.deadzoneX = followData.deadzoneX;\n\t\t\t\t\t\t\texisting.deadzoneY = followData.deadzoneY;\n\t\t\t\t\t\t\texisting.offsetX = followData.offsetX;\n\t\t\t\t\t\t\texisting.offsetY = followData.offsetY;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tecs.addComponent(cameraState.entityId, 'cameraFollow', followData);\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\n\t\t\t\t\tcameraState.unfollow = () => {\n\t\t\t\t\t\tconst existing = ecs.getComponent(cameraState.entityId, 'cameraFollow');\n\t\t\t\t\t\tif (existing) {\n\t\t\t\t\t\t\tecs.removeComponent(cameraState.entityId, 'cameraFollow');\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\n\t\t\t\t\tcameraState.setPosition = (x: number, y: number) => {\n\t\t\t\t\t\tconst camera = ecs.getComponent(cameraState.entityId, 'camera');\n\t\t\t\t\t\tif (!camera) return;\n\t\t\t\t\t\tcamera.x = x;\n\t\t\t\t\t\tcamera.y = y;\n\t\t\t\t\t};\n\n\t\t\t\t\tcameraState.setZoom = (zoom: number) => {\n\t\t\t\t\t\tconst camera = ecs.getComponent(cameraState.entityId, 'camera');\n\t\t\t\t\t\tif (!camera) return;\n\t\t\t\t\t\tcamera.zoom = zoom;\n\t\t\t\t\t};\n\n\t\t\t\t\tcameraState.setRotation = (rotation: number) => {\n\t\t\t\t\t\tconst camera = ecs.getComponent(cameraState.entityId, 'camera');\n\t\t\t\t\t\tif (!camera) return;\n\t\t\t\t\t\tcamera.rotation = rotation;\n\t\t\t\t\t};\n\n\t\t\t\t\tcameraState.setBounds = (minX: number, minY: number, maxX: number, maxY: number) => {\n\t\t\t\t\t\tconst existing = ecs.getComponent(cameraState.entityId, 'cameraBounds');\n\t\t\t\t\t\tif (existing) {\n\t\t\t\t\t\t\texisting.minX = minX;\n\t\t\t\t\t\t\texisting.minY = minY;\n\t\t\t\t\t\t\texisting.maxX = maxX;\n\t\t\t\t\t\t\texisting.maxY = maxY;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tecs.addComponent(cameraState.entityId, 'cameraBounds', { minX, minY, maxX, maxY });\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\n\t\t\t\t\tcameraState.clearBounds = () => {\n\t\t\t\t\t\tconst existing = ecs.getComponent(cameraState.entityId, 'cameraBounds');\n\t\t\t\t\t\tif (existing) {\n\t\t\t\t\t\t\tecs.removeComponent(cameraState.entityId, 'cameraBounds');\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\n\t\t\t\t\tcameraState.addTrauma = (amount: number) => {\n\t\t\t\t\t\tconst shake = ecs.getComponent(cameraState.entityId, 'cameraShake');\n\t\t\t\t\t\tif (shake) {\n\t\t\t\t\t\t\tshake.trauma = Math.min(1, Math.max(0, shake.trauma + amount));\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tecs.addComponent(cameraState.entityId, 'cameraShake', {\n\t\t\t\t\t\t\t\t...resolveShakeOptions(true),\n\t\t\t\t\t\t\t\ttrauma: Math.min(1, Math.max(0, amount)),\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\t\t\t\t});\n\n\t\t\t// camera-follow: priority 400 (after transform propagation at 500)\n\t\t\tworld\n\t\t\t\t.addSystem('camera-follow')\n\t\t\t\t.setPriority(400)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('cameras', {\n\t\t\t\t\twith: ['camera', 'cameraFollow'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, dt, ecs }) => {\n\t\t\t\t\tconst t = Math.min(1, dt);\n\t\t\t\t\tfor (const entity of queries.cameras) {\n\t\t\t\t\t\tconst { camera, cameraFollow } = entity.components;\n\t\t\t\t\t\tif (cameraFollow.target < 0) continue;\n\n\t\t\t\t\t\tconst targetWorld = ecs.getComponent(cameraFollow.target, 'worldTransform');\n\t\t\t\t\t\tif (!targetWorld) continue;\n\n\t\t\t\t\t\tconst goalX = targetWorld.x + cameraFollow.offsetX;\n\t\t\t\t\t\tconst goalY = targetWorld.y + cameraFollow.offsetY;\n\t\t\t\t\t\tconst dx = goalX - camera.x;\n\t\t\t\t\t\tconst dy = goalY - camera.y;\n\n\t\t\t\t\t\tif (Math.abs(dx) > cameraFollow.deadzoneX) {\n\t\t\t\t\t\t\tconst sign = dx > 0 ? 1 : -1;\n\t\t\t\t\t\t\tconst excessX = dx - sign * cameraFollow.deadzoneX;\n\t\t\t\t\t\t\tconst factor = Math.min(1, cameraFollow.smoothing * t);\n\t\t\t\t\t\t\tcamera.x += excessX * factor;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (Math.abs(dy) > cameraFollow.deadzoneY) {\n\t\t\t\t\t\t\tconst sign = dy > 0 ? 1 : -1;\n\t\t\t\t\t\t\tconst excessY = dy - sign * cameraFollow.deadzoneY;\n\t\t\t\t\t\t\tconst factor = Math.min(1, cameraFollow.smoothing * t);\n\t\t\t\t\t\t\tcamera.y += excessY * factor;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// camera-shake-update: priority 390\n\t\t\tworld\n\t\t\t\t.addSystem('camera-shake-update')\n\t\t\t\t.setPriority(390)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('shakeCameras', {\n\t\t\t\t\twith: ['camera', 'cameraShake'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, dt }) => {\n\t\t\t\t\tfor (const entity of queries.shakeCameras) {\n\t\t\t\t\t\tconst { cameraShake } = entity.components;\n\t\t\t\t\t\tcameraShake.trauma = Math.max(0, cameraShake.trauma - cameraShake.traumaDecay * dt);\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// camera-bounds: priority 380\n\t\t\tworld\n\t\t\t\t.addSystem('camera-bounds')\n\t\t\t\t.setPriority(380)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('boundedCameras', {\n\t\t\t\t\twith: ['camera', 'cameraBounds'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries }) => {\n\t\t\t\t\tfor (const entity of queries.boundedCameras) {\n\t\t\t\t\t\tconst { camera, cameraBounds } = entity.components;\n\t\t\t\t\t\tconst halfW = cameraState.viewportWidth / (2 * camera.zoom);\n\t\t\t\t\t\tconst halfH = cameraState.viewportHeight / (2 * camera.zoom);\n\n\t\t\t\t\t\tconst effectiveMinX = cameraBounds.minX + halfW;\n\t\t\t\t\t\tconst effectiveMaxX = cameraBounds.maxX - halfW;\n\t\t\t\t\t\tconst effectiveMinY = cameraBounds.minY + halfH;\n\t\t\t\t\t\tconst effectiveMaxY = cameraBounds.maxY - halfH;\n\n\t\t\t\t\t\tif (effectiveMinX > effectiveMaxX) {\n\t\t\t\t\t\t\tcamera.x = (cameraBounds.minX + cameraBounds.maxX) / 2;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tcamera.x = Math.max(effectiveMinX, Math.min(effectiveMaxX, camera.x));\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (effectiveMinY > effectiveMaxY) {\n\t\t\t\t\t\t\tcamera.y = (cameraBounds.minY + cameraBounds.maxY) / 2;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tcamera.y = Math.max(effectiveMinY, Math.min(effectiveMaxY, camera.y));\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// camera-state-sync: priority 370\n\t\t\tworld\n\t\t\t\t.addSystem('camera-state-sync')\n\t\t\t\t.setPriority(370)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setProcess(({ ecs }) => {\n\t\t\t\t\tconst camera = ecs.getComponent(cameraState.entityId, 'camera');\n\t\t\t\t\tif (!camera) {\n\t\t\t\t\t\tcameraState.x = 0;\n\t\t\t\t\t\tcameraState.y = 0;\n\t\t\t\t\t\tcameraState.zoom = 1;\n\t\t\t\t\t\tcameraState.rotation = 0;\n\t\t\t\t\t\tcameraState.shakeOffsetX = 0;\n\t\t\t\t\t\tcameraState.shakeOffsetY = 0;\n\t\t\t\t\t\tcameraState.shakeRotation = 0;\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tcameraState.x = camera.x;\n\t\t\t\t\tcameraState.y = camera.y;\n\t\t\t\t\tcameraState.zoom = camera.zoom;\n\t\t\t\t\tcameraState.rotation = camera.rotation;\n\n\t\t\t\t\tconst shake = ecs.getComponent(cameraState.entityId, 'cameraShake');\n\t\t\t\t\tif (shake && shake.trauma > 0) {\n\t\t\t\t\t\tconst intensity = shake.trauma * shake.trauma;\n\t\t\t\t\t\tcameraState.shakeOffsetX = shake.maxOffsetX * intensity * (randomFn() * 2 - 1);\n\t\t\t\t\t\tcameraState.shakeOffsetY = shake.maxOffsetY * intensity * (randomFn() * 2 - 1);\n\t\t\t\t\t\tcameraState.shakeRotation = shake.maxRotation * intensity * (randomFn() * 2 - 1);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tcameraState.shakeOffsetX = 0;\n\t\t\t\t\t\tcameraState.shakeOffsetY = 0;\n\t\t\t\t\t\tcameraState.shakeRotation = 0;\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// camera-zoom: conditionally registered when zoom option is provided\n\t\t\tif (zoomConfig) {\n\t\t\t\tconst {\n\t\t\t\t\tzoomStep = 0.1,\n\t\t\t\t\tminZoom = 0.1,\n\t\t\t\t\tmaxZoom = 10,\n\t\t\t\t} = zoomConfig;\n\n\t\t\t\ttype ZoomInputState = { pointer: { position: { x: number; y: number } } };\n\n\t\t\t\tlet pendingSteps = 0;\n\t\t\t\tlet zoomActive = false;\n\t\t\t\tlet canvas: HTMLCanvasElement | undefined;\n\t\t\t\tlet isoState: { tileWidth: number; tileHeight: number; originX: number; originY: number } | undefined;\n\n\t\t\t\tfunction onWheel(e: WheelEvent) {\n\t\t\t\t\te.preventDefault();\n\t\t\t\t\tpendingSteps += Math.sign(e.deltaY);\n\t\t\t\t}\n\n\t\t\t\tworld\n\t\t\t\t\t.addSystem('camera-zoom')\n\t\t\t\t\t.setPriority(410)\n\t\t\t\t\t.inPhase('preUpdate')\n\t\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t\t.addQuery('cameras', {\n\t\t\t\t\t\twith: ['camera'],\n\t\t\t\t\t})\n\t\t\t\t\t.setOnInitialize((ecs) => {\n\t\t\t\t\t\t// Check for required dependencies\n\t\t\t\t\t\tconst inputState = ecs.tryGetResource<ZoomInputState>('inputState');\n\t\t\t\t\t\tconst pixiApp = ecs.tryGetResource<{ canvas: HTMLCanvasElement }>('pixiApp');\n\n\t\t\t\t\t\tif (!inputState || !pixiApp) {\n\t\t\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t\t\t'[camera] zoom requires the input plugin and renderer2D plugin. ' +\n\t\t\t\t\t\t\t\t'Zoom will be disabled.',\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tcanvas = pixiApp.canvas;\n\t\t\t\t\t\tcanvas.addEventListener('wheel', onWheel as EventListener, { passive: false });\n\n\t\t\t\t\t\t// Detect isometric projection for iso-aware cursor-centered zoom\n\t\t\t\t\t\tisoState = ecs.tryGetResource<{\n\t\t\t\t\t\t\ttileWidth: number; tileHeight: number;\n\t\t\t\t\t\t\toriginX: number; originY: number;\n\t\t\t\t\t\t}>('isoProjection');\n\n\t\t\t\t\t\tzoomActive = true;\n\t\t\t\t\t})\n\t\t\t\t\t.setOnDetach(() => {\n\t\t\t\t\t\tif (!zoomActive || !canvas) return;\n\t\t\t\t\t\tcanvas.removeEventListener('wheel', onWheel as EventListener);\n\t\t\t\t\t})\n\t\t\t\t\t.setProcess(({ queries, ecs }) => {\n\t\t\t\t\t\tif (!zoomActive || pendingSteps === 0) return;\n\n\t\t\t\t\t\tconst steps = pendingSteps;\n\t\t\t\t\t\tpendingSteps = 0;\n\n\t\t\t\t\t\tconst [cameraEntity] = queries.cameras;\n\t\t\t\t\t\tif (!cameraEntity) return;\n\n\t\t\t\t\t\tconst cam = cameraEntity.components.camera;\n\t\t\t\t\t\tconst inputState = ecs.tryGetResource<ZoomInputState>('inputState');\n\t\t\t\t\t\tif (!inputState) return;\n\n\t\t\t\t\t\t// Apply zoom — proportional to number of wheel steps\n\t\t\t\t\t\tconst direction = steps > 0 ? (1 - zoomStep) : (1 + zoomStep);\n\t\t\t\t\t\tconst newZoom = Math.max(minZoom, Math.min(maxZoom, cam.zoom * Math.pow(direction, Math.abs(steps))));\n\n\t\t\t\t\t\tif (isoState && canvas) {\n\t\t\t\t\t\t\t// Iso-aware cursor-centered zoom: work in iso-screen space\n\t\t\t\t\t\t\tconst rect = canvas.getBoundingClientRect();\n\t\t\t\t\t\t\tconst screenOffX = inputState.pointer.position.x - (rect.left + rect.width / 2);\n\t\t\t\t\t\t\tconst screenOffY = inputState.pointer.position.y - (rect.top + rect.height / 2);\n\n\t\t\t\t\t\t\t// Inlined worldToIso — avoids cross-plugin import\n\t\t\t\t\t\t\tconst halfW = isoState.tileWidth / 2;\n\t\t\t\t\t\t\tconst halfH = isoState.tileHeight / 2;\n\t\t\t\t\t\t\tconst camIsoX = (cam.x - cam.y) * halfW + isoState.originX;\n\t\t\t\t\t\t\tconst camIsoY = (cam.x + cam.y) * halfH + isoState.originY;\n\t\t\t\t\t\t\tconst isoBeforeX = camIsoX + screenOffX / cam.zoom;\n\t\t\t\t\t\t\tconst isoBeforeY = camIsoY + screenOffY / cam.zoom;\n\n\t\t\t\t\t\t\tcam.zoom = newZoom;\n\n\t\t\t\t\t\t\t// New camera iso position so the same point stays under cursor\n\t\t\t\t\t\t\tconst newCamIsoX = isoBeforeX - screenOffX / newZoom;\n\t\t\t\t\t\t\tconst newCamIsoY = isoBeforeY - screenOffY / newZoom;\n\n\t\t\t\t\t\t\t// Inlined isoToWorld\n\t\t\t\t\t\t\tconst relX = newCamIsoX - isoState.originX;\n\t\t\t\t\t\t\tconst relY = newCamIsoY - isoState.originY;\n\t\t\t\t\t\t\tcam.x = relX / isoState.tileWidth + relY / isoState.tileHeight;\n\t\t\t\t\t\t\tcam.y = -relX / isoState.tileWidth + relY / isoState.tileHeight;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Pixel-space cursor-centered zoom\n\t\t\t\t\t\t\tconst worldBefore = screenToWorld(\n\t\t\t\t\t\t\t\tinputState.pointer.position.x,\n\t\t\t\t\t\t\t\tinputState.pointer.position.y,\n\t\t\t\t\t\t\t\tcameraState,\n\t\t\t\t\t\t\t);\n\n\t\t\t\t\t\t\tcam.zoom = newZoom;\n\n\t\t\t\t\t\t\tcam.x = worldBefore.x - (inputState.pointer.position.x - cameraState.viewportWidth / 2) / newZoom;\n\t\t\t\t\t\t\tcam.y = worldBefore.y - (inputState.pointer.position.y - cameraState.viewportHeight / 2) / newZoom;\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t}\n\n\t\t\t// camera-pan: conditionally registered when pan option is provided\n\t\t\tif (panConfig) {\n\t\t\t\ttype PanInputState = { actions: { isActive(action: string): boolean } };\n\n\t\t\t\tconst {\n\t\t\t\t\tspeed,\n\t\t\t\t\tactions: panActions,\n\t\t\t\t} = panConfig;\n\n\t\t\t\tconst actionUp = panActions?.up ?? 'panUp';\n\t\t\t\tconst actionDown = panActions?.down ?? 'panDown';\n\t\t\t\tconst actionLeft = panActions?.left ?? 'panLeft';\n\t\t\t\tconst actionRight = panActions?.right ?? 'panRight';\n\n\t\t\t\tlet panActive = false;\n\n\t\t\t\tworld\n\t\t\t\t\t.addSystem('camera-pan')\n\t\t\t\t\t.setPriority(420)\n\t\t\t\t\t.inPhase('preUpdate')\n\t\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t\t.setOnInitialize((ecs) => {\n\t\t\t\t\t\tconst inputState = ecs.tryGetResource<PanInputState>('inputState');\n\t\t\t\t\t\tif (!inputState) {\n\t\t\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t\t\t'[camera] pan requires the input plugin. Pan will be disabled.',\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tpanActive = true;\n\t\t\t\t\t})\n\t\t\t\t\t.setProcess(({ ecs, dt }) => {\n\t\t\t\t\t\tif (!panActive) return;\n\n\t\t\t\t\t\tconst inputState = ecs.tryGetResource<PanInputState>('inputState');\n\t\t\t\t\t\tif (!inputState) return;\n\n\t\t\t\t\t\tconst delta = (speed / cameraState.zoom) * dt;\n\t\t\t\t\t\tconst dx = (inputState.actions.isActive(actionRight) ? 1 : 0)\n\t\t\t\t\t\t\t- (inputState.actions.isActive(actionLeft) ? 1 : 0);\n\t\t\t\t\t\tconst dy = (inputState.actions.isActive(actionDown) ? 1 : 0)\n\t\t\t\t\t\t\t- (inputState.actions.isActive(actionUp) ? 1 : 0);\n\n\t\t\t\t\t\tif (dx !== 0 || dy !== 0) {\n\t\t\t\t\t\t\tcameraState.setPosition(\n\t\t\t\t\t\t\t\tcameraState.x + dx * delta,\n\t\t\t\t\t\t\t\tcameraState.y + dy * delta,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t}\n\t\t});\n}\n",
|
|
6
|
-
"/**\n * Selection Plugin for ECSpresso\n *\n * Provides pointer-driven entity selection via box-drag and click.\n * Entities with a `selectable` component can be selected by the user.\n * Selected entities receive a `selected` component that other systems\n * can query for.\n *\n * Requires the input plugin (for pointer state) and the renderer2D plugin\n * (for graphics rendering of the selection box).\n *\n * Camera-aware: when a `cameraState` resource is present (from the camera\n * plugin), pointer coordinates are automatically converted to world space\n * for hit-testing. The selection box overlay remains in screen space.\n */\n\nimport { Graphics } from 'pixi.js';\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type { WorldConfigFrom } from 'ecspresso';\nimport type { InputResourceTypes } from './input';\nimport type { Renderer2DComponentTypes, Renderer2DResourceTypes } from '../rendering/renderer2D';\nimport type { CameraState } from '../spatial/camera';\nimport { screenToWorld } from '../spatial/camera';\n\n// ==================== Component Types ====================\n\n/**\n * Component types provided by the selection plugin.\n */\nexport interface SelectionComponentTypes {\n\t/** Tag marking an entity as eligible for selection */\n\tselectable: true;\n\t/** Tag marking an entity as currently selected (added/removed dynamically) */\n\tselected: true;\n}\n\n// ==================== Resource Types ====================\n\n/**\n * Internal state tracking the current drag selection.\n */\nexport interface SelectionState {\n\tdragStart: { x: number; y: number };\n\tboxEntityId: number | null;\n}\n\n/**\n * Resource types provided by the selection plugin.\n */\nexport interface SelectionResourceTypes {\n\tselectionState: SelectionState;\n}\n\n// ==================== WorldConfig ====================\n\n/**\n * WorldConfig representing the selection plugin's provided types.\n */\nexport type SelectionWorldConfig = WorldConfigFrom<SelectionComponentTypes, {}, SelectionResourceTypes>;\n\n// ==================== Dependency Types ====================\n\ntype SelectionRequires = WorldConfigFrom<Renderer2DComponentTypes, {}, InputResourceTypes & Renderer2DResourceTypes>;\n\n// ==================== Plugin Options ====================\n\n/**\n * Configuration options for the selection plugin.\n */\nexport interface SelectionPluginOptions<G extends string = 'selection'> extends BasePluginOptions<G> {\n\t/** Minimum drag distance (px) to trigger box select vs click select (default: 5) */\n\tclickThreshold?: number;\n\t/** Selection box fill color (default: 0x00FF00) */\n\tboxFillColor?: number;\n\t/** Selection box fill alpha (default: 0.15) */\n\tboxFillAlpha?: number;\n\t/** Selection box stroke color (default: 0x00FF00) */\n\tboxStrokeColor?: number;\n\t/** Selection box stroke alpha (default: 0.8) */\n\tboxStrokeAlpha?: number;\n\t/** Tint applied to selected entities' sprites (default: 0x44FF44) */\n\tselectedTint?: number;\n\t/** Render layer for the selection box entity (default: undefined) */\n\trenderLayer?: string;\n}\n\n// ==================== Helper Functions ====================\n\n/**\n * Create a selectable component.\n *\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createTransform(100, 200),\n * sprite,\n * ...createSelectable(),\n * });\n * ```\n */\nexport function createSelectable(): Pick<SelectionComponentTypes, 'selectable'> {\n\treturn { selectable: true };\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a selection plugin for ECSpresso.\n *\n * Provides:\n * - Box-drag selection (left-click drag to select multiple entities)\n * - Click selection (left-click to select a single entity)\n * - Visual feedback (configurable sprite tint for selected entities)\n * - Selection box overlay (rendered as a PixiJS Graphics entity)\n * - Automatic camera-awareness when cameraState resource is present\n *\n * Requires the input plugin and renderer2D plugin to be installed.\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createRenderer2DPlugin({ renderLayers: ['game', 'ui'] }))\n * .withPlugin(createInputPlugin())\n * .withPlugin(createSelectionPlugin({ renderLayer: 'ui' }))\n * .build();\n *\n * await ecs.initialize();\n *\n * ecs.spawn({\n * sprite,\n * ...createTransform(100, 200),\n * ...createSelectable(),\n * });\n * ```\n */\nexport function createSelectionPlugin<G extends string = 'selection'>(\n\toptions?: SelectionPluginOptions<G>\n) {\n\tconst {\n\t\tsystemGroup = 'selection',\n\t\tpriority = 100,\n\t\tphase = 'preUpdate',\n\t\tclickThreshold = 5,\n\t\tboxFillColor = 0x00FF00,\n\t\tboxFillAlpha = 0.15,\n\t\tboxStrokeColor = 0x00FF00,\n\t\tboxStrokeAlpha = 0.8,\n\t\tselectedTint = 0x44FF44,\n\t\trenderLayer,\n\t} = options ?? {};\n\n\t// Pre-allocate draw options to avoid per-frame allocations during drag\n\tconst fillOptions = { color: boxFillColor, alpha: boxFillAlpha };\n\tconst strokeOptions = { color: boxStrokeColor, width: 1.5, alpha: boxStrokeAlpha };\n\n\treturn definePlugin('selection')\n\t\t.withComponentTypes<SelectionComponentTypes>()\n\t\t.withResourceTypes<SelectionResourceTypes>()\n\t\t.withLabels<'selection-input' | 'selection-visual'>()\n\t\t.withGroups<G>()\n\t\t.requires<SelectionRequires>()\n\t\t.install((world) => {\n\t\t\tworld.addResource('selectionState', {\n\t\t\t\tdragStart: { x: 0, y: 0 },\n\t\t\t\tboxEntityId: null,\n\t\t\t});\n\n\t\t\tlet preventContextMenu: ((e: Event) => void) | null = null;\n\n\t\t\tworld\n\t\t\t\t.addSystem('selection-input')\n\t\t\t\t.setPriority(priority)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('selectables', {\n\t\t\t\t\twith: ['selectable', 'worldTransform'],\n\t\t\t\t})\n\t\t\t\t.addQuery('currentlySelected', {\n\t\t\t\t\twith: ['selected'],\n\t\t\t\t})\n\t\t\t\t.withResources(['inputState', 'selectionState', 'pixiApp'])\n\t\t\t\t.setOnInitialize((ecs) => {\n\t\t\t\t\tconst pixiApp = ecs.getResource('pixiApp');\n\t\t\t\t\tpreventContextMenu = (e: Event) => e.preventDefault();\n\t\t\t\t\tpixiApp.canvas.addEventListener('contextmenu', preventContextMenu);\n\t\t\t\t})\n\t\t\t\t.setOnDetach((ecs) => {\n\t\t\t\t\tif (!preventContextMenu) return;\n\t\t\t\t\tconst pixiApp = ecs.getResource('pixiApp');\n\t\t\t\t\tpixiApp.canvas.removeEventListener('contextmenu', preventContextMenu);\n\t\t\t\t\tpreventContextMenu = null;\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, ecs, resources }) => {\n\t\t\t\t\tconst { inputState: input, selectionState } = resources;\n\t\t\t\t\tconst pointer = input.pointer;\n\n\t\t\t\t\t// Start drag\n\t\t\t\t\tif (pointer.justPressed(0)) {\n\t\t\t\t\t\t// Clean up any orphaned box entity from an interrupted drag\n\t\t\t\t\t\tif (selectionState.boxEntityId !== null) {\n\t\t\t\t\t\t\tecs.commands.removeEntity(selectionState.boxEntityId);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tselectionState.dragStart.x = pointer.position.x;\n\t\t\t\t\t\tselectionState.dragStart.y = pointer.position.y;\n\n\t\t\t\t\t\tconst boxEntity = ecs.spawn({\n\t\t\t\t\t\t\tgraphics: new Graphics(),\n\t\t\t\t\t\t});\n\t\t\t\t\t\tif (renderLayer) {\n\t\t\t\t\t\t\tecs.addComponent(boxEntity.id, 'renderLayer', renderLayer);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tselectionState.boxEntityId = boxEntity.id;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Update drag visual (screen-space — no camera conversion)\n\t\t\t\t\tif (pointer.isDown(0) && selectionState.boxEntityId !== null) {\n\t\t\t\t\t\tconst g = ecs.getComponent(selectionState.boxEntityId, 'graphics');\n\t\t\t\t\t\tif (!g) return;\n\n\t\t\t\t\t\tconst startX = selectionState.dragStart.x;\n\t\t\t\t\t\tconst startY = selectionState.dragStart.y;\n\t\t\t\t\t\tconst curX = pointer.position.x;\n\t\t\t\t\t\tconst curY = pointer.position.y;\n\t\t\t\t\t\tconst minX = Math.min(startX, curX);\n\t\t\t\t\t\tconst minY = Math.min(startY, curY);\n\t\t\t\t\t\tconst w = Math.abs(curX - startX);\n\t\t\t\t\t\tconst h = Math.abs(curY - startY);\n\n\t\t\t\t\t\tg.clear();\n\t\t\t\t\t\tg.rect(minX, minY, w, h);\n\t\t\t\t\t\tg.fill(fillOptions);\n\t\t\t\t\t\tg.stroke(strokeOptions);\n\t\t\t\t\t}\n\n\t\t\t\t\t// End drag — perform selection\n\t\t\t\t\tif (!pointer.justReleased(0) || selectionState.boxEntityId === null) return;\n\n\t\t\t\t\tconst startX = selectionState.dragStart.x;\n\t\t\t\t\tconst startY = selectionState.dragStart.y;\n\t\t\t\t\tconst endX = pointer.position.x;\n\t\t\t\t\tconst endY = pointer.position.y;\n\n\t\t\t\t\tconst w = Math.abs(endX - startX);\n\t\t\t\t\tconst h = Math.abs(endY - startY);\n\n\t\t\t\t\t// Clear current selection\n\t\t\t\t\tfor (const entity of queries.currentlySelected) {\n\t\t\t\t\t\tecs.removeComponent(entity.id, 'selected');\n\t\t\t\t\t}\n\n\t\t\t\t\tconst isClick = w < clickThreshold && h < clickThreshold;\n\n\t\t\t\t\t// Convert screen coords to world space for hit-testing\n\t\t\t\t\tconst camState = ecs.tryGetResource('cameraState') as CameraState | undefined;\n\t\t\t\t\tconst worldEnd = camState\n\t\t\t\t\t\t? screenToWorld(endX, endY, camState)\n\t\t\t\t\t\t: { x: endX, y: endY };\n\n\t\t\t\t\tif (isClick) {\n\t\t\t\t\t\tconst clickRadiusSq = 400; // 20px radius in world space\n\t\t\t\t\t\tlet nearestId: number | null = null;\n\t\t\t\t\t\tlet nearestDistSq = Infinity;\n\n\t\t\t\t\t\tfor (const entity of queries.selectables) {\n\t\t\t\t\t\t\tconst { worldTransform } = entity.components;\n\t\t\t\t\t\t\tconst dx = worldTransform.x - worldEnd.x;\n\t\t\t\t\t\t\tconst dy = worldTransform.y - worldEnd.y;\n\t\t\t\t\t\t\tconst distSq = dx * dx + dy * dy;\n\t\t\t\t\t\t\tif (distSq < clickRadiusSq && distSq < nearestDistSq) {\n\t\t\t\t\t\t\t\tnearestDistSq = distSq;\n\t\t\t\t\t\t\t\tnearestId = entity.id;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (nearestId !== null) {\n\t\t\t\t\t\t\tecs.addComponent(nearestId, 'selected', true);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst worldStart = camState\n\t\t\t\t\t\t\t? screenToWorld(startX, startY, camState)\n\t\t\t\t\t\t\t: { x: startX, y: startY };\n\t\t\t\t\t\tconst minWX = Math.min(worldStart.x, worldEnd.x);\n\t\t\t\t\t\tconst maxWX = Math.max(worldStart.x, worldEnd.x);\n\t\t\t\t\t\tconst minWY = Math.min(worldStart.y, worldEnd.y);\n\t\t\t\t\t\tconst maxWY = Math.max(worldStart.y, worldEnd.y);\n\n\t\t\t\t\t\tfor (const entity of queries.selectables) {\n\t\t\t\t\t\t\tconst { worldTransform } = entity.components;\n\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\tworldTransform.x >= minWX &&\n\t\t\t\t\t\t\t\tworldTransform.x <= maxWX &&\n\t\t\t\t\t\t\t\tworldTransform.y >= minWY &&\n\t\t\t\t\t\t\t\tworldTransform.y <= maxWY\n\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\tecs.addComponent(entity.id, 'selected', true);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tecs.commands.removeEntity(selectionState.boxEntityId);\n\t\t\t\t\tselectionState.boxEntityId = null;\n\t\t\t\t});\n\n\t\t\t// Visual feedback via enter/exit callbacks — only fires on selection change\n\t\t\tworld\n\t\t\t\t.addSystem('selection-visual')\n\t\t\t\t.setPriority(priority)\n\t\t\t\t.inPhase('render')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('selectedUnits', {\n\t\t\t\t\twith: ['selected', 'sprite'],\n\t\t\t\t})\n\t\t\t\t.setOnEntityEnter('selectedUnits', ({ entity }) => {\n\t\t\t\t\tentity.components.sprite.tint = selectedTint;\n\t\t\t\t})\n\t\t\t\t.addQuery('deselectedUnits', {\n\t\t\t\t\twith: ['selectable', 'sprite'],\n\t\t\t\t\twithout: ['selected'],\n\t\t\t\t})\n\t\t\t\t.setOnEntityEnter('deselectedUnits', ({ entity }) => {\n\t\t\t\t\tentity.components.sprite.tint = 0xFFFFFF;\n\t\t\t\t});\n\t\t});\n}\n"
|
|
5
|
+
"/**\n * Camera / Viewport Plugin for ECSpresso\n *\n * Provides a declarative camera with world/screen coordinate conversion, smooth follow,\n * trauma-based shake, bounds clamping, cursor-centered zoom, and logical viewport dimensions.\n *\n * This plugin is renderer-agnostic. PixiJS or other renderer integration (applying\n * cameraState to a container/stage transform) is the consumer's responsibility.\n *\n * Camera uses its own x/y/zoom/rotation rather than localTransform/worldTransform.\n * It reads the target entity's worldTransform for follow, but doesn't participate\n * in the transform hierarchy itself.\n */\n\nimport { definePlugin } from 'ecspresso';\nimport type { SystemPhase } from 'ecspresso';\nimport type ECSpresso from 'ecspresso';\nimport type { ComponentsConfig, ResourcesConfig } from '../../type-utils';\nimport type { TransformWorldConfig } from './transform';\n\n// ==================== Component Types ====================\n\nexport interface Camera {\n\tx: number;\n\ty: number;\n\tzoom: number;\n\trotation: number;\n}\n\nexport interface CameraFollow {\n\ttarget: number;\n\tsmoothing: number;\n\tdeadzoneX: number;\n\tdeadzoneY: number;\n\toffsetX: number;\n\toffsetY: number;\n}\n\nexport interface CameraShake {\n\ttrauma: number;\n\ttraumaDecay: number;\n\tmaxOffsetX: number;\n\tmaxOffsetY: number;\n\tmaxRotation: number;\n}\n\nexport interface CameraBounds {\n\tminX: number;\n\tminY: number;\n\tmaxX: number;\n\tmaxY: number;\n}\n\nexport interface CameraComponentTypes {\n\tcamera: Camera;\n\tcameraFollow: CameraFollow;\n\tcameraShake: CameraShake;\n\tcameraBounds: CameraBounds;\n}\n\n// ==================== Resource Types ====================\n\nexport interface FollowOptions {\n\tsmoothing?: number;\n\tdeadzoneX?: number;\n\tdeadzoneY?: number;\n\toffsetX?: number;\n\toffsetY?: number;\n}\n\nexport type EntityHandle = { id: number };\n\nexport interface CameraState {\n\t// Read-only data (synced from camera entity each frame)\n\tx: number;\n\ty: number;\n\tzoom: number;\n\trotation: number;\n\tshakeOffsetX: number;\n\tshakeOffsetY: number;\n\tshakeRotation: number;\n\tviewportWidth: number;\n\tviewportHeight: number;\n\tentityId: number;\n\n\t// Mutation methods\n\tfollow(target: number | EntityHandle, options?: FollowOptions): void;\n\tunfollow(): void;\n\tsetPosition(x: number, y: number): void;\n\tsetZoom(zoom: number): void;\n\tsetRotation(rotation: number): void;\n\tsetBounds(minX: number, minY: number, maxX: number, maxY: number): void;\n\tclearBounds(): void;\n\taddTrauma(amount: number): void;\n}\n\nexport interface CameraResourceTypes {\n\tcameraState: CameraState;\n}\n\n// ==================== Plugin Options ====================\n\nexport interface CameraPluginOptions<G extends string = 'camera'> {\n\tviewportWidth?: number;\n\tviewportHeight?: number;\n\tinitial?: {\n\t\tx?: number;\n\t\ty?: number;\n\t\tzoom?: number;\n\t\trotation?: number;\n\t};\n\tfollow?: FollowOptions;\n\tshake?: boolean | Partial<Omit<CameraShake, 'trauma'>>;\n\tbounds?:\n\t\t| { minX: number; minY: number; maxX: number; maxY: number }\n\t\t| [number, number, number, number];\n\tzoom?: {\n\t\tzoomStep?: number;\n\t\tminZoom?: number;\n\t\tmaxZoom?: number;\n\t};\n\tpan?: {\n\t\tspeed: number;\n\t\tactions?: {\n\t\t\tup?: string;\n\t\t\tdown?: string;\n\t\t\tleft?: string;\n\t\t\tright?: string;\n\t\t};\n\t};\n\tsystemGroup?: G;\n\tphase?: SystemPhase;\n\trandomFn?: () => number;\n}\n\n// ==================== Default Values ====================\n\nconst DEFAULT_SHAKE: Readonly<Omit<CameraShake, 'trauma'>> = {\n\ttraumaDecay: 1,\n\tmaxOffsetX: 10,\n\tmaxOffsetY: 10,\n\tmaxRotation: 0.05,\n};\n\nconst DEFAULT_FOLLOW: Readonly<Omit<CameraFollow, 'target'>> = {\n\tsmoothing: 5,\n\tdeadzoneX: 0,\n\tdeadzoneY: 0,\n\toffsetX: 0,\n\toffsetY: 0,\n};\n\n// ==================== Coordinate Conversion ====================\n\nexport function worldToScreen(\n\tworldX: number,\n\tworldY: number,\n\tstate: CameraState,\n): { x: number; y: number } {\n\tconst dx = worldX - (state.x + state.shakeOffsetX);\n\tconst dy = worldY - (state.y + state.shakeOffsetY);\n\n\tconst angle = -(state.rotation + state.shakeRotation);\n\tconst cos = Math.cos(angle);\n\tconst sin = Math.sin(angle);\n\tconst rx = dx * cos - dy * sin;\n\tconst ry = dx * sin + dy * cos;\n\n\treturn {\n\t\tx: rx * state.zoom + state.viewportWidth / 2,\n\t\ty: ry * state.zoom + state.viewportHeight / 2,\n\t};\n}\n\nexport function screenToWorld(\n\tscreenX: number,\n\tscreenY: number,\n\tstate: CameraState,\n): { x: number; y: number } {\n\tconst cx = (screenX - state.viewportWidth / 2) / state.zoom;\n\tconst cy = (screenY - state.viewportHeight / 2) / state.zoom;\n\n\tconst angle = state.rotation + state.shakeRotation;\n\tconst cos = Math.cos(angle);\n\tconst sin = Math.sin(angle);\n\tconst rx = cx * cos - cy * sin;\n\tconst ry = cx * sin + cy * cos;\n\n\treturn {\n\t\tx: rx + state.x + state.shakeOffsetX,\n\t\ty: ry + state.y + state.shakeOffsetY,\n\t};\n}\n\n// ==================== Internal Helpers ====================\n\nfunction resolveTarget(target: number | EntityHandle): number {\n\treturn typeof target === 'number' ? target : target.id;\n}\n\nfunction resolveShakeOptions(shake: true | Partial<Omit<CameraShake, 'trauma'>>): CameraShake {\n\tconst opts = shake === true ? {} : shake;\n\treturn {\n\t\ttrauma: 0,\n\t\ttraumaDecay: opts.traumaDecay ?? DEFAULT_SHAKE.traumaDecay,\n\t\tmaxOffsetX: opts.maxOffsetX ?? DEFAULT_SHAKE.maxOffsetX,\n\t\tmaxOffsetY: opts.maxOffsetY ?? DEFAULT_SHAKE.maxOffsetY,\n\t\tmaxRotation: opts.maxRotation ?? DEFAULT_SHAKE.maxRotation,\n\t};\n}\n\nfunction resolveBounds(\n\tbounds: { minX: number; minY: number; maxX: number; maxY: number } | [number, number, number, number],\n): CameraBounds {\n\tif (Array.isArray(bounds)) {\n\t\treturn { minX: bounds[0], minY: bounds[1], maxX: bounds[2], maxY: bounds[3] };\n\t}\n\treturn { ...bounds };\n}\n\nfunction resolveFollowOptions(options?: FollowOptions): Omit<CameraFollow, 'target'> {\n\treturn {\n\t\tsmoothing: options?.smoothing ?? DEFAULT_FOLLOW.smoothing,\n\t\tdeadzoneX: options?.deadzoneX ?? DEFAULT_FOLLOW.deadzoneX,\n\t\tdeadzoneY: options?.deadzoneY ?? DEFAULT_FOLLOW.deadzoneY,\n\t\toffsetX: options?.offsetX ?? DEFAULT_FOLLOW.offsetX,\n\t\toffsetY: options?.offsetY ?? DEFAULT_FOLLOW.offsetY,\n\t};\n}\n\n// ==================== Plugin Factory ====================\n\ntype CameraWorldConfig =\n\tComponentsConfig<CameraComponentTypes>\n\t& ResourcesConfig<CameraResourceTypes>;\n\ntype CameraLabels =\n\t| 'camera-init'\n\t| 'camera-follow'\n\t| 'camera-shake-update'\n\t| 'camera-bounds'\n\t| 'camera-state-sync'\n\t| 'camera-zoom'\n\t| 'camera-pan';\n\nexport function createCameraPlugin<G extends string = 'camera'>(\n\toptions?: CameraPluginOptions<G>,\n) {\n\tconst {\n\t\tviewportWidth = 800,\n\t\tviewportHeight = 600,\n\t\tinitial,\n\t\tfollow: followConfig,\n\t\tshake: shakeConfig,\n\t\tbounds: boundsConfig,\n\t\tzoom: zoomConfig,\n\t\tpan: panConfig,\n\t\tsystemGroup = 'camera',\n\t\tphase = 'postUpdate',\n\t\trandomFn = Math.random,\n\t} = options ?? {};\n\n\treturn definePlugin('camera')\n\t\t.withComponentTypes<CameraComponentTypes>()\n\t\t.withResourceTypes<CameraResourceTypes>()\n\t\t.withLabels<CameraLabels>()\n\t\t.withGroups<G>()\n\t\t.requires<TransformWorldConfig>()\n\t\t.install((world) => {\n\t\t\t// Build mutation methods as closures over the world reference.\n\t\t\t// The cameraState resource is created immediately with placeholder methods,\n\t\t\t// then the init system populates entityId and wires up real methods.\n\n\t\t\tconst cameraState: CameraState = {\n\t\t\t\tx: initial?.x ?? 0,\n\t\t\t\ty: initial?.y ?? 0,\n\t\t\t\tzoom: initial?.zoom ?? 1,\n\t\t\t\trotation: initial?.rotation ?? 0,\n\t\t\t\tshakeOffsetX: 0,\n\t\t\t\tshakeOffsetY: 0,\n\t\t\t\tshakeRotation: 0,\n\t\t\t\tviewportWidth,\n\t\t\t\tviewportHeight,\n\t\t\t\tentityId: -1,\n\n\t\t\t\t// Mutation methods — wired up after camera entity is spawned\n\t\t\t\tfollow: () => {},\n\t\t\t\tunfollow: () => {},\n\t\t\t\tsetPosition: () => {},\n\t\t\t\tsetZoom: () => {},\n\t\t\t\tsetRotation: () => {},\n\t\t\t\tsetBounds: () => {},\n\t\t\t\tclearBounds: () => {},\n\t\t\t\taddTrauma: () => {},\n\t\t\t};\n\n\t\t\tworld.addResource('cameraState', cameraState);\n\n\t\t\t// camera-init: spawns camera entity and wires up mutation closures\n\t\t\tworld\n\t\t\t\t.addSystem('camera-init')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setOnInitialize((ecs: ECSpresso<CameraWorldConfig & TransformWorldConfig>) => {\n\t\t\t\t\t// Spawn with required camera component\n\t\t\t\t\tconst entity = ecs.spawn({\n\t\t\t\t\t\tcamera: {\n\t\t\t\t\t\t\tx: initial?.x ?? 0,\n\t\t\t\t\t\t\ty: initial?.y ?? 0,\n\t\t\t\t\t\t\tzoom: initial?.zoom ?? 1,\n\t\t\t\t\t\t\trotation: initial?.rotation ?? 0,\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\n\t\t\t\t\t// Conditionally add optional components\n\t\t\t\t\tif (followConfig) {\n\t\t\t\t\t\tecs.addComponent(entity.id, 'cameraFollow', {\n\t\t\t\t\t\t\ttarget: -1,\n\t\t\t\t\t\t\t...resolveFollowOptions(followConfig),\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\tif (shakeConfig) {\n\t\t\t\t\t\tecs.addComponent(entity.id, 'cameraShake', resolveShakeOptions(shakeConfig));\n\t\t\t\t\t}\n\n\t\t\t\t\tif (boundsConfig) {\n\t\t\t\t\t\tecs.addComponent(entity.id, 'cameraBounds', resolveBounds(boundsConfig));\n\t\t\t\t\t}\n\t\t\t\t\tcameraState.entityId = entity.id;\n\n\t\t\t\t\t// Wire up mutation methods\n\t\t\t\t\tcameraState.follow = (target: number | EntityHandle, opts?: FollowOptions) => {\n\t\t\t\t\t\tconst targetId = resolveTarget(target);\n\t\t\t\t\t\tconst followData: CameraFollow = {\n\t\t\t\t\t\t\ttarget: targetId,\n\t\t\t\t\t\t\t...resolveFollowOptions(opts),\n\t\t\t\t\t\t};\n\t\t\t\t\t\tconst existing = ecs.getComponent(cameraState.entityId, 'cameraFollow');\n\t\t\t\t\t\tif (existing) {\n\t\t\t\t\t\t\texisting.target = followData.target;\n\t\t\t\t\t\t\texisting.smoothing = followData.smoothing;\n\t\t\t\t\t\t\texisting.deadzoneX = followData.deadzoneX;\n\t\t\t\t\t\t\texisting.deadzoneY = followData.deadzoneY;\n\t\t\t\t\t\t\texisting.offsetX = followData.offsetX;\n\t\t\t\t\t\t\texisting.offsetY = followData.offsetY;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tecs.addComponent(cameraState.entityId, 'cameraFollow', followData);\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\n\t\t\t\t\tcameraState.unfollow = () => {\n\t\t\t\t\t\tconst existing = ecs.getComponent(cameraState.entityId, 'cameraFollow');\n\t\t\t\t\t\tif (existing) {\n\t\t\t\t\t\t\tecs.removeComponent(cameraState.entityId, 'cameraFollow');\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\n\t\t\t\t\tcameraState.setPosition = (x: number, y: number) => {\n\t\t\t\t\t\tconst camera = ecs.getComponent(cameraState.entityId, 'camera');\n\t\t\t\t\t\tif (!camera) return;\n\t\t\t\t\t\tcamera.x = x;\n\t\t\t\t\t\tcamera.y = y;\n\t\t\t\t\t};\n\n\t\t\t\t\tcameraState.setZoom = (zoom: number) => {\n\t\t\t\t\t\tconst camera = ecs.getComponent(cameraState.entityId, 'camera');\n\t\t\t\t\t\tif (!camera) return;\n\t\t\t\t\t\tcamera.zoom = zoom;\n\t\t\t\t\t};\n\n\t\t\t\t\tcameraState.setRotation = (rotation: number) => {\n\t\t\t\t\t\tconst camera = ecs.getComponent(cameraState.entityId, 'camera');\n\t\t\t\t\t\tif (!camera) return;\n\t\t\t\t\t\tcamera.rotation = rotation;\n\t\t\t\t\t};\n\n\t\t\t\t\tcameraState.setBounds = (minX: number, minY: number, maxX: number, maxY: number) => {\n\t\t\t\t\t\tconst existing = ecs.getComponent(cameraState.entityId, 'cameraBounds');\n\t\t\t\t\t\tif (existing) {\n\t\t\t\t\t\t\texisting.minX = minX;\n\t\t\t\t\t\t\texisting.minY = minY;\n\t\t\t\t\t\t\texisting.maxX = maxX;\n\t\t\t\t\t\t\texisting.maxY = maxY;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tecs.addComponent(cameraState.entityId, 'cameraBounds', { minX, minY, maxX, maxY });\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\n\t\t\t\t\tcameraState.clearBounds = () => {\n\t\t\t\t\t\tconst existing = ecs.getComponent(cameraState.entityId, 'cameraBounds');\n\t\t\t\t\t\tif (existing) {\n\t\t\t\t\t\t\tecs.removeComponent(cameraState.entityId, 'cameraBounds');\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\n\t\t\t\t\tcameraState.addTrauma = (amount: number) => {\n\t\t\t\t\t\tconst shake = ecs.getComponent(cameraState.entityId, 'cameraShake');\n\t\t\t\t\t\tif (shake) {\n\t\t\t\t\t\t\tshake.trauma = Math.min(1, Math.max(0, shake.trauma + amount));\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tecs.addComponent(cameraState.entityId, 'cameraShake', {\n\t\t\t\t\t\t\t\t...resolveShakeOptions(true),\n\t\t\t\t\t\t\t\ttrauma: Math.min(1, Math.max(0, amount)),\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\t\t\t\t});\n\n\t\t\t// camera-follow: priority 400 (after transform propagation at 500)\n\t\t\tworld\n\t\t\t\t.addSystem('camera-follow')\n\t\t\t\t.setPriority(400)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('cameras', {\n\t\t\t\t\twith: ['camera', 'cameraFollow'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, dt, ecs }) => {\n\t\t\t\t\tconst t = Math.min(1, dt);\n\t\t\t\t\tfor (const entity of queries.cameras) {\n\t\t\t\t\t\tconst { camera, cameraFollow } = entity.components;\n\t\t\t\t\t\tif (cameraFollow.target < 0) continue;\n\n\t\t\t\t\t\tconst targetWorld = ecs.getComponent(cameraFollow.target, 'worldTransform');\n\t\t\t\t\t\tif (!targetWorld) continue;\n\n\t\t\t\t\t\tconst goalX = targetWorld.x + cameraFollow.offsetX;\n\t\t\t\t\t\tconst goalY = targetWorld.y + cameraFollow.offsetY;\n\t\t\t\t\t\tconst dx = goalX - camera.x;\n\t\t\t\t\t\tconst dy = goalY - camera.y;\n\n\t\t\t\t\t\tif (Math.abs(dx) > cameraFollow.deadzoneX) {\n\t\t\t\t\t\t\tconst sign = dx > 0 ? 1 : -1;\n\t\t\t\t\t\t\tconst excessX = dx - sign * cameraFollow.deadzoneX;\n\t\t\t\t\t\t\tconst factor = Math.min(1, cameraFollow.smoothing * t);\n\t\t\t\t\t\t\tcamera.x += excessX * factor;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (Math.abs(dy) > cameraFollow.deadzoneY) {\n\t\t\t\t\t\t\tconst sign = dy > 0 ? 1 : -1;\n\t\t\t\t\t\t\tconst excessY = dy - sign * cameraFollow.deadzoneY;\n\t\t\t\t\t\t\tconst factor = Math.min(1, cameraFollow.smoothing * t);\n\t\t\t\t\t\t\tcamera.y += excessY * factor;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// camera-shake-update: priority 390\n\t\t\tworld\n\t\t\t\t.addSystem('camera-shake-update')\n\t\t\t\t.setPriority(390)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('shakeCameras', {\n\t\t\t\t\twith: ['camera', 'cameraShake'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, dt }) => {\n\t\t\t\t\tfor (const entity of queries.shakeCameras) {\n\t\t\t\t\t\tconst { cameraShake } = entity.components;\n\t\t\t\t\t\tcameraShake.trauma = Math.max(0, cameraShake.trauma - cameraShake.traumaDecay * dt);\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// camera-bounds: priority 380\n\t\t\tworld\n\t\t\t\t.addSystem('camera-bounds')\n\t\t\t\t.setPriority(380)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('boundedCameras', {\n\t\t\t\t\twith: ['camera', 'cameraBounds'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries }) => {\n\t\t\t\t\tfor (const entity of queries.boundedCameras) {\n\t\t\t\t\t\tconst { camera, cameraBounds } = entity.components;\n\t\t\t\t\t\tconst halfW = cameraState.viewportWidth / (2 * camera.zoom);\n\t\t\t\t\t\tconst halfH = cameraState.viewportHeight / (2 * camera.zoom);\n\n\t\t\t\t\t\tconst effectiveMinX = cameraBounds.minX + halfW;\n\t\t\t\t\t\tconst effectiveMaxX = cameraBounds.maxX - halfW;\n\t\t\t\t\t\tconst effectiveMinY = cameraBounds.minY + halfH;\n\t\t\t\t\t\tconst effectiveMaxY = cameraBounds.maxY - halfH;\n\n\t\t\t\t\t\tif (effectiveMinX > effectiveMaxX) {\n\t\t\t\t\t\t\tcamera.x = (cameraBounds.minX + cameraBounds.maxX) / 2;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tcamera.x = Math.max(effectiveMinX, Math.min(effectiveMaxX, camera.x));\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (effectiveMinY > effectiveMaxY) {\n\t\t\t\t\t\t\tcamera.y = (cameraBounds.minY + cameraBounds.maxY) / 2;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tcamera.y = Math.max(effectiveMinY, Math.min(effectiveMaxY, camera.y));\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// camera-state-sync: priority 370\n\t\t\tworld\n\t\t\t\t.addSystem('camera-state-sync')\n\t\t\t\t.setPriority(370)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setProcess(({ ecs }) => {\n\t\t\t\t\tconst camera = ecs.getComponent(cameraState.entityId, 'camera');\n\t\t\t\t\tif (!camera) {\n\t\t\t\t\t\tcameraState.x = 0;\n\t\t\t\t\t\tcameraState.y = 0;\n\t\t\t\t\t\tcameraState.zoom = 1;\n\t\t\t\t\t\tcameraState.rotation = 0;\n\t\t\t\t\t\tcameraState.shakeOffsetX = 0;\n\t\t\t\t\t\tcameraState.shakeOffsetY = 0;\n\t\t\t\t\t\tcameraState.shakeRotation = 0;\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tcameraState.x = camera.x;\n\t\t\t\t\tcameraState.y = camera.y;\n\t\t\t\t\tcameraState.zoom = camera.zoom;\n\t\t\t\t\tcameraState.rotation = camera.rotation;\n\n\t\t\t\t\tconst shake = ecs.getComponent(cameraState.entityId, 'cameraShake');\n\t\t\t\t\tif (shake && shake.trauma > 0) {\n\t\t\t\t\t\tconst intensity = shake.trauma * shake.trauma;\n\t\t\t\t\t\tcameraState.shakeOffsetX = shake.maxOffsetX * intensity * (randomFn() * 2 - 1);\n\t\t\t\t\t\tcameraState.shakeOffsetY = shake.maxOffsetY * intensity * (randomFn() * 2 - 1);\n\t\t\t\t\t\tcameraState.shakeRotation = shake.maxRotation * intensity * (randomFn() * 2 - 1);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tcameraState.shakeOffsetX = 0;\n\t\t\t\t\t\tcameraState.shakeOffsetY = 0;\n\t\t\t\t\t\tcameraState.shakeRotation = 0;\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// camera-zoom: conditionally registered when zoom option is provided\n\t\t\tif (zoomConfig) {\n\t\t\t\tconst {\n\t\t\t\t\tzoomStep = 0.1,\n\t\t\t\t\tminZoom = 0.1,\n\t\t\t\t\tmaxZoom = 10,\n\t\t\t\t} = zoomConfig;\n\n\t\t\t\ttype ZoomInputState = { pointer: { position: { x: number; y: number } } };\n\n\t\t\t\tlet pendingSteps = 0;\n\t\t\t\tlet zoomActive = false;\n\t\t\t\tlet canvas: HTMLCanvasElement | undefined;\n\t\t\t\tlet isoState: { tileWidth: number; tileHeight: number; originX: number; originY: number } | undefined;\n\n\t\t\t\tfunction onWheel(e: WheelEvent) {\n\t\t\t\t\te.preventDefault();\n\t\t\t\t\tpendingSteps += Math.sign(e.deltaY);\n\t\t\t\t}\n\n\t\t\t\tworld\n\t\t\t\t\t.addSystem('camera-zoom')\n\t\t\t\t\t.setPriority(410)\n\t\t\t\t\t.inPhase('preUpdate')\n\t\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t\t.addQuery('cameras', {\n\t\t\t\t\t\twith: ['camera'],\n\t\t\t\t\t})\n\t\t\t\t\t.setOnInitialize((ecs) => {\n\t\t\t\t\t\t// Check for required dependencies\n\t\t\t\t\t\tconst inputState = ecs.tryGetResource<ZoomInputState>('inputState');\n\t\t\t\t\t\tconst pixiApp = ecs.tryGetResource<{ canvas: HTMLCanvasElement }>('pixiApp');\n\n\t\t\t\t\t\tif (!inputState || !pixiApp) {\n\t\t\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t\t\t'[camera] zoom requires the input plugin and renderer2D plugin. ' +\n\t\t\t\t\t\t\t\t'Zoom will be disabled.',\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tcanvas = pixiApp.canvas;\n\t\t\t\t\t\tcanvas.addEventListener('wheel', onWheel as EventListener, { passive: false });\n\n\t\t\t\t\t\t// Detect isometric projection for iso-aware cursor-centered zoom\n\t\t\t\t\t\tisoState = ecs.tryGetResource<{\n\t\t\t\t\t\t\ttileWidth: number; tileHeight: number;\n\t\t\t\t\t\t\toriginX: number; originY: number;\n\t\t\t\t\t\t}>('isoProjection');\n\n\t\t\t\t\t\tzoomActive = true;\n\t\t\t\t\t})\n\t\t\t\t\t.setOnDetach(() => {\n\t\t\t\t\t\tif (!zoomActive || !canvas) return;\n\t\t\t\t\t\tcanvas.removeEventListener('wheel', onWheel as EventListener);\n\t\t\t\t\t})\n\t\t\t\t\t.setProcess(({ queries, ecs }) => {\n\t\t\t\t\t\tif (!zoomActive || pendingSteps === 0) return;\n\n\t\t\t\t\t\tconst steps = pendingSteps;\n\t\t\t\t\t\tpendingSteps = 0;\n\n\t\t\t\t\t\tconst [cameraEntity] = queries.cameras;\n\t\t\t\t\t\tif (!cameraEntity) return;\n\n\t\t\t\t\t\tconst cam = cameraEntity.components.camera;\n\t\t\t\t\t\tconst inputState = ecs.tryGetResource<ZoomInputState>('inputState');\n\t\t\t\t\t\tif (!inputState) return;\n\n\t\t\t\t\t\t// Apply zoom — proportional to number of wheel steps\n\t\t\t\t\t\tconst direction = steps > 0 ? (1 - zoomStep) : (1 + zoomStep);\n\t\t\t\t\t\tconst newZoom = Math.max(minZoom, Math.min(maxZoom, cam.zoom * Math.pow(direction, Math.abs(steps))));\n\n\t\t\t\t\t\tif (isoState && canvas) {\n\t\t\t\t\t\t\t// Iso-aware cursor-centered zoom: work in iso-screen space\n\t\t\t\t\t\t\tconst rect = canvas.getBoundingClientRect();\n\t\t\t\t\t\t\tconst screenOffX = inputState.pointer.position.x - (rect.left + rect.width / 2);\n\t\t\t\t\t\t\tconst screenOffY = inputState.pointer.position.y - (rect.top + rect.height / 2);\n\n\t\t\t\t\t\t\t// Inlined worldToIso — avoids cross-plugin import\n\t\t\t\t\t\t\tconst halfW = isoState.tileWidth / 2;\n\t\t\t\t\t\t\tconst halfH = isoState.tileHeight / 2;\n\t\t\t\t\t\t\tconst camIsoX = (cam.x - cam.y) * halfW + isoState.originX;\n\t\t\t\t\t\t\tconst camIsoY = (cam.x + cam.y) * halfH + isoState.originY;\n\t\t\t\t\t\t\tconst isoBeforeX = camIsoX + screenOffX / cam.zoom;\n\t\t\t\t\t\t\tconst isoBeforeY = camIsoY + screenOffY / cam.zoom;\n\n\t\t\t\t\t\t\tcam.zoom = newZoom;\n\n\t\t\t\t\t\t\t// New camera iso position so the same point stays under cursor\n\t\t\t\t\t\t\tconst newCamIsoX = isoBeforeX - screenOffX / newZoom;\n\t\t\t\t\t\t\tconst newCamIsoY = isoBeforeY - screenOffY / newZoom;\n\n\t\t\t\t\t\t\t// Inlined isoToWorld\n\t\t\t\t\t\t\tconst relX = newCamIsoX - isoState.originX;\n\t\t\t\t\t\t\tconst relY = newCamIsoY - isoState.originY;\n\t\t\t\t\t\t\tcam.x = relX / isoState.tileWidth + relY / isoState.tileHeight;\n\t\t\t\t\t\t\tcam.y = -relX / isoState.tileWidth + relY / isoState.tileHeight;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Pixel-space cursor-centered zoom\n\t\t\t\t\t\t\tconst worldBefore = screenToWorld(\n\t\t\t\t\t\t\t\tinputState.pointer.position.x,\n\t\t\t\t\t\t\t\tinputState.pointer.position.y,\n\t\t\t\t\t\t\t\tcameraState,\n\t\t\t\t\t\t\t);\n\n\t\t\t\t\t\t\tcam.zoom = newZoom;\n\n\t\t\t\t\t\t\tcam.x = worldBefore.x - (inputState.pointer.position.x - cameraState.viewportWidth / 2) / newZoom;\n\t\t\t\t\t\t\tcam.y = worldBefore.y - (inputState.pointer.position.y - cameraState.viewportHeight / 2) / newZoom;\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t}\n\n\t\t\t// camera-pan: conditionally registered when pan option is provided\n\t\t\tif (panConfig) {\n\t\t\t\ttype PanInputState = { actions: { isActive(action: string): boolean } };\n\n\t\t\t\tconst {\n\t\t\t\t\tspeed,\n\t\t\t\t\tactions: panActions,\n\t\t\t\t} = panConfig;\n\n\t\t\t\tconst actionUp = panActions?.up ?? 'panUp';\n\t\t\t\tconst actionDown = panActions?.down ?? 'panDown';\n\t\t\t\tconst actionLeft = panActions?.left ?? 'panLeft';\n\t\t\t\tconst actionRight = panActions?.right ?? 'panRight';\n\n\t\t\t\tlet panActive = false;\n\n\t\t\t\tworld\n\t\t\t\t\t.addSystem('camera-pan')\n\t\t\t\t\t.setPriority(420)\n\t\t\t\t\t.inPhase('preUpdate')\n\t\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t\t.setOnInitialize((ecs) => {\n\t\t\t\t\t\tconst inputState = ecs.tryGetResource<PanInputState>('inputState');\n\t\t\t\t\t\tif (!inputState) {\n\t\t\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t\t\t'[camera] pan requires the input plugin. Pan will be disabled.',\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tpanActive = true;\n\t\t\t\t\t})\n\t\t\t\t\t.setProcess(({ ecs, dt }) => {\n\t\t\t\t\t\tif (!panActive) return;\n\n\t\t\t\t\t\tconst inputState = ecs.tryGetResource<PanInputState>('inputState');\n\t\t\t\t\t\tif (!inputState) return;\n\n\t\t\t\t\t\tconst delta = (speed / cameraState.zoom) * dt;\n\t\t\t\t\t\tconst dx = (inputState.actions.isActive(actionRight) ? 1 : 0)\n\t\t\t\t\t\t\t- (inputState.actions.isActive(actionLeft) ? 1 : 0);\n\t\t\t\t\t\tconst dy = (inputState.actions.isActive(actionDown) ? 1 : 0)\n\t\t\t\t\t\t\t- (inputState.actions.isActive(actionUp) ? 1 : 0);\n\n\t\t\t\t\t\tif (dx !== 0 || dy !== 0) {\n\t\t\t\t\t\t\tcameraState.setPosition(\n\t\t\t\t\t\t\t\tcameraState.x + dx * delta,\n\t\t\t\t\t\t\t\tcameraState.y + dy * delta,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t}\n\t\t});\n}\n",
|
|
6
|
+
"/**\n * Selection Plugin for ECSpresso\n *\n * Provides pointer-driven entity selection via box-drag and click.\n * Entities with a `selectable` component can be selected by the user.\n * Selected entities receive a `selected` component that other systems\n * can query for.\n *\n * Requires the input plugin (for pointer state) and the renderer2D plugin\n * (for graphics rendering of the selection box).\n *\n * Camera-aware: when a `cameraState` resource is present (from the camera\n * plugin), pointer coordinates are automatically converted to world space\n * for hit-testing. The selection box overlay remains in screen space.\n */\n\nimport { Graphics } from 'pixi.js';\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type { ComponentsConfig, ResourcesConfig } from 'ecspresso';\nimport type { InputResourceTypes } from './input';\nimport type { Renderer2DComponentTypes, Renderer2DResourceTypes } from '../rendering/renderer2D';\nimport type { CameraState } from '../spatial/camera';\nimport { screenToWorld } from '../spatial/camera';\n\n// ==================== Component Types ====================\n\n/**\n * Component types provided by the selection plugin.\n */\nexport interface SelectionComponentTypes {\n\t/** Tag marking an entity as eligible for selection */\n\tselectable: true;\n\t/** Tag marking an entity as currently selected (added/removed dynamically) */\n\tselected: true;\n}\n\n// ==================== Resource Types ====================\n\n/**\n * Internal state tracking the current drag selection.\n */\nexport interface SelectionState {\n\tdragStart: { x: number; y: number };\n\tboxEntityId: number | null;\n}\n\n/**\n * Resource types provided by the selection plugin.\n */\nexport interface SelectionResourceTypes {\n\tselectionState: SelectionState;\n}\n\n// ==================== WorldConfig ====================\n\n/**\n * WorldConfig representing the selection plugin's provided types.\n */\nexport type SelectionWorldConfig =\n\tComponentsConfig<SelectionComponentTypes>\n\t& ResourcesConfig<SelectionResourceTypes>;\n\n// ==================== Dependency Types ====================\n\ntype SelectionRequires =\n\tComponentsConfig<Renderer2DComponentTypes>\n\t& ResourcesConfig<InputResourceTypes & Renderer2DResourceTypes>;\n\n// ==================== Plugin Options ====================\n\n/**\n * Configuration options for the selection plugin.\n */\nexport interface SelectionPluginOptions<G extends string = 'selection'> extends BasePluginOptions<G> {\n\t/** Minimum drag distance (px) to trigger box select vs click select (default: 5) */\n\tclickThreshold?: number;\n\t/** Selection box fill color (default: 0x00FF00) */\n\tboxFillColor?: number;\n\t/** Selection box fill alpha (default: 0.15) */\n\tboxFillAlpha?: number;\n\t/** Selection box stroke color (default: 0x00FF00) */\n\tboxStrokeColor?: number;\n\t/** Selection box stroke alpha (default: 0.8) */\n\tboxStrokeAlpha?: number;\n\t/** Tint applied to selected entities' sprites (default: 0x44FF44) */\n\tselectedTint?: number;\n\t/** Render layer for the selection box entity (default: undefined) */\n\trenderLayer?: string;\n}\n\n// ==================== Helper Functions ====================\n\n/**\n * Create a selectable component.\n *\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createTransform(100, 200),\n * sprite,\n * ...createSelectable(),\n * });\n * ```\n */\nexport function createSelectable(): Pick<SelectionComponentTypes, 'selectable'> {\n\treturn { selectable: true };\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a selection plugin for ECSpresso.\n *\n * Provides:\n * - Box-drag selection (left-click drag to select multiple entities)\n * - Click selection (left-click to select a single entity)\n * - Visual feedback (configurable sprite tint for selected entities)\n * - Selection box overlay (rendered as a PixiJS Graphics entity)\n * - Automatic camera-awareness when cameraState resource is present\n *\n * Requires the input plugin and renderer2D plugin to be installed.\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createRenderer2DPlugin({ renderLayers: ['game', 'ui'] }))\n * .withPlugin(createInputPlugin())\n * .withPlugin(createSelectionPlugin({ renderLayer: 'ui' }))\n * .build();\n *\n * await ecs.initialize();\n *\n * ecs.spawn({\n * sprite,\n * ...createTransform(100, 200),\n * ...createSelectable(),\n * });\n * ```\n */\nexport function createSelectionPlugin<G extends string = 'selection'>(\n\toptions?: SelectionPluginOptions<G>\n) {\n\tconst {\n\t\tsystemGroup = 'selection',\n\t\tpriority = 100,\n\t\tphase = 'preUpdate',\n\t\tclickThreshold = 5,\n\t\tboxFillColor = 0x00FF00,\n\t\tboxFillAlpha = 0.15,\n\t\tboxStrokeColor = 0x00FF00,\n\t\tboxStrokeAlpha = 0.8,\n\t\tselectedTint = 0x44FF44,\n\t\trenderLayer,\n\t} = options ?? {};\n\n\t// Pre-allocate draw options to avoid per-frame allocations during drag\n\tconst fillOptions = { color: boxFillColor, alpha: boxFillAlpha };\n\tconst strokeOptions = { color: boxStrokeColor, width: 1.5, alpha: boxStrokeAlpha };\n\n\treturn definePlugin('selection')\n\t\t.withComponentTypes<SelectionComponentTypes>()\n\t\t.withResourceTypes<SelectionResourceTypes>()\n\t\t.withLabels<'selection-input' | 'selection-visual'>()\n\t\t.withGroups<G>()\n\t\t.requires<SelectionRequires>()\n\t\t.install((world) => {\n\t\t\tworld.addResource('selectionState', {\n\t\t\t\tdragStart: { x: 0, y: 0 },\n\t\t\t\tboxEntityId: null,\n\t\t\t});\n\n\t\t\tlet preventContextMenu: ((e: Event) => void) | null = null;\n\n\t\t\tworld\n\t\t\t\t.addSystem('selection-input')\n\t\t\t\t.setPriority(priority)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('selectables', {\n\t\t\t\t\twith: ['selectable', 'worldTransform'],\n\t\t\t\t})\n\t\t\t\t.addQuery('currentlySelected', {\n\t\t\t\t\twith: ['selected'],\n\t\t\t\t})\n\t\t\t\t.withResources(['inputState', 'selectionState', 'pixiApp'])\n\t\t\t\t.setOnInitialize((ecs) => {\n\t\t\t\t\tconst pixiApp = ecs.getResource('pixiApp');\n\t\t\t\t\tpreventContextMenu = (e: Event) => e.preventDefault();\n\t\t\t\t\tpixiApp.canvas.addEventListener('contextmenu', preventContextMenu);\n\t\t\t\t})\n\t\t\t\t.setOnDetach((ecs) => {\n\t\t\t\t\tif (!preventContextMenu) return;\n\t\t\t\t\tconst pixiApp = ecs.getResource('pixiApp');\n\t\t\t\t\tpixiApp.canvas.removeEventListener('contextmenu', preventContextMenu);\n\t\t\t\t\tpreventContextMenu = null;\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, ecs, resources }) => {\n\t\t\t\t\tconst { inputState: input, selectionState } = resources;\n\t\t\t\t\tconst pointer = input.pointer;\n\n\t\t\t\t\t// Start drag\n\t\t\t\t\tif (pointer.justPressed(0)) {\n\t\t\t\t\t\t// Clean up any orphaned box entity from an interrupted drag\n\t\t\t\t\t\tif (selectionState.boxEntityId !== null) {\n\t\t\t\t\t\t\tecs.commands.removeEntity(selectionState.boxEntityId);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tselectionState.dragStart.x = pointer.position.x;\n\t\t\t\t\t\tselectionState.dragStart.y = pointer.position.y;\n\n\t\t\t\t\t\tconst boxEntity = ecs.spawn({\n\t\t\t\t\t\t\tgraphics: new Graphics(),\n\t\t\t\t\t\t});\n\t\t\t\t\t\tif (renderLayer) {\n\t\t\t\t\t\t\tecs.addComponent(boxEntity.id, 'renderLayer', renderLayer);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tselectionState.boxEntityId = boxEntity.id;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Update drag visual (screen-space — no camera conversion)\n\t\t\t\t\tif (pointer.isDown(0) && selectionState.boxEntityId !== null) {\n\t\t\t\t\t\tconst g = ecs.getComponent(selectionState.boxEntityId, 'graphics');\n\t\t\t\t\t\tif (!g) return;\n\n\t\t\t\t\t\tconst startX = selectionState.dragStart.x;\n\t\t\t\t\t\tconst startY = selectionState.dragStart.y;\n\t\t\t\t\t\tconst curX = pointer.position.x;\n\t\t\t\t\t\tconst curY = pointer.position.y;\n\t\t\t\t\t\tconst minX = Math.min(startX, curX);\n\t\t\t\t\t\tconst minY = Math.min(startY, curY);\n\t\t\t\t\t\tconst w = Math.abs(curX - startX);\n\t\t\t\t\t\tconst h = Math.abs(curY - startY);\n\n\t\t\t\t\t\tg.clear();\n\t\t\t\t\t\tg.rect(minX, minY, w, h);\n\t\t\t\t\t\tg.fill(fillOptions);\n\t\t\t\t\t\tg.stroke(strokeOptions);\n\t\t\t\t\t}\n\n\t\t\t\t\t// End drag — perform selection\n\t\t\t\t\tif (!pointer.justReleased(0) || selectionState.boxEntityId === null) return;\n\n\t\t\t\t\tconst startX = selectionState.dragStart.x;\n\t\t\t\t\tconst startY = selectionState.dragStart.y;\n\t\t\t\t\tconst endX = pointer.position.x;\n\t\t\t\t\tconst endY = pointer.position.y;\n\n\t\t\t\t\tconst w = Math.abs(endX - startX);\n\t\t\t\t\tconst h = Math.abs(endY - startY);\n\n\t\t\t\t\t// Clear current selection\n\t\t\t\t\tfor (const entity of queries.currentlySelected) {\n\t\t\t\t\t\tecs.removeComponent(entity.id, 'selected');\n\t\t\t\t\t}\n\n\t\t\t\t\tconst isClick = w < clickThreshold && h < clickThreshold;\n\n\t\t\t\t\t// Convert screen coords to world space for hit-testing\n\t\t\t\t\tconst camState = ecs.tryGetResource('cameraState') as CameraState | undefined;\n\t\t\t\t\tconst worldEnd = camState\n\t\t\t\t\t\t? screenToWorld(endX, endY, camState)\n\t\t\t\t\t\t: { x: endX, y: endY };\n\n\t\t\t\t\tif (isClick) {\n\t\t\t\t\t\tconst clickRadiusSq = 400; // 20px radius in world space\n\t\t\t\t\t\tlet nearestId: number | null = null;\n\t\t\t\t\t\tlet nearestDistSq = Infinity;\n\n\t\t\t\t\t\tfor (const entity of queries.selectables) {\n\t\t\t\t\t\t\tconst { worldTransform } = entity.components;\n\t\t\t\t\t\t\tconst dx = worldTransform.x - worldEnd.x;\n\t\t\t\t\t\t\tconst dy = worldTransform.y - worldEnd.y;\n\t\t\t\t\t\t\tconst distSq = dx * dx + dy * dy;\n\t\t\t\t\t\t\tif (distSq < clickRadiusSq && distSq < nearestDistSq) {\n\t\t\t\t\t\t\t\tnearestDistSq = distSq;\n\t\t\t\t\t\t\t\tnearestId = entity.id;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (nearestId !== null) {\n\t\t\t\t\t\t\tecs.addComponent(nearestId, 'selected', true);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst worldStart = camState\n\t\t\t\t\t\t\t? screenToWorld(startX, startY, camState)\n\t\t\t\t\t\t\t: { x: startX, y: startY };\n\t\t\t\t\t\tconst minWX = Math.min(worldStart.x, worldEnd.x);\n\t\t\t\t\t\tconst maxWX = Math.max(worldStart.x, worldEnd.x);\n\t\t\t\t\t\tconst minWY = Math.min(worldStart.y, worldEnd.y);\n\t\t\t\t\t\tconst maxWY = Math.max(worldStart.y, worldEnd.y);\n\n\t\t\t\t\t\tfor (const entity of queries.selectables) {\n\t\t\t\t\t\t\tconst { worldTransform } = entity.components;\n\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\tworldTransform.x >= minWX &&\n\t\t\t\t\t\t\t\tworldTransform.x <= maxWX &&\n\t\t\t\t\t\t\t\tworldTransform.y >= minWY &&\n\t\t\t\t\t\t\t\tworldTransform.y <= maxWY\n\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\tecs.addComponent(entity.id, 'selected', true);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tecs.commands.removeEntity(selectionState.boxEntityId);\n\t\t\t\t\tselectionState.boxEntityId = null;\n\t\t\t\t});\n\n\t\t\t// Visual feedback via enter/exit callbacks — only fires on selection change\n\t\t\tworld\n\t\t\t\t.addSystem('selection-visual')\n\t\t\t\t.setPriority(priority)\n\t\t\t\t.inPhase('render')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('selectedUnits', {\n\t\t\t\t\twith: ['selected', 'sprite'],\n\t\t\t\t})\n\t\t\t\t.setOnEntityEnter('selectedUnits', ({ entity }) => {\n\t\t\t\t\tentity.components.sprite.tint = selectedTint;\n\t\t\t\t})\n\t\t\t\t.addQuery('deselectedUnits', {\n\t\t\t\t\twith: ['selectable', 'sprite'],\n\t\t\t\t\twithout: ['selected'],\n\t\t\t\t})\n\t\t\t\t.setOnEntityEnter('deselectedUnits', ({ entity }) => {\n\t\t\t\t\tentity.components.sprite.tint = 0xFFFFFF;\n\t\t\t\t});\n\t\t});\n}\n"
|
|
7
7
|
],
|
|
8
|
-
"mappings": "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",
|
|
8
|
+
"mappings": "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",
|
|
9
9
|
"debugId": "A100A9606EA8CF3164756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Enables `sortableChildren` on the root container at initialization.
|
|
10
10
|
*/
|
|
11
11
|
import type { BasePluginOptions } from 'ecspresso';
|
|
12
|
-
import type {
|
|
12
|
+
import type { ComponentsConfig, ResourcesConfig } from '../../type-utils';
|
|
13
13
|
import type { TransformComponentTypes } from '../spatial/transform';
|
|
14
14
|
import type { Renderer2DComponentTypes, Renderer2DResourceTypes } from '../rendering/renderer2D';
|
|
15
15
|
/**
|
|
@@ -20,7 +20,7 @@ import type { Renderer2DComponentTypes, Renderer2DResourceTypes } from '../rende
|
|
|
20
20
|
export interface IsoDepthSortComponentTypes {
|
|
21
21
|
depthOffset: number;
|
|
22
22
|
}
|
|
23
|
-
type IsoDepthSortRequires =
|
|
23
|
+
type IsoDepthSortRequires = ComponentsConfig<TransformComponentTypes & Pick<Renderer2DComponentTypes, 'sprite' | 'graphics' | 'container'>> & ResourcesConfig<Renderer2DResourceTypes>;
|
|
24
24
|
export interface IsoDepthSortPluginOptions<G extends string = 'isometric'> extends BasePluginOptions<G> {
|
|
25
25
|
/** Custom depth function. Receives world-space x/y, returns a sort key.
|
|
26
26
|
* Default: `(x, y) => x + y` */
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/plugins/isometric/depth-sort.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * Isometric Depth Sort Plugin for ECSpresso\n *\n * Sets PixiJS `zIndex` on entities based on their world-space position,\n * ensuring correct visual overlap in isometric rendering. Entities with\n * higher world X + Y values render in front.\n *\n * Requires `rootContainer` from the renderer2D plugin.\n * Enables `sortableChildren` on the root container at initialization.\n */\n\nimport { definePlugin } from 'ecspresso';\nimport type { BasePluginOptions } from 'ecspresso';\nimport type {
|
|
5
|
+
"/**\n * Isometric Depth Sort Plugin for ECSpresso\n *\n * Sets PixiJS `zIndex` on entities based on their world-space position,\n * ensuring correct visual overlap in isometric rendering. Entities with\n * higher world X + Y values render in front.\n *\n * Requires `rootContainer` from the renderer2D plugin.\n * Enables `sortableChildren` on the root container at initialization.\n */\n\nimport { definePlugin } from 'ecspresso';\nimport type { BasePluginOptions } from 'ecspresso';\nimport type { ComponentsConfig, ResourcesConfig } from '../../type-utils';\nimport type { TransformComponentTypes } from '../spatial/transform';\nimport type { Renderer2DComponentTypes, Renderer2DResourceTypes } from '../rendering/renderer2D';\n\n// ==================== Component Types ====================\n\n/**\n * Optional component that offsets an entity's depth sort value.\n * Entities with a positive depthOffset render in front of entities\n * at the same world position (e.g., a player on top of a ground tile).\n */\nexport interface IsoDepthSortComponentTypes {\n\tdepthOffset: number;\n}\n\ntype IsoDepthSortRequires =\n\tComponentsConfig<TransformComponentTypes & Pick<Renderer2DComponentTypes, 'sprite' | 'graphics' | 'container'>>\n\t& ResourcesConfig<Renderer2DResourceTypes>;\n\n// ==================== Plugin Options ====================\n\nexport interface IsoDepthSortPluginOptions<G extends string = 'isometric'> extends BasePluginOptions<G> {\n\t/** Custom depth function. Receives world-space x/y, returns a sort key.\n\t * Default: `(x, y) => x + y` */\n\tdepthFn?: (worldX: number, worldY: number) => number;\n}\n\n// ==================== Default Depth Function ====================\n\nfunction defaultDepthFn(worldX: number, worldY: number): number {\n\treturn worldX + worldY;\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create an isometric depth sort plugin.\n *\n * Adds a render-phase system that sets PixiJS `zIndex` based on world-space\n * position, enabling correct front-to-back ordering in isometric views.\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createRenderer2DPlugin({ ... }))\n * .withPlugin(createIsoDepthSortPlugin())\n * .build();\n * ```\n */\nexport function createIsoDepthSortPlugin<G extends string = 'isometric'>(\n\toptions?: IsoDepthSortPluginOptions<G>,\n) {\n\tconst {\n\t\tdepthFn = defaultDepthFn,\n\t\tsystemGroup = 'isometric',\n\t} = options ?? {};\n\n\treturn definePlugin('isometric-depth-sort')\n\t\t.withComponentTypes<IsoDepthSortComponentTypes>()\n\t\t.requires<IsoDepthSortRequires>()\n\t\t.withGroups<G>()\n\t\t.install((world) => {\n\t\t\t// ==================== Init: Enable Sorting ====================\n\n\t\t\tworld\n\t\t\t\t.addSystem('isometric-depth-sort-init')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setOnInitialize((ecs) => {\n\t\t\t\t\tconst root = ecs.getResource('rootContainer');\n\t\t\t\t\troot.sortableChildren = true;\n\t\t\t\t});\n\n\t\t\t// ==================== Depth Sort System ====================\n\n\t\t\tworld\n\t\t\t\t.addSystem('isometric-depth-sort')\n\t\t\t\t.setPriority(350)\n\t\t\t\t.inPhase('render')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('sprites', {\n\t\t\t\t\twith: ['sprite', 'worldTransform'],\n\t\t\t\t\tchanged: ['worldTransform'],\n\t\t\t\t\toptional: ['depthOffset'],\n\t\t\t\t})\n\t\t\t\t.addQuery('graphics', {\n\t\t\t\t\twith: ['graphics', 'worldTransform'],\n\t\t\t\t\tchanged: ['worldTransform'],\n\t\t\t\t\toptional: ['depthOffset'],\n\t\t\t\t})\n\t\t\t\t.addQuery('containers', {\n\t\t\t\t\twith: ['container', 'worldTransform'],\n\t\t\t\t\tchanged: ['worldTransform'],\n\t\t\t\t\toptional: ['depthOffset'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries }) => {\n\t\t\t\t\tfor (const entity of queries.sprites) {\n\t\t\t\t\t\tconst { sprite, worldTransform, depthOffset } = entity.components;\n\t\t\t\t\t\tsprite.zIndex = depthFn(worldTransform.x, worldTransform.y) + (depthOffset ?? 0);\n\t\t\t\t\t}\n\n\t\t\t\t\tfor (const entity of queries.graphics) {\n\t\t\t\t\t\tconst { graphics, worldTransform, depthOffset } = entity.components;\n\t\t\t\t\t\tgraphics.zIndex = depthFn(worldTransform.x, worldTransform.y) + (depthOffset ?? 0);\n\t\t\t\t\t}\n\n\t\t\t\t\tfor (const entity of queries.containers) {\n\t\t\t\t\t\tconst { container, worldTransform, depthOffset } = entity.components;\n\t\t\t\t\t\tcontainer.zIndex = depthFn(worldTransform.x, worldTransform.y) + (depthOffset ?? 0);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": "2PAWA,uBAAS,
|
|
7
|
+
"mappings": "2PAWA,uBAAS,kBA+BT,SAAS,CAAc,CAAC,EAAgB,EAAwB,CAC/D,OAAO,EAAS,EAmBV,SAAS,CAAwD,CACvE,EACC,CACD,IACC,UAAU,EACV,cAAc,aACX,GAAW,CAAC,EAEhB,OAAO,EAAa,sBAAsB,EACxC,mBAA+C,EAC/C,SAA+B,EAC/B,WAAc,EACd,QAAQ,CAAC,IAAU,CAGnB,EACE,UAAU,2BAA2B,EACrC,QAAQ,CAAW,EACnB,gBAAgB,CAAC,IAAQ,CACzB,IAAM,EAAO,EAAI,YAAY,eAAe,EAC5C,EAAK,iBAAmB,GACxB,EAIF,EACE,UAAU,sBAAsB,EAChC,YAAY,GAAG,EACf,QAAQ,QAAQ,EAChB,QAAQ,CAAW,EACnB,SAAS,UAAW,CACpB,KAAM,CAAC,SAAU,gBAAgB,EACjC,QAAS,CAAC,gBAAgB,EAC1B,SAAU,CAAC,aAAa,CACzB,CAAC,EACA,SAAS,WAAY,CACrB,KAAM,CAAC,WAAY,gBAAgB,EACnC,QAAS,CAAC,gBAAgB,EAC1B,SAAU,CAAC,aAAa,CACzB,CAAC,EACA,SAAS,aAAc,CACvB,KAAM,CAAC,YAAa,gBAAgB,EACpC,QAAS,CAAC,gBAAgB,EAC1B,SAAU,CAAC,aAAa,CACzB,CAAC,EACA,WAAW,EAAG,aAAc,CAC5B,QAAW,KAAU,EAAQ,QAAS,CACrC,IAAQ,SAAQ,iBAAgB,eAAgB,EAAO,WACvD,EAAO,OAAS,EAAQ,EAAe,EAAG,EAAe,CAAC,GAAK,GAAe,GAG/E,QAAW,KAAU,EAAQ,SAAU,CACtC,IAAQ,WAAU,iBAAgB,eAAgB,EAAO,WACzD,EAAS,OAAS,EAAQ,EAAe,EAAG,EAAe,CAAC,GAAK,GAAe,GAGjF,QAAW,KAAU,EAAQ,WAAY,CACxC,IAAQ,YAAW,iBAAgB,eAAgB,EAAO,WAC1D,EAAU,OAAS,EAAQ,EAAe,EAAG,EAAe,CAAC,GAAK,GAAe,IAElF,EACF",
|
|
8
8
|
"debugId": "E8C9DCC3EA4239A864756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|