@energy8platform/game-engine 0.19.0 → 0.20.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/core.cjs.js +33 -156
- package/dist/core.cjs.js.map +1 -1
- package/dist/core.esm.js +34 -157
- package/dist/core.esm.js.map +1 -1
- package/dist/host.cjs.js +70 -186
- package/dist/host.cjs.js.map +1 -1
- package/dist/host.d.ts +6 -0
- package/dist/host.esm.js +72 -188
- package/dist/host.esm.js.map +1 -1
- package/dist/index.cjs.js +33 -156
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +10 -11
- package/dist/index.esm.js +34 -157
- package/dist/index.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/core/GameApplication.ts +6 -5
- package/src/host/shellConfig.ts +44 -31
- package/src/loading/LoadingScene.ts +31 -174
package/package.json
CHANGED
|
@@ -181,19 +181,20 @@ export class GameApplication extends EventEmitter<GameEngineEvents> {
|
|
|
181
181
|
|
|
182
182
|
this.emit('initialized');
|
|
183
183
|
|
|
184
|
-
// 7.
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
// 8. Load assets with loading screen
|
|
184
|
+
// 7. Load assets. The CSS preloader stays on screen — LoadingScene drives
|
|
185
|
+
// its progress/tap and removes it before entering the game, so there's
|
|
186
|
+
// a single continuous overlay from boot to gameplay (no logo flash).
|
|
188
187
|
await this.loadAssets(firstScene, sceneData);
|
|
189
188
|
|
|
190
189
|
this.emit('loaded');
|
|
191
190
|
|
|
192
|
-
//
|
|
191
|
+
// 8. Start the game loop
|
|
193
192
|
this._running = true;
|
|
194
193
|
this.emit('started');
|
|
195
194
|
} catch (err) {
|
|
196
195
|
console.error('[GameEngine] Failed to start:', err);
|
|
196
|
+
// Tear down the preloader so a failure doesn't strand the brand frame.
|
|
197
|
+
if (this._container) removeCSSPreloader(this._container);
|
|
197
198
|
this.emit('error', err instanceof Error ? err : new Error(String(err)));
|
|
198
199
|
throw err;
|
|
199
200
|
}
|
package/src/host/shellConfig.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// packages/game-engine/src/host/shellConfig.ts
|
|
2
|
-
// `socialize`
|
|
3
|
-
// types
|
|
4
|
-
import { socialize } from '@energy8platform/platform-core/shell';
|
|
2
|
+
// `socialize` / `createI18n` are runtime helpers sourced from platform-core/shell;
|
|
3
|
+
// pixi-shell re-exports only types so we import directly from platform-core.
|
|
4
|
+
import { socialize, createI18n } from '@energy8platform/platform-core/shell';
|
|
5
|
+
import type { Lang } from '@energy8platform/platform-core/shell';
|
|
5
6
|
import type {
|
|
6
7
|
PixiShellConfig, ShellMode, CurrencyConfig, GameInfoContent, GameInfoSection, PaytableRow,
|
|
7
8
|
BonusOption, ShellFeatures, GameMode,
|
|
@@ -26,6 +27,11 @@ export interface SlotShellOptions {
|
|
|
26
27
|
buyBonus?: BonusOption[];
|
|
27
28
|
tiers?: WinTier[];
|
|
28
29
|
features?: Partial<ShellFeatures>;
|
|
30
|
+
/** Per-game translation map. Keys are the English source strings (from the spec or copy);
|
|
31
|
+
* values are the translated strings for each language. Merged with the shell's built-in
|
|
32
|
+
* `LOCALES` catalog — game strings take precedence over the built-in entries for the same key.
|
|
33
|
+
* When not supplied, English source strings pass through verbatim (socialised in social mode). */
|
|
34
|
+
i18n?: Partial<Record<Lang, Record<string, string>>>;
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
/** The currency metadata surfaced on `initData.config.currency` (game-sdk `CurrencyMetaData`).
|
|
@@ -130,8 +136,9 @@ export function stakeForAction(model: GameModel, action: string, bet: number): n
|
|
|
130
136
|
return cost * bet;
|
|
131
137
|
}
|
|
132
138
|
|
|
133
|
-
/** Derive shell buy cards + ante toggles from the spec's buy/feature actions (SSOT).
|
|
134
|
-
|
|
139
|
+
/** Derive shell buy cards + ante toggles from the spec's buy/feature actions (SSOT).
|
|
140
|
+
* @param t Optional translator applied to `title` and `description` before they enter the shell. */
|
|
141
|
+
export function toBonusOptions(model: GameModel, t: (s: string) => string = (s) => s): BonusOption[] {
|
|
135
142
|
const out: BonusOption[] = [];
|
|
136
143
|
for (const [key, action] of Object.entries(model.spec.actions)) {
|
|
137
144
|
const role = action.role ?? 'base';
|
|
@@ -139,16 +146,19 @@ export function toBonusOptions(model: GameModel): BonusOption[] {
|
|
|
139
146
|
out.push({
|
|
140
147
|
id: key,
|
|
141
148
|
type: role === 'buy' ? 'bonus' : 'feature',
|
|
142
|
-
title: action.title ?? key.replace(/_/g, ' ').toUpperCase(),
|
|
143
|
-
description: action.description ?? '',
|
|
149
|
+
title: t(action.title ?? key.replace(/_/g, ' ').toUpperCase()),
|
|
150
|
+
description: t(action.description ?? ''),
|
|
144
151
|
priceMultiplier: action.cost ?? (role === 'buy' ? 100 : 1),
|
|
152
|
+
// Volatility (1–5 bolts) is part of the spec action SSOT; forward it so the buy card shows it.
|
|
153
|
+
...(action.volatility != null ? { volatility: action.volatility } : {}),
|
|
145
154
|
});
|
|
146
155
|
}
|
|
147
156
|
return out;
|
|
148
157
|
}
|
|
149
158
|
|
|
150
|
-
/** Build a paytable section from the model's derived paytable view (multipliers per symbol count).
|
|
151
|
-
|
|
159
|
+
/** Build a paytable section from the model's derived paytable view (multipliers per symbol count).
|
|
160
|
+
* @param t Optional translator applied to symbol names before they enter the shell. */
|
|
161
|
+
function paytableSection(model: GameModel, t: (s: string) => string = (s) => s): GameInfoSection | null {
|
|
152
162
|
const symbols = model.paytable?.symbols ?? [];
|
|
153
163
|
const rows: PaytableRow[] = [];
|
|
154
164
|
for (const s of symbols) {
|
|
@@ -157,10 +167,12 @@ function paytableSection(model: GameModel): GameInfoSection | null {
|
|
|
157
167
|
.filter((w) => Number.isFinite(w.multiplier) && w.multiplier > 0)
|
|
158
168
|
.sort((a, b) => Number(a.count) - Number(b.count));
|
|
159
169
|
if (!wins.length) continue;
|
|
160
|
-
rows.push({ symbol: { text: s.name ?? s.id }, wins });
|
|
170
|
+
rows.push({ symbol: { text: t(s.name ?? s.id) }, wins });
|
|
161
171
|
}
|
|
162
172
|
if (!rows.length) return null;
|
|
163
|
-
|
|
173
|
+
// No literal title — the shell renders `s.title ?? host.t('Paytable')`, so the heading is
|
|
174
|
+
// localized (matching how the modes/wins sections rely on the shell's translated fallback).
|
|
175
|
+
return { type: 'paytable', rows };
|
|
164
176
|
}
|
|
165
177
|
|
|
166
178
|
/** Build a "wins" illustration section sized to the grid; `kind` follows the spec mechanic hint. */
|
|
@@ -206,13 +218,14 @@ function orderDisclaimerLast(sections: GameInfoSection[]): GameInfoSection[] {
|
|
|
206
218
|
* gets a real info panel for free (paytable, win illustration, controls, and the Stake
|
|
207
219
|
* disclaimer when present). Author-supplied `opts.gameInfo` is MERGED over this set by
|
|
208
220
|
* section identity (see `mergeGameInfo`), not wholesale-replaced.
|
|
221
|
+
* @param t Optional translator applied to spec-derived player-facing strings (symbol names, mode titles, etc.).
|
|
209
222
|
*/
|
|
210
|
-
export function defaultGameInfo(model: GameModel, runtime: ShellRuntime): GameInfoContent {
|
|
223
|
+
export function defaultGameInfo(model: GameModel, runtime: ShellRuntime, t: (s: string) => string = (s) => s): GameInfoContent {
|
|
211
224
|
const sections: GameInfoSection[] = [];
|
|
212
225
|
sections.push(winsSection(model));
|
|
213
|
-
const pay = paytableSection(model);
|
|
226
|
+
const pay = paytableSection(model, t);
|
|
214
227
|
if (pay) sections.push(pay);
|
|
215
|
-
const modes = modesSection(model);
|
|
228
|
+
const modes = modesSection(model, t);
|
|
216
229
|
if (modes) sections.push(modes);
|
|
217
230
|
sections.push({ type: 'controls' });
|
|
218
231
|
const disclaimer = disclaimerSection(runtime.disclaimerLines);
|
|
@@ -224,21 +237,22 @@ export function defaultGameInfo(model: GameModel, runtime: ShellRuntime): GameIn
|
|
|
224
237
|
* (`model.mathModes` + `spec.actions`) that drives the buy cards and the math pipeline. Stake
|
|
225
238
|
* compliance requires Cost / RTP / Max Win per mode; deriving it here means the author declares a
|
|
226
239
|
* mode once (in game.spec) and the info table can't drift. `free` actions are excluded (mathModes
|
|
227
|
-
* already drops them — free spins are part of a bonus, not a purchasable mode).
|
|
228
|
-
|
|
240
|
+
* already drops them — free spins are part of a bonus, not a purchasable mode).
|
|
241
|
+
* @param t Optional translator applied to row `title` and `description` before they enter the shell. */
|
|
242
|
+
function modesSection(model: GameModel, t: (s: string) => string = (s) => s): GameInfoSection | null {
|
|
229
243
|
const modes = model.mathModes ?? [];
|
|
230
244
|
if (!modes.length) return null;
|
|
231
245
|
const rows: GameMode[] = modes.map((m) => {
|
|
232
246
|
const action = model.spec.actions[m.action];
|
|
233
247
|
const isBase = (action?.role ?? 'base') === 'base' || m.mode === 'BASE';
|
|
234
248
|
const row: GameMode = {
|
|
235
|
-
title: action?.title ?? (isBase ? 'Base game' : m.mode.replace(/_/g, ' ')),
|
|
249
|
+
title: t(action?.title ?? (isBase ? 'Base game' : m.mode.replace(/_/g, ' '))),
|
|
236
250
|
maxWin: `${m.maxWin.toLocaleString('en-US')}×`,
|
|
237
251
|
};
|
|
238
252
|
// Cost is a bet-multiplier; a base spin (1×) reads as no premium, so only show it for buys/features.
|
|
239
253
|
if (m.costMultiplier && m.costMultiplier !== 1) row.price = `${m.costMultiplier}×`;
|
|
240
254
|
if (typeof m.rtp === 'number') row.rtp = Math.round(m.rtp * 1000) / 10; // 0.965 → 96.5 (%)
|
|
241
|
-
if (action?.description) row.description = action.description;
|
|
255
|
+
if (action?.description) row.description = t(action.description);
|
|
242
256
|
return row;
|
|
243
257
|
});
|
|
244
258
|
return { type: 'modes', title: 'MODES', modes: rows };
|
|
@@ -336,18 +350,17 @@ export function buildShellConfig(
|
|
|
336
350
|
const currency =
|
|
337
351
|
opts.currency ?? runtime.currency ?? resolveCurrency(null, model.spec.currency);
|
|
338
352
|
const isSocial = runtime.social ?? false;
|
|
339
|
-
//
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
|
|
346
|
-
// social mode (identity otherwise) so authors can wrap copy explicitly; the full merged set is
|
|
347
|
-
// still socialized below as a safety net.
|
|
348
|
-
const t = isSocial ? socialize : (text: string) => text;
|
|
353
|
+
// Build the merged resolver: game i18n map + shell LOCALES (via createI18n), then socialize on
|
|
354
|
+
// top for English social mode. For non-English languages, createI18n already handles the
|
|
355
|
+
// translation lookup; social rewriting is English-only and applied separately after.
|
|
356
|
+
// Author gameInfo may be a plain object or a `(t) => content` factory. `t` is the resolver so
|
|
357
|
+
// authors can wrap player-facing copy explicitly; the full merged set is still socialized below
|
|
358
|
+
// (section pass) as a safety net so restricted words can't slip through even if t() was missed.
|
|
359
|
+
const { t } = createI18n({ language: runtime.language ?? 'en', isSocial, messages: opts.i18n });
|
|
349
360
|
const authored = typeof opts.gameInfo === 'function' ? opts.gameInfo(t) : opts.gameInfo;
|
|
350
|
-
|
|
361
|
+
// Pass t() through to spec-derived sections so symbol names, mode titles, and descriptions are
|
|
362
|
+
// pre-translated before they reach the shell renderer.
|
|
363
|
+
let gameInfo = mergeGameInfo(defaultGameInfo(model, runtime, t), authored);
|
|
351
364
|
// The DISCLAIMER is required legal copy and must be shown VERBATIM — never socialized (its
|
|
352
365
|
// wording is mandated, and word-swaps like "bet → play" would corrupt the legal text).
|
|
353
366
|
if (isSocial) {
|
|
@@ -357,8 +370,8 @@ export function buildShellConfig(
|
|
|
357
370
|
}
|
|
358
371
|
// The legal DISCLAIMER always renders LAST — author-merged or extra sections never push below it.
|
|
359
372
|
gameInfo = { sections: orderDisclaimerLast(gameInfo.sections ?? []) };
|
|
360
|
-
// Buy-bonus cards:
|
|
361
|
-
const buyBonus = socializeBonusOptions(opts.buyBonus ?? toBonusOptions(model), isSocial);
|
|
373
|
+
// Buy-bonus cards: apply t() to spec-derived options (pre-translate), then socialize for en+social.
|
|
374
|
+
const buyBonus = socializeBonusOptions(opts.buyBonus ?? toBonusOptions(model, t), isSocial);
|
|
362
375
|
// Features: defaults, then author overrides, THEN jurisdiction restrictions (a restriction wins).
|
|
363
376
|
const features: ShellFeatures = {
|
|
364
377
|
turbo: 0,
|
|
@@ -1,23 +1,10 @@
|
|
|
1
|
-
import { Container, Graphics, Text, Sprite, Assets } from 'pixi.js';
|
|
2
1
|
import { Scene } from '../core/Scene';
|
|
3
|
-
import { Tween } from '../animation/Tween';
|
|
4
|
-
import { Easing } from '../animation/Easing';
|
|
5
2
|
import type { LoadingScreenConfig } from '../types';
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
*/
|
|
12
|
-
function buildLoadingLogoSVG(): string {
|
|
13
|
-
return buildLogoSVG({
|
|
14
|
-
idPrefix: 'ls',
|
|
15
|
-
svgStyle: 'width:100%;height:auto;',
|
|
16
|
-
clipRectId: 'ge-loader-rect',
|
|
17
|
-
textId: 'ge-loader-pct',
|
|
18
|
-
textContent: '0%',
|
|
19
|
-
});
|
|
20
|
-
}
|
|
3
|
+
import {
|
|
4
|
+
setCSSPreloaderProgress,
|
|
5
|
+
waitCSSPreloaderTap,
|
|
6
|
+
removeCSSPreloader,
|
|
7
|
+
} from '@energy8platform/platform-core/loading';
|
|
21
8
|
|
|
22
9
|
interface LoadingSceneData {
|
|
23
10
|
engine: any; // GameApplication — avoid circular import
|
|
@@ -26,10 +13,14 @@ interface LoadingSceneData {
|
|
|
26
13
|
}
|
|
27
14
|
|
|
28
15
|
/**
|
|
29
|
-
* Built-in loading screen
|
|
16
|
+
* Built-in loading screen.
|
|
30
17
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
18
|
+
* It does NOT render its own overlay — the CSS preloader created at boot
|
|
19
|
+
* (`createPlatformSession`/`GameApplication.start`) stays on screen, and this
|
|
20
|
+
* scene merely drives it: asset-load progress → `setCSSPreloaderProgress`,
|
|
21
|
+
* tap-to-start → `waitCSSPreloaderTap`, then fades it out via
|
|
22
|
+
* `removeCSSPreloader` before entering the game. One continuous overlay from
|
|
23
|
+
* boot to gameplay — no second logo, no mid-load flash.
|
|
33
24
|
*/
|
|
34
25
|
export class LoadingScene extends Scene {
|
|
35
26
|
private _engine!: any;
|
|
@@ -37,12 +28,6 @@ export class LoadingScene extends Scene {
|
|
|
37
28
|
private _targetData?: unknown;
|
|
38
29
|
private _config!: LoadingScreenConfig;
|
|
39
30
|
|
|
40
|
-
// HTML overlay
|
|
41
|
-
private _overlay: HTMLDivElement | null = null;
|
|
42
|
-
private _loaderRect: SVGRectElement | null = null;
|
|
43
|
-
private _percentEl: Element | null = null;
|
|
44
|
-
private _tapToStartEl: Element | null = null;
|
|
45
|
-
|
|
46
31
|
// State
|
|
47
32
|
private _displayedProgress = 0;
|
|
48
33
|
private _targetProgress = 0;
|
|
@@ -57,9 +42,6 @@ export class LoadingScene extends Scene {
|
|
|
57
42
|
this._config = engine.config.loading ?? {};
|
|
58
43
|
this._startTime = Date.now();
|
|
59
44
|
|
|
60
|
-
// Create the HTML overlay with the SVG logo
|
|
61
|
-
this.createOverlay();
|
|
62
|
-
|
|
63
45
|
// Initialize asset manager
|
|
64
46
|
await this._engine.assets.init();
|
|
65
47
|
|
|
@@ -122,16 +104,14 @@ export class LoadingScene extends Scene {
|
|
|
122
104
|
this._displayedProgress = 1;
|
|
123
105
|
this.updateLoaderBar(1);
|
|
124
106
|
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
await this.transitionToGame();
|
|
130
|
-
}
|
|
107
|
+
// Wait for the player's tap — resolves immediately when tapToStart is
|
|
108
|
+
// false (the preloader honours that flag) — then enter the game.
|
|
109
|
+
await waitCSSPreloaderTap();
|
|
110
|
+
await this.transitionToGame();
|
|
131
111
|
}
|
|
132
112
|
|
|
133
113
|
override onUpdate(dt: number): void {
|
|
134
|
-
// Smooth progress bar fill
|
|
114
|
+
// Smooth progress bar fill (during active loading)
|
|
135
115
|
if (!this._loadingComplete && this._displayedProgress < this._targetProgress) {
|
|
136
116
|
this._displayedProgress = Math.min(
|
|
137
117
|
this._displayedProgress + dt * 1.5,
|
|
@@ -142,107 +122,19 @@ export class LoadingScene extends Scene {
|
|
|
142
122
|
}
|
|
143
123
|
|
|
144
124
|
override onResize(_width: number, _height: number): void {
|
|
145
|
-
//
|
|
125
|
+
// The preloader overlay is CSS-based and auto-resizes.
|
|
146
126
|
}
|
|
147
127
|
|
|
148
128
|
override onDestroy(): void {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
// ─── HTML Overlay ──────────────────────────────────────
|
|
153
|
-
|
|
154
|
-
private createOverlay(): void {
|
|
155
|
-
const bgColor =
|
|
156
|
-
typeof this._config.backgroundColor === 'string'
|
|
157
|
-
? this._config.backgroundColor
|
|
158
|
-
: typeof this._config.backgroundColor === 'number'
|
|
159
|
-
? `#${this._config.backgroundColor.toString(16).padStart(6, '0')}`
|
|
160
|
-
: '#0a0a1a';
|
|
161
|
-
|
|
162
|
-
const bgGradient =
|
|
163
|
-
this._config.backgroundGradient ??
|
|
164
|
-
`linear-gradient(135deg, ${bgColor} 0%, #1a1a3e 100%)`;
|
|
165
|
-
|
|
166
|
-
this._overlay = document.createElement('div');
|
|
167
|
-
this._overlay.id = '__ge-loading-overlay__';
|
|
168
|
-
this._overlay.innerHTML = `
|
|
169
|
-
<div class="ge-loading-content">
|
|
170
|
-
${buildLoadingLogoSVG()}
|
|
171
|
-
</div>
|
|
172
|
-
`;
|
|
173
|
-
|
|
174
|
-
const style = document.createElement('style');
|
|
175
|
-
style.id = '__ge-loading-style__';
|
|
176
|
-
style.textContent = `
|
|
177
|
-
#__ge-loading-overlay__ {
|
|
178
|
-
position: absolute;
|
|
179
|
-
top: 0; left: 0;
|
|
180
|
-
width: 100%; height: 100%;
|
|
181
|
-
background: ${bgGradient};
|
|
182
|
-
display: flex;
|
|
183
|
-
align-items: center;
|
|
184
|
-
justify-content: center;
|
|
185
|
-
z-index: 9999;
|
|
186
|
-
transition: opacity 0.5s ease-out;
|
|
187
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
188
|
-
}
|
|
189
|
-
#__ge-loading-overlay__.ge-fade-out {
|
|
190
|
-
opacity: 0;
|
|
191
|
-
pointer-events: none;
|
|
192
|
-
}
|
|
193
|
-
.ge-loading-content {
|
|
194
|
-
display: flex;
|
|
195
|
-
flex-direction: column;
|
|
196
|
-
align-items: center;
|
|
197
|
-
width: 75%;
|
|
198
|
-
max-width: 650px;
|
|
199
|
-
}
|
|
200
|
-
.ge-loading-content svg {
|
|
201
|
-
filter: drop-shadow(0 0 40px rgba(121, 57, 194, 0.5));
|
|
202
|
-
cursor: default;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
.ge-svg-pulse {
|
|
206
|
-
animation: ge-tap-pulse 1.2s ease-in-out infinite;
|
|
207
|
-
}
|
|
208
|
-
@keyframes ge-tap-pulse {
|
|
209
|
-
0%, 100% { opacity: 0.5; }
|
|
210
|
-
50% { opacity: 1; }
|
|
211
|
-
}
|
|
212
|
-
`;
|
|
213
|
-
|
|
214
|
-
// Get the container that holds the canvas
|
|
215
|
-
const container = this._engine.app?.canvas?.parentElement;
|
|
216
|
-
if (container) {
|
|
217
|
-
container.style.position = container.style.position || 'relative';
|
|
218
|
-
container.appendChild(style);
|
|
219
|
-
container.appendChild(this._overlay);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Cache the SVG loader rect for progress updates
|
|
223
|
-
this._loaderRect = this._overlay.querySelector('#ge-loader-rect');
|
|
224
|
-
this._percentEl = this._overlay.querySelector('#ge-loader-pct');
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
private removeOverlay(): void {
|
|
228
|
-
this._overlay?.remove();
|
|
229
|
-
document.getElementById('__ge-loading-style__')?.remove();
|
|
230
|
-
this._overlay = null;
|
|
231
|
-
this._loaderRect = null;
|
|
232
|
-
this._percentEl = null;
|
|
233
|
-
this._tapToStartEl = null;
|
|
129
|
+
// Defensive: ensure the preloader is gone even if we never transitioned
|
|
130
|
+
// (e.g. the scene was popped externally). Idempotent.
|
|
131
|
+
void removeCSSPreloader(this.hostElement());
|
|
234
132
|
}
|
|
235
133
|
|
|
236
134
|
// ─── Progress ──────────────────────────────────────────
|
|
237
135
|
|
|
238
136
|
private updateLoaderBar(progress: number): void {
|
|
239
|
-
|
|
240
|
-
this._loaderRect.setAttribute('width', String(LOADER_BAR_MAX_WIDTH * progress));
|
|
241
|
-
}
|
|
242
|
-
if (this._percentEl) {
|
|
243
|
-
const pct = Math.round(progress * 100);
|
|
244
|
-
(this._percentEl as SVGTextElement).textContent = `${pct}%`;
|
|
245
|
-
}
|
|
137
|
+
setCSSPreloaderProgress(Math.max(0, Math.min(1, progress)));
|
|
246
138
|
}
|
|
247
139
|
|
|
248
140
|
/**
|
|
@@ -275,58 +167,23 @@ export class LoadingScene extends Scene {
|
|
|
275
167
|
});
|
|
276
168
|
}
|
|
277
169
|
|
|
278
|
-
// ───
|
|
279
|
-
|
|
280
|
-
private async showTapToStart(): Promise<void> {
|
|
281
|
-
const tapText = this._config.tapToStartText ?? 'TAP TO START';
|
|
282
|
-
|
|
283
|
-
// Reuse the same SVG text element — replace percentage with tap text
|
|
284
|
-
if (this._percentEl) {
|
|
285
|
-
const el = this._percentEl as SVGTextElement;
|
|
286
|
-
el.textContent = tapText;
|
|
287
|
-
el.setAttribute('fill', '#ffffff');
|
|
288
|
-
el.classList.add('ge-svg-pulse');
|
|
289
|
-
this._tapToStartEl = el;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Make overlay clickable
|
|
293
|
-
if (this._overlay) {
|
|
294
|
-
this._overlay.style.cursor = 'pointer';
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// Wait for tap
|
|
298
|
-
return new Promise<void>((resolve) => {
|
|
299
|
-
const handler = async () => {
|
|
300
|
-
this._overlay?.removeEventListener('click', handler);
|
|
301
|
-
await this.transitionToGame();
|
|
302
|
-
resolve();
|
|
303
|
-
};
|
|
170
|
+
// ─── Transition ────────────────────────────────────────
|
|
304
171
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
172
|
+
/** The DOM element hosting the canvas + preloader overlay. */
|
|
173
|
+
private hostElement(): HTMLElement {
|
|
174
|
+
return this._engine?.app?.canvas?.parentElement ?? document.body;
|
|
308
175
|
}
|
|
309
176
|
|
|
310
|
-
// ─── Transition ────────────────────────────────────────
|
|
311
|
-
|
|
312
177
|
private async transitionToGame(): Promise<void> {
|
|
313
|
-
// Fade out the
|
|
314
|
-
|
|
315
|
-
this._overlay.classList.add('ge-fade-out');
|
|
316
|
-
await new Promise<void>((resolve) => {
|
|
317
|
-
this._overlay!.addEventListener('transitionend', () => resolve(), { once: true });
|
|
318
|
-
// Safety timeout
|
|
319
|
-
setTimeout(resolve, 600);
|
|
320
|
-
});
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// Remove overlay
|
|
324
|
-
this.removeOverlay();
|
|
178
|
+
// Fade out and remove the shared CSS preloader (resolves after the fade).
|
|
179
|
+
await removeCSSPreloader(this.hostElement());
|
|
325
180
|
|
|
326
181
|
// Navigate to the target scene, always passing the engine reference
|
|
327
182
|
await this._engine.scenes.goto(this._targetScene, {
|
|
328
183
|
engine: this._engine,
|
|
329
|
-
...(this._targetData && typeof this._targetData === 'object'
|
|
184
|
+
...(this._targetData && typeof this._targetData === 'object'
|
|
185
|
+
? (this._targetData as Record<string, unknown>)
|
|
186
|
+
: { data: this._targetData }),
|
|
330
187
|
});
|
|
331
188
|
}
|
|
332
189
|
}
|