@energy8platform/shell 0.11.0 → 0.11.2

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.11.0",
3
+ "version": "0.11.2",
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",
@@ -1,4 +1,5 @@
1
1
  import type { ShellState } from './types';
2
+ import { bonusBuyLocked } from './state';
2
3
 
3
4
  export interface KeyboardHost {
4
5
  readonly state: ShellState;
@@ -153,7 +154,12 @@ export class KeyboardController {
153
154
  if (h.turboLevels > 0 && !s.replay) { h.cycleTurbo(); return; }
154
155
  break;
155
156
  case 'KeyB':
156
- if (h.buyBonusEnabled && s.mode === 'base' && !s.replay) { h.openBuyBonus(); return; }
157
+ // `bonusBuyLocked` is the same predicate the bar's coin uses. Without it this hotkey
158
+ // reached past a disabled coin and opened the overlay mid-round.
159
+ if (h.buyBonusEnabled && s.mode === 'base' && !s.replay && !bonusBuyLocked(s)) {
160
+ h.openBuyBonus();
161
+ return;
162
+ }
157
163
  break;
158
164
  case 'KeyI':
159
165
  h.openInfo(); return;
@@ -867,7 +867,7 @@ const BASE_LOCALES: Partial<Record<Lang, Record<string, string>>> = {
867
867
  // Kept in a separate block (grouped per language, not interleaved) because these are long legal
868
868
  // sentences, MACHINE-TRANSLATED and pending native QA. The keys are the exact English source lines
869
869
  // the Stake bridge emits (see @energy8platform/stake-bridge `buildDisclaimer`). The copyright/brand
870
- // line ("TM and © {year} Stake Engine.") is deliberately omitted — it renders verbatim.
870
+ // line ("TM and © {year} Engine.") is deliberately omitted — it renders verbatim.
871
871
  const D = {
872
872
  L1: 'Malfunction voids all wins and plays.',
873
873
  L2: 'A consistent internet connection is required. In the event of a disconnection, reload the game to finish any uncompleted rounds.',
package/src/core/state.ts CHANGED
@@ -41,3 +41,18 @@ export function nextTurbo(current: number, maxLevels: number): number {
41
41
  if (maxLevels <= 0) return 0;
42
42
  return current >= maxLevels ? 0 : current + 1;
43
43
  }
44
+
45
+ /**
46
+ * Is a bonus buy unavailable right now?
47
+ *
48
+ * The three RUNTIME locks, in one place because they used to be in three: both bottom bars spelled
49
+ * them out for the coin, and the Shift+B hotkey spelled out a different, shorter set — so the
50
+ * keyboard opened the buy-bonus overlay mid-round and let a player stake a second bet on top of a
51
+ * round already in flight. A predicate the bars and the hotkey share cannot drift apart again.
52
+ *
53
+ * Runtime only. Whether the feature EXISTS at all is a config question (`features.buyBonus`), and
54
+ * whether this particular surface should offer it (mode, replay) belongs to the caller.
55
+ */
56
+ export function bonusBuyLocked(s: ShellState): boolean {
57
+ return s.busy || s.autoplay.active || !s.buyBonusEnabled;
58
+ }
@@ -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.11.0';
3
+ export const PACKAGE_VERSION = '0.11.2';
@@ -2,6 +2,7 @@ import type { ShellHost } from '@/core/renderer';
2
2
  import { effectiveAccent } from '@/core/colors';
3
3
  import { icon, type IconName } from '../icons';
4
4
  import { BUY_BONUS_ART, BUY_BONUS_SOCIAL_ART, BUY_BONUS_DISABLED_ART } from '../../buy-bonus-art';
5
+ import { bonusBuyLocked } from '@/core/state';
5
6
 
6
7
  /** A floating labelled money readout (balance/win/bet). */
7
8
  function readout(ge: string, label: string, value: string): HTMLElement {
@@ -295,5 +296,5 @@ function applyBusy(host: ShellHost, bar: HTMLElement): void {
295
296
  if (betVal) betVal.classList.toggle('ge-disabled', lockBet);
296
297
  const buy = bar.querySelector('[data-ge="buybonus"]') as HTMLButtonElement | null;
297
298
  // disabled for the whole autoplay run (not just per-spin busy) so it doesn't flicker/pulse
298
- if (buy) buy.disabled = busy || auto || !host.state.buyBonusEnabled;
299
+ if (buy) buy.disabled = bonusBuyLocked(host.state);
299
300
  }
@@ -15,6 +15,7 @@ import {
15
15
  } from '../primitives/widgets';
16
16
  import type { SpinAutoplayMode } from '../primitives/widgets';
17
17
  import type { IconName } from '../icons';
18
+ import { bonusBuyLocked } from '@/core/state';
18
19
 
19
20
  // ── design constants (mirror the DOM `.ge-bar-panel` / mobile rules) ──────────
20
21
  /** Which of the disc's three faces the autoplay state calls for — see `SpinAutoplayMode`. A run
@@ -425,7 +426,12 @@ export class BottomBar extends Container {
425
426
  if (config.features.turbo > 0) {
426
427
  this.turboBtn = makeTurboButton(this.host, state.turbo, () => this.onTurbo(), 40, 22);
427
428
  }
428
- const buy = isBase ? (this.buildBuy(M_BUY, 9, 2) ?? undefined) : undefined;
429
+ // On `this`, not a local: applyBusy() guards every line with `if (this.<control>)`, so a coin
430
+ // kept in a local is drawn, laid out, tappable — and unreachable by every lock there is.
431
+ // That is exactly how a phone player could tap SPIN and open buy-bonus on the round in
432
+ // flight. (Safe for layout: applyFit() hands mobile to applyFitMobile() before it touches
433
+ // `this.buy` for the wide bar's positioning.)
434
+ this.buy = isBase ? (this.buildBuy(M_BUY, 9, 2) ?? undefined) : undefined;
429
435
 
430
436
  // level 1 — controls bar (dark)
431
437
  const controls = new FlexBox({
@@ -445,7 +451,7 @@ export class BottomBar extends Container {
445
451
  if (this.autoBtn) lz.add(this.autoBtn);
446
452
  const rz = new FlexBox({ direction: 'row', align: 'center', gap: SIDE, justify: 'end' });
447
453
  if (this.turboBtn) rz.add(this.turboBtn);
448
- if (buy) rz.add(buy);
454
+ if (this.buy) rz.add(this.buy);
449
455
  const zw = Math.max(lz.measureSize().w, rz.measureSize().w);
450
456
  lz.setLayoutSize(zw, undefined);
451
457
  rz.setLayoutSize(zw, undefined);
@@ -608,7 +614,7 @@ export class BottomBar extends Container {
608
614
  if (this.betDown) this.betDown.disabled = lockBet || i <= 0;
609
615
  if (this.spin) this.spin.disabled = state.busy && !auto;
610
616
  if (this.autoBtn) this.autoBtn.disabled = state.busy && !auto;
611
- if (this.buy) this.buy.disabled = state.busy || auto || !state.buyBonusEnabled;
617
+ if (this.buy) this.buy.disabled = bonusBuyLocked(state);
612
618
  if (this.betReadout && lockBet) {
613
619
  this.betReadout.eventMode = 'none';
614
620
  this.betReadout.cursor = 'default';