@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.
Files changed (154) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/LICENSE +21 -21
  3. package/README.md +206 -139
  4. package/dist/auto.d.ts +6 -6
  5. package/dist/auto.js +2 -2
  6. package/dist/doctor/cli.mjs +62 -25
  7. package/dist/doctor/cli.mjs.map +1 -1
  8. package/dist/index.d.ts +6 -6
  9. package/dist/index.js +2 -2
  10. package/dist/mp.d.ts +3 -3
  11. package/dist/race.d.ts +1 -1
  12. package/dist/rooms.d.ts +3 -3
  13. package/dist/shared/bridge.js +9 -3
  14. package/dist/shared/bridge.js.map +1 -1
  15. package/dist/shared/client-version.gen.js +1 -1
  16. package/dist/shared/daily.d.ts +10 -4
  17. package/dist/shared/daily.d.ts.map +1 -1
  18. package/dist/shared/daily.js +12 -2
  19. package/dist/shared/daily.js.map +1 -1
  20. package/dist/shared/globals.js +8 -3
  21. package/dist/shared/globals.js.map +1 -1
  22. package/dist/shared/index.d.ts +314 -35
  23. package/dist/shared/index.d.ts.map +1 -1
  24. package/dist/shared/input.js +7 -2
  25. package/dist/shared/input.js.map +1 -1
  26. package/dist/shared/latest-state.js +1 -1
  27. package/dist/shared/mp.js +10 -2
  28. package/dist/shared/mp.js.map +1 -1
  29. package/dist/shared/multiplayer-types.d.ts +15 -2
  30. package/dist/shared/multiplayer-types.d.ts.map +1 -1
  31. package/dist/shared/race-types.d.ts +1 -1
  32. package/dist/shared/race.js +1 -1
  33. package/dist/shared/room-types.d.ts +28 -3
  34. package/dist/shared/room-types.d.ts.map +1 -1
  35. package/dist/shared/rooms.d.ts.map +1 -1
  36. package/dist/shared/rooms.js +10 -2
  37. package/dist/shared/rooms.js.map +1 -1
  38. package/dist/shared/runtime-opener.js +1 -1
  39. package/dist/shared/social.d.ts +23 -8
  40. package/dist/shared/social.d.ts.map +1 -1
  41. package/dist/shared/social.js +6 -1
  42. package/dist/shared/social.js.map +1 -1
  43. package/dist/shared/src.js +1588 -388
  44. package/dist/shared/src.js.map +1 -1
  45. package/dist/shared/wire-types.d.ts +6 -2
  46. package/dist/shared/wire-types.d.ts.map +1 -1
  47. package/dist/social.d.ts +5 -5
  48. package/dist/social.js +1 -1
  49. package/package.json +89 -89
  50. package/src/auto.ts +39 -39
  51. package/src/core/connection.ts +394 -299
  52. package/src/core/errors.ts +147 -147
  53. package/src/core/game-data.ts +40 -38
  54. package/src/core/hub-economy.ts +216 -179
  55. package/src/core/hub-progress.ts +202 -205
  56. package/src/core/hub.ts +133 -131
  57. package/src/core/menu-control.ts +61 -61
  58. package/src/core/offline/events.ts +50 -50
  59. package/src/core/offline/favorites.ts +21 -21
  60. package/src/core/offline/hub-kinds/daily-kind.ts +68 -0
  61. package/src/core/offline/hub-kinds/economy-kinds.ts +86 -0
  62. package/src/core/offline/hub-kinds/game-fields.ts +2 -0
  63. package/src/core/offline/hub-kinds/index.ts +35 -0
  64. package/src/core/offline/hub-kinds/legacy-ops.ts +37 -0
  65. package/src/core/offline/hub-kinds/progress-kinds.ts +148 -0
  66. package/src/core/offline/kind-adapters.ts +48 -0
  67. package/src/core/offline/local-inventory.ts +64 -0
  68. package/src/core/offline/local-store.ts +259 -261
  69. package/src/core/offline/offline-api.ts +65 -0
  70. package/src/core/offline/outbox-entry.ts +59 -0
  71. package/src/core/offline/outbox-kinds.ts +78 -0
  72. package/src/core/offline/outbox-merge.ts +39 -0
  73. package/src/core/offline/outbox-replayer.ts +141 -0
  74. package/src/core/offline/outbox-store.ts +85 -0
  75. package/src/core/offline/outbox.ts +232 -0
  76. package/src/core/offline/queue.ts +126 -171
  77. package/src/core/offline/replay-lease.ts +87 -0
  78. package/src/core/offline/replay-policy.ts +52 -0
  79. package/src/core/page.ts +74 -74
  80. package/src/core/rest/endpoint-types.ts +22 -22
  81. package/src/core/rest/rest-client.ts +163 -152
  82. package/src/core/rest/routes.gen.ts +108 -87
  83. package/src/core/storage/json-guards.ts +80 -74
  84. package/src/core/storage/key-value.ts +66 -66
  85. package/src/core/ticket.ts +43 -43
  86. package/src/core/types.ts +134 -134
  87. package/src/core/version/client-version.gen.ts +1 -1
  88. package/src/core/version/hub-version.ts +62 -62
  89. package/src/core/version/semver.ts +97 -97
  90. package/src/daily/daily-system.ts +168 -161
  91. package/src/daily/page-daily.ts +36 -36
  92. package/src/daily/types.ts +68 -67
  93. package/src/daily.ts +10 -10
  94. package/src/doctor/checks.ts +86 -76
  95. package/src/doctor/cli.ts +98 -106
  96. package/src/doctor/hub-version-fetch.ts +43 -0
  97. package/src/doctor/tsconfig.json +12 -12
  98. package/src/globals.ts +115 -110
  99. package/src/index.ts +55 -50
  100. package/src/input/device-state.ts +146 -146
  101. package/src/input/edges.ts +55 -55
  102. package/src/input/gamepad.ts +61 -61
  103. package/src/input/input-events.ts +32 -32
  104. package/src/input/input-registry.ts +171 -164
  105. package/src/input/input-system.ts +393 -393
  106. package/src/input/legacy-controls.ts +64 -64
  107. package/src/input/nav-state.ts +191 -191
  108. package/src/input/navigator.ts +171 -171
  109. package/src/input/on-screen-controls.ts +101 -101
  110. package/src/input/page-control.ts +43 -43
  111. package/src/input/page-input.ts +193 -193
  112. package/src/input/player-remaps.ts +77 -77
  113. package/src/input/source-values.ts +116 -116
  114. package/src/input/spatial.ts +47 -47
  115. package/src/input/types.ts +331 -331
  116. package/src/input/zero-config.ts +177 -177
  117. package/src/input.ts +22 -22
  118. package/src/mp.ts +10 -10
  119. package/src/overlay/controls-panel.ts +163 -163
  120. package/src/overlay/dom.ts +46 -46
  121. package/src/overlay/layout-clusters.ts +162 -162
  122. package/src/overlay/layout-geometry.ts +136 -136
  123. package/src/overlay/layout.ts +203 -203
  124. package/src/overlay/listener-scope.ts +23 -23
  125. package/src/overlay/on-screen-keyboard.ts +137 -137
  126. package/src/overlay/overlay.ts +208 -208
  127. package/src/overlay/styles.ts +9 -9
  128. package/src/overlay/virtual-controls.ts +262 -262
  129. package/src/race.ts +10 -10
  130. package/src/realtime/bridge.ts +126 -121
  131. package/src/realtime/cross-bundle.ts +29 -29
  132. package/src/realtime/fallback-notice.ts +48 -48
  133. package/src/realtime/hub-link.ts +65 -65
  134. package/src/realtime/latest-state.ts +33 -33
  135. package/src/realtime/multiplayer-forwarder.ts +40 -28
  136. package/src/realtime/multiplayer-types.ts +66 -52
  137. package/src/realtime/page-runtime.ts +60 -60
  138. package/src/realtime/race-forwarder.ts +59 -59
  139. package/src/realtime/race-types.ts +132 -132
  140. package/src/realtime/room-types.ts +151 -125
  141. package/src/realtime/rooms-forwarder.ts +91 -79
  142. package/src/realtime/runtime-api.ts +60 -49
  143. package/src/realtime/runtime-opener.ts +61 -61
  144. package/src/realtime/social-forwarder.ts +77 -72
  145. package/src/realtime/social-types.ts +86 -78
  146. package/src/realtime/types.ts +53 -53
  147. package/src/realtime/wire-types.ts +79 -72
  148. package/src/rooms.ts +10 -10
  149. package/src/social.ts +18 -18
  150. package/src/types.ts +13 -13
  151. package/src/uistack/page-stack.ts +45 -45
  152. package/src/uistack/play-suspension.ts +82 -82
  153. package/src/uistack/types.ts +76 -76
  154. 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
+ }
@@ -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
+ }
@@ -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
+ }