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