ecspresso 0.14.0 → 0.14.3
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/README.md +3 -6
- package/dist/index.js +2 -2
- package/dist/index.js.map +3 -3
- package/dist/plugins/ai/pathfinding.d.ts +163 -0
- package/dist/plugins/ai/pathfinding.js +4 -0
- package/dist/plugins/ai/pathfinding.js.map +10 -0
- package/dist/plugins/input/input.d.ts +105 -27
- package/dist/plugins/input/input.js +2 -2
- package/dist/plugins/input/input.js.map +3 -3
- package/dist/plugins/rendering/renderer3D.d.ts +23 -2
- package/dist/plugins/rendering/renderer3D.js +239 -184
- package/dist/plugins/rendering/renderer3D.js.map +5 -5
- package/dist/plugins/rendering/tilemap.d.ts +230 -0
- package/dist/plugins/rendering/tilemap.js +4 -0
- package/dist/plugins/rendering/tilemap.js.map +11 -0
- package/dist/plugins/spatial/camera3D.d.ts +30 -9
- package/dist/plugins/spatial/camera3D.js +2 -2
- package/dist/plugins/spatial/camera3D.js.map +3 -3
- package/dist/plugins/ui/ui.d.ts +116 -0
- package/dist/plugins/ui/ui.js +4 -0
- package/dist/plugins/ui/ui.js.map +11 -0
- package/dist/system-builder.d.ts +31 -0
- package/package.json +29 -5
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pathfinding Plugin for ECSpresso
|
|
3
|
+
*
|
|
4
|
+
* A* pathfinding on a weighted grid. Produces waypoint lists consumed by the
|
|
5
|
+
* steering plugin — the pathfinding system writes the `path` component and
|
|
6
|
+
* sets `moveTarget` to the first waypoint; the waypoint advancement handler
|
|
7
|
+
* listens for `arriveAtTarget` and advances to the next waypoint.
|
|
8
|
+
*
|
|
9
|
+
* Exports the pure `findPath(grid, start, goal, options?)` function for
|
|
10
|
+
* turn-based / non-realtime consumers that don't need the component dance.
|
|
11
|
+
*/
|
|
12
|
+
import { type BasePluginOptions } from 'ecspresso';
|
|
13
|
+
import type { WorldConfigFrom } from 'ecspresso';
|
|
14
|
+
import type { Vector2D } from '../../utils/math';
|
|
15
|
+
import type { TransformWorldConfig } from '../spatial/transform';
|
|
16
|
+
import type { SteeringWorldConfig } from '../physics/steering';
|
|
17
|
+
/** Flat-indexed cell position in a `NavGrid`. Transparent alias, not branded. */
|
|
18
|
+
export type CellIndex = number;
|
|
19
|
+
/**
|
|
20
|
+
* Grid topology. v1 ships `square4`; other values are accepted at construction
|
|
21
|
+
* but throw at `findPath` time.
|
|
22
|
+
*/
|
|
23
|
+
export type NavGridTopology = 'square4' | 'square8' | 'hex-pointy' | 'hex-flat';
|
|
24
|
+
/**
|
|
25
|
+
* Weighted navigation grid. Row-major storage (`idx = row * width + col`).
|
|
26
|
+
* Cell value `0` = impassable, `1`–`255` = traversal cost into that cell.
|
|
27
|
+
*/
|
|
28
|
+
export interface NavGrid {
|
|
29
|
+
readonly topology: NavGridTopology;
|
|
30
|
+
readonly width: number;
|
|
31
|
+
readonly height: number;
|
|
32
|
+
readonly cellSize: number;
|
|
33
|
+
readonly originX: number;
|
|
34
|
+
readonly originY: number;
|
|
35
|
+
readonly cells: Uint8Array;
|
|
36
|
+
worldToCell(wx: number, wy: number): CellIndex;
|
|
37
|
+
cellToWorld(idx: CellIndex): Vector2D;
|
|
38
|
+
cellFromXY(x: number, y: number): CellIndex;
|
|
39
|
+
cellToXY(idx: CellIndex): {
|
|
40
|
+
x: number;
|
|
41
|
+
y: number;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** Options accepted by `createNavGrid`. */
|
|
45
|
+
export interface CreateNavGridOptions {
|
|
46
|
+
topology?: NavGridTopology;
|
|
47
|
+
width: number;
|
|
48
|
+
height: number;
|
|
49
|
+
cellSize?: number;
|
|
50
|
+
originX?: number;
|
|
51
|
+
originY?: number;
|
|
52
|
+
cells?: Uint8Array;
|
|
53
|
+
defaultCost?: number;
|
|
54
|
+
}
|
|
55
|
+
/** Signals the pathfinding system to compute a route to `target`. */
|
|
56
|
+
export interface PathRequest {
|
|
57
|
+
target: Vector2D;
|
|
58
|
+
}
|
|
59
|
+
/** Active route; waypoints are in world-space, advanced by `currentIndex`. */
|
|
60
|
+
export interface Path {
|
|
61
|
+
waypoints: Vector2D[];
|
|
62
|
+
currentIndex: number;
|
|
63
|
+
}
|
|
64
|
+
/** Component types provided by the pathfinding plugin. */
|
|
65
|
+
export interface PathfindingComponentTypes {
|
|
66
|
+
pathRequest: PathRequest;
|
|
67
|
+
path: Path;
|
|
68
|
+
}
|
|
69
|
+
/** Fired when A* produces a route. `path` is empty when start is already at the goal. */
|
|
70
|
+
export interface PathFoundEvent {
|
|
71
|
+
entityId: number;
|
|
72
|
+
path: Vector2D[];
|
|
73
|
+
}
|
|
74
|
+
/** Fired when no path exists to the target. */
|
|
75
|
+
export interface PathBlockedEvent {
|
|
76
|
+
entityId: number;
|
|
77
|
+
}
|
|
78
|
+
/** Event types provided by the pathfinding plugin. */
|
|
79
|
+
export interface PathfindingEventTypes {
|
|
80
|
+
pathFound: PathFoundEvent;
|
|
81
|
+
pathBlocked: PathBlockedEvent;
|
|
82
|
+
}
|
|
83
|
+
/** Resource types provided by the pathfinding plugin. */
|
|
84
|
+
export interface PathfindingResourceTypes {
|
|
85
|
+
navGrid: NavGrid;
|
|
86
|
+
}
|
|
87
|
+
/** WorldConfig representing the pathfinding plugin's provided types. */
|
|
88
|
+
export type PathfindingWorldConfig = WorldConfigFrom<PathfindingComponentTypes, PathfindingEventTypes, PathfindingResourceTypes>;
|
|
89
|
+
export interface PathfindingPluginOptions<G extends string = 'ai'> extends BasePluginOptions<G> {
|
|
90
|
+
/** The navigation grid. Construct via `createNavGrid`. */
|
|
91
|
+
grid: NavGrid;
|
|
92
|
+
/** Max path requests processed per frame (default 4). */
|
|
93
|
+
maxRequestsPerFrame?: number;
|
|
94
|
+
/** Default `maxNodesExpanded` passed to A* per request (default 10_000). */
|
|
95
|
+
maxNodesExpanded?: number;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Create a weighted navigation grid.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```typescript
|
|
102
|
+
* const grid = createNavGrid({ width: 32, height: 32, cellSize: 16 });
|
|
103
|
+
* grid.cells[grid.cellFromXY(5, 5)] = 0; // block a cell
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
export declare function createNavGrid(options: CreateNavGridOptions): NavGrid;
|
|
107
|
+
/**
|
|
108
|
+
* Create a `pathRequest` component for spreading into `spawn()` / `addComponent()`.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```typescript
|
|
112
|
+
* ecs.spawn({
|
|
113
|
+
* ...createTransform(0, 0),
|
|
114
|
+
* ...createMoveSpeed(100),
|
|
115
|
+
* ...createPathRequest({ x: 200, y: 300 }),
|
|
116
|
+
* });
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
export declare function createPathRequest(target: Vector2D): Pick<PathfindingComponentTypes, 'pathRequest'>;
|
|
120
|
+
export interface FindPathOptions {
|
|
121
|
+
/** Cap on A* node expansions; returns `null` if exceeded. Default 10_000. */
|
|
122
|
+
maxNodesExpanded?: number;
|
|
123
|
+
/** Dynamic per-call obstacles layered on top of the static grid. */
|
|
124
|
+
blockedCells?: Set<CellIndex>;
|
|
125
|
+
/** Accept arrival within N cells of goal (topology-aware distance). Default 0. */
|
|
126
|
+
goalTolerance?: number;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Compute a path through `grid` from `start` to `goal`.
|
|
130
|
+
*
|
|
131
|
+
* Returns a list of cell indices starting with `start` and ending at a cell
|
|
132
|
+
* within `goalTolerance` of `goal`, or `null` if no such path exists within
|
|
133
|
+
* `maxNodesExpanded` expansions.
|
|
134
|
+
*
|
|
135
|
+
* `start` is always treated as passable (even if its grid cell is 0 or the
|
|
136
|
+
* cell is in `blockedCells`) — actors physics-pushed onto a wall still get a
|
|
137
|
+
* valid origin.
|
|
138
|
+
*/
|
|
139
|
+
export declare function findPath(grid: NavGrid, start: CellIndex, goal: CellIndex, options?: FindPathOptions): CellIndex[] | null;
|
|
140
|
+
/**
|
|
141
|
+
* Create a pathfinding plugin for ECSpresso.
|
|
142
|
+
*
|
|
143
|
+
* Requires the transform and steering plugins to be installed (entities need
|
|
144
|
+
* `worldTransform` for start-cell detection and `moveTarget`/`moveSpeed` for
|
|
145
|
+
* waypoint traversal).
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* ```typescript
|
|
149
|
+
* const grid = createNavGrid({ width: 32, height: 32, cellSize: 16 });
|
|
150
|
+
* const ecs = ECSpresso.create()
|
|
151
|
+
* .withPlugin(createTransformPlugin())
|
|
152
|
+
* .withPlugin(createSteeringPlugin())
|
|
153
|
+
* .withPlugin(createPathfindingPlugin({ grid }))
|
|
154
|
+
* .build();
|
|
155
|
+
*
|
|
156
|
+
* ecs.spawn({
|
|
157
|
+
* ...createTransform(0, 0),
|
|
158
|
+
* ...createMoveSpeed(100),
|
|
159
|
+
* ...createPathRequest({ x: 500, y: 300 }),
|
|
160
|
+
* });
|
|
161
|
+
* ```
|
|
162
|
+
*/
|
|
163
|
+
export declare function createPathfindingPlugin<G extends string = 'ai'>(options: PathfindingPluginOptions<G>): import("ecspresso").Plugin<import("ecspresso").WithResources<import("ecspresso").WithEvents<import("ecspresso").WithComponents<import("ecspresso").EmptyConfig, PathfindingComponentTypes>, PathfindingEventTypes>, PathfindingResourceTypes>, TransformWorldConfig & SteeringWorldConfig, "pathfinding-request" | "pathfinding-waypoint-advance", G, never, never>;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
var q=Object.defineProperty;var T=(j)=>j;function z(j,D){this[j]=T.bind(null,D)}var f=(j,D)=>{for(var A in D)q(j,A,{get:D[A],enumerable:!0,configurable:!0,set:z.bind(D,A)})};var y=(j,D)=>()=>(j&&(D=j(j=0)),D);var m=((j)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(j,{get:(D,A)=>(typeof require<"u"?require:D)[A]}):j)(function(j){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+j+'" is not supported')});import{definePlugin as C}from"ecspresso";var F={neighbors(j,D,A){let J=D%j.width,K=(D-J)/j.width,H=0;if(J>0)A[H++]=D-1;if(J<j.width-1)A[H++]=D+1;if(K>0)A[H++]=D-j.width;if(K<j.height-1)A[H++]=D+j.width;return H},stepCost(j,D,A){return j.cells[A]??0},heuristic(j,D,A){let J=D%j.width,K=(D-J)/j.width,H=A%j.width,E=(A-H)/j.width;return Math.abs(J-H)+Math.abs(K-E)}},k=(j)=>{let D=()=>{throw Error(`pathfinding: topology '${j}' is not implemented in v1`)};return{neighbors:D,stepCost:D,heuristic:D}},b=Object.freeze({square4:F,square8:k("square8"),"hex-pointy":k("hex-pointy"),"hex-flat":k("hex-flat")});function x(j){let D=j.topology??"square4",A=j.cellSize??32,J=j.originX??0,K=j.originY??0,{width:H,height:E}=j,_=j.defaultCost??1;if(!Number.isInteger(H)||H<=0)throw Error(`pathfinding: width must be a positive integer, got ${H}`);if(!Number.isInteger(E)||E<=0)throw Error(`pathfinding: height must be a positive integer, got ${E}`);if(A<=0)throw Error(`pathfinding: cellSize must be > 0, got ${A}`);if(_<0||_>255)throw Error(`pathfinding: defaultCost must be in 0–255, got ${_}`);let U=H*E,Q=j.cells??new Uint8Array(U).fill(_);if(Q.length!==U)throw Error(`pathfinding: cells length ${Q.length} does not match width*height ${U}`);let $=1/A;return{topology:D,width:H,height:E,cellSize:A,originX:J,originY:K,cells:Q,worldToCell:(W,N)=>{let B=Math.floor((W-J)*$),Z=Math.floor((N-K)*$),X=B<0?0:B>=H?H-1:B;return(Z<0?0:Z>=E?E-1:Z)*H+X},cellToWorld:(W)=>{let N=W%H,B=(W-N)/H;return{x:J+(N+0.5)*A,y:K+(B+0.5)*A}},cellFromXY:(W,N)=>N*H+W,cellToXY:(W)=>{let N=W%H;return{x:N,y:(W-N)/H}}}}function l(j){return{pathRequest:{target:{x:j.x,y:j.y}}}}function O(j,D,A){let J=j.size;j.size=J+1;while(J>0){let K=J-1>>1;if((j.priorities[K]??0)<=A)break;j.ids[J]=j.ids[K]??0,j.priorities[J]=j.priorities[K]??0,J=K}j.ids[J]=D,j.priorities[J]=A}function P(j){let D=j.ids[0]??-1,A=j.size-1;if(j.size=A,A<=0)return D;let J=j.ids[A]??0,K=j.priorities[A]??0,H=0,E=A>>1;while(H<E){let _=(H<<1)+1,U=_+1;if(U<A&&(j.priorities[U]??0)<(j.priorities[_]??0))_=U;if((j.priorities[_]??0)>=K)break;j.ids[H]=j.ids[_]??0,j.priorities[H]=j.priorities[_]??0,H=_}return j.ids[H]=J,j.priorities[H]=K,D}function S(j,D){let A=1,J=D;while((j[J]??-1)!==-1)A++,J=j[J]??-1;let K=Array(A);J=D;for(let H=A-1;H>=0;H--)if(K[H]=J,H>0)J=j[J]??-1;return K}function v(j,D,A,J){let K=j.cells.length;if(D<0||D>=K)return null;if(A<0||A>=K)return null;let H=J?.maxNodesExpanded??1e4,E=J?.blockedCells,_=J?.goalTolerance??0,U=b[j.topology],Q=new Float32Array(K);Q.fill(Number.POSITIVE_INFINITY);let $=new Int32Array(K);$.fill(-1);let I=new Uint8Array(K),V={ids:new Int32Array(K),priorities:new Float32Array(K),size:0},M=[];Q[D]=0,O(V,D,U.heuristic(j,D,A));let R=0;while(V.size>0){if(R>=H)return null;let W=P(V);if(I[W])continue;if(I[W]=1,R++,U.heuristic(j,W,A)<=_)return S($,W);M.length=0;let N=U.neighbors(j,W,M);for(let B=0;B<N;B++){let Z=M[B]??-1;if(Z<0||I[Z])continue;if((j.cells[Z]??0)===0)continue;if(E&&E.has(Z))continue;let L=(Q[W]??Number.POSITIVE_INFINITY)+U.stepCost(j,W,Z);if(L<(Q[Z]??Number.POSITIVE_INFINITY))Q[Z]=L,$[Z]=W,O(V,Z,L+U.heuristic(j,Z,A))}}return null}function g(j){let{grid:D,systemGroup:A="ai",priority:J=150,phase:K="update",maxRequestsPerFrame:H=4,maxNodesExpanded:E=1e4}=j;return C("pathfinding").withComponentTypes().withEventTypes().withResourceTypes().withLabels().withGroups().requires().install((_)=>{_.addResource("navGrid",D),_.addSystem("pathfinding-request").setPriority(J).inPhase(K).inGroup(A).addQuery("requests",{with:["pathRequest","worldTransform"]}).setProcess(({queries:U,ecs:Q})=>{let $=Q.getResource("navGrid"),I=0;for(let V of U.requests){if(I>=H)break;I++;let{pathRequest:M,worldTransform:R}=V.components,W=$.worldToCell(R.x,R.y),N=$.worldToCell(M.target.x,M.target.y),B=v($,W,N,{maxNodesExpanded:E});if(Q.commands.removeComponent(V.id,"pathRequest"),B===null){Q.eventBus.publish("pathBlocked",{entityId:V.id});continue}let Z=B.slice(1).map((G)=>$.cellToWorld(G));if(Q.eventBus.publish("pathFound",{entityId:V.id,path:Z}),Z.length===0)continue;let X=Q.getComponent(V.id,"path");if(X)X.waypoints=Z,X.currentIndex=0,Q.markChanged(V.id,"path");else Q.addComponent(V.id,"path",{waypoints:Z,currentIndex:0});let L=Z[0];if(!L)continue;let Y=Q.getComponent(V.id,"moveTarget");if(Y)Y.x=L.x,Y.y=L.y,Q.markChanged(V.id,"moveTarget");else Q.addComponent(V.id,"moveTarget",{x:L.x,y:L.y})}}),_.addSystem("pathfinding-waypoint-advance").inGroup(A).setEventHandlers({arriveAtTarget({data:U,ecs:Q}){let $=Q.getComponent(U.entityId,"path");if(!$)return;let I=$.currentIndex+1;if(I>=$.waypoints.length){Q.commands.removeComponent(U.entityId,"path");return}$.currentIndex=I,Q.markChanged(U.entityId,"path");let V=$.waypoints[I];if(!V)return;Q.commands.addComponent(U.entityId,"moveTarget",{x:V.x,y:V.y})}})})}export{v as findPath,g as createPathfindingPlugin,l as createPathRequest,x as createNavGrid};
|
|
2
|
+
|
|
3
|
+
//# debugId=8835265364E0B69F64756E2164756E21
|
|
4
|
+
//# sourceMappingURL=pathfinding.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/plugins/ai/pathfinding.ts"],
|
|
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"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "8835265364E0B69F64756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Input Plugin for ECSpresso
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Resource-only plugin — input is polled via the `inputState` resource. Provides
|
|
5
|
+
* frame-accurate keyboard, pointer (mouse + touch via PointerEvent), up to 4
|
|
6
|
+
* gamepads, and unified + per-player action maps.
|
|
7
7
|
*
|
|
8
|
-
* DOM events
|
|
9
|
-
*
|
|
8
|
+
* Mutation model: DOM events accumulate into `raw` between frames and are
|
|
9
|
+
* flattened once per frame into a stable `frame` object whose Sets are cleared
|
|
10
|
+
* and refilled in place (no per-frame allocations). Gamepads are polled once
|
|
11
|
+
* per frame via `navigator.getGamepads()` (or an injected poll function).
|
|
12
|
+
* Unified and per-player action states ping-pong two Sets (`active` / `prev`)
|
|
13
|
+
* so edge detection costs nothing beyond one `.add()` per active action.
|
|
10
14
|
*/
|
|
11
|
-
import { type BasePluginOptions } from 'ecspresso';
|
|
12
|
-
export interface Vec2 {
|
|
13
|
-
x: number;
|
|
14
|
-
y: number;
|
|
15
|
-
}
|
|
15
|
+
import { type BasePluginOptions, type Vector2D } from 'ecspresso';
|
|
16
16
|
type LowercaseLetter = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z';
|
|
17
17
|
type Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
|
|
18
18
|
type Punctuation = '`' | '~' | '!' | '@' | '#' | '$' | '%' | '^' | '&' | '*' | '(' | ')' | '-' | '_' | '=' | '+' | '[' | '{' | ']' | '}' | '\\' | '|' | ';' | ':' | "'" | '"' | ',' | '<' | '.' | '>' | '/' | '?';
|
|
@@ -41,37 +41,107 @@ export interface KeyboardState {
|
|
|
41
41
|
justReleased(key: KeyCode): boolean;
|
|
42
42
|
}
|
|
43
43
|
export interface PointerState {
|
|
44
|
-
readonly position: Readonly<
|
|
45
|
-
readonly delta: Readonly<
|
|
44
|
+
readonly position: Readonly<Vector2D>;
|
|
45
|
+
readonly delta: Readonly<Vector2D>;
|
|
46
|
+
isDown(button: number): boolean;
|
|
47
|
+
justPressed(button: number): boolean;
|
|
48
|
+
justReleased(button: number): boolean;
|
|
49
|
+
}
|
|
50
|
+
export interface GamepadState {
|
|
51
|
+
readonly connected: boolean;
|
|
52
|
+
readonly id: string | null;
|
|
46
53
|
isDown(button: number): boolean;
|
|
47
54
|
justPressed(button: number): boolean;
|
|
48
55
|
justReleased(button: number): boolean;
|
|
56
|
+
/** Analog button value in [0, 1]. Useful for triggers. Returns 0 when disconnected or out of range. */
|
|
57
|
+
buttonValue(button: number): number;
|
|
58
|
+
/** Deadzone-applied axis value in [-1, 1]. Sticks use radial deadzone on axis pairs (0,1) and (2,3). */
|
|
59
|
+
axis(index: number): number;
|
|
60
|
+
/** Raw axis value in [-1, 1] with no deadzone applied. */
|
|
61
|
+
rawAxis(index: number): number;
|
|
49
62
|
}
|
|
50
63
|
export interface ActionState<A extends string = string> {
|
|
51
64
|
isActive(action: A): boolean;
|
|
52
65
|
justActivated(action: A): boolean;
|
|
53
66
|
justDeactivated(action: A): boolean;
|
|
54
67
|
}
|
|
68
|
+
export interface PlayerInput<A extends string = string> {
|
|
69
|
+
readonly actions: ActionState<A>;
|
|
70
|
+
setActionMap(map: ActionMap<A>): void;
|
|
71
|
+
getActionMap(): Readonly<ActionMap<A>>;
|
|
72
|
+
}
|
|
55
73
|
export interface InputState<A extends string = string> {
|
|
56
74
|
readonly keyboard: KeyboardState;
|
|
57
75
|
readonly pointer: PointerState;
|
|
76
|
+
/** Always length 4 (standard web gamepad slot count). Disconnected slots return `connected: false`. */
|
|
77
|
+
readonly gamepads: ReadonlyArray<GamepadState>;
|
|
78
|
+
/** Unified action state — fires when any bound input (keyboard, pointer, any pad) is active. Intended for menu/shared input. */
|
|
58
79
|
readonly actions: ActionState<A>;
|
|
59
80
|
setActionMap(actions: ActionMap<A>): void;
|
|
60
81
|
getActionMap(): Readonly<ActionMap<A>>;
|
|
82
|
+
/** Register or replace a player's action map. Per-player states are isolated from the unified `actions`. */
|
|
83
|
+
definePlayer(id: string, map: ActionMap<A>): void;
|
|
84
|
+
/** Returns true if the player existed and was removed. */
|
|
85
|
+
removePlayer(id: string): boolean;
|
|
86
|
+
/** Returns a handle to a registered player's input, or undefined if no such player. */
|
|
87
|
+
player(id: string): PlayerInput<A> | undefined;
|
|
88
|
+
playerIds(): readonly string[];
|
|
89
|
+
}
|
|
90
|
+
export interface GamepadButtonRef {
|
|
91
|
+
pad: number;
|
|
92
|
+
button: number;
|
|
93
|
+
}
|
|
94
|
+
export interface GamepadAxisRef {
|
|
95
|
+
pad: number;
|
|
96
|
+
axis: number;
|
|
97
|
+
/** Which half of the axis counts as "active". */
|
|
98
|
+
direction: 1 | -1;
|
|
99
|
+
/** Magnitude at which the axis triggers the action. Applied to the deadzone-adjusted axis value. Default: 0.5. */
|
|
100
|
+
threshold?: number;
|
|
61
101
|
}
|
|
62
102
|
export interface ActionBinding {
|
|
63
103
|
keys?: KeyCode[];
|
|
64
|
-
|
|
104
|
+
/** Pointer (mouse/touch) button indices — 0 = primary, 1 = auxiliary, 2 = secondary, etc. */
|
|
105
|
+
pointerButtons?: number[];
|
|
106
|
+
gamepadButtons?: GamepadButtonRef[];
|
|
107
|
+
gamepadAxes?: GamepadAxisRef[];
|
|
65
108
|
}
|
|
66
109
|
export type ActionMap<A extends string = string> = Record<A, ActionBinding>;
|
|
67
110
|
export interface InputResourceTypes<A extends string = string> {
|
|
68
111
|
inputState: InputState<A>;
|
|
69
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Minimal gamepad shape required by the injectable poll function. A structural
|
|
115
|
+
* subset of the browser `Gamepad` interface — `navigator.getGamepads()` satisfies
|
|
116
|
+
* it directly, and test doubles can supply just these fields.
|
|
117
|
+
*/
|
|
118
|
+
export interface GamepadLike {
|
|
119
|
+
id: string;
|
|
120
|
+
connected: boolean;
|
|
121
|
+
buttons: ReadonlyArray<{
|
|
122
|
+
pressed: boolean;
|
|
123
|
+
value: number;
|
|
124
|
+
}>;
|
|
125
|
+
axes: ReadonlyArray<number>;
|
|
126
|
+
}
|
|
127
|
+
export interface GamepadOptions {
|
|
128
|
+
/** Radial deadzone applied to stick pairs (axes 0,1 and 2,3). Value in [0, 1]. Default: 0.15. */
|
|
129
|
+
deadzone?: number;
|
|
130
|
+
/**
|
|
131
|
+
* Custom poll function returning up to 4 gamepad slots. Defaults to `navigator.getGamepads()`.
|
|
132
|
+
* Primarily an injection point for tests; in the browser the default is correct.
|
|
133
|
+
*/
|
|
134
|
+
poll?: () => ReadonlyArray<GamepadLike | null>;
|
|
135
|
+
}
|
|
70
136
|
export interface InputPluginOptions<A extends string = string, G extends string = 'input'> extends BasePluginOptions<G> {
|
|
71
|
-
/** Initial action
|
|
137
|
+
/** Initial unified action map. */
|
|
72
138
|
actions?: ActionMap<A>;
|
|
139
|
+
/** Initial per-player action maps, keyed by player id. */
|
|
140
|
+
players?: Record<string, ActionMap<A>>;
|
|
73
141
|
/** EventTarget to attach listeners to (default: globalThis). Pass a custom target for testability. */
|
|
74
142
|
target?: EventTarget;
|
|
143
|
+
/** Gamepad polling and deadzone configuration. */
|
|
144
|
+
gamepad?: GamepadOptions;
|
|
75
145
|
/**
|
|
76
146
|
* Optional conversion from raw DOM client coordinates to the space `inputState.pointer.position` should report.
|
|
77
147
|
* Renderer-agnostic: wire to `clientToLogical(...)` from renderer2D when using `screenScale`, or to a renderer-specific helper.
|
|
@@ -82,20 +152,21 @@ export interface InputPluginOptions<A extends string = string, G extends string
|
|
|
82
152
|
y: number;
|
|
83
153
|
};
|
|
84
154
|
}
|
|
85
|
-
/**
|
|
86
|
-
* Create a single action binding.
|
|
87
|
-
*
|
|
88
|
-
* @param binding The binding configuration
|
|
89
|
-
* @returns The same binding object
|
|
90
|
-
*/
|
|
155
|
+
/** Create a single action binding. Identity function that provides type inference for inline literals. */
|
|
91
156
|
export declare function createActionBinding(binding: ActionBinding): ActionBinding;
|
|
157
|
+
/** Build an array of gamepad button refs scoped to one pad — `gamepadButtonsOn(0, 0, 1, 9)` = pad 0's buttons 0, 1, 9. */
|
|
158
|
+
export declare function gamepadButtonsOn(pad: number, ...buttons: number[]): GamepadButtonRef[];
|
|
159
|
+
/** Build a gamepad axis ref. `threshold` defaults to 0.5 at activation time. */
|
|
160
|
+
export declare function gamepadAxisOn(pad: number, axis: number, direction: 1 | -1, threshold?: number): GamepadAxisRef;
|
|
92
161
|
/**
|
|
93
162
|
* Create an input plugin for ECSpresso.
|
|
94
163
|
*
|
|
95
|
-
*
|
|
164
|
+
* Provides:
|
|
96
165
|
* - Frame-accurate keyboard state (isDown, justPressed, justReleased)
|
|
97
166
|
* - Pointer position/delta and button state (mouse + touch via PointerEvent)
|
|
98
|
-
* -
|
|
167
|
+
* - Up to 4 gamepads polled per frame, with radial deadzone on sticks and analog button values
|
|
168
|
+
* - Unified action mapping (keyboard + pointer + any pad)
|
|
169
|
+
* - Per-player action maps for local co-op (`definePlayer`, `player(id)`)
|
|
99
170
|
* - Automatic listener cleanup on detach
|
|
100
171
|
*
|
|
101
172
|
* @example
|
|
@@ -103,16 +174,23 @@ export declare function createActionBinding(binding: ActionBinding): ActionBindi
|
|
|
103
174
|
* const ecs = ECSpresso.create()
|
|
104
175
|
* .withPlugin(createInputPlugin({
|
|
105
176
|
* actions: {
|
|
106
|
-
* jump: { keys: [' ', 'ArrowUp'] },
|
|
107
|
-
* shoot: { keys: ['z'],
|
|
177
|
+
* jump: { keys: [' ', 'ArrowUp'], gamepadButtons: [{ pad: 0, button: 0 }] },
|
|
178
|
+
* shoot: { keys: ['z'], pointerButtons: [0] },
|
|
179
|
+
* },
|
|
180
|
+
* players: {
|
|
181
|
+
* p1: { jump: { keys: [' '] }, shoot: { keys: ['z'] } },
|
|
182
|
+
* p2: {
|
|
183
|
+
* jump: { gamepadButtons: gamepadButtonsOn(0, 0) },
|
|
184
|
+
* shoot: { gamepadButtons: gamepadButtonsOn(0, 2) },
|
|
185
|
+
* },
|
|
108
186
|
* },
|
|
109
187
|
* }))
|
|
110
188
|
* .build();
|
|
111
189
|
*
|
|
112
|
-
* // In a system:
|
|
113
190
|
* const input = ecs.getResource('inputState');
|
|
114
|
-
* if (input.actions.justActivated('jump')) { ... }
|
|
115
|
-
* if (input.
|
|
191
|
+
* if (input.actions.justActivated('jump')) { ... } // any source
|
|
192
|
+
* if (input.player('p1')?.actions.isActive('jump')) { ... } // just player 1
|
|
193
|
+
* if (input.gamepads[0].isDown(0)) { ... } // raw pad 0 A-button
|
|
116
194
|
* ```
|
|
117
195
|
*/
|
|
118
196
|
export declare function createInputPlugin<A extends string = string, G extends string = 'input'>(options?: InputPluginOptions<A, G>): import("ecspresso").Plugin<import("ecspresso").WithResources<import("ecspresso").EmptyConfig, InputResourceTypes<A>>, import("ecspresso").EmptyConfig, "input-state", G, never, never>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var
|
|
1
|
+
var i=Object.defineProperty;var s=(j)=>j;function o(j,q){this[j]=s.bind(null,q)}var Wj=(j,q)=>{for(var W in q)i(j,W,{get:q[W],enumerable:!0,configurable:!0,set:o.bind(q,W)})};var Yj=(j,q)=>()=>(j&&(q=j(j=0)),q);var $j=((j)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(j,{get:(q,W)=>(typeof require<"u"?require:q)[W]}):j)(function(j){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+j+'" is not supported')});import{definePlugin as n}from"ecspresso";function Gj(j){return j}function Kj(j,...q){return q.map((W)=>({pad:j,button:W}))}function Cj(j,q,W,G){return G===void 0?{pad:j,axis:q,direction:W}:{pad:j,axis:q,direction:W,threshold:G}}var d=0.5,r=0.15,H=4;function t(){return{keysDown:new Set,keysPressed:[],keysReleased:[],pointerButtonsDown:new Set,pointerButtonsPressed:[],pointerButtonsReleased:[],pointerX:0,pointerY:0,lastPointerX:0,lastPointerY:0,pointerMoved:!1}}function a(){return{keysDown:new Set,keysPressed:new Set,keysReleased:new Set,pointerButtonsDown:new Set,pointerButtonsPressed:new Set,pointerButtonsReleased:new Set,pointerX:0,pointerY:0,pointerDeltaX:0,pointerDeltaY:0}}function e(){return{connected:!1,id:null,buttonsDown:new Set,buttonsPrev:new Set,buttonsPressed:new Set,buttonsReleased:new Set,buttonValues:[],axes:[],rawAxes:[]}}function h(){return{active:new Set,prev:new Set}}function B(j,q){j.clear();for(let W of q)j.add(W)}function jj(j,q){B(j.keysDown,q.keysDown),B(j.keysPressed,q.keysPressed),B(j.keysReleased,q.keysReleased),B(j.pointerButtonsDown,q.pointerButtonsDown),B(j.pointerButtonsPressed,q.pointerButtonsPressed),B(j.pointerButtonsReleased,q.pointerButtonsReleased),j.pointerDeltaX=q.pointerMoved?q.pointerX-q.lastPointerX:0,j.pointerDeltaY=q.pointerMoved?q.pointerY-q.lastPointerY:0,j.pointerX=q.pointerX,j.pointerY=q.pointerY,q.keysPressed.length=0,q.keysReleased.length=0,q.pointerButtonsPressed.length=0,q.pointerButtonsReleased.length=0,q.lastPointerX=q.pointerX,q.lastPointerY=q.pointerY,q.pointerMoved=!1}function qj(j){return()=>{if(typeof navigator>"u"||typeof navigator.getGamepads!=="function"){for(let W=0;W<j.length;W++)j[W]=null;return j}let q=navigator.getGamepads();for(let W=0;W<j.length;W++)j[W]=q[W]??null;return j}}function D(j,q,W,G,Z){let $=Math.sqrt(j*j+q*q);if($<W){G[Z]=0,G[Z+1]=0;return}let V=Math.min(($-W)/(1-W),1);G[Z]=j/$*V,G[Z+1]=q/$*V}function Jj(j,q,W){if(q.length=j.length,j.length>=2)D(j[0]??0,j[1]??0,W,q,0);if(j.length>=4)D(j[2]??0,j[3]??0,W,q,2);for(let G=4;G<j.length;G++)q[G]=j[G]??0}function Qj(j,q,W){let G=q();for(let Z=0;Z<H;Z++){let $=G[Z]??null,V=j[Z];if(!V)continue;let R=V.buttonsPrev;if(B(R,V.buttonsDown),V.buttonsDown.clear(),V.buttonsPressed.clear(),V.buttonsReleased.clear(),!$||!$.connected){if(V.connected){for(let Y of R)V.buttonsReleased.add(Y);V.connected=!1,V.id=null,V.buttonValues.length=0,V.axes.length=0,V.rawAxes.length=0}continue}V.connected=!0,V.id=$.id,V.buttonValues.length=$.buttons.length;for(let Y=0;Y<$.buttons.length;Y++){let E=$.buttons[Y];if(!E){V.buttonValues[Y]=0;continue}if(V.buttonValues[Y]=E.value,E.pressed)V.buttonsDown.add(Y)}for(let Y of V.buttonsDown)if(!R.has(Y))V.buttonsPressed.add(Y);for(let Y of R)if(!V.buttonsDown.has(Y))V.buttonsReleased.add(Y);V.rawAxes.length=$.axes.length;for(let Y=0;Y<$.axes.length;Y++)V.rawAxes[Y]=$.axes[Y]??0;Jj(V.rawAxes,V.axes,W)}}function Vj(j,q,W,G){if(j.keys?.some((Z)=>q.has(Z)))return!0;if(j.pointerButtons?.some((Z)=>W.has(Z)))return!0;if(j.gamepadButtons?.some(({pad:Z,button:$})=>G[Z]?.buttonsDown.has($)??!1))return!0;if(j.gamepadAxes?.some(({pad:Z,axis:$,direction:V,threshold:R=d})=>{let Y=G[Z]?.axes[$]??0;return V>0?Y>R:Y<-R}))return!0;return!1}function k(j,q,W,G,Z){let $=j.prev;j.prev=j.active,j.active=$,$.clear();for(let[V,R]of Object.entries(q))if(Vj(R,W,G,Z))$.add(V)}function g(j){return{isActive:(q)=>j.active.has(q),justActivated:(q)=>j.active.has(q)&&!j.prev.has(q),justDeactivated:(q)=>!j.active.has(q)&&j.prev.has(q)}}function Rj(j){let{systemGroup:q="input",priority:W=100,phase:G="preUpdate",target:Z=globalThis,gamepad:$={},coordinateTransform:V}=j??{},R={...j?.actions??{}},Y=new Map(Object.entries(j?.players??{})),E=$.deadzone??r,S=$.poll??qj(Array(H).fill(null)),C=t(),K=a(),L=Array.from({length:H},e),z=h(),O=new Map,N=new Map,T=[],I={x:0,y:0},F={x:0,y:0},U=R,A={isDown:(J)=>K.keysDown.has(J),justPressed:(J)=>K.keysPressed.has(J),justReleased:(J)=>K.keysReleased.has(J)},y={position:I,delta:F,isDown:(J)=>K.pointerButtonsDown.has(J),justPressed:(J)=>K.pointerButtonsPressed.has(J),justReleased:(J)=>K.pointerButtonsReleased.has(J)};function b(J){let Q=L[J];if(!Q)throw Error(`Invalid gamepad index: ${J}`);return{get connected(){return Q.connected},get id(){return Q.id},isDown:(X)=>Q.buttonsDown.has(X),justPressed:(X)=>Q.buttonsPressed.has(X),justReleased:(X)=>Q.buttonsReleased.has(X),buttonValue:(X)=>Q.buttonValues[X]??0,axis:(X)=>Q.axes[X]??0,rawAxis:(X)=>Q.rawAxes[X]??0}}let c=Array.from({length:H},(J,Q)=>b(Q)),f=g(z);function M(J){let Q=O.get(J);if(Q)return Q;let X=h();return O.set(J,X),X}function P(J){let Q=M(J);return{actions:g(Q),setActionMap:(X)=>{if(!Y.has(J))throw Error(`Player '${J}' was removed`);Y.set(J,{...X})},getActionMap:()=>{let X=Y.get(J);if(!X)throw Error(`Player '${J}' was removed`);return{...X}}}}for(let J of Y.keys())N.set(J,P(J));let x={keyboard:A,pointer:y,gamepads:c,actions:f,setActionMap(J){U={...J}},getActionMap(){return{...U}},definePlayer(J,Q){if(Y.set(J,{...Q}),!N.has(J))N.set(J,P(J))},removePlayer(J){let Q=Y.delete(J);return N.delete(J),O.delete(J),Q},player(J){return N.get(J)},playerIds(){return Array.from(Y.keys())}};function m(J){let Q=J;if(Q.repeat)return;C.keysDown.add(Q.key),C.keysPressed.push(Q.key)}function u(J){let Q=J;C.keysDown.delete(Q.key),C.keysReleased.push(Q.key)}function w(J){let Q=J;C.pointerButtonsDown.add(Q.button),C.pointerButtonsPressed.push(Q.button)}function l(J){let Q=J;if(V){let{x:X,y:v}=V(Q.clientX,Q.clientY);C.pointerX=X,C.pointerY=v}else C.pointerX=Q.clientX,C.pointerY=Q.clientY;C.pointerMoved=!0}function p(J){let Q=J;C.pointerButtonsDown.delete(Q.button),C.pointerButtonsReleased.push(Q.button)}function _(J,Q){Z.addEventListener(J,Q),T.push(()=>{Z.removeEventListener(J,Q)})}return n("input").withResourceTypes().withLabels().withGroups().install((J)=>{J.addResource("inputState",x),J.addSystem("input-state").setPriority(W).inPhase(G).inGroup(q).setOnInitialize(()=>{_("keydown",m),_("keyup",u),_("pointerdown",w),_("pointermove",l),_("pointerup",p)}).setOnDetach(()=>{for(let Q of T)Q();T.length=0}).setProcess(()=>{Qj(L,S,E),jj(K,C),I.x=K.pointerX,I.y=K.pointerY,F.x=K.pointerDeltaX,F.y=K.pointerDeltaY,k(z,U,K.keysDown,K.pointerButtonsDown,L);for(let[Q,X]of Y){let v=M(Q);k(v,X,K.keysDown,K.pointerButtonsDown,L)}})})}export{Kj as gamepadButtonsOn,Cj as gamepadAxisOn,Rj as createInputPlugin,Gj as createActionBinding};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=A22CC6420E47F29C64756E2164756E21
|
|
4
4
|
//# sourceMappingURL=input.js.map
|