@moku-labs/game 0.15.30 → 0.15.32

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.
@@ -1,4 +1,4 @@
1
- import { m as StartedPage, r as GameConfig, t as AnyGameApp, u as PageOptions } from "../types-BWRJcZ3n.mjs";
1
+ import { m as StartedPage, r as GameConfig, t as AnyGameApp, u as PageOptions } from "../types-DmU_qlvA.mjs";
2
2
 
3
3
  //#region src/app/page.d.ts
4
4
  /**
package/dist/app/page.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { r as systemSource, t as memory } from "../memory-CWlqhS7l.mjs";
2
2
  import { t as applyTo } from "../drafts-BawpSaVp.mjs";
3
3
  import { i as isControl } from "../dev-BNndV-hU.mjs";
4
+ import { n as fakeClock, t as VISUAL_CLOCK } from "../fake-hnJcJeYf.mjs";
4
5
  import { n as read, r as watch, t as sources } from "../sources-DM5Zcl4o.mjs";
5
6
  import { t as resolveConfig } from "../config-B87aFTW-.mjs";
6
7
  //#region src/app/save.ts
@@ -211,6 +212,20 @@ function toError(thrown) {
211
212
  return thrown instanceof Error ? thrown : new Error(String(thrown));
212
213
  }
213
214
  /**
215
+ * The clock of the page: the device clock; on a dev page a visual test's pixel leg opened, the fake
216
+ * clock its headless leg runs on, standing at the same moment (F86). The leg puts the moment on the
217
+ * page before it loads, so the app never sees the device time.
218
+ *
219
+ * @returns The clock source of the page.
220
+ */
221
+ function pageClock() {
222
+ if (typeof __MOKU_GAME_DEV__ !== "undefined" && __MOKU_GAME_DEV__) {
223
+ const moment = Reflect.get(globalThis, VISUAL_CLOCK);
224
+ if (typeof moment === "number") return fakeClock(moment);
225
+ }
226
+ return systemSource();
227
+ }
228
+ /**
214
229
  * Picks the start of the page from `?player=`: the named scenario's player and session, at the
215
230
  * device time. An unknown name is reported, and the page starts from the starting player.
216
231
  *
@@ -382,7 +397,7 @@ async function startPage(game, config, options = {}) {
382
397
  const settings = resolveConfig(config);
383
398
  const query = new URLSearchParams(location.search);
384
399
  const { report, attach } = lateReporter();
385
- const clock = systemSource();
400
+ const clock = pageClock();
386
401
  const start = scenarioStart(query.get("player"), options.scenarios ?? {}, clock.now(), report);
387
402
  const shell = options.system === void 0 ? void 0 : await options.system(settings, report);
388
403
  const namespace = settings.native?.identifier ?? "moku-game";
@@ -1,6 +1,6 @@
1
1
  import { Mr as PlayerStateProvider } from "../types-D27I4Q7b.mjs";
2
- import { ht as PlatformProvider } from "../index-B2GejbLf.mjs";
3
- import { _ as SystemShellFactory, g as SystemName, h as SystemModules } from "../types-BWRJcZ3n.mjs";
2
+ import { ht as PlatformProvider } from "../index-DMeqKGSu.mjs";
3
+ import { _ as SystemShellFactory, g as SystemName, h as SystemModules } from "../types-DmU_qlvA.mjs";
4
4
  import { Back, Haptics, KeepAwake, Lifecycle, Store } from "@moku-labs/system";
5
5
 
6
6
  //#region src/app/system.d.ts
package/dist/app.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Dr as Json } from "./types-D27I4Q7b.mjs";
2
2
  import { U as StringTable } from "./types-BAp9HdR4.mjs";
3
- import { a as GameHandle, c as MemoryProvider, d as SaveKind, f as Scenario, g as SystemName, i as GameDefinition, l as PageAgent, n as GameApp, o as GamePluginConfigs, p as ScreenSeams, r as GameConfig, s as HeadlessSeams, v as WithGameState } from "./types-BWRJcZ3n.mjs";
3
+ import { a as GameHandle, c as MemoryProvider, d as SaveKind, f as Scenario, g as SystemName, i as GameDefinition, l as PageAgent, n as GameApp, o as GamePluginConfigs, p as ScreenSeams, r as GameConfig, s as HeadlessSeams, v as WithGameState } from "./types-DmU_qlvA.mjs";
4
4
  import { AnyPluginInstance } from "@moku-labs/core";
5
5
 
6
6
  //#region src/app/define.d.ts
package/dist/app.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { Ft as audioPlugin, H as platformPlugin, i as screen, m as flattenFeatures, t as createApp, wt as effectsPlugin } from "./src-B4JjsuEy.mjs";
1
+ import { Ft as audioPlugin, H as platformPlugin, i as screen, m as flattenFeatures, t as createApp, wt as effectsPlugin } from "./src-5qdNjLKN.mjs";
2
2
  import { t as memory } from "./memory-CWlqhS7l.mjs";
3
- import { n as isFakeClock, t as fakeClock } from "./fake-6mazmHK3.mjs";
3
+ import { n as fakeClock, r as isFakeClock } from "./fake-hnJcJeYf.mjs";
4
4
  //#region src/app/define.ts
5
5
  /**
6
6
  * The moment the default fake clock starts at, in epoch milliseconds. A test stamps times from it.
@@ -1,5 +1,5 @@
1
1
  import { $r as Time, D as DebugSwitches, Ir as Snapshot, Rr as StoreApi, Vr as Api, Y as Api$1, Yr as FrameRate, ar as FlowState, nn as Api$2, nr as Bookmark, ot as Captured, w as ScenesApi } from "./types-D27I4Q7b.mjs";
2
- import { P as AnimApi, a as AudioApi, h as EffectsApi, r as UiApi } from "./types-DjQkfbfZ.mjs";
2
+ import { P as AnimApi, a as AudioApi, h as EffectsApi, r as UiApi } from "./types-DHamyZq1.mjs";
3
3
  import { N as InputApi } from "./types-BAp9HdR4.mjs";
4
4
  import { t as HeadlessApp } from "./headless-f31cEZ7f.mjs";
5
5
  import { n as Command, r as ControlApp } from "./types-CgE5L_7f.mjs";
@@ -122,7 +122,7 @@ declare const commands: Readonly<{
122
122
  readonly renderer: Api$1;
123
123
  }>;
124
124
  reducedMotion: Command<{
125
- on: "boolean";
125
+ readonly on: "boolean";
126
126
  }, boolean, HeadlessApp & {
127
127
  readonly log: import("@moku-labs/common/browser").LogApi;
128
128
  } & {
@@ -165,8 +165,8 @@ declare const commands: Readonly<{
165
165
  readonly input: InputApi;
166
166
  }>;
167
167
  filter: Command<{
168
- readonly kind: "string";
169
168
  readonly on: "boolean";
169
+ readonly kind: "string";
170
170
  }, readonly string[], HeadlessApp & {
171
171
  readonly log: import("@moku-labs/common/browser").LogApi;
172
172
  } & {
@@ -1,6 +1,6 @@
1
1
  import { t as HeadlessApp } from "./headless-f31cEZ7f.mjs";
2
2
  import { i as InputArguments, n as Command, o as InputSchema, s as Ran } from "./types-CgE5L_7f.mjs";
3
- import { t as commands } from "./commands-BUfZJUx6.mjs";
3
+ import { t as commands } from "./commands-C8N_rpwt.mjs";
4
4
  import { t as defineCommand } from "./define-CE1uxbe9.mjs";
5
5
 
6
6
  //#region src/plugins/flow/doors/dev.d.ts
package/dist/control.mjs CHANGED
@@ -1,4 +1,4 @@
1
1
  import { n as controlRefused, t as controlReadOnly } from "./dev-BNndV-hU.mjs";
2
2
  import { s as defineCommand } from "./session-DuZfGZWk.mjs";
3
- import { n as commands, t as run } from "./run-DyL8rl2P.mjs";
3
+ import { n as commands, t as run } from "./run-BnriyKUu.mjs";
4
4
  export { commands, controlReadOnly, controlRefused, defineCommand, run };
@@ -101,5 +101,11 @@ function fakeClock(start = 0) {
101
101
  function isFakeClock(source) {
102
102
  return fakeClocks.has(source);
103
103
  }
104
+ /**
105
+ * The global a visual test's pixel leg sets on the dev page before it loads (F86): the moment of
106
+ * the fake clock its headless leg runs on. The page of a dev build then runs on `fakeClock` at
107
+ * that moment, so both legs see the same `clock.now()`. Not in the root.
108
+ */
109
+ const VISUAL_CLOCK = "__mokuVisualClock";
104
110
  //#endregion
105
- export { isFakeClock as n, fakeClock as t };
111
+ export { fakeClock as n, isFakeClock as r, VISUAL_CLOCK as t };
@@ -1,5 +1,5 @@
1
1
  import { $n as FeaturePlugin, An as NumericFields, B as ShapeValue, Bn as EmitterIdOf, C as SceneSpec, Cn as TagType, Dr as Json, Dt as PassKind, En as Ease, F as NineSliceValue, Gn as TextStylesOf, Gr as State$4, Hn as HudKeysOf, Hr as Config$4, Jr as Config$8, K as TransformValue, L as OverlayValue, Ln as Api$2, Mn as ProjectionSpec, N as HudPartValue, Nn as ReducedMotion, Or as Root, Qn as FeatureDescription, Qr as ThermalReading, Rn as BundlesOf, Rr as StoreApi, S as SceneDefinition, Sn as SystemFor, Sr as State$2, T as State$7, Tn as DescriptionNode, Tr as Config$5, U as SpriteOptions, Ur as Events, Vn as GameTypes, Vr as Api$3, W as SpriteValue, Wn as State$3, Wr as PauseReason, X as Config$6, Xr as RateChange, Y as Api$4, Zr as State$8, _ as AnySceneProjection, _n as Mut, _r as Answer, _t as FrameRing, a as Config$1, an as State$9, bn as ResourceType, br as Config$2, c as LoadBundles, dr as Descriptor, dt as ViewportSize, er as FeatureTree, f as ReadyBundles, fn as ComponentType, fr as Effect, gt as FrameRecord, hr as Hint, i as BundleSpec, jn as ProjectionMotion, jr as Patch, kr as State$5, l as LoadReason, lr as SceneIdOf, m as Tier, mr as GuideOptions, ni as Events$1, nn as Api$6, o as DefineBundles, p as State$1, pn as ComponentValue, pr as EffectOptions, q as sprite, qn as defineFlow, qr as Api$5, r as BundleMap, ri as Require, rn as Config$9, rr as DefineNode, rt as State$6, t as Api, ti as Config$10, u as LoadResult, un as ComponentHandle, v as Config$7, vn as Narrowed, vr as Api$1, w as ScenesApi, x as LayerMap, xn as SystemDefinition, y as DefineScene, yn as QueryTerm, zn as Config$3, zr as RngApi } from "./types-D27I4Q7b.mjs";
2
- import { A as GlowValue, B as Target, C as BlurValue, D as FilterComponent, F as AnimPlayerValue, H as HapticKind, I as Config$11, K as defineAnimation, L as FramesValue, M as OutlineValue, N as UniformDeclaration, O as FilterSpec, P as AnimApi, Q as play, R as MotionKeyframe, S as AlphaValue, T as DisplacementValue, U as SfxDescriptor, W as Step, _ as State$12, a as AudioApi, b as EmitterDefinition, c as MusicDescriptor, d as State$11, f as VolumesFor, h as EffectsApi, j as NoiseValue, k as FilterValueOf, l as MusicOptions, lt as OverlayApi, m as Config$13, n as State$13, r as UiApi, s as Config$12, t as Config$14, w as ColorMatrixValue, x as EmitterValue, y as EmitterConfig, z as State$10 } from "./types-DjQkfbfZ.mjs";
2
+ import { A as GlowValue, B as Target, C as BlurValue, D as FilterComponent, F as AnimPlayerValue, H as HapticKind, I as Config$11, K as defineAnimation, L as FramesValue, M as OutlineValue, N as UniformDeclaration, O as FilterSpec, P as AnimApi, Q as play, R as MotionKeyframe, S as AlphaValue, T as DisplacementValue, U as SfxDescriptor, W as Step, _ as State$12, a as AudioApi, b as EmitterDefinition, c as MusicDescriptor, d as State$11, f as VolumesFor, h as EffectsApi, j as NoiseValue, k as FilterValueOf, l as MusicOptions, lt as OverlayApi, m as Config$13, n as State$13, r as UiApi, s as Config$12, t as Config$14, w as ColorMatrixValue, x as EmitterValue, y as EmitterConfig, z as State$10 } from "./types-DHamyZq1.mjs";
3
3
  import { B as Message, C as State$16, D as TextStyleInput, G as TypedTr, H as State$14, I as Argument, J as ResolvedStyle, L as Config$15, M as Config$16, N as InputApi, O as TextStyles, P as State$15, R as I18nApi, S as Point, T as TextBind, V as Part, W as TextKey, Y as Style, Z as WhenFlags, b as Config$17, g as IntrinsicElementsFor, h as defineTokens, j as defineComponent, k as TextValue, q as IsFlags, s as PopupComponent, v as BindField, w as TextApi, x as CountdownValue, y as BindOptions, z as IntlKit } from "./types-BAp9HdR4.mjs";
4
4
  import { Log } from "@moku-labs/common/browser";
5
5
  import { PluginCtx } from "@moku-labs/core";
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { $t as ShaderWhat, Ar as types_d_exports$10, At as PerfChange, Bt as PerfLongFrameLine, Cr as types_d_exports$3, Ct as PERF_CAUSES, Dt as PassKind, E as types_d_exports$13, Et as PROBLEM_RULES, Fr as SaveUnreadableError, Ft as PerfEventLine, G as Transform, Gt as PerfStartupLine, H as SpriteBlend, Ht as PerfMeasured, I as Overlay, It as PerfEventName, J as AdaptiveScale, Jn as exit, Jt as PoolTrimDetail, Kn as types_d_exports$6, Kr as types_d_exports$9, Kt as PerfTelemetryLine, L as OverlayValue, Lt as PerfGapLine, M as HudPart, Mt as PerfCollectorLine, N as HudPartValue, Nt as PerfCounts, Ot as PassRecord, P as NineSlice, Pt as PerfDisplay, Qt as ShaderEventDetail, R as Parent, Rt as PerfHeaderLine, St as OverlayWhat, Tn as DescriptionNode, Tt as PERF_LOG_VERSION, Ut as PerfSampleLine, V as Sprite, Vt as PerfMarkLine, Wt as PerfSpikeLine, Xn as to, Xt as ProblemRule, Yn as slot, Yt as ProblemEventDetail, Z as MonitorConfig, Zn as type, Zt as SHADER_WHATS, _t as FrameRing, at as types_d_exports$12, bt as MonitorSections, ei as types_d_exports$15, en as WarmDetail, g as types_d_exports$1, gt as FrameRecord, ht as FrameReadout, j as Display, jt as PerfChangeEntry, kt as PerfCause, mt as FrameReadResult, nt as PoolTrim, on as types_d_exports$17, pt as FrameColumns, q as sprite, qt as PerfThermalLine, st as MonitorCounts, tn as DEMAND_REASONS, vt as LongFrameEntry, wt as PERF_GAP_BUCKETS, xt as OverlayEventDetail, yt as MONITOR_SECTIONS, z as Shape, zt as PerfLogLine } from "./types-D27I4Q7b.mjs";
2
- import { $ as sequence, E as FilterArea, G as HAPTIC_KINDS, H as HapticKind, J as frames, K as defineAnimation, Q as play, V as types_d_exports, X as mark, Y as haptic, Z as parallel, at as stopSfx, ct as wait, et as set, i as types_d_exports$16, it as stagger, nt as spawn, ot as tween, p as types_d_exports$2, q as external, rt as spawned, st as use, tt as sfx, ut as types_d_exports$4, v as types_d_exports$5 } from "./types-DjQkfbfZ.mjs";
2
+ import { $ as sequence, E as FilterArea, G as HAPTIC_KINDS, H as HapticKind, J as frames, K as defineAnimation, Q as play, V as types_d_exports, X as mark, Y as haptic, Z as parallel, at as stopSfx, ct as wait, et as set, i as types_d_exports$16, it as stagger, nt as spawn, ot as tween, p as types_d_exports$2, q as external, rt as spawned, st as use, tt as sfx, ut as types_d_exports$4, v as types_d_exports$5 } from "./types-DHamyZq1.mjs";
3
3
  import { A as types_d_exports$14, E as TextFormat, F as types_d_exports$8, K as types_d_exports$7, d as DebugTrigger, f as LocalWrite, h as defineTokens, j as defineComponent, m as popup, p as ScrollState, u as Box, x as CountdownValue, y as BindOptions } from "./types-BAp9HdR4.mjs";
4
- import { $ as system, A as Draggable, At as Text, B as Touchable, C as AnimPlayer, Ct as debugPlugin, D as defineBundles, Dt as animPlugin, E as defineScene, Et as assetsPlugin, F as PointerOver, G as Exiting, H as Traced, I as Pressable, J as Order, K as Layer, L as Pressed, M as Held, Mt as defineTextStyles, N as Hovered, Nt as label, O as load, Ot as projection, P as Pointer, Q as resource, R as Swipeable, S as defineMotion, St as effectsPlugin, T as Frames, Tt as audioPlugin, U as PASS_KINDS, V as Traceable, W as frameAt, X as component, Y as Tree, Z as mut, _ as defineStyle, _t as modelPlugin, a as defineEmitter, at as defineFeature, b as messageDuration, bt as i18nPlugin, c as Alpha, ct as uiPlugin, d as Displacement, dt as scenesPlugin, et as tag, f as Glow, ft as rendererPlugin, g as resolve, gt as types_d_exports$11, h as music, ht as PlatformProvider, i as screen, it as schedule, j as DropTarget, jt as bind, k as ready, kt as Countdown, l as Blur, lt as timePlugin, m as Outline, mt as BackResult, n as createPlugin, nt as guide, o as Emitter, ot as flattenFeatures, p as Noise, pt as platformPlugin, q as Now, r as defineGame, rt as hint, s as defineFilter, st as worldPlugin, t as createApp, tt as defineEffect, u as ColorMatrix, ut as textPlugin, v as tr, vt as lifecyclePlugin, w as Animation, wt as clockPlugin, x as cubicBezier, xt as flowPlugin, y as messageArgument, yt as inputPlugin, z as Tappable } from "./index-B2GejbLf.mjs";
4
+ import { $ as system, A as Draggable, At as Text, B as Touchable, C as AnimPlayer, Ct as debugPlugin, D as defineBundles, Dt as animPlugin, E as defineScene, Et as assetsPlugin, F as PointerOver, G as Exiting, H as Traced, I as Pressable, J as Order, K as Layer, L as Pressed, M as Held, Mt as defineTextStyles, N as Hovered, Nt as label, O as load, Ot as projection, P as Pointer, Q as resource, R as Swipeable, S as defineMotion, St as effectsPlugin, T as Frames, Tt as audioPlugin, U as PASS_KINDS, V as Traceable, W as frameAt, X as component, Y as Tree, Z as mut, _ as defineStyle, _t as modelPlugin, a as defineEmitter, at as defineFeature, b as messageDuration, bt as i18nPlugin, c as Alpha, ct as uiPlugin, d as Displacement, dt as scenesPlugin, et as tag, f as Glow, ft as rendererPlugin, g as resolve, gt as types_d_exports$11, h as music, ht as PlatformProvider, i as screen, it as schedule, j as DropTarget, jt as bind, k as ready, kt as Countdown, l as Blur, lt as timePlugin, m as Outline, mt as BackResult, n as createPlugin, nt as guide, o as Emitter, ot as flattenFeatures, p as Noise, pt as platformPlugin, q as Now, r as defineGame, rt as hint, s as defineFilter, st as worldPlugin, t as createApp, tt as defineEffect, u as ColorMatrix, ut as textPlugin, v as tr, vt as lifecyclePlugin, w as Animation, wt as clockPlugin, x as cubicBezier, xt as flowPlugin, y as messageArgument, yt as inputPlugin, z as Tappable } from "./index-DMeqKGSu.mjs";
5
5
  export { type AdaptiveScale, Alpha, types_d_exports as Anim, AnimPlayer, Animation, types_d_exports$1 as Assets, types_d_exports$2 as Audio, type BackResult, type BindOptions, Blur, Box, types_d_exports$3 as Clock, ColorMatrix, Countdown, type CountdownValue, DEMAND_REASONS, types_d_exports$4 as Debug, DebugTrigger, type DescriptionNode, Displacement, Display, Draggable, DropTarget, types_d_exports$5 as Effects, Emitter, Exiting, type FilterArea, types_d_exports$6 as Flow, type FrameColumns, type FrameReadResult, type FrameReadout, type FrameRecord, type FrameRing, Frames, Glow, HAPTIC_KINDS, type HapticKind, Held, Hovered, HudPart, type HudPartValue, types_d_exports$7 as I18n, types_d_exports$8 as Input, Layer, types_d_exports$9 as Lifecycle, LocalWrite, type LongFrameEntry, MONITOR_SECTIONS, types_d_exports$10 as Model, type MonitorConfig, type MonitorCounts, type MonitorSections, NineSlice, Noise, Now, Order, Outline, Overlay, type OverlayEventDetail, type OverlayValue, type OverlayWhat, PASS_KINDS, PERF_CAUSES, PERF_GAP_BUCKETS, PERF_LOG_VERSION, PROBLEM_RULES, Parent, type PassKind, type PassRecord, type PerfCause, type PerfChange, type PerfChangeEntry, type PerfCollectorLine, type PerfCounts, type PerfDisplay, type PerfEventLine, type PerfEventName, type PerfGapLine, type PerfHeaderLine, type PerfLogLine, type PerfLongFrameLine, type PerfMarkLine, type PerfMeasured, type PerfSampleLine, type PerfSpikeLine, type PerfStartupLine, type PerfTelemetryLine, type PerfThermalLine, types_d_exports$11 as Platform, type PlatformProvider, Pointer, PointerOver, type PoolTrim, type PoolTrimDetail, Pressable, Pressed, type ProblemEventDetail, type ProblemRule, types_d_exports$12 as Renderer, SHADER_WHATS, SaveUnreadableError, types_d_exports$13 as Scenes, ScrollState, type ShaderEventDetail, type ShaderWhat, Shape, Sprite, type SpriteBlend, Swipeable, Tappable, Text, type TextFormat, types_d_exports$14 as TextTypes, types_d_exports$15 as Time, Touchable, Traceable, Traced, Transform, Tree, types_d_exports$16 as Ui, type WarmDetail, types_d_exports$17 as World, animPlugin, assetsPlugin, audioPlugin, bind, clockPlugin, component, createApp, createPlugin, cubicBezier, debugPlugin, defineAnimation, defineBundles, defineComponent, defineEffect, defineEmitter, defineFeature, defineFilter, defineGame, defineMotion, defineScene, defineStyle, defineTextStyles, defineTokens, effectsPlugin, exit, external, flattenFeatures, flowPlugin, frameAt, frames, guide, haptic, hint, i18nPlugin, inputPlugin, label, lifecyclePlugin, load, mark, messageArgument, messageDuration, modelPlugin, music, mut, parallel, platformPlugin, play, popup, projection, ready, rendererPlugin, resolve, resource, scenesPlugin, schedule, screen, sequence, set, sfx, slot, spawn, spawned, sprite, stagger, stopSfx, system, tag, textPlugin, timePlugin, to, tr, tween, type, uiPlugin, use, wait, worldPlugin };
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as sfx, $t as flowPlugin, A as types_exports$7, At as Glow, B as defineMotion, Bt as Animation, C as types_exports$14, Ct as messageDuration, D as types_exports$10, Dt as Blur, E as types_exports$11, Et as Alpha, F as types_exports$2, Ft as audioPlugin, G as external, Gt as MONITOR_SECTIONS, H as platformPlugin, Ht as rendererPlugin, I as types_exports$1, It as scenesPlugin, J as mark, Jt as PERF_LOG_VERSION, K as frames, Kt as PERF_CAUSES, L as types_exports, Lt as assetsPlugin, M as types_exports$5, Mt as Outline, N as types_exports$4, Nt as defineFilter, O as types_exports$9, Ot as ColorMatrix, P as types_exports$3, Pt as debugPlugin, Q as set, Qt as worldPlugin, R as uiPlugin, Rt as animPlugin, S as types_exports$15, St as messageArgument, T as types_exports$12, Tt as Emitter, U as HAPTIC_KINDS, Ut as PASS_KINDS, V as textPlugin, Vt as Frames, W as defineAnimation, Wt as frameAt, X as play, Xt as SHADER_WHATS, Y as parallel, Yt as PROBLEM_RULES, Z as sequence, Zt as DEMAND_REASONS, _ as defineBundles, _t as Tappable, a as cubicBezier, an as modelPlugin, at as use, b as types_exports$17, bt as Traced, c as hint, cn as timePlugin, ct as Draggable, d as defineScene, dt as Hovered, et as spawn, f as tr, ft as Pointer, g as music, gt as Swipeable, h as defineEmitter, ht as Pressed, i as screen, in as type, it as tween, j as types_exports$6, jt as Noise, k as types_exports$8, kt as Displacement, l as schedule, ln as clockPlugin, lt as DropTarget, m as flattenFeatures, mt as Pressable, n as createPlugin, nn as slot, nt as stagger, o as defineEffect, on as SaveUnreadableError, ot as wait, p as defineFeature, pt as PointerOver, q as haptic, qt as PERF_GAP_BUCKETS, r as defineGame, rn as to, rt as stopSfx, s as guide, sn as lifecyclePlugin, st as inputPlugin, t as createApp, tn as exit, tt as spawned, u as projection, ut as Held, v as load, vt as Touchable, w as types_exports$13, wt as effectsPlugin, x as types_exports$16, xt as i18nPlugin, y as ready, yt as Traceable, z as resolve, zt as AnimPlayer } from "./src-B4JjsuEy.mjs";
1
+ import { $ as sfx, $t as flowPlugin, A as types_exports$7, At as Glow, B as defineMotion, Bt as Animation, C as types_exports$14, Ct as messageDuration, D as types_exports$10, Dt as Blur, E as types_exports$11, Et as Alpha, F as types_exports$2, Ft as audioPlugin, G as external, Gt as MONITOR_SECTIONS, H as platformPlugin, Ht as rendererPlugin, I as types_exports$1, It as scenesPlugin, J as mark, Jt as PERF_LOG_VERSION, K as frames, Kt as PERF_CAUSES, L as types_exports, Lt as assetsPlugin, M as types_exports$5, Mt as Outline, N as types_exports$4, Nt as defineFilter, O as types_exports$9, Ot as ColorMatrix, P as types_exports$3, Pt as debugPlugin, Q as set, Qt as worldPlugin, R as uiPlugin, Rt as animPlugin, S as types_exports$15, St as messageArgument, T as types_exports$12, Tt as Emitter, U as HAPTIC_KINDS, Ut as PASS_KINDS, V as textPlugin, Vt as Frames, W as defineAnimation, Wt as frameAt, X as play, Xt as SHADER_WHATS, Y as parallel, Yt as PROBLEM_RULES, Z as sequence, Zt as DEMAND_REASONS, _ as defineBundles, _t as Tappable, a as cubicBezier, an as modelPlugin, at as use, b as types_exports$17, bt as Traced, c as hint, cn as timePlugin, ct as Draggable, d as defineScene, dt as Hovered, et as spawn, f as tr, ft as Pointer, g as music, gt as Swipeable, h as defineEmitter, ht as Pressed, i as screen, in as type, it as tween, j as types_exports$6, jt as Noise, k as types_exports$8, kt as Displacement, l as schedule, ln as clockPlugin, lt as DropTarget, m as flattenFeatures, mt as Pressable, n as createPlugin, nn as slot, nt as stagger, o as defineEffect, on as SaveUnreadableError, ot as wait, p as defineFeature, pt as PointerOver, q as haptic, qt as PERF_GAP_BUCKETS, r as defineGame, rn as to, rt as stopSfx, s as guide, sn as lifecyclePlugin, st as inputPlugin, t as createApp, tn as exit, tt as spawned, u as projection, ut as Held, v as load, vt as Touchable, w as types_exports$13, wt as effectsPlugin, x as types_exports$16, xt as i18nPlugin, y as ready, yt as Traceable, z as resolve, zt as AnimPlayer } from "./src-5qdNjLKN.mjs";
2
2
  import { $ as component, H as Shape, I as Display, J as Exiting, L as HudPart, Q as Tree, R as NineSlice, S as bind, T as label, U as Sprite, V as Parent, W as Transform, X as Now, Y as Layer, Z as Order, b as Countdown, d as LocalWrite, et as mut, g as popup, it as tag, l as DebugTrigger, nt as system, p as ScrollState, q as sprite, s as Box, tt as resource, v as defineTokens, w as defineTextStyles, x as Text, y as defineStyle, z as Overlay } from "./visual-ClVRMUAi.mjs";
3
3
  import { t as defineComponent } from "./component-DGg5DqKK.mjs";
4
4
  export { Alpha, types_exports as Anim, AnimPlayer, Animation, types_exports$1 as Assets, types_exports$2 as Audio, Blur, Box, types_exports$3 as Clock, ColorMatrix, Countdown, DEMAND_REASONS, types_exports$4 as Debug, DebugTrigger, Displacement, Display, Draggable, DropTarget, types_exports$5 as Effects, Emitter, Exiting, types_exports$6 as Flow, Frames, Glow, HAPTIC_KINDS, Held, Hovered, HudPart, types_exports$7 as I18n, types_exports$8 as Input, Layer, types_exports$9 as Lifecycle, LocalWrite, MONITOR_SECTIONS, types_exports$10 as Model, NineSlice, Noise, Now, Order, Outline, Overlay, PASS_KINDS, PERF_CAUSES, PERF_GAP_BUCKETS, PERF_LOG_VERSION, PROBLEM_RULES, Parent, types_exports$11 as Platform, Pointer, PointerOver, Pressable, Pressed, types_exports$12 as Renderer, SHADER_WHATS, SaveUnreadableError, types_exports$13 as Scenes, ScrollState, Shape, Sprite, Swipeable, Tappable, Text, types_exports$14 as TextTypes, types_exports$15 as Time, Touchable, Traceable, Traced, Transform, Tree, types_exports$16 as Ui, types_exports$17 as World, animPlugin, assetsPlugin, audioPlugin, bind, clockPlugin, component, createApp, createPlugin, cubicBezier, debugPlugin, defineAnimation, defineBundles, defineComponent, defineEffect, defineEmitter, defineFeature, defineFilter, defineGame, defineMotion, defineScene, defineStyle, defineTextStyles, defineTokens, effectsPlugin, exit, external, flattenFeatures, flowPlugin, frameAt, frames, guide, haptic, hint, i18nPlugin, inputPlugin, label, lifecyclePlugin, load, mark, messageArgument, messageDuration, modelPlugin, music, mut, parallel, platformPlugin, play, popup, projection, ready, rendererPlugin, resolve, resource, scenesPlugin, schedule, screen, sequence, set, sfx, slot, spawn, spawned, sprite, stagger, stopSfx, system, tag, textPlugin, timePlugin, to, tr, tween, type, uiPlugin, use, wait, worldPlugin };
@@ -1,5 +1,5 @@
1
1
  import { Ir as Snapshot, Un as Position, Y as Api$2, ct as RenderStats, dn as ComponentSchema, gn as FrameDiff, h as Usage, hn as EntitySnapshot, in as Explained, ir as FlowGraph, it as Under, nn as Api$1, or as JournalEntry, t as Api$3, wr as Api } from "./types-D27I4Q7b.mjs";
2
- import { a as AudioApi, g as EffectsStats, h as EffectsApi, r as UiApi, u as SoundEntry } from "./types-DjQkfbfZ.mjs";
2
+ import { a as AudioApi, g as EffectsStats, h as EffectsApi, r as UiApi, u as SoundEntry } from "./types-DHamyZq1.mjs";
3
3
  import { c as UiNode } from "./types-BAp9HdR4.mjs";
4
4
  import { t as HeadlessApp } from "./headless-f31cEZ7f.mjs";
5
5
  import { a as InputOf, c as Source, i as InputArguments, l as WatchApp, o as InputSchema, t as CheatEntry, u as WatchInput } from "./types-CgE5L_7f.mjs";
@@ -21,7 +21,7 @@ import { a as readTarget, i as isProjectionTarget, l as inputSchema, n as isTain
21
21
  const reducedMotionCommand = defineCommand({
22
22
  id: "game.reducedMotion",
23
23
  title: "Reduced motion",
24
- input: { on: "boolean" },
24
+ input: inputSchema(["on", "boolean"]),
25
25
  effect: "cosmetic",
26
26
  run: (app, { on }) => {
27
27
  if ((typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) && (typeof __MOKU_GAME_AGENT__ === "undefined" || !__MOKU_GAME_AGENT__)) throw controlRefused();
@@ -32876,36 +32876,91 @@ function spriteOf(cctx, step) {
32876
32876
  function startTween(cctx, step) {
32877
32877
  const entity = targetOf$2(cctx, step);
32878
32878
  if (entity === void 0) return void 0;
32879
- return cctx.rt.start(entity, step.component, tweenTargetOf(cctx, entity, step), {
32879
+ return cctx.rt.start(entity, step.component, inSpace(cctx, entity, step, step.to), trackOptionsOf$1(cctx, entity, step));
32880
+ }
32881
+ /**
32882
+ * The options a `tween` step starts its track with. The keyframe segments and the repeat are
32883
+ * passed only when the step has them, the segment targets turned like `to` in the root space.
32884
+ *
32885
+ * @param cctx - What the cursor carries down the tree.
32886
+ * @param entity - The entity the step moves.
32887
+ * @param step - The tween step.
32888
+ * @returns The track options.
32889
+ */
32890
+ function trackOptionsOf$1(cctx, entity, step) {
32891
+ const options = {
32880
32892
  ms: step.ms,
32881
32893
  ease: step.ease,
32882
32894
  delayMs: step.delayMs,
32883
32895
  additive: step.additive,
32884
32896
  reduced: step.reduced
32885
- });
32897
+ };
32898
+ const segments = step.segments;
32899
+ if (segments !== void 0) options.segments = step.space === "root" ? segments.map((segment) => ({
32900
+ ...segment,
32901
+ to: inSpace(cctx, entity, step, segment.to)
32902
+ })) : segments;
32903
+ if (step.repeat !== void 0) options.repeat = step.repeat;
32904
+ return options;
32886
32905
  }
32887
32906
  /**
32888
- * The target fields of a `tween` step for one entity: as written in the local space, turned into
32889
- * the space of the entity's parent in the root space.
32907
+ * Target fields of a `tween` step for one entity: as written in the local space, turned into the
32908
+ * space of the entity's parent in the root space.
32890
32909
  *
32891
32910
  * @param cctx - What the cursor carries down the tree.
32892
32911
  * @param entity - The entity the step moves.
32893
32912
  * @param step - The tween step.
32913
+ * @param fields - Its `to`, the target of one of its segments, or where it lands.
32894
32914
  * @returns The fields the track or the write aims at.
32895
32915
  */
32896
- function tweenTargetOf(cctx, entity, step) {
32897
- if (step.space === "root") return cctx.rt.toLocal(entity, step.component, step.to);
32898
- return { ...step.to };
32916
+ function inSpace(cctx, entity, step, fields) {
32917
+ if (step.space === "root") return cctx.rt.toLocal(entity, step.component, fields);
32918
+ return { ...fields };
32919
+ }
32920
+ /**
32921
+ * The last value each field reaches over a list of segments.
32922
+ *
32923
+ * @param segments - The segments of a step.
32924
+ * @returns One value per field any segment names.
32925
+ * @example
32926
+ * ```ts
32927
+ * lastValues([{ at: 0.5, to: { y: 10, rotation: 2 } }, { at: 1, to: { y: 0 } }]); // { y: 0, rotation: 2 }
32928
+ * ```
32929
+ */
32930
+ function lastValues$1(segments) {
32931
+ const values = {};
32932
+ for (const segment of segments) Object.assign(values, segment.to);
32933
+ return values;
32934
+ }
32935
+ /**
32936
+ * Where a `tween` step that never started lands when it is finished: its target, and for a
32937
+ * keyframe walk every field its segments name too, at the last value they give it. A step that
32938
+ * repeats forever lands on its start pose, where it already stands, and an additive walk names
32939
+ * offsets that end with it: neither writes anything.
32940
+ *
32941
+ * @param step - The tween step.
32942
+ * @returns The fields to write, or `undefined` when the step writes nothing.
32943
+ */
32944
+ function landingOf(step) {
32945
+ if (step.repeat === "forever") return void 0;
32946
+ if (step.segments === void 0) return step.to;
32947
+ if (step.additive) return void 0;
32948
+ return {
32949
+ ...lastValues$1(step.segments),
32950
+ ...step.to
32951
+ };
32899
32952
  }
32900
32953
  /**
32901
- * Writes the target of a `tween` step that never started, in the space the step names.
32954
+ * Writes where a `tween` step that never started lands, in the space the step names.
32902
32955
  *
32903
32956
  * @param cctx - What the cursor carries down the tree.
32904
32957
  * @param step - The tween step.
32905
32958
  */
32906
32959
  function writeTween(cctx, step) {
32960
+ const landing = landingOf(step);
32961
+ if (landing === void 0) return;
32907
32962
  const entity = targetOf$2(cctx, step);
32908
- if (entity !== void 0) cctx.rt.write(entity, step.component, tweenTargetOf(cctx, entity, step));
32963
+ if (entity !== void 0) cctx.rt.write(entity, step.component, inSpace(cctx, entity, step, landing));
32909
32964
  }
32910
32965
  /**
32911
32966
  * Writes the patch of a `set` step.
@@ -33126,7 +33181,9 @@ function advanceCursor(cctx, cursor, deltaMs) {
33126
33181
  /**
33127
33182
  * Ends one step at its own end: a track writes its exact target, a step that never started writes
33128
33183
  * it at once, the marks are jumped in tree order, no effect is fired and nothing is spawned: the
33129
- * timeline despawns its entities as it ends, so a skipped spawn would only flash.
33184
+ * timeline despawns its entities as it ends, so a skipped spawn would only flash. A track that
33185
+ * repeats forever has no end: it lands on its start pose, and one that never started writes
33186
+ * nothing.
33130
33187
  *
33131
33188
  * @param cctx - What the cursor carries down the tree.
33132
33189
  * @param cursor - The cursor of the step.
@@ -34447,7 +34504,8 @@ function repeatsOf(repeat) {
34447
34504
  * keyframe segments walks them over `ms` and books every field they name; one with `repeat`
34448
34505
  * walks again when a run ends. With reduced motion on, every track but a loop takes 0 ms, its
34449
34506
  * delay and its repeats dropped: it lands on its target at its first frame step. A track started
34450
- * with `reduced: "keep"` keeps its length, its delay and its repeats.
34507
+ * with `reduced: "keep"` keeps its length, its delay and its repeats. A timeline's loop under
34508
+ * reduced motion does not play at all: it is born ended, writes nothing, and its step goes on.
34451
34509
  *
34452
34510
  * @param actx - Domain context of the anim plugin.
34453
34511
  * @param entity - The entity to animate.
@@ -34462,9 +34520,11 @@ function startTrack$1(actx, entity, component, to, options, muted, driven = fals
34462
34520
  const target = targetOf$1(to, options.segments);
34463
34521
  const repeats = repeatsOf(options.repeat);
34464
34522
  const keep = options.reduced === "keep";
34465
- const instant = actx.state.reducedMotion && !keep && repeats !== Number.POSITIVE_INFINITY;
34523
+ const cut = actx.state.reducedMotion && !keep;
34524
+ const instant = cut && repeats !== Number.POSITIVE_INFINITY;
34466
34525
  const keys = keysOf(entity, component.componentName, target);
34467
34526
  const names = Object.keys(target);
34527
+ const bornEnded = names.length === 0 || cut && driven && repeats === Number.POSITIVE_INFINITY;
34468
34528
  const track = {
34469
34529
  id: actx.state.nextId,
34470
34530
  entity,
@@ -34488,7 +34548,7 @@ function startTrack$1(actx, entity, component, to, options, muted, driven = fals
34488
34548
  additive: options.additive === true,
34489
34549
  driven,
34490
34550
  bornFrame: actx.state.frame,
34491
- ended: names.length === 0,
34551
+ ended: bornEnded,
34492
34552
  segments: options.segments,
34493
34553
  repeatsLeft: instant ? 0 : repeats,
34494
34554
  held: false,
@@ -34593,7 +34653,7 @@ function advanceTrack(actx, track, deltaMs) {
34593
34653
  const run = clock[1] ?? 0;
34594
34654
  const fraction = track.ms <= 0 ? 1 : Math.min(run / track.ms, 1);
34595
34655
  const final = fraction >= 1 && (track.repeatsLeft === 0 || track.ms <= 0);
34596
- if (!final && !track.driven && track.repeatsLeft === Number.POSITIVE_INFINITY && actx.state.loopsHeld?.(track.entity, track.reveals) === true) return 0;
34656
+ if (isHeldLoop(actx, track, final)) return 0;
34597
34657
  clock[1] = fraction;
34598
34658
  writeFields(actx, track, final);
34599
34659
  if (!final) return 0;
@@ -34601,15 +34661,29 @@ function advanceTrack(actx, track, deltaMs) {
34601
34661
  return Math.max(0, run - track.ms);
34602
34662
  }
34603
34663
  /**
34604
- * Writes the exact target of a track and ends it. Muted fields are left alone here too.
34664
+ * Tells whether an endless loop writes nothing this frame because the player cannot see it.
34665
+ *
34666
+ * @param actx - Domain context of the anim plugin.
34667
+ * @param track - The track that advances.
34668
+ * @param final - Whether this step lands the track.
34669
+ * @returns True when the loop is held.
34670
+ */
34671
+ function isHeldLoop(actx, track, final) {
34672
+ return !final && track.repeatsLeft === Number.POSITIVE_INFINITY && actx.state.loopsHeld?.(track.entity, track.reveals) === true;
34673
+ }
34674
+ /**
34675
+ * Writes the exact target of a track and ends it. Muted fields are left alone here too. A
34676
+ * timeline's loop has no end to land on: it lands on its first key, the pose its walk starts
34677
+ * from, which is where a cut or a fast walk leaves it (F75).
34605
34678
  *
34606
34679
  * @param actx - Domain context of the anim plugin.
34607
34680
  * @param track - The track to finish.
34608
34681
  */
34609
34682
  function finishTrack(actx, track) {
34610
34683
  if (track.ended) return;
34611
- track.clock[1] = 1;
34612
- writeFields(actx, track, true);
34684
+ const final = !track.driven || track.repeatsLeft !== Number.POSITIVE_INFINITY;
34685
+ track.clock[1] = final ? 1 : 0;
34686
+ writeFields(actx, track, final);
34613
34687
  endTrack(actx, track);
34614
34688
  }
34615
34689
  /**
@@ -49843,6 +49917,102 @@ function createHandlers$1(ctx) {
49843
49917
  } };
49844
49918
  }
49845
49919
  //#endregion
49920
+ //#region src/plugins/anim/timeline/checks.ts
49921
+ /**
49922
+ * Checks the keyframe segments of a `tween` step: at least one, every `at` in 0..1, each after the
49923
+ * one before, and the last at 1, because `to` is where the walk lands.
49924
+ *
49925
+ * @param component - Name of the component the step drives, for the error.
49926
+ * @param segments - The segments the author gave.
49927
+ * @throws {Error} When the list is empty, an `at` is out of range or out of order, or the last
49928
+ * segment ends before 1.
49929
+ * @example
49930
+ * ```ts
49931
+ * checkSegments("Transform", [{ at: 0.5 }, { at: 1 }]); // returns; [{ at: 0.5 }] throws
49932
+ * ```
49933
+ */
49934
+ function checkSegments(component, segments) {
49935
+ if (segments.length === 0) throw new Error(`[game] A tween of ${component} has no segments.\n Give it at least one segment, the last one at 1.`);
49936
+ let previous = Number.NEGATIVE_INFINITY;
49937
+ for (const { at } of segments) {
49938
+ if (Number.isNaN(at) || at < 0 || at > 1) throw new Error(`[game] A tween of ${component} has a segment at ${at}.\n Keep every at between 0 and 1.`);
49939
+ if (at <= previous) throw new Error(`[game] A tween of ${component} has its segments out of order at ${at}.\n Sort the segments by at, each one after the one before.`);
49940
+ previous = at;
49941
+ }
49942
+ if (previous !== 1) throw new Error(`[game] A tween of ${component} ends its last segment at ${previous}.\n End the last segment at 1: to is where the walk lands.`);
49943
+ }
49944
+ /**
49945
+ * Checks the space of a keyframe walk: an additive one names offsets, and the root space would
49946
+ * turn each offset as if it were a root position, so a chest under a slot at root y 500 would jump
49947
+ * by about 500 at its first segment. An additive walk stays in the local space.
49948
+ *
49949
+ * @param component - Name of the component the step drives, for the error.
49950
+ * @param additive - Whether the walk adds offsets.
49951
+ * @param space - The space the step names.
49952
+ * @throws {Error} When an additive walk names the root space.
49953
+ * @example
49954
+ * ```ts
49955
+ * checkWalkSpace("Transform", true, "local"); // returns; (…, true, "root") throws
49956
+ * ```
49957
+ */
49958
+ function checkWalkSpace(component, additive, space) {
49959
+ if (!additive || space === "local") return;
49960
+ throw new Error(`[game] An additive tween of ${component} walks segments in the root space.\n Use space "local" for an additive walk.`);
49961
+ }
49962
+ /**
49963
+ * Checks the repeat of a `tween` step: a whole number of extra runs, 0 or more, or `"forever"`.
49964
+ *
49965
+ * @param component - Name of the component the step drives, for the error.
49966
+ * @param repeat - The repeat the author gave.
49967
+ * @throws {Error} When the repeat is negative, not whole or not a number.
49968
+ * @example
49969
+ * ```ts
49970
+ * checkRepeat("Transform", "forever"); // returns; checkRepeat("Transform", 1.5) throws
49971
+ * ```
49972
+ */
49973
+ function checkRepeat(component, repeat) {
49974
+ if (repeat === "forever" || Number.isInteger(repeat) && repeat >= 0) return;
49975
+ throw new Error(`[game] A tween of ${component} has repeat ${repeat}.\n Give repeat a whole number of extra runs, 0 or more, or "forever".`);
49976
+ }
49977
+ /**
49978
+ * Tells whether a step never ends by itself: a `tween` that repeats forever, a `parallel` or a
49979
+ * `sequence` that holds one, and a `use` of one. Only a cancel or a finish ends it.
49980
+ *
49981
+ * @param step - The step to look into.
49982
+ * @returns True when the step never ends by itself.
49983
+ * @example
49984
+ * ```ts
49985
+ * neverEnds(wait(100)); // false
49986
+ * neverEnds(tween(1_048_576, Transform, { y: 0 }, { ms: 2400, repeat: "forever" })); // true
49987
+ * ```
49988
+ */
49989
+ function neverEnds(step) {
49990
+ switch (step.kind) {
49991
+ case "tween": return step.repeat === "forever";
49992
+ case "sequence":
49993
+ case "parallel": return step.steps.some((child) => neverEnds(child));
49994
+ case "use": return neverEnds(step.step);
49995
+ default: return false;
49996
+ }
49997
+ }
49998
+ /**
49999
+ * Checks the children of a `sequence`: a child that never ends may only be the last one, since
50000
+ * the children after it would never play. A `parallel` takes one anywhere.
50001
+ *
50002
+ * @param steps - The children, in order.
50003
+ * @throws {Error} When a child that never ends has another child after it.
50004
+ * @example
50005
+ * ```ts
50006
+ * checkSequence([wait(100), mark("landed")]); // returns; a forever tween before the mark throws
50007
+ * ```
50008
+ */
50009
+ function checkSequence(steps) {
50010
+ for (let index = 0; index < steps.length - 1; index++) {
50011
+ const step = steps[index];
50012
+ if (step !== void 0 && neverEnds(step)) throw new Error(`[game] Step ${index + 1} of a sequence never ends, so the steps after it would never play.\n Put the forever step last, or run it in a parallel.`);
50013
+ }
50014
+ }
50015
+ //#endregion
49846
50016
  //#region src/plugins/anim/timeline/steps.ts
49847
50017
  /**
49848
50018
  * Every haptic tick `haptic(kind)` takes, in one frozen list: the three impacts, the selection
@@ -49943,10 +50113,13 @@ function slotsJson(slots) {
49943
50113
  }
49944
50114
  /**
49945
50115
  * Runs the steps one after another. The remainder past a step's end reaches the next step in the
49946
- * same frame, so the tree takes exactly the sum of its durations.
50116
+ * same frame, so the tree takes exactly the sum of its durations. A step that never ends (a
50117
+ * `tween` with `repeat: "forever"`, or a `parallel`, `sequence` or `use` that holds one) may only
50118
+ * come last: the steps after it would never play.
49947
50119
  *
49948
50120
  * @param steps - The steps, in order.
49949
50121
  * @returns The sequence step.
50122
+ * @throws {Error} When a step that never ends has another step after it.
49950
50123
  * @example
49951
50124
  * ```ts
49952
50125
  * sequence(wait(100), mark("done"));
@@ -49954,6 +50127,7 @@ function slotsJson(slots) {
49954
50127
  * ```
49955
50128
  */
49956
50129
  function sequence(...steps) {
50130
+ checkSequence(steps);
49957
50131
  return Object.freeze({
49958
50132
  kind: "sequence",
49959
50133
  steps: Object.freeze([...steps])
@@ -50026,6 +50200,28 @@ function mark(name) {
50026
50200
  });
50027
50201
  }
50028
50202
  /**
50203
+ * Copies one keyframe segment into frozen plain data: its `at`, its ease when it names one and
50204
+ * its numeric target fields.
50205
+ *
50206
+ * @param segment - The segment the author gave.
50207
+ * @param segment.at - Where it ends, as a fraction of the track's `ms`.
50208
+ * @param segment.ease - Its curve; the step's `ease` when left out.
50209
+ * @param segment.to - The fields it reaches.
50210
+ * @returns A frozen copy.
50211
+ * @example
50212
+ * ```ts
50213
+ * frozenSegment({ at: 1, to: { y: 0, texture: "a" } }); // { at: 1, to: { y: 0 } }
50214
+ * ```
50215
+ */
50216
+ function frozenSegment(segment) {
50217
+ const frozen = {
50218
+ at: segment.at,
50219
+ to: Object.freeze(numbersOf(segment.to))
50220
+ };
50221
+ if (segment.ease !== void 0) frozen.ease = segment.ease;
50222
+ return Object.freeze(frozen);
50223
+ }
50224
+ /**
50029
50225
  * Moves the numeric fields of one component of one target to an exact target value. The component
50030
50226
  * is any handle `world.ecs` takes, so the kit's `Sprite` and `NineSlice` of `defineGame` pass as
50031
50227
  * they are.
@@ -50033,10 +50229,12 @@ function mark(name) {
50033
50229
  * By default the fields are local: they are written into the target's own `Transform`. With
50034
50230
  * `space: "root"` the `x`, `y`, `rotation` and `scale` of a `Transform` are a root pose, the space
50035
50231
  * `at()` answers in, and are turned into the space of the target's `Parent` when the track starts.
50036
- * That aims a hosted view, such as a board item inside the board slot, at another element. A
50037
- * field the step does not name stays where it is, so under a turned parent name `x` and `y`
50038
- * together. A target without a parent, and a component other than `Transform`, move as in the
50039
- * local space.
50232
+ * That aims a hosted view at another element: an order's board item inside the scaled board slot
50233
+ * flies onto the order card with `{ x: at(card).x, y: at(card).y, scale: at(card).scale }` in the
50234
+ * root space, covers the card at the card's size, and its `Transform` stays slot-local. A field
50235
+ * the step does not name stays where it is, so under a turned parent name `x` and `y` together. A
50236
+ * target without a parent, and a component other than `Transform`, move as in the local space. An
50237
+ * additive walk names offsets, so it stays in the local space.
50040
50238
  *
50041
50239
  * @param target - The projection key or the entity to animate.
50042
50240
  * @param component - The component to animate.
@@ -50049,30 +50247,34 @@ function mark(name) {
50049
50247
  * @param options.space - `"root"` reads the `Transform` fields as a root pose; `"local"` when
50050
50248
  * omitted.
50051
50249
  * @param options.reduced - `"keep"` plays the track at its length under reduced motion, a fade
50052
- * for one; `"cut"` (the default) lands it at once.
50053
- * @returns The tween step.
50054
- * @example
50055
- * ```ts
50056
- * // An order is delivered: the board item flies out of the scaled board slot onto the order card.
50057
- * const deliverFly = defineAnimation("orders.deliverFly", {
50058
- * slots: { item: type<Target>(), card: type<Target>() },
50059
- * build: ({ item, card }, { at }) =>
50060
- * tween(item, Transform, { x: at(card).x, y: at(card).y, scale: at(card).scale }, {
50061
- * ms: 400, ease: "inCubic", space: "root"
50062
- * })
50063
- * });
50064
- * app.anim.play(deliverFly, {
50065
- * item: { projection: "board.items", key: "i1" },
50066
- * card: { projection: "hud", key: "card0" }
50067
- * });
50068
- * // 400 ms later the item covers the card at the card's size; its Transform stays slot-local
50250
+ * for one; `"cut"` (the default) lands it at once, and a `"forever"` track does not play.
50251
+ * @param options.segments - Keyframe segments `{ at, ease?, to }` walked one after another over
50252
+ * `ms` on one clock; a field a segment does not name holds, `to` is where the last one ends. An
50253
+ * additive walk names offsets. The same as `TweenOptions` of `ViewHandle.tween`.
50254
+ * @param options.repeat - How many more times the walk runs, or `"forever"`: the step never ends
50255
+ * by itself, so it comes last in a `sequence`.
50256
+ * @returns The tween step; `segments` and `repeat` are in it only when given.
50257
+ * @throws {Error} When the segments are empty, an `at` is outside 0..1 or out of order, the last
50258
+ * segment ends before 1, an additive walk names the root space, or the repeat is not a whole
50259
+ * number of 0 or more.
50260
+ * @example
50261
+ * ```ts
50262
+ * // A ready chest (entity 1048576) hops 14 up and back every 2.4 s, as offsets over its pose.
50263
+ * const chestBounce = defineAnimation("board.chestBounce", { slots: { chest: type<Target>() },
50264
+ * build: ({ chest }) => tween(chest, Transform, { y: 0, scale: 0 }, { ms: 2400, additive: true,
50265
+ * repeat: "forever", segments: [{ at: 0.08, ease: "out", to: { y: -14, scale: 0.06 } },
50266
+ * { at: 0.22, ease: "in", to: { y: 0, scale: 0 } }, { at: 1, to: { y: 0, scale: 0 } }] }) });
50267
+ * const bounce = res(AnimPlayer).play(chestBounce, { chest: 1_048_576 });
50268
+ * // 192 ms later the chest stands 14 higher, its scale 0.06 larger; it bounces until cancelled
50269
+ * bounce.cancel(); // it stands on its own pose again
50069
50270
  * ```
50070
50271
  */
50071
50272
  function tween(target, component, to, options) {
50072
- return Object.freeze({
50273
+ const erased = asComponent(component);
50274
+ const step = {
50073
50275
  kind: "tween",
50074
50276
  target,
50075
- component: asComponent(component),
50277
+ component: erased,
50076
50278
  to: Object.freeze(numbersOf(to)),
50077
50279
  ms: options.ms,
50078
50280
  ease: options.ease ?? "out",
@@ -50080,7 +50282,17 @@ function tween(target, component, to, options) {
50080
50282
  additive: options.additive === true,
50081
50283
  space: options.space ?? "local",
50082
50284
  reduced: options.reduced ?? "cut"
50083
- });
50285
+ };
50286
+ if (options.segments !== void 0) {
50287
+ checkSegments(erased.componentName, options.segments);
50288
+ checkWalkSpace(erased.componentName, step.additive, step.space);
50289
+ step.segments = Object.freeze(options.segments.map((segment) => frozenSegment(segment)));
50290
+ }
50291
+ if (options.repeat !== void 0) {
50292
+ checkRepeat(erased.componentName, options.repeat);
50293
+ step.repeat = options.repeat;
50294
+ }
50295
+ return Object.freeze(step);
50084
50296
  }
50085
50297
  /**
50086
50298
  * Writes a component patch at once. Any field may be written, not only the numeric ones. The
@@ -1,6 +1,6 @@
1
1
  import { $n as FeaturePlugin, Dr as Json, dr as Descriptor, gr as NodeFx, hr as Hint, sr as NodeTable, tr as AnyFlow, xr as FakeClock, yr as ClockSource } from "./types-D27I4Q7b.mjs";
2
- import { t as createApp } from "./index-B2GejbLf.mjs";
3
- import { b as saveOf, v as WithGameState, y as memory } from "./types-BWRJcZ3n.mjs";
2
+ import { t as createApp } from "./index-DMeqKGSu.mjs";
3
+ import { b as saveOf, v as WithGameState, y as memory } from "./types-DmU_qlvA.mjs";
4
4
  import { a as createHeadless, i as ReproResult, n as HeadlessGame, o as runRepro, r as Repro, s as stepFrames, t as HeadlessApp } from "./headless-f31cEZ7f.mjs";
5
5
  import { AnyPluginInstance } from "@moku-labs/core";
6
6
 
package/dist/testing.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { en as refuseStart, in as type, m as flattenFeatures, t as createApp, tn as exit } from "./src-B4JjsuEy.mjs";
1
+ import { en as refuseStart, in as type, m as flattenFeatures, t as createApp, tn as exit } from "./src-5qdNjLKN.mjs";
2
2
  import { n as saveOf, t as memory } from "./memory-CWlqhS7l.mjs";
3
- import { t as fakeClock } from "./fake-6mazmHK3.mjs";
3
+ import { n as fakeClock } from "./fake-hnJcJeYf.mjs";
4
4
  import { i as stepFrames, r as runRepro, t as createHeadless } from "./headless-Bxxl_mQX.mjs";
5
5
  //#region src/testing/fake-fx.ts
6
6
  /** The node path a refused `fx.start` names when the test gives none. */
@@ -207,10 +207,13 @@ type StepMotion = MotionHandle & {
207
207
  declare const HAPTIC_KINDS: readonly ["light", "medium", "heavy", "selection", "success", "warning", "error"];
208
208
  /**
209
209
  * Runs the steps one after another. The remainder past a step's end reaches the next step in the
210
- * same frame, so the tree takes exactly the sum of its durations.
210
+ * same frame, so the tree takes exactly the sum of its durations. A step that never ends (a
211
+ * `tween` with `repeat: "forever"`, or a `parallel`, `sequence` or `use` that holds one) may only
212
+ * come last: the steps after it would never play.
211
213
  *
212
214
  * @param steps - The steps, in order.
213
215
  * @returns The sequence step.
216
+ * @throws {Error} When a step that never ends has another step after it.
214
217
  * @example
215
218
  * ```ts
216
219
  * sequence(wait(100), mark("done"));
@@ -275,10 +278,12 @@ declare function mark(name: string): Step;
275
278
  * By default the fields are local: they are written into the target's own `Transform`. With
276
279
  * `space: "root"` the `x`, `y`, `rotation` and `scale` of a `Transform` are a root pose, the space
277
280
  * `at()` answers in, and are turned into the space of the target's `Parent` when the track starts.
278
- * That aims a hosted view, such as a board item inside the board slot, at another element. A
279
- * field the step does not name stays where it is, so under a turned parent name `x` and `y`
280
- * together. A target without a parent, and a component other than `Transform`, move as in the
281
- * local space.
281
+ * That aims a hosted view at another element: an order's board item inside the scaled board slot
282
+ * flies onto the order card with `{ x: at(card).x, y: at(card).y, scale: at(card).scale }` in the
283
+ * root space, covers the card at the card's size, and its `Transform` stays slot-local. A field
284
+ * the step does not name stays where it is, so under a turned parent name `x` and `y` together. A
285
+ * target without a parent, and a component other than `Transform`, move as in the local space. An
286
+ * additive walk names offsets, so it stays in the local space.
282
287
  *
283
288
  * @param target - The projection key or the entity to animate.
284
289
  * @param component - The component to animate.
@@ -291,23 +296,26 @@ declare function mark(name: string): Step;
291
296
  * @param options.space - `"root"` reads the `Transform` fields as a root pose; `"local"` when
292
297
  * omitted.
293
298
  * @param options.reduced - `"keep"` plays the track at its length under reduced motion, a fade
294
- * for one; `"cut"` (the default) lands it at once.
295
- * @returns The tween step.
296
- * @example
297
- * ```ts
298
- * // An order is delivered: the board item flies out of the scaled board slot onto the order card.
299
- * const deliverFly = defineAnimation("orders.deliverFly", {
300
- * slots: { item: type<Target>(), card: type<Target>() },
301
- * build: ({ item, card }, { at }) =>
302
- * tween(item, Transform, { x: at(card).x, y: at(card).y, scale: at(card).scale }, {
303
- * ms: 400, ease: "inCubic", space: "root"
304
- * })
305
- * });
306
- * app.anim.play(deliverFly, {
307
- * item: { projection: "board.items", key: "i1" },
308
- * card: { projection: "hud", key: "card0" }
309
- * });
310
- * // 400 ms later the item covers the card at the card's size; its Transform stays slot-local
299
+ * for one; `"cut"` (the default) lands it at once, and a `"forever"` track does not play.
300
+ * @param options.segments - Keyframe segments `{ at, ease?, to }` walked one after another over
301
+ * `ms` on one clock; a field a segment does not name holds, `to` is where the last one ends. An
302
+ * additive walk names offsets. The same as `TweenOptions` of `ViewHandle.tween`.
303
+ * @param options.repeat - How many more times the walk runs, or `"forever"`: the step never ends
304
+ * by itself, so it comes last in a `sequence`.
305
+ * @returns The tween step; `segments` and `repeat` are in it only when given.
306
+ * @throws {Error} When the segments are empty, an `at` is outside 0..1 or out of order, the last
307
+ * segment ends before 1, an additive walk names the root space, or the repeat is not a whole
308
+ * number of 0 or more.
309
+ * @example
310
+ * ```ts
311
+ * // A ready chest (entity 1048576) hops 14 up and back every 2.4 s, as offsets over its pose.
312
+ * const chestBounce = defineAnimation("board.chestBounce", { slots: { chest: type<Target>() },
313
+ * build: ({ chest }) => tween(chest, Transform, { y: 0, scale: 0 }, { ms: 2400, additive: true,
314
+ * repeat: "forever", segments: [{ at: 0.08, ease: "out", to: { y: -14, scale: 0.06 } },
315
+ * { at: 0.22, ease: "in", to: { y: 0, scale: 0 } }, { at: 1, to: { y: 0, scale: 0 } }] }) });
316
+ * const bounce = res(AnimPlayer).play(chestBounce, { chest: 1_048_576 });
317
+ * // 192 ms later the chest stands 14 higher, its scale 0.06 larger; it bounces until cancelled
318
+ * bounce.cancel(); // it stands on its own pose again
311
319
  * ```
312
320
  */
313
321
  declare function tween<Value extends object>(target: Target, component: ComponentHandle<Value>, to: Partial<NumericFields<Value>>, options: {
@@ -317,6 +325,12 @@ declare function tween<Value extends object>(target: Target, component: Componen
317
325
  additive?: boolean;
318
326
  space?: "local" | "root";
319
327
  reduced?: ReducedMotion;
328
+ segments?: readonly {
329
+ at: number;
330
+ ease?: Ease;
331
+ to: Partial<NumericFields<Value>>;
332
+ }[];
333
+ repeat?: number | "forever";
320
334
  }): Step;
321
335
  /**
322
336
  * Writes a component patch at once. Any field may be written, not only the numeric ones. The
@@ -658,6 +672,16 @@ type Step = {
658
672
  readonly additive: boolean; /** `"root"`: the `Transform` fields are a root pose, turned local when the track starts. */
659
673
  readonly space: "local" | "root"; /** `"keep"`: reduced motion plays the track as written; `"cut"` lands it at once. */
660
674
  readonly reduced: ReducedMotion;
675
+ /**
676
+ * The keyframe segments the track walks over `ms`, frozen; only on a step built with them.
677
+ * In the root space each segment's target is turned local when the track starts, like `to`.
678
+ */
679
+ readonly segments?: readonly TrackSegment[];
680
+ /**
681
+ * The runs after the first one, or `"forever"` for a step that never ends by itself; only on
682
+ * a step built with it.
683
+ */
684
+ readonly repeat?: number | "forever";
661
685
  } | {
662
686
  readonly kind: "set";
663
687
  readonly target: Target;
@@ -1,7 +1,7 @@
1
1
  import { $ as PixiModule, $n as FeaturePlugin, Dr as Json, Lr as SnapshotOf, Mr as PlayerStateProvider, Nr as ProviderCall, Pr as SaveDoc, Tr as Config, X as Config$2, a as Config$3, d as Manifest, lt as Orientation, n as AssetsIo, tr as AnyFlow, ur as TypeTag, xr as FakeClock, yr as ClockSource, zn as Config$1 } from "./types-D27I4Q7b.mjs";
2
- import { o as AudioContextLike, s as Config$4 } from "./types-DjQkfbfZ.mjs";
2
+ import { o as AudioContextLike, s as Config$4 } from "./types-DHamyZq1.mjs";
3
3
  import { R as I18nApi, U as StringTable, W as TextKey } from "./types-BAp9HdR4.mjs";
4
- import { St as effectsPlugin, Tt as audioPlugin, ht as PlatformProvider, i as screen, pt as platformPlugin, t as createApp } from "./index-B2GejbLf.mjs";
4
+ import { St as effectsPlugin, Tt as audioPlugin, ht as PlatformProvider, i as screen, pt as platformPlugin, t as createApp } from "./index-DMeqKGSu.mjs";
5
5
  import { AnyPluginInstance } from "@moku-labs/core";
6
6
 
7
7
  //#region src/plugins/model/store/providers/memory.d.ts
@@ -1,9 +1,9 @@
1
- import { Vr as Api$3, Y as Api, nn as Api$1, wr as Api$2 } from "./types-D27I4Q7b.mjs";
2
- import { P as AnimApi, r as UiApi } from "./types-DjQkfbfZ.mjs";
1
+ import { Vr as Api$3, Y as Api, nn as Api$1, vr as Api$4, wr as Api$2 } from "./types-D27I4Q7b.mjs";
2
+ import { P as AnimApi, r as UiApi } from "./types-DHamyZq1.mjs";
3
3
  import { N as InputApi } from "./types-BAp9HdR4.mjs";
4
4
  import { r as Repro } from "./headless-f31cEZ7f.mjs";
5
5
  import { a as InputOf, r as ControlApp } from "./types-CgE5L_7f.mjs";
6
- import { t as commands } from "./commands-BUfZJUx6.mjs";
6
+ import { t as commands } from "./commands-C8N_rpwt.mjs";
7
7
 
8
8
  //#region src/visual/types.d.ts
9
9
  /** The base commands of the `/control` door, keyed by their short names. */
@@ -32,7 +32,9 @@ type StepCommand = Exclude<keyof Catalogue, "bookmark" | "capture" | "debug">;
32
32
  type VisualStart = Omit<Repro, "route">;
33
33
  /**
34
34
  * One step of a visual test: a `/control` command by its short name with that command's input,
35
- * typed from the catalogue, or a checkpoint by its name.
35
+ * typed from the catalogue, or a checkpoint by its name. A checkpoint lands every motion
36
+ * (`anim.finishAll()`) before its snapshot; with `finish: false` it does not, and the snapshot
37
+ * shows the motions where they stand after the same frames in both legs (F87).
36
38
  *
37
39
  * @example
38
40
  * ```ts
@@ -40,12 +42,15 @@ type VisualStart = Omit<Repro, "route">;
40
42
  * const steps: VisualStep[] = [
41
43
  * { tap: { key: "play" } },
42
44
  * { answer: { intent: "deliver", payload: { orderId: "o1" } } },
43
- * { checkpoint: "open" }
45
+ * { checkpoint: "open" },
46
+ * // The hint that fades out at the end of its animation, caught while it still shows.
47
+ * { checkpoint: "hint", finish: false }
44
48
  * ];
45
49
  * ```
46
50
  */
47
51
  type VisualStep = {
48
52
  checkpoint: string;
53
+ finish?: boolean;
49
54
  } | { [K in StepCommand]: { [P in K]: InputOf<Catalogue[K]["input"]> } }[StepCommand];
50
55
  /**
51
56
  * A visual test as `defineVisualTest` answers it: its name, where it starts, its steps, and
@@ -84,7 +89,8 @@ type VisualApp = ControlApp & {
84
89
  readonly world: Api$1;
85
90
  readonly model: Api$2;
86
91
  readonly input: InputApi;
87
- readonly lifecycle: Api$3;
92
+ readonly lifecycle: Api$3; /** The clock: the pixel leg puts its moment on the page, so both legs see one `clock.now()`. */
93
+ readonly clock: Api$4;
88
94
  };
89
95
  /**
90
96
  * The dev page the browser leg opens: its URL, the viewport width in CSS px (390 by default), the
@@ -254,7 +260,8 @@ type VisualFlags = {
254
260
  /**
255
261
  * Defines a visual test: where it starts, what `game.restore` loads, and its steps. A step is a
256
262
  * `/control` command by its short name with that command's input, or `{ checkpoint: name }`,
257
- * where the runner saves `state.json` and `describe.json` next to the test. `walk` runs fast: a
263
+ * where the runner lands every motion and saves `state.json` and `describe.json` next to the test;
264
+ * `{ checkpoint: name, finish: false }` keeps the motions running for the snapshot. `walk` runs fast: a
258
265
  * popup its last node opens never mounts, so the step before a checkpoint that shows a node's
259
266
  * result is `answer` or `tap`. With `webgl: true` the test also runs in the WebGL leg
260
267
  * (`--webgl`), whose pictures go to `screen.webgl.webp`: set it on a screen with a custom filter.
package/dist/visual.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { a as VisualApp, c as VisualRenderer, d as VisualStart, f as VisualStep, h as VisualTolerance, i as CheckpointResult, l as VisualReport, m as VisualTestResult, n as runVisualTests, o as VisualOptions, p as VisualTest, r as defineVisualTest, s as VisualPage, t as parseVisualArgv, u as VisualSetup } from "./visual-f6mtELhG.mjs";
1
+ import { a as VisualApp, c as VisualRenderer, d as VisualStart, f as VisualStep, h as VisualTolerance, i as CheckpointResult, l as VisualReport, m as VisualTestResult, n as runVisualTests, o as VisualOptions, p as VisualTest, r as defineVisualTest, s as VisualPage, t as parseVisualArgv, u as VisualSetup } from "./visual-DEo5Hndx.mjs";
2
2
  export { type CheckpointResult, type VisualApp, type VisualOptions, type VisualPage, type VisualRenderer, type VisualReport, type VisualSetup, type VisualStart, type VisualStep, type VisualTest, type VisualTestResult, type VisualTolerance, defineVisualTest, parseVisualArgv, runVisualTests };
package/dist/visual.mjs CHANGED
@@ -1,5 +1,6 @@
1
+ import { t as VISUAL_CLOCK } from "./fake-hnJcJeYf.mjs";
1
2
  import { n as read, t as sources } from "./sources-DM5Zcl4o.mjs";
2
- import { n as commands, t as run } from "./run-DyL8rl2P.mjs";
3
+ import { n as commands, t as run } from "./run-BnriyKUu.mjs";
3
4
  import path from "node:path";
4
5
  import { createBrandConsole } from "@moku-labs/common/cli";
5
6
  import { mkdir, readFile, writeFile } from "node:fs/promises";
@@ -62,15 +63,41 @@ function stepNames() {
62
63
  return Object.keys(commands).filter((name) => isStepCommand(name)).join(", ");
63
64
  }
64
65
  /**
65
- * Takes one step apart and checks its shape: exactly one key, either `checkpoint` or a step
66
- * command with an input object. The checkpoint name is checked by the caller.
66
+ * Takes a checkpoint step apart: its name, a string, and `finish`, a boolean when it is there; no
67
+ * other key.
68
+ *
69
+ * @param at - The test and the step, for the message.
70
+ * @param fields - The step.
71
+ * @returns The checkpoint.
72
+ * @throws {Error} When the name is not a string, `finish` not a boolean, or another key is there.
73
+ * @example
74
+ * ```ts
75
+ * checkpointEntry('[game] Visual test "fade", step 2', { checkpoint: "glow", finish: false }); // { kind: "checkpoint", name: "glow", finish: false }
76
+ * ```
77
+ */
78
+ function checkpointEntry(at, fields) {
79
+ const { checkpoint: name, finish = true, ...others } = fields;
80
+ if (Object.keys(others).length > 0) throw new Error(`${at} is not one command.\n A checkpoint takes its name and finish, like { checkpoint: "name", finish: false }.`);
81
+ if (typeof name !== "string") throw new TypeError(`${at}: the checkpoint name is not a string.\n Write the checkpoint as { checkpoint: "name" }.`);
82
+ if (typeof finish !== "boolean") throw new TypeError(`${at}: finish of the checkpoint "${name}" is not a boolean.\n Write finish: false to keep the motions running, or leave it out.`);
83
+ return {
84
+ kind: "checkpoint",
85
+ name,
86
+ finish
87
+ };
88
+ }
89
+ /**
90
+ * Takes one step apart and checks its shape: a checkpoint (its name, and `finish` when it is
91
+ * there), or exactly one step command with an input object. The checkpoint name is checked by the
92
+ * caller.
67
93
  *
68
94
  * @param test - The test name, for the message.
69
95
  * @param step - The step.
70
96
  * @param index - Its index in the steps, from 0.
71
97
  * @returns The checkpoint or the command with its input.
72
98
  * @throws {Error} When the step holds no key or more than one, names something that is not a
73
- * step, gives a command no input object, or a checkpoint no string.
99
+ * step, gives a command no input object, or a checkpoint no string name or a `finish` that is
100
+ * not a boolean.
74
101
  * @example
75
102
  * ```ts
76
103
  * stepEntry("reward-popup", { tap: { key: "play" } }, 0); // { kind: "command", name: "tap", input: { key: "play" } }
@@ -80,15 +107,9 @@ function stepEntry(test, step, index) {
80
107
  const fields = step;
81
108
  const [name, ...others] = Object.keys(fields);
82
109
  const at = `[game] Visual test "${test}", step ${index + 1}`;
110
+ if (Object.hasOwn(fields, "checkpoint")) return checkpointEntry(at, fields);
83
111
  if (name === void 0 || others.length > 0) throw new Error(`${at} is not one command.\n Write one command by its short name, or { checkpoint: "name" }.`);
84
112
  const value = fields[name];
85
- if (name === "checkpoint") {
86
- if (typeof value === "string") return {
87
- kind: "checkpoint",
88
- name: value
89
- };
90
- throw new Error(`${at}: the checkpoint name is not a string.\n Write the checkpoint as { checkpoint: "name" }.`);
91
- }
92
113
  if (!isStepCommand(name)) throw new Error(`${at} names "${name}", which is not a step.\n Use a checkpoint or one of: ${stepNames()}.`);
93
114
  if (!isInputObject(value)) throw new Error(`${at} (${name}) has no input object.\n Pass the input of the command, like { tap: { key: "play" } }.`);
94
115
  return {
@@ -173,7 +194,8 @@ function checkVisualTest(name, steps) {
173
194
  /**
174
195
  * Defines a visual test: where it starts, what `game.restore` loads, and its steps. A step is a
175
196
  * `/control` command by its short name with that command's input, or `{ checkpoint: name }`,
176
- * where the runner saves `state.json` and `describe.json` next to the test. `walk` runs fast: a
197
+ * where the runner lands every motion and saves `state.json` and `describe.json` next to the test;
198
+ * `{ checkpoint: name, finish: false }` keeps the motions running for the snapshot. `walk` runs fast: a
177
199
  * popup its last node opens never mounts, so the step before a checkpoint that shows a node's
178
200
  * result is `answer` or `tap`. With `webgl: true` the test also runs in the WebGL leg
179
201
  * (`--webgl`), whose pictures go to `screen.webgl.webp`: set it on a screen with a custom filter.
@@ -773,17 +795,19 @@ async function settleAt(play, where) {
773
795
  if (play.loop.error !== void 0) throw play.loop.error;
774
796
  }
775
797
  /**
776
- * Plays a checkpoint: settle, land every track and loop, two frames, then read and compare.
798
+ * Plays a checkpoint: settle, land every track and loop unless it says `finish: false`, two
799
+ * frames, then read and compare.
777
800
  *
778
801
  * @param play - The test being played.
779
- * @param name - The checkpoint name.
802
+ * @param entry - The checkpoint: its name, and whether it lands the motions.
780
803
  * @param where - The step, for the message.
781
804
  * @returns The outcome of the checkpoint; `pixels` is skipped in this leg.
782
805
  */
783
- async function checkpoint$1(play, name, where) {
806
+ async function checkpoint$1(play, entry, where) {
784
807
  const { app, test, run: options } = play;
808
+ const { name } = entry;
785
809
  await settleAt(play, where);
786
- app.anim.finishAll();
810
+ if (entry.finish) app.anim.finishAll();
787
811
  await frames(app, REST_FRAMES);
788
812
  const file = (kind) => baselineFile(options.dir, test.name, name, kind);
789
813
  const state = await checkBaseline(file("state.json"), stableJson(stateOf(app)), options.update);
@@ -819,7 +843,7 @@ async function playSteps$1(play, checkpoints) {
819
843
  const entry = stepEntry(test.name, step, index);
820
844
  const where = `step ${index + 1} (${entry.kind === "checkpoint" ? "checkpoint" : entry.name})`;
821
845
  if (entry.kind === "checkpoint") {
822
- checkpoints.push(await checkpoint$1(play, entry.name, where));
846
+ checkpoints.push(await checkpoint$1(play, entry, where));
823
847
  continue;
824
848
  }
825
849
  await failing(test.name, where, () => runStep(app, entry.name, entry.input));
@@ -1234,13 +1258,16 @@ async function pageStep(argument) {
1234
1258
  }
1235
1259
  }
1236
1260
  /**
1237
- * Plays a checkpoint in the page: settle, land every track and loop, two stepped frames, then the
1261
+ * Plays a checkpoint in the page: settle, land every track and loop unless the step says
1262
+ * `finish: false` (F87), two stepped frames, then the
1238
1263
  * picture of the whole canvas as lossless WebP and the state through the doors. The game is
1239
1264
  * paused, so the picture is the stage after the last stepped frame. With the frame check on, a
1240
1265
  * missed frame or mark logged in the meantime fails the checkpoint.
1241
1266
  *
1242
- * @param argument - The test and the step, and the most frames to settle.
1267
+ * @param argument - The test and the step, the most frames to settle, and whether to land the
1268
+ * motions.
1243
1269
  * @param argument.settleFrames - The most frames to settle.
1270
+ * @param argument.finish - True to land every motion before the picture.
1244
1271
  * @returns The state and the picture, or why the checkpoint failed.
1245
1272
  */
1246
1273
  async function pageCheckpoint(argument) {
@@ -1252,7 +1279,7 @@ async function pageCheckpoint(argument) {
1252
1279
  ok: false,
1253
1280
  error: unsettled
1254
1281
  };
1255
- game.anim.finishAll();
1282
+ if (argument.finish) game.anim.finishAll();
1256
1283
  await pageTick(handles);
1257
1284
  await pageTick(handles);
1258
1285
  const captured = await game.renderer.capture();
@@ -1497,6 +1524,17 @@ function pageScript() {
1497
1524
  }
1498
1525
  //#endregion
1499
1526
  //#region src/visual/leg-browser.ts
1527
+ /**
1528
+ * @file visual — the browser leg: Chrome with WebGPU on a Mac, driven by Playwright. It loads
1529
+ * `playwright-core` with a dynamic import only here, opens the dev page once per test, waits until
1530
+ * the page has loaded what it asked for, turns on the renderer's check of skipped frames
1531
+ * (`debug.verifyFrames`), pauses the game and steps it to the start time, plays the
1532
+ * test inside the page through `globalThis.game` and `globalThis.doors` on stepped frames, and at
1533
+ * each checkpoint checks the page's state against `state.json` and its picture against
1534
+ * `screen.webp`. The WebGL leg opens the page with `?renderer=webgl` and keeps its pictures in
1535
+ * `screen.webgl.webp`. The pixels are compared in the page (`page-script.ts`); the files are written by
1536
+ * `files.ts`. A failure of the page itself fails the leg; a failure of a step ends its test only.
1537
+ */
1500
1538
  /** The Chrome flags of `.planning/e2e/run.mjs`: WebGPU on Metal, on any GPU. */
1501
1539
  const WEBGPU_FLAGS = [
1502
1540
  "--enable-unsafe-webgpu",
@@ -1636,6 +1674,20 @@ async function launchChrome(chromium, browser) {
1636
1674
  throw new Error(`[game] No Chrome launched: ${own.reason}.\n Install Playwright's Chromium (bunx playwright-core install chromium), set page.browser, or run with --no-pixels.`);
1637
1675
  }
1638
1676
  /**
1677
+ * The init script that puts the moment of the headless leg's clock on the page before it loads
1678
+ * (F86): a dev page then runs its app on a fake clock standing there, as the headless leg does.
1679
+ *
1680
+ * @param moment - The moment, epoch milliseconds.
1681
+ * @returns The script.
1682
+ * @example
1683
+ * ```ts
1684
+ * clockScript(1_000_000); // 'globalThis["__mokuVisualClock"] = 1000000;'
1685
+ * ```
1686
+ */
1687
+ function clockScript(moment) {
1688
+ return `globalThis[${JSON.stringify(VISUAL_CLOCK)}] = ${JSON.stringify(moment)};`;
1689
+ }
1690
+ /**
1639
1691
  * Sizes the page: `page.width` CSS px wide (390), as tall as the inert frame of the game is at
1640
1692
  * that width, so the reference frame of the page equals the one of the headless leg; a phone
1641
1693
  * with touch at `page.deviceScaleFactor` (2). Builds one app of the setup, never started, to read
@@ -1762,14 +1814,16 @@ async function startPage(page, url, run) {
1762
1814
  * @param context - The browser context.
1763
1815
  * @param url - The URL of the page the leg opens.
1764
1816
  * @param run - The run options, for `settleFrames` and `renderer`.
1817
+ * @param moment - The moment the headless leg's clock stands at, for the page's clock (F86).
1765
1818
  * @returns The open page.
1766
1819
  * @throws {Error} When the page does not open, never exposes `game` and `doors`, does not start,
1767
1820
  * draws with another backend or never stops loading. The page is closed then.
1768
1821
  */
1769
- async function openPage(context, url, run) {
1822
+ async function openPage(context, url, run, moment) {
1770
1823
  const page = await context.newPage();
1771
1824
  try {
1772
1825
  await page.addInitScript({ content: pageScript() });
1826
+ await page.addInitScript({ content: clockScript(moment) });
1773
1827
  await page.goto(url).catch((error) => {
1774
1828
  throw new Error(`[game] The page at ${url} did not open: ${firstLine(error)}.\n Serve the dev page (moku-game dev), or run with --no-pixels.`);
1775
1829
  });
@@ -1825,17 +1879,19 @@ async function checkScreen(play, file, where, screen) {
1825
1879
  * headless leg wrote, the picture against the baseline of the leg's backend.
1826
1880
  *
1827
1881
  * @param play - The test being played.
1828
- * @param name - The checkpoint name.
1882
+ * @param entry - The checkpoint: its name, and whether it lands the motions.
1829
1883
  * @param where - The step, for the message.
1830
1884
  * @returns What the browser leg found there.
1831
1885
  * @throws {Error} When the page did not settle, gave no picture, or broke.
1832
1886
  */
1833
- async function checkpoint(play, name, where) {
1887
+ async function checkpoint(play, entry, where) {
1834
1888
  const { test, run } = play;
1889
+ const { name } = entry;
1835
1890
  const shot = await callPage(play.page, pageCheckpoint, {
1836
1891
  test: test.name,
1837
1892
  where,
1838
- settleFrames: run.settleFrames
1893
+ settleFrames: run.settleFrames,
1894
+ finish: entry.finish
1839
1895
  });
1840
1896
  if (shot.screen === void 0) throw new Error(`[game] Visual test "${test.name}", ${where}: the page gave no picture.\n Serve a dev build with ${BACKEND[run.renderer]}: renderer.capture() answers only there.`);
1841
1897
  const file = (kind) => baselineFile(run.dir, test.name, name, kind);
@@ -1882,7 +1938,7 @@ async function playSteps(play, found) {
1882
1938
  const entry = stepEntry(test.name, step, index);
1883
1939
  const where = `step ${index + 1} (${entry.kind === "checkpoint" ? "checkpoint" : entry.name})`;
1884
1940
  if (entry.kind === "checkpoint") {
1885
- found.set(entry.name, await checkpoint(play, entry.name, where));
1941
+ found.set(entry.name, await checkpoint(play, entry, where));
1886
1942
  continue;
1887
1943
  }
1888
1944
  await callPage(play.page, pageStep, {
@@ -1901,13 +1957,15 @@ async function playSteps(play, found) {
1901
1957
  * @param play - The test and the run options; the page is opened here.
1902
1958
  * @param play.test - The test.
1903
1959
  * @param play.run - The run options.
1904
- * @param url - The URL of the page the leg opens.
1960
+ * @param target - The page the leg opens: its URL, and the moment of the headless leg's clock.
1961
+ * @param target.url - The URL.
1962
+ * @param target.moment - The moment, epoch milliseconds.
1905
1963
  * @param headless - The result of the test in the headless leg.
1906
1964
  * @returns The result of the test with its pixel fields.
1907
1965
  * @throws {Error} When the page fails to open: that fails the leg.
1908
1966
  */
1909
- async function browserTest(context, play, url, headless) {
1910
- const page = await openPage(context, url, play.run);
1967
+ async function browserTest(context, play, target, headless) {
1968
+ const page = await openPage(context, target.url, play.run, target.moment);
1911
1969
  const found = /* @__PURE__ */ new Map();
1912
1970
  let failed;
1913
1971
  try {
@@ -1954,7 +2012,10 @@ async function runBrowserLeg(setup, tests, run, headless, load = loadChromium) {
1954
2012
  if (page === void 0) throw new Error("[game] The pixel leg needs the dev page.\n Pass page: { url } in the setup, or run with --no-pixels.");
1955
2013
  const chromium = await load();
1956
2014
  const options = pageSize(setup, page);
1957
- const url = rendererUrl(page.url, run.renderer);
2015
+ const target = {
2016
+ url: rendererUrl(page.url, run.renderer),
2017
+ moment: setup.app().clock.now()
2018
+ };
1958
2019
  const browser = await launchChrome(chromium, page.browser);
1959
2020
  try {
1960
2021
  const context = await browser.newContext(options);
@@ -1967,7 +2028,7 @@ async function runBrowserLeg(setup, tests, run, headless, load = loadChromium) {
1967
2028
  results.push(before.error === void 0 ? await browserTest(context, {
1968
2029
  test,
1969
2030
  run
1970
- }, url, before) : before);
2031
+ }, target, before) : before);
1971
2032
  }
1972
2033
  return results;
1973
2034
  } finally {
package/llms.txt CHANGED
@@ -12,7 +12,7 @@ Facts: Bun only. ESM only. `"sideEffects"` lists the root entry `./dist/index.mj
12
12
  - `@moku-labs/game/app/system` (the browser, the native shell): `systemShellOf`, `fromSystem`, `storeSave`, `createSystemApp`, the type `SystemModules`, over the optional peer `@moku-labs/system`. The one entry that names it, in types only: the page hands it the loaders.
13
13
  - `@moku-labs/game/cli` (node and bun only): `runCli`, the bin `moku-game`, `preparePage`, the editor's dev page, `watchKeys`, the watch that keeps `generated/` fresh next to it, and `watchEngine`, the watch that tells when the installed engine changed.
14
14
  - `@moku-labs/game/testing`: headless play, repro runs, isolated feature tests (`isolate`, `stub`), fake clock, the `fx` of a node for a unit test (`fakeFx`), in-memory save provider. No `node:` import.
15
- - `@moku-labs/game/visual` (node and bun only): the visual tests `defineVisualTest`, `runVisualTests`, `parseVisualArgv`. The bin runs them: `moku-game visual`.
15
+ - `@moku-labs/game/visual` (node and bun only): the visual tests `defineVisualTest`, `runVisualTests`, `parseVisualArgv`. The bin runs them: `moku-game visual`. A step is a `/control` command or `{ checkpoint: name, finish?: boolean }`: a checkpoint lands every motion before its snapshot, `finish: false` keeps them running (same frames in both legs). Both legs run on one clock: the pixel leg's dev page runs on a fake clock at the headless app's `clock.now()` (`startMoment`), standing still like the headless one.
16
16
  - `@moku-labs/game/assets` (node and bun only): the asset key scanner, the production packer and the string compiler. The package bin `moku-game-assets` runs it.
17
17
  - `@moku-labs/game/inspect`: the read door of the editor, safe in a production build. See "Doors for the editor".
18
18
  - `@moku-labs/game/control`: the write door of the editor, dev builds only. See "Doors for the editor".
@@ -557,6 +557,7 @@ A screen is a projection whose `view` returns JSX. The game's `tsconfig.json` se
557
557
  - Keys that come from data: `I18n.TextKey<Strings>` is the keys of the generated `Strings` without parameters (nothing new is generated). A data table types its keys `TextKey` or builds them with the kit's `key("board.hint.red")`; the kit's `tr` never takes a plain `string`. A key read back from a save is checked where the app is (an effect handler, app or page code, loading a save) with one `isKey(value)`: `defineGameApp({ ..., strings: type<Strings>() })` types the game's `app.i18n`, so a true `app.i18n.isKey(value)` narrows the value to `TextKey`; `ctx.require(i18nPlugin).isKey(value)` in a plugin narrows to `string`. `app.i18n.isKey(value)` answers true for a key of the loaded tables of the current locale or the fallback, and for the `<feature>.` prefix of a sub-feature whose strings are not loaded yet; a key that takes parameters answers true too.
558
558
  - 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? (a width, "none" or "box": wrap at the laid-out width of its ui element), 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, width?)` gives the same numbers in a browser and in Bun. `app.text.replaceStyles(styles)` is the dev hot swap of a saved `defineTextStyles` value.
559
559
  - 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?, space?, reduced? })`, `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 timeline resolves each `{ projection, key }` target once, when it starts: every step writes the view that held the key then, also after a commit removed it and while its exit plays, until it despawns; a view spawned again under the same key is not followed, and a key nothing held at the start is looked up at its step. `reduced: "keep"` (on a `tween` step, `transition` and `loop` of `defineMotion`, `TweenOptions`, `RestOptions`) plays a track as written under reduced motion, a fade for one; the default `"cut"` lands it at once or holds a loop on its first key. A node plays it with `await fx(play(animation, slots))`; in fast mode it finishes at once. `defineMotion({ states, transition?, on: { enter?, exit?, change? }, when? })` builds the enter, exit and change hooks for a projection or the `motion` prop of a tag; `when: "change"` plays enter and exit only for a commit, so an element that comes or goes with its screen stands at rest and leaves without a motion (default `"always"`). Events `anim:mark` `{ animation, mark }` and `anim:finished` `{ animation }`. `app.anim.replace(definition)` is the dev hot swap: the next `play` builds the new tree. A system cannot `require` a plugin, so it plays through the resource `AnimPlayer`: `res(AnimPlayer).play(animation, slots)` returns the same handle as `app.anim.play`, and the registered animation of `animation.id` wins, so a hot swap reaches the system. Play on a change, never every frame; from phase `input` the tree moves in `animate` of the same frame. Before `anim` starts it throws `[game] AnimPlayer is not ready: anim has not started.` Config `maxTracks: 2000`.
560
+ - A `tween` step takes `segments` and `repeat` as `ViewHandle.tween` does: `tween(chest, Transform, { y: 0, scale: 0 }, { ms: 2400, additive: true, repeat: "forever", segments: [{ at: 0.08, ease: "out", to: { y: -14, scale: 0.06 } }, { at: 1, to: { y: 0, scale: 0 } }] })`. Segments walk over `ms` on one clock, `to` is where the last one ends, an additive walk names offsets; a number counts the extra runs. A definition error throws for empty segments, an `at` outside 0..1 or out of order, a last segment before 1, a bad repeat, and a `sequence` step after one that never ends. A forever step ends only by cancel or finish (a finish and a fast world land it on its start pose), does not play under reduced motion `"cut"`, writes nothing while its view is unseen, and lets the clock idle; a system plays it once through `AnimPlayer` and keeps the handle.
560
561
  - 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)`, `muted(bus)`, `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). The context is asked to run at start: where the shell lets audio play (a native webview with the gesture requirement off) `unlocked()` is true with no touch; a browser that wants a gesture unlocks on the first `pointerdown` or `touchend`. Without `audio` every `sfx` and `music` effect resolves `undefined` at once, so the same node runs in a game with no sound. Named sounds: `sfx(key, { bus?, id?, fadeMs? })` with an `id` plays through its own gain, and the next sound with the same `id` fades the old one out over `fadeMs`, else config `sfxFadeMs` (default `120`, real milliseconds); `fx.start(stopSfx(id, { fadeMs? }))` and `app.audio.stop(id, { fadeMs? })` fade it out and end it (`fadeMs: 0` cuts it); an `id` nothing plays under changes nothing, and a sound of that `id` still decoding never starts. Without `id` an `sfx` plays to its end as before.
561
562
  - `app.ui`: `tree()` the live screen as plain data (a text node carries `content`, the words it draws after `tr()` and `bind`; a text field carries `value`), `find(key)` the entity of a keyed element, `fill(key, value)` types into the text field with that `key`, `scroll(key, to)` puts the `scroll` with that `key` at `to` from its start at once, inside `0..max`, its fling or glide stopped (`false` and `ui:scroll-without-scroll` for no live `scroll`, `false` and `ui:scroll-before-solve` for a `scroll` its first solve has not placed yet), `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 only the dev global event `ui:hot-swap`, see "Hot swap".
562
563
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moku-labs/game",
3
- "version": "0.15.30",
3
+ "version": "0.15.32",
4
4
  "description": "2D puzzle game engine on TypeScript and PixiJS, built on @moku-labs/core.",
5
5
  "type": "module",
6
6
  "sideEffects": [