@energy8platform/game-engine 0.2.0 → 0.3.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.
Files changed (59) hide show
  1. package/README.md +318 -49
  2. package/dist/animation.cjs.js +191 -1
  3. package/dist/animation.cjs.js.map +1 -1
  4. package/dist/animation.d.ts +117 -1
  5. package/dist/animation.esm.js +192 -3
  6. package/dist/animation.esm.js.map +1 -1
  7. package/dist/audio.cjs.js +66 -16
  8. package/dist/audio.cjs.js.map +1 -1
  9. package/dist/audio.d.ts +4 -0
  10. package/dist/audio.esm.js +66 -16
  11. package/dist/audio.esm.js.map +1 -1
  12. package/dist/core.cjs.js +310 -84
  13. package/dist/core.cjs.js.map +1 -1
  14. package/dist/core.d.ts +60 -1
  15. package/dist/core.esm.js +311 -85
  16. package/dist/core.esm.js.map +1 -1
  17. package/dist/debug.cjs.js +36 -68
  18. package/dist/debug.cjs.js.map +1 -1
  19. package/dist/debug.d.ts +4 -6
  20. package/dist/debug.esm.js +36 -68
  21. package/dist/debug.esm.js.map +1 -1
  22. package/dist/index.cjs.js +1250 -251
  23. package/dist/index.cjs.js.map +1 -1
  24. package/dist/index.d.ts +386 -41
  25. package/dist/index.esm.js +1250 -254
  26. package/dist/index.esm.js.map +1 -1
  27. package/dist/ui.cjs.js +757 -1
  28. package/dist/ui.cjs.js.map +1 -1
  29. package/dist/ui.d.ts +208 -2
  30. package/dist/ui.esm.js +756 -2
  31. package/dist/ui.esm.js.map +1 -1
  32. package/dist/vite.cjs.js +65 -68
  33. package/dist/vite.cjs.js.map +1 -1
  34. package/dist/vite.d.ts +17 -23
  35. package/dist/vite.esm.js +66 -68
  36. package/dist/vite.esm.js.map +1 -1
  37. package/package.json +4 -5
  38. package/src/animation/SpriteAnimation.ts +210 -0
  39. package/src/animation/Tween.ts +27 -1
  40. package/src/animation/index.ts +2 -0
  41. package/src/audio/AudioManager.ts +64 -15
  42. package/src/core/EventEmitter.ts +7 -1
  43. package/src/core/GameApplication.ts +25 -7
  44. package/src/core/SceneManager.ts +3 -1
  45. package/src/debug/DevBridge.ts +49 -80
  46. package/src/index.ts +6 -0
  47. package/src/input/InputManager.ts +26 -0
  48. package/src/loading/CSSPreloader.ts +7 -33
  49. package/src/loading/LoadingScene.ts +17 -41
  50. package/src/loading/index.ts +1 -0
  51. package/src/loading/logo.ts +95 -0
  52. package/src/types.ts +4 -0
  53. package/src/ui/BalanceDisplay.ts +14 -0
  54. package/src/ui/Button.ts +1 -1
  55. package/src/ui/Layout.ts +364 -0
  56. package/src/ui/ScrollContainer.ts +557 -0
  57. package/src/ui/index.ts +4 -0
  58. package/src/viewport/ViewportManager.ts +2 -0
  59. package/src/vite/index.ts +83 -83
@@ -11,6 +11,7 @@ import { InputManager } from '../input/InputManager';
11
11
  import { ViewportManager } from '../viewport/ViewportManager';
12
12
  import { LoadingScene } from '../loading/LoadingScene';
13
13
  import { createCSSPreloader, removeCSSPreloader } from '../loading/CSSPreloader';
14
+ import { FPSOverlay } from '../debug/FPSOverlay';
14
15
 
15
16
  /**
16
17
  * The main entry point for a game built on @energy8platform/game-engine.
@@ -67,6 +68,9 @@ export class GameApplication extends EventEmitter<GameEngineEvents> {
67
68
  /** SDK instance (null in offline mode) */
68
69
  public sdk: CasinoGameSDK | null = null;
69
70
 
71
+ /** FPS overlay instance (only when debug: true) */
72
+ public fpsOverlay: FPSOverlay | null = null;
73
+
70
74
  /** Data received from SDK initialization */
71
75
  public initData: InitData | null = null;
72
76
 
@@ -154,7 +158,7 @@ export class GameApplication extends EventEmitter<GameEngineEvents> {
154
158
  // 6. Initialize sub-systems
155
159
  this.initSubSystems();
156
160
 
157
- this.emit('initialized', undefined as any);
161
+ this.emit('initialized');
158
162
 
159
163
  // 7. Remove CSS preloader, show Canvas loading screen
160
164
  removeCSSPreloader(this._container);
@@ -162,9 +166,11 @@ export class GameApplication extends EventEmitter<GameEngineEvents> {
162
166
  // 8. Load assets with loading screen
163
167
  await this.loadAssets(firstScene, sceneData);
164
168
 
169
+ this.emit('loaded');
170
+
165
171
  // 9. Start the game loop
166
172
  this._running = true;
167
- this.emit('started', undefined as any);
173
+ this.emit('started');
168
174
  } catch (err) {
169
175
  console.error('[GameEngine] Failed to start:', err);
170
176
  this.emit('error', err instanceof Error ? err : new Error(String(err)));
@@ -186,9 +192,9 @@ export class GameApplication extends EventEmitter<GameEngineEvents> {
186
192
  this.viewport?.destroy();
187
193
  this.sdk?.destroy();
188
194
  this.app?.destroy(true, { children: true, texture: true });
189
- this.removeAllListeners();
190
195
 
191
- this.emit('destroyed', undefined as any);
196
+ this.emit('destroyed');
197
+ this.removeAllListeners();
192
198
  }
193
199
 
194
200
  // ─── Private initialization steps ──────────────────────
@@ -280,9 +286,10 @@ export class GameApplication extends EventEmitter<GameEngineEvents> {
280
286
  // Wire SceneManager to the PixiJS stage
281
287
  this.scenes.setRoot(this.app.stage);
282
288
 
283
- // Wire viewport resize → scene manager
284
- this.viewport.on('resize', ({ width, height }) => {
289
+ // Wire viewport resize → scene manager + input manager
290
+ this.viewport.on('resize', ({ width, height, scale }) => {
285
291
  this.scenes.resize(width, height);
292
+ this.input.setViewportTransform(scale, this.app.stage.x, this.app.stage.y);
286
293
  this.emit('resize', { width, height });
287
294
  });
288
295
 
@@ -290,6 +297,11 @@ export class GameApplication extends EventEmitter<GameEngineEvents> {
290
297
  this.emit('orientationChange', orientation);
291
298
  });
292
299
 
300
+ // Wire scene changes → engine event
301
+ this.scenes.on('change', ({ from, to }) => {
302
+ this.emit('sceneChange', { from, to });
303
+ });
304
+
293
305
  // Connect ticker → scene updates
294
306
  this.app.ticker.add((ticker) => {
295
307
  // Always update scenes (loading screen needs onUpdate before _running=true)
@@ -298,11 +310,17 @@ export class GameApplication extends EventEmitter<GameEngineEvents> {
298
310
 
299
311
  // Trigger initial resize
300
312
  this.viewport.refresh();
313
+
314
+ // Enable FPS overlay in debug mode
315
+ if (this.config.debug) {
316
+ this.fpsOverlay = new FPSOverlay(this.app);
317
+ this.fpsOverlay.show();
318
+ }
301
319
  }
302
320
 
303
321
  private async loadAssets(firstScene: string, sceneData?: unknown): Promise<void> {
304
322
  // Register built-in loading scene
305
- this.scenes.register('__loading__', LoadingScene as any);
323
+ this.scenes.register('__loading__', LoadingScene);
306
324
 
307
325
  // Enter loading scene
308
326
  await this.scenes.goto('__loading__', {
@@ -190,9 +190,11 @@ export class SceneManager extends EventEmitter<SceneManagerEvents> {
190
190
  // Transition in
191
191
  await this.transitionIn(scene.container, transition);
192
192
 
193
+ // Push to stack BEFORE onEnter so currentKey is correct during initialization
194
+ this.stack.push({ scene, key });
195
+
193
196
  await scene.onEnter?.(data);
194
197
 
195
- this.stack.push({ scene, key });
196
198
  this._transitioning = false;
197
199
  }
198
200
 
@@ -1,8 +1,10 @@
1
- import type {
2
- InitData,
3
- GameConfigData,
4
- PlayResultData,
5
- SessionData,
1
+ import {
2
+ Bridge,
3
+ type BridgeMessageType,
4
+ type InitData,
5
+ type GameConfigData,
6
+ type PlayResultData,
7
+ type SessionData,
6
8
  } from '@energy8platform/game-sdk';
7
9
 
8
10
  export interface DevBridgeConfig {
@@ -44,8 +46,9 @@ const DEFAULT_CONFIG: Required<DevBridgeConfig> = {
44
46
  /**
45
47
  * Mock host bridge for local development.
46
48
  *
47
- * Intercepts postMessage communication from the SDK and responds
48
- * with mock data, simulating a real casino host environment.
49
+ * Uses the SDK's `Bridge` class in `devMode` to communicate with
50
+ * `CasinoGameSDK` via a shared in-memory `MemoryChannel`, removing
51
+ * the need for postMessage and iframes.
49
52
  *
50
53
  * This allows games to be developed and tested without a real backend.
51
54
  *
@@ -73,8 +76,7 @@ export class DevBridge {
73
76
  private _config: Required<DevBridgeConfig>;
74
77
  private _balance: number;
75
78
  private _roundCounter = 0;
76
- private _listening = false;
77
- private _handler: ((e: MessageEvent) => void) | null = null;
79
+ private _bridge: Bridge | null = null;
78
80
 
79
81
  constructor(config: DevBridgeConfig = {}) {
80
82
  this._config = { ...DEFAULT_CONFIG, ...config };
@@ -88,27 +90,47 @@ export class DevBridge {
88
90
 
89
91
  /** Start listening for SDK messages */
90
92
  start(): void {
91
- if (this._listening) return;
93
+ if (this._bridge) return;
92
94
 
93
- this._handler = (e: MessageEvent) => {
94
- this.handleMessage(e);
95
- };
95
+ console.debug('[DevBridge] Starting with config:', this._config);
96
+
97
+ this._bridge = new Bridge({ devMode: true, debug: this._config.debug });
98
+
99
+ this._bridge.on('GAME_READY', (_payload: unknown, id?: string) => {
100
+ this.handleGameReady(id);
101
+ });
102
+
103
+ this._bridge.on('PLAY_REQUEST', (payload: { action: string; bet: number; roundId?: string }, id?: string) => {
104
+ this.handlePlayRequest(payload, id);
105
+ });
106
+
107
+ this._bridge.on('PLAY_RESULT_ACK', (payload: unknown) => {
108
+ this.handlePlayAck(payload);
109
+ });
110
+
111
+ this._bridge.on('GET_BALANCE', (_payload: unknown, id?: string) => {
112
+ this.handleGetBalance(id);
113
+ });
96
114
 
97
- window.addEventListener('message', this._handler);
98
- this._listening = true;
115
+ this._bridge.on('GET_STATE', (_payload: unknown, id?: string) => {
116
+ this.handleGetState(id);
117
+ });
118
+
119
+ this._bridge.on('OPEN_DEPOSIT', () => {
120
+ this.handleOpenDeposit();
121
+ });
99
122
 
100
123
  if (this._config.debug) {
101
- console.log('[DevBridge] Started — listening for SDK messages');
124
+ console.log('[DevBridge] Started — listening via Bridge (devMode)');
102
125
  }
103
126
  }
104
127
 
105
128
  /** Stop listening */
106
129
  stop(): void {
107
- if (this._handler) {
108
- window.removeEventListener('message', this._handler);
109
- this._handler = null;
130
+ if (this._bridge) {
131
+ this._bridge.destroy();
132
+ this._bridge = null;
110
133
  }
111
- this._listening = false;
112
134
 
113
135
  if (this._config.debug) {
114
136
  console.log('[DevBridge] Stopped');
@@ -118,8 +140,7 @@ export class DevBridge {
118
140
  /** Set mock balance */
119
141
  setBalance(balance: number): void {
120
142
  this._balance = balance;
121
- // Send balance update
122
- this.sendMessage('BALANCE_UPDATE', { balance: this._balance });
143
+ this._bridge?.send('BALANCE_UPDATE', { balance: this._balance });
123
144
  }
124
145
 
125
146
  /** Destroy the dev bridge */
@@ -129,42 +150,6 @@ export class DevBridge {
129
150
 
130
151
  // ─── Message Handling ──────────────────────────────────
131
152
 
132
- private handleMessage(e: MessageEvent): void {
133
- const data = e.data;
134
-
135
- // Only process bridge messages
136
- if (!data || data.__casino_bridge !== true) return;
137
-
138
- if (this._config.debug) {
139
- console.log('[DevBridge] ←', data.type, data.payload);
140
- }
141
-
142
- switch (data.type) {
143
- case 'GAME_READY':
144
- this.handleGameReady(data.id);
145
- break;
146
- case 'PLAY_REQUEST':
147
- this.handlePlayRequest(data.payload, data.id);
148
- break;
149
- case 'PLAY_RESULT_ACK':
150
- this.handlePlayAck(data.payload, data.id);
151
- break;
152
- case 'GET_BALANCE':
153
- this.handleGetBalance(data.id);
154
- break;
155
- case 'GET_STATE':
156
- this.handleGetState(data.id);
157
- break;
158
- case 'OPEN_DEPOSIT':
159
- this.handleOpenDeposit();
160
- break;
161
- default:
162
- if (this._config.debug) {
163
- console.log('[DevBridge] Unknown message type:', data.type);
164
- }
165
- }
166
- }
167
-
168
153
  private handleGameReady(id?: string): void {
169
154
  const initData: InitData = {
170
155
  balance: this._balance,
@@ -208,14 +193,14 @@ export class DevBridge {
208
193
  this.delayedSend('PLAY_RESULT', result, id);
209
194
  }
210
195
 
211
- private handlePlayAck(_payload: unknown, _id?: string): void {
196
+ private handlePlayAck(_payload: unknown): void {
212
197
  if (this._config.debug) {
213
198
  console.log('[DevBridge] Play acknowledged');
214
199
  }
215
200
  }
216
201
 
217
202
  private handleGetBalance(id?: string): void {
218
- this.delayedSend('BALANCE_RESPONSE', { balance: this._balance }, id);
203
+ this.delayedSend('BALANCE_UPDATE', { balance: this._balance }, id);
219
204
  }
220
205
 
221
206
  private handleGetState(id?: string): void {
@@ -227,33 +212,17 @@ export class DevBridge {
227
212
  console.log('[DevBridge] 💰 Open deposit requested (mock: adding 1000)');
228
213
  }
229
214
  this._balance += 1000;
230
- this.sendMessage('BALANCE_UPDATE', { balance: this._balance });
215
+ this._bridge?.send('BALANCE_UPDATE', { balance: this._balance });
231
216
  }
232
217
 
233
218
  // ─── Communication ─────────────────────────────────────
234
219
 
235
- private delayedSend(type: string, payload: unknown, id?: string): void {
220
+ private delayedSend(type: BridgeMessageType, payload: unknown, id?: string): void {
236
221
  const delay = this._config.networkDelay;
237
222
  if (delay > 0) {
238
- setTimeout(() => this.sendMessage(type, payload, id), delay);
223
+ setTimeout(() => this._bridge?.send(type, payload, id), delay);
239
224
  } else {
240
- this.sendMessage(type, payload, id);
225
+ this._bridge?.send(type, payload, id);
241
226
  }
242
227
  }
243
-
244
- private sendMessage(type: string, payload: unknown, id?: string): void {
245
- const message = {
246
- __casino_bridge: true,
247
- type,
248
- payload,
249
- id,
250
- };
251
-
252
- if (this._config.debug) {
253
- console.log('[DevBridge] →', type, payload);
254
- }
255
-
256
- // Post to the same window (SDK listens on window)
257
- window.postMessage(message, '*');
258
- }
259
228
  }
package/src/index.ts CHANGED
@@ -48,6 +48,8 @@ export { Tween } from './animation/Tween';
48
48
  export { Timeline } from './animation/Timeline';
49
49
  export { Easing } from './animation/Easing';
50
50
  export { SpineHelper } from './animation/SpineHelper';
51
+ export { SpriteAnimation } from './animation/SpriteAnimation';
52
+ export type { SpriteAnimationConfig } from './animation/SpriteAnimation';
51
53
 
52
54
  // ─── Input ───────────────────────────────────────────────
53
55
  export { InputManager } from './input/InputManager';
@@ -61,6 +63,10 @@ export { BalanceDisplay } from './ui/BalanceDisplay';
61
63
  export { WinDisplay } from './ui/WinDisplay';
62
64
  export { Modal } from './ui/Modal';
63
65
  export { Toast } from './ui/Toast';
66
+ export { Layout } from './ui/Layout';
67
+ export type { LayoutConfig, LayoutDirection, LayoutAlignment, LayoutAnchor } from './ui/Layout';
68
+ export { ScrollContainer } from './ui/ScrollContainer';
69
+ export type { ScrollContainerConfig, ScrollDirection } from './ui/ScrollContainer';
64
70
 
65
71
  // ─── Loading ─────────────────────────────────────────────
66
72
  export { LoadingScene } from './loading/LoadingScene';
@@ -41,6 +41,11 @@ export class InputManager extends EventEmitter<InputEvents> {
41
41
  private _keysDown = new Set<string>();
42
42
  private _destroyed = false;
43
43
 
44
+ // Viewport transform (set by ViewportManager via setViewportTransform)
45
+ private _viewportScale = 1;
46
+ private _viewportOffsetX = 0;
47
+ private _viewportOffsetY = 0;
48
+
44
49
  // Gesture tracking
45
50
  private _pointerStart: { x: number; y: number; time: number } | null = null;
46
51
  private _swipeThreshold = 50; // minimum distance in px
@@ -73,6 +78,27 @@ export class InputManager extends EventEmitter<InputEvents> {
73
78
  return this._keysDown.has(key.toLowerCase());
74
79
  }
75
80
 
81
+ /**
82
+ * Update the viewport transform used for DOM→world coordinate mapping.
83
+ * Called automatically by GameApplication when ViewportManager emits resize.
84
+ */
85
+ setViewportTransform(scale: number, offsetX: number, offsetY: number): void {
86
+ this._viewportScale = scale;
87
+ this._viewportOffsetX = offsetX;
88
+ this._viewportOffsetY = offsetY;
89
+ }
90
+
91
+ /**
92
+ * Convert a DOM canvas position to game-world coordinates,
93
+ * accounting for viewport scaling and offset.
94
+ */
95
+ getWorldPosition(canvasX: number, canvasY: number): { x: number; y: number } {
96
+ return {
97
+ x: (canvasX - this._viewportOffsetX) / this._viewportScale,
98
+ y: (canvasY - this._viewportOffsetY) / this._viewportScale,
99
+ };
100
+ }
101
+
76
102
  /** Destroy the input manager */
77
103
  destroy(): void {
78
104
  this._destroyed = true;
@@ -1,4 +1,5 @@
1
1
  import type { LoadingScreenConfig } from '../types';
2
+ import { buildLogoSVG } from './logo';
2
3
 
3
4
  const PRELOADER_ID = '__ge-css-preloader__';
4
5
 
@@ -6,39 +7,12 @@ const PRELOADER_ID = '__ge-css-preloader__';
6
7
  * Inline SVG logo with animated loader bar.
7
8
  * The `#loader` path acts as the progress fill — animated via clipPath.
8
9
  */
9
- const LOGO_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 250 200" fill="none" class="ge-logo-svg">
10
- <path d="m241 81.75h-19.28c-1.77 0-6.73 4.98-7.43 6.99l-4.36 12.22c-0.49 1.37 0.05 2.92 1.06 4.32-2.07 1.19-3.69 3.08-4.36 5.43l-3.25 10.41c-0.86 2.89 2.39 6.63 4.31 6.63h19.28c1.96 0 7.4-5.56 7.96-7.51l2.96-10.22c0.63-2.25 0.1-3.98-1.22-4.99 2.55-1.56 3.86-4.14 4.55-6.31l2.77-9.31c0.74-2.57-1.37-7.66-2.99-7.66zm-13.36 28.31-2.27 7.03h-8.28l2.58-8.28h8.28l-0.31 1.25zm4.06-16.97-2.11 6.7h-7.04l2.25-7.34h7.26l-0.36 0.64z" fill="url(#pl0)"/>
11
- <path d="m202.5 81.75-9.31 14.97-2.32-14.97h-11.82l4.32 25.15-0.57 4.91-8.64 26.44 15.31-12.76 5.63-16.48 19.96-27.26h-12.56z" fill="url(#pl1)"/>
12
- <path d="m174.2 81.75h-19.78l-5.75 5.16-10.79 33.2c-0.77 2.53 2.48 6.93 4.87 6.93h17.38c2.63 0 7.85-5.34 8.32-6.83l5.37-18.14h-15.17l-2.2 7.64h3.78l-2.25 7.2h-8.01l7.1-25.52h7.58l-1.48 8.4 12.78-5.98c1.28-0.63 1.97-3.99 1.61-6.61-0.36-2.34-1.64-5.45-3.36-5.45z" fill="url(#pl2)"/>
13
- <path d="m140.6 81.75h-70.6l-5.36 19.37-4.26-19.37h-46.76l2.95 5.88-10.58 39.28h26.84l2.95-9.52-15.63-0.13 2.55-8.34h8.74l8.47-9.81h-14.61l2.11-7.3h15.47l2.54-8.71 2.58 4.74-11.4 39.07h11.05l6.46-21.49 8.84 36.33 19.18-55.67-1.83-3.36 3.68 4.09-12.07 40.1h28.18l3.39-10.31h-17.01l2.67-8.03h9.98l7.58-9.52h-14.28l1.93-6.6h14.61l3.25-9.73 2.81 5.12-11.3 38.89h11.05l5.23-17.81h1.62l1.48 17.6h10.69l-1.48-16.81c4.75-1.28 7.52-5.9 8.64-9.81l2.95-11.3c0.86-2.73-1.43-6.85-3.3-6.85zm-9.8 17.3h-8.69l2.54-7.84h8.35l-2.2 7.84z" fill="url(#pl3)"/>
14
- <path d="m205.9 148.9h-122.6l-2.61-3.12h-32.4l-2.51 3.12h-1.59c-5.34 0-7.94 4.88-7.94 7.65v0.03c0 4.2 3.55 7.6 7.74 7.6h103.6l2.11 3.12h36.09l1.82-3.12h18.3c5.25 0 6.64-5.3 6.64-7.35v-0.25c0-4.23-2.9-7.68-6.64-7.68zm-0.7 12.83h-160.6c-3.69 0-6.11-2.58-6.11-5.47v-0.03c0-2.89 2.1-5.47 5.61-5.47h161.1c3.45 0 4.89 3.12 4.89 5.65v0.17c0 2.57-2.11 5.15-4.89 5.15z" fill="url(#pl4)"/>
15
- <!-- Loader fill with clip for progress animation -->
16
- <clipPath id="ge-loader-clip">
17
- <rect x="37" y="148" width="0" height="20" class="ge-clip-rect"/>
18
- </clipPath>
19
- <path d="m204.5 152.6h-159.8c-2.78 0-4.45 1.69-4.45 3.99v0.11c0 2.04 1.42 3.43 3.64 3.43h160.6c2.88 0 3.67-2.07 3.67-3.43v-0.25c0-2.04-1.48-3.85-3.67-3.85z" fill="url(#pl5)" clip-path="url(#ge-loader-clip)"/>
20
- <text x="125" y="196" text-anchor="middle" fill="rgba(255,255,255,0.6)" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif" font-size="8" font-weight="600" letter-spacing="1.5" class="ge-preloader-svg-text">Loading...</text>
21
- <defs>
22
- <linearGradient id="pl0" x1="223.7" x2="223.7" y1="81.75" y2="127.8" gradientUnits="userSpaceOnUse">
23
- <stop stop-color="#663BA6"/><stop stop-color="#7939C2" offset=".349"/><stop stop-color="#8A2FC0" offset=".6615"/><stop stop-color="#791BA3" offset="1"/>
24
- </linearGradient>
25
- <linearGradient id="pl1" x1="194.6" x2="194.6" y1="81.75" y2="138.3" gradientUnits="userSpaceOnUse">
26
- <stop stop-color="#663BA6"/><stop stop-color="#7939C2" offset=".349"/><stop stop-color="#8A2FC0" offset=".6615"/><stop stop-color="#791BA3" offset="1"/>
27
- </linearGradient>
28
- <linearGradient id="pl2" x1="157.8" x2="157.8" y1="81.75" y2="127" gradientUnits="userSpaceOnUse">
29
- <stop stop-color="#663BA6"/><stop stop-color="#7939C2" offset=".349"/><stop stop-color="#8A2FC0" offset=".6615"/><stop stop-color="#791BA3" offset="1"/>
30
- </linearGradient>
31
- <linearGradient id="pl3" x1="79.96" x2="79.96" y1="81.75" y2="141.8" gradientUnits="userSpaceOnUse">
32
- <stop stop-color="#663BA6"/><stop stop-color="#7939C2" offset=".349"/><stop stop-color="#8A2FC0" offset=".6615"/><stop stop-color="#791BA3" offset="1"/>
33
- </linearGradient>
34
- <linearGradient id="pl4" x1="36.18" x2="212.5" y1="156.6" y2="156.6" gradientUnits="userSpaceOnUse">
35
- <stop stop-color="#316FB0"/><stop stop-color="#1FCDE6" offset=".5"/><stop stop-color="#29FEE7" offset="1"/>
36
- </linearGradient>
37
- <linearGradient id="pl5" x1="40.27" x2="208.2" y1="156.4" y2="156.4" gradientUnits="userSpaceOnUse">
38
- <stop stop-color="#316FB0"/><stop stop-color="#1FCDE6" offset=".5"/><stop stop-color="#29FEE7" offset="1"/>
39
- </linearGradient>
40
- </defs>
41
- </svg>`;
10
+ const LOGO_SVG = buildLogoSVG({
11
+ idPrefix: 'pl',
12
+ svgClass: 'ge-logo-svg',
13
+ clipRectClass: 'ge-clip-rect',
14
+ textClass: 'ge-preloader-svg-text',
15
+ });
42
16
 
43
17
  /**
44
18
  * Creates a lightweight CSS-only preloader that appears instantly,
@@ -3,49 +3,22 @@ import { Scene } from '../core/Scene';
3
3
  import { Tween } from '../animation/Tween';
4
4
  import { Easing } from '../animation/Easing';
5
5
  import type { LoadingScreenConfig } from '../types';
6
+ import { buildLogoSVG, LOADER_BAR_MAX_WIDTH } from './logo';
6
7
 
7
8
  /**
8
- * Inline SVG logo with a loader bar (clip-animated for progress).
9
- * The clipPath rect width is set to 0 initially, expanded as loading progresses.
9
+ * Build the loading scene variant of the logo SVG.
10
+ * Uses unique IDs (prefixed with 'ls') to avoid collisions with CSSPreloader.
10
11
  */
11
- function buildLogoSVG(): string {
12
- return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 250 200" fill="none" style="width:100%;height:auto;">
13
- <path d="m241 81.75h-19.28c-1.77 0-6.73 4.98-7.43 6.99l-4.36 12.22c-0.49 1.37 0.05 2.92 1.06 4.32-2.07 1.19-3.69 3.08-4.36 5.43l-3.25 10.41c-0.86 2.89 2.39 6.63 4.31 6.63h19.28c1.96 0 7.4-5.56 7.96-7.51l2.96-10.22c0.63-2.25 0.1-3.98-1.22-4.99 2.55-1.56 3.86-4.14 4.55-6.31l2.77-9.31c0.74-2.57-1.37-7.66-2.99-7.66zm-13.36 28.31-2.27 7.03h-8.28l2.58-8.28h8.28l-0.31 1.25zm4.06-16.97-2.11 6.7h-7.04l2.25-7.34h7.26l-0.36 0.64z" fill="url(#ls0)"/>
14
- <path d="m202.5 81.75-9.31 14.97-2.32-14.97h-11.82l4.32 25.15-0.57 4.91-8.64 26.44 15.31-12.76 5.63-16.48 19.96-27.26h-12.56z" fill="url(#ls1)"/>
15
- <path d="m174.2 81.75h-19.78l-5.75 5.16-10.79 33.2c-0.77 2.53 2.48 6.93 4.87 6.93h17.38c2.63 0 7.85-5.34 8.32-6.83l5.37-18.14h-15.17l-2.2 7.64h3.78l-2.25 7.2h-8.01l7.1-25.52h7.58l-1.48 8.4 12.78-5.98c1.28-0.63 1.97-3.99 1.61-6.61-0.36-2.34-1.64-5.45-3.36-5.45z" fill="url(#ls2)"/>
16
- <path d="m140.6 81.75h-70.6l-5.36 19.37-4.26-19.37h-46.76l2.95 5.88-10.58 39.28h26.84l2.95-9.52-15.63-0.13 2.55-8.34h8.74l8.47-9.81h-14.61l2.11-7.3h15.47l2.54-8.71 2.58 4.74-11.4 39.07h11.05l6.46-21.49 8.84 36.33 19.18-55.67-1.83-3.36 3.68 4.09-12.07 40.1h28.18l3.39-10.31h-17.01l2.67-8.03h9.98l7.58-9.52h-14.28l1.93-6.6h14.61l3.25-9.73 2.81 5.12-11.3 38.89h11.05l5.23-17.81h1.62l1.48 17.6h10.69l-1.48-16.81c4.75-1.28 7.52-5.9 8.64-9.81l2.95-11.3c0.86-2.73-1.43-6.85-3.3-6.85zm-9.8 17.3h-8.69l2.54-7.84h8.35l-2.2 7.84z" fill="url(#ls3)"/>
17
- <path d="m205.9 148.9h-122.6l-2.61-3.12h-32.4l-2.51 3.12h-1.59c-5.34 0-7.94 4.88-7.94 7.65v0.03c0 4.2 3.55 7.6 7.74 7.6h103.6l2.11 3.12h36.09l1.82-3.12h18.3c5.25 0 6.64-5.3 6.64-7.35v-0.25c0-4.23-2.9-7.68-6.64-7.68zm-0.7 12.83h-160.6c-3.69 0-6.11-2.58-6.11-5.47v-0.03c0-2.89 2.1-5.47 5.61-5.47h161.1c3.45 0 4.89 3.12 4.89 5.65v0.17c0 2.57-2.11 5.15-4.89 5.15z" fill="url(#ls4)"/>
18
- <clipPath id="ge-canvas-loader-clip">
19
- <rect id="ge-loader-rect" x="37" y="148" width="0" height="20"/>
20
- </clipPath>
21
- <path d="m204.5 152.6h-159.8c-2.78 0-4.45 1.69-4.45 3.99v0.11c0 2.04 1.42 3.43 3.64 3.43h160.6c2.88 0 3.67-2.07 3.67-3.43v-0.25c0-2.04-1.48-3.85-3.67-3.85z" fill="url(#ls5)" clip-path="url(#ge-canvas-loader-clip)"/>
22
- <text id="ge-loader-pct" x="125" y="196" text-anchor="middle" fill="rgba(255,255,255,0.7)" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif" font-size="8" font-weight="600" letter-spacing="1.5">0%</text>
23
- <defs>
24
- <linearGradient id="ls0" x1="223.7" x2="223.7" y1="81.75" y2="127.8" gradientUnits="userSpaceOnUse">
25
- <stop stop-color="#663BA6"/><stop stop-color="#7939C2" offset=".349"/><stop stop-color="#8A2FC0" offset=".6615"/><stop stop-color="#791BA3" offset="1"/>
26
- </linearGradient>
27
- <linearGradient id="ls1" x1="194.6" x2="194.6" y1="81.75" y2="138.3" gradientUnits="userSpaceOnUse">
28
- <stop stop-color="#663BA6"/><stop stop-color="#7939C2" offset=".349"/><stop stop-color="#8A2FC0" offset=".6615"/><stop stop-color="#791BA3" offset="1"/>
29
- </linearGradient>
30
- <linearGradient id="ls2" x1="157.8" x2="157.8" y1="81.75" y2="127" gradientUnits="userSpaceOnUse">
31
- <stop stop-color="#663BA6"/><stop stop-color="#7939C2" offset=".349"/><stop stop-color="#8A2FC0" offset=".6615"/><stop stop-color="#791BA3" offset="1"/>
32
- </linearGradient>
33
- <linearGradient id="ls3" x1="79.96" x2="79.96" y1="81.75" y2="141.8" gradientUnits="userSpaceOnUse">
34
- <stop stop-color="#663BA6"/><stop stop-color="#7939C2" offset=".349"/><stop stop-color="#8A2FC0" offset=".6615"/><stop stop-color="#791BA3" offset="1"/>
35
- </linearGradient>
36
- <linearGradient id="ls4" x1="36.18" x2="212.5" y1="156.6" y2="156.6" gradientUnits="userSpaceOnUse">
37
- <stop stop-color="#316FB0"/><stop stop-color="#1FCDE6" offset=".5"/><stop stop-color="#29FEE7" offset="1"/>
38
- </linearGradient>
39
- <linearGradient id="ls5" x1="40.27" x2="208.2" y1="156.4" y2="156.4" gradientUnits="userSpaceOnUse">
40
- <stop stop-color="#316FB0"/><stop stop-color="#1FCDE6" offset=".5"/><stop stop-color="#29FEE7" offset="1"/>
41
- </linearGradient>
42
- </defs>
43
- </svg>`;
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
+ });
44
20
  }
45
21
 
46
- /** Max width of the loader bar in SVG units */
47
- const LOADER_BAR_MAX_WIDTH = 174;
48
-
49
22
  interface LoadingSceneData {
50
23
  engine: any; // GameApplication — avoid circular import
51
24
  targetScene: string;
@@ -194,7 +167,7 @@ export class LoadingScene extends Scene {
194
167
  this._overlay.id = '__ge-loading-overlay__';
195
168
  this._overlay.innerHTML = `
196
169
  <div class="ge-loading-content">
197
- ${buildLogoSVG()}
170
+ ${buildLoadingLogoSVG()}
198
171
  </div>
199
172
  `;
200
173
 
@@ -350,7 +323,10 @@ export class LoadingScene extends Scene {
350
323
  // Remove overlay
351
324
  this.removeOverlay();
352
325
 
353
- // Navigate to the target scene
354
- await this._engine.scenes.goto(this._targetScene, this._targetData);
326
+ // Navigate to the target scene, always passing the engine reference
327
+ await this._engine.scenes.goto(this._targetScene, {
328
+ engine: this._engine,
329
+ ...(this._targetData && typeof this._targetData === 'object' ? this._targetData as Record<string, unknown> : { data: this._targetData }),
330
+ });
355
331
  }
356
332
  }
@@ -1,2 +1,3 @@
1
1
  export { LoadingScene } from './LoadingScene';
2
2
  export { createCSSPreloader, removeCSSPreloader } from './CSSPreloader';
3
+ export { buildLogoSVG, LOADER_BAR_MAX_WIDTH } from './logo';
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Shared Energy8 SVG logo with an embedded loader bar.
3
+ *
4
+ * The loader bar fill is controlled via a `<clipPath>` whose `<rect>` width
5
+ * is animatable. Different consumers customise gradient IDs and the clip
6
+ * element's ID/class to avoid collisions when both CSSPreloader and
7
+ * LoadingScene appear in the same DOM.
8
+ */
9
+
10
+ /** SVG path data for the Energy8 wordmark — reused across loaders */
11
+ const WORDMARK_PATHS = `
12
+ <path d="m241 81.75h-19.28c-1.77 0-6.73 4.98-7.43 6.99l-4.36 12.22c-0.49 1.37 0.05 2.92 1.06 4.32-2.07 1.19-3.69 3.08-4.36 5.43l-3.25 10.41c-0.86 2.89 2.39 6.63 4.31 6.63h19.28c1.96 0 7.4-5.56 7.96-7.51l2.96-10.22c0.63-2.25 0.1-3.98-1.22-4.99 2.55-1.56 3.86-4.14 4.55-6.31l2.77-9.31c0.74-2.57-1.37-7.66-2.99-7.66zm-13.36 28.31-2.27 7.03h-8.28l2.58-8.28h8.28l-0.31 1.25zm4.06-16.97-2.11 6.7h-7.04l2.25-7.34h7.26l-0.36 0.64z" fill="url(#GID0)"/>
13
+ <path d="m202.5 81.75-9.31 14.97-2.32-14.97h-11.82l4.32 25.15-0.57 4.91-8.64 26.44 15.31-12.76 5.63-16.48 19.96-27.26h-12.56z" fill="url(#GID1)"/>
14
+ <path d="m174.2 81.75h-19.78l-5.75 5.16-10.79 33.2c-0.77 2.53 2.48 6.93 4.87 6.93h17.38c2.63 0 7.85-5.34 8.32-6.83l5.37-18.14h-15.17l-2.2 7.64h3.78l-2.25 7.2h-8.01l7.1-25.52h7.58l-1.48 8.4 12.78-5.98c1.28-0.63 1.97-3.99 1.61-6.61-0.36-2.34-1.64-5.45-3.36-5.45z" fill="url(#GID2)"/>
15
+ <path d="m140.6 81.75h-70.6l-5.36 19.37-4.26-19.37h-46.76l2.95 5.88-10.58 39.28h26.84l2.95-9.52-15.63-0.13 2.55-8.34h8.74l8.47-9.81h-14.61l2.11-7.3h15.47l2.54-8.71 2.58 4.74-11.4 39.07h11.05l6.46-21.49 8.84 36.33 19.18-55.67-1.83-3.36 3.68 4.09-12.07 40.1h28.18l3.39-10.31h-17.01l2.67-8.03h9.98l7.58-9.52h-14.28l1.93-6.6h14.61l3.25-9.73 2.81 5.12-11.3 38.89h11.05l5.23-17.81h1.62l1.48 17.6h10.69l-1.48-16.81c4.75-1.28 7.52-5.9 8.64-9.81l2.95-11.3c0.86-2.73-1.43-6.85-3.3-6.85zm-9.8 17.3h-8.69l2.54-7.84h8.35l-2.2 7.84z" fill="url(#GID3)"/>
16
+ <path d="m205.9 148.9h-122.6l-2.61-3.12h-32.4l-2.51 3.12h-1.59c-5.34 0-7.94 4.88-7.94 7.65v0.03c0 4.2 3.55 7.6 7.74 7.6h103.6l2.11 3.12h36.09l1.82-3.12h18.3c5.25 0 6.64-5.3 6.64-7.35v-0.25c0-4.23-2.9-7.68-6.64-7.68zm-0.7 12.83h-160.6c-3.69 0-6.11-2.58-6.11-5.47v-0.03c0-2.89 2.1-5.47 5.61-5.47h161.1c3.45 0 4.89 3.12 4.89 5.65v0.17c0 2.57-2.11 5.15-4.89 5.15z" fill="url(#GID4)"/>`;
17
+
18
+ /** Gradient definitions template (gradient IDs are replaced per-consumer) */
19
+ const GRADIENT_DEFS = `
20
+ <linearGradient id="GID0" x1="223.7" x2="223.7" y1="81.75" y2="127.8" gradientUnits="userSpaceOnUse">
21
+ <stop stop-color="#663BA6"/><stop stop-color="#7939C2" offset=".349"/><stop stop-color="#8A2FC0" offset=".6615"/><stop stop-color="#791BA3" offset="1"/>
22
+ </linearGradient>
23
+ <linearGradient id="GID1" x1="194.6" x2="194.6" y1="81.75" y2="138.3" gradientUnits="userSpaceOnUse">
24
+ <stop stop-color="#663BA6"/><stop stop-color="#7939C2" offset=".349"/><stop stop-color="#8A2FC0" offset=".6615"/><stop stop-color="#791BA3" offset="1"/>
25
+ </linearGradient>
26
+ <linearGradient id="GID2" x1="157.8" x2="157.8" y1="81.75" y2="127" gradientUnits="userSpaceOnUse">
27
+ <stop stop-color="#663BA6"/><stop stop-color="#7939C2" offset=".349"/><stop stop-color="#8A2FC0" offset=".6615"/><stop stop-color="#791BA3" offset="1"/>
28
+ </linearGradient>
29
+ <linearGradient id="GID3" x1="79.96" x2="79.96" y1="81.75" y2="141.8" gradientUnits="userSpaceOnUse">
30
+ <stop stop-color="#663BA6"/><stop stop-color="#7939C2" offset=".349"/><stop stop-color="#8A2FC0" offset=".6615"/><stop stop-color="#791BA3" offset="1"/>
31
+ </linearGradient>
32
+ <linearGradient id="GID4" x1="36.18" x2="212.5" y1="156.6" y2="156.6" gradientUnits="userSpaceOnUse">
33
+ <stop stop-color="#316FB0"/><stop stop-color="#1FCDE6" offset=".5"/><stop stop-color="#29FEE7" offset="1"/>
34
+ </linearGradient>
35
+ <linearGradient id="GID5" x1="40.27" x2="208.2" y1="156.4" y2="156.4" gradientUnits="userSpaceOnUse">
36
+ <stop stop-color="#316FB0"/><stop stop-color="#1FCDE6" offset=".5"/><stop stop-color="#29FEE7" offset="1"/>
37
+ </linearGradient>`;
38
+
39
+ /** Max width of the loader bar in SVG units */
40
+ export const LOADER_BAR_MAX_WIDTH = 174;
41
+
42
+ interface LogoSVGOptions {
43
+ /** Prefix for gradient/clip IDs to avoid collisions (e.g. 'pl' or 'ls') */
44
+ idPrefix: string;
45
+ /** Optional CSS class on the root <svg> */
46
+ svgClass?: string;
47
+ /** Optional inline style on the root <svg> */
48
+ svgStyle?: string;
49
+ /** Optional CSS class on the clip <rect> */
50
+ clipRectClass?: string;
51
+ /** Optional id on the clip <rect> (for JS access) */
52
+ clipRectId?: string;
53
+ /** Optional id on the percentage <text> */
54
+ textId?: string;
55
+ /** Default text content */
56
+ textContent?: string;
57
+ /** Optional CSS class on the <text> */
58
+ textClass?: string;
59
+ }
60
+
61
+ /**
62
+ * Build the Energy8 SVG logo with a loader bar, using unique IDs.
63
+ *
64
+ * @param opts - Configuration to avoid element ID collisions
65
+ * @returns SVG markup string
66
+ */
67
+ export function buildLogoSVG(opts: LogoSVGOptions): string {
68
+ const { idPrefix, svgClass, svgStyle, clipRectClass, clipRectId, textId, textContent, textClass } = opts;
69
+
70
+ // Replace gradient ID placeholders with prefixed versions
71
+ const paths = WORDMARK_PATHS.replace(/GID(\d)/g, `${idPrefix}$1`);
72
+ const defs = GRADIENT_DEFS.replace(/GID(\d)/g, `${idPrefix}$1`);
73
+
74
+ const clipId = `${idPrefix}-loader-clip`;
75
+ const fillGradientId = `${idPrefix}5`;
76
+
77
+ const classAttr = svgClass ? ` class="${svgClass}"` : '';
78
+ const styleAttr = svgStyle ? ` style="${svgStyle}"` : '';
79
+ const rectClassAttr = clipRectClass ? ` class="${clipRectClass}"` : '';
80
+ const rectIdAttr = clipRectId ? ` id="${clipRectId}"` : '';
81
+ const txtIdAttr = textId ? ` id="${textId}"` : '';
82
+ const txtClassAttr = textClass ? ` class="${textClass}"` : '';
83
+
84
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 250 200" fill="none"${classAttr}${styleAttr}>
85
+ ${paths}
86
+ <clipPath id="${clipId}">
87
+ <rect${rectIdAttr} x="37" y="148" width="0" height="20"${rectClassAttr}/>
88
+ </clipPath>
89
+ <path d="m204.5 152.6h-159.8c-2.78 0-4.45 1.69-4.45 3.99v0.11c0 2.04 1.42 3.43 3.64 3.43h160.6c2.88 0 3.67-2.07 3.67-3.43v-0.25c0-2.04-1.48-3.85-3.67-3.85z" fill="url(#${fillGradientId})" clip-path="url(#${clipId})"/>
90
+ <text${txtIdAttr} x="125" y="196" text-anchor="middle" fill="rgba(255,255,255,0.6)" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif" font-size="8" font-weight="600" letter-spacing="1.5"${txtClassAttr}>${textContent ?? 'Loading...'}</text>
91
+ <defs>
92
+ ${defs}
93
+ </defs>
94
+ </svg>`;
95
+ }
package/src/types.ts CHANGED
@@ -121,6 +121,8 @@ export interface GameApplicationConfig {
121
121
  parentOrigin?: string;
122
122
  timeout?: number;
123
123
  debug?: boolean;
124
+ /** Use in-memory channel instead of postMessage (no iframe required) */
125
+ devMode?: boolean;
124
126
  }
125
127
  | false;
126
128
 
@@ -135,6 +137,8 @@ export interface GameApplicationConfig {
135
137
 
136
138
  export interface SceneConstructor {
137
139
  new (): IScene;
140
+ // Allow Scene subclasses to be used as constructors
141
+ [key: string]: any;
138
142
  }
139
143
 
140
144
  export interface IScene {