@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.
- package/README.md +81 -22
- package/bin/moku-game.mjs +4 -0
- package/dist/app/page.d.mts +44 -0
- package/dist/app/page.mjs +379 -0
- package/dist/app/system.d.mts +144 -0
- package/dist/app/system.mjs +329 -0
- package/dist/app.d.mts +37 -0
- package/dist/app.mjs +235 -0
- package/dist/assets-BHwTLVta.mjs +7391 -0
- package/dist/assets.d.mts +1 -1
- package/dist/assets.mjs +1 -7390
- package/dist/cli.d.mts +70 -0
- package/dist/cli.mjs +1685 -0
- package/dist/{commands-DuwCxK5t.d.mts → commands-C4QlqijJ.d.mts} +5 -5
- package/dist/config-CdARrrhU.mjs +81 -0
- package/dist/control.d.mts +11 -10
- package/dist/control.mjs +3 -3
- package/dist/{define-B6pHpdBi.d.mts → define-CCc0ozKe.d.mts} +2 -2
- package/dist/{dev-wKLgOw7c.mjs → dev-Cc50Dk7H.mjs} +1 -1
- package/dist/drafts-BawpSaVp.mjs +156 -0
- package/dist/fake-MpzwZhIZ.mjs +87 -0
- package/dist/{headless-SCQXJxQf.d.mts → headless-G4nuZ-xz.d.mts} +1 -1
- package/dist/hot.d.mts +4 -6
- package/dist/hot.mjs +8 -9
- package/dist/{index-BO9kZzY3.d.mts → index-8TiJQjQA.d.mts} +30 -17
- package/dist/index.d.mts +5 -5
- package/dist/index.mjs +1 -1
- package/dist/inspect-la_KkjCS.mjs +1 -0
- package/dist/inspect.d.mts +7 -7
- package/dist/inspect.mjs +3 -2
- package/dist/jsx-dev-runtime.d.mts +1 -1
- package/dist/jsx-runtime.d.mts +1 -1
- package/dist/lint.mjs +10 -6
- package/dist/memory-CWlqhS7l.mjs +136 -0
- package/dist/memory-DwDhfFWK.d.mts +50 -0
- package/dist/project.mjs +3 -3
- package/dist/{run-CofJEdYx.mjs → run-wYty_Lwo.mjs} +2 -2
- package/dist/{runtime-CdNJ0UBm.d.mts → runtime-DOBYM6g4.d.mts} +1 -1
- package/dist/{input-z_35Jwfn.mjs → session-DgukhR_7.mjs} +20 -20
- package/dist/{sources-DNnzyCYf.mjs → sources-Duzj6lhV.mjs} +2 -2
- package/dist/{src-536arXjV.mjs → src-BSbtHZWk.mjs} +35 -300
- package/dist/testing.d.mts +4 -50
- package/dist/testing.mjs +3 -87
- package/dist/{types-VZVWwqCg.d.mts → types-BM4shh8K.d.mts} +259 -259
- package/dist/{types-xXfyk56Q.d.mts → types-BUwDHKXm.d.mts} +1377 -1377
- package/dist/types-Ca_z_4tQ.d.mts +586 -0
- package/dist/{types-CQueODzo.d.mts → types-CefgYzFt.d.mts} +2 -2
- package/dist/{types-DUMoYPYe.d.mts → types-DAdwEJMp.d.mts} +2 -2
- package/dist/{types-B0v5VVa3.d.mts → types-ZQLYXo5X.d.mts} +304 -304
- package/dist/visual.d.mts +6 -6
- package/dist/visual.mjs +5 -5
- package/llms.txt +120 -43
- 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 };
|