@moku-labs/game 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +2 -0
  2. package/dist/assets.d.mts +181 -22
  3. package/dist/assets.mjs +1186 -160
  4. package/dist/{commands-C0BljsM5.d.mts → commands-DYO22lzi.d.mts} +22 -5
  5. package/dist/control.d.mts +3 -3
  6. package/dist/control.mjs +3 -3
  7. package/dist/{define-CjP_4H0G.d.mts → define-BKWjWgyQ.d.mts} +1 -1
  8. package/dist/dev-CQYJPmcD.mjs +1183 -0
  9. package/dist/flatten-BkXuscAV.mjs +100 -0
  10. package/dist/index.d.mts +85 -31
  11. package/dist/index.mjs +3208 -490
  12. package/dist/{session-DmAxY6Ll.mjs → input-CK4nxBO1.mjs} +41 -12
  13. package/dist/inspect.d.mts +32 -9
  14. package/dist/inspect.mjs +2 -2
  15. package/dist/jsx-dev-runtime.d.mts +1 -1
  16. package/dist/jsx-dev-runtime.mjs +1 -1
  17. package/dist/jsx-runtime.d.mts +1 -1
  18. package/dist/jsx-runtime.mjs +1 -1
  19. package/dist/{run-tlcn1w7y.mjs → run-BfuMLahc.mjs} +489 -213
  20. package/dist/{runtime-Bn5TXNxP.d.mts → runtime-Bhu442L8.d.mts} +1 -1
  21. package/dist/{runtime-DRlwxkIv.mjs → runtime-CBZgWgzx.mjs} +1 -57
  22. package/dist/{sources-BooL2MsX.mjs → sources-C1uGjz4l.mjs} +198 -21
  23. package/dist/testing.d.mts +5 -5
  24. package/dist/testing.mjs +5 -4
  25. package/dist/{types-CxZy7Pi4.d.mts → types-BnWH-75b.d.mts} +95 -21
  26. package/dist/{types-CqkrEY06.d.mts → types-CVUHPvMB.d.mts} +5 -434
  27. package/dist/{types-DmhTHRVW.d.mts → types-DtmFVCHf.d.mts} +1 -1
  28. package/dist/{types-BZ1OLf24.d.mts → types-Dy1xB68L.d.mts} +2318 -1169
  29. package/dist/{types-X9jXjka3.d.mts → types-VErbiEXy.d.mts} +867 -198
  30. package/dist/{visual-NPrDkvUk.mjs → visual-DUvvPPCt.mjs} +51 -1
  31. package/llms.txt +5 -5
  32. package/package.json +5 -5
  33. package/dist/component-DGg5DqKK.mjs +0 -44
  34. package/dist/dev-CcwFrwok.mjs +0 -413
  35. package/dist/types-C47fKNcn.d.mts +0 -695
@@ -10,7 +10,7 @@ import { AnyPluginInstance, PluginCtx } from "@moku-labs/core";
10
10
  * createApp({ config: { orientation: "portrait", referenceSide: 1080, referenceLong: 2100 } });
11
11
  * ```
12
12
  */
13
- type Config$7 = {
13
+ type Config$8 = {
14
14
  /** Screen orientation the game is designed for. */orientation: "portrait" | "landscape"; /** Short side of the reference resolution in pixels. */
15
15
  referenceSide: number;
16
16
  /**
@@ -29,7 +29,7 @@ type Config$7 = {
29
29
  /**
30
30
  * Global events. Empty: every event belongs to a plugin.
31
31
  */
32
- type Events$5 = Record<never, never>;
32
+ type Events$6 = Record<never, never>;
33
33
  /**
34
34
  * Public API type of a plugin instance, read from its phantom carrier.
35
35
  * Mirrors the kernel's non-exported `ExtractPluginApi`.
@@ -58,8 +58,8 @@ type Require = <Plugin extends {
58
58
  readonly events: Record<string, unknown>;
59
59
  };
60
60
  }>(plugin: Plugin) => ApiOf<Plugin>;
61
- declare namespace types_d_exports$6 {
62
- export { Api$6 as Api, ClockCtx, ClockSource, Config$6 as Config, Elapsed, FakeClock, State$6 as State };
61
+ declare namespace types_d_exports$7 {
62
+ export { Api$7 as Api, ClockCtx, ClockSource, Config$7 as Config, Elapsed, FakeClock, State$7 as State };
63
63
  }
64
64
  /**
65
65
  * Source of time and timers. The system source is the default; a game passes its own when time
@@ -160,7 +160,7 @@ type Elapsed = {
160
160
  * createApp({ pluginConfigs: { clock: { source: fakeClock(1000) } } });
161
161
  * ```
162
162
  */
163
- type Config$6 = {
163
+ type Config$7 = {
164
164
  /**
165
165
  * Time source. `undefined` means the system source: `Date.now` and `setTimeout`.
166
166
  */
@@ -169,7 +169,7 @@ type Config$6 = {
169
169
  /**
170
170
  * clock plugin state.
171
171
  */
172
- type State$6 = {
172
+ type State$7 = {
173
173
  source: ClockSource;
174
174
  last: number;
175
175
  dueAt: number | undefined;
@@ -190,7 +190,7 @@ type State$6 = {
190
190
  * app.clock.scheduleAt(nextRefillAt(app.clock.now()));
191
191
  * ```
192
192
  */
193
- type Api$6 = {
193
+ type Api$7 = {
194
194
  /**
195
195
  * Reads trusted time. It never decreases, whatever the device clock does, so a player who sets
196
196
  * the clock back gains nothing.
@@ -262,11 +262,11 @@ type Api$6 = {
262
262
  /**
263
263
  * Domain context of the clock plugin.
264
264
  */
265
- type ClockCtx = PluginCtx<Config$6, State$6> & {
265
+ type ClockCtx = PluginCtx<Config$7, State$7> & {
266
266
  readonly global: object;
267
267
  };
268
- declare namespace types_d_exports$5 {
269
- export { Api$5 as Api, Config$5 as Config, Events$4 as Events, LifecycleCtx, PauseReason, State$5 as State };
268
+ declare namespace types_d_exports$6 {
269
+ export { Api$6 as Api, Config$6 as Config, Events$5 as Events, LifecycleCtx, PauseReason, State$6 as State };
270
270
  }
271
271
  /**
272
272
  * Why the game is paused. The named reasons are the engine's own; any other string is allowed.
@@ -289,7 +289,7 @@ type PauseReason = "background" | "devtools" | "system-dialog" | "device-lost" |
289
289
  * };
290
290
  * ```
291
291
  */
292
- type Events$4 = {
292
+ type Events$5 = {
293
293
  /**
294
294
  * The pause stack changed. `reason` and `action` say what changed; `resumed` is true only on
295
295
  * the change that emptied the stack.
@@ -310,11 +310,11 @@ type Events$4 = {
310
310
  * const config: Config = {};
311
311
  * ```
312
312
  */
313
- type Config$5 = Record<string, never>;
313
+ type Config$6 = Record<string, never>;
314
314
  /**
315
315
  * lifecycle plugin state: the pause reasons in insertion order, no duplicates.
316
316
  */
317
- type State$5 = {
317
+ type State$6 = {
318
318
  reasons: PauseReason[];
319
319
  };
320
320
  /**
@@ -331,7 +331,7 @@ type State$5 = {
331
331
  * app.lifecycle.pop("ad"); // time resumes
332
332
  * ```
333
333
  */
334
- type Api$5 = {
334
+ type Api$6 = {
335
335
  /**
336
336
  * Pushes a reason why the game is paused. A reason already on the stack is ignored, so two
337
337
  * plugins pausing for the same reason never pause twice.
@@ -390,7 +390,7 @@ type Api$5 = {
390
390
  * `index.ts` writes `events` with an annotated `register` (core spec `14` row 8), so the own event
391
391
  * reaches a factory passed by direct reference.
392
392
  */
393
- type LifecycleCtx = PluginCtx<Config$5, State$5, Events$4> & {
393
+ type LifecycleCtx = PluginCtx<Config$6, State$6, Events$5> & {
394
394
  readonly require: Require;
395
395
  };
396
396
  //#endregion
@@ -960,8 +960,8 @@ declare class SaveUnreadableError extends Error {
960
960
  */
961
961
  constructor(savedVersion: number, schemaVersion: number, cause: unknown);
962
962
  }
963
- declare namespace types_d_exports$4 {
964
- export { Api$4 as Api, CommitResult, Config$4 as Config, Events$3 as Events, Json, Migration, ModelCtx, Patch, PlayerStateProvider, ProviderCall, RngApi, RngState, RngStream, RngView, Root, SaveDoc, Snapshot, State$4 as State, StoreApi, Transaction };
963
+ declare namespace types_d_exports$5 {
964
+ export { Api$5 as Api, CommitResult, Config$5 as Config, Events$4 as Events, Json, Migration, ModelCtx, Patch, PlayerStateProvider, ProviderCall, RngApi, RngState, RngStream, RngView, Root, SaveDoc, Snapshot, State$5 as State, StoreApi, Transaction };
965
965
  }
966
966
  /**
967
967
  * Plain JSON value. Everything in state is JSON.
@@ -992,7 +992,7 @@ type Root = "player" | "session" | "rng";
992
992
  * const payload: Events["model:committed"] = { roots: ["player", "rng"], cause: "edge" };
993
993
  * ```
994
994
  */
995
- type Events$3 = {
995
+ type Events$4 = {
996
996
  /** Committed state changed. Projections reconcile from the snapshot. */"model:committed": {
997
997
  roots: readonly Root[];
998
998
  cause: "edge" | "rollback" | "restore" | "load";
@@ -1006,7 +1006,7 @@ type Events$3 = {
1006
1006
  * createApp({ pluginConfigs: { model: { initialPlayer: { coins: 0 }, seed: 42 } } });
1007
1007
  * ```
1008
1008
  */
1009
- type Config$4 = {
1009
+ type Config$5 = {
1010
1010
  /** The save seam. `undefined`: an in-memory provider, nothing is persisted. */playerProvider: PlayerStateProvider | undefined; /** Player state of a new player. Deep-cloned. */
1011
1011
  initialPlayer: Json; /** Session state at every start. Deep-cloned. */
1012
1012
  initialSession: Json; /** "from-save": a new player gets a random seed once; a number fixes it (tests). */
@@ -1018,7 +1018,7 @@ type Config$4 = {
1018
1018
  * model plugin state: one branch per module.
1019
1019
  *
1020
1020
  */
1021
- type State$4 = {
1021
+ type State$5 = {
1022
1022
  store: StoreState;
1023
1023
  rng: Record<string, never>;
1024
1024
  };
@@ -1031,7 +1031,7 @@ type State$4 = {
1031
1031
  * app.model.rng.peek("chest:42"); // undefined: this chest was never opened
1032
1032
  * ```
1033
1033
  */
1034
- type Api$4 = {
1034
+ type Api$5 = {
1035
1035
  store: StoreApi;
1036
1036
  rng: RngApi;
1037
1037
  };
@@ -1040,12 +1040,12 @@ type Api$4 = {
1040
1040
  * an annotated `register` (core spec `14` row 8), so the own event reaches a factory passed by
1041
1041
  * direct reference.
1042
1042
  */
1043
- type ModelCtx = PluginCtx<Config$4, State$4, Events$3> & {
1043
+ type ModelCtx = PluginCtx<Config$5, State$5, Events$4> & {
1044
1044
  readonly global: object;
1045
1045
  readonly log: Log.LogApi;
1046
1046
  };
1047
- declare namespace types_d_exports$3 {
1048
- export { Api$3 as Api, Config$3 as Config, FrameCallback, Phase, State$3 as State, Time, TimeCtx };
1047
+ declare namespace types_d_exports$4 {
1048
+ export { Api$4 as Api, Config$4 as Config, FrameCallback, Phase, State$4 as State, Time, TimeCtx };
1049
1049
  }
1050
1050
  /**
1051
1051
  * Frame phases, in call order.
@@ -1089,7 +1089,7 @@ type FrameCallback = (time: Readonly<Time>) => void;
1089
1089
  * createApp({ pluginConfigs: { time: { maxFps: 30 } } });
1090
1090
  * ```
1091
1091
  */
1092
- type Config$3 = {
1092
+ type Config$4 = {
1093
1093
  /**
1094
1094
  * Frame rate cap. The default is 60. 120 is for WebViews that really deliver 120 Hz frames:
1095
1095
  * Android today; WKWebView on iOS and macOS is capped at 60 by WebKit (bug 294338).
@@ -1112,7 +1112,7 @@ type Config$3 = {
1112
1112
  /**
1113
1113
  * time plugin state.
1114
1114
  */
1115
- type State$3 = {
1115
+ type State$4 = {
1116
1116
  callbacks: Record<Phase, FrameCallback[]>;
1117
1117
  /**
1118
1118
  * Scratch array of a frame: the six callback lists as they were when the frame started.
@@ -1155,7 +1155,7 @@ type State$3 = {
1155
1155
  * off();
1156
1156
  * ```
1157
1157
  */
1158
- type Api$3 = {
1158
+ type Api$4 = {
1159
1159
  /**
1160
1160
  * Registers a frame callback. Callbacks of one phase run in registration order; a callback
1161
1161
  * registered during a frame runs from the next frame on.
@@ -1279,7 +1279,7 @@ type Api$3 = {
1279
1279
  /**
1280
1280
  * Domain context of the time plugin.
1281
1281
  */
1282
- type TimeCtx = PluginCtx<Config$3, State$3> & {
1282
+ type TimeCtx = PluginCtx<Config$4, State$4> & {
1283
1283
  readonly global: object;
1284
1284
  readonly log: Log.LogApi;
1285
1285
  };
@@ -2723,8 +2723,8 @@ declare function to<const TargetName extends string, Payload, Output>(target: Ta
2723
2723
  * ```
2724
2724
  */
2725
2725
  declare function slot(name: string): SlotNode;
2726
- declare namespace types_d_exports$2 {
2727
- export { Allow, Answer, AnyFlow, AnyNode, AnyNodeContext, Api$2 as Api, Bookmark, BundlesOf, CheckedEdges, Config$2 as Config, Contribution, DefineNode, Deps$2 as Deps, Descriptor, Edges, EmitterIdOf, EnterCallback, Events$2 as Events, Exit, FeatureDescription, FeatureEmitter, FeatureFilter, FeaturePlugin, FeaturesApi, FlowCtx, FlowDefinition, FlowGraph, FlowKit, FlowSpec, FlowState, FxApi, FxHandler, GameState, GameTypes, GateApi, GraphError, GraphNode, GuideOptions, Hint, InboxApi, JournalEntry, KernelSlice$2 as KernelSlice, LifecycleChanged, Mapped, NodeContext, NodeDefinition, NodeFx, NodeInfo, NodeSpec, OutcomeTags, Position, Result, RouteStep, RunnerApi, SceneIdOf, SlotNode, Stage, State$2 as State, StringTableLike, Target, TextStylesOf, TypeTag, WorldEvent };
2726
+ declare namespace types_d_exports$3 {
2727
+ export { Allow, Answer, AnyFlow, AnyNode, AnyNodeContext, Api$3 as Api, Bookmark, BundlesOf, CheckedEdges, Config$3 as Config, Contribution, DefineNode, Deps$3 as Deps, Descriptor, Edges, EmitterIdOf, EnterCallback, Events$3 as Events, Exit, FeatureDescription, FeatureEmitter, FeatureFilter, FeaturePlugin, FeaturesApi, FlowCtx, FlowDefinition, FlowGraph, FlowKit, FlowSpec, FlowState, FxApi, FxHandler, GameState, GameTypes, GateApi, GraphError, GraphNode, GuideOptions, Hint, InboxApi, JournalEntry, KernelSlice$3 as KernelSlice, LifecycleChanged, Mapped, NodeContext, NodeDefinition, NodeFx, NodeInfo, NodeSpec, OutcomeTags, Position, Result, RouteStep, RunnerApi, SceneIdOf, SlotNode, Stage, State$3 as State, StringTableLike, Target, TextStylesOf, TypeTag, WorldEvent };
2728
2728
  }
2729
2729
  /**
2730
2730
  * flow plugin events.
@@ -2738,7 +2738,7 @@ declare namespace types_d_exports$2 {
2738
2738
  * }); // the play button on "home" logs { node: "home", outcome: "play" }
2739
2739
  * ```
2740
2740
  */
2741
- type Events$2 = {
2741
+ type Events$3 = {
2742
2742
  /** An edge was taken and its state committed. */"flow:edge": {
2743
2743
  flow: string;
2744
2744
  node: string;
@@ -2771,7 +2771,7 @@ type Events$2 = {
2771
2771
  * createApp({ pluginConfigs: { flow: { mainFlow, safeNode: "home" } } });
2772
2772
  * ```
2773
2773
  */
2774
- type Config$2 = {
2774
+ type Config$3 = {
2775
2775
  /** The top-level flow. Required before `run()`. */mainFlow: AnyFlow | undefined; /** Path of the checkpoint entered after a failed retry. `undefined`: the main flow's `start`. */
2776
2776
  safeNode: string | undefined; /** Retries of a failed transition before going to `safeNode`. */
2777
2777
  retries: number; /** How long `onStop` waits for the active node to settle after abort, in real milliseconds. */
@@ -2781,7 +2781,7 @@ type Config$2 = {
2781
2781
  /**
2782
2782
  * flow plugin state: one branch per module.
2783
2783
  */
2784
- type State$2 = {
2784
+ type State$3 = {
2785
2785
  features: FeaturesState;
2786
2786
  fx: FxState;
2787
2787
  gate: GateState;
@@ -2800,7 +2800,7 @@ type State$2 = {
2800
2800
  * app.flow.inbox.post({ type: "purchased" }); // the shop SDK: waits for a node that lists it
2801
2801
  * ```
2802
2802
  */
2803
- type Api$2 = RunnerApi & {
2803
+ type Api$3 = RunnerApi & {
2804
2804
  gate: GateApi;
2805
2805
  inbox: InboxApi;
2806
2806
  fx: FxApi;
@@ -2810,17 +2810,17 @@ type Api$2 = RunnerApi & {
2810
2810
  * The APIs flow requires from below (`time`, `model`, `clock`), resolved once in `onInit` and
2811
2811
  * shared by the runner, gate, inbox and fx.
2812
2812
  */
2813
- type Deps$2 = {
2814
- time: Api$3;
2815
- model: Api$4;
2816
- clock: Api$6;
2813
+ type Deps$3 = {
2814
+ time: Api$4;
2815
+ model: Api$5;
2816
+ clock: Api$7;
2817
2817
  };
2818
2818
  /**
2819
2819
  * What the kernel context offers before the deps are attached. `emit` is the kernel's: `index.ts`
2820
2820
  * writes `events` with an annotated `register` (core spec `14` row 8), so the own events reach a
2821
2821
  * factory passed by direct reference (`api`, `hooks`, `onInit`, `onStart`).
2822
2822
  */
2823
- type KernelSlice$2 = PluginCtx<Config$2, State$2, Events$2> & {
2823
+ type KernelSlice$3 = PluginCtx<Config$3, State$3, Events$3> & {
2824
2824
  readonly global: object;
2825
2825
  readonly log: Log.LogApi;
2826
2826
  readonly require: Require;
@@ -2828,13 +2828,13 @@ type KernelSlice$2 = PluginCtx<Config$2, State$2, Events$2> & {
2828
2828
  /**
2829
2829
  * Domain context shared by the modules.
2830
2830
  */
2831
- type FlowCtx = KernelSlice$2 & {
2832
- readonly deps: Deps$2;
2831
+ type FlowCtx = KernelSlice$3 & {
2832
+ readonly deps: Deps$3;
2833
2833
  };
2834
2834
  /**
2835
2835
  * Payload of the one event flow listens to.
2836
2836
  */
2837
- type LifecycleChanged = Events$4["lifecycle:changed"];
2837
+ type LifecycleChanged = Events$5["lifecycle:changed"];
2838
2838
  /**
2839
2839
  * The bound of the `strings` slot of `GameTypes`: the key-to-parameters table every game's
2840
2840
  * generated `strings.ts` exports, whatever its keys. `i18n` reads the keys and the parameters;
@@ -2947,1608 +2947,1801 @@ type Position = {
2947
2947
  waiting: readonly string[];
2948
2948
  };
2949
2949
  //#endregion
2950
- //#region src/plugins/world/ecs/types.d.ts
2950
+ //#region src/plugins/world/projection/types.d.ts
2951
2951
  /**
2952
- * Entity id: `generation * 2 ** 20 + index`, a safe integer. A freed index comes back with a
2953
- * higher generation, so an id is never silently reused.
2952
+ * How a layer orders the entities drawn in it.
2954
2953
  *
2955
2954
  * @example
2956
2955
  * ```ts
2957
- * const entity: Entity = 1_048_576; // generation 1, index 0
2956
+ * const sort: LayerSort = "y";
2958
2957
  * ```
2959
2958
  */
2960
- type Entity = number;
2959
+ type LayerSort = "none" | "y" | "order";
2961
2960
  /**
2962
- * Who owns an entity. Every `spawn` names one, so `despawnOwnedBy` can free exactly one owner's
2963
- * entities and `snapshot()` can say where an entity came from.
2961
+ * One named layer of a scene. The order of the list is draw order.
2964
2962
  *
2965
2963
  * @example
2966
2964
  * ```ts
2967
- * const owner: Owner = { kind: "projection", name: "board.items" };
2965
+ * const layer: LayerSpec = { name: "items", sort: "y" };
2968
2966
  * ```
2969
2967
  */
2970
- type Owner = {
2971
- kind: "projection" | "node" | "plugin";
2968
+ type LayerSpec = {
2972
2969
  name: string;
2970
+ sort: LayerSort;
2973
2971
  };
2974
2972
  /**
2975
- * One entity as `snapshot()` reads it: its id and the two halves of the id, its owner, every
2976
- * component value that is plain JSON, and the names of the ones that are not.
2973
+ * Easing of a tween: one of the named curves the driver knows, or a function that gets the
2974
+ * normalised time and returns the eased fraction.
2977
2975
  *
2978
2976
  * @example
2979
2977
  * ```ts
2980
- * const entity: EntitySnapshot = {
2981
- * id: 1_048_576, index: 0, generation: 1, owner: { kind: "projection", name: "board.items" },
2982
- * components: { Layer: { name: "items" } }, skipped: ["Display"]
2983
- * };
2978
+ * const ease: Ease = "outBack";
2984
2979
  * ```
2985
2980
  */
2986
- type EntitySnapshot = {
2987
- id: Entity;
2988
- index: number;
2989
- generation: number;
2990
- owner: Owner;
2991
- components: Record<string, Json>;
2992
- skipped: string[];
2993
- };
2981
+ type Ease = "linear" | "in" | "out" | "inOut" | "inCubic" | "outCubic" | "inOutCubic" | "inBack" | "outBack" | ((t: number) => number);
2994
2982
  /**
2995
- * The whole world as plain JSON: the effective mode, every entity sorted by index and every
2996
- * resource that is JSON.
2983
+ * What every motion returns: it can be finished, cancelled and asked whether it still runs.
2984
+ * V3 `anim` hands out the same shape.
2997
2985
  *
2998
2986
  * @example
2999
2987
  * ```ts
3000
- * const empty: WorldSnapshot = { mode: "live", entities: [], resources: {} };
2988
+ * const handle: MotionHandle = view.toRest(Transform);
2989
+ * handle.active(); // true until the last frame wrote the exact target
3001
2990
  * ```
3002
2991
  */
3003
- type WorldSnapshot = {
3004
- mode: WorldMode;
3005
- entities: EntitySnapshot[];
3006
- resources: Record<string, Json>;
2992
+ type MotionHandle = {
2993
+ finish(): void;
2994
+ cancel(): void;
2995
+ active(): boolean;
3007
2996
  };
3008
2997
  /**
3009
- * The four phases systems run in, in frame order.
2998
+ * What a motion hook returns. `void` means the hook wrote the component directly.
3010
2999
  *
3011
3000
  * @example
3012
3001
  * ```ts
3013
- * const phase: WorldPhase = "animate";
3002
+ * const motion: Motion = undefined; // the hook called view.set and is already done
3014
3003
  * ```
3015
3004
  */
3016
- type WorldPhase = "input" | "animate" | "layout" | "sync";
3005
+ type Motion = MotionHandle | void;
3017
3006
  /**
3018
- * World mode. `"paused"` and `"fast"` skip the phases `input`, `animate` and `layout`; phase
3019
- * `sync` runs in every mode.
3007
+ * Only the numeric fields of a component value: what a tween can interpolate.
3020
3008
  *
3021
3009
  * @example
3022
3010
  * ```ts
3023
- * const mode: WorldMode = "fast";
3011
+ * type Numbers = NumericFields<{ x: number; texture: string }>; // { x: number }
3024
3012
  * ```
3025
3013
  */
3026
- type WorldMode = "live" | "paused" | "fast";
3014
+ type NumericFields<Value extends object> = { [Key in keyof Value as Value[Key] extends number ? Key : never]: Value[Key] };
3027
3015
  /**
3028
- * One component value ready to be written: the storage key and the frozen data.
3016
+ * One keyframe segment of a track: the fraction of the track's `ms` it ends at (`0..1`), the
3017
+ * curve it eases with (the track's `ease` when left out) and the numeric fields it reaches. A
3018
+ * field it does not name holds where the segment before left it.
3029
3019
  *
3030
3020
  * @example
3031
3021
  * ```ts
3032
- * const value: ComponentValue<{ level: number }> = { type: Item, value: { level: 2 } };
3022
+ * // The swing of a popup board: past the rest pose at 42 % of the track, then home.
3023
+ * const segments: TrackSegment[] = [
3024
+ * { at: 0.42, ease: "out", to: { y: 974, rotation: 0.087 } },
3025
+ * { at: 1, ease: "inOut", to: { y: 960, rotation: 0 } }
3026
+ * ];
3033
3027
  * ```
3034
3028
  */
3035
- type ComponentValue<Value extends object> = {
3036
- readonly type: ComponentType<Value>;
3037
- readonly value: Readonly<Value>;
3029
+ type TrackSegment = {
3030
+ at: number;
3031
+ ease?: Ease;
3032
+ to: Record<string, number>;
3038
3033
  };
3039
3034
  /**
3040
- * A tag value. A tag carries no data and is stored as `true`.
3035
+ * Options of `ViewHandle.tween`. An additive tween adds its delta over the value the absolute
3036
+ * writer of the field holds, instead of owning the field. `segments` turns the tween into a
3037
+ * keyframe walk: the segments run one after another over `ms` on one clock, and `to` is where the
3038
+ * last one ends. `repeat` runs the walk again from its first segment when it ends: a number
3039
+ * counts the extra runs, `"forever"` never ends. The world passes it to the driver unchanged;
3040
+ * without a driver the target is written once.
3041
3041
  *
3042
3042
  * @example
3043
3043
  * ```ts
3044
- * const held: TagValue = { type: Held, value: true };
3044
+ * const options: TweenOptions = { ms: 350, ease: "out", delayMs: 40 };
3045
+ * const walk: TweenOptions = { ms: 1000, segments: [{ at: 0.42, to: { y: 974 } }, { at: 1, to: { y: 960 } }] };
3046
+ * const sway: TweenOptions = { ms: 2400, additive: true, repeat: "forever", segments: [{ at: 0.5, to: { rotation: 0.05 } }, { at: 1, to: { rotation: 0 } }] };
3045
3047
  * ```
3046
3048
  */
3047
- type TagValue = {
3048
- readonly type: TagType;
3049
- readonly value: true;
3049
+ type TweenOptions = {
3050
+ ms: number;
3051
+ ease?: Ease;
3052
+ delayMs?: number;
3053
+ additive?: boolean;
3054
+ segments?: readonly TrackSegment[];
3055
+ repeat?: number | "forever";
3050
3056
  };
3051
3057
  /**
3052
- * Any component or tag value, as `spawn` and `view` return them.
3058
+ * Options of `ViewHandle.toRest`. Defaults: the plugin's `settleMs` and ease `"out"`. `delayMs`
3059
+ * lets a hook wait, for example for a flight to land, before the view comes home.
3053
3060
  *
3054
3061
  * @example
3055
3062
  * ```ts
3056
- * const values: AnyComponentValue[] = [Held()];
3063
+ * const options: RestOptions = { ms: 200, delayMs: 120 };
3057
3064
  * ```
3058
3065
  */
3059
- type AnyComponentValue = {
3060
- readonly type: AnyComponentType;
3061
- readonly value: object | true;
3066
+ type RestOptions = {
3067
+ ms?: number;
3068
+ ease?: Ease;
3069
+ delayMs?: number;
3062
3070
  };
3063
3071
  /**
3064
- * A component type whose value shape is erased to a plain record: what the projection writes when
3065
- * it only knows the component by name.
3072
+ * What the driver is told about one track: how long it runs, how it eases, how long it waits,
3073
+ * whether it adds to the field or owns it, and the keyframe segments it walks. A track with
3074
+ * segments walks them over `ms` on one clock and claims every field a segment names; its target
3075
+ * `to` is the last segment's target. A track with `repeat` starts its walk again when it ends
3076
+ * (a number counts the extra runs, `"forever"` never ends); the instant driver ignores it and
3077
+ * writes the end pose once.
3066
3078
  *
3067
3079
  * @example
3068
3080
  * ```ts
3069
- * const erased: AnyComponent = component("Transform", { x: 0, y: 0 }) as AnyComponent;
3070
- * erased.componentName; // "Transform"
3081
+ * const options: TrackOptions = { ms: 250, ease: "outBack", delayMs: 0, additive: false };
3082
+ * const walk: TrackOptions = { ms: 1000, segments: [{ at: 0.42, to: { y: 974 } }, { at: 1, to: { y: 960 } }] };
3083
+ * const twice: TrackOptions = { ms: 400, ease: "inOut", delayMs: 0, additive: true, repeat: 1 };
3071
3084
  * ```
3072
3085
  */
3073
- type AnyComponent = ComponentType<Record<string, unknown>>;
3086
+ type TrackOptions = {
3087
+ ms: number;
3088
+ ease?: Ease;
3089
+ delayMs?: number;
3090
+ additive?: boolean;
3091
+ segments?: readonly TrackSegment[];
3092
+ repeat?: number | "forever";
3093
+ };
3074
3094
  /**
3075
- * A component type made by `component()`: callable to build a value, and carrying the storage
3076
- * name plus the defaults that give the TypeScript type, the JSON shape and the inspector schema.
3095
+ * The tween engine behind `ViewHandle.tween`, `toRest` and `all`. `anim` installs one with
3096
+ * `setDriver`; without it the world writes every target at once.
3097
+ *
3098
+ * A track reads its start values when its delay ends, writes the exact target on its last frame
3099
+ * and goes through `ecs.set`, so `changed()` sees it. Muted fields are never written, also not on
3100
+ * `finish()`.
3077
3101
  *
3078
3102
  * @example
3079
3103
  * ```ts
3080
- * const Item: ComponentType<{ kind: string; level: number }> = component("Item", {
3081
- * kind: "",
3082
- * level: 1
3083
- * });
3084
- * Item({ level: 2 }); // { type: Item, value: { kind: "", level: 2 } }
3085
- * Item.owned; // []: a projection view writes every field
3104
+ * // `anim` hands the world its own tween core in `onStart`.
3105
+ * const driver: TweenDriver = { track: startTrack, cancelAll: cancelTracksOf };
3106
+ * const off = ctx.require(worldPlugin).projection.setDriver(driver);
3107
+ *
3108
+ * off(); // `anim` stopped: the world writes instantly again
3086
3109
  * ```
3087
3110
  */
3088
- type ComponentType<Value extends object> = {
3089
- (patch?: Partial<Value>): ComponentValue<Value>;
3090
- readonly componentName: string;
3091
- readonly defaults: Readonly<Value>;
3092
- readonly kind: "component"; /** Fields a plugin writes and a projection view never does, so a view never corrects them. */
3093
- readonly owned: readonly string[];
3111
+ type TweenDriver = {
3112
+ /**
3113
+ * Starts one track on one component of one entity.
3114
+ *
3115
+ * @param entity - The entity to animate.
3116
+ * @param component - The component to animate.
3117
+ * @param to - The numeric target fields.
3118
+ * @param options - Duration, easing, delay and the additive flag.
3119
+ * @param muted - The fields another writer owns, read at every write.
3120
+ * @returns The motion handle of the track.
3121
+ */
3122
+ track(entity: Entity, component: AnyComponent, to: Record<string, number>, options: TrackOptions, muted: () => ReadonlySet<string>): MotionHandle;
3123
+ /**
3124
+ * Ends every track of one entity. Called when a view despawns and on every flush.
3125
+ *
3126
+ * @param entity - The entity whose tracks end.
3127
+ */
3128
+ cancelAll(entity: Entity): void;
3094
3129
  };
3095
3130
  /**
3096
- * A tag type made by `tag()`: a component with no data.
3131
+ * One node of an element description, as `ui` builds it. The world never imports `ui`: it knows
3132
+ * the shape structurally, carries it in `Tree` and hands it back unchanged.
3097
3133
  *
3098
3134
  * @example
3099
3135
  * ```ts
3100
- * const Held: TagType = tag("Held");
3101
- * Held(); // { type: Held, value: true }
3136
+ * const node: DescriptionNode = { type: "box", props: { gap: 8 }, children: [] };
3102
3137
  * ```
3103
3138
  */
3104
- type TagType = {
3105
- (): TagValue;
3106
- readonly componentName: string;
3107
- readonly kind: "tag";
3139
+ type DescriptionNode = {
3140
+ type: string;
3141
+ key?: string;
3142
+ props: object;
3143
+ children: readonly unknown[];
3108
3144
  };
3109
3145
  /**
3110
- * What the world stores about a component or tag type, independent of its data type.
3146
+ * What one `view(item)` call returns: the components of the item, or one description node that
3147
+ * the world wraps as `Tree`.
3111
3148
  *
3112
3149
  * @example
3113
3150
  * ```ts
3114
- * const registered: AnyComponentType = { componentName: "Held", kind: "tag" };
3151
+ * const components: ViewOutput = [Level({ level: 2 })];
3115
3152
  * ```
3116
3153
  */
3117
- type AnyComponentType = {
3118
- readonly componentName: string;
3119
- readonly kind: "component" | "tag";
3120
- };
3154
+ type ViewOutput = readonly AnyComponentValue[] | DescriptionNode;
3121
3155
  /**
3122
- * What the ECS needs from a component type to read and write its store: the name and the
3123
- * defaults, never the call signature. A component bound to a game's keys by a `…For` binder
3124
- * (`Narrowed`) is the same object with a narrower call, so it passes here unchanged.
3156
+ * What a motion hook is handed: the one view it animates. A view never holds a reference to
3157
+ * another view — `peer` answers with the other key's item, never with its entity.
3125
3158
  *
3126
3159
  * @example
3127
3160
  * ```ts
3128
- * const ref: ComponentHandle<{ x: number }> = component("Pos", { x: 0 });
3129
- * ref.defaults; // { x: 0 }
3161
+ * const enter = (view: ViewHandle<Item>, item: Item): Motion => {
3162
+ * view.set(Transform, { scale: 0 });
3163
+ * return view.toRest(Transform, { ms: 250 });
3164
+ * };
3130
3165
  * ```
3131
3166
  */
3132
- type ComponentHandle<Value extends object> = {
3133
- readonly componentName: string;
3134
- readonly defaults: Readonly<Value>;
3135
- readonly kind: "component";
3167
+ type ViewHandle<Item> = {
3168
+ readonly entity: Entity;
3169
+ readonly key: string;
3170
+ get<Value extends object>(component: ComponentType<Value>): Readonly<Value> | undefined;
3171
+ rest<Value extends object>(component: ComponentType<Value>): Readonly<Value> | undefined;
3172
+ set<Value extends object>(component: ComponentType<Value>, patch: Partial<Value>): void;
3173
+ tween<Value extends object>(component: ComponentType<Value>, to: Partial<NumericFields<Value>>, options: TweenOptions): MotionHandle;
3174
+ toRest<Value extends object>(component: ComponentType<Value>, options?: RestOptions): MotionHandle;
3175
+ all(handles: readonly Motion[]): MotionHandle;
3176
+ peer(key: string): Item | undefined;
3136
3177
  };
3137
3178
  /**
3138
- * A component type whose call signature takes a narrower patch: the same object, the same
3139
- * store, the compiler refuses what the game's generated keys do not know.
3179
+ * One `motion.change` hook, keyed by component name. `previous` and `next` are both passed, so a
3180
+ * rollback that lowers a level can pick another motion than a level-up.
3140
3181
  *
3141
3182
  * @example
3142
3183
  * ```ts
3143
- * const Sprite: Narrowed<{ texture: string }, { texture?: "board.cell" }> = component("Sprite", { texture: "" });
3144
- * Sprite({ texture: "board.cell" }).value.texture; // "board.cell"
3184
+ * const slide: ChangeHook<Item> = view => view.toRest(Transform, { ms: 350 });
3145
3185
  * ```
3146
3186
  */
3147
- type Narrowed<Value extends object, Patch> = Omit<ComponentType<Value>, never> & ((patch?: Patch) => ComponentValue<Value>);
3187
+ type ChangeHook<Item> = (view: ViewHandle<Item>, previous: Item, next: Item, hint?: Hint) => Motion;
3148
3188
  /**
3149
- * A resource type made by `resource()`: one mutable object per world, created from a deep clone
3150
- * of the defaults on first read.
3189
+ * The `change` table of a projection: one hook per component name.
3151
3190
  *
3152
3191
  * @example
3153
3192
  * ```ts
3154
- * const Pointer: ResourceType<{ x: number; y: number }> = resource("Pointer", { x: 0, y: 0 });
3155
- * Pointer.resourceName; // "Pointer"
3193
+ * const change: ChangeHooks<Item> = { Transform: view => view.toRest(Transform) };
3156
3194
  * ```
3157
3195
  */
3158
- type ResourceType<Value extends object> = {
3159
- readonly resourceName: string;
3160
- readonly defaults: Readonly<Value>;
3196
+ type ChangeHooks<Item> = {
3197
+ readonly [component: string]: ChangeHook<Item>;
3161
3198
  };
3162
3199
  /**
3163
- * A query term marked as written by `mut()`. Its only effect is change detection.
3200
+ * The motion hooks of a projection. Every one is optional; without a hook the component diff is
3201
+ * written directly. `loop` starts what moves a view for its whole life (an additive sway, a spin):
3202
+ * it is played wherever `enter` plays, next to it, and its motion is kept apart from the view's
3203
+ * other motions, so a change never cancels it; it ends when the view dies.
3164
3204
  *
3165
3205
  * @example
3166
3206
  * ```ts
3167
- * const written: Mut<{ x: number }> = mut(Transform);
3168
- * written.kind; // "mut"
3207
+ * const motion: ProjectionMotion<Item> = {
3208
+ * change: { Transform: view => view.toRest(Transform, { ms: 350 }) }
3209
+ * };
3210
+ * // An order card that sways forever once it entered.
3211
+ * const swaying: ProjectionMotion<Item> = {
3212
+ * loop: view =>
3213
+ * view.tween(Transform, { rotation: 0.05 }, { ms: 1200, additive: true, repeat: "forever" })
3214
+ * };
3169
3215
  * ```
3170
3216
  */
3171
- type Mut<Value extends object> = {
3172
- readonly kind: "mut";
3173
- readonly of: ComponentHandle<Value>;
3217
+ type ProjectionMotion<Item> = {
3218
+ enter?(view: ViewHandle<Item>, item: Item, hint?: Hint): Motion;
3219
+ loop?(view: ViewHandle<Item>): Motion;
3220
+ exit?(view: ViewHandle<Item>, item: Item, hint?: Hint): Motion;
3221
+ readonly change?: ChangeHooks<Item>;
3222
+ settle?(view: ViewHandle<Item>, components: readonly string[]): Motion;
3174
3223
  };
3175
3224
  /**
3176
- * A `mut()` term with its data type erased, as the world stores it.
3225
+ * The motion hooks with the item type erased, as the world stores them.
3177
3226
  *
3178
3227
  * @example
3179
3228
  * ```ts
3180
- * const term: AnyMut = { kind: "mut", of: { componentName: "Transform", kind: "component" } };
3229
+ * const stored: AnyProjectionMotion = { change: {} };
3181
3230
  * ```
3182
3231
  */
3183
- type AnyMut = {
3184
- readonly kind: "mut";
3185
- readonly of: AnyComponentType;
3232
+ type AnyProjectionMotion = {
3233
+ enter?(view: ViewHandle<unknown>, item: unknown, hint?: Hint): Motion;
3234
+ loop?(view: ViewHandle<unknown>): Motion;
3235
+ exit?(view: ViewHandle<unknown>, item: unknown, hint?: Hint): Motion;
3236
+ readonly change?: ChangeHooks<never>;
3237
+ settle?(view: ViewHandle<unknown>, components: readonly string[]): Motion;
3186
3238
  };
3187
3239
  /**
3188
- * One term of a query: a component type, a tag type, or a `mut()` of a component type.
3240
+ * One projection: keyed items of the model turned into entities. `Item` is inferred from the
3241
+ * return of `from`; `layer` and `lift` keep their literal types so a scene can check them.
3189
3242
  *
3190
3243
  * @example
3191
3244
  * ```ts
3192
- * const terms: QueryTerm[] = [mut(Transform), Sprite, Held];
3245
+ * const spec: ProjectionSpec<Item, "items", "lifted", Player, Session> = {
3246
+ * name: "board.items",
3247
+ * layer: "items",
3248
+ * lift: "lifted",
3249
+ * from: player => player.board.items,
3250
+ * key: item => item.id,
3251
+ * view: item => [Item({ level: item.level })]
3252
+ * };
3193
3253
  * ```
3194
3254
  */
3195
- type QueryTerm = AnyComponentType | AnyMut;
3255
+ type ProjectionSpec<Item, LayerName extends string, LiftName extends string, Player, Session> = {
3256
+ readonly name: string;
3257
+ readonly layer: LayerName;
3258
+ readonly lift?: LiftName;
3259
+ from(player: Player, session: Session): readonly Item[] | Item; /** Omitted when `from` returns one plain object: the key is then the name of the projection. */
3260
+ key?(item: Item): string;
3261
+ view(item: Item): ViewOutput;
3262
+ readonly motion?: ProjectionMotion<Item>;
3263
+ };
3196
3264
  /**
3197
- * The value a query yields for one term: writable for `mut()`, `true` for a tag, read-only for a
3198
- * plain component.
3265
+ * A projection spec with its item and layer types erased, as the world stores it.
3199
3266
  *
3200
3267
  * @example
3201
3268
  * ```ts
3202
- * type Written = ValueOf<Mut<{ x: number }>>; // { x: number }
3269
+ * const stored: AnyProjectionSpec = {
3270
+ * name: "board.items",
3271
+ * layer: "items",
3272
+ * from: () => [],
3273
+ * key: () => "",
3274
+ * view: () => []
3275
+ * };
3203
3276
  * ```
3204
3277
  */
3205
- type ValueOf<Term> = Term extends Mut<infer Value> ? Value : Term extends TagType ? true : Term extends ComponentHandle<infer Value> ? Readonly<Value> : never;
3278
+ type AnyProjectionSpec = {
3279
+ readonly name: string;
3280
+ readonly layer: string;
3281
+ readonly lift?: string;
3282
+ from(player: Json, session: Json): unknown;
3283
+ key?(item: unknown): string;
3284
+ view(item: unknown): ViewOutput;
3285
+ readonly motion?: AnyProjectionMotion;
3286
+ };
3206
3287
  /**
3207
- * The value part of a query row, one entry per term, in term order.
3288
+ * Why a reconcile runs. Merged over the frame; the cause table decides play or direct.
3208
3289
  *
3209
3290
  * @example
3210
3291
  * ```ts
3211
- * type Values = QueryValues<[TagType]>; // [true]
3292
+ * const cause: Cause = "edge";
3212
3293
  * ```
3213
3294
  */
3214
- type QueryValues<Terms extends readonly QueryTerm[]> = { -readonly [Index in keyof Terms]: ValueOf<Terms[Index]> };
3295
+ type Cause = "edge" | "rollback" | "restore" | "load" | "mount" | "rerun";
3215
3296
  /**
3216
- * One row a query yields: the entity, then one value per term.
3297
+ * One live or exiting view: the entity of one key of one projection, its rest pose and the
3298
+ * motions that still run on it.
3217
3299
  *
3218
3300
  * @example
3219
3301
  * ```ts
3220
- * type Row = QueryTuple<[ComponentType<{ x: number }>]>; // [Entity, Readonly<{ x: number }>]
3302
+ * const view: View = {
3303
+ * entity: 1_048_576,
3304
+ * projection: "board.items",
3305
+ * key: "i7",
3306
+ * item: { id: "i7" },
3307
+ * rest: new Map(),
3308
+ * handles: [],
3309
+ * lifted: false,
3310
+ * dropWhenStill: false,
3311
+ * exiting: false
3312
+ * };
3221
3313
  * ```
3222
3314
  */
3223
- type QueryTuple<Terms extends readonly QueryTerm[]> = [Entity, ...QueryValues<Terms>];
3315
+ type View$1 = {
3316
+ entity: Entity;
3317
+ projection: string;
3318
+ key: string;
3319
+ item: unknown; /** The last output of `view(item)`, by component name: what the picture equals at rest. */
3320
+ rest: Map<string, AnyComponentValue>;
3321
+ handles: MotionHandle[];
3322
+ lifted: boolean; /** `lift(false)` is waiting for the last motion to end. */
3323
+ dropWhenStill: boolean;
3324
+ exiting: boolean;
3325
+ };
3224
3326
  /**
3225
- * What a system receives next to its entities: the world, the mutable resources, the frozen model
3226
- * snapshot of the frame and the frame's `Time`. A system may read pure game rules; it never
3227
- * writes the model.
3327
+ * One mounted projection: its owner, the live views by key, the despawn queue and the items of
3328
+ * the last reconcile.
3228
3329
  *
3229
3330
  * @example
3230
3331
  * ```ts
3231
- * const run = (entities: Iterable<[Entity]>, ctx: SystemContext): void => {
3232
- * ctx.res(Pointer).x = 0;
3233
- * ctx.time.delta; // 16
3332
+ * const mounted: Mounted = {
3333
+ * owner: { kind: "projection", name: "board.items" },
3334
+ * live: new Map(),
3335
+ * queue: [],
3336
+ * items: new Map()
3234
3337
  * };
3235
3338
  * ```
3236
3339
  */
3237
- type SystemContext = {
3238
- readonly world: EcsApi;
3239
- res<Value extends object>(resource: ResourceType<Value>): Value;
3240
- readonly snapshot: Snapshot;
3241
- readonly time: Readonly<Time>;
3340
+ type Mounted = {
3341
+ owner: Owner;
3342
+ live: Map<string, View$1>;
3343
+ queue: View$1[];
3344
+ items: Map<string, unknown>;
3242
3345
  };
3243
3346
  /**
3244
- * A system definition, typed over its query tuple.
3347
+ * One track the projection started on the driver. The world keeps the component name and the
3348
+ * handle, so it knows which components are driven without knowing how the driver runs them.
3245
3349
  *
3246
3350
  * @example
3247
3351
  * ```ts
3248
- * const bob: SystemDefinition<[ComponentType<{ y: number }>]> = {
3249
- * name: "bob",
3250
- * phase: "animate",
3251
- * query: [Transform],
3252
- * run: entities => {
3253
- * for (const [, transform] of entities) transform.y; // read-only
3254
- * }
3255
- * };
3352
+ * const track: Track = { entity: 1_048_576, component: "Transform", handle: flight };
3353
+ * track.handle.active(); // true while the flight runs
3256
3354
  * ```
3257
3355
  */
3258
- type SystemDefinition<Terms extends readonly QueryTerm[]> = {
3259
- readonly name: string;
3260
- readonly phase: WorldPhase;
3261
- readonly query: Terms;
3262
- run(entities: Iterable<QueryTuple<Terms>>, ctx: SystemContext): void;
3356
+ type Track = {
3357
+ entity: Entity;
3358
+ component: string;
3359
+ handle: MotionHandle;
3263
3360
  };
3264
3361
  /**
3265
- * A system with its query tuple erased, as the world stores it.
3362
+ * The rest pose of one entity, by component name: the last output of `view(item)` for a view, or
3363
+ * what `setRest` recorded for an element another plugin owns.
3364
+ */
3365
+ type RestPose = Map<string, AnyComponentValue>;
3366
+ /**
3367
+ * Where a keyed element lives: a plugin above registered it with `registerKey`, so `entityOf`
3368
+ * and `keyOf` answer for it next to the views.
3266
3369
  *
3267
3370
  * @example
3268
3371
  * ```ts
3269
- * const entry: AnySystem = { name: "bob", phase: "animate", query: [], run: () => {} };
3372
+ * const key: ProjectionKey = { projection: "hud", key: "coins" };
3270
3373
  * ```
3271
3374
  */
3272
- type AnySystem = {
3273
- readonly name: string;
3274
- readonly phase: WorldPhase;
3275
- readonly query: readonly QueryTerm[];
3276
- run(entities: Iterable<readonly unknown[]>, ctx: SystemContext): void;
3375
+ type ProjectionKey = {
3376
+ projection: string;
3377
+ key: string;
3277
3378
  };
3278
3379
  /**
3279
- * One registered system and the frame it was registered in. A system registered during a frame
3280
- * runs from the next frame on.
3380
+ * What the next reconcile has to do: why it runs, which roots changed and whether `view` is
3381
+ * forced for every item.
3281
3382
  *
3282
3383
  * @example
3283
3384
  * ```ts
3284
- * const entry: SystemEntry = { definition: bobSystem, frame: 12 };
3385
+ * const dirty: Dirty = { causes: ["edge"], roots: new Set(["player"]), force: false };
3285
3386
  * ```
3286
3387
  */
3287
- type SystemEntry = {
3288
- readonly definition: AnySystem;
3289
- readonly frame: number;
3388
+ type Dirty = {
3389
+ causes: Cause[];
3390
+ roots: Set<Root>;
3391
+ force: boolean;
3290
3392
  };
3291
3393
  /**
3292
- * A structural change queued while a phase runs. Applied in call order when the phase ends.
3394
+ * projection module state.
3395
+ */
3396
+ type ProjectionState = {
3397
+ specs: Map<string, AnyProjectionSpec>;
3398
+ layers: readonly LayerSpec[];
3399
+ mounted: Map<string, Mounted>;
3400
+ byEntity: Map<Entity, View$1>;
3401
+ dirty: Dirty | undefined;
3402
+ hints: Hint[]; /** The tracks the projection started, with the handle that says whether they still run. */
3403
+ tracks: Track[]; /** The tween engine `anim` installed, or `undefined`: then every target is written at once. */
3404
+ driver: TweenDriver | undefined; /** The rest pose of an entity that is not a view, recorded by its owner through `setRest`. */
3405
+ rests: Map<Entity, RestPose>; /** Projection name to key to the entity a plugin above registered under it. */
3406
+ keys: Map<string, Map<string, Entity>>; /** The reverse of `keys`, so `keyOf` answers for a registered element too. */
3407
+ keysByEntity: Map<Entity, ProjectionKey>; /** Entity to component name to the fields another writer owns. */
3408
+ mutes: Map<Entity, Map<string, Set<string>>>;
3409
+ offHints: Array<() => void>;
3410
+ };
3411
+ /**
3412
+ * projection module API, `app.world.projection`: the bridge from the model to entities.
3293
3413
  *
3294
3414
  * @example
3295
3415
  * ```ts
3296
- * const command: Command = { kind: "remove", entity: 1_048_576, component: "Held" };
3416
+ * // A test mounts the board and asks which entity carries one model key.
3417
+ * app.world.projection.mount(["board.items"], { kind: "plugin", name: "test" });
3418
+ * app.world.projection.entityOf("board.items", "i7"); // 1048576
3297
3419
  * ```
3298
3420
  */
3299
- type Command = {
3300
- kind: "attach";
3301
- entity: Entity;
3302
- components: readonly AnyComponentValue[];
3303
- } | {
3304
- kind: "despawn";
3305
- entity: Entity;
3306
- } | {
3307
- kind: "despawnOwnedBy";
3308
- owner: Owner;
3309
- } | {
3310
- kind: "add";
3311
- entity: Entity;
3312
- value: AnyComponentValue;
3313
- } | {
3314
- kind: "remove";
3315
- entity: Entity;
3316
- component: string;
3317
- };
3318
- /**
3319
- * A structural listener registered with `onAdded` or `onRemoved`.
3320
- *
3321
- * @example
3322
- * ```ts
3323
- * const hook: StructuralHook = (entity, value) => void entity + String(value);
3324
- * ```
3325
- */
3326
- type StructuralHook = (entity: Entity, value: unknown) => void;
3327
- /**
3328
- * The frozen model snapshot of one frame, kept so four phases read it once.
3329
- *
3330
- * @example
3331
- * ```ts
3332
- * const cached: FrameSnapshot = { frame: 12, snapshot: { player: {}, session: {}, rng: rngState } };
3333
- * ```
3334
- */
3335
- type FrameSnapshot = {
3336
- frame: number;
3337
- snapshot: Snapshot;
3338
- };
3339
- /**
3340
- * ecs module state.
3341
- */
3342
- type EcsState = {
3343
- generations: number[];
3344
- free: number[];
3345
- owners: Map<Entity, Owner>; /** Key is `"kind:name"` of the owner. */
3346
- byOwner: Map<string, Set<Entity>>;
3347
- types: Map<string, AnyComponentType>;
3348
- stores: Map<string, Map<Entity, object | true>>;
3349
- resources: Map<string, object>;
3350
- systems: Record<WorldPhase, SystemEntry[]>;
3351
- running: WorldPhase | undefined;
3352
- commands: Command[];
3353
- changed: Map<string, Set<Entity>>;
3354
- added: Map<string, StructuralHook[]>;
3355
- removed: Map<string, StructuralHook[]>;
3356
- ownerLeft: Array<(owner: Owner) => void>;
3357
- mode: WorldMode;
3358
- offFrame: Array<() => void>;
3359
- frameSnapshot: FrameSnapshot | undefined;
3360
- };
3361
- /**
3362
- * ecs module API, `app.world.ecs`: the screen as data. Plain-object components in one store per
3363
- * component type, generational entity ids, a mandatory owner, a command buffer applied between
3364
- * phases, coarse change detection and fixed phases driven by `time`.
3365
- *
3366
- * @example
3367
- * ```ts
3368
- * // A test owns its entities and reads them back.
3369
- * const entity = app.world.ecs.spawn({ kind: "plugin", name: "test" }, [Transform({ x: 10 })]);
3370
- * app.world.ecs.get(entity, Transform); // { x: 10, y: 0, rotation: 0, scale: 1, pivot: { x: 0, y: 0 } }
3371
- * ```
3372
- */
3373
- type EcsApi = {
3421
+ type ProjectionApi = {
3374
3422
  /**
3375
- * Registers a system at the end of its phase list. A system registered during a frame runs
3376
- * from the next frame on.
3423
+ * Stores a projection spec. Nothing is drawn until `mount`.
3377
3424
  *
3378
- * @param definition - The system, built with `system()`.
3379
- * @returns The remover; calling it twice is a no-op.
3380
- * @throws {Error} When a system of that name is already registered.
3425
+ * @param spec - The projection, built with `projection()`.
3426
+ * @throws {Error} When a projection of that name is already registered.
3381
3427
  * @example
3382
3428
  * ```ts
3383
- * // `text` registers its layout system when the plugin starts.
3384
- * const world = ctx.require(worldPlugin);
3385
- * const off = world.ecs.system(measureLabels); // runs from the next frame
3386
- *
3387
- * off(); // the plugin stops
3429
+ * // A test registers one projection by hand instead of composing a feature.
3430
+ * app.world.projection.register(boardItems);
3431
+ * app.world.projection.entityOf("board.items", "i7"); // undefined: nothing is mounted yet
3388
3432
  * ```
3389
3433
  */
3390
- system(definition: AnySystem): () => void;
3434
+ register(spec: AnyProjectionSpec): void;
3391
3435
  /**
3392
- * Creates an entity. The id is reserved and returned at once, also inside a phase, where the
3393
- * components are attached when the phase ends.
3436
+ * Stores the layer list of the scene. The order of the list is draw order; every call stores a
3437
+ * new frozen array.
3394
3438
  *
3395
- * @param owner - Who owns the entity. Required by the type.
3396
- * @param components - Component and tag values the entity starts with.
3397
- * @returns The new entity id.
3439
+ * @param list - The layers, in draw order.
3398
3440
  * @example
3399
3441
  * ```ts
3400
- * // `ui` spawns a popup entity it owns, so closing the node frees it in one call.
3401
- * const entity = app.world.ecs.spawn({ kind: "node", name: "reward" }, [
3402
- * Transform({ x: 540, y: 960 })
3442
+ * // `scenes` switches to the board scene.
3443
+ * const world = ctx.require(worldPlugin);
3444
+ * world.projection.setLayers([
3445
+ * { name: "board", sort: "none" },
3446
+ * { name: "items", sort: "y" },
3447
+ * { name: "lifted", sort: "none" }
3403
3448
  * ]);
3404
- * app.world.ecs.has(entity, Transform); // true
3405
3449
  * ```
3406
3450
  */
3407
- spawn(owner: Owner, components: readonly AnyComponentValue[]): Entity;
3451
+ setLayers(list: ReadonlyArray<LayerSpec>): void;
3408
3452
  /**
3409
- * Removes every component of an entity, each firing `onRemoved`, frees the index and bumps the
3410
- * generation. A stale id is a no-op.
3453
+ * The current layer list, empty before the first `setLayers`. A new list is a new reference, so
3454
+ * a reader compares by identity.
3411
3455
  *
3412
- * @param entity - The entity to remove.
3456
+ * @returns The layers, in draw order.
3413
3457
  * @example
3414
3458
  * ```ts
3415
- * // `ui` closes the popup it spawned. Asking again answers with the empty world.
3416
- * app.world.ecs.despawn(popup);
3417
- * app.world.ecs.has(popup, Transform); // false
3459
+ * // `renderer.sync` rebuilds its containers only when the scene really changed.
3460
+ * const current = app.world.projection.layers();
3461
+ * if (current !== lastLayers) rebuildContainers(current); // [{ name: "board", sort: "none" }, ...]
3418
3462
  * ```
3419
3463
  */
3420
- despawn(entity: Entity): void;
3464
+ layers(): ReadonlyArray<LayerSpec>;
3421
3465
  /**
3422
- * Despawns exactly the entities of one owner and tells `projection` that the owner left.
3466
+ * Marks projections mounted under an owner and reconciles them at once, direct: a scene brings
3467
+ * its own transition. Nothing is mounted when the call throws.
3423
3468
  *
3424
- * @param owner - The owner whose entities go.
3469
+ * @param names - Names of the projections to mount.
3470
+ * @param owner - Who owns the entities of these projections.
3471
+ * @throws {Error} For an unknown name, or a `layer` or `lift` the scene does not declare.
3425
3472
  * @example
3426
3473
  * ```ts
3427
- * // A test cleans up after itself without touching the projections of the game.
3428
- * app.world.ecs.despawnOwnedBy({ kind: "plugin", name: "test" });
3429
- * app.world.ecs.snapshot(); // { mode: "live", entities: [], resources: {} }
3474
+ * // `scenes` mounts the projections of the board scene it just switched to.
3475
+ * const world = ctx.require(worldPlugin);
3476
+ * world.projection.mount(["board.cells", "board.items"], { kind: "plugin", name: "scenes" });
3477
+ * world.projection.entityOf("board.items", "i7"); // 1048576, the picture is already there
3430
3478
  * ```
3431
3479
  */
3432
- despawnOwnedBy(owner: Owner): void;
3480
+ mount(names: readonly string[], owner: Owner): void;
3433
3481
  /**
3434
- * Reads a component value. A stale id or a missing component gives `undefined`.
3482
+ * Unmounts projections: every motion of their views is finished, and the live views and the
3483
+ * despawn queue are despawned in the same call.
3435
3484
  *
3436
- * @param entity - The entity to read.
3437
- * @param component - The component type.
3438
- * @returns The stored value, read-only, or `undefined`.
3485
+ * @param names - Names of the projections to unmount.
3439
3486
  * @example
3440
3487
  * ```ts
3441
- * // `input` asks whether the entity under the finger may be dragged.
3488
+ * // `scenes` leaves the board scene.
3442
3489
  * const world = ctx.require(worldPlugin);
3443
- * world.ecs.get(hit, Draggable); // { payload: { id: "i7" } }
3444
- * world.ecs.get(hit, DropTarget); // undefined: this item takes no drop
3490
+ * world.projection.unmount(["board.cells", "board.items"]);
3491
+ * world.projection.entityOf("board.items", "i7"); // undefined
3445
3492
  * ```
3446
3493
  */
3447
- get<Value extends object>(entity: Entity, component: ComponentHandle<Value>): Readonly<Value> | undefined;
3494
+ unmount(names: readonly string[]): void;
3448
3495
  /**
3449
- * Shallow-merges a patch into a stored component and marks the entity changed for it. Never
3450
- * queued: a `set` inside a phase is visible to the next system of that phase.
3496
+ * A drop with no commit: cancels the view's motions and plays settle for every loose component.
3497
+ * In mode `paused` or `fast` the rest pose is written at once. No-op for an entity that is not
3498
+ * a live view.
3451
3499
  *
3452
- * @param entity - The entity to write.
3453
- * @param component - The component type.
3454
- * @param patch - The fields to overwrite.
3455
- * @throws {Error} When the entity is stale or does not carry the component.
3500
+ * @param entity - The entity of the view that was dropped.
3456
3501
  * @example
3457
3502
  * ```ts
3458
- * // `input` moves the held view with the finger.
3503
+ * // `input` released the finger over nothing, so the gate refused the answer.
3459
3504
  * const world = ctx.require(worldPlugin);
3460
- * world.ecs.set(held, Transform, { x: 420, y: 810 });
3461
- * world.ecs.get(held, Transform); // { x: 420, y: 810, rotation: 0, scale: 1, pivot: { x: 0, y: 0 } }
3505
+ * world.projection.settle(held); // the item glides back to its cell over settleMs
3462
3506
  * ```
3463
3507
  */
3464
- set<Value extends object>(entity: Entity, component: ComponentHandle<Value>, patch: Partial<Value>): void;
3508
+ settle(entity: Entity): void;
3465
3509
  /**
3466
- * Adds a component value. On an entity that already carries it the value is replaced and the
3467
- * entity is marked changed; no `onAdded` fires.
3510
+ * Hands a set of fields to another writer. Tracks never write them, also not on `finish()`.
3468
3511
  *
3469
- * @param entity - The entity to write.
3470
- * @param value - The component or tag value, built by calling its type.
3512
+ * @param entity - The entity of the view.
3513
+ * @param component - The component whose fields are taken over.
3514
+ * @param fields - The field names the caller owns.
3515
+ * @returns The remover; safe after the entity left, and a no-op when called twice.
3471
3516
  * @example
3472
3517
  * ```ts
3473
- * // `text` gives the label entity its display object once the atlas is there.
3518
+ * // `input` owns the position of the held view while the finger drags it.
3474
3519
  * const world = ctx.require(worldPlugin);
3475
- * world.ecs.add(label, Display({ object: bitmapText }));
3476
- * world.ecs.has(label, Display); // true
3520
+ * const release = world.projection.mute(held, Transform, ["x", "y"]);
3521
+ *
3522
+ * release(); // the finger let go
3477
3523
  * ```
3478
3524
  */
3479
- add(entity: Entity, value: AnyComponentValue): void;
3525
+ mute(entity: Entity, component: ComponentType<object>, fields: readonly string[]): () => void;
3480
3526
  /**
3481
- * Removes a component. Fires `onRemoved` when the entity carried it.
3527
+ * Moves a view into or out of its projection's `lift` layer. `lift(entity, false)` on a view
3528
+ * that still moves takes effect when its last motion ends, so a settling view stays above the
3529
+ * board until it is home.
3482
3530
  *
3483
- * @param entity - The entity to write.
3484
- * @param component - The component or tag type to drop.
3531
+ * @param entity - The entity of the view.
3532
+ * @param on - True to lift, false to drop back.
3485
3533
  * @example
3486
3534
  * ```ts
3487
- * // `text` drops the display object when the label leaves the screen.
3535
+ * // `input` lifts the item the finger picked up so it draws above the board.
3488
3536
  * const world = ctx.require(worldPlugin);
3489
- * world.ecs.remove(label, Display);
3490
- * world.ecs.has(label, Display); // false
3537
+ * world.projection.lift(held, true);
3538
+ *
3539
+ * world.projection.lift(held, false); // on release: takes effect when the settle motion ends
3491
3540
  * ```
3492
3541
  */
3493
- remove(entity: Entity, component: AnyComponentType): void;
3542
+ lift(entity: Entity, on: boolean): void;
3494
3543
  /**
3495
- * Tells whether an entity carries a component or tag. False for a stale id.
3544
+ * The projection and key of an entity. Also answers for a view in the despawn queue and for an
3545
+ * element registered with `registerKey`.
3496
3546
  *
3497
- * @param entity - The entity to read.
3498
- * @param component - The component or tag type.
3499
- * @returns True when the entity carries it.
3547
+ * @param entity - The entity to ask about.
3548
+ * @returns The projection name and the key, or `undefined`.
3500
3549
  * @example
3501
3550
  * ```ts
3502
- * // `input` never hit-tests a view that is on its way out.
3503
- * const world = ctx.require(worldPlugin);
3504
- * world.ecs.has(candidate, Exiting); // true while the merge animation plays
3551
+ * // `renderer.sync` labels its display objects for the inspector.
3552
+ * app.world.projection.keyOf(1_048_576); // { projection: "board.items", key: "i7" }
3553
+ * app.world.projection.keyOf(42); // undefined: not a view
3505
3554
  * ```
3506
3555
  */
3507
- has(entity: Entity, component: AnyComponentType): boolean;
3556
+ keyOf(entity: Entity): ProjectionKey | undefined;
3508
3557
  /**
3509
- * Adds a tag. Structural and idempotent.
3558
+ * The components a motion still drives on an entity: its running tracks, in start order, each
3559
+ * name once. An entity no motion drives answers `[]`: a resting view, an entity that is not a
3560
+ * view, a stale id. Every call hands out a new array.
3510
3561
  *
3511
- * @param entity - The entity to write.
3512
- * @param tagType - The tag type.
3562
+ * @param entity - The entity to ask about.
3563
+ * @returns The component names, or `[]`.
3513
3564
  * @example
3514
3565
  * ```ts
3515
- * // `input` marks the view under the finger while the drag runs.
3516
- * const world = ctx.require(worldPlugin);
3517
- * world.ecs.tag(dragged, Held);
3518
- * world.ecs.has(dragged, Held); // true
3566
+ * // The editor explains a coin in flight: its Transform still tweens, its Sprite rests.
3567
+ * app.world.projection.motionsOf(coin); // ["Transform"]
3568
+ * app.time.step(400);
3569
+ * app.world.projection.motionsOf(coin); // []
3519
3570
  * ```
3520
3571
  */
3521
- tag(entity: Entity, tagType: TagType): void;
3572
+ motionsOf(entity: Entity): readonly string[];
3522
3573
  /**
3523
- * Removes a tag. Structural and idempotent.
3574
+ * The rest value of one component of an entity: what a view holds when nothing animates it, or
3575
+ * what `setRest` recorded for an element. Read-only and owner-free, so a plugin that owns no
3576
+ * entity can aim at one.
3524
3577
  *
3525
- * @param entity - The entity to write.
3526
- * @param tagType - The tag type.
3578
+ * @param entity - A projection view or a registered element.
3579
+ * @param component - The component whose rest value is asked.
3580
+ * @returns The rest value, or `undefined` when the entity has none recorded.
3527
3581
  * @example
3528
3582
  * ```ts
3529
- * // `input` releases the view on pointer up.
3583
+ * // `anim` builds a coin flight toward the counter: `at(target)` is the counter's rest pose.
3530
3584
  * const world = ctx.require(worldPlugin);
3531
- * world.ecs.untag(dragged, Held);
3532
- * world.ecs.has(dragged, Held); // false
3585
+ * const counter = world.projection.entityOf("hud", "coins") ?? 0;
3586
+ *
3587
+ * world.projection.restOf(counter, Transform); // { x: 40, y: 120, rotation: 0, scale: 1, pivot: { x: 0, y: 0 } }
3533
3588
  * ```
3534
3589
  */
3535
- untag(entity: Entity, tagType: TagType): void;
3590
+ restOf<Value extends object>(entity: Entity, component: ComponentType<Value>): Value | undefined;
3536
3591
  /**
3537
- * Walks the store of the FIRST term in insertion order and keeps the entities that carry every
3538
- * term. Deterministic. A `mut()` term marks every yielded entity changed for that component.
3592
+ * The entity of one key of one projection: a live view, never the despawn queue, or an element
3593
+ * a plugin above registered under that key.
3539
3594
  *
3540
- * @param terms - Component types, tag types and `mut()` terms.
3541
- * @returns The matching rows: the entity and one value per term.
3595
+ * @param projection - Name of the projection.
3596
+ * @param key - The model key.
3597
+ * @returns The entity, or `undefined`.
3542
3598
  * @example
3543
3599
  * ```ts
3544
- * // `renderer.sync` walks every sprite that has a place on the screen.
3545
- * for (const [entity, sprite, transform] of app.world.ecs.query(Sprite, Transform)) {
3546
- * display.get(entity)?.position.set(transform.x, transform.y);
3547
- * sprite.texture; // "board.item-red-1"
3548
- * }
3600
+ * // `input` resolves the target of a scripted drag.
3601
+ * const world = ctx.require(worldPlugin);
3602
+ * world.projection.entityOf("board.items", "i7"); // 1048576
3603
+ * world.projection.entityOf("board.items", "gone"); // undefined
3549
3604
  * ```
3550
3605
  */
3551
- query<const Terms extends readonly QueryTerm[]>(...terms: Terms): Iterable<QueryTuple<Terms>>;
3606
+ entityOf(projection: string, key: string): Entity | undefined;
3552
3607
  /**
3553
- * Returns the one mutable value of a resource, created from a deep clone of its defaults on
3554
- * first read.
3608
+ * The live view entities of a mounted projection, in the order of its model keys. A view that
3609
+ * plays its exit is left out, and so is an element registered with `registerKey`. Every call
3610
+ * hands out a new array.
3555
3611
  *
3556
- * @param resourceType - The resource type.
3557
- * @returns The mutable resource value.
3612
+ * @param name - Name of the projection.
3613
+ * @returns The entities, or `[]` when the projection is not mounted.
3558
3614
  * @example
3559
3615
  * ```ts
3560
- * // `input` writes the pointer once per frame; every game system reads the same object.
3616
+ * // `ui` hosts the board inside a slot and parents every board item to it on reconcile.
3561
3617
  * const world = ctx.require(worldPlugin);
3562
- * const pointer = world.ecs.resource(Pointer);
3563
- * pointer.down = true;
3618
+ * world.projection.entitiesOf("board.items"); // [1048580, 1048581]
3619
+ * world.projection.entitiesOf("shop.items"); // []: not mounted
3564
3620
  * ```
3565
3621
  */
3566
- resource<Value extends object>(resourceType: ResourceType<Value>): Value;
3622
+ entitiesOf(name: string): readonly Entity[];
3567
3623
  /**
3568
- * Registers a listener for the moment a component is added, called when the structural change
3569
- * is applied. A throwing listener is logged and the others still run.
3624
+ * Installs the tween engine every `ViewHandle.tween`, `toRest` and `all` then runs on. Without
3625
+ * one the world writes every target at once and hands back an inactive handle.
3570
3626
  *
3571
- * @param component - The component type to watch.
3572
- * @param fn - Called with the entity and the stored value.
3573
- * @returns The remover; calling it twice is a no-op.
3627
+ * @param driver - The engine that runs the tracks.
3628
+ * @returns The remover; it puts the instant writes back.
3574
3629
  * @example
3575
3630
  * ```ts
3576
- * // `renderer.sync` creates a Pixi object for every new sprite.
3631
+ * // `anim` installs its tween core when it starts and takes it back when it stops.
3577
3632
  * const world = ctx.require(worldPlugin);
3578
- * const off = world.ecs.onAdded(Sprite, (entity, sprite) => {
3579
- * objects.set(entity, makeSprite(sprite.texture));
3580
- * });
3633
+ * ctx.state.removeDriver = world.projection.setDriver(createDriver(ctx, ctx.state));
3581
3634
  *
3582
- * off(); // the renderer stops
3635
+ * ctx.state.removeDriver(); // onStop: motions are instant again
3583
3636
  * ```
3584
3637
  */
3585
- onAdded<Value extends object>(component: ComponentHandle<Value>, fn: (entity: Entity, value: Readonly<Value>) => void): () => void;
3638
+ setDriver(driver: TweenDriver): () => void;
3586
3639
  /**
3587
- * Registers a listener for the moment a tag is added: `tag` on an entity without it, or a spawn
3588
- * that carries it. A tag has no value, so the listener gets the entity only.
3640
+ * A view handle for an entity that is not a projection view, so an element another plugin owns
3641
+ * can be animated the same way. `set`, `get`, `tween`, `toRest` and `all` work; `rest` answers
3642
+ * what `setRest` recorded; `peer` answers `undefined`.
3589
3643
  *
3590
- * @param tagType - The tag type to watch.
3591
- * @param fn - Called with the entity that got the tag.
3592
- * @returns The remover; calling it twice is a no-op.
3644
+ * @param entity - The entity of the element.
3645
+ * @param owner - Who is asking; without it only the kind of the owner is checked.
3646
+ * @returns The handle, or `undefined` for a projection view and for a foreign entity.
3593
3647
  * @example
3594
3648
  * ```ts
3595
- * // `ui` shows the pressed look while `input` holds the `Pressed` tag on a button.
3649
+ * // `ui` plays the `motion` prop of an element it owns.
3596
3650
  * const world = ctx.require(worldPlugin);
3597
- * const off = world.ecs.onAdded(Pressed, entity => markPointer(entity, "pressed", true));
3651
+ * const handle = world.projection.viewOf(button, { kind: "plugin", name: "ui" });
3598
3652
  *
3599
- * world.ecs.tag(button, Pressed); // markPointer(button, "pressed", true)
3600
- * off(); // ui stops
3653
+ * handle?.tween(Transform, { scale: 1.1 }, { ms: 120, ease: "outBack" });
3601
3654
  * ```
3602
3655
  */
3603
- onAdded(tagType: TagType, fn: (entity: Entity) => void): () => void;
3656
+ viewOf(entity: Entity, owner: Owner): ViewHandle<unknown> | undefined;
3604
3657
  /**
3605
- * Registers a listener for the moment a component is removed.
3658
+ * Records what one component of an element looks like at rest, so `toRest` knows where home is.
3606
3659
  *
3607
- * @param component - The component type to watch.
3608
- * @param fn - Called with the entity and the value it had.
3609
- * @returns The remover; calling it twice is a no-op.
3660
+ * @param entity - The entity of the element.
3661
+ * @param component - The component whose rest value is recorded.
3662
+ * @param value - The value the element holds when nothing animates.
3610
3663
  * @example
3611
3664
  * ```ts
3612
- * // `renderer.sync` destroys the Pixi object of a sprite that left.
3665
+ * // `ui` laid the coin counter out and tells the world where it belongs.
3613
3666
  * const world = ctx.require(worldPlugin);
3614
- * const off = world.ecs.onRemoved(Sprite, entity => {
3615
- * objects.get(entity)?.destroy();
3616
- * objects.delete(entity);
3617
- * });
3667
+ * world.projection.setRest(counter, Transform, { x: 40, y: 120, rotation: 0, scale: 1, pivot: { x: 0, y: 0 } });
3618
3668
  *
3619
- * off(); // the renderer stops
3669
+ * world.projection.viewOf(counter, { kind: "plugin", name: "ui" })?.rest(Transform); // { x: 40, y: 120, rotation: 0, scale: 1, pivot: { x: 0, y: 0 } }
3620
3670
  * ```
3621
3671
  */
3622
- onRemoved<Value extends object>(component: ComponentHandle<Value>, fn: (entity: Entity, value: Readonly<Value>) => void): () => void;
3672
+ setRest<Value extends object>(entity: Entity, component: ComponentType<Value>, value: Value): void;
3623
3673
  /**
3624
- * Registers a listener for the moment a tag is removed: `untag` on an entity that had it, or a
3625
- * despawn. The listener gets the entity only.
3674
+ * Gives an entity a projection and key of its own, so `entityOf` and `keyOf` answer for it next
3675
+ * to the live views. That is how an element becomes a target for `anim`, `input` and `guide`.
3626
3676
  *
3627
- * @param tagType - The tag type to watch.
3628
- * @param fn - Called with the entity that lost the tag.
3629
- * @returns The remover; calling it twice is a no-op.
3677
+ * @param projection - Name the element is addressed under.
3678
+ * @param key - Key the element is addressed under.
3679
+ * @param entity - The entity of the element.
3680
+ * @returns The remover; it drops the key again.
3681
+ * @throws {Error} When a live view of that projection already holds the key.
3630
3682
  * @example
3631
3683
  * ```ts
3632
- * // `ui` drops the pressed look when `input` releases the button.
3684
+ * // `ui` publishes the coin counter of the HUD.
3633
3685
  * const world = ctx.require(worldPlugin);
3634
- * const off = world.ecs.onRemoved(Pressed, entity => markPointer(entity, "pressed", false));
3686
+ * const drop = world.projection.registerKey("hud", "coins", counter);
3635
3687
  *
3636
- * world.ecs.untag(button, Pressed); // markPointer(button, "pressed", false)
3637
- * off(); // ui stops
3688
+ * world.projection.entityOf("hud", "coins"); // the entity of the counter
3689
+ * drop(); // the element left the screen
3638
3690
  * ```
3639
3691
  */
3640
- onRemoved(tagType: TagType, fn: (entity: Entity) => void): () => void;
3692
+ registerKey(projection: string, key: string, entity: Entity): () => void;
3641
3693
  /**
3642
- * The coarse change set of the frame, cleared in `time` phase `signals` after every `sync`
3643
- * system ran.
3694
+ * Marks every mounted projection dirty and forces `view` for every item, so the next frame
3695
+ * rebuilds the picture directly.
3644
3696
  *
3645
- * @param component - The component type to ask about.
3646
- * @returns The entities marked changed for it this frame.
3647
3697
  * @example
3648
3698
  * ```ts
3649
- * // `renderer.sync` moves only what moved.
3650
- * for (const entity of app.world.ecs.changed(Transform)) {
3651
- * const transform = app.world.ecs.get(entity, Transform);
3652
- * display.get(entity)?.position.set(transform?.x ?? 0, transform?.y ?? 0);
3653
- * }
3699
+ * // `i18n` switched the locale: every label has to be projected again.
3700
+ * const world = ctx.require(worldPlugin);
3701
+ * world.projection.rerunAll(); // the next frame writes the new strings with no motion
3654
3702
  * ```
3655
3703
  */
3656
- changed(component: AnyComponentType): Iterable<Entity>;
3704
+ rerunAll(): void;
3705
+ };
3706
+ /**
3707
+ * projection methods the plugin root drives from the frame and the hooks. Not public.
3708
+ */
3709
+ type ProjectionInternal = {
3657
3710
  /**
3658
- * The effective mode: `"fast"` while the flow walks fast, otherwise the stored one.
3711
+ * Records a commit. In fast mode it reconciles at once, direct.
3659
3712
  *
3660
- * @returns The mode the frame runs in.
3661
- * @example
3662
- * ```ts
3663
- * // A test checks that a fast walk froze the tweens.
3664
- * app.flow.setMode("fast");
3665
- * app.world.ecs.mode(); // "fast", even though setMode was never called on the world
3666
- * ```
3713
+ * @param roots - The state roots the commit touched.
3714
+ * @param cause - Why the commit happened.
3667
3715
  */
3668
- mode(): WorldMode;
3716
+ markDirty(roots: readonly Root[], cause: Cause): void;
3669
3717
  /**
3670
- * Stores the world mode. `"fast"` finishes every motion and flushes every despawn queue at once.
3671
- *
3672
- * @param mode - The new stored mode.
3673
- * @example
3674
- * ```ts
3675
- * // A headless test wants the final picture without stepping frames.
3676
- * app.world.ecs.setMode("fast");
3677
- * app.world.ecs.mode(); // "fast"
3678
- * ```
3718
+ * Reconciles when the world is dirty. Runs at the start of `time` phase `input`.
3679
3719
  */
3680
- setMode(mode: WorldMode): void;
3720
+ reconcileIfDirty(): void;
3681
3721
  /**
3682
- * The component type behind a name. A type registers itself on first use, so a name nobody
3683
- * wrote, read or queried yet is unknown.
3684
- *
3685
- * @param name - The storage name of the component.
3686
- * @returns The type, or `undefined` when the world never met it.
3687
- * @example
3688
- * ```ts
3689
- * // `text` resolves the component a label binds its number to.
3690
- * const world = ctx.require(worldPlugin);
3691
- * const type = world.ecs.typeOf("Coins"); // the Coins component, registered when it was written
3692
- * const pose = type === undefined ? undefined : world.ecs.get(counter, type);
3693
- *
3694
- * pose?.amount; // 120
3695
- * ```
3722
+ * Empties the hint buffer. Runs once per frame, reconcile or not.
3696
3723
  */
3697
- typeOf(name: string): AnyComponent | undefined;
3724
+ dropHints(): void;
3698
3725
  /**
3699
- * The owner of an entity, which is also the liveness check: a stale id has none, also after its
3700
- * index came back with a new generation. `projection` asks before it hands out a view handle for
3701
- * an entity another plugin owns; `renderer` asks before it forgets the filters of an entity whose
3702
- * visual left.
3726
+ * Buffers one released hint.
3703
3727
  *
3704
- * @param entity - The entity to ask about.
3705
- * @returns The owner, or `undefined` when the id is stale.
3706
- * @example
3707
- * ```ts
3708
- * // renderer forgets the filter slots of an entity only after it despawned
3709
- * const ecs = ctx.require(worldPlugin).ecs;
3710
- * ecs.ownerOf(button); // { kind: "projection", name: "hud" }
3711
- * ecs.despawn(button);
3712
- * ecs.ownerOf(button); // undefined
3713
- * ```
3728
+ * @param hint - The hint `flow.fx` released after a commit.
3714
3729
  */
3715
- ownerOf(entity: Entity): Owner | undefined;
3730
+ pushHint(hint: Hint): void;
3716
3731
  /**
3717
- * The whole world as plain JSON, sorted by entity index. A component value that is not JSON is
3718
- * left out and its name is listed in `skipped`.
3719
- *
3720
- * @returns The world as JSON: the mode, the entities and the resources.
3721
- * @example
3722
- * ```ts
3723
- * // A test asserts what the board holds after the first reconcile.
3724
- * app.world.ecs.snapshot();
3725
- * // { mode: "live", entities: [{ id: 1048576, index: 0, generation: 1,
3726
- * // owner: { kind: "projection", name: "board.items" },
3727
- * // components: { Layer: { name: "items" } }, skipped: [] }], resources: {} }
3728
- * ```
3732
+ * Sweeps the views whose motions ended: the despawn queue, the convergence check and the
3733
+ * pending drops. The tracks themselves are advanced by the driver.
3729
3734
  */
3730
- snapshot(): WorldSnapshot;
3731
- };
3732
- /**
3733
- * ecs methods the plugin root calls: frame phases, the owner listener, the clears. Not public.
3734
- */
3735
- type EcsInternal = {
3735
+ sweep(): void;
3736
3736
  /**
3737
- * Registers a listener for `despawnOwnedBy`, so `projection` can drop the views of an owner
3738
- * that left.
3739
- *
3740
- * @param fn - Called with the owner whose entities were despawned.
3741
- * @returns The remover.
3737
+ * Finishes every motion and flushes every despawn queue. Called when the mode turns `fast`.
3742
3738
  */
3743
- onOwnerLeft(fn: (owner: Owner) => void): () => void;
3739
+ flushAll(): void;
3744
3740
  /**
3745
- * Runs the systems of one phase, then flushes the command buffer.
3741
+ * Drops the views of a projection whose owner despawned its entities.
3746
3742
  *
3747
- * @param phase - The phase to run.
3748
- * @param time - The frame's `Time`.
3749
- */
3750
- runPhase(phase: WorldPhase, time: Readonly<Time>): void;
3751
- /**
3752
- * Applies the queued structural commands in call order.
3753
- */
3754
- flush(): void;
3755
- /**
3756
- * Clears every change set. Called in `time` phase `signals`.
3743
+ * @param owner - The owner that left.
3757
3744
  */
3758
- clearChanges(): void;
3745
+ ownerLeft(owner: Owner): void;
3759
3746
  /**
3760
- * Drops every entity, component, resource, system and hook.
3747
+ * Drops every view, track, mute and spec.
3761
3748
  */
3762
3749
  clear(): void;
3763
3750
  };
3764
3751
  //#endregion
3765
- //#region src/plugins/world/projection/types.d.ts
3752
+ //#region src/plugins/world/ecs/types.d.ts
3766
3753
  /**
3767
- * How a layer orders the entities drawn in it.
3754
+ * Entity id: `generation * 2 ** 20 + index`, a safe integer. A freed index comes back with a
3755
+ * higher generation, so an id is never silently reused.
3768
3756
  *
3769
3757
  * @example
3770
3758
  * ```ts
3771
- * const sort: LayerSort = "y";
3759
+ * const entity: Entity = 1_048_576; // generation 1, index 0
3772
3760
  * ```
3773
3761
  */
3774
- type LayerSort = "none" | "y" | "order";
3762
+ type Entity = number;
3775
3763
  /**
3776
- * One named layer of a scene. The order of the list is draw order.
3764
+ * Who owns an entity. Every `spawn` names one, so `despawnOwnedBy` can free exactly one owner's
3765
+ * entities and `snapshot()` can say where an entity came from.
3777
3766
  *
3778
3767
  * @example
3779
3768
  * ```ts
3780
- * const layer: LayerSpec = { name: "items", sort: "y" };
3769
+ * const owner: Owner = { kind: "projection", name: "board.items" };
3781
3770
  * ```
3782
3771
  */
3783
- type LayerSpec = {
3772
+ type Owner = {
3773
+ kind: "projection" | "node" | "plugin";
3784
3774
  name: string;
3785
- sort: LayerSort;
3786
3775
  };
3787
3776
  /**
3788
- * Easing of a tween: one of the named curves the driver knows, or a function that gets the
3789
- * normalised time and returns the eased fraction.
3777
+ * One entity as `snapshot()` reads it: its id and the two halves of the id, its owner, every
3778
+ * component value that is plain JSON, and the names of the ones that are not.
3790
3779
  *
3791
3780
  * @example
3792
3781
  * ```ts
3793
- * const ease: Ease = "outBack";
3794
- * ```
3795
- */
3796
- type Ease = "linear" | "in" | "out" | "inOut" | "inCubic" | "outCubic" | "inOutCubic" | "inBack" | "outBack" | ((t: number) => number);
3782
+ * const entity: EntitySnapshot = {
3783
+ * id: 1_048_576, index: 0, generation: 1, owner: { kind: "projection", name: "board.items" },
3784
+ * components: { Layer: { name: "items" } }, skipped: ["Display"]
3785
+ * };
3786
+ * ```
3787
+ */
3788
+ type EntitySnapshot = {
3789
+ id: Entity;
3790
+ index: number;
3791
+ generation: number;
3792
+ owner: Owner;
3793
+ components: Record<string, Json>;
3794
+ skipped: string[];
3795
+ };
3797
3796
  /**
3798
- * What every motion returns: it can be finished, cancelled and asked whether it still runs.
3799
- * V3 `anim` hands out the same shape.
3797
+ * The whole world as plain JSON: the effective mode, every entity sorted by index and every
3798
+ * resource that is JSON.
3800
3799
  *
3801
3800
  * @example
3802
3801
  * ```ts
3803
- * const handle: MotionHandle = view.toRest(Transform);
3804
- * handle.active(); // true until the last frame wrote the exact target
3802
+ * const empty: WorldSnapshot = { mode: "live", entities: [], resources: {} };
3805
3803
  * ```
3806
3804
  */
3807
- type MotionHandle = {
3808
- finish(): void;
3809
- cancel(): void;
3810
- active(): boolean;
3805
+ type WorldSnapshot = {
3806
+ mode: WorldMode;
3807
+ entities: EntitySnapshot[];
3808
+ resources: Record<string, Json>;
3811
3809
  };
3812
3810
  /**
3813
- * What a motion hook returns. `void` means the hook wrote the component directly.
3811
+ * The four phases systems run in, in frame order.
3814
3812
  *
3815
3813
  * @example
3816
3814
  * ```ts
3817
- * const motion: Motion = undefined; // the hook called view.set and is already done
3815
+ * const phase: WorldPhase = "animate";
3818
3816
  * ```
3819
3817
  */
3820
- type Motion = MotionHandle | void;
3818
+ type WorldPhase = "input" | "animate" | "layout" | "sync";
3821
3819
  /**
3822
- * Only the numeric fields of a component value: what a tween can interpolate.
3820
+ * World mode. `"paused"` and `"fast"` skip the phases `input`, `animate` and `layout`; phase
3821
+ * `sync` runs in every mode.
3823
3822
  *
3824
3823
  * @example
3825
3824
  * ```ts
3826
- * type Numbers = NumericFields<{ x: number; texture: string }>; // { x: number }
3825
+ * const mode: WorldMode = "fast";
3827
3826
  * ```
3828
3827
  */
3829
- type NumericFields<Value extends object> = { [Key in keyof Value as Value[Key] extends number ? Key : never]: Value[Key] };
3828
+ type WorldMode = "live" | "paused" | "fast";
3830
3829
  /**
3831
- * One keyframe segment of a track: the fraction of the track's `ms` it ends at (`0..1`), the
3832
- * curve it eases with (the track's `ease` when left out) and the numeric fields it reaches. A
3833
- * field it does not name holds where the segment before left it.
3830
+ * One component value ready to be written: the storage key and the frozen data.
3834
3831
  *
3835
3832
  * @example
3836
3833
  * ```ts
3837
- * // The swing of a popup board: past the rest pose at 42 % of the track, then home.
3838
- * const segments: TrackSegment[] = [
3839
- * { at: 0.42, ease: "out", to: { y: 974, rotation: 0.087 } },
3840
- * { at: 1, ease: "inOut", to: { y: 960, rotation: 0 } }
3841
- * ];
3834
+ * const value: ComponentValue<{ level: number }> = { type: Item, value: { level: 2 } };
3842
3835
  * ```
3843
3836
  */
3844
- type TrackSegment = {
3845
- at: number;
3846
- ease?: Ease;
3847
- to: Record<string, number>;
3837
+ type ComponentValue<Value extends object> = {
3838
+ readonly type: ComponentType<Value>;
3839
+ readonly value: Readonly<Value>;
3848
3840
  };
3849
3841
  /**
3850
- * Options of `ViewHandle.tween`. An additive tween adds its delta over the value the absolute
3851
- * writer of the field holds, instead of owning the field. `segments` turns the tween into a
3852
- * keyframe walk: the segments run one after another over `ms` on one clock, and `to` is where the
3853
- * last one ends. `repeat` runs the walk again from its first segment when it ends: a number
3854
- * counts the extra runs, `"forever"` never ends. The world passes it to the driver unchanged;
3855
- * without a driver the target is written once.
3842
+ * A tag value. A tag carries no data and is stored as `true`.
3856
3843
  *
3857
3844
  * @example
3858
3845
  * ```ts
3859
- * const options: TweenOptions = { ms: 350, ease: "out", delayMs: 40 };
3860
- * const walk: TweenOptions = { ms: 1000, segments: [{ at: 0.42, to: { y: 974 } }, { at: 1, to: { y: 960 } }] };
3861
- * const sway: TweenOptions = { ms: 2400, additive: true, repeat: "forever", segments: [{ at: 0.5, to: { rotation: 0.05 } }, { at: 1, to: { rotation: 0 } }] };
3846
+ * const held: TagValue = { type: Held, value: true };
3862
3847
  * ```
3863
3848
  */
3864
- type TweenOptions = {
3865
- ms: number;
3866
- ease?: Ease;
3867
- delayMs?: number;
3868
- additive?: boolean;
3869
- segments?: readonly TrackSegment[];
3870
- repeat?: number | "forever";
3849
+ type TagValue = {
3850
+ readonly type: TagType;
3851
+ readonly value: true;
3871
3852
  };
3872
3853
  /**
3873
- * Options of `ViewHandle.toRest`. Defaults: the plugin's `settleMs` and ease `"out"`. `delayMs`
3874
- * lets a hook wait, for example for a flight to land, before the view comes home.
3854
+ * Any component or tag value, as `spawn` and `view` return them.
3875
3855
  *
3876
3856
  * @example
3877
3857
  * ```ts
3878
- * const options: RestOptions = { ms: 200, delayMs: 120 };
3858
+ * const values: AnyComponentValue[] = [Held()];
3879
3859
  * ```
3880
3860
  */
3881
- type RestOptions = {
3882
- ms?: number;
3883
- ease?: Ease;
3884
- delayMs?: number;
3861
+ type AnyComponentValue = {
3862
+ readonly type: AnyComponentType;
3863
+ readonly value: object | true;
3885
3864
  };
3886
3865
  /**
3887
- * What the driver is told about one track: how long it runs, how it eases, how long it waits,
3888
- * whether it adds to the field or owns it, and the keyframe segments it walks. A track with
3889
- * segments walks them over `ms` on one clock and claims every field a segment names; its target
3890
- * `to` is the last segment's target. A track with `repeat` starts its walk again when it ends
3891
- * (a number counts the extra runs, `"forever"` never ends); the instant driver ignores it and
3892
- * writes the end pose once.
3866
+ * A component type whose value shape is erased to a plain record: what the projection writes when
3867
+ * it only knows the component by name.
3893
3868
  *
3894
3869
  * @example
3895
3870
  * ```ts
3896
- * const options: TrackOptions = { ms: 250, ease: "outBack", delayMs: 0, additive: false };
3897
- * const walk: TrackOptions = { ms: 1000, segments: [{ at: 0.42, to: { y: 974 } }, { at: 1, to: { y: 960 } }] };
3898
- * const twice: TrackOptions = { ms: 400, ease: "inOut", delayMs: 0, additive: true, repeat: 1 };
3871
+ * const erased: AnyComponent = component("Transform", { x: 0, y: 0 }) as AnyComponent;
3872
+ * erased.componentName; // "Transform"
3899
3873
  * ```
3900
3874
  */
3901
- type TrackOptions = {
3902
- ms: number;
3903
- ease?: Ease;
3904
- delayMs?: number;
3905
- additive?: boolean;
3906
- segments?: readonly TrackSegment[];
3907
- repeat?: number | "forever";
3908
- };
3875
+ type AnyComponent = ComponentType<Record<string, unknown>>;
3909
3876
  /**
3910
- * The tween engine behind `ViewHandle.tween`, `toRest` and `all`. `anim` installs one with
3911
- * `setDriver`; without it the world writes every target at once.
3912
- *
3913
- * A track reads its start values when its delay ends, writes the exact target on its last frame
3914
- * and goes through `ecs.set`, so `changed()` sees it. Muted fields are never written, also not on
3915
- * `finish()`.
3877
+ * A component type made by `component()`: callable to build a value, and carrying the storage
3878
+ * name plus the defaults that give the TypeScript type, the JSON shape and the inspector schema.
3916
3879
  *
3917
3880
  * @example
3918
3881
  * ```ts
3919
- * // `anim` hands the world its own tween core in `onStart`.
3920
- * const driver: TweenDriver = { track: startTrack, cancelAll: cancelTracksOf };
3921
- * const off = ctx.require(worldPlugin).projection.setDriver(driver);
3922
- *
3923
- * off(); // `anim` stopped: the world writes instantly again
3882
+ * const Item: ComponentType<{ kind: string; level: number }> = component("Item", {
3883
+ * kind: "",
3884
+ * level: 1
3885
+ * });
3886
+ * Item({ level: 2 }); // { type: Item, value: { kind: "", level: 2 } }
3887
+ * Item.owned; // []: a projection view writes every field
3924
3888
  * ```
3925
3889
  */
3926
- type TweenDriver = {
3927
- /**
3928
- * Starts one track on one component of one entity.
3929
- *
3930
- * @param entity - The entity to animate.
3931
- * @param component - The component to animate.
3932
- * @param to - The numeric target fields.
3933
- * @param options - Duration, easing, delay and the additive flag.
3934
- * @param muted - The fields another writer owns, read at every write.
3935
- * @returns The motion handle of the track.
3936
- */
3937
- track(entity: Entity, component: AnyComponent, to: Record<string, number>, options: TrackOptions, muted: () => ReadonlySet<string>): MotionHandle;
3938
- /**
3939
- * Ends every track of one entity. Called when a view despawns and on every flush.
3940
- *
3941
- * @param entity - The entity whose tracks end.
3942
- */
3943
- cancelAll(entity: Entity): void;
3890
+ type ComponentType<Value extends object> = {
3891
+ (patch?: Partial<Value>): ComponentValue<Value>;
3892
+ readonly componentName: string;
3893
+ readonly defaults: Readonly<Value>;
3894
+ readonly kind: "component"; /** Fields a plugin writes and a projection view never does, so a view never corrects them. */
3895
+ readonly owned: readonly string[];
3944
3896
  };
3945
3897
  /**
3946
- * One node of an element description, as `ui` builds it. The world never imports `ui`: it knows
3947
- * the shape structurally, carries it in `Tree` and hands it back unchanged.
3898
+ * A tag type made by `tag()`: a component with no data.
3948
3899
  *
3949
3900
  * @example
3950
3901
  * ```ts
3951
- * const node: DescriptionNode = { type: "box", props: { gap: 8 }, children: [] };
3902
+ * const Held: TagType = tag("Held");
3903
+ * Held(); // { type: Held, value: true }
3952
3904
  * ```
3953
3905
  */
3954
- type DescriptionNode = {
3955
- type: string;
3956
- key?: string;
3957
- props: object;
3958
- children: readonly unknown[];
3906
+ type TagType = {
3907
+ (): TagValue;
3908
+ readonly componentName: string;
3909
+ readonly kind: "tag";
3959
3910
  };
3960
3911
  /**
3961
- * What one `view(item)` call returns: the components of the item, or one description node that
3962
- * the world wraps as `Tree`.
3912
+ * What the world stores about a component or tag type, independent of its data type.
3963
3913
  *
3964
3914
  * @example
3965
3915
  * ```ts
3966
- * const components: ViewOutput = [Level({ level: 2 })];
3916
+ * const registered: AnyComponentType = { componentName: "Held", kind: "tag" };
3967
3917
  * ```
3968
3918
  */
3969
- type ViewOutput = readonly AnyComponentValue[] | DescriptionNode;
3919
+ type AnyComponentType = {
3920
+ readonly componentName: string;
3921
+ readonly kind: "component" | "tag";
3922
+ };
3970
3923
  /**
3971
- * What a motion hook is handed: the one view it animates. A view never holds a reference to
3972
- * another view — `peer` answers with the other key's item, never with its entity.
3924
+ * What the ECS needs from a component type to read and write its store: the name and the
3925
+ * defaults, never the call signature. A component bound to a game's keys by a `…For` binder
3926
+ * (`Narrowed`) is the same object with a narrower call, so it passes here unchanged.
3973
3927
  *
3974
3928
  * @example
3975
3929
  * ```ts
3976
- * const enter = (view: ViewHandle<Item>, item: Item): Motion => {
3977
- * view.set(Transform, { scale: 0 });
3978
- * return view.toRest(Transform, { ms: 250 });
3979
- * };
3930
+ * const ref: ComponentHandle<{ x: number }> = component("Pos", { x: 0 });
3931
+ * ref.defaults; // { x: 0 }
3980
3932
  * ```
3981
3933
  */
3982
- type ViewHandle<Item> = {
3983
- readonly entity: Entity;
3984
- readonly key: string;
3985
- get<Value extends object>(component: ComponentType<Value>): Readonly<Value> | undefined;
3986
- rest<Value extends object>(component: ComponentType<Value>): Readonly<Value> | undefined;
3987
- set<Value extends object>(component: ComponentType<Value>, patch: Partial<Value>): void;
3988
- tween<Value extends object>(component: ComponentType<Value>, to: Partial<NumericFields<Value>>, options: TweenOptions): MotionHandle;
3989
- toRest<Value extends object>(component: ComponentType<Value>, options?: RestOptions): MotionHandle;
3990
- all(handles: readonly Motion[]): MotionHandle;
3991
- peer(key: string): Item | undefined;
3934
+ type ComponentHandle<Value extends object> = {
3935
+ readonly componentName: string;
3936
+ readonly defaults: Readonly<Value>;
3937
+ readonly kind: "component";
3992
3938
  };
3993
3939
  /**
3994
- * One `motion.change` hook, keyed by component name. `previous` and `next` are both passed, so a
3995
- * rollback that lowers a level can pick another motion than a level-up.
3940
+ * A component type whose call signature takes a narrower patch: the same object, the same
3941
+ * store, the compiler refuses what the game's generated keys do not know.
3996
3942
  *
3997
3943
  * @example
3998
3944
  * ```ts
3999
- * const slide: ChangeHook<Item> = view => view.toRest(Transform, { ms: 350 });
3945
+ * const Sprite: Narrowed<{ texture: string }, { texture?: "board.cell" }> = component("Sprite", { texture: "" });
3946
+ * Sprite({ texture: "board.cell" }).value.texture; // "board.cell"
4000
3947
  * ```
4001
3948
  */
4002
- type ChangeHook<Item> = (view: ViewHandle<Item>, previous: Item, next: Item, hint?: Hint) => Motion;
3949
+ type Narrowed<Value extends object, Patch> = Omit<ComponentType<Value>, never> & ((patch?: Patch) => ComponentValue<Value>);
4003
3950
  /**
4004
- * The `change` table of a projection: one hook per component name.
3951
+ * A resource type made by `resource()`: one mutable object per world, created from a deep clone
3952
+ * of the defaults on first read.
4005
3953
  *
4006
3954
  * @example
4007
3955
  * ```ts
4008
- * const change: ChangeHooks<Item> = { Transform: view => view.toRest(Transform) };
3956
+ * const Pointer: ResourceType<{ x: number; y: number }> = resource("Pointer", { x: 0, y: 0 });
3957
+ * Pointer.resourceName; // "Pointer"
4009
3958
  * ```
4010
3959
  */
4011
- type ChangeHooks<Item> = {
4012
- readonly [component: string]: ChangeHook<Item>;
3960
+ type ResourceType<Value extends object> = {
3961
+ readonly resourceName: string;
3962
+ readonly defaults: Readonly<Value>;
4013
3963
  };
4014
3964
  /**
4015
- * The motion hooks of a projection. Every one is optional; without a hook the component diff is
4016
- * written directly. `loop` starts what moves a view for its whole life (an additive sway, a spin):
4017
- * it is played wherever `enter` plays, next to it, and its motion is kept apart from the view's
4018
- * other motions, so a change never cancels it; it ends when the view dies.
3965
+ * A query term marked as written by `mut()`. Its only effect is change detection.
4019
3966
  *
4020
3967
  * @example
4021
3968
  * ```ts
4022
- * const motion: ProjectionMotion<Item> = {
4023
- * change: { Transform: view => view.toRest(Transform, { ms: 350 }) }
4024
- * };
4025
- * // An order card that sways forever once it entered.
4026
- * const swaying: ProjectionMotion<Item> = {
4027
- * loop: view =>
4028
- * view.tween(Transform, { rotation: 0.05 }, { ms: 1200, additive: true, repeat: "forever" })
4029
- * };
3969
+ * const written: Mut<{ x: number }> = mut(Transform);
3970
+ * written.kind; // "mut"
4030
3971
  * ```
4031
3972
  */
4032
- type ProjectionMotion<Item> = {
4033
- enter?(view: ViewHandle<Item>, item: Item, hint?: Hint): Motion;
4034
- loop?(view: ViewHandle<Item>): Motion;
4035
- exit?(view: ViewHandle<Item>, item: Item, hint?: Hint): Motion;
4036
- readonly change?: ChangeHooks<Item>;
4037
- settle?(view: ViewHandle<Item>, components: readonly string[]): Motion;
3973
+ type Mut<Value extends object> = {
3974
+ readonly kind: "mut";
3975
+ readonly of: ComponentHandle<Value>;
4038
3976
  };
4039
3977
  /**
4040
- * The motion hooks with the item type erased, as the world stores them.
3978
+ * A `mut()` term with its data type erased, as the world stores it.
4041
3979
  *
4042
3980
  * @example
4043
3981
  * ```ts
4044
- * const stored: AnyProjectionMotion = { change: {} };
3982
+ * const term: AnyMut = { kind: "mut", of: { componentName: "Transform", kind: "component" } };
4045
3983
  * ```
4046
3984
  */
4047
- type AnyProjectionMotion = {
4048
- enter?(view: ViewHandle<unknown>, item: unknown, hint?: Hint): Motion;
4049
- loop?(view: ViewHandle<unknown>): Motion;
4050
- exit?(view: ViewHandle<unknown>, item: unknown, hint?: Hint): Motion;
4051
- readonly change?: ChangeHooks<never>;
4052
- settle?(view: ViewHandle<unknown>, components: readonly string[]): Motion;
3985
+ type AnyMut = {
3986
+ readonly kind: "mut";
3987
+ readonly of: AnyComponentType;
4053
3988
  };
4054
3989
  /**
4055
- * One projection: keyed items of the model turned into entities. `Item` is inferred from the
4056
- * return of `from`; `layer` and `lift` keep their literal types so a scene can check them.
3990
+ * One term of a query: a component type, a tag type, or a `mut()` of a component type.
4057
3991
  *
4058
3992
  * @example
4059
3993
  * ```ts
4060
- * const spec: ProjectionSpec<Item, "items", "lifted", Player, Session> = {
4061
- * name: "board.items",
4062
- * layer: "items",
4063
- * lift: "lifted",
4064
- * from: player => player.board.items,
4065
- * key: item => item.id,
4066
- * view: item => [Item({ level: item.level })]
3994
+ * const terms: QueryTerm[] = [mut(Transform), Sprite, Held];
3995
+ * ```
3996
+ */
3997
+ type QueryTerm = AnyComponentType | AnyMut;
3998
+ /**
3999
+ * The value a query yields for one term: writable for `mut()`, `true` for a tag, read-only for a
4000
+ * plain component.
4001
+ *
4002
+ * @example
4003
+ * ```ts
4004
+ * type Written = ValueOf<Mut<{ x: number }>>; // { x: number }
4005
+ * ```
4006
+ */
4007
+ type ValueOf<Term> = Term extends Mut<infer Value> ? Value : Term extends TagType ? true : Term extends ComponentHandle<infer Value> ? Readonly<Value> : never;
4008
+ /**
4009
+ * The value part of a query row, one entry per term, in term order.
4010
+ *
4011
+ * @example
4012
+ * ```ts
4013
+ * type Values = QueryValues<[TagType]>; // [true]
4014
+ * ```
4015
+ */
4016
+ type QueryValues<Terms extends readonly QueryTerm[]> = { -readonly [Index in keyof Terms]: ValueOf<Terms[Index]> };
4017
+ /**
4018
+ * One row a query yields: the entity, then one value per term.
4019
+ *
4020
+ * @example
4021
+ * ```ts
4022
+ * type Row = QueryTuple<[ComponentType<{ x: number }>]>; // [Entity, Readonly<{ x: number }>]
4023
+ * ```
4024
+ */
4025
+ type QueryTuple<Terms extends readonly QueryTerm[]> = [Entity, ...QueryValues<Terms>];
4026
+ /**
4027
+ * What a system receives next to its entities: the world, the mutable resources, the frozen model
4028
+ * snapshot of the frame and the frame's `Time`. A system may read pure game rules; it never
4029
+ * writes the model.
4030
+ *
4031
+ * @example
4032
+ * ```ts
4033
+ * const run = (entities: Iterable<[Entity]>, ctx: SystemContext): void => {
4034
+ * ctx.res(Pointer).x = 0;
4035
+ * ctx.time.delta; // 16
4067
4036
  * };
4068
4037
  * ```
4069
4038
  */
4070
- type ProjectionSpec<Item, LayerName extends string, LiftName extends string, Player, Session> = {
4071
- readonly name: string;
4072
- readonly layer: LayerName;
4073
- readonly lift?: LiftName;
4074
- from(player: Player, session: Session): readonly Item[] | Item; /** Omitted when `from` returns one plain object: the key is then the name of the projection. */
4075
- key?(item: Item): string;
4076
- view(item: Item): ViewOutput;
4077
- readonly motion?: ProjectionMotion<Item>;
4039
+ type SystemContext = {
4040
+ readonly world: EcsApi;
4041
+ res<Value extends object>(resource: ResourceType<Value>): Value;
4042
+ readonly snapshot: Snapshot;
4043
+ readonly time: Readonly<Time>;
4078
4044
  };
4079
4045
  /**
4080
- * A projection spec with its item and layer types erased, as the world stores it.
4046
+ * A system definition, typed over its query tuple.
4081
4047
  *
4082
4048
  * @example
4083
4049
  * ```ts
4084
- * const stored: AnyProjectionSpec = {
4085
- * name: "board.items",
4086
- * layer: "items",
4087
- * from: () => [],
4088
- * key: () => "",
4089
- * view: () => []
4050
+ * const bob: SystemDefinition<[ComponentType<{ y: number }>]> = {
4051
+ * name: "bob",
4052
+ * phase: "animate",
4053
+ * query: [Transform],
4054
+ * run: entities => {
4055
+ * for (const [, transform] of entities) transform.y; // read-only
4056
+ * }
4090
4057
  * };
4091
4058
  * ```
4092
4059
  */
4093
- type AnyProjectionSpec = {
4060
+ type SystemDefinition<Terms extends readonly QueryTerm[]> = {
4094
4061
  readonly name: string;
4095
- readonly layer: string;
4096
- readonly lift?: string;
4097
- from(player: Json, session: Json): unknown;
4098
- key?(item: unknown): string;
4099
- view(item: unknown): ViewOutput;
4100
- readonly motion?: AnyProjectionMotion;
4062
+ readonly phase: WorldPhase;
4063
+ readonly query: Terms;
4064
+ run(entities: Iterable<QueryTuple<Terms>>, ctx: SystemContext): void;
4101
4065
  };
4102
4066
  /**
4103
- * Why a reconcile runs. Merged over the frame; the cause table decides play or direct.
4067
+ * A system with its query tuple erased, as the world stores it.
4104
4068
  *
4105
4069
  * @example
4106
4070
  * ```ts
4107
- * const cause: Cause = "edge";
4071
+ * const entry: AnySystem = { name: "bob", phase: "animate", query: [], run: () => {} };
4108
4072
  * ```
4109
4073
  */
4110
- type Cause = "edge" | "rollback" | "restore" | "load" | "mount" | "rerun";
4074
+ type AnySystem = {
4075
+ readonly name: string;
4076
+ readonly phase: WorldPhase;
4077
+ readonly query: readonly QueryTerm[];
4078
+ run(entities: Iterable<readonly unknown[]>, ctx: SystemContext): void;
4079
+ };
4111
4080
  /**
4112
- * One live or exiting view: the entity of one key of one projection, its rest pose and the
4113
- * motions that still run on it.
4081
+ * One registered system and the frame it was registered in. A system registered during a frame
4082
+ * runs from the next frame on.
4114
4083
  *
4115
4084
  * @example
4116
4085
  * ```ts
4117
- * const view: View = {
4118
- * entity: 1_048_576,
4119
- * projection: "board.items",
4120
- * key: "i7",
4121
- * item: { id: "i7" },
4122
- * rest: new Map(),
4123
- * handles: [],
4124
- * lifted: false,
4125
- * dropWhenStill: false,
4126
- * exiting: false
4127
- * };
4086
+ * const entry: SystemEntry = { definition: bobSystem, frame: 12 };
4128
4087
  * ```
4129
4088
  */
4130
- type View$1 = {
4131
- entity: Entity;
4132
- projection: string;
4133
- key: string;
4134
- item: unknown; /** The last output of `view(item)`, by component name: what the picture equals at rest. */
4135
- rest: Map<string, AnyComponentValue>;
4136
- handles: MotionHandle[];
4137
- lifted: boolean; /** `lift(false)` is waiting for the last motion to end. */
4138
- dropWhenStill: boolean;
4139
- exiting: boolean;
4089
+ type SystemEntry = {
4090
+ readonly definition: AnySystem;
4091
+ readonly frame: number;
4140
4092
  };
4141
4093
  /**
4142
- * One mounted projection: its owner, the live views by key, the despawn queue and the items of
4143
- * the last reconcile.
4094
+ * A structural change queued while a phase runs. Applied in call order when the phase ends.
4144
4095
  *
4145
4096
  * @example
4146
4097
  * ```ts
4147
- * const mounted: Mounted = {
4148
- * owner: { kind: "projection", name: "board.items" },
4149
- * live: new Map(),
4150
- * queue: [],
4151
- * items: new Map()
4152
- * };
4098
+ * const command: Command = { kind: "remove", entity: 1_048_576, component: "Held" };
4153
4099
  * ```
4154
4100
  */
4155
- type Mounted = {
4101
+ type Command = {
4102
+ kind: "attach";
4103
+ entity: Entity;
4104
+ components: readonly AnyComponentValue[];
4105
+ } | {
4106
+ kind: "despawn";
4107
+ entity: Entity;
4108
+ } | {
4109
+ kind: "despawnOwnedBy";
4156
4110
  owner: Owner;
4157
- live: Map<string, View$1>;
4158
- queue: View$1[];
4159
- items: Map<string, unknown>;
4111
+ } | {
4112
+ kind: "add";
4113
+ entity: Entity;
4114
+ value: AnyComponentValue;
4115
+ } | {
4116
+ kind: "remove";
4117
+ entity: Entity;
4118
+ component: string;
4160
4119
  };
4161
4120
  /**
4162
- * One track the projection started on the driver. The world keeps the component name and the
4163
- * handle, so it knows which components are driven without knowing how the driver runs them.
4121
+ * A structural listener registered with `onAdded` or `onRemoved`.
4164
4122
  *
4165
4123
  * @example
4166
4124
  * ```ts
4167
- * const track: Track = { entity: 1_048_576, component: "Transform", handle: flight };
4168
- * track.handle.active(); // true while the flight runs
4125
+ * const hook: StructuralHook = (entity, value) => void entity + String(value);
4169
4126
  * ```
4170
4127
  */
4171
- type Track = {
4172
- entity: Entity;
4173
- component: string;
4174
- handle: MotionHandle;
4128
+ type StructuralHook = (entity: Entity, value: unknown) => void;
4129
+ /**
4130
+ * The frozen model snapshot of one frame, kept so four phases read it once.
4131
+ *
4132
+ * @example
4133
+ * ```ts
4134
+ * const cached: FrameSnapshot = { frame: 12, snapshot: { player: {}, session: {}, rng: rngState } };
4135
+ * ```
4136
+ */
4137
+ type FrameSnapshot = {
4138
+ frame: number;
4139
+ snapshot: Snapshot;
4175
4140
  };
4176
4141
  /**
4177
- * The rest pose of one entity, by component name: the last output of `view(item)` for a view, or
4178
- * what `setRest` recorded for an element another plugin owns.
4142
+ * The JSON kind of one component field, read from its default value: what the editor's component
4143
+ * palette shows next to the field name. A nested shape is not described; `defaults` carries it.
4144
+ *
4145
+ * @example
4146
+ * ```ts
4147
+ * const kinds: Record<string, FieldKind> = { x: "number", pivot: "object", tags: "array" };
4148
+ * ```
4179
4149
  */
4180
- type RestPose = Map<string, AnyComponentValue>;
4150
+ type FieldKind = "number" | "string" | "boolean" | "object" | "array" | "null";
4181
4151
  /**
4182
- * Where a keyed element lives: a plugin above registered it with `registerKey`, so `entityOf`
4183
- * and `keyOf` answer for it next to the views.
4152
+ * One component or tag type the world met, as `ecs.schema()` lists it.
4184
4153
  *
4185
4154
  * @example
4186
4155
  * ```ts
4187
- * const key: ProjectionKey = { projection: "hud", key: "coins" };
4156
+ * const held: ComponentSchema = {
4157
+ * name: "Held", kind: "tag", json: true, fields: {}, defaults: true, owned: []
4158
+ * };
4159
+ * const display: ComponentSchema = {
4160
+ * name: "Display", kind: "component", json: false, fields: {}, defaults: null, owned: []
4161
+ * };
4188
4162
  * ```
4189
4163
  */
4190
- type ProjectionKey = {
4191
- projection: string;
4192
- key: string;
4164
+ type ComponentSchema = {
4165
+ /** The storage name, `componentName`. */name: string;
4166
+ kind: "component" | "tag"; /** False when the defaults are not JSON, as `Display`'s: `fields` is then `{}` and `defaults` `null`. */
4167
+ json: boolean; /** Field name to the JSON kind of its default. */
4168
+ fields: Record<string, FieldKind>; /** The defaults as JSON; `true` for a tag. */
4169
+ defaults: Json; /** The fields a plugin writes and a view never does; `[]` for a tag. */
4170
+ owned: readonly string[];
4193
4171
  };
4194
4172
  /**
4195
- * What the next reconcile has to do: why it runs, which roots changed and whether `view` is
4196
- * forced for every item.
4173
+ * The first and the last value of one component inside a frame window. `null` on a side means
4174
+ * the entity did not carry the component there: component values are objects or `true`, never
4175
+ * `null`.
4197
4176
  *
4198
4177
  * @example
4199
4178
  * ```ts
4200
- * const dirty: Dirty = { causes: ["edge"], roots: new Set(["player"]), force: false };
4179
+ * const levelUp: ComponentChange = { from: { level: 2 }, to: { level: 3 } };
4180
+ * const picked: ComponentChange = { from: null, to: true }; // the tag Held was added
4201
4181
  * ```
4202
4182
  */
4203
- type Dirty = {
4204
- causes: Cause[];
4205
- roots: Set<Root>;
4206
- force: boolean;
4183
+ type ComponentChange = {
4184
+ from: Json | null;
4185
+ to: Json | null;
4207
4186
  };
4208
4187
  /**
4209
- * projection module state.
4188
+ * What happened to one entity between two frames: who owns it, where it lives now, whether it
4189
+ * came, went or changed, and every component that really changed.
4190
+ *
4191
+ * @example
4192
+ * ```ts
4193
+ * const merged: EntityDiff = {
4194
+ * id: 1_048_577,
4195
+ * owner: { kind: "projection", name: "board.items" },
4196
+ * key: { projection: "board.items", key: "i7" },
4197
+ * change: "changed",
4198
+ * components: { Level: { from: { level: 2 }, to: { level: 3 } } }
4199
+ * };
4200
+ * ```
4210
4201
  */
4211
- type ProjectionState = {
4212
- specs: Map<string, AnyProjectionSpec>;
4213
- layers: readonly LayerSpec[];
4214
- mounted: Map<string, Mounted>;
4215
- byEntity: Map<Entity, View$1>;
4216
- dirty: Dirty | undefined;
4217
- hints: Hint[]; /** The tracks the projection started, with the handle that says whether they still run. */
4218
- tracks: Track[]; /** The tween engine `anim` installed, or `undefined`: then every target is written at once. */
4219
- driver: TweenDriver | undefined; /** The rest pose of an entity that is not a view, recorded by its owner through `setRest`. */
4220
- rests: Map<Entity, RestPose>; /** Projection name to key to the entity a plugin above registered under it. */
4221
- keys: Map<string, Map<string, Entity>>; /** The reverse of `keys`, so `keyOf` answers for a registered element too. */
4222
- keysByEntity: Map<Entity, ProjectionKey>; /** Entity to component name to the fields another writer owns. */
4223
- mutes: Map<Entity, Map<string, Set<string>>>;
4224
- offHints: Array<() => void>;
4202
+ type EntityDiff = {
4203
+ id: Entity;
4204
+ owner: Owner; /** The live address at read time; `undefined` once the entity is gone, or when it has no key. */
4205
+ key: ProjectionKey | undefined;
4206
+ change: "spawned" | "despawned" | "changed";
4207
+ components: Record<string, ComponentChange>;
4225
4208
  };
4226
4209
  /**
4227
- * projection module API, `app.world.projection`: the bridge from the model to entities.
4210
+ * What changed between the world at the end of frame `from` and the world at the end of frame
4211
+ * `to`, as `ecs.diff` answers it. The entities are sorted by id.
4228
4212
  *
4229
4213
  * @example
4230
4214
  * ```ts
4231
- * // A test mounts the board and asks which entity carries one model key.
4232
- * app.world.projection.mount(["board.items"], { kind: "plugin", name: "test" });
4233
- * app.world.projection.entityOf("board.items", "i7"); // 1048576
4215
+ * const quiet: FrameDiff = { from: 120, to: 180, entities: [] };
4234
4216
  * ```
4235
4217
  */
4236
- type ProjectionApi = {
4237
- /**
4238
- * Stores a projection spec. Nothing is drawn until `mount`.
4239
- *
4240
- * @param spec - The projection, built with `projection()`.
4241
- * @throws {Error} When a projection of that name is already registered.
4242
- * @example
4243
- * ```ts
4244
- * // A test registers one projection by hand instead of composing a feature.
4245
- * app.world.projection.register(boardItems);
4246
- * app.world.projection.entityOf("board.items", "i7"); // undefined: nothing is mounted yet
4247
- * ```
4248
- */
4249
- register(spec: AnyProjectionSpec): void;
4250
- /**
4251
- * Stores the layer list of the scene. The order of the list is draw order; every call stores a
4252
- * new frozen array.
4218
+ type FrameDiff = {
4219
+ from: number;
4220
+ to: number;
4221
+ entities: readonly EntityDiff[];
4222
+ };
4223
+ /**
4224
+ * One record of the frame history: one component of one entity, before and after. `null` means
4225
+ * absent, so a spawn records `from: null` and a despawn `to: null`.
4226
+ *
4227
+ * @example
4228
+ * ```ts
4229
+ * const levelUp: HistoryChange = {
4230
+ * id: 1_048_577,
4231
+ * owner: { kind: "projection", name: "board.items" },
4232
+ * component: "Level",
4233
+ * from: { level: 2 },
4234
+ * to: { level: 3 }
4235
+ * };
4236
+ * ```
4237
+ */
4238
+ type HistoryChange = {
4239
+ id: Entity;
4240
+ owner: Owner;
4241
+ component: string;
4242
+ from: Json | null;
4243
+ to: Json | null;
4244
+ };
4245
+ /**
4246
+ * The records of one frame, and the entities that frame spawned and despawned: a record alone
4247
+ * cannot tell a spawn from a component added to a live entity.
4248
+ *
4249
+ * @example
4250
+ * ```ts
4251
+ * const quiet: HistorySlot = { frame: 96, changes: [], spawned: [], despawned: [] };
4252
+ * ```
4253
+ */
4254
+ type HistorySlot = {
4255
+ frame: number;
4256
+ changes: HistoryChange[];
4257
+ spawned: Entity[];
4258
+ despawned: Entity[];
4259
+ };
4260
+ /**
4261
+ * The last recorded JSON of one live entity: its owner and its components by name.
4262
+ */
4263
+ type ShadowEntry = {
4264
+ owner: Owner;
4265
+ components: Map<string, Json>;
4266
+ };
4267
+ /**
4268
+ * The frame history of a dev build: one shadow copy of the world as JSON and a ring of the
4269
+ * records of the last frames, one slot per frame at index `frame % HISTORY_FRAMES`.
4270
+ */
4271
+ type History = {
4272
+ /** The last recorded JSON of every live entity. */shadow: Map<Entity, ShadowEntry>; /** `HISTORY_FRAMES` long. */
4273
+ slots: Array<HistorySlot | undefined>; /** The last frame recorded, `undefined` before the first one. */
4274
+ newest: number | undefined; /** The frame the recording started on: the shadow is the world at its end. */
4275
+ started: number | undefined;
4276
+ };
4277
+ /**
4278
+ * ecs module state.
4279
+ */
4280
+ type EcsState = {
4281
+ generations: number[];
4282
+ free: number[];
4283
+ owners: Map<Entity, Owner>; /** Key is `"kind:name"` of the owner. */
4284
+ byOwner: Map<string, Set<Entity>>;
4285
+ types: Map<string, AnyComponentType>;
4286
+ stores: Map<string, Map<Entity, object | true>>;
4287
+ resources: Map<string, object>;
4288
+ systems: Record<WorldPhase, SystemEntry[]>;
4289
+ running: WorldPhase | undefined;
4290
+ commands: Command[];
4291
+ changed: Map<string, Set<Entity>>;
4292
+ added: Map<string, StructuralHook[]>;
4293
+ removed: Map<string, StructuralHook[]>;
4294
+ ownerLeft: Array<(owner: Owner) => void>;
4295
+ mode: WorldMode;
4296
+ offFrame: Array<() => void>;
4297
+ frameSnapshot: FrameSnapshot | undefined; /** The frame history: dev builds only, from the first frame on; `undefined` before it. */
4298
+ history: History | undefined;
4299
+ };
4300
+ /**
4301
+ * ecs module API, `app.world.ecs`: the screen as data. Plain-object components in one store per
4302
+ * component type, generational entity ids, a mandatory owner, a command buffer applied between
4303
+ * phases, coarse change detection and fixed phases driven by `time`.
4304
+ *
4305
+ * @example
4306
+ * ```ts
4307
+ * // A test owns its entities and reads them back.
4308
+ * const entity = app.world.ecs.spawn({ kind: "plugin", name: "test" }, [Transform({ x: 10 })]);
4309
+ * app.world.ecs.get(entity, Transform); // { x: 10, y: 0, rotation: 0, scale: 1, pivot: { x: 0, y: 0 } }
4310
+ * ```
4311
+ */
4312
+ type EcsApi = {
4313
+ /**
4314
+ * Registers a system at the end of its phase list. A system registered during a frame runs
4315
+ * from the next frame on.
4253
4316
  *
4254
- * @param list - The layers, in draw order.
4317
+ * @param definition - The system, built with `system()`.
4318
+ * @returns The remover; calling it twice is a no-op.
4319
+ * @throws {Error} When a system of that name is already registered.
4255
4320
  * @example
4256
4321
  * ```ts
4257
- * // `scenes` switches to the board scene.
4322
+ * // `text` registers its layout system when the plugin starts.
4258
4323
  * const world = ctx.require(worldPlugin);
4259
- * world.projection.setLayers([
4260
- * { name: "board", sort: "none" },
4261
- * { name: "items", sort: "y" },
4262
- * { name: "lifted", sort: "none" }
4263
- * ]);
4324
+ * const off = world.ecs.system(measureLabels); // runs from the next frame
4325
+ *
4326
+ * off(); // the plugin stops
4264
4327
  * ```
4265
4328
  */
4266
- setLayers(list: ReadonlyArray<LayerSpec>): void;
4329
+ system(definition: AnySystem): () => void;
4267
4330
  /**
4268
- * The current layer list, empty before the first `setLayers`. A new list is a new reference, so
4269
- * a reader compares by identity.
4331
+ * Creates an entity. The id is reserved and returned at once, also inside a phase, where the
4332
+ * components are attached when the phase ends.
4270
4333
  *
4271
- * @returns The layers, in draw order.
4334
+ * @param owner - Who owns the entity. Required by the type.
4335
+ * @param components - Component and tag values the entity starts with.
4336
+ * @returns The new entity id.
4272
4337
  * @example
4273
4338
  * ```ts
4274
- * // `renderer.sync` rebuilds its containers only when the scene really changed.
4275
- * const current = app.world.projection.layers();
4276
- * if (current !== lastLayers) rebuildContainers(current); // [{ name: "board", sort: "none" }, ...]
4339
+ * // `ui` spawns a popup entity it owns, so closing the node frees it in one call.
4340
+ * const entity = app.world.ecs.spawn({ kind: "node", name: "reward" }, [
4341
+ * Transform({ x: 540, y: 960 })
4342
+ * ]);
4343
+ * app.world.ecs.has(entity, Transform); // true
4277
4344
  * ```
4278
4345
  */
4279
- layers(): ReadonlyArray<LayerSpec>;
4346
+ spawn(owner: Owner, components: readonly AnyComponentValue[]): Entity;
4280
4347
  /**
4281
- * Marks projections mounted under an owner and reconciles them at once, direct: a scene brings
4282
- * its own transition. Nothing is mounted when the call throws.
4348
+ * Removes every component of an entity, each firing `onRemoved`, frees the index and bumps the
4349
+ * generation. A stale id is a no-op.
4283
4350
  *
4284
- * @param names - Names of the projections to mount.
4285
- * @param owner - Who owns the entities of these projections.
4286
- * @throws {Error} For an unknown name, or a `layer` or `lift` the scene does not declare.
4351
+ * @param entity - The entity to remove.
4287
4352
  * @example
4288
4353
  * ```ts
4289
- * // `scenes` mounts the projections of the board scene it just switched to.
4290
- * const world = ctx.require(worldPlugin);
4291
- * world.projection.mount(["board.cells", "board.items"], { kind: "plugin", name: "scenes" });
4292
- * world.projection.entityOf("board.items", "i7"); // 1048576, the picture is already there
4354
+ * // `ui` closes the popup it spawned. Asking again answers with the empty world.
4355
+ * app.world.ecs.despawn(popup);
4356
+ * app.world.ecs.has(popup, Transform); // false
4293
4357
  * ```
4294
4358
  */
4295
- mount(names: readonly string[], owner: Owner): void;
4359
+ despawn(entity: Entity): void;
4296
4360
  /**
4297
- * Unmounts projections: every motion of their views is finished, and the live views and the
4298
- * despawn queue are despawned in the same call.
4361
+ * Despawns exactly the entities of one owner and tells `projection` that the owner left.
4299
4362
  *
4300
- * @param names - Names of the projections to unmount.
4363
+ * @param owner - The owner whose entities go.
4301
4364
  * @example
4302
4365
  * ```ts
4303
- * // `scenes` leaves the board scene.
4304
- * const world = ctx.require(worldPlugin);
4305
- * world.projection.unmount(["board.cells", "board.items"]);
4306
- * world.projection.entityOf("board.items", "i7"); // undefined
4366
+ * // A test cleans up after itself without touching the projections of the game.
4367
+ * app.world.ecs.despawnOwnedBy({ kind: "plugin", name: "test" });
4368
+ * app.world.ecs.snapshot(); // { mode: "live", entities: [], resources: {} }
4307
4369
  * ```
4308
4370
  */
4309
- unmount(names: readonly string[]): void;
4371
+ despawnOwnedBy(owner: Owner): void;
4310
4372
  /**
4311
- * A drop with no commit: cancels the view's motions and plays settle for every loose component.
4312
- * In mode `paused` or `fast` the rest pose is written at once. No-op for an entity that is not
4313
- * a live view.
4373
+ * Reads a component value. A stale id or a missing component gives `undefined`.
4314
4374
  *
4315
- * @param entity - The entity of the view that was dropped.
4375
+ * @param entity - The entity to read.
4376
+ * @param component - The component type.
4377
+ * @returns The stored value, read-only, or `undefined`.
4316
4378
  * @example
4317
4379
  * ```ts
4318
- * // `input` released the finger over nothing, so the gate refused the answer.
4380
+ * // `input` asks whether the entity under the finger may be dragged.
4319
4381
  * const world = ctx.require(worldPlugin);
4320
- * world.projection.settle(held); // the item glides back to its cell over settleMs
4382
+ * world.ecs.get(hit, Draggable); // { payload: { id: "i7" } }
4383
+ * world.ecs.get(hit, DropTarget); // undefined: this item takes no drop
4321
4384
  * ```
4322
4385
  */
4323
- settle(entity: Entity): void;
4386
+ get<Value extends object>(entity: Entity, component: ComponentHandle<Value>): Readonly<Value> | undefined;
4324
4387
  /**
4325
- * Hands a set of fields to another writer. Tracks never write them, also not on `finish()`.
4388
+ * Shallow-merges a patch into a stored component and marks the entity changed for it. Never
4389
+ * queued: a `set` inside a phase is visible to the next system of that phase.
4326
4390
  *
4327
- * @param entity - The entity of the view.
4328
- * @param component - The component whose fields are taken over.
4329
- * @param fields - The field names the caller owns.
4330
- * @returns The remover; safe after the entity left, and a no-op when called twice.
4391
+ * @param entity - The entity to write.
4392
+ * @param component - The component type.
4393
+ * @param patch - The fields to overwrite.
4394
+ * @throws {Error} When the entity is stale or does not carry the component.
4331
4395
  * @example
4332
4396
  * ```ts
4333
- * // `input` owns the position of the held view while the finger drags it.
4397
+ * // `input` moves the held view with the finger.
4334
4398
  * const world = ctx.require(worldPlugin);
4335
- * const release = world.projection.mute(held, Transform, ["x", "y"]);
4336
- *
4337
- * release(); // the finger let go
4399
+ * world.ecs.set(held, Transform, { x: 420, y: 810 });
4400
+ * world.ecs.get(held, Transform); // { x: 420, y: 810, rotation: 0, scale: 1, pivot: { x: 0, y: 0 } }
4338
4401
  * ```
4339
4402
  */
4340
- mute(entity: Entity, component: ComponentType<object>, fields: readonly string[]): () => void;
4403
+ set<Value extends object>(entity: Entity, component: ComponentHandle<Value>, patch: Partial<Value>): void;
4341
4404
  /**
4342
- * Moves a view into or out of its projection's `lift` layer. `lift(entity, false)` on a view
4343
- * that still moves takes effect when its last motion ends, so a settling view stays above the
4344
- * board until it is home.
4405
+ * Adds a component value. On an entity that already carries it the value is replaced and the
4406
+ * entity is marked changed; no `onAdded` fires.
4345
4407
  *
4346
- * @param entity - The entity of the view.
4347
- * @param on - True to lift, false to drop back.
4408
+ * @param entity - The entity to write.
4409
+ * @param value - The component or tag value, built by calling its type.
4348
4410
  * @example
4349
4411
  * ```ts
4350
- * // `input` lifts the item the finger picked up so it draws above the board.
4412
+ * // `text` gives the label entity its display object once the atlas is there.
4351
4413
  * const world = ctx.require(worldPlugin);
4352
- * world.projection.lift(held, true);
4353
- *
4354
- * world.projection.lift(held, false); // on release: takes effect when the settle motion ends
4414
+ * world.ecs.add(label, Display({ object: bitmapText }));
4415
+ * world.ecs.has(label, Display); // true
4355
4416
  * ```
4356
4417
  */
4357
- lift(entity: Entity, on: boolean): void;
4418
+ add(entity: Entity, value: AnyComponentValue): void;
4358
4419
  /**
4359
- * The projection and key of an entity. Also answers for a view in the despawn queue and for an
4360
- * element registered with `registerKey`.
4420
+ * Removes a component. Fires `onRemoved` when the entity carried it.
4361
4421
  *
4362
- * @param entity - The entity to ask about.
4363
- * @returns The projection name and the key, or `undefined`.
4422
+ * @param entity - The entity to write.
4423
+ * @param component - The component or tag type to drop.
4364
4424
  * @example
4365
4425
  * ```ts
4366
- * // `renderer.sync` labels its display objects for the inspector.
4367
- * app.world.projection.keyOf(1_048_576); // { projection: "board.items", key: "i7" }
4368
- * app.world.projection.keyOf(42); // undefined: not a view
4426
+ * // `text` drops the display object when the label leaves the screen.
4427
+ * const world = ctx.require(worldPlugin);
4428
+ * world.ecs.remove(label, Display);
4429
+ * world.ecs.has(label, Display); // false
4369
4430
  * ```
4370
4431
  */
4371
- keyOf(entity: Entity): ProjectionKey | undefined;
4432
+ remove(entity: Entity, component: AnyComponentType): void;
4372
4433
  /**
4373
- * The rest value of one component of an entity: what a view holds when nothing animates it, or
4374
- * what `setRest` recorded for an element. Read-only and owner-free, so a plugin that owns no
4375
- * entity can aim at one.
4434
+ * Tells whether an entity carries a component or tag. False for a stale id.
4376
4435
  *
4377
- * @param entity - A projection view or a registered element.
4378
- * @param component - The component whose rest value is asked.
4379
- * @returns The rest value, or `undefined` when the entity has none recorded.
4436
+ * @param entity - The entity to read.
4437
+ * @param component - The component or tag type.
4438
+ * @returns True when the entity carries it.
4380
4439
  * @example
4381
4440
  * ```ts
4382
- * // `anim` builds a coin flight toward the counter: `at(target)` is the counter's rest pose.
4441
+ * // `input` never hit-tests a view that is on its way out.
4383
4442
  * const world = ctx.require(worldPlugin);
4384
- * const counter = world.projection.entityOf("hud", "coins") ?? 0;
4385
- *
4386
- * world.projection.restOf(counter, Transform); // { x: 40, y: 120, rotation: 0, scale: 1, pivot: { x: 0, y: 0 } }
4443
+ * world.ecs.has(candidate, Exiting); // true while the merge animation plays
4387
4444
  * ```
4388
4445
  */
4389
- restOf<Value extends object>(entity: Entity, component: ComponentType<Value>): Value | undefined;
4446
+ has(entity: Entity, component: AnyComponentType): boolean;
4390
4447
  /**
4391
- * The entity of one key of one projection: a live view, never the despawn queue, or an element
4392
- * a plugin above registered under that key.
4448
+ * Adds a tag. Structural and idempotent.
4393
4449
  *
4394
- * @param projection - Name of the projection.
4395
- * @param key - The model key.
4396
- * @returns The entity, or `undefined`.
4450
+ * @param entity - The entity to write.
4451
+ * @param tagType - The tag type.
4397
4452
  * @example
4398
4453
  * ```ts
4399
- * // `input` resolves the target of a scripted drag.
4454
+ * // `input` marks the view under the finger while the drag runs.
4400
4455
  * const world = ctx.require(worldPlugin);
4401
- * world.projection.entityOf("board.items", "i7"); // 1048576
4402
- * world.projection.entityOf("board.items", "gone"); // undefined
4456
+ * world.ecs.tag(dragged, Held);
4457
+ * world.ecs.has(dragged, Held); // true
4403
4458
  * ```
4404
4459
  */
4405
- entityOf(projection: string, key: string): Entity | undefined;
4460
+ tag(entity: Entity, tagType: TagType): void;
4406
4461
  /**
4407
- * The live view entities of a mounted projection, in the order of its model keys. A view that
4408
- * plays its exit is left out, and so is an element registered with `registerKey`. Every call
4409
- * hands out a new array.
4462
+ * Removes a tag. Structural and idempotent.
4410
4463
  *
4411
- * @param name - Name of the projection.
4412
- * @returns The entities, or `[]` when the projection is not mounted.
4464
+ * @param entity - The entity to write.
4465
+ * @param tagType - The tag type.
4413
4466
  * @example
4414
4467
  * ```ts
4415
- * // `ui` hosts the board inside a slot and parents every board item to it on reconcile.
4468
+ * // `input` releases the view on pointer up.
4416
4469
  * const world = ctx.require(worldPlugin);
4417
- * world.projection.entitiesOf("board.items"); // [1048580, 1048581]
4418
- * world.projection.entitiesOf("shop.items"); // []: not mounted
4470
+ * world.ecs.untag(dragged, Held);
4471
+ * world.ecs.has(dragged, Held); // false
4419
4472
  * ```
4420
4473
  */
4421
- entitiesOf(name: string): readonly Entity[];
4474
+ untag(entity: Entity, tagType: TagType): void;
4422
4475
  /**
4423
- * Installs the tween engine every `ViewHandle.tween`, `toRest` and `all` then runs on. Without
4424
- * one the world writes every target at once and hands back an inactive handle.
4476
+ * Walks the store of the FIRST term in insertion order and keeps the entities that carry every
4477
+ * term. Deterministic. A `mut()` term marks every yielded entity changed for that component.
4425
4478
  *
4426
- * @param driver - The engine that runs the tracks.
4427
- * @returns The remover; it puts the instant writes back.
4479
+ * @param terms - Component types, tag types and `mut()` terms.
4480
+ * @returns The matching rows: the entity and one value per term.
4428
4481
  * @example
4429
4482
  * ```ts
4430
- * // `anim` installs its tween core when it starts and takes it back when it stops.
4431
- * const world = ctx.require(worldPlugin);
4432
- * ctx.state.removeDriver = world.projection.setDriver(createDriver(ctx, ctx.state));
4483
+ * // `renderer.sync` walks every sprite that has a place on the screen.
4484
+ * for (const [entity, sprite, transform] of app.world.ecs.query(Sprite, Transform)) {
4485
+ * display.get(entity)?.position.set(transform.x, transform.y);
4486
+ * sprite.texture; // "board.item-red-1"
4487
+ * }
4488
+ * ```
4489
+ */
4490
+ query<const Terms extends readonly QueryTerm[]>(...terms: Terms): Iterable<QueryTuple<Terms>>;
4491
+ /**
4492
+ * Returns the one mutable value of a resource, created from a deep clone of its defaults on
4493
+ * first read.
4433
4494
  *
4434
- * ctx.state.removeDriver(); // onStop: motions are instant again
4495
+ * @param resourceType - The resource type.
4496
+ * @returns The mutable resource value.
4497
+ * @example
4498
+ * ```ts
4499
+ * // `input` writes the pointer once per frame; every game system reads the same object.
4500
+ * const world = ctx.require(worldPlugin);
4501
+ * const pointer = world.ecs.resource(Pointer);
4502
+ * pointer.down = true;
4435
4503
  * ```
4436
4504
  */
4437
- setDriver(driver: TweenDriver): () => void;
4505
+ resource<Value extends object>(resourceType: ResourceType<Value>): Value;
4438
4506
  /**
4439
- * A view handle for an entity that is not a projection view, so an element another plugin owns
4440
- * can be animated the same way. `set`, `get`, `tween`, `toRest` and `all` work; `rest` answers
4441
- * what `setRest` recorded; `peer` answers `undefined`.
4507
+ * Registers a listener for the moment a component is added, called when the structural change
4508
+ * is applied. A throwing listener is logged and the others still run.
4442
4509
  *
4443
- * @param entity - The entity of the element.
4444
- * @param owner - Who is asking; without it only the kind of the owner is checked.
4445
- * @returns The handle, or `undefined` for a projection view and for a foreign entity.
4510
+ * @param component - The component type to watch.
4511
+ * @param fn - Called with the entity and the stored value.
4512
+ * @returns The remover; calling it twice is a no-op.
4446
4513
  * @example
4447
4514
  * ```ts
4448
- * // `ui` plays the `motion` prop of an element it owns.
4515
+ * // `renderer.sync` creates a Pixi object for every new sprite.
4449
4516
  * const world = ctx.require(worldPlugin);
4450
- * const handle = world.projection.viewOf(button, { kind: "plugin", name: "ui" });
4517
+ * const off = world.ecs.onAdded(Sprite, (entity, sprite) => {
4518
+ * objects.set(entity, makeSprite(sprite.texture));
4519
+ * });
4451
4520
  *
4452
- * handle?.tween(Transform, { scale: 1.1 }, { ms: 120, ease: "outBack" });
4521
+ * off(); // the renderer stops
4453
4522
  * ```
4454
4523
  */
4455
- viewOf(entity: Entity, owner: Owner): ViewHandle<unknown> | undefined;
4524
+ onAdded<Value extends object>(component: ComponentHandle<Value>, fn: (entity: Entity, value: Readonly<Value>) => void): () => void;
4456
4525
  /**
4457
- * Records what one component of an element looks like at rest, so `toRest` knows where home is.
4526
+ * Registers a listener for the moment a tag is added: `tag` on an entity without it, or a spawn
4527
+ * that carries it. A tag has no value, so the listener gets the entity only.
4458
4528
  *
4459
- * @param entity - The entity of the element.
4460
- * @param component - The component whose rest value is recorded.
4461
- * @param value - The value the element holds when nothing animates.
4529
+ * @param tagType - The tag type to watch.
4530
+ * @param fn - Called with the entity that got the tag.
4531
+ * @returns The remover; calling it twice is a no-op.
4462
4532
  * @example
4463
4533
  * ```ts
4464
- * // `ui` laid the coin counter out and tells the world where it belongs.
4534
+ * // `ui` shows the pressed look while `input` holds the `Pressed` tag on a button.
4465
4535
  * const world = ctx.require(worldPlugin);
4466
- * world.projection.setRest(counter, Transform, { x: 40, y: 120, rotation: 0, scale: 1, pivot: { x: 0, y: 0 } });
4536
+ * const off = world.ecs.onAdded(Pressed, entity => markPointer(entity, "pressed", true));
4467
4537
  *
4468
- * world.projection.viewOf(counter, { kind: "plugin", name: "ui" })?.rest(Transform); // { x: 40, y: 120, rotation: 0, scale: 1, pivot: { x: 0, y: 0 } }
4538
+ * world.ecs.tag(button, Pressed); // markPointer(button, "pressed", true)
4539
+ * off(); // ui stops
4469
4540
  * ```
4470
4541
  */
4471
- setRest<Value extends object>(entity: Entity, component: ComponentType<Value>, value: Value): void;
4542
+ onAdded(tagType: TagType, fn: (entity: Entity) => void): () => void;
4472
4543
  /**
4473
- * Gives an entity a projection and key of its own, so `entityOf` and `keyOf` answer for it next
4474
- * to the live views. That is how an element becomes a target for `anim`, `input` and `guide`.
4544
+ * Registers a listener for the moment a component is removed.
4475
4545
  *
4476
- * @param projection - Name the element is addressed under.
4477
- * @param key - Key the element is addressed under.
4478
- * @param entity - The entity of the element.
4479
- * @returns The remover; it drops the key again.
4480
- * @throws {Error} When a live view of that projection already holds the key.
4546
+ * @param component - The component type to watch.
4547
+ * @param fn - Called with the entity and the value it had.
4548
+ * @returns The remover; calling it twice is a no-op.
4481
4549
  * @example
4482
4550
  * ```ts
4483
- * // `ui` publishes the coin counter of the HUD.
4551
+ * // `renderer.sync` destroys the Pixi object of a sprite that left.
4484
4552
  * const world = ctx.require(worldPlugin);
4485
- * const drop = world.projection.registerKey("hud", "coins", counter);
4553
+ * const off = world.ecs.onRemoved(Sprite, entity => {
4554
+ * objects.get(entity)?.destroy();
4555
+ * objects.delete(entity);
4556
+ * });
4486
4557
  *
4487
- * world.projection.entityOf("hud", "coins"); // the entity of the counter
4488
- * drop(); // the element left the screen
4558
+ * off(); // the renderer stops
4489
4559
  * ```
4490
4560
  */
4491
- registerKey(projection: string, key: string, entity: Entity): () => void;
4561
+ onRemoved<Value extends object>(component: ComponentHandle<Value>, fn: (entity: Entity, value: Readonly<Value>) => void): () => void;
4492
4562
  /**
4493
- * Marks every mounted projection dirty and forces `view` for every item, so the next frame
4494
- * rebuilds the picture directly.
4563
+ * Registers a listener for the moment a tag is removed: `untag` on an entity that had it, or a
4564
+ * despawn. The listener gets the entity only.
4495
4565
  *
4566
+ * @param tagType - The tag type to watch.
4567
+ * @param fn - Called with the entity that lost the tag.
4568
+ * @returns The remover; calling it twice is a no-op.
4496
4569
  * @example
4497
4570
  * ```ts
4498
- * // `i18n` switched the locale: every label has to be projected again.
4571
+ * // `ui` drops the pressed look when `input` releases the button.
4499
4572
  * const world = ctx.require(worldPlugin);
4500
- * world.projection.rerunAll(); // the next frame writes the new strings with no motion
4573
+ * const off = world.ecs.onRemoved(Pressed, entity => markPointer(entity, "pressed", false));
4574
+ *
4575
+ * world.ecs.untag(button, Pressed); // markPointer(button, "pressed", false)
4576
+ * off(); // ui stops
4501
4577
  * ```
4502
4578
  */
4503
- rerunAll(): void;
4504
- };
4505
- /**
4506
- * projection methods the plugin root drives from the frame and the hooks. Not public.
4507
- */
4508
- type ProjectionInternal = {
4579
+ onRemoved(tagType: TagType, fn: (entity: Entity) => void): () => void;
4509
4580
  /**
4510
- * Records a commit. In fast mode it reconciles at once, direct.
4581
+ * The coarse change set of the frame, cleared in `time` phase `signals` after every `sync`
4582
+ * system ran.
4511
4583
  *
4512
- * @param roots - The state roots the commit touched.
4513
- * @param cause - Why the commit happened.
4584
+ * @param component - The component type to ask about.
4585
+ * @returns The entities marked changed for it this frame.
4586
+ * @example
4587
+ * ```ts
4588
+ * // `renderer.sync` moves only what moved.
4589
+ * for (const entity of app.world.ecs.changed(Transform)) {
4590
+ * const transform = app.world.ecs.get(entity, Transform);
4591
+ * display.get(entity)?.position.set(transform?.x ?? 0, transform?.y ?? 0);
4592
+ * }
4593
+ * ```
4514
4594
  */
4515
- markDirty(roots: readonly Root[], cause: Cause): void;
4595
+ changed(component: AnyComponentType): Iterable<Entity>;
4516
4596
  /**
4517
- * Reconciles when the world is dirty. Runs at the start of `time` phase `input`.
4597
+ * The effective mode: `"fast"` while the flow walks fast, otherwise the stored one.
4598
+ *
4599
+ * @returns The mode the frame runs in.
4600
+ * @example
4601
+ * ```ts
4602
+ * // A test checks that a fast walk froze the tweens.
4603
+ * app.flow.setMode("fast");
4604
+ * app.world.ecs.mode(); // "fast", even though setMode was never called on the world
4605
+ * ```
4518
4606
  */
4519
- reconcileIfDirty(): void;
4607
+ mode(): WorldMode;
4520
4608
  /**
4521
- * Empties the hint buffer. Runs once per frame, reconcile or not.
4609
+ * Stores the world mode. `"fast"` finishes every motion and flushes every despawn queue at once.
4610
+ *
4611
+ * @param mode - The new stored mode.
4612
+ * @example
4613
+ * ```ts
4614
+ * // A headless test wants the final picture without stepping frames.
4615
+ * app.world.ecs.setMode("fast");
4616
+ * app.world.ecs.mode(); // "fast"
4617
+ * ```
4522
4618
  */
4523
- dropHints(): void;
4619
+ setMode(mode: WorldMode): void;
4524
4620
  /**
4525
- * Buffers one released hint.
4621
+ * The component type behind a name. A type registers itself on first use, so a name nobody
4622
+ * wrote, read or queried yet is unknown.
4526
4623
  *
4527
- * @param hint - The hint `flow.fx` released after a commit.
4624
+ * @param name - The storage name of the component.
4625
+ * @returns The type, or `undefined` when the world never met it.
4626
+ * @example
4627
+ * ```ts
4628
+ * // `text` resolves the component a label binds its number to.
4629
+ * const world = ctx.require(worldPlugin);
4630
+ * const type = world.ecs.typeOf("Coins"); // the Coins component, registered when it was written
4631
+ * const pose = type === undefined ? undefined : world.ecs.get(counter, type);
4632
+ *
4633
+ * pose?.amount; // 120
4634
+ * ```
4528
4635
  */
4529
- pushHint(hint: Hint): void;
4636
+ typeOf(name: string): AnyComponent | undefined;
4530
4637
  /**
4531
- * Sweeps the views whose motions ended: the despawn queue, the convergence check and the
4532
- * pending drops. The tracks themselves are advanced by the driver.
4638
+ * The owner of an entity, which is also the liveness check: a stale id has none, also after its
4639
+ * index came back with a new generation. `projection` asks before it hands out a view handle for
4640
+ * an entity another plugin owns; `renderer` asks before it forgets the filters of an entity whose
4641
+ * visual left.
4642
+ *
4643
+ * @param entity - The entity to ask about.
4644
+ * @returns The owner, or `undefined` when the id is stale.
4645
+ * @example
4646
+ * ```ts
4647
+ * // renderer forgets the filter slots of an entity only after it despawned
4648
+ * const ecs = ctx.require(worldPlugin).ecs;
4649
+ * ecs.ownerOf(button); // { kind: "projection", name: "hud" }
4650
+ * ecs.despawn(button);
4651
+ * ecs.ownerOf(button); // undefined
4652
+ * ```
4533
4653
  */
4534
- sweep(): void;
4654
+ ownerOf(entity: Entity): Owner | undefined;
4535
4655
  /**
4536
- * Finishes every motion and flushes every despawn queue. Called when the mode turns `fast`.
4656
+ * The whole world as plain JSON, sorted by entity index. A component value that is not JSON is
4657
+ * left out and its name is listed in `skipped`.
4658
+ *
4659
+ * @returns The world as JSON: the mode, the entities and the resources.
4660
+ * @example
4661
+ * ```ts
4662
+ * // A test asserts what the board holds after the first reconcile.
4663
+ * app.world.ecs.snapshot();
4664
+ * // { mode: "live", entities: [{ id: 1048576, index: 0, generation: 1,
4665
+ * // owner: { kind: "projection", name: "board.items" },
4666
+ * // components: { Layer: { name: "items" } }, skipped: [] }], resources: {} }
4667
+ * ```
4537
4668
  */
4538
- flushAll(): void;
4669
+ snapshot(): WorldSnapshot;
4539
4670
  /**
4540
- * Drops the views of a projection whose owner despawned its entities.
4671
+ * What changed between the world at the end of frame `from` and the world at the end of frame
4672
+ * `to`: one entry per entity, sorted by id, with the first and the last value of every
4673
+ * component that really changed. A value that went and came back inside the window is left
4674
+ * out, and so is an entity spawned and despawned inside it. Dev builds only: the world keeps
4675
+ * the change records of the last 120 frames.
4541
4676
  *
4542
- * @param owner - The owner that left.
4677
+ * @param from - The frame the window starts after.
4678
+ * @param to - The last frame of the window.
4679
+ * @returns The entities that changed, spawned or despawned in the window.
4680
+ * @throws {Error} In a production build, for a frame outside the last 120, and when `from` is
4681
+ * after `to`.
4682
+ * @example
4683
+ * ```ts
4684
+ * // The editor asks what the last second of play changed: item i7 merged up to level 3.
4685
+ * app.world.ecs.diff(120, 180);
4686
+ * // { from: 120, to: 180, entities: [{ id: 1048577,
4687
+ * // owner: { kind: "projection", name: "board.items" },
4688
+ * // key: { projection: "board.items", key: "i7" }, change: "changed",
4689
+ * // components: { Level: { from: { level: 2 }, to: { level: 3 } } } }] }
4690
+ * ```
4543
4691
  */
4544
- ownerLeft(owner: Owner): void;
4692
+ diff(from: number, to: number): FrameDiff;
4545
4693
  /**
4546
- * Drops every view, track, mute and spec.
4694
+ * Every component and tag type the world met, sorted by name: its kind, whether its defaults
4695
+ * are JSON, the JSON kind of each field, the defaults and the fields a plugin owns. A type
4696
+ * registers on first use, so the list is what the world has met so far, not what the game
4697
+ * declared. Every call builds a new list.
4698
+ *
4699
+ * @returns One entry per type.
4700
+ * @example
4701
+ * ```ts
4702
+ * // The editor's component palette shows the fields of Transform.
4703
+ * app.world.ecs.schema().find(entry => entry.name === "Transform");
4704
+ * // { name: "Transform", kind: "component", json: true,
4705
+ * // fields: { x: "number", y: "number", rotation: "number", scale: "number", pivot: "object" },
4706
+ * // defaults: { x: 0, y: 0, rotation: 0, scale: 1, pivot: { x: 0, y: 0 } }, owned: [] }
4707
+ * ```
4708
+ */
4709
+ schema(): readonly ComponentSchema[];
4710
+ };
4711
+ /**
4712
+ * ecs methods the plugin root calls: frame phases, the owner listener, the clears. Not public.
4713
+ */
4714
+ type EcsInternal = {
4715
+ /**
4716
+ * Registers a listener for `despawnOwnedBy`, so `projection` can drop the views of an owner
4717
+ * that left.
4718
+ *
4719
+ * @param fn - Called with the owner whose entities were despawned.
4720
+ * @returns The remover.
4721
+ */
4722
+ onOwnerLeft(fn: (owner: Owner) => void): () => void;
4723
+ /**
4724
+ * Runs the systems of one phase, then flushes the command buffer.
4725
+ *
4726
+ * @param phase - The phase to run.
4727
+ * @param time - The frame's `Time`.
4728
+ */
4729
+ runPhase(phase: WorldPhase, time: Readonly<Time>): void;
4730
+ /**
4731
+ * Applies the queued structural commands in call order.
4732
+ */
4733
+ flush(): void;
4734
+ /**
4735
+ * Clears every change set. Called in `time` phase `signals`.
4736
+ */
4737
+ clearChanges(): void;
4738
+ /**
4739
+ * Drops every entity, component, resource, system and hook.
4547
4740
  */
4548
4741
  clear(): void;
4549
4742
  };
4550
- declare namespace types_d_exports$1 {
4551
- export { AnyComponentType, AnyComponentValue, AnyProjectionSpec, AnySystem, Api$1 as Api, Cause, ChangeHook, ComponentHandle, ComponentType, ComponentValue, Config$1 as Config, Deps$1 as Deps, DescriptionNode, Ease, EcsApi, EcsModule, Entity, EntitySnapshot, Events$1 as Events, KernelSlice$1 as KernelSlice, LayerSort, LayerSpec, ModelCommitted, Motion, MotionHandle, Mut, Narrowed, NumericFields, Owner, ProjectionApi, ProjectionKey, ProjectionModule, ProjectionMotion, ProjectionSpec, QueryTerm, QueryTuple, ResourceType, RestOptions, State$1 as State, SystemContext, SystemDefinition, TagType, TrackOptions, TrackSegment, TweenDriver, TweenOptions, ViewHandle, ViewOutput, WorldCtx, WorldMode, WorldPhase, WorldSnapshot };
4743
+ declare namespace types_d_exports$2 {
4744
+ export { AnyComponentType, AnyComponentValue, AnyProjectionSpec, AnySystem, Api$2 as Api, Cause, ChangeHook, ComponentHandle, ComponentSchema, ComponentType, ComponentValue, Config$2 as Config, Deps$2 as Deps, DescriptionNode, Ease, EcsApi, EcsModule, Entity, EntityDiff, EntitySnapshot, Events$2 as Events, Explained, FrameDiff, KernelSlice$2 as KernelSlice, LayerSort, LayerSpec, ModelCommitted, Motion, MotionHandle, Mut, Narrowed, NumericFields, Owner, ProjectionApi, ProjectionKey, ProjectionModule, ProjectionMotion, ProjectionSpec, QueryTerm, QueryTuple, ResourceType, RestOptions, State$2 as State, SystemContext, SystemDefinition, TagType, TrackOptions, TrackSegment, TweenDriver, TweenOptions, ViewHandle, ViewOutput, WorldCtx, WorldMode, WorldPhase, WorldSnapshot };
4552
4745
  }
4553
4746
  /**
4554
4747
  * world plugin events.
@@ -4562,7 +4755,7 @@ declare namespace types_d_exports$1 {
4562
4755
  * }); // one merge logs { mode: "play", entered: 0, changed: 1, exited: 1, ... }
4563
4756
  * ```
4564
4757
  */
4565
- type Events$1 = {
4758
+ type Events$2 = {
4566
4759
  /** Dev only (config `reconciledEvent`). Counts of one reconcile. */"world:reconciled": {
4567
4760
  mode: "play" | "direct";
4568
4761
  projections: number;
@@ -4583,14 +4776,14 @@ type Events$1 = {
4583
4776
  * createApp({ pluginConfigs: { world: { settleMs: 250, reconciledEvent: true } } });
4584
4777
  * ```
4585
4778
  */
4586
- type Config$1 = {
4779
+ type Config$2 = {
4587
4780
  /** Duration of the built-in settle motion, in game milliseconds. */settleMs: number; /** Emit `world:reconciled` after every reconcile. Off in production; tests and the editor turn it on. */
4588
4781
  reconciledEvent: boolean;
4589
4782
  };
4590
4783
  /**
4591
4784
  * world plugin state: one branch per module.
4592
4785
  */
4593
- type State$1 = {
4786
+ type State$2 = {
4594
4787
  ecs: EcsState;
4595
4788
  projection: ProjectionState;
4596
4789
  };
@@ -4603,17 +4796,17 @@ type State$1 = {
4603
4796
  * app.world.projection.entityOf("board.items", "i7"); // 1048576
4604
4797
  * ```
4605
4798
  */
4606
- type Api$1 = {
4799
+ type Api$2 = {
4607
4800
  ecs: EcsApi;
4608
4801
  projection: ProjectionApi;
4609
4802
  };
4610
4803
  /**
4611
4804
  * Resolved dependency APIs.
4612
4805
  */
4613
- type Deps$1 = {
4614
- time: Api$3;
4615
- model: Api$4;
4616
- flow: Api$2;
4806
+ type Deps$2 = {
4807
+ time: Api$4;
4808
+ model: Api$5;
4809
+ flow: Api$3;
4617
4810
  };
4618
4811
  /**
4619
4812
  * What the kernel context offers before the deps are attached.
@@ -4622,7 +4815,7 @@ type Deps$1 = {
4622
4815
  * row 8), so the own event reaches the context the kernel hands the factories and `emit` is the
4623
4816
  * kernel's own, world-typed one. No member of the context is cast.
4624
4817
  */
4625
- type KernelSlice$1 = PluginCtx<Config$1, State$1, Events$1> & {
4818
+ type KernelSlice$2 = PluginCtx<Config$2, State$2, Events$2> & {
4626
4819
  readonly global: object;
4627
4820
  readonly log: Log.LogApi;
4628
4821
  readonly require: Require;
@@ -4630,13 +4823,37 @@ type KernelSlice$1 = PluginCtx<Config$1, State$1, Events$1> & {
4630
4823
  /**
4631
4824
  * Domain context shared by the two modules.
4632
4825
  */
4633
- type WorldCtx = KernelSlice$1 & {
4634
- readonly deps: Deps$1;
4826
+ type WorldCtx = KernelSlice$2 & {
4827
+ readonly deps: Deps$2;
4635
4828
  };
4636
4829
  /**
4637
4830
  * Payload of the one event world listens to.
4638
4831
  */
4639
- type ModelCommitted = Events$3["model:committed"];
4832
+ type ModelCommitted = Events$4["model:committed"];
4833
+ /**
4834
+ * One entity in full, as the `/inspect` source `game.explain` reads it: its row of
4835
+ * `ecs.snapshot()`, its projection key and the components a motion still drives.
4836
+ *
4837
+ * @example
4838
+ * ```ts
4839
+ * const sliding: Explained = {
4840
+ * id: 1_048_577,
4841
+ * owner: { kind: "projection", name: "board.items" },
4842
+ * key: { projection: "board.items", key: "i7" },
4843
+ * components: { Layer: { name: "items" } },
4844
+ * skipped: ["Display"],
4845
+ * motions: ["Transform"]
4846
+ * };
4847
+ * ```
4848
+ */
4849
+ type Explained = {
4850
+ id: Entity;
4851
+ owner: Owner; /** `projection.keyOf`: the view address, also for an element registered under a key. */
4852
+ key: ProjectionKey | undefined; /** The JSON components, as `EntitySnapshot.components`. */
4853
+ components: Record<string, Json>; /** The names of the components that are not JSON, as `EntitySnapshot.skipped`. */
4854
+ skipped: string[]; /** `projection.motionsOf`: the components a running track drives, in start order. */
4855
+ motions: readonly string[];
4856
+ };
4640
4857
  /**
4641
4858
  * Both halves of the `ecs` module: what a game calls and what `projection` gets injected.
4642
4859
  */
@@ -4833,11 +5050,21 @@ type HostInternal = {
4833
5050
  */
4834
5051
  render(): void;
4835
5052
  /**
4836
- * Draws the stage into a PNG over the whole canvas. A failed read is logged.
5053
+ * Draws the stage into a PNG over the whole canvas. Every container in `hidden` is set
5054
+ * `visible = false` around the synchronous render inside Pixi's extract and set back right after
5055
+ * that call returns its promise, so no drawn frame sees it hidden. A failed read is logged.
4837
5056
  *
5057
+ * @param hidden - The layer containers to leave out of the picture; `[]` for the whole stage.
4838
5058
  * @returns A PNG data URL, or `undefined` while nothing draws or when Pixi could not read it.
4839
5059
  */
4840
- extract(): Promise<string | undefined>;
5060
+ extract(hidden: readonly PixiContainer[]): Promise<string | undefined>;
5061
+ /**
5062
+ * The resolution the application draws at: device pixels per CSS pixel, capped by
5063
+ * `maxResolution`. A picture of the canvas is its CSS size times this.
5064
+ *
5065
+ * @returns The resolution; 1 while inert.
5066
+ */
5067
+ resolution(): number;
4841
5068
  /**
4842
5069
  * The texture sources the GPU holds and their estimated memory, read at call time.
4843
5070
  *
@@ -4862,7 +5089,7 @@ type HostInternal = {
4862
5089
  * const orientation: Orientation = "portrait";
4863
5090
  * ```
4864
5091
  */
4865
- type Orientation = Config$7["orientation"];
5092
+ type Orientation = Config$8["orientation"];
4866
5093
  /**
4867
5094
  * A point in reference units.
4868
5095
  *
@@ -4980,10 +5207,9 @@ type ViewportApi = {
4980
5207
  * units, since there is no canvas to place them on.
4981
5208
  * @example
4982
5209
  * ```ts
4983
- * // The `game.rect` source of the editor places Home's Play plank on the page. A 390x844
4984
- * // phone, the canvas at the top left: the scale is 390 / 1080.
4985
- * const renderer = ctx.require(rendererPlugin);
4986
- * renderer.viewport.toScreen({ x: 540, y: 960 }); // { x: 195, y: 346.666… }
5210
+ * // The `game.locate` source places Home's Play plank on the page for an e2e script. A
5211
+ * // 390x844 phone, the canvas at the top left: the scale is 390 / 1080.
5212
+ * app.renderer.viewport.toScreen({ x: 540, y: 960 }); // { x: 195, y: 346.666… }
4987
5213
  * ```
4988
5214
  */
4989
5215
  toScreen(point: Point): Point;
@@ -5079,7 +5305,14 @@ type DisplayEntry = {
5079
5305
  removers: Array<() => void>;
5080
5306
  };
5081
5307
  /**
5082
- * The rectangle a hit test checks, in the local space of the view: anchor already applied.
5308
+ * A rectangle: the one a hit test checks, in the local space of the view with the anchor already
5309
+ * applied, or the bounds of a view in reference units.
5310
+ *
5311
+ * @example
5312
+ * ```ts
5313
+ * // A 64 x 64 sprite with the default anchor, in its own units.
5314
+ * const box: HitBox = { x: -32, y: -32, width: 64, height: 64 };
5315
+ * ```
5083
5316
  */
5084
5317
  type HitBox = {
5085
5318
  x: number;
@@ -5149,7 +5382,7 @@ type TextureProvider = (key: string) => PixiTexture | undefined;
5149
5382
  * const borders: NineBorders = [12, 12, 12, 12];
5150
5383
  * ```
5151
5384
  */
5152
- type NineBorders = readonly [number, number, number, number];
5385
+ type NineBorders$1 = readonly [number, number, number, number];
5153
5386
  /**
5154
5387
  * Options of `textures.create`.
5155
5388
  *
@@ -5158,8 +5391,8 @@ type NineBorders = readonly [number, number, number, number];
5158
5391
  * const options: CreateTextureOptions = { nine: [12, 12, 12, 12] };
5159
5392
  * ```
5160
5393
  */
5161
- type CreateTextureOptions = {
5162
- nine?: NineBorders;
5394
+ type CreateTextureOptions$1 = {
5395
+ nine?: NineBorders$1;
5163
5396
  };
5164
5397
  /**
5165
5398
  * Where `textures.slice` cuts a texture out of an atlas page, in page pixels. The manifest `atlas`
@@ -5221,7 +5454,7 @@ type TexturesApi = {
5221
5454
  * const texture = renderer.sync.textures.create(bitmap, { nine: [12, 12, 12, 12] });
5222
5455
  * ```
5223
5456
  */
5224
- create(image: ImageBitmap | HTMLImageElement, options?: CreateTextureOptions): PixiTexture;
5457
+ create(image: ImageBitmap | HTMLImageElement, options?: CreateTextureOptions$1): PixiTexture;
5225
5458
  /**
5226
5459
  * Cuts a texture out of an atlas page, so `assets` never imports Pixi. The slice shares the
5227
5460
  * page's source: no pixel is copied, and the page keeps the GPU memory. A frame is measured
@@ -5244,7 +5477,7 @@ type TexturesApi = {
5244
5477
  * renderer.sync.textures.destroy(button); // the wrapper goes, the page's source stays
5245
5478
  * ```
5246
5479
  */
5247
- slice(page: PixiTexture, frame: SliceFrame, options?: CreateTextureOptions): PixiTexture;
5480
+ slice(page: PixiTexture, frame: SliceFrame, options?: CreateTextureOptions$1): PixiTexture;
5248
5481
  /**
5249
5482
  * Destroys a texture and its source. A slice loses only its wrapper: the source belongs to its
5250
5483
  * page, which frees it. The crops of `"cover"` sprites cut from the texture go with it.
@@ -5483,6 +5716,54 @@ type SyncApi = {
5483
5716
  * ```
5484
5717
  */
5485
5718
  hitTest(x: number, y: number, accept: (entity: Entity) => boolean): Entity | undefined;
5719
+ /**
5720
+ * Every entity whose hit box holds the point, topmost first: the walk and the skip rules of
5721
+ * `hitTest` (a hidden or fully transparent view, a point outside a clipping ancestor), with no
5722
+ * filter, so the caller filters. `hitTest` stays the fast path of a pointer event: it stops at
5723
+ * the first accepted hit.
5724
+ *
5725
+ * @param x - Reference x.
5726
+ * @param y - Reference y.
5727
+ * @returns The entities, topmost first, as a fresh array. Inert, and where nothing is drawn: `[]`.
5728
+ * @example
5729
+ * ```ts
5730
+ * // The `game.at` door lists what lies under a point: a coin drawn over its board cell.
5731
+ * app.renderer.sync.hitAll(540, 300); // [1048581, 1048576]
5732
+ * app.renderer.sync.hitAll(20, 20); // []: nothing is drawn there
5733
+ * ```
5734
+ */
5735
+ hitAll(x: number, y: number): readonly Entity[];
5736
+ /**
5737
+ * Where a view is drawn, in reference units: the axis-aligned bounds of the four corners of its
5738
+ * hit box brought through its `Transform` and its `Parent` chain, rotation and scale included.
5739
+ * Only a view the player can see has bounds, so a located element is one a click reaches.
5740
+ *
5741
+ * @param entity - The entity to place.
5742
+ * @returns The bounds, a fresh object. `undefined` for an entity without a view, a view that
5743
+ * hangs in no tree, a view hidden or with an effective alpha of 0.01 or less on the way to the
5744
+ * root, and bounds without area. Inert: always `undefined`.
5745
+ * @example
5746
+ * ```ts
5747
+ * // The `game.locate` door finds a 64 x 64 coin drawn at (540, 300) with the default anchor.
5748
+ * app.renderer.sync.boundsOf(coin); // { x: 508, y: 268, width: 64, height: 64 }
5749
+ * ```
5750
+ */
5751
+ boundsOf(entity: Entity): HitBox | undefined;
5752
+ /**
5753
+ * The very box `hitTest` tests for a view, in the entity's own units: the anchor applied, before
5754
+ * the `Transform` and the `Parent` chain, so no rotation. `boundsOf` is this box brought to
5755
+ * reference units.
5756
+ *
5757
+ * @param entity - The entity to ask about.
5758
+ * @returns The box, a fresh object, or `undefined` for an entity without a view.
5759
+ * @example
5760
+ * ```ts
5761
+ * // `input` fits the hit circle of a trace inside a cell: a 64 x 64 Sprite, default anchor.
5762
+ * const renderer = ctx.require(rendererPlugin);
5763
+ * renderer.sync.hitBoxOf(cell); // { x: -32, y: -32, width: 64, height: 64 }
5764
+ * ```
5765
+ */
5766
+ hitBoxOf(entity: Entity): HitBox | undefined;
5486
5767
  /**
5487
5768
  * The texture registry `assets` drives.
5488
5769
  *
@@ -5603,6 +5884,21 @@ type SyncInternal = {
5603
5884
  * @returns The views of entities and the objects waiting in the pools.
5604
5885
  */
5605
5886
  counts(): SyncCounts;
5887
+ /**
5888
+ * The container of one layer of the scene, so `monitor` can leave a layer out of a picture.
5889
+ *
5890
+ * @param name - Layer name.
5891
+ * @returns The container, or `undefined` for a name the scene does not declare and while inert.
5892
+ */
5893
+ layerContainer(name: string): PixiContainer | undefined;
5894
+ /**
5895
+ * The layer an entity is drawn in: the `Layer` of the root of its `Parent` chain, since a child
5896
+ * draws with its parent. An entity without a parent names its own.
5897
+ *
5898
+ * @param entity - The entity.
5899
+ * @returns The layer name, or `undefined` when the root of the chain has no `Layer`.
5900
+ */
5901
+ layerOf(entity: Entity): string | undefined;
5606
5902
  };
5607
5903
  /**
5608
5904
  * The display objects `sync` holds: views of entities and pooled objects.
@@ -5703,7 +5999,7 @@ type CountingClasses = {
5703
5999
  };
5704
6000
  /**
5705
6001
  * monitor module state: the open measuring window, the last closed one, and the captures that
5706
- * wait for a frame.
6002
+ * wait for a drawn frame.
5707
6003
  */
5708
6004
  type MonitorState = {
5709
6005
  /** Clock time at the start of the frame being drawn; `undefined` between frames. */frameStart: number | undefined; /** Clock time at the start of the last frame, the far end of the next interval. */
@@ -5713,8 +6009,8 @@ type MonitorState = {
5713
6009
  workMs: number; /** Frames finished in the open window. */
5714
6010
  frames: number; /** Frames per second of the last closed window. */
5715
6011
  fps: number; /** Mean frame work of the last closed window, in milliseconds. */
5716
- frameMs: number; /** Callers of `capture()` waiting for the next drawn frame. */
5717
- captures: Array<(url: string | undefined) => void>; /** The draws the dev counter saw; stays at 0 in a production build. */
6012
+ frameMs: number; /** Captures waiting for a drawn frame, served in `end()`. */
6013
+ captures: PictureRequest[]; /** The draws the dev counter saw; stays at 0 in a production build. */
5718
6014
  draws: DrawCounter;
5719
6015
  };
5720
6016
  /**
@@ -5750,17 +6046,43 @@ type MonitorApi = {
5750
6046
  * picture shows the state the game just reached; at once while the clock is paused, since no
5751
6047
  * frame comes then. Dev builds only: `__MOKU_GAME_DEV__` must be `true`.
5752
6048
  *
5753
- * @returns A `data:image/png;base64,…` URL. `undefined` in a production build, while inert, lost
5754
- * or unsupported, and when Pixi could not read the frame (logged with `ctx.log.error`).
6049
+ * The options draw on the picture after the extract, on an `OffscreenCanvas`; the game never
6050
+ * shows any of it. `layers` hides the other layer containers for the extract alone. `legend`
6051
+ * numbers every keyed view the player can see with a badge at the top-left corner of its rect,
6052
+ * sorted by `rect.y` then `rect.x`, and lists them. `sheet` takes `frames` pictures `everyMs` of
6053
+ * game time apart (while the clock is paused, by one `time.step(everyMs)` each) and lays them out
6054
+ * in `ceil(sqrt(frames))` columns. `against` answers the pixel diff: red where a channel differs
6055
+ * by more than 24, the current picture faded to grey elsewhere. `legend` combines with `against`;
6056
+ * `sheet` combines with `layers` only.
6057
+ *
6058
+ * @param options - What to draw on the picture; a plain picture when left out.
6059
+ * @returns `{ png }`, with `legend` when asked for. `undefined` in a production build, while
6060
+ * inert, lost or unsupported, and when Pixi could not read the frame (logged with
6061
+ * `ctx.log.error`).
6062
+ * @throws {Error} When a layer is not in the scene or the list is empty, when a sheet is out of
6063
+ * 2 to 12 frames every 1 to 5000 ms or comes with `legend` or `against`, when there is no
6064
+ * `OffscreenCanvas` for an option, when the two pictures of `against` differ in size, and when
6065
+ * game time stood still for 600 drawn frames of a sheet.
5755
6066
  * @example
5756
6067
  * ```ts
5757
6068
  * // The editor's capture button, on the dev page that set globalThis.__MOKU_GAME_DEV__ = true.
5758
- * const url = await app.renderer.capture(); // "data:image/png;base64,iVBORw0KGgo…"
6069
+ * await app.renderer.capture(); // { png: "data:image/png;base64,iVBORw0KGgo…" }
5759
6070
  * // The same call in a production build.
5760
6071
  * await app.renderer.capture(); // undefined
5761
6072
  * ```
6073
+ * @example
6074
+ * ```ts
6075
+ * // An agent finds the claim button of the HUD on a 1080 x 1920 canvas drawn at resolution 2.
6076
+ * await app.renderer.capture({ legend: true });
6077
+ * // { png: "data:image/png;base64,…", legend: [{ n: 1, projection: "hud", key: "claim", rect: { x: 1140, y: 2310, w: 567, h: 164 } }] }
6078
+ * ```
6079
+ * @example
6080
+ * ```ts
6081
+ * // The coin flight of a merge, six frames 100 ms apart: 3 columns and 2 rows on one picture.
6082
+ * await app.renderer.capture({ sheet: { frames: 6, everyMs: 100 } }); // { png: "data:image/png;base64,…" }
6083
+ * ```
5762
6084
  */
5763
- capture(): Promise<string | undefined>;
6085
+ capture(options?: CaptureOptions): Promise<Captured | undefined>;
5764
6086
  };
5765
6087
  /**
5766
6088
  * monitor methods the frame drives. Not public.
@@ -5778,222 +6100,1049 @@ type MonitorInternal = {
5778
6100
  end(): void;
5779
6101
  };
5780
6102
  /**
5781
- * What `monitor` gets injected: the modules it reads its counters from.
6103
+ * What `monitor` gets injected: the modules it reads its counters from and places a legend with.
6104
+ */
6105
+ type MonitorDeps = {
6106
+ host: HostApi & HostInternal;
6107
+ viewport: ViewportApi & ViewportInternal;
6108
+ sync: SyncApi & SyncInternal;
6109
+ };
6110
+ /**
6111
+ * A rectangle in picture pixels, from the top-left corner of the PNG: CSS pixels of the canvas
6112
+ * times the resolution, so an agent draws on the picture with it.
6113
+ *
6114
+ * @example
6115
+ * ```ts
6116
+ * // The claim button of the HUD on a 1080 x 1920 canvas drawn at resolution 2.
6117
+ * const rect: PictureRect = { x: 1140, y: 2310, w: 567, h: 164 };
6118
+ * ```
6119
+ */
6120
+ type PictureRect = {
6121
+ x: number;
6122
+ y: number;
6123
+ w: number;
6124
+ h: number;
6125
+ };
6126
+ /**
6127
+ * One numbered view of a legend: the number on its badge, the projection and key that address
6128
+ * it, and its rect on the picture.
6129
+ *
6130
+ * @example
6131
+ * ```ts
6132
+ * const entry: LegendEntry = {
6133
+ * n: 1, projection: "hud", key: "claim", rect: { x: 1140, y: 2310, w: 567, h: 164 }
6134
+ * };
6135
+ * ```
6136
+ */
6137
+ type LegendEntry = {
6138
+ n: number;
6139
+ projection: string;
6140
+ key: string;
6141
+ rect: PictureRect;
6142
+ };
6143
+ /**
6144
+ * What `capture()` draws on the picture. Every field is optional; none gives a plain picture.
6145
+ *
6146
+ * @example
6147
+ * ```ts
6148
+ * // The numbered board without the HUD over it.
6149
+ * const options: CaptureOptions = { legend: true, layers: ["board", "items"] };
6150
+ * // Six frames of a motion, 100 ms of game time apart.
6151
+ * const sheet: CaptureOptions = { sheet: { frames: 6, everyMs: 100 } };
6152
+ * ```
6153
+ */
6154
+ type CaptureOptions = {
6155
+ /** Number every keyed view on the picture and list them in `legend` of the answer. */legend?: boolean; /** Draw only these layers of the scene; a `Parent` child follows its parent's layer. */
6156
+ layers?: readonly string[]; /** A contact sheet: 2 to 12 frames, every 1 to 5000 ms of game time. */
6157
+ sheet?: {
6158
+ frames: number;
6159
+ everyMs: number;
6160
+ }; /** A PNG data URL of an earlier capture: the answer is the pixel diff against it. */
6161
+ against?: string;
6162
+ };
6163
+ /**
6164
+ * What `capture()` answers: the picture, and the legend when it was asked for.
6165
+ *
6166
+ * @example
6167
+ * ```ts
6168
+ * const plain: Captured = { png: "data:image/png;base64,iVBORw0KGgo=" };
6169
+ * const numbered: Captured = {
6170
+ * png: "data:image/png;base64,iVBORw0KGgo=",
6171
+ * legend: [{ n: 1, projection: "hud", key: "claim", rect: { x: 1140, y: 2310, w: 567, h: 164 } }]
6172
+ * };
6173
+ * ```
6174
+ */
6175
+ type Captured = {
6176
+ png: string;
6177
+ legend?: readonly LegendEntry[];
6178
+ };
6179
+ /**
6180
+ * The picture of one drawn frame, the legend measured on that same frame when it was asked for,
6181
+ * and the game time of the frame. Not public.
6182
+ */
6183
+ type Shot = {
6184
+ png: string;
6185
+ legend: LegendEntry[] | undefined;
6186
+ elapsed: number;
6187
+ };
6188
+ /**
6189
+ * A capture waiting for a drawn frame. Not public.
6190
+ */
6191
+ type PictureRequest = {
6192
+ /** The layers to draw; every layer when `undefined`. */layers: readonly string[] | undefined; /** True to measure the legend on the frame of the picture. */
6193
+ legend: boolean; /** True when the frame just drawn is the one to take. A plain capture takes the next one. */
6194
+ due: () => boolean; /** Takes the shot instead when the game pauses before that frame came: at once, or stepped. */
6195
+ whilePaused: () => Promise<Shot | undefined>; /** Drawn frames that went by while `due` said no. */
6196
+ waited: number; /** Hands the shot over; nothing when the renderer stopped first. */
6197
+ resolve: (shot?: Shot) => void; /** Hands over why the shot cannot come. */
6198
+ reject: (error: Error) => void;
6199
+ };
6200
+ declare namespace types_d_exports$1 {
6201
+ export { Api$1 as Api, AspectRange, BitmapFontData, CaptureOptions, Captured, Config$1 as Config, CountingClasses, CoverFrame, CreateTextureOptions$1 as CreateTextureOptions, DebugApi, DebugSwitches, Deps$1 as Deps, DisplayAdapter, DisplayEntry, DisplaysApi, DrawCounter, Events$1 as Events, FilterSlot, FiltersApi, FontsApi, HitBox, HostApi, HostInternal, HostModule, HostState, KernelSlice$1 as KernelSlice, LayerEntry, LegendEntry, Modules, MonitorApi, MonitorDeps, MonitorInternal, MonitorModule, MonitorState, NineBorders$1 as NineBorders, Orientation, PictureRect, PixiApplication, PixiBitmapFont, PixiContainer, PixiFilter, PixiGraphics, PixiModule, PixiNineSliceSprite, PixiSprite, PixiTexture, Point, Rect, RenderStats, RendererCtx, RendererKind, SafeArea, SliceFrame, State$1 as State, SyncApi, SyncCounts, SyncInternal, SyncModule, SyncState, TeardownScope, TextureProvider, TextureUsage, TexturesApi, Under, View, ViewKind, ViewportApi, ViewportInternal, ViewportModule, ViewportSize, ViewportState };
6202
+ }
6203
+ /**
6204
+ * The part of Pixi the engine uses: the classes `sync` builds views from (a `Rectangle` frames the
6205
+ * crop of a `"cover"` sprite), the bitmap-font pieces `fonts.install` needs, the `BitmapText` a
6206
+ * plugin above builds through `host.pixi()`, the filter and particle classes `effects` builds
6207
+ * through it (a custom filter carries a `GpuProgram` and a `GlProgram` over `defaultFilterVert`),
6208
+ * and the extension registry with the three WebGPU draw classes the dev draw-call
6209
+ * counter swaps and the bitmap text pipe `host` swaps for one whose SDF shader applies the alpha
6210
+ * once (with the WGSL and GLSL shader pieces it compiles that shader from). The module object
6211
+ * never arrives through a static import: `config.loadPixi()` returns it, so a game without a screen carries no
6212
+ * Pixi in its bundle.
6213
+ *
6214
+ * @example
6215
+ * ```ts
6216
+ * const pixi: PixiModule = await import("pixi.js");
6217
+ * pixi.Texture.WHITE.width; // 1
6218
+ * pixi.GpuBatchAdaptor.extension.name; // "batch"
6219
+ * pixi.BitmapTextPipe.extension.name; // "bitmapText"
6220
+ * pixi.defaultFilterVert.startsWith("in vec2 aPosition;"); // true
6221
+ * ```
6222
+ */
6223
+ type PixiModule = Pick<typeof import("pixi.js"), "AlphaFilter" | "Application" | "BitmapFont" | "BitmapText" | "BitmapTextPipe" | "BlurFilter" | "Cache" | "ColorMatrixFilter" | "Container" | "DisplacementFilter" | "Filter" | "GlProgram" | "GpuBatchAdaptor" | "GpuEncoderSystem" | "GpuGraphicsAdaptor" | "GpuProgram" | "Graphics" | "Matrix" | "NineSliceSprite" | "NoiseFilter" | "Particle" | "ParticleContainer" | "Rectangle" | "Shader" | "Sprite" | "Texture" | "UniformGroup" | "bitmapFontTextParser" | "bitmapFontXMLStringParser" | "colorBit" | "colorBitGl" | "compileHighShaderGlProgram" | "compileHighShaderGpuProgram" | "defaultFilterVert" | "extensions" | "generateTextureBatchBit" | "generateTextureBatchBitGl" | "getBatchSamplersUniformGroup" | "localUniformMSDFBit" | "localUniformMSDFBitGl" | "mSDFBit" | "mSDFBitGl" | "roundPixelsBit" | "roundPixelsBitGl">;
6224
+ /**
6225
+ * One Pixi application: renderer, canvas and stage.
6226
+ */
6227
+ type PixiApplication = InstanceType<PixiModule["Application"]>;
6228
+ /**
6229
+ * A Pixi display object. `Sprite` and `NineSliceSprite` are containers too, so every view is one.
6230
+ */
6231
+ type PixiContainer = InstanceType<PixiModule["Container"]>;
6232
+ /**
6233
+ * A Pixi sprite.
6234
+ */
6235
+ type PixiSprite = InstanceType<PixiModule["Sprite"]>;
6236
+ /**
6237
+ * A Pixi nine-slice sprite.
6238
+ */
6239
+ type PixiNineSliceSprite = InstanceType<PixiModule["NineSliceSprite"]>;
6240
+ /**
6241
+ * A Pixi graphics object: what a `Shape` component is drawn with, and what clips its children.
6242
+ */
6243
+ type PixiGraphics = InstanceType<PixiModule["Graphics"]>;
6244
+ /**
6245
+ * A Pixi texture. `assets` owns its lifetime; the renderer only makes and destroys it on request.
6246
+ */
6247
+ type PixiTexture = InstanceType<PixiModule["Texture"]>;
6248
+ /**
6249
+ * A Pixi filter: what `effects` builds and `sync.filters.set` hangs on a view. `effects` owns
6250
+ * the instance; the renderer only assigns it.
6251
+ */
6252
+ type PixiFilter = InstanceType<PixiModule["Filter"]>;
6253
+ /**
6254
+ * A bitmap font the renderer installed for a font asset key. One per key.
6255
+ */
6256
+ type PixiBitmapFont = InstanceType<PixiModule["BitmapFont"]>;
6257
+ /**
6258
+ * The parsed shape of a `.fnt` file: what `BitmapFont` is built from.
6259
+ */
6260
+ type BitmapFontData = ConstructorParameters<PixiModule["BitmapFont"]>[0]["data"];
6261
+ /**
6262
+ * Which backend the one application drew with, or `"none"` while it draws nothing.
6263
+ *
6264
+ * @example
6265
+ * ```ts
6266
+ * const kind: RendererKind = "webgpu";
6267
+ * ```
6268
+ */
6269
+ type RendererKind = "webgpu" | "webgl" | "none";
6270
+ /**
6271
+ * renderer plugin events.
6272
+ *
6273
+ * @example
6274
+ * ```ts
6275
+ * // A game reports a lost GPU to its analytics; the pause itself is handled by `lifecycle`.
6276
+ * createPlugin("gpuReport", {
6277
+ * depends: [rendererPlugin],
6278
+ * hooks: ctx => ({ "renderer:device-lost": ({ kind }) => ctx.log.warn("gpu-lost", { kind }) })
6279
+ * }); // a lost WebGPU device logs { kind: "webgpu" }
6280
+ * ```
6281
+ */
6282
+ type Events$1 = {
6283
+ /** The GPU device or the WebGL context was lost; the game is paused until the restore lands. */"renderer:device-lost": {
6284
+ kind: "webgpu" | "webgl";
6285
+ reason: string;
6286
+ };
6287
+ };
6288
+ /**
6289
+ * The two numbers that bound the shape of the drawn frame: allowed long side / short side.
6290
+ *
6291
+ * @example
6292
+ * ```ts
6293
+ * const aspect: AspectRange = { min: 4 / 3, max: 21 / 9 };
6294
+ * ```
6295
+ */
6296
+ type AspectRange = {
6297
+ min: number;
6298
+ max: number;
6299
+ };
6300
+ /**
6301
+ * renderer plugin config.
6302
+ *
6303
+ * @example
6304
+ * ```ts
6305
+ * createApp({
6306
+ * plugins: [...screen],
6307
+ * pluginConfigs: { renderer: { mount: "#game", background: 0x101018, maxResolution: 2 } }
6308
+ * });
6309
+ * // A debug build of the fixture outlines every nine-slice from the first frame.
6310
+ * createApp({ pluginConfigs: { renderer: { mount: "#game", debug: { nineSlice: true } } } });
6311
+ * ```
6312
+ */
6313
+ type Config$1 = {
6314
+ /** Where the canvas goes: a selector or an element. `undefined` keeps the plugin inert. */mount: string | HTMLElement | undefined; /** Clear colour. Also the colour of the bars around the frame. */
6315
+ background: number; /** Antialias the whole canvas. Off by default: pixel art and sprites do not need it. */
6316
+ antialias: boolean; /** Cap of `devicePixelRatio`. Memory grows with its square. */
6317
+ maxResolution: number; /** Passed to Pixi. Pixi falls back to WebGL by itself. */
6318
+ preference: "webgpu" | "webgl"; /** Allowed long side / short side of the frame. A window outside it gets bars. */
6319
+ aspect: AspectRange; /** Display objects kept in all pools together. */
6320
+ poolLimit: number; /** Text of the unsupported-device screen. */
6321
+ unsupportedMessage: string; /** Loader seam. Tests pass a fake module. */
6322
+ loadPixi: () => Promise<PixiModule>; /** Debug drawing at start. `app.renderer.sync.debug` switches it while the game runs. */
6323
+ debug: DebugSwitches;
6324
+ };
6325
+ /**
6326
+ * renderer plugin state: one branch per module.
6327
+ */
6328
+ type State$1 = {
6329
+ host: HostState;
6330
+ viewport: ViewportState;
6331
+ sync: SyncState;
6332
+ monitor: MonitorState;
6333
+ };
6334
+ /**
6335
+ * renderer plugin API, `app.renderer`: grouped by module, and the two members of `monitor`,
6336
+ * `stats()` and `capture()`, on the plugin itself.
6337
+ *
6338
+ * @example
6339
+ * ```ts
6340
+ * app.renderer.host.kind(); // "webgpu"
6341
+ * app.renderer.viewport.size().width; // 1080, reference units
6342
+ * app.renderer.sync.hitTest(540, 300, () => true); // 1048576
6343
+ * app.renderer.stats().views; // 180
6344
+ * app.renderer.stats().renderPasses; // 1: no filter on screen
6345
+ * ```
6346
+ */
6347
+ type Api$1 = {
6348
+ host: HostApi;
6349
+ viewport: ViewportApi;
6350
+ sync: SyncApi;
6351
+ } & MonitorApi;
6352
+ /**
6353
+ * One entity under a point of the page, what the `game.at` door answers: the entity, its owner,
6354
+ * the projection and key that address it when it has them, and the layer its own `Layer` names
6355
+ * (`undefined` for a `Parent` child, which draws in its parent's layer).
6356
+ *
6357
+ * @example
6358
+ * ```ts
6359
+ * // A coin of the board under the pointer.
6360
+ * const under: Under = {
6361
+ * entity: 1_048_581, owner: { kind: "projection", name: "board.items" },
6362
+ * key: { projection: "board.items", key: "c7" }, layer: "items"
6363
+ * };
6364
+ * ```
6365
+ */
6366
+ type Under = {
6367
+ entity: Entity;
6368
+ owner: Owner;
6369
+ key: ProjectionKey | undefined;
6370
+ layer: string | undefined;
6371
+ };
6372
+ /**
6373
+ * Resolved dependency APIs. `clock` is the time source of the frame counters.
6374
+ */
6375
+ type Deps$1 = {
6376
+ time: Api$4;
6377
+ lifecycle: Api$6;
6378
+ world: Api$2;
6379
+ clock: Api$7;
6380
+ };
6381
+ /**
6382
+ * What the kernel context offers before the deps are attached.
6383
+ *
6384
+ * `emit` is the kernel's own: `index.ts` writes `events` with an annotated `register`
6385
+ * (core spec `14-EVENT-REGISTRATION.md` row 8), so the plugin's own event reaches the pre-typed
6386
+ * `api` factory and no member of this context is cast.
6387
+ */
6388
+ type KernelSlice$1 = PluginCtx<Config$1, State$1, Events$1> & {
6389
+ readonly global: Readonly<Config$8>;
6390
+ readonly log: Log.LogApi;
6391
+ readonly require: Require;
6392
+ };
6393
+ /**
6394
+ * Domain context shared by the three modules.
6395
+ */
6396
+ type RendererCtx = KernelSlice$1 & {
6397
+ readonly deps: Deps$1;
6398
+ };
6399
+ /**
6400
+ * Both halves of the `host` module: what a game calls and what the two modules above get injected.
6401
+ */
6402
+ type HostModule = HostApi & HostInternal;
6403
+ /**
6404
+ * Both halves of the `viewport` module: what a game calls and what `sync` and the frame drive.
6405
+ */
6406
+ type ViewportModule = ViewportApi & ViewportInternal;
6407
+ /**
6408
+ * Both halves of the `sync` module: what `input` and `assets` call and what the frame drives.
6409
+ */
6410
+ type SyncModule = SyncApi & SyncInternal;
6411
+ /**
6412
+ * Both halves of the `monitor` module: what a tool calls and what the frame drives.
6413
+ */
6414
+ type MonitorModule = MonitorApi & MonitorInternal;
6415
+ /**
6416
+ * The four modules in injection order.
6417
+ */
6418
+ type Modules = {
6419
+ host: HostModule;
6420
+ viewport: ViewportModule;
6421
+ sync: SyncModule;
6422
+ monitor: MonitorModule;
6423
+ };
6424
+ /**
6425
+ * What `onStop` receives: the frozen config and the plugin state, nothing else.
6426
+ */
6427
+ type TeardownScope = {
6428
+ readonly config: Readonly<Config$1>;
6429
+ readonly state: State$1;
6430
+ };
6431
+ declare namespace types_d_exports {
6432
+ export { Api, AssetKind, AssetsCtx, AssetsIo, AtlasFrame, AtlasPage, AudioAsset, AudioMime, BundleMap, BundleRecord, BundleSpec, BundleUsage, Config, CreateTextureOptions, DecodedImage, DefineBundles, Deps, Events, FetchResponse, FlowRest, FontAsset, FontPage, Inflight, KernelSlice, LoadBundles, LoadReason, LoadResult, LoadedAssets, LoadedFont, Manifest, ManifestBundle, ManifestFile, NineBorders, NineSlice, PreloadQueue, State, Texture, Tier, Usage };
6433
+ }
6434
+ /**
6435
+ * When a bundle is loaded. `boot` and `core` stay for the whole session, `scene` and `feature`
6436
+ * come and go with the graph, `lazy` is never preloaded.
6437
+ *
6438
+ * @example
6439
+ * ```ts
6440
+ * const tier: Tier = "scene";
6441
+ * ```
6442
+ */
6443
+ type Tier = "boot" | "core" | "scene" | "feature" | "lazy";
6444
+ /**
6445
+ * What one file of a bundle is. A `.fnt` with its `.png` pages is one `font`, an `.mp3` or an
6446
+ * `.m4a` is `audio`, everything else is a `texture`. A file of an older manifest that names no
6447
+ * kind is a texture.
6448
+ *
6449
+ * @example
6450
+ * ```ts
6451
+ * const kind: AssetKind = "font";
6452
+ * ```
6453
+ */
6454
+ type AssetKind = "texture" | "font" | "audio";
6455
+ /**
6456
+ * A GPU texture. `assets` owns its lifetime and never imports Pixi: `renderer` makes and destroys
6457
+ * it, this plugin only says when.
6458
+ */
6459
+ type Texture = PixiTexture;
6460
+ /**
6461
+ * A decoded image, ready for the upload. `createImageBitmap` produces the first shape.
6462
+ */
6463
+ type DecodedImage = ImageBitmap | HTMLImageElement;
6464
+ /**
6465
+ * Nine-slice borders in pixels, in the order left, top, right, bottom.
6466
+ *
6467
+ * @example
6468
+ * ```ts
6469
+ * const borders: NineBorders = [48, 48, 48, 48];
6470
+ * ```
6471
+ */
6472
+ type NineBorders = readonly [number, number, number, number];
6473
+ /**
6474
+ * Options of `io.createTexture`. Same shape as `renderer.sync.textures.create`.
6475
+ *
6476
+ * @example
6477
+ * ```ts
6478
+ * const options: CreateTextureOptions = { nine: [48, 48, 48, 48] };
6479
+ * ```
6480
+ */
6481
+ type CreateTextureOptions = {
6482
+ nine?: NineBorders;
6483
+ };
6484
+ /**
6485
+ * The part of a fetch response the plugin reads: the manifest is `json`, a texture is a `blob`,
6486
+ * a font file is `text` and an audio file stays the raw `arrayBuffer`. The global `Response`
6487
+ * fits it. `headers` gives the `content-type`: on `tauri://` a missing file comes back `200` with
6488
+ * `index.html`, so `text/html` for a path that is not `.html` counts as missing.
6489
+ */
6490
+ type FetchResponse = {
6491
+ ok: boolean;
6492
+ status: number;
6493
+ headers: {
6494
+ get(name: string): string | null;
6495
+ };
6496
+ json(): Promise<unknown>;
6497
+ blob(): Promise<Blob>;
6498
+ text(): Promise<string>;
6499
+ arrayBuffer(): Promise<ArrayBuffer>;
6500
+ };
6501
+ /**
6502
+ * The I/O seam: everything that leaves the plugin. `undefined` in the config means the browser
6503
+ * pair plus `renderer.sync.textures`; a test passes a fake and nothing touches the network or a GPU.
6504
+ *
6505
+ * @example
6506
+ * ```ts
6507
+ * // A unit test serves two files from memory and counts the textures it handed out.
6508
+ * const io: AssetsIo = {
6509
+ * fetch: async () => ({
6510
+ * ok: true,
6511
+ * status: 200,
6512
+ * headers: { get: () => null },
6513
+ * json: async () => ({}),
6514
+ * blob: async () => blob,
6515
+ * text: async () => 'info face="body"',
6516
+ * arrayBuffer: async () => new ArrayBuffer(8)
6517
+ * }),
6518
+ * decode: async () => bitmap,
6519
+ * createTexture: () => ({ id: "t1" }) as unknown as Texture,
6520
+ * sliceTexture: (page, frame) => ({ id: "s1", page, frame }) as unknown as Texture,
6521
+ * destroyTexture: texture => destroyed.push(texture)
6522
+ * };
6523
+ *
6524
+ * createApp({ plugins: [...screen], pluginConfigs: { assets: { io, manifest } } });
6525
+ * ```
6526
+ */
6527
+ type AssetsIo = {
6528
+ /**
6529
+ * Fetches one URL: the manifest, or a file of a bundle.
6530
+ *
6531
+ * @param url - Absolute or root-relative URL.
6532
+ * @param init - Carries the abort signal of the running load.
6533
+ * @param init.signal - Aborts the request when the load is cancelled.
6534
+ * @returns The response.
6535
+ */
6536
+ fetch(url: string, init: {
6537
+ signal: AbortSignal;
6538
+ }): Promise<FetchResponse>;
6539
+ /**
6540
+ * Decodes one image file.
6541
+ *
6542
+ * @param blob - The bytes of the file.
6543
+ * @returns The decoded image.
6544
+ */
6545
+ decode(blob: Blob): Promise<DecodedImage>;
6546
+ /**
6547
+ * Uploads one decoded image to the GPU.
6548
+ *
6549
+ * @param image - The decoded image.
6550
+ * @param options - `nine` becomes the default nine-slice borders of the texture.
6551
+ * @returns The new texture.
6552
+ */
6553
+ createTexture(image: DecodedImage, options?: CreateTextureOptions): Texture;
6554
+ /**
6555
+ * Cuts the texture of one packed file out of its atlas page. The slice shares the page's
6556
+ * source: no pixel is copied. Same shape as `renderer.sync.textures.slice`; the manifest's
6557
+ * `atlas` frame is passed as it is.
6558
+ *
6559
+ * @param page - The page texture, made by `createTexture`.
6560
+ * @param frame - Where the file lies on the page, in page pixels.
6561
+ * @param options - `nine` becomes the default nine-slice borders of the slice.
6562
+ * @returns The new texture over the page's source.
6563
+ */
6564
+ sliceTexture(page: Texture, frame: SliceFrame, options?: CreateTextureOptions): Texture;
6565
+ /**
6566
+ * Frees one texture. A page frees its source too; a slice frees only itself, so the slices of
6567
+ * a bundle go first and its pages after them.
6568
+ *
6569
+ * @param texture - The texture to free.
6570
+ */
6571
+ destroyTexture(texture: Texture): void;
6572
+ };
6573
+ /**
6574
+ * Nine-slice metadata of one file, as the scanner writes it. Always four numbers: the tags
6575
+ * `{nine=N}`, `{nine=H,V}` and `{nine=L,T,R,B}` all land here.
6576
+ *
6577
+ * @example
6578
+ * ```ts
6579
+ * const nine: NineSlice = { left: 48, top: 48, right: 48, bottom: 48 };
6580
+ * ```
6581
+ */
6582
+ type NineSlice = {
6583
+ left: number;
6584
+ top: number;
6585
+ right: number;
6586
+ bottom: number;
6587
+ };
6588
+ /**
6589
+ * Where a packed file lies: `page` is the id of a page of the same bundle, never a file name, and
6590
+ * the frame is in page pixels. `width` and `height` equal the file's own, so its `nine` stays
6591
+ * valid as it is. The packer writes it; the loader cuts the file out of the page.
6592
+ *
6593
+ * @example
6594
+ * ```ts
6595
+ * const frame: AtlasFrame = { page: "ui/main-0", x: 583, y: 595, width: 256, height: 128 };
6596
+ * ```
6597
+ */
6598
+ type AtlasFrame = {
6599
+ page: string;
6600
+ x: number;
6601
+ y: number;
6602
+ width: number;
6603
+ height: number;
6604
+ };
6605
+ /**
6606
+ * One atlas page of a packed bundle. `id` is `<bundle>/<group>-<index>`, the name an `atlas`
6607
+ * frame uses; `path` is the hashed WebP file, read like the `path` of a file. `mb` is
6608
+ * `width × height × 4 / 1 048 576`: the page carries the cost of every file packed on it.
6609
+ *
6610
+ * @example
6611
+ * ```ts
6612
+ * const page: AtlasPage = {
6613
+ * id: "ui/main-0",
6614
+ * path: "ui/main-0-3b1d55a0c9.webp",
6615
+ * width: 966,
6616
+ * height: 1365,
6617
+ * mb: 5.03
6618
+ * };
6619
+ * ```
6620
+ */
6621
+ type AtlasPage = {
6622
+ id: string;
6623
+ path: string;
6624
+ width: number;
6625
+ height: number;
6626
+ mb: number;
6627
+ };
6628
+ /**
6629
+ * One page image of a font. A page has no key of its own: it belongs to the `.fnt` file that
6630
+ * names it, and is loaded and freed with it.
6631
+ *
6632
+ * @example
6633
+ * ```ts
6634
+ * const page: FontPage = {
6635
+ * path: "features/ui/assets/body_0.png",
6636
+ * width: 256,
6637
+ * height: 128,
6638
+ * mb: 0.125
6639
+ * };
6640
+ * ```
6641
+ */
6642
+ type FontPage = {
6643
+ path: string;
6644
+ width: number;
6645
+ height: number;
6646
+ mb: number;
6647
+ };
6648
+ /**
6649
+ * One file of a bundle. `mb` is what it costs: `width × height × 4` bytes for a texture, the sum
6650
+ * of the pages for a font, the size of the file for audio, and `0` for a texture packed in an
6651
+ * atlas, whose page carries the cost. `kind` is absent for a texture, which is what every
6652
+ * manifest written before fonts and audio carries. A loose file has `path`; a packed texture has
6653
+ * `atlas` and no `path`.
6654
+ *
6655
+ * @example
6656
+ * ```ts
6657
+ * // The dev manifest of the scanner: a loose file.
6658
+ * const loose: ManifestFile = {
6659
+ * key: "ui.panel",
6660
+ * path: "features/ui/assets/panel{nine=48}.png",
6661
+ * width: 256,
6662
+ * height: 128,
6663
+ * mb: 0.125,
6664
+ * nine: { left: 48, top: 48, right: 48, bottom: 48 }
6665
+ * };
6666
+ *
6667
+ * // The packed manifest: the same key, cut out of the page "ui/main-0".
6668
+ * const packed: ManifestFile = {
6669
+ * key: "ui.panel",
6670
+ * width: 256,
6671
+ * height: 128,
6672
+ * mb: 0,
6673
+ * nine: { left: 48, top: 48, right: 48, bottom: 48 },
6674
+ * atlas: { page: "ui/main-0", x: 583, y: 595, width: 256, height: 128 }
6675
+ * };
6676
+ * ```
6677
+ */
6678
+ type ManifestFile = {
6679
+ key: string;
6680
+ path?: string;
6681
+ kind?: AssetKind;
6682
+ width: number;
6683
+ height: number;
6684
+ mb: number;
6685
+ pages?: readonly FontPage[];
6686
+ nine?: NineSlice;
6687
+ atlas?: AtlasFrame;
6688
+ };
6689
+ /**
6690
+ * One bundle of the manifest: which feature owns it, when it loads and what it costs. `pages` is
6691
+ * written by the packer only, sorted by id; `mb` is then its pages, its loose textures, its font
6692
+ * pages and its audio bytes together.
6693
+ *
6694
+ * @example
6695
+ * ```ts
6696
+ * const bundle: ManifestBundle = { feature: "ui", tier: "core", mb: 0.125, files: [] };
6697
+ * ```
5782
6698
  */
5783
- type MonitorDeps = {
5784
- host: HostApi & HostInternal;
5785
- sync: SyncApi & SyncInternal;
6699
+ type ManifestBundle = {
6700
+ feature: string;
6701
+ tier: Tier;
6702
+ mb: number;
6703
+ pages?: readonly AtlasPage[];
6704
+ files: readonly ManifestFile[];
5786
6705
  };
5787
- declare namespace types_d_exports {
5788
- export { Api, AspectRange, BitmapFontData, Config, CountingClasses, CoverFrame, CreateTextureOptions, DebugApi, DebugSwitches, Deps, DisplayAdapter, DisplayEntry, DisplaysApi, DrawCounter, Events, FilterSlot, FiltersApi, FontsApi, HitBox, HostApi, HostInternal, HostModule, HostState, KernelSlice, LayerEntry, Modules, MonitorApi, MonitorDeps, MonitorInternal, MonitorModule, MonitorState, NineBorders, Orientation, PixiApplication, PixiBitmapFont, PixiContainer, PixiFilter, PixiGraphics, PixiModule, PixiNineSliceSprite, PixiSprite, PixiTexture, Point, Rect, RenderStats, RendererCtx, RendererKind, SafeArea, SliceFrame, State, SyncApi, SyncCounts, SyncInternal, SyncModule, SyncState, TeardownScope, TextureProvider, TextureUsage, TexturesApi, View, ViewKind, ViewportApi, ViewportInternal, ViewportModule, ViewportSize, ViewportState };
5789
- }
5790
6706
  /**
5791
- * The part of Pixi the engine uses: the classes `sync` builds views from (a `Rectangle` frames the
5792
- * crop of a `"cover"` sprite), the bitmap-font pieces `fonts.install` needs, the `BitmapText` a
5793
- * plugin above builds through `host.pixi()`, the filter and particle classes `effects` builds
5794
- * through it (a custom filter carries a `GpuProgram` and a `GlProgram` over `defaultFilterVert`),
5795
- * and the extension registry with the three WebGPU draw classes the dev draw-call
5796
- * counter swaps and the bitmap text pipe `host` swaps for one whose SDF shader applies the alpha
5797
- * once (with the WGSL and GLSL shader pieces it compiles that shader from). The module object
5798
- * never arrives through a static import: `config.loadPixi()` returns it, so a game without a screen carries no
5799
- * Pixi in its bundle.
6707
+ * The manifest: the contract between the scanner, the packer, this plugin and the editor. Version
6708
+ * `1` is the dev manifest of `bun run assets:keys`, version `2` the packed one of
6709
+ * `bun run assets:pack`; the plugin reads both. Bundles are sorted by name, pages by id and files
6710
+ * by key, so two runs on the same tree give the same bytes.
5800
6711
  *
5801
6712
  * @example
5802
6713
  * ```ts
5803
- * const pixi: PixiModule = await import("pixi.js");
5804
- * pixi.Texture.WHITE.width; // 1
5805
- * pixi.GpuBatchAdaptor.extension.name; // "batch"
5806
- * pixi.BitmapTextPipe.extension.name; // "bitmapText"
5807
- * pixi.defaultFilterVert.startsWith("in vec2 aPosition;"); // true
6714
+ * // A dev build: loose files, straight from the features.
6715
+ * const manifest: Manifest = {
6716
+ * version: 1,
6717
+ * bundles: { ui: { feature: "ui", tier: "core", mb: 0, files: [] } }
6718
+ * };
6719
+ *
6720
+ * createApp({ plugins: [...screen], pluginConfigs: { assets: { manifest } } });
6721
+ *
6722
+ * // The packed manifest of the same game: the panel is cut out of the page "ui/main-0".
6723
+ * const page = { id: "ui/main-0", path: "ui/main-0-3b1d55a0c9.webp", width: 966, height: 1365, mb: 5.03 };
6724
+ * const atlas = { page: "ui/main-0", x: 583, y: 595, width: 256, height: 128 };
6725
+ * const panel = { key: "ui.panel", width: 256, height: 128, mb: 0, atlas };
6726
+ * const packed: Manifest = {
6727
+ * version: 2,
6728
+ * bundles: { ui: { feature: "ui", tier: "core", mb: 5.03, pages: [page], files: [panel] } }
6729
+ * };
5808
6730
  * ```
5809
6731
  */
5810
- type PixiModule = Pick<typeof import("pixi.js"), "AlphaFilter" | "Application" | "BitmapFont" | "BitmapText" | "BitmapTextPipe" | "BlurFilter" | "Cache" | "ColorMatrixFilter" | "Container" | "DisplacementFilter" | "Filter" | "GlProgram" | "GpuBatchAdaptor" | "GpuEncoderSystem" | "GpuGraphicsAdaptor" | "GpuProgram" | "Graphics" | "Matrix" | "NineSliceSprite" | "NoiseFilter" | "Particle" | "ParticleContainer" | "Rectangle" | "Shader" | "Sprite" | "Texture" | "UniformGroup" | "bitmapFontTextParser" | "bitmapFontXMLStringParser" | "colorBit" | "colorBitGl" | "compileHighShaderGlProgram" | "compileHighShaderGpuProgram" | "defaultFilterVert" | "extensions" | "generateTextureBatchBit" | "generateTextureBatchBitGl" | "getBatchSamplersUniformGroup" | "localUniformMSDFBit" | "localUniformMSDFBitGl" | "mSDFBit" | "mSDFBitGl" | "roundPixelsBit" | "roundPixelsBitGl">;
6732
+ type Manifest = {
6733
+ version: 1 | 2;
6734
+ bundles: Readonly<Record<string, ManifestBundle>>;
6735
+ };
5811
6736
  /**
5812
- * One Pixi application: renderer, canvas and stage.
6737
+ * What a feature declares about one of its bundles. `files` are globs relative to the feature's
6738
+ * `assets/`; a bundle with `files` takes those files out of the feature's default bundle.
6739
+ *
6740
+ * @example
6741
+ * ```ts
6742
+ * const spec: BundleSpec = { tier: "lazy", files: ["chains/*.png"] };
6743
+ * ```
5813
6744
  */
5814
- type PixiApplication = InstanceType<PixiModule["Application"]>;
6745
+ type BundleSpec = {
6746
+ tier: Tier;
6747
+ files?: readonly string[];
6748
+ };
5815
6749
  /**
5816
- * A Pixi display object. `Sprite` and `NineSliceSprite` are containers too, so every view is one.
6750
+ * What `defineBundles` returns: plain data the scanner and the plugin both read.
6751
+ *
6752
+ * @example
6753
+ * ```ts
6754
+ * const bundles: BundleMap = {
6755
+ * kind: "bundles",
6756
+ * map: { board: { tier: "scene" }, "board.chains": { tier: "lazy", files: ["chains/*.png"] } }
6757
+ * };
6758
+ * ```
5817
6759
  */
5818
- type PixiContainer = InstanceType<PixiModule["Container"]>;
6760
+ type BundleMap<Key extends string = string> = {
6761
+ kind: "bundles";
6762
+ map: Partial<Record<Key, BundleSpec>>;
6763
+ };
5819
6764
  /**
5820
- * A Pixi sprite.
6765
+ * `defineBundles` bound to the bundle keys of one game. `defineGame` returns it, so a key that is
6766
+ * not in `BundleKey` does not compile.
6767
+ *
6768
+ * @example
6769
+ * ```ts
6770
+ * const defineGameBundles: DefineBundles<"board" | "board.chains"> = defineBundles;
6771
+ * ```
5821
6772
  */
5822
- type PixiSprite = InstanceType<PixiModule["Sprite"]>;
6773
+ type DefineBundles<Key extends string> = (map: Partial<Record<Key, BundleSpec>>) => BundleMap<Key>;
5823
6774
  /**
5824
- * A Pixi nine-slice sprite.
6775
+ * The `load` descriptor helper bound to the bundle keys of one game.
6776
+ *
6777
+ * @example
6778
+ * ```ts
6779
+ * const loadGameBundle: LoadBundles<"board" | "board.chains"> = load;
6780
+ * ```
5825
6781
  */
5826
- type PixiNineSliceSprite = InstanceType<PixiModule["NineSliceSprite"]>;
6782
+ type LoadBundles<Key extends string> = (bundle: Key | readonly Key[]) => Descriptor;
5827
6783
  /**
5828
- * A Pixi graphics object: what a `Shape` component is drawn with, and what clips its children.
6784
+ * What the `load` effect resolves with, so a loading node can write its own progress.
6785
+ *
6786
+ * @example
6787
+ * ```ts
6788
+ * const result: LoadResult = { loaded: ["board.chains"], mb: 1.25 };
6789
+ * ```
5829
6790
  */
5830
- type PixiGraphics = InstanceType<PixiModule["Graphics"]>;
6791
+ type LoadResult = {
6792
+ loaded: string[];
6793
+ mb: number;
6794
+ };
5831
6795
  /**
5832
- * A Pixi texture. `assets` owns its lifetime; the renderer only makes and destroys it on request.
6796
+ * Why a bundle was loaded. It is the reason of the caller that started the load; a later waiter
6797
+ * changes nothing.
6798
+ *
6799
+ * @example
6800
+ * ```ts
6801
+ * const reason: LoadReason = "enter";
6802
+ * ```
5833
6803
  */
5834
- type PixiTexture = InstanceType<PixiModule["Texture"]>;
6804
+ type LoadReason = "boot" | "enter" | "request" | "preload";
5835
6805
  /**
5836
- * A Pixi filter: what `effects` builds and `sync.filters.set` hangs on a view. `effects` owns
5837
- * the instance; the renderer only assigns it.
6806
+ * The running load of one bundle. Every caller is a waiter; the last one to leave aborts it.
6807
+ * `promise` never rejects: what broke waits in `error`, so a load nobody joined any more cannot
6808
+ * become an unhandled rejection.
5838
6809
  */
5839
- type PixiFilter = InstanceType<PixiModule["Filter"]>;
6810
+ type Inflight = {
6811
+ promise: Promise<void>;
6812
+ controller: AbortController;
6813
+ waiters: number;
6814
+ error: unknown;
6815
+ };
5840
6816
  /**
5841
- * A bitmap font the renderer installed for a font asset key. One per key.
6817
+ * A loaded font: the `.fnt` file as text and the page textures it names. `text` installs the
6818
+ * first page in the renderer; every page is destroyed with the bundle.
6819
+ *
6820
+ * @example
6821
+ * ```ts
6822
+ * const font: FontAsset = { fnt: 'info face="body" size=32', texture };
6823
+ * ```
5842
6824
  */
5843
- type PixiBitmapFont = InstanceType<PixiModule["BitmapFont"]>;
6825
+ type FontAsset = {
6826
+ fnt: string;
6827
+ texture: Texture;
6828
+ };
5844
6829
  /**
5845
- * The parsed shape of a `.fnt` file: what `BitmapFont` is built from.
6830
+ * What the plugin keeps for a loaded font: the public pair plus every page, because a font of
6831
+ * two pages owns two textures.
5846
6832
  */
5847
- type BitmapFontData = ConstructorParameters<PixiModule["BitmapFont"]>[0]["data"];
6833
+ type LoadedFont = FontAsset & {
6834
+ pages: readonly Texture[];
6835
+ };
5848
6836
  /**
5849
- * Which backend the one application drew with, or `"none"` while it draws nothing.
6837
+ * The container type of a sound, read from the extension of its file: `.mp3` is `"audio/mpeg"`,
6838
+ * `.m4a` (AAC in an MP4 container) is `"audio/mp4"`. These are the two formats the scanner takes,
6839
+ * and the two every browser and WebView decodes.
5850
6840
  *
5851
6841
  * @example
5852
6842
  * ```ts
5853
- * const kind: RendererKind = "webgpu";
6843
+ * const mime: AudioMime = "audio/mp4"; // the type of theme.m4a
5854
6844
  * ```
5855
6845
  */
5856
- type RendererKind = "webgpu" | "webgl" | "none";
6846
+ type AudioMime = "audio/mpeg" | "audio/mp4";
5857
6847
  /**
5858
- * renderer plugin events.
6848
+ * A loaded sound: the bytes exactly as they were fetched, never decoded here, and the MIME type
6849
+ * of their container. A `Blob` of the bytes needs the type: Safari does not sniff a typeless one.
5859
6850
  *
5860
6851
  * @example
5861
6852
  * ```ts
5862
- * // A game reports a lost GPU to its analytics; the pause itself is handled by `lifecycle`.
5863
- * createPlugin("gpuReport", {
5864
- * depends: [rendererPlugin],
5865
- * hooks: ctx => ({ "renderer:device-lost": ({ kind }) => ctx.log.warn("gpu-lost", { kind }) })
5866
- * }); // a lost WebGPU device logs { kind: "webgpu" }
6853
+ * const asset: AudioAsset = { bytes: new ArrayBuffer(12_288), mime: "audio/mpeg" }; // click.mp3
5867
6854
  * ```
5868
6855
  */
5869
- type Events = {
5870
- /** The GPU device or the WebGL context was lost; the game is paused until the restore lands. */"renderer:device-lost": {
5871
- kind: "webgpu" | "webgl";
5872
- reason: string;
5873
- };
6856
+ type AudioAsset = {
6857
+ bytes: ArrayBuffer;
6858
+ mime: AudioMime;
5874
6859
  };
5875
6860
  /**
5876
- * The two numbers that bound the shape of the drawn frame: allowed long side / short side.
5877
- *
5878
- * @example
5879
- * ```ts
5880
- * const aspect: AspectRange = { min: 4 / 3, max: 21 / 9 };
5881
- * ```
6861
+ * What one bundle brought: textures by asset key (a packed file's is a slice of its page), fonts
6862
+ * with their pages, the undecoded sounds with their MIME types, and the atlas pages by page id. A
6863
+ * running load fills the same four maps before they are published.
5882
6864
  */
5883
- type AspectRange = {
5884
- min: number;
5885
- max: number;
6865
+ type LoadedAssets = {
6866
+ textures: Map<string, Texture>;
6867
+ fonts: Map<string, LoadedFont>;
6868
+ audio: Map<string, AudioAsset>;
6869
+ pages: Map<string, Texture>;
5886
6870
  };
5887
6871
  /**
5888
- * renderer plugin config.
6872
+ * What the plugin knows about one bundle. `lastUsed` is the value of `useCounter` at the last
6873
+ * touch: a counter, never a clock.
6874
+ */
6875
+ type BundleRecord = LoadedAssets & {
6876
+ status: "idle" | "loading" | "loaded";
6877
+ inflight: Inflight | undefined;
6878
+ lastUsed: number;
6879
+ };
6880
+ /**
6881
+ * The background preload. A new rest node replaces it and aborts the old controller.
6882
+ */
6883
+ type PreloadQueue = {
6884
+ bundles: string[];
6885
+ controller: AbortController;
6886
+ };
6887
+ /**
6888
+ * assets plugin config.
5889
6889
  *
5890
6890
  * @example
5891
6891
  * ```ts
5892
6892
  * createApp({
5893
- * plugins: [...screen],
5894
- * pluginConfigs: { renderer: { mount: "#game", background: 0x101018, maxResolution: 2 } }
6893
+ * plugins: [...screen, boardFeature],
6894
+ * pluginConfigs: { assets: { manifest: "/assets/manifest.json", textureBudgetMb: 192 } }
5895
6895
  * });
5896
- * // A debug build of the fixture outlines every nine-slice from the first frame.
5897
- * createApp({ pluginConfigs: { renderer: { mount: "#game", debug: { nineSlice: true } } } });
5898
6896
  * ```
5899
6897
  */
5900
6898
  type Config = {
5901
- /** Where the canvas goes: a selector or an element. `undefined` keeps the plugin inert. */mount: string | HTMLElement | undefined; /** Clear colour. Also the colour of the bars around the frame. */
5902
- background: number; /** Antialias the whole canvas. Off by default: pixel art and sprites do not need it. */
5903
- antialias: boolean; /** Cap of `devicePixelRatio`. Memory grows with its square. */
5904
- maxResolution: number; /** Passed to Pixi. Pixi falls back to WebGL by itself. */
5905
- preference: "webgpu" | "webgl"; /** Allowed long side / short side of the frame. A window outside it gets bars. */
5906
- aspect: AspectRange; /** Display objects kept in all pools together. */
5907
- poolLimit: number; /** Text of the unsupported-device screen. */
5908
- unsupportedMessage: string; /** Loader seam. Tests pass a fake module. */
5909
- loadPixi: () => Promise<PixiModule>; /** Debug drawing at start. `app.renderer.sync.debug` switches it while the game runs. */
5910
- debug: DebugSwitches;
6899
+ /** URL of `manifest.json`, or the parsed manifest itself (tests, headless). `undefined`: an empty manifest. */manifest: string | Manifest | undefined; /** Texture memory budget in MB. */
6900
+ textureBudgetMb: number; /** How many edges from a rest node the background preload looks ahead. `0` turns preload off. */
6901
+ preloadDepth: number; /** Prefix of every file URL. The CDN seam. `undefined`: the folder of the manifest URL, or `"/"`. */
6902
+ baseUrl: string | undefined; /** I/O seam. `undefined`: the browser pair plus `renderer.sync.textures`, or headless. */
6903
+ io: AssetsIo | undefined;
5911
6904
  };
5912
6905
  /**
5913
- * renderer plugin state: one branch per module.
6906
+ * assets plugin state.
5914
6907
  */
5915
6908
  type State = {
5916
- host: HostState;
5917
- viewport: ViewportState;
5918
- sync: SyncState;
5919
- monitor: MonitorState;
6909
+ /** `undefined` means headless: manifest only, no fetches, no textures. */io: AssetsIo | undefined; /** Empty until `onStart` read it. */
6910
+ manifest: Manifest; /** Asset key to the bundle that carries it. */
6911
+ bundleOfKey: Map<string, string>; /** Scene id to its bundle, read from the `scenes` key of `flow.features`. */
6912
+ bundleOfScene: Map<string, string>; /** Flow id to the feature that owns it, read from the `flows` key of `flow.features`. */
6913
+ featureOfFlow: Map<string, string>;
6914
+ records: Map<string, BundleRecord>; /** Bundles of the node entered last. They are never unloaded. */
6915
+ pinned: Set<string>; /** The bundle of the scene entered last. A node without `scene` keeps it. */
6916
+ sceneBundle: string | undefined;
6917
+ queue: PreloadQueue | undefined;
6918
+ current: NodeInfo | undefined;
6919
+ useCounter: number; /** Keys already reported missing, so one key warns and starts its background load once. */
6920
+ warned: Set<string>; /** `onEnter`, the `load` handler and the texture provider. */
6921
+ removers: Array<() => void>;
5920
6922
  };
5921
6923
  /**
5922
- * renderer plugin API, `app.renderer`: grouped by module, and the two members of `monitor`,
5923
- * `stats()` and `capture()`, on the plugin itself.
6924
+ * What `usage()` reports about one loaded bundle.
5924
6925
  *
5925
6926
  * @example
5926
6927
  * ```ts
5927
- * app.renderer.host.kind(); // "webgpu"
5928
- * app.renderer.viewport.size().width; // 1080, reference units
5929
- * app.renderer.sync.hitTest(540, 300, () => true); // 1048576
5930
- * app.renderer.stats().views; // 180
5931
- * app.renderer.stats().renderPasses; // 1: no filter on screen
6928
+ * const entry: BundleUsage = { name: "board", tier: "scene", mb: 3.5, lastUsed: 12 };
5932
6929
  * ```
5933
6930
  */
5934
- type Api = {
5935
- host: HostApi;
5936
- viewport: ViewportApi;
5937
- sync: SyncApi;
5938
- } & MonitorApi;
5939
- /**
5940
- * Resolved dependency APIs. `clock` is the time source of the frame counters.
5941
- */
5942
- type Deps = {
5943
- time: Api$3;
5944
- lifecycle: Api$5;
5945
- world: Api$1;
5946
- clock: Api$6;
6931
+ type BundleUsage = {
6932
+ name: string;
6933
+ tier: Tier;
6934
+ mb: number;
6935
+ lastUsed: number;
5947
6936
  };
5948
6937
  /**
5949
- * What the kernel context offers before the deps are attached.
6938
+ * What `usage()` reports: the numbers a dev overlay and the tests read.
5950
6939
  *
5951
- * `emit` is the kernel's own: `index.ts` writes `events` with an annotated `register`
5952
- * (core spec `14-EVENT-REGISTRATION.md` row 8), so the plugin's own event reaches the pre-typed
5953
- * `api` factory and no member of this context is cast.
6940
+ * @example
6941
+ * ```ts
6942
+ * const report: Usage = { textureMb: 3.5, budgetMb: 192, bundles: [] };
6943
+ * ```
5954
6944
  */
5955
- type KernelSlice = PluginCtx<Config, State, Events> & {
5956
- readonly global: Readonly<Config$7>;
5957
- readonly log: Log.LogApi;
5958
- readonly require: Require;
6945
+ type Usage = {
6946
+ textureMb: number;
6947
+ budgetMb: number;
6948
+ bundles: readonly BundleUsage[];
5959
6949
  };
5960
6950
  /**
5961
- * Domain context shared by the three modules.
6951
+ * assets plugin events.
6952
+ *
6953
+ * @example
6954
+ * ```ts
6955
+ * // A game reports every loaded bundle to its analytics.
6956
+ * createPlugin("loadReport", {
6957
+ * depends: [assetsPlugin],
6958
+ * hooks: ctx => ({ "assets:bundle-loaded": ({ bundle, mb }) => ctx.log.info("loaded", { bundle, mb }) })
6959
+ * }); // the board bundle logs { bundle: "board", mb: 3.5 }
6960
+ *
6961
+ * // A splash screen fills its loading bar file by file.
6962
+ * createPlugin("loadingBar", {
6963
+ * depends: [assetsPlugin],
6964
+ * createState: () => ({ share: 0 }),
6965
+ * hooks: ctx => ({
6966
+ * "assets:bundle-progress": ({ loaded, total }) => {
6967
+ * ctx.state.share = loaded / total;
6968
+ * }
6969
+ * })
6970
+ * }); // a board bundle of four files sets share to 0.25, 0.5, 0.75 and 1; bundle-loaded follows
6971
+ * ```
5962
6972
  */
5963
- type RendererCtx = KernelSlice & {
5964
- readonly deps: Deps;
6973
+ type Events = {
6974
+ /** Every file of a bundle is a texture now. */"assets:bundle-loaded": {
6975
+ bundle: string;
6976
+ tier: Tier;
6977
+ mb: number;
6978
+ reason: LoadReason;
6979
+ };
6980
+ /**
6981
+ * One more file of a running load settled. `loaded` counts the settled files (a font once, with
6982
+ * its pages), `total` is the file count of the bundle; the last one of a load has
6983
+ * `loaded === total` and comes before `assets:bundle-loaded`. An aborted load sends none.
6984
+ */
6985
+ "assets:bundle-progress": {
6986
+ bundle: string;
6987
+ loaded: number;
6988
+ total: number;
6989
+ }; /** The textures of a bundle were destroyed. `keys` names every asset that went with it. */
6990
+ "assets:bundle-unloaded": {
6991
+ bundle: string;
6992
+ tier: Tier;
6993
+ mb: number;
6994
+ reason: "budget" | "request";
6995
+ keys: readonly string[];
6996
+ };
5965
6997
  };
5966
6998
  /**
5967
- * Both halves of the `host` module: what a game calls and what the two modules above get injected.
5968
- */
5969
- type HostModule = HostApi & HostInternal;
5970
- /**
5971
- * Both halves of the `viewport` module: what a game calls and what `sync` and the frame drive.
6999
+ * assets plugin API, `app.assets`. Bundles of textures by key: the graph decides when they arrive,
7000
+ * the budget decides when they leave.
7001
+ *
7002
+ * @example
7003
+ * ```ts
7004
+ * // A loading node asks for a bundle and a game system asks for one texture of it.
7005
+ * await app.assets.load("board");
7006
+ * app.assets.texture("board.cell"); // the Pixi texture, once the bundle is there
7007
+ * ```
5972
7008
  */
5973
- type ViewportModule = ViewportApi & ViewportInternal;
7009
+ type Api = {
7010
+ /**
7011
+ * Loads every file of a bundle. An already loaded bundle resolves at once, a loading one joins
7012
+ * the running load. Headless it resolves at once and touches nothing.
7013
+ *
7014
+ * @param bundle - Name of a bundle of the manifest.
7015
+ * @returns A promise that resolves when every texture of the bundle exists.
7016
+ * @throws {Error} When the manifest has no such bundle, and when a file fails to load.
7017
+ * @example
7018
+ * ```ts
7019
+ * // A game plugin warms the bundle of a timed event before its popup can open.
7020
+ * await app.assets.load("event.halloween");
7021
+ * app.assets.isLoaded("event.halloween"); // true
7022
+ * ```
7023
+ */
7024
+ load(bundle: string): Promise<void>;
7025
+ /**
7026
+ * Destroys the textures of a bundle and tells `renderer` that its keys are gone. A pinned
7027
+ * bundle and the tiers `boot` and `core` are refused with a warning. A running load is aborted.
7028
+ *
7029
+ * @param bundle - Name of a loaded bundle.
7030
+ * @example
7031
+ * ```ts
7032
+ * // The timed event ended: its textures go back to the GPU.
7033
+ * app.assets.unload("event.halloween");
7034
+ * app.assets.isLoaded("event.halloween"); // false
7035
+ * ```
7036
+ */
7037
+ unload(bundle: string): void;
7038
+ /**
7039
+ * Tells whether every texture of a bundle exists. Headless every bundle of the manifest counts
7040
+ * as loaded, so a headless game never waits for a loading screen.
7041
+ *
7042
+ * @param bundle - Name of a bundle of the manifest.
7043
+ * @returns True when the bundle is loaded.
7044
+ * @example
7045
+ * ```ts
7046
+ * // `scenes` skips its loading path when the bundle of the next scene is already there.
7047
+ * if (!app.assets.isLoaded("board")) await app.assets.load("board");
7048
+ * ```
7049
+ */
7050
+ isLoaded(bundle: string): boolean;
7051
+ /**
7052
+ * The texture of a loaded asset key. It touches the use counter of the bundle, which is what
7053
+ * the LRU reads. A key of a bundle that is not loaded warns once, starts a background load and
7054
+ * answers `undefined`; the sprites waiting for it are textured when that load lands. A key packed
7055
+ * in an atlas answers its slice of the page, so a game never sees the difference.
7056
+ *
7057
+ * @param key - Asset key, as `generated/assets.ts` types it.
7058
+ * @returns The texture, or `undefined` while the bundle is not loaded.
7059
+ * @example
7060
+ * ```ts
7061
+ * // A game system builds one Pixi object by hand instead of using the Sprite component.
7062
+ * const texture = app.assets.texture("board.cell"); // undefined until the board bundle lands
7063
+ * ```
7064
+ */
7065
+ texture(key: string): Texture | undefined;
7066
+ /**
7067
+ * The font of a loaded asset key: the `.fnt` file as text and the texture of its first page.
7068
+ * It touches the use counter of the bundle. A font of a bundle that is not loaded, a key of
7069
+ * another kind and every headless run answer `undefined`.
7070
+ *
7071
+ * @param key - Asset key of a `.fnt` file, as `generated/assets.ts` types it in `FontKey`.
7072
+ * @returns The font, or `undefined` while its bundle is not loaded.
7073
+ * @example
7074
+ * ```ts
7075
+ * // `text` installs the font in the renderer when the bundle that carries it arrived.
7076
+ * const font = app.assets.font("ui.body"); // { fnt: 'info face="body" size=32', texture }
7077
+ *
7078
+ * if (font !== undefined) app.renderer.sync.fonts.install("ui.body", font.fnt, font.texture);
7079
+ * ```
7080
+ */
7081
+ font(key: string): FontAsset | undefined;
7082
+ /**
7083
+ * The sound of a loaded audio key: its bytes exactly as they were fetched (this plugin never
7084
+ * decodes them) and the MIME type of its container, `"audio/mpeg"` for an `.mp3` and
7085
+ * `"audio/mp4"` for an `.m4a`. A packed file keeps its extension, so it keeps its type. It
7086
+ * touches the use counter of the bundle. A bundle that is not loaded, a key of another kind and
7087
+ * every headless run answer `undefined`.
7088
+ *
7089
+ * @param key - Asset key of an `.mp3` or `.m4a` file, as `generated/assets.ts` types it in
7090
+ * `AudioKey`.
7091
+ * @returns The bytes and their type, or `undefined` while its bundle is not loaded.
7092
+ * @example
7093
+ * ```ts
7094
+ * // The audio plugin decodes a sound, or streams a long track from a blob: URL of its bytes.
7095
+ * const asset = ctx.require(assetsPlugin).audio("ui.click");
7096
+ * // { bytes: ArrayBuffer(12 288), mime: "audio/mpeg" }
7097
+ * if (asset !== undefined) URL.createObjectURL(new Blob([asset.bytes], { type: asset.mime }));
7098
+ * ```
7099
+ */
7100
+ audio(key: string): AudioAsset | undefined;
7101
+ /**
7102
+ * What the loaded bundles cost, sorted by name. `lastUsed` is the use counter, not a clock.
7103
+ *
7104
+ * @returns The used and allowed megabytes and one entry per loaded bundle.
7105
+ * @example
7106
+ * ```ts
7107
+ * // A dev overlay draws the memory bar of the game.
7108
+ * const { textureMb, budgetMb, bundles } = app.assets.usage();
7109
+ * // textureMb: 3.5, budgetMb: 192, bundles: [{ name: "board", tier: "scene", mb: 3.5, lastUsed: 12 }]
7110
+ * ```
7111
+ */
7112
+ usage(): Usage;
7113
+ };
5974
7114
  /**
5975
- * Both halves of the `sync` module: what `input` and `assets` call and what the frame drives.
7115
+ * Resolved dependency APIs.
5976
7116
  */
5977
- type SyncModule = SyncApi & SyncInternal;
7117
+ type Deps = {
7118
+ flow: Api$3;
7119
+ renderer: Api$1;
7120
+ time: Api$4;
7121
+ };
5978
7122
  /**
5979
- * Both halves of the `monitor` module: what a tool calls and what the frame drives.
7123
+ * What the kernel context offers before the deps are attached.
7124
+ *
7125
+ * `index.ts` writes `events` with an annotated `register` (core spec `14-EVENT-REGISTRATION.md`
7126
+ * row 8), so the two own events reach the context the kernel hands the factories and `emit` is the
7127
+ * kernel's own, assets-typed one. No member of the context is cast.
5980
7128
  */
5981
- type MonitorModule = MonitorApi & MonitorInternal;
7129
+ type KernelSlice = PluginCtx<Config, State, Events> & {
7130
+ readonly global: object;
7131
+ readonly log: Log.LogApi;
7132
+ readonly require: Require;
7133
+ };
5982
7134
  /**
5983
- * The four modules in injection order.
7135
+ * Domain context shared by the files of the plugin.
5984
7136
  */
5985
- type Modules = {
5986
- host: HostModule;
5987
- viewport: ViewportModule;
5988
- sync: SyncModule;
5989
- monitor: MonitorModule;
7137
+ type AssetsCtx = KernelSlice & {
7138
+ readonly deps: Deps;
5990
7139
  };
5991
7140
  /**
5992
- * What `onStop` receives: the frozen config and the plugin state, nothing else.
7141
+ * Payload of the one event assets listens to.
5993
7142
  */
5994
- type TeardownScope = {
5995
- readonly config: Readonly<Config>;
5996
- readonly state: State;
7143
+ type FlowRest = {
7144
+ path: string;
7145
+ checkpoint: boolean;
5997
7146
  };
5998
7147
  //#endregion
5999
- export { Position as $, types_d_exports$6 as $t, TrackSegment as A, Json as At, Mut as B, StoreApi as Bt, LayerSpec as C, Config$3 as Ct, ProjectionMotion as D, Api$4 as Dt, NumericFields as E, types_d_exports$3 as Et, ComponentHandle as F, PlayerStateProvider as Ft, SystemDefinition as G, Events$4 as Gt, Owner as H, RngState as Ht, ComponentType as I, ProviderCall as It, BundlesOf as J, types_d_exports$5 as Jt, TagType as K, PauseReason as Kt, ComponentValue as L, SaveDoc as Lt, AnyComponent as M, State$4 as Mt, AnyComponentType as N, types_d_exports$4 as Nt, ProjectionSpec as O, Config$4 as Ot, AnyComponentValue as P, Patch as Pt, GameTypes as Q, State$6 as Qt, Entity as R, SaveUnreadableError as Rt, LayerSort as S, Api$3 as St, MotionHandle as T, Time as Tt, QueryTerm as U, Api$5 as Ut, Narrowed as V, RngApi as Vt, ResourceType as W, Config$5 as Wt, EmitterIdOf as X, Config$6 as Xt, Config$2 as Y, Api$6 as Yt, FeaturePlugin as Z, FakeClock as Zt, State$1 as _, TypeTag as _t, PixiSprite as a, slot as at, DescriptionNode as b, Hint as bt, types_d_exports as c, FeatureDescription as ct, FilterSlot as d, FlowGraph as dt, Config$7 as en, State$2 as et, SliceFrame as f, FlowState as ft, Config$1 as g, SceneIdOf as gt, Api$1 as h, RouteStep as ht, PixiModule as i, exit as it, ViewHandle as j, Root as jt, TrackOptions as k, Events$3 as kt, RenderStats as l, Bookmark as lt, ViewportSize as m, NodeInfo as mt, Config as n, Require as nn, types_d_exports$2 as nt, PixiTexture as o, to as ot, Point as p, JournalEntry as pt, Api$2 as q, State$5 as qt, PixiFilter as r, defineFlow as rt, State as s, type as st, Api as t, Events$5 as tn, TextStylesOf as tt, DebugSwitches as u, DefineNode as ut, types_d_exports$1 as v, Descriptor as vt, Motion as w, State$3 as wt, Ease as x, Answer as xt, ChangeHook as y, GuideOptions as yt, EntitySnapshot as z, Snapshot as zt };
7148
+ export { Ease as $, SaveDoc as $t, Config$2 as A, JournalEntry as At, ComponentValue as B, State$4 as Bt, Captured as C, to as Ct, Point as D, DefineNode as Dt, FilterSlot as E, Bookmark as Et, AnyComponentType as F, GuideOptions as Ft, Narrowed as G, Events$4 as Gt, EntitySnapshot as H, types_d_exports$4 as Ht, AnyComponentValue as I, Hint as It, ResourceType as J, State$5 as Jt, Owner as K, Json as Kt, ComponentHandle as L, Answer as Lt, State$2 as M, SceneIdOf as Mt, types_d_exports$2 as N, TypeTag as Nt, ViewportSize as O, FlowGraph as Ot, AnyComponent as P, Descriptor as Pt, DescriptionNode as Q, ProviderCall as Qt, ComponentSchema as R, Api$4 as Rt, types_d_exports$1 as S, slot as St, DebugSwitches as T, FeatureDescription as Tt, FrameDiff as U, Api$5 as Ut, Entity as V, Time as Vt, Mut as W, Config$5 as Wt, TagType as X, Patch as Xt, SystemDefinition as Y, types_d_exports$5 as Yt, ChangeHook as Z, PlayerStateProvider as Zt, PixiModule as _, Events$6 as _n, State$3 as _t, DefineBundles as a, Api$6 as an, ProjectionMotion as at, State$1 as b, defineFlow as bt, LoadReason as c, PauseReason as cn, TrackSegment as ct, Tier as d, Api$7 as dn, BundlesOf as dt, SaveUnreadableError as en, LayerSort as et, Usage as f, Config$7 as fn, Config$3 as ft, PixiFilter as g, Config$8 as gn, Position as gt, Config$1 as h, types_d_exports$7 as hn, GameTypes as ht, Config as i, RngState as in, NumericFields as it, Explained as j, RouteStep as jt, Api$2 as k, FlowState as kt, Manifest as l, State$6 as ln, ViewHandle as lt, Api$1 as m, State$7 as mn, FeaturePlugin as mt, BundleMap as n, StoreApi as nn, Motion as nt, Events as o, Config$6 as on, ProjectionSpec as ot, types_d_exports as p, FakeClock as pn, EmitterIdOf as pt, QueryTerm as q, Root as qt, BundleSpec as r, RngApi as rn, MotionHandle as rt, LoadBundles as s, Events$5 as sn, TrackOptions as st, Api as t, Snapshot as tn, LayerSpec as tt, State as u, types_d_exports$6 as un, Api$3 as ut, PixiSprite as v, Require as vn, TextStylesOf as vt, RenderStats as w, type as wt, Under as x, exit as xt, PixiTexture as y, types_d_exports$3 as yt, ComponentType as z, Config$4 as zt };