@joystyk/arcade 0.0.0-stage → 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/CHANGELOG.md ADDED
@@ -0,0 +1,21 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - bb095c7: Add shared real-time input, pause, pace records and ghost races, plus a WebGL 2 renderer
8
+ lifecycle with context recovery and adaptive quality. Launch daily fixed-step simulations
9
+ from published configs and retain their puzzle identity through saved games.
10
+
11
+ ### Patch Changes
12
+
13
+ - Updated dependencies [bb095c7]
14
+ - Updated dependencies [bb095c7]
15
+ - @joystyk/contracts@0.2.0
16
+ - @joystyk/session@0.2.0
17
+ - @joystyk/kit@0.2.0
18
+
19
+ ## 0.1.0
20
+
21
+ Initial shared arcade kit (host policy).
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Joystyk contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/NOTICE ADDED
@@ -0,0 +1,36 @@
1
+ Third-party material in the Joystyk library release
2
+
3
+ This notice preserves third-party attribution. Original Joystyk code in the 17
4
+ platform libraries under packages/ uses the MIT license in packages/LICENSE.
5
+ That choice does not license the private applications, games or other repository
6
+ material, and does not replace the third-party terms below.
7
+
8
+ @joystyk/contracts
9
+ ------------------
10
+ Standard Schema v1 type definitions from @standard-schema/spec 1.1.0:
11
+ https://github.com/standard-schema/standard-schema
12
+ https://standardschema.dev
13
+ Copyright (c) 2024 Colin McDonnell. MIT License.
14
+ The complete license and source/change notes accompany the distributed
15
+ dist/standard-schema.d.ts definitions. Changes: individually exported declarations
16
+ and formatting; the declarations themselves are unchanged.
17
+
18
+ @joystyk/identity and @joystyk/content
19
+ -----------------------------------
20
+ Word blocklists derived from List of Dirty, Naughty, Obscene, and Otherwise Bad
21
+ Words, copyright Shutterstock, Inc. and contributors:
22
+ https://github.com/LDNOOBW/List-of-Dirty-Naughty-Obscene-and-Otherwise-Bad-Words
23
+ Licensed under Creative Commons Attribution 4.0 International:
24
+ https://creativecommons.org/licenses/by/4.0/
25
+ Identity uses the English list at commit
26
+ 5faf2ba42d7b1c0977169ec3611df25a3c08eb13. Changes include reducing or dropping
27
+ phrases, dropping words with everyday meanings, substituting stems for inflections
28
+ and adding entries. Identity's README and content's lists/blocklist.txt retain
29
+ their source-specific provenance and changes.
30
+
31
+ @joystyk/content
32
+ ---------------
33
+ SCOWL-derived common, vocabulary and ranked word lists use SCOWL 2020.12.07.
34
+ Their complete source licenses and attribution accompany the package in
35
+ lists/SCOWL-COPYRIGHT. Word-list transformations and build provenance are described
36
+ in the package README. Preserve that copyright file when redistributing the lists.
package/README.md CHANGED
@@ -1,3 +1,24 @@
1
- # Temporary Holding Version
1
+ # @joystyk/arcade
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Shared browser helpers for real-time games. It has no Three.js dependency. The platform's
4
+ fixed-step runner remains the authority; these helpers send intentions and draw its snapshots.
5
+
6
+ - `bindInput` maps keys, swipe/tap/drag and standard gamepads to named actions. Printable-key
7
+ shortcuts follow the player's preference; held actions release on blur and disconnect.
8
+ - `bindHold` gives native buttons pointer, keyboard and switch activation. `bindHoldPad`
9
+ delegates those events for pads rendered by Gyral; `bindDrag` projects absolute pointer
10
+ movement for paddles. Both share the owning game’s abort signal.
11
+ - `bindPause` pauses on hide, blur and page hide; explicit resume waits one second. Its
12
+ injected `after` port makes countdown tests deterministic.
13
+ - `createRecords` stores pace and best score/replay through the game's `GameStorage`.
14
+ Another rules version discards the replay and retains the score.
15
+ - `createGhost` owns an independent state and RNG. Call `advance(liveTick)` for the ghost view;
16
+ it never dispatches to the live runner. Inputs use the runner's zero-based tick convention.
17
+ - `mountArcade` combines the Gyral HUD with these helpers. A game supplies its status, input
18
+ mapping, score and a lazy renderer factory. Daily locks pace and omits ghost races.
19
+ - `createControlDrivers`, `resumeAfter`, `bindPagePause`, `swipeDirection` and the direction
20
+ maps let existing games adopt the helpers without changing their public controls or rules.
21
+
22
+ Dispose every returned binding when its game leaves the page. Games import this package from
23
+ `ui/` or `render/`. The design and performance budgets are in
24
+ host policy (Joystyk host reference).
package/dist/game.d.ts ADDED
@@ -0,0 +1,33 @@
1
+ import type { AnyFixedStepSimulation, AnyGameSession, Json, PlatformApi } from '@joystyk/contracts';
2
+ import { type InputAction } from './input.js';
3
+ import { type Pace } from './records.js';
4
+ export type ArcadeView = {
5
+ phase: string;
6
+ pace: Pace;
7
+ daily: boolean;
8
+ course: number;
9
+ };
10
+ export type ArcadeDrawing<View> = {
11
+ draw(view: View, alpha: number, ghost?: View | null): void;
12
+ graphics(value: 'auto' | 'low' | 'medium' | 'high'): void;
13
+ dispose(): void;
14
+ available?: boolean;
15
+ };
16
+ export type ArcadeGameOptions<View extends ArcadeView> = {
17
+ gameId: string;
18
+ title: string;
19
+ help: string;
20
+ simulation: AnyFixedStepSimulation;
21
+ score(view: View): number;
22
+ status(view: View): string;
23
+ action(action: InputAction, on: boolean, view: View): readonly Json[];
24
+ drag?(x: number, y: number): Json;
25
+ gesture: 'swipe' | 'tap' | 'drag';
26
+ ghost?: boolean;
27
+ alignment?: boolean;
28
+ eventLine?(event: Json, view: View): string | null;
29
+ render(host: HTMLElement, pause: () => void, announce: (text: string) => void): Promise<ArcadeDrawing<View>>;
30
+ };
31
+ export declare function mountArcade<View extends ArcadeView>(slot: HTMLElement, anySession: AnyGameSession, platform: PlatformApi, o: ArcadeGameOptions<View>): {
32
+ dispose(): void;
33
+ };
package/dist/game.js ADDED
@@ -0,0 +1,334 @@
1
+ import { bindHold, bindInput } from "./input.js";
2
+ import { bindPause } from "./pause.js";
3
+ import { createGhost, createRecords } from "./records.js";
4
+ import { ArcadePanel, panelDrivers } from "./panel.js";
5
+ //#region src/game.ts
6
+ var CSS = `:host{display:block;font:1rem/1.5 system-ui;color:var(--game-ink,CanvasText);container-type:inline-size}*{box-sizing:border-box}[hidden]{display:none!important}.layout{display:grid;gap:1rem;padding:clamp(.75rem,2cqi,1.5rem);max-inline-size:72rem;margin-inline:auto}.viewport{position:relative;block-size:clamp(18rem,65svh,42rem);background:#10232b;border-radius:1rem;overflow:hidden;touch-action:none;outline-offset:4px}.viewport>canvas{display:block;inline-size:100%;block-size:100%}.viewport>p,.viewport>button{position:absolute;inset-inline:1rem;inset-block-start:35%;background:Canvas;color:CanvasText;padding:1rem}.viewport>button{inset-block-start:60%}.viewport>output{position:absolute;inset:0 auto auto 0;background:#000c;color:white;font:12px monospace;padding:.25rem;pointer-events:none}.actions,.pad{display:flex;gap:.5rem;flex-wrap:wrap}.pad{margin-block-start:1rem}.pad[hidden]{display:none}button,select,summary{font:inherit;min-block-size:44px;padding:.55rem .8rem}button,select{border:1px solid currentColor;border-radius:.45rem;color:inherit;background:var(--game-surface,Canvas)}button{cursor:pointer}button:disabled{opacity:.55;cursor:default}button:focus-visible,select:focus-visible,summary:focus-visible,.viewport:focus-visible{outline:3px solid var(--game-accent,currentColor);outline-offset:3px}h2{margin:0;font-size:clamp(1.4rem,4cqi,2rem)}.status{font-variant-numeric:tabular-nums}fieldset{border:0;padding:0;margin-block:1rem}label{display:flex;align-items:center;gap:.5rem;min-block-size:44px}input{inline-size:1.25rem;block-size:1.25rem}summary{cursor:pointer}.pad button{flex:1;touch-action:none;user-select:none}.help{max-inline-size:65ch}@container(width>=52rem){.layout{grid-template-columns:minmax(14rem,.7fr) minmax(0,1.5fr);align-items:start}}`;
7
+ function mountArcade(slot, anySession, platform, o) {
8
+ const session = anySession;
9
+ const host = document.createElement("div"), root = host.attachShadow({ mode: "open" }), sheet = new CSSStyleSheet();
10
+ sheet.replaceSync(CSS);
11
+ root.adoptedStyleSheets = [sheet];
12
+ root.innerHTML = "<div class=\"layout\"><div class=\"controls\"></div><section><div class=\"viewport\" tabindex=\"0\" role=\"group\" aria-roledescription=\"game board\"></div><div class=\"pad\"></div><p class=\"help\"></p></section></div>";
13
+ slot.append(host);
14
+ const viewport = root.querySelector(".viewport"), pad = root.querySelector(".pad");
15
+ viewport.setAttribute("aria-label", o.title);
16
+ root.querySelector(".help").textContent = o.help;
17
+ const controls = new ArcadePanel();
18
+ const records = createRecords(platform.storage, o.gameId, o.simulation), listeners = /* @__PURE__ */ new Set();
19
+ let best = {
20
+ score: 0,
21
+ replay: null
22
+ }, drawing = null, ghost = null, disposed = false, loaded = false, resuming = false, lastTick = -1, lastPhase = "ready", lastPace = session.get().view.pace, lastHud = "", padShown = o.gameId === "tower-stacker" || matchMedia("(pointer:coarse)").matches, tone = false, autoFire = o.gesture === "drag" && matchMedia("(pointer:coarse)").matches, graphics = "auto";
23
+ const send = (input) => session.dispatch({
24
+ kind: "input",
25
+ input
26
+ });
27
+ const announce = (text) => platform.announce(text, "polite");
28
+ const pause = bindPause({
29
+ pause: () => {
30
+ for (const action of [
31
+ "left",
32
+ "right",
33
+ "up",
34
+ "main"
35
+ ]) for (const input of o.action(action, false, session.get().view)) send(input);
36
+ if (o.drag !== void 0) send(o.drag(0, 0));
37
+ session.dispatch({ kind: "pause" });
38
+ },
39
+ resume: () => {
40
+ session.dispatch({ kind: "resume" });
41
+ if (o.gesture === "drag") send({
42
+ type: "fire",
43
+ on: autoFire
44
+ });
45
+ },
46
+ announce,
47
+ changed: (value) => {
48
+ resuming = value;
49
+ publish();
50
+ }
51
+ });
52
+ const publish = () => {
53
+ if (disposed) return;
54
+ const snapshot = session.get(), v = snapshot.view;
55
+ const hud = {
56
+ title: o.title,
57
+ status: resuming ? "Resuming in one second." : snapshot.paused ? "Paused" : o.status(v),
58
+ phase: v.phase,
59
+ paused: snapshot.paused,
60
+ resuming,
61
+ pace: v.pace,
62
+ daily: v.daily,
63
+ best: Math.max(best.score, o.score(v)),
64
+ race: o.ghost === true && best.replay !== null,
65
+ pad: padShown,
66
+ tone,
67
+ autoFire,
68
+ graphics,
69
+ loaded,
70
+ alignment: o.alignment === true,
71
+ shooter: o.gesture === "drag"
72
+ };
73
+ const key = JSON.stringify(hud);
74
+ if (key !== lastHud) {
75
+ lastHud = key;
76
+ for (const listener of listeners) listener(hud);
77
+ }
78
+ pad.hidden = !padShown && !(o.gesture === "drag" && !autoFire);
79
+ if (drawing !== null) {
80
+ const g = ghost?.advance(snapshot.tick);
81
+ drawing.draw(v, session.alpha(), g ?? null);
82
+ }
83
+ };
84
+ const chooseBest = async (pace) => {
85
+ const found = await records.best(pace);
86
+ if (!disposed && session.get().view.pace === pace) {
87
+ best = found;
88
+ publish();
89
+ }
90
+ };
91
+ const emit = (action, on) => {
92
+ if (disposed || !loaded || drawing === null || drawing.available === false) return;
93
+ const snap = session.get();
94
+ if (action === "auxiliary") {
95
+ if (on && o.gesture === "drag") {
96
+ autoFire = !autoFire;
97
+ send({
98
+ type: "fire",
99
+ on: autoFire
100
+ });
101
+ announce(`Automatic fire ${autoFire ? "on" : "off"}.`);
102
+ publish();
103
+ }
104
+ return;
105
+ }
106
+ if (action === "pause") {
107
+ if (on) {
108
+ if (snap.paused) pause.resume();
109
+ else pause.pause();
110
+ }
111
+ return;
112
+ }
113
+ if (snap.paused) {
114
+ if (snap.view.phase === "ready") session.dispatch({ kind: "resume" });
115
+ else return;
116
+ }
117
+ if (o.gesture === "drag" && action === "main" && snap.view.phase !== "ready" && autoFire) {
118
+ send({
119
+ type: "fire",
120
+ on: true
121
+ });
122
+ return;
123
+ }
124
+ for (const input of o.action(action, on, snap.view)) send(input);
125
+ };
126
+ const stops = [];
127
+ const perform = (a) => {
128
+ const snap = session.get(), v = snap.view;
129
+ switch (a.type) {
130
+ case "primary":
131
+ if (v.phase === "ready") {
132
+ emit("main", true);
133
+ emit("main", false);
134
+ } else if (snap.paused) pause.resume();
135
+ else pause.pause();
136
+ break;
137
+ case "pace":
138
+ if (v.phase === "ready" && !v.daily) {
139
+ send({
140
+ type: "pace",
141
+ pace: a.pace
142
+ });
143
+ best = {
144
+ score: 0,
145
+ replay: null
146
+ };
147
+ ghost = null;
148
+ records.remember(a.pace).catch(() => {});
149
+ chooseBest(a.pace);
150
+ }
151
+ break;
152
+ case "race":
153
+ if (v.phase === "ready" && !v.daily && best.replay !== null) {
154
+ ghost = createGhost(o.simulation, best.replay);
155
+ const initial = ghost?.advance(0);
156
+ if (initial !== void 0) {
157
+ send({
158
+ type: "course",
159
+ course: initial.course
160
+ });
161
+ emit("main", true);
162
+ }
163
+ }
164
+ break;
165
+ case "graphics":
166
+ graphics = a.value;
167
+ drawing?.graphics(graphics);
168
+ platform.storage.set("arcade.graphics", graphics).catch(() => {});
169
+ break;
170
+ case "pad":
171
+ padShown = !padShown;
172
+ platform.storage.set("arcade.pad", padShown).catch(() => {});
173
+ break;
174
+ case "tone":
175
+ tone = !tone;
176
+ break;
177
+ case "fire":
178
+ autoFire = !autoFire;
179
+ send({
180
+ type: "fire",
181
+ on: autoFire
182
+ });
183
+ }
184
+ publish();
185
+ };
186
+ controls.drivers = panelDrivers((emit) => {
187
+ listeners.add(emit);
188
+ lastHud = "";
189
+ publish();
190
+ return () => listeners.delete(emit);
191
+ }, perform);
192
+ root.querySelector(".controls").append(controls);
193
+ for (const [action, label] of [
194
+ ["left", "Left"],
195
+ ["up", "Forward / Thrust"],
196
+ ["right", "Right"],
197
+ ["down", "Back"],
198
+ ["main", o.gameId === "tower-stacker" ? "Drop" : o.gesture === "drag" ? "Fire" : "Hop"]
199
+ ]) {
200
+ const button = document.createElement("button");
201
+ button.type = "button";
202
+ button.textContent = label;
203
+ pad.append(button);
204
+ stops.push(bindHold(button, (on) => emit(action, on)));
205
+ button.addEventListener("click", (e) => {
206
+ if (e.detail === 0) {
207
+ emit(action, true);
208
+ emit(action, false);
209
+ }
210
+ });
211
+ }
212
+ stops.push(bindInput({
213
+ surface: viewport,
214
+ keys: {
215
+ arrowup: "up",
216
+ arrowdown: "down",
217
+ arrowleft: "left",
218
+ arrowright: "right",
219
+ w: "up",
220
+ s: "down",
221
+ a: "left",
222
+ d: "right",
223
+ " ": "main",
224
+ enter: "main",
225
+ f: "auxiliary",
226
+ p: "pause",
227
+ escape: "pause"
228
+ },
229
+ shortcuts: () => platform.preferences.get().singleKeyShortcuts,
230
+ emit,
231
+ announce,
232
+ gesture: o.gesture,
233
+ drag: (x, y) => {
234
+ if (loaded && !session.get().paused && o.drag !== void 0) send(o.drag(x, y));
235
+ }
236
+ }));
237
+ stops.push(session.subscribe((snap) => {
238
+ const v = snap.view;
239
+ if (v.pace !== lastPace) {
240
+ lastPace = v.pace;
241
+ chooseBest(v.pace);
242
+ }
243
+ if (lastPhase === "ready" && v.phase !== "ready") {
244
+ viewport.focus({ preventScroll: true });
245
+ if (autoFire) send({
246
+ type: "fire",
247
+ on: true
248
+ });
249
+ }
250
+ if (v.phase === "over" && lastPhase !== "over" && !v.daily) {
251
+ records.record(v.pace, o.score(v), o.ghost === true ? session.replay() : null).then(() => chooseBest(v.pace)).catch(() => {});
252
+ announce(o.status(v));
253
+ }
254
+ lastPhase = v.phase;
255
+ if (lastTick !== snap.tick) {
256
+ lastTick = snap.tick;
257
+ for (const e of snap.events) {
258
+ const cue = {
259
+ began: "start",
260
+ hopped: "move",
261
+ signal: "warning",
262
+ milestone: "level",
263
+ placed: "hit",
264
+ regrew: "power",
265
+ missed: "lose",
266
+ wave: "level",
267
+ lifeLost: "life",
268
+ extraLife: "life",
269
+ droneIn: "warning",
270
+ ended: "lose"
271
+ }[e.type];
272
+ if (cue !== void 0) platform.audio?.play(cue);
273
+ if (o.alignment && tone && e.type === "aligned") platform.audio?.play("tick");
274
+ const line = o.eventLine?.(e, v);
275
+ if (line !== void 0 && line !== null) announce(line);
276
+ else if ([
277
+ "wave",
278
+ "lifeLost",
279
+ "extraLife",
280
+ "droneIn",
281
+ "regrew",
282
+ "milestone",
283
+ "signal"
284
+ ].includes(e.type)) announce(o.status(v));
285
+ }
286
+ }
287
+ publish();
288
+ }));
289
+ Promise.all([
290
+ records.pace(),
291
+ platform.storage.get("arcade.pad").catch(() => null),
292
+ platform.storage.get("arcade.graphics").catch(() => null)
293
+ ]).then(async ([pace, padValue, quality]) => {
294
+ if (disposed) return;
295
+ if (typeof padValue === "boolean") padShown = padValue;
296
+ if ([
297
+ "auto",
298
+ "low",
299
+ "medium",
300
+ "high"
301
+ ].includes(String(quality))) graphics = quality;
302
+ const v = session.get().view;
303
+ if (!v.daily && v.phase === "ready") send({
304
+ type: "pace",
305
+ pace
306
+ });
307
+ await chooseBest(v.daily ? "standard" : pace);
308
+ if (disposed) return;
309
+ drawing = await o.render(viewport, pause.pause, announce);
310
+ if (disposed) {
311
+ drawing.dispose();
312
+ return;
313
+ }
314
+ drawing.graphics(graphics);
315
+ loaded = true;
316
+ publish();
317
+ }).catch(() => {
318
+ if (!disposed) announce("Game settings could not be loaded.");
319
+ });
320
+ if (session.get().view.phase !== "ready" && session.get().view.phase !== "over") pause.pause();
321
+ publish();
322
+ return { dispose() {
323
+ if (disposed) return;
324
+ disposed = true;
325
+ for (const stop of stops) stop();
326
+ pause.dispose();
327
+ drawing?.dispose();
328
+ host.remove();
329
+ } };
330
+ }
331
+ //#endregion
332
+ export { mountArcade };
333
+
334
+ //# sourceMappingURL=game.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"game.js","names":[],"sources":["../src/game.ts"],"sourcesContent":["import type { AnyFixedStepSimulation, AnyGameSession, Json, PlatformApi, SoundCue } from '@joystyk/contracts';\nimport type { FixedStepRunner } from '@joystyk/session';\nimport { bindHold, bindInput, type InputAction } from './input.ts';\nimport { bindPause } from './pause.ts';\nimport { createGhost, createRecords, type BestRun, type Pace } from './records.ts';\nimport { ArcadePanel, panelDrivers, type Hud, type PanelAction } from './panel.ts';\nexport type ArcadeView = {\n phase: string;\n pace: Pace;\n daily: boolean;\n course: number;\n};\nexport type ArcadeDrawing<View> = {\n draw(view: View, alpha: number, ghost?: View | null): void;\n graphics(value: 'auto' | 'low' | 'medium' | 'high'): void;\n dispose(): void;\n available?: boolean;\n};\nexport type ArcadeGameOptions<View extends ArcadeView> = {\n gameId: string;\n title: string;\n help: string;\n simulation: AnyFixedStepSimulation;\n score(view: View): number;\n status(view: View): string;\n action(action: InputAction, on: boolean, view: View): readonly Json[];\n drag?(x: number, y: number): Json;\n gesture: 'swipe' | 'tap' | 'drag';\n ghost?: boolean;\n alignment?: boolean;\n eventLine?(event: Json, view: View): string | null;\n render(host: HTMLElement, pause: () => void, announce: (text: string) => void): Promise<ArcadeDrawing<View>>;\n};\nconst CSS = `:host{display:block;font:1rem/1.5 system-ui;color:var(--game-ink,CanvasText);container-type:inline-size}*{box-sizing:border-box}[hidden]{display:none!important}.layout{display:grid;gap:1rem;padding:clamp(.75rem,2cqi,1.5rem);max-inline-size:72rem;margin-inline:auto}.viewport{position:relative;block-size:clamp(18rem,65svh,42rem);background:#10232b;border-radius:1rem;overflow:hidden;touch-action:none;outline-offset:4px}.viewport>canvas{display:block;inline-size:100%;block-size:100%}.viewport>p,.viewport>button{position:absolute;inset-inline:1rem;inset-block-start:35%;background:Canvas;color:CanvasText;padding:1rem}.viewport>button{inset-block-start:60%}.viewport>output{position:absolute;inset:0 auto auto 0;background:#000c;color:white;font:12px monospace;padding:.25rem;pointer-events:none}.actions,.pad{display:flex;gap:.5rem;flex-wrap:wrap}.pad{margin-block-start:1rem}.pad[hidden]{display:none}button,select,summary{font:inherit;min-block-size:44px;padding:.55rem .8rem}button,select{border:1px solid currentColor;border-radius:.45rem;color:inherit;background:var(--game-surface,Canvas)}button{cursor:pointer}button:disabled{opacity:.55;cursor:default}button:focus-visible,select:focus-visible,summary:focus-visible,.viewport:focus-visible{outline:3px solid var(--game-accent,currentColor);outline-offset:3px}h2{margin:0;font-size:clamp(1.4rem,4cqi,2rem)}.status{font-variant-numeric:tabular-nums}fieldset{border:0;padding:0;margin-block:1rem}label{display:flex;align-items:center;gap:.5rem;min-block-size:44px}input{inline-size:1.25rem;block-size:1.25rem}summary{cursor:pointer}.pad button{flex:1;touch-action:none;user-select:none}.help{max-inline-size:65ch}@container(width>=52rem){.layout{grid-template-columns:minmax(14rem,.7fr) minmax(0,1.5fr);align-items:start}}`;\nexport function mountArcade<View extends ArcadeView>(\n slot: HTMLElement,\n anySession: AnyGameSession,\n platform: PlatformApi,\n o: ArcadeGameOptions<View>,\n) {\n const session = anySession as unknown as FixedStepRunner<\n Json,\n View,\n {\n type: string;\n }\n >;\n const host = document.createElement('div'),\n root = host.attachShadow({ mode: 'open' }),\n sheet = new CSSStyleSheet();\n sheet.replaceSync(CSS);\n root.adoptedStyleSheets = [sheet];\n root.innerHTML =\n '<div class=\"layout\"><div class=\"controls\"></div><section><div class=\"viewport\" tabindex=\"0\" role=\"group\" aria-roledescription=\"game board\"></div><div class=\"pad\"></div><p class=\"help\"></p></section></div>';\n slot.append(host);\n const viewport = root.querySelector<HTMLElement>('.viewport')!,\n pad = root.querySelector<HTMLElement>('.pad')!;\n viewport.setAttribute('aria-label', o.title);\n root.querySelector('.help')!.textContent = o.help;\n const controls = new ArcadePanel();\n const records = createRecords(platform.storage, o.gameId, o.simulation),\n listeners = new Set<(hud: Hud) => void>();\n let best: BestRun = { score: 0, replay: null },\n drawing: ArcadeDrawing<View> | null = null,\n ghost: ReturnType<typeof createGhost> = null,\n disposed = false,\n loaded = false,\n resuming = false,\n lastTick = -1,\n lastPhase = 'ready',\n lastPace: Pace = session.get().view.pace,\n lastHud = '',\n padShown = o.gameId === 'tower-stacker' || matchMedia('(pointer:coarse)').matches,\n tone = false,\n autoFire = o.gesture === 'drag' && matchMedia('(pointer:coarse)').matches,\n graphics: 'auto' | 'low' | 'medium' | 'high' = 'auto';\n const send = (input: Json): void => session.dispatch({ kind: 'input', input });\n const announce = (text: string): void => platform.announce(text, 'polite');\n const pause = bindPause({\n pause: () => {\n for (const action of ['left', 'right', 'up', 'main'] as const)\n for (const input of o.action(action, false, session.get().view)) send(input);\n if (o.drag !== undefined) send(o.drag(0, 0));\n session.dispatch({ kind: 'pause' });\n },\n resume: () => {\n session.dispatch({ kind: 'resume' });\n if (o.gesture === 'drag') send({ type: 'fire', on: autoFire });\n },\n announce,\n changed: (value) => {\n resuming = value;\n publish();\n },\n });\n const publish = (): void => {\n if (disposed) return;\n const snapshot = session.get(),\n v = snapshot.view;\n const hud: Hud = {\n title: o.title,\n status: resuming ? 'Resuming in one second.' : snapshot.paused ? 'Paused' : o.status(v),\n phase: v.phase,\n paused: snapshot.paused,\n resuming,\n pace: v.pace,\n daily: v.daily,\n best: Math.max(best.score, o.score(v)),\n race: o.ghost === true && best.replay !== null,\n pad: padShown,\n tone,\n autoFire,\n graphics,\n loaded,\n alignment: o.alignment === true,\n shooter: o.gesture === 'drag',\n };\n const key = JSON.stringify(hud);\n if (key !== lastHud) {\n lastHud = key;\n for (const listener of listeners) listener(hud);\n }\n pad.hidden = !padShown && !(o.gesture === 'drag' && !autoFire);\n if (drawing !== null) {\n const g = ghost?.advance(snapshot.tick) as View | undefined;\n drawing.draw(v, session.alpha(), g ?? null);\n }\n };\n const chooseBest = async (pace: Pace): Promise<void> => {\n const found = await records.best(pace);\n if (!disposed && session.get().view.pace === pace) {\n best = found;\n publish();\n }\n };\n const emit = (action: InputAction, on: boolean): void => {\n if (disposed || !loaded || drawing === null || drawing.available === false) return;\n const snap = session.get();\n if (action === 'auxiliary') {\n if (on && o.gesture === 'drag') {\n autoFire = !autoFire;\n send({ type: 'fire', on: autoFire });\n announce(`Automatic fire ${autoFire ? 'on' : 'off'}.`);\n publish();\n }\n return;\n }\n if (action === 'pause') {\n if (on) {\n if (snap.paused) pause.resume();\n else pause.pause();\n }\n return;\n }\n if (snap.paused) {\n if (snap.view.phase === 'ready') session.dispatch({ kind: 'resume' });\n else return;\n }\n if (o.gesture === 'drag' && action === 'main' && snap.view.phase !== 'ready' && autoFire) {\n send({ type: 'fire', on: true });\n return;\n }\n for (const input of o.action(action, on, snap.view)) send(input);\n };\n const stops: (() => void)[] = [];\n const perform = (a: PanelAction): void => {\n const snap = session.get(),\n v = snap.view;\n switch (a.type) {\n case 'primary':\n if (v.phase === 'ready') {\n emit('main', true);\n emit('main', false);\n } else if (snap.paused) pause.resume();\n else pause.pause();\n break;\n case 'pace':\n if (v.phase === 'ready' && !v.daily) {\n send({ type: 'pace', pace: a.pace });\n best = { score: 0, replay: null };\n ghost = null;\n void records.remember(a.pace).catch(() => {});\n void chooseBest(a.pace);\n }\n break;\n case 'race':\n if (v.phase === 'ready' && !v.daily && best.replay !== null) {\n ghost = createGhost(o.simulation, best.replay);\n const initial = ghost?.advance(0) as View | undefined;\n if (initial !== undefined) {\n send({ type: 'course', course: initial.course });\n emit('main', true);\n }\n }\n break;\n case 'graphics':\n graphics = a.value;\n drawing?.graphics(graphics);\n void platform.storage.set('arcade.graphics', graphics).catch(() => {});\n break;\n case 'pad':\n padShown = !padShown;\n void platform.storage.set('arcade.pad', padShown).catch(() => {});\n break;\n case 'tone':\n tone = !tone;\n break;\n case 'fire':\n autoFire = !autoFire;\n send({ type: 'fire', on: autoFire });\n break;\n }\n publish();\n };\n controls.drivers = panelDrivers((emit) => {\n listeners.add(emit);\n lastHud = '';\n publish();\n return () => listeners.delete(emit);\n }, perform);\n root.querySelector('.controls')!.append(controls);\n for (const [action, label] of [\n ['left', 'Left'],\n ['up', 'Forward / Thrust'],\n ['right', 'Right'],\n ['down', 'Back'],\n ['main', o.gameId === 'tower-stacker' ? 'Drop' : o.gesture === 'drag' ? 'Fire' : 'Hop'],\n ] as const) {\n const button = document.createElement('button');\n button.type = 'button';\n button.textContent = label;\n pad.append(button);\n stops.push(bindHold(button, (on) => emit(action, on)));\n button.addEventListener('click', (e) => {\n if (e.detail === 0) {\n emit(action, true);\n emit(action, false);\n }\n });\n }\n stops.push(\n bindInput({\n surface: viewport,\n keys: {\n arrowup: 'up',\n arrowdown: 'down',\n arrowleft: 'left',\n arrowright: 'right',\n w: 'up',\n s: 'down',\n a: 'left',\n d: 'right',\n ' ': 'main',\n enter: 'main',\n f: 'auxiliary',\n p: 'pause',\n escape: 'pause',\n },\n shortcuts: () => platform.preferences.get().singleKeyShortcuts,\n emit,\n announce,\n gesture: o.gesture,\n drag: (x, y) => {\n if (loaded && !session.get().paused && o.drag !== undefined) send(o.drag(x, y));\n },\n }),\n );\n stops.push(\n session.subscribe((snap) => {\n const v = snap.view;\n if (v.pace !== lastPace) {\n lastPace = v.pace;\n void chooseBest(v.pace);\n }\n if (lastPhase === 'ready' && v.phase !== 'ready') {\n viewport.focus({ preventScroll: true });\n if (autoFire) send({ type: 'fire', on: true });\n }\n if (v.phase === 'over' && lastPhase !== 'over' && !v.daily) {\n void records\n .record(v.pace, o.score(v), o.ghost === true ? session.replay() : null)\n .then(() => chooseBest(v.pace))\n .catch(() => {});\n announce(o.status(v));\n }\n lastPhase = v.phase;\n if (lastTick !== snap.tick) {\n lastTick = snap.tick;\n for (const e of snap.events) {\n const cue: SoundCue | undefined = (\n {\n began: 'start',\n hopped: 'move',\n signal: 'warning',\n milestone: 'level',\n placed: 'hit',\n regrew: 'power',\n missed: 'lose',\n wave: 'level',\n lifeLost: 'life',\n extraLife: 'life',\n droneIn: 'warning',\n ended: 'lose',\n } as Record<string, SoundCue>\n )[e.type];\n if (cue !== undefined) platform.audio?.play(cue);\n if (o.alignment && tone && e.type === 'aligned') platform.audio?.play('tick');\n const line = o.eventLine?.(e as Json, v);\n if (line !== undefined && line !== null) announce(line);\n else if (['wave', 'lifeLost', 'extraLife', 'droneIn', 'regrew', 'milestone', 'signal'].includes(e.type))\n announce(o.status(v));\n }\n }\n publish();\n }),\n );\n void Promise.all([\n records.pace(),\n platform.storage.get('arcade.pad').catch(() => null),\n platform.storage.get('arcade.graphics').catch(() => null),\n ])\n .then(async ([pace, padValue, quality]) => {\n if (disposed) return;\n if (typeof padValue === 'boolean') padShown = padValue;\n if (['auto', 'low', 'medium', 'high'].includes(String(quality))) graphics = quality as typeof graphics;\n const v = session.get().view;\n if (!v.daily && v.phase === 'ready') send({ type: 'pace', pace });\n await chooseBest(v.daily ? 'standard' : pace);\n if (disposed) return;\n drawing = await o.render(viewport, pause.pause, announce);\n if (disposed) {\n drawing.dispose();\n return;\n }\n drawing.graphics(graphics);\n loaded = true;\n publish();\n })\n .catch(() => {\n if (!disposed) announce('Game settings could not be loaded.');\n });\n if (session.get().view.phase !== 'ready' && session.get().view.phase !== 'over') pause.pause();\n publish();\n return {\n dispose(): void {\n if (disposed) return;\n disposed = true;\n for (const stop of stops) stop();\n pause.dispose();\n drawing?.dispose();\n host.remove();\n },\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ export { bindInput, bindHold, bindHoldPad, bindDrag, type InputAction, type InputOptions } from './input.js';
2
+ export { bindPause, type PauseOptions } from './pause.js';
3
+ export { PACES, createRecords, createGhost, type Pace, type BestRun } from './records.js';
4
+ export { mountArcade, type ArcadeView, type ArcadeDrawing, type ArcadeGameOptions } from './game.js';
5
+ export { createControlDrivers, resumeAfter, bindPagePause, swipeDirection, SWIPE_THRESHOLD, ARROW_DIRECTIONS, LETTER_DIRECTIONS, } from './legacy.js';
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ import { ARROW_DIRECTIONS, LETTER_DIRECTIONS, SWIPE_THRESHOLD, bindPagePause, createControlDrivers, resumeAfter, swipeDirection } from "./legacy.js";
2
+ import { bindDrag, bindHold, bindHoldPad, bindInput } from "./input.js";
3
+ import { bindPause } from "./pause.js";
4
+ import { PACES, createGhost, createRecords } from "./records.js";
5
+ import { mountArcade } from "./game.js";
6
+ export { ARROW_DIRECTIONS, LETTER_DIRECTIONS, PACES, SWIPE_THRESHOLD, bindDrag, bindHold, bindHoldPad, bindInput, bindPagePause, bindPause, createControlDrivers, createGhost, createRecords, mountArcade, resumeAfter, swipeDirection };
@@ -0,0 +1,28 @@
1
+ /** All sources share the same actions. A held action is released on blur or disconnect. */
2
+ export type InputAction = 'up' | 'down' | 'left' | 'right' | 'main' | 'pause' | 'auxiliary';
3
+ export type InputOptions = {
4
+ readonly surface: HTMLElement;
5
+ readonly keys: Readonly<Record<string, InputAction>>;
6
+ readonly shortcuts: () => boolean;
7
+ readonly emit: (action: InputAction, held: boolean) => void;
8
+ readonly announce: (text: string) => void;
9
+ readonly gesture?: 'swipe' | 'tap' | 'drag';
10
+ readonly drag?: (x: number, y: number) => void;
11
+ readonly deadZone?: number;
12
+ readonly gamepads?: () => readonly (Gamepad | null)[];
13
+ readonly frame?: (callback: FrameRequestCallback) => number;
14
+ readonly cancelFrame?: (id: number) => void;
15
+ };
16
+ export declare function bindInput(o: InputOptions): () => void;
17
+ /** Native hold buttons also support keyboard and switch activation. */
18
+ export declare function bindHold(button: HTMLButtonElement, emit: (held: boolean) => void): () => void;
19
+ /** Delegated holds survive Gyral updates that add or replace native pad buttons. */
20
+ export declare function bindHoldPad(root: HTMLElement | ShadowRoot, selector: string, emit: (button: HTMLButtonElement, on: boolean, source: string) => void, signal?: AbortSignal): () => void;
21
+ /** Absolute dragging for paddles; games own projection into their court's units. */
22
+ export declare function bindDrag(surface: HTMLElement, o: {
23
+ start(event: PointerEvent): boolean | void;
24
+ move(event: PointerEvent): void;
25
+ hoverMouse?: boolean;
26
+ capture?: boolean;
27
+ primaryOnly?: boolean;
28
+ }, signal?: AbortSignal): () => void;