@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
@@ -1,10 +1,214 @@
1
- import { At as Json, D as ProjectionMotion, I as ComponentType, L as ComponentValue, N as AnyComponentType, P as AnyComponentValue, R as Entity, St as Api, T as MotionHandle, bt as Hint, h as Api$2, j as ViewHandle, m as ViewportSize, nn as Require, p as Point$1, q as Api$1, t as Api$3, vt as Descriptor, w as Motion, y as ChangeHook } from "./types-BZ1OLf24.mjs";
1
+ import { B as ComponentValue, D as Point$2, F as AnyComponentType, I as AnyComponentValue, It as Hint, Kt as Json, O as ViewportSize, P as AnyComponent, Pt as Descriptor, Rt as Api, V as Entity, Z as ChangeHook, at as ProjectionMotion, dn as Api$5, it as NumericFields, k as Api$2, lt as ViewHandle, m as Api$3, nt as Motion, o as Events$1, rt as MotionHandle, t as Api$4, ut as Api$1, vn as Require, z as ComponentType } from "./types-Dy1xB68L.mjs";
2
2
  import { Log } from "@moku-labs/common/browser";
3
3
  import { PluginCtx } from "@moku-labs/core";
4
4
 
5
- //#region src/plugins/input/types.d.ts
6
- declare namespace types_d_exports$1 {
7
- export { Config$1 as Config, Deps, Direction, GesturePhase, InputApi, InputCtx, KernelSlice, KeyInput, KeyListener, Point, PointerListener, RawSample, State$1 as State, TapListener, Target };
5
+ //#region src/plugins/renderer/components.d.ts
6
+ /**
7
+ * Where a view sits, in reference units and radians. Relative to the `Parent` when there is one.
8
+ * `pivot` is the local point the view turns and scales around, and `x`, `y` is where that point
9
+ * lands; the default pivot `{ x: 0, y: 0 }` is the view's own origin.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * // A 200 x 80 button that grows around its centre.
14
+ * const value: TransformValue = {
15
+ * x: 640, y: 340, rotation: 0, scale: 1.1, pivot: { x: 100, y: 40 }
16
+ * };
17
+ * ```
18
+ */
19
+ type TransformValue = {
20
+ x: number;
21
+ y: number;
22
+ rotation: number;
23
+ scale: number;
24
+ pivot: Point$2;
25
+ };
26
+ /**
27
+ * How a sized sprite fills its box: `"fill"` stretches, `"contain"` scales uniformly inside the
28
+ * box and centres, `"cover"` scales uniformly over the box and crops the overflow.
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * const fit: SpriteFit = "cover";
33
+ * ```
34
+ */
35
+ type SpriteFit = "fill" | "contain" | "cover";
36
+ /**
37
+ * One textured quad. `texture` is an asset key; `defineGame` narrows it to the game's keys.
38
+ * `width` and `height` are the box in reference units; 0 on an axis keeps the texture's own size
39
+ * there. `anchor` is the point of the box that sits on the `Transform`. `alpha` fades the quad
40
+ * and the entity's children.
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * const value: SpriteValue = {
45
+ * texture: "board.bg-forest-meadow", tint: 0xffffff, alpha: 1, anchor: { x: 0, y: 0 },
46
+ * width: 1080, height: 1920, fit: "cover"
47
+ * };
48
+ * ```
49
+ */
50
+ type SpriteValue = {
51
+ texture: string;
52
+ tint: number;
53
+ alpha: number;
54
+ anchor: Point$2;
55
+ width: number;
56
+ height: number;
57
+ fit: SpriteFit;
58
+ };
59
+ /**
60
+ * A stretchable panel. The slice borders come with the texture, not with the component. `debug`
61
+ * strokes the bounds and the four cut lines over the panel: cyan, red when the corners overlap or
62
+ * the texture is missing. `clip` masks the children of the entity to the `width × height` box,
63
+ * the same rule as `Shape.clip`: the mask is a filled rectangle and is never drawn, and the panel
64
+ * itself is never masked. `alpha` fades the panel and the entity's children.
65
+ *
66
+ * @example
67
+ * ```ts
68
+ * const value: NineSliceValue = {
69
+ * texture: "ui.panel", width: 600, height: 320, alpha: 1, tint: 0xffffff, debug: false,
70
+ * clip: false
71
+ * };
72
+ * // The board tray, outlined while its insets are checked.
73
+ * const tray: NineSliceValue = {
74
+ * ...value, texture: "board.board-tray", width: 1000, height: 1040, debug: true
75
+ * };
76
+ * // A text field: a long value never draws outside the frame.
77
+ * const field: NineSliceValue = {
78
+ * ...value, texture: "ui.field", width: 480, height: 96, clip: true
79
+ * };
80
+ * ```
81
+ */
82
+ type NineSliceValue = {
83
+ texture: string;
84
+ width: number;
85
+ height: number;
86
+ alpha: number;
87
+ tint: number;
88
+ debug: boolean;
89
+ clip: boolean;
90
+ };
91
+ /**
92
+ * The escape hatch: the game owns a Pixi object and `sync` only attaches and detaches it.
93
+ *
94
+ * @example
95
+ * ```ts
96
+ * const value: DisplayValue = { object: spineAnimation };
97
+ * ```
98
+ */
99
+ type DisplayValue = {
100
+ object: unknown;
101
+ };
102
+ /**
103
+ * A filled rounded rectangle or a triangle, anchored at the top left of the `Transform`. `kind`
104
+ * picks the outline: `"rect"` (the default) or `"triangle"`, which fills its `w × h` box pointing
105
+ * right (rotate the element for another direction) and ignores `radius`. `fillAlpha` is the alpha
106
+ * of the fill alone: 0 draws only the stroke, a ring. `alpha` fades the whole shape and the
107
+ * entity's children: a disabled button at 0.6 draws its icon at 0.6 too. `dash` is the
108
+ * dash length of the stroke in reference units, with gaps of half a dash; 0 strokes a solid line.
109
+ * `clip` masks the children of the entity to the shape, always solid, which is how `ui` draws a
110
+ * scroll and an overflow. The shape itself is never masked, so its stroke is drawn whole.
111
+ *
112
+ * @example
113
+ * ```ts
114
+ * const value: ShapeValue = {
115
+ * kind: "rect", w: 320, h: 96, fill: 0x101018, fillAlpha: 1, alpha: 1, radius: 16,
116
+ * stroke: 0x000000, strokeWidth: 0, dash: 0, clip: false
117
+ * };
118
+ * // The honey ring on a selected cell: a stroke and no fill.
119
+ * const ring: ShapeValue = {
120
+ * kind: "rect", w: 140, h: 140, fill: 0xffffff, fillAlpha: 0, alpha: 1, radius: 24,
121
+ * stroke: 0xffc233, strokeWidth: 6, dash: 0, clip: false
122
+ * };
123
+ * // The play glyph of a watch button, and a dashed focus ring: 10 u dashes, 5 u gaps.
124
+ * const play: ShapeValue = { ...ring, kind: "triangle", w: 36, h: 40, fillAlpha: 1, strokeWidth: 0 };
125
+ * const focus: ShapeValue = { ...ring, w: 200, h: 80, stroke: 0x3a2212, strokeWidth: 4, dash: 10 };
126
+ * ```
127
+ */
128
+ type ShapeValue = {
129
+ kind: "rect" | "triangle";
130
+ w: number;
131
+ h: number;
132
+ fill: number;
133
+ fillAlpha: number;
134
+ alpha: number;
135
+ radius: number;
136
+ stroke: number;
137
+ strokeWidth: number;
138
+ dash: number;
139
+ clip: boolean;
140
+ };
141
+ /**
142
+ * What `sprite()` takes: the two things every visual needs, and the four that have defaults.
143
+ *
144
+ * @example
145
+ * ```ts
146
+ * const options: SpriteOptions = { texture: "board.cell", at: { x: 540, y: 300 } };
147
+ * ```
148
+ */
149
+ type SpriteOptions = {
150
+ texture: string;
151
+ at: Point$2;
152
+ tint?: number;
153
+ alpha?: number;
154
+ anchor?: Point$2;
155
+ scale?: number;
156
+ };
157
+ /**
158
+ * Where a view sits: reference units, radians, uniform scale, and the local point it turns
159
+ * around.
160
+ */
161
+ declare const Transform: ComponentType<TransformValue>;
162
+ /**
163
+ * One textured quad. `texture` is an asset key, resolved through the texture providers; a size
164
+ * and a `fit` draw it into a box.
165
+ */
166
+ declare const Sprite: ComponentType<SpriteValue>;
167
+ /**
168
+ * A stretchable panel, sized in reference units, with its own alpha and tint. `debug: true` draws
169
+ * the slice outline over it. `clip: true` masks the children of the entity to the panel box.
170
+ */
171
+ declare const NineSlice: ComponentType<{
172
+ texture: string;
173
+ width: number;
174
+ height: number;
175
+ alpha: number;
176
+ tint: number;
177
+ debug: boolean;
178
+ clip: boolean;
179
+ }>;
180
+ /**
181
+ * The entity this view moves with. `0` means no parent.
182
+ */
183
+ declare const Parent: ComponentType<{
184
+ entity: number;
185
+ }>;
186
+ /**
187
+ * A display object the game owns. Never pooled, never destroyed by `sync`.
188
+ */
189
+ declare const Display: ComponentType<DisplayValue>;
190
+ /**
191
+ * A filled rounded rectangle or a right-pointing triangle, drawn with Pixi `Graphics` and redrawn
192
+ * only when a field changed. `fillAlpha: 0` draws only the stroke, `dash` above 0 dashes it.
193
+ * `clip: true` masks the children of the entity to the shape.
194
+ */
195
+ declare const Shape: ComponentType<ShapeValue>;
196
+ /**
197
+ * Bundles the two components every visual needs, so a projection `view` reads as one line.
198
+ *
199
+ * @param options - Texture key, position, and the four values that have defaults.
200
+ * @returns The `Sprite` and the `Transform` value, in that order.
201
+ * @example
202
+ * ```ts
203
+ * sprite({ texture: "board.cell", at: { x: 540, y: 300 } });
204
+ * // [Sprite({ texture: "board.cell", tint: 0xffffff, alpha: 1, anchor: { x: 0.5, y: 0.5 },
205
+ * // width: 0, height: 0, fit: "fill" }),
206
+ * // Transform({ x: 540, y: 300, rotation: 0, scale: 1, pivot: { x: 0, y: 0 } })]
207
+ * ```
208
+ */
209
+ declare function sprite(options: SpriteOptions): [ComponentValue<SpriteValue>, ComponentValue<TransformValue>];
210
+ declare namespace types_d_exports$2 {
211
+ export { Carried, Config$2 as Config, Deps$1 as Deps, Direction, GesturePhase, InputApi, InputCtx, KernelSlice$1 as KernelSlice, KeyInput, KeyListener, Point$1 as Point, PointerListener, RawSample, State$2 as State, TapListener, Target };
8
212
  }
9
213
  /**
10
214
  * A point in reference units, as `renderer.viewport.toReference` answers it.
@@ -14,7 +218,7 @@ declare namespace types_d_exports$1 {
14
218
  * const point: Point = { x: 540, y: 960 };
15
219
  * ```
16
220
  */
17
- type Point = {
221
+ type Point$1 = {
18
222
  x: number;
19
223
  y: number;
20
224
  };
@@ -67,7 +271,7 @@ type RawSample = {
67
271
  * const phase: GesturePhase = "dragging";
68
272
  * ```
69
273
  */
70
- type GesturePhase = "idle" | "pressed" | "longPressed" | "dragging";
274
+ type GesturePhase = "idle" | "pressed" | "longPressed" | "dragging" | "tracing";
71
275
  /**
72
276
  * What `onTap` hands a listener: the view the finger let go on, inside the tap slop.
73
277
  *
@@ -127,9 +331,12 @@ type KeyListener = (key: KeyInput) => boolean | void;
127
331
  * createApp({ plugins: [...screen], pluginConfigs: { input: { heldScale: 1.08 } } });
128
332
  * // A game with its own cursors: the config merges shallowly, so both values are given.
129
333
  * createApp({ plugins: [...screen], pluginConfigs: { input: { cursor: { control: "grab", idle: "default" } } } });
334
+ * // A word grid with 40 px cells: the finger's path is hit-tested every 16 px, below the
335
+ * // shortest cell span, so a fast finger skips no letter.
336
+ * createApp({ plugins: [...screen], pluginConfigs: { input: { traceStepPx: 16 } } });
130
337
  * ```
131
338
  */
132
- type Config$1 = {
339
+ type Config$2 = {
133
340
  /** Largest move, in reference px, that still counts as a tap or a long press. */tapSlopPx: number; /** Hold time, in ms of `time`, after which a `Pressable` answers. */
134
341
  longPressMs: number; /** Move, in reference px, that turns a press on a `Draggable` into a drag. */
135
342
  dragStartPx: number; /** Shortest swipe, in reference px. */
@@ -152,11 +359,30 @@ type Config$1 = {
152
359
  control: string;
153
360
  idle: string;
154
361
  };
362
+ /**
363
+ * Reference px between two hit tests along the finger's path inside one frame of a trace. Keep
364
+ * it below the shortest cell span: a longer step jumps over a cell.
365
+ */
366
+ traceStepPx: number;
367
+ /**
368
+ * The hit circle of a trace cell: its radius is this times the short side of the cell's hit
369
+ * box, around the box centre. A diagonal then crosses no corner of a neighbour.
370
+ */
371
+ traceInset: number;
372
+ };
373
+ /**
374
+ * One view a drag carries on top of the held one: hung under the held view from the grab to the
375
+ * release.
376
+ */
377
+ type Carried = {
378
+ /** The carried view. */entity: Entity; /** The `Parent` it had at the grab, hung back under it on the release. */
379
+ parent: Entity | undefined; /** The remover `world.projection.mute` returned for its root pose. */
380
+ unmute: () => void;
155
381
  };
156
382
  /**
157
383
  * input plugin state: the raw queue the DOM handlers write, and the one gesture in progress.
158
384
  */
159
- type State$1 = {
385
+ type State$2 = {
160
386
  /** Written by the DOM handlers, drained by the frame step. */samples: RawSample[];
161
387
  phase: GesturePhase; /** The one active pointer. A sample of any other pointer is dropped while this is set. */
162
388
  pointerId: number | undefined; /** The pressed or held entity. */
@@ -165,14 +391,24 @@ type State$1 = {
165
391
  projection: string;
166
392
  key: string;
167
393
  } | undefined; /** Reference px at pointer down. */
168
- start: Point; /** `Transform` minus the pointer at the moment of the grab. */
169
- grabOffset: Point; /** Sum of `time.delta` since pointer down. No device clock is ever read. */
394
+ start: Point$1; /** `Transform` minus the pointer at the moment of the grab. */
395
+ grabOffset: Point$1; /** Sum of `time.delta` since pointer down. No device clock is ever read. */
170
396
  pressedMs: number; /** The drop target that carries `Hovered` during a drag. */
171
397
  hovered: Entity | undefined; /** The view that carries `PointerOver`: a press would take it, and a mouse or a pen is over it. */
172
398
  pointerOver: Entity | undefined; /** The `Parent` the held view had at the grab. It is hung back under it on the release. */
173
399
  parent: Entity | undefined; /** The rest scale of the held view in root space: the base of `heldScale`, set back on drop. */
174
- restScale: number | undefined; /** The remover `world.projection.mute` returned, while a drag runs. */
175
- unmute: (() => void) | undefined;
400
+ restScale: number | undefined;
401
+ /**
402
+ * The root pose of the held view, written at the grab and at every move. A carried view is
403
+ * placed from it when the held view despawns before the release and takes its `Transform`.
404
+ */
405
+ heldPose: TransformValue | undefined; /** The remover `world.projection.mute` returned, while a drag runs. */
406
+ unmute: (() => void) | undefined; /** The views a drag carries on top of the held one, in `carry` order; empty outside a drag. */
407
+ carried: Carried[]; /** The cells of the trace in progress, in path order; empty outside a trace. */
408
+ path: Entity[]; /** The intent of the first cell of the trace; a cell of another intent closes the path. */
409
+ traceIntent: string | undefined; /** True once a cell of another intent was crossed: the path neither grows nor pops. */
410
+ traceClosed: boolean; /** Where the finger was at the last frame of the trace: the segment starts there. */
411
+ lastPoint: Point$1 | undefined;
176
412
  canvas: HTMLCanvasElement | undefined;
177
413
  offFrame: (() => void) | undefined;
178
414
  detach: (() => void) | undefined; /** Registered through `onTap`, called in this order on every tap. */
@@ -252,6 +488,26 @@ type InputApi = {
252
488
  * ```
253
489
  */
254
490
  swipe(target: Target, direction: Direction): boolean;
491
+ /**
492
+ * Reads the `Traceable` of every cell and answers one path, the way a finger drawn through the
493
+ * cells would: the intent of the cells and `{ path }`, the cell payloads in order. Every cell
494
+ * must name the same intent. An empty list, a cell with no `Traceable` and two intents warn and
495
+ * answer `false` without calling the gate. Nothing is tagged `Traced`.
496
+ *
497
+ * @param path - The cells in the order the finger goes through them.
498
+ * @returns What `flow.gate.answer` returned: true when the gate took the answer.
499
+ * @example
500
+ * ```ts
501
+ * // A word-game test spells a word without a finger.
502
+ * app.input.trace([
503
+ * { projection: "board.cells", key: "b3" },
504
+ * { projection: "board.cells", key: "c3" },
505
+ * { projection: "board.cells", key: "d3" }
506
+ * ]);
507
+ * // true: answers { intent: "word", payload: { path: [{ cell: "b3" }, { cell: "c3" }, { cell: "d3" }] } }
508
+ * ```
509
+ */
510
+ trace(path: readonly Target[]): boolean;
255
511
  /**
256
512
  * Registers a listener called on every tap — a press and a release inside `tapSlopPx` — on the
257
513
  * topmost view the hit test accepted, before the `Tappable` answer. A view that carries only
@@ -375,7 +631,7 @@ type InputApi = {
375
631
  /**
376
632
  * Resolved dependency APIs.
377
633
  */
378
- type Deps = {
634
+ type Deps$1 = {
379
635
  time: Api;
380
636
  flow: Api$1;
381
637
  world: Api$2;
@@ -386,7 +642,7 @@ type Deps = {
386
642
  *
387
643
  * `input` owns no event, so `emit` is the kernel's and never called here.
388
644
  */
389
- type KernelSlice = PluginCtx<Config$1, State$1> & {
645
+ type KernelSlice$1 = PluginCtx<Config$2, State$2> & {
390
646
  readonly global: object;
391
647
  readonly log: Log.LogApi;
392
648
  readonly require: Require;
@@ -394,11 +650,11 @@ type KernelSlice = PluginCtx<Config$1, State$1> & {
394
650
  /**
395
651
  * Domain context of the input plugin: the kernel slice plus the four resolved dependencies.
396
652
  */
397
- type InputCtx = KernelSlice & {
398
- readonly deps: Deps;
653
+ type InputCtx = KernelSlice$1 & {
654
+ readonly deps: Deps$1;
399
655
  };
400
- declare namespace types_d_exports {
401
- export { Argument, CompiledMessage, CompiledMessages, Config, ElementNode, Events, I18nApi, I18nCtx, I18nKit, IntlKit, KeysWithoutParameters, Message, Part, RegisteredModule, ResolvedModule, State, StringTable, StringsLoader, TypedTr };
656
+ declare namespace types_d_exports$1 {
657
+ export { Argument, CompiledMessage, CompiledMessages, Config$1 as Config, DurationStyle, ElementNode, Events, I18nApi, I18nCtx, I18nKit, IntlKit, KeysWithoutParameters, Message, Part, RegisteredModule, ResolvedModule, State$1 as State, StringTable, StringsLoader, TypedTr };
402
658
  }
403
659
  /**
404
660
  * One interface element handed to a message as a parameter. Structural on purpose: it mirrors
@@ -475,7 +731,31 @@ type IntlKit = {
475
731
  number(options?: Intl.NumberFormatOptions): Intl.NumberFormat; /** The list format of the locale, which ICU MessageFormat 1 has no syntax for. */
476
732
  list(options?: Intl.ListFormatOptions): Intl.ListFormat; /** The date and time format of the locale. */
477
733
  date(options?: Intl.DateTimeFormatOptions): Intl.DateTimeFormat;
734
+ /**
735
+ * The duration format of the locale. A compiled `{left, duration}` reads it with
736
+ * `secondsDisplay: "always"`, so a countdown at zero still says "0 sec".
737
+ *
738
+ * @param options - The `Intl.DurationFormat` options.
739
+ * @returns The memoised formatter for these options.
740
+ * @throws {Error} When the runtime has no `Intl.DurationFormat`.
741
+ * @example
742
+ * ```ts
743
+ * const intl = createIntlKit("en");
744
+ * intl.duration({ style: "short" }).format({ minutes: 1, seconds: 35 }); // "1 min, 35 sec"
745
+ * ```
746
+ */
747
+ duration(options?: Intl.DurationFormatOptions): Intl.DurationFormat;
478
748
  };
749
+ /**
750
+ * How a duration reads: `"long"` (1 minute, 35 seconds), `"short"` (1 min, 35 sec), `"narrow"`
751
+ * (1m 35s) or `"digital"` (0:01:35). A string file names one in `{left, duration, short}`.
752
+ *
753
+ * @example
754
+ * ```ts
755
+ * const style: DurationStyle = "digital"; // app.i18n.duration(95_000, style) is "0:01:35"
756
+ * ```
757
+ */
758
+ type DurationStyle = "long" | "short" | "narrow" | "digital";
479
759
  /**
480
760
  * One message as the build wrote it: a plain function of the parameters and the locale's kit.
481
761
  * There is no ICU parser at run time.
@@ -515,7 +795,7 @@ type StringsLoader = () => Promise<CompiledMessages | {
515
795
  * createApp({ pluginConfigs: { i18n: { locale: "ru", fallback: "en", locales: {} } } });
516
796
  * ```
517
797
  */
518
- type Config = {
798
+ type Config$1 = {
519
799
  /** The locale at start. */locale: string; /** The locale a missing key is read from before it is reported missing. */
520
800
  fallback: string;
521
801
  /**
@@ -542,7 +822,7 @@ type ResolvedModule = {
542
822
  /**
543
823
  * i18n plugin state.
544
824
  */
545
- type State = {
825
+ type State$1 = {
546
826
  /** The current locale; `config.locale` until `setLocale`. */locale: string; /** Per locale, one entry per feature and one for the config, filled in `onStart`. */
547
827
  registered: Map<string, RegisteredModule[]>; /** Merged and resolved modules per locale. */
548
828
  loaded: Map<string, CompiledMessages>; /** One kit per locale, built on first use. */
@@ -650,6 +930,25 @@ type I18nApi = {
650
930
  * ```
651
931
  */
652
932
  has(key: string): boolean;
933
+ /**
934
+ * Formats milliseconds in the current locale through `Intl.DurationFormat`. The value is rounded
935
+ * up to whole seconds, so a countdown never reads zero while time is left; a negative or
936
+ * non-finite value reads as zero; the seconds are always shown. `text` formats a field bound
937
+ * with `format: "duration"` through this call.
938
+ *
939
+ * @param ms - The duration in milliseconds.
940
+ * @param style - How the duration reads. Default `"short"`.
941
+ * @returns The duration as text.
942
+ * @throws {Error} When the runtime has no `Intl.DurationFormat`.
943
+ * @example
944
+ * ```ts
945
+ * // The out-of-energy popup shows when the next refill comes, locale "en".
946
+ * app.i18n.duration(95_000); // "1 min, 35 sec"
947
+ * app.i18n.duration(95_000, "digital"); // "0:01:35"
948
+ * app.i18n.duration(500); // "1 sec": rounded up
949
+ * ```
950
+ */
951
+ duration(ms: number, style?: DurationStyle): string;
653
952
  /**
654
953
  * Every locale at least one feature or the config brought a module for, sorted.
655
954
  *
@@ -669,264 +968,543 @@ type I18nApi = {
669
968
  * row 8), so the own event reaches the context the kernel hands the factories and `emit` is the
670
969
  * kernel's own, i18n-typed one. No member of the context is cast.
671
970
  */
672
- type I18nCtx = PluginCtx<Config, State, Events> & {
971
+ type I18nCtx = PluginCtx<Config$1, State$1, Events> & {
673
972
  readonly global: object;
674
973
  readonly log: Log.LogApi;
675
974
  readonly require: Require;
676
975
  };
677
976
  /**
678
- * The key-to-parameters table a game generates: `generated/strings.ts` exports it as `Strings`.
679
- * A key with no parameters carries `Record<never, never>`.
977
+ * The key-to-parameters table a game generates: `generated/strings.ts` exports it as `Strings`.
978
+ * A key with no parameters carries `Record<never, never>`.
979
+ *
980
+ * @example
981
+ * ```ts
982
+ * const table: StringTable = { "hud.orders": { n: 3 }, "orders.complete": {} };
983
+ * ```
984
+ */
985
+ type StringTable = Record<string, unknown>;
986
+ /**
987
+ * The keys of a table whose message takes no parameters, so `tr` may be called with the key
988
+ * alone.
989
+ *
990
+ * @example
991
+ * ```ts
992
+ * type Keys = KeysWithoutParameters<{ "home.play": Record<never, never>; "hud.orders": { n: number } }>;
993
+ * // "home.play"
994
+ * ```
995
+ */
996
+ type KeysWithoutParameters<Table extends StringTable> = { [Key in keyof Table]: Record<never, never> extends Table[Key] ? Key : never }[keyof Table] & string;
997
+ /**
998
+ * `tr` as one game reads it: the key is checked against the generated table, and the parameters
999
+ * are the ones that key declares. A key with no parameters takes no second argument.
1000
+ *
1001
+ * @example
1002
+ * ```ts
1003
+ * const { tr } = i18nFor<{ "hud.orders": { n: number } }>();
1004
+ * tr("hud.orders", { n: 3 }); // { key: "hud.orders", params: { n: 3 } }
1005
+ * ```
1006
+ */
1007
+ type TypedTr<Table extends StringTable> = {
1008
+ <Key extends KeysWithoutParameters<Table>>(key: Key): Message<Key, Table[Key]>;
1009
+ <Key extends keyof Table & string>(key: Key, params: Table[Key]): Message<Key, Table[Key]>;
1010
+ };
1011
+ /**
1012
+ * What `defineGame` spreads from this plugin.
1013
+ *
1014
+ * @example
1015
+ * ```ts
1016
+ * const kit: I18nKit<{ "home.play": Record<never, never> }> = i18nFor();
1017
+ * kit.tr("home.play"); // { key: "home.play" }
1018
+ * ```
1019
+ */
1020
+ type I18nKit<Table extends StringTable> = {
1021
+ tr: TypedTr<Table>;
1022
+ };
1023
+ declare namespace types_d_exports {
1024
+ export { AdvanceTable, BindField, BindOptions, BundleLoaded, BundleUnloaded, Config, CountdownValue, Deps, DrawnLabel, IconRun, KernelSlice, LayoutOptions, Line, LocaleChanged, Point, Run, SeenText, Size, State, TextAlign, TextApi, TextBind, TextCtx, TextFormat, TextLayout, TextRun, TextShadow, TextStyle, TextStyleInput, TextStyles, TextValue, TextWrap, Warn };
1025
+ }
1026
+ /**
1027
+ * A point in reference units: where a label sits, and which point of the block sits there.
1028
+ *
1029
+ * @example
1030
+ * ```ts
1031
+ * const anchor: Point = { x: 0.5, y: 0.5 };
1032
+ * ```
1033
+ */
1034
+ type Point = {
1035
+ x: number;
1036
+ y: number;
1037
+ };
1038
+ /**
1039
+ * The size of a measured block, in reference pixels.
1040
+ *
1041
+ * @example
1042
+ * ```ts
1043
+ * const size: Size = { width: 57.6, height: 38.4 };
1044
+ * ```
1045
+ */
1046
+ type Size = {
1047
+ width: number;
1048
+ height: number;
1049
+ };
1050
+ /**
1051
+ * How a bound number is shown. `"int"` rounds it; the three time formats take milliseconds and
1052
+ * show whole seconds, rounded up, so a timer never reads zero while time is left.
1053
+ *
1054
+ * @example
1055
+ * ```ts
1056
+ * const format: TextFormat = "mm:ss"; // 95_000 ms shows "01:35"
1057
+ * ```
1058
+ */
1059
+ type TextFormat = "int" | "mm:ss" | "h:mm:ss" | "duration";
1060
+ /** The brand only `bind()` puts on a `TextBind`. Type-only: nothing is emitted for it. */
1061
+ declare const textBind: unique symbol;
1062
+ /**
1063
+ * Which numeric component field a label shows, and how. Plain data in the component, so a
1064
+ * snapshot lists it; nominal, so only `bind()` makes one and a hand-written literal does not
1065
+ * compile.
1066
+ *
1067
+ * @example
1068
+ * ```ts
1069
+ * const Counter = component("Counter", { value: 0 });
1070
+ * const coins: TextBind = bind(Counter, "value");
1071
+ * // { component: "Counter", field: "value", format: "int" }
1072
+ * ```
1073
+ */
1074
+ type TextBind = {
1075
+ readonly component: string;
1076
+ readonly field: string;
1077
+ readonly format: TextFormat;
1078
+ } & {
1079
+ readonly [textBind]: true;
1080
+ };
1081
+ /**
1082
+ * The numeric fields of a component value, by name: what `bind()` takes as its field.
1083
+ *
1084
+ * @example
1085
+ * ```ts
1086
+ * type Fields = BindField<{ value: number; name: string }>; // "value"
1087
+ * ```
1088
+ */
1089
+ type BindField<Value extends object> = keyof NumericFields<Value> & string;
1090
+ /**
1091
+ * What `bind()` takes after the field. The format is `"int"` when left out.
1092
+ *
1093
+ * @example
1094
+ * ```ts
1095
+ * const options: BindOptions = { format: "mm:ss" };
1096
+ * ```
1097
+ */
1098
+ type BindOptions = {
1099
+ format?: TextFormat;
1100
+ };
1101
+ /**
1102
+ * A moment to count down to, and how much is left. A game writes `until`, a moment of `clock` in
1103
+ * epoch milliseconds; `left` is engine-owned and follows what the label shows.
1104
+ *
1105
+ * @example
1106
+ * ```ts
1107
+ * // clock.now() is 1_790_000_000_000: the chest opens in 95 s.
1108
+ * const chest: CountdownValue = { until: 1_790_000_095_000, left: 95_000 };
1109
+ * ```
1110
+ */
1111
+ type CountdownValue = {
1112
+ until: number;
1113
+ left: number;
1114
+ };
1115
+ /**
1116
+ * What the `Text` component holds. `resolved` is engine-owned: a game writes `content`, `style`,
1117
+ * `bind`, `anchor` and `alpha`, and reads `resolved`. `alpha` fades every run, shadow and icon of
1118
+ * the block together, 1 by default; a change is written in place and a tween can drive it.
1119
+ *
1120
+ * @example
1121
+ * ```ts
1122
+ * const value: TextValue = {
1123
+ * content: "+5", style: "body", bind: undefined, anchor: { x: 0.5, y: 0.5 }, alpha: 1,
1124
+ * resolved: "+5"
1125
+ * };
1126
+ * // The hint of an empty name field, at half alpha.
1127
+ * const hint: TextValue = { ...value, content: "Your name", alpha: 0.5, resolved: "Your name" };
1128
+ * ```
1129
+ */
1130
+ type TextValue = {
1131
+ content: string | Message;
1132
+ style: string;
1133
+ bind: TextBind | undefined;
1134
+ anchor: Point;
1135
+ alpha: number;
1136
+ resolved: string;
1137
+ };
1138
+ /**
1139
+ * Where the lines of a block sit inside the widest of them.
1140
+ *
1141
+ * @example
1142
+ * ```ts
1143
+ * const align: TextAlign = "center";
1144
+ * ```
1145
+ */
1146
+ type TextAlign = "left" | "center" | "right";
1147
+ /**
1148
+ * The wrap width of a style in reference pixels, or `"none"` for one line per `\n` segment.
1149
+ *
1150
+ * @example
1151
+ * ```ts
1152
+ * const wrap: TextWrap = 480;
1153
+ * ```
1154
+ */
1155
+ type TextWrap = number | "none";
1156
+ /**
1157
+ * The drop shadow of a style as the plugin stores it: a copy of every glyph run in `color`, at
1158
+ * `alpha`, moved by `dx` and `dy` reference pixels.
1159
+ *
1160
+ * @example
1161
+ * ```ts
1162
+ * const shadow: TextShadow = { color: 0x5b3a1e, dx: 0, dy: 4, alpha: 1 };
1163
+ * ```
1164
+ */
1165
+ type TextShadow = {
1166
+ color: number;
1167
+ dx: number;
1168
+ dy: number;
1169
+ alpha: number;
1170
+ };
1171
+ /**
1172
+ * One style as a game writes it: the font and the size are required, the rest has defaults.
1173
+ *
1174
+ * @example
1175
+ * ```ts
1176
+ * // A popup title: cream display glyphs over a brown shadow 4 px down.
1177
+ * const title: TextStyleInput = {
1178
+ * font: "ui.font-display", size: 56, fill: 0xfff3d6, shadow: { color: 0x5b3a1e, dx: 0, dy: 4 }
1179
+ * };
1180
+ *
1181
+ * defineTextStyles({ "popup.title": title }).map["popup.title"]?.shadow;
1182
+ * // { color: 0x5b3a1e, dx: 0, dy: 4, alpha: 1 }
1183
+ * ```
1184
+ */
1185
+ type TextStyleInput = {
1186
+ font: string;
1187
+ bold?: string;
1188
+ italic?: string;
1189
+ size: number;
1190
+ fill: number; /** The outline colour. Drawn only when `strokeWidth` is above 0. */
1191
+ stroke?: number;
1192
+ /**
1193
+ * The outline width in reference px: 8 copies of each glyph run on a circle this wide (12 from 6
1194
+ * on), tinted `stroke`. 0 draws none. It never changes what `measure` answers.
1195
+ */
1196
+ strokeWidth?: number;
1197
+ letterSpacing?: number;
1198
+ align?: TextAlign;
1199
+ wrap?: TextWrap;
1200
+ digits?: boolean;
1201
+ /**
1202
+ * A drop shadow under every glyph run, in reference px at the style size. `alpha` is 1 when
1203
+ * left out. It never changes what `measure` answers.
1204
+ */
1205
+ shadow?: {
1206
+ color: number;
1207
+ dx: number;
1208
+ dy: number;
1209
+ alpha?: number;
1210
+ };
1211
+ };
1212
+ /**
1213
+ * One style as the plugin stores it: every field filled, `bold` and `italic` only when the game
1214
+ * shipped those MSDF fonts, `shadow` only when the style has one.
680
1215
  *
681
1216
  * @example
682
1217
  * ```ts
683
- * const table: StringTable = { "hud.orders": { n: 3 }, "orders.complete": {} };
1218
+ * const style: TextStyle = {
1219
+ * font: "ui.font-body", bold: undefined, italic: undefined, size: 32, fill: 0xffffff,
1220
+ * stroke: 0x000000, strokeWidth: 0, letterSpacing: 0, align: "left", wrap: "none",
1221
+ * digits: false, shadow: undefined
1222
+ * };
684
1223
  * ```
685
1224
  */
686
- type StringTable = Record<string, unknown>;
1225
+ type TextStyle = {
1226
+ font: string;
1227
+ bold: string | undefined;
1228
+ italic: string | undefined;
1229
+ size: number;
1230
+ fill: number;
1231
+ stroke: number;
1232
+ strokeWidth: number;
1233
+ letterSpacing: number;
1234
+ align: TextAlign;
1235
+ wrap: TextWrap;
1236
+ digits: boolean;
1237
+ shadow: TextShadow | undefined;
1238
+ };
687
1239
  /**
688
- * The keys of a table whose message takes no parameters, so `tr` may be called with the key
689
- * alone.
1240
+ * What `defineTextStyles` returns and a feature registers under its `textStyles` key. Structural
1241
+ * on purpose: `flow` carries the map, `text` reads it.
690
1242
  *
691
1243
  * @example
692
1244
  * ```ts
693
- * type Keys = KeysWithoutParameters<{ "home.play": Record<never, never>; "hud.orders": { n: number } }>;
694
- * // "home.play"
1245
+ * const styles: TextStyles = { kind: "textStyles", map: { "hud.title": bodyStyle } };
695
1246
  * ```
696
1247
  */
697
- type KeysWithoutParameters<Table extends StringTable> = { [Key in keyof Table]: Record<never, never> extends Table[Key] ? Key : never }[keyof Table] & string;
1248
+ type TextStyles = {
1249
+ readonly kind: "textStyles";
1250
+ readonly map: Record<string, TextStyle>;
1251
+ };
698
1252
  /**
699
- * `tr` as one game reads it: the key is checked against the generated table, and the parameters
700
- * are the ones that key declares. A key with no parameters takes no second argument.
1253
+ * The advances of one font at the size the `.fnt` was exported with. Widths scale by
1254
+ * `style.size / size`; kerning pairs are ignored.
701
1255
  *
702
1256
  * @example
703
1257
  * ```ts
704
- * const { tr } = i18nFor<{ "hud.orders": { n: number } }>();
705
- * tr("hud.orders", { n: 3 }); // { key: "hud.orders", params: { n: 3 } }
1258
+ * const table: AdvanceTable = { size: 32, lineHeight: 40, advances: new Map([["1", 18]]) };
706
1259
  * ```
707
1260
  */
708
- type TypedTr<Table extends StringTable> = {
709
- <Key extends KeysWithoutParameters<Table>>(key: Key): Message<Key, Table[Key]>;
710
- <Key extends keyof Table & string>(key: Key, params: Table[Key]): Message<Key, Table[Key]>;
1261
+ type AdvanceTable = {
1262
+ size: number;
1263
+ lineHeight: number;
1264
+ advances: Map<string, number>;
711
1265
  };
712
1266
  /**
713
- * What `defineGame` spreads from this plugin.
1267
+ * One run of glyphs: the text and the flags every glyph in it shares.
714
1268
  *
715
1269
  * @example
716
1270
  * ```ts
717
- * const kit: I18nKit<{ "home.play": Record<never, never> }> = i18nFor();
718
- * kit.tr("home.play"); // { key: "home.play" }
1271
+ * const run: TextRun = { kind: "text", text: "+5", bold: true, italic: false, color: 0xffe082 };
719
1272
  * ```
720
1273
  */
721
- type I18nKit<Table extends StringTable> = {
722
- tr: TypedTr<Table>;
1274
+ type TextRun = {
1275
+ kind: "text";
1276
+ text: string;
1277
+ bold: boolean;
1278
+ italic: boolean;
1279
+ color: number | undefined;
723
1280
  };
724
- //#endregion
725
- //#region src/plugins/renderer/components.d.ts
726
1281
  /**
727
- * Where a view sits, in reference units and radians. Relative to the `Parent` when there is one.
728
- * `pivot` is the local point the view turns and scales around, and `x`, `y` is where that point
729
- * lands; the default pivot `{ x: 0, y: 0 }` is the view's own origin.
1282
+ * One inline icon: an asset key drawn square at the line height.
730
1283
  *
731
1284
  * @example
732
1285
  * ```ts
733
- * // A 200 x 80 button that grows around its centre.
734
- * const value: TransformValue = {
735
- * x: 640, y: 340, rotation: 0, scale: 1.1, pivot: { x: 100, y: 40 }
736
- * };
1286
+ * const run: IconRun = { kind: "icon", key: "hud.coin" };
737
1287
  * ```
738
1288
  */
739
- type TransformValue = {
740
- x: number;
741
- y: number;
742
- rotation: number;
743
- scale: number;
744
- pivot: Point$1;
1289
+ type IconRun = {
1290
+ kind: "icon";
1291
+ key: string;
745
1292
  };
746
1293
  /**
747
- * How a sized sprite fills its box: `"fill"` stretches, `"contain"` scales uniformly inside the
748
- * box and centres, `"cover"` scales uniformly over the box and crops the overflow.
1294
+ * A piece of a line: glyphs or one inline icon.
749
1295
  *
750
1296
  * @example
751
1297
  * ```ts
752
- * const fit: SpriteFit = "cover";
1298
+ * const runs: Run[] = [{ kind: "icon", key: "hud.coin" }];
753
1299
  * ```
754
1300
  */
755
- type SpriteFit = "fill" | "contain" | "cover";
1301
+ type Run = TextRun | IconRun;
756
1302
  /**
757
- * One textured quad. `texture` is an asset key; `defineGame` narrows it to the game's keys.
758
- * `width` and `height` are the box in reference units; 0 on an axis keeps the texture's own size
759
- * there. `anchor` is the point of the box that sits on the `Transform`. `alpha` fades the quad
760
- * and the entity's children.
1303
+ * One laid-out line and how wide it is.
761
1304
  *
762
1305
  * @example
763
1306
  * ```ts
764
- * const value: SpriteValue = {
765
- * texture: "board.bg-forest-meadow", tint: 0xffffff, alpha: 1, anchor: { x: 0, y: 0 },
766
- * width: 1080, height: 1920, fit: "cover"
767
- * };
1307
+ * const line: Line = { runs: [{ kind: "icon", key: "hud.coin" }], width: 38.4 };
768
1308
  * ```
769
1309
  */
770
- type SpriteValue = {
771
- texture: string;
772
- tint: number;
773
- alpha: number;
774
- anchor: Point$1;
1310
+ type Line = {
1311
+ runs: Run[];
775
1312
  width: number;
776
- height: number;
777
- fit: SpriteFit;
778
1313
  };
779
1314
  /**
780
- * A stretchable panel. The slice borders come with the texture, not with the component. `debug`
781
- * strokes the bounds and the four cut lines over the panel: cyan, red when the corners overlap or
782
- * the texture is missing. `clip` masks the children of the entity to the `width × height` box,
783
- * the same rule as `Shape.clip`: the mask is a filled rectangle and is never drawn, and the panel
784
- * itself is never masked. `alpha` fades the panel and the entity's children.
1315
+ * A laid-out block: its lines and the size `measure` answers with.
785
1316
  *
786
1317
  * @example
787
1318
  * ```ts
788
- * const value: NineSliceValue = {
789
- * texture: "ui.panel", width: 600, height: 320, alpha: 1, tint: 0xffffff, debug: false,
790
- * clip: false
791
- * };
792
- * // The board tray, outlined while its insets are checked.
793
- * const tray: NineSliceValue = {
794
- * ...value, texture: "board.board-tray", width: 1000, height: 1040, debug: true
795
- * };
796
- * // A text field: a long value never draws outside the frame.
797
- * const field: NineSliceValue = {
798
- * ...value, texture: "ui.field", width: 480, height: 96, clip: true
799
- * };
1319
+ * const layout: TextLayout = { lines: [], width: 0, height: 0 };
800
1320
  * ```
801
1321
  */
802
- type NineSliceValue = {
803
- texture: string;
1322
+ type TextLayout = {
1323
+ lines: Line[];
804
1324
  width: number;
805
1325
  height: number;
806
- alpha: number;
807
- tint: number;
808
- debug: boolean;
809
- clip: boolean;
810
1326
  };
811
1327
  /**
812
- * The escape hatch: the game owns a Pixi object and `sync` only attaches and detaches it.
1328
+ * What one label container was last filled from: the style object and the laid-out block. An
1329
+ * update with the same style object, the same anchor and the same lines and runs writes the new
1330
+ * text and positions into the objects already there.
1331
+ */
1332
+ type DrawnLabel = {
1333
+ style: TextStyle;
1334
+ layout: TextLayout;
1335
+ };
1336
+ /**
1337
+ * A dev warning that is written once per key. Pure modules take it as an argument, so nothing
1338
+ * below the plugin context reaches the log itself.
813
1339
  *
814
1340
  * @example
815
1341
  * ```ts
816
- * const value: DisplayValue = { object: spineAnimation };
1342
+ * const warn: Warn = (key, message) => seen.has(key) || log.warn(message);
817
1343
  * ```
818
1344
  */
819
- type DisplayValue = {
820
- object: unknown;
1345
+ type Warn = (key: string, message: string, data?: Record<string, unknown>) => void;
1346
+ /**
1347
+ * What the pure layout needs next to the runs, the style and the tables.
1348
+ *
1349
+ * @example
1350
+ * ```ts
1351
+ * const options: LayoutOptions = { warn: () => undefined };
1352
+ * ```
1353
+ */
1354
+ type LayoutOptions = {
1355
+ warn: Warn;
821
1356
  };
822
1357
  /**
823
- * A filled rounded rectangle or a triangle, anchored at the top left of the `Transform`. `kind`
824
- * picks the outline: `"rect"` (the default) or `"triangle"`, which fills its `w × h` box pointing
825
- * right (rotate the element for another direction) and ignores `radius`. `fillAlpha` is the alpha
826
- * of the fill alone: 0 draws only the stroke, a ring. `alpha` fades the whole shape and the
827
- * entity's children: a disabled button at 0.6 draws its icon at 0.6 too. `dash` is the
828
- * dash length of the stroke in reference units, with gaps of half a dash; 0 strokes a solid line.
829
- * `clip` masks the children of the entity to the shape, always solid, which is how `ui` draws a
830
- * scroll and an overflow. The shape itself is never masked, so its stroke is drawn whole.
1358
+ * What was resolved for one entity last, so the frame step knows whether anything moved. `unit`
1359
+ * is what the shown string of a bound label was built from: the rounded value for `"int"`, whole
1360
+ * seconds for a time format.
831
1361
  *
832
1362
  * @example
833
1363
  * ```ts
834
- * const value: ShapeValue = {
835
- * kind: "rect", w: 320, h: 96, fill: 0x101018, fillAlpha: 1, alpha: 1, radius: 16,
836
- * stroke: 0x000000, strokeWidth: 0, dash: 0, clip: false
837
- * };
838
- * // The honey ring on a selected cell: a stroke and no fill.
839
- * const ring: ShapeValue = {
840
- * kind: "rect", w: 140, h: 140, fill: 0xffffff, fillAlpha: 0, alpha: 1, radius: 24,
841
- * stroke: 0xffc233, strokeWidth: 6, dash: 0, clip: false
1364
+ * const seen: SeenText = {
1365
+ * content: "", style: "digits", bind: "Countdown.left:mm:ss", locale: "ru", unit: 95
842
1366
  * };
843
- * // The play glyph of a watch button, and a dashed focus ring: 10 u dashes, 5 u gaps.
844
- * const play: ShapeValue = { ...ring, kind: "triangle", w: 36, h: 40, fillAlpha: 1, strokeWidth: 0 };
845
- * const focus: ShapeValue = { ...ring, w: 200, h: 80, stroke: 0x3a2212, strokeWidth: 4, dash: 10 };
846
1367
  * ```
847
1368
  */
848
- type ShapeValue = {
849
- kind: "rect" | "triangle";
850
- w: number;
851
- h: number;
852
- fill: number;
853
- fillAlpha: number;
854
- alpha: number;
855
- radius: number;
856
- stroke: number;
857
- strokeWidth: number;
858
- dash: number;
859
- clip: boolean;
1369
+ type SeenText = {
1370
+ content: unknown;
1371
+ style: string;
1372
+ bind: string | undefined;
1373
+ locale: string;
1374
+ unit: number | undefined;
860
1375
  };
861
1376
  /**
862
- * What `sprite()` takes: the two things every visual needs, and the four that have defaults.
1377
+ * text plugin config.
863
1378
  *
864
1379
  * @example
865
1380
  * ```ts
866
- * const options: SpriteOptions = { texture: "board.cell", at: { x: 540, y: 300 } };
1381
+ * createApp({ pluginConfigs: { text: { fonts: { body: "ui.body", digits: "ui.digits" } } } });
867
1382
  * ```
868
1383
  */
869
- type SpriteOptions = {
870
- texture: string;
871
- at: Point$1;
872
- tint?: number;
873
- alpha?: number;
874
- anchor?: Point$1;
875
- scale?: number;
1384
+ type Config = {
1385
+ /** The two fonts every game ships in its boot bundle. They back the built-in styles. */fonts: {
1386
+ body: string;
1387
+ digits: string;
1388
+ };
876
1389
  };
877
1390
  /**
878
- * Where a view sits: reference units, radians, uniform scale, and the local point it turns
879
- * around.
1391
+ * text plugin state.
880
1392
  */
881
- declare const Transform: ComponentType<TransformValue>;
1393
+ type State = {
1394
+ /** Built-in styles first, then the styles of every feature in feature order. */styles: Map<string, TextStyle>; /** Style name to the feature that brought it, for the duplicate error. */
1395
+ styleOwner: Map<string, string>; /** The config fonts plus every `font`, `bold` and `italic` of every style. */
1396
+ fontKeys: Set<string>; /** The parsed advance table per font key, filled when `assets` has the font. */
1397
+ tables: Map<string, AdvanceTable>; /** The font keys handed to `renderer.sync.fonts.install`. */
1398
+ installed: Set<string>; /** What was resolved for an entity last. */
1399
+ seen: Map<Entity, SeenText>; /** `bind.component` to its world type; `undefined` after the one warning. */
1400
+ bindTypes: Map<string, AnyComponent | undefined>; /** The measured size of every live label, for `ui`. */
1401
+ measured: Map<Entity, Size>; /** Layout per `style + resolved`, oldest dropped first. */
1402
+ cache: Map<string, TextLayout>; /** What every live label container was last filled from, by container. */
1403
+ drawn: WeakMap<object, DrawnLabel>; /** Entities to re-resolve in the next layout phase. */
1404
+ dirty: Set<Entity>; /** Warning keys already written, so a style, a tag, a glyph or a font warns once. */
1405
+ warned: Set<string>; /** The system, the two world hooks and the display adapter. */
1406
+ removers: Array<() => void>;
1407
+ };
882
1408
  /**
883
- * One textured quad. `texture` is an asset key, resolved through the texture providers; a size
884
- * and a `fit` draw it into a box.
1409
+ * text plugin API, `app.text`. Two questions: how big a piece of text is, and which styles the
1410
+ * game registered. Everything else happens in the frame.
1411
+ *
1412
+ * @example
1413
+ * ```ts
1414
+ * app.text.measure("120", "digits"); // { width: 57.6, height: 38.4 }
1415
+ * app.text.styles(); // ["body", "digits"]
1416
+ * ```
885
1417
  */
886
- declare const Sprite: ComponentType<SpriteValue>;
1418
+ type TextApi = {
1419
+ /**
1420
+ * The size of a piece of text in reference pixels, from the advance table of the style's font.
1421
+ * A `Message` is formatted in the current locale first. Never touches a canvas, so the answer
1422
+ * is the same in a browser and in plain Bun. Cached per style and resolved string.
1423
+ *
1424
+ * @param content - A plain string, or what `tr` returned.
1425
+ * @param style - A registered style name. An unknown one warns once and measures as `body`.
1426
+ * @returns The width and height of the block.
1427
+ * @example
1428
+ * ```ts
1429
+ * // `ui.layout` asks once per invalidation, from the Yoga measure function of a text element.
1430
+ * const text = ctx.require(textPlugin);
1431
+ *
1432
+ * text.measure("120", "digits"); // { width: 57.6, height: 38.4 } on the 0.6 em fallback
1433
+ * ```
1434
+ */
1435
+ measure(content: string | Message, style: string): Size;
1436
+ /**
1437
+ * Whether the font of a style has a glyph for one character, the lookup Pixi draws by. A
1438
+ * character its font lacks is drawn as nothing and `measure` counts it 0 wide. While the font is
1439
+ * not loaded every character counts, as the 0.6 em fallback measures it. An unknown style warns
1440
+ * once and looks up `body`.
1441
+ *
1442
+ * @param char - One character, a whole code point.
1443
+ * @param style - A registered style name.
1444
+ * @returns False only for a character a loaded font has no glyph for.
1445
+ * @example
1446
+ * ```ts
1447
+ * // `ui` measures the caret of a text field over the characters the font draws.
1448
+ * const text = ctx.require(textPlugin);
1449
+ *
1450
+ * text.hasGlyph("A", "body"); // true
1451
+ * text.hasGlyph("😀", "body"); // false once the body font is loaded: it has no emoji glyph
1452
+ * ```
1453
+ */
1454
+ hasGlyph(char: string, style: string): boolean;
1455
+ /**
1456
+ * The registered style names: the two built-ins first, then the styles of every feature in
1457
+ * feature order.
1458
+ *
1459
+ * @returns A frozen list of style names.
1460
+ * @example
1461
+ * ```ts
1462
+ * // A dev overlay lists what a game may write in a `style` prop.
1463
+ * app.text.styles(); // ["body", "digits", "hud.digits"]
1464
+ * ```
1465
+ */
1466
+ styles(): readonly string[];
1467
+ };
887
1468
  /**
888
- * A stretchable panel, sized in reference units, with its own alpha and tint. `debug: true` draws
889
- * the slice outline over it. `clip: true` masks the children of the entity to the panel box.
1469
+ * Resolved dependency APIs.
890
1470
  */
891
- declare const NineSlice: ComponentType<{
892
- texture: string;
893
- width: number;
894
- height: number;
895
- alpha: number;
896
- tint: number;
897
- debug: boolean;
898
- clip: boolean;
899
- }>;
1471
+ type Deps = {
1472
+ time: Api;
1473
+ flow: Api$1;
1474
+ world: Api$2;
1475
+ renderer: Api$3;
1476
+ assets: Api$4;
1477
+ i18n: I18nApi;
1478
+ clock: Api$5;
1479
+ };
900
1480
  /**
901
- * The entity this view moves with. `0` means no parent.
1481
+ * What the kernel context offers before the deps are attached.
1482
+ *
1483
+ * `text` owns no event, so `emit` is the kernel's and never called here.
902
1484
  */
903
- declare const Parent: ComponentType<{
904
- entity: number;
905
- }>;
1485
+ type KernelSlice = PluginCtx<Config, State> & {
1486
+ readonly global: object;
1487
+ readonly log: Log.LogApi;
1488
+ readonly require: Require;
1489
+ };
906
1490
  /**
907
- * A display object the game owns. Never pooled, never destroyed by `sync`.
1491
+ * Domain context shared by the files of the plugin: the kernel slice plus the resolved deps.
908
1492
  */
909
- declare const Display: ComponentType<DisplayValue>;
1493
+ type TextCtx = KernelSlice & {
1494
+ readonly deps: Deps;
1495
+ };
910
1496
  /**
911
- * A filled rounded rectangle or a right-pointing triangle, drawn with Pixi `Graphics` and redrawn
912
- * only when a field changed. `fillAlpha: 0` draws only the stroke, `dash` above 0 dashes it.
913
- * `clip: true` masks the children of the entity to the shape.
1497
+ * Payload of the `assets:bundle-loaded` hook: a bundle landed, so a font may be readable now.
914
1498
  */
915
- declare const Shape: ComponentType<ShapeValue>;
1499
+ type BundleLoaded = Events$1["assets:bundle-loaded"];
916
1500
  /**
917
- * Bundles the two components every visual needs, so a projection `view` reads as one line.
918
- *
919
- * @param options - Texture key, position, and the four values that have defaults.
920
- * @returns The `Sprite` and the `Transform` value, in that order.
921
- * @example
922
- * ```ts
923
- * sprite({ texture: "board.cell", at: { x: 540, y: 300 } });
924
- * // [Sprite({ texture: "board.cell", tint: 0xffffff, alpha: 1, anchor: { x: 0.5, y: 0.5 },
925
- * // width: 0, height: 0, fit: "fill" }),
926
- * // Transform({ x: 540, y: 300, rotation: 0, scale: 1, pivot: { x: 0, y: 0 } })]
927
- * ```
1501
+ * Payload of the `assets:bundle-unloaded` hook: `keys` names the assets that left.
928
1502
  */
929
- declare function sprite(options: SpriteOptions): [ComponentValue<SpriteValue>, ComponentValue<TransformValue>];
1503
+ type BundleUnloaded = Events$1["assets:bundle-unloaded"];
1504
+ /**
1505
+ * Payload of the `i18n:locale-changed` hook: every message has to be resolved again.
1506
+ */
1507
+ type LocaleChanged = Events["i18n:locale-changed"];
930
1508
  //#endregion
931
1509
  //#region src/plugins/ui/jsx/component.d.ts
932
1510
  /**
@@ -1314,11 +1892,6 @@ type ButtonLocal = Record<string, unknown> & Brand<"ButtonLocal">;
1314
1892
  type ButtonEscape = boolean & Brand<"ButtonEscape">;
1315
1893
  /** What a text draws: a plain string, or a message of the game's string table. */
1316
1894
  type TextContent = (string | Message) & Brand<"TextContent">;
1317
- /** The component field a text follows instead of a content. */
1318
- type TextBind = {
1319
- component: string;
1320
- field: string;
1321
- } & Brand<"TextBind">;
1322
1895
  /** The text style key, or a layout style when the built-in `body` is meant. */
1323
1896
  type TextStyleProp = (string | Style) & Brand<"TextStyleProp">;
1324
1897
  /** The asset key of an image. */
@@ -1329,6 +1902,23 @@ type IconName = string & Brand<"IconName">;
1329
1902
  type ImageFit = ("contain" | "cover" | "fill") & Brand<"ImageFit">;
1330
1903
  /** The axis a scroll container moves on. `"x"` throws until V4. */
1331
1904
  type ScrollAxis = ("x" | "y") & Brand<"ScrollAxis">;
1905
+ /** How many rows a windowed scroll has. */
1906
+ type ScrollRows = number & Brand<"ScrollRows">;
1907
+ /** The height of every row, in reference units; above 0. */
1908
+ type ScrollRowHeight = number & Brand<"ScrollRowHeight">;
1909
+ /** Rows kept alive beyond each edge of the viewport; 5 when left out. */
1910
+ type ScrollOverscan = number & Brand<"ScrollOverscan">;
1911
+ /**
1912
+ * Builds the row at an index; called at reconcile for the rows in the window, never at build time.
1913
+ *
1914
+ * @example
1915
+ * ```tsx
1916
+ * <scroll key="shop" style={list} rows={items.length} rowHeight={80} overscan={5}
1917
+ * row={index => <ShopRow key={items[index].id} item={items[index]} />} />;
1918
+ * // at offset 0: rows 0 to 24 exist, 25 of the 1000, for 80 u rows in a 1600 u list with overscan 5
1919
+ * ```
1920
+ */
1921
+ type ScrollRow = ((index: number) => DescriptionNode) & Brand<"ScrollRow">;
1332
1922
  /** The world projections whose live views a container draws inside itself. */
1333
1923
  type HostedProjections = readonly string[] & Brand<"HostedProjections">;
1334
1924
  /** The local field of the nearest component instance a text field writes on every keystroke. */
@@ -1409,11 +1999,15 @@ type ButtonTagProps = CommonProps & {
1409
1999
  local?: ButtonLocal;
1410
2000
  });
1411
2001
  /**
1412
- * What a text takes: a content or a bind, and the style key that carries its size.
2002
+ * What a text takes: a content or a bind, and the style key that carries its size. Only `bind()`
2003
+ * makes a bind; the text shows the bound field of a component of its own entity, so the bound
2004
+ * component goes in `components`.
1413
2005
  *
1414
2006
  * @example
1415
2007
  * ```ts
1416
2008
  * const props: TextTagProps = { content: "120", style: "digits" };
2009
+ * // The coin counter: Counter is an extra component, the text shows Math.round of its value.
2010
+ * const coins: TextTagProps = { bind: bind(Counter, "value"), components: [Counter({ value: 25 })] };
1417
2011
  * ```
1418
2012
  */
1419
2013
  type TextTagProps = Omit<CommonProps, "style"> & {
@@ -1460,17 +2054,40 @@ type IconProps = CommonProps & {
1460
2054
  * ```
1461
2055
  */
1462
2056
  type PanelProps = CommonProps;
2057
+ /** The child form of a scroll: its children are the content, laid out whole. */
2058
+ type ScrollChildForm = {
2059
+ rows?: never;
2060
+ rowHeight?: never;
2061
+ overscan?: never;
2062
+ row?: never;
2063
+ };
2064
+ /** The windowed form of a scroll: only the rows in view plus `overscan` exist. */
2065
+ type ScrollWindowForm = {
2066
+ rows: ScrollRows;
2067
+ rowHeight: ScrollRowHeight;
2068
+ overscan?: ScrollOverscan;
2069
+ row: ScrollRow;
2070
+ children?: never;
2071
+ };
1463
2072
  /**
1464
- * What a scroll container takes.
2073
+ * What a scroll container takes: children for a short list, or `rows`, `rowHeight` and `row` for
2074
+ * a long one, whose rows exist only in the window.
1465
2075
  *
1466
2076
  * @example
1467
2077
  * ```ts
1468
2078
  * const props: ScrollProps = { axis: "y" };
2079
+ * // 1000 orders of 120 u in a 1600 u list: rows 0 to 18 exist at offset 0.
2080
+ * const orders: ScrollProps = {
2081
+ * style: { height: 1600 },
2082
+ * rows: 1000,
2083
+ * rowHeight: 120,
2084
+ * row: index => ({ type: "row", key: `order${index}`, props: {}, children: [] })
2085
+ * };
1469
2086
  * ```
1470
2087
  */
1471
2088
  type ScrollProps = CommonProps & {
1472
2089
  axis?: ScrollAxis;
1473
- };
2090
+ } & (ScrollChildForm | ScrollWindowForm);
1474
2091
  /**
1475
2092
  * What a text field takes. `local` is required: every keystroke writes that field of the local
1476
2093
  * state of the nearest component, and Enter answers `submit` with `{ [local]: value }`. `style` is
@@ -1686,9 +2303,10 @@ type JsxChild = DescriptionNode | string | number | boolean | null | undefined |
1686
2303
  type ElementChange<Value> = (view: ViewHandle<unknown>, previous: Value, next: Value, hint?: Hint) => Motion;
1687
2304
  /**
1688
2305
  * The motion hooks of one element: the hook triple of `world.projection`, which `defineMotion`
1689
- * of `anim` returns. `ui` plays two `change` hooks itself and hands them typed values: `Box` gets
1690
- * the rects, `Transform` the rest poses. A hook for any other component name is accepted, so every
1691
- * `defineMotion` result fits, and `ui` never plays it.
2306
+ * of `anim` returns. `ui` plays the `change` hooks of `Box` (the rects), of `Transform` (the rest
2307
+ * poses) and of every extra component of the `components` prop (its values before and after). A
2308
+ * hook for any other component name is accepted, so every `defineMotion` result fits, and `ui`
2309
+ * never plays it. Every hook gets the hint routed to the element's key in that frame step.
1692
2310
  *
1693
2311
  * @example
1694
2312
  * ```ts
@@ -1773,14 +2391,37 @@ type Instance = {
1773
2391
  props: object;
1774
2392
  dirty: boolean;
1775
2393
  };
2394
+ /**
2395
+ * The range a windowed scroll holds and what it was cut with: `rows` rows of `rowHeight`, the
2396
+ * indexes `first` to `last` alive, `overscan` of them beyond each edge of the viewport. `last` is
2397
+ * `first - 1` for an empty list.
2398
+ *
2399
+ * @example
2400
+ * ```ts
2401
+ * // P18's list scrolled to -3840 u: 24 of its 1000 rows exist.
2402
+ * const window: ScrollWindow = { rows: 1000, rowHeight: 120, overscan: 5, first: 27, last: 50 };
2403
+ * ```
2404
+ */
2405
+ type ScrollWindow = {
2406
+ rows: number;
2407
+ rowHeight: number;
2408
+ overscan: number;
2409
+ first: number;
2410
+ last: number;
2411
+ };
1776
2412
  /**
1777
2413
  * One live element: the entity, the Yoga node, the resolved style and the place in the tree.
1778
2414
  * `rect` is natural: under a `fit` ancestor it is the rect before that ancestor's scale. `fit`
1779
2415
  * is the element's own fit scale (1 without `fit: "contain"`), and `rest` the rest `Transform`
1780
2416
  * last written for it. `loop` is the motion of the running `loop` hook, kept apart from
1781
2417
  * `handles`: it never ends, so the exit sweep must not wait for it. `extras` holds the values of
1782
- * the `components` prop added last time, by component name; `warnedOwned` the names of the
1783
- * `components` values the element owns that were already logged.
2418
+ * the `components` prop added last time, by component name; `extraHandles` the motion the last
2419
+ * `change` hook of each of them returned; `warnedOwned` the names of the `components` values the
2420
+ * element owns that were already logged. A windowed scroll carries its `window` and the one-time
2421
+ * warnings of its props in `warned`; `scrolledIn` marks an element a range change of a windowed
2422
+ * scroll created: it plays no `enter` hook. A text or an icon that sizes itself keeps in
2423
+ * `measuredSize` what its measure function last answered, before Yoga's clamp: a bound text whose
2424
+ * new string measures the same asks for no solve.
1784
2425
  */
1785
2426
  type Element = {
1786
2427
  entity: Entity;
@@ -1817,7 +2458,15 @@ type Element = {
1817
2458
  entered: boolean;
1818
2459
  dropKey: (() => void) | undefined;
1819
2460
  extras: ReadonlyMap<string, AnyComponentValue>;
2461
+ extraHandles: Map<string, MotionHandle>;
1820
2462
  warnedOwned: Set<string>;
2463
+ warned: Set<string>;
2464
+ scrolledIn: boolean;
2465
+ window?: ScrollWindow;
2466
+ measuredSize?: {
2467
+ width: number;
2468
+ height: number;
2469
+ };
1821
2470
  };
1822
2471
  /**
1823
2472
  * The popup side of a root: the handler that shows it now and the root it was opened over.
@@ -1971,7 +2620,9 @@ type TextState = {
1971
2620
  };
1972
2621
  /**
1973
2622
  * jsx module state. `hosts` holds the elements with a `hosts` prop; `hosted` maps a world view to
1974
- * the element that hosts it; `fields` holds the text fields by entity.
2623
+ * the element that hosts it; `fields` holds the text fields by entity. `hints` buffers the hints
2624
+ * released since the last frame step; `scrolls` holds the scroll containers; `windowRenders`
2625
+ * counts the re-renders of a windowed list its range made.
1975
2626
  */
1976
2627
  type JsxState = {
1977
2628
  components: Map<string, AnyComponentDefinition>;
@@ -1988,12 +2639,16 @@ type JsxState = {
1988
2639
  focus: FocusState;
1989
2640
  fields: Map<Entity, Field>;
1990
2641
  text: TextState;
2642
+ hints: Hint[];
2643
+ scrolls: Set<Entity>;
2644
+ windowRenders: number;
1991
2645
  };
1992
2646
  /**
1993
2647
  * One node of the snapshot `tree()` answers with. `rect` is natural: under a `fit` ancestor it
1994
2648
  * is the rect before that ancestor's scale. An element with `fit: "contain"` adds `fitScale`,
1995
2649
  * the scale it is drawn at. An `input` adds `value`: the text being typed while it is edited,
1996
- * else the local field it writes.
2650
+ * else the local field it writes. A windowed `scroll` adds `window`: the rows that exist, which
2651
+ * are the children of its content, and how many the list has.
1997
2652
  *
1998
2653
  * @example
1999
2654
  * ```ts
@@ -2019,6 +2674,20 @@ type UiNode = {
2019
2674
  local?: Record<string, unknown>;
2020
2675
  fitScale?: number;
2021
2676
  value?: string;
2677
+ /**
2678
+ * The rows of a windowed scroll that exist and how many the list has.
2679
+ *
2680
+ * @example
2681
+ * ```ts
2682
+ * // P18's shop list of 1000 rows, 120 u each in 1600 u, scrolled to -3840 u: 24 rows exist.
2683
+ * const window: UiNode["window"] = { first: 27, last: 50, rows: 1000 };
2684
+ * ```
2685
+ */
2686
+ window?: {
2687
+ first: number;
2688
+ last: number;
2689
+ rows: number;
2690
+ };
2022
2691
  children: UiNode[];
2023
2692
  };
2024
2693
  /**
@@ -2055,4 +2724,4 @@ type CommonProps = {
2055
2724
  components?: ElementComponents;
2056
2725
  };
2057
2726
  //#endregion
2058
- export { Transform as A, types_d_exports as B, NineSliceValue as C, Sprite as D, ShapeValue as E, Events as F, InputApi as H, I18nApi as I, Message as L, sprite as M, Argument as N, SpriteOptions as O, Config as P, State as R, NineSlice as S, Shape as T, State$1 as U, Config$1 as V, types_d_exports$1 as W, Style as _, JsxChild as a, defineComponent as b, UiNode as c, popup as d, defineTokens as f, ResolvedStyle as g, IsFlags as h, Finding as i, TransformValue as j, SpriteValue as k, Box as l, UiIntrinsicElements as m, ElementChange as n, JsxState as o, IntrinsicElementsFor as p, ElementMotion as r, PopupComponent as s, DescriptionNode as t, LocalWrite as u, StylesState as v, Parent as w, Display as x, WhenFlags as y, TypedTr as z };
2727
+ export { SpriteValue as $, TextStyleInput as A, types_d_exports$1 as B, Config as C, TextApi as D, State as E, Config$1 as F, Display as G, InputApi as H, I18nApi as I, Parent as J, NineSlice as K, Message as L, TextValue as M, types_d_exports as N, TextBind as O, Argument as P, SpriteOptions as Q, State$1 as R, BindOptions as S, Point as T, State$2 as U, Config$2 as V, types_d_exports$2 as W, ShapeValue as X, Shape as Y, Sprite as Z, Style as _, JsxChild as a, defineComponent as b, UiNode as c, popup as d, Transform as et, defineTokens as f, ResolvedStyle as g, IsFlags as h, Finding as i, TextStyles as j, TextFormat as k, Box as l, UiIntrinsicElements as m, ElementChange as n, sprite as nt, JsxState as o, IntrinsicElementsFor as p, NineSliceValue as q, ElementMotion as r, PopupComponent as s, DescriptionNode as t, TransformValue as tt, LocalWrite as u, StylesState as v, CountdownValue as w, BindField as x, WhenFlags as y, TypedTr as z };