@moku-labs/game 0.14.9 → 0.14.10

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,7 +1,7 @@
1
- import { Dt as AnyFlow, Ht as ClockSource, Nt as NodeTable, Wt as FakeClock, Xt as Json, ht as FeaturePlugin } from "./types-CUNcMkD1.mjs";
2
- import { t as createApp } from "./index-BRdctuIb.mjs";
3
- import { n as saveOf, t as memory } from "./memory-D6S7BUAo.mjs";
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-TowWxARz.mjs";
1
+ import { Dt as AnyFlow, Ht as ClockSource, Nt as NodeTable, Wt as FakeClock, Xt as Json, ht as FeaturePlugin } from "./types-BT9S_Lo2.mjs";
2
+ import { t as createApp } from "./index-pXWn22ql.mjs";
3
+ import { n as saveOf, t as memory } from "./memory-DR1ibF_s.mjs";
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-CrhefcLu.mjs";
5
5
  import { AnyPluginInstance } from "@moku-labs/core";
6
6
 
7
7
  //#region src/plugins/clock/fake.d.ts
package/dist/testing.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { Kt as type, Ut as exit, t as createApp } from "./src-DiPISDVO.mjs";
1
+ import { Kt as type, Ut as exit, t as createApp } from "./src-CzfMhkry.mjs";
2
2
  import { n as saveOf, t as memory } from "./memory-CWlqhS7l.mjs";
3
3
  import { t as fakeClock } from "./fake-MpzwZhIZ.mjs";
4
4
  import { i as stepFrames, r as runRepro, t as createHeadless } from "./headless-BA6ffzqD.mjs";
@@ -1,4 +1,4 @@
1
- import { A as Api$2, B as ComponentType, F as AnyComponent, H as Entity, I as AnyComponentType, L as AnyComponentValue, Lt as Descriptor, O as Point$2, Q as ChangeHook, Sn as Require, V as ComponentValue, Vt as Api$5, Xt as Json, at as NumericFields, dt as Api$1, h as Api$3, hn as Api, it as MotionHandle, k as ViewportSize, ot as ProjectionMotion, rt as Motion, s as Events$1, t as Api$4, ut as ViewHandle, zt as Hint } from "./types-CUNcMkD1.mjs";
1
+ import { A as Api$2, B as ComponentType, F as AnyComponent, H as Entity, I as AnyComponentType, L as AnyComponentValue, Lt as Descriptor, O as Point$2, Q as ChangeHook, Sn as Require, V as ComponentValue, Vt as Api$5, Xt as Json, at as NumericFields, dt as Api$1, h as Api$3, hn as Api, it as MotionHandle, k as ViewportSize, ot as ProjectionMotion, rt as Motion, s as Events$1, t as Api$4, ut as ViewHandle, zt as Hint } from "./types-BT9S_Lo2.mjs";
2
2
  import { Log } from "@moku-labs/common/browser";
3
3
  import { PluginCtx } from "@moku-labs/core";
4
4
 
@@ -1583,7 +1583,9 @@ type FxApi = {
1583
1583
  onHint(listener: HintListener): () => void;
1584
1584
  };
1585
1585
  /**
1586
- * The `fx` of a node context: awaited effects by call, cosmetic hints by `emit`.
1586
+ * The `fx` of a node context, and of the context of `recover`. Three ways to reach what is not
1587
+ * logic: `await fx(descriptor)` runs an effect and waits for it, `fx.start(descriptor)` starts one
1588
+ * now and does not wait, `fx.emit(hint)` keeps a cosmetic hint until the edge has committed.
1587
1589
  *
1588
1590
  * @example
1589
1591
  * ```ts
@@ -1594,7 +1596,45 @@ type FxApi = {
1594
1596
  * ```
1595
1597
  */
1596
1598
  type NodeFx = ((descriptor: Descriptor) => Promise<unknown>) & {
1599
+ /**
1600
+ * Buffers one cosmetic hint. It reaches its handler and the `onHint` listeners only after the
1601
+ * commit of the edge, so a node that fails shows nothing. Fast mode drops it.
1602
+ *
1603
+ * @param hint - The hint, built with `hint(kind, payload)`.
1604
+ * @example
1605
+ * ```ts
1606
+ * // The "merge" node: the sparkle belongs to the merge, so it is shown once the merge is real.
1607
+ * run: ({ fx, session, input, out }) => {
1608
+ * session.merged.push(input.cell);
1609
+ * fx.emit(hint("sparkle", { cell: "c3" })); // nothing is shown yet
1610
+ *
1611
+ * return out.done(); // the edge commits, then the handler of "sparkle" gets the hint
1612
+ * }
1613
+ * ```
1614
+ */
1597
1615
  emit(hint: Hint): void;
1616
+ /**
1617
+ * Starts an effect now and does not wait for it: the handler is called at once and the body goes
1618
+ * on. Fast mode and the node's signal apply as for `await fx(descriptor)`. A handler that fails
1619
+ * never fails the node: the log gets the error entry `flow:fx-start-failed` with
1620
+ * `{ path, kind }`, and a `cosmetic` descriptor is silent. After the abort of the node nothing is
1621
+ * started, and a rejection that comes after the abort is not logged: the handler was told to
1622
+ * stop.
1623
+ *
1624
+ * @param descriptor - The effect to start. Not one with `answers` and not a `guide`.
1625
+ * @throws {Error} When the descriptor has `answers` or is a `guide`: an answer has to be awaited.
1626
+ * @example
1627
+ * ```ts
1628
+ * // The "place" node of a board game: the tap sound starts, the mark is placed at once.
1629
+ * run: ({ fx, session, input, out }) => {
1630
+ * fx.start(sfx("ui.tap")); // undefined: the handler of "sfx" is already playing "ui.tap"
1631
+ * session.marks.push(input.cell);
1632
+ *
1633
+ * return out.placed(); // the edge commits while the sound still plays
1634
+ * }
1635
+ * ```
1636
+ */
1637
+ start(descriptor: Descriptor): void;
1598
1638
  };
1599
1639
  //#endregion
1600
1640
  //#region src/plugins/flow/inbox/types.d.ts
@@ -3284,10 +3324,19 @@ type TrackOptions = {
3284
3324
  * and goes through `ecs.set`, so `changed()` sees it. Muted fields are never written, also not on
3285
3325
  * `finish()`.
3286
3326
  *
3327
+ * A driver with additive tracks keeps the value under them: an additive track adds an offset to
3328
+ * it, and an absolute track of 0 ms to a field moves it. Such a driver reports the offsets with
3329
+ * `offsetsOf`, so the world can compare the value under a loop with the rest pose.
3330
+ *
3287
3331
  * @example
3288
3332
  * ```ts
3289
3333
  * // `anim` hands the world its own tween core in `onStart`.
3290
- * const driver: TweenDriver = { track: startTrack, cancelAll: cancelTracksOf };
3334
+ * const driver: TweenDriver = {
3335
+ * track: startTrack,
3336
+ * cancelAll: cancelTracksOf,
3337
+ * // A card sways 0.04 rad to one side right now: { rotation: 0.04 }
3338
+ * offsetsOf: addedOffsets
3339
+ * };
3291
3340
  * const off = ctx.require(worldPlugin).projection.setDriver(driver);
3292
3341
  *
3293
3342
  * off(); // `anim` stopped: the world writes instantly again
@@ -3311,6 +3360,18 @@ type TweenDriver = {
3311
3360
  * @param entity - The entity whose tracks end.
3312
3361
  */
3313
3362
  cancelAll(entity: Entity): void;
3363
+ /**
3364
+ * What the running additive tracks add to each field of one component of one entity right now:
3365
+ * the offset of a loop. The world takes it off the stored value before it compares a view with
3366
+ * its rest pose, so a view that only sways is home. Optional: a driver without additive tracks
3367
+ * leaves it out, and the world reads that as no offset.
3368
+ *
3369
+ * @param entity - The entity to ask about.
3370
+ * @param component - The component to ask about.
3371
+ * @returns Field name to the sum of the offsets on it. A field no additive track adds to is
3372
+ * left out.
3373
+ */
3374
+ offsetsOf?(entity: Entity, component: AnyComponent): Readonly<Record<string, number>>;
3314
3375
  };
3315
3376
  /**
3316
3377
  * One node of an element description, as `ui` builds it. The world never imports `ui`: it knows
@@ -3385,7 +3446,10 @@ type ChangeHooks<Item> = {
3385
3446
  * The motion hooks of a projection. Every one is optional; without a hook the component diff is
3386
3447
  * written directly. `loop` starts what moves a view for its whole life (an additive sway, a spin):
3387
3448
  * it is played wherever `enter` plays, next to it, and its motion is kept apart from the view's
3388
- * other motions, so a change never cancels it; it ends when the view dies.
3449
+ * other motions, so a change never cancels it; it ends when the view dies. A view that entered
3450
+ * directly (a mount, a load, a restore, a paused or fast world) plays no `enter`, and a direct
3451
+ * reconcile ends every track of a view: the loop is then started by the first frame in which the
3452
+ * world is live and a tween driver is installed, once.
3389
3453
  *
3390
3454
  * @example
3391
3455
  * ```ts
@@ -3473,7 +3537,9 @@ type AnyProjectionSpec = {
3473
3537
  * Why a reconcile runs. Merged over the frame; the cause table decides play or direct. A
3474
3538
  * `"restore"` and a `"load"` replace the state, so their reconcile also brings every live view to
3475
3539
  * its rest pose, whether its item changed or not, and every element to the rest pose its owner
3476
- * recorded with `setRest`.
3540
+ * recorded with `setRest`. A `"rollback"` commits the state of the last rest point: a live view
3541
+ * whose item did not change and that stands off its rest pose is settled, as `settle(entity)`
3542
+ * does. An element another plugin owns is not.
3477
3543
  *
3478
3544
  * @example
3479
3545
  * ```ts
@@ -3484,21 +3550,6 @@ type Cause = "edge" | "rollback" | "restore" | "load" | "mount" | "rerun";
3484
3550
  /**
3485
3551
  * One live or exiting view: the entity of one key of one projection, its rest pose and the
3486
3552
  * motions that still run on it.
3487
- *
3488
- * @example
3489
- * ```ts
3490
- * const view: View = {
3491
- * entity: 1_048_576,
3492
- * projection: "board.items",
3493
- * key: "i7",
3494
- * item: { id: "i7" },
3495
- * rest: new Map(),
3496
- * handles: [],
3497
- * lifted: false,
3498
- * dropWhenStill: false,
3499
- * exiting: false
3500
- * };
3501
- * ```
3502
3553
  */
3503
3554
  type View$1 = {
3504
3555
  entity: Entity;
@@ -3509,6 +3560,13 @@ type View$1 = {
3509
3560
  handles: MotionHandle[];
3510
3561
  lifted: boolean; /** `lift(false)` is waiting for the last motion to end. */
3511
3562
  dropWhenStill: boolean;
3563
+ /**
3564
+ * The `loop` hook of the view is still to be started. Set when the view enters in a direct
3565
+ * reconcile, which plays no hook, and whenever a flush ends its tracks, the loop among them.
3566
+ * The sweep starts the loop and clears the flag, only while the world is live and a tween
3567
+ * driver is installed. It is the only trigger, so a loop starts once.
3568
+ */
3569
+ loopOwed: boolean;
3512
3570
  exiting: boolean;
3513
3571
  };
3514
3572
  /**
@@ -3534,17 +3592,16 @@ type Mounted = {
3534
3592
  /**
3535
3593
  * One track the projection started on the driver. The world keeps the component name and the
3536
3594
  * handle, so it knows which components are driven without knowing how the driver runs them.
3537
- *
3538
- * @example
3539
- * ```ts
3540
- * const track: Track = { entity: 1_048_576, component: "Transform", handle: flight };
3541
- * track.handle.active(); // true while the flight runs
3542
- * ```
3543
3595
  */
3544
3596
  type Track = {
3545
3597
  entity: Entity;
3546
3598
  component: string;
3547
3599
  handle: MotionHandle;
3600
+ /**
3601
+ * Whether the track adds to its fields instead of owning them, as the loop of a view does. An
3602
+ * additive track drives no component: a settle may bring the component home under it.
3603
+ */
3604
+ additive: boolean;
3548
3605
  };
3549
3606
  /**
3550
3607
  * The rest pose of one entity, by component name: the last output of `view(item)` for a view, or
@@ -3589,9 +3646,17 @@ type ProjectionState = {
3589
3646
  dirty: Dirty | undefined;
3590
3647
  hints: Hint[]; /** The tracks the projection started, with the handle that says whether they still run. */
3591
3648
  tracks: Track[]; /** The tween engine `anim` installed, or `undefined`: then every target is written at once. */
3592
- driver: TweenDriver | undefined; /** The rest pose of an entity that is not a view, recorded by its owner through `setRest`. */
3649
+ driver: TweenDriver | undefined;
3650
+ /**
3651
+ * The rest pose of an entity that is not a view, recorded by its owner through `setRest`. The
3652
+ * entry of an entity that is gone is dropped by the sweep and by the converge step.
3653
+ */
3593
3654
  rests: Map<Entity, RestPose>; /** Projection name to key to the entity a plugin above registered under it. */
3594
- keys: Map<string, Map<string, Entity>>; /** The reverse of `keys`, so `keyOf` answers for a registered element too. */
3655
+ keys: Map<string, Map<string, Entity>>;
3656
+ /**
3657
+ * The reverse of `keys`, so `keyOf` answers for a registered element too. The entry of an
3658
+ * entity that is gone is dropped with its rest pose, and its side of `keys` with it.
3659
+ */
3595
3660
  keysByEntity: Map<Entity, ProjectionKey>; /** Entity to component name to the fields another writer owns. */
3596
3661
  mutes: Map<Entity, Map<string, Set<string>>>; /** The `onConverged` listeners: the owners of elements, told after a restore or a load. */
3597
3662
  converged: Array<() => void>;
@@ -3700,8 +3765,8 @@ type ProjectionApi = {
3700
3765
  unmount(names: readonly string[]): void;
3701
3766
  /**
3702
3767
  * A drop with no commit: cancels the view's motions and plays settle for every loose component.
3703
- * In mode `paused` or `fast` the rest pose is written at once. No-op for an entity that is not
3704
- * a live view.
3768
+ * A view that loops comes home too, and its loop runs on. In mode `paused` or `fast` the rest
3769
+ * pose is written at once. No-op for an entity that is not a live view.
3705
3770
  *
3706
3771
  * @param entity - The entity of the view that was dropped.
3707
3772
  * @example
@@ -3732,7 +3797,8 @@ type ProjectionApi = {
3732
3797
  /**
3733
3798
  * Moves a view into or out of its projection's `lift` layer. `lift(entity, false)` on a view
3734
3799
  * that still moves takes effect when its last motion ends, so a settling view stays above the
3735
- * board until it is home.
3800
+ * board until it is home. A reconcile or a `settle(entity)` that ends the motions of the view
3801
+ * applies the pending drop itself.
3736
3802
  *
3737
3803
  * @param entity - The entity of the view.
3738
3804
  * @param on - True to lift, false to drop back.
@@ -3865,6 +3931,7 @@ type ProjectionApi = {
3865
3931
  * It is also where a restore and a load put the element: after their reconcile every track on
3866
3932
  * the entity is ended and every recorded component it still carries is written, muted fields
3867
3933
  * left out. So the owner records the pose it draws the element with, every time that changes.
3934
+ * The pose of an entity that is gone is dropped in the next live frame.
3868
3935
  *
3869
3936
  * @param entity - The entity of the element.
3870
3937
  * @param component - The component whose rest value is recorded.
@@ -3882,6 +3949,8 @@ type ProjectionApi = {
3882
3949
  /**
3883
3950
  * Gives an entity a projection and key of its own, so `entityOf` and `keyOf` answer for it next
3884
3951
  * to the live views. That is how an element becomes a target for `anim`, `input` and `guide`.
3952
+ * A key whose entity is gone is dropped in the next live frame, also when the remover was never
3953
+ * called.
3885
3954
  *
3886
3955
  * @param projection - Name the element is addressed under.
3887
3956
  * @param key - Key the element is addressed under.
@@ -3957,7 +4026,10 @@ type ProjectionInternal = {
3957
4026
  pushHint(hint: Hint): void;
3958
4027
  /**
3959
4028
  * Sweeps the views whose motions ended: the despawn queue, the convergence check and the
3960
- * pending drops. The tracks themselves are advanced by the driver.
4029
+ * pending drops. Then it drops the rest pose and the registered key of every element whose
4030
+ * entity is gone, and starts the loop of every live view that owes one. It runs once per
4031
+ * frame and does nothing unless the world is live. The tracks themselves are advanced by the
4032
+ * driver.
3961
4033
  */
3962
4034
  sweep(): void;
3963
4035
  /**
@@ -1,5 +1,5 @@
1
- import { Xt as Json, an as Snapshot } from "./types-CUNcMkD1.mjs";
2
- import { t as HeadlessApp } from "./headless-TowWxARz.mjs";
1
+ import { Xt as Json, an as Snapshot } from "./types-BT9S_Lo2.mjs";
2
+ import { t as HeadlessApp } from "./headless-CrhefcLu.mjs";
3
3
  import { Log } from "@moku-labs/common/browser";
4
4
 
5
5
  //#region src/plugins/flow/doors/types.d.ts
@@ -1,7 +1,7 @@
1
- import { Dt as AnyFlow, Ht as ClockSource, Jt as Config, Wt as FakeClock, Xt as Json, a as Config$3, g as Config$2, ht as FeaturePlugin, n as AssetsIo, pt as Config$1, tn as PlayerStateProvider, u as Manifest, v as PixiModule } from "./types-CUNcMkD1.mjs";
2
- import { o as AudioContextLike, s as Config$4 } from "./types-CmhczThS.mjs";
3
- import { _t as audioPlugin, ct as PlatformProvider, ht as effectsPlugin, i as screen, ot as platformPlugin, t as createApp } from "./index-BRdctuIb.mjs";
4
- import { t as memory } from "./memory-D6S7BUAo.mjs";
1
+ import { Dt as AnyFlow, Ht as ClockSource, Jt as Config, Wt as FakeClock, Xt as Json, a as Config$3, g as Config$2, ht as FeaturePlugin, n as AssetsIo, pt as Config$1, tn as PlayerStateProvider, u as Manifest, v as PixiModule } from "./types-BT9S_Lo2.mjs";
2
+ import { o as AudioContextLike, s as Config$4 } from "./types-DxKupZ6I.mjs";
3
+ import { _t as audioPlugin, ct as PlatformProvider, ht as effectsPlugin, i as screen, ot as platformPlugin, t as createApp } from "./index-pXWn22ql.mjs";
4
+ import { t as memory } from "./memory-DR1ibF_s.mjs";
5
5
  import { AnyPluginInstance } from "@moku-labs/core";
6
6
 
7
7
  //#region src/app/types.d.ts
@@ -154,9 +154,18 @@ type GameDefinition<Plugins extends readonly AnyPluginInstance[] = readonly [],
154
154
  session: Session; /** The long side the layout needs, in reference units. Default 1920. */
155
155
  referenceLong?: number; /** The rng seed of a new save. Default 42. */
156
156
  seed?: number; /** The shared layer: composed first among the features. */
157
- shared?: FeaturePlugin; /** The features of the screen app, in order, after `shared`. */
157
+ shared?: FeaturePlugin;
158
+ /**
159
+ * The features of the screen app, in order, after `shared`. The headless app composes their
160
+ * logic too, unless `headless.features` names its own list.
161
+ */
158
162
  features?: readonly FeaturePlugin[]; /** The game's own plugins, after the features. Their APIs land on `screen().app`. */
159
- plugins?: Plugins; /** What the headless app composes: the logic of these features and these plugins, nothing else. */
163
+ plugins?: Plugins;
164
+ /**
165
+ * What the headless app composes: the logic of these features and these plugins, nothing else.
166
+ * `headless.features` left out is the definition's `features`; `headless: { features: [] }`
167
+ * composes no feature. `shared` and `plugins` are never taken by default.
168
+ */
160
169
  headless?: {
161
170
  features?: readonly FeaturePlugin[];
162
171
  plugins?: HeadlessPlugins;
@@ -360,8 +369,9 @@ type ScreenSeams<Player, Session, Source, Provider> = HeadlessSeams<Player, Sess
360
369
  type GameApp<Plugins extends readonly AnyPluginInstance[] = readonly [], HeadlessPlugins extends readonly AnyPluginInstance[] = readonly [], Player extends Json = Json, Session extends Json = Json> = {
361
370
  /**
362
371
  * Makes the logic app: the core plugins, the logic of `headless.features` and the
363
- * `headless.plugins`. No screen plugin, no game plugin unless listed. Each call gives a fresh
364
- * app, clock and provider.
372
+ * `headless.plugins`. A definition without `headless.features` gives the logic of its
373
+ * `features`. No screen plugin, no `shared`, no game plugin unless listed. Each call gives a
374
+ * fresh app, clock and provider.
365
375
  *
366
376
  * @param seams - What the test pins: seed, clock, provider, player, session.
367
377
  * @returns The app, not started, and its clock and provider.
@@ -1,5 +1,5 @@
1
- import { A as Api$2, B as ComponentType, F as AnyComponent, H as Entity, It as TypeTag, L as AnyComponentValue, Lt as Descriptor, R as ComponentHandle, Sn as Require, Xt as Json, Yt as Events$2, at as NumericFields, ct as TrackOptions, dn as Events$3, dt as Api$1, et as Ease, h as Api$5, hn as Api, it as MotionHandle, lt as TrackSegment, nt as LayerSpec, q as Owner, qt as Api$4, s as Events$4, t as Api$3, tt as LayerSort, xn as Events$5 } from "./types-CUNcMkD1.mjs";
2
- import { A as InputApi, c as UiNode, i as Finding, n as ElementChange, o as JsxState, q as I18nApi, r as ElementMotion, rt as StylesState, x as TextApi } from "./types-p12uOu00.mjs";
1
+ import { A as Api$2, B as ComponentType, F as AnyComponent, H as Entity, It as TypeTag, L as AnyComponentValue, Lt as Descriptor, R as ComponentHandle, Sn as Require, Xt as Json, Yt as Events$2, at as NumericFields, ct as TrackOptions, dn as Events$3, dt as Api$1, et as Ease, h as Api$5, hn as Api, it as MotionHandle, lt as TrackSegment, nt as LayerSpec, q as Owner, qt as Api$4, s as Events$4, t as Api$3, tt as LayerSort, xn as Events$5 } from "./types-BT9S_Lo2.mjs";
2
+ import { A as InputApi, c as UiNode, i as Finding, n as ElementChange, o as JsxState, q as I18nApi, r as ElementMotion, rt as StylesState, x as TextApi } from "./types-6yCbzHHn.mjs";
3
3
  import { Log } from "@moku-labs/common/browser";
4
4
  import { PluginCtx } from "@moku-labs/core";
5
5
  import { Node, Yoga } from "yoga-layout/load";
@@ -16,7 +16,7 @@ import { Node, Yoga } from "yoga-layout/load";
16
16
  * id: 1, entity: 1_048_576, component: Transform, to: { x: 200 },
17
17
  * keys: { x: "1048576:Transform:x" }, ms: 350, ease: "out", delayMs: 0, elapsed: 0,
18
18
  * from: undefined, muted: () => new Set(), additive: false, driven: false, bornFrame: 4,
19
- * ended: false, segments: undefined, repeatsLeft: 0, held: false
19
+ * ended: false, segments: undefined, repeatsLeft: 0, held: false, masked: new Set()
20
20
  * };
21
21
  * ```
22
22
  */
@@ -55,6 +55,12 @@ type Track = {
55
55
  * walk clears it, so the next hold writes the first key again.
56
56
  */
57
57
  held: boolean;
58
+ /**
59
+ * The fields an additive track skipped at its last write because another writer owned them.
60
+ * When such a field is given back, the track adds to the value it finds there and not to the
61
+ * base it kept. Empty for a track that owns its fields.
62
+ */
63
+ masked: Set<string>;
58
64
  };
59
65
  /**
60
66
  * A motion handle that can also be advanced by hand: what the timeline cursor needs, so a track
@@ -901,7 +907,12 @@ type State$3 = {
901
907
  nextId: number; /** `"<entity>:<component>:<field>"` to the id of the absolute track that drives the field. */
902
908
  owner: Map<string, number>; /** Per field key, the delta each additive track contributes. */
903
909
  offsets: Map<string, Map<number, number>>; /** Per field key, the value the offsets are added to. Dropped when no track drives the field. */
904
- bases: Map<string, number>; /** The running timelines, by id. */
910
+ bases: Map<string, number>;
911
+ /**
912
+ * Per entity, its running additive tracks. `offsetsOf` of the driver reads it, so the cost of
913
+ * one answer is the loops of that entity and not every track of the game.
914
+ */
915
+ additive: Map<Entity, Set<Track>>; /** The running timelines, by id. */
905
916
  timelines: Map<number, RunningTimeline>; /** The `animations` of every feature, read in `onStart`. */
906
917
  registry: Map<string, AnyAnimationDefinition>; /** `onMark` subscribers, in subscription order. */
907
918
  markListeners: Set<MarkListener>; /** Counter of the frame steps. A track born in the running step is not advanced by it. */
@@ -1,5 +1,5 @@
1
- import { A as Api$1, B as ComponentType, D as FilterSlot, H as Entity, O as Point, R as ComponentHandle, Sn as Require, _ as PixiFilter, b as PixiTexture, dt as Api, h as Api$2, s as Events, t as Api$3, v as PixiModule, xn as Events$1, y as PixiSprite } from "./types-CUNcMkD1.mjs";
2
- import { x as AnimApi } from "./types-CmhczThS.mjs";
1
+ import { A as Api$1, B as ComponentType, D as FilterSlot, H as Entity, O as Point, R as ComponentHandle, Sn as Require, _ as PixiFilter, b as PixiTexture, dt as Api, h as Api$2, s as Events, t as Api$3, v as PixiModule, xn as Events$1, y as PixiSprite } from "./types-BT9S_Lo2.mjs";
2
+ import { x as AnimApi } from "./types-DxKupZ6I.mjs";
3
3
  import { Log } from "@moku-labs/common/browser";
4
4
  import { PluginCtx } from "@moku-labs/core";
5
5
 
package/dist/visual.d.mts CHANGED
@@ -1,9 +1,9 @@
1
- import { A as Api$1, h as Api, ln as Api$3, qt as Api$2 } from "./types-CUNcMkD1.mjs";
2
- import { r as UiApi, x as AnimApi } from "./types-CmhczThS.mjs";
3
- import { A as InputApi } from "./types-p12uOu00.mjs";
4
- import { r as Repro } from "./headless-TowWxARz.mjs";
5
- import { a as InputOf, r as ControlApp } from "./types-CsejHij6.mjs";
6
- import { t as commands } from "./commands-BiZwnDBd.mjs";
1
+ import { A as Api$1, h as Api, ln as Api$3, qt as Api$2 } from "./types-BT9S_Lo2.mjs";
2
+ import { r as UiApi, x as AnimApi } from "./types-DxKupZ6I.mjs";
3
+ import { A as InputApi } from "./types-6yCbzHHn.mjs";
4
+ import { r as Repro } from "./headless-CrhefcLu.mjs";
5
+ import { a as InputOf, r as ControlApp } from "./types-Bc9AvZVO.mjs";
6
+ import { t as commands } from "./commands-fUjYUA67.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. */
package/llms.txt CHANGED
@@ -43,6 +43,7 @@ Root exports:
43
43
  - Transit node: has `run`, no `rest`. Rest node: `rest: true`. The graph waits there. Without `run` it is a pure wait.
44
44
  - A pure wait ends in two ways. A gate answer whose `intent` is an outcome name: the answer payload becomes the outcome payload. Or a world event whose `type` is listed in the node's `inbox`: the event payload becomes the outcome payload.
45
45
  - Transition: rest node to rest node. It is a transaction. On an error the runner discards, rolls back to the last rest point, retries `retries` times, then enters `safeNode`. A failure inside `safeNode` rejects `run()`.
46
+ - Three ways to run an effect in a node. `await fx(descriptor)`: the node waits for it. `fx.start(descriptor)`: it starts now and the node goes on; write it in place of `void fx(...)`. A failure of a started effect is the log entry `flow:fx-start-failed` and never fails the node; a descriptor with `answers` and a `guide` are refused, they have to be awaited. `fx.emit(hint(...))`: released after the commit of the edge, dropped in fast mode.
46
47
  - Every failure is written into the log by the engine: an `error` entry `flow:error` with `{ path, rolledBackTo, retry, error: { message, stack } }`. Read it with `app.log.trace()`. A game needs no plugin for it.
47
48
  - `recover` on a rest node: `recover: async ({ player, session, rng, fx, signal, now }) => { ... }`. It runs only when the runner gave up on a transition and enters this node as `safeNode`, before the node is entered. The rollback left the state of the failed rest point; `recover` writes the drafts so the safe node can draw itself. What it wrote commits as one edge with `outcome: "recover"` and becomes the rest point. A `recover` that throws rejects `run()`. A retry does not run it. So the safe node stays a pure wait: no `run` body, no extra outcome, no self edge.
48
49
  - Entering a rest node calls `model.store.markRest()`: the save provider gets the patches. Leaving a `barrier` node calls `markBarrier(txId)`: a durable write, and rollback cannot cross it.
@@ -121,7 +122,7 @@ type NodeContext = {
121
122
  player: Player; // draft of the saved player tree, mutate it
122
123
  session: Session; // draft of the session tree, never saved
123
124
  rng: RngView; // rng.stream(id) gives a deterministic stream
124
- fx: NodeFx; // await fx(descriptor), fx.emit(hint(...))
125
+ fx: NodeFx; // await fx(descriptor), fx.start(descriptor), fx.emit(hint(...))
125
126
  out: Out; // one method per declared outcome
126
127
  signal: AbortSignal; // aborted on stop, restore and inbox delivery
127
128
  now: number; // clock.now() read once at node entry
@@ -260,9 +261,9 @@ The game never calls `flow.run()`. The page runs the graph after `app.start()`;
260
261
 
261
262
  A game is a folder: `index.ts` (`export default defineGameApp({ ... })`), `config.ts` (plain data, `satisfies GameConfig`), `core/`, `shared/`, `features/`, `plugins/`, `generated/`, `tests/` with `tests/scenarios/<name>.ts`. No `web/`, no `native.ts`, no `platform-bridge.ts`, no `bunfig.toml`, no `createApp` call. `.moku/` holds what `moku-game` writes; the game's `.gitignore` lists it. Scripts of the game's `package.json`: `"dev": "moku-game dev"`, `"build": "moku-game build"`, `"native:ios-sim": "moku-game native build ios --simulator"`.
262
263
 
263
- - `defineGameApp(definition)` returns the game, `{ headless(seams?), screen(seams?) }`, and creates no app. Every type comes from the object; no generic at the call site. Keys: `flow` (required), `safeNode` (default: the start of the main flow), `player` and `session` (required), `referenceLong` (1920), `seed` (42), `shared` (composed first among the features), `features`, `plugins` (their APIs land on `screen().app`), `headless: { features?, plugins? }`, `pluginConfigs`.
264
+ - `defineGameApp(definition)` returns the game, `{ headless(seams?), screen(seams?) }`, and creates no app. Every type comes from the object; no generic at the call site. Keys: `flow` (required), `safeNode` (default: the start of the main flow), `player` and `session` (required), `referenceLong` (1920), `seed` (42), `shared` (composed first among the features), `features`, `plugins` (their APIs land on `screen().app`), `headless: { features?, plugins? }` (`features` left out takes the game's `features`, `[]` means none, `shared` is not composed), `pluginConfigs`.
264
265
  - `pluginConfigs` takes every plugin of the screen app but the keys the shell writes. A seam-owned key is a compile error: `model.playerProvider`, `initialPlayer`, `initialSession`, `seed` (the game keeps `schemaVersion`, `migrations`); all of `clock` and `platform`; `flow.mainFlow`, `safeNode`; `renderer.mount`; `assets.manifest`, `io`; `audio.context`.
265
- - `game.headless(seams?)` composes the core plugins, `logicOnly` of `headless.features`, then `headless.plugins`. `game.screen(seams?)` composes the screen set, `audio`, `effects`, `platform`, then `shared`, the features and the game's plugins. Both return `{ app, clock, provider }`, the app not started; each call is fresh. Defaults: `fakeClock(startMoment)` (`startMoment` is `1_000_000`), a fresh `memory()` save, the seed of the definition or 42.
266
+ - `game.headless(seams?)` composes the core plugins, `logicOnly` of `headless.features` (of `features` when left out), then `headless.plugins`. `game.screen(seams?)` composes the screen set, `audio`, `effects`, `platform`, then `shared`, the features and the game's plugins. Both return `{ app, clock, provider }`, the app not started; each call is fresh. Defaults: `fakeClock(startMoment)` (`startMoment` is `1_000_000`), a fresh `memory()` save, the seed of the definition or 42.
266
267
  - Seams of both: `seed`, `clock`, `provider`, `player`, `session`. Screen only: `manifest` (a URL or the parsed file), `io`, `platform`, `keepAwake` (false), `audio: { context?, journal? }`, `renderer: { mount?, loadPixi?, preference? }`. Without a mount the renderer is inert, so `game.screen()` runs in plain Bun.
267
268
  - `defineGameApp` throws `[game] defineGameApp needs flow.` and `[game] defineGameApp: features[2] is not a feature.` for what TypeScript cannot stop.
268
269
  - A game's exit plugin calls `ctx.require(platformPlugin).exit()` itself.
@@ -479,7 +480,11 @@ The graph. Modules `runner`, `gate`, `inbox`, `fx`, `features`. Runner methods s
479
480
  - `features.register(name, description)`, `features.all()`, `features.contributions(slotName)`.
480
481
  - `FlowState` is `{ running, path, stack, pending: { fx?, gate? }, mode }`.
481
482
  - `Bookmark` is `{ path, input, player, session, rng, graph, scene?, rest? }`. Serialisable. `scene` is written by the `game.bookmark` door and read by `game.restore`; `flow.restore` ignores it. `path` names a rest node, or the transit node that waits at the gate for an effect that takes answers (a `popup`, or a hand-written descriptor with `answers`); `rest: { path, input }` then names the rest point before it (left out when the graph started at that node or a restore entered it), and the state is the one committed when that node was entered. `restore` needs a running graph. A rest checkpoint is always accepted. Any other node is accepted only when the graph did not change since the bookmark; with another graph a transit bookmark is entered at `rest` instead when that is a checkpoint, and the log gets `flow:restore-fell-back`.
482
- - After a `restore` or a load the picture is the one of the restored state, whatever played before: every view of a mounted projection stands on its rest pose with no motion running, and every ui root is built again through the mount path, so `enter` hooks run and a loop starts as after a fresh mount. Scroll offsets, local state, hover and focus start anew. Look an element up again with `ui.find(key)` after a restore. A rest node names the scene it is shown on (`scene: "splash"`): a restore into a node without `scene` keeps the mounted scene.
483
+ - After a rollback (a node failed) a view of a projection that the node moved and that still stands off its rest pose is settled, also when its item did not change. Ui elements are not settled by a rollback, and a timeline the failed node started without `await` runs on.
484
+ - A loop adds its sway on top of the value under it. The world compares that value with the rest pose, so a looping view that stands at home is never settled, and one a node moved is brought home while its loop runs on. A custom tween driver may report what its additive tracks add with the optional `offsetsOf(entity, component)`; without it the stored value is compared as it stands.
485
+ - The `loop` hook of a view runs while the world is live and `anim` is installed: a view that entered on a load, a mount or a restore gets its loop from the next live frame. `anim.finishAll()` ends it; the next flush of the world (a load, a restore, a commit in a paused or fast world) owes it again.
486
+ - A projection may build a new item object on every commit. A view whose components did not change keeps its running motions; only a real component change cancels them. No frozen item tables are needed.
487
+ - After a `restore` or a load the picture is the one of the restored state, whatever played before: every view of a mounted projection stands on its rest pose, its `loop` starts again on the next live frame and no other motion runs, and every ui root is built again through the mount path, so `enter` hooks run and a loop starts as after a fresh mount. Scroll offsets, local state, hover and focus start anew. Look an element up again with `ui.find(key)` after a restore. A rest node names the scene it is shown on (`scene: "splash"`): a restore into a node without `scene` keeps the mounted scene.
483
488
  - `restore` of a rest node resolves when the node is entered. `restore` of a transit node runs it again from its start, so its effects before the gate play again, and resolves once its gate is open. The open gate is not the draw: the popup shows on the next frame. `restore` also resolves when the loop ends before the bookmark was entered, after a stop or a fatal error, and nothing is entered then.
484
489
  - Events: `flow:edge` `{ flow, node, outcome, payload, next, patches: { doc, session }, index, now }` after the commit of an edge. `flow:rest` `{ path, checkpoint }`. `flow:error` `{ path, error, rolledBackTo, retry }`.
485
490
  - Config: `mainFlow` default `undefined` and required before `run()`, `safeNode: string | undefined` default `undefined` which means the main flow's `start`, `retries` default `1`, `settleTimeoutMs` default `2000`, `journalLimit` default `500`, `holdMs` default `0`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moku-labs/game",
3
- "version": "0.14.9",
3
+ "version": "0.14.10",
4
4
  "description": "2D puzzle game engine on TypeScript and PixiJS, built on @moku-labs/core.",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -155,11 +155,11 @@
155
155
  "@arethetypeswrong/core": "0.18.3",
156
156
  "@biomejs/biome": "2.4.16",
157
157
  "@formatjs/icu-messageformat-parser": "3.5.20",
158
- "@moku-labs/ci": "1.5.5",
159
- "@moku-labs/common": "0.3.7",
160
- "@moku-labs/core": "1.7.5",
161
- "@moku-labs/native": "0.3.5",
162
- "@moku-labs/system": "0.3.4",
158
+ "@moku-labs/ci": "1.5.7",
159
+ "@moku-labs/common": "0.3.10",
160
+ "@moku-labs/core": "1.7.6",
161
+ "@moku-labs/native": "0.3.8",
162
+ "@moku-labs/system": "0.3.7",
163
163
  "@types/bun": "1.4.2",
164
164
  "@vitest/coverage-istanbul": "4.0.18",
165
165
  "eslint": "9.39.3",