@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
|
@@ -9,8 +9,12 @@ function readout(ge: string, label: string, value: string): HTMLElement {
|
|
|
9
9
|
el.className = `ge-rd ge-${ge}`;
|
|
10
10
|
// The value lives in its own inline-block span (.ge-rd-val) so it can be measured & shrunk to fit
|
|
11
11
|
// (see fitReadouts) independently of the label, and so the count-up animates just the number.
|
|
12
|
-
const lbl = document.createElement('span');
|
|
13
|
-
|
|
12
|
+
const lbl = document.createElement('span');
|
|
13
|
+
lbl.className = 'ge-lbl';
|
|
14
|
+
lbl.textContent = label;
|
|
15
|
+
const val = document.createElement('span');
|
|
16
|
+
val.className = 'ge-rd-val';
|
|
17
|
+
val.textContent = value;
|
|
14
18
|
el.append(lbl, val);
|
|
15
19
|
return el;
|
|
16
20
|
}
|
|
@@ -25,12 +29,19 @@ function turboBtn(host: ShellHost, level: number): HTMLButtonElement {
|
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
/** A borderless icon button. */
|
|
28
|
-
function iconBtn(
|
|
32
|
+
function iconBtn(
|
|
33
|
+
ge: string,
|
|
34
|
+
name: IconName,
|
|
35
|
+
onClick: () => void,
|
|
36
|
+
active = false,
|
|
37
|
+
): HTMLButtonElement {
|
|
29
38
|
const b = document.createElement('button');
|
|
30
39
|
b.className = `ge-iconbtn${active ? ' ge-active' : ''}`;
|
|
31
40
|
b.dataset.ge = ge;
|
|
32
41
|
b.innerHTML = icon(name);
|
|
33
|
-
b.addEventListener('click', () => {
|
|
42
|
+
b.addEventListener('click', () => {
|
|
43
|
+
if (!b.disabled) onClick();
|
|
44
|
+
});
|
|
34
45
|
return b;
|
|
35
46
|
}
|
|
36
47
|
|
|
@@ -49,16 +60,14 @@ export function renderBottomBar(host: ShellHost): HTMLElement {
|
|
|
49
60
|
// All three modes share the base plaque layout. FS/replay hide the controls that don't apply
|
|
50
61
|
// and add Free Spins + Total Win blocks on the left; the per-spin WIN uses the base pill.
|
|
51
62
|
const isBase = state.mode === 'base';
|
|
52
|
-
const isFS = state.mode === 'freeSpins';
|
|
63
|
+
const isFS = state.mode === 'freeSpins' || state.mode === 'bonus';
|
|
53
64
|
// FS always shows the spins counter + accumulated Total Win (even €0); a replay shows them
|
|
54
65
|
// only when it's a free-spins replay (freeSpins.total > 0).
|
|
55
66
|
const showFsBlocks = isFS || (state.mode === 'replay' && state.freeSpins.total > 0);
|
|
56
67
|
|
|
57
68
|
// Replay is a read-only historical round — there's no real balance to show, so hide it. Keyed on
|
|
58
69
|
// the sticky `replay` flag (not `mode`) so it stays hidden through a replay's free-spins phase.
|
|
59
|
-
const balance = state.replay
|
|
60
|
-
? null
|
|
61
|
-
: readout('balance', host.t('Balance'), fmt(state.balance));
|
|
70
|
+
const balance = state.replay ? null : readout('balance', host.t('Balance'), fmt(state.balance));
|
|
62
71
|
// With a feature active (e.g. Ante) the BET readout shows the effective stake, tinted with
|
|
63
72
|
// the feature accent; the base state.bet is unchanged and returns once the feature is off.
|
|
64
73
|
const feature = state.activeFeature;
|
|
@@ -75,27 +84,35 @@ export function renderBottomBar(host: ShellHost): HTMLElement {
|
|
|
75
84
|
const turbo = config.features.turbo > 0 ? turboBtn(host, state.turbo) : null;
|
|
76
85
|
|
|
77
86
|
// interactive controls — base mode only
|
|
78
|
-
let betDown: HTMLElement | null = null,
|
|
79
|
-
|
|
87
|
+
let betDown: HTMLElement | null = null,
|
|
88
|
+
betUp: HTMLElement | null = null;
|
|
89
|
+
let spin: HTMLElement | null = null,
|
|
90
|
+
auto: HTMLElement | null = null,
|
|
91
|
+
buy: HTMLElement | null = null;
|
|
80
92
|
if (isBase) {
|
|
81
93
|
betDown = iconBtn('bet-down', 'minus', () => host.actions.stepBet(-1));
|
|
82
94
|
betUp = iconBtn('bet-up', 'plus', () => host.actions.stepBet(1));
|
|
83
|
-
betValue.classList.add('ge-betbtn');
|
|
84
|
-
betValue.addEventListener('click', () => {
|
|
95
|
+
betValue.classList.add('ge-betbtn'); // tap the stake → bet picker
|
|
96
|
+
betValue.addEventListener('click', () => {
|
|
97
|
+
if (!betLocked(host)) host.actions.openBetPicker();
|
|
98
|
+
});
|
|
85
99
|
spin = spinButton(host);
|
|
86
100
|
auto = config.features.autoplay ? autoButton(host) : null;
|
|
87
|
-
buy =
|
|
101
|
+
buy = config.features.buyBonus !== false || config.onBonusBuy ? buyBtn(host) : null;
|
|
88
102
|
}
|
|
89
103
|
|
|
90
104
|
const winEl = state.win > 0 ? readout('win', host.t('Win'), fmtWin(state.win)) : null;
|
|
91
105
|
// FS/replay left blocks: spins counter + accumulated Total Win (shown even at €0).
|
|
92
106
|
// current = number → "current / total"; current = null/undefined → just the (game-driven) total.
|
|
93
107
|
const fs = state.freeSpins;
|
|
94
|
-
const fsText =
|
|
108
|
+
const fsText =
|
|
109
|
+
state.bonus?.value ?? (fs.current == null ? `${fs.total}` : `${fs.current} / ${fs.total}`);
|
|
95
110
|
// In FS the spins counter takes the SPIN slot as a rectangular hero plaque (same white/black-ring
|
|
96
111
|
// style as the SPIN disc); Total Win stays an inline readout.
|
|
97
|
-
const fsHero = showFsBlocks ? fsHeroPlaque(host, fsText) : null;
|
|
98
|
-
const fsTotalWin = showFsBlocks
|
|
112
|
+
const fsHero = showFsBlocks ? fsHeroPlaque(host, fsText, state.bonus?.label) : null;
|
|
113
|
+
const fsTotalWin = showFsBlocks
|
|
114
|
+
? readout('fs-totalwin', host.t('Total win'), fmtWin(fs.totalWin))
|
|
115
|
+
: null;
|
|
99
116
|
// The hero in the centre/spin position: SPIN in base, the FS counter in free spins, nothing else.
|
|
100
117
|
const hero = isBase ? spin : fsHero;
|
|
101
118
|
|
|
@@ -117,12 +134,16 @@ export function renderBottomBar(host: ShellHost): HTMLElement {
|
|
|
117
134
|
// RIGHT (the controls): [bet (+ step)] · |divider| · [auto · SPIN-or-FS · turbo]
|
|
118
135
|
const betKids: HTMLElement[] = [betValue];
|
|
119
136
|
if (betUp && betDown) {
|
|
120
|
-
const step = document.createElement('div');
|
|
137
|
+
const step = document.createElement('div');
|
|
138
|
+
step.className = 'ge-betstep';
|
|
139
|
+
step.append(betUp, betDown);
|
|
121
140
|
betKids.push(step);
|
|
122
141
|
}
|
|
123
142
|
const betGroup = plaque('ge-betgroup', betKids);
|
|
124
|
-
const divider = document.createElement('div');
|
|
125
|
-
|
|
143
|
+
const divider = document.createElement('div');
|
|
144
|
+
divider.className = 'ge-pl-divider';
|
|
145
|
+
const spinWrap = document.createElement('div');
|
|
146
|
+
spinWrap.className = 'ge-spinwrap';
|
|
126
147
|
spinWrap.append(...compact([auto, hero, turbo]));
|
|
127
148
|
const right = zone('ge-zone-right', betGroup, divider, spinWrap);
|
|
128
149
|
|
|
@@ -138,11 +159,16 @@ export function renderBottomBar(host: ShellHost): HTMLElement {
|
|
|
138
159
|
|
|
139
160
|
/** Free-spins hero plaque — takes the SPIN slot in FS: same white disc/black-ring language as SPIN,
|
|
140
161
|
* but a rounded RECTANGLE showing the spins counter ("3 / 10"). */
|
|
141
|
-
function fsHeroPlaque(host: ShellHost, text: string): HTMLElement {
|
|
162
|
+
function fsHeroPlaque(host: ShellHost, text: string, label?: string): HTMLElement {
|
|
142
163
|
const el = document.createElement('div');
|
|
143
|
-
el.className = 'ge-fs-hero';
|
|
144
|
-
|
|
145
|
-
const
|
|
164
|
+
el.className = 'ge-fs-hero';
|
|
165
|
+
el.dataset.ge = 'fs-counter';
|
|
166
|
+
const lbl = document.createElement('span');
|
|
167
|
+
lbl.className = 'ge-fs-lbl';
|
|
168
|
+
lbl.textContent = host.t(label ?? 'Free spins');
|
|
169
|
+
const num = document.createElement('span');
|
|
170
|
+
num.className = 'ge-fs-num';
|
|
171
|
+
num.textContent = text;
|
|
146
172
|
el.append(lbl, num);
|
|
147
173
|
return el;
|
|
148
174
|
}
|
|
@@ -160,24 +186,32 @@ function plaque(cls: string, children: HTMLElement[]): HTMLElement {
|
|
|
160
186
|
d.append(...children);
|
|
161
187
|
return d;
|
|
162
188
|
}
|
|
163
|
-
function compact(items: (HTMLElement | null)[]): HTMLElement[] {
|
|
189
|
+
function compact(items: (HTMLElement | null)[]): HTMLElement[] {
|
|
190
|
+
return items.filter((x): x is HTMLElement => x !== null);
|
|
191
|
+
}
|
|
164
192
|
|
|
165
193
|
function buyBtn(host: ShellHost): HTMLButtonElement {
|
|
166
194
|
const buy = document.createElement('button');
|
|
167
|
-
buy.className = 'ge-shell-buybonus';
|
|
195
|
+
buy.className = 'ge-shell-buybonus';
|
|
196
|
+
buy.dataset.ge = 'buybonus';
|
|
168
197
|
const feature = host.state.activeFeature;
|
|
169
198
|
if (feature) {
|
|
170
199
|
// A feature is active → this button turns into DISABLE (tinted with the feature accent).
|
|
171
200
|
const accent = effectiveAccent(feature);
|
|
172
201
|
buy.classList.add('ge-disable');
|
|
173
202
|
buy.innerHTML = `<span>${host.t('DISABLE')}</span>`;
|
|
174
|
-
buy.style.background = accent;
|
|
175
|
-
buy.
|
|
203
|
+
buy.style.background = accent;
|
|
204
|
+
buy.style.color = contrastText(accent);
|
|
205
|
+
buy.addEventListener('click', () => {
|
|
206
|
+
if (!buy.disabled) host.actions.deactivateFeature();
|
|
207
|
+
});
|
|
176
208
|
} else {
|
|
177
209
|
// Ticket icon (no text); keep the label for screen readers.
|
|
178
210
|
buy.setAttribute('aria-label', host.t('BUY BONUS'));
|
|
179
211
|
buy.innerHTML = `<span class="ge-bb-tk">${icon('ticket')}</span>`;
|
|
180
|
-
buy.addEventListener('click', () => {
|
|
212
|
+
buy.addEventListener('click', () => {
|
|
213
|
+
if (!buy.disabled) host.actions.openBuyBonus();
|
|
214
|
+
});
|
|
181
215
|
}
|
|
182
216
|
return buy;
|
|
183
217
|
}
|
|
@@ -190,17 +224,22 @@ function betLocked(host: ShellHost): boolean {
|
|
|
190
224
|
function spinButton(host: ShellHost): HTMLButtonElement {
|
|
191
225
|
const { state } = host;
|
|
192
226
|
const sp = document.createElement('button');
|
|
193
|
-
sp.className = 'ge-shell-spin';
|
|
227
|
+
sp.className = 'ge-shell-spin';
|
|
228
|
+
sp.dataset.ge = 'spin';
|
|
194
229
|
if (state.autoplay.active) {
|
|
195
230
|
sp.classList.add('ge-stop');
|
|
196
231
|
const rem = state.autoplay.remaining;
|
|
197
232
|
const label = Number.isFinite(rem) ? String(rem) : '∞';
|
|
198
233
|
sp.innerHTML = `<span class="ge-spin-stop">${icon('stop')}</span><span class="ge-spin-count">${label}</span>`;
|
|
199
|
-
sp.addEventListener('click', () => {
|
|
234
|
+
sp.addEventListener('click', () => {
|
|
235
|
+
if (!sp.disabled) host.actions.stopAutoplay();
|
|
236
|
+
});
|
|
200
237
|
} else {
|
|
201
238
|
sp.innerHTML = icon('spin');
|
|
202
239
|
if (state.busy) sp.classList.add('ge-spinning');
|
|
203
|
-
sp.addEventListener('click', () => {
|
|
240
|
+
sp.addEventListener('click', () => {
|
|
241
|
+
if (!sp.disabled) host.actions.spin();
|
|
242
|
+
});
|
|
204
243
|
}
|
|
205
244
|
return sp;
|
|
206
245
|
}
|
|
@@ -230,7 +269,7 @@ function applyBusy(host: ShellHost, bar: HTMLElement): void {
|
|
|
230
269
|
const i = host.state.availableBets.indexOf(host.state.bet);
|
|
231
270
|
disable('bet-up', lockBet || i >= host.state.availableBets.length - 1);
|
|
232
271
|
disable('bet-down', lockBet || i <= 0);
|
|
233
|
-
disable('spin', busy && !auto);
|
|
272
|
+
disable('spin', busy && !auto); // keep the STOP disc clickable through autoplay
|
|
234
273
|
disable('autoplay', busy && !auto); // keep autoplay (stop) clickable through autoplay
|
|
235
274
|
const betVal = bar.querySelector('[data-ge="bet-value"]') as HTMLElement | null;
|
|
236
275
|
if (betVal) betVal.classList.toggle('ge-disabled', lockBet);
|
|
@@ -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');
|
|
@@ -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; },
|