minimojs 1.0.0-alpha.22 → 1.0.0-alpha.24

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
@@ -22,6 +22,8 @@ import { TextSystem } from "./internal/TextSystem.js";
22
22
  import { TrailSystem } from "./internal/TrailSystem.js";
23
23
  import { TransitionSystem } from "./internal/TransitionSystem.js";
24
24
  import { TimerSystem } from "./internal/TimerSystem.js";
25
+ /** How long a scene may load before its progress ring appears, so a fast load shows none. */
26
+ const SCENE_LOADING_INDICATOR_DELAY_MS = 250;
25
27
  // ---------------------------------------------------------------------------
26
28
  // EmojiSprite
27
29
  // ---------------------------------------------------------------------------
@@ -535,7 +537,11 @@ export class TextSprite extends Sprite {
535
537
  super();
536
538
  this.anchorX = "center";
537
539
  this.anchorY = "middle";
538
- this.fontFamily = '"Press Start 2P", monospace';
540
+ /**
541
+ * CSS font family. Default: `monospace`, which needs no download. Name a web
542
+ * font here, and register it with {@link Game.requireFont}, to use one.
543
+ */
544
+ this.fontFamily = "monospace";
539
545
  this.fontWeight = "400";
540
546
  this.lineHeight = 1.2;
541
547
  this.maxWidth = 0;
@@ -866,12 +872,12 @@ export class BackgroundLayer {
866
872
  *
867
873
  * ---
868
874
  *
869
- * ## Required Font Setup
875
+ * ## Font Setup
870
876
  *
871
- * `drawText()` and `TextSprite` default to `"Press Start 2P", monospace`.
872
- * MinimoJS automatically waits for that default family before the first frame.
873
- * If you use additional families, register them with {@link Game.requireFont}
874
- * before calling {@link Game.start}.
877
+ * `drawText()` and `TextSprite` default to `monospace`, which every device
878
+ * has, so a game that names no font downloads none and waits for none. To use
879
+ * a web font, declare it in your page, register it with {@link Game.requireFont}
880
+ * before calling {@link Game.start}, and name it in {@link TextSprite.fontFamily}.
875
881
  *
876
882
  * Prefer {@link TextSprite} for persistent UI text, interactive labels,
877
883
  * buttons, fixed-size text boxes, and styled text elements. Keep
@@ -1091,6 +1097,15 @@ export class BackgroundLayer {
1091
1097
  * Legacy `game.onCreate` / `game.onUpdate` callbacks still work when no active
1092
1098
  * {@link IScene} is set.
1093
1099
  *
1100
+ * ### Loading a Scene's Own Images
1101
+ *
1102
+ * Load images in {@link Game.onPreload}: once, at boot. Only a game with many
1103
+ * scenes that each carry many assets of their own — typically a game with many
1104
+ * levels, whose player does not reach every level in a session — should give
1105
+ * scenes an {@link IScene.onPreload}. Entering such a scene loads its images
1106
+ * first, with the scene being left frozen under a progress ring, so `reset()`
1107
+ * and `transitionTo()` may return before the new scene's `onCreate()` runs.
1108
+ *
1094
1109
  * ---
1095
1110
  *
1096
1111
  * ## EmojiSprite Lifecycle Ownership
@@ -1210,6 +1225,15 @@ export class Game {
1210
1225
  get isTransitioning() {
1211
1226
  return this._transitionSystem.isActive;
1212
1227
  }
1228
+ /**
1229
+ * Returns `true` while images are loading: at boot, or while a scene with
1230
+ * pending images in its {@link IScene.onPreload} is being entered. While a
1231
+ * scene loads, the scene being left is frozen and `reset()` and
1232
+ * `transitionTo()` are ignored.
1233
+ */
1234
+ get isLoading() {
1235
+ return this._preloadPromise !== null || this._sceneLoad !== null;
1236
+ }
1213
1237
  // -------------------------------------------------------------------------
1214
1238
  // Constructor
1215
1239
  // -------------------------------------------------------------------------
@@ -1238,6 +1262,7 @@ export class Game {
1238
1262
  /** @internal */ this._isRegisteringPreloadAssets = false;
1239
1263
  /** @internal */ this._hasCompletedPreload = false;
1240
1264
  /** @internal */ this._preloadPromise = null;
1265
+ /** @internal */ this._sceneLoad = null;
1241
1266
  /** @internal */ this._onCreate = null;
1242
1267
  /** @internal */ this._currentScene = null;
1243
1268
  /**
@@ -1313,11 +1338,15 @@ export class Game {
1313
1338
  /**
1314
1339
  * Asset registration callback invoked once before the first scene is created.
1315
1340
  *
1316
- * Use this to register images with {@link Game.loadImage}. The callback is
1317
- * synchronous: it should only queue assets, not await network work.
1341
+ * This is the normal place to load a game's images. Use it to register them
1342
+ * with {@link Game.loadImage}. The callback is synchronous: it should only
1343
+ * queue assets, not await network work.
1318
1344
  *
1319
- * After `onPreload` returns, MinimoJS loads the queued images automatically
1320
- * and shows a default loading screen until they are ready.
1345
+ * After `onPreload` returns, MinimoJS loads the queued images, together with
1346
+ * those of the first scene's {@link IScene.onPreload} if it has one, and
1347
+ * shows a loading line until they are ready. If any image fails, it asks the
1348
+ * player with `window.confirm` whether to retry them; declining leaves the
1349
+ * game on the loading screen and reports the failed images on the console.
1321
1350
  *
1322
1351
  * @example
1323
1352
  * ```ts
@@ -1376,9 +1405,6 @@ export class Game {
1376
1405
  this._loopSystem = new LoopSystem(this._onLoopFrameCallback.bind(this));
1377
1406
  this._loopSystem.onCreate = this._invokeCreate.bind(this);
1378
1407
  this._inputSystem.bindInputEvents();
1379
- this.requireFont('"Press Start 2P"', {
1380
- sampleText: "Loading... SCORE LIVES GAME OVER YOU WIN",
1381
- });
1382
1408
  this.applyGlobalFontRequirements();
1383
1409
  }
1384
1410
  // -------------------------------------------------------------------------
@@ -1537,7 +1563,8 @@ export class Game {
1537
1563
  * This method registers the asset only. The actual network/image fetch begins
1538
1564
  * after `onPreload` returns and is managed automatically by the engine.
1539
1565
  *
1540
- * `loadImage` may only be called from within {@link Game.onPreload}.
1566
+ * `loadImage` may only be called from within {@link Game.onPreload} or a
1567
+ * scene's {@link IScene.onPreload}.
1541
1568
  *
1542
1569
  * @param key - Stable key used later by {@link BackgroundLayer.imageKey} or {@link Game.getImage}.
1543
1570
  * @param src - Image URL or relative path.
@@ -1551,7 +1578,7 @@ export class Game {
1551
1578
  */
1552
1579
  loadImage(key, src) {
1553
1580
  if (!this._isRegisteringPreloadAssets) {
1554
- throw new Error("MinimoJS: loadImage() may only be called inside game.onPreload.");
1581
+ throw new Error("MinimoJS: loadImage() may only be called inside game.onPreload or a scene's onPreload().");
1555
1582
  }
1556
1583
  this._assetSystem.queueImage(key, src);
1557
1584
  }
@@ -2415,14 +2442,8 @@ export class Game {
2415
2442
  * The text overlay list is cleared after each render. Call this inside `onUpdate`.
2416
2443
  *
2417
2444
  * **Layer:** Text is always drawn on top of all sprites.
2418
- * **Font:** Text uses `"Press Start 2P", monospace` by default.
2419
- * You MUST declare that font yourself in `index.html` (for example via Google
2420
- * Fonts) before calling {@link Game.start}. MinimoJS does NOT inject or
2421
- * download external fonts for you.
2422
- *
2423
- * `Game.start()` waits for the default font and for any extra families you
2424
- * registered with {@link Game.requireFont}. If a font is unavailable, the
2425
- * browser falls back to `monospace`.
2445
+ * **Font:** Text always uses `monospace`. Use {@link TextSprite} for any
2446
+ * other family.
2426
2447
  *
2427
2448
  * Prefer {@link TextSprite} for UI buttons or labels that need persistent
2428
2449
  * bounds, hit testing, fixed sizes, backgrounds, borders, or text stroke.
@@ -2558,6 +2579,12 @@ export class Game {
2558
2579
  * `onCreate()` (or legacy {@link Game.onCreate} if no scene is active) so it
2559
2580
  * can rebuild synchronously.
2560
2581
  *
2582
+ * The exception is a scene whose {@link IScene.onPreload} registers images
2583
+ * that are not loaded yet. Then the current scene freezes under a progress
2584
+ * ring, nothing is cleared until the images load, and `reset()` returns
2585
+ * before the scene is rebuilt; its `onCreate()` runs once they have loaded.
2586
+ * A `reset()` called while a scene is loading is ignored.
2587
+ *
2561
2588
  * @param scene - Optional new scene to make active before rebuilding.
2562
2589
  *
2563
2590
  * @example
@@ -2574,6 +2601,31 @@ export class Game {
2574
2601
  * ```
2575
2602
  */
2576
2603
  reset(scene) {
2604
+ if (this._sceneLoad !== null) {
2605
+ return;
2606
+ }
2607
+ if (!this._hasCompletedPreload) {
2608
+ if (scene !== undefined) {
2609
+ this._currentScene = scene;
2610
+ }
2611
+ // Boot builds the active scene once its images load, so a scene chosen
2612
+ // now is only recorded. Before start(), a scene with images of its own
2613
+ // waits for start() to load them.
2614
+ if (this._preloadPromise !== null || this._currentScene?.onPreload) {
2615
+ return;
2616
+ }
2617
+ this._resetNow();
2618
+ return;
2619
+ }
2620
+ this._registerSceneAssets(scene ?? this._currentScene);
2621
+ if (this._assetSystem.getQueuedImageCount() === 0) {
2622
+ this._resetNow(scene);
2623
+ return;
2624
+ }
2625
+ this._beginSceneLoad(() => this._resetNow(scene));
2626
+ }
2627
+ /** @internal */
2628
+ _resetNow(scene) {
2577
2629
  if (scene !== undefined) {
2578
2630
  this._currentScene = scene;
2579
2631
  }
@@ -2601,7 +2653,17 @@ export class Game {
2601
2653
  *
2602
2654
  * While the transition is active, gameplay updates are paused.
2603
2655
  *
2604
- * If the game loop is not running yet, this falls back to an immediate
2656
+ * If the target scene's {@link IScene.onPreload} registers images that are
2657
+ * not loaded yet, the current scene first freezes under a progress ring
2658
+ * until they load, and the transition plays after that. `transitionTo()`
2659
+ * then returns before the target scene exists; continue in its
2660
+ * `onCreate()` or in `onComplete`. If the player declines to retry failed
2661
+ * images, the game stays on the current scene and `onComplete` is not
2662
+ * called.
2663
+ *
2664
+ * A call made while a transition plays or a scene loads is ignored.
2665
+ *
2666
+ * If the game loop is not running yet, this falls back to
2605
2667
  * {@link Game.reset}.
2606
2668
  *
2607
2669
  * @param scene - Target scene to make active.
@@ -2614,11 +2676,20 @@ export class Game {
2614
2676
  onComplete?.();
2615
2677
  return;
2616
2678
  }
2617
- if (this._transitionSystem.isActive) {
2679
+ if (this._transitionSystem.isActive || this._sceneLoad !== null) {
2618
2680
  return;
2619
2681
  }
2682
+ this._registerSceneAssets(scene);
2683
+ if (this._assetSystem.getQueuedImageCount() === 0) {
2684
+ this._transitionNow(scene, options, onComplete);
2685
+ return;
2686
+ }
2687
+ this._beginSceneLoad(() => this._transitionNow(scene, options, onComplete));
2688
+ }
2689
+ /** @internal */
2690
+ _transitionNow(scene, options, onComplete) {
2620
2691
  const fromCanvas = this._captureFrameSnapshot();
2621
- this.reset(scene);
2692
+ this._resetNow(scene);
2622
2693
  const toCanvas = this._captureFrameSnapshot();
2623
2694
  this._transitionSystem.start(fromCanvas, toCanvas, this.width, this.height, options, onComplete);
2624
2695
  }
@@ -2628,12 +2699,11 @@ export class Game {
2628
2699
  /**
2629
2700
  * Starts the `requestAnimationFrame` game loop.
2630
2701
  * Safe to call multiple times — does nothing if already running.
2631
- * On the first start, if {@link Game.onPreload} is set, MinimoJS first runs
2632
- * asset registration, loads queued images, waits for all fonts registered via
2633
- * {@link Game.requireFont}, and only then, if images were queued, shows a
2634
- * default loading screen while those image assets are still loading.
2635
- * After preload completes, the active scene's `onCreate()` is called before
2636
- * the first frame.
2702
+ * On the first start, MinimoJS runs {@link Game.onPreload} and the active
2703
+ * scene's {@link IScene.onPreload}, loads the images they queued while
2704
+ * showing a thin loading line, and waits for all fonts registered via
2705
+ * {@link Game.requireFont}. After that, the active scene's `onCreate()` is
2706
+ * called before the first frame.
2637
2707
  *
2638
2708
  * The loop calls the active scene's `onUpdate(dt)` (or legacy
2639
2709
  * {@link Game.onUpdate}) once per frame, then renders all sprites and text
@@ -2680,51 +2750,57 @@ export class Game {
2680
2750
  this._isRegisteringPreloadAssets = false;
2681
2751
  }
2682
2752
  }
2683
- const totalAssets = this._assetSystem.getQueuedImageCount();
2684
- let fontsReady = false;
2685
- let loadingLoaded = 0;
2686
- let loadingCurrentKey = null;
2687
- const renderLoadingProgress = () => {
2688
- if (!fontsReady || totalAssets === 0) {
2753
+ const preloadPromise = Promise.all([
2754
+ this._loadBootImages(),
2755
+ this.waitForDocumentFonts(),
2756
+ ]).then(([loaded]) => {
2757
+ if (this._preloadPromise !== preloadPromise)
2689
2758
  return;
2690
- }
2691
- this._renderLoadingScreen(loadingLoaded, totalAssets, loadingCurrentKey);
2692
- };
2693
- const fontReadyPromise = this.waitForDocumentFonts().then(() => {
2694
- fontsReady = true;
2695
- renderLoadingProgress();
2696
- });
2697
- if (totalAssets === 0) {
2698
- this._preloadPromise = fontReadyPromise
2699
- .then(() => {
2700
- this._hasCompletedPreload = true;
2701
- this._preloadPromise = null;
2702
- this._loopSystem.start();
2703
- })
2704
- .catch((error) => {
2705
- this._preloadPromise = null;
2706
- throw error;
2707
- });
2708
- return;
2709
- }
2710
- const assetPromise = this._assetSystem.loadQueuedImages((loaded, total, currentKey) => {
2711
- loadingLoaded = loaded;
2712
- loadingCurrentKey = currentKey;
2713
- if (!fontsReady)
2759
+ this._preloadPromise = null;
2760
+ if (!loaded)
2714
2761
  return;
2715
- this._renderLoadingScreen(loaded, total, currentKey);
2716
- });
2717
- this._preloadPromise = Promise.all([assetPromise, fontReadyPromise])
2718
- .then(() => {
2719
2762
  this._hasCompletedPreload = true;
2720
- this._preloadPromise = null;
2721
2763
  this._loopSystem.start();
2722
- })
2723
- .catch((error) => {
2724
- this._preloadPromise = null;
2764
+ });
2765
+ this._preloadPromise = preloadPromise;
2766
+ preloadPromise.catch((error) => {
2767
+ if (this._preloadPromise === preloadPromise) {
2768
+ this._preloadPromise = null;
2769
+ }
2725
2770
  throw error;
2726
2771
  });
2727
2772
  }
2773
+ /**
2774
+ * Loads the boot images, then those of any scene made active while they
2775
+ * loaded, retrying failures while the player agrees. Resolves `false` when
2776
+ * the player declines, which leaves the game on the loading screen.
2777
+ *
2778
+ * @internal
2779
+ */
2780
+ async _loadBootImages() {
2781
+ let registeredScene = undefined;
2782
+ for (;;) {
2783
+ if (registeredScene !== this._currentScene) {
2784
+ registeredScene = this._currentScene;
2785
+ this._registerSceneAssets(registeredScene);
2786
+ }
2787
+ const failed = await this._assetSystem.loadQueuedImages((settled, total) => {
2788
+ if (total > 0)
2789
+ this._renderLoadingScreen(settled / total);
2790
+ });
2791
+ if (failed.length > 0) {
2792
+ this._reportFailedImages(failed);
2793
+ if (!this._confirmImageRetry(failed.length)) {
2794
+ this._assetSystem.discardQueuedImages();
2795
+ return false;
2796
+ }
2797
+ continue;
2798
+ }
2799
+ if (registeredScene === this._currentScene) {
2800
+ return true;
2801
+ }
2802
+ }
2803
+ }
2728
2804
  /**
2729
2805
  * Stops the game loop. The canvas retains its last rendered frame.
2730
2806
  * Call {@link Game.start} to resume.
@@ -2737,6 +2813,10 @@ export class Game {
2737
2813
  // -------------------------------------------------------------------------
2738
2814
  /** @internal */ _onLoopFrameCallback(dt, dtMs) {
2739
2815
  this._renderSystem.beginDynamicSurfacePass();
2816
+ if (this._sceneLoad !== null) {
2817
+ this._renderInterruption();
2818
+ return;
2819
+ }
2740
2820
  if (this._transitionSystem.isActive) {
2741
2821
  this._transitionSystem.update(dtMs);
2742
2822
  if (this._transitionSystem.isActive) {
@@ -2750,29 +2830,23 @@ export class Game {
2750
2830
  return;
2751
2831
  }
2752
2832
  this._timerSystem.update(dtMs);
2753
- if (this._transitionSystem.isActive) {
2754
- this._renderTransition();
2755
- this._inputSystem.clearFramePressedState();
2756
- this._textSystem.clear();
2833
+ if (this._isSceneChangeInterrupting()) {
2834
+ this._renderInterruption();
2757
2835
  return;
2758
2836
  }
2759
2837
  this._animationSystem.update(dtMs);
2760
2838
  this._explosionSystem.update(dt, dtMs);
2761
2839
  this._physicsSystem.update(this._spriteSystem.getMutableSprites(), dt);
2762
- if (this._transitionSystem.isActive) {
2763
- this._renderTransition();
2764
- this._inputSystem.clearFramePressedState();
2765
- this._textSystem.clear();
2840
+ if (this._isSceneChangeInterrupting()) {
2841
+ this._renderInterruption();
2766
2842
  return;
2767
2843
  }
2768
2844
  if (this._currentScene?.onUpdate)
2769
2845
  this._currentScene.onUpdate(dt);
2770
2846
  else if (this.onUpdate)
2771
2847
  this.onUpdate(dt);
2772
- if (this._transitionSystem.isActive) {
2773
- this._renderTransition();
2774
- this._inputSystem.clearFramePressedState();
2775
- this._textSystem.clear();
2848
+ if (this._isSceneChangeInterrupting()) {
2849
+ this._renderInterruption();
2776
2850
  return;
2777
2851
  }
2778
2852
  this._trailSystem.update(dtMs, this._renderSystem.getSpriteRenderSnapshot.bind(this._renderSystem));
@@ -2781,6 +2855,118 @@ export class Game {
2781
2855
  this._textSystem.clear();
2782
2856
  }
2783
2857
  /** @internal */
2858
+ _isSceneChangeInterrupting() {
2859
+ return this._transitionSystem.isActive || this._sceneLoad !== null;
2860
+ }
2861
+ /**
2862
+ * Draws a frame in which gameplay does not advance: a loading scene under
2863
+ * its ring, or a transition.
2864
+ *
2865
+ * @internal
2866
+ */
2867
+ _renderInterruption() {
2868
+ const load = this._sceneLoad;
2869
+ if (load !== null) {
2870
+ this._renderSystem.renderSceneLoading({
2871
+ canvas: this._canvas,
2872
+ context: this._ctx,
2873
+ pageBackground: this.pageBackground,
2874
+ snapshot: load.snapshot,
2875
+ progress: load.progress,
2876
+ showIndicator: performance.now() - load.startedAt >= SCENE_LOADING_INDICATOR_DELAY_MS,
2877
+ ink: load.ink,
2878
+ });
2879
+ }
2880
+ else {
2881
+ this._renderTransition();
2882
+ }
2883
+ this._inputSystem.clearFramePressedState();
2884
+ this._textSystem.clear();
2885
+ }
2886
+ /**
2887
+ * Runs a scene's `onPreload()`, if it has one, with image registration
2888
+ * allowed.
2889
+ *
2890
+ * @internal
2891
+ */
2892
+ _registerSceneAssets(scene) {
2893
+ if (!scene?.onPreload)
2894
+ return;
2895
+ this._isRegisteringPreloadAssets = true;
2896
+ try {
2897
+ scene.onPreload();
2898
+ }
2899
+ finally {
2900
+ this._isRegisteringPreloadAssets = false;
2901
+ }
2902
+ }
2903
+ /**
2904
+ * Freezes the current scene under a progress ring while the queued images
2905
+ * load, then calls `finish` to enter the next scene. If images fail and the
2906
+ * player declines to retry, the current scene resumes instead.
2907
+ *
2908
+ * @internal
2909
+ */
2910
+ _beginSceneLoad(finish) {
2911
+ this._transitionSystem.clear();
2912
+ const snapshot = document.createElement("canvas");
2913
+ snapshot.width = this._canvas.width;
2914
+ snapshot.height = this._canvas.height;
2915
+ snapshot.getContext("2d")?.drawImage(this._canvas, 0, 0);
2916
+ const load = {
2917
+ snapshot,
2918
+ ink: this._renderSystem.pickLoadingInk(snapshot),
2919
+ progress: 0,
2920
+ startedAt: performance.now(),
2921
+ };
2922
+ this._sceneLoad = load;
2923
+ void this._loadSceneImages(load, finish);
2924
+ }
2925
+ /** @internal */
2926
+ async _loadSceneImages(load, finish) {
2927
+ for (;;) {
2928
+ const failed = await this._assetSystem.loadQueuedImages((settled, total) => {
2929
+ load.progress = total === 0 ? 1 : settled / total;
2930
+ });
2931
+ if (failed.length === 0)
2932
+ break;
2933
+ this._reportFailedImages(failed);
2934
+ if (!this._confirmImageRetry(failed.length)) {
2935
+ this._assetSystem.discardQueuedImages();
2936
+ this._sceneLoad = null;
2937
+ return;
2938
+ }
2939
+ load.progress = 0;
2940
+ }
2941
+ this._sceneLoad = null;
2942
+ finish();
2943
+ }
2944
+ /** @internal */
2945
+ _reportFailedImages(keys) {
2946
+ const list = keys
2947
+ .map((key) => `"${key}" (${this._assetSystem.getImageSource(key) ?? "no source"})`)
2948
+ .join(", ");
2949
+ console.error(`MinimoJS: ${keys.length} image${keys.length === 1 ? "" : "s"} failed to load: ${list}.`);
2950
+ }
2951
+ /**
2952
+ * Asks the player whether to retry failed images, with the browser's own
2953
+ * confirmation dialog. Returns `false` where no dialog can be shown.
2954
+ *
2955
+ * @internal
2956
+ */
2957
+ _confirmImageRetry(failedCount) {
2958
+ if (typeof window === "undefined" || typeof window.confirm !== "function") {
2959
+ return false;
2960
+ }
2961
+ const noun = failedCount === 1 ? "image" : "images";
2962
+ try {
2963
+ return window.confirm(`${failedCount} ${noun} could not be loaded. Try again?`);
2964
+ }
2965
+ catch {
2966
+ return false;
2967
+ }
2968
+ }
2969
+ /** @internal */
2784
2970
  _invokeCreate() {
2785
2971
  if (this._currentScene?.onCreate)
2786
2972
  this._currentScene.onCreate();
@@ -2902,16 +3088,14 @@ export class Game {
2902
3088
  });
2903
3089
  }
2904
3090
  /** @internal */
2905
- _renderLoadingScreen(loaded, total, currentKey) {
3091
+ _renderLoadingScreen(progress) {
2906
3092
  this._renderSystem.renderLoadingScreen({
2907
3093
  canvas: this._canvas,
2908
3094
  context: this._ctx,
2909
3095
  pageBackground: this.pageBackground,
2910
3096
  background: this.background,
2911
3097
  backgroundGradient: this.backgroundGradient,
2912
- loaded,
2913
- total,
2914
- currentKey,
3098
+ progress,
2915
3099
  });
2916
3100
  }
2917
3101
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minimojs",
3
- "version": "1.0.0-alpha.22",
3
+ "version": "1.0.0-alpha.24",
4
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",