@eva/plugin-input-action 2.1.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,163 @@
1
+ import { Component, decorators } from '@eva/eva.js';
2
+ import { getSignalBus } from '@eva/plugin-signal-bus';
3
+ import type { InputActionMapParams, ActionBinding } from './types';
4
+
5
+ interface ActionState {
6
+ binding: ActionBinding;
7
+ pressed: boolean;
8
+ }
9
+
10
+ /**
11
+ * InputActionMap 组件 — 把原始 input 抽象成语义化 action 并 emit 信号。
12
+ *
13
+ * DSL 用法:
14
+ * ```json
15
+ * {
16
+ * "type": "InputActionMap",
17
+ * "props": {
18
+ * "bindings": [
19
+ * { "action": "fire", "sources": [{ "type": "click" }, { "type": "key", "code": "Space" }] },
20
+ * { "action": "left", "sources": [{ "type": "key", "code": "ArrowLeft" }] }
21
+ * ]
22
+ * }
23
+ * }
24
+ * ```
25
+ *
26
+ * 信号:
27
+ * - 默认 emit `input:fire:press` / `input:fire:release` / `input:fire:hold`(每帧)
28
+ * - 可在 binding 显式指定 pressSignal / releaseSignal / holdSignal 覆盖
29
+ *
30
+ * 设计原则:Component 不直接修改游戏状态,只 emit 语义化信号。
31
+ */
32
+ @decorators.componentObserver({})
33
+ export class InputActionMap extends Component<InputActionMapParams> {
34
+ static componentName = 'InputActionMap';
35
+
36
+ private bindings: ActionBinding[] = [];
37
+ private states = new Map<string, ActionState>();
38
+ private root?: HTMLElement | Document;
39
+ // 注意:不能命名 `listeners`,会与 EventEmitter 基类的 listeners(event) 方法签名冲突
40
+ private domListeners: Array<() => void> = [];
41
+ private pressedKeys = new Set<string>();
42
+ private pressedButtons = new Set<number>();
43
+ private touchActive = false;
44
+
45
+ init(params?: InputActionMapParams) {
46
+ if (!params) return;
47
+ this.bindings = params.bindings ?? [];
48
+ for (const b of this.bindings) {
49
+ this.states.set(b.action, { binding: b, pressed: false });
50
+ }
51
+ if (typeof document === 'undefined') return;
52
+ this.root = params.rootSelector
53
+ ? (document.querySelector(params.rootSelector) as HTMLElement) || document
54
+ : document;
55
+ this.bind();
56
+ }
57
+
58
+ private bind() {
59
+ if (!this.root) return;
60
+ const onKD = (e: KeyboardEvent) => {
61
+ if (e.repeat) return;
62
+ this.pressedKeys.add(e.code);
63
+ this.evaluate('press', { type: 'key', code: e.code });
64
+ };
65
+ const onKU = (e: KeyboardEvent) => {
66
+ this.pressedKeys.delete(e.code);
67
+ this.evaluate('release', { type: 'key', code: e.code });
68
+ };
69
+ const onMD = (e: MouseEvent) => {
70
+ this.pressedButtons.add(e.button);
71
+ this.evaluate('press', { type: 'mouse', button: e.button });
72
+ this.evaluate('press', { type: 'click' });
73
+ };
74
+ const onMU = (e: MouseEvent) => {
75
+ this.pressedButtons.delete(e.button);
76
+ this.evaluate('release', { type: 'mouse', button: e.button });
77
+ this.evaluate('release', { type: 'click' });
78
+ };
79
+ const onTS = () => {
80
+ this.touchActive = true;
81
+ this.evaluate('press', { type: 'touch' });
82
+ this.evaluate('press', { type: 'click' });
83
+ };
84
+ const onTE = () => {
85
+ this.touchActive = false;
86
+ this.evaluate('release', { type: 'touch' });
87
+ this.evaluate('release', { type: 'click' });
88
+ };
89
+ const r = this.root as any;
90
+ r.addEventListener('keydown', onKD);
91
+ r.addEventListener('keyup', onKU);
92
+ r.addEventListener('mousedown', onMD);
93
+ r.addEventListener('mouseup', onMU);
94
+ r.addEventListener('touchstart', onTS);
95
+ r.addEventListener('touchend', onTE);
96
+ this.domListeners = [
97
+ () => r.removeEventListener('keydown', onKD),
98
+ () => r.removeEventListener('keyup', onKU),
99
+ () => r.removeEventListener('mousedown', onMD),
100
+ () => r.removeEventListener('mouseup', onMU),
101
+ () => r.removeEventListener('touchstart', onTS),
102
+ () => r.removeEventListener('touchend', onTE),
103
+ ];
104
+ }
105
+
106
+ private evaluate(phase: 'press' | 'release', src: any) {
107
+ const bus = getSignalBus();
108
+ for (const [name, state] of this.states) {
109
+ if (!state.binding.sources.some((s) => this.sourceMatches(s, src))) continue;
110
+ if (phase === 'press' && !state.pressed) {
111
+ state.pressed = true;
112
+ const sig = state.binding.pressSignal ?? `input:${name}:press`;
113
+ bus.emit(sig, { action: name });
114
+ } else if (phase === 'release' && state.pressed) {
115
+ // 检查是否所有源都已 release
116
+ if (this.anySourceActive(state.binding)) continue;
117
+ state.pressed = false;
118
+ const sig = state.binding.releaseSignal ?? `input:${name}:release`;
119
+ bus.emit(sig, { action: name });
120
+ }
121
+ }
122
+ }
123
+
124
+ private sourceMatches(s: any, e: any): boolean {
125
+ if (s.type !== e.type) return false;
126
+ if (s.type === 'key') return s.code === e.code;
127
+ if (s.type === 'mouse') return s.button == null || s.button === e.button;
128
+ return true;
129
+ }
130
+
131
+ private anySourceActive(b: ActionBinding): boolean {
132
+ for (const s of b.sources) {
133
+ if (s.type === 'key' && this.pressedKeys.has(s.code)) return true;
134
+ if (s.type === 'mouse' && (s.button == null || this.pressedButtons.has(s.button))) return true;
135
+ if (s.type === 'touch' && this.touchActive) return true;
136
+ if (s.type === 'click' && (this.touchActive || this.pressedButtons.size > 0)) return true;
137
+ }
138
+ return false;
139
+ }
140
+
141
+ /** 每帧 emit hold 信号,允许移动型 action 用 update 读 */
142
+ update() {
143
+ const bus = getSignalBus();
144
+ for (const [name, state] of this.states) {
145
+ if (!state.pressed) continue;
146
+ const sig = state.binding.holdSignal ?? `input:${name}:hold`;
147
+ bus.emit(sig, { action: name });
148
+ }
149
+ }
150
+
151
+ /** 查询某 action 当前是否按下(给非信号场景用) */
152
+ isPressed(action: string): boolean {
153
+ return !!this.states.get(action)?.pressed;
154
+ }
155
+
156
+ onDestroy() {
157
+ for (const off of this.domListeners) off();
158
+ this.domListeners = [];
159
+ this.states.clear();
160
+ this.pressedKeys.clear();
161
+ this.pressedButtons.clear();
162
+ }
163
+ }
@@ -0,0 +1,6 @@
1
+ import { System } from '@eva/eva.js';
2
+
3
+ export class InputActionSystem extends System {
4
+ static systemName = 'InputAction';
5
+ readonly name = 'InputAction';
6
+ }
package/lib/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { InputActionMap } from './InputActionMap';
2
+ export { InputActionSystem } from './InputActionSystem';
3
+ export type { InputActionMapParams, ActionBinding, InputSource } from './types';
package/lib/types.ts ADDED
@@ -0,0 +1,24 @@
1
+ /** 输入源 */
2
+ export type InputSource =
3
+ | { type: 'key'; code: string } // 例如 "Space" / "ArrowLeft"
4
+ | { type: 'mouse'; button?: number } // 0=left
5
+ | { type: 'touch' } // 任意触屏按下
6
+ | { type: 'click' }; // 任意点击(touch+mouse 合并)
7
+
8
+ /** action 配置 */
9
+ export interface ActionBinding {
10
+ /** action 名称,emit 信号时使用 (例如 "fire" / "jump") */
11
+ action: string;
12
+ /** 至少一个输入源触发 */
13
+ sources: InputSource[];
14
+ /** 触发哪些信号 (默认 emit `input:{action}`) */
15
+ pressSignal?: string;
16
+ releaseSignal?: string;
17
+ holdSignal?: string;
18
+ }
19
+
20
+ export interface InputActionMapParams {
21
+ bindings?: ActionBinding[];
22
+ /** 监听的根元素;不传走 document */
23
+ rootSelector?: string;
24
+ }
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@eva/plugin-input-action",
3
+ "version": "2.1.0-beta.1",
4
+ "description": "Action map — DSL 把 keyboard/touch/click 映射为语义化 action,emit 到 SignalBus,游戏逻辑只关心 action。",
5
+ "main": "lib/index.ts",
6
+ "module": "lib/index.ts",
7
+ "types": "lib/index.ts",
8
+ "files": [
9
+ "lib"
10
+ ],
11
+ "keywords": [
12
+ "eva.js",
13
+ "plugin",
14
+ "input",
15
+ "action-map"
16
+ ],
17
+ "license": "MIT",
18
+ "dependencies": {
19
+ "@eva/eva.js": "2.1.0-beta.1",
20
+ "@eva/plugin-signal-bus": "2.1.0-beta.1"
21
+ }
22
+ }