@moku-labs/game 0.2.0 → 0.4.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.
Files changed (35) hide show
  1. package/README.md +2 -0
  2. package/dist/assets.d.mts +181 -22
  3. package/dist/assets.mjs +1186 -160
  4. package/dist/{commands-C0BljsM5.d.mts → commands-DYO22lzi.d.mts} +22 -5
  5. package/dist/control.d.mts +3 -3
  6. package/dist/control.mjs +3 -3
  7. package/dist/{define-CjP_4H0G.d.mts → define-BKWjWgyQ.d.mts} +1 -1
  8. package/dist/dev-CQYJPmcD.mjs +1183 -0
  9. package/dist/flatten-BkXuscAV.mjs +100 -0
  10. package/dist/index.d.mts +85 -31
  11. package/dist/index.mjs +3208 -490
  12. package/dist/{session-DmAxY6Ll.mjs → input-CK4nxBO1.mjs} +41 -12
  13. package/dist/inspect.d.mts +32 -9
  14. package/dist/inspect.mjs +2 -2
  15. package/dist/jsx-dev-runtime.d.mts +1 -1
  16. package/dist/jsx-dev-runtime.mjs +1 -1
  17. package/dist/jsx-runtime.d.mts +1 -1
  18. package/dist/jsx-runtime.mjs +1 -1
  19. package/dist/{run-tlcn1w7y.mjs → run-BfuMLahc.mjs} +489 -213
  20. package/dist/{runtime-Bn5TXNxP.d.mts → runtime-Bhu442L8.d.mts} +1 -1
  21. package/dist/{runtime-DRlwxkIv.mjs → runtime-CBZgWgzx.mjs} +1 -57
  22. package/dist/{sources-BooL2MsX.mjs → sources-C1uGjz4l.mjs} +198 -21
  23. package/dist/testing.d.mts +5 -5
  24. package/dist/testing.mjs +5 -4
  25. package/dist/{types-CxZy7Pi4.d.mts → types-BnWH-75b.d.mts} +95 -21
  26. package/dist/{types-CqkrEY06.d.mts → types-CVUHPvMB.d.mts} +5 -434
  27. package/dist/{types-DmhTHRVW.d.mts → types-DtmFVCHf.d.mts} +1 -1
  28. package/dist/{types-BZ1OLf24.d.mts → types-Dy1xB68L.d.mts} +2318 -1169
  29. package/dist/{types-X9jXjka3.d.mts → types-VErbiEXy.d.mts} +867 -198
  30. package/dist/{visual-NPrDkvUk.mjs → visual-DUvvPPCt.mjs} +51 -1
  31. package/llms.txt +5 -5
  32. package/package.json +5 -5
  33. package/dist/component-DGg5DqKK.mjs +0 -44
  34. package/dist/dev-CcwFrwok.mjs +0 -413
  35. package/dist/types-C47fKNcn.d.mts +0 -695
@@ -282,6 +282,56 @@ const Text = /*#__PURE__*/ component("Text", {
282
282
  alpha: 1,
283
283
  resolved: ""
284
284
  }, { owned: ["resolved"] });
285
+ /** What a `Countdown` starts as. Its own const, because a component takes a typed defaults object. */
286
+ const countdownDefaults = {
287
+ until: 0,
288
+ left: 0
289
+ };
290
+ /**
291
+ * Shows a numeric component field of the label's own entity: read every frame, written to
292
+ * `Text.resolved` only when the shown string changes. The field must hold a number; the format
293
+ * is `"int"` (rounded) unless named. Reads the component's own name, so a renamed component
294
+ * cannot drift from its label.
295
+ *
296
+ * @param component - The component whose field the label shows.
297
+ * @param field - A numeric field of that component.
298
+ * @param options - How the number is shown.
299
+ * @returns The bind a `Text` carries.
300
+ * @example
301
+ * ```ts
302
+ * const Counter = component("Counter", { value: 0 });
303
+ *
304
+ * bind(Counter, "value"); // { component: "Counter", field: "value", format: "int" }
305
+ *
306
+ * // The chest timer: the time left, as minutes and seconds.
307
+ * Text({ style: "digits", bind: bind(Countdown, "left", { format: "mm:ss" }) });
308
+ * // bind is { component: "Countdown", field: "left", format: "mm:ss" }; 95 s left shows "01:35"
309
+ * ```
310
+ */
311
+ function bind(component, field, options = {}) {
312
+ return {
313
+ component: component.componentName,
314
+ field,
315
+ format: options.format ?? "int"
316
+ };
317
+ }
318
+ /**
319
+ * A moment to count down to. A game writes `until`, a moment of `clock` in epoch milliseconds;
320
+ * `text` keeps `left` at the time that is left, computed from one `clock.now()` per frame on the
321
+ * render side, and writes it only when the label bound to it changes. At zero both stay at zero.
322
+ * Nothing of it reaches the save.
323
+ *
324
+ * @example
325
+ * ```ts
326
+ * // The chest of the board opens at a moment of the save; clock.now() is 1_790_000_000_000.
327
+ * view: chest => [
328
+ * Countdown({ until: chest.opensAt }), // opensAt: 1_790_000_095_000
329
+ * Text({ style: "digits", bind: bind(Countdown, "left", { format: "mm:ss" }) })
330
+ * ]
331
+ * // after the frame that spawns it: Text.resolved is "01:35", Countdown.left is 95000
332
+ * ```
333
+ */
334
+ const Countdown = /*#__PURE__*/ component("Countdown", countdownDefaults, { owned: ["left"] });
285
335
  /**
286
336
  * Bundles the two components a label needs, so a projection `view` reads as one line. The anchor
287
337
  * is the point of the block that sits on the transform.
@@ -737,4 +787,4 @@ function samePose(first, second) {
737
787
  return first.x === second.x && first.y === second.y && first.rotation === second.rotation && first.scale === second.scale && first.pivot.x === second.pivot.x && first.pivot.y === second.pivot.y;
738
788
  }
739
789
  //#endregion
740
- export { Order as C, resource as D, mut as E, system as O, Layer as S, component as T, Sprite as _, visualOf as a, sprite as b, builtInStyles as c, readStyle as d, textFor as f, Shape as g, Parent as h, scaleByFits as i, tag as k, defineTextStyles as l, NineSlice as m, fitScaleOf as n, visualRectOf as o, Display as p, samePose as r, Text as s, CONTENT as t, label as u, Transform as v, Tree as w, Exiting as x, componentsFor as y };
790
+ export { system as A, Exiting as C, component as D, Tree as E, mut as O, sprite as S, Order as T, Parent as _, visualOf as a, Transform as b, Text as c, defineTextStyles as d, label as f, NineSlice as g, Display as h, scaleByFits as i, tag as j, resource as k, bind as l, textFor as m, fitScaleOf as n, visualRectOf as o, readStyle as p, samePose as r, Countdown as s, CONTENT as t, builtInStyles as u, Shape as v, Layer as w, componentsFor as x, Sprite as y };
package/llms.txt CHANGED
@@ -18,12 +18,12 @@ Root exports:
18
18
  - Functions: `createApp`, `createPlugin`, `defineGame`, `defineFeature`, `type`, `exit`, `to`, `slot`, `schedule`, `guide`, `hint`.
19
19
  - Class: `SaveUnreadableError`.
20
20
  - Plugin instances: `timePlugin`, `lifecyclePlugin`, `modelPlugin`, `clockPlugin`, `flowPlugin`, `worldPlugin`, `rendererPlugin`, `inputPlugin`, `assetsPlugin`, `scenesPlugin`, `animPlugin`, `i18nPlugin`, `textPlugin`, `uiPlugin`, `audioPlugin`, `effectsPlugin`, `platformPlugin`; the list `screen` = `world`, `renderer`, `input`, `assets`, `scenes`, `anim`, `i18n`, `text`, `ui`. `effects`, `audio` and `platform` are opt-in: `plugins: [...screen, effectsPlugin, audioPlugin, platformPlugin]`, `platform` last.
21
- - Screen helpers from the root: `component`, `tag`, `resource`, `mut`, `system`, `projection`, `Layer`, `Order`, `Exiting`, `Tree`, `Transform`, `Sprite`, `NineSlice`, `Shape`, `Parent`, `Display`, `sprite`, `Tappable`, `Pressable`, `Draggable`, `DropTarget`, `Swipeable`, `Touchable`, `Held`, `Hovered`, `PointerOver`, `Pressed`, `Pointer`, `defineBundles`, `load`, `defineScene`.
22
- - Interface helpers from the root (V3): `defineAnimation`, `sequence`, `parallel`, `stagger`, `tween`, `set`, `wait`, `mark`, `frames`, `sfx`, `haptic`, `use`, `spawn`, `spawned`, `play`, `external`, `defineMotion`, `Animation`, `Frames`; `tr`; `Text`, `label`, `defineTextStyles`; `defineComponent`, `popup`, `defineStyle`, `defineTokens`, `resolve`, `Box`, `LocalWrite`; `music`.
21
+ - Screen helpers from the root: `component`, `tag`, `resource`, `mut`, `system`, `projection`, `Layer`, `Order`, `Exiting`, `Tree`, `Transform`, `Sprite`, `NineSlice`, `Shape`, `Parent`, `Display`, `sprite`, `Tappable`, `Pressable`, `Draggable`, `DropTarget`, `Swipeable`, `Traceable`, `Touchable`, `Held`, `Hovered`, `PointerOver`, `Pressed`, `Pointer`, `Traced`, `defineBundles`, `load`, `defineScene`.
22
+ - Interface helpers from the root (V3): `defineAnimation`, `sequence`, `parallel`, `stagger`, `tween`, `set`, `wait`, `mark`, `frames`, `sfx`, `haptic`, `use`, `spawn`, `spawned`, `play`, `external`, `defineMotion`, `Animation`, `Frames`; `tr`; `Text`, `label`, `defineTextStyles`, `bind`, `Countdown` and the types `BindOptions`, `CountdownValue`, `TextFormat`; `defineComponent`, `popup`, `defineStyle`, `defineTokens`, `resolve`, `Box`, `LocalWrite`; `music`.
23
23
  - Effects helpers from the root (V5): `defineEmitter`, `Emitter`, `defineFilter`, `Glow`, `Outline`, `Blur`, `ColorMatrix`, `Noise`, `Displacement`, `Alpha`.
24
24
  - Platform from the root (V6): the types `PlatformProvider`, `BackResult`, `HapticKind` and the frozen list `HAPTIC_KINDS`.
25
25
  - `defineGame<Types>()` returns, typed by the game's `player`, `session`, `assets`, `bundles`, `scenes`, `strings` and `textStyles`: `defineNode`, `defineFlow`, `defineFeature`, `projection`, `sprite`, `Sprite`, `NineSlice`, `defineBundles`, `load`, `defineScene`, `defineAnimation`, `frames`, `sfx`, `play`, `tr`, `label`, `defineTextStyles`, `defineComponent`, `defineStyle`, `defineTokens`, `popup`, `music`, `Frames`, `defineEmitter`, `Emitter`, `Displacement`. `Types` is `{ player; session; assets: string; bundles?: string; scenes?: string; strings: Record<string, unknown>; textStyles?: string; emitters?: string }`.
26
- - `@moku-labs/game/assets`: `scanAssets(options)`, `emitKeys`, `emitManifest`, `compileStrings(root, out, options?)`, `checkStrings`, `packAssets(options)`, `runCli(argv, compileStrings)`. Asset key = feature + "." + path inside `assets/`. `runCli` (a game's `bun run assets:keys`) scans the assets and compiles `features/*/strings/<locale>.json` into `generated/strings.ts` (the `Strings` type for `defineGame`) and one `generated/strings.<locale>.ts` per locale; `--check` fails when any output is out of date; `--pack <dir>` writes the production build. The package bin `moku-game-assets` is this CLI: a game's script is `"assets:keys": "moku-game-assets --root src --manifest public/assets/manifest.json --keys src/generated/assets.ts"`.
26
+ - `@moku-labs/game/assets`: `scanAssets(options)`, `emitKeys`, `emitManifest`, `compileStrings(root, out, options?)`, `checkStrings`, `packAssets(options)`, `exportStrings`, `importStrings`, `runCli(argv, strings)` with `strings: StringsTools` = `{ compile: compileStrings, exportStrings, importStrings }`. Audio files are `.mp3` or `.m4a`. Asset key = feature + "." + path inside `assets/`. `runCli` (a game's `bun run assets:keys`) scans the assets and compiles `features/*/strings/<locale>.json` into `generated/strings.ts` (the `Strings` type for `defineGame`) and one `generated/strings.<locale>.ts` per locale; `--check` fails when any output is out of date; `--pack <dir>` writes the production build. `--pseudo` also writes the pseudo-locale `en-XA`. `--export <dir>` and `--import <dir>` exchange strings with translators; `--source <locale>` names the locale they read from, `"en"` by default. The package bin `moku-game-assets` is this CLI: a game's script is `"assets:keys": "moku-game-assets --root src --manifest public/assets/manifest.json --keys src/generated/assets.ts"`.
27
27
  - Type namespaces: `Time`, `Lifecycle`, `Model`, `Clock`, `Flow`, `World`, `Renderer`, `Input`, `Assets`, `Scenes`, `Anim`, `I18n`, `TextTypes`, `Ui`, `Audio`, `Effects`, `Platform`. Use `import type { Flow } from "@moku-labs/game"` and then `Flow.RouteStep`, `Flow.NodeContext`, `Model.Json`, `Model.PlayerStateProvider`, `Clock.ClockSource`, `Time.Phase`, `Lifecycle.PauseReason`, `Anim.Target`, `I18n.Message`. `Text` is the component, so the text types are `TextTypes`.
28
28
  - `defineNode` and `defineFlow` are NOT root exports. They come from `defineGame<Types>()`.
29
29
 
@@ -367,13 +367,13 @@ The graph. Modules `runner`, `gate`, `inbox`, `fx`, `features`. Runner methods s
367
367
 
368
368
  A screen is a projection whose `view` returns JSX. The game's `tsconfig.json` sets `"jsx": "react-jsx"` and `"jsxImportSource": "@moku-labs/game"`; screens are `.tsx` files. There is no DOM and no React.
369
369
 
370
- - Tags: `screen`, `layer`, `row`, `column`, `stack`, `spacer`, `panel` (swallows taps), `image` (`texture`, `fit?: "contain" | "cover" | "fill"`), `icon` (`name`, `fit?`), `text` (`content: string | Message`, or `bind: { component, field }`; `style` is a text style key), `button` (`intent` + `payload`, or `local`, never both), `scroll` (`axis: "y"` only in V3), `input` (a text field; its text lives in the component's `local`, see the ui README). Common props: `key`, `style`, `state: { active?, disabled?, selected? }`, `motion`, `components` (extra component values for the element's entity, such as a filter: `components={[Glow({ strength: 2 })]}`); containers take `hosts?: string[]` (projections drawn inside the element). Style adds `nineSlice`, `tint`, `offsetX`, `offsetY`, `scale`, `origin`, `fit: "contain"`; state variants `is: { hover, pressed, disabled, active, selected, covered }`. `popup(Component, props, { over })` stacks a popup over a kept one.
370
+ - Tags: `screen`, `layer`, `row`, `column`, `stack`, `spacer`, `panel` (swallows taps), `image` (`texture`, `fit?: "contain" | "cover" | "fill"`), `icon` (`name`, `fit?`), `text` (`content: string | Message`, or `bind={bind(Counter, "value")}` with `components={[Counter({ value })]}`; formats `int`, `mm:ss`, `h:mm:ss`, `duration`; `Countdown({ until })` with `bind(Countdown, "left", { format: "mm:ss" })` counts down to a `clock` moment; `style` is a text style key), `button` (`intent` + `payload`, or `local`, never both), `scroll` (`axis: "y"`; children for a short list, or the windowed form `rows`, `rowHeight`, `overscan?` (5), `row: index => node`; row state belongs in the model, a row that leaves the window loses its local state), `input` (a text field; its text lives in the component's `local`, see the ui README). Common props: `key`, `style`, `state: { active?, disabled?, selected? }`, `motion`, `components` (extra component values for the element's entity, such as a filter: `components={[Glow({ strength: 2 })]}`); containers take `hosts?: string[]` (projections drawn inside the element). Style adds `nineSlice`, `tint`, `offsetX`, `offsetY`, `scale`, `origin`, `fit: "contain"`; state variants `is: { hover, pressed, disabled, active, selected, covered }`. `popup(Component, props, { over })` stacks a popup over a kept one.
371
371
  - Style vocabulary (`defineStyle`, plain data): flow `direction`, `wrap`, `justify`, `align`, `alignSelf`, `gap`, `grow`, `shrink`; box `padding`, `margin`, `width`, `height`, `minWidth`, `minHeight`, `maxWidth`, `maxHeight`, `aspect`, `overflow`; position `position`, `left`, `top`, `right`, `bottom`, `reason`; visual `fill`, `stroke`, `strokeWidth`, `radius`, `alpha`; variants `is: { pressed?, disabled?, active?, selected? }` and `when: { portrait?, landscape?, tall?, wide? }`, each a partial style merged in when the flag holds. Lengths are reference units or `"safeArea.top" | "safeArea.right" | "safeArea.bottom" | "safeArea.left"`. No font size here: it comes from the text style.
372
372
  - `defineComponent(name, { local?, outcomes?, view(props, local) })`. A component with `outcomes` is a popup: `await fx(popup(Component, props))` opens the gate for its outcomes and resolves with `{ intent, payload }` of the pressed button. A button with `local` writes into the nearest component instance; it never reaches the graph.
373
373
  - `tr(key, params?)` returns a frozen `Message` `{ key, params }`; no locale is read at the call site. Strings live in `features/<feature>/strings/<locale>.json` as ICU MessageFormat 1 (`{n, plural, one {# order} other {# orders}}`, `select`, `selectordinal`, `number`, `date`, `time`; no skeletons, no `currency`). `bun run assets:keys` compiles them. `app.i18n`: `locale()`, `setLocale(locale)` (async, emits `i18n:locale-changed`), `format(message, locale?)` returns `Part[]`, `plain(message)` returns a string, `has(key)`, `locales()`. Config: `locale: "en"`, `fallback: "en"`, `locales: {}`.
374
374
  - Text: `Text` component, `label({ text, style, at, anchor? })` for board text in a projection view, `defineTextStyles({ "hud.digits": { font, size, fill, bold?, italic?, stroke?, strokeWidth?, letterSpacing?, align?, wrap?, digits? } })` under the feature's `textStyles` key. Tags inside a string: `<b>`, `<i>`, `<color=#rrggbb>`, `<icon=key>`, `\<`, `\n`. Built-in styles `body` and `digits` from config `fonts: { body: "ui.font-body", digits: "ui.font-digits" }`. `app.text.measure(content, style)` gives the same numbers in a browser and in Bun.
375
375
  - Animation is data: `defineAnimation(id, { slots: { name: type<Anim.Target>() }, build: (slots, { at }) => Step })`. Steps: `sequence(...)`, `parallel(...)`, `stagger(items, ms, item => Step)`, `tween(target, Component, to, { ms, ease?, delayMs?, additive? })`, `set(target, Component, patch)`, `wait(ms)`, `mark(name)`, `frames(target, { keys, fps, loop? })`, `sfx(key, { bus? })`, `haptic(kind)`, `use(animation, slots, tools?)`, `spawn` (a temporary entity, flying coins, that the timeline despawns when it ends; see the anim README). `Frames({ keys, fps, loop, playing })` is a component that walks a sprite through its textures for the entity's whole life. A target is `{ projection, key }` or an entity. A node plays it with `await fx(play(animation, slots))`; in fast mode it finishes at once. `defineMotion({ states, transition?, on: { enter?, exit?, change? } })` builds the enter, exit and change hooks for a projection or the `motion` prop of a tag. Events `anim:mark` `{ animation, mark }` and `anim:finished` `{ animation }`. Config `maxTracks: 2000`.
376
- - Audio is opt-in: `plugins: [...screen, audioPlugin]`. `fx(sfx("orders.complete"))` resolves when the sound started. `await fx(music("board.theme"))` cross-fades; `music(null)` fades out; a scene's `music` key switches the track on `scenes:changed`. Buses `master`, `music`, `sfx`. Volumes come from the committed player: config `volumes: player => (player as Player).settings.audio`, applied on every `model:committed`. `app.audio`: `setVolume(bus, value)`, `volume(bus)`, `mute(bus, on)`, `unlocked()`, `journal()` (the last started sounds, empty unless config `journal` is above 0). Config: `buses: { master: 1, music: 0.6, sfx: 1 }`, `musicFadeMs: 600`, `volumes`, `context`, `journal: 0`. Without `audio` every `sfx` and `music` effect resolves `undefined` at once, so the same node runs in a game with no sound.
376
+ - Audio is opt-in: `plugins: [...screen, audioPlugin]`. `fx(sfx("orders.complete"))` resolves when the sound started. `await fx(music("board.theme"))` cross-fades; `music(null)` fades out; a scene's `music` key switches the track on `scenes:changed`. Buses `master`, `music`, `sfx`. Volumes come from the committed player: config `volumes: player => (player as Player).settings.audio`, applied on every `model:committed`. `app.audio`: `setVolume(bus, value)`, `volume(bus)`, `mute(bus, on)`, `unlocked()`, `journal()` (the last started sounds, empty unless config `journal` is above 0). Config: `buses: { master: 1, music: 0.6, sfx: 1 }`, `musicFadeMs: 600`, `volumes`, `context`, `journal: 0`, `music: "decode" | "stream"` default `"decode"` (gapless buffer; `"stream"` is an `<audio>` element, less memory, a gap at every loop), `session: "ambient" | "playback" | "auto"` default `"ambient"` (iOS only: ambient mixes with other apps and obeys the silent switch). Without `audio` every `sfx` and `music` effect resolves `undefined` at once, so the same node runs in a game with no sound.
377
377
  - `app.ui`: `tree()` the live screen as plain data, `find(key)` the entity of a keyed element, `fill(key, value)` types into the text field with that `key`, `lint()` tap targets under `tapTargetPt` (default `44`), text that overflows in some locale, absolute elements without `reason`. Config: `tapTargetPt: 44`, `breakpoints: { tall: 2, wide: 1.5 }`. `ui` emits nothing.
378
378
 
379
379
  ```tsx
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moku-labs/game",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "2D puzzle game engine on TypeScript and PixiJS, built on @moku-labs/core.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -66,10 +66,10 @@
66
66
  "lint:fix": "biome check --write . && eslint --fix .",
67
67
  "format": "biome format --write .",
68
68
  "typecheck": "tsc --noEmit",
69
- "test": "vitest run",
70
- "test:unit": "vitest run --project unit",
71
- "test:integration": "vitest run --project integration",
72
- "test:coverage": "vitest run --project unit --project integration --coverage",
69
+ "test": "bun --bun vitest run",
70
+ "test:unit": "bun --bun vitest run --project unit",
71
+ "test:integration": "bun --bun vitest run --project integration",
72
+ "test:coverage": "bun --bun vitest run --project unit --project integration --coverage",
73
73
  "build": "tsdown",
74
74
  "fixture:pack": "bun src/assets.ts --root tests/integration/merge-game --keys tests/integration/merge-game/generated/assets.ts --pack tests/integration/merge-game/dist/assets",
75
75
  "fixture:visual": "bun tests/visual/run.ts",
@@ -1,44 +0,0 @@
1
- //#region src/plugins/ui/jsx/component.ts
2
- /** The local state a component that declares none starts every instance with. */
3
- const noLocal = {};
4
- /**
5
- * Tells a component definition from a plain function component, which JSX calls at build time.
6
- *
7
- * @param type - What stood in the tag position.
8
- * @returns True when `defineComponent` built it.
9
- * @example
10
- * ```ts
11
- * isComponentDefinition(() => ({ type: "row", props: {}, children: [] })); // false
12
- * ```
13
- */
14
- function isComponentDefinition(type) {
15
- return typeof type === "function" && type.isUiComponent === true;
16
- }
17
- /**
18
- * The one implementation behind the two overloads.
19
- *
20
- * @param name - Component name.
21
- * @param spec - What the game declared.
22
- * @returns The callable definition.
23
- */
24
- function defineComponent(name, spec) {
25
- const local = spec.local ?? noLocal;
26
- const call = (props) => ({
27
- type: name,
28
- props,
29
- children: []
30
- });
31
- const definition = Object.assign(call, {
32
- outcomes: spec.outcomes,
33
- local,
34
- view: spec.view,
35
- isUiComponent: true
36
- });
37
- Object.defineProperty(definition, "name", {
38
- value: name,
39
- configurable: true
40
- });
41
- return definition;
42
- }
43
- //#endregion
44
- export { isComponentDefinition as n, defineComponent as t };
@@ -1,413 +0,0 @@
1
- //#region src/plugins/flow/runner/journal.ts
2
- /** FNV-1a 32-bit offset basis. */
3
- const OFFSET_BASIS = 2166136261;
4
- /** FNV-1a 32-bit prime. */
5
- const PRIME = 16777619;
6
- /** Separates the parts of a hashed edge, so "a|b" and "a" + "b" never collide. */
7
- const SEPARATOR = "\0";
8
- /**
9
- * Hashes text with FNV-1a, 32 bit. Pure and deterministic: the same text always gives the same
10
- * eight hexadecimal characters, in every runtime.
11
- *
12
- * @param text - The text to hash.
13
- * @returns Eight lowercase hexadecimal characters.
14
- * @example
15
- * ```ts
16
- * hashText("board/merge"); // "53ceca70"
17
- * ```
18
- */
19
- function hashText(text) {
20
- let hash = OFFSET_BASIS;
21
- for (const character of text) hash = Math.imul(hash ^ (character.codePointAt(0) ?? 0), PRIME);
22
- return (hash >>> 0).toString(16).padStart(8, "0");
23
- }
24
- /**
25
- * Hashes one edge, so a replay against edited code fails loudly in dev.
26
- *
27
- * @param path - Path of the node that produced the outcome.
28
- * @param outcome - Outcome name.
29
- * @param next - Rendered edge target.
30
- * @returns The digest of the three parts together.
31
- * @example
32
- * ```ts
33
- * entryHash("board/merge", "done", "board/awaitIntent"); // "e881176b"
34
- * ```
35
- */
36
- function entryHash(path, outcome, next) {
37
- return hashText([
38
- path,
39
- outcome,
40
- next
41
- ].join(SEPARATOR));
42
- }
43
- /**
44
- * Appends an entry with the next index and its hash, and drops the oldest entries above the limit.
45
- * The index keeps counting while entries are dropped, so two entries never share a number.
46
- *
47
- * @param state - Runner state that holds the journal.
48
- * @param entry - The taken edge without `index` and `hash`.
49
- * @param limit - Journal entries kept between checkpoints.
50
- * @returns The stored entry.
51
- */
52
- function pushEntry(state, entry, limit) {
53
- const stored = {
54
- ...entry,
55
- index: state.journalIndex,
56
- hash: entryHash(entry.path, entry.outcome, entry.next)
57
- };
58
- state.journalIndex += 1;
59
- state.journal.push(stored);
60
- if (state.journal.length > limit) state.journal.splice(0, state.journal.length - limit);
61
- return stored;
62
- }
63
- /**
64
- * Clears the journal at a checkpoint. The running index is kept, and so is the array itself: a
65
- * reader that holds `history()` sees the compaction.
66
- *
67
- * @param state - Runner state that holds the journal.
68
- */
69
- function compact(state) {
70
- state.journal.length = 0;
71
- }
72
- //#endregion
73
- //#region src/plugins/flow/runner/registry.ts
74
- /**
75
- * Creates the empty flow map. It lives in its own function because the plugin's lint rule L5
76
- * refuses a collection built inside an exported declaration.
77
- *
78
- * @returns An empty map of flow id to flow.
79
- * @example
80
- * ```ts
81
- * emptyFlows().size; // 0
82
- * ```
83
- */
84
- function emptyFlows() {
85
- return /* @__PURE__ */ new Map();
86
- }
87
- /**
88
- * Adds a flow and every flow it reaches by reference to the map. The first flow under an id wins,
89
- * so a collision is visible to `validateGraph` instead of silently replacing a flow.
90
- *
91
- * @param flow - The flow to add.
92
- * @param flows - The map being filled.
93
- */
94
- function addFlow(flow, flows) {
95
- if (flows.has(flow.id)) return;
96
- flows.set(flow.id, flow);
97
- for (const entry of Object.values(flow.nodes)) if (entry.kind === "flow") addFlow(entry, flows);
98
- }
99
- /**
100
- * Collects every flow reachable from the main flow by reference, plus the registered extras.
101
- * The main flow is the first entry: `describeGraph` reads it as the entry point of the graph.
102
- *
103
- * @param main - The top-level flow.
104
- * @param extra - Flows added with `register`, not reachable by reference.
105
- * @returns Flow id to flow, main first.
106
- * @example
107
- * ```ts
108
- * [...collectFlows(mainFlow, []).keys()]; // ["main", "board"]: the main flow first
109
- * ```
110
- */
111
- function collectFlows(main, extra) {
112
- const flows = emptyFlows();
113
- addFlow(main, flows);
114
- for (const flow of extra) addFlow(flow, flows);
115
- return flows;
116
- }
117
- /**
118
- * Renders a position as a path: the node of every frame, outermost first.
119
- *
120
- * @param stack - One frame per nesting level.
121
- * @returns The path, for example `"board/awaitIntent"`; empty without a position.
122
- * @example
123
- * ```ts
124
- * const board = { flow: "main", node: "board", input: null };
125
- * framePath([board, { flow: "board", node: "merge", input: null }]); // "board/merge"
126
- * ```
127
- */
128
- function framePath(stack) {
129
- return stack.map((frame) => frame.node).join("/");
130
- }
131
- /**
132
- * Picks the flow a path continues in after one entry: the sub-flow itself, or, behind a slot, the
133
- * first contribution in order that has the next segment as a node. A path keeps node names only,
134
- * so two contributions of one slot with the same node name cannot be told apart; `validateGraph`
135
- * warns about that.
136
- *
137
- * @param entry - The entry the path just passed.
138
- * @param next - The next segment of the path.
139
- * @param contributionsOf - Reads the contributions of a slot, in order.
140
- * @returns The flow to go on in, or `undefined` when the path ends here.
141
- */
142
- function flowBehind(entry, next, contributionsOf) {
143
- if (entry.kind === "flow") return entry;
144
- if (entry.kind !== "slot") return void 0;
145
- return contributionsOf(entry.name).find((item) => Object.hasOwn(item.flow.nodes, next))?.flow;
146
- }
147
- /**
148
- * Reads no contributions: the default of `findNode` for a caller that has no features API.
149
- *
150
- * @returns An empty list.
151
- * @example
152
- * ```ts
153
- * noContributions(); // []
154
- * ```
155
- */
156
- function noContributions() {
157
- return [];
158
- }
159
- /**
160
- * Finds the node, sub-flow or slot a path names, starting at the main flow. A path descends one
161
- * sub-flow per segment, and through a slot into the contribution that has the next segment; a
162
- * segment after a plain node finds nothing.
163
- *
164
- * @param main - The top-level flow.
165
- * @param path - A path as `framePath` renders it.
166
- * @param contributionsOf - Reads the contributions of a slot, in order. Without it a path ends at
167
- * a slot.
168
- * @returns Where the path leads, or `undefined` when no such node exists.
169
- * @example
170
- * ```ts
171
- * findNode(mainFlow, "board/awaitIntent")?.trail;
172
- * // [{ flow: "main", node: "board" }, { flow: "board", node: "awaitIntent" }]
173
- * findNode(mainFlow, "afterOrder/show", () => [{ flow: rewardFlow }])?.trail;
174
- * // [{ flow: "main", node: "afterOrder" }, { flow: "rewardPopup", node: "show" }]
175
- * ```
176
- */
177
- function findNode(main, path, contributionsOf = noContributions) {
178
- const names = path.split("/").filter(Boolean);
179
- const trail = [];
180
- let flow = main;
181
- for (const [step, name] of names.entries()) {
182
- const entry = flow.nodes[name];
183
- if (!entry) return void 0;
184
- trail.push({
185
- flow: flow.id,
186
- node: name
187
- });
188
- if (step === names.length - 1) return {
189
- flow,
190
- name,
191
- entry,
192
- trail
193
- };
194
- const behind = flowBehind(entry, names[step + 1] ?? "", contributionsOf);
195
- if (behind === void 0) return void 0;
196
- flow = behind;
197
- }
198
- }
199
- /**
200
- * Renders an edge target as a string: a node name, `"exit:win"` or `"map:node"`.
201
- *
202
- * @param target - The target as the edge table holds it.
203
- * @returns The rendered target.
204
- * @example
205
- * ```ts
206
- * renderTarget("home"); // "home"
207
- * renderTarget({ kind: "exit", outcome: "win" }); // "exit:win"
208
- * ```
209
- */
210
- function renderTarget(target) {
211
- if (typeof target === "string") return target;
212
- return target.kind === "exit" ? `exit:${target.outcome}` : `map:${target.target}`;
213
- }
214
- /**
215
- * Renders the edge table of one flow.
216
- *
217
- * @param flow - The flow to render.
218
- * @returns Node name to outcome name to rendered target.
219
- * @example
220
- * ```ts
221
- * renderEdges(boardFlow).awaitIntent?.leave; // "exit:left"
222
- * ```
223
- */
224
- function renderEdges(flow) {
225
- const edges = {};
226
- for (const [name, row] of Object.entries(flow.edges)) {
227
- const rendered = {};
228
- for (const [outcome, target] of Object.entries(row)) rendered[outcome] = renderTarget(target);
229
- edges[name] = rendered;
230
- }
231
- return edges;
232
- }
233
- /**
234
- * Maps every node and flow a feature brought to the name of that feature.
235
- *
236
- * @param features - Features API.
237
- * @returns Node or flow object to feature name.
238
- */
239
- function collectOwners(features) {
240
- const owners = /* @__PURE__ */ new Map();
241
- for (const { name, description } of features.all()) {
242
- for (const node of description.nodes ?? []) owners.set(node, name);
243
- for (const flow of description.flows ?? []) owners.set(flow, name);
244
- }
245
- return owners;
246
- }
247
- /**
248
- * Describes one entry of a flow: its flags, its scene, its outcome names, and the slot, sub-flow
249
- * or owning feature when it has one.
250
- *
251
- * @param flow - The flow that holds the entry.
252
- * @param name - Name of the entry inside that flow.
253
- * @param entry - The node, sub-flow or slot.
254
- * @param owner - Name of the feature that brought it, when a feature did.
255
- * @returns The entry as JSON.
256
- * @example
257
- * ```ts
258
- * describeNode(mainFlow, "home", home, undefined).rest; // true
259
- * describeNode(mainFlow, "board", boardFlow, "board").subFlow; // "board"
260
- * ```
261
- */
262
- function describeNode(flow, name, entry, owner) {
263
- const node = entry.kind === "node" ? entry : void 0;
264
- return {
265
- flow: flow.id,
266
- node: name,
267
- rest: node?.rest ?? false,
268
- over: node?.over ?? false,
269
- checkpoint: node?.checkpoint ?? false,
270
- barrier: node?.barrier ?? false,
271
- outcomes: Object.keys(entry.outcomes),
272
- ...node?.scene === void 0 ? {} : { scene: node.scene },
273
- ...entry.kind === "slot" ? { slot: entry.name } : {},
274
- ...entry.kind === "flow" ? { subFlow: entry.id } : {},
275
- ...owner === void 0 ? {} : { owner }
276
- };
277
- }
278
- /**
279
- * Describes every entry of one flow.
280
- *
281
- * @param flow - The flow to describe.
282
- * @param owners - Node or flow object to feature name.
283
- * @returns Node name to description.
284
- * @example
285
- * ```ts
286
- * describeNodes(boardFlow, new Map()).merge?.outcomes; // ["done", "rejected"]
287
- * ```
288
- */
289
- function describeNodes(flow, owners) {
290
- const nodes = {};
291
- for (const [name, entry] of Object.entries(flow.nodes)) nodes[name] = describeNode(flow, name, entry, owners.get(entry) ?? owners.get(flow));
292
- return nodes;
293
- }
294
- /**
295
- * Collects the slot names of every flow of the graph. `describe()` and `validate` both read the
296
- * contributions of exactly these slots.
297
- *
298
- * @param flows - Every collected flow by id.
299
- * @returns The slot names, in the order the graph declares them, each once.
300
- * @example
301
- * ```ts
302
- * slotNames(collectFlows(mainFlow, [])); // ["afterOrder"]
303
- * ```
304
- */
305
- function slotNames(flows) {
306
- const names = [];
307
- for (const flow of flows.values()) for (const entry of Object.values(flow.nodes)) if (entry.kind === "slot" && !names.includes(entry.name)) names.push(entry.name);
308
- return names;
309
- }
310
- /**
311
- * Renders the whole graph as JSON without running the game: nodes, flags, outcomes, edges,
312
- * slots and who contributed. Edge targets become `"node"`, `"exit:win"`, `"map:node"`.
313
- *
314
- * @param flows - Every collected flow by id, the main flow first, as `collectFlows` returns them.
315
- * @param features - Features API: owners and slot contributions.
316
- * @returns The graph as plain JSON.
317
- */
318
- function describeGraph(flows, features) {
319
- const owners = collectOwners(features);
320
- const described = {};
321
- const slots = {};
322
- for (const flow of flows.values()) described[flow.id] = {
323
- nodes: describeNodes(flow, owners),
324
- start: flow.start,
325
- edges: renderEdges(flow)
326
- };
327
- for (const name of slotNames(flows)) slots[name] = features.contributions(name).map(({ feature, flow, order }) => ({
328
- feature,
329
- flow: flow.id,
330
- order
331
- }));
332
- return {
333
- main: [...flows.keys()][0] ?? "",
334
- flows: described,
335
- slots
336
- };
337
- }
338
- /**
339
- * Compares two object keys without a locale, so the rendering is the same everywhere.
340
- *
341
- * @param first - First key.
342
- * @param second - Second key.
343
- * @returns A negative number, zero or a positive number.
344
- * @example
345
- * ```ts
346
- * compareKeys("edges", "nodes"); // -1
347
- * ```
348
- */
349
- function compareKeys(first, second) {
350
- if (first === second) return 0;
351
- return first < second ? -1 : 1;
352
- }
353
- /**
354
- * Renders a JSON value with its object keys sorted, so two equal graphs written in a different
355
- * order hash the same.
356
- *
357
- * @param value - Any JSON value.
358
- * @returns The value as text.
359
- * @example
360
- * ```ts
361
- * stableJson({ b: 1, a: 2 }); // '{"a":2,"b":1}'
362
- * ```
363
- */
364
- function stableJson(value) {
365
- if (Array.isArray(value)) return `[${value.map((item) => stableJson(item)).join(",")}]`;
366
- if (typeof value !== "object" || !value) return JSON.stringify(value) ?? "";
367
- return `{${Object.entries(value).filter(([, item]) => item !== void 0).toSorted(([first], [second]) => compareKeys(first, second)).map(([key, item]) => `${JSON.stringify(key)}:${stableJson(item)}`).join(",")}}`;
368
- }
369
- /**
370
- * Hashes a graph description. A bookmark of a plain rest node is accepted only while this hash
371
- * is unchanged.
372
- *
373
- * @param graph - Result of `describeGraph`.
374
- * @returns Eight lowercase hexadecimal characters.
375
- * @example
376
- * ```ts
377
- * graphHash({ main: "main", flows: {}, slots: {} }); // "cfb16c03"
378
- * ```
379
- */
380
- function graphHash(graph) {
381
- return hashText(stableJson(graph));
382
- }
383
- //#endregion
384
- //#region src/plugins/flow/doors/dev.ts
385
- /**
386
- * Reads the dev flag at call time. Undefined means off: production is the default.
387
- *
388
- * @returns True only when `__MOKU_GAME_DEV__` is exactly `true`.
389
- * @example
390
- * ```ts
391
- * // The dev page set the flag in web/dev.ts before the engine started.
392
- * globalThis.__MOKU_GAME_DEV__ = true;
393
- * isDev(); // true
394
- * ```
395
- */
396
- function isDev() {
397
- return typeof __MOKU_GAME_DEV__ !== "undefined" && __MOKU_GAME_DEV__ === true;
398
- }
399
- /**
400
- * Builds the error a control command throws outside a dev build.
401
- *
402
- * @returns The error, ready to throw.
403
- * @example
404
- * ```ts
405
- * // A production build called a /control command.
406
- * controlRefused().message; // "[game] Control commands run in dev builds only.\n Define __MOKU_GAME_DEV__ as true in the dev build."
407
- * ```
408
- */
409
- function controlRefused() {
410
- return /* @__PURE__ */ new Error("[game] Control commands run in dev builds only.\n Define __MOKU_GAME_DEV__ as true in the dev build.");
411
- }
412
- //#endregion
413
- export { findNode as a, slotNames as c, describeGraph as i, compact as l, isDev as n, framePath as o, collectFlows as r, graphHash as s, controlRefused as t, pushEntry as u };