@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.cjs.js
CHANGED
|
@@ -736,6 +736,7 @@ class AudioManager {
|
|
|
736
736
|
_persist;
|
|
737
737
|
_storageKey;
|
|
738
738
|
_categories;
|
|
739
|
+
_masterGain = 1.0;
|
|
739
740
|
_currentMusic = null;
|
|
740
741
|
_unlocked = false;
|
|
741
742
|
_unlockHandler = null;
|
|
@@ -795,7 +796,7 @@ class AudioManager {
|
|
|
795
796
|
if (this._globalMuted || this._categories[category].muted)
|
|
796
797
|
return;
|
|
797
798
|
const { sound } = this._soundModule;
|
|
798
|
-
const vol = (options?.volume ?? 1) * this._categories[category].volume;
|
|
799
|
+
const vol = (options?.volume ?? 1) * this._categories[category].volume * this._masterGain;
|
|
799
800
|
try {
|
|
800
801
|
sound.play(alias, {
|
|
801
802
|
volume: vol,
|
|
@@ -824,7 +825,7 @@ class AudioManager {
|
|
|
824
825
|
if (this._globalMuted || this._categories.music.muted)
|
|
825
826
|
return;
|
|
826
827
|
// Fade out the previous track
|
|
827
|
-
this.fadeVolume(prevAlias, this._categories.music.volume, 0, fadeDuration, () => {
|
|
828
|
+
this.fadeVolume(prevAlias, this._categories.music.volume * this._masterGain, 0, fadeDuration, () => {
|
|
828
829
|
try {
|
|
829
830
|
sound.stop(prevAlias);
|
|
830
831
|
}
|
|
@@ -836,7 +837,7 @@ class AudioManager {
|
|
|
836
837
|
volume: 0,
|
|
837
838
|
loop: true,
|
|
838
839
|
});
|
|
839
|
-
this.fadeVolume(alias, 0, this._categories.music.volume, fadeDuration);
|
|
840
|
+
this.fadeVolume(alias, 0, this._categories.music.volume * this._masterGain, fadeDuration);
|
|
840
841
|
}
|
|
841
842
|
catch (e) {
|
|
842
843
|
console.warn(`[AudioManager] Failed to play music "${alias}":`, e);
|
|
@@ -855,7 +856,7 @@ class AudioManager {
|
|
|
855
856
|
return;
|
|
856
857
|
try {
|
|
857
858
|
sound.play(alias, {
|
|
858
|
-
volume: this._categories.music.volume,
|
|
859
|
+
volume: this._categories.music.volume * this._masterGain,
|
|
859
860
|
loop: true,
|
|
860
861
|
});
|
|
861
862
|
}
|
|
@@ -889,6 +890,15 @@ class AudioManager {
|
|
|
889
890
|
sound.stopAll();
|
|
890
891
|
this._currentMusic = null;
|
|
891
892
|
}
|
|
893
|
+
/** Global gain (0..1) folded into every category's effective volume. Driven by the shell's
|
|
894
|
+
* 'master' settingChange. Does not affect the persisted per-category volumes. */
|
|
895
|
+
setMasterVolume(volume) {
|
|
896
|
+
this._masterGain = Math.max(0, Math.min(1, volume));
|
|
897
|
+
this.applyVolumes();
|
|
898
|
+
}
|
|
899
|
+
getMasterVolume() {
|
|
900
|
+
return this._masterGain;
|
|
901
|
+
}
|
|
892
902
|
/**
|
|
893
903
|
* Set volume for a category.
|
|
894
904
|
*/
|
|
@@ -1034,7 +1044,7 @@ class AudioManager {
|
|
|
1034
1044
|
const { sound } = this._soundModule;
|
|
1035
1045
|
// Global mute is owned by sound.muteAll()/unmuteAll() (context.muted),
|
|
1036
1046
|
// not by volumeAll — mixing both leaves mute un-undoable after reload.
|
|
1037
|
-
sound.volumeAll =
|
|
1047
|
+
sound.volumeAll = this._masterGain; // master multiplies the global bus
|
|
1038
1048
|
}
|
|
1039
1049
|
setupMobileUnlock() {
|
|
1040
1050
|
if (this._unlocked)
|
|
@@ -1302,6 +1312,7 @@ class ViewportManager extends EventEmitter {
|
|
|
1302
1312
|
_app;
|
|
1303
1313
|
_container;
|
|
1304
1314
|
_config;
|
|
1315
|
+
_target;
|
|
1305
1316
|
_resizeObserver = null;
|
|
1306
1317
|
_currentOrientation = exports.Orientation.LANDSCAPE;
|
|
1307
1318
|
_currentWidth = 0;
|
|
@@ -1309,11 +1320,15 @@ class ViewportManager extends EventEmitter {
|
|
|
1309
1320
|
_currentScale = 1;
|
|
1310
1321
|
_destroyed = false;
|
|
1311
1322
|
_resizeTimeout = null;
|
|
1312
|
-
constructor(app, container, config) {
|
|
1323
|
+
constructor(app, container, config, target) {
|
|
1313
1324
|
super();
|
|
1314
1325
|
this._app = app;
|
|
1315
1326
|
this._container = container;
|
|
1316
1327
|
this._config = config;
|
|
1328
|
+
// The container this manager scales/offsets. Defaults to app.stage for backward
|
|
1329
|
+
// compatibility; the engine passes a dedicated scaled world root so app.stage stays
|
|
1330
|
+
// identity (screen space) for unscaled UI layers.
|
|
1331
|
+
this._target = target ?? app.stage;
|
|
1317
1332
|
this.setupObserver();
|
|
1318
1333
|
}
|
|
1319
1334
|
/** Current canvas width in game units */
|
|
@@ -1402,19 +1417,19 @@ class ViewportManager extends EventEmitter {
|
|
|
1402
1417
|
const stageScale = scaleMode === exports.ScaleMode.STRETCH
|
|
1403
1418
|
? Math.min(containerWidth / designWidth, containerHeight / designHeight)
|
|
1404
1419
|
: scale;
|
|
1405
|
-
this.
|
|
1420
|
+
this._target.scale.set(stageScale);
|
|
1406
1421
|
// Center the stage for FIT mode
|
|
1407
1422
|
if (scaleMode === exports.ScaleMode.FIT) {
|
|
1408
|
-
this.
|
|
1409
|
-
this.
|
|
1423
|
+
this._target.x = Math.round((containerWidth - designWidth * stageScale) / 2);
|
|
1424
|
+
this._target.y = Math.round((containerHeight - designHeight * stageScale) / 2);
|
|
1410
1425
|
}
|
|
1411
1426
|
else if (scaleMode === exports.ScaleMode.FILL) {
|
|
1412
|
-
this.
|
|
1413
|
-
this.
|
|
1427
|
+
this._target.x = Math.round((containerWidth - gameWidth * stageScale) / 2);
|
|
1428
|
+
this._target.y = Math.round((containerHeight - gameHeight * stageScale) / 2);
|
|
1414
1429
|
}
|
|
1415
1430
|
else {
|
|
1416
|
-
this.
|
|
1417
|
-
this.
|
|
1431
|
+
this._target.x = 0;
|
|
1432
|
+
this._target.y = 0;
|
|
1418
1433
|
}
|
|
1419
1434
|
this._currentWidth = gameWidth;
|
|
1420
1435
|
this._currentHeight = gameHeight;
|
|
@@ -1503,34 +1518,20 @@ class Scene {
|
|
|
1503
1518
|
}
|
|
1504
1519
|
|
|
1505
1520
|
/**
|
|
1506
|
-
*
|
|
1507
|
-
* Uses unique IDs (prefixed with 'ls') to avoid collisions with CSSPreloader.
|
|
1508
|
-
*/
|
|
1509
|
-
function buildLoadingLogoSVG() {
|
|
1510
|
-
return loading.buildLogoSVG({
|
|
1511
|
-
idPrefix: 'ls',
|
|
1512
|
-
svgStyle: 'width:100%;height:auto;',
|
|
1513
|
-
clipRectId: 'ge-loader-rect',
|
|
1514
|
-
textId: 'ge-loader-pct',
|
|
1515
|
-
textContent: '0%',
|
|
1516
|
-
});
|
|
1517
|
-
}
|
|
1518
|
-
/**
|
|
1519
|
-
* Built-in loading screen using the Energy8 SVG logo with animated loader bar.
|
|
1521
|
+
* Built-in loading screen.
|
|
1520
1522
|
*
|
|
1521
|
-
*
|
|
1522
|
-
*
|
|
1523
|
+
* It does NOT render its own overlay — the CSS preloader created at boot
|
|
1524
|
+
* (`createPlatformSession`/`GameApplication.start`) stays on screen, and this
|
|
1525
|
+
* scene merely drives it: asset-load progress → `setCSSPreloaderProgress`,
|
|
1526
|
+
* tap-to-start → `waitCSSPreloaderTap`, then fades it out via
|
|
1527
|
+
* `removeCSSPreloader` before entering the game. One continuous overlay from
|
|
1528
|
+
* boot to gameplay — no second logo, no mid-load flash.
|
|
1523
1529
|
*/
|
|
1524
1530
|
class LoadingScene extends Scene {
|
|
1525
1531
|
_engine;
|
|
1526
1532
|
_targetScene;
|
|
1527
1533
|
_targetData;
|
|
1528
1534
|
_config;
|
|
1529
|
-
// HTML overlay
|
|
1530
|
-
_overlay = null;
|
|
1531
|
-
_loaderRect = null;
|
|
1532
|
-
_percentEl = null;
|
|
1533
|
-
_tapToStartEl = null;
|
|
1534
1535
|
// State
|
|
1535
1536
|
_displayedProgress = 0;
|
|
1536
1537
|
_targetProgress = 0;
|
|
@@ -1543,8 +1544,6 @@ class LoadingScene extends Scene {
|
|
|
1543
1544
|
this._targetData = targetData;
|
|
1544
1545
|
this._config = engine.config.loading ?? {};
|
|
1545
1546
|
this._startTime = Date.now();
|
|
1546
|
-
// Create the HTML overlay with the SVG logo
|
|
1547
|
-
this.createOverlay();
|
|
1548
1547
|
// Initialize asset manager
|
|
1549
1548
|
await this._engine.assets.init();
|
|
1550
1549
|
// Initialize audio manager
|
|
@@ -1590,110 +1589,29 @@ class LoadingScene extends Scene {
|
|
|
1590
1589
|
// Final snap to 100%
|
|
1591
1590
|
this._displayedProgress = 1;
|
|
1592
1591
|
this.updateLoaderBar(1);
|
|
1593
|
-
//
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
else {
|
|
1598
|
-
await this.transitionToGame();
|
|
1599
|
-
}
|
|
1592
|
+
// Wait for the player's tap — resolves immediately when tapToStart is
|
|
1593
|
+
// false (the preloader honours that flag) — then enter the game.
|
|
1594
|
+
await loading.waitCSSPreloaderTap();
|
|
1595
|
+
await this.transitionToGame();
|
|
1600
1596
|
}
|
|
1601
1597
|
onUpdate(dt) {
|
|
1602
|
-
// Smooth progress bar fill
|
|
1598
|
+
// Smooth progress bar fill (during active loading)
|
|
1603
1599
|
if (!this._loadingComplete && this._displayedProgress < this._targetProgress) {
|
|
1604
1600
|
this._displayedProgress = Math.min(this._displayedProgress + dt * 1.5, this._targetProgress);
|
|
1605
1601
|
this.updateLoaderBar(this._displayedProgress);
|
|
1606
1602
|
}
|
|
1607
1603
|
}
|
|
1608
1604
|
onResize(_width, _height) {
|
|
1609
|
-
//
|
|
1605
|
+
// The preloader overlay is CSS-based and auto-resizes.
|
|
1610
1606
|
}
|
|
1611
1607
|
onDestroy() {
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
createOverlay() {
|
|
1616
|
-
const bgColor = typeof this._config.backgroundColor === 'string'
|
|
1617
|
-
? this._config.backgroundColor
|
|
1618
|
-
: typeof this._config.backgroundColor === 'number'
|
|
1619
|
-
? `#${this._config.backgroundColor.toString(16).padStart(6, '0')}`
|
|
1620
|
-
: '#0a0a1a';
|
|
1621
|
-
const bgGradient = this._config.backgroundGradient ??
|
|
1622
|
-
`linear-gradient(135deg, ${bgColor} 0%, #1a1a3e 100%)`;
|
|
1623
|
-
this._overlay = document.createElement('div');
|
|
1624
|
-
this._overlay.id = '__ge-loading-overlay__';
|
|
1625
|
-
this._overlay.innerHTML = `
|
|
1626
|
-
<div class="ge-loading-content">
|
|
1627
|
-
${buildLoadingLogoSVG()}
|
|
1628
|
-
</div>
|
|
1629
|
-
`;
|
|
1630
|
-
const style = document.createElement('style');
|
|
1631
|
-
style.id = '__ge-loading-style__';
|
|
1632
|
-
style.textContent = `
|
|
1633
|
-
#__ge-loading-overlay__ {
|
|
1634
|
-
position: absolute;
|
|
1635
|
-
top: 0; left: 0;
|
|
1636
|
-
width: 100%; height: 100%;
|
|
1637
|
-
background: ${bgGradient};
|
|
1638
|
-
display: flex;
|
|
1639
|
-
align-items: center;
|
|
1640
|
-
justify-content: center;
|
|
1641
|
-
z-index: 9999;
|
|
1642
|
-
transition: opacity 0.5s ease-out;
|
|
1643
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
1644
|
-
}
|
|
1645
|
-
#__ge-loading-overlay__.ge-fade-out {
|
|
1646
|
-
opacity: 0;
|
|
1647
|
-
pointer-events: none;
|
|
1648
|
-
}
|
|
1649
|
-
.ge-loading-content {
|
|
1650
|
-
display: flex;
|
|
1651
|
-
flex-direction: column;
|
|
1652
|
-
align-items: center;
|
|
1653
|
-
width: 75%;
|
|
1654
|
-
max-width: 650px;
|
|
1655
|
-
}
|
|
1656
|
-
.ge-loading-content svg {
|
|
1657
|
-
filter: drop-shadow(0 0 40px rgba(121, 57, 194, 0.5));
|
|
1658
|
-
cursor: default;
|
|
1659
|
-
}
|
|
1660
|
-
|
|
1661
|
-
.ge-svg-pulse {
|
|
1662
|
-
animation: ge-tap-pulse 1.2s ease-in-out infinite;
|
|
1663
|
-
}
|
|
1664
|
-
@keyframes ge-tap-pulse {
|
|
1665
|
-
0%, 100% { opacity: 0.5; }
|
|
1666
|
-
50% { opacity: 1; }
|
|
1667
|
-
}
|
|
1668
|
-
`;
|
|
1669
|
-
// Get the container that holds the canvas
|
|
1670
|
-
const container = this._engine.app?.canvas?.parentElement;
|
|
1671
|
-
if (container) {
|
|
1672
|
-
container.style.position = container.style.position || 'relative';
|
|
1673
|
-
container.appendChild(style);
|
|
1674
|
-
container.appendChild(this._overlay);
|
|
1675
|
-
}
|
|
1676
|
-
// Cache the SVG loader rect for progress updates
|
|
1677
|
-
this._loaderRect = this._overlay.querySelector('#ge-loader-rect');
|
|
1678
|
-
this._percentEl = this._overlay.querySelector('#ge-loader-pct');
|
|
1679
|
-
}
|
|
1680
|
-
removeOverlay() {
|
|
1681
|
-
this._overlay?.remove();
|
|
1682
|
-
document.getElementById('__ge-loading-style__')?.remove();
|
|
1683
|
-
this._overlay = null;
|
|
1684
|
-
this._loaderRect = null;
|
|
1685
|
-
this._percentEl = null;
|
|
1686
|
-
this._tapToStartEl = null;
|
|
1608
|
+
// Defensive: ensure the preloader is gone even if we never transitioned
|
|
1609
|
+
// (e.g. the scene was popped externally). Idempotent.
|
|
1610
|
+
void loading.removeCSSPreloader(this.hostElement());
|
|
1687
1611
|
}
|
|
1688
1612
|
// ─── Progress ──────────────────────────────────────────
|
|
1689
1613
|
updateLoaderBar(progress) {
|
|
1690
|
-
|
|
1691
|
-
this._loaderRect.setAttribute('width', String(loading.LOADER_BAR_MAX_WIDTH * progress));
|
|
1692
|
-
}
|
|
1693
|
-
if (this._percentEl) {
|
|
1694
|
-
const pct = Math.round(progress * 100);
|
|
1695
|
-
this._percentEl.textContent = `${pct}%`;
|
|
1696
|
-
}
|
|
1614
|
+
loading.setCSSPreloaderProgress(Math.max(0, Math.min(1, progress)));
|
|
1697
1615
|
}
|
|
1698
1616
|
/**
|
|
1699
1617
|
* Smoothly animate the displayed progress from its current value to `target`
|
|
@@ -1723,49 +1641,20 @@ class LoadingScene extends Scene {
|
|
|
1723
1641
|
requestAnimationFrame(tick);
|
|
1724
1642
|
});
|
|
1725
1643
|
}
|
|
1726
|
-
// ─── Tap to Start ─────────────────────────────────────
|
|
1727
|
-
async showTapToStart() {
|
|
1728
|
-
const tapText = this._config.tapToStartText ?? 'TAP TO START';
|
|
1729
|
-
// Reuse the same SVG text element — replace percentage with tap text
|
|
1730
|
-
if (this._percentEl) {
|
|
1731
|
-
const el = this._percentEl;
|
|
1732
|
-
el.textContent = tapText;
|
|
1733
|
-
el.setAttribute('fill', '#ffffff');
|
|
1734
|
-
el.classList.add('ge-svg-pulse');
|
|
1735
|
-
this._tapToStartEl = el;
|
|
1736
|
-
}
|
|
1737
|
-
// Make overlay clickable
|
|
1738
|
-
if (this._overlay) {
|
|
1739
|
-
this._overlay.style.cursor = 'pointer';
|
|
1740
|
-
}
|
|
1741
|
-
// Wait for tap
|
|
1742
|
-
return new Promise((resolve) => {
|
|
1743
|
-
const handler = async () => {
|
|
1744
|
-
this._overlay?.removeEventListener('click', handler);
|
|
1745
|
-
await this.transitionToGame();
|
|
1746
|
-
resolve();
|
|
1747
|
-
};
|
|
1748
|
-
// Listen on the full overlay for easier mobile tap
|
|
1749
|
-
this._overlay?.addEventListener('click', handler);
|
|
1750
|
-
});
|
|
1751
|
-
}
|
|
1752
1644
|
// ─── Transition ────────────────────────────────────────
|
|
1645
|
+
/** The DOM element hosting the canvas + preloader overlay. */
|
|
1646
|
+
hostElement() {
|
|
1647
|
+
return this._engine?.app?.canvas?.parentElement ?? document.body;
|
|
1648
|
+
}
|
|
1753
1649
|
async transitionToGame() {
|
|
1754
|
-
// Fade out the
|
|
1755
|
-
|
|
1756
|
-
this._overlay.classList.add('ge-fade-out');
|
|
1757
|
-
await new Promise((resolve) => {
|
|
1758
|
-
this._overlay.addEventListener('transitionend', () => resolve(), { once: true });
|
|
1759
|
-
// Safety timeout
|
|
1760
|
-
setTimeout(resolve, 600);
|
|
1761
|
-
});
|
|
1762
|
-
}
|
|
1763
|
-
// Remove overlay
|
|
1764
|
-
this.removeOverlay();
|
|
1650
|
+
// Fade out and remove the shared CSS preloader (resolves after the fade).
|
|
1651
|
+
await loading.removeCSSPreloader(this.hostElement());
|
|
1765
1652
|
// Navigate to the target scene, always passing the engine reference
|
|
1766
1653
|
await this._engine.scenes.goto(this._targetScene, {
|
|
1767
1654
|
engine: this._engine,
|
|
1768
|
-
...(this._targetData && typeof this._targetData === 'object'
|
|
1655
|
+
...(this._targetData && typeof this._targetData === 'object'
|
|
1656
|
+
? this._targetData
|
|
1657
|
+
: { data: this._targetData }),
|
|
1769
1658
|
});
|
|
1770
1659
|
}
|
|
1771
1660
|
}
|
|
@@ -1905,6 +1794,12 @@ class GameApplication extends EventEmitter {
|
|
|
1905
1794
|
input;
|
|
1906
1795
|
/** Viewport manager */
|
|
1907
1796
|
viewport;
|
|
1797
|
+
/** Scaled world root (holds scenes). Transformed by the ViewportManager to fit the design
|
|
1798
|
+
* resolution; lives below the UI layer on app.stage. */
|
|
1799
|
+
worldRoot;
|
|
1800
|
+
/** Unscaled, screen-space UI layer. Sits above {@link worldRoot} and is NOT touched by the
|
|
1801
|
+
* viewport transform — children fill the real screen (e.g. the host's shell + overlay). */
|
|
1802
|
+
uiLayer;
|
|
1908
1803
|
/** SDK instance (null in offline mode) */
|
|
1909
1804
|
sdk = null;
|
|
1910
1805
|
/** FPS overlay instance (only when debug: true) */
|
|
@@ -1986,17 +1881,20 @@ class GameApplication extends EventEmitter {
|
|
|
1986
1881
|
// 6. Initialize sub-systems
|
|
1987
1882
|
this.initSubSystems();
|
|
1988
1883
|
this.emit('initialized');
|
|
1989
|
-
// 7.
|
|
1990
|
-
|
|
1991
|
-
//
|
|
1884
|
+
// 7. Load assets. The CSS preloader stays on screen — LoadingScene drives
|
|
1885
|
+
// its progress/tap and removes it before entering the game, so there's
|
|
1886
|
+
// a single continuous overlay from boot to gameplay (no logo flash).
|
|
1992
1887
|
await this.loadAssets(firstScene, sceneData);
|
|
1993
1888
|
this.emit('loaded');
|
|
1994
|
-
//
|
|
1889
|
+
// 8. Start the game loop
|
|
1995
1890
|
this._running = true;
|
|
1996
1891
|
this.emit('started');
|
|
1997
1892
|
}
|
|
1998
1893
|
catch (err) {
|
|
1999
1894
|
console.error('[GameEngine] Failed to start:', err);
|
|
1895
|
+
// Tear down the preloader so a failure doesn't strand the brand frame.
|
|
1896
|
+
if (this._container)
|
|
1897
|
+
loading.removeCSSPreloader(this._container);
|
|
2000
1898
|
this.emit('error', err instanceof Error ? err : new Error(String(err)));
|
|
2001
1899
|
throw err;
|
|
2002
1900
|
}
|
|
@@ -2084,20 +1982,27 @@ class GameApplication extends EventEmitter {
|
|
|
2084
1982
|
this.audio = new AudioManager(this.config.audio);
|
|
2085
1983
|
// Input Manager
|
|
2086
1984
|
this.input = new InputManager(this.app.canvas);
|
|
2087
|
-
//
|
|
1985
|
+
// Stage layers: a scaled world root (scenes, transformed to design resolution by the
|
|
1986
|
+
// viewport) below an unscaled UI layer (screen space). app.stage itself stays identity.
|
|
1987
|
+
this.worldRoot = new pixi_js.Container();
|
|
1988
|
+
this.worldRoot.label = 'world';
|
|
1989
|
+
this.uiLayer = new pixi_js.Container();
|
|
1990
|
+
this.uiLayer.label = 'ui';
|
|
1991
|
+
this.app.stage.addChild(this.worldRoot, this.uiLayer);
|
|
1992
|
+
// Viewport Manager — scales worldRoot (NOT app.stage), so the UI layer is unscaled.
|
|
2088
1993
|
this.viewport = new ViewportManager(this.app, this._container, {
|
|
2089
1994
|
designWidth: this.config.designWidth,
|
|
2090
1995
|
designHeight: this.config.designHeight,
|
|
2091
1996
|
scaleMode: this.config.scaleMode,
|
|
2092
1997
|
orientation: this.config.orientation,
|
|
2093
|
-
});
|
|
2094
|
-
// Wire SceneManager to the
|
|
2095
|
-
this.scenes.setRoot(this.
|
|
1998
|
+
}, this.worldRoot);
|
|
1999
|
+
// Wire SceneManager to the scaled world root
|
|
2000
|
+
this.scenes.setRoot(this.worldRoot);
|
|
2096
2001
|
this.scenes.setApp(this);
|
|
2097
2002
|
// Wire viewport resize → scene manager + input manager
|
|
2098
2003
|
this.viewport.on('resize', ({ width, height, scale }) => {
|
|
2099
2004
|
this.scenes.resize(width, height);
|
|
2100
|
-
this.input.setViewportTransform(scale, this.
|
|
2005
|
+
this.input.setViewportTransform(scale, this.worldRoot.x, this.worldRoot.y);
|
|
2101
2006
|
this.emit('resize', { width, height });
|
|
2102
2007
|
});
|
|
2103
2008
|
this.viewport.on('orientationChange', (orientation) => {
|