@diffenderfer-games/hub 2.0.0-next.0 → 2.0.0-next.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/CHANGELOG.md +36 -0
- package/LICENSE +21 -21
- package/README.md +206 -139
- package/dist/auto.d.ts +6 -6
- package/dist/auto.js +2 -2
- package/dist/doctor/cli.mjs +62 -25
- package/dist/doctor/cli.mjs.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.js +2 -2
- package/dist/mp.d.ts +3 -3
- package/dist/race.d.ts +1 -1
- package/dist/rooms.d.ts +3 -3
- package/dist/shared/bridge.js +9 -3
- package/dist/shared/bridge.js.map +1 -1
- package/dist/shared/client-version.gen.js +1 -1
- package/dist/shared/daily.d.ts +10 -4
- package/dist/shared/daily.d.ts.map +1 -1
- package/dist/shared/daily.js +12 -2
- package/dist/shared/daily.js.map +1 -1
- package/dist/shared/globals.js +8 -3
- package/dist/shared/globals.js.map +1 -1
- package/dist/shared/index.d.ts +314 -35
- package/dist/shared/index.d.ts.map +1 -1
- package/dist/shared/input.js +7 -2
- package/dist/shared/input.js.map +1 -1
- package/dist/shared/latest-state.js +1 -1
- package/dist/shared/mp.js +10 -2
- package/dist/shared/mp.js.map +1 -1
- package/dist/shared/multiplayer-types.d.ts +15 -2
- package/dist/shared/multiplayer-types.d.ts.map +1 -1
- package/dist/shared/race-types.d.ts +1 -1
- package/dist/shared/race.js +1 -1
- package/dist/shared/room-types.d.ts +28 -3
- package/dist/shared/room-types.d.ts.map +1 -1
- package/dist/shared/rooms.d.ts.map +1 -1
- package/dist/shared/rooms.js +10 -2
- package/dist/shared/rooms.js.map +1 -1
- package/dist/shared/runtime-opener.js +1 -1
- package/dist/shared/social.d.ts +23 -8
- package/dist/shared/social.d.ts.map +1 -1
- package/dist/shared/social.js +6 -1
- package/dist/shared/social.js.map +1 -1
- package/dist/shared/src.js +1588 -388
- package/dist/shared/src.js.map +1 -1
- package/dist/shared/wire-types.d.ts +6 -2
- package/dist/shared/wire-types.d.ts.map +1 -1
- package/dist/social.d.ts +5 -5
- package/dist/social.js +1 -1
- package/package.json +89 -89
- package/src/auto.ts +39 -39
- package/src/core/connection.ts +394 -299
- package/src/core/errors.ts +147 -147
- package/src/core/game-data.ts +40 -38
- package/src/core/hub-economy.ts +216 -179
- package/src/core/hub-progress.ts +202 -205
- package/src/core/hub.ts +133 -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/hub-kinds/daily-kind.ts +68 -0
- package/src/core/offline/hub-kinds/economy-kinds.ts +86 -0
- package/src/core/offline/hub-kinds/game-fields.ts +2 -0
- package/src/core/offline/hub-kinds/index.ts +35 -0
- package/src/core/offline/hub-kinds/legacy-ops.ts +37 -0
- package/src/core/offline/hub-kinds/progress-kinds.ts +148 -0
- package/src/core/offline/kind-adapters.ts +48 -0
- package/src/core/offline/local-inventory.ts +64 -0
- package/src/core/offline/local-store.ts +259 -261
- package/src/core/offline/offline-api.ts +65 -0
- package/src/core/offline/outbox-entry.ts +59 -0
- package/src/core/offline/outbox-kinds.ts +78 -0
- package/src/core/offline/outbox-merge.ts +39 -0
- package/src/core/offline/outbox-replayer.ts +141 -0
- package/src/core/offline/outbox-store.ts +85 -0
- package/src/core/offline/outbox.ts +232 -0
- package/src/core/offline/queue.ts +126 -171
- package/src/core/offline/replay-lease.ts +87 -0
- package/src/core/offline/replay-policy.ts +52 -0
- package/src/core/page.ts +74 -74
- package/src/core/rest/endpoint-types.ts +22 -22
- package/src/core/rest/rest-client.ts +163 -152
- package/src/core/rest/routes.gen.ts +108 -87
- package/src/core/storage/json-guards.ts +80 -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 +168 -161
- package/src/daily/page-daily.ts +36 -36
- package/src/daily/types.ts +68 -67
- package/src/daily.ts +10 -10
- package/src/doctor/checks.ts +86 -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 +55 -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 +171 -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 +126 -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 +40 -28
- package/src/realtime/multiplayer-types.ts +66 -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 +151 -125
- package/src/realtime/rooms-forwarder.ts +91 -79
- package/src/realtime/runtime-api.ts +60 -49
- package/src/realtime/runtime-opener.ts +61 -61
- package/src/realtime/social-forwarder.ts +77 -72
- package/src/realtime/social-types.ts +86 -78
- package/src/realtime/types.ts +53 -53
- package/src/realtime/wire-types.ts +79 -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
|
@@ -1,146 +1,146 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The raw state of every input device, updated by DOM events and sampled once
|
|
3
|
-
* per frame: held keys and mouse buttons, the pointer, movement and wheel
|
|
4
|
-
* accumulators, one-frame pulses, on-screen sticks and buttons, and gamepads.
|
|
5
|
-
*/
|
|
6
|
-
import { AXIS_DEADZONE, readGamepads } from './gamepad';
|
|
7
|
-
|
|
8
|
-
/** An on-screen stick's output, -1..1 per axis. */
|
|
9
|
-
export interface StickOutput {
|
|
10
|
-
outX: number;
|
|
11
|
-
outY: number;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/** A 2D accumulator. */
|
|
15
|
-
interface Delta {
|
|
16
|
-
dx: number;
|
|
17
|
-
dy: number;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** Keys that scroll the page; cancelled during play (not while typing). */
|
|
21
|
-
const SCROLL_KEYS = new Set([' ', 'arrowup', 'arrowdown', 'arrowleft', 'arrowright']);
|
|
22
|
-
|
|
23
|
-
/** Input types that don't take text (buttons, checkboxes, sliders…). */
|
|
24
|
-
const NON_TEXT_INPUT_TYPES = new Set(['button', 'submit', 'reset', 'checkbox', 'radio', 'range', 'color', 'file', 'image']);
|
|
25
|
-
|
|
26
|
-
/** Whether typing in `target` owns Space and the arrow keys. */
|
|
27
|
-
export function isTypingTarget(target: EventTarget | null): boolean {
|
|
28
|
-
if (!(target instanceof HTMLElement)) {
|
|
29
|
-
return false;
|
|
30
|
-
}
|
|
31
|
-
if (target.isContentEditable || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement) {
|
|
32
|
-
return true;
|
|
33
|
-
}
|
|
34
|
-
return target instanceof HTMLInputElement && !NON_TEXT_INPUT_TYPES.has(target.type);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** What the device state reports back to the input system. */
|
|
38
|
-
export interface DeviceStateEvents {
|
|
39
|
-
/** The mouse moved or clicked: it is the active pointer. */
|
|
40
|
-
onMouseActivity(): void;
|
|
41
|
-
/** A gamepad connected (`true`) or disconnected (`false`). */
|
|
42
|
-
onGamepadConnection(isConnected: boolean): void;
|
|
43
|
-
/** Whether keydown should cancel scrolling keys (a game defined its inputs). */
|
|
44
|
-
shouldCancelScrollKeys(): boolean;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Raw device state for one page. */
|
|
48
|
-
export class DeviceState {
|
|
49
|
-
readonly heldKeys = new Set<string>();
|
|
50
|
-
readonly heldMouseButtons = new Set<number>();
|
|
51
|
-
readonly pressedVirtualButtons = new Set<string>();
|
|
52
|
-
readonly sticks = new Map<string, StickOutput>();
|
|
53
|
-
pointerX = 0;
|
|
54
|
-
pointerY = 0;
|
|
55
|
-
readonly movement: Delta = { dx: 0, dy: 0 };
|
|
56
|
-
readonly wheel: Delta = { dx: 0, dy: 0 };
|
|
57
|
-
gamepads: Gamepad[] = [];
|
|
58
|
-
frame = 0;
|
|
59
|
-
private readonly pulseFrames = new Map<string, number>();
|
|
60
|
-
|
|
61
|
-
/** Fire a one-frame pulse (`tap`, `mouse:dbl`). */
|
|
62
|
-
pulse(name: string): void {
|
|
63
|
-
this.pulseFrames.set(name, this.frame);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** Whether a pulse fired this frame or the last. */
|
|
67
|
-
isPulseActive(name: string): boolean {
|
|
68
|
-
const firedAt = this.pulseFrames.get(name);
|
|
69
|
-
return firedAt === this.frame || firedAt === this.frame - 1;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** Poll the gamepads. Returns whether any are connected and whether any moved. */
|
|
73
|
-
pollGamepads(): { isConnected: boolean; hasActivity: boolean } {
|
|
74
|
-
this.gamepads = readGamepads();
|
|
75
|
-
const hasActivity = this.gamepads.some((pad) => pad.buttons.some((button) => button.pressed || button.value > 0.5)
|
|
76
|
-
|| pad.axes.some((axis) => Math.abs(axis) > AXIS_DEADZONE));
|
|
77
|
-
return { isConnected: this.gamepads.length > 0, hasActivity };
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** End-of-frame decay of the relative accumulators. */
|
|
81
|
-
decay(): void {
|
|
82
|
-
this.movement.dx = 0;
|
|
83
|
-
this.movement.dy = 0;
|
|
84
|
-
this.wheel.dx *= 0.6;
|
|
85
|
-
this.wheel.dy *= 0.6;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** Release everything held (focus loss, hidden tab), so nothing sticks. */
|
|
89
|
-
releaseAll(): void {
|
|
90
|
-
this.heldKeys.clear();
|
|
91
|
-
this.heldMouseButtons.clear();
|
|
92
|
-
this.pressedVirtualButtons.clear();
|
|
93
|
-
for (const stick of this.sticks.values()) {
|
|
94
|
-
stick.outX = 0;
|
|
95
|
-
stick.outY = 0;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/** Wire the DOM listeners that feed this state. */
|
|
100
|
-
listen(events: DeviceStateEvents): void {
|
|
101
|
-
if (typeof window === 'undefined') {
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
window.addEventListener('keydown', (event) => {
|
|
105
|
-
const key = event.key.toLowerCase();
|
|
106
|
-
this.heldKeys.add(key);
|
|
107
|
-
if (events.shouldCancelScrollKeys() && SCROLL_KEYS.has(key) && !isTypingTarget(event.target)) {
|
|
108
|
-
event.preventDefault();
|
|
109
|
-
}
|
|
110
|
-
});
|
|
111
|
-
window.addEventListener('keyup', (event) => this.heldKeys.delete(event.key.toLowerCase()));
|
|
112
|
-
window.addEventListener('mousemove', (event) => {
|
|
113
|
-
this.movement.dx += event.movementX;
|
|
114
|
-
this.movement.dy += event.movementY;
|
|
115
|
-
this.pointerX = event.clientX;
|
|
116
|
-
this.pointerY = event.clientY;
|
|
117
|
-
events.onMouseActivity();
|
|
118
|
-
});
|
|
119
|
-
window.addEventListener('mousedown', (event) => {
|
|
120
|
-
this.heldMouseButtons.add(event.button);
|
|
121
|
-
events.onMouseActivity();
|
|
122
|
-
});
|
|
123
|
-
window.addEventListener('mouseup', (event) => this.heldMouseButtons.delete(event.button));
|
|
124
|
-
window.addEventListener('dblclick', () => {
|
|
125
|
-
this.pulse('mouse:dbl');
|
|
126
|
-
});
|
|
127
|
-
window.addEventListener('wheel', (event) => {
|
|
128
|
-
this.wheel.dx += Math.sign(event.deltaX);
|
|
129
|
-
this.wheel.dy += Math.sign(event.deltaY);
|
|
130
|
-
}, { passive: true });
|
|
131
|
-
window.addEventListener('blur', () => {
|
|
132
|
-
this.releaseAll();
|
|
133
|
-
});
|
|
134
|
-
document.addEventListener('visibilitychange', () => {
|
|
135
|
-
if (document.hidden) {
|
|
136
|
-
this.releaseAll();
|
|
137
|
-
}
|
|
138
|
-
});
|
|
139
|
-
window.addEventListener('gamepadconnected', () => {
|
|
140
|
-
events.onGamepadConnection(true);
|
|
141
|
-
});
|
|
142
|
-
window.addEventListener('gamepaddisconnected', () => {
|
|
143
|
-
events.onGamepadConnection(false);
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* The raw state of every input device, updated by DOM events and sampled once
|
|
3
|
+
* per frame: held keys and mouse buttons, the pointer, movement and wheel
|
|
4
|
+
* accumulators, one-frame pulses, on-screen sticks and buttons, and gamepads.
|
|
5
|
+
*/
|
|
6
|
+
import { AXIS_DEADZONE, readGamepads } from './gamepad';
|
|
7
|
+
|
|
8
|
+
/** An on-screen stick's output, -1..1 per axis. */
|
|
9
|
+
export interface StickOutput {
|
|
10
|
+
outX: number;
|
|
11
|
+
outY: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** A 2D accumulator. */
|
|
15
|
+
interface Delta {
|
|
16
|
+
dx: number;
|
|
17
|
+
dy: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Keys that scroll the page; cancelled during play (not while typing). */
|
|
21
|
+
const SCROLL_KEYS = new Set([' ', 'arrowup', 'arrowdown', 'arrowleft', 'arrowright']);
|
|
22
|
+
|
|
23
|
+
/** Input types that don't take text (buttons, checkboxes, sliders…). */
|
|
24
|
+
const NON_TEXT_INPUT_TYPES = new Set(['button', 'submit', 'reset', 'checkbox', 'radio', 'range', 'color', 'file', 'image']);
|
|
25
|
+
|
|
26
|
+
/** Whether typing in `target` owns Space and the arrow keys. */
|
|
27
|
+
export function isTypingTarget(target: EventTarget | null): boolean {
|
|
28
|
+
if (!(target instanceof HTMLElement)) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
if (target.isContentEditable || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
return target instanceof HTMLInputElement && !NON_TEXT_INPUT_TYPES.has(target.type);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** What the device state reports back to the input system. */
|
|
38
|
+
export interface DeviceStateEvents {
|
|
39
|
+
/** The mouse moved or clicked: it is the active pointer. */
|
|
40
|
+
onMouseActivity(): void;
|
|
41
|
+
/** A gamepad connected (`true`) or disconnected (`false`). */
|
|
42
|
+
onGamepadConnection(isConnected: boolean): void;
|
|
43
|
+
/** Whether keydown should cancel scrolling keys (a game defined its inputs). */
|
|
44
|
+
shouldCancelScrollKeys(): boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Raw device state for one page. */
|
|
48
|
+
export class DeviceState {
|
|
49
|
+
readonly heldKeys = new Set<string>();
|
|
50
|
+
readonly heldMouseButtons = new Set<number>();
|
|
51
|
+
readonly pressedVirtualButtons = new Set<string>();
|
|
52
|
+
readonly sticks = new Map<string, StickOutput>();
|
|
53
|
+
pointerX = 0;
|
|
54
|
+
pointerY = 0;
|
|
55
|
+
readonly movement: Delta = { dx: 0, dy: 0 };
|
|
56
|
+
readonly wheel: Delta = { dx: 0, dy: 0 };
|
|
57
|
+
gamepads: Gamepad[] = [];
|
|
58
|
+
frame = 0;
|
|
59
|
+
private readonly pulseFrames = new Map<string, number>();
|
|
60
|
+
|
|
61
|
+
/** Fire a one-frame pulse (`tap`, `mouse:dbl`). */
|
|
62
|
+
pulse(name: string): void {
|
|
63
|
+
this.pulseFrames.set(name, this.frame);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Whether a pulse fired this frame or the last. */
|
|
67
|
+
isPulseActive(name: string): boolean {
|
|
68
|
+
const firedAt = this.pulseFrames.get(name);
|
|
69
|
+
return firedAt === this.frame || firedAt === this.frame - 1;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Poll the gamepads. Returns whether any are connected and whether any moved. */
|
|
73
|
+
pollGamepads(): { isConnected: boolean; hasActivity: boolean } {
|
|
74
|
+
this.gamepads = readGamepads();
|
|
75
|
+
const hasActivity = this.gamepads.some((pad) => pad.buttons.some((button) => button.pressed || button.value > 0.5)
|
|
76
|
+
|| pad.axes.some((axis) => Math.abs(axis) > AXIS_DEADZONE));
|
|
77
|
+
return { isConnected: this.gamepads.length > 0, hasActivity };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** End-of-frame decay of the relative accumulators. */
|
|
81
|
+
decay(): void {
|
|
82
|
+
this.movement.dx = 0;
|
|
83
|
+
this.movement.dy = 0;
|
|
84
|
+
this.wheel.dx *= 0.6;
|
|
85
|
+
this.wheel.dy *= 0.6;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Release everything held (focus loss, hidden tab), so nothing sticks. */
|
|
89
|
+
releaseAll(): void {
|
|
90
|
+
this.heldKeys.clear();
|
|
91
|
+
this.heldMouseButtons.clear();
|
|
92
|
+
this.pressedVirtualButtons.clear();
|
|
93
|
+
for (const stick of this.sticks.values()) {
|
|
94
|
+
stick.outX = 0;
|
|
95
|
+
stick.outY = 0;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Wire the DOM listeners that feed this state. */
|
|
100
|
+
listen(events: DeviceStateEvents): void {
|
|
101
|
+
if (typeof window === 'undefined') {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
window.addEventListener('keydown', (event) => {
|
|
105
|
+
const key = event.key.toLowerCase();
|
|
106
|
+
this.heldKeys.add(key);
|
|
107
|
+
if (events.shouldCancelScrollKeys() && SCROLL_KEYS.has(key) && !isTypingTarget(event.target)) {
|
|
108
|
+
event.preventDefault();
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
window.addEventListener('keyup', (event) => this.heldKeys.delete(event.key.toLowerCase()));
|
|
112
|
+
window.addEventListener('mousemove', (event) => {
|
|
113
|
+
this.movement.dx += event.movementX;
|
|
114
|
+
this.movement.dy += event.movementY;
|
|
115
|
+
this.pointerX = event.clientX;
|
|
116
|
+
this.pointerY = event.clientY;
|
|
117
|
+
events.onMouseActivity();
|
|
118
|
+
});
|
|
119
|
+
window.addEventListener('mousedown', (event) => {
|
|
120
|
+
this.heldMouseButtons.add(event.button);
|
|
121
|
+
events.onMouseActivity();
|
|
122
|
+
});
|
|
123
|
+
window.addEventListener('mouseup', (event) => this.heldMouseButtons.delete(event.button));
|
|
124
|
+
window.addEventListener('dblclick', () => {
|
|
125
|
+
this.pulse('mouse:dbl');
|
|
126
|
+
});
|
|
127
|
+
window.addEventListener('wheel', (event) => {
|
|
128
|
+
this.wheel.dx += Math.sign(event.deltaX);
|
|
129
|
+
this.wheel.dy += Math.sign(event.deltaY);
|
|
130
|
+
}, { passive: true });
|
|
131
|
+
window.addEventListener('blur', () => {
|
|
132
|
+
this.releaseAll();
|
|
133
|
+
});
|
|
134
|
+
document.addEventListener('visibilitychange', () => {
|
|
135
|
+
if (document.hidden) {
|
|
136
|
+
this.releaseAll();
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
window.addEventListener('gamepadconnected', () => {
|
|
140
|
+
events.onGamepadConnection(true);
|
|
141
|
+
});
|
|
142
|
+
window.addEventListener('gamepaddisconnected', () => {
|
|
143
|
+
events.onGamepadConnection(false);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
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
|
+
}
|