@energy8platform/game-engine 0.18.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.
@@ -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 { buildLogoSVG, LOADER_BAR_MAX_WIDTH } from '@energy8platform/platform-core/loading';
7
-
8
- /**
9
- * Build the loading scene variant of the logo SVG.
10
- * Uses unique IDs (prefixed with 'ls') to avoid collisions with CSSPreloader.
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 using the Energy8 SVG logo with animated loader bar.
16
+ * Built-in loading screen.
30
17
  *
31
- * Renders as an HTML overlay on top of the canvas for crisp SVG quality.
32
- * The loader bar fill width is driven by asset loading progress.
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
- // Show "Tap to Start" or transition directly
126
- if (this._config.tapToStart !== false) {
127
- await this.showTapToStart();
128
- } else {
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 via HTML (during active loading)
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
- // Overlay is CSS-based, auto-resizes
125
+ // The preloader overlay is CSS-based and auto-resizes.
146
126
  }
147
127
 
148
128
  override onDestroy(): void {
149
- this.removeOverlay();
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
- if (this._loaderRect) {
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
- // ─── Tap to Start ─────────────────────────────────────
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
- // Listen on the full overlay for easier mobile tap
306
- this._overlay?.addEventListener('click', handler);
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 HTML overlay
314
- if (this._overlay) {
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' ? this._targetData as Record<string, unknown> : { data: this._targetData }),
184
+ ...(this._targetData && typeof this._targetData === 'object'
185
+ ? (this._targetData as Record<string, unknown>)
186
+ : { data: this._targetData }),
330
187
  });
331
188
  }
332
189
  }
@@ -1,4 +1,4 @@
1
- import type { Application } from 'pixi.js';
1
+ import type { Application, Container } from 'pixi.js';
2
2
  import { EventEmitter } from '../core/EventEmitter';
3
3
  import { ScaleMode, Orientation } from '../types';
4
4
 
@@ -45,6 +45,7 @@ export class ViewportManager extends EventEmitter<ViewportEvents> {
45
45
  private _app: Application;
46
46
  private _container: HTMLElement;
47
47
  private _config: ViewportConfig;
48
+ private _target: Container;
48
49
  private _resizeObserver: ResizeObserver | null = null;
49
50
  private _currentOrientation: Orientation = Orientation.LANDSCAPE;
50
51
  private _currentWidth = 0;
@@ -53,11 +54,20 @@ export class ViewportManager extends EventEmitter<ViewportEvents> {
53
54
  private _destroyed = false;
54
55
  private _resizeTimeout: number | null = null;
55
56
 
56
- constructor(app: Application, container: HTMLElement, config: ViewportConfig) {
57
+ constructor(
58
+ app: Application,
59
+ container: HTMLElement,
60
+ config: ViewportConfig,
61
+ target?: Container,
62
+ ) {
57
63
  super();
58
64
  this._app = app;
59
65
  this._container = container;
60
66
  this._config = config;
67
+ // The container this manager scales/offsets. Defaults to app.stage for backward
68
+ // compatibility; the engine passes a dedicated scaled world root so app.stage stays
69
+ // identity (screen space) for unscaled UI layers.
70
+ this._target = target ?? app.stage;
61
71
 
62
72
  this.setupObserver();
63
73
  }
@@ -164,18 +174,18 @@ export class ViewportManager extends EventEmitter<ViewportEvents> {
164
174
  ? Math.min(containerWidth / designWidth, containerHeight / designHeight)
165
175
  : scale;
166
176
 
167
- this._app.stage.scale.set(stageScale);
177
+ this._target.scale.set(stageScale);
168
178
 
169
179
  // Center the stage for FIT mode
170
180
  if (scaleMode === ScaleMode.FIT) {
171
- this._app.stage.x = Math.round((containerWidth - designWidth * stageScale) / 2);
172
- this._app.stage.y = Math.round((containerHeight - designHeight * stageScale) / 2);
181
+ this._target.x = Math.round((containerWidth - designWidth * stageScale) / 2);
182
+ this._target.y = Math.round((containerHeight - designHeight * stageScale) / 2);
173
183
  } else if (scaleMode === ScaleMode.FILL) {
174
- this._app.stage.x = Math.round((containerWidth - gameWidth * stageScale) / 2);
175
- this._app.stage.y = Math.round((containerHeight - gameHeight * stageScale) / 2);
184
+ this._target.x = Math.round((containerWidth - gameWidth * stageScale) / 2);
185
+ this._target.y = Math.round((containerHeight - gameHeight * stageScale) / 2);
176
186
  } else {
177
- this._app.stage.x = 0;
178
- this._app.stage.y = 0;
187
+ this._target.x = 0;
188
+ this._target.y = 0;
179
189
  }
180
190
 
181
191
  this._currentWidth = gameWidth;