blacktrigram 0.7.51 → 0.7.52

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.
@@ -69,9 +69,6 @@ function getAudioEffectType(type) {
69
69
  default: return null;
70
70
  }
71
71
  }
72
- /** Maximum concurrent effects to prevent performance issues */
73
- var MAX_BLOOD_EFFECTS = 8;
74
- var MAX_ORGAN_EFFECTS = 4;
75
72
  /**
76
73
  * Simple deterministic hash from string → [0, 1)
77
74
  * Used for deterministic direction calculations inside useMemo
@@ -140,8 +137,8 @@ var CombatParticleEffects3D = ({ hitEffects, enabled = true, isMobile = false })
140
137
  timestamp: hit.timestamp
141
138
  });
142
139
  }
143
- if (newBlood.length > 0) setBloodEffects((prev) => [...prev, ...newBlood].slice(-MAX_BLOOD_EFFECTS));
144
- if (newOrgan.length > 0) setOrganEffects((prev) => [...prev, ...newOrgan].slice(-MAX_ORGAN_EFFECTS));
140
+ if (newBlood.length > 0) setBloodEffects((prev) => [...prev, ...newBlood].slice(-8));
141
+ if (newOrgan.length > 0) setOrganEffects((prev) => [...prev, ...newOrgan].slice(-4));
145
142
  if (newAudio.length > 0) setAudioTriggers((prev) => [...prev, ...newAudio]);
146
143
  if (processedIds.current.size > 500) {
147
144
  const arr = Array.from(processedIds.current);
@@ -1 +1 @@
1
- {"version":3,"file":"CombatParticleEffects3D.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/effects/CombatParticleEffects3D.tsx"],"sourcesContent":["/**\n * CombatParticleEffects3D - Particle effects coordinator for combat\n * 전투 입자 효과 통합 관리\n *\n * Maps HitEffect events to advanced particle effects:\n * - BloodViscosity3D for blood physics on hits\n * - InternalDamage3D for organ damage visualization on vital point strikes\n * - ParticleAudio3D for synchronized combat audio\n *\n * @module components/effects\n * @category Combat Effects\n * @korean 전투입자효과\n */\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport type { HitEffect } from \"../../../../../systems\";\nimport { HitEffectType } from \"../../../../../systems/effects\";\nimport {\n BloodViscosity3D,\n type BloodViscosityEffect,\n type ViscosityType,\n} from \"./BloodViscosity3D\";\nimport {\n InternalDamage3D,\n type InternalDamageEffect,\n type OrganType,\n type PenetrationDepth,\n} from \"./InternalDamage3D\";\nimport {\n ParticleAudio3D,\n type ParticleAudioTrigger,\n type ParticleEffectType,\n} from \"./ParticleAudio3D\";\n\n/**\n * Props for CombatParticleEffects3D\n */\nexport interface CombatParticleEffects3DProps {\n /** Active hit effects from the combat state */\n readonly hitEffects: readonly HitEffect[];\n /** Whether effects are enabled */\n readonly enabled?: boolean;\n /** Mobile optimization flag */\n readonly isMobile?: boolean;\n}\n\n/**\n * Map HitEffectType → blood viscosity type\n * 타격 유형 → 혈액 점도 매핑\n */\nfunction getViscosityForHitType(type: HitEffectType): ViscosityType | null {\n switch (type) {\n case HitEffectType.HIT:\n return \"thin\";\n case HitEffectType.COUNTER:\n return \"medium\";\n case HitEffectType.CRITICAL_HIT:\n return \"thick\";\n case HitEffectType.VITAL_POINT_STRIKE:\n return \"gout\";\n default:\n return null;\n }\n}\n\n/**\n * Map HitEffectType → organ type based on strike position\n * 급소 유형 → 장기 매핑 (타격 위치 기반)\n */\nfunction getOrganForPosition(\n position: { x: number; y: number } | undefined,\n): OrganType {\n if (!position) return \"stomach\";\n const y = position.y;\n if (y > 1.4) return \"heart\"; // 심장 - upper chest\n if (y > 1.1) return \"stomach\"; // 명치 - solar plexus\n if (y > 0.8) return \"liver\"; // 간 - right side mid-torso\n if (y > 0.5) return \"spleen\"; // 비장 - left side mid-torso\n return \"kidney\"; // 신장 - lower back\n}\n\n/**\n * Map HitEffect intensity → penetration depth\n * 타격 강도 → 관통 깊이 매핑\n */\nfunction getPenetrationDepth(intensity: number): PenetrationDepth {\n if (intensity >= 1.5) return \"critical\";\n if (intensity >= 1.0) return \"deep\";\n if (intensity >= 0.5) return \"shallow\";\n return \"surface\";\n}\n\n/**\n * Map HitEffectType → audio effect type\n * 타격 유형 → 오디오 효과 매핑\n */\nfunction getAudioEffectType(type: HitEffectType): ParticleEffectType | null {\n switch (type) {\n case HitEffectType.HIT:\n case HitEffectType.COUNTER:\n return \"viscosity\";\n case HitEffectType.CRITICAL_HIT:\n return \"bone\";\n case HitEffectType.VITAL_POINT_STRIKE:\n return \"organ\";\n case HitEffectType.BLOCK:\n case HitEffectType.PARRY:\n return \"bone\";\n default:\n return null;\n }\n}\n\n/** Maximum concurrent effects to prevent performance issues */\nconst MAX_BLOOD_EFFECTS = 8;\nconst MAX_ORGAN_EFFECTS = 4;\n\n/**\n * Simple deterministic hash from string → [0, 1)\n * Used for deterministic direction calculations inside useMemo\n */\nfunction hashToFloat(str: string): number {\n let hash = 0;\n for (let i = 0; i < str.length; i++) {\n hash = (hash * 31 + str.charCodeAt(i)) | 0;\n }\n return Math.abs(hash % 10000) / 10000;\n}\n\n/**\n * CombatParticleEffects3D - Coordinates particle effects for combat\n *\n * Converts HitEffect events into:\n * - Blood viscosity particles (thin/medium/thick/gout based on strike type)\n * - Internal organ damage pulses (for vital point strikes)\n * - Synchronized audio triggers\n *\n * @korean 전투 입자 효과 통합 (타격→혈액물리+장기손상+오디오)\n */\nexport const CombatParticleEffects3D: React.FC<\n CombatParticleEffects3DProps\n> = ({ hitEffects, enabled = true, isMobile = false }) => {\n const processedIds = useRef<Set<string>>(new Set());\n\n const [bloodEffects, setBloodEffects] = useState<BloodViscosityEffect[]>([]);\n const [organEffects, setOrganEffects] = useState<InternalDamageEffect[]>([]);\n const [audioTriggers, setAudioTriggers] = useState<ParticleAudioTrigger[]>(\n [],\n );\n\n useEffect(() => {\n const newBlood: BloodViscosityEffect[] = [];\n const newOrgan: InternalDamageEffect[] = [];\n const newAudio: ParticleAudioTrigger[] = [];\n\n for (const hit of hitEffects) {\n if (processedIds.current.has(hit.id)) continue;\n processedIds.current.add(hit.id);\n\n const pos: [number, number, number] = [\n hit.position?.x ?? 0,\n hit.position?.y ?? 1.0,\n 0,\n ];\n\n const viscosity = getViscosityForHitType(hit.type);\n if (viscosity) {\n const angle = hashToFloat(hit.id) * Math.PI * 2;\n const ySpread = 0.3 + hashToFloat(hit.id + \"_y\") * 0.4;\n const dir: [number, number, number] = [\n Math.cos(angle) * 0.5,\n ySpread,\n Math.sin(angle) * 0.5,\n ];\n\n newBlood.push({\n id: `blood_${hit.id}`,\n position: pos,\n direction: dir,\n viscosityType: viscosity,\n intensity: Math.min(1, hit.intensity),\n startTime: hit.startTime,\n });\n }\n\n if (hit.type === HitEffectType.VITAL_POINT_STRIKE) {\n newOrgan.push({\n id: `organ_${hit.id}`,\n position: pos,\n organType: getOrganForPosition(hit.position),\n penetrationDepth: getPenetrationDepth(hit.intensity),\n startTime: hit.startTime,\n });\n }\n\n const audioType = getAudioEffectType(hit.type);\n if (audioType) {\n newAudio.push({\n effectType: audioType,\n intensity: Math.min(1, hit.intensity),\n timestamp: hit.timestamp,\n });\n }\n }\n\n if (newBlood.length > 0) {\n setBloodEffects((prev) =>\n [...prev, ...newBlood].slice(-MAX_BLOOD_EFFECTS),\n );\n }\n if (newOrgan.length > 0) {\n setOrganEffects((prev) =>\n [...prev, ...newOrgan].slice(-MAX_ORGAN_EFFECTS),\n );\n }\n if (newAudio.length > 0) {\n setAudioTriggers((prev) => [...prev, ...newAudio]);\n }\n\n if (processedIds.current.size > 500) {\n const arr = Array.from(processedIds.current);\n processedIds.current = new Set(arr.slice(-250));\n }\n }, [hitEffects]);\n\n const handleBloodComplete = useCallback((id: string) => {\n setBloodEffects((prev) => prev.filter((e) => e.id !== id));\n }, []);\n\n const handleOrganComplete = useCallback((id: string) => {\n setOrganEffects((prev) => prev.filter((e) => e.id !== id));\n }, []);\n\n const handleAudioProcessed = useCallback((timestamp: number) => {\n setAudioTriggers((prev) => prev.filter((t) => t.timestamp !== timestamp));\n }, []);\n\n if (!enabled) return null;\n\n return (\n <>\n {/* Blood Viscosity Particles - 혈액 점도 입자 */}\n <BloodViscosity3D\n effects={bloodEffects}\n enabled={enabled}\n isMobile={isMobile}\n onEffectComplete={handleBloodComplete}\n />\n\n {/* Internal Organ Damage Pulses - 장기 손상 시각화 */}\n <InternalDamage3D\n effects={organEffects}\n enabled={enabled}\n isMobile={isMobile}\n onEffectComplete={handleOrganComplete}\n />\n\n {/* Audio Coordination - 입자 효과 오디오 동기화 */}\n <ParticleAudio3D\n triggers={audioTriggers}\n enabled={enabled}\n onTriggerProcessed={handleAudioProcessed}\n />\n </>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"CombatParticleEffects3D.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/effects/CombatParticleEffects3D.tsx"],"sourcesContent":["/**\n * CombatParticleEffects3D - Particle effects coordinator for combat\n * 전투 입자 효과 통합 관리\n *\n * Maps HitEffect events to advanced particle effects:\n * - BloodViscosity3D for blood physics on hits\n * - InternalDamage3D for organ damage visualization on vital point strikes\n * - ParticleAudio3D for synchronized combat audio\n *\n * @module components/effects\n * @category Combat Effects\n * @korean 전투입자효과\n */\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport type { HitEffect } from \"../../../../../systems\";\nimport { HitEffectType } from \"../../../../../systems/effects\";\nimport {\n BloodViscosity3D,\n type BloodViscosityEffect,\n type ViscosityType,\n} from \"./BloodViscosity3D\";\nimport {\n InternalDamage3D,\n type InternalDamageEffect,\n type OrganType,\n type PenetrationDepth,\n} from \"./InternalDamage3D\";\nimport {\n ParticleAudio3D,\n type ParticleAudioTrigger,\n type ParticleEffectType,\n} from \"./ParticleAudio3D\";\n\n/**\n * Props for CombatParticleEffects3D\n */\nexport interface CombatParticleEffects3DProps {\n /** Active hit effects from the combat state */\n readonly hitEffects: readonly HitEffect[];\n /** Whether effects are enabled */\n readonly enabled?: boolean;\n /** Mobile optimization flag */\n readonly isMobile?: boolean;\n}\n\n/**\n * Map HitEffectType → blood viscosity type\n * 타격 유형 → 혈액 점도 매핑\n */\nfunction getViscosityForHitType(type: HitEffectType): ViscosityType | null {\n switch (type) {\n case HitEffectType.HIT:\n return \"thin\";\n case HitEffectType.COUNTER:\n return \"medium\";\n case HitEffectType.CRITICAL_HIT:\n return \"thick\";\n case HitEffectType.VITAL_POINT_STRIKE:\n return \"gout\";\n default:\n return null;\n }\n}\n\n/**\n * Map HitEffectType → organ type based on strike position\n * 급소 유형 → 장기 매핑 (타격 위치 기반)\n */\nfunction getOrganForPosition(\n position: { x: number; y: number } | undefined,\n): OrganType {\n if (!position) return \"stomach\";\n const y = position.y;\n if (y > 1.4) return \"heart\"; // 심장 - upper chest\n if (y > 1.1) return \"stomach\"; // 명치 - solar plexus\n if (y > 0.8) return \"liver\"; // 간 - right side mid-torso\n if (y > 0.5) return \"spleen\"; // 비장 - left side mid-torso\n return \"kidney\"; // 신장 - lower back\n}\n\n/**\n * Map HitEffect intensity → penetration depth\n * 타격 강도 → 관통 깊이 매핑\n */\nfunction getPenetrationDepth(intensity: number): PenetrationDepth {\n if (intensity >= 1.5) return \"critical\";\n if (intensity >= 1.0) return \"deep\";\n if (intensity >= 0.5) return \"shallow\";\n return \"surface\";\n}\n\n/**\n * Map HitEffectType → audio effect type\n * 타격 유형 → 오디오 효과 매핑\n */\nfunction getAudioEffectType(type: HitEffectType): ParticleEffectType | null {\n switch (type) {\n case HitEffectType.HIT:\n case HitEffectType.COUNTER:\n return \"viscosity\";\n case HitEffectType.CRITICAL_HIT:\n return \"bone\";\n case HitEffectType.VITAL_POINT_STRIKE:\n return \"organ\";\n case HitEffectType.BLOCK:\n case HitEffectType.PARRY:\n return \"bone\";\n default:\n return null;\n }\n}\n\n/** Maximum concurrent effects to prevent performance issues */\nconst MAX_BLOOD_EFFECTS = 8;\nconst MAX_ORGAN_EFFECTS = 4;\n\n/**\n * Simple deterministic hash from string → [0, 1)\n * Used for deterministic direction calculations inside useMemo\n */\nfunction hashToFloat(str: string): number {\n let hash = 0;\n for (let i = 0; i < str.length; i++) {\n hash = (hash * 31 + str.charCodeAt(i)) | 0;\n }\n return Math.abs(hash % 10000) / 10000;\n}\n\n/**\n * CombatParticleEffects3D - Coordinates particle effects for combat\n *\n * Converts HitEffect events into:\n * - Blood viscosity particles (thin/medium/thick/gout based on strike type)\n * - Internal organ damage pulses (for vital point strikes)\n * - Synchronized audio triggers\n *\n * @korean 전투 입자 효과 통합 (타격→혈액물리+장기손상+오디오)\n */\nexport const CombatParticleEffects3D: React.FC<\n CombatParticleEffects3DProps\n> = ({ hitEffects, enabled = true, isMobile = false }) => {\n const processedIds = useRef<Set<string>>(new Set());\n\n const [bloodEffects, setBloodEffects] = useState<BloodViscosityEffect[]>([]);\n const [organEffects, setOrganEffects] = useState<InternalDamageEffect[]>([]);\n const [audioTriggers, setAudioTriggers] = useState<ParticleAudioTrigger[]>(\n [],\n );\n\n useEffect(() => {\n const newBlood: BloodViscosityEffect[] = [];\n const newOrgan: InternalDamageEffect[] = [];\n const newAudio: ParticleAudioTrigger[] = [];\n\n for (const hit of hitEffects) {\n if (processedIds.current.has(hit.id)) continue;\n processedIds.current.add(hit.id);\n\n const pos: [number, number, number] = [\n hit.position?.x ?? 0,\n hit.position?.y ?? 1.0,\n 0,\n ];\n\n const viscosity = getViscosityForHitType(hit.type);\n if (viscosity) {\n const angle = hashToFloat(hit.id) * Math.PI * 2;\n const ySpread = 0.3 + hashToFloat(hit.id + \"_y\") * 0.4;\n const dir: [number, number, number] = [\n Math.cos(angle) * 0.5,\n ySpread,\n Math.sin(angle) * 0.5,\n ];\n\n newBlood.push({\n id: `blood_${hit.id}`,\n position: pos,\n direction: dir,\n viscosityType: viscosity,\n intensity: Math.min(1, hit.intensity),\n startTime: hit.startTime,\n });\n }\n\n if (hit.type === HitEffectType.VITAL_POINT_STRIKE) {\n newOrgan.push({\n id: `organ_${hit.id}`,\n position: pos,\n organType: getOrganForPosition(hit.position),\n penetrationDepth: getPenetrationDepth(hit.intensity),\n startTime: hit.startTime,\n });\n }\n\n const audioType = getAudioEffectType(hit.type);\n if (audioType) {\n newAudio.push({\n effectType: audioType,\n intensity: Math.min(1, hit.intensity),\n timestamp: hit.timestamp,\n });\n }\n }\n\n if (newBlood.length > 0) {\n setBloodEffects((prev) =>\n [...prev, ...newBlood].slice(-MAX_BLOOD_EFFECTS),\n );\n }\n if (newOrgan.length > 0) {\n setOrganEffects((prev) =>\n [...prev, ...newOrgan].slice(-MAX_ORGAN_EFFECTS),\n );\n }\n if (newAudio.length > 0) {\n setAudioTriggers((prev) => [...prev, ...newAudio]);\n }\n\n if (processedIds.current.size > 500) {\n const arr = Array.from(processedIds.current);\n processedIds.current = new Set(arr.slice(-250));\n }\n }, [hitEffects]);\n\n const handleBloodComplete = useCallback((id: string) => {\n setBloodEffects((prev) => prev.filter((e) => e.id !== id));\n }, []);\n\n const handleOrganComplete = useCallback((id: string) => {\n setOrganEffects((prev) => prev.filter((e) => e.id !== id));\n }, []);\n\n const handleAudioProcessed = useCallback((timestamp: number) => {\n setAudioTriggers((prev) => prev.filter((t) => t.timestamp !== timestamp));\n }, []);\n\n if (!enabled) return null;\n\n return (\n <>\n {/* Blood Viscosity Particles - 혈액 점도 입자 */}\n <BloodViscosity3D\n effects={bloodEffects}\n enabled={enabled}\n isMobile={isMobile}\n onEffectComplete={handleBloodComplete}\n />\n\n {/* Internal Organ Damage Pulses - 장기 손상 시각화 */}\n <InternalDamage3D\n effects={organEffects}\n enabled={enabled}\n isMobile={isMobile}\n onEffectComplete={handleOrganComplete}\n />\n\n {/* Audio Coordination - 입자 효과 오디오 동기화 */}\n <ParticleAudio3D\n triggers={audioTriggers}\n enabled={enabled}\n onTriggerProcessed={handleAudioProcessed}\n />\n </>\n );\n};\n"],"mappings":"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"}
@@ -380,10 +380,10 @@ function useCombatActions(config) {
380
380
  [
381
381
  {
382
382
  x: shakeIntensity,
383
- y: -shakeIntensity * .5
383
+ y: -8 * .5
384
384
  },
385
385
  {
386
- x: -shakeIntensity * .7,
386
+ x: -8 * .7,
387
387
  y: shakeIntensity * .8
388
388
  },
389
389
  {
@@ -391,8 +391,8 @@ function useCombatActions(config) {
391
391
  y: shakeIntensity * .3
392
392
  },
393
393
  {
394
- x: -shakeIntensity * .3,
395
- y: -shakeIntensity * .6
394
+ x: -8 * .3,
395
+ y: -8 * .6
396
396
  },
397
397
  {
398
398
  x: 0,
@@ -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\nexport const SCREEN_SHAKE_FRAME_INTERVAL_MS = 50;\nexport const SCREEN_SHAKE_FRAME_COUNT = 5;\n\n/**\n * Clears every timeout in a tracked set and empties the set afterwards.\n *\n * @param timeoutIds Timeout identifiers to clear and remove\n */\nfunction clearTimeoutSet(timeoutIds: Set<ReturnType<typeof setTimeout>>): void {\n timeoutIds.forEach((timeoutId) => {\n clearTimeout(timeoutId);\n });\n timeoutIds.clear();\n}\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 if (technique.damageType === \"slashing\") {\n return result.damage > 20 ? InjuryType.LACERATION : InjuryType.CUT;\n }\n\n if (result.damage > 25) {\n return InjuryType.BRUISE;\n }\n\n if (result.damage > 15) {\n return InjuryType.BRUISE;\n }\n\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 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 const bodyRegion = BodyRegion.TORSO; // TODO: Extract from result when available\n\n let injuryType = determineInjuryType(result, technique);\n\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 const severity = Math.min(1.0, result.damage / 30);\n\n const basePosition = getBodyRegionPosition(bodyRegion);\n\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 const newPos = {\n x: defenderPos.x + result.knockback.displacement.x,\n y: defenderPos.y + result.knockback.displacement.z,\n };\n\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 const player1KnockbackTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const player2KnockbackTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const screenShakeTimeoutsRef = useRef<Set<ReturnType<typeof setTimeout>>>(\n new Set(),\n );\n\n useEffect(() => {\n const screenShakeTimeouts = screenShakeTimeoutsRef.current;\n return () => {\n clearTimeoutSet(screenShakeTimeouts);\n if (player1KnockbackTimeoutRef.current) {\n clearTimeout(player1KnockbackTimeoutRef.current);\n }\n if (player2KnockbackTimeoutRef.current) {\n clearTimeout(player2KnockbackTimeoutRef.current);\n }\n };\n }, []);\n\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 let attackTechnique: KoreanTechnique;\n\n if (technique) {\n attackTechnique = convertTechniqueToKorean(technique, currentStance);\n } else {\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 const selectedTechnique = availableTechniques[0];\n\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 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 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 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 const hitPosition = calculateHitPosition(playerPositions[1]);\n\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 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 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 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 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 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 if (result.knockback.duration > 0) {\n if (player2KnockbackTimeoutRef.current) {\n clearTimeout(player2KnockbackTimeoutRef.current);\n }\n\n onPlayerUpdate(1, { isStunned: true });\n\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 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 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 const handleDefend = useCallback(() => {\n if (!combatState.roundStarted || combatState.roundEnded) return;\n\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 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 combatAudio?.playSpecialTechniqueSound();\n\n addHitEffect(HitEffectType.CRITICAL_HIT, playerPositions[0], 1.5);\n\n clearTimeoutSet(screenShakeTimeoutsRef.current);\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 ].slice(0, SCREEN_SHAKE_FRAME_COUNT);\n\n shakeFrames.forEach((shake, index) => {\n const timeoutId = setTimeout(\n () => {\n screenShakeTimeoutsRef.current.delete(timeoutId);\n combatActions.setScreenShake(shake);\n },\n index * SCREEN_SHAKE_FRAME_INTERVAL_MS,\n );\n screenShakeTimeoutsRef.current.add(timeoutId);\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 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 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 const handleStanceSwitch = useCallback(\n (stance: TrigramStance) => {\n if (!combatState.roundStarted || combatState.roundEnded) return;\n\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 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 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 onPlayerUpdate(playerIndex, result.updatedPlayer);\n\n onLateralityUpdate?.(playerIndex, result.laterality);\n\n combatAudio?.playStanceChangeSound?.();\n\n const koreanText =\n result.laterality === \"left\" ? \"왼발서기\" : \"오른발서기\";\n const englishText =\n result.laterality === \"left\" ? \"Left Stance\" : \"Right Stance\";\n addCombatMessage(koreanText, englishText);\n\n addHitEffect(\n HitEffectType.STATUS_EFFECT,\n playerPositions[playerIndex],\n 0.5,\n );\n } else {\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 const attackTechnique = technique ?? createAITechnique(\"basic\", aiPlayer);\n\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 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 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 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 const { updatedAttacker, updatedDefender } =\n combatSystem.applyCombatResult(result, aiPlayer, targetPlayer);\n\n onPlayerUpdate(1, updatedAttacker);\n onPlayerUpdate(0, updatedDefender);\n\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 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 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 if (result.knockback.duration > 0) {\n if (player1KnockbackTimeoutRef.current) {\n clearTimeout(player1KnockbackTimeoutRef.current);\n }\n\n onPlayerUpdate(0, { isStunned: true });\n\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 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 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 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 const handleAIDefend = useCallback(() => {\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 const specialTechnique =\n technique ?? createAITechnique(\"special\", aiPlayer);\n\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 combatAudio?.playSpecialTechniqueSound();\n\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 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 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 const { updatedAttacker, updatedDefender } =\n combatSystem.applyCombatResult(result, aiPlayer, targetPlayer);\n\n onPlayerUpdate(1, updatedAttacker);\n onPlayerUpdate(0, updatedDefender);\n\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 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 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 if (result.knockback.duration > 0) {\n if (player1KnockbackTimeoutRef.current) {\n clearTimeout(player1KnockbackTimeoutRef.current);\n }\n\n onPlayerUpdate(0, { isStunned: true });\n\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 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 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 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 const moveAIPlayer = useCallback(\n (targetPos: Position) => {\n const currentPos = playerPositions[1];\n const aiPlayer = validPlayers[1];\n\n const AI_DECISION_FREQUENCY_HZ = 20; // 20 calls/second (50ms interval)\n const baseSpeed = 2.5 / AI_DECISION_FREQUENCY_HZ; // meters per call (0.125m per call)\n\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 let finalSpeed = baseSpeed;\n if (aiPlayer.bodyPartHealth && aiPlayer.bodyPartMaxHealth) {\n const movementDirection = {\n x: distance > 0 ? dx / distance : 0,\n y: distance > 0 ? dy / distance : 0,\n };\n\n finalSpeed = movementPenaltySystem.calculateModifiedSpeed(\n baseSpeed,\n aiPlayer.bodyPartHealth,\n aiPlayer.bodyPartMaxHealth,\n movementDirection,\n );\n }\n\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 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 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\nexport const SCREEN_SHAKE_FRAME_INTERVAL_MS = 50;\nexport const SCREEN_SHAKE_FRAME_COUNT = 5;\n\n/**\n * Clears every timeout in a tracked set and empties the set afterwards.\n *\n * @param timeoutIds Timeout identifiers to clear and remove\n */\nfunction clearTimeoutSet(timeoutIds: Set<ReturnType<typeof setTimeout>>): void {\n timeoutIds.forEach((timeoutId) => {\n clearTimeout(timeoutId);\n });\n timeoutIds.clear();\n}\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 if (technique.damageType === \"slashing\") {\n return result.damage > 20 ? InjuryType.LACERATION : InjuryType.CUT;\n }\n\n if (result.damage > 25) {\n return InjuryType.BRUISE;\n }\n\n if (result.damage > 15) {\n return InjuryType.BRUISE;\n }\n\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 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 const bodyRegion = BodyRegion.TORSO; // TODO: Extract from result when available\n\n let injuryType = determineInjuryType(result, technique);\n\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 const severity = Math.min(1.0, result.damage / 30);\n\n const basePosition = getBodyRegionPosition(bodyRegion);\n\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 const newPos = {\n x: defenderPos.x + result.knockback.displacement.x,\n y: defenderPos.y + result.knockback.displacement.z,\n };\n\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 const player1KnockbackTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const player2KnockbackTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const screenShakeTimeoutsRef = useRef<Set<ReturnType<typeof setTimeout>>>(\n new Set(),\n );\n\n useEffect(() => {\n const screenShakeTimeouts = screenShakeTimeoutsRef.current;\n return () => {\n clearTimeoutSet(screenShakeTimeouts);\n if (player1KnockbackTimeoutRef.current) {\n clearTimeout(player1KnockbackTimeoutRef.current);\n }\n if (player2KnockbackTimeoutRef.current) {\n clearTimeout(player2KnockbackTimeoutRef.current);\n }\n };\n }, []);\n\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 let attackTechnique: KoreanTechnique;\n\n if (technique) {\n attackTechnique = convertTechniqueToKorean(technique, currentStance);\n } else {\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 const selectedTechnique = availableTechniques[0];\n\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 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 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 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 const hitPosition = calculateHitPosition(playerPositions[1]);\n\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 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 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 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 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 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 if (result.knockback.duration > 0) {\n if (player2KnockbackTimeoutRef.current) {\n clearTimeout(player2KnockbackTimeoutRef.current);\n }\n\n onPlayerUpdate(1, { isStunned: true });\n\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 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 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 const handleDefend = useCallback(() => {\n if (!combatState.roundStarted || combatState.roundEnded) return;\n\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 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 combatAudio?.playSpecialTechniqueSound();\n\n addHitEffect(HitEffectType.CRITICAL_HIT, playerPositions[0], 1.5);\n\n clearTimeoutSet(screenShakeTimeoutsRef.current);\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 ].slice(0, SCREEN_SHAKE_FRAME_COUNT);\n\n shakeFrames.forEach((shake, index) => {\n const timeoutId = setTimeout(\n () => {\n screenShakeTimeoutsRef.current.delete(timeoutId);\n combatActions.setScreenShake(shake);\n },\n index * SCREEN_SHAKE_FRAME_INTERVAL_MS,\n );\n screenShakeTimeoutsRef.current.add(timeoutId);\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 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 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 const handleStanceSwitch = useCallback(\n (stance: TrigramStance) => {\n if (!combatState.roundStarted || combatState.roundEnded) return;\n\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 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 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 onPlayerUpdate(playerIndex, result.updatedPlayer);\n\n onLateralityUpdate?.(playerIndex, result.laterality);\n\n combatAudio?.playStanceChangeSound?.();\n\n const koreanText =\n result.laterality === \"left\" ? \"왼발서기\" : \"오른발서기\";\n const englishText =\n result.laterality === \"left\" ? \"Left Stance\" : \"Right Stance\";\n addCombatMessage(koreanText, englishText);\n\n addHitEffect(\n HitEffectType.STATUS_EFFECT,\n playerPositions[playerIndex],\n 0.5,\n );\n } else {\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 const attackTechnique = technique ?? createAITechnique(\"basic\", aiPlayer);\n\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 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 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 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 const { updatedAttacker, updatedDefender } =\n combatSystem.applyCombatResult(result, aiPlayer, targetPlayer);\n\n onPlayerUpdate(1, updatedAttacker);\n onPlayerUpdate(0, updatedDefender);\n\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 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 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 if (result.knockback.duration > 0) {\n if (player1KnockbackTimeoutRef.current) {\n clearTimeout(player1KnockbackTimeoutRef.current);\n }\n\n onPlayerUpdate(0, { isStunned: true });\n\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 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 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 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 const handleAIDefend = useCallback(() => {\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 const specialTechnique =\n technique ?? createAITechnique(\"special\", aiPlayer);\n\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 combatAudio?.playSpecialTechniqueSound();\n\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 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 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 const { updatedAttacker, updatedDefender } =\n combatSystem.applyCombatResult(result, aiPlayer, targetPlayer);\n\n onPlayerUpdate(1, updatedAttacker);\n onPlayerUpdate(0, updatedDefender);\n\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 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 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 if (result.knockback.duration > 0) {\n if (player1KnockbackTimeoutRef.current) {\n clearTimeout(player1KnockbackTimeoutRef.current);\n }\n\n onPlayerUpdate(0, { isStunned: true });\n\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 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 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 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 const moveAIPlayer = useCallback(\n (targetPos: Position) => {\n const currentPos = playerPositions[1];\n const aiPlayer = validPlayers[1];\n\n const AI_DECISION_FREQUENCY_HZ = 20; // 20 calls/second (50ms interval)\n const baseSpeed = 2.5 / AI_DECISION_FREQUENCY_HZ; // meters per call (0.125m per call)\n\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 let finalSpeed = baseSpeed;\n if (aiPlayer.bodyPartHealth && aiPlayer.bodyPartMaxHealth) {\n const movementDirection = {\n x: distance > 0 ? dx / distance : 0,\n y: distance > 0 ? dy / distance : 0,\n };\n\n finalSpeed = movementPenaltySystem.calculateModifiedSpeed(\n baseSpeed,\n aiPlayer.bodyPartHealth,\n aiPlayer.bodyPartMaxHealth,\n movementDirection,\n );\n }\n\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 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 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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"}
@@ -120,7 +120,7 @@ var Key3D = ({ keyData, isPressed }) => {
120
120
  position: [
121
121
  0,
122
122
  0,
123
- keyDepth / 2 + .01
123
+ .11
124
124
  ],
125
125
  center: true,
126
126
  distanceFactor: 2,
@@ -141,7 +141,7 @@ var Key3D = ({ keyData, isPressed }) => {
141
141
  position: [
142
142
  0,
143
143
  0,
144
- -keyDepth / 2 - .02
144
+ -.2 / 2 - .02
145
145
  ],
146
146
  rotation: [
147
147
  0,
@@ -1 +1 @@
1
- {"version":3,"file":"Key3D.js","names":[],"sources":["../../../../../src/components/screens/controls/components/Key3D.tsx"],"sourcesContent":["/**\n * Key3D - Individual 3D keyboard key component with press animation\n * \n * Renders a 3D box mesh for a keyboard key with category-based coloring,\n * press animation, and bilingual label overlay.\n * \n * @module components/screens/controls/components\n */\n\nimport { Html } from \"@react-three/drei\";\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useEffect, useMemo, useRef } from \"react\";\nimport * as THREE from \"three\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport { getKeyCategoryColor, type KeyData } from \"../constants/ControlsConstants\";\n\n/**\n * Props for Key3D component\n */\nexport interface Key3DProps {\n /** Key data containing position, label, and category */\n readonly keyData: KeyData;\n /** Whether this key is currently pressed */\n readonly isPressed: boolean;\n}\n\n/**\n * Key3D Component\n * \n * Individual 3D keyboard key with press animation and label overlay.\n * Uses category-based coloring and emissive glow when pressed.\n * \n * @example\n * ```tsx\n * <Key3D\n * keyData={{ code: 'Space', label: 'Space', row: 4, col: 2, width: 3, category: 'combat' }}\n * isPressed={pressedKeys.has('Space')}\n * />\n * ```\n */\nexport const Key3D: React.FC<Key3DProps> = ({ keyData, isPressed }) => {\n const meshRef = useRef<THREE.Mesh>(null);\n const targetScale = useRef(new THREE.Vector3(1, 1, 1));\n\n const keyWidth = (keyData.width ?? 1) * 0.6; // 0.6 units per key width\n const keyHeight = 0.6;\n const keyDepth = 0.2;\n\n const position = useMemo<[number, number, number]>(() => {\n const x = keyData.col * 0.6 + (keyWidth / 2) - 3.0; // Center around origin\n const y = -keyData.row * 0.6;\n const z = isPressed ? -0.05 : 0; // Press down slightly when pressed\n return [x, y, z];\n }, [keyData.col, keyData.row, keyWidth, isPressed]);\n\n const categoryColor = useMemo(() => getKeyCategoryColor(keyData.category), [keyData.category]);\n\n useFrame(() => {\n if (!meshRef.current) return;\n\n const targetScaleValue = isPressed ? 0.9 : 1.0;\n targetScale.current.set(targetScaleValue, targetScaleValue, 1.0);\n\n meshRef.current.scale.lerp(targetScale.current, 0.2);\n });\n\n const materials = useMemo(\n () => ({\n unpressed: new THREE.MeshStandardMaterial({\n color: categoryColor,\n emissive: categoryColor,\n emissiveIntensity: 0.3,\n metalness: 0.6,\n roughness: 0.4,\n transparent: true,\n opacity: 0.9,\n }),\n pressed: new THREE.MeshStandardMaterial({\n color: categoryColor,\n emissive: categoryColor,\n emissiveIntensity: 2.5,\n metalness: 0.6,\n roughness: 0.4,\n transparent: true,\n opacity: 0.9,\n }),\n }),\n [categoryColor]\n );\n\n const keyMaterial = isPressed ? materials.pressed : materials.unpressed;\n\n useEffect(() => {\n return () => {\n materials.unpressed.dispose();\n materials.pressed.dispose();\n };\n }, [materials]);\n\n const labelStyle = useMemo(() => ({\n fontFamily: FONT_FAMILY.KOREAN,\n fontSize: keyData.width && keyData.width > 2 ? '11px' : '13px',\n fontWeight: 'bold' as const,\n color: isPressed ? hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD) : hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY),\n textAlign: 'center' as const,\n pointerEvents: 'none' as const,\n userSelect: 'none' as const,\n textShadow: isPressed ? `0 0 8px ${hexToRgbaString(categoryColor, 0.8)}` : 'none',\n whiteSpace: 'nowrap' as const,\n lineHeight: 1.2,\n }), [isPressed, categoryColor, keyData.width]);\n\n return (\n <group position={position}>\n {/* 3D key box */}\n <mesh\n ref={meshRef}\n castShadow\n receiveShadow\n name={`key-${keyData.code}`}\n data-testid={`key-${keyData.code}`}\n >\n <boxGeometry args={[keyWidth, keyHeight, keyDepth]} />\n <primitive object={keyMaterial} attach=\"material\" />\n </mesh>\n\n {/* Label overlay */}\n <Html\n position={[0, 0, keyDepth / 2 + 0.01]}\n center\n distanceFactor={2}\n style={{ pointerEvents: 'none' }}\n >\n <div style={labelStyle}>\n {/* Main label */}\n <div>{keyData.label}</div>\n {/* Korean label (if available) */}\n {keyData.labelKorean && (\n <div style={{\n fontSize: '10px',\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD),\n marginTop: '2px',\n }}>\n {keyData.labelKorean}\n </div>\n )}\n </div>\n </Html>\n\n {/* Glow ring when pressed */}\n {isPressed && (\n <mesh position={[0, 0, -keyDepth / 2 - 0.02]} rotation={[0, 0, 0]}>\n <ringGeometry args={[Math.max(keyWidth, keyHeight) * 0.6, Math.max(keyWidth, keyHeight) * 0.7, 32]} />\n <meshBasicMaterial\n color={categoryColor}\n transparent\n opacity={0.6}\n side={THREE.DoubleSide}\n />\n </mesh>\n )}\n </group>\n );\n};\n\nexport default Key3D;\n"],"mappings":"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"}
1
+ {"version":3,"file":"Key3D.js","names":[],"sources":["../../../../../src/components/screens/controls/components/Key3D.tsx"],"sourcesContent":["/**\n * Key3D - Individual 3D keyboard key component with press animation\n * \n * Renders a 3D box mesh for a keyboard key with category-based coloring,\n * press animation, and bilingual label overlay.\n * \n * @module components/screens/controls/components\n */\n\nimport { Html } from \"@react-three/drei\";\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useEffect, useMemo, useRef } from \"react\";\nimport * as THREE from \"three\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport { getKeyCategoryColor, type KeyData } from \"../constants/ControlsConstants\";\n\n/**\n * Props for Key3D component\n */\nexport interface Key3DProps {\n /** Key data containing position, label, and category */\n readonly keyData: KeyData;\n /** Whether this key is currently pressed */\n readonly isPressed: boolean;\n}\n\n/**\n * Key3D Component\n * \n * Individual 3D keyboard key with press animation and label overlay.\n * Uses category-based coloring and emissive glow when pressed.\n * \n * @example\n * ```tsx\n * <Key3D\n * keyData={{ code: 'Space', label: 'Space', row: 4, col: 2, width: 3, category: 'combat' }}\n * isPressed={pressedKeys.has('Space')}\n * />\n * ```\n */\nexport const Key3D: React.FC<Key3DProps> = ({ keyData, isPressed }) => {\n const meshRef = useRef<THREE.Mesh>(null);\n const targetScale = useRef(new THREE.Vector3(1, 1, 1));\n\n const keyWidth = (keyData.width ?? 1) * 0.6; // 0.6 units per key width\n const keyHeight = 0.6;\n const keyDepth = 0.2;\n\n const position = useMemo<[number, number, number]>(() => {\n const x = keyData.col * 0.6 + (keyWidth / 2) - 3.0; // Center around origin\n const y = -keyData.row * 0.6;\n const z = isPressed ? -0.05 : 0; // Press down slightly when pressed\n return [x, y, z];\n }, [keyData.col, keyData.row, keyWidth, isPressed]);\n\n const categoryColor = useMemo(() => getKeyCategoryColor(keyData.category), [keyData.category]);\n\n useFrame(() => {\n if (!meshRef.current) return;\n\n const targetScaleValue = isPressed ? 0.9 : 1.0;\n targetScale.current.set(targetScaleValue, targetScaleValue, 1.0);\n\n meshRef.current.scale.lerp(targetScale.current, 0.2);\n });\n\n const materials = useMemo(\n () => ({\n unpressed: new THREE.MeshStandardMaterial({\n color: categoryColor,\n emissive: categoryColor,\n emissiveIntensity: 0.3,\n metalness: 0.6,\n roughness: 0.4,\n transparent: true,\n opacity: 0.9,\n }),\n pressed: new THREE.MeshStandardMaterial({\n color: categoryColor,\n emissive: categoryColor,\n emissiveIntensity: 2.5,\n metalness: 0.6,\n roughness: 0.4,\n transparent: true,\n opacity: 0.9,\n }),\n }),\n [categoryColor]\n );\n\n const keyMaterial = isPressed ? materials.pressed : materials.unpressed;\n\n useEffect(() => {\n return () => {\n materials.unpressed.dispose();\n materials.pressed.dispose();\n };\n }, [materials]);\n\n const labelStyle = useMemo(() => ({\n fontFamily: FONT_FAMILY.KOREAN,\n fontSize: keyData.width && keyData.width > 2 ? '11px' : '13px',\n fontWeight: 'bold' as const,\n color: isPressed ? hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD) : hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY),\n textAlign: 'center' as const,\n pointerEvents: 'none' as const,\n userSelect: 'none' as const,\n textShadow: isPressed ? `0 0 8px ${hexToRgbaString(categoryColor, 0.8)}` : 'none',\n whiteSpace: 'nowrap' as const,\n lineHeight: 1.2,\n }), [isPressed, categoryColor, keyData.width]);\n\n return (\n <group position={position}>\n {/* 3D key box */}\n <mesh\n ref={meshRef}\n castShadow\n receiveShadow\n name={`key-${keyData.code}`}\n data-testid={`key-${keyData.code}`}\n >\n <boxGeometry args={[keyWidth, keyHeight, keyDepth]} />\n <primitive object={keyMaterial} attach=\"material\" />\n </mesh>\n\n {/* Label overlay */}\n <Html\n position={[0, 0, keyDepth / 2 + 0.01]}\n center\n distanceFactor={2}\n style={{ pointerEvents: 'none' }}\n >\n <div style={labelStyle}>\n {/* Main label */}\n <div>{keyData.label}</div>\n {/* Korean label (if available) */}\n {keyData.labelKorean && (\n <div style={{\n fontSize: '10px',\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD),\n marginTop: '2px',\n }}>\n {keyData.labelKorean}\n </div>\n )}\n </div>\n </Html>\n\n {/* Glow ring when pressed */}\n {isPressed && (\n <mesh position={[0, 0, -keyDepth / 2 - 0.02]} rotation={[0, 0, 0]}>\n <ringGeometry args={[Math.max(keyWidth, keyHeight) * 0.6, Math.max(keyWidth, keyHeight) * 0.7, 32]} />\n <meshBasicMaterial\n color={categoryColor}\n transparent\n opacity={0.6}\n side={THREE.DoubleSide}\n />\n </mesh>\n )}\n </group>\n );\n};\n\nexport default Key3D;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,IAAa,SAA+B,EAAE,SAAS,gBAAgB;CACrE,MAAM,UAAU,OAAmB,IAAI;CACvC,MAAM,cAAc,OAAO,IAAI,MAAM,QAAQ,GAAG,GAAG,CAAC,CAAC;CAErD,MAAM,YAAY,QAAQ,SAAS,KAAK;CACxC,MAAM,YAAY;CAClB,MAAM,WAAW;CAEjB,MAAM,WAAW,cAAwC;EAIvD,OAAO;GAHG,QAAQ,MAAM,KAAO,WAAW,IAAK;GACrC,CAAC,QAAQ,MAAM;GACf,YAAY,OAAQ;EACf;CACjB,GAAG;EAAC,QAAQ;EAAK,QAAQ;EAAK;EAAU;CAAS,CAAC;CAElD,MAAM,gBAAgB,cAAc,oBAAoB,QAAQ,QAAQ,GAAG,CAAC,QAAQ,QAAQ,CAAC;CAE7F,eAAe;EACb,IAAI,CAAC,QAAQ,SAAS;EAEtB,MAAM,mBAAmB,YAAY,KAAM;EAC3C,YAAY,QAAQ,IAAI,kBAAkB,kBAAkB,CAAG;EAE/D,QAAQ,QAAQ,MAAM,KAAK,YAAY,SAAS,EAAG;CACrD,CAAC;CAED,MAAM,YAAY,eACT;EACL,WAAW,IAAI,MAAM,qBAAqB;GACxC,OAAO;GACP,UAAU;GACV,mBAAmB;GACnB,WAAW;GACX,WAAW;GACX,aAAa;GACb,SAAS;EACX,CAAC;EACD,SAAS,IAAI,MAAM,qBAAqB;GACtC,OAAO;GACP,UAAU;GACV,mBAAmB;GACnB,WAAW;GACX,WAAW;GACX,aAAa;GACb,SAAS;EACX,CAAC;CACH,IACA,CAAC,aAAa,CAChB;CAEA,MAAM,cAAc,YAAY,UAAU,UAAU,UAAU;CAE9D,gBAAgB;EACd,aAAa;GACX,UAAU,UAAU,QAAQ;GAC5B,UAAU,QAAQ,QAAQ;EAC5B;CACF,GAAG,CAAC,SAAS,CAAC;CAEd,MAAM,aAAa,eAAe;EAChC,YAAY,YAAY;EACxB,UAAU,QAAQ,SAAS,QAAQ,QAAQ,IAAI,SAAS;EACxD,YAAY;EACZ,OAAO,YAAY,gBAAgB,cAAc,WAAW,IAAI,gBAAgB,cAAc,YAAY;EAC1G,WAAW;EACX,eAAe;EACf,YAAY;EACZ,YAAY,YAAY,WAAW,gBAAgB,eAAe,EAAG,MAAM;EAC3E,YAAY;EACZ,YAAY;CACd,IAAI;EAAC;EAAW;EAAe,QAAQ;CAAK,CAAC;CAE7C,OACE,qBAAC,SAAD;EAAiB;YAAjB;GAEE,qBAAC,QAAD;IACE,KAAK;IACL,YAAA;IACA,eAAA;IACA,MAAM,OAAO,QAAQ;IACrB,eAAa,OAAO,QAAQ;cAL9B,CAOE,oBAAC,eAAD,EAAa,MAAM;KAAC;KAAU;KAAW;IAAQ,EAAI,CAAA,GACrD,oBAAC,aAAD;KAAW,QAAQ;KAAa,QAAO;IAAY,CAAA,CAC/C;;GAGN,oBAAC,MAAD;IACE,UAAU;KAAC;KAAG;KAAG;IAAmB;IACpC,QAAA;IACA,gBAAgB;IAChB,OAAO,EAAE,eAAe,OAAO;cAE/B,qBAAC,OAAD;KAAK,OAAO;eAAZ,CAEE,oBAAC,OAAD,EAAA,UAAM,QAAQ,MAAW,CAAA,GAExB,QAAQ,eACP,oBAAC,OAAD;MAAK,OAAO;OACV,UAAU;OACV,OAAO,gBAAgB,cAAc,WAAW;OAChD,WAAW;MACb;gBACG,QAAQ;KACN,CAAA,CAEJ;;GACD,CAAA;GAGL,aACC,qBAAC,QAAD;IAAM,UAAU;KAAC;KAAG;KAAG,MAAY,IAAI;IAAI;IAAG,UAAU;KAAC;KAAG;KAAG;IAAC;cAAhE,CACE,oBAAC,gBAAD,EAAc,MAAM;KAAC,KAAK,IAAI,UAAU,SAAS,IAAI;KAAK,KAAK,IAAI,UAAU,SAAS,IAAI;KAAK;IAAE,EAAI,CAAA,GACrG,oBAAC,qBAAD;KACE,OAAO;KACP,aAAA;KACA,SAAS;KACT,MAAM,MAAM;IACb,CAAA,CACG;;EAEH;;AAEX"}
@@ -21,7 +21,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
21
21
  import { jsx, jsxs } from "react/jsx-runtime";
22
22
  import { Canvas } from "@react-three/fiber";
23
23
  //#region src/components/screens/intro/IntroScreen3D.tsx
24
- var APP_VERSION = "0.7.51";
24
+ var APP_VERSION = "0.7.52";
25
25
  var MENU_ITEMS = [
26
26
  {
27
27
  mode: GameMode.VERSUS,
@@ -116,7 +116,7 @@ var DummyHealthBar = ({ health, maxHealth, position }) => {
116
116
  const healthPercent = Math.max(0, Math.min(100, health / maxHealth * 100));
117
117
  const bgGeometry = useMemo(() => new THREE.BoxGeometry(HEALTH_BAR_WIDTH, HEALTH_BAR_HEIGHT, .02), []);
118
118
  const healthGeometry = useMemo(() => new THREE.BoxGeometry(HEALTH_BAR_WIDTH, HEALTH_BAR_HEIGHT, .02), []);
119
- const borderGeometry = useMemo(() => new THREE.BoxGeometry(HEALTH_BAR_WIDTH + .04, HEALTH_BAR_HEIGHT + .04, .01), []);
119
+ const borderGeometry = useMemo(() => new THREE.BoxGeometry(1.24, .14, .01), []);
120
120
  const healthColor = useMemo(() => {
121
121
  if (healthPercent > 70) return KOREAN_COLORS.HEALTH_FULL;
122
122
  if (healthPercent > 40) return KOREAN_COLORS.HEALTH_MEDIUM;
@@ -1 +1 @@
1
- {"version":3,"file":"TrainingDummy3D.js","names":[],"sources":["../../../../../src/components/screens/training/components/TrainingDummy3D.tsx"],"sourcesContent":["/**\n * TrainingDummy3D - 3D training dummy with vital points\n *\n * Provides anatomically accurate training dummy using SkeletalPlayer3D\n * for Korean martial arts practice. Supports anatomy overlays and difficulty modes.\n *\n * Refactored to extend SkeletalPlayer3D for visual consistency with player characters.\n *\n * @module components/screens/training/TrainingDummy3D\n * @category 3D Components\n * @korean 훈련인형3D컴포넌트\n */\n\nimport { useFrame } from \"@react-three/fiber\";\nimport React, {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport * as THREE from \"three\";\nimport { KOREAN_VITAL_POINTS } from \"../../../../systems/vitalpoint/KoreanVitalPoints\";\nimport { PlayerArchetype, TrigramStance } from \"../../../../types/common\";\nimport { KOREAN_COLORS } from \"../../../../types/constants\";\nimport SkeletalPlayer3D from \"../../../shared/three/models/SkeletalPlayer3D\";\nimport VitalPointMarker3D from \"./VitalPointMarker3D\";\n\n/**\n * Difficulty mode for training\n * @korean 난이도모드\n */\nexport type DifficultyMode = \"easy\" | \"normal\" | \"hard\";\n\n/**\n * Props for TrainingDummy3D component\n * @korean 훈련인형3D속성\n */\nexport interface TrainingDummy3DProps {\n /** 3D world position of the dummy */\n readonly position: [number, number, number];\n /** Currently selected vital point for targeting */\n readonly selectedVitalPoint: string | null;\n /** Whether training is active */\n readonly isTraining: boolean;\n /** Current health of dummy (0-100) */\n readonly health?: number;\n /** Maximum health of dummy */\n readonly maxHealth?: number;\n /** Callback when vital point is hit */\n readonly onVitalPointHit?: (vitalPointId: string) => void;\n /** Callback when dummy is defeated */\n readonly onDefeated?: () => void;\n /** Difficulty mode (affects marker sizes) */\n readonly difficulty?: DifficultyMode;\n /** Number of vital points to display (3-70) */\n readonly vitalPointCount?: number;\n /** Whether on mobile device */\n readonly isMobile?: boolean;\n /** Archetype to display (affects body type and appearance) */\n readonly archetype?: PlayerArchetype;\n /** Current stance for the dummy */\n readonly stance?: TrigramStance;\n}\n\n/**\n * Map body region to 3D position on dummy\n *\n * Positions are calibrated for SkeletalPlayer3D bone structure.\n *\n * @param pointId - Unique identifier for the vital point\n * @param category - Anatomical category (head, neck, torso, etc.)\n * @returns 3D coordinates [x, y, z] relative to dummy center\n * @korean 급소위치계산\n */\nconst getVitalPointPosition = (\n pointId: string,\n category: string,\n): [number, number, number] => {\n const positions: Record<string, [number, number, number]> = {\n head: [0, 1.75, 0.12], // Head bone + offset\n neck: [0, 1.5, 0.1], // Neck bone\n torso: [0, 1.15, 0.18], // Chest/spine area\n chest: [0, 1.2, 0.2], // Upper chest\n abdomen: [0, 0.85, 0.18], // Lower torso\n back: [0, 1.1, -0.15], // Spine back\n arm: [-0.45, 1.15, 0], // Upper arm area\n leg: [-0.18, 0.4, 0.05], // Thigh area\n neurological: [0, 1.7, 0.08], // Temple/head neural points\n vascular: [0, 1.45, 0.12], // Neck vascular points\n muscular: [0, 1.1, 0.2], // Muscle groups\n skeletal: [0, 0.9, 0.15], // Joints/bones\n };\n\n const basePos = positions[category.toLowerCase()] ?? [0, 1.1, 0.15];\n\n const hash =\n pointId.split(\"\").reduce((acc, char) => acc + char.charCodeAt(0), 0) *\n 0.001;\n return [\n basePos[0] + Math.sin(hash * 7.3) * 0.08,\n basePos[1] + Math.cos(hash * 5.1) * 0.08,\n basePos[2] + Math.sin(hash * 3.7) * 0.03,\n ];\n};\n\n/**\n * Health bar component for training dummy\n *\n * Displays above the dummy with Korean cyberpunk styling.\n * @korean 훈련인형체력바\n */\n\nconst HEALTH_BAR_WIDTH = 1.2;\nconst HEALTH_BAR_HEIGHT = 0.1;\n\nconst DummyHealthBar: React.FC<{\n readonly health: number;\n readonly maxHealth: number;\n readonly position: [number, number, number];\n}> = ({ health, maxHealth, position }) => {\n const healthPercent = Math.max(0, Math.min(100, (health / maxHealth) * 100));\n\n const bgGeometry = useMemo(\n () => new THREE.BoxGeometry(HEALTH_BAR_WIDTH, HEALTH_BAR_HEIGHT, 0.02),\n [],\n );\n const healthGeometry = useMemo(\n () => new THREE.BoxGeometry(HEALTH_BAR_WIDTH, HEALTH_BAR_HEIGHT, 0.02),\n [],\n );\n const borderGeometry = useMemo(\n () =>\n new THREE.BoxGeometry(\n HEALTH_BAR_WIDTH + 0.04,\n HEALTH_BAR_HEIGHT + 0.04,\n 0.01,\n ),\n [],\n );\n\n const healthColor = useMemo(() => {\n if (healthPercent > 70) return KOREAN_COLORS.HEALTH_FULL;\n if (healthPercent > 40) return KOREAN_COLORS.HEALTH_MEDIUM;\n if (healthPercent > 20) return KOREAN_COLORS.HEALTH_LOW;\n return KOREAN_COLORS.HEALTH_CRITICAL;\n }, [healthPercent]);\n\n const healthScale: [number, number, number] = useMemo(\n () => [healthPercent / 100, 1, 1],\n [healthPercent],\n );\n\n useEffect(() => {\n return () => {\n bgGeometry.dispose();\n healthGeometry.dispose();\n borderGeometry.dispose();\n };\n }, [bgGeometry, healthGeometry, borderGeometry]);\n\n return (\n <group position={position} name=\"dummy-health-bar\">\n {/* Background bar */}\n <mesh>\n <primitive object={bgGeometry} />\n <meshBasicMaterial\n color={KOREAN_COLORS.UI_BACKGROUND_DARK}\n transparent\n opacity={0.7}\n />\n </mesh>\n\n {/* Health bar (scaled based on health, anchored to left edge) */}\n <mesh\n position={[\n -(HEALTH_BAR_WIDTH * (1 - healthPercent / 100)) / 2,\n 0,\n 0.01,\n ]}\n scale={healthScale}\n >\n <primitive object={healthGeometry} />\n <meshBasicMaterial color={healthColor} transparent opacity={0.9} />\n </mesh>\n\n {/* Border frame - outline using EdgesGeometry for crisp border */}\n <lineSegments>\n <edgesGeometry args={[borderGeometry]} />\n <lineBasicMaterial\n color={KOREAN_COLORS.PRIMARY_CYAN}\n transparent\n opacity={0.8}\n />\n </lineSegments>\n </group>\n );\n};\n\n/**\n * TrainingDummy3D Component\n *\n * Main training dummy using SkeletalPlayer3D for consistent visual appearance.\n * Displays vital points for martial arts practice with difficulty-based sizing.\n *\n * @example\n * ```tsx\n * <TrainingDummy3D\n * position={[0, 0, 5]}\n * selectedVitalPoint=\"temple\"\n * isTraining={true}\n * health={75}\n * difficulty=\"normal\"\n * onVitalPointHit={(id) => console.log(`Hit ${id}`)}\n * />\n * ```\n *\n * @korean 훈련인형3D컴포넌트\n */\nexport const TrainingDummy3D: React.FC<TrainingDummy3DProps> = ({\n position,\n selectedVitalPoint,\n isTraining,\n health = 100,\n maxHealth = 100,\n onVitalPointHit,\n onDefeated,\n difficulty = \"normal\",\n vitalPointCount = 12,\n isMobile = false,\n archetype = PlayerArchetype.MUSA,\n stance = TrigramStance.GEON,\n}) => {\n const groupRef = useRef<THREE.Group>(null);\n const [isStunned, setIsStunned] = useState(false);\n\n const vitalPoints = useMemo(\n () =>\n KOREAN_VITAL_POINTS.slice(\n 0,\n Math.min(vitalPointCount, KOREAN_VITAL_POINTS.length),\n ),\n [vitalPointCount],\n );\n\n const sizeMultiplier = useMemo(() => {\n switch (difficulty) {\n case \"easy\":\n return 1.5; // Larger targets\n case \"normal\":\n return 1.0; // Standard size\n case \"hard\":\n return 0.7; // Smaller targets\n default:\n return 1.0;\n }\n }, [difficulty]);\n\n const prevHealthRef = useRef(health);\n\n const onDefeatedRef = useRef(onDefeated);\n useEffect(() => {\n onDefeatedRef.current = onDefeated;\n }, [onDefeated]);\n\n useEffect(() => {\n if (prevHealthRef.current > 0 && health <= 0) {\n onDefeatedRef.current?.();\n }\n\n if (prevHealthRef.current - health > 20) {\n setIsStunned(true);\n const timer = setTimeout(() => setIsStunned(false), 500);\n return () => clearTimeout(timer);\n }\n\n prevHealthRef.current = health;\n return undefined;\n }, [health]);\n\n useFrame((state) => {\n if (!groupRef.current) return;\n\n const time = state.clock.elapsedTime;\n const breathScale = Math.sin(time * 1.5) * 0.01 + 1;\n groupRef.current.scale.y = breathScale;\n\n groupRef.current.rotation.y = Math.sin(time * 0.5) * 0.02;\n });\n\n const handlePointHit = useCallback(\n (pointId: string) => {\n onVitalPointHit?.(pointId);\n },\n [onVitalPointHit],\n );\n\n return (\n <group ref={groupRef} position={position} name=\"training-dummy-3d\">\n {/* SkeletalPlayer3D as the base character model */}\n <SkeletalPlayer3D\n playerId=\"training-dummy\"\n archetype={archetype}\n stance={stance}\n position={[0, 0, 0]}\n rotation={Math.PI} // Face the player\n facing=\"left\"\n health={health}\n maxHealth={maxHealth}\n stamina={100}\n ki={50}\n balance=\"READY\"\n pain={0}\n consciousness={100}\n isMobile={isMobile}\n currentAnimation=\"idle\"\n isBlocking={false}\n isStunned={isStunned}\n showHealthBar={false} // We use custom health bar\n showStanceIndicator={false}\n enableFacialExpressions={true}\n enableEyeTracking={true}\n />\n\n {/* Vital point markers overlaid on the skeletal model */}\n {vitalPoints.map((point) => (\n <group\n key={point.id}\n position={getVitalPointPosition(point.id, point.category)}\n >\n <VitalPointMarker3D\n vitalPoint={point}\n isSelected={point.id === selectedVitalPoint}\n isTraining={isTraining}\n isMobile={isMobile}\n onHit={handlePointHit}\n sizeMultiplier={sizeMultiplier}\n />\n </group>\n ))}\n\n {/* Health bar above dummy */}\n {isTraining && (\n <DummyHealthBar\n health={health}\n maxHealth={maxHealth}\n position={[0, 2.3, 0]}\n />\n )}\n\n {/* Training mode indicator glow */}\n {isTraining && (\n <pointLight\n position={[0, 1.2, 0.5]}\n color={KOREAN_COLORS.PRIMARY_CYAN}\n intensity={0.3}\n distance={3}\n decay={2}\n />\n )}\n </group>\n );\n};\n\nexport default TrainingDummy3D;\n"],"mappings":"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"}
1
+ {"version":3,"file":"TrainingDummy3D.js","names":[],"sources":["../../../../../src/components/screens/training/components/TrainingDummy3D.tsx"],"sourcesContent":["/**\n * TrainingDummy3D - 3D training dummy with vital points\n *\n * Provides anatomically accurate training dummy using SkeletalPlayer3D\n * for Korean martial arts practice. Supports anatomy overlays and difficulty modes.\n *\n * Refactored to extend SkeletalPlayer3D for visual consistency with player characters.\n *\n * @module components/screens/training/TrainingDummy3D\n * @category 3D Components\n * @korean 훈련인형3D컴포넌트\n */\n\nimport { useFrame } from \"@react-three/fiber\";\nimport React, {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport * as THREE from \"three\";\nimport { KOREAN_VITAL_POINTS } from \"../../../../systems/vitalpoint/KoreanVitalPoints\";\nimport { PlayerArchetype, TrigramStance } from \"../../../../types/common\";\nimport { KOREAN_COLORS } from \"../../../../types/constants\";\nimport SkeletalPlayer3D from \"../../../shared/three/models/SkeletalPlayer3D\";\nimport VitalPointMarker3D from \"./VitalPointMarker3D\";\n\n/**\n * Difficulty mode for training\n * @korean 난이도모드\n */\nexport type DifficultyMode = \"easy\" | \"normal\" | \"hard\";\n\n/**\n * Props for TrainingDummy3D component\n * @korean 훈련인형3D속성\n */\nexport interface TrainingDummy3DProps {\n /** 3D world position of the dummy */\n readonly position: [number, number, number];\n /** Currently selected vital point for targeting */\n readonly selectedVitalPoint: string | null;\n /** Whether training is active */\n readonly isTraining: boolean;\n /** Current health of dummy (0-100) */\n readonly health?: number;\n /** Maximum health of dummy */\n readonly maxHealth?: number;\n /** Callback when vital point is hit */\n readonly onVitalPointHit?: (vitalPointId: string) => void;\n /** Callback when dummy is defeated */\n readonly onDefeated?: () => void;\n /** Difficulty mode (affects marker sizes) */\n readonly difficulty?: DifficultyMode;\n /** Number of vital points to display (3-70) */\n readonly vitalPointCount?: number;\n /** Whether on mobile device */\n readonly isMobile?: boolean;\n /** Archetype to display (affects body type and appearance) */\n readonly archetype?: PlayerArchetype;\n /** Current stance for the dummy */\n readonly stance?: TrigramStance;\n}\n\n/**\n * Map body region to 3D position on dummy\n *\n * Positions are calibrated for SkeletalPlayer3D bone structure.\n *\n * @param pointId - Unique identifier for the vital point\n * @param category - Anatomical category (head, neck, torso, etc.)\n * @returns 3D coordinates [x, y, z] relative to dummy center\n * @korean 급소위치계산\n */\nconst getVitalPointPosition = (\n pointId: string,\n category: string,\n): [number, number, number] => {\n const positions: Record<string, [number, number, number]> = {\n head: [0, 1.75, 0.12], // Head bone + offset\n neck: [0, 1.5, 0.1], // Neck bone\n torso: [0, 1.15, 0.18], // Chest/spine area\n chest: [0, 1.2, 0.2], // Upper chest\n abdomen: [0, 0.85, 0.18], // Lower torso\n back: [0, 1.1, -0.15], // Spine back\n arm: [-0.45, 1.15, 0], // Upper arm area\n leg: [-0.18, 0.4, 0.05], // Thigh area\n neurological: [0, 1.7, 0.08], // Temple/head neural points\n vascular: [0, 1.45, 0.12], // Neck vascular points\n muscular: [0, 1.1, 0.2], // Muscle groups\n skeletal: [0, 0.9, 0.15], // Joints/bones\n };\n\n const basePos = positions[category.toLowerCase()] ?? [0, 1.1, 0.15];\n\n const hash =\n pointId.split(\"\").reduce((acc, char) => acc + char.charCodeAt(0), 0) *\n 0.001;\n return [\n basePos[0] + Math.sin(hash * 7.3) * 0.08,\n basePos[1] + Math.cos(hash * 5.1) * 0.08,\n basePos[2] + Math.sin(hash * 3.7) * 0.03,\n ];\n};\n\n/**\n * Health bar component for training dummy\n *\n * Displays above the dummy with Korean cyberpunk styling.\n * @korean 훈련인형체력바\n */\n\nconst HEALTH_BAR_WIDTH = 1.2;\nconst HEALTH_BAR_HEIGHT = 0.1;\n\nconst DummyHealthBar: React.FC<{\n readonly health: number;\n readonly maxHealth: number;\n readonly position: [number, number, number];\n}> = ({ health, maxHealth, position }) => {\n const healthPercent = Math.max(0, Math.min(100, (health / maxHealth) * 100));\n\n const bgGeometry = useMemo(\n () => new THREE.BoxGeometry(HEALTH_BAR_WIDTH, HEALTH_BAR_HEIGHT, 0.02),\n [],\n );\n const healthGeometry = useMemo(\n () => new THREE.BoxGeometry(HEALTH_BAR_WIDTH, HEALTH_BAR_HEIGHT, 0.02),\n [],\n );\n const borderGeometry = useMemo(\n () =>\n new THREE.BoxGeometry(\n HEALTH_BAR_WIDTH + 0.04,\n HEALTH_BAR_HEIGHT + 0.04,\n 0.01,\n ),\n [],\n );\n\n const healthColor = useMemo(() => {\n if (healthPercent > 70) return KOREAN_COLORS.HEALTH_FULL;\n if (healthPercent > 40) return KOREAN_COLORS.HEALTH_MEDIUM;\n if (healthPercent > 20) return KOREAN_COLORS.HEALTH_LOW;\n return KOREAN_COLORS.HEALTH_CRITICAL;\n }, [healthPercent]);\n\n const healthScale: [number, number, number] = useMemo(\n () => [healthPercent / 100, 1, 1],\n [healthPercent],\n );\n\n useEffect(() => {\n return () => {\n bgGeometry.dispose();\n healthGeometry.dispose();\n borderGeometry.dispose();\n };\n }, [bgGeometry, healthGeometry, borderGeometry]);\n\n return (\n <group position={position} name=\"dummy-health-bar\">\n {/* Background bar */}\n <mesh>\n <primitive object={bgGeometry} />\n <meshBasicMaterial\n color={KOREAN_COLORS.UI_BACKGROUND_DARK}\n transparent\n opacity={0.7}\n />\n </mesh>\n\n {/* Health bar (scaled based on health, anchored to left edge) */}\n <mesh\n position={[\n -(HEALTH_BAR_WIDTH * (1 - healthPercent / 100)) / 2,\n 0,\n 0.01,\n ]}\n scale={healthScale}\n >\n <primitive object={healthGeometry} />\n <meshBasicMaterial color={healthColor} transparent opacity={0.9} />\n </mesh>\n\n {/* Border frame - outline using EdgesGeometry for crisp border */}\n <lineSegments>\n <edgesGeometry args={[borderGeometry]} />\n <lineBasicMaterial\n color={KOREAN_COLORS.PRIMARY_CYAN}\n transparent\n opacity={0.8}\n />\n </lineSegments>\n </group>\n );\n};\n\n/**\n * TrainingDummy3D Component\n *\n * Main training dummy using SkeletalPlayer3D for consistent visual appearance.\n * Displays vital points for martial arts practice with difficulty-based sizing.\n *\n * @example\n * ```tsx\n * <TrainingDummy3D\n * position={[0, 0, 5]}\n * selectedVitalPoint=\"temple\"\n * isTraining={true}\n * health={75}\n * difficulty=\"normal\"\n * onVitalPointHit={(id) => console.log(`Hit ${id}`)}\n * />\n * ```\n *\n * @korean 훈련인형3D컴포넌트\n */\nexport const TrainingDummy3D: React.FC<TrainingDummy3DProps> = ({\n position,\n selectedVitalPoint,\n isTraining,\n health = 100,\n maxHealth = 100,\n onVitalPointHit,\n onDefeated,\n difficulty = \"normal\",\n vitalPointCount = 12,\n isMobile = false,\n archetype = PlayerArchetype.MUSA,\n stance = TrigramStance.GEON,\n}) => {\n const groupRef = useRef<THREE.Group>(null);\n const [isStunned, setIsStunned] = useState(false);\n\n const vitalPoints = useMemo(\n () =>\n KOREAN_VITAL_POINTS.slice(\n 0,\n Math.min(vitalPointCount, KOREAN_VITAL_POINTS.length),\n ),\n [vitalPointCount],\n );\n\n const sizeMultiplier = useMemo(() => {\n switch (difficulty) {\n case \"easy\":\n return 1.5; // Larger targets\n case \"normal\":\n return 1.0; // Standard size\n case \"hard\":\n return 0.7; // Smaller targets\n default:\n return 1.0;\n }\n }, [difficulty]);\n\n const prevHealthRef = useRef(health);\n\n const onDefeatedRef = useRef(onDefeated);\n useEffect(() => {\n onDefeatedRef.current = onDefeated;\n }, [onDefeated]);\n\n useEffect(() => {\n if (prevHealthRef.current > 0 && health <= 0) {\n onDefeatedRef.current?.();\n }\n\n if (prevHealthRef.current - health > 20) {\n setIsStunned(true);\n const timer = setTimeout(() => setIsStunned(false), 500);\n return () => clearTimeout(timer);\n }\n\n prevHealthRef.current = health;\n return undefined;\n }, [health]);\n\n useFrame((state) => {\n if (!groupRef.current) return;\n\n const time = state.clock.elapsedTime;\n const breathScale = Math.sin(time * 1.5) * 0.01 + 1;\n groupRef.current.scale.y = breathScale;\n\n groupRef.current.rotation.y = Math.sin(time * 0.5) * 0.02;\n });\n\n const handlePointHit = useCallback(\n (pointId: string) => {\n onVitalPointHit?.(pointId);\n },\n [onVitalPointHit],\n );\n\n return (\n <group ref={groupRef} position={position} name=\"training-dummy-3d\">\n {/* SkeletalPlayer3D as the base character model */}\n <SkeletalPlayer3D\n playerId=\"training-dummy\"\n archetype={archetype}\n stance={stance}\n position={[0, 0, 0]}\n rotation={Math.PI} // Face the player\n facing=\"left\"\n health={health}\n maxHealth={maxHealth}\n stamina={100}\n ki={50}\n balance=\"READY\"\n pain={0}\n consciousness={100}\n isMobile={isMobile}\n currentAnimation=\"idle\"\n isBlocking={false}\n isStunned={isStunned}\n showHealthBar={false} // We use custom health bar\n showStanceIndicator={false}\n enableFacialExpressions={true}\n enableEyeTracking={true}\n />\n\n {/* Vital point markers overlaid on the skeletal model */}\n {vitalPoints.map((point) => (\n <group\n key={point.id}\n position={getVitalPointPosition(point.id, point.category)}\n >\n <VitalPointMarker3D\n vitalPoint={point}\n isSelected={point.id === selectedVitalPoint}\n isTraining={isTraining}\n isMobile={isMobile}\n onHit={handlePointHit}\n sizeMultiplier={sizeMultiplier}\n />\n </group>\n ))}\n\n {/* Health bar above dummy */}\n {isTraining && (\n <DummyHealthBar\n health={health}\n maxHealth={maxHealth}\n position={[0, 2.3, 0]}\n />\n )}\n\n {/* Training mode indicator glow */}\n {isTraining && (\n <pointLight\n position={[0, 1.2, 0.5]}\n color={KOREAN_COLORS.PRIMARY_CYAN}\n intensity={0.3}\n distance={3}\n decay={2}\n />\n )}\n </group>\n );\n};\n\nexport default TrainingDummy3D;\n"],"mappings":"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"}
@@ -183,7 +183,7 @@ var SplashScreen = ({ onStart, width, height }) => {
183
183
  }),
184
184
  /* @__PURE__ */ jsxs("div", {
185
185
  role: "contentinfo",
186
- "aria-label": `Application version 0.7.51`,
186
+ "aria-label": `Application version 0.7.52`,
187
187
  style: {
188
188
  position: "absolute",
189
189
  bottom: "20px",
@@ -192,7 +192,7 @@ var SplashScreen = ({ onStart, width, height }) => {
192
192
  fontSize: "10px",
193
193
  zIndex: 1
194
194
  },
195
- children: ["v", "0.7.51"]
195
+ children: ["v", "0.7.52"]
196
196
  })
197
197
  ]
198
198
  });
@@ -196,8 +196,7 @@ var generateFabricNormalMap = (config) => {
196
196
  ctx.fillStyle = "rgb(140, 128, 255)";
197
197
  ctx.fillRect(xPos, yPos, threadWidth * .4, threadWidth * .9);
198
198
  }
199
- const edgeIntensity = 20;
200
- ctx.fillStyle = `rgb(${128 + edgeIntensity}, ${128 + edgeIntensity}, 255)`;
199
+ ctx.fillStyle = `rgb(148, 148, 255)`;
201
200
  ctx.fillRect(xPos, yPos, threadWidth * .1, threadWidth);
202
201
  ctx.fillRect(xPos, yPos, threadWidth, threadWidth * .1);
203
202
  }
@@ -1 +1 @@
1
- {"version":3,"file":"fabricTextures.js","names":[],"sources":["../../src/utils/fabricTextures.ts"],"sourcesContent":["/**\n * Procedural fabric texture generation for realistic clothing\n *\n * **Korean**: 절차적 직물 텍스처 (Procedural Fabric Textures)\n *\n * Generates canvas-based textures for realistic dobok (도복) martial arts\n * uniform rendering without external image assets. Uses Three.js CanvasTexture\n * for efficient GPU-based texture mapping.\n *\n * **Features**:\n * - Procedural weave patterns for fabric realism\n * - Normal maps for surface detail and depth\n * - Roughness maps for material variation\n * - Memory-safe with proper cleanup\n *\n * @module utils/fabricTextures\n * @category Visual Effects\n * @korean 직물텍스처유틸\n */\n\nimport * as THREE from \"three\";\n\n/**\n * Fabric texture configuration\n */\nexport interface FabricTextureConfig {\n /** Base color for the fabric */\n readonly baseColor: string;\n /** Weave pattern density (higher = finer weave) */\n readonly weaveDensity: number;\n /** Texture resolution (power of 2 recommended) */\n readonly resolution: number;\n /** Thread variation intensity (0-1) */\n readonly threadVariation: number;\n /** Whether to generate normal map */\n readonly generateNormalMap: boolean;\n /** Whether to generate roughness map */\n readonly generateRoughnessMap: boolean;\n}\n\n/**\n * Generated fabric texture set\n */\nexport interface FabricTextureSet {\n /** Color/diffuse map */\n readonly colorMap: THREE.CanvasTexture;\n /** Normal map for surface detail (optional) */\n readonly normalMap?: THREE.CanvasTexture;\n /** Roughness map for material variation (optional) */\n readonly roughnessMap?: THREE.CanvasTexture;\n /** Cleanup function to dispose all textures */\n readonly dispose: () => void;\n}\n\n/**\n * Default fabric texture configurations for different materials\n */\nexport const FABRIC_PRESETS: Record<string, Partial<FabricTextureConfig>> = {\n /** Traditional cotton dobok (도복) */\n dobok: {\n weaveDensity: 32,\n threadVariation: 0.15,\n resolution: 256,\n generateNormalMap: true,\n generateRoughnessMap: true,\n },\n /** Tactical synthetic fabric */\n tactical: {\n weaveDensity: 48,\n threadVariation: 0.08,\n resolution: 256,\n generateNormalMap: true,\n generateRoughnessMap: true,\n },\n /** Leather material */\n leather: {\n weaveDensity: 16,\n threadVariation: 0.25,\n resolution: 256,\n generateNormalMap: true,\n generateRoughnessMap: false,\n },\n /** Silk/satin material */\n silk: {\n weaveDensity: 64,\n threadVariation: 0.05,\n resolution: 256,\n generateNormalMap: true,\n generateRoughnessMap: true,\n },\n};\n\n/**\n * Convert hex color to RGB components\n */\nconst hexToRgb = (hex: string): { r: number; g: number; b: number } => {\n const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n return result\n ? {\n r: parseInt(result[1], 16),\n g: parseInt(result[2], 16),\n b: parseInt(result[3], 16),\n }\n : { r: 128, g: 128, b: 128 };\n};\n\n/**\n * Simple seeded random for reproducible textures\n */\nconst seededRandom = (seed: number): number => {\n const x = Math.sin(seed) * 10000;\n return x - Math.floor(x);\n};\n\n/**\n * Check if we're in a browser environment with canvas support\n */\nconst canCreateCanvas = (): boolean => {\n if (typeof document === \"undefined\") return false;\n try {\n const canvas = document.createElement(\"canvas\");\n return canvas.getContext(\"2d\") !== null;\n } catch {\n return false;\n }\n};\n\n/**\n * Create a fallback texture for test environments\n * Uses a minimal texture that works in all environments\n */\nconst createFallbackTexture = (): THREE.CanvasTexture => {\n // In test environments, THREE mocks may not have all features\n // Create a minimal mock that satisfies the interface\n try {\n const data = new Uint8Array([128, 128, 128, 255]);\n const dataTexture = new THREE.DataTexture(data, 1, 1, THREE.RGBAFormat);\n dataTexture.needsUpdate = true;\n return dataTexture as unknown as THREE.CanvasTexture;\n } catch {\n // Ultimate fallback - create a minimal mock texture object\n // Use numeric constants instead of THREE constants for test compatibility\n const mockTexture = {\n dispose: () => {},\n needsUpdate: true,\n wrapS: 1000, // THREE.RepeatWrapping value\n wrapT: 1000,\n repeat: { set: () => {} },\n uuid: \"mock-fabric-texture\",\n isTexture: true,\n };\n return mockTexture as unknown as THREE.CanvasTexture;\n }\n};\n\n/**\n * Generate a procedural fabric weave color map\n *\n * Creates a canvas-based texture with realistic thread patterns\n * simulating woven fabric like cotton dobok material.\n *\n * @param config - Fabric texture configuration\n * @returns CanvasTexture with weave pattern\n *\n * @korean 직물색상맵생성\n */\nexport const generateFabricColorMap = (\n config: FabricTextureConfig,\n): THREE.CanvasTexture => {\n // Return fallback for test environments without canvas support\n if (!canCreateCanvas()) {\n return createFallbackTexture();\n }\n\n const { baseColor, weaveDensity, resolution, threadVariation } = config;\n\n const canvas = document.createElement(\"canvas\");\n canvas.width = resolution;\n canvas.height = resolution;\n const ctx = canvas.getContext(\"2d\");\n\n if (!ctx) {\n return createFallbackTexture();\n }\n\n const rgb = hexToRgb(baseColor);\n\n // Fill base color\n ctx.fillStyle = baseColor;\n ctx.fillRect(0, 0, resolution, resolution);\n\n // Create weave pattern\n const threadWidth = resolution / weaveDensity;\n\n // Horizontal threads (weft)\n for (let y = 0; y < weaveDensity; y++) {\n const yPos = y * threadWidth;\n const variation = (seededRandom(y * 17) - 0.5) * threadVariation * 255;\n\n const r = Math.max(0, Math.min(255, rgb.r + variation));\n const g = Math.max(0, Math.min(255, rgb.g + variation));\n const b = Math.max(0, Math.min(255, rgb.b + variation));\n\n ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;\n\n // Draw thread with slight offset pattern\n for (let x = 0; x < weaveDensity; x++) {\n const xPos = x * threadWidth;\n // Alternating over/under pattern\n if ((x + y) % 2 === 0) {\n ctx.fillRect(xPos, yPos, threadWidth * 0.95, threadWidth * 0.45);\n }\n }\n }\n\n // Vertical threads (warp)\n for (let x = 0; x < weaveDensity; x++) {\n const xPos = x * threadWidth;\n const variation = (seededRandom(x * 31) - 0.5) * threadVariation * 255;\n\n const r = Math.max(0, Math.min(255, rgb.r + variation * 0.8));\n const g = Math.max(0, Math.min(255, rgb.g + variation * 0.8));\n const b = Math.max(0, Math.min(255, rgb.b + variation * 0.8));\n\n ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;\n\n for (let y = 0; y < weaveDensity; y++) {\n const yPos = y * threadWidth;\n // Opposite pattern for weave effect\n if ((x + y) % 2 === 1) {\n ctx.fillRect(xPos, yPos, threadWidth * 0.45, threadWidth * 0.95);\n }\n }\n }\n\n const texture = new THREE.CanvasTexture(canvas);\n texture.wrapS = THREE.RepeatWrapping;\n texture.wrapT = THREE.RepeatWrapping;\n texture.repeat.set(4, 4); // Tile the texture\n texture.needsUpdate = true;\n\n return texture;\n};\n\n/**\n * Generate a normal map for fabric surface detail\n *\n * Creates subtle bumps and ridges that simulate thread\n * texture on the fabric surface for enhanced realism.\n *\n * @param config - Fabric texture configuration\n * @returns CanvasTexture normal map\n *\n * @korean 법선맵생성\n */\nexport const generateFabricNormalMap = (\n config: FabricTextureConfig,\n): THREE.CanvasTexture => {\n // Return fallback for test environments without canvas support\n if (!canCreateCanvas()) {\n return createFallbackTexture();\n }\n\n const { weaveDensity, resolution } = config;\n\n const canvas = document.createElement(\"canvas\");\n canvas.width = resolution;\n canvas.height = resolution;\n const ctx = canvas.getContext(\"2d\");\n\n if (!ctx) {\n return createFallbackTexture();\n }\n\n // Neutral normal (pointing straight out)\n ctx.fillStyle = \"rgb(128, 128, 255)\";\n ctx.fillRect(0, 0, resolution, resolution);\n\n const threadWidth = resolution / weaveDensity;\n\n // Create normal variations for weave pattern\n for (let y = 0; y < weaveDensity; y++) {\n for (let x = 0; x < weaveDensity; x++) {\n const xPos = x * threadWidth;\n const yPos = y * threadWidth;\n\n // Create subtle normal variation based on weave position\n if ((x + y) % 2 === 0) {\n // Horizontal thread - slight upward normal\n ctx.fillStyle = \"rgb(128, 140, 255)\";\n ctx.fillRect(xPos, yPos, threadWidth * 0.9, threadWidth * 0.4);\n } else {\n // Vertical thread - slight rightward normal\n ctx.fillStyle = \"rgb(140, 128, 255)\";\n ctx.fillRect(xPos, yPos, threadWidth * 0.4, threadWidth * 0.9);\n }\n\n // Thread edge highlights\n const edgeIntensity = 20;\n ctx.fillStyle = `rgb(${128 + edgeIntensity}, ${128 + edgeIntensity}, 255)`;\n ctx.fillRect(xPos, yPos, threadWidth * 0.1, threadWidth);\n ctx.fillRect(xPos, yPos, threadWidth, threadWidth * 0.1);\n }\n }\n\n const texture = new THREE.CanvasTexture(canvas);\n texture.wrapS = THREE.RepeatWrapping;\n texture.wrapT = THREE.RepeatWrapping;\n texture.repeat.set(4, 4);\n texture.needsUpdate = true;\n\n return texture;\n};\n\n/**\n * Generate a roughness map for fabric material variation\n *\n * Creates subtle roughness variations that simulate the\n * different reflective properties of woven threads.\n *\n * @param config - Fabric texture configuration\n * @returns CanvasTexture roughness map\n *\n * @korean 거칠기맵생성\n */\nexport const generateFabricRoughnessMap = (\n config: FabricTextureConfig,\n): THREE.CanvasTexture => {\n // Return fallback for test environments without canvas support\n if (!canCreateCanvas()) {\n return createFallbackTexture();\n }\n\n const { weaveDensity, resolution, threadVariation } = config;\n\n const canvas = document.createElement(\"canvas\");\n canvas.width = resolution;\n canvas.height = resolution;\n const ctx = canvas.getContext(\"2d\");\n\n if (!ctx) {\n return createFallbackTexture();\n }\n\n // Base roughness (white = rough, black = smooth)\n ctx.fillStyle = \"rgb(180, 180, 180)\";\n ctx.fillRect(0, 0, resolution, resolution);\n\n const threadWidth = resolution / weaveDensity;\n\n // Create roughness variation for weave pattern\n for (let y = 0; y < weaveDensity; y++) {\n for (let x = 0; x < weaveDensity; x++) {\n const xPos = x * threadWidth;\n const yPos = y * threadWidth;\n\n // Random roughness variation per thread\n const variation = seededRandom(x * 13 + y * 29) * threadVariation * 50;\n const roughness = Math.max(128, Math.min(230, 180 + variation));\n\n ctx.fillStyle = `rgb(${roughness}, ${roughness}, ${roughness})`;\n ctx.fillRect(xPos, yPos, threadWidth * 0.9, threadWidth * 0.9);\n\n // Thread gaps are slightly smoother\n ctx.fillStyle = \"rgb(160, 160, 160)\";\n ctx.fillRect(\n xPos + threadWidth * 0.9,\n yPos,\n threadWidth * 0.1,\n threadWidth,\n );\n ctx.fillRect(\n xPos,\n yPos + threadWidth * 0.9,\n threadWidth,\n threadWidth * 0.1,\n );\n }\n }\n\n const texture = new THREE.CanvasTexture(canvas);\n texture.wrapS = THREE.RepeatWrapping;\n texture.wrapT = THREE.RepeatWrapping;\n texture.repeat.set(4, 4);\n texture.needsUpdate = true;\n\n return texture;\n};\n\n/**\n * Generate complete fabric texture set with all maps\n *\n * Creates a set of textures (color, normal, roughness) for\n * realistic fabric rendering with proper memory management.\n *\n * @param baseColor - Base color for the fabric (hex string)\n * @param preset - Preset name or custom config\n * @returns FabricTextureSet with all generated textures\n *\n * @example\n * ```typescript\n * const textures = generateFabricTextureSet(\"#2d2d2d\", \"dobok\");\n *\n * const material = new THREE.MeshPhysicalMaterial({\n * map: textures.colorMap,\n * normalMap: textures.normalMap,\n * roughnessMap: textures.roughnessMap,\n * });\n *\n * // Cleanup when done\n * textures.dispose();\n * ```\n *\n * @korean 완전직물텍스처세트생성\n */\nexport const generateFabricTextureSet = (\n baseColor: string,\n preset: keyof typeof FABRIC_PRESETS | Partial<FabricTextureConfig> = \"dobok\",\n): FabricTextureSet => {\n // Merge preset with defaults\n const presetConfig =\n typeof preset === \"string\" ? FABRIC_PRESETS[preset] : preset;\n\n const config: FabricTextureConfig = {\n baseColor,\n weaveDensity: presetConfig?.weaveDensity ?? 32,\n resolution: presetConfig?.resolution ?? 256,\n threadVariation: presetConfig?.threadVariation ?? 0.15,\n generateNormalMap: presetConfig?.generateNormalMap ?? true,\n generateRoughnessMap: presetConfig?.generateRoughnessMap ?? true,\n };\n\n // Generate textures\n const colorMap = generateFabricColorMap(config);\n const normalMap = config.generateNormalMap\n ? generateFabricNormalMap(config)\n : undefined;\n const roughnessMap = config.generateRoughnessMap\n ? generateFabricRoughnessMap(config)\n : undefined;\n\n // Cleanup function\n const dispose = () => {\n colorMap.dispose();\n normalMap?.dispose();\n roughnessMap?.dispose();\n };\n\n return {\n colorMap,\n normalMap,\n roughnessMap,\n dispose,\n };\n};\n\n/**\n * Pre-defined dobok colors for Korean martial arts uniforms\n */\nexport const DOBOK_COLORS = {\n /** Traditional white dobok (흰 도복) */\n WHITE: \"#f5f5f5\",\n /** Black dobok for masters (검정 도복) */\n BLACK: \"#1a1a1a\",\n /** Navy blue tactical (남색) */\n NAVY: \"#1a2744\",\n /** Traditional Korean gray (회색) */\n GRAY: \"#2d2d2d\",\n /** Dark red for elite (암적색) */\n DARK_RED: \"#4a1a1a\",\n /** Cyber cyan accent */\n CYBER_CYAN: \"#003333\",\n} as const;\n\nexport default generateFabricTextureSet;\n"],"mappings":"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"}
1
+ {"version":3,"file":"fabricTextures.js","names":[],"sources":["../../src/utils/fabricTextures.ts"],"sourcesContent":["/**\n * Procedural fabric texture generation for realistic clothing\n *\n * **Korean**: 절차적 직물 텍스처 (Procedural Fabric Textures)\n *\n * Generates canvas-based textures for realistic dobok (도복) martial arts\n * uniform rendering without external image assets. Uses Three.js CanvasTexture\n * for efficient GPU-based texture mapping.\n *\n * **Features**:\n * - Procedural weave patterns for fabric realism\n * - Normal maps for surface detail and depth\n * - Roughness maps for material variation\n * - Memory-safe with proper cleanup\n *\n * @module utils/fabricTextures\n * @category Visual Effects\n * @korean 직물텍스처유틸\n */\n\nimport * as THREE from \"three\";\n\n/**\n * Fabric texture configuration\n */\nexport interface FabricTextureConfig {\n /** Base color for the fabric */\n readonly baseColor: string;\n /** Weave pattern density (higher = finer weave) */\n readonly weaveDensity: number;\n /** Texture resolution (power of 2 recommended) */\n readonly resolution: number;\n /** Thread variation intensity (0-1) */\n readonly threadVariation: number;\n /** Whether to generate normal map */\n readonly generateNormalMap: boolean;\n /** Whether to generate roughness map */\n readonly generateRoughnessMap: boolean;\n}\n\n/**\n * Generated fabric texture set\n */\nexport interface FabricTextureSet {\n /** Color/diffuse map */\n readonly colorMap: THREE.CanvasTexture;\n /** Normal map for surface detail (optional) */\n readonly normalMap?: THREE.CanvasTexture;\n /** Roughness map for material variation (optional) */\n readonly roughnessMap?: THREE.CanvasTexture;\n /** Cleanup function to dispose all textures */\n readonly dispose: () => void;\n}\n\n/**\n * Default fabric texture configurations for different materials\n */\nexport const FABRIC_PRESETS: Record<string, Partial<FabricTextureConfig>> = {\n /** Traditional cotton dobok (도복) */\n dobok: {\n weaveDensity: 32,\n threadVariation: 0.15,\n resolution: 256,\n generateNormalMap: true,\n generateRoughnessMap: true,\n },\n /** Tactical synthetic fabric */\n tactical: {\n weaveDensity: 48,\n threadVariation: 0.08,\n resolution: 256,\n generateNormalMap: true,\n generateRoughnessMap: true,\n },\n /** Leather material */\n leather: {\n weaveDensity: 16,\n threadVariation: 0.25,\n resolution: 256,\n generateNormalMap: true,\n generateRoughnessMap: false,\n },\n /** Silk/satin material */\n silk: {\n weaveDensity: 64,\n threadVariation: 0.05,\n resolution: 256,\n generateNormalMap: true,\n generateRoughnessMap: true,\n },\n};\n\n/**\n * Convert hex color to RGB components\n */\nconst hexToRgb = (hex: string): { r: number; g: number; b: number } => {\n const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n return result\n ? {\n r: parseInt(result[1], 16),\n g: parseInt(result[2], 16),\n b: parseInt(result[3], 16),\n }\n : { r: 128, g: 128, b: 128 };\n};\n\n/**\n * Simple seeded random for reproducible textures\n */\nconst seededRandom = (seed: number): number => {\n const x = Math.sin(seed) * 10000;\n return x - Math.floor(x);\n};\n\n/**\n * Check if we're in a browser environment with canvas support\n */\nconst canCreateCanvas = (): boolean => {\n if (typeof document === \"undefined\") return false;\n try {\n const canvas = document.createElement(\"canvas\");\n return canvas.getContext(\"2d\") !== null;\n } catch {\n return false;\n }\n};\n\n/**\n * Create a fallback texture for test environments\n * Uses a minimal texture that works in all environments\n */\nconst createFallbackTexture = (): THREE.CanvasTexture => {\n // In test environments, THREE mocks may not have all features\n // Create a minimal mock that satisfies the interface\n try {\n const data = new Uint8Array([128, 128, 128, 255]);\n const dataTexture = new THREE.DataTexture(data, 1, 1, THREE.RGBAFormat);\n dataTexture.needsUpdate = true;\n return dataTexture as unknown as THREE.CanvasTexture;\n } catch {\n // Ultimate fallback - create a minimal mock texture object\n // Use numeric constants instead of THREE constants for test compatibility\n const mockTexture = {\n dispose: () => {},\n needsUpdate: true,\n wrapS: 1000, // THREE.RepeatWrapping value\n wrapT: 1000,\n repeat: { set: () => {} },\n uuid: \"mock-fabric-texture\",\n isTexture: true,\n };\n return mockTexture as unknown as THREE.CanvasTexture;\n }\n};\n\n/**\n * Generate a procedural fabric weave color map\n *\n * Creates a canvas-based texture with realistic thread patterns\n * simulating woven fabric like cotton dobok material.\n *\n * @param config - Fabric texture configuration\n * @returns CanvasTexture with weave pattern\n *\n * @korean 직물색상맵생성\n */\nexport const generateFabricColorMap = (\n config: FabricTextureConfig,\n): THREE.CanvasTexture => {\n // Return fallback for test environments without canvas support\n if (!canCreateCanvas()) {\n return createFallbackTexture();\n }\n\n const { baseColor, weaveDensity, resolution, threadVariation } = config;\n\n const canvas = document.createElement(\"canvas\");\n canvas.width = resolution;\n canvas.height = resolution;\n const ctx = canvas.getContext(\"2d\");\n\n if (!ctx) {\n return createFallbackTexture();\n }\n\n const rgb = hexToRgb(baseColor);\n\n // Fill base color\n ctx.fillStyle = baseColor;\n ctx.fillRect(0, 0, resolution, resolution);\n\n // Create weave pattern\n const threadWidth = resolution / weaveDensity;\n\n // Horizontal threads (weft)\n for (let y = 0; y < weaveDensity; y++) {\n const yPos = y * threadWidth;\n const variation = (seededRandom(y * 17) - 0.5) * threadVariation * 255;\n\n const r = Math.max(0, Math.min(255, rgb.r + variation));\n const g = Math.max(0, Math.min(255, rgb.g + variation));\n const b = Math.max(0, Math.min(255, rgb.b + variation));\n\n ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;\n\n // Draw thread with slight offset pattern\n for (let x = 0; x < weaveDensity; x++) {\n const xPos = x * threadWidth;\n // Alternating over/under pattern\n if ((x + y) % 2 === 0) {\n ctx.fillRect(xPos, yPos, threadWidth * 0.95, threadWidth * 0.45);\n }\n }\n }\n\n // Vertical threads (warp)\n for (let x = 0; x < weaveDensity; x++) {\n const xPos = x * threadWidth;\n const variation = (seededRandom(x * 31) - 0.5) * threadVariation * 255;\n\n const r = Math.max(0, Math.min(255, rgb.r + variation * 0.8));\n const g = Math.max(0, Math.min(255, rgb.g + variation * 0.8));\n const b = Math.max(0, Math.min(255, rgb.b + variation * 0.8));\n\n ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;\n\n for (let y = 0; y < weaveDensity; y++) {\n const yPos = y * threadWidth;\n // Opposite pattern for weave effect\n if ((x + y) % 2 === 1) {\n ctx.fillRect(xPos, yPos, threadWidth * 0.45, threadWidth * 0.95);\n }\n }\n }\n\n const texture = new THREE.CanvasTexture(canvas);\n texture.wrapS = THREE.RepeatWrapping;\n texture.wrapT = THREE.RepeatWrapping;\n texture.repeat.set(4, 4); // Tile the texture\n texture.needsUpdate = true;\n\n return texture;\n};\n\n/**\n * Generate a normal map for fabric surface detail\n *\n * Creates subtle bumps and ridges that simulate thread\n * texture on the fabric surface for enhanced realism.\n *\n * @param config - Fabric texture configuration\n * @returns CanvasTexture normal map\n *\n * @korean 법선맵생성\n */\nexport const generateFabricNormalMap = (\n config: FabricTextureConfig,\n): THREE.CanvasTexture => {\n // Return fallback for test environments without canvas support\n if (!canCreateCanvas()) {\n return createFallbackTexture();\n }\n\n const { weaveDensity, resolution } = config;\n\n const canvas = document.createElement(\"canvas\");\n canvas.width = resolution;\n canvas.height = resolution;\n const ctx = canvas.getContext(\"2d\");\n\n if (!ctx) {\n return createFallbackTexture();\n }\n\n // Neutral normal (pointing straight out)\n ctx.fillStyle = \"rgb(128, 128, 255)\";\n ctx.fillRect(0, 0, resolution, resolution);\n\n const threadWidth = resolution / weaveDensity;\n\n // Create normal variations for weave pattern\n for (let y = 0; y < weaveDensity; y++) {\n for (let x = 0; x < weaveDensity; x++) {\n const xPos = x * threadWidth;\n const yPos = y * threadWidth;\n\n // Create subtle normal variation based on weave position\n if ((x + y) % 2 === 0) {\n // Horizontal thread - slight upward normal\n ctx.fillStyle = \"rgb(128, 140, 255)\";\n ctx.fillRect(xPos, yPos, threadWidth * 0.9, threadWidth * 0.4);\n } else {\n // Vertical thread - slight rightward normal\n ctx.fillStyle = \"rgb(140, 128, 255)\";\n ctx.fillRect(xPos, yPos, threadWidth * 0.4, threadWidth * 0.9);\n }\n\n // Thread edge highlights\n const edgeIntensity = 20;\n ctx.fillStyle = `rgb(${128 + edgeIntensity}, ${128 + edgeIntensity}, 255)`;\n ctx.fillRect(xPos, yPos, threadWidth * 0.1, threadWidth);\n ctx.fillRect(xPos, yPos, threadWidth, threadWidth * 0.1);\n }\n }\n\n const texture = new THREE.CanvasTexture(canvas);\n texture.wrapS = THREE.RepeatWrapping;\n texture.wrapT = THREE.RepeatWrapping;\n texture.repeat.set(4, 4);\n texture.needsUpdate = true;\n\n return texture;\n};\n\n/**\n * Generate a roughness map for fabric material variation\n *\n * Creates subtle roughness variations that simulate the\n * different reflective properties of woven threads.\n *\n * @param config - Fabric texture configuration\n * @returns CanvasTexture roughness map\n *\n * @korean 거칠기맵생성\n */\nexport const generateFabricRoughnessMap = (\n config: FabricTextureConfig,\n): THREE.CanvasTexture => {\n // Return fallback for test environments without canvas support\n if (!canCreateCanvas()) {\n return createFallbackTexture();\n }\n\n const { weaveDensity, resolution, threadVariation } = config;\n\n const canvas = document.createElement(\"canvas\");\n canvas.width = resolution;\n canvas.height = resolution;\n const ctx = canvas.getContext(\"2d\");\n\n if (!ctx) {\n return createFallbackTexture();\n }\n\n // Base roughness (white = rough, black = smooth)\n ctx.fillStyle = \"rgb(180, 180, 180)\";\n ctx.fillRect(0, 0, resolution, resolution);\n\n const threadWidth = resolution / weaveDensity;\n\n // Create roughness variation for weave pattern\n for (let y = 0; y < weaveDensity; y++) {\n for (let x = 0; x < weaveDensity; x++) {\n const xPos = x * threadWidth;\n const yPos = y * threadWidth;\n\n // Random roughness variation per thread\n const variation = seededRandom(x * 13 + y * 29) * threadVariation * 50;\n const roughness = Math.max(128, Math.min(230, 180 + variation));\n\n ctx.fillStyle = `rgb(${roughness}, ${roughness}, ${roughness})`;\n ctx.fillRect(xPos, yPos, threadWidth * 0.9, threadWidth * 0.9);\n\n // Thread gaps are slightly smoother\n ctx.fillStyle = \"rgb(160, 160, 160)\";\n ctx.fillRect(\n xPos + threadWidth * 0.9,\n yPos,\n threadWidth * 0.1,\n threadWidth,\n );\n ctx.fillRect(\n xPos,\n yPos + threadWidth * 0.9,\n threadWidth,\n threadWidth * 0.1,\n );\n }\n }\n\n const texture = new THREE.CanvasTexture(canvas);\n texture.wrapS = THREE.RepeatWrapping;\n texture.wrapT = THREE.RepeatWrapping;\n texture.repeat.set(4, 4);\n texture.needsUpdate = true;\n\n return texture;\n};\n\n/**\n * Generate complete fabric texture set with all maps\n *\n * Creates a set of textures (color, normal, roughness) for\n * realistic fabric rendering with proper memory management.\n *\n * @param baseColor - Base color for the fabric (hex string)\n * @param preset - Preset name or custom config\n * @returns FabricTextureSet with all generated textures\n *\n * @example\n * ```typescript\n * const textures = generateFabricTextureSet(\"#2d2d2d\", \"dobok\");\n *\n * const material = new THREE.MeshPhysicalMaterial({\n * map: textures.colorMap,\n * normalMap: textures.normalMap,\n * roughnessMap: textures.roughnessMap,\n * });\n *\n * // Cleanup when done\n * textures.dispose();\n * ```\n *\n * @korean 완전직물텍스처세트생성\n */\nexport const generateFabricTextureSet = (\n baseColor: string,\n preset: keyof typeof FABRIC_PRESETS | Partial<FabricTextureConfig> = \"dobok\",\n): FabricTextureSet => {\n // Merge preset with defaults\n const presetConfig =\n typeof preset === \"string\" ? FABRIC_PRESETS[preset] : preset;\n\n const config: FabricTextureConfig = {\n baseColor,\n weaveDensity: presetConfig?.weaveDensity ?? 32,\n resolution: presetConfig?.resolution ?? 256,\n threadVariation: presetConfig?.threadVariation ?? 0.15,\n generateNormalMap: presetConfig?.generateNormalMap ?? true,\n generateRoughnessMap: presetConfig?.generateRoughnessMap ?? true,\n };\n\n // Generate textures\n const colorMap = generateFabricColorMap(config);\n const normalMap = config.generateNormalMap\n ? generateFabricNormalMap(config)\n : undefined;\n const roughnessMap = config.generateRoughnessMap\n ? generateFabricRoughnessMap(config)\n : undefined;\n\n // Cleanup function\n const dispose = () => {\n colorMap.dispose();\n normalMap?.dispose();\n roughnessMap?.dispose();\n };\n\n return {\n colorMap,\n normalMap,\n roughnessMap,\n dispose,\n };\n};\n\n/**\n * Pre-defined dobok colors for Korean martial arts uniforms\n */\nexport const DOBOK_COLORS = {\n /** Traditional white dobok (흰 도복) */\n WHITE: \"#f5f5f5\",\n /** Black dobok for masters (검정 도복) */\n BLACK: \"#1a1a1a\",\n /** Navy blue tactical (남색) */\n NAVY: \"#1a2744\",\n /** Traditional Korean gray (회색) */\n GRAY: \"#2d2d2d\",\n /** Dark red for elite (암적색) */\n DARK_RED: \"#4a1a1a\",\n /** Cyber cyan accent */\n CYBER_CYAN: \"#003333\",\n} as const;\n\nexport default generateFabricTextureSet;\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blacktrigram",
3
- "version": "0.7.51",
3
+ "version": "0.7.52",
4
4
  "description": "Black Trigram (흑괘) - Korean Martial Arts Combat Simulator. Reusable game systems, combat mechanics, animation framework, and Korean martial arts data built with React, Three.js, and TypeScript.",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -187,7 +187,7 @@
187
187
  "three": "0.184.0"
188
188
  },
189
189
  "devDependencies": {
190
- "@aws-sdk/client-bedrock-runtime": "3.1050.0",
190
+ "@aws-sdk/client-bedrock-runtime": "3.1051.0",
191
191
  "@eslint/js": "10.0.1",
192
192
  "@react-three/drei": "10.7.7",
193
193
  "@react-three/fiber": "9.6.1",
@@ -201,8 +201,8 @@
201
201
  "@types/react-dom": "19.2.3",
202
202
  "@types/three": "0.184.1",
203
203
  "@vitejs/plugin-react": "6.0.2",
204
- "@vitest/coverage-v8": "4.1.6",
205
- "@vitest/ui": "4.1.6",
204
+ "@vitest/coverage-v8": "4.1.7",
205
+ "@vitest/ui": "4.1.7",
206
206
  "cypress": "15.15.0",
207
207
  "cypress-junit-reporter": "1.3.1",
208
208
  "cypress-multi-reporters": "2.0.5",
@@ -238,10 +238,10 @@
238
238
  "typedoc-plugin-missing-exports": "4.1.3",
239
239
  "typescript": "6.0.3",
240
240
  "typescript-eslint": "8.59.4",
241
- "vite": "8.0.13",
241
+ "vite": "8.0.14",
242
242
  "vite-bundle-analyzer": "1.3.8",
243
243
  "vite-tsconfig-paths": "6.1.1",
244
- "vitest": "4.1.6"
244
+ "vitest": "4.1.7"
245
245
  },
246
246
  "overrides": {
247
247
  "eslint-plugin-react-hooks": {