minimojs 1.0.0-alpha.4 → 1.0.0-alpha.40

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 (71) hide show
  1. package/README.md +58 -20
  2. package/dist/internal/AnimationSystem.js +345 -0
  3. package/dist/internal/AssetSystem.d.ts +6 -0
  4. package/dist/internal/AssetSystem.js +281 -0
  5. package/dist/internal/BackgroundSystem.d.ts +1 -0
  6. package/dist/internal/BackgroundSystem.js +26 -0
  7. package/dist/internal/CanvasSystem.d.ts +1 -0
  8. package/dist/internal/CanvasSystem.js +51 -0
  9. package/dist/internal/DesignNamingCheck.d.ts +1 -0
  10. package/dist/internal/DesignNamingCheck.js +7 -0
  11. package/dist/internal/ExplosionSystem.d.ts +1 -0
  12. package/dist/internal/ExplosionSystem.js +540 -0
  13. package/dist/internal/InputSystem.d.ts +1 -0
  14. package/dist/internal/InputSystem.js +292 -0
  15. package/dist/internal/LoopSystem.d.ts +1 -0
  16. package/dist/internal/LoopSystem.js +61 -0
  17. package/dist/internal/PhysicsSystem.d.ts +1 -0
  18. package/dist/internal/PhysicsSystem.js +174 -0
  19. package/dist/internal/RenderSystem.d.ts +1 -0
  20. package/dist/internal/RenderSystem.js +1021 -0
  21. package/dist/internal/SoundSystem.d.ts +39 -0
  22. package/dist/internal/SoundSystem.js +292 -0
  23. package/dist/internal/SpriteSystem.d.ts +1 -0
  24. package/dist/internal/SpriteSystem.js +32 -0
  25. package/dist/internal/TextSystem.d.ts +1 -0
  26. package/dist/internal/TextSystem.js +16 -0
  27. package/dist/internal/TimerSystem.d.ts +1 -0
  28. package/dist/internal/TimerSystem.js +43 -0
  29. package/dist/internal/TrailSystem.d.ts +1 -0
  30. package/dist/internal/TrailSystem.js +116 -0
  31. package/dist/internal/TransitionSystem.d.ts +1 -0
  32. package/dist/internal/TransitionSystem.js +74 -0
  33. package/dist/internal/arcade-racer/ArcadeRacerCollisionSystem.d.ts +1 -0
  34. package/dist/internal/arcade-racer/ArcadeRacerCollisionSystem.js +198 -0
  35. package/dist/internal/arcade-racer/ArcadeRacerLaneSystem.d.ts +1 -0
  36. package/dist/internal/arcade-racer/ArcadeRacerLaneSystem.js +120 -0
  37. package/dist/internal/arcade-racer/ArcadeRacerRenderSystem.d.ts +1 -0
  38. package/dist/internal/arcade-racer/ArcadeRacerRenderSystem.js +599 -0
  39. package/dist/internal/arcade-racer/ArcadeRacerRoadSprite.d.ts +1 -0
  40. package/dist/internal/arcade-racer/ArcadeRacerRoadSprite.js +13 -0
  41. package/dist/internal/arcade-racer/ArcadeRacerTrackSystem.d.ts +1 -0
  42. package/dist/internal/arcade-racer/ArcadeRacerTrackSystem.js +447 -0
  43. package/dist/minimo-ambient.d.ts +299 -0
  44. package/dist/minimo-ambient.js +1445 -0
  45. package/dist/minimo-arcaderacer.d.ts +1431 -0
  46. package/dist/minimo-arcaderacer.js +2060 -0
  47. package/dist/minimo-cards.d.ts +359 -0
  48. package/dist/minimo-cards.js +819 -0
  49. package/dist/minimo-dice.d.ts +206 -0
  50. package/dist/minimo-dice.js +719 -0
  51. package/dist/minimo-fire.d.ts +282 -0
  52. package/dist/minimo-fire.js +930 -0
  53. package/dist/minimo-format.d.ts +632 -0
  54. package/dist/minimo-format.js +669 -0
  55. package/dist/minimo-leaderboard.d.ts +199 -0
  56. package/dist/minimo-leaderboard.js +566 -0
  57. package/dist/minimo-multiplayer.d.ts +374 -0
  58. package/dist/minimo-multiplayer.js +1310 -0
  59. package/dist/minimo.d.ts +1690 -233
  60. package/dist/minimo.js +2587 -892
  61. package/package.json +10 -3
  62. package/dist/animations.js +0 -30
  63. package/dist/audio.js +0 -17
  64. package/dist/game.js +0 -1105
  65. package/dist/input.js +0 -185
  66. package/dist/internal-types.js +0 -4
  67. package/dist/physics.js +0 -10
  68. package/dist/render.js +0 -75
  69. package/dist/sprite.js +0 -149
  70. package/dist/timers.js +0 -23
  71. /package/dist/{pointer-info.js → internal/AnimationSystem.d.ts} +0 -0
@@ -0,0 +1,930 @@
1
+ import { DrawSprite } from "./minimo.js";
2
+ const KINDS = {
3
+ torch: { spread: 0.16, spreadY: 0.04, rise: 4.2, life: 0.62, radius: 0.42, rate: 46, smoke: 0.15, sparks: 0.25, light: true, width: 3.6, up: 3.8, down: 1.6, bodyWidth: 0.6, bodyHeight: 1.1, bodyRise: 0.5, boxWidth: 0.7, boxHeight: 1.9, boxRise: 0.8 },
4
+ campfire: { spread: 0.4, spreadY: 0.05, rise: 3.4, life: 0.72, radius: 0.34, rate: 80, smoke: 0.4, sparks: 0.5, light: true, width: 3.8, up: 3.8, down: 1.5, bodyWidth: 0.85, bodyHeight: 1, bodyRise: 0.45, boxWidth: 1.2, boxHeight: 1.8, boxRise: 0.75 },
5
+ burning: { spread: 0.45, spreadY: 0.35, rise: 2.1, life: 0.5, radius: 0.24, rate: 90, smoke: 0.3, sparks: 0.25, light: false, width: 2.4, up: 2.6, down: 1, bodyWidth: 0.9, bodyHeight: 0.9, bodyRise: 0.1, boxWidth: 1.3, boxHeight: 1.6, boxRise: 0.5 },
6
+ trail: { spread: 0.12, spreadY: 0.12, rise: 0.35, life: 0.45, radius: 0.55, rate: 160, smoke: 0.35, sparks: 0.3, light: false, width: 14, up: 7, down: 7, bodyWidth: 0.8, bodyHeight: 0.8, bodyRise: 0, boxWidth: 1.2, boxHeight: 1.2, boxRise: 0 },
7
+ burst: { spread: 0.42, spreadY: 0.06, rise: 3.2, life: 0.7, radius: 0.34, rate: 80, smoke: 0.5, sparks: 0.5, light: true, width: 4, up: 4, down: 1.8, bodyWidth: 0.85, bodyHeight: 1, bodyRise: 0.45, boxWidth: 1.2, boxHeight: 1.8, boxRise: 0.75 },
8
+ explosion: { spread: 0, spreadY: 0, rise: 0, life: 0.5, radius: 0.34, rate: 0, smoke: 0.7, sparks: 0.7, light: true, width: 3.2, up: 2.8, down: 1.7, bodyWidth: 0.8, bodyHeight: 0.8, bodyRise: 0, boxWidth: 2, boxHeight: 2, boxRise: 0 },
9
+ };
10
+ /** How long of burning a flame placed in a design has behind it when it first appears, in seconds. */
11
+ const PREWARM_SECONDS = 1.5;
12
+ const PREWARM_STEP = 1 / 30;
13
+ const PALETTES = {
14
+ fire: ["#fff4b8", "#ffab2e", "#e8401c"],
15
+ blue: ["#eef9ff", "#5cc8ff", "#2c4fff"],
16
+ green: ["#f2ffd2", "#8cff4a", "#14a038"],
17
+ purple: ["#ffe9ff", "#d27bff", "#7326ff"],
18
+ };
19
+ const DEFAULT_SMOKE = "#4a4440";
20
+ const FLAME = 0;
21
+ const SMOKE = 1;
22
+ const SPARK = 2;
23
+ /**
24
+ * Fire for a game: torches, campfires, burning sprites, fireballs, fire where
25
+ * something was destroyed, and explosions. It makes each flame and advances
26
+ * all of them with one {@link Fire.update} call per frame.
27
+ *
28
+ * Every flame is a sprite, a {@link Flame}, so the game places it, layers it,
29
+ * hides it, and destroys it as any other. Its particles are painted into its
30
+ * own surface, so a fire costs one sprite however many particles it has.
31
+ *
32
+ * The module draws fire; the game decides what catches fire, what it hurts,
33
+ * and what puts it out.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * const fire = new Fire(game, { style: "pixel" });
38
+ * game.onUpdate = (dt) => fire.update(dt);
39
+ *
40
+ * const torch = fire.create("torch", { x: 120, y: 200, size: 28 });
41
+ * fire.burst(crate.x, crate.y, { size: 48, burnMs: 4000 }); // where the crate was
42
+ * fire.attach(fireball, { kind: "trail", palette: "blue" });
43
+ * torch.extinguish(600);
44
+ * ```
45
+ *
46
+ * ## In a design
47
+ *
48
+ * A design can place `torch`, `campfire`, `burning`, and `trail` flames, so
49
+ * the creator moves and resizes them in the design editor. Each is a
50
+ * `module` object of a version 3 design. It takes `x`, `y`, `alpha`,
51
+ * `visible`, `layer`, `ignoreScroll`, and the collision body's properties,
52
+ * never `scale` or `rotation`: its `size` option resizes it. Its `options`
53
+ * are `size`, `intensity`, `palette` (a palette's name), `style`, `smoke`,
54
+ * `sparks`, `smokeColor`, and `light`, each as in {@link FlameOptions}.
55
+ *
56
+ * ```json
57
+ * { "id": "3e7b1c9a-5d2f-4a8e-b0c6-9f1d2e4a7b35", "name": "left-torch", "type": "module", "module": "fire", "kind": "torch", "x": 120, "y": 200, "layer": 2, "options": { "size": 28, "palette": "blue" } }
58
+ * ```
59
+ *
60
+ * A placed flame looks as its options say and the module's defaults, never
61
+ * as the look given to `new Fire(game, …)`, so the game shows what the editor
62
+ * showed, and it is already burning when its screen opens. `burst` and
63
+ * `explosion` are moments, made in code. Create the fire in the scene's
64
+ * `onCreate()` before the design's objects, then find a flame by its name
65
+ * to change it while the game plays, and advance the fire every frame:
66
+ *
67
+ * ```ts
68
+ * onCreate() {
69
+ * this.fire = new Fire(game);
70
+ * const room = game.createObjectList(dungeon.objects);
71
+ * this.torch = room.findByName("left-torch"); // later: this.torch.extinguish()
72
+ * room.findByName("barrel-fire").follow(room.findByName("barrel"));
73
+ * }
74
+ * onUpdate(dt) {
75
+ * this.fire.update(dt);
76
+ * }
77
+ * ```
78
+ */
79
+ export class Fire {
80
+ constructor(game, options = {}) {
81
+ /** @internal */
82
+ this._particleCount = 0;
83
+ /** @internal */
84
+ this._flames = [];
85
+ this.game = game;
86
+ const { maxParticles, reducedMotion, wind, seed, ...look } = options;
87
+ this.look = look;
88
+ if (look.palette !== undefined)
89
+ resolvePalette(look.palette);
90
+ if (look.style !== undefined)
91
+ checkStyle(look.style);
92
+ this.wind = finiteOr(wind, 0);
93
+ this.reducedMotion = reducedMotion ?? prefersReducedMotion();
94
+ this._random = typeof seed === "number" && Number.isFinite(seed) ? seededRandom(seed) : Math.random;
95
+ let max = Math.max(0, Math.floor(finiteOr(maxParticles, 1200)));
96
+ const cores = typeof navigator !== "undefined" ? navigator.hardwareConcurrency ?? 8 : 8;
97
+ if (maxParticles === undefined && game.isMobileDevice() && cores <= 4)
98
+ max = Math.round(max * 0.6);
99
+ this._maxParticles = max;
100
+ game._registerDesignModule("fire", (obj) => this._fromDesign(obj));
101
+ }
102
+ /** The flames burning now, in the order they were made. */
103
+ get flames() {
104
+ return this._flames;
105
+ }
106
+ /** How many particles are alive across every flame. */
107
+ get particleCount() {
108
+ return this._particleCount;
109
+ }
110
+ /**
111
+ * Makes a flame, adds it to the game, and returns it. It burns until the
112
+ * game puts it out with {@link Flame.extinguish} or removes it.
113
+ * @param kind - `"torch"`, `"campfire"`, `"burning"`, or `"trail"`.
114
+ * @param options - Position, size, intensity, and any look over the fire's.
115
+ */
116
+ create(kind, options = {}) {
117
+ checkKind(kind);
118
+ return this._add(new Flame(this, kind, options));
119
+ }
120
+ /**
121
+ * Makes a flame that follows `sprite` every frame and adds it to the game.
122
+ * Its particles stay where they were emitted, so a moving sprite leaves a
123
+ * trail behind it and a still one does not. The flame goes out by itself
124
+ * when the sprite is destroyed, or after `ms`.
125
+ * @param sprite - A sprite already added to the game.
126
+ * @param options - The kind, `"burning"` by default, where on the sprite, for how long, and the look.
127
+ */
128
+ attach(sprite, options = {}) {
129
+ const kind = options.kind ?? "burning";
130
+ checkKind(kind);
131
+ const { kind: _kind, offsetX, offsetY, ms, ...rest } = options;
132
+ const size = options.size ?? (kind === "burning" ? Math.max(8, sprite.displayWidth) : kind === "trail" ? Math.max(8, sprite.displayWidth * 0.8) : 32);
133
+ const top = kind === "torch" || kind === "campfire" ? -sprite.displayHeight / 2 : 0;
134
+ const flame = new Flame(this, kind, { ...rest, size, layer: options.layer ?? sprite.layer });
135
+ flame._target = sprite;
136
+ flame._offsetX = finiteOr(offsetX, 0);
137
+ flame._offsetY = finiteOr(offsetY, top);
138
+ if (typeof ms === "number" && Number.isFinite(ms))
139
+ flame._expiresIn = Math.max(0, ms) / 1000;
140
+ flame._follow();
141
+ return this._add(flame);
142
+ }
143
+ /**
144
+ * Leaves fire where something was destroyed: a burst of flame, then flames
145
+ * that die down into smoke and are gone after `burnMs`.
146
+ * @returns A promise that resolves when the fire is out and removed.
147
+ */
148
+ burst(x, y, options = {}) {
149
+ const { size, burnMs, layer, ...look } = options;
150
+ const flame = this._add(new Flame(this, "burst", { ...look, x, y, size: size ?? 48, layer }));
151
+ flame._blast(0.5);
152
+ flame._burnFor(Math.max(0, finiteOr(burnMs, 3000)) / 1000);
153
+ return flame._whenOut();
154
+ }
155
+ /**
156
+ * Blows up at a point: a fireball with sparks and a cloud of smoke, which
157
+ * brightens the scene once and is gone in about a second and a half.
158
+ * @returns A promise that resolves when the explosion is over and removed.
159
+ */
160
+ explosion(x, y, options = {}) {
161
+ const { size, layer, ...look } = options;
162
+ const flame = this._add(new Flame(this, "explosion", { ...look, x, y, size: size ?? 96, layer, intensity: 0 }));
163
+ flame._blast(1);
164
+ flame._out = true;
165
+ return flame._whenOut();
166
+ }
167
+ /** Removes a flame at once, with no dying down. */
168
+ remove(flame) {
169
+ this.game.destroySprite(flame);
170
+ this._drop(flame);
171
+ }
172
+ /** Removes every flame at once. */
173
+ clear() {
174
+ for (const flame of [...this._flames])
175
+ this.remove(flame);
176
+ }
177
+ /** Advances every flame. Call it once per frame from `onUpdate`. */
178
+ update(dt) {
179
+ const seconds = Math.max(0, finiteOr(dt, 0));
180
+ if (this._flames.length === 0)
181
+ return;
182
+ // A flame the game destroyed, such as by game.reset(), is dropped, and a
183
+ // flame whose sprite was destroyed goes out.
184
+ const alive = new Set(this.game.getSprites());
185
+ for (const flame of [...this._flames]) {
186
+ if (!alive.has(flame)) {
187
+ // A flame a design built waits to be added, as with createObject's
188
+ // `add: false`; one that was in the game and left it is dropped.
189
+ if (!flame._pending)
190
+ this._drop(flame);
191
+ continue;
192
+ }
193
+ flame._pending = false;
194
+ if (flame._target && !alive.has(flame._target)) {
195
+ flame._target = null;
196
+ void flame.extinguish(250);
197
+ }
198
+ flame._advance(seconds);
199
+ if (flame._finished())
200
+ this.remove(flame);
201
+ }
202
+ }
203
+ /**
204
+ * Builds a flame placed in a design, with the look its options give it over
205
+ * the module's defaults, never the fire's own look. The game adds it.
206
+ *
207
+ * @internal
208
+ */
209
+ _fromDesign(obj) {
210
+ const kind = obj.kind;
211
+ checkKind(kind);
212
+ const options = (obj.options ?? {});
213
+ const spec = KINDS[kind];
214
+ const flame = new Flame(this, kind, {
215
+ size: options.size,
216
+ intensity: options.intensity,
217
+ light: options.light,
218
+ palette: options.palette ?? "fire",
219
+ style: options.style ?? "soft",
220
+ smoke: options.smoke ?? spec.smoke,
221
+ sparks: options.sparks ?? spec.sparks,
222
+ smokeColor: options.smokeColor ?? DEFAULT_SMOKE,
223
+ });
224
+ flame._pending = true;
225
+ flame._prewarm = true;
226
+ this._flames.push(flame);
227
+ return flame;
228
+ }
229
+ /** @internal */
230
+ _add(flame) {
231
+ this._flames.push(flame);
232
+ this.game.add(flame);
233
+ return flame;
234
+ }
235
+ /** @internal */
236
+ _drop(flame) {
237
+ const index = this._flames.indexOf(flame);
238
+ if (index < 0)
239
+ return;
240
+ this._flames.splice(index, 1);
241
+ this._particleCount -= flame._particles.length;
242
+ flame._particles.length = 0;
243
+ flame._settle();
244
+ }
245
+ }
246
+ /**
247
+ * One fire: a `DrawSprite` whose position is the base of the flame. It has
248
+ * everything a sprite has, layer, visibility, `ignoreScroll` for fire on the
249
+ * HUD, and a collision body over the flame's core, and it carries the tag
250
+ * `fire`. Do not rotate or scale it: its particles are placed in the world.
251
+ *
252
+ * Make flames with {@link Fire.create}, {@link Fire.attach},
253
+ * {@link Fire.burst}, or {@link Fire.explosion}, which also advance them.
254
+ */
255
+ export class Flame extends DrawSprite {
256
+ /**
257
+ * Use {@link Fire.create}, {@link Fire.attach}, {@link Fire.burst}, or
258
+ * {@link Fire.explosion} instead, which also add and advance the flame.
259
+ */
260
+ constructor(fire, kind, options = {}) {
261
+ const spec = KINDS[kind];
262
+ const size = Math.max(4, finiteOr(options.size, 32));
263
+ const width = Math.ceil(spec.width * size);
264
+ const height = Math.ceil((spec.up + spec.down) * size);
265
+ super(width, height, finiteOr(options.x, 0), finiteOr(options.y, 0));
266
+ /** Called once when the flame is out and removed. */
267
+ this.onOut = null;
268
+ /** @internal */
269
+ this._particles = [];
270
+ /** @internal */
271
+ this._target = null;
272
+ /** @internal */
273
+ this._offsetX = 0;
274
+ /** @internal */
275
+ this._offsetY = 0;
276
+ /** @internal Seconds left before an attached flame goes out by itself. */
277
+ this._expiresIn = Infinity;
278
+ /** @internal No more particles are emitted; the flame is removed once the last one is gone. */
279
+ this._out = false;
280
+ /** @internal Built from a design and not yet seen in the game. */
281
+ this._pending = false;
282
+ /** @internal Burns for a moment before its first frame, as a flame placed in a design does. */
283
+ this._prewarm = false;
284
+ /** @internal */
285
+ this._tween = null;
286
+ /** @internal */
287
+ this._time = 0;
288
+ /** @internal */
289
+ this._flash = 0;
290
+ /** @internal */
291
+ this._emitFlames = 0;
292
+ /** @internal */
293
+ this._emitSmoke = 0;
294
+ /** @internal */
295
+ this._emitSparks = 0;
296
+ /** @internal */
297
+ this._burn = null;
298
+ /** @internal Going out once the intensity reaches 0. */
299
+ this._extinguishing = false;
300
+ /** @internal */
301
+ this._waiting = [];
302
+ /** @internal */
303
+ this._settled = false;
304
+ /** @internal */
305
+ this._heat = null;
306
+ /** @internal Where the flame was at the last frame, to spread a moving flame's particles along its path. */
307
+ this._lastX = NaN;
308
+ /** @internal */
309
+ this._lastY = NaN;
310
+ this._fire = fire;
311
+ this._spec = spec;
312
+ this.kind = kind;
313
+ this.size = size;
314
+ this._baseX = width / 2;
315
+ this._baseY = spec.up * size;
316
+ const look = { ...fire.look, ...definedOnly(options) };
317
+ this.palette = look.palette ?? "fire";
318
+ resolvePalette(this.palette);
319
+ this.style = look.style ?? "soft";
320
+ checkStyle(this.style);
321
+ this.smoke = clamp01(finiteOr(look.smoke, spec.smoke));
322
+ this.sparks = clamp01(finiteOr(look.sparks, spec.sparks));
323
+ this.smokeColor = look.smokeColor ?? DEFAULT_SMOKE;
324
+ this.light = options.light ?? spec.light;
325
+ this._intensity = clamp01(finiteOr(options.intensity, 1));
326
+ this.layer = finiteOr(options.layer, 0);
327
+ this.tags = ["fire"];
328
+ this.bodyWidth = spec.bodyWidth * size;
329
+ this.bodyHeight = spec.bodyHeight * size;
330
+ this.bodyOffsetY = -spec.bodyRise * size - this.getAnchorOffsetY();
331
+ const random = fire._random;
332
+ this._phases = [random() * 6.283, random() * 6.283, random() * 6.283];
333
+ }
334
+ /** How strongly it burns, from `0` to `1`. Setting it stops a change in progress. */
335
+ get intensity() {
336
+ return this._intensity;
337
+ }
338
+ set intensity(value) {
339
+ this._finishTween();
340
+ this._intensity = clamp01(finiteOr(value, this._intensity));
341
+ }
342
+ /** `true` once the flame is going out for good: it emits nothing more and is removed when its last particle is gone. */
343
+ get out() {
344
+ return this._out;
345
+ }
346
+ /** The sprite this flame follows, or `null`. */
347
+ get target() {
348
+ return this._target;
349
+ }
350
+ /**
351
+ * Makes the flame follow `sprite` every frame, keeping the offset it has
352
+ * from it now, as a flame placed over a sprite in a design keeps where the
353
+ * creator put it. It hides with the sprite and goes out when the sprite is
354
+ * destroyed. `null` stops following and leaves the flame where it is.
355
+ */
356
+ follow(sprite) {
357
+ if (sprite === null) {
358
+ this._target = null;
359
+ return;
360
+ }
361
+ this._target = sprite;
362
+ this._offsetX = this.x - sprite.x;
363
+ this._offsetY = this.y - sprite.y;
364
+ this._expiresIn = Infinity;
365
+ }
366
+ /**
367
+ * Changes the intensity over `ms` milliseconds.
368
+ * @returns A promise that resolves when it gets there, or when another change replaces it.
369
+ */
370
+ to(intensity, ms) {
371
+ this._finishTween();
372
+ const target = clamp01(finiteOr(intensity, this._intensity));
373
+ const seconds = Math.max(0, finiteOr(ms, 0)) / 1000;
374
+ if (seconds === 0) {
375
+ this._intensity = target;
376
+ return Promise.resolve();
377
+ }
378
+ return new Promise((resolve) => {
379
+ this._tween = { from: this._intensity, to: target, seconds, elapsed: 0, resolve };
380
+ });
381
+ }
382
+ /**
383
+ * Puts the flame out: it dies down over `ms` milliseconds into a puff of
384
+ * smoke, and is removed from the game when the smoke is gone.
385
+ * @returns A promise that resolves when the flame is out and removed.
386
+ */
387
+ extinguish(ms = 600) {
388
+ const promise = this._whenOut();
389
+ if (this._out || this._settled)
390
+ return promise;
391
+ this._burn = null;
392
+ void this.to(0, ms);
393
+ this._extinguishing = true;
394
+ this._goOutWhenDown();
395
+ return promise;
396
+ }
397
+ getAnchorOffsetY() {
398
+ // The position is the flame's base, which sits low in its surface, so
399
+ // the surface's center is above it.
400
+ return this.height / 2 - this._baseY;
401
+ }
402
+ /** @internal */
403
+ _editorBox() {
404
+ const spec = this._spec;
405
+ return { cx: this.x, cy: this.y - spec.boxRise * this.size, w: spec.boxWidth * this.size, h: spec.boxHeight * this.size };
406
+ }
407
+ /** @internal Called by the fire once per frame. */
408
+ _advance(dt) {
409
+ if (this._prewarm) {
410
+ // Placed where the design says only once the game has applied its
411
+ // position, so its burning is caught up on its first frame.
412
+ this._prewarm = false;
413
+ for (let t = 0; t < PREWARM_SECONDS; t += PREWARM_STEP)
414
+ this._advance(PREWARM_STEP);
415
+ }
416
+ this._time += dt;
417
+ this._advanceTween(dt);
418
+ this._goOutWhenDown();
419
+ if (this._burn) {
420
+ const burn = this._burn;
421
+ burn.elapsed = Math.min(burn.seconds, burn.elapsed + dt);
422
+ const u = burn.seconds > 0 ? burn.elapsed / burn.seconds : 1;
423
+ this._intensity = 1 - Math.pow(u, 1.6);
424
+ if (u >= 1) {
425
+ this._burn = null;
426
+ this._puff();
427
+ this._out = true;
428
+ }
429
+ }
430
+ if (this._target) {
431
+ this._follow();
432
+ this._expiresIn -= dt;
433
+ if (this._expiresIn <= 0) {
434
+ this._target = null;
435
+ void this.extinguish(500);
436
+ }
437
+ }
438
+ this._flash = Math.max(0, this._flash - dt);
439
+ if (!this._out && dt > 0)
440
+ this._emit(dt);
441
+ this._lastX = this.x;
442
+ this._lastY = this.y;
443
+ this._move(dt);
444
+ if (this._particles.length > 0 || this._glows())
445
+ this.frozen = false;
446
+ }
447
+ /** @internal */
448
+ _finished() {
449
+ return this._out && this._particles.length === 0 && this._flash <= 0;
450
+ }
451
+ /** @internal */
452
+ _follow() {
453
+ const target = this._target;
454
+ if (!target)
455
+ return;
456
+ this.x = target.x + this._offsetX;
457
+ this.y = target.y + this._offsetY;
458
+ this.visible = target.visible;
459
+ }
460
+ /** @internal Burns down from full intensity to out over `seconds`. */
461
+ _burnFor(seconds) {
462
+ this._burn = { seconds, elapsed: 0 };
463
+ }
464
+ /** @internal A fireball with sparks and smoke, at `strength` of an explosion's. */
465
+ _blast(strength) {
466
+ const random = this._fire._random;
467
+ const s = this.size;
468
+ const full = strength >= 1;
469
+ const flames = Math.round((full ? 30 : 14) * this._density());
470
+ for (let i = 0; i < flames; i++) {
471
+ const angle = random() * Math.PI * 2;
472
+ const speed = s * (full ? 1.6 + random() * 1.4 : 0.9 + random() * 0.9);
473
+ this._spawn(FLAME, this.x, this.y - s * 0.1, Math.cos(angle) * speed, Math.sin(angle) * speed * 0.8 - s * 0.4, 0.3 + random() * 0.3, s * (0.24 + random() * 0.16) * (full ? 1.2 : 1), 3.2, 0);
474
+ }
475
+ const sparks = Math.round((full ? 20 : 8) * this.sparks * this._density());
476
+ for (let i = 0; i < sparks; i++) {
477
+ const angle = random() * Math.PI * 2;
478
+ const speed = s * (2.6 + random() * 2.4);
479
+ this._spawn(SPARK, this.x, this.y, Math.cos(angle) * speed, Math.sin(angle) * speed - s, 0.4 + random() * 0.45, Math.max(1.5, s * 0.035), 1.8, 0);
480
+ }
481
+ const smoke = Math.round((full ? 10 : 5) * this.smoke * this._density());
482
+ for (let i = 0; i < smoke; i++) {
483
+ const angle = random() * Math.PI * 2;
484
+ const r = random() * s * 0.3;
485
+ const p = this._spawn(SMOKE, this.x + Math.cos(angle) * r, this.y + Math.sin(angle) * r - s * 0.2, Math.cos(angle) * s * 0.3, -s * (0.4 + random() * 0.4), 1.2 + random() * 0.6, s * (0.22 + random() * 0.1), 0.8, 0);
486
+ if (p)
487
+ p.age = -(0.08 + random() * 0.22);
488
+ }
489
+ this._flash = full ? 0.45 : 0.25;
490
+ this.frozen = false;
491
+ }
492
+ /** @internal Resolves when the flame is out and removed. */
493
+ _whenOut() {
494
+ if (this._settled)
495
+ return Promise.resolve();
496
+ return new Promise((resolve) => this._waiting.push(resolve));
497
+ }
498
+ /** @internal The flame is removed: answer every promise and call onOut once. */
499
+ _settle() {
500
+ if (this._settled)
501
+ return;
502
+ this._settled = true;
503
+ this._finishTween();
504
+ const waiting = this._waiting;
505
+ this._waiting = [];
506
+ for (const resolve of waiting)
507
+ resolve();
508
+ this.onOut?.();
509
+ }
510
+ redraw(ctx) {
511
+ if (this._particles.length === 0 && !this._glows()) {
512
+ this.frozen = true;
513
+ return;
514
+ }
515
+ const colors = resolvePalette(this.palette, PALETTES.fire);
516
+ if (this.style === "pixel")
517
+ this._drawPixel(ctx, colors);
518
+ else
519
+ this._drawSoft(ctx, colors);
520
+ }
521
+ // -------------------------------------------------------------------------
522
+ // Particles
523
+ // -------------------------------------------------------------------------
524
+ /** @internal The share of particles the device and the player's settings allow. */
525
+ _density() {
526
+ return this._fire.reducedMotion ? 0.5 : 1;
527
+ }
528
+ /** @internal */
529
+ _emit(dt) {
530
+ const spec = this._spec;
531
+ const random = this._fire._random;
532
+ const s = this.size;
533
+ const k = this._intensity;
534
+ if (k <= 0)
535
+ return;
536
+ const density = this._density();
537
+ this._emitFlames += spec.rate * k * density * dt;
538
+ this._emitSmoke += 14 * this.smoke * (0.35 + 0.65 * k) * density * dt;
539
+ this._emitSparks += 7 * this.sparks * k * density * dt;
540
+ const rise = spec.rise * s * (0.6 + 0.4 * k);
541
+ const radius = spec.radius * s * (0.55 + 0.45 * k);
542
+ // A flame that moved since the last frame emits along the way, so a fast
543
+ // fireball leaves a continuous trail rather than a row of dots.
544
+ const fromX = Number.isFinite(this._lastX) ? this._lastX : this.x;
545
+ const fromY = Number.isFinite(this._lastY) ? this._lastY : this.y;
546
+ const count = Math.floor(this._emitFlames);
547
+ for (let i = 0; this._emitFlames >= 1; this._emitFlames--, i++) {
548
+ const along = (i + 1) / count;
549
+ const across = random() * 2 - 1;
550
+ const x = fromX + (this.x - fromX) * along + across * spec.spread * s;
551
+ const y = fromY + (this.y - fromY) * along + (random() * 2 - 1) * spec.spreadY * s;
552
+ const life = spec.life * (0.75 + random() * 0.5);
553
+ // Flames lean in toward the middle as they rise, so they taper to a tip.
554
+ const vx = (-across * spec.spread * s * 0.8) / life + (random() - 0.5) * s * 0.3;
555
+ this._spawn(FLAME, x, y, vx, -rise * (0.8 + random() * 0.4), life, radius * (0.75 + random() * 0.4), 0, across);
556
+ }
557
+ const tip = spec.rise * s * spec.life * 0.55;
558
+ for (; this._emitSmoke >= 1; this._emitSmoke--) {
559
+ const x = this.x + (random() * 2 - 1) * spec.spread * s * 0.6;
560
+ this._spawn(SMOKE, x, this.y - tip, (random() - 0.5) * s * 0.25, -s * (0.7 + random() * 0.5), 1.3 + random() * 0.7, s * (0.18 + random() * 0.08), 0.3, 0);
561
+ }
562
+ for (; this._emitSparks >= 1; this._emitSparks--) {
563
+ const x = this.x + (random() * 2 - 1) * spec.spread * s;
564
+ this._spawn(SPARK, x, this.y - s * 0.2, (random() - 0.5) * s * 1.4, -rise * (1.3 + random() * 0.6), 0.5 + random() * 0.6, Math.max(1.5, s * 0.035), 0.6, 0);
565
+ }
566
+ }
567
+ /** @internal An extinguished flame goes out, in a puff of smoke, once it has died down. */
568
+ _goOutWhenDown() {
569
+ if (!this._extinguishing || this._out || this._tween)
570
+ return;
571
+ this._extinguishing = false;
572
+ this._puff();
573
+ this._out = true;
574
+ }
575
+ /** @internal */
576
+ _spawn(type, x, y, vx, vy, life, radius, drag, across) {
577
+ const fire = this._fire;
578
+ if (this._settled || fire._particleCount >= fire._maxParticles)
579
+ return null;
580
+ const p = { type, x, y, vx, vy, age: 0, life, radius, drag, across, phase: fire._random() * 6.283 };
581
+ this._particles.push(p);
582
+ fire._particleCount++;
583
+ return p;
584
+ }
585
+ /** @internal A puff of smoke where the flame was. */
586
+ _puff() {
587
+ const random = this._fire._random;
588
+ const s = this.size;
589
+ const count = Math.round(6 * this._density());
590
+ for (let i = 0; i < count; i++) {
591
+ this._spawn(SMOKE, this.x + (random() * 2 - 1) * s * 0.3, this.y - s * 0.3, (random() - 0.5) * s * 0.4, -s * (0.6 + random() * 0.5), 1 + random() * 0.6, s * (0.2 + random() * 0.1), 0.4, 0);
592
+ }
593
+ }
594
+ /** @internal */
595
+ _move(dt) {
596
+ const fire = this._fire;
597
+ const particles = this._particles;
598
+ const wind = fire.wind;
599
+ const s = this.size;
600
+ for (let i = particles.length - 1; i >= 0; i--) {
601
+ const p = particles[i];
602
+ p.age += dt;
603
+ if (p.age >= p.life) {
604
+ particles[i] = particles[particles.length - 1];
605
+ particles.pop();
606
+ fire._particleCount--;
607
+ continue;
608
+ }
609
+ if (p.age < 0)
610
+ continue;
611
+ const keep = Math.exp(-p.drag * dt);
612
+ p.vx *= keep;
613
+ p.vy *= keep;
614
+ const lean = p.type === SMOKE ? 0.45 : p.type === SPARK ? 0.35 : 0.25;
615
+ let vx = p.vx + wind * lean;
616
+ if (p.type === FLAME)
617
+ vx += Math.sin(this._time * 9 + p.phase) * s * 0.35;
618
+ if (p.type === SPARK)
619
+ p.vy += s * 1.2 * dt;
620
+ p.x += vx * dt;
621
+ p.y += p.vy * dt;
622
+ }
623
+ }
624
+ /** @internal */
625
+ _glows() {
626
+ return (this.light && this._intensity > 0 && !this._out) || this._flash > 0;
627
+ }
628
+ /** @internal A gentle flicker around 1: small and smooth, never a flash. */
629
+ _flicker() {
630
+ const [a, b, c] = this._phases;
631
+ const t = this._time;
632
+ const amount = this._fire.reducedMotion ? 0.5 : 1;
633
+ return 1 + amount * (0.07 * Math.sin(t * 11 + a) + 0.05 * Math.sin(t * 17.3 + b) + 0.04 * Math.sin(t * 6.1 + c));
634
+ }
635
+ /** @internal How bright the glow is, and how far it reaches, in pixels. */
636
+ _glow() {
637
+ const s = this.size;
638
+ const flicker = this._flicker();
639
+ let alpha = this.light && !this._out ? 0.3 * this._intensity * flicker : 0;
640
+ let radius = s * 1.6 * (0.8 + 0.2 * this._intensity) * flicker;
641
+ if (this._flash > 0) {
642
+ // An explosion or a burst brightens once and fades.
643
+ const total = this.kind === "explosion" ? 0.45 : 0.25;
644
+ const u = 1 - this._flash / total;
645
+ alpha = Math.max(alpha, 0.8 * (1 - u));
646
+ radius = Math.max(radius, s * (0.9 + 0.5 * u));
647
+ }
648
+ return { alpha: Math.min(1, alpha), radius };
649
+ }
650
+ /** @internal The particle's place in the surface, and how much the surface's edge fades it. */
651
+ _local(p) {
652
+ const lx = p.x - this.x + this._baseX;
653
+ const ly = p.y - this.y + this._baseY;
654
+ const band = this.size * 0.6;
655
+ const edge = Math.min(lx, this.width - lx, ly, this.height - ly);
656
+ return [lx, ly, Math.max(0, Math.min(1, edge / band))];
657
+ }
658
+ // -------------------------------------------------------------------------
659
+ // Soft style
660
+ // -------------------------------------------------------------------------
661
+ /** @internal */
662
+ _drawSoft(ctx, colors) {
663
+ const glow = this._glow();
664
+ const cx = this._baseX;
665
+ const cy = this._baseY - this.size * this._spec.bodyRise;
666
+ ctx.globalCompositeOperation = "lighter";
667
+ if (glow.alpha > 0.004) {
668
+ ctx.globalAlpha = glow.alpha;
669
+ ctx.drawImage(blob(colors[2], 0.9), cx - glow.radius, cy - glow.radius, glow.radius * 2, glow.radius * 2);
670
+ }
671
+ ctx.globalCompositeOperation = "source-over";
672
+ const smoke = blob(this.smokeColor, 0.55);
673
+ for (const p of this._particles) {
674
+ if (p.type !== SMOKE || p.age < 0)
675
+ continue;
676
+ const a = p.age / p.life;
677
+ const [lx, ly, fade] = this._local(p);
678
+ const alpha = 0.32 * Math.min(1, a / 0.2) * (1 - a) * fade;
679
+ if (alpha <= 0.004)
680
+ continue;
681
+ const r = p.radius * (1 + a * 2.2);
682
+ ctx.globalAlpha = alpha;
683
+ ctx.drawImage(smoke, lx - r, ly - r, r * 2, r * 2);
684
+ }
685
+ ctx.globalCompositeOperation = "lighter";
686
+ for (const p of this._particles) {
687
+ if (p.type === SMOKE || p.age < 0)
688
+ continue;
689
+ const a = p.age / p.life;
690
+ const [lx, ly, fade] = this._local(p);
691
+ if (p.type === SPARK) {
692
+ const alpha = (1 - a) * fade * (0.7 + 0.3 * Math.sin(p.phase + this._time * 30));
693
+ if (alpha <= 0.004)
694
+ continue;
695
+ const r = p.radius * 2;
696
+ ctx.globalAlpha = alpha;
697
+ ctx.drawImage(blob(a < 0.5 ? colors[0] : colors[1], 1), lx - r, ly - r, r * 2, r * 2);
698
+ continue;
699
+ }
700
+ const r = p.radius * Math.pow(1 - a, 0.6);
701
+ if (r < 0.5)
702
+ continue;
703
+ const alpha = 0.55 * Math.pow(1 - a, 0.8) * fade;
704
+ // Hot at birth, cooling to the edge color: two neighbouring stops, mixed.
705
+ const [inner, outer, mix] = a < 0.2 ? [0, 1, a / 0.2] : [1, 2, Math.min(1, (a - 0.2) / 0.45)];
706
+ ctx.globalAlpha = alpha * (1 - mix);
707
+ ctx.drawImage(blob(colors[inner], 0.75), lx - r, ly - r, r * 2, r * 2);
708
+ ctx.globalAlpha = alpha * mix;
709
+ ctx.drawImage(blob(colors[outer], 0.75), lx - r, ly - r, r * 2, r * 2);
710
+ }
711
+ ctx.globalAlpha = 1;
712
+ ctx.globalCompositeOperation = "source-over";
713
+ }
714
+ // -------------------------------------------------------------------------
715
+ // Pixel style
716
+ // -------------------------------------------------------------------------
717
+ /** @internal */
718
+ _drawPixel(ctx, colors) {
719
+ const cell = Math.max(2, Math.round(this.size / 9));
720
+ const columns = Math.ceil(this.width / cell);
721
+ const rows = Math.ceil(this.height / cell);
722
+ const glow = this._glow();
723
+ const cx = this._baseX;
724
+ const cy = this._baseY - this.size * this._spec.bodyRise;
725
+ // The glow: two stepped discs of the edge color.
726
+ if (glow.alpha > 0.004) {
727
+ ctx.fillStyle = colors[2];
728
+ for (const [share, alpha] of [[1, 0.35], [0.6, 0.45]]) {
729
+ ctx.globalAlpha = glow.alpha * alpha;
730
+ pixelDisc(ctx, cx, cy, glow.radius * share, cell);
731
+ }
732
+ }
733
+ // Smoke: squares on the grid, in three steps of transparency.
734
+ ctx.fillStyle = this.smokeColor;
735
+ for (const p of this._particles) {
736
+ if (p.type !== SMOKE || p.age < 0)
737
+ continue;
738
+ const a = p.age / p.life;
739
+ const [lx, ly, fade] = this._local(p);
740
+ const alpha = 0.4 * Math.min(1, a / 0.2) * (1 - a) * fade;
741
+ const step = Math.round(alpha * 6) / 6;
742
+ if (step <= 0)
743
+ continue;
744
+ const half = Math.max(1, Math.round((p.radius * (1 + a * 2.2)) / cell)) * cell;
745
+ ctx.globalAlpha = step;
746
+ ctx.fillRect(snap(lx - half, cell), snap(ly - half, cell), half * 2, half * 2);
747
+ }
748
+ ctx.globalAlpha = 1;
749
+ // Flames: each particle heats the cells under it, and each cell takes the
750
+ // palette color of its heat.
751
+ const size = columns * rows;
752
+ if (!this._heat || this._heat.length !== size)
753
+ this._heat = new Float32Array(size);
754
+ const heat = this._heat;
755
+ heat.fill(0);
756
+ let any = false;
757
+ for (const p of this._particles) {
758
+ if (p.type !== FLAME || p.age < 0)
759
+ continue;
760
+ const a = p.age / p.life;
761
+ const r = p.radius * Math.pow(1 - a, 0.6);
762
+ if (r < 0.5)
763
+ continue;
764
+ const [lx, ly, fade] = this._local(p);
765
+ const hot = (1 - a) * fade * 1.25;
766
+ const reach = r / cell;
767
+ const col = lx / cell;
768
+ const row = ly / cell;
769
+ const c0 = Math.max(0, Math.floor(col - reach));
770
+ const c1 = Math.min(columns - 1, Math.ceil(col + reach));
771
+ const r0 = Math.max(0, Math.floor(row - reach));
772
+ const r1 = Math.min(rows - 1, Math.ceil(row + reach));
773
+ for (let y = r0; y <= r1; y++) {
774
+ for (let x = c0; x <= c1; x++) {
775
+ const d = Math.hypot(x + 0.5 - col, y + 0.5 - row) / reach;
776
+ if (d < 1) {
777
+ heat[y * columns + x] += hot * (1 - d);
778
+ any = true;
779
+ }
780
+ }
781
+ }
782
+ }
783
+ if (any) {
784
+ const bands = [[0.25, colors[2]], [0.7, colors[1]], [1.5, colors[0]]];
785
+ for (const [threshold, color] of bands) {
786
+ ctx.fillStyle = color;
787
+ ctx.beginPath();
788
+ for (let i = 0; i < size; i++) {
789
+ if (heat[i] >= threshold)
790
+ ctx.rect((i % columns) * cell, Math.floor(i / columns) * cell, cell, cell);
791
+ }
792
+ ctx.fill();
793
+ }
794
+ }
795
+ // Sparks: one cell each.
796
+ for (const p of this._particles) {
797
+ if (p.type !== SPARK || p.age < 0)
798
+ continue;
799
+ const a = p.age / p.life;
800
+ const [lx, ly, fade] = this._local(p);
801
+ if (fade <= 0)
802
+ continue;
803
+ ctx.fillStyle = a < 0.5 ? colors[0] : colors[1];
804
+ ctx.fillRect(snap(lx, cell), snap(ly, cell), cell, cell);
805
+ }
806
+ }
807
+ // -------------------------------------------------------------------------
808
+ // Intensity changes
809
+ // -------------------------------------------------------------------------
810
+ /** @internal */
811
+ _advanceTween(dt) {
812
+ const tween = this._tween;
813
+ if (!tween)
814
+ return;
815
+ tween.elapsed = Math.min(tween.seconds, tween.elapsed + dt);
816
+ const u = tween.elapsed / tween.seconds;
817
+ this._intensity = tween.from + (tween.to - tween.from) * u;
818
+ if (u >= 1)
819
+ this._finishTween();
820
+ }
821
+ /** @internal */
822
+ _finishTween() {
823
+ const tween = this._tween;
824
+ if (!tween)
825
+ return;
826
+ this._tween = null;
827
+ this._intensity = tween.to;
828
+ tween.resolve();
829
+ }
830
+ }
831
+ // ---------------------------------------------------------------------------
832
+ // Helpers
833
+ // ---------------------------------------------------------------------------
834
+ const blobs = new Map();
835
+ /** A round, soft dot of `color`, drawn once and kept: the soft style's particle. */
836
+ function blob(color, core) {
837
+ const key = `${color}|${core}`;
838
+ let canvas = blobs.get(key);
839
+ if (!canvas) {
840
+ canvas = document.createElement("canvas");
841
+ canvas.width = 64;
842
+ canvas.height = 64;
843
+ const ctx = canvas.getContext("2d");
844
+ if (ctx) {
845
+ const gradient = ctx.createRadialGradient(32, 32, 0, 32, 32, 32);
846
+ gradient.addColorStop(0, color);
847
+ gradient.addColorStop(core * 0.5, color);
848
+ gradient.addColorStop(1, "rgba(0, 0, 0, 0)");
849
+ ctx.fillStyle = gradient;
850
+ ctx.globalAlpha = 1;
851
+ ctx.fillRect(0, 0, 64, 64);
852
+ // The gradient's last stop is transparent black; redraw its color into
853
+ // the fade so the edge does not darken.
854
+ ctx.globalCompositeOperation = "source-in";
855
+ ctx.fillStyle = color;
856
+ ctx.fillRect(0, 0, 64, 64);
857
+ }
858
+ blobs.set(key, canvas);
859
+ }
860
+ return canvas;
861
+ }
862
+ /** A disc made of grid cells, row by row. */
863
+ function pixelDisc(ctx, cx, cy, radius, cell) {
864
+ const rows = Math.floor(radius / cell);
865
+ ctx.beginPath();
866
+ for (let r = -rows; r <= rows; r++) {
867
+ const y = cy + r * cell;
868
+ const half = Math.sqrt(Math.max(0, radius * radius - (r * cell) ** 2));
869
+ const left = snap(cx - half, cell);
870
+ const right = snap(cx + half, cell);
871
+ if (right > left)
872
+ ctx.rect(left, snap(y, cell), right - left, cell);
873
+ }
874
+ ctx.fill();
875
+ }
876
+ function snap(value, cell) {
877
+ return Math.round(value / cell) * cell;
878
+ }
879
+ function checkKind(kind) {
880
+ if (kind !== "torch" && kind !== "campfire" && kind !== "burning" && kind !== "trail") {
881
+ throw new Error(`MinimoJS: There is no fire kind "${String(kind)}"; the kinds are torch, campfire, burning, and trail.`);
882
+ }
883
+ }
884
+ function checkStyle(style) {
885
+ if (style !== "soft" && style !== "pixel") {
886
+ throw new Error(`MinimoJS: There is no fire style "${String(style)}"; the styles are soft and pixel.`);
887
+ }
888
+ }
889
+ function resolvePalette(palette, fallback) {
890
+ if (Array.isArray(palette) && palette.length === 3 && palette.every((c) => typeof c === "string")) {
891
+ return palette;
892
+ }
893
+ if (typeof palette === "string" && Object.prototype.hasOwnProperty.call(PALETTES, palette)) {
894
+ return PALETTES[palette];
895
+ }
896
+ if (fallback)
897
+ return fallback;
898
+ throw new Error(`MinimoJS: There is no fire palette ${JSON.stringify(palette)}; use fire, blue, green, purple, or three colors from the core to the edge.`);
899
+ }
900
+ function definedOnly(value) {
901
+ const out = {};
902
+ for (const [key, field] of Object.entries(value))
903
+ if (field !== undefined)
904
+ out[key] = field;
905
+ return out;
906
+ }
907
+ function clamp01(value) {
908
+ return Math.max(0, Math.min(1, value));
909
+ }
910
+ function finiteOr(value, fallback) {
911
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
912
+ }
913
+ function prefersReducedMotion() {
914
+ try {
915
+ return typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true;
916
+ }
917
+ catch {
918
+ return false;
919
+ }
920
+ }
921
+ function seededRandom(seed) {
922
+ let state = Math.floor(seed) >>> 0;
923
+ return () => {
924
+ state = (state + 0x6d2b79f5) >>> 0;
925
+ let t = state;
926
+ t = Math.imul(t ^ (t >>> 15), t | 1);
927
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
928
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
929
+ };
930
+ }