@moku-labs/game 0.9.0 → 0.11.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.
Files changed (53) hide show
  1. package/README.md +81 -22
  2. package/bin/moku-game.mjs +4 -0
  3. package/dist/app/page.d.mts +44 -0
  4. package/dist/app/page.mjs +379 -0
  5. package/dist/app/system.d.mts +144 -0
  6. package/dist/app/system.mjs +329 -0
  7. package/dist/app.d.mts +37 -0
  8. package/dist/app.mjs +235 -0
  9. package/dist/assets-BHwTLVta.mjs +7391 -0
  10. package/dist/assets.d.mts +1 -1
  11. package/dist/assets.mjs +1 -7390
  12. package/dist/cli.d.mts +70 -0
  13. package/dist/cli.mjs +1685 -0
  14. package/dist/{commands-DuwCxK5t.d.mts → commands-C4QlqijJ.d.mts} +5 -5
  15. package/dist/config-CdARrrhU.mjs +81 -0
  16. package/dist/control.d.mts +11 -10
  17. package/dist/control.mjs +3 -3
  18. package/dist/{define-B6pHpdBi.d.mts → define-CCc0ozKe.d.mts} +2 -2
  19. package/dist/{dev-wKLgOw7c.mjs → dev-Cc50Dk7H.mjs} +1 -1
  20. package/dist/drafts-BawpSaVp.mjs +156 -0
  21. package/dist/fake-MpzwZhIZ.mjs +87 -0
  22. package/dist/{headless-SCQXJxQf.d.mts → headless-G4nuZ-xz.d.mts} +1 -1
  23. package/dist/hot.d.mts +4 -6
  24. package/dist/hot.mjs +8 -9
  25. package/dist/{index-BO9kZzY3.d.mts → index-8TiJQjQA.d.mts} +30 -17
  26. package/dist/index.d.mts +5 -5
  27. package/dist/index.mjs +1 -1
  28. package/dist/inspect-la_KkjCS.mjs +1 -0
  29. package/dist/inspect.d.mts +7 -7
  30. package/dist/inspect.mjs +3 -2
  31. package/dist/jsx-dev-runtime.d.mts +1 -1
  32. package/dist/jsx-runtime.d.mts +1 -1
  33. package/dist/lint.mjs +10 -6
  34. package/dist/memory-CWlqhS7l.mjs +136 -0
  35. package/dist/memory-DwDhfFWK.d.mts +50 -0
  36. package/dist/project.mjs +3 -3
  37. package/dist/{run-CofJEdYx.mjs → run-wYty_Lwo.mjs} +2 -2
  38. package/dist/{runtime-CdNJ0UBm.d.mts → runtime-DOBYM6g4.d.mts} +1 -1
  39. package/dist/{input-z_35Jwfn.mjs → session-DgukhR_7.mjs} +20 -20
  40. package/dist/{sources-DNnzyCYf.mjs → sources-Duzj6lhV.mjs} +2 -2
  41. package/dist/{src-536arXjV.mjs → src-BSbtHZWk.mjs} +35 -300
  42. package/dist/testing.d.mts +4 -50
  43. package/dist/testing.mjs +3 -87
  44. package/dist/{types-VZVWwqCg.d.mts → types-BM4shh8K.d.mts} +259 -259
  45. package/dist/{types-xXfyk56Q.d.mts → types-BUwDHKXm.d.mts} +1377 -1377
  46. package/dist/types-Ca_z_4tQ.d.mts +586 -0
  47. package/dist/{types-CQueODzo.d.mts → types-CefgYzFt.d.mts} +2 -2
  48. package/dist/{types-DUMoYPYe.d.mts → types-DAdwEJMp.d.mts} +2 -2
  49. package/dist/{types-B0v5VVa3.d.mts → types-ZQLYXo5X.d.mts} +304 -304
  50. package/dist/visual.d.mts +6 -6
  51. package/dist/visual.mjs +5 -5
  52. package/llms.txt +120 -43
  53. package/package.json +30 -2
@@ -0,0 +1,586 @@
1
+ import { Dt as AnyFlow, Ht as ClockSource, Jt as Config, Wt as FakeClock, Xt as Json, a as Config$3, g as Config$2, ht as FeaturePlugin, n as AssetsIo, pt as Config$1, tn as PlayerStateProvider, u as Manifest, v as PixiModule } from "./types-ZQLYXo5X.mjs";
2
+ import { o as AudioContextLike, s as Config$4 } from "./types-BM4shh8K.mjs";
3
+ import { ht as audioPlugin, i as screen, it as platformPlugin, ot as PlatformProvider, pt as effectsPlugin, t as createApp } from "./index-8TiJQjQA.mjs";
4
+ import { t as memory } from "./memory-DwDhfFWK.mjs";
5
+ import { AnyPluginInstance } from "@moku-labs/core";
6
+
7
+ //#region src/app/types.d.ts
8
+ /**
9
+ * The name of a `@moku-labs/system` plugin the game shell wires, as the system package names it.
10
+ * Only the five a game shell uses.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * const wanted: SystemName[] = ["lifecycle", "back", "haptics", "keepAwake"];
15
+ * ```
16
+ */
17
+ type SystemName = "lifecycle" | "back" | "haptics" | "keepAwake" | "store";
18
+ /**
19
+ * Where the player's save lives: in memory for this page only, in `localStorage`, or in the
20
+ * system store (idb on the web, the Tauri store in the native shell).
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * const save: SaveKind = "local"; // progress survives a reload of the page
25
+ * ```
26
+ */
27
+ type SaveKind = "memory" | "local" | "store";
28
+ /**
29
+ * What `config.ts` of a game holds: plain data, no call. The page, the native build and the shell
30
+ * read it; the game logic never does.
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * // config.ts of a game
35
+ * import type { GameConfig } from "@moku-labs/game/app";
36
+ * export default {
37
+ * page: { title: "Лесной городок", lang: "ru", background: "#10161d", orientation: "portrait", icons: { favicon: "assets/icon.png" } },
38
+ * native: { name: "Лесной городок", identifier: "com.mokulabs.timber", icon: "assets/icon.png" },
39
+ * system: ["lifecycle", "back", "haptics", "keepAwake"],
40
+ * save: "memory",
41
+ * assets: { layers: { shared: "ui" } }
42
+ * } satisfies GameConfig;
43
+ * ```
44
+ */
45
+ type GameConfig = {
46
+ /** The HTML page the game runs in. */page: {
47
+ /** The page title. Required, never empty. */title: string; /** The `lang` of the page. Default `"en"`. */
48
+ lang?: string; /** CSS colour of the page behind the canvas, also the native window. Default `"#000000"`. */
49
+ background?: string; /** The orientation the native build locks. Default `"portrait"`. */
50
+ orientation?: "portrait" | "landscape" | "any"; /** Icon files, relative to the game folder. None by default. */
51
+ icons?: {
52
+ favicon?: string;
53
+ appleTouch?: string;
54
+ }; /** Raw tags written into `<head>`, verbatim, in order. None by default. */
55
+ head?: readonly string[];
56
+ }; /** The native app. Left out, the game has no native build. */
57
+ native?: {
58
+ /** The app name under the icon. */name: string; /** The bundle identifier, reverse DNS. */
59
+ identifier: string; /** A 1024 px PNG, relative to the game folder. A placeholder by default. */
60
+ icon?: string; /** The native targets a build makes. The target of the command by default. */
61
+ targets?: readonly ("ios" | "android" | "macos")[];
62
+ }; /** The system plugins the shell wires. Default `[]`, a web page with no shell. */
63
+ system?: readonly SystemName[]; /** Where the save lives. Default `"memory"`. */
64
+ save?: SaveKind; /** The asset build. Default: no layers. */
65
+ assets?: {
66
+ /** Asset layers by name: `{ shared: "ui" }` packs the shared layer from `features/ui`. */layers?: Readonly<Record<string, string>>;
67
+ };
68
+ };
69
+ /**
70
+ * `GameConfig` with every default filled, as `resolveConfig` returns it. The page, the CLI and the
71
+ * shell read this one, never the raw config.
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * const settings: ResolvedGameConfig = {
76
+ * page: { title: "mini-game", lang: "en", background: "#000000", orientation: "portrait", icons: {}, head: [] },
77
+ * native: undefined, system: [], save: "memory", assets: { layers: {} }
78
+ * };
79
+ * ```
80
+ */
81
+ type ResolvedGameConfig = {
82
+ /** The page, every field set. */page: Required<GameConfig["page"]>; /** The native app, or `undefined` for a game with no native build. */
83
+ native: GameConfig["native"]; /** The system plugins the shell wires. */
84
+ system: readonly SystemName[]; /** Where the save lives. */
85
+ save: SaveKind; /** The asset build. */
86
+ assets: {
87
+ layers: Readonly<Record<string, string>>;
88
+ };
89
+ };
90
+ /**
91
+ * The engine part of a screen app, in dependency order: the screen set, `audio`, `effects`,
92
+ * `platform`. The game's features and plugins come after it.
93
+ */
94
+ type ScreenSet = readonly [...typeof screen, typeof audioPlugin, typeof effectsPlugin, typeof platformPlugin];
95
+ /**
96
+ * The screen app of a game whose own plugins are `Plugins`, as `createApp` types it.
97
+ *
98
+ * @example
99
+ * ```ts
100
+ * type App = ScreenAppOf<readonly [typeof scorePlugin]>; // app.ui, app.platform, app.score
101
+ * ```
102
+ */
103
+ type ScreenAppOf<Plugins extends readonly AnyPluginInstance[]> = ReturnType<typeof createApp<[...ScreenSet, ...Plugins]>>;
104
+ /**
105
+ * The headless app of a game whose headless plugins are `Plugins`, as `createApp` types it.
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * type App = HeadlessAppOf<readonly []>; // app.time, .lifecycle, .model, .clock, .flow, .log, .env
110
+ * ```
111
+ */
112
+ type HeadlessAppOf<Plugins extends readonly AnyPluginInstance[]> = ReturnType<typeof createApp<[...Plugins]>>;
113
+ /** Every `pluginConfigs` key of the screen app of a game, as `createApp` accepts them. */
114
+ type AllConfigs<Plugins extends readonly AnyPluginInstance[]> = NonNullable<NonNullable<Parameters<typeof createApp<[...ScreenSet, ...Plugins]>>[0]>["pluginConfigs"]>;
115
+ /** The plugin configs the shell writes as a whole, or in part: they leave the game's type. */
116
+ type SeamOwned = "model" | "clock" | "platform" | "flow" | "renderer" | "assets" | "audio";
117
+ /**
118
+ * The `pluginConfigs` a game writes in `defineGameApp`: every plugin of its screen app, without
119
+ * the keys the shell writes from the seams. `clock` and `platform` are gone; `model` keeps
120
+ * `schemaVersion` and `migrations`; `flow` loses `mainFlow` and `safeNode`; `renderer` loses
121
+ * `mount`; `assets` loses `manifest` and `io`; `audio` loses `context`. A seam-owned key is a
122
+ * compile error, never a value the shallow merge would drop.
123
+ *
124
+ * @example
125
+ * ```ts
126
+ * defineGameApp({ flow: mainFlow, player, session, pluginConfigs: { ui: { tapTargetPt: 48 } } }); // passes
127
+ * defineGameApp({ flow: mainFlow, player, session, pluginConfigs: { clock: {} } }); // compile error: the shell owns clock
128
+ * ```
129
+ */
130
+ type GamePluginConfigs<Plugins extends readonly AnyPluginInstance[] = readonly []> = Omit<AllConfigs<Plugins>, SeamOwned> & {
131
+ /** The save schema of the game. The shell writes the provider, the seed and the starting state. */model?: Pick<Partial<Config>, "schemaVersion" | "migrations">; /** The runner limits. The shell writes `mainFlow` and `safeNode` from the definition. */
132
+ flow?: Omit<Partial<Config$1>, "mainFlow" | "safeNode">; /** The canvas. The shell writes `mount`; a seam's `loadPixi` and `preference` win. */
133
+ renderer?: Omit<Partial<Config$2>, "mount">; /** The texture budget and the CDN prefix. The shell writes `manifest` and `io`. */
134
+ assets?: Omit<Partial<Config$3>, "manifest" | "io">; /** Buses, volumes, music. The shell writes `context`; a seam's `journal` wins. */
135
+ audio?: Omit<Partial<Config$4>, "context">;
136
+ };
137
+ /**
138
+ * What `defineGameApp` takes: the whole game as one data object. Nothing here is a seam: the
139
+ * clock, the save provider, the manifest and the platform come from `headless()` and `screen()`.
140
+ *
141
+ * @example
142
+ * ```ts
143
+ * // tests/fixtures/mini-game/index.ts
144
+ * const definition: GameDefinition = {
145
+ * flow: mainFlow, safeNode: "home", player: { count: 0 }, session: { opened: 0 },
146
+ * features: [homeFeature, infoFeature], headless: { features: [infoFeature] }
147
+ * };
148
+ * ```
149
+ */
150
+ type GameDefinition<Plugins extends readonly AnyPluginInstance[] = readonly [], HeadlessPlugins extends readonly AnyPluginInstance[] = readonly [], Player extends Json = Json, Session extends Json = Json> = {
151
+ /** The main flow. */flow: AnyFlow; /** The checkpoint entered after a failed retry. Default: the start of the main flow. */
152
+ safeNode?: string; /** The player a new save starts from. */
153
+ player: Player; /** The session at every start. */
154
+ session: Session; /** The long side the layout needs, in reference units. Default 1920. */
155
+ referenceLong?: number; /** The rng seed of a new save. Default 42. */
156
+ seed?: number; /** The shared layer: composed first among the features. */
157
+ shared?: FeaturePlugin; /** The features of the screen app, in order, after `shared`. */
158
+ features?: readonly FeaturePlugin[]; /** The game's own plugins, after the features. Their APIs land on `screen().app`. */
159
+ plugins?: Plugins; /** What the headless app composes: the logic of these features and these plugins, nothing else. */
160
+ headless?: {
161
+ features?: readonly FeaturePlugin[];
162
+ plugins?: HeadlessPlugins;
163
+ }; /** Plugin configs of the game. Seam-owned keys are not accepted. */
164
+ pluginConfigs?: NoInfer<GamePluginConfigs<Plugins>>;
165
+ };
166
+ /**
167
+ * The in-memory save provider a seam gets when it passes none: it records every call.
168
+ *
169
+ * @example
170
+ * ```ts
171
+ * const { app, provider } = game.headless();
172
+ * await createHeadless(app); // flow.run() loads the save and commits a new player at once
173
+ * provider.calls.map(call => call.method); // ["load", "commit"]
174
+ * ```
175
+ */
176
+ type MemoryProvider = ReturnType<typeof memory>;
177
+ /**
178
+ * What one call of `headless()` or `screen()` gives: the app, not started, and the clock and save
179
+ * provider it runs on, so a test can move the time and read the save.
180
+ *
181
+ * @example
182
+ * ```ts
183
+ * const { app, clock, provider } = game.headless({ seed: 7 });
184
+ * ```
185
+ */
186
+ type GameHandle<App, Source, Provider> = {
187
+ /**
188
+ * The app, not started.
189
+ *
190
+ * @example
191
+ * ```ts
192
+ * const { app } = game.headless();
193
+ * await app.start(); // the caller starts it, and runs the graph
194
+ * ```
195
+ */
196
+ app: App;
197
+ /**
198
+ * The clock source the app runs on: the fake clock at `startMoment` unless a seam passed one.
199
+ *
200
+ * @example
201
+ * ```ts
202
+ * const { clock } = game.headless();
203
+ * clock.advance(60_000); // clock.now() is 1060000
204
+ * ```
205
+ */
206
+ clock: Source;
207
+ /**
208
+ * The save provider the app runs on: a fresh memory provider unless a seam passed one.
209
+ *
210
+ * @example
211
+ * ```ts
212
+ * const { provider } = game.headless();
213
+ * provider.calls; // []: flow.run() loads the save, start() does not
214
+ * ```
215
+ */
216
+ provider: Provider;
217
+ };
218
+ /**
219
+ * What a test or the page may pin on a headless app. Every field is optional; the definition and
220
+ * the engine defaults fill the rest.
221
+ *
222
+ * @example
223
+ * ```ts
224
+ * game.headless({ seed: 7, player: { count: 3 } });
225
+ * ```
226
+ */
227
+ type HeadlessSeams<Player, Session, Source, Provider> = {
228
+ /**
229
+ * The rng seed of a new save. Default: the definition's seed, else 42.
230
+ *
231
+ * @example
232
+ * ```ts
233
+ * game.headless({ seed: 7 }); // a new save draws from seed 7
234
+ * ```
235
+ */
236
+ seed?: number;
237
+ /**
238
+ * The clock source. Default: `fakeClock(startMoment)`; the page passes the device clock.
239
+ *
240
+ * @example
241
+ * ```ts
242
+ * game.headless({ clock: fakeClock(5000) }).clock.now(); // 5000
243
+ * ```
244
+ */
245
+ clock?: Source;
246
+ /**
247
+ * The save provider. Default: a fresh `memory()`.
248
+ *
249
+ * @example
250
+ * ```ts
251
+ * game.headless({ provider: memory({ state: saveOf({ count: 3 }, 42), version: 1 }) }); // a returning player
252
+ * ```
253
+ */
254
+ provider?: Provider;
255
+ /**
256
+ * The player a new save starts from. Default: the definition's player.
257
+ *
258
+ * @example
259
+ * ```ts
260
+ * game.headless({ player: { count: 3 } }); // a new save counts 3
261
+ * ```
262
+ */
263
+ player?: Player;
264
+ /**
265
+ * The session at start. Default: the definition's session.
266
+ *
267
+ * @example
268
+ * ```ts
269
+ * game.headless({ session: { opened: 2 } });
270
+ * ```
271
+ */
272
+ session?: Session;
273
+ };
274
+ /**
275
+ * What a test or the page may pin on a screen app: the headless seams, and the manifest, the
276
+ * file seam, the platform and the screen-on switch, the audio and the renderer seams.
277
+ *
278
+ * @example
279
+ * ```ts
280
+ * game.screen({ manifest: "/manifest.json", renderer: { mount: "#game" } });
281
+ * ```
282
+ */
283
+ type ScreenSeams<Player, Session, Source, Provider> = HeadlessSeams<Player, Session, Source, Provider> & {
284
+ /**
285
+ * The manifest the assets plugin reads: a URL on the page, the parsed file in a test.
286
+ *
287
+ * @example
288
+ * ```ts
289
+ * game.screen({ manifest: await readManifest() }); // tests/integration/mini-helpers.ts
290
+ * ```
291
+ */
292
+ manifest?: string | Manifest;
293
+ /**
294
+ * The file seam of the assets plugin. Left out, the browser fetch, or headless in a test.
295
+ *
296
+ * @example
297
+ * ```ts
298
+ * game.screen({ manifest, io: folderIo().io }); // the fixture's files, read from disk
299
+ * ```
300
+ */
301
+ io?: AssetsIo;
302
+ /**
303
+ * The platform provider. Left out, the platform plugin is inert and `back()` answers `"none"`.
304
+ *
305
+ * @example
306
+ * ```ts
307
+ * const { app } = game.screen({ platform: provider });
308
+ * await app.start();
309
+ * void app.flow.run();
310
+ * app.platform.back(); // "exit" once the graph rests on Home: Home does not list "back"
311
+ * ```
312
+ */
313
+ platform?: PlatformProvider;
314
+ /**
315
+ * Keeps the screen on while the game runs. Default `false`.
316
+ *
317
+ * @example
318
+ * ```ts
319
+ * await game.screen({ platform: provider, keepAwake: true }).app.start(); // provider.keepAwake(true)
320
+ * ```
321
+ */
322
+ keepAwake?: boolean;
323
+ /**
324
+ * The audio seams: the context factory, and how many started sounds the journal keeps.
325
+ *
326
+ * @example
327
+ * ```ts
328
+ * game.screen({ audio: { context: () => fakeContext, journal: 200 } }); // the dev page keeps 200
329
+ * ```
330
+ */
331
+ audio?: {
332
+ context?: () => AudioContextLike;
333
+ journal?: number;
334
+ };
335
+ /**
336
+ * The renderer seams: where the canvas goes, how Pixi loads, which backend it asks for.
337
+ *
338
+ * @example
339
+ * ```ts
340
+ * game.screen({ renderer: { mount: "#game", preference: "webgl" } }); // the page under ?renderer=webgl
341
+ * ```
342
+ */
343
+ renderer?: {
344
+ mount?: string | HTMLElement;
345
+ loadPixi?: () => Promise<PixiModule>;
346
+ preference?: "webgpu" | "webgl";
347
+ };
348
+ };
349
+ /**
350
+ * The game, as `defineGameApp` returns it: two ways to make its app. `headless()` composes the
351
+ * logic only, for tests; `screen()` composes the engine's screen set, the features and the game's
352
+ * plugins, for the page and the screen tests. Neither starts the app.
353
+ *
354
+ * @example
355
+ * ```ts
356
+ * import game from "../index";
357
+ * const { app, clock, provider } = game.headless({ seed: 7 });
358
+ * ```
359
+ */
360
+ type GameApp<Plugins extends readonly AnyPluginInstance[] = readonly [], HeadlessPlugins extends readonly AnyPluginInstance[] = readonly [], Player extends Json = Json, Session extends Json = Json> = {
361
+ /**
362
+ * Makes the logic app: the core plugins, the logic of `headless.features` and the
363
+ * `headless.plugins`. No screen plugin, no game plugin unless listed. Each call gives a fresh
364
+ * app, clock and provider.
365
+ *
366
+ * @param seams - What the test pins: seed, clock, provider, player, session.
367
+ * @returns The app, not started, and its clock and provider.
368
+ * @example
369
+ * ```ts
370
+ * const { app, clock } = game.headless({ seed: 7 });
371
+ * clock.now(); // 1000000, the fake clock at startMoment
372
+ * const run = await createHeadless(app);
373
+ * run.state().path; // "home"
374
+ * ```
375
+ */
376
+ headless<Source extends ClockSource = FakeClock, Provider extends PlayerStateProvider = MemoryProvider>(seams?: HeadlessSeams<Player, Session, Source, Provider>): GameHandle<HeadlessAppOf<HeadlessPlugins>, Source, Provider>;
377
+ /**
378
+ * Makes the screen app: the screen set, `audio`, `effects`, `platform`, then `shared`, the
379
+ * features and the game's plugins, in that order. Without a mount the renderer is inert, so the
380
+ * same app runs in a test. Each call gives a fresh app, clock and provider.
381
+ *
382
+ * @param seams - What the test or the page pins, the headless seams and the screen seams.
383
+ * @returns The app, not started, and its clock and provider.
384
+ * @example
385
+ * ```ts
386
+ * const { app } = game.screen({ manifest: await readManifest(), io: folderIo().io });
387
+ * await app.start();
388
+ * void app.flow.run(); // the page runs the graph; the mini game starts on its Home checkpoint
389
+ * app.flow.state().path; // "home" once the runner rests
390
+ * ```
391
+ */
392
+ screen<Source extends ClockSource = FakeClock, Provider extends PlayerStateProvider = MemoryProvider>(seams?: ScreenSeams<Player, Session, Source, Provider>): GameHandle<ScreenAppOf<Plugins>, Source, Provider>;
393
+ };
394
+ /**
395
+ * Any game, whatever its plugins and state: what the page takes.
396
+ *
397
+ * @example
398
+ * ```ts
399
+ * const page: AnyGameApp = game; // the default export of a game's index.ts
400
+ * ```
401
+ */
402
+ type AnyGameApp = GameApp<readonly AnyPluginInstance[], readonly AnyPluginInstance[], Json, Json>;
403
+ /**
404
+ * The screen app of a game, as `game.screen().app` types it.
405
+ *
406
+ * @example
407
+ * ```ts
408
+ * type MiniApp = ScreenAppOfGame<typeof game>; // has app.flow, app.ui, app.platform
409
+ * ```
410
+ */
411
+ type ScreenAppOfGame<Game extends AnyGameApp> = ReturnType<Game["screen"]>["app"];
412
+ /**
413
+ * One prepared save of the dev page, picked with `?player=<name>`: the default export of
414
+ * `tests/scenarios/<name>.ts`. It gets the device time, so a timer can be due already.
415
+ *
416
+ * @example
417
+ * ```ts
418
+ * // tests/scenarios/full.ts of a game: `?player=full` opens on a full board, a refill due in 60 s.
419
+ * const full: Scenario<Player> = now => ({ player: { ...startingPlayer, board: fullBoard, refillAt: now + 60_000 } });
420
+ * export default full;
421
+ * ```
422
+ */
423
+ type Scenario<Player = Json, Session = Json> = (now: number) => {
424
+ player: Player;
425
+ session?: Session;
426
+ };
427
+ /**
428
+ * A dev agent the page starts after the app: the default export of an agent module the CLI names,
429
+ * the editor's among them. It gets the running app, the page title and the game's `.dev` modules.
430
+ *
431
+ * @example
432
+ * ```ts
433
+ * // .moku/main.ts, written by moku-game dev: the editor's agent module's default export.
434
+ * await startPage(game, config, { agents: [agent0] }); // agent0({ app, name: "mini-game", modules: [] })
435
+ * ```
436
+ */
437
+ type PageAgent<App = ScreenAppOfGame<AnyGameApp>> = (page: {
438
+ app: App;
439
+ name: string;
440
+ modules: readonly object[];
441
+ }) => void | Promise<void>;
442
+ /**
443
+ * Builds the system shell from the resolved config: what `systemShellOf` of
444
+ * `@moku-labs/game/app/system` returns over the loaders of the page. The page reaches the shell
445
+ * only through this.
446
+ *
447
+ * @example
448
+ * ```ts
449
+ * const factory: SystemShellFactory = systemShellOf({ system: () => import("@moku-labs/system"), back: () => import("@moku-labs/system/back") });
450
+ * await startPage(game, config, { system: factory }); // the page builds the shell of config.system: ["back"]
451
+ * ```
452
+ */
453
+ type SystemShellFactory = (config: ResolvedGameConfig, report: (problem: string) => void) => Promise<SystemShell>;
454
+ /**
455
+ * What the generated `.moku/main.ts` hands the page besides the game and its config.
456
+ *
457
+ * @example
458
+ * ```ts
459
+ * const system = systemShellOf({ system: () => import("@moku-labs/system"), haptics: () => import("@moku-labs/system/haptics") });
460
+ * const options: PageOptions = { scenarios: { ready }, system, agents: [agent0], devModules: [devModule0] };
461
+ * ```
462
+ */
463
+ type PageOptions = {
464
+ /** Prepared saves by name, for `?player=`. Dev only; the build passes none. */scenarios?: Readonly<Record<string, Scenario>>; /** The system shell factory, when `config.ts` names a system plugin or the store save. */
465
+ system?: SystemShellFactory; /** Agents started after the app. Dev only. */
466
+ agents?: readonly PageAgent[]; /** The game's `.dev` modules, handed to the agents. Dev only. */
467
+ devModules?: readonly object[];
468
+ };
469
+ /**
470
+ * What `startPage` resolves to: the running screen app, and the system app for
471
+ * `globalThis.system` (`undefined` without a shell).
472
+ *
473
+ * @example
474
+ * ```ts
475
+ * const { app } = await startPage(game, config);
476
+ * app.flow.state().path; // "home" once the runner rests
477
+ * ```
478
+ */
479
+ type StartedPage<Game extends AnyGameApp> = {
480
+ /** The screen app, started. */app: ScreenAppOfGame<Game>; /** The `@moku-labs/system` app, or `undefined` without a shell. Typed `unknown`: see `SystemShell.handle`. */
481
+ system: unknown;
482
+ };
483
+ /**
484
+ * The modules of `@moku-labs/system` the page loads, one loader per name of `config.ts`: the
485
+ * package itself always, and the entry of each plugin the game names, the store for a store save.
486
+ * The generated `main.ts` writes only the loaders of the names, so the page bundles only those
487
+ * plugins and never meets the native package of another. Typed `unknown`: a system type here would
488
+ * land in the `.d.mts` of every game and break the typecheck of a web-only one.
489
+ *
490
+ * @example
491
+ * ```ts
492
+ * // A page of its own for a game whose config.ts names haptics, with a memory save.
493
+ * const modules: SystemModules = {
494
+ * system: () => import("@moku-labs/system"),
495
+ * haptics: () => import("@moku-labs/system/haptics")
496
+ * };
497
+ * await startPage(game, config, { system: systemShellOf(modules) }); // the bundle holds no store plugin
498
+ * ```
499
+ */
500
+ type SystemModules = {
501
+ /** `@moku-labs/system` itself, for its `createApp`. Always there. */system: () => Promise<{
502
+ createApp: unknown;
503
+ }>; /** `@moku-labs/system/lifecycle`, when the game names `lifecycle`. */
504
+ lifecycle?: () => Promise<{
505
+ lifecyclePlugin: unknown;
506
+ }>; /** `@moku-labs/system/back`, when the game names `back`. */
507
+ back?: () => Promise<{
508
+ backPlugin: unknown;
509
+ }>; /** `@moku-labs/system/haptics`, when the game names `haptics`. */
510
+ haptics?: () => Promise<{
511
+ hapticsPlugin: unknown;
512
+ }>; /** `@moku-labs/system/keep-awake`, when the game names `keepAwake`. */
513
+ keepAwake?: () => Promise<{
514
+ keepAwakePlugin: unknown;
515
+ }>; /** `@moku-labs/system/store`, when the game names `store` or its save is `"store"`. */
516
+ store?: () => Promise<{
517
+ storePlugin: unknown;
518
+ }>;
519
+ };
520
+ /**
521
+ * The system shell: the `@moku-labs/system` app the config names, and what the engine takes from
522
+ * it. The factory of `systemShellOf` builds it; the page starts it before the game and stops it
523
+ * after.
524
+ *
525
+ * @example
526
+ * ```ts
527
+ * const build = systemShellOf({ system: () => import("@moku-labs/system"), store: () => import("@moku-labs/system/store") });
528
+ * const shell = await build(resolveConfig({ page: { title: "T" }, save: "store" }), problem => problems.push(problem));
529
+ * game.screen({ platform: shell.platform, provider: shell.save });
530
+ * ```
531
+ */
532
+ type SystemShell = {
533
+ /**
534
+ * The `@moku-labs/system` app, for `globalThis.system`. Typed `unknown`: a system type here
535
+ * would land in the `.d.mts` of every game and break the typecheck of a web-only one.
536
+ *
537
+ * @example
538
+ * ```ts
539
+ * Reflect.set(globalThis, "system", shell.handle); // e2e: system.haptics.selection()
540
+ * ```
541
+ */
542
+ handle: unknown;
543
+ /**
544
+ * The engine's platform provider over the system app. A capability the config does not name is
545
+ * inert.
546
+ *
547
+ * @example
548
+ * ```ts
549
+ * shell.platform.haptic("success"); // the shell plays system.haptics.notify("success")
550
+ * ```
551
+ */
552
+ platform: PlatformProvider;
553
+ /**
554
+ * The save provider over the system store, set when `config.save` is `"store"`.
555
+ *
556
+ * @example
557
+ * ```ts
558
+ * game.screen({ provider: shell.save }); // the save survives a restart of the native app
559
+ * ```
560
+ */
561
+ save: PlayerStateProvider | undefined;
562
+ /**
563
+ * Starts the system app. The page starts it before the game, so keep-awake finds it running.
564
+ *
565
+ * @returns Resolves when the system app runs.
566
+ * @example
567
+ * ```ts
568
+ * await shell.start();
569
+ * await app.start();
570
+ * ```
571
+ */
572
+ start(): Promise<void>;
573
+ /**
574
+ * Stops the system app.
575
+ *
576
+ * @returns Resolves when the system app stopped.
577
+ * @example
578
+ * ```ts
579
+ * await app.stop();
580
+ * await shell.stop();
581
+ * ```
582
+ */
583
+ stop(): Promise<void>;
584
+ };
585
+ //#endregion
586
+ export { SystemShellFactory as _, GameHandle as a, MemoryProvider as c, SaveKind as d, Scenario as f, SystemName as g, SystemModules as h, GameDefinition as i, PageAgent as l, StartedPage as m, GameApp as n, GamePluginConfigs as o, ScreenSeams as p, GameConfig as r, HeadlessSeams as s, AnyGameApp as t, PageOptions as u };
@@ -1,5 +1,5 @@
1
- import { D as Point, E as FilterSlot, L as ComponentHandle, V as Entity, _ as PixiModule, bn as Events$1, g as PixiFilter, k as Api$1, m as Api$2, o as Events, t as Api$3, ut as Api, v as PixiSprite, xn as Require, y as PixiTexture, z as ComponentType } from "./types-B0v5VVa3.mjs";
2
- import { b as AnimApi } from "./types-VZVWwqCg.mjs";
1
+ import { A as Api$1, B as ComponentType, D as FilterSlot, H as Entity, O as Point, R as ComponentHandle, Sn as Require, _ as PixiFilter, b as PixiTexture, dt as Api, h as Api$2, s as Events, t as Api$3, v as PixiModule, xn as Events$1, y as PixiSprite } from "./types-ZQLYXo5X.mjs";
2
+ import { x as AnimApi } from "./types-BM4shh8K.mjs";
3
3
  import { Log } from "@moku-labs/common/browser";
4
4
  import { PluginCtx } from "@moku-labs/core";
5
5
 
@@ -1,5 +1,5 @@
1
- import { Jt as Json, rn as Snapshot } from "./types-B0v5VVa3.mjs";
2
- import { t as HeadlessApp } from "./headless-SCQXJxQf.mjs";
1
+ import { Xt as Json, an as Snapshot } from "./types-ZQLYXo5X.mjs";
2
+ import { t as HeadlessApp } from "./headless-G4nuZ-xz.mjs";
3
3
  import { Log } from "@moku-labs/common/browser";
4
4
 
5
5
  //#region src/plugins/flow/doors/types.d.ts