blacktrigram 0.7.6 → 0.7.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -379,7 +379,7 @@ George Dorn provides detailed repository deep-dives based on actual code inspect
379
379
 
380
380
  **[Repository Deep-Dive](https://hack23.com/blog-george-dorn-trigram-code.html)**
381
381
 
382
- **Stack:** TypeScript 5.9, React 19, Three.js 0.183, Vite 8
382
+ **Stack:** TypeScript 6.0, React 19, Three.js 0.183, Vite 8
383
383
  **Metrics:** 132 TypeScript files, 70 vital points system, 5 fighter archetypes
384
384
 
385
385
  Examined package.json dependencies, explored src/ structure, verified combat system implementation, and reviewed AI integrations.
@@ -1 +1 @@
1
- {"version":3,"file":"AudioAssetLoader.js","names":[],"sources":["../../src/audio/AudioAssetLoader.ts"],"sourcesContent":["/**\n * AudioAssetLoader - Handles audio asset loading with retry logic and format fallback\n * Implements production-ready asset loading strategies for Black Trigram\n */\n\nimport type { AudioAsset } from \"./types\";\n\nexport type LoadPriority = \"critical\" | \"high\" | \"normal\" | \"low\";\n\nexport interface LoadOptions {\n readonly priority?: LoadPriority;\n readonly maxRetries?: number;\n readonly retryDelay?: number;\n readonly timeout?: number;\n}\n\nexport interface LoadResult {\n readonly success: boolean;\n readonly audio?: HTMLAudioElement;\n readonly error?: Error;\n readonly attemptCount: number;\n readonly loadTime: number;\n readonly formatUsed?: string;\n}\n\nexport interface BatchLoadProgress {\n readonly total: number;\n readonly loaded: number;\n readonly failed: number;\n readonly currentAsset?: string;\n readonly progress: number;\n}\n\nexport class AudioAssetLoader {\n private loadAttempts: Map<string, number> = new Map();\n private loadCache: Map<string, HTMLAudioElement> = new Map();\n private loadingPromises: Map<string, Promise<LoadResult>> = new Map();\n\n /**\n * Load a single audio asset with retry logic and format fallback\n */\n async loadAsset(\n asset: AudioAsset,\n options: LoadOptions = {}\n ): Promise<LoadResult> {\n const {\n priority = \"normal\",\n maxRetries = 3,\n retryDelay = 1000,\n timeout = 10000,\n } = options;\n\n // Check cache first\n const cached = this.loadCache.get(asset.id);\n if (cached) {\n return {\n success: true,\n audio: cached,\n attemptCount: 0,\n loadTime: 0,\n formatUsed: cached.src,\n };\n }\n\n // Check if already loading\n const existingPromise = this.loadingPromises.get(asset.id);\n if (existingPromise) {\n return existingPromise;\n }\n\n const startTime = performance.now();\n const loadPromise = this.loadWithRetry(\n asset,\n maxRetries,\n retryDelay,\n timeout,\n priority\n );\n\n this.loadingPromises.set(asset.id, loadPromise);\n\n try {\n const result = await loadPromise;\n this.loadingPromises.delete(asset.id);\n\n if (result.success && result.audio) {\n this.loadCache.set(asset.id, result.audio);\n }\n\n return {\n ...result,\n loadTime: performance.now() - startTime,\n };\n } catch (error) {\n this.loadingPromises.delete(asset.id);\n return {\n success: false,\n error: error instanceof Error ? error : new Error(String(error)),\n attemptCount: this.loadAttempts.get(asset.id) ?? 0,\n loadTime: performance.now() - startTime,\n };\n }\n }\n\n /**\n * Load asset with exponential backoff retry\n */\n private async loadWithRetry(\n asset: AudioAsset,\n maxRetries: number,\n baseRetryDelay: number,\n timeout: number,\n priority: LoadPriority\n ): Promise<LoadResult> {\n let lastError: Error | null = null;\n const attemptCount = this.loadAttempts.get(asset.id) ?? 0;\n this.loadAttempts.set(asset.id, attemptCount + 1);\n\n // Try all available formats with fallback\n const formats = this.getFormatsToTry(asset);\n\n for (let attempt = 0; attempt < maxRetries; attempt++) {\n for (const format of formats) {\n try {\n const audio = await this.tryLoadFormat(format, timeout, priority);\n this.loadAttempts.delete(asset.id);\n return {\n success: true,\n audio,\n attemptCount: attempt + 1,\n loadTime: 0, // Will be set by caller\n formatUsed: format,\n };\n } catch (error) {\n lastError = error instanceof Error ? error : new Error(String(error));\n console.warn(\n `Failed to load ${asset.id} format ${format} on attempt ${attempt + 1}:`,\n error\n );\n }\n }\n\n // Exponential backoff before retry\n if (attempt < maxRetries - 1) {\n const delay = baseRetryDelay * Math.pow(2, attempt);\n await this.sleep(delay);\n }\n }\n\n // All retries failed, return silent placeholder\n console.error(\n `All attempts failed for ${asset.id}, using silent placeholder`\n );\n return {\n success: false,\n audio: this.createSilentPlaceholder(),\n error: lastError ?? new Error(\"All load attempts failed\"),\n attemptCount: maxRetries,\n loadTime: 0,\n formatUsed: \"placeholder\",\n };\n }\n\n /**\n * Get formats to try in order of preference\n */\n private getFormatsToTry(asset: AudioAsset): string[] {\n const formats: string[] = [];\n\n // Try asset URL first\n if (asset.url) {\n formats.push(asset.url);\n }\n\n // Try variations if available\n if (\"variations\" in asset && Array.isArray(asset.variations)) {\n formats.push(...asset.variations);\n }\n\n // Format fallback: webm → mp3 → wav\n if (asset.url.endsWith(\".webm\")) {\n const mp3Url = asset.url.replace(\".webm\", \".mp3\");\n if (!formats.includes(mp3Url)) {\n formats.push(mp3Url);\n }\n }\n\n return formats;\n }\n\n /**\n * Try loading a specific format with timeout\n */\n private tryLoadFormat(\n url: string,\n timeout: number,\n _priority: LoadPriority\n ): Promise<HTMLAudioElement> {\n return new Promise((resolve, reject) => {\n const audio = new Audio();\n audio.preload = \"auto\";\n\n let timeoutId: NodeJS.Timeout | null = null;\n let resolved = false;\n\n const onLoad = () => {\n if (resolved) return;\n resolved = true;\n if (timeoutId) clearTimeout(timeoutId);\n resolve(audio);\n };\n\n const onError = () => {\n if (resolved) return;\n resolved = true;\n if (timeoutId) clearTimeout(timeoutId);\n audio.src = \"\";\n reject(new Error(`Failed to load: ${url}`));\n };\n\n timeoutId = setTimeout(() => {\n if (resolved) return;\n resolved = true;\n audio.src = \"\";\n reject(new Error(`Load timeout after ${timeout}ms`));\n }, timeout);\n\n audio.addEventListener(\"canplaythrough\", onLoad, { once: true });\n audio.addEventListener(\"error\", onError, { once: true });\n\n audio.src = url;\n audio.load();\n });\n }\n\n /**\n * Batch load multiple assets\n */\n async batchLoad(\n assets: readonly AudioAsset[],\n options: LoadOptions = {},\n onProgress?: (progress: BatchLoadProgress) => void\n ): Promise<LoadResult[]> {\n const results: LoadResult[] = [];\n let loaded = 0;\n let failed = 0;\n\n for (const asset of assets) {\n onProgress?.({\n total: assets.length,\n loaded,\n failed,\n currentAsset: asset.id,\n progress: (loaded + 1) / assets.length, // +1 to reflect current asset being loaded\n });\n\n const result = await this.loadAsset(asset, options);\n results.push(result);\n\n if (result.success) {\n loaded++;\n } else {\n failed++;\n }\n }\n\n onProgress?.({\n total: assets.length,\n loaded,\n failed,\n progress: 1.0,\n });\n\n return results;\n }\n\n /**\n * Preload assets by priority level\n */\n async preloadByPriority(\n assets: readonly AudioAsset[],\n priority: LoadPriority\n ): Promise<LoadResult[]> {\n // Filter assets by priority if they have metadata\n const priorityAssets = assets.filter(\n (asset) =>\n \"preloadPriority\" in asset && asset.preloadPriority === priority\n );\n\n if (priorityAssets.length === 0) {\n return [];\n }\n\n return this.batchLoad(priorityAssets, { priority });\n }\n\n /**\n * Unload an asset and free memory\n * @param assetId - ID of the asset to unload\n * @returns true if the asset was cached and unloaded, false if not found\n */\n unloadAsset(assetId: string): boolean {\n const audio = this.loadCache.get(assetId);\n if (audio) {\n audio.pause();\n audio.src = \"\";\n audio.load(); // Reset to release memory\n this.loadCache.delete(assetId);\n this.loadAttempts.delete(assetId);\n return true;\n }\n return false;\n }\n\n /**\n * Get cached audio element\n * @param assetId - ID of the asset to retrieve\n * @returns The cached audio element or undefined if not found\n */\n getCached(assetId: string): HTMLAudioElement | undefined {\n return this.loadCache.get(assetId);\n }\n\n /**\n * Check if asset is cached\n * @param assetId - ID of the asset to check\n * @returns true if the asset is in cache, false otherwise\n */\n isCached(assetId: string): boolean {\n return this.loadCache.has(assetId);\n }\n\n /**\n * Get total number of cached assets\n * @returns The number of assets currently in cache\n */\n getCacheSize(): number {\n return this.loadCache.size;\n }\n\n /**\n * Clear all cached assets and free memory\n */\n clearCache(): void {\n this.loadCache.forEach((audio) => {\n audio.pause();\n audio.src = \"\";\n audio.load();\n });\n this.loadCache.clear();\n this.loadAttempts.clear();\n this.loadingPromises.clear();\n }\n\n /**\n * Create a silent audio placeholder for failed loads\n */\n private createSilentPlaceholder(): HTMLAudioElement {\n const audio = new Audio();\n // Valid 16-bit 44.1kHz mono silent WAV file (0.1s duration = 4410 samples)\n // This is a properly formatted WAV file with actual silent audio data\n // RIFF header + fmt chunk + data chunk with 4410 samples of 0x00 (16-bit silence)\n audio.src =\n \"data:audio/wav;base64,UklGRuQRAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQARAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\";\n return audio;\n }\n\n /**\n * Sleep utility for retry delays\n */\n private sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n\n /**\n * Get loading statistics including cache size, loading count, and total attempts\n * @returns Object with cached count, loading count, and total attempts\n */\n getStatistics(): {\n readonly cached: number;\n readonly loading: number;\n readonly totalAttempts: number;\n } {\n return {\n cached: this.loadCache.size,\n loading: this.loadingPromises.size,\n totalAttempts: Array.from(this.loadAttempts.values()).reduce(\n (sum, val) => sum + val,\n 0\n ),\n };\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"AudioAssetLoader.js","names":[],"sources":["../../src/audio/AudioAssetLoader.ts"],"sourcesContent":["/**\n * AudioAssetLoader - Handles audio asset loading with retry logic and format fallback\n * Implements production-ready asset loading strategies for Black Trigram\n */\n\nimport type { AudioAsset } from \"./types\";\n\nexport type LoadPriority = \"critical\" | \"high\" | \"normal\" | \"low\";\n\nexport interface LoadOptions {\n readonly priority?: LoadPriority;\n readonly maxRetries?: number;\n readonly retryDelay?: number;\n readonly timeout?: number;\n}\n\nexport interface LoadResult {\n readonly success: boolean;\n readonly audio?: HTMLAudioElement;\n readonly error?: Error;\n readonly attemptCount: number;\n readonly loadTime: number;\n readonly formatUsed?: string;\n}\n\nexport interface BatchLoadProgress {\n readonly total: number;\n readonly loaded: number;\n readonly failed: number;\n readonly currentAsset?: string;\n readonly progress: number;\n}\n\nexport class AudioAssetLoader {\n private loadAttempts: Map<string, number> = new Map();\n private loadCache: Map<string, HTMLAudioElement> = new Map();\n private loadingPromises: Map<string, Promise<LoadResult>> = new Map();\n\n /**\n * Load a single audio asset with retry logic and format fallback\n */\n async loadAsset(\n asset: AudioAsset,\n options: LoadOptions = {}\n ): Promise<LoadResult> {\n const {\n priority = \"normal\",\n maxRetries = 3,\n retryDelay = 1000,\n timeout = 10000,\n } = options;\n\n // Check cache first\n const cached = this.loadCache.get(asset.id);\n if (cached) {\n return {\n success: true,\n audio: cached,\n attemptCount: 0,\n loadTime: 0,\n formatUsed: cached.src,\n };\n }\n\n // Check if already loading\n const existingPromise = this.loadingPromises.get(asset.id);\n if (existingPromise) {\n return existingPromise;\n }\n\n const startTime = performance.now();\n const loadPromise = this.loadWithRetry(\n asset,\n maxRetries,\n retryDelay,\n timeout,\n priority\n );\n\n this.loadingPromises.set(asset.id, loadPromise);\n\n try {\n const result = await loadPromise;\n this.loadingPromises.delete(asset.id);\n\n if (result.success && result.audio) {\n this.loadCache.set(asset.id, result.audio);\n }\n\n return {\n ...result,\n loadTime: performance.now() - startTime,\n };\n } catch (error) {\n this.loadingPromises.delete(asset.id);\n return {\n success: false,\n error: error instanceof Error ? error : new Error(String(error)),\n attemptCount: this.loadAttempts.get(asset.id) ?? 0,\n loadTime: performance.now() - startTime,\n };\n }\n }\n\n /**\n * Load asset with exponential backoff retry\n */\n private async loadWithRetry(\n asset: AudioAsset,\n maxRetries: number,\n baseRetryDelay: number,\n timeout: number,\n priority: LoadPriority\n ): Promise<LoadResult> {\n let lastError: Error | null = null;\n const attemptCount = this.loadAttempts.get(asset.id) ?? 0;\n this.loadAttempts.set(asset.id, attemptCount + 1);\n\n // Try all available formats with fallback\n const formats = this.getFormatsToTry(asset);\n\n for (let attempt = 0; attempt < maxRetries; attempt++) {\n for (const format of formats) {\n try {\n const audio = await this.tryLoadFormat(format, timeout, priority);\n this.loadAttempts.delete(asset.id);\n return {\n success: true,\n audio,\n attemptCount: attempt + 1,\n loadTime: 0, // Will be set by caller\n formatUsed: format,\n };\n } catch (error) {\n lastError = error instanceof Error ? error : new Error(String(error));\n console.warn(\n `Failed to load ${asset.id} format ${format} on attempt ${attempt + 1}:`,\n error\n );\n }\n }\n\n // Exponential backoff before retry\n if (attempt < maxRetries - 1) {\n const delay = baseRetryDelay * Math.pow(2, attempt);\n await this.sleep(delay);\n }\n }\n\n // All retries failed, return silent placeholder\n console.error(\n `All attempts failed for ${asset.id}, using silent placeholder`\n );\n return {\n success: false,\n audio: this.createSilentPlaceholder(),\n error: lastError ?? new Error(\"All load attempts failed\"),\n attemptCount: maxRetries,\n loadTime: 0,\n formatUsed: \"placeholder\",\n };\n }\n\n /**\n * Get formats to try in order of preference\n */\n private getFormatsToTry(asset: AudioAsset): string[] {\n const formats: string[] = [];\n\n // Try asset URL first\n if (asset.url) {\n formats.push(asset.url);\n }\n\n // Try variations if available\n if (\"variations\" in asset && Array.isArray(asset.variations)) {\n formats.push(...asset.variations);\n }\n\n // Format fallback: webm → mp3 → wav\n if (asset.url.endsWith(\".webm\")) {\n const mp3Url = asset.url.replace(\".webm\", \".mp3\");\n if (!formats.includes(mp3Url)) {\n formats.push(mp3Url);\n }\n }\n\n return formats;\n }\n\n /**\n * Try loading a specific format with timeout\n */\n private tryLoadFormat(\n url: string,\n timeout: number,\n _priority: LoadPriority\n ): Promise<HTMLAudioElement> {\n return new Promise((resolve, reject) => {\n const audio = new Audio();\n audio.preload = \"auto\";\n\n let timeoutId: ReturnType<typeof setTimeout> | null = null;\n let resolved = false;\n\n const onLoad = () => {\n if (resolved) return;\n resolved = true;\n if (timeoutId) clearTimeout(timeoutId);\n resolve(audio);\n };\n\n const onError = () => {\n if (resolved) return;\n resolved = true;\n if (timeoutId) clearTimeout(timeoutId);\n audio.src = \"\";\n reject(new Error(`Failed to load: ${url}`));\n };\n\n timeoutId = setTimeout(() => {\n if (resolved) return;\n resolved = true;\n audio.src = \"\";\n reject(new Error(`Load timeout after ${timeout}ms`));\n }, timeout);\n\n audio.addEventListener(\"canplaythrough\", onLoad, { once: true });\n audio.addEventListener(\"error\", onError, { once: true });\n\n audio.src = url;\n audio.load();\n });\n }\n\n /**\n * Batch load multiple assets\n */\n async batchLoad(\n assets: readonly AudioAsset[],\n options: LoadOptions = {},\n onProgress?: (progress: BatchLoadProgress) => void\n ): Promise<LoadResult[]> {\n const results: LoadResult[] = [];\n let loaded = 0;\n let failed = 0;\n\n for (const asset of assets) {\n onProgress?.({\n total: assets.length,\n loaded,\n failed,\n currentAsset: asset.id,\n progress: (loaded + 1) / assets.length, // +1 to reflect current asset being loaded\n });\n\n const result = await this.loadAsset(asset, options);\n results.push(result);\n\n if (result.success) {\n loaded++;\n } else {\n failed++;\n }\n }\n\n onProgress?.({\n total: assets.length,\n loaded,\n failed,\n progress: 1.0,\n });\n\n return results;\n }\n\n /**\n * Preload assets by priority level\n */\n async preloadByPriority(\n assets: readonly AudioAsset[],\n priority: LoadPriority\n ): Promise<LoadResult[]> {\n // Filter assets by priority if they have metadata\n const priorityAssets = assets.filter(\n (asset) =>\n \"preloadPriority\" in asset && asset.preloadPriority === priority\n );\n\n if (priorityAssets.length === 0) {\n return [];\n }\n\n return this.batchLoad(priorityAssets, { priority });\n }\n\n /**\n * Unload an asset and free memory\n * @param assetId - ID of the asset to unload\n * @returns true if the asset was cached and unloaded, false if not found\n */\n unloadAsset(assetId: string): boolean {\n const audio = this.loadCache.get(assetId);\n if (audio) {\n audio.pause();\n audio.src = \"\";\n audio.load(); // Reset to release memory\n this.loadCache.delete(assetId);\n this.loadAttempts.delete(assetId);\n return true;\n }\n return false;\n }\n\n /**\n * Get cached audio element\n * @param assetId - ID of the asset to retrieve\n * @returns The cached audio element or undefined if not found\n */\n getCached(assetId: string): HTMLAudioElement | undefined {\n return this.loadCache.get(assetId);\n }\n\n /**\n * Check if asset is cached\n * @param assetId - ID of the asset to check\n * @returns true if the asset is in cache, false otherwise\n */\n isCached(assetId: string): boolean {\n return this.loadCache.has(assetId);\n }\n\n /**\n * Get total number of cached assets\n * @returns The number of assets currently in cache\n */\n getCacheSize(): number {\n return this.loadCache.size;\n }\n\n /**\n * Clear all cached assets and free memory\n */\n clearCache(): void {\n this.loadCache.forEach((audio) => {\n audio.pause();\n audio.src = \"\";\n audio.load();\n });\n this.loadCache.clear();\n this.loadAttempts.clear();\n this.loadingPromises.clear();\n }\n\n /**\n * Create a silent audio placeholder for failed loads\n */\n private createSilentPlaceholder(): HTMLAudioElement {\n const audio = new Audio();\n // Valid 16-bit 44.1kHz mono silent WAV file (0.1s duration = 4410 samples)\n // This is a properly formatted WAV file with actual silent audio data\n // RIFF header + fmt chunk + data chunk with 4410 samples of 0x00 (16-bit silence)\n audio.src =\n \"data:audio/wav;base64,UklGRuQRAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQARAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\";\n return audio;\n }\n\n /**\n * Sleep utility for retry delays\n */\n private sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n\n /**\n * Get loading statistics including cache size, loading count, and total attempts\n * @returns Object with cached count, loading count, and total attempts\n */\n getStatistics(): {\n readonly cached: number;\n readonly loading: number;\n readonly totalAttempts: number;\n } {\n return {\n cached: this.loadCache.size,\n loading: this.loadingPromises.size,\n totalAttempts: Array.from(this.loadAttempts.values()).reduce(\n (sum, val) => sum + val,\n 0\n ),\n };\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"useCombatActions.js","names":[],"sources":["../../../../../src/components/screens/combat/hooks/useCombatActions.ts"],"sourcesContent":["/**\n * useCombatActions Hook - Combat Action Handlers\n *\n * Custom hook for managing combat action handlers.\n * Consolidates player attack, defend, technique, and AI action logic.\n *\n * Performance:\n * - Memoized callbacks to prevent recreation\n * - Centralized action logic for better maintainability\n * - Reduces main component complexity\n *\n * @param config Combat action configuration\n * @returns Combat action handlers\n *\n * @example\n * ```typescript\n * const {\n * handleAttack,\n * handleDefend,\n * handleTechniqueExecute,\n * handleStanceSwitch,\n * handleAIAttack,\n * handleAIDefend,\n * handleAITechnique,\n * moveAIPlayer\n * } = useCombatActions({\n * validPlayers,\n * playerPositions,\n * combatState,\n * combatActions,\n * combatSystem,\n * onPlayerUpdate,\n * addCombatMessage,\n * addHitEffect,\n * arenaBounds\n * });\n * ```\n */\n\nimport { PlayerState } from \"@/systems\";\nimport {\n AnimationType,\n getAnimationHitTiming,\n type AnimationState,\n} from \"@/systems/animation\";\nimport { movementPenaltySystem } from \"@/systems/bodypart\";\nimport { clampToArenaBounds, type PhysicsArenaBounds } from \"@/types/PhysicsTypes\";\nimport {\n checkForFall,\n getFallTypeName,\n} from \"@/systems/combat/FallIntegration\";\nimport type { CombatResult } from \"@/systems/combat/types\";\nimport { CombatSystem } from \"@/systems/CombatSystem\";\nimport { HitEffectType } from \"@/systems/effects\";\nimport { KnockbackPhysics } from \"@/systems/physics\";\nimport { StanceManager } from \"@/systems/trigram\";\nimport { KoreanTechniquesSystem } from \"@/systems/trigram/KoreanTechniques\";\nimport { getVitalPointById } from \"@/systems/vitalpoint/KoreanVitalPoints\";\nimport { KoreanTechnique } from \"@/systems/vitalpoint/types\";\nimport { Position, Technique, TrigramStance, BodyRegion } from \"@/types\";\nimport { Injury, InjuryType } from \"@/types/injury\";\nimport { useCallback, useEffect, useRef } from \"react\";\nimport { AttackIntensity } from \"./useCombatAudio\";\nimport { CombatActions, CombatScreenState } from \"./useCombatState\";\n\n/**\n * Hit position variation range for randomizing strike heights\n * Produces ±0.2 absolute units (±10% of 2.0 character height)\n */\nconst HIT_Y_VARIATION_RANGE = 0.4;\n\n/**\n * Calculate randomized hit position based on defender position\n * Adds vertical variation to simulate different strike heights\n *\n * @param defenderPos - Position of the defender being struck\n * @returns Hit position with randomized Y coordinate\n */\nfunction calculateHitPosition(defenderPos: Position): { x: number; y: number } {\n const hitYVariation = (Math.random() - 0.5) * HIT_Y_VARIATION_RANGE; // ±0.2 units\n return {\n x: defenderPos.x,\n y: Math.max(0.3, Math.min(1.8, defenderPos.y + hitYVariation)),\n };\n}\n\n/**\n * Determine injury type from combat result and technique damage type\n * 전투 결과와 기술 피해 유형으로 부상 유형 결정\n *\n * @param result - Combat result with damage information\n * @param technique - Technique that was executed\n * @returns InjuryType to create\n */\nfunction determineInjuryType(\n result: CombatResult,\n technique: KoreanTechnique,\n): InjuryType {\n // Slashing damage creates cuts (damageType is a string, not enum)\n if (technique.damageType === \"slashing\") {\n return result.damage > 20 ? InjuryType.LACERATION : InjuryType.CUT;\n }\n\n // Heavy damage creates severe bruising\n if (result.damage > 25) {\n return InjuryType.BRUISE;\n }\n\n // Medium damage creates moderate bruising\n if (result.damage > 15) {\n return InjuryType.BRUISE;\n }\n\n // Light damage creates light bruising\n return InjuryType.BRUISE;\n}\n\n/**\n * Map body region to 3D position offset on character model\n * 신체 부위를 캐릭터 모델의 3D 위치 오프셋으로 매핑\n *\n * @param region - Body region that was hit\n * @returns Position offset [x, y, z] relative to character center\n */\nfunction getBodyRegionPosition(region: BodyRegion): [number, number, number] {\n // Map body regions to approximate positions on character model\n // Character is ~2 units tall, centered at [0, 0, 0]\n switch (region) {\n case BodyRegion.HEAD:\n return [0, 1.6, 0];\n case BodyRegion.NECK:\n return [0, 1.3, 0];\n case BodyRegion.TORSO:\n case BodyRegion.CORE:\n return [0, 0.8, 0];\n case BodyRegion.LEFT_ARM:\n return [-0.4, 1.0, 0];\n case BodyRegion.RIGHT_ARM:\n return [0.4, 1.0, 0];\n case BodyRegion.LEFT_LEG:\n return [-0.2, 0.2, 0];\n case BodyRegion.RIGHT_LEG:\n return [0.2, 0.2, 0];\n default:\n return [0, 0.8, 0]; // Default to torso\n }\n}\n\n/**\n * Create injury from combat damage result\n * 전투 피해 결과로부터 부상 생성\n *\n * @param result - Combat result with damage details\n * @param technique - Technique that caused the damage\n * @param defenderHealth - Current defender health after damage (0-100 scale)\n * @param targetPlayerIndex - Index of the player who was hit (0 or 1)\n * @returns Injury object for visualization\n */\nfunction createInjuryFromDamage(\n result: CombatResult,\n technique: KoreanTechnique,\n defenderHealth: number,\n targetPlayerIndex: number,\n): Injury {\n // Determine body region - use torso as default if not specified\n const bodyRegion = BodyRegion.TORSO; // TODO: Extract from result when available\n\n // Determine injury type based on damage and technique\n let injuryType = determineInjuryType(result, technique);\n\n // Promote to fracture when health is critically low and damage is severe\n // to align with TraumaOverlay3D fracture behavior\n const isLowHealth = defenderHealth <= 30; // 30% health threshold\n const isSevereDamage = result.damage >= 25; // Severe damage threshold\n if (isLowHealth && isSevereDamage && injuryType !== InjuryType.FRACTURE) {\n injuryType = InjuryType.FRACTURE;\n }\n\n // Calculate severity (0.0 to 1.0) based on damage\n // Normalized so that a ~30-damage hit is treated as near-max severity\n const severity = Math.min(1.0, result.damage / 30);\n\n // Get position on character model for this body region\n const basePosition = getBodyRegionPosition(bodyRegion);\n\n // Add small random offset for variety\n const randomOffset: [number, number, number] = [\n (Math.random() - 0.5) * 0.1,\n (Math.random() - 0.5) * 0.1,\n (Math.random() - 0.5) * 0.1,\n ];\n\n const position: [number, number, number] = [\n basePosition[0] + randomOffset[0],\n basePosition[1] + randomOffset[1],\n basePosition[2] + randomOffset[2],\n ];\n\n return {\n id: `injury_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`,\n region: bodyRegion,\n type: injuryType,\n position,\n severity,\n hitCount: 1,\n timestamp: Date.now(),\n playerId: targetPlayerIndex === 0 ? \"player\" : \"enemy\",\n };\n}\n\n/**\n * Apply knockback displacement to defender position\n *\n * **Korean**: 밀침 적용 (Apply Knockback)\n *\n * Updates the defender's position based on knockback physics calculation.\n * The knockback displacement is applied in the direction of the attack vector\n * (attacker → defender), respecting arena boundaries.\n *\n * **Physics-First Architecture**: Both positions and knockback displacement\n * are in meters. Arena bounds are centered at origin (0, 0) with extent\n * ±worldWidthMeters/2 in X and ±worldDepthMeters/2 in Z.\n *\n * @param result - Combat result containing knockback data (in meters)\n * @param defenderPos - Current defender position (in meters)\n * @param arenaBounds - Arena boundary limits with meter dimensions\n * @returns Updated defender position after knockback (in meters)\n *\n * @example\n * ```typescript\n * // 10m × 7.5m arena, player at x=2m knocked back 2.5m to right\n * const newPosition = applyKnockbackDisplacement(\n * result, // knockback.displacement.x = 2.5m\n * { x: 2, y: 0 }, // Current position: 2m from center\n * { worldWidthMeters: 10, worldDepthMeters: 7.5, ... }\n * );\n * // Returns: { x: 4.5, y: 0 } (2m + 2.5m = 4.5m, within ±5m boundary)\n *\n * // Same knockback but would exceed boundary\n * const clampedPosition = applyKnockbackDisplacement(\n * result, // knockback.displacement.x = 2.5m\n * { x: 4, y: 0 }, // Current position: 4m from center\n * { worldWidthMeters: 10, worldDepthMeters: 7.5, ... }\n * );\n * // Returns: { x: 5, y: 0 } (4m + 2.5m = 6.5m, clamped to +5m boundary)\n * ```\n */\nfunction applyKnockbackDisplacement(\n result: CombatResult,\n defenderPos: Position,\n arenaBounds: PhysicsArenaBounds,\n): Position {\n if (!result.knockback) {\n return defenderPos;\n }\n\n // Apply knockback displacement (both in meters)\n // Note: knockback.displacement.z maps to position.y in 2D arena\n const newPos = {\n x: defenderPos.x + result.knockback.displacement.x,\n y: defenderPos.y + result.knockback.displacement.z,\n };\n\n // Clamp to arena boundaries using shared physics helper\n return clampToArenaBounds(newPos, arenaBounds);\n}\n\nexport interface UseCombatActionsConfig {\n readonly validPlayers: readonly [PlayerState, PlayerState];\n readonly playerPositions: readonly [Position, Position];\n readonly combatState: CombatScreenState;\n readonly combatActions: CombatActions;\n readonly combatSystem: CombatSystem;\n readonly onPlayerUpdate: (\n playerIndex: number,\n updates: Partial<PlayerState>,\n ) => void;\n readonly onPlayerPositionUpdate?: (\n playerIndex: number,\n position: Position,\n ) => void;\n readonly onLateralityUpdate?: (\n playerIndex: number,\n laterality: \"left\" | \"right\",\n ) => void;\n readonly onInjuryCreated?: (\n injury: Injury,\n targetPlayerIndex: number,\n ) => void;\n readonly addCombatMessage: (korean: string, english: string) => void;\n readonly addHitEffect: (\n type: HitEffectType,\n position: Position,\n intensity?: number,\n ) => void;\n readonly arenaBounds: PhysicsArenaBounds;\n readonly combatAudio?: {\n readonly playAttackSound: (intensity?: AttackIntensity) => Promise<void>;\n readonly playHitSound: (damage: number) => Promise<void>;\n readonly playBoneImpactSound: (options: {\n region?: import(\"../../../../audio/types\").AudioBodyRegion;\n intensity?: import(\"../../../../audio/types\").ImpactIntensity;\n damage?: number;\n remainingHealth?: number;\n vitalPoint?: boolean;\n hitPosition?: { x: number; y: number; z?: number };\n }) => Promise<void>;\n readonly playBlockSound: (guardBroken?: boolean) => Promise<void>;\n readonly playDodgeSound: () => Promise<void>;\n readonly playStanceChangeSound: () => Promise<void>;\n readonly playSpecialTechniqueSound: () => Promise<void>;\n };\n readonly playerAnimations?: {\n readonly player1: {\n readonly transitionTo: (state: AnimationState) => boolean;\n };\n readonly player2: {\n readonly transitionTo: (state: AnimationState) => boolean;\n };\n };\n}\n\nexport interface UseCombatActionsReturn {\n readonly handleAttack: (technique?: Technique) => void;\n readonly handleDefend: () => void;\n readonly handleTechniqueExecute: () => void;\n readonly handleStanceSwitch: (stance: TrigramStance) => void;\n readonly handleStanceSideSwitch: (playerIndex: 0 | 1) => void;\n readonly handleAIAttack: (\n technique?: KoreanTechnique,\n targetVitalPoint?: string,\n ) => void;\n readonly handleAIDefend: () => void;\n readonly handleAITechnique: (\n technique?: KoreanTechnique,\n targetVitalPoint?: string,\n ) => void;\n readonly moveAIPlayer: (targetPos: Position) => void;\n}\n\n/**\n * Helper function to convert Technique to KoreanTechnique format\n * @param technique - The technique to convert\n * @param stance - Current player stance\n * @returns KoreanTechnique compatible with CombatSystem\n */\nfunction convertTechniqueToKorean(\n technique: Technique,\n stance: TrigramStance,\n): KoreanTechnique {\n return {\n id: technique.id,\n name: {\n korean: technique.name.korean,\n english: technique.name.english,\n romanized: technique.name.romanized ?? \"\",\n },\n koreanName: technique.name.korean,\n englishName: technique.name.english,\n romanized: technique.name.romanized ?? \"\",\n description: {\n korean: technique.description.korean,\n english: technique.description.english,\n },\n stance: technique.requiredStance ?? stance,\n type: \"attack\",\n damageType: technique.damageType,\n damage: (technique.damage.min + technique.damage.max) / 2, // Use average damage\n kiCost: technique.kiCost,\n staminaCost: technique.staminaCost,\n accuracy: 0.85, // Default accuracy\n reachConfig: {\n bodyPart: \"arm\" as const,\n techniqueType: \"punch\" as const,\n baseExtension: 0.9,\n },\n executionTime: technique.animationDuration ?? 400,\n recoveryTime: 300,\n critChance: technique.criticalChance ?? 0.1,\n critMultiplier: 1.5,\n effects: [],\n };\n}\n\n/**\n * Custom hook for combat action handlers\n */\nexport function useCombatActions(\n config: UseCombatActionsConfig,\n): UseCombatActionsReturn {\n const {\n validPlayers,\n playerPositions,\n combatState,\n combatActions,\n combatSystem,\n onPlayerUpdate,\n onLateralityUpdate,\n onInjuryCreated,\n addCombatMessage,\n addHitEffect,\n arenaBounds,\n combatAudio,\n } = config;\n\n // Refs to track knockback recovery timeouts for cleanup\n const player1KnockbackTimeoutRef = useRef<NodeJS.Timeout | null>(null);\n const player2KnockbackTimeoutRef = useRef<NodeJS.Timeout | null>(null);\n\n // Cleanup timeouts on unmount\n useEffect(() => {\n return () => {\n if (player1KnockbackTimeoutRef.current) {\n clearTimeout(player1KnockbackTimeoutRef.current);\n }\n if (player2KnockbackTimeoutRef.current) {\n clearTimeout(player2KnockbackTimeoutRef.current);\n }\n };\n }, []);\n\n // Player attack handler\n const handleAttack = useCallback(\n (technique?: Technique) => {\n if (\n combatState.isExecutingTechnique ||\n !combatState.roundStarted ||\n combatState.roundEnded\n )\n return;\n\n const player = validPlayers[0];\n const currentStance = player.currentStance;\n const archetype = player.archetype;\n\n // Use provided technique or select from stance techniques\n let attackTechnique: KoreanTechnique;\n\n if (technique) {\n // Convert selected technique to KoreanTechnique format\n attackTechnique = convertTechniqueToKorean(technique, currentStance);\n } else {\n // Get techniques for current stance and archetype\n const availableTechniques =\n KoreanTechniquesSystem.getAllAvailableTechniques(\n currentStance,\n archetype,\n );\n\n if (availableTechniques.length === 0) {\n console.warn(\n `No techniques found for stance: ${currentStance}, archetype: ${archetype}`,\n );\n addCombatMessage(\"기술 없음\", \"No techniques available\");\n return;\n }\n\n // Select primary technique (first in list)\n const selectedTechnique = availableTechniques[0];\n\n // Check if player has sufficient resources\n if (\n !KoreanTechniquesSystem.canExecuteTechnique(player, selectedTechnique)\n ) {\n addCombatMessage(\"기력/체력 부족\", \"Insufficient Ki/Stamina\");\n return;\n }\n\n attackTechnique = selectedTechnique;\n }\n\n combatActions.setExecutingTechnique(true);\n\n // Play attack sound based on technique damage/intensity\n const damage = attackTechnique.damage ?? 10;\n const intensity: AttackIntensity =\n damage >= 40\n ? \"critical\"\n : damage >= 25\n ? \"heavy\"\n : damage >= 10\n ? \"medium\"\n : \"light\";\n combatAudio?.playAttackSound(intensity);\n\n // Calculate animation timing context for hit detection.\n // For synchronous hit detection, we use the animation's peak time (when limb\n // is fully extended) rather than t=0 (attack start). This ensures the hit\n // window check passes when the attack would visually connect.\n // 동기식 타격 판정: 애니메이션 피크 타임 사용 (팔/다리 완전 신전 시점)\n const animationType = attackTechnique.animationType ?? AnimationType.JAB;\n const hitTiming = getAnimationHitTiming(animationType);\n const peakTime = hitTiming?.hitWindow.peakTime ?? 0.15; // Default to typical punch peak\n const animationContext = {\n animationType,\n currentTime: peakTime, // Use peak time for synchronous hit detection\n };\n\n // Use combat system for proper calculation with animation context\n const result = combatSystem.resolveAttack(\n validPlayers[0],\n validPlayers[1],\n attackTechnique,\n undefined,\n animationContext,\n );\n\n const effectType = result.hit\n ? result.isCritical\n ? HitEffectType.CRITICAL_HIT\n : HitEffectType.HIT\n : HitEffectType.MISS;\n\n addHitEffect(effectType, playerPositions[0], result.hit ? 1 : 0.5);\n\n if (result.hit) {\n // Play bone impact sound with body region and damage context\n const hitPosition = calculateHitPosition(playerPositions[1]);\n\n // Use bone impact audio instead of generic hit sound\n combatAudio?.playBoneImpactSound({\n damage: result.damage,\n remainingHealth: validPlayers[1].health - result.damage,\n vitalPoint: result.isCritical, // Critical hits are often vital points\n hitPosition,\n });\n\n // Combo tracking: reset combo if too much time passed\n const now = Date.now();\n const timeSinceLastHit = now - combatState.lastHitTime;\n const newCombo =\n timeSinceLastHit < 2000 ? combatState.comboCount + 1 : 1;\n combatActions.setComboCount(newCombo);\n combatActions.setLastHitTime(now);\n\n // Apply damage through combat system\n const { updatedAttacker, updatedDefender } =\n combatSystem.applyCombatResult(\n result,\n validPlayers[0],\n validPlayers[1],\n );\n\n onPlayerUpdate(0, updatedAttacker);\n onPlayerUpdate(1, updatedDefender);\n\n // Create injury for trauma visualization\n if (onInjuryCreated) {\n const injury = createInjuryFromDamage(\n result,\n attackTechnique,\n updatedDefender.health,\n 1, // Player 2 (enemy) was hit\n );\n onInjuryCreated(injury, 1);\n }\n\n // Apply knockback displacement (밀침 적용)\n if (result.knockback && config.onPlayerPositionUpdate) {\n const newDefenderPosition = applyKnockbackDisplacement(\n result,\n playerPositions[1],\n config.arenaBounds,\n );\n config.onPlayerPositionUpdate(1, newDefenderPosition);\n\n // Add combat message for significant knockback\n const knockbackDistance = Math.sqrt(\n result.knockback.displacement.x ** 2 +\n result.knockback.displacement.z ** 2,\n );\n if (knockbackDistance > 1.5) {\n const knockbackName = KnockbackPhysics.getKnockbackStateName(\n result.knockback.shouldFall,\n );\n addCombatMessage(knockbackName.korean, knockbackName.english);\n }\n\n // Set stunned state for knockback duration (non-interruptible)\n if (result.knockback.duration > 0) {\n // Clear any existing timeout for player 2\n if (player2KnockbackTimeoutRef.current) {\n clearTimeout(player2KnockbackTimeoutRef.current);\n }\n\n onPlayerUpdate(1, { isStunned: true });\n\n // Schedule recovery after knockback duration + recovery window\n player2KnockbackTimeoutRef.current = setTimeout(\n () => {\n onPlayerUpdate(1, { isStunned: false });\n player2KnockbackTimeoutRef.current = null;\n },\n (result.knockback.duration + result.knockback.recoveryWindow) *\n 1000,\n );\n }\n }\n\n // Check if defender should fall after taking damage\n if (config.playerAnimations?.player2) {\n const fallCheck = checkForFall(\n updatedDefender,\n combatSystem,\n undefined, // lastImpactAngle not tracked yet\n undefined, // attackAngle not tracked yet\n );\n\n if (\n fallCheck.shouldFall &&\n fallCheck.animationState &&\n fallCheck.fallType\n ) {\n config.playerAnimations.player2.transitionTo(\n fallCheck.animationState,\n );\n const fallName = getFallTypeName(fallCheck.fallType);\n addCombatMessage(`${fallName.korean}!`, `${fallName.english}!`);\n }\n }\n\n // Display technique name in combat log\n const techniqueNameKorean =\n attackTechnique.koreanName ?? attackTechnique.name.korean;\n const techniqueNameEnglish =\n attackTechnique.englishName ?? attackTechnique.name.english;\n\n if (result.isCritical) {\n addCombatMessage(\n `치명타! ${techniqueNameKorean}`,\n `Critical Hit! ${techniqueNameEnglish}`,\n );\n } else if (newCombo > 2) {\n addCombatMessage(\n `${newCombo} 연속! ${techniqueNameKorean}`,\n `${newCombo} Combo! ${techniqueNameEnglish}`,\n );\n } else {\n addCombatMessage(\n `${techniqueNameKorean} 성공!`,\n `${techniqueNameEnglish} Hit!`,\n );\n }\n } else {\n combatActions.resetCombo();\n const techniqueNameKorean =\n attackTechnique.koreanName ?? attackTechnique.name.korean;\n const techniqueNameEnglish =\n attackTechnique.englishName ?? attackTechnique.name.english;\n addCombatMessage(\n `${techniqueNameKorean} 빗나감`,\n `${techniqueNameEnglish} Missed`,\n );\n }\n\n setTimeout(() => combatActions.setExecutingTechnique(false), 500);\n },\n [\n validPlayers,\n playerPositions,\n combatState.isExecutingTechnique,\n combatState.roundStarted,\n combatState.roundEnded,\n combatState.comboCount,\n combatState.lastHitTime,\n combatActions,\n combatSystem,\n onPlayerUpdate,\n onInjuryCreated,\n addCombatMessage,\n addHitEffect,\n combatAudio,\n config,\n ],\n );\n\n // Player defend handler\n const handleDefend = useCallback(() => {\n if (!combatState.roundStarted || combatState.roundEnded) return;\n\n // Play block sound\n combatAudio?.playBlockSound(false);\n\n onPlayerUpdate(0, { isBlocking: true });\n addCombatMessage(\"방어 자세\", \"Defensive Stance\");\n addHitEffect(HitEffectType.BLOCK, playerPositions[0], 0.8);\n\n setTimeout(() => {\n onPlayerUpdate(0, { isBlocking: false });\n }, 1000);\n }, [\n combatState.roundStarted,\n combatState.roundEnded,\n onPlayerUpdate,\n addCombatMessage,\n addHitEffect,\n playerPositions,\n combatAudio,\n ]);\n\n // Player technique handler\n const handleTechniqueExecute = useCallback(() => {\n if (\n combatState.isExecutingTechnique ||\n !combatState.roundStarted ||\n combatState.roundEnded\n )\n return;\n if (validPlayers[0].ki < 10 || validPlayers[0].stamina < 15) {\n addCombatMessage(\"기력/체력 부족\", \"Insufficient Ki/Stamina\");\n return;\n }\n\n combatActions.setExecutingTechnique(true);\n\n // Play special technique sound\n combatAudio?.playSpecialTechniqueSound();\n\n addHitEffect(HitEffectType.CRITICAL_HIT, playerPositions[0], 1.5);\n\n // Screen shake effect for impact\n const shakeIntensity = 8;\n const shakeFrames = [\n { x: shakeIntensity, y: -shakeIntensity * 0.5 },\n { x: -shakeIntensity * 0.7, y: shakeIntensity * 0.8 },\n { x: shakeIntensity * 0.5, y: shakeIntensity * 0.3 },\n { x: -shakeIntensity * 0.3, y: -shakeIntensity * 0.6 },\n { x: 0, y: 0 },\n ];\n\n shakeFrames.forEach((shake, index) => {\n setTimeout(() => combatActions.setScreenShake(shake), index * 50);\n });\n\n const distance = Math.sqrt(\n Math.pow(playerPositions[0].x - playerPositions[1].x, 2) +\n Math.pow(playerPositions[0].y - playerPositions[1].y, 2),\n );\n\n if (distance < 150) {\n // Play bone impact sound for special technique hit\n const hitPosition = calculateHitPosition(playerPositions[1]);\n\n combatAudio?.playBoneImpactSound({\n damage: 25,\n remainingHealth: validPlayers[1].health - 25,\n vitalPoint: false,\n hitPosition,\n });\n\n onPlayerUpdate(1, {\n health: Math.max(0, validPlayers[1].health - 25),\n hitsTaken: validPlayers[1].hitsTaken + 1,\n });\n addCombatMessage(\"특수 기술 성공!\", \"Special Technique Hit!\");\n } else {\n addCombatMessage(\"기술 실패\", \"Technique Failed\");\n }\n\n // Consume resources\n onPlayerUpdate(0, {\n ki: Math.max(0, validPlayers[0].ki - 10),\n stamina: Math.max(0, validPlayers[0].stamina - 15),\n });\n\n setTimeout(() => combatActions.setExecutingTechnique(false), 800);\n }, [\n validPlayers,\n playerPositions,\n combatState.isExecutingTechnique,\n combatState.roundStarted,\n combatState.roundEnded,\n combatActions,\n onPlayerUpdate,\n addCombatMessage,\n addHitEffect,\n combatAudio,\n ]);\n\n // Player stance switch handler\n const handleStanceSwitch = useCallback(\n (stance: TrigramStance) => {\n if (!combatState.roundStarted || combatState.roundEnded) return;\n\n // Play stance change sound\n combatAudio?.playStanceChangeSound();\n\n onPlayerUpdate(0, { currentStance: stance });\n addCombatMessage(`자세 변경: ${stance}`, `Stance Change: ${stance}`);\n addHitEffect(HitEffectType.STATUS_EFFECT, playerPositions[0], 0.6);\n },\n [\n combatState.roundStarted,\n combatState.roundEnded,\n onPlayerUpdate,\n addCombatMessage,\n addHitEffect,\n playerPositions,\n combatAudio,\n ],\n );\n\n /**\n * Handle stance side switch (left/right)\n * @korean 자세측면전환처리\n */\n // Reuse StanceManager instance for stance side switches\n const stanceManagerRef = useRef<StanceManager>(new StanceManager());\n\n const handleStanceSideSwitch = useCallback(\n (playerIndex: 0 | 1) => {\n if (!combatState.roundStarted || combatState.roundEnded) return;\n\n const player = validPlayers[playerIndex];\n // Get current laterality from combat state\n const currentLaterality = combatState.playerLaterality[playerIndex];\n\n const result = stanceManagerRef.current.switchStanceSide(\n player,\n currentLaterality,\n );\n\n if (result.success && result.laterality) {\n // Update player state with new stamina\n onPlayerUpdate(playerIndex, result.updatedPlayer);\n\n // Update laterality in combat state via callback\n onLateralityUpdate?.(playerIndex, result.laterality);\n\n // Audio feedback\n combatAudio?.playStanceChangeSound?.();\n\n // Visual feedback\n const koreanText =\n result.laterality === \"left\" ? \"왼발서기\" : \"오른발서기\";\n const englishText =\n result.laterality === \"left\" ? \"Left Stance\" : \"Right Stance\";\n addCombatMessage(koreanText, englishText);\n\n // Visual effect\n addHitEffect(\n HitEffectType.STATUS_EFFECT,\n playerPositions[playerIndex],\n 0.5,\n );\n } else {\n // Feedback for failed switch\n if (result.message?.includes(\"stamina\")) {\n addCombatMessage(\"체력 부족\", \"Insufficient Stamina\");\n } else if (result.message?.includes(\"cooldown\")) {\n addCombatMessage(\"대기 중\", \"On Cooldown\");\n }\n }\n },\n [\n combatState.roundStarted,\n combatState.roundEnded,\n combatState.playerLaterality,\n validPlayers,\n onPlayerUpdate,\n onLateralityUpdate,\n combatAudio,\n addCombatMessage,\n addHitEffect,\n playerPositions,\n ],\n );\n\n /**\n * Helper function to create AI technique objects\n * Reduces code duplication between basic attacks and special techniques\n */\n const createAITechnique = useCallback(\n (type: \"basic\" | \"special\", aiPlayer: PlayerState) => {\n if (type === \"basic\") {\n return {\n id: \"ai_basic_attack\",\n name: {\n korean: \"AI 기본공격\",\n english: \"AI Basic Attack\",\n romanized: \"ai_gibon_gonggyeok\",\n },\n koreanName: \"AI 기본공격\",\n englishName: \"AI Basic Attack\",\n romanized: \"ai_gibon_gonggyeok\",\n description: { korean: \"AI 기본 공격\", english: \"AI basic attack\" },\n stance: aiPlayer.currentStance,\n type: \"attack\" as const,\n damageType: \"physical\" as const,\n damage: 15,\n kiCost: 5,\n staminaCost: 8,\n accuracy: 0.8,\n reachConfig: {\n bodyPart: \"arm\" as const,\n techniqueType: \"punch\" as const,\n baseExtension: 0.95,\n },\n executionTime: 400,\n recoveryTime: 300,\n critChance: 0.1,\n critMultiplier: 1.5,\n effects: [],\n animationType: AnimationType.JAB, // Default animation for basic attack\n };\n } else {\n return {\n id: \"ai_special_technique\",\n name: {\n korean: \"AI 특수기술\",\n english: \"AI Special Technique\",\n romanized: \"ai_teuksu_gisul\",\n },\n koreanName: \"AI 특수기술\",\n englishName: \"AI Special Technique\",\n romanized: \"ai_teuksu_gisul\",\n description: {\n korean: \"AI 특수 기술\",\n english: \"AI special technique\",\n },\n stance: aiPlayer.currentStance,\n type: \"technique\" as const,\n damageType: \"physical\" as const,\n damage: 25,\n kiCost: 10,\n staminaCost: 15,\n accuracy: 0.85,\n reachConfig: {\n bodyPart: \"leg\" as const,\n techniqueType: \"kick\" as const,\n baseExtension: 1.1,\n },\n executionTime: 600,\n recoveryTime: 800,\n critChance: 0.15,\n critMultiplier: 1.8,\n effects: [],\n animationType: AnimationType.SPINNING_HOOK, // Default animation for special technique\n };\n }\n },\n [],\n );\n\n /**\n * Helper function to determine hit effect type based on combat result\n * Reduces duplication between attack and technique handlers\n */\n const getHitEffectType = useCallback(\n (result: { hit: boolean; isCritical?: boolean }): HitEffectType => {\n if (!result.hit) return HitEffectType.MISS;\n return result.isCritical ? HitEffectType.CRITICAL_HIT : HitEffectType.HIT;\n },\n [],\n );\n\n /**\n * AI attack handler with technique and vital point targeting\n *\n * @param technique - Optional Korean martial arts technique to execute. If not provided, creates a basic attack.\n * @param targetVitalPoint - Optional vital point ID to target for increased damage effectiveness.\n */\n const handleAIAttack = useCallback(\n (technique?: KoreanTechnique, targetVitalPoint?: string) => {\n const aiPlayer = validPlayers[1];\n const targetPlayer = validPlayers[0];\n\n // Use provided technique or create basic attack technique\n const attackTechnique = technique ?? createAITechnique(\"basic\", aiPlayer);\n\n // Play attack sound based on technique damage/intensity (consistent with player)\n const damage = attackTechnique.damage ?? 10;\n const intensity: AttackIntensity =\n damage >= 40\n ? \"critical\"\n : damage >= 25\n ? \"heavy\"\n : damage >= 10\n ? \"medium\"\n : \"light\";\n combatAudio?.playAttackSound(intensity);\n\n // Calculate animation timing context for AI hit detection (same as player)\n // 동기식 타격 판정: AI도 피크 타임 사용\n const animationType = attackTechnique.animationType ?? AnimationType.JAB;\n const hitTiming = getAnimationHitTiming(animationType);\n const peakTime = hitTiming?.hitWindow.peakTime ?? 0.15;\n const animationContext = {\n animationType,\n currentTime: peakTime,\n };\n\n // Use combat system for proper calculation with vital point targeting and animation context\n const result = combatSystem.resolveAttack(\n aiPlayer,\n targetPlayer,\n attackTechnique,\n targetVitalPoint, // Pass vital point ID for targeting\n animationContext, // Pass animation context for distance/reach check\n );\n\n const effectType = getHitEffectType(result);\n addHitEffect(effectType, playerPositions[1], result.hit ? 1 : 0.5);\n\n if (result.hit) {\n // Play bone impact sound for AI hits on player\n const hitPosition = calculateHitPosition(playerPositions[0]);\n\n combatAudio?.playBoneImpactSound({\n damage: result.damage,\n remainingHealth: validPlayers[0].health - result.damage,\n vitalPoint: result.isCritical,\n hitPosition,\n });\n\n // Apply damage through combat system (deducts resources)\n const { updatedAttacker, updatedDefender } =\n combatSystem.applyCombatResult(result, aiPlayer, targetPlayer);\n\n onPlayerUpdate(1, updatedAttacker);\n onPlayerUpdate(0, updatedDefender);\n\n // Create injury for trauma visualization (AI hit player)\n if (onInjuryCreated) {\n const injury = createInjuryFromDamage(\n result,\n attackTechnique,\n updatedDefender.health,\n 0, // Player 1 (player) was hit by AI\n );\n onInjuryCreated(injury, 0);\n }\n\n // Apply knockback displacement for AI attacks (밀침 적용)\n if (result.knockback && config.onPlayerPositionUpdate) {\n const newDefenderPosition = applyKnockbackDisplacement(\n result,\n playerPositions[0],\n config.arenaBounds,\n );\n config.onPlayerPositionUpdate(0, newDefenderPosition);\n\n // Add combat message for significant knockback\n const knockbackDistance = Math.sqrt(\n result.knockback.displacement.x ** 2 +\n result.knockback.displacement.z ** 2,\n );\n if (knockbackDistance > 1.5) {\n const knockbackName = KnockbackPhysics.getKnockbackStateName(\n result.knockback.shouldFall,\n );\n addCombatMessage(\n `AI ${knockbackName.korean}`,\n `AI ${knockbackName.english}`,\n );\n }\n\n // Set stunned state for knockback duration (non-interruptible)\n if (result.knockback.duration > 0) {\n // Clear any existing timeout for player 1\n if (player1KnockbackTimeoutRef.current) {\n clearTimeout(player1KnockbackTimeoutRef.current);\n }\n\n onPlayerUpdate(0, { isStunned: true });\n\n // Schedule recovery after knockback duration + recovery window\n player1KnockbackTimeoutRef.current = setTimeout(\n () => {\n onPlayerUpdate(0, { isStunned: false });\n player1KnockbackTimeoutRef.current = null;\n },\n (result.knockback.duration + result.knockback.recoveryWindow) *\n 1000,\n );\n }\n }\n\n // Check if player should fall after taking damage from AI\n if (config.playerAnimations?.player1) {\n const fallCheck = checkForFall(\n updatedDefender,\n combatSystem,\n undefined, // lastImpactAngle not tracked yet\n undefined, // attackAngle not tracked yet\n );\n\n if (\n fallCheck.shouldFall &&\n fallCheck.animationState &&\n fallCheck.fallType\n ) {\n config.playerAnimations.player1.transitionTo(\n fallCheck.animationState,\n );\n const fallName = getFallTypeName(fallCheck.fallType);\n addCombatMessage(`${fallName.korean}!`, `${fallName.english}!`);\n }\n }\n\n // Enhanced combat message with vital point info\n if (result.vitalPointHit && targetVitalPoint) {\n const vitalPoint = getVitalPointById(targetVitalPoint);\n const vpName = vitalPoint\n ? vitalPoint.names.korean\n : targetVitalPoint;\n addCombatMessage(\n `AI 급소 타격! ${vpName}`,\n `AI Vital Point Hit! ${\n vitalPoint?.names.english ?? targetVitalPoint\n }`,\n );\n } else if (result.isCritical) {\n addCombatMessage(\"AI 치명타!\", \"AI Critical Hit!\");\n } else {\n addCombatMessage(\"AI 공격 성공!\", \"AI Attack Hit!\");\n }\n } else {\n // Consume resources on miss for consistency with technique behavior\n onPlayerUpdate(1, {\n ki: Math.max(0, aiPlayer.ki - attackTechnique.kiCost),\n stamina: Math.max(0, aiPlayer.stamina - attackTechnique.staminaCost),\n });\n addCombatMessage(\"AI 공격 빗나감\", \"AI Attack Missed\");\n }\n },\n [\n validPlayers,\n playerPositions,\n combatSystem,\n onPlayerUpdate,\n onInjuryCreated,\n addCombatMessage,\n addHitEffect,\n combatAudio,\n createAITechnique,\n getHitEffectType,\n config,\n ],\n );\n\n // AI defend handler\n const handleAIDefend = useCallback(() => {\n // Play block sound\n combatAudio?.playBlockSound(false);\n\n onPlayerUpdate(1, { isBlocking: true });\n addCombatMessage(\"AI 방어 자세\", \"AI Defensive Stance\");\n addHitEffect(HitEffectType.BLOCK, playerPositions[1], 0.8);\n\n setTimeout(() => {\n onPlayerUpdate(1, { isBlocking: false });\n }, 1000);\n }, [\n onPlayerUpdate,\n addCombatMessage,\n addHitEffect,\n playerPositions,\n combatAudio,\n ]);\n\n /**\n * AI technique handler with technique and vital point targeting\n *\n * @param technique - Optional special Korean martial arts technique to execute. If not provided, creates a special technique.\n * @param targetVitalPoint - Optional vital point ID to target for increased damage effectiveness.\n */\n const handleAITechnique = useCallback(\n (technique?: KoreanTechnique, targetVitalPoint?: string) => {\n const aiPlayer = validPlayers[1];\n const targetPlayer = validPlayers[0];\n\n // Use provided technique or create special technique\n const specialTechnique =\n technique ?? createAITechnique(\"special\", aiPlayer);\n\n // Check if AI has sufficient resources for the technique\n if (\n aiPlayer.ki < specialTechnique.kiCost ||\n aiPlayer.stamina < specialTechnique.staminaCost\n ) {\n handleAIAttack(undefined, targetVitalPoint); // Fallback to basic attack with same targeting\n return;\n }\n\n // Play special technique sound\n combatAudio?.playSpecialTechniqueSound();\n\n // Calculate animation timing context for AI technique hit detection\n // 동기식 타격 판정: AI 특수 기술도 피크 타임 사용\n const animationType =\n specialTechnique.animationType ?? AnimationType.SPINNING_HOOK;\n const hitTiming = getAnimationHitTiming(animationType);\n const peakTime = hitTiming?.hitWindow.peakTime ?? 0.25; // Special techniques often have longer peak times\n const animationContext = {\n animationType,\n currentTime: peakTime,\n };\n\n // Use combat system for proper calculation with vital point targeting and animation context\n const result = combatSystem.resolveAttack(\n aiPlayer,\n targetPlayer,\n specialTechnique,\n targetVitalPoint, // Pass vital point ID for targeting\n animationContext, // Pass animation context for distance/reach check\n );\n\n const effectType = result.hit\n ? HitEffectType.CRITICAL_HIT\n : HitEffectType.MISS;\n\n addHitEffect(effectType, playerPositions[1], result.hit ? 1.5 : 0.5);\n\n if (result.hit) {\n // Play bone impact sound for AI technique hits\n const hitPosition = calculateHitPosition(playerPositions[0]);\n\n combatAudio?.playBoneImpactSound({\n damage: result.damage,\n remainingHealth: validPlayers[0].health - result.damage,\n vitalPoint: result.isCritical === true || !!targetVitalPoint, // Special techniques often target vital points\n hitPosition,\n });\n\n // Apply damage through combat system (deducts resources)\n const { updatedAttacker, updatedDefender } =\n combatSystem.applyCombatResult(result, aiPlayer, targetPlayer);\n\n onPlayerUpdate(1, updatedAttacker);\n onPlayerUpdate(0, updatedDefender);\n\n // Create injury for trauma visualization (AI technique hit player)\n if (onInjuryCreated) {\n const injury = createInjuryFromDamage(\n result,\n specialTechnique,\n updatedDefender.health,\n 0, // Player 1 (player) was hit by AI technique\n );\n onInjuryCreated(injury, 0);\n }\n\n // Apply knockback displacement for AI special techniques (밀침 적용)\n if (result.knockback && config.onPlayerPositionUpdate) {\n const newDefenderPosition = applyKnockbackDisplacement(\n result,\n playerPositions[0],\n config.arenaBounds,\n );\n config.onPlayerPositionUpdate(0, newDefenderPosition);\n\n // Add combat message for significant knockback\n const knockbackDistance = Math.sqrt(\n result.knockback.displacement.x ** 2 +\n result.knockback.displacement.z ** 2,\n );\n if (knockbackDistance > 1.5) {\n const knockbackName = KnockbackPhysics.getKnockbackStateName(\n result.knockback.shouldFall,\n );\n addCombatMessage(\n `AI 특수 ${knockbackName.korean}`,\n `AI Special ${knockbackName.english}`,\n );\n }\n\n // Set stunned state for knockback duration (non-interruptible)\n if (result.knockback.duration > 0) {\n // Clear any existing timeout for player 1\n if (player1KnockbackTimeoutRef.current) {\n clearTimeout(player1KnockbackTimeoutRef.current);\n }\n\n onPlayerUpdate(0, { isStunned: true });\n\n // Schedule recovery after knockback duration + recovery window\n player1KnockbackTimeoutRef.current = setTimeout(\n () => {\n onPlayerUpdate(0, { isStunned: false });\n player1KnockbackTimeoutRef.current = null;\n },\n (result.knockback.duration + result.knockback.recoveryWindow) *\n 1000,\n );\n }\n }\n\n // Check if player should fall after taking damage from AI technique\n if (config.playerAnimations?.player1) {\n const fallCheck = checkForFall(\n updatedDefender,\n combatSystem,\n undefined, // lastImpactAngle not tracked yet\n undefined, // attackAngle not tracked yet\n );\n\n if (\n fallCheck.shouldFall &&\n fallCheck.animationState &&\n fallCheck.fallType\n ) {\n config.playerAnimations.player1.transitionTo(\n fallCheck.animationState,\n );\n const fallName = getFallTypeName(fallCheck.fallType);\n addCombatMessage(`${fallName.korean}!`, `${fallName.english}!`);\n }\n }\n\n // Enhanced combat message with vital point info\n if (result.vitalPointHit && targetVitalPoint) {\n const vitalPoint = getVitalPointById(targetVitalPoint);\n const vpName = vitalPoint\n ? vitalPoint.names.korean\n : targetVitalPoint;\n addCombatMessage(\n `AI 특수 급소 기술! ${vpName}`,\n `AI Special Vital Point Technique! ${\n vitalPoint?.names.english ?? targetVitalPoint\n }`,\n );\n } else {\n addCombatMessage(\"AI 특수 기술!\", \"AI Special Technique!\");\n }\n } else {\n // Consume resources on miss (technique was attempted)\n onPlayerUpdate(1, {\n ki: Math.max(0, aiPlayer.ki - specialTechnique.kiCost),\n stamina: Math.max(0, aiPlayer.stamina - specialTechnique.staminaCost),\n });\n addCombatMessage(\"AI 기술 빗나감\", \"AI Technique Missed\");\n }\n },\n [\n validPlayers,\n playerPositions,\n combatSystem,\n onPlayerUpdate,\n onInjuryCreated,\n addCombatMessage,\n addHitEffect,\n handleAIAttack,\n combatAudio,\n createAITechnique,\n config,\n ],\n );\n\n // AI movement handler with injury-based movement penalties\n // **UPDATED**: Now scale-aware for consistent movement and distance calculations\n // **FIX**: Positions are in METERS, not pixels - use meters-based speed\n const moveAIPlayer = useCallback(\n (targetPos: Position) => {\n const currentPos = playerPositions[1];\n const aiPlayer = validPlayers[1];\n\n // Movement speed calibrated for physics-first system (all in METERS)\n // Combat closing speed: ~2.5 m/s (fast tactical approach, not slow walking)\n // Real fights are over in 4-5 seconds - AI must close distance quickly\n // AI decision loop frequency (defined in useAICombat.ts)\n const AI_DECISION_FREQUENCY_HZ = 20; // 20 calls/second (50ms interval)\n // Calculation: 2.5 m/s / 20 calls/s = 0.125 meters per call\n const baseSpeed = 2.5 / AI_DECISION_FREQUENCY_HZ; // meters per call (0.125m per call)\n\n // Calculate movement direction vector (in meters)\n const dx = targetPos.x - currentPos.x;\n const dy = targetPos.y - currentPos.y;\n const distance = Math.sqrt(dx * dx + dy * dy);\n\n // Apply movement penalties from leg injuries if body part health exists\n let finalSpeed = baseSpeed;\n if (aiPlayer.bodyPartHealth && aiPlayer.bodyPartMaxHealth) {\n // Normalize movement direction\n const movementDirection = {\n x: distance > 0 ? dx / distance : 0,\n y: distance > 0 ? dy / distance : 0,\n };\n\n // Calculate modified speed with all penalties applied\n finalSpeed = movementPenaltySystem.calculateModifiedSpeed(\n baseSpeed,\n aiPlayer.bodyPartHealth,\n aiPlayer.bodyPartMaxHealth,\n movementDirection,\n );\n }\n\n // Physics-first: positions are in METERS, so distance is in meters\n // Stop moving when within 0.05 meters (5cm) of target - close enough for melee range\n const MIN_MOVEMENT_THRESHOLD_METERS = 0.05;\n\n if (distance > MIN_MOVEMENT_THRESHOLD_METERS) {\n const newPos = {\n x: currentPos.x + (dx / distance) * finalSpeed,\n y: currentPos.y + (dy / distance) * finalSpeed,\n };\n\n // Keep AI within arena bounds (positions in meters, centered at origin)\n const halfWidth = arenaBounds.worldWidthMeters / 2;\n const halfDepth = arenaBounds.worldDepthMeters / 2;\n newPos.x = Math.max(-halfWidth, Math.min(halfWidth, newPos.x));\n newPos.y = Math.max(-halfDepth, Math.min(halfDepth, newPos.y));\n\n // Update position through parent - this should trigger playerPositions state update in parent\n onPlayerUpdate(1, { position: newPos });\n }\n },\n [playerPositions, validPlayers, arenaBounds, onPlayerUpdate],\n );\n\n return {\n handleAttack,\n handleDefend,\n handleTechniqueExecute,\n handleStanceSwitch,\n handleStanceSideSwitch,\n handleAIAttack,\n handleAIDefend,\n handleAITechnique,\n moveAIPlayer,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"useCombatActions.js","names":[],"sources":["../../../../../src/components/screens/combat/hooks/useCombatActions.ts"],"sourcesContent":["/**\n * useCombatActions Hook - Combat Action Handlers\n *\n * Custom hook for managing combat action handlers.\n * Consolidates player attack, defend, technique, and AI action logic.\n *\n * Performance:\n * - Memoized callbacks to prevent recreation\n * - Centralized action logic for better maintainability\n * - Reduces main component complexity\n *\n * @param config Combat action configuration\n * @returns Combat action handlers\n *\n * @example\n * ```typescript\n * const {\n * handleAttack,\n * handleDefend,\n * handleTechniqueExecute,\n * handleStanceSwitch,\n * handleAIAttack,\n * handleAIDefend,\n * handleAITechnique,\n * moveAIPlayer\n * } = useCombatActions({\n * validPlayers,\n * playerPositions,\n * combatState,\n * combatActions,\n * combatSystem,\n * onPlayerUpdate,\n * addCombatMessage,\n * addHitEffect,\n * arenaBounds\n * });\n * ```\n */\n\nimport { PlayerState } from \"@/systems\";\nimport {\n AnimationType,\n getAnimationHitTiming,\n type AnimationState,\n} from \"@/systems/animation\";\nimport { movementPenaltySystem } from \"@/systems/bodypart\";\nimport { clampToArenaBounds, type PhysicsArenaBounds } from \"@/types/PhysicsTypes\";\nimport {\n checkForFall,\n getFallTypeName,\n} from \"@/systems/combat/FallIntegration\";\nimport type { CombatResult } from \"@/systems/combat/types\";\nimport { CombatSystem } from \"@/systems/CombatSystem\";\nimport { HitEffectType } from \"@/systems/effects\";\nimport { KnockbackPhysics } from \"@/systems/physics\";\nimport { StanceManager } from \"@/systems/trigram\";\nimport { KoreanTechniquesSystem } from \"@/systems/trigram/KoreanTechniques\";\nimport { getVitalPointById } from \"@/systems/vitalpoint/KoreanVitalPoints\";\nimport { KoreanTechnique } from \"@/systems/vitalpoint/types\";\nimport { Position, Technique, TrigramStance, BodyRegion } from \"@/types\";\nimport { Injury, InjuryType } from \"@/types/injury\";\nimport { useCallback, useEffect, useRef } from \"react\";\nimport { AttackIntensity } from \"./useCombatAudio\";\nimport { CombatActions, CombatScreenState } from \"./useCombatState\";\n\n/**\n * Hit position variation range for randomizing strike heights\n * Produces ±0.2 absolute units (±10% of 2.0 character height)\n */\nconst HIT_Y_VARIATION_RANGE = 0.4;\n\n/**\n * Calculate randomized hit position based on defender position\n * Adds vertical variation to simulate different strike heights\n *\n * @param defenderPos - Position of the defender being struck\n * @returns Hit position with randomized Y coordinate\n */\nfunction calculateHitPosition(defenderPos: Position): { x: number; y: number } {\n const hitYVariation = (Math.random() - 0.5) * HIT_Y_VARIATION_RANGE; // ±0.2 units\n return {\n x: defenderPos.x,\n y: Math.max(0.3, Math.min(1.8, defenderPos.y + hitYVariation)),\n };\n}\n\n/**\n * Determine injury type from combat result and technique damage type\n * 전투 결과와 기술 피해 유형으로 부상 유형 결정\n *\n * @param result - Combat result with damage information\n * @param technique - Technique that was executed\n * @returns InjuryType to create\n */\nfunction determineInjuryType(\n result: CombatResult,\n technique: KoreanTechnique,\n): InjuryType {\n // Slashing damage creates cuts (damageType is a string, not enum)\n if (technique.damageType === \"slashing\") {\n return result.damage > 20 ? InjuryType.LACERATION : InjuryType.CUT;\n }\n\n // Heavy damage creates severe bruising\n if (result.damage > 25) {\n return InjuryType.BRUISE;\n }\n\n // Medium damage creates moderate bruising\n if (result.damage > 15) {\n return InjuryType.BRUISE;\n }\n\n // Light damage creates light bruising\n return InjuryType.BRUISE;\n}\n\n/**\n * Map body region to 3D position offset on character model\n * 신체 부위를 캐릭터 모델의 3D 위치 오프셋으로 매핑\n *\n * @param region - Body region that was hit\n * @returns Position offset [x, y, z] relative to character center\n */\nfunction getBodyRegionPosition(region: BodyRegion): [number, number, number] {\n // Map body regions to approximate positions on character model\n // Character is ~2 units tall, centered at [0, 0, 0]\n switch (region) {\n case BodyRegion.HEAD:\n return [0, 1.6, 0];\n case BodyRegion.NECK:\n return [0, 1.3, 0];\n case BodyRegion.TORSO:\n case BodyRegion.CORE:\n return [0, 0.8, 0];\n case BodyRegion.LEFT_ARM:\n return [-0.4, 1.0, 0];\n case BodyRegion.RIGHT_ARM:\n return [0.4, 1.0, 0];\n case BodyRegion.LEFT_LEG:\n return [-0.2, 0.2, 0];\n case BodyRegion.RIGHT_LEG:\n return [0.2, 0.2, 0];\n default:\n return [0, 0.8, 0]; // Default to torso\n }\n}\n\n/**\n * Create injury from combat damage result\n * 전투 피해 결과로부터 부상 생성\n *\n * @param result - Combat result with damage details\n * @param technique - Technique that caused the damage\n * @param defenderHealth - Current defender health after damage (0-100 scale)\n * @param targetPlayerIndex - Index of the player who was hit (0 or 1)\n * @returns Injury object for visualization\n */\nfunction createInjuryFromDamage(\n result: CombatResult,\n technique: KoreanTechnique,\n defenderHealth: number,\n targetPlayerIndex: number,\n): Injury {\n // Determine body region - use torso as default if not specified\n const bodyRegion = BodyRegion.TORSO; // TODO: Extract from result when available\n\n // Determine injury type based on damage and technique\n let injuryType = determineInjuryType(result, technique);\n\n // Promote to fracture when health is critically low and damage is severe\n // to align with TraumaOverlay3D fracture behavior\n const isLowHealth = defenderHealth <= 30; // 30% health threshold\n const isSevereDamage = result.damage >= 25; // Severe damage threshold\n if (isLowHealth && isSevereDamage && injuryType !== InjuryType.FRACTURE) {\n injuryType = InjuryType.FRACTURE;\n }\n\n // Calculate severity (0.0 to 1.0) based on damage\n // Normalized so that a ~30-damage hit is treated as near-max severity\n const severity = Math.min(1.0, result.damage / 30);\n\n // Get position on character model for this body region\n const basePosition = getBodyRegionPosition(bodyRegion);\n\n // Add small random offset for variety\n const randomOffset: [number, number, number] = [\n (Math.random() - 0.5) * 0.1,\n (Math.random() - 0.5) * 0.1,\n (Math.random() - 0.5) * 0.1,\n ];\n\n const position: [number, number, number] = [\n basePosition[0] + randomOffset[0],\n basePosition[1] + randomOffset[1],\n basePosition[2] + randomOffset[2],\n ];\n\n return {\n id: `injury_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`,\n region: bodyRegion,\n type: injuryType,\n position,\n severity,\n hitCount: 1,\n timestamp: Date.now(),\n playerId: targetPlayerIndex === 0 ? \"player\" : \"enemy\",\n };\n}\n\n/**\n * Apply knockback displacement to defender position\n *\n * **Korean**: 밀침 적용 (Apply Knockback)\n *\n * Updates the defender's position based on knockback physics calculation.\n * The knockback displacement is applied in the direction of the attack vector\n * (attacker → defender), respecting arena boundaries.\n *\n * **Physics-First Architecture**: Both positions and knockback displacement\n * are in meters. Arena bounds are centered at origin (0, 0) with extent\n * ±worldWidthMeters/2 in X and ±worldDepthMeters/2 in Z.\n *\n * @param result - Combat result containing knockback data (in meters)\n * @param defenderPos - Current defender position (in meters)\n * @param arenaBounds - Arena boundary limits with meter dimensions\n * @returns Updated defender position after knockback (in meters)\n *\n * @example\n * ```typescript\n * // 10m × 7.5m arena, player at x=2m knocked back 2.5m to right\n * const newPosition = applyKnockbackDisplacement(\n * result, // knockback.displacement.x = 2.5m\n * { x: 2, y: 0 }, // Current position: 2m from center\n * { worldWidthMeters: 10, worldDepthMeters: 7.5, ... }\n * );\n * // Returns: { x: 4.5, y: 0 } (2m + 2.5m = 4.5m, within ±5m boundary)\n *\n * // Same knockback but would exceed boundary\n * const clampedPosition = applyKnockbackDisplacement(\n * result, // knockback.displacement.x = 2.5m\n * { x: 4, y: 0 }, // Current position: 4m from center\n * { worldWidthMeters: 10, worldDepthMeters: 7.5, ... }\n * );\n * // Returns: { x: 5, y: 0 } (4m + 2.5m = 6.5m, clamped to +5m boundary)\n * ```\n */\nfunction applyKnockbackDisplacement(\n result: CombatResult,\n defenderPos: Position,\n arenaBounds: PhysicsArenaBounds,\n): Position {\n if (!result.knockback) {\n return defenderPos;\n }\n\n // Apply knockback displacement (both in meters)\n // Note: knockback.displacement.z maps to position.y in 2D arena\n const newPos = {\n x: defenderPos.x + result.knockback.displacement.x,\n y: defenderPos.y + result.knockback.displacement.z,\n };\n\n // Clamp to arena boundaries using shared physics helper\n return clampToArenaBounds(newPos, arenaBounds);\n}\n\nexport interface UseCombatActionsConfig {\n readonly validPlayers: readonly [PlayerState, PlayerState];\n readonly playerPositions: readonly [Position, Position];\n readonly combatState: CombatScreenState;\n readonly combatActions: CombatActions;\n readonly combatSystem: CombatSystem;\n readonly onPlayerUpdate: (\n playerIndex: number,\n updates: Partial<PlayerState>,\n ) => void;\n readonly onPlayerPositionUpdate?: (\n playerIndex: number,\n position: Position,\n ) => void;\n readonly onLateralityUpdate?: (\n playerIndex: number,\n laterality: \"left\" | \"right\",\n ) => void;\n readonly onInjuryCreated?: (\n injury: Injury,\n targetPlayerIndex: number,\n ) => void;\n readonly addCombatMessage: (korean: string, english: string) => void;\n readonly addHitEffect: (\n type: HitEffectType,\n position: Position,\n intensity?: number,\n ) => void;\n readonly arenaBounds: PhysicsArenaBounds;\n readonly combatAudio?: {\n readonly playAttackSound: (intensity?: AttackIntensity) => Promise<void>;\n readonly playHitSound: (damage: number) => Promise<void>;\n readonly playBoneImpactSound: (options: {\n region?: import(\"../../../../audio/types\").AudioBodyRegion;\n intensity?: import(\"../../../../audio/types\").ImpactIntensity;\n damage?: number;\n remainingHealth?: number;\n vitalPoint?: boolean;\n hitPosition?: { x: number; y: number; z?: number };\n }) => Promise<void>;\n readonly playBlockSound: (guardBroken?: boolean) => Promise<void>;\n readonly playDodgeSound: () => Promise<void>;\n readonly playStanceChangeSound: () => Promise<void>;\n readonly playSpecialTechniqueSound: () => Promise<void>;\n };\n readonly playerAnimations?: {\n readonly player1: {\n readonly transitionTo: (state: AnimationState) => boolean;\n };\n readonly player2: {\n readonly transitionTo: (state: AnimationState) => boolean;\n };\n };\n}\n\nexport interface UseCombatActionsReturn {\n readonly handleAttack: (technique?: Technique) => void;\n readonly handleDefend: () => void;\n readonly handleTechniqueExecute: () => void;\n readonly handleStanceSwitch: (stance: TrigramStance) => void;\n readonly handleStanceSideSwitch: (playerIndex: 0 | 1) => void;\n readonly handleAIAttack: (\n technique?: KoreanTechnique,\n targetVitalPoint?: string,\n ) => void;\n readonly handleAIDefend: () => void;\n readonly handleAITechnique: (\n technique?: KoreanTechnique,\n targetVitalPoint?: string,\n ) => void;\n readonly moveAIPlayer: (targetPos: Position) => void;\n}\n\n/**\n * Helper function to convert Technique to KoreanTechnique format\n * @param technique - The technique to convert\n * @param stance - Current player stance\n * @returns KoreanTechnique compatible with CombatSystem\n */\nfunction convertTechniqueToKorean(\n technique: Technique,\n stance: TrigramStance,\n): KoreanTechnique {\n return {\n id: technique.id,\n name: {\n korean: technique.name.korean,\n english: technique.name.english,\n romanized: technique.name.romanized ?? \"\",\n },\n koreanName: technique.name.korean,\n englishName: technique.name.english,\n romanized: technique.name.romanized ?? \"\",\n description: {\n korean: technique.description.korean,\n english: technique.description.english,\n },\n stance: technique.requiredStance ?? stance,\n type: \"attack\",\n damageType: technique.damageType,\n damage: (technique.damage.min + technique.damage.max) / 2, // Use average damage\n kiCost: technique.kiCost,\n staminaCost: technique.staminaCost,\n accuracy: 0.85, // Default accuracy\n reachConfig: {\n bodyPart: \"arm\" as const,\n techniqueType: \"punch\" as const,\n baseExtension: 0.9,\n },\n executionTime: technique.animationDuration ?? 400,\n recoveryTime: 300,\n critChance: technique.criticalChance ?? 0.1,\n critMultiplier: 1.5,\n effects: [],\n };\n}\n\n/**\n * Custom hook for combat action handlers\n */\nexport function useCombatActions(\n config: UseCombatActionsConfig,\n): UseCombatActionsReturn {\n const {\n validPlayers,\n playerPositions,\n combatState,\n combatActions,\n combatSystem,\n onPlayerUpdate,\n onLateralityUpdate,\n onInjuryCreated,\n addCombatMessage,\n addHitEffect,\n arenaBounds,\n combatAudio,\n } = config;\n\n // Refs to track knockback recovery timeouts for cleanup\n const player1KnockbackTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const player2KnockbackTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n // Cleanup timeouts on unmount\n useEffect(() => {\n return () => {\n if (player1KnockbackTimeoutRef.current) {\n clearTimeout(player1KnockbackTimeoutRef.current);\n }\n if (player2KnockbackTimeoutRef.current) {\n clearTimeout(player2KnockbackTimeoutRef.current);\n }\n };\n }, []);\n\n // Player attack handler\n const handleAttack = useCallback(\n (technique?: Technique) => {\n if (\n combatState.isExecutingTechnique ||\n !combatState.roundStarted ||\n combatState.roundEnded\n )\n return;\n\n const player = validPlayers[0];\n const currentStance = player.currentStance;\n const archetype = player.archetype;\n\n // Use provided technique or select from stance techniques\n let attackTechnique: KoreanTechnique;\n\n if (technique) {\n // Convert selected technique to KoreanTechnique format\n attackTechnique = convertTechniqueToKorean(technique, currentStance);\n } else {\n // Get techniques for current stance and archetype\n const availableTechniques =\n KoreanTechniquesSystem.getAllAvailableTechniques(\n currentStance,\n archetype,\n );\n\n if (availableTechniques.length === 0) {\n console.warn(\n `No techniques found for stance: ${currentStance}, archetype: ${archetype}`,\n );\n addCombatMessage(\"기술 없음\", \"No techniques available\");\n return;\n }\n\n // Select primary technique (first in list)\n const selectedTechnique = availableTechniques[0];\n\n // Check if player has sufficient resources\n if (\n !KoreanTechniquesSystem.canExecuteTechnique(player, selectedTechnique)\n ) {\n addCombatMessage(\"기력/체력 부족\", \"Insufficient Ki/Stamina\");\n return;\n }\n\n attackTechnique = selectedTechnique;\n }\n\n combatActions.setExecutingTechnique(true);\n\n // Play attack sound based on technique damage/intensity\n const damage = attackTechnique.damage ?? 10;\n const intensity: AttackIntensity =\n damage >= 40\n ? \"critical\"\n : damage >= 25\n ? \"heavy\"\n : damage >= 10\n ? \"medium\"\n : \"light\";\n combatAudio?.playAttackSound(intensity);\n\n // Calculate animation timing context for hit detection.\n // For synchronous hit detection, we use the animation's peak time (when limb\n // is fully extended) rather than t=0 (attack start). This ensures the hit\n // window check passes when the attack would visually connect.\n // 동기식 타격 판정: 애니메이션 피크 타임 사용 (팔/다리 완전 신전 시점)\n const animationType = attackTechnique.animationType ?? AnimationType.JAB;\n const hitTiming = getAnimationHitTiming(animationType);\n const peakTime = hitTiming?.hitWindow.peakTime ?? 0.15; // Default to typical punch peak\n const animationContext = {\n animationType,\n currentTime: peakTime, // Use peak time for synchronous hit detection\n };\n\n // Use combat system for proper calculation with animation context\n const result = combatSystem.resolveAttack(\n validPlayers[0],\n validPlayers[1],\n attackTechnique,\n undefined,\n animationContext,\n );\n\n const effectType = result.hit\n ? result.isCritical\n ? HitEffectType.CRITICAL_HIT\n : HitEffectType.HIT\n : HitEffectType.MISS;\n\n addHitEffect(effectType, playerPositions[0], result.hit ? 1 : 0.5);\n\n if (result.hit) {\n // Play bone impact sound with body region and damage context\n const hitPosition = calculateHitPosition(playerPositions[1]);\n\n // Use bone impact audio instead of generic hit sound\n combatAudio?.playBoneImpactSound({\n damage: result.damage,\n remainingHealth: validPlayers[1].health - result.damage,\n vitalPoint: result.isCritical, // Critical hits are often vital points\n hitPosition,\n });\n\n // Combo tracking: reset combo if too much time passed\n const now = Date.now();\n const timeSinceLastHit = now - combatState.lastHitTime;\n const newCombo =\n timeSinceLastHit < 2000 ? combatState.comboCount + 1 : 1;\n combatActions.setComboCount(newCombo);\n combatActions.setLastHitTime(now);\n\n // Apply damage through combat system\n const { updatedAttacker, updatedDefender } =\n combatSystem.applyCombatResult(\n result,\n validPlayers[0],\n validPlayers[1],\n );\n\n onPlayerUpdate(0, updatedAttacker);\n onPlayerUpdate(1, updatedDefender);\n\n // Create injury for trauma visualization\n if (onInjuryCreated) {\n const injury = createInjuryFromDamage(\n result,\n attackTechnique,\n updatedDefender.health,\n 1, // Player 2 (enemy) was hit\n );\n onInjuryCreated(injury, 1);\n }\n\n // Apply knockback displacement (밀침 적용)\n if (result.knockback && config.onPlayerPositionUpdate) {\n const newDefenderPosition = applyKnockbackDisplacement(\n result,\n playerPositions[1],\n config.arenaBounds,\n );\n config.onPlayerPositionUpdate(1, newDefenderPosition);\n\n // Add combat message for significant knockback\n const knockbackDistance = Math.sqrt(\n result.knockback.displacement.x ** 2 +\n result.knockback.displacement.z ** 2,\n );\n if (knockbackDistance > 1.5) {\n const knockbackName = KnockbackPhysics.getKnockbackStateName(\n result.knockback.shouldFall,\n );\n addCombatMessage(knockbackName.korean, knockbackName.english);\n }\n\n // Set stunned state for knockback duration (non-interruptible)\n if (result.knockback.duration > 0) {\n // Clear any existing timeout for player 2\n if (player2KnockbackTimeoutRef.current) {\n clearTimeout(player2KnockbackTimeoutRef.current);\n }\n\n onPlayerUpdate(1, { isStunned: true });\n\n // Schedule recovery after knockback duration + recovery window\n player2KnockbackTimeoutRef.current = setTimeout(\n () => {\n onPlayerUpdate(1, { isStunned: false });\n player2KnockbackTimeoutRef.current = null;\n },\n (result.knockback.duration + result.knockback.recoveryWindow) *\n 1000,\n );\n }\n }\n\n // Check if defender should fall after taking damage\n if (config.playerAnimations?.player2) {\n const fallCheck = checkForFall(\n updatedDefender,\n combatSystem,\n undefined, // lastImpactAngle not tracked yet\n undefined, // attackAngle not tracked yet\n );\n\n if (\n fallCheck.shouldFall &&\n fallCheck.animationState &&\n fallCheck.fallType\n ) {\n config.playerAnimations.player2.transitionTo(\n fallCheck.animationState,\n );\n const fallName = getFallTypeName(fallCheck.fallType);\n addCombatMessage(`${fallName.korean}!`, `${fallName.english}!`);\n }\n }\n\n // Display technique name in combat log\n const techniqueNameKorean =\n attackTechnique.koreanName ?? attackTechnique.name.korean;\n const techniqueNameEnglish =\n attackTechnique.englishName ?? attackTechnique.name.english;\n\n if (result.isCritical) {\n addCombatMessage(\n `치명타! ${techniqueNameKorean}`,\n `Critical Hit! ${techniqueNameEnglish}`,\n );\n } else if (newCombo > 2) {\n addCombatMessage(\n `${newCombo} 연속! ${techniqueNameKorean}`,\n `${newCombo} Combo! ${techniqueNameEnglish}`,\n );\n } else {\n addCombatMessage(\n `${techniqueNameKorean} 성공!`,\n `${techniqueNameEnglish} Hit!`,\n );\n }\n } else {\n combatActions.resetCombo();\n const techniqueNameKorean =\n attackTechnique.koreanName ?? attackTechnique.name.korean;\n const techniqueNameEnglish =\n attackTechnique.englishName ?? attackTechnique.name.english;\n addCombatMessage(\n `${techniqueNameKorean} 빗나감`,\n `${techniqueNameEnglish} Missed`,\n );\n }\n\n setTimeout(() => combatActions.setExecutingTechnique(false), 500);\n },\n [\n validPlayers,\n playerPositions,\n combatState.isExecutingTechnique,\n combatState.roundStarted,\n combatState.roundEnded,\n combatState.comboCount,\n combatState.lastHitTime,\n combatActions,\n combatSystem,\n onPlayerUpdate,\n onInjuryCreated,\n addCombatMessage,\n addHitEffect,\n combatAudio,\n config,\n ],\n );\n\n // Player defend handler\n const handleDefend = useCallback(() => {\n if (!combatState.roundStarted || combatState.roundEnded) return;\n\n // Play block sound\n combatAudio?.playBlockSound(false);\n\n onPlayerUpdate(0, { isBlocking: true });\n addCombatMessage(\"방어 자세\", \"Defensive Stance\");\n addHitEffect(HitEffectType.BLOCK, playerPositions[0], 0.8);\n\n setTimeout(() => {\n onPlayerUpdate(0, { isBlocking: false });\n }, 1000);\n }, [\n combatState.roundStarted,\n combatState.roundEnded,\n onPlayerUpdate,\n addCombatMessage,\n addHitEffect,\n playerPositions,\n combatAudio,\n ]);\n\n // Player technique handler\n const handleTechniqueExecute = useCallback(() => {\n if (\n combatState.isExecutingTechnique ||\n !combatState.roundStarted ||\n combatState.roundEnded\n )\n return;\n if (validPlayers[0].ki < 10 || validPlayers[0].stamina < 15) {\n addCombatMessage(\"기력/체력 부족\", \"Insufficient Ki/Stamina\");\n return;\n }\n\n combatActions.setExecutingTechnique(true);\n\n // Play special technique sound\n combatAudio?.playSpecialTechniqueSound();\n\n addHitEffect(HitEffectType.CRITICAL_HIT, playerPositions[0], 1.5);\n\n // Screen shake effect for impact\n const shakeIntensity = 8;\n const shakeFrames = [\n { x: shakeIntensity, y: -shakeIntensity * 0.5 },\n { x: -shakeIntensity * 0.7, y: shakeIntensity * 0.8 },\n { x: shakeIntensity * 0.5, y: shakeIntensity * 0.3 },\n { x: -shakeIntensity * 0.3, y: -shakeIntensity * 0.6 },\n { x: 0, y: 0 },\n ];\n\n shakeFrames.forEach((shake, index) => {\n setTimeout(() => combatActions.setScreenShake(shake), index * 50);\n });\n\n const distance = Math.sqrt(\n Math.pow(playerPositions[0].x - playerPositions[1].x, 2) +\n Math.pow(playerPositions[0].y - playerPositions[1].y, 2),\n );\n\n if (distance < 150) {\n // Play bone impact sound for special technique hit\n const hitPosition = calculateHitPosition(playerPositions[1]);\n\n combatAudio?.playBoneImpactSound({\n damage: 25,\n remainingHealth: validPlayers[1].health - 25,\n vitalPoint: false,\n hitPosition,\n });\n\n onPlayerUpdate(1, {\n health: Math.max(0, validPlayers[1].health - 25),\n hitsTaken: validPlayers[1].hitsTaken + 1,\n });\n addCombatMessage(\"특수 기술 성공!\", \"Special Technique Hit!\");\n } else {\n addCombatMessage(\"기술 실패\", \"Technique Failed\");\n }\n\n // Consume resources\n onPlayerUpdate(0, {\n ki: Math.max(0, validPlayers[0].ki - 10),\n stamina: Math.max(0, validPlayers[0].stamina - 15),\n });\n\n setTimeout(() => combatActions.setExecutingTechnique(false), 800);\n }, [\n validPlayers,\n playerPositions,\n combatState.isExecutingTechnique,\n combatState.roundStarted,\n combatState.roundEnded,\n combatActions,\n onPlayerUpdate,\n addCombatMessage,\n addHitEffect,\n combatAudio,\n ]);\n\n // Player stance switch handler\n const handleStanceSwitch = useCallback(\n (stance: TrigramStance) => {\n if (!combatState.roundStarted || combatState.roundEnded) return;\n\n // Play stance change sound\n combatAudio?.playStanceChangeSound();\n\n onPlayerUpdate(0, { currentStance: stance });\n addCombatMessage(`자세 변경: ${stance}`, `Stance Change: ${stance}`);\n addHitEffect(HitEffectType.STATUS_EFFECT, playerPositions[0], 0.6);\n },\n [\n combatState.roundStarted,\n combatState.roundEnded,\n onPlayerUpdate,\n addCombatMessage,\n addHitEffect,\n playerPositions,\n combatAudio,\n ],\n );\n\n /**\n * Handle stance side switch (left/right)\n * @korean 자세측면전환처리\n */\n // Reuse StanceManager instance for stance side switches\n const stanceManagerRef = useRef<StanceManager>(new StanceManager());\n\n const handleStanceSideSwitch = useCallback(\n (playerIndex: 0 | 1) => {\n if (!combatState.roundStarted || combatState.roundEnded) return;\n\n const player = validPlayers[playerIndex];\n // Get current laterality from combat state\n const currentLaterality = combatState.playerLaterality[playerIndex];\n\n const result = stanceManagerRef.current.switchStanceSide(\n player,\n currentLaterality,\n );\n\n if (result.success && result.laterality) {\n // Update player state with new stamina\n onPlayerUpdate(playerIndex, result.updatedPlayer);\n\n // Update laterality in combat state via callback\n onLateralityUpdate?.(playerIndex, result.laterality);\n\n // Audio feedback\n combatAudio?.playStanceChangeSound?.();\n\n // Visual feedback\n const koreanText =\n result.laterality === \"left\" ? \"왼발서기\" : \"오른발서기\";\n const englishText =\n result.laterality === \"left\" ? \"Left Stance\" : \"Right Stance\";\n addCombatMessage(koreanText, englishText);\n\n // Visual effect\n addHitEffect(\n HitEffectType.STATUS_EFFECT,\n playerPositions[playerIndex],\n 0.5,\n );\n } else {\n // Feedback for failed switch\n if (result.message?.includes(\"stamina\")) {\n addCombatMessage(\"체력 부족\", \"Insufficient Stamina\");\n } else if (result.message?.includes(\"cooldown\")) {\n addCombatMessage(\"대기 중\", \"On Cooldown\");\n }\n }\n },\n [\n combatState.roundStarted,\n combatState.roundEnded,\n combatState.playerLaterality,\n validPlayers,\n onPlayerUpdate,\n onLateralityUpdate,\n combatAudio,\n addCombatMessage,\n addHitEffect,\n playerPositions,\n ],\n );\n\n /**\n * Helper function to create AI technique objects\n * Reduces code duplication between basic attacks and special techniques\n */\n const createAITechnique = useCallback(\n (type: \"basic\" | \"special\", aiPlayer: PlayerState) => {\n if (type === \"basic\") {\n return {\n id: \"ai_basic_attack\",\n name: {\n korean: \"AI 기본공격\",\n english: \"AI Basic Attack\",\n romanized: \"ai_gibon_gonggyeok\",\n },\n koreanName: \"AI 기본공격\",\n englishName: \"AI Basic Attack\",\n romanized: \"ai_gibon_gonggyeok\",\n description: { korean: \"AI 기본 공격\", english: \"AI basic attack\" },\n stance: aiPlayer.currentStance,\n type: \"attack\" as const,\n damageType: \"physical\" as const,\n damage: 15,\n kiCost: 5,\n staminaCost: 8,\n accuracy: 0.8,\n reachConfig: {\n bodyPart: \"arm\" as const,\n techniqueType: \"punch\" as const,\n baseExtension: 0.95,\n },\n executionTime: 400,\n recoveryTime: 300,\n critChance: 0.1,\n critMultiplier: 1.5,\n effects: [],\n animationType: AnimationType.JAB, // Default animation for basic attack\n };\n } else {\n return {\n id: \"ai_special_technique\",\n name: {\n korean: \"AI 특수기술\",\n english: \"AI Special Technique\",\n romanized: \"ai_teuksu_gisul\",\n },\n koreanName: \"AI 특수기술\",\n englishName: \"AI Special Technique\",\n romanized: \"ai_teuksu_gisul\",\n description: {\n korean: \"AI 특수 기술\",\n english: \"AI special technique\",\n },\n stance: aiPlayer.currentStance,\n type: \"technique\" as const,\n damageType: \"physical\" as const,\n damage: 25,\n kiCost: 10,\n staminaCost: 15,\n accuracy: 0.85,\n reachConfig: {\n bodyPart: \"leg\" as const,\n techniqueType: \"kick\" as const,\n baseExtension: 1.1,\n },\n executionTime: 600,\n recoveryTime: 800,\n critChance: 0.15,\n critMultiplier: 1.8,\n effects: [],\n animationType: AnimationType.SPINNING_HOOK, // Default animation for special technique\n };\n }\n },\n [],\n );\n\n /**\n * Helper function to determine hit effect type based on combat result\n * Reduces duplication between attack and technique handlers\n */\n const getHitEffectType = useCallback(\n (result: { hit: boolean; isCritical?: boolean }): HitEffectType => {\n if (!result.hit) return HitEffectType.MISS;\n return result.isCritical ? HitEffectType.CRITICAL_HIT : HitEffectType.HIT;\n },\n [],\n );\n\n /**\n * AI attack handler with technique and vital point targeting\n *\n * @param technique - Optional Korean martial arts technique to execute. If not provided, creates a basic attack.\n * @param targetVitalPoint - Optional vital point ID to target for increased damage effectiveness.\n */\n const handleAIAttack = useCallback(\n (technique?: KoreanTechnique, targetVitalPoint?: string) => {\n const aiPlayer = validPlayers[1];\n const targetPlayer = validPlayers[0];\n\n // Use provided technique or create basic attack technique\n const attackTechnique = technique ?? createAITechnique(\"basic\", aiPlayer);\n\n // Play attack sound based on technique damage/intensity (consistent with player)\n const damage = attackTechnique.damage ?? 10;\n const intensity: AttackIntensity =\n damage >= 40\n ? \"critical\"\n : damage >= 25\n ? \"heavy\"\n : damage >= 10\n ? \"medium\"\n : \"light\";\n combatAudio?.playAttackSound(intensity);\n\n // Calculate animation timing context for AI hit detection (same as player)\n // 동기식 타격 판정: AI도 피크 타임 사용\n const animationType = attackTechnique.animationType ?? AnimationType.JAB;\n const hitTiming = getAnimationHitTiming(animationType);\n const peakTime = hitTiming?.hitWindow.peakTime ?? 0.15;\n const animationContext = {\n animationType,\n currentTime: peakTime,\n };\n\n // Use combat system for proper calculation with vital point targeting and animation context\n const result = combatSystem.resolveAttack(\n aiPlayer,\n targetPlayer,\n attackTechnique,\n targetVitalPoint, // Pass vital point ID for targeting\n animationContext, // Pass animation context for distance/reach check\n );\n\n const effectType = getHitEffectType(result);\n addHitEffect(effectType, playerPositions[1], result.hit ? 1 : 0.5);\n\n if (result.hit) {\n // Play bone impact sound for AI hits on player\n const hitPosition = calculateHitPosition(playerPositions[0]);\n\n combatAudio?.playBoneImpactSound({\n damage: result.damage,\n remainingHealth: validPlayers[0].health - result.damage,\n vitalPoint: result.isCritical,\n hitPosition,\n });\n\n // Apply damage through combat system (deducts resources)\n const { updatedAttacker, updatedDefender } =\n combatSystem.applyCombatResult(result, aiPlayer, targetPlayer);\n\n onPlayerUpdate(1, updatedAttacker);\n onPlayerUpdate(0, updatedDefender);\n\n // Create injury for trauma visualization (AI hit player)\n if (onInjuryCreated) {\n const injury = createInjuryFromDamage(\n result,\n attackTechnique,\n updatedDefender.health,\n 0, // Player 1 (player) was hit by AI\n );\n onInjuryCreated(injury, 0);\n }\n\n // Apply knockback displacement for AI attacks (밀침 적용)\n if (result.knockback && config.onPlayerPositionUpdate) {\n const newDefenderPosition = applyKnockbackDisplacement(\n result,\n playerPositions[0],\n config.arenaBounds,\n );\n config.onPlayerPositionUpdate(0, newDefenderPosition);\n\n // Add combat message for significant knockback\n const knockbackDistance = Math.sqrt(\n result.knockback.displacement.x ** 2 +\n result.knockback.displacement.z ** 2,\n );\n if (knockbackDistance > 1.5) {\n const knockbackName = KnockbackPhysics.getKnockbackStateName(\n result.knockback.shouldFall,\n );\n addCombatMessage(\n `AI ${knockbackName.korean}`,\n `AI ${knockbackName.english}`,\n );\n }\n\n // Set stunned state for knockback duration (non-interruptible)\n if (result.knockback.duration > 0) {\n // Clear any existing timeout for player 1\n if (player1KnockbackTimeoutRef.current) {\n clearTimeout(player1KnockbackTimeoutRef.current);\n }\n\n onPlayerUpdate(0, { isStunned: true });\n\n // Schedule recovery after knockback duration + recovery window\n player1KnockbackTimeoutRef.current = setTimeout(\n () => {\n onPlayerUpdate(0, { isStunned: false });\n player1KnockbackTimeoutRef.current = null;\n },\n (result.knockback.duration + result.knockback.recoveryWindow) *\n 1000,\n );\n }\n }\n\n // Check if player should fall after taking damage from AI\n if (config.playerAnimations?.player1) {\n const fallCheck = checkForFall(\n updatedDefender,\n combatSystem,\n undefined, // lastImpactAngle not tracked yet\n undefined, // attackAngle not tracked yet\n );\n\n if (\n fallCheck.shouldFall &&\n fallCheck.animationState &&\n fallCheck.fallType\n ) {\n config.playerAnimations.player1.transitionTo(\n fallCheck.animationState,\n );\n const fallName = getFallTypeName(fallCheck.fallType);\n addCombatMessage(`${fallName.korean}!`, `${fallName.english}!`);\n }\n }\n\n // Enhanced combat message with vital point info\n if (result.vitalPointHit && targetVitalPoint) {\n const vitalPoint = getVitalPointById(targetVitalPoint);\n const vpName = vitalPoint\n ? vitalPoint.names.korean\n : targetVitalPoint;\n addCombatMessage(\n `AI 급소 타격! ${vpName}`,\n `AI Vital Point Hit! ${\n vitalPoint?.names.english ?? targetVitalPoint\n }`,\n );\n } else if (result.isCritical) {\n addCombatMessage(\"AI 치명타!\", \"AI Critical Hit!\");\n } else {\n addCombatMessage(\"AI 공격 성공!\", \"AI Attack Hit!\");\n }\n } else {\n // Consume resources on miss for consistency with technique behavior\n onPlayerUpdate(1, {\n ki: Math.max(0, aiPlayer.ki - attackTechnique.kiCost),\n stamina: Math.max(0, aiPlayer.stamina - attackTechnique.staminaCost),\n });\n addCombatMessage(\"AI 공격 빗나감\", \"AI Attack Missed\");\n }\n },\n [\n validPlayers,\n playerPositions,\n combatSystem,\n onPlayerUpdate,\n onInjuryCreated,\n addCombatMessage,\n addHitEffect,\n combatAudio,\n createAITechnique,\n getHitEffectType,\n config,\n ],\n );\n\n // AI defend handler\n const handleAIDefend = useCallback(() => {\n // Play block sound\n combatAudio?.playBlockSound(false);\n\n onPlayerUpdate(1, { isBlocking: true });\n addCombatMessage(\"AI 방어 자세\", \"AI Defensive Stance\");\n addHitEffect(HitEffectType.BLOCK, playerPositions[1], 0.8);\n\n setTimeout(() => {\n onPlayerUpdate(1, { isBlocking: false });\n }, 1000);\n }, [\n onPlayerUpdate,\n addCombatMessage,\n addHitEffect,\n playerPositions,\n combatAudio,\n ]);\n\n /**\n * AI technique handler with technique and vital point targeting\n *\n * @param technique - Optional special Korean martial arts technique to execute. If not provided, creates a special technique.\n * @param targetVitalPoint - Optional vital point ID to target for increased damage effectiveness.\n */\n const handleAITechnique = useCallback(\n (technique?: KoreanTechnique, targetVitalPoint?: string) => {\n const aiPlayer = validPlayers[1];\n const targetPlayer = validPlayers[0];\n\n // Use provided technique or create special technique\n const specialTechnique =\n technique ?? createAITechnique(\"special\", aiPlayer);\n\n // Check if AI has sufficient resources for the technique\n if (\n aiPlayer.ki < specialTechnique.kiCost ||\n aiPlayer.stamina < specialTechnique.staminaCost\n ) {\n handleAIAttack(undefined, targetVitalPoint); // Fallback to basic attack with same targeting\n return;\n }\n\n // Play special technique sound\n combatAudio?.playSpecialTechniqueSound();\n\n // Calculate animation timing context for AI technique hit detection\n // 동기식 타격 판정: AI 특수 기술도 피크 타임 사용\n const animationType =\n specialTechnique.animationType ?? AnimationType.SPINNING_HOOK;\n const hitTiming = getAnimationHitTiming(animationType);\n const peakTime = hitTiming?.hitWindow.peakTime ?? 0.25; // Special techniques often have longer peak times\n const animationContext = {\n animationType,\n currentTime: peakTime,\n };\n\n // Use combat system for proper calculation with vital point targeting and animation context\n const result = combatSystem.resolveAttack(\n aiPlayer,\n targetPlayer,\n specialTechnique,\n targetVitalPoint, // Pass vital point ID for targeting\n animationContext, // Pass animation context for distance/reach check\n );\n\n const effectType = result.hit\n ? HitEffectType.CRITICAL_HIT\n : HitEffectType.MISS;\n\n addHitEffect(effectType, playerPositions[1], result.hit ? 1.5 : 0.5);\n\n if (result.hit) {\n // Play bone impact sound for AI technique hits\n const hitPosition = calculateHitPosition(playerPositions[0]);\n\n combatAudio?.playBoneImpactSound({\n damage: result.damage,\n remainingHealth: validPlayers[0].health - result.damage,\n vitalPoint: result.isCritical === true || !!targetVitalPoint, // Special techniques often target vital points\n hitPosition,\n });\n\n // Apply damage through combat system (deducts resources)\n const { updatedAttacker, updatedDefender } =\n combatSystem.applyCombatResult(result, aiPlayer, targetPlayer);\n\n onPlayerUpdate(1, updatedAttacker);\n onPlayerUpdate(0, updatedDefender);\n\n // Create injury for trauma visualization (AI technique hit player)\n if (onInjuryCreated) {\n const injury = createInjuryFromDamage(\n result,\n specialTechnique,\n updatedDefender.health,\n 0, // Player 1 (player) was hit by AI technique\n );\n onInjuryCreated(injury, 0);\n }\n\n // Apply knockback displacement for AI special techniques (밀침 적용)\n if (result.knockback && config.onPlayerPositionUpdate) {\n const newDefenderPosition = applyKnockbackDisplacement(\n result,\n playerPositions[0],\n config.arenaBounds,\n );\n config.onPlayerPositionUpdate(0, newDefenderPosition);\n\n // Add combat message for significant knockback\n const knockbackDistance = Math.sqrt(\n result.knockback.displacement.x ** 2 +\n result.knockback.displacement.z ** 2,\n );\n if (knockbackDistance > 1.5) {\n const knockbackName = KnockbackPhysics.getKnockbackStateName(\n result.knockback.shouldFall,\n );\n addCombatMessage(\n `AI 특수 ${knockbackName.korean}`,\n `AI Special ${knockbackName.english}`,\n );\n }\n\n // Set stunned state for knockback duration (non-interruptible)\n if (result.knockback.duration > 0) {\n // Clear any existing timeout for player 1\n if (player1KnockbackTimeoutRef.current) {\n clearTimeout(player1KnockbackTimeoutRef.current);\n }\n\n onPlayerUpdate(0, { isStunned: true });\n\n // Schedule recovery after knockback duration + recovery window\n player1KnockbackTimeoutRef.current = setTimeout(\n () => {\n onPlayerUpdate(0, { isStunned: false });\n player1KnockbackTimeoutRef.current = null;\n },\n (result.knockback.duration + result.knockback.recoveryWindow) *\n 1000,\n );\n }\n }\n\n // Check if player should fall after taking damage from AI technique\n if (config.playerAnimations?.player1) {\n const fallCheck = checkForFall(\n updatedDefender,\n combatSystem,\n undefined, // lastImpactAngle not tracked yet\n undefined, // attackAngle not tracked yet\n );\n\n if (\n fallCheck.shouldFall &&\n fallCheck.animationState &&\n fallCheck.fallType\n ) {\n config.playerAnimations.player1.transitionTo(\n fallCheck.animationState,\n );\n const fallName = getFallTypeName(fallCheck.fallType);\n addCombatMessage(`${fallName.korean}!`, `${fallName.english}!`);\n }\n }\n\n // Enhanced combat message with vital point info\n if (result.vitalPointHit && targetVitalPoint) {\n const vitalPoint = getVitalPointById(targetVitalPoint);\n const vpName = vitalPoint\n ? vitalPoint.names.korean\n : targetVitalPoint;\n addCombatMessage(\n `AI 특수 급소 기술! ${vpName}`,\n `AI Special Vital Point Technique! ${\n vitalPoint?.names.english ?? targetVitalPoint\n }`,\n );\n } else {\n addCombatMessage(\"AI 특수 기술!\", \"AI Special Technique!\");\n }\n } else {\n // Consume resources on miss (technique was attempted)\n onPlayerUpdate(1, {\n ki: Math.max(0, aiPlayer.ki - specialTechnique.kiCost),\n stamina: Math.max(0, aiPlayer.stamina - specialTechnique.staminaCost),\n });\n addCombatMessage(\"AI 기술 빗나감\", \"AI Technique Missed\");\n }\n },\n [\n validPlayers,\n playerPositions,\n combatSystem,\n onPlayerUpdate,\n onInjuryCreated,\n addCombatMessage,\n addHitEffect,\n handleAIAttack,\n combatAudio,\n createAITechnique,\n config,\n ],\n );\n\n // AI movement handler with injury-based movement penalties\n // **UPDATED**: Now scale-aware for consistent movement and distance calculations\n // **FIX**: Positions are in METERS, not pixels - use meters-based speed\n const moveAIPlayer = useCallback(\n (targetPos: Position) => {\n const currentPos = playerPositions[1];\n const aiPlayer = validPlayers[1];\n\n // Movement speed calibrated for physics-first system (all in METERS)\n // Combat closing speed: ~2.5 m/s (fast tactical approach, not slow walking)\n // Real fights are over in 4-5 seconds - AI must close distance quickly\n // AI decision loop frequency (defined in useAICombat.ts)\n const AI_DECISION_FREQUENCY_HZ = 20; // 20 calls/second (50ms interval)\n // Calculation: 2.5 m/s / 20 calls/s = 0.125 meters per call\n const baseSpeed = 2.5 / AI_DECISION_FREQUENCY_HZ; // meters per call (0.125m per call)\n\n // Calculate movement direction vector (in meters)\n const dx = targetPos.x - currentPos.x;\n const dy = targetPos.y - currentPos.y;\n const distance = Math.sqrt(dx * dx + dy * dy);\n\n // Apply movement penalties from leg injuries if body part health exists\n let finalSpeed = baseSpeed;\n if (aiPlayer.bodyPartHealth && aiPlayer.bodyPartMaxHealth) {\n // Normalize movement direction\n const movementDirection = {\n x: distance > 0 ? dx / distance : 0,\n y: distance > 0 ? dy / distance : 0,\n };\n\n // Calculate modified speed with all penalties applied\n finalSpeed = movementPenaltySystem.calculateModifiedSpeed(\n baseSpeed,\n aiPlayer.bodyPartHealth,\n aiPlayer.bodyPartMaxHealth,\n movementDirection,\n );\n }\n\n // Physics-first: positions are in METERS, so distance is in meters\n // Stop moving when within 0.05 meters (5cm) of target - close enough for melee range\n const MIN_MOVEMENT_THRESHOLD_METERS = 0.05;\n\n if (distance > MIN_MOVEMENT_THRESHOLD_METERS) {\n const newPos = {\n x: currentPos.x + (dx / distance) * finalSpeed,\n y: currentPos.y + (dy / distance) * finalSpeed,\n };\n\n // Keep AI within arena bounds (positions in meters, centered at origin)\n const halfWidth = arenaBounds.worldWidthMeters / 2;\n const halfDepth = arenaBounds.worldDepthMeters / 2;\n newPos.x = Math.max(-halfWidth, Math.min(halfWidth, newPos.x));\n newPos.y = Math.max(-halfDepth, Math.min(halfDepth, newPos.y));\n\n // Update position through parent - this should trigger playerPositions state update in parent\n onPlayerUpdate(1, { position: newPos });\n }\n },\n [playerPositions, validPlayers, arenaBounds, onPlayerUpdate],\n );\n\n return {\n handleAttack,\n handleDefend,\n handleTechniqueExecute,\n handleStanceSwitch,\n handleStanceSideSwitch,\n handleAIAttack,\n handleAIDefend,\n handleAITechnique,\n moveAIPlayer,\n };\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"useCombatAudio.js","names":[],"sources":["../../../../../src/components/screens/combat/hooks/useCombatAudio.ts"],"sourcesContent":["/**\n * Combat Audio Hook for Black Trigram\n * Provides comprehensive audio feedback for combat actions including\n * bone impact sounds, fracture audio, and body-region-specific hit sounds\n */\n\nimport { useCallback, useEffect, useRef } from \"react\";\nimport { useAudio } from \"../../../../audio/AudioProvider\";\nimport {\n calculateImpactIntensity,\n detectAudioBodyRegion,\n getBoneImpactSoundId,\n getImpactVolumeMultiplier,\n} from \"../../../../audio/BoneImpactAudioMap\";\nimport { AudioBodyRegion, ImpactIntensity } from \"../../../../audio/types\";\n\n/**\n * Attack intensity levels for sound selection\n */\nexport type AttackIntensity = \"light\" | \"medium\" | \"heavy\" | \"critical\";\n\n/**\n * Maximum number of simultaneous sounds to prevent audio chaos\n */\nconst MAX_SIMULTANEOUS_SOUNDS = 5;\n\n/**\n * Combat audio hook for playing attack, hit, block, dodge, and stance sounds\n * @returns Object with methods for playing various combat sounds\n */\nexport const useCombatAudio = () => {\n const audio = useAudio();\n const lastPlayTime = useRef<Record<string, number>>({});\n const activeSounds = useRef(new Set<string>());\n const timeoutIds = useRef<Set<NodeJS.Timeout>>(new Set());\n\n // Cleanup all timeouts on unmount\n useEffect(() => {\n // Copy ref value to local variable for cleanup\n const timeoutIdsSet = timeoutIds.current;\n return () => {\n timeoutIdsSet.forEach(clearTimeout);\n timeoutIdsSet.clear();\n };\n }, []);\n\n /**\n * Check if we can play a sound (rate limiting and simultaneous sound check)\n * @param soundType - Type of sound to check\n * @param minInterval - Minimum interval between plays in milliseconds\n * @returns True if sound can be played\n */\n const canPlaySound = useCallback(\n (soundType: string, minInterval = 50): boolean => {\n const now = Date.now();\n const lastTime = lastPlayTime.current[soundType] ?? 0;\n\n // Rate limiting check\n if (now - lastTime < minInterval) {\n return false;\n }\n\n // Check simultaneous sounds limit\n if (activeSounds.current.size >= MAX_SIMULTANEOUS_SOUNDS) {\n return false;\n }\n\n return true;\n },\n [],\n );\n\n /**\n * Register a sound as active and auto-remove after duration\n * @param soundId - ID of the sound to register\n * @param duration - Duration in milliseconds (default 500ms)\n */\n const registerActiveSound = useCallback((soundId: string, duration = 500) => {\n activeSounds.current.add(soundId);\n const timeoutId = setTimeout(() => {\n activeSounds.current.delete(soundId);\n timeoutIds.current.delete(timeoutId);\n }, duration);\n timeoutIds.current.add(timeoutId);\n }, []);\n\n /**\n * Get a random variant from a pool\n * @param base - Base sound ID\n * @param count - Number of variants\n * @returns Random variant ID\n */\n const getRandomVariant = useCallback(\n (base: string, count: number): string => {\n const variant = Math.floor(Math.random() * count) + 1;\n return `${base}_${variant}`;\n },\n [],\n );\n\n /**\n * Play attack sound based on intensity\n * @param intensity - Attack intensity (light, medium, heavy, critical)\n */\n const playAttackSound = useCallback(\n async (intensity: AttackIntensity = \"light\") => {\n const soundType = `attack_${intensity}`;\n\n if (!canPlaySound(soundType)) {\n return;\n }\n\n let soundId: string;\n\n switch (intensity) {\n case \"light\":\n // 8 variations of light punch sounds\n soundId = getRandomVariant(\"attack_punch_light\", 8);\n break;\n case \"medium\":\n // 4 variations of medium punch sounds\n soundId = getRandomVariant(\"attack_punch_medium\", 4);\n break;\n case \"heavy\":\n soundId = \"attack_heavy\";\n break;\n case \"critical\":\n // 4 variations of critical attack sounds\n soundId = getRandomVariant(\"attack_critical\", 4);\n break;\n default:\n console.warn(\n `Unknown attack intensity: ${intensity}, defaulting to light`,\n );\n soundId = getRandomVariant(\"attack_punch_light\", 8);\n }\n\n try {\n await audio.playSFX(soundId);\n lastPlayTime.current[soundType] = Date.now();\n registerActiveSound(soundId, 400);\n } catch (error) {\n console.warn(`Failed to play attack sound: ${soundId}`, error);\n }\n },\n [audio, canPlaySound, getRandomVariant, registerActiveSound],\n );\n\n /**\n * Play hit reaction sound based on damage amount\n * @param damage - Damage amount to determine hit intensity\n */\n const playHitSound = useCallback(\n async (damage: number) => {\n const soundType = \"hit\";\n\n if (!canPlaySound(soundType, 100)) {\n return;\n }\n\n let soundId: string;\n\n // Determine hit intensity based on damage\n if (damage >= 40) {\n // Critical hit (4 variations)\n soundId = getRandomVariant(\"hit_critical\", 4);\n } else if (damage >= 25) {\n // Heavy hit (4 variations)\n soundId = getRandomVariant(\"hit_heavy\", 4);\n } else if (damage >= 10) {\n // Medium hit (4 variations)\n soundId = getRandomVariant(\"hit_medium\", 4);\n } else {\n // Light hit (4 variations)\n soundId = getRandomVariant(\"hit_light\", 4);\n }\n\n try {\n await audio.playSFX(soundId);\n lastPlayTime.current[soundType] = Date.now();\n registerActiveSound(soundId, 500);\n } catch (error) {\n console.warn(`Failed to play hit sound: ${soundId}`, error);\n }\n },\n [audio, canPlaySound, getRandomVariant, registerActiveSound],\n );\n\n /**\n * Play block/parry sound\n * @param guardBroken - Whether the guard was broken or successfully blocked\n */\n const playBlockSound = useCallback(\n async (guardBroken: boolean = false) => {\n const soundType = \"block\";\n\n if (!canPlaySound(soundType, 150)) {\n return;\n }\n\n let soundId: string;\n\n if (guardBroken) {\n // 4 variations of guard break sounds\n soundId = getRandomVariant(\"block_break\", 4);\n } else {\n // 4 variations of successful block sounds\n soundId = getRandomVariant(\"block_success\", 4);\n }\n\n try {\n await audio.playSFX(soundId);\n lastPlayTime.current[soundType] = Date.now();\n registerActiveSound(soundId, 400);\n } catch (error) {\n console.warn(`Failed to play block sound: ${soundId}`, error);\n }\n },\n [audio, canPlaySound, getRandomVariant, registerActiveSound],\n );\n\n /**\n * Play dodge sound\n */\n const playDodgeSound = useCallback(async () => {\n const soundType = \"dodge\";\n\n if (!canPlaySound(soundType, 200)) {\n return;\n }\n\n // 8 variations of dodge sounds\n const soundId = getRandomVariant(\"dodge\", 8);\n\n try {\n await audio.playSFX(soundId);\n lastPlayTime.current[soundType] = Date.now();\n registerActiveSound(soundId, 300);\n } catch (error) {\n console.warn(`Failed to play dodge sound: ${soundId}`, error);\n }\n }, [audio, canPlaySound, getRandomVariant, registerActiveSound]);\n\n /**\n * Play stance change sound\n */\n const playStanceChangeSound = useCallback(async () => {\n const soundType = \"stance\";\n\n if (!canPlaySound(soundType, 250)) {\n return;\n }\n\n // 4 variations of stance change sounds\n const soundId = getRandomVariant(\"stance_change\", 4);\n\n try {\n await audio.playSFX(soundId);\n lastPlayTime.current[soundType] = Date.now();\n registerActiveSound(soundId, 400);\n } catch (error) {\n console.warn(`Failed to play stance change sound: ${soundId}`, error);\n }\n }, [audio, canPlaySound, getRandomVariant, registerActiveSound]);\n\n /**\n * Play special technique sound (e.g., Geon special)\n */\n const playSpecialTechniqueSound = useCallback(async () => {\n const soundType = \"special\";\n\n if (!canPlaySound(soundType, 300)) {\n return;\n }\n\n // 4 variations of special Geon technique sounds\n const soundId = getRandomVariant(\"attack_special_geon\", 4);\n\n try {\n await audio.playSFX(soundId, 0.8);\n lastPlayTime.current[soundType] = Date.now();\n registerActiveSound(soundId, 600);\n } catch (error) {\n console.warn(`Failed to play special technique sound: ${soundId}`, error);\n }\n }, [audio, canPlaySound, getRandomVariant, registerActiveSound]);\n\n /**\n * Play combat theme music with fade-in\n * @param fadeInDuration - Fade-in duration in milliseconds\n */\n const playCombatMusic = useCallback(\n async (fadeInDuration: number = 2000) => {\n try {\n await audio.fadeIn(\"combat_theme\", fadeInDuration);\n } catch (error) {\n console.warn(\"Failed to play combat music\", error);\n }\n },\n [audio],\n );\n\n /**\n * Play archetype-specific music theme\n * @param archetype - Player archetype (musa, amsalja, hacker, jeongbo_yowon, jojik_pokryeokbae)\n * @param fadeInDuration - Fade-in duration in milliseconds\n */\n const playArchetypeMusic = useCallback(\n async (archetype: string, fadeInDuration: number = 2000) => {\n const archetypeMap: Record<string, string> = {\n musa: \"musa_warrior_theme\",\n amsalja: \"amsalja_shadow_theme\",\n hacker: \"hacker_cyber_theme\",\n jeongbo_yowon: \"jeongbo_intel_theme\",\n jojik_pokryeokbae: \"jojik_street_theme\",\n };\n\n const musicId = archetypeMap[archetype.toLowerCase()];\n\n if (!musicId) {\n console.warn(`Unknown archetype: ${archetype}, using combat theme`);\n await playCombatMusic(fadeInDuration);\n return;\n }\n\n try {\n await audio.fadeIn(musicId, fadeInDuration);\n } catch (error) {\n console.warn(`Failed to play archetype music: ${musicId}`, error);\n // Fallback to combat theme\n await playCombatMusic(fadeInDuration);\n }\n },\n [audio, playCombatMusic],\n );\n\n /**\n * Stop combat music with fade-out\n * @param fadeOutDuration - Fade-out duration in milliseconds\n */\n const stopCombatMusic = useCallback(\n async (fadeOutDuration: number = 2000) => {\n try {\n await audio.fadeOut(fadeOutDuration);\n } catch (error) {\n console.warn(\"Failed to stop combat music\", error);\n }\n },\n [audio],\n );\n\n /**\n * Get number of currently active sounds\n * @returns Number of active sounds\n */\n const getActiveSoundCount = useCallback((): number => {\n return activeSounds.current.size;\n }, []);\n\n /**\n * Play bone impact sound with body region and intensity awareness\n * Implements realistic bone/flesh audio with fracture detection\n *\n * @param options - Bone impact event parameters\n * @param options.region - Body region struck (head, torso, arms, legs, soft_tissue)\n * @param options.intensity - Impact intensity (auto-calculated if omitted)\n * @param options.damage - Damage amount (for intensity calculation)\n * @param options.remainingHealth - Target's remaining health (for fracture detection)\n * @param options.vitalPoint - Whether strike hit a vital point\n * @param options.hitPosition - 3D position of strike (for auto region detection)\n *\n * @example\n * // Explicit region and intensity\n * playBoneImpactSound({ region: 'head', intensity: 'heavy' });\n *\n * @example\n * // Auto-calculate intensity from damage and health\n * playBoneImpactSound({\n * region: 'torso',\n * damage: 35,\n * remainingHealth: 25,\n * vitalPoint: false\n * });\n *\n * @example\n * // Auto-detect region from 3D hit position\n * playBoneImpactSound({\n * damage: 40,\n * remainingHealth: 60,\n * hitPosition: { x: 0.1, y: 1.8, z: 0 }\n * });\n */\n const playBoneImpactSound = useCallback(\n async (options: {\n region?: AudioBodyRegion;\n intensity?: ImpactIntensity;\n damage?: number;\n remainingHealth?: number;\n vitalPoint?: boolean;\n hitPosition?: { x: number; y: number; z?: number };\n }) => {\n const soundType = \"bone_impact\";\n\n if (!canPlaySound(soundType, 100)) {\n return;\n }\n\n // Auto-detect region from hit position if not provided\n let region = options.region;\n if (!region && options.hitPosition) {\n region = detectAudioBodyRegion(options.hitPosition);\n }\n\n // Default to torso if region still undefined\n region ??= \"torso\";\n\n // Auto-calculate intensity if not provided\n let intensity = options.intensity;\n if (!intensity && options.damage !== undefined) {\n intensity = calculateImpactIntensity(\n options.damage,\n options.remainingHealth,\n options.vitalPoint,\n );\n }\n\n // Default to medium intensity if still undefined\n intensity ??= \"medium\";\n\n // Get appropriate sound ID with random variant\n const soundId = getBoneImpactSoundId(region, intensity, true);\n\n // Get volume multiplier based on intensity\n const volumeMultiplier = getImpactVolumeMultiplier(intensity);\n const finalVolume = Math.min(1.0, 0.8 * volumeMultiplier);\n\n try {\n await audio.playSFX(soundId, finalVolume);\n lastPlayTime.current[soundType] = Date.now();\n\n // Longer active duration for fracture sounds (more impactful)\n const duration = intensity === \"fracture\" ? 800 : 500;\n registerActiveSound(soundId, duration);\n } catch (error) {\n console.warn(\n `Failed to play bone impact sound: ${soundId} (region: ${region}, intensity: ${intensity})`,\n error,\n );\n }\n },\n [audio, canPlaySound, registerActiveSound],\n );\n\n return {\n playAttackSound,\n playHitSound,\n playBlockSound,\n playDodgeSound,\n playStanceChangeSound,\n playSpecialTechniqueSound,\n playCombatMusic,\n playArchetypeMusic,\n stopCombatMusic,\n getActiveSoundCount,\n playBoneImpactSound, // NEW: Body-region-specific bone/flesh impact sounds\n };\n};\n\nexport default useCombatAudio;\n"],"mappings":"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"}
1
+ {"version":3,"file":"useCombatAudio.js","names":[],"sources":["../../../../../src/components/screens/combat/hooks/useCombatAudio.ts"],"sourcesContent":["/**\n * Combat Audio Hook for Black Trigram\n * Provides comprehensive audio feedback for combat actions including\n * bone impact sounds, fracture audio, and body-region-specific hit sounds\n */\n\nimport { useCallback, useEffect, useRef } from \"react\";\nimport { useAudio } from \"../../../../audio/AudioProvider\";\nimport {\n calculateImpactIntensity,\n detectAudioBodyRegion,\n getBoneImpactSoundId,\n getImpactVolumeMultiplier,\n} from \"../../../../audio/BoneImpactAudioMap\";\nimport { AudioBodyRegion, ImpactIntensity } from \"../../../../audio/types\";\n\n/**\n * Attack intensity levels for sound selection\n */\nexport type AttackIntensity = \"light\" | \"medium\" | \"heavy\" | \"critical\";\n\n/**\n * Maximum number of simultaneous sounds to prevent audio chaos\n */\nconst MAX_SIMULTANEOUS_SOUNDS = 5;\n\n/**\n * Combat audio hook for playing attack, hit, block, dodge, and stance sounds\n * @returns Object with methods for playing various combat sounds\n */\nexport const useCombatAudio = () => {\n const audio = useAudio();\n const lastPlayTime = useRef<Record<string, number>>({});\n const activeSounds = useRef(new Set<string>());\n const timeoutIds = useRef<Set<ReturnType<typeof setTimeout>>>(new Set());\n\n // Cleanup all timeouts on unmount\n useEffect(() => {\n // Copy ref value to local variable for cleanup\n const timeoutIdsSet = timeoutIds.current;\n return () => {\n timeoutIdsSet.forEach(clearTimeout);\n timeoutIdsSet.clear();\n };\n }, []);\n\n /**\n * Check if we can play a sound (rate limiting and simultaneous sound check)\n * @param soundType - Type of sound to check\n * @param minInterval - Minimum interval between plays in milliseconds\n * @returns True if sound can be played\n */\n const canPlaySound = useCallback(\n (soundType: string, minInterval = 50): boolean => {\n const now = Date.now();\n const lastTime = lastPlayTime.current[soundType] ?? 0;\n\n // Rate limiting check\n if (now - lastTime < minInterval) {\n return false;\n }\n\n // Check simultaneous sounds limit\n if (activeSounds.current.size >= MAX_SIMULTANEOUS_SOUNDS) {\n return false;\n }\n\n return true;\n },\n [],\n );\n\n /**\n * Register a sound as active and auto-remove after duration\n * @param soundId - ID of the sound to register\n * @param duration - Duration in milliseconds (default 500ms)\n */\n const registerActiveSound = useCallback((soundId: string, duration = 500) => {\n activeSounds.current.add(soundId);\n const timeoutId = setTimeout(() => {\n activeSounds.current.delete(soundId);\n timeoutIds.current.delete(timeoutId);\n }, duration);\n timeoutIds.current.add(timeoutId);\n }, []);\n\n /**\n * Get a random variant from a pool\n * @param base - Base sound ID\n * @param count - Number of variants\n * @returns Random variant ID\n */\n const getRandomVariant = useCallback(\n (base: string, count: number): string => {\n const variant = Math.floor(Math.random() * count) + 1;\n return `${base}_${variant}`;\n },\n [],\n );\n\n /**\n * Play attack sound based on intensity\n * @param intensity - Attack intensity (light, medium, heavy, critical)\n */\n const playAttackSound = useCallback(\n async (intensity: AttackIntensity = \"light\") => {\n const soundType = `attack_${intensity}`;\n\n if (!canPlaySound(soundType)) {\n return;\n }\n\n let soundId: string;\n\n switch (intensity) {\n case \"light\":\n // 8 variations of light punch sounds\n soundId = getRandomVariant(\"attack_punch_light\", 8);\n break;\n case \"medium\":\n // 4 variations of medium punch sounds\n soundId = getRandomVariant(\"attack_punch_medium\", 4);\n break;\n case \"heavy\":\n soundId = \"attack_heavy\";\n break;\n case \"critical\":\n // 4 variations of critical attack sounds\n soundId = getRandomVariant(\"attack_critical\", 4);\n break;\n default:\n console.warn(\n `Unknown attack intensity: ${intensity}, defaulting to light`,\n );\n soundId = getRandomVariant(\"attack_punch_light\", 8);\n }\n\n try {\n await audio.playSFX(soundId);\n lastPlayTime.current[soundType] = Date.now();\n registerActiveSound(soundId, 400);\n } catch (error) {\n console.warn(`Failed to play attack sound: ${soundId}`, error);\n }\n },\n [audio, canPlaySound, getRandomVariant, registerActiveSound],\n );\n\n /**\n * Play hit reaction sound based on damage amount\n * @param damage - Damage amount to determine hit intensity\n */\n const playHitSound = useCallback(\n async (damage: number) => {\n const soundType = \"hit\";\n\n if (!canPlaySound(soundType, 100)) {\n return;\n }\n\n let soundId: string;\n\n // Determine hit intensity based on damage\n if (damage >= 40) {\n // Critical hit (4 variations)\n soundId = getRandomVariant(\"hit_critical\", 4);\n } else if (damage >= 25) {\n // Heavy hit (4 variations)\n soundId = getRandomVariant(\"hit_heavy\", 4);\n } else if (damage >= 10) {\n // Medium hit (4 variations)\n soundId = getRandomVariant(\"hit_medium\", 4);\n } else {\n // Light hit (4 variations)\n soundId = getRandomVariant(\"hit_light\", 4);\n }\n\n try {\n await audio.playSFX(soundId);\n lastPlayTime.current[soundType] = Date.now();\n registerActiveSound(soundId, 500);\n } catch (error) {\n console.warn(`Failed to play hit sound: ${soundId}`, error);\n }\n },\n [audio, canPlaySound, getRandomVariant, registerActiveSound],\n );\n\n /**\n * Play block/parry sound\n * @param guardBroken - Whether the guard was broken or successfully blocked\n */\n const playBlockSound = useCallback(\n async (guardBroken: boolean = false) => {\n const soundType = \"block\";\n\n if (!canPlaySound(soundType, 150)) {\n return;\n }\n\n let soundId: string;\n\n if (guardBroken) {\n // 4 variations of guard break sounds\n soundId = getRandomVariant(\"block_break\", 4);\n } else {\n // 4 variations of successful block sounds\n soundId = getRandomVariant(\"block_success\", 4);\n }\n\n try {\n await audio.playSFX(soundId);\n lastPlayTime.current[soundType] = Date.now();\n registerActiveSound(soundId, 400);\n } catch (error) {\n console.warn(`Failed to play block sound: ${soundId}`, error);\n }\n },\n [audio, canPlaySound, getRandomVariant, registerActiveSound],\n );\n\n /**\n * Play dodge sound\n */\n const playDodgeSound = useCallback(async () => {\n const soundType = \"dodge\";\n\n if (!canPlaySound(soundType, 200)) {\n return;\n }\n\n // 8 variations of dodge sounds\n const soundId = getRandomVariant(\"dodge\", 8);\n\n try {\n await audio.playSFX(soundId);\n lastPlayTime.current[soundType] = Date.now();\n registerActiveSound(soundId, 300);\n } catch (error) {\n console.warn(`Failed to play dodge sound: ${soundId}`, error);\n }\n }, [audio, canPlaySound, getRandomVariant, registerActiveSound]);\n\n /**\n * Play stance change sound\n */\n const playStanceChangeSound = useCallback(async () => {\n const soundType = \"stance\";\n\n if (!canPlaySound(soundType, 250)) {\n return;\n }\n\n // 4 variations of stance change sounds\n const soundId = getRandomVariant(\"stance_change\", 4);\n\n try {\n await audio.playSFX(soundId);\n lastPlayTime.current[soundType] = Date.now();\n registerActiveSound(soundId, 400);\n } catch (error) {\n console.warn(`Failed to play stance change sound: ${soundId}`, error);\n }\n }, [audio, canPlaySound, getRandomVariant, registerActiveSound]);\n\n /**\n * Play special technique sound (e.g., Geon special)\n */\n const playSpecialTechniqueSound = useCallback(async () => {\n const soundType = \"special\";\n\n if (!canPlaySound(soundType, 300)) {\n return;\n }\n\n // 4 variations of special Geon technique sounds\n const soundId = getRandomVariant(\"attack_special_geon\", 4);\n\n try {\n await audio.playSFX(soundId, 0.8);\n lastPlayTime.current[soundType] = Date.now();\n registerActiveSound(soundId, 600);\n } catch (error) {\n console.warn(`Failed to play special technique sound: ${soundId}`, error);\n }\n }, [audio, canPlaySound, getRandomVariant, registerActiveSound]);\n\n /**\n * Play combat theme music with fade-in\n * @param fadeInDuration - Fade-in duration in milliseconds\n */\n const playCombatMusic = useCallback(\n async (fadeInDuration: number = 2000) => {\n try {\n await audio.fadeIn(\"combat_theme\", fadeInDuration);\n } catch (error) {\n console.warn(\"Failed to play combat music\", error);\n }\n },\n [audio],\n );\n\n /**\n * Play archetype-specific music theme\n * @param archetype - Player archetype (musa, amsalja, hacker, jeongbo_yowon, jojik_pokryeokbae)\n * @param fadeInDuration - Fade-in duration in milliseconds\n */\n const playArchetypeMusic = useCallback(\n async (archetype: string, fadeInDuration: number = 2000) => {\n const archetypeMap: Record<string, string> = {\n musa: \"musa_warrior_theme\",\n amsalja: \"amsalja_shadow_theme\",\n hacker: \"hacker_cyber_theme\",\n jeongbo_yowon: \"jeongbo_intel_theme\",\n jojik_pokryeokbae: \"jojik_street_theme\",\n };\n\n const musicId = archetypeMap[archetype.toLowerCase()];\n\n if (!musicId) {\n console.warn(`Unknown archetype: ${archetype}, using combat theme`);\n await playCombatMusic(fadeInDuration);\n return;\n }\n\n try {\n await audio.fadeIn(musicId, fadeInDuration);\n } catch (error) {\n console.warn(`Failed to play archetype music: ${musicId}`, error);\n // Fallback to combat theme\n await playCombatMusic(fadeInDuration);\n }\n },\n [audio, playCombatMusic],\n );\n\n /**\n * Stop combat music with fade-out\n * @param fadeOutDuration - Fade-out duration in milliseconds\n */\n const stopCombatMusic = useCallback(\n async (fadeOutDuration: number = 2000) => {\n try {\n await audio.fadeOut(fadeOutDuration);\n } catch (error) {\n console.warn(\"Failed to stop combat music\", error);\n }\n },\n [audio],\n );\n\n /**\n * Get number of currently active sounds\n * @returns Number of active sounds\n */\n const getActiveSoundCount = useCallback((): number => {\n return activeSounds.current.size;\n }, []);\n\n /**\n * Play bone impact sound with body region and intensity awareness\n * Implements realistic bone/flesh audio with fracture detection\n *\n * @param options - Bone impact event parameters\n * @param options.region - Body region struck (head, torso, arms, legs, soft_tissue)\n * @param options.intensity - Impact intensity (auto-calculated if omitted)\n * @param options.damage - Damage amount (for intensity calculation)\n * @param options.remainingHealth - Target's remaining health (for fracture detection)\n * @param options.vitalPoint - Whether strike hit a vital point\n * @param options.hitPosition - 3D position of strike (for auto region detection)\n *\n * @example\n * // Explicit region and intensity\n * playBoneImpactSound({ region: 'head', intensity: 'heavy' });\n *\n * @example\n * // Auto-calculate intensity from damage and health\n * playBoneImpactSound({\n * region: 'torso',\n * damage: 35,\n * remainingHealth: 25,\n * vitalPoint: false\n * });\n *\n * @example\n * // Auto-detect region from 3D hit position\n * playBoneImpactSound({\n * damage: 40,\n * remainingHealth: 60,\n * hitPosition: { x: 0.1, y: 1.8, z: 0 }\n * });\n */\n const playBoneImpactSound = useCallback(\n async (options: {\n region?: AudioBodyRegion;\n intensity?: ImpactIntensity;\n damage?: number;\n remainingHealth?: number;\n vitalPoint?: boolean;\n hitPosition?: { x: number; y: number; z?: number };\n }) => {\n const soundType = \"bone_impact\";\n\n if (!canPlaySound(soundType, 100)) {\n return;\n }\n\n // Auto-detect region from hit position if not provided\n let region = options.region;\n if (!region && options.hitPosition) {\n region = detectAudioBodyRegion(options.hitPosition);\n }\n\n // Default to torso if region still undefined\n region ??= \"torso\";\n\n // Auto-calculate intensity if not provided\n let intensity = options.intensity;\n if (!intensity && options.damage !== undefined) {\n intensity = calculateImpactIntensity(\n options.damage,\n options.remainingHealth,\n options.vitalPoint,\n );\n }\n\n // Default to medium intensity if still undefined\n intensity ??= \"medium\";\n\n // Get appropriate sound ID with random variant\n const soundId = getBoneImpactSoundId(region, intensity, true);\n\n // Get volume multiplier based on intensity\n const volumeMultiplier = getImpactVolumeMultiplier(intensity);\n const finalVolume = Math.min(1.0, 0.8 * volumeMultiplier);\n\n try {\n await audio.playSFX(soundId, finalVolume);\n lastPlayTime.current[soundType] = Date.now();\n\n // Longer active duration for fracture sounds (more impactful)\n const duration = intensity === \"fracture\" ? 800 : 500;\n registerActiveSound(soundId, duration);\n } catch (error) {\n console.warn(\n `Failed to play bone impact sound: ${soundId} (region: ${region}, intensity: ${intensity})`,\n error,\n );\n }\n },\n [audio, canPlaySound, registerActiveSound],\n );\n\n return {\n playAttackSound,\n playHitSound,\n playBlockSound,\n playDodgeSound,\n playStanceChangeSound,\n playSpecialTechniqueSound,\n playCombatMusic,\n playArchetypeMusic,\n stopCombatMusic,\n getActiveSoundCount,\n playBoneImpactSound, // NEW: Body-region-specific bone/flesh impact sounds\n };\n};\n\nexport default useCombatAudio;\n"],"mappings":"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"}
@@ -22,7 +22,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
22
22
  import { jsx, jsxs } from "react/jsx-runtime";
23
23
  import { Canvas } from "@react-three/fiber";
24
24
  //#region src/components/screens/intro/IntroScreen3D.tsx
25
- var APP_VERSION = "0.7.6";
25
+ var APP_VERSION = "0.7.8";
26
26
  var MENU_ITEMS = [
27
27
  {
28
28
  mode: GameMode.VERSUS,