@energy8platform/shell 0.6.4 → 0.6.6

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.6.4",
3
+ "version": "0.6.6",
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",
@@ -304,9 +304,9 @@ export class ShellController extends EventEmitter<ShellEvents> implements ShellH
304
304
  }
305
305
 
306
306
  // ── game-facing public API (mirrors GameShell/PixiGameShell) ───────────────────
307
- private money(field: 'balance' | 'win', from: number, to: number): void {
307
+ private money(field: 'balance' | 'win', from: number, to: number, durationMs?: number): void {
308
308
  this.renderer.renderBar();
309
- if (to !== from) this.renderer.animateMoney(field, from, to);
309
+ if (to !== from) this.renderer.animateMoney(field, from, to, durationMs);
310
310
  }
311
311
  setBalance(n: number): void {
312
312
  const from = this.prevBalance;
@@ -316,8 +316,10 @@ export class ShellController extends EventEmitter<ShellEvents> implements ShellH
316
316
  }
317
317
  /** Set the WIN readout. Counts up/down from the previous value by default. Pass
318
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 {
319
+ * host to clear WIN to 0 at spin start, where an animated count-DOWN would look wrong.
320
+ * `{ durationMs }` shortens/lengthens the count-up (default 450ms) a scene reporting a win per
321
+ * cascade step passes its step length so each count-up finishes before the next step lands. */
322
+ setWin(n: number, opts?: { animate?: boolean; durationMs?: number }): void {
321
323
  const from = this.prevWin;
322
324
  this.state.win = n;
323
325
  this.prevWin = n;
@@ -325,7 +327,7 @@ export class ShellController extends EventEmitter<ShellEvents> implements ShellH
325
327
  this.renderer.renderBar(); // instant repaint from state; cancels running money anims
326
328
  return;
327
329
  }
328
- this.money('win', from, n);
330
+ this.money('win', from, n, opts?.durationMs);
329
331
  }
330
332
  setBet(n: number): void {
331
333
  this.state.bet = n;
@@ -18,8 +18,10 @@ export interface ShellRenderer {
18
18
  setLayout(layout: ShellLayoutMode): void;
19
19
  /** Apply colour tokens (CSS vars in DOM / repaint in Pixi). */
20
20
  applyTheme(tokens: ShellTokens): void;
21
- /** Count a money readout from→to on the freshly-rendered bar (DOM rAF / Pixi ticker). */
22
- animateMoney(field: 'balance' | 'win', from: number, to: number): void;
21
+ /** Count a money readout from→to on the freshly-rendered bar (DOM rAF / Pixi ticker).
22
+ * `durationMs` overrides the renderer's default count-up length a cascade/tumble scene
23
+ * reporting a win per step needs each count-up to fit inside its step. */
24
+ animateMoney(field: 'balance' | 'win', from: number, to: number, durationMs?: number): void;
23
25
  /** Build + show an overlay from a controller-supplied model; return a handle for key routing
24
26
  * and programmatic close. Returns void when nothing was shown. */
25
27
  openOverlay(req: OverlayRequest): OverlayHandle | void;
@@ -86,6 +88,11 @@ export interface ShellHost {
86
88
  setVolumeRefresh(fn: ((key: VolumeKey, value: number) => void) | null): void;
87
89
  /** Logic-bearing actions invoked by renderer controls. */
88
90
  readonly actions: ShellActions;
91
+ /** Re-show the replay summary modal through the controller (keeps its OverlayHandle in sync).
92
+ * Used as the modal's own reopen callback after START REPLAY, so a renderer never re-pushes a
93
+ * replay modal behind the controller's back (which would strand `overlay` and dead-end the
94
+ * next close). */
95
+ openReplay(opts: ReplayModalOptions): void;
89
96
  }
90
97
 
91
98
  /** Every state-changing thing a control can do. Each runs logic in the controller, emits the
@@ -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.6.4';
3
+ export const PACKAGE_VERSION = '0.6.6';
@@ -19,10 +19,10 @@ const REMOVE_FADE_MS = 300;
19
19
 
20
20
  /** Count-up the value span (.ge-rd-val) of a readout, leaving its label untouched.
21
21
  * Returns the count-up canceler so the renderer can stop it before the node is replaced. */
22
- function animateReadout(el: HTMLElement, from: number, to: number, fmt: (n: number) => string): () => void {
22
+ function animateReadout(el: HTMLElement, from: number, to: number, fmt: (n: number) => string, durationMs?: number): () => void {
23
23
  const val = el.querySelector('.ge-rd-val') as HTMLElement | null;
24
24
  if (!val) { el.textContent = fmt(to); return () => {}; }
25
- return countUp(val, from, to, fmt);
25
+ return durationMs == null ? countUp(val, from, to, fmt) : countUp(val, from, to, fmt, durationMs);
26
26
  }
27
27
 
28
28
  export class HtmlRenderer implements ShellRenderer {
@@ -87,10 +87,10 @@ export class HtmlRenderer implements ShellRenderer {
87
87
  this.renderBar();
88
88
  }
89
89
 
90
- animateMoney(field: 'balance' | 'win', from: number, to: number): void {
90
+ animateMoney(field: 'balance' | 'win', from: number, to: number, durationMs?: number): void {
91
91
  const fmt = (n: number) => this.host.formatCurrency(n, field === 'win');
92
92
  const el = this.barHost.querySelector(`[data-ge="${field}"]`) as HTMLElement | null;
93
- if (el) this.moneyAnims.push(animateReadout(el, from, to, fmt));
93
+ if (el) this.moneyAnims.push(animateReadout(el, from, to, fmt, durationMs));
94
94
  }
95
95
 
96
96
  openOverlay(req: OverlayRequest): OverlayHandle | void {
@@ -162,7 +162,9 @@ export class HtmlRenderer implements ShellRenderer {
162
162
  }
163
163
  case 'replay': {
164
164
  const opts = req.opts;
165
- const reopen = (): void => { this.openReplayInternal(opts); };
165
+ // Reopen through the controller (not a renderer-direct re-push) so its OverlayHandle stays
166
+ // in sync — otherwise a reopened replay modal is untracked and the next close no-ops.
167
+ const reopen = (): void => { this.host.openReplay(opts); };
166
168
  const root = buildReplayModal(this.host, opts, reopen);
167
169
  return { root };
168
170
  }
@@ -173,14 +175,6 @@ export class HtmlRenderer implements ShellRenderer {
173
175
  }
174
176
  }
175
177
 
176
- /** Opens a replay modal and shows it — used as the reopen callback after START REPLAY. */
177
- private openReplayInternal(opts: import('@/core/types').ReplayModalOptions): void {
178
- if (this.destroyed) return;
179
- const reopen = (): void => { this.openReplayInternal(opts); };
180
- const root = buildReplayModal(this.host, opts, reopen);
181
- this.showModal(root);
182
- }
183
-
184
178
  private showModal(el: HTMLElement, onKey?: (e: KeyboardEvent) => boolean): void {
185
179
  // Drop focus from any open shell control so a stray Space/Enter doesn't re-activate it
186
180
  const active = document.activeElement as HTMLElement | null;
@@ -6,6 +6,7 @@ import {
6
6
  RenderTexture,
7
7
  Sprite,
8
8
  type Application,
9
+ type Text,
9
10
  type Ticker,
10
11
  } from 'pixi.js';
11
12
  import type { ShellRenderer, ShellHost, OverlayRequest, OverlayHandle } from '@/core/renderer';
@@ -20,7 +21,6 @@ import { openBuyBonus } from './components/BuyBonus';
20
21
  import { openBetPicker, openAutoplayPicker } from './components/pickers';
21
22
  import { buildModal } from './components/Modal';
22
23
  import { buildReplayModal } from './components/ReplayModal';
23
- import type { ReplayModalOptions } from '@/core/types';
24
24
 
25
25
  export interface PixiRendererOptions {
26
26
  app: Application;
@@ -110,16 +110,17 @@ export class PixiRenderer implements ShellRenderer {
110
110
 
111
111
  /** Count a money readout from→to on the freshly-rendered bar's value Text node. Mirrors
112
112
  * PixiGameShell.animateMoney (which counted on the just-built bar's value Texts). */
113
- animateMoney(field: 'balance' | 'win', from: number, to: number): void {
113
+ animateMoney(field: 'balance' | 'win', from: number, to: number, durationMs?: number): void {
114
114
  if (!this.bar) return;
115
+ // countUpText defaults durationMs to 450 — pass it only when the caller overrode it.
116
+ const count = (text: Text, fmt: (n: number) => string) =>
117
+ durationMs == null
118
+ ? countUpText(this.ticker, text, from, to, fmt)
119
+ : countUpText(this.ticker, text, from, to, fmt, durationMs);
115
120
  if (field === 'balance' && this.bar.balanceValue) {
116
- this.moneyAnims.push(
117
- countUpText(this.ticker, this.bar.balanceValue, from, to, (n) => this.ctx.fmt(n)),
118
- );
121
+ this.moneyAnims.push(count(this.bar.balanceValue, (n) => this.ctx.fmt(n)));
119
122
  } else if (field === 'win' && this.bar.winValue) {
120
- this.moneyAnims.push(
121
- countUpText(this.ticker, this.bar.winValue, from, to, (n) => this.ctx.fmtWin(n)),
122
- );
123
+ this.moneyAnims.push(count(this.bar.winValue, (n) => this.ctx.fmtWin(n)));
123
124
  }
124
125
  }
125
126
 
@@ -147,7 +148,7 @@ export class PixiRenderer implements ShellRenderer {
147
148
  layer = openAutoplayPicker(this.ctx);
148
149
  break;
149
150
  case 'replay':
150
- layer = buildReplayModal(this.ctx, req.opts, () => this.openReplayInternal(req.opts));
151
+ layer = buildReplayModal(this.ctx, req.opts, () => this.host.openReplay(req.opts));
151
152
  break;
152
153
  case 'modal':
153
154
  layer = buildModal(this.ctx, req.opts);
@@ -226,15 +227,6 @@ export class PixiRenderer implements ShellRenderer {
226
227
  this.currentLayer?.fit?.();
227
228
  }
228
229
 
229
- /** Replay's reopen path: a replay modal can rebuild itself (e.g. after a replay completes). It is
230
- * routed back through the controller-equivalent open flow so the layer stack + backdrop stay in
231
- * sync — mirrors PixiGameShell.openReplay (push a fresh replay modal). */
232
- private openReplayInternal(opts: ReplayModalOptions): void {
233
- if (this.destroyed) return;
234
- const layer = buildReplayModal(this.ctx, opts, () => this.openReplayInternal(opts));
235
- this.pushLayer(layer);
236
- }
237
-
238
230
  // ── frosted backdrop ─────────────────────────────────────────────────────────
239
231
  /** Snapshot the scene behind the modal layer and blur it — the Pixi analogue of the overlay's
240
232
  * `backdrop-filter: blur(20px) saturate(120%)`. Static (captured at open time): the game is paused
@@ -333,6 +325,7 @@ export class PixiRenderer implements ShellRenderer {
333
325
  get layout() { return host.layout; },
334
326
  get soundOn() { return host.soundOn; },
335
327
  get actions() { return host.actions; },
328
+ openReplay: (opts) => host.openReplay(opts),
336
329
  t: (s) => host.t(s),
337
330
  formatCurrency: (n, win) => host.formatCurrency(n, win),
338
331
  emit: host.emit.bind(host),
@@ -103,6 +103,11 @@ export function makeText(str: string, opts: TextOpts): Text {
103
103
  if (opts.wrapWidth != null) {
104
104
  style.wordWrap = true;
105
105
  style.wordWrapWidth = opts.wrapWidth;
106
+ // CJK (and any long unbroken run) has no spaces to wrap on — without breakWords the line
107
+ // overflows the wrap width instead of wrapping. This is the single shell text helper (How to
108
+ // Play, disclaimer, Game Info section titles, mode table, win messages, buy-bonus cards), so
109
+ // one flag fixes wrapping everywhere.
110
+ style.breakWords = true;
106
111
  }
107
112
  if (opts.lineHeight != null) style.lineHeight = opts.lineHeight;
108
113
  if (opts.shadow) {