@moku-labs/game 0.8.0 → 0.10.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 (60) hide show
  1. package/README.md +88 -26
  2. package/bin/moku-game.mjs +4 -0
  3. package/dist/app/page.d.mts +41 -0
  4. package/dist/app/page.mjs +376 -0
  5. package/dist/app/system.d.mts +129 -0
  6. package/dist/app/system.mjs +307 -0
  7. package/dist/app.d.mts +37 -0
  8. package/dist/app.mjs +235 -0
  9. package/dist/assets-BHwTLVta.mjs +7391 -0
  10. package/dist/assets.d.mts +97 -33
  11. package/dist/assets.mjs +1 -7150
  12. package/dist/{capture-BX5Gfoty.mjs → capture-fpd83Mi5.mjs} +384 -1
  13. package/dist/cli.d.mts +70 -0
  14. package/dist/cli.mjs +1650 -0
  15. package/dist/{commands-C1kIfOG5.d.mts → commands-C4QlqijJ.d.mts} +5 -6
  16. package/dist/config-CdARrrhU.mjs +81 -0
  17. package/dist/control.d.mts +11 -10
  18. package/dist/control.mjs +3 -3
  19. package/dist/{define-BPpZcpxW.d.mts → define-CCc0ozKe.d.mts} +2 -2
  20. package/dist/{dev-wKLgOw7c.mjs → dev-Cc50Dk7H.mjs} +1 -1
  21. package/dist/{memory-CvgdnsQO.mjs → drafts-BawpSaVp.mjs} +1 -104
  22. package/dist/fake-MpzwZhIZ.mjs +87 -0
  23. package/dist/{headless-CvamCUcR.mjs → headless-BsNdJs9W.mjs} +1 -1
  24. package/dist/{headless-q98SwADR.d.mts → headless-G4nuZ-xz.d.mts} +1 -1
  25. package/dist/hot.d.mts +8 -8
  26. package/dist/hot.mjs +15 -14
  27. package/dist/index-xxSxKJJW.d.mts +1611 -0
  28. package/dist/index.d.mts +6 -1597
  29. package/dist/index.mjs +4 -34343
  30. package/dist/inspect-la_KkjCS.mjs +1 -0
  31. package/dist/inspect.d.mts +7 -8
  32. package/dist/inspect.mjs +3 -2
  33. package/dist/jsx-dev-runtime.d.mts +1 -1
  34. package/dist/jsx-runtime.d.mts +1 -1
  35. package/dist/lint.d.mts +55 -4
  36. package/dist/lint.mjs +999 -42
  37. package/dist/memory-CWlqhS7l.mjs +136 -0
  38. package/dist/memory-DwDhfFWK.d.mts +50 -0
  39. package/dist/project.d.mts +29 -13
  40. package/dist/project.mjs +1120 -631
  41. package/dist/{run-CUPSFM85.mjs → run-wYty_Lwo.mjs} +4 -4
  42. package/dist/{runtime-CvDZZg1S.d.mts → runtime-DOBYM6g4.d.mts} +1 -1
  43. package/dist/{input-z_35Jwfn.mjs → session-DgukhR_7.mjs} +20 -20
  44. package/dist/{sources-BBUd15vQ.mjs → sources-Duzj6lhV.mjs} +3 -3
  45. package/dist/src-DSl1eNB1.mjs +34366 -0
  46. package/dist/testing.d.mts +115 -34
  47. package/dist/testing.mjs +175 -70
  48. package/dist/{types-DlgsfOmW.d.mts → types-BM4shh8K.d.mts} +300 -263
  49. package/dist/{types-DgdMLZnV.d.mts → types-BUwDHKXm.d.mts} +1372 -1373
  50. package/dist/{types-DF5kkW-C.d.mts → types-CefgYzFt.d.mts} +4 -5
  51. package/dist/{types-CDSKa0vl.d.mts → types-DAdwEJMp.d.mts} +2 -2
  52. package/dist/{types-BWzCSRM2.d.mts → types-ZQLYXo5X.d.mts} +2983 -20
  53. package/dist/types-hQgI_wrq.d.mts +544 -0
  54. package/dist/{visual-DUvvPPCt.mjs → visual-dMW8iqAE.mjs} +8 -4
  55. package/dist/visual.d.mts +10 -11
  56. package/dist/visual.mjs +5 -5
  57. package/llms.txt +170 -57
  58. package/package.json +30 -2
  59. package/dist/registry-DlpRCibU.mjs +0 -384
  60. package/dist/types-ERb2Y9U5.d.mts +0 -2967
@@ -1,1914 +1,1913 @@
1
- import { A as Api, E as Descriptor, O as Hint, R as Json, rt as Api$2, t as Api$1, ut as Require } from "./types-ERb2Y9U5.mjs";
2
- import { B as ComponentValue, D as Point$2, F as AnyComponentType, I as AnyComponentValue, O as ViewportSize, P as AnyComponent, V as Entity, Z as ChangeHook, at as ProjectionMotion, it as NumericFields, k as Api$3, lt as ViewHandle, m as Api$4, nt as Motion, o as Events$1, rt as MotionHandle, t as Api$5, z as ComponentType } from "./types-BWzCSRM2.mjs";
1
+ import { A as Api$2, B as ComponentType, F as AnyComponent, H as Entity, I as AnyComponentType, L as AnyComponentValue, Lt as Descriptor, O as Point$2, Q as ChangeHook, Sn as Require, V as ComponentValue, Vt as Api$5, Xt as Json, at as NumericFields, dt as Api$1, h as Api$3, hn as Api, it as MotionHandle, k as ViewportSize, ot as ProjectionMotion, rt as Motion, s as Events$1, t as Api$4, ut as ViewHandle, zt as Hint } from "./types-ZQLYXo5X.mjs";
3
2
  import { Log } from "@moku-labs/common/browser";
4
3
  import { PluginCtx } from "@moku-labs/core";
5
4
 
6
- //#region src/plugins/renderer/components.d.ts
5
+ //#region src/plugins/ui/styles/types.d.ts
7
6
  /**
8
- * Where a view sits, in reference units and radians. Relative to the `Parent` when there is one.
9
- * `pivot` is the local point the view turns and scales around, and `x`, `y` is where that point
10
- * lands; the default pivot `{ x: 0, y: 0 }` is the view's own origin.
7
+ * A length that may also come from the notch. The four tokens are replaced by `resolve` with the
8
+ * matching `viewport.safeArea` value.
11
9
  *
12
10
  * @example
13
11
  * ```ts
14
- * // A 200 x 80 button that grows around its centre.
15
- * const value: TransformValue = {
16
- * x: 640, y: 340, rotation: 0, scale: 1.1, pivot: { x: 100, y: 40 }
17
- * };
12
+ * const top: SafeAreaToken = "safeArea.top";
18
13
  * ```
19
14
  */
20
- type TransformValue = {
21
- x: number;
22
- y: number;
23
- rotation: number;
24
- scale: number;
25
- pivot: Point$2;
26
- };
15
+ type SafeAreaToken = "safeArea.top" | "safeArea.right" | "safeArea.bottom" | "safeArea.left";
27
16
  /**
28
- * How a sized sprite fills its box: `"fill"` stretches, `"contain"` scales uniformly inside the
29
- * box and centres, `"cover"` scales uniformly over the box and crops the overflow.
17
+ * A length in reference units, or one of the safe-area tokens.
30
18
  *
31
19
  * @example
32
20
  * ```ts
33
- * const fit: SpriteFit = "cover";
21
+ * const length: Length = 24;
34
22
  * ```
35
23
  */
36
- type SpriteFit = "fill" | "contain" | "cover";
24
+ type Length = number | SafeAreaToken;
37
25
  /**
38
- * One textured quad. `texture` is an asset key; `defineGame` narrows it to the game's keys.
39
- * `width` and `height` are the box in reference units; 0 on an axis keeps the texture's own size
40
- * there. `anchor` is the point of the box that sits on the `Transform`. `alpha` fades the quad
41
- * and the entity's children.
26
+ * Padding or margin: one number for every edge, or a value per edge.
42
27
  *
43
28
  * @example
44
29
  * ```ts
45
- * const value: SpriteValue = {
46
- * texture: "board.bg-forest-meadow", tint: 0xffffff, alpha: 1, anchor: { x: 0, y: 0 },
47
- * width: 1080, height: 1920, fit: "cover"
48
- * };
30
+ * const padding: Edges = { top: "safeArea.top", left: 16 };
49
31
  * ```
50
32
  */
51
- type SpriteValue = {
52
- texture: string;
53
- tint: number;
54
- alpha: number;
55
- anchor: Point$2;
56
- width: number;
57
- height: number;
58
- fit: SpriteFit;
33
+ type Edges = Length | {
34
+ top?: Length;
35
+ right?: Length;
36
+ bottom?: Length;
37
+ left?: Length;
59
38
  };
60
39
  /**
61
- * A stretchable panel. The slice borders come with the texture, not with the component. `debug`
62
- * strokes the bounds and the four cut lines over the panel: cyan, red when the corners overlap or
63
- * the texture is missing. `clip` masks the children of the entity to the `width × height` box,
64
- * the same rule as `Shape.clip`: the mask is a filled rectangle and is never drawn, and the panel
65
- * itself is never masked. `alpha` fades the panel and the entity's children.
40
+ * A main-axis or cross-axis size: reference units, a percent of the parent, or `"auto"`.
66
41
  *
67
42
  * @example
68
43
  * ```ts
69
- * const value: NineSliceValue = {
70
- * texture: "ui.panel", width: 600, height: 320, alpha: 1, tint: 0xffffff, debug: false,
71
- * clip: false
72
- * };
73
- * // The board tray, outlined while its insets are checked.
74
- * const tray: NineSliceValue = {
75
- * ...value, texture: "board.board-tray", width: 1000, height: 1040, debug: true
76
- * };
77
- * // A text field: a long value never draws outside the frame.
78
- * const field: NineSliceValue = {
79
- * ...value, texture: "ui.field", width: 480, height: 96, clip: true
80
- * };
44
+ * const width: Extent = "50%";
81
45
  * ```
82
46
  */
83
- type NineSliceValue = {
84
- texture: string;
85
- width: number;
86
- height: number;
87
- alpha: number;
88
- tint: number;
89
- debug: boolean;
90
- clip: boolean;
47
+ type Extent = number | `${number}%` | "auto";
48
+ /**
49
+ * The flow group of the vocabulary: what Yoga's flexbox calls are set from.
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * const flow: FlowStyle = { direction: "row", gap: 12, justify: "between" };
54
+ * ```
55
+ */
56
+ type FlowStyle = {
57
+ direction?: "row" | "column";
58
+ wrap?: boolean;
59
+ justify?: "start" | "center" | "end" | "between" | "around" | "evenly";
60
+ align?: "start" | "center" | "end" | "stretch";
61
+ alignSelf?: "auto" | "start" | "center" | "end" | "stretch";
62
+ gap?: number;
63
+ grow?: number;
64
+ shrink?: number;
91
65
  };
92
66
  /**
93
- * The escape hatch: the game owns a Pixi object and `sync` only attaches and detaches it.
67
+ * The box group: what the element takes up before its children are placed.
94
68
  *
95
69
  * @example
96
70
  * ```ts
97
- * const value: DisplayValue = { object: spineAnimation };
71
+ * // The board slot: 970 u of cells, scaled down on a short phone.
72
+ * const box: BoxStyle = { width: 970, height: 970, fit: "contain" };
98
73
  * ```
99
74
  */
100
- type DisplayValue = {
101
- object: unknown;
75
+ type BoxStyle = {
76
+ padding?: Edges;
77
+ margin?: Edges;
78
+ width?: Extent;
79
+ height?: Extent;
80
+ minWidth?: Extent;
81
+ minHeight?: Extent;
82
+ maxWidth?: Extent;
83
+ maxHeight?: Extent;
84
+ aspect?: number; /** `"hidden"` clips the children to the rect, as a scroll container does. */
85
+ overflow?: "visible" | "hidden" | "scroll";
86
+ /**
87
+ * `"contain"`: the element keeps its own size and leaves the flow, then is scaled down to fit
88
+ * the content box of its parent and centred in it. Its children keep their natural rects.
89
+ */
90
+ fit?: "contain";
102
91
  };
103
92
  /**
104
- * A filled rounded rectangle or a triangle, anchored at the top left of the `Transform`. `kind`
105
- * picks the outline: `"rect"` (the default) or `"triangle"`, which fills its `w × h` box pointing
106
- * right (rotate the element for another direction) and ignores `radius`. `fillAlpha` is the alpha
107
- * of the fill alone: 0 draws only the stroke, a ring. `alpha` fades the whole shape and the
108
- * entity's children: a disabled button at 0.6 draws its icon at 0.6 too. `dash` is the
109
- * dash length of the stroke in reference units, with gaps of half a dash; 0 strokes a solid line.
110
- * `clip` masks the children of the entity to the shape, always solid, which is how `ui` draws a
111
- * scroll and an overflow. The shape itself is never masked, so its stroke is drawn whole.
93
+ * The position group. `reason` is the comment `lint()` asks for next to an absolute element.
112
94
  *
113
95
  * @example
114
96
  * ```ts
115
- * const value: ShapeValue = {
116
- * kind: "rect", w: 320, h: 96, fill: 0x101018, fillAlpha: 1, alpha: 1, radius: 16,
117
- * stroke: 0x000000, strokeWidth: 0, dash: 0, clip: false
118
- * };
119
- * // The honey ring on a selected cell: a stroke and no fill.
120
- * const ring: ShapeValue = {
121
- * kind: "rect", w: 140, h: 140, fill: 0xffffff, fillAlpha: 0, alpha: 1, radius: 24,
122
- * stroke: 0xffc233, strokeWidth: 6, dash: 0, clip: false
123
- * };
124
- * // The play glyph of a watch button, and a dashed focus ring: 10 u dashes, 5 u gaps.
125
- * const play: ShapeValue = { ...ring, kind: "triangle", w: 36, h: 40, fillAlpha: 1, strokeWidth: 0 };
126
- * const focus: ShapeValue = { ...ring, w: 200, h: 80, stroke: 0x3a2212, strokeWidth: 4, dash: 10 };
97
+ * const pinned: PositionStyle = { position: "absolute", top: 0, right: 0, reason: "badge" };
98
+ * // The order strip of the board drawn over the tray that follows it.
99
+ * const strip: PositionStyle = { zIndex: 1 };
127
100
  * ```
128
101
  */
129
- type ShapeValue = {
130
- kind: "rect" | "triangle";
131
- w: number;
132
- h: number;
133
- fill: number;
134
- fillAlpha: number;
135
- alpha: number;
136
- radius: number;
137
- stroke: number;
138
- strokeWidth: number;
139
- dash: number;
140
- clip: boolean;
102
+ type PositionStyle = {
103
+ position?: "relative" | "absolute";
104
+ left?: Length;
105
+ top?: Length;
106
+ right?: Length;
107
+ bottom?: Length;
108
+ reason?: string;
109
+ /**
110
+ * The draw order among the siblings, an integer: a higher one draws over a lower one, and a
111
+ * sibling without it draws at 0 in markup order. Ignored on a root element, which `lint()`
112
+ * reports; a root draws at the order of its layer.
113
+ */
114
+ zIndex?: number;
141
115
  };
142
116
  /**
143
- * What `sprite()` takes: the two things every visual needs, and the four that have defaults.
117
+ * The visual group: what is written to `Shape`, `Sprite` or `NineSlice`. There is no font size
118
+ * here — the size of a text comes from its text style key.
144
119
  *
145
120
  * @example
146
121
  * ```ts
147
- * const options: SpriteOptions = { texture: "board.cell", at: { x: 540, y: 300 } };
122
+ * // A wooden button: the nine-slice of the style, greyed while disabled.
123
+ * const visual: VisualStyle = { nineSlice: "ui.button-wood", alpha: 1, tint: 0xffffff };
124
+ * // The popup board while its insets are checked: the slice lines drawn over it.
125
+ * const checked: VisualStyle = { nineSlice: "ui.panel-signboard", debug: true };
126
+ * // The play glyph of the Watch button, and a dashed ring around a slot.
127
+ * const play: VisualStyle = { shape: "triangle", fill: 0xfffbe8 };
128
+ * const slot: VisualStyle = { stroke: 0x3a2212, strokeWidth: 4, dash: 10, radius: 24 };
148
129
  * ```
149
130
  */
150
- type SpriteOptions = {
151
- texture: string;
152
- at: Point$2;
153
- tint?: number;
131
+ type VisualStyle<Asset extends string = string> = {
132
+ fill?: number;
133
+ stroke?: number;
134
+ strokeWidth?: number;
135
+ radius?: number;
154
136
  alpha?: number;
155
- anchor?: Point$2;
156
- scale?: number;
137
+ /**
138
+ * The shape of the rectangle an element without a nine-slice is drawn with: `"rect"` (the
139
+ * default) or `"triangle"`, which fills the box pointing right and ignores `radius`. Turn the
140
+ * element with `rotation` for another direction.
141
+ */
142
+ shape?: "rect" | "triangle"; /** The dash length of the stroke in reference units, with gaps of half a dash; 0 is solid. */
143
+ dash?: number;
144
+ /**
145
+ * The asset key of a nine-slice drawn at the rect instead of the rounded rectangle. Any tag
146
+ * but `image`, `icon` and `text` takes it; a clipping element (`scroll`, `overflow: "hidden"`)
147
+ * keeps its rectangle, which carries the clip.
148
+ */
149
+ nineSlice?: Asset; /** Multiplies the colour of a nine-slice or an image. */
150
+ tint?: number;
151
+ /**
152
+ * Outlines the nine-slice of the style: its bounds and the four slice lines, cyan, red when
153
+ * the corners overlap or the texture is missing. A look for checking art, never for a player.
154
+ */
155
+ debug?: boolean;
157
156
  };
158
157
  /**
159
- * Where a view sits: reference units, radians, uniform scale, and the local point it turns
160
- * around.
158
+ * Where an element turns and scales: its centre, the middle of its top edge, its top-left
159
+ * corner, or a point given in fractions of its box.
160
+ *
161
+ * @example
162
+ * ```ts
163
+ * // A sign that hangs from its ropes swings around the middle of its top edge.
164
+ * const origin: Origin = "top";
165
+ * ```
161
166
  */
162
- declare const Transform: ComponentType<TransformValue>;
167
+ type Origin = "center" | "top" | "topLeft" | {
168
+ x: number;
169
+ y: number;
170
+ };
163
171
  /**
164
- * One textured quad. `texture` is an asset key, resolved through the texture providers; a size
165
- * and a `fit` draw it into a box.
172
+ * The transform group: how an element is drawn, never where it is laid out. Written into the
173
+ * rest `Transform`, so a hover lift or a pressed sink never moves a sibling.
174
+ *
175
+ * @example
176
+ * ```ts
177
+ * // A button that lifts under the mouse.
178
+ * const lift: TransformStyle = { offsetY: -6, scale: 1.05, origin: "center" };
179
+ * // The title plaque of a popup, tilted by 1.5 degrees around the middle of its top edge.
180
+ * const plaque: TransformStyle = { rotation: -0.026, origin: "top" };
181
+ * ```
166
182
  */
167
- declare const Sprite: ComponentType<SpriteValue>;
183
+ type TransformStyle = {
184
+ /** Moves the drawn element right, in reference units. */offsetX?: number; /** Moves the drawn element down, in reference units. */
185
+ offsetY?: number; /** Uniform scale around the origin. */
186
+ scale?: number; /** Turns the drawn element around the origin, in radians, clockwise; 0 by default. */
187
+ rotation?: number; /** The point the element scales and turns around; the centre by default. */
188
+ origin?: Origin;
189
+ };
168
190
  /**
169
- * A stretchable panel, sized in reference units, with its own alpha and tint. `debug: true` draws
170
- * the slice outline over it. `clip: true` masks the children of the entity to the panel box.
191
+ * Everything but the variants: the five groups of the vocabulary.
192
+ *
193
+ * @example
194
+ * ```ts
195
+ * const base: BaseStyle = { direction: "row", gap: 8, fill: 0x101018 };
196
+ * ```
171
197
  */
172
- declare const NineSlice: ComponentType<{
173
- texture: string;
174
- width: number;
175
- height: number;
176
- alpha: number;
177
- tint: number;
178
- debug: boolean;
179
- clip: boolean;
180
- }>;
181
- /**
182
- * The entity this view moves with. `0` means no parent.
183
- */
184
- declare const Parent: ComponentType<{
185
- entity: number;
186
- }>;
187
- /**
188
- * A display object the game owns. Never pooled, never destroyed by `sync`.
189
- */
190
- declare const Display: ComponentType<DisplayValue>;
198
+ type BaseStyle<Asset extends string = string> = FlowStyle & BoxStyle & PositionStyle & VisualStyle<Asset> & TransformStyle;
191
199
  /**
192
- * A filled rounded rectangle or a right-pointing triangle, drawn with Pixi `Graphics` and redrawn
193
- * only when a field changed. `fillAlpha: 0` draws only the stroke, `dash` above 0 dashes it.
194
- * `clip: true` masks the children of the entity to the shape.
200
+ * The seven state variants of a style, applied in the order disabled, active, selected, hover,
201
+ * focus, pressed, covered. While `disabled` is true, `hover` and `pressed` are not applied.
202
+ *
203
+ * @example
204
+ * ```ts
205
+ * // One rule set for every control: lift on hover, sink when pressed, swap the texture when off.
206
+ * const variants: IsVariants = {
207
+ * hover: { offsetY: -6, scale: 1.05 },
208
+ * focus: { scale: 1.05 },
209
+ * pressed: { offsetY: 4, scale: 0.95 },
210
+ * disabled: { nineSlice: "ui.button-disabled" }
211
+ * };
212
+ * ```
195
213
  */
196
- declare const Shape: ComponentType<ShapeValue>;
214
+ type IsVariants<Asset extends string = string> = {
215
+ pressed?: BaseStyle<Asset>;
216
+ disabled?: BaseStyle<Asset>;
217
+ active?: BaseStyle<Asset>;
218
+ selected?: BaseStyle<Asset>; /** The mouse or pen is over the element. Touch never hovers. */
219
+ hover?: BaseStyle<Asset>; /** The keyboard focus is on the element: Tab moved it there, and no pointer tapped since. */
220
+ focus?: BaseStyle<Asset>; /** The popup the element belongs to is kept under another one. */
221
+ covered?: BaseStyle<Asset>;
222
+ };
197
223
  /**
198
- * Bundles the two components every visual needs, so a projection `view` reads as one line.
224
+ * The four viewport variants of a style, applied in the order portrait, landscape, tall, wide.
199
225
  *
200
- * @param options - Texture key, position, and the four values that have defaults.
201
- * @returns The `Sprite` and the `Transform` value, in that order.
202
226
  * @example
203
227
  * ```ts
204
- * sprite({ texture: "board.cell", at: { x: 540, y: 300 } });
205
- * // [Sprite({ texture: "board.cell", tint: 0xffffff, alpha: 1, anchor: { x: 0.5, y: 0.5 },
206
- * // width: 0, height: 0, fit: "fill" }),
207
- * // Transform({ x: 540, y: 300, rotation: 0, scale: 1, pivot: { x: 0, y: 0 } })]
228
+ * const variants: WhenVariants = { landscape: { direction: "row" } };
208
229
  * ```
209
230
  */
210
- declare function sprite(options: SpriteOptions): [ComponentValue<SpriteValue>, ComponentValue<TransformValue>];
211
- declare namespace types_d_exports$2 {
212
- 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 };
213
- }
231
+ type WhenVariants<Asset extends string = string> = {
232
+ portrait?: BaseStyle<Asset>;
233
+ landscape?: BaseStyle<Asset>;
234
+ tall?: BaseStyle<Asset>;
235
+ wide?: BaseStyle<Asset>;
236
+ };
214
237
  /**
215
- * A point in reference units, as `renderer.viewport.toReference` answers it.
238
+ * One style as a game writes it: the base vocabulary plus the two variant tables. `Asset` is the
239
+ * game's asset key union: `uiFor` narrows `nineSlice` to it, the way it narrows `texture`.
216
240
  *
217
241
  * @example
218
242
  * ```ts
219
- * const point: Point = { x: 540, y: 960 };
243
+ * const topBar: Style = { direction: "row", gap: 12, when: { landscape: { justify: "end" } } };
220
244
  * ```
221
245
  */
222
- type Point$1 = {
223
- x: number;
224
- y: number;
246
+ type Style<Asset extends string = string> = BaseStyle<Asset> & {
247
+ is?: IsVariants<Asset>;
248
+ when?: WhenVariants<Asset>;
225
249
  };
226
250
  /**
227
- * The dominant axis of a swipe.
251
+ * What `resolve` answers with: the base vocabulary with every safe-area token replaced by a
252
+ * number and every variant already merged in. Frozen.
228
253
  *
229
254
  * @example
230
255
  * ```ts
231
- * const direction: Direction = "left";
256
+ * const resolved: ResolvedStyle = { direction: "row", gap: 12, padding: 24 };
232
257
  * ```
233
258
  */
234
- type Direction = "up" | "down" | "left" | "right";
259
+ type ResolvedStyle = Omit<BaseStyle, "padding" | "margin" | "left" | "top" | "right" | "bottom"> & {
260
+ padding?: number | {
261
+ top?: number;
262
+ right?: number;
263
+ bottom?: number;
264
+ left?: number;
265
+ };
266
+ margin?: number | {
267
+ top?: number;
268
+ right?: number;
269
+ bottom?: number;
270
+ left?: number;
271
+ };
272
+ left?: number;
273
+ top?: number;
274
+ right?: number;
275
+ bottom?: number;
276
+ };
235
277
  /**
236
- * What `app.input.*` is pointed at: the projection key of a live view, or an entity. A key never
237
- * addresses a view in the despawn queue.
278
+ * The viewport flags of a style: which `when` variants apply this frame.
238
279
  *
239
280
  * @example
240
281
  * ```ts
241
- * const target: Target = { projection: "board.items", key: "i5" };
282
+ * const flags: WhenFlags = { portrait: true, landscape: false, tall: true, wide: false };
242
283
  * ```
243
284
  */
244
- type Target = {
245
- projection: string;
246
- key: string;
247
- } | Entity;
285
+ type WhenFlags = {
286
+ portrait: boolean;
287
+ landscape: boolean;
288
+ tall: boolean;
289
+ wide: boolean;
290
+ };
248
291
  /**
249
- * One pointer event as the DOM handlers queue it: no hit test, no reference units, no decision.
250
- * `cancel` is a `pointercancel`, `lost` a lost pointer capture, `leave` the pointer leaving the
251
- * canvas. `pointerType` is the device: only a mouse or a pen hovers.
292
+ * The state flags of an element: what the markup declared (`disabled`, `active`, `selected`),
293
+ * `pressed` and `hover` from the pointer, `focus` from the keyboard, and `covered` from the root
294
+ * it belongs to.
252
295
  *
253
296
  * @example
254
297
  * ```ts
255
- * const sample: RawSample = {
256
- * kind: "down", pointerType: "touch", pointerId: 1, clientX: 320, clientY: 640
298
+ * const flags: IsFlags = {
299
+ * pressed: false, hover: true, focus: false, disabled: false, active: true, selected: false,
300
+ * covered: false
257
301
  * };
258
302
  * ```
259
303
  */
260
- type RawSample = {
261
- kind: "down" | "move" | "up" | "cancel" | "lost" | "leave";
262
- pointerType: "mouse" | "touch" | "pen";
263
- pointerId: number;
264
- clientX: number;
265
- clientY: number;
304
+ type IsFlags = {
305
+ pressed: boolean;
306
+ hover: boolean;
307
+ focus: boolean;
308
+ disabled: boolean;
309
+ active: boolean;
310
+ selected: boolean;
311
+ covered: boolean;
266
312
  };
267
313
  /**
268
- * Where the one active pointer stands in the gesture machine.
314
+ * A flat table of design tokens a game reads in its styles.
269
315
  *
270
316
  * @example
271
317
  * ```ts
272
- * const phase: GesturePhase = "dragging";
318
+ * const tokens: Tokens = { space: { md: 16 }, color: { panel: 0x101018 } };
273
319
  * ```
274
320
  */
275
- type GesturePhase = "idle" | "pressed" | "longPressed" | "dragging" | "tracing";
321
+ type Tokens = {
322
+ readonly space?: Readonly<Record<string, number>>;
323
+ readonly color?: Readonly<Record<string, number>>;
324
+ readonly radius?: Readonly<Record<string, number>>;
325
+ readonly font?: Readonly<Record<string, string>>;
326
+ };
276
327
  /**
277
- * What `onTap` hands a listener: the view the finger let go on, inside the tap slop.
328
+ * styles module state: the flags of the frame and the viewport they were read from.
329
+ */
330
+ type StylesState = {
331
+ flags: WhenFlags;
332
+ viewport: ViewportSize | undefined;
333
+ };
334
+ declare namespace types_d_exports$2 {
335
+ export { Argument, CompiledMessage, CompiledMessages, Config$2 as Config, DurationStyle, ElementNode, Events, I18nApi, I18nCtx, I18nKit, IntlKit, KeysWithoutParameters, Message, Part, RegisteredModule, ResolvedModule, State$2 as State, StringTable, StringsLoader, TypedTr };
336
+ }
337
+ /**
338
+ * One interface element handed to a message as a parameter. Structural on purpose: it mirrors
339
+ * the `DescriptionNode` of `world/projection`, which sits beside `i18n`, so neither plugin
340
+ * imports the other (seam resolution 1).
278
341
  *
279
342
  * @example
280
343
  * ```ts
281
- * const tapped: Entity[] = [];
282
- * const listener: TapListener = entity => tapped.push(entity);
344
+ * const icon: ElementNode = { type: "icon", props: { name: "hud.coin" }, children: [] };
283
345
  * ```
284
346
  */
285
- type TapListener = (entity: Entity) => void;
347
+ type ElementNode = {
348
+ type: string;
349
+ key?: string;
350
+ props: object;
351
+ children: readonly unknown[];
352
+ };
286
353
  /**
287
- * What `onPointer` registers: called with the raw sample inside the DOM listener, for the kinds
288
- * `down`, `up` and `cancel`, right after the sample is queued.
354
+ * What a message parameter may be: a word, a number, a list of words, or an element the
355
+ * interface draws in the middle of the sentence.
289
356
  *
290
357
  * @example
291
358
  * ```ts
292
- * const lifts: RawSample[] = [];
293
- * const listener: PointerListener = sample => {
294
- * if (sample.kind === "up") lifts.push(sample);
295
- * };
359
+ * const reward: Argument = 25;
360
+ * const players: Argument = ["Ann", "Bo"];
296
361
  * ```
297
362
  */
298
- type PointerListener = (sample: RawSample) => void;
363
+ type Argument = string | number | readonly string[] | ElementNode;
299
364
  /**
300
- * One key press as `onKey` hands it to a listener: the DOM `KeyboardEvent.key` and whether Shift
301
- * was held.
365
+ * What `tr` returns: the key and the parameters, frozen. No locale and no string is read at the
366
+ * call site, so the same value survives a locale change and can be compared and logged.
302
367
  *
303
368
  * @example
304
369
  * ```ts
305
- * const backTab: KeyInput = { key: "Tab", shift: true };
370
+ * const orders: Message<"hud.orders", { n: number }> = { key: "hud.orders", params: { n: 3 } };
306
371
  * ```
307
372
  */
308
- type KeyInput = {
309
- key: string;
310
- shift: boolean;
373
+ type Message<Key extends string = string, Values = Record<string, Argument>> = {
374
+ key: Key;
375
+ params?: Values;
311
376
  };
312
377
  /**
313
- * What `onKey` registers. Returning `true` marks the key handled: input then calls
314
- * `preventDefault()` on the DOM event, and `app.input.pressKey` answers `true`.
378
+ * One piece of a formatted message: a run of text, or an element to draw in place. The consumer
379
+ * never joins the pieces itself, which is what keeps an icon inside a sentence possible.
315
380
  *
316
381
  * @example
317
382
  * ```ts
318
- * const closeOnEscape: KeyListener = key => key.key === "Escape";
383
+ * const parts: Part[] = [
384
+ * { kind: "element", node: { type: "icon", props: { name: "hud.coin" }, children: [] } },
385
+ * { kind: "text", text: " 25" }
386
+ * ];
319
387
  * ```
320
388
  */
321
- type KeyListener = (key: KeyInput) => boolean | void;
389
+ type Part = {
390
+ kind: "text";
391
+ text: string;
392
+ } | {
393
+ kind: "element";
394
+ node: ElementNode;
395
+ };
322
396
  /**
323
- * input plugin config. Every distance is in reference px, every duration in milliseconds of
324
- * `time`, so a time scale and a pause apply to the gestures too.
397
+ * The `Intl.*` formatters of one locale, each memoised by the JSON of its options. Compiled
398
+ * messages are handed one and never construct a formatter themselves.
325
399
  *
326
400
  * @example
327
401
  * ```ts
328
- * createApp({ plugins: [...screen], pluginConfigs: { input: { longPressMs: 300, swipeMinPx: 64 } } });
329
- * // A merge game carries the item in the hand 8% bigger: a board item resting at scale 0.5 on
330
- * // screen is drawn at 0.54 from the grab to the drop, even after a press squashed it, then
331
- * // flies home at 0.5.
332
- * createApp({ plugins: [...screen], pluginConfigs: { input: { heldScale: 1.08 } } });
333
- * // A game with its own cursors: the config merges shallowly, so both values are given.
334
- * createApp({ plugins: [...screen], pluginConfigs: { input: { cursor: { control: "grab", idle: "default" } } } });
335
- * // A word grid with 40 px cells: the finger's path is hit-tested every 16 px, below the
336
- * // shortest cell span, so a fast finger skips no letter.
337
- * createApp({ plugins: [...screen], pluginConfigs: { input: { traceStepPx: 16 } } });
402
+ * const intl = createIntlKit("ru");
403
+ * intl.plural().select(3); // "few"
338
404
  * ```
339
405
  */
340
- type Config$2 = {
341
- /** 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. */
342
- longPressMs: number; /** Move, in reference px, that turns a press on a `Draggable` into a drag. */
343
- dragStartPx: number; /** Shortest swipe, in reference px. */
344
- swipeMinPx: number; /** Longest swipe, in ms of `time`, from pointer down to pointer up. */
345
- swipeMaxMs: number;
406
+ type IntlKit = {
407
+ /** The locale every formatter of this kit is built for. */locale: string; /** The plural rules of the locale; `{ type: "ordinal" }` for `selectordinal`. */
408
+ plural(options?: Intl.PluralRulesOptions): Intl.PluralRules; /** The number format of the locale. */
409
+ number(options?: Intl.NumberFormatOptions): Intl.NumberFormat; /** The list format of the locale, which ICU MessageFormat 1 has no syntax for. */
410
+ list(options?: Intl.ListFormatOptions): Intl.ListFormat; /** The date and time format of the locale. */
411
+ date(options?: Intl.DateTimeFormatOptions): Intl.DateTimeFormat;
346
412
  /**
347
- * How much bigger the view in the hand is drawn: its rest scale in root space times this, from
348
- * the grab to the release, so a squash a press left on it does not shrink the lift. A view with
349
- * no recorded rest starts from its scale at the grab. The drop, the cancel and the abort write
350
- * the rest scale back, so the way home starts at the view's own size. `1` changes nothing.
413
+ * The duration format of the locale. A compiled `{left, duration}` reads it with
414
+ * `secondsDisplay: "always"`, so a countdown at zero still says "0 sec".
415
+ *
416
+ * @param options - The `Intl.DurationFormat` options.
417
+ * @returns The memoised formatter for these options.
418
+ * @throws {Error} When the runtime has no `Intl.DurationFormat`.
419
+ * @example
420
+ * ```ts
421
+ * const intl = createIntlKit("en");
422
+ * intl.duration({ style: "short" }).format({ minutes: 1, seconds: 35 }); // "1 min, 35 sec"
423
+ * ```
351
424
  */
352
- heldScale: number;
353
- /**
354
- * CSS cursors of the canvas. `control` shows while a mouse or a pen rests on a view that
355
- * carries `Tappable`, `Draggable`, `Pressable`, `Swipeable` or a component another plugin
356
- * registered through `controls.add`, such as ui's `LocalWrite`; `idle` shows everywhere else.
357
- * `""` hands the cursor back to the page's own style.
358
- */
359
- cursor: {
360
- control: string;
361
- idle: string;
362
- };
363
- /**
364
- * Reference px between two hit tests along the finger's path inside one frame of a trace. Keep
365
- * it below the shortest cell span: a longer step jumps over a cell.
366
- */
367
- traceStepPx: number;
425
+ duration(options?: Intl.DurationFormatOptions): Intl.DurationFormat;
426
+ };
427
+ /**
428
+ * How a duration reads: `"long"` (1 minute, 35 seconds), `"short"` (1 min, 35 sec), `"narrow"`
429
+ * (1m 35s) or `"digital"` (0:01:35). A string file names one in `{left, duration, short}`.
430
+ *
431
+ * @example
432
+ * ```ts
433
+ * const style: DurationStyle = "digital"; // app.i18n.duration(95_000, style) is "0:01:35"
434
+ * ```
435
+ */
436
+ type DurationStyle = "long" | "short" | "narrow" | "digital";
437
+ /**
438
+ * One message as the build wrote it: a plain function of the parameters and the locale's kit.
439
+ * There is no ICU parser at run time.
440
+ *
441
+ * @example
442
+ * ```ts
443
+ * const hello: CompiledMessage = p => [{ kind: "text", text: `Hi ${String(p.name)}` }];
444
+ * ```
445
+ */
446
+ type CompiledMessage = (params: Record<string, unknown>, intl: IntlKit) => Part[];
447
+ /**
448
+ * One compiled module: every message of one locale by its key.
449
+ *
450
+ * @example
451
+ * ```ts
452
+ * const en: CompiledMessages = { "home.play": () => [{ kind: "text", text: "Play" }] };
453
+ * ```
454
+ */
455
+ type CompiledMessages = Record<string, CompiledMessage>;
456
+ /**
457
+ * A compiled module that is imported on first use. A module namespace is unwrapped to its
458
+ * `default` member (seam resolution 2).
459
+ *
460
+ * @example
461
+ * ```ts
462
+ * const loadEnglish: StringsLoader = () => import("./generated/strings.en");
463
+ * ```
464
+ */
465
+ type StringsLoader = () => Promise<CompiledMessages | {
466
+ default: CompiledMessages;
467
+ }>;
468
+ /**
469
+ * i18n plugin config.
470
+ *
471
+ * @example
472
+ * ```ts
473
+ * createApp({ pluginConfigs: { i18n: { locale: "ru", fallback: "en", locales: {} } } });
474
+ * ```
475
+ */
476
+ type Config$2 = {
477
+ /** The locale at start. */locale: string; /** The locale a missing key is read from before it is reported missing. */
478
+ fallback: string;
368
479
  /**
369
- * The hit circle of a trace cell: its radius is this times the short side of the cell's hit
370
- * box, around the box centre. A diagonal then crosses no corner of a neighbour.
480
+ * Compiled modules outside features, per locale; a loader is imported on first use and its
481
+ * `default` is unwrapped.
371
482
  */
372
- traceInset: number;
483
+ locales: Record<string, CompiledMessages | StringsLoader>;
373
484
  };
374
485
  /**
375
- * One view a drag carries on top of the held one: hung under the held view from the grab to the
376
- * release.
486
+ * One module registered for one locale, with the name it can be blamed under when two of them
487
+ * carry the same key.
377
488
  */
378
- type Carried = {
379
- /** The carried view. */entity: Entity; /** The `Parent` it had at the grab, hung back under it on the release. */
380
- parent: Entity | undefined; /** The remover `world.projection.mute` returned for its root pose. */
381
- unmute: () => void;
489
+ type RegisteredModule = {
490
+ /** The feature that brought the module, or `"pluginConfigs.i18n"` for a configured one. */from: string; /** The module itself, or the loader that fetches it on first use. */
491
+ messages: CompiledMessages | StringsLoader;
382
492
  };
383
493
  /**
384
- * input plugin state: the raw queue the DOM handlers write, and the one gesture in progress.
494
+ * One registered module whose loader already ran, ready to be merged into a locale.
495
+ */
496
+ type ResolvedModule = {
497
+ /** The feature that brought the module, or `"pluginConfigs.i18n"` for a configured one. */from: string; /** The module itself. */
498
+ messages: CompiledMessages;
499
+ };
500
+ /**
501
+ * i18n plugin state.
385
502
  */
386
503
  type State$2 = {
387
- /** Written by the DOM handlers, drained by the frame step. */samples: RawSample[];
388
- phase: GesturePhase; /** The one active pointer. A sample of any other pointer is dropped while this is set. */
389
- pointerId: number | undefined; /** The pressed or held entity. */
390
- entity: Entity | undefined; /** The projection key of `entity`, read once at press time. */
391
- key: {
392
- projection: string;
393
- key: string;
394
- } | undefined; /** Reference px at pointer down. */
395
- start: Point$1; /** `Transform` minus the pointer at the moment of the grab. */
396
- grabOffset: Point$1; /** Sum of `time.delta` since pointer down. No device clock is ever read. */
397
- pressedMs: number; /** The drop target that carries `Hovered` during a drag. */
398
- hovered: Entity | undefined; /** The view that carries `PointerOver`: a press would take it, and a mouse or a pen is over it. */
399
- pointerOver: Entity | undefined; /** The `Parent` the held view had at the grab. It is hung back under it on the release. */
400
- parent: Entity | undefined; /** The rest scale of the held view in root space: the base of `heldScale`, set back on drop. */
401
- restScale: number | undefined;
402
- /**
403
- * The root pose of the held view, written at the grab and at every move. A carried view is
404
- * placed from it when the held view despawns before the release and takes its `Transform`.
405
- */
406
- heldPose: TransformValue | undefined; /** The remover `world.projection.mute` returned, while a drag runs. */
407
- unmute: (() => void) | undefined; /** The views a drag carries on top of the held one, in `carry` order; empty outside a drag. */
408
- carried: Carried[]; /** The cells of the trace in progress, in path order; empty outside a trace. */
409
- path: Entity[]; /** The intent of the first cell of the trace; a cell of another intent closes the path. */
410
- traceIntent: string | undefined; /** True once a cell of another intent was crossed: the path neither grows nor pops. */
411
- traceClosed: boolean; /** Where the finger was at the last frame of the trace: the segment starts there. */
412
- lastPoint: Point$1 | undefined;
413
- canvas: HTMLCanvasElement | undefined;
414
- offFrame: (() => void) | undefined;
415
- detach: (() => void) | undefined; /** Registered through `onTap`, called in this order on every tap. */
416
- tapListeners: TapListener[]; /** Registered through `onKey`, called in this order on every key. */
417
- keyListeners: KeyListener[]; /** Registered through `onPointer`, called in this order inside the DOM listener. */
418
- pointerListeners: PointerListener[]; /** Takes the one `keydown` listener off `window`; set while a canvas is attached. */
419
- detachKeys: (() => void) | undefined; /** `time.wake`, bound in `onInit`: every pointer sample leaves the idle frame rate. */
420
- wake: (() => void) | undefined; /** The cursor last written on the attached canvas; `undefined` while nothing was written. */
421
- cursor: string | undefined; /** Component types other plugins registered through `controls.add`, once per registration. */
422
- controls: AnyComponentType[];
504
+ /** The current locale; `config.locale` until `setLocale`. */locale: string; /** Per locale, one entry per feature and one for the config, filled in `onStart`. */
505
+ registered: Map<string, RegisteredModule[]>; /** Merged and resolved modules per locale. */
506
+ loaded: Map<string, CompiledMessages>; /** One kit per locale, built on first use. */
507
+ intl: Map<string, IntlKit>; /** Keys already reported missing, so each warns once. */
508
+ warned: Set<string>; /** The `setLocale` in flight; a later call waits for it. */
509
+ loading: Promise<void> | undefined;
423
510
  };
424
511
  /**
425
- * input plugin API, `app.input`: the same door for the finger, for a test and for an agent. Each
426
- * method builds the Answer the gesture would build and hands it to `flow.gate.answer`. Nothing
427
- * moves: no coordinates, no frames, no `Held`, no `settle`.
512
+ * i18n plugin events.
513
+ */
514
+ type Events = {
515
+ /** The current locale changed, or its messages were replaced. */"i18n:locale-changed": {
516
+ locale: string;
517
+ };
518
+ };
519
+ /**
520
+ * i18n plugin API, `app.i18n`. Messages are data everywhere else in the game; this is the one
521
+ * place a locale is read and a sentence becomes parts.
428
522
  *
429
523
  * @example
430
524
  * ```ts
431
- * // A headless test plays the board without a screen.
432
- * app.input.tap({ projection: "board.generators", key: "g1" }); // true: the generator spat an item
433
- * app.input.drag({ projection: "board.items", key: "i5" }, { projection: "board.items", key: "i7" });
525
+ * // A settings screen shows the languages the game was built with and switches to one.
526
+ * app.i18n.locales(); // ["en", "ru"]
527
+ * await app.i18n.setLocale("en");
528
+ * app.i18n.plain(tr("hud.orders", { n: 3 })); // "3 orders"
434
529
  * ```
435
530
  */
436
- type InputApi = {
531
+ type I18nApi = {
437
532
  /**
438
- * Reads the `Tappable` of a view and answers its intent.
533
+ * The locale every `format` without a second argument reads.
439
534
  *
440
- * @param target - The projection key of a live view, or an entity.
441
- * @returns What `flow.gate.answer` returned: true when the gate took the answer.
535
+ * @returns The current locale.
442
536
  * @example
443
537
  * ```ts
444
- * // A hidden-object test finds one object without touching a pixel.
445
- * app.input.tap({ projection: "scene.objects", key: "lamp" });
446
- * // true: answers { intent: "found", payload: { id: "lamp" } }
538
+ * app.i18n.locale(); // "ru", the pluginConfigs.i18n.locale the game started with
447
539
  * ```
448
540
  */
449
- tap(target: Target): boolean;
541
+ locale(): string;
450
542
  /**
451
- * Reads the `Pressable` of a view and answers its intent, the way a long press would.
543
+ * Switches the current locale. A lazy module is imported once, then `i18n:locale-changed` is
544
+ * emitted; `text` re-resolves every message on it. A call for the locale already current
545
+ * resolves at once and emits nothing.
452
546
  *
453
- * @param target - The projection key of a live view, or an entity.
454
- * @returns What `flow.gate.answer` returned: true when the gate took the answer.
547
+ * @param locale - The locale to switch to.
548
+ * @returns A promise that settles when the messages of that locale are loaded.
549
+ * @throws {Error} When no feature and no config brought a module for the locale.
455
550
  * @example
456
551
  * ```ts
457
- * // An agent opens the info card of an item without holding a finger for 450 ms.
458
- * app.input.press({ projection: "board.items", key: "i5" });
459
- * // true: answers { intent: "info", payload: { id: "i5" } }
552
+ * // features/settings/nodes.ts: the language button of the settings screen.
553
+ * await app.i18n.setLocale("en"); // loads the compiled module, then emits i18n:locale-changed
554
+ * app.i18n.locale(); // "en"
460
555
  * ```
461
556
  */
462
- press(target: Target): boolean;
557
+ setLocale(locale: string): Promise<void>;
463
558
  /**
464
- * Reads the `Draggable` of the source and the `DropTarget` of the destination and answers the
465
- * destination's intent with both payloads merged. The destination wins a key both carry.
559
+ * Formats a message in the current locale. Resolution order is the current locale, then
560
+ * `config.fallback`, then missing: a missing key gives one text part `⟦key⟧` and one warning.
466
561
  *
467
- * @param from - The view that is carried.
468
- * @param to - The view it is dropped on; this one names the intent.
469
- * @returns What `flow.gate.answer` returned: true when the gate took the answer.
562
+ * @param message - What `tr` returned.
563
+ * @returns The parts of the sentence, adjacent text merged.
470
564
  * @example
471
565
  * ```ts
472
- * // The merge test of the fixture game: two items of level 1 become one of level 2.
473
- * app.input.drag({ projection: "board.items", key: "i5" }, { projection: "board.items", key: "i7" });
474
- * // true: answers { intent: "merge", payload: { from: "c2", to: "c3" } }
566
+ * // text resolves the `content` of a Text component every time the locale changes.
567
+ * app.i18n.format(tr("hud.orders", { n: 3 })); // [{ kind: "text", text: "3 заказа" }]
475
568
  * ```
476
569
  */
477
- drag(from: Target, to: Target): boolean;
570
+ format(message: Message): readonly Part[];
478
571
  /**
479
- * Reads the `Swipeable` of a view and answers its intent with the direction in the payload.
572
+ * Formats a message in another registered locale, without switching. `ui.lint` measures a
573
+ * label in every locale this way.
480
574
  *
481
- * @param target - The projection key of a live view, or an entity.
482
- * @param direction - Which way the finger would have gone.
483
- * @returns What `flow.gate.answer` returned: true when the gate took the answer.
575
+ * @param message - What `tr` returned.
576
+ * @param locale - The registered locale to read.
577
+ * @returns The parts of the sentence in that locale.
578
+ * @throws {Error} When the locale is not registered, or is registered but not loaded yet.
484
579
  * @example
485
580
  * ```ts
486
- * // A match-3 test swaps the cell c2 with its right neighbour.
487
- * app.input.swipe({ projection: "board.cells", key: "c2" }, "right");
488
- * // true: answers { intent: "swap", payload: { cell: "c2", direction: "right" } }
581
+ * // ui.lint does this for every entry of app.i18n.locales() and keeps the widest result.
582
+ * app.i18n.format(tr("hud.orders", { n: 3 }), "en"); // [{ kind: "text", text: "3 orders" }]
583
+ * app.i18n.locale(); // "ru": reading another locale never switches
489
584
  * ```
490
585
  */
491
- swipe(target: Target, direction: Direction): boolean;
586
+ format(message: Message, locale: string): readonly Part[];
492
587
  /**
493
- * Reads the `Traceable` of every cell and answers one path, the way a finger drawn through the
494
- * cells would: the intent of the cells and `{ path }`, the cell payloads in order. Every cell
495
- * must name the same intent. An empty list, a cell with no `Traceable` and two intents warn and
496
- * answer `false` without calling the gate. Nothing is tagged `Traced`.
588
+ * The parts joined into one string, elements dropped. For logs, tests and `flow.describe()`.
497
589
  *
498
- * @param path - The cells in the order the finger goes through them.
499
- * @returns What `flow.gate.answer` returned: true when the gate took the answer.
590
+ * @param message - What `tr` returned.
591
+ * @returns The sentence as text.
500
592
  * @example
501
593
  * ```ts
502
- * // A word-game test spells a word without a finger.
503
- * app.input.trace([
504
- * { projection: "board.cells", key: "b3" },
505
- * { projection: "board.cells", key: "c3" },
506
- * { projection: "board.cells", key: "d3" }
507
- * ]);
508
- * // true: answers { intent: "word", payload: { path: [{ cell: "b3" }, { cell: "c3" }, { cell: "d3" }] } }
594
+ * // A test asserts what the HUD says without touching the renderer.
595
+ * app.i18n.plain(tr("hud.coins", { n: 25, icon: coinIcon })); // "25": the icon is dropped
509
596
  * ```
510
597
  */
511
- trace(path: readonly Target[]): boolean;
598
+ plain(message: Message): string;
512
599
  /**
513
- * Registers a listener called on every tap — a press and a release inside `tapSlopPx` — on the
514
- * topmost view the hit test accepted, before the `Tappable` answer. A view that carries only
515
- * `Touchable` reaches the listeners and answers nothing. A listener that throws is logged with
516
- * its entity, and the listeners after it still run.
600
+ * Tells whether a key exists in the current locale or in the fallback.
517
601
  *
518
- * @param fn - What to run with the tapped entity.
519
- * @returns The remover; call it to stop listening.
602
+ * @param key - The message key.
603
+ * @returns True when `format` would find a message for it.
520
604
  * @example
521
605
  * ```ts
522
- * // ui applies the local patch of the button the finger tapped.
523
- * const off = ctx.require(inputPlugin).onTap(entity => {
524
- * const write = ctx.require(worldPlugin).ecs.get(entity, LocalWrite);
525
- *
526
- * if (write !== undefined) applyLocal(entity, write.patch);
527
- * });
528
- * off(); // in onStop
606
+ * // A projection hides the label of an order that has no description yet.
607
+ * app.i18n.has("orders.hint"); // false: no locale brought that key
529
608
  * ```
530
609
  */
531
- onTap(fn: TapListener): () => void;
532
- /**
533
- * Registers a listener called synchronously inside the DOM pointer listener, right after the
534
- * sample is queued, for `pointerdown`, `pointerup` and `pointercancel`. It is the one place a
535
- * browser call that needs the user gesture can run, such as `focus()` on a text field: in the
536
- * next frame iOS shows no keyboard. Never called for a move, a leave or a lost capture, and never
537
- * by `tap`, `press`, `drag` or `swipe`, which have no DOM moment. The sample is raw: client px,
538
- * no hit test. Listeners run in registration order; a listener that throws is logged with the
539
- * sample kind, and the listeners after it still run.
540
- *
541
- * @param fn - What to run with the raw sample.
542
- * @returns The remover; call it to stop listening.
543
- * @example
544
- * ```ts
545
- * // ui opens the keyboard the moment the finger lifts on a text field, inside the DOM listener.
546
- * const off = ctx.require(inputPlugin).onPointer(sample => {
547
- * if (sample.kind === "up" && fieldAt(sample.clientX, sample.clientY) !== undefined) element.focus({ preventScroll: true });
548
- * if (sample.kind === "down" && fieldAt(sample.clientX, sample.clientY) === undefined) element.blur();
549
- * });
550
- * // a tap on the Rename field on an iPhone: the keyboard shows; a tap on the parchment next to it: the keyboard hides
551
- * off(); // in onStop
552
- * ```
553
- */
554
- onPointer(fn: PointerListener): () => void;
610
+ has(key: string): boolean;
555
611
  /**
556
- * Registers a listener called on every key pressed while the canvas is attached, and on every
557
- * `pressKey` call. A key typed into a text field is not handed over, except Enter and Escape,
558
- * and an Enter that commits an IME composition is dropped. Listeners run in registration order,
559
- * all of them, each time. A listener that
560
- * returns `true` marks the key handled, and input calls `preventDefault()` on the DOM event. A
561
- * listener that throws is logged with its key, and the listeners after it still run.
612
+ * Formats milliseconds in the current locale through `Intl.DurationFormat`. The value is rounded
613
+ * up to whole seconds, so a countdown never reads zero while time is left; a negative or
614
+ * non-finite value reads as zero; the seconds are always shown. `text` formats a field bound
615
+ * with `format: "duration"` through this call.
562
616
  *
563
- * @param fn - What to run with the key; return `true` when it handled it.
564
- * @returns The remover; call it to stop listening.
617
+ * @param ms - The duration in milliseconds.
618
+ * @param style - How the duration reads. Default `"short"`.
619
+ * @returns The duration as text.
620
+ * @throws {Error} When the runtime has no `Intl.DurationFormat`.
565
621
  * @example
566
622
  * ```ts
567
- * // ui moves the keyboard focus on Tab and keeps the browser from leaving the canvas.
568
- * const off = ctx.require(inputPlugin).onKey(key => {
569
- * if (key.key !== "Tab") return false;
570
- * moveFocus(key.shift ? -1 : 1);
571
- *
572
- * return true;
573
- * });
574
- * off(); // in onStop
623
+ * // The out-of-energy popup shows when the next refill comes, locale "en".
624
+ * app.i18n.duration(95_000); // "1 min, 35 sec"
625
+ * app.i18n.duration(95_000, "digital"); // "0:01:35"
626
+ * app.i18n.duration(500); // "1 sec": rounded up
575
627
  * ```
576
628
  */
577
- onKey(fn: KeyListener): () => void;
629
+ duration(ms: number, style?: DurationStyle): string;
578
630
  /**
579
- * Presses a key without a keyboard: runs the `onKey` listeners exactly as a DOM `keydown` does.
631
+ * Every locale at least one feature or the config brought a module for, sorted.
580
632
  *
581
- * @param key - The DOM `KeyboardEvent.key`, such as `"Tab"`, `"Enter"`, `" "` or `"Escape"`.
582
- * @param options - How the key is pressed.
583
- * @param options.shift - True presses it with Shift held; the default is `false`.
584
- * @returns True when a listener handled the key.
633
+ * @returns The registered locales.
585
634
  * @example
586
635
  * ```ts
587
- * // A headless test walks the settings popup backwards and closes it.
588
- * app.input.pressKey("Tab", { shift: true }); // true: ui moved the focus to the previous control
589
- * app.input.pressKey("Escape"); // true: ui tapped the popup's close button
590
- * app.input.pressKey("q"); // false: no listener handles it
636
+ * // The language menu is built from this list, and ui.lint measures the widest locale.
637
+ * app.i18n.locales(); // ["en", "ru"]
591
638
  * ```
592
639
  */
593
- pressKey(key: string, options?: {
594
- shift?: boolean;
595
- }): boolean;
640
+ locales(): readonly string[];
596
641
  /**
597
- * The CSS cursor input set on the canvas: `config.cursor.control` while a mouse or a pen rests
598
- * on a control, `config.cursor.idle` otherwise and whenever nothing is attached.
642
+ * Swaps the compiled module of one feature in a loaded locale, for the dev hot swap. The one
643
+ * registered module whose keys overlap `messages` takes them, the locale is merged again and
644
+ * every key may warn once more. When the locale is the current one or the fallback,
645
+ * `i18n:locale-changed` is emitted with the current locale, so `text` re-resolves every label.
646
+ * A locale not loaded yet is left alone: its loader imports the new module later.
599
647
  *
600
- * @returns The cursor value.
648
+ * @param locale - The locale of the saved `generated/strings.<locale>.ts`.
649
+ * @param messages - The `default` export of the new module.
650
+ * @throws {Error} When no registered module, or more than one, shares a key with `messages`.
601
651
  * @example
602
652
  * ```ts
603
- * // An e2e check: the mouse rests on the Play button of the home screen.
604
- * app.input.cursor(); // "pointer"
605
- * // The mouse moves to the empty sky above it.
606
- * app.input.cursor(); // ""
653
+ * // ui's hot swap: generated/strings.ru.ts was written again with "hud.orders" as "3 заказа!".
654
+ * ctx.require(i18nPlugin).replace("ru", next.default); // emits i18n:locale-changed
655
+ * app.i18n.plain(tr("hud.orders", { n: 3 })); // "3 заказа!": text redraws the label
607
656
  * ```
608
657
  */
609
- cursor(): string;
610
- /**
611
- * The components that make a view a control besides input's own: the cursor shows
612
- * `config.cursor.control` over a view that carries one of them.
613
- */
614
- controls: {
615
- /**
616
- * Registers a component type of another plugin as a control. A type registered twice stays
617
- * a control until both removers ran.
618
- *
619
- * @param component - The component or tag type that makes a view answer a press.
620
- * @returns The remover; call it to take the registration back.
621
- * @example
622
- * ```ts
623
- * // ui counts its local-state buttons as controls, so the mouse over a tab shows a hand.
624
- * const off = ctx.require(inputPlugin).controls.add(LocalWrite);
625
- * app.input.cursor(); // "pointer" while the mouse rests on the Audio tab
626
- * off(); // in onStop
627
- * ```
628
- */
629
- add(component: AnyComponentType): () => void;
630
- };
631
- };
632
- /**
633
- * Resolved dependency APIs.
634
- */
635
- type Deps$1 = {
636
- time: Api;
637
- flow: Api$1;
638
- world: Api$3;
639
- renderer: Api$4;
658
+ replace(locale: string, messages: CompiledMessages): void;
640
659
  };
641
660
  /**
642
- * What the kernel context offers before the deps are attached.
661
+ * What the kernel context offers this plugin.
643
662
  *
644
- * `input` owns no event, so `emit` is the kernel's and never called here.
663
+ * `index.ts` writes `events` with an annotated `register` (core spec `14-EVENT-REGISTRATION.md`
664
+ * row 8), so the own event reaches the context the kernel hands the factories and `emit` is the
665
+ * kernel's own, i18n-typed one. No member of the context is cast.
645
666
  */
646
- type KernelSlice$1 = PluginCtx<Config$2, State$2> & {
667
+ type I18nCtx = PluginCtx<Config$2, State$2, Events> & {
647
668
  readonly global: object;
648
669
  readonly log: Log.LogApi;
649
670
  readonly require: Require;
650
671
  };
651
672
  /**
652
- * Domain context of the input plugin: the kernel slice plus the four resolved dependencies.
653
- */
654
- type InputCtx = KernelSlice$1 & {
655
- readonly deps: Deps$1;
656
- };
657
- declare namespace types_d_exports$1 {
658
- 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 };
659
- }
660
- /**
661
- * One interface element handed to a message as a parameter. Structural on purpose: it mirrors
662
- * the `DescriptionNode` of `world/projection`, which sits beside `i18n`, so neither plugin
663
- * imports the other (seam resolution 1).
673
+ * The key-to-parameters table a game generates: `generated/strings.ts` exports it as `Strings`.
674
+ * A key with no parameters carries `Record<never, never>`.
664
675
  *
665
676
  * @example
666
677
  * ```ts
667
- * const icon: ElementNode = { type: "icon", props: { name: "hud.coin" }, children: [] };
678
+ * const table: StringTable = { "hud.orders": { n: 3 }, "orders.complete": {} };
668
679
  * ```
669
680
  */
670
- type ElementNode = {
671
- type: string;
672
- key?: string;
673
- props: object;
674
- children: readonly unknown[];
675
- };
681
+ type StringTable = Record<string, unknown>;
676
682
  /**
677
- * What a message parameter may be: a word, a number, a list of words, or an element the
678
- * interface draws in the middle of the sentence.
683
+ * The keys of a table whose message takes no parameters, so `tr` may be called with the key
684
+ * alone.
679
685
  *
680
686
  * @example
681
687
  * ```ts
682
- * const reward: Argument = 25;
683
- * const players: Argument = ["Ann", "Bo"];
688
+ * type Keys = KeysWithoutParameters<{ "home.play": Record<never, never>; "hud.orders": { n: number } }>;
689
+ * // "home.play"
684
690
  * ```
685
691
  */
686
- type Argument = string | number | readonly string[] | ElementNode;
692
+ type KeysWithoutParameters<Table extends StringTable> = { [Key in keyof Table]: Record<never, never> extends Table[Key] ? Key : never }[keyof Table] & string;
687
693
  /**
688
- * What `tr` returns: the key and the parameters, frozen. No locale and no string is read at the
689
- * call site, so the same value survives a locale change and can be compared and logged.
694
+ * `tr` as one game reads it: the key is checked against the generated table, and the parameters
695
+ * are the ones that key declares. A key with no parameters takes no second argument.
690
696
  *
691
697
  * @example
692
698
  * ```ts
693
- * const orders: Message<"hud.orders", { n: number }> = { key: "hud.orders", params: { n: 3 } };
699
+ * const { tr } = i18nFor<{ "hud.orders": { n: number } }>();
700
+ * tr("hud.orders", { n: 3 }); // { key: "hud.orders", params: { n: 3 } }
694
701
  * ```
695
702
  */
696
- type Message<Key extends string = string, Values = Record<string, Argument>> = {
697
- key: Key;
698
- params?: Values;
703
+ type TypedTr<Table extends StringTable> = {
704
+ <Key extends KeysWithoutParameters<Table>>(key: Key): Message<Key, Table[Key]>;
705
+ <Key extends keyof Table & string>(key: Key, params: Table[Key]): Message<Key, Table[Key]>;
699
706
  };
700
707
  /**
701
- * One piece of a formatted message: a run of text, or an element to draw in place. The consumer
702
- * never joins the pieces itself, which is what keeps an icon inside a sentence possible.
708
+ * What `defineGame` spreads from this plugin.
703
709
  *
704
710
  * @example
705
711
  * ```ts
706
- * const parts: Part[] = [
707
- * { kind: "element", node: { type: "icon", props: { name: "hud.coin" }, children: [] } },
708
- * { kind: "text", text: " 25" }
709
- * ];
712
+ * const kit: I18nKit<{ "home.play": Record<never, never> }> = i18nFor();
713
+ * kit.tr("home.play"); // { key: "home.play" }
710
714
  * ```
711
715
  */
712
- type Part = {
713
- kind: "text";
714
- text: string;
715
- } | {
716
- kind: "element";
717
- node: ElementNode;
716
+ type I18nKit<Table extends StringTable> = {
717
+ tr: TypedTr<Table>;
718
718
  };
719
+ //#endregion
720
+ //#region src/plugins/renderer/components.d.ts
719
721
  /**
720
- * The `Intl.*` formatters of one locale, each memoised by the JSON of its options. Compiled
721
- * messages are handed one and never construct a formatter themselves.
722
+ * Where a view sits, in reference units and radians. Relative to the `Parent` when there is one.
723
+ * `pivot` is the local point the view turns and scales around, and `x`, `y` is where that point
724
+ * lands; the default pivot `{ x: 0, y: 0 }` is the view's own origin.
722
725
  *
723
726
  * @example
724
727
  * ```ts
725
- * const intl = createIntlKit("ru");
726
- * intl.plural().select(3); // "few"
728
+ * // A 200 x 80 button that grows around its centre.
729
+ * const value: TransformValue = {
730
+ * x: 640, y: 340, rotation: 0, scale: 1.1, pivot: { x: 100, y: 40 }
731
+ * };
727
732
  * ```
728
733
  */
729
- type IntlKit = {
730
- /** The locale every formatter of this kit is built for. */locale: string; /** The plural rules of the locale; `{ type: "ordinal" }` for `selectordinal`. */
731
- plural(options?: Intl.PluralRulesOptions): Intl.PluralRules; /** The number format of the locale. */
732
- number(options?: Intl.NumberFormatOptions): Intl.NumberFormat; /** The list format of the locale, which ICU MessageFormat 1 has no syntax for. */
733
- list(options?: Intl.ListFormatOptions): Intl.ListFormat; /** The date and time format of the locale. */
734
- date(options?: Intl.DateTimeFormatOptions): Intl.DateTimeFormat;
735
- /**
736
- * The duration format of the locale. A compiled `{left, duration}` reads it with
737
- * `secondsDisplay: "always"`, so a countdown at zero still says "0 sec".
738
- *
739
- * @param options - The `Intl.DurationFormat` options.
740
- * @returns The memoised formatter for these options.
741
- * @throws {Error} When the runtime has no `Intl.DurationFormat`.
742
- * @example
743
- * ```ts
744
- * const intl = createIntlKit("en");
745
- * intl.duration({ style: "short" }).format({ minutes: 1, seconds: 35 }); // "1 min, 35 sec"
746
- * ```
747
- */
748
- duration(options?: Intl.DurationFormatOptions): Intl.DurationFormat;
734
+ type TransformValue = {
735
+ x: number;
736
+ y: number;
737
+ rotation: number;
738
+ scale: number;
739
+ pivot: Point$2;
749
740
  };
750
741
  /**
751
- * How a duration reads: `"long"` (1 minute, 35 seconds), `"short"` (1 min, 35 sec), `"narrow"`
752
- * (1m 35s) or `"digital"` (0:01:35). A string file names one in `{left, duration, short}`.
742
+ * How a sized sprite fills its box: `"fill"` stretches, `"contain"` scales uniformly inside the
743
+ * box and centres, `"cover"` scales uniformly over the box and crops the overflow.
753
744
  *
754
745
  * @example
755
746
  * ```ts
756
- * const style: DurationStyle = "digital"; // app.i18n.duration(95_000, style) is "0:01:35"
747
+ * const fit: SpriteFit = "cover";
757
748
  * ```
758
749
  */
759
- type DurationStyle = "long" | "short" | "narrow" | "digital";
750
+ type SpriteFit = "fill" | "contain" | "cover";
760
751
  /**
761
- * One message as the build wrote it: a plain function of the parameters and the locale's kit.
762
- * There is no ICU parser at run time.
752
+ * One textured quad. `texture` is an asset key; `defineGame` narrows it to the game's keys.
753
+ * `width` and `height` are the box in reference units; 0 on an axis keeps the texture's own size
754
+ * there. `anchor` is the point of the box that sits on the `Transform`. `alpha` fades the quad
755
+ * and the entity's children.
763
756
  *
764
757
  * @example
765
758
  * ```ts
766
- * const hello: CompiledMessage = p => [{ kind: "text", text: `Hi ${String(p.name)}` }];
759
+ * const value: SpriteValue = {
760
+ * texture: "board.bg-forest-meadow", tint: 0xffffff, alpha: 1, anchor: { x: 0, y: 0 },
761
+ * width: 1080, height: 1920, fit: "cover"
762
+ * };
767
763
  * ```
768
764
  */
769
- type CompiledMessage = (params: Record<string, unknown>, intl: IntlKit) => Part[];
765
+ type SpriteValue = {
766
+ texture: string;
767
+ tint: number;
768
+ alpha: number;
769
+ anchor: Point$2;
770
+ width: number;
771
+ height: number;
772
+ fit: SpriteFit;
773
+ };
770
774
  /**
771
- * One compiled module: every message of one locale by its key.
775
+ * A stretchable panel. The slice borders come with the texture, not with the component. `debug`
776
+ * strokes the bounds and the four cut lines over the panel: cyan, red when the corners overlap or
777
+ * the texture is missing. `clip` masks the children of the entity to the `width × height` box,
778
+ * the same rule as `Shape.clip`: the mask is a filled rectangle and is never drawn, and the panel
779
+ * itself is never masked. `alpha` fades the panel and the entity's children.
772
780
  *
773
781
  * @example
774
782
  * ```ts
775
- * const en: CompiledMessages = { "home.play": () => [{ kind: "text", text: "Play" }] };
783
+ * const value: NineSliceValue = {
784
+ * texture: "ui.panel", width: 600, height: 320, alpha: 1, tint: 0xffffff, debug: false,
785
+ * clip: false
786
+ * };
787
+ * // The board tray, outlined while its insets are checked.
788
+ * const tray: NineSliceValue = {
789
+ * ...value, texture: "board.board-tray", width: 1000, height: 1040, debug: true
790
+ * };
791
+ * // A text field: a long value never draws outside the frame.
792
+ * const field: NineSliceValue = {
793
+ * ...value, texture: "ui.field", width: 480, height: 96, clip: true
794
+ * };
776
795
  * ```
777
796
  */
778
- type CompiledMessages = Record<string, CompiledMessage>;
797
+ type NineSliceValue = {
798
+ texture: string;
799
+ width: number;
800
+ height: number;
801
+ alpha: number;
802
+ tint: number;
803
+ debug: boolean;
804
+ clip: boolean;
805
+ };
779
806
  /**
780
- * A compiled module that is imported on first use. A module namespace is unwrapped to its
781
- * `default` member (seam resolution 2).
807
+ * The escape hatch: the game owns a Pixi object and `sync` only attaches and detaches it.
782
808
  *
783
809
  * @example
784
810
  * ```ts
785
- * const loadEnglish: StringsLoader = () => import("./generated/strings.en");
811
+ * const value: DisplayValue = { object: spineAnimation };
786
812
  * ```
787
813
  */
788
- type StringsLoader = () => Promise<CompiledMessages | {
789
- default: CompiledMessages;
790
- }>;
814
+ type DisplayValue = {
815
+ object: unknown;
816
+ };
791
817
  /**
792
- * i18n plugin config.
818
+ * A filled rounded rectangle or a triangle, anchored at the top left of the `Transform`. `kind`
819
+ * picks the outline: `"rect"` (the default) or `"triangle"`, which fills its `w × h` box pointing
820
+ * right (rotate the element for another direction) and ignores `radius`. `fillAlpha` is the alpha
821
+ * of the fill alone: 0 draws only the stroke, a ring. `alpha` fades the whole shape and the
822
+ * entity's children: a disabled button at 0.6 draws its icon at 0.6 too. `dash` is the
823
+ * dash length of the stroke in reference units, with gaps of half a dash; 0 strokes a solid line.
824
+ * `clip` masks the children of the entity to the shape, always solid, which is how `ui` draws a
825
+ * scroll and an overflow. The shape itself is never masked, so its stroke is drawn whole.
793
826
  *
794
827
  * @example
795
828
  * ```ts
796
- * createApp({ pluginConfigs: { i18n: { locale: "ru", fallback: "en", locales: {} } } });
829
+ * const value: ShapeValue = {
830
+ * kind: "rect", w: 320, h: 96, fill: 0x101018, fillAlpha: 1, alpha: 1, radius: 16,
831
+ * stroke: 0x000000, strokeWidth: 0, dash: 0, clip: false
832
+ * };
833
+ * // The honey ring on a selected cell: a stroke and no fill.
834
+ * const ring: ShapeValue = {
835
+ * kind: "rect", w: 140, h: 140, fill: 0xffffff, fillAlpha: 0, alpha: 1, radius: 24,
836
+ * stroke: 0xffc233, strokeWidth: 6, dash: 0, clip: false
837
+ * };
838
+ * // The play glyph of a watch button, and a dashed focus ring: 10 u dashes, 5 u gaps.
839
+ * const play: ShapeValue = { ...ring, kind: "triangle", w: 36, h: 40, fillAlpha: 1, strokeWidth: 0 };
840
+ * const focus: ShapeValue = { ...ring, w: 200, h: 80, stroke: 0x3a2212, strokeWidth: 4, dash: 10 };
797
841
  * ```
798
842
  */
799
- type Config$1 = {
800
- /** The locale at start. */locale: string; /** The locale a missing key is read from before it is reported missing. */
801
- fallback: string;
802
- /**
803
- * Compiled modules outside features, per locale; a loader is imported on first use and its
804
- * `default` is unwrapped.
805
- */
806
- locales: Record<string, CompiledMessages | StringsLoader>;
843
+ type ShapeValue = {
844
+ kind: "rect" | "triangle";
845
+ w: number;
846
+ h: number;
847
+ fill: number;
848
+ fillAlpha: number;
849
+ alpha: number;
850
+ radius: number;
851
+ stroke: number;
852
+ strokeWidth: number;
853
+ dash: number;
854
+ clip: boolean;
807
855
  };
808
856
  /**
809
- * One module registered for one locale, with the name it can be blamed under when two of them
810
- * carry the same key.
857
+ * What `sprite()` takes: the two things every visual needs, and the four that have defaults.
858
+ *
859
+ * @example
860
+ * ```ts
861
+ * const options: SpriteOptions = { texture: "board.cell", at: { x: 540, y: 300 } };
862
+ * ```
811
863
  */
812
- type RegisteredModule = {
813
- /** The feature that brought the module, or `"pluginConfigs.i18n"` for a configured one. */from: string; /** The module itself, or the loader that fetches it on first use. */
814
- messages: CompiledMessages | StringsLoader;
864
+ type SpriteOptions = {
865
+ texture: string;
866
+ at: Point$2;
867
+ tint?: number;
868
+ alpha?: number;
869
+ anchor?: Point$2;
870
+ scale?: number;
815
871
  };
816
872
  /**
817
- * One registered module whose loader already ran, ready to be merged into a locale.
873
+ * Where a view sits: reference units, radians, uniform scale, and the local point it turns
874
+ * around.
818
875
  */
819
- type ResolvedModule = {
820
- /** The feature that brought the module, or `"pluginConfigs.i18n"` for a configured one. */from: string; /** The module itself. */
821
- messages: CompiledMessages;
822
- };
876
+ declare const Transform: ComponentType<TransformValue>;
823
877
  /**
824
- * i18n plugin state.
878
+ * One textured quad. `texture` is an asset key, resolved through the texture providers; a size
879
+ * and a `fit` draw it into a box.
825
880
  */
826
- type State$1 = {
827
- /** The current locale; `config.locale` until `setLocale`. */locale: string; /** Per locale, one entry per feature and one for the config, filled in `onStart`. */
828
- registered: Map<string, RegisteredModule[]>; /** Merged and resolved modules per locale. */
829
- loaded: Map<string, CompiledMessages>; /** One kit per locale, built on first use. */
830
- intl: Map<string, IntlKit>; /** Keys already reported missing, so each warns once. */
831
- warned: Set<string>; /** The `setLocale` in flight; a later call waits for it. */
832
- loading: Promise<void> | undefined;
881
+ declare const Sprite: ComponentType<SpriteValue>;
882
+ /**
883
+ * A stretchable panel, sized in reference units, with its own alpha and tint. `debug: true` draws
884
+ * the slice outline over it. `clip: true` masks the children of the entity to the panel box.
885
+ */
886
+ declare const NineSlice: ComponentType<{
887
+ texture: string;
888
+ width: number;
889
+ height: number;
890
+ alpha: number;
891
+ tint: number;
892
+ debug: boolean;
893
+ clip: boolean;
894
+ }>;
895
+ /**
896
+ * The entity this view moves with. `0` means no parent.
897
+ */
898
+ declare const Parent: ComponentType<{
899
+ entity: number;
900
+ }>;
901
+ /**
902
+ * A display object the game owns. Never pooled, never destroyed by `sync`.
903
+ */
904
+ declare const Display: ComponentType<DisplayValue>;
905
+ /**
906
+ * A filled rounded rectangle or a right-pointing triangle, drawn with Pixi `Graphics` and redrawn
907
+ * only when a field changed. `fillAlpha: 0` draws only the stroke, `dash` above 0 dashes it.
908
+ * `clip: true` masks the children of the entity to the shape.
909
+ */
910
+ declare const Shape: ComponentType<ShapeValue>;
911
+ /**
912
+ * Bundles the two components every visual needs, so a projection `view` reads as one line.
913
+ *
914
+ * @param options - Texture key, position, and the four values that have defaults.
915
+ * @returns The `Sprite` and the `Transform` value, in that order.
916
+ * @example
917
+ * ```ts
918
+ * sprite({ texture: "board.cell", at: { x: 540, y: 300 } });
919
+ * // [Sprite({ texture: "board.cell", tint: 0xffffff, alpha: 1, anchor: { x: 0.5, y: 0.5 },
920
+ * // width: 0, height: 0, fit: "fill" }),
921
+ * // Transform({ x: 540, y: 300, rotation: 0, scale: 1, pivot: { x: 0, y: 0 } })]
922
+ * ```
923
+ */
924
+ declare function sprite(options: SpriteOptions): [ComponentValue<SpriteValue>, ComponentValue<TransformValue>];
925
+ declare namespace types_d_exports$1 {
926
+ export { Carried, Config$1 as Config, Deps$1 as Deps, Direction, GesturePhase, InputApi, InputCtx, KernelSlice$1 as KernelSlice, KeyInput, KeyListener, Point$1 as Point, PointerListener, RawSample, State$1 as State, TapListener, Target };
927
+ }
928
+ /**
929
+ * A point in reference units, as `renderer.viewport.toReference` answers it.
930
+ *
931
+ * @example
932
+ * ```ts
933
+ * const point: Point = { x: 540, y: 960 };
934
+ * ```
935
+ */
936
+ type Point$1 = {
937
+ x: number;
938
+ y: number;
833
939
  };
834
940
  /**
835
- * i18n plugin events.
941
+ * The dominant axis of a swipe.
942
+ *
943
+ * @example
944
+ * ```ts
945
+ * const direction: Direction = "left";
946
+ * ```
836
947
  */
837
- type Events = {
838
- /** The current locale changed, or its messages were replaced. */"i18n:locale-changed": {
839
- locale: string;
840
- };
948
+ type Direction = "up" | "down" | "left" | "right";
949
+ /**
950
+ * What `app.input.*` is pointed at: the projection key of a live view, or an entity. A key never
951
+ * addresses a view in the despawn queue.
952
+ *
953
+ * @example
954
+ * ```ts
955
+ * const target: Target = { projection: "board.items", key: "i5" };
956
+ * ```
957
+ */
958
+ type Target = {
959
+ projection: string;
960
+ key: string;
961
+ } | Entity;
962
+ /**
963
+ * One pointer event as the DOM handlers queue it: no hit test, no reference units, no decision.
964
+ * `cancel` is a `pointercancel`, `lost` a lost pointer capture, `leave` the pointer leaving the
965
+ * canvas. `pointerType` is the device: only a mouse or a pen hovers.
966
+ *
967
+ * @example
968
+ * ```ts
969
+ * const sample: RawSample = {
970
+ * kind: "down", pointerType: "touch", pointerId: 1, clientX: 320, clientY: 640
971
+ * };
972
+ * ```
973
+ */
974
+ type RawSample = {
975
+ kind: "down" | "move" | "up" | "cancel" | "lost" | "leave";
976
+ pointerType: "mouse" | "touch" | "pen";
977
+ pointerId: number;
978
+ clientX: number;
979
+ clientY: number;
841
980
  };
842
981
  /**
843
- * i18n plugin API, `app.i18n`. Messages are data everywhere else in the game; this is the one
844
- * place a locale is read and a sentence becomes parts.
982
+ * Where the one active pointer stands in the gesture machine.
845
983
  *
846
984
  * @example
847
985
  * ```ts
848
- * // A settings screen shows the languages the game was built with and switches to one.
849
- * app.i18n.locales(); // ["en", "ru"]
850
- * await app.i18n.setLocale("en");
851
- * app.i18n.plain(tr("hud.orders", { n: 3 })); // "3 orders"
986
+ * const phase: GesturePhase = "dragging";
852
987
  * ```
853
988
  */
854
- type I18nApi = {
855
- /**
856
- * The locale every `format` without a second argument reads.
857
- *
858
- * @returns The current locale.
859
- * @example
860
- * ```ts
861
- * app.i18n.locale(); // "ru", the pluginConfigs.i18n.locale the game started with
862
- * ```
863
- */
864
- locale(): string;
865
- /**
866
- * Switches the current locale. A lazy module is imported once, then `i18n:locale-changed` is
867
- * emitted; `text` re-resolves every message on it. A call for the locale already current
868
- * resolves at once and emits nothing.
869
- *
870
- * @param locale - The locale to switch to.
871
- * @returns A promise that settles when the messages of that locale are loaded.
872
- * @throws {Error} When no feature and no config brought a module for the locale.
873
- * @example
874
- * ```ts
875
- * // features/settings/nodes.ts: the language button of the settings screen.
876
- * await app.i18n.setLocale("en"); // loads the compiled module, then emits i18n:locale-changed
877
- * app.i18n.locale(); // "en"
878
- * ```
879
- */
880
- setLocale(locale: string): Promise<void>;
881
- /**
882
- * Formats a message in the current locale. Resolution order is the current locale, then
883
- * `config.fallback`, then missing: a missing key gives one text part `⟦key⟧` and one warning.
884
- *
885
- * @param message - What `tr` returned.
886
- * @returns The parts of the sentence, adjacent text merged.
887
- * @example
888
- * ```ts
889
- * // text resolves the `content` of a Text component every time the locale changes.
890
- * app.i18n.format(tr("hud.orders", { n: 3 })); // [{ kind: "text", text: "3 заказа" }]
891
- * ```
892
- */
893
- format(message: Message): readonly Part[];
894
- /**
895
- * Formats a message in another registered locale, without switching. `ui.lint` measures a
896
- * label in every locale this way.
897
- *
898
- * @param message - What `tr` returned.
899
- * @param locale - The registered locale to read.
900
- * @returns The parts of the sentence in that locale.
901
- * @throws {Error} When the locale is not registered, or is registered but not loaded yet.
902
- * @example
903
- * ```ts
904
- * // ui.lint does this for every entry of app.i18n.locales() and keeps the widest result.
905
- * app.i18n.format(tr("hud.orders", { n: 3 }), "en"); // [{ kind: "text", text: "3 orders" }]
906
- * app.i18n.locale(); // "ru": reading another locale never switches
907
- * ```
908
- */
909
- format(message: Message, locale: string): readonly Part[];
910
- /**
911
- * The parts joined into one string, elements dropped. For logs, tests and `flow.describe()`.
912
- *
913
- * @param message - What `tr` returned.
914
- * @returns The sentence as text.
915
- * @example
916
- * ```ts
917
- * // A test asserts what the HUD says without touching the renderer.
918
- * app.i18n.plain(tr("hud.coins", { n: 25, icon: coinIcon })); // "25": the icon is dropped
919
- * ```
920
- */
921
- plain(message: Message): string;
922
- /**
923
- * Tells whether a key exists in the current locale or in the fallback.
924
- *
925
- * @param key - The message key.
926
- * @returns True when `format` would find a message for it.
927
- * @example
928
- * ```ts
929
- * // A projection hides the label of an order that has no description yet.
930
- * app.i18n.has("orders.hint"); // false: no locale brought that key
931
- * ```
932
- */
933
- has(key: string): boolean;
934
- /**
935
- * Formats milliseconds in the current locale through `Intl.DurationFormat`. The value is rounded
936
- * up to whole seconds, so a countdown never reads zero while time is left; a negative or
937
- * non-finite value reads as zero; the seconds are always shown. `text` formats a field bound
938
- * with `format: "duration"` through this call.
939
- *
940
- * @param ms - The duration in milliseconds.
941
- * @param style - How the duration reads. Default `"short"`.
942
- * @returns The duration as text.
943
- * @throws {Error} When the runtime has no `Intl.DurationFormat`.
944
- * @example
945
- * ```ts
946
- * // The out-of-energy popup shows when the next refill comes, locale "en".
947
- * app.i18n.duration(95_000); // "1 min, 35 sec"
948
- * app.i18n.duration(95_000, "digital"); // "0:01:35"
949
- * app.i18n.duration(500); // "1 sec": rounded up
950
- * ```
951
- */
952
- duration(ms: number, style?: DurationStyle): string;
953
- /**
954
- * Every locale at least one feature or the config brought a module for, sorted.
955
- *
956
- * @returns The registered locales.
957
- * @example
958
- * ```ts
959
- * // The language menu is built from this list, and ui.lint measures the widest locale.
960
- * app.i18n.locales(); // ["en", "ru"]
961
- * ```
962
- */
963
- locales(): readonly string[];
964
- /**
965
- * Swaps the compiled module of one feature in a loaded locale, for the dev hot swap. The one
966
- * registered module whose keys overlap `messages` takes them, the locale is merged again and
967
- * every key may warn once more. When the locale is the current one or the fallback,
968
- * `i18n:locale-changed` is emitted with the current locale, so `text` re-resolves every label.
969
- * A locale not loaded yet is left alone: its loader imports the new module later.
970
- *
971
- * @param locale - The locale of the saved `generated/strings.<locale>.ts`.
972
- * @param messages - The `default` export of the new module.
973
- * @throws {Error} When no registered module, or more than one, shares a key with `messages`.
974
- * @example
975
- * ```ts
976
- * // ui's hot swap: generated/strings.ru.ts was written again with "hud.orders" as "3 заказа!".
977
- * ctx.require(i18nPlugin).replace("ru", next.default); // emits i18n:locale-changed
978
- * app.i18n.plain(tr("hud.orders", { n: 3 })); // "3 заказа!": text redraws the label
979
- * ```
980
- */
981
- replace(locale: string, messages: CompiledMessages): void;
982
- };
983
- /**
984
- * What the kernel context offers this plugin.
985
- *
986
- * `index.ts` writes `events` with an annotated `register` (core spec `14-EVENT-REGISTRATION.md`
987
- * row 8), so the own event reaches the context the kernel hands the factories and `emit` is the
988
- * kernel's own, i18n-typed one. No member of the context is cast.
989
- */
990
- type I18nCtx = PluginCtx<Config$1, State$1, Events> & {
991
- readonly global: object;
992
- readonly log: Log.LogApi;
993
- readonly require: Require;
994
- };
989
+ type GesturePhase = "idle" | "pressed" | "longPressed" | "dragging" | "tracing";
995
990
  /**
996
- * The key-to-parameters table a game generates: `generated/strings.ts` exports it as `Strings`.
997
- * A key with no parameters carries `Record<never, never>`.
991
+ * What `onTap` hands a listener: the view the finger let go on, inside the tap slop.
998
992
  *
999
993
  * @example
1000
994
  * ```ts
1001
- * const table: StringTable = { "hud.orders": { n: 3 }, "orders.complete": {} };
995
+ * const tapped: Entity[] = [];
996
+ * const listener: TapListener = entity => tapped.push(entity);
1002
997
  * ```
1003
998
  */
1004
- type StringTable = Record<string, unknown>;
999
+ type TapListener = (entity: Entity) => void;
1005
1000
  /**
1006
- * The keys of a table whose message takes no parameters, so `tr` may be called with the key
1007
- * alone.
1001
+ * What `onPointer` registers: called with the raw sample inside the DOM listener, for the kinds
1002
+ * `down`, `up` and `cancel`, right after the sample is queued.
1008
1003
  *
1009
1004
  * @example
1010
1005
  * ```ts
1011
- * type Keys = KeysWithoutParameters<{ "home.play": Record<never, never>; "hud.orders": { n: number } }>;
1012
- * // "home.play"
1006
+ * const lifts: RawSample[] = [];
1007
+ * const listener: PointerListener = sample => {
1008
+ * if (sample.kind === "up") lifts.push(sample);
1009
+ * };
1013
1010
  * ```
1014
1011
  */
1015
- type KeysWithoutParameters<Table extends StringTable> = { [Key in keyof Table]: Record<never, never> extends Table[Key] ? Key : never }[keyof Table] & string;
1012
+ type PointerListener = (sample: RawSample) => void;
1016
1013
  /**
1017
- * `tr` as one game reads it: the key is checked against the generated table, and the parameters
1018
- * are the ones that key declares. A key with no parameters takes no second argument.
1014
+ * One key press as `onKey` hands it to a listener: the DOM `KeyboardEvent.key` and whether Shift
1015
+ * was held.
1019
1016
  *
1020
1017
  * @example
1021
1018
  * ```ts
1022
- * const { tr } = i18nFor<{ "hud.orders": { n: number } }>();
1023
- * tr("hud.orders", { n: 3 }); // { key: "hud.orders", params: { n: 3 } }
1019
+ * const backTab: KeyInput = { key: "Tab", shift: true };
1024
1020
  * ```
1025
1021
  */
1026
- type TypedTr<Table extends StringTable> = {
1027
- <Key extends KeysWithoutParameters<Table>>(key: Key): Message<Key, Table[Key]>;
1028
- <Key extends keyof Table & string>(key: Key, params: Table[Key]): Message<Key, Table[Key]>;
1022
+ type KeyInput = {
1023
+ key: string;
1024
+ shift: boolean;
1029
1025
  };
1030
1026
  /**
1031
- * What `defineGame` spreads from this plugin.
1027
+ * What `onKey` registers. Returning `true` marks the key handled: input then calls
1028
+ * `preventDefault()` on the DOM event, and `app.input.pressKey` answers `true`.
1032
1029
  *
1033
1030
  * @example
1034
1031
  * ```ts
1035
- * const kit: I18nKit<{ "home.play": Record<never, never> }> = i18nFor();
1036
- * kit.tr("home.play"); // { key: "home.play" }
1032
+ * const closeOnEscape: KeyListener = key => key.key === "Escape";
1037
1033
  * ```
1038
1034
  */
1039
- type I18nKit<Table extends StringTable> = {
1040
- tr: TypedTr<Table>;
1041
- };
1042
- declare namespace types_d_exports {
1043
- 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 };
1044
- }
1035
+ type KeyListener = (key: KeyInput) => boolean | void;
1045
1036
  /**
1046
- * A point in reference units: where a label sits, and which point of the block sits there.
1037
+ * input plugin config. Every distance is in reference px, every duration in milliseconds of
1038
+ * `time`, so a time scale and a pause apply to the gestures too.
1047
1039
  *
1048
1040
  * @example
1049
1041
  * ```ts
1050
- * const anchor: Point = { x: 0.5, y: 0.5 };
1042
+ * createApp({ plugins: [...screen], pluginConfigs: { input: { longPressMs: 300, swipeMinPx: 64 } } });
1043
+ * // A merge game carries the item in the hand 8% bigger: a board item resting at scale 0.5 on
1044
+ * // screen is drawn at 0.54 from the grab to the drop, even after a press squashed it, then
1045
+ * // flies home at 0.5.
1046
+ * createApp({ plugins: [...screen], pluginConfigs: { input: { heldScale: 1.08 } } });
1047
+ * // A game with its own cursors: the config merges shallowly, so both values are given.
1048
+ * createApp({ plugins: [...screen], pluginConfigs: { input: { cursor: { control: "grab", idle: "default" } } } });
1049
+ * // A word grid with 40 px cells: the finger's path is hit-tested every 16 px, below the
1050
+ * // shortest cell span, so a fast finger skips no letter.
1051
+ * createApp({ plugins: [...screen], pluginConfigs: { input: { traceStepPx: 16 } } });
1051
1052
  * ```
1052
1053
  */
1053
- type Point = {
1054
- x: number;
1055
- y: number;
1054
+ type Config$1 = {
1055
+ /** 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. */
1056
+ longPressMs: number; /** Move, in reference px, that turns a press on a `Draggable` into a drag. */
1057
+ dragStartPx: number; /** Shortest swipe, in reference px. */
1058
+ swipeMinPx: number; /** Longest swipe, in ms of `time`, from pointer down to pointer up. */
1059
+ swipeMaxMs: number;
1060
+ /**
1061
+ * How much bigger the view in the hand is drawn: its rest scale in root space times this, from
1062
+ * the grab to the release, so a squash a press left on it does not shrink the lift. A view with
1063
+ * no recorded rest starts from its scale at the grab. The drop, the cancel and the abort write
1064
+ * the rest scale back, so the way home starts at the view's own size. `1` changes nothing.
1065
+ */
1066
+ heldScale: number;
1067
+ /**
1068
+ * CSS cursors of the canvas. `control` shows while a mouse or a pen rests on a view that
1069
+ * carries `Tappable`, `Draggable`, `Pressable`, `Swipeable` or a component another plugin
1070
+ * registered through `controls.add`, such as ui's `LocalWrite`; `idle` shows everywhere else.
1071
+ * `""` hands the cursor back to the page's own style.
1072
+ */
1073
+ cursor: {
1074
+ control: string;
1075
+ idle: string;
1076
+ };
1077
+ /**
1078
+ * Reference px between two hit tests along the finger's path inside one frame of a trace. Keep
1079
+ * it below the shortest cell span: a longer step jumps over a cell.
1080
+ */
1081
+ traceStepPx: number;
1082
+ /**
1083
+ * The hit circle of a trace cell: its radius is this times the short side of the cell's hit
1084
+ * box, around the box centre. A diagonal then crosses no corner of a neighbour.
1085
+ */
1086
+ traceInset: number;
1056
1087
  };
1057
1088
  /**
1058
- * The size of a measured block, in reference pixels.
1059
- *
1060
- * @example
1061
- * ```ts
1062
- * const size: Size = { width: 57.6, height: 38.4 };
1063
- * ```
1089
+ * One view a drag carries on top of the held one: hung under the held view from the grab to the
1090
+ * release.
1064
1091
  */
1065
- type Size = {
1066
- width: number;
1067
- height: number;
1092
+ type Carried = {
1093
+ /** The carried view. */entity: Entity; /** The `Parent` it had at the grab, hung back under it on the release. */
1094
+ parent: Entity | undefined; /** The remover `world.projection.mute` returned for its root pose. */
1095
+ unmute: () => void;
1068
1096
  };
1069
1097
  /**
1070
- * How a bound number is shown. `"int"` rounds it; the three time formats take milliseconds and
1071
- * show whole seconds, rounded up, so a timer never reads zero while time is left.
1072
- *
1073
- * @example
1074
- * ```ts
1075
- * const format: TextFormat = "mm:ss"; // 95_000 ms shows "01:35"
1076
- * ```
1077
- */
1078
- type TextFormat = "int" | "mm:ss" | "h:mm:ss" | "duration";
1079
- /** The brand only `bind()` puts on a `TextBind`. Type-only: nothing is emitted for it. */
1080
- declare const textBind: unique symbol;
1081
- /**
1082
- * Which numeric component field a label shows, and how. Plain data in the component, so a
1083
- * snapshot lists it; nominal, so only `bind()` makes one and a hand-written literal does not
1084
- * compile.
1085
- *
1086
- * @example
1087
- * ```ts
1088
- * const Counter = component("Counter", { value: 0 });
1089
- * const coins: TextBind = bind(Counter, "value");
1090
- * // { component: "Counter", field: "value", format: "int" }
1091
- * ```
1098
+ * input plugin state: the raw queue the DOM handlers write, and the one gesture in progress.
1092
1099
  */
1093
- type TextBind = {
1094
- readonly component: string;
1095
- readonly field: string;
1096
- readonly format: TextFormat;
1097
- } & {
1098
- readonly [textBind]: true;
1099
- };
1100
- /**
1101
- * The numeric fields of a component value, by name: what `bind()` takes as its field.
1102
- *
1103
- * @example
1104
- * ```ts
1105
- * type Fields = BindField<{ value: number; name: string }>; // "value"
1106
- * ```
1107
- */
1108
- type BindField<Value extends object> = keyof NumericFields<Value> & string;
1109
- /**
1110
- * What `bind()` takes after the field. The format is `"int"` when left out.
1111
- *
1112
- * @example
1113
- * ```ts
1114
- * const options: BindOptions = { format: "mm:ss" };
1115
- * ```
1116
- */
1117
- type BindOptions = {
1118
- format?: TextFormat;
1119
- };
1120
- /**
1121
- * A moment to count down to, and how much is left. A game writes `until`, a moment of `clock` in
1122
- * epoch milliseconds; `left` is engine-owned and follows what the label shows.
1123
- *
1124
- * @example
1125
- * ```ts
1126
- * // clock.now() is 1_790_000_000_000: the chest opens in 95 s.
1127
- * const chest: CountdownValue = { until: 1_790_000_095_000, left: 95_000 };
1128
- * ```
1129
- */
1130
- type CountdownValue = {
1131
- until: number;
1132
- left: number;
1133
- };
1134
- /**
1135
- * What the `Text` component holds. `resolved` is engine-owned: a game writes `content`, `style`,
1136
- * `bind`, `anchor` and `alpha`, and reads `resolved`. `alpha` fades every run, shadow and icon of
1137
- * the block together, 1 by default; a change is written in place and a tween can drive it.
1138
- *
1139
- * @example
1140
- * ```ts
1141
- * const value: TextValue = {
1142
- * content: "+5", style: "body", bind: undefined, anchor: { x: 0.5, y: 0.5 }, alpha: 1,
1143
- * resolved: "+5"
1144
- * };
1145
- * // The hint of an empty name field, at half alpha.
1146
- * const hint: TextValue = { ...value, content: "Your name", alpha: 0.5, resolved: "Your name" };
1147
- * ```
1148
- */
1149
- type TextValue = {
1150
- content: string | Message;
1151
- style: string;
1152
- bind: TextBind | undefined;
1153
- anchor: Point;
1154
- alpha: number;
1155
- resolved: string;
1156
- };
1157
- /**
1158
- * Where the lines of a block sit inside the widest of them.
1159
- *
1160
- * @example
1161
- * ```ts
1162
- * const align: TextAlign = "center";
1163
- * ```
1164
- */
1165
- type TextAlign = "left" | "center" | "right";
1166
- /**
1167
- * The wrap width of a style in reference pixels, or `"none"` for one line per `\n` segment.
1168
- *
1169
- * @example
1170
- * ```ts
1171
- * const wrap: TextWrap = 480;
1172
- * ```
1173
- */
1174
- type TextWrap = number | "none";
1175
- /**
1176
- * The drop shadow of a style as the plugin stores it: a copy of every glyph run in `color`, at
1177
- * `alpha`, moved by `dx` and `dy` reference pixels.
1178
- *
1179
- * @example
1180
- * ```ts
1181
- * const shadow: TextShadow = { color: 0x5b3a1e, dx: 0, dy: 4, alpha: 1 };
1182
- * ```
1183
- */
1184
- type TextShadow = {
1185
- color: number;
1186
- dx: number;
1187
- dy: number;
1188
- alpha: number;
1100
+ type State$1 = {
1101
+ /** Written by the DOM handlers, drained by the frame step. */samples: RawSample[];
1102
+ phase: GesturePhase; /** The one active pointer. A sample of any other pointer is dropped while this is set. */
1103
+ pointerId: number | undefined; /** The pressed or held entity. */
1104
+ entity: Entity | undefined; /** The projection key of `entity`, read once at press time. */
1105
+ key: {
1106
+ projection: string;
1107
+ key: string;
1108
+ } | undefined; /** Reference px at pointer down. */
1109
+ start: Point$1; /** `Transform` minus the pointer at the moment of the grab. */
1110
+ grabOffset: Point$1; /** Sum of `time.delta` since pointer down. No device clock is ever read. */
1111
+ pressedMs: number; /** The drop target that carries `Hovered` during a drag. */
1112
+ hovered: Entity | undefined; /** The view that carries `PointerOver`: a press would take it, and a mouse or a pen is over it. */
1113
+ pointerOver: Entity | undefined; /** The `Parent` the held view had at the grab. It is hung back under it on the release. */
1114
+ parent: Entity | undefined; /** The rest scale of the held view in root space: the base of `heldScale`, set back on drop. */
1115
+ restScale: number | undefined;
1116
+ /**
1117
+ * The root pose of the held view, written at the grab and at every move. A carried view is
1118
+ * placed from it when the held view despawns before the release and takes its `Transform`.
1119
+ */
1120
+ heldPose: TransformValue | undefined; /** The remover `world.projection.mute` returned, while a drag runs. */
1121
+ unmute: (() => void) | undefined; /** The views a drag carries on top of the held one, in `carry` order; empty outside a drag. */
1122
+ carried: Carried[]; /** The cells of the trace in progress, in path order; empty outside a trace. */
1123
+ path: Entity[]; /** The intent of the first cell of the trace; a cell of another intent closes the path. */
1124
+ traceIntent: string | undefined; /** True once a cell of another intent was crossed: the path neither grows nor pops. */
1125
+ traceClosed: boolean; /** Where the finger was at the last frame of the trace: the segment starts there. */
1126
+ lastPoint: Point$1 | undefined;
1127
+ canvas: HTMLCanvasElement | undefined;
1128
+ offFrame: (() => void) | undefined;
1129
+ detach: (() => void) | undefined; /** Registered through `onTap`, called in this order on every tap. */
1130
+ tapListeners: TapListener[]; /** Registered through `onKey`, called in this order on every key. */
1131
+ keyListeners: KeyListener[]; /** Registered through `onPointer`, called in this order inside the DOM listener. */
1132
+ pointerListeners: PointerListener[]; /** Takes the one `keydown` listener off `window`; set while a canvas is attached. */
1133
+ detachKeys: (() => void) | undefined; /** `time.wake`, bound in `onInit`: every pointer sample leaves the idle frame rate. */
1134
+ wake: (() => void) | undefined; /** The cursor last written on the attached canvas; `undefined` while nothing was written. */
1135
+ cursor: string | undefined; /** Component types other plugins registered through `controls.add`, once per registration. */
1136
+ controls: AnyComponentType[];
1189
1137
  };
1190
1138
  /**
1191
- * One style as a game writes it: the font and the size are required, the rest has defaults.
1139
+ * input plugin API, `app.input`: the same door for the finger, for a test and for an agent. Each
1140
+ * method builds the Answer the gesture would build and hands it to `flow.gate.answer`. Nothing
1141
+ * moves: no coordinates, no frames, no `Held`, no `settle`.
1192
1142
  *
1193
1143
  * @example
1194
1144
  * ```ts
1195
- * // A popup title: cream display glyphs over a brown shadow 4 px down.
1196
- * const title: TextStyleInput = {
1197
- * font: "ui.font-display", size: 56, fill: 0xfff3d6, shadow: { color: 0x5b3a1e, dx: 0, dy: 4 }
1198
- * };
1199
- *
1200
- * defineTextStyles({ "popup.title": title }).map["popup.title"]?.shadow;
1201
- * // { color: 0x5b3a1e, dx: 0, dy: 4, alpha: 1 }
1145
+ * // A headless test plays the board without a screen.
1146
+ * app.input.tap({ projection: "board.generators", key: "g1" }); // true: the generator spat an item
1147
+ * app.input.drag({ projection: "board.items", key: "i5" }, { projection: "board.items", key: "i7" });
1202
1148
  * ```
1203
1149
  */
1204
- type TextStyleInput = {
1205
- font: string;
1206
- bold?: string;
1207
- italic?: string;
1208
- size: number;
1209
- fill: number; /** The outline colour. Drawn only when `strokeWidth` is above 0. */
1210
- stroke?: number;
1150
+ type InputApi = {
1211
1151
  /**
1212
- * The outline width in reference px: 8 copies of each glyph run on a circle this wide (12 from 6
1213
- * on), tinted `stroke`. 0 draws none. It never changes what `measure` answers.
1152
+ * Reads the `Tappable` of a view and answers its intent.
1153
+ *
1154
+ * @param target - The projection key of a live view, or an entity.
1155
+ * @returns What `flow.gate.answer` returned: true when the gate took the answer.
1156
+ * @example
1157
+ * ```ts
1158
+ * // A hidden-object test finds one object without touching a pixel.
1159
+ * app.input.tap({ projection: "scene.objects", key: "lamp" });
1160
+ * // true: answers { intent: "found", payload: { id: "lamp" } }
1161
+ * ```
1214
1162
  */
1215
- strokeWidth?: number;
1216
- letterSpacing?: number;
1217
- align?: TextAlign;
1218
- wrap?: TextWrap;
1219
- digits?: boolean;
1163
+ tap(target: Target): boolean;
1220
1164
  /**
1221
- * A drop shadow under every glyph run, in reference px at the style size. `alpha` is 1 when
1222
- * left out. It never changes what `measure` answers.
1165
+ * Reads the `Pressable` of a view and answers its intent, the way a long press would.
1166
+ *
1167
+ * @param target - The projection key of a live view, or an entity.
1168
+ * @returns What `flow.gate.answer` returned: true when the gate took the answer.
1169
+ * @example
1170
+ * ```ts
1171
+ * // An agent opens the info card of an item without holding a finger for 450 ms.
1172
+ * app.input.press({ projection: "board.items", key: "i5" });
1173
+ * // true: answers { intent: "info", payload: { id: "i5" } }
1174
+ * ```
1223
1175
  */
1224
- shadow?: {
1225
- color: number;
1226
- dx: number;
1227
- dy: number;
1228
- alpha?: number;
1176
+ press(target: Target): boolean;
1177
+ /**
1178
+ * Reads the `Draggable` of the source and the `DropTarget` of the destination and answers the
1179
+ * destination's intent with both payloads merged. The destination wins a key both carry.
1180
+ *
1181
+ * @param from - The view that is carried.
1182
+ * @param to - The view it is dropped on; this one names the intent.
1183
+ * @returns What `flow.gate.answer` returned: true when the gate took the answer.
1184
+ * @example
1185
+ * ```ts
1186
+ * // The merge test of the fixture game: two items of level 1 become one of level 2.
1187
+ * app.input.drag({ projection: "board.items", key: "i5" }, { projection: "board.items", key: "i7" });
1188
+ * // true: answers { intent: "merge", payload: { from: "c2", to: "c3" } }
1189
+ * ```
1190
+ */
1191
+ drag(from: Target, to: Target): boolean;
1192
+ /**
1193
+ * Reads the `Swipeable` of a view and answers its intent with the direction in the payload.
1194
+ *
1195
+ * @param target - The projection key of a live view, or an entity.
1196
+ * @param direction - Which way the finger would have gone.
1197
+ * @returns What `flow.gate.answer` returned: true when the gate took the answer.
1198
+ * @example
1199
+ * ```ts
1200
+ * // A match-3 test swaps the cell c2 with its right neighbour.
1201
+ * app.input.swipe({ projection: "board.cells", key: "c2" }, "right");
1202
+ * // true: answers { intent: "swap", payload: { cell: "c2", direction: "right" } }
1203
+ * ```
1204
+ */
1205
+ swipe(target: Target, direction: Direction): boolean;
1206
+ /**
1207
+ * Reads the `Traceable` of every cell and answers one path, the way a finger drawn through the
1208
+ * cells would: the intent of the cells and `{ path }`, the cell payloads in order. Every cell
1209
+ * must name the same intent. An empty list, a cell with no `Traceable` and two intents warn and
1210
+ * answer `false` without calling the gate. Nothing is tagged `Traced`.
1211
+ *
1212
+ * @param path - The cells in the order the finger goes through them.
1213
+ * @returns What `flow.gate.answer` returned: true when the gate took the answer.
1214
+ * @example
1215
+ * ```ts
1216
+ * // A word-game test spells a word without a finger.
1217
+ * app.input.trace([
1218
+ * { projection: "board.cells", key: "b3" },
1219
+ * { projection: "board.cells", key: "c3" },
1220
+ * { projection: "board.cells", key: "d3" }
1221
+ * ]);
1222
+ * // true: answers { intent: "word", payload: { path: [{ cell: "b3" }, { cell: "c3" }, { cell: "d3" }] } }
1223
+ * ```
1224
+ */
1225
+ trace(path: readonly Target[]): boolean;
1226
+ /**
1227
+ * Registers a listener called on every tap — a press and a release inside `tapSlopPx` — on the
1228
+ * topmost view the hit test accepted, before the `Tappable` answer. A view that carries only
1229
+ * `Touchable` reaches the listeners and answers nothing. A listener that throws is logged with
1230
+ * its entity, and the listeners after it still run.
1231
+ *
1232
+ * @param fn - What to run with the tapped entity.
1233
+ * @returns The remover; call it to stop listening.
1234
+ * @example
1235
+ * ```ts
1236
+ * // ui applies the local patch of the button the finger tapped.
1237
+ * const off = ctx.require(inputPlugin).onTap(entity => {
1238
+ * const write = ctx.require(worldPlugin).ecs.get(entity, LocalWrite);
1239
+ *
1240
+ * if (write !== undefined) applyLocal(entity, write.patch);
1241
+ * });
1242
+ * off(); // in onStop
1243
+ * ```
1244
+ */
1245
+ onTap(fn: TapListener): () => void;
1246
+ /**
1247
+ * Registers a listener called synchronously inside the DOM pointer listener, right after the
1248
+ * sample is queued, for `pointerdown`, `pointerup` and `pointercancel`. It is the one place a
1249
+ * browser call that needs the user gesture can run, such as `focus()` on a text field: in the
1250
+ * next frame iOS shows no keyboard. Never called for a move, a leave or a lost capture, and never
1251
+ * by `tap`, `press`, `drag` or `swipe`, which have no DOM moment. The sample is raw: client px,
1252
+ * no hit test. Listeners run in registration order; a listener that throws is logged with the
1253
+ * sample kind, and the listeners after it still run.
1254
+ *
1255
+ * @param fn - What to run with the raw sample.
1256
+ * @returns The remover; call it to stop listening.
1257
+ * @example
1258
+ * ```ts
1259
+ * // ui opens the keyboard the moment the finger lifts on a text field, inside the DOM listener.
1260
+ * const off = ctx.require(inputPlugin).onPointer(sample => {
1261
+ * if (sample.kind === "up" && fieldAt(sample.clientX, sample.clientY) !== undefined) element.focus({ preventScroll: true });
1262
+ * if (sample.kind === "down" && fieldAt(sample.clientX, sample.clientY) === undefined) element.blur();
1263
+ * });
1264
+ * // a tap on the Rename field on an iPhone: the keyboard shows; a tap on the parchment next to it: the keyboard hides
1265
+ * off(); // in onStop
1266
+ * ```
1267
+ */
1268
+ onPointer(fn: PointerListener): () => void;
1269
+ /**
1270
+ * Registers a listener called on every key pressed while the canvas is attached, and on every
1271
+ * `pressKey` call. A key typed into a text field is not handed over, except Enter and Escape,
1272
+ * and an Enter that commits an IME composition is dropped. Listeners run in registration order,
1273
+ * all of them, each time. A listener that
1274
+ * returns `true` marks the key handled, and input calls `preventDefault()` on the DOM event. A
1275
+ * listener that throws is logged with its key, and the listeners after it still run.
1276
+ *
1277
+ * @param fn - What to run with the key; return `true` when it handled it.
1278
+ * @returns The remover; call it to stop listening.
1279
+ * @example
1280
+ * ```ts
1281
+ * // ui moves the keyboard focus on Tab and keeps the browser from leaving the canvas.
1282
+ * const off = ctx.require(inputPlugin).onKey(key => {
1283
+ * if (key.key !== "Tab") return false;
1284
+ * moveFocus(key.shift ? -1 : 1);
1285
+ *
1286
+ * return true;
1287
+ * });
1288
+ * off(); // in onStop
1289
+ * ```
1290
+ */
1291
+ onKey(fn: KeyListener): () => void;
1292
+ /**
1293
+ * Presses a key without a keyboard: runs the `onKey` listeners exactly as a DOM `keydown` does.
1294
+ *
1295
+ * @param key - The DOM `KeyboardEvent.key`, such as `"Tab"`, `"Enter"`, `" "` or `"Escape"`.
1296
+ * @param options - How the key is pressed.
1297
+ * @param options.shift - True presses it with Shift held; the default is `false`.
1298
+ * @returns True when a listener handled the key.
1299
+ * @example
1300
+ * ```ts
1301
+ * // A headless test walks the settings popup backwards and closes it.
1302
+ * app.input.pressKey("Tab", { shift: true }); // true: ui moved the focus to the previous control
1303
+ * app.input.pressKey("Escape"); // true: ui tapped the popup's close button
1304
+ * app.input.pressKey("q"); // false: no listener handles it
1305
+ * ```
1306
+ */
1307
+ pressKey(key: string, options?: {
1308
+ shift?: boolean;
1309
+ }): boolean;
1310
+ /**
1311
+ * The CSS cursor input set on the canvas: `config.cursor.control` while a mouse or a pen rests
1312
+ * on a control, `config.cursor.idle` otherwise and whenever nothing is attached.
1313
+ *
1314
+ * @returns The cursor value.
1315
+ * @example
1316
+ * ```ts
1317
+ * // An e2e check: the mouse rests on the Play button of the home screen.
1318
+ * app.input.cursor(); // "pointer"
1319
+ * // The mouse moves to the empty sky above it.
1320
+ * app.input.cursor(); // ""
1321
+ * ```
1322
+ */
1323
+ cursor(): string;
1324
+ /**
1325
+ * The components that make a view a control besides input's own: the cursor shows
1326
+ * `config.cursor.control` over a view that carries one of them.
1327
+ */
1328
+ controls: {
1329
+ /**
1330
+ * Registers a component type of another plugin as a control. A type registered twice stays
1331
+ * a control until both removers ran.
1332
+ *
1333
+ * @param component - The component or tag type that makes a view answer a press.
1334
+ * @returns The remover; call it to take the registration back.
1335
+ * @example
1336
+ * ```ts
1337
+ * // ui counts its local-state buttons as controls, so the mouse over a tab shows a hand.
1338
+ * const off = ctx.require(inputPlugin).controls.add(LocalWrite);
1339
+ * app.input.cursor(); // "pointer" while the mouse rests on the Audio tab
1340
+ * off(); // in onStop
1341
+ * ```
1342
+ */
1343
+ add(component: AnyComponentType): () => void;
1229
1344
  };
1230
1345
  };
1231
1346
  /**
1232
- * One style as the plugin stores it: every field filled, `bold` and `italic` only when the game
1233
- * shipped those MSDF fonts, `shadow` only when the style has one.
1234
- *
1235
- * @example
1236
- * ```ts
1237
- * const style: TextStyle = {
1238
- * font: "ui.font-body", bold: undefined, italic: undefined, size: 32, fill: 0xffffff,
1239
- * stroke: 0x000000, strokeWidth: 0, letterSpacing: 0, align: "left", wrap: "none",
1240
- * digits: false, shadow: undefined
1241
- * };
1242
- * ```
1243
- */
1244
- type TextStyle = {
1245
- font: string;
1246
- bold: string | undefined;
1247
- italic: string | undefined;
1248
- size: number;
1249
- fill: number;
1250
- stroke: number;
1251
- strokeWidth: number;
1252
- letterSpacing: number;
1253
- align: TextAlign;
1254
- wrap: TextWrap;
1255
- digits: boolean;
1256
- shadow: TextShadow | undefined;
1257
- };
1258
- /**
1259
- * What `defineTextStyles` returns and a feature registers under its `textStyles` key. Structural
1260
- * on purpose: `flow` carries the map, `text` reads it.
1261
- *
1262
- * @example
1263
- * ```ts
1264
- * const styles: TextStyles = { kind: "textStyles", map: { "hud.title": bodyStyle } };
1265
- * ```
1347
+ * Resolved dependency APIs.
1266
1348
  */
1267
- type TextStyles = {
1268
- readonly kind: "textStyles";
1269
- readonly map: Record<string, TextStyle>;
1349
+ type Deps$1 = {
1350
+ time: Api;
1351
+ flow: Api$1;
1352
+ world: Api$2;
1353
+ renderer: Api$3;
1270
1354
  };
1271
1355
  /**
1272
- * The advances of one font at the size the `.fnt` was exported with. Widths scale by
1273
- * `style.size / size`; kerning pairs are ignored.
1356
+ * What the kernel context offers before the deps are attached.
1274
1357
  *
1275
- * @example
1276
- * ```ts
1277
- * const table: AdvanceTable = { size: 32, lineHeight: 40, advances: new Map([["1", 18]]) };
1278
- * ```
1358
+ * `input` owns no event, so `emit` is the kernel's and never called here.
1279
1359
  */
1280
- type AdvanceTable = {
1281
- size: number;
1282
- lineHeight: number;
1283
- advances: Map<string, number>;
1360
+ type KernelSlice$1 = PluginCtx<Config$1, State$1> & {
1361
+ readonly global: object;
1362
+ readonly log: Log.LogApi;
1363
+ readonly require: Require;
1284
1364
  };
1285
1365
  /**
1286
- * One run of glyphs: the text and the flags every glyph in it shares.
1287
- *
1288
- * @example
1289
- * ```ts
1290
- * const run: TextRun = { kind: "text", text: "+5", bold: true, italic: false, color: 0xffe082 };
1291
- * ```
1292
- */
1293
- type TextRun = {
1294
- kind: "text";
1295
- text: string;
1296
- bold: boolean;
1297
- italic: boolean;
1298
- color: number | undefined;
1366
+ * Domain context of the input plugin: the kernel slice plus the four resolved dependencies.
1367
+ */
1368
+ type InputCtx = KernelSlice$1 & {
1369
+ readonly deps: Deps$1;
1299
1370
  };
1371
+ //#endregion
1372
+ //#region src/plugins/ui/jsx/component.d.ts
1300
1373
  /**
1301
- * One inline icon: an asset key drawn square at the line height.
1374
+ * Declares a component: a piece of interface with local state, outcomes, or both. The view runs
1375
+ * at every reconcile with the instance's own local state, never at build time.
1302
1376
  *
1377
+ * @param name - Unique name across every feature of the game.
1378
+ * @param spec - The initial local state, the outcomes a popup resolves with, and the view.
1379
+ * @returns The component, ready to stand in a tag and in a feature's `ui` key.
1303
1380
  * @example
1304
1381
  * ```ts
1305
- * const run: IconRun = { kind: "icon", key: "hud.coin" };
1382
+ * const Panel = defineComponent("Panel", {
1383
+ * local: { tab: "audio" },
1384
+ * view: (props: { volume: number }, local) => ({ type: "column", props: {}, children: [] })
1385
+ * });
1386
+ * Panel.name; // "Panel"
1306
1387
  * ```
1307
1388
  */
1308
- type IconRun = {
1309
- kind: "icon";
1310
- key: string;
1311
- };
1389
+ declare function defineComponent<Properties extends object, Local extends object, Outcomes extends Record<string, unknown>>(name: string, spec: ComponentSpec<Properties, Local, Outcomes> & {
1390
+ readonly outcomes: Outcomes;
1391
+ }): ComponentDefinition<Properties, Local, Outcomes>;
1312
1392
  /**
1313
- * A piece of a line: glyphs or one inline icon.
1393
+ * Declares a component without outcomes: it may stand in a tree, never in a `popup`.
1314
1394
  *
1395
+ * @param name - Unique name across every feature of the game.
1396
+ * @param spec - The initial local state and the view.
1397
+ * @returns The component, ready to stand in a tag.
1315
1398
  * @example
1316
1399
  * ```ts
1317
- * const runs: Run[] = [{ kind: "icon", key: "hud.coin" }];
1400
+ * const Row = defineComponent("Row", { view: () => ({ type: "row", props: {}, children: [] }) });
1401
+ * Row.outcomes; // undefined
1318
1402
  * ```
1319
1403
  */
1320
- type Run = TextRun | IconRun;
1404
+ declare function defineComponent<Properties extends object, Local extends object>(name: string, spec: ComponentSpec<Properties, Local, never>): ComponentDefinition<Properties, Local, undefined>;
1405
+ declare namespace types_d_exports {
1406
+ 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 };
1407
+ }
1321
1408
  /**
1322
- * One laid-out line and how wide it is.
1409
+ * A point in reference units: where a label sits, and which point of the block sits there.
1323
1410
  *
1324
1411
  * @example
1325
1412
  * ```ts
1326
- * const line: Line = { runs: [{ kind: "icon", key: "hud.coin" }], width: 38.4 };
1413
+ * const anchor: Point = { x: 0.5, y: 0.5 };
1327
1414
  * ```
1328
1415
  */
1329
- type Line = {
1330
- runs: Run[];
1331
- width: number;
1416
+ type Point = {
1417
+ x: number;
1418
+ y: number;
1332
1419
  };
1333
1420
  /**
1334
- * A laid-out block: its lines and the size `measure` answers with.
1421
+ * The size of a measured block, in reference pixels.
1335
1422
  *
1336
1423
  * @example
1337
1424
  * ```ts
1338
- * const layout: TextLayout = { lines: [], width: 0, height: 0 };
1425
+ * const size: Size = { width: 57.6, height: 38.4 };
1339
1426
  * ```
1340
1427
  */
1341
- type TextLayout = {
1342
- lines: Line[];
1428
+ type Size = {
1343
1429
  width: number;
1344
1430
  height: number;
1345
1431
  };
1346
1432
  /**
1347
- * What one label container was last filled from: the style object and the laid-out block. An
1348
- * update with the same style object, the same anchor and the same lines and runs writes the new
1349
- * text and positions into the objects already there.
1350
- */
1351
- type DrawnLabel = {
1352
- style: TextStyle;
1353
- layout: TextLayout;
1354
- };
1355
- /**
1356
- * A dev warning that is written once per key. Pure modules take it as an argument, so nothing
1357
- * below the plugin context reaches the log itself.
1433
+ * How a bound number is shown. `"int"` rounds it; the three time formats take milliseconds and
1434
+ * show whole seconds, rounded up, so a timer never reads zero while time is left.
1358
1435
  *
1359
1436
  * @example
1360
1437
  * ```ts
1361
- * const warn: Warn = (key, message) => seen.has(key) || log.warn(message);
1438
+ * const format: TextFormat = "mm:ss"; // 95_000 ms shows "01:35"
1362
1439
  * ```
1363
1440
  */
1364
- type Warn = (key: string, message: string, data?: Record<string, unknown>) => void;
1441
+ type TextFormat = "int" | "mm:ss" | "h:mm:ss" | "duration";
1442
+ /** The brand only `bind()` puts on a `TextBind`. Type-only: nothing is emitted for it. */
1443
+ declare const textBind: unique symbol;
1365
1444
  /**
1366
- * What the pure layout needs next to the runs, the style and the tables.
1445
+ * Which numeric component field a label shows, and how. Plain data in the component, so a
1446
+ * snapshot lists it; nominal, so only `bind()` makes one and a hand-written literal does not
1447
+ * compile.
1367
1448
  *
1368
1449
  * @example
1369
1450
  * ```ts
1370
- * const options: LayoutOptions = { warn: () => undefined };
1451
+ * const Counter = component("Counter", { value: 0 });
1452
+ * const coins: TextBind = bind(Counter, "value");
1453
+ * // { component: "Counter", field: "value", format: "int" }
1371
1454
  * ```
1372
1455
  */
1373
- type LayoutOptions = {
1374
- warn: Warn;
1456
+ type TextBind = {
1457
+ readonly component: string;
1458
+ readonly field: string;
1459
+ readonly format: TextFormat;
1460
+ } & {
1461
+ readonly [textBind]: true;
1375
1462
  };
1376
1463
  /**
1377
- * What was resolved for one entity last, so the frame step knows whether anything moved. `unit`
1378
- * is what the shown string of a bound label was built from: the rounded value for `"int"`, whole
1379
- * seconds for a time format.
1464
+ * The numeric fields of a component value, by name: what `bind()` takes as its field.
1380
1465
  *
1381
1466
  * @example
1382
1467
  * ```ts
1383
- * const seen: SeenText = {
1384
- * content: "", style: "digits", bind: "Countdown.left:mm:ss", locale: "ru", unit: 95
1385
- * };
1468
+ * type Fields = BindField<{ value: number; name: string }>; // "value"
1386
1469
  * ```
1387
1470
  */
1388
- type SeenText = {
1389
- content: unknown;
1390
- style: string;
1391
- bind: string | undefined;
1392
- locale: string;
1393
- unit: number | undefined;
1394
- };
1471
+ type BindField<Value extends object> = keyof NumericFields<Value> & string;
1395
1472
  /**
1396
- * text plugin config.
1473
+ * What `bind()` takes after the field. The format is `"int"` when left out.
1397
1474
  *
1398
1475
  * @example
1399
1476
  * ```ts
1400
- * createApp({ pluginConfigs: { text: { fonts: { body: "ui.body", digits: "ui.digits" } } } });
1477
+ * const options: BindOptions = { format: "mm:ss" };
1401
1478
  * ```
1402
1479
  */
1403
- type Config = {
1404
- /** The two fonts every game ships in its boot bundle. They back the built-in styles. */fonts: {
1405
- body: string;
1406
- digits: string;
1407
- };
1408
- };
1409
- /**
1410
- * text plugin state.
1411
- */
1412
- type State = {
1413
- /** 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. */
1414
- styleOwner: Map<string, string>; /** The config fonts plus every `font`, `bold` and `italic` of every style. */
1415
- fontKeys: Set<string>; /** The parsed advance table per font key, filled when `assets` has the font. */
1416
- tables: Map<string, AdvanceTable>; /** The font keys handed to `renderer.sync.fonts.install`. */
1417
- installed: Set<string>; /** What was resolved for an entity last. */
1418
- seen: Map<Entity, SeenText>; /** `bind.component` to its world type; `undefined` after the one warning. */
1419
- bindTypes: Map<string, AnyComponent | undefined>; /** The measured size of every live label, for `ui`. */
1420
- measured: Map<Entity, Size>; /** Layout per `style + resolved`, oldest dropped first. */
1421
- cache: Map<string, TextLayout>; /** What every live label container was last filled from, by container. */
1422
- drawn: WeakMap<object, DrawnLabel>; /** Entities to re-resolve in the next layout phase. */
1423
- dirty: Set<Entity>; /** Warning keys already written, so a style, a tag, a glyph or a font warns once. */
1424
- warned: Set<string>; /** The system, the two world hooks and the display adapter. */
1425
- removers: Array<() => void>;
1480
+ type BindOptions = {
1481
+ format?: TextFormat;
1426
1482
  };
1427
1483
  /**
1428
- * text plugin API, `app.text`. Two questions: how big a piece of text is, and which styles the
1429
- * game registered; and one order, the dev hot swap of a styles file. Everything else happens in
1430
- * the frame.
1484
+ * A moment to count down to, and how much is left. A game writes `until`, a moment of `clock` in
1485
+ * epoch milliseconds; `left` is engine-owned and follows what the label shows.
1431
1486
  *
1432
1487
  * @example
1433
1488
  * ```ts
1434
- * app.text.measure("120", "digits"); // { width: 57.6, height: 38.4 }
1435
- * app.text.styles(); // ["body", "digits"]
1489
+ * // clock.now() is 1_790_000_000_000: the chest opens in 95 s.
1490
+ * const chest: CountdownValue = { until: 1_790_000_095_000, left: 95_000 };
1436
1491
  * ```
1437
1492
  */
1438
- type TextApi = {
1439
- /**
1440
- * The size of a piece of text in reference pixels, from the advance table of the style's font.
1441
- * A `Message` is formatted in the current locale first. Never touches a canvas, so the answer
1442
- * is the same in a browser and in plain Bun. Cached per style and resolved string.
1443
- *
1444
- * @param content - A plain string, or what `tr` returned.
1445
- * @param style - A registered style name. An unknown one warns once and measures as `body`.
1446
- * @returns The width and height of the block.
1447
- * @example
1448
- * ```ts
1449
- * // `ui.layout` asks once per invalidation, from the Yoga measure function of a text element.
1450
- * const text = ctx.require(textPlugin);
1451
- *
1452
- * text.measure("120", "digits"); // { width: 57.6, height: 38.4 } on the 0.6 em fallback
1453
- * ```
1454
- */
1455
- measure(content: string | Message, style: string): Size;
1456
- /**
1457
- * Whether the font of a style has a glyph for one character, the lookup Pixi draws by. A
1458
- * character its font lacks is drawn as nothing and `measure` counts it 0 wide. While the font is
1459
- * not loaded every character counts, as the 0.6 em fallback measures it. An unknown style warns
1460
- * once and looks up `body`.
1461
- *
1462
- * @param char - One character, a whole code point.
1463
- * @param style - A registered style name.
1464
- * @returns False only for a character a loaded font has no glyph for.
1465
- * @example
1466
- * ```ts
1467
- * // `ui` measures the caret of a text field over the characters the font draws.
1468
- * const text = ctx.require(textPlugin);
1469
- *
1470
- * text.hasGlyph("A", "body"); // true
1471
- * text.hasGlyph("😀", "body"); // false once the body font is loaded: it has no emoji glyph
1472
- * ```
1473
- */
1474
- hasGlyph(char: string, style: string): boolean;
1475
- /**
1476
- * The registered style names: the two built-ins first, then the styles of every feature in
1477
- * feature order.
1478
- *
1479
- * @returns A frozen list of style names.
1480
- * @example
1481
- * ```ts
1482
- * // A dev overlay lists what a game may write in a `style` prop.
1483
- * app.text.styles(); // ["body", "digits", "hud.digits"]
1484
- * ```
1485
- */
1486
- styles(): readonly string[];
1487
- /**
1488
- * The dev hot swap of a styles file: writes every style of the map over the table. A known
1489
- * name keeps the feature that owns it, a new name is added and usable at once. The fonts the
1490
- * styles name are read, the measured layouts are dropped, and every label is laid out again
1491
- * on the next frame and redrawn with the new style, even when its text and its style name did
1492
- * not change. The game loop is woken for that frame.
1493
- *
1494
- * @param styles - What `defineTextStyles` returned in the saved module.
1495
- * @example
1496
- * ```ts
1497
- * // `ui` hot swaps a saved `hud/styles.ts`: the "hud.title" labels redraw at 64 px.
1498
- * const uiStyles = defineTextStyles({
1499
- * "hud.title": { font: "ui.font-body", size: 64, fill: 0xff0000 }
1500
- * });
1501
- *
1502
- * ctx.require(textPlugin).replaceStyles(uiStyles);
1503
- * ctx.require(textPlugin).measure("12", "hud.title").height; // 76.8 on the 0.6 em fallback
1504
- * ```
1505
- */
1506
- replaceStyles(styles: TextStyles): void;
1507
- };
1508
- /**
1509
- * Resolved dependency APIs.
1510
- */
1511
- type Deps = {
1512
- time: Api;
1513
- flow: Api$1;
1514
- world: Api$3;
1515
- renderer: Api$4;
1516
- assets: Api$5;
1517
- i18n: I18nApi;
1518
- clock: Api$2;
1519
- };
1520
- /**
1521
- * What the kernel context offers before the deps are attached.
1522
- *
1523
- * `text` owns no event, so `emit` is the kernel's and never called here.
1524
- */
1525
- type KernelSlice = PluginCtx<Config, State> & {
1526
- readonly global: object;
1527
- readonly log: Log.LogApi;
1528
- readonly require: Require;
1529
- };
1530
- /**
1531
- * Domain context shared by the files of the plugin: the kernel slice plus the resolved deps.
1532
- */
1533
- type TextCtx = KernelSlice & {
1534
- readonly deps: Deps;
1493
+ type CountdownValue = {
1494
+ until: number;
1495
+ left: number;
1535
1496
  };
1536
1497
  /**
1537
- * Payload of the `assets:bundle-loaded` hook: a bundle landed, so a font may be readable now.
1538
- */
1539
- type BundleLoaded = Events$1["assets:bundle-loaded"];
1540
- /**
1541
- * Payload of the `assets:bundle-unloaded` hook: `keys` names the assets that left.
1542
- */
1543
- type BundleUnloaded = Events$1["assets:bundle-unloaded"];
1544
- /**
1545
- * Payload of the `i18n:locale-changed` hook: every message has to be resolved again.
1546
- */
1547
- type LocaleChanged = Events["i18n:locale-changed"];
1548
- //#endregion
1549
- //#region src/plugins/ui/jsx/component.d.ts
1550
- /**
1551
- * Declares a component: a piece of interface with local state, outcomes, or both. The view runs
1552
- * at every reconcile with the instance's own local state, never at build time.
1498
+ * What the `Text` component holds. `resolved` is engine-owned: a game writes `content`, `style`,
1499
+ * `bind`, `anchor` and `alpha`, and reads `resolved`. `alpha` fades every run, shadow and icon of
1500
+ * the block together, 1 by default; a change is written in place and a tween can drive it.
1553
1501
  *
1554
- * @param name - Unique name across every feature of the game.
1555
- * @param spec - The initial local state, the outcomes a popup resolves with, and the view.
1556
- * @returns The component, ready to stand in a tag and in a feature's `ui` key.
1557
1502
  * @example
1558
1503
  * ```ts
1559
- * const Panel = defineComponent("Panel", {
1560
- * local: { tab: "audio" },
1561
- * view: (props: { volume: number }, local) => ({ type: "column", props: {}, children: [] })
1562
- * });
1563
- * Panel.name; // "Panel"
1504
+ * const value: TextValue = {
1505
+ * content: "+5", style: "body", bind: undefined, anchor: { x: 0.5, y: 0.5 }, alpha: 1,
1506
+ * resolved: "+5"
1507
+ * };
1508
+ * // The hint of an empty name field, at half alpha.
1509
+ * const hint: TextValue = { ...value, content: "Your name", alpha: 0.5, resolved: "Your name" };
1564
1510
  * ```
1565
1511
  */
1566
- declare function defineComponent<Properties extends object, Local extends object, Outcomes extends Record<string, unknown>>(name: string, spec: ComponentSpec<Properties, Local, Outcomes> & {
1567
- readonly outcomes: Outcomes;
1568
- }): ComponentDefinition<Properties, Local, Outcomes>;
1512
+ type TextValue = {
1513
+ content: string | Message;
1514
+ style: string;
1515
+ bind: TextBind | undefined;
1516
+ anchor: Point;
1517
+ alpha: number;
1518
+ resolved: string;
1519
+ };
1569
1520
  /**
1570
- * Declares a component without outcomes: it may stand in a tree, never in a `popup`.
1521
+ * Where the lines of a block sit inside the widest of them.
1571
1522
  *
1572
- * @param name - Unique name across every feature of the game.
1573
- * @param spec - The initial local state and the view.
1574
- * @returns The component, ready to stand in a tag.
1575
1523
  * @example
1576
1524
  * ```ts
1577
- * const Row = defineComponent("Row", { view: () => ({ type: "row", props: {}, children: [] }) });
1578
- * Row.outcomes; // undefined
1525
+ * const align: TextAlign = "center";
1579
1526
  * ```
1580
1527
  */
1581
- declare function defineComponent<Properties extends object, Local extends object>(name: string, spec: ComponentSpec<Properties, Local, never>): ComponentDefinition<Properties, Local, undefined>;
1582
- //#endregion
1583
- //#region src/plugins/ui/styles/types.d.ts
1528
+ type TextAlign = "left" | "center" | "right";
1584
1529
  /**
1585
- * A length that may also come from the notch. The four tokens are replaced by `resolve` with the
1586
- * matching `viewport.safeArea` value.
1530
+ * The wrap width of a style in reference pixels, or `"none"` for one line per `\n` segment.
1587
1531
  *
1588
1532
  * @example
1589
1533
  * ```ts
1590
- * const top: SafeAreaToken = "safeArea.top";
1534
+ * const wrap: TextWrap = 480;
1591
1535
  * ```
1592
1536
  */
1593
- type SafeAreaToken = "safeArea.top" | "safeArea.right" | "safeArea.bottom" | "safeArea.left";
1537
+ type TextWrap = number | "none";
1594
1538
  /**
1595
- * A length in reference units, or one of the safe-area tokens.
1539
+ * The drop shadow of a style as the plugin stores it: a copy of every glyph run in `color`, at
1540
+ * `alpha`, moved by `dx` and `dy` reference pixels.
1596
1541
  *
1597
1542
  * @example
1598
1543
  * ```ts
1599
- * const length: Length = 24;
1544
+ * const shadow: TextShadow = { color: 0x5b3a1e, dx: 0, dy: 4, alpha: 1 };
1600
1545
  * ```
1601
1546
  */
1602
- type Length = number | SafeAreaToken;
1547
+ type TextShadow = {
1548
+ color: number;
1549
+ dx: number;
1550
+ dy: number;
1551
+ alpha: number;
1552
+ };
1603
1553
  /**
1604
- * Padding or margin: one number for every edge, or a value per edge.
1554
+ * One style as a game writes it: the font and the size are required, the rest has defaults.
1605
1555
  *
1606
1556
  * @example
1607
1557
  * ```ts
1608
- * const padding: Edges = { top: "safeArea.top", left: 16 };
1558
+ * // A popup title: cream display glyphs over a brown shadow 4 px down.
1559
+ * const title: TextStyleInput = {
1560
+ * font: "ui.font-display", size: 56, fill: 0xfff3d6, shadow: { color: 0x5b3a1e, dx: 0, dy: 4 }
1561
+ * };
1562
+ *
1563
+ * defineTextStyles({ "popup.title": title }).map["popup.title"]?.shadow;
1564
+ * // { color: 0x5b3a1e, dx: 0, dy: 4, alpha: 1 }
1609
1565
  * ```
1610
1566
  */
1611
- type Edges = Length | {
1612
- top?: Length;
1613
- right?: Length;
1614
- bottom?: Length;
1615
- left?: Length;
1567
+ type TextStyleInput = {
1568
+ font: string;
1569
+ bold?: string;
1570
+ italic?: string;
1571
+ size: number;
1572
+ fill: number; /** The outline colour. Drawn only when `strokeWidth` is above 0. */
1573
+ stroke?: number;
1574
+ /**
1575
+ * The outline width in reference px: 8 copies of each glyph run on a circle this wide (12 from 6
1576
+ * on), tinted `stroke`. 0 draws none. It never changes what `measure` answers.
1577
+ */
1578
+ strokeWidth?: number;
1579
+ letterSpacing?: number;
1580
+ align?: TextAlign;
1581
+ wrap?: TextWrap;
1582
+ digits?: boolean;
1583
+ /**
1584
+ * A drop shadow under every glyph run, in reference px at the style size. `alpha` is 1 when
1585
+ * left out. It never changes what `measure` answers.
1586
+ */
1587
+ shadow?: {
1588
+ color: number;
1589
+ dx: number;
1590
+ dy: number;
1591
+ alpha?: number;
1592
+ };
1616
1593
  };
1617
1594
  /**
1618
- * A main-axis or cross-axis size: reference units, a percent of the parent, or `"auto"`.
1595
+ * One style as the plugin stores it: every field filled, `bold` and `italic` only when the game
1596
+ * shipped those MSDF fonts, `shadow` only when the style has one.
1619
1597
  *
1620
1598
  * @example
1621
1599
  * ```ts
1622
- * const width: Extent = "50%";
1600
+ * const style: TextStyle = {
1601
+ * font: "ui.font-body", bold: undefined, italic: undefined, size: 32, fill: 0xffffff,
1602
+ * stroke: 0x000000, strokeWidth: 0, letterSpacing: 0, align: "left", wrap: "none",
1603
+ * digits: false, shadow: undefined
1604
+ * };
1623
1605
  * ```
1624
1606
  */
1625
- type Extent = number | `${number}%` | "auto";
1607
+ type TextStyle = {
1608
+ font: string;
1609
+ bold: string | undefined;
1610
+ italic: string | undefined;
1611
+ size: number;
1612
+ fill: number;
1613
+ stroke: number;
1614
+ strokeWidth: number;
1615
+ letterSpacing: number;
1616
+ align: TextAlign;
1617
+ wrap: TextWrap;
1618
+ digits: boolean;
1619
+ shadow: TextShadow | undefined;
1620
+ };
1626
1621
  /**
1627
- * The flow group of the vocabulary: what Yoga's flexbox calls are set from.
1622
+ * What `defineTextStyles` returns and a feature registers under its `textStyles` key. Structural
1623
+ * on purpose: `flow` carries the map, `text` reads it.
1628
1624
  *
1629
1625
  * @example
1630
1626
  * ```ts
1631
- * const flow: FlowStyle = { direction: "row", gap: 12, justify: "between" };
1627
+ * const styles: TextStyles = { kind: "textStyles", map: { "hud.title": bodyStyle } };
1632
1628
  * ```
1633
1629
  */
1634
- type FlowStyle = {
1635
- direction?: "row" | "column";
1636
- wrap?: boolean;
1637
- justify?: "start" | "center" | "end" | "between" | "around" | "evenly";
1638
- align?: "start" | "center" | "end" | "stretch";
1639
- alignSelf?: "auto" | "start" | "center" | "end" | "stretch";
1640
- gap?: number;
1641
- grow?: number;
1642
- shrink?: number;
1630
+ type TextStyles = {
1631
+ readonly kind: "textStyles";
1632
+ readonly map: Record<string, TextStyle>;
1643
1633
  };
1644
1634
  /**
1645
- * The box group: what the element takes up before its children are placed.
1635
+ * The advances of one font at the size the `.fnt` was exported with. Widths scale by
1636
+ * `style.size / size`; kerning pairs are ignored.
1646
1637
  *
1647
1638
  * @example
1648
1639
  * ```ts
1649
- * // The board slot: 970 u of cells, scaled down on a short phone.
1650
- * const box: BoxStyle = { width: 970, height: 970, fit: "contain" };
1640
+ * const table: AdvanceTable = { size: 32, lineHeight: 40, advances: new Map([["1", 18]]) };
1651
1641
  * ```
1652
1642
  */
1653
- type BoxStyle = {
1654
- padding?: Edges;
1655
- margin?: Edges;
1656
- width?: Extent;
1657
- height?: Extent;
1658
- minWidth?: Extent;
1659
- minHeight?: Extent;
1660
- maxWidth?: Extent;
1661
- maxHeight?: Extent;
1662
- aspect?: number; /** `"hidden"` clips the children to the rect, as a scroll container does. */
1663
- overflow?: "visible" | "hidden" | "scroll";
1664
- /**
1665
- * `"contain"`: the element keeps its own size and leaves the flow, then is scaled down to fit
1666
- * the content box of its parent and centred in it. Its children keep their natural rects.
1667
- */
1668
- fit?: "contain";
1643
+ type AdvanceTable = {
1644
+ size: number;
1645
+ lineHeight: number;
1646
+ advances: Map<string, number>;
1669
1647
  };
1670
1648
  /**
1671
- * The position group. `reason` is the comment `lint()` asks for next to an absolute element.
1649
+ * One run of glyphs: the text and the flags every glyph in it shares.
1672
1650
  *
1673
1651
  * @example
1674
1652
  * ```ts
1675
- * const pinned: PositionStyle = { position: "absolute", top: 0, right: 0, reason: "badge" };
1676
- * // The order strip of the board drawn over the tray that follows it.
1677
- * const strip: PositionStyle = { zIndex: 1 };
1653
+ * const run: TextRun = { kind: "text", text: "+5", bold: true, italic: false, color: 0xffe082 };
1678
1654
  * ```
1679
1655
  */
1680
- type PositionStyle = {
1681
- position?: "relative" | "absolute";
1682
- left?: Length;
1683
- top?: Length;
1684
- right?: Length;
1685
- bottom?: Length;
1686
- reason?: string;
1687
- /**
1688
- * The draw order among the siblings, an integer: a higher one draws over a lower one, and a
1689
- * sibling without it draws at 0 in markup order. Ignored on a root element, which `lint()`
1690
- * reports; a root draws at the order of its layer.
1691
- */
1692
- zIndex?: number;
1656
+ type TextRun = {
1657
+ kind: "text";
1658
+ text: string;
1659
+ bold: boolean;
1660
+ italic: boolean;
1661
+ color: number | undefined;
1693
1662
  };
1694
1663
  /**
1695
- * The visual group: what is written to `Shape`, `Sprite` or `NineSlice`. There is no font size
1696
- * here — the size of a text comes from its text style key.
1664
+ * One inline icon: an asset key drawn square at the line height.
1697
1665
  *
1698
1666
  * @example
1699
1667
  * ```ts
1700
- * // A wooden button: the nine-slice of the style, greyed while disabled.
1701
- * const visual: VisualStyle = { nineSlice: "ui.button-wood", alpha: 1, tint: 0xffffff };
1702
- * // The popup board while its insets are checked: the slice lines drawn over it.
1703
- * const checked: VisualStyle = { nineSlice: "ui.panel-signboard", debug: true };
1704
- * // The play glyph of the Watch button, and a dashed ring around a slot.
1705
- * const play: VisualStyle = { shape: "triangle", fill: 0xfffbe8 };
1706
- * const slot: VisualStyle = { stroke: 0x3a2212, strokeWidth: 4, dash: 10, radius: 24 };
1668
+ * const run: IconRun = { kind: "icon", key: "hud.coin" };
1707
1669
  * ```
1708
1670
  */
1709
- type VisualStyle<Asset extends string = string> = {
1710
- fill?: number;
1711
- stroke?: number;
1712
- strokeWidth?: number;
1713
- radius?: number;
1714
- alpha?: number;
1715
- /**
1716
- * The shape of the rectangle an element without a nine-slice is drawn with: `"rect"` (the
1717
- * default) or `"triangle"`, which fills the box pointing right and ignores `radius`. Turn the
1718
- * element with `rotation` for another direction.
1719
- */
1720
- shape?: "rect" | "triangle"; /** The dash length of the stroke in reference units, with gaps of half a dash; 0 is solid. */
1721
- dash?: number;
1722
- /**
1723
- * The asset key of a nine-slice drawn at the rect instead of the rounded rectangle. Any tag
1724
- * but `image`, `icon` and `text` takes it; a clipping element (`scroll`, `overflow: "hidden"`)
1725
- * keeps its rectangle, which carries the clip.
1726
- */
1727
- nineSlice?: Asset; /** Multiplies the colour of a nine-slice or an image. */
1728
- tint?: number;
1729
- /**
1730
- * Outlines the nine-slice of the style: its bounds and the four slice lines, cyan, red when
1731
- * the corners overlap or the texture is missing. A look for checking art, never for a player.
1732
- */
1733
- debug?: boolean;
1671
+ type IconRun = {
1672
+ kind: "icon";
1673
+ key: string;
1734
1674
  };
1735
1675
  /**
1736
- * Where an element turns and scales: its centre, the middle of its top edge, its top-left
1737
- * corner, or a point given in fractions of its box.
1676
+ * A piece of a line: glyphs or one inline icon.
1738
1677
  *
1739
1678
  * @example
1740
1679
  * ```ts
1741
- * // A sign that hangs from its ropes swings around the middle of its top edge.
1742
- * const origin: Origin = "top";
1680
+ * const runs: Run[] = [{ kind: "icon", key: "hud.coin" }];
1743
1681
  * ```
1744
1682
  */
1745
- type Origin = "center" | "top" | "topLeft" | {
1746
- x: number;
1747
- y: number;
1748
- };
1683
+ type Run = TextRun | IconRun;
1749
1684
  /**
1750
- * The transform group: how an element is drawn, never where it is laid out. Written into the
1751
- * rest `Transform`, so a hover lift or a pressed sink never moves a sibling.
1685
+ * One laid-out line and how wide it is.
1752
1686
  *
1753
1687
  * @example
1754
1688
  * ```ts
1755
- * // A button that lifts under the mouse.
1756
- * const lift: TransformStyle = { offsetY: -6, scale: 1.05, origin: "center" };
1757
- * // The title plaque of a popup, tilted by 1.5 degrees around the middle of its top edge.
1758
- * const plaque: TransformStyle = { rotation: -0.026, origin: "top" };
1689
+ * const line: Line = { runs: [{ kind: "icon", key: "hud.coin" }], width: 38.4 };
1759
1690
  * ```
1760
1691
  */
1761
- type TransformStyle = {
1762
- /** Moves the drawn element right, in reference units. */offsetX?: number; /** Moves the drawn element down, in reference units. */
1763
- offsetY?: number; /** Uniform scale around the origin. */
1764
- scale?: number; /** Turns the drawn element around the origin, in radians, clockwise; 0 by default. */
1765
- rotation?: number; /** The point the element scales and turns around; the centre by default. */
1766
- origin?: Origin;
1692
+ type Line = {
1693
+ runs: Run[];
1694
+ width: number;
1767
1695
  };
1768
1696
  /**
1769
- * Everything but the variants: the five groups of the vocabulary.
1697
+ * A laid-out block: its lines and the size `measure` answers with.
1770
1698
  *
1771
1699
  * @example
1772
1700
  * ```ts
1773
- * const base: BaseStyle = { direction: "row", gap: 8, fill: 0x101018 };
1701
+ * const layout: TextLayout = { lines: [], width: 0, height: 0 };
1774
1702
  * ```
1775
1703
  */
1776
- type BaseStyle<Asset extends string = string> = FlowStyle & BoxStyle & PositionStyle & VisualStyle<Asset> & TransformStyle;
1704
+ type TextLayout = {
1705
+ lines: Line[];
1706
+ width: number;
1707
+ height: number;
1708
+ };
1777
1709
  /**
1778
- * The seven state variants of a style, applied in the order disabled, active, selected, hover,
1779
- * focus, pressed, covered. While `disabled` is true, `hover` and `pressed` are not applied.
1710
+ * What one label container was last filled from: the style object and the laid-out block. An
1711
+ * update with the same style object, the same anchor and the same lines and runs writes the new
1712
+ * text and positions into the objects already there.
1713
+ */
1714
+ type DrawnLabel = {
1715
+ style: TextStyle;
1716
+ layout: TextLayout;
1717
+ };
1718
+ /**
1719
+ * A dev warning that is written once per key. Pure modules take it as an argument, so nothing
1720
+ * below the plugin context reaches the log itself.
1780
1721
  *
1781
1722
  * @example
1782
1723
  * ```ts
1783
- * // One rule set for every control: lift on hover, sink when pressed, swap the texture when off.
1784
- * const variants: IsVariants = {
1785
- * hover: { offsetY: -6, scale: 1.05 },
1786
- * focus: { scale: 1.05 },
1787
- * pressed: { offsetY: 4, scale: 0.95 },
1788
- * disabled: { nineSlice: "ui.button-disabled" }
1789
- * };
1724
+ * const warn: Warn = (key, message) => seen.has(key) || log.warn(message);
1790
1725
  * ```
1791
1726
  */
1792
- type IsVariants<Asset extends string = string> = {
1793
- pressed?: BaseStyle<Asset>;
1794
- disabled?: BaseStyle<Asset>;
1795
- active?: BaseStyle<Asset>;
1796
- selected?: BaseStyle<Asset>; /** The mouse or pen is over the element. Touch never hovers. */
1797
- hover?: BaseStyle<Asset>; /** The keyboard focus is on the element: Tab moved it there, and no pointer tapped since. */
1798
- focus?: BaseStyle<Asset>; /** The popup the element belongs to is kept under another one. */
1799
- covered?: BaseStyle<Asset>;
1800
- };
1727
+ type Warn = (key: string, message: string, data?: Record<string, unknown>) => void;
1801
1728
  /**
1802
- * The four viewport variants of a style, applied in the order portrait, landscape, tall, wide.
1729
+ * What the pure layout needs next to the runs, the style and the tables.
1803
1730
  *
1804
1731
  * @example
1805
1732
  * ```ts
1806
- * const variants: WhenVariants = { landscape: { direction: "row" } };
1733
+ * const options: LayoutOptions = { warn: () => undefined };
1807
1734
  * ```
1808
1735
  */
1809
- type WhenVariants<Asset extends string = string> = {
1810
- portrait?: BaseStyle<Asset>;
1811
- landscape?: BaseStyle<Asset>;
1812
- tall?: BaseStyle<Asset>;
1813
- wide?: BaseStyle<Asset>;
1736
+ type LayoutOptions = {
1737
+ warn: Warn;
1814
1738
  };
1815
1739
  /**
1816
- * One style as a game writes it: the base vocabulary plus the two variant tables. `Asset` is the
1817
- * game's asset key union: `uiFor` narrows `nineSlice` to it, the way it narrows `texture`.
1740
+ * What was resolved for one entity last, so the frame step knows whether anything moved. `unit`
1741
+ * is what the shown string of a bound label was built from: the rounded value for `"int"`, whole
1742
+ * seconds for a time format.
1818
1743
  *
1819
1744
  * @example
1820
1745
  * ```ts
1821
- * const topBar: Style = { direction: "row", gap: 12, when: { landscape: { justify: "end" } } };
1746
+ * const seen: SeenText = {
1747
+ * content: "", style: "digits", bind: "Countdown.left:mm:ss", locale: "ru", unit: 95
1748
+ * };
1822
1749
  * ```
1823
1750
  */
1824
- type Style<Asset extends string = string> = BaseStyle<Asset> & {
1825
- is?: IsVariants<Asset>;
1826
- when?: WhenVariants<Asset>;
1751
+ type SeenText = {
1752
+ content: unknown;
1753
+ style: string;
1754
+ bind: string | undefined;
1755
+ locale: string;
1756
+ unit: number | undefined;
1827
1757
  };
1828
1758
  /**
1829
- * What `resolve` answers with: the base vocabulary with every safe-area token replaced by a
1830
- * number and every variant already merged in. Frozen.
1759
+ * text plugin config.
1831
1760
  *
1832
1761
  * @example
1833
1762
  * ```ts
1834
- * const resolved: ResolvedStyle = { direction: "row", gap: 12, padding: 24 };
1763
+ * createApp({ pluginConfigs: { text: { fonts: { body: "ui.body", digits: "ui.digits" } } } });
1835
1764
  * ```
1836
1765
  */
1837
- type ResolvedStyle = Omit<BaseStyle, "padding" | "margin" | "left" | "top" | "right" | "bottom"> & {
1838
- padding?: number | {
1839
- top?: number;
1840
- right?: number;
1841
- bottom?: number;
1842
- left?: number;
1843
- };
1844
- margin?: number | {
1845
- top?: number;
1846
- right?: number;
1847
- bottom?: number;
1848
- left?: number;
1766
+ type Config = {
1767
+ /** The two fonts every game ships in its boot bundle. They back the built-in styles. */fonts: {
1768
+ body: string;
1769
+ digits: string;
1849
1770
  };
1850
- left?: number;
1851
- top?: number;
1852
- right?: number;
1853
- bottom?: number;
1854
1771
  };
1855
1772
  /**
1856
- * The viewport flags of a style: which `when` variants apply this frame.
1857
- *
1858
- * @example
1859
- * ```ts
1860
- * const flags: WhenFlags = { portrait: true, landscape: false, tall: true, wide: false };
1861
- * ```
1773
+ * text plugin state.
1862
1774
  */
1863
- type WhenFlags = {
1864
- portrait: boolean;
1865
- landscape: boolean;
1866
- tall: boolean;
1867
- wide: boolean;
1775
+ type State = {
1776
+ /** 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. */
1777
+ styleOwner: Map<string, string>; /** The config fonts plus every `font`, `bold` and `italic` of every style. */
1778
+ fontKeys: Set<string>; /** The parsed advance table per font key, filled when `assets` has the font. */
1779
+ tables: Map<string, AdvanceTable>; /** The font keys handed to `renderer.sync.fonts.install`. */
1780
+ installed: Set<string>; /** What was resolved for an entity last. */
1781
+ seen: Map<Entity, SeenText>; /** `bind.component` to its world type; `undefined` after the one warning. */
1782
+ bindTypes: Map<string, AnyComponent | undefined>; /** The measured size of every live label, for `ui`. */
1783
+ measured: Map<Entity, Size>; /** Layout per `style + resolved`, oldest dropped first. */
1784
+ cache: Map<string, TextLayout>; /** What every live label container was last filled from, by container. */
1785
+ drawn: WeakMap<object, DrawnLabel>; /** Entities to re-resolve in the next layout phase. */
1786
+ dirty: Set<Entity>; /** Warning keys already written, so a style, a tag, a glyph or a font warns once. */
1787
+ warned: Set<string>; /** The system, the two world hooks and the display adapter. */
1788
+ removers: Array<() => void>;
1868
1789
  };
1869
1790
  /**
1870
- * The state flags of an element: what the markup declared (`disabled`, `active`, `selected`),
1871
- * `pressed` and `hover` from the pointer, `focus` from the keyboard, and `covered` from the root
1872
- * it belongs to.
1791
+ * text plugin API, `app.text`. Two questions: how big a piece of text is, and which styles the
1792
+ * game registered; and one order, the dev hot swap of a styles file. Everything else happens in
1793
+ * the frame.
1873
1794
  *
1874
1795
  * @example
1875
1796
  * ```ts
1876
- * const flags: IsFlags = {
1877
- * pressed: false, hover: true, focus: false, disabled: false, active: true, selected: false,
1878
- * covered: false
1879
- * };
1797
+ * app.text.measure("120", "digits"); // { width: 57.6, height: 38.4 }
1798
+ * app.text.styles(); // ["body", "digits"]
1880
1799
  * ```
1881
1800
  */
1882
- type IsFlags = {
1883
- pressed: boolean;
1884
- hover: boolean;
1885
- focus: boolean;
1886
- disabled: boolean;
1887
- active: boolean;
1888
- selected: boolean;
1889
- covered: boolean;
1801
+ type TextApi = {
1802
+ /**
1803
+ * The size of a piece of text in reference pixels, from the advance table of the style's font.
1804
+ * A `Message` is formatted in the current locale first. Never touches a canvas, so the answer
1805
+ * is the same in a browser and in plain Bun. Cached per style and resolved string.
1806
+ *
1807
+ * @param content - A plain string, or what `tr` returned.
1808
+ * @param style - A registered style name. An unknown one warns once and measures as `body`.
1809
+ * @returns The width and height of the block.
1810
+ * @example
1811
+ * ```ts
1812
+ * // `ui.layout` asks once per invalidation, from the Yoga measure function of a text element.
1813
+ * const text = ctx.require(textPlugin);
1814
+ *
1815
+ * text.measure("120", "digits"); // { width: 57.6, height: 38.4 } on the 0.6 em fallback
1816
+ * ```
1817
+ */
1818
+ measure(content: string | Message, style: string): Size;
1819
+ /**
1820
+ * Whether the font of a style has a glyph for one character, the lookup Pixi draws by. A
1821
+ * character its font lacks is drawn as nothing and `measure` counts it 0 wide. While the font is
1822
+ * not loaded every character counts, as the 0.6 em fallback measures it. An unknown style warns
1823
+ * once and looks up `body`.
1824
+ *
1825
+ * @param char - One character, a whole code point.
1826
+ * @param style - A registered style name.
1827
+ * @returns False only for a character a loaded font has no glyph for.
1828
+ * @example
1829
+ * ```ts
1830
+ * // `ui` measures the caret of a text field over the characters the font draws.
1831
+ * const text = ctx.require(textPlugin);
1832
+ *
1833
+ * text.hasGlyph("A", "body"); // true
1834
+ * text.hasGlyph("😀", "body"); // false once the body font is loaded: it has no emoji glyph
1835
+ * ```
1836
+ */
1837
+ hasGlyph(char: string, style: string): boolean;
1838
+ /**
1839
+ * The registered style names: the two built-ins first, then the styles of every feature in
1840
+ * feature order.
1841
+ *
1842
+ * @returns A frozen list of style names.
1843
+ * @example
1844
+ * ```ts
1845
+ * // A dev overlay lists what a game may write in a `style` prop.
1846
+ * app.text.styles(); // ["body", "digits", "hud.digits"]
1847
+ * ```
1848
+ */
1849
+ styles(): readonly string[];
1850
+ /**
1851
+ * The dev hot swap of a styles file: writes every style of the map over the table. A known
1852
+ * name keeps the feature that owns it, a new name is added and usable at once. The fonts the
1853
+ * styles name are read, the measured layouts are dropped, and every label is laid out again
1854
+ * on the next frame and redrawn with the new style, even when its text and its style name did
1855
+ * not change. The game loop is woken for that frame.
1856
+ *
1857
+ * @param styles - What `defineTextStyles` returned in the saved module.
1858
+ * @example
1859
+ * ```ts
1860
+ * // `ui` hot swaps a saved `hud/styles.ts`: the "hud.title" labels redraw at 64 px.
1861
+ * const uiStyles = defineTextStyles({
1862
+ * "hud.title": { font: "ui.font-body", size: 64, fill: 0xff0000 }
1863
+ * });
1864
+ *
1865
+ * ctx.require(textPlugin).replaceStyles(uiStyles);
1866
+ * ctx.require(textPlugin).measure("12", "hud.title").height; // 76.8 on the 0.6 em fallback
1867
+ * ```
1868
+ */
1869
+ replaceStyles(styles: TextStyles): void;
1890
1870
  };
1891
1871
  /**
1892
- * A flat table of design tokens a game reads in its styles.
1872
+ * Resolved dependency APIs.
1873
+ */
1874
+ type Deps = {
1875
+ time: Api;
1876
+ flow: Api$1;
1877
+ world: Api$2;
1878
+ renderer: Api$3;
1879
+ assets: Api$4;
1880
+ i18n: I18nApi;
1881
+ clock: Api$5;
1882
+ };
1883
+ /**
1884
+ * What the kernel context offers before the deps are attached.
1893
1885
  *
1894
- * @example
1895
- * ```ts
1896
- * const tokens: Tokens = { space: { md: 16 }, color: { panel: 0x101018 } };
1897
- * ```
1886
+ * `text` owns no event, so `emit` is the kernel's and never called here.
1898
1887
  */
1899
- type Tokens = {
1900
- readonly space?: Readonly<Record<string, number>>;
1901
- readonly color?: Readonly<Record<string, number>>;
1902
- readonly radius?: Readonly<Record<string, number>>;
1903
- readonly font?: Readonly<Record<string, string>>;
1888
+ type KernelSlice = PluginCtx<Config, State> & {
1889
+ readonly global: object;
1890
+ readonly log: Log.LogApi;
1891
+ readonly require: Require;
1904
1892
  };
1905
1893
  /**
1906
- * styles module state: the flags of the frame and the viewport they were read from.
1894
+ * Domain context shared by the files of the plugin: the kernel slice plus the resolved deps.
1907
1895
  */
1908
- type StylesState = {
1909
- flags: WhenFlags;
1910
- viewport: ViewportSize | undefined;
1896
+ type TextCtx = KernelSlice & {
1897
+ readonly deps: Deps;
1911
1898
  };
1899
+ /**
1900
+ * Payload of the `assets:bundle-loaded` hook: a bundle landed, so a font may be readable now.
1901
+ */
1902
+ type BundleLoaded = Events$1["assets:bundle-loaded"];
1903
+ /**
1904
+ * Payload of the `assets:bundle-unloaded` hook: `keys` names the assets that left.
1905
+ */
1906
+ type BundleUnloaded = Events$1["assets:bundle-unloaded"];
1907
+ /**
1908
+ * Payload of the `i18n:locale-changed` hook: every message has to be resolved again.
1909
+ */
1910
+ type LocaleChanged = Events["i18n:locale-changed"];
1912
1911
  //#endregion
1913
1912
  //#region src/plugins/ui/jsx/intrinsics.d.ts
1914
1913
  /**
@@ -2778,4 +2777,4 @@ type CommonProps = {
2778
2777
  components?: ElementComponents;
2779
2778
  };
2780
2779
  //#endregion
2781
- 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 };
2780
+ export { ResolvedStyle as $, InputApi as A, SpriteOptions as B, TextFormat as C, types_d_exports as D, TextValue as E, NineSliceValue as F, Argument as G, Transform as H, Parent as I, Message as J, Config$2 as K, Shape as L, types_d_exports$1 as M, Display as N, defineComponent as O, NineSlice as P, IsFlags as Q, ShapeValue as R, TextBind as S, TextStyles as T, TransformValue as U, SpriteValue as V, sprite as W, TypedTr as X, State$2 as Y, types_d_exports$2 as Z, Config as _, JsxChild as a, State as b, UiNode as c, popup as d, Style as et, defineTokens as f, BindOptions as g, BindField as h, Finding as i, State$1 as j, Config$1 as k, Box as l, UiIntrinsicElements as m, ElementChange as n, WhenFlags as nt, JsxState as o, IntrinsicElementsFor as p, I18nApi as q, ElementMotion as r, PopupComponent as s, DescriptionNode as t, StylesState as tt, LocalWrite as u, CountdownValue as v, TextStyleInput as w, TextApi as x, Point as y, Sprite as z };