@energy8platform/shell 0.5.0 → 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.5.0",
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",
@@ -18,6 +18,7 @@ import type {
18
18
  ThemeConfig,
19
19
  ModalOptions,
20
20
  ReplayModalOptions,
21
+ VolumeKey,
21
22
  } from './types';
22
23
  import type {
23
24
  ShellRenderer,
@@ -47,6 +48,7 @@ export function resolveConfig(config: ShellConfig): ResolvedShellConfig {
47
48
  features: config.features,
48
49
  theme: config.theme,
49
50
  onBonusBuy: config.onBonusBuy,
51
+ volumes: config.volumes,
50
52
  version: config.version ?? '1.0.0',
51
53
  isSocial: config.isSocial ?? false,
52
54
  replay: config.replay ?? config.mode === 'replay',
@@ -70,6 +72,7 @@ export class ShellController extends EventEmitter<ShellEvents> implements ShellH
70
72
  private kbd?: KeyboardController;
71
73
  private overlay: OverlayHandle | null = null;
72
74
  private soundRefresh: ((on: boolean) => void) | null = null;
75
+ private volumeRefresh: ((key: VolumeKey, value: number) => void) | null = null;
73
76
  private prevBalance: number;
74
77
  private prevWin: number;
75
78
  private destroyed = false;
@@ -254,6 +257,7 @@ export class ShellController extends EventEmitter<ShellEvents> implements ShellH
254
257
  if (!this.overlay) return;
255
258
  this.overlay = null;
256
259
  this.soundRefresh = null;
260
+ this.volumeRefresh = null;
257
261
  this.renderer.closeOverlay();
258
262
  }
259
263
 
@@ -268,6 +272,23 @@ export class ShellController extends EventEmitter<ShellEvents> implements ShellH
268
272
  this.soundRefresh = fn;
269
273
  }
270
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
+ }
291
+
271
292
  // ── features ─────────────────────────────────────────────────────────────────
272
293
  activateFeature(bonus: BonusOption): void {
273
294
  this.state.activeFeature = bonus;
@@ -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
@@ -15,9 +15,19 @@ export function createInitialState(config: ShellConfig): ShellState {
15
15
  freeSpins: { current: 0, total: 0, totalWin: 0 },
16
16
  bonus: null,
17
17
  activeFeature: null,
18
+ volumes: {
19
+ master: clampVolume(config.volumes?.master),
20
+ music: clampVolume(config.volumes?.music),
21
+ sfx: clampVolume(config.volumes?.sfx),
22
+ },
18
23
  };
19
24
  }
20
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
+
21
31
  /** Step bet up/down within availableBets, clamped at the ends. */
22
32
  export function stepBet(state: ShellState, direction: 1 | -1): number {
23
33
  const idx = state.availableBets.indexOf(state.bet);
package/src/core/types.ts CHANGED
@@ -4,6 +4,10 @@
4
4
  * respins — anything that isn't a plain free-spins counter). */
5
5
  export type ShellMode = 'base' | 'bonus' | 'freeSpins' | 'replay';
6
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>;
10
+
7
11
  export interface CurrencyConfig {
8
12
  symbol: string;
9
13
  position: 'left' | 'right';
@@ -244,6 +248,10 @@ export interface ShellConfig {
244
248
  * opening the built-in buy-bonus overlay (e.g. the game shows its own bonus UI). The button
245
249
  * is shown whenever this OR `features.buyBonus` is set. */
246
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>;
247
255
  }
248
256
 
249
257
  /** ShellConfig after the controller applies defaults (version, isSocial, replay, theme). No mount. */
@@ -264,7 +272,7 @@ export type ResolvedShellConfig = Required<
264
272
  | 'replay'
265
273
  >
266
274
  > &
267
- Pick<ShellConfig, 'currentBet' | 'theme' | 'onBonusBuy'>;
275
+ Pick<ShellConfig, 'currentBet' | 'theme' | 'onBonusBuy' | 'volumes'>;
268
276
 
269
277
  export interface ShellState {
270
278
  mode: ShellMode;
@@ -287,6 +295,9 @@ export interface ShellState {
287
295
  /** The currently activated `feature` option (e.g. Ante), or null. Drives the
288
296
  * effective-bet readout tint and the BUY BONUS → DISABLE toggle on the bar. */
289
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;
290
301
  }
291
302
 
292
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.5.0';
3
+ export const PACKAGE_VERSION = '0.6.0';
@@ -1,4 +1,5 @@
1
1
  import type { ShellHost } from '@/core/renderer';
2
+ import type { VolumeKey } from '@/core/types';
2
3
  import { createOverlay } from '../primitives';
3
4
  import { icon } from '../icons';
4
5
 
@@ -27,25 +28,33 @@ export function openSettingsModal(host: ShellHost): HTMLElement {
27
28
  })();
28
29
  body.appendChild(sound);
29
30
 
30
- // Volume sliders — full-width column rows with a live value readout
31
- const slider = (key: string, label: string) => {
31
+ // Volume sliders — full-width column rows with a live value readout. Positions are read from the
32
+ // shell's stored volumes (not hardcoded to 100%), so reopening the overlay reflects the last set
33
+ // value, and `host.setVolume()` from game code updates them live via the registered refreshers.
34
+ const updaters: Partial<Record<VolumeKey, (v: number) => void>> = {};
35
+ const slider = (key: VolumeKey, label: string) => {
32
36
  const row = document.createElement('div'); row.className = 'ge-ov-row ge-col';
33
37
  const head = document.createElement('div'); head.className = 'ge-row-head';
34
- const val = document.createElement('span'); val.className = 'ge-val'; val.textContent = '100%';
38
+ const val = document.createElement('span'); val.className = 'ge-val';
35
39
  head.innerHTML = `<span>${label}</span>`; head.appendChild(val);
36
40
  const input = document.createElement('input');
37
- input.type = 'range'; input.min = '0'; input.max = '1'; input.step = '0.05'; input.value = '1';
41
+ input.type = 'range'; input.min = '0'; input.max = '1'; input.step = '0.05';
38
42
  input.className = 'ge-slider'; input.dataset.ge = `setting-${key}`;
43
+ const paint = (v: number) => { input.value = String(v); val.textContent = `${Math.round(v * 100)}%`; };
44
+ paint(host.getVolume(key));
39
45
  input.addEventListener('input', () => {
40
46
  val.textContent = `${Math.round(Number(input.value) * 100)}%`;
41
- host.emit('settingChange', { key, value: Number(input.value) });
47
+ host.setVolume(key, Number(input.value));
42
48
  });
49
+ updaters[key] = paint;
43
50
  row.append(head, input);
44
51
  return row;
45
52
  };
46
53
  body.appendChild(slider('master', host.t('Master volume')));
47
54
  body.appendChild(slider('music', host.t('Music')));
48
55
  body.appendChild(slider('sfx', host.t('SFX')));
56
+ // Live-update sliders when volume changes via host.setVolume (shell clears on close).
57
+ host.setVolumeRefresh((key, v) => updaters[key]?.(v));
49
58
 
50
59
  // Game info — full-width row button that opens its own overlay
51
60
  const gameInfo = document.createElement('button');
@@ -333,6 +333,9 @@ export class PixiRenderer implements ShellRenderer {
333
333
  notifyResize: (w, h) => host.notifyResize(w, h),
334
334
  setSound: (on) => host.setSound(on),
335
335
  setSoundRefresh: (fn) => host.setSoundRefresh(fn),
336
+ getVolume: (key) => host.getVolume(key),
337
+ setVolume: (key, v) => host.setVolume(key, v),
338
+ setVolumeRefresh: (fn) => host.setVolumeRefresh(fn),
336
339
  // — Pixi-specific surface —
337
340
  get ticker() { return self.app.ticker; },
338
341
  get canvas() { return self.app.canvas as HTMLCanvasElement | undefined; },
@@ -1,4 +1,5 @@
1
1
  import { Container, Text } from 'pixi.js';
2
+ import type { VolumeKey } from '@/core/types';
2
3
  import type { PixiComponentContext, ShellLayer } from '../context';
3
4
  import { Overlay } from '../primitives/overlay';
4
5
  import { makeText } from '../text';
@@ -40,10 +41,13 @@ function buildBody(host: PixiComponentContext, width: number): Container {
40
41
  });
41
42
  col.add(glassRow(host, [textNode(host, host.t('Sound')), new Spacer(), speaker]));
42
43
 
43
- // Volume sliders
44
- col.add(sliderRow(host, width, 'master', host.t('Master volume')));
45
- col.add(sliderRow(host, width, 'music', host.t('Music')));
46
- col.add(sliderRow(host, width, 'sfx', host.t('SFX')));
44
+ // Volume sliders — positions read from the shell's stored volumes (stateful across opens); the
45
+ // registered refreshers let `host.setVolume()` from game code move the thumbs live.
46
+ const updaters: Partial<Record<VolumeKey, (v: number) => void>> = {};
47
+ col.add(sliderRow(host, width, 'master', host.t('Master volume'), updaters));
48
+ col.add(sliderRow(host, width, 'music', host.t('Music'), updaters));
49
+ col.add(sliderRow(host, width, 'sfx', host.t('SFX'), updaters));
50
+ host.setVolumeRefresh?.((key, v) => updaters[key]?.(v));
47
51
 
48
52
  // Game info link
49
53
  const infoIcon = makeIcon('info', 22, '#ffffff');
@@ -94,7 +98,13 @@ function glassRow(host: PixiComponentContext, children: Container[], opts: { but
94
98
  }
95
99
 
96
100
  /** A column row: head (label + live % value) over a draggable slider. */
97
- function sliderRow(host: PixiComponentContext, bodyWidth: number, key: string, label: string): FlexBox {
101
+ function sliderRow(
102
+ host: PixiComponentContext,
103
+ bodyWidth: number,
104
+ key: VolumeKey,
105
+ label: string,
106
+ updaters: Partial<Record<VolumeKey, (v: number) => void>>,
107
+ ): FlexBox {
98
108
  const row = new FlexBox({
99
109
  direction: 'column',
100
110
  align: 'stretch',
@@ -103,14 +113,19 @@ function sliderRow(host: PixiComponentContext, bodyWidth: number, key: string, l
103
113
  background: { fill: host.tokens.plaqueGlass, radius: 16 },
104
114
  });
105
115
  const head = new FlexBox({ direction: 'row', align: 'center', justify: 'space-between' });
106
- const valueText = makeText('100%', { size: 13, weight: '700', color: host.tokens.plaqueLabel });
116
+ const initial = host.getVolume(key);
117
+ const valueText = makeText(`${Math.round(initial * 100)}%`, { size: 13, weight: '700', color: host.tokens.plaqueLabel });
107
118
  head.add(makeText(label, { size: 14, weight: '600', color: '#ffffff' }));
108
119
  head.add(new Spacer(), { grow: 1 });
109
120
  head.add(valueText);
110
- const slider = new Slider(host, 1, (v) => {
121
+ const slider = new Slider(host, initial, (v) => {
111
122
  valueText.text = `${Math.round(v * 100)}%`;
112
- host.emit('settingChange', { key, value: v });
123
+ host.setVolume(key, v);
113
124
  });
125
+ updaters[key] = (v) => {
126
+ valueText.text = `${Math.round(v * 100)}%`;
127
+ slider.setValue(v);
128
+ };
114
129
  row.add(head);
115
130
  row.add(slider);
116
131
  return row;
@@ -182,6 +182,13 @@ export class Slider extends Container implements Sizable {
182
182
  this.hitArea = new Rectangle(0, 0, this.w, h);
183
183
  }
184
184
 
185
+ /** Programmatically move the thumb (0..1) without firing onInput — for live updates driven by
186
+ * `host.setVolume()` while the overlay is open. */
187
+ setValue(v: number): void {
188
+ this._value = Math.max(0, Math.min(1, v));
189
+ this.draw();
190
+ }
191
+
185
192
  setLayoutSize(w: number | undefined): void {
186
193
  if (w != null) this.w = w;
187
194
  this.draw();