@joystyk/launcher 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 +38 -0
- package/README.md +245 -2
- package/dist/browser/daily-prefetch.d.ts +8 -0
- package/dist/browser/daily-prefetch.js +32 -0
- package/dist/browser/daily-prefetch.js.map +1 -0
- package/dist/browser/index.d.ts +3 -0
- package/dist/browser/index.js +4 -0
- package/dist/browser/rooms.d.ts +2 -0
- package/dist/browser/rooms.js +49 -0
- package/dist/browser/rooms.js.map +1 -0
- package/dist/browser/services.d.ts +127 -0
- package/dist/browser/services.js +95 -0
- package/dist/browser/services.js.map +1 -0
- package/dist/browser/stage.d.ts +28 -0
- package/dist/browser/stage.js +1195 -0
- package/dist/browser/stage.js.map +1 -0
- package/dist/fixture/engine.d.ts +60 -0
- package/dist/fixture/engine.js +164 -0
- package/dist/fixture/engine.js.map +1 -0
- package/dist/fixture/index.d.ts +52 -0
- package/dist/fixture/index.js +63 -0
- package/dist/fixture/index.js.map +1 -0
- package/dist/fixture/module.d.ts +2 -0
- package/dist/fixture/module.js +135 -0
- package/dist/fixture/module.js.map +1 -0
- package/dist/fixture/probe.d.ts +10 -0
- package/dist/fixture/probe.js +19 -0
- package/dist/fixture/probe.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -0
- package/dist/launcher.d.ts +190 -0
- package/dist/launcher.js +4614 -0
- package/dist/launcher.js.map +1 -0
- package/dist/model.d.ts +431 -0
- package/dist/model.js +566 -0
- package/dist/model.js.map +1 -0
- package/dist/report-problem.d.ts +39 -0
- package/dist/report-problem.js +359 -0
- package/dist/report-problem.js.map +1 -0
- package/dist/report.d.ts +18 -0
- package/dist/report.js +32 -0
- package/dist/report.js.map +1 -0
- package/dist/room-drivers.d.ts +26 -0
- package/dist/room-drivers.js +92 -0
- package/dist/room-drivers.js.map +1 -0
- package/dist/safe-point.d.ts +33 -0
- package/dist/safe-point.js +44 -0
- package/dist/safe-point.js.map +1 -0
- package/dist/save-slots.d.ts +6 -0
- package/dist/save-slots.js +18 -0
- package/dist/save-slots.js.map +1 -0
- package/dist/theme.d.ts +35 -0
- package/dist/theme.js +111 -0
- package/dist/theme.js.map +1 -0
- package/package.json +67 -11
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# @joystyk/launcher
|
|
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/storage@0.2.0
|
|
17
|
+
- @joystyk/session@0.2.0
|
|
18
|
+
- @joystyk/daily@0.2.0
|
|
19
|
+
- @joystyk/identity@0.2.0
|
|
20
|
+
- @joystyk/kit@0.2.0
|
|
21
|
+
- @joystyk/ui@0.2.0
|
|
22
|
+
|
|
23
|
+
## 0.1.0
|
|
24
|
+
|
|
25
|
+
### Minor Changes
|
|
26
|
+
|
|
27
|
+
- Prepare the first compiled library release: ESM and API declarations for every public entry point, runtime assets, and checks against installed npm tarballs. Games and applications remain private.
|
|
28
|
+
|
|
29
|
+
### Patch Changes
|
|
30
|
+
|
|
31
|
+
- Updated dependencies
|
|
32
|
+
- @joystyk/contracts@0.1.0
|
|
33
|
+
- @joystyk/kit@0.1.0
|
|
34
|
+
- @joystyk/storage@0.1.0
|
|
35
|
+
- @joystyk/identity@0.1.0
|
|
36
|
+
- @joystyk/session@0.1.0
|
|
37
|
+
- @joystyk/daily@0.1.0
|
|
38
|
+
- @joystyk/ui@0.1.0
|
package/README.md
CHANGED
|
@@ -1,5 +1,248 @@
|
|
|
1
1
|
# @joystyk/launcher
|
|
2
2
|
|
|
3
|
-
The
|
|
3
|
+
The frame around a game (host policy). `<jy-launcher>` lets the player choose how to play, starts a
|
|
4
|
+
local session or opens a server room, mounts the game's canvas, and draws the platform's own
|
|
5
|
+
screens around it: the resume offer, the pass-and-play hand-off (host policy), the room lobby, the
|
|
6
|
+
pause overlay with preferences, and the results with their ad slot. It talks to games only through the contracts in
|
|
7
|
+
game-contract.md (Joystyk host reference) and never imports a game.
|
|
4
8
|
|
|
5
|
-
|
|
9
|
+
## npm package
|
|
10
|
+
|
|
11
|
+
The first `0.1.0` release is being prepared; it has not been published. Once available:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npm install @joystyk/launcher
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Node consumers require Node 24.15 or later. Browser entry points require a modern
|
|
18
|
+
browser and should be loaded in the browser. Tested with Gyral 0.3.1.
|
|
19
|
+
|
|
20
|
+
| Import | Format |
|
|
21
|
+
| --- | --- |
|
|
22
|
+
| `@joystyk/launcher` | ES module and TypeScript declarations |
|
|
23
|
+
| `@joystyk/launcher/browser` | ES module and TypeScript declarations |
|
|
24
|
+
| `@joystyk/launcher/fixture` | ES module and TypeScript declarations |
|
|
25
|
+
| `@joystyk/launcher/report` | ES module and TypeScript declarations |
|
|
26
|
+
|
|
27
|
+
Only the entries listed above are public API. Installed packages contain compiled ESM,
|
|
28
|
+
type declarations and runtime assets. Repository-relative links describe the Joystyk
|
|
29
|
+
workspace and are not needed to use the npm package.
|
|
30
|
+
|
|
31
|
+
## Entry points
|
|
32
|
+
|
|
33
|
+
| Import | Runs in | Contents |
|
|
34
|
+
| --- | --- | --- |
|
|
35
|
+
| `@joystyk/launcher` | Server and browser | `<jy-launcher>` (server-rendered), its pure model, the safe-point event |
|
|
36
|
+
| `@joystyk/launcher/browser` | Browser | `createLauncherServices`, `provideLauncherServices`, `watchSafePoints`, `requestSave` |
|
|
37
|
+
| `@joystyk/launcher/fixture` | Tests | Tally, a tiny test game, and `tallyProbe` |
|
|
38
|
+
|
|
39
|
+
The stage element (`<jy-game-stage>`), the session runners, storage and identity load on
|
|
40
|
+
the first Start, so a game page that nobody plays never downloads them.
|
|
41
|
+
|
|
42
|
+
## Use
|
|
43
|
+
|
|
44
|
+
A page renders the launcher with what its game page knows about the game:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
html`<jy-launcher .game=${{ id, name, modes, players, aiLevels, status, aiContent }}></jy-launcher>`;
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`players` is `{ min, max }`, plus the manifest's optional `default`: against the computer
|
|
51
|
+
the Players picker starts at that count, and otherwise at the fewest.
|
|
52
|
+
|
|
53
|
+
The app provides the games it can load, once per page (platform packages never import a
|
|
54
|
+
game, so the list comes from the app; `apps/site/src/game-loaders.ts` builds it):
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { createLauncherServices, provideLauncherServices } from '@joystyk/launcher/browser';
|
|
58
|
+
|
|
59
|
+
provideLauncherServices(document, createLauncherServices({
|
|
60
|
+
games: { nim: { entry: () => import('@joystyk/game-nim').then((m) => m.entry) } },
|
|
61
|
+
}));
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
A loader may also give `aiWorker: () => new Worker(…)` for a manifest with `ai.heavy`; the
|
|
65
|
+
local runner then chooses AI moves in that worker (`workerAiRunner`). A game whose engine
|
|
66
|
+
requests jobs that local play may run gives `jobWorker: (kind) => new Worker(…)`; the runner
|
|
67
|
+
then performs each job in a fresh worker (`workerJobRunner`). Greed's dice throws work this
|
|
68
|
+
way, with the factory from `@joystyk/game-greed/jobs`.
|
|
69
|
+
|
|
70
|
+
Without JavaScript the setup form still renders, with a disabled Start button and the line
|
|
71
|
+
"JavaScript is needed to play".
|
|
72
|
+
|
|
73
|
+
## How a game starts
|
|
74
|
+
|
|
75
|
+
1. **Setup.** The modes come from the manifest. Solo, vs AI (with a level), pass-and-play
|
|
76
|
+
(with seat names, checked by `@joystyk/identity`'s name filter) and, for a manifest that
|
|
77
|
+
lists `daily`, the daily puzzle are playable; P2P rooms show as coming soon (host policy). Solo,
|
|
78
|
+
vs-AI and daily play seat the guest under their display name. In pass-and-play the count is
|
|
79
|
+
people: a game with AI and more seats than people (Hearts and Spades seat four) lets two or
|
|
80
|
+
more share the device, and the computer plays the other seats at the chosen level
|
|
81
|
+
(`computerSeatsFor`). Online with friends is
|
|
82
|
+
playable only for a game the room server hosts: once live, a launcher whose manifest lists
|
|
83
|
+
`server` asks `GET /api/rooms/games` (the `launcher-online-rooms` driver) and reads an
|
|
84
|
+
invite after the answer. A game the server doesn't host shows online play as coming soon
|
|
85
|
+
(host policy); while it asks, the option waits without a note; and when the server can't be
|
|
86
|
+
reached (an error, a page that isn't the answer, or 10 seconds of silence) it shows as
|
|
87
|
+
"Unavailable right now". The local modes never wait for it. The answer holds for the page,
|
|
88
|
+
so client navigation to another game reuses it.
|
|
89
|
+
2. **Loading.** `<jy-game-stage>` loads the entry through the app's loader, checks that the
|
|
90
|
+
manifest allows the mode and that the access gate is open (always, until premium access
|
|
91
|
+
in v2), loads the module, opens the player's storage and reads their preferences.
|
|
92
|
+
3. **AI-made content (host policy).** A manifest with `aiContent: true` stays locked until the
|
|
93
|
+
player accepts a short notice. The answer is a preference (`aiContentAccepted`), stored
|
|
94
|
+
once per device and changeable in the overlay. Such a game's results and pause menu carry
|
|
95
|
+
`<jy-report-problem>` (*Report a problem*, host policy): a reason from a fixed list and an
|
|
96
|
+
optional note, posted to the site's `POST /api/reports` with the game and, for a daily
|
|
97
|
+
puzzle, its ID (the stage's `Puzzle` output). `@joystyk/launcher/report` holds the wire
|
|
98
|
+
constants the site shares.
|
|
99
|
+
4. **Resume or new.** Each local mode has its own save slot. A save in progress is offered
|
|
100
|
+
for resuming; a finished one, one from older rules, or one the engine's `worthSaving`
|
|
101
|
+
rejects (an untouched game), is cleared. The slot's port lets the runner clear it too.
|
|
102
|
+
5. **Playing.** The stage builds the session (`createLocalSession` for a Layer 2 engine,
|
|
103
|
+
`createFixedStepRunner` for a simulation, or the game's own `createSession`) and calls
|
|
104
|
+
`mount(canvas, session, platform)`.
|
|
105
|
+
6. **Results.** At the outcome the stage clears the save slot, records stats and, if the
|
|
106
|
+
engine has a `rematch` hook, the config for "Play again". The launcher shows the outcome,
|
|
107
|
+
play time, any assistance (`Outcome.undosUsed` and `assists`), Play again, Change mode,
|
|
108
|
+
Back to all games, and the `results` ad slot, over the final board (see below). The
|
|
109
|
+
session is disposed only when the player picks Play again or Change mode, the game asks
|
|
110
|
+
to exit, or the page goes away.
|
|
111
|
+
|
|
112
|
+
## Online with friends
|
|
113
|
+
|
|
114
|
+
Server rooms (host policy), for any game whose manifest lists `server` and that the room server
|
|
115
|
+
hosts:
|
|
116
|
+
|
|
117
|
+
1. **Create or join.** "Create a room" asks the room server for a room (`POST /api/rooms`)
|
|
118
|
+
with the guest's display name; the creator holds seat 0 and hosts. Below it, a join form
|
|
119
|
+
takes a code or a pasted invite link, and "Just watch" joins as a spectator (host policy). A page
|
|
120
|
+
opened from an invite (`?room=CODE`) picks the mode and fills in the code; the player still
|
|
121
|
+
presses Join. A malformed code is refused in the form; an unknown one, or one for another
|
|
122
|
+
game, gets the stage's message.
|
|
123
|
+
2. **Lobby.** The invite link (in a read-only field, with Copy link and Copy code) and the
|
|
124
|
+
code; the seats, each open, the computer at the room's level, or a player marked host, you,
|
|
125
|
+
ready or away; the spectator count; the turn clock. Everyone with a seat presses Ready
|
|
126
|
+
(`aria-pressed`). The host picks the clock, from the manifest's presets (the usual one
|
|
127
|
+
marked) or Off, which is never the default (host policy, host policy), and the computer's level; adds the
|
|
128
|
+
computer to an open seat or removes a seat; and starts once every seat is filled and
|
|
129
|
+
ready. The hint under Start says what it waits for. Leave room gives the seat up.
|
|
130
|
+
3. **Play.** When the match starts the stage mounts the game on the remote session. The bar
|
|
131
|
+
shows the room code, the connection ("Connection lost. Reconnecting…"), and the running
|
|
132
|
+
turn clock (`role="timer"`). The menu doesn't pause a room ("The match goes on while this
|
|
133
|
+
menu is open") and offers Leave room. A newer tab taking the seat shows "Use this tab
|
|
134
|
+
instead". Hosts can't remove a player mid-match (host policy).
|
|
135
|
+
4. **Results** show over the final board as for local play, worded for the viewer ("You
|
|
136
|
+
won!", "Ben wins", or "Ben wins!" for a spectator). The host's Rematch starts a new match
|
|
137
|
+
in the same room, on a fresh canvas; Leave room goes back to the setup.
|
|
138
|
+
|
|
139
|
+
The stage reports the room after every change as a `Room` output (`RoomFrame`: plain data,
|
|
140
|
+
checked at the boundary) and performs the lobby's requests (`StageControl.roomRequests`) in
|
|
141
|
+
order. The room's code goes in the address bar while it is open (`history.replaceState`),
|
|
142
|
+
so a reload or a shared address finds it again. `LauncherServices.rooms` (`RoomServices`) is
|
|
143
|
+
how the stage reaches the room server; the default (`browser/rooms.ts`) uses the page's own
|
|
144
|
+
origin, where Traefik routes `/api/rooms` and `/ws`, and tests pass a loopback.
|
|
145
|
+
|
|
146
|
+
## The daily puzzle
|
|
147
|
+
|
|
148
|
+
For a manifest that lists `daily` (host policy, content-pipeline.md (Joystyk host reference)):
|
|
149
|
+
|
|
150
|
+
1. **After the notice.** The stage downloads nothing until the game may start: an AI-content
|
|
151
|
+
game's notice comes first (host policy).
|
|
152
|
+
2. **Today's puzzle.** It asks `@joystyk/daily/browser`'s content client for today's UTC date
|
|
153
|
+
(`LauncherServices.daily`: the base URL, default `/daily`, a `fetch` and the wall clock).
|
|
154
|
+
The client revalidates the index, uses or refreshes the cached file and decodes `config`
|
|
155
|
+
with the engine's `schemas.config`; offline, a cached copy plays. With nothing to play, the
|
|
156
|
+
stage fails with the client's plain message ("This puzzle needs a connection…").
|
|
157
|
+
3. **Played once.** If the results store already holds today's result, the launcher shows it
|
|
158
|
+
("You've played today's puzzle") with the streak and share text, and mounts nothing.
|
|
159
|
+
Otherwise the game starts on the day's config (a resumed save finishes the puzzle it holds,
|
|
160
|
+
even after the next release, and its result counts for that puzzle's date).
|
|
161
|
+
4. **Results.** At the outcome the stage records the `DailyResult` (with the engine's `share`
|
|
162
|
+
rows in both forms), reads the streaks and reports a `Daily` frame. The results show the
|
|
163
|
+
streak, the win streak for a daily that can be lost, "New puzzle at 8:00 PM (in 6 hours)"
|
|
164
|
+
in local time, the share preview, *Share* (Web Share, else the clipboard) and *Copy as
|
|
165
|
+
text*. There is no Play again; *Change mode* appears when the game has other modes.
|
|
166
|
+
5. **Prefetch.** `dailyPrefetchers(services, gameIds)` gives the PWA's `registerPwa` one
|
|
167
|
+
prefetcher per daily game. It prefetches today and tomorrow through the same client and
|
|
168
|
+
storage, and nothing for an AI-content game until the notice is accepted.
|
|
169
|
+
|
|
170
|
+
## What the game gets
|
|
171
|
+
|
|
172
|
+
`mount` receives the canvas, a `<div>` in the stage's own shadow root (the frame's styles
|
|
173
|
+
never reach it), and a `PlatformApi`:
|
|
174
|
+
|
|
175
|
+
| Field | Source |
|
|
176
|
+
| --- | --- |
|
|
177
|
+
| `storage` | `@joystyk/storage`'s game storage, scoped to the game id and the player |
|
|
178
|
+
| `preferences` | The stored preferences resolved against the system (reduced motion, sound and volume, single-key shortcuts, color scheme, locale), updated live when the player changes them in the overlay or the system setting changes |
|
|
179
|
+
| `announce` | The launcher's polite and assertive live regions |
|
|
180
|
+
| `requestExit` | Back to the setup, saving the game |
|
|
181
|
+
|
|
182
|
+
**Theme handoff.** The canvas carries the manifest's theme as `--game-accent`,
|
|
183
|
+
`--game-surface` and `--game-ink`, and the player's color scheme, which resolves any
|
|
184
|
+
`light-dark()` pair; the frame keeps Joystyk's tokens. It paints the surface and sets its
|
|
185
|
+
text color to the ink. A surface and its ink always follow the same scheme: a single surface
|
|
186
|
+
without an ink gets near-black or near-white, whichever reads better (`readableInk`), and a
|
|
187
|
+
game without a surface gets the frame's own surface and text. `canvasTheme` in `src/theme.ts`
|
|
188
|
+
computes the tokens; the rules are in the
|
|
189
|
+
game contract (Joystyk host reference) ("Theme").
|
|
190
|
+
|
|
191
|
+
## The frame's screens
|
|
192
|
+
|
|
193
|
+
- **Hand-off (host policy).** When the session publishes a `handoff`, the canvas is hidden and the
|
|
194
|
+
frame shows "Pass to <name>, then tap to reveal". Focus moves to the reveal button.
|
|
195
|
+
- **Results over the final board.** The results open as a non-modal `<dialog>` laid over the
|
|
196
|
+
canvas, out of the flow, so neither the board nor the page moves; a board shorter than the
|
|
197
|
+
results scrolls them. The canvas stays mounted, dimmed and `inert` behind them. Focus moves
|
|
198
|
+
to the headline, which names the dialog. Escape or "Review the board" closes the results
|
|
199
|
+
and moves focus to the board, which can be read (and heard) again but not played: the
|
|
200
|
+
session refuses moves once the game is over. "Show results" in the bar reopens them, with
|
|
201
|
+
the same ad slot. The dialog is not modal because a modal one would make the page inert,
|
|
202
|
+
and the results are where the PWA may offer an update.
|
|
203
|
+
- **Overlay.** The Menu button opens a modal `<dialog>`: Resume, Save and exit, and the
|
|
204
|
+
preferences. The dialog traps focus and closes on Escape; the session is paused while it
|
|
205
|
+
is open. Focus returns to the Menu button.
|
|
206
|
+
- **Reduced motion.** The frame's own transitions stop when the system asks for reduced
|
|
207
|
+
motion, unless the player chose full motion, and always when the player chose to reduce
|
|
208
|
+
it. Games receive the resolved value and remove only decorative motion.
|
|
209
|
+
|
|
210
|
+
## Safe points
|
|
211
|
+
|
|
212
|
+
The launcher reports every change with a `joystyk:safe-point` event on the document
|
|
213
|
+
(`{ safe, gameId }`). The page is safe outside play and on the results screen, open or closed; during play
|
|
214
|
+
it is safe only when the session says so (`meta.safePoint`). The PWA listens with
|
|
215
|
+
`watchSafePoints` and offers updates only when `safe` is true
|
|
216
|
+
(offline-and-updates.md (Joystyk host reference), Rule 2).
|
|
217
|
+
|
|
218
|
+
A forced security update can't wait for one, so it asks: `requestSave()` dispatches a
|
|
219
|
+
`joystyk:save-request` event, and each game stage stops play (pausing a real-time game) and
|
|
220
|
+
writes the current position through its save slot. The promise settles once every write has
|
|
221
|
+
landed or failed, and at once when no game is in play. Rooms and Layer 1 games add nothing:
|
|
222
|
+
the room server keeps a room's state, and those games save themselves.
|
|
223
|
+
|
|
224
|
+
## Tests
|
|
225
|
+
|
|
226
|
+
- `model.test.ts` and `launcher.test.ts`: the pure helpers and the component's model, with
|
|
227
|
+
`@gyral/testing`, in Node.
|
|
228
|
+
- `theme.test.ts` and `theme.browser.test.ts`: the canvas theme tokens, the picked ink and
|
|
229
|
+
its CSS fallback.
|
|
230
|
+
- `fixture/engine.test.ts`: the Tally fixture's rules.
|
|
231
|
+
- `launcher.browser.test.ts`: the real stage, runner and storage (memory backend) in
|
|
232
|
+
Chromium: modes from the keyboard, mounting and disposal, theme tokens and readable canvas
|
|
233
|
+
text in light and dark, resume, the hand-off screen, rematch, the AI-content notice, the
|
|
234
|
+
overlay's focus trap and Escape, preferences and reduced motion, safe points, results over
|
|
235
|
+
the inert final board (Escape, Show results, no layout shift, disposal on Play again) and a
|
|
236
|
+
Layer 1 game.
|
|
237
|
+
- `launcher.daily.browser.test.ts`: the daily mode with a fake `fetch` and a fixed clock:
|
|
238
|
+
nothing downloads before the notice, the published config plays, the results show the
|
|
239
|
+
streak and share text, a second visit shows today's result, offline with nothing cached says
|
|
240
|
+
so, and the prefetcher waits for the notice.
|
|
241
|
+
- `launcher.rooms.browser.test.ts`: Online with friends in Chromium against the real room
|
|
242
|
+
core running in the page (`__tests__/loopback-room.ts`): create, invite, the computer in a
|
|
243
|
+
seat, ready, start, results, rematch and leave; a guest joining from an invite with a
|
|
244
|
+
spectator watching; malformed and unknown codes.
|
|
245
|
+
- `apps/site`'s end-to-end tests play Tally and Nim through the launcher on the real server,
|
|
246
|
+
where no room server answers, so online play shows as unavailable; `rooms.e2e.test.ts`
|
|
247
|
+
plays Nim, Greed and Shift 23 rooms on the real site and room servers, and checks that a
|
|
248
|
+
game the room server doesn't host shows online play as coming soon.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { LauncherServices } from './services.js';
|
|
2
|
+
/** What `registerPwa` takes for each daily game. */
|
|
3
|
+
export type DailyPrefetch = {
|
|
4
|
+
readonly gameId: string;
|
|
5
|
+
prefetch(today: string): Promise<unknown>;
|
|
6
|
+
};
|
|
7
|
+
/** Prefetchers for the daily games among `gameIds` (games whose manifest lists `daily`). */
|
|
8
|
+
export declare const dailyPrefetchers: (services: LauncherServices, gameIds: readonly string[]) => readonly DailyPrefetch[];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//#region src/browser/daily-prefetch.ts
|
|
2
|
+
/** Prefetchers for the daily games among `gameIds` (games whose manifest lists `daily`). */
|
|
3
|
+
var dailyPrefetchers = (services, gameIds) => gameIds.map((gameId) => ({
|
|
4
|
+
gameId,
|
|
5
|
+
prefetch: async (today) => {
|
|
6
|
+
const loader = services.games[gameId];
|
|
7
|
+
if (loader === void 0) return null;
|
|
8
|
+
const entry = await loader.entry();
|
|
9
|
+
if (!entry.manifest.modes.includes("daily")) return null;
|
|
10
|
+
const player = await services.openPlayer();
|
|
11
|
+
if (player.device === void 0) return null;
|
|
12
|
+
if (entry.manifest.aiContent === true) {
|
|
13
|
+
const preferences = await player.storage.preferences.get();
|
|
14
|
+
if (!preferences.ok || preferences.value.aiContentAccepted !== true) return null;
|
|
15
|
+
}
|
|
16
|
+
const module = await entry.load();
|
|
17
|
+
const rules = module.engine ?? module.simulation;
|
|
18
|
+
if (rules === void 0) return null;
|
|
19
|
+
const { createDailyClient } = await import("@joystyk/daily/browser");
|
|
20
|
+
return createDailyClient({
|
|
21
|
+
baseUrl: services.daily?.baseUrl ?? "/daily",
|
|
22
|
+
gameId,
|
|
23
|
+
config: rules.schemas.config,
|
|
24
|
+
storage: player.device,
|
|
25
|
+
...services.daily?.fetch === void 0 ? {} : { fetch: services.daily.fetch }
|
|
26
|
+
}).prefetch(today);
|
|
27
|
+
}
|
|
28
|
+
}));
|
|
29
|
+
//#endregion
|
|
30
|
+
export { dailyPrefetchers };
|
|
31
|
+
|
|
32
|
+
//# sourceMappingURL=daily-prefetch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"daily-prefetch.js","names":[],"sources":["../../src/browser/daily-prefetch.ts"],"sourcesContent":["// Daily prefetching for the PWA (D-46, content-pipeline.md, \"Prefetching for offline play\"):\n// one prefetcher per daily game, which the app hands to `registerPwa`. The PWA decides when\n// (at a safe point, while idle and online) and for whom (an installed app, or a pinned game);\n// this decides how: through the same storage, player and content client the daily mode plays\n// from, so a prefetched day plays offline. An AI-content game prefetches nothing until the\n// player has accepted the notice (D-44). Everything loads lazily, on the first prefetch.\nimport type { Validator } from '@joystyk/contracts';\nimport type { LauncherServices } from './services.ts';\n\n/** What `registerPwa` takes for each daily game. */\nexport type DailyPrefetch = {\n readonly gameId: string;\n prefetch(today: string): Promise<unknown>;\n};\n\n/** Prefetchers for the daily games among `gameIds` (games whose manifest lists `daily`). */\nexport const dailyPrefetchers = (services: LauncherServices, gameIds: readonly string[]): readonly DailyPrefetch[] =>\n gameIds.map((gameId) => ({\n gameId,\n prefetch: async (today: string) => {\n const loader = services.games[gameId];\n if (loader === undefined) return null;\n const entry = await loader.entry();\n if (!entry.manifest.modes.includes('daily')) return null;\n const player = await services.openPlayer();\n if (player.device === undefined) return null;\n if (entry.manifest.aiContent === true) {\n const preferences = await player.storage.preferences.get();\n if (!preferences.ok || preferences.value.aiContentAccepted !== true) return null;\n }\n const module = await entry.load();\n const rules = module.engine ?? module.simulation;\n if (rules === undefined) return null;\n const { createDailyClient } = await import('@joystyk/daily/browser');\n const client = createDailyClient({\n baseUrl: services.daily?.baseUrl ?? '/daily',\n gameId,\n config: rules.schemas.config as Validator<unknown>,\n storage: player.device,\n ...(services.daily?.fetch === undefined ? {} : { fetch: services.daily.fetch }),\n });\n return client.prefetch(today);\n },\n }));\n"],"mappings":";;AAgBA,IAAa,oBAAoB,UAA4B,YAC3D,QAAQ,KAAK,YAAY;CACvB;CACA,UAAU,OAAO,UAAkB;EACjC,MAAM,SAAS,SAAS,MAAM;EAC9B,IAAI,WAAW,KAAA,GAAW,OAAO;EACjC,MAAM,QAAQ,MAAM,OAAO,MAAM;EACjC,IAAI,CAAC,MAAM,SAAS,MAAM,SAAS,OAAO,GAAG,OAAO;EACpD,MAAM,SAAS,MAAM,SAAS,WAAW;EACzC,IAAI,OAAO,WAAW,KAAA,GAAW,OAAO;EACxC,IAAI,MAAM,SAAS,cAAc,MAAM;GACrC,MAAM,cAAc,MAAM,OAAO,QAAQ,YAAY,IAAI;GACzD,IAAI,CAAC,YAAY,MAAM,YAAY,MAAM,sBAAsB,MAAM,OAAO;EAC9E;EACA,MAAM,SAAS,MAAM,MAAM,KAAK;EAChC,MAAM,QAAQ,OAAO,UAAU,OAAO;EACtC,IAAI,UAAU,KAAA,GAAW,OAAO;EAChC,MAAM,EAAE,sBAAsB,MAAM,OAAO;EAQ3C,OAPe,kBAAkB;GAC/B,SAAS,SAAS,OAAO,WAAW;GACpC;GACA,QAAQ,MAAM,QAAQ;GACtB,SAAS,OAAO;GAChB,GAAI,SAAS,OAAO,UAAU,KAAA,IAAY,CAAC,IAAI,EAAE,OAAO,SAAS,MAAM,MAAM;EAC/E,CACO,CAAA,CAAO,SAAS,KAAK;CAC9B;AACF,EAAE"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { createLauncherServices, cryptoSeed, findLauncherServices, mediaSystem, provideLauncherServices, type DailyServices, type GameLoader, type GameLoaders, type LauncherServiceOptions, type LauncherServices, type PlayerContext, type RoomAnswer, type RoomServices, type RunnerPorts, type SystemSource, } from './services.js';
|
|
2
|
+
export { dailyPrefetchers, type DailyPrefetch } from './daily-prefetch.js';
|
|
3
|
+
export { requestSave, SAFE_POINT_EVENT, SAVE_REQUEST_EVENT, watchSafePoints, type SafePointDetail, type SaveRequestDetail, } from '../safe-point.js';
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { dailyPrefetchers } from "./daily-prefetch.js";
|
|
2
|
+
import { createLauncherServices, cryptoSeed, findLauncherServices, mediaSystem, provideLauncherServices } from "./services.js";
|
|
3
|
+
import { SAFE_POINT_EVENT, SAVE_REQUEST_EVENT, requestSave, watchSafePoints } from "../safe-point.js";
|
|
4
|
+
export { SAFE_POINT_EVENT, SAVE_REQUEST_EVENT, createLauncherServices, cryptoSeed, dailyPrefetchers, findLauncherServices, mediaSystem, provideLauncherServices, requestSave, watchSafePoints };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { browserSocket, createRoom, fetchRoomInfo, randomActionId, roomsSocketUrl, webStorageTokens } from "@joystyk/session/browser/rooms";
|
|
2
|
+
//#region src/browser/rooms.ts
|
|
3
|
+
var pageRooms = (page = location) => {
|
|
4
|
+
const origin = page.origin;
|
|
5
|
+
return {
|
|
6
|
+
create: async (gameId, name) => {
|
|
7
|
+
const created = await createRoom(origin, {
|
|
8
|
+
game: gameId,
|
|
9
|
+
name
|
|
10
|
+
});
|
|
11
|
+
if (!created.ok) return {
|
|
12
|
+
ok: false,
|
|
13
|
+
message: created.error.message
|
|
14
|
+
};
|
|
15
|
+
return {
|
|
16
|
+
ok: true,
|
|
17
|
+
value: {
|
|
18
|
+
room: created.value.room,
|
|
19
|
+
token: created.value.token,
|
|
20
|
+
notice: created.value.notice?.message ?? null
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
},
|
|
24
|
+
find: async (code) => {
|
|
25
|
+
const found = await fetchRoomInfo(origin, code);
|
|
26
|
+
return found.ok ? {
|
|
27
|
+
ok: true,
|
|
28
|
+
value: { gameId: found.value.game.id }
|
|
29
|
+
} : {
|
|
30
|
+
ok: false,
|
|
31
|
+
message: found.error.message
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
connect: browserSocket(roomsSocketUrl(origin)),
|
|
35
|
+
tokens: webStorageTokens(),
|
|
36
|
+
actionId: randomActionId,
|
|
37
|
+
inviteLink: (code) => {
|
|
38
|
+
const url = new URL(page.href);
|
|
39
|
+
url.hash = "";
|
|
40
|
+
url.search = "";
|
|
41
|
+
url.searchParams.set("room", code);
|
|
42
|
+
return url.href;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
//#endregion
|
|
47
|
+
export { pageRooms };
|
|
48
|
+
|
|
49
|
+
//# sourceMappingURL=rooms.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rooms.js","names":[],"sources":["../../src/browser/rooms.ts"],"sourcesContent":["// The default room services: the room server on this page's own origin. In production\n// Traefik routes `/api/rooms` and `/ws` on the site's host to the room service (DEPLOY.md);\n// in development the site's dev server forwards them to a local apps/rooms. Loaded with the\n// stage, so a page nobody plays never fetches the protocol's schemas.\nimport {\n browserSocket,\n createRoom,\n fetchRoomInfo,\n randomActionId,\n roomsSocketUrl,\n webStorageTokens,\n} from '@joystyk/session/browser/rooms';\nimport type { RoomServices } from './services.ts';\n\nexport const pageRooms = (page: Location = location): RoomServices => {\n const origin = page.origin;\n return {\n create: async (gameId, name) => {\n const created = await createRoom(origin, { game: gameId, name });\n if (!created.ok) return { ok: false, message: created.error.message };\n return {\n ok: true,\n value: { room: created.value.room, token: created.value.token, notice: created.value.notice?.message ?? null },\n };\n },\n find: async (code) => {\n const found = await fetchRoomInfo(origin, code);\n return found.ok\n ? { ok: true, value: { gameId: found.value.game.id } }\n : { ok: false, message: found.error.message };\n },\n connect: browserSocket(roomsSocketUrl(origin)),\n tokens: webStorageTokens(),\n actionId: randomActionId,\n inviteLink: (code) => {\n const url = new URL(page.href);\n url.hash = '';\n url.search = '';\n url.searchParams.set('room', code);\n return url.href;\n },\n };\n};\n"],"mappings":";;AAcA,IAAa,aAAa,OAAiB,aAA2B;CACpE,MAAM,SAAS,KAAK;CACpB,OAAO;EACL,QAAQ,OAAO,QAAQ,SAAS;GAC9B,MAAM,UAAU,MAAM,WAAW,QAAQ;IAAE,MAAM;IAAQ;GAAK,CAAC;GAC/D,IAAI,CAAC,QAAQ,IAAI,OAAO;IAAE,IAAI;IAAO,SAAS,QAAQ,MAAM;GAAQ;GACpE,OAAO;IACL,IAAI;IACJ,OAAO;KAAE,MAAM,QAAQ,MAAM;KAAM,OAAO,QAAQ,MAAM;KAAO,QAAQ,QAAQ,MAAM,QAAQ,WAAW;IAAK;GAC/G;EACF;EACA,MAAM,OAAO,SAAS;GACpB,MAAM,QAAQ,MAAM,cAAc,QAAQ,IAAI;GAC9C,OAAO,MAAM,KACT;IAAE,IAAI;IAAM,OAAO,EAAE,QAAQ,MAAM,MAAM,KAAK,GAAG;GAAE,IACnD;IAAE,IAAI;IAAO,SAAS,MAAM,MAAM;GAAQ;EAChD;EACA,SAAS,cAAc,eAAe,MAAM,CAAC;EAC7C,QAAQ,iBAAiB;EACzB,UAAU;EACV,aAAa,SAAS;GACpB,MAAM,MAAM,IAAI,IAAI,KAAK,IAAI;GAC7B,IAAI,OAAO;GACX,IAAI,SAAS;GACb,IAAI,aAAa,IAAI,QAAQ,IAAI;GACjC,OAAO,IAAI;EACb;CACF;AACF"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { GameEntry, RngState } from '@joystyk/contracts';
|
|
2
|
+
import type { Clock, FrameScheduler, Reporter, SocketFactory, TokenStore, Visibility } from '@joystyk/session';
|
|
3
|
+
import type { PlayerStorage, Storage, StorageNotice, SystemEnvironment } from '@joystyk/storage';
|
|
4
|
+
/** How the app loads one game: its entry, and a worker for heavy AI if the game has one. */
|
|
5
|
+
export type GameLoader = {
|
|
6
|
+
/** The game's `entry.ts` (manifest plus lazy loaders), usually a dynamic import. */
|
|
7
|
+
readonly entry: () => Promise<GameEntry<HTMLElement>>;
|
|
8
|
+
/**
|
|
9
|
+
* For a manifest with `ai.heavy`: a worker running the game's `jobs/ai.worker.ts`
|
|
10
|
+
* (`new Worker(new URL(…, import.meta.url), { type: 'module' })`). Without it the runner
|
|
11
|
+
* plays heavy AI inline and reports it.
|
|
12
|
+
*/
|
|
13
|
+
readonly aiWorker?: () => Worker;
|
|
14
|
+
/**
|
|
15
|
+
* For an engine that requests jobs a local game may run (`where: 'either'`, such as Greed's
|
|
16
|
+
* throws): a fresh worker for one job of `kind`, running the game's `jobs/<kind>.worker.ts`.
|
|
17
|
+
* Without it the runner fails those jobs, and the engine decides what that means.
|
|
18
|
+
*/
|
|
19
|
+
readonly jobWorker?: (kind: string) => Worker;
|
|
20
|
+
};
|
|
21
|
+
/** The games a page can launch, by id. */
|
|
22
|
+
export type GameLoaders = Readonly<Record<string, GameLoader>>;
|
|
23
|
+
/** The guest, with their storage. */
|
|
24
|
+
export type PlayerContext = {
|
|
25
|
+
readonly playerId: string;
|
|
26
|
+
readonly displayName: string;
|
|
27
|
+
readonly storage: PlayerStorage;
|
|
28
|
+
/** The device's storage, which the daily puzzle's content cache and results use (`@joystyk/daily/browser`). */
|
|
29
|
+
readonly device?: Storage;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Where the daily mode finds published puzzles, and the wall clock that says which UTC day it
|
|
33
|
+
* is (D-46). Tests pass a fake `fetch` and a fixed clock.
|
|
34
|
+
*/
|
|
35
|
+
export type DailyServices = {
|
|
36
|
+
/** Default "/daily", the site's published-content path (content-pipeline.md, "Delivery"). */
|
|
37
|
+
readonly baseUrl?: string;
|
|
38
|
+
readonly fetch?: (url: string, init?: RequestInit) => Promise<Response>;
|
|
39
|
+
/** Epoch milliseconds. Default `Date.now`. */
|
|
40
|
+
readonly now?: () => number;
|
|
41
|
+
};
|
|
42
|
+
/** The system settings `resolvePreferences` falls back on, now and on change. */
|
|
43
|
+
export interface SystemSource {
|
|
44
|
+
get(): SystemEnvironment;
|
|
45
|
+
subscribe(listener: (environment: SystemEnvironment) => void): () => void;
|
|
46
|
+
}
|
|
47
|
+
/** A room server answer: the value, or why not, worded for players. */
|
|
48
|
+
export type RoomAnswer<T> = {
|
|
49
|
+
readonly ok: true;
|
|
50
|
+
readonly value: T;
|
|
51
|
+
} | {
|
|
52
|
+
readonly ok: false;
|
|
53
|
+
readonly message: string;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* How the stage reaches the room server (D-48). The default (`browser/rooms.ts`, loaded with
|
|
57
|
+
* the stage) uses this page's origin, where Traefik routes `/api/rooms` and `/ws` to the room
|
|
58
|
+
* service; tests pass their own.
|
|
59
|
+
*/
|
|
60
|
+
export interface RoomServices {
|
|
61
|
+
/** `POST /api/rooms`: a new room with the creator in seat 0, and a notice when the name was replaced. */
|
|
62
|
+
create(gameId: string, name: string): Promise<RoomAnswer<{
|
|
63
|
+
readonly room: string;
|
|
64
|
+
readonly token: string;
|
|
65
|
+
readonly notice: string | null;
|
|
66
|
+
}>>;
|
|
67
|
+
/** `GET /api/rooms/:code`: which game the room plays. */
|
|
68
|
+
find(code: string): Promise<RoomAnswer<{
|
|
69
|
+
readonly gameId: string;
|
|
70
|
+
}>>;
|
|
71
|
+
readonly connect: SocketFactory;
|
|
72
|
+
readonly tokens: TokenStore;
|
|
73
|
+
/** A fresh action ID: 8–40 URL-safe characters. */
|
|
74
|
+
actionId(): string;
|
|
75
|
+
/** The invite link: this game page with `?room=CODE`. */
|
|
76
|
+
inviteLink(code: string): string;
|
|
77
|
+
}
|
|
78
|
+
export type RunnerPorts = {
|
|
79
|
+
readonly clock: Clock;
|
|
80
|
+
readonly visibility: Visibility;
|
|
81
|
+
readonly frames: FrameScheduler;
|
|
82
|
+
readonly report: Reporter;
|
|
83
|
+
};
|
|
84
|
+
export interface LauncherServices {
|
|
85
|
+
readonly games: GameLoaders;
|
|
86
|
+
/** Opens storage and the guest identity; the same promise every time. */
|
|
87
|
+
openPlayer(): Promise<PlayerContext>;
|
|
88
|
+
readonly system: SystemSource;
|
|
89
|
+
readonly ports: RunnerPorts;
|
|
90
|
+
/** A fresh, unpredictable seed for a new game (and, separately, its AI). */
|
|
91
|
+
newSeed(): RngState;
|
|
92
|
+
/** Storage problems worded for players (a full disk, private browsing). Returns how to stop. */
|
|
93
|
+
onNotice(listener: (message: string) => void): () => void;
|
|
94
|
+
/** The pause before an inline AI moves, so players can follow. Default 500 ms. */
|
|
95
|
+
readonly aiDelayMs?: number;
|
|
96
|
+
/** Deep-freeze snapshots (development). Default true. */
|
|
97
|
+
readonly freeze?: boolean;
|
|
98
|
+
/** The room server for Online with friends. Default: this page's origin. */
|
|
99
|
+
readonly rooms?: RoomServices;
|
|
100
|
+
/** The daily puzzle's content and clock. Default: this page's `/daily/` and the device clock. */
|
|
101
|
+
readonly daily?: DailyServices;
|
|
102
|
+
}
|
|
103
|
+
/** Makes `services` available to every launcher below `target` (the document, or a test container). */
|
|
104
|
+
export declare const provideLauncherServices: (target: Node, services: LauncherServices) => (() => void);
|
|
105
|
+
/** The services of the nearest provider above `from`, crossing shadow roots, or null. */
|
|
106
|
+
export declare const findLauncherServices: (from: Node) => LauncherServices | null;
|
|
107
|
+
/** The system settings from media queries, following changes. */
|
|
108
|
+
export declare const mediaSystem: (view?: Window) => SystemSource;
|
|
109
|
+
/** A seed from 128 bits of the browser's cryptographic randomness. */
|
|
110
|
+
export declare const cryptoSeed: () => RngState;
|
|
111
|
+
export type LauncherServiceOptions = {
|
|
112
|
+
readonly games: GameLoaders;
|
|
113
|
+
/** Opens storage. Default: IndexedDB (`openBrowserStorage`), falling back to memory. */
|
|
114
|
+
readonly openStorage?: (onNotice: (notice: StorageNotice) => void) => Promise<Storage>;
|
|
115
|
+
readonly system?: SystemSource;
|
|
116
|
+
readonly ports?: Partial<RunnerPorts>;
|
|
117
|
+
readonly newSeed?: () => RngState;
|
|
118
|
+
readonly aiDelayMs?: number;
|
|
119
|
+
readonly freeze?: boolean;
|
|
120
|
+
readonly rooms?: RoomServices;
|
|
121
|
+
readonly daily?: DailyServices;
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* The page's services. Storage and identity open on the first game, not on page load, and
|
|
125
|
+
* their code loads then too.
|
|
126
|
+
*/
|
|
127
|
+
export declare const createLauncherServices: (options: LauncherServiceOptions) => LauncherServices;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { seedState } from "@joystyk/kit/rng";
|
|
2
|
+
import { browserPorts } from "@joystyk/session/browser";
|
|
3
|
+
//#region src/browser/services.ts
|
|
4
|
+
var provided = /* @__PURE__ */ new WeakMap();
|
|
5
|
+
/** Makes `services` available to every launcher below `target` (the document, or a test container). */
|
|
6
|
+
var provideLauncherServices = (target, services) => {
|
|
7
|
+
provided.set(target, services);
|
|
8
|
+
return () => {
|
|
9
|
+
if (provided.get(target) === services) provided.delete(target);
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
/** The services of the nearest provider above `from`, crossing shadow roots, or null. */
|
|
13
|
+
var findLauncherServices = (from) => {
|
|
14
|
+
let node = from;
|
|
15
|
+
while (node !== null) {
|
|
16
|
+
const found = provided.get(node);
|
|
17
|
+
if (found !== void 0) return found;
|
|
18
|
+
node = node.parentNode ?? (node instanceof ShadowRoot ? node.host : null);
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
};
|
|
22
|
+
/** The system settings from media queries, following changes. */
|
|
23
|
+
var mediaSystem = (view = window) => {
|
|
24
|
+
const motion = view.matchMedia("(prefers-reduced-motion: reduce)");
|
|
25
|
+
const dark = view.matchMedia("(prefers-color-scheme: dark)");
|
|
26
|
+
const get = () => ({
|
|
27
|
+
prefersReducedMotion: motion.matches,
|
|
28
|
+
prefersDark: dark.matches,
|
|
29
|
+
locale: view.navigator.language
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
get,
|
|
33
|
+
subscribe: (listener) => {
|
|
34
|
+
const onChange = () => listener(get());
|
|
35
|
+
motion.addEventListener("change", onChange);
|
|
36
|
+
dark.addEventListener("change", onChange);
|
|
37
|
+
return () => {
|
|
38
|
+
motion.removeEventListener("change", onChange);
|
|
39
|
+
dark.removeEventListener("change", onChange);
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
/** A seed from 128 bits of the browser's cryptographic randomness. */
|
|
45
|
+
var cryptoSeed = () => seedState(crypto.randomUUID());
|
|
46
|
+
/**
|
|
47
|
+
* The page's services. Storage and identity open on the first game, not on page load, and
|
|
48
|
+
* their code loads then too.
|
|
49
|
+
*/
|
|
50
|
+
var createLauncherServices = (options) => {
|
|
51
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
52
|
+
const notify = (notice) => {
|
|
53
|
+
for (const listener of [...listeners]) listener(notice.message);
|
|
54
|
+
};
|
|
55
|
+
let player = null;
|
|
56
|
+
const openPlayer = async () => {
|
|
57
|
+
const storage = await (options.openStorage ?? (async (onNotice) => (await import("@joystyk/storage/browser")).openBrowserStorage({ onNotice })))(notify);
|
|
58
|
+
const { createBrowserIdentity } = await import("@joystyk/identity/browser");
|
|
59
|
+
const profile = await createBrowserIdentity(storage).current();
|
|
60
|
+
return {
|
|
61
|
+
playerId: profile.id,
|
|
62
|
+
displayName: profile.displayName,
|
|
63
|
+
storage: storage.player(profile.id),
|
|
64
|
+
device: storage
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
return {
|
|
68
|
+
games: options.games,
|
|
69
|
+
openPlayer: () => {
|
|
70
|
+
player ??= openPlayer().catch((error) => {
|
|
71
|
+
player = null;
|
|
72
|
+
throw error;
|
|
73
|
+
});
|
|
74
|
+
return player;
|
|
75
|
+
},
|
|
76
|
+
system: options.system ?? mediaSystem(),
|
|
77
|
+
ports: {
|
|
78
|
+
...browserPorts(),
|
|
79
|
+
...options.ports
|
|
80
|
+
},
|
|
81
|
+
newSeed: options.newSeed ?? cryptoSeed,
|
|
82
|
+
onNotice: (listener) => {
|
|
83
|
+
listeners.add(listener);
|
|
84
|
+
return () => listeners.delete(listener);
|
|
85
|
+
},
|
|
86
|
+
...options.aiDelayMs === void 0 ? {} : { aiDelayMs: options.aiDelayMs },
|
|
87
|
+
...options.freeze === void 0 ? {} : { freeze: options.freeze },
|
|
88
|
+
...options.rooms === void 0 ? {} : { rooms: options.rooms },
|
|
89
|
+
...options.daily === void 0 ? {} : { daily: options.daily }
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
//#endregion
|
|
93
|
+
export { createLauncherServices, cryptoSeed, findLauncherServices, mediaSystem, provideLauncherServices };
|
|
94
|
+
|
|
95
|
+
//# sourceMappingURL=services.js.map
|