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