@energy8platform/shell 0.2.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 +3894 -0
- package/dist/html.cjs.js.map +1 -0
- package/dist/html.d.ts +616 -0
- package/dist/html.esm.js +3879 -0
- package/dist/html.esm.js.map +1 -0
- package/dist/index.cjs.js +1593 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +554 -0
- package/dist/index.esm.js +1582 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/pixi.cjs.js +5740 -0
- package/dist/pixi.cjs.js.map +1 -0
- package/dist/pixi.d.ts +682 -0
- package/dist/pixi.esm.js +5726 -0
- package/dist/pixi.esm.js.map +1 -0
- package/package.json +79 -0
- package/src/core/EventEmitter.ts +55 -0
- package/src/core/ShellController.ts +234 -0
- package/src/core/colors.ts +32 -0
- package/src/core/fonts-digits.ts +12 -0
- package/src/core/fonts.ts +13 -0
- package/src/core/format.ts +39 -0
- package/src/core/i18n.ts +96 -0
- package/src/core/index.ts +35 -0
- package/src/core/keyboard.ts +229 -0
- package/src/core/locales.ts +864 -0
- package/src/core/motion.ts +10 -0
- package/src/core/renderer.ts +121 -0
- package/src/core/state.ts +31 -0
- package/src/core/theme.ts +81 -0
- package/src/core/types.ts +269 -0
- package/src/core/version.ts +3 -0
- package/src/ui/html/HtmlRenderer.ts +292 -0
- package/src/ui/html/components/BottomBar.ts +240 -0
- package/src/ui/html/components/BuyBonus.ts +326 -0
- package/src/ui/html/components/GameInfo.ts +384 -0
- package/src/ui/html/components/Modal.ts +36 -0
- package/src/ui/html/components/ReplayModal.ts +58 -0
- package/src/ui/html/components/Settings.ts +59 -0
- package/src/ui/html/components/pickers.ts +146 -0
- package/src/ui/html/icons-preview.svg +224 -0
- package/src/ui/html/icons.ts +31 -0
- package/src/ui/html/index.ts +36 -0
- package/src/ui/html/motion-dom.ts +29 -0
- package/src/ui/html/primitives.ts +85 -0
- package/src/ui/html/shell.css.ts +528 -0
- package/src/ui/html/theme-css.ts +21 -0
- package/src/ui/pixi/PixiRenderer.ts +350 -0
- package/src/ui/pixi/components/BottomBar.ts +477 -0
- package/src/ui/pixi/components/BuyBonus.ts +763 -0
- package/src/ui/pixi/components/GameInfo.ts +559 -0
- package/src/ui/pixi/components/Modal.ts +40 -0
- package/src/ui/pixi/components/ReplayModal.ts +80 -0
- package/src/ui/pixi/components/Settings.ts +117 -0
- package/src/ui/pixi/components/pickers.ts +170 -0
- package/src/ui/pixi/context.ts +52 -0
- package/src/ui/pixi/icons.ts +45 -0
- package/src/ui/pixi/index.ts +56 -0
- package/src/ui/pixi/motion-pixi.ts +58 -0
- package/src/ui/pixi/pixi-icon.ts +119 -0
- package/src/ui/pixi/primitives/card.ts +227 -0
- package/src/ui/pixi/primitives/controls.ts +243 -0
- package/src/ui/pixi/primitives/flex.ts +335 -0
- package/src/ui/pixi/primitives/overlay.ts +181 -0
- package/src/ui/pixi/primitives/scroll.ts +117 -0
- package/src/ui/pixi/primitives/widgets.ts +680 -0
- package/src/ui/pixi/text.ts +131 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BlurFilter,
|
|
3
|
+
ColorMatrixFilter,
|
|
4
|
+
Container,
|
|
5
|
+
Graphics,
|
|
6
|
+
RenderTexture,
|
|
7
|
+
Sprite,
|
|
8
|
+
type Application,
|
|
9
|
+
type Ticker,
|
|
10
|
+
} from 'pixi.js';
|
|
11
|
+
import type { ShellRenderer, ShellHost, OverlayRequest, OverlayHandle } from '@/core/renderer';
|
|
12
|
+
import type { ShellTokens } from '@/core/theme';
|
|
13
|
+
import type { PixiComponentContext, ShellLayer, LayerHandle } from './context';
|
|
14
|
+
import { installShellFont, whenFontReady } from './text';
|
|
15
|
+
import { countUpText, tween } from './motion-pixi';
|
|
16
|
+
import { BottomBar } from './components/BottomBar';
|
|
17
|
+
import { openSettings } from './components/Settings';
|
|
18
|
+
import { openGameInfo } from './components/GameInfo';
|
|
19
|
+
import { openBuyBonus } from './components/BuyBonus';
|
|
20
|
+
import { openBetPicker, openAutoplayPicker } from './components/pickers';
|
|
21
|
+
import { buildModal } from './components/Modal';
|
|
22
|
+
import { buildReplayModal } from './components/ReplayModal';
|
|
23
|
+
import type { ReplayModalOptions } from '@/core/types';
|
|
24
|
+
|
|
25
|
+
export interface PixiRendererOptions {
|
|
26
|
+
app: Application;
|
|
27
|
+
parent?: Container;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The Pixi VIEW half of the shell — the renderer the controller drives. Owns the root/bar/modal
|
|
31
|
+
* containers, builds the BottomBar + overlays, runs money count-ups + the frosted backdrop, and
|
|
32
|
+
* reports its surface size back to the controller. All the keyboard / pull-focus / overlay-flow
|
|
33
|
+
* logic lives in the ShellController; this class is pure Pixi rendering — a 1:1 port of the
|
|
34
|
+
* PixiGameShell rendering methods. */
|
|
35
|
+
export class PixiRenderer implements ShellRenderer {
|
|
36
|
+
private app: Application;
|
|
37
|
+
private parent?: Container;
|
|
38
|
+
private host!: ShellHost;
|
|
39
|
+
private ctx!: PixiComponentContext;
|
|
40
|
+
|
|
41
|
+
private root = new Container();
|
|
42
|
+
private barLayer = new Container();
|
|
43
|
+
private modalLayer = new Container();
|
|
44
|
+
private bar?: BottomBar;
|
|
45
|
+
private currentLayer: ShellLayer | null = null;
|
|
46
|
+
private backdrop?: { node: Container; texture: RenderTexture };
|
|
47
|
+
private moneyAnims: Array<() => void> = [];
|
|
48
|
+
private destroyed = false;
|
|
49
|
+
|
|
50
|
+
constructor(opts: PixiRendererOptions) {
|
|
51
|
+
this.app = opts.app;
|
|
52
|
+
this.parent = opts.parent;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private get ticker(): Ticker {
|
|
56
|
+
return this.app.ticker;
|
|
57
|
+
}
|
|
58
|
+
private get screenW(): number {
|
|
59
|
+
return this.app.screen.width;
|
|
60
|
+
}
|
|
61
|
+
private get screenH(): number {
|
|
62
|
+
return this.app.screen.height;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── ShellRenderer ────────────────────────────────────────────────────────────
|
|
66
|
+
mount(host: ShellHost): void {
|
|
67
|
+
installShellFont();
|
|
68
|
+
this.host = host;
|
|
69
|
+
this.ctx = this.makeContext();
|
|
70
|
+
|
|
71
|
+
this.root.eventMode = 'static';
|
|
72
|
+
this.root.addChild(this.barLayer, this.modalLayer);
|
|
73
|
+
(this.parent ?? this.app.stage).addChild(this.root);
|
|
74
|
+
// make sure the stage delivers global pointer moves (drag, sliders)
|
|
75
|
+
this.app.stage.eventMode = 'static';
|
|
76
|
+
|
|
77
|
+
this.app.renderer.on('resize', this.onResize);
|
|
78
|
+
whenFontReady(() => {
|
|
79
|
+
if (!this.destroyed) this.renderBar();
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
renderBar(): void {
|
|
84
|
+
if (this.destroyed) return;
|
|
85
|
+
this.cancelMoneyAnims();
|
|
86
|
+
if (this.bar) {
|
|
87
|
+
this.barLayer.removeChild(this.bar);
|
|
88
|
+
this.bar.destroy({ children: true });
|
|
89
|
+
}
|
|
90
|
+
this.bar = new BottomBar(this.ctx);
|
|
91
|
+
this.barLayer.addChild(this.bar);
|
|
92
|
+
this.bar.applyFit();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
setLayout(): void {
|
|
96
|
+
this.renderBar();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Tokens are read live from host.tokens by the components, so a theme change is just a re-render
|
|
100
|
+
* (mirrors PixiGameShell.setTheme which re-rendered). */
|
|
101
|
+
applyTheme(_tokens: ShellTokens): void {
|
|
102
|
+
this.renderBar();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Count a money readout from→to on the freshly-rendered bar's value Text node. Mirrors
|
|
106
|
+
* PixiGameShell.animateMoney (which counted on the just-built bar's value Texts). */
|
|
107
|
+
animateMoney(field: 'balance' | 'win', from: number, to: number): void {
|
|
108
|
+
if (!this.bar) return;
|
|
109
|
+
if (field === 'balance' && this.bar.balanceValue) {
|
|
110
|
+
this.moneyAnims.push(
|
|
111
|
+
countUpText(this.ticker, this.bar.balanceValue, from, to, (n) => this.ctx.fmt(n)),
|
|
112
|
+
);
|
|
113
|
+
} else if (field === 'win' && this.bar.winValue) {
|
|
114
|
+
this.moneyAnims.push(
|
|
115
|
+
countUpText(this.ticker, this.bar.winValue, from, to, (n) => this.ctx.fmtWin(n)),
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private cancelMoneyAnims(): void {
|
|
121
|
+
for (const c of this.moneyAnims) c();
|
|
122
|
+
this.moneyAnims = [];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
openOverlay(req: OverlayRequest): OverlayHandle | void {
|
|
126
|
+
let layer: ShellLayer | null = null;
|
|
127
|
+
switch (req.kind) {
|
|
128
|
+
case 'settings':
|
|
129
|
+
layer = openSettings(this.ctx);
|
|
130
|
+
break;
|
|
131
|
+
case 'gameInfo':
|
|
132
|
+
layer = openGameInfo(this.ctx);
|
|
133
|
+
break;
|
|
134
|
+
case 'buyBonus':
|
|
135
|
+
layer = openBuyBonus(this.ctx);
|
|
136
|
+
break;
|
|
137
|
+
case 'betPicker':
|
|
138
|
+
layer = openBetPicker(this.ctx);
|
|
139
|
+
break;
|
|
140
|
+
case 'autoplayPicker':
|
|
141
|
+
layer = openAutoplayPicker(this.ctx);
|
|
142
|
+
break;
|
|
143
|
+
case 'replay':
|
|
144
|
+
layer = buildReplayModal(this.ctx, req.opts, () => this.openReplayInternal(req.opts));
|
|
145
|
+
break;
|
|
146
|
+
case 'modal':
|
|
147
|
+
layer = buildModal(this.ctx, req.opts);
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
if (!layer) return;
|
|
151
|
+
this.pushLayer(layer);
|
|
152
|
+
const built = layer;
|
|
153
|
+
return {
|
|
154
|
+
onKey: built.onKey ? built.onKey.bind(built) : undefined,
|
|
155
|
+
close: () => this.closeOverlay(),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
closeOverlay(): void {
|
|
160
|
+
this.closeLayer();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
refreshSoundIcon(_on: boolean): void {
|
|
164
|
+
// No-op: the open Settings overlay registers its icon updater via host.setSoundRefresh, which
|
|
165
|
+
// the controller's setSound path drives. The renderer has nothing extra to refresh.
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Fade out (≈250ms, like GameShell's REMOVE_FADE_MS) then tear down; resolves when removed. */
|
|
169
|
+
destroy(): Promise<void> {
|
|
170
|
+
if (this.destroyed) return Promise.resolve();
|
|
171
|
+
this.destroyed = true;
|
|
172
|
+
this.app.renderer.off('resize', this.onResize);
|
|
173
|
+
this.cancelMoneyAnims();
|
|
174
|
+
this.clearLayer();
|
|
175
|
+
this.root.eventMode = 'none';
|
|
176
|
+
return new Promise<void>((resolve) => {
|
|
177
|
+
tween(this.ticker, {
|
|
178
|
+
duration: 250,
|
|
179
|
+
onUpdate: (p) => {
|
|
180
|
+
this.root.alpha = 1 - p;
|
|
181
|
+
},
|
|
182
|
+
onComplete: () => {
|
|
183
|
+
this.root.destroy({ children: true });
|
|
184
|
+
resolve();
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ── layer stack ────────────────────────────────────────────────────────────
|
|
191
|
+
pushLayer(node: ShellLayer): LayerHandle {
|
|
192
|
+
this.clearLayer();
|
|
193
|
+
this.makeBackdrop(); // frosted snapshot of the scene behind (the DOM's backdrop-filter:blur)
|
|
194
|
+
this.currentLayer = node;
|
|
195
|
+
this.modalLayer.addChild(node);
|
|
196
|
+
this.fitModals();
|
|
197
|
+
return {
|
|
198
|
+
root: node,
|
|
199
|
+
close: () => {
|
|
200
|
+
if (this.currentLayer === node) this.closeLayer();
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
closeLayer(): void {
|
|
206
|
+
this.clearLayer();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private clearLayer(): void {
|
|
210
|
+
if (this.currentLayer) {
|
|
211
|
+
this.currentLayer.onRemove?.();
|
|
212
|
+
this.modalLayer.removeChild(this.currentLayer);
|
|
213
|
+
this.currentLayer.destroy({ children: true });
|
|
214
|
+
this.currentLayer = null;
|
|
215
|
+
}
|
|
216
|
+
this.removeBackdrop();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
fitModals(): void {
|
|
220
|
+
this.currentLayer?.fit?.();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Replay's reopen path: a replay modal can rebuild itself (e.g. after a replay completes). It is
|
|
224
|
+
* routed back through the controller-equivalent open flow so the layer stack + backdrop stay in
|
|
225
|
+
* sync — mirrors PixiGameShell.openReplay (push a fresh replay modal). */
|
|
226
|
+
private openReplayInternal(opts: ReplayModalOptions): void {
|
|
227
|
+
if (this.destroyed) return;
|
|
228
|
+
const layer = buildReplayModal(this.ctx, opts, () => this.openReplayInternal(opts));
|
|
229
|
+
this.pushLayer(layer);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ── frosted backdrop ─────────────────────────────────────────────────────────
|
|
233
|
+
/** Snapshot the scene behind the modal layer and blur it — the Pixi analogue of the overlay's
|
|
234
|
+
* `backdrop-filter: blur(20px) saturate(120%)`. Static (captured at open time): the game is paused
|
|
235
|
+
* under a modal, so a live re-blur each frame isn't worth the cost. */
|
|
236
|
+
private makeBackdrop(): void {
|
|
237
|
+
this.removeBackdrop();
|
|
238
|
+
const renderer = this.app.renderer;
|
|
239
|
+
const w = this.screenW;
|
|
240
|
+
const h = this.screenH;
|
|
241
|
+
if (w <= 0 || h <= 0) return;
|
|
242
|
+
const node = new Container();
|
|
243
|
+
// A SOLID base the size of the screen, under the blurred snapshot: the low-res blurred sprite can
|
|
244
|
+
// fall a hair short at the right/bottom edges, and without this the SHARP scene would show through
|
|
245
|
+
// the semi-transparent veil there. The base is the veil's own colour so the (rare) uncovered strip
|
|
246
|
+
// reads as more frost, never sharp game.
|
|
247
|
+
node.addChild(new Graphics().rect(0, 0, w, h).fill(0x0c111c));
|
|
248
|
+
this.modalLayer.addChild(node);
|
|
249
|
+
this.backdrop = { node, texture: RenderTexture.create({ width: w, height: h, resolution: 0.25 }) };
|
|
250
|
+
try {
|
|
251
|
+
// Heavy downscale (¼ res) is the blur-STRENGTH lever (each texel covers more screen), far cheaper
|
|
252
|
+
// than a full-res large-radius blur and smooth over fine detail. Effective on-screen blur ≈
|
|
253
|
+
// strength / resolution = 14 / 0.25 ≈ 56px → the background dissolves into a frost. A huge
|
|
254
|
+
// `strength` instead under-samples into a thin/streaky blur, so we keep the kernel moderate.
|
|
255
|
+
const texture = this.backdrop.texture;
|
|
256
|
+
this.modalLayer.visible = false; // never capture the (empty) modal layer / a stale backdrop
|
|
257
|
+
renderer.render({ container: this.app.stage, target: texture, clear: true });
|
|
258
|
+
this.modalLayer.visible = true;
|
|
259
|
+
const sprite = new Sprite(texture);
|
|
260
|
+
// Overscan ~6% so the blurred snapshot's faded edges sit just off-screen.
|
|
261
|
+
sprite.anchor.set(0.5);
|
|
262
|
+
sprite.position.set(w / 2, h / 2);
|
|
263
|
+
sprite.width = w * 1.06;
|
|
264
|
+
sprite.height = h * 1.06;
|
|
265
|
+
const blur = new BlurFilter({ strength: 14, quality: 6 });
|
|
266
|
+
blur.repeatEdgePixels = true; // no transparent edge halo
|
|
267
|
+
const saturate = new ColorMatrixFilter();
|
|
268
|
+
saturate.saturate(0.3, false); // x = amount*2/3 + 1 ≈ 1.2 → saturate(120%) (matches the DOM)
|
|
269
|
+
sprite.filters = [blur, saturate];
|
|
270
|
+
node.addChild(sprite); // over the solid base, below the modal layer node
|
|
271
|
+
} catch {
|
|
272
|
+
this.modalLayer.visible = true; // headless / no GL → skip the blur, the solid base + veil show
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private removeBackdrop(): void {
|
|
277
|
+
if (this.backdrop) {
|
|
278
|
+
this.modalLayer.removeChild(this.backdrop.node);
|
|
279
|
+
this.backdrop.node.destroy({ children: true });
|
|
280
|
+
this.backdrop.texture.destroy(true);
|
|
281
|
+
this.backdrop = undefined;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ── safe area / visibility (PixiGameShell parity) ─────────────────────────────
|
|
286
|
+
/** Height of the bottom control bar in px (0 before first layout, or in replay-only chrome). */
|
|
287
|
+
get barHeight(): number {
|
|
288
|
+
return this.bar?.height ?? 0;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Insets a scene should avoid. Only the bottom bar is reserved; the rest is full-bleed. */
|
|
292
|
+
get safeArea(): { top: number; right: number; bottom: number; left: number } {
|
|
293
|
+
return { top: 0, right: 0, bottom: this.barHeight, left: 0 };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Show/hide the whole shell (bar + overlays). */
|
|
297
|
+
setVisible(visible: boolean): void {
|
|
298
|
+
this.root.visible = visible;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ── resize ───────────────────────────────────────────────────────────────────
|
|
302
|
+
private onResize = (): void => {
|
|
303
|
+
if (this.destroyed) return;
|
|
304
|
+
// Report the new surface size; the controller recomputes layout (wide|mobile) and re-renders the
|
|
305
|
+
// bar (setLayout → renderBar / renderBar). This keeps PixiGameShell.onResize behaviour: the bar
|
|
306
|
+
// is rebuilt at the new size, then the open layer is re-fit and the backdrop re-snapshotted.
|
|
307
|
+
this.host.notifyResize(this.screenW, this.screenH);
|
|
308
|
+
if (this.currentLayer) {
|
|
309
|
+
this.currentLayer.resize?.(this.screenW, this.screenH);
|
|
310
|
+
this.makeBackdrop(); // re-snapshot at the new size
|
|
311
|
+
if (this.backdrop) this.modalLayer.setChildIndex(this.backdrop.node, 0); // keep it below the layer
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
// ── PixiComponentContext factory ──────────────────────────────────────────────
|
|
316
|
+
/** Build the surface pixi components read: the core brain (host) plus the Pixi-specific members
|
|
317
|
+
* (ticker, canvas, screen size, the layer-stack methods, fmt/fmtWin shorthands). */
|
|
318
|
+
private makeContext(): PixiComponentContext {
|
|
319
|
+
const host = this.host;
|
|
320
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
321
|
+
const self = this;
|
|
322
|
+
const ctx: PixiComponentContext = {
|
|
323
|
+
// — core ShellHost (forwarded) —
|
|
324
|
+
get state() { return host.state; },
|
|
325
|
+
get config() { return host.config; },
|
|
326
|
+
get tokens() { return host.tokens; },
|
|
327
|
+
get layout() { return host.layout; },
|
|
328
|
+
get soundOn() { return host.soundOn; },
|
|
329
|
+
get actions() { return host.actions; },
|
|
330
|
+
t: (s) => host.t(s),
|
|
331
|
+
formatCurrency: (n, win) => host.formatCurrency(n, win),
|
|
332
|
+
emit: host.emit.bind(host),
|
|
333
|
+
notifyResize: (w, h) => host.notifyResize(w, h),
|
|
334
|
+
setSound: (on) => host.setSound(on),
|
|
335
|
+
setSoundRefresh: (fn) => host.setSoundRefresh(fn),
|
|
336
|
+
// — Pixi-specific surface —
|
|
337
|
+
get ticker() { return self.app.ticker; },
|
|
338
|
+
get canvas() { return self.app.canvas as HTMLCanvasElement | undefined; },
|
|
339
|
+
get screenW() { return self.app.screen.width; },
|
|
340
|
+
get screenH() { return self.app.screen.height; },
|
|
341
|
+
render: () => self.renderBar(),
|
|
342
|
+
pushLayer: (node) => self.pushLayer(node),
|
|
343
|
+
closeLayer: () => self.closeLayer(),
|
|
344
|
+
fitModals: () => self.fitModals(),
|
|
345
|
+
fmt: (n) => host.formatCurrency(n),
|
|
346
|
+
fmtWin: (n) => host.formatCurrency(n, true),
|
|
347
|
+
};
|
|
348
|
+
return ctx;
|
|
349
|
+
}
|
|
350
|
+
}
|