@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.
@@ -0,0 +1,107 @@
1
+ import { ShardError } from "@aethervtt/shard-core";
2
+ import { GAMEPAD_AXES, GAMEPAD_BUTTONS } from "./devices.js";
3
+ const MOUSE_BUTTONS = {
4
+ Left: "left",
5
+ Middle: "middle",
6
+ Right: "right",
7
+ Back: "back",
8
+ Forward: "forward"
9
+ };
10
+ const MOUSE_AXES = ["WheelX", "WheelY", "DeltaX", "DeltaY"];
11
+ const KEY_CODES = /* @__PURE__ */ new Set([
12
+ ..."ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("").map((c) => `Key${c}`),
13
+ ..."0123456789".split("").map((d) => `Digit${d}`),
14
+ ...Array.from({ length: 12 }, (_, i) => `F${i + 1}`),
15
+ ..."0123456789".split("").map((d) => `Numpad${d}`),
16
+ "Space",
17
+ "Enter",
18
+ "Escape",
19
+ "Tab",
20
+ "Backspace",
21
+ "Delete",
22
+ "Insert",
23
+ "Home",
24
+ "End",
25
+ "PageUp",
26
+ "PageDown",
27
+ "ArrowUp",
28
+ "ArrowDown",
29
+ "ArrowLeft",
30
+ "ArrowRight",
31
+ "ShiftLeft",
32
+ "ShiftRight",
33
+ "ControlLeft",
34
+ "ControlRight",
35
+ "AltLeft",
36
+ "AltRight",
37
+ "MetaLeft",
38
+ "MetaRight",
39
+ "CapsLock",
40
+ "Minus",
41
+ "Equal",
42
+ "BracketLeft",
43
+ "BracketRight",
44
+ "Backslash",
45
+ "Semicolon",
46
+ "Quote",
47
+ "Backquote",
48
+ "Comma",
49
+ "Period",
50
+ "Slash",
51
+ "NumpadAdd",
52
+ "NumpadSubtract",
53
+ "NumpadMultiply",
54
+ "NumpadDivide",
55
+ "NumpadEnter",
56
+ "NumpadDecimal"
57
+ ]);
58
+ function unknown(binding, valid) {
59
+ return new ShardError("input/unknown-binding", `Unknown input binding "${binding}"`, {
60
+ hint: `Valid: ${valid.join(", ")}.`,
61
+ path: binding
62
+ });
63
+ }
64
+ function parseBinding(binding) {
65
+ const [device, name] = binding.split(":");
66
+ if (!name) throw unknown(binding, ["Key:<code>", "Mouse:<name>", "Gamepad:<name>", "Touch:Any"]);
67
+ switch (device) {
68
+ case "Key":
69
+ if (!KEY_CODES.has(name))
70
+ throw unknown(binding, [
71
+ "Key:KeyA\u2026KeyZ",
72
+ "Key:Digit0\u20269",
73
+ "Key:Space",
74
+ "Key:ArrowUp",
75
+ "Key:ShiftLeft",
76
+ "\u2026"
77
+ ]);
78
+ return { device: "key", code: name };
79
+ case "Mouse":
80
+ if (name in MOUSE_BUTTONS) return { device: "mouse-button", button: MOUSE_BUTTONS[name] };
81
+ if (MOUSE_AXES.includes(name))
82
+ return { device: "mouse-axis", axis: name };
83
+ throw unknown(
84
+ binding,
85
+ [...Object.keys(MOUSE_BUTTONS), ...MOUSE_AXES].map((n) => `Mouse:${n}`)
86
+ );
87
+ case "Gamepad":
88
+ if (GAMEPAD_BUTTONS.includes(name))
89
+ return { device: "gamepad-button", button: name };
90
+ if (GAMEPAD_AXES.includes(name))
91
+ return { device: "gamepad-axis", axis: name };
92
+ if (name === "LeftStick" || name === "RightStick")
93
+ return { device: "gamepad-stick", stick: name };
94
+ throw unknown(
95
+ binding,
96
+ [...GAMEPAD_BUTTONS, ...GAMEPAD_AXES, "LeftStick", "RightStick"].map((n) => `Gamepad:${n}`)
97
+ );
98
+ case "Touch":
99
+ if (name === "Any") return { device: "touch" };
100
+ throw unknown(binding, ["Touch:Any"]);
101
+ default:
102
+ throw unknown(binding, ["Key:<code>", "Mouse:<name>", "Gamepad:<name>", "Touch:Any"]);
103
+ }
104
+ }
105
+ export {
106
+ parseBinding
107
+ };
@@ -0,0 +1,77 @@
1
+ import type { MouseButton } from '@aethervtt/shard-platform';
2
+ /** Pressed/just-pressed/just-released bookkeeping for a set of named buttons. */
3
+ declare class ButtonSet<K extends string> {
4
+ private readonly down;
5
+ private readonly pressedNow;
6
+ private readonly releasedNow;
7
+ /** @internal start of frame */
8
+ beginFrame(): void;
9
+ /** @internal */
10
+ set(key: K, pressed: boolean): void;
11
+ /** @internal release everything (focus loss) */
12
+ releaseAll(): void;
13
+ pressed(key: K): boolean;
14
+ justPressed(key: K): boolean;
15
+ justReleased(key: K): boolean;
16
+ /** Currently held keys. */
17
+ held(): K[];
18
+ }
19
+ /** Keys by `KeyboardEvent.code` (physical position: `KeyW`, `Space`, `ArrowUp`, `ShiftLeft`). */
20
+ export declare class KeyboardState extends ButtonSet<string> {
21
+ /** Characters typed this frame, key repeats included; "\b" is a backspace. For text fields. */
22
+ typed: string;
23
+ /** @internal */
24
+ beginFrame(): void;
25
+ }
26
+ export declare class MouseState extends ButtonSet<MouseButton> {
27
+ /** Pointer position in canvas pixels. */
28
+ readonly position: [number, number];
29
+ /** Movement this frame, in canvas pixels (works under pointer lock). */
30
+ readonly delta: [number, number];
31
+ /** Wheel movement this frame, in pixels. */
32
+ readonly wheel: [number, number];
33
+ /**
34
+ * The pointer belongs to something drawn over the game (UI) this frame: action maps read mouse
35
+ * buttons and the wheel as released, so clicking a HUD button doesn't also fire.
36
+ */
37
+ captured: boolean;
38
+ /** @internal */
39
+ beginFrame(): void;
40
+ }
41
+ export declare const GAMEPAD_BUTTONS: readonly ['South', 'East', 'West', 'North', 'LeftBumper', 'RightBumper', 'LeftTrigger', 'RightTrigger', 'Select', 'Start', 'LeftStickPress', 'RightStickPress', 'DpadUp', 'DpadDown', 'DpadLeft', 'DpadRight', 'Home'];
42
+ export declare const GAMEPAD_AXES: readonly ['LeftStickX', 'LeftStickY', 'RightStickX', 'RightStickY'];
43
+ export type GamepadButton = (typeof GAMEPAD_BUTTONS)[number];
44
+ export type GamepadAxis = (typeof GAMEPAD_AXES)[number];
45
+ /** One gamepad in the standard mapping. Stick Y is up-positive here (the DOM's is down-positive). */
46
+ export declare class GamepadState extends ButtonSet<GamepadButton> {
47
+ connected: boolean;
48
+ readonly buttonValues: Float32Array<ArrayBuffer>;
49
+ readonly axisValues: Float32Array<ArrayBuffer>;
50
+ button(name: GamepadButton): number;
51
+ axis(name: GamepadAxis): number;
52
+ }
53
+ export declare class GamepadsState {
54
+ private readonly pads;
55
+ get(index: number): GamepadState | undefined;
56
+ /** Connected gamepads. */
57
+ connected(): GamepadState[];
58
+ /** @internal */
59
+ slot(index: number): GamepadState;
60
+ /** @internal */
61
+ beginFrame(): void;
62
+ }
63
+ export interface Touch {
64
+ id: number;
65
+ position: [number, number];
66
+ start: [number, number];
67
+ }
68
+ export declare class TouchesState {
69
+ readonly active: Map<number, Touch>;
70
+ readonly started: number[];
71
+ readonly ended: number[];
72
+ /** Touches that belong to UI: action maps don't see them. */
73
+ readonly captured: Set<number>;
74
+ /** @internal */
75
+ beginFrame(): void;
76
+ }
77
+ export {};
@@ -0,0 +1,137 @@
1
+ class ButtonSet {
2
+ down = /* @__PURE__ */ new Set();
3
+ pressedNow = /* @__PURE__ */ new Set();
4
+ releasedNow = /* @__PURE__ */ new Set();
5
+ /** @internal start of frame */
6
+ beginFrame() {
7
+ this.pressedNow.clear();
8
+ this.releasedNow.clear();
9
+ }
10
+ /** @internal */
11
+ set(key, pressed) {
12
+ if (pressed && !this.down.has(key)) {
13
+ this.down.add(key);
14
+ this.pressedNow.add(key);
15
+ } else if (!pressed && this.down.has(key)) {
16
+ this.down.delete(key);
17
+ this.releasedNow.add(key);
18
+ }
19
+ }
20
+ /** @internal release everything (focus loss) */
21
+ releaseAll() {
22
+ for (const key of this.down) this.releasedNow.add(key);
23
+ this.down.clear();
24
+ }
25
+ pressed(key) {
26
+ return this.down.has(key);
27
+ }
28
+ justPressed(key) {
29
+ return this.pressedNow.has(key);
30
+ }
31
+ justReleased(key) {
32
+ return this.releasedNow.has(key);
33
+ }
34
+ /** Currently held keys. */
35
+ held() {
36
+ return [...this.down];
37
+ }
38
+ }
39
+ class KeyboardState extends ButtonSet {
40
+ /** Characters typed this frame, key repeats included; "\b" is a backspace. For text fields. */
41
+ typed = "";
42
+ /** @internal */
43
+ beginFrame() {
44
+ super.beginFrame();
45
+ this.typed = "";
46
+ }
47
+ }
48
+ class MouseState extends ButtonSet {
49
+ /** Pointer position in canvas pixels. */
50
+ position = [0, 0];
51
+ /** Movement this frame, in canvas pixels (works under pointer lock). */
52
+ delta = [0, 0];
53
+ /** Wheel movement this frame, in pixels. */
54
+ wheel = [0, 0];
55
+ /**
56
+ * The pointer belongs to something drawn over the game (UI) this frame: action maps read mouse
57
+ * buttons and the wheel as released, so clicking a HUD button doesn't also fire.
58
+ */
59
+ captured = false;
60
+ /** @internal */
61
+ beginFrame() {
62
+ super.beginFrame();
63
+ this.delta[0] = this.delta[1] = 0;
64
+ this.wheel[0] = this.wheel[1] = 0;
65
+ }
66
+ }
67
+ const GAMEPAD_BUTTONS = [
68
+ "South",
69
+ "East",
70
+ "West",
71
+ "North",
72
+ "LeftBumper",
73
+ "RightBumper",
74
+ "LeftTrigger",
75
+ "RightTrigger",
76
+ "Select",
77
+ "Start",
78
+ "LeftStickPress",
79
+ "RightStickPress",
80
+ "DpadUp",
81
+ "DpadDown",
82
+ "DpadLeft",
83
+ "DpadRight",
84
+ "Home"
85
+ ];
86
+ const GAMEPAD_AXES = ["LeftStickX", "LeftStickY", "RightStickX", "RightStickY"];
87
+ class GamepadState extends ButtonSet {
88
+ connected = false;
89
+ buttonValues = new Float32Array(GAMEPAD_BUTTONS.length);
90
+ axisValues = new Float32Array(GAMEPAD_AXES.length);
91
+ button(name) {
92
+ return this.buttonValues[GAMEPAD_BUTTONS.indexOf(name)] ?? 0;
93
+ }
94
+ axis(name) {
95
+ return this.axisValues[GAMEPAD_AXES.indexOf(name)] ?? 0;
96
+ }
97
+ }
98
+ class GamepadsState {
99
+ pads = [];
100
+ get(index) {
101
+ return this.pads[index]?.connected ? this.pads[index] : void 0;
102
+ }
103
+ /** Connected gamepads. */
104
+ connected() {
105
+ return this.pads.filter((p) => p.connected);
106
+ }
107
+ /** @internal */
108
+ slot(index) {
109
+ this.pads[index] ??= new GamepadState();
110
+ return this.pads[index];
111
+ }
112
+ /** @internal */
113
+ beginFrame() {
114
+ for (const pad of this.pads) pad?.beginFrame();
115
+ }
116
+ }
117
+ class TouchesState {
118
+ active = /* @__PURE__ */ new Map();
119
+ started = [];
120
+ ended = [];
121
+ /** Touches that belong to UI: action maps don't see them. */
122
+ captured = /* @__PURE__ */ new Set();
123
+ /** @internal */
124
+ beginFrame() {
125
+ this.started.length = 0;
126
+ this.ended.length = 0;
127
+ }
128
+ }
129
+ export {
130
+ GAMEPAD_AXES,
131
+ GAMEPAD_BUTTONS,
132
+ GamepadState,
133
+ GamepadsState,
134
+ KeyboardState,
135
+ MouseState,
136
+ TouchesState
137
+ };
@@ -0,0 +1,4 @@
1
+ export { type ActionDef, type ActionMapDef, ActionState, type Axis1dAction, type Axis2dAction, actionMapSchema, type ButtonAction, defineActions, type Interaction, } from './actions.js';
2
+ export { type ParsedBinding, parseBinding } from './bindings.js';
3
+ export { GAMEPAD_AXES, GAMEPAD_BUTTONS, type GamepadAxis, type GamepadButton, GamepadState, GamepadsState, KeyboardState, MouseState, type Touch, TouchesState, } from './devices.js';
4
+ export { addActions, describeInput, Gamepads, type InjectedInput, InputContext, type InputContextValue, type InputPluginOptions, InputQueue, injectInput, inputPlugin, Keyboard, Mouse, rebindAction, startRecording, startReplay, stopRecording, Touches, updateActions, updateInput, } from './plugin.js';
package/dist/index.js ADDED
@@ -0,0 +1,62 @@
1
+ import {
2
+ ActionState,
3
+ actionMapSchema,
4
+ defineActions
5
+ } from "./actions.js";
6
+ import { parseBinding } from "./bindings.js";
7
+ import {
8
+ GAMEPAD_AXES,
9
+ GAMEPAD_BUTTONS,
10
+ GamepadState,
11
+ GamepadsState,
12
+ KeyboardState,
13
+ MouseState,
14
+ TouchesState
15
+ } from "./devices.js";
16
+ import {
17
+ addActions,
18
+ describeInput,
19
+ Gamepads,
20
+ InputContext,
21
+ InputQueue,
22
+ injectInput,
23
+ inputPlugin,
24
+ Keyboard,
25
+ Mouse,
26
+ rebindAction,
27
+ startRecording,
28
+ startReplay,
29
+ stopRecording,
30
+ Touches,
31
+ updateActions,
32
+ updateInput
33
+ } from "./plugin.js";
34
+ export {
35
+ ActionState,
36
+ GAMEPAD_AXES,
37
+ GAMEPAD_BUTTONS,
38
+ GamepadState,
39
+ Gamepads,
40
+ GamepadsState,
41
+ InputContext,
42
+ InputQueue,
43
+ Keyboard,
44
+ KeyboardState,
45
+ Mouse,
46
+ MouseState,
47
+ Touches,
48
+ TouchesState,
49
+ actionMapSchema,
50
+ addActions,
51
+ defineActions,
52
+ describeInput,
53
+ injectInput,
54
+ inputPlugin,
55
+ parseBinding,
56
+ rebindAction,
57
+ startRecording,
58
+ startReplay,
59
+ stopRecording,
60
+ updateActions,
61
+ updateInput
62
+ };
@@ -0,0 +1,93 @@
1
+ import { type World } from '@aethervtt/shard-core';
2
+ import type { InputSource, RawInputEvent } from '@aethervtt/shard-platform';
3
+ import { type Plugin } from '@aethervtt/shard-runtime';
4
+ import type { ActionMapDef, ActionState } from './actions.js';
5
+ import { GamepadsState, KeyboardState, MouseState, TouchesState } from './devices.js';
6
+ export declare const Keyboard: import("@aethervtt/shard-core").ResourceDef<KeyboardState>;
7
+ export declare const Mouse: import("@aethervtt/shard-core").ResourceDef<MouseState>;
8
+ export declare const Gamepads: import("@aethervtt/shard-core").ResourceDef<GamepadsState>;
9
+ export declare const Touches: import("@aethervtt/shard-core").ResourceDef<TouchesState>;
10
+ interface InputQueueState {
11
+ source: InputSource | undefined;
12
+ /** Injected events, applied at the start of the next frame. */
13
+ pending: RawInputEvent[];
14
+ scratch: RawInputEvent[];
15
+ maps: ActionState<string>[];
16
+ /** Frames recorded so far (events per frame), when recording. */
17
+ recording: RawInputEvent[][] | undefined;
18
+ /** Frames to play back instead of live input, when replaying. */
19
+ replay: {
20
+ frames: Map<number, RawInputEvent[]>;
21
+ frame: number;
22
+ length: number;
23
+ } | undefined;
24
+ }
25
+ export declare const InputQueue: import("@aethervtt/shard-core").ResourceDef<InputQueueState>;
26
+ export interface InputPluginOptions {
27
+ /** Live input (e.g. `createDomInputSource(canvas)`). Omit when headless. */
28
+ source?: InputSource;
29
+ /** Action maps to install as resources. */
30
+ actions?: readonly ActionMapDef<string>[];
31
+ }
32
+ export interface InputContextValue {
33
+ /** Action maps listen only in their own context (`game` by default) or `any`. */
34
+ active: string;
35
+ }
36
+ export declare const InputContext: import("@aethervtt/shard-core").ResourceDef<InputContextValue>;
37
+ /** Drains input once, at the start of the frame, so every system sees the same state. */
38
+ export declare const updateInput: import("@aethervtt/shard-core").SystemDef<undefined>;
39
+ /**
40
+ * Recomputes action maps from device state, after `input/update`. Systems that claim input first
41
+ * (UI capturing the pointer, setting the context) run between the two.
42
+ */
43
+ export declare const updateActions: import("@aethervtt/shard-core").SystemDef<undefined>;
44
+ export declare function inputPlugin(options?: InputPluginOptions): Plugin;
45
+ /** Installs an action map as a resource (also callable after startup). */
46
+ export declare function addActions<K extends string>(world: World, map: ActionMapDef<K>): ActionState<K>;
47
+ /**
48
+ * Rebinds one action by `"<map>.<action>"` (`rebindAction(world, 'game/Controls.jump',
49
+ * ['Key:KeyJ'])`); `undefined` restores the authored bindings. With the save plugin, the engine
50
+ * settings keep rebindings across restarts.
51
+ */
52
+ export declare function rebindAction(world: World, action: string, bindings: readonly unknown[] | undefined): void;
53
+ export type InjectedInput = RawInputEvent | {
54
+ key: string;
55
+ pressed: boolean;
56
+ } | {
57
+ mouse: 'left' | 'middle' | 'right';
58
+ pressed: boolean;
59
+ } | {
60
+ action: string;
61
+ pressed: boolean;
62
+ value?: number;
63
+ };
64
+ /**
65
+ * Queues synthetic input, applied at the start of the next frame through the same path as real
66
+ * input (so it's recorded and replayed too). Actions are named `"<map>.<action>"`.
67
+ */
68
+ export declare function injectInput(world: World, input: InjectedInput): void;
69
+ export declare function startRecording(world: World): void;
70
+ /** Stops recording and returns it as JSON lines: `{"f":<frame>,"e":[...events]}` per non-empty frame. */
71
+ export declare function stopRecording(world: World): string;
72
+ /** Plays a recording back from the next frame on, ignoring live input until it ends. */
73
+ export declare function startReplay(world: World, recording: string): void;
74
+ /** Devices, action maps, and current values. For agents. */
75
+ export declare function describeInput(world: World): {
76
+ source: string;
77
+ recording: boolean;
78
+ replaying: boolean;
79
+ keysHeld: string[];
80
+ gamepads: number;
81
+ touches: number;
82
+ context: string;
83
+ pointerCaptured: boolean;
84
+ actionMaps: {
85
+ name: string;
86
+ enabled: boolean;
87
+ context: string;
88
+ live: boolean;
89
+ values: Record<string, unknown>;
90
+ rebindings: Partial<Record<string, readonly string[] | readonly import("./actions.js").Axis1dBinding[] | readonly import("./actions.js").Axis2dBinding[]>>;
91
+ }[];
92
+ };
93
+ export {};