@energy8platform/game-engine 0.19.0 → 0.21.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/README.md +121 -0
- package/dist/animation.cjs.js +18 -3
- package/dist/animation.cjs.js.map +1 -1
- package/dist/animation.esm.js +18 -3
- package/dist/animation.esm.js.map +1 -1
- package/dist/core.cjs.js +51 -159
- package/dist/core.cjs.js.map +1 -1
- package/dist/core.esm.js +52 -160
- package/dist/core.esm.js.map +1 -1
- package/dist/host.cjs.js +88 -189
- package/dist/host.cjs.js.map +1 -1
- package/dist/host.d.ts +6 -0
- package/dist/host.esm.js +90 -191
- package/dist/host.esm.js.map +1 -1
- package/dist/index.cjs.js +51 -159
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +10 -11
- package/dist/index.esm.js +52 -160
- package/dist/index.esm.js.map +1 -1
- package/dist/react.cjs.js +18 -3
- package/dist/react.cjs.js.map +1 -1
- package/dist/react.esm.js +18 -3
- package/dist/react.esm.js.map +1 -1
- package/dist/slot.cjs.js +1872 -31
- package/dist/slot.cjs.js.map +1 -1
- package/dist/slot.d.ts +555 -5
- package/dist/slot.esm.js +1857 -33
- package/dist/slot.esm.js.map +1 -1
- package/dist/ui.cjs.js +18 -3
- package/dist/ui.cjs.js.map +1 -1
- package/dist/ui.esm.js +18 -3
- package/dist/ui.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/animation/Tween.ts +14 -3
- package/src/core/GameApplication.ts +6 -5
- package/src/host/shellConfig.ts +44 -31
- package/src/loading/LoadingScene.ts +31 -174
- package/src/slot/anim/easing-map.ts +16 -2
- package/src/slot/cascade/TumbleController.ts +230 -0
- package/src/slot/config/ReelSystemConfig.ts +559 -0
- package/src/slot/config/presets.ts +222 -0
- package/src/slot/features/extra.ts +189 -0
- package/src/slot/features/index.ts +44 -0
- package/src/slot/features/symbols.ts +183 -0
- package/src/slot/features/types.ts +136 -0
- package/src/slot/features/wilds.ts +151 -0
- package/src/slot/grid/ReelGrid.ts +93 -24
- package/src/slot/grid/SymbolCell.ts +45 -11
- package/src/slot/index.ts +61 -0
- package/src/slot/motion/AnticipationController.ts +96 -0
- package/src/slot/motion/SpinEngine.ts +384 -0
- package/src/slot/system/ReelSystem.ts +295 -0
package/dist/index.cjs.js
CHANGED
|
@@ -189,6 +189,10 @@ class Tween {
|
|
|
189
189
|
* @param onUpdate - Progress callback (0..1)
|
|
190
190
|
*/
|
|
191
191
|
static to(target, props, duration, easing, onUpdate) {
|
|
192
|
+
// A destroyed (Pixi) target has null transform fields — skip rather than throw. This guards
|
|
193
|
+
// animations whose target is torn down mid-flight (e.g. a reel grid rebuilt during a spin).
|
|
194
|
+
if (target == null || target.destroyed)
|
|
195
|
+
return Promise.resolve();
|
|
192
196
|
return new Promise((resolve) => {
|
|
193
197
|
// Capture starting values
|
|
194
198
|
const from = {};
|
|
@@ -214,6 +218,8 @@ class Tween {
|
|
|
214
218
|
* Animate properties from given values to current values.
|
|
215
219
|
*/
|
|
216
220
|
static from(target, props, duration, easing, onUpdate) {
|
|
221
|
+
if (target == null || target.destroyed)
|
|
222
|
+
return Promise.resolve();
|
|
217
223
|
// Capture current values as "to"
|
|
218
224
|
const to = {};
|
|
219
225
|
for (const key of Object.keys(props)) {
|
|
@@ -226,6 +232,8 @@ class Tween {
|
|
|
226
232
|
* Animate from one set of values to another.
|
|
227
233
|
*/
|
|
228
234
|
static fromTo(target, fromProps, toProps, duration, easing, onUpdate) {
|
|
235
|
+
if (target == null || target.destroyed)
|
|
236
|
+
return Promise.resolve();
|
|
229
237
|
// Set starting values
|
|
230
238
|
for (const key of Object.keys(fromProps)) {
|
|
231
239
|
Tween.setProperty(target, key, fromProps[key]);
|
|
@@ -299,6 +307,11 @@ class Tween {
|
|
|
299
307
|
const dt = ticker.deltaMS;
|
|
300
308
|
const completed = [];
|
|
301
309
|
for (const tw of Tween._tweens) {
|
|
310
|
+
// target torn down mid-tween → finish it quietly
|
|
311
|
+
if (tw.target?.destroyed) {
|
|
312
|
+
completed.push(tw);
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
302
315
|
tw.elapsed += dt;
|
|
303
316
|
if (tw.elapsed < tw.delay)
|
|
304
317
|
continue;
|
|
@@ -336,9 +349,9 @@ class Tween {
|
|
|
336
349
|
const parts = key.split('.');
|
|
337
350
|
let obj = target;
|
|
338
351
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
339
|
-
obj = obj[parts[i]];
|
|
352
|
+
obj = obj?.[parts[i]];
|
|
340
353
|
}
|
|
341
|
-
return obj[parts[parts.length - 1]] ?? 0;
|
|
354
|
+
return obj?.[parts[parts.length - 1]] ?? 0;
|
|
342
355
|
}
|
|
343
356
|
/**
|
|
344
357
|
* Set a potentially nested property.
|
|
@@ -347,8 +360,10 @@ class Tween {
|
|
|
347
360
|
const parts = key.split('.');
|
|
348
361
|
let obj = target;
|
|
349
362
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
350
|
-
obj = obj[parts[i]];
|
|
363
|
+
obj = obj?.[parts[i]];
|
|
351
364
|
}
|
|
365
|
+
if (obj == null)
|
|
366
|
+
return;
|
|
352
367
|
obj[parts[parts.length - 1]] = value;
|
|
353
368
|
}
|
|
354
369
|
}
|
|
@@ -1518,34 +1533,20 @@ class Scene {
|
|
|
1518
1533
|
}
|
|
1519
1534
|
|
|
1520
1535
|
/**
|
|
1521
|
-
*
|
|
1522
|
-
* Uses unique IDs (prefixed with 'ls') to avoid collisions with CSSPreloader.
|
|
1523
|
-
*/
|
|
1524
|
-
function buildLoadingLogoSVG() {
|
|
1525
|
-
return loading.buildLogoSVG({
|
|
1526
|
-
idPrefix: 'ls',
|
|
1527
|
-
svgStyle: 'width:100%;height:auto;',
|
|
1528
|
-
clipRectId: 'ge-loader-rect',
|
|
1529
|
-
textId: 'ge-loader-pct',
|
|
1530
|
-
textContent: '0%',
|
|
1531
|
-
});
|
|
1532
|
-
}
|
|
1533
|
-
/**
|
|
1534
|
-
* Built-in loading screen using the Energy8 SVG logo with animated loader bar.
|
|
1536
|
+
* Built-in loading screen.
|
|
1535
1537
|
*
|
|
1536
|
-
*
|
|
1537
|
-
*
|
|
1538
|
+
* It does NOT render its own overlay — the CSS preloader created at boot
|
|
1539
|
+
* (`createPlatformSession`/`GameApplication.start`) stays on screen, and this
|
|
1540
|
+
* scene merely drives it: asset-load progress → `setCSSPreloaderProgress`,
|
|
1541
|
+
* tap-to-start → `waitCSSPreloaderTap`, then fades it out via
|
|
1542
|
+
* `removeCSSPreloader` before entering the game. One continuous overlay from
|
|
1543
|
+
* boot to gameplay — no second logo, no mid-load flash.
|
|
1538
1544
|
*/
|
|
1539
1545
|
class LoadingScene extends Scene {
|
|
1540
1546
|
_engine;
|
|
1541
1547
|
_targetScene;
|
|
1542
1548
|
_targetData;
|
|
1543
1549
|
_config;
|
|
1544
|
-
// HTML overlay
|
|
1545
|
-
_overlay = null;
|
|
1546
|
-
_loaderRect = null;
|
|
1547
|
-
_percentEl = null;
|
|
1548
|
-
_tapToStartEl = null;
|
|
1549
1550
|
// State
|
|
1550
1551
|
_displayedProgress = 0;
|
|
1551
1552
|
_targetProgress = 0;
|
|
@@ -1558,8 +1559,6 @@ class LoadingScene extends Scene {
|
|
|
1558
1559
|
this._targetData = targetData;
|
|
1559
1560
|
this._config = engine.config.loading ?? {};
|
|
1560
1561
|
this._startTime = Date.now();
|
|
1561
|
-
// Create the HTML overlay with the SVG logo
|
|
1562
|
-
this.createOverlay();
|
|
1563
1562
|
// Initialize asset manager
|
|
1564
1563
|
await this._engine.assets.init();
|
|
1565
1564
|
// Initialize audio manager
|
|
@@ -1605,110 +1604,29 @@ class LoadingScene extends Scene {
|
|
|
1605
1604
|
// Final snap to 100%
|
|
1606
1605
|
this._displayedProgress = 1;
|
|
1607
1606
|
this.updateLoaderBar(1);
|
|
1608
|
-
//
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
else {
|
|
1613
|
-
await this.transitionToGame();
|
|
1614
|
-
}
|
|
1607
|
+
// Wait for the player's tap — resolves immediately when tapToStart is
|
|
1608
|
+
// false (the preloader honours that flag) — then enter the game.
|
|
1609
|
+
await loading.waitCSSPreloaderTap();
|
|
1610
|
+
await this.transitionToGame();
|
|
1615
1611
|
}
|
|
1616
1612
|
onUpdate(dt) {
|
|
1617
|
-
// Smooth progress bar fill
|
|
1613
|
+
// Smooth progress bar fill (during active loading)
|
|
1618
1614
|
if (!this._loadingComplete && this._displayedProgress < this._targetProgress) {
|
|
1619
1615
|
this._displayedProgress = Math.min(this._displayedProgress + dt * 1.5, this._targetProgress);
|
|
1620
1616
|
this.updateLoaderBar(this._displayedProgress);
|
|
1621
1617
|
}
|
|
1622
1618
|
}
|
|
1623
1619
|
onResize(_width, _height) {
|
|
1624
|
-
//
|
|
1620
|
+
// The preloader overlay is CSS-based and auto-resizes.
|
|
1625
1621
|
}
|
|
1626
1622
|
onDestroy() {
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
createOverlay() {
|
|
1631
|
-
const bgColor = typeof this._config.backgroundColor === 'string'
|
|
1632
|
-
? this._config.backgroundColor
|
|
1633
|
-
: typeof this._config.backgroundColor === 'number'
|
|
1634
|
-
? `#${this._config.backgroundColor.toString(16).padStart(6, '0')}`
|
|
1635
|
-
: '#0a0a1a';
|
|
1636
|
-
const bgGradient = this._config.backgroundGradient ??
|
|
1637
|
-
`linear-gradient(135deg, ${bgColor} 0%, #1a1a3e 100%)`;
|
|
1638
|
-
this._overlay = document.createElement('div');
|
|
1639
|
-
this._overlay.id = '__ge-loading-overlay__';
|
|
1640
|
-
this._overlay.innerHTML = `
|
|
1641
|
-
<div class="ge-loading-content">
|
|
1642
|
-
${buildLoadingLogoSVG()}
|
|
1643
|
-
</div>
|
|
1644
|
-
`;
|
|
1645
|
-
const style = document.createElement('style');
|
|
1646
|
-
style.id = '__ge-loading-style__';
|
|
1647
|
-
style.textContent = `
|
|
1648
|
-
#__ge-loading-overlay__ {
|
|
1649
|
-
position: absolute;
|
|
1650
|
-
top: 0; left: 0;
|
|
1651
|
-
width: 100%; height: 100%;
|
|
1652
|
-
background: ${bgGradient};
|
|
1653
|
-
display: flex;
|
|
1654
|
-
align-items: center;
|
|
1655
|
-
justify-content: center;
|
|
1656
|
-
z-index: 9999;
|
|
1657
|
-
transition: opacity 0.5s ease-out;
|
|
1658
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
1659
|
-
}
|
|
1660
|
-
#__ge-loading-overlay__.ge-fade-out {
|
|
1661
|
-
opacity: 0;
|
|
1662
|
-
pointer-events: none;
|
|
1663
|
-
}
|
|
1664
|
-
.ge-loading-content {
|
|
1665
|
-
display: flex;
|
|
1666
|
-
flex-direction: column;
|
|
1667
|
-
align-items: center;
|
|
1668
|
-
width: 75%;
|
|
1669
|
-
max-width: 650px;
|
|
1670
|
-
}
|
|
1671
|
-
.ge-loading-content svg {
|
|
1672
|
-
filter: drop-shadow(0 0 40px rgba(121, 57, 194, 0.5));
|
|
1673
|
-
cursor: default;
|
|
1674
|
-
}
|
|
1675
|
-
|
|
1676
|
-
.ge-svg-pulse {
|
|
1677
|
-
animation: ge-tap-pulse 1.2s ease-in-out infinite;
|
|
1678
|
-
}
|
|
1679
|
-
@keyframes ge-tap-pulse {
|
|
1680
|
-
0%, 100% { opacity: 0.5; }
|
|
1681
|
-
50% { opacity: 1; }
|
|
1682
|
-
}
|
|
1683
|
-
`;
|
|
1684
|
-
// Get the container that holds the canvas
|
|
1685
|
-
const container = this._engine.app?.canvas?.parentElement;
|
|
1686
|
-
if (container) {
|
|
1687
|
-
container.style.position = container.style.position || 'relative';
|
|
1688
|
-
container.appendChild(style);
|
|
1689
|
-
container.appendChild(this._overlay);
|
|
1690
|
-
}
|
|
1691
|
-
// Cache the SVG loader rect for progress updates
|
|
1692
|
-
this._loaderRect = this._overlay.querySelector('#ge-loader-rect');
|
|
1693
|
-
this._percentEl = this._overlay.querySelector('#ge-loader-pct');
|
|
1694
|
-
}
|
|
1695
|
-
removeOverlay() {
|
|
1696
|
-
this._overlay?.remove();
|
|
1697
|
-
document.getElementById('__ge-loading-style__')?.remove();
|
|
1698
|
-
this._overlay = null;
|
|
1699
|
-
this._loaderRect = null;
|
|
1700
|
-
this._percentEl = null;
|
|
1701
|
-
this._tapToStartEl = null;
|
|
1623
|
+
// Defensive: ensure the preloader is gone even if we never transitioned
|
|
1624
|
+
// (e.g. the scene was popped externally). Idempotent.
|
|
1625
|
+
void loading.removeCSSPreloader(this.hostElement());
|
|
1702
1626
|
}
|
|
1703
1627
|
// ─── Progress ──────────────────────────────────────────
|
|
1704
1628
|
updateLoaderBar(progress) {
|
|
1705
|
-
|
|
1706
|
-
this._loaderRect.setAttribute('width', String(loading.LOADER_BAR_MAX_WIDTH * progress));
|
|
1707
|
-
}
|
|
1708
|
-
if (this._percentEl) {
|
|
1709
|
-
const pct = Math.round(progress * 100);
|
|
1710
|
-
this._percentEl.textContent = `${pct}%`;
|
|
1711
|
-
}
|
|
1629
|
+
loading.setCSSPreloaderProgress(Math.max(0, Math.min(1, progress)));
|
|
1712
1630
|
}
|
|
1713
1631
|
/**
|
|
1714
1632
|
* Smoothly animate the displayed progress from its current value to `target`
|
|
@@ -1738,49 +1656,20 @@ class LoadingScene extends Scene {
|
|
|
1738
1656
|
requestAnimationFrame(tick);
|
|
1739
1657
|
});
|
|
1740
1658
|
}
|
|
1741
|
-
// ─── Tap to Start ─────────────────────────────────────
|
|
1742
|
-
async showTapToStart() {
|
|
1743
|
-
const tapText = this._config.tapToStartText ?? 'TAP TO START';
|
|
1744
|
-
// Reuse the same SVG text element — replace percentage with tap text
|
|
1745
|
-
if (this._percentEl) {
|
|
1746
|
-
const el = this._percentEl;
|
|
1747
|
-
el.textContent = tapText;
|
|
1748
|
-
el.setAttribute('fill', '#ffffff');
|
|
1749
|
-
el.classList.add('ge-svg-pulse');
|
|
1750
|
-
this._tapToStartEl = el;
|
|
1751
|
-
}
|
|
1752
|
-
// Make overlay clickable
|
|
1753
|
-
if (this._overlay) {
|
|
1754
|
-
this._overlay.style.cursor = 'pointer';
|
|
1755
|
-
}
|
|
1756
|
-
// Wait for tap
|
|
1757
|
-
return new Promise((resolve) => {
|
|
1758
|
-
const handler = async () => {
|
|
1759
|
-
this._overlay?.removeEventListener('click', handler);
|
|
1760
|
-
await this.transitionToGame();
|
|
1761
|
-
resolve();
|
|
1762
|
-
};
|
|
1763
|
-
// Listen on the full overlay for easier mobile tap
|
|
1764
|
-
this._overlay?.addEventListener('click', handler);
|
|
1765
|
-
});
|
|
1766
|
-
}
|
|
1767
1659
|
// ─── Transition ────────────────────────────────────────
|
|
1660
|
+
/** The DOM element hosting the canvas + preloader overlay. */
|
|
1661
|
+
hostElement() {
|
|
1662
|
+
return this._engine?.app?.canvas?.parentElement ?? document.body;
|
|
1663
|
+
}
|
|
1768
1664
|
async transitionToGame() {
|
|
1769
|
-
// Fade out the
|
|
1770
|
-
|
|
1771
|
-
this._overlay.classList.add('ge-fade-out');
|
|
1772
|
-
await new Promise((resolve) => {
|
|
1773
|
-
this._overlay.addEventListener('transitionend', () => resolve(), { once: true });
|
|
1774
|
-
// Safety timeout
|
|
1775
|
-
setTimeout(resolve, 600);
|
|
1776
|
-
});
|
|
1777
|
-
}
|
|
1778
|
-
// Remove overlay
|
|
1779
|
-
this.removeOverlay();
|
|
1665
|
+
// Fade out and remove the shared CSS preloader (resolves after the fade).
|
|
1666
|
+
await loading.removeCSSPreloader(this.hostElement());
|
|
1780
1667
|
// Navigate to the target scene, always passing the engine reference
|
|
1781
1668
|
await this._engine.scenes.goto(this._targetScene, {
|
|
1782
1669
|
engine: this._engine,
|
|
1783
|
-
...(this._targetData && typeof this._targetData === 'object'
|
|
1670
|
+
...(this._targetData && typeof this._targetData === 'object'
|
|
1671
|
+
? this._targetData
|
|
1672
|
+
: { data: this._targetData }),
|
|
1784
1673
|
});
|
|
1785
1674
|
}
|
|
1786
1675
|
}
|
|
@@ -2007,17 +1896,20 @@ class GameApplication extends EventEmitter {
|
|
|
2007
1896
|
// 6. Initialize sub-systems
|
|
2008
1897
|
this.initSubSystems();
|
|
2009
1898
|
this.emit('initialized');
|
|
2010
|
-
// 7.
|
|
2011
|
-
|
|
2012
|
-
//
|
|
1899
|
+
// 7. Load assets. The CSS preloader stays on screen — LoadingScene drives
|
|
1900
|
+
// its progress/tap and removes it before entering the game, so there's
|
|
1901
|
+
// a single continuous overlay from boot to gameplay (no logo flash).
|
|
2013
1902
|
await this.loadAssets(firstScene, sceneData);
|
|
2014
1903
|
this.emit('loaded');
|
|
2015
|
-
//
|
|
1904
|
+
// 8. Start the game loop
|
|
2016
1905
|
this._running = true;
|
|
2017
1906
|
this.emit('started');
|
|
2018
1907
|
}
|
|
2019
1908
|
catch (err) {
|
|
2020
1909
|
console.error('[GameEngine] Failed to start:', err);
|
|
1910
|
+
// Tear down the preloader so a failure doesn't strand the brand frame.
|
|
1911
|
+
if (this._container)
|
|
1912
|
+
loading.removeCSSPreloader(this._container);
|
|
2021
1913
|
this.emit('error', err instanceof Error ? err : new Error(String(err)));
|
|
2022
1914
|
throw err;
|
|
2023
1915
|
}
|