@energy8platform/shell 0.4.1 → 0.5.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.
@@ -16,26 +16,30 @@ import {
16
16
  import { IconView, makeRingedIcon } from '../pixi-icon';
17
17
 
18
18
  // ── design constants (mirror the DOM `.ge-bar-panel` / mobile rules) ──────────
19
- const BAR_H = 68; // continuous dark panel height
20
- const SPIN = 84; // hero disc — pops above/below the bar
19
+ const BAR_H = 68; // continuous dark panel height
20
+ const SPIN = 84; // hero disc — pops above/below the bar
21
21
  const SPIN_POP = (SPIN - BAR_H) / 2; // 8 — how far the disc sticks out top/bottom
22
- const MAX_BAR_W = 850; // .ge-shell-bottom max-width
23
- const OUTER_PAD = 14; // .ge-shell-bottom padding
24
- const PANEL_PAD = 14; // .ge-bar-panel padding
25
- const ZONE_GAP = 12; // .ge-zone gap
26
- const ROW_GAP = 10; // BUY BONUS ↔ panel gap
27
- const MID_GAP = 24; // minimum gap between the left info group and the right controls
28
- const BAR_REF_W = 840; // zoom reference width
22
+ const MAX_BAR_W = 850; // .ge-shell-bottom max-width
23
+ const OUTER_PAD = 14; // .ge-shell-bottom padding
24
+ const PANEL_PAD = 14; // .ge-bar-panel padding
25
+ const ZONE_GAP = 12; // .ge-zone gap
26
+ const ROW_GAP = 10; // BUY BONUS ↔ panel gap
27
+ const MID_GAP = 24; // minimum gap between the left info group and the right controls
28
+ const BAR_REF_W = 840; // zoom reference width
29
29
  const BAR_MIN_SCALE = 0.5;
30
30
  const WIDE_PAD_BOTTOM = 8;
31
31
  export const WIDE_BAR_H = WIDE_PAD_BOTTOM + SPIN; // ≈92
32
32
 
33
- const BUY_W = 62; // buy-bonus disc on desktop
34
- const DISC = 38; // white-disc icon button box (auto/turbo)
35
- const FS_BOX = 90; // fixed bet-value box width
33
+ const BUY_W = 62; // buy-bonus disc on desktop
34
+ const DISC = 38; // white-disc icon button box (auto/turbo)
35
+ const FS_BOX = 90; // fixed bet-value box width
36
36
 
37
37
  // mobile
38
- const M_CTRL_H = 62, M_INFO_H = 40, M_GAP = 10, M_PAD_BOTTOM = 8, M_SIDE = 10;
38
+ const M_CTRL_H = 62,
39
+ M_INFO_H = 40,
40
+ M_GAP = 10,
41
+ M_PAD_BOTTOM = 8,
42
+ M_SIDE = 10;
39
43
  const M_BUY = 50;
40
44
  export const MOBILE_BAR_H = M_PAD_BOTTOM + SPIN + M_GAP + M_INFO_H; // spin pops above the controls
41
45
 
@@ -53,7 +57,15 @@ class TurboButton extends Container {
53
57
  private level: number;
54
58
  private accent: string;
55
59
 
56
- constructor(opts: { size: number; glyph: number; discFill: string; discBorder?: number; accent: string; level: number; onTap: () => void }) {
60
+ constructor(opts: {
61
+ size: number;
62
+ glyph: number;
63
+ discFill: string;
64
+ discBorder?: number;
65
+ accent: string;
66
+ level: number;
67
+ onTap: () => void;
68
+ }) {
57
69
  super();
58
70
  this.box = opts.size;
59
71
  this.accent = opts.accent;
@@ -70,7 +82,11 @@ class TurboButton extends Container {
70
82
  this.cursor = 'pointer';
71
83
  this.hitArea = new Rectangle(0, 0, this.box, this.box);
72
84
  this._paint(false);
73
- attachHover(this, () => this._paint(true), () => this._paint(false));
85
+ attachHover(
86
+ this,
87
+ () => this._paint(true),
88
+ () => this._paint(false),
89
+ );
74
90
  attachPress(this, 0.92, () => opts.onTap());
75
91
  }
76
92
 
@@ -99,12 +115,28 @@ class TurboButton extends Container {
99
115
  this._paint(false);
100
116
  }
101
117
 
102
- measureSize(): { w: number; h: number } { return { w: this.box, h: this.box }; }
103
- setLayoutSize(): void { /* fixed */ }
118
+ measureSize(): { w: number; h: number } {
119
+ return { w: this.box, h: this.box };
120
+ }
121
+ setLayoutSize(): void {
122
+ /* fixed */
123
+ }
104
124
  }
105
125
 
106
126
  /** A readout in the bar (white Oswald value, plaque-label caption, no shadow). */
107
- function readout(host: PixiComponentContext, label: string, value: string, opts: { valueSize?: number; align?: 'left' | 'center' | 'right'; fixedWidth?: number; maxWidth?: number; color?: string; muted?: string } = {}): Readout {
127
+ function readout(
128
+ host: PixiComponentContext,
129
+ label: string,
130
+ value: string,
131
+ opts: {
132
+ valueSize?: number;
133
+ align?: 'left' | 'center' | 'right';
134
+ fixedWidth?: number;
135
+ maxWidth?: number;
136
+ color?: string;
137
+ muted?: string;
138
+ } = {},
139
+ ): Readout {
108
140
  return new Readout({
109
141
  label: host.t(label),
110
142
  value,
@@ -151,21 +183,30 @@ export class BottomBar extends Container {
151
183
  private buildWide(): void {
152
184
  const { state, tokens } = this.host;
153
185
  const isBase = state.mode === 'base';
154
- const isFS = state.mode === 'freeSpins';
186
+ const isFS = state.mode === 'freeSpins' || state.mode === 'bonus';
155
187
  const showFsBlocks = isFS || (state.mode === 'replay' && state.freeSpins.total > 0);
156
188
 
157
189
  // BUY BONUS — floats outside-left of the panel (base only)
158
- this.buy = isBase ? this.buildBuy(BUY_W, 13, 3) ?? undefined : undefined;
190
+ this.buy = isBase ? (this.buildBuy(BUY_W, 13, 3) ?? undefined) : undefined;
159
191
 
160
192
  // LEFT info group: menu · balance · (Total win) · (Win)
161
193
  const left = new FlexBox({ direction: 'row', align: 'center', gap: ZONE_GAP });
162
- left.add(new IconButton('menu', { size: 36, glyph: 30, color: '#ffffff', hover: tokens.accent, onTap: () => this.host.actions.openMenu() }));
194
+ left.add(
195
+ new IconButton('menu', {
196
+ size: 36,
197
+ glyph: 30,
198
+ color: '#ffffff',
199
+ hover: tokens.accent,
200
+ onTap: () => this.host.actions.openMenu(),
201
+ }),
202
+ );
163
203
  if (!state.replay) {
164
204
  const bal = readout(this.host, 'Balance', this.host.fmt(state.balance));
165
205
  this.balanceValue = bal.valueText;
166
206
  left.add(bal);
167
207
  }
168
- if (showFsBlocks) left.add(readout(this.host, 'Total win', this.host.fmtWin(state.freeSpins.totalWin)));
208
+ if (showFsBlocks)
209
+ left.add(readout(this.host, 'Total win', this.host.fmtWin(state.freeSpins.totalWin)));
169
210
  if (state.win > 0) {
170
211
  const win = readout(this.host, 'Win', this.host.fmtWin(state.win));
171
212
  this.winValue = win.valueText;
@@ -199,7 +240,9 @@ export class BottomBar extends Container {
199
240
  if (isBase) {
200
241
  bet.eventMode = 'static';
201
242
  bet.cursor = 'pointer';
202
- bet.on('pointertap', () => { if (!this.betLocked()) this.host.actions.openBetPicker(); });
243
+ bet.on('pointertap', () => {
244
+ if (!this.betLocked()) this.host.actions.openBetPicker();
245
+ });
203
246
  }
204
247
 
205
248
  const betGroup = new FlexBox({ direction: 'row', align: 'center', gap: 8 });
@@ -207,8 +250,20 @@ export class BottomBar extends Container {
207
250
  if (isBase) {
208
251
  // plain (borderless) +/- icons, bolder — stacked
209
252
  const step = new FlexBox({ direction: 'column', align: 'center', gap: 2 });
210
- this.betUp = new IconButton('plus', { size: 24, glyph: 22, color: '#ffffff', hover: tokens.accent, onTap: () => this.onBet(1) });
211
- this.betDown = new IconButton('minus', { size: 24, glyph: 22, color: '#ffffff', hover: tokens.accent, onTap: () => this.onBet(-1) });
253
+ this.betUp = new IconButton('plus', {
254
+ size: 24,
255
+ glyph: 22,
256
+ color: '#ffffff',
257
+ hover: tokens.accent,
258
+ onTap: () => this.onBet(1),
259
+ });
260
+ this.betDown = new IconButton('minus', {
261
+ size: 24,
262
+ glyph: 22,
263
+ color: '#ffffff',
264
+ hover: tokens.accent,
265
+ onTap: () => this.onBet(-1),
266
+ });
212
267
  step.add(this.betUp);
213
268
  step.add(this.betDown);
214
269
  betGroup.add(step);
@@ -217,26 +272,52 @@ export class BottomBar extends Container {
217
272
  const spinWrap = new FlexBox({ direction: 'row', align: 'center', gap: 8 });
218
273
  if (isBase && config.features.autoplay) {
219
274
  this.autoBtn = new IconButton('autoplay', {
220
- size: DISC, glyph: 25, disc: tokens.btn, color: tokens.btnInk, hover: tokens.accent, activeColor: tokens.accent,
221
- active: state.autoplay.active, onTap: () => this.onAutoplay(),
275
+ size: DISC,
276
+ glyph: 25,
277
+ disc: tokens.btn,
278
+ color: tokens.btnInk,
279
+ hover: tokens.accent,
280
+ activeColor: tokens.accent,
281
+ active: state.autoplay.active,
282
+ onTap: () => this.onAutoplay(),
222
283
  });
223
284
  if (state.autoplay.active) this.autoBtn.setGlow(true);
224
285
  spinWrap.add(this.autoBtn);
225
286
  }
226
287
  if (isBase) {
227
- this.spin = new SpinDisc({ size: SPIN, glyph: 65, tokens, ticker: this.host.ticker, onSpin: () => this.host.actions.spin(), onStop: () => this.stopAutoplay() });
288
+ this.spin = new SpinDisc({
289
+ size: SPIN,
290
+ glyph: 65,
291
+ tokens,
292
+ ticker: this.host.ticker,
293
+ onSpin: () => this.host.actions.spin(),
294
+ onStop: () => this.stopAutoplay(),
295
+ });
228
296
  if (state.autoplay.active) this.spin.setAutoplay(true, state.autoplay.remaining);
229
297
  if (state.busy) this.spin.setBusy(true);
230
298
  spinWrap.add(this.spin);
231
299
  } else if (showFsBlocks) {
232
300
  const fs = state.freeSpins;
233
- const fsText = fs.current == null ? `${fs.total}` : `${fs.current} / ${fs.total}`;
234
- spinWrap.add(new FsHero({ label: this.host.t('Free spins'), value: fsText, tokens, height: SPIN }));
301
+ const fsText =
302
+ state.bonus?.value ?? (fs.current == null ? `${fs.total}` : `${fs.current} / ${fs.total}`);
303
+ spinWrap.add(
304
+ new FsHero({
305
+ label: this.host.t(state.bonus?.label ?? 'Free spins'),
306
+ value: fsText,
307
+ tokens,
308
+ height: SPIN,
309
+ }),
310
+ );
235
311
  }
236
312
  if (config.features.turbo > 0) {
237
313
  this.turboBtn = new TurboButton({
238
- size: DISC, glyph: 19, discFill: tokens.btn, discBorder: 2, accent: tokens.accent,
239
- level: state.turbo, onTap: () => this.onTurbo(),
314
+ size: DISC,
315
+ glyph: 19,
316
+ discFill: tokens.btn,
317
+ discBorder: 2,
318
+ accent: tokens.accent,
319
+ level: state.turbo,
320
+ onTap: () => this.onTurbo(),
240
321
  });
241
322
  spinWrap.add(this.turboBtn);
242
323
  }
@@ -255,57 +336,124 @@ export class BottomBar extends Container {
255
336
  const feature = state.activeFeature;
256
337
  if (feature) {
257
338
  const accent = effectiveAccent(feature);
258
- return new BuyBonusBadge({ size, fontSize, border, bg: accent, fg: contrastText(accent), label: this.host.t('DISABLE'), tokens, ticker: this.host.ticker, onTap: () => this.host.actions.deactivateFeature() });
339
+ return new BuyBonusBadge({
340
+ size,
341
+ fontSize,
342
+ border,
343
+ bg: accent,
344
+ fg: contrastText(accent),
345
+ label: this.host.t('DISABLE'),
346
+ tokens,
347
+ ticker: this.host.ticker,
348
+ onTap: () => this.host.actions.deactivateFeature(),
349
+ });
259
350
  }
260
- return new BuyBonusBadge({ size, border, bg: tokens.accent, icon: 'ticket', iconSize: size * 0.55, iconColor: tokens.btnInk, label: '', tokens, ticker: this.host.ticker, onTap: () => this.host.actions.openBuyBonus() });
351
+ return new BuyBonusBadge({
352
+ size,
353
+ border,
354
+ bg: tokens.accent,
355
+ icon: 'ticket',
356
+ iconSize: size * 0.55,
357
+ iconColor: tokens.btnInk,
358
+ label: '',
359
+ tokens,
360
+ ticker: this.host.ticker,
361
+ onTap: () => this.host.actions.openBuyBonus(),
362
+ });
261
363
  }
262
364
 
263
365
  // ── mobile / portrait ─────────────────────────────────────────────────────
264
366
  private buildMobile(): void {
265
367
  const { state, config, tokens } = this.host;
266
368
  const isBase = state.mode === 'base';
267
- const isFS = state.mode === 'freeSpins';
369
+ const isFS = state.mode === 'freeSpins' || state.mode === 'bonus';
268
370
  const showFsBlocks = isFS || (state.mode === 'replay' && state.freeSpins.total > 0);
269
371
  const W = this.host.screenW - 2 * M_SIDE;
270
372
 
271
373
  // hero (centre): SPIN in base, FS counter in free spins
272
374
  let hero: Container | undefined;
273
375
  if (isBase) {
274
- this.spin = new SpinDisc({ size: SPIN, glyph: 65, tokens, ticker: this.host.ticker, onSpin: () => this.host.actions.spin(), onStop: () => this.stopAutoplay() });
376
+ this.spin = new SpinDisc({
377
+ size: SPIN,
378
+ glyph: 65,
379
+ tokens,
380
+ ticker: this.host.ticker,
381
+ onSpin: () => this.host.actions.spin(),
382
+ onStop: () => this.stopAutoplay(),
383
+ });
275
384
  if (state.autoplay.active) this.spin.setAutoplay(true, state.autoplay.remaining);
276
385
  if (state.busy) this.spin.setBusy(true);
277
386
  hero = this.spin;
278
387
  } else if (showFsBlocks) {
279
388
  const fs = state.freeSpins;
280
- const fsText = fs.current == null ? `${fs.total}` : `${fs.current} / ${fs.total}`;
281
- hero = new FsHero({ label: this.host.t('Free spins'), value: fsText, tokens, height: SPIN });
389
+ const fsText =
390
+ state.bonus?.value ?? (fs.current == null ? `${fs.total}` : `${fs.current} / ${fs.total}`);
391
+ hero = new FsHero({
392
+ label: this.host.t(state.bonus?.label ?? 'Free spins'),
393
+ value: fsText,
394
+ tokens,
395
+ height: SPIN,
396
+ });
282
397
  }
283
398
 
284
- const menu = new IconButton('menu', { size: 40, glyph: 26, color: '#ffffff', hover: tokens.accent, onTap: () => this.host.actions.openMenu() });
399
+ const menu = new IconButton('menu', {
400
+ size: 40,
401
+ glyph: 26,
402
+ color: '#ffffff',
403
+ hover: tokens.accent,
404
+ onTap: () => this.host.actions.openMenu(),
405
+ });
285
406
  // autoplay is a base-mode control only — hidden in free spins / replay (matches the DOM bar)
286
407
  if (isBase && config.features.autoplay) {
287
- this.autoBtn = new IconButton('autoplay', { size: 40, glyph: 26, color: '#ffffff', hover: tokens.accent, activeColor: tokens.accent, active: state.autoplay.active, onTap: () => this.onAutoplay() });
408
+ this.autoBtn = new IconButton('autoplay', {
409
+ size: 40,
410
+ glyph: 26,
411
+ color: '#ffffff',
412
+ hover: tokens.accent,
413
+ activeColor: tokens.accent,
414
+ active: state.autoplay.active,
415
+ onTap: () => this.onAutoplay(),
416
+ });
288
417
  if (state.autoplay.active) this.autoBtn.setGlow(true);
289
418
  }
290
419
  if (config.features.turbo > 0) {
291
420
  this.turboBtn = new TurboButton({
292
- size: 40, glyph: 22, discFill: tokens.btn, discBorder: 2, accent: tokens.accent,
293
- level: state.turbo, onTap: () => this.onTurbo(),
421
+ size: 40,
422
+ glyph: 22,
423
+ discFill: tokens.btn,
424
+ discBorder: 2,
425
+ accent: tokens.accent,
426
+ level: state.turbo,
427
+ onTap: () => this.onTurbo(),
294
428
  });
295
429
  }
296
- const buy = isBase ? this.buildBuy(M_BUY, 9, 2) ?? undefined : undefined;
430
+ const buy = isBase ? (this.buildBuy(M_BUY, 9, 2) ?? undefined) : undefined;
297
431
 
298
432
  // level 1 — controls bar (dark)
299
- const controls = new FlexBox({ direction: 'row', align: 'center', justify: 'space-between', gap: 0, width: W, height: M_CTRL_H, padding: { left: 18, right: 18 }, background: { fill: tokens.bar, radius: 16 } });
433
+ const controls = new FlexBox({
434
+ direction: 'row',
435
+ align: 'center',
436
+ justify: 'space-between',
437
+ gap: 0,
438
+ width: W,
439
+ height: M_CTRL_H,
440
+ padding: { left: 18, right: 18 },
441
+ background: { fill: tokens.bar, radius: 16 },
442
+ });
300
443
  if (isBase && hero) {
301
444
  const SIDE = 12;
302
445
  const lz = new FlexBox({ direction: 'row', align: 'center', gap: SIDE, justify: 'start' });
303
- lz.add(menu); if (this.autoBtn) lz.add(this.autoBtn);
446
+ lz.add(menu);
447
+ if (this.autoBtn) lz.add(this.autoBtn);
304
448
  const rz = new FlexBox({ direction: 'row', align: 'center', gap: SIDE, justify: 'end' });
305
- if (this.turboBtn) rz.add(this.turboBtn); if (buy) rz.add(buy);
449
+ if (this.turboBtn) rz.add(this.turboBtn);
450
+ if (buy) rz.add(buy);
306
451
  const zw = Math.max(lz.measureSize().w, rz.measureSize().w);
307
- lz.setLayoutSize(zw, undefined); rz.setLayoutSize(zw, undefined);
308
- controls.add(lz); controls.add(hero); controls.add(rz);
452
+ lz.setLayoutSize(zw, undefined);
453
+ rz.setLayoutSize(zw, undefined);
454
+ controls.add(lz);
455
+ controls.add(hero);
456
+ controls.add(rz);
309
457
  } else {
310
458
  controls.add(menu);
311
459
  if (this.autoBtn) controls.add(this.autoBtn);
@@ -316,12 +464,22 @@ export class BottomBar extends Container {
316
464
  // (centred post-scale) instead of widening the row and forcing a whole-bar down-scale. Mirrors
317
465
  // the DOM's shrinkable total-win slot. The floor keeps it readable; below that applyFitMobile
318
466
  // scales the stack as a last resort.
319
- const ICON = 40, PAD = 18, GAPS = 24;
467
+ const ICON = 40,
468
+ PAD = 18,
469
+ GAPS = 24;
320
470
  const heroSized = hero as unknown as { measureSize?: () => { w: number; h: number } };
321
471
  const heroW = hero ? (heroSized.measureSize?.().w ?? hero.getLocalBounds().width) : 0;
322
- const fixed = ICON /* menu */ + (this.autoBtn ? ICON : 0) + heroW + (this.turboBtn ? ICON : 0);
472
+ const fixed =
473
+ ICON /* menu */ + (this.autoBtn ? ICON : 0) + heroW + (this.turboBtn ? ICON : 0);
323
474
  const twSlot = Math.max(60, W - 2 * PAD - fixed - GAPS);
324
- controls.add(readout(this.host, 'Total win', this.host.fmtWin(state.freeSpins.totalWin), { color: '#ffffff', muted: '#ffffff', align: 'center', maxWidth: twSlot }));
475
+ controls.add(
476
+ readout(this.host, 'Total win', this.host.fmtWin(state.freeSpins.totalWin), {
477
+ color: '#ffffff',
478
+ muted: '#ffffff',
479
+ align: 'center',
480
+ maxWidth: twSlot,
481
+ }),
482
+ );
325
483
  }
326
484
  if (this.turboBtn) controls.add(this.turboBtn);
327
485
  }
@@ -332,29 +490,76 @@ export class BottomBar extends Container {
332
490
  const feature = state.activeFeature;
333
491
  const betShown = feature ? state.bet * feature.priceMultiplier : state.bet;
334
492
  if (isBase) {
335
- this.betDown = new IconButton('minus', { size: 26, glyph: 18, color: '#ffffff', hover: tokens.accent, onTap: () => this.onBet(-1) });
493
+ this.betDown = new IconButton('minus', {
494
+ size: 26,
495
+ glyph: 18,
496
+ color: '#ffffff',
497
+ hover: tokens.accent,
498
+ onTap: () => this.onBet(-1),
499
+ });
336
500
  betGroup.add(this.betDown);
337
501
  }
338
- const bet = new Readout({ label: this.host.t('Bet'), value: this.host.fmt(betShown), muted: feature ? effectiveAccent(feature) : tokens.plaqueLabel, fg: feature ? effectiveAccent(feature) : '#ffffff', valueSize: 11, align: 'center', fixedWidth: 76, shadow: false });
502
+ const bet = new Readout({
503
+ label: this.host.t('Bet'),
504
+ value: this.host.fmt(betShown),
505
+ muted: feature ? effectiveAccent(feature) : tokens.plaqueLabel,
506
+ fg: feature ? effectiveAccent(feature) : '#ffffff',
507
+ valueSize: 11,
508
+ align: 'center',
509
+ fixedWidth: 76,
510
+ shadow: false,
511
+ });
339
512
  this.betReadout = bet;
340
- if (isBase) { bet.eventMode = 'static'; bet.cursor = 'pointer'; bet.on('pointertap', () => { if (!this.betLocked()) this.host.actions.openBetPicker(); }); }
513
+ if (isBase) {
514
+ bet.eventMode = 'static';
515
+ bet.cursor = 'pointer';
516
+ bet.on('pointertap', () => {
517
+ if (!this.betLocked()) this.host.actions.openBetPicker();
518
+ });
519
+ }
341
520
  betGroup.add(bet);
342
- if (isBase) { this.betUp = new IconButton('plus', { size: 26, glyph: 18, color: '#ffffff', hover: tokens.accent, onTap: () => this.onBet(1) }); betGroup.add(this.betUp); }
521
+ if (isBase) {
522
+ this.betUp = new IconButton('plus', {
523
+ size: 26,
524
+ glyph: 18,
525
+ color: '#ffffff',
526
+ hover: tokens.accent,
527
+ onTap: () => this.onBet(1),
528
+ });
529
+ betGroup.add(this.betUp);
530
+ }
343
531
  betGroup.layout();
344
532
 
345
533
  const innerW = W - 28; // pad 14 each side
346
534
  const slot = Math.max(40, (innerW - betGroup.outerWidth - 20) / 2);
347
- const info = new FlexBox({ direction: 'row', align: 'center', justify: 'space-between', width: W, height: M_INFO_H, padding: { left: 14, right: 14 }, gap: 10, background: { fill: tokens.plaqueGlass, radius: 12 } });
535
+ const info = new FlexBox({
536
+ direction: 'row',
537
+ align: 'center',
538
+ justify: 'space-between',
539
+ width: W,
540
+ height: M_INFO_H,
541
+ padding: { left: 14, right: 14 },
542
+ gap: 10,
543
+ background: { fill: tokens.plaqueGlass, radius: 12 },
544
+ });
348
545
  // replay is a read-only historical round — no real balance to show (sticky `replay`, so it stays
349
546
  // hidden through a replay's free-spins phase too); matches the DOM bar.
350
547
  if (!state.replay) {
351
- const bal = readout(this.host, 'Balance', this.host.fmt(state.balance), { valueSize: 11, align: 'left', fixedWidth: slot });
548
+ const bal = readout(this.host, 'Balance', this.host.fmt(state.balance), {
549
+ valueSize: 11,
550
+ align: 'left',
551
+ fixedWidth: slot,
552
+ });
352
553
  this.balanceValue = bal.valueText;
353
554
  info.add(bal);
354
555
  }
355
556
  info.add(betGroup);
356
557
  // WIN always present (no jiggle on win↔0)
357
- const win = readout(this.host, 'Win', this.host.fmtWin(state.win), { valueSize: 11, align: 'right', fixedWidth: slot });
558
+ const win = readout(this.host, 'Win', this.host.fmtWin(state.win), {
559
+ valueSize: 11,
560
+ align: 'right',
561
+ fixedWidth: slot,
562
+ });
358
563
  this.winValue = win.valueText;
359
564
  info.add(win);
360
565
  info.layout();
@@ -399,7 +604,10 @@ export class BottomBar extends Container {
399
604
  if (this.spin) this.spin.disabled = state.busy && !auto;
400
605
  if (this.autoBtn) this.autoBtn.disabled = state.busy && !auto;
401
606
  if (this.buy) this.buy.disabled = state.busy || auto || !state.buyBonusEnabled;
402
- if (this.betReadout && lockBet) { this.betReadout.eventMode = 'none'; this.betReadout.cursor = 'default'; }
607
+ if (this.betReadout && lockBet) {
608
+ this.betReadout.eventMode = 'none';
609
+ this.betReadout.cursor = 'default';
610
+ }
403
611
  }
404
612
 
405
613
  // ── height / fit ──────────────────────────────────────────────────────────
@@ -414,9 +622,13 @@ export class BottomBar extends Container {
414
622
  }
415
623
 
416
624
  applyFit(): void {
417
- if (this.host.layout === 'mobile') { this.applyFitMobile(); return; }
625
+ if (this.host.layout === 'mobile') {
626
+ this.applyFitMobile();
627
+ return;
628
+ }
418
629
  const { screenW: W, screenH: H, tokens } = this.host;
419
- const left = this.leftZone!, right = this.rightZone!;
630
+ const left = this.leftZone!,
631
+ right = this.rightZone!;
420
632
  const buyW = this.buy ? BUY_W + ROW_GAP : 0;
421
633
 
422
634
  // content area = buy + panel; panel holds left (hard-left) and right (hard-right) with ≥MID_GAP.
@@ -436,16 +648,20 @@ export class BottomBar extends Container {
436
648
 
437
649
  if (this.buy) this.buy.position.set(OUTER_PAD, panelCenterY - BUY_W / 2);
438
650
  left.position.set(panelX + PANEL_PAD, panelCenterY - left.outerHeight / 2);
439
- right.position.set(panelRight - PANEL_PAD - right.outerWidth, panelCenterY - right.outerHeight / 2);
651
+ right.position.set(
652
+ panelRight - PANEL_PAD - right.outerWidth,
653
+ panelCenterY - right.outerHeight / 2,
654
+ );
440
655
 
441
656
  this.inner.scale.set(s);
442
- this.inner.position.set((W - barW * s) / 2, (H - WIDE_PAD_BOTTOM) - SPIN * s);
657
+ this.inner.position.set((W - barW * s) / 2, H - WIDE_PAD_BOTTOM - SPIN * s);
443
658
  this.measureFootprint();
444
659
  }
445
660
 
446
661
  private applyFitMobile(): void {
447
662
  const { screenW: W, screenH: H } = this.host;
448
- const controls = this.mobileControls!, info = this.mobileInfo!;
663
+ const controls = this.mobileControls!,
664
+ info = this.mobileInfo!;
449
665
  const pop = this.mobileHeroPop; // how far the hero sticks above the controls bar
450
666
  // vertical stack: [controls (hero pops `pop` above)] gap [info]
451
667
  const topPad = pop;