@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@energy8platform/shell",
3
- "version": "0.4.1",
3
+ "version": "0.6.0",
4
4
  "description": "Energy8 branded game shell — one logic core, pluggable html/pixi renderers behind a stable contract.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs.js",
@@ -6,14 +6,32 @@ import { createI18n, type I18n } from './i18n';
6
6
  import { KeyboardController, type KeyboardHost } from './keyboard';
7
7
  import { PACKAGE_VERSION } from './version';
8
8
  import type {
9
- ShellConfig, ResolvedShellConfig, ShellState, ShellEvents, ShellMode,
10
- AutoplayOptions, FreeSpinsState, BonusOption, ThemeConfig, ModalOptions, ReplayModalOptions,
9
+ ShellConfig,
10
+ ResolvedShellConfig,
11
+ ShellState,
12
+ ShellEvents,
13
+ ShellMode,
14
+ AutoplayOptions,
15
+ FreeSpinsState,
16
+ BonusReadout,
17
+ BonusOption,
18
+ ThemeConfig,
19
+ ModalOptions,
20
+ ReplayModalOptions,
21
+ VolumeKey,
11
22
  } from './types';
12
23
  import type {
13
- ShellRenderer, ShellHost, ShellActions, OverlayHandle, OverlayRequest, ShellLayoutMode,
24
+ ShellRenderer,
25
+ ShellHost,
26
+ ShellActions,
27
+ OverlayHandle,
28
+ OverlayRequest,
29
+ ShellLayoutMode,
14
30
  } from './renderer';
15
31
 
16
- export interface CreateShellOptions extends ShellConfig { renderer: ShellRenderer; }
32
+ export interface CreateShellOptions extends ShellConfig {
33
+ renderer: ShellRenderer;
34
+ }
17
35
 
18
36
  /** Apply defaults to the raw config (the mount target lives on the renderer, not here). */
19
37
  export function resolveConfig(config: ShellConfig): ResolvedShellConfig {
@@ -30,6 +48,7 @@ export function resolveConfig(config: ShellConfig): ResolvedShellConfig {
30
48
  features: config.features,
31
49
  theme: config.theme,
32
50
  onBonusBuy: config.onBonusBuy,
51
+ volumes: config.volumes,
33
52
  version: config.version ?? '1.0.0',
34
53
  isSocial: config.isSocial ?? false,
35
54
  replay: config.replay ?? config.mode === 'replay',
@@ -53,6 +72,7 @@ export class ShellController extends EventEmitter<ShellEvents> implements ShellH
53
72
  private kbd?: KeyboardController;
54
73
  private overlay: OverlayHandle | null = null;
55
74
  private soundRefresh: ((on: boolean) => void) | null = null;
75
+ private volumeRefresh: ((key: VolumeKey, value: number) => void) | null = null;
56
76
  private prevBalance: number;
57
77
  private prevWin: number;
58
78
  private destroyed = false;
@@ -79,12 +99,21 @@ export class ShellController extends EventEmitter<ShellEvents> implements ShellH
79
99
  }
80
100
 
81
101
  // ── ShellHost ──────────────────────────────────────────────────────────────
82
- t(text: string): string { return this.i18n.t(text); }
83
- formatCurrency(n: number, win = false): string { return formatCurrency(n, this.config.currency, win); }
84
- formatWin(value: number): string { return this.formatCurrency(value, true); }
102
+ t(text: string): string {
103
+ return this.i18n.t(text);
104
+ }
105
+ formatCurrency(n: number, win = false): string {
106
+ return formatCurrency(n, this.config.currency, win);
107
+ }
108
+ formatWin(value: number): string {
109
+ return this.formatCurrency(value, true);
110
+ }
85
111
  notifyResize(w: number, h: number): void {
86
112
  const layout: ShellLayoutMode = w !== 0 && h > w ? 'mobile' : 'wide';
87
- if (layout !== this.layout) { this.layout = layout; this.renderer.setLayout(layout); }
113
+ if (layout !== this.layout) {
114
+ this.layout = layout;
115
+ this.renderer.setLayout(layout);
116
+ }
88
117
  this.renderer.renderBar();
89
118
  }
90
119
 
@@ -94,7 +123,9 @@ export class ShellController extends EventEmitter<ShellEvents> implements ShellH
94
123
  stepBet: (dir) => {
95
124
  const next = stepBet(this.state, dir);
96
125
  if (next === this.state.bet) return;
97
- this.state.bet = next; this.emit('betChange', next); this.renderer.renderBar();
126
+ this.state.bet = next;
127
+ this.emit('betChange', next);
128
+ this.renderer.renderBar();
98
129
  },
99
130
  setBet: (n) => {
100
131
  if (n !== this.state.bet) {
@@ -105,7 +136,9 @@ export class ShellController extends EventEmitter<ShellEvents> implements ShellH
105
136
  },
106
137
  cycleTurbo: () => {
107
138
  const next = nextTurbo(this.state.turbo, this.config.features.turbo);
108
- this.state.turbo = next; this.emit('turboChange', next); this.renderer.renderBar();
139
+ this.state.turbo = next;
140
+ this.emit('turboChange', next);
141
+ this.renderer.renderBar();
109
142
  },
110
143
  toggleAutoplay: () => {
111
144
  if (this.state.autoplay.active) a.stopAutoplay();
@@ -113,11 +146,13 @@ export class ShellController extends EventEmitter<ShellEvents> implements ShellH
113
146
  },
114
147
  startAutoplay: (remaining) => {
115
148
  this.state.autoplay = { active: true, remaining };
116
- this.emit('autoplayStart', { active: true, remaining }); this.renderer.renderBar();
149
+ this.emit('autoplayStart', { active: true, remaining });
150
+ this.renderer.renderBar();
117
151
  },
118
152
  stopAutoplay: () => {
119
153
  this.state.autoplay = { active: false, remaining: 0 };
120
- this.emit('autoplayStop'); this.renderer.renderBar();
154
+ this.emit('autoplayStop');
155
+ this.renderer.renderBar();
121
156
  },
122
157
  openMenu: () => this.openMenu(),
123
158
  openSettings: () => this.openSettings(),
@@ -138,12 +173,24 @@ export class ShellController extends EventEmitter<ShellEvents> implements ShellH
138
173
  // eslint-disable-next-line @typescript-eslint/no-this-alias
139
174
  const self = this;
140
175
  const host: KeyboardHost = {
141
- get state() { return self.state; },
142
- get hotkeysEnabled() { return self.config.features.hotkeys !== false; },
143
- get spacebarEnabled() { return self.config.features.spacebar !== false; },
144
- get turboLevels() { return self.config.features.turbo; },
145
- get autoplayEnabled() { return self.config.features.autoplay != null; },
146
- get buyBonusEnabled() { return self.config.features.buyBonus !== false; },
176
+ get state() {
177
+ return self.state;
178
+ },
179
+ get hotkeysEnabled() {
180
+ return self.config.features.hotkeys !== false;
181
+ },
182
+ get spacebarEnabled() {
183
+ return self.config.features.spacebar !== false;
184
+ },
185
+ get turboLevels() {
186
+ return self.config.features.turbo;
187
+ },
188
+ get autoplayEnabled() {
189
+ return self.config.features.autoplay != null;
190
+ },
191
+ get buyBonusEnabled() {
192
+ return self.config.features.buyBonus !== false;
193
+ },
147
194
  hasOpenLayer: () => self.overlay !== null,
148
195
  routeToLayer: (e) => self.overlay?.onKey?.(e) ?? false,
149
196
  spin: () => self.actions.spin(),
@@ -160,26 +207,57 @@ export class ShellController extends EventEmitter<ShellEvents> implements ShellH
160
207
  this.kbd.attach();
161
208
  }
162
209
 
163
- private pullFocus = (): void => { try { (globalThis as { focus?: () => void }).focus?.(); } catch { /* cross-origin */ } };
210
+ private pullFocus = (): void => {
211
+ try {
212
+ (globalThis as { focus?: () => void }).focus?.();
213
+ } catch {
214
+ /* cross-origin */
215
+ }
216
+ };
164
217
 
165
218
  // ── overlay flow ─────────────────────────────────────────────────────────────
166
219
  private show(req: OverlayRequest): void {
167
220
  this.closeModal();
168
221
  this.overlay = this.renderer.openOverlay(req) ?? null;
169
222
  }
170
- openMenu(): void { this.emit('menuOpen'); this.openSettings(); }
171
- openSettings(): void { this.emit('settingsOpen'); this.show({ kind: 'settings' }); }
172
- openInfo(): void { this.emit('infoOpen'); this.show({ kind: 'gameInfo' }); }
173
- openBuyBonus(): void { if (this.config.onBonusBuy) { this.config.onBonusBuy(); return; } this.show({ kind: 'buyBonus' }); }
174
- openBetPicker(): void { this.show({ kind: 'betPicker' }); }
175
- openAutoplayPicker(): void { this.show({ kind: 'autoplayPicker' }); }
176
- openReplay(opts: ReplayModalOptions): void { if (this.destroyed) return; this.show({ kind: 'replay', opts }); }
177
- openModal(opts: ModalOptions): void { this.show({ kind: 'modal', opts }); }
223
+ openMenu(): void {
224
+ this.emit('menuOpen');
225
+ this.openSettings();
226
+ }
227
+ openSettings(): void {
228
+ this.emit('settingsOpen');
229
+ this.show({ kind: 'settings' });
230
+ }
231
+ openInfo(): void {
232
+ this.emit('infoOpen');
233
+ this.show({ kind: 'gameInfo' });
234
+ }
235
+ openBuyBonus(): void {
236
+ if (this.config.onBonusBuy) {
237
+ this.config.onBonusBuy();
238
+ return;
239
+ }
240
+ this.show({ kind: 'buyBonus' });
241
+ }
242
+ openBetPicker(): void {
243
+ this.show({ kind: 'betPicker' });
244
+ }
245
+ openAutoplayPicker(): void {
246
+ this.show({ kind: 'autoplayPicker' });
247
+ }
248
+ openReplay(opts: ReplayModalOptions): void {
249
+ if (this.destroyed) return;
250
+ this.show({ kind: 'replay', opts });
251
+ }
252
+ openModal(opts: ModalOptions): void {
253
+ this.show({ kind: 'modal', opts });
254
+ }
178
255
  /** Programmatically dismiss whatever overlay/modal is open. No-op when nothing is shown. */
179
256
  closeModal(): void {
180
257
  if (!this.overlay) return;
181
258
  this.overlay = null;
182
259
  this.soundRefresh = null;
260
+ this.volumeRefresh = null;
183
261
  this.renderer.closeOverlay();
184
262
  }
185
263
 
@@ -190,16 +268,39 @@ export class ShellController extends EventEmitter<ShellEvents> implements ShellH
190
268
  this.soundRefresh?.(on);
191
269
  this.renderer.refreshSoundIcon?.(on);
192
270
  }
193
- setSoundRefresh(fn: ((on: boolean) => void) | null): void { this.soundRefresh = fn; }
271
+ setSoundRefresh(fn: ((on: boolean) => void) | null): void {
272
+ this.soundRefresh = fn;
273
+ }
274
+
275
+ // ── volume ─────────────────────────────────────────────────────────────────
276
+ getVolume(key: VolumeKey): number {
277
+ return this.state.volumes[key];
278
+ }
279
+ /** Set a volume slider (0..1). Shared by the slider control (drag) and game code (public API):
280
+ * clamps, stores so a reopened Settings overlay reflects it, emits `settingChange`, and
281
+ * live-updates the slider if the overlay is currently open. */
282
+ setVolume(key: VolumeKey, value: number): void {
283
+ const v = Math.max(0, Math.min(1, value));
284
+ this.state.volumes[key] = v;
285
+ this.emit('settingChange', { key, value: v });
286
+ this.volumeRefresh?.(key, v);
287
+ }
288
+ setVolumeRefresh(fn: ((key: VolumeKey, value: number) => void) | null): void {
289
+ this.volumeRefresh = fn;
290
+ }
194
291
 
195
292
  // ── features ─────────────────────────────────────────────────────────────────
196
293
  activateFeature(bonus: BonusOption): void {
197
- this.state.activeFeature = bonus; this.emit('featureActivate', { id: bonus.id }); this.renderer.renderBar();
294
+ this.state.activeFeature = bonus;
295
+ this.emit('featureActivate', { id: bonus.id });
296
+ this.renderer.renderBar();
198
297
  }
199
298
  deactivateFeature(): void {
200
299
  const prev = this.state.activeFeature;
201
300
  if (!prev) return;
202
- this.state.activeFeature = null; this.emit('featureDeactivate', { id: prev.id }); this.renderer.renderBar();
301
+ this.state.activeFeature = null;
302
+ this.emit('featureDeactivate', { id: prev.id });
303
+ this.renderer.renderBar();
203
304
  }
204
305
 
205
306
  // ── game-facing public API (mirrors GameShell/PixiGameShell) ───────────────────
@@ -207,19 +308,79 @@ export class ShellController extends EventEmitter<ShellEvents> implements ShellH
207
308
  this.renderer.renderBar();
208
309
  if (to !== from) this.renderer.animateMoney(field, from, to);
209
310
  }
210
- setBalance(n: number): void { const from = this.prevBalance; this.state.balance = n; this.prevBalance = n; this.money('balance', from, n); }
211
- setWin(n: number): void { const from = this.prevWin; this.state.win = n; this.prevWin = n; this.money('win', from, n); }
212
- setBet(n: number): void { this.state.bet = n; this.renderer.renderBar(); }
213
- setMode(mode: ShellMode): void { if (mode === 'replay') this.state.replay = true; this.state.mode = mode; this.renderer.renderBar(); }
214
- setBusy(busy: boolean): void { this.state.busy = busy; this.renderer.renderBar(); this.kbd?.notifyBusyChanged(busy); }
215
- setAutoplay(a: AutoplayOptions): void { this.state.autoplay = a; this.renderer.renderBar(); }
216
- setTurbo(level: number): void { this.state.turbo = level; this.renderer.renderBar(); }
217
- setBuyBonusEnabled(enabled: boolean): void { this.state.buyBonusEnabled = enabled; this.renderer.renderBar(); }
218
- setFreeSpins(fs: FreeSpinsState): void { this.state.freeSpins = fs; this.renderer.renderBar(); }
219
- setTheme(theme: ThemeConfig): void { this.config.theme = theme; this.tokens = resolveTheme(theme); this.renderer.applyTheme(this.tokens); this.renderer.renderBar(); }
220
- setLanguage(lang: string): void { this.config.language = lang; this.i18n = createI18n({ language: lang, isSocial: this.config.isSocial }); this.renderer.renderBar(); }
221
- setSocial(isSocial: boolean): void { this.config.isSocial = isSocial; this.i18n = createI18n({ language: this.config.language, isSocial }); this.renderer.renderBar(); }
222
- setLayout(layout: ShellLayoutMode): void { if (layout === this.layout) return; this.layout = layout; this.renderer.setLayout(layout); this.renderer.renderBar(); }
311
+ setBalance(n: number): void {
312
+ const from = this.prevBalance;
313
+ this.state.balance = n;
314
+ this.prevBalance = n;
315
+ this.money('balance', from, n);
316
+ }
317
+ setWin(n: number): void {
318
+ const from = this.prevWin;
319
+ this.state.win = n;
320
+ this.prevWin = n;
321
+ this.money('win', from, n);
322
+ }
323
+ setBet(n: number): void {
324
+ this.state.bet = n;
325
+ this.renderer.renderBar();
326
+ }
327
+ setMode(mode: ShellMode): void {
328
+ if (mode === 'replay') this.state.replay = true;
329
+ this.state.mode = mode;
330
+ this.renderer.renderBar();
331
+ }
332
+ setBusy(busy: boolean): void {
333
+ this.state.busy = busy;
334
+ this.renderer.renderBar();
335
+ this.kbd?.notifyBusyChanged(busy);
336
+ }
337
+ setAutoplay(a: AutoplayOptions): void {
338
+ this.state.autoplay = a;
339
+ this.renderer.renderBar();
340
+ }
341
+ setTurbo(level: number): void {
342
+ this.state.turbo = level;
343
+ this.renderer.renderBar();
344
+ }
345
+ setBuyBonusEnabled(enabled: boolean): void {
346
+ this.state.buyBonusEnabled = enabled;
347
+ this.renderer.renderBar();
348
+ }
349
+ setFreeSpins(fs: FreeSpinsState): void {
350
+ this.state.freeSpins = fs;
351
+ this.state.bonus = null;
352
+ this.renderer.renderBar();
353
+ }
354
+ /** Generic bonus readout (adventure / hold-and-spin / respins). Sets a game-supplied label+value
355
+ * override for the bar hero and folds `totalWin` into the shared accumulator. Pairs with
356
+ * `setMode('bonus')`. `setFreeSpins()` clears the override back to the derived current/total. */
357
+ setBonus(b: BonusReadout): void {
358
+ this.state.bonus = { label: b.label, value: b.value };
359
+ this.state.freeSpins = { ...this.state.freeSpins, totalWin: b.totalWin };
360
+ this.renderer.renderBar();
361
+ }
362
+ setTheme(theme: ThemeConfig): void {
363
+ this.config.theme = theme;
364
+ this.tokens = resolveTheme(theme);
365
+ this.renderer.applyTheme(this.tokens);
366
+ this.renderer.renderBar();
367
+ }
368
+ setLanguage(lang: string): void {
369
+ this.config.language = lang;
370
+ this.i18n = createI18n({ language: lang, isSocial: this.config.isSocial });
371
+ this.renderer.renderBar();
372
+ }
373
+ setSocial(isSocial: boolean): void {
374
+ this.config.isSocial = isSocial;
375
+ this.i18n = createI18n({ language: this.config.language, isSocial });
376
+ this.renderer.renderBar();
377
+ }
378
+ setLayout(layout: ShellLayoutMode): void {
379
+ if (layout === this.layout) return;
380
+ this.layout = layout;
381
+ this.renderer.setLayout(layout);
382
+ this.renderer.renderBar();
383
+ }
223
384
 
224
385
  destroy(): Promise<void> {
225
386
  if (this.destroyed) return Promise.resolve();
@@ -2,7 +2,7 @@ import type { EventEmitter } from './EventEmitter';
2
2
  import type { ShellTokens } from './theme';
3
3
  import type {
4
4
  ResolvedShellConfig, ShellState, ShellEvents, BonusOption,
5
- ModalOptions, ReplayModalOptions,
5
+ ModalOptions, ReplayModalOptions, VolumeKey,
6
6
  } from './types';
7
7
 
8
8
  export type ShellLayoutMode = 'wide' | 'mobile';
@@ -77,6 +77,13 @@ export interface ShellHost {
77
77
  setSound(on: boolean): void;
78
78
  /** An open Settings overlay registers an icon updater here (null clears it on close). */
79
79
  setSoundRefresh(fn: ((on: boolean) => void) | null): void;
80
+ /** Current volume slider position (0..1) for master/music/sfx. */
81
+ getVolume(key: VolumeKey): number;
82
+ /** Set a volume slider (0..1): clamps, stores, emits `settingChange`, and live-updates an open
83
+ * Settings overlay. Called by the slider control on drag AND by game code as the public API. */
84
+ setVolume(key: VolumeKey, value: number): void;
85
+ /** An open Settings overlay registers a slider updater here (null clears it on close). */
86
+ setVolumeRefresh(fn: ((key: VolumeKey, value: number) => void) | null): void;
80
87
  /** Logic-bearing actions invoked by renderer controls. */
81
88
  readonly actions: ShellActions;
82
89
  }
package/src/core/state.ts CHANGED
@@ -13,10 +13,21 @@ export function createInitialState(config: ShellConfig): ShellState {
13
13
  turbo: 0,
14
14
  buyBonusEnabled: true,
15
15
  freeSpins: { current: 0, total: 0, totalWin: 0 },
16
+ bonus: null,
16
17
  activeFeature: null,
18
+ volumes: {
19
+ master: clampVolume(config.volumes?.master),
20
+ music: clampVolume(config.volumes?.music),
21
+ sfx: clampVolume(config.volumes?.sfx),
22
+ },
17
23
  };
18
24
  }
19
25
 
26
+ /** Clamp a configured volume to 0..1, defaulting to full (1) when unset/invalid. */
27
+ export function clampVolume(v: number | undefined): number {
28
+ return typeof v === 'number' && Number.isFinite(v) ? Math.max(0, Math.min(1, v)) : 1;
29
+ }
30
+
20
31
  /** Step bet up/down within availableBets, clamped at the ends. */
21
32
  export function stepBet(state: ShellState, direction: 1 | -1): number {
22
33
  const idx = state.availableBets.indexOf(state.bet);
package/src/core/types.ts CHANGED
@@ -1,4 +1,12 @@
1
- export type ShellMode = 'base' | 'freeSpins' | 'replay';
1
+ /** `freeSpins` and `bonus` are the SAME bar layout (host-driven hero + Total Win); `freeSpins` is
2
+ * kept as a back-compat alias for the common case (its readout is derived current/total), while
3
+ * `bonus` pairs with `setBonus()` to show a game-supplied label + value (adventure, hold-and-spin,
4
+ * respins — anything that isn't a plain free-spins counter). */
5
+ export type ShellMode = 'base' | 'bonus' | 'freeSpins' | 'replay';
6
+
7
+ /** The three independent volume sliders shown in the Settings overlay. */
8
+ export type VolumeKey = 'master' | 'music' | 'sfx';
9
+ export type VolumeLevels = Record<VolumeKey, number>;
2
10
 
3
11
  export interface CurrencyConfig {
4
12
  symbol: string;
@@ -162,6 +170,20 @@ export interface FreeSpinsState {
162
170
  totalWin: number;
163
171
  }
164
172
 
173
+ /** A game-supplied bonus readout for the bar hero, set via `setBonus()`. Generalizes the
174
+ * free-spins counter: the shell renders `label` (localized via the shell translator, so a game
175
+ * i18n entry for it is honoured) + `value` (a pre-formatted string, shown verbatim — "2 / 10",
176
+ * "3", "×5", "7 coins") + the Total Win accumulator. The shell stays free of any per-game bonus
177
+ * concept — the label/value semantics live in the game/host. */
178
+ export interface BonusReadout {
179
+ /** Bar label, e.g. 'Free spins' | 'Adventure' | 'Hold & Spin'. Run through the shell translator. */
180
+ label: string;
181
+ /** Pre-formatted counter value shown verbatim (NOT translated). */
182
+ value: string;
183
+ /** Cumulative bonus win for the Total Win readout. */
184
+ totalWin: number;
185
+ }
186
+
165
187
  /** One footer button of a generic modal. Clicking it runs `on` (if any), then closes the modal. */
166
188
  export interface ModalAction {
167
189
  title: string;
@@ -226,12 +248,31 @@ export interface ShellConfig {
226
248
  * opening the built-in buy-bonus overlay (e.g. the game shows its own bonus UI). The button
227
249
  * is shown whenever this OR `features.buyBonus` is set. */
228
250
  onBonusBuy?: () => void;
251
+ /** Initial Settings-overlay volume slider positions (each 0..1, defaults to 1 = 100%). The shell
252
+ * keeps them stateful across opens; read/update at runtime via `shell.getVolume()` /
253
+ * `shell.setVolume()`, and listen to `settingChange` ({ key: 'master'|'music'|'sfx' }) to apply. */
254
+ volumes?: Partial<VolumeLevels>;
229
255
  }
230
256
 
231
257
  /** ShellConfig after the controller applies defaults (version, isSocial, replay, theme). No mount. */
232
- export type ResolvedShellConfig = Required<Pick<ShellConfig,
233
- 'language' | 'currency' | 'availableBets' | 'defaultBet' | 'balance' | 'win' | 'mode' | 'features' | 'gameInfo' | 'version' | 'isSocial' | 'replay'>>
234
- & Pick<ShellConfig, 'currentBet' | 'theme' | 'onBonusBuy'>;
258
+ export type ResolvedShellConfig = Required<
259
+ Pick<
260
+ ShellConfig,
261
+ | 'language'
262
+ | 'currency'
263
+ | 'availableBets'
264
+ | 'defaultBet'
265
+ | 'balance'
266
+ | 'win'
267
+ | 'mode'
268
+ | 'features'
269
+ | 'gameInfo'
270
+ | 'version'
271
+ | 'isSocial'
272
+ | 'replay'
273
+ >
274
+ > &
275
+ Pick<ShellConfig, 'currentBet' | 'theme' | 'onBonusBuy' | 'volumes'>;
235
276
 
236
277
  export interface ShellState {
237
278
  mode: ShellMode;
@@ -248,9 +289,15 @@ export interface ShellState {
248
289
  turbo: number;
249
290
  buyBonusEnabled: boolean;
250
291
  freeSpins: FreeSpinsState;
292
+ /** Game-supplied bonus label + value override (from `setBonus()`). When null the bar derives the
293
+ * readout from `freeSpins` (label 'Free spins', value current/total) — the back-compat default. */
294
+ bonus: { label: string; value: string } | null;
251
295
  /** The currently activated `feature` option (e.g. Ante), or null. Drives the
252
296
  * effective-bet readout tint and the BUY BONUS → DISABLE toggle on the bar. */
253
297
  activeFeature: BonusOption | null;
298
+ /** Volume slider positions (0..1) surfaced in the Settings overlay. Stateful across opens so a
299
+ * reopened overlay reflects the last-set positions instead of resetting to 100%. */
300
+ volumes: VolumeLevels;
254
301
  }
255
302
 
256
303
  export interface ShellEvents {
@@ -1,3 +1,3 @@
1
1
  // AUTO-GENERATED by scripts/gen-version.mjs — do not edit. Mirrors package.json "version".
2
2
  /** The @energy8platform/shell package version, stamped into the game-info footer. */
3
- export const PACKAGE_VERSION = '0.4.1';
3
+ export const PACKAGE_VERSION = '0.6.0';