@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,366 @@
|
|
|
1
|
+
import type { AudioConfig } from '../types';
|
|
2
|
+
|
|
3
|
+
type AudioCategoryName = 'music' | 'sfx' | 'ui' | 'ambient';
|
|
4
|
+
|
|
5
|
+
interface CategoryState {
|
|
6
|
+
volume: number;
|
|
7
|
+
muted: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Manages all game audio: music, SFX, UI sounds, ambient.
|
|
12
|
+
*
|
|
13
|
+
* Optional dependency on @pixi/sound — if not installed, AudioManager
|
|
14
|
+
* operates as a silent no-op (graceful degradation).
|
|
15
|
+
*
|
|
16
|
+
* Features:
|
|
17
|
+
* - Per-category volume control (music, sfx, ui, ambient)
|
|
18
|
+
* - Music crossfade and looping
|
|
19
|
+
* - Mobile audio unlock on first interaction
|
|
20
|
+
* - Mute state persistence in localStorage
|
|
21
|
+
* - Global mute/unmute
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* const audio = new AudioManager({ music: 0.5, sfx: 0.8 });
|
|
26
|
+
* await audio.init();
|
|
27
|
+
* audio.playMusic('bg-music');
|
|
28
|
+
* audio.play('spin-click', 'sfx');
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export class AudioManager {
|
|
32
|
+
private _soundModule: any = null;
|
|
33
|
+
private _initialized = false;
|
|
34
|
+
private _globalMuted = false;
|
|
35
|
+
private _persist: boolean;
|
|
36
|
+
private _storageKey: string;
|
|
37
|
+
private _categories: Record<AudioCategoryName, CategoryState>;
|
|
38
|
+
private _currentMusic: string | null = null;
|
|
39
|
+
private _unlocked = false;
|
|
40
|
+
private _unlockHandler: (() => void) | null = null;
|
|
41
|
+
|
|
42
|
+
constructor(config?: AudioConfig) {
|
|
43
|
+
this._persist = config?.persist ?? true;
|
|
44
|
+
this._storageKey = config?.storageKey ?? 'ge_audio';
|
|
45
|
+
|
|
46
|
+
this._categories = {
|
|
47
|
+
music: { volume: config?.music ?? 0.7, muted: false },
|
|
48
|
+
sfx: { volume: config?.sfx ?? 1.0, muted: false },
|
|
49
|
+
ui: { volume: config?.ui ?? 0.8, muted: false },
|
|
50
|
+
ambient: { volume: config?.ambient ?? 0.5, muted: false },
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Restore persisted state
|
|
54
|
+
if (this._persist) {
|
|
55
|
+
this.restoreState();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Whether the audio system is initialized */
|
|
60
|
+
get initialized(): boolean {
|
|
61
|
+
return this._initialized;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Whether audio is globally muted */
|
|
65
|
+
get muted(): boolean {
|
|
66
|
+
return this._globalMuted;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Initialize the audio system.
|
|
71
|
+
* Dynamically imports @pixi/sound to keep it optional.
|
|
72
|
+
*/
|
|
73
|
+
async init(): Promise<void> {
|
|
74
|
+
if (this._initialized) return;
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
this._soundModule = await import('@pixi/sound');
|
|
78
|
+
this._initialized = true;
|
|
79
|
+
this.applyVolumes();
|
|
80
|
+
this.setupMobileUnlock();
|
|
81
|
+
} catch {
|
|
82
|
+
console.warn(
|
|
83
|
+
'[AudioManager] @pixi/sound not available. Audio disabled.',
|
|
84
|
+
);
|
|
85
|
+
this._initialized = false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Play a sound effect.
|
|
91
|
+
*
|
|
92
|
+
* @param alias - Sound alias (must be loaded via AssetManager)
|
|
93
|
+
* @param category - Audio category (default: 'sfx')
|
|
94
|
+
* @param options - Additional play options
|
|
95
|
+
*/
|
|
96
|
+
play(
|
|
97
|
+
alias: string,
|
|
98
|
+
category: AudioCategoryName = 'sfx',
|
|
99
|
+
options?: { volume?: number; loop?: boolean; speed?: number },
|
|
100
|
+
): void {
|
|
101
|
+
if (!this._initialized || !this._soundModule) return;
|
|
102
|
+
if (this._globalMuted || this._categories[category].muted) return;
|
|
103
|
+
|
|
104
|
+
const { sound } = this._soundModule;
|
|
105
|
+
const vol = (options?.volume ?? 1) * this._categories[category].volume;
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
sound.play(alias, {
|
|
109
|
+
volume: vol,
|
|
110
|
+
loop: options?.loop ?? false,
|
|
111
|
+
speed: options?.speed ?? 1,
|
|
112
|
+
});
|
|
113
|
+
} catch (e) {
|
|
114
|
+
console.warn(`[AudioManager] Failed to play "${alias}":`, e);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Play background music with optional crossfade.
|
|
120
|
+
*
|
|
121
|
+
* @param alias - Music alias
|
|
122
|
+
* @param fadeDuration - Crossfade duration in ms (default: 500)
|
|
123
|
+
*/
|
|
124
|
+
playMusic(alias: string, fadeDuration = 500): void {
|
|
125
|
+
if (!this._initialized || !this._soundModule) return;
|
|
126
|
+
|
|
127
|
+
const { sound } = this._soundModule;
|
|
128
|
+
|
|
129
|
+
// Stop current music
|
|
130
|
+
if (this._currentMusic) {
|
|
131
|
+
try {
|
|
132
|
+
sound.stop(this._currentMusic);
|
|
133
|
+
} catch {
|
|
134
|
+
// ignore
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
this._currentMusic = alias;
|
|
139
|
+
if (this._globalMuted || this._categories.music.muted) return;
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
sound.play(alias, {
|
|
143
|
+
volume: this._categories.music.volume,
|
|
144
|
+
loop: true,
|
|
145
|
+
});
|
|
146
|
+
} catch (e) {
|
|
147
|
+
console.warn(`[AudioManager] Failed to play music "${alias}":`, e);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Stop current music.
|
|
153
|
+
*/
|
|
154
|
+
stopMusic(): void {
|
|
155
|
+
if (!this._initialized || !this._soundModule || !this._currentMusic) return;
|
|
156
|
+
const { sound } = this._soundModule;
|
|
157
|
+
try {
|
|
158
|
+
sound.stop(this._currentMusic);
|
|
159
|
+
} catch {
|
|
160
|
+
// ignore
|
|
161
|
+
}
|
|
162
|
+
this._currentMusic = null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Stop all sounds.
|
|
167
|
+
*/
|
|
168
|
+
stopAll(): void {
|
|
169
|
+
if (!this._initialized || !this._soundModule) return;
|
|
170
|
+
const { sound } = this._soundModule;
|
|
171
|
+
sound.stopAll();
|
|
172
|
+
this._currentMusic = null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Set volume for a category.
|
|
177
|
+
*/
|
|
178
|
+
setVolume(category: AudioCategoryName, volume: number): void {
|
|
179
|
+
this._categories[category].volume = Math.max(0, Math.min(1, volume));
|
|
180
|
+
this.applyVolumes();
|
|
181
|
+
this.saveState();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Get volume for a category.
|
|
186
|
+
*/
|
|
187
|
+
getVolume(category: AudioCategoryName): number {
|
|
188
|
+
return this._categories[category].volume;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Mute a specific category.
|
|
193
|
+
*/
|
|
194
|
+
muteCategory(category: AudioCategoryName): void {
|
|
195
|
+
this._categories[category].muted = true;
|
|
196
|
+
this.applyVolumes();
|
|
197
|
+
this.saveState();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Unmute a specific category.
|
|
202
|
+
*/
|
|
203
|
+
unmuteCategory(category: AudioCategoryName): void {
|
|
204
|
+
this._categories[category].muted = false;
|
|
205
|
+
this.applyVolumes();
|
|
206
|
+
this.saveState();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Toggle mute for a category.
|
|
211
|
+
*/
|
|
212
|
+
toggleCategory(category: AudioCategoryName): boolean {
|
|
213
|
+
this._categories[category].muted = !this._categories[category].muted;
|
|
214
|
+
this.applyVolumes();
|
|
215
|
+
this.saveState();
|
|
216
|
+
return this._categories[category].muted;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Mute all audio globally.
|
|
221
|
+
*/
|
|
222
|
+
muteAll(): void {
|
|
223
|
+
this._globalMuted = true;
|
|
224
|
+
if (this._soundModule) {
|
|
225
|
+
this._soundModule.sound.muteAll();
|
|
226
|
+
}
|
|
227
|
+
this.saveState();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Unmute all audio globally.
|
|
232
|
+
*/
|
|
233
|
+
unmuteAll(): void {
|
|
234
|
+
this._globalMuted = false;
|
|
235
|
+
if (this._soundModule) {
|
|
236
|
+
this._soundModule.sound.unmuteAll();
|
|
237
|
+
}
|
|
238
|
+
this.saveState();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Toggle global mute.
|
|
243
|
+
*/
|
|
244
|
+
toggleMute(): boolean {
|
|
245
|
+
if (this._globalMuted) {
|
|
246
|
+
this.unmuteAll();
|
|
247
|
+
} else {
|
|
248
|
+
this.muteAll();
|
|
249
|
+
}
|
|
250
|
+
return this._globalMuted;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Duck music volume (e.g., during big win presentation).
|
|
255
|
+
*
|
|
256
|
+
* @param factor - Volume multiplier (0..1), e.g. 0.3 = 30% of normal
|
|
257
|
+
*/
|
|
258
|
+
duckMusic(factor: number): void {
|
|
259
|
+
if (!this._initialized || !this._soundModule || !this._currentMusic) return;
|
|
260
|
+
const { sound } = this._soundModule;
|
|
261
|
+
const vol = this._categories.music.volume * factor;
|
|
262
|
+
try {
|
|
263
|
+
sound.volume(this._currentMusic, vol);
|
|
264
|
+
} catch {
|
|
265
|
+
// ignore
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Restore music to normal volume after ducking.
|
|
271
|
+
*/
|
|
272
|
+
unduckMusic(): void {
|
|
273
|
+
if (!this._initialized || !this._soundModule || !this._currentMusic) return;
|
|
274
|
+
const { sound } = this._soundModule;
|
|
275
|
+
try {
|
|
276
|
+
sound.volume(this._currentMusic, this._categories.music.volume);
|
|
277
|
+
} catch {
|
|
278
|
+
// ignore
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Destroy the audio manager and free resources.
|
|
284
|
+
*/
|
|
285
|
+
destroy(): void {
|
|
286
|
+
this.stopAll();
|
|
287
|
+
this.removeMobileUnlock();
|
|
288
|
+
if (this._soundModule) {
|
|
289
|
+
this._soundModule.sound.removeAll();
|
|
290
|
+
}
|
|
291
|
+
this._initialized = false;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ─── Private ───────────────────────────────────────────
|
|
295
|
+
|
|
296
|
+
private applyVolumes(): void {
|
|
297
|
+
if (!this._soundModule) return;
|
|
298
|
+
const { sound } = this._soundModule;
|
|
299
|
+
sound.volumeAll = this._globalMuted ? 0 : 1;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private setupMobileUnlock(): void {
|
|
303
|
+
if (this._unlocked) return;
|
|
304
|
+
|
|
305
|
+
this._unlockHandler = () => {
|
|
306
|
+
if (!this._soundModule) return;
|
|
307
|
+
const { sound } = this._soundModule;
|
|
308
|
+
// Resume WebAudio context
|
|
309
|
+
if (sound.context?.audioContext?.state === 'suspended') {
|
|
310
|
+
sound.context.audioContext.resume();
|
|
311
|
+
}
|
|
312
|
+
this._unlocked = true;
|
|
313
|
+
this.removeMobileUnlock();
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const events = ['touchstart', 'mousedown', 'pointerdown', 'keydown'];
|
|
317
|
+
for (const event of events) {
|
|
318
|
+
document.addEventListener(event, this._unlockHandler, { once: true });
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private removeMobileUnlock(): void {
|
|
323
|
+
if (!this._unlockHandler) return;
|
|
324
|
+
const events = ['touchstart', 'mousedown', 'pointerdown', 'keydown'];
|
|
325
|
+
for (const event of events) {
|
|
326
|
+
document.removeEventListener(event, this._unlockHandler);
|
|
327
|
+
}
|
|
328
|
+
this._unlockHandler = null;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private saveState(): void {
|
|
332
|
+
if (!this._persist) return;
|
|
333
|
+
try {
|
|
334
|
+
const state = {
|
|
335
|
+
globalMuted: this._globalMuted,
|
|
336
|
+
categories: this._categories,
|
|
337
|
+
};
|
|
338
|
+
localStorage.setItem(this._storageKey, JSON.stringify(state));
|
|
339
|
+
} catch {
|
|
340
|
+
// localStorage may not be available
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
private restoreState(): void {
|
|
345
|
+
try {
|
|
346
|
+
const raw = localStorage.getItem(this._storageKey);
|
|
347
|
+
if (!raw) return;
|
|
348
|
+
const state = JSON.parse(raw);
|
|
349
|
+
if (typeof state.globalMuted === 'boolean') {
|
|
350
|
+
this._globalMuted = state.globalMuted;
|
|
351
|
+
}
|
|
352
|
+
if (state.categories) {
|
|
353
|
+
for (const key of ['music', 'sfx', 'ui', 'ambient'] as const) {
|
|
354
|
+
if (state.categories[key]) {
|
|
355
|
+
this._categories[key] = {
|
|
356
|
+
volume: state.categories[key].volume ?? this._categories[key].volume,
|
|
357
|
+
muted: state.categories[key].muted ?? false,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
} catch {
|
|
363
|
+
// ignore
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { AudioManager } from './AudioManager';
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal typed event emitter.
|
|
3
|
+
* Used internally by GameApplication, SceneManager, AudioManager, etc.
|
|
4
|
+
*/
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
6
|
+
export class EventEmitter<TEvents extends {}> {
|
|
7
|
+
private listeners = new Map<keyof TEvents, Set<(data: any) => void>>();
|
|
8
|
+
|
|
9
|
+
on<K extends keyof TEvents>(event: K, handler: (data: TEvents[K]) => void): this {
|
|
10
|
+
if (!this.listeners.has(event)) {
|
|
11
|
+
this.listeners.set(event, new Set());
|
|
12
|
+
}
|
|
13
|
+
this.listeners.get(event)!.add(handler);
|
|
14
|
+
return this;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
once<K extends keyof TEvents>(event: K, handler: (data: TEvents[K]) => void): this {
|
|
18
|
+
const wrapper = (data: TEvents[K]) => {
|
|
19
|
+
this.off(event, wrapper);
|
|
20
|
+
handler(data);
|
|
21
|
+
};
|
|
22
|
+
return this.on(event, wrapper);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
off<K extends keyof TEvents>(event: K, handler: (data: TEvents[K]) => void): this {
|
|
26
|
+
this.listeners.get(event)?.delete(handler);
|
|
27
|
+
return this;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
emit<K extends keyof TEvents>(event: K, data: TEvents[K]): void {
|
|
31
|
+
const handlers = this.listeners.get(event);
|
|
32
|
+
if (handlers) {
|
|
33
|
+
for (const handler of handlers) {
|
|
34
|
+
handler(data);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
removeAllListeners(event?: keyof TEvents): this {
|
|
40
|
+
if (event) {
|
|
41
|
+
this.listeners.delete(event);
|
|
42
|
+
} else {
|
|
43
|
+
this.listeners.clear();
|
|
44
|
+
}
|
|
45
|
+
return this;
|
|
46
|
+
}
|
|
47
|
+
}
|