@joystyk/arcade 0.0.0 → 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/README.md +21 -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 +46 -11
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/README.md
CHANGED
|
@@ -1,5 +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.
|
|
4
5
|
|
|
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;
|