@energy8platform/game-engine 0.18.0 → 0.20.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 +15 -5
- package/dist/audio.cjs.js.map +1 -1
- package/dist/audio.d.ts +5 -0
- package/dist/audio.esm.js +15 -5
- package/dist/audio.esm.js.map +1 -1
- package/dist/core.cjs.js +79 -174
- package/dist/core.cjs.js.map +1 -1
- package/dist/core.d.ts +13 -1
- package/dist/core.esm.js +80 -175
- package/dist/core.esm.js.map +1 -1
- package/dist/host.cjs.js +401 -251
- package/dist/host.cjs.js.map +1 -1
- package/dist/host.d.ts +117 -26
- package/dist/host.esm.js +403 -253
- package/dist/host.esm.js.map +1 -1
- package/dist/index.cjs.js +79 -174
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +23 -12
- package/dist/index.esm.js +80 -175
- package/dist/index.esm.js.map +1 -1
- package/dist/react.d.ts +13 -1
- package/package.json +3 -2
- package/src/audio/AudioManager.ts +17 -5
- package/src/core/GameApplication.ts +28 -10
- package/src/host/createSlotGame.ts +143 -23
- package/src/host/index.ts +4 -1
- package/src/host/overlayController.ts +81 -0
- package/src/host/pauseController.ts +21 -0
- package/src/host/runRound.ts +35 -43
- package/src/host/sceneAudio.ts +14 -0
- package/src/host/sceneController.ts +84 -19
- package/src/host/shellConfig.ts +53 -35
- package/src/host/skipGesture.ts +24 -0
- package/src/host/types.ts +5 -2
- package/src/loading/LoadingScene.ts +31 -174
- package/src/viewport/ViewportManager.ts +19 -9
package/dist/index.d.ts
CHANGED
|
@@ -342,6 +342,7 @@ declare class AudioManager {
|
|
|
342
342
|
private _persist;
|
|
343
343
|
private _storageKey;
|
|
344
344
|
private _categories;
|
|
345
|
+
private _masterGain;
|
|
345
346
|
private _currentMusic;
|
|
346
347
|
private _unlocked;
|
|
347
348
|
private _unlockHandler;
|
|
@@ -382,6 +383,10 @@ declare class AudioManager {
|
|
|
382
383
|
* Stop all sounds.
|
|
383
384
|
*/
|
|
384
385
|
stopAll(): void;
|
|
386
|
+
/** Global gain (0..1) folded into every category's effective volume. Driven by the shell's
|
|
387
|
+
* 'master' settingChange. Does not affect the persisted per-category volumes. */
|
|
388
|
+
setMasterVolume(volume: number): void;
|
|
389
|
+
getMasterVolume(): number;
|
|
385
390
|
/**
|
|
386
391
|
* Set volume for a category.
|
|
387
392
|
*/
|
|
@@ -584,6 +589,7 @@ declare class ViewportManager extends EventEmitter<ViewportEvents> {
|
|
|
584
589
|
private _app;
|
|
585
590
|
private _container;
|
|
586
591
|
private _config;
|
|
592
|
+
private _target;
|
|
587
593
|
private _resizeObserver;
|
|
588
594
|
private _currentOrientation;
|
|
589
595
|
private _currentWidth;
|
|
@@ -591,7 +597,7 @@ declare class ViewportManager extends EventEmitter<ViewportEvents> {
|
|
|
591
597
|
private _currentScale;
|
|
592
598
|
private _destroyed;
|
|
593
599
|
private _resizeTimeout;
|
|
594
|
-
constructor(app: Application, container: HTMLElement, config: ViewportConfig);
|
|
600
|
+
constructor(app: Application, container: HTMLElement, config: ViewportConfig, target?: Container);
|
|
595
601
|
/** Current canvas width in game units */
|
|
596
602
|
get width(): number;
|
|
597
603
|
/** Current canvas height in game units */
|
|
@@ -692,6 +698,12 @@ declare class GameApplication extends EventEmitter<GameEngineEvents> {
|
|
|
692
698
|
input: InputManager;
|
|
693
699
|
/** Viewport manager */
|
|
694
700
|
viewport: ViewportManager;
|
|
701
|
+
/** Scaled world root (holds scenes). Transformed by the ViewportManager to fit the design
|
|
702
|
+
* resolution; lives below the UI layer on app.stage. */
|
|
703
|
+
worldRoot: Container;
|
|
704
|
+
/** Unscaled, screen-space UI layer. Sits above {@link worldRoot} and is NOT touched by the
|
|
705
|
+
* viewport transform — children fill the real screen (e.g. the host's shell + overlay). */
|
|
706
|
+
uiLayer: Container;
|
|
695
707
|
/** SDK instance (null in offline mode) */
|
|
696
708
|
sdk: CasinoGameSDK | null;
|
|
697
709
|
/** FPS overlay instance (only when debug: true) */
|
|
@@ -2137,20 +2149,20 @@ declare class ScrollContainer extends Container {
|
|
|
2137
2149
|
}
|
|
2138
2150
|
|
|
2139
2151
|
/**
|
|
2140
|
-
* Built-in loading screen
|
|
2141
|
-
*
|
|
2142
|
-
*
|
|
2143
|
-
*
|
|
2152
|
+
* Built-in loading screen.
|
|
2153
|
+
*
|
|
2154
|
+
* It does NOT render its own overlay — the CSS preloader created at boot
|
|
2155
|
+
* (`createPlatformSession`/`GameApplication.start`) stays on screen, and this
|
|
2156
|
+
* scene merely drives it: asset-load progress → `setCSSPreloaderProgress`,
|
|
2157
|
+
* tap-to-start → `waitCSSPreloaderTap`, then fades it out via
|
|
2158
|
+
* `removeCSSPreloader` before entering the game. One continuous overlay from
|
|
2159
|
+
* boot to gameplay — no second logo, no mid-load flash.
|
|
2144
2160
|
*/
|
|
2145
2161
|
declare class LoadingScene extends Scene {
|
|
2146
2162
|
private _engine;
|
|
2147
2163
|
private _targetScene;
|
|
2148
2164
|
private _targetData?;
|
|
2149
2165
|
private _config;
|
|
2150
|
-
private _overlay;
|
|
2151
|
-
private _loaderRect;
|
|
2152
|
-
private _percentEl;
|
|
2153
|
-
private _tapToStartEl;
|
|
2154
2166
|
private _displayedProgress;
|
|
2155
2167
|
private _targetProgress;
|
|
2156
2168
|
private _loadingComplete;
|
|
@@ -2159,15 +2171,14 @@ declare class LoadingScene extends Scene {
|
|
|
2159
2171
|
onUpdate(dt: number): void;
|
|
2160
2172
|
onResize(_width: number, _height: number): void;
|
|
2161
2173
|
onDestroy(): void;
|
|
2162
|
-
private createOverlay;
|
|
2163
|
-
private removeOverlay;
|
|
2164
2174
|
private updateLoaderBar;
|
|
2165
2175
|
/**
|
|
2166
2176
|
* Smoothly animate the displayed progress from its current value to `target`
|
|
2167
2177
|
* over `durationMs` using an easeOutCubic curve.
|
|
2168
2178
|
*/
|
|
2169
2179
|
private animateProgressTo;
|
|
2170
|
-
|
|
2180
|
+
/** The DOM element hosting the canvas + preloader overlay. */
|
|
2181
|
+
private hostElement;
|
|
2171
2182
|
private transitionToGame;
|
|
2172
2183
|
}
|
|
2173
2184
|
|
package/dist/index.esm.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Ticker, Assets, Container, Text, Application, AnimatedSprite, Texture, Sprite, Graphics, NineSliceSprite } from 'pixi.js';
|
|
2
2
|
import { createPlatformSession } from '@energy8platform/platform-core';
|
|
3
|
-
import {
|
|
3
|
+
import { waitCSSPreloaderTap, removeCSSPreloader, setCSSPreloaderProgress, createCSSPreloader } 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
|
|
|
@@ -734,6 +734,7 @@ class AudioManager {
|
|
|
734
734
|
_persist;
|
|
735
735
|
_storageKey;
|
|
736
736
|
_categories;
|
|
737
|
+
_masterGain = 1.0;
|
|
737
738
|
_currentMusic = null;
|
|
738
739
|
_unlocked = false;
|
|
739
740
|
_unlockHandler = null;
|
|
@@ -793,7 +794,7 @@ class AudioManager {
|
|
|
793
794
|
if (this._globalMuted || this._categories[category].muted)
|
|
794
795
|
return;
|
|
795
796
|
const { sound } = this._soundModule;
|
|
796
|
-
const vol = (options?.volume ?? 1) * this._categories[category].volume;
|
|
797
|
+
const vol = (options?.volume ?? 1) * this._categories[category].volume * this._masterGain;
|
|
797
798
|
try {
|
|
798
799
|
sound.play(alias, {
|
|
799
800
|
volume: vol,
|
|
@@ -822,7 +823,7 @@ class AudioManager {
|
|
|
822
823
|
if (this._globalMuted || this._categories.music.muted)
|
|
823
824
|
return;
|
|
824
825
|
// Fade out the previous track
|
|
825
|
-
this.fadeVolume(prevAlias, this._categories.music.volume, 0, fadeDuration, () => {
|
|
826
|
+
this.fadeVolume(prevAlias, this._categories.music.volume * this._masterGain, 0, fadeDuration, () => {
|
|
826
827
|
try {
|
|
827
828
|
sound.stop(prevAlias);
|
|
828
829
|
}
|
|
@@ -834,7 +835,7 @@ class AudioManager {
|
|
|
834
835
|
volume: 0,
|
|
835
836
|
loop: true,
|
|
836
837
|
});
|
|
837
|
-
this.fadeVolume(alias, 0, this._categories.music.volume, fadeDuration);
|
|
838
|
+
this.fadeVolume(alias, 0, this._categories.music.volume * this._masterGain, fadeDuration);
|
|
838
839
|
}
|
|
839
840
|
catch (e) {
|
|
840
841
|
console.warn(`[AudioManager] Failed to play music "${alias}":`, e);
|
|
@@ -853,7 +854,7 @@ class AudioManager {
|
|
|
853
854
|
return;
|
|
854
855
|
try {
|
|
855
856
|
sound.play(alias, {
|
|
856
|
-
volume: this._categories.music.volume,
|
|
857
|
+
volume: this._categories.music.volume * this._masterGain,
|
|
857
858
|
loop: true,
|
|
858
859
|
});
|
|
859
860
|
}
|
|
@@ -887,6 +888,15 @@ class AudioManager {
|
|
|
887
888
|
sound.stopAll();
|
|
888
889
|
this._currentMusic = null;
|
|
889
890
|
}
|
|
891
|
+
/** Global gain (0..1) folded into every category's effective volume. Driven by the shell's
|
|
892
|
+
* 'master' settingChange. Does not affect the persisted per-category volumes. */
|
|
893
|
+
setMasterVolume(volume) {
|
|
894
|
+
this._masterGain = Math.max(0, Math.min(1, volume));
|
|
895
|
+
this.applyVolumes();
|
|
896
|
+
}
|
|
897
|
+
getMasterVolume() {
|
|
898
|
+
return this._masterGain;
|
|
899
|
+
}
|
|
890
900
|
/**
|
|
891
901
|
* Set volume for a category.
|
|
892
902
|
*/
|
|
@@ -1032,7 +1042,7 @@ class AudioManager {
|
|
|
1032
1042
|
const { sound } = this._soundModule;
|
|
1033
1043
|
// Global mute is owned by sound.muteAll()/unmuteAll() (context.muted),
|
|
1034
1044
|
// not by volumeAll — mixing both leaves mute un-undoable after reload.
|
|
1035
|
-
sound.volumeAll =
|
|
1045
|
+
sound.volumeAll = this._masterGain; // master multiplies the global bus
|
|
1036
1046
|
}
|
|
1037
1047
|
setupMobileUnlock() {
|
|
1038
1048
|
if (this._unlocked)
|
|
@@ -1300,6 +1310,7 @@ class ViewportManager extends EventEmitter {
|
|
|
1300
1310
|
_app;
|
|
1301
1311
|
_container;
|
|
1302
1312
|
_config;
|
|
1313
|
+
_target;
|
|
1303
1314
|
_resizeObserver = null;
|
|
1304
1315
|
_currentOrientation = Orientation.LANDSCAPE;
|
|
1305
1316
|
_currentWidth = 0;
|
|
@@ -1307,11 +1318,15 @@ class ViewportManager extends EventEmitter {
|
|
|
1307
1318
|
_currentScale = 1;
|
|
1308
1319
|
_destroyed = false;
|
|
1309
1320
|
_resizeTimeout = null;
|
|
1310
|
-
constructor(app, container, config) {
|
|
1321
|
+
constructor(app, container, config, target) {
|
|
1311
1322
|
super();
|
|
1312
1323
|
this._app = app;
|
|
1313
1324
|
this._container = container;
|
|
1314
1325
|
this._config = config;
|
|
1326
|
+
// The container this manager scales/offsets. Defaults to app.stage for backward
|
|
1327
|
+
// compatibility; the engine passes a dedicated scaled world root so app.stage stays
|
|
1328
|
+
// identity (screen space) for unscaled UI layers.
|
|
1329
|
+
this._target = target ?? app.stage;
|
|
1315
1330
|
this.setupObserver();
|
|
1316
1331
|
}
|
|
1317
1332
|
/** Current canvas width in game units */
|
|
@@ -1400,19 +1415,19 @@ class ViewportManager extends EventEmitter {
|
|
|
1400
1415
|
const stageScale = scaleMode === ScaleMode.STRETCH
|
|
1401
1416
|
? Math.min(containerWidth / designWidth, containerHeight / designHeight)
|
|
1402
1417
|
: scale;
|
|
1403
|
-
this.
|
|
1418
|
+
this._target.scale.set(stageScale);
|
|
1404
1419
|
// Center the stage for FIT mode
|
|
1405
1420
|
if (scaleMode === ScaleMode.FIT) {
|
|
1406
|
-
this.
|
|
1407
|
-
this.
|
|
1421
|
+
this._target.x = Math.round((containerWidth - designWidth * stageScale) / 2);
|
|
1422
|
+
this._target.y = Math.round((containerHeight - designHeight * stageScale) / 2);
|
|
1408
1423
|
}
|
|
1409
1424
|
else if (scaleMode === ScaleMode.FILL) {
|
|
1410
|
-
this.
|
|
1411
|
-
this.
|
|
1425
|
+
this._target.x = Math.round((containerWidth - gameWidth * stageScale) / 2);
|
|
1426
|
+
this._target.y = Math.round((containerHeight - gameHeight * stageScale) / 2);
|
|
1412
1427
|
}
|
|
1413
1428
|
else {
|
|
1414
|
-
this.
|
|
1415
|
-
this.
|
|
1429
|
+
this._target.x = 0;
|
|
1430
|
+
this._target.y = 0;
|
|
1416
1431
|
}
|
|
1417
1432
|
this._currentWidth = gameWidth;
|
|
1418
1433
|
this._currentHeight = gameHeight;
|
|
@@ -1501,34 +1516,20 @@ class Scene {
|
|
|
1501
1516
|
}
|
|
1502
1517
|
|
|
1503
1518
|
/**
|
|
1504
|
-
*
|
|
1505
|
-
* Uses unique IDs (prefixed with 'ls') to avoid collisions with CSSPreloader.
|
|
1506
|
-
*/
|
|
1507
|
-
function buildLoadingLogoSVG() {
|
|
1508
|
-
return buildLogoSVG({
|
|
1509
|
-
idPrefix: 'ls',
|
|
1510
|
-
svgStyle: 'width:100%;height:auto;',
|
|
1511
|
-
clipRectId: 'ge-loader-rect',
|
|
1512
|
-
textId: 'ge-loader-pct',
|
|
1513
|
-
textContent: '0%',
|
|
1514
|
-
});
|
|
1515
|
-
}
|
|
1516
|
-
/**
|
|
1517
|
-
* Built-in loading screen using the Energy8 SVG logo with animated loader bar.
|
|
1519
|
+
* Built-in loading screen.
|
|
1518
1520
|
*
|
|
1519
|
-
*
|
|
1520
|
-
*
|
|
1521
|
+
* It does NOT render its own overlay — the CSS preloader created at boot
|
|
1522
|
+
* (`createPlatformSession`/`GameApplication.start`) stays on screen, and this
|
|
1523
|
+
* scene merely drives it: asset-load progress → `setCSSPreloaderProgress`,
|
|
1524
|
+
* tap-to-start → `waitCSSPreloaderTap`, then fades it out via
|
|
1525
|
+
* `removeCSSPreloader` before entering the game. One continuous overlay from
|
|
1526
|
+
* boot to gameplay — no second logo, no mid-load flash.
|
|
1521
1527
|
*/
|
|
1522
1528
|
class LoadingScene extends Scene {
|
|
1523
1529
|
_engine;
|
|
1524
1530
|
_targetScene;
|
|
1525
1531
|
_targetData;
|
|
1526
1532
|
_config;
|
|
1527
|
-
// HTML overlay
|
|
1528
|
-
_overlay = null;
|
|
1529
|
-
_loaderRect = null;
|
|
1530
|
-
_percentEl = null;
|
|
1531
|
-
_tapToStartEl = null;
|
|
1532
1533
|
// State
|
|
1533
1534
|
_displayedProgress = 0;
|
|
1534
1535
|
_targetProgress = 0;
|
|
@@ -1541,8 +1542,6 @@ class LoadingScene extends Scene {
|
|
|
1541
1542
|
this._targetData = targetData;
|
|
1542
1543
|
this._config = engine.config.loading ?? {};
|
|
1543
1544
|
this._startTime = Date.now();
|
|
1544
|
-
// Create the HTML overlay with the SVG logo
|
|
1545
|
-
this.createOverlay();
|
|
1546
1545
|
// Initialize asset manager
|
|
1547
1546
|
await this._engine.assets.init();
|
|
1548
1547
|
// Initialize audio manager
|
|
@@ -1588,110 +1587,29 @@ class LoadingScene extends Scene {
|
|
|
1588
1587
|
// Final snap to 100%
|
|
1589
1588
|
this._displayedProgress = 1;
|
|
1590
1589
|
this.updateLoaderBar(1);
|
|
1591
|
-
//
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
else {
|
|
1596
|
-
await this.transitionToGame();
|
|
1597
|
-
}
|
|
1590
|
+
// Wait for the player's tap — resolves immediately when tapToStart is
|
|
1591
|
+
// false (the preloader honours that flag) — then enter the game.
|
|
1592
|
+
await waitCSSPreloaderTap();
|
|
1593
|
+
await this.transitionToGame();
|
|
1598
1594
|
}
|
|
1599
1595
|
onUpdate(dt) {
|
|
1600
|
-
// Smooth progress bar fill
|
|
1596
|
+
// Smooth progress bar fill (during active loading)
|
|
1601
1597
|
if (!this._loadingComplete && this._displayedProgress < this._targetProgress) {
|
|
1602
1598
|
this._displayedProgress = Math.min(this._displayedProgress + dt * 1.5, this._targetProgress);
|
|
1603
1599
|
this.updateLoaderBar(this._displayedProgress);
|
|
1604
1600
|
}
|
|
1605
1601
|
}
|
|
1606
1602
|
onResize(_width, _height) {
|
|
1607
|
-
//
|
|
1603
|
+
// The preloader overlay is CSS-based and auto-resizes.
|
|
1608
1604
|
}
|
|
1609
1605
|
onDestroy() {
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
createOverlay() {
|
|
1614
|
-
const bgColor = typeof this._config.backgroundColor === 'string'
|
|
1615
|
-
? this._config.backgroundColor
|
|
1616
|
-
: typeof this._config.backgroundColor === 'number'
|
|
1617
|
-
? `#${this._config.backgroundColor.toString(16).padStart(6, '0')}`
|
|
1618
|
-
: '#0a0a1a';
|
|
1619
|
-
const bgGradient = this._config.backgroundGradient ??
|
|
1620
|
-
`linear-gradient(135deg, ${bgColor} 0%, #1a1a3e 100%)`;
|
|
1621
|
-
this._overlay = document.createElement('div');
|
|
1622
|
-
this._overlay.id = '__ge-loading-overlay__';
|
|
1623
|
-
this._overlay.innerHTML = `
|
|
1624
|
-
<div class="ge-loading-content">
|
|
1625
|
-
${buildLoadingLogoSVG()}
|
|
1626
|
-
</div>
|
|
1627
|
-
`;
|
|
1628
|
-
const style = document.createElement('style');
|
|
1629
|
-
style.id = '__ge-loading-style__';
|
|
1630
|
-
style.textContent = `
|
|
1631
|
-
#__ge-loading-overlay__ {
|
|
1632
|
-
position: absolute;
|
|
1633
|
-
top: 0; left: 0;
|
|
1634
|
-
width: 100%; height: 100%;
|
|
1635
|
-
background: ${bgGradient};
|
|
1636
|
-
display: flex;
|
|
1637
|
-
align-items: center;
|
|
1638
|
-
justify-content: center;
|
|
1639
|
-
z-index: 9999;
|
|
1640
|
-
transition: opacity 0.5s ease-out;
|
|
1641
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
1642
|
-
}
|
|
1643
|
-
#__ge-loading-overlay__.ge-fade-out {
|
|
1644
|
-
opacity: 0;
|
|
1645
|
-
pointer-events: none;
|
|
1646
|
-
}
|
|
1647
|
-
.ge-loading-content {
|
|
1648
|
-
display: flex;
|
|
1649
|
-
flex-direction: column;
|
|
1650
|
-
align-items: center;
|
|
1651
|
-
width: 75%;
|
|
1652
|
-
max-width: 650px;
|
|
1653
|
-
}
|
|
1654
|
-
.ge-loading-content svg {
|
|
1655
|
-
filter: drop-shadow(0 0 40px rgba(121, 57, 194, 0.5));
|
|
1656
|
-
cursor: default;
|
|
1657
|
-
}
|
|
1658
|
-
|
|
1659
|
-
.ge-svg-pulse {
|
|
1660
|
-
animation: ge-tap-pulse 1.2s ease-in-out infinite;
|
|
1661
|
-
}
|
|
1662
|
-
@keyframes ge-tap-pulse {
|
|
1663
|
-
0%, 100% { opacity: 0.5; }
|
|
1664
|
-
50% { opacity: 1; }
|
|
1665
|
-
}
|
|
1666
|
-
`;
|
|
1667
|
-
// Get the container that holds the canvas
|
|
1668
|
-
const container = this._engine.app?.canvas?.parentElement;
|
|
1669
|
-
if (container) {
|
|
1670
|
-
container.style.position = container.style.position || 'relative';
|
|
1671
|
-
container.appendChild(style);
|
|
1672
|
-
container.appendChild(this._overlay);
|
|
1673
|
-
}
|
|
1674
|
-
// Cache the SVG loader rect for progress updates
|
|
1675
|
-
this._loaderRect = this._overlay.querySelector('#ge-loader-rect');
|
|
1676
|
-
this._percentEl = this._overlay.querySelector('#ge-loader-pct');
|
|
1677
|
-
}
|
|
1678
|
-
removeOverlay() {
|
|
1679
|
-
this._overlay?.remove();
|
|
1680
|
-
document.getElementById('__ge-loading-style__')?.remove();
|
|
1681
|
-
this._overlay = null;
|
|
1682
|
-
this._loaderRect = null;
|
|
1683
|
-
this._percentEl = null;
|
|
1684
|
-
this._tapToStartEl = null;
|
|
1606
|
+
// Defensive: ensure the preloader is gone even if we never transitioned
|
|
1607
|
+
// (e.g. the scene was popped externally). Idempotent.
|
|
1608
|
+
void removeCSSPreloader(this.hostElement());
|
|
1685
1609
|
}
|
|
1686
1610
|
// ─── Progress ──────────────────────────────────────────
|
|
1687
1611
|
updateLoaderBar(progress) {
|
|
1688
|
-
|
|
1689
|
-
this._loaderRect.setAttribute('width', String(LOADER_BAR_MAX_WIDTH * progress));
|
|
1690
|
-
}
|
|
1691
|
-
if (this._percentEl) {
|
|
1692
|
-
const pct = Math.round(progress * 100);
|
|
1693
|
-
this._percentEl.textContent = `${pct}%`;
|
|
1694
|
-
}
|
|
1612
|
+
setCSSPreloaderProgress(Math.max(0, Math.min(1, progress)));
|
|
1695
1613
|
}
|
|
1696
1614
|
/**
|
|
1697
1615
|
* Smoothly animate the displayed progress from its current value to `target`
|
|
@@ -1721,49 +1639,20 @@ class LoadingScene extends Scene {
|
|
|
1721
1639
|
requestAnimationFrame(tick);
|
|
1722
1640
|
});
|
|
1723
1641
|
}
|
|
1724
|
-
// ─── Tap to Start ─────────────────────────────────────
|
|
1725
|
-
async showTapToStart() {
|
|
1726
|
-
const tapText = this._config.tapToStartText ?? 'TAP TO START';
|
|
1727
|
-
// Reuse the same SVG text element — replace percentage with tap text
|
|
1728
|
-
if (this._percentEl) {
|
|
1729
|
-
const el = this._percentEl;
|
|
1730
|
-
el.textContent = tapText;
|
|
1731
|
-
el.setAttribute('fill', '#ffffff');
|
|
1732
|
-
el.classList.add('ge-svg-pulse');
|
|
1733
|
-
this._tapToStartEl = el;
|
|
1734
|
-
}
|
|
1735
|
-
// Make overlay clickable
|
|
1736
|
-
if (this._overlay) {
|
|
1737
|
-
this._overlay.style.cursor = 'pointer';
|
|
1738
|
-
}
|
|
1739
|
-
// Wait for tap
|
|
1740
|
-
return new Promise((resolve) => {
|
|
1741
|
-
const handler = async () => {
|
|
1742
|
-
this._overlay?.removeEventListener('click', handler);
|
|
1743
|
-
await this.transitionToGame();
|
|
1744
|
-
resolve();
|
|
1745
|
-
};
|
|
1746
|
-
// Listen on the full overlay for easier mobile tap
|
|
1747
|
-
this._overlay?.addEventListener('click', handler);
|
|
1748
|
-
});
|
|
1749
|
-
}
|
|
1750
1642
|
// ─── Transition ────────────────────────────────────────
|
|
1643
|
+
/** The DOM element hosting the canvas + preloader overlay. */
|
|
1644
|
+
hostElement() {
|
|
1645
|
+
return this._engine?.app?.canvas?.parentElement ?? document.body;
|
|
1646
|
+
}
|
|
1751
1647
|
async transitionToGame() {
|
|
1752
|
-
// Fade out the
|
|
1753
|
-
|
|
1754
|
-
this._overlay.classList.add('ge-fade-out');
|
|
1755
|
-
await new Promise((resolve) => {
|
|
1756
|
-
this._overlay.addEventListener('transitionend', () => resolve(), { once: true });
|
|
1757
|
-
// Safety timeout
|
|
1758
|
-
setTimeout(resolve, 600);
|
|
1759
|
-
});
|
|
1760
|
-
}
|
|
1761
|
-
// Remove overlay
|
|
1762
|
-
this.removeOverlay();
|
|
1648
|
+
// Fade out and remove the shared CSS preloader (resolves after the fade).
|
|
1649
|
+
await removeCSSPreloader(this.hostElement());
|
|
1763
1650
|
// Navigate to the target scene, always passing the engine reference
|
|
1764
1651
|
await this._engine.scenes.goto(this._targetScene, {
|
|
1765
1652
|
engine: this._engine,
|
|
1766
|
-
...(this._targetData && typeof this._targetData === 'object'
|
|
1653
|
+
...(this._targetData && typeof this._targetData === 'object'
|
|
1654
|
+
? this._targetData
|
|
1655
|
+
: { data: this._targetData }),
|
|
1767
1656
|
});
|
|
1768
1657
|
}
|
|
1769
1658
|
}
|
|
@@ -1903,6 +1792,12 @@ class GameApplication extends EventEmitter {
|
|
|
1903
1792
|
input;
|
|
1904
1793
|
/** Viewport manager */
|
|
1905
1794
|
viewport;
|
|
1795
|
+
/** Scaled world root (holds scenes). Transformed by the ViewportManager to fit the design
|
|
1796
|
+
* resolution; lives below the UI layer on app.stage. */
|
|
1797
|
+
worldRoot;
|
|
1798
|
+
/** Unscaled, screen-space UI layer. Sits above {@link worldRoot} and is NOT touched by the
|
|
1799
|
+
* viewport transform — children fill the real screen (e.g. the host's shell + overlay). */
|
|
1800
|
+
uiLayer;
|
|
1906
1801
|
/** SDK instance (null in offline mode) */
|
|
1907
1802
|
sdk = null;
|
|
1908
1803
|
/** FPS overlay instance (only when debug: true) */
|
|
@@ -1984,17 +1879,20 @@ class GameApplication extends EventEmitter {
|
|
|
1984
1879
|
// 6. Initialize sub-systems
|
|
1985
1880
|
this.initSubSystems();
|
|
1986
1881
|
this.emit('initialized');
|
|
1987
|
-
// 7.
|
|
1988
|
-
|
|
1989
|
-
//
|
|
1882
|
+
// 7. Load assets. The CSS preloader stays on screen — LoadingScene drives
|
|
1883
|
+
// its progress/tap and removes it before entering the game, so there's
|
|
1884
|
+
// a single continuous overlay from boot to gameplay (no logo flash).
|
|
1990
1885
|
await this.loadAssets(firstScene, sceneData);
|
|
1991
1886
|
this.emit('loaded');
|
|
1992
|
-
//
|
|
1887
|
+
// 8. Start the game loop
|
|
1993
1888
|
this._running = true;
|
|
1994
1889
|
this.emit('started');
|
|
1995
1890
|
}
|
|
1996
1891
|
catch (err) {
|
|
1997
1892
|
console.error('[GameEngine] Failed to start:', err);
|
|
1893
|
+
// Tear down the preloader so a failure doesn't strand the brand frame.
|
|
1894
|
+
if (this._container)
|
|
1895
|
+
removeCSSPreloader(this._container);
|
|
1998
1896
|
this.emit('error', err instanceof Error ? err : new Error(String(err)));
|
|
1999
1897
|
throw err;
|
|
2000
1898
|
}
|
|
@@ -2082,20 +1980,27 @@ class GameApplication extends EventEmitter {
|
|
|
2082
1980
|
this.audio = new AudioManager(this.config.audio);
|
|
2083
1981
|
// Input Manager
|
|
2084
1982
|
this.input = new InputManager(this.app.canvas);
|
|
2085
|
-
//
|
|
1983
|
+
// Stage layers: a scaled world root (scenes, transformed to design resolution by the
|
|
1984
|
+
// viewport) below an unscaled UI layer (screen space). app.stage itself stays identity.
|
|
1985
|
+
this.worldRoot = new Container();
|
|
1986
|
+
this.worldRoot.label = 'world';
|
|
1987
|
+
this.uiLayer = new Container();
|
|
1988
|
+
this.uiLayer.label = 'ui';
|
|
1989
|
+
this.app.stage.addChild(this.worldRoot, this.uiLayer);
|
|
1990
|
+
// Viewport Manager — scales worldRoot (NOT app.stage), so the UI layer is unscaled.
|
|
2086
1991
|
this.viewport = new ViewportManager(this.app, this._container, {
|
|
2087
1992
|
designWidth: this.config.designWidth,
|
|
2088
1993
|
designHeight: this.config.designHeight,
|
|
2089
1994
|
scaleMode: this.config.scaleMode,
|
|
2090
1995
|
orientation: this.config.orientation,
|
|
2091
|
-
});
|
|
2092
|
-
// Wire SceneManager to the
|
|
2093
|
-
this.scenes.setRoot(this.
|
|
1996
|
+
}, this.worldRoot);
|
|
1997
|
+
// Wire SceneManager to the scaled world root
|
|
1998
|
+
this.scenes.setRoot(this.worldRoot);
|
|
2094
1999
|
this.scenes.setApp(this);
|
|
2095
2000
|
// Wire viewport resize → scene manager + input manager
|
|
2096
2001
|
this.viewport.on('resize', ({ width, height, scale }) => {
|
|
2097
2002
|
this.scenes.resize(width, height);
|
|
2098
|
-
this.input.setViewportTransform(scale, this.
|
|
2003
|
+
this.input.setViewportTransform(scale, this.worldRoot.x, this.worldRoot.y);
|
|
2099
2004
|
this.emit('resize', { width, height });
|
|
2100
2005
|
});
|
|
2101
2006
|
this.viewport.on('orientationChange', (orientation) => {
|