@moku-labs/game 0.10.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.
@@ -1,4 +1,4 @@
1
- import { h as StartedPage, r as GameConfig, t as AnyGameApp, u as PageOptions } from "../types-hQgI_wrq.mjs";
1
+ import { m as StartedPage, r as GameConfig, t as AnyGameApp, u as PageOptions } from "../types-Ca_z_4tQ.mjs";
2
2
 
3
3
  //#region src/app/page.d.ts
4
4
  /**
@@ -20,7 +20,7 @@ import { h as StartedPage, r as GameConfig, t as AnyGameApp, u as PageOptions }
20
20
  * // Written by moku-game dev. Do not edit.
21
21
  * import "./dev.ts";
22
22
  * import { startPage } from "@moku-labs/game/app/page";
23
- * import { systemShell } from "@moku-labs/game/app/system";
23
+ * import { systemShellOf } from "@moku-labs/game/app/system";
24
24
  * import game from "../index.ts";
25
25
  * import config from "../config.ts";
26
26
  * import scenario0 from "../tests/scenarios/empty.ts";
@@ -30,7 +30,10 @@ import { h as StartedPage, r as GameConfig, t as AnyGameApp, u as PageOptions }
30
30
  *
31
31
  * await startPage(game, config, {
32
32
  * scenarios: { "empty": scenario0, "full": scenario1 },
33
- * system: systemShell,
33
+ * system: systemShellOf({
34
+ * system: () => import("@moku-labs/system"),
35
+ * lifecycle: () => import("@moku-labs/system/lifecycle")
36
+ * }),
34
37
  * agents: [agent0],
35
38
  * devModules: [devModule0]
36
39
  * });
package/dist/app/page.mjs CHANGED
@@ -322,7 +322,7 @@ async function startAgents(app, settings, options) {
322
322
  * // Written by moku-game dev. Do not edit.
323
323
  * import "./dev.ts";
324
324
  * import { startPage } from "@moku-labs/game/app/page";
325
- * import { systemShell } from "@moku-labs/game/app/system";
325
+ * import { systemShellOf } from "@moku-labs/game/app/system";
326
326
  * import game from "../index.ts";
327
327
  * import config from "../config.ts";
328
328
  * import scenario0 from "../tests/scenarios/empty.ts";
@@ -332,7 +332,10 @@ async function startAgents(app, settings, options) {
332
332
  *
333
333
  * await startPage(game, config, {
334
334
  * scenarios: { "empty": scenario0, "full": scenario1 },
335
- * system: systemShell,
335
+ * system: systemShellOf({
336
+ * system: () => import("@moku-labs/system"),
337
+ * lifecycle: () => import("@moku-labs/system/lifecycle")
338
+ * }),
336
339
  * agents: [agent0],
337
340
  * devModules: [devModule0]
338
341
  * });
@@ -1,6 +1,6 @@
1
1
  import { tn as PlayerStateProvider } from "../types-ZQLYXo5X.mjs";
2
- import { ot as PlatformProvider } from "../index-xxSxKJJW.mjs";
3
- import { _ as SystemShell, d as ResolvedGameConfig, g as SystemName } from "../types-hQgI_wrq.mjs";
2
+ import { ot as PlatformProvider } from "../index-8TiJQjQA.mjs";
3
+ import { _ as SystemShellFactory, g as SystemName, h as SystemModules } from "../types-Ca_z_4tQ.mjs";
4
4
  import { Back, Haptics, KeepAwake, Lifecycle, Store } from "@moku-labs/system";
5
5
 
6
6
  //#region src/app/system.d.ts
@@ -11,7 +11,8 @@ import { Back, Haptics, KeepAwake, Lifecycle, Store } from "@moku-labs/system";
11
11
  *
12
12
  * @example
13
13
  * ```ts
14
- * const slice: SystemSlice = await createSystemApp(["haptics"], "moku-game"); // slice.haptics only
14
+ * const modules = { system: () => import("@moku-labs/system"), haptics: () => import("@moku-labs/system/haptics") };
15
+ * const slice: SystemSlice = await createSystemApp(["haptics"], "moku-game", modules); // slice.haptics only
15
16
  * ```
16
17
  */
17
18
  type SystemSlice = {
@@ -36,7 +37,11 @@ type StoreSlice = Pick<Store.StoreApi, "get" | "set">;
36
37
  *
37
38
  * @example
38
39
  * ```ts
39
- * const system: SystemApp = await createSystemApp(["lifecycle", "store"], "com.mokulabs.timber");
40
+ * const system: SystemApp = await createSystemApp(["lifecycle", "store"], "com.mokulabs.timber", {
41
+ * system: () => import("@moku-labs/system"),
42
+ * lifecycle: () => import("@moku-labs/system/lifecycle"),
43
+ * store: () => import("@moku-labs/system/store")
44
+ * });
40
45
  * await system.start(); // system.lifecycle and system.store answer from here on
41
46
  * ```
42
47
  */
@@ -64,22 +69,28 @@ declare function fromSystem(system: SystemSlice): PlatformProvider;
64
69
  /**
65
70
  * Creates the `@moku-labs/system` app of the named plugins, in the fixed order `lifecycle`,
66
71
  * `back`, `haptics`, `keepAwake`, `store`, each name once. The store is named after `namespace`.
67
- * Each plugin is loaded by its own `import()`, so a page loads only the plugins its game names.
68
- * The app is not started.
72
+ * Each plugin is loaded by its loader in `modules`, and only the named ones are called, so a page
73
+ * loads only the plugins its game names. The app is not started.
69
74
  *
70
75
  * @param names - The system plugins the game names, in any order.
71
76
  * @param namespace - The store name: the native identifier, or `"moku-game"`.
77
+ * @param modules - The loaders of the page: the package, and one per named plugin.
72
78
  * @returns The system app, not started.
73
- * @throws {Error} When `@moku-labs/system` is not installed.
79
+ * @throws {Error} When a named plugin has no loader in `modules`, or a loader rejects because
80
+ * `@moku-labs/system` is not installed.
74
81
  * @example
75
82
  * ```ts
76
83
  * // A shell of its own: the store named after the app, and the haptics.
77
- * const system = await createSystemApp(["store", "haptics"], "com.mokulabs.timber");
84
+ * const system = await createSystemApp(["store", "haptics"], "com.mokulabs.timber", {
85
+ * system: () => import("@moku-labs/system"),
86
+ * haptics: () => import("@moku-labs/system/haptics"),
87
+ * store: () => import("@moku-labs/system/store")
88
+ * });
78
89
  * await system.start();
79
90
  * await system.haptics?.selection(); // { ok: false, provider: "web", reason: "unsupported" } in iOS Safari
80
91
  * ```
81
92
  */
82
- declare function createSystemApp(names: readonly SystemName[], namespace: string): Promise<SystemApp>;
93
+ declare function createSystemApp(names: readonly SystemName[], namespace: string, modules: SystemModules): Promise<SystemApp>;
83
94
  /**
84
95
  * Builds the save provider over the system store: idb on the web, the Tauri store in the native
85
96
  * app. It holds the document it loaded and applies each commit's patches to it, then queues one
@@ -102,28 +113,32 @@ declare function createSystemApp(names: readonly SystemName[], namespace: string
102
113
  */
103
114
  declare function storeSave(store: StoreSlice, key: string, report: (problem: string) => void): PlayerStateProvider;
104
115
  /**
105
- * Builds the system shell from the resolved `config.ts`: the `@moku-labs/system` app of the names
106
- * `config.system` lists, plus the store when `config.save` is `"store"`, in the fixed order. It
107
- * hands the engine the platform provider over that app and, for a store save, the save provider.
108
- * The page starts the shell before the game; this function does not start it. The generated
109
- * `main.ts` passes the function itself to `startPage`, only when `config.ts` names a system
110
- * plugin or the store save.
116
+ * Makes the system shell factory of a page over its loaders. The factory builds the shell from the
117
+ * resolved `config.ts`: the `@moku-labs/system` app of the names `config.system` lists, plus the
118
+ * store when `config.save` is `"store"`, in the fixed order. It hands the engine the platform
119
+ * provider over that app and, for a store save, the save provider. Nothing loads until the page
120
+ * calls the factory, and the page starts the shell before the game. The generated `main.ts`
121
+ * passes it to `startPage` with one loader per name, only when `config.ts` names a system plugin
122
+ * or the store save.
111
123
  *
112
- * @param config - The resolved `config.ts` of the game.
113
- * @param report - Takes a problem the page reports later, such as a write the store refused.
114
- * @returns The shell, not started.
115
- * @throws {Error} When `@moku-labs/system` is not installed.
124
+ * @param modules - The loaders of the page: the package, and one per named plugin.
125
+ * @returns The factory the page calls; it throws the messages of `createSystemApp`.
116
126
  * @example
117
127
  * ```ts
118
- * // The in-memory .moku/build/main.ts of moku-game build, for a game whose config.ts names system plugins.
128
+ * // The in-memory .moku/build/main.ts of moku-game build, for a game whose config.ts names back.
119
129
  * import { startPage } from "@moku-labs/game/app/page";
120
- * import { systemShell } from "@moku-labs/game/app/system";
130
+ * import { systemShellOf } from "@moku-labs/game/app/system";
121
131
  * import game from "../../index.ts";
122
132
  * import config from "../../config.ts";
123
133
  *
124
- * await startPage(game, config, { system: systemShell });
134
+ * await startPage(game, config, {
135
+ * system: systemShellOf({
136
+ * system: () => import("@moku-labs/system"),
137
+ * back: () => import("@moku-labs/system/back")
138
+ * })
139
+ * });
125
140
  * ```
126
141
  */
127
- declare function systemShell(config: ResolvedGameConfig, report: (problem: string) => void): Promise<SystemShell>;
142
+ declare function systemShellOf(modules: SystemModules): SystemShellFactory;
128
143
  //#endregion
129
- export { StoreSlice, SystemApp, SystemSlice, createSystemApp, fromSystem, storeSave, systemShell };
144
+ export { StoreSlice, SystemApp, type SystemModules, SystemSlice, createSystemApp, fromSystem, storeSave, systemShellOf };
@@ -8,21 +8,6 @@ const systemOrder = [
8
8
  "keepAwake",
9
9
  "store"
10
10
  ];
11
- /** One lazy import per capability plugin: the page loads only the plugins the game names. */
12
- const capabilityLoaders = {
13
- lifecycle: () => import("@moku-labs/system/lifecycle").then((module) => module.lifecyclePlugin),
14
- back: () => import("@moku-labs/system/back").then((module) => module.backPlugin),
15
- haptics: () => import("@moku-labs/system/haptics").then((module) => module.hapticsPlugin),
16
- keepAwake: () => import("@moku-labs/system/keep-awake").then((module) => module.keepAwakePlugin)
17
- };
18
- /**
19
- * Loads the store plugin, by its own lazy import.
20
- *
21
- * @returns The store plugin.
22
- */
23
- function loadStore() {
24
- return import("@moku-labs/system/store").then((module) => module.storePlugin);
25
- }
26
11
  /** The store namespace of a game with no native identifier. */
27
12
  const defaultNamespace = "moku-game";
28
13
  /** The store key of the save, inside the game's namespace. */
@@ -99,20 +84,45 @@ function fromSystem(system) {
99
84
  };
100
85
  }
101
86
  /**
102
- * Loads `@moku-labs/system` and the plugins of the names, all at once.
87
+ * Refuses a named plugin the page has no loader for, before anything loads. The generated
88
+ * `main.ts` writes a loader per name; a page of its own may miss one.
89
+ *
90
+ * @param names - The system plugins of the shell.
91
+ * @param modules - The loaders of the page.
92
+ * @throws {Error} When a name has no loader.
93
+ */
94
+ function checkLoaders(names, modules) {
95
+ const missing = names.find((name) => modules[name] === void 0);
96
+ if (missing !== void 0) throw new Error(`[game] config.system names "${missing}", but main.ts has no loader for it.`);
97
+ }
98
+ /**
99
+ * One plugin per capability, through the loader of the page. The loaders are checked first, so
100
+ * each one called is there.
101
+ */
102
+ const capabilityPlugins = {
103
+ lifecycle: (modules) => modules.lifecycle?.().then((module) => module.lifecyclePlugin),
104
+ back: (modules) => modules.back?.().then((module) => module.backPlugin),
105
+ haptics: (modules) => modules.haptics?.().then((module) => module.hapticsPlugin),
106
+ keepAwake: (modules) => modules.keepAwake?.().then((module) => module.keepAwakePlugin)
107
+ };
108
+ /**
109
+ * Loads `@moku-labs/system` and the plugins of the names, all at once, with the loaders of the
110
+ * page.
103
111
  *
104
112
  * @param names - The system plugins, in the fixed order.
113
+ * @param modules - The loaders of the page.
105
114
  * @returns The `createApp` of the package, the capability plugins in the order of the names, and
106
115
  * the store plugin when the names hold it.
107
- * @throws {Error} When the package is not installed.
116
+ * @throws {Error} When a name has no loader, or a loader rejects: the package is not installed.
108
117
  */
109
- async function loadSystem(names) {
118
+ async function loadSystem(names, modules) {
119
+ checkLoaders(names, modules);
110
120
  const capabilities = names.filter((name) => name !== "store");
111
121
  try {
112
122
  const [system, store, ...plugins] = await Promise.all([
113
- import("@moku-labs/system"),
114
- names.includes("store") ? loadStore() : void 0,
115
- ...capabilities.map((name) => capabilityLoaders[name]())
123
+ modules.system(),
124
+ names.includes("store") ? modules.store?.().then((module) => module.storePlugin) : void 0,
125
+ ...capabilities.map((name) => capabilityPlugins[name](modules))
116
126
  ]);
117
127
  return {
118
128
  createApp: system.createApp,
@@ -126,23 +136,29 @@ async function loadSystem(names) {
126
136
  /**
127
137
  * Creates the `@moku-labs/system` app of the named plugins, in the fixed order `lifecycle`,
128
138
  * `back`, `haptics`, `keepAwake`, `store`, each name once. The store is named after `namespace`.
129
- * Each plugin is loaded by its own `import()`, so a page loads only the plugins its game names.
130
- * The app is not started.
139
+ * Each plugin is loaded by its loader in `modules`, and only the named ones are called, so a page
140
+ * loads only the plugins its game names. The app is not started.
131
141
  *
132
142
  * @param names - The system plugins the game names, in any order.
133
143
  * @param namespace - The store name: the native identifier, or `"moku-game"`.
144
+ * @param modules - The loaders of the page: the package, and one per named plugin.
134
145
  * @returns The system app, not started.
135
- * @throws {Error} When `@moku-labs/system` is not installed.
146
+ * @throws {Error} When a named plugin has no loader in `modules`, or a loader rejects because
147
+ * `@moku-labs/system` is not installed.
136
148
  * @example
137
149
  * ```ts
138
150
  * // A shell of its own: the store named after the app, and the haptics.
139
- * const system = await createSystemApp(["store", "haptics"], "com.mokulabs.timber");
151
+ * const system = await createSystemApp(["store", "haptics"], "com.mokulabs.timber", {
152
+ * system: () => import("@moku-labs/system"),
153
+ * haptics: () => import("@moku-labs/system/haptics"),
154
+ * store: () => import("@moku-labs/system/store")
155
+ * });
140
156
  * await system.start();
141
157
  * await system.haptics?.selection(); // { ok: false, provider: "web", reason: "unsupported" } in iOS Safari
142
158
  * ```
143
159
  */
144
- async function createSystemApp(names, namespace) {
145
- const { createApp, capabilities, store } = await loadSystem(systemOrder.filter((name) => names.includes(name)));
160
+ async function createSystemApp(names, namespace, modules) {
161
+ const { createApp, capabilities, store } = await loadSystem(systemOrder.filter((name) => names.includes(name)), modules);
146
162
  if (store === void 0) return createApp({ plugins: capabilities });
147
163
  return createApp({
148
164
  plugins: [...capabilities, store],
@@ -271,37 +287,43 @@ function storeSave(store, key, report) {
271
287
  };
272
288
  }
273
289
  /**
274
- * Builds the system shell from the resolved `config.ts`: the `@moku-labs/system` app of the names
275
- * `config.system` lists, plus the store when `config.save` is `"store"`, in the fixed order. It
276
- * hands the engine the platform provider over that app and, for a store save, the save provider.
277
- * The page starts the shell before the game; this function does not start it. The generated
278
- * `main.ts` passes the function itself to `startPage`, only when `config.ts` names a system
279
- * plugin or the store save.
290
+ * Makes the system shell factory of a page over its loaders. The factory builds the shell from the
291
+ * resolved `config.ts`: the `@moku-labs/system` app of the names `config.system` lists, plus the
292
+ * store when `config.save` is `"store"`, in the fixed order. It hands the engine the platform
293
+ * provider over that app and, for a store save, the save provider. Nothing loads until the page
294
+ * calls the factory, and the page starts the shell before the game. The generated `main.ts`
295
+ * passes it to `startPage` with one loader per name, only when `config.ts` names a system plugin
296
+ * or the store save.
280
297
  *
281
- * @param config - The resolved `config.ts` of the game.
282
- * @param report - Takes a problem the page reports later, such as a write the store refused.
283
- * @returns The shell, not started.
284
- * @throws {Error} When `@moku-labs/system` is not installed.
298
+ * @param modules - The loaders of the page: the package, and one per named plugin.
299
+ * @returns The factory the page calls; it throws the messages of `createSystemApp`.
285
300
  * @example
286
301
  * ```ts
287
- * // The in-memory .moku/build/main.ts of moku-game build, for a game whose config.ts names system plugins.
302
+ * // The in-memory .moku/build/main.ts of moku-game build, for a game whose config.ts names back.
288
303
  * import { startPage } from "@moku-labs/game/app/page";
289
- * import { systemShell } from "@moku-labs/game/app/system";
304
+ * import { systemShellOf } from "@moku-labs/game/app/system";
290
305
  * import game from "../../index.ts";
291
306
  * import config from "../../config.ts";
292
307
  *
293
- * await startPage(game, config, { system: systemShell });
308
+ * await startPage(game, config, {
309
+ * system: systemShellOf({
310
+ * system: () => import("@moku-labs/system"),
311
+ * back: () => import("@moku-labs/system/back")
312
+ * })
313
+ * });
294
314
  * ```
295
315
  */
296
- async function systemShell(config, report) {
297
- const app = await createSystemApp(config.save === "store" ? [...config.system, "store"] : config.system, config.native?.identifier ?? defaultNamespace);
298
- return {
299
- handle: app,
300
- platform: fromSystem(app),
301
- save: app.store === void 0 ? void 0 : storeSave(app.store, saveKey, report),
302
- start: () => app.start(),
303
- stop: () => app.stop()
316
+ function systemShellOf(modules) {
317
+ return async (config, report) => {
318
+ const app = await createSystemApp(config.save === "store" ? [...config.system, "store"] : config.system, config.native?.identifier ?? defaultNamespace, modules);
319
+ return {
320
+ handle: app,
321
+ platform: fromSystem(app),
322
+ save: app.store === void 0 ? void 0 : storeSave(app.store, saveKey, report),
323
+ start: () => app.start(),
324
+ stop: () => app.stop()
325
+ };
304
326
  };
305
327
  }
306
328
  //#endregion
307
- export { createSystemApp, fromSystem, storeSave, systemShell };
329
+ export { createSystemApp, fromSystem, storeSave, systemShellOf };
package/dist/app.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Xt as Json } from "./types-ZQLYXo5X.mjs";
2
- import { a as GameHandle, c as MemoryProvider, f as SaveKind, g as SystemName, i as GameDefinition, l as PageAgent, m as ScreenSeams, n as GameApp, o as GamePluginConfigs, p as Scenario, r as GameConfig, s as HeadlessSeams } from "./types-hQgI_wrq.mjs";
2
+ import { a as GameHandle, c as MemoryProvider, d as SaveKind, f as Scenario, g as SystemName, i as GameDefinition, l as PageAgent, n as GameApp, o as GamePluginConfigs, p as ScreenSeams, r as GameConfig, s as HeadlessSeams } from "./types-Ca_z_4tQ.mjs";
3
3
  import { AnyPluginInstance } from "@moku-labs/core";
4
4
 
5
5
  //#region src/app/define.d.ts
package/dist/app.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { H as platformPlugin, i as screen, jt as audioPlugin, t as createApp, xt as effectsPlugin } from "./src-DSl1eNB1.mjs";
1
+ import { H as platformPlugin, i as screen, jt as audioPlugin, t as createApp, xt as effectsPlugin } from "./src-BSbtHZWk.mjs";
2
2
  import { t as memory } from "./memory-CWlqhS7l.mjs";
3
3
  import { t as fakeClock } from "./fake-MpzwZhIZ.mjs";
4
4
  //#region src/app/define.ts
package/dist/cli.mjs CHANGED
@@ -13,6 +13,17 @@ const SAFE_VALUE = /^[#\w(),.%\s-]+$/;
13
13
  /** The files of `tests/scenarios/` that are not scenarios. */
14
14
  const NOT_SCENARIO = /\.(?:test|spec|d)\.ts$|^index\.ts$/;
15
15
  /**
16
+ * The entry of `@moku-labs/system` each system plugin loads from, in the order the shell composes
17
+ * them.
18
+ */
19
+ const SYSTEM_ENTRIES = [
20
+ ["lifecycle", "lifecycle"],
21
+ ["back", "back"],
22
+ ["haptics", "haptics"],
23
+ ["keepAwake", "keep-awake"],
24
+ ["store", "store"]
25
+ ];
26
+ /**
16
27
  * Escapes the five characters HTML reads as markup.
17
28
  *
18
29
  * @param text - The text.
@@ -133,7 +144,7 @@ function scenarioFiles(files) {
133
144
  * Whether the page needs the system shell: a system plugin is named, or the save is the store.
134
145
  *
135
146
  * @param settings - The resolved `config.ts`.
136
- * @returns True when `main.ts` imports `systemShell`.
147
+ * @returns True when `main.ts` imports `systemShellOf`.
137
148
  * @example
138
149
  * ```ts
139
150
  * needsShell(resolveConfig({ page: { title: "T" }, save: "store" })); // true
@@ -143,6 +154,26 @@ function needsShell(settings) {
143
154
  return settings.system.length > 0 || settings.save === "store";
144
155
  }
145
156
  /**
157
+ * The `system` option of `startPage`: `systemShellOf` over the loader of the package and one
158
+ * loader per plugin the shell composes, in the fixed order. The store is one when `config.system`
159
+ * names it or the save is the store. Only these `import()`s are written, so the page bundles no
160
+ * other plugin and never needs its native package.
161
+ *
162
+ * @param settings - The resolved `config.ts`.
163
+ * @returns The option, indented two spaces, with no comma after it.
164
+ * @example
165
+ * ```ts
166
+ * shellOption(resolveConfig({ page: { title: "T" }, system: ["haptics"] })).split("\n")[2]; // ' haptics: () => import("@moku-labs/system/haptics")'
167
+ * ```
168
+ */
169
+ function shellOption(settings) {
170
+ return [
171
+ " system: systemShellOf({",
172
+ [" system: () => import(\"@moku-labs/system\")", ...SYSTEM_ENTRIES.filter(([name]) => settings.system.includes(name) || name === "store" && settings.save === "store").map(([name, entry]) => ` ${name}: () => import("@moku-labs/system/${entry}")`)].join(",\n"),
173
+ " })"
174
+ ].join("\n");
175
+ }
176
+ /**
146
177
  * The imports of `main.ts` every page has: the page entry, the shell when needed, the game and its
147
178
  * config.
148
179
  *
@@ -157,7 +188,7 @@ function needsShell(settings) {
157
188
  function pageImports(settings, toRoot) {
158
189
  return [
159
190
  "import { startPage } from \"@moku-labs/game/app/page\";",
160
- ...needsShell(settings) ? ["import { systemShell } from \"@moku-labs/game/app/system\";"] : [],
191
+ ...needsShell(settings) ? ["import { systemShellOf } from \"@moku-labs/game/app/system\";"] : [],
161
192
  `import game from "${toRoot}index.ts";`,
162
193
  `import config from "${toRoot}config.ts";`
163
194
  ];
@@ -194,8 +225,8 @@ function importLine(binding, specifier) {
194
225
  /**
195
226
  * The `main.ts` of the dev page: the dev flag first, then the page entry, the game, its config,
196
227
  * one import per scenario, and, with agents, one per `.dev` module and one per agent. It calls
197
- * `startPage` with the scenarios by file stem, the shell when the game needs it, the agents and
198
- * the `.dev` modules.
228
+ * `startPage` with the scenarios by file stem, the shell with the loaders of the named plugins
229
+ * when the game needs it, the agents and the `.dev` modules.
199
230
  *
200
231
  * @param settings - The resolved `config.ts`.
201
232
  * @param sources - The scenario files, the `.dev` modules and the agents.
@@ -212,7 +243,7 @@ function devMain(settings, sources) {
212
243
  const keys = scenarios.map((file, index) => `${JSON.stringify(file.slice(0, -3))}: scenario${index}`);
213
244
  const options = [
214
245
  ` scenarios: ${keys.length === 0 ? "{}" : `{ ${keys.join(", ")} }`}`,
215
- ...needsShell(settings) ? [" system: systemShell"] : [],
246
+ ...needsShell(settings) ? [shellOption(settings)] : [],
216
247
  ...agents.length === 0 ? [] : [` agents: [${numbered("agent", agents.length)}]`],
217
248
  ...devModules.length === 0 ? [] : [` devModules: [${numbered("devModule", devModules.length)}]`]
218
249
  ];
@@ -232,8 +263,8 @@ function devMain(settings, sources) {
232
263
  }
233
264
  /**
234
265
  * The `main.ts` of the production page: the page entry, the game and its config, and the shell
235
- * when the game needs it. No dev flag, no scenario, no agent, no `.dev` module, no door: none of
236
- * them ships.
266
+ * with the loaders of the named plugins when the game needs it. No dev flag, no scenario, no
267
+ * agent, no `.dev` module, no door: none of them ships.
237
268
  *
238
269
  * @param settings - The resolved `config.ts`.
239
270
  * @returns The text of the in-memory `.moku/build/main.ts`.
@@ -243,11 +274,15 @@ function devMain(settings, sources) {
243
274
  * ```
244
275
  */
245
276
  function buildMain(settings) {
246
- const call = needsShell(settings) ? "await startPage(game, config, { system: systemShell });" : "await startPage(game, config);";
277
+ const call = needsShell(settings) ? [
278
+ "await startPage(game, config, {",
279
+ shellOption(settings),
280
+ "});"
281
+ ] : ["await startPage(game, config);"];
247
282
  return [
248
283
  ...pageImports(settings, "../../"),
249
284
  "",
250
- call,
285
+ ...call,
251
286
  ""
252
287
  ].join("\n");
253
288
  }
@@ -476,6 +476,19 @@ type PlatformApi = {
476
476
  * ```
477
477
  */
478
478
  back(): BackResult;
479
+ /**
480
+ * Leaves the app through the provider, as the last step of `back()` does: no Escape, no intent
481
+ * `back`. A provider `exit` that throws is logged, never thrown. Without a provider it does
482
+ * nothing.
483
+ *
484
+ * @example
485
+ * ```ts
486
+ * // merge-game's exit plugin answers the fx "exit" of the Leave button.
487
+ * ctx.require(flowPlugin).fx.handle("exit", () => ctx.require(platformPlugin).exit());
488
+ * // a press of Leave: the provider leaves the app; on the web page without a provider, nothing
489
+ * ```
490
+ */
491
+ exit(): void;
479
492
  };
480
493
  /**
481
494
  * What the kernel context offers the files of the plugin.
package/dist/index.d.mts CHANGED
@@ -2,5 +2,5 @@ import { $ as DescriptionNode, $t as types_d_exports$9, C as types_d_exports$11,
2
2
  import { B as sequence, F as frames, G as stagger, H as sfx, I as haptic, J as wait, K as tween, L as mark, M as HAPTIC_KINDS, N as defineAnimation, O as types_d_exports, P as external, R as parallel, U as spawn, V as set, W as spawned, b as types_d_exports$12, f as types_d_exports$2, i as types_d_exports$15, k as HapticKind, q as use, z as play } from "./types-BM4shh8K.mjs";
3
3
  import { a as types_d_exports$4 } from "./types-CefgYzFt.mjs";
4
4
  import { C as TextFormat, D as types_d_exports$13, H as Transform, I as Parent, L as Shape, M as types_d_exports$7, N as Display, O as defineComponent, P as NineSlice, W as sprite, Z as types_d_exports$6, d as popup, f as defineTokens, g as BindOptions, l as Box, u as LocalWrite, v as CountdownValue, z as Sprite } from "./types-BUwDHKXm.mjs";
5
- import { $ as uiPlugin, A as Pointer, B as Layer, C as defineScene, Ct as label, D as DropTarget, E as Draggable, F as Tappable, G as resource, H as Tree, I as Touchable, J as guide, K as system, L as Traceable, M as Pressable, N as Pressed, O as Held, P as Swipeable, Q as worldPlugin, R as Traced, S as Frames, St as defineTextStyles, T as load, U as component, V as Order, W as mut, X as schedule, Y as hint, Z as defineFeature, _ as defineStyle, _t as animPlugin, a as defineEmitter, at as BackResult, b as AnimPlayer, bt as Text, c as Alpha, ct as modelPlugin, d as Displacement, dt as i18nPlugin, et as timePlugin, f as Glow, ft as flowPlugin, g as resolve, gt as assetsPlugin, h as music, ht as audioPlugin, i as screen, it as platformPlugin, j as PointerOver, k as Hovered, l as Blur, lt as lifecyclePlugin, m as Outline, mt as clockPlugin, n as createPlugin, nt as scenesPlugin, o as Emitter, ot as PlatformProvider, p as Noise, pt as effectsPlugin, q as tag, r as defineGame, rt as rendererPlugin, s as defineFilter, st as types_d_exports$10, t as createApp, tt as textPlugin, u as ColorMatrix, ut as inputPlugin, v as tr, vt as projection, w as defineBundles, x as Animation, xt as bind, y as defineMotion, yt as Countdown, z as Exiting } from "./index-xxSxKJJW.mjs";
5
+ import { $ as uiPlugin, A as Pointer, B as Layer, C as defineScene, Ct as label, D as DropTarget, E as Draggable, F as Tappable, G as resource, H as Tree, I as Touchable, J as guide, K as system, L as Traceable, M as Pressable, N as Pressed, O as Held, P as Swipeable, Q as worldPlugin, R as Traced, S as Frames, St as defineTextStyles, T as load, U as component, V as Order, W as mut, X as schedule, Y as hint, Z as defineFeature, _ as defineStyle, _t as animPlugin, a as defineEmitter, at as BackResult, b as AnimPlayer, bt as Text, c as Alpha, ct as modelPlugin, d as Displacement, dt as i18nPlugin, et as timePlugin, f as Glow, ft as flowPlugin, g as resolve, gt as assetsPlugin, h as music, ht as audioPlugin, i as screen, it as platformPlugin, j as PointerOver, k as Hovered, l as Blur, lt as lifecyclePlugin, m as Outline, mt as clockPlugin, n as createPlugin, nt as scenesPlugin, o as Emitter, ot as PlatformProvider, p as Noise, pt as effectsPlugin, q as tag, r as defineGame, rt as rendererPlugin, s as defineFilter, st as types_d_exports$10, t as createApp, tt as textPlugin, u as ColorMatrix, ut as inputPlugin, v as tr, vt as projection, w as defineBundles, x as Animation, xt as bind, y as defineMotion, yt as Countdown, z as Exiting } from "./index-8TiJQjQA.mjs";
6
6
  export { Alpha, types_d_exports as Anim, AnimPlayer, Animation, types_d_exports$1 as Assets, types_d_exports$2 as Audio, type BackResult, type BindOptions, Blur, Box, types_d_exports$3 as Clock, ColorMatrix, Countdown, type CountdownValue, type DescriptionNode, Displacement, Display, Draggable, DropTarget, types_d_exports$4 as Effects, Emitter, Exiting, types_d_exports$5 as Flow, Frames, Glow, HAPTIC_KINDS, type HapticKind, Held, Hovered, types_d_exports$6 as I18n, types_d_exports$7 as Input, Layer, types_d_exports$8 as Lifecycle, LocalWrite, types_d_exports$9 as Model, NineSlice, Noise, Order, Outline, Parent, types_d_exports$10 as Platform, type PlatformProvider, Pointer, PointerOver, Pressable, Pressed, types_d_exports$11 as Renderer, SaveUnreadableError, types_d_exports$12 as Scenes, Shape, Sprite, Swipeable, Tappable, Text, type TextFormat, types_d_exports$13 as TextTypes, types_d_exports$14 as Time, Touchable, Traceable, Traced, Transform, Tree, types_d_exports$15 as Ui, types_d_exports$16 as World, animPlugin, assetsPlugin, audioPlugin, bind, clockPlugin, component, createApp, createPlugin, defineAnimation, defineBundles, defineComponent, defineEmitter, defineFeature, defineFilter, defineGame, defineMotion, defineScene, defineStyle, defineTextStyles, defineTokens, effectsPlugin, exit, external, flowPlugin, frames, guide, haptic, hint, i18nPlugin, inputPlugin, label, lifecyclePlugin, load, mark, modelPlugin, music, mut, parallel, platformPlugin, play, popup, projection, rendererPlugin, resolve, resource, scenesPlugin, schedule, screen, sequence, set, sfx, slot, spawn, spawned, sprite, stagger, system, tag, textPlugin, timePlugin, to, tr, tween, type, uiPlugin, use, wait, worldPlugin };
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as sfx, A as types_exports$3, At as defineFilter, B as defineStyle, Bt as flowPlugin, C as types_exports$10, Ct as Alpha, D as types_exports$6, Dt as Glow, E as types_exports$7, Et as Displacement, F as resolve, Ft as AnimPlayer, G as external, Gt as modelPlugin, H as platformPlugin, Ht as slot, I as Box, It as Animation, J as mark, Jt as timePlugin, K as frames, Kt as SaveUnreadableError, L as LocalWrite, Lt as Frames, M as types_exports$1, Mt as scenesPlugin, N as types_exports, Nt as assetsPlugin, O as types_exports$5, Ot as Noise, P as uiPlugin, Pt as animPlugin, Q as set, R as popup, Rt as rendererPlugin, S as types_exports$11, St as Emitter, T as types_exports$8, Tt as ColorMatrix, U as HAPTIC_KINDS, Ut as to, V as textPlugin, Vt as exit, W as defineAnimation, Wt as type, X as play, Y as parallel, Yt as clockPlugin, Z as sequence, _ as types_exports$16, _t as Touchable, a as defineMotion, at as wait, b as types_exports$13, bt as i18nPlugin, c as schedule, ct as DropTarget, d as tr, dt as Pointer, et as spawn, f as defineFeature, ft as PointerOver, g as load, gt as Tappable, h as defineBundles, ht as Swipeable, i as screen, it as use, j as types_exports$2, jt as audioPlugin, k as types_exports$4, kt as Outline, l as projection, lt as Held, m as music, mt as Pressed, n as createPlugin, nt as stagger, o as guide, ot as inputPlugin, p as defineEmitter, pt as Pressable, q as haptic, qt as lifecyclePlugin, r as defineGame, rt as tween, s as hint, st as Draggable, t as createApp, tt as spawned, u as defineScene, ut as Hovered, v as types_exports$15, vt as Traceable, w as types_exports$9, wt as Blur, x as types_exports$12, xt as effectsPlugin, y as types_exports$14, yt as Traced, z as defineTokens, zt as worldPlugin } from "./src-DSl1eNB1.mjs";
1
+ import { $ as sfx, A as types_exports$3, At as defineFilter, B as defineStyle, Bt as flowPlugin, C as types_exports$10, Ct as Alpha, D as types_exports$6, Dt as Glow, E as types_exports$7, Et as Displacement, F as resolve, Ft as AnimPlayer, G as external, Gt as modelPlugin, H as platformPlugin, Ht as slot, I as Box, It as Animation, J as mark, Jt as timePlugin, K as frames, Kt as SaveUnreadableError, L as LocalWrite, Lt as Frames, M as types_exports$1, Mt as scenesPlugin, N as types_exports, Nt as assetsPlugin, O as types_exports$5, Ot as Noise, P as uiPlugin, Pt as animPlugin, Q as set, R as popup, Rt as rendererPlugin, S as types_exports$11, St as Emitter, T as types_exports$8, Tt as ColorMatrix, U as HAPTIC_KINDS, Ut as to, V as textPlugin, Vt as exit, W as defineAnimation, Wt as type, X as play, Y as parallel, Yt as clockPlugin, Z as sequence, _ as types_exports$16, _t as Touchable, a as defineMotion, at as wait, b as types_exports$13, bt as i18nPlugin, c as schedule, ct as DropTarget, d as tr, dt as Pointer, et as spawn, f as defineFeature, ft as PointerOver, g as load, gt as Tappable, h as defineBundles, ht as Swipeable, i as screen, it as use, j as types_exports$2, jt as audioPlugin, k as types_exports$4, kt as Outline, l as projection, lt as Held, m as music, mt as Pressed, n as createPlugin, nt as stagger, o as guide, ot as inputPlugin, p as defineEmitter, pt as Pressable, q as haptic, qt as lifecyclePlugin, r as defineGame, rt as tween, s as hint, st as Draggable, t as createApp, tt as spawned, u as defineScene, ut as Hovered, v as types_exports$15, vt as Traceable, w as types_exports$9, wt as Blur, x as types_exports$12, xt as effectsPlugin, y as types_exports$14, yt as Traced, z as defineTokens, zt as worldPlugin } from "./src-BSbtHZWk.mjs";
2
2
  import { A as system, C as Exiting, D as component, E as Tree, O as mut, S as sprite, T as Order, _ as Parent, b as Transform, c as Text, d as defineTextStyles, f as label, g as NineSlice, h as Display, j as tag, k as resource, l as bind, s as Countdown, v as Shape, w as Layer, y as Sprite } from "./visual-dMW8iqAE.mjs";
3
3
  import { r as defineComponent } from "./flatten-BkXuscAV.mjs";
4
4
  export { Alpha, types_exports as Anim, AnimPlayer, Animation, types_exports$1 as Assets, types_exports$2 as Audio, Blur, Box, types_exports$3 as Clock, ColorMatrix, Countdown, Displacement, Display, Draggable, DropTarget, types_exports$4 as Effects, Emitter, Exiting, types_exports$5 as Flow, Frames, Glow, HAPTIC_KINDS, Held, Hovered, types_exports$6 as I18n, types_exports$7 as Input, Layer, types_exports$8 as Lifecycle, LocalWrite, types_exports$9 as Model, NineSlice, Noise, Order, Outline, Parent, types_exports$10 as Platform, Pointer, PointerOver, Pressable, Pressed, types_exports$11 as Renderer, SaveUnreadableError, types_exports$12 as Scenes, Shape, Sprite, Swipeable, Tappable, Text, types_exports$13 as TextTypes, types_exports$14 as Time, Touchable, Traceable, Traced, Transform, Tree, types_exports$15 as Ui, types_exports$16 as World, animPlugin, assetsPlugin, audioPlugin, bind, clockPlugin, component, createApp, createPlugin, defineAnimation, defineBundles, defineComponent, defineEmitter, defineFeature, defineFilter, defineGame, defineMotion, defineScene, defineStyle, defineTextStyles, defineTokens, effectsPlugin, exit, external, flowPlugin, frames, guide, haptic, hint, i18nPlugin, inputPlugin, label, lifecyclePlugin, load, mark, modelPlugin, music, mut, parallel, platformPlugin, play, popup, projection, rendererPlugin, resolve, resource, scenesPlugin, schedule, screen, sequence, set, sfx, slot, spawn, spawned, sprite, stagger, system, tag, textPlugin, timePlugin, to, tr, tween, type, uiPlugin, use, wait, worldPlugin };
@@ -23490,13 +23490,24 @@ function setAwake(ctx, provider, on) {
23490
23490
  //#endregion
23491
23491
  //#region src/plugins/platform/api.ts
23492
23492
  /**
23493
- * @file platform plugin — API factory and the Back chain. One press runs three steps and the
23494
- * first that takes it wins: the Escape button of the top popup, the intent `back` of the resting
23495
- * node, the provider leaving the app.
23493
+ * @file platform plugin — API factory, the Back chain and the exit. One press runs three steps and
23494
+ * the first that takes it wins: the Escape button of the top popup, the intent `back` of the
23495
+ * resting node, the provider leaving the app. `exit()` is that last step alone.
23496
23496
  */
23497
23497
  /** The intent a resting node lists to take the Back press itself. */
23498
23498
  const BACK_INTENT = "back";
23499
23499
  /**
23500
+ * Leaves the app through the provider, guarded like every provider call. Without a provider it
23501
+ * does nothing. The last step of the Back chain and `exit()` both go through here.
23502
+ *
23503
+ * @param ctx - Kernel context of the platform plugin.
23504
+ */
23505
+ function leave(ctx) {
23506
+ const provider = ctx.config.provider;
23507
+ if (provider === void 0) return;
23508
+ safely(ctx.log, "exit", () => provider.exit());
23509
+ }
23510
+ /**
23500
23511
  * Runs the Back chain once. `back()` and the provider's `onBack` both go through here, so a test
23501
23512
  * and a real press end the same way. Escape is what ui answers with the `escape` button of its
23502
23513
  * top root; it also ends the editing of a text field. While the graph moves between nodes the gate
@@ -23507,22 +23518,24 @@ const BACK_INTENT = "back";
23507
23518
  * @returns What took the press, `"none"` without a provider.
23508
23519
  */
23509
23520
  function runBack(ctx) {
23510
- const provider = ctx.config.provider;
23511
- if (provider === void 0) return "none";
23521
+ if (ctx.config.provider === void 0) return "none";
23512
23522
  if (ctx.require(inputPlugin).pressKey("Escape")) return "popup";
23513
23523
  const gate = ctx.require(flowPlugin).gate;
23514
23524
  if (gate.answer({ intent: BACK_INTENT }) || !gate.state().open) return "intent";
23515
- safely(ctx.log, "exit", () => provider.exit());
23525
+ leave(ctx);
23516
23526
  return "exit";
23517
23527
  }
23518
23528
  /**
23519
- * Creates the platform API: the Back chain on demand.
23529
+ * Creates the platform API: the Back chain and the exit on demand.
23520
23530
  *
23521
23531
  * @param ctx - Kernel context of the platform plugin.
23522
23532
  * @returns The plugin API.
23523
23533
  */
23524
23534
  function createPlatformApi(ctx) {
23525
- return { back: () => runBack(ctx) };
23535
+ return {
23536
+ back: () => runBack(ctx),
23537
+ exit: () => leave(ctx)
23538
+ };
23526
23539
  }
23527
23540
  //#endregion
23528
23541
  //#region src/plugins/platform/state.ts
@@ -34254,7 +34267,7 @@ function defineMotion(spec) {
34254
34267
  * |---|---|
34255
34268
  * | `@moku-labs/game/app` | `defineGameApp`, the game as one data object with `headless()` and `screen()`; `startMoment`; the types of `index.ts` and `config.ts` (`GameConfig`) |
34256
34269
  * | `@moku-labs/game/app/page` | `startPage`, the page the generated `.moku/main.ts` calls: `?player=` scenarios, the system shell, the dev agents |
34257
- * | `@moku-labs/game/app/system` | the system shell over the optional peer `@moku-labs/system`: `systemShell`, `fromSystem`, `storeSave`, `createSystemApp` |
34270
+ * | `@moku-labs/game/app/system` | the system shell over the optional peer `@moku-labs/system`: `systemShellOf`, `fromSystem`, `storeSave`, `createSystemApp` |
34258
34271
  * | `@moku-labs/game/cli` | `runCli` of the bin `moku-game` and `preparePage` for the editor, node and bun only |
34259
34272
  * | `@moku-labs/game/testing` | headless play, repro runs, isolated feature tests (`isolate`, `stub`), the in-memory save, the fake clock |
34260
34273
  * | `@moku-labs/game/visual` | the visual tests, node and bun only |
@@ -1,5 +1,5 @@
1
1
  import { Dt as AnyFlow, Ht as ClockSource, Nt as NodeTable, Wt as FakeClock, Xt as Json, ht as FeaturePlugin } from "./types-ZQLYXo5X.mjs";
2
- import { t as createApp } from "./index-xxSxKJJW.mjs";
2
+ import { t as createApp } from "./index-8TiJQjQA.mjs";
3
3
  import { n as saveOf, t as memory } from "./memory-DwDhfFWK.mjs";
4
4
  import { a as createHeadless, i as ReproResult, n as HeadlessGame, o as runRepro, r as Repro, s as stepFrames, t as HeadlessApp } from "./headless-G4nuZ-xz.mjs";
5
5
  import { AnyPluginInstance } from "@moku-labs/core";
package/dist/testing.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { Vt as exit, Wt as type, t as createApp } from "./src-DSl1eNB1.mjs";
1
+ import { Vt as exit, Wt as type, t as createApp } from "./src-BSbtHZWk.mjs";
2
2
  import { n as saveOf, t as memory } from "./memory-CWlqhS7l.mjs";
3
3
  import { t as fakeClock } from "./fake-MpzwZhIZ.mjs";
4
4
  import { i as stepFrames, r as runRepro, t as createHeadless } from "./headless-BsNdJs9W.mjs";
@@ -1,6 +1,6 @@
1
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
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-xxSxKJJW.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
4
  import { t as memory } from "./memory-DwDhfFWK.mjs";
5
5
  import { AnyPluginInstance } from "@moku-labs/core";
6
6
 
@@ -440,12 +440,14 @@ type PageAgent<App = ScreenAppOfGame<AnyGameApp>> = (page: {
440
440
  modules: readonly object[];
441
441
  }) => void | Promise<void>;
442
442
  /**
443
- * Builds the system shell from the resolved config: the type of `systemShell` of
444
- * `@moku-labs/game/app/system`. The page reaches the shell only through this.
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.
445
446
  *
446
447
  * @example
447
448
  * ```ts
448
- * await startPage(game, config, { system: systemShell }); // systemShell is a SystemShellFactory
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"]
449
451
  * ```
450
452
  */
451
453
  type SystemShellFactory = (config: ResolvedGameConfig, report: (problem: string) => void) => Promise<SystemShell>;
@@ -454,7 +456,8 @@ type SystemShellFactory = (config: ResolvedGameConfig, report: (problem: string)
454
456
  *
455
457
  * @example
456
458
  * ```ts
457
- * const options: PageOptions = { scenarios: { ready }, system: systemShell, agents: [agent0], devModules: [devModule0] };
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] };
458
461
  * ```
459
462
  */
460
463
  type PageOptions = {
@@ -477,13 +480,52 @@ type StartedPage<Game extends AnyGameApp> = {
477
480
  /** The screen app, started. */app: ScreenAppOfGame<Game>; /** The `@moku-labs/system` app, or `undefined` without a shell. Typed `unknown`: see `SystemShell.handle`. */
478
481
  system: unknown;
479
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
+ };
480
520
  /**
481
521
  * The system shell: the `@moku-labs/system` app the config names, and what the engine takes from
482
- * it. `systemShell` builds it; the page starts it before the game and stops it after.
522
+ * it. The factory of `systemShellOf` builds it; the page starts it before the game and stops it
523
+ * after.
483
524
  *
484
525
  * @example
485
526
  * ```ts
486
- * const shell = await systemShell(resolveConfig(config), problem => problems.push(problem));
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));
487
529
  * game.screen({ platform: shell.platform, provider: shell.save });
488
530
  * ```
489
531
  */
@@ -541,4 +583,4 @@ type SystemShell = {
541
583
  stop(): Promise<void>;
542
584
  };
543
585
  //#endregion
544
- export { SystemShell as _, GameHandle as a, MemoryProvider as c, ResolvedGameConfig as d, SaveKind as f, SystemName as g, StartedPage as h, GameDefinition as i, PageAgent as l, ScreenSeams as m, GameApp as n, GamePluginConfigs as o, Scenario as p, GameConfig as r, HeadlessSeams as s, AnyGameApp as t, PageOptions as u };
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 };
package/llms.txt CHANGED
@@ -9,7 +9,7 @@ Facts: Bun only. ESM only. `"sideEffects": false`. `pixi.js` `^8.0.0` is a peer
9
9
  - `@moku-labs/game`: everything a game imports.
10
10
  - `@moku-labs/game/app`: `defineGameApp`, `startMoment` and the types of a game's `index.ts` and `config.ts`. No `node:` import, no system or native package. See "The game shell".
11
11
  - `@moku-labs/game/app/page` (the browser): `startPage`, the page the generated `.moku/main.ts` calls.
12
- - `@moku-labs/game/app/system` (the browser, the native shell): `systemShell`, `fromSystem`, `storeSave`, `createSystemApp` over the optional peer `@moku-labs/system`. The one entry that reaches it.
12
+ - `@moku-labs/game/app/system` (the browser, the native shell): `systemShellOf`, `fromSystem`, `storeSave`, `createSystemApp`, the type `SystemModules`, over the optional peer `@moku-labs/system`. The one entry that names it, in types only: the page hands it the loaders.
13
13
  - `@moku-labs/game/cli` (node and bun only): `runCli`, the bin `moku-game`, and `preparePage`, the editor's dev page.
14
14
  - `@moku-labs/game/testing`: headless play, repro runs, isolated feature tests (`isolate`, `stub`), fake clock, in-memory save provider. No `node:` import.
15
15
  - `@moku-labs/game/visual` (node and bun only): the visual tests `defineVisualTest`, `runVisualTests`, `parseVisualArgv`.
@@ -300,7 +300,7 @@ The page. `moku-game dev` and `build` write a `main.ts` that calls `startPage(ga
300
300
  - `?player=<name>` starts from the scenario `tests/scenarios/<name>.ts` on a fresh memory save, never on the stored one. An unknown name logs `[game] No scenario "<name>".` and starts from the starting player. A scenario is `Scenario<Player>`: `(now) => ({ player, session? })`, its default export.
301
301
  - `?renderer=webgl` asks Pixi for WebGL.
302
302
  - Save kinds: `"memory"` lives for the page only; `"local"` is `localStorage` under `<native.identifier>:save` or `moku-game:save`, and falls back to memory with a warning where storage is refused; `"store"` is the system store (idb on the web, the Tauri store native), needs the shell, rejects a load it cannot read instead of starting a new player.
303
- - System: the generated `main.ts` imports `systemShell` of `@moku-labs/game/app/system` only when `system` is not empty or `save` is `"store"`, so a web-only page bundles no system code. The shell loads each named `@moku-labs/system` plugin with its own `import()`. Not installed: `[game] config.system needs @moku-labs/system.` with `bun add @moku-labs/system@^0.3.1`.
303
+ - System: the generated `main.ts` imports `systemShellOf` of `@moku-labs/game/app/system` only when `system` is not empty or `save` is `"store"`, so a web-only page bundles no system code. It passes `system: systemShellOf({ system: () => import("@moku-labs/system"), haptics: () => import("@moku-labs/system/haptics") })`: one loader per named plugin, in the order `lifecycle`, `back`, `haptics`, `keepAwake` (entry `keep-awake`), `store` (when named or `save` is `"store"`). The engine holds no `import()` of the package, so the page bundles only the named plugins: a game with `system: ["haptics"]` and a memory save builds without `@tauri-apps/plugin-store`. A loader that rejects: `[game] config.system needs @moku-labs/system.` with `bun add @moku-labs/system@^0.3.1`. A named plugin without a loader: `[game] config.system names "<name>", but main.ts has no loader for it.`
304
304
 
305
305
  ```ts
306
306
  // .moku/main.ts of the engine's mini game, written by moku-game dev
@@ -571,6 +571,7 @@ Cosmetic rendering extras as data, drawn by `effectsPlugin`: `plugins: [...scree
571
571
  The phone as a provider, last in the array: `plugins: [...screen, effectsPlugin, audioPlugin, platformPlugin]` with `pluginConfigs: { platform: { provider, keepAwake: true } }`. `game.screen()` composes it and takes the provider as its `platform` seam. A game imports no native package: the engine page builds the provider from the `system` list of `config.ts` through `@moku-labs/game/app/system`, and turns keep-awake on when the list names `keepAwake`. The full reference is `src/plugins/platform/README.md`.
572
572
  - `PlatformProvider` has six members: `onPause(fn)`, `onResume(fn)`, `onBack(fn)` (each returns its remover), `haptic(kind)`, `keepAwake(on)`, `exit()`. Pause and resume push and pop the `"background"` reason of `lifecycle`. A provider method that throws is logged, never thrown into the frame.
573
573
  - Back chain, `app.platform.back(): BackResult`. First an Escape press: a popup with an `escape` button closes, `"popup"`. Then `flow.gate.answer({ intent: "back" })` when the resting node lists `back`, `"intent"`. Then `provider.exit()`, `"exit"`. Without a provider `"none"`.
574
+ - `app.platform.exit()`: the last step of the Back chain alone, `provider.exit()`, for a Leave button: `ctx.require(flowPlugin).fx.handle("exit", () => ctx.require(platformPlugin).exit())`. A throw is logged; without a provider it does nothing.
574
575
  - Haptics: `await fx(haptic("success"))` in a node, or `haptic("light")` as a timeline step. The kinds are `HAPTIC_KINDS`: `light`, `medium`, `heavy`, `selection`, `success`, `warning`, `error`. A fast walk is silent.
575
576
  - Config: `provider: undefined`, which leaves the plugin inert: a `haptic` effect resolves at once, so the same node runs on the web and headless. `keepAwake: false`.
576
577
  - No events. Depends on `lifecycle`, `flow`, `input`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moku-labs/game",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "2D puzzle game engine on TypeScript and PixiJS, built on @moku-labs/core.",
5
5
  "type": "module",
6
6
  "sideEffects": false,