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/README.md +4 -1
- package/dist/internal/AssetSystem.js +40 -8
- package/dist/internal/RenderSystem.js +104 -21
- package/dist/minimo-ambient.d.ts +299 -0
- package/dist/minimo-ambient.js +1445 -0
- package/dist/minimo.d.ts +96 -26
- package/dist/minimo.js +270 -86
- package/package.json +1 -1
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
|
-
|
|
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
|
-
* ##
|
|
875
|
+
* ## Font Setup
|
|
870
876
|
*
|
|
871
|
-
* `drawText()` and `TextSprite` default to `
|
|
872
|
-
*
|
|
873
|
-
*
|
|
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
|
-
*
|
|
1317
|
-
*
|
|
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
|
|
1320
|
-
*
|
|
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 `
|
|
2419
|
-
*
|
|
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
|
|
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.
|
|
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,
|
|
2632
|
-
*
|
|
2633
|
-
*
|
|
2634
|
-
*
|
|
2635
|
-
*
|
|
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
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2724
|
-
|
|
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.
|
|
2754
|
-
this.
|
|
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.
|
|
2763
|
-
this.
|
|
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.
|
|
2773
|
-
this.
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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",
|