minimojs 1.0.0-alpha.20 → 1.0.0-alpha.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/minimo.js CHANGED
@@ -23,7 +23,7 @@ import { TrailSystem } from "./internal/TrailSystem.js";
23
23
  import { TransitionSystem } from "./internal/TransitionSystem.js";
24
24
  import { TimerSystem } from "./internal/TimerSystem.js";
25
25
  // ---------------------------------------------------------------------------
26
- // Sprite
26
+ // EmojiSprite
27
27
  // ---------------------------------------------------------------------------
28
28
  /**
29
29
  * Base class for all renderable MinimoJS actors.
@@ -32,7 +32,7 @@ import { TimerSystem } from "./internal/TimerSystem.js";
32
32
  * shared transform, visibility, physics, and animation state required by the
33
33
  * engine.
34
34
  */
35
- export class BaseSprite {
35
+ export class Sprite {
36
36
  constructor(game = null) {
37
37
  /**
38
38
  * X position in world space (horizontal center of the sprite), in pixels.
@@ -56,16 +56,16 @@ export class BaseSprite {
56
56
  /**
57
57
  * Optional logical body width used by physics helpers and collision checks.
58
58
  *
59
- * When `null` (default), MinimoJS uses the sprite's visual {@link BaseSprite.width}.
60
- * When set, this value is scaled by {@link BaseSprite.scale} the same way as the
59
+ * When `null` (default), MinimoJS uses the sprite's visual {@link Sprite.width}.
60
+ * When set, this value is scaled by {@link Sprite.scale} the same way as the
61
61
  * visual sprite size.
62
62
  */
63
63
  this.bodyWidth = null;
64
64
  /**
65
65
  * Optional logical body height used by physics helpers and collision checks.
66
66
  *
67
- * When `null` (default), MinimoJS uses the sprite's visual {@link BaseSprite.height}.
68
- * When set, this value is scaled by {@link BaseSprite.scale} the same way as the
67
+ * When `null` (default), MinimoJS uses the sprite's visual {@link Sprite.height}.
68
+ * When set, this value is scaled by {@link Sprite.scale} the same way as the
69
69
  * visual sprite size.
70
70
  */
71
71
  this.bodyHeight = null;
@@ -168,6 +168,17 @@ export class BaseSprite {
168
168
  * Values > 1 amplify gravity; negative values invert it.
169
169
  */
170
170
  this.gravityScale = 0;
171
+ // `abstract` and `protected` are erased by the TypeScript compiler, and games
172
+ // are written in JavaScript, so nothing but this check stops old code from
173
+ // calling `new Sprite(glyph, x, y, size)`. Without it the glyph would be
174
+ // stored as this sprite's game reference, the position and size would be
175
+ // discarded, and the failure would surface one call later inside `Game.add`
176
+ // as an unrelated "attached to multiple Game instances" error.
177
+ if (new.target === Sprite) {
178
+ throw new Error("MinimoJS: Sprite is an abstract type and cannot be constructed directly. " +
179
+ "Use EmojiSprite for an emoji, ImageSprite for a preloaded image, " +
180
+ "TextSprite for text, or DrawSprite for procedural drawing.");
181
+ }
171
182
  this._game = game;
172
183
  }
173
184
  /**
@@ -210,7 +221,7 @@ export class BaseSprite {
210
221
  return this.height * Math.max(0, safeScale);
211
222
  }
212
223
  /**
213
- * Effective collision-body width in pixels after applying {@link BaseSprite.scale}.
224
+ * Effective collision-body width in pixels after applying {@link Sprite.scale}.
214
225
  */
215
226
  get bodyDisplayWidth() {
216
227
  const safeScale = Number.isFinite(this.scale) ? this.scale : 1;
@@ -222,7 +233,7 @@ export class BaseSprite {
222
233
  return baseWidth * Math.max(0, safeScale);
223
234
  }
224
235
  /**
225
- * Effective collision-body height in pixels after applying {@link BaseSprite.scale}.
236
+ * Effective collision-body height in pixels after applying {@link Sprite.scale}.
226
237
  */
227
238
  get bodyDisplayHeight() {
228
239
  const safeScale = Number.isFinite(this.scale) ? this.scale : 1;
@@ -268,11 +279,11 @@ export class BaseSprite {
268
279
  *
269
280
  * @example
270
281
  * ```ts
271
- * const coin = new Sprite("🪙", 300, 200, 32);
282
+ * const coin = new EmojiSprite("🪙", 300, 200, 32);
272
283
  * game.add(coin);
273
284
  * ```
274
285
  */
275
- export class Sprite extends BaseSprite {
286
+ export class EmojiSprite extends Sprite {
276
287
  get size() {
277
288
  return this._size;
278
289
  }
@@ -286,7 +297,7 @@ export class Sprite extends BaseSprite {
286
297
  return this.displayWidth;
287
298
  }
288
299
  /**
289
- * Creates a new Sprite with the given emoji, optional position, and base size.
300
+ * Creates a new EmojiSprite with the given emoji, optional position, and base size.
290
301
  * All other properties use their defaults and can be set after construction.
291
302
  *
292
303
  * @param sprite - The emoji character to render. Must be a single emoji.
@@ -298,7 +309,7 @@ export class Sprite extends BaseSprite {
298
309
  *
299
310
  * @example
300
311
  * ```ts
301
- * const enemy = new Sprite("👾", 200, 100, 40);
312
+ * const enemy = new EmojiSprite("👾", 200, 100, 40);
302
313
  * game.add(enemy);
303
314
  * ```
304
315
  */
@@ -318,25 +329,29 @@ export class Sprite extends BaseSprite {
318
329
  * A renderable sprite backed by a preloaded image asset.
319
330
  *
320
331
  * Width and height are always resolved from the current texture. To resize the
321
- * sprite visually, use {@link BaseSprite.scale}.
332
+ * sprite visually, use {@link Sprite.scale}.
322
333
  */
323
- export class ImageSprite extends BaseSprite {
334
+ export class ImageSprite extends Sprite {
324
335
  get imageKey() {
325
336
  return this._imageKey;
326
337
  }
327
338
  get width() {
328
339
  const image = this.getResolvedImage();
329
- return image.naturalWidth || image.width;
340
+ return image instanceof HTMLImageElement
341
+ ? image.naturalWidth || image.width
342
+ : image.width;
330
343
  }
331
344
  get height() {
332
345
  const image = this.getResolvedImage();
333
- return image.naturalHeight || image.height;
346
+ return image instanceof HTMLImageElement
347
+ ? image.naturalHeight || image.height
348
+ : image.height;
334
349
  }
335
350
  /**
336
351
  * Creates a new image-backed sprite.
337
352
  *
338
353
  * @param game - Game instance used to resolve the texture key.
339
- * @param imageKey - Preloaded image key previously registered with {@link Game.loadImage}.
354
+ * @param imageKey - Texture key previously registered with {@link Game.loadImage} or {@link Game.createTexture}.
340
355
  * @param x - Initial X position in world space (center), in pixels. Default: `0`.
341
356
  * @param y - Initial Y position in world space (center), in pixels. Default: `0`.
342
357
  */
@@ -349,7 +364,7 @@ export class ImageSprite extends BaseSprite {
349
364
  /**
350
365
  * Replaces the current texture with another preloaded image.
351
366
  *
352
- * @param imageKey - New preloaded image key to render.
367
+ * @param imageKey - New texture key to render.
353
368
  */
354
369
  setTexture(imageKey) {
355
370
  if (imageKey === this._imageKey)
@@ -359,11 +374,14 @@ export class ImageSprite extends BaseSprite {
359
374
  }
360
375
  getRenderCacheKey() {
361
376
  const image = this.getResolvedImage();
377
+ const width = image instanceof HTMLImageElement ? image.naturalWidth || image.width : image.width;
378
+ const height = image instanceof HTMLImageElement ? image.naturalHeight || image.height : image.height;
362
379
  return [
363
380
  "image",
364
381
  this._imageKey,
365
- image.naturalWidth || image.width,
366
- image.naturalHeight || image.height,
382
+ this.game?.getImageVersion(this._imageKey) ?? 0,
383
+ width,
384
+ height,
367
385
  ].join("|");
368
386
  }
369
387
  /** @internal */
@@ -394,7 +412,7 @@ export class ImageSprite extends BaseSprite {
394
412
  *
395
413
  * Treat `DrawSprite` as a specialized tool, not the default sprite type.
396
414
  * Because it may execute custom Canvas 2D drawing code repeatedly, overusing it
397
- * can affect game performance much more than regular {@link Sprite},
415
+ * can affect game performance much more than regular {@link EmojiSprite},
398
416
  * {@link ImageSprite}, or {@link TextSprite} instances.
399
417
  *
400
418
  * Prefer the other sprite types whenever they can express the same result more
@@ -432,7 +450,7 @@ export class ImageSprite extends BaseSprite {
432
450
  * Override {@link DrawSprite.redraw} in a subclass, or assign your own method
433
451
  * on an instance if you prefer an inline style in JavaScript.
434
452
  */
435
- export class DrawSprite extends BaseSprite {
453
+ export class DrawSprite extends Sprite {
436
454
  get width() {
437
455
  return this._width;
438
456
  }
@@ -500,11 +518,11 @@ DrawSprite._nextSurfaceId = 1;
500
518
  * layout features such as wrapping, fixed button sizes, background/border, or
501
519
  * text stroke.
502
520
  *
503
- * For controls that are only a single emoji, prefer {@link Sprite}. Emoji-only
521
+ * For controls that are only a single emoji, prefer {@link EmojiSprite}. Emoji-only
504
522
  * buttons usually behave better as sprites because they do not need text
505
523
  * padding/layout and their bounds match the rendered emoji more directly.
506
524
  */
507
- export class TextSprite extends BaseSprite {
525
+ export class TextSprite extends Sprite {
508
526
  get width() {
509
527
  this.ensureMeasured();
510
528
  return this._measuredWidth;
@@ -752,8 +770,9 @@ TextSprite._measurementContext = null;
752
770
  * `x` / `y` use **top-left screen-space coordinates**, unlike {@link Sprite},
753
771
  * which uses center-based world space.
754
772
  *
755
- * The image referenced by {@link BackgroundLayer.imageKey} must be registered
756
- * with {@link Game.loadImage} during {@link Game.onPreload}.
773
+ * The texture referenced by {@link BackgroundLayer.imageKey} must be registered
774
+ * with {@link Game.loadImage} during {@link Game.onPreload}, or created
775
+ * dynamically with {@link Game.createTexture}.
757
776
  *
758
777
  * @example
759
778
  * ```ts
@@ -824,21 +843,21 @@ export class BackgroundLayer {
824
843
  * You must import it with standard ESM syntax such as:
825
844
  *
826
845
  * ```ts
827
- * import { Game, Sprite } from "minimojs";
846
+ * import { Game, EmojiSprite } from "minimojs";
828
847
  * ```
829
848
  *
830
849
  * In the browser, use it from a module script:
831
850
  *
832
851
  * ```html
833
852
  * <script type="module">
834
- * import { Game, Sprite } from "./dist/minimo.js";
853
+ * import { Game, EmojiSprite } from "./dist/minimo.js";
835
854
  * </script>
836
855
  * ```
837
856
  *
838
857
  * Or from a CDN:
839
858
  *
840
859
  * ```ts
841
- * import { Game, Sprite } from "https://cdn.jsdelivr.net/npm/minimojs@<version>/dist/minimo.js";
860
+ * import { Game, EmojiSprite } from "https://cdn.jsdelivr.net/npm/minimojs@<version>/dist/minimo.js";
842
861
  * ```
843
862
  *
844
863
  * Do NOT use a classic `<script>` tag without `type="module"`.
@@ -887,12 +906,19 @@ export class BackgroundLayer {
887
906
  * ## Quick Start
888
907
  *
889
908
  * ```ts
890
- * import { Game, Sprite } from "https://cdn.jsdelivr.net/npm/minimojs@<version>/dist/minimo.js";
909
+ * import { Game, ImageSprite } from "https://cdn.jsdelivr.net/npm/minimojs@<version>/dist/minimo.js";
891
910
  *
892
911
  * const game = new Game(720, 1280);
893
912
  *
894
- * const player = new Sprite("🐢", 400, 500, 48);
895
- * game.add(player);
913
+ * game.onPreload = () => {
914
+ * game.loadImage("player", "assets/player.png");
915
+ * };
916
+ *
917
+ * let player: ImageSprite;
918
+ *
919
+ * game.onCreate = () => {
920
+ * player = game.add(new ImageSprite(game, "player", 400, 500));
921
+ * };
896
922
  *
897
923
  * game.onUpdate = (dt) => {
898
924
  * if (game.isKeyDown("ArrowLeft")) player.vx = -200;
@@ -905,6 +931,13 @@ export class BackgroundLayer {
905
931
  * game.start();
906
932
  * ```
907
933
  *
934
+ * Prototyping, and have no art yet? Swap the texture for an emoji and drop the
935
+ * preload entirely — everything else stays the same:
936
+ *
937
+ * ```ts
938
+ * const player = game.add(new EmojiSprite("🐢", 400, 500, 48));
939
+ * ```
940
+ *
908
941
  * ---
909
942
  *
910
943
  * ## Engine Philosophy
@@ -930,13 +963,28 @@ export class BackgroundLayer {
930
963
  *
931
964
  * ---
932
965
  *
933
- * ## Emoji-Only Sprites
934
- *
935
- * Every sprite MUST use a single emoji character as its visual representation.
936
- * PNG, SVG, spritesheet, and image sprites are NOT supported.
937
- * Use Unicode emoji: `"🔥"`, `"⭐"`, `"💣"`, `"🐢"`, `"👾"`, `"🧱"`, etc.
938
- * AI agents can also use text-like emojis (regional indicators, symbols, letters)
939
- * to build fun title art, HUD labels, and expressive in-game text.
966
+ * ## Choosing a Visual Source
967
+ *
968
+ * A sprite's look comes from one of four concrete types, all of which extend the
969
+ * shared abstract {@link Sprite} type. Use `Sprite` as the type in your own
970
+ * signatures; never construct it directly.
971
+ *
972
+ * 1. {@link ImageSprite} the game's own images, and the first option to reach
973
+ * for. Register a texture with {@link Game.loadImage} inside
974
+ * {@link Game.onPreload}, or build one at runtime with
975
+ * {@link Game.createTexture}, then render it with
976
+ * `new ImageSprite(game, key, x, y)`. An image looks identical on every
977
+ * device and belongs to the game.
978
+ * 2. {@link EmojiSprite} — a single Unicode emoji, and the shortest path to a
979
+ * first prototype: `new EmojiSprite("🐢", x, y, size)`. Note that an
980
+ * emoji is drawn with the player's own system emoji font, so the same game
981
+ * looks different across iOS, Android, and Windows, and a glyph the platform
982
+ * lacks renders as an empty box. Prefer an image whenever the look matters.
983
+ * 3. {@link TextSprite} — text, labels, and buttons.
984
+ * 4. {@link DrawSprite} — procedural Canvas 2D drawing, for shapes and gauges
985
+ * the other three cannot express cleanly.
986
+ *
987
+ * Emoji and images mix freely in one game.
940
988
  *
941
989
  * ---
942
990
  *
@@ -1025,7 +1073,7 @@ export class BackgroundLayer {
1025
1073
  * ```ts
1026
1074
  * class SkullScene implements IScene {
1027
1075
  * onCreate() {
1028
- * const skull = new Sprite("💀", 400, 300, 96);
1076
+ * const skull = new EmojiSprite("💀", 400, 300, 96);
1029
1077
  * game.add(skull);
1030
1078
  * }
1031
1079
  *
@@ -1045,7 +1093,7 @@ export class BackgroundLayer {
1045
1093
  *
1046
1094
  * ---
1047
1095
  *
1048
- * ## Sprite Lifecycle Ownership
1096
+ * ## EmojiSprite Lifecycle Ownership
1049
1097
  *
1050
1098
  * The `Game` instance owns all sprites.
1051
1099
  * - Create sprites with {@link Game.add}.
@@ -1137,7 +1185,7 @@ export class Game {
1137
1185
  * @example
1138
1186
  * ```ts
1139
1187
  * game.onCreate = () => {
1140
- * const player = new Sprite("🐢");
1188
+ * const player = new EmojiSprite("🐢");
1141
1189
  * player.x = 200;
1142
1190
  * player.y = 300;
1143
1191
  * game.add(player);
@@ -1367,10 +1415,10 @@ export class Game {
1367
1415
  return this._inputSystem.pointerY;
1368
1416
  }
1369
1417
  // -------------------------------------------------------------------------
1370
- // Sprite management
1418
+ // EmojiSprite management
1371
1419
  // -------------------------------------------------------------------------
1372
1420
  /**
1373
- * Registers a {@link BaseSprite} (or subclass instance) with the engine.
1421
+ * Registers a {@link Sprite} (or subclass instance) with the engine.
1374
1422
  *
1375
1423
  * After calling `add`, the sprite is rendered and, if dynamic
1376
1424
  * (`isStatic = false`), receives built-in velocity/gravity integration every
@@ -1379,21 +1427,21 @@ export class Game {
1379
1427
  * **Ownership:** The game instance takes ownership of the sprite from this
1380
1428
  * point forward. It will appear in {@link Game.getSprites} on the same frame.
1381
1429
  *
1382
- * **Subclasses:** Any class that extends {@link BaseSprite} can be passed here.
1430
+ * **Subclasses:** Any class that extends {@link Sprite} can be passed here.
1383
1431
  * The engine stores and processes it as a live sprite; your custom properties
1384
1432
  * are preserved on the instance.
1385
1433
  *
1386
- * @param sprite - A {@link BaseSprite} instance (or subclass) to add.
1434
+ * @param sprite - A {@link Sprite} instance (or subclass) to add.
1387
1435
  * @returns The same sprite instance, for chaining or inline assignment.
1388
1436
  *
1389
1437
  * @example
1390
1438
  * ```ts
1391
1439
  * // Plain sprite
1392
- * const coin = new Sprite("🪙", 300, 200, 32);
1440
+ * const coin = new EmojiSprite("🪙", 300, 200, 32);
1393
1441
  * game.add(coin);
1394
1442
  *
1395
1443
  * // Custom subclass
1396
- * class Enemy extends Sprite {
1444
+ * class Enemy extends EmojiSprite {
1397
1445
  * speed = 150;
1398
1446
  * constructor(x: number, y: number) {
1399
1447
  * super("👾", x, y, 40);
@@ -1508,14 +1556,47 @@ export class Game {
1508
1556
  this._assetSystem.queueImage(key, src);
1509
1557
  }
1510
1558
  /**
1511
- * Returns a loaded image previously registered with {@link Game.loadImage},
1512
- * or `undefined` if it is not available.
1559
+ * Creates a dynamic texture immediately and registers it under a stable key.
1560
+ *
1561
+ * Unlike {@link Game.loadImage}, this does not require {@link Game.onPreload}.
1562
+ * The `painter` callback receives a fresh offscreen canvas and should draw the
1563
+ * full texture contents into it.
1564
+ *
1565
+ * The resulting texture can be used anywhere a normal image key is accepted,
1566
+ * including {@link ImageSprite}, background layers, and optional modules.
1567
+ *
1568
+ * @param key - Stable texture key used later by sprites and render helpers.
1569
+ * @param width - Texture width in pixels. Minimum `1`.
1570
+ * @param height - Texture height in pixels. Minimum `1`.
1571
+ * @param painter - Function that paints into the offscreen texture canvas.
1572
+ *
1573
+ * @example
1574
+ * ```ts
1575
+ * game.createTexture("checkpoint", 128, 64, (ctx, canvas) => {
1576
+ * ctx.fillStyle = "#101820";
1577
+ * ctx.fillRect(0, 0, canvas.width, canvas.height);
1578
+ * ctx.fillStyle = "#ffd54f";
1579
+ * ctx.fillRect(8, 8, canvas.width - 16, canvas.height - 16);
1580
+ * });
1581
+ * ```
1582
+ */
1583
+ createTexture(key, width, height, painter) {
1584
+ return this._assetSystem.createTexture(key, width, height, painter);
1585
+ }
1586
+ /**
1587
+ * Returns a loaded image or created texture previously registered with
1588
+ * {@link Game.loadImage} or {@link Game.createTexture}, or `undefined` if it
1589
+ * is not available.
1513
1590
  */
1514
1591
  getImage(key) {
1515
1592
  return this._assetSystem.getImage(key);
1516
1593
  }
1594
+ /** @internal */
1595
+ getImageVersion(key) {
1596
+ return this._assetSystem.getImageVersion(key);
1597
+ }
1517
1598
  /**
1518
- * Returns `true` if an image key has completed loading.
1599
+ * Returns `true` if a texture key is available.
1519
1600
  */
1520
1601
  hasImage(key) {
1521
1602
  return this._assetSystem.hasImage(key);
@@ -1557,7 +1638,7 @@ export class Game {
1557
1638
  *
1558
1639
  * @param listA - First group of sprites.
1559
1640
  * @param listB - Second group of sprites. May share sprites with `listA`.
1560
- * @returns A `[Sprite, Sprite]` tuple of the first overlapping pair,
1641
+ * @returns A `[EmojiSprite, EmojiSprite]` tuple of the first overlapping pair,
1561
1642
  * or `null` if no pair overlaps.
1562
1643
  *
1563
1644
  * @example
@@ -1613,7 +1694,7 @@ export class Game {
1613
1694
  *
1614
1695
  * @param listA - First group of sprites.
1615
1696
  * @param listB - Second group of sprites.
1616
- * @returns A `[Sprite, Sprite, CollisionInfo]` tuple for the first collision found,
1697
+ * @returns A `[EmojiSprite, EmojiSprite, CollisionInfo]` tuple for the first collision found,
1617
1698
  * or `null` if no pair overlaps.
1618
1699
  * @throws Error if the game's physics helpers are not enabled.
1619
1700
  *
@@ -1939,7 +2020,7 @@ export class Game {
1939
2020
  * normalized pivot point.
1940
2021
  *
1941
2022
  * This affects rendering only. Physics and collision bounds continue to use
1942
- * {@link Sprite.displaySize} and ignore the deform result.
2023
+ * {@link EmojiSprite.displaySize} and ignore the deform result.
1943
2024
  *
1944
2025
  * `pivotX` / `pivotY` are normalized anchors in the sprite's square display box:
1945
2026
  * - `0` = left/top
@@ -2347,7 +2428,7 @@ export class Game {
2347
2428
  * bounds, hit testing, fixed sizes, backgrounds, borders, or text stroke.
2348
2429
  * `drawText()` is the lightweight overlay API, not the primary UI API.
2349
2430
  *
2350
- * For controls that are only a single emoji, prefer {@link Sprite} over
2431
+ * For controls that are only a single emoji, prefer {@link EmojiSprite} over
2351
2432
  * {@link TextSprite}. Emoji-only buttons do not benefit from text padding and
2352
2433
  * usually fit more naturally in the sprite pipeline.
2353
2434
  *
@@ -2483,7 +2564,7 @@ export class Game {
2483
2564
  * ```ts
2484
2565
  * class GameOverScene {
2485
2566
  * onCreate() {
2486
- * const skull = new Sprite("💀", 400, 300, 96);
2567
+ * const skull = new EmojiSprite("💀", 400, 300, 96);
2487
2568
  * game.add(skull);
2488
2569
  * game.addTimer(3000, false, () => game.reset());
2489
2570
  * }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "minimojs",
3
- "version": "1.0.0-alpha.20",
4
- "description": "MinimoJS v1 — ultra-minimal, flat, deterministic 2D web game engine. Emoji-only sprites, rAF loop, TypeScript-first, LLM-friendly.",
3
+ "version": "1.0.0-alpha.21",
4
+ "description": "MinimoJS v1 — ultra-minimal, flat, deterministic 2D web game engine. Image-first sprites with emoji as a zero-asset alternative, rAF loop, TypeScript-first, LLM-friendly.",
5
5
  "type": "module",
6
6
  "main": "dist/minimo.js",
7
7
  "types": "dist/minimo.d.ts",
@@ -28,6 +28,7 @@
28
28
  "engine",
29
29
  "2d",
30
30
  "canvas",
31
+ "sprites",
31
32
  "emoji",
32
33
  "minimal",
33
34
  "typescript"