@aethervtt/shard-input 0.0.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.
- package/LICENSE +21 -0
- package/README.md +3 -0
- package/dist/actions.d.ts +149 -0
- package/dist/actions.js +443 -0
- package/dist/bindings.d.ts +31 -0
- package/dist/bindings.js +107 -0
- package/dist/devices.d.ts +77 -0
- package/dist/devices.js +137 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +62 -0
- package/dist/plugin.d.ts +93 -0
- package/dist/plugin.js +270 -0
- package/package.json +35 -0
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { defineResource, defineSystem, First, ShardError } from "@aethervtt/shard-core";
|
|
2
|
+
import { definePlugin, Time } from "@aethervtt/shard-runtime";
|
|
3
|
+
import {
|
|
4
|
+
GAMEPAD_AXES,
|
|
5
|
+
GAMEPAD_BUTTONS,
|
|
6
|
+
GamepadsState,
|
|
7
|
+
KeyboardState,
|
|
8
|
+
MouseState,
|
|
9
|
+
TouchesState
|
|
10
|
+
} from "./devices.js";
|
|
11
|
+
import * as pluginModule from "./plugin.js";
|
|
12
|
+
const Keyboard = defineResource("input/Keyboard", {
|
|
13
|
+
description: "Keys by KeyboardEvent.code."
|
|
14
|
+
});
|
|
15
|
+
const Mouse = defineResource("input/Mouse", {
|
|
16
|
+
description: "Mouse buttons, position, delta, wheel."
|
|
17
|
+
});
|
|
18
|
+
const Gamepads = defineResource("input/Gamepads", {
|
|
19
|
+
description: "Connected gamepads (standard mapping)."
|
|
20
|
+
});
|
|
21
|
+
const Touches = defineResource("input/Touches", {
|
|
22
|
+
description: "Active touches."
|
|
23
|
+
});
|
|
24
|
+
const InputQueue = defineResource("input/Queue", {
|
|
25
|
+
description: "Raw input buffer: live source, injected events, recording, replay."
|
|
26
|
+
});
|
|
27
|
+
function devices(world) {
|
|
28
|
+
return {
|
|
29
|
+
keyboard: world.resource(Keyboard),
|
|
30
|
+
mouse: world.resource(Mouse),
|
|
31
|
+
gamepads: world.resource(Gamepads),
|
|
32
|
+
touches: world.resource(Touches)
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function apply(world, d, e) {
|
|
36
|
+
switch (e.type) {
|
|
37
|
+
case "key":
|
|
38
|
+
d.keyboard.set(e.code, e.pressed);
|
|
39
|
+
break;
|
|
40
|
+
case "mouse-button":
|
|
41
|
+
d.mouse.set(e.button, e.pressed);
|
|
42
|
+
break;
|
|
43
|
+
case "mouse-move":
|
|
44
|
+
d.mouse.position[0] = e.x;
|
|
45
|
+
d.mouse.position[1] = e.y;
|
|
46
|
+
d.mouse.delta[0] += e.dx;
|
|
47
|
+
d.mouse.delta[1] += e.dy;
|
|
48
|
+
break;
|
|
49
|
+
case "wheel":
|
|
50
|
+
d.mouse.wheel[0] += e.dx;
|
|
51
|
+
d.mouse.wheel[1] += e.dy;
|
|
52
|
+
break;
|
|
53
|
+
case "touch": {
|
|
54
|
+
if (e.phase === "start") {
|
|
55
|
+
d.touches.active.set(e.id, { id: e.id, position: [e.x, e.y], start: [e.x, e.y] });
|
|
56
|
+
d.touches.started.push(e.id);
|
|
57
|
+
} else if (e.phase === "move") {
|
|
58
|
+
const t = d.touches.active.get(e.id);
|
|
59
|
+
if (t) {
|
|
60
|
+
t.position[0] = e.x;
|
|
61
|
+
t.position[1] = e.y;
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
d.touches.active.delete(e.id);
|
|
65
|
+
d.touches.captured.delete(e.id);
|
|
66
|
+
d.touches.ended.push(e.id);
|
|
67
|
+
}
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
case "text":
|
|
71
|
+
d.keyboard.typed += e.text;
|
|
72
|
+
break;
|
|
73
|
+
case "gamepad": {
|
|
74
|
+
const pad = d.gamepads.slot(e.index);
|
|
75
|
+
pad.connected = e.connected;
|
|
76
|
+
for (let i = 0; i < GAMEPAD_BUTTONS.length; i++) {
|
|
77
|
+
const v = e.buttons[i] ?? 0;
|
|
78
|
+
pad.buttonValues[i] = v;
|
|
79
|
+
pad.set(GAMEPAD_BUTTONS[i], v > 0.5);
|
|
80
|
+
}
|
|
81
|
+
for (let i = 0; i < GAMEPAD_AXES.length; i++) {
|
|
82
|
+
const v = e.axes[i] ?? 0;
|
|
83
|
+
pad.axisValues[i] = i % 2 === 1 ? -v : v;
|
|
84
|
+
}
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
case "focus":
|
|
88
|
+
if (!e.focused) {
|
|
89
|
+
d.keyboard.releaseAll();
|
|
90
|
+
d.mouse.releaseAll();
|
|
91
|
+
d.touches.active.clear();
|
|
92
|
+
d.touches.captured.clear();
|
|
93
|
+
}
|
|
94
|
+
break;
|
|
95
|
+
case "action": {
|
|
96
|
+
const dot = e.name.lastIndexOf(".");
|
|
97
|
+
const mapName = e.name.slice(0, dot);
|
|
98
|
+
const map = world.resource(InputQueue).maps.find((m) => m.name === mapName);
|
|
99
|
+
if (!map) {
|
|
100
|
+
throw new ShardError(
|
|
101
|
+
"input/unknown-action",
|
|
102
|
+
`No action map "${mapName}" for injected "${e.name}"`,
|
|
103
|
+
{
|
|
104
|
+
hint: 'Use "<map name>.<action>", e.g. "game/Controls.jump".'
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
map.inject(e.name.slice(dot + 1), e.pressed, e.value);
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const InputContext = defineResource("input/Context", {
|
|
114
|
+
description: "The active input context: action maps of other contexts report nothing. UI sets 'ui' while a node has focus, so gameplay actions don't fire while typing.",
|
|
115
|
+
init: () => ({ active: "game" })
|
|
116
|
+
});
|
|
117
|
+
const updateInput = defineSystem({
|
|
118
|
+
name: "input/update",
|
|
119
|
+
description: "Applies this frame's raw input to devices.",
|
|
120
|
+
run: (_, world) => {
|
|
121
|
+
const q = world.resource(InputQueue);
|
|
122
|
+
const d = devices(world);
|
|
123
|
+
d.keyboard.beginFrame();
|
|
124
|
+
d.mouse.beginFrame();
|
|
125
|
+
d.gamepads.beginFrame();
|
|
126
|
+
d.touches.beginFrame();
|
|
127
|
+
const events = q.scratch;
|
|
128
|
+
events.length = 0;
|
|
129
|
+
if (q.replay) {
|
|
130
|
+
for (const e of q.replay.frames.get(q.replay.frame) ?? []) events.push(e);
|
|
131
|
+
q.replay.frame++;
|
|
132
|
+
q.source?.drain([]);
|
|
133
|
+
if (q.replay.frame >= q.replay.length) q.replay = void 0;
|
|
134
|
+
} else {
|
|
135
|
+
q.source?.drain(events);
|
|
136
|
+
}
|
|
137
|
+
for (const e of q.pending) events.push(e);
|
|
138
|
+
q.pending.length = 0;
|
|
139
|
+
for (const e of events) apply(world, d, e);
|
|
140
|
+
if (q.recording) q.recording.push(events.map((e) => ({ ...e })));
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
const updateActions = defineSystem({
|
|
144
|
+
name: "input/actions",
|
|
145
|
+
description: "Updates action maps from device state, in the active input context.",
|
|
146
|
+
run: (_, world) => {
|
|
147
|
+
const q = world.resource(InputQueue);
|
|
148
|
+
const d = devices(world);
|
|
149
|
+
const context = world.resource(InputContext).active;
|
|
150
|
+
const now = world.resource(Time).elapsed * 1e3;
|
|
151
|
+
for (const map of q.maps) map.update(d, now, context);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
function inputPlugin(options = {}) {
|
|
155
|
+
return definePlugin({
|
|
156
|
+
name: "input",
|
|
157
|
+
provides: [pluginModule],
|
|
158
|
+
dependencies: ["core/time"],
|
|
159
|
+
build(app) {
|
|
160
|
+
const queue = {
|
|
161
|
+
source: options.source,
|
|
162
|
+
pending: [],
|
|
163
|
+
scratch: [],
|
|
164
|
+
maps: [],
|
|
165
|
+
recording: void 0,
|
|
166
|
+
replay: void 0
|
|
167
|
+
};
|
|
168
|
+
app.insertResource(Keyboard, new KeyboardState()).insertResource(Mouse, new MouseState()).insertResource(Gamepads, new GamepadsState()).insertResource(Touches, new TouchesState()).insertResource(InputQueue, queue);
|
|
169
|
+
app.world.initResource(InputContext);
|
|
170
|
+
for (const map of options.actions ?? []) addActions(app.world, map);
|
|
171
|
+
app.addSystems(First, updateInput, updateActions.after(updateInput));
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
function addActions(world, map) {
|
|
176
|
+
const state = map.create();
|
|
177
|
+
world.insertResource(map.resource, state);
|
|
178
|
+
const maps = world.resource(InputQueue).maps;
|
|
179
|
+
const i = maps.findIndex((m) => m.name === map.name);
|
|
180
|
+
if (i === -1) maps.push(state);
|
|
181
|
+
else maps[i] = state;
|
|
182
|
+
return state;
|
|
183
|
+
}
|
|
184
|
+
function rebindAction(world, action, bindings) {
|
|
185
|
+
const dot = action.lastIndexOf(".");
|
|
186
|
+
const mapName = action.slice(0, dot);
|
|
187
|
+
const map = world.resource(InputQueue).maps.find((m) => m.name === mapName);
|
|
188
|
+
if (dot === -1 || !map) {
|
|
189
|
+
throw new ShardError("input/unknown-action", `No action map "${mapName}" for "${action}"`, {
|
|
190
|
+
hint: 'Use "<map name>.<action>", e.g. "game/Controls.jump".'
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
map.rebind(action.slice(dot + 1), bindings);
|
|
194
|
+
}
|
|
195
|
+
function injectInput(world, input) {
|
|
196
|
+
const q = world.resource(InputQueue);
|
|
197
|
+
if ("type" in input) q.pending.push(input);
|
|
198
|
+
else if ("key" in input) q.pending.push({ type: "key", code: input.key, pressed: input.pressed });
|
|
199
|
+
else if ("mouse" in input)
|
|
200
|
+
q.pending.push({ type: "mouse-button", button: input.mouse, pressed: input.pressed });
|
|
201
|
+
else
|
|
202
|
+
q.pending.push({
|
|
203
|
+
type: "action",
|
|
204
|
+
name: input.action,
|
|
205
|
+
pressed: input.pressed,
|
|
206
|
+
value: input.value
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
function startRecording(world) {
|
|
210
|
+
world.resource(InputQueue).recording = [];
|
|
211
|
+
}
|
|
212
|
+
function stopRecording(world) {
|
|
213
|
+
const q = world.resource(InputQueue);
|
|
214
|
+
const frames = q.recording ?? [];
|
|
215
|
+
q.recording = void 0;
|
|
216
|
+
const lines = [JSON.stringify({ frames: frames.length })];
|
|
217
|
+
frames.forEach((events, f) => {
|
|
218
|
+
if (events.length > 0) lines.push(JSON.stringify({ f, e: events }));
|
|
219
|
+
});
|
|
220
|
+
return lines.join("\n");
|
|
221
|
+
}
|
|
222
|
+
function startReplay(world, recording) {
|
|
223
|
+
const [header, ...lines] = recording.split("\n").filter(Boolean);
|
|
224
|
+
const length = JSON.parse(header ?? "{}").frames ?? 0;
|
|
225
|
+
const frames = /* @__PURE__ */ new Map();
|
|
226
|
+
for (const line of lines) {
|
|
227
|
+
const { f, e } = JSON.parse(line);
|
|
228
|
+
frames.set(f, e);
|
|
229
|
+
}
|
|
230
|
+
world.resource(InputQueue).replay = { frames, frame: 0, length };
|
|
231
|
+
}
|
|
232
|
+
function describeInput(world) {
|
|
233
|
+
const q = world.resource(InputQueue);
|
|
234
|
+
return {
|
|
235
|
+
source: q.source ? "live" : "none (headless)",
|
|
236
|
+
recording: q.recording !== void 0,
|
|
237
|
+
replaying: q.replay !== void 0,
|
|
238
|
+
keysHeld: world.resource(Keyboard).held(),
|
|
239
|
+
gamepads: world.resource(Gamepads).connected().length,
|
|
240
|
+
touches: world.resource(Touches).active.size,
|
|
241
|
+
context: world.resource(InputContext).active,
|
|
242
|
+
pointerCaptured: world.resource(Mouse).captured,
|
|
243
|
+
actionMaps: q.maps.map((m) => ({
|
|
244
|
+
name: m.name,
|
|
245
|
+
enabled: m.enabled,
|
|
246
|
+
context: m.context,
|
|
247
|
+
live: m.live,
|
|
248
|
+
values: m.snapshot(),
|
|
249
|
+
rebindings: m.rebindings()
|
|
250
|
+
}))
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
export {
|
|
254
|
+
Gamepads,
|
|
255
|
+
InputContext,
|
|
256
|
+
InputQueue,
|
|
257
|
+
Keyboard,
|
|
258
|
+
Mouse,
|
|
259
|
+
Touches,
|
|
260
|
+
addActions,
|
|
261
|
+
describeInput,
|
|
262
|
+
injectInput,
|
|
263
|
+
inputPlugin,
|
|
264
|
+
rebindAction,
|
|
265
|
+
startRecording,
|
|
266
|
+
startReplay,
|
|
267
|
+
stopRecording,
|
|
268
|
+
updateActions,
|
|
269
|
+
updateInput
|
|
270
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aethervtt/shard-input",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"LICENSE",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@aethervtt/shard-core": "0.0.1",
|
|
20
|
+
"@aethervtt/shard-platform": "0.0.1",
|
|
21
|
+
"@aethervtt/shard-runtime": "0.0.1"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/aethervtt/shard.git",
|
|
26
|
+
"directory": "packages/input"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public",
|
|
30
|
+
"provenance": true
|
|
31
|
+
},
|
|
32
|
+
"shard": {
|
|
33
|
+
"buildSha": "d6c5c6aef4675575c878b02cf2c8f738b4190229"
|
|
34
|
+
}
|
|
35
|
+
}
|