@rydr/game-sdk 6.1.0 → 7.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/dist/client/PlatformClient.d.ts +25 -2
- package/dist/client/PlatformClient.d.ts.map +1 -1
- package/dist/client/PlatformClient.js +47 -17
- package/dist/client/PlatformClient.js.map +1 -1
- package/dist/host/PlatformHost.d.ts +0 -4
- package/dist/host/PlatformHost.d.ts.map +1 -1
- package/dist/host/PlatformHost.js +0 -3
- package/dist/host/PlatformHost.js.map +1 -1
- package/dist/protocol/boards.d.ts +13 -5
- package/dist/protocol/boards.d.ts.map +1 -1
- package/dist/protocol/boards.js.map +1 -1
- package/dist/protocol/buttons.d.ts +10 -8
- package/dist/protocol/buttons.d.ts.map +1 -1
- package/dist/protocol/buttons.js +10 -8
- package/dist/protocol/buttons.js.map +1 -1
- package/dist/protocol/glyphs.d.ts.map +1 -1
- package/dist/protocol/glyphs.js +22 -1
- package/dist/protocol/glyphs.js.map +1 -1
- package/dist/protocol/messages.d.ts +1 -15
- package/dist/protocol/messages.d.ts.map +1 -1
- package/dist/protocol/version.d.ts +2 -2
- package/dist/protocol/version.d.ts.map +1 -1
- package/dist/protocol/version.js +15 -2
- package/dist/protocol/version.js.map +1 -1
- package/dist/three/perf-overlay.d.ts +52 -2
- package/dist/three/perf-overlay.d.ts.map +1 -1
- package/dist/three/perf-overlay.js +188 -3
- package/dist/three/perf-overlay.js.map +1 -1
- package/dist/ui/README.md +45 -1
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +1 -0
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/keycap.d.ts +23 -0
- package/dist/ui/keycap.d.ts.map +1 -1
- package/dist/ui/keycap.js +42 -0
- package/dist/ui/keycap.js.map +1 -1
- package/dist/ui/pause-menu.d.ts +144 -0
- package/dist/ui/pause-menu.d.ts.map +1 -0
- package/dist/ui/pause-menu.js +500 -0
- package/dist/ui/pause-menu.js.map +1 -0
- package/dist/ui/showcase/index.d.ts.map +1 -1
- package/dist/ui/showcase/index.js +35 -1
- package/dist/ui/showcase/index.js.map +1 -1
- package/dist/ui/styles.js +21 -0
- package/dist/ui/styles.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PAUSE MENU — the in-game menu every RYDR game shares, opened by the game's own `OPTIONS` button.
|
|
3
|
+
*
|
|
4
|
+
* This is the GAME's menu, deliberately distinct from the PLATFORM's overlay (which the shell opens on
|
|
5
|
+
* `MENU` / `M` / phone MENU and which never reaches a game — see `ButtonName`). The platform overlay
|
|
6
|
+
* carries the rider's concerns: activity, hardware, volume, exit. This one carries the *game's*:
|
|
7
|
+
* resume, restart, back to your own menu. It deliberately quotes the platform overlay's visual language
|
|
8
|
+
* — dim + blurred backdrop, frosted rows, the staggered "warp" entrance — so pause reads as a RYDR
|
|
9
|
+
* surface one level shallower than the platform's, rather than as a second, unrelated menu.
|
|
10
|
+
*
|
|
11
|
+
* const pause = mountPauseMenu(document.body, {
|
|
12
|
+
* session,
|
|
13
|
+
* title: "Split Racing",
|
|
14
|
+
* canOpen: () => phase === "playing", // the game decides WHEN it may open
|
|
15
|
+
* onOpen: () => { paused = true }, // the game freezes its own loop
|
|
16
|
+
* onClose: () => { paused = false },
|
|
17
|
+
* items: [
|
|
18
|
+
* { label: "Restart", shortcut: "DIAMOND_UP", onSelect: () => restart() },
|
|
19
|
+
* { label: "Back to menu", onSelect: () => go("/") },
|
|
20
|
+
* ],
|
|
21
|
+
* });
|
|
22
|
+
*
|
|
23
|
+
* `Resume` is prepended for free, so the shortest useful menu is no items at all.
|
|
24
|
+
*
|
|
25
|
+
* **Everything the rider reads is in the GAME's font.** The title and every row inherit the host's
|
|
26
|
+
* font-family (override with the `font` option or the `--rydr-pause-font` custom property), because
|
|
27
|
+
* this menu belongs to the game, not to the shell — the one exception being the keycaps, which keep
|
|
28
|
+
* their standard RYDR type because they are pictures of the rider's physical controller.
|
|
29
|
+
*
|
|
30
|
+
* **What it does NOT do: stop your game.** No library can — your loop, timers and physics are yours.
|
|
31
|
+
* The menu tells you when it opens and closes; freeze there. What it *does* guarantee is that a game
|
|
32
|
+
* which keeps running cannot be *driven* while the rider is in the menu: it takes the controller over
|
|
33
|
+
* for the duration (`session.grabInput`), so your `onButton` handlers go quiet, `isDown`/`stick` read
|
|
34
|
+
* resting, and anything held is released first. It also declares `"menu"` to the shell while it's up,
|
|
35
|
+
* so trainer resistance eases like on any other non-racing screen, and restores `"playing"` on resume.
|
|
36
|
+
*
|
|
37
|
+
* A row may advertise a `shortcut` — the button the rider can press *in-game* to do the same thing
|
|
38
|
+
* (`Restart · Shortcut [Y]`). Binding that shortcut during play stays the game's own job; the menu
|
|
39
|
+
* only teaches it, and honours it while the menu itself is open (where it owns input anyway).
|
|
40
|
+
*/
|
|
41
|
+
import type { ButtonEdge, ButtonName, GlyphSet } from "../protocol/index.js";
|
|
42
|
+
import { type NavCue } from "../nav/spatial-nav.js";
|
|
43
|
+
/**
|
|
44
|
+
* The slice of a `PlatformSession` the menu uses. A real session satisfies it structurally, and every
|
|
45
|
+
* capability past `onButton` is optional so a stand-in (a showcase double, a test) works too — without
|
|
46
|
+
* `grabInput` the menu simply can't quiet the game's own input, and without `setActivity` the trainer
|
|
47
|
+
* isn't eased.
|
|
48
|
+
*/
|
|
49
|
+
export interface PauseMenuSession {
|
|
50
|
+
onButton(cb: (e: {
|
|
51
|
+
name: ButtonName;
|
|
52
|
+
edge: ButtonEdge;
|
|
53
|
+
}) => void): () => void;
|
|
54
|
+
grabInput?(handler: (e: {
|
|
55
|
+
name: ButtonName;
|
|
56
|
+
edge: ButtonEdge;
|
|
57
|
+
}) => void): () => void;
|
|
58
|
+
setActivity?(state: "playing" | "menu"): void;
|
|
59
|
+
}
|
|
60
|
+
/** One row of the menu. */
|
|
61
|
+
export interface PauseMenuItem {
|
|
62
|
+
/** The row's text — keep it short (it's read from a few feet away): "Restart", "Back to menu". */
|
|
63
|
+
label: string;
|
|
64
|
+
/** Run when the rider picks the row (with `A`, a click, or the row's `shortcut`). */
|
|
65
|
+
onSelect: () => void;
|
|
66
|
+
/**
|
|
67
|
+
* The button that does this same thing **directly in-game**, shown on the row as `Shortcut [Y]` so
|
|
68
|
+
* the rider learns it. The game keeps owning that binding during play; the menu honours it too while
|
|
69
|
+
* it's open.
|
|
70
|
+
*/
|
|
71
|
+
shortcut?: ButtonName;
|
|
72
|
+
/** Optional second line under the label — a consequence worth spelling out ("You'll lose this lap"). */
|
|
73
|
+
hint?: string;
|
|
74
|
+
/** Unavailable right now: shown inert, skipped by the focus ring. */
|
|
75
|
+
disabled?: boolean;
|
|
76
|
+
/** Destructive tint (leaving, wiping a run) — the same red the platform overlay gives Exit. */
|
|
77
|
+
danger?: boolean;
|
|
78
|
+
/** Keep the menu open after `onSelect` (a toggle, a row that swaps the list via `setItems`). Default
|
|
79
|
+
* is to close first, so the action runs on a resumed game with input already handed back. */
|
|
80
|
+
keepOpen?: boolean;
|
|
81
|
+
}
|
|
82
|
+
export interface PauseMenuOptions {
|
|
83
|
+
/** The platform session — for input, the trainer-easing hint, and the rider's controller lettering. */
|
|
84
|
+
session?: PauseMenuSession;
|
|
85
|
+
/** The game's name, printed big at the top in the game's own font. */
|
|
86
|
+
title: string;
|
|
87
|
+
/** The rows after `Resume`. Replaceable at any time with `setItems`. */
|
|
88
|
+
items?: PauseMenuItem[];
|
|
89
|
+
/** Which button opens (and closes) the menu. Default `"OPTIONS"` — the game's own menu button. */
|
|
90
|
+
openButton?: ButtonName;
|
|
91
|
+
/**
|
|
92
|
+
* Asked before opening: return `false` to refuse. This is how a game keeps the menu to its PLAYING
|
|
93
|
+
* phase (`() => phase === "playing"`) instead of letting it stack on top of its own menus. Default:
|
|
94
|
+
* always allowed.
|
|
95
|
+
*/
|
|
96
|
+
canOpen?: () => boolean;
|
|
97
|
+
/** Label of the free first row. Default `"Resume"`. */
|
|
98
|
+
resumeLabel?: string;
|
|
99
|
+
/** Declare `"menu"` to the shell while open and `"playing"` on close (eases trainer resistance).
|
|
100
|
+
* Default `true`. Turn it off only if the game manages activity state itself around the menu. */
|
|
101
|
+
activity?: boolean;
|
|
102
|
+
/** Take the controller over while open via `session.grabInput` (default `true`). Off = the game keeps
|
|
103
|
+
* receiving input behind the menu, which it must then ignore itself. */
|
|
104
|
+
captureInput?: boolean;
|
|
105
|
+
/** Nav sounds — pass a `Soundboard.cue`; omit for silence. */
|
|
106
|
+
cue?: (id: NavCue) => void;
|
|
107
|
+
/** Force the menu's font (any CSS `font-family` value). Default: inherited from the host — i.e. the
|
|
108
|
+
* game's own font, which is what you want. */
|
|
109
|
+
font?: string;
|
|
110
|
+
/** Controller lettering for the keycaps. Default: the session's, else Xbox. */
|
|
111
|
+
glyphSet?: GlyphSet;
|
|
112
|
+
/** The menu opened — freeze your loop here. */
|
|
113
|
+
onOpen?: () => void;
|
|
114
|
+
/** The menu closed (resume, back, the backdrop, or a row that ran) — unfreeze here. */
|
|
115
|
+
onClose?: () => void;
|
|
116
|
+
}
|
|
117
|
+
export interface PauseMenu {
|
|
118
|
+
/** The overlay root (mounted in the host; hidden while closed). */
|
|
119
|
+
root: HTMLElement;
|
|
120
|
+
/** Open it (no-op if already open or `canOpen` refuses). Returns whether it opened. */
|
|
121
|
+
open(): boolean;
|
|
122
|
+
/** Close it, playing the exit animation. */
|
|
123
|
+
close(): void;
|
|
124
|
+
/** Open if closed, close if open. Returns whether it ended up open. */
|
|
125
|
+
toggle(): boolean;
|
|
126
|
+
isOpen(): boolean;
|
|
127
|
+
/** Replace the rows after `Resume` (rebuilt live if the menu is open). */
|
|
128
|
+
setItems(items: PauseMenuItem[]): void;
|
|
129
|
+
/** Change the big title. */
|
|
130
|
+
setTitle(title: string): void;
|
|
131
|
+
/**
|
|
132
|
+
* Feed one canonical button in by hand — for a host that arbitrates input itself instead of letting
|
|
133
|
+
* the menu subscribe (`session` omitted, or a shell-style router). Returns `true` if the menu
|
|
134
|
+
* consumed it. Pass `down` edges.
|
|
135
|
+
*/
|
|
136
|
+
handleButton(name: ButtonName): boolean;
|
|
137
|
+
dispose(): void;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Mount the pause menu (hidden until opened). Appended to `host` — normally `document.body`, so the
|
|
141
|
+
* overlay covers the game whatever the DOM under it looks like.
|
|
142
|
+
*/
|
|
143
|
+
export declare function mountPauseMenu(host: HTMLElement, opts: PauseMenuOptions): PauseMenu;
|
|
144
|
+
//# sourceMappingURL=pause-menu.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pause-menu.d.ts","sourceRoot":"","sources":["../../src/ui/pause-menu.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PAUSE MENU — the in-game menu every RYDR game shares, opened by the game's own `OPTIONS` button.
|
|
3
|
+
*
|
|
4
|
+
* This is the GAME's menu, deliberately distinct from the PLATFORM's overlay (which the shell opens on
|
|
5
|
+
* `MENU` / `M` / phone MENU and which never reaches a game — see `ButtonName`). The platform overlay
|
|
6
|
+
* carries the rider's concerns: activity, hardware, volume, exit. This one carries the *game's*:
|
|
7
|
+
* resume, restart, back to your own menu. It deliberately quotes the platform overlay's visual language
|
|
8
|
+
* — dim + blurred backdrop, frosted rows, the staggered "warp" entrance — so pause reads as a RYDR
|
|
9
|
+
* surface one level shallower than the platform's, rather than as a second, unrelated menu.
|
|
10
|
+
*
|
|
11
|
+
* const pause = mountPauseMenu(document.body, {
|
|
12
|
+
* session,
|
|
13
|
+
* title: "Split Racing",
|
|
14
|
+
* canOpen: () => phase === "playing", // the game decides WHEN it may open
|
|
15
|
+
* onOpen: () => { paused = true }, // the game freezes its own loop
|
|
16
|
+
* onClose: () => { paused = false },
|
|
17
|
+
* items: [
|
|
18
|
+
* { label: "Restart", shortcut: "DIAMOND_UP", onSelect: () => restart() },
|
|
19
|
+
* { label: "Back to menu", onSelect: () => go("/") },
|
|
20
|
+
* ],
|
|
21
|
+
* });
|
|
22
|
+
*
|
|
23
|
+
* `Resume` is prepended for free, so the shortest useful menu is no items at all.
|
|
24
|
+
*
|
|
25
|
+
* **Everything the rider reads is in the GAME's font.** The title and every row inherit the host's
|
|
26
|
+
* font-family (override with the `font` option or the `--rydr-pause-font` custom property), because
|
|
27
|
+
* this menu belongs to the game, not to the shell — the one exception being the keycaps, which keep
|
|
28
|
+
* their standard RYDR type because they are pictures of the rider's physical controller.
|
|
29
|
+
*
|
|
30
|
+
* **What it does NOT do: stop your game.** No library can — your loop, timers and physics are yours.
|
|
31
|
+
* The menu tells you when it opens and closes; freeze there. What it *does* guarantee is that a game
|
|
32
|
+
* which keeps running cannot be *driven* while the rider is in the menu: it takes the controller over
|
|
33
|
+
* for the duration (`session.grabInput`), so your `onButton` handlers go quiet, `isDown`/`stick` read
|
|
34
|
+
* resting, and anything held is released first. It also declares `"menu"` to the shell while it's up,
|
|
35
|
+
* so trainer resistance eases like on any other non-racing screen, and restores `"playing"` on resume.
|
|
36
|
+
*
|
|
37
|
+
* A row may advertise a `shortcut` — the button the rider can press *in-game* to do the same thing
|
|
38
|
+
* (`Restart · Shortcut [Y]`). Binding that shortcut during play stays the game's own job; the menu
|
|
39
|
+
* only teaches it, and honours it while the menu itself is open (where it owns input anyway).
|
|
40
|
+
*/
|
|
41
|
+
import { DEFAULT_GLYPH_SET } from "../protocol/glyphs.js";
|
|
42
|
+
import { createSpatialNav } from "../nav/spatial-nav.js";
|
|
43
|
+
import { createKeycap, createLabelKeycap } from "./keycap.js";
|
|
44
|
+
import { injectCss } from "./styles.js";
|
|
45
|
+
/**
|
|
46
|
+
* The rider's controller lettering: an explicit `glyphSet` wins, else the one a real `PlatformSession`
|
|
47
|
+
* carries, else Xbox. Probed structurally rather than declared on {@link PauseMenuSession} — same
|
|
48
|
+
* reasoning as `createKeycap`'s own probe: that type is the *minimal* slice the menu needs, and
|
|
49
|
+
* demanding a hardware snapshot would break every hand-rolled stub for a value that's optional anyway.
|
|
50
|
+
*/
|
|
51
|
+
function resolveGlyphSet(opts) {
|
|
52
|
+
if (opts.glyphSet)
|
|
53
|
+
return opts.glyphSet;
|
|
54
|
+
const probe = opts.session;
|
|
55
|
+
return probe?.hardware?.current?.glyphSet ?? DEFAULT_GLYPH_SET;
|
|
56
|
+
}
|
|
57
|
+
/** Entrance/exit duration + per-row stagger, mirrored in the CSS below. */
|
|
58
|
+
const ANIM_MS = 500;
|
|
59
|
+
const STAGGER_MS = 26;
|
|
60
|
+
/** Rows warp outward this fraction of their distance from the list's centre. */
|
|
61
|
+
const WARP_PUSH = 0.9;
|
|
62
|
+
let pauseCssInjected = false;
|
|
63
|
+
function injectPauseCss() {
|
|
64
|
+
if (pauseCssInjected || document.getElementById("rydr-pause-css")) {
|
|
65
|
+
pauseCssInjected = true;
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const style = document.createElement("style");
|
|
69
|
+
style.id = "rydr-pause-css";
|
|
70
|
+
style.textContent = CSS;
|
|
71
|
+
document.head.appendChild(style);
|
|
72
|
+
pauseCssInjected = true;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Mount the pause menu (hidden until opened). Appended to `host` — normally `document.body`, so the
|
|
76
|
+
* overlay covers the game whatever the DOM under it looks like.
|
|
77
|
+
*/
|
|
78
|
+
export function mountPauseMenu(host, opts) {
|
|
79
|
+
injectCss(); // the keycaps' own .rydr-ui-* styles
|
|
80
|
+
injectPauseCss();
|
|
81
|
+
const { session, openButton = "OPTIONS", resumeLabel = "Resume", activity = true, captureInput = true, cue, } = opts;
|
|
82
|
+
const glyphSet = resolveGlyphSet(opts);
|
|
83
|
+
let items = opts.items ? [...opts.items] : [];
|
|
84
|
+
let open = false;
|
|
85
|
+
let closing = false;
|
|
86
|
+
let closeTimer = null;
|
|
87
|
+
let releaseInput = null;
|
|
88
|
+
let nav = null;
|
|
89
|
+
/** The rows currently in the DOM, index-aligned with {@link rowItems}. */
|
|
90
|
+
let rows = [];
|
|
91
|
+
/** The item each row stands for — `Resume` first, then `items`. */
|
|
92
|
+
let rowItems = [];
|
|
93
|
+
/** Keycaps built for the current rows (disposed on every rebuild). */
|
|
94
|
+
let rowCaps = [];
|
|
95
|
+
// ── DOM ────────────────────────────────────────────────────────────────────────────────────────
|
|
96
|
+
const root = document.createElement("div");
|
|
97
|
+
root.className = "rydr-pause";
|
|
98
|
+
root.tabIndex = -1;
|
|
99
|
+
if (opts.font)
|
|
100
|
+
root.style.setProperty("--rydr-pause-font", opts.font);
|
|
101
|
+
const inner = document.createElement("div");
|
|
102
|
+
inner.className = "rydr-pause-inner";
|
|
103
|
+
const titleEl = document.createElement("div");
|
|
104
|
+
titleEl.className = "rydr-pause-title";
|
|
105
|
+
titleEl.textContent = opts.title;
|
|
106
|
+
const listEl = document.createElement("div");
|
|
107
|
+
listEl.className = "rydr-pause-list";
|
|
108
|
+
inner.append(titleEl, listEl);
|
|
109
|
+
root.appendChild(inner);
|
|
110
|
+
host.appendChild(root);
|
|
111
|
+
// Backdrop click resumes — the mouse equivalent of pressing the open button again.
|
|
112
|
+
root.addEventListener("click", (e) => {
|
|
113
|
+
if (e.target === root)
|
|
114
|
+
close();
|
|
115
|
+
});
|
|
116
|
+
// ── Keycap press feedback ──────────────────────────────────────────────────────────────────────
|
|
117
|
+
// The keycaps can't listen to `session.onButton`: while the menu holds an input grab, the session
|
|
118
|
+
// delivers nothing to ordinary listeners (that's the point of the grab). So they subscribe to this
|
|
119
|
+
// local source, which the menu feeds from whichever path is actually live.
|
|
120
|
+
const capListeners = new Set();
|
|
121
|
+
const capSource = {
|
|
122
|
+
onButton: (cb) => {
|
|
123
|
+
capListeners.add(cb);
|
|
124
|
+
return () => capListeners.delete(cb);
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* The confirm keycap, which RIDES the focused row: every row carries a fixed-width slot at its end
|
|
129
|
+
* and the cap is moved into the focused one. Deliberately not the platform overlay's fixed-position
|
|
130
|
+
* roving cap — these rows are full-width, so a cap anchored to a row's right edge would land on top
|
|
131
|
+
* of that row's shortcut keycap. Riding a real slot also needs no measuring, so it can't drift
|
|
132
|
+
* mid-animation or on scroll.
|
|
133
|
+
*/
|
|
134
|
+
const confirmCap = createKeycap("DIAMOND_DOWN", {
|
|
135
|
+
variant: "solo",
|
|
136
|
+
colored: false,
|
|
137
|
+
animate: true,
|
|
138
|
+
press: capSource,
|
|
139
|
+
glyphSet,
|
|
140
|
+
});
|
|
141
|
+
confirmCap.el.classList.add("rydr-pause-cap");
|
|
142
|
+
// ── Rows ───────────────────────────────────────────────────────────────────────────────────────
|
|
143
|
+
function buildRows() {
|
|
144
|
+
for (const cap of rowCaps)
|
|
145
|
+
cap.dispose();
|
|
146
|
+
rowCaps = [];
|
|
147
|
+
listEl.textContent = "";
|
|
148
|
+
rowItems = [{ label: resumeLabel, onSelect: () => { }, shortcut: openButton }, ...items];
|
|
149
|
+
rows = rowItems.map((item, i) => {
|
|
150
|
+
const row = document.createElement("button");
|
|
151
|
+
row.type = "button";
|
|
152
|
+
row.className = "rydr-pause-row" + (item.danger ? " danger" : "");
|
|
153
|
+
if (item.disabled)
|
|
154
|
+
row.disabled = true;
|
|
155
|
+
row.style.setProperty("--i", String(i));
|
|
156
|
+
row.style.setProperty("--ri", String(rowItems.length - 1 - i));
|
|
157
|
+
const text = document.createElement("span");
|
|
158
|
+
text.className = "rydr-pause-text";
|
|
159
|
+
const label = document.createElement("span");
|
|
160
|
+
label.className = "rydr-pause-label";
|
|
161
|
+
label.textContent = item.label;
|
|
162
|
+
text.appendChild(label);
|
|
163
|
+
if (item.hint) {
|
|
164
|
+
const hint = document.createElement("span");
|
|
165
|
+
hint.className = "rydr-pause-hint";
|
|
166
|
+
hint.textContent = item.hint;
|
|
167
|
+
text.appendChild(hint);
|
|
168
|
+
}
|
|
169
|
+
row.appendChild(text);
|
|
170
|
+
const right = document.createElement("span");
|
|
171
|
+
right.className = "rydr-pause-right";
|
|
172
|
+
if (item.shortcut) {
|
|
173
|
+
const shortcut = document.createElement("span");
|
|
174
|
+
shortcut.className = "rydr-pause-shortcut";
|
|
175
|
+
const tag = document.createElement("span");
|
|
176
|
+
tag.className = "rydr-pause-shortcut-tag";
|
|
177
|
+
tag.textContent = "Shortcut";
|
|
178
|
+
const cap = createLabelKeycap(item.shortcut, { press: capSource, glyphSet });
|
|
179
|
+
rowCaps.push(cap);
|
|
180
|
+
if (item.disabled)
|
|
181
|
+
cap.setDisabled(true);
|
|
182
|
+
shortcut.append(tag, cap.el);
|
|
183
|
+
right.appendChild(shortcut);
|
|
184
|
+
}
|
|
185
|
+
// The slot the confirm keycap moves into when this row takes focus. Always present (and always
|
|
186
|
+
// the same width) so focus moving between rows never reflows the list.
|
|
187
|
+
const slot = document.createElement("span");
|
|
188
|
+
slot.className = "rydr-pause-confirm";
|
|
189
|
+
right.appendChild(slot);
|
|
190
|
+
row.appendChild(right);
|
|
191
|
+
row.addEventListener("click", () => select(i));
|
|
192
|
+
listEl.appendChild(row);
|
|
193
|
+
return row;
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Set each row's warp offset from its position in the list, so the entrance flies rows out from (and
|
|
198
|
+
* back to) their own side of the centre. Measured with the transform-invariant layout box, never
|
|
199
|
+
* `getBoundingClientRect()`, which returns the live *transformed* rect mid-animation and would make
|
|
200
|
+
* every replay drift.
|
|
201
|
+
*/
|
|
202
|
+
function computeOffsets() {
|
|
203
|
+
const cy = listEl.offsetHeight / 2;
|
|
204
|
+
for (const row of rows) {
|
|
205
|
+
const rc = row.offsetTop + row.offsetHeight / 2;
|
|
206
|
+
row.style.setProperty("--dy", ((rc - cy) * WARP_PUSH).toFixed(0) + "px");
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/** Move the confirm keycap into the focused row's slot (out of the DOM when nothing is focused). */
|
|
210
|
+
function placeCap() {
|
|
211
|
+
const row = open ? nav?.current() ?? null : null;
|
|
212
|
+
const slot = row?.querySelector(".rydr-pause-confirm");
|
|
213
|
+
if (!slot) {
|
|
214
|
+
confirmCap.el.remove();
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (confirmCap.el.parentNode !== slot)
|
|
218
|
+
slot.appendChild(confirmCap.el);
|
|
219
|
+
}
|
|
220
|
+
// ── Selection ──────────────────────────────────────────────────────────────────────────────────
|
|
221
|
+
/**
|
|
222
|
+
* Run row `i`. Closes FIRST by default — so the game is already unfrozen and holding its own input
|
|
223
|
+
* again by the time the action runs (a restart shouldn't begin behind a menu that's still up).
|
|
224
|
+
*/
|
|
225
|
+
function select(i) {
|
|
226
|
+
const item = rowItems[i];
|
|
227
|
+
if (!item || item.disabled)
|
|
228
|
+
return;
|
|
229
|
+
confirmCap.setPressed(true);
|
|
230
|
+
setTimeout(() => confirmCap.setPressed(false), 160);
|
|
231
|
+
cue?.("nav.confirm");
|
|
232
|
+
if (item.keepOpen) {
|
|
233
|
+
item.onSelect();
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
close();
|
|
237
|
+
item.onSelect();
|
|
238
|
+
}
|
|
239
|
+
// ── Input ──────────────────────────────────────────────────────────────────────────────────────
|
|
240
|
+
/** Route one `down` edge. Returns whether the menu consumed it. */
|
|
241
|
+
function handleButton(name) {
|
|
242
|
+
if (!open) {
|
|
243
|
+
if (name !== openButton)
|
|
244
|
+
return false;
|
|
245
|
+
return openMenu();
|
|
246
|
+
}
|
|
247
|
+
if (name === openButton) {
|
|
248
|
+
close();
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
// A row's advertised shortcut fires that row directly — the menu owns input here, so honouring it
|
|
252
|
+
// costs nothing and matches what the row promises.
|
|
253
|
+
const shortcutIndex = rowItems.findIndex((it) => it.shortcut === name && !it.disabled);
|
|
254
|
+
if (shortcutIndex > 0) {
|
|
255
|
+
select(shortcutIndex);
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
nav?.handle(name); // directions, A (→ onActivate), B (→ onBack → close)
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
/** Feed an edge to the in-menu keycaps, then route `down` edges. Shared by both input paths. */
|
|
262
|
+
function onEdge(e) {
|
|
263
|
+
for (const cb of capListeners)
|
|
264
|
+
cb(e);
|
|
265
|
+
if (e.edge === "down")
|
|
266
|
+
handleButton(e.name);
|
|
267
|
+
}
|
|
268
|
+
// The always-on subscription: it's what hears the open button while the menu is closed. While a grab
|
|
269
|
+
// is active this receives nothing (the grab is the only recipient) — hence the `releaseInput` guard,
|
|
270
|
+
// which keeps a session WITHOUT `grabInput` (a stub) working off this one path.
|
|
271
|
+
const offSession = session?.onButton((e) => {
|
|
272
|
+
if (releaseInput)
|
|
273
|
+
return; // the grab handler already has it
|
|
274
|
+
onEdge(e);
|
|
275
|
+
});
|
|
276
|
+
// ── Open / close ───────────────────────────────────────────────────────────────────────────────
|
|
277
|
+
function openMenu() {
|
|
278
|
+
if (open)
|
|
279
|
+
return true;
|
|
280
|
+
if (opts.canOpen && !opts.canOpen())
|
|
281
|
+
return false;
|
|
282
|
+
if (closeTimer !== null) {
|
|
283
|
+
clearTimeout(closeTimer);
|
|
284
|
+
closeTimer = null;
|
|
285
|
+
}
|
|
286
|
+
open = true;
|
|
287
|
+
closing = false;
|
|
288
|
+
root.classList.remove("closing");
|
|
289
|
+
root.classList.add("open");
|
|
290
|
+
buildRows();
|
|
291
|
+
computeOffsets();
|
|
292
|
+
if (captureInput && session?.grabInput)
|
|
293
|
+
releaseInput = session.grabInput(onEdge);
|
|
294
|
+
if (activity)
|
|
295
|
+
session?.setActivity?.("menu");
|
|
296
|
+
// Pull keyboard focus onto the overlay so a game that also handles DOM focus can't keep it.
|
|
297
|
+
root.focus?.();
|
|
298
|
+
nav = createSpatialNav({
|
|
299
|
+
root: listEl,
|
|
300
|
+
selector: ".rydr-pause-row:not([disabled])",
|
|
301
|
+
ring: false, // the ring is styled below
|
|
302
|
+
wrap: true, // a single-axis list: DOWN past the last returns to the first
|
|
303
|
+
pointer: true, // hover and controller share one focus state
|
|
304
|
+
cue,
|
|
305
|
+
onActivate: (el) => select(rows.indexOf(el)),
|
|
306
|
+
onBack: close,
|
|
307
|
+
onFocusChange: () => placeCap(),
|
|
308
|
+
});
|
|
309
|
+
nav.focusFirst(); // land on Resume — the safe default
|
|
310
|
+
placeCap();
|
|
311
|
+
opts.onOpen?.();
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
function close() {
|
|
315
|
+
if (!open && !closing)
|
|
316
|
+
return;
|
|
317
|
+
if (closeTimer !== null)
|
|
318
|
+
return; // already closing
|
|
319
|
+
open = false;
|
|
320
|
+
closing = true;
|
|
321
|
+
// Keep the overlay mounted and lit for the exit animation; `.closing` only fades the backdrop.
|
|
322
|
+
root.classList.remove("open");
|
|
323
|
+
root.classList.add("closing");
|
|
324
|
+
releaseInput?.();
|
|
325
|
+
releaseInput = null;
|
|
326
|
+
if (activity)
|
|
327
|
+
session?.setActivity?.("playing");
|
|
328
|
+
nav?.dispose();
|
|
329
|
+
nav = null;
|
|
330
|
+
// The cap leaves with the focus ring, but the rows stay mounted for the warp-out.
|
|
331
|
+
confirmCap.el.remove();
|
|
332
|
+
cue?.("nav.back");
|
|
333
|
+
opts.onClose?.();
|
|
334
|
+
closeTimer = setTimeout(() => {
|
|
335
|
+
closeTimer = null;
|
|
336
|
+
closing = false;
|
|
337
|
+
root.classList.remove("closing");
|
|
338
|
+
listEl.textContent = "";
|
|
339
|
+
for (const cap of rowCaps)
|
|
340
|
+
cap.dispose();
|
|
341
|
+
rowCaps = [];
|
|
342
|
+
rows = [];
|
|
343
|
+
}, ANIM_MS + rows.length * STAGGER_MS + 140);
|
|
344
|
+
}
|
|
345
|
+
return {
|
|
346
|
+
root,
|
|
347
|
+
open: openMenu,
|
|
348
|
+
close,
|
|
349
|
+
toggle: () => {
|
|
350
|
+
if (open) {
|
|
351
|
+
close();
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
return openMenu();
|
|
355
|
+
},
|
|
356
|
+
isOpen: () => open,
|
|
357
|
+
setItems: (next) => {
|
|
358
|
+
items = [...next];
|
|
359
|
+
if (open) {
|
|
360
|
+
buildRows();
|
|
361
|
+
computeOffsets();
|
|
362
|
+
nav?.revalidate();
|
|
363
|
+
nav?.focusFirst();
|
|
364
|
+
placeCap();
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
setTitle: (title) => {
|
|
368
|
+
titleEl.textContent = title;
|
|
369
|
+
},
|
|
370
|
+
handleButton,
|
|
371
|
+
dispose: () => {
|
|
372
|
+
offSession?.();
|
|
373
|
+
releaseInput?.();
|
|
374
|
+
releaseInput = null;
|
|
375
|
+
// Torn down mid-open (a game unmounting its screen): hand the trainer back to gameplay, or the
|
|
376
|
+
// rider would keep riding the menu's eased resistance with no menu on screen.
|
|
377
|
+
if (open && activity)
|
|
378
|
+
session?.setActivity?.("playing");
|
|
379
|
+
open = false;
|
|
380
|
+
if (closeTimer !== null)
|
|
381
|
+
clearTimeout(closeTimer);
|
|
382
|
+
nav?.dispose();
|
|
383
|
+
nav = null;
|
|
384
|
+
for (const cap of rowCaps)
|
|
385
|
+
cap.dispose();
|
|
386
|
+
rowCaps = [];
|
|
387
|
+
confirmCap.dispose();
|
|
388
|
+
capListeners.clear();
|
|
389
|
+
root.remove();
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* The overlay's own `.rydr-pause-*` CSS — self-contained, injected once, no global styles or CSS
|
|
395
|
+
* variables required. Deliberately quotes the platform menu's numbers (the same dim, blur, radii and
|
|
396
|
+
* warp curve) so the two surfaces read as one family.
|
|
397
|
+
*
|
|
398
|
+
* `font-family: var(--rydr-pause-font, inherit)` on every text rule is load-bearing: the menu must
|
|
399
|
+
* render in the GAME's font, which it gets by inheriting from the host. The keycaps are the exception —
|
|
400
|
+
* they carry their own type from `styles.ts`, because they depict the rider's physical controller.
|
|
401
|
+
*/
|
|
402
|
+
const CSS = `
|
|
403
|
+
.rydr-pause { position: fixed; inset: 0; z-index: 1400; display: flex; justify-content: center;
|
|
404
|
+
overflow-x: hidden; overflow-y: auto; padding: clamp(40px, 7vh, 88px) 24px 96px;
|
|
405
|
+
font-family: var(--rydr-pause-font, inherit);
|
|
406
|
+
background: rgba(2, 6, 23, calc(0.82 * var(--bd, 0)));
|
|
407
|
+
-webkit-backdrop-filter: blur(calc(14px * var(--bd, 0)));
|
|
408
|
+
backdrop-filter: blur(calc(14px * var(--bd, 0)));
|
|
409
|
+
opacity: 0; pointer-events: none; visibility: hidden;
|
|
410
|
+
transition: background 0.45s ease, backdrop-filter 0.45s ease, opacity 0.35s ease; }
|
|
411
|
+
.rydr-pause:focus { outline: none; }
|
|
412
|
+
.rydr-pause.open { opacity: 1; pointer-events: auto; visibility: visible; --bd: 1; }
|
|
413
|
+
/* closing: stay visible so the rows can warp back out — only the backdrop fades */
|
|
414
|
+
.rydr-pause.closing { opacity: 1; visibility: visible; pointer-events: none; --bd: 0;
|
|
415
|
+
transition: background 0.45s ease, backdrop-filter 0.45s ease; }
|
|
416
|
+
|
|
417
|
+
.rydr-pause-inner { width: 100%; max-width: 620px; margin: auto 0; display: flex; flex-direction: column; gap: 26px; }
|
|
418
|
+
/* the game's name, in the game's font — the biggest thing on the screen */
|
|
419
|
+
.rydr-pause-title { font-family: var(--rydr-pause-font, inherit); text-align: center;
|
|
420
|
+
font-size: clamp(2.4rem, 8vw, 3.4rem); font-weight: 800; letter-spacing: -0.03em; line-height: 1;
|
|
421
|
+
color: #f2f5fb; text-shadow: 0 6px 30px rgba(0, 0, 0, 0.6); }
|
|
422
|
+
.rydr-pause-list { position: relative; display: flex; flex-direction: column; gap: 12px; }
|
|
423
|
+
|
|
424
|
+
/* a row: frosted glass, label left, shortcut right */
|
|
425
|
+
.rydr-pause-row { appearance: none; cursor: pointer; width: 100%; box-sizing: border-box;
|
|
426
|
+
display: flex; align-items: center; justify-content: space-between; gap: 18px;
|
|
427
|
+
min-height: 74px; padding: 0.9rem 1.1rem 0.9rem 1.4rem; border-radius: 18px;
|
|
428
|
+
font-family: var(--rydr-pause-font, inherit); color: #f2f5fb; text-align: left;
|
|
429
|
+
background: rgba(255, 255, 255, 0.06); border: 1px solid rgba(255, 255, 255, 0.14);
|
|
430
|
+
-webkit-backdrop-filter: blur(20px) saturate(120%);
|
|
431
|
+
backdrop-filter: blur(20px) saturate(120%);
|
|
432
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.28), inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
|
433
|
+
transition: transform 0.14s ease, border-color 0.14s ease, box-shadow 0.14s ease, background 0.14s ease;
|
|
434
|
+
will-change: transform, opacity, filter; }
|
|
435
|
+
.rydr-pause-row:hover:not([disabled]) { background: rgba(255, 255, 255, 0.1); border-color: rgba(255, 255, 255, 0.3); }
|
|
436
|
+
.rydr-pause-row:focus-visible { outline: none; }
|
|
437
|
+
.rydr-pause-row[disabled] { cursor: default; opacity: 0.45; }
|
|
438
|
+
.rydr-pause-row.danger .rydr-pause-label { color: #ff8b8b; }
|
|
439
|
+
.rydr-pause-text { display: flex; flex-direction: column; gap: 0.2rem; min-width: 0; }
|
|
440
|
+
.rydr-pause-label { font-family: var(--rydr-pause-font, inherit); font-weight: 700; font-size: 1.6rem;
|
|
441
|
+
letter-spacing: 0.01em; line-height: 1.1; overflow-wrap: anywhere; }
|
|
442
|
+
.rydr-pause-hint { font-family: var(--rydr-pause-font, inherit); font-size: 0.95rem; font-weight: 500;
|
|
443
|
+
color: rgba(238, 244, 255, 0.62); line-height: 1.25; }
|
|
444
|
+
/* right side: the "Shortcut" tag + its keycap, then the slot the confirm cap rides into */
|
|
445
|
+
.rydr-pause-right { display: flex; align-items: center; gap: 18px; flex: 0 0 auto; }
|
|
446
|
+
.rydr-pause-shortcut { display: flex; align-items: center; gap: 9px; flex: 0 0 auto; }
|
|
447
|
+
.rydr-pause-shortcut-tag { font-family: var(--rydr-pause-font, inherit); font-size: 0.7rem; font-weight: 700;
|
|
448
|
+
letter-spacing: 0.14em; text-transform: uppercase; color: rgba(238, 244, 255, 0.5); }
|
|
449
|
+
/* the slot is always there, at a FIXED width (one solo keycap), so focus moving row to row can never
|
|
450
|
+
reflow the list — only the cap inside it appears and disappears */
|
|
451
|
+
.rydr-pause-confirm { flex: 0 0 38px; height: 38px; display: flex; align-items: center; justify-content: center; }
|
|
452
|
+
|
|
453
|
+
/* roving controller focus: white ring + halo, as on the platform overlay */
|
|
454
|
+
.rydr-pause-row[nav-focused] { outline: none; transform: scale(1.02);
|
|
455
|
+
border-color: rgba(255, 255, 255, 0.9);
|
|
456
|
+
box-shadow: 0 0 0 3px #ffffff, 0 0 22px 0 rgba(255, 255, 255, 0.16), 0 22px 50px rgba(0, 0, 0, 0.55);
|
|
457
|
+
z-index: 2; }
|
|
458
|
+
|
|
459
|
+
/* the confirm keycap, sitting in the focused row's slot */
|
|
460
|
+
.rydr-pause-cap { pointer-events: none;
|
|
461
|
+
filter: drop-shadow(0 2px 3px rgba(0, 0, 0, 0.7)) drop-shadow(0 8px 16px rgba(0, 0, 0, 0.5)); }
|
|
462
|
+
|
|
463
|
+
/* ── entrance / exit: rows warp in from their own side of the list, staggered ──
|
|
464
|
+
fill: backwards (not both) so the resting frame hands control back to plain CSS and the
|
|
465
|
+
[nav-focused] scale can take over without a flash. */
|
|
466
|
+
.rydr-pause.open .rydr-pause-row { animation: rydr-pause-warp-in ${ANIM_MS}ms cubic-bezier(0.1, 0.8, 0.4, 1) backwards;
|
|
467
|
+
animation-delay: calc(var(--ri, 0) * ${STAGGER_MS}ms); }
|
|
468
|
+
.rydr-pause.open .rydr-pause-title { animation: rydr-pause-warp-in-top ${ANIM_MS}ms cubic-bezier(0.1, 0.8, 0.4, 1) backwards; }
|
|
469
|
+
.rydr-pause.closing .rydr-pause-row { animation: rydr-pause-warp-out ${ANIM_MS}ms cubic-bezier(0.6, 0, 0.9, 0.2) both;
|
|
470
|
+
animation-delay: calc(var(--i, 0) * ${STAGGER_MS}ms); }
|
|
471
|
+
.rydr-pause.closing .rydr-pause-title { animation: rydr-pause-warp-out-top ${ANIM_MS}ms cubic-bezier(0.6, 0, 0.9, 0.2) both; }
|
|
472
|
+
@keyframes rydr-pause-warp-in {
|
|
473
|
+
0% { opacity: 0; transform: translateY(calc(var(--dy, 0px) * 1.5)) scale(2.4); filter: blur(6px); }
|
|
474
|
+
55% { opacity: 1; filter: blur(0); }
|
|
475
|
+
100% { opacity: 1; transform: translateY(0) scale(1); filter: blur(0); }
|
|
476
|
+
}
|
|
477
|
+
@keyframes rydr-pause-warp-out {
|
|
478
|
+
0% { opacity: 1; transform: translateY(0) scale(1); filter: blur(0); }
|
|
479
|
+
45% { opacity: 1; filter: blur(0); }
|
|
480
|
+
100% { opacity: 0; transform: translateY(calc(var(--dy, 0px) * 1.5)) scale(2.4); filter: blur(6px); }
|
|
481
|
+
}
|
|
482
|
+
@keyframes rydr-pause-warp-in-top {
|
|
483
|
+
0% { opacity: 0; transform: translateY(-70px) scale(1.9); filter: blur(6px); }
|
|
484
|
+
55% { opacity: 1; filter: blur(0); }
|
|
485
|
+
100% { opacity: 1; transform: translateY(0) scale(1); filter: blur(0); }
|
|
486
|
+
}
|
|
487
|
+
@keyframes rydr-pause-warp-out-top {
|
|
488
|
+
0% { opacity: 1; transform: translateY(0) scale(1); filter: blur(0); }
|
|
489
|
+
45% { opacity: 1; filter: blur(0); }
|
|
490
|
+
100% { opacity: 0; transform: translateY(-70px) scale(1.9); filter: blur(6px); }
|
|
491
|
+
}
|
|
492
|
+
@media (prefers-reduced-motion: reduce) {
|
|
493
|
+
.rydr-pause.open .rydr-pause-row,
|
|
494
|
+
.rydr-pause.open .rydr-pause-title { animation: rydr-pause-fade 0.3s ease both !important; }
|
|
495
|
+
.rydr-pause.closing .rydr-pause-row,
|
|
496
|
+
.rydr-pause.closing .rydr-pause-title { animation: rydr-pause-fade 0.2s ease reverse both !important; }
|
|
497
|
+
@keyframes rydr-pause-fade { from { opacity: 0; } to { opacity: 1; } }
|
|
498
|
+
}
|
|
499
|
+
`;
|
|
500
|
+
//# sourceMappingURL=pause-menu.js.map
|