@energy8platform/shell 0.5.0 → 0.6.1
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 +47 -6
- package/dist/html.cjs.js.map +1 -1
- package/dist/html.d.ts +34 -5
- package/dist/html.esm.js +47 -6
- package/dist/html.esm.js.map +1 -1
- package/dist/index.cjs.js +37 -2
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +34 -5
- package/dist/index.esm.js +37 -2
- package/dist/index.esm.js.map +1 -1
- package/dist/pixi.cjs.js +69 -10
- package/dist/pixi.cjs.js.map +1 -1
- package/dist/pixi.d.ts +34 -5
- package/dist/pixi.esm.js +69 -10
- package/dist/pixi.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/core/ShellController.ts +29 -1
- package/src/core/renderer.ts +8 -1
- package/src/core/state.ts +10 -0
- package/src/core/types.ts +12 -1
- package/src/core/version.ts +1 -1
- package/src/ui/html/components/Settings.ts +14 -5
- package/src/ui/pixi/PixiRenderer.ts +9 -0
- package/src/ui/pixi/components/Settings.ts +23 -8
- package/src/ui/pixi/primitives/controls.ts +7 -0
package/package.json
CHANGED
|
@@ -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;
|
|
@@ -293,10 +314,17 @@ export class ShellController extends EventEmitter<ShellEvents> implements ShellH
|
|
|
293
314
|
this.prevBalance = n;
|
|
294
315
|
this.money('balance', from, n);
|
|
295
316
|
}
|
|
296
|
-
|
|
317
|
+
/** Set the WIN readout. Counts up/down from the previous value by default. Pass
|
|
318
|
+
* `{ animate: false }` to SNAP instantly (renderBar cancels any in-flight count-up) — used by the
|
|
319
|
+
* host to clear WIN to 0 at spin start, where an animated count-DOWN would look wrong. */
|
|
320
|
+
setWin(n: number, opts?: { animate?: boolean }): void {
|
|
297
321
|
const from = this.prevWin;
|
|
298
322
|
this.state.win = n;
|
|
299
323
|
this.prevWin = n;
|
|
324
|
+
if (opts?.animate === false) {
|
|
325
|
+
this.renderer.renderBar(); // instant repaint from state; cancels running money anims
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
300
328
|
this.money('win', from, n);
|
|
301
329
|
}
|
|
302
330
|
setBet(n: number): void {
|
package/src/core/renderer.ts
CHANGED
|
@@ -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 {
|
package/src/core/version.ts
CHANGED
|
@@ -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
|
-
|
|
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';
|
|
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';
|
|
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.
|
|
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');
|
|
@@ -75,6 +75,12 @@ export class PixiRenderer implements ShellRenderer {
|
|
|
75
75
|
this.app.stage.eventMode = 'static';
|
|
76
76
|
|
|
77
77
|
this.app.renderer.on('resize', this.onResize);
|
|
78
|
+
// Seed the layout from the CURRENT screen size. The renderer was resized to the container during
|
|
79
|
+
// boot (ViewportManager.refresh) BEFORE this shell mounted and subscribed above, so that initial
|
|
80
|
+
// 'resize' event is already gone and won't fire again on a stationary device. Without this seed
|
|
81
|
+
// the controller's layout stays at its 'wide' DEFAULT — a portrait mobile would show the DESKTOP
|
|
82
|
+
// bar. (The HTML renderer gets this for free: its ResizeObserver fires immediately on observe.)
|
|
83
|
+
if (this.screenW > 0) this.host.notifyResize(this.screenW, this.screenH);
|
|
78
84
|
whenFontReady(() => {
|
|
79
85
|
if (!this.destroyed) this.renderBar();
|
|
80
86
|
});
|
|
@@ -333,6 +339,9 @@ export class PixiRenderer implements ShellRenderer {
|
|
|
333
339
|
notifyResize: (w, h) => host.notifyResize(w, h),
|
|
334
340
|
setSound: (on) => host.setSound(on),
|
|
335
341
|
setSoundRefresh: (fn) => host.setSoundRefresh(fn),
|
|
342
|
+
getVolume: (key) => host.getVolume(key),
|
|
343
|
+
setVolume: (key, v) => host.setVolume(key, v),
|
|
344
|
+
setVolumeRefresh: (fn) => host.setVolumeRefresh(fn),
|
|
336
345
|
// — Pixi-specific surface —
|
|
337
346
|
get ticker() { return self.app.ticker; },
|
|
338
347
|
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
|
-
|
|
45
|
-
|
|
46
|
-
col.add(sliderRow(host, width, '
|
|
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(
|
|
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
|
|
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,
|
|
121
|
+
const slider = new Slider(host, initial, (v) => {
|
|
111
122
|
valueText.text = `${Math.round(v * 100)}%`;
|
|
112
|
-
host.
|
|
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();
|