@moku-labs/game 0.14.11 → 0.14.13

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 (56) hide show
  1. package/README.md +13 -12
  2. package/dist/app/page.d.mts +8 -3
  3. package/dist/app/page.mjs +57 -12
  4. package/dist/app/system.d.mts +5 -3
  5. package/dist/app/system.mjs +2 -0
  6. package/dist/app.d.mts +15 -7
  7. package/dist/app.mjs +131 -27
  8. package/dist/{assets-BrKSg1O6.mjs → assets-CEMfBogh.mjs} +471 -120
  9. package/dist/assets.d.mts +30 -13
  10. package/dist/assets.mjs +1 -1
  11. package/dist/cli.d.mts +105 -8
  12. package/dist/cli.mjs +1036 -128
  13. package/dist/{commands-d5Zlh9hf.d.mts → commands-DAETmK86.d.mts} +43 -7
  14. package/dist/{config-CdARrrhU.mjs → config-B87aFTW-.mjs} +25 -4
  15. package/dist/control.d.mts +130 -17
  16. package/dist/control.mjs +4 -4
  17. package/dist/{define-BN15lAWb.d.mts → define-BASd9ZUh.d.mts} +32 -4
  18. package/dist/dev-DbHgx-R-.mjs +89 -0
  19. package/dist/{fake-MpzwZhIZ.mjs → fake-6mazmHK3.mjs} +20 -2
  20. package/dist/features-DowEilZm.mjs +266 -0
  21. package/dist/{headless-CrhefcLu.d.mts → headless-cFveciuY.d.mts} +1 -1
  22. package/dist/hot.d.mts +4 -2
  23. package/dist/hot.mjs +7 -4
  24. package/dist/{index-0GIP3tgm.d.mts → index-DedbED3T.d.mts} +317 -69
  25. package/dist/index.d.mts +5 -6
  26. package/dist/index.mjs +3 -3
  27. package/dist/inspect.d.mts +12 -7
  28. package/dist/inspect.mjs +2 -2
  29. package/dist/jsx-dev-runtime.d.mts +1 -1
  30. package/dist/jsx-runtime.d.mts +1 -1
  31. package/dist/lint.d.mts +8 -3
  32. package/dist/lint.mjs +432 -20
  33. package/dist/project.d.mts +15 -1
  34. package/dist/project.mjs +1 -1
  35. package/dist/{run-Bf2hKyjZ.mjs → run-BbipFRc6.mjs} +250 -51
  36. package/dist/{runtime-aLkoeoGj.d.mts → runtime-DoSlRx8X.d.mts} +1 -1
  37. package/dist/{session-DgukhR_7.mjs → session-CeKgO2hy.mjs} +30 -2
  38. package/dist/{sources-Duzj6lhV.mjs → sources-Cdvb0rOS.mjs} +33 -6
  39. package/dist/{src-Do697seR.mjs → src-BEbJ_sSx.mjs} +29154 -11121
  40. package/dist/testing.d.mts +119 -14
  41. package/dist/testing.mjs +97 -7
  42. package/dist/{types-BZa187kz.d.mts → types-7GXcXxxv.d.mts} +196 -242
  43. package/dist/{types-BT9S_Lo2.d.mts → types-BGcda5Le.d.mts} +4464 -735
  44. package/dist/{types-yiHtzg1Y.d.mts → types-C73hbmxi.d.mts} +129 -17
  45. package/dist/{types-Bc9AvZVO.d.mts → types-DngFQJro.d.mts} +43 -5
  46. package/dist/{types-kNiV7iFj.d.mts → types-GHE4J5Uf.d.mts} +1027 -192
  47. package/dist/{visual-dMW8iqAE.mjs → visual-2m4wJL_z.mjs} +111 -7
  48. package/dist/visual-_XhoGeIC.d.mts +345 -0
  49. package/dist/visual.d.mts +1 -332
  50. package/dist/visual.mjs +130 -20
  51. package/dist/{watch-CWI_on4T.mjs → watch-BEdrp80i.mjs} +149 -30
  52. package/llms.txt +103 -51
  53. package/package.json +8 -6
  54. package/dist/dev-Cc50Dk7H.mjs +0 -47
  55. package/dist/memory-DR1ibF_s.d.mts +0 -50
  56. package/dist/types-DMoi447V.d.mts +0 -656
@@ -1,12 +1,12 @@
1
- import { a as sheetAlone } from "./capture-D7XvoHdH.mjs";
2
- import { n as isDev, r as notInstalled, t as controlRefused } from "./dev-Cc50Dk7H.mjs";
1
+ import { a as sheetAlone, d as graphHash } from "./capture-D7XvoHdH.mjs";
2
+ import { a as isDev, i as isControl, n as controlRefused, o as notInstalled, r as isAgent, t as controlReadOnly } from "./dev-DbHgx-R-.mjs";
3
3
  import { n as reproBookmark } from "./headless-BA6ffzqD.mjs";
4
- import { a as readTarget, i as isProjectionTarget, n as isTainted, o as inputOrEmpty, r as recordCheat, s as defineCommand } from "./session-DgukhR_7.mjs";
4
+ import { a as readTarget, i as isProjectionTarget, n as isTainted, o as inputOrEmpty, r as recordCheat, s as defineCommand } from "./session-CeKgO2hy.mjs";
5
5
  //#region src/plugins/anim/control.ts
6
6
  /**
7
- * @file anim plugin — the anim command of the `/control` door: switch reduced motion. Dev builds
8
- * only: the body starts with the inline dev guard, so a bundler `define` of `false` drops it, and
9
- * logs the `moku:dev` marker.
7
+ * @file anim plugin — the anim command of the `/control` door: switch reduced motion. Dev and
8
+ * debug builds: the body starts with the inline agent guard of a cosmetic command, so a bundler
9
+ * `define` of `false` drops it, and logs the `moku:dev` marker.
10
10
  */
11
11
  /**
12
12
  * Switches reduced motion on or off, the way the player's "Less motion" setting does. Answers
@@ -24,7 +24,7 @@ const reducedMotionCommand = defineCommand({
24
24
  input: { on: "boolean" },
25
25
  effect: "cosmetic",
26
26
  run: (app, { on }) => {
27
- if (typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) throw controlRefused();
27
+ if ((typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) && (typeof __MOKU_GAME_AGENT__ === "undefined" || !__MOKU_GAME_AGENT__)) throw controlRefused();
28
28
  app.log.debug("moku:dev", {
29
29
  command: "game.reducedMotion",
30
30
  on
@@ -36,9 +36,9 @@ const reducedMotionCommand = defineCommand({
36
36
  //#endregion
37
37
  //#region src/plugins/audio/control.ts
38
38
  /**
39
- * @file audio plugin — the audio command of the `/control` door: mute the whole game. Dev builds
40
- * only: the body starts with the inline dev guard, so a bundler `define` of `false` drops it, and
41
- * logs the `moku:dev` marker.
39
+ * @file audio plugin — the audio command of the `/control` door: mute the whole game. Dev and
40
+ * debug builds: the body starts with the inline agent guard of a cosmetic command, so a bundler
41
+ * `define` of `false` drops it, and logs the `moku:dev` marker.
42
42
  */
43
43
  /**
44
44
  * Mutes or unmutes the `master` bus, so music and sfx both go silent; the stored volumes stay.
@@ -57,7 +57,7 @@ const muteCommand = defineCommand({
57
57
  input: { muted: "boolean" },
58
58
  effect: "cosmetic",
59
59
  run: (app, { muted }) => {
60
- if (typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) throw controlRefused();
60
+ if ((typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) && (typeof __MOKU_GAME_AGENT__ === "undefined" || !__MOKU_GAME_AGENT__)) throw controlRefused();
61
61
  if (app.audio === void 0) throw notInstalled("game.mute", "audioPlugin", "command");
62
62
  app.log.debug("moku:dev", {
63
63
  command: "game.mute",
@@ -68,12 +68,54 @@ const muteCommand = defineCommand({
68
68
  }
69
69
  });
70
70
  //#endregion
71
+ //#region src/plugins/effects/control.ts
72
+ /**
73
+ * @file effects plugin — the filter command of the `/control` door: turn one filter kind off or on through
74
+ * `effects.diagnostics`. Dev and debug builds: the body starts with the inline guard of a cosmetic command, so a
75
+ * bundler `define` of `false` drops it, and logs the `moku:dev` marker.
76
+ */
77
+ /**
78
+ * Turns one filter kind off or on, through `effects.diagnostics`. Answers the kinds that are off afterwards.
79
+ * Cosmetic: it runs on a `--debug` page. A door for the MCP and the tools page; it carries no `overlay` marker.
80
+ *
81
+ * @throws {Error} `[game] No filter kind "fx.nope".` naming every known kind, and `notInstalled` without
82
+ * `effectsPlugin`.
83
+ * @example
84
+ * ```ts
85
+ * // The editor's MCP turns the glow off on the phone, then on again.
86
+ * (await run(app, commands.filter, { kind: "effects.glow", on: false })).value; // ["effects.glow"]
87
+ * (await run(app, commands.filter, { kind: "effects.glow", on: true })).value; // []
88
+ * ```
89
+ */
90
+ const filterCommand = defineCommand({
91
+ id: "game.filter",
92
+ title: "Filter kind on or off",
93
+ input: {
94
+ kind: "string",
95
+ on: "boolean"
96
+ },
97
+ effect: "cosmetic",
98
+ run: (app, { kind, on }) => {
99
+ if ((typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) && (typeof __MOKU_GAME_AGENT__ === "undefined" || !__MOKU_GAME_AGENT__)) throw controlRefused();
100
+ app.log.debug("moku:dev", {
101
+ command: "game.filter",
102
+ kind,
103
+ on
104
+ });
105
+ if (app.effects === void 0) throw notInstalled("game.filter", "effectsPlugin", "command");
106
+ const { diagnostics } = app.effects;
107
+ if (!(on ? diagnostics.enable(kind) : diagnostics.disable(kind))) throw new Error(`[game] No filter kind "${kind}".\n Known: ${diagnostics.kinds().join(", ")}.`);
108
+ return diagnostics.disabled();
109
+ }
110
+ });
111
+ //#endregion
71
112
  //#region src/plugins/input/control.ts
72
113
  /**
73
114
  * @file input plugin — the input commands of the `/control` door: a drag from one view onto
74
115
  * another, a trace through many views and a key press. Each goes through `app.input`, so the gate
75
- * decides and the session stays clean. Dev builds only: every body starts with the inline dev
76
- * guard, so a bundler `define` of `false` drops it, and logs the `moku:dev` marker. The targets
116
+ * decides and the session stays clean. Dev builds and `--debug=full` builds: every body starts
117
+ * with the inline control guard of a route command, so a bundler `define` of `false` drops it,
118
+ * and logs the `moku:dev` marker. The targets
77
119
  * are read with `target.ts`; `readTarget` is re-exported here for the `game.tap` command of ui,
78
120
  * which taps a view by the same projection key.
79
121
  */
@@ -111,7 +153,7 @@ const dragCommand = defineCommand({
111
153
  },
112
154
  effect: "route",
113
155
  run: (app, { from, to }) => {
114
- if (typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) throw controlRefused();
156
+ if ((typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) && (typeof __MOKU_GAME_CONTROL__ === "undefined" || !__MOKU_GAME_CONTROL__)) throw controlRefused();
115
157
  app.log.debug("moku:dev", { command: "game.drag" });
116
158
  return app.input.drag(readTarget(from), readTarget(to));
117
159
  }
@@ -133,7 +175,7 @@ const traceCommand = defineCommand({
133
175
  input: { path: "json" },
134
176
  effect: "route",
135
177
  run: (app, { path }) => {
136
- if (typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) throw controlRefused();
178
+ if ((typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) && (typeof __MOKU_GAME_CONTROL__ === "undefined" || !__MOKU_GAME_CONTROL__)) throw controlRefused();
137
179
  app.log.debug("moku:dev", { command: "game.trace" });
138
180
  return app.input.trace(readPath(path));
139
181
  }
@@ -158,7 +200,7 @@ const keyCommand = defineCommand({
158
200
  },
159
201
  effect: "route",
160
202
  run: (app, { key, shift = false }) => {
161
- if (typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) throw controlRefused();
203
+ if ((typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) && (typeof __MOKU_GAME_CONTROL__ === "undefined" || !__MOKU_GAME_CONTROL__)) throw controlRefused();
162
204
  app.log.debug("moku:dev", {
163
205
  command: "game.key",
164
206
  key
@@ -171,8 +213,8 @@ const keyCommand = defineCommand({
171
213
  /**
172
214
  * @file lifecycle plugin — the lifecycle commands of the `/control` door: pause and resume the
173
215
  * game for the editor, through the `devtools` pause reason, so another reason keeps its hold.
174
- * Dev builds only: every body starts with the inline dev guard, so a bundler `define` of `false`
175
- * drops it, and logs the `moku:dev` marker.
216
+ * Dev and debug builds: every body starts with the inline agent guard of a cosmetic command, so
217
+ * a bundler `define` of `false` drops it, and logs the `moku:dev` marker.
176
218
  */
177
219
  /**
178
220
  * Pauses the game with the pause reason `devtools`. Answers whether the game is paused.
@@ -190,7 +232,7 @@ const pauseCommand = defineCommand({
190
232
  input: {},
191
233
  effect: "cosmetic",
192
234
  run: (app) => {
193
- if (typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) throw controlRefused();
235
+ if ((typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) && (typeof __MOKU_GAME_AGENT__ === "undefined" || !__MOKU_GAME_AGENT__)) throw controlRefused();
194
236
  app.log.debug("moku:dev", { command: "game.pause" });
195
237
  app.lifecycle.push("devtools");
196
238
  return app.lifecycle.isPaused();
@@ -212,7 +254,7 @@ const resumeCommand = defineCommand({
212
254
  input: {},
213
255
  effect: "cosmetic",
214
256
  run: (app) => {
215
- if (typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) throw controlRefused();
257
+ if ((typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) && (typeof __MOKU_GAME_AGENT__ === "undefined" || !__MOKU_GAME_AGENT__)) throw controlRefused();
216
258
  app.log.debug("moku:dev", { command: "game.resume" });
217
259
  app.lifecycle.pop("devtools");
218
260
  return app.lifecycle.isPaused();
@@ -476,8 +518,10 @@ function readRepro(value) {
476
518
  //#region src/plugins/renderer/control.ts
477
519
  /**
478
520
  * @file renderer plugin — the renderer commands of the `/control` door: take a picture of the
479
- * canvas, with its options, and switch the debug drawing. Dev builds only: every body starts with
480
- * the inline dev guard, so a bundler `define` of `false` drops it, and logs the `moku:dev` marker.
521
+ * canvas, with its options, and switch the debug drawing. Dev and debug builds: every body starts with
522
+ * the inline agent guard of a read or cosmetic command, so a bundler `define` of `false` drops it,
523
+ * and logs the `moku:dev` marker. A capture with `diff` writes the state, so it also takes the
524
+ * control guard: a `--debug` page refuses it.
481
525
  */
482
526
  /** The input fields of `game.capture`, in the order its `moku:dev` entry lists them. */
483
527
  const CAPTURE_FIELDS = [
@@ -729,7 +773,8 @@ async function captureDiff(app, options, diff, raw) {
729
773
  * itself as a raw write (`tainted` turns true), restores the bookmark and waits for its gate, takes
730
774
  * that picture, restores where the game stood and waits again, and answers the pixel diff of now
731
775
  * against then. While the game is paused it steps each frame it waits on, 1000/60 ms of game time.
732
- * `undefined` while the renderer is inert, as in a headless test.
776
+ * `undefined` while the renderer is inert, as in a headless test. A `--debug` page takes the
777
+ * picture and refuses `diff` with the read-only error of `controlReadOnly`.
733
778
  *
734
779
  * @example
735
780
  * ```ts
@@ -751,7 +796,7 @@ const captureCommand = defineCommand({
751
796
  },
752
797
  effect: "read",
753
798
  run: (app, input) => {
754
- if (typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) throw controlRefused();
799
+ if ((typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) && (typeof __MOKU_GAME_AGENT__ === "undefined" || !__MOKU_GAME_AGENT__)) throw controlRefused();
755
800
  const given = givenFields(input);
756
801
  app.log.debug("moku:dev", given.length === 0 ? { command: "game.capture" } : {
757
802
  command: "game.capture",
@@ -759,42 +804,58 @@ const captureCommand = defineCommand({
759
804
  });
760
805
  const options = readOptions(input);
761
806
  if (input.diff === void 0) return app.renderer.capture(options);
807
+ if ((typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) && (typeof __MOKU_GAME_CONTROL__ === "undefined" || !__MOKU_GAME_CONTROL__)) throw controlReadOnly("game.capture diff");
762
808
  const diff = readDiff(input.diff);
763
809
  if (options.sheet !== void 0) throw sheetAlone();
764
810
  return captureDiff(app, options, diff, input.diff);
765
811
  }
766
812
  });
767
813
  /**
768
- * Switches the debug drawing: `nineSlice` outlines every nine-slice and the lines Pixi cuts its
769
- * texture at. Answers the switches as they are afterwards.
814
+ * Switches the debug drawing and checks: `nineSlice` outlines every nine-slice and the lines Pixi
815
+ * cuts its texture at, `verifyFrames` compares every frame the renderer skips (and every kept
816
+ * picture) with the same frame drawn live, `kept` draws every filtered view live (false) or lets
817
+ * the renderer keep pictures (true). Each field given is applied, one left out keeps its switch; a
818
+ * call with none answers the switches unchanged. Answers the switches as they are afterwards.
770
819
  *
771
820
  * @example
772
821
  * ```ts
773
822
  * // Check where the settings popup cuts its parchment.
774
- * (await run(app, commands.debug, { nineSlice: true })).value; // { nineSlice: true }
823
+ * (await run(app, commands.debug, { nineSlice: true })).value; // { nineSlice: true, verifyFrames: false, kept: true }
824
+ * // The visual runner checks the skipped frames of a test in the browser.
825
+ * (await run(app, commands.debug, { verifyFrames: true })).value; // { nineSlice: true, verifyFrames: true, kept: true }
826
+ * // The editor measures the memory screen without kept pictures.
827
+ * (await run(app, commands.debug, { kept: false })).value; // { nineSlice: true, verifyFrames: true, kept: false }
775
828
  * ```
776
829
  */
777
830
  const debugCommand = defineCommand({
778
831
  id: "game.debug",
779
832
  title: "Debug drawing",
780
- input: { nineSlice: "boolean" },
833
+ input: {
834
+ nineSlice: "boolean?",
835
+ verifyFrames: "boolean?",
836
+ kept: "boolean?"
837
+ },
781
838
  effect: "cosmetic",
782
- run: (app, { nineSlice }) => {
783
- if (typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) throw controlRefused();
839
+ run: (app, { nineSlice, verifyFrames, kept }) => {
840
+ if ((typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) && (typeof __MOKU_GAME_AGENT__ === "undefined" || !__MOKU_GAME_AGENT__)) throw controlRefused();
784
841
  app.log.debug("moku:dev", {
785
842
  command: "game.debug",
786
- nineSlice
843
+ ...nineSlice === void 0 ? {} : { nineSlice },
844
+ ...verifyFrames === void 0 ? {} : { verifyFrames },
845
+ ...kept === void 0 ? {} : { kept }
787
846
  });
788
- app.renderer.sync.debug.nineSlice(nineSlice);
847
+ if (nineSlice !== void 0) app.renderer.sync.debug.nineSlice(nineSlice);
848
+ if (verifyFrames !== void 0) app.renderer.sync.debug.verifyFrames(verifyFrames);
849
+ if (kept !== void 0) app.renderer.sync.debug.kept(kept);
789
850
  return app.renderer.sync.debug.state();
790
851
  }
791
852
  });
792
853
  //#endregion
793
854
  //#region src/plugins/time/control.ts
794
855
  /**
795
- * @file time plugin — the time commands of the `/control` door: step frames by hand and set the
796
- * time scale. Dev builds only: every body starts with the inline dev guard, so a bundler `define`
797
- * of `false` drops it, and logs the `moku:dev` marker.
856
+ * @file time plugin — the time commands of the `/control` door: step frames by hand, set the
857
+ * time scale and force the frame rate. Dev and debug builds: every body starts with the inline agent guard of a cosmetic
858
+ * command, so a bundler `define` of `false` drops it, and logs the `moku:dev` marker.
798
859
  */
799
860
  /** The length of one frame at 60 fps, the step a frame count takes by default. */
800
861
  const FRAME_MS = 1e3 / 60;
@@ -839,7 +900,7 @@ const stepCommand = defineCommand({
839
900
  },
840
901
  effect: "cosmetic",
841
902
  run: (app, { frames, deltaMs = FRAME_MS }) => {
842
- if (typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) throw controlRefused();
903
+ if ((typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) && (typeof __MOKU_GAME_AGENT__ === "undefined" || !__MOKU_GAME_AGENT__)) throw controlRefused();
843
904
  app.log.debug("moku:dev", {
844
905
  command: "game.step",
845
906
  frames
@@ -865,7 +926,7 @@ const timeScaleCommand = defineCommand({
865
926
  input: { scale: "number" },
866
927
  effect: "cosmetic",
867
928
  run: (app, { scale }) => {
868
- if (typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) throw controlRefused();
929
+ if ((typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) && (typeof __MOKU_GAME_AGENT__ === "undefined" || !__MOKU_GAME_AGENT__)) throw controlRefused();
869
930
  app.log.debug("moku:dev", {
870
931
  command: "game.timeScale",
871
932
  scale
@@ -875,14 +936,40 @@ const timeScaleCommand = defineCommand({
875
936
  return app.time.snapshot();
876
937
  }
877
938
  });
939
+ /**
940
+ * Forces the frame rate, or gives it back to the config without `fps`, and answers the rate after
941
+ * the call. For an A/B run on the phone: the same screen at 120 and at 60. A wrong `fps` is
942
+ * refused by `setRate` with its own message.
943
+ *
944
+ * @example
945
+ * ```ts
946
+ * (await run(app, commands.frameRate, { fps: 60 })).value.fps; // 60
947
+ * (await run(app, commands.frameRate, {})).value.mode; // "auto" with adaptive, "fixed" without
948
+ * ```
949
+ */
950
+ const frameRateCommand = defineCommand({
951
+ id: "game.frameRate",
952
+ title: "Frame rate",
953
+ input: { fps: "number?" },
954
+ effect: "cosmetic",
955
+ run: (app, { fps }) => {
956
+ if ((typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) && (typeof __MOKU_GAME_AGENT__ === "undefined" || !__MOKU_GAME_AGENT__)) throw controlRefused();
957
+ app.log.debug("moku:dev", {
958
+ command: "game.frameRate",
959
+ fps
960
+ });
961
+ return app.time.setRate(fps === void 0 ? "auto" : fps);
962
+ }
963
+ });
878
964
  //#endregion
879
965
  //#region src/plugins/ui/control.ts
880
966
  /**
881
967
  * @file ui plugin — the two ui commands of the `/control` door: a tap on a keyed element or on a
882
968
  * view, and the text typed into a field. The tap goes through `app.input`, so the gate decides;
883
969
  * the text goes through `app.ui.fill`, which writes a component's local and answers nothing. The
884
- * session stays clean. Dev builds only: each body starts with the inline dev guard, so a bundler
885
- * `define` of `false` drops it, and logs the `moku:dev` marker.
970
+ * session stays clean. Dev builds and `--debug=full` builds: each body starts with the inline
971
+ * control guard of a route command, so a bundler `define` of `false` drops it, and logs the
972
+ * `moku:dev` marker.
886
973
  */
887
974
  /**
888
975
  * Finds the entity of a keyed ui element on screen.
@@ -917,7 +1004,7 @@ const tapCommand = defineCommand({
917
1004
  },
918
1005
  effect: "route",
919
1006
  run: (app, { key, target }) => {
920
- if (typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) throw controlRefused();
1007
+ if ((typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) && (typeof __MOKU_GAME_CONTROL__ === "undefined" || !__MOKU_GAME_CONTROL__)) throw controlRefused();
921
1008
  app.log.debug("moku:dev", {
922
1009
  command: "game.tap",
923
1010
  key
@@ -948,7 +1035,7 @@ const fillCommand = defineCommand({
948
1035
  },
949
1036
  effect: "route",
950
1037
  run: (app, { key, value }) => {
951
- if (typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) throw controlRefused();
1038
+ if ((typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) && (typeof __MOKU_GAME_CONTROL__ === "undefined" || !__MOKU_GAME_CONTROL__)) throw controlRefused();
952
1039
  app.log.debug("moku:dev", {
953
1040
  command: "game.fill",
954
1041
  key
@@ -958,6 +1045,7 @@ const fillCommand = defineCommand({
958
1045
  });
959
1046
  //#endregion
960
1047
  //#region src/plugins/flow/control.ts
1048
+ const noPayload = null;
961
1049
  /**
962
1050
  * Restores a bookmark with its `scene` expected first, so the node the restore enters mounts it.
963
1051
  * A refused restore puts the record back to the mounted scene, when one is mounted: no later
@@ -1006,6 +1094,46 @@ async function restoreFrom(app, input) {
1006
1094
  throw new Error("[game] game.restore takes a bookmark or a repro.\n Pass exactly one of { bookmark } and { repro }.");
1007
1095
  }
1008
1096
  /**
1097
+ * Reads the save `game.saveImport` got: an object with the three trees of `game.saveExport`.
1098
+ *
1099
+ * @param save - The JSON the editor sent.
1100
+ * @returns The player, the session and the rng.
1101
+ * @throws {Error} When it is not an object with `player`, `session` and `rng`.
1102
+ * @example
1103
+ * ```ts
1104
+ * readSave({ player: { coins: 40 }, session: {}, rng: { seed: 42, streams: {} } }).player; // { coins: 40 }
1105
+ * ```
1106
+ */
1107
+ function readSave(save) {
1108
+ if (typeof save === "object" && save !== null && !Array.isArray(save)) {
1109
+ const { player, session, rng } = save;
1110
+ if (player !== void 0 && session !== void 0 && rng !== void 0) return {
1111
+ player,
1112
+ session,
1113
+ rng
1114
+ };
1115
+ }
1116
+ throw new Error("[game] game.saveImport needs { player, session, rng } of game.saveExport.");
1117
+ }
1118
+ /**
1119
+ * Builds the bookmark a save is imported at: the start of the main flow with the save's trees and
1120
+ * the hash of the graph that runs now. The start may be a transit node: `flow.restore` enters
1121
+ * one of the graph it was made for and plays it, as a fresh start does, on the imported trees.
1122
+ *
1123
+ * @param app - The started app.
1124
+ * @param save - The player, the session and the rng of the save.
1125
+ * @returns The bookmark, as JSON for `restoreFrom`.
1126
+ */
1127
+ function startBookmark(app, save) {
1128
+ const graph = app.flow.describe();
1129
+ return {
1130
+ path: graph.flows[graph.main]?.start ?? "",
1131
+ input: noPayload,
1132
+ ...save,
1133
+ graph: graphHash(graph)
1134
+ };
1135
+ }
1136
+ /**
1009
1137
  * Answers the gate, the way a tap on a button does. Goes through the graph: the session stays
1010
1138
  * clean.
1011
1139
  *
@@ -1026,7 +1154,7 @@ const answerCommand = defineCommand({
1026
1154
  },
1027
1155
  effect: "route",
1028
1156
  run: (app, { intent, payload }) => {
1029
- if (typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) throw controlRefused();
1157
+ if ((typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) && (typeof __MOKU_GAME_CONTROL__ === "undefined" || !__MOKU_GAME_CONTROL__)) throw controlRefused();
1030
1158
  app.log.debug("moku:dev", {
1031
1159
  command: "game.answer",
1032
1160
  intent
@@ -1053,7 +1181,7 @@ const walkCommand = defineCommand({
1053
1181
  input: { route: "json" },
1054
1182
  effect: "route",
1055
1183
  run: (app, { route }) => {
1056
- if (typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) throw controlRefused();
1184
+ if ((typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) && (typeof __MOKU_GAME_CONTROL__ === "undefined" || !__MOKU_GAME_CONTROL__)) throw controlRefused();
1057
1185
  app.log.debug("moku:dev", { command: "game.walk" });
1058
1186
  return app.flow.walk(readRoute(route));
1059
1187
  }
@@ -1080,7 +1208,7 @@ const bookmarkCommand = defineCommand({
1080
1208
  input: {},
1081
1209
  effect: "read",
1082
1210
  run: (app) => {
1083
- if (typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) throw controlRefused();
1211
+ if ((typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) && (typeof __MOKU_GAME_AGENT__ === "undefined" || !__MOKU_GAME_AGENT__)) throw controlRefused();
1084
1212
  app.log.debug("moku:dev", { command: "game.bookmark" });
1085
1213
  const bookmark = app.flow.bookmark();
1086
1214
  const scene = app.scenes?.current();
@@ -1122,11 +1250,70 @@ const restoreCommand = defineCommand({
1122
1250
  },
1123
1251
  effect: "raw",
1124
1252
  run: (app, input) => {
1125
- if (typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) throw controlRefused();
1253
+ if ((typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) && (typeof __MOKU_GAME_CONTROL__ === "undefined" || !__MOKU_GAME_CONTROL__)) throw controlRefused();
1126
1254
  app.log.debug("moku:dev", { command: "game.restore" });
1127
1255
  return restoreFrom(app, input);
1128
1256
  }
1129
1257
  });
1258
+ /**
1259
+ * Answers the committed state as the save holds it: the player, the session and the rng, plain
1260
+ * JSON. A read: it runs on a `--debug` page too, so a tester's progress can leave the store app.
1261
+ *
1262
+ * @example
1263
+ * ```ts
1264
+ * // The editor takes the tester's progress from the store app on the phone.
1265
+ * (await run(app, commands.saveExport, {})).value; // { player: { coins: 40, … }, session: { … }, rng: { seed: 42, streams: {} } }
1266
+ * ```
1267
+ */
1268
+ const saveExportCommand = defineCommand({
1269
+ id: "game.saveExport",
1270
+ title: "Export the save",
1271
+ input: {},
1272
+ effect: "read",
1273
+ run: (app) => {
1274
+ if ((typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) && (typeof __MOKU_GAME_AGENT__ === "undefined" || !__MOKU_GAME_AGENT__)) throw controlRefused();
1275
+ app.log.debug("moku:dev", { command: "game.saveExport" });
1276
+ const { player, session, rng } = app.model.store.snapshot();
1277
+ return {
1278
+ player,
1279
+ session,
1280
+ rng
1281
+ };
1282
+ }
1283
+ });
1284
+ /**
1285
+ * Puts a save of `game.saveExport` in place and writes it, so the next start of the app reads it:
1286
+ * the trees are restored at the main flow's start, and the game lands where a fresh start with
1287
+ * them lands. A start that is a rest node is entered; a transit node runs again on the imported
1288
+ * trees, rng included, up to the first gate it opens, at a rest node or a popup. Then the store
1289
+ * flushes. Replaces the state outside the graph, so the session is tainted and the
1290
+ * command journaled. A `--debug` page refuses it; `--debug=full` and the dev page run it.
1291
+ *
1292
+ * @throws {Error} `[game] game.saveImport needs { player, session, rng } of game.saveExport.` for
1293
+ * a value without the three trees.
1294
+ * @example
1295
+ * ```ts
1296
+ * // The debug app beside the store app gets the save exported from the store app. Its main
1297
+ * // flow starts at "boot", a transit node: boot runs again on the save and the game rests at home.
1298
+ * const ran = await run(app, commands.saveImport, { save: exported });
1299
+ * ran.value; // { restored: true }
1300
+ * ran.state.path; // "home"
1301
+ * ```
1302
+ */
1303
+ const saveImportCommand = defineCommand({
1304
+ id: "game.saveImport",
1305
+ title: "Import a save",
1306
+ input: { save: "json" },
1307
+ effect: "raw",
1308
+ run: async (app, { save }) => {
1309
+ if ((typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) && (typeof __MOKU_GAME_CONTROL__ === "undefined" || !__MOKU_GAME_CONTROL__)) throw controlRefused();
1310
+ app.log.debug("moku:dev", { command: "game.saveImport" });
1311
+ await restoreFrom(app, { bookmark: startBookmark(app, readSave(save)) });
1312
+ await app.flow.walk([]);
1313
+ await app.model.store.flush();
1314
+ return { restored: true };
1315
+ }
1316
+ });
1130
1317
  //#endregion
1131
1318
  //#region src/plugins/flow/doors/commands.ts
1132
1319
  /**
@@ -1136,7 +1323,8 @@ const restoreCommand = defineCommand({
1136
1323
  */
1137
1324
  /**
1138
1325
  * Every command of the engine, keyed by its short name: what an editor lists as buttons and MCP
1139
- * tools. Frozen. Each runs in dev builds only.
1326
+ * tools. Frozen. Each runs on the dev page and on a debug page, by the profile `run` checks: a
1327
+ * `--debug` page runs the read and cosmetic ones.
1140
1328
  *
1141
1329
  * @example
1142
1330
  * ```ts
@@ -1153,6 +1341,8 @@ const commands = Object.freeze({
1153
1341
  walk: walkCommand,
1154
1342
  bookmark: bookmarkCommand,
1155
1343
  restore: restoreCommand,
1344
+ saveExport: saveExportCommand,
1345
+ saveImport: saveImportCommand,
1156
1346
  step: stepCommand,
1157
1347
  pause: pauseCommand,
1158
1348
  resume: resumeCommand,
@@ -1161,8 +1351,10 @@ const commands = Object.freeze({
1161
1351
  reducedMotion: reducedMotionCommand,
1162
1352
  fill: fillCommand,
1163
1353
  timeScale: timeScaleCommand,
1354
+ frameRate: frameRateCommand,
1164
1355
  mute: muteCommand,
1165
- trace: traceCommand
1356
+ trace: traceCommand,
1357
+ filter: filterCommand
1166
1358
  });
1167
1359
  //#endregion
1168
1360
  //#region src/plugins/flow/doors/run.ts
@@ -1180,14 +1372,17 @@ function envelopeOf(app) {
1180
1372
  };
1181
1373
  }
1182
1374
  /**
1183
- * Runs a command in a dev build. A `cheat` or `raw` command taints the session and is journaled
1184
- * before it runs, so a failing one still counts.
1375
+ * Runs a command in a dev build or a debug build. A debug build of `--debug` runs the read and
1376
+ * cosmetic commands only; `--debug=full` and the dev page run every command. A `cheat` or `raw`
1377
+ * command taints the session and is journaled before it runs, so a failing one still counts.
1185
1378
  *
1186
1379
  * @param app - The app the command acts on.
1187
1380
  * @param command - The command.
1188
1381
  * @param input - Its input; left out when every field of its schema is optional.
1189
1382
  * @returns The command's value and the envelope read after it.
1190
- * @throws {Error} Outside a dev build, and whatever the command throws.
1383
+ * @throws {Error} Outside dev builds and debug builds (read and cosmetic commands; every command
1384
+ * with `--debug=full`), the read-only refusal of `controlReadOnly` for a `route`, `cheat` or
1385
+ * `raw` command on a `--debug` page, and whatever the command throws.
1191
1386
  * @example
1192
1387
  * ```ts
1193
1388
  * // An e2e script: load the bug report, then tap Play.
@@ -1195,10 +1390,14 @@ function envelopeOf(app) {
1195
1390
  * const ran = await run(app, commands.answer, { intent: "play" });
1196
1391
  * ran.value; // true
1197
1392
  * ran.state; // { path: "board/awaitIntent", frame: 14, tainted: true }
1393
+ * // On a --debug page: await run(app, commands.tap, { key: "play" }); // throws "[game] The command game.tap changes the game: …"
1198
1394
  * ```
1199
1395
  */
1200
1396
  async function run(app, command, ...input) {
1201
- if (!isDev()) throw controlRefused();
1397
+ if (!isDev() && !isControl()) {
1398
+ if (!isAgent()) throw controlRefused();
1399
+ if (command.effect !== "read" && command.effect !== "cosmetic") throw controlReadOnly(command.id);
1400
+ }
1202
1401
  const given = inputOrEmpty(input[0]);
1203
1402
  if (command.effect === "cheat" || command.effect === "raw") recordCheat(app, command.id, given, app.time.snapshot().frame);
1204
1403
  return {
@@ -1,4 +1,4 @@
1
- import { a as JsxChild, m as UiIntrinsicElements, t as DescriptionNode } from "./types-BZa187kz.mjs";
1
+ import { a as JsxChild, h as UiIntrinsicElements, t as DescriptionNode } from "./types-7GXcXxxv.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. */
@@ -39,8 +39,13 @@ function defineSource(source) {
39
39
  return Object.freeze(source);
40
40
  }
41
41
  /**
42
- * Declares a command: a dev-only action the editor lists and runs. Its body starts with the
43
- * inline dev guard, so a production build drops it.
42
+ * Declares a command: an action the editor lists and runs on the dev page and on a debug page.
43
+ * Its body starts with the guard of its effect: the agent form for `read` and `cosmetic`, the
44
+ * control form for the others (see docs/doors.md), so a production build drops it. A game's
45
+ * `.dev` command with only the `__MOKU_GAME_DEV__` guard runs in dev and is refused on a debug
46
+ * page. `overlay` shows it in the editor's overlay: `"toggle"` with the input `{ on: "boolean" }`
47
+ * and a `value(app)` the switch is drawn from, `"button"` with no required input and no value;
48
+ * any other combination is a type error.
44
49
  *
45
50
  * @param command - The descriptor.
46
51
  * @returns The same descriptor, frozen.
@@ -60,6 +65,29 @@ function defineSource(source) {
60
65
  * });
61
66
  * (await run(app, openShop)).state.path; // "shop"
62
67
  * ```
68
+ * @example
69
+ * ```ts
70
+ * // features/ward/glow.dev.ts — the overlay's "Glow" switch of this game.
71
+ * export const glowSwitch = defineCommand({
72
+ * id: "ward.glow",
73
+ * title: "Glow",
74
+ * input: { on: "boolean" },
75
+ * effect: "cosmetic",
76
+ * overlay: "toggle",
77
+ * value: (app: { readonly effects: Effects.EffectsApi }) =>
78
+ * !app.effects.diagnostics.disabled().includes("effects.glow"),
79
+ * run: (app, { on }) => {
80
+ * if (
81
+ * (typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) &&
82
+ * (typeof __MOKU_GAME_AGENT__ === "undefined" || !__MOKU_GAME_AGENT__)
83
+ * ) {
84
+ * throw controlRefused();
85
+ * }
86
+ * return on ? app.effects.diagnostics.enable("effects.glow") : app.effects.diagnostics.disable("effects.glow");
87
+ * }
88
+ * });
89
+ * // The overlay draws a switch "Glow", on; a tap runs it with { on: false } and the next frame draws no glow.
90
+ * ```
63
91
  */
64
92
  function defineCommand(command) {
65
93
  checkId("command", command.id);