minimojs 1.0.0-alpha.3 → 1.0.0-alpha.30

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 (63) hide show
  1. package/README.md +85 -285
  2. package/dist/internal/AnimationSystem.js +345 -0
  3. package/dist/internal/AssetSystem.d.ts +1 -0
  4. package/dist/internal/AssetSystem.js +186 -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 +1 -0
  22. package/dist/internal/SoundSystem.js +55 -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-format.d.ts +407 -0
  50. package/dist/minimo-format.js +446 -0
  51. package/dist/minimo.d.ts +1754 -225
  52. package/dist/minimo.js +2551 -761
  53. package/package.json +10 -3
  54. package/dist/animations.js +0 -30
  55. package/dist/audio.js +0 -17
  56. package/dist/game.js +0 -1105
  57. package/dist/input.js +0 -185
  58. package/dist/internal-types.js +0 -4
  59. package/dist/physics.js +0 -10
  60. package/dist/render.js +0 -75
  61. package/dist/sprite.js +0 -149
  62. package/dist/timers.js +0 -23
  63. /package/dist/{pointer-info.js → internal/AnimationSystem.d.ts} +0 -0
package/dist/minimo.d.ts CHANGED
@@ -7,54 +7,249 @@
7
7
  * ALL ROTATIONS ARE IN DEGREES.
8
8
  * THE ENGINE LOOP USES requestAnimationFrame ONLY.
9
9
  */
10
+ import type { DesignObject } from "./minimo-format.js";
11
+ export { Design, parseAssetFile } from "./minimo-format.js";
12
+ export type { AssetFile, DesignBackground, DesignEmoji, DesignFile, DesignImage, DesignObject, DesignObjectBase, DesignObjectType, DesignSpriteBase, DesignText, ImageAsset, UnknownAsset, } from "./minimo-format.js";
10
13
  /**
11
- * A 2D game object rendered as an emoji on the canvas.
14
+ * Optional tuning values for {@link Game.animateExplode}.
12
15
  *
13
- * Instantiate directly or extend to create custom sprite types.
14
- * Register with the engine by passing the instance to {@link Game.add}.
16
+ * All fields are optional. Omitted fields use the engine defaults.
17
+ */
18
+ export interface ExplodeOptions {
19
+ /** Number of vertical slices used to break the sprite apart. */
20
+ rows?: number;
21
+ /** Number of horizontal slices used to break the sprite apart. */
22
+ cols?: number;
23
+ /** Total effect duration in **milliseconds**. */
24
+ durationMs?: number;
25
+ /** Initial outward velocity magnitude in pixels per second. */
26
+ speed?: number;
27
+ /** Downward acceleration applied to pieces in pixels per second squared. */
28
+ gravityY?: number;
29
+ /** Maximum angular speed applied to pieces, in degrees per second. */
30
+ spin?: number;
31
+ /** Whether pieces fade out as the effect approaches completion. */
32
+ fade?: boolean;
33
+ /** Whether the original sprite is destroyed instead of restored after the effect. */
34
+ destroySprite?: boolean;
35
+ }
36
+ /**
37
+ * Cardinal flow direction used by directional visual effects.
38
+ */
39
+ export type FlowDirection = "left-to-right" | "right-to-left" | "top-to-bottom" | "bottom-to-top";
40
+ /**
41
+ * Direction used by piece-based local integration and disintegration effects.
15
42
  *
16
- * **Coordinate system:** center-based world space. `(x, y)` is the center of
17
- * the sprite. Positive X = right, positive Y = down.
43
+ * - Cardinal values apply a directional sweep across the sprite.
44
+ * - `"in-place"` keeps pieces in their final positions and only randomizes
45
+ * when each piece appears or disappears.
46
+ */
47
+ export type PieceFlowDirection = FlowDirection | "in-place";
48
+ /**
49
+ * Optional tuning values for {@link Game.animateAssemble}.
18
50
  *
19
- * **Lifecycle:** A sprite exists until {@link Game.destroySprite} is called or
20
- * {@link Game.reset} is invoked. After destruction, do not read or write its fields.
51
+ * All fields are optional. Omitted fields use the engine defaults.
52
+ */
53
+ export interface AssembleOptions {
54
+ /** Number of vertical slices used to reconstruct the sprite. */
55
+ rows?: number;
56
+ /** Number of horizontal slices used to reconstruct the sprite. */
57
+ cols?: number;
58
+ /** Total effect duration in **milliseconds**. */
59
+ durationMs?: number;
60
+ /** Initial outward distance magnitude in pixels per second equivalent. */
61
+ speed?: number;
62
+ /** Downward pull applied while pieces converge, in pixels per second squared. */
63
+ gravityY?: number;
64
+ /** Maximum angular speed applied to pieces, in degrees per second. */
65
+ spin?: number;
66
+ /** Whether pieces fade in while assembling. */
67
+ fade?: boolean;
68
+ }
69
+ /**
70
+ * Optional tuning values for {@link Game.animateDisintegrate}.
21
71
  *
22
- * @example
23
- * ```ts
24
- * // Direct instantiation
25
- * const coin = new Sprite("🪙");
26
- * coin.x = 300;
27
- * coin.y = 200;
28
- * coin.size = 32;
29
- * game.add(coin);
30
- * ```
72
+ * All fields are optional. Omitted fields use the engine defaults.
73
+ */
74
+ export interface DisintegrateOptions {
75
+ /** Number of vertical slices used to dissolve the sprite. */
76
+ rows?: number;
77
+ /** Number of horizontal slices used to dissolve the sprite. */
78
+ cols?: number;
79
+ /** Total effect duration in **milliseconds**. */
80
+ durationMs?: number;
81
+ /** Primary sweep direction across the sprite. */
82
+ direction?: PieceFlowDirection;
83
+ /** Maximum local travel distance in pixels for each piece. */
84
+ distance?: number;
85
+ /** Maximum angular speed applied to pieces, in degrees per second. */
86
+ spin?: number;
87
+ /** Whether pieces fade out as they disintegrate. */
88
+ fade?: boolean;
89
+ /** Whether the original sprite is destroyed instead of left hidden after the effect. */
90
+ destroySprite?: boolean;
91
+ }
92
+ /**
93
+ * Optional tuning values for {@link Game.animateIntegrate}.
31
94
  *
32
- * @example
33
- * ```ts
34
- * // Subclassing for custom game objects
35
- * class Player extends Sprite {
36
- * health = 3;
37
- *
38
- * constructor(x: number, y: number) {
39
- * super("🐢");
40
- * this.x = x;
41
- * this.y = y;
42
- * this.size = 48;
43
- * this.gravityScale = 1;
44
- * }
45
- * }
95
+ * All fields are optional. Omitted fields use the engine defaults.
96
+ */
97
+ export interface IntegrateOptions {
98
+ /** Number of vertical slices used to reconstruct the sprite. */
99
+ rows?: number;
100
+ /** Number of horizontal slices used to reconstruct the sprite. */
101
+ cols?: number;
102
+ /** Total effect duration in **milliseconds**. */
103
+ durationMs?: number;
104
+ /** Primary sweep direction across the sprite. */
105
+ direction?: PieceFlowDirection;
106
+ /** Maximum local travel distance in pixels for each piece. */
107
+ distance?: number;
108
+ /** Maximum angular speed applied to pieces, in degrees per second. */
109
+ spin?: number;
110
+ /** Whether pieces fade in while integrating. */
111
+ fade?: boolean;
112
+ }
113
+ /**
114
+ * Wipe mode used by {@link Game.animateWipe}.
115
+ */
116
+ export type WipeMode = "reveal" | "cover";
117
+ /**
118
+ * Full-screen transition type used by {@link Game.transitionTo}.
119
+ */
120
+ export type ScreenTransitionType = "fade" | "wipe" | "slide" | "iris" | "pixelate" | "flash";
121
+ /**
122
+ * Optional tuning values for {@link Game.animateWipe}.
46
123
  *
47
- * const player = new Player(400, 300);
48
- * game.add(player);
49
- * ```
124
+ * All fields are optional. Omitted fields use the engine defaults.
125
+ */
126
+ export interface WipeOptions {
127
+ /** Whether the wipe reveals the sprite or covers it away. */
128
+ mode?: WipeMode;
129
+ /** Primary wipe direction. */
130
+ direction?: FlowDirection;
131
+ /** Total effect duration in **milliseconds**. */
132
+ durationMs?: number;
133
+ /** Whether the original sprite is destroyed for `mode: "cover"`. */
134
+ destroySprite?: boolean;
135
+ }
136
+ /**
137
+ * Optional tuning values for {@link Game.transitionTo}.
138
+ *
139
+ * MinimoJS scene transitions operate on frozen snapshots of the outgoing and
140
+ * incoming scenes. The target scene is created immediately, then both
141
+ * snapshots are composited for the duration of the effect.
142
+ */
143
+ export interface SceneTransitionOptions {
144
+ /** Required transition style. */
145
+ type: ScreenTransitionType;
146
+ /** Total transition duration in **milliseconds**. */
147
+ durationMs?: number;
148
+ /** Optional color used by `fade` and `flash`. */
149
+ color?: string;
150
+ /** Direction used by `wipe` and `slide`. */
151
+ direction?: FlowDirection;
152
+ /** Optional iris center X position in canvas pixels. */
153
+ centerX?: number;
154
+ /** Optional iris center Y position in canvas pixels. */
155
+ centerY?: number;
156
+ /** Maximum pixel block size used by `pixelate`. */
157
+ pixelSize?: number;
158
+ }
159
+ /**
160
+ * Optional tuning values for {@link Game.animateTrail}.
161
+ *
162
+ * All fields are optional. Omitted fields use the engine defaults.
163
+ */
164
+ export interface TrailOptions {
165
+ /** How long the emitter spawns afterimages, in **milliseconds**. */
166
+ durationMs?: number;
167
+ /** Time between emitted afterimages, in **milliseconds**. */
168
+ spacingMs?: number;
169
+ /** Lifetime of each afterimage before it disappears, in **milliseconds**. */
170
+ fadeMs?: number;
171
+ /** Starting opacity multiplier for each emitted afterimage, in range `[0, 1]`. */
172
+ alpha?: number;
173
+ /** Final scale multiplier reached by each afterimage before it vanishes. */
174
+ scaleTo?: number;
175
+ }
176
+ /**
177
+ * How a {@link BackgroundLayer} image is fit inside its destination rectangle.
178
+ *
179
+ * - `"none"`: draws the image at its original size with no scaling.
180
+ * - `"stretch"`: fills the destination exactly, ignoring aspect ratio.
181
+ * - `"contain"`: preserves aspect ratio and keeps the entire image visible.
182
+ * - `"cover"`: preserves aspect ratio and fully covers the destination, cropping if needed.
50
183
  */
51
- export declare class Sprite {
184
+ export type BackgroundFit = "none" | "stretch" | "contain" | "cover";
185
+ /**
186
+ * Minimal scene contract understood by {@link Game.start} and {@link Game.reset}.
187
+ *
188
+ * Scene methods are invoked directly on the scene instance, so class-based
189
+ * scenes keep their expected `this` value.
190
+ */
191
+ export interface IScene {
52
192
  /**
53
- * The emoji character used to render this sprite.
54
- * Must be a single emoji. Change this at runtime to animate between frames.
55
- * @example "🔥", "⭐", "🐢"
193
+ * Registers images this scene needs that the rest of the game does not.
194
+ *
195
+ * **Only for games with many scenes that each carry many assets of their
196
+ * own**, such as a game with many levels, where a player does not reach
197
+ * every scene in a session and loading all of them at boot would make the
198
+ * first wait needlessly long. Every other game should load its images in
199
+ * {@link Game.onPreload}, once, at boot.
200
+ *
201
+ * Called whenever the scene is entered — by {@link Game.start},
202
+ * {@link Game.reset}, or {@link Game.transitionTo} — before
203
+ * {@link IScene.onCreate}. Call {@link Game.loadAssets} or
204
+ * {@link Game.loadImage} inside it, as inside `game.onPreload`. Images that
205
+ * are already loaded are skipped, so replaying a scene loads nothing.
206
+ *
207
+ * When images are pending, the scene being left freezes, dimmed, under a
208
+ * progress ring until they load, and only then is this scene created.
209
+ * Because of that, `reset()` and `transitionTo()` may return before this
210
+ * scene exists: continue in `onCreate()`, not in the code after the call.
211
+ *
212
+ * @example
213
+ * ```ts
214
+ * class Level {
215
+ * constructor(n) { this.n = n; }
216
+ * onPreload() {
217
+ * game.loadImage(`level-${this.n}-bg`, `assets/level-${this.n}-bg.png`);
218
+ * }
219
+ * onCreate() {
220
+ * game.addBackground(new BackgroundLayer(`level-${this.n}-bg`));
221
+ * }
222
+ * }
223
+ * ```
56
224
  */
57
- sprite: string;
225
+ onPreload?(): void;
226
+ /**
227
+ * Called when the scene is created, before the first frame and again after
228
+ * each {@link Game.reset} that targets this scene. When the scene has an
229
+ * {@link IScene.onPreload} with images still loading, it is called once they
230
+ * have loaded.
231
+ */
232
+ onCreate?(): void;
233
+ /**
234
+ * Called once per frame after timers, animation, and physics updates.
235
+ *
236
+ * @param dt - Delta time in seconds since the previous frame.
237
+ */
238
+ onUpdate?(dt: number): void;
239
+ }
240
+ /**
241
+ * Base class for all renderable MinimoJS actors.
242
+ *
243
+ * Concrete subclasses provide their own visual content while inheriting the
244
+ * shared transform, visibility, physics, and animation state required by the
245
+ * engine.
246
+ */
247
+ export declare abstract class Sprite {
248
+ protected constructor(game?: Game | null);
249
+ /**
250
+ * Game instance associated with this sprite, if any.
251
+ */
252
+ get game(): Game | null;
58
253
  /**
59
254
  * X position in world space (horizontal center of the sprite), in pixels.
60
255
  * Positive X points right. Updated each frame by: `x += vx * dt`.
@@ -68,10 +263,80 @@ export declare class Sprite {
68
263
  */
69
264
  y: number;
70
265
  /**
71
- * Width and height of the sprite's bounding square, in pixels.
72
- * Used for both canvas rendering (font size) and AABB collision detection.
266
+ * Resolved center X used internally by rendering, hit testing, and collisions.
267
+ */
268
+ get renderX(): number;
269
+ /**
270
+ * Resolved center Y used internally by rendering, hit testing, and collisions.
271
+ */
272
+ get renderY(): number;
273
+ /**
274
+ * Visual scale multiplier applied to this sprite's logical width/height.
275
+ * Default: `1`.
276
+ */
277
+ scale: number;
278
+ /**
279
+ * Base logical width in pixels before applying {@link Sprite.scale}.
280
+ */
281
+ abstract get width(): number;
282
+ /**
283
+ * Base logical height in pixels before applying {@link Sprite.scale}.
284
+ */
285
+ abstract get height(): number;
286
+ /**
287
+ * Effective rendered/collision width in pixels.
288
+ */
289
+ get displayWidth(): number;
290
+ /**
291
+ * Effective rendered/collision height in pixels.
292
+ */
293
+ get displayHeight(): number;
294
+ /**
295
+ * Optional logical body width used by physics helpers and collision checks.
296
+ *
297
+ * When `null` (default), MinimoJS uses the sprite's visual {@link Sprite.width}.
298
+ * When set, this value is scaled by {@link Sprite.scale} the same way as the
299
+ * visual sprite size.
300
+ */
301
+ bodyWidth: number | null;
302
+ /**
303
+ * Optional logical body height used by physics helpers and collision checks.
304
+ *
305
+ * When `null` (default), MinimoJS uses the sprite's visual {@link Sprite.height}.
306
+ * When set, this value is scaled by {@link Sprite.scale} the same way as the
307
+ * visual sprite size.
308
+ */
309
+ bodyHeight: number | null;
310
+ /**
311
+ * Horizontal body offset, in local sprite pixels before scale is applied.
312
+ *
313
+ * Positive values move the collision body to the right of the sprite's rendered center.
314
+ * Negative values move it to the left.
315
+ */
316
+ bodyOffsetX: number;
317
+ /**
318
+ * Vertical body offset, in local sprite pixels before scale is applied.
319
+ *
320
+ * Positive values move the collision body downward relative to the sprite's
321
+ * rendered center. Negative values move it upward.
322
+ */
323
+ bodyOffsetY: number;
324
+ /**
325
+ * Effective collision-body width in pixels after applying {@link Sprite.scale}.
73
326
  */
74
- size: number;
327
+ get bodyDisplayWidth(): number;
328
+ /**
329
+ * Effective collision-body height in pixels after applying {@link Sprite.scale}.
330
+ */
331
+ get bodyDisplayHeight(): number;
332
+ /**
333
+ * Resolved body center X used internally by physics helpers and collision checks.
334
+ */
335
+ get bodyCenterX(): number;
336
+ /**
337
+ * Resolved body center Y used internally by physics helpers and collision checks.
338
+ */
339
+ get bodyCenterY(): number;
75
340
  /**
76
341
  * CSS text color used when rendering this sprite.
77
342
  *
@@ -80,6 +345,19 @@ export declare class Sprite {
80
345
  * depending on browser behavior.
81
346
  */
82
347
  color: string;
348
+ /**
349
+ * Physics body type flag.
350
+ *
351
+ * - `false` (default): the sprite is dynamic and can be moved by velocity,
352
+ * gravity, and explicit collision resolution.
353
+ * - `true`: the sprite is static and is not moved by the engine's built-in
354
+ * velocity/gravity integration. Static sprites act as stable obstacles for
355
+ * simple platform collisions.
356
+ *
357
+ * Static sprites can still be repositioned manually by setting `x` and `y`
358
+ * directly in your own game code.
359
+ */
360
+ isStatic: boolean;
83
361
  /**
84
362
  * Visual rotation of the sprite in degrees.
85
363
  * `0` = upright. Positive values rotate clockwise.
@@ -145,24 +423,415 @@ export declare class Sprite {
145
423
  */
146
424
  gravityScale: number;
147
425
  /**
148
- * Creates a new Sprite with the given emoji and optional position.
426
+ * Optional name, so code can ask an object what it is. Names may repeat,
427
+ * like a `name` attribute in the DOM. The engine never reads it.
428
+ * A sprite created from a design file takes the object's `name`.
429
+ *
430
+ * @example
431
+ * ```ts
432
+ * if (other.name === "door") openDoor();
433
+ * ```
434
+ */
435
+ name: string | undefined;
436
+ /**
437
+ * Tags for grouping, like CSS classes in the DOM. Empty by default. The
438
+ * engine never reads them. A sprite created from a design file takes the
439
+ * object's `tags`, which {@link Game.registerBuilderByTag} also matches.
440
+ *
441
+ * @example
442
+ * ```ts
443
+ * if (other.tags.includes("coin")) collect(other);
444
+ * ```
445
+ */
446
+ tags: string[];
447
+ /**
448
+ * The game's own values for this object, such as a button's action or an
449
+ * enemy's speed. An empty object by default, so `sprite.data.hp = 3` needs
450
+ * no check. The engine never reads it. A sprite created from a design file
451
+ * takes a copy of the object's `data`.
452
+ */
453
+ data: Record<string, unknown>;
454
+ /**
455
+ * Stable cache key describing the sprite's rendered appearance.
456
+ *
457
+ * The renderer uses this to reuse prerendered canvases across frames.
458
+ */
459
+ abstract getRenderCacheKey(): string;
460
+ protected getAnchorOffsetX(): number;
461
+ protected getAnchorOffsetY(): number;
462
+ }
463
+ /**
464
+ * A 2D game object rendered as an emoji on the canvas.
465
+ *
466
+ * Instantiate directly or extend to create custom sprite types.
467
+ * Register with the engine by passing the instance to {@link Game.add}.
468
+ *
469
+ * **Coordinate system:** center-based world space. `(x, y)` is the center of
470
+ * the sprite. Positive X = right, positive Y = down.
471
+ *
472
+ * **Lifecycle:** A sprite exists until {@link Game.destroySprite} is called or
473
+ * {@link Game.reset} is invoked. After destruction, do not read or write its fields.
474
+ *
475
+ * @example
476
+ * ```ts
477
+ * const coin = new EmojiSprite("🪙", 300, 200, 32);
478
+ * game.add(coin);
479
+ * ```
480
+ */
481
+ export declare class EmojiSprite extends Sprite {
482
+ /**
483
+ * The emoji character used to render this sprite.
484
+ * Must be a single emoji. Change this at runtime to animate between frames.
485
+ * @example "🔥", "⭐", "🐢"
486
+ */
487
+ sprite: string;
488
+ get size(): number;
489
+ get width(): number;
490
+ get height(): number;
491
+ get displaySize(): number;
492
+ /**
493
+ * Creates a new EmojiSprite with the given emoji, optional position, and base size.
149
494
  * All other properties use their defaults and can be set after construction.
150
495
  *
151
496
  * @param sprite - The emoji character to render. Must be a single emoji.
152
- * Image sprites are NOT supported — emoji only.
497
+ * Use {@link ImageSprite} for preloaded bitmap textures.
153
498
  * @example "🔥", "⭐", "🐢", "💣", "👾"
154
499
  * @param x - Initial X position in world space (center), in pixels. Default: `0`.
155
500
  * @param y - Initial Y position in world space (center), in pixels. Default: `0`.
501
+ * @param size - Base sprite size in pixels. Default: `32`.
156
502
  *
157
503
  * @example
158
504
  * ```ts
159
- * const enemy = new Sprite("👾", 200, 100);
160
- * enemy.size = 40;
505
+ * const enemy = new EmojiSprite("👾", 200, 100, 40);
161
506
  * game.add(enemy);
162
507
  * ```
163
508
  */
164
- constructor(sprite: string, x?: number, y?: number);
509
+ constructor(sprite: string, x?: number, y?: number, size?: number);
510
+ getRenderCacheKey(): string;
511
+ }
512
+ /**
513
+ * A renderable sprite backed by a preloaded image asset.
514
+ *
515
+ * Width and height are always resolved from the current texture. To resize the
516
+ * sprite visually, use {@link Sprite.scale}.
517
+ */
518
+ export declare class ImageSprite extends Sprite {
519
+ get imageKey(): string;
520
+ get width(): number;
521
+ get height(): number;
522
+ /**
523
+ * Creates a new image-backed sprite.
524
+ *
525
+ * @param game - Game instance used to resolve the texture key.
526
+ * @param imageKey - Texture key previously registered with {@link Game.loadImage} or {@link Game.createTexture}.
527
+ * @param x - Initial X position in world space (center), in pixels. Default: `0`.
528
+ * @param y - Initial Y position in world space (center), in pixels. Default: `0`.
529
+ */
530
+ constructor(game: Game, imageKey: string, x?: number, y?: number);
531
+ /**
532
+ * Replaces the current texture with another preloaded image.
533
+ *
534
+ * @param imageKey - New texture key to render.
535
+ */
536
+ setTexture(imageKey: string): void;
537
+ getRenderCacheKey(): string;
538
+ }
539
+ /**
540
+ * A renderable sprite backed by a per-instance canvas that is repainted on demand.
541
+ *
542
+ * `DrawSprite` is useful for procedural shapes, gauges, charts, minimaps, and
543
+ * HUD widgets whose appearance changes often and is easier to express with
544
+ * Canvas 2D drawing commands than with emoji, text, or image swaps.
545
+ *
546
+ * Treat `DrawSprite` as a specialized tool, not the default sprite type.
547
+ * Because it may execute custom Canvas 2D drawing code repeatedly, overusing it
548
+ * can affect game performance much more than regular {@link EmojiSprite},
549
+ * {@link ImageSprite}, or {@link TextSprite} instances.
550
+ *
551
+ * Prefer the other sprite types whenever they can express the same result more
552
+ * simply. Reach for `DrawSprite` only when you truly need procedural drawing or
553
+ * custom per-sprite canvas rendering that the built-in sprite types cannot
554
+ * provide cleanly.
555
+ *
556
+ * MinimoJS creates and owns an internal canvas for each `DrawSprite` instance.
557
+ * Before every engine render that needs this sprite's surface, the engine:
558
+ *
559
+ * 1. Resolves the sprite's internal canvas size
560
+ * 2. Optionally clears and repaints that internal canvas
561
+ * 3. Draws the resulting canvas like any other sprite surface
562
+ *
563
+ * By default, `DrawSprite` is live-rendered: the engine clears the internal
564
+ * canvas and calls {@link DrawSprite.redraw} every frame.
565
+ *
566
+ * Set {@link DrawSprite.frozen} to `true` if you want to keep and reuse the
567
+ * last rendered canvas contents without repainting on each frame. This is
568
+ * useful for shapes or procedural art that you only want to draw once.
569
+ *
570
+ * While `frozen` is `true`, MinimoJS reuses the existing surface exactly as-is:
571
+ * it does not clear the canvas and does not call {@link DrawSprite.redraw}
572
+ * again, unless the surface does not exist yet or its size had to be rebuilt.
573
+ *
574
+ * This means freezing is a rendering optimization and content-preservation
575
+ * flag, not a separate caching system. You can switch `frozen` on or off at
576
+ * runtime whenever it makes sense for your sprite.
577
+ *
578
+ * The local drawing coordinate system uses the sprite surface itself:
579
+ * - `(0, 0)` is the top-left corner of the internal canvas
580
+ * - `width` / `height` match the sprite's logical size before `scale`
581
+ * - draw centered content yourself if you want the visual origin in the middle
582
+ *
583
+ * Override {@link DrawSprite.redraw} in a subclass, or assign your own method
584
+ * on an instance if you prefer an inline style in JavaScript.
585
+ */
586
+ export declare class DrawSprite extends Sprite {
587
+ /**
588
+ * When `false` (default), MinimoJS clears this sprite's internal canvas and
589
+ * calls {@link DrawSprite.redraw} on every render pass.
590
+ *
591
+ * When `true`, MinimoJS keeps and reuses the existing canvas contents without
592
+ * clearing or repainting them again, unless the internal surface does not yet
593
+ * exist or had to be resized.
594
+ *
595
+ * Use this when your procedural drawing becomes static after its first paint,
596
+ * or when you want explicit manual control over when the sprite is redrawn by
597
+ * toggling `frozen` at runtime.
598
+ */
599
+ frozen: boolean;
600
+ get width(): number;
601
+ get height(): number;
602
+ /**
603
+ * Creates a new dynamic canvas-backed sprite.
604
+ *
605
+ * @param width - Internal canvas width in pixels. Minimum `1`.
606
+ * @param height - Internal canvas height in pixels. Minimum `1`.
607
+ * @param x - Initial X position in world space (center), in pixels. Default: `0`.
608
+ * @param y - Initial Y position in world space (center), in pixels. Default: `0`.
609
+ */
610
+ constructor(width: number, height: number, x?: number, y?: number);
611
+ /**
612
+ * Called by the engine whenever the sprite's internal surface must be repainted.
613
+ *
614
+ * The provided context is already cleared and reset to the default 2D canvas
615
+ * state for the current surface size. MinimoJS repaints each `DrawSprite` at
616
+ * most once per render pass while it is not {@link DrawSprite.frozen}, so
617
+ * repeated snapshot reads during the same frame reuse the already-redrawn
618
+ * surface.
619
+ *
620
+ * @param ctx - The sprite's internal 2D drawing context.
621
+ */
622
+ redraw(_ctx: CanvasRenderingContext2D): void;
623
+ getRenderCacheKey(): string;
165
624
  }
625
+ /**
626
+ * A renderable text actor that participates in the same animation/effects
627
+ * pipeline as regular sprites.
628
+ *
629
+ * Use `TextSprite` when the content is primarily text, or when you need text
630
+ * layout features such as wrapping, fixed button sizes, background/border, or
631
+ * text stroke.
632
+ *
633
+ * For controls that are only a single emoji, prefer {@link EmojiSprite}. Emoji-only
634
+ * buttons usually behave better as sprites because they do not need text
635
+ * padding/layout and their bounds match the rendered emoji more directly.
636
+ */
637
+ export declare class TextSprite extends Sprite {
638
+ anchorX: "left" | "center" | "right";
639
+ anchorY: "top" | "middle" | "bottom";
640
+ text: string;
641
+ /**
642
+ * CSS font family. Default: `monospace`, which needs no download. Name a web
643
+ * font here, and register it with {@link Game.requireFont}, to use one.
644
+ */
645
+ fontFamily: string;
646
+ fontSize: number;
647
+ fontWeight: string;
648
+ lineHeight: number;
649
+ maxWidth: number;
650
+ fixedWidth: number;
651
+ fixedHeight: number;
652
+ paddingX: number;
653
+ paddingY: number;
654
+ backgroundColor: string | null;
655
+ borderColor: string | null;
656
+ borderWidth: number;
657
+ cornerRadius: number;
658
+ strokeColor: string | null;
659
+ strokeWidth: number;
660
+ textAlign: CanvasTextAlign;
661
+ get width(): number;
662
+ get height(): number;
663
+ constructor(text: string, x?: number, y?: number, fontSizeOrConfig?: number | TextSpriteConfig, config?: TextSpriteConfig);
664
+ getRenderCacheKey(): string;
665
+ get align(): CanvasTextAlign;
666
+ set align(value: CanvasTextAlign);
667
+ protected getAnchorOffsetX(): number;
668
+ protected getAnchorOffsetY(): number;
669
+ }
670
+ export type TextSpriteConfig = {
671
+ anchorX?: "left" | "center" | "right";
672
+ anchorY?: "top" | "middle" | "bottom";
673
+ fontFamily?: string;
674
+ fontSize?: number;
675
+ fontWeight?: string;
676
+ lineHeight?: number;
677
+ maxWidth?: number;
678
+ fixedWidth?: number;
679
+ fixedHeight?: number;
680
+ paddingX?: number;
681
+ paddingY?: number;
682
+ backgroundColor?: string | null;
683
+ borderColor?: string | null;
684
+ borderWidth?: number;
685
+ cornerRadius?: number;
686
+ strokeColor?: string | null;
687
+ strokeWidth?: number;
688
+ textAlign?: CanvasTextAlign;
689
+ align?: CanvasTextAlign;
690
+ };
691
+ export type FontRequirementOptions = {
692
+ weight?: string;
693
+ style?: "normal" | "italic" | "oblique";
694
+ size?: number;
695
+ sampleText?: string;
696
+ };
697
+ /**
698
+ * A static image-based background layer rendered behind all sprites.
699
+ *
700
+ * Background layers are visual-only: they do not participate in physics,
701
+ * collisions, input hit-testing, or sprite queries.
702
+ *
703
+ * `x` / `y` use **top-left screen-space coordinates**, unlike {@link Sprite},
704
+ * which uses center-based world space.
705
+ *
706
+ * The texture referenced by {@link BackgroundLayer.imageKey} must be registered
707
+ * with {@link Game.loadImage} during {@link Game.onPreload}, or created
708
+ * dynamically with {@link Game.createTexture}.
709
+ *
710
+ * @example
711
+ * ```ts
712
+ * game.onPreload = () => {
713
+ * game.loadImage("sky", "assets/sky.png");
714
+ * };
715
+ *
716
+ * game.onCreate = () => {
717
+ * const bg = game.addBackground(new BackgroundLayer("sky"));
718
+ * bg.fit = "cover";
719
+ * };
720
+ * ```
721
+ */
722
+ export declare class BackgroundLayer {
723
+ /**
724
+ * Key of the preloaded image to render.
725
+ */
726
+ imageKey: string;
727
+ /**
728
+ * Left edge of the destination rectangle in screen space, in pixels.
729
+ */
730
+ x: number;
731
+ /**
732
+ * Top edge of the destination rectangle in screen space, in pixels.
733
+ */
734
+ y: number;
735
+ /**
736
+ * Width of the destination rectangle in pixels.
737
+ * If `0` (default), the canvas width is used.
738
+ */
739
+ width: number;
740
+ /**
741
+ * Height of the destination rectangle in pixels.
742
+ * If `0` (default), the canvas height is used.
743
+ */
744
+ height: number;
745
+ /**
746
+ * Opacity of the layer in range `[0, 1]`.
747
+ */
748
+ alpha: number;
749
+ /**
750
+ * Controls whether the layer is drawn.
751
+ */
752
+ visible: boolean;
753
+ /**
754
+ * Orders background layers relative to each other.
755
+ * Higher values render on top of lower background layers, but still behind sprites.
756
+ */
757
+ layer: number;
758
+ /**
759
+ * How the image is fit inside the destination rectangle.
760
+ * Default: `"cover"`.
761
+ */
762
+ fit: BackgroundFit;
763
+ /**
764
+ * Optional name, so code can ask a layer what it is. Names may repeat. The
765
+ * engine never reads it. See {@link Sprite.name}.
766
+ */
767
+ name: string | undefined;
768
+ /**
769
+ * Tags for grouping. Empty by default. The engine never reads them. See
770
+ * {@link Sprite.tags}.
771
+ */
772
+ tags: string[];
773
+ /**
774
+ * The game's own values for this layer. An empty object by default. The
775
+ * engine never reads it. See {@link Sprite.data}.
776
+ */
777
+ data: Record<string, unknown>;
778
+ /**
779
+ * Creates a new background layer.
780
+ *
781
+ * @param imageKey - Preloaded image key previously registered with {@link Game.loadImage}.
782
+ * @param x - Destination rectangle left edge in screen space. Default: `0`.
783
+ * @param y - Destination rectangle top edge in screen space. Default: `0`.
784
+ * @param width - Destination rectangle width. `0` means canvas width. Default: `0`.
785
+ * @param height - Destination rectangle height. `0` means canvas height. Default: `0`.
786
+ */
787
+ constructor(imageKey: string, x?: number, y?: number, width?: number, height?: number);
788
+ }
789
+ /**
790
+ * The objects {@link Game.createObjectList} created, in the order of their
791
+ * data, and searches on them.
792
+ *
793
+ * An `ObjectList` only holds and finds its objects; it never changes them.
794
+ * Remove them through the game, as any other object:
795
+ *
796
+ * ```ts
797
+ * for (const obj of ui.objects) {
798
+ * if (obj instanceof BackgroundLayer) game.destroyBackgroundLayer(obj);
799
+ * else game.destroySprite(obj);
800
+ * }
801
+ * ```
802
+ *
803
+ * Searches read each object's current {@link Sprite.name} and
804
+ * {@link Sprite.tags}. In TypeScript, name the type you expect:
805
+ * `ui.findByName<ImageSprite>("play")`.
806
+ */
807
+ export declare class ObjectList {
808
+ /** Every object created, in the order of the data. */
809
+ readonly objects: readonly (Sprite | BackgroundLayer)[];
810
+ /**
811
+ * Returns the first object whose `name` is `name`, in order.
812
+ * Throws an error naming `name` when there is none.
813
+ *
814
+ * @example
815
+ * ```ts
816
+ * const play = ui.findByName("play");
817
+ * ```
818
+ */
819
+ findByName<T extends Sprite | BackgroundLayer = Sprite | BackgroundLayer>(name: string): T;
820
+ /** Returns every object whose `name` is `name`, in order; an empty list when there is none. */
821
+ findAllByName<T extends Sprite | BackgroundLayer = Sprite | BackgroundLayer>(name: string): T[];
822
+ /**
823
+ * Returns the first object whose `tags` include `tag`, in order.
824
+ * Throws an error naming `tag` when there is none.
825
+ */
826
+ findByTag<T extends Sprite | BackgroundLayer = Sprite | BackgroundLayer>(tag: string): T;
827
+ /** Returns every object whose `tags` include `tag`, in order; an empty list when there is none. */
828
+ findAllByTag<T extends Sprite | BackgroundLayer = Sprite | BackgroundLayer>(tag: string): T[];
829
+ }
830
+ /**
831
+ * Builds the object for a design object whose tags include the tag it was
832
+ * registered for. See {@link Game.registerBuilderByTag}.
833
+ */
834
+ export type ObjectBuilder = (obj: DesignObject, game: Game) => Sprite | BackgroundLayer;
166
835
  /**
167
836
  * Snapshot of an active pointer tracked by the engine.
168
837
  *
@@ -189,6 +858,36 @@ export interface PointerInfo {
189
858
  */
190
859
  pressed: boolean;
191
860
  }
861
+ /**
862
+ * Information about a resolved AABB collision.
863
+ *
864
+ * Flags are reported relative to the **first** sprite passed to
865
+ * {@link Game.collide}. For example, `bottom = true` means the bottom side of
866
+ * the first sprite contacted the top side of the second sprite.
867
+ */
868
+ export interface CollisionInfo {
869
+ /**
870
+ * `true` when the first sprite's left side hit the second sprite.
871
+ */
872
+ left: boolean;
873
+ /**
874
+ * `true` when the first sprite's right side hit the second sprite.
875
+ */
876
+ right: boolean;
877
+ /**
878
+ * `true` when the first sprite's top side hit the second sprite from below.
879
+ */
880
+ top: boolean;
881
+ /**
882
+ * `true` when the first sprite's bottom side landed on the second sprite.
883
+ */
884
+ bottom: boolean;
885
+ /**
886
+ * Convenience flag for platformers.
887
+ * Equivalent to `bottom = true`.
888
+ */
889
+ grounded: boolean;
890
+ }
192
891
  /**
193
892
  * # MinimoJS v1 — AI Agent Integration Guide
194
893
  *
@@ -198,18 +897,88 @@ export interface PointerInfo {
198
897
  *
199
898
  * ---
200
899
  *
900
+ * ## ES Module Only
901
+ *
902
+ * MinimoJS is an **ES module only** package.
903
+ * You must import it with standard ESM syntax such as:
904
+ *
905
+ * ```ts
906
+ * import { Game, EmojiSprite } from "minimojs";
907
+ * ```
908
+ *
909
+ * In the browser, use it from a module script:
910
+ *
911
+ * ```html
912
+ * <script type="module">
913
+ * import { Game, EmojiSprite } from "./dist/minimo.js";
914
+ * </script>
915
+ * ```
916
+ *
917
+ * Or from a CDN:
918
+ *
919
+ * ```ts
920
+ * import { Game, EmojiSprite } from "https://cdn.jsdelivr.net/npm/minimojs@<version>/dist/minimo.js";
921
+ * ```
922
+ *
923
+ * Do NOT use a classic `<script>` tag without `type="module"`.
924
+ * Do NOT expect a global `window.MinimoJS`.
925
+ * Do NOT use `require("minimojs")` (CommonJS is not supported).
926
+ *
927
+ * ---
928
+ *
929
+ * ## Font Setup
930
+ *
931
+ * `drawText()` and `TextSprite` default to `monospace`, which every device
932
+ * has, so a game that names no font downloads none and waits for none. To use
933
+ * a web font, declare it in your page, register it with {@link Game.requireFont}
934
+ * before calling {@link Game.start}, and name it in {@link TextSprite.fontFamily}.
935
+ *
936
+ * Prefer {@link TextSprite} for persistent UI text, interactive labels,
937
+ * buttons, fixed-size text boxes, and styled text elements. Keep
938
+ * `drawText()` for simple screen-space overlays such as HUD counters, debug
939
+ * text, or other text that is redrawn every frame.
940
+ *
941
+ * You MUST still declare the fonts yourself in your `index.html`. MinimoJS
942
+ * does NOT download, inject, or manage web fonts for you. If a font is missing,
943
+ * the browser falls back to `monospace`.
944
+ *
945
+ * Example `index.html` `<head>` setup:
946
+ *
947
+ * ```html
948
+ * <link rel="preconnect" href="https://fonts.googleapis.com" />
949
+ * <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
950
+ * <link
951
+ * href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap"
952
+ * rel="stylesheet"
953
+ * />
954
+ * ```
955
+ *
956
+ * Example custom font registration:
957
+ *
958
+ * ```ts
959
+ * const game = new Game(720, 1280);
960
+ * game.requireFont('"Bangers"', { weight: "400" });
961
+ * game.start();
962
+ * ```
963
+ *
964
+ * ---
965
+ *
201
966
  * ## Quick Start
202
967
  *
203
968
  * ```ts
204
- * import { Game, Sprite } from "https://cdn.jsdelivr.net/npm/minimojs@<version>/dist/minimo.js";
969
+ * import { Game, ImageSprite } from "https://cdn.jsdelivr.net/npm/minimojs@<version>/dist/minimo.js";
205
970
  *
206
- * const game = new Game(800, 600);
971
+ * const game = new Game(720, 1280);
207
972
  *
208
- * const player = new Sprite("🐢");
209
- * player.x = 400;
210
- * player.y = 500;
211
- * player.size = 48;
212
- * game.add(player);
973
+ * game.onPreload = () => {
974
+ * game.loadImage("player", "assets/player.png");
975
+ * };
976
+ *
977
+ * let player: ImageSprite;
978
+ *
979
+ * game.onCreate = () => {
980
+ * player = game.add(new ImageSprite(game, "player", 400, 500));
981
+ * };
213
982
  *
214
983
  * game.onUpdate = (dt) => {
215
984
  * if (game.isKeyDown("ArrowLeft")) player.vx = -200;
@@ -222,6 +991,13 @@ export interface PointerInfo {
222
991
  * game.start();
223
992
  * ```
224
993
  *
994
+ * Prototyping, and have no art yet? Swap the texture for an emoji and drop the
995
+ * preload entirely — everything else stays the same:
996
+ *
997
+ * ```ts
998
+ * const player = game.add(new EmojiSprite("🐢", 400, 500, 48));
999
+ * ```
1000
+ *
225
1001
  * ---
226
1002
  *
227
1003
  * ## Engine Philosophy
@@ -235,7 +1011,8 @@ export interface PointerInfo {
235
1011
  * ## Flat API
236
1012
  *
237
1013
  * ALL engine functionality is on the `game` object.
238
- * Do not look for sub-objects like `game.physics`, `game.input`, etc. They do not exist.
1014
+ * Do not look for nested subsystem objects like `game.input.keyboard`,
1015
+ * `game.physics.world`, etc. They do not exist in MinimoJS v1.
239
1016
  *
240
1017
  * ---
241
1018
  *
@@ -246,13 +1023,28 @@ export interface PointerInfo {
246
1023
  *
247
1024
  * ---
248
1025
  *
249
- * ## Emoji-Only Sprites
250
- *
251
- * Every sprite MUST use a single emoji character as its visual representation.
252
- * PNG, SVG, spritesheet, and image sprites are NOT supported.
253
- * Use Unicode emoji: `"🔥"`, `"⭐"`, `"💣"`, `"🐢"`, `"👾"`, `"🧱"`, etc.
254
- * AI agents can also use text-like emojis (regional indicators, symbols, letters)
255
- * to build fun title art, HUD labels, and expressive in-game text.
1026
+ * ## Choosing a Visual Source
1027
+ *
1028
+ * A sprite's look comes from one of four concrete types, all of which extend the
1029
+ * shared abstract {@link Sprite} type. Use `Sprite` as the type in your own
1030
+ * signatures; never construct it directly.
1031
+ *
1032
+ * 1. {@link ImageSprite} — the game's own images, and the first option to reach
1033
+ * for. Register a texture with {@link Game.loadImage} inside
1034
+ * {@link Game.onPreload}, or build one at runtime with
1035
+ * {@link Game.createTexture}, then render it with
1036
+ * `new ImageSprite(game, key, x, y)`. An image looks identical on every
1037
+ * device and belongs to the game.
1038
+ * 2. {@link EmojiSprite} — a single Unicode emoji, and the shortest path to a
1039
+ * first prototype: `new EmojiSprite("🐢", x, y, size)`. Note that an
1040
+ * emoji is drawn with the player's own system emoji font, so the same game
1041
+ * looks different across iOS, Android, and Windows, and a glyph the platform
1042
+ * lacks renders as an empty box. Prefer an image whenever the look matters.
1043
+ * 3. {@link TextSprite} — text, labels, and buttons.
1044
+ * 4. {@link DrawSprite} — procedural Canvas 2D drawing, for shapes and gauges
1045
+ * the other three cannot express cleanly.
1046
+ *
1047
+ * Emoji and images mix freely in one game.
256
1048
  *
257
1049
  * ---
258
1050
  *
@@ -268,7 +1060,7 @@ export interface PointerInfo {
268
1060
  *
269
1061
  * ALL time parameters to engine methods are in **milliseconds (ms)**.
270
1062
  * This includes: {@link Game.addTimer}, {@link Game.animateAlpha},
271
- * {@link Game.animateRotation}, and {@link Game.sound}.
1063
+ * {@link Game.animateRotation}, {@link Game.animateDeform}, and {@link Game.sound}.
272
1064
  *
273
1065
  * The `dt` parameter in {@link Game.onUpdate} is an exception — it is in
274
1066
  * **seconds** for convenient velocity math (`position += velocity * dt`).
@@ -320,10 +1112,10 @@ export interface PointerInfo {
320
1112
  *
321
1113
  * ---
322
1114
  *
323
- * ## Scene Initialization with `onCreate`
1115
+ * ## Scene Initialization with `IScene`
324
1116
  *
325
- * MinimoJS has NO scene system. Use {@link Game.onCreate} to build a scene.
326
- * The engine calls `onCreate`:
1117
+ * MinimoJS supports simple scene objects via {@link IScene}. The engine calls
1118
+ * a scene's `onCreate()`:
327
1119
  * - Once before the first frame (when {@link Game.start} is called).
328
1120
  * - Again after each {@link Game.reset}.
329
1121
  *
@@ -335,29 +1127,42 @@ export interface PointerInfo {
335
1127
  * - Scroll position (scrollX and scrollY reset to 0)
336
1128
  * - Per-frame input state (pressed keys/pointer)
337
1129
  *
338
- * After clearing, `reset()` calls `onCreate()` so your callback can rebuild the
339
- * new scene immediately. Simply re-add sprites and re-register timers.
1130
+ * After clearing, `reset()` calls the active scene's `onCreate()` so it can
1131
+ * rebuild immediately. Simply re-add sprites and re-register timers.
340
1132
  *
341
1133
  * ```ts
342
- * game.onCreate = () => {
343
- * // scene init: add sprites, setup timers
344
- * const skull = new Sprite("💀");
345
- * skull.x = 400; skull.y = 300; skull.size = 96;
346
- * game.add(skull);
347
- * };
1134
+ * class SkullScene implements IScene {
1135
+ * onCreate() {
1136
+ * const skull = new EmojiSprite("💀", 400, 300, 96);
1137
+ * game.add(skull);
1138
+ * }
348
1139
  *
349
- * game.onUpdate = (dt) => {
350
- * // normal per-frame update
351
- * };
1140
+ * onUpdate(dt: number) {
1141
+ * // normal per-frame update
1142
+ * }
1143
+ * }
352
1144
  *
353
- * game.start(); // calls onCreate() once before first frame
1145
+ * const scene = new SkullScene();
1146
+ * game.start(scene); // calls onCreate() once before first frame
354
1147
  * // later:
355
1148
  * game.reset(); // clears + calls onCreate() again
356
1149
  * ```
357
1150
  *
1151
+ * Legacy `game.onCreate` / `game.onUpdate` callbacks still work when no active
1152
+ * {@link IScene} is set.
1153
+ *
1154
+ * ### Loading a Scene's Own Images
1155
+ *
1156
+ * Load images in {@link Game.onPreload}: once, at boot. Only a game with many
1157
+ * scenes that each carry many assets of their own — typically a game with many
1158
+ * levels, whose player does not reach every level in a session — should give
1159
+ * scenes an {@link IScene.onPreload}. Entering such a scene loads its images
1160
+ * first, with the scene being left frozen and dimmed under a progress ring, so `reset()`
1161
+ * and `transitionTo()` may return before the new scene's `onCreate()` runs.
1162
+ *
358
1163
  * ---
359
1164
  *
360
- * ## Sprite Lifecycle Ownership
1165
+ * ## EmojiSprite Lifecycle Ownership
361
1166
  *
362
1167
  * The `Game` instance owns all sprites.
363
1168
  * - Create sprites with {@link Game.add}.
@@ -371,7 +1176,7 @@ export interface PointerInfo {
371
1176
  * ## Forbidden Features
372
1177
  *
373
1178
  * The following do NOT exist in MinimoJS v1. Do NOT attempt to use them:
374
- * - Scene system or scene manager
1179
+ * - Full scene manager architecture (scene stacks, transitions, loaders, etc.)
375
1180
  * - Entity Component System (ECS)
376
1181
  * - Physics engine (no Box2D, Matter.js, etc.)
377
1182
  * - Camera zoom or scale
@@ -379,8 +1184,7 @@ export interface PointerInfo {
379
1184
  * - Text input / HTML form elements
380
1185
  * - Parallax layers
381
1186
  * - Multiple cameras
382
- * - Collision resolution (only detection is supported)
383
- * - Image sprites (PNG, SVG, canvas, etc.)
1187
+ * - Full physics engine (only basic explicit AABB collision helpers are provided)
384
1188
  * - `setTimeout` or `setInterval`
385
1189
  */
386
1190
  export declare class Game {
@@ -395,7 +1199,8 @@ export declare class Game {
395
1199
  * game.gravityX = 0; // no horizontal gravity
396
1200
  * ```
397
1201
  */
398
- gravityX: number;
1202
+ get gravityX(): number;
1203
+ set gravityX(value: number);
399
1204
  /**
400
1205
  * Vertical gravity acceleration in pixels per second².
401
1206
  * Applied to every sprite whose {@link Sprite.gravityScale} is non-zero.
@@ -408,7 +1213,23 @@ export declare class Game {
408
1213
  * game.gravityY = 980; // standard downward gravity
409
1214
  * ```
410
1215
  */
411
- gravityY: number;
1216
+ get gravityY(): number;
1217
+ set gravityY(value: number);
1218
+ /**
1219
+ * Enables the basic collision-resolution helpers (`collide` / `collideAny`).
1220
+ *
1221
+ * When `false` (default), overlap detection still works, but collision
1222
+ * resolution helpers are unavailable.
1223
+ *
1224
+ * Set this to `true` for simple platformer-style collision handling.
1225
+ *
1226
+ * @example
1227
+ * ```ts
1228
+ * game.physics = true;
1229
+ * ```
1230
+ */
1231
+ get physics(): boolean;
1232
+ set physics(value: boolean);
412
1233
  /**
413
1234
  * Horizontal scroll offset of the world camera, in pixels.
414
1235
  * The canvas viewport is shifted left by `scrollX` — sprites with higher `x`
@@ -456,6 +1277,20 @@ export declare class Game {
456
1277
  from: string;
457
1278
  to: string;
458
1279
  } | null;
1280
+ /**
1281
+ * When `true`, MinimoJS draws every sprite's collision body as an overlay.
1282
+ *
1283
+ * This is a runtime debugging aid only. It does not change collisions,
1284
+ * input, rendering order, or physics behavior.
1285
+ */
1286
+ debugBodies: boolean;
1287
+ /**
1288
+ * When `true`, MinimoJS draws every sprite's input area as an overlay.
1289
+ *
1290
+ * This is a runtime debugging aid only. It does not change input behavior,
1291
+ * physics, or rendering.
1292
+ */
1293
+ debugInputAreas: boolean;
459
1294
  /**
460
1295
  * Background color for the full web page (`document.body`).
461
1296
  * Set to any valid CSS color string. Default: `null` (engine leaves page background unchanged).
@@ -469,24 +1304,49 @@ export declare class Game {
469
1304
  */
470
1305
  pageBackground: string | null;
471
1306
  /**
472
- * Scene creation callback.
1307
+ * Asset registration callback invoked once before the first scene is created.
1308
+ *
1309
+ * This is the normal place to load a game's images. Use it to register them
1310
+ * with {@link Game.loadAssets} or {@link Game.loadImage}. The callback is synchronous: it should only
1311
+ * queue assets, not await network work.
1312
+ *
1313
+ * After `onPreload` returns, MinimoJS loads the queued images, together with
1314
+ * those of the first scene's {@link IScene.onPreload} if it has one, and
1315
+ * shows a loading line until they are ready. If any image fails, it asks the
1316
+ * player with `window.confirm` whether to retry them; declining leaves the
1317
+ * game on the loading screen and reports the failed images on the console.
1318
+ *
1319
+ * @example
1320
+ * ```ts
1321
+ * game.onPreload = () => {
1322
+ * game.loadImage("sky", "assets/sky.png");
1323
+ * game.loadImage("land", "assets/land.png");
1324
+ * };
1325
+ * ```
1326
+ */
1327
+ onPreload: (() => void) | null;
1328
+ /**
1329
+ * Legacy scene creation callback.
473
1330
  *
474
1331
  * Called once before the first frame on {@link Game.start}, and again after
475
- * each {@link Game.reset}. Use this to create sprites and timers for a scene.
1332
+ * each {@link Game.reset} when no active {@link IScene} is set.
476
1333
  *
477
1334
  * @example
478
1335
  * ```ts
479
1336
  * game.onCreate = () => {
480
- * const player = new Sprite("🐢");
1337
+ * const player = new EmojiSprite("🐢");
481
1338
  * player.x = 200;
482
1339
  * player.y = 300;
483
1340
  * game.add(player);
484
1341
  * };
485
1342
  * ```
486
1343
  */
487
- onCreate: (() => void) | null;
1344
+ get onCreate(): (() => void) | null;
1345
+ set onCreate(callback: (() => void) | null);
488
1346
  /**
489
- * Callback invoked once per frame after physics and timer updates.
1347
+ * Legacy per-frame callback invoked after physics and timer updates.
1348
+ *
1349
+ * This is used only when no active {@link IScene} is set.
490
1350
  *
491
1351
  * @param dt - Delta time in **seconds** since the last frame.
492
1352
  * Use this for velocity-based movement: `sprite.x += speed * dt`.
@@ -494,130 +1354,371 @@ export declare class Game {
494
1354
  *
495
1355
  * @example
496
1356
  * ```ts
497
- * game.onUpdate = (dt) => {
498
- * if (game.isKeyDown("ArrowRight")) player.vx = 200;
499
- * else player.vx = 0;
500
- * };
1357
+ * game.onUpdate = (dt) => {
1358
+ * if (game.isKeyDown("ArrowRight")) player.vx = 200;
1359
+ * else player.vx = 0;
1360
+ * };
1361
+ * ```
1362
+ */
1363
+ onUpdate: ((dt: number) => void) | null;
1364
+ /**
1365
+ * Currently active scene object, if any.
1366
+ */
1367
+ get currentScene(): IScene | null;
1368
+ /**
1369
+ * Returns `true` while a full-screen scene transition is playing.
1370
+ */
1371
+ get isTransitioning(): boolean;
1372
+ /**
1373
+ * Returns `true` while images are loading: at boot, or while a scene with
1374
+ * pending images in its {@link IScene.onPreload} is being entered. While a
1375
+ * scene loads, the scene being left is frozen and `reset()` and
1376
+ * `transitionTo()` are ignored.
1377
+ */
1378
+ get isLoading(): boolean;
1379
+ /**
1380
+ * Creates a new MinimoJS game instance.
1381
+ *
1382
+ * The engine creates its own `<canvas>`, sets its dimensions, and appends it
1383
+ * to `document.body`. The canvas is automatically centered and responsively
1384
+ * scaled to use the maximum available viewport space while preserving aspect ratio.
1385
+ * For new mobile-first Minimo Games, prefer a portrait canvas such as `720x1280`.
1386
+ *
1387
+ * @param width - Canvas width in pixels. Default: `720`.
1388
+ * @param height - Canvas height in pixels. Default: `1280`.
1389
+ *
1390
+ * @throws Error if a 2D context cannot be obtained.
1391
+ *
1392
+ * @example
1393
+ * ```ts
1394
+ * const game = new Game(720, 1280);
1395
+ * game.physics = true;
1396
+ * ```
1397
+ */
1398
+ constructor(width?: number, height?: number);
1399
+ /**
1400
+ * The width of the canvas in pixels. Read-only — set via constructor.
1401
+ */
1402
+ get width(): number;
1403
+ /**
1404
+ * The height of the canvas in pixels. Read-only — set via constructor.
1405
+ */
1406
+ get height(): number;
1407
+ /**
1408
+ * Current pointer (mouse or touch) X position in **canvas/screen space**,
1409
+ * in pixels. Updated every `mousemove` / `touchmove` event.
1410
+ * `(0, 0)` is the top-left corner of the canvas.
1411
+ * To convert to world space: `worldX = game.pointerX + game.scrollX`.
1412
+ *
1413
+ * On a touch screen the pointer is one finger: the first to touch while no
1414
+ * finger is followed. Other fingers never move it, and when the followed
1415
+ * finger lifts, the pointer stays where it left the screen even if others
1416
+ * are still down. Use {@link Game.getPointers} to follow several fingers.
1417
+ */
1418
+ get pointerX(): number;
1419
+ /**
1420
+ * Current pointer (mouse or touch) Y position in **canvas/screen space**,
1421
+ * in pixels. Updated every `mousemove` / `touchmove` event.
1422
+ * `(0, 0)` is the top-left corner of the canvas.
1423
+ * To convert to world space: `worldY = game.pointerY + game.scrollY`.
1424
+ */
1425
+ get pointerY(): number;
1426
+ /**
1427
+ * Registers a {@link Sprite} (or subclass instance) with the engine.
1428
+ *
1429
+ * After calling `add`, the sprite is rendered and, if dynamic
1430
+ * (`isStatic = false`), receives built-in velocity/gravity integration every
1431
+ * frame until {@link Game.destroySprite} or {@link Game.reset} is called.
1432
+ *
1433
+ * **Ownership:** The game instance takes ownership of the sprite from this
1434
+ * point forward. It will appear in {@link Game.getSprites} on the same frame.
1435
+ *
1436
+ * **Subclasses:** Any class that extends {@link Sprite} can be passed here.
1437
+ * The engine stores and processes it as a live sprite; your custom properties
1438
+ * are preserved on the instance.
1439
+ *
1440
+ * @param sprite - A {@link Sprite} instance (or subclass) to add.
1441
+ * @returns The same sprite instance, for chaining or inline assignment.
1442
+ *
1443
+ * @example
1444
+ * ```ts
1445
+ * // Plain sprite
1446
+ * const coin = new EmojiSprite("🪙", 300, 200, 32);
1447
+ * game.add(coin);
1448
+ *
1449
+ * // Custom subclass
1450
+ * class Enemy extends EmojiSprite {
1451
+ * speed = 150;
1452
+ * constructor(x: number, y: number) {
1453
+ * super("👾", x, y, 40);
1454
+ * }
1455
+ * }
1456
+ * const enemy = game.add(new Enemy(600, 100));
1457
+ * ```
1458
+ */
1459
+ add<T extends Sprite>(sprite: T): T;
1460
+ /**
1461
+ * Registers a {@link BackgroundLayer} with the engine.
1462
+ *
1463
+ * Background layers are rendered behind all sprites and are not affected by
1464
+ * physics, collisions, or sprite queries.
1465
+ *
1466
+ * @param layer - The background layer to add.
1467
+ * @returns The same background layer instance.
1468
+ *
1469
+ * @example
1470
+ * ```ts
1471
+ * const sky = game.addBackground(new BackgroundLayer("sky"));
1472
+ * sky.fit = "cover";
1473
+ * ```
1474
+ */
1475
+ addBackground(layer: BackgroundLayer): BackgroundLayer;
1476
+ /**
1477
+ * Removes a sprite from the engine, stopping its rendering and physics updates.
1478
+ * Also cancels any running animations targeting this sprite.
1479
+ *
1480
+ * **Side effects:** The sprite is removed immediately. Accessing the sprite's
1481
+ * fields after destruction has no effect on the engine, but reading them
1482
+ * returns stale values. Do not keep references to destroyed sprites.
1483
+ *
1484
+ * If `sprite` is not found (already destroyed or never added), this is a no-op.
1485
+ *
1486
+ * @param sprite - The sprite instance to destroy, as passed to {@link Game.add}.
1487
+ *
1488
+ * @example
1489
+ * ```ts
1490
+ * game.destroySprite(enemy); // remove enemy from game
1491
+ * ```
1492
+ */
1493
+ destroySprite(sprite: Sprite): void;
1494
+ /**
1495
+ * Removes a background layer from the engine.
1496
+ *
1497
+ * If the layer is not currently registered, this is a safe no-op.
1498
+ *
1499
+ * @param layer - The background layer to remove.
1500
+ */
1501
+ destroyBackgroundLayer(layer: BackgroundLayer): void;
1502
+ /**
1503
+ * Returns a **read-only snapshot** of all currently active sprites.
1504
+ * The array is a shallow copy — mutating it has no effect on the engine.
1505
+ * Individual sprite objects in the array are the live instances.
1506
+ *
1507
+ * **Deterministic guarantee:** Sprites appear in creation order within each
1508
+ * layer. The order matches the render order (lower layer = earlier in array).
1509
+ *
1510
+ * @returns A read-only array of active {@link Sprite} instances.
1511
+ *
1512
+ * @example
1513
+ * ```ts
1514
+ * const enemies = game.getSprites().filter(s => s.sprite === "👾");
1515
+ * ```
1516
+ */
1517
+ getSprites(): readonly Sprite[];
1518
+ /**
1519
+ * Returns a read-only snapshot of all active background layers.
1520
+ *
1521
+ * Background layers are returned in creation order within each layer value.
1522
+ */
1523
+ getBackgroundLayers(): readonly BackgroundLayer[];
1524
+ /**
1525
+ * Queues an image to be loaded during {@link Game.onPreload}.
1526
+ *
1527
+ * This method registers the asset only. The actual network/image fetch begins
1528
+ * after `onPreload` returns and is managed automatically by the engine.
1529
+ *
1530
+ * `loadImage` may only be called from within {@link Game.onPreload} or a
1531
+ * scene's {@link IScene.onPreload}.
1532
+ *
1533
+ * @param key - Stable key used later by {@link BackgroundLayer.imageKey} or {@link Game.getImage}.
1534
+ * @param src - Image URL or relative path.
1535
+ *
1536
+ * @example
1537
+ * ```ts
1538
+ * game.onPreload = () => {
1539
+ * game.loadImage("sky", "assets/sky.png");
1540
+ * };
1541
+ * ```
1542
+ */
1543
+ loadImage(key: string, src: string): void;
1544
+ /**
1545
+ * Queues every asset an asset file declares, as one {@link Game.loadImage}
1546
+ * call per `image` entry would.
1547
+ *
1548
+ * Pass the content of a `*.assets.json` file, obtained by JSON import. The
1549
+ * file is checked with {@link parseAssetFile}, and an invalid one throws an
1550
+ * error naming what is wrong. Like `loadImage`, `loadAssets` may only be
1551
+ * called from within {@link Game.onPreload} or a scene's
1552
+ * {@link IScene.onPreload}.
1553
+ *
1554
+ * - Keys are unique in the whole game. A key that another asset file
1555
+ * declares, or that {@link Game.loadImage} or {@link Game.createTexture}
1556
+ * registered, throws an error naming it, and so does a later
1557
+ * `loadImage` or `createTexture` of a key an asset file declares.
1558
+ * - Loading the same file again, as a scene's `onPreload()` does each time
1559
+ * the scene is entered, queues only its images that are not loaded.
1560
+ * - An entry of a type this engine does not support is skipped, with a
1561
+ * console warning naming it, so the game still loads every asset it can.
1562
+ *
1563
+ * @param assets - The content of a `*.assets.json` file. See {@link AssetFile}.
1564
+ *
1565
+ * @example
1566
+ * ```ts
1567
+ * import common from "./common.assets.json" with { type: "json" };
1568
+ *
1569
+ * game.onPreload = () => game.loadAssets(common);
501
1570
  * ```
502
1571
  */
503
- onUpdate: ((dt: number) => void) | null;
1572
+ loadAssets(assets: unknown): void;
504
1573
  /**
505
- * Creates a new MinimoJS game instance.
1574
+ * Creates a dynamic texture immediately and registers it under a stable key.
506
1575
  *
507
- * The engine creates its own `<canvas>`, sets its dimensions, and appends it
508
- * to `document.body`. The canvas is automatically centered and responsively
509
- * scaled to use the maximum available viewport space while preserving aspect ratio.
1576
+ * Unlike {@link Game.loadImage}, this does not require {@link Game.onPreload}.
1577
+ * The `painter` callback receives a fresh offscreen canvas and should draw the
1578
+ * full texture contents into it.
510
1579
  *
511
- * @param width - Canvas width in pixels. Default: `800`.
512
- * @param height - Canvas height in pixels. Default: `600`.
1580
+ * The resulting texture can be used anywhere a normal image key is accepted,
1581
+ * including {@link ImageSprite}, background layers, and optional modules.
513
1582
  *
514
- * @throws Error if a 2D context cannot be obtained.
1583
+ * @param key - Stable texture key used later by sprites and render helpers.
1584
+ * @param width - Texture width in pixels. Minimum `1`.
1585
+ * @param height - Texture height in pixels. Minimum `1`.
1586
+ * @param painter - Function that paints into the offscreen texture canvas.
515
1587
  *
516
1588
  * @example
517
1589
  * ```ts
518
- * const game = new Game(800, 600);
1590
+ * game.createTexture("checkpoint", 128, 64, (ctx, canvas) => {
1591
+ * ctx.fillStyle = "#101820";
1592
+ * ctx.fillRect(0, 0, canvas.width, canvas.height);
1593
+ * ctx.fillStyle = "#ffd54f";
1594
+ * ctx.fillRect(8, 8, canvas.width - 16, canvas.height - 16);
1595
+ * });
519
1596
  * ```
520
1597
  */
521
- constructor(width?: number, height?: number);
522
- /**
523
- * The width of the canvas in pixels. Read-only — set via constructor.
524
- */
525
- get width(): number;
526
- /**
527
- * The height of the canvas in pixels. Read-only — set via constructor.
528
- */
529
- get height(): number;
1598
+ createTexture(key: string, width: number, height: number, painter: (ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement) => void): HTMLCanvasElement;
530
1599
  /**
531
- * Current pointer (mouse or touch) X position in **canvas/screen space**,
532
- * in pixels. Updated every `mousemove` / `touchmove` event.
533
- * `(0, 0)` is the top-left corner of the canvas.
534
- * To convert to world space: `worldX = game.pointerX + game.scrollX`.
1600
+ * Returns a loaded image or created texture previously registered with
1601
+ * {@link Game.loadImage} or {@link Game.createTexture}, or `undefined` if it
1602
+ * is not available.
535
1603
  */
536
- get pointerX(): number;
1604
+ getImage(key: string): HTMLImageElement | HTMLCanvasElement | undefined;
537
1605
  /**
538
- * Current pointer (mouse or touch) Y position in **canvas/screen space**,
539
- * in pixels. Updated every `mousemove` / `touchmove` event.
540
- * `(0, 0)` is the top-left corner of the canvas.
541
- * To convert to world space: `worldY = game.pointerY + game.scrollY`.
1606
+ * Returns `true` if a texture key is available.
542
1607
  */
543
- get pointerY(): number;
1608
+ hasImage(key: string): boolean;
544
1609
  /**
545
- * Registers a {@link Sprite} (or subclass instance) with the engine.
546
- * After calling `add`, the sprite is rendered and receives physics updates
547
- * every frame until {@link Game.destroySprite} or {@link Game.reset} is called.
1610
+ * Creates one runtime object from object data, such as a template found in
1611
+ * a {@link Design}, and adds it to the game.
548
1612
  *
549
- * **Ownership:** The game instance takes ownership of the sprite from this
550
- * point forward. It will appear in {@link Game.getSprites} on the same frame.
1613
+ * The object is built through the same public constructors and
1614
+ * {@link Game.add} or {@link Game.addBackground} that code uses, so it
1615
+ * behaves as the equivalent code. Its type picks its class: `image` builds
1616
+ * an {@link ImageSprite}, `emoji` an {@link EmojiSprite}, `text` a
1617
+ * {@link TextSprite}, and `background` a {@link BackgroundLayer}, unless a
1618
+ * builder registered with {@link Game.registerBuilderByTag} matches one of
1619
+ * its tags. Then every property the data holds is applied, including
1620
+ * `name`, `tags`, and a copy of `data`.
551
1621
  *
552
- * **Subclasses:** Any class that extends {@link Sprite} can be passed here.
553
- * The engine stores and processes it as a `Sprite`; your custom properties
554
- * are preserved on the instance.
1622
+ * Each call creates a new object, so one template can be created any number
1623
+ * of times. Set its position afterwards to place it; the object is never
1624
+ * drawn at the template's position.
555
1625
  *
556
- * @param sprite - A {@link Sprite} instance (or subclass) to add.
557
- * @returns The same sprite instance, for chaining or inline assignment.
1626
+ * Throws an error naming what is wrong when the data is not a valid design
1627
+ * object, or naming the key when it uses an image key that is not loaded.
1628
+ *
1629
+ * @param data - One design object, such as `design.findByName("player")`. Not a `Design`.
1630
+ * @param options - `add: false` creates the object without adding it to the game. Default: added.
1631
+ * @returns The created object. In TypeScript, name its type: `game.createObject<ImageSprite>(data)`.
558
1632
  *
559
1633
  * @example
560
1634
  * ```ts
561
- * // Plain sprite
562
- * const coin = new Sprite("🪙");
563
- * coin.x = 300; coin.y = 200; coin.size = 32;
564
- * game.add(coin);
1635
+ * import charactersJson from "./characters.design.json" with { type: "json" };
1636
+ * const characters = new Design(charactersJson);
565
1637
  *
566
- * // Custom subclass
567
- * class Enemy extends Sprite {
568
- * speed = 150;
569
- * constructor(x: number, y: number) {
570
- * super("👾");
571
- * this.x = x; this.y = y; this.size = 40;
572
- * }
573
- * }
574
- * const enemy = game.add(new Enemy(600, 100));
1638
+ * const bat = game.createObject(characters.findByName("bat"));
1639
+ * bat.x = spawn.x;
1640
+ * bat.y = spawn.y;
575
1641
  * ```
576
1642
  */
577
- add(sprite: Sprite): Sprite;
1643
+ createObject<T extends Sprite | BackgroundLayer = Sprite | BackgroundLayer>(data: DesignObject, options?: {
1644
+ add?: boolean;
1645
+ }): T;
578
1646
  /**
579
- * Removes a sprite from the engine, stopping its rendering and physics updates.
580
- * Also cancels any running animations targeting this sprite.
581
- *
582
- * **Side effects:** The sprite is removed immediately. Accessing the sprite's
583
- * fields after destruction has no effect on the engine, but reading them
584
- * returns stale values. Do not keep references to destroyed sprites.
1647
+ * Creates a runtime object from each object data, in order, and adds them
1648
+ * to the game, as {@link Game.createObject} does for one. Objects in the
1649
+ * same layer draw in the order of the data.
585
1650
  *
586
- * If `sprite` is not found (already destroyed or never added), this is a no-op.
1651
+ * Every object is checked before any is created, so invalid data or an
1652
+ * image key that is not loaded creates nothing.
587
1653
  *
588
- * @param sprite - The sprite instance to destroy, as passed to {@link Game.add}.
1654
+ * @param objects - Object data, such as `design.objects` or `design.findAllByTag("enemy")`. Not a `Design`.
1655
+ * @param options - `add: false` creates the objects without adding them to the game. Default: added.
1656
+ * @returns An {@link ObjectList} of the created objects, to find them by name or tag.
589
1657
  *
590
1658
  * @example
591
1659
  * ```ts
592
- * game.destroySprite(enemy); // remove enemy from game
1660
+ * import menuJson from "./menu.design.json" with { type: "json" };
1661
+ * const menu = new Design(menuJson);
1662
+ *
1663
+ * class MenuScene {
1664
+ * onCreate() {
1665
+ * const ui = game.createObjectList(menu.objects);
1666
+ * this.play = ui.findByName("play");
1667
+ * }
1668
+ * onUpdate() {
1669
+ * if (game.isPointerPressedOverSprite(this.play)) start(this.play.data.action);
1670
+ * }
1671
+ * }
593
1672
  * ```
594
1673
  */
595
- destroySprite(sprite: Sprite): void;
1674
+ createObjectList(objects: readonly DesignObject[], options?: {
1675
+ add?: boolean;
1676
+ }): ObjectList;
596
1677
  /**
597
- * Returns a **read-only snapshot** of all currently active sprites.
598
- * The array is a shallow copy — mutating it has no effect on the engine.
599
- * Individual sprite objects in the array are the live instances.
600
- *
601
- * **Deterministic guarantee:** Sprites appear in creation order within each
602
- * layer. The order matches the render order (lower layer = earlier in array).
603
- *
604
- * @returns A read-only array of active {@link Sprite} instances.
1678
+ * Makes {@link Game.createObject} and {@link Game.createObjectList} build
1679
+ * the design objects tagged `tag` with `builder`, such as an `Enemy` class
1680
+ * of the game's own.
1681
+ *
1682
+ * - The builder returns an instance of the object's type class or a
1683
+ * subclass of it: {@link ImageSprite} for `image`, {@link EmojiSprite} for
1684
+ * `emoji`, {@link TextSprite} for `text`, and {@link BackgroundLayer} for
1685
+ * `background`. Anything else throws an error naming the object.
1686
+ * - The engine then applies every property the data holds, so the file wins
1687
+ * over the constructor. Properties the data omits keep what the builder set.
1688
+ * An `emoji` object's `size` cannot be changed after construction, so a
1689
+ * builder must pass it on, or the creation throws.
1690
+ * - An object matching the builders of two of its tags throws an error
1691
+ * naming the object and both tags.
1692
+ * - Builders are scene state: {@link Game.reset} clears them, as it clears
1693
+ * timers, so each scene registers its own in `onCreate()`.
1694
+ *
1695
+ * @param tag - The tag whose objects the builder builds.
1696
+ * @param builder - Receives the object's data and the game, and returns the object, not yet added.
605
1697
  *
606
1698
  * @example
607
1699
  * ```ts
608
- * const enemies = game.getSprites().filter(s => s.sprite === "👾");
1700
+ * class Enemy extends ImageSprite {
1701
+ * update(dt) { this.x += this.data.speed * dt; }
1702
+ * }
1703
+ *
1704
+ * class Level1Scene {
1705
+ * onCreate() {
1706
+ * game.registerBuilderByTag("enemy", (obj, game) => new Enemy(game, obj.imageKey));
1707
+ * this.enemies = game.createObjectList(level1.findAllByTag("enemy")).objects;
1708
+ * }
1709
+ * }
609
1710
  * ```
610
1711
  */
611
- getSprites(): readonly Sprite[];
1712
+ registerBuilderByTag(tag: string, builder: ObjectBuilder): void;
612
1713
  /**
613
1714
  * Tests whether two sprites overlap using **Axis-Aligned Bounding Box (AABB)**
614
1715
  * collision detection.
615
1716
  *
616
1717
  * Each sprite's bounding box is a square centered at `(x, y)` with side
617
- * length `size`. Rotation is **ignored** — the box is always axis-aligned.
1718
+ * length `displaySize`. Rotation is **ignored** — the box is always axis-aligned.
618
1719
  *
619
1720
  * **No collision resolution is performed.** This method is detection-only.
620
- * If you need bounce or push-apart behavior, implement it in `onUpdate`.
1721
+ * Use {@link Game.collide} for the engine's basic explicit push-out helper.
621
1722
  *
622
1723
  * @param a - First sprite.
623
1724
  * @param b - Second sprite.
@@ -641,7 +1742,7 @@ export declare class Game {
641
1742
  *
642
1743
  * @param listA - First group of sprites.
643
1744
  * @param listB - Second group of sprites. May share sprites with `listA`.
644
- * @returns A `[Sprite, Sprite]` tuple of the first overlapping pair,
1745
+ * @returns A `[EmojiSprite, EmojiSprite]` tuple of the first overlapping pair,
645
1746
  * or `null` if no pair overlaps.
646
1747
  *
647
1748
  * @example
@@ -655,6 +1756,57 @@ export declare class Game {
655
1756
  * ```
656
1757
  */
657
1758
  overlapAny(listA: Sprite[], listB: Sprite[]): [Sprite, Sprite] | null;
1759
+ /**
1760
+ * Tests and resolves a basic AABB collision between two sprites.
1761
+ *
1762
+ * This helper is intended for simple platformer-style collision response.
1763
+ * It uses the same axis-aligned square bounds as {@link Game.overlap}, but
1764
+ * additionally pushes one sprite out of the collision and zeroes velocity on
1765
+ * the resolved axis.
1766
+ *
1767
+ * **Resolution rules:**
1768
+ * - If the first sprite is dynamic (`isStatic = false`), the first sprite is resolved.
1769
+ * - Otherwise, if the second sprite is dynamic, the second sprite is resolved.
1770
+ * - If both sprites are static, collision is reported but no movement occurs.
1771
+ *
1772
+ * The returned flags are always reported relative to the **first** sprite.
1773
+ *
1774
+ * @param a - First sprite. Usually the moving actor (for example, the player).
1775
+ * @param b - Second sprite. Usually a static obstacle or platform.
1776
+ * @returns Collision details, or `null` if the sprites do not overlap.
1777
+ * @throws Error if the game's physics helpers are not enabled.
1778
+ *
1779
+ * @example
1780
+ * ```ts
1781
+ * const hit = game.collide(player, floorTile);
1782
+ * if (hit?.grounded) {
1783
+ * canJump = true;
1784
+ * }
1785
+ * ```
1786
+ */
1787
+ collide(a: Sprite, b: Sprite): CollisionInfo | null;
1788
+ /**
1789
+ * Tests and resolves the first collision found between two groups of sprites.
1790
+ *
1791
+ * This behaves like {@link Game.overlapAny}, but uses the explicit collision
1792
+ * rules from {@link Game.collide} and returns the collision details as the
1793
+ * third tuple item.
1794
+ *
1795
+ * @param listA - First group of sprites.
1796
+ * @param listB - Second group of sprites.
1797
+ * @returns A `[EmojiSprite, EmojiSprite, CollisionInfo]` tuple for the first collision found,
1798
+ * or `null` if no pair overlaps.
1799
+ * @throws Error if the game's physics helpers are not enabled.
1800
+ *
1801
+ * @example
1802
+ * ```ts
1803
+ * const hit = game.collideAny([player], floorTiles);
1804
+ * if (hit?.[2].grounded) {
1805
+ * canJump = true;
1806
+ * }
1807
+ * ```
1808
+ */
1809
+ collideAny(listA: Sprite[], listB: Sprite[]): [Sprite, Sprite, CollisionInfo] | null;
658
1810
  /**
659
1811
  * Returns `true` while the specified key is held down (every frame it is held).
660
1812
  * Use this for continuous actions like movement.
@@ -695,6 +1847,8 @@ export declare class Game {
695
1847
  * Use this for continuous pointer actions.
696
1848
  *
697
1849
  * **Hold behavior:** returns `true` every frame from press until release.
1850
+ * On a touch screen it follows one finger, as {@link Game.pointerX} does,
1851
+ * so it turns `false` when that finger lifts, even if another is down.
698
1852
  *
699
1853
  * @returns `true` if the pointer is currently pressed.
700
1854
  *
@@ -723,50 +1877,50 @@ export declare class Game {
723
1877
  * Returns `true` while any active pointer is held down over the target sprite.
724
1878
  * Works with both mouse input and multiple simultaneous touches.
725
1879
  *
726
- * Pointer hit testing uses a circular area centered on the sprite. The radius
727
- * is `sprite.size * radiusScale`. World-space sprites are tested against the
728
- * current camera scroll. HUD sprites with `ignoreScroll = true` are tested in
729
- * screen space.
1880
+ * Pointer hit testing uses the sprite's exact displayed rectangular bounds.
1881
+ * World-space sprites are tested against the current camera scroll. HUD
1882
+ * sprites with `ignoreScroll = true` are tested in screen space. For
1883
+ * anchored {@link TextSprite} instances, the engine first resolves
1884
+ * `anchorX` / `anchorY` into the final rendered center, then tests against
1885
+ * that rectangle.
730
1886
  *
731
1887
  * Use this for continuous virtual buttons such as touch movement controls.
732
1888
  *
733
1889
  * @param sprite - Target sprite to test. If `null` / `undefined`, returns `false`.
734
- * @param radiusScale - Multiplier applied to `sprite.size` to define the hit radius.
735
- * Default: `0.5`.
736
1890
  * @returns `true` if any currently held pointer overlaps the sprite hit area.
737
1891
  *
738
1892
  * @example
739
1893
  * ```ts
740
- * if (game.isPointerDownOverSprite(leftButton, 0.72)) {
1894
+ * if (game.isPointerDownOverSprite(leftButton)) {
741
1895
  * player.x -= 200 * dt;
742
1896
  * }
743
1897
  * ```
744
1898
  */
745
- isPointerDownOverSprite(sprite: Sprite | null | undefined, radiusScale?: number): boolean;
1899
+ isPointerDownOverSprite(sprite: Sprite | null | undefined): boolean;
746
1900
  /**
747
1901
  * Returns `true` only on the frame any pointer first pressed over the target
748
1902
  * sprite. Works with both mouse input and multiple simultaneous touches.
749
1903
  *
750
- * Pointer hit testing uses a circular area centered on the sprite. The radius
751
- * is `sprite.size * radiusScale`. World-space sprites are tested against the
752
- * current camera scroll. HUD sprites with `ignoreScroll = true` are tested in
753
- * screen space.
1904
+ * Pointer hit testing uses the sprite's exact displayed rectangular bounds.
1905
+ * World-space sprites are tested against the current camera scroll. HUD
1906
+ * sprites with `ignoreScroll = true` are tested in screen space. For
1907
+ * anchored {@link TextSprite} instances, the engine first resolves
1908
+ * `anchorX` / `anchorY` into the final rendered center, then tests against
1909
+ * that rectangle.
754
1910
  *
755
1911
  * Use this for one-shot virtual buttons such as menu taps.
756
1912
  *
757
1913
  * @param sprite - Target sprite to test. If `null` / `undefined`, returns `false`.
758
- * @param radiusScale - Multiplier applied to `sprite.size` to define the hit radius.
759
- * Default: `0.5`.
760
1914
  * @returns `true` if any pointer began pressing this frame over the sprite hit area.
761
1915
  *
762
1916
  * @example
763
1917
  * ```ts
764
- * if (game.isPointerPressedOverSprite(startButton, 0.8)) {
1918
+ * if (game.isPointerPressedOverSprite(startButton)) {
765
1919
  * startGame();
766
1920
  * }
767
1921
  * ```
768
1922
  */
769
- isPointerPressedOverSprite(sprite: Sprite | null | undefined, radiusScale?: number): boolean;
1923
+ isPointerPressedOverSprite(sprite: Sprite | null | undefined): boolean;
770
1924
  /**
771
1925
  * Returns a read-only snapshot of all currently active pointers.
772
1926
  *
@@ -784,7 +1938,7 @@ export declare class Game {
784
1938
  * const pointers = game.getPointers();
785
1939
  * if (pointers.length > 0) {
786
1940
  * const first = pointers[0];
787
- * game.text(`Pointer: ${first.x}, ${first.y}`, 10, 10);
1941
+ * game.drawText(`Pointer: ${first.x}, ${first.y}`, 10, 10, 14);
788
1942
  * }
789
1943
  * ```
790
1944
  */
@@ -793,29 +1947,29 @@ export declare class Game {
793
1947
  * Returns a read-only snapshot of all active pointers currently overlapping
794
1948
  * the target sprite.
795
1949
  *
796
- * Pointer hit testing uses a circular area centered on the sprite. The radius
797
- * is `sprite.size * radiusScale`. World-space sprites are tested against the
798
- * current camera scroll. HUD sprites with `ignoreScroll = true` are tested in
799
- * screen space.
1950
+ * Pointer hit testing uses the sprite's exact displayed rectangular bounds.
1951
+ * World-space sprites are tested against the current camera scroll. HUD
1952
+ * sprites with `ignoreScroll = true` are tested in screen space. For
1953
+ * anchored {@link TextSprite} instances, the engine first resolves
1954
+ * `anchorX` / `anchorY` into the final rendered center, then tests against
1955
+ * that rectangle.
800
1956
  *
801
1957
  * Use this when you need more than a boolean result, such as reading the exact
802
1958
  * pointer position over a virtual joystick or draggable control.
803
1959
  *
804
1960
  * @param sprite - Target sprite to test. If `null` / `undefined`, returns an empty array.
805
- * @param radiusScale - Multiplier applied to `sprite.size` to define the hit radius.
806
- * Default: `0.5`.
807
1961
  * @returns A read-only array of active pointer snapshots currently over the sprite.
808
1962
  *
809
1963
  * @example
810
1964
  * ```ts
811
- * const touches = game.getPointersOverSprite(joystickBase, 1.0);
1965
+ * const touches = game.getPointersOverSprite(joystickBase);
812
1966
  * if (touches.length > 0) {
813
1967
  * const p = touches[0];
814
1968
  * const dx = p.x - joystickBase.x;
815
1969
  * }
816
1970
  * ```
817
1971
  */
818
- getPointersOverSprite(sprite: Sprite | null | undefined, radiusScale?: number): readonly PointerInfo[];
1972
+ getPointersOverSprite(sprite: Sprite | null | undefined): readonly PointerInfo[];
819
1973
  /**
820
1974
  * Returns `true` when the current device appears to be mobile/touch-first.
821
1975
  *
@@ -845,6 +1999,9 @@ export declare class Game {
845
1999
  * gesture (click, keypress) before audio can play — always call `sound()` in
846
2000
  * response to user input or a game event triggered by input.
847
2001
  *
2002
+ * If you need a melody/sequence, use {@link Game.soundSequence} instead of
2003
+ * multiple `sound()` calls in the same frame.
2004
+ *
848
2005
  * The tone fades out exponentially over `durationMs` to avoid clicks.
849
2006
  *
850
2007
  * @param freq - Frequency in Hz. Middle C = 261.6. Typical range: 100–4000 Hz.
@@ -858,6 +2015,20 @@ export declare class Game {
858
2015
  * ```
859
2016
  */
860
2017
  sound(freq: number, durationMs: number): void;
2018
+ /**
2019
+ * Plays multiple square-wave tones in strict sequence.
2020
+ *
2021
+ * Each tuple is `[frequencyHz, durationMs]`. The next tone starts when the
2022
+ * previous one ends.
2023
+ *
2024
+ * @param tones - Tone tuples in playback order.
2025
+ *
2026
+ * @example
2027
+ * ```ts
2028
+ * game.soundSequence([660, 80], [820, 90], [980, 110], [1180, 150]);
2029
+ * ```
2030
+ */
2031
+ soundSequence(...tones: Array<readonly [number, number]>): void;
861
2032
  /**
862
2033
  * Animates a sprite's {@link Sprite.alpha} from its current value to `to`
863
2034
  * over `durationMs` milliseconds using **linear interpolation**.
@@ -906,6 +2077,289 @@ export declare class Game {
906
2077
  * ```
907
2078
  */
908
2079
  animateRotation(sprite: Sprite, to: number, durationMs: number, onComplete?: () => void): void;
2080
+ /**
2081
+ * Animates a sprite's visual deformation using non-uniform scaling around a
2082
+ * normalized pivot point.
2083
+ *
2084
+ * This affects rendering only. Physics and collision bounds continue to use
2085
+ * {@link EmojiSprite.displaySize} and ignore the deform result.
2086
+ *
2087
+ * `pivotX` / `pivotY` are normalized anchors in the sprite's square display box:
2088
+ * - `0` = left/top
2089
+ * - `0.5` = center
2090
+ * - `1` = right/bottom
2091
+ *
2092
+ * For example, `pivotY = 1` keeps the bottom visually pinned while squashing
2093
+ * or stretching, which is useful for floor-contact animation.
2094
+ *
2095
+ * Like other MinimoJS animations, this animates from the sprite's current
2096
+ * deform state to the requested target and leaves the final result applied.
2097
+ *
2098
+ * @param sprite - The sprite to deform.
2099
+ * @param toScaleX - Target horizontal deform scale. `1` = unchanged.
2100
+ * @param toScaleY - Target vertical deform scale. `1` = unchanged.
2101
+ * @param pivotX - Horizontal pivot in normalized `[0, 1]` sprite space.
2102
+ * @param pivotY - Vertical pivot in normalized `[0, 1]` sprite space.
2103
+ * @param durationMs - Duration of the animation in **milliseconds**.
2104
+ * @param onComplete - Optional callback invoked when the animation finishes.
2105
+ *
2106
+ * @example
2107
+ * ```ts
2108
+ * game.animateDeform(player, 1.18, 0.85, 0.5, 1, 90, () => {
2109
+ * game.animateDeform(player, 1, 1, 0.5, 1, 120);
2110
+ * });
2111
+ * ```
2112
+ */
2113
+ animateDeform(sprite: Sprite, toScaleX: number, toScaleY: number, pivotX: number, pivotY: number, durationMs: number, onComplete?: () => void): void;
2114
+ /**
2115
+ * Convenience helper that squashes a sprite around its horizontal center.
2116
+ *
2117
+ * This is equivalent to animating toward a wider, shorter visual pose using
2118
+ * {@link Game.animateDeform}. `pivotY = 1` is the common floor-anchored case.
2119
+ *
2120
+ * @param sprite - The sprite to squash.
2121
+ * @param pivotY - Vertical pivot in normalized `[0, 1]` sprite space.
2122
+ * @param durationMs - Duration of the animation in **milliseconds**.
2123
+ * @param onComplete - Optional callback invoked when the animation finishes.
2124
+ */
2125
+ animateSquash(sprite: Sprite, pivotY: number, durationMs: number, onComplete?: () => void): void;
2126
+ /**
2127
+ * Convenience helper that stretches a sprite around its horizontal center.
2128
+ *
2129
+ * This is equivalent to animating toward a narrower, taller visual pose using
2130
+ * {@link Game.animateDeform}. `pivotY = 1` is the common floor-anchored case.
2131
+ *
2132
+ * @param sprite - The sprite to stretch.
2133
+ * @param pivotY - Vertical pivot in normalized `[0, 1]` sprite space.
2134
+ * @param durationMs - Duration of the animation in **milliseconds**.
2135
+ * @param onComplete - Optional callback invoked when the animation finishes.
2136
+ */
2137
+ animateStretch(sprite: Sprite, pivotY: number, durationMs: number, onComplete?: () => void): void;
2138
+ /**
2139
+ * Convenience helper that applies a centered uniform scale pulse and then
2140
+ * returns the sprite to its neutral shape.
2141
+ *
2142
+ * Internally this runs two deform animations back-to-back using
2143
+ * {@link Game.animateDeform}.
2144
+ *
2145
+ * @param sprite - The sprite to pulse.
2146
+ * @param scale - Peak uniform scale reached midway through the pulse. `1` leaves the sprite unchanged.
2147
+ * @param durationMs - Total pulse duration in **milliseconds**.
2148
+ * @param onComplete - Optional callback invoked after the sprite returns to neutral.
2149
+ *
2150
+ * @example
2151
+ * ```ts
2152
+ * game.animatePulse(coin, 1.25, 220);
2153
+ * ```
2154
+ */
2155
+ animatePulse(sprite: Sprite, scale: number, durationMs: number, onComplete?: () => void): void;
2156
+ /**
2157
+ * Applies a decaying screen-space shake to a sprite without changing its
2158
+ * logical position.
2159
+ *
2160
+ * This is a render-only effect. Physics, collisions, and the sprite's
2161
+ * `x` / `y` values are not modified.
2162
+ *
2163
+ * @param sprite - The sprite to shake.
2164
+ * @param intensity - Maximum shake offset in pixels.
2165
+ * @param durationMs - Duration of the shake in **milliseconds**.
2166
+ * @param onComplete - Optional callback invoked when the shake ends.
2167
+ *
2168
+ * @example
2169
+ * ```ts
2170
+ * game.animateShake(player, 18, 300);
2171
+ * ```
2172
+ */
2173
+ animateShake(sprite: Sprite, intensity: number, durationMs: number, onComplete?: () => void): void;
2174
+ /**
2175
+ * Moves a sprite upward and back down to its starting position along a simple
2176
+ * parabolic arc.
2177
+ *
2178
+ * This animation writes directly to {@link Sprite.x} / {@link Sprite.y} while
2179
+ * it is active.
2180
+ *
2181
+ * @param sprite - The sprite to bounce.
2182
+ * @param height - Peak bounce height in pixels.
2183
+ * @param durationMs - Total bounce duration in **milliseconds**.
2184
+ * @param onComplete - Optional callback invoked when the bounce ends.
2185
+ *
2186
+ * @example
2187
+ * ```ts
2188
+ * game.animateBounce(ball, 120, 600);
2189
+ * ```
2190
+ */
2191
+ animateBounce(sprite: Sprite, height: number, durationMs: number, onComplete?: () => void): void;
2192
+ /**
2193
+ * Moves a sprite upward and back down with a smooth sine-shaped float motion.
2194
+ *
2195
+ * This animation writes directly to {@link Sprite.x} / {@link Sprite.y} while
2196
+ * it is active.
2197
+ *
2198
+ * @param sprite - The sprite to float.
2199
+ * @param distance - Maximum upward travel in pixels.
2200
+ * @param durationMs - Total float duration in **milliseconds**.
2201
+ * @param onComplete - Optional callback invoked when the float ends.
2202
+ *
2203
+ * @example
2204
+ * ```ts
2205
+ * game.animateFloat(balloon, 90, 1200);
2206
+ * ```
2207
+ */
2208
+ animateFloat(sprite: Sprite, distance: number, durationMs: number, onComplete?: () => void): void;
2209
+ /**
2210
+ * Toggles a sprite's rendered opacity on and off a fixed number of times.
2211
+ *
2212
+ * This is a render-only effect. The sprite remains present in the world and
2213
+ * keeps receiving physics updates while blinking.
2214
+ *
2215
+ * @param sprite - The sprite to blink.
2216
+ * @param times - Number of visible/invisible toggle cycles.
2217
+ * @param durationMs - Total blink duration in **milliseconds**.
2218
+ * @param onComplete - Optional callback invoked when blinking ends.
2219
+ *
2220
+ * @example
2221
+ * ```ts
2222
+ * game.animateBlink(player, 4, 700);
2223
+ * ```
2224
+ */
2225
+ animateBlink(sprite: Sprite, times: number, durationMs: number, onComplete?: () => void): void;
2226
+ /**
2227
+ * Applies a rapid irregular alpha variation to create a damaged, unstable, or
2228
+ * ghost-like flicker.
2229
+ *
2230
+ * This is a render-only effect and does not change the sprite's logical
2231
+ * position or collision state.
2232
+ *
2233
+ * @param sprite - The sprite to flicker.
2234
+ * @param durationMs - Total flicker duration in **milliseconds**.
2235
+ * @param onComplete - Optional callback invoked when the flicker ends.
2236
+ *
2237
+ * @example
2238
+ * ```ts
2239
+ * game.animateFlicker(ghost, 900);
2240
+ * ```
2241
+ */
2242
+ animateFlicker(sprite: Sprite, durationMs: number, onComplete?: () => void): void;
2243
+ /**
2244
+ * Moves a sprite from its current position to a target position along a
2245
+ * parabolic arc.
2246
+ *
2247
+ * This animation writes directly to {@link Sprite.x} / {@link Sprite.y} while
2248
+ * it is active.
2249
+ *
2250
+ * @param sprite - The sprite to move.
2251
+ * @param toX - Destination X position in world space.
2252
+ * @param toY - Destination Y position in world space.
2253
+ * @param arcHeight - Maximum height of the arc above the linear path, in pixels.
2254
+ * @param durationMs - Total arc duration in **milliseconds**.
2255
+ * @param onComplete - Optional callback invoked when the arc ends.
2256
+ *
2257
+ * @example
2258
+ * ```ts
2259
+ * game.animateArc(coin, player.x, player.y, 80, 420);
2260
+ * ```
2261
+ */
2262
+ animateArc(sprite: Sprite, toX: number, toY: number, arcHeight: number, durationMs: number, onComplete?: () => void): void;
2263
+ /**
2264
+ * Emits temporary afterimages from a sprite to create a motion trail.
2265
+ *
2266
+ * This is a render-only effect. Trail ghosts are not real sprites and do not
2267
+ * appear in {@link Game.getSprites}, collide, or receive physics.
2268
+ *
2269
+ * @param sprite - The sprite to sample for trail ghosts.
2270
+ * @param options - Optional trail tuning values.
2271
+ * @param onComplete - Optional callback invoked when the emitter stops spawning ghosts.
2272
+ *
2273
+ * @example
2274
+ * ```ts
2275
+ * game.animateTrail(player, { durationMs: 300, spacingMs: 30, fadeMs: 180 });
2276
+ * ```
2277
+ */
2278
+ animateTrail(sprite: Sprite, options?: TrailOptions, onComplete?: () => void): void;
2279
+ /**
2280
+ * Breaks a sprite into visual pieces that burst outward from its current
2281
+ * rendered appearance.
2282
+ *
2283
+ * This is a render-only effect. The spawned pieces are not real sprites and do
2284
+ * not appear in {@link Game.getSprites}, receive physics, or collide.
2285
+ *
2286
+ * By default, the original sprite is destroyed immediately after the explosion
2287
+ * effect is spawned. Set `destroySprite: false` to hide the sprite during the
2288
+ * effect and restore it when the explosion ends.
2289
+ *
2290
+ * @param sprite - The sprite to explode.
2291
+ * @param options - Optional explosion tuning values.
2292
+ * @param onComplete - Optional callback invoked when the effect finishes.
2293
+ *
2294
+ * @example
2295
+ * ```ts
2296
+ * game.animateExplode(enemy, { rows: 4, cols: 4, durationMs: 650, speed: 320 });
2297
+ * ```
2298
+ */
2299
+ animateExplode(sprite: Sprite, options?: ExplodeOptions, onComplete?: () => void): void;
2300
+ /**
2301
+ * Spawns visual pieces around a sprite and converges them back into its
2302
+ * current rendered appearance.
2303
+ *
2304
+ * This is a render-only effect. The spawned pieces are not real sprites and
2305
+ * do not appear in {@link Game.getSprites}, receive physics, or collide.
2306
+ *
2307
+ * The original sprite is hidden while the assembly plays and is restored when
2308
+ * the effect finishes.
2309
+ *
2310
+ * @param sprite - The sprite to assemble.
2311
+ * @param options - Optional assembly tuning values.
2312
+ * @param onComplete - Optional callback invoked when the effect finishes.
2313
+ */
2314
+ animateAssemble(sprite: Sprite, options?: AssembleOptions, onComplete?: () => void): void;
2315
+ /**
2316
+ * Breaks a sprite apart locally inside its own bounds using a directional
2317
+ * dissolve sweep.
2318
+ *
2319
+ * This is a render-only effect. The spawned pieces are not real sprites and
2320
+ * do not appear in {@link Game.getSprites}, receive physics, or collide.
2321
+ *
2322
+ * By default, the original sprite is destroyed immediately after the effect
2323
+ * is spawned. Set `destroySprite: false` to leave the sprite hidden instead.
2324
+ *
2325
+ * @param sprite - The sprite to disintegrate.
2326
+ * @param options - Optional disintegration tuning values.
2327
+ * @param onComplete - Optional callback invoked when the effect finishes.
2328
+ */
2329
+ animateDisintegrate(sprite: Sprite, options?: DisintegrateOptions, onComplete?: () => void): void;
2330
+ /**
2331
+ * Reconstructs a sprite locally inside its own bounds using a directional
2332
+ * integration sweep.
2333
+ *
2334
+ * This is a render-only effect. The spawned pieces are not real sprites and
2335
+ * do not appear in {@link Game.getSprites}, receive physics, or collide.
2336
+ *
2337
+ * The original sprite is hidden while the integration plays and is restored
2338
+ * when the effect finishes.
2339
+ *
2340
+ * @param sprite - The sprite to integrate.
2341
+ * @param options - Optional integration tuning values.
2342
+ * @param onComplete - Optional callback invoked when the effect finishes.
2343
+ */
2344
+ animateIntegrate(sprite: Sprite, options?: IntegrateOptions, onComplete?: () => void): void;
2345
+ /**
2346
+ * Reveals or covers a sprite with a directional wipe mask.
2347
+ *
2348
+ * This is a render-only effect that captures the sprite's current appearance
2349
+ * and animates a clipping region over that snapshot.
2350
+ *
2351
+ * For `mode: "reveal"`, the original sprite is hidden during the wipe and is
2352
+ * restored when the effect finishes.
2353
+ *
2354
+ * For `mode: "cover"`, the effect hides the sprite over time. If
2355
+ * `destroySprite` is omitted or `true`, the sprite is destroyed immediately
2356
+ * after the wipe starts and only the wipe snapshot remains visible.
2357
+ *
2358
+ * @param sprite - The sprite to wipe.
2359
+ * @param options - Optional wipe tuning values.
2360
+ * @param onComplete - Optional callback invoked when the effect finishes.
2361
+ */
2362
+ animateWipe(sprite: Sprite, options?: WipeOptions, onComplete?: () => void): void;
909
2363
  /**
910
2364
  * Schedules a callback to fire after `delayMs` milliseconds, driven by the
911
2365
  * rAF loop (not `setTimeout`). Timers accumulate elapsed time each frame and
@@ -952,18 +2406,26 @@ export declare class Game {
952
2406
  */
953
2407
  clearTimer(id: number): void;
954
2408
  /**
955
- * Draws text on screen as a **screen-space overlay** this frame.
2409
+ * Draws text on screen as a **simple screen-space overlay** this frame.
956
2410
  *
957
2411
  * **Overlay behavior:** Text is drawn in canvas/screen space — it ignores
958
2412
  * `scrollX` / `scrollY`. Position `(0, 0)` is always the top-left of the canvas.
959
- * Use this for HUD elements: score, lives, timer, debug info.
2413
+ * Use this for lightweight HUD elements: score, lives, timer, debug info.
960
2414
  *
961
2415
  * **Per-frame:** `drawText` must be called every frame to keep text visible.
962
2416
  * The text overlay list is cleared after each render. Call this inside `onUpdate`.
963
2417
  *
964
2418
  * **Layer:** Text is always drawn on top of all sprites.
965
- * **Font:** Text always uses a fixed `monospace` font family.
966
- * Font family cannot be customized in MinimoJS v1.
2419
+ * **Font:** Text always uses `monospace`. Use {@link TextSprite} for any
2420
+ * other family.
2421
+ *
2422
+ * Prefer {@link TextSprite} for UI buttons or labels that need persistent
2423
+ * bounds, hit testing, fixed sizes, backgrounds, borders, or text stroke.
2424
+ * `drawText()` is the lightweight overlay API, not the primary UI API.
2425
+ *
2426
+ * For controls that are only a single emoji, prefer {@link EmojiSprite} over
2427
+ * {@link TextSprite}. Emoji-only buttons do not benefit from text padding and
2428
+ * usually fit more naturally in the sprite pipeline.
967
2429
  *
968
2430
  * @param text - The string to render. Supports emoji and Unicode.
969
2431
  * @param x - X position in **screen space** (pixels from canvas left edge).
@@ -1000,6 +2462,25 @@ export declare class Game {
1000
2462
  * ```
1001
2463
  */
1002
2464
  clearSpriteCache(): void;
2465
+ /**
2466
+ * Registers a web font family that must be loaded before the first frame.
2467
+ *
2468
+ * Call this before {@link Game.start}, typically right after constructing the
2469
+ * game or inside {@link Game.onPreload}. MinimoJS waits for every registered
2470
+ * font via `document.fonts.load(...)` before rendering anything. Host
2471
+ * environments may also pre-register fonts by setting
2472
+ * `globalThis.__MINIMO_WEB_FONT_REQUIREMENTS__` before your game code runs.
2473
+ *
2474
+ * The font itself must still be declared by your page (for example with a
2475
+ * `<link rel="stylesheet">` in `index.html`). MinimoJS only waits for it.
2476
+ *
2477
+ * @example
2478
+ * ```ts
2479
+ * game.requireFont('"Press Start 2P"');
2480
+ * game.requireFont('"Bangers"', { weight: "700" });
2481
+ * ```
2482
+ */
2483
+ requireFont(family: string, options?: FontRequirementOptions): void;
1003
2484
  /**
1004
2485
  * Returns a pseudo-random floating-point number in the range `[0, 1)`.
1005
2486
  * Delegates to `Math.random()`.
@@ -1021,9 +2502,12 @@ export declare class Game {
1021
2502
  * Performs a full engine state reset to enable scene switching.
1022
2503
  *
1023
2504
  * **Cleared by reset:**
2505
+ * - All background layers
1024
2506
  * - All sprites (equivalent to calling {@link Game.destroySprite} on every sprite)
1025
2507
  * - All timers (regardless of repeat state)
1026
2508
  * - All running animations
2509
+ * - All active explosion effects
2510
+ * - All active trail effects
1027
2511
  * - All pending text overlays
1028
2512
  * - Scroll position (`scrollX = 0`, `scrollY = 0`)
1029
2513
  * - Per-frame input state (pressed keys, pressed pointer)
@@ -1036,50 +2520,95 @@ export declare class Game {
1036
2520
  * - Canvas dimensions
1037
2521
  * - AudioContext
1038
2522
  *
1039
- * After clearing, `reset()` immediately calls `onCreate()` so your callback
1040
- * can rebuild the new scene synchronously.
2523
+ * After clearing, `reset()` immediately calls the active scene's
2524
+ * `onCreate()` (or legacy {@link Game.onCreate} if no scene is active) so it
2525
+ * can rebuild synchronously.
2526
+ *
2527
+ * The exception is a scene whose {@link IScene.onPreload} registers images
2528
+ * that are not loaded yet. Then the current scene freezes and dims under a progress
2529
+ * ring, nothing is cleared until the images load, and `reset()` returns
2530
+ * before the scene is rebuilt; its `onCreate()` runs once they have loaded.
2531
+ * A `reset()` called while a scene is loading is ignored.
2532
+ *
2533
+ * @param scene - Optional new scene to make active before rebuilding.
1041
2534
  *
1042
2535
  * @example
1043
2536
  * ```ts
1044
- * // Switch from gameplay to game-over screen
1045
- * function gameOver() {
1046
- * game.reset(); // onCreate() is called here, rebuild inside it
1047
- * }
1048
- *
1049
- * game.onCreate = () => {
1050
- * // Scene init
1051
- * const skull = new Sprite("💀");
1052
- * skull.x = 400; skull.y = 300; skull.size = 96;
1053
- * game.add(skull);
1054
- * game.addTimer(3000, false, () => game.reset()); // auto-restart
1055
- * };
2537
+ * class GameOverScene {
2538
+ * onCreate() {
2539
+ * const skull = new EmojiSprite("💀", 400, 300, 96);
2540
+ * game.add(skull);
2541
+ * game.addTimer(3000, false, () => game.reset());
2542
+ * }
2543
+ * }
2544
+ *
2545
+ * game.reset(new GameOverScene());
1056
2546
  * ```
1057
2547
  */
1058
- reset(): void;
2548
+ reset(scene?: IScene): void;
2549
+ /**
2550
+ * Changes to a new scene using a full-screen transition between frozen scene
2551
+ * snapshots.
2552
+ *
2553
+ * `transitionTo()` does not change the behavior of {@link Game.reset}. It
2554
+ * captures the current scene as a snapshot, rebuilds the target scene
2555
+ * immediately, captures the new scene as another snapshot, and then animates
2556
+ * between those two images for the requested duration.
2557
+ *
2558
+ * While the transition is active, gameplay updates are paused.
2559
+ *
2560
+ * If the target scene's {@link IScene.onPreload} registers images that are
2561
+ * not loaded yet, the current scene first freezes and dims under a progress ring
2562
+ * until they load, and the transition plays after that. `transitionTo()`
2563
+ * then returns before the target scene exists; continue in its
2564
+ * `onCreate()` or in `onComplete`. If the player declines to retry failed
2565
+ * images, the game stays on the current scene and `onComplete` is not
2566
+ * called.
2567
+ *
2568
+ * A call made while a transition plays or a scene loads is ignored.
2569
+ *
2570
+ * If the game loop is not running yet, this falls back to
2571
+ * {@link Game.reset}.
2572
+ *
2573
+ * @param scene - Target scene to make active.
2574
+ * @param options - Transition style and optional tuning values.
2575
+ * @param onComplete - Optional callback invoked when the transition finishes.
2576
+ */
2577
+ transitionTo(scene: IScene, options: SceneTransitionOptions, onComplete?: () => void): void;
1059
2578
  /**
1060
2579
  * Starts the `requestAnimationFrame` game loop.
1061
2580
  * Safe to call multiple times — does nothing if already running.
1062
- * If this is the first start (or after a reset), `onCreate()` is called
1063
- * before the first frame.
1064
- *
1065
- * The loop calls `onUpdate` once per frame, then renders all sprites and
1066
- * text overlays. Order per frame:
2581
+ * On the first start, MinimoJS runs {@link Game.onPreload} and the active
2582
+ * scene's {@link IScene.onPreload}, loads the images they queued while
2583
+ * showing a thin loading line, and waits for all fonts registered via
2584
+ * {@link Game.requireFont}. After that, the active scene's `onCreate()` is
2585
+ * called before the first frame.
2586
+ *
2587
+ * The loop calls the active scene's `onUpdate(dt)` (or legacy
2588
+ * {@link Game.onUpdate}) once per frame, then renders all sprites and text
2589
+ * overlays. Order per frame:
1067
2590
  * 1. Accumulate timer elapsed time; fire ready callbacks.
1068
2591
  * 2. Advance animations (linear interpolation).
1069
- * 3. Apply gravity to sprite velocities.
1070
- * 4. Apply velocities to sprite positions.
1071
- * 5. Call `onUpdate(dt)`.
1072
- * 6. Render sprites (sorted by layer) with scroll offset.
1073
- * 7. Render text overlays (screen space, on top of sprites).
1074
- * 8. Clear per-frame input state.
2592
+ * 3. Advance active piece and wipe effects.
2593
+ * 4. Apply gravity to sprite velocities.
2594
+ * 5. Apply velocities to sprite positions.
2595
+ * 6. Call `onUpdate(dt)`.
2596
+ * 7. Advance active trail effects.
2597
+ * 8. Render sprites/effects (with scroll offset).
2598
+ * 9. Render text overlays (screen space, on top of sprites).
2599
+ * 10. Clear per-frame input state.
1075
2600
  *
1076
2601
  * @example
1077
2602
  * ```ts
1078
- * game.onUpdate = (dt) => { ... };
1079
- * game.start();
2603
+ * class DemoScene {
2604
+ * onCreate() {}
2605
+ * onUpdate(dt: number) {}
2606
+ * }
2607
+ *
2608
+ * game.start(new DemoScene());
1080
2609
  * ```
1081
2610
  */
1082
- start(): void;
2611
+ start(scene?: IScene): void;
1083
2612
  /**
1084
2613
  * Stops the game loop. The canvas retains its last rendered frame.
1085
2614
  * Call {@link Game.start} to resume.