@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.
Files changed (80) hide show
  1. package/README.md +1134 -0
  2. package/dist/animation.cjs.js +505 -0
  3. package/dist/animation.cjs.js.map +1 -0
  4. package/dist/animation.d.ts +235 -0
  5. package/dist/animation.esm.js +500 -0
  6. package/dist/animation.esm.js.map +1 -0
  7. package/dist/assets.cjs.js +148 -0
  8. package/dist/assets.cjs.js.map +1 -0
  9. package/dist/assets.d.ts +97 -0
  10. package/dist/assets.esm.js +146 -0
  11. package/dist/assets.esm.js.map +1 -0
  12. package/dist/audio.cjs.js +345 -0
  13. package/dist/audio.cjs.js.map +1 -0
  14. package/dist/audio.d.ts +135 -0
  15. package/dist/audio.esm.js +343 -0
  16. package/dist/audio.esm.js.map +1 -0
  17. package/dist/core.cjs.js +1832 -0
  18. package/dist/core.cjs.js.map +1 -0
  19. package/dist/core.d.ts +633 -0
  20. package/dist/core.esm.js +1827 -0
  21. package/dist/core.esm.js.map +1 -0
  22. package/dist/debug.cjs.js +298 -0
  23. package/dist/debug.cjs.js.map +1 -0
  24. package/dist/debug.d.ts +114 -0
  25. package/dist/debug.esm.js +295 -0
  26. package/dist/debug.esm.js.map +1 -0
  27. package/dist/index.cjs.js +3623 -0
  28. package/dist/index.cjs.js.map +1 -0
  29. package/dist/index.d.ts +1607 -0
  30. package/dist/index.esm.js +3598 -0
  31. package/dist/index.esm.js.map +1 -0
  32. package/dist/ui.cjs.js +1081 -0
  33. package/dist/ui.cjs.js.map +1 -0
  34. package/dist/ui.d.ts +387 -0
  35. package/dist/ui.esm.js +1072 -0
  36. package/dist/ui.esm.js.map +1 -0
  37. package/dist/vite.cjs.js +125 -0
  38. package/dist/vite.cjs.js.map +1 -0
  39. package/dist/vite.d.ts +42 -0
  40. package/dist/vite.esm.js +122 -0
  41. package/dist/vite.esm.js.map +1 -0
  42. package/package.json +107 -0
  43. package/src/animation/Easing.ts +116 -0
  44. package/src/animation/SpineHelper.ts +162 -0
  45. package/src/animation/Timeline.ts +138 -0
  46. package/src/animation/Tween.ts +225 -0
  47. package/src/animation/index.ts +4 -0
  48. package/src/assets/AssetManager.ts +174 -0
  49. package/src/assets/index.ts +2 -0
  50. package/src/audio/AudioManager.ts +366 -0
  51. package/src/audio/index.ts +1 -0
  52. package/src/core/EventEmitter.ts +47 -0
  53. package/src/core/GameApplication.ts +306 -0
  54. package/src/core/Scene.ts +48 -0
  55. package/src/core/SceneManager.ts +258 -0
  56. package/src/core/index.ts +4 -0
  57. package/src/debug/DevBridge.ts +259 -0
  58. package/src/debug/FPSOverlay.ts +102 -0
  59. package/src/debug/index.ts +3 -0
  60. package/src/index.ts +71 -0
  61. package/src/input/InputManager.ts +171 -0
  62. package/src/input/index.ts +1 -0
  63. package/src/loading/CSSPreloader.ts +155 -0
  64. package/src/loading/LoadingScene.ts +356 -0
  65. package/src/loading/index.ts +2 -0
  66. package/src/state/StateMachine.ts +228 -0
  67. package/src/state/index.ts +1 -0
  68. package/src/types.ts +218 -0
  69. package/src/ui/BalanceDisplay.ts +155 -0
  70. package/src/ui/Button.ts +199 -0
  71. package/src/ui/Label.ts +111 -0
  72. package/src/ui/Modal.ts +134 -0
  73. package/src/ui/Panel.ts +125 -0
  74. package/src/ui/ProgressBar.ts +121 -0
  75. package/src/ui/Toast.ts +124 -0
  76. package/src/ui/WinDisplay.ts +133 -0
  77. package/src/ui/index.ts +16 -0
  78. package/src/viewport/ViewportManager.ts +241 -0
  79. package/src/viewport/index.ts +1 -0
  80. 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
+ }