@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/core.esm.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Ticker, Assets, Container, Text, Application, Graphics } 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
|
|
|
5
5
|
// ─── Scale Modes ───────────────────────────────────────────
|
|
6
6
|
var ScaleMode;
|
|
@@ -662,6 +662,7 @@ class AudioManager {
|
|
|
662
662
|
_persist;
|
|
663
663
|
_storageKey;
|
|
664
664
|
_categories;
|
|
665
|
+
_masterGain = 1.0;
|
|
665
666
|
_currentMusic = null;
|
|
666
667
|
_unlocked = false;
|
|
667
668
|
_unlockHandler = null;
|
|
@@ -721,7 +722,7 @@ class AudioManager {
|
|
|
721
722
|
if (this._globalMuted || this._categories[category].muted)
|
|
722
723
|
return;
|
|
723
724
|
const { sound } = this._soundModule;
|
|
724
|
-
const vol = (options?.volume ?? 1) * this._categories[category].volume;
|
|
725
|
+
const vol = (options?.volume ?? 1) * this._categories[category].volume * this._masterGain;
|
|
725
726
|
try {
|
|
726
727
|
sound.play(alias, {
|
|
727
728
|
volume: vol,
|
|
@@ -750,7 +751,7 @@ class AudioManager {
|
|
|
750
751
|
if (this._globalMuted || this._categories.music.muted)
|
|
751
752
|
return;
|
|
752
753
|
// Fade out the previous track
|
|
753
|
-
this.fadeVolume(prevAlias, this._categories.music.volume, 0, fadeDuration, () => {
|
|
754
|
+
this.fadeVolume(prevAlias, this._categories.music.volume * this._masterGain, 0, fadeDuration, () => {
|
|
754
755
|
try {
|
|
755
756
|
sound.stop(prevAlias);
|
|
756
757
|
}
|
|
@@ -762,7 +763,7 @@ class AudioManager {
|
|
|
762
763
|
volume: 0,
|
|
763
764
|
loop: true,
|
|
764
765
|
});
|
|
765
|
-
this.fadeVolume(alias, 0, this._categories.music.volume, fadeDuration);
|
|
766
|
+
this.fadeVolume(alias, 0, this._categories.music.volume * this._masterGain, fadeDuration);
|
|
766
767
|
}
|
|
767
768
|
catch (e) {
|
|
768
769
|
console.warn(`[AudioManager] Failed to play music "${alias}":`, e);
|
|
@@ -781,7 +782,7 @@ class AudioManager {
|
|
|
781
782
|
return;
|
|
782
783
|
try {
|
|
783
784
|
sound.play(alias, {
|
|
784
|
-
volume: this._categories.music.volume,
|
|
785
|
+
volume: this._categories.music.volume * this._masterGain,
|
|
785
786
|
loop: true,
|
|
786
787
|
});
|
|
787
788
|
}
|
|
@@ -815,6 +816,15 @@ class AudioManager {
|
|
|
815
816
|
sound.stopAll();
|
|
816
817
|
this._currentMusic = null;
|
|
817
818
|
}
|
|
819
|
+
/** Global gain (0..1) folded into every category's effective volume. Driven by the shell's
|
|
820
|
+
* 'master' settingChange. Does not affect the persisted per-category volumes. */
|
|
821
|
+
setMasterVolume(volume) {
|
|
822
|
+
this._masterGain = Math.max(0, Math.min(1, volume));
|
|
823
|
+
this.applyVolumes();
|
|
824
|
+
}
|
|
825
|
+
getMasterVolume() {
|
|
826
|
+
return this._masterGain;
|
|
827
|
+
}
|
|
818
828
|
/**
|
|
819
829
|
* Set volume for a category.
|
|
820
830
|
*/
|
|
@@ -960,7 +970,7 @@ class AudioManager {
|
|
|
960
970
|
const { sound } = this._soundModule;
|
|
961
971
|
// Global mute is owned by sound.muteAll()/unmuteAll() (context.muted),
|
|
962
972
|
// not by volumeAll — mixing both leaves mute un-undoable after reload.
|
|
963
|
-
sound.volumeAll =
|
|
973
|
+
sound.volumeAll = this._masterGain; // master multiplies the global bus
|
|
964
974
|
}
|
|
965
975
|
setupMobileUnlock() {
|
|
966
976
|
if (this._unlocked)
|
|
@@ -1228,6 +1238,7 @@ class ViewportManager extends EventEmitter {
|
|
|
1228
1238
|
_app;
|
|
1229
1239
|
_container;
|
|
1230
1240
|
_config;
|
|
1241
|
+
_target;
|
|
1231
1242
|
_resizeObserver = null;
|
|
1232
1243
|
_currentOrientation = Orientation.LANDSCAPE;
|
|
1233
1244
|
_currentWidth = 0;
|
|
@@ -1235,11 +1246,15 @@ class ViewportManager extends EventEmitter {
|
|
|
1235
1246
|
_currentScale = 1;
|
|
1236
1247
|
_destroyed = false;
|
|
1237
1248
|
_resizeTimeout = null;
|
|
1238
|
-
constructor(app, container, config) {
|
|
1249
|
+
constructor(app, container, config, target) {
|
|
1239
1250
|
super();
|
|
1240
1251
|
this._app = app;
|
|
1241
1252
|
this._container = container;
|
|
1242
1253
|
this._config = config;
|
|
1254
|
+
// The container this manager scales/offsets. Defaults to app.stage for backward
|
|
1255
|
+
// compatibility; the engine passes a dedicated scaled world root so app.stage stays
|
|
1256
|
+
// identity (screen space) for unscaled UI layers.
|
|
1257
|
+
this._target = target ?? app.stage;
|
|
1243
1258
|
this.setupObserver();
|
|
1244
1259
|
}
|
|
1245
1260
|
/** Current canvas width in game units */
|
|
@@ -1328,19 +1343,19 @@ class ViewportManager extends EventEmitter {
|
|
|
1328
1343
|
const stageScale = scaleMode === ScaleMode.STRETCH
|
|
1329
1344
|
? Math.min(containerWidth / designWidth, containerHeight / designHeight)
|
|
1330
1345
|
: scale;
|
|
1331
|
-
this.
|
|
1346
|
+
this._target.scale.set(stageScale);
|
|
1332
1347
|
// Center the stage for FIT mode
|
|
1333
1348
|
if (scaleMode === ScaleMode.FIT) {
|
|
1334
|
-
this.
|
|
1335
|
-
this.
|
|
1349
|
+
this._target.x = Math.round((containerWidth - designWidth * stageScale) / 2);
|
|
1350
|
+
this._target.y = Math.round((containerHeight - designHeight * stageScale) / 2);
|
|
1336
1351
|
}
|
|
1337
1352
|
else if (scaleMode === ScaleMode.FILL) {
|
|
1338
|
-
this.
|
|
1339
|
-
this.
|
|
1353
|
+
this._target.x = Math.round((containerWidth - gameWidth * stageScale) / 2);
|
|
1354
|
+
this._target.y = Math.round((containerHeight - gameHeight * stageScale) / 2);
|
|
1340
1355
|
}
|
|
1341
1356
|
else {
|
|
1342
|
-
this.
|
|
1343
|
-
this.
|
|
1357
|
+
this._target.x = 0;
|
|
1358
|
+
this._target.y = 0;
|
|
1344
1359
|
}
|
|
1345
1360
|
this._currentWidth = gameWidth;
|
|
1346
1361
|
this._currentHeight = gameHeight;
|
|
@@ -1429,34 +1444,20 @@ class Scene {
|
|
|
1429
1444
|
}
|
|
1430
1445
|
|
|
1431
1446
|
/**
|
|
1432
|
-
*
|
|
1433
|
-
* Uses unique IDs (prefixed with 'ls') to avoid collisions with CSSPreloader.
|
|
1434
|
-
*/
|
|
1435
|
-
function buildLoadingLogoSVG() {
|
|
1436
|
-
return buildLogoSVG({
|
|
1437
|
-
idPrefix: 'ls',
|
|
1438
|
-
svgStyle: 'width:100%;height:auto;',
|
|
1439
|
-
clipRectId: 'ge-loader-rect',
|
|
1440
|
-
textId: 'ge-loader-pct',
|
|
1441
|
-
textContent: '0%',
|
|
1442
|
-
});
|
|
1443
|
-
}
|
|
1444
|
-
/**
|
|
1445
|
-
* Built-in loading screen using the Energy8 SVG logo with animated loader bar.
|
|
1447
|
+
* Built-in loading screen.
|
|
1446
1448
|
*
|
|
1447
|
-
*
|
|
1448
|
-
*
|
|
1449
|
+
* It does NOT render its own overlay — the CSS preloader created at boot
|
|
1450
|
+
* (`createPlatformSession`/`GameApplication.start`) stays on screen, and this
|
|
1451
|
+
* scene merely drives it: asset-load progress → `setCSSPreloaderProgress`,
|
|
1452
|
+
* tap-to-start → `waitCSSPreloaderTap`, then fades it out via
|
|
1453
|
+
* `removeCSSPreloader` before entering the game. One continuous overlay from
|
|
1454
|
+
* boot to gameplay — no second logo, no mid-load flash.
|
|
1449
1455
|
*/
|
|
1450
1456
|
class LoadingScene extends Scene {
|
|
1451
1457
|
_engine;
|
|
1452
1458
|
_targetScene;
|
|
1453
1459
|
_targetData;
|
|
1454
1460
|
_config;
|
|
1455
|
-
// HTML overlay
|
|
1456
|
-
_overlay = null;
|
|
1457
|
-
_loaderRect = null;
|
|
1458
|
-
_percentEl = null;
|
|
1459
|
-
_tapToStartEl = null;
|
|
1460
1461
|
// State
|
|
1461
1462
|
_displayedProgress = 0;
|
|
1462
1463
|
_targetProgress = 0;
|
|
@@ -1469,8 +1470,6 @@ class LoadingScene extends Scene {
|
|
|
1469
1470
|
this._targetData = targetData;
|
|
1470
1471
|
this._config = engine.config.loading ?? {};
|
|
1471
1472
|
this._startTime = Date.now();
|
|
1472
|
-
// Create the HTML overlay with the SVG logo
|
|
1473
|
-
this.createOverlay();
|
|
1474
1473
|
// Initialize asset manager
|
|
1475
1474
|
await this._engine.assets.init();
|
|
1476
1475
|
// Initialize audio manager
|
|
@@ -1516,110 +1515,29 @@ class LoadingScene extends Scene {
|
|
|
1516
1515
|
// Final snap to 100%
|
|
1517
1516
|
this._displayedProgress = 1;
|
|
1518
1517
|
this.updateLoaderBar(1);
|
|
1519
|
-
//
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
else {
|
|
1524
|
-
await this.transitionToGame();
|
|
1525
|
-
}
|
|
1518
|
+
// Wait for the player's tap — resolves immediately when tapToStart is
|
|
1519
|
+
// false (the preloader honours that flag) — then enter the game.
|
|
1520
|
+
await waitCSSPreloaderTap();
|
|
1521
|
+
await this.transitionToGame();
|
|
1526
1522
|
}
|
|
1527
1523
|
onUpdate(dt) {
|
|
1528
|
-
// Smooth progress bar fill
|
|
1524
|
+
// Smooth progress bar fill (during active loading)
|
|
1529
1525
|
if (!this._loadingComplete && this._displayedProgress < this._targetProgress) {
|
|
1530
1526
|
this._displayedProgress = Math.min(this._displayedProgress + dt * 1.5, this._targetProgress);
|
|
1531
1527
|
this.updateLoaderBar(this._displayedProgress);
|
|
1532
1528
|
}
|
|
1533
1529
|
}
|
|
1534
1530
|
onResize(_width, _height) {
|
|
1535
|
-
//
|
|
1531
|
+
// The preloader overlay is CSS-based and auto-resizes.
|
|
1536
1532
|
}
|
|
1537
1533
|
onDestroy() {
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
createOverlay() {
|
|
1542
|
-
const bgColor = typeof this._config.backgroundColor === 'string'
|
|
1543
|
-
? this._config.backgroundColor
|
|
1544
|
-
: typeof this._config.backgroundColor === 'number'
|
|
1545
|
-
? `#${this._config.backgroundColor.toString(16).padStart(6, '0')}`
|
|
1546
|
-
: '#0a0a1a';
|
|
1547
|
-
const bgGradient = this._config.backgroundGradient ??
|
|
1548
|
-
`linear-gradient(135deg, ${bgColor} 0%, #1a1a3e 100%)`;
|
|
1549
|
-
this._overlay = document.createElement('div');
|
|
1550
|
-
this._overlay.id = '__ge-loading-overlay__';
|
|
1551
|
-
this._overlay.innerHTML = `
|
|
1552
|
-
<div class="ge-loading-content">
|
|
1553
|
-
${buildLoadingLogoSVG()}
|
|
1554
|
-
</div>
|
|
1555
|
-
`;
|
|
1556
|
-
const style = document.createElement('style');
|
|
1557
|
-
style.id = '__ge-loading-style__';
|
|
1558
|
-
style.textContent = `
|
|
1559
|
-
#__ge-loading-overlay__ {
|
|
1560
|
-
position: absolute;
|
|
1561
|
-
top: 0; left: 0;
|
|
1562
|
-
width: 100%; height: 100%;
|
|
1563
|
-
background: ${bgGradient};
|
|
1564
|
-
display: flex;
|
|
1565
|
-
align-items: center;
|
|
1566
|
-
justify-content: center;
|
|
1567
|
-
z-index: 9999;
|
|
1568
|
-
transition: opacity 0.5s ease-out;
|
|
1569
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
1570
|
-
}
|
|
1571
|
-
#__ge-loading-overlay__.ge-fade-out {
|
|
1572
|
-
opacity: 0;
|
|
1573
|
-
pointer-events: none;
|
|
1574
|
-
}
|
|
1575
|
-
.ge-loading-content {
|
|
1576
|
-
display: flex;
|
|
1577
|
-
flex-direction: column;
|
|
1578
|
-
align-items: center;
|
|
1579
|
-
width: 75%;
|
|
1580
|
-
max-width: 650px;
|
|
1581
|
-
}
|
|
1582
|
-
.ge-loading-content svg {
|
|
1583
|
-
filter: drop-shadow(0 0 40px rgba(121, 57, 194, 0.5));
|
|
1584
|
-
cursor: default;
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
|
-
.ge-svg-pulse {
|
|
1588
|
-
animation: ge-tap-pulse 1.2s ease-in-out infinite;
|
|
1589
|
-
}
|
|
1590
|
-
@keyframes ge-tap-pulse {
|
|
1591
|
-
0%, 100% { opacity: 0.5; }
|
|
1592
|
-
50% { opacity: 1; }
|
|
1593
|
-
}
|
|
1594
|
-
`;
|
|
1595
|
-
// Get the container that holds the canvas
|
|
1596
|
-
const container = this._engine.app?.canvas?.parentElement;
|
|
1597
|
-
if (container) {
|
|
1598
|
-
container.style.position = container.style.position || 'relative';
|
|
1599
|
-
container.appendChild(style);
|
|
1600
|
-
container.appendChild(this._overlay);
|
|
1601
|
-
}
|
|
1602
|
-
// Cache the SVG loader rect for progress updates
|
|
1603
|
-
this._loaderRect = this._overlay.querySelector('#ge-loader-rect');
|
|
1604
|
-
this._percentEl = this._overlay.querySelector('#ge-loader-pct');
|
|
1605
|
-
}
|
|
1606
|
-
removeOverlay() {
|
|
1607
|
-
this._overlay?.remove();
|
|
1608
|
-
document.getElementById('__ge-loading-style__')?.remove();
|
|
1609
|
-
this._overlay = null;
|
|
1610
|
-
this._loaderRect = null;
|
|
1611
|
-
this._percentEl = null;
|
|
1612
|
-
this._tapToStartEl = null;
|
|
1534
|
+
// Defensive: ensure the preloader is gone even if we never transitioned
|
|
1535
|
+
// (e.g. the scene was popped externally). Idempotent.
|
|
1536
|
+
void removeCSSPreloader(this.hostElement());
|
|
1613
1537
|
}
|
|
1614
1538
|
// ─── Progress ──────────────────────────────────────────
|
|
1615
1539
|
updateLoaderBar(progress) {
|
|
1616
|
-
|
|
1617
|
-
this._loaderRect.setAttribute('width', String(LOADER_BAR_MAX_WIDTH * progress));
|
|
1618
|
-
}
|
|
1619
|
-
if (this._percentEl) {
|
|
1620
|
-
const pct = Math.round(progress * 100);
|
|
1621
|
-
this._percentEl.textContent = `${pct}%`;
|
|
1622
|
-
}
|
|
1540
|
+
setCSSPreloaderProgress(Math.max(0, Math.min(1, progress)));
|
|
1623
1541
|
}
|
|
1624
1542
|
/**
|
|
1625
1543
|
* Smoothly animate the displayed progress from its current value to `target`
|
|
@@ -1649,49 +1567,20 @@ class LoadingScene extends Scene {
|
|
|
1649
1567
|
requestAnimationFrame(tick);
|
|
1650
1568
|
});
|
|
1651
1569
|
}
|
|
1652
|
-
// ─── Tap to Start ─────────────────────────────────────
|
|
1653
|
-
async showTapToStart() {
|
|
1654
|
-
const tapText = this._config.tapToStartText ?? 'TAP TO START';
|
|
1655
|
-
// Reuse the same SVG text element — replace percentage with tap text
|
|
1656
|
-
if (this._percentEl) {
|
|
1657
|
-
const el = this._percentEl;
|
|
1658
|
-
el.textContent = tapText;
|
|
1659
|
-
el.setAttribute('fill', '#ffffff');
|
|
1660
|
-
el.classList.add('ge-svg-pulse');
|
|
1661
|
-
this._tapToStartEl = el;
|
|
1662
|
-
}
|
|
1663
|
-
// Make overlay clickable
|
|
1664
|
-
if (this._overlay) {
|
|
1665
|
-
this._overlay.style.cursor = 'pointer';
|
|
1666
|
-
}
|
|
1667
|
-
// Wait for tap
|
|
1668
|
-
return new Promise((resolve) => {
|
|
1669
|
-
const handler = async () => {
|
|
1670
|
-
this._overlay?.removeEventListener('click', handler);
|
|
1671
|
-
await this.transitionToGame();
|
|
1672
|
-
resolve();
|
|
1673
|
-
};
|
|
1674
|
-
// Listen on the full overlay for easier mobile tap
|
|
1675
|
-
this._overlay?.addEventListener('click', handler);
|
|
1676
|
-
});
|
|
1677
|
-
}
|
|
1678
1570
|
// ─── Transition ────────────────────────────────────────
|
|
1571
|
+
/** The DOM element hosting the canvas + preloader overlay. */
|
|
1572
|
+
hostElement() {
|
|
1573
|
+
return this._engine?.app?.canvas?.parentElement ?? document.body;
|
|
1574
|
+
}
|
|
1679
1575
|
async transitionToGame() {
|
|
1680
|
-
// Fade out the
|
|
1681
|
-
|
|
1682
|
-
this._overlay.classList.add('ge-fade-out');
|
|
1683
|
-
await new Promise((resolve) => {
|
|
1684
|
-
this._overlay.addEventListener('transitionend', () => resolve(), { once: true });
|
|
1685
|
-
// Safety timeout
|
|
1686
|
-
setTimeout(resolve, 600);
|
|
1687
|
-
});
|
|
1688
|
-
}
|
|
1689
|
-
// Remove overlay
|
|
1690
|
-
this.removeOverlay();
|
|
1576
|
+
// Fade out and remove the shared CSS preloader (resolves after the fade).
|
|
1577
|
+
await removeCSSPreloader(this.hostElement());
|
|
1691
1578
|
// Navigate to the target scene, always passing the engine reference
|
|
1692
1579
|
await this._engine.scenes.goto(this._targetScene, {
|
|
1693
1580
|
engine: this._engine,
|
|
1694
|
-
...(this._targetData && typeof this._targetData === 'object'
|
|
1581
|
+
...(this._targetData && typeof this._targetData === 'object'
|
|
1582
|
+
? this._targetData
|
|
1583
|
+
: { data: this._targetData }),
|
|
1695
1584
|
});
|
|
1696
1585
|
}
|
|
1697
1586
|
}
|
|
@@ -1831,6 +1720,12 @@ class GameApplication extends EventEmitter {
|
|
|
1831
1720
|
input;
|
|
1832
1721
|
/** Viewport manager */
|
|
1833
1722
|
viewport;
|
|
1723
|
+
/** Scaled world root (holds scenes). Transformed by the ViewportManager to fit the design
|
|
1724
|
+
* resolution; lives below the UI layer on app.stage. */
|
|
1725
|
+
worldRoot;
|
|
1726
|
+
/** Unscaled, screen-space UI layer. Sits above {@link worldRoot} and is NOT touched by the
|
|
1727
|
+
* viewport transform — children fill the real screen (e.g. the host's shell + overlay). */
|
|
1728
|
+
uiLayer;
|
|
1834
1729
|
/** SDK instance (null in offline mode) */
|
|
1835
1730
|
sdk = null;
|
|
1836
1731
|
/** FPS overlay instance (only when debug: true) */
|
|
@@ -1912,17 +1807,20 @@ class GameApplication extends EventEmitter {
|
|
|
1912
1807
|
// 6. Initialize sub-systems
|
|
1913
1808
|
this.initSubSystems();
|
|
1914
1809
|
this.emit('initialized');
|
|
1915
|
-
// 7.
|
|
1916
|
-
|
|
1917
|
-
//
|
|
1810
|
+
// 7. Load assets. The CSS preloader stays on screen — LoadingScene drives
|
|
1811
|
+
// its progress/tap and removes it before entering the game, so there's
|
|
1812
|
+
// a single continuous overlay from boot to gameplay (no logo flash).
|
|
1918
1813
|
await this.loadAssets(firstScene, sceneData);
|
|
1919
1814
|
this.emit('loaded');
|
|
1920
|
-
//
|
|
1815
|
+
// 8. Start the game loop
|
|
1921
1816
|
this._running = true;
|
|
1922
1817
|
this.emit('started');
|
|
1923
1818
|
}
|
|
1924
1819
|
catch (err) {
|
|
1925
1820
|
console.error('[GameEngine] Failed to start:', err);
|
|
1821
|
+
// Tear down the preloader so a failure doesn't strand the brand frame.
|
|
1822
|
+
if (this._container)
|
|
1823
|
+
removeCSSPreloader(this._container);
|
|
1926
1824
|
this.emit('error', err instanceof Error ? err : new Error(String(err)));
|
|
1927
1825
|
throw err;
|
|
1928
1826
|
}
|
|
@@ -2010,20 +1908,27 @@ class GameApplication extends EventEmitter {
|
|
|
2010
1908
|
this.audio = new AudioManager(this.config.audio);
|
|
2011
1909
|
// Input Manager
|
|
2012
1910
|
this.input = new InputManager(this.app.canvas);
|
|
2013
|
-
//
|
|
1911
|
+
// Stage layers: a scaled world root (scenes, transformed to design resolution by the
|
|
1912
|
+
// viewport) below an unscaled UI layer (screen space). app.stage itself stays identity.
|
|
1913
|
+
this.worldRoot = new Container();
|
|
1914
|
+
this.worldRoot.label = 'world';
|
|
1915
|
+
this.uiLayer = new Container();
|
|
1916
|
+
this.uiLayer.label = 'ui';
|
|
1917
|
+
this.app.stage.addChild(this.worldRoot, this.uiLayer);
|
|
1918
|
+
// Viewport Manager — scales worldRoot (NOT app.stage), so the UI layer is unscaled.
|
|
2014
1919
|
this.viewport = new ViewportManager(this.app, this._container, {
|
|
2015
1920
|
designWidth: this.config.designWidth,
|
|
2016
1921
|
designHeight: this.config.designHeight,
|
|
2017
1922
|
scaleMode: this.config.scaleMode,
|
|
2018
1923
|
orientation: this.config.orientation,
|
|
2019
|
-
});
|
|
2020
|
-
// Wire SceneManager to the
|
|
2021
|
-
this.scenes.setRoot(this.
|
|
1924
|
+
}, this.worldRoot);
|
|
1925
|
+
// Wire SceneManager to the scaled world root
|
|
1926
|
+
this.scenes.setRoot(this.worldRoot);
|
|
2022
1927
|
this.scenes.setApp(this);
|
|
2023
1928
|
// Wire viewport resize → scene manager + input manager
|
|
2024
1929
|
this.viewport.on('resize', ({ width, height, scale }) => {
|
|
2025
1930
|
this.scenes.resize(width, height);
|
|
2026
|
-
this.input.setViewportTransform(scale, this.
|
|
1931
|
+
this.input.setViewportTransform(scale, this.worldRoot.x, this.worldRoot.y);
|
|
2027
1932
|
this.emit('resize', { width, height });
|
|
2028
1933
|
});
|
|
2029
1934
|
this.viewport.on('orientationChange', (orientation) => {
|