@moku-labs/game 0.0.1 → 0.0.2
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.
- package/README.md +424 -37
- package/dist/assets.d.mts +161 -0
- package/dist/assets.mjs +4729 -0
- package/dist/component-DGg5DqKK.mjs +44 -0
- package/dist/control.d.mts +162 -0
- package/dist/control.mjs +758 -0
- package/dist/define-sFoO3y6X.d.mts +263 -0
- package/dist/headless-CvamCUcR.mjs +186 -0
- package/dist/headless-KaSWcd0s.d.mts +181 -0
- package/dist/index.d.mts +1204 -50
- package/dist/index.mjs +20978 -2010
- package/dist/inspect.d.mts +109 -0
- package/dist/inspect.mjs +519 -0
- package/dist/jsx-dev-runtime.d.mts +2 -0
- package/dist/jsx-dev-runtime.mjs +2 -0
- package/dist/jsx-runtime.d.mts +2 -0
- package/dist/jsx-runtime.mjs +2 -0
- package/dist/memory-CvgdnsQO.mjs +259 -0
- package/dist/registry-DlpRCibU.mjs +384 -0
- package/dist/runtime-DRlwxkIv.mjs +182 -0
- package/dist/runtime-DiOTkDZz.d.mts +77 -0
- package/dist/session-DmAxY6Ll.mjs +145 -0
- package/dist/testing.d.mts +23 -111
- package/dist/testing.mjs +12 -289
- package/dist/types-BxkNNYul.d.mts +2840 -0
- package/dist/types-CTPS9GBu.d.mts +742 -0
- package/dist/types-DD-QrG_z.d.mts +588 -0
- package/dist/types-DWILGrPn.d.mts +622 -0
- package/dist/types-DYLnSgMI.d.mts +570 -0
- package/dist/types-JNc_UQBo.d.mts +2896 -0
- package/dist/types-yg_ywtT-.d.mts +1859 -0
- package/dist/visual-BDUHSRvf.mjs +734 -0
- package/package.json +26 -4
- package/dist/registry-DWV5C0Mf.mjs +0 -666
- package/dist/types-BfsmUzLC.d.mts +0 -1908
|
@@ -0,0 +1,734 @@
|
|
|
1
|
+
//#region src/plugins/world/ecs/define.ts
|
|
2
|
+
/**
|
|
3
|
+
* Declares a component type. The defaults give the TypeScript type, the JSON shape and the
|
|
4
|
+
* inspector schema; the name is the storage key and the key of `motion.change`.
|
|
5
|
+
*
|
|
6
|
+
* @param name - Storage name, unique per world.
|
|
7
|
+
* @param defaults - Every field with its default value. Plain JSON only.
|
|
8
|
+
* @param options - The fields a plugin owns: a projection view neither writes nor corrects them.
|
|
9
|
+
* @param options.owned - Names of the owned fields.
|
|
10
|
+
* @returns A callable component type.
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* const Item = component("Item", { kind: "", level: 1 });
|
|
14
|
+
* Item({ level: 2 }); // { type: Item, value: { kind: "", level: 2 } }
|
|
15
|
+
* Item({ level: 2 }).type.componentName; // "Item", the storage key
|
|
16
|
+
*
|
|
17
|
+
* // A caption whose drawn string is written by the plugin that draws it.
|
|
18
|
+
* const Caption = component("Caption", { text: "", shown: "" }, { owned: ["shown"] });
|
|
19
|
+
* Caption.owned; // ["shown"]
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
function component(name, defaults, options = {}) {
|
|
23
|
+
const frozenDefaults = Object.freeze({ ...defaults });
|
|
24
|
+
const make = (patch) => Object.freeze({
|
|
25
|
+
type: componentType,
|
|
26
|
+
value: Object.freeze({
|
|
27
|
+
...frozenDefaults,
|
|
28
|
+
...patch
|
|
29
|
+
})
|
|
30
|
+
});
|
|
31
|
+
const componentType = Object.assign(make, {
|
|
32
|
+
componentName: name,
|
|
33
|
+
defaults: frozenDefaults,
|
|
34
|
+
kind: "component",
|
|
35
|
+
owned: Object.freeze([...options.owned ?? []])
|
|
36
|
+
});
|
|
37
|
+
return componentType;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Declares a tag: a component with no data, stored as `true`.
|
|
41
|
+
*
|
|
42
|
+
* @param name - Storage name, unique per world.
|
|
43
|
+
* @returns A callable tag type.
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* const Held = tag("Held");
|
|
47
|
+
* Held(); // { type: Held, value: true }
|
|
48
|
+
* Held().type.componentName; // "Held"
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
function tag(name) {
|
|
52
|
+
const make = () => Object.freeze({
|
|
53
|
+
type: tagType,
|
|
54
|
+
value: true
|
|
55
|
+
});
|
|
56
|
+
const tagType = Object.assign(make, {
|
|
57
|
+
componentName: name,
|
|
58
|
+
kind: "tag"
|
|
59
|
+
});
|
|
60
|
+
return tagType;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Declares a resource: one mutable object per world, created from a deep clone of the defaults
|
|
64
|
+
* on first read.
|
|
65
|
+
*
|
|
66
|
+
* @param name - Storage name, unique per world.
|
|
67
|
+
* @param defaults - The value a fresh world starts with.
|
|
68
|
+
* @returns The resource type.
|
|
69
|
+
* @example
|
|
70
|
+
* ```ts
|
|
71
|
+
* const Pointer = resource("Pointer", { x: 0, y: 0, down: false });
|
|
72
|
+
* Pointer.defaults; // { x: 0, y: 0, down: false }
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
function resource(name, defaults) {
|
|
76
|
+
return {
|
|
77
|
+
resourceName: name,
|
|
78
|
+
defaults: Object.freeze({ ...defaults })
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Marks a query term as written. The only effect is change detection: every entity the query
|
|
83
|
+
* yields is marked changed for this component.
|
|
84
|
+
*
|
|
85
|
+
* @param componentType - The component the system writes.
|
|
86
|
+
* @returns The marked term.
|
|
87
|
+
* @example
|
|
88
|
+
* ```ts
|
|
89
|
+
* const Transform = component("Transform", { x: 0, y: 0 });
|
|
90
|
+
* mut(Transform).kind; // "mut"
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
function mut(componentType) {
|
|
94
|
+
return {
|
|
95
|
+
kind: "mut",
|
|
96
|
+
of: componentType
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Types a system definition. It returns the same plain object; the work is the type, which reads
|
|
101
|
+
* the query tuple and types the first argument of `run`.
|
|
102
|
+
*
|
|
103
|
+
* @param definition - Name, phase, query and the run function.
|
|
104
|
+
* @returns The same definition, typed.
|
|
105
|
+
* @example
|
|
106
|
+
* ```ts
|
|
107
|
+
* const Transform = component("Transform", { x: 0, y: 0 });
|
|
108
|
+
* const drift = system({
|
|
109
|
+
* name: "drift",
|
|
110
|
+
* phase: "animate",
|
|
111
|
+
* query: [mut(Transform)],
|
|
112
|
+
* run: entities => {
|
|
113
|
+
* for (const [, transform] of entities) transform.x += 1;
|
|
114
|
+
* }
|
|
115
|
+
* });
|
|
116
|
+
* drift.phase; // "animate"
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
function system(definition) {
|
|
120
|
+
return definition;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The layer a view is drawn in. Written only by the projection; a `view` that returns it is
|
|
124
|
+
* logged and the value ignored.
|
|
125
|
+
*/
|
|
126
|
+
const Layer = /*#__PURE__*/ component("Layer", { name: "" });
|
|
127
|
+
/**
|
|
128
|
+
* Explicit draw order inside a layer sorted by `"order"`.
|
|
129
|
+
*/
|
|
130
|
+
const Order = /*#__PURE__*/ component("Order", { value: 0 });
|
|
131
|
+
/**
|
|
132
|
+
* The view is in the despawn queue: still drawn, never hit-tested.
|
|
133
|
+
*/
|
|
134
|
+
const Exiting = /*#__PURE__*/ tag("Exiting");
|
|
135
|
+
/**
|
|
136
|
+
* The element description of a screen: a projection whose `view` returns one node gets it wrapped
|
|
137
|
+
* as this component, and `ui` reconciles the node into child entities. The node is a foreign
|
|
138
|
+
* object, so the world diffs it by identity and `snapshot()` leaves it out.
|
|
139
|
+
*/
|
|
140
|
+
const Tree = /*#__PURE__*/ component("Tree", { node: {
|
|
141
|
+
type: "",
|
|
142
|
+
props: {},
|
|
143
|
+
children: []
|
|
144
|
+
} });
|
|
145
|
+
//#endregion
|
|
146
|
+
//#region src/plugins/renderer/components.ts
|
|
147
|
+
/**
|
|
148
|
+
* @file renderer plugin — the five components a game writes and the `sprite()` bundle helper.
|
|
149
|
+
* Pure: no ctx, no state, no Pixi. Made with the `component()` helper of `world`, so nothing has
|
|
150
|
+
* to be registered.
|
|
151
|
+
*/
|
|
152
|
+
const displayDefaults = { object: void 0 };
|
|
153
|
+
const shapeDefaults = {
|
|
154
|
+
kind: "rect",
|
|
155
|
+
w: 0,
|
|
156
|
+
h: 0,
|
|
157
|
+
fill: 16777215,
|
|
158
|
+
fillAlpha: 1,
|
|
159
|
+
alpha: 1,
|
|
160
|
+
radius: 0,
|
|
161
|
+
stroke: 0,
|
|
162
|
+
strokeWidth: 0,
|
|
163
|
+
dash: 0,
|
|
164
|
+
clip: false
|
|
165
|
+
};
|
|
166
|
+
const transformDefaults = {
|
|
167
|
+
x: 0,
|
|
168
|
+
y: 0,
|
|
169
|
+
rotation: 0,
|
|
170
|
+
scale: 1,
|
|
171
|
+
pivot: {
|
|
172
|
+
x: 0,
|
|
173
|
+
y: 0
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
const spriteDefaults = {
|
|
177
|
+
texture: "",
|
|
178
|
+
tint: 16777215,
|
|
179
|
+
alpha: 1,
|
|
180
|
+
anchor: {
|
|
181
|
+
x: .5,
|
|
182
|
+
y: .5
|
|
183
|
+
},
|
|
184
|
+
width: 0,
|
|
185
|
+
height: 0,
|
|
186
|
+
fit: "fill"
|
|
187
|
+
};
|
|
188
|
+
/**
|
|
189
|
+
* Where a view sits: reference units, radians, uniform scale, and the local point it turns
|
|
190
|
+
* around.
|
|
191
|
+
*/
|
|
192
|
+
const Transform = /*#__PURE__*/ component("Transform", transformDefaults);
|
|
193
|
+
/**
|
|
194
|
+
* One textured quad. `texture` is an asset key, resolved through the texture providers; a size
|
|
195
|
+
* and a `fit` draw it into a box.
|
|
196
|
+
*/
|
|
197
|
+
const Sprite = /*#__PURE__*/ component("Sprite", spriteDefaults);
|
|
198
|
+
/**
|
|
199
|
+
* A stretchable panel, sized in reference units, with its own alpha and tint. `debug: true` draws
|
|
200
|
+
* the slice outline over it.
|
|
201
|
+
*/
|
|
202
|
+
const NineSlice = /*#__PURE__*/ component("NineSlice", {
|
|
203
|
+
texture: "",
|
|
204
|
+
width: 0,
|
|
205
|
+
height: 0,
|
|
206
|
+
alpha: 1,
|
|
207
|
+
tint: 16777215,
|
|
208
|
+
debug: false
|
|
209
|
+
});
|
|
210
|
+
/**
|
|
211
|
+
* The entity this view moves with. `0` means no parent.
|
|
212
|
+
*/
|
|
213
|
+
const Parent = /*#__PURE__*/ component("Parent", { entity: 0 });
|
|
214
|
+
/**
|
|
215
|
+
* A display object the game owns. Never pooled, never destroyed by `sync`.
|
|
216
|
+
*/
|
|
217
|
+
const Display = /*#__PURE__*/ component("Display", displayDefaults);
|
|
218
|
+
/**
|
|
219
|
+
* A filled rounded rectangle or a right-pointing triangle, drawn with Pixi `Graphics` and redrawn
|
|
220
|
+
* only when a field changed. `fillAlpha: 0` draws only the stroke, `dash` above 0 dashes it.
|
|
221
|
+
* `clip: true` masks the children of the entity to the shape.
|
|
222
|
+
*/
|
|
223
|
+
const Shape = /*#__PURE__*/ component("Shape", shapeDefaults);
|
|
224
|
+
/**
|
|
225
|
+
* Bundles the two components every visual needs, so a projection `view` reads as one line.
|
|
226
|
+
*
|
|
227
|
+
* @param options - Texture key, position, and the four values that have defaults.
|
|
228
|
+
* @returns The `Sprite` and the `Transform` value, in that order.
|
|
229
|
+
* @example
|
|
230
|
+
* ```ts
|
|
231
|
+
* sprite({ texture: "board.cell", at: { x: 540, y: 300 } });
|
|
232
|
+
* // [Sprite({ texture: "board.cell", tint: 0xffffff, alpha: 1, anchor: { x: 0.5, y: 0.5 },
|
|
233
|
+
* // width: 0, height: 0, fit: "fill" }),
|
|
234
|
+
* // Transform({ x: 540, y: 300, rotation: 0, scale: 1, pivot: { x: 0, y: 0 } })]
|
|
235
|
+
* ```
|
|
236
|
+
*/
|
|
237
|
+
function sprite(options) {
|
|
238
|
+
return [Sprite({
|
|
239
|
+
texture: options.texture,
|
|
240
|
+
tint: options.tint ?? Sprite.defaults.tint,
|
|
241
|
+
alpha: options.alpha ?? Sprite.defaults.alpha,
|
|
242
|
+
anchor: options.anchor ?? Sprite.defaults.anchor
|
|
243
|
+
}), Transform({
|
|
244
|
+
x: options.at.x,
|
|
245
|
+
y: options.at.y,
|
|
246
|
+
rotation: Transform.defaults.rotation,
|
|
247
|
+
scale: options.scale ?? Transform.defaults.scale
|
|
248
|
+
})];
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Binds `sprite`, `Sprite` and `NineSlice` to one game's asset keys. Type-only: the same objects.
|
|
252
|
+
*
|
|
253
|
+
* @returns The three helpers with `texture` narrowed to `Asset`.
|
|
254
|
+
* @example
|
|
255
|
+
* ```ts
|
|
256
|
+
* const { Sprite } = componentsFor<"board.cell" | "board.item-wood-1">();
|
|
257
|
+
* Sprite({ texture: "board.cell" }).value.texture; // "board.cell"
|
|
258
|
+
* ```
|
|
259
|
+
*/
|
|
260
|
+
function componentsFor() {
|
|
261
|
+
return {
|
|
262
|
+
sprite,
|
|
263
|
+
Sprite,
|
|
264
|
+
NineSlice
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Words on the screen: a string or a message, the style it is drawn in, and the string `text`
|
|
269
|
+
* resolved out of it. A game writes `content`, `style`, `bind` and `anchor`; `resolved` is
|
|
270
|
+
* engine-owned and `ui` and the tests read it.
|
|
271
|
+
*/
|
|
272
|
+
const Text = /*#__PURE__*/ component("Text", {
|
|
273
|
+
content: "",
|
|
274
|
+
style: "body",
|
|
275
|
+
bind: void 0,
|
|
276
|
+
anchor: {
|
|
277
|
+
x: .5,
|
|
278
|
+
y: .5
|
|
279
|
+
},
|
|
280
|
+
resolved: ""
|
|
281
|
+
}, { owned: ["resolved"] });
|
|
282
|
+
/**
|
|
283
|
+
* Bundles the two components a label needs, so a projection `view` reads as one line. The anchor
|
|
284
|
+
* is the point of the block that sits on the transform.
|
|
285
|
+
*
|
|
286
|
+
* @param options - The text, its style, where it sits, and which point of it sits there.
|
|
287
|
+
* @returns The `Text` and the `Transform` value, in that order.
|
|
288
|
+
* @example
|
|
289
|
+
* ```ts
|
|
290
|
+
* label({ text: "+5", style: "board.float", at: { x: 90, y: 180 } });
|
|
291
|
+
* // [Text({ content: "+5", style: "board.float", bind: undefined,
|
|
292
|
+
* // anchor: { x: 0.5, y: 0.5 }, resolved: "" }),
|
|
293
|
+
* // Transform({ x: 90, y: 180, rotation: 0, scale: 1 })]
|
|
294
|
+
* ```
|
|
295
|
+
*/
|
|
296
|
+
function label(options) {
|
|
297
|
+
return [Text({
|
|
298
|
+
content: options.text,
|
|
299
|
+
style: options.style,
|
|
300
|
+
anchor: options.anchor ?? Text.defaults.anchor
|
|
301
|
+
}), Transform({
|
|
302
|
+
x: options.at.x,
|
|
303
|
+
y: options.at.y
|
|
304
|
+
})];
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Fills the alpha of a shadow a game wrote. Its own function, so the stored shadow carries the
|
|
308
|
+
* four fields and nothing else.
|
|
309
|
+
*
|
|
310
|
+
* @param input - The shadow of a style, when it has one.
|
|
311
|
+
* @returns The shadow to draw with, or `undefined`.
|
|
312
|
+
* @example
|
|
313
|
+
* ```ts
|
|
314
|
+
* shadowOf({ color: 0, dx: 0, dy: 4 }); // { color: 0, dx: 0, dy: 4, alpha: 1 }
|
|
315
|
+
* ```
|
|
316
|
+
*/
|
|
317
|
+
function shadowOf(input) {
|
|
318
|
+
if (input === void 0) return void 0;
|
|
319
|
+
return {
|
|
320
|
+
color: input.color,
|
|
321
|
+
dx: input.dx,
|
|
322
|
+
dy: input.dy,
|
|
323
|
+
alpha: input.alpha ?? 1
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Fills the defaults of one style and refuses a wrap that is neither a width nor `"none"`.
|
|
328
|
+
*
|
|
329
|
+
* @param name - The style name, for the error.
|
|
330
|
+
* @param input - What the game wrote.
|
|
331
|
+
* @returns The style with every field filled.
|
|
332
|
+
* @throws {Error} When `wrap` is a number that is not greater than zero.
|
|
333
|
+
*/
|
|
334
|
+
function normalizeStyle(name, input) {
|
|
335
|
+
const wrap = input.wrap ?? "none";
|
|
336
|
+
if (wrap !== "none" && wrap <= 0) throw new Error(`[game] Text style "${name}" has wrap ${wrap}.\n Use a width in reference px or "none".`);
|
|
337
|
+
return {
|
|
338
|
+
font: input.font,
|
|
339
|
+
bold: input.bold,
|
|
340
|
+
italic: input.italic,
|
|
341
|
+
size: input.size,
|
|
342
|
+
fill: input.fill,
|
|
343
|
+
stroke: input.stroke ?? 0,
|
|
344
|
+
strokeWidth: input.strokeWidth ?? 0,
|
|
345
|
+
letterSpacing: input.letterSpacing ?? 0,
|
|
346
|
+
align: input.align ?? "left",
|
|
347
|
+
wrap,
|
|
348
|
+
digits: input.digits ?? false,
|
|
349
|
+
shadow: shadowOf(input.shadow)
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Declares the text styles of a feature. The result goes under the `textStyles` key of the
|
|
354
|
+
* feature description, and `text` reads it in `onStart`.
|
|
355
|
+
*
|
|
356
|
+
* @param map - Style name to what that style looks like.
|
|
357
|
+
* @returns The registration a feature carries.
|
|
358
|
+
* @throws {Error} When a style names a wrap that is not a width and not `"none"`.
|
|
359
|
+
* @example
|
|
360
|
+
* ```ts
|
|
361
|
+
* defineTextStyles({ "hud.digits": { font: "ui.font-digits", size: 40, fill: 0xffe082 } });
|
|
362
|
+
* // { kind: "textStyles", map: { "hud.digits": { font: "ui.font-digits", bold: undefined,
|
|
363
|
+
* // italic: undefined, size: 40, fill: 0xffe082, stroke: 0x000000, strokeWidth: 0,
|
|
364
|
+
* // letterSpacing: 0, align: "left", wrap: "none", digits: false, shadow: undefined } } }
|
|
365
|
+
* ```
|
|
366
|
+
*/
|
|
367
|
+
function defineTextStyles(map) {
|
|
368
|
+
const styles = {};
|
|
369
|
+
for (const [name, input] of Object.entries(map)) styles[name] = normalizeStyle(name, input);
|
|
370
|
+
return {
|
|
371
|
+
kind: "textStyles",
|
|
372
|
+
map: styles
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Tells whether a value is a plain record, so its fields can be read one by one.
|
|
377
|
+
*
|
|
378
|
+
* @param value - What a feature put in its description.
|
|
379
|
+
* @returns True for a plain object.
|
|
380
|
+
* @example
|
|
381
|
+
* ```ts
|
|
382
|
+
* isRecord({ font: "ui.font-body" }); // true
|
|
383
|
+
* ```
|
|
384
|
+
*/
|
|
385
|
+
function isRecord(value) {
|
|
386
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Reads one entry of a registered style map back into a full style. A feature carries the map as
|
|
390
|
+
* plain objects, so this is where it becomes a style again.
|
|
391
|
+
*
|
|
392
|
+
* @param name - The style name, for the error message.
|
|
393
|
+
* @param value - One entry of a feature's `textStyles` map.
|
|
394
|
+
* @returns The style, or `undefined` when the entry is not one.
|
|
395
|
+
* @example
|
|
396
|
+
* ```ts
|
|
397
|
+
* readStyle("hud.body", { font: "ui.font-body", size: 32, fill: 0 })?.align; // "left"
|
|
398
|
+
* ```
|
|
399
|
+
*/
|
|
400
|
+
function readStyle(name, value) {
|
|
401
|
+
if (!isRecord(value)) return void 0;
|
|
402
|
+
if (typeof value.font !== "string" || typeof value.size !== "number") return void 0;
|
|
403
|
+
if (typeof value.fill !== "number") return void 0;
|
|
404
|
+
const input = {
|
|
405
|
+
font: value.font,
|
|
406
|
+
size: value.size,
|
|
407
|
+
fill: value.fill
|
|
408
|
+
};
|
|
409
|
+
const optional = {
|
|
410
|
+
bold: value.bold,
|
|
411
|
+
italic: value.italic,
|
|
412
|
+
stroke: value.stroke,
|
|
413
|
+
strokeWidth: value.strokeWidth,
|
|
414
|
+
letterSpacing: value.letterSpacing,
|
|
415
|
+
align: value.align,
|
|
416
|
+
wrap: value.wrap,
|
|
417
|
+
digits: value.digits,
|
|
418
|
+
shadow: readShadow(value.shadow)
|
|
419
|
+
};
|
|
420
|
+
return normalizeStyle(name, {
|
|
421
|
+
...input,
|
|
422
|
+
...pruned(optional)
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Reads the shadow of a registered style back. One that lacks a numeric colour or offset is not
|
|
427
|
+
* a shadow and is left out, so the style still draws.
|
|
428
|
+
*
|
|
429
|
+
* @param value - The `shadow` field of a stored style.
|
|
430
|
+
* @returns The shadow as a game writes it, or `undefined`.
|
|
431
|
+
* @example
|
|
432
|
+
* ```ts
|
|
433
|
+
* readShadow({ color: 0, dx: 0, dy: 4, alpha: 1 }); // { color: 0, dx: 0, dy: 4, alpha: 1 }
|
|
434
|
+
* ```
|
|
435
|
+
*/
|
|
436
|
+
function readShadow(value) {
|
|
437
|
+
if (!isRecord(value)) return void 0;
|
|
438
|
+
const { color, dx, dy, alpha } = value;
|
|
439
|
+
if (typeof color !== "number" || typeof dx !== "number" || typeof dy !== "number") return;
|
|
440
|
+
return typeof alpha === "number" ? {
|
|
441
|
+
color,
|
|
442
|
+
dx,
|
|
443
|
+
dy,
|
|
444
|
+
alpha
|
|
445
|
+
} : {
|
|
446
|
+
color,
|
|
447
|
+
dx,
|
|
448
|
+
dy
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Drops the fields a style did not name, so the defaults win over an explicit `undefined`.
|
|
453
|
+
*
|
|
454
|
+
* @param fields - The optional fields of a style, as they were stored.
|
|
455
|
+
* @returns The fields that carry a value.
|
|
456
|
+
* @example
|
|
457
|
+
* ```ts
|
|
458
|
+
* pruned({ bold: undefined, wrap: 480 }); // { wrap: 480 }
|
|
459
|
+
* ```
|
|
460
|
+
*/
|
|
461
|
+
function pruned(fields) {
|
|
462
|
+
const kept = {};
|
|
463
|
+
for (const [name, value] of Object.entries(fields)) if (value !== void 0) kept[name] = value;
|
|
464
|
+
return kept;
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* The two styles every game has, built from the fonts of the plugin config. A feature may
|
|
468
|
+
* override either by name.
|
|
469
|
+
*
|
|
470
|
+
* @param fonts - The body and digits font keys of the config.
|
|
471
|
+
* @param fonts.body - Font key of the built-in `body` style.
|
|
472
|
+
* @param fonts.digits - Font key of the built-in `digits` style.
|
|
473
|
+
* @returns The built-in styles, `body` first.
|
|
474
|
+
* @example
|
|
475
|
+
* ```ts
|
|
476
|
+
* builtInStyles({ body: "ui.font-body", digits: "ui.font-digits" }).digits.digits; // true
|
|
477
|
+
* ```
|
|
478
|
+
*/
|
|
479
|
+
function builtInStyles(fonts) {
|
|
480
|
+
return {
|
|
481
|
+
body: normalizeStyle("body", {
|
|
482
|
+
font: fonts.body,
|
|
483
|
+
size: 32,
|
|
484
|
+
fill: 16777215
|
|
485
|
+
}),
|
|
486
|
+
digits: normalizeStyle("digits", {
|
|
487
|
+
font: fonts.digits,
|
|
488
|
+
size: 32,
|
|
489
|
+
fill: 16777215,
|
|
490
|
+
digits: true
|
|
491
|
+
})
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Binds `label` and `defineTextStyles` to one game's style names and font keys. Type-only: the
|
|
496
|
+
* same two functions.
|
|
497
|
+
*
|
|
498
|
+
* @returns The two helpers, with the style and font arguments narrowed.
|
|
499
|
+
* @example
|
|
500
|
+
* ```ts
|
|
501
|
+
* const { label } = textFor<"body", "ui.font-body">();
|
|
502
|
+
* label({ text: "+5", style: "body", at: { x: 90, y: 180 } })[1].value.x; // 90
|
|
503
|
+
* ```
|
|
504
|
+
*/
|
|
505
|
+
function textFor() {
|
|
506
|
+
return {
|
|
507
|
+
label,
|
|
508
|
+
defineTextStyles
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
//#endregion
|
|
512
|
+
//#region src/plugins/ui/visual.ts
|
|
513
|
+
/**
|
|
514
|
+
* @file ui plugin — the look of an element: the visual component it is drawn with, and where a
|
|
515
|
+
* `fit: "contain"` ancestor really draws it. Pure: no ctx, no state. Shared by `jsx` (the
|
|
516
|
+
* components of an entity, `lint`) and `layout` (the rest pose, the guide hole), which may not
|
|
517
|
+
* import each other's run-time code.
|
|
518
|
+
*/
|
|
519
|
+
/** The tag of the one child a scroll container holds: everything inside it moves as one. */
|
|
520
|
+
const CONTENT = "content";
|
|
521
|
+
/** The three ways a sized sprite fills its box, as the `fit` prop of `image` and `icon` names them. */
|
|
522
|
+
const FITS = [
|
|
523
|
+
"contain",
|
|
524
|
+
"cover",
|
|
525
|
+
"fill"
|
|
526
|
+
];
|
|
527
|
+
/**
|
|
528
|
+
* Tells whether a tag draws nothing of its own, so its `Shape` is invisible unless its style fills.
|
|
529
|
+
*
|
|
530
|
+
* @param type - The intrinsic tag.
|
|
531
|
+
* @returns True for the six container tags and the content of a scroll.
|
|
532
|
+
* @example
|
|
533
|
+
* ```ts
|
|
534
|
+
* isContainer("row"); // true
|
|
535
|
+
* ```
|
|
536
|
+
*/
|
|
537
|
+
function isContainer(type) {
|
|
538
|
+
return [
|
|
539
|
+
"screen",
|
|
540
|
+
"layer",
|
|
541
|
+
"row",
|
|
542
|
+
"column",
|
|
543
|
+
"stack",
|
|
544
|
+
"spacer",
|
|
545
|
+
CONTENT
|
|
546
|
+
].includes(type);
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Reads the asset key of an image or an icon out of its props.
|
|
550
|
+
*
|
|
551
|
+
* @param props - The props of the element.
|
|
552
|
+
* @returns The texture key, empty when the markup named none.
|
|
553
|
+
* @example
|
|
554
|
+
* ```ts
|
|
555
|
+
* textureOf({ name: "ui.gear" }); // "ui.gear"
|
|
556
|
+
* ```
|
|
557
|
+
*/
|
|
558
|
+
function textureOf(props) {
|
|
559
|
+
const key = props.texture ?? props.name;
|
|
560
|
+
return typeof key === "string" ? key : "";
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Reads the `fit` prop of an image or an icon.
|
|
564
|
+
*
|
|
565
|
+
* @param props - The props of the element.
|
|
566
|
+
* @returns The fit, `"contain"` when the markup named none.
|
|
567
|
+
* @example
|
|
568
|
+
* ```ts
|
|
569
|
+
* spriteFitOf({ fit: "cover" }); // "cover"
|
|
570
|
+
* ```
|
|
571
|
+
*/
|
|
572
|
+
function spriteFitOf(props) {
|
|
573
|
+
return FITS.find((fit) => fit === props.fit) ?? "contain";
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Reads the content of a text out of its props.
|
|
577
|
+
*
|
|
578
|
+
* @param props - The props of the element.
|
|
579
|
+
* @returns The string or message, empty when the markup named none.
|
|
580
|
+
*/
|
|
581
|
+
function contentOf(props) {
|
|
582
|
+
return props.content ?? "";
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* The visual component of an element: a sprite for an image or an icon, the text for a text, the
|
|
586
|
+
* nine-slice of its style (outlined when the style sets `debug`), or a rounded rectangle, a
|
|
587
|
+
* triangle with `shape: "triangle"`, its stroke dashed with `dash`. A clipping element (`scroll`,
|
|
588
|
+
* `overflow: "hidden"`) keeps the rectangle, which carries the clip. A style with a stroke and no fill draws
|
|
589
|
+
* only the stroke, a ring. A container with no fill and no stroke gets an invisible rectangle:
|
|
590
|
+
* the renderer hangs children under the display object of their parent, so every parent needs
|
|
591
|
+
* one.
|
|
592
|
+
*
|
|
593
|
+
* @param element - The element to draw, with its rect known.
|
|
594
|
+
* @returns The component values: one visual.
|
|
595
|
+
*/
|
|
596
|
+
function visualOf(element) {
|
|
597
|
+
const { style, type, node, rect } = element;
|
|
598
|
+
const alpha = style.alpha ?? 1;
|
|
599
|
+
const tint = style.tint ?? Sprite.defaults.tint;
|
|
600
|
+
if (type === "image" || type === "icon") return [Sprite({
|
|
601
|
+
texture: textureOf(node.props),
|
|
602
|
+
tint,
|
|
603
|
+
alpha,
|
|
604
|
+
anchor: {
|
|
605
|
+
x: 0,
|
|
606
|
+
y: 0
|
|
607
|
+
},
|
|
608
|
+
width: rect.w,
|
|
609
|
+
height: rect.h,
|
|
610
|
+
fit: spriteFitOf(node.props)
|
|
611
|
+
})];
|
|
612
|
+
if (type === "text") {
|
|
613
|
+
const styleKey = typeof node.props.style === "string" ? node.props.style : "body";
|
|
614
|
+
return [Text({
|
|
615
|
+
content: contentOf(node.props),
|
|
616
|
+
style: styleKey,
|
|
617
|
+
bind: node.props.bind,
|
|
618
|
+
anchor: {
|
|
619
|
+
x: 0,
|
|
620
|
+
y: 0
|
|
621
|
+
}
|
|
622
|
+
})];
|
|
623
|
+
}
|
|
624
|
+
const clip = type === "scroll" || style.overflow === "hidden";
|
|
625
|
+
if (style.nineSlice !== void 0 && !clip) return [NineSlice({
|
|
626
|
+
texture: style.nineSlice,
|
|
627
|
+
width: rect.w,
|
|
628
|
+
height: rect.h,
|
|
629
|
+
alpha,
|
|
630
|
+
tint,
|
|
631
|
+
debug: style.debug ?? false
|
|
632
|
+
})];
|
|
633
|
+
const filled = style.fill !== void 0 || style.stroke !== void 0;
|
|
634
|
+
const invisible = isContainer(type) && !filled;
|
|
635
|
+
return [Shape({
|
|
636
|
+
kind: style.shape ?? "rect",
|
|
637
|
+
w: rect.w,
|
|
638
|
+
h: rect.h,
|
|
639
|
+
fill: style.fill ?? Shape.defaults.fill,
|
|
640
|
+
fillAlpha: style.fill === void 0 ? 0 : Shape.defaults.fillAlpha,
|
|
641
|
+
alpha: invisible ? 0 : alpha,
|
|
642
|
+
radius: style.radius ?? 0,
|
|
643
|
+
stroke: style.stroke ?? Shape.defaults.stroke,
|
|
644
|
+
strokeWidth: style.strokeWidth ?? 0,
|
|
645
|
+
dash: style.dash ?? 0,
|
|
646
|
+
clip
|
|
647
|
+
})];
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* The element itself and every element above it, nearest first.
|
|
651
|
+
*
|
|
652
|
+
* @param element - Where the walk starts.
|
|
653
|
+
* @param lookup - How a parent entity becomes its element.
|
|
654
|
+
* @returns The chain up to the root element.
|
|
655
|
+
*/
|
|
656
|
+
function chainOf(element, lookup) {
|
|
657
|
+
const chain = [];
|
|
658
|
+
let current = element;
|
|
659
|
+
while (current !== void 0) {
|
|
660
|
+
chain.push(current);
|
|
661
|
+
current = current.parent === void 0 ? void 0 : lookup(current.parent);
|
|
662
|
+
}
|
|
663
|
+
return chain;
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* The scale an element is drawn at by the `fit: "contain"` of itself and its ancestors.
|
|
667
|
+
*
|
|
668
|
+
* @param element - The element to ask about.
|
|
669
|
+
* @param lookup - How a parent entity becomes its element.
|
|
670
|
+
* @returns The product of the fit scales, 1 when nothing on the way up fits.
|
|
671
|
+
*/
|
|
672
|
+
function fitScaleOf(element, lookup) {
|
|
673
|
+
return chainOf(element, lookup).reduce((scale, above) => scale * above.fit, 1);
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Scales a rect about the centre of every fitted link of a chain, in order: the way ui draws a
|
|
677
|
+
* `fit: "contain"`. A link with fit 1 leaves the rect alone.
|
|
678
|
+
*
|
|
679
|
+
* @param rect - The natural rect.
|
|
680
|
+
* @param chain - The element and its ancestors, nearest first, each with its rect and fit scale.
|
|
681
|
+
* @returns The drawn rect, a new object.
|
|
682
|
+
* @example
|
|
683
|
+
* ```ts
|
|
684
|
+
* scaleByFits({ x: 0, y: 0, w: 100, h: 100 }, [{ rect: { x: 0, y: 0, w: 100, h: 100 }, fit: 0.5 }]); // { x: 25, y: 25, w: 50, h: 50 }
|
|
685
|
+
* ```
|
|
686
|
+
*/
|
|
687
|
+
function scaleByFits(rect, chain) {
|
|
688
|
+
let drawn = { ...rect };
|
|
689
|
+
for (const above of chain) {
|
|
690
|
+
if (above.fit === 1) continue;
|
|
691
|
+
const centre = {
|
|
692
|
+
x: above.rect.x + above.rect.w / 2,
|
|
693
|
+
y: above.rect.y + above.rect.h / 2
|
|
694
|
+
};
|
|
695
|
+
drawn = {
|
|
696
|
+
x: centre.x + above.fit * (drawn.x - centre.x),
|
|
697
|
+
y: centre.y + above.fit * (drawn.y - centre.y),
|
|
698
|
+
w: drawn.w * above.fit,
|
|
699
|
+
h: drawn.h * above.fit
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
return drawn;
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Where an element is drawn at rest, in root coordinates: its natural rect, scaled about the
|
|
706
|
+
* centre of every fitted element on the way up, the element itself included. The visual
|
|
707
|
+
* transform styles (offset, scale) are left out: they are a state look, never a place.
|
|
708
|
+
*
|
|
709
|
+
* @param element - The element to place.
|
|
710
|
+
* @param lookup - How a parent entity becomes its element.
|
|
711
|
+
* @returns The drawn rect.
|
|
712
|
+
*/
|
|
713
|
+
function visualRectOf(element, lookup) {
|
|
714
|
+
return scaleByFits(element.rect, chainOf(element, lookup));
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Tells whether two transforms put a view in the same place.
|
|
718
|
+
*
|
|
719
|
+
* @param first - One pose.
|
|
720
|
+
* @param second - The other.
|
|
721
|
+
* @returns True when every field and the pivot are equal.
|
|
722
|
+
* @example
|
|
723
|
+
* ```ts
|
|
724
|
+
* samePose(
|
|
725
|
+
* { x: 1, y: 2, rotation: 0, scale: 1, pivot: { x: 0, y: 0 } },
|
|
726
|
+
* { x: 1, y: 2, rotation: 0, scale: 1, pivot: { x: 0, y: 0 } }
|
|
727
|
+
* ); // true
|
|
728
|
+
* ```
|
|
729
|
+
*/
|
|
730
|
+
function samePose(first, second) {
|
|
731
|
+
return first.x === second.x && first.y === second.y && first.rotation === second.rotation && first.scale === second.scale && first.pivot.x === second.pivot.x && first.pivot.y === second.pivot.y;
|
|
732
|
+
}
|
|
733
|
+
//#endregion
|
|
734
|
+
export { Order as C, resource as D, mut as E, system as O, Layer as S, component as T, Sprite as _, visualOf as a, sprite as b, builtInStyles as c, readStyle as d, textFor as f, Shape as g, Parent as h, scaleByFits as i, tag as k, defineTextStyles as l, NineSlice as m, fitScaleOf as n, visualRectOf as o, Display as p, samePose as r, Text as s, CONTENT as t, label as u, Transform as v, Tree as w, Exiting as x, componentsFor as y };
|