@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/navigator.ts
CHANGED
|
@@ -1,171 +1,171 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Gamepad navigation the input system drives each frame: the hub menu while
|
|
3
|
-
* it is open, a game's own menus opted in with `autoNavigate`, and the
|
|
4
|
-
* navigable groups a game declares.
|
|
5
|
-
*/
|
|
6
|
-
import type { RepeatingEdges, RisingEdges } from './edges';
|
|
7
|
-
import { anyPadDirections, AXIS_DEADZONE, isAnyPadButtonPressed, PAD_BUTTON } from './gamepad';
|
|
8
|
-
import type { Direction } from './gamepad';
|
|
9
|
-
import { NavState } from './nav-state';
|
|
10
|
-
import type { NavHighlighter, NavListeners } from './nav-state';
|
|
11
|
-
import type { MenuBridge, NavigableDef, NavTarget } from './types';
|
|
12
|
-
|
|
13
|
-
/** What the navigator reads from the input system. */
|
|
14
|
-
export interface NavigatorContext {
|
|
15
|
-
readonly gamepads: readonly Gamepad[];
|
|
16
|
-
readonly repeats: RepeatingEdges;
|
|
17
|
-
readonly presses: RisingEdges;
|
|
18
|
-
readonly listeners: NavListeners;
|
|
19
|
-
/** The overlay, created on demand. */
|
|
20
|
-
highlighter(): NavHighlighter;
|
|
21
|
-
/** Whether an input is held (≥ 0.5). */
|
|
22
|
-
isPressed(inputName: string): boolean;
|
|
23
|
-
/** Whether an input went down this frame. */
|
|
24
|
-
wentDown(inputName: string): boolean;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const DIRECTIONS: readonly Direction[] = ['up', 'down', 'left', 'right'];
|
|
28
|
-
|
|
29
|
-
/** The visible focusables of the hub menu. */
|
|
30
|
-
function menuFocusables(root: ShadowRoot | null): HTMLElement[] {
|
|
31
|
-
if (root === null) {
|
|
32
|
-
return [];
|
|
33
|
-
}
|
|
34
|
-
return [...root.querySelectorAll<HTMLElement>('a[href], button:not([disabled]), input, [tabindex]')]
|
|
35
|
-
.filter((element) => element.getClientRects().length > 0);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Visible elements matching a CSS selector. */
|
|
39
|
-
function visibleMatches(selector: string): HTMLElement[] {
|
|
40
|
-
return [...document.querySelectorAll<HTMLElement>(selector)].filter((element) => element.getClientRects().length > 0);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** A navigable group with its highlight state. */
|
|
44
|
-
interface GroupNavigation {
|
|
45
|
-
readonly definition: NavigableDef;
|
|
46
|
-
readonly state: NavState;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** The menu, auto-navigate and group navigation of one input system. */
|
|
50
|
-
export class Navigator {
|
|
51
|
-
private menuState: NavState | null = null;
|
|
52
|
-
private autoNavigateQuery: (() => NavTarget[]) | null = null;
|
|
53
|
-
private autoNavigateState: NavState | null = null;
|
|
54
|
-
private autoNavigateBack: (() => void) | null = null;
|
|
55
|
-
private readonly groups = new Map<string, GroupNavigation>();
|
|
56
|
-
|
|
57
|
-
/** Forget the declared groups (a new `define`). */
|
|
58
|
-
clearGroups(): void {
|
|
59
|
-
this.groups.clear();
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** Declare a navigable group. */
|
|
63
|
-
addGroup(group: string, definition: NavigableDef): void {
|
|
64
|
-
this.groups.set(group, { definition, state: new NavState(definition.elements, definition.wrap !== false) });
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** A group was enabled: highlight its first element. */
|
|
68
|
-
refreshGroup(group: string, context: NavigatorContext): void {
|
|
69
|
-
this.groups.get(group)?.state.requery(context.highlighter(), context.listeners);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** A group was disabled: drop its highlight. */
|
|
73
|
-
blurGroup(group: string, highlighter: NavHighlighter | null): void {
|
|
74
|
-
this.groups.get(group)?.state.blur(highlighter);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** Opt a game's own menus into gamepad navigation. */
|
|
78
|
-
setAutoNavigate(query: () => NavTarget[], back: (() => void) | null): void {
|
|
79
|
-
this.autoNavigateQuery = query;
|
|
80
|
-
this.autoNavigateBack = back;
|
|
81
|
-
this.autoNavigateState = null;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** Make a CSS selector's visible matches the auto-navigate targets. */
|
|
85
|
-
static queryFor(selector: string | (() => NavTarget[])): () => NavTarget[] {
|
|
86
|
-
return typeof selector === 'string' ? () => visibleMatches(selector) : selector;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** Stop auto-navigating. */
|
|
90
|
-
clearAutoNavigate(highlighter: NavHighlighter | null): void {
|
|
91
|
-
this.autoNavigateQuery = null;
|
|
92
|
-
this.autoNavigateState?.blur(highlighter);
|
|
93
|
-
this.autoNavigateState = null;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** The hub menu is open: drive it with the highlight ring (gamepad only). */
|
|
97
|
-
driveMenu(menu: MenuBridge, isGamepadActive: boolean, context: NavigatorContext, highlighter: NavHighlighter | null): void {
|
|
98
|
-
if (!isGamepadActive) {
|
|
99
|
-
highlighter?.hideHighlight();
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
this.menuState ??= new NavState(() => menuFocusables(menu.root));
|
|
103
|
-
this.driveList(this.menuState, 'm', context, () => {
|
|
104
|
-
menu.close();
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** The hub menu closed: drop its highlight. */
|
|
109
|
-
leaveMenu(highlighter: NavHighlighter | null): void {
|
|
110
|
-
this.menuState?.blur(highlighter);
|
|
111
|
-
this.menuState = null;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/** A game's own menus, when opted in, visible, and a gamepad is in use. */
|
|
115
|
-
driveAutoNavigate(isGamepadActive: boolean, context: NavigatorContext, highlighter: NavHighlighter | null): void {
|
|
116
|
-
const query = this.autoNavigateQuery;
|
|
117
|
-
if (query === null || !isGamepadActive || query().length === 0) {
|
|
118
|
-
this.autoNavigateState?.blur(highlighter);
|
|
119
|
-
this.autoNavigateState = null;
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
this.autoNavigateState ??= new NavState(query);
|
|
123
|
-
this.driveList(this.autoNavigateState, 'an', context, this.autoNavigateBack);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** The declared navigable groups that are enabled. */
|
|
127
|
-
driveGroups(isGroupEnabled: (group: string) => boolean, context: NavigatorContext): void {
|
|
128
|
-
for (const [group, { definition, state }] of this.groups) {
|
|
129
|
-
if (!isGroupEnabled(group)) {
|
|
130
|
-
continue;
|
|
131
|
-
}
|
|
132
|
-
const highlighter = context.highlighter();
|
|
133
|
-
const fires = (prefix: string, inputName: string | undefined): boolean => inputName !== undefined && context.repeats.fires(prefix + group, context.isPressed(inputName));
|
|
134
|
-
const shouldStepPrevious = fires('np:', definition.prev);
|
|
135
|
-
const shouldStepNext = fires('nn:', definition.next);
|
|
136
|
-
const shouldMoveUp = fires('nu:', definition.up);
|
|
137
|
-
const shouldMoveDown = fires('nd:', definition.down);
|
|
138
|
-
if (shouldStepNext) {
|
|
139
|
-
state.step(1, highlighter, context.listeners);
|
|
140
|
-
} else if (shouldStepPrevious) {
|
|
141
|
-
state.step(-1, highlighter, context.listeners);
|
|
142
|
-
} else if (shouldMoveUp) {
|
|
143
|
-
state.moveInDirection('up', highlighter, context.listeners);
|
|
144
|
-
} else if (shouldMoveDown) {
|
|
145
|
-
state.moveInDirection('down', highlighter, context.listeners);
|
|
146
|
-
}
|
|
147
|
-
if (definition.act !== '' && context.wentDown(definition.act)) {
|
|
148
|
-
state.act(highlighter, context.listeners);
|
|
149
|
-
}
|
|
150
|
-
state.position(highlighter);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/** Move with the stick or d-pad, A acts, B goes back. */
|
|
155
|
-
private driveList(state: NavState, keyPrefix: string, context: NavigatorContext, back: (() => void) | null): void {
|
|
156
|
-
const highlighter = context.highlighter();
|
|
157
|
-
state.requery(highlighter, context.listeners);
|
|
158
|
-
const held = anyPadDirections(context.gamepads, AXIS_DEADZONE);
|
|
159
|
-
const direction = DIRECTIONS.find((candidate) => context.repeats.fires(`${keyPrefix}:${candidate[0] ?? ''}`, held[candidate]));
|
|
160
|
-
if (direction !== undefined) {
|
|
161
|
-
state.moveInDirection(direction, highlighter, context.listeners);
|
|
162
|
-
}
|
|
163
|
-
if (context.presses.rose('act', isAnyPadButtonPressed(context.gamepads, PAD_BUTTON.A))) {
|
|
164
|
-
state.act(highlighter, context.listeners);
|
|
165
|
-
}
|
|
166
|
-
if (context.presses.rose('back', isAnyPadButtonPressed(context.gamepads, PAD_BUTTON.B))) {
|
|
167
|
-
back?.();
|
|
168
|
-
}
|
|
169
|
-
state.position(highlighter);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Gamepad navigation the input system drives each frame: the hub menu while
|
|
3
|
+
* it is open, a game's own menus opted in with `autoNavigate`, and the
|
|
4
|
+
* navigable groups a game declares.
|
|
5
|
+
*/
|
|
6
|
+
import type { RepeatingEdges, RisingEdges } from './edges';
|
|
7
|
+
import { anyPadDirections, AXIS_DEADZONE, isAnyPadButtonPressed, PAD_BUTTON } from './gamepad';
|
|
8
|
+
import type { Direction } from './gamepad';
|
|
9
|
+
import { NavState } from './nav-state';
|
|
10
|
+
import type { NavHighlighter, NavListeners } from './nav-state';
|
|
11
|
+
import type { MenuBridge, NavigableDef, NavTarget } from './types';
|
|
12
|
+
|
|
13
|
+
/** What the navigator reads from the input system. */
|
|
14
|
+
export interface NavigatorContext {
|
|
15
|
+
readonly gamepads: readonly Gamepad[];
|
|
16
|
+
readonly repeats: RepeatingEdges;
|
|
17
|
+
readonly presses: RisingEdges;
|
|
18
|
+
readonly listeners: NavListeners;
|
|
19
|
+
/** The overlay, created on demand. */
|
|
20
|
+
highlighter(): NavHighlighter;
|
|
21
|
+
/** Whether an input is held (≥ 0.5). */
|
|
22
|
+
isPressed(inputName: string): boolean;
|
|
23
|
+
/** Whether an input went down this frame. */
|
|
24
|
+
wentDown(inputName: string): boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const DIRECTIONS: readonly Direction[] = ['up', 'down', 'left', 'right'];
|
|
28
|
+
|
|
29
|
+
/** The visible focusables of the hub menu. */
|
|
30
|
+
function menuFocusables(root: ShadowRoot | null): HTMLElement[] {
|
|
31
|
+
if (root === null) {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
return [...root.querySelectorAll<HTMLElement>('a[href], button:not([disabled]), input, [tabindex]')]
|
|
35
|
+
.filter((element) => element.getClientRects().length > 0);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Visible elements matching a CSS selector. */
|
|
39
|
+
function visibleMatches(selector: string): HTMLElement[] {
|
|
40
|
+
return [...document.querySelectorAll<HTMLElement>(selector)].filter((element) => element.getClientRects().length > 0);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A navigable group with its highlight state. */
|
|
44
|
+
interface GroupNavigation {
|
|
45
|
+
readonly definition: NavigableDef;
|
|
46
|
+
readonly state: NavState;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The menu, auto-navigate and group navigation of one input system. */
|
|
50
|
+
export class Navigator {
|
|
51
|
+
private menuState: NavState | null = null;
|
|
52
|
+
private autoNavigateQuery: (() => NavTarget[]) | null = null;
|
|
53
|
+
private autoNavigateState: NavState | null = null;
|
|
54
|
+
private autoNavigateBack: (() => void) | null = null;
|
|
55
|
+
private readonly groups = new Map<string, GroupNavigation>();
|
|
56
|
+
|
|
57
|
+
/** Forget the declared groups (a new `define`). */
|
|
58
|
+
clearGroups(): void {
|
|
59
|
+
this.groups.clear();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Declare a navigable group. */
|
|
63
|
+
addGroup(group: string, definition: NavigableDef): void {
|
|
64
|
+
this.groups.set(group, { definition, state: new NavState(definition.elements, definition.wrap !== false) });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A group was enabled: highlight its first element. */
|
|
68
|
+
refreshGroup(group: string, context: NavigatorContext): void {
|
|
69
|
+
this.groups.get(group)?.state.requery(context.highlighter(), context.listeners);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** A group was disabled: drop its highlight. */
|
|
73
|
+
blurGroup(group: string, highlighter: NavHighlighter | null): void {
|
|
74
|
+
this.groups.get(group)?.state.blur(highlighter);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Opt a game's own menus into gamepad navigation. */
|
|
78
|
+
setAutoNavigate(query: () => NavTarget[], back: (() => void) | null): void {
|
|
79
|
+
this.autoNavigateQuery = query;
|
|
80
|
+
this.autoNavigateBack = back;
|
|
81
|
+
this.autoNavigateState = null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Make a CSS selector's visible matches the auto-navigate targets. */
|
|
85
|
+
static queryFor(selector: string | (() => NavTarget[])): () => NavTarget[] {
|
|
86
|
+
return typeof selector === 'string' ? () => visibleMatches(selector) : selector;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Stop auto-navigating. */
|
|
90
|
+
clearAutoNavigate(highlighter: NavHighlighter | null): void {
|
|
91
|
+
this.autoNavigateQuery = null;
|
|
92
|
+
this.autoNavigateState?.blur(highlighter);
|
|
93
|
+
this.autoNavigateState = null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The hub menu is open: drive it with the highlight ring (gamepad only). */
|
|
97
|
+
driveMenu(menu: MenuBridge, isGamepadActive: boolean, context: NavigatorContext, highlighter: NavHighlighter | null): void {
|
|
98
|
+
if (!isGamepadActive) {
|
|
99
|
+
highlighter?.hideHighlight();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
this.menuState ??= new NavState(() => menuFocusables(menu.root));
|
|
103
|
+
this.driveList(this.menuState, 'm', context, () => {
|
|
104
|
+
menu.close();
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The hub menu closed: drop its highlight. */
|
|
109
|
+
leaveMenu(highlighter: NavHighlighter | null): void {
|
|
110
|
+
this.menuState?.blur(highlighter);
|
|
111
|
+
this.menuState = null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** A game's own menus, when opted in, visible, and a gamepad is in use. */
|
|
115
|
+
driveAutoNavigate(isGamepadActive: boolean, context: NavigatorContext, highlighter: NavHighlighter | null): void {
|
|
116
|
+
const query = this.autoNavigateQuery;
|
|
117
|
+
if (query === null || !isGamepadActive || query().length === 0) {
|
|
118
|
+
this.autoNavigateState?.blur(highlighter);
|
|
119
|
+
this.autoNavigateState = null;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
this.autoNavigateState ??= new NavState(query);
|
|
123
|
+
this.driveList(this.autoNavigateState, 'an', context, this.autoNavigateBack);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The declared navigable groups that are enabled. */
|
|
127
|
+
driveGroups(isGroupEnabled: (group: string) => boolean, context: NavigatorContext): void {
|
|
128
|
+
for (const [group, { definition, state }] of this.groups) {
|
|
129
|
+
if (!isGroupEnabled(group)) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const highlighter = context.highlighter();
|
|
133
|
+
const fires = (prefix: string, inputName: string | undefined): boolean => inputName !== undefined && context.repeats.fires(prefix + group, context.isPressed(inputName));
|
|
134
|
+
const shouldStepPrevious = fires('np:', definition.prev);
|
|
135
|
+
const shouldStepNext = fires('nn:', definition.next);
|
|
136
|
+
const shouldMoveUp = fires('nu:', definition.up);
|
|
137
|
+
const shouldMoveDown = fires('nd:', definition.down);
|
|
138
|
+
if (shouldStepNext) {
|
|
139
|
+
state.step(1, highlighter, context.listeners);
|
|
140
|
+
} else if (shouldStepPrevious) {
|
|
141
|
+
state.step(-1, highlighter, context.listeners);
|
|
142
|
+
} else if (shouldMoveUp) {
|
|
143
|
+
state.moveInDirection('up', highlighter, context.listeners);
|
|
144
|
+
} else if (shouldMoveDown) {
|
|
145
|
+
state.moveInDirection('down', highlighter, context.listeners);
|
|
146
|
+
}
|
|
147
|
+
if (definition.act !== '' && context.wentDown(definition.act)) {
|
|
148
|
+
state.act(highlighter, context.listeners);
|
|
149
|
+
}
|
|
150
|
+
state.position(highlighter);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Move with the stick or d-pad, A acts, B goes back. */
|
|
155
|
+
private driveList(state: NavState, keyPrefix: string, context: NavigatorContext, back: (() => void) | null): void {
|
|
156
|
+
const highlighter = context.highlighter();
|
|
157
|
+
state.requery(highlighter, context.listeners);
|
|
158
|
+
const held = anyPadDirections(context.gamepads, AXIS_DEADZONE);
|
|
159
|
+
const direction = DIRECTIONS.find((candidate) => context.repeats.fires(`${keyPrefix}:${candidate[0] ?? ''}`, held[candidate]));
|
|
160
|
+
if (direction !== undefined) {
|
|
161
|
+
state.moveInDirection(direction, highlighter, context.listeners);
|
|
162
|
+
}
|
|
163
|
+
if (context.presses.rose('act', isAnyPadButtonPressed(context.gamepads, PAD_BUTTON.A))) {
|
|
164
|
+
state.act(highlighter, context.listeners);
|
|
165
|
+
}
|
|
166
|
+
if (context.presses.rose('back', isAnyPadButtonPressed(context.gamepads, PAD_BUTTON.B))) {
|
|
167
|
+
back?.();
|
|
168
|
+
}
|
|
169
|
+
state.position(highlighter);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -1,101 +1,101 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The input system's side of the overlay: creating it on demand, building
|
|
3
|
-
* the on-screen controls, and deciding when they show (the player's choice,
|
|
4
|
-
* else the game's default, else on touch devices without a gamepad; editing
|
|
5
|
-
* the layout shows them all).
|
|
6
|
-
*/
|
|
7
|
-
import { Overlay } from '../overlay/overlay';
|
|
8
|
-
import type { VirtualControlCallbacks } from '../overlay/virtual-controls';
|
|
9
|
-
import type { DeviceState } from './device-state';
|
|
10
|
-
import type { InputConfig, InputOverrides } from './types';
|
|
11
|
-
|
|
12
|
-
/** Whether this page counts as a touch device. */
|
|
13
|
-
export function detectTouchDevice(): boolean {
|
|
14
|
-
return typeof navigator !== 'undefined' && (navigator.maxTouchPoints > 0 || 'ontouchstart' in window);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** On-screen controls write into the device state; touching one makes touch the active source. */
|
|
18
|
-
export function feedDeviceState(devices: DeviceState, reactions: { onTouch(): void; onDragged: VirtualControlCallbacks['onDragged'] }): VirtualControlCallbacks {
|
|
19
|
-
return {
|
|
20
|
-
onButtonDown: (id) => {
|
|
21
|
-
devices.pressedVirtualButtons.add(id);
|
|
22
|
-
reactions.onTouch();
|
|
23
|
-
},
|
|
24
|
-
onButtonUp: (id) => {
|
|
25
|
-
devices.pressedVirtualButtons.delete(id);
|
|
26
|
-
},
|
|
27
|
-
onStick: (id, outX, outY) => {
|
|
28
|
-
devices.sticks.set(id, { outX, outY });
|
|
29
|
-
reactions.onTouch();
|
|
30
|
-
},
|
|
31
|
-
onTap: () => {
|
|
32
|
-
devices.pulse('tap');
|
|
33
|
-
reactions.onTouch();
|
|
34
|
-
},
|
|
35
|
-
onDragged: reactions.onDragged,
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** The overlay and the on-screen controls' visibility. */
|
|
40
|
-
export class OnScreenControls {
|
|
41
|
-
/** Whether this page counts as a touch device. */
|
|
42
|
-
readonly isTouchDevice: boolean = detectTouchDevice();
|
|
43
|
-
/** Whether a gamepad is connected (controls hide by default then). */
|
|
44
|
-
isGamepadConnected = false;
|
|
45
|
-
/** The player's explicit show/hide choice, or `null`. */
|
|
46
|
-
playerChoice: boolean | null = null;
|
|
47
|
-
private overlayInstance: Overlay | null = null;
|
|
48
|
-
private gameDefault: boolean | null = null;
|
|
49
|
-
private isEditing = false;
|
|
50
|
-
private readonly enabledGroups: () => ReadonlySet<string>;
|
|
51
|
-
|
|
52
|
-
constructor(enabledGroups: () => ReadonlySet<string>) {
|
|
53
|
-
this.enabledGroups = enabledGroups;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** The overlay if it exists yet. */
|
|
57
|
-
get existingOverlay(): Overlay | null {
|
|
58
|
-
return this.overlayInstance;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** The overlay, created on first use. */
|
|
62
|
-
overlay(): Overlay {
|
|
63
|
-
this.overlayInstance ??= new Overlay();
|
|
64
|
-
return this.overlayInstance;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** What the "Show on-screen controls" toggle reflects (excludes the editing force). */
|
|
68
|
-
get isShowing(): boolean {
|
|
69
|
-
return this.playerChoice ?? this.gameDefault ?? (this.isTouchDevice && !this.isGamepadConnected);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** Set the game's default visibility (`null` = automatic). */
|
|
73
|
-
setGameDefault(isVisible: boolean | null): void {
|
|
74
|
-
this.gameDefault = isVisible;
|
|
75
|
-
this.sync();
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** Force every control visible while the player edits the layout. */
|
|
79
|
-
setEditing(isEditing: boolean): void {
|
|
80
|
-
this.isEditing = isEditing;
|
|
81
|
-
this.sync();
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** (Re)build the controls from the config and overrides. */
|
|
85
|
-
build(config: InputConfig, overrides: InputOverrides, callbacks: VirtualControlCallbacks): void {
|
|
86
|
-
this.overlay().build(config, overrides, callbacks);
|
|
87
|
-
this.sync();
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** Apply the visibility rules to the built controls. */
|
|
91
|
-
sync(): void {
|
|
92
|
-
if (this.overlayInstance === null) {
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
if (this.isEditing) {
|
|
96
|
-
this.overlayInstance.setVirtualVisible(true, null);
|
|
97
|
-
} else {
|
|
98
|
-
this.overlayInstance.setVirtualVisible(this.isShowing, this.enabledGroups());
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* The input system's side of the overlay: creating it on demand, building
|
|
3
|
+
* the on-screen controls, and deciding when they show (the player's choice,
|
|
4
|
+
* else the game's default, else on touch devices without a gamepad; editing
|
|
5
|
+
* the layout shows them all).
|
|
6
|
+
*/
|
|
7
|
+
import { Overlay } from '../overlay/overlay';
|
|
8
|
+
import type { VirtualControlCallbacks } from '../overlay/virtual-controls';
|
|
9
|
+
import type { DeviceState } from './device-state';
|
|
10
|
+
import type { InputConfig, InputOverrides } from './types';
|
|
11
|
+
|
|
12
|
+
/** Whether this page counts as a touch device. */
|
|
13
|
+
export function detectTouchDevice(): boolean {
|
|
14
|
+
return typeof navigator !== 'undefined' && (navigator.maxTouchPoints > 0 || 'ontouchstart' in window);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** On-screen controls write into the device state; touching one makes touch the active source. */
|
|
18
|
+
export function feedDeviceState(devices: DeviceState, reactions: { onTouch(): void; onDragged: VirtualControlCallbacks['onDragged'] }): VirtualControlCallbacks {
|
|
19
|
+
return {
|
|
20
|
+
onButtonDown: (id) => {
|
|
21
|
+
devices.pressedVirtualButtons.add(id);
|
|
22
|
+
reactions.onTouch();
|
|
23
|
+
},
|
|
24
|
+
onButtonUp: (id) => {
|
|
25
|
+
devices.pressedVirtualButtons.delete(id);
|
|
26
|
+
},
|
|
27
|
+
onStick: (id, outX, outY) => {
|
|
28
|
+
devices.sticks.set(id, { outX, outY });
|
|
29
|
+
reactions.onTouch();
|
|
30
|
+
},
|
|
31
|
+
onTap: () => {
|
|
32
|
+
devices.pulse('tap');
|
|
33
|
+
reactions.onTouch();
|
|
34
|
+
},
|
|
35
|
+
onDragged: reactions.onDragged,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The overlay and the on-screen controls' visibility. */
|
|
40
|
+
export class OnScreenControls {
|
|
41
|
+
/** Whether this page counts as a touch device. */
|
|
42
|
+
readonly isTouchDevice: boolean = detectTouchDevice();
|
|
43
|
+
/** Whether a gamepad is connected (controls hide by default then). */
|
|
44
|
+
isGamepadConnected = false;
|
|
45
|
+
/** The player's explicit show/hide choice, or `null`. */
|
|
46
|
+
playerChoice: boolean | null = null;
|
|
47
|
+
private overlayInstance: Overlay | null = null;
|
|
48
|
+
private gameDefault: boolean | null = null;
|
|
49
|
+
private isEditing = false;
|
|
50
|
+
private readonly enabledGroups: () => ReadonlySet<string>;
|
|
51
|
+
|
|
52
|
+
constructor(enabledGroups: () => ReadonlySet<string>) {
|
|
53
|
+
this.enabledGroups = enabledGroups;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The overlay if it exists yet. */
|
|
57
|
+
get existingOverlay(): Overlay | null {
|
|
58
|
+
return this.overlayInstance;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The overlay, created on first use. */
|
|
62
|
+
overlay(): Overlay {
|
|
63
|
+
this.overlayInstance ??= new Overlay();
|
|
64
|
+
return this.overlayInstance;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** What the "Show on-screen controls" toggle reflects (excludes the editing force). */
|
|
68
|
+
get isShowing(): boolean {
|
|
69
|
+
return this.playerChoice ?? this.gameDefault ?? (this.isTouchDevice && !this.isGamepadConnected);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Set the game's default visibility (`null` = automatic). */
|
|
73
|
+
setGameDefault(isVisible: boolean | null): void {
|
|
74
|
+
this.gameDefault = isVisible;
|
|
75
|
+
this.sync();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Force every control visible while the player edits the layout. */
|
|
79
|
+
setEditing(isEditing: boolean): void {
|
|
80
|
+
this.isEditing = isEditing;
|
|
81
|
+
this.sync();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** (Re)build the controls from the config and overrides. */
|
|
85
|
+
build(config: InputConfig, overrides: InputOverrides, callbacks: VirtualControlCallbacks): void {
|
|
86
|
+
this.overlay().build(config, overrides, callbacks);
|
|
87
|
+
this.sync();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Apply the visibility rules to the built controls. */
|
|
91
|
+
sync(): void {
|
|
92
|
+
if (this.overlayInstance === null) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (this.isEditing) {
|
|
96
|
+
this.overlayInstance.setVirtualVisible(true, null);
|
|
97
|
+
} else {
|
|
98
|
+
this.overlayInstance.setVirtualVisible(this.isShowing, this.enabledGroups());
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The small, stable slice of the input system that other bundles on the page
|
|
3
|
-
* use (plan §4.4): the host menu (attach itself, `game.controls`, the
|
|
4
|
-
* Controls button), the social runtime's dialogs (navigate them), the
|
|
5
|
-
* overlay stack (suspend `play`), and a handover between two client copies.
|
|
6
|
-
* Every client version keeps these methods.
|
|
7
|
-
*/
|
|
8
|
-
import { hasMethods } from '../realtime/cross-bundle';
|
|
9
|
-
import type { MenuBridge, NavTarget } from './types';
|
|
10
|
-
|
|
11
|
-
/** The cross-bundle input surface. */
|
|
12
|
-
export interface PageInputControl {
|
|
13
|
-
/** Whether the game declared its inputs. */
|
|
14
|
-
readonly isDefined: boolean;
|
|
15
|
-
/** The host menu attaches itself. */
|
|
16
|
-
attachMenu(bridge: MenuBridge | null): void;
|
|
17
|
-
/** The host menu passes `game.controls`. */
|
|
18
|
-
// eslint-disable-next-line no-restricted-syntax -- trust boundary: game.controls from the page, parsed by the implementation
|
|
19
|
-
setLegacyControls(controls: unknown): void;
|
|
20
|
-
/** The menu's Controls button. */
|
|
21
|
-
openControls(): void;
|
|
22
|
-
/** Whether a group is on. */
|
|
23
|
-
isEnabled(group: string): boolean;
|
|
24
|
-
/** Turn a group on. */
|
|
25
|
-
enable(group: string): void;
|
|
26
|
-
/** Turn a group off. */
|
|
27
|
-
disable(group: string): void;
|
|
28
|
-
/** Make elements gamepad-navigable (the runtime's dialogs). */
|
|
29
|
-
autoNavigate(selector: string | (() => NavTarget[]), options?: { back?: () => void }): void;
|
|
30
|
-
/** Stop navigating them. */
|
|
31
|
-
clearAutoNavigate(): void;
|
|
32
|
-
/** Stop driving the page and hand over to `owner`. Absent on v1 copies. */
|
|
33
|
-
yieldPageTo?(owner: PageInputControl): void;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** The methods every input system on the page has, v1 copies included. */
|
|
37
|
-
const PAGE_INPUT_METHODS = ['attachMenu', 'setLegacyControls', 'openControls', 'isEnabled', 'enable', 'disable', 'autoNavigate', 'clearAutoNavigate', 'define', 'value'] as const;
|
|
38
|
-
|
|
39
|
-
/** Whether `candidate` is some bundle's input system. */
|
|
40
|
-
// eslint-disable-next-line no-restricted-syntax -- trust boundary: the instance may come from another bundle
|
|
41
|
-
export function isPageInputControl(candidate: unknown): candidate is PageInputControl {
|
|
42
|
-
return hasMethods(candidate, PAGE_INPUT_METHODS);
|
|
43
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* The small, stable slice of the input system that other bundles on the page
|
|
3
|
+
* use (plan §4.4): the host menu (attach itself, `game.controls`, the
|
|
4
|
+
* Controls button), the social runtime's dialogs (navigate them), the
|
|
5
|
+
* overlay stack (suspend `play`), and a handover between two client copies.
|
|
6
|
+
* Every client version keeps these methods.
|
|
7
|
+
*/
|
|
8
|
+
import { hasMethods } from '../realtime/cross-bundle';
|
|
9
|
+
import type { MenuBridge, NavTarget } from './types';
|
|
10
|
+
|
|
11
|
+
/** The cross-bundle input surface. */
|
|
12
|
+
export interface PageInputControl {
|
|
13
|
+
/** Whether the game declared its inputs. */
|
|
14
|
+
readonly isDefined: boolean;
|
|
15
|
+
/** The host menu attaches itself. */
|
|
16
|
+
attachMenu(bridge: MenuBridge | null): void;
|
|
17
|
+
/** The host menu passes `game.controls`. */
|
|
18
|
+
// eslint-disable-next-line no-restricted-syntax -- trust boundary: game.controls from the page, parsed by the implementation
|
|
19
|
+
setLegacyControls(controls: unknown): void;
|
|
20
|
+
/** The menu's Controls button. */
|
|
21
|
+
openControls(): void;
|
|
22
|
+
/** Whether a group is on. */
|
|
23
|
+
isEnabled(group: string): boolean;
|
|
24
|
+
/** Turn a group on. */
|
|
25
|
+
enable(group: string): void;
|
|
26
|
+
/** Turn a group off. */
|
|
27
|
+
disable(group: string): void;
|
|
28
|
+
/** Make elements gamepad-navigable (the runtime's dialogs). */
|
|
29
|
+
autoNavigate(selector: string | (() => NavTarget[]), options?: { back?: () => void }): void;
|
|
30
|
+
/** Stop navigating them. */
|
|
31
|
+
clearAutoNavigate(): void;
|
|
32
|
+
/** Stop driving the page and hand over to `owner`. Absent on v1 copies. */
|
|
33
|
+
yieldPageTo?(owner: PageInputControl): void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The methods every input system on the page has, v1 copies included. */
|
|
37
|
+
const PAGE_INPUT_METHODS = ['attachMenu', 'setLegacyControls', 'openControls', 'isEnabled', 'enable', 'disable', 'autoNavigate', 'clearAutoNavigate', 'define', 'value'] as const;
|
|
38
|
+
|
|
39
|
+
/** Whether `candidate` is some bundle's input system. */
|
|
40
|
+
// eslint-disable-next-line no-restricted-syntax -- trust boundary: the instance may come from another bundle
|
|
41
|
+
export function isPageInputControl(candidate: unknown): candidate is PageInputControl {
|
|
42
|
+
return hasMethods(candidate, PAGE_INPUT_METHODS);
|
|
43
|
+
}
|