@wasm-gaming/rsdkv5-wasm 0.1.1 → 0.2.0
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 +106 -6
- package/dist/rsdkv5/app/app.css +176 -0
- package/dist/rsdkv5/app/app.js +527 -0
- package/dist/rsdkv5/app/assets/player-knuckles.svg +757 -0
- package/dist/rsdkv5/app/assets/player-sonic.svg +720 -0
- package/dist/rsdkv5/app/assets/player-sonictails.svg +1414 -0
- package/dist/rsdkv5/app/assets/player-tails.svg +979 -0
- package/dist/rsdkv5/engine-specs.js +382 -0
- package/dist/rsdkv5/rsdkv5.js +1 -1
- package/dist/rsdkv5/rsdkv5.options.d.ts +61 -13
- package/dist/rsdkv5/rsdkv5.options.d.ts.map +1 -1
- package/dist/rsdkv5/rsdkv5.options.js +35 -21
- package/dist/rsdkv5/rsdkv5.options.js.map +1 -1
- package/dist/rsdkv5/rsdkv5.sdk.d.ts +137 -32
- package/dist/rsdkv5/rsdkv5.sdk.d.ts.map +1 -1
- package/dist/rsdkv5/rsdkv5.sdk.js +474 -181
- package/dist/rsdkv5/rsdkv5.sdk.js.map +1 -1
- package/dist/rsdkv5/rsdkv5.wasm +0 -0
- package/package.json +9 -15
- package/src/app/app.css +176 -0
- package/src/app/app.js +527 -0
- package/src/app/assets/player-knuckles.svg +757 -0
- package/src/app/assets/player-sonic.svg +720 -0
- package/src/app/assets/player-sonictails.svg +1414 -0
- package/src/app/assets/player-tails.svg +979 -0
- package/src/rsdkv5.options.ts +96 -43
- package/src/rsdkv5.sdk.ts +562 -203
- package/dist/manifest.json +0 -69
- package/dist/rsdkv5/rsdkv5.manifest.d.ts +0 -4
- package/dist/rsdkv5/rsdkv5.manifest.d.ts.map +0 -1
- package/dist/rsdkv5/rsdkv5.manifest.js +0 -39
- package/dist/rsdkv5/rsdkv5.manifest.js.map +0 -1
- package/src/rsdkv5.manifest.ts +0 -44
package/README.md
CHANGED
|
@@ -1,9 +1,109 @@
|
|
|
1
1
|
# @wasm-gaming/rsdkv5-wasm
|
|
2
2
|
|
|
3
|
-
RSDKv5U (
|
|
3
|
+
RSDKv5U (Retro Software Development Kit v5 Ultimate, the Sonic Mania decompilation) compiled to
|
|
4
|
+
WebAssembly, wrapped in a self-contained SDK that implements the
|
|
5
|
+
[wasm-gaming engine contract](https://github.com/wasm-gaming/engine-specs) 1.0.
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
The engine bakes no game data: the host hands over `Data.rsdk` at runtime, so one `rsdkv5.wasm`
|
|
8
|
+
serves any install.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @wasm-gaming/rsdkv5-wasm
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The SDK carries the contract's runtime beside it (`dist/rsdkv5/engine-specs.js`) and imports it
|
|
17
|
+
relatively: it loads from any static server, with no bundler and no import map. The wasm and its
|
|
18
|
+
glue are built in CI and shipped in the GitHub Release (`rsdkv5.tar.gz`, layout `rsdkv5/rsdkv5/…`).
|
|
19
|
+
|
|
20
|
+
## Use
|
|
21
|
+
|
|
22
|
+
```js
|
|
23
|
+
import Rsdkv5SDK from './rsdkv5/rsdkv5.sdk.js';
|
|
24
|
+
|
|
25
|
+
const play = await new Rsdkv5SDK({ config: { region: -1 } }) // auto region
|
|
26
|
+
.assets({ data: () => readDataRsdk() }) // bytes, or a thunk
|
|
27
|
+
.storage({ namespace: 'sonic-mania' }) // → OPFS rsdkv5/sonic-mania/
|
|
28
|
+
.on('saves', ({ detail }) => console.log('saved', detail.files))
|
|
29
|
+
.mount(document.querySelector('#stage'))
|
|
30
|
+
.start();
|
|
31
|
+
|
|
32
|
+
play.pause('menu'); play.resume('menu'); // owner-aware; sound stops too
|
|
33
|
+
play.devMenu.getStageList(); // draw your own stage select…
|
|
34
|
+
play.devMenu.loadStage(0, 2); // …and warp
|
|
35
|
+
await play.destroy(); // save flushed, loop stopped, audio closed
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
| payload | keys |
|
|
39
|
+
| --- | --- |
|
|
40
|
+
| `config` | `devMenu`, `vsync`, `language`, `region` (-1 auto) (→ generated `Settings.ini`), `jsUrl`, `wasmUrl` |
|
|
41
|
+
| `options` | — |
|
|
42
|
+
| `assets` | `data` (`Data.rsdk`, required), `settings` (`Settings.ini`, optional — capital S, the name RSDKv5U opens) |
|
|
43
|
+
| `storage` | `namespace` (default `default`), `persist` (`'opfs'` \| `null`) |
|
|
44
|
+
|
|
45
|
+
Events: the contract's `start`, `error`, `exit`, `pause`, `resume`, plus `saves`.
|
|
46
|
+
RSDKv5 cannot restart in process: after `destroy()`, a new session needs a new page (or iframe).
|
|
47
|
+
|
|
48
|
+
## The engine's own app
|
|
49
|
+
|
|
50
|
+
A host that would rather not draw menus mounts the app shipped next to the SDK
|
|
51
|
+
([engine-specs](https://github.com/wasm-gaming/engine-specs) `EngineApp`, `UX.md`):
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
const app = await import('@wasm-gaming/rsdkv5-wasm/app'); // dist/rsdkv5/app/app.js
|
|
55
|
+
const handle = await app.mount(element, { game, storage, assets, input, exit });
|
|
56
|
+
await handle.unmount();
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
It shows a load-game screen first — craft's cards (as rsdkv4's app) on Mania's eight save slots:
|
|
60
|
+
zone, character, emeralds, lives and score; an empty slot picks the character (Mighty and Ray with
|
|
61
|
+
Plus) — plus a card to Mania's own menus (Encore, Time Attack…). A slot is started through the
|
|
62
|
+
build's bridge, which replays Mania's own save-select action, so Mania writes `SaveData.bin` itself.
|
|
63
|
+
The SDK exposes the same as `play.saves` (`list()`, `ready()`, `start(slot, character)`) and
|
|
64
|
+
`play.mute()`. Mania only loads its save once the title screen is up, so the game runs muted
|
|
65
|
+
behind the slot screen until then; a slot picked earlier starts as soon as it is ready.
|
|
66
|
+
|
|
67
|
+
Select + Start or Escape opens the pause menu: *Continuar*, *Cambiar de partida* (back to the
|
|
68
|
+
slots), the game's own screens found in the stage list (title screen, main menu, level select —
|
|
69
|
+
only those the data has) and *Volver al lanzador* (asks first). A wasm without the bridge gets
|
|
70
|
+
the game with its own menus instead of the slot screen.
|
|
71
|
+
|
|
72
|
+
`pause()` freezes the game by stopping emscripten's main loop. The stage list comes from the
|
|
73
|
+
build's own bridge (`web_devmenu_*` in `scripts/build.sh`).
|
|
74
|
+
|
|
75
|
+
## Saves live in the engine's own folder
|
|
76
|
+
|
|
77
|
+
The working dir is `/data/rsdkv5/<namespace>` (in memory), and Sonic Mania writes its user files there
|
|
78
|
+
(`SaveData.bin`, `TimeAttackDB.bin`, `ReplayDB.bin`, `Replay_*.bin`). The SDK seeds them from OPFS
|
|
79
|
+
`rsdkv5/<namespace>/` before boot and mirrors back whatever changed — every 10 s, on pause, when
|
|
80
|
+
the page is hidden and on `destroy()` — so saves survive reloads. `play.storagePath` gives the
|
|
81
|
+
folder; `storagePath(engineId, namespace)` is exported.
|
|
82
|
+
|
|
83
|
+
## Build
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
make build-sdk # TypeScript → dist/rsdkv5/ (fast, no Docker)
|
|
87
|
+
make test # type-check + tests against the built package
|
|
88
|
+
make build-wasm # RSDKv5U → WebAssembly in emscripten/emsdk:6.0.2 (Docker)
|
|
89
|
+
make demo # dist/index.html: a no-build demo (pick or drop Data.rsdk)
|
|
90
|
+
make preview # serve dist/ on :8025
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The upstream decompilation is pinned (`9dc69942`) together with its submodules, and so is the emsdk
|
|
94
|
+
image; see [docs/](docs/) for how the build is put together.
|
|
95
|
+
|
|
96
|
+
## Project Specs
|
|
97
|
+
|
|
98
|
+
- Contract 1.0 class (`Rsdkv5SDK` / `Rsdkv5Play`), self-contained ESM, engine-specs as the one
|
|
99
|
+
dependency; conformance line `RSDKV5_ENGINE` checked by `make test`.
|
|
100
|
+
- Engine-owned storage `rsdkv5/<namespace>/`, mirroring every user file the game writes.
|
|
101
|
+
- Records of decisions in [RECORD/](RECORD/).
|
|
102
|
+
|
|
103
|
+
## Todos
|
|
104
|
+
|
|
105
|
+
- [ ] Verify with a real `Data.rsdk` (save written by the game → mirrored → restored).
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
MIT for this wrapper. The upstream engine and the game data keep their own licenses.
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/* rsdkv5 app — load-game screen (craft's cards, as rsdkv4's app), boot screen and pause menu.
|
|
2
|
+
Tokens come from engine-specs (UI_TOKENS, `--wg-*`) when the host sets them; the
|
|
3
|
+
fallbacks are the same values, so the app looks right on a bare page too. */
|
|
4
|
+
|
|
5
|
+
.rsdk-app {
|
|
6
|
+
--ring: var(--wg-focus-ring, 0 0 0 3px #fff);
|
|
7
|
+
--surface: var(--wg-surface, rgb(18 20 28 / 0.78));
|
|
8
|
+
--line: var(--wg-line, rgb(255 255 255 / 0.12));
|
|
9
|
+
--text: var(--wg-text, #f4f6fb);
|
|
10
|
+
--muted: var(--wg-muted, #8e97a9);
|
|
11
|
+
--danger: var(--wg-danger, #f87171);
|
|
12
|
+
--ui: var(--wg-font-ui, 'Inter Variable', 'Inter', system-ui, sans-serif);
|
|
13
|
+
--display: var(--wg-font-display, 'Unbounded Variable', 'Unbounded', system-ui, sans-serif);
|
|
14
|
+
--ease: var(--wg-ease, cubic-bezier(0.2, 0.8, 0.2, 1));
|
|
15
|
+
position: absolute;
|
|
16
|
+
inset: 0;
|
|
17
|
+
overflow: hidden;
|
|
18
|
+
background: #000;
|
|
19
|
+
color: var(--text);
|
|
20
|
+
font-family: var(--ui);
|
|
21
|
+
user-select: none;
|
|
22
|
+
-webkit-tap-highlight-color: transparent;
|
|
23
|
+
}
|
|
24
|
+
.rsdk-app * { box-sizing: border-box; }
|
|
25
|
+
.rsdk-stage { position: absolute; inset: 0; display: grid; place-items: center; }
|
|
26
|
+
.rsdk-stage canvas { outline: none; }
|
|
27
|
+
.rsdk-overlay { position: absolute; inset: 0; display: grid; place-items: center; pointer-events: none; }
|
|
28
|
+
.rsdk-overlay > * { pointer-events: auto; }
|
|
29
|
+
.rsdk-app[data-screen='game'] .rsdk-overlay { display: none; }
|
|
30
|
+
|
|
31
|
+
/* Everything but the game sits on a backdrop that keeps the game visible underneath. */
|
|
32
|
+
.rsdk-app:not([data-screen='game']) .rsdk-overlay {
|
|
33
|
+
background:
|
|
34
|
+
radial-gradient(120% 80% at 50% 0%, color-mix(in srgb, var(--accent) 38%, transparent), transparent 70%),
|
|
35
|
+
rgb(4 6 12 / 0.72);
|
|
36
|
+
backdrop-filter: blur(14px) saturate(1.2);
|
|
37
|
+
-webkit-backdrop-filter: blur(14px) saturate(1.2);
|
|
38
|
+
}
|
|
39
|
+
.rsdk-app[data-screen='boot'] .rsdk-overlay { background-color: rgb(4 6 12 / 0.9); }
|
|
40
|
+
|
|
41
|
+
.kicker { margin: 0; font: 600 0.72rem var(--ui); letter-spacing: 0.22em; text-transform: uppercase; color: var(--muted); }
|
|
42
|
+
.rsdk-app h2 { margin: 0; font: 700 clamp(1.4rem, 3vw, 2.1rem) var(--display); letter-spacing: -0.02em; }
|
|
43
|
+
|
|
44
|
+
/* ---- boot ---- */
|
|
45
|
+
.rsdk-boot { display: grid; justify-items: center; gap: 22px; }
|
|
46
|
+
.rsdk-boot img { width: min(260px, 50vw); border-radius: 22px; box-shadow: 0 30px 80px -30px #000; animation: rsdk-breathe 2.4s var(--ease) infinite alternate; }
|
|
47
|
+
.rsdk-boot p { margin: 0; color: var(--muted); font-weight: 500; }
|
|
48
|
+
@keyframes rsdk-breathe { from { opacity: 0.55; transform: scale(0.98); } to { opacity: 1; transform: none; } }
|
|
49
|
+
|
|
50
|
+
/* ---- load game: craft's slot cards (src/craft/views/GameLauncher.html) ---- */
|
|
51
|
+
.rsdk-slots { width: min(1120px, 100%); padding: 0 clamp(16px, 4vw, 48px); display: grid; gap: clamp(22px, 4vh, 40px); }
|
|
52
|
+
.rsdk-slots header { display: grid; gap: 6px; }
|
|
53
|
+
.slot-cards {
|
|
54
|
+
--w: clamp(150px, 13vw, 190px);
|
|
55
|
+
display: flex; justify-content: center; align-items: flex-start; gap: clamp(12px, 1.6vw, 24px);
|
|
56
|
+
}
|
|
57
|
+
.slot-card {
|
|
58
|
+
flex: none; width: var(--w); height: calc(var(--w) * 220 / 170);
|
|
59
|
+
display: flex; flex-direction: column; justify-content: space-between; gap: 0.4rem;
|
|
60
|
+
padding: 0.75rem; border-radius: 0.5rem; border: 2px solid #000; background: #333; color: #fff;
|
|
61
|
+
font-family: system-ui, sans-serif;
|
|
62
|
+
transition: transform 0.2s var(--ease), box-shadow 0.2s var(--ease);
|
|
63
|
+
}
|
|
64
|
+
.slot-card.is-focused { box-shadow: var(--ring), 0 24px 50px -20px #000; transform: translateY(-4px); }
|
|
65
|
+
.slot-card.is-nosave {
|
|
66
|
+
height: calc(var(--w) * 130 / 170); justify-content: center; align-items: center;
|
|
67
|
+
font-weight: 700; cursor: pointer;
|
|
68
|
+
}
|
|
69
|
+
.slot-card.is-nosave:hover:not(:active) { background: #222; }
|
|
70
|
+
.slot-card img { display: block; }
|
|
71
|
+
|
|
72
|
+
.slot-play {
|
|
73
|
+
all: unset; box-sizing: border-box; flex-shrink: 0; width: 100%; height: 34px; text-align: center;
|
|
74
|
+
border-radius: 4px; background: royalblue; color: #fff; font-weight: 700; text-shadow: 0 0 2px #000; cursor: pointer;
|
|
75
|
+
}
|
|
76
|
+
.slot-play:hover:not(:active), .slot-card.is-focused .slot-play { background: blue; }
|
|
77
|
+
|
|
78
|
+
/* the character picker: white square, the character over two drifting colour bands */
|
|
79
|
+
.player-choose {
|
|
80
|
+
all: unset; box-sizing: border-box; position: relative; width: 100%; aspect-ratio: 1; padding: 1rem 0.5rem 0;
|
|
81
|
+
display: flex; align-items: center; justify-content: center; overflow: hidden;
|
|
82
|
+
background: #fff; cursor: pointer; transition: padding 0.05s linear;
|
|
83
|
+
}
|
|
84
|
+
.player-choose::before, .player-choose::after { content: ''; position: absolute; inset: 0 1rem 0 3rem; background: rgb(180 180 180 / 0.2); }
|
|
85
|
+
.player-choose[data-player='sonic']::after { background: rgb(13 122 233 / 0.2); left: 30%; right: 20%; }
|
|
86
|
+
.player-choose[data-player='tails']::after { background: rgb(241 176 0 / 0.2); left: 40%; right: 10%; animation-delay: -5s; }
|
|
87
|
+
.player-choose[data-player='knuckles']::after { background: rgb(255 20 0 / 0.2); left: 20%; right: 30%; animation-delay: -10s; }
|
|
88
|
+
.player-choose[data-player='sonictails']::after { background: rgb(160 32 240 / 0.2); left: 20%; right: 70%; animation-delay: -15s; }
|
|
89
|
+
.slot-card:hover .player-choose, .slot-card.is-focused .player-choose { padding: 0; }
|
|
90
|
+
.player-choose:hover::before, .slot-card.is-focused .player-choose::before { animation: rsdk-player-bg 20s linear infinite; }
|
|
91
|
+
.player-choose:hover::after, .slot-card.is-focused .player-choose::after { animation: rsdk-player-bg 30s linear reverse infinite; }
|
|
92
|
+
.player-choose img { position: relative; z-index: 1; max-width: 100%; max-height: 100%; }
|
|
93
|
+
@keyframes rsdk-player-bg {
|
|
94
|
+
0% { left: 30%; right: 20%; } 10% { left: 50%; right: 50%; } 20% { left: 10%; right: 20%; }
|
|
95
|
+
30% { left: 60%; right: 40%; } 40% { left: 70%; right: 10%; } 50% { left: 20%; right: 80%; }
|
|
96
|
+
60% { left: 0; right: 60%; } 70% { left: 40%; right: 10%; } 80% { left: 10%; right: 70%; }
|
|
97
|
+
90% { left: 80%; right: 0; } 100% { left: 30%; right: 20%; }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* a saved slot */
|
|
101
|
+
.slot-card.is-saved { gap: 0.25rem; }
|
|
102
|
+
.stage-shot { position: relative; width: 100%; flex-shrink: 0; }
|
|
103
|
+
.stage-badge {
|
|
104
|
+
width: 100%; aspect-ratio: 16 / 9; border-radius: 4px; overflow: hidden;
|
|
105
|
+
border: 1px solid rgb(255 255 255 / 0.2); background: linear-gradient(160deg, #1b2a4a, #080d1c);
|
|
106
|
+
}
|
|
107
|
+
.stage-badge img { width: 100%; height: 100%; object-fit: cover; filter: brightness(0.9); image-rendering: pixelated; }
|
|
108
|
+
.zone-tag {
|
|
109
|
+
position: absolute; top: 4px; left: 4px; padding: 1px 4px; border-radius: 3px; background: rgb(0 0 0 / 0.72);
|
|
110
|
+
color: #fbbf24; font-size: 0.5rem; font-weight: 800; letter-spacing: 0.06em; line-height: 1.4;
|
|
111
|
+
}
|
|
112
|
+
.player-icon { position: absolute; right: 0; bottom: -1rem; pointer-events: none; }
|
|
113
|
+
.player-icon img { height: calc(var(--w) * 64 / 170); width: auto; filter: drop-shadow(0 2px 4px rgb(0 0 0 / 0.85)); }
|
|
114
|
+
.zone-title {
|
|
115
|
+
height: 2.3em; padding-right: 26%; font-weight: 800; font-size: 0.72rem; color: #fbbf24; text-transform: uppercase;
|
|
116
|
+
letter-spacing: 0.4px; text-shadow: 0 1px 3px rgb(0 0 0 / 0.9); line-height: 1.15;
|
|
117
|
+
}
|
|
118
|
+
.emeralds { display: flex; justify-content: center; gap: 3px; flex-shrink: 0; }
|
|
119
|
+
.emeralds i { width: 11px; height: 11px; background: var(--gem); clip-path: polygon(50% 0, 100% 38%, 50% 100%, 0 38%); opacity: 0.2; }
|
|
120
|
+
.emeralds i.on { opacity: 1; filter: drop-shadow(0 0 3px rgb(255 255 255 / 0.4)); }
|
|
121
|
+
.stats-grid { display: flex; flex-wrap: wrap; justify-content: center; gap: 0.2rem 0.4rem; font-size: 0.68rem; color: #e2e8f0; }
|
|
122
|
+
.stats-grid span {
|
|
123
|
+
display: inline-flex; align-items: center; gap: 0.25rem; padding: 0.1rem 0.3rem; border-radius: 3px;
|
|
124
|
+
background: rgb(0 0 0 / 0.4); border: 1px solid rgb(255 255 255 / 0.1); font-variant-numeric: tabular-nums;
|
|
125
|
+
}
|
|
126
|
+
.stats-grid svg { width: 10px; height: 10px; }
|
|
127
|
+
|
|
128
|
+
/* Mania: no zone portraits here — the zone's name on a gradient; Mighty and Ray as badges */
|
|
129
|
+
.slot-cards { --w: clamp(140px, 12vw, 190px); justify-content: flex-start; overflow-x: auto; padding: 16px 8px 18px; scrollbar-width: none; scroll-behavior: smooth; }
|
|
130
|
+
.slot-cards::-webkit-scrollbar { display: none; }
|
|
131
|
+
.stage-badge.is-mania { display: grid; place-items: end start; padding: 6px 10px; background: linear-gradient(160deg, color-mix(in srgb, var(--accent) 70%, #0b1a3a), #080d1c); }
|
|
132
|
+
.stage-badge.is-mania span { font: 900 clamp(1.1rem, 1.6vw, 1.5rem)/1 system-ui, sans-serif; letter-spacing: 0.06em; color: #fff; text-shadow: 0 2px 3px #000; }
|
|
133
|
+
.slot-card .zone-title { padding-right: 34%; }
|
|
134
|
+
.player-badge {
|
|
135
|
+
display: grid; place-items: center; width: 64%; aspect-ratio: 1; border-radius: 50%;
|
|
136
|
+
font: 900 2.4rem system-ui, sans-serif; color: #fff; text-shadow: 0 2px 4px rgb(0 0 0 / 0.6);
|
|
137
|
+
background: radial-gradient(circle at 35% 30%, #fff6, transparent 60%), var(--badge, #6b7280);
|
|
138
|
+
}
|
|
139
|
+
.player-badge[data-char='4'] { --badge: #dc2626; }
|
|
140
|
+
.player-badge[data-char='5'] { --badge: #ca8a04; }
|
|
141
|
+
.player-icon .player-badge { width: calc(var(--w) * 46 / 170); font-size: 1rem; }
|
|
142
|
+
.player-choose[data-player='badge']::after { background: rgb(220 38 38 / 0.2); left: 25%; right: 25%; }
|
|
143
|
+
.rsdk-app[data-screen='slots'] .rsdk-overlay { background-color: rgb(4 6 12 / 0.9); }
|
|
144
|
+
|
|
145
|
+
/* ---- pause & confirm ---- */
|
|
146
|
+
.rsdk-panel {
|
|
147
|
+
width: min(440px, 92vw); display: grid; gap: 10px; padding: 26px; border-radius: 28px;
|
|
148
|
+
background: rgb(20 22 31 / 0.9); border: 1px solid var(--line); box-shadow: 0 40px 100px -30px #000;
|
|
149
|
+
}
|
|
150
|
+
.rsdk-panel h2 { margin-bottom: 8px; }
|
|
151
|
+
.rsdk-panel .text { margin: 0 0 8px; color: var(--muted); line-height: 1.5; }
|
|
152
|
+
.item {
|
|
153
|
+
all: unset; box-sizing: border-box; display: grid; gap: 2px; padding: 13px 16px; border-radius: 14px;
|
|
154
|
+
background: rgb(255 255 255 / 0.05); font-weight: 600; cursor: pointer; transition: background 0.15s, box-shadow 0.15s;
|
|
155
|
+
}
|
|
156
|
+
.item small { font-weight: 500; font-size: 0.78rem; color: var(--muted); }
|
|
157
|
+
.item.is-focused { background: #fff; color: #0b0d12; box-shadow: var(--ring); }
|
|
158
|
+
.item.is-focused small { color: #4b5563; }
|
|
159
|
+
.item.is-danger.is-focused { background: var(--danger); color: #1a0505; box-shadow: 0 0 0 3px var(--danger); }
|
|
160
|
+
.rsdk-panel .row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
|
161
|
+
.rsdk-panel .row .item { text-align: center; }
|
|
162
|
+
.rsdk-confirm { position: absolute; inset: 0; display: grid; place-items: center; background: rgb(0 0 0 / 0.55); }
|
|
163
|
+
|
|
164
|
+
/* ---- hints ---- */
|
|
165
|
+
.rsdk-hints { display: flex; flex-wrap: wrap; gap: 8px 18px; justify-content: center; color: var(--muted); font-size: 0.78rem; font-weight: 500; }
|
|
166
|
+
.rsdk-hints span { display: inline-flex; align-items: center; gap: 7px; }
|
|
167
|
+
.rsdk-hints kbd { display: inline-grid; place-items: center; min-width: 22px; height: 22px; padding: 0 6px; border-radius: 11px; background: rgb(255 255 255 / 0.12); color: var(--text); font: 700 0.68rem var(--ui); }
|
|
168
|
+
.rsdk-panel .rsdk-hints { justify-content: flex-start; margin-top: 6px; }
|
|
169
|
+
|
|
170
|
+
@media (max-width: 960px) {
|
|
171
|
+
.slot-cards { justify-content: flex-start; overflow-x: auto; padding: 16px 6px 18px; margin: 0 -6px; scrollbar-width: none; scroll-snap-type: x mandatory; }
|
|
172
|
+
.slot-card { scroll-snap-align: center; }
|
|
173
|
+
}
|
|
174
|
+
@media (prefers-reduced-motion: reduce) {
|
|
175
|
+
.slot-card, .player-choose, .player-choose::before, .player-choose::after, .rsdk-boot img { transition: none; animation: none; }
|
|
176
|
+
}
|