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