@diffenderfer-games/hub 2.0.0-next.0 → 2.0.0-next.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/CHANGELOG.md +9 -0
- package/LICENSE +21 -21
- package/README.md +191 -139
- package/dist/doctor/cli.mjs +49 -19
- package/dist/doctor/cli.mjs.map +1 -1
- package/dist/shared/client-version.gen.js +1 -1
- package/dist/shared/daily.js +1 -1
- package/dist/shared/globals.js +8 -3
- package/dist/shared/globals.js.map +1 -1
- package/dist/shared/index.d.ts +1 -1
- package/package.json +89 -89
- package/src/auto.ts +39 -39
- package/src/core/connection.ts +299 -299
- package/src/core/errors.ts +147 -147
- package/src/core/game-data.ts +38 -38
- package/src/core/hub-economy.ts +179 -179
- package/src/core/hub-progress.ts +205 -205
- package/src/core/hub.ts +131 -131
- package/src/core/menu-control.ts +61 -61
- package/src/core/offline/events.ts +50 -50
- package/src/core/offline/favorites.ts +21 -21
- package/src/core/offline/local-store.ts +261 -261
- package/src/core/offline/queue.ts +171 -171
- package/src/core/page.ts +74 -74
- package/src/core/rest/endpoint-types.ts +22 -22
- package/src/core/rest/rest-client.ts +152 -152
- package/src/core/storage/json-guards.ts +74 -74
- package/src/core/storage/key-value.ts +66 -66
- package/src/core/ticket.ts +43 -43
- package/src/core/types.ts +134 -134
- package/src/core/version/client-version.gen.ts +1 -1
- package/src/core/version/hub-version.ts +62 -62
- package/src/core/version/semver.ts +97 -97
- package/src/daily/daily-system.ts +161 -161
- package/src/daily/page-daily.ts +36 -36
- package/src/daily/types.ts +67 -67
- package/src/daily.ts +10 -10
- package/src/doctor/checks.ts +78 -76
- package/src/doctor/cli.ts +98 -106
- package/src/doctor/hub-version-fetch.ts +43 -0
- package/src/doctor/tsconfig.json +12 -12
- package/src/globals.ts +115 -110
- package/src/index.ts +50 -50
- package/src/input/device-state.ts +146 -146
- package/src/input/edges.ts +55 -55
- package/src/input/gamepad.ts +61 -61
- package/src/input/input-events.ts +32 -32
- package/src/input/input-registry.ts +164 -164
- package/src/input/input-system.ts +393 -393
- package/src/input/legacy-controls.ts +64 -64
- package/src/input/nav-state.ts +191 -191
- package/src/input/navigator.ts +171 -171
- package/src/input/on-screen-controls.ts +101 -101
- package/src/input/page-control.ts +43 -43
- package/src/input/page-input.ts +193 -193
- package/src/input/player-remaps.ts +77 -77
- package/src/input/source-values.ts +116 -116
- package/src/input/spatial.ts +47 -47
- package/src/input/types.ts +331 -331
- package/src/input/zero-config.ts +177 -177
- package/src/input.ts +22 -22
- package/src/mp.ts +10 -10
- package/src/overlay/controls-panel.ts +163 -163
- package/src/overlay/dom.ts +46 -46
- package/src/overlay/layout-clusters.ts +162 -162
- package/src/overlay/layout-geometry.ts +136 -136
- package/src/overlay/layout.ts +203 -203
- package/src/overlay/listener-scope.ts +23 -23
- package/src/overlay/on-screen-keyboard.ts +137 -137
- package/src/overlay/overlay.ts +208 -208
- package/src/overlay/styles.ts +9 -9
- package/src/overlay/virtual-controls.ts +262 -262
- package/src/race.ts +10 -10
- package/src/realtime/bridge.ts +121 -121
- package/src/realtime/cross-bundle.ts +29 -29
- package/src/realtime/fallback-notice.ts +48 -48
- package/src/realtime/hub-link.ts +65 -65
- package/src/realtime/latest-state.ts +33 -33
- package/src/realtime/multiplayer-forwarder.ts +28 -28
- package/src/realtime/multiplayer-types.ts +52 -52
- package/src/realtime/page-runtime.ts +60 -60
- package/src/realtime/race-forwarder.ts +59 -59
- package/src/realtime/race-types.ts +132 -132
- package/src/realtime/room-types.ts +125 -125
- package/src/realtime/rooms-forwarder.ts +79 -79
- package/src/realtime/runtime-api.ts +49 -49
- package/src/realtime/runtime-opener.ts +61 -61
- package/src/realtime/social-forwarder.ts +72 -72
- package/src/realtime/social-types.ts +78 -78
- package/src/realtime/types.ts +53 -53
- package/src/realtime/wire-types.ts +72 -72
- package/src/rooms.ts +10 -10
- package/src/social.ts +18 -18
- package/src/types.ts +13 -13
- package/src/uistack/page-stack.ts +45 -45
- package/src/uistack/play-suspension.ts +82 -82
- package/src/uistack/types.ts +76 -76
- package/src/uistack/ui-stack.ts +144 -144
package/src/input/edges.ts
CHANGED
|
@@ -1,55 +1,55 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Edge detection for polled buttons: a plain rising edge, and a rising edge
|
|
3
|
-
* that repeats while held (menu navigation: a first step, a pause, then steady
|
|
4
|
-
* steps).
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
/** Delay before a held direction starts repeating. */
|
|
8
|
-
const FIRST_REPEAT_DELAY_MS = 320;
|
|
9
|
-
/** Interval between repeats while held. */
|
|
10
|
-
const REPEAT_INTERVAL_MS = 130;
|
|
11
|
-
|
|
12
|
-
/** A monotonic clock in ms. */
|
|
13
|
-
export function nowMs(): number {
|
|
14
|
-
return typeof performance === 'undefined' ? Date.now() : performance.now();
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** Rising edges of named polled buttons. */
|
|
18
|
-
export class RisingEdges {
|
|
19
|
-
private readonly wasPressed = new Map<string, boolean>();
|
|
20
|
-
|
|
21
|
-
/** Whether `key` went from released to pressed since the last call for it. */
|
|
22
|
-
rose(key: string, isPressed: boolean): boolean {
|
|
23
|
-
const wasPressedBefore = this.wasPressed.get(key) === true;
|
|
24
|
-
this.wasPressed.set(key, isPressed);
|
|
25
|
-
return isPressed && !wasPressedBefore;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Rising edges that repeat while held. */
|
|
30
|
-
export class RepeatingEdges {
|
|
31
|
-
private readonly timers = new Map<string, { isHeld: boolean; nextAt: number }>();
|
|
32
|
-
|
|
33
|
-
/** Whether `key` fires now: on the press, then after a delay, then steadily while held. */
|
|
34
|
-
fires(key: string, isPressed: boolean, at: number = nowMs()): boolean {
|
|
35
|
-
let timer = this.timers.get(key);
|
|
36
|
-
if (timer === undefined) {
|
|
37
|
-
timer = { isHeld: false, nextAt: 0 };
|
|
38
|
-
this.timers.set(key, timer);
|
|
39
|
-
}
|
|
40
|
-
if (!isPressed) {
|
|
41
|
-
timer.isHeld = false;
|
|
42
|
-
return false;
|
|
43
|
-
}
|
|
44
|
-
if (!timer.isHeld) {
|
|
45
|
-
timer.isHeld = true;
|
|
46
|
-
timer.nextAt = at + FIRST_REPEAT_DELAY_MS;
|
|
47
|
-
return true;
|
|
48
|
-
}
|
|
49
|
-
if (at >= timer.nextAt) {
|
|
50
|
-
timer.nextAt = at + REPEAT_INTERVAL_MS;
|
|
51
|
-
return true;
|
|
52
|
-
}
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Edge detection for polled buttons: a plain rising edge, and a rising edge
|
|
3
|
+
* that repeats while held (menu navigation: a first step, a pause, then steady
|
|
4
|
+
* steps).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Delay before a held direction starts repeating. */
|
|
8
|
+
const FIRST_REPEAT_DELAY_MS = 320;
|
|
9
|
+
/** Interval between repeats while held. */
|
|
10
|
+
const REPEAT_INTERVAL_MS = 130;
|
|
11
|
+
|
|
12
|
+
/** A monotonic clock in ms. */
|
|
13
|
+
export function nowMs(): number {
|
|
14
|
+
return typeof performance === 'undefined' ? Date.now() : performance.now();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Rising edges of named polled buttons. */
|
|
18
|
+
export class RisingEdges {
|
|
19
|
+
private readonly wasPressed = new Map<string, boolean>();
|
|
20
|
+
|
|
21
|
+
/** Whether `key` went from released to pressed since the last call for it. */
|
|
22
|
+
rose(key: string, isPressed: boolean): boolean {
|
|
23
|
+
const wasPressedBefore = this.wasPressed.get(key) === true;
|
|
24
|
+
this.wasPressed.set(key, isPressed);
|
|
25
|
+
return isPressed && !wasPressedBefore;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Rising edges that repeat while held. */
|
|
30
|
+
export class RepeatingEdges {
|
|
31
|
+
private readonly timers = new Map<string, { isHeld: boolean; nextAt: number }>();
|
|
32
|
+
|
|
33
|
+
/** Whether `key` fires now: on the press, then after a delay, then steadily while held. */
|
|
34
|
+
fires(key: string, isPressed: boolean, at: number = nowMs()): boolean {
|
|
35
|
+
let timer = this.timers.get(key);
|
|
36
|
+
if (timer === undefined) {
|
|
37
|
+
timer = { isHeld: false, nextAt: 0 };
|
|
38
|
+
this.timers.set(key, timer);
|
|
39
|
+
}
|
|
40
|
+
if (!isPressed) {
|
|
41
|
+
timer.isHeld = false;
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
if (!timer.isHeld) {
|
|
45
|
+
timer.isHeld = true;
|
|
46
|
+
timer.nextAt = at + FIRST_REPEAT_DELAY_MS;
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
if (at >= timer.nextAt) {
|
|
50
|
+
timer.nextAt = at + REPEAT_INTERVAL_MS;
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/input/gamepad.ts
CHANGED
|
@@ -1,61 +1,61 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reading standard gamepads: button indices, the stick deadzone and the
|
|
3
|
-
* directions a pad is pointing.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/** Standard-gamepad button indices. */
|
|
7
|
-
export const PAD_BUTTON = {
|
|
8
|
-
A: 0, B: 1, X: 2, Y: 3, LB: 4, RB: 5, LT: 6, RT: 7, SELECT: 8, START: 9, UP: 12, DOWN: 13, LEFT: 14, RIGHT: 15,
|
|
9
|
-
} as const;
|
|
10
|
-
|
|
11
|
-
/** Stick deflection below this is ignored. */
|
|
12
|
-
export const AXIS_DEADZONE = 0.35;
|
|
13
|
-
|
|
14
|
-
/** A screen direction. */
|
|
15
|
-
export type Direction = 'up' | 'down' | 'left' | 'right';
|
|
16
|
-
|
|
17
|
-
/** Which directions are held. */
|
|
18
|
-
export type HeldDirections = Record<Direction, boolean>;
|
|
19
|
-
|
|
20
|
-
/** The connected gamepads (the browser's list has holes). */
|
|
21
|
-
export function readGamepads(): Gamepad[] {
|
|
22
|
-
if (typeof navigator === 'undefined' || typeof navigator.getGamepads !== 'function') {
|
|
23
|
-
return [];
|
|
24
|
-
}
|
|
25
|
-
return navigator.getGamepads().filter((pad): pad is Gamepad => pad !== null);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Whether `pad` holds button `index`. */
|
|
29
|
-
export function isPadButtonPressed(pad: Gamepad, index: number): boolean {
|
|
30
|
-
return pad.buttons[index]?.pressed === true;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** Whether any pad holds button `index`. */
|
|
34
|
-
export function isAnyPadButtonPressed(pads: readonly Gamepad[], index: number): boolean {
|
|
35
|
-
return pads.some((pad) => isPadButtonPressed(pad, index));
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** The directions one pad points (left stick past `deadzone`, or the d-pad). */
|
|
39
|
-
export function padDirections(pad: Gamepad, deadzone: number): HeldDirections {
|
|
40
|
-
const horizontal = pad.axes[0] ?? 0;
|
|
41
|
-
const vertical = pad.axes[1] ?? 0;
|
|
42
|
-
return {
|
|
43
|
-
up: vertical < -deadzone || isPadButtonPressed(pad, PAD_BUTTON.UP),
|
|
44
|
-
down: vertical > deadzone || isPadButtonPressed(pad, PAD_BUTTON.DOWN),
|
|
45
|
-
left: horizontal < -deadzone || isPadButtonPressed(pad, PAD_BUTTON.LEFT),
|
|
46
|
-
right: horizontal > deadzone || isPadButtonPressed(pad, PAD_BUTTON.RIGHT),
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** The directions any pad points. */
|
|
51
|
-
export function anyPadDirections(pads: readonly Gamepad[], deadzone: number): HeldDirections {
|
|
52
|
-
const held: HeldDirections = { up: false, down: false, left: false, right: false };
|
|
53
|
-
for (const pad of pads) {
|
|
54
|
-
const directions = padDirections(pad, deadzone);
|
|
55
|
-
held.up ||= directions.up;
|
|
56
|
-
held.down ||= directions.down;
|
|
57
|
-
held.left ||= directions.left;
|
|
58
|
-
held.right ||= directions.right;
|
|
59
|
-
}
|
|
60
|
-
return held;
|
|
61
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Reading standard gamepads: button indices, the stick deadzone and the
|
|
3
|
+
* directions a pad is pointing.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Standard-gamepad button indices. */
|
|
7
|
+
export const PAD_BUTTON = {
|
|
8
|
+
A: 0, B: 1, X: 2, Y: 3, LB: 4, RB: 5, LT: 6, RT: 7, SELECT: 8, START: 9, UP: 12, DOWN: 13, LEFT: 14, RIGHT: 15,
|
|
9
|
+
} as const;
|
|
10
|
+
|
|
11
|
+
/** Stick deflection below this is ignored. */
|
|
12
|
+
export const AXIS_DEADZONE = 0.35;
|
|
13
|
+
|
|
14
|
+
/** A screen direction. */
|
|
15
|
+
export type Direction = 'up' | 'down' | 'left' | 'right';
|
|
16
|
+
|
|
17
|
+
/** Which directions are held. */
|
|
18
|
+
export type HeldDirections = Record<Direction, boolean>;
|
|
19
|
+
|
|
20
|
+
/** The connected gamepads (the browser's list has holes). */
|
|
21
|
+
export function readGamepads(): Gamepad[] {
|
|
22
|
+
if (typeof navigator === 'undefined' || typeof navigator.getGamepads !== 'function') {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
return navigator.getGamepads().filter((pad): pad is Gamepad => pad !== null);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Whether `pad` holds button `index`. */
|
|
29
|
+
export function isPadButtonPressed(pad: Gamepad, index: number): boolean {
|
|
30
|
+
return pad.buttons[index]?.pressed === true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Whether any pad holds button `index`. */
|
|
34
|
+
export function isAnyPadButtonPressed(pads: readonly Gamepad[], index: number): boolean {
|
|
35
|
+
return pads.some((pad) => isPadButtonPressed(pad, index));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The directions one pad points (left stick past `deadzone`, or the d-pad). */
|
|
39
|
+
export function padDirections(pad: Gamepad, deadzone: number): HeldDirections {
|
|
40
|
+
const horizontal = pad.axes[0] ?? 0;
|
|
41
|
+
const vertical = pad.axes[1] ?? 0;
|
|
42
|
+
return {
|
|
43
|
+
up: vertical < -deadzone || isPadButtonPressed(pad, PAD_BUTTON.UP),
|
|
44
|
+
down: vertical > deadzone || isPadButtonPressed(pad, PAD_BUTTON.DOWN),
|
|
45
|
+
left: horizontal < -deadzone || isPadButtonPressed(pad, PAD_BUTTON.LEFT),
|
|
46
|
+
right: horizontal > deadzone || isPadButtonPressed(pad, PAD_BUTTON.RIGHT),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The directions any pad points. */
|
|
51
|
+
export function anyPadDirections(pads: readonly Gamepad[], deadzone: number): HeldDirections {
|
|
52
|
+
const held: HeldDirections = { up: false, down: false, left: false, right: false };
|
|
53
|
+
for (const pad of pads) {
|
|
54
|
+
const directions = padDirections(pad, deadzone);
|
|
55
|
+
held.up ||= directions.up;
|
|
56
|
+
held.down ||= directions.down;
|
|
57
|
+
held.left ||= directions.left;
|
|
58
|
+
held.right ||= directions.right;
|
|
59
|
+
}
|
|
60
|
+
return held;
|
|
61
|
+
}
|
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The input system's events (`hub.input.on`): typed listeners per event,
|
|
3
|
-
* isolated so one failing listener can't stop the frame loop.
|
|
4
|
-
*/
|
|
5
|
-
import type { InputEvents } from './types';
|
|
6
|
-
|
|
7
|
-
/** Listeners by event name. */
|
|
8
|
-
export class InputEventHub {
|
|
9
|
-
private readonly listeners: { [Name in keyof InputEvents]: Set<(payload: InputEvents[Name]) => void> } = {
|
|
10
|
-
focus: new Set(), blur: new Set(), act: new Set(), sourcechange: new Set(), navigate: new Set(),
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
/** Subscribe. Returns the unsubscribe function. */
|
|
14
|
-
on<Name extends keyof InputEvents>(event: Name, listener: (payload: InputEvents[Name]) => void): () => void {
|
|
15
|
-
const registered = this.listeners[event];
|
|
16
|
-
registered.add(listener);
|
|
17
|
-
return () => {
|
|
18
|
-
registered.delete(listener);
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Deliver an event to every listener. */
|
|
23
|
-
emit<Name extends keyof InputEvents>(event: Name, payload: InputEvents[Name]): void {
|
|
24
|
-
for (const listener of [...this.listeners[event]]) {
|
|
25
|
-
try {
|
|
26
|
-
listener(payload);
|
|
27
|
-
} catch {
|
|
28
|
-
// A game's listener failing must not stop the frame loop.
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* The input system's events (`hub.input.on`): typed listeners per event,
|
|
3
|
+
* isolated so one failing listener can't stop the frame loop.
|
|
4
|
+
*/
|
|
5
|
+
import type { InputEvents } from './types';
|
|
6
|
+
|
|
7
|
+
/** Listeners by event name. */
|
|
8
|
+
export class InputEventHub {
|
|
9
|
+
private readonly listeners: { [Name in keyof InputEvents]: Set<(payload: InputEvents[Name]) => void> } = {
|
|
10
|
+
focus: new Set(), blur: new Set(), act: new Set(), sourcechange: new Set(), navigate: new Set(),
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** Subscribe. Returns the unsubscribe function. */
|
|
14
|
+
on<Name extends keyof InputEvents>(event: Name, listener: (payload: InputEvents[Name]) => void): () => void {
|
|
15
|
+
const registered = this.listeners[event];
|
|
16
|
+
registered.add(listener);
|
|
17
|
+
return () => {
|
|
18
|
+
registered.delete(listener);
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Deliver an event to every listener. */
|
|
23
|
+
emit<Name extends keyof InputEvents>(event: Name, payload: InputEvents[Name]): void {
|
|
24
|
+
for (const listener of [...this.listeners[event]]) {
|
|
25
|
+
try {
|
|
26
|
+
listener(payload);
|
|
27
|
+
} catch {
|
|
28
|
+
// A game's listener failing must not stop the frame loop.
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -1,164 +1,164 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The declared inputs, groups and axes, and their readings: each input's raw
|
|
3
|
-
* value this frame and last frame, from which edges, axes and vectors follow.
|
|
4
|
-
*/
|
|
5
|
-
import type { DeviceState } from './device-state';
|
|
6
|
-
import { clampUnit, readRawValue } from './source-values';
|
|
7
|
-
import type { AxisDef, InputConfig, InputDef, InputReading, InputSource, Vector } from './types';
|
|
8
|
-
|
|
9
|
-
/** One input's live state. */
|
|
10
|
-
interface LiveInput {
|
|
11
|
-
definition: InputDef;
|
|
12
|
-
readonly group: string;
|
|
13
|
-
raw: number;
|
|
14
|
-
previous: number;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** Inputs cross this to count as pressed. */
|
|
18
|
-
const PRESS_THRESHOLD = 0.5;
|
|
19
|
-
|
|
20
|
-
/** One side of an axis definition as -1..1. */
|
|
21
|
-
function readAxisSide(side: AxisDef['x'], valueOf: (name: string) => number): number {
|
|
22
|
-
if (side === undefined) {
|
|
23
|
-
return 0;
|
|
24
|
-
}
|
|
25
|
-
if (typeof side === 'string') {
|
|
26
|
-
return valueOf(side);
|
|
27
|
-
}
|
|
28
|
-
return clampUnit(valueOf(side[1])) - clampUnit(valueOf(side[0]));
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** The declared inputs and their readings. */
|
|
32
|
-
export class InputRegistry {
|
|
33
|
-
private readonly inputs = new Map<string, LiveInput>();
|
|
34
|
-
private readonly enabledGroups = new Map<string, boolean>();
|
|
35
|
-
private readonly axes = new Map<string, AxisDef>();
|
|
36
|
-
|
|
37
|
-
/** Replace the declared inputs. Groups keep an enabled state set before; new ones start as declared. */
|
|
38
|
-
declare(config: InputConfig, bindingOf: (name: string, declared: InputDef) => InputDef): void {
|
|
39
|
-
this.inputs.clear();
|
|
40
|
-
this.axes.clear();
|
|
41
|
-
for (const [group, groupDefinition] of Object.entries(config.groups)) {
|
|
42
|
-
if (!this.enabledGroups.has(group)) {
|
|
43
|
-
this.enabledGroups.set(group, groupDefinition.enabled ?? false);
|
|
44
|
-
}
|
|
45
|
-
for (const [name, definition] of Object.entries(groupDefinition.inputs)) {
|
|
46
|
-
this.inputs.set(name, { definition: bindingOf(name, definition), group, raw: 0, previous: 0 });
|
|
47
|
-
}
|
|
48
|
-
for (const [name, axis] of Object.entries(groupDefinition.axes ?? {})) {
|
|
49
|
-
this.axes.set(name, axis);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** The group an input belongs to. */
|
|
55
|
-
groupOf(name: string): string | undefined {
|
|
56
|
-
return this.inputs.get(name)?.group;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** Replace one input's sources (after a rebind). */
|
|
60
|
-
redefine(name: string, definition: InputDef): void {
|
|
61
|
-
const input = this.inputs.get(name);
|
|
62
|
-
if (input !== undefined) {
|
|
63
|
-
input.definition = definition;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** Turn a group on or off. On: held sources don't fire a phantom `down`. Off: its inputs read 0. */
|
|
68
|
-
setGroupEnabled(group: string, isEnabled: boolean, devices: DeviceState, source: InputSource): void {
|
|
69
|
-
this.enabledGroups.set(group, isEnabled);
|
|
70
|
-
for (const input of this.inputs.values()) {
|
|
71
|
-
if (input.group !== group) {
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
input.raw = isEnabled ? readRawValue(devices, input.definition, source) : 0;
|
|
75
|
-
if (isEnabled) {
|
|
76
|
-
input.previous = input.raw;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** Whether a group is on. */
|
|
82
|
-
isEnabled(group: string): boolean {
|
|
83
|
-
return this.enabledGroups.get(group) === true;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** The groups that are on. */
|
|
87
|
-
enabledGroupNames(): Set<string> {
|
|
88
|
-
return new Set([...this.enabledGroups].filter(([, isOn]) => isOn).map(([group]) => group));
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** Take this frame's readings (inputs of disabled groups read 0). */
|
|
92
|
-
sample(devices: DeviceState, source: InputSource): void {
|
|
93
|
-
for (const input of this.inputs.values()) {
|
|
94
|
-
input.previous = input.raw;
|
|
95
|
-
input.raw = this.isEnabled(input.group) ? readRawValue(devices, input.definition, source) : 0;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/** Every input reads 0 this frame (the hub menu is open). */
|
|
100
|
-
silence(): void {
|
|
101
|
-
for (const input of this.inputs.values()) {
|
|
102
|
-
input.previous = input.raw;
|
|
103
|
-
input.raw = 0;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** An input's value, 0..1. */
|
|
108
|
-
value(name: string): number {
|
|
109
|
-
return this.inputs.get(name)?.raw ?? 0;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/** Whether an input crossed the press threshold upward this frame. */
|
|
113
|
-
wentDown(name: string): boolean {
|
|
114
|
-
const input = this.inputs.get(name);
|
|
115
|
-
return input !== undefined && input.previous < PRESS_THRESHOLD && input.raw >= PRESS_THRESHOLD;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** Whether an input crossed the press threshold downward this frame. */
|
|
119
|
-
wentUp(name: string): boolean {
|
|
120
|
-
const input = this.inputs.get(name);
|
|
121
|
-
return input !== undefined && input.previous >= PRESS_THRESHOLD && input.raw < PRESS_THRESHOLD;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/** Whether an input is held. */
|
|
125
|
-
isPressed(name: string): boolean {
|
|
126
|
-
return this.value(name) >= PRESS_THRESHOLD;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** An input's full reading. */
|
|
130
|
-
reading(name: string): InputReading {
|
|
131
|
-
const raw = this.value(name);
|
|
132
|
-
return { value: raw, raw, isDown: this.wentDown(name), isUp: this.wentUp(name) };
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/** A named axis (or an ad-hoc negative/positive pair) as -1..1. */
|
|
136
|
-
axis(name: string, positive?: string): number {
|
|
137
|
-
if (positive !== undefined) {
|
|
138
|
-
return clampUnit(this.value(positive)) - clampUnit(this.value(name));
|
|
139
|
-
}
|
|
140
|
-
const axis = this.axes.get(name);
|
|
141
|
-
return axis === undefined ? this.value(name) : readAxisSide(axis.x, (input) => this.value(input));
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/** A named axis pair as a vector clamped to the unit circle. */
|
|
145
|
-
vector(name: string): Vector {
|
|
146
|
-
const axis = this.axes.get(name);
|
|
147
|
-
if (axis === undefined) {
|
|
148
|
-
return { x: 0, y: 0, mag: 0 };
|
|
149
|
-
}
|
|
150
|
-
let horizontal = readAxisSide(axis.x, (input) => this.value(input));
|
|
151
|
-
let vertical = readAxisSide(axis.y, (input) => this.value(input));
|
|
152
|
-
const magnitude = Math.hypot(horizontal, vertical);
|
|
153
|
-
if (magnitude > 1) {
|
|
154
|
-
horizontal /= magnitude;
|
|
155
|
-
vertical /= magnitude;
|
|
156
|
-
}
|
|
157
|
-
return { x: horizontal, y: vertical, mag: Math.min(magnitude, 1) };
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/** An input's current sources (for the binding label). */
|
|
161
|
-
definitionOf(name: string): InputDef | undefined {
|
|
162
|
-
return this.inputs.get(name)?.definition;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* The declared inputs, groups and axes, and their readings: each input's raw
|
|
3
|
+
* value this frame and last frame, from which edges, axes and vectors follow.
|
|
4
|
+
*/
|
|
5
|
+
import type { DeviceState } from './device-state';
|
|
6
|
+
import { clampUnit, readRawValue } from './source-values';
|
|
7
|
+
import type { AxisDef, InputConfig, InputDef, InputReading, InputSource, Vector } from './types';
|
|
8
|
+
|
|
9
|
+
/** One input's live state. */
|
|
10
|
+
interface LiveInput {
|
|
11
|
+
definition: InputDef;
|
|
12
|
+
readonly group: string;
|
|
13
|
+
raw: number;
|
|
14
|
+
previous: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Inputs cross this to count as pressed. */
|
|
18
|
+
const PRESS_THRESHOLD = 0.5;
|
|
19
|
+
|
|
20
|
+
/** One side of an axis definition as -1..1. */
|
|
21
|
+
function readAxisSide(side: AxisDef['x'], valueOf: (name: string) => number): number {
|
|
22
|
+
if (side === undefined) {
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
if (typeof side === 'string') {
|
|
26
|
+
return valueOf(side);
|
|
27
|
+
}
|
|
28
|
+
return clampUnit(valueOf(side[1])) - clampUnit(valueOf(side[0]));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The declared inputs and their readings. */
|
|
32
|
+
export class InputRegistry {
|
|
33
|
+
private readonly inputs = new Map<string, LiveInput>();
|
|
34
|
+
private readonly enabledGroups = new Map<string, boolean>();
|
|
35
|
+
private readonly axes = new Map<string, AxisDef>();
|
|
36
|
+
|
|
37
|
+
/** Replace the declared inputs. Groups keep an enabled state set before; new ones start as declared. */
|
|
38
|
+
declare(config: InputConfig, bindingOf: (name: string, declared: InputDef) => InputDef): void {
|
|
39
|
+
this.inputs.clear();
|
|
40
|
+
this.axes.clear();
|
|
41
|
+
for (const [group, groupDefinition] of Object.entries(config.groups)) {
|
|
42
|
+
if (!this.enabledGroups.has(group)) {
|
|
43
|
+
this.enabledGroups.set(group, groupDefinition.enabled ?? false);
|
|
44
|
+
}
|
|
45
|
+
for (const [name, definition] of Object.entries(groupDefinition.inputs)) {
|
|
46
|
+
this.inputs.set(name, { definition: bindingOf(name, definition), group, raw: 0, previous: 0 });
|
|
47
|
+
}
|
|
48
|
+
for (const [name, axis] of Object.entries(groupDefinition.axes ?? {})) {
|
|
49
|
+
this.axes.set(name, axis);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The group an input belongs to. */
|
|
55
|
+
groupOf(name: string): string | undefined {
|
|
56
|
+
return this.inputs.get(name)?.group;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Replace one input's sources (after a rebind). */
|
|
60
|
+
redefine(name: string, definition: InputDef): void {
|
|
61
|
+
const input = this.inputs.get(name);
|
|
62
|
+
if (input !== undefined) {
|
|
63
|
+
input.definition = definition;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Turn a group on or off. On: held sources don't fire a phantom `down`. Off: its inputs read 0. */
|
|
68
|
+
setGroupEnabled(group: string, isEnabled: boolean, devices: DeviceState, source: InputSource): void {
|
|
69
|
+
this.enabledGroups.set(group, isEnabled);
|
|
70
|
+
for (const input of this.inputs.values()) {
|
|
71
|
+
if (input.group !== group) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
input.raw = isEnabled ? readRawValue(devices, input.definition, source) : 0;
|
|
75
|
+
if (isEnabled) {
|
|
76
|
+
input.previous = input.raw;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Whether a group is on. */
|
|
82
|
+
isEnabled(group: string): boolean {
|
|
83
|
+
return this.enabledGroups.get(group) === true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The groups that are on. */
|
|
87
|
+
enabledGroupNames(): Set<string> {
|
|
88
|
+
return new Set([...this.enabledGroups].filter(([, isOn]) => isOn).map(([group]) => group));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Take this frame's readings (inputs of disabled groups read 0). */
|
|
92
|
+
sample(devices: DeviceState, source: InputSource): void {
|
|
93
|
+
for (const input of this.inputs.values()) {
|
|
94
|
+
input.previous = input.raw;
|
|
95
|
+
input.raw = this.isEnabled(input.group) ? readRawValue(devices, input.definition, source) : 0;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Every input reads 0 this frame (the hub menu is open). */
|
|
100
|
+
silence(): void {
|
|
101
|
+
for (const input of this.inputs.values()) {
|
|
102
|
+
input.previous = input.raw;
|
|
103
|
+
input.raw = 0;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** An input's value, 0..1. */
|
|
108
|
+
value(name: string): number {
|
|
109
|
+
return this.inputs.get(name)?.raw ?? 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Whether an input crossed the press threshold upward this frame. */
|
|
113
|
+
wentDown(name: string): boolean {
|
|
114
|
+
const input = this.inputs.get(name);
|
|
115
|
+
return input !== undefined && input.previous < PRESS_THRESHOLD && input.raw >= PRESS_THRESHOLD;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Whether an input crossed the press threshold downward this frame. */
|
|
119
|
+
wentUp(name: string): boolean {
|
|
120
|
+
const input = this.inputs.get(name);
|
|
121
|
+
return input !== undefined && input.previous >= PRESS_THRESHOLD && input.raw < PRESS_THRESHOLD;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Whether an input is held. */
|
|
125
|
+
isPressed(name: string): boolean {
|
|
126
|
+
return this.value(name) >= PRESS_THRESHOLD;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** An input's full reading. */
|
|
130
|
+
reading(name: string): InputReading {
|
|
131
|
+
const raw = this.value(name);
|
|
132
|
+
return { value: raw, raw, isDown: this.wentDown(name), isUp: this.wentUp(name) };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** A named axis (or an ad-hoc negative/positive pair) as -1..1. */
|
|
136
|
+
axis(name: string, positive?: string): number {
|
|
137
|
+
if (positive !== undefined) {
|
|
138
|
+
return clampUnit(this.value(positive)) - clampUnit(this.value(name));
|
|
139
|
+
}
|
|
140
|
+
const axis = this.axes.get(name);
|
|
141
|
+
return axis === undefined ? this.value(name) : readAxisSide(axis.x, (input) => this.value(input));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** A named axis pair as a vector clamped to the unit circle. */
|
|
145
|
+
vector(name: string): Vector {
|
|
146
|
+
const axis = this.axes.get(name);
|
|
147
|
+
if (axis === undefined) {
|
|
148
|
+
return { x: 0, y: 0, mag: 0 };
|
|
149
|
+
}
|
|
150
|
+
let horizontal = readAxisSide(axis.x, (input) => this.value(input));
|
|
151
|
+
let vertical = readAxisSide(axis.y, (input) => this.value(input));
|
|
152
|
+
const magnitude = Math.hypot(horizontal, vertical);
|
|
153
|
+
if (magnitude > 1) {
|
|
154
|
+
horizontal /= magnitude;
|
|
155
|
+
vertical /= magnitude;
|
|
156
|
+
}
|
|
157
|
+
return { x: horizontal, y: vertical, mag: Math.min(magnitude, 1) };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** An input's current sources (for the binding label). */
|
|
161
|
+
definitionOf(name: string): InputDef | undefined {
|
|
162
|
+
return this.inputs.get(name)?.definition;
|
|
163
|
+
}
|
|
164
|
+
}
|