@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.
Files changed (52) hide show
  1. package/README.md +121 -0
  2. package/dist/animation.cjs.js +18 -3
  3. package/dist/animation.cjs.js.map +1 -1
  4. package/dist/animation.esm.js +18 -3
  5. package/dist/animation.esm.js.map +1 -1
  6. package/dist/core.cjs.js +51 -159
  7. package/dist/core.cjs.js.map +1 -1
  8. package/dist/core.esm.js +52 -160
  9. package/dist/core.esm.js.map +1 -1
  10. package/dist/host.cjs.js +88 -189
  11. package/dist/host.cjs.js.map +1 -1
  12. package/dist/host.d.ts +6 -0
  13. package/dist/host.esm.js +90 -191
  14. package/dist/host.esm.js.map +1 -1
  15. package/dist/index.cjs.js +51 -159
  16. package/dist/index.cjs.js.map +1 -1
  17. package/dist/index.d.ts +10 -11
  18. package/dist/index.esm.js +52 -160
  19. package/dist/index.esm.js.map +1 -1
  20. package/dist/react.cjs.js +18 -3
  21. package/dist/react.cjs.js.map +1 -1
  22. package/dist/react.esm.js +18 -3
  23. package/dist/react.esm.js.map +1 -1
  24. package/dist/slot.cjs.js +1872 -31
  25. package/dist/slot.cjs.js.map +1 -1
  26. package/dist/slot.d.ts +555 -5
  27. package/dist/slot.esm.js +1857 -33
  28. package/dist/slot.esm.js.map +1 -1
  29. package/dist/ui.cjs.js +18 -3
  30. package/dist/ui.cjs.js.map +1 -1
  31. package/dist/ui.esm.js +18 -3
  32. package/dist/ui.esm.js.map +1 -1
  33. package/package.json +1 -1
  34. package/src/animation/Tween.ts +14 -3
  35. package/src/core/GameApplication.ts +6 -5
  36. package/src/host/shellConfig.ts +44 -31
  37. package/src/loading/LoadingScene.ts +31 -174
  38. package/src/slot/anim/easing-map.ts +16 -2
  39. package/src/slot/cascade/TumbleController.ts +230 -0
  40. package/src/slot/config/ReelSystemConfig.ts +559 -0
  41. package/src/slot/config/presets.ts +222 -0
  42. package/src/slot/features/extra.ts +189 -0
  43. package/src/slot/features/index.ts +44 -0
  44. package/src/slot/features/symbols.ts +183 -0
  45. package/src/slot/features/types.ts +136 -0
  46. package/src/slot/features/wilds.ts +151 -0
  47. package/src/slot/grid/ReelGrid.ts +93 -24
  48. package/src/slot/grid/SymbolCell.ts +45 -11
  49. package/src/slot/index.ts +61 -0
  50. package/src/slot/motion/AnticipationController.ts +96 -0
  51. package/src/slot/motion/SpinEngine.ts +384 -0
  52. 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
- * Build the loading scene variant of the logo SVG.
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
- * Renders as an HTML overlay on top of the canvas for crisp SVG quality.
1537
- * The loader bar fill width is driven by asset loading progress.
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
- // Show "Tap to Start" or transition directly
1609
- if (this._config.tapToStart !== false) {
1610
- await this.showTapToStart();
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 via HTML (during active loading)
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
- // Overlay is CSS-based, auto-resizes
1620
+ // The preloader overlay is CSS-based and auto-resizes.
1625
1621
  }
1626
1622
  onDestroy() {
1627
- this.removeOverlay();
1628
- }
1629
- // ─── HTML Overlay ──────────────────────────────────────
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
- if (this._loaderRect) {
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 HTML overlay
1770
- if (this._overlay) {
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' ? this._targetData : { data: this._targetData }),
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. Remove CSS preloader, show Canvas loading screen
2011
- loading.removeCSSPreloader(this._container);
2012
- // 8. Load assets with loading screen
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
- // 9. Start the game loop
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
  }