blacktrigram 0.7.111 → 0.7.112

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 (43) hide show
  1. package/lib/assets/index.css +0 -1
  2. package/lib/audio/AudioManager.js +22 -20
  3. package/lib/audio/AudioManager.js.map +1 -1
  4. package/lib/audio/AudioPool.js +6 -4
  5. package/lib/audio/AudioPool.js.map +1 -1
  6. package/lib/audio/VariantSelector.js +12 -8
  7. package/lib/audio/VariantSelector.js.map +1 -1
  8. package/lib/components/screens/intro/IntroScreen3D.js +1 -1
  9. package/lib/components/screens/training/TrainingScreen3D.js +4 -3
  10. package/lib/components/screens/training/TrainingScreen3D.js.map +1 -1
  11. package/lib/components/shared/mobile/HapticController.js +4 -2
  12. package/lib/components/shared/mobile/HapticController.js.map +1 -1
  13. package/lib/components/shared/mobile/StanceWheelPure.js +1 -1
  14. package/lib/components/shared/mobile/StanceWheelPure.js.map +1 -1
  15. package/lib/components/shared/three/effects/VitalPointMarkers3D.js +7 -5
  16. package/lib/components/shared/three/effects/VitalPointMarkers3D.js.map +1 -1
  17. package/lib/components/shared/three/optimization/AdaptiveQuality.js.map +1 -1
  18. package/lib/components/shared/three/ui/VitalPointOverlayControlsHtml.js +7 -5
  19. package/lib/components/shared/three/ui/VitalPointOverlayControlsHtml.js.map +1 -1
  20. package/lib/components/shared/ui/SplashScreen.js +2 -2
  21. package/lib/components/shared/ui/VitalPointOverlayControlsPure.js +7 -5
  22. package/lib/components/shared/ui/VitalPointOverlayControlsPure.js.map +1 -1
  23. package/lib/components/shared/ui/VolumeControl.js +4 -2
  24. package/lib/components/shared/ui/VolumeControl.js.map +1 -1
  25. package/lib/hooks/useSkeletalAnimation.js +6 -4
  26. package/lib/hooks/useSkeletalAnimation.js.map +1 -1
  27. package/lib/hooks/useTouchControls.js +18 -17
  28. package/lib/hooks/useTouchControls.js.map +1 -1
  29. package/lib/systems/animation/builders/AnimationBuilder.js +6 -2
  30. package/lib/systems/animation/builders/AnimationBuilder.js.map +1 -1
  31. package/lib/systems/animation/builders/PunchPhaseApplicator.js +8 -4
  32. package/lib/systems/animation/builders/PunchPhaseApplicator.js.map +1 -1
  33. package/lib/systems/animation/core/AnimationStateMachine.js +39 -37
  34. package/lib/systems/animation/core/AnimationStateMachine.js.map +1 -1
  35. package/lib/systems/bodypart/MovementPenaltySystem.js.map +1 -1
  36. package/lib/systems/combat/LimbExposureSystem.js.map +1 -1
  37. package/lib/systems/physics/MovementPhysics.js +25 -24
  38. package/lib/systems/physics/MovementPhysics.js.map +1 -1
  39. package/lib/utils/deviceDetection.js +14 -13
  40. package/lib/utils/deviceDetection.js.map +1 -1
  41. package/lib/utils/inputSystem.js +4 -2
  42. package/lib/utils/inputSystem.js.map +1 -1
  43. package/package.json +5 -5
@@ -424,7 +424,6 @@
424
424
 
425
425
 
426
426
  /* Black Trigram - Korean Cyberpunk Martial Arts Styling */
427
-
428
427
  /* Note: CSS variables are now centralized in index.css */
429
428
 
430
429
  * {
@@ -247,27 +247,29 @@ var AudioManager = class {
247
247
  this.stopMusic();
248
248
  this.audioCache.get(id);
249
249
  let audio = this.soundCache.get(id);
250
- if (!audio) if (this.loadingMusic.has(id)) {
251
- await this.waitForMusicLoad(id);
252
- audio = this.soundCache.get(id);
253
- } else {
254
- this.loadingMusic.add(id);
255
- try {
256
- const { audioAssetRegistry } = await import("./AudioAssetRegistry.js");
257
- const musicAsset = audioAssetRegistry.getMusic(id);
258
- if (musicAsset) try {
259
- await this.loadAsset(musicAsset);
260
- audio = this.soundCache.get(id);
261
- } catch (error) {
262
- console.warn(`Failed to load music asset ${id} on-demand:`, error);
263
- return;
264
- }
265
- else {
266
- console.warn(`Music asset not found in registry: ${id}`);
267
- return;
250
+ if (!audio) {
251
+ if (this.loadingMusic.has(id)) {
252
+ await this.waitForMusicLoad(id);
253
+ audio = this.soundCache.get(id);
254
+ } else {
255
+ this.loadingMusic.add(id);
256
+ try {
257
+ const { audioAssetRegistry } = await import("./AudioAssetRegistry.js");
258
+ const musicAsset = audioAssetRegistry.getMusic(id);
259
+ if (musicAsset) try {
260
+ await this.loadAsset(musicAsset);
261
+ audio = this.soundCache.get(id);
262
+ } catch (error) {
263
+ console.warn(`Failed to load music asset ${id} on-demand:`, error);
264
+ return;
265
+ }
266
+ else {
267
+ console.warn(`Music asset not found in registry: ${id}`);
268
+ return;
269
+ }
270
+ } finally {
271
+ this.loadingMusic.delete(id);
268
272
  }
269
- } finally {
270
- this.loadingMusic.delete(id);
271
273
  }
272
274
  }
273
275
  if (audio) try {
@@ -1 +1 @@
1
- {"version":3,"file":"AudioManager.js","names":[],"sources":["../../src/audio/AudioManager.ts"],"sourcesContent":["import { AudioAssetLoader, LoadOptions } from \"./AudioAssetLoader\";\nimport { AudioCache, AudioCacheConfig } from \"./AudioCache\";\nimport {\n AudioFPSImpact,\n AudioMemoryStats,\n AudioMonitor,\n AudioPerformanceStats,\n MemoryWarning,\n} from \"./AudioMonitor\";\nimport { AudioElementPool, PoolStatistics } from \"./AudioPool\";\nimport {\n AudioAsset,\n AudioConfig,\n IAudioManager,\n MusicTrackId,\n SoundEffectId,\n} from \"./types\";\n\n// Estimated average size per audio asset in MB (based on typical compressed audio file sizes)\nconst ESTIMATED_ASSET_SIZE_MB = 0.5;\n\n/**\n * Critical assets that should never be evicted from cache\n * 캐시에서 절대 제거되지 않아야 하는 중요한 자산\n */\nconst CRITICAL_AUDIO_ASSETS = [\n // Menu sounds - instant playback required\n \"menu_hover\",\n \"menu_select\",\n \"menu_click\",\n \"menu_navigate\",\n \"menu_back\",\n // Common combat sounds - instant playback required\n \"hit_impact\",\n \"hit_light\",\n \"hit_medium\",\n \"hit_heavy\",\n \"guard_block\",\n \"attack_whoosh\",\n \"attack_light\",\n \"stance_change\",\n] as const;\n\n/**\n * Manages audio playback for the Black Trigram combat game.\n *\n * Handles music tracks, sound effects, and Korean martial arts audio\n * including technique sounds, trigram stance audio, and vital point hit\n * feedback. Uses an LRU asset cache, pooled `HTMLAudioElement` instances,\n * and an `AudioMonitor` for performance tracking.\n *\n * @example\n * ```ts\n * const audio = new AudioManager({ masterVolume: 0.8 });\n * await audio.initialize();\n * await audio.playMusic(\"dojang_theme\");\n * await audio.playSFX(\"hit_medium\");\n * ```\n *\n * @korean 오디오관리자\n */\nexport class AudioManager implements IAudioManager {\n private _masterVolume: number = 1.0;\n private _musicVolume: number = 0.7;\n private _sfxVolume: number = 0.8;\n private _muted: boolean = false;\n private _currentMusicTrack: string | null = null;\n private _fallbackMode: boolean = false;\n private currentMusic: HTMLAudioElement | null = null;\n private soundCache: Map<string, HTMLAudioElement> = new Map();\n private _isInitialized: boolean = false;\n\n /** Music assets currently being loaded — prevents race conditions on concurrent playMusic calls */\n private loadingMusic: Set<string> = new Set();\n\n private assetLoader: AudioAssetLoader;\n private audioPool: AudioElementPool;\n private monitor: AudioMonitor;\n private audioCache: AudioCache;\n private frequentSounds: Set<string> = new Set([\n \"hit_light\",\n \"attack_light\",\n \"stance_change\",\n ]);\n\n constructor(config?: Partial<AudioConfig>) {\n if (config) {\n this._masterVolume = config.masterVolume ?? 1.0;\n this._musicVolume = config.musicVolume ?? 0.7;\n this._sfxVolume = config.sfxVolume ?? 0.8;\n }\n\n this.assetLoader = new AudioAssetLoader();\n this.audioPool = new AudioElementPool();\n this.monitor = new AudioMonitor();\n\n // Initialize AudioCache with 30MB limit and critical assets\n // 30MB 제한 및 중요 자산으로 AudioCache 초기화\n const cacheConfig: AudioCacheConfig = {\n maxSizeBytes: 30 * 1024 * 1024, // 30MB default\n criticalAssets: [...CRITICAL_AUDIO_ASSETS],\n debug: typeof process !== \"undefined\" && process?.env?.NODE_ENV === \"development\",\n };\n this.audioCache = new AudioCache(cacheConfig);\n }\n\n /**\n * Helper method to release pooled audio back to the pool after playback\n * @param id - Asset ID\n * @param audio - Audio element to release\n */\n private releasePooledAudio(id: string, audio: HTMLAudioElement): void {\n if (!audio.paused) {\n audio.addEventListener(\n \"ended\",\n () => {\n this.audioPool.release(id, audio);\n },\n { once: true }\n );\n } else {\n this.audioPool.release(id, audio);\n }\n }\n\n /** @returns Whether the audio system has been initialized */\n get isInitialized(): boolean {\n return this._isInitialized;\n }\n\n get fallbackMode(): boolean {\n return this._fallbackMode;\n }\n\n get currentMusicTrack(): string | null {\n return this._currentMusicTrack;\n }\n\n get masterVolume(): number {\n return this._masterVolume;\n }\n\n get sfxVolume(): number {\n return this._sfxVolume;\n }\n\n get musicVolume(): number {\n return this._musicVolume;\n }\n\n get muted(): boolean {\n return this._muted;\n }\n\n async initialize(config?: AudioConfig): Promise<void> {\n try {\n // Create AudioContext using globalThis for cross-platform compatibility\n const AudioContextClass =\n globalThis.AudioContext ||\n (globalThis as unknown as { webkitAudioContext: typeof AudioContext })\n .webkitAudioContext;\n new AudioContextClass();\n this._isInitialized = true;\n this._fallbackMode = false;\n\n if (config) {\n this._masterVolume = config.masterVolume ?? this._masterVolume;\n this._musicVolume = config.musicVolume ?? this._musicVolume;\n this._sfxVolume = config.sfxVolume ?? this._sfxVolume;\n }\n\n // Set memory threshold if configured\n if (config?.maxSimultaneousSounds) {\n const estimatedMemoryMB =\n config.maxSimultaneousSounds * ESTIMATED_ASSET_SIZE_MB;\n this.monitor.setMemoryThreshold(estimatedMemoryMB);\n }\n } catch (error) {\n console.warn(\n \"AudioContext initialization failed, using fallback mode:\",\n error\n );\n this._isInitialized = true;\n this._fallbackMode = true;\n }\n }\n\n async loadAsset(asset: AudioAsset, options?: LoadOptions): Promise<void> {\n const startTime = performance.now();\n\n try {\n // Check cache first - instant playback for cached assets\n // 캐시 먼저 확인 - 캐시된 자산의 즉시 재생\n const cachedAsset = this.audioCache.get(asset.id);\n if (cachedAsset) {\n // Asset already loaded in cache, use it\n const cached = this.soundCache.get(asset.id);\n if (cached) {\n return; // Already loaded and cached\n }\n }\n\n // Use faster timeouts for testing environment\n // Safely check for Node.js environment to avoid runtime errors in browsers\n const isTest =\n typeof process !== \"undefined\" && process?.env?.NODE_ENV === \"test\";\n const loadOptions: LoadOptions = {\n timeout: isTest ? 100 : 10000,\n maxRetries: isTest ? 1 : 3,\n retryDelay: isTest ? 10 : 1000,\n ...options,\n };\n\n // Use AudioAssetLoader with retry and fallback\n const result = await this.assetLoader.loadAsset(asset, loadOptions);\n\n if (result.success && result.audio) {\n result.audio.volume = asset.volume ?? 1.0;\n this.soundCache.set(asset.id, result.audio);\n\n // Estimate size for cache management\n // 캐시 관리를 위한 크기 추정\n const estimatedSize = this.estimateAudioSize(asset);\n\n // Add to LRU cache with size tracking\n // 크기 추적과 함께 LRU 캐시에 추가\n this.audioCache.set(asset.id, asset, estimatedSize);\n\n // Track performance and memory\n const loadTime = performance.now() - startTime;\n this.monitor.recordLoad(asset.id, loadTime, estimatedSize / (1024 * 1024));\n\n // Create pool for frequently used sounds\n if (this.frequentSounds.has(asset.id)) {\n this.audioPool.createPool(asset.id, asset.url, {\n initialSize: 3,\n maxSize: 10,\n autoExpand: true,\n });\n }\n } else {\n // Record failure\n const error = result.error ?? new Error(\"Unknown load error\");\n this.monitor.recordLoadFailure(asset.id, error);\n\n // Still cache the placeholder audio if available\n if (result.audio) {\n this.soundCache.set(asset.id, result.audio);\n\n // Add to cache even if failed (placeholder)\n const estimatedSize = this.estimateAudioSize(asset);\n this.audioCache.set(asset.id, asset, estimatedSize);\n }\n }\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error));\n console.warn(`Failed to load audio asset ${asset.id}:`, err);\n this.monitor.recordLoadFailure(asset.id, err);\n }\n }\n\n /**\n * Estimate audio asset size in bytes\n * 바이트 단위의 오디오 자산 크기 추정\n *\n * @returns Estimated size in bytes\n */\n private estimateAudioSize(_asset: AudioAsset): number {\n // Use ESTIMATED_ASSET_SIZE_MB constant for consistent estimation\n // Conservative estimate: ~0.5MB per audio file (compressed MP3/WebM)\n return ESTIMATED_ASSET_SIZE_MB * 1024 * 1024;\n }\n\n async playSoundEffect(id: SoundEffectId): Promise<void> {\n if (this._muted) return;\n\n const playbackStart = performance.now();\n\n try {\n // Update cache access time for LRU tracking\n // LRU 추적을 위한 캐시 액세스 시간 업데이트\n this.audioCache.get(id);\n\n // Use pool for frequently played sounds\n if (this.frequentSounds.has(id) && this.audioPool.hasPool(id)) {\n const audio = this.audioPool.acquire(id);\n if (audio) {\n audio.volume = this._sfxVolume * this._masterVolume;\n await audio.play();\n\n // Release back to pool after playback (non-blocking)\n this.releasePooledAudio(id, audio);\n\n const latency = performance.now() - playbackStart;\n this.monitor.recordPlaybackLatency(latency);\n return;\n }\n }\n\n // Fallback to cached audio\n const audio = this.soundCache.get(id);\n if (audio) {\n audio.currentTime = 0;\n audio.volume = this._sfxVolume * this._masterVolume;\n await audio.play();\n\n const latency = performance.now() - playbackStart;\n this.monitor.recordPlaybackLatency(latency);\n }\n } catch (error) {\n console.warn(`Failed to play sound effect ${id}:`, error);\n }\n }\n\n /**\n * Alias for {@link playSoundEffect} — preferred for explicit volume control.\n * @param id - Sound effect identifier\n * @param volume - Override volume (defaults to sfxVolume × masterVolume)\n */\n async playSFX(id: SoundEffectId, volume?: number): Promise<void> {\n if (this._muted) return;\n\n const playbackStart = performance.now();\n\n try {\n // Update cache access time for LRU tracking\n // LRU 추적을 위한 캐시 액세스 시간 업데이트\n this.audioCache.get(id);\n\n // Use pool for frequently played sounds\n if (this.frequentSounds.has(id) && this.audioPool.hasPool(id)) {\n const audio = this.audioPool.acquire(id);\n if (audio) {\n audio.volume = (volume ?? this._sfxVolume) * this._masterVolume;\n await audio.play();\n\n // Release back to pool after playback (non-blocking)\n this.releasePooledAudio(id, audio);\n\n const latency = performance.now() - playbackStart;\n this.monitor.recordPlaybackLatency(latency);\n return;\n }\n }\n\n // Fallback to cached audio\n const audio = this.soundCache.get(id);\n if (audio) {\n audio.currentTime = 0;\n audio.volume = (volume ?? this._sfxVolume) * this._masterVolume;\n await audio.play();\n\n const latency = performance.now() - playbackStart;\n this.monitor.recordPlaybackLatency(latency);\n }\n } catch (error) {\n console.warn(`Failed to play sound effect ${id}:`, error);\n }\n }\n\n async playMusic(id: MusicTrackId, volume?: number): Promise<void> {\n if (this._muted) return;\n\n this.stopMusic();\n\n // Update cache access time for LRU tracking\n // LRU 추적을 위한 캐시 액세스 시간 업데이트\n this.audioCache.get(id);\n\n let audio = this.soundCache.get(id);\n \n // On-demand loading: If music not in cache, load it first\n // 온디맨드 로딩: 음악이 캐시에 없으면 먼저 로드\n if (!audio) {\n // Check if this music is already being loaded to prevent race conditions\n // JavaScript's single-threaded nature ensures atomic check-and-add operations\n // 레이스 컨디션 방지를 위해 이미 로드 중인지 확인\n // JavaScript의 싱글 스레드 특성으로 원자적 check-and-add 작업 보장\n if (this.loadingMusic.has(id)) {\n if (import.meta.env.DEV) {\n console.log(`[AudioManager] Music \"${id}\" is already being loaded, waiting...`);\n }\n // Wait for the ongoing load to complete\n // 진행 중인 로드가 완료될 때까지 대기\n await this.waitForMusicLoad(id);\n audio = this.soundCache.get(id);\n } else {\n // Mark as loading before starting the load (atomic operation in single-threaded JS)\n // 로드 시작 전에 로딩 중으로 표시 (싱글 스레드 JS에서 원자적 작업)\n this.loadingMusic.add(id);\n \n if (import.meta.env.DEV) {\n console.log(`[AudioManager] Music \"${id}\" not in cache, loading on-demand...`);\n }\n \n try {\n const { audioAssetRegistry } = await import(\"./AudioAssetRegistry\");\n const musicAsset = audioAssetRegistry.getMusic(id);\n \n if (musicAsset) {\n try {\n await this.loadAsset(musicAsset);\n audio = this.soundCache.get(id);\n \n if (import.meta.env.DEV && audio) {\n console.log(`[AudioManager] Successfully loaded music \"${id}\" on-demand`);\n }\n } catch (error) {\n console.warn(`Failed to load music asset ${id} on-demand:`, error);\n return;\n }\n } else {\n console.warn(`Music asset not found in registry: ${id}`);\n return;\n }\n } finally {\n // Always remove from loading set when done\n // 완료 시 항상 로딩 세트에서 제거\n this.loadingMusic.delete(id);\n }\n }\n }\n\n if (audio) {\n try {\n audio.currentTime = 0;\n audio.volume = (volume ?? this._musicVolume) * this._masterVolume;\n audio.loop = true;\n this.currentMusic = audio;\n this._currentMusicTrack = id;\n await audio.play();\n \n if (import.meta.env.DEV) {\n console.log(`[AudioManager] Playing music: ${id} (volume: ${audio.volume.toFixed(2)})`);\n }\n } catch (error) {\n console.warn(`Failed to play music ${id}:`, error);\n }\n }\n }\n\n /**\n * Wait for a music asset to finish loading\n * 음악 자산 로드가 완료될 때까지 대기\n * @param id - Music track ID\n * @param maxWaitMs - Maximum time to wait in milliseconds\n */\n private async waitForMusicLoad(id: string, maxWaitMs: number = 5000): Promise<void> {\n const startTime = Date.now();\n const checkInterval = 100; // Check every 100ms\n \n while (this.loadingMusic.has(id)) {\n if (Date.now() - startTime > maxWaitMs) {\n console.warn(`[AudioManager] Timeout waiting for music \"${id}\" to load`);\n return;\n }\n await new Promise(resolve => setTimeout(resolve, checkInterval));\n }\n }\n\n stopMusic(): void {\n if (this.currentMusic) {\n this.currentMusic.pause();\n this.currentMusic.currentTime = 0;\n this.currentMusic = null;\n this._currentMusicTrack = null;\n }\n }\n\n stopAll(): void {\n this.stopMusic();\n this.soundCache.forEach((audio) => {\n if (!audio.paused) {\n audio.pause();\n audio.currentTime = 0;\n }\n });\n }\n\n setVolume(type: \"master\" | \"sfx\" | \"music\" | \"voice\", volume: number): void {\n const clampedVolume = Math.max(0, Math.min(1, volume));\n\n switch (type) {\n case \"master\":\n this._masterVolume = clampedVolume;\n break;\n case \"sfx\":\n this._sfxVolume = clampedVolume;\n break;\n case \"music\":\n this._musicVolume = clampedVolume;\n if (this.currentMusic) {\n this.currentMusic.volume = this._musicVolume * this._masterVolume;\n }\n break;\n case \"voice\":\n break;\n }\n }\n\n mute(): void {\n this._muted = true;\n if (this.currentMusic) {\n this.currentMusic.volume = 0;\n }\n }\n\n unmute(): void {\n this._muted = false;\n if (this.currentMusic) {\n this.currentMusic.volume = this._musicVolume * this._masterVolume;\n }\n }\n\n async fadeOut(duration: number = 1000): Promise<void> {\n if (!this.currentMusic) return;\n\n const musicElement = this.currentMusic;\n return new Promise((resolve) => {\n const startVolume = musicElement.volume;\n const fadeStep = startVolume / (duration / 50);\n\n const fadeInterval = setInterval(() => {\n if (this.currentMusic && this.currentMusic.volume > 0) {\n this.currentMusic.volume = Math.max(\n 0,\n this.currentMusic.volume - fadeStep\n );\n } else {\n clearInterval(fadeInterval);\n this.stopMusic();\n resolve();\n }\n }, 50);\n });\n }\n\n async fadeIn(trackId: MusicTrackId, duration: number = 1000): Promise<void> {\n await this.playMusic(trackId, 0);\n\n if (!this.currentMusic) return;\n\n return new Promise((resolve) => {\n const targetVolume = this._musicVolume * this._masterVolume;\n const fadeStep = targetVolume / (duration / 50);\n\n const fadeInterval = setInterval(() => {\n if (this.currentMusic && this.currentMusic.volume < targetVolume) {\n this.currentMusic.volume = Math.min(\n targetVolume,\n this.currentMusic.volume + fadeStep\n );\n } else {\n clearInterval(fadeInterval);\n resolve();\n }\n }, 50);\n });\n }\n\n async crossfade(\n fromTrackId: MusicTrackId,\n toTrackId: MusicTrackId,\n duration: number = 1000\n ): Promise<void> {\n const fadeOutPromise = this.fadeOut(duration);\n await fadeOutPromise;\n await this.fadeIn(toTrackId, duration);\n console.log(`Crossfaded from ${fromTrackId} to ${toTrackId}`);\n }\n\n /** @returns A snapshot of all currently loaded sound elements */\n getLoadedAssets(): ReadonlyMap<string, HTMLAudioElement> {\n return new Map(this.soundCache);\n }\n\n async playVoice(id: string): Promise<void> {\n return this.playSoundEffect(id);\n }\n\n async playKoreanTechniqueSound(\n techniqueId: string,\n archetype: string\n ): Promise<void> {\n const soundId = `${archetype}_${techniqueId}`;\n return this.playSoundEffect(soundId);\n }\n\n async playTrigramStanceSound(stance: string): Promise<void> {\n const soundId = `stance_${stance}`;\n return this.playSoundEffect(soundId);\n }\n\n async playVitalPointHitSound(severity: string): Promise<void> {\n const soundId = `vital_point_${severity}`;\n return this.playSoundEffect(soundId);\n }\n\n async playDojiangAmbience(): Promise<void> {\n return this.playMusic(\"dojang_ambience\");\n }\n\n // Legacy getters for backward compatibility\n getMasterVolume(): number {\n return this._masterVolume;\n }\n\n getMusicVolume(): number {\n return this._musicVolume;\n }\n\n getSfxVolume(): number {\n return this._sfxVolume;\n }\n\n get initialized(): boolean {\n return this._isInitialized;\n }\n\n /**\n * Batch load multiple assets with progress tracking\n * @param assets - Array of audio assets to load\n * @param options - Optional load configuration (timeout, retries, etc.)\n * @param onProgress - Optional callback for progress updates with loaded and total counts\n * @returns Promise that resolves when all assets are processed\n */\n async batchLoadAssets(\n assets: readonly AudioAsset[],\n options?: LoadOptions,\n onProgress?: (loaded: number, total: number) => void\n ): Promise<void> {\n const results = await this.assetLoader.batchLoad(\n assets,\n options,\n (progress) => {\n onProgress?.(progress.loaded, progress.total);\n }\n );\n\n for (let i = 0; i < results.length; i++) {\n const result = results[i];\n const asset = assets[i];\n\n if (result.success && result.audio) {\n result.audio.volume = asset.volume ?? 1.0;\n this.soundCache.set(asset.id, result.audio);\n\n // Create pool for frequently used sounds\n if (this.frequentSounds.has(asset.id)) {\n this.audioPool.createPool(asset.id, asset.url, {\n initialSize: 3,\n maxSize: 10,\n autoExpand: true,\n });\n }\n }\n }\n }\n\n /**\n * Unload an asset to free memory\n * @param assetId - ID of the asset to unload\n * @returns true if asset was unloaded, false if asset was not found\n */\n unloadAsset(assetId: string): boolean {\n const audio = this.soundCache.get(assetId);\n if (audio) {\n audio.pause();\n audio.src = \"\";\n this.soundCache.delete(assetId);\n }\n\n // LRU 캐시에서 제거\n const removedFromCache = this.audioCache.remove(assetId);\n\n const unloaded = this.assetLoader.unloadAsset(assetId);\n\n if (this.audioPool.hasPool(assetId)) {\n this.audioPool.removePool(assetId);\n }\n\n this.monitor.unregisterAsset(assetId);\n\n return unloaded || audio !== undefined || removedFromCache;\n }\n\n /**\n * Get cache statistics including LRU cache metrics\n * LRU 캐시 메트릭을 포함한 캐시 통계 가져오기\n *\n * @returns Cache statistics\n */\n getCacheStats(): {\n readonly lruCache: {\n readonly totalSize: number;\n readonly assetCount: number;\n readonly criticalCount: number;\n readonly utilizationPercent: number;\n readonly evictionCount: number;\n readonly hitCount: number;\n readonly missCount: number;\n readonly hitRate: number;\n };\n readonly soundCache: number;\n readonly poolStats: Map<string, PoolStatistics>;\n } {\n return {\n lruCache: this.audioCache.getStats(),\n soundCache: this.soundCache.size,\n poolStats: this.audioPool.getAllStatistics(),\n };\n }\n\n /**\n * Get memory statistics including total loaded MB, asset count, and warnings\n * @returns Memory statistics object\n */\n getMemoryStats(): AudioMemoryStats {\n return this.monitor.getMemoryStats();\n }\n\n /**\n * Get performance statistics including load times and playback latency\n * @returns Performance statistics object\n */\n getPerformanceStats(): AudioPerformanceStats {\n return this.monitor.getPerformanceStats();\n }\n\n /**\n * Get FPS impact analysis showing baseline vs current FPS and detected drops\n * @returns FPS impact analysis object\n */\n getFPSImpact(): AudioFPSImpact {\n return this.monitor.getFPSImpact();\n }\n\n /**\n * Update FPS measurement for monitoring\n * @param fps - Current frames per second measurement\n */\n updateFPS(fps: number): void {\n this.monitor.updateFPS(fps);\n }\n\n /**\n * Get comprehensive monitoring report including memory, performance, FPS, and warnings\n * @returns Comprehensive monitoring report object\n */\n getMonitoringReport(): {\n readonly memory: AudioMemoryStats;\n readonly performance: AudioPerformanceStats;\n readonly fps: AudioFPSImpact;\n readonly warnings: readonly MemoryWarning[];\n } {\n return this.monitor.getReport();\n }\n\n /**\n * Get all memory warnings\n * @returns Array of memory warnings\n */\n getMemoryWarnings(): readonly MemoryWarning[] {\n return this.monitor.getWarnings();\n }\n\n /**\n * Clear memory warnings\n */\n clearMemoryWarnings(): void {\n this.monitor.clearWarnings();\n }\n\n /**\n * Get pool statistics for a specific asset or all pools\n * @param assetId - Optional asset ID to get specific pool stats\n * @returns Pool statistics for the specified asset or all pools\n */\n getPoolStatistics(\n assetId?: string\n ): PoolStatistics | Map<string, PoolStatistics> | undefined {\n if (assetId) {\n return this.audioPool.getPoolStatistics(assetId);\n }\n return this.audioPool.getAllStatistics();\n }\n\n /**\n * Get loader statistics including cached assets and loading state\n * @returns Loader statistics object\n */\n getLoaderStatistics(): {\n readonly cached: number;\n readonly loading: number;\n readonly totalAttempts: number;\n } {\n return this.assetLoader.getStatistics();\n }\n}\n\nexport default AudioManager;\n"],"mappings":"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"}
1
+ {"version":3,"file":"AudioManager.js","names":[],"sources":["../../src/audio/AudioManager.ts"],"sourcesContent":["import { AudioAssetLoader, LoadOptions } from \"./AudioAssetLoader\";\nimport { AudioCache, AudioCacheConfig } from \"./AudioCache\";\nimport {\n AudioFPSImpact,\n AudioMemoryStats,\n AudioMonitor,\n AudioPerformanceStats,\n MemoryWarning,\n} from \"./AudioMonitor\";\nimport { AudioElementPool, PoolStatistics } from \"./AudioPool\";\nimport {\n AudioAsset,\n AudioConfig,\n IAudioManager,\n MusicTrackId,\n SoundEffectId,\n} from \"./types\";\n\n// Estimated average size per audio asset in MB (based on typical compressed audio file sizes)\nconst ESTIMATED_ASSET_SIZE_MB = 0.5;\n\n/**\n * Critical assets that should never be evicted from cache\n * 캐시에서 절대 제거되지 않아야 하는 중요한 자산\n */\nconst CRITICAL_AUDIO_ASSETS = [\n // Menu sounds - instant playback required\n \"menu_hover\",\n \"menu_select\",\n \"menu_click\",\n \"menu_navigate\",\n \"menu_back\",\n // Common combat sounds - instant playback required\n \"hit_impact\",\n \"hit_light\",\n \"hit_medium\",\n \"hit_heavy\",\n \"guard_block\",\n \"attack_whoosh\",\n \"attack_light\",\n \"stance_change\",\n] as const;\n\n/**\n * Manages audio playback for the Black Trigram combat game.\n *\n * Handles music tracks, sound effects, and Korean martial arts audio\n * including technique sounds, trigram stance audio, and vital point hit\n * feedback. Uses an LRU asset cache, pooled `HTMLAudioElement` instances,\n * and an `AudioMonitor` for performance tracking.\n *\n * @example\n * ```ts\n * const audio = new AudioManager({ masterVolume: 0.8 });\n * await audio.initialize();\n * await audio.playMusic(\"dojang_theme\");\n * await audio.playSFX(\"hit_medium\");\n * ```\n *\n * @korean 오디오관리자\n */\nexport class AudioManager implements IAudioManager {\n private _masterVolume: number = 1.0;\n private _musicVolume: number = 0.7;\n private _sfxVolume: number = 0.8;\n private _muted: boolean = false;\n private _currentMusicTrack: string | null = null;\n private _fallbackMode: boolean = false;\n private currentMusic: HTMLAudioElement | null = null;\n private soundCache: Map<string, HTMLAudioElement> = new Map();\n private _isInitialized: boolean = false;\n\n /** Music assets currently being loaded — prevents race conditions on concurrent playMusic calls */\n private loadingMusic: Set<string> = new Set();\n\n private assetLoader: AudioAssetLoader;\n private audioPool: AudioElementPool;\n private monitor: AudioMonitor;\n private audioCache: AudioCache;\n private frequentSounds: Set<string> = new Set([\n \"hit_light\",\n \"attack_light\",\n \"stance_change\",\n ]);\n\n constructor(config?: Partial<AudioConfig>) {\n if (config) {\n this._masterVolume = config.masterVolume ?? 1.0;\n this._musicVolume = config.musicVolume ?? 0.7;\n this._sfxVolume = config.sfxVolume ?? 0.8;\n }\n\n this.assetLoader = new AudioAssetLoader();\n this.audioPool = new AudioElementPool();\n this.monitor = new AudioMonitor();\n\n // Initialize AudioCache with 30MB limit and critical assets\n // 30MB 제한 및 중요 자산으로 AudioCache 초기화\n const cacheConfig: AudioCacheConfig = {\n maxSizeBytes: 30 * 1024 * 1024, // 30MB default\n criticalAssets: [...CRITICAL_AUDIO_ASSETS],\n debug: typeof process !== \"undefined\" && process?.env?.NODE_ENV === \"development\",\n };\n this.audioCache = new AudioCache(cacheConfig);\n }\n\n /**\n * Helper method to release pooled audio back to the pool after playback\n * @param id - Asset ID\n * @param audio - Audio element to release\n */\n private releasePooledAudio(id: string, audio: HTMLAudioElement): void {\n if (!audio.paused) {\n audio.addEventListener(\n \"ended\",\n () => {\n this.audioPool.release(id, audio);\n },\n { once: true }\n );\n } else {\n this.audioPool.release(id, audio);\n }\n }\n\n /** @returns Whether the audio system has been initialized */\n get isInitialized(): boolean {\n return this._isInitialized;\n }\n\n get fallbackMode(): boolean {\n return this._fallbackMode;\n }\n\n get currentMusicTrack(): string | null {\n return this._currentMusicTrack;\n }\n\n get masterVolume(): number {\n return this._masterVolume;\n }\n\n get sfxVolume(): number {\n return this._sfxVolume;\n }\n\n get musicVolume(): number {\n return this._musicVolume;\n }\n\n get muted(): boolean {\n return this._muted;\n }\n\n async initialize(config?: AudioConfig): Promise<void> {\n try {\n // Create AudioContext using globalThis for cross-platform compatibility\n const AudioContextClass =\n globalThis.AudioContext ||\n (globalThis as unknown as { webkitAudioContext: typeof AudioContext })\n .webkitAudioContext;\n new AudioContextClass();\n this._isInitialized = true;\n this._fallbackMode = false;\n\n if (config) {\n this._masterVolume = config.masterVolume ?? this._masterVolume;\n this._musicVolume = config.musicVolume ?? this._musicVolume;\n this._sfxVolume = config.sfxVolume ?? this._sfxVolume;\n }\n\n // Set memory threshold if configured\n if (config?.maxSimultaneousSounds) {\n const estimatedMemoryMB =\n config.maxSimultaneousSounds * ESTIMATED_ASSET_SIZE_MB;\n this.monitor.setMemoryThreshold(estimatedMemoryMB);\n }\n } catch (error) {\n console.warn(\n \"AudioContext initialization failed, using fallback mode:\",\n error\n );\n this._isInitialized = true;\n this._fallbackMode = true;\n }\n }\n\n async loadAsset(asset: AudioAsset, options?: LoadOptions): Promise<void> {\n const startTime = performance.now();\n\n try {\n // Check cache first - instant playback for cached assets\n // 캐시 먼저 확인 - 캐시된 자산의 즉시 재생\n const cachedAsset = this.audioCache.get(asset.id);\n if (cachedAsset) {\n // Asset already loaded in cache, use it\n const cached = this.soundCache.get(asset.id);\n if (cached) {\n return; // Already loaded and cached\n }\n }\n\n // Use faster timeouts for testing environment\n // Safely check for Node.js environment to avoid runtime errors in browsers\n const isTest =\n typeof process !== \"undefined\" && process?.env?.NODE_ENV === \"test\";\n const loadOptions: LoadOptions = {\n timeout: isTest ? 100 : 10000,\n maxRetries: isTest ? 1 : 3,\n retryDelay: isTest ? 10 : 1000,\n ...options,\n };\n\n // Use AudioAssetLoader with retry and fallback\n const result = await this.assetLoader.loadAsset(asset, loadOptions);\n\n if (result.success && result.audio) {\n result.audio.volume = asset.volume ?? 1.0;\n this.soundCache.set(asset.id, result.audio);\n\n // Estimate size for cache management\n // 캐시 관리를 위한 크기 추정\n const estimatedSize = this.estimateAudioSize(asset);\n\n // Add to LRU cache with size tracking\n // 크기 추적과 함께 LRU 캐시에 추가\n this.audioCache.set(asset.id, asset, estimatedSize);\n\n // Track performance and memory\n const loadTime = performance.now() - startTime;\n this.monitor.recordLoad(asset.id, loadTime, estimatedSize / (1024 * 1024));\n\n // Create pool for frequently used sounds\n if (this.frequentSounds.has(asset.id)) {\n this.audioPool.createPool(asset.id, asset.url, {\n initialSize: 3,\n maxSize: 10,\n autoExpand: true,\n });\n }\n } else {\n // Record failure\n const error = result.error ?? new Error(\"Unknown load error\");\n this.monitor.recordLoadFailure(asset.id, error);\n\n // Still cache the placeholder audio if available\n if (result.audio) {\n this.soundCache.set(asset.id, result.audio);\n\n // Add to cache even if failed (placeholder)\n const estimatedSize = this.estimateAudioSize(asset);\n this.audioCache.set(asset.id, asset, estimatedSize);\n }\n }\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error));\n console.warn(`Failed to load audio asset ${asset.id}:`, err);\n this.monitor.recordLoadFailure(asset.id, err);\n }\n }\n\n /**\n * Estimate audio asset size in bytes\n * 바이트 단위의 오디오 자산 크기 추정\n *\n * @returns Estimated size in bytes\n */\n private estimateAudioSize(_asset: AudioAsset): number {\n // Use ESTIMATED_ASSET_SIZE_MB constant for consistent estimation\n // Conservative estimate: ~0.5MB per audio file (compressed MP3/WebM)\n return ESTIMATED_ASSET_SIZE_MB * 1024 * 1024;\n }\n\n async playSoundEffect(id: SoundEffectId): Promise<void> {\n if (this._muted) return;\n\n const playbackStart = performance.now();\n\n try {\n // Update cache access time for LRU tracking\n // LRU 추적을 위한 캐시 액세스 시간 업데이트\n this.audioCache.get(id);\n\n // Use pool for frequently played sounds\n if (this.frequentSounds.has(id) && this.audioPool.hasPool(id)) {\n const audio = this.audioPool.acquire(id);\n if (audio) {\n audio.volume = this._sfxVolume * this._masterVolume;\n await audio.play();\n\n // Release back to pool after playback (non-blocking)\n this.releasePooledAudio(id, audio);\n\n const latency = performance.now() - playbackStart;\n this.monitor.recordPlaybackLatency(latency);\n return;\n }\n }\n\n // Fallback to cached audio\n const audio = this.soundCache.get(id);\n if (audio) {\n audio.currentTime = 0;\n audio.volume = this._sfxVolume * this._masterVolume;\n await audio.play();\n\n const latency = performance.now() - playbackStart;\n this.monitor.recordPlaybackLatency(latency);\n }\n } catch (error) {\n console.warn(`Failed to play sound effect ${id}:`, error);\n }\n }\n\n /**\n * Alias for {@link playSoundEffect} — preferred for explicit volume control.\n * @param id - Sound effect identifier\n * @param volume - Override volume (defaults to sfxVolume × masterVolume)\n */\n async playSFX(id: SoundEffectId, volume?: number): Promise<void> {\n if (this._muted) return;\n\n const playbackStart = performance.now();\n\n try {\n // Update cache access time for LRU tracking\n // LRU 추적을 위한 캐시 액세스 시간 업데이트\n this.audioCache.get(id);\n\n // Use pool for frequently played sounds\n if (this.frequentSounds.has(id) && this.audioPool.hasPool(id)) {\n const audio = this.audioPool.acquire(id);\n if (audio) {\n audio.volume = (volume ?? this._sfxVolume) * this._masterVolume;\n await audio.play();\n\n // Release back to pool after playback (non-blocking)\n this.releasePooledAudio(id, audio);\n\n const latency = performance.now() - playbackStart;\n this.monitor.recordPlaybackLatency(latency);\n return;\n }\n }\n\n // Fallback to cached audio\n const audio = this.soundCache.get(id);\n if (audio) {\n audio.currentTime = 0;\n audio.volume = (volume ?? this._sfxVolume) * this._masterVolume;\n await audio.play();\n\n const latency = performance.now() - playbackStart;\n this.monitor.recordPlaybackLatency(latency);\n }\n } catch (error) {\n console.warn(`Failed to play sound effect ${id}:`, error);\n }\n }\n\n async playMusic(id: MusicTrackId, volume?: number): Promise<void> {\n if (this._muted) return;\n\n this.stopMusic();\n\n // Update cache access time for LRU tracking\n // LRU 추적을 위한 캐시 액세스 시간 업데이트\n this.audioCache.get(id);\n\n let audio = this.soundCache.get(id);\n \n // On-demand loading: If music not in cache, load it first\n // 온디맨드 로딩: 음악이 캐시에 없으면 먼저 로드\n if (!audio) {\n // Check if this music is already being loaded to prevent race conditions\n // JavaScript's single-threaded nature ensures atomic check-and-add operations\n // 레이스 컨디션 방지를 위해 이미 로드 중인지 확인\n // JavaScript의 싱글 스레드 특성으로 원자적 check-and-add 작업 보장\n if (this.loadingMusic.has(id)) {\n if (import.meta.env.DEV) {\n console.log(`[AudioManager] Music \"${id}\" is already being loaded, waiting...`);\n }\n // Wait for the ongoing load to complete\n // 진행 중인 로드가 완료될 때까지 대기\n await this.waitForMusicLoad(id);\n audio = this.soundCache.get(id);\n } else {\n // Mark as loading before starting the load (atomic operation in single-threaded JS)\n // 로드 시작 전에 로딩 중으로 표시 (싱글 스레드 JS에서 원자적 작업)\n this.loadingMusic.add(id);\n \n if (import.meta.env.DEV) {\n console.log(`[AudioManager] Music \"${id}\" not in cache, loading on-demand...`);\n }\n \n try {\n const { audioAssetRegistry } = await import(\"./AudioAssetRegistry\");\n const musicAsset = audioAssetRegistry.getMusic(id);\n \n if (musicAsset) {\n try {\n await this.loadAsset(musicAsset);\n audio = this.soundCache.get(id);\n \n if (import.meta.env.DEV && audio) {\n console.log(`[AudioManager] Successfully loaded music \"${id}\" on-demand`);\n }\n } catch (error) {\n console.warn(`Failed to load music asset ${id} on-demand:`, error);\n return;\n }\n } else {\n console.warn(`Music asset not found in registry: ${id}`);\n return;\n }\n } finally {\n // Always remove from loading set when done\n // 완료 시 항상 로딩 세트에서 제거\n this.loadingMusic.delete(id);\n }\n }\n }\n\n if (audio) {\n try {\n audio.currentTime = 0;\n audio.volume = (volume ?? this._musicVolume) * this._masterVolume;\n audio.loop = true;\n this.currentMusic = audio;\n this._currentMusicTrack = id;\n await audio.play();\n \n if (import.meta.env.DEV) {\n console.log(`[AudioManager] Playing music: ${id} (volume: ${audio.volume.toFixed(2)})`);\n }\n } catch (error) {\n console.warn(`Failed to play music ${id}:`, error);\n }\n }\n }\n\n /**\n * Wait for a music asset to finish loading\n * 음악 자산 로드가 완료될 때까지 대기\n * @param id - Music track ID\n * @param maxWaitMs - Maximum time to wait in milliseconds\n */\n private async waitForMusicLoad(id: string, maxWaitMs: number = 5000): Promise<void> {\n const startTime = Date.now();\n const checkInterval = 100; // Check every 100ms\n \n while (this.loadingMusic.has(id)) {\n if (Date.now() - startTime > maxWaitMs) {\n console.warn(`[AudioManager] Timeout waiting for music \"${id}\" to load`);\n return;\n }\n await new Promise(resolve => setTimeout(resolve, checkInterval));\n }\n }\n\n stopMusic(): void {\n if (this.currentMusic) {\n this.currentMusic.pause();\n this.currentMusic.currentTime = 0;\n this.currentMusic = null;\n this._currentMusicTrack = null;\n }\n }\n\n stopAll(): void {\n this.stopMusic();\n this.soundCache.forEach((audio) => {\n if (!audio.paused) {\n audio.pause();\n audio.currentTime = 0;\n }\n });\n }\n\n setVolume(type: \"master\" | \"sfx\" | \"music\" | \"voice\", volume: number): void {\n const clampedVolume = Math.max(0, Math.min(1, volume));\n\n switch (type) {\n case \"master\":\n this._masterVolume = clampedVolume;\n break;\n case \"sfx\":\n this._sfxVolume = clampedVolume;\n break;\n case \"music\":\n this._musicVolume = clampedVolume;\n if (this.currentMusic) {\n this.currentMusic.volume = this._musicVolume * this._masterVolume;\n }\n break;\n case \"voice\":\n break;\n }\n }\n\n mute(): void {\n this._muted = true;\n if (this.currentMusic) {\n this.currentMusic.volume = 0;\n }\n }\n\n unmute(): void {\n this._muted = false;\n if (this.currentMusic) {\n this.currentMusic.volume = this._musicVolume * this._masterVolume;\n }\n }\n\n async fadeOut(duration: number = 1000): Promise<void> {\n if (!this.currentMusic) return;\n\n const musicElement = this.currentMusic;\n return new Promise((resolve) => {\n const startVolume = musicElement.volume;\n const fadeStep = startVolume / (duration / 50);\n\n const fadeInterval = setInterval(() => {\n if (this.currentMusic && this.currentMusic.volume > 0) {\n this.currentMusic.volume = Math.max(\n 0,\n this.currentMusic.volume - fadeStep\n );\n } else {\n clearInterval(fadeInterval);\n this.stopMusic();\n resolve();\n }\n }, 50);\n });\n }\n\n async fadeIn(trackId: MusicTrackId, duration: number = 1000): Promise<void> {\n await this.playMusic(trackId, 0);\n\n if (!this.currentMusic) return;\n\n return new Promise((resolve) => {\n const targetVolume = this._musicVolume * this._masterVolume;\n const fadeStep = targetVolume / (duration / 50);\n\n const fadeInterval = setInterval(() => {\n if (this.currentMusic && this.currentMusic.volume < targetVolume) {\n this.currentMusic.volume = Math.min(\n targetVolume,\n this.currentMusic.volume + fadeStep\n );\n } else {\n clearInterval(fadeInterval);\n resolve();\n }\n }, 50);\n });\n }\n\n async crossfade(\n fromTrackId: MusicTrackId,\n toTrackId: MusicTrackId,\n duration: number = 1000\n ): Promise<void> {\n const fadeOutPromise = this.fadeOut(duration);\n await fadeOutPromise;\n await this.fadeIn(toTrackId, duration);\n console.log(`Crossfaded from ${fromTrackId} to ${toTrackId}`);\n }\n\n /** @returns A snapshot of all currently loaded sound elements */\n getLoadedAssets(): ReadonlyMap<string, HTMLAudioElement> {\n return new Map(this.soundCache);\n }\n\n async playVoice(id: string): Promise<void> {\n return this.playSoundEffect(id);\n }\n\n async playKoreanTechniqueSound(\n techniqueId: string,\n archetype: string\n ): Promise<void> {\n const soundId = `${archetype}_${techniqueId}`;\n return this.playSoundEffect(soundId);\n }\n\n async playTrigramStanceSound(stance: string): Promise<void> {\n const soundId = `stance_${stance}`;\n return this.playSoundEffect(soundId);\n }\n\n async playVitalPointHitSound(severity: string): Promise<void> {\n const soundId = `vital_point_${severity}`;\n return this.playSoundEffect(soundId);\n }\n\n async playDojiangAmbience(): Promise<void> {\n return this.playMusic(\"dojang_ambience\");\n }\n\n // Legacy getters for backward compatibility\n getMasterVolume(): number {\n return this._masterVolume;\n }\n\n getMusicVolume(): number {\n return this._musicVolume;\n }\n\n getSfxVolume(): number {\n return this._sfxVolume;\n }\n\n get initialized(): boolean {\n return this._isInitialized;\n }\n\n /**\n * Batch load multiple assets with progress tracking\n * @param assets - Array of audio assets to load\n * @param options - Optional load configuration (timeout, retries, etc.)\n * @param onProgress - Optional callback for progress updates with loaded and total counts\n * @returns Promise that resolves when all assets are processed\n */\n async batchLoadAssets(\n assets: readonly AudioAsset[],\n options?: LoadOptions,\n onProgress?: (loaded: number, total: number) => void\n ): Promise<void> {\n const results = await this.assetLoader.batchLoad(\n assets,\n options,\n (progress) => {\n onProgress?.(progress.loaded, progress.total);\n }\n );\n\n for (let i = 0; i < results.length; i++) {\n const result = results[i];\n const asset = assets[i];\n\n if (result.success && result.audio) {\n result.audio.volume = asset.volume ?? 1.0;\n this.soundCache.set(asset.id, result.audio);\n\n // Create pool for frequently used sounds\n if (this.frequentSounds.has(asset.id)) {\n this.audioPool.createPool(asset.id, asset.url, {\n initialSize: 3,\n maxSize: 10,\n autoExpand: true,\n });\n }\n }\n }\n }\n\n /**\n * Unload an asset to free memory\n * @param assetId - ID of the asset to unload\n * @returns true if asset was unloaded, false if asset was not found\n */\n unloadAsset(assetId: string): boolean {\n const audio = this.soundCache.get(assetId);\n if (audio) {\n audio.pause();\n audio.src = \"\";\n this.soundCache.delete(assetId);\n }\n\n // LRU 캐시에서 제거\n const removedFromCache = this.audioCache.remove(assetId);\n\n const unloaded = this.assetLoader.unloadAsset(assetId);\n\n if (this.audioPool.hasPool(assetId)) {\n this.audioPool.removePool(assetId);\n }\n\n this.monitor.unregisterAsset(assetId);\n\n return unloaded || audio !== undefined || removedFromCache;\n }\n\n /**\n * Get cache statistics including LRU cache metrics\n * LRU 캐시 메트릭을 포함한 캐시 통계 가져오기\n *\n * @returns Cache statistics\n */\n getCacheStats(): {\n readonly lruCache: {\n readonly totalSize: number;\n readonly assetCount: number;\n readonly criticalCount: number;\n readonly utilizationPercent: number;\n readonly evictionCount: number;\n readonly hitCount: number;\n readonly missCount: number;\n readonly hitRate: number;\n };\n readonly soundCache: number;\n readonly poolStats: Map<string, PoolStatistics>;\n } {\n return {\n lruCache: this.audioCache.getStats(),\n soundCache: this.soundCache.size,\n poolStats: this.audioPool.getAllStatistics(),\n };\n }\n\n /**\n * Get memory statistics including total loaded MB, asset count, and warnings\n * @returns Memory statistics object\n */\n getMemoryStats(): AudioMemoryStats {\n return this.monitor.getMemoryStats();\n }\n\n /**\n * Get performance statistics including load times and playback latency\n * @returns Performance statistics object\n */\n getPerformanceStats(): AudioPerformanceStats {\n return this.monitor.getPerformanceStats();\n }\n\n /**\n * Get FPS impact analysis showing baseline vs current FPS and detected drops\n * @returns FPS impact analysis object\n */\n getFPSImpact(): AudioFPSImpact {\n return this.monitor.getFPSImpact();\n }\n\n /**\n * Update FPS measurement for monitoring\n * @param fps - Current frames per second measurement\n */\n updateFPS(fps: number): void {\n this.monitor.updateFPS(fps);\n }\n\n /**\n * Get comprehensive monitoring report including memory, performance, FPS, and warnings\n * @returns Comprehensive monitoring report object\n */\n getMonitoringReport(): {\n readonly memory: AudioMemoryStats;\n readonly performance: AudioPerformanceStats;\n readonly fps: AudioFPSImpact;\n readonly warnings: readonly MemoryWarning[];\n } {\n return this.monitor.getReport();\n }\n\n /**\n * Get all memory warnings\n * @returns Array of memory warnings\n */\n getMemoryWarnings(): readonly MemoryWarning[] {\n return this.monitor.getWarnings();\n }\n\n /**\n * Clear memory warnings\n */\n clearMemoryWarnings(): void {\n this.monitor.clearWarnings();\n }\n\n /**\n * Get pool statistics for a specific asset or all pools\n * @param assetId - Optional asset ID to get specific pool stats\n * @returns Pool statistics for the specified asset or all pools\n */\n getPoolStatistics(\n assetId?: string\n ): PoolStatistics | Map<string, PoolStatistics> | undefined {\n if (assetId) {\n return this.audioPool.getPoolStatistics(assetId);\n }\n return this.audioPool.getAllStatistics();\n }\n\n /**\n * Get loader statistics including cached assets and loading state\n * @returns Loader statistics object\n */\n getLoaderStatistics(): {\n readonly cached: number;\n readonly loading: number;\n readonly totalAttempts: number;\n } {\n return this.assetLoader.getStatistics();\n }\n}\n\nexport default AudioManager;\n"],"mappings":"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"}
@@ -27,10 +27,12 @@ var ObjectPool = class {
27
27
  acquire() {
28
28
  this.acquisitionCount++;
29
29
  let obj = this.available.pop();
30
- if (!obj) if (this.config.autoExpand && this.canExpand()) obj = this.createObject();
31
- else {
32
- console.warn("Pool exhausted and cannot expand");
33
- return null;
30
+ if (!obj) {
31
+ if (this.config.autoExpand && this.canExpand()) obj = this.createObject();
32
+ else {
33
+ console.warn("Pool exhausted and cannot expand");
34
+ return null;
35
+ }
34
36
  }
35
37
  this.inUse.add(obj);
36
38
  return obj;
@@ -1 +1 @@
1
- {"version":3,"file":"AudioPool.js","names":[],"sources":["../../src/audio/AudioPool.ts"],"sourcesContent":["/**\n * AudioPool - Object pooling for audio instances\n * Reduces memory allocation overhead for frequently played sounds\n */\n\nexport interface PoolStatistics {\n readonly available: number;\n readonly inUse: number;\n readonly total: number;\n readonly acquisitions: number;\n readonly releases: number;\n readonly created: number;\n}\n\nexport interface PoolConfig {\n readonly initialSize?: number;\n readonly maxSize?: number;\n readonly autoExpand?: boolean;\n}\n\n/**\n * Generic object pool implementation\n */\nexport class ObjectPool<T> {\n private available: T[] = [];\n private inUse = new Set<T>();\n private acquisitionCount = 0;\n private releaseCount = 0;\n private createdCount = 0;\n\n constructor(\n private readonly factory: () => T,\n private readonly reset: (obj: T) => void,\n private readonly config: Required<PoolConfig> = {\n initialSize: 10,\n maxSize: 100,\n autoExpand: true,\n }\n ) {\n // Pre-populate pool\n for (let i = 0; i < config.initialSize; i++) {\n this.available.push(this.createObject());\n }\n }\n\n /**\n * Acquire an object from the pool\n */\n acquire(): T | null {\n this.acquisitionCount++;\n\n let obj = this.available.pop();\n\n if (!obj) {\n // Pool is empty\n if (this.config.autoExpand && this.canExpand()) {\n obj = this.createObject();\n } else {\n console.warn(\"Pool exhausted and cannot expand\");\n return null;\n }\n }\n\n this.inUse.add(obj);\n return obj;\n }\n\n /**\n * Release an object back to the pool\n */\n release(obj: T): void {\n if (!this.inUse.has(obj)) {\n console.warn(\"Attempted to release object not in use\");\n return;\n }\n\n this.releaseCount++;\n this.reset(obj);\n this.inUse.delete(obj);\n this.available.push(obj);\n }\n\n /**\n * Release all objects back to the pool\n * Resets all in-use objects and returns them to the available pool\n */\n releaseAll(): void {\n this.inUse.forEach((obj) => {\n this.releaseCount++;\n this.reset(obj);\n this.available.push(obj);\n });\n this.inUse.clear();\n }\n\n /**\n * Get pool statistics\n */\n getStatistics(): PoolStatistics {\n return {\n available: this.available.length,\n inUse: this.inUse.size,\n total: this.available.length + this.inUse.size,\n acquisitions: this.acquisitionCount,\n releases: this.releaseCount,\n created: this.createdCount,\n };\n }\n\n /**\n * Clear the pool and dispose all objects\n */\n clear(): void {\n this.available = [];\n this.inUse.clear();\n }\n\n private createObject(): T {\n this.createdCount++;\n return this.factory();\n }\n\n private canExpand(): boolean {\n const total = this.available.length + this.inUse.size;\n return total < this.config.maxSize;\n }\n}\n\n/**\n * Specialized audio pool for HTMLAudioElement instances\n */\nexport class AudioElementPool {\n private pools: Map<string, ObjectPool<HTMLAudioElement>> = new Map();\n private defaultConfig: Required<PoolConfig> = {\n initialSize: 5,\n maxSize: 20,\n autoExpand: true,\n };\n\n /**\n * Create a pool for a specific audio asset\n */\n createPool(\n assetId: string,\n audioSrc: string,\n config?: PoolConfig\n ): ObjectPool<HTMLAudioElement> {\n const existingPool = this.pools.get(assetId);\n if (existingPool) {\n return existingPool;\n }\n\n const poolConfig = { ...this.defaultConfig, ...config };\n\n const pool = new ObjectPool<HTMLAudioElement>(\n () => {\n const audio = new Audio(audioSrc);\n audio.preload = \"auto\";\n audio.load();\n return audio;\n },\n (audio) => {\n audio.pause();\n audio.currentTime = 0;\n },\n poolConfig\n );\n\n this.pools.set(assetId, pool);\n return pool;\n }\n\n /**\n * Get pool for an asset (creates if doesn't exist)\n */\n getPool(assetId: string): ObjectPool<HTMLAudioElement> | undefined {\n return this.pools.get(assetId);\n }\n\n /**\n * Acquire audio element from pool\n */\n acquire(assetId: string): HTMLAudioElement | null {\n const pool = this.pools.get(assetId);\n if (!pool) {\n console.warn(`No pool exists for asset: ${assetId}`);\n return null;\n }\n return pool.acquire();\n }\n\n /**\n * Release audio element back to pool\n */\n release(assetId: string, audio: HTMLAudioElement): void {\n const pool = this.pools.get(assetId);\n if (!pool) {\n console.warn(`No pool exists for asset: ${assetId}`);\n return;\n }\n pool.release(audio);\n }\n\n /**\n * Release all audio elements in all pools\n */\n releaseAll(): void {\n this.pools.forEach((pool) => pool.releaseAll());\n }\n\n /**\n * Get statistics for a specific pool\n */\n getPoolStatistics(assetId: string): PoolStatistics | undefined {\n const pool = this.pools.get(assetId);\n return pool?.getStatistics();\n }\n\n /**\n * Get statistics for all pools\n */\n getAllStatistics(): Map<string, PoolStatistics> {\n const stats = new Map<string, PoolStatistics>();\n this.pools.forEach((pool, assetId) => {\n stats.set(assetId, pool.getStatistics());\n });\n return stats;\n }\n\n /**\n * Remove a pool and dispose its resources\n * @param assetId - The ID of the asset whose pool should be removed\n * @returns true if the pool existed and was removed, false if no pool was found for the asset\n */\n removePool(assetId: string): boolean {\n const pool = this.pools.get(assetId);\n if (pool) {\n pool.clear();\n this.pools.delete(assetId);\n return true;\n }\n return false;\n }\n\n /**\n * Clear all pools\n */\n clearAll(): void {\n this.pools.forEach((pool) => pool.clear());\n this.pools.clear();\n }\n\n /**\n * Check if pool exists for asset\n */\n hasPool(assetId: string): boolean {\n return this.pools.has(assetId);\n }\n\n /**\n * Get total number of pools\n */\n getPoolCount(): number {\n return this.pools.size;\n }\n}\n"],"mappings":";;;;AAuBA,IAAa,aAAb,MAA2B;CAQN;CACA;CACA;CATnB,YAAyB,CAAC;CAC1B,wBAAgB,IAAI,IAAO;CAC3B,mBAA2B;CAC3B,eAAuB;CACvB,eAAuB;CAEvB,YACE,SACA,OACA,SAAgD;EAC9C,aAAa;EACb,SAAS;EACT,YAAY;CACd,GACA;EAPiB,KAAA,UAAA;EACA,KAAA,QAAA;EACA,KAAA,SAAA;EAOjB,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,aAAa,KACtC,KAAK,UAAU,KAAK,KAAK,aAAa,CAAC;CAE3C;;;;CAKA,UAAoB;EAClB,KAAK;EAEL,IAAI,MAAM,KAAK,UAAU,IAAI;EAE7B,IAAI,CAAC,KAEH,IAAI,KAAK,OAAO,cAAc,KAAK,UAAU,GAC3C,MAAM,KAAK,aAAa;OACnB;GACL,QAAQ,KAAK,kCAAkC;GAC/C,OAAO;EACT;EAGF,KAAK,MAAM,IAAI,GAAG;EAClB,OAAO;CACT;;;;CAKA,QAAQ,KAAc;EACpB,IAAI,CAAC,KAAK,MAAM,IAAI,GAAG,GAAG;GACxB,QAAQ,KAAK,wCAAwC;GACrD;EACF;EAEA,KAAK;EACL,KAAK,MAAM,GAAG;EACd,KAAK,MAAM,OAAO,GAAG;EACrB,KAAK,UAAU,KAAK,GAAG;CACzB;;;;;CAMA,aAAmB;EACjB,KAAK,MAAM,SAAS,QAAQ;GAC1B,KAAK;GACL,KAAK,MAAM,GAAG;GACd,KAAK,UAAU,KAAK,GAAG;EACzB,CAAC;EACD,KAAK,MAAM,MAAM;CACnB;;;;CAKA,gBAAgC;EAC9B,OAAO;GACL,WAAW,KAAK,UAAU;GAC1B,OAAO,KAAK,MAAM;GAClB,OAAO,KAAK,UAAU,SAAS,KAAK,MAAM;GAC1C,cAAc,KAAK;GACnB,UAAU,KAAK;GACf,SAAS,KAAK;EAChB;CACF;;;;CAKA,QAAc;EACZ,KAAK,YAAY,CAAC;EAClB,KAAK,MAAM,MAAM;CACnB;CAEA,eAA0B;EACxB,KAAK;EACL,OAAO,KAAK,QAAQ;CACtB;CAEA,YAA6B;EAE3B,OADc,KAAK,UAAU,SAAS,KAAK,MAAM,OAClC,KAAK,OAAO;CAC7B;AACF;;;;AAKA,IAAa,mBAAb,MAA8B;CAC5B,wBAA2D,IAAI,IAAI;CACnE,gBAA8C;EAC5C,aAAa;EACb,SAAS;EACT,YAAY;CACd;;;;CAKA,WACE,SACA,UACA,QAC8B;EAC9B,MAAM,eAAe,KAAK,MAAM,IAAI,OAAO;EAC3C,IAAI,cACF,OAAO;EAKT,MAAM,OAAO,IAAI,iBACT;GACJ,MAAM,QAAQ,IAAI,MAAM,QAAQ;GAChC,MAAM,UAAU;GAChB,MAAM,KAAK;GACX,OAAO;EACT,IACC,UAAU;GACT,MAAM,MAAM;GACZ,MAAM,cAAc;EACtB,GACA;GAbmB,GAAG,KAAK;GAAe,GAAG;EAa7C,CACF;EAEA,KAAK,MAAM,IAAI,SAAS,IAAI;EAC5B,OAAO;CACT;;;;CAKA,QAAQ,SAA2D;EACjE,OAAO,KAAK,MAAM,IAAI,OAAO;CAC/B;;;;CAKA,QAAQ,SAA0C;EAChD,MAAM,OAAO,KAAK,MAAM,IAAI,OAAO;EACnC,IAAI,CAAC,MAAM;GACT,QAAQ,KAAK,6BAA6B,SAAS;GACnD,OAAO;EACT;EACA,OAAO,KAAK,QAAQ;CACtB;;;;CAKA,QAAQ,SAAiB,OAA+B;EACtD,MAAM,OAAO,KAAK,MAAM,IAAI,OAAO;EACnC,IAAI,CAAC,MAAM;GACT,QAAQ,KAAK,6BAA6B,SAAS;GACnD;EACF;EACA,KAAK,QAAQ,KAAK;CACpB;;;;CAKA,aAAmB;EACjB,KAAK,MAAM,SAAS,SAAS,KAAK,WAAW,CAAC;CAChD;;;;CAKA,kBAAkB,SAA6C;EAE7D,OADa,KAAK,MAAM,IAAI,OACrB,CAAA,EAAM,cAAc;CAC7B;;;;CAKA,mBAAgD;EAC9C,MAAM,wBAAQ,IAAI,IAA4B;EAC9C,KAAK,MAAM,SAAS,MAAM,YAAY;GACpC,MAAM,IAAI,SAAS,KAAK,cAAc,CAAC;EACzC,CAAC;EACD,OAAO;CACT;;;;;;CAOA,WAAW,SAA0B;EACnC,MAAM,OAAO,KAAK,MAAM,IAAI,OAAO;EACnC,IAAI,MAAM;GACR,KAAK,MAAM;GACX,KAAK,MAAM,OAAO,OAAO;GACzB,OAAO;EACT;EACA,OAAO;CACT;;;;CAKA,WAAiB;EACf,KAAK,MAAM,SAAS,SAAS,KAAK,MAAM,CAAC;EACzC,KAAK,MAAM,MAAM;CACnB;;;;CAKA,QAAQ,SAA0B;EAChC,OAAO,KAAK,MAAM,IAAI,OAAO;CAC/B;;;;CAKA,eAAuB;EACrB,OAAO,KAAK,MAAM;CACpB;AACF"}
1
+ {"version":3,"file":"AudioPool.js","names":[],"sources":["../../src/audio/AudioPool.ts"],"sourcesContent":["/**\n * AudioPool - Object pooling for audio instances\n * Reduces memory allocation overhead for frequently played sounds\n */\n\nexport interface PoolStatistics {\n readonly available: number;\n readonly inUse: number;\n readonly total: number;\n readonly acquisitions: number;\n readonly releases: number;\n readonly created: number;\n}\n\nexport interface PoolConfig {\n readonly initialSize?: number;\n readonly maxSize?: number;\n readonly autoExpand?: boolean;\n}\n\n/**\n * Generic object pool implementation\n */\nexport class ObjectPool<T> {\n private available: T[] = [];\n private inUse = new Set<T>();\n private acquisitionCount = 0;\n private releaseCount = 0;\n private createdCount = 0;\n\n constructor(\n private readonly factory: () => T,\n private readonly reset: (obj: T) => void,\n private readonly config: Required<PoolConfig> = {\n initialSize: 10,\n maxSize: 100,\n autoExpand: true,\n }\n ) {\n // Pre-populate pool\n for (let i = 0; i < config.initialSize; i++) {\n this.available.push(this.createObject());\n }\n }\n\n /**\n * Acquire an object from the pool\n */\n acquire(): T | null {\n this.acquisitionCount++;\n\n let obj = this.available.pop();\n\n if (!obj) {\n // Pool is empty\n if (this.config.autoExpand && this.canExpand()) {\n obj = this.createObject();\n } else {\n console.warn(\"Pool exhausted and cannot expand\");\n return null;\n }\n }\n\n this.inUse.add(obj);\n return obj;\n }\n\n /**\n * Release an object back to the pool\n */\n release(obj: T): void {\n if (!this.inUse.has(obj)) {\n console.warn(\"Attempted to release object not in use\");\n return;\n }\n\n this.releaseCount++;\n this.reset(obj);\n this.inUse.delete(obj);\n this.available.push(obj);\n }\n\n /**\n * Release all objects back to the pool\n * Resets all in-use objects and returns them to the available pool\n */\n releaseAll(): void {\n this.inUse.forEach((obj) => {\n this.releaseCount++;\n this.reset(obj);\n this.available.push(obj);\n });\n this.inUse.clear();\n }\n\n /**\n * Get pool statistics\n */\n getStatistics(): PoolStatistics {\n return {\n available: this.available.length,\n inUse: this.inUse.size,\n total: this.available.length + this.inUse.size,\n acquisitions: this.acquisitionCount,\n releases: this.releaseCount,\n created: this.createdCount,\n };\n }\n\n /**\n * Clear the pool and dispose all objects\n */\n clear(): void {\n this.available = [];\n this.inUse.clear();\n }\n\n private createObject(): T {\n this.createdCount++;\n return this.factory();\n }\n\n private canExpand(): boolean {\n const total = this.available.length + this.inUse.size;\n return total < this.config.maxSize;\n }\n}\n\n/**\n * Specialized audio pool for HTMLAudioElement instances\n */\nexport class AudioElementPool {\n private pools: Map<string, ObjectPool<HTMLAudioElement>> = new Map();\n private defaultConfig: Required<PoolConfig> = {\n initialSize: 5,\n maxSize: 20,\n autoExpand: true,\n };\n\n /**\n * Create a pool for a specific audio asset\n */\n createPool(\n assetId: string,\n audioSrc: string,\n config?: PoolConfig\n ): ObjectPool<HTMLAudioElement> {\n const existingPool = this.pools.get(assetId);\n if (existingPool) {\n return existingPool;\n }\n\n const poolConfig = { ...this.defaultConfig, ...config };\n\n const pool = new ObjectPool<HTMLAudioElement>(\n () => {\n const audio = new Audio(audioSrc);\n audio.preload = \"auto\";\n audio.load();\n return audio;\n },\n (audio) => {\n audio.pause();\n audio.currentTime = 0;\n },\n poolConfig\n );\n\n this.pools.set(assetId, pool);\n return pool;\n }\n\n /**\n * Get pool for an asset (creates if doesn't exist)\n */\n getPool(assetId: string): ObjectPool<HTMLAudioElement> | undefined {\n return this.pools.get(assetId);\n }\n\n /**\n * Acquire audio element from pool\n */\n acquire(assetId: string): HTMLAudioElement | null {\n const pool = this.pools.get(assetId);\n if (!pool) {\n console.warn(`No pool exists for asset: ${assetId}`);\n return null;\n }\n return pool.acquire();\n }\n\n /**\n * Release audio element back to pool\n */\n release(assetId: string, audio: HTMLAudioElement): void {\n const pool = this.pools.get(assetId);\n if (!pool) {\n console.warn(`No pool exists for asset: ${assetId}`);\n return;\n }\n pool.release(audio);\n }\n\n /**\n * Release all audio elements in all pools\n */\n releaseAll(): void {\n this.pools.forEach((pool) => pool.releaseAll());\n }\n\n /**\n * Get statistics for a specific pool\n */\n getPoolStatistics(assetId: string): PoolStatistics | undefined {\n const pool = this.pools.get(assetId);\n return pool?.getStatistics();\n }\n\n /**\n * Get statistics for all pools\n */\n getAllStatistics(): Map<string, PoolStatistics> {\n const stats = new Map<string, PoolStatistics>();\n this.pools.forEach((pool, assetId) => {\n stats.set(assetId, pool.getStatistics());\n });\n return stats;\n }\n\n /**\n * Remove a pool and dispose its resources\n * @param assetId - The ID of the asset whose pool should be removed\n * @returns true if the pool existed and was removed, false if no pool was found for the asset\n */\n removePool(assetId: string): boolean {\n const pool = this.pools.get(assetId);\n if (pool) {\n pool.clear();\n this.pools.delete(assetId);\n return true;\n }\n return false;\n }\n\n /**\n * Clear all pools\n */\n clearAll(): void {\n this.pools.forEach((pool) => pool.clear());\n this.pools.clear();\n }\n\n /**\n * Check if pool exists for asset\n */\n hasPool(assetId: string): boolean {\n return this.pools.has(assetId);\n }\n\n /**\n * Get total number of pools\n */\n getPoolCount(): number {\n return this.pools.size;\n }\n}\n"],"mappings":"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"}
@@ -57,10 +57,12 @@ var VariantSelector = class {
57
57
  static getAttackSound(context) {
58
58
  const { damageAmount = 0, intensity } = context;
59
59
  let soundIntensity = intensity;
60
- if (!soundIntensity) if (damageAmount > 50) soundIntensity = "critical";
61
- else if (damageAmount > 30) soundIntensity = "heavy";
62
- else if (damageAmount > 15) soundIntensity = "medium";
63
- else soundIntensity = "light";
60
+ if (!soundIntensity) {
61
+ if (damageAmount > 50) soundIntensity = "critical";
62
+ else if (damageAmount > 30) soundIntensity = "heavy";
63
+ else if (damageAmount > 15) soundIntensity = "medium";
64
+ else soundIntensity = "light";
65
+ }
64
66
  switch (soundIntensity) {
65
67
  case "critical": return context.isVitalPoint ? "critical_hit" : "attack_critical";
66
68
  case "heavy": return "attack_heavy";
@@ -75,10 +77,12 @@ var VariantSelector = class {
75
77
  const { damageAmount = 0, isVitalPoint = false, intensity } = context;
76
78
  if (isVitalPoint) return "critical_hit";
77
79
  let soundIntensity = intensity;
78
- if (!soundIntensity) if (damageAmount > 50) soundIntensity = "critical";
79
- else if (damageAmount > 30) soundIntensity = "heavy";
80
- else if (damageAmount > 15) soundIntensity = "medium";
81
- else soundIntensity = "light";
80
+ if (!soundIntensity) {
81
+ if (damageAmount > 50) soundIntensity = "critical";
82
+ else if (damageAmount > 30) soundIntensity = "heavy";
83
+ else if (damageAmount > 15) soundIntensity = "medium";
84
+ else soundIntensity = "light";
85
+ }
82
86
  switch (soundIntensity) {
83
87
  case "critical": return "hit_critical";
84
88
  case "heavy": return "hit_heavy";
@@ -1 +1 @@
1
- {"version":3,"file":"VariantSelector.js","names":[],"sources":["../../src/audio/VariantSelector.ts"],"sourcesContent":["/**\n * Audio Variant Selector for Korean martial arts context\n * Selects appropriate audio variants based on combat context and archetype\n */\n\nimport type { PlayerArchetype, TrigramStance } from \"../types/common\";\nimport { MusicTrack, MusicTrackId, SoundEffect, SoundEffectId } from \"./types\";\n\nexport interface AudioVariantContext {\n readonly archetype?: PlayerArchetype;\n readonly stance?: TrigramStance;\n readonly intensity?: \"light\" | \"medium\" | \"heavy\" | \"critical\";\n readonly combatPhase?: \"intro\" | \"combat\" | \"victory\" | \"defeat\";\n readonly damageAmount?: number;\n readonly isVitalPoint?: boolean;\n readonly comboCount?: number;\n}\n\nexport class VariantSelector {\n /**\n * Select the best sound variant for the given context\n */\n static selectSoundVariant(\n baseSound: SoundEffect,\n archetype: PlayerArchetype,\n stance?: TrigramStance\n ): SoundEffect {\n if (!baseSound.variations || baseSound.variations.length === 0) {\n return baseSound;\n }\n\n // Select variant based on archetype\n let variantIndex: number;\n switch (archetype) {\n case \"musa\": // Traditional warrior - use original\n variantIndex = 0;\n break;\n case \"amsalja\": // Assassin - use quieter variants\n variantIndex = Math.min(1, baseSound.variations.length - 1);\n break;\n case \"hacker\": // Cyber warrior - use electronic variants\n variantIndex = Math.min(2, baseSound.variations.length - 1);\n break;\n case \"jeongbo_yowon\": // Intelligence - use subtle variants\n variantIndex = Math.min(3, baseSound.variations.length - 1);\n break;\n case \"jojik_pokryeokbae\": // Crime - use aggressive variants\n variantIndex = Math.min(4, baseSound.variations.length - 1);\n break;\n default:\n variantIndex = 0;\n }\n\n // Modify based on stance if provided\n if (stance) {\n const stanceModifier = this.getStanceModifier(stance);\n variantIndex =\n (variantIndex + stanceModifier) % baseSound.variations.length;\n }\n\n const selectedVariant = baseSound.variations[variantIndex];\n\n return {\n ...baseSound,\n url: selectedVariant,\n id: `${baseSound.id}_${archetype}_${stance ?? \"default\"}`,\n };\n }\n\n /**\n * Select the best music variant for the given context\n */\n static selectMusicVariant(\n baseMusic: MusicTrack,\n archetype: PlayerArchetype,\n intensity: number = 0.5\n ): MusicTrack {\n if (!baseMusic.variations || baseMusic.variations.length === 0) {\n return baseMusic;\n }\n\n // Select based on archetype and intensity\n const archetypeWeight = this.getArchetypeWeight(archetype);\n const intensityWeight = Math.floor(intensity * 3); // 0-2 range\n\n const variantIndex =\n (archetypeWeight + intensityWeight) % baseMusic.variations.length;\n const selectedVariant = baseMusic.variations[variantIndex];\n\n return {\n ...baseMusic,\n url: selectedVariant,\n id: `${baseMusic.id}_${archetype}_intensity${intensityWeight}`,\n volume: (baseMusic.volume ?? 0.7) * (0.8 + intensity * 0.4), // Fix: Handle undefined volume\n };\n }\n\n /**\n * Get attack sound based on damage and archetype\n */\n static getAttackSound(context: AudioVariantContext): SoundEffectId {\n const { damageAmount = 0, intensity } = context;\n\n // Determine intensity from damage if not provided\n let soundIntensity = intensity;\n if (!soundIntensity) {\n if (damageAmount > 50) soundIntensity = \"critical\";\n else if (damageAmount > 30) soundIntensity = \"heavy\";\n else if (damageAmount > 15) soundIntensity = \"medium\";\n else soundIntensity = \"light\";\n }\n\n // Map intensity to sound ID\n switch (soundIntensity) {\n case \"critical\":\n return context.isVitalPoint ? \"critical_hit\" : \"attack_critical\";\n case \"heavy\":\n return \"attack_heavy\";\n case \"medium\":\n return \"attack_medium\";\n case \"light\":\n default:\n return \"attack_light\";\n }\n }\n\n /**\n * Get hit sound based on damage and vital point status\n */\n static getHitSound(context: AudioVariantContext): SoundEffectId {\n const { damageAmount = 0, isVitalPoint = false, intensity } = context;\n\n if (isVitalPoint) {\n return \"critical_hit\";\n }\n\n // Determine intensity from damage if not provided\n let soundIntensity = intensity;\n if (!soundIntensity) {\n if (damageAmount > 50) soundIntensity = \"critical\";\n else if (damageAmount > 30) soundIntensity = \"heavy\";\n else if (damageAmount > 15) soundIntensity = \"medium\";\n else soundIntensity = \"light\";\n }\n\n switch (soundIntensity) {\n case \"critical\":\n return \"hit_critical\";\n case \"heavy\":\n return \"hit_heavy\";\n case \"medium\":\n return \"hit_medium\";\n case \"light\":\n default:\n return \"hit_light\";\n }\n }\n\n /**\n * Get appropriate music for combat phase and archetype\n */\n static getCombatMusic(context: AudioVariantContext): MusicTrackId {\n const { combatPhase = \"combat\" } = context;\n\n switch (combatPhase) {\n case \"intro\":\n return \"intro_theme\";\n case \"victory\":\n return \"victory_theme\";\n case \"defeat\":\n return \"ambient_dojang\"; // Somber ambient for defeat\n case \"combat\":\n default:\n // Could vary by archetype in full implementation\n return \"combat_theme\";\n }\n }\n\n /**\n * Get stance modifier for audio selection\n */\n private static getStanceModifier(stance: TrigramStance): number {\n const stanceMap: Record<TrigramStance, number> = {\n geon: 0, // Heaven - original\n tae: 1, // Lake - fluid\n li: 2, // Fire - intense\n jin: 3, // Thunder - explosive\n son: 4, // Wind - swift\n gam: 5, // Water - flowing\n gan: 6, // Mountain - stable\n gon: 7, // Earth - grounded\n };\n return stanceMap[stance] || 0;\n }\n\n /**\n * Get archetype weight for variant selection\n */\n private static getArchetypeWeight(archetype: PlayerArchetype): number {\n const archetypeMap: Record<PlayerArchetype, number> = {\n musa: 0, // Traditional\n amsalja: 1, // Stealth\n hacker: 2, // Tech\n jeongbo_yowon: 3, // Intelligence\n jojik_pokryeokbae: 4, // Aggressive\n };\n return archetypeMap[archetype] || 0;\n }\n\n /**\n * Select context-appropriate audio variant\n */\n public static selectByContext(\n baseAssetId: string,\n context: {\n archetype: PlayerArchetype;\n stance?: TrigramStance;\n intensity?: number;\n }\n ): { soundId?: string; musicId?: string } {\n // Return string IDs instead of typed IDs\n return {\n soundId: `${baseAssetId}_${context.archetype}`,\n musicId: `${baseAssetId}_${context.archetype}`,\n };\n }\n\n /**\n * Select the best music variant for the given context\n */\n public static selectMusicVariantForArchetype(\n baseMusic: MusicTrack,\n archetype: PlayerArchetype,\n intensity: number = 1.0\n ): string {\n // Fix: Check for variations property existence\n if (\n !(\"variations\" in baseMusic) ||\n !baseMusic.variations ||\n baseMusic.variations.length === 0\n ) {\n console.warn(`No variations available for music track: ${baseMusic.id}`);\n return baseMusic.url;\n }\n\n const archetypeWeight = this.getArchetypeWeight(archetype);\n const intensityWeight = Math.floor(intensity * 10);\n\n const variantIndex =\n (archetypeWeight + intensityWeight) % baseMusic.variations.length;\n const selectedVariant = baseMusic.variations[variantIndex];\n\n console.log(\n `Selected variant for ${archetype} at intensity ${intensity}: ${selectedVariant}`\n );\n\n return selectedVariant;\n }\n\n /**\n * Get random variant from available options\n */\n public static selectRandomVariant(\n baseAsset: MusicTrack | SoundEffect\n ): string {\n // Fix: Type guard to check for variations\n const hasVariations = (\n asset: MusicTrack | SoundEffect\n ): asset is SoundEffect => {\n return (\n \"variations\" in asset &&\n Array.isArray(asset.variations) &&\n asset.variations.length > 0\n );\n };\n\n if (!hasVariations(baseAsset)) {\n return baseAsset.url;\n }\n\n // TypeScript knows baseAsset.variations exists due to earlier check\n const variations = baseAsset.variations;\n if (!variations || variations.length === 0) {\n return baseAsset.url;\n }\n const randomIndex = Math.floor(Math.random() * variations.length);\n const selectedVariant = variations[randomIndex];\n\n console.log(`Random variant selected: ${selectedVariant}`);\n return selectedVariant;\n }\n}\n\nexport default VariantSelector;\n\n// Export convenience functions\nexport function selectCombatSound(context: AudioVariantContext): SoundEffectId {\n return VariantSelector.getAttackSound(context);\n}\n\nexport function selectHitSound(context: AudioVariantContext): SoundEffectId {\n return VariantSelector.getHitSound(context);\n}\n\nexport function selectCombatMusic(context: AudioVariantContext): MusicTrackId {\n return VariantSelector.getCombatMusic(context);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"VariantSelector.js","names":[],"sources":["../../src/audio/VariantSelector.ts"],"sourcesContent":["/**\n * Audio Variant Selector for Korean martial arts context\n * Selects appropriate audio variants based on combat context and archetype\n */\n\nimport type { PlayerArchetype, TrigramStance } from \"../types/common\";\nimport { MusicTrack, MusicTrackId, SoundEffect, SoundEffectId } from \"./types\";\n\nexport interface AudioVariantContext {\n readonly archetype?: PlayerArchetype;\n readonly stance?: TrigramStance;\n readonly intensity?: \"light\" | \"medium\" | \"heavy\" | \"critical\";\n readonly combatPhase?: \"intro\" | \"combat\" | \"victory\" | \"defeat\";\n readonly damageAmount?: number;\n readonly isVitalPoint?: boolean;\n readonly comboCount?: number;\n}\n\nexport class VariantSelector {\n /**\n * Select the best sound variant for the given context\n */\n static selectSoundVariant(\n baseSound: SoundEffect,\n archetype: PlayerArchetype,\n stance?: TrigramStance\n ): SoundEffect {\n if (!baseSound.variations || baseSound.variations.length === 0) {\n return baseSound;\n }\n\n // Select variant based on archetype\n let variantIndex: number;\n switch (archetype) {\n case \"musa\": // Traditional warrior - use original\n variantIndex = 0;\n break;\n case \"amsalja\": // Assassin - use quieter variants\n variantIndex = Math.min(1, baseSound.variations.length - 1);\n break;\n case \"hacker\": // Cyber warrior - use electronic variants\n variantIndex = Math.min(2, baseSound.variations.length - 1);\n break;\n case \"jeongbo_yowon\": // Intelligence - use subtle variants\n variantIndex = Math.min(3, baseSound.variations.length - 1);\n break;\n case \"jojik_pokryeokbae\": // Crime - use aggressive variants\n variantIndex = Math.min(4, baseSound.variations.length - 1);\n break;\n default:\n variantIndex = 0;\n }\n\n // Modify based on stance if provided\n if (stance) {\n const stanceModifier = this.getStanceModifier(stance);\n variantIndex =\n (variantIndex + stanceModifier) % baseSound.variations.length;\n }\n\n const selectedVariant = baseSound.variations[variantIndex];\n\n return {\n ...baseSound,\n url: selectedVariant,\n id: `${baseSound.id}_${archetype}_${stance ?? \"default\"}`,\n };\n }\n\n /**\n * Select the best music variant for the given context\n */\n static selectMusicVariant(\n baseMusic: MusicTrack,\n archetype: PlayerArchetype,\n intensity: number = 0.5\n ): MusicTrack {\n if (!baseMusic.variations || baseMusic.variations.length === 0) {\n return baseMusic;\n }\n\n // Select based on archetype and intensity\n const archetypeWeight = this.getArchetypeWeight(archetype);\n const intensityWeight = Math.floor(intensity * 3); // 0-2 range\n\n const variantIndex =\n (archetypeWeight + intensityWeight) % baseMusic.variations.length;\n const selectedVariant = baseMusic.variations[variantIndex];\n\n return {\n ...baseMusic,\n url: selectedVariant,\n id: `${baseMusic.id}_${archetype}_intensity${intensityWeight}`,\n volume: (baseMusic.volume ?? 0.7) * (0.8 + intensity * 0.4), // Fix: Handle undefined volume\n };\n }\n\n /**\n * Get attack sound based on damage and archetype\n */\n static getAttackSound(context: AudioVariantContext): SoundEffectId {\n const { damageAmount = 0, intensity } = context;\n\n // Determine intensity from damage if not provided\n let soundIntensity = intensity;\n if (!soundIntensity) {\n if (damageAmount > 50) soundIntensity = \"critical\";\n else if (damageAmount > 30) soundIntensity = \"heavy\";\n else if (damageAmount > 15) soundIntensity = \"medium\";\n else soundIntensity = \"light\";\n }\n\n // Map intensity to sound ID\n switch (soundIntensity) {\n case \"critical\":\n return context.isVitalPoint ? \"critical_hit\" : \"attack_critical\";\n case \"heavy\":\n return \"attack_heavy\";\n case \"medium\":\n return \"attack_medium\";\n case \"light\":\n default:\n return \"attack_light\";\n }\n }\n\n /**\n * Get hit sound based on damage and vital point status\n */\n static getHitSound(context: AudioVariantContext): SoundEffectId {\n const { damageAmount = 0, isVitalPoint = false, intensity } = context;\n\n if (isVitalPoint) {\n return \"critical_hit\";\n }\n\n // Determine intensity from damage if not provided\n let soundIntensity = intensity;\n if (!soundIntensity) {\n if (damageAmount > 50) soundIntensity = \"critical\";\n else if (damageAmount > 30) soundIntensity = \"heavy\";\n else if (damageAmount > 15) soundIntensity = \"medium\";\n else soundIntensity = \"light\";\n }\n\n switch (soundIntensity) {\n case \"critical\":\n return \"hit_critical\";\n case \"heavy\":\n return \"hit_heavy\";\n case \"medium\":\n return \"hit_medium\";\n case \"light\":\n default:\n return \"hit_light\";\n }\n }\n\n /**\n * Get appropriate music for combat phase and archetype\n */\n static getCombatMusic(context: AudioVariantContext): MusicTrackId {\n const { combatPhase = \"combat\" } = context;\n\n switch (combatPhase) {\n case \"intro\":\n return \"intro_theme\";\n case \"victory\":\n return \"victory_theme\";\n case \"defeat\":\n return \"ambient_dojang\"; // Somber ambient for defeat\n case \"combat\":\n default:\n // Could vary by archetype in full implementation\n return \"combat_theme\";\n }\n }\n\n /**\n * Get stance modifier for audio selection\n */\n private static getStanceModifier(stance: TrigramStance): number {\n const stanceMap: Record<TrigramStance, number> = {\n geon: 0, // Heaven - original\n tae: 1, // Lake - fluid\n li: 2, // Fire - intense\n jin: 3, // Thunder - explosive\n son: 4, // Wind - swift\n gam: 5, // Water - flowing\n gan: 6, // Mountain - stable\n gon: 7, // Earth - grounded\n };\n return stanceMap[stance] || 0;\n }\n\n /**\n * Get archetype weight for variant selection\n */\n private static getArchetypeWeight(archetype: PlayerArchetype): number {\n const archetypeMap: Record<PlayerArchetype, number> = {\n musa: 0, // Traditional\n amsalja: 1, // Stealth\n hacker: 2, // Tech\n jeongbo_yowon: 3, // Intelligence\n jojik_pokryeokbae: 4, // Aggressive\n };\n return archetypeMap[archetype] || 0;\n }\n\n /**\n * Select context-appropriate audio variant\n */\n public static selectByContext(\n baseAssetId: string,\n context: {\n archetype: PlayerArchetype;\n stance?: TrigramStance;\n intensity?: number;\n }\n ): { soundId?: string; musicId?: string } {\n // Return string IDs instead of typed IDs\n return {\n soundId: `${baseAssetId}_${context.archetype}`,\n musicId: `${baseAssetId}_${context.archetype}`,\n };\n }\n\n /**\n * Select the best music variant for the given context\n */\n public static selectMusicVariantForArchetype(\n baseMusic: MusicTrack,\n archetype: PlayerArchetype,\n intensity: number = 1.0\n ): string {\n // Fix: Check for variations property existence\n if (\n !(\"variations\" in baseMusic) ||\n !baseMusic.variations ||\n baseMusic.variations.length === 0\n ) {\n console.warn(`No variations available for music track: ${baseMusic.id}`);\n return baseMusic.url;\n }\n\n const archetypeWeight = this.getArchetypeWeight(archetype);\n const intensityWeight = Math.floor(intensity * 10);\n\n const variantIndex =\n (archetypeWeight + intensityWeight) % baseMusic.variations.length;\n const selectedVariant = baseMusic.variations[variantIndex];\n\n console.log(\n `Selected variant for ${archetype} at intensity ${intensity}: ${selectedVariant}`\n );\n\n return selectedVariant;\n }\n\n /**\n * Get random variant from available options\n */\n public static selectRandomVariant(\n baseAsset: MusicTrack | SoundEffect\n ): string {\n // Fix: Type guard to check for variations\n const hasVariations = (\n asset: MusicTrack | SoundEffect\n ): asset is SoundEffect => {\n return (\n \"variations\" in asset &&\n Array.isArray(asset.variations) &&\n asset.variations.length > 0\n );\n };\n\n if (!hasVariations(baseAsset)) {\n return baseAsset.url;\n }\n\n // TypeScript knows baseAsset.variations exists due to earlier check\n const variations = baseAsset.variations;\n if (!variations || variations.length === 0) {\n return baseAsset.url;\n }\n const randomIndex = Math.floor(Math.random() * variations.length);\n const selectedVariant = variations[randomIndex];\n\n console.log(`Random variant selected: ${selectedVariant}`);\n return selectedVariant;\n }\n}\n\nexport default VariantSelector;\n\n// Export convenience functions\nexport function selectCombatSound(context: AudioVariantContext): SoundEffectId {\n return VariantSelector.getAttackSound(context);\n}\n\nexport function selectHitSound(context: AudioVariantContext): SoundEffectId {\n return VariantSelector.getHitSound(context);\n}\n\nexport function selectCombatMusic(context: AudioVariantContext): MusicTrackId {\n return VariantSelector.getCombatMusic(context);\n}\n"],"mappings":"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"}
@@ -21,7 +21,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
21
21
  import { jsx, jsxs } from "react/jsx-runtime";
22
22
  import { Canvas } from "@react-three/fiber";
23
23
  //#region src/components/screens/intro/IntroScreen3D.tsx
24
- var APP_VERSION = "0.7.111";
24
+ var APP_VERSION = "0.7.112";
25
25
  var MENU_ITEMS = [
26
26
  {
27
27
  mode: GameMode.VERSUS,
@@ -403,9 +403,10 @@ var TrainingScreen3D = ({ onPlayerUpdate, onReturnToMenu, width = 1200, height =
403
403
  const isRunningChanged = prevIsRunningRef.current !== isRunning;
404
404
  const stanceChanged = prevStanceRef.current !== currentStance;
405
405
  if (isMovingChanged || isRunningChanged) {
406
- if (isMoving) if (isRunning) playerAnimation.transitionTo(AnimationState.RUN);
407
- else playerAnimation.transitionTo(AnimationState.WALK);
408
- else if (playerAnimation.currentState === AnimationState.WALK || playerAnimation.currentState === AnimationState.RUN) playerAnimation.transitionToStanceGuard(currentStance);
406
+ if (isMoving) {
407
+ if (isRunning) playerAnimation.transitionTo(AnimationState.RUN);
408
+ else playerAnimation.transitionTo(AnimationState.WALK);
409
+ } else if (playerAnimation.currentState === AnimationState.WALK || playerAnimation.currentState === AnimationState.RUN) playerAnimation.transitionToStanceGuard(currentStance);
409
410
  prevIsMovingRef.current = isMoving;
410
411
  prevIsRunningRef.current = isRunning;
411
412
  }