@moku-labs/game 0.0.2 → 0.0.3
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 +89 -21
- package/dist/assets.d.mts +58 -6
- package/dist/assets.mjs +1630 -234
- package/dist/commands-BOxqlv7w.d.mts +99 -0
- package/dist/control.d.mts +3 -92
- package/dist/control.mjs +3 -757
- package/dist/define-BMo0pHe_.d.mts +51 -0
- package/dist/{registry-DlpRCibU.mjs → dev-CcwFrwok.mjs} +30 -1
- package/dist/index.d.mts +217 -29
- package/dist/index.mjs +6179 -727
- package/dist/inspect.d.mts +10 -8
- package/dist/inspect.mjs +2 -518
- package/dist/jsx-dev-runtime.d.mts +1 -1
- package/dist/jsx-runtime.d.mts +1 -1
- package/dist/{rolldown-runtime-D7D4PA-g.mjs → rolldown-runtime-8H4AJuhK.mjs} +1 -0
- package/dist/run-tlcn1w7y.mjs +944 -0
- package/dist/{runtime-DiOTkDZz.d.mts → runtime-BstIkrhH.d.mts} +1 -1
- package/dist/sources-BooL2MsX.mjs +545 -0
- package/dist/testing.d.mts +305 -3
- package/dist/testing.mjs +2056 -2
- package/dist/types-BTycq8_Z.d.mts +1196 -0
- package/dist/{types-yg_ywtT-.d.mts → types-C0SLQsx0.d.mts} +230 -31
- package/dist/types-Cjf3ywai.d.mts +5982 -0
- package/dist/{define-sFoO3y6X.d.mts → types-D0uYTTC3.d.mts} +180 -51
- package/dist/{types-DWILGrPn.d.mts → types-DgwbzDLP.d.mts} +87 -19
- package/dist/types-KlE_GL1z.d.mts +1444 -0
- package/dist/{visual-BDUHSRvf.mjs → visual-NPrDkvUk.mjs} +19 -13
- package/package.json +17 -2
- package/dist/headless-CvamCUcR.mjs +0 -186
- package/dist/headless-KaSWcd0s.d.mts +0 -181
- package/dist/types-BxkNNYul.d.mts +0 -2840
- package/dist/types-CTPS9GBu.d.mts +0 -742
- package/dist/types-DD-QrG_z.d.mts +0 -588
- package/dist/types-DYLnSgMI.d.mts +0 -570
- package/dist/types-JNc_UQBo.d.mts +0 -2896
|
@@ -1,7 +1,184 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as HeadlessApp } from "./headless-KaSWcd0s.mjs";
|
|
1
|
+
import { At as Json, Ht as RngState, St as Api, ft as FlowState, ht as RouteStep, pt as JournalEntry, q as Api$1, xt as Answer, zt as Snapshot } from "./types-Cjf3ywai.mjs";
|
|
3
2
|
import { Log } from "@moku-labs/common/browser";
|
|
4
3
|
|
|
4
|
+
//#region src/plugins/flow/headless.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* The part of an app the headless helpers use. Structural on purpose: any app created with the
|
|
7
|
+
* default plugins fits.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* const app: HeadlessApp = createApp({ pluginConfigs: { flow: { mainFlow } } });
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
type HeadlessApp = {
|
|
15
|
+
start(): Promise<void>;
|
|
16
|
+
stop(): Promise<void>;
|
|
17
|
+
time: Api;
|
|
18
|
+
flow: Api$1;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* A game played without a screen: answers go through the gate, effects resolve instantly.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* // A test of the dice game: two rolls, no screen, no waiting.
|
|
26
|
+
* const game = await createHeadless(app);
|
|
27
|
+
* const state = await game.walk([{ at: "home", intent: "roll" }, { at: "home", intent: "roll" }]);
|
|
28
|
+
*
|
|
29
|
+
* state.path; // "home"
|
|
30
|
+
* await game.stop();
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
type HeadlessGame = {
|
|
34
|
+
/**
|
|
35
|
+
* Walks a route through the running loop.
|
|
36
|
+
*
|
|
37
|
+
* @param route - The player's answers and substituted sub-flow results, in order.
|
|
38
|
+
* @returns The state the walk ended in.
|
|
39
|
+
* @throws {Error} When a step is never reached, or the loop failed fatally.
|
|
40
|
+
* @example
|
|
41
|
+
* ```ts
|
|
42
|
+
* // Open the board and tap the generator twice. Every node's logic runs for real.
|
|
43
|
+
* const tap = { at: "board/awaitIntent", intent: "tap", payload: { generatorId: "sawmill" } };
|
|
44
|
+
* const state = await game.walk([{ at: "home", intent: "play" }, tap, tap]);
|
|
45
|
+
*
|
|
46
|
+
* state.path; // "board/awaitIntent"
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
walk(route: readonly RouteStep[]): Promise<FlowState>;
|
|
50
|
+
/**
|
|
51
|
+
* Gives one answer to the gate, the way a tap does.
|
|
52
|
+
*
|
|
53
|
+
* @param answer - Intent and optional payload.
|
|
54
|
+
* @returns Whether the gate took it.
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* // One tap without a route. The test then waits for the loop by itself.
|
|
58
|
+
* game.answer({ intent: "play" }); // true: "home" rests and lists "play"
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
answer(answer: Answer): boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Reads where the graph stands.
|
|
64
|
+
*
|
|
65
|
+
* @returns Whether it runs, the path, the stack, what it waits for and the mode.
|
|
66
|
+
* @example
|
|
67
|
+
* ```ts
|
|
68
|
+
* // Right after createHeadless: the graph rests at its first rest node.
|
|
69
|
+
* game.state().path; // "home"
|
|
70
|
+
* game.state().pending; // { gate: ["play"] }
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
state(): FlowState;
|
|
74
|
+
/**
|
|
75
|
+
* Reads the edges taken since the last checkpoint.
|
|
76
|
+
*
|
|
77
|
+
* @returns A copy of the journal.
|
|
78
|
+
* @example
|
|
79
|
+
* ```ts
|
|
80
|
+
* // After a merge onto an empty cell: the last edge says why the move was refused.
|
|
81
|
+
* const last = game.history().at(-1);
|
|
82
|
+
* // last?.path: "board/merge", last?.outcome: "rejected", last?.payload: { reason: "empty" }
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
history(): readonly JournalEntry[];
|
|
86
|
+
/**
|
|
87
|
+
* Stops the app, then re-throws a fatal error of the loop.
|
|
88
|
+
*
|
|
89
|
+
* @throws {Error} When the loop failed fatally.
|
|
90
|
+
* @example
|
|
91
|
+
* ```ts
|
|
92
|
+
* // The last line of a headless test. A fatal error of the loop fails the test here.
|
|
93
|
+
* await game.stop();
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
stop(): Promise<void>;
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* A reproducible run: a starting state, an optional checkpoint and the route played from it.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```ts
|
|
103
|
+
* const repro: Repro = { player: { coins: 0 }, checkpoint: "home", route: [{ at: "home", intent: "play" }] };
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
type Repro = {
|
|
107
|
+
player: Json;
|
|
108
|
+
session?: Json;
|
|
109
|
+
rng?: RngState;
|
|
110
|
+
checkpoint?: string;
|
|
111
|
+
route: RouteStep[];
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* What a repro run ends with: the path where the loop rests and the committed state.
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* ```ts
|
|
118
|
+
* const result: ReproResult = { path: ["home"], player: { coins: 3 }, session: { visits: 1 } };
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
type ReproResult = {
|
|
122
|
+
path: string[];
|
|
123
|
+
player: Json;
|
|
124
|
+
session: Json;
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* Starts an app headless: sets flow mode `"fast"`, awaits `app.start()`, starts `flow.run()`
|
|
128
|
+
* unless the app's own `onStart` already did, and waits until the loop rests for the first time.
|
|
129
|
+
* `game.state().path` therefore names the first rest node of the graph as soon as the call
|
|
130
|
+
* resolves. A fatal error of the loop rejects this call, and a later one is re-thrown by `walk`
|
|
131
|
+
* and by `stop`, so a headless test never loses it.
|
|
132
|
+
*
|
|
133
|
+
* @param app - An app that is not started yet.
|
|
134
|
+
* @returns The game: walk it, answer it, read it, stop it.
|
|
135
|
+
* @throws {Error} When the loop failed fatally before it reached its first rest node.
|
|
136
|
+
* @example
|
|
137
|
+
* ```ts
|
|
138
|
+
* // A test starts the merge game without a screen. The call resolves at the first rest node.
|
|
139
|
+
* const game = await createHeadless(app);
|
|
140
|
+
*
|
|
141
|
+
* game.state().path; // "home": the transit node "boot" was played out
|
|
142
|
+
* game.state().mode; // "fast"
|
|
143
|
+
* await game.stop();
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
declare function createHeadless(app: HeadlessApp): Promise<HeadlessGame>;
|
|
147
|
+
/**
|
|
148
|
+
* Plays a repro: starts the app headless, restores the bookmark built from its state and
|
|
149
|
+
* checkpoint, then walks its route. The app keeps running, so the caller can read more than the
|
|
150
|
+
* result and stops it itself.
|
|
151
|
+
*
|
|
152
|
+
* @param app - An app that is not started yet.
|
|
153
|
+
* @param repro - Starting state, optional checkpoint and the route.
|
|
154
|
+
* @returns The path the run ended at, and the committed state.
|
|
155
|
+
* @example
|
|
156
|
+
* ```ts
|
|
157
|
+
* // A bug report on the dice game: "reset keeps my coins". Replay it from the "home" checkpoint.
|
|
158
|
+
* const reset = { at: "home", intent: "reset" };
|
|
159
|
+
* const result = await runRepro(app, { player: { coins: 7 }, checkpoint: "home", route: [reset] });
|
|
160
|
+
*
|
|
161
|
+
* result.player; // { coins: 0 }; result.path is ["home"]
|
|
162
|
+
* await app.stop(); // the app keeps running after the run
|
|
163
|
+
* ```
|
|
164
|
+
*/
|
|
165
|
+
declare function runRepro(app: HeadlessApp, repro: Repro): Promise<ReproResult>;
|
|
166
|
+
/**
|
|
167
|
+
* Steps the frame loop by hand: `count` calls of `app.time.step(deltaMs)`. A headless game has no
|
|
168
|
+
* frame source, so this is the only thing that moves the frame phases.
|
|
169
|
+
*
|
|
170
|
+
* @param app - A started app.
|
|
171
|
+
* @param count - Number of frames.
|
|
172
|
+
* @param deltaMs - Milliseconds per frame.
|
|
173
|
+
* @example
|
|
174
|
+
* ```ts
|
|
175
|
+
* // A headless game has no frame source: the test moves the six frame phases by hand.
|
|
176
|
+
* stepFrames(app, 2, 16);
|
|
177
|
+
* app.time.snapshot(); // frame: 2, delta: 16, elapsed: 32
|
|
178
|
+
* ```
|
|
179
|
+
*/
|
|
180
|
+
declare function stepFrames(app: HeadlessApp, count: number, deltaMs: number): void;
|
|
181
|
+
//#endregion
|
|
5
182
|
//#region src/plugins/flow/doors/types.d.ts
|
|
6
183
|
/**
|
|
7
184
|
* The kinds an input field can take. `json` is any JSON value the command checks itself.
|
|
@@ -212,52 +389,4 @@ type ControlApp = HeadlessApp & {
|
|
|
212
389
|
readonly log: Log.LogApi;
|
|
213
390
|
};
|
|
214
391
|
//#endregion
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Declares a source: a read-only view the editor lists, reads and watches. The types flow from
|
|
218
|
-
* the object: the input from `input`, the app from the `read` parameter, the output from its
|
|
219
|
-
* result.
|
|
220
|
-
*
|
|
221
|
-
* @param source - The descriptor.
|
|
222
|
-
* @returns The same descriptor, frozen.
|
|
223
|
-
* @throws {Error} When the id is not a dotted name such as `"game.position"`.
|
|
224
|
-
* @example
|
|
225
|
-
* ```ts
|
|
226
|
-
* // A game's .dev module: the orders on the board, re-read after every edge.
|
|
227
|
-
* export const orders = defineSource({
|
|
228
|
-
* id: "timber.orders",
|
|
229
|
-
* title: "Orders",
|
|
230
|
-
* input: {},
|
|
231
|
-
* changes: "edge",
|
|
232
|
-
* read: app => app.flow.state().path
|
|
233
|
-
* });
|
|
234
|
-
* read(app, orders); // "board/awaitIntent"
|
|
235
|
-
* ```
|
|
236
|
-
*/
|
|
237
|
-
declare function defineSource<S extends InputSchema, O, App = HeadlessApp>(source: Source<S, O, App>): Source<S, O, App>;
|
|
238
|
-
/**
|
|
239
|
-
* Declares a command: a dev-only action the editor lists and runs. Its body starts with the
|
|
240
|
-
* inline dev guard, so a production build drops it.
|
|
241
|
-
*
|
|
242
|
-
* @param command - The descriptor.
|
|
243
|
-
* @returns The same descriptor, frozen.
|
|
244
|
-
* @throws {Error} When the id is not a dotted name such as `"game.answer"`.
|
|
245
|
-
* @example
|
|
246
|
-
* ```ts
|
|
247
|
-
* // A game's .dev module: open the shop from anywhere, through the graph.
|
|
248
|
-
* export const openShop = defineCommand({
|
|
249
|
-
* id: "timber.openShop",
|
|
250
|
-
* title: "Open the shop",
|
|
251
|
-
* input: {},
|
|
252
|
-
* effect: "route",
|
|
253
|
-
* run: app => {
|
|
254
|
-
* if (typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) throw controlRefused();
|
|
255
|
-
* return app.flow.walk([{ at: "home", intent: "shop" }]);
|
|
256
|
-
* }
|
|
257
|
-
* });
|
|
258
|
-
* (await run(app, openShop)).state.path; // "shop"
|
|
259
|
-
* ```
|
|
260
|
-
*/
|
|
261
|
-
declare function defineCommand<S extends InputSchema, O, App = HeadlessApp>(command: Command<S, O, App>): Command<S, O, App>;
|
|
262
|
-
//#endregion
|
|
263
|
-
export { ControlApp as a, InputSchema as c, WatchApp as d, WatchInput as f, Command as i, Ran as l, defineSource as n, InputArguments as o, CheatEntry as r, InputOf as s, defineCommand as t, Source as u };
|
|
392
|
+
export { stepFrames as _, InputOf as a, Source as c, HeadlessApp as d, HeadlessGame as f, runRepro as g, createHeadless as h, InputArguments as i, WatchApp as l, ReproResult as m, Command as n, InputSchema as o, Repro as p, ControlApp as r, Ran as s, CheatEntry as t, WatchInput as u };
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { r as PixiTexture, t as Api$3 } from "./types-BxkNNYul.mjs";
|
|
1
|
+
import { St as Api$3, f as SliceFrame, mt as NodeInfo, nn as Require, o as PixiTexture, q as Api$1, t as Api$2, vt as Descriptor } from "./types-Cjf3ywai.mjs";
|
|
3
2
|
import { Log } from "@moku-labs/common/browser";
|
|
4
3
|
import { PluginCtx } from "@moku-labs/core";
|
|
5
4
|
|
|
6
5
|
//#region src/plugins/assets/types.d.ts
|
|
7
6
|
declare namespace types_d_exports {
|
|
8
|
-
export { Api, AssetKind, AssetsCtx, AssetsIo, AtlasFrame, BundleMap, BundleRecord, BundleSpec, BundleUsage, Config, CreateTextureOptions, DecodedImage, DefineBundles, Deps, Events, FetchResponse, FlowRest, FontAsset, FontPage, Inflight, KernelSlice, LoadBundles, LoadReason, LoadResult, LoadedAssets, LoadedFont, Manifest, ManifestBundle, ManifestFile, NineBorders, NineSlice, PreloadQueue, State, Texture, Tier, Usage };
|
|
7
|
+
export { Api, AssetKind, AssetsCtx, AssetsIo, AtlasFrame, AtlasPage, BundleMap, BundleRecord, BundleSpec, BundleUsage, Config, CreateTextureOptions, DecodedImage, DefineBundles, Deps, Events, FetchResponse, FlowRest, FontAsset, FontPage, Inflight, KernelSlice, LoadBundles, LoadReason, LoadResult, LoadedAssets, LoadedFont, Manifest, ManifestBundle, ManifestFile, NineBorders, NineSlice, PreloadQueue, State, Texture, Tier, Usage };
|
|
9
8
|
}
|
|
10
9
|
/**
|
|
11
10
|
* When a bundle is loaded. `boot` and `core` stay for the whole session, `scene` and `feature`
|
|
@@ -88,6 +87,7 @@ type FetchResponse = {
|
|
|
88
87
|
* }),
|
|
89
88
|
* decode: async () => bitmap,
|
|
90
89
|
* createTexture: () => ({ id: "t1" }) as unknown as Texture,
|
|
90
|
+
* sliceTexture: (page, frame) => ({ id: "s1", page, frame }) as unknown as Texture,
|
|
91
91
|
* destroyTexture: texture => destroyed.push(texture)
|
|
92
92
|
* };
|
|
93
93
|
*
|
|
@@ -122,7 +122,19 @@ type AssetsIo = {
|
|
|
122
122
|
*/
|
|
123
123
|
createTexture(image: DecodedImage, options?: CreateTextureOptions): Texture;
|
|
124
124
|
/**
|
|
125
|
-
*
|
|
125
|
+
* Cuts the texture of one packed file out of its atlas page. The slice shares the page's
|
|
126
|
+
* source: no pixel is copied. Same shape as `renderer.sync.textures.slice`; the manifest's
|
|
127
|
+
* `atlas` frame is passed as it is.
|
|
128
|
+
*
|
|
129
|
+
* @param page - The page texture, made by `createTexture`.
|
|
130
|
+
* @param frame - Where the file lies on the page, in page pixels.
|
|
131
|
+
* @param options - `nine` becomes the default nine-slice borders of the slice.
|
|
132
|
+
* @returns The new texture over the page's source.
|
|
133
|
+
*/
|
|
134
|
+
sliceTexture(page: Texture, frame: SliceFrame, options?: CreateTextureOptions): Texture;
|
|
135
|
+
/**
|
|
136
|
+
* Frees one texture. A page frees its source too; a slice frees only itself, so the slices of
|
|
137
|
+
* a bundle go first and its pages after them.
|
|
126
138
|
*
|
|
127
139
|
* @param texture - The texture to free.
|
|
128
140
|
*/
|
|
@@ -144,11 +156,13 @@ type NineSlice = {
|
|
|
144
156
|
bottom: number;
|
|
145
157
|
};
|
|
146
158
|
/**
|
|
147
|
-
*
|
|
159
|
+
* Where a packed file lies: `page` is the id of a page of the same bundle, never a file name, and
|
|
160
|
+
* the frame is in page pixels. `width` and `height` equal the file's own, so its `nine` stays
|
|
161
|
+
* valid as it is. The packer writes it; the loader cuts the file out of the page.
|
|
148
162
|
*
|
|
149
163
|
* @example
|
|
150
164
|
* ```ts
|
|
151
|
-
* const frame: AtlasFrame = { page: "ui-0
|
|
165
|
+
* const frame: AtlasFrame = { page: "ui/main-0", x: 583, y: 595, width: 256, height: 128 };
|
|
152
166
|
* ```
|
|
153
167
|
*/
|
|
154
168
|
type AtlasFrame = {
|
|
@@ -158,6 +172,29 @@ type AtlasFrame = {
|
|
|
158
172
|
width: number;
|
|
159
173
|
height: number;
|
|
160
174
|
};
|
|
175
|
+
/**
|
|
176
|
+
* One atlas page of a packed bundle. `id` is `<bundle>/<group>-<index>`, the name an `atlas`
|
|
177
|
+
* frame uses; `path` is the hashed WebP file, read like the `path` of a file. `mb` is
|
|
178
|
+
* `width × height × 4 / 1 048 576`: the page carries the cost of every file packed on it.
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* ```ts
|
|
182
|
+
* const page: AtlasPage = {
|
|
183
|
+
* id: "ui/main-0",
|
|
184
|
+
* path: "ui/main-0-3b1d55a0c9.webp",
|
|
185
|
+
* width: 966,
|
|
186
|
+
* height: 1365,
|
|
187
|
+
* mb: 5.03
|
|
188
|
+
* };
|
|
189
|
+
* ```
|
|
190
|
+
*/
|
|
191
|
+
type AtlasPage = {
|
|
192
|
+
id: string;
|
|
193
|
+
path: string;
|
|
194
|
+
width: number;
|
|
195
|
+
height: number;
|
|
196
|
+
mb: number;
|
|
197
|
+
};
|
|
161
198
|
/**
|
|
162
199
|
* One page image of a font. A page has no key of its own: it belongs to the `.fnt` file that
|
|
163
200
|
* names it, and is loaded and freed with it.
|
|
@@ -180,12 +217,15 @@ type FontPage = {
|
|
|
180
217
|
};
|
|
181
218
|
/**
|
|
182
219
|
* One file of a bundle. `mb` is what it costs: `width × height × 4` bytes for a texture, the sum
|
|
183
|
-
* of the pages for a font, the size of the file for audio
|
|
184
|
-
* is
|
|
220
|
+
* of the pages for a font, the size of the file for audio, and `0` for a texture packed in an
|
|
221
|
+
* atlas, whose page carries the cost. `kind` is absent for a texture, which is what every
|
|
222
|
+
* manifest written before fonts and audio carries. A loose file has `path`; a packed texture has
|
|
223
|
+
* `atlas` and no `path`.
|
|
185
224
|
*
|
|
186
225
|
* @example
|
|
187
226
|
* ```ts
|
|
188
|
-
*
|
|
227
|
+
* // The dev manifest of the scanner: a loose file.
|
|
228
|
+
* const loose: ManifestFile = {
|
|
189
229
|
* key: "ui.panel",
|
|
190
230
|
* path: "features/ui/assets/panel{nine=48}.png",
|
|
191
231
|
* width: 256,
|
|
@@ -193,11 +233,21 @@ type FontPage = {
|
|
|
193
233
|
* mb: 0.125,
|
|
194
234
|
* nine: { left: 48, top: 48, right: 48, bottom: 48 }
|
|
195
235
|
* };
|
|
236
|
+
*
|
|
237
|
+
* // The packed manifest: the same key, cut out of the page "ui/main-0".
|
|
238
|
+
* const packed: ManifestFile = {
|
|
239
|
+
* key: "ui.panel",
|
|
240
|
+
* width: 256,
|
|
241
|
+
* height: 128,
|
|
242
|
+
* mb: 0,
|
|
243
|
+
* nine: { left: 48, top: 48, right: 48, bottom: 48 },
|
|
244
|
+
* atlas: { page: "ui/main-0", x: 583, y: 595, width: 256, height: 128 }
|
|
245
|
+
* };
|
|
196
246
|
* ```
|
|
197
247
|
*/
|
|
198
248
|
type ManifestFile = {
|
|
199
249
|
key: string;
|
|
200
|
-
path
|
|
250
|
+
path?: string;
|
|
201
251
|
kind?: AssetKind;
|
|
202
252
|
width: number;
|
|
203
253
|
height: number;
|
|
@@ -207,7 +257,9 @@ type ManifestFile = {
|
|
|
207
257
|
atlas?: AtlasFrame;
|
|
208
258
|
};
|
|
209
259
|
/**
|
|
210
|
-
* One bundle of the manifest: which feature owns it, when it loads and what it costs.
|
|
260
|
+
* One bundle of the manifest: which feature owns it, when it loads and what it costs. `pages` is
|
|
261
|
+
* written by the packer only, sorted by id; `mb` is then its pages, its loose textures, its font
|
|
262
|
+
* pages and its audio bytes together.
|
|
211
263
|
*
|
|
212
264
|
* @example
|
|
213
265
|
* ```ts
|
|
@@ -218,24 +270,37 @@ type ManifestBundle = {
|
|
|
218
270
|
feature: string;
|
|
219
271
|
tier: Tier;
|
|
220
272
|
mb: number;
|
|
273
|
+
pages?: readonly AtlasPage[];
|
|
221
274
|
files: readonly ManifestFile[];
|
|
222
275
|
};
|
|
223
276
|
/**
|
|
224
|
-
* The manifest: the contract between the scanner, this plugin and the editor
|
|
225
|
-
*
|
|
277
|
+
* The manifest: the contract between the scanner, the packer, this plugin and the editor. Version
|
|
278
|
+
* `1` is the dev manifest of `bun run assets:keys`, version `2` the packed one of
|
|
279
|
+
* `bun run assets:pack`; the plugin reads both. Bundles are sorted by name, pages by id and files
|
|
280
|
+
* by key, so two runs on the same tree give the same bytes.
|
|
226
281
|
*
|
|
227
282
|
* @example
|
|
228
283
|
* ```ts
|
|
284
|
+
* // A dev build: loose files, straight from the features.
|
|
229
285
|
* const manifest: Manifest = {
|
|
230
286
|
* version: 1,
|
|
231
287
|
* bundles: { ui: { feature: "ui", tier: "core", mb: 0, files: [] } }
|
|
232
288
|
* };
|
|
233
289
|
*
|
|
234
290
|
* createApp({ plugins: [...screen], pluginConfigs: { assets: { manifest } } });
|
|
291
|
+
*
|
|
292
|
+
* // The packed manifest of the same game: the panel is cut out of the page "ui/main-0".
|
|
293
|
+
* const page = { id: "ui/main-0", path: "ui/main-0-3b1d55a0c9.webp", width: 966, height: 1365, mb: 5.03 };
|
|
294
|
+
* const atlas = { page: "ui/main-0", x: 583, y: 595, width: 256, height: 128 };
|
|
295
|
+
* const panel = { key: "ui.panel", width: 256, height: 128, mb: 0, atlas };
|
|
296
|
+
* const packed: Manifest = {
|
|
297
|
+
* version: 2,
|
|
298
|
+
* bundles: { ui: { feature: "ui", tier: "core", mb: 5.03, pages: [page], files: [panel] } }
|
|
299
|
+
* };
|
|
235
300
|
* ```
|
|
236
301
|
*/
|
|
237
302
|
type Manifest = {
|
|
238
|
-
version: 1;
|
|
303
|
+
version: 1 | 2;
|
|
239
304
|
bundles: Readonly<Record<string, ManifestBundle>>;
|
|
240
305
|
};
|
|
241
306
|
/**
|
|
@@ -339,13 +404,15 @@ type LoadedFont = FontAsset & {
|
|
|
339
404
|
pages: readonly Texture[];
|
|
340
405
|
};
|
|
341
406
|
/**
|
|
342
|
-
* What one bundle brought
|
|
343
|
-
* bytes of the audio files
|
|
407
|
+
* What one bundle brought: textures by asset key (a packed file's is a slice of its page), fonts
|
|
408
|
+
* with their pages, the undecoded bytes of the audio files, and the atlas pages by page id. A
|
|
409
|
+
* running load fills the same four maps before they are published.
|
|
344
410
|
*/
|
|
345
411
|
type LoadedAssets = {
|
|
346
412
|
textures: Map<string, Texture>;
|
|
347
413
|
fonts: Map<string, LoadedFont>;
|
|
348
414
|
audio: Map<string, ArrayBuffer>;
|
|
415
|
+
pages: Map<string, Texture>;
|
|
349
416
|
};
|
|
350
417
|
/**
|
|
351
418
|
* What the plugin knows about one bundle. `lastUsed` is the value of `useCounter` at the last
|
|
@@ -530,7 +597,8 @@ type Api = {
|
|
|
530
597
|
/**
|
|
531
598
|
* The texture of a loaded asset key. It touches the use counter of the bundle, which is what
|
|
532
599
|
* the LRU reads. A key of a bundle that is not loaded warns once, starts a background load and
|
|
533
|
-
* answers `undefined`; the sprites waiting for it are textured when that load lands.
|
|
600
|
+
* answers `undefined`; the sprites waiting for it are textured when that load lands. A key packed
|
|
601
|
+
* in an atlas answers its slice of the page, so a game never sees the difference.
|
|
534
602
|
*
|
|
535
603
|
* @param key - Asset key, as `generated/assets.ts` types it.
|
|
536
604
|
* @returns The texture, or `undefined` while the bundle is not loaded.
|
|
@@ -590,8 +658,8 @@ type Api = {
|
|
|
590
658
|
*/
|
|
591
659
|
type Deps = {
|
|
592
660
|
flow: Api$1;
|
|
593
|
-
renderer: Api$
|
|
594
|
-
time: Api$
|
|
661
|
+
renderer: Api$2;
|
|
662
|
+
time: Api$3;
|
|
595
663
|
};
|
|
596
664
|
/**
|
|
597
665
|
* What the kernel context offers before the deps are attached.
|