@energy8platform/game-engine 0.34.2 → 0.35.0
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/audio.cjs.js +114 -59
- package/dist/audio.cjs.js.map +1 -1
- package/dist/audio.d.ts +25 -0
- package/dist/audio.esm.js +114 -59
- package/dist/audio.esm.js.map +1 -1
- package/dist/core.cjs.js +222 -66
- package/dist/core.cjs.js.map +1 -1
- package/dist/core.d.ts +25 -0
- package/dist/core.esm.js +223 -67
- package/dist/core.esm.js.map +1 -1
- package/dist/flow.cjs.js +246 -0
- package/dist/flow.cjs.js.map +1 -1
- package/dist/flow.d.ts +192 -33
- package/dist/flow.esm.js +238 -1
- package/dist/flow.esm.js.map +1 -1
- package/dist/host.cjs.js +343 -82
- package/dist/host.cjs.js.map +1 -1
- package/dist/host.d.ts +82 -2
- package/dist/host.esm.js +344 -83
- package/dist/host.esm.js.map +1 -1
- package/dist/index.cjs.js +222 -66
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +72 -0
- package/dist/index.esm.js +223 -67
- package/dist/index.esm.js.map +1 -1
- package/dist/scene-devtools.cjs.js +529 -115
- package/dist/scene-devtools.cjs.js.map +1 -1
- package/dist/scene-devtools.d.ts +187 -34
- package/dist/scene-devtools.esm.js +529 -115
- package/dist/scene-devtools.esm.js.map +1 -1
- package/dist/scene.cjs.js +704 -46
- package/dist/scene.cjs.js.map +1 -1
- package/dist/scene.d.ts +228 -41
- package/dist/scene.esm.js +698 -47
- package/dist/scene.esm.js.map +1 -1
- package/package.json +2 -2
- package/src/audio/AudioManager.ts +111 -53
- package/src/core/GameApplication.ts +47 -5
- package/src/host/buildConfig.ts +17 -4
- package/src/host/createSlotGame.ts +114 -12
- package/src/host/index.ts +3 -0
- package/src/host/types.ts +58 -0
- package/src/loading/LoadingScene.ts +76 -2
- package/src/loading/index.ts +6 -0
- package/src/types.ts +2 -0
package/dist/index.esm.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Ticker, Assets, Container, Text, Application, AnimatedSprite, Texture } from 'pixi.js';
|
|
2
2
|
import { createPlatformSession } from '@energy8platform/platform-core';
|
|
3
|
-
import { waitCSSPreloaderTap, removeCSSPreloader,
|
|
3
|
+
import { waitCSSPreloaderTap, removeCSSPreloader, hasExternalOverlay, externalOverlayHold, createCSSPreloader, releaseExternalOverlay, setCSSPreloaderProgress, adoptExternalOverlay, advanceExternalOverlay } from '@energy8platform/platform-core/loading';
|
|
4
4
|
export { BridgeDestroyedError, BridgeNotReadyError, SDKError, TimeoutError } from '@energy8platform/game-sdk';
|
|
5
5
|
export { DevBridge } from '@energy8platform/platform-core/dev-bridge';
|
|
6
6
|
|
|
@@ -751,6 +751,12 @@ class AudioManager {
|
|
|
751
751
|
_categories;
|
|
752
752
|
_masterGain = 1.0;
|
|
753
753
|
_currentMusic = null;
|
|
754
|
+
/** Duck factor (0..1) from duckMusic/unduckMusic. A presentation state, not a player setting. */
|
|
755
|
+
_musicDuck = 1;
|
|
756
|
+
/** Crossfade ramp (0..1) for the track that is fading IN. 1 whenever no fade is running. */
|
|
757
|
+
_musicFade = 1;
|
|
758
|
+
/** Generation counter so a superseded crossfade ramp stops writing over the new track's. */
|
|
759
|
+
_musicFadeToken = 0;
|
|
754
760
|
_unlocked = false;
|
|
755
761
|
_unlockHandler = null;
|
|
756
762
|
constructor(config) {
|
|
@@ -809,7 +815,10 @@ class AudioManager {
|
|
|
809
815
|
if (this._globalMuted || this._categories[category].muted)
|
|
810
816
|
return;
|
|
811
817
|
const { sound } = this._soundModule;
|
|
812
|
-
|
|
818
|
+
// The master gain lives on the GLOBAL bus (`sound.volumeAll`, see applyVolumes) and @pixi/sound
|
|
819
|
+
// already multiplies it in — folding it in here as well squared it, so a master of 0.5 played
|
|
820
|
+
// sfx at 0.25.
|
|
821
|
+
const vol = (options?.volume ?? 1) * this._categories[category].volume;
|
|
813
822
|
try {
|
|
814
823
|
sound.play(alias, {
|
|
815
824
|
volume: vol,
|
|
@@ -831,52 +840,50 @@ class AudioManager {
|
|
|
831
840
|
if (!this._initialized || !this._soundModule)
|
|
832
841
|
return;
|
|
833
842
|
const { sound } = this._soundModule;
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
});
|
|
847
|
-
// Start new track at zero volume, fade in
|
|
848
|
-
try {
|
|
849
|
-
sound.play(alias, {
|
|
850
|
-
volume: 0,
|
|
851
|
-
loop: true,
|
|
843
|
+
const prevAlias = this._currentMusic;
|
|
844
|
+
const crossfade = !!prevAlias && prevAlias !== alias && fadeDuration > 0;
|
|
845
|
+
// Retire the outgoing track. Its own SOUND-level volume is the only thing still pointing at it,
|
|
846
|
+
// so fading that to 0 is safe — nothing else writes it once `_currentMusic` has moved on.
|
|
847
|
+
if (prevAlias) {
|
|
848
|
+
if (crossfade) {
|
|
849
|
+
const from = this.soundVolumeOf(prevAlias);
|
|
850
|
+
this.fadeVolume(prevAlias, from, 0, fadeDuration, () => {
|
|
851
|
+
try {
|
|
852
|
+
sound.stop(prevAlias);
|
|
853
|
+
}
|
|
854
|
+
catch { /* ignore */ }
|
|
852
855
|
});
|
|
853
|
-
this.fadeVolume(alias, 0, this._categories.music.volume * this._masterGain, fadeDuration);
|
|
854
|
-
}
|
|
855
|
-
catch (e) {
|
|
856
|
-
console.warn(`[AudioManager] Failed to play music "${alias}":`, e);
|
|
857
856
|
}
|
|
858
|
-
|
|
859
|
-
else {
|
|
860
|
-
// No crossfade — instant switch
|
|
861
|
-
if (this._currentMusic) {
|
|
857
|
+
else {
|
|
862
858
|
try {
|
|
863
|
-
sound.stop(
|
|
859
|
+
sound.stop(prevAlias);
|
|
864
860
|
}
|
|
865
861
|
catch { /* ignore */ }
|
|
866
862
|
}
|
|
867
|
-
this._currentMusic = alias;
|
|
868
|
-
if (this._globalMuted || this._categories.music.muted)
|
|
869
|
-
return;
|
|
870
|
-
try {
|
|
871
|
-
sound.play(alias, {
|
|
872
|
-
volume: this._categories.music.volume * this._masterGain,
|
|
873
|
-
loop: true,
|
|
874
|
-
});
|
|
875
|
-
}
|
|
876
|
-
catch (e) {
|
|
877
|
-
console.warn(`[AudioManager] Failed to play music "${alias}":`, e);
|
|
878
|
-
}
|
|
879
863
|
}
|
|
864
|
+
this._currentMusic = alias;
|
|
865
|
+
this._musicFadeToken++; // any ramp still running belongs to a track we just replaced
|
|
866
|
+
// Deliberately started even while muted. Global mute is the @pixi/sound CONTEXT mute and a
|
|
867
|
+
// muted music category is a 0 term in `musicGain()` — both already make this inaudible, and
|
|
868
|
+
// both undo themselves the moment the player flips them back. Returning early here instead
|
|
869
|
+
// meant a track begun while muted never existed, so unmuting restored silence until some
|
|
870
|
+
// later mode change happened to switch tracks.
|
|
871
|
+
// The incoming track plays at INSTANCE volume 1 and carries its whole gain on the SOUND layer
|
|
872
|
+
// (`musicGain()`), which is the layer the slider, the duck and this fade all write. Splitting
|
|
873
|
+
// them across layers is what silenced every crossfade: the track was started at instance volume
|
|
874
|
+
// 0 and the ramp then moved the sound layer, whose product with 0 is 0 for the track's life.
|
|
875
|
+
// The gain is written BEFORE play() so the first frame is never at full volume.
|
|
876
|
+
this._musicFade = crossfade ? 0 : 1;
|
|
877
|
+
this.applyMusicGain();
|
|
878
|
+
try {
|
|
879
|
+
sound.play(alias, { volume: 1, loop: true });
|
|
880
|
+
}
|
|
881
|
+
catch (e) {
|
|
882
|
+
console.warn(`[AudioManager] Failed to play music "${alias}":`, e);
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
if (crossfade)
|
|
886
|
+
this.rampMusicFade(fadeDuration);
|
|
880
887
|
}
|
|
881
888
|
/**
|
|
882
889
|
* Stop current music.
|
|
@@ -892,6 +899,10 @@ class AudioManager {
|
|
|
892
899
|
// ignore
|
|
893
900
|
}
|
|
894
901
|
this._currentMusic = null;
|
|
902
|
+
// Retire any running ramp and clear the fade term, so the next track does not inherit a
|
|
903
|
+
// half-finished crossfade and start silent.
|
|
904
|
+
this._musicFadeToken++;
|
|
905
|
+
this._musicFade = 1;
|
|
895
906
|
}
|
|
896
907
|
/**
|
|
897
908
|
* Stop all sounds.
|
|
@@ -917,6 +928,9 @@ class AudioManager {
|
|
|
917
928
|
*/
|
|
918
929
|
setVolume(category, volume) {
|
|
919
930
|
this._categories[category].volume = Math.max(0, Math.min(1, volume));
|
|
931
|
+
// applyVolumes() re-pushes the music gain, so moving the Music slider is heard on the track
|
|
932
|
+
// that is ALREADY playing — it used to take effect only at the next playMusic (a mode change).
|
|
933
|
+
// SFX need no push: play() reads the category volume fresh on every call.
|
|
920
934
|
this.applyVolumes();
|
|
921
935
|
this.saveState();
|
|
922
936
|
}
|
|
@@ -989,30 +1003,19 @@ class AudioManager {
|
|
|
989
1003
|
* @param factor - Volume multiplier (0..1), e.g. 0.3 = 30% of normal
|
|
990
1004
|
*/
|
|
991
1005
|
duckMusic(factor) {
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
}
|
|
999
|
-
catch {
|
|
1000
|
-
// ignore
|
|
1001
|
-
}
|
|
1006
|
+
// Held as a FACTOR rather than written as a finished volume: the duck used to write
|
|
1007
|
+
// `category × factor` onto a track whose instance already carried the category volume, so it
|
|
1008
|
+
// ducked to category², and unducking restored category² instead of category. Keeping it as one
|
|
1009
|
+
// term of `musicGain()` also keeps the slider live while ducked.
|
|
1010
|
+
this._musicDuck = Math.max(0, Math.min(1, factor));
|
|
1011
|
+
this.applyMusicGain();
|
|
1002
1012
|
}
|
|
1003
1013
|
/**
|
|
1004
1014
|
* Restore music to normal volume after ducking.
|
|
1005
1015
|
*/
|
|
1006
1016
|
unduckMusic() {
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
const { sound } = this._soundModule;
|
|
1010
|
-
try {
|
|
1011
|
-
sound.volume(this._currentMusic, this._categories.music.volume);
|
|
1012
|
-
}
|
|
1013
|
-
catch {
|
|
1014
|
-
// ignore
|
|
1015
|
-
}
|
|
1017
|
+
this._musicDuck = 1;
|
|
1018
|
+
this.applyMusicGain();
|
|
1016
1019
|
}
|
|
1017
1020
|
/**
|
|
1018
1021
|
* Destroy the audio manager and free resources.
|
|
@@ -1051,6 +1054,57 @@ class AudioManager {
|
|
|
1051
1054
|
};
|
|
1052
1055
|
requestAnimationFrame(tick);
|
|
1053
1056
|
}
|
|
1057
|
+
/**
|
|
1058
|
+
* The SOUND-layer gain for the running music track.
|
|
1059
|
+
*
|
|
1060
|
+
* @pixi/sound resolves a playing instance as `instance × sound × global` (WebAudioInstance.
|
|
1061
|
+
* refresh). Each of those three has exactly ONE owner here, which is what keeps the mixer honest:
|
|
1062
|
+
* global — the master gain (`applyVolumes`)
|
|
1063
|
+
* sound — music: this function. sfx: untouched, left at 1.
|
|
1064
|
+
* instance — sfx: the per-call volume × the sfx category. music: always 1.
|
|
1065
|
+
* Everything that can move music volume — the player's slider, the category mute, a big-win duck,
|
|
1066
|
+
* a crossfade — is a term below, so they compose instead of overwriting each other.
|
|
1067
|
+
*/
|
|
1068
|
+
musicGain() {
|
|
1069
|
+
const c = this._categories.music;
|
|
1070
|
+
return (c.muted ? 0 : 1) * c.volume * this._musicDuck * this._musicFade;
|
|
1071
|
+
}
|
|
1072
|
+
/** Push `musicGain()` at the current track. Safe before it starts playing and with none playing. */
|
|
1073
|
+
applyMusicGain() {
|
|
1074
|
+
if (!this._soundModule || !this._currentMusic)
|
|
1075
|
+
return;
|
|
1076
|
+
try {
|
|
1077
|
+
this._soundModule.sound.volume(this._currentMusic, this.musicGain());
|
|
1078
|
+
}
|
|
1079
|
+
catch {
|
|
1080
|
+
// ignore — alias not registered yet
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
/** Current SOUND-layer volume of `alias`, or 0 when it cannot be read. */
|
|
1084
|
+
soundVolumeOf(alias) {
|
|
1085
|
+
try {
|
|
1086
|
+
return Number(this._soundModule.sound.volume(alias)) || 0;
|
|
1087
|
+
}
|
|
1088
|
+
catch {
|
|
1089
|
+
return 0;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
/** Ramp the crossfade term 0 → 1 over `durationMs`, recomposing the gain each frame so a slider
|
|
1093
|
+
* drag or a duck landing mid-fade is honoured rather than overwritten when the fade ends. */
|
|
1094
|
+
rampMusicFade(durationMs) {
|
|
1095
|
+
const token = this._musicFadeToken;
|
|
1096
|
+
const start = Date.now();
|
|
1097
|
+
const tick = () => {
|
|
1098
|
+
if (token !== this._musicFadeToken)
|
|
1099
|
+
return; // a newer track owns the music now
|
|
1100
|
+
const t = Math.min((Date.now() - start) / durationMs, 1);
|
|
1101
|
+
this._musicFade = t;
|
|
1102
|
+
this.applyMusicGain();
|
|
1103
|
+
if (t < 1)
|
|
1104
|
+
requestAnimationFrame(tick);
|
|
1105
|
+
};
|
|
1106
|
+
requestAnimationFrame(tick);
|
|
1107
|
+
}
|
|
1054
1108
|
applyVolumes() {
|
|
1055
1109
|
if (!this._soundModule)
|
|
1056
1110
|
return;
|
|
@@ -1058,6 +1112,7 @@ class AudioManager {
|
|
|
1058
1112
|
// Global mute is owned by sound.muteAll()/unmuteAll() (context.muted),
|
|
1059
1113
|
// not by volumeAll — mixing both leaves mute un-undoable after reload.
|
|
1060
1114
|
sound.volumeAll = this._masterGain; // master multiplies the global bus
|
|
1115
|
+
this.applyMusicGain(); // category volume/mute reach the RUNNING track
|
|
1061
1116
|
}
|
|
1062
1117
|
setupMobileUnlock() {
|
|
1063
1118
|
if (this._unlocked)
|
|
@@ -1539,6 +1594,14 @@ class Scene {
|
|
|
1539
1594
|
* tap-to-start → `waitCSSPreloaderTap`, then fades it out via
|
|
1540
1595
|
* `removeCSSPreloader` before entering the game. One continuous overlay from
|
|
1541
1596
|
* boot to gameplay — no second logo, no mid-load flash.
|
|
1597
|
+
*
|
|
1598
|
+
* When the game supplied its own overlay (`loading.externalOverlay`, e.g.
|
|
1599
|
+
* Artube's `LoaderViewController`), this scene is also the HAND-OVER point: that
|
|
1600
|
+
* overlay covered the gap this scene's existence ends — the bundle download,
|
|
1601
|
+
* Pixi init and the SDK handshake, none of which the engine can paint over. The
|
|
1602
|
+
* first thing `onEnter` does is mount the preloader, wait for it to be painted,
|
|
1603
|
+
* and dismiss the game's overlay. Everything after that line is identical on
|
|
1604
|
+
* every platform.
|
|
1542
1605
|
*/
|
|
1543
1606
|
class LoadingScene extends Scene {
|
|
1544
1607
|
_engine;
|
|
@@ -1556,6 +1619,10 @@ class LoadingScene extends Scene {
|
|
|
1556
1619
|
this._targetScene = targetScene;
|
|
1557
1620
|
this._targetData = targetData;
|
|
1558
1621
|
this._config = engine.config.loading ?? {};
|
|
1622
|
+
// Take the screen from a game-supplied loading overlay, if there is one. Before any awaited
|
|
1623
|
+
// work: from here on the player is looking at OUR loading screen, and `_startTime` (which
|
|
1624
|
+
// `minDisplayTime` is measured from) must start when that becomes true.
|
|
1625
|
+
await this.takeOverFromExternalOverlay();
|
|
1559
1626
|
this._startTime = Date.now();
|
|
1560
1627
|
// Initialize asset manager
|
|
1561
1628
|
await this._engine.assets.init();
|
|
@@ -1602,8 +1669,10 @@ class LoadingScene extends Scene {
|
|
|
1602
1669
|
// Final snap to 100%
|
|
1603
1670
|
this._displayedProgress = 1;
|
|
1604
1671
|
this.updateLoaderBar(1);
|
|
1605
|
-
// Wait for the player's tap — resolves immediately when tapToStart is
|
|
1606
|
-
//
|
|
1672
|
+
// Wait for the player's tap — resolves immediately when tapToStart is false — then enter the
|
|
1673
|
+
// game. This is the preloader's gate and it reads the preloader's config, so it means the same
|
|
1674
|
+
// thing on every target: a game-supplied overlay has no say in it, and by now no part in the
|
|
1675
|
+
// screen either. It was dismissed at the hand-over above; the player is looking at ours.
|
|
1607
1676
|
await waitCSSPreloaderTap();
|
|
1608
1677
|
await this.transitionToGame();
|
|
1609
1678
|
}
|
|
@@ -1622,6 +1691,59 @@ class LoadingScene extends Scene {
|
|
|
1622
1691
|
// (e.g. the scene was popped externally). Idempotent.
|
|
1623
1692
|
void removeCSSPreloader(this.hostElement());
|
|
1624
1693
|
}
|
|
1694
|
+
// ─── Hand-over from a game-supplied overlay ────────────
|
|
1695
|
+
/**
|
|
1696
|
+
* Swap a game-supplied loading overlay for the engine's own loading screen.
|
|
1697
|
+
*
|
|
1698
|
+
* The overlay (Artube's) has been on screen since before this bundle was fetched, covering a gap
|
|
1699
|
+
* nothing of ours could. Its job ends here, at the first frame the engine paints; the player then
|
|
1700
|
+
* gets the game's own brand, progress bar and tap-to-start, exactly as on every other target.
|
|
1701
|
+
*
|
|
1702
|
+
* The order of the four steps is the whole design, and each is wrong on its own:
|
|
1703
|
+
*
|
|
1704
|
+
* 0. Wait out whatever the overlay is still owed on screen (`externalOverlayMinDisplayTime`,
|
|
1705
|
+
* default 1.5s, plus room for a phase crossfade already in flight). The gap this overlay
|
|
1706
|
+
* covers can be under half a second, which is not long enough for a partner's brand to
|
|
1707
|
+
* register. Waiting here — BEFORE mounting ours — rather than after is what keeps the two
|
|
1708
|
+
* screens' timelines from overlapping: our splash and brand floor start when the player can
|
|
1709
|
+
* actually see them, not behind someone else's overlay. On any boot slower than the floor
|
|
1710
|
+
* this step costs nothing, and on a non-Artube target it is not reached at all.
|
|
1711
|
+
* 1. Mount the preloader, opaque and full-bleed, while theirs is still up. Both are on screen
|
|
1712
|
+
* together for a few frames, so there is never a moment with neither, whatever happens next.
|
|
1713
|
+
* 2. Wait for that frame to actually be PAINTED — mounting only queues it. Dismissing theirs
|
|
1714
|
+
* before the paint is precisely the flash of bare background this ordering exists to avoid.
|
|
1715
|
+
* Two `requestAnimationFrame`s: the first callback runs before the frame it belongs to is
|
|
1716
|
+
* composited, the second after. Two frames is also enough for Pixi's own rAF-driven ticker
|
|
1717
|
+
* to have rendered this scene at least once, so "the loading scene has painted" is literally
|
|
1718
|
+
* true by the time step 3 runs.
|
|
1719
|
+
* 3. Only then dismiss theirs. Their `hideLoader()` plays a 0.3s fade and removes the element.
|
|
1720
|
+
* Not waiting for that fade is deliberate — it is an animation on someone else's element,
|
|
1721
|
+
* and blocking a boot on it would be a hang waiting to happen.
|
|
1722
|
+
*
|
|
1723
|
+
* Which of the two is visually on top is the host page's business, not ours, and it does NOT
|
|
1724
|
+
* change the guarantee. On a typical game page (`#game { position: fixed; inset: 0 }`) the fixed
|
|
1725
|
+
* container establishes a stacking context, so the preloader's z-index is scoped inside it and
|
|
1726
|
+
* Artube's `position: fixed; z-index: 9999` sits above — their fade then crossfades onto our
|
|
1727
|
+
* loading screen, which is what was observed live and looks right. On a page where ours wins
|
|
1728
|
+
* instead, their fade simply plays underneath, unseen. Either way the seam is covered, because
|
|
1729
|
+
* what step 2 buys is that OUR screen is already painted before theirs starts going away.
|
|
1730
|
+
*/
|
|
1731
|
+
async takeOverFromExternalOverlay() {
|
|
1732
|
+
if (!hasExternalOverlay())
|
|
1733
|
+
return;
|
|
1734
|
+
await externalOverlayHold();
|
|
1735
|
+
createCSSPreloader(this.hostElement(), this._config);
|
|
1736
|
+
await this.nextPaint();
|
|
1737
|
+
releaseExternalOverlay();
|
|
1738
|
+
}
|
|
1739
|
+
/** Resolves after the browser has composited at least one frame (see step 2 above). */
|
|
1740
|
+
nextPaint() {
|
|
1741
|
+
if (typeof requestAnimationFrame !== 'function')
|
|
1742
|
+
return Promise.resolve();
|
|
1743
|
+
return new Promise((resolve) => {
|
|
1744
|
+
requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
|
|
1745
|
+
});
|
|
1746
|
+
}
|
|
1625
1747
|
// ─── Progress ──────────────────────────────────────────
|
|
1626
1748
|
updateLoaderBar(progress) {
|
|
1627
1749
|
setCSSPreloaderProgress(Math.max(0, Math.min(1, progress)));
|
|
@@ -1876,14 +1998,37 @@ class GameApplication extends EventEmitter {
|
|
|
1876
1998
|
return;
|
|
1877
1999
|
}
|
|
1878
2000
|
try {
|
|
2001
|
+
// 0. Adopt a game-supplied loading overlay (`loading.externalOverlay`) BEFORE anything that
|
|
2002
|
+
// can throw. Such an overlay is already on screen — Artube's is injected into index.html,
|
|
2003
|
+
// so it paints before this bundle is even fetched — and until the engine has adopted it,
|
|
2004
|
+
// the catch below has no way to take it down. A bad `container` selector (step 1) would
|
|
2005
|
+
// otherwise strand it on screen forever. It needs no container of ours.
|
|
2006
|
+
// Adoption is also where its minimum display time starts counting, which is why the
|
|
2007
|
+
// config value is handed over here rather than read at the hand-over: this is the
|
|
2008
|
+
// earliest moment the engine runs, and the overlay has been on screen since before it.
|
|
2009
|
+
const external = this.config.loading?.externalOverlay;
|
|
2010
|
+
if (external)
|
|
2011
|
+
adoptExternalOverlay(external, this.config.loading?.externalOverlayMinDisplayTime);
|
|
1879
2012
|
// 1. Resolve container element
|
|
1880
2013
|
this._container = this.resolveContainer();
|
|
1881
|
-
// 2. Show CSS preloader immediately (before PixiJS)
|
|
1882
|
-
|
|
2014
|
+
// 2. Show the CSS preloader immediately (before PixiJS) — UNLESS a game-supplied overlay is
|
|
2015
|
+
// already covering the screen. In that case the preloader is mounted later, by LoadingScene
|
|
2016
|
+
// at its first frame, which is where the hand-over happens. Mounting it here instead would
|
|
2017
|
+
// put our brand over theirs for the whole of Pixi init and the SDK handshake, i.e. hand
|
|
2018
|
+
// over long before the gap the external overlay exists to cover has closed.
|
|
2019
|
+
if (!hasExternalOverlay())
|
|
2020
|
+
createCSSPreloader(this._container, this.config.loading);
|
|
1883
2021
|
// 3. Initialize PixiJS
|
|
1884
2022
|
await this.initPixi();
|
|
2023
|
+
// Milestones through the pre-first-frame gap, for a game-supplied overlay only (no-ops
|
|
2024
|
+
// otherwise, so the built-in preloader's behaviour is untouched). They are also what makes
|
|
2025
|
+
// Artube's loader crossfade from its dark partner phase to its branded one: that transition
|
|
2026
|
+
// fires on the first progress above zero, and without it the player would never see the
|
|
2027
|
+
// brand the loader exists to show. Values are honest weights of what remains, not a timer.
|
|
2028
|
+
advanceExternalOverlay(0.35);
|
|
1885
2029
|
// 4. Initialize SDK (if enabled)
|
|
1886
2030
|
await this.initSDK();
|
|
2031
|
+
advanceExternalOverlay(0.7);
|
|
1887
2032
|
// 4b. Mount the branded game shell after the SDK handshake (optional)
|
|
1888
2033
|
if (this.config.shell) {
|
|
1889
2034
|
const { createGameShell } = await import('@energy8platform/shell/html');
|
|
@@ -1893,10 +2038,17 @@ class GameApplication extends EventEmitter {
|
|
|
1893
2038
|
this.applySDKConfig();
|
|
1894
2039
|
// 6. Initialize sub-systems
|
|
1895
2040
|
this.initSubSystems();
|
|
2041
|
+
advanceExternalOverlay(0.85);
|
|
1896
2042
|
this.emit('initialized');
|
|
1897
2043
|
// 7. Load assets. The CSS preloader stays on screen — LoadingScene drives
|
|
1898
2044
|
// its progress/tap and removes it before entering the game, so there's
|
|
1899
2045
|
// a single continuous overlay from boot to gameplay (no logo flash).
|
|
2046
|
+
//
|
|
2047
|
+
// With a game-supplied overlay the sequence has one extra step at the
|
|
2048
|
+
// front: LoadingScene MOUNTS the preloader, waits for its first painted
|
|
2049
|
+
// frame, and only then dismisses the external overlay. From that frame
|
|
2050
|
+
// on this path and every other are identical — same brand, same bar,
|
|
2051
|
+
// same tap-to-start.
|
|
1900
2052
|
await this.loadAssets(firstScene, sceneData);
|
|
1901
2053
|
this.emit('loaded');
|
|
1902
2054
|
// 8. Start the game loop
|
|
@@ -1905,9 +2057,13 @@ class GameApplication extends EventEmitter {
|
|
|
1905
2057
|
}
|
|
1906
2058
|
catch (err) {
|
|
1907
2059
|
console.error('[GameEngine] Failed to start:', err);
|
|
1908
|
-
// Tear down
|
|
1909
|
-
|
|
1910
|
-
|
|
2060
|
+
// Tear down both possible overlays so a failure strands neither brand frame. BOTH calls run:
|
|
2061
|
+
// a throw during the hand-over window can leave the preloader mounted AND the external
|
|
2062
|
+
// overlay still adopted, and each call is a no-op when there is nothing to remove. The
|
|
2063
|
+
// container may never have resolved (step 1 is inside this try), hence the `document.body`
|
|
2064
|
+
// fallback — the external overlay ignores the element entirely.
|
|
2065
|
+
releaseExternalOverlay();
|
|
2066
|
+
void removeCSSPreloader(this._container ?? document.body);
|
|
1911
2067
|
this.emit('error', err instanceof Error ? err : new Error(String(err)));
|
|
1912
2068
|
throw err;
|
|
1913
2069
|
}
|