@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 +21 -0
- package/LICENSE +21 -0
- package/NOTICE +36 -0
- package/README.md +23 -2
- package/dist/game.d.ts +33 -0
- package/dist/game.js +334 -0
- package/dist/game.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +6 -0
- package/dist/input.d.ts +28 -0
- package/dist/input.js +226 -0
- package/dist/input.js.map +1 -0
- package/dist/legacy.d.ts +28 -0
- package/dist/legacy.js +66 -0
- package/dist/legacy.js.map +1 -0
- package/dist/panel.d.ts +41 -0
- package/dist/panel.js +356 -0
- package/dist/panel.js.map +1 -0
- package/dist/pause.d.ts +13 -0
- package/dist/pause.js +36 -0
- package/dist/pause.js.map +1 -0
- package/dist/records.d.ts +19 -0
- package/dist/records.js +105 -0
- package/dist/records.js.map +1 -0
- package/package.json +52 -3
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
|
-
#
|
|
1
|
+
# @joystyk/arcade
|
|
2
2
|
|
|
3
|
-
|
|
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
|
package/dist/game.js.map
ADDED
|
@@ -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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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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 };
|
package/dist/input.d.ts
ADDED
|
@@ -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;
|