@energy8platform/game-engine 0.1.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/README.md +1134 -0
- package/dist/animation.cjs.js +505 -0
- package/dist/animation.cjs.js.map +1 -0
- package/dist/animation.d.ts +235 -0
- package/dist/animation.esm.js +500 -0
- package/dist/animation.esm.js.map +1 -0
- package/dist/assets.cjs.js +148 -0
- package/dist/assets.cjs.js.map +1 -0
- package/dist/assets.d.ts +97 -0
- package/dist/assets.esm.js +146 -0
- package/dist/assets.esm.js.map +1 -0
- package/dist/audio.cjs.js +345 -0
- package/dist/audio.cjs.js.map +1 -0
- package/dist/audio.d.ts +135 -0
- package/dist/audio.esm.js +343 -0
- package/dist/audio.esm.js.map +1 -0
- package/dist/core.cjs.js +1832 -0
- package/dist/core.cjs.js.map +1 -0
- package/dist/core.d.ts +633 -0
- package/dist/core.esm.js +1827 -0
- package/dist/core.esm.js.map +1 -0
- package/dist/debug.cjs.js +298 -0
- package/dist/debug.cjs.js.map +1 -0
- package/dist/debug.d.ts +114 -0
- package/dist/debug.esm.js +295 -0
- package/dist/debug.esm.js.map +1 -0
- package/dist/index.cjs.js +3623 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +1607 -0
- package/dist/index.esm.js +3598 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/ui.cjs.js +1081 -0
- package/dist/ui.cjs.js.map +1 -0
- package/dist/ui.d.ts +387 -0
- package/dist/ui.esm.js +1072 -0
- package/dist/ui.esm.js.map +1 -0
- package/dist/vite.cjs.js +125 -0
- package/dist/vite.cjs.js.map +1 -0
- package/dist/vite.d.ts +42 -0
- package/dist/vite.esm.js +122 -0
- package/dist/vite.esm.js.map +1 -0
- package/package.json +107 -0
- package/src/animation/Easing.ts +116 -0
- package/src/animation/SpineHelper.ts +162 -0
- package/src/animation/Timeline.ts +138 -0
- package/src/animation/Tween.ts +225 -0
- package/src/animation/index.ts +4 -0
- package/src/assets/AssetManager.ts +174 -0
- package/src/assets/index.ts +2 -0
- package/src/audio/AudioManager.ts +366 -0
- package/src/audio/index.ts +1 -0
- package/src/core/EventEmitter.ts +47 -0
- package/src/core/GameApplication.ts +306 -0
- package/src/core/Scene.ts +48 -0
- package/src/core/SceneManager.ts +258 -0
- package/src/core/index.ts +4 -0
- package/src/debug/DevBridge.ts +259 -0
- package/src/debug/FPSOverlay.ts +102 -0
- package/src/debug/index.ts +3 -0
- package/src/index.ts +71 -0
- package/src/input/InputManager.ts +171 -0
- package/src/input/index.ts +1 -0
- package/src/loading/CSSPreloader.ts +155 -0
- package/src/loading/LoadingScene.ts +356 -0
- package/src/loading/index.ts +2 -0
- package/src/state/StateMachine.ts +228 -0
- package/src/state/index.ts +1 -0
- package/src/types.ts +218 -0
- package/src/ui/BalanceDisplay.ts +155 -0
- package/src/ui/Button.ts +199 -0
- package/src/ui/Label.ts +111 -0
- package/src/ui/Modal.ts +134 -0
- package/src/ui/Panel.ts +125 -0
- package/src/ui/ProgressBar.ts +121 -0
- package/src/ui/Toast.ts +124 -0
- package/src/ui/WinDisplay.ts +133 -0
- package/src/ui/index.ts +16 -0
- package/src/viewport/ViewportManager.ts +241 -0
- package/src/viewport/index.ts +1 -0
- package/src/vite/index.ts +153 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { Application, Assets, Ticker } from 'pixi.js';
|
|
2
|
+
import { CasinoGameSDK } from '@energy8platform/game-sdk';
|
|
3
|
+
import type { InitData, GameConfigData, SessionData } from '@energy8platform/game-sdk';
|
|
4
|
+
import type { GameApplicationConfig, GameEngineEvents, AssetManifest } from '../types';
|
|
5
|
+
import { ScaleMode, Orientation, TransitionType } from '../types';
|
|
6
|
+
import { EventEmitter } from './EventEmitter';
|
|
7
|
+
import { SceneManager } from './SceneManager';
|
|
8
|
+
import { AssetManager } from '../assets/AssetManager';
|
|
9
|
+
import { AudioManager } from '../audio/AudioManager';
|
|
10
|
+
import { ViewportManager } from '../viewport/ViewportManager';
|
|
11
|
+
import { LoadingScene } from '../loading/LoadingScene';
|
|
12
|
+
import { createCSSPreloader, removeCSSPreloader } from '../loading/CSSPreloader';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The main entry point for a game built on @energy8platform/game-engine.
|
|
16
|
+
*
|
|
17
|
+
* Orchestrates the full lifecycle:
|
|
18
|
+
* 1. Create PixiJS Application
|
|
19
|
+
* 2. Initialize SDK (or run offline)
|
|
20
|
+
* 3. Show CSS preloader → Canvas loading screen with progress bar
|
|
21
|
+
* 4. Load asset manifest
|
|
22
|
+
* 5. Transition to the first game scene
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* import { GameApplication, ScaleMode } from '@energy8platform/game-engine';
|
|
27
|
+
* import { GameScene } from './scenes/GameScene';
|
|
28
|
+
*
|
|
29
|
+
* const game = new GameApplication({
|
|
30
|
+
* container: '#game',
|
|
31
|
+
* designWidth: 1920,
|
|
32
|
+
* designHeight: 1080,
|
|
33
|
+
* scaleMode: ScaleMode.FIT,
|
|
34
|
+
* manifest: { bundles: [
|
|
35
|
+
* { name: 'preload', assets: [{ alias: 'logo', src: 'logo.png' }] },
|
|
36
|
+
* { name: 'game', assets: [{ alias: 'bg', src: 'background.png' }] },
|
|
37
|
+
* ]},
|
|
38
|
+
* loading: { tapToStart: true },
|
|
39
|
+
* });
|
|
40
|
+
*
|
|
41
|
+
* game.scenes.register('game', GameScene);
|
|
42
|
+
* await game.start('game');
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export class GameApplication extends EventEmitter<GameEngineEvents> {
|
|
46
|
+
// ─── Public references ──────────────────────────────────
|
|
47
|
+
|
|
48
|
+
/** PixiJS Application instance */
|
|
49
|
+
public app!: Application;
|
|
50
|
+
|
|
51
|
+
/** Scene manager */
|
|
52
|
+
public scenes!: SceneManager;
|
|
53
|
+
|
|
54
|
+
/** Asset manager */
|
|
55
|
+
public assets!: AssetManager;
|
|
56
|
+
|
|
57
|
+
/** Audio manager */
|
|
58
|
+
public audio!: AudioManager;
|
|
59
|
+
|
|
60
|
+
/** Viewport manager */
|
|
61
|
+
public viewport!: ViewportManager;
|
|
62
|
+
|
|
63
|
+
/** SDK instance (null in offline mode) */
|
|
64
|
+
public sdk: CasinoGameSDK | null = null;
|
|
65
|
+
|
|
66
|
+
/** Data received from SDK initialization */
|
|
67
|
+
public initData: InitData | null = null;
|
|
68
|
+
|
|
69
|
+
/** Configuration */
|
|
70
|
+
public readonly config: GameApplicationConfig;
|
|
71
|
+
|
|
72
|
+
// ─── Private state ──────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
private _running = false;
|
|
75
|
+
private _destroyed = false;
|
|
76
|
+
private _container: HTMLElement | null = null;
|
|
77
|
+
|
|
78
|
+
constructor(config: GameApplicationConfig = {}) {
|
|
79
|
+
super();
|
|
80
|
+
this.config = {
|
|
81
|
+
designWidth: 1920,
|
|
82
|
+
designHeight: 1080,
|
|
83
|
+
scaleMode: ScaleMode.FIT,
|
|
84
|
+
orientation: Orientation.ANY,
|
|
85
|
+
debug: false,
|
|
86
|
+
...config,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Create SceneManager early so scenes can be registered before start()
|
|
90
|
+
this.scenes = new SceneManager();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ─── Public getters ─────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
/** Current game config from SDK (or null in offline mode) */
|
|
96
|
+
get gameConfig(): GameConfigData | null {
|
|
97
|
+
return this.initData?.config ?? null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Current session data */
|
|
101
|
+
get session(): SessionData | null {
|
|
102
|
+
return this.initData?.session ?? null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Current balance */
|
|
106
|
+
get balance(): number {
|
|
107
|
+
return this.sdk?.balance ?? 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Current currency */
|
|
111
|
+
get currency(): string {
|
|
112
|
+
return this.sdk?.currency ?? 'USD';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Whether the engine is running */
|
|
116
|
+
get isRunning(): boolean {
|
|
117
|
+
return this._running;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ─── Lifecycle ──────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Start the game engine. This is the main entry point.
|
|
124
|
+
*
|
|
125
|
+
* @param firstScene - Key of the first scene to show after loading (must be registered)
|
|
126
|
+
* @param sceneData - Optional data to pass to the first scene's onEnter
|
|
127
|
+
*/
|
|
128
|
+
async start(firstScene: string, sceneData?: unknown): Promise<void> {
|
|
129
|
+
if (this._running) {
|
|
130
|
+
console.warn('[GameEngine] Already running');
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
// 1. Resolve container element
|
|
136
|
+
this._container = this.resolveContainer();
|
|
137
|
+
|
|
138
|
+
// 2. Show CSS preloader immediately (before PixiJS)
|
|
139
|
+
createCSSPreloader(this._container, this.config.loading);
|
|
140
|
+
|
|
141
|
+
// 3. Initialize PixiJS
|
|
142
|
+
await this.initPixi();
|
|
143
|
+
|
|
144
|
+
// 4. Initialize SDK (if enabled)
|
|
145
|
+
await this.initSDK();
|
|
146
|
+
|
|
147
|
+
// 5. Merge design dimensions from SDK config
|
|
148
|
+
this.applySDKConfig();
|
|
149
|
+
|
|
150
|
+
// 6. Initialize sub-systems
|
|
151
|
+
this.initSubSystems();
|
|
152
|
+
|
|
153
|
+
this.emit('initialized', undefined as any);
|
|
154
|
+
|
|
155
|
+
// 7. Remove CSS preloader, show Canvas loading screen
|
|
156
|
+
removeCSSPreloader(this._container);
|
|
157
|
+
|
|
158
|
+
// 8. Load assets with loading screen
|
|
159
|
+
await this.loadAssets(firstScene, sceneData);
|
|
160
|
+
|
|
161
|
+
// 9. Start the game loop
|
|
162
|
+
this._running = true;
|
|
163
|
+
this.emit('started', undefined as any);
|
|
164
|
+
} catch (err) {
|
|
165
|
+
console.error('[GameEngine] Failed to start:', err);
|
|
166
|
+
this.emit('error', err instanceof Error ? err : new Error(String(err)));
|
|
167
|
+
throw err;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Destroy the engine and free all resources.
|
|
173
|
+
*/
|
|
174
|
+
destroy(): void {
|
|
175
|
+
if (this._destroyed) return;
|
|
176
|
+
this._destroyed = true;
|
|
177
|
+
this._running = false;
|
|
178
|
+
|
|
179
|
+
this.scenes?.destroy();
|
|
180
|
+
this.audio?.destroy();
|
|
181
|
+
this.viewport?.destroy();
|
|
182
|
+
this.sdk?.destroy();
|
|
183
|
+
this.app?.destroy(true, { children: true, texture: true });
|
|
184
|
+
this.removeAllListeners();
|
|
185
|
+
|
|
186
|
+
this.emit('destroyed', undefined as any);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ─── Private initialization steps ──────────────────────
|
|
190
|
+
|
|
191
|
+
private resolveContainer(): HTMLElement {
|
|
192
|
+
if (typeof this.config.container === 'string') {
|
|
193
|
+
const el = document.querySelector<HTMLElement>(this.config.container);
|
|
194
|
+
if (!el) throw new Error(`[GameEngine] Container "${this.config.container}" not found`);
|
|
195
|
+
return el;
|
|
196
|
+
}
|
|
197
|
+
return this.config.container ?? document.body;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private async initPixi(): Promise<void> {
|
|
201
|
+
this.app = new Application();
|
|
202
|
+
|
|
203
|
+
const pixiOpts = {
|
|
204
|
+
background: typeof this.config.loading?.backgroundColor === 'number'
|
|
205
|
+
? this.config.loading.backgroundColor
|
|
206
|
+
: 0x000000,
|
|
207
|
+
antialias: true,
|
|
208
|
+
resolution: Math.min(window.devicePixelRatio, 2),
|
|
209
|
+
autoDensity: true,
|
|
210
|
+
...this.config.pixi,
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
await this.app.init(pixiOpts);
|
|
214
|
+
|
|
215
|
+
// Append canvas to container
|
|
216
|
+
this._container!.appendChild(this.app.canvas);
|
|
217
|
+
|
|
218
|
+
// Set canvas style
|
|
219
|
+
this.app.canvas.style.display = 'block';
|
|
220
|
+
this.app.canvas.style.width = '100%';
|
|
221
|
+
this.app.canvas.style.height = '100%';
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private async initSDK(): Promise<void> {
|
|
225
|
+
if (this.config.sdk === false) {
|
|
226
|
+
// Offline / development mode — no SDK
|
|
227
|
+
this.initData = null;
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const sdkOpts = typeof this.config.sdk === 'object' ? this.config.sdk : {};
|
|
232
|
+
this.sdk = new CasinoGameSDK(sdkOpts);
|
|
233
|
+
|
|
234
|
+
// Perform the handshake
|
|
235
|
+
this.initData = await this.sdk.ready();
|
|
236
|
+
|
|
237
|
+
// Forward SDK events
|
|
238
|
+
this.sdk.on('error', (err: Error) => {
|
|
239
|
+
this.emit('error', err);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private applySDKConfig(): void {
|
|
244
|
+
// If SDK provides viewport dimensions, use them as design reference
|
|
245
|
+
if (this.initData?.config?.viewport) {
|
|
246
|
+
const vp = this.initData.config.viewport;
|
|
247
|
+
if (!this.config.designWidth) this.config.designWidth = vp.width;
|
|
248
|
+
if (!this.config.designHeight) this.config.designHeight = vp.height;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private initSubSystems(): void {
|
|
253
|
+
// Asset Manager
|
|
254
|
+
const basePath = this.initData?.assetsUrl ?? '';
|
|
255
|
+
this.assets = new AssetManager(basePath, this.config.manifest);
|
|
256
|
+
|
|
257
|
+
// Audio Manager
|
|
258
|
+
this.audio = new AudioManager(this.config.audio);
|
|
259
|
+
|
|
260
|
+
// Viewport Manager
|
|
261
|
+
this.viewport = new ViewportManager(
|
|
262
|
+
this.app,
|
|
263
|
+
this._container!,
|
|
264
|
+
{
|
|
265
|
+
designWidth: this.config.designWidth!,
|
|
266
|
+
designHeight: this.config.designHeight!,
|
|
267
|
+
scaleMode: this.config.scaleMode!,
|
|
268
|
+
orientation: this.config.orientation!,
|
|
269
|
+
},
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
// Wire SceneManager to the PixiJS stage
|
|
273
|
+
this.scenes.setRoot(this.app.stage);
|
|
274
|
+
|
|
275
|
+
// Wire viewport resize → scene manager
|
|
276
|
+
this.viewport.on('resize', ({ width, height }) => {
|
|
277
|
+
this.scenes.resize(width, height);
|
|
278
|
+
this.emit('resize', { width, height });
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
this.viewport.on('orientationChange', (orientation) => {
|
|
282
|
+
this.emit('orientationChange', orientation);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// Connect ticker → scene updates
|
|
286
|
+
this.app.ticker.add((ticker) => {
|
|
287
|
+
// Always update scenes (loading screen needs onUpdate before _running=true)
|
|
288
|
+
this.scenes.update(ticker.deltaTime / 60); // convert to seconds
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// Trigger initial resize
|
|
292
|
+
this.viewport.refresh();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private async loadAssets(firstScene: string, sceneData?: unknown): Promise<void> {
|
|
296
|
+
// Register built-in loading scene
|
|
297
|
+
this.scenes.register('__loading__', LoadingScene as any);
|
|
298
|
+
|
|
299
|
+
// Enter loading scene
|
|
300
|
+
await this.scenes.goto('__loading__', {
|
|
301
|
+
engine: this,
|
|
302
|
+
targetScene: firstScene,
|
|
303
|
+
targetData: sceneData,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Container } from 'pixi.js';
|
|
2
|
+
import type { IScene } from '../types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Base class for all scenes.
|
|
6
|
+
* Provides a root PixiJS Container and lifecycle hooks.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* class MenuScene extends Scene {
|
|
11
|
+
* async onEnter() {
|
|
12
|
+
* const bg = Sprite.from('menu-bg');
|
|
13
|
+
* this.container.addChild(bg);
|
|
14
|
+
* }
|
|
15
|
+
*
|
|
16
|
+
* onUpdate(dt: number) {
|
|
17
|
+
* // per-frame logic
|
|
18
|
+
* }
|
|
19
|
+
*
|
|
20
|
+
* onResize(width: number, height: number) {
|
|
21
|
+
* // reposition UI
|
|
22
|
+
* }
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export abstract class Scene implements IScene {
|
|
27
|
+
public readonly container: Container;
|
|
28
|
+
|
|
29
|
+
constructor() {
|
|
30
|
+
this.container = new Container();
|
|
31
|
+
this.container.label = this.constructor.name;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Called when this scene becomes active. Override in subclass. */
|
|
35
|
+
onEnter?(data?: unknown): Promise<void> | void;
|
|
36
|
+
|
|
37
|
+
/** Called when this scene is deactivated. Override in subclass. */
|
|
38
|
+
onExit?(): Promise<void> | void;
|
|
39
|
+
|
|
40
|
+
/** Called every frame with delta time (in seconds). Override in subclass. */
|
|
41
|
+
onUpdate?(dt: number): void;
|
|
42
|
+
|
|
43
|
+
/** Called when the viewport resizes. Override in subclass. */
|
|
44
|
+
onResize?(width: number, height: number): void;
|
|
45
|
+
|
|
46
|
+
/** Cleanup — called when the scene is permanently removed. */
|
|
47
|
+
onDestroy?(): void;
|
|
48
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { Container } from 'pixi.js';
|
|
2
|
+
import type { IScene, SceneConstructor, TransitionConfig, TransitionType } from '../types';
|
|
3
|
+
import { TransitionType as TT } from '../types';
|
|
4
|
+
import { Tween } from '../animation/Tween';
|
|
5
|
+
import { EventEmitter } from './EventEmitter';
|
|
6
|
+
|
|
7
|
+
interface SceneEntry {
|
|
8
|
+
scene: IScene;
|
|
9
|
+
key: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface SceneManagerEvents {
|
|
13
|
+
change: { from: string | null; to: string };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Manages the scene stack and transitions between scenes.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* const scenes = new SceneManager(app.stage);
|
|
22
|
+
* scenes.register('loading', LoadingScene);
|
|
23
|
+
* scenes.register('game', GameScene);
|
|
24
|
+
* await scenes.goto('loading');
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export class SceneManager extends EventEmitter<SceneManagerEvents> {
|
|
28
|
+
/** Root container that scenes are added to */
|
|
29
|
+
public root!: Container;
|
|
30
|
+
|
|
31
|
+
private registry = new Map<string, SceneConstructor>();
|
|
32
|
+
private stack: SceneEntry[] = [];
|
|
33
|
+
private _transitioning = false;
|
|
34
|
+
|
|
35
|
+
/** Current viewport dimensions — set by ViewportManager */
|
|
36
|
+
private _width = 0;
|
|
37
|
+
private _height = 0;
|
|
38
|
+
|
|
39
|
+
constructor(root?: Container) {
|
|
40
|
+
super();
|
|
41
|
+
if (root) this.root = root;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** @internal Set the root container (called by GameApplication after PixiJS init) */
|
|
45
|
+
setRoot(root: Container): void {
|
|
46
|
+
this.root = root;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Register a scene class by key */
|
|
50
|
+
register(key: string, ctor: SceneConstructor): this {
|
|
51
|
+
this.registry.set(key, ctor);
|
|
52
|
+
return this;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Get the current (topmost) scene entry */
|
|
56
|
+
get current(): SceneEntry | null {
|
|
57
|
+
return this.stack.length > 0 ? this.stack[this.stack.length - 1] : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Get the current scene key */
|
|
61
|
+
get currentKey(): string | null {
|
|
62
|
+
return this.current?.key ?? null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Whether a scene transition is in progress */
|
|
66
|
+
get isTransitioning(): boolean {
|
|
67
|
+
return this._transitioning;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Navigate to a scene, replacing the entire stack.
|
|
72
|
+
*/
|
|
73
|
+
async goto(
|
|
74
|
+
key: string,
|
|
75
|
+
data?: unknown,
|
|
76
|
+
transition?: TransitionConfig,
|
|
77
|
+
): Promise<void> {
|
|
78
|
+
const prevKey = this.currentKey;
|
|
79
|
+
|
|
80
|
+
// Exit all current scenes
|
|
81
|
+
while (this.stack.length > 0) {
|
|
82
|
+
await this.popInternal(false);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Enter new scene
|
|
86
|
+
await this.pushInternal(key, data, transition);
|
|
87
|
+
this.emit('change', { from: prevKey, to: key });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Push a scene onto the stack (the previous scene stays underneath).
|
|
92
|
+
* Useful for overlays, modals, pause screens.
|
|
93
|
+
*/
|
|
94
|
+
async push(
|
|
95
|
+
key: string,
|
|
96
|
+
data?: unknown,
|
|
97
|
+
transition?: TransitionConfig,
|
|
98
|
+
): Promise<void> {
|
|
99
|
+
const prevKey = this.currentKey;
|
|
100
|
+
await this.pushInternal(key, data, transition);
|
|
101
|
+
this.emit('change', { from: prevKey, to: key });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Pop the top scene from the stack.
|
|
106
|
+
*/
|
|
107
|
+
async pop(transition?: TransitionConfig): Promise<void> {
|
|
108
|
+
if (this.stack.length <= 1) {
|
|
109
|
+
console.warn('[SceneManager] Cannot pop the last scene');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const prevKey = this.currentKey;
|
|
113
|
+
await this.popInternal(true, transition);
|
|
114
|
+
this.emit('change', { from: prevKey, to: this.currentKey! });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Replace the top scene with a new one.
|
|
119
|
+
*/
|
|
120
|
+
async replace(
|
|
121
|
+
key: string,
|
|
122
|
+
data?: unknown,
|
|
123
|
+
transition?: TransitionConfig,
|
|
124
|
+
): Promise<void> {
|
|
125
|
+
const prevKey = this.currentKey;
|
|
126
|
+
await this.popInternal(false);
|
|
127
|
+
await this.pushInternal(key, data, transition);
|
|
128
|
+
this.emit('change', { from: prevKey, to: key });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Called every frame by GameApplication.
|
|
133
|
+
*/
|
|
134
|
+
update(dt: number): void {
|
|
135
|
+
// Update only the top scene
|
|
136
|
+
this.current?.scene.onUpdate?.(dt);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Called on viewport resize.
|
|
141
|
+
*/
|
|
142
|
+
resize(width: number, height: number): void {
|
|
143
|
+
this._width = width;
|
|
144
|
+
this._height = height;
|
|
145
|
+
|
|
146
|
+
// Notify all scenes in the stack
|
|
147
|
+
for (const entry of this.stack) {
|
|
148
|
+
entry.scene.onResize?.(width, height);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Destroy all scenes and clear the manager.
|
|
154
|
+
*/
|
|
155
|
+
destroy(): void {
|
|
156
|
+
for (const entry of this.stack) {
|
|
157
|
+
entry.scene.onDestroy?.();
|
|
158
|
+
entry.scene.container.destroy({ children: true });
|
|
159
|
+
}
|
|
160
|
+
this.stack.length = 0;
|
|
161
|
+
this.registry.clear();
|
|
162
|
+
this.removeAllListeners();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ─── Internal ──────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
private createScene(key: string): IScene {
|
|
168
|
+
const Ctor = this.registry.get(key);
|
|
169
|
+
if (!Ctor) {
|
|
170
|
+
throw new Error(`[SceneManager] Scene "${key}" is not registered`);
|
|
171
|
+
}
|
|
172
|
+
return new Ctor();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private async pushInternal(
|
|
176
|
+
key: string,
|
|
177
|
+
data?: unknown,
|
|
178
|
+
transition?: TransitionConfig,
|
|
179
|
+
): Promise<void> {
|
|
180
|
+
this._transitioning = true;
|
|
181
|
+
|
|
182
|
+
const scene = this.createScene(key);
|
|
183
|
+
this.root.addChild(scene.container);
|
|
184
|
+
|
|
185
|
+
// Set initial size
|
|
186
|
+
if (this._width && this._height) {
|
|
187
|
+
scene.onResize?.(this._width, this._height);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Transition in
|
|
191
|
+
await this.transitionIn(scene.container, transition);
|
|
192
|
+
|
|
193
|
+
await scene.onEnter?.(data);
|
|
194
|
+
|
|
195
|
+
this.stack.push({ scene, key });
|
|
196
|
+
this._transitioning = false;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private async popInternal(
|
|
200
|
+
showTransition: boolean,
|
|
201
|
+
transition?: TransitionConfig,
|
|
202
|
+
): Promise<void> {
|
|
203
|
+
const entry = this.stack.pop();
|
|
204
|
+
if (!entry) return;
|
|
205
|
+
|
|
206
|
+
this._transitioning = true;
|
|
207
|
+
|
|
208
|
+
await entry.scene.onExit?.();
|
|
209
|
+
|
|
210
|
+
if (showTransition) {
|
|
211
|
+
await this.transitionOut(entry.scene.container, transition);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
entry.scene.onDestroy?.();
|
|
215
|
+
entry.scene.container.destroy({ children: true });
|
|
216
|
+
|
|
217
|
+
this._transitioning = false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private async transitionIn(
|
|
221
|
+
container: Container,
|
|
222
|
+
config?: TransitionConfig,
|
|
223
|
+
): Promise<void> {
|
|
224
|
+
const type = config?.type ?? TT.NONE;
|
|
225
|
+
const duration = config?.duration ?? 300;
|
|
226
|
+
|
|
227
|
+
if (type === TT.NONE || duration <= 0) return;
|
|
228
|
+
|
|
229
|
+
if (type === TT.FADE) {
|
|
230
|
+
container.alpha = 0;
|
|
231
|
+
await Tween.to(container, { alpha: 1 }, duration, config?.easing);
|
|
232
|
+
} else if (type === TT.SLIDE_LEFT) {
|
|
233
|
+
container.x = this._width;
|
|
234
|
+
await Tween.to(container, { x: 0 }, duration, config?.easing);
|
|
235
|
+
} else if (type === TT.SLIDE_RIGHT) {
|
|
236
|
+
container.x = -this._width;
|
|
237
|
+
await Tween.to(container, { x: 0 }, duration, config?.easing);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private async transitionOut(
|
|
242
|
+
container: Container,
|
|
243
|
+
config?: TransitionConfig,
|
|
244
|
+
): Promise<void> {
|
|
245
|
+
const type = config?.type ?? TT.FADE;
|
|
246
|
+
const duration = config?.duration ?? 300;
|
|
247
|
+
|
|
248
|
+
if (type === TT.NONE || duration <= 0) return;
|
|
249
|
+
|
|
250
|
+
if (type === TT.FADE) {
|
|
251
|
+
await Tween.to(container, { alpha: 0 }, duration, config?.easing);
|
|
252
|
+
} else if (type === TT.SLIDE_LEFT) {
|
|
253
|
+
await Tween.to(container, { x: -this._width }, duration, config?.easing);
|
|
254
|
+
} else if (type === TT.SLIDE_RIGHT) {
|
|
255
|
+
await Tween.to(container, { x: this._width }, duration, config?.easing);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|