@energy8platform/shell 0.4.1 → 0.6.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/html.cjs.js +223 -69
- package/dist/html.cjs.js.map +1 -1
- package/dist/html.d.ts +56 -5
- package/dist/html.esm.js +223 -69
- package/dist/html.esm.js.map +1 -1
- package/dist/index.cjs.js +176 -42
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +56 -5
- package/dist/index.esm.js +176 -42
- package/dist/index.esm.js.map +1 -1
- package/dist/pixi.cjs.js +392 -88
- package/dist/pixi.cjs.js.map +1 -1
- package/dist/pixi.d.ts +56 -5
- package/dist/pixi.esm.js +392 -88
- package/dist/pixi.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/core/ShellController.ts +204 -43
- package/src/core/renderer.ts +8 -1
- package/src/core/state.ts +11 -0
- package/src/core/types.ts +51 -4
- package/src/core/version.ts +1 -1
- package/src/ui/html/components/BottomBar.ts +71 -32
- package/src/ui/html/components/Settings.ts +14 -5
- package/src/ui/pixi/PixiRenderer.ts +3 -0
- package/src/ui/pixi/components/BottomBar.ts +280 -64
- package/src/ui/pixi/components/Settings.ts +23 -8
- package/src/ui/pixi/primitives/controls.ts +7 -0
package/dist/html.d.ts
CHANGED
|
@@ -16,7 +16,14 @@ declare class EventEmitter<TEvents extends {}> {
|
|
|
16
16
|
removeAllListeners(event?: keyof TEvents): this;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
/** `freeSpins` and `bonus` are the SAME bar layout (host-driven hero + Total Win); `freeSpins` is
|
|
20
|
+
* kept as a back-compat alias for the common case (its readout is derived current/total), while
|
|
21
|
+
* `bonus` pairs with `setBonus()` to show a game-supplied label + value (adventure, hold-and-spin,
|
|
22
|
+
* respins — anything that isn't a plain free-spins counter). */
|
|
23
|
+
type ShellMode = 'base' | 'bonus' | 'freeSpins' | 'replay';
|
|
24
|
+
/** The three independent volume sliders shown in the Settings overlay. */
|
|
25
|
+
type VolumeKey = 'master' | 'music' | 'sfx';
|
|
26
|
+
type VolumeLevels = Record<VolumeKey, number>;
|
|
20
27
|
interface CurrencyConfig {
|
|
21
28
|
symbol: string;
|
|
22
29
|
position: 'left' | 'right';
|
|
@@ -205,6 +212,19 @@ interface FreeSpinsState {
|
|
|
205
212
|
total: number;
|
|
206
213
|
totalWin: number;
|
|
207
214
|
}
|
|
215
|
+
/** A game-supplied bonus readout for the bar hero, set via `setBonus()`. Generalizes the
|
|
216
|
+
* free-spins counter: the shell renders `label` (localized via the shell translator, so a game
|
|
217
|
+
* i18n entry for it is honoured) + `value` (a pre-formatted string, shown verbatim — "2 / 10",
|
|
218
|
+
* "3", "×5", "7 coins") + the Total Win accumulator. The shell stays free of any per-game bonus
|
|
219
|
+
* concept — the label/value semantics live in the game/host. */
|
|
220
|
+
interface BonusReadout {
|
|
221
|
+
/** Bar label, e.g. 'Free spins' | 'Adventure' | 'Hold & Spin'. Run through the shell translator. */
|
|
222
|
+
label: string;
|
|
223
|
+
/** Pre-formatted counter value shown verbatim (NOT translated). */
|
|
224
|
+
value: string;
|
|
225
|
+
/** Cumulative bonus win for the Total Win readout. */
|
|
226
|
+
totalWin: number;
|
|
227
|
+
}
|
|
208
228
|
/** One footer button of a generic modal. Clicking it runs `on` (if any), then closes the modal. */
|
|
209
229
|
interface ModalAction {
|
|
210
230
|
title: string;
|
|
@@ -265,9 +285,13 @@ interface ShellConfig {
|
|
|
265
285
|
* opening the built-in buy-bonus overlay (e.g. the game shows its own bonus UI). The button
|
|
266
286
|
* is shown whenever this OR `features.buyBonus` is set. */
|
|
267
287
|
onBonusBuy?: () => void;
|
|
288
|
+
/** Initial Settings-overlay volume slider positions (each 0..1, defaults to 1 = 100%). The shell
|
|
289
|
+
* keeps them stateful across opens; read/update at runtime via `shell.getVolume()` /
|
|
290
|
+
* `shell.setVolume()`, and listen to `settingChange` ({ key: 'master'|'music'|'sfx' }) to apply. */
|
|
291
|
+
volumes?: Partial<VolumeLevels>;
|
|
268
292
|
}
|
|
269
293
|
/** ShellConfig after the controller applies defaults (version, isSocial, replay, theme). No mount. */
|
|
270
|
-
type ResolvedShellConfig = Required<Pick<ShellConfig, 'language' | 'currency' | 'availableBets' | 'defaultBet' | 'balance' | 'win' | 'mode' | 'features' | 'gameInfo' | 'version' | 'isSocial' | 'replay'>> & Pick<ShellConfig, 'currentBet' | 'theme' | 'onBonusBuy'>;
|
|
294
|
+
type ResolvedShellConfig = Required<Pick<ShellConfig, 'language' | 'currency' | 'availableBets' | 'defaultBet' | 'balance' | 'win' | 'mode' | 'features' | 'gameInfo' | 'version' | 'isSocial' | 'replay'>> & Pick<ShellConfig, 'currentBet' | 'theme' | 'onBonusBuy' | 'volumes'>;
|
|
271
295
|
interface ShellState {
|
|
272
296
|
mode: ShellMode;
|
|
273
297
|
/** Sticky replay marker — true for a historical-round replay, regardless of the current
|
|
@@ -283,9 +307,18 @@ interface ShellState {
|
|
|
283
307
|
turbo: number;
|
|
284
308
|
buyBonusEnabled: boolean;
|
|
285
309
|
freeSpins: FreeSpinsState;
|
|
310
|
+
/** Game-supplied bonus label + value override (from `setBonus()`). When null the bar derives the
|
|
311
|
+
* readout from `freeSpins` (label 'Free spins', value current/total) — the back-compat default. */
|
|
312
|
+
bonus: {
|
|
313
|
+
label: string;
|
|
314
|
+
value: string;
|
|
315
|
+
} | null;
|
|
286
316
|
/** The currently activated `feature` option (e.g. Ante), or null. Drives the
|
|
287
317
|
* effective-bet readout tint and the BUY BONUS → DISABLE toggle on the bar. */
|
|
288
318
|
activeFeature: BonusOption | null;
|
|
319
|
+
/** Volume slider positions (0..1) surfaced in the Settings overlay. Stateful across opens so a
|
|
320
|
+
* reopened overlay reflects the last-set positions instead of resetting to 100%. */
|
|
321
|
+
volumes: VolumeLevels;
|
|
289
322
|
}
|
|
290
323
|
interface ShellEvents {
|
|
291
324
|
spin: void;
|
|
@@ -408,6 +441,13 @@ interface ShellHost {
|
|
|
408
441
|
setSound(on: boolean): void;
|
|
409
442
|
/** An open Settings overlay registers an icon updater here (null clears it on close). */
|
|
410
443
|
setSoundRefresh(fn: ((on: boolean) => void) | null): void;
|
|
444
|
+
/** Current volume slider position (0..1) for master/music/sfx. */
|
|
445
|
+
getVolume(key: VolumeKey): number;
|
|
446
|
+
/** Set a volume slider (0..1): clamps, stores, emits `settingChange`, and live-updates an open
|
|
447
|
+
* Settings overlay. Called by the slider control on drag AND by game code as the public API. */
|
|
448
|
+
setVolume(key: VolumeKey, value: number): void;
|
|
449
|
+
/** An open Settings overlay registers a slider updater here (null clears it on close). */
|
|
450
|
+
setVolumeRefresh(fn: ((key: VolumeKey, value: number) => void) | null): void;
|
|
411
451
|
/** Logic-bearing actions invoked by renderer controls. */
|
|
412
452
|
readonly actions: ShellActions;
|
|
413
453
|
}
|
|
@@ -471,13 +511,14 @@ declare class ShellController extends EventEmitter<ShellEvents> implements Shell
|
|
|
471
511
|
tokens: ShellTokens;
|
|
472
512
|
layout: ShellLayoutMode;
|
|
473
513
|
soundOn: boolean;
|
|
474
|
-
readonly engineVersion = "0.
|
|
514
|
+
readonly engineVersion = "0.6.0";
|
|
475
515
|
readonly actions: ShellActions;
|
|
476
516
|
private renderer;
|
|
477
517
|
private i18n;
|
|
478
518
|
private kbd?;
|
|
479
519
|
private overlay;
|
|
480
520
|
private soundRefresh;
|
|
521
|
+
private volumeRefresh;
|
|
481
522
|
private prevBalance;
|
|
482
523
|
private prevWin;
|
|
483
524
|
private destroyed;
|
|
@@ -502,6 +543,12 @@ declare class ShellController extends EventEmitter<ShellEvents> implements Shell
|
|
|
502
543
|
closeModal(): void;
|
|
503
544
|
setSound(on: boolean): void;
|
|
504
545
|
setSoundRefresh(fn: ((on: boolean) => void) | null): void;
|
|
546
|
+
getVolume(key: VolumeKey): number;
|
|
547
|
+
/** Set a volume slider (0..1). Shared by the slider control (drag) and game code (public API):
|
|
548
|
+
* clamps, stores so a reopened Settings overlay reflects it, emits `settingChange`, and
|
|
549
|
+
* live-updates the slider if the overlay is currently open. */
|
|
550
|
+
setVolume(key: VolumeKey, value: number): void;
|
|
551
|
+
setVolumeRefresh(fn: ((key: VolumeKey, value: number) => void) | null): void;
|
|
505
552
|
activateFeature(bonus: BonusOption): void;
|
|
506
553
|
deactivateFeature(): void;
|
|
507
554
|
private money;
|
|
@@ -514,6 +561,10 @@ declare class ShellController extends EventEmitter<ShellEvents> implements Shell
|
|
|
514
561
|
setTurbo(level: number): void;
|
|
515
562
|
setBuyBonusEnabled(enabled: boolean): void;
|
|
516
563
|
setFreeSpins(fs: FreeSpinsState): void;
|
|
564
|
+
/** Generic bonus readout (adventure / hold-and-spin / respins). Sets a game-supplied label+value
|
|
565
|
+
* override for the bar hero and folds `totalWin` into the shared accumulator. Pairs with
|
|
566
|
+
* `setMode('bonus')`. `setFreeSpins()` clears the override back to the derived current/total. */
|
|
567
|
+
setBonus(b: BonusReadout): void;
|
|
517
568
|
setTheme(theme: ThemeConfig): void;
|
|
518
569
|
setLanguage(lang: string): void;
|
|
519
570
|
setSocial(isSocial: boolean): void;
|
|
@@ -537,7 +588,7 @@ interface I18n {
|
|
|
537
588
|
declare function createI18n(opts: I18nOptions): I18n;
|
|
538
589
|
|
|
539
590
|
/** The @energy8platform/shell package version, stamped into the game-info footer. */
|
|
540
|
-
declare const PACKAGE_VERSION = "0.
|
|
591
|
+
declare const PACKAGE_VERSION = "0.6.0";
|
|
541
592
|
|
|
542
593
|
/** A shell: the renderer-agnostic controller plus the surface facade (safeArea/barHeight/setVisible)
|
|
543
594
|
* an embedding host reads. `createShell`, `createGameShell` and `createPixiShell` all return this. */
|
|
@@ -613,4 +664,4 @@ declare function createGameShell(config: HtmlShellConfig): ShellController;
|
|
|
613
664
|
declare function removeGameShell(): Promise<void>;
|
|
614
665
|
|
|
615
666
|
export { DEFAULT_ACCENT, ShellController as GameShell, HtmlRenderer, PACKAGE_VERSION, SCHEMES, ShellController, createGameShell, createI18n, createShell, normalizeLang, removeGameShell, resolveConfig, resolveTheme, socialize };
|
|
616
|
-
export type { AutoplayConfig, AutoplayOptions, BonusCardContext, BonusOption, CellRef, CreateShellOptions, CurrencyConfig, FreeSpinsState, GameInfoContent, GameInfoSection, GameMode, HtmlShellConfig, I18n, I18nOptions, Lang, ModalAction, ModalOptions, OverlayHandle, OverlayRequest, PaylineDef, PaytableRow, ReplayModalOptions, ResolvedShellConfig, SafeArea, ShapeDef, Shell, ShellActions, HtmlShellConfig as ShellConfig, ShellEvents, ShellFeatures, ShellHost, ShellLayoutMode, ShellMode, ShellRenderer, ShellState, ShellSurface, ShellTokens, ThemeConfig, WinSection };
|
|
667
|
+
export type { AutoplayConfig, AutoplayOptions, BonusCardContext, BonusOption, BonusReadout, CellRef, CreateShellOptions, CurrencyConfig, FreeSpinsState, GameInfoContent, GameInfoSection, GameMode, HtmlShellConfig, I18n, I18nOptions, Lang, ModalAction, ModalOptions, OverlayHandle, OverlayRequest, PaylineDef, PaytableRow, ReplayModalOptions, ResolvedShellConfig, SafeArea, ShapeDef, Shell, ShellActions, HtmlShellConfig as ShellConfig, ShellEvents, ShellFeatures, ShellHost, ShellLayoutMode, ShellMode, ShellRenderer, ShellState, ShellSurface, ShellTokens, ThemeConfig, VolumeKey, VolumeLevels, WinSection };
|
package/dist/html.esm.js
CHANGED
|
@@ -63,9 +63,19 @@ function createInitialState(config) {
|
|
|
63
63
|
turbo: 0,
|
|
64
64
|
buyBonusEnabled: true,
|
|
65
65
|
freeSpins: { current: 0, total: 0, totalWin: 0 },
|
|
66
|
+
bonus: null,
|
|
66
67
|
activeFeature: null,
|
|
68
|
+
volumes: {
|
|
69
|
+
master: clampVolume(config.volumes?.master),
|
|
70
|
+
music: clampVolume(config.volumes?.music),
|
|
71
|
+
sfx: clampVolume(config.volumes?.sfx),
|
|
72
|
+
},
|
|
67
73
|
};
|
|
68
74
|
}
|
|
75
|
+
/** Clamp a configured volume to 0..1, defaulting to full (1) when unset/invalid. */
|
|
76
|
+
function clampVolume(v) {
|
|
77
|
+
return typeof v === 'number' && Number.isFinite(v) ? Math.max(0, Math.min(1, v)) : 1;
|
|
78
|
+
}
|
|
69
79
|
/** Step bet up/down within availableBets, clamped at the ends. */
|
|
70
80
|
function stepBet(state, direction) {
|
|
71
81
|
const idx = state.availableBets.indexOf(state.bet);
|
|
@@ -1436,7 +1446,7 @@ class KeyboardController {
|
|
|
1436
1446
|
|
|
1437
1447
|
// AUTO-GENERATED by scripts/gen-version.mjs — do not edit. Mirrors package.json "version".
|
|
1438
1448
|
/** The @energy8platform/shell package version, stamped into the game-info footer. */
|
|
1439
|
-
const PACKAGE_VERSION = '0.
|
|
1449
|
+
const PACKAGE_VERSION = '0.6.0';
|
|
1440
1450
|
|
|
1441
1451
|
/** Apply defaults to the raw config (the mount target lives on the renderer, not here). */
|
|
1442
1452
|
function resolveConfig(config) {
|
|
@@ -1453,6 +1463,7 @@ function resolveConfig(config) {
|
|
|
1453
1463
|
features: config.features,
|
|
1454
1464
|
theme: config.theme,
|
|
1455
1465
|
onBonusBuy: config.onBonusBuy,
|
|
1466
|
+
volumes: config.volumes,
|
|
1456
1467
|
version: config.version ?? '1.0.0',
|
|
1457
1468
|
isSocial: config.isSocial ?? false,
|
|
1458
1469
|
replay: config.replay ?? config.mode === 'replay',
|
|
@@ -1474,6 +1485,7 @@ class ShellController extends EventEmitter {
|
|
|
1474
1485
|
kbd;
|
|
1475
1486
|
overlay = null;
|
|
1476
1487
|
soundRefresh = null;
|
|
1488
|
+
volumeRefresh = null;
|
|
1477
1489
|
prevBalance;
|
|
1478
1490
|
prevWin;
|
|
1479
1491
|
destroyed = false;
|
|
@@ -1497,9 +1509,15 @@ class ShellController extends EventEmitter {
|
|
|
1497
1509
|
this.renderer.renderBar();
|
|
1498
1510
|
}
|
|
1499
1511
|
// ── ShellHost ──────────────────────────────────────────────────────────────
|
|
1500
|
-
t(text) {
|
|
1501
|
-
|
|
1502
|
-
|
|
1512
|
+
t(text) {
|
|
1513
|
+
return this.i18n.t(text);
|
|
1514
|
+
}
|
|
1515
|
+
formatCurrency(n, win = false) {
|
|
1516
|
+
return formatCurrency(n, this.config.currency, win);
|
|
1517
|
+
}
|
|
1518
|
+
formatWin(value) {
|
|
1519
|
+
return this.formatCurrency(value, true);
|
|
1520
|
+
}
|
|
1503
1521
|
notifyResize(w, h) {
|
|
1504
1522
|
const layout = w !== 0 && h > w ? 'mobile' : 'wide';
|
|
1505
1523
|
if (layout !== this.layout) {
|
|
@@ -1566,12 +1584,24 @@ class ShellController extends EventEmitter {
|
|
|
1566
1584
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
1567
1585
|
const self = this;
|
|
1568
1586
|
const host = {
|
|
1569
|
-
get state() {
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
get
|
|
1573
|
-
|
|
1574
|
-
|
|
1587
|
+
get state() {
|
|
1588
|
+
return self.state;
|
|
1589
|
+
},
|
|
1590
|
+
get hotkeysEnabled() {
|
|
1591
|
+
return self.config.features.hotkeys !== false;
|
|
1592
|
+
},
|
|
1593
|
+
get spacebarEnabled() {
|
|
1594
|
+
return self.config.features.spacebar !== false;
|
|
1595
|
+
},
|
|
1596
|
+
get turboLevels() {
|
|
1597
|
+
return self.config.features.turbo;
|
|
1598
|
+
},
|
|
1599
|
+
get autoplayEnabled() {
|
|
1600
|
+
return self.config.features.autoplay != null;
|
|
1601
|
+
},
|
|
1602
|
+
get buyBonusEnabled() {
|
|
1603
|
+
return self.config.features.buyBonus !== false;
|
|
1604
|
+
},
|
|
1575
1605
|
hasOpenLayer: () => self.overlay !== null,
|
|
1576
1606
|
routeToLayer: (e) => self.overlay?.onKey?.(e) ?? false,
|
|
1577
1607
|
spin: () => self.actions.spin(),
|
|
@@ -1587,33 +1617,59 @@ class ShellController extends EventEmitter {
|
|
|
1587
1617
|
this.kbd = new KeyboardController(host);
|
|
1588
1618
|
this.kbd.attach();
|
|
1589
1619
|
}
|
|
1590
|
-
pullFocus = () => {
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1620
|
+
pullFocus = () => {
|
|
1621
|
+
try {
|
|
1622
|
+
globalThis.focus?.();
|
|
1623
|
+
}
|
|
1624
|
+
catch {
|
|
1625
|
+
/* cross-origin */
|
|
1626
|
+
}
|
|
1627
|
+
};
|
|
1594
1628
|
// ── overlay flow ─────────────────────────────────────────────────────────────
|
|
1595
1629
|
show(req) {
|
|
1596
1630
|
this.closeModal();
|
|
1597
1631
|
this.overlay = this.renderer.openOverlay(req) ?? null;
|
|
1598
1632
|
}
|
|
1599
|
-
openMenu() {
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1633
|
+
openMenu() {
|
|
1634
|
+
this.emit('menuOpen');
|
|
1635
|
+
this.openSettings();
|
|
1636
|
+
}
|
|
1637
|
+
openSettings() {
|
|
1638
|
+
this.emit('settingsOpen');
|
|
1639
|
+
this.show({ kind: 'settings' });
|
|
1640
|
+
}
|
|
1641
|
+
openInfo() {
|
|
1642
|
+
this.emit('infoOpen');
|
|
1643
|
+
this.show({ kind: 'gameInfo' });
|
|
1644
|
+
}
|
|
1645
|
+
openBuyBonus() {
|
|
1646
|
+
if (this.config.onBonusBuy) {
|
|
1647
|
+
this.config.onBonusBuy();
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1650
|
+
this.show({ kind: 'buyBonus' });
|
|
1651
|
+
}
|
|
1652
|
+
openBetPicker() {
|
|
1653
|
+
this.show({ kind: 'betPicker' });
|
|
1654
|
+
}
|
|
1655
|
+
openAutoplayPicker() {
|
|
1656
|
+
this.show({ kind: 'autoplayPicker' });
|
|
1657
|
+
}
|
|
1658
|
+
openReplay(opts) {
|
|
1659
|
+
if (this.destroyed)
|
|
1660
|
+
return;
|
|
1661
|
+
this.show({ kind: 'replay', opts });
|
|
1662
|
+
}
|
|
1663
|
+
openModal(opts) {
|
|
1664
|
+
this.show({ kind: 'modal', opts });
|
|
1665
|
+
}
|
|
1611
1666
|
/** Programmatically dismiss whatever overlay/modal is open. No-op when nothing is shown. */
|
|
1612
1667
|
closeModal() {
|
|
1613
1668
|
if (!this.overlay)
|
|
1614
1669
|
return;
|
|
1615
1670
|
this.overlay = null;
|
|
1616
1671
|
this.soundRefresh = null;
|
|
1672
|
+
this.volumeRefresh = null;
|
|
1617
1673
|
this.renderer.closeOverlay();
|
|
1618
1674
|
}
|
|
1619
1675
|
// ── sound ──────────────────────────────────────────────────────────────────
|
|
@@ -1623,7 +1679,25 @@ class ShellController extends EventEmitter {
|
|
|
1623
1679
|
this.soundRefresh?.(on);
|
|
1624
1680
|
this.renderer.refreshSoundIcon?.(on);
|
|
1625
1681
|
}
|
|
1626
|
-
setSoundRefresh(fn) {
|
|
1682
|
+
setSoundRefresh(fn) {
|
|
1683
|
+
this.soundRefresh = fn;
|
|
1684
|
+
}
|
|
1685
|
+
// ── volume ─────────────────────────────────────────────────────────────────
|
|
1686
|
+
getVolume(key) {
|
|
1687
|
+
return this.state.volumes[key];
|
|
1688
|
+
}
|
|
1689
|
+
/** Set a volume slider (0..1). Shared by the slider control (drag) and game code (public API):
|
|
1690
|
+
* clamps, stores so a reopened Settings overlay reflects it, emits `settingChange`, and
|
|
1691
|
+
* live-updates the slider if the overlay is currently open. */
|
|
1692
|
+
setVolume(key, value) {
|
|
1693
|
+
const v = Math.max(0, Math.min(1, value));
|
|
1694
|
+
this.state.volumes[key] = v;
|
|
1695
|
+
this.emit('settingChange', { key, value: v });
|
|
1696
|
+
this.volumeRefresh?.(key, v);
|
|
1697
|
+
}
|
|
1698
|
+
setVolumeRefresh(fn) {
|
|
1699
|
+
this.volumeRefresh = fn;
|
|
1700
|
+
}
|
|
1627
1701
|
// ── features ─────────────────────────────────────────────────────────────────
|
|
1628
1702
|
activateFeature(bonus) {
|
|
1629
1703
|
this.state.activeFeature = bonus;
|
|
@@ -1644,21 +1718,81 @@ class ShellController extends EventEmitter {
|
|
|
1644
1718
|
if (to !== from)
|
|
1645
1719
|
this.renderer.animateMoney(field, from, to);
|
|
1646
1720
|
}
|
|
1647
|
-
setBalance(n) {
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
this.
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1721
|
+
setBalance(n) {
|
|
1722
|
+
const from = this.prevBalance;
|
|
1723
|
+
this.state.balance = n;
|
|
1724
|
+
this.prevBalance = n;
|
|
1725
|
+
this.money('balance', from, n);
|
|
1726
|
+
}
|
|
1727
|
+
setWin(n) {
|
|
1728
|
+
const from = this.prevWin;
|
|
1729
|
+
this.state.win = n;
|
|
1730
|
+
this.prevWin = n;
|
|
1731
|
+
this.money('win', from, n);
|
|
1732
|
+
}
|
|
1733
|
+
setBet(n) {
|
|
1734
|
+
this.state.bet = n;
|
|
1735
|
+
this.renderer.renderBar();
|
|
1736
|
+
}
|
|
1737
|
+
setMode(mode) {
|
|
1738
|
+
if (mode === 'replay')
|
|
1739
|
+
this.state.replay = true;
|
|
1740
|
+
this.state.mode = mode;
|
|
1741
|
+
this.renderer.renderBar();
|
|
1742
|
+
}
|
|
1743
|
+
setBusy(busy) {
|
|
1744
|
+
this.state.busy = busy;
|
|
1745
|
+
this.renderer.renderBar();
|
|
1746
|
+
this.kbd?.notifyBusyChanged(busy);
|
|
1747
|
+
}
|
|
1748
|
+
setAutoplay(a) {
|
|
1749
|
+
this.state.autoplay = a;
|
|
1750
|
+
this.renderer.renderBar();
|
|
1751
|
+
}
|
|
1752
|
+
setTurbo(level) {
|
|
1753
|
+
this.state.turbo = level;
|
|
1754
|
+
this.renderer.renderBar();
|
|
1755
|
+
}
|
|
1756
|
+
setBuyBonusEnabled(enabled) {
|
|
1757
|
+
this.state.buyBonusEnabled = enabled;
|
|
1758
|
+
this.renderer.renderBar();
|
|
1759
|
+
}
|
|
1760
|
+
setFreeSpins(fs) {
|
|
1761
|
+
this.state.freeSpins = fs;
|
|
1762
|
+
this.state.bonus = null;
|
|
1763
|
+
this.renderer.renderBar();
|
|
1764
|
+
}
|
|
1765
|
+
/** Generic bonus readout (adventure / hold-and-spin / respins). Sets a game-supplied label+value
|
|
1766
|
+
* override for the bar hero and folds `totalWin` into the shared accumulator. Pairs with
|
|
1767
|
+
* `setMode('bonus')`. `setFreeSpins()` clears the override back to the derived current/total. */
|
|
1768
|
+
setBonus(b) {
|
|
1769
|
+
this.state.bonus = { label: b.label, value: b.value };
|
|
1770
|
+
this.state.freeSpins = { ...this.state.freeSpins, totalWin: b.totalWin };
|
|
1771
|
+
this.renderer.renderBar();
|
|
1772
|
+
}
|
|
1773
|
+
setTheme(theme) {
|
|
1774
|
+
this.config.theme = theme;
|
|
1775
|
+
this.tokens = resolveTheme(theme);
|
|
1776
|
+
this.renderer.applyTheme(this.tokens);
|
|
1777
|
+
this.renderer.renderBar();
|
|
1778
|
+
}
|
|
1779
|
+
setLanguage(lang) {
|
|
1780
|
+
this.config.language = lang;
|
|
1781
|
+
this.i18n = createI18n({ language: lang, isSocial: this.config.isSocial });
|
|
1782
|
+
this.renderer.renderBar();
|
|
1783
|
+
}
|
|
1784
|
+
setSocial(isSocial) {
|
|
1785
|
+
this.config.isSocial = isSocial;
|
|
1786
|
+
this.i18n = createI18n({ language: this.config.language, isSocial });
|
|
1787
|
+
this.renderer.renderBar();
|
|
1788
|
+
}
|
|
1789
|
+
setLayout(layout) {
|
|
1790
|
+
if (layout === this.layout)
|
|
1791
|
+
return;
|
|
1792
|
+
this.layout = layout;
|
|
1793
|
+
this.renderer.setLayout(layout);
|
|
1794
|
+
this.renderer.renderBar();
|
|
1795
|
+
}
|
|
1662
1796
|
destroy() {
|
|
1663
1797
|
if (this.destroyed)
|
|
1664
1798
|
return Promise.resolve();
|
|
@@ -2380,8 +2514,10 @@ function iconBtn(ge, name, onClick, active = false) {
|
|
|
2380
2514
|
b.className = `ge-iconbtn${active ? ' ge-active' : ''}`;
|
|
2381
2515
|
b.dataset.ge = ge;
|
|
2382
2516
|
b.innerHTML = icon(name);
|
|
2383
|
-
b.addEventListener('click', () => {
|
|
2384
|
-
|
|
2517
|
+
b.addEventListener('click', () => {
|
|
2518
|
+
if (!b.disabled)
|
|
2519
|
+
onClick();
|
|
2520
|
+
});
|
|
2385
2521
|
return b;
|
|
2386
2522
|
}
|
|
2387
2523
|
function renderBottomBar(host) {
|
|
@@ -2397,15 +2533,13 @@ function renderBottomBar(host) {
|
|
|
2397
2533
|
// All three modes share the base plaque layout. FS/replay hide the controls that don't apply
|
|
2398
2534
|
// and add Free Spins + Total Win blocks on the left; the per-spin WIN uses the base pill.
|
|
2399
2535
|
const isBase = state.mode === 'base';
|
|
2400
|
-
const isFS = state.mode === 'freeSpins';
|
|
2536
|
+
const isFS = state.mode === 'freeSpins' || state.mode === 'bonus';
|
|
2401
2537
|
// FS always shows the spins counter + accumulated Total Win (even €0); a replay shows them
|
|
2402
2538
|
// only when it's a free-spins replay (freeSpins.total > 0).
|
|
2403
2539
|
const showFsBlocks = isFS || (state.mode === 'replay' && state.freeSpins.total > 0);
|
|
2404
2540
|
// Replay is a read-only historical round — there's no real balance to show, so hide it. Keyed on
|
|
2405
2541
|
// the sticky `replay` flag (not `mode`) so it stays hidden through a replay's free-spins phase.
|
|
2406
|
-
const balance = state.replay
|
|
2407
|
-
? null
|
|
2408
|
-
: readout('balance', host.t('Balance'), fmt(state.balance));
|
|
2542
|
+
const balance = state.replay ? null : readout('balance', host.t('Balance'), fmt(state.balance));
|
|
2409
2543
|
// With a feature active (e.g. Ante) the BET readout shows the effective stake, tinted with
|
|
2410
2544
|
// the feature accent; the base state.bet is unchanged and returns once the feature is off.
|
|
2411
2545
|
const feature = state.activeFeature;
|
|
@@ -2428,21 +2562,25 @@ function renderBottomBar(host) {
|
|
|
2428
2562
|
betDown = iconBtn('bet-down', 'minus', () => host.actions.stepBet(-1));
|
|
2429
2563
|
betUp = iconBtn('bet-up', 'plus', () => host.actions.stepBet(1));
|
|
2430
2564
|
betValue.classList.add('ge-betbtn'); // tap the stake → bet picker
|
|
2431
|
-
betValue.addEventListener('click', () => {
|
|
2432
|
-
host
|
|
2565
|
+
betValue.addEventListener('click', () => {
|
|
2566
|
+
if (!betLocked(host))
|
|
2567
|
+
host.actions.openBetPicker();
|
|
2568
|
+
});
|
|
2433
2569
|
spin = spinButton(host);
|
|
2434
2570
|
auto = config.features.autoplay ? autoButton(host) : null;
|
|
2435
|
-
buy =
|
|
2571
|
+
buy = config.features.buyBonus !== false || config.onBonusBuy ? buyBtn(host) : null;
|
|
2436
2572
|
}
|
|
2437
2573
|
const winEl = state.win > 0 ? readout('win', host.t('Win'), fmtWin(state.win)) : null;
|
|
2438
2574
|
// FS/replay left blocks: spins counter + accumulated Total Win (shown even at €0).
|
|
2439
2575
|
// current = number → "current / total"; current = null/undefined → just the (game-driven) total.
|
|
2440
2576
|
const fs = state.freeSpins;
|
|
2441
|
-
const fsText = fs.current == null ? `${fs.total}` : `${fs.current} / ${fs.total}
|
|
2577
|
+
const fsText = state.bonus?.value ?? (fs.current == null ? `${fs.total}` : `${fs.current} / ${fs.total}`);
|
|
2442
2578
|
// In FS the spins counter takes the SPIN slot as a rectangular hero plaque (same white/black-ring
|
|
2443
2579
|
// style as the SPIN disc); Total Win stays an inline readout.
|
|
2444
|
-
const fsHero = showFsBlocks ? fsHeroPlaque(host, fsText) : null;
|
|
2445
|
-
const fsTotalWin = showFsBlocks
|
|
2580
|
+
const fsHero = showFsBlocks ? fsHeroPlaque(host, fsText, state.bonus?.label) : null;
|
|
2581
|
+
const fsTotalWin = showFsBlocks
|
|
2582
|
+
? readout('fs-totalwin', host.t('Total win'), fmtWin(fs.totalWin))
|
|
2583
|
+
: null;
|
|
2446
2584
|
// The hero in the centre/spin position: SPIN in base, the FS counter in free spins, nothing else.
|
|
2447
2585
|
const hero = isBase ? spin : fsHero;
|
|
2448
2586
|
if (mobile) {
|
|
@@ -2485,13 +2623,13 @@ function renderBottomBar(host) {
|
|
|
2485
2623
|
}
|
|
2486
2624
|
/** Free-spins hero plaque — takes the SPIN slot in FS: same white disc/black-ring language as SPIN,
|
|
2487
2625
|
* but a rounded RECTANGLE showing the spins counter ("3 / 10"). */
|
|
2488
|
-
function fsHeroPlaque(host, text) {
|
|
2626
|
+
function fsHeroPlaque(host, text, label) {
|
|
2489
2627
|
const el = document.createElement('div');
|
|
2490
2628
|
el.className = 'ge-fs-hero';
|
|
2491
2629
|
el.dataset.ge = 'fs-counter';
|
|
2492
2630
|
const lbl = document.createElement('span');
|
|
2493
2631
|
lbl.className = 'ge-fs-lbl';
|
|
2494
|
-
lbl.textContent = host.t('Free spins');
|
|
2632
|
+
lbl.textContent = host.t(label ?? 'Free spins');
|
|
2495
2633
|
const num = document.createElement('span');
|
|
2496
2634
|
num.className = 'ge-fs-num';
|
|
2497
2635
|
num.textContent = text;
|
|
@@ -2511,7 +2649,9 @@ function plaque(cls, children) {
|
|
|
2511
2649
|
d.append(...children);
|
|
2512
2650
|
return d;
|
|
2513
2651
|
}
|
|
2514
|
-
function compact(items) {
|
|
2652
|
+
function compact(items) {
|
|
2653
|
+
return items.filter((x) => x !== null);
|
|
2654
|
+
}
|
|
2515
2655
|
function buyBtn(host) {
|
|
2516
2656
|
const buy = document.createElement('button');
|
|
2517
2657
|
buy.className = 'ge-shell-buybonus';
|
|
@@ -2524,15 +2664,19 @@ function buyBtn(host) {
|
|
|
2524
2664
|
buy.innerHTML = `<span>${host.t('DISABLE')}</span>`;
|
|
2525
2665
|
buy.style.background = accent;
|
|
2526
2666
|
buy.style.color = contrastText(accent);
|
|
2527
|
-
buy.addEventListener('click', () => {
|
|
2528
|
-
|
|
2667
|
+
buy.addEventListener('click', () => {
|
|
2668
|
+
if (!buy.disabled)
|
|
2669
|
+
host.actions.deactivateFeature();
|
|
2670
|
+
});
|
|
2529
2671
|
}
|
|
2530
2672
|
else {
|
|
2531
2673
|
// Ticket icon (no text); keep the label for screen readers.
|
|
2532
2674
|
buy.setAttribute('aria-label', host.t('BUY BONUS'));
|
|
2533
2675
|
buy.innerHTML = `<span class="ge-bb-tk">${icon('ticket')}</span>`;
|
|
2534
|
-
buy.addEventListener('click', () => {
|
|
2535
|
-
|
|
2676
|
+
buy.addEventListener('click', () => {
|
|
2677
|
+
if (!buy.disabled)
|
|
2678
|
+
host.actions.openBuyBonus();
|
|
2679
|
+
});
|
|
2536
2680
|
}
|
|
2537
2681
|
return buy;
|
|
2538
2682
|
}
|
|
@@ -2550,15 +2694,19 @@ function spinButton(host) {
|
|
|
2550
2694
|
const rem = state.autoplay.remaining;
|
|
2551
2695
|
const label = Number.isFinite(rem) ? String(rem) : '∞';
|
|
2552
2696
|
sp.innerHTML = `<span class="ge-spin-stop">${icon('stop')}</span><span class="ge-spin-count">${label}</span>`;
|
|
2553
|
-
sp.addEventListener('click', () => {
|
|
2554
|
-
|
|
2697
|
+
sp.addEventListener('click', () => {
|
|
2698
|
+
if (!sp.disabled)
|
|
2699
|
+
host.actions.stopAutoplay();
|
|
2700
|
+
});
|
|
2555
2701
|
}
|
|
2556
2702
|
else {
|
|
2557
2703
|
sp.innerHTML = icon('spin');
|
|
2558
2704
|
if (state.busy)
|
|
2559
2705
|
sp.classList.add('ge-spinning');
|
|
2560
|
-
sp.addEventListener('click', () => {
|
|
2561
|
-
|
|
2706
|
+
sp.addEventListener('click', () => {
|
|
2707
|
+
if (!sp.disabled)
|
|
2708
|
+
host.actions.spin();
|
|
2709
|
+
});
|
|
2562
2710
|
}
|
|
2563
2711
|
return sp;
|
|
2564
2712
|
}
|
|
@@ -2707,7 +2855,10 @@ function openSettingsModal(host) {
|
|
|
2707
2855
|
return row;
|
|
2708
2856
|
})();
|
|
2709
2857
|
body.appendChild(sound);
|
|
2710
|
-
// Volume sliders — full-width column rows with a live value readout
|
|
2858
|
+
// Volume sliders — full-width column rows with a live value readout. Positions are read from the
|
|
2859
|
+
// shell's stored volumes (not hardcoded to 100%), so reopening the overlay reflects the last set
|
|
2860
|
+
// value, and `host.setVolume()` from game code updates them live via the registered refreshers.
|
|
2861
|
+
const updaters = {};
|
|
2711
2862
|
const slider = (key, label) => {
|
|
2712
2863
|
const row = document.createElement('div');
|
|
2713
2864
|
row.className = 'ge-ov-row ge-col';
|
|
@@ -2715,7 +2866,6 @@ function openSettingsModal(host) {
|
|
|
2715
2866
|
head.className = 'ge-row-head';
|
|
2716
2867
|
const val = document.createElement('span');
|
|
2717
2868
|
val.className = 'ge-val';
|
|
2718
|
-
val.textContent = '100%';
|
|
2719
2869
|
head.innerHTML = `<span>${label}</span>`;
|
|
2720
2870
|
head.appendChild(val);
|
|
2721
2871
|
const input = document.createElement('input');
|
|
@@ -2723,19 +2873,23 @@ function openSettingsModal(host) {
|
|
|
2723
2873
|
input.min = '0';
|
|
2724
2874
|
input.max = '1';
|
|
2725
2875
|
input.step = '0.05';
|
|
2726
|
-
input.value = '1';
|
|
2727
2876
|
input.className = 'ge-slider';
|
|
2728
2877
|
input.dataset.ge = `setting-${key}`;
|
|
2878
|
+
const paint = (v) => { input.value = String(v); val.textContent = `${Math.round(v * 100)}%`; };
|
|
2879
|
+
paint(host.getVolume(key));
|
|
2729
2880
|
input.addEventListener('input', () => {
|
|
2730
2881
|
val.textContent = `${Math.round(Number(input.value) * 100)}%`;
|
|
2731
|
-
host.
|
|
2882
|
+
host.setVolume(key, Number(input.value));
|
|
2732
2883
|
});
|
|
2884
|
+
updaters[key] = paint;
|
|
2733
2885
|
row.append(head, input);
|
|
2734
2886
|
return row;
|
|
2735
2887
|
};
|
|
2736
2888
|
body.appendChild(slider('master', host.t('Master volume')));
|
|
2737
2889
|
body.appendChild(slider('music', host.t('Music')));
|
|
2738
2890
|
body.appendChild(slider('sfx', host.t('SFX')));
|
|
2891
|
+
// Live-update sliders when volume changes via host.setVolume (shell clears on close).
|
|
2892
|
+
host.setVolumeRefresh((key, v) => updaters[key]?.(v));
|
|
2739
2893
|
// Game info — full-width row button that opens its own overlay
|
|
2740
2894
|
const gameInfo = document.createElement('button');
|
|
2741
2895
|
gameInfo.className = 'ge-ov-row';
|