@moku-labs/game 0.11.0 → 0.13.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 CHANGED
@@ -165,8 +165,9 @@ The engine bin `moku-game` serves, builds and packs a game folder. A game writes
165
165
  | `moku-game native build <target> [--simulator]` | Builds the native app: `ios`, `macos`, `android` |
166
166
  | `moku-game native dev <target>` | Runs the native shell on the dev server |
167
167
  | `moku-game native doctor`, `native clean` | Checks or removes the native project |
168
- | `moku-game keys [--check]` | Writes `generated/assets.ts`, the strings and `manifest.json` |
168
+ | `moku-game keys [--check]` | Writes `generated/assets.ts`, the strings and `generated/manifest.json` |
169
169
  | `moku-game pack [--no-cache]` | Packs the assets into `dist/assets` |
170
+ | `moku-game visual [--update] [--only <name>] [--no-pixels]` | Runs the visual tests of `tests/visual/index.ts` against `tests/visual/baselines/`. The pixel leg runs on a Mac, on a page served for the run. Exit 1 when a checkpoint differs |
170
171
 
171
172
  Every command takes `--root <dir>`, `--preload <path>` and `--serve-plugin <path>`.
172
173
 
@@ -1,4 +1,4 @@
1
- import { m as StartedPage, r as GameConfig, t as AnyGameApp, u as PageOptions } from "../types-Ca_z_4tQ.mjs";
1
+ import { m as StartedPage, r as GameConfig, t as AnyGameApp, u as PageOptions } from "../types-DLghaGEy.mjs";
2
2
 
3
3
  //#region src/app/page.d.ts
4
4
  /**
@@ -1,6 +1,6 @@
1
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";
2
+ import { ct as PlatformProvider } from "../index-rnOYAriI.mjs";
3
+ import { _ as SystemShellFactory, g as SystemName, h as SystemModules } from "../types-DLghaGEy.mjs";
4
4
  import { Back, Haptics, KeepAwake, Lifecycle, Store } from "@moku-labs/system";
5
5
 
6
6
  //#region src/app/system.d.ts
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, 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";
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-DLghaGEy.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-BSbtHZWk.mjs";
1
+ import { Ct as effectsPlugin, H as platformPlugin, Nt as audioPlugin, i as screen, t as createApp } from "./src-SmT-XxVf.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
@@ -1059,21 +1059,30 @@ function copyLoose(bundle, file, bytes) {
1059
1059
  const LOOSE_SIDE = 512;
1060
1060
  /** The last segment of a key that names a particle texture starts with this. */
1061
1061
  const FX_PREFIX = "fx-";
1062
+ /** A folder of particle textures: every texture below it is one. */
1063
+ const FX_FOLDER = "fx";
1062
1064
  /** The groups in the order a bundle packs them. */
1063
1065
  const GROUP_ORDER = ["fx", "main"];
1064
1066
  /**
1065
- * Picks the atlas group of one texture. A key whose last segment starts with `fx-` goes to `fx`
1066
- * whatever its size; any other texture goes to `main` when no side is above 512 px.
1067
+ * Picks the atlas group of one texture. A key whose last segment starts with `fx-`, or with a
1068
+ * folder segment `fx` between the bundle name and the stem, goes to `fx` whatever its size: the
1069
+ * frames of an animation in `fx/` too. Any other texture goes to `main` when no side is above
1070
+ * 512 px.
1067
1071
  *
1068
1072
  * @param texture - The key and the size of the texture.
1069
1073
  * @returns The group, or `undefined` when the texture stays loose by its size.
1070
1074
  * @example
1071
1075
  * ```ts
1072
1076
  * groupOf({ key: "ui.fx-rays", width: 504, height: 512 }); // "fx"
1077
+ * groupOf({ key: "ui.fx.leaf", width: 64, height: 64 }); // "fx", from assets/fx/leaf.webp of ui
1078
+ * groupOf({ key: "ui.fx", width: 64, height: 64 }); // "main": a stem, no folder
1073
1079
  * ```
1074
1080
  */
1075
1081
  function groupOf(texture) {
1076
- if (texture.key.slice(texture.key.lastIndexOf(".") + 1).startsWith(FX_PREFIX)) return "fx";
1082
+ const segments = texture.key.split(".");
1083
+ const name = segments.at(-1) ?? "";
1084
+ const folders = segments.slice(1, -1);
1085
+ if (name.startsWith(FX_PREFIX) || folders.includes(FX_FOLDER)) return "fx";
1077
1086
  return texture.width <= LOOSE_SIDE && texture.height <= LOOSE_SIDE ? "main" : void 0;
1078
1087
  }
1079
1088
  /**
@@ -3080,65 +3089,23 @@ async function runCli(argv, strings, ui = createBrandConsole()) {
3080
3089
  */
3081
3090
  /** How every generated file starts. */
3082
3091
  const HEADER = "// generated by @moku-labs/game/assets, do not edit";
3083
- /** The TypeScript cast keyword, kept apart so this file writes one and holds none. */
3084
- const CAST$1 = "as";
3092
+ /** The engine root a generated module imports its types and helpers from, quoted. */
3093
+ const ENGINE = "\"@moku-labs/game\"";
3085
3094
  /** How a key with no parameters is typed, so `tr` takes it with no second argument. */
3086
3095
  const NO_PARAMS = "Record<string, never>";
3087
3096
  /** The name of the union a plain argument is typed by. */
3088
3097
  const ARGUMENT = "Argument";
3089
3098
  /**
3090
- * The helper a compiled message calls for a plain argument: an element node keeps its place, a
3091
- * list and a number go through `Intl`, everything else is read as text.
3099
+ * The import of the helpers the messages of a module call, by the names the message bodies use:
3100
+ * `messageArgument` as `argument` for a plain argument, `messageDuration` as `duration` for a
3101
+ * duration. None when no message calls one.
3092
3102
  *
3093
- * @returns The helper as source.
3103
+ * @param entries - The keys of the locale.
3104
+ * @returns The import line, or `undefined`.
3094
3105
  */
3095
- function argumentHelper() {
3096
- return [
3097
- "/** One plain argument: an element stays an element, a list and a number go through Intl. */",
3098
- "function argument(value: unknown, intl: I18n.IntlKit): I18n.Part {",
3099
- " if (Array.isArray(value)) {",
3100
- ` return { kind: "text", text: intl.list().format(value ${CAST$1} readonly string[]) };`,
3101
- " }",
3102
- "",
3103
- " if (typeof value === \"number\") return { kind: \"text\", text: intl.number().format(value) };",
3104
- "",
3105
- " if (",
3106
- " typeof value === \"object\" &&",
3107
- " value !== null &&",
3108
- " \"type\" in value &&",
3109
- " \"props\" in value &&",
3110
- " \"children\" in value",
3111
- " ) {",
3112
- ` return { kind: "element", node: value ${CAST$1} I18n.ElementNode };`,
3113
- " }",
3114
- "",
3115
- " return { kind: \"text\", text: String(value) };",
3116
- "}"
3117
- ].join("\n");
3118
- }
3119
- /**
3120
- * The helper a compiled `duration` calls: milliseconds into the record `Intl.DurationFormat`
3121
- * reads. The same split as `durationInput` of the plugin: whole seconds rounded up, a negative or
3122
- * non-finite value as zero, hours and minutes when above zero, seconds always.
3123
- *
3124
- * @returns The helper as source.
3125
- */
3126
- function durationHelper() {
3127
- return [
3128
- "/** Milliseconds as Intl.DurationFormat reads them: whole seconds rounded up, seconds always. */",
3129
- "function duration(ms: number): Partial<Record<Intl.DurationFormatUnit, number>> {",
3130
- " const total = Number.isFinite(ms) ? Math.max(0, Math.ceil(ms / 1000)) : 0;",
3131
- " const hours = Math.floor(total / 3600);",
3132
- " const minutes = Math.floor((total % 3600) / 60);",
3133
- " const input: Partial<Record<Intl.DurationFormatUnit, number>> = {};",
3134
- "",
3135
- " if (hours > 0) input.hours = hours;",
3136
- " if (minutes > 0) input.minutes = minutes;",
3137
- " input.seconds = total % 60;",
3138
- "",
3139
- " return input;",
3140
- "}"
3141
- ].join("\n");
3106
+ function helperImport(entries) {
3107
+ const helpers = [...entries.some((entry) => entry.compiled.usesArgument) ? ["messageArgument as argument"] : [], ...entries.some((entry) => entry.compiled.usesDuration) ? ["messageDuration as duration"] : []];
3108
+ return helpers.length === 0 ? void 0 : `import { ${helpers.join(", ")} } from ${ENGINE};`;
3142
3109
  }
3143
3110
  /**
3144
3111
  * Compares two keys, so every list the compiler writes has one order.
@@ -3152,8 +3119,8 @@ function byName(left, right) {
3152
3119
  return left < right ? -1 : 1;
3153
3120
  }
3154
3121
  /**
3155
- * Writes `generated/strings.<locale>.ts`: one arrow per key, keys sorted, each helper only when a
3156
- * message needs it.
3122
+ * Writes `generated/strings.<locale>.ts`: one arrow per key, keys sorted, and the import of each
3123
+ * engine helper only when a message calls it.
3157
3124
  *
3158
3125
  * @param entries - The keys of the locale, in any order.
3159
3126
  * @returns The text of the module.
@@ -3166,11 +3133,8 @@ function byName(left, right) {
3166
3133
  function emitLocale(entries) {
3167
3134
  const sorted = [...entries].toSorted((left, right) => byName(left.key, right.key));
3168
3135
  const rows = sorted.map((entry) => ` ${JSON.stringify(entry.key)}: ${entry.compiled.source}`);
3169
- const blocks = [`${HEADER}\nimport type { I18n } from "@moku-labs/game";`];
3170
- if (sorted.some((entry) => entry.compiled.usesArgument)) blocks.push(argumentHelper());
3171
- if (sorted.some((entry) => entry.compiled.usesDuration)) blocks.push(durationHelper());
3172
- blocks.push(`export default {\n${rows.join(",\n")}\n} satisfies I18n.CompiledMessages;`);
3173
- return `${blocks.join("\n\n")}\n`;
3136
+ const imports = [`import type { I18n } from ${ENGINE};`, helperImport(sorted)].filter((line) => line !== void 0);
3137
+ return [`${HEADER}\n${imports.join("\n")}`, `export default {\n${rows.join(",\n")}\n} satisfies I18n.CompiledMessages;\n`].join("\n\n");
3174
3138
  }
3175
3139
  /**
3176
3140
  * Writes the type of one parameter.
@@ -3230,7 +3194,7 @@ function paramsOf(params) {
3230
3194
  function emitTypes(entries) {
3231
3195
  const sorted = [...entries].toSorted((left, right) => byName(left.key, right.key));
3232
3196
  const rows = sorted.map((entry) => ` ${JSON.stringify(entry.key)}: ${paramsOf(entry.params)};`);
3233
- const blocks = sorted.some((entry) => Object.values(entry.params).some((entry) => entry.kind === "argument")) ? [`${HEADER}\nimport type { DescriptionNode } from "@moku-labs/game";`, `type ${ARGUMENT} = string | number | readonly string[] | DescriptionNode;`] : [HEADER];
3197
+ const blocks = sorted.some((entry) => Object.values(entry.params).some((entry) => entry.kind === "argument")) ? [`${HEADER}\nimport type { DescriptionNode } from ${ENGINE};`, `type ${ARGUMENT} = string | number | readonly string[] | DescriptionNode;`] : [HEADER];
3234
3198
  blocks.push(`export type Strings = {\n${rows.join("\n")}\n};`, "export type StringKey = keyof Strings;");
3235
3199
  return `${blocks.join("\n\n")}\n`;
3236
3200
  }
package/dist/assets.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { a as runCli, c as emitKeys, i as compileStrings, l as emitManifest, n as importStrings, o as scanAssets, r as checkStrings, s as packAssets, t as exportStrings } from "./assets-BHwTLVta.mjs";
1
+ import { a as runCli, c as emitKeys, i as compileStrings, l as emitManifest, n as importStrings, o as scanAssets, r as checkStrings, s as packAssets, t as exportStrings } from "./assets-DYiKxPBX.mjs";
2
2
  export { checkStrings, compileStrings, emitKeys, emitManifest, exportStrings, importStrings, packAssets, runCli, scanAssets };
package/dist/cli.d.mts CHANGED
@@ -33,8 +33,8 @@ type PreparedPage = {
33
33
  };
34
34
  /**
35
35
  * Runs one `moku-game` command line in this process: `dev`, `build`, `native <verb>`, `keys`,
36
- * `pack` or `help`, with `--root`, `--preload` and `--serve-plugin` before or after the command
37
- * word. Every line goes through the branded console; nothing exits the process.
36
+ * `pack`, `visual` or `help`, with `--root`, `--preload` and `--serve-plugin` before or after the
37
+ * command word. Every line goes through the branded console; nothing exits the process.
38
38
  *
39
39
  * @param argv - The arguments after the bin.
40
40
  * @returns The exit code.
@@ -42,6 +42,7 @@ type PreparedPage = {
42
42
  * ```ts
43
43
  * // bin/moku-game.mjs in a game folder:
44
44
  * await runCli(["dev", "--port", "0"]); // 0 after Ctrl+C; stdout had "http://localhost:<port>/"
45
+ * await runCli(["visual", "--no-pixels"]); // 0 when every checkpoint of tests/visual/index.ts is the same
45
46
  * await runCli(["help"]); // 0
46
47
  * ```
47
48
  */