@moku-labs/game 0.4.2 → 0.4.4
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/dist/{commands-DYO22lzi.d.mts → commands-C83eQ5B9.d.mts} +8 -1
- package/dist/control.d.mts +1 -1
- package/dist/control.mjs +2 -2
- package/dist/{dev-B5p9_3fz.mjs → dev-wKLgOw7c.mjs} +8 -5
- package/dist/index.d.mts +9 -9
- package/dist/index.mjs +2 -1
- package/dist/inspect.d.mts +5 -2
- package/dist/inspect.mjs +1 -1
- package/dist/{run-C7O77UQR.mjs → run-D_R0oJ6g.mjs} +36 -1
- package/dist/{sources-CTGuY3Fe.mjs → sources-DVWJ8nfr.mjs} +26 -3
- package/dist/testing.d.mts +2 -2
- package/dist/testing.mjs +2 -2
- package/dist/{types-CVUHPvMB.d.mts → types-C-4f2jD_.d.mts} +669 -18
- package/dist/types-w646dMeA.d.mts +648 -0
- package/llms.txt +3 -3
- package/package.json +13 -11
- package/dist/types-BnWH-75b.d.mts +0 -1284
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
import { D as Point, E as FilterSlot, L as ComponentHandle, V as Entity, _ as PixiModule, g as PixiFilter, k as Api$1, m as Api$2, o as Events, t as Api$3, ut as Api, v as PixiSprite, vn as Require, y as PixiTexture, z as ComponentType } from "./types-Dy1xB68L.mjs";
|
|
2
|
+
import { g as AnimApi } from "./types-C-4f2jD_.mjs";
|
|
3
|
+
import { Log } from "@moku-labs/common/browser";
|
|
4
|
+
import { PluginCtx } from "@moku-labs/core";
|
|
5
|
+
|
|
6
|
+
//#region src/plugins/effects/filters/types.d.ts
|
|
7
|
+
/**
|
|
8
|
+
* One uniform of a custom filter, as declared: a number is an `f32` (`float` in GLSL), `{ color }`
|
|
9
|
+
* a `0xrrggbb` hex sent as a `vec3<f32>` (`vec3`), a tuple of 2 to 4 numbers a `vecN<f32>`
|
|
10
|
+
* (`vecN`).
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* const uniforms: Record<string, UniformDeclaration> = { amount: 0.5, color: { color: 0xffd700 }, offset: [2, 2] };
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
type UniformDeclaration = number | {
|
|
18
|
+
readonly color: number;
|
|
19
|
+
} | readonly [number, number] | readonly [number, number, number] | readonly [number, number, number, number];
|
|
20
|
+
/**
|
|
21
|
+
* The component field a uniform declaration becomes: a number for a number and for a colour, a
|
|
22
|
+
* list of numbers for a tuple.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* type Amount = UniformValue<0.5>; // number
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
type UniformValue<Declaration> = Declaration extends number ? number : Declaration extends {
|
|
30
|
+
readonly color: number;
|
|
31
|
+
} ? number : readonly number[];
|
|
32
|
+
/**
|
|
33
|
+
* The value of a filter component made by `defineFilter`: one field per uniform, then `enabled`
|
|
34
|
+
* and `order`.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* type TintValue = FilterValueOf<{ amount: number; color: { color: number } }>;
|
|
39
|
+
* // { amount: number; color: number; enabled: boolean; order: number }
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
type FilterValueOf<Uniforms extends Record<string, UniformDeclaration>> = { -readonly [Name in keyof Uniforms]: UniformValue<Uniforms[Name]> } & {
|
|
43
|
+
enabled: boolean;
|
|
44
|
+
order: number;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* What `defineFilter` takes besides the id. Both bodies are required, so the filter draws on
|
|
48
|
+
* WebGPU and on the WebGL fallback: `wgsl` is the WGSL fragment body only, the engine prepends the
|
|
49
|
+
* vertex stage, the input bindings and the `FilterUniforms` struct; `glsl` is its GLSL ES 3.0 twin
|
|
50
|
+
* over the same uniform names, the engine prepends the version, the inputs, `finalColor` and one
|
|
51
|
+
* `uniform` per declared uniform.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* const spec: FilterSpec<{ amount: number }> = {
|
|
56
|
+
* wgsl: "@fragment fn mainFragment(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> { return textureSample(uTexture, uSampler, uv) * fu.amount; }",
|
|
57
|
+
* glsl: "void main() { finalColor = texture(uTexture, vTextureCoord) * amount; }",
|
|
58
|
+
* uniforms: { amount: 1 }
|
|
59
|
+
* };
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
type FilterSpec<Uniforms extends Record<string, UniformDeclaration>> = {
|
|
63
|
+
/** The WGSL fragment body: `fn mainFragment` and its helpers, without the engine's header. */readonly wgsl: string;
|
|
64
|
+
/**
|
|
65
|
+
* The GLSL ES 3.0 fragment body: `void main()`, which sets `finalColor`, and its helpers, without
|
|
66
|
+
* the engine's header. It reads each uniform under its bare name: `amount`, not `fu.amount`.
|
|
67
|
+
*/
|
|
68
|
+
readonly glsl: string; /** The uniforms in struct order. Default `{}`. */
|
|
69
|
+
readonly uniforms?: Uniforms; /** Render passes one apply of the filter costs, an integer of at least 1. Default `1`. */
|
|
70
|
+
readonly passes?: number; /** Pixels around the view's bounds, or the name of a number uniform whose live value they are. Default `0`. */
|
|
71
|
+
readonly padding?: number | string;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* How one uniform travels to the GPU: its name, its WGSL type and the form it was declared in.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```ts
|
|
78
|
+
* const spec: UniformSpec = { name: "color", type: "vec3<f32>", form: "color", size: 3 };
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
type UniformSpec = {
|
|
82
|
+
readonly name: string;
|
|
83
|
+
readonly type: "f32" | "vec2<f32>" | "vec3<f32>" | "vec4<f32>";
|
|
84
|
+
readonly form: "number" | "color" | "vector";
|
|
85
|
+
readonly size: number;
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* The filter a component made by `defineFilter` carries: the assembled WGSL in `source`, the
|
|
89
|
+
* assembled GLSL in `glsl`, the uniforms in struct order, the passes one apply costs and the
|
|
90
|
+
* padding.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```ts
|
|
94
|
+
* Tint.filter.source.includes("fn mainFragment"); // true: the WGSL, drawn on WebGPU
|
|
95
|
+
* Tint.filter.glsl.startsWith("#version 300 es"); // true: the GLSL, drawn on WebGL
|
|
96
|
+
* Tint.filter.uniforms.map(uniform => uniform.name); // ["amount", "color"]
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
type FilterDefinition = {
|
|
100
|
+
readonly id: string; /** The assembled WGSL: the engine's header, the uniform struct, the body. */
|
|
101
|
+
readonly source: string; /** The assembled GLSL ES 3.0: the engine's header, one `uniform` per uniform, the body. */
|
|
102
|
+
readonly glsl: string;
|
|
103
|
+
readonly uniforms: readonly UniformSpec[];
|
|
104
|
+
readonly passes: number;
|
|
105
|
+
readonly padding: number | string;
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* A filter component type: the `component()` of its id, with the filter definition attached. Its
|
|
109
|
+
* numeric fields are tweenable like any component.
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* ```ts
|
|
113
|
+
* const Tint: FilterComponent<{ amount: number; enabled: boolean; order: number }> =
|
|
114
|
+
* defineFilter("fx.tint", { wgsl: tintWgsl, glsl: tintGlsl, uniforms: { amount: 0 } });
|
|
115
|
+
* Tint.componentName; // "fx.tint"
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
type FilterComponent<Value extends object> = ComponentType<Value> & {
|
|
119
|
+
readonly filter: FilterDefinition;
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* One field of a stored filter value: a number, a switch, an asset key or a vector.
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```ts
|
|
126
|
+
* const field: FilterField = 0.5;
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
type FilterField = number | boolean | string | readonly number[];
|
|
130
|
+
/**
|
|
131
|
+
* The stored value of any filter component, with the kind erased: the filter sync reads every
|
|
132
|
+
* kind through it.
|
|
133
|
+
*/
|
|
134
|
+
type FilterFields = {
|
|
135
|
+
readonly enabled: boolean;
|
|
136
|
+
readonly order: number;
|
|
137
|
+
readonly [field: string]: FilterField;
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* `Glow`: a soft halo of `color` around the opaque pixels of the view.
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```ts
|
|
144
|
+
* const value: GlowValue = { strength: 2, distance: 10, color: 0xffffff, alpha: 1, enabled: true, order: 0 };
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
type GlowValue = {
|
|
148
|
+
strength: number;
|
|
149
|
+
distance: number;
|
|
150
|
+
color: number;
|
|
151
|
+
alpha: number;
|
|
152
|
+
enabled: boolean;
|
|
153
|
+
order: number;
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* `Outline`: a ring of `thickness` pixels in `color` around the opaque pixels of the view.
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* ```ts
|
|
160
|
+
* const value: OutlineValue = { thickness: 2, color: 0x000000, alpha: 1, enabled: true, order: 0 };
|
|
161
|
+
* ```
|
|
162
|
+
*/
|
|
163
|
+
type OutlineValue = {
|
|
164
|
+
thickness: number;
|
|
165
|
+
color: number;
|
|
166
|
+
alpha: number;
|
|
167
|
+
enabled: boolean;
|
|
168
|
+
order: number;
|
|
169
|
+
};
|
|
170
|
+
/**
|
|
171
|
+
* `Blur`: Pixi's blur. `quality: 0` means `config.blur.quality`; `resolution: 0` means
|
|
172
|
+
* `config.blur.phoneResolution` on a phone, otherwise 1.
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* ```ts
|
|
176
|
+
* const value: BlurValue = { strength: 8, quality: 0, resolution: 0, repeatEdgePixels: true, enabled: true, order: 0 };
|
|
177
|
+
* ```
|
|
178
|
+
*/
|
|
179
|
+
type BlurValue = {
|
|
180
|
+
strength: number;
|
|
181
|
+
quality: number;
|
|
182
|
+
resolution: number;
|
|
183
|
+
repeatEdgePixels: boolean;
|
|
184
|
+
enabled: boolean;
|
|
185
|
+
order: number;
|
|
186
|
+
};
|
|
187
|
+
/**
|
|
188
|
+
* `ColorMatrix`: brightness, saturation, contrast, hue and grayscale composed in that order.
|
|
189
|
+
* Every default is the identity.
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* ```ts
|
|
193
|
+
* const value: ColorMatrixValue = { brightness: 1, saturation: 0, contrast: 0, hue: 0, grayscale: 1, enabled: true, order: 0 };
|
|
194
|
+
* ```
|
|
195
|
+
*/
|
|
196
|
+
type ColorMatrixValue = {
|
|
197
|
+
brightness: number;
|
|
198
|
+
saturation: number;
|
|
199
|
+
contrast: number;
|
|
200
|
+
hue: number;
|
|
201
|
+
grayscale: number;
|
|
202
|
+
enabled: boolean;
|
|
203
|
+
order: number;
|
|
204
|
+
};
|
|
205
|
+
/**
|
|
206
|
+
* `Noise`: Pixi's noise with its seed always given, so two runs draw the same grain.
|
|
207
|
+
*
|
|
208
|
+
* @example
|
|
209
|
+
* ```ts
|
|
210
|
+
* const value: NoiseValue = { amount: 0.5, seed: 7, enabled: true, order: 0 };
|
|
211
|
+
* ```
|
|
212
|
+
*/
|
|
213
|
+
type NoiseValue = {
|
|
214
|
+
amount: number;
|
|
215
|
+
seed: number;
|
|
216
|
+
enabled: boolean;
|
|
217
|
+
order: number;
|
|
218
|
+
};
|
|
219
|
+
/**
|
|
220
|
+
* `Displacement`: Pixi's displacement over the texture of an asset key.
|
|
221
|
+
*
|
|
222
|
+
* @example
|
|
223
|
+
* ```ts
|
|
224
|
+
* const value: DisplacementValue = { map: "fx.ripple", scaleX: 20, scaleY: 20, enabled: true, order: 0 };
|
|
225
|
+
* ```
|
|
226
|
+
*/
|
|
227
|
+
type DisplacementValue = {
|
|
228
|
+
map: string;
|
|
229
|
+
scaleX: number;
|
|
230
|
+
scaleY: number;
|
|
231
|
+
enabled: boolean;
|
|
232
|
+
order: number;
|
|
233
|
+
};
|
|
234
|
+
/**
|
|
235
|
+
* `Alpha`: Pixi's alpha over the whole subtree, as one layer.
|
|
236
|
+
*
|
|
237
|
+
* @example
|
|
238
|
+
* ```ts
|
|
239
|
+
* const value: AlphaValue = { alpha: 0.5, enabled: true, order: 0 };
|
|
240
|
+
* ```
|
|
241
|
+
*/
|
|
242
|
+
type AlphaValue = {
|
|
243
|
+
alpha: number;
|
|
244
|
+
enabled: boolean;
|
|
245
|
+
order: number;
|
|
246
|
+
};
|
|
247
|
+
/**
|
|
248
|
+
* The five built-in kinds drawn by a Pixi core filter.
|
|
249
|
+
*
|
|
250
|
+
* @example
|
|
251
|
+
* ```ts
|
|
252
|
+
* const kind: CoreKind = "blur";
|
|
253
|
+
* ```
|
|
254
|
+
*/
|
|
255
|
+
type CoreKind = "blur" | "colorMatrix" | "noise" | "displacement" | "alpha";
|
|
256
|
+
/**
|
|
257
|
+
* Where a filter kind comes from: our or a game's shaders (`"wgsl"`, with the GLSL twin beside
|
|
258
|
+
* it), or a Pixi core filter.
|
|
259
|
+
*/
|
|
260
|
+
type KindSource = {
|
|
261
|
+
readonly source: "wgsl";
|
|
262
|
+
readonly definition: FilterDefinition;
|
|
263
|
+
} | {
|
|
264
|
+
readonly source: "core";
|
|
265
|
+
readonly core: CoreKind;
|
|
266
|
+
};
|
|
267
|
+
/**
|
|
268
|
+
* A filter kind before it is registered: its id, its component and its source.
|
|
269
|
+
*/
|
|
270
|
+
type KindEntry = KindSource & {
|
|
271
|
+
readonly id: string;
|
|
272
|
+
readonly component: ComponentHandle<FilterFields>;
|
|
273
|
+
};
|
|
274
|
+
/**
|
|
275
|
+
* One registered filter kind. `index` is the registration order: the built-ins first, then the
|
|
276
|
+
* filters of every feature in feature order.
|
|
277
|
+
*/
|
|
278
|
+
type FilterKind = KindEntry & {
|
|
279
|
+
readonly index: number;
|
|
280
|
+
};
|
|
281
|
+
/**
|
|
282
|
+
* The values of the uniform group of one of our filter instances: a number per `f32`, a vector
|
|
283
|
+
* per colour and tuple. Written every frame.
|
|
284
|
+
*/
|
|
285
|
+
type UniformValues = Record<string, number | Float32Array>;
|
|
286
|
+
/**
|
|
287
|
+
* The uniform group of one of our filter instances, as the instance writes it.
|
|
288
|
+
*/
|
|
289
|
+
type FilterUniforms = {
|
|
290
|
+
readonly uniforms: UniformValues;
|
|
291
|
+
};
|
|
292
|
+
/**
|
|
293
|
+
* The Pixi object of one kind on one view, with the passes one apply of it costs now: what the
|
|
294
|
+
* builders of `instance.ts` make before the instance gets its sort keys.
|
|
295
|
+
*/
|
|
296
|
+
type FilterBody = {
|
|
297
|
+
readonly kind: "wgsl";
|
|
298
|
+
readonly filter: PixiFilter;
|
|
299
|
+
readonly group: FilterUniforms | undefined;
|
|
300
|
+
readonly definition: FilterDefinition;
|
|
301
|
+
passes: number;
|
|
302
|
+
} | {
|
|
303
|
+
readonly kind: "blur";
|
|
304
|
+
readonly filter: InstanceType<PixiModule["BlurFilter"]>;
|
|
305
|
+
passes: number;
|
|
306
|
+
} | {
|
|
307
|
+
readonly kind: "colorMatrix";
|
|
308
|
+
readonly filter: InstanceType<PixiModule["ColorMatrixFilter"]>;
|
|
309
|
+
passes: number;
|
|
310
|
+
} | {
|
|
311
|
+
readonly kind: "noise";
|
|
312
|
+
readonly filter: InstanceType<PixiModule["NoiseFilter"]>;
|
|
313
|
+
passes: number;
|
|
314
|
+
} | {
|
|
315
|
+
readonly kind: "displacement";
|
|
316
|
+
readonly filter: InstanceType<PixiModule["DisplacementFilter"]>;
|
|
317
|
+
readonly sprite: PixiSprite; /** The asset key the map sprite shows now. */
|
|
318
|
+
map: string;
|
|
319
|
+
passes: number;
|
|
320
|
+
} | {
|
|
321
|
+
readonly kind: "alpha";
|
|
322
|
+
readonly filter: InstanceType<PixiModule["AlphaFilter"]>;
|
|
323
|
+
passes: number;
|
|
324
|
+
};
|
|
325
|
+
/**
|
|
326
|
+
* The instance of one kind on one view: its Pixi object, and what it sorts by on the view, the
|
|
327
|
+
* `order` of its component (written every frame) and the `index` of its kind.
|
|
328
|
+
*/
|
|
329
|
+
type FilterInstance = FilterBody & {
|
|
330
|
+
order: number;
|
|
331
|
+
readonly index: number;
|
|
332
|
+
};
|
|
333
|
+
/**
|
|
334
|
+
* What the filter sync keeps for one entity: the kinds it carries, one instance per kind, the
|
|
335
|
+
* instances waiting to be destroyed after the next assignment, the slots last handed to
|
|
336
|
+
* `renderer.sync.filters.set`, the core kinds whose component changed this frame, and the
|
|
337
|
+
* instances of this frame in their sorted order (one array, reused every frame).
|
|
338
|
+
*/
|
|
339
|
+
type FilteredView = {
|
|
340
|
+
readonly kinds: Set<string>;
|
|
341
|
+
readonly instances: Map<string, FilterInstance>;
|
|
342
|
+
readonly retired: FilterInstance[];
|
|
343
|
+
assigned: readonly FilterSlot[];
|
|
344
|
+
readonly changed: Set<string>;
|
|
345
|
+
readonly placed: FilterInstance[];
|
|
346
|
+
};
|
|
347
|
+
//#endregion
|
|
348
|
+
//#region src/plugins/effects/particles/types.d.ts
|
|
349
|
+
/**
|
|
350
|
+
* An inclusive `[min, max]` range; every particle draws its own value from it.
|
|
351
|
+
*
|
|
352
|
+
* @example
|
|
353
|
+
* ```ts
|
|
354
|
+
* const lifeMs: Range = [500, 900];
|
|
355
|
+
* ```
|
|
356
|
+
*/
|
|
357
|
+
type Range = readonly [min: number, max: number];
|
|
358
|
+
/**
|
|
359
|
+
* A value over the life of a particle, linear from its birth (`from`) to its death (`to`).
|
|
360
|
+
*
|
|
361
|
+
* @example
|
|
362
|
+
* ```ts
|
|
363
|
+
* const shrink: Curve = { from: 1, to: 0.2 };
|
|
364
|
+
* ```
|
|
365
|
+
*/
|
|
366
|
+
type Curve = {
|
|
367
|
+
readonly from: number;
|
|
368
|
+
readonly to: number;
|
|
369
|
+
};
|
|
370
|
+
/**
|
|
371
|
+
* Where a particle is born, around the origin of its emitter, in reference units. `circle` is
|
|
372
|
+
* uniform over the disc, `ring` uniform between `radius − width / 2` and `radius + width / 2`,
|
|
373
|
+
* `rect` uniform over the centred box.
|
|
374
|
+
*
|
|
375
|
+
* @example
|
|
376
|
+
* ```ts
|
|
377
|
+
* const burstRing: Shape = { kind: "ring", radius: 48, width: 16 };
|
|
378
|
+
* ```
|
|
379
|
+
*/
|
|
380
|
+
type Shape = {
|
|
381
|
+
readonly kind: "point";
|
|
382
|
+
} | {
|
|
383
|
+
readonly kind: "circle";
|
|
384
|
+
readonly radius: number;
|
|
385
|
+
} | {
|
|
386
|
+
readonly kind: "rect";
|
|
387
|
+
readonly w: number;
|
|
388
|
+
readonly h: number;
|
|
389
|
+
} | {
|
|
390
|
+
readonly kind: "ring";
|
|
391
|
+
readonly radius: number;
|
|
392
|
+
readonly width: number;
|
|
393
|
+
};
|
|
394
|
+
/**
|
|
395
|
+
* A particle effect as data, what `defineEmitter` takes. Every field is optional except
|
|
396
|
+
* `textures` and exactly one of `burst` and `rate`. Speeds are reference units per second, angles
|
|
397
|
+
* degrees with 0 to the right and 90 down, spins radians per second.
|
|
398
|
+
*
|
|
399
|
+
* @example
|
|
400
|
+
* ```ts
|
|
401
|
+
* const stars: EmitterConfig = {
|
|
402
|
+
* textures: ["fx.star", "fx.sparkle"], burst: 40, lifeMs: [500, 900], speed: [300, 700],
|
|
403
|
+
* gravity: 900, drag: 0.2, shape: { kind: "circle", radius: 24 }, alpha: { from: 1, to: 0 }
|
|
404
|
+
* };
|
|
405
|
+
* ```
|
|
406
|
+
*/
|
|
407
|
+
type EmitterConfig = {
|
|
408
|
+
/** 1 to 16 asset keys of one atlas page; a particle picks one at random. */readonly textures: readonly string[]; /** Particles emitted once, when the component appears or its `effect` changes. */
|
|
409
|
+
readonly burst?: number; /** Particles per second while `active` is true and the entity lives. */
|
|
410
|
+
readonly rate?: number; /** Life of a particle in milliseconds. Default `[600, 1000]`. */
|
|
411
|
+
readonly lifeMs?: Range; /** Start speed in reference units per second. Default `[0, 0]`. */
|
|
412
|
+
readonly speed?: Range; /** Start direction in degrees, 0 right, 90 down. Default `[0, 360]`. */
|
|
413
|
+
readonly angle?: Range; /** Downward acceleration in reference units per second². Default `0`. */
|
|
414
|
+
readonly gravity?: number; /** Velocity lost per second, 0..1: `v *= (1 − drag) ^ seconds`. Default `0`. */
|
|
415
|
+
readonly drag?: number; /** Spin in radians per second. Default `[0, 0]`. */
|
|
416
|
+
readonly spin?: Range; /** Where particles are born around the origin. Default `{ kind: "point" }`. */
|
|
417
|
+
readonly shape?: Shape; /** Scale over life. Default `{ from: 1, to: 1 }`. */
|
|
418
|
+
readonly scale?: Curve; /** Alpha over life, 0..1. Default `{ from: 1, to: 1 }`. */
|
|
419
|
+
readonly alpha?: Curve; /** Tint over life, `0xrrggbb`, linear per channel. Default white to white. */
|
|
420
|
+
readonly tint?: Curve; /** `"world"`: particles outlive the entity. `"local"`: they move and die with it. Default `"world"`. */
|
|
421
|
+
readonly space?: "world" | "local"; /** Milliseconds simulated before the first frame, for streams, 0..5000. Default `0`. */
|
|
422
|
+
readonly prewarmMs?: number; /** Hard ceiling of live particles of one instance, 1..5000. Default `256`. */
|
|
423
|
+
readonly maxParticles?: number; /** Blend mode of the particle container. Default `"normal"`. */
|
|
424
|
+
readonly blend?: "normal" | "add";
|
|
425
|
+
};
|
|
426
|
+
/**
|
|
427
|
+
* An `EmitterConfig` with every default filled in. `burst` and `rate` stay as given: exactly one
|
|
428
|
+
* of them is present.
|
|
429
|
+
*
|
|
430
|
+
* @example
|
|
431
|
+
* ```ts
|
|
432
|
+
* defineEmitter("fx.steam", { textures: ["fx.puff"], rate: 12 }).config.lifeMs; // [600, 1000]
|
|
433
|
+
* ```
|
|
434
|
+
*/
|
|
435
|
+
type ResolvedEmitterConfig = Required<Omit<EmitterConfig, "burst" | "rate">> & Pick<EmitterConfig, "burst" | "rate">;
|
|
436
|
+
/**
|
|
437
|
+
* A particle effect as `defineEmitter` returns it: frozen, registered through the `emitters` key
|
|
438
|
+
* of a feature and started by an `Emitter` component that names its id.
|
|
439
|
+
*
|
|
440
|
+
* @example
|
|
441
|
+
* ```ts
|
|
442
|
+
* const steam: EmitterDefinition = defineEmitter("fx.steam", { textures: ["fx.puff"], rate: 12 });
|
|
443
|
+
* steam.config.rate; // 12
|
|
444
|
+
* ```
|
|
445
|
+
*/
|
|
446
|
+
type EmitterDefinition = {
|
|
447
|
+
readonly id: string;
|
|
448
|
+
readonly config: ResolvedEmitterConfig;
|
|
449
|
+
};
|
|
450
|
+
/**
|
|
451
|
+
* The value of the `Emitter` component: which effect runs on the entity and whether a stream
|
|
452
|
+
* emits. `""` draws nothing.
|
|
453
|
+
*
|
|
454
|
+
* @example
|
|
455
|
+
* ```ts
|
|
456
|
+
* const value: EmitterValue = { effect: "fx.steam", active: true };
|
|
457
|
+
* ```
|
|
458
|
+
*/
|
|
459
|
+
type EmitterValue = {
|
|
460
|
+
effect: string;
|
|
461
|
+
active: boolean;
|
|
462
|
+
};
|
|
463
|
+
/**
|
|
464
|
+
* The cosmetic random source of one instance: xorshift32 over its own seed, never `Math.random`
|
|
465
|
+
* and never the model's rng.
|
|
466
|
+
*/
|
|
467
|
+
type Rng = {
|
|
468
|
+
/** The next number in `[0, 1)`. */next(): number; /** An integer in `[0, count)`. */
|
|
469
|
+
int(count: number): number; /** A number in `[min, max]`. */
|
|
470
|
+
between(min: number, max: number): number;
|
|
471
|
+
};
|
|
472
|
+
/**
|
|
473
|
+
* A Pixi particle container: what one emitter instance draws in.
|
|
474
|
+
*/
|
|
475
|
+
type PixiParticleContainer = InstanceType<PixiModule["ParticleContainer"]>;
|
|
476
|
+
/**
|
|
477
|
+
* One particle of a container, as the container keeps it.
|
|
478
|
+
*/
|
|
479
|
+
type PixiParticle = PixiParticleContainer["particleChildren"][number];
|
|
480
|
+
/**
|
|
481
|
+
* What one effect id bakes to on first use: the 64-entry curve tables, the reach rectangle's half
|
|
482
|
+
* side and the resolved textures. Dropped when a bundle with one of its textures leaves.
|
|
483
|
+
*/
|
|
484
|
+
type BakedEmitter = {
|
|
485
|
+
readonly id: string;
|
|
486
|
+
readonly config: ResolvedEmitterConfig; /** Scale per 1/64 of the life. */
|
|
487
|
+
readonly scaleTable: Float32Array; /** Alpha per 1/64 of the life, 0..1. */
|
|
488
|
+
readonly alphaTable: Float32Array; /** Tint per 1/64 of the life, packed as Pixi's `0xbbggrr`. */
|
|
489
|
+
readonly tintTable: Uint32Array; /** Half side of the box no particle leaves, in reference units. */
|
|
490
|
+
readonly reach: number;
|
|
491
|
+
readonly textures: readonly PixiTexture[]; /** The texture source of the first texture: every texture of the effect samples it. */
|
|
492
|
+
readonly source: PixiTexture["source"] | undefined; /** The largest side of the textures, in pixels. */
|
|
493
|
+
readonly maxSide: number;
|
|
494
|
+
};
|
|
495
|
+
/**
|
|
496
|
+
* The per-particle numbers a Pixi particle has no field for, one entry per live particle, in the
|
|
497
|
+
* order of `particleChildren`.
|
|
498
|
+
*/
|
|
499
|
+
type BornFields = {
|
|
500
|
+
readonly age: Float32Array;
|
|
501
|
+
readonly life: Float32Array;
|
|
502
|
+
readonly vx: Float32Array;
|
|
503
|
+
readonly vy: Float32Array;
|
|
504
|
+
readonly spin: Float32Array;
|
|
505
|
+
readonly variant: Float32Array;
|
|
506
|
+
};
|
|
507
|
+
/**
|
|
508
|
+
* One running effect: its container, the plugin-owned entity that carries it, the particle pool
|
|
509
|
+
* and the numbers the step needs.
|
|
510
|
+
*/
|
|
511
|
+
type EmitterInstance = {
|
|
512
|
+
readonly id: string;
|
|
513
|
+
readonly baked: BakedEmitter; /** The entity whose `Emitter` started it. */
|
|
514
|
+
readonly host: Entity; /** The plugin-owned entity whose `Display` is the container. */
|
|
515
|
+
readonly entity: Entity;
|
|
516
|
+
readonly container: PixiParticleContainer; /** Retired particles, reused before a new one is made. */
|
|
517
|
+
readonly pool: PixiParticle[];
|
|
518
|
+
readonly born: BornFields; /** Fraction of a particle owed to the next frame of a stream. */
|
|
519
|
+
carry: number;
|
|
520
|
+
readonly rng: Rng; /** The point the container was placed at, in root space (world space only). */
|
|
521
|
+
readonly origin: Readonly<Point>;
|
|
522
|
+
readonly space: "world" | "local"; /** Pixi's particle class, from the module the renderer loaded. */
|
|
523
|
+
readonly particleClass: PixiModule["Particle"];
|
|
524
|
+
};
|
|
525
|
+
declare namespace types_d_exports {
|
|
526
|
+
export { AlphaValue, BlurValue, BundleUnloaded, ColorMatrixValue, Config, CoreKind, Curve, Deps, DisplacementValue, EffectsApi, EffectsCtx, EffectsStats, EmitterConfig, EmitterDefinition, EmitterValue, FilterComponent, FilterDefinition, FilterSpec, FilterValueOf, GlowValue, KernelSlice, NoiseValue, OutlineValue, Range, ResolvedEmitterConfig, Shape, State, UniformDeclaration, UniformSpec, UniformValue };
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* effects plugin config. `maxParticles` here is the global warning line; the `maxParticles` of an
|
|
530
|
+
* emitter config caps one instance.
|
|
531
|
+
*
|
|
532
|
+
* @example
|
|
533
|
+
* ```ts
|
|
534
|
+
* createApp({
|
|
535
|
+
* plugins: [...screen, effectsPlugin],
|
|
536
|
+
* pluginConfigs: { effects: { maxPasses: 16, phone: false } }
|
|
537
|
+
* });
|
|
538
|
+
* ```
|
|
539
|
+
*/
|
|
540
|
+
type Config = {
|
|
541
|
+
/** Live particles over all instances above which one dev warning fires per crossing. */maxParticles: number; /** Render passes per frame above which one dev warning fires per crossing. */
|
|
542
|
+
maxPasses: number; /** Whether this device is a phone. `"auto"`: a coarse pointer and a short side of at most 820 CSS px, read once in `onStart`. */
|
|
543
|
+
phone: boolean | "auto";
|
|
544
|
+
/**
|
|
545
|
+
* What a `Blur` with `quality: 0` and `resolution: 0` resolves to. Shallow merge: a game that
|
|
546
|
+
* sets `blur` gives both fields.
|
|
547
|
+
*/
|
|
548
|
+
blur: {
|
|
549
|
+
quality: number;
|
|
550
|
+
phoneResolution: number;
|
|
551
|
+
};
|
|
552
|
+
};
|
|
553
|
+
/**
|
|
554
|
+
* effects plugin state: the particle half, the filter half, and what both share.
|
|
555
|
+
*/
|
|
556
|
+
type State = {
|
|
557
|
+
/** Resolved in `onStart`. */phone: boolean; /** The `emitters` of every feature, read in `onStart`. */
|
|
558
|
+
emitters: Map<string, EmitterDefinition>; /** Per effect id, built on first use. */
|
|
559
|
+
baked: Map<string, BakedEmitter>; /** Host entity to its running instance. */
|
|
560
|
+
instances: Map<Entity, EmitterInstance>; /** World-space instances whose host left, stepped until empty. */
|
|
561
|
+
orphans: Set<EmitterInstance>; /** Ordinal of the next instance: the cosmetic seed. */
|
|
562
|
+
seedCounter: number; /** Live particles after the last step. */
|
|
563
|
+
particles: number; /** Filter kinds whose shader failed the dev check on the running backend. */
|
|
564
|
+
broken: Set<string>; /** The filter kinds, built-ins first, then the filters of every feature. */
|
|
565
|
+
kinds: Map<string, FilterKind>; /** The dev shader check per kind; `broken` holds the failed ones. */
|
|
566
|
+
checks: Map<string, "pending" | "ok">; /** Entity to the filters the sync keeps for it. */
|
|
567
|
+
views: Map<Entity, FilteredView>; /** Keys of the one-shot warnings: `"emitter:fx.x"`, `"texture:board.cell"`, `"atlas:fx.x"`. */
|
|
568
|
+
warned: Set<string>; /** Whether each budget warning is in force, so a crossing warns once. */
|
|
569
|
+
over: {
|
|
570
|
+
particles: boolean;
|
|
571
|
+
passes: boolean;
|
|
572
|
+
fullScreen: boolean;
|
|
573
|
+
}; /** The teardown closure, the two systems and the world hooks. */
|
|
574
|
+
removers: Array<() => void>;
|
|
575
|
+
};
|
|
576
|
+
/**
|
|
577
|
+
* What `app.effects.stats()` answers: live particles, running emitter instances, filter instances
|
|
578
|
+
* on views, and the render passes of the frame as the renderer counts them.
|
|
579
|
+
*
|
|
580
|
+
* @example
|
|
581
|
+
* ```ts
|
|
582
|
+
* const stats: EffectsStats = { particles: 120, emitters: 3, filters: 25, renderPasses: 49 };
|
|
583
|
+
* ```
|
|
584
|
+
*/
|
|
585
|
+
type EffectsStats = {
|
|
586
|
+
/** Live particles over every instance and orphan after the last step. */particles: number; /** Running instances plus the orphans still flying. */
|
|
587
|
+
emitters: number; /** Filter instances over every view. */
|
|
588
|
+
filters: number; /** `renderer.sync.renderPasses()`, read at call time. */
|
|
589
|
+
renderPasses: number;
|
|
590
|
+
};
|
|
591
|
+
/**
|
|
592
|
+
* effects plugin API, `app.effects`. Everything a game does with effects is data on entities;
|
|
593
|
+
* the API only counts.
|
|
594
|
+
*
|
|
595
|
+
* @example
|
|
596
|
+
* ```ts
|
|
597
|
+
* // A test waits for a merge burst to land before it compares the board.
|
|
598
|
+
* while (app.effects.stats().particles > 0) app.time.step(16);
|
|
599
|
+
* app.effects.stats().particles; // 0
|
|
600
|
+
* ```
|
|
601
|
+
*/
|
|
602
|
+
type EffectsApi = {
|
|
603
|
+
/**
|
|
604
|
+
* Counts what runs now. `renderPasses` is the renderer's own number, read at call time; the
|
|
605
|
+
* others are the plugin's. Headless every number is 0.
|
|
606
|
+
*
|
|
607
|
+
* @returns A fresh object: live particles, emitter instances with orphans, filter instances
|
|
608
|
+
* and render passes.
|
|
609
|
+
* @example
|
|
610
|
+
* ```ts
|
|
611
|
+
* // After the board rests: one steam stream, 24 enabled glows on 24 views, one disabled blur.
|
|
612
|
+
* app.effects.stats();
|
|
613
|
+
* // { particles: 18, emitters: 1, filters: 25, renderPasses: 49 }: 1 + 24 × (1 + 1), the disabled blur costs 0
|
|
614
|
+
* ```
|
|
615
|
+
*/
|
|
616
|
+
stats(): EffectsStats;
|
|
617
|
+
};
|
|
618
|
+
/**
|
|
619
|
+
* Resolved dependency APIs.
|
|
620
|
+
*/
|
|
621
|
+
type Deps = {
|
|
622
|
+
flow: Api;
|
|
623
|
+
world: Api$1;
|
|
624
|
+
renderer: Api$2;
|
|
625
|
+
assets: Api$3;
|
|
626
|
+
anim: AnimApi;
|
|
627
|
+
};
|
|
628
|
+
/**
|
|
629
|
+
* What the kernel context offers before the deps are attached. `effects` owns no event, so
|
|
630
|
+
* `emit` is the kernel's and never called here.
|
|
631
|
+
*/
|
|
632
|
+
type KernelSlice = PluginCtx<Config, State> & {
|
|
633
|
+
readonly global: object;
|
|
634
|
+
readonly log: Log.LogApi;
|
|
635
|
+
readonly require: Require;
|
|
636
|
+
};
|
|
637
|
+
/**
|
|
638
|
+
* Domain context shared by both modules: the kernel slice plus the resolved deps.
|
|
639
|
+
*/
|
|
640
|
+
type EffectsCtx = KernelSlice & {
|
|
641
|
+
readonly deps: Deps;
|
|
642
|
+
};
|
|
643
|
+
/**
|
|
644
|
+
* Payload of the `assets:bundle-unloaded` hook: `keys` names every texture that is gone.
|
|
645
|
+
*/
|
|
646
|
+
type BundleUnloaded = Events["assets:bundle-unloaded"];
|
|
647
|
+
//#endregion
|
|
648
|
+
export { NoiseValue as _, types_d_exports as a, EmitterValue as c, ColorMatrixValue as d, DisplacementValue as f, GlowValue as g, FilterValueOf as h, State as i, AlphaValue as l, FilterSpec as m, EffectsApi as n, EmitterConfig as o, FilterComponent as p, EffectsStats as r, EmitterDefinition as s, Config as t, BlurValue as u, OutlineValue as v, UniformDeclaration as y };
|
package/llms.txt
CHANGED
|
@@ -373,7 +373,7 @@ A screen is a projection whose `view` returns JSX. The game's `tsconfig.json` se
|
|
|
373
373
|
- `tr(key, params?)` returns a frozen `Message` `{ key, params }`; no locale is read at the call site. Strings live in `features/<feature>/strings/<locale>.json` as ICU MessageFormat 1 (`{n, plural, one {# order} other {# orders}}`, `select`, `selectordinal`, `number`, `date`, `time`; no skeletons, no `currency`). `bun run assets:keys` compiles them. `app.i18n`: `locale()`, `setLocale(locale)` (async, emits `i18n:locale-changed`), `format(message, locale?)` returns `Part[]`, `plain(message)` returns a string, `has(key)`, `locales()`. Config: `locale: "en"`, `fallback: "en"`, `locales: {}`.
|
|
374
374
|
- Text: `Text` component, `label({ text, style, at, anchor? })` for board text in a projection view, `defineTextStyles({ "hud.digits": { font, size, fill, bold?, italic?, stroke?, strokeWidth?, letterSpacing?, align?, wrap?, digits? } })` under the feature's `textStyles` key. Tags inside a string: `<b>`, `<i>`, `<color=#rrggbb>`, `<icon=key>`, `\<`, `\n`. Built-in styles `body` and `digits` from config `fonts: { body: "ui.font-body", digits: "ui.font-digits" }`. `app.text.measure(content, style)` gives the same numbers in a browser and in Bun.
|
|
375
375
|
- Animation is data: `defineAnimation(id, { slots: { name: type<Anim.Target>() }, build: (slots, { at }) => Step })`. Steps: `sequence(...)`, `parallel(...)`, `stagger(items, ms, item => Step)`, `tween(target, Component, to, { ms, ease?, delayMs?, additive? })`, `set(target, Component, patch)`, `wait(ms)`, `mark(name)`, `frames(target, { keys, fps, loop? })`, `sfx(key, { bus? })`, `haptic(kind)`, `use(animation, slots, tools?)`, `spawn` (a temporary entity, flying coins, that the timeline despawns when it ends; see the anim README). `Frames({ keys, fps, loop, playing })` is a component that walks a sprite through its textures for the entity's whole life. A target is `{ projection, key }` or an entity. A node plays it with `await fx(play(animation, slots))`; in fast mode it finishes at once. `defineMotion({ states, transition?, on: { enter?, exit?, change? } })` builds the enter, exit and change hooks for a projection or the `motion` prop of a tag. Events `anim:mark` `{ animation, mark }` and `anim:finished` `{ animation }`. Config `maxTracks: 2000`.
|
|
376
|
-
- Audio is opt-in: `plugins: [...screen, audioPlugin]`. `fx(sfx("orders.complete"))` resolves when the sound started. `await fx(music("board.theme"))` cross-fades; `music(null)` fades out; a scene's `music` key switches the track on `scenes:changed`. Buses `master`, `music`, `sfx`. Volumes come from the committed player: config `volumes: player => (player as Player).settings.audio`, applied on every `model:committed`. `app.audio`: `setVolume(bus, value)`, `volume(bus)`, `mute(bus, on)`, `unlocked()`, `journal()` (the last started sounds, empty unless config `journal` is above 0). Config: `buses: { master: 1, music: 0.6, sfx: 1 }`, `musicFadeMs: 600`, `volumes`, `context`, `journal: 0`, `music: "decode" | "stream"` default `"decode"` (gapless buffer; `"stream"` is an `<audio>` element, less memory, a gap at every loop), `session: "ambient" | "playback" | "auto"` default `"ambient"` (iOS only: ambient mixes with other apps and obeys the silent switch). Without `audio` every `sfx` and `music` effect resolves `undefined` at once, so the same node runs in a game with no sound.
|
|
376
|
+
- Audio is opt-in: `plugins: [...screen, audioPlugin]`. `fx(sfx("orders.complete"))` resolves when the sound started. `await fx(music("board.theme"))` cross-fades; `music(null)` fades out; a scene's `music` key switches the track on `scenes:changed`. Buses `master`, `music`, `sfx`. Volumes come from the committed player: config `volumes: player => (player as Player).settings.audio`, applied on every `model:committed`. `app.audio`: `setVolume(bus, value)`, `volume(bus)`, `mute(bus, on)`, `muted(bus)`, `unlocked()`, `journal()` (the last started sounds, empty unless config `journal` is above 0). Config: `buses: { master: 1, music: 0.6, sfx: 1 }`, `musicFadeMs: 600`, `volumes`, `context`, `journal: 0`, `music: "decode" | "stream"` default `"decode"` (gapless buffer; `"stream"` is an `<audio>` element, less memory, a gap at every loop), `session: "ambient" | "playback" | "auto"` default `"ambient"` (iOS only: ambient mixes with other apps and obeys the silent switch). Without `audio` every `sfx` and `music` effect resolves `undefined` at once, so the same node runs in a game with no sound.
|
|
377
377
|
- `app.ui`: `tree()` the live screen as plain data, `find(key)` the entity of a keyed element, `fill(key, value)` types into the text field with that `key`, `lint()` tap targets under `tapTargetPt` (default `44`), text that overflows in some locale, absolute elements without `reason`. Config: `tapTargetPt: 44`, `breakpoints: { tall: 2, wide: 1.5 }`. `ui` emits nothing.
|
|
378
378
|
|
|
379
379
|
```tsx
|
|
@@ -466,8 +466,8 @@ A plugin that owns a resource stores it in its state and frees it in `onStop: ({
|
|
|
466
466
|
|
|
467
467
|
Two subpaths, neither on the root (V4). An editor, MCP tools and e2e scripts reach a running game through them; `docs/doors.md` in the engine repository has the full catalogue.
|
|
468
468
|
|
|
469
|
-
- `@moku-labs/game/inspect`: `read(app, source, input?)` reads once; `watch(app, source, input, fn)` reads on the `signals` phase when the source changes and returns the stop function; `defineSource`; the frozen catalogue `sources`; the types `Source`, `InputSchema`, `InputOf`. Every source only reads, so the door is safe in a production build. Keys of `sources`: `graph`, `position`, `history`, `tainted`, `cheats`, `model`, `entities`, `projections`, `ui`, `locate`, `render`, `sounds`, `effects`, `assets`, `log`, `explain`, `diff`, `schema`, `at`; their ids are `game.<key>`. `locate` takes `{ key }` (a ui element) or `{ target }` (a view by projection key) and answers its box in CSS px, the numbers Playwright clicks. `at` takes `{ x, y }` in CSS px and lists the views under that point, topmost first. `explain` takes `{ entity }` and answers that entity in full, `undefined` for a stale id. `diff` takes `{ from, to }` frames and answers what changed per entity; it throws in a production build and for a frame outside the last 120. `schema` lists every component and tag type the world met, with the JSON kind of each field. `sounds` throws `[game] The source game.sounds needs audioPlugin.` and `effects` throws `[game] The source game.effects needs effectsPlugin.` when that plugin is not installed; the editor lists such a source as not installed.
|
|
470
|
-
- `@moku-labs/game/control`: `run(app, command, input?)` resolves `{ value, state }`, `state` being `{ path, frame, tainted }`; `defineCommand`; `controlRefused`; the frozen catalogue `commands`; the types `Command`, `Ran`. Keys of `commands`: `answer`, `tap`, `drag`, `key`, `fill`, `walk`, `bookmark`, `restore`, `step`, `pause`, `resume`, `capture`, `debug`, `reducedMotion`, `timeScale`, `trace`. `capture` resolves `{ png }`, and `{ png, legend }` with `legend: true`; `undefined` while the renderer is inert. `timeScale` takes `{ scale }`; 0 freezes game time while the frames keep running. `trace` takes `{ path }`, a list of views by projection key, and answers whether the gate took the answer.
|
|
469
|
+
- `@moku-labs/game/inspect`: `read(app, source, input?)` reads once; `watch(app, source, input, fn)` reads on the `signals` phase when the source changes and returns the stop function; `defineSource`; the frozen catalogue `sources`; the types `Source`, `InputSchema`, `InputOf`. Every source only reads, so the door is safe in a production build. Keys of `sources`: `graph`, `position`, `history`, `tainted`, `cheats`, `model`, `entities`, `projections`, `ui`, `locate`, `render`, `sounds`, `audioMuted`, `effects`, `assets`, `log`, `explain`, `diff`, `schema`, `at`; their ids are `game.<key>`. `locate` takes `{ key }` (a ui element) or `{ target }` (a view by projection key) and answers its box in CSS px, the numbers Playwright clicks. `at` takes `{ x, y }` in CSS px and lists the views under that point, topmost first. `explain` takes `{ entity }` and answers that entity in full, `undefined` for a stale id. `diff` takes `{ from, to }` frames and answers what changed per entity; it throws in a production build and for a frame outside the last 120. `schema` lists every component and tag type the world met, with the JSON kind of each field. `audioMuted` answers whether the master bus is muted. `sounds` throws `[game] The source game.sounds needs audioPlugin.`, `audioMuted` throws `[game] The source game.audioMuted needs audioPlugin.` and `effects` throws `[game] The source game.effects needs effectsPlugin.` when that plugin is not installed; the editor lists such a source as not installed.
|
|
470
|
+
- `@moku-labs/game/control`: `run(app, command, input?)` resolves `{ value, state }`, `state` being `{ path, frame, tainted }`; `defineCommand`; `controlRefused`; the frozen catalogue `commands`; the types `Command`, `Ran`. Keys of `commands`: `answer`, `tap`, `drag`, `key`, `fill`, `walk`, `bookmark`, `restore`, `step`, `pause`, `resume`, `capture`, `debug`, `reducedMotion`, `timeScale`, `mute`, `trace`. `capture` resolves `{ png }`, and `{ png, legend }` with `legend: true`; `undefined` while the renderer is inert. `timeScale` takes `{ scale }`; 0 freezes game time while the frames keep running. `mute` takes `{ muted }`, mutes the master bus and answers the flag. Without `audioPlugin` it throws `[game] The command game.mute needs audioPlugin.` `trace` takes `{ path }`, a list of views by projection key, and answers whether the gate took the answer.
|
|
471
471
|
- Commands run in dev builds only. Only `__MOKU_GAME_DEV__ === true` turns the dev build on: a bundler `define` (`true` in dev, `false` in production), `globalThis.__MOKU_GAME_DEV__ = true` in the dev entry, or `vi.stubGlobal("__MOKU_GAME_DEV__", true)` in a test. Otherwise `run` throws `[game] Control commands run in dev builds only.`
|
|
472
472
|
- A command's effect is `read`, `route`, `cosmetic`, `cheat` or `raw`. A `cheat` or `raw` command taints the session and is journaled; read it with `sources.tainted` and `sources.cheats`. Of the base commands only `restore` is `raw`.
|
|
473
473
|
- A game's own: `defineSource({ id, title, input, changes, read })` with `changes` `"frame"`, `"commit"` or `"edge"`, and `defineCommand({ id, title, input, effect, run })`. The id is camelCase words joined by dots, at least two: `dice.rolls`. An input schema maps field names to `"string"`, `"number"`, `"boolean"` or `"json"`; a trailing `?` makes one optional. A command body starts with `if (typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) throw controlRefused();`. Keep them in `.dev` modules that only the dev entry and the tests import.
|