@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,144 @@
1
+ import { tn as PlayerStateProvider } from "../types-ZQLYXo5X.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
+ import { Back, Haptics, KeepAwake, Lifecycle, Store } from "@moku-labs/system";
5
+
6
+ //#region src/app/system.d.ts
7
+ /**
8
+ * What the bridge reads of the system app: the four capabilities, each one there only when the
9
+ * game names its plugin. A system app of `lifecyclePlugin`, `backPlugin`, `hapticsPlugin` and
10
+ * `keepAwakePlugin` has all of them.
11
+ *
12
+ * @example
13
+ * ```ts
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
16
+ * ```
17
+ */
18
+ type SystemSlice = {
19
+ /** Pause and resume, from `@moku-labs/system/lifecycle`. */readonly lifecycle?: Lifecycle.LifecycleApi; /** The hardware Back press and the exit, from `@moku-labs/system/back`. */
20
+ readonly back?: Back.BackApi; /** Impact, outcome and selection ticks, from `@moku-labs/system/haptics`. */
21
+ readonly haptics?: Haptics.HapticsApi; /** The screen wake lock, from `@moku-labs/system/keep-awake`. */
22
+ readonly keepAwake?: KeepAwake.KeepAwakeApi;
23
+ };
24
+ /**
25
+ * What the store save reads of the system store: one read and one write.
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * const system = createApp({ plugins: [storePlugin] }); // createApp of @moku-labs/system
30
+ * const store: StoreSlice = system.store;
31
+ * ```
32
+ */
33
+ type StoreSlice = Pick<Store.StoreApi, "get" | "set">;
34
+ /**
35
+ * The system app of the game shell: the capabilities of the plugins the game names, the store
36
+ * among them, and the start and stop of the app.
37
+ *
38
+ * @example
39
+ * ```ts
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
+ * });
45
+ * await system.start(); // system.lifecycle and system.store answer from here on
46
+ * ```
47
+ */
48
+ type SystemApp = SystemSlice & {
49
+ /** Key-value persistence, from `@moku-labs/system/store`. */readonly store?: Store.StoreApi; /** Starts the system app: every capability picks its provider. */
50
+ readonly start: () => Promise<void>; /** Stops the system app: every capability lets go of its provider. */
51
+ readonly stop: () => Promise<void>;
52
+ };
53
+ /**
54
+ * Builds the engine's provider over the system app. On iOS `exit` answers `unsupported`, so Leave
55
+ * lands back on Home there, as on the web page. A capability the slice lacks is inert, as the
56
+ * platform plugin is without a provider: its subscriptions remove nothing and its calls do nothing.
57
+ *
58
+ * @param system - The system app, with the capabilities the game names.
59
+ * @returns The provider to pass as the `platform` seam of `game.screen()`.
60
+ * @example
61
+ * ```ts
62
+ * import { createApp as createSystem } from "@moku-labs/system";
63
+ * const system = createSystem({ plugins: [lifecyclePlugin, backPlugin, hapticsPlugin, keepAwakePlugin] });
64
+ * fromSystem(system).haptic("success"); // the shell plays system.haptics.notify("success")
65
+ * fromSystem({}).exit(); // does nothing: the game named no back capability
66
+ * ```
67
+ */
68
+ declare function fromSystem(system: SystemSlice): PlatformProvider;
69
+ /**
70
+ * Creates the `@moku-labs/system` app of the named plugins, in the fixed order `lifecycle`,
71
+ * `back`, `haptics`, `keepAwake`, `store`, each name once. The store is named after `namespace`.
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.
74
+ *
75
+ * @param names - The system plugins the game names, in any order.
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.
78
+ * @returns The system app, not started.
79
+ * @throws {Error} When a named plugin has no loader in `modules`, or a loader rejects because
80
+ * `@moku-labs/system` is not installed.
81
+ * @example
82
+ * ```ts
83
+ * // A shell of its own: the store named after the app, and the haptics.
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
+ * });
89
+ * await system.start();
90
+ * await system.haptics?.selection(); // { ok: false, provider: "web", reason: "unsupported" } in iOS Safari
91
+ * ```
92
+ */
93
+ declare function createSystemApp(names: readonly SystemName[], namespace: string, modules: SystemModules): Promise<SystemApp>;
94
+ /**
95
+ * Builds the save provider over the system store: idb on the web, the Tauri store in the native
96
+ * app. It holds the document it loaded and applies each commit's patches to it, then queues one
97
+ * write of the whole value, so the writes stay in commit order. A read the store refuses rejects
98
+ * the load: answering "new player" would overwrite a real save at the first commit. A write the
99
+ * store refuses is reported from `commit` and rejects `commitDurable`; the next write still goes.
100
+ *
101
+ * @param store - The system store.
102
+ * @param key - The store key of the save.
103
+ * @param report - Takes the problem of a write `commit` could not store.
104
+ * @returns The provider to pass as the `provider` seam of `game.screen()`.
105
+ * @example
106
+ * ```ts
107
+ * import { createApp as createSystem } from "@moku-labs/system";
108
+ * const system = createSystem({ plugins: [storePlugin], pluginConfigs: { store: { name: "com.mokulabs.timber" } } });
109
+ * await system.start();
110
+ * const save = storeSave(system.store, "save", problem => problems.push(problem));
111
+ * await save.load(); // null for a new player; rejects when the store cannot be read
112
+ * ```
113
+ */
114
+ declare function storeSave(store: StoreSlice, key: string, report: (problem: string) => void): PlayerStateProvider;
115
+ /**
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.
123
+ *
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`.
126
+ * @example
127
+ * ```ts
128
+ * // The in-memory .moku/build/main.ts of moku-game build, for a game whose config.ts names back.
129
+ * import { startPage } from "@moku-labs/game/app/page";
130
+ * import { systemShellOf } from "@moku-labs/game/app/system";
131
+ * import game from "../../index.ts";
132
+ * import config from "../../config.ts";
133
+ *
134
+ * await startPage(game, config, {
135
+ * system: systemShellOf({
136
+ * system: () => import("@moku-labs/system"),
137
+ * back: () => import("@moku-labs/system/back")
138
+ * })
139
+ * });
140
+ * ```
141
+ */
142
+ declare function systemShellOf(modules: SystemModules): SystemShellFactory;
143
+ //#endregion
144
+ export { StoreSlice, SystemApp, type SystemModules, SystemSlice, createSystemApp, fromSystem, storeSave, systemShellOf };
@@ -0,0 +1,329 @@
1
+ import { t as applyTo } from "../drafts-BawpSaVp.mjs";
2
+ //#region src/app/system.ts
3
+ /** The system plugins in the order the shell composes them, whatever the order of `config.ts`. */
4
+ const systemOrder = [
5
+ "lifecycle",
6
+ "back",
7
+ "haptics",
8
+ "keepAwake",
9
+ "store"
10
+ ];
11
+ /** The store namespace of a game with no native identifier. */
12
+ const defaultNamespace = "moku-game";
13
+ /** The store key of the save, inside the game's namespace. */
14
+ const saveKey = "save";
15
+ /**
16
+ * Removes nothing: the remover of a subscription the shell cannot make, because the game did not
17
+ * name the capability.
18
+ */
19
+ function removeNothing() {}
20
+ /**
21
+ * Plays one tick of the engine with the capability of its kind: the three impacts, the three
22
+ * outcome patterns, and the selection tick.
23
+ *
24
+ * @param haptics - The haptics capability of the system app.
25
+ * @param kind - One of the seven kinds of `HAPTIC_KINDS`.
26
+ * @returns What the shell answered.
27
+ */
28
+ function play(haptics, kind) {
29
+ switch (kind) {
30
+ case "light":
31
+ case "medium":
32
+ case "heavy": return haptics.impact(kind);
33
+ case "success":
34
+ case "warning":
35
+ case "error": return haptics.notify(kind);
36
+ case "selection": return haptics.selection();
37
+ }
38
+ }
39
+ /**
40
+ * Asks the shell without waiting for the answer. A capability answers with a `SystemResult`, never
41
+ * a throw, and the provider of the engine returns nothing: a tick the shell cannot play (iOS
42
+ * Safari, a desktop) is an honest `unsupported`, not an error of the game. A capability the game
43
+ * did not name is not asked.
44
+ *
45
+ * @param capability - The capability, or `undefined` when the game did not name it.
46
+ * @param ask - The capability call.
47
+ */
48
+ function send(capability, ask) {
49
+ if (capability !== void 0) ask(capability);
50
+ }
51
+ /**
52
+ * Subscribes through a capability, or subscribes nothing when the game did not name it.
53
+ *
54
+ * @param capability - The capability, or `undefined` when the game did not name it.
55
+ * @param listen - The subscription; it returns its remover.
56
+ * @returns The remover of the subscription, or one that removes nothing.
57
+ */
58
+ function subscribe(capability, listen) {
59
+ return capability === void 0 ? removeNothing : listen(capability);
60
+ }
61
+ /**
62
+ * Builds the engine's provider over the system app. On iOS `exit` answers `unsupported`, so Leave
63
+ * lands back on Home there, as on the web page. A capability the slice lacks is inert, as the
64
+ * platform plugin is without a provider: its subscriptions remove nothing and its calls do nothing.
65
+ *
66
+ * @param system - The system app, with the capabilities the game names.
67
+ * @returns The provider to pass as the `platform` seam of `game.screen()`.
68
+ * @example
69
+ * ```ts
70
+ * import { createApp as createSystem } from "@moku-labs/system";
71
+ * const system = createSystem({ plugins: [lifecyclePlugin, backPlugin, hapticsPlugin, keepAwakePlugin] });
72
+ * fromSystem(system).haptic("success"); // the shell plays system.haptics.notify("success")
73
+ * fromSystem({}).exit(); // does nothing: the game named no back capability
74
+ * ```
75
+ */
76
+ function fromSystem(system) {
77
+ return {
78
+ onPause: (fn) => subscribe(system.lifecycle, (lifecycle) => lifecycle.onPause(fn)),
79
+ onResume: (fn) => subscribe(system.lifecycle, (lifecycle) => lifecycle.onResume(fn)),
80
+ onBack: (fn) => subscribe(system.back, (back) => back.onPress(fn)),
81
+ haptic: (kind) => send(system.haptics, (haptics) => play(haptics, kind)),
82
+ keepAwake: (on) => send(system.keepAwake, (keepAwake) => keepAwake.set(on)),
83
+ exit: () => send(system.back, (back) => back.exit())
84
+ };
85
+ }
86
+ /**
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.
111
+ *
112
+ * @param names - The system plugins, in the fixed order.
113
+ * @param modules - The loaders of the page.
114
+ * @returns The `createApp` of the package, the capability plugins in the order of the names, and
115
+ * the store plugin when the names hold it.
116
+ * @throws {Error} When a name has no loader, or a loader rejects: the package is not installed.
117
+ */
118
+ async function loadSystem(names, modules) {
119
+ checkLoaders(names, modules);
120
+ const capabilities = names.filter((name) => name !== "store");
121
+ try {
122
+ const [system, store, ...plugins] = await Promise.all([
123
+ modules.system(),
124
+ names.includes("store") ? modules.store?.().then((module) => module.storePlugin) : void 0,
125
+ ...capabilities.map((name) => capabilityPlugins[name](modules))
126
+ ]);
127
+ return {
128
+ createApp: system.createApp,
129
+ capabilities: plugins,
130
+ store
131
+ };
132
+ } catch (error) {
133
+ throw new Error("[game] config.system needs @moku-labs/system.\n Install it: bun add @moku-labs/system@^0.3.1.", { cause: error });
134
+ }
135
+ }
136
+ /**
137
+ * Creates the `@moku-labs/system` app of the named plugins, in the fixed order `lifecycle`,
138
+ * `back`, `haptics`, `keepAwake`, `store`, each name once. The store is named after `namespace`.
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.
141
+ *
142
+ * @param names - The system plugins the game names, in any order.
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.
145
+ * @returns The system app, not started.
146
+ * @throws {Error} When a named plugin has no loader in `modules`, or a loader rejects because
147
+ * `@moku-labs/system` is not installed.
148
+ * @example
149
+ * ```ts
150
+ * // A shell of its own: the store named after the app, and the haptics.
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
+ * });
156
+ * await system.start();
157
+ * await system.haptics?.selection(); // { ok: false, provider: "web", reason: "unsupported" } in iOS Safari
158
+ * ```
159
+ */
160
+ async function createSystemApp(names, namespace, modules) {
161
+ const { createApp, capabilities, store } = await loadSystem(systemOrder.filter((name) => names.includes(name)), modules);
162
+ if (store === void 0) return createApp({ plugins: capabilities });
163
+ return createApp({
164
+ plugins: [...capabilities, store],
165
+ pluginConfigs: { store: { name: namespace } }
166
+ });
167
+ }
168
+ /**
169
+ * Names why the store said no: the reason, and the store's own words when it gave some.
170
+ *
171
+ * @param failure - The failed answer of the store.
172
+ * @returns The reason, for an error message.
173
+ * @example
174
+ * ```ts
175
+ * failureText({ ok: false, provider: "tauri", reason: "denied", message: "not allowed" }); // "denied, not allowed"
176
+ * ```
177
+ */
178
+ function failureText(failure) {
179
+ return failure.message === void 0 ? failure.reason : `${failure.reason}, ${failure.message}`;
180
+ }
181
+ /**
182
+ * Builds the message of a save the store did not take.
183
+ *
184
+ * @param reason - Why the store did not take it.
185
+ * @returns The message.
186
+ * @example
187
+ * ```ts
188
+ * notStored("denied"); // "[game] The save was not stored: denied.\n Check the store permission of the native build; progress since the last stored save is lost on close."
189
+ * ```
190
+ */
191
+ function notStored(reason) {
192
+ return `[game] The save was not stored: ${reason}.\n Check the store permission of the native build; progress since the last stored save is lost on close.`;
193
+ }
194
+ /**
195
+ * Writes the whole save once, after the write before it. It never rejects, so one refused write
196
+ * never stops the writes after it.
197
+ *
198
+ * @param previous - The write before this one, or `undefined` for the first.
199
+ * @param store - The system store.
200
+ * @param key - The store key of the save.
201
+ * @param value - The document and its version.
202
+ * @returns The problem of a write the store did not take, or `undefined` once it is stored.
203
+ */
204
+ async function write(previous, store, key, value) {
205
+ await previous;
206
+ try {
207
+ const result = await store.set(key, value);
208
+ return result.ok ? void 0 : notStored(failureText(result));
209
+ } catch (error) {
210
+ return notStored(error instanceof Error ? error.message : String(error));
211
+ }
212
+ }
213
+ /**
214
+ * Reports the problem of a write nobody waits for, once the write is done.
215
+ *
216
+ * @param written - The queued write.
217
+ * @param report - Takes the problem of a write the store did not take.
218
+ */
219
+ async function reportProblem(written, report) {
220
+ const problem = await written;
221
+ if (problem !== void 0) report(problem);
222
+ }
223
+ /**
224
+ * Builds the save provider over the system store: idb on the web, the Tauri store in the native
225
+ * app. It holds the document it loaded and applies each commit's patches to it, then queues one
226
+ * write of the whole value, so the writes stay in commit order. A read the store refuses rejects
227
+ * the load: answering "new player" would overwrite a real save at the first commit. A write the
228
+ * store refuses is reported from `commit` and rejects `commitDurable`; the next write still goes.
229
+ *
230
+ * @param store - The system store.
231
+ * @param key - The store key of the save.
232
+ * @param report - Takes the problem of a write `commit` could not store.
233
+ * @returns The provider to pass as the `provider` seam of `game.screen()`.
234
+ * @example
235
+ * ```ts
236
+ * import { createApp as createSystem } from "@moku-labs/system";
237
+ * const system = createSystem({ plugins: [storePlugin], pluginConfigs: { store: { name: "com.mokulabs.timber" } } });
238
+ * await system.start();
239
+ * const save = storeSave(system.store, "save", problem => problems.push(problem));
240
+ * await save.load(); // null for a new player; rejects when the store cannot be read
241
+ * ```
242
+ */
243
+ function storeSave(store, key, report) {
244
+ const held = {
245
+ document: void 0,
246
+ written: void 0
247
+ };
248
+ /**
249
+ * Applies the patches of one commit to the held document and queues the write of the whole
250
+ * value after the last one.
251
+ *
252
+ * @param patches - Doc patches since the last commit.
253
+ * @param version - Schema version this build writes.
254
+ * @returns The problem of this write, or `undefined` once it is stored.
255
+ */
256
+ const queue = (patches, version) => {
257
+ const document = applyTo(held.document ?? {}, patches);
258
+ const written = write(held.written, store, key, {
259
+ state: document,
260
+ version
261
+ });
262
+ held.document = document;
263
+ held.written = written;
264
+ return written;
265
+ };
266
+ return {
267
+ load: async () => {
268
+ const read = await store.get(key);
269
+ if (!read.ok) throw new Error(`[game] The save could not be read from the system store: ${failureText(read)}.\n The game does not start over it; check the store permission of the native build.`);
270
+ if (read.value === void 0) return null;
271
+ held.document = read.value.state;
272
+ return {
273
+ state: read.value.state,
274
+ version: read.value.version
275
+ };
276
+ },
277
+ commit: (patches, version) => {
278
+ reportProblem(queue(patches, version), report);
279
+ },
280
+ commitDurable: async (patches, _txId, version) => {
281
+ const problem = await queue(patches, version);
282
+ if (problem !== void 0) throw new Error(problem);
283
+ },
284
+ flush: async () => {
285
+ await held.written;
286
+ }
287
+ };
288
+ }
289
+ /**
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.
297
+ *
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`.
300
+ * @example
301
+ * ```ts
302
+ * // The in-memory .moku/build/main.ts of moku-game build, for a game whose config.ts names back.
303
+ * import { startPage } from "@moku-labs/game/app/page";
304
+ * import { systemShellOf } from "@moku-labs/game/app/system";
305
+ * import game from "../../index.ts";
306
+ * import config from "../../config.ts";
307
+ *
308
+ * await startPage(game, config, {
309
+ * system: systemShellOf({
310
+ * system: () => import("@moku-labs/system"),
311
+ * back: () => import("@moku-labs/system/back")
312
+ * })
313
+ * });
314
+ * ```
315
+ */
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
+ };
326
+ };
327
+ }
328
+ //#endregion
329
+ export { createSystemApp, fromSystem, storeSave, systemShellOf };
package/dist/app.d.mts ADDED
@@ -0,0 +1,37 @@
1
+ import { Xt as Json } from "./types-ZQLYXo5X.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
+ import { AnyPluginInstance } from "@moku-labs/core";
4
+
5
+ //#region src/app/define.d.ts
6
+ /**
7
+ * The moment the default fake clock starts at, in epoch milliseconds. A test stamps times from it.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * game.headless().clock.now() === startMoment; // true
12
+ * ```
13
+ */
14
+ declare const startMoment = 1000000;
15
+ /**
16
+ * Turns a game's one data object into the game: `game.headless()` for logic tests and
17
+ * `game.screen()` for the page and the screen tests. Every type comes from the object: the plugin
18
+ * tuples, the player and the session. Checks the definition at once, so an error points at
19
+ * `index.ts`.
20
+ *
21
+ * @param definition - The game: its flow, starting state, features, plugins and plugin configs.
22
+ * @returns The game, with `headless()` and `screen()`; no app is created yet.
23
+ * @throws {Error} When `flow` is missing, or an entry of `shared`, `features` or `headless.features` is not a feature.
24
+ * @example
25
+ * ```ts
26
+ * // index.ts of a game: the whole game, no createApp call. Its tests import it as `game`.
27
+ * export default defineGameApp({
28
+ * flow: mainFlow, safeNode: "home", player: startingPlayer, session: startingSession,
29
+ * shared: sharedFeature, features: [homeFeature, boardFeature, rewardFeature], plugins: [loadingPlugin],
30
+ * headless: { features: [rewardFeature] }, pluginConfigs: { ui: { tapTargetPt: 48 } }
31
+ * });
32
+ * game.headless({ seed: 7 }).app.flow.state().running; // false: the test starts it
33
+ * ```
34
+ */
35
+ declare function defineGameApp<const Plugins extends readonly AnyPluginInstance[] = readonly [], const HeadlessPlugins extends readonly AnyPluginInstance[] = readonly [], Player extends Json = Json, Session extends Json = Json>(definition: GameDefinition<Plugins, HeadlessPlugins, Player, Session>): GameApp<Plugins, HeadlessPlugins, Player, Session>;
36
+ //#endregion
37
+ export { type GameApp, type GameConfig, type GameDefinition, type GameHandle, type GamePluginConfigs, type HeadlessSeams, type MemoryProvider, type PageAgent, type SaveKind, type Scenario, type ScreenSeams, type SystemName, defineGameApp, startMoment };