@energy8platform/game-engine 0.19.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/core.cjs.js +33 -156
- package/dist/core.cjs.js.map +1 -1
- package/dist/core.esm.js +34 -157
- package/dist/core.esm.js.map +1 -1
- package/dist/host.cjs.js +70 -186
- package/dist/host.cjs.js.map +1 -1
- package/dist/host.d.ts +6 -0
- package/dist/host.esm.js +72 -188
- package/dist/host.esm.js.map +1 -1
- package/dist/index.cjs.js +33 -156
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +10 -11
- package/dist/index.esm.js +34 -157
- package/dist/index.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/core/GameApplication.ts +6 -5
- package/src/host/shellConfig.ts +44 -31
- package/src/loading/LoadingScene.ts +31 -174
package/dist/host.esm.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Ticker, Assets, Container, Text, Application, Graphics, TextureSource } from 'pixi.js';
|
|
2
2
|
import { createPlatformSession } from '@energy8platform/platform-core';
|
|
3
|
-
import {
|
|
4
|
-
import { socialize } from '@energy8platform/platform-core/shell';
|
|
3
|
+
import { waitCSSPreloaderTap, removeCSSPreloader, setCSSPreloaderProgress, createCSSPreloader } from '@energy8platform/platform-core/loading';
|
|
4
|
+
import { socialize, createI18n } from '@energy8platform/platform-core/shell';
|
|
5
5
|
export { socialize } from '@energy8platform/platform-core/shell';
|
|
6
6
|
|
|
7
7
|
// ─── Scale Modes ───────────────────────────────────────────
|
|
@@ -1516,34 +1516,20 @@ class Scene {
|
|
|
1516
1516
|
}
|
|
1517
1517
|
|
|
1518
1518
|
/**
|
|
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.
|
|
1519
|
+
* Built-in loading screen.
|
|
1533
1520
|
*
|
|
1534
|
-
*
|
|
1535
|
-
*
|
|
1521
|
+
* It does NOT render its own overlay — the CSS preloader created at boot
|
|
1522
|
+
* (`createPlatformSession`/`GameApplication.start`) stays on screen, and this
|
|
1523
|
+
* scene merely drives it: asset-load progress → `setCSSPreloaderProgress`,
|
|
1524
|
+
* tap-to-start → `waitCSSPreloaderTap`, then fades it out via
|
|
1525
|
+
* `removeCSSPreloader` before entering the game. One continuous overlay from
|
|
1526
|
+
* boot to gameplay — no second logo, no mid-load flash.
|
|
1536
1527
|
*/
|
|
1537
1528
|
class LoadingScene extends Scene {
|
|
1538
1529
|
_engine;
|
|
1539
1530
|
_targetScene;
|
|
1540
1531
|
_targetData;
|
|
1541
1532
|
_config;
|
|
1542
|
-
// HTML overlay
|
|
1543
|
-
_overlay = null;
|
|
1544
|
-
_loaderRect = null;
|
|
1545
|
-
_percentEl = null;
|
|
1546
|
-
_tapToStartEl = null;
|
|
1547
1533
|
// State
|
|
1548
1534
|
_displayedProgress = 0;
|
|
1549
1535
|
_targetProgress = 0;
|
|
@@ -1556,8 +1542,6 @@ class LoadingScene extends Scene {
|
|
|
1556
1542
|
this._targetData = targetData;
|
|
1557
1543
|
this._config = engine.config.loading ?? {};
|
|
1558
1544
|
this._startTime = Date.now();
|
|
1559
|
-
// Create the HTML overlay with the SVG logo
|
|
1560
|
-
this.createOverlay();
|
|
1561
1545
|
// Initialize asset manager
|
|
1562
1546
|
await this._engine.assets.init();
|
|
1563
1547
|
// Initialize audio manager
|
|
@@ -1603,110 +1587,29 @@ class LoadingScene extends Scene {
|
|
|
1603
1587
|
// Final snap to 100%
|
|
1604
1588
|
this._displayedProgress = 1;
|
|
1605
1589
|
this.updateLoaderBar(1);
|
|
1606
|
-
//
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
else {
|
|
1611
|
-
await this.transitionToGame();
|
|
1612
|
-
}
|
|
1590
|
+
// Wait for the player's tap — resolves immediately when tapToStart is
|
|
1591
|
+
// false (the preloader honours that flag) — then enter the game.
|
|
1592
|
+
await waitCSSPreloaderTap();
|
|
1593
|
+
await this.transitionToGame();
|
|
1613
1594
|
}
|
|
1614
1595
|
onUpdate(dt) {
|
|
1615
|
-
// Smooth progress bar fill
|
|
1596
|
+
// Smooth progress bar fill (during active loading)
|
|
1616
1597
|
if (!this._loadingComplete && this._displayedProgress < this._targetProgress) {
|
|
1617
1598
|
this._displayedProgress = Math.min(this._displayedProgress + dt * 1.5, this._targetProgress);
|
|
1618
1599
|
this.updateLoaderBar(this._displayedProgress);
|
|
1619
1600
|
}
|
|
1620
1601
|
}
|
|
1621
1602
|
onResize(_width, _height) {
|
|
1622
|
-
//
|
|
1603
|
+
// The preloader overlay is CSS-based and auto-resizes.
|
|
1623
1604
|
}
|
|
1624
1605
|
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;
|
|
1606
|
+
// Defensive: ensure the preloader is gone even if we never transitioned
|
|
1607
|
+
// (e.g. the scene was popped externally). Idempotent.
|
|
1608
|
+
void removeCSSPreloader(this.hostElement());
|
|
1700
1609
|
}
|
|
1701
1610
|
// ─── Progress ──────────────────────────────────────────
|
|
1702
1611
|
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
|
-
}
|
|
1612
|
+
setCSSPreloaderProgress(Math.max(0, Math.min(1, progress)));
|
|
1710
1613
|
}
|
|
1711
1614
|
/**
|
|
1712
1615
|
* Smoothly animate the displayed progress from its current value to `target`
|
|
@@ -1736,49 +1639,20 @@ class LoadingScene extends Scene {
|
|
|
1736
1639
|
requestAnimationFrame(tick);
|
|
1737
1640
|
});
|
|
1738
1641
|
}
|
|
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
1642
|
// ─── Transition ────────────────────────────────────────
|
|
1643
|
+
/** The DOM element hosting the canvas + preloader overlay. */
|
|
1644
|
+
hostElement() {
|
|
1645
|
+
return this._engine?.app?.canvas?.parentElement ?? document.body;
|
|
1646
|
+
}
|
|
1766
1647
|
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();
|
|
1648
|
+
// Fade out and remove the shared CSS preloader (resolves after the fade).
|
|
1649
|
+
await removeCSSPreloader(this.hostElement());
|
|
1778
1650
|
// Navigate to the target scene, always passing the engine reference
|
|
1779
1651
|
await this._engine.scenes.goto(this._targetScene, {
|
|
1780
1652
|
engine: this._engine,
|
|
1781
|
-
...(this._targetData && typeof this._targetData === 'object'
|
|
1653
|
+
...(this._targetData && typeof this._targetData === 'object'
|
|
1654
|
+
? this._targetData
|
|
1655
|
+
: { data: this._targetData }),
|
|
1782
1656
|
});
|
|
1783
1657
|
}
|
|
1784
1658
|
}
|
|
@@ -2005,17 +1879,20 @@ class GameApplication extends EventEmitter {
|
|
|
2005
1879
|
// 6. Initialize sub-systems
|
|
2006
1880
|
this.initSubSystems();
|
|
2007
1881
|
this.emit('initialized');
|
|
2008
|
-
// 7.
|
|
2009
|
-
|
|
2010
|
-
//
|
|
1882
|
+
// 7. Load assets. The CSS preloader stays on screen — LoadingScene drives
|
|
1883
|
+
// its progress/tap and removes it before entering the game, so there's
|
|
1884
|
+
// a single continuous overlay from boot to gameplay (no logo flash).
|
|
2011
1885
|
await this.loadAssets(firstScene, sceneData);
|
|
2012
1886
|
this.emit('loaded');
|
|
2013
|
-
//
|
|
1887
|
+
// 8. Start the game loop
|
|
2014
1888
|
this._running = true;
|
|
2015
1889
|
this.emit('started');
|
|
2016
1890
|
}
|
|
2017
1891
|
catch (err) {
|
|
2018
1892
|
console.error('[GameEngine] Failed to start:', err);
|
|
1893
|
+
// Tear down the preloader so a failure doesn't strand the brand frame.
|
|
1894
|
+
if (this._container)
|
|
1895
|
+
removeCSSPreloader(this._container);
|
|
2019
1896
|
this.emit('error', err instanceof Error ? err : new Error(String(err)));
|
|
2020
1897
|
throw err;
|
|
2021
1898
|
}
|
|
@@ -2878,8 +2755,8 @@ async function createSlotGame(opts) {
|
|
|
2878
2755
|
}
|
|
2879
2756
|
|
|
2880
2757
|
// packages/game-engine/src/host/shellConfig.ts
|
|
2881
|
-
// `socialize`
|
|
2882
|
-
// types
|
|
2758
|
+
// `socialize` / `createI18n` are runtime helpers sourced from platform-core/shell;
|
|
2759
|
+
// pixi-shell re-exports only types so we import directly from platform-core.
|
|
2883
2760
|
/**
|
|
2884
2761
|
* Apply jurisdiction restrictions over the resolved shell features, in place. A restriction ALWAYS
|
|
2885
2762
|
* wins over the author's intent (a forbidden control must stay off even if the game enabled it).
|
|
@@ -2934,8 +2811,9 @@ function stakeForAction(model, action, bet) {
|
|
|
2934
2811
|
const cost = (model.spec.actions?.[action]?.cost ?? 1);
|
|
2935
2812
|
return cost * bet;
|
|
2936
2813
|
}
|
|
2937
|
-
/** Derive shell buy cards + ante toggles from the spec's buy/feature actions (SSOT).
|
|
2938
|
-
|
|
2814
|
+
/** Derive shell buy cards + ante toggles from the spec's buy/feature actions (SSOT).
|
|
2815
|
+
* @param t Optional translator applied to `title` and `description` before they enter the shell. */
|
|
2816
|
+
function toBonusOptions(model, t = (s) => s) {
|
|
2939
2817
|
const out = [];
|
|
2940
2818
|
for (const [key, action] of Object.entries(model.spec.actions)) {
|
|
2941
2819
|
const role = action.role ?? 'base';
|
|
@@ -2944,15 +2822,18 @@ function toBonusOptions(model) {
|
|
|
2944
2822
|
out.push({
|
|
2945
2823
|
id: key,
|
|
2946
2824
|
type: role === 'buy' ? 'bonus' : 'feature',
|
|
2947
|
-
title: action.title ?? key.replace(/_/g, ' ').toUpperCase(),
|
|
2948
|
-
description: action.description ?? '',
|
|
2825
|
+
title: t(action.title ?? key.replace(/_/g, ' ').toUpperCase()),
|
|
2826
|
+
description: t(action.description ?? ''),
|
|
2949
2827
|
priceMultiplier: action.cost ?? (role === 'buy' ? 100 : 1),
|
|
2828
|
+
// Volatility (1–5 bolts) is part of the spec action SSOT; forward it so the buy card shows it.
|
|
2829
|
+
...(action.volatility != null ? { volatility: action.volatility } : {}),
|
|
2950
2830
|
});
|
|
2951
2831
|
}
|
|
2952
2832
|
return out;
|
|
2953
2833
|
}
|
|
2954
|
-
/** Build a paytable section from the model's derived paytable view (multipliers per symbol count).
|
|
2955
|
-
|
|
2834
|
+
/** Build a paytable section from the model's derived paytable view (multipliers per symbol count).
|
|
2835
|
+
* @param t Optional translator applied to symbol names before they enter the shell. */
|
|
2836
|
+
function paytableSection(model, t = (s) => s) {
|
|
2956
2837
|
const symbols = model.paytable?.symbols ?? [];
|
|
2957
2838
|
const rows = [];
|
|
2958
2839
|
for (const s of symbols) {
|
|
@@ -2962,11 +2843,13 @@ function paytableSection(model) {
|
|
|
2962
2843
|
.sort((a, b) => Number(a.count) - Number(b.count));
|
|
2963
2844
|
if (!wins.length)
|
|
2964
2845
|
continue;
|
|
2965
|
-
rows.push({ symbol: { text: s.name ?? s.id }, wins });
|
|
2846
|
+
rows.push({ symbol: { text: t(s.name ?? s.id) }, wins });
|
|
2966
2847
|
}
|
|
2967
2848
|
if (!rows.length)
|
|
2968
2849
|
return null;
|
|
2969
|
-
|
|
2850
|
+
// No literal title — the shell renders `s.title ?? host.t('Paytable')`, so the heading is
|
|
2851
|
+
// localized (matching how the modes/wins sections rely on the shell's translated fallback).
|
|
2852
|
+
return { type: 'paytable', rows };
|
|
2970
2853
|
}
|
|
2971
2854
|
/** Build a "wins" illustration section sized to the grid; `kind` follows the spec mechanic hint. */
|
|
2972
2855
|
function winsSection(model) {
|
|
@@ -3008,14 +2891,15 @@ function orderDisclaimerLast(sections) {
|
|
|
3008
2891
|
* gets a real info panel for free (paytable, win illustration, controls, and the Stake
|
|
3009
2892
|
* disclaimer when present). Author-supplied `opts.gameInfo` is MERGED over this set by
|
|
3010
2893
|
* section identity (see `mergeGameInfo`), not wholesale-replaced.
|
|
2894
|
+
* @param t Optional translator applied to spec-derived player-facing strings (symbol names, mode titles, etc.).
|
|
3011
2895
|
*/
|
|
3012
|
-
function defaultGameInfo(model, runtime) {
|
|
2896
|
+
function defaultGameInfo(model, runtime, t = (s) => s) {
|
|
3013
2897
|
const sections = [];
|
|
3014
2898
|
sections.push(winsSection(model));
|
|
3015
|
-
const pay = paytableSection(model);
|
|
2899
|
+
const pay = paytableSection(model, t);
|
|
3016
2900
|
if (pay)
|
|
3017
2901
|
sections.push(pay);
|
|
3018
|
-
const modes = modesSection(model);
|
|
2902
|
+
const modes = modesSection(model, t);
|
|
3019
2903
|
if (modes)
|
|
3020
2904
|
sections.push(modes);
|
|
3021
2905
|
sections.push({ type: 'controls' });
|
|
@@ -3028,8 +2912,9 @@ function defaultGameInfo(model, runtime) {
|
|
|
3028
2912
|
* (`model.mathModes` + `spec.actions`) that drives the buy cards and the math pipeline. Stake
|
|
3029
2913
|
* compliance requires Cost / RTP / Max Win per mode; deriving it here means the author declares a
|
|
3030
2914
|
* mode once (in game.spec) and the info table can't drift. `free` actions are excluded (mathModes
|
|
3031
|
-
* already drops them — free spins are part of a bonus, not a purchasable mode).
|
|
3032
|
-
|
|
2915
|
+
* already drops them — free spins are part of a bonus, not a purchasable mode).
|
|
2916
|
+
* @param t Optional translator applied to row `title` and `description` before they enter the shell. */
|
|
2917
|
+
function modesSection(model, t = (s) => s) {
|
|
3033
2918
|
const modes = model.mathModes ?? [];
|
|
3034
2919
|
if (!modes.length)
|
|
3035
2920
|
return null;
|
|
@@ -3037,7 +2922,7 @@ function modesSection(model) {
|
|
|
3037
2922
|
const action = model.spec.actions[m.action];
|
|
3038
2923
|
const isBase = (action?.role ?? 'base') === 'base' || m.mode === 'BASE';
|
|
3039
2924
|
const row = {
|
|
3040
|
-
title: action?.title ?? (isBase ? 'Base game' : m.mode.replace(/_/g, ' ')),
|
|
2925
|
+
title: t(action?.title ?? (isBase ? 'Base game' : m.mode.replace(/_/g, ' '))),
|
|
3041
2926
|
maxWin: `${m.maxWin.toLocaleString('en-US')}×`,
|
|
3042
2927
|
};
|
|
3043
2928
|
// Cost is a bet-multiplier; a base spin (1×) reads as no premium, so only show it for buys/features.
|
|
@@ -3046,7 +2931,7 @@ function modesSection(model) {
|
|
|
3046
2931
|
if (typeof m.rtp === 'number')
|
|
3047
2932
|
row.rtp = Math.round(m.rtp * 1000) / 10; // 0.965 → 96.5 (%)
|
|
3048
2933
|
if (action?.description)
|
|
3049
|
-
row.description = action.description;
|
|
2934
|
+
row.description = t(action.description);
|
|
3050
2935
|
return row;
|
|
3051
2936
|
});
|
|
3052
2937
|
return { type: 'modes', title: 'MODES', modes: rows };
|
|
@@ -3143,18 +3028,17 @@ function buildShellConfig(opts, model, runtime) {
|
|
|
3143
3028
|
// host); opts.currency still wins. Fall back to the spec code, then a neutral euro.
|
|
3144
3029
|
const currency = opts.currency ?? runtime.currency ?? resolveCurrency(null, model.spec.currency);
|
|
3145
3030
|
const isSocial = runtime.social ?? false;
|
|
3146
|
-
//
|
|
3147
|
-
//
|
|
3148
|
-
//
|
|
3149
|
-
//
|
|
3150
|
-
//
|
|
3151
|
-
//
|
|
3152
|
-
|
|
3153
|
-
// social mode (identity otherwise) so authors can wrap copy explicitly; the full merged set is
|
|
3154
|
-
// still socialized below as a safety net.
|
|
3155
|
-
const t = isSocial ? socialize : (text) => text;
|
|
3031
|
+
// Build the merged resolver: game i18n map + shell LOCALES (via createI18n), then socialize on
|
|
3032
|
+
// top for English social mode. For non-English languages, createI18n already handles the
|
|
3033
|
+
// translation lookup; social rewriting is English-only and applied separately after.
|
|
3034
|
+
// Author gameInfo may be a plain object or a `(t) => content` factory. `t` is the resolver so
|
|
3035
|
+
// authors can wrap player-facing copy explicitly; the full merged set is still socialized below
|
|
3036
|
+
// (section pass) as a safety net so restricted words can't slip through even if t() was missed.
|
|
3037
|
+
const { t } = createI18n({ language: runtime.language ?? 'en', isSocial, messages: opts.i18n });
|
|
3156
3038
|
const authored = typeof opts.gameInfo === 'function' ? opts.gameInfo(t) : opts.gameInfo;
|
|
3157
|
-
|
|
3039
|
+
// Pass t() through to spec-derived sections so symbol names, mode titles, and descriptions are
|
|
3040
|
+
// pre-translated before they reach the shell renderer.
|
|
3041
|
+
let gameInfo = mergeGameInfo(defaultGameInfo(model, runtime, t), authored);
|
|
3158
3042
|
// The DISCLAIMER is required legal copy and must be shown VERBATIM — never socialized (its
|
|
3159
3043
|
// wording is mandated, and word-swaps like "bet → play" would corrupt the legal text).
|
|
3160
3044
|
if (isSocial) {
|
|
@@ -3164,8 +3048,8 @@ function buildShellConfig(opts, model, runtime) {
|
|
|
3164
3048
|
}
|
|
3165
3049
|
// The legal DISCLAIMER always renders LAST — author-merged or extra sections never push below it.
|
|
3166
3050
|
gameInfo = { sections: orderDisclaimerLast(gameInfo.sections ?? []) };
|
|
3167
|
-
// Buy-bonus cards:
|
|
3168
|
-
const buyBonus = socializeBonusOptions(opts.buyBonus ?? toBonusOptions(model), isSocial);
|
|
3051
|
+
// Buy-bonus cards: apply t() to spec-derived options (pre-translate), then socialize for en+social.
|
|
3052
|
+
const buyBonus = socializeBonusOptions(opts.buyBonus ?? toBonusOptions(model, t), isSocial);
|
|
3169
3053
|
// Features: defaults, then author overrides, THEN jurisdiction restrictions (a restriction wins).
|
|
3170
3054
|
const features = {
|
|
3171
3055
|
turbo: 0,
|