@moku-labs/game 0.4.6 → 0.6.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.
@@ -525,7 +525,8 @@ function invalidBookmark() {
525
525
  return /* @__PURE__ */ new Error("[game] The bookmark is not a flow.bookmark() value.\n Pass the JSON that game.bookmark returned.");
526
526
  }
527
527
  /**
528
- * Reads a bookmark out of JSON, the value `flow.bookmark()` made and the editor kept.
528
+ * Reads a bookmark out of JSON, the value `flow.bookmark()` made and the editor kept. The
529
+ * `scene` the `game.bookmark` door added is kept when present.
529
530
  *
530
531
  * @param value - The JSON the editor sent.
531
532
  * @returns The bookmark.
@@ -539,8 +540,9 @@ function readBookmark(value) {
539
540
  if (!isRecord(value)) throw invalidBookmark();
540
541
  const { path, input, player, session, graph } = value;
541
542
  const rng = readRng(value.rng);
542
- if (typeof path !== "string" || typeof graph !== "string" || !(input !== void 0 && player !== void 0 && session !== void 0) || rng === void 0) throw invalidBookmark();
543
- return {
543
+ if (!(typeof path === "string" && typeof graph === "string") || !(input !== void 0 && player !== void 0 && session !== void 0) || rng === void 0) throw invalidBookmark();
544
+ const { scene } = value;
545
+ if (scene === void 0) return {
544
546
  path,
545
547
  input,
546
548
  player,
@@ -548,6 +550,16 @@ function readBookmark(value) {
548
550
  rng,
549
551
  graph
550
552
  };
553
+ if (typeof scene !== "string") throw invalidBookmark();
554
+ return {
555
+ path,
556
+ input,
557
+ player,
558
+ session,
559
+ rng,
560
+ graph,
561
+ scene
562
+ };
551
563
  }
552
564
  /**
553
565
  * Builds the error of a value that is not a repro.
@@ -1051,9 +1063,10 @@ const fillCommand = defineCommand({
1051
1063
  //#endregion
1052
1064
  //#region src/plugins/flow/control.ts
1053
1065
  /**
1054
- * Enters a bookmark, or a repro's bookmark followed by its route.
1066
+ * Enters a bookmark, or a repro's bookmark followed by its route. A bookmark's `scene` is handed
1067
+ * to `scenes.expect` first, so a rest node without a scene of its own mounts it.
1055
1068
  *
1056
- * @param app - The app.
1069
+ * @param app - The app; with `scenes`, the bookmark's scene is expected before the restore.
1057
1070
  * @param input - Exactly one of a bookmark and a repro, as JSON.
1058
1071
  * @param input.bookmark - A `flow.bookmark()` value.
1059
1072
  * @param input.repro - A `/testing` repro.
@@ -1063,7 +1076,9 @@ const fillCommand = defineCommand({
1063
1076
  async function restoreFrom(app, input) {
1064
1077
  const { bookmark, repro } = input;
1065
1078
  if (bookmark !== void 0 && repro === void 0) {
1066
- await app.flow.restore(readBookmark(bookmark));
1079
+ const read = readBookmark(bookmark);
1080
+ if (read.scene !== void 0) app.scenes?.expect(read.scene);
1081
+ await app.flow.restore(read);
1067
1082
  return app.flow.state();
1068
1083
  }
1069
1084
  if (repro !== void 0 && bookmark === void 0) {
@@ -1127,13 +1142,15 @@ const walkCommand = defineCommand({
1127
1142
  }
1128
1143
  });
1129
1144
  /**
1130
- * Takes a bookmark of the rest point: the node plus the committed state, plain JSON.
1145
+ * Takes a bookmark of the rest point: the node plus the committed state, plain JSON. An app with
1146
+ * the screen plugins adds the mounted `scene`, so a restore in a fresh page builds it again.
1131
1147
  *
1132
1148
  * @example
1133
1149
  * ```ts
1134
- * // The editor keeps the position before a risky test.
1150
+ * // The editor keeps the position while the Settings popup stands over Home.
1135
1151
  * const { value } = await run(app, commands.bookmark);
1136
- * value.path; // "board/awaitIntent"
1152
+ * value.path; // "settings/open"
1153
+ * value.scene; // "home"
1137
1154
  * ```
1138
1155
  */
1139
1156
  const bookmarkCommand = defineCommand({
@@ -1144,12 +1161,18 @@ const bookmarkCommand = defineCommand({
1144
1161
  run: (app) => {
1145
1162
  if (typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) throw controlRefused();
1146
1163
  app.log.debug("moku:dev", { command: "game.bookmark" });
1147
- return app.flow.bookmark();
1164
+ const bookmark = app.flow.bookmark();
1165
+ const scene = app.scenes?.current();
1166
+ return scene === void 0 ? bookmark : {
1167
+ ...bookmark,
1168
+ scene
1169
+ };
1148
1170
  }
1149
1171
  });
1150
1172
  /**
1151
1173
  * Restores a bookmark, or a repro: its state at its checkpoint, then its route. Replaces the
1152
- * state outside the graph, so the session is tainted and the command journaled.
1174
+ * state outside the graph, so the session is tainted and the command journaled. A bookmark's
1175
+ * `scene` goes to `scenes.expect` first: a bookmark at a popup comes back with the scene under it.
1153
1176
  *
1154
1177
  * @example
1155
1178
  * ```ts
@@ -1157,6 +1180,12 @@ const bookmarkCommand = defineCommand({
1157
1180
  * const repro = { player: { coins: 40 }, checkpoint: "home", route: [{ at: "home", intent: "play" }] };
1158
1181
  * const ran = await run(app, commands.restore, { repro });
1159
1182
  * ran.state; // { path: "board/awaitIntent", frame: 12, tainted: true }
1183
+ *
1184
+ * // A fresh page restores the bookmark game.bookmark took at the Settings popup over Home.
1185
+ * bookmark.scene; // "home"
1186
+ * await run(app, commands.restore, { bookmark });
1187
+ * await app.flow.walk([]); // the scene stage runs after the restore resolved
1188
+ * app.scenes.current(); // "home"
1160
1189
  * ```
1161
1190
  */
1162
1191
  const restoreCommand = defineCommand({
@@ -1,4 +1,4 @@
1
- import { a as JsxChild, m as UiIntrinsicElements, t as DescriptionNode } from "./types-VErbiEXy.mjs";
1
+ import { a as JsxChild, m as UiIntrinsicElements, t as DescriptionNode } from "./types-BaUUcIaN.mjs";
2
2
 
3
3
  //#region src/plugins/ui/jsx/runtime.d.ts
4
4
  /** What the compiler passes as props: the children plus whatever the tag declared. */
@@ -1,8 +1,8 @@
1
- import { $t as SaveDoc, Kt as Json, Qt as ProviderCall, Ut as Api$2, Zt as PlayerStateProvider, an as Api$3, k as Api$1, m as Api, pn as FakeClock } from "./types-Dy1xB68L.mjs";
2
- import { _ as stepFrames, a as InputOf, d as HeadlessApp, f as HeadlessGame, g as runRepro, h as createHeadless, m as ReproResult, p as Repro, r as ControlApp } from "./types-DtmFVCHf.mjs";
3
- import { H as InputApi } from "./types-VErbiEXy.mjs";
4
- import { G as UiApi, g as AnimApi } from "./types-C-4f2jD_.mjs";
5
- import { t as commands } from "./commands-C83eQ5B9.mjs";
1
+ import { $t as SaveDoc, Kt as Json, Qt as ProviderCall, Ut as Api$2, Zt as PlayerStateProvider, an as Api$3, k as Api$1, m as Api, pn as FakeClock } from "./types-BMEn0tSC.mjs";
2
+ import { _ as stepFrames, a as InputOf, d as HeadlessApp, f as HeadlessGame, g as runRepro, h as createHeadless, m as ReproResult, p as Repro, r as ControlApp } from "./types-CYYKjvLn.mjs";
3
+ import { H as InputApi } from "./types-BaUUcIaN.mjs";
4
+ import { b as AnimApi, v as UiApi } from "./types-CXyipKhE.mjs";
5
+ import { t as commands } from "./commands-mzE9PX0H.mjs";
6
6
 
7
7
  //#region src/plugins/clock/fake.d.ts
8
8
  /**
package/dist/testing.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { n as saveOf, t as memory } from "./memory-CvgdnsQO.mjs";
2
- import { a as stepFrames, i as runRepro, n as commands, r as createHeadless, t as run } from "./run-D_R0oJ6g.mjs";
2
+ import { a as stepFrames, i as runRepro, n as commands, r as createHeadless, t as run } from "./run-Bcyg32M5.mjs";
3
3
  import { n as read, t as sources } from "./sources-DVWJ8nfr.mjs";
4
4
  import { createBrandConsole } from "@moku-labs/common/cli";
5
5
  import { mkdir, readFile, writeFile } from "node:fs/promises";
@@ -629,7 +629,7 @@ function addressesOf(keys) {
629
629
  * ```
630
630
  */
631
631
  function parentOf(parent) {
632
- if (typeof parent !== "object" || parent === null || Array.isArray(parent)) return void 0;
632
+ if (!(typeof parent === "object" && parent !== null && !Array.isArray(parent))) return void 0;
633
633
  return typeof parent.entity === "number" ? parent.entity : void 0;
634
634
  }
635
635
  /**
@@ -663,6 +663,20 @@ function stateOf(app) {
663
663
  };
664
664
  }
665
665
  /**
666
+ * The ui tree without the words of its texts, at every depth.
667
+ *
668
+ * @param node - A node of the ui source.
669
+ * @returns The node and its children, none with `content`.
670
+ */
671
+ function withoutContent(node) {
672
+ const copy = {
673
+ ...node,
674
+ children: node.children.map((child) => withoutContent(child))
675
+ };
676
+ delete copy.content;
677
+ return copy;
678
+ }
679
+ /**
666
680
  * Reads the screen a checkpoint saves: the ui tree, and every entity a projection names as
667
681
  * `{ projection, key, components }`, sorted by projection then key. Ui parts, rings and unkeyed
668
682
  * views appear through the tree only, or not at all.
@@ -675,13 +689,14 @@ function describeOf(app) {
675
689
  const views = [];
676
690
  for (const entity of read(app, sources.entities)) {
677
691
  const address = addresses.get(entity.id);
678
- if (address !== void 0) views.push({
692
+ if (address === void 0) continue;
693
+ views.push({
679
694
  ...address,
680
695
  components: withParentAddress(entity.components, addresses)
681
696
  });
682
697
  }
683
698
  return {
684
- ui: read(app, sources.ui),
699
+ ui: withoutContent(read(app, sources.ui)),
685
700
  views: views.toSorted(byAddress)
686
701
  };
687
702
  }
@@ -27,9 +27,21 @@ type Config$8 = {
27
27
  referenceLong: number;
28
28
  };
29
29
  /**
30
- * Global events. Empty: every event belongs to a plugin.
30
+ * Global events: the few that cross plugins with no `depends` edge. Every other event belongs to a
31
+ * plugin.
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * // ui, after a dev save of features/board/effects.ts; effects hooks it with no depends on ui.
36
+ * ctx.emit("ui:hot-swap", { file: "features/board/effects.ts", module: { starsBurst } });
37
+ * ```
31
38
  */
32
- type Events$6 = Record<never, never>;
39
+ type Events$6 = {
40
+ /** A dev hot swap was applied: the saved module's new exports. `ui` emits it, `effects` replaces its emitters on it. */"ui:hot-swap": {
41
+ file: string;
42
+ module: Readonly<Record<string, unknown>>;
43
+ };
44
+ };
33
45
  /**
34
46
  * Public API type of a plugin instance, read from its phantom carrier.
35
47
  * Mirrors the kernel's non-exported `ExtractPluginApi`.
@@ -2141,6 +2153,11 @@ type Bookmark = {
2141
2153
  session: Json;
2142
2154
  rng: RngState;
2143
2155
  graph: string;
2156
+ /**
2157
+ * The scene mounted when the bookmark was taken. Written by the `game.bookmark` door, read by
2158
+ * `game.restore`; `flow.restore` ignores it.
2159
+ */
2160
+ scene?: string;
2144
2161
  };
2145
2162
  /**
2146
2163
  * What `onEnter` callbacks learn about the node being entered.
@@ -3432,6 +3449,23 @@ type ProjectionApi = {
3432
3449
  * ```
3433
3450
  */
3434
3451
  register(spec: AnyProjectionSpec): void;
3452
+ /**
3453
+ * Swaps the stored spec of a registered projection. A mounted one has its layers checked against
3454
+ * the current scene and runs the new `view` for every item on the next frame, directly. An
3455
+ * unmounted one is only stored; its next `mount` uses it. Nothing changes when the call throws.
3456
+ *
3457
+ * @param spec - The new projection, under the name it was registered with.
3458
+ * @throws {Error} When no projection of that name is registered, or when a mounted one names a
3459
+ * `layer` or `lift` the scene does not declare.
3460
+ * @example
3461
+ * ```ts
3462
+ * // The dev hot swap in `ui`: the saved `view.ts` exports a new `boardItems`.
3463
+ * const world = ctx.require(worldPlugin);
3464
+ * world.projection.replace(boardItems); // the next frame draws every item with the new view
3465
+ * world.projection.replace(shopItems); // throws: Projection "shop.items" is not registered
3466
+ * ```
3467
+ */
3468
+ replace(spec: AnyProjectionSpec): void;
3435
3469
  /**
3436
3470
  * Stores the layer list of the scene. The order of the list is draw order; every call stores a
3437
3471
  * new frozen array.
@@ -3696,9 +3730,10 @@ type ProjectionApi = {
3696
3730
  *
3697
3731
  * @example
3698
3732
  * ```ts
3699
- * // `i18n` switched the locale: every label has to be projected again.
3733
+ * // The ui hot swap replaced the saved `hud` projection: every mounted view runs `view` again.
3700
3734
  * const world = ctx.require(worldPlugin);
3701
- * world.projection.rerunAll(); // the next frame writes the new strings with no motion
3735
+ * world.projection.replace(hud);
3736
+ * world.projection.rerunAll(); // the next frame draws the new view, with no motion
3702
3737
  * ```
3703
3738
  */
3704
3739
  rerunAll(): void;
@@ -1,4 +1,4 @@
1
- import { B as ComponentValue, D as Point$2, F as AnyComponentType, I as AnyComponentValue, It as Hint, Kt as Json, O as ViewportSize, P as AnyComponent, Pt as Descriptor, Rt as Api, V as Entity, Z as ChangeHook, at as ProjectionMotion, dn as Api$5, it as NumericFields, k as Api$2, lt as ViewHandle, m as Api$3, nt as Motion, o as Events$1, rt as MotionHandle, t as Api$4, ut as Api$1, vn as Require, z as ComponentType } from "./types-Dy1xB68L.mjs";
1
+ import { B as ComponentValue, D as Point$2, F as AnyComponentType, I as AnyComponentValue, It as Hint, Kt as Json, O as ViewportSize, P as AnyComponent, Pt as Descriptor, Rt as Api, V as Entity, Z as ChangeHook, at as ProjectionMotion, dn as Api$5, it as NumericFields, k as Api$2, lt as ViewHandle, m as Api$3, nt as Motion, o as Events$1, rt as MotionHandle, t as Api$4, ut as Api$1, vn as Require, z as ComponentType } from "./types-BMEn0tSC.mjs";
2
2
  import { Log } from "@moku-labs/common/browser";
3
3
  import { PluginCtx } from "@moku-labs/core";
4
4
 
@@ -834,7 +834,7 @@ type State$1 = {
834
834
  * i18n plugin events.
835
835
  */
836
836
  type Events = {
837
- /** The current locale changed and its messages are loaded. */"i18n:locale-changed": {
837
+ /** The current locale changed, or its messages were replaced. */"i18n:locale-changed": {
838
838
  locale: string;
839
839
  };
840
840
  };
@@ -960,6 +960,24 @@ type I18nApi = {
960
960
  * ```
961
961
  */
962
962
  locales(): readonly string[];
963
+ /**
964
+ * Swaps the compiled module of one feature in a loaded locale, for the dev hot swap. The one
965
+ * registered module whose keys overlap `messages` takes them, the locale is merged again and
966
+ * every key may warn once more. When the locale is the current one or the fallback,
967
+ * `i18n:locale-changed` is emitted with the current locale, so `text` re-resolves every label.
968
+ * A locale not loaded yet is left alone: its loader imports the new module later.
969
+ *
970
+ * @param locale - The locale of the saved `generated/strings.<locale>.ts`.
971
+ * @param messages - The `default` export of the new module.
972
+ * @throws {Error} When no registered module, or more than one, shares a key with `messages`.
973
+ * @example
974
+ * ```ts
975
+ * // ui's hot swap: generated/strings.ru.ts was written again with "hud.orders" as "3 заказа!".
976
+ * ctx.require(i18nPlugin).replace("ru", next.default); // emits i18n:locale-changed
977
+ * app.i18n.plain(tr("hud.orders", { n: 3 })); // "3 заказа!": text redraws the label
978
+ * ```
979
+ */
980
+ replace(locale: string, messages: CompiledMessages): void;
963
981
  };
964
982
  /**
965
983
  * What the kernel context offers this plugin.
@@ -1407,7 +1425,8 @@ type State = {
1407
1425
  };
1408
1426
  /**
1409
1427
  * text plugin API, `app.text`. Two questions: how big a piece of text is, and which styles the
1410
- * game registered. Everything else happens in the frame.
1428
+ * game registered; and one order, the dev hot swap of a styles file. Everything else happens in
1429
+ * the frame.
1411
1430
  *
1412
1431
  * @example
1413
1432
  * ```ts
@@ -1464,6 +1483,26 @@ type TextApi = {
1464
1483
  * ```
1465
1484
  */
1466
1485
  styles(): readonly string[];
1486
+ /**
1487
+ * The dev hot swap of a styles file: writes every style of the map over the table. A known
1488
+ * name keeps the feature that owns it, a new name is added and usable at once. The fonts the
1489
+ * styles name are read, the measured layouts are dropped, and every label is laid out again
1490
+ * on the next frame and redrawn with the new style, even when its text and its style name did
1491
+ * not change. The game loop is woken for that frame.
1492
+ *
1493
+ * @param styles - What `defineTextStyles` returned in the saved module.
1494
+ * @example
1495
+ * ```ts
1496
+ * // `ui` hot swaps a saved `hud/styles.ts`: the "hud.title" labels redraw at 64 px.
1497
+ * const uiStyles = defineTextStyles({
1498
+ * "hud.title": { font: "ui.font-body", size: 64, fill: 0xff0000 }
1499
+ * });
1500
+ *
1501
+ * ctx.require(textPlugin).replaceStyles(uiStyles);
1502
+ * ctx.require(textPlugin).measure("12", "hud.title").height; // 76.8 on the 0.6 em fallback
1503
+ * ```
1504
+ */
1505
+ replaceStyles(styles: TextStyles): void;
1467
1506
  };
1468
1507
  /**
1469
1508
  * Resolved dependency APIs.
@@ -2646,9 +2685,10 @@ type JsxState = {
2646
2685
  /**
2647
2686
  * One node of the snapshot `tree()` answers with. `rect` is natural: under a `fit` ancestor it
2648
2687
  * is the rect before that ancestor's scale. An element with `fit: "contain"` adds `fitScale`,
2649
- * the scale it is drawn at. An `input` adds `value`: the text being typed while it is edited,
2650
- * else the local field it writes. A windowed `scroll` adds `window`: the rows that exist, which
2651
- * are the children of its content, and how many the list has.
2688
+ * the scale it is drawn at. A `text` adds `content`: the words it draws. An `input` adds `value`:
2689
+ * the text being typed while it is edited, else the local field it writes. A windowed `scroll`
2690
+ * adds `window`: the rows that exist, which are the children of its content, and how many the
2691
+ * list has.
2652
2692
  *
2653
2693
  * @example
2654
2694
  * ```ts
@@ -2673,6 +2713,19 @@ type UiNode = {
2673
2713
  state: IsFlags;
2674
2714
  local?: Record<string, unknown>;
2675
2715
  fitScale?: number;
2716
+ /**
2717
+ * The words a `text` draws, as one plain string: the string `text` resolved after `tr()` and
2718
+ * `bind`, its tags and icons left out, else its `content` prop while that is a plain string.
2719
+ * Absent on any other tag and on a text that draws nothing.
2720
+ *
2721
+ * @example
2722
+ * ```ts
2723
+ * // Home in Russian: `<text key="title" content={tr("home.title")} />` under the root row.
2724
+ * const title = app.ui.tree().children.find(node => node.key === "title");
2725
+ * title?.content; // "Лесной городок"
2726
+ * ```
2727
+ */
2728
+ content?: string;
2676
2729
  value?: string;
2677
2730
  /**
2678
2731
  * The rows of a windowed scroll that exist and how many the list has.