ecspresso 0.13.4 → 0.14.2
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 +4 -4
- package/dist/plugins/ai/behavior-tree.d.ts +369 -0
- package/dist/plugins/ai/behavior-tree.js +4 -0
- package/dist/plugins/ai/behavior-tree.js.map +10 -0
- package/dist/plugins/ai/detection.js +2 -2
- package/dist/plugins/ai/detection.js.map +2 -2
- package/dist/plugins/ai/flocking.js +2 -2
- package/dist/plugins/ai/flocking.js.map +2 -2
- 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/audio/audio.js +2 -2
- package/dist/plugins/audio/audio.js.map +2 -2
- package/dist/plugins/combat/health.js +2 -2
- package/dist/plugins/combat/health.js.map +2 -2
- package/dist/plugins/combat/projectile.js +2 -2
- package/dist/plugins/combat/projectile.js.map +2 -2
- package/dist/plugins/debug/diagnostics.js +3 -3
- package/dist/plugins/debug/diagnostics.js.map +2 -2
- 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/input/selection.js +2 -2
- package/dist/plugins/input/selection.js.map +2 -2
- package/dist/plugins/isometric/depth-sort.js +2 -2
- package/dist/plugins/isometric/depth-sort.js.map +2 -2
- package/dist/plugins/isometric/projection.js +2 -2
- package/dist/plugins/isometric/projection.js.map +2 -2
- package/dist/plugins/physics/collision.js +2 -2
- package/dist/plugins/physics/collision.js.map +2 -2
- package/dist/plugins/physics/collision3D.d.ts +83 -0
- package/dist/plugins/physics/collision3D.js +4 -0
- package/dist/plugins/physics/collision3D.js.map +13 -0
- package/dist/plugins/physics/physics2D.js +2 -2
- package/dist/plugins/physics/physics2D.js.map +2 -2
- package/dist/plugins/physics/physics3D.d.ts +140 -0
- package/dist/plugins/physics/physics3D.js +4 -0
- package/dist/plugins/physics/physics3D.js.map +11 -0
- package/dist/plugins/physics/steering.js +2 -2
- package/dist/plugins/physics/steering.js.map +2 -2
- package/dist/plugins/rendering/particles.js +2 -2
- package/dist/plugins/rendering/particles.js.map +2 -2
- package/dist/plugins/rendering/renderer2D.js +2 -2
- package/dist/plugins/rendering/renderer2D.js.map +3 -3
- package/dist/plugins/rendering/renderer3D.d.ts +247 -0
- package/dist/plugins/rendering/renderer3D.js +4107 -0
- package/dist/plugins/rendering/renderer3D.js.map +12 -0
- package/dist/plugins/rendering/sprite-animation.js +2 -2
- package/dist/plugins/rendering/sprite-animation.js.map +2 -2
- 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/scripting/coroutine.js +2 -2
- package/dist/plugins/scripting/coroutine.js.map +2 -2
- package/dist/plugins/scripting/state-machine.js +2 -2
- package/dist/plugins/scripting/state-machine.js.map +2 -2
- package/dist/plugins/scripting/timers.js +2 -2
- package/dist/plugins/scripting/timers.js.map +2 -2
- package/dist/plugins/scripting/tween.js +2 -2
- package/dist/plugins/scripting/tween.js.map +2 -2
- package/dist/plugins/spatial/bounds.js +2 -2
- package/dist/plugins/spatial/bounds.js.map +2 -2
- package/dist/plugins/spatial/camera.js +2 -2
- package/dist/plugins/spatial/camera.js.map +2 -2
- package/dist/plugins/spatial/camera3D.d.ts +112 -0
- package/dist/plugins/spatial/camera3D.js +4 -0
- package/dist/plugins/spatial/camera3D.js.map +10 -0
- package/dist/plugins/spatial/spatial-index.js +2 -2
- package/dist/plugins/spatial/spatial-index.js.map +3 -3
- package/dist/plugins/spatial/spatial-index3D.d.ts +80 -0
- package/dist/plugins/spatial/spatial-index3D.js +4 -0
- package/dist/plugins/spatial/spatial-index3D.js.map +11 -0
- package/dist/plugins/spatial/transform.js +2 -2
- package/dist/plugins/spatial/transform.js.map +2 -2
- package/dist/plugins/spatial/transform3D.d.ts +148 -0
- package/dist/plugins/spatial/transform3D.js +4 -0
- package/dist/plugins/spatial/transform3D.js.map +10 -0
- 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/dist/utils/math.d.ts +65 -1
- package/dist/utils/narrowphase3D.d.ts +120 -0
- package/dist/utils/spatial-hash3D.d.ts +72 -0
- package/package.json +44 -4
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
var
|
|
2
|
-
`)},
|
|
1
|
+
var $=Object.defineProperty;var A=(j)=>j;function G(j,k){this[j]=A.bind(null,k)}var Y=(j,k)=>{for(var q in k)$(j,q,{get:k[q],enumerable:!0,configurable:!0,set:G.bind(k,q)})};var _=(j,k)=>()=>(j&&(k=j(j=0)),k);var D=((j)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(j,{get:(k,q)=>(typeof require<"u"?require:k)[q]}):j)(function(j){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+j+'" is not supported')});import{definePlugin as L}from"ecspresso";function M(j){let k=new Float64Array(j),q=0,z=0;return{push(Q){if(k[q]=Q,q=(q+1)%j,z<j)z++},computeFps(){if(z<2)return 0;let Q=k[(q-1+j)%j]??0,U=k[(q-z+j)%j]??0,F=Q-U;if(F<=0)return 0;return(z-1)/F*1000},computeAverageFrameTime(){if(z<2)return 0;let Q=k[(q-1+j)%j]??0,U=k[(q-z+j)%j]??0,F=Q-U;if(F<=0)return 0;return F/(z-1)},get size(){return z}}}function R(j){let{systemGroup:k="diagnostics",enableTimingOnInit:q=!0,fpsSampleCount:z=60}=j??{},Q={fps:0,entityCount:0,systemTimings:new Map,phaseTimings:{preUpdate:0,fixedUpdate:0,update:0,postUpdate:0,render:0},averageFrameTime:0},U=M(z);return L("diagnostics").withResourceTypes().withLabels().withGroups().install((F)=>{F.addResource("diagnostics",Q),F.addSystem("diagnostics-collect").setPriority(-999999).inPhase("render").inGroup(k).setOnInitialize((J)=>{if(q)J.enableDiagnostics(!0)}).setOnDetach((J)=>{J.enableDiagnostics(!1)}).setProcess(({ecs:J})=>{let V=performance.now();U.push(V);let K=J.getResource("diagnostics"),H={fps:U.computeFps(),entityCount:J.entityCount,systemTimings:J.systemTimings,phaseTimings:J.phaseTimings,averageFrameTime:U.computeAverageFrameTime()};K.fps=H.fps,K.entityCount=H.entityCount,K.systemTimings=H.systemTimings,K.phaseTimings=H.phaseTimings,K.averageFrameTime=H.averageFrameTime})})}var N={"top-left":"top:8px;left:8px","top-right":"top:8px;right:8px","bottom-left":"bottom:8px;left:8px","bottom-right":"bottom:8px;right:8px"};function B(j,k){let{position:q="top-left",updateInterval:z=200,showSystemTimings:Q=!0,maxSystemsShown:U=10}=k??{},F=document.createElement("div");F.style.cssText=`position:fixed;${N[q]};z-index:999999;background:rgba(0,0,0,0.8);color:#0f0;font:12px/1.4 monospace;padding:8px 12px;border-radius:4px;pointer-events:none;white-space:pre`,document.body.appendChild(F);let J=setInterval(()=>{let V=j.getResource("diagnostics"),K=[`FPS: ${V.fps.toFixed(0)}`,`Frame: ${V.averageFrameTime.toFixed(2)}ms`,`Entities: ${V.entityCount}`],H=V.phaseTimings;if(K.push(`Phases: pre=${H.preUpdate.toFixed(2)} fix=${H.fixedUpdate.toFixed(2)} upd=${H.update.toFixed(2)} post=${H.postUpdate.toFixed(2)} ren=${H.render.toFixed(2)}`),Q&&V.systemTimings.size>0){K.push("--- Systems ---");let Z=[...V.systemTimings.entries()].sort((W,X)=>X[1]-W[1]).slice(0,U);for(let[W,X]of Z)K.push(` ${W}: ${X.toFixed(3)}ms`)}F.textContent=K.join(`
|
|
2
|
+
`)},z);return()=>{clearInterval(J),F.remove()}}export{R as createDiagnosticsPlugin,B as createDiagnosticsOverlay};
|
|
3
3
|
|
|
4
|
-
//# debugId=
|
|
4
|
+
//# debugId=2396ABCE6E466B2664756E2164756E21
|
|
5
5
|
//# sourceMappingURL=diagnostics.js.map
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"/**\n * Diagnostics Plugin for ECSpresso\n *\n * Runtime diagnostics: FPS, entity count, per-system timing, per-phase timing,\n * and an optional DOM overlay for visual debugging.\n */\n\nimport { definePlugin } from 'ecspresso';\nimport type { SystemPhase } from 'ecspresso';\n\n// ==================== Types ====================\n\nexport interface DiagnosticsData {\n\tfps: number;\n\tentityCount: number;\n\tsystemTimings: ReadonlyMap<string, number>;\n\tphaseTimings: Readonly<Record<SystemPhase, number>>;\n\taverageFrameTime: number;\n}\n\nexport interface DiagnosticsResourceTypes {\n\tdiagnostics: DiagnosticsData;\n}\n\nexport interface DiagnosticsPluginOptions<G extends string = 'diagnostics'> {\n\t/** System group name (default: 'diagnostics') */\n\tsystemGroup?: G;\n\t/** Enable timing collection on initialize (default: true) */\n\tenableTimingOnInit?: boolean;\n\t/** Number of frames to sample for FPS average (default: 60) */\n\tfpsSampleCount?: number;\n}\n\nexport interface DiagnosticsOverlayOptions {\n\t/** Corner position (default: 'top-left') */\n\tposition?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';\n\t/** Milliseconds between DOM updates (default: 200) */\n\tupdateInterval?: number;\n\t/** Show per-system timings (default: true) */\n\tshowSystemTimings?: boolean;\n\t/** Maximum systems to show in overlay (default: 10) */\n\tmaxSystemsShown?: number;\n}\n\n// ==================== Ring Buffer ====================\n\n/**\n * Fixed-size circular buffer for frame timestamps.\n * Avoids Array.shift() allocation on every frame.\n */\nfunction createRingBuffer(capacity: number) {\n\tconst buffer = new Float64Array(capacity);\n\tlet writeIndex = 0;\n\tlet count = 0;\n\n\treturn {\n\t\tpush(value: number): void {\n\t\t\tbuffer[writeIndex] = value;\n\t\t\twriteIndex = (writeIndex + 1) % capacity;\n\t\t\tif (count < capacity) count++;\n\t\t},\n\n\t\t/** Compute FPS from stored timestamps */\n\t\tcomputeFps(): number {\n\t\t\tif (count < 2) return 0;\n\t\t\tconst newest = buffer[(writeIndex - 1 + capacity) % capacity] ?? 0;\n\t\t\tconst oldest = buffer[(writeIndex - count + capacity) % capacity] ?? 0;\n\t\t\tconst elapsed = newest - oldest;\n\t\t\tif (elapsed <= 0) return 0;\n\t\t\treturn ((count - 1) / elapsed) * 1000;\n\t\t},\n\n\t\t/** Compute average frame time in ms */\n\t\tcomputeAverageFrameTime(): number {\n\t\t\tif (count < 2) return 0;\n\t\t\tconst newest = buffer[(writeIndex - 1 + capacity) % capacity] ?? 0;\n\t\t\tconst oldest = buffer[(writeIndex - count + capacity) % capacity] ?? 0;\n\t\t\tconst elapsed = newest - oldest;\n\t\t\tif (elapsed <= 0) return 0;\n\t\t\treturn elapsed / (count - 1);\n\t\t},\n\n\t\tget size(): number {\n\t\t\treturn count;\n\t\t},\n\t};\n}\n\n// ==================== Plugin Factory ====================\n\nexport function createDiagnosticsPlugin<G extends string = 'diagnostics'>(\n\toptions?: DiagnosticsPluginOptions<G>,\n) {\n\tconst {\n\t\tsystemGroup = 'diagnostics',\n\t\tenableTimingOnInit = true,\n\t\tfpsSampleCount = 60,\n\t} = options ?? {};\n\n\tconst initialData: DiagnosticsData = {\n\t\tfps: 0,\n\t\tentityCount: 0,\n\t\tsystemTimings: new Map(),\n\t\tphaseTimings: { preUpdate: 0, fixedUpdate: 0, update: 0, postUpdate: 0, render: 0 },\n\t\taverageFrameTime: 0,\n\t};\n\n\tconst ringBuffer = createRingBuffer(fpsSampleCount);\n\n\treturn definePlugin('diagnostics')\n\t\t.withResourceTypes<DiagnosticsResourceTypes>()\n\t\t.withLabels<'diagnostics-collect'>()\n\t\t.withGroups<G>()\n\t\t.install((world) => {\n\t\t\tworld.addResource('diagnostics', initialData);\n\n\t\t\tworld\n\t\t\t\t.addSystem('diagnostics-collect')\n\t\t\t\t.setPriority(-999999)\n\t\t\t\t.inPhase('render')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setOnInitialize((ecs) => {\n\t\t\t\t\tif (enableTimingOnInit) {\n\t\t\t\t\t\tecs.enableDiagnostics(true);\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.setOnDetach((ecs) => {\n\t\t\t\t\tecs.enableDiagnostics(false);\n\t\t\t\t})\n\t\t\t\t.setProcess(({ ecs }) => {\n\t\t\t\t\tconst now = performance.now();\n\t\t\t\t\tringBuffer.push(now);\n\n\t\t\t\t\tconst resource = ecs.getResource('diagnostics');\n\t\t\t\t\tconst updated: DiagnosticsData = {\n\t\t\t\t\t\tfps: ringBuffer.computeFps(),\n\t\t\t\t\t\tentityCount: ecs.entityCount,\n\t\t\t\t\t\tsystemTimings: ecs.systemTimings,\n\t\t\t\t\t\tphaseTimings: ecs.phaseTimings,\n\t\t\t\t\t\taverageFrameTime: ringBuffer.computeAverageFrameTime(),\n\t\t\t\t\t};\n\n\t\t\t\t\t// Mutate fields on the existing resource object to avoid allocation\n\t\t\t\t\t(resource as { -readonly [K in keyof DiagnosticsData]: DiagnosticsData[K] }).fps = updated.fps;\n\t\t\t\t\t(resource as { -readonly [K in keyof DiagnosticsData]: DiagnosticsData[K] }).entityCount = updated.entityCount;\n\t\t\t\t\t(resource as { -readonly [K in keyof DiagnosticsData]: DiagnosticsData[K] }).systemTimings = updated.systemTimings;\n\t\t\t\t\t(resource as { -readonly [K in keyof DiagnosticsData]: DiagnosticsData[K] }).phaseTimings = updated.phaseTimings;\n\t\t\t\t\t(resource as { -readonly [K in keyof DiagnosticsData]: DiagnosticsData[K] }).averageFrameTime = updated.averageFrameTime;\n\t\t\t\t});\n\t\t});\n}\n\n// ==================== Overlay Helper ====================\n\nconst POSITION_STYLES: Record<NonNullable<DiagnosticsOverlayOptions['position']>, string> = {\n\t'top-left': 'top:8px;left:8px',\n\t'top-right': 'top:8px;right:8px',\n\t'bottom-left': 'bottom:8px;left:8px',\n\t'bottom-right': 'bottom:8px;right:8px',\n} as const;\n\n/**\n * Create a DOM overlay that displays diagnostics data.\n * Returns a cleanup function that removes the element and clears the interval.\n *\n * @param ecs An ECSpresso instance with the diagnostics resource\n * @param options Overlay configuration\n * @returns Cleanup function\n */\nexport function createDiagnosticsOverlay<\n\tR extends DiagnosticsResourceTypes,\n>(\n\tecs: { getResource<K extends keyof R>(key: K): R[K] },\n\toptions?: DiagnosticsOverlayOptions,\n): () => void {\n\tconst {\n\t\tposition = 'top-left',\n\t\tupdateInterval = 200,\n\t\tshowSystemTimings = true,\n\t\tmaxSystemsShown = 10,\n\t} = options ?? {};\n\n\tconst el = document.createElement('div');\n\tel.style.cssText = `position:fixed;${POSITION_STYLES[position]};z-index:999999;background:rgba(0,0,0,0.8);color:#0f0;font:12px/1.4 monospace;padding:8px 12px;border-radius:4px;pointer-events:none;white-space:pre`;\n\tdocument.body.appendChild(el);\n\n\tconst intervalId = setInterval(() => {\n\t\tconst d = ecs.getResource('diagnostics' as keyof R) as DiagnosticsData;\n\n\t\tconst lines: string[] = [\n\t\t\t`FPS: ${d.fps.toFixed(0)}`,\n\t\t\t`Frame: ${d.averageFrameTime.toFixed(2)}ms`,\n\t\t\t`Entities: ${d.entityCount}`,\n\t\t];\n\n\t\tconst phases = d.phaseTimings;\n\t\tlines.push(\n\t\t\t`Phases: pre=${phases.preUpdate.toFixed(2)} fix=${phases.fixedUpdate.toFixed(2)} upd=${phases.update.toFixed(2)} post=${phases.postUpdate.toFixed(2)} ren=${phases.render.toFixed(2)}`,\n\t\t);\n\n\t\tif (showSystemTimings && d.systemTimings.size > 0) {\n\t\t\tlines.push('--- Systems ---');\n\t\t\tconst sorted = [...d.systemTimings.entries()]\n\t\t\t\t.sort((a, b) => b[1] - a[1])\n\t\t\t\t.slice(0, maxSystemsShown);\n\t\t\tfor (const [label, ms] of sorted) {\n\t\t\t\tlines.push(` ${label}: ${ms.toFixed(3)}ms`);\n\t\t\t}\n\t\t}\n\n\t\tel.textContent = lines.join('\\n');\n\t}, updateInterval);\n\n\treturn () => {\n\t\tclearInterval(intervalId);\n\t\tel.remove();\n\t};\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": "
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "2396ABCE6E466B2664756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
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
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/plugins/input/input.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * Input Plugin for ECSpresso\n *\n * Provides frame-accurate keyboard, pointer (mouse + touch via PointerEvent),\n * and action mapping input. Resource-only plugin — input is polled via the\n * `inputState` resource. No ECS components or events.\n *\n * DOM events are accumulated between frames and snapshotted once per frame\n * in the system's process step, so all systems see consistent state.\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\n\n// ==================== Public Types ====================\n\nexport interface Vec2 {\n\tx: number;\n\ty: number;\n}\n\n// Key codes per the UI Events spec (KeyboardEvent.key values)\n// https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values\n\ntype LowercaseLetter =\n\t| 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm'\n\t| 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z';\n\ntype Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';\n\ntype Punctuation =\n\t| '`' | '~' | '!' | '@' | '#' | '$' | '%' | '^' | '&' | '*' | '(' | ')'\n\t| '-' | '_' | '=' | '+' | '[' | '{' | ']' | '}' | '\\\\' | '|'\n\t| ';' | ':' | \"'\" | '\"' | ',' | '<' | '.' | '>' | '/' | '?';\n\ntype ModifierKey =\n\t| 'Alt' | 'AltGraph' | 'CapsLock' | 'Control' | 'Fn' | 'FnLock'\n\t| 'Hyper' | 'Meta' | 'NumLock' | 'ScrollLock' | 'Shift'\n\t| 'Super' | 'Symbol' | 'SymbolLock';\n\ntype WhitespaceKey = 'Enter' | 'Tab' | ' ';\n\ntype NavigationKey =\n\t| `Arrow${'Down' | 'Left' | 'Right' | 'Up'}`\n\t| 'End' | 'Home' | 'PageDown' | 'PageUp';\n\ntype EditingKey =\n\t| 'Backspace' | 'Clear' | 'Copy' | 'CrSel' | 'Cut' | 'Delete'\n\t| 'EraseEof' | 'ExSel' | 'Insert' | 'Paste' | 'Redo' | 'Undo';\n\ntype UIKey =\n\t| 'Accept' | 'Again' | 'Attn' | 'Cancel' | 'ContextMenu' | 'Escape'\n\t| 'Execute' | 'Find' | 'Finish' | 'Help' | 'Pause' | 'Play'\n\t| 'Props' | 'Select' | 'ZoomIn' | 'ZoomOut';\n\ntype DeviceKey =\n\t| 'BrightnessDown' | 'BrightnessUp' | 'Eject' | 'Hibernate'\n\t| 'LogOff' | 'Power' | 'PowerOff' | 'PrintScreen' | 'Standby' | 'WakeUp';\n\ntype IMEKey =\n\t| 'AllCandidates' | 'Alphanumeric' | 'CodeInput' | 'Compose' | 'Convert'\n\t| 'FinalMode' | 'GroupFirst' | 'GroupLast' | 'GroupNext' | 'GroupPrevious'\n\t| 'ModeChange' | 'NextCandidate' | 'NonConvert' | 'PreviousCandidate'\n\t| 'Process' | 'SingleCandidate'\n\t| 'HangulMode' | 'HanjaMode' | 'JunjaMode'\n\t| 'Eisu' | 'Hankaku' | 'Hiragana' | 'HiraganaKatakana' | 'KanaMode'\n\t| 'KanjiMode' | 'Katakana' | 'Romaji' | 'Zenkaku' | 'ZenkakuHankaku';\n\ntype FunctionKey =\n\t| `F${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24}`\n\t| 'Soft1' | 'Soft2' | 'Soft3' | 'Soft4';\n\ntype PhoneKey =\n\t| 'AppSwitch' | 'Call' | 'Camera' | 'CameraFocus' | 'EndCall'\n\t| 'GoBack' | 'GoHome' | 'HeadsetHook' | 'LastNumberRedial'\n\t| 'Notification' | 'MannerMode' | 'VoiceDial';\n\ntype MultimediaKey =\n\t| 'ChannelDown' | 'ChannelUp'\n\t| `Media${\n\t\t'FastForward' | 'Pause' | 'Play' | 'PlayPause'\n\t\t| 'Record' | 'Rewind' | 'Stop' | 'TrackNext' | 'TrackPrevious'\n\t}`;\n\ntype AudioKey =\n\t| `Audio${\n\t\t'BalanceLeft' | 'BalanceRight' | 'BassDown' | 'BassBoostDown'\n\t\t| 'BassBoostToggle' | 'BassBoostUp' | 'BassUp' | 'FaderFront' | 'FaderRear'\n\t\t| 'SurroundModeNext' | 'TrebleDown' | 'TrebleUp'\n\t\t| 'VolumeDown' | 'VolumeMute' | 'VolumeUp'\n\t}`\n\t| `Microphone${'Toggle' | 'VolumeDown' | 'VolumeMute' | 'VolumeUp'}`;\n\ntype TVKey =\n\t| 'TV'\n\t| `TV${\n\t\t'3DMode' | 'AntennaCable' | 'AudioDescription' | 'AudioDescriptionMixDown'\n\t\t| 'AudioDescriptionMixUp' | 'ContentsMenu' | 'DataService' | 'Input'\n\t\t| 'InputComponent1' | 'InputComponent2' | 'InputComposite1' | 'InputComposite2'\n\t\t| 'InputHDMI1' | 'InputHDMI2' | 'InputHDMI3' | 'InputHDMI4' | 'InputVGA1'\n\t\t| 'MediaContext' | 'Network' | 'NumberEntry' | 'Power' | 'RadioService'\n\t\t| 'Satellite' | 'SatelliteBS' | 'SatelliteCS' | 'SatelliteToggle'\n\t\t| 'TerrestrialAnalog' | 'TerrestrialDigital' | 'Timer'\n\t}`;\n\ntype MediaControllerKey =\n\t| 'AVRInput' | 'AVRPower'\n\t| `Color${'F0Red' | 'F1Green' | 'F2Yellow' | 'F3Blue' | 'F4Grey' | 'F5Brown'}`\n\t| 'ClosedCaptionToggle' | 'Dimmer' | 'DisplaySwap' | 'DVR' | 'Exit'\n\t| `Favorite${'Clear' | 'Recall' | 'Store'}${0 | 1 | 2 | 3}`\n\t| 'Guide' | 'GuideNextDay' | 'GuidePreviousDay' | 'Info' | 'InstantReplay'\n\t| 'Link' | 'ListProgram' | 'LiveContent' | 'Lock'\n\t| `Media${\n\t\t'Apps' | 'AudioTrack' | 'Last' | 'SkipBackward'\n\t\t| 'SkipForward' | 'StepBackward' | 'StepForward' | 'TopMenu'\n\t}`\n\t| `Navigate${'In' | 'Next' | 'Out' | 'Previous'}`\n\t| 'NextFavoriteChannel' | 'NextUserProfile' | 'OnDemand' | 'Pairing'\n\t| `PinP${'Down' | 'Move' | 'Toggle' | 'Up'}`\n\t| `PlaySpeed${'Down' | 'Reset' | 'Up'}`\n\t| 'RandomToggle' | 'RcLowBattery' | 'RecordSpeedNext' | 'RfBypass'\n\t| 'ScanChannelsToggle' | 'ScreenModeNext' | 'Settings' | 'SplitScreenToggle'\n\t| 'STBInput' | 'STBPower' | 'Subtitle' | 'Teletext'\n\t| 'VideoModeNext' | 'Wink' | 'ZoomToggle';\n\ntype SpeechKey = 'SpeechCorrectionList' | 'SpeechInputToggle';\n\ntype DocumentKey =\n\t| 'Close' | 'New' | 'Open' | 'Print' | 'Save' | 'SpellCheck'\n\t| 'MailForward' | 'MailReply' | 'MailSend';\n\ntype LaunchKey = `Launch${\n\t| 'Calculator' | 'Calendar' | 'Contacts' | 'Mail' | 'MediaPlayer'\n\t| 'MusicPlayer' | 'MyComputer' | 'Phone' | 'ScreenSaver' | 'Spreadsheet'\n\t| 'WebBrowser' | 'WebCam' | 'WordProcessor'\n\t| `Application${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16}`\n}`;\n\ntype BrowserKey = `Browser${'Back' | 'Favorites' | 'Forward' | 'Home' | 'Refresh' | 'Search' | 'Stop'}`;\n\ntype NumpadKey = 'Decimal' | 'Key11' | 'Key12' | 'Multiply' | 'Add' | 'Divide' | 'Subtract' | 'Separator';\n\nexport type KeyCode =\n\t| LowercaseLetter | Uppercase<LowercaseLetter> | Digit | Punctuation\n\t| ModifierKey | WhitespaceKey | NavigationKey | EditingKey | UIKey | DeviceKey\n\t| IMEKey | FunctionKey | PhoneKey | MultimediaKey | AudioKey | TVKey\n\t| MediaControllerKey | SpeechKey | DocumentKey | LaunchKey | BrowserKey | NumpadKey\n\t| 'Unidentified' | 'Dead';\n\nexport interface KeyboardState {\n\tisDown(key: KeyCode): boolean;\n\tjustPressed(key: KeyCode): boolean;\n\tjustReleased(key: KeyCode): boolean;\n}\n\nexport interface PointerState {\n\treadonly position: Readonly<Vec2>;\n\treadonly delta: Readonly<Vec2>;\n\tisDown(button: number): boolean;\n\tjustPressed(button: number): boolean;\n\tjustReleased(button: number): boolean;\n}\n\nexport interface ActionState<A extends string = string> {\n\tisActive(action: A): boolean;\n\tjustActivated(action: A): boolean;\n\tjustDeactivated(action: A): boolean;\n}\n\nexport interface InputState<A extends string = string> {\n\treadonly keyboard: KeyboardState;\n\treadonly pointer: PointerState;\n\treadonly actions: ActionState<A>;\n\tsetActionMap(actions: ActionMap<A>): void;\n\tgetActionMap(): Readonly<ActionMap<A>>;\n}\n\nexport interface ActionBinding {\n\tkeys?: KeyCode[];\n\tbuttons?: number[];\n}\n\nexport type ActionMap<A extends string = string> = Record<A, ActionBinding>;\n\nexport interface InputResourceTypes<A extends string = string> {\n\tinputState: InputState<A>;\n}\n\nexport interface InputPluginOptions<A extends string = string, G extends string = 'input'> extends BasePluginOptions<G> {\n\t/** Initial action mappings */\n\tactions?: ActionMap<A>;\n\t/** EventTarget to attach listeners to (default: globalThis). Pass a custom target for testability. */\n\ttarget?: EventTarget;\n\t/**\n\t * Optional conversion from raw DOM client coordinates to the space `inputState.pointer.position` should report.\n\t * Renderer-agnostic: wire to `clientToLogical(...)` from renderer2D when using `screenScale`, or to a renderer-specific helper.\n\t * When omitted, pointer coords remain raw `clientX`/`clientY` (not canvas-relative).\n\t */\n\tcoordinateTransform?: (clientX: number, clientY: number) => { x: number; y: number };\n}\n\n// ==================== Helper Functions ====================\n\n/**\n * Create a single action binding.\n *\n * @param binding The binding configuration\n * @returns The same binding object\n */\nexport function createActionBinding(binding: ActionBinding): ActionBinding {\n\treturn binding;\n}\n\n// ==================== Internal Types ====================\n\ninterface RawInputState {\n\tkeysDown: Set<string>;\n\tkeysPressed: string[];\n\tkeysReleased: string[];\n\tbuttonsDown: Set<number>;\n\tbuttonsPressed: number[];\n\tbuttonsReleased: number[];\n\tpointerX: number;\n\tpointerY: number;\n\tpointerDeltaX: number;\n\tpointerDeltaY: number;\n\tlastPointerX: number;\n\tlastPointerY: number;\n\tpointerMoved: boolean;\n}\n\ninterface FrameSnapshot {\n\tkeysDown: ReadonlySet<string>;\n\tkeysPressed: ReadonlySet<string>;\n\tkeysReleased: ReadonlySet<string>;\n\tbuttonsDown: ReadonlySet<number>;\n\tbuttonsPressed: ReadonlySet<number>;\n\tbuttonsReleased: ReadonlySet<number>;\n\tpointerX: number;\n\tpointerY: number;\n\tpointerDeltaX: number;\n\tpointerDeltaY: number;\n\tactionsActive: ReadonlySet<string>;\n\tprevActionsActive: ReadonlySet<string>;\n}\n\n// ==================== Plugin Factory ====================\n\nfunction createRawInputState(): RawInputState {\n\treturn {\n\t\tkeysDown: new Set(),\n\t\tkeysPressed: [],\n\t\tkeysReleased: [],\n\t\tbuttonsDown: new Set(),\n\t\tbuttonsPressed: [],\n\t\tbuttonsReleased: [],\n\t\tpointerX: 0,\n\t\tpointerY: 0,\n\t\tpointerDeltaX: 0,\n\t\tpointerDeltaY: 0,\n\t\tlastPointerX: 0,\n\t\tlastPointerY: 0,\n\t\tpointerMoved: false,\n\t};\n}\n\nconst EMPTY_SET_STRING: ReadonlySet<string> = new Set<string>();\nconst EMPTY_SET_NUMBER: ReadonlySet<number> = new Set<number>();\n\nfunction createEmptySnapshot(): FrameSnapshot {\n\treturn {\n\t\tkeysDown: EMPTY_SET_STRING,\n\t\tkeysPressed: EMPTY_SET_STRING,\n\t\tkeysReleased: EMPTY_SET_STRING,\n\t\tbuttonsDown: EMPTY_SET_NUMBER,\n\t\tbuttonsPressed: EMPTY_SET_NUMBER,\n\t\tbuttonsReleased: EMPTY_SET_NUMBER,\n\t\tpointerX: 0,\n\t\tpointerY: 0,\n\t\tpointerDeltaX: 0,\n\t\tpointerDeltaY: 0,\n\t\tactionsActive: EMPTY_SET_STRING,\n\t\tprevActionsActive: EMPTY_SET_STRING,\n\t};\n}\n\nfunction computeActiveActions(\n\tactionMap: ActionMap,\n\tkeysDown: ReadonlySet<string>,\n\tbuttonsDown: ReadonlySet<number>,\n): Set<string> {\n\tconst active = new Set<string>();\n\tfor (const [name, binding] of Object.entries(actionMap)) {\n\t\tconst keyActive = binding.keys?.some((k) => keysDown.has(k)) ?? false;\n\t\tconst buttonActive = binding.buttons?.some((b) => buttonsDown.has(b)) ?? false;\n\t\tif (keyActive || buttonActive) {\n\t\t\tactive.add(name);\n\t\t}\n\t}\n\treturn active;\n}\n\nfunction snapshotRaw(raw: RawInputState, prevActionsActive: ReadonlySet<string>, actionMap: ActionMap): FrameSnapshot {\n\tconst keysDown = new Set(raw.keysDown);\n\tconst keysPressed = new Set(raw.keysPressed);\n\tconst keysReleased = new Set(raw.keysReleased);\n\tconst buttonsDown = new Set(raw.buttonsDown);\n\tconst buttonsPressed = new Set(raw.buttonsPressed);\n\tconst buttonsReleased = new Set(raw.buttonsReleased);\n\n\tconst pointerDeltaX = raw.pointerMoved ? raw.pointerX - raw.lastPointerX : 0;\n\tconst pointerDeltaY = raw.pointerMoved ? raw.pointerY - raw.lastPointerY : 0;\n\n\tconst actionsActive = computeActiveActions(actionMap, keysDown, buttonsDown);\n\n\tconst snapshot: FrameSnapshot = {\n\t\tkeysDown,\n\t\tkeysPressed,\n\t\tkeysReleased,\n\t\tbuttonsDown,\n\t\tbuttonsPressed,\n\t\tbuttonsReleased,\n\t\tpointerX: raw.pointerX,\n\t\tpointerY: raw.pointerY,\n\t\tpointerDeltaX,\n\t\tpointerDeltaY,\n\t\tactionsActive,\n\t\tprevActionsActive,\n\t};\n\n\t// Clear accumulation buffers\n\traw.keysPressed = [];\n\traw.keysReleased = [];\n\traw.buttonsPressed = [];\n\traw.buttonsReleased = [];\n\traw.lastPointerX = raw.pointerX;\n\traw.lastPointerY = raw.pointerY;\n\traw.pointerMoved = false;\n\n\treturn snapshot;\n}\n\n/**\n * Create an input plugin for ECSpresso.\n *\n * This plugin provides:\n * - Frame-accurate keyboard state (isDown, justPressed, justReleased)\n * - Pointer position/delta and button state (mouse + touch via PointerEvent)\n * - Named action mapping with runtime remapping\n * - Automatic listener cleanup on detach\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createInputPlugin({\n * actions: {\n * jump: { keys: [' ', 'ArrowUp'] },\n * shoot: { keys: ['z'], buttons: [0] },\n * },\n * }))\n * .build();\n *\n * // In a system:\n * const input = ecs.getResource('inputState');\n * if (input.actions.justActivated('jump')) { ... }\n * if (input.keyboard.isDown('ArrowRight')) { ... }\n * ```\n */\nexport function createInputPlugin<A extends string = string, G extends string = 'input'>(\n\toptions?: InputPluginOptions<A, G>\n) {\n\tconst {\n\t\tsystemGroup = 'input',\n\t\tpriority = 100,\n\t\tphase = 'preUpdate',\n\t\tactions: initialActions = {},\n\t\ttarget = globalThis,\n\t\tcoordinateTransform,\n\t} = options ?? {};\n\n\t// Closure state\n\tconst raw = createRawInputState();\n\tlet snapshot = createEmptySnapshot();\n\tlet actionMap: ActionMap = { ...initialActions };\n\tconst cleanupFns: Array<() => void> = [];\n\n\t// The position/delta objects exposed via the resource.\n\t// Updated in-place each frame to avoid allocations.\n\tconst position: Vec2 = { x: 0, y: 0 };\n\tconst delta: Vec2 = { x: 0, y: 0 };\n\n\t// Build the InputState resource that closes over snapshot\n\tconst keyboard: KeyboardState = {\n\t\tisDown: (key) => snapshot.keysDown.has(key),\n\t\tjustPressed: (key) => snapshot.keysPressed.has(key),\n\t\tjustReleased: (key) => snapshot.keysReleased.has(key),\n\t};\n\n\tconst pointer: PointerState = {\n\t\tposition,\n\t\tdelta,\n\t\tisDown: (button) => snapshot.buttonsDown.has(button),\n\t\tjustPressed: (button) => snapshot.buttonsPressed.has(button),\n\t\tjustReleased: (button) => snapshot.buttonsReleased.has(button),\n\t};\n\n\tconst actionState: ActionState<A> = {\n\t\tisActive: (action) => snapshot.actionsActive.has(action),\n\t\tjustActivated: (action) =>\n\t\t\tsnapshot.actionsActive.has(action) && !snapshot.prevActionsActive.has(action),\n\t\tjustDeactivated: (action) =>\n\t\t\t!snapshot.actionsActive.has(action) && snapshot.prevActionsActive.has(action),\n\t};\n\n\tconst inputState: InputState<A> = {\n\t\tkeyboard,\n\t\tpointer,\n\t\tactions: actionState,\n\t\tsetActionMap(newMap) {\n\t\t\tactionMap = { ...newMap };\n\t\t},\n\t\tgetActionMap() {\n\t\t\treturn { ...actionMap } as ActionMap<A>;\n\t\t},\n\t};\n\n\t// DOM event handlers\n\tfunction onKeyDown(e: Event) {\n\t\tconst ke = e as KeyboardEvent;\n\t\tif (ke.repeat) return;\n\t\traw.keysDown.add(ke.key);\n\t\traw.keysPressed.push(ke.key);\n\t}\n\n\tfunction onKeyUp(e: Event) {\n\t\tconst ke = e as KeyboardEvent;\n\t\traw.keysDown.delete(ke.key);\n\t\traw.keysReleased.push(ke.key);\n\t}\n\n\tfunction onPointerDown(e: Event) {\n\t\tconst pe = e as PointerEvent;\n\t\traw.buttonsDown.add(pe.button);\n\t\traw.buttonsPressed.push(pe.button);\n\t}\n\n\tfunction onPointerMove(e: Event) {\n\t\tconst pe = e as PointerEvent;\n\t\tif (coordinateTransform) {\n\t\t\tconst { x, y } = coordinateTransform(pe.clientX, pe.clientY);\n\t\t\traw.pointerX = x;\n\t\t\traw.pointerY = y;\n\t\t} else {\n\t\t\traw.pointerX = pe.clientX;\n\t\t\traw.pointerY = pe.clientY;\n\t\t}\n\t\traw.pointerMoved = true;\n\t}\n\n\tfunction onPointerUp(e: Event) {\n\t\tconst pe = e as PointerEvent;\n\t\traw.buttonsDown.delete(pe.button);\n\t\traw.buttonsReleased.push(pe.button);\n\t}\n\n\tfunction addListener(type: string, handler: (e: Event) => void) {\n\t\ttarget.addEventListener(type, handler);\n\t\tcleanupFns.push(() => { target.removeEventListener(type, handler); });\n\t}\n\n\treturn definePlugin('input')\n\t\t.withResourceTypes<InputResourceTypes<A>>()\n\t\t.withLabels<'input-state'>()\n\t\t.withGroups<G>()\n\t\t.install((world) => {\n\t\t\tworld.addResource('inputState', inputState);\n\n\t\t\tworld\n\t\t\t\t.addSystem('input-state')\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.setOnInitialize(() => {\n\t\t\t\t\taddListener('keydown', onKeyDown);\n\t\t\t\t\taddListener('keyup', onKeyUp);\n\t\t\t\t\taddListener('pointerdown', onPointerDown);\n\t\t\t\t\taddListener('pointermove', onPointerMove);\n\t\t\t\t\taddListener('pointerup', onPointerUp);\n\t\t\t\t})\n\t\t\t\t.setOnDetach(() => {\n\t\t\t\t\tfor (const cleanup of cleanupFns) {\n\t\t\t\t\t\tcleanup();\n\t\t\t\t\t}\n\t\t\t\t\tcleanupFns.length = 0;\n\t\t\t\t})\n\t\t\t\t.setProcess(() => {\n\t\t\t\t\tconst prevActionsActive = snapshot.actionsActive;\n\t\t\t\t\tsnapshot = snapshotRaw(raw, prevActionsActive, actionMap);\n\n\t\t\t\t\t// Update the exposed position/delta objects in-place\n\t\t\t\t\tposition.x = snapshot.pointerX;\n\t\t\t\t\tposition.y = snapshot.pointerY;\n\t\t\t\t\tdelta.x = snapshot.pointerDeltaX;\n\t\t\t\t\tdelta.y = snapshot.pointerDeltaY;\n\t\t\t\t});\n\t\t});\n}\n"
|
|
5
|
+
"/**\n * Input Plugin for ECSpresso\n *\n * Resource-only plugin — input is polled via the `inputState` resource. Provides\n * frame-accurate keyboard, pointer (mouse + touch via PointerEvent), up to 4\n * gamepads, and unified + per-player action maps.\n *\n * Mutation model: DOM events accumulate into `raw` between frames and are\n * flattened once per frame into a stable `frame` object whose Sets are cleared\n * and refilled in place (no per-frame allocations). Gamepads are polled once\n * per frame via `navigator.getGamepads()` (or an injected poll function).\n * Unified and per-player action states ping-pong two Sets (`active` / `prev`)\n * so edge detection costs nothing beyond one `.add()` per active action.\n */\n\nimport { definePlugin, type BasePluginOptions, type Vector2D } from 'ecspresso';\n\n// ==================== Public Types ====================\n\n// Key codes per the UI Events spec (KeyboardEvent.key values)\n// https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values\n\ntype LowercaseLetter =\n\t| 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm'\n\t| 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z';\n\ntype Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';\n\ntype Punctuation =\n\t| '`' | '~' | '!' | '@' | '#' | '$' | '%' | '^' | '&' | '*' | '(' | ')'\n\t| '-' | '_' | '=' | '+' | '[' | '{' | ']' | '}' | '\\\\' | '|'\n\t| ';' | ':' | \"'\" | '\"' | ',' | '<' | '.' | '>' | '/' | '?';\n\ntype ModifierKey =\n\t| 'Alt' | 'AltGraph' | 'CapsLock' | 'Control' | 'Fn' | 'FnLock'\n\t| 'Hyper' | 'Meta' | 'NumLock' | 'ScrollLock' | 'Shift'\n\t| 'Super' | 'Symbol' | 'SymbolLock';\n\ntype WhitespaceKey = 'Enter' | 'Tab' | ' ';\n\ntype NavigationKey =\n\t| `Arrow${'Down' | 'Left' | 'Right' | 'Up'}`\n\t| 'End' | 'Home' | 'PageDown' | 'PageUp';\n\ntype EditingKey =\n\t| 'Backspace' | 'Clear' | 'Copy' | 'CrSel' | 'Cut' | 'Delete'\n\t| 'EraseEof' | 'ExSel' | 'Insert' | 'Paste' | 'Redo' | 'Undo';\n\ntype UIKey =\n\t| 'Accept' | 'Again' | 'Attn' | 'Cancel' | 'ContextMenu' | 'Escape'\n\t| 'Execute' | 'Find' | 'Finish' | 'Help' | 'Pause' | 'Play'\n\t| 'Props' | 'Select' | 'ZoomIn' | 'ZoomOut';\n\ntype DeviceKey =\n\t| 'BrightnessDown' | 'BrightnessUp' | 'Eject' | 'Hibernate'\n\t| 'LogOff' | 'Power' | 'PowerOff' | 'PrintScreen' | 'Standby' | 'WakeUp';\n\ntype IMEKey =\n\t| 'AllCandidates' | 'Alphanumeric' | 'CodeInput' | 'Compose' | 'Convert'\n\t| 'FinalMode' | 'GroupFirst' | 'GroupLast' | 'GroupNext' | 'GroupPrevious'\n\t| 'ModeChange' | 'NextCandidate' | 'NonConvert' | 'PreviousCandidate'\n\t| 'Process' | 'SingleCandidate'\n\t| 'HangulMode' | 'HanjaMode' | 'JunjaMode'\n\t| 'Eisu' | 'Hankaku' | 'Hiragana' | 'HiraganaKatakana' | 'KanaMode'\n\t| 'KanjiMode' | 'Katakana' | 'Romaji' | 'Zenkaku' | 'ZenkakuHankaku';\n\ntype FunctionKey =\n\t| `F${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24}`\n\t| 'Soft1' | 'Soft2' | 'Soft3' | 'Soft4';\n\ntype PhoneKey =\n\t| 'AppSwitch' | 'Call' | 'Camera' | 'CameraFocus' | 'EndCall'\n\t| 'GoBack' | 'GoHome' | 'HeadsetHook' | 'LastNumberRedial'\n\t| 'Notification' | 'MannerMode' | 'VoiceDial';\n\ntype MultimediaKey =\n\t| 'ChannelDown' | 'ChannelUp'\n\t| `Media${\n\t\t'FastForward' | 'Pause' | 'Play' | 'PlayPause'\n\t\t| 'Record' | 'Rewind' | 'Stop' | 'TrackNext' | 'TrackPrevious'\n\t}`;\n\ntype AudioKey =\n\t| `Audio${\n\t\t'BalanceLeft' | 'BalanceRight' | 'BassDown' | 'BassBoostDown'\n\t\t| 'BassBoostToggle' | 'BassBoostUp' | 'BassUp' | 'FaderFront' | 'FaderRear'\n\t\t| 'SurroundModeNext' | 'TrebleDown' | 'TrebleUp'\n\t\t| 'VolumeDown' | 'VolumeMute' | 'VolumeUp'\n\t}`\n\t| `Microphone${'Toggle' | 'VolumeDown' | 'VolumeMute' | 'VolumeUp'}`;\n\ntype TVKey =\n\t| 'TV'\n\t| `TV${\n\t\t'3DMode' | 'AntennaCable' | 'AudioDescription' | 'AudioDescriptionMixDown'\n\t\t| 'AudioDescriptionMixUp' | 'ContentsMenu' | 'DataService' | 'Input'\n\t\t| 'InputComponent1' | 'InputComponent2' | 'InputComposite1' | 'InputComposite2'\n\t\t| 'InputHDMI1' | 'InputHDMI2' | 'InputHDMI3' | 'InputHDMI4' | 'InputVGA1'\n\t\t| 'MediaContext' | 'Network' | 'NumberEntry' | 'Power' | 'RadioService'\n\t\t| 'Satellite' | 'SatelliteBS' | 'SatelliteCS' | 'SatelliteToggle'\n\t\t| 'TerrestrialAnalog' | 'TerrestrialDigital' | 'Timer'\n\t}`;\n\ntype MediaControllerKey =\n\t| 'AVRInput' | 'AVRPower'\n\t| `Color${'F0Red' | 'F1Green' | 'F2Yellow' | 'F3Blue' | 'F4Grey' | 'F5Brown'}`\n\t| 'ClosedCaptionToggle' | 'Dimmer' | 'DisplaySwap' | 'DVR' | 'Exit'\n\t| `Favorite${'Clear' | 'Recall' | 'Store'}${0 | 1 | 2 | 3}`\n\t| 'Guide' | 'GuideNextDay' | 'GuidePreviousDay' | 'Info' | 'InstantReplay'\n\t| 'Link' | 'ListProgram' | 'LiveContent' | 'Lock'\n\t| `Media${\n\t\t'Apps' | 'AudioTrack' | 'Last' | 'SkipBackward'\n\t\t| 'SkipForward' | 'StepBackward' | 'StepForward' | 'TopMenu'\n\t}`\n\t| `Navigate${'In' | 'Next' | 'Out' | 'Previous'}`\n\t| 'NextFavoriteChannel' | 'NextUserProfile' | 'OnDemand' | 'Pairing'\n\t| `PinP${'Down' | 'Move' | 'Toggle' | 'Up'}`\n\t| `PlaySpeed${'Down' | 'Reset' | 'Up'}`\n\t| 'RandomToggle' | 'RcLowBattery' | 'RecordSpeedNext' | 'RfBypass'\n\t| 'ScanChannelsToggle' | 'ScreenModeNext' | 'Settings' | 'SplitScreenToggle'\n\t| 'STBInput' | 'STBPower' | 'Subtitle' | 'Teletext'\n\t| 'VideoModeNext' | 'Wink' | 'ZoomToggle';\n\ntype SpeechKey = 'SpeechCorrectionList' | 'SpeechInputToggle';\n\ntype DocumentKey =\n\t| 'Close' | 'New' | 'Open' | 'Print' | 'Save' | 'SpellCheck'\n\t| 'MailForward' | 'MailReply' | 'MailSend';\n\ntype LaunchKey = `Launch${\n\t| 'Calculator' | 'Calendar' | 'Contacts' | 'Mail' | 'MediaPlayer'\n\t| 'MusicPlayer' | 'MyComputer' | 'Phone' | 'ScreenSaver' | 'Spreadsheet'\n\t| 'WebBrowser' | 'WebCam' | 'WordProcessor'\n\t| `Application${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16}`\n}`;\n\ntype BrowserKey = `Browser${'Back' | 'Favorites' | 'Forward' | 'Home' | 'Refresh' | 'Search' | 'Stop'}`;\n\ntype NumpadKey = 'Decimal' | 'Key11' | 'Key12' | 'Multiply' | 'Add' | 'Divide' | 'Subtract' | 'Separator';\n\nexport type KeyCode =\n\t| LowercaseLetter | Uppercase<LowercaseLetter> | Digit | Punctuation\n\t| ModifierKey | WhitespaceKey | NavigationKey | EditingKey | UIKey | DeviceKey\n\t| IMEKey | FunctionKey | PhoneKey | MultimediaKey | AudioKey | TVKey\n\t| MediaControllerKey | SpeechKey | DocumentKey | LaunchKey | BrowserKey | NumpadKey\n\t| 'Unidentified' | 'Dead';\n\nexport interface KeyboardState {\n\tisDown(key: KeyCode): boolean;\n\tjustPressed(key: KeyCode): boolean;\n\tjustReleased(key: KeyCode): boolean;\n}\n\nexport interface PointerState {\n\treadonly position: Readonly<Vector2D>;\n\treadonly delta: Readonly<Vector2D>;\n\tisDown(button: number): boolean;\n\tjustPressed(button: number): boolean;\n\tjustReleased(button: number): boolean;\n}\n\nexport interface GamepadState {\n\treadonly connected: boolean;\n\treadonly id: string | null;\n\tisDown(button: number): boolean;\n\tjustPressed(button: number): boolean;\n\tjustReleased(button: number): boolean;\n\t/** Analog button value in [0, 1]. Useful for triggers. Returns 0 when disconnected or out of range. */\n\tbuttonValue(button: number): number;\n\t/** Deadzone-applied axis value in [-1, 1]. Sticks use radial deadzone on axis pairs (0,1) and (2,3). */\n\taxis(index: number): number;\n\t/** Raw axis value in [-1, 1] with no deadzone applied. */\n\trawAxis(index: number): number;\n}\n\nexport interface ActionState<A extends string = string> {\n\tisActive(action: A): boolean;\n\tjustActivated(action: A): boolean;\n\tjustDeactivated(action: A): boolean;\n}\n\nexport interface PlayerInput<A extends string = string> {\n\treadonly actions: ActionState<A>;\n\tsetActionMap(map: ActionMap<A>): void;\n\tgetActionMap(): Readonly<ActionMap<A>>;\n}\n\nexport interface InputState<A extends string = string> {\n\treadonly keyboard: KeyboardState;\n\treadonly pointer: PointerState;\n\t/** Always length 4 (standard web gamepad slot count). Disconnected slots return `connected: false`. */\n\treadonly gamepads: ReadonlyArray<GamepadState>;\n\t/** Unified action state — fires when any bound input (keyboard, pointer, any pad) is active. Intended for menu/shared input. */\n\treadonly actions: ActionState<A>;\n\tsetActionMap(actions: ActionMap<A>): void;\n\tgetActionMap(): Readonly<ActionMap<A>>;\n\t/** Register or replace a player's action map. Per-player states are isolated from the unified `actions`. */\n\tdefinePlayer(id: string, map: ActionMap<A>): void;\n\t/** Returns true if the player existed and was removed. */\n\tremovePlayer(id: string): boolean;\n\t/** Returns a handle to a registered player's input, or undefined if no such player. */\n\tplayer(id: string): PlayerInput<A> | undefined;\n\tplayerIds(): readonly string[];\n}\n\nexport interface GamepadButtonRef {\n\tpad: number;\n\tbutton: number;\n}\n\nexport interface GamepadAxisRef {\n\tpad: number;\n\taxis: number;\n\t/** Which half of the axis counts as \"active\". */\n\tdirection: 1 | -1;\n\t/** Magnitude at which the axis triggers the action. Applied to the deadzone-adjusted axis value. Default: 0.5. */\n\tthreshold?: number;\n}\n\nexport interface ActionBinding {\n\tkeys?: KeyCode[];\n\t/** Pointer (mouse/touch) button indices — 0 = primary, 1 = auxiliary, 2 = secondary, etc. */\n\tpointerButtons?: number[];\n\tgamepadButtons?: GamepadButtonRef[];\n\tgamepadAxes?: GamepadAxisRef[];\n}\n\nexport type ActionMap<A extends string = string> = Record<A, ActionBinding>;\n\nexport interface InputResourceTypes<A extends string = string> {\n\tinputState: InputState<A>;\n}\n\n/**\n * Minimal gamepad shape required by the injectable poll function. A structural\n * subset of the browser `Gamepad` interface — `navigator.getGamepads()` satisfies\n * it directly, and test doubles can supply just these fields.\n */\nexport interface GamepadLike {\n\tid: string;\n\tconnected: boolean;\n\tbuttons: ReadonlyArray<{ pressed: boolean; value: number }>;\n\taxes: ReadonlyArray<number>;\n}\n\nexport interface GamepadOptions {\n\t/** Radial deadzone applied to stick pairs (axes 0,1 and 2,3). Value in [0, 1]. Default: 0.15. */\n\tdeadzone?: number;\n\t/**\n\t * Custom poll function returning up to 4 gamepad slots. Defaults to `navigator.getGamepads()`.\n\t * Primarily an injection point for tests; in the browser the default is correct.\n\t */\n\tpoll?: () => ReadonlyArray<GamepadLike | null>;\n}\n\nexport interface InputPluginOptions<A extends string = string, G extends string = 'input'> extends BasePluginOptions<G> {\n\t/** Initial unified action map. */\n\tactions?: ActionMap<A>;\n\t/** Initial per-player action maps, keyed by player id. */\n\tplayers?: Record<string, ActionMap<A>>;\n\t/** EventTarget to attach listeners to (default: globalThis). Pass a custom target for testability. */\n\ttarget?: EventTarget;\n\t/** Gamepad polling and deadzone configuration. */\n\tgamepad?: GamepadOptions;\n\t/**\n\t * Optional conversion from raw DOM client coordinates to the space `inputState.pointer.position` should report.\n\t * Renderer-agnostic: wire to `clientToLogical(...)` from renderer2D when using `screenScale`, or to a renderer-specific helper.\n\t * When omitted, pointer coords remain raw `clientX`/`clientY` (not canvas-relative).\n\t */\n\tcoordinateTransform?: (clientX: number, clientY: number) => { x: number; y: number };\n}\n\n// ==================== Helper Functions ====================\n\n/** Create a single action binding. Identity function that provides type inference for inline literals. */\nexport function createActionBinding(binding: ActionBinding): ActionBinding {\n\treturn binding;\n}\n\n/** Build an array of gamepad button refs scoped to one pad — `gamepadButtonsOn(0, 0, 1, 9)` = pad 0's buttons 0, 1, 9. */\nexport function gamepadButtonsOn(pad: number, ...buttons: number[]): GamepadButtonRef[] {\n\treturn buttons.map((button) => ({ pad, button }));\n}\n\n/** Build a gamepad axis ref. `threshold` defaults to 0.5 at activation time. */\nexport function gamepadAxisOn(pad: number, axis: number, direction: 1 | -1, threshold?: number): GamepadAxisRef {\n\treturn threshold === undefined ? { pad, axis, direction } : { pad, axis, direction, threshold };\n}\n\n// ==================== Internal Types ====================\n\ninterface RawKeyPointerState {\n\tkeysDown: Set<string>;\n\tkeysPressed: string[];\n\tkeysReleased: string[];\n\tpointerButtonsDown: Set<number>;\n\tpointerButtonsPressed: number[];\n\tpointerButtonsReleased: number[];\n\tpointerX: number;\n\tpointerY: number;\n\tlastPointerX: number;\n\tlastPointerY: number;\n\tpointerMoved: boolean;\n}\n\n/**\n * Stable per-frame view of keyboard + pointer input. Sets are mutated in place\n * each frame (cleared and refilled from raw), so closures over this object see\n * consistent state throughout a frame without per-frame Set allocation.\n */\ninterface FrameState {\n\tkeysDown: Set<string>;\n\tkeysPressed: Set<string>;\n\tkeysReleased: Set<string>;\n\tpointerButtonsDown: Set<number>;\n\tpointerButtonsPressed: Set<number>;\n\tpointerButtonsReleased: Set<number>;\n\tpointerX: number;\n\tpointerY: number;\n\tpointerDeltaX: number;\n\tpointerDeltaY: number;\n}\n\ninterface PadRuntime {\n\tconnected: boolean;\n\tid: string | null;\n\tbuttonsDown: Set<number>;\n\tbuttonsPrev: Set<number>;\n\tbuttonsPressed: Set<number>;\n\tbuttonsReleased: Set<number>;\n\tbuttonValues: number[];\n\taxes: number[];\n\trawAxes: number[];\n}\n\n/** Two ping-ponged Sets backing an ActionState. Each frame we swap `active` ↔ `prev`, clear the new `active`, and refill. */\ninterface ActionSlot {\n\tactive: Set<string>;\n\tprev: Set<string>;\n}\n\n// ==================== Helpers ====================\n\nconst DEFAULT_AXIS_THRESHOLD = 0.5;\nconst DEFAULT_DEADZONE = 0.15;\nconst PAD_COUNT = 4;\n\nfunction createRawKeyPointerState(): RawKeyPointerState {\n\treturn {\n\t\tkeysDown: new Set(),\n\t\tkeysPressed: [],\n\t\tkeysReleased: [],\n\t\tpointerButtonsDown: new Set(),\n\t\tpointerButtonsPressed: [],\n\t\tpointerButtonsReleased: [],\n\t\tpointerX: 0,\n\t\tpointerY: 0,\n\t\tlastPointerX: 0,\n\t\tlastPointerY: 0,\n\t\tpointerMoved: false,\n\t};\n}\n\nfunction createFrameState(): FrameState {\n\treturn {\n\t\tkeysDown: new Set(),\n\t\tkeysPressed: new Set(),\n\t\tkeysReleased: new Set(),\n\t\tpointerButtonsDown: new Set(),\n\t\tpointerButtonsPressed: new Set(),\n\t\tpointerButtonsReleased: new Set(),\n\t\tpointerX: 0,\n\t\tpointerY: 0,\n\t\tpointerDeltaX: 0,\n\t\tpointerDeltaY: 0,\n\t};\n}\n\nfunction createPadRuntime(): PadRuntime {\n\treturn {\n\t\tconnected: false,\n\t\tid: null,\n\t\tbuttonsDown: new Set(),\n\t\tbuttonsPrev: new Set(),\n\t\tbuttonsPressed: new Set(),\n\t\tbuttonsReleased: new Set(),\n\t\tbuttonValues: [],\n\t\taxes: [],\n\t\trawAxes: [],\n\t};\n}\n\nfunction createActionSlot(): ActionSlot {\n\treturn { active: new Set(), prev: new Set() };\n}\n\nfunction refillSet<T>(dest: Set<T>, source: Iterable<T>): void {\n\tdest.clear();\n\tfor (const item of source) dest.add(item);\n}\n\nfunction updateFrameStateFromRaw(frame: FrameState, raw: RawKeyPointerState): void {\n\trefillSet(frame.keysDown, raw.keysDown);\n\trefillSet(frame.keysPressed, raw.keysPressed);\n\trefillSet(frame.keysReleased, raw.keysReleased);\n\trefillSet(frame.pointerButtonsDown, raw.pointerButtonsDown);\n\trefillSet(frame.pointerButtonsPressed, raw.pointerButtonsPressed);\n\trefillSet(frame.pointerButtonsReleased, raw.pointerButtonsReleased);\n\n\tframe.pointerDeltaX = raw.pointerMoved ? raw.pointerX - raw.lastPointerX : 0;\n\tframe.pointerDeltaY = raw.pointerMoved ? raw.pointerY - raw.lastPointerY : 0;\n\tframe.pointerX = raw.pointerX;\n\tframe.pointerY = raw.pointerY;\n\n\traw.keysPressed.length = 0;\n\traw.keysReleased.length = 0;\n\traw.pointerButtonsPressed.length = 0;\n\traw.pointerButtonsReleased.length = 0;\n\traw.lastPointerX = raw.pointerX;\n\traw.lastPointerY = raw.pointerY;\n\traw.pointerMoved = false;\n}\n\nfunction defaultPoll(out: Array<GamepadLike | null>): () => ReadonlyArray<GamepadLike | null> {\n\treturn () => {\n\t\tif (typeof navigator === 'undefined' || typeof navigator.getGamepads !== 'function') {\n\t\t\tfor (let i = 0; i < out.length; i++) out[i] = null;\n\t\t\treturn out;\n\t\t}\n\t\tconst pads = navigator.getGamepads();\n\t\tfor (let i = 0; i < out.length; i++) out[i] = pads[i] ?? null;\n\t\treturn out;\n\t};\n}\n\nfunction applyStickDeadzone(x: number, y: number, deadzone: number, out: number[], baseIndex: number): void {\n\tconst mag = Math.sqrt(x * x + y * y);\n\tif (mag < deadzone) {\n\t\tout[baseIndex] = 0;\n\t\tout[baseIndex + 1] = 0;\n\t\treturn;\n\t}\n\tconst scaled = Math.min((mag - deadzone) / (1 - deadzone), 1);\n\tout[baseIndex] = (x / mag) * scaled;\n\tout[baseIndex + 1] = (y / mag) * scaled;\n}\n\nfunction applyAxisDeadzoning(rawAxes: number[], axes: number[], deadzone: number): void {\n\taxes.length = rawAxes.length;\n\tif (rawAxes.length >= 2) {\n\t\tapplyStickDeadzone(rawAxes[0] ?? 0, rawAxes[1] ?? 0, deadzone, axes, 0);\n\t}\n\tif (rawAxes.length >= 4) {\n\t\tapplyStickDeadzone(rawAxes[2] ?? 0, rawAxes[3] ?? 0, deadzone, axes, 2);\n\t}\n\t// Axes beyond the two standard sticks pass through with no deadzone (triggers, dpad-as-axis, etc.)\n\tfor (let i = 4; i < rawAxes.length; i++) {\n\t\taxes[i] = rawAxes[i] ?? 0;\n\t}\n}\n\nfunction pollGamepadsInto(pads: PadRuntime[], pollFn: () => ReadonlyArray<GamepadLike | null>, deadzone: number): void {\n\tconst polled = pollFn();\n\tfor (let i = 0; i < PAD_COUNT; i++) {\n\t\tconst pad = polled[i] ?? null;\n\t\tconst state = pads[i];\n\t\tif (!state) continue;\n\n\t\t// Rotate button sets using the existing `prev` set as scratch, then clear what we'll refill.\n\t\tconst reusedPrev = state.buttonsPrev;\n\t\trefillSet(reusedPrev, state.buttonsDown);\n\t\tstate.buttonsDown.clear();\n\t\tstate.buttonsPressed.clear();\n\t\tstate.buttonsReleased.clear();\n\n\t\tif (!pad || !pad.connected) {\n\t\t\tif (state.connected) {\n\t\t\t\t// Newly disconnected: synthesize justReleased for anything that was held, then clear values.\n\t\t\t\tfor (const b of reusedPrev) state.buttonsReleased.add(b);\n\t\t\t\tstate.connected = false;\n\t\t\t\tstate.id = null;\n\t\t\t\tstate.buttonValues.length = 0;\n\t\t\t\tstate.axes.length = 0;\n\t\t\t\tstate.rawAxes.length = 0;\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tstate.connected = true;\n\t\tstate.id = pad.id;\n\n\t\tstate.buttonValues.length = pad.buttons.length;\n\t\tfor (let b = 0; b < pad.buttons.length; b++) {\n\t\t\tconst info = pad.buttons[b];\n\t\t\tif (!info) {\n\t\t\t\tstate.buttonValues[b] = 0;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tstate.buttonValues[b] = info.value;\n\t\t\tif (info.pressed) state.buttonsDown.add(b);\n\t\t}\n\n\t\tfor (const b of state.buttonsDown) {\n\t\t\tif (!reusedPrev.has(b)) state.buttonsPressed.add(b);\n\t\t}\n\t\tfor (const b of reusedPrev) {\n\t\t\tif (!state.buttonsDown.has(b)) state.buttonsReleased.add(b);\n\t\t}\n\n\t\tstate.rawAxes.length = pad.axes.length;\n\t\tfor (let a = 0; a < pad.axes.length; a++) {\n\t\t\tstate.rawAxes[a] = pad.axes[a] ?? 0;\n\t\t}\n\t\tapplyAxisDeadzoning(state.rawAxes, state.axes, deadzone);\n\t}\n}\n\nfunction isBindingActive(\n\tbinding: ActionBinding,\n\tkeysDown: ReadonlySet<string>,\n\tpointerButtonsDown: ReadonlySet<number>,\n\tpads: ReadonlyArray<PadRuntime>,\n): boolean {\n\tif (binding.keys?.some((k) => keysDown.has(k))) return true;\n\tif (binding.pointerButtons?.some((b) => pointerButtonsDown.has(b))) return true;\n\tif (binding.gamepadButtons?.some(({ pad, button }) => pads[pad]?.buttonsDown.has(button) ?? false)) return true;\n\tif (binding.gamepadAxes?.some(({ pad, axis, direction, threshold = DEFAULT_AXIS_THRESHOLD }) => {\n\t\tconst value = pads[pad]?.axes[axis] ?? 0;\n\t\treturn direction > 0 ? value > threshold : value < -threshold;\n\t})) return true;\n\treturn false;\n}\n\n/**\n * Recompute the slot's `active` set in place from `map` against current input sources.\n * Rotates `active` ↔ `prev` (reusing Set instances) so edge detection works with no allocations.\n */\nfunction advanceActionSlot(\n\tslot: ActionSlot,\n\tmap: ActionMap,\n\tkeysDown: ReadonlySet<string>,\n\tpointerButtonsDown: ReadonlySet<number>,\n\tpads: ReadonlyArray<PadRuntime>,\n): void {\n\tconst nextActive = slot.prev;\n\tslot.prev = slot.active;\n\tslot.active = nextActive;\n\tnextActive.clear();\n\n\tfor (const [name, binding] of Object.entries(map)) {\n\t\tif (isBindingActive(binding, keysDown, pointerButtonsDown, pads)) nextActive.add(name);\n\t}\n}\n\nfunction makeActionState<A extends string>(slot: ActionSlot): ActionState<A> {\n\treturn {\n\t\tisActive: (action) => slot.active.has(action),\n\t\tjustActivated: (action) => slot.active.has(action) && !slot.prev.has(action),\n\t\tjustDeactivated: (action) => !slot.active.has(action) && slot.prev.has(action),\n\t};\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create an input plugin for ECSpresso.\n *\n * Provides:\n * - Frame-accurate keyboard state (isDown, justPressed, justReleased)\n * - Pointer position/delta and button state (mouse + touch via PointerEvent)\n * - Up to 4 gamepads polled per frame, with radial deadzone on sticks and analog button values\n * - Unified action mapping (keyboard + pointer + any pad)\n * - Per-player action maps for local co-op (`definePlayer`, `player(id)`)\n * - Automatic listener cleanup on detach\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createInputPlugin({\n * actions: {\n * jump: { keys: [' ', 'ArrowUp'], gamepadButtons: [{ pad: 0, button: 0 }] },\n * shoot: { keys: ['z'], pointerButtons: [0] },\n * },\n * players: {\n * p1: { jump: { keys: [' '] }, shoot: { keys: ['z'] } },\n * p2: {\n * jump: { gamepadButtons: gamepadButtonsOn(0, 0) },\n * shoot: { gamepadButtons: gamepadButtonsOn(0, 2) },\n * },\n * },\n * }))\n * .build();\n *\n * const input = ecs.getResource('inputState');\n * if (input.actions.justActivated('jump')) { ... } // any source\n * if (input.player('p1')?.actions.isActive('jump')) { ... } // just player 1\n * if (input.gamepads[0].isDown(0)) { ... } // raw pad 0 A-button\n * ```\n */\nexport function createInputPlugin<A extends string = string, G extends string = 'input'>(\n\toptions?: InputPluginOptions<A, G>\n) {\n\tconst {\n\t\tsystemGroup = 'input',\n\t\tpriority = 100,\n\t\tphase = 'preUpdate',\n\t\ttarget = globalThis,\n\t\tgamepad: gamepadOpts = {},\n\t\tcoordinateTransform,\n\t} = options ?? {};\n\n\t// Construction-time casts: option defaults of `{}` don't structurally satisfy a narrow `ActionMap<A>`,\n\t// but at this boundary we know the user either supplied a valid map or is using A = string.\n\tconst unifiedActionMap = { ...(options?.actions ?? {}) } as ActionMap<A>;\n\tconst playerMaps = new Map<string, ActionMap<A>>(\n\t\tObject.entries(options?.players ?? {}) as Array<[string, ActionMap<A>]>,\n\t);\n\n\tconst deadzone = gamepadOpts.deadzone ?? DEFAULT_DEADZONE;\n\tconst pollFn = gamepadOpts.poll ?? defaultPoll(new Array<GamepadLike | null>(PAD_COUNT).fill(null));\n\n\tconst raw = createRawKeyPointerState();\n\tconst frame = createFrameState();\n\tconst pads: PadRuntime[] = Array.from({ length: PAD_COUNT }, createPadRuntime);\n\tconst unifiedSlot = createActionSlot();\n\tconst playerSlots = new Map<string, ActionSlot>();\n\tconst playerHandles = new Map<string, PlayerInput<A>>();\n\tconst cleanupFns: Array<() => void> = [];\n\n\t// Vector2Ds exposed via the resource — updated in place each frame.\n\tconst position: Vector2D = { x: 0, y: 0 };\n\tconst delta: Vector2D = { x: 0, y: 0 };\n\n\tlet currentUnifiedMap = unifiedActionMap;\n\n\tconst keyboard: KeyboardState = {\n\t\tisDown: (key) => frame.keysDown.has(key),\n\t\tjustPressed: (key) => frame.keysPressed.has(key),\n\t\tjustReleased: (key) => frame.keysReleased.has(key),\n\t};\n\n\tconst pointer: PointerState = {\n\t\tposition,\n\t\tdelta,\n\t\tisDown: (button) => frame.pointerButtonsDown.has(button),\n\t\tjustPressed: (button) => frame.pointerButtonsPressed.has(button),\n\t\tjustReleased: (button) => frame.pointerButtonsReleased.has(button),\n\t};\n\n\tfunction makeGamepadState(index: number): GamepadState {\n\t\tconst state = pads[index];\n\t\tif (!state) throw new Error(`Invalid gamepad index: ${index}`);\n\t\treturn {\n\t\t\tget connected() { return state.connected; },\n\t\t\tget id() { return state.id; },\n\t\t\tisDown: (button) => state.buttonsDown.has(button),\n\t\t\tjustPressed: (button) => state.buttonsPressed.has(button),\n\t\t\tjustReleased: (button) => state.buttonsReleased.has(button),\n\t\t\tbuttonValue: (button) => state.buttonValues[button] ?? 0,\n\t\t\taxis: (index) => state.axes[index] ?? 0,\n\t\t\trawAxis: (index) => state.rawAxes[index] ?? 0,\n\t\t};\n\t}\n\n\tconst gamepadStates: ReadonlyArray<GamepadState> = Array.from({ length: PAD_COUNT }, (_, i) => makeGamepadState(i));\n\n\tconst unifiedActions = makeActionState<A>(unifiedSlot);\n\n\tfunction ensurePlayerSlot(id: string): ActionSlot {\n\t\tconst existing = playerSlots.get(id);\n\t\tif (existing) return existing;\n\t\tconst slot = createActionSlot();\n\t\tplayerSlots.set(id, slot);\n\t\treturn slot;\n\t}\n\n\tfunction createPlayerHandle(id: string): PlayerInput<A> {\n\t\tconst slot = ensurePlayerSlot(id);\n\t\treturn {\n\t\t\tactions: makeActionState<A>(slot),\n\t\t\tsetActionMap: (map) => {\n\t\t\t\tif (!playerMaps.has(id)) throw new Error(`Player '${id}' was removed`);\n\t\t\t\tplayerMaps.set(id, { ...map });\n\t\t\t},\n\t\t\tgetActionMap: () => {\n\t\t\t\tconst map = playerMaps.get(id);\n\t\t\t\tif (!map) throw new Error(`Player '${id}' was removed`);\n\t\t\t\treturn { ...map };\n\t\t\t},\n\t\t};\n\t}\n\n\tfor (const id of playerMaps.keys()) {\n\t\tplayerHandles.set(id, createPlayerHandle(id));\n\t}\n\n\tconst inputState: InputState<A> = {\n\t\tkeyboard,\n\t\tpointer,\n\t\tgamepads: gamepadStates,\n\t\tactions: unifiedActions,\n\t\tsetActionMap(newMap) {\n\t\t\tcurrentUnifiedMap = { ...newMap };\n\t\t},\n\t\tgetActionMap() {\n\t\t\treturn { ...currentUnifiedMap };\n\t\t},\n\t\tdefinePlayer(id, map) {\n\t\t\tplayerMaps.set(id, { ...map });\n\t\t\tif (!playerHandles.has(id)) playerHandles.set(id, createPlayerHandle(id));\n\t\t},\n\t\tremovePlayer(id) {\n\t\t\tconst existed = playerMaps.delete(id);\n\t\t\tplayerHandles.delete(id);\n\t\t\tplayerSlots.delete(id);\n\t\t\treturn existed;\n\t\t},\n\t\tplayer(id) {\n\t\t\treturn playerHandles.get(id);\n\t\t},\n\t\tplayerIds() {\n\t\t\treturn Array.from(playerMaps.keys());\n\t\t},\n\t};\n\n\tfunction onKeyDown(e: Event) {\n\t\tconst ke = e as KeyboardEvent;\n\t\tif (ke.repeat) return;\n\t\traw.keysDown.add(ke.key);\n\t\traw.keysPressed.push(ke.key);\n\t}\n\n\tfunction onKeyUp(e: Event) {\n\t\tconst ke = e as KeyboardEvent;\n\t\traw.keysDown.delete(ke.key);\n\t\traw.keysReleased.push(ke.key);\n\t}\n\n\tfunction onPointerDown(e: Event) {\n\t\tconst pe = e as PointerEvent;\n\t\traw.pointerButtonsDown.add(pe.button);\n\t\traw.pointerButtonsPressed.push(pe.button);\n\t}\n\n\tfunction onPointerMove(e: Event) {\n\t\tconst pe = e as PointerEvent;\n\t\tif (coordinateTransform) {\n\t\t\tconst { x, y } = coordinateTransform(pe.clientX, pe.clientY);\n\t\t\traw.pointerX = x;\n\t\t\traw.pointerY = y;\n\t\t} else {\n\t\t\traw.pointerX = pe.clientX;\n\t\t\traw.pointerY = pe.clientY;\n\t\t}\n\t\traw.pointerMoved = true;\n\t}\n\n\tfunction onPointerUp(e: Event) {\n\t\tconst pe = e as PointerEvent;\n\t\traw.pointerButtonsDown.delete(pe.button);\n\t\traw.pointerButtonsReleased.push(pe.button);\n\t}\n\n\tfunction addListener(type: string, handler: (e: Event) => void) {\n\t\ttarget.addEventListener(type, handler);\n\t\tcleanupFns.push(() => { target.removeEventListener(type, handler); });\n\t}\n\n\treturn definePlugin('input')\n\t\t.withResourceTypes<InputResourceTypes<A>>()\n\t\t.withLabels<'input-state'>()\n\t\t.withGroups<G>()\n\t\t.install((world) => {\n\t\t\tworld.addResource('inputState', inputState);\n\n\t\t\tworld\n\t\t\t\t.addSystem('input-state')\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.setOnInitialize(() => {\n\t\t\t\t\taddListener('keydown', onKeyDown);\n\t\t\t\t\taddListener('keyup', onKeyUp);\n\t\t\t\t\taddListener('pointerdown', onPointerDown);\n\t\t\t\t\taddListener('pointermove', onPointerMove);\n\t\t\t\t\taddListener('pointerup', onPointerUp);\n\t\t\t\t})\n\t\t\t\t.setOnDetach(() => {\n\t\t\t\t\tfor (const cleanup of cleanupFns) cleanup();\n\t\t\t\t\tcleanupFns.length = 0;\n\t\t\t\t})\n\t\t\t\t.setProcess(() => {\n\t\t\t\t\t// Pads must be polled before action computation so a single frame reflects\n\t\t\t\t\t// both DOM-driven (keyboard/pointer) and polled (gamepad) sources consistently.\n\t\t\t\t\tpollGamepadsInto(pads, pollFn, deadzone);\n\t\t\t\t\tupdateFrameStateFromRaw(frame, raw);\n\n\t\t\t\t\tposition.x = frame.pointerX;\n\t\t\t\t\tposition.y = frame.pointerY;\n\t\t\t\t\tdelta.x = frame.pointerDeltaX;\n\t\t\t\t\tdelta.y = frame.pointerDeltaY;\n\n\t\t\t\t\tadvanceActionSlot(unifiedSlot, currentUnifiedMap, frame.keysDown, frame.pointerButtonsDown, pads);\n\t\t\t\t\tfor (const [id, map] of playerMaps) {\n\t\t\t\t\t\tconst slot = ensurePlayerSlot(id);\n\t\t\t\t\t\tadvanceActionSlot(slot, map, frame.keysDown, frame.pointerButtonsDown, pads);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": "
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "A22CC6420E47F29C64756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var e=((P)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(P,{get:(M,K)=>(typeof require<"u"?require:M)[K]}):P)(function(P){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+P+'" is not supported')});import{definePlugin as r}from"ecspresso";var w={traumaDecay:1,maxOffsetX:10,maxOffsetY:10,maxRotation:0.05},u={smoothing:5,deadzoneX:0,deadzoneY:0,offsetX:0,offsetY:0};function QJ(P,M,K){let U=P-(K.x+K.shakeOffsetX),O=M-(K.y+K.shakeOffsetY),W=-(K.rotation+K.shakeRotation),E=Math.cos(W),A=Math.sin(W),y=U*E-O*A,q=U*A+O*E;return{x:y*K.zoom+K.viewportWidth/2,y:q*K.zoom+K.viewportHeight/2}}function p(P,M,K){let U=(P-K.viewportWidth/2)/K.zoom,O=(M-K.viewportHeight/2)/K.zoom,W=K.rotation+K.shakeRotation,E=Math.cos(W),A=Math.sin(W),y=U*E-O*A,q=U*A+O*E;return{x:y+K.x+K.shakeOffsetX,y:q+K.y+K.shakeOffsetY}}function o(P){return typeof P==="number"?P:P.id}function m(P){let M=P===!0?{}:P;return{trauma:0,traumaDecay:M.traumaDecay??w.traumaDecay,maxOffsetX:M.maxOffsetX??w.maxOffsetX,maxOffsetY:M.maxOffsetY??w.maxOffsetY,maxRotation:M.maxRotation??w.maxRotation}}function s(P){if(Array.isArray(P))return{minX:P[0],minY:P[1],maxX:P[2],maxY:P[3]};return{...P}}function i(P){return{smoothing:P?.smoothing??u.smoothing,deadzoneX:P?.deadzoneX??u.deadzoneX,deadzoneY:P?.deadzoneY??u.deadzoneY,offsetX:P?.offsetX??u.offsetX,offsetY:P?.offsetY??u.offsetY}}function $J(P){let{viewportWidth:M=800,viewportHeight:K=600,initial:U,follow:O,shake:W,bounds:E,zoom:A,pan:y,systemGroup:q="camera",phase:g="postUpdate",randomFn:f=Math.random}=P??{};return r("camera").withComponentTypes().withResourceTypes().withLabels().withGroups().requires().install((k)=>{let J={x:U?.x??0,y:U?.y??0,zoom:U?.zoom??1,rotation:U?.rotation??0,shakeOffsetX:0,shakeOffsetY:0,shakeRotation:0,viewportWidth:M,viewportHeight:K,entityId:-1,follow:()=>{},unfollow:()=>{},setPosition:()=>{},setZoom:()=>{},setRotation:()=>{},setBounds:()=>{},clearBounds:()=>{},addTrauma:()=>{}};if(k.addResource("cameraState",J),k.addSystem("camera-init").inGroup(q).setOnInitialize((R)=>{let V=R.spawn({camera:{x:U?.x??0,y:U?.y??0,zoom:U?.zoom??1,rotation:U?.rotation??0}});if(O)R.addComponent(V.id,"cameraFollow",{target:-1,...i(O)});if(W)R.addComponent(V.id,"cameraShake",m(W));if(E)R.addComponent(V.id,"cameraBounds",s(E));J.entityId=V.id,J.follow=(N,$)=>{let j={target:o(N),...i($)},Q=R.getComponent(J.entityId,"cameraFollow");if(Q)Q.target=j.target,Q.smoothing=j.smoothing,Q.deadzoneX=j.deadzoneX,Q.deadzoneY=j.deadzoneY,Q.offsetX=j.offsetX,Q.offsetY=j.offsetY;else R.addComponent(J.entityId,"cameraFollow",j)},J.unfollow=()=>{if(R.getComponent(J.entityId,"cameraFollow"))R.removeComponent(J.entityId,"cameraFollow")},J.setPosition=(N,$)=>{let Z=R.getComponent(J.entityId,"camera");if(!Z)return;Z.x=N,Z.y=$},J.setZoom=(N)=>{let $=R.getComponent(J.entityId,"camera");if(!$)return;$.zoom=N},J.setRotation=(N)=>{let $=R.getComponent(J.entityId,"camera");if(!$)return;$.rotation=N},J.setBounds=(N,$,Z,j)=>{let Q=R.getComponent(J.entityId,"cameraBounds");if(Q)Q.minX=N,Q.minY=$,Q.maxX=Z,Q.maxY=j;else R.addComponent(J.entityId,"cameraBounds",{minX:N,minY:$,maxX:Z,maxY:j})},J.clearBounds=()=>{if(R.getComponent(J.entityId,"cameraBounds"))R.removeComponent(J.entityId,"cameraBounds")},J.addTrauma=(N)=>{let $=R.getComponent(J.entityId,"cameraShake");if($)$.trauma=Math.min(1,Math.max(0,$.trauma+N));else R.addComponent(J.entityId,"cameraShake",{...m(!0),trauma:Math.min(1,Math.max(0,N))})}}),k.addSystem("camera-follow").setPriority(400).inPhase(g).inGroup(q).addQuery("cameras",{with:["camera","cameraFollow"]}).setProcess(({queries:R,dt:V,ecs:N})=>{let $=Math.min(1,V);for(let Z of R.cameras){let{camera:j,cameraFollow:Q}=Z.components;if(Q.target<0)continue;let _;try{_=N.getComponent(Q.target,"worldTransform")}catch{continue}if(!_)continue;let z=_.x+Q.offsetX,G=_.y+Q.offsetY,B=z-j.x,b=G-j.y;if(Math.abs(B)>Q.deadzoneX){let H=B>0?1:-1,D=B-H*Q.deadzoneX,F=Math.min(1,Q.smoothing*$);j.x+=D*F}if(Math.abs(b)>Q.deadzoneY){let H=b>0?1:-1,D=b-H*Q.deadzoneY,F=Math.min(1,Q.smoothing*$);j.y+=D*F}}}),k.addSystem("camera-shake-update").setPriority(390).inPhase(g).inGroup(q).addQuery("shakeCameras",{with:["camera","cameraShake"]}).setProcess(({queries:R,dt:V})=>{for(let N of R.shakeCameras){let{cameraShake:$}=N.components;$.trauma=Math.max(0,$.trauma-$.traumaDecay*V)}}),k.addSystem("camera-bounds").setPriority(380).inPhase(g).inGroup(q).addQuery("boundedCameras",{with:["camera","cameraBounds"]}).setProcess(({queries:R})=>{for(let V of R.boundedCameras){let{camera:N,cameraBounds:$}=V.components,Z=J.viewportWidth/(2*N.zoom),j=J.viewportHeight/(2*N.zoom),Q=$.minX+Z,_=$.maxX-Z,z=$.minY+j,G=$.maxY-j;if(Q>_)N.x=($.minX+$.maxX)/2;else N.x=Math.max(Q,Math.min(_,N.x));if(z>G)N.y=($.minY+$.maxY)/2;else N.y=Math.max(z,Math.min(G,N.y))}}),k.addSystem("camera-state-sync").setPriority(370).inPhase(g).inGroup(q).setProcess(({ecs:R})=>{let V=R.getComponent(J.entityId,"camera");if(!V){J.x=0,J.y=0,J.zoom=1,J.rotation=0,J.shakeOffsetX=0,J.shakeOffsetY=0,J.shakeRotation=0;return}J.x=V.x,J.y=V.y,J.zoom=V.zoom,J.rotation=V.rotation;let N=R.getComponent(J.entityId,"cameraShake");if(N&&N.trauma>0){let $=N.trauma*N.trauma;J.shakeOffsetX=N.maxOffsetX*$*(f()*2-1),J.shakeOffsetY=N.maxOffsetY*$*(f()*2-1),J.shakeRotation=N.maxRotation*$*(f()*2-1)}else J.shakeOffsetX=0,J.shakeOffsetY=0,J.shakeRotation=0}),A){let _=function(z){z.preventDefault(),$+=Math.sign(z.deltaY)},{zoomStep:R=0.1,minZoom:V=0.1,maxZoom:N=10}=A,$=0,Z=!1,j,Q;k.addSystem("camera-zoom").setPriority(410).inPhase("preUpdate").inGroup(q).addQuery("cameras",{with:["camera"]}).setOnInitialize((z)=>{let G=z.tryGetResource("inputState"),B=z.tryGetResource("pixiApp");if(!G||!B){console.error("[camera] zoom requires the input plugin and renderer2D plugin. Zoom will be disabled.");return}j=B.canvas,j.addEventListener("wheel",_,{passive:!1}),Q=z.tryGetResource("isoProjection"),Z=!0}).setOnDetach(()=>{if(!Z||!j)return;j.removeEventListener("wheel",_)}).setProcess(({queries:z,ecs:G})=>{if(!Z||$===0)return;let B=$;$=0;let[b]=z.cameras;if(!b)return;let H=b.components.camera,D=G.tryGetResource("inputState");if(!D)return;let F=B>0?1-R:1+R,C=Math.max(V,Math.min(N,H.zoom*Math.pow(F,Math.abs(B))));if(Q&&j){let I=j.getBoundingClientRect(),L=D.pointer.position.x-(I.left+I.width/2),v=D.pointer.position.y-(I.top+I.height/2),X=Q.tileWidth/2,Y=Q.tileHeight/2,h=(H.x-H.y)*X+Q.originX,T=(H.x+H.y)*Y+Q.originY,x=h+L/H.zoom,S=T+v/H.zoom;H.zoom=C;let c=x-L/C,n=S-v/C,d=c-Q.originX,l=n-Q.originY;H.x=d/Q.tileWidth+l/Q.tileHeight,H.y=-d/Q.tileWidth+l/Q.tileHeight}else{let I=p(D.pointer.position.x,D.pointer.position.y,J);H.zoom=C,H.x=I.x-(D.pointer.position.x-J.viewportWidth/2)/C,H.y=I.y-(D.pointer.position.y-J.viewportHeight/2)/C}})}if(y){let{speed:R,actions:V}=y,N=V?.up??"panUp",$=V?.down??"panDown",Z=V?.left??"panLeft",j=V?.right??"panRight",Q=!1;k.addSystem("camera-pan").setPriority(420).inPhase("preUpdate").inGroup(q).setOnInitialize((_)=>{if(!_.tryGetResource("inputState")){console.error("[camera] pan requires the input plugin. Pan will be disabled.");return}Q=!0}).setProcess(({ecs:_,dt:z})=>{if(!Q)return;let G=_.tryGetResource("inputState");if(!G)return;let B=R/J.zoom*z,b=(G.actions.isActive(j)?1:0)-(G.actions.isActive(Z)?1:0),H=(G.actions.isActive($)?1:0)-(G.actions.isActive(N)?1:0);if(b!==0||H!==0)J.setPosition(J.x+b*B,J.y+H*B)})}})}import{Graphics as a}from"pixi.js";import{definePlugin as t}from"ecspresso";function KJ(){return{selectable:!0}}function IJ(P){let{systemGroup:M="selection",priority:K=100,phase:U="preUpdate",clickThreshold:O=5,boxFillColor:W=65280,boxFillAlpha:E=0.15,boxStrokeColor:A=65280,boxStrokeAlpha:y=0.8,selectedTint:q=4521796,renderLayer:g}=P??{},f={color:W,alpha:E},k={color:A,width:1.5,alpha:y};return t("selection").withComponentTypes().withResourceTypes().withLabels().withGroups().requires().install((J)=>{J.addResource("selectionState",{dragStart:{x:0,y:0},boxEntityId:null});let R=null;J.addSystem("selection-input").setPriority(K).inPhase(U).inGroup(M).addQuery("selectables",{with:["selectable","worldTransform"]}).addQuery("currentlySelected",{with:["selected"]}).withResources(["inputState","selectionState","pixiApp"]).setOnInitialize((V)=>{let N=V.getResource("pixiApp");R=($)=>$.preventDefault(),N.canvas.addEventListener("contextmenu",R)}).setOnDetach((V)=>{if(!R)return;V.getResource("pixiApp").canvas.removeEventListener("contextmenu",R),R=null}).setProcess(({queries:V,ecs:N,resources:$})=>{let{inputState:Z,selectionState:j}=$,Q=Z.pointer;if(Q.justPressed(0)){if(j.boxEntityId!==null)N.commands.removeEntity(j.boxEntityId);j.dragStart.x=Q.position.x,j.dragStart.y=Q.position.y;let I=N.spawn({graphics:new a});if(g)N.addComponent(I.id,"renderLayer",g);j.boxEntityId=I.id}if(Q.isDown(0)&&j.boxEntityId!==null){let I=N.getComponent(j.boxEntityId,"graphics");if(!I)return;let L=j.dragStart.x,v=j.dragStart.y,X=Q.position.x,Y=Q.position.y,h=Math.min(L,X),T=Math.min(v,Y),x=Math.abs(X-L),S=Math.abs(Y-v);I.clear(),I.rect(h,T,x,S),I.fill(f),I.stroke(k)}if(!Q.justReleased(0)||j.boxEntityId===null)return;let _=j.dragStart.x,z=j.dragStart.y,G=Q.position.x,B=Q.position.y,b=Math.abs(G-_),H=Math.abs(B-z);for(let I of V.currentlySelected)N.removeComponent(I.id,"selected");let D=b<O&&H<O,F=N.tryGetResource("cameraState"),C=F?p(G,B,F):{x:G,y:B};if(D){let L=null,v=1/0;for(let X of V.selectables){let{worldTransform:Y}=X.components,h=Y.x-C.x,T=Y.y-C.y,x=h*h+T*T;if(x<400&&x<v)v=x,L=X.id}if(L!==null)N.addComponent(L,"selected",!0)}else{let I=F?p(_,z,F):{x:_,y:z},L=Math.min(I.x,C.x),v=Math.max(I.x,C.x),X=Math.min(I.y,C.y),Y=Math.max(I.y,C.y);for(let h of V.selectables){let{worldTransform:T}=h.components;if(T.x>=L&&T.x<=v&&T.y>=X&&T.y<=Y)N.addComponent(h.id,"selected",!0)}}N.commands.removeEntity(j.boxEntityId),j.boxEntityId=null}),J.addSystem("selection-visual").setPriority(K).inPhase("render").inGroup(M).addQuery("selectedUnits",{with:["selected","sprite"]}).setOnEntityEnter("selectedUnits",({entity:V})=>{V.components.sprite.tint=q}).addQuery("deselectedUnits",{with:["selectable","sprite"],without:["selected"]}).setOnEntityEnter("deselectedUnits",({entity:V})=>{V.components.sprite.tint=16777215})})}export{IJ as createSelectionPlugin,KJ as createSelectable};
|
|
1
|
+
var r=Object.defineProperty;var o=(j)=>j;function s(j,I){this[j]=o.bind(null,I)}var QJ=(j,I)=>{for(var K in I)r(j,K,{get:I[K],enumerable:!0,configurable:!0,set:s.bind(I,K)})};var $J=(j,I)=>()=>(j&&(I=j(j=0)),I);var jJ=((j)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(j,{get:(I,K)=>(typeof require<"u"?require:I)[K]}):j)(function(j){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+j+'" is not supported')});import{definePlugin as a}from"ecspresso";var w={traumaDecay:1,maxOffsetX:10,maxOffsetY:10,maxRotation:0.05},u={smoothing:5,deadzoneX:0,deadzoneY:0,offsetX:0,offsetY:0};function PJ(j,I,K){let B=j-(K.x+K.shakeOffsetX),O=I-(K.y+K.shakeOffsetY),W=-(K.rotation+K.shakeRotation),E=Math.cos(W),A=Math.sin(W),y=B*E-O*A,q=B*A+O*E;return{x:y*K.zoom+K.viewportWidth/2,y:q*K.zoom+K.viewportHeight/2}}function p(j,I,K){let B=(j-K.viewportWidth/2)/K.zoom,O=(I-K.viewportHeight/2)/K.zoom,W=K.rotation+K.shakeRotation,E=Math.cos(W),A=Math.sin(W),y=B*E-O*A,q=B*A+O*E;return{x:y+K.x+K.shakeOffsetX,y:q+K.y+K.shakeOffsetY}}function t(j){return typeof j==="number"?j:j.id}function m(j){let I=j===!0?{}:j;return{trauma:0,traumaDecay:I.traumaDecay??w.traumaDecay,maxOffsetX:I.maxOffsetX??w.maxOffsetX,maxOffsetY:I.maxOffsetY??w.maxOffsetY,maxRotation:I.maxRotation??w.maxRotation}}function e(j){if(Array.isArray(j))return{minX:j[0],minY:j[1],maxX:j[2],maxY:j[3]};return{...j}}function i(j){return{smoothing:j?.smoothing??u.smoothing,deadzoneX:j?.deadzoneX??u.deadzoneX,deadzoneY:j?.deadzoneY??u.deadzoneY,offsetX:j?.offsetX??u.offsetX,offsetY:j?.offsetY??u.offsetY}}function KJ(j){let{viewportWidth:I=800,viewportHeight:K=600,initial:B,follow:O,shake:W,bounds:E,zoom:A,pan:y,systemGroup:q="camera",phase:g="postUpdate",randomFn:f=Math.random}=j??{};return a("camera").withComponentTypes().withResourceTypes().withLabels().withGroups().requires().install((k)=>{let J={x:B?.x??0,y:B?.y??0,zoom:B?.zoom??1,rotation:B?.rotation??0,shakeOffsetX:0,shakeOffsetY:0,shakeRotation:0,viewportWidth:I,viewportHeight:K,entityId:-1,follow:()=>{},unfollow:()=>{},setPosition:()=>{},setZoom:()=>{},setRotation:()=>{},setBounds:()=>{},clearBounds:()=>{},addTrauma:()=>{}};if(k.addResource("cameraState",J),k.addSystem("camera-init").inGroup(q).setOnInitialize((V)=>{let P=V.spawn({camera:{x:B?.x??0,y:B?.y??0,zoom:B?.zoom??1,rotation:B?.rotation??0}});if(O)V.addComponent(P.id,"cameraFollow",{target:-1,...i(O)});if(W)V.addComponent(P.id,"cameraShake",m(W));if(E)V.addComponent(P.id,"cameraBounds",e(E));J.entityId=P.id,J.follow=(N,$)=>{let R={target:t(N),...i($)},Q=V.getComponent(J.entityId,"cameraFollow");if(Q)Q.target=R.target,Q.smoothing=R.smoothing,Q.deadzoneX=R.deadzoneX,Q.deadzoneY=R.deadzoneY,Q.offsetX=R.offsetX,Q.offsetY=R.offsetY;else V.addComponent(J.entityId,"cameraFollow",R)},J.unfollow=()=>{if(V.getComponent(J.entityId,"cameraFollow"))V.removeComponent(J.entityId,"cameraFollow")},J.setPosition=(N,$)=>{let z=V.getComponent(J.entityId,"camera");if(!z)return;z.x=N,z.y=$},J.setZoom=(N)=>{let $=V.getComponent(J.entityId,"camera");if(!$)return;$.zoom=N},J.setRotation=(N)=>{let $=V.getComponent(J.entityId,"camera");if(!$)return;$.rotation=N},J.setBounds=(N,$,z,R)=>{let Q=V.getComponent(J.entityId,"cameraBounds");if(Q)Q.minX=N,Q.minY=$,Q.maxX=z,Q.maxY=R;else V.addComponent(J.entityId,"cameraBounds",{minX:N,minY:$,maxX:z,maxY:R})},J.clearBounds=()=>{if(V.getComponent(J.entityId,"cameraBounds"))V.removeComponent(J.entityId,"cameraBounds")},J.addTrauma=(N)=>{let $=V.getComponent(J.entityId,"cameraShake");if($)$.trauma=Math.min(1,Math.max(0,$.trauma+N));else V.addComponent(J.entityId,"cameraShake",{...m(!0),trauma:Math.min(1,Math.max(0,N))})}}),k.addSystem("camera-follow").setPriority(400).inPhase(g).inGroup(q).addQuery("cameras",{with:["camera","cameraFollow"]}).setProcess(({queries:V,dt:P,ecs:N})=>{let $=Math.min(1,P);for(let z of V.cameras){let{camera:R,cameraFollow:Q}=z.components;if(Q.target<0)continue;let G;try{G=N.getComponent(Q.target,"worldTransform")}catch{continue}if(!G)continue;let _=G.x+Q.offsetX,U=G.y+Q.offsetY,M=_-R.x,b=U-R.y;if(Math.abs(M)>Q.deadzoneX){let Z=M>0?1:-1,D=M-Z*Q.deadzoneX,F=Math.min(1,Q.smoothing*$);R.x+=D*F}if(Math.abs(b)>Q.deadzoneY){let Z=b>0?1:-1,D=b-Z*Q.deadzoneY,F=Math.min(1,Q.smoothing*$);R.y+=D*F}}}),k.addSystem("camera-shake-update").setPriority(390).inPhase(g).inGroup(q).addQuery("shakeCameras",{with:["camera","cameraShake"]}).setProcess(({queries:V,dt:P})=>{for(let N of V.shakeCameras){let{cameraShake:$}=N.components;$.trauma=Math.max(0,$.trauma-$.traumaDecay*P)}}),k.addSystem("camera-bounds").setPriority(380).inPhase(g).inGroup(q).addQuery("boundedCameras",{with:["camera","cameraBounds"]}).setProcess(({queries:V})=>{for(let P of V.boundedCameras){let{camera:N,cameraBounds:$}=P.components,z=J.viewportWidth/(2*N.zoom),R=J.viewportHeight/(2*N.zoom),Q=$.minX+z,G=$.maxX-z,_=$.minY+R,U=$.maxY-R;if(Q>G)N.x=($.minX+$.maxX)/2;else N.x=Math.max(Q,Math.min(G,N.x));if(_>U)N.y=($.minY+$.maxY)/2;else N.y=Math.max(_,Math.min(U,N.y))}}),k.addSystem("camera-state-sync").setPriority(370).inPhase(g).inGroup(q).setProcess(({ecs:V})=>{let P=V.getComponent(J.entityId,"camera");if(!P){J.x=0,J.y=0,J.zoom=1,J.rotation=0,J.shakeOffsetX=0,J.shakeOffsetY=0,J.shakeRotation=0;return}J.x=P.x,J.y=P.y,J.zoom=P.zoom,J.rotation=P.rotation;let N=V.getComponent(J.entityId,"cameraShake");if(N&&N.trauma>0){let $=N.trauma*N.trauma;J.shakeOffsetX=N.maxOffsetX*$*(f()*2-1),J.shakeOffsetY=N.maxOffsetY*$*(f()*2-1),J.shakeRotation=N.maxRotation*$*(f()*2-1)}else J.shakeOffsetX=0,J.shakeOffsetY=0,J.shakeRotation=0}),A){let G=function(_){_.preventDefault(),$+=Math.sign(_.deltaY)},{zoomStep:V=0.1,minZoom:P=0.1,maxZoom:N=10}=A,$=0,z=!1,R,Q;k.addSystem("camera-zoom").setPriority(410).inPhase("preUpdate").inGroup(q).addQuery("cameras",{with:["camera"]}).setOnInitialize((_)=>{let U=_.tryGetResource("inputState"),M=_.tryGetResource("pixiApp");if(!U||!M){console.error("[camera] zoom requires the input plugin and renderer2D plugin. Zoom will be disabled.");return}R=M.canvas,R.addEventListener("wheel",G,{passive:!1}),Q=_.tryGetResource("isoProjection"),z=!0}).setOnDetach(()=>{if(!z||!R)return;R.removeEventListener("wheel",G)}).setProcess(({queries:_,ecs:U})=>{if(!z||$===0)return;let M=$;$=0;let[b]=_.cameras;if(!b)return;let Z=b.components.camera,D=U.tryGetResource("inputState");if(!D)return;let F=M>0?1-V:1+V,C=Math.max(P,Math.min(N,Z.zoom*Math.pow(F,Math.abs(M))));if(Q&&R){let H=R.getBoundingClientRect(),L=D.pointer.position.x-(H.left+H.width/2),v=D.pointer.position.y-(H.top+H.height/2),X=Q.tileWidth/2,Y=Q.tileHeight/2,h=(Z.x-Z.y)*X+Q.originX,T=(Z.x+Z.y)*Y+Q.originY,x=h+L/Z.zoom,S=T+v/Z.zoom;Z.zoom=C;let c=x-L/C,n=S-v/C,d=c-Q.originX,l=n-Q.originY;Z.x=d/Q.tileWidth+l/Q.tileHeight,Z.y=-d/Q.tileWidth+l/Q.tileHeight}else{let H=p(D.pointer.position.x,D.pointer.position.y,J);Z.zoom=C,Z.x=H.x-(D.pointer.position.x-J.viewportWidth/2)/C,Z.y=H.y-(D.pointer.position.y-J.viewportHeight/2)/C}})}if(y){let{speed:V,actions:P}=y,N=P?.up??"panUp",$=P?.down??"panDown",z=P?.left??"panLeft",R=P?.right??"panRight",Q=!1;k.addSystem("camera-pan").setPriority(420).inPhase("preUpdate").inGroup(q).setOnInitialize((G)=>{if(!G.tryGetResource("inputState")){console.error("[camera] pan requires the input plugin. Pan will be disabled.");return}Q=!0}).setProcess(({ecs:G,dt:_})=>{if(!Q)return;let U=G.tryGetResource("inputState");if(!U)return;let M=V/J.zoom*_,b=(U.actions.isActive(R)?1:0)-(U.actions.isActive(z)?1:0),Z=(U.actions.isActive($)?1:0)-(U.actions.isActive(N)?1:0);if(b!==0||Z!==0)J.setPosition(J.x+b*M,J.y+Z*M)})}})}import{Graphics as JJ}from"pixi.js";import{definePlugin as NJ}from"ecspresso";function _J(){return{selectable:!0}}function GJ(j){let{systemGroup:I="selection",priority:K=100,phase:B="preUpdate",clickThreshold:O=5,boxFillColor:W=65280,boxFillAlpha:E=0.15,boxStrokeColor:A=65280,boxStrokeAlpha:y=0.8,selectedTint:q=4521796,renderLayer:g}=j??{},f={color:W,alpha:E},k={color:A,width:1.5,alpha:y};return NJ("selection").withComponentTypes().withResourceTypes().withLabels().withGroups().requires().install((J)=>{J.addResource("selectionState",{dragStart:{x:0,y:0},boxEntityId:null});let V=null;J.addSystem("selection-input").setPriority(K).inPhase(B).inGroup(I).addQuery("selectables",{with:["selectable","worldTransform"]}).addQuery("currentlySelected",{with:["selected"]}).withResources(["inputState","selectionState","pixiApp"]).setOnInitialize((P)=>{let N=P.getResource("pixiApp");V=($)=>$.preventDefault(),N.canvas.addEventListener("contextmenu",V)}).setOnDetach((P)=>{if(!V)return;P.getResource("pixiApp").canvas.removeEventListener("contextmenu",V),V=null}).setProcess(({queries:P,ecs:N,resources:$})=>{let{inputState:z,selectionState:R}=$,Q=z.pointer;if(Q.justPressed(0)){if(R.boxEntityId!==null)N.commands.removeEntity(R.boxEntityId);R.dragStart.x=Q.position.x,R.dragStart.y=Q.position.y;let H=N.spawn({graphics:new JJ});if(g)N.addComponent(H.id,"renderLayer",g);R.boxEntityId=H.id}if(Q.isDown(0)&&R.boxEntityId!==null){let H=N.getComponent(R.boxEntityId,"graphics");if(!H)return;let L=R.dragStart.x,v=R.dragStart.y,X=Q.position.x,Y=Q.position.y,h=Math.min(L,X),T=Math.min(v,Y),x=Math.abs(X-L),S=Math.abs(Y-v);H.clear(),H.rect(h,T,x,S),H.fill(f),H.stroke(k)}if(!Q.justReleased(0)||R.boxEntityId===null)return;let G=R.dragStart.x,_=R.dragStart.y,U=Q.position.x,M=Q.position.y,b=Math.abs(U-G),Z=Math.abs(M-_);for(let H of P.currentlySelected)N.removeComponent(H.id,"selected");let D=b<O&&Z<O,F=N.tryGetResource("cameraState"),C=F?p(U,M,F):{x:U,y:M};if(D){let L=null,v=1/0;for(let X of P.selectables){let{worldTransform:Y}=X.components,h=Y.x-C.x,T=Y.y-C.y,x=h*h+T*T;if(x<400&&x<v)v=x,L=X.id}if(L!==null)N.addComponent(L,"selected",!0)}else{let H=F?p(G,_,F):{x:G,y:_},L=Math.min(H.x,C.x),v=Math.max(H.x,C.x),X=Math.min(H.y,C.y),Y=Math.max(H.y,C.y);for(let h of P.selectables){let{worldTransform:T}=h.components;if(T.x>=L&&T.x<=v&&T.y>=X&&T.y<=Y)N.addComponent(h.id,"selected",!0)}}N.commands.removeEntity(R.boxEntityId),R.boxEntityId=null}),J.addSystem("selection-visual").setPriority(K).inPhase("render").inGroup(I).addQuery("selectedUnits",{with:["selected","sprite"]}).setOnEntityEnter("selectedUnits",({entity:P})=>{P.components.sprite.tint=q}).addQuery("deselectedUnits",{with:["selectable","sprite"],without:["selected"]}).setOnEntityEnter("deselectedUnits",({entity:P})=>{P.components.sprite.tint=16777215})})}export{GJ as createSelectionPlugin,_J as createSelectable};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=E108E241311BC76E64756E2164756E21
|
|
4
4
|
//# sourceMappingURL=selection.js.map
|