@energy8platform/shell 0.6.3 → 0.6.5

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.3",
3
+ "version": "0.6.5",
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",
package/src/core/i18n.ts CHANGED
@@ -26,11 +26,18 @@ export interface I18nOptions { language: string; isSocial?: boolean; messages?:
26
26
  export interface I18n { readonly lang: Lang; t(src: string): string; }
27
27
 
28
28
  export function createI18n(opts: I18nOptions): I18n {
29
- // Social is the `en-social` pseudo-locale: it rewrites the English source. Non-English locales are
30
- // authored social-safe already, so `socialize` only runs on the English source string.
29
+ // Social is the `en-social` pseudo-locale: it rewrites the English source into social-safe
30
+ // vocabulary. The replacement dictionary only exists for English, so social mode FORCES English
31
+ // the requested `language` is ignored, matching the `isSocial` contract ("…derived from English…
32
+ // regardless of `language`"). Rendering a non-English locale in social mode would leak untranslated
33
+ // restricted terms, so we never do it.
34
+ if (opts.isSocial) {
35
+ const t = (src: string): string => socialize(src);
36
+ return { lang: 'en', t };
37
+ }
31
38
  const lang = normalizeLang(opts.language);
32
39
  const t = (src: string): string => {
33
- if (lang === 'en') return opts.isSocial ? socialize(src) : src;
40
+ if (lang === 'en') return src;
34
41
  return opts.messages?.[lang]?.[src] ?? LOCALES[lang]?.[src] ?? src;
35
42
  };
36
43
  return { lang, t };
@@ -86,6 +86,11 @@ export interface ShellHost {
86
86
  setVolumeRefresh(fn: ((key: VolumeKey, value: number) => void) | null): void;
87
87
  /** Logic-bearing actions invoked by renderer controls. */
88
88
  readonly actions: ShellActions;
89
+ /** Re-show the replay summary modal through the controller (keeps its OverlayHandle in sync).
90
+ * Used as the modal's own reopen callback after START REPLAY, so a renderer never re-pushes a
91
+ * replay modal behind the controller's back (which would strand `overlay` and dead-end the
92
+ * next close). */
93
+ openReplay(opts: ReplayModalOptions): void;
89
94
  }
90
95
 
91
96
  /** 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.3';
3
+ export const PACKAGE_VERSION = '0.6.5';
@@ -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;
@@ -20,7 +20,6 @@ import { openBuyBonus } from './components/BuyBonus';
20
20
  import { openBetPicker, openAutoplayPicker } from './components/pickers';
21
21
  import { buildModal } from './components/Modal';
22
22
  import { buildReplayModal } from './components/ReplayModal';
23
- import type { ReplayModalOptions } from '@/core/types';
24
23
 
25
24
  export interface PixiRendererOptions {
26
25
  app: Application;
@@ -147,7 +146,7 @@ export class PixiRenderer implements ShellRenderer {
147
146
  layer = openAutoplayPicker(this.ctx);
148
147
  break;
149
148
  case 'replay':
150
- layer = buildReplayModal(this.ctx, req.opts, () => this.openReplayInternal(req.opts));
149
+ layer = buildReplayModal(this.ctx, req.opts, () => this.host.openReplay(req.opts));
151
150
  break;
152
151
  case 'modal':
153
152
  layer = buildModal(this.ctx, req.opts);
@@ -226,15 +225,6 @@ export class PixiRenderer implements ShellRenderer {
226
225
  this.currentLayer?.fit?.();
227
226
  }
228
227
 
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
228
  // ── frosted backdrop ─────────────────────────────────────────────────────────
239
229
  /** Snapshot the scene behind the modal layer and blur it — the Pixi analogue of the overlay's
240
230
  * `backdrop-filter: blur(20px) saturate(120%)`. Static (captured at open time): the game is paused
@@ -333,6 +323,7 @@ export class PixiRenderer implements ShellRenderer {
333
323
  get layout() { return host.layout; },
334
324
  get soundOn() { return host.soundOn; },
335
325
  get actions() { return host.actions; },
326
+ openReplay: (opts) => host.openReplay(opts),
336
327
  t: (s) => host.t(s),
337
328
  formatCurrency: (n, win) => host.formatCurrency(n, win),
338
329
  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) {