blacktrigram 0.7.81 → 0.7.83
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/components/screens/combat/CombatScreen3D.js +16 -11
- package/lib/components/screens/combat/CombatScreen3D.js.map +1 -1
- package/lib/components/screens/combat/components/effects/ParticleAudio3D.js +2 -1
- package/lib/components/screens/combat/components/effects/ParticleAudio3D.js.map +1 -1
- package/lib/components/screens/combat/hooks/useAICombat.js +2 -1
- package/lib/components/screens/combat/hooks/useAICombat.js.map +1 -1
- package/lib/components/screens/combat/hooks/useCombatActions.js +25 -9
- package/lib/components/screens/combat/hooks/useCombatActions.js.map +1 -1
- package/lib/components/screens/combat/hooks/useCombatAttackMovement.js +10 -6
- package/lib/components/screens/combat/hooks/useCombatAttackMovement.js.map +1 -1
- package/lib/components/screens/intro/IntroScreen3D.js +8 -4
- package/lib/components/screens/intro/IntroScreen3D.js.map +1 -1
- package/lib/components/screens/training/TrainingScreen3D.js +5 -3
- package/lib/components/screens/training/TrainingScreen3D.js.map +1 -1
- package/lib/components/screens/training/hooks/useAttackMovement.js +2 -1
- package/lib/components/screens/training/hooks/useAttackMovement.js.map +1 -1
- package/lib/components/screens/training/hooks/useTrainingActions.js +2 -1
- package/lib/components/screens/training/hooks/useTrainingActions.js.map +1 -1
- package/lib/components/shared/three/effects/ActionFeedback.js +2 -1
- package/lib/components/shared/three/effects/ActionFeedback.js.map +1 -1
- package/lib/components/shared/three/effects/DamageNumbers.js +2 -1
- package/lib/components/shared/three/effects/DamageNumbers.js.map +1 -1
- package/lib/components/shared/three/effects/HitEffects3D.js +2 -1
- package/lib/components/shared/three/effects/HitEffects3D.js.map +1 -1
- package/lib/components/shared/ui/SplashScreen.js +2 -2
- package/lib/hooks/useMuscleActivation.js +4 -1
- package/lib/hooks/useMuscleActivation.js.map +1 -1
- package/lib/hooks/useRoundTransition.js +2 -1
- package/lib/hooks/useRoundTransition.js.map +1 -1
- package/lib/hooks/useSkeletalAnimation.js +1 -1
- package/lib/index.js +2 -2
- package/lib/systems/VitalPointSystem.js +2 -1
- package/lib/systems/VitalPointSystem.js.map +1 -1
- package/lib/systems/animation/core/AnimationStateMachine.js +1 -1
- package/lib/systems/index.js +2 -2
- package/lib/utils/inputSystem.js +2 -1
- package/lib/utils/inputSystem.js.map +1 -1
- package/package.json +7 -7
|
@@ -55,7 +55,8 @@ var SingleDamageNumber = React.memo(({ damage, isMobile, arenaBounds, animationD
|
|
|
55
55
|
];
|
|
56
56
|
useFrame(() => {
|
|
57
57
|
const elapsed = Date.now() - startTimeRef.current;
|
|
58
|
-
|
|
58
|
+
const newProgress = Math.min(elapsed / animationDuration, 1);
|
|
59
|
+
setProgress(newProgress);
|
|
59
60
|
});
|
|
60
61
|
if (progress >= 1) return null;
|
|
61
62
|
const opacity = 1 - progress;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DamageNumbers.js","names":[],"sources":["../../../../../src/components/shared/three/effects/DamageNumbers.tsx"],"sourcesContent":["/**\n * DamageNumbers - Floating damage number display component\n *\n * Displays floating damage numbers that animate upward and fade out.\n * Color-coded based on damage type: normal (cyan), critical (gold), vital (red).\n *\n * Uses Html overlays from @react-three/drei for rendering within 3D scenes.\n * Performance optimized with React.memo to reduce unnecessary re-renders.\n *\n * @module components/shared/three/effects/DamageNumbers\n * @category Shared Effects\n * @korean 피해숫자\n */\n\nimport { Html } from \"@react-three/drei\";\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useMemo, useRef, useState } from \"react\";\nimport { DamageNumber, DamageType } from \"../../../../hooks/useActionFeedback\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { DEFAULT_PHYSICS_ARENA_BOUNDS, type PhysicsArenaBounds } from \"../../../../types/PhysicsTypes\";\nimport { hexColorToCSS, hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport { withGPUAcceleration } from \"../../../../utils/performanceOptimization\";\n\n/**\n * Props for the DamageNumbers component\n */\nexport interface DamageNumbersProps {\n /** Array of damage numbers to display */\n readonly damages: readonly DamageNumber[];\n /** Whether to use mobile-optimized sizing */\n readonly isMobile?: boolean;\n /** Arena bounds for 3D positioning (physics-first with meter dimensions) */\n readonly arenaBounds?: PhysicsArenaBounds;\n /** Duration of animation in ms (default: 1500) */\n readonly animationDuration?: number;\n}\n\n/**\n * Get color based on damage type\n */\nfunction getDamageColor(type: DamageType): string {\n switch (type) {\n case \"critical\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD);\n case \"vital\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_RED);\n case \"normal\":\n default:\n return hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN);\n }\n}\n\n/**\n * Get glow color based on damage type\n */\nfunction getGlowColor(type: DamageType): string {\n switch (type) {\n case \"critical\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.8);\n case \"vital\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_RED, 0.8);\n case \"normal\":\n default:\n return hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.8);\n }\n}\n\n/**\n * Individual damage number display\n * Memoized to prevent unnecessary re-renders\n */\ninterface SingleDamageNumberProps {\n readonly damage: DamageNumber;\n readonly isMobile: boolean;\n readonly arenaBounds: PhysicsArenaBounds;\n readonly animationDuration: number;\n}\n\nconst SingleDamageNumber = React.memo<SingleDamageNumberProps>(({\n damage,\n isMobile,\n arenaBounds,\n animationDuration,\n}) => {\n const [progress, setProgress] = useState(0);\n const startTimeRef = useRef(damage.timestamp);\n\n const halfWidth = arenaBounds.worldWidthMeters / 2;\n const halfDepth = arenaBounds.worldDepthMeters / 2;\n \n const clampedX = Math.min(halfWidth, Math.max(-halfWidth, damage.position.x));\n const clampedZ = Math.min(halfDepth, Math.max(-halfDepth, damage.position.y));\n \n const x = clampedX; // Meter position X\n const y = 2 + progress * 2; // Float upward\n const z = clampedZ; // Meter position Z (depth)\n const position3D: [number, number, number] = [x, y, z];\n\n useFrame(() => {\n const elapsed = Date.now() - startTimeRef.current;\n const newProgress = Math.min(elapsed / animationDuration, 1);\n setProgress(newProgress);\n });\n\n if (progress >= 1) return null;\n\n const opacity = 1 - progress;\n const scale = 1 + progress * 0.3; // Slight scale up during animation\n const fontSize = isMobile ? 20 : 28;\n const getCriticalBonus = (): number => {\n if (damage.type === \"critical\") return 8;\n if (damage.type === \"vital\") return 4;\n return 0;\n };\n const criticalBonus = getCriticalBonus();\n\n return (\n <Html\n position={position3D}\n center\n distanceFactor={10}\n style={{ pointerEvents: \"none\" }}\n >\n <div\n data-testid={`damage-${damage.id}`}\n style={withGPUAcceleration({\n fontSize: `${fontSize + criticalBonus}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: getDamageColor(damage.type),\n opacity,\n transform: `scale(${scale})`,\n textShadow: `\n 0 0 10px ${getGlowColor(damage.type)},\n 0 0 20px ${getGlowColor(damage.type)},\n 2px 2px 4px rgba(0, 0, 0, 0.8)\n `,\n whiteSpace: \"nowrap\",\n userSelect: \"none\",\n })}\n >\n {damage.damage}\n {damage.type === \"critical\" && \"!\"}\n {damage.type === \"vital\" && \"!!\"}\n </div>\n </Html>\n );\n}, (prevProps, nextProps) => {\n const prevArena = prevProps.arenaBounds;\n const nextArena = nextProps.arenaBounds;\n\n const sameArenaBounds =\n prevArena?.x === nextArena?.x &&\n prevArena?.y === nextArena?.y &&\n prevArena?.width === nextArena?.width &&\n prevArena?.height === nextArena?.height &&\n prevArena?.worldWidthMeters === nextArena?.worldWidthMeters &&\n prevArena?.worldDepthMeters === nextArena?.worldDepthMeters;\n\n return (\n prevProps.damage.id === nextProps.damage.id &&\n prevProps.isMobile === nextProps.isMobile &&\n prevProps.animationDuration === nextProps.animationDuration &&\n sameArenaBounds\n );\n});\n\nSingleDamageNumber.displayName = \"SingleDamageNumber\";\n\n/**\n * DamageNumbers Component\n *\n * Renders multiple floating damage numbers in the 3D scene.\n * Each number floats upward and fades out over time.\n * Performance optimized with React.memo.\n *\n * @example\n * ```tsx\n * <DamageNumbers\n * damages={damageNumbers}\n * isMobile={isMobile}\n * arenaBounds={arenaBounds}\n * />\n * ```\n */\nconst DamageNumbersComponent: React.FC<DamageNumbersProps> = ({\n damages,\n isMobile = false,\n arenaBounds = DEFAULT_PHYSICS_ARENA_BOUNDS,\n animationDuration = 1500,\n}) => {\n const visibleDamages = useMemo(() => [...damages], [damages]);\n\n return (\n <group data-testid=\"damage-numbers-container\">\n {visibleDamages.map((damage) => (\n <SingleDamageNumber\n key={damage.id}\n damage={damage}\n isMobile={isMobile}\n arenaBounds={arenaBounds}\n animationDuration={animationDuration}\n />\n ))}\n </group>\n );\n};\n\n/**\n * Memoized DamageNumbers with custom comparison\n * Only re-renders when damage array changes\n */\nexport const DamageNumbers = React.memo(\n DamageNumbersComponent,\n (prevProps, nextProps) => {\n if (prevProps.damages.length !== nextProps.damages.length) {\n return false;\n }\n \n for (let i = 0; i < prevProps.damages.length; i++) {\n if (prevProps.damages[i].id !== nextProps.damages[i].id) {\n return false;\n }\n }\n \n return (\n prevProps.isMobile === nextProps.isMobile &&\n prevProps.animationDuration === nextProps.animationDuration &&\n prevProps.arenaBounds?.x === nextProps.arenaBounds?.x &&\n prevProps.arenaBounds?.y === nextProps.arenaBounds?.y &&\n prevProps.arenaBounds?.width === nextProps.arenaBounds?.width &&\n prevProps.arenaBounds?.height === nextProps.arenaBounds?.height\n );\n }\n);\n\nDamageNumbers.displayName = \"DamageNumbers\";\n\nexport default DamageNumbers;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"DamageNumbers.js","names":[],"sources":["../../../../../src/components/shared/three/effects/DamageNumbers.tsx"],"sourcesContent":["/**\n * DamageNumbers - Floating damage number display component\n *\n * Displays floating damage numbers that animate upward and fade out.\n * Color-coded based on damage type: normal (cyan), critical (gold), vital (red).\n *\n * Uses Html overlays from @react-three/drei for rendering within 3D scenes.\n * Performance optimized with React.memo to reduce unnecessary re-renders.\n *\n * @module components/shared/three/effects/DamageNumbers\n * @category Shared Effects\n * @korean 피해숫자\n */\n\nimport { Html } from \"@react-three/drei\";\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useMemo, useRef, useState } from \"react\";\nimport { DamageNumber, DamageType } from \"../../../../hooks/useActionFeedback\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { DEFAULT_PHYSICS_ARENA_BOUNDS, type PhysicsArenaBounds } from \"../../../../types/PhysicsTypes\";\nimport { hexColorToCSS, hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport { withGPUAcceleration } from \"../../../../utils/performanceOptimization\";\n\n/**\n * Props for the DamageNumbers component\n */\nexport interface DamageNumbersProps {\n /** Array of damage numbers to display */\n readonly damages: readonly DamageNumber[];\n /** Whether to use mobile-optimized sizing */\n readonly isMobile?: boolean;\n /** Arena bounds for 3D positioning (physics-first with meter dimensions) */\n readonly arenaBounds?: PhysicsArenaBounds;\n /** Duration of animation in ms (default: 1500) */\n readonly animationDuration?: number;\n}\n\n/**\n * Get color based on damage type\n */\nfunction getDamageColor(type: DamageType): string {\n switch (type) {\n case \"critical\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD);\n case \"vital\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_RED);\n case \"normal\":\n default:\n return hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN);\n }\n}\n\n/**\n * Get glow color based on damage type\n */\nfunction getGlowColor(type: DamageType): string {\n switch (type) {\n case \"critical\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.8);\n case \"vital\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_RED, 0.8);\n case \"normal\":\n default:\n return hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.8);\n }\n}\n\n/**\n * Individual damage number display\n * Memoized to prevent unnecessary re-renders\n */\ninterface SingleDamageNumberProps {\n readonly damage: DamageNumber;\n readonly isMobile: boolean;\n readonly arenaBounds: PhysicsArenaBounds;\n readonly animationDuration: number;\n}\n\nconst SingleDamageNumber = React.memo<SingleDamageNumberProps>(({\n damage,\n isMobile,\n arenaBounds,\n animationDuration,\n}) => {\n const [progress, setProgress] = useState(0);\n const startTimeRef = useRef(damage.timestamp);\n\n const halfWidth = arenaBounds.worldWidthMeters / 2;\n const halfDepth = arenaBounds.worldDepthMeters / 2;\n \n const clampedX = Math.min(halfWidth, Math.max(-halfWidth, damage.position.x));\n const clampedZ = Math.min(halfDepth, Math.max(-halfDepth, damage.position.y));\n \n const x = clampedX; // Meter position X\n const y = 2 + progress * 2; // Float upward\n const z = clampedZ; // Meter position Z (depth)\n const position3D: [number, number, number] = [x, y, z];\n\n useFrame(() => {\n const elapsed = Date.now() - startTimeRef.current;\n const newProgress = Math.min(elapsed / animationDuration, 1);\n setProgress(newProgress);\n });\n\n if (progress >= 1) return null;\n\n const opacity = 1 - progress;\n const scale = 1 + progress * 0.3; // Slight scale up during animation\n const fontSize = isMobile ? 20 : 28;\n const getCriticalBonus = (): number => {\n if (damage.type === \"critical\") return 8;\n if (damage.type === \"vital\") return 4;\n return 0;\n };\n const criticalBonus = getCriticalBonus();\n\n return (\n <Html\n position={position3D}\n center\n distanceFactor={10}\n style={{ pointerEvents: \"none\" }}\n >\n <div\n data-testid={`damage-${damage.id}`}\n style={withGPUAcceleration({\n fontSize: `${fontSize + criticalBonus}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: getDamageColor(damage.type),\n opacity,\n transform: `scale(${scale})`,\n textShadow: `\n 0 0 10px ${getGlowColor(damage.type)},\n 0 0 20px ${getGlowColor(damage.type)},\n 2px 2px 4px rgba(0, 0, 0, 0.8)\n `,\n whiteSpace: \"nowrap\",\n userSelect: \"none\",\n })}\n >\n {damage.damage}\n {damage.type === \"critical\" && \"!\"}\n {damage.type === \"vital\" && \"!!\"}\n </div>\n </Html>\n );\n}, (prevProps, nextProps) => {\n const prevArena = prevProps.arenaBounds;\n const nextArena = nextProps.arenaBounds;\n\n const sameArenaBounds =\n prevArena?.x === nextArena?.x &&\n prevArena?.y === nextArena?.y &&\n prevArena?.width === nextArena?.width &&\n prevArena?.height === nextArena?.height &&\n prevArena?.worldWidthMeters === nextArena?.worldWidthMeters &&\n prevArena?.worldDepthMeters === nextArena?.worldDepthMeters;\n\n return (\n prevProps.damage.id === nextProps.damage.id &&\n prevProps.isMobile === nextProps.isMobile &&\n prevProps.animationDuration === nextProps.animationDuration &&\n sameArenaBounds\n );\n});\n\nSingleDamageNumber.displayName = \"SingleDamageNumber\";\n\n/**\n * DamageNumbers Component\n *\n * Renders multiple floating damage numbers in the 3D scene.\n * Each number floats upward and fades out over time.\n * Performance optimized with React.memo.\n *\n * @example\n * ```tsx\n * <DamageNumbers\n * damages={damageNumbers}\n * isMobile={isMobile}\n * arenaBounds={arenaBounds}\n * />\n * ```\n */\nconst DamageNumbersComponent: React.FC<DamageNumbersProps> = ({\n damages,\n isMobile = false,\n arenaBounds = DEFAULT_PHYSICS_ARENA_BOUNDS,\n animationDuration = 1500,\n}) => {\n const visibleDamages = useMemo(() => [...damages], [damages]);\n\n return (\n <group data-testid=\"damage-numbers-container\">\n {visibleDamages.map((damage) => (\n <SingleDamageNumber\n key={damage.id}\n damage={damage}\n isMobile={isMobile}\n arenaBounds={arenaBounds}\n animationDuration={animationDuration}\n />\n ))}\n </group>\n );\n};\n\n/**\n * Memoized DamageNumbers with custom comparison\n * Only re-renders when damage array changes\n */\nexport const DamageNumbers = React.memo(\n DamageNumbersComponent,\n (prevProps, nextProps) => {\n if (prevProps.damages.length !== nextProps.damages.length) {\n return false;\n }\n \n for (let i = 0; i < prevProps.damages.length; i++) {\n if (prevProps.damages[i].id !== nextProps.damages[i].id) {\n return false;\n }\n }\n \n return (\n prevProps.isMobile === nextProps.isMobile &&\n prevProps.animationDuration === nextProps.animationDuration &&\n prevProps.arenaBounds?.x === nextProps.arenaBounds?.x &&\n prevProps.arenaBounds?.y === nextProps.arenaBounds?.y &&\n prevProps.arenaBounds?.width === nextProps.arenaBounds?.width &&\n prevProps.arenaBounds?.height === nextProps.arenaBounds?.height\n );\n }\n);\n\nDamageNumbers.displayName = \"DamageNumbers\";\n\nexport default DamageNumbers;\n"],"mappings":"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"}
|
|
@@ -35,7 +35,8 @@ var HitEffectVisual = ({ effect, effectRef, arenaBounds }) => {
|
|
|
35
35
|
}, [effect.position, arenaBounds]);
|
|
36
36
|
useFrame(() => {
|
|
37
37
|
if (!groupRef.current || !effectRef.current) return;
|
|
38
|
-
|
|
38
|
+
const progress = effectRef.current.progress;
|
|
39
|
+
alphaRef.current = 1 - progress;
|
|
39
40
|
let sparkIndex = 0;
|
|
40
41
|
groupRef.current.traverse((object) => {
|
|
41
42
|
if (object instanceof THREE.Mesh && object.material instanceof THREE.MeshBasicMaterial) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HitEffects3D.js","names":[],"sources":["../../../../../src/components/shared/three/effects/HitEffects3D.tsx"],"sourcesContent":["/**\n * HitEffects3D - Three.js particle effects for combat\n *\n * Maintains Korean theming and visual feedback for combat actions\n */\n\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useEffect, useMemo, useRef, useState } from \"react\";\nimport * as THREE from \"three\";\nimport { HitEffect } from \"../../../../systems\";\nimport { HitEffectType } from \"../../../../systems/effects\";\nimport { KOREAN_COLORS } from \"../../../../types/constants\";\nimport { DEFAULT_PHYSICS_ARENA_BOUNDS, type PhysicsArenaBounds } from \"../../../../types/PhysicsTypes\";\n\n/**\n * Props for the HitEffects3D component.\n * Controls which effects are displayed and callbacks for effect lifecycle.\n */\nexport interface HitEffects3DProps {\n /** Array of active hit effects to render in the scene */\n readonly effects: HitEffect[];\n /** Callback invoked when an effect completes its duration */\n readonly onEffectComplete?: (effectId: string) => void;\n /** Arena bounds for accurate coordinate conversion (physics-first with meter dimensions) */\n readonly arenaBounds?: PhysicsArenaBounds;\n}\n\ninterface ActiveEffect extends HitEffect {\n progress: number;\n}\n\n/**\n * Individual Hit Effect Component\n * Renders a single effect with Three.js primitives\n */\nconst HitEffectVisual: React.FC<{\n effect: HitEffect;\n effectRef: React.MutableRefObject<ActiveEffect | null>;\n arenaBounds?: PhysicsArenaBounds;\n}> = ({ effect, effectRef, arenaBounds }) => {\n const groupRef = useRef<THREE.Group>(null);\n const alphaRef = useRef(1);\n\n const position3D: [number, number, number] = useMemo(() => {\n if (!effect.position) return [0, 1, 0];\n\n const bounds = arenaBounds ?? DEFAULT_PHYSICS_ARENA_BOUNDS;\n \n const halfWidth = bounds.worldWidthMeters / 2;\n const halfDepth = bounds.worldDepthMeters / 2;\n \n const clampedX = Math.min(halfWidth, Math.max(-halfWidth, effect.position.x));\n const clampedZ = Math.min(halfDepth, Math.max(-halfDepth, effect.position.y));\n \n const x = clampedX; // Meter position X\n const y = 1.5; // Mid-height for effects\n const z = clampedZ; // Meter position Z (depth)\n\n return [x, y, z];\n }, [effect.position, arenaBounds]);\n\n useFrame(() => {\n if (!groupRef.current || !effectRef.current) return;\n\n const progress = effectRef.current.progress;\n alphaRef.current = 1 - progress;\n\n let sparkIndex = 0;\n groupRef.current.traverse((object) => {\n if (object instanceof THREE.Mesh && object.material instanceof THREE.MeshBasicMaterial) {\n const baseOpacity = object.material.userData.baseOpacity ?? 1;\n object.material.opacity = alphaRef.current * baseOpacity;\n \n if (effect.type === HitEffectType.BLOCK && object.geometry instanceof THREE.SphereGeometry) {\n if (object.geometry.parameters?.radius === 0.05) { // Spark particles have radius 0.05\n const i = sparkIndex++;\n if (i < 3) { // Only update the 3 spark particles\n object.position.y = Math.sin((1 - alphaRef.current) * Math.PI) * 0.3;\n }\n }\n }\n }\n });\n\n if (\n effect.type === HitEffectType.COUNTER ||\n effect.type === HitEffectType.VITAL_POINT_STRIKE\n ) {\n groupRef.current.rotation.y += 0.1;\n }\n\n if (effect.type === HitEffectType.CRITICAL_HIT) {\n const pulse =\n 1 + Math.sin(effectRef.current.progress * Math.PI * 4) * 0.2;\n groupRef.current.scale.set(pulse, pulse, pulse);\n }\n });\n\n switch (effect.type) {\n case HitEffectType.HIT:\n return (\n <group ref={groupRef} position={position3D}>\n {/* Impact flash sphere */}\n <mesh>\n <sphereGeometry args={[0.3 * effect.intensity, 16, 16]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.ACCENT_RED}\n transparent\n opacity={0.5}\n userData={{ baseOpacity: 0.5 }}\n />\n </mesh>\n {/* Expanding ring */}\n <mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0, 0]}>\n <ringGeometry\n args={[0.3 * effect.intensity, 0.35 * effect.intensity, 32]}\n />\n <meshBasicMaterial\n color={KOREAN_COLORS.ACCENT_RED}\n transparent\n opacity={1}\n side={THREE.DoubleSide}\n userData={{ baseOpacity: 1.0 }}\n />\n </mesh>\n </group>\n );\n\n case HitEffectType.CRITICAL_HIT:\n return (\n <group ref={groupRef} position={position3D}>\n {/* Large impact sphere */}\n <mesh>\n <sphereGeometry args={[0.5 * effect.intensity, 16, 16]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.ACCENT_GOLD}\n transparent\n opacity={0.7}\n userData={{ baseOpacity: 0.7 }}\n />\n </mesh>\n {/* Star burst lines */}\n {[0, 1, 2, 3].map((i) => {\n const angle = (i * Math.PI) / 2;\n return (\n <mesh\n key={i}\n position={[Math.cos(angle) * 0.3, 0, Math.sin(angle) * 0.3]}\n rotation={[0, angle, 0]}\n >\n <boxGeometry args={[0.6, 0.05, 0.05]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.ACCENT_RED}\n transparent\n opacity={1}\n userData={{ baseOpacity: 1.0 }}\n />\n </mesh>\n );\n })}\n </group>\n );\n\n case HitEffectType.BLOCK:\n return (\n <group ref={groupRef} position={position3D}>\n {/* Shield arc */}\n <mesh rotation={[0, 0, Math.PI / 2]}>\n <torusGeometry\n args={[0.4 * effect.intensity, 0.05, 8, 16, Math.PI]}\n />\n <meshBasicMaterial\n color={KOREAN_COLORS.ACCENT_CYAN}\n transparent\n opacity={1}\n userData={{ baseOpacity: 1.0 }}\n />\n </mesh>\n {/* Spark particles - positions will be updated in useFrame */}\n {[0, 1, 2].map((i) => (\n <mesh\n key={i}\n position={[\n (i - 1) * 0.2,\n 0,\n 0,\n ]}\n >\n <sphereGeometry args={[0.05, 8, 8]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.ACCENT_CYAN}\n transparent\n opacity={0.8}\n userData={{ baseOpacity: 0.8 }}\n />\n </mesh>\n ))}\n </group>\n );\n\n case HitEffectType.MISS:\n return (\n <group ref={groupRef} position={position3D}>\n {/* Swish trail lines */}\n {[0, 1].map((i) => (\n <mesh\n key={i}\n position={[(i - 0.5) * 0.2, i * 0.1, 0]}\n rotation={[0, 0, (i - 0.5) * 0.3]}\n >\n <boxGeometry args={[0.6, 0.02, 0.02]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.TEXT_TERTIARY}\n transparent\n opacity={1}\n userData={{ baseOpacity: 1.0 }}\n />\n </mesh>\n ))}\n </group>\n );\n\n case HitEffectType.VITAL_POINT_STRIKE:\n return (\n <group ref={groupRef} position={position3D}>\n {/* Pulsing sphere */}\n <mesh>\n <sphereGeometry args={[0.35 * effect.intensity, 16, 16]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.SECONDARY_MAGENTA}\n transparent\n opacity={0.5}\n userData={{ baseOpacity: 0.5 }}\n />\n </mesh>\n {/* Concentric rings */}\n {[0.2, 0.3, 0.4].map((radius, i) => (\n <mesh key={i} rotation={[-Math.PI / 2, 0, 0]}>\n <ringGeometry args={[radius, radius + 0.02, 32]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.SECONDARY_MAGENTA}\n transparent\n opacity={1}\n side={THREE.DoubleSide}\n userData={{ baseOpacity: 1.0 }}\n />\n </mesh>\n ))}\n {/* Crosshair */}\n <mesh position={[0, 0, 0]}>\n <boxGeometry args={[0.8, 0.02, 0.02]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.SECONDARY_MAGENTA}\n transparent\n opacity={0.8}\n userData={{ baseOpacity: 0.8 }}\n />\n </mesh>\n <mesh position={[0, 0, 0]} rotation={[0, 0, Math.PI / 2]}>\n <boxGeometry args={[0.8, 0.02, 0.02]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.SECONDARY_MAGENTA}\n transparent\n opacity={0.8}\n userData={{ baseOpacity: 0.8 }}\n />\n </mesh>\n </group>\n );\n\n case HitEffectType.PARRY:\n return (\n <group ref={groupRef} position={position3D}>\n {/* Deflection arc */}\n <mesh>\n <torusGeometry\n args={[0.35 * effect.intensity, 0.05, 8, 16, Math.PI / 2]}\n />\n <meshBasicMaterial\n color={KOREAN_COLORS.ACCENT_GOLD}\n transparent\n opacity={1}\n userData={{ baseOpacity: 1.0 }}\n />\n </mesh>\n {/* Sparks */}\n {[0, 1, 2].map((i) => {\n const angle = (Math.PI / 6) * (i - 1);\n return (\n <mesh\n key={i}\n position={[Math.cos(angle) * 0.4, Math.sin(angle) * 0.4, 0]}\n >\n <sphereGeometry args={[0.04, 8, 8]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.ACCENT_GOLD}\n transparent\n opacity={0.8}\n userData={{ baseOpacity: 0.8 }}\n />\n </mesh>\n );\n })}\n </group>\n );\n\n case HitEffectType.COUNTER:\n return (\n <group ref={groupRef} position={position3D}>\n {/* Spinning energy blades */}\n {[0, 1, 2, 3].map((i) => (\n <mesh\n key={i}\n rotation={[0, (i * Math.PI) / 2, 0]}\n position={[0, 0, 0]}\n >\n <boxGeometry args={[0.6, 0.05, 0.05]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.PRIMARY_CYAN}\n transparent\n opacity={1}\n userData={{ baseOpacity: 1.0 }}\n />\n </mesh>\n ))}\n </group>\n );\n\n case HitEffectType.GENERAL_DAMAGE:\n case HitEffectType.STATUS_EFFECT:\n default:\n return (\n <group ref={groupRef} position={position3D}>\n <mesh>\n <sphereGeometry args={[0.3 * effect.intensity, 16, 16]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.ACCENT_GREEN}\n transparent\n opacity={0.5}\n userData={{ baseOpacity: 0.5 }}\n />\n </mesh>\n </group>\n );\n }\n};\n\n/**\n * HitEffects3D Component\n * Manages all active hit effects in the combat scene\n * Uses refs to avoid triggering React re-renders at 60fps\n */\nexport const HitEffects3D: React.FC<HitEffects3DProps> = ({\n effects,\n onEffectComplete,\n arenaBounds,\n}) => {\n const effectRefsMap = useRef<\n Map<string, React.MutableRefObject<ActiveEffect | null>>\n >(new Map());\n const completedEffectsRef = useRef<Set<string>>(new Set());\n\n const [effectRefsSnapshot, setEffectRefsSnapshot] = useState<\n Map<string, React.MutableRefObject<ActiveEffect | null>>\n >(new Map());\n\n useEffect(() => {\n const currentIdSet = new Set(effects.map((e) => e.id));\n effectRefsMap.current.forEach((_ref, id) => {\n if (!currentIdSet.has(id)) {\n effectRefsMap.current.delete(id);\n completedEffectsRef.current.delete(id);\n }\n });\n\n effects.forEach((effect) => {\n if (!effectRefsMap.current.has(effect.id)) {\n effectRefsMap.current.set(effect.id, {\n current: { ...effect, progress: 0 },\n });\n }\n });\n\n setEffectRefsSnapshot(new Map(effectRefsMap.current));\n }, [effects]);\n\n useFrame(() => {\n const now = Date.now();\n\n effectRefsMap.current.forEach((ref, id) => {\n if (!ref.current) return;\n\n const progress = Math.min(\n (now - ref.current.startTime) / ref.current.duration,\n 1\n );\n ref.current.progress = progress;\n\n const isExpired = progress >= 1;\n if (\n isExpired &&\n onEffectComplete &&\n !completedEffectsRef.current.has(id)\n ) {\n completedEffectsRef.current.add(id);\n onEffectComplete(id);\n }\n });\n });\n\n const effectsToRender = useMemo(() => {\n return effects\n .map((effect) => {\n const effectRef = effectRefsSnapshot.get(effect.id);\n return { effect, effectRef };\n })\n .filter(\n (\n item\n ): item is {\n effect: HitEffect;\n effectRef: React.MutableRefObject<ActiveEffect | null>;\n } => item.effectRef !== undefined\n );\n }, [effects, effectRefsSnapshot]);\n\n return (\n <group>\n {effectsToRender.map(({ effect, effectRef }) => {\n return (\n <HitEffectVisual\n key={effect.id}\n effect={effect}\n effectRef={effectRef}\n arenaBounds={arenaBounds}\n />\n );\n })}\n </group>\n );\n};\n\nexport default HitEffects3D;\n"],"mappings":";;;;;;;;;;;;;;;;;AAmCA,IAAM,mBAIA,EAAE,QAAQ,WAAW,kBAAkB;CAC3C,MAAM,WAAW,OAAoB,IAAI;CACzC,MAAM,WAAW,OAAO,CAAC;CAEzB,MAAM,aAAuC,cAAc;EACzD,IAAI,CAAC,OAAO,UAAU,OAAO;GAAC;GAAG;GAAG;EAAC;EAErC,MAAM,SAAS,eAAe;EAE9B,MAAM,YAAY,OAAO,mBAAmB;EAC5C,MAAM,YAAY,OAAO,mBAAmB;EAS5C,OAAO;GAPU,KAAK,IAAI,WAAW,KAAK,IAAI,CAAC,WAAW,OAAO,SAAS,CAAC,CAOnE;GAAG;GANM,KAAK,IAAI,WAAW,KAAK,IAAI,CAAC,WAAW,OAAO,SAAS,CAAC,CAM7D;EAAC;CACjB,GAAG,CAAC,OAAO,UAAU,WAAW,CAAC;CAEjC,eAAe;EACb,IAAI,CAAC,SAAS,WAAW,CAAC,UAAU,SAAS;EAG7C,SAAS,UAAU,IADF,UAAU,QAAQ;EAGnC,IAAI,aAAa;EACjB,SAAS,QAAQ,UAAU,WAAW;GACpC,IAAI,kBAAkB,MAAM,QAAQ,OAAO,oBAAoB,MAAM,mBAAmB;IACtF,MAAM,cAAc,OAAO,SAAS,SAAS,eAAe;IAC5D,OAAO,SAAS,UAAU,SAAS,UAAU;IAE7C,IAAI,OAAO,SAAS,cAAc,SAAS,OAAO,oBAAoB,MAAM;SACtE,OAAO,SAAS,YAAY,WAAW;UAErC,eAAI,GACN,OAAO,SAAS,IAAI,KAAK,KAAK,IAAI,SAAS,WAAW,KAAK,EAAE,IAAI;KAAA;IACnE;GAGN;EACF,CAAC;EAED,IACE,OAAO,SAAS,cAAc,WAC9B,OAAO,SAAS,cAAc,oBAE9B,SAAS,QAAQ,SAAS,KAAK;EAGjC,IAAI,OAAO,SAAS,cAAc,cAAc;GAC9C,MAAM,QACJ,IAAI,KAAK,IAAI,UAAU,QAAQ,WAAW,KAAK,KAAK,CAAC,IAAI;GAC3D,SAAS,QAAQ,MAAM,IAAI,OAAO,OAAO,KAAK;EAChD;CACF,CAAC;CAED,QAAQ,OAAO,MAAf;EACE,KAAK,cAAc,KACjB,OACE,qBAAC,SAAD;GAAO,KAAK;GAAU,UAAU;aAAhC,CAEE,qBAAC,QAAD,EAAA,UAAA,CACE,oBAAC,kBAAD,EAAgB,MAAM;IAAC,KAAM,OAAO;IAAW;IAAI;GAAE,EAAI,CAAA,GACzD,oBAAC,qBAAD;IACE,OAAO,cAAc;IACrB,aAAA;IACA,SAAS;IACT,UAAU,EAAE,aAAa,GAAI;GAC9B,CAAA,CACG,EAAA,CAAA,GAEN,qBAAC,QAAD;IAAM,UAAU;KAAC,CAAC,KAAK,KAAK;KAAG;KAAG;IAAC;IAAG,UAAU;KAAC;KAAG;KAAG;IAAC;cAAxD,CACE,oBAAC,gBAAD,EACE,MAAM;KAAC,KAAM,OAAO;KAAW,MAAO,OAAO;KAAW;IAAE,EAC3D,CAAA,GACD,oBAAC,qBAAD;KACE,OAAO,cAAc;KACrB,aAAA;KACA,SAAS;KACT,MAAM,MAAM;KACZ,UAAU,EAAE,aAAa,EAAI;IAC9B,CAAA,CACG;KACD;;EAGX,KAAK,cAAc,cACjB,OACE,qBAAC,SAAD;GAAO,KAAK;GAAU,UAAU;aAAhC,CAEE,qBAAC,QAAD,EAAA,UAAA,CACE,oBAAC,kBAAD,EAAgB,MAAM;IAAC,KAAM,OAAO;IAAW;IAAI;GAAE,EAAI,CAAA,GACzD,oBAAC,qBAAD;IACE,OAAO,cAAc;IACrB,aAAA;IACA,SAAS;IACT,UAAU,EAAE,aAAa,GAAI;GAC9B,CAAA,CACG,EAAA,CAAA,GAEL;IAAC;IAAG;IAAG;IAAG;GAAC,CAAC,CAAC,KAAK,MAAM;IACvB,MAAM,QAAS,IAAI,KAAK,KAAM;IAC9B,OACE,qBAAC,QAAD;KAEE,UAAU;MAAC,KAAK,IAAI,KAAK,IAAI;MAAK;MAAG,KAAK,IAAI,KAAK,IAAI;KAAG;KAC1D,UAAU;MAAC;MAAG;MAAO;KAAC;eAHxB,CAKE,oBAAC,eAAD,EAAa,MAAM;MAAC;MAAK;MAAM;KAAI,EAAI,CAAA,GACvC,oBAAC,qBAAD;MACE,OAAO,cAAc;MACrB,aAAA;MACA,SAAS;MACT,UAAU,EAAE,aAAa,EAAI;KAC9B,CAAA,CACG;OAXC,CAWD;GAEV,CAAC,CACI;;EAGX,KAAK,cAAc,OACjB,OACE,qBAAC,SAAD;GAAO,KAAK;GAAU,UAAU;aAAhC,CAEE,qBAAC,QAAD;IAAM,UAAU;KAAC;KAAG;KAAG,KAAK,KAAK;IAAC;cAAlC,CACE,oBAAC,iBAAD,EACE,MAAM;KAAC,KAAM,OAAO;KAAW;KAAM;KAAG;KAAI,KAAK;IAAE,EACpD,CAAA,GACD,oBAAC,qBAAD;KACE,OAAO,cAAc;KACrB,aAAA;KACA,SAAS;KACT,UAAU,EAAE,aAAa,EAAI;IAC9B,CAAA,CACG;OAEL;IAAC;IAAG;IAAG;GAAC,CAAC,CAAC,KAAK,MACd,qBAAC,QAAD;IAEE,UAAU;MACP,IAAI,KAAK;KACV;KACA;IACF;cANF,CAQE,oBAAC,kBAAD,EAAgB,MAAM;KAAC;KAAM;KAAG;IAAC,EAAI,CAAA,GACrC,oBAAC,qBAAD;KACE,OAAO,cAAc;KACrB,aAAA;KACA,SAAS;KACT,UAAU,EAAE,aAAa,GAAI;IAC9B,CAAA,CACG;MAdC,CAcD,CACP,CACI;;EAGX,KAAK,cAAc,MACjB,OACE,oBAAC,SAAD;GAAO,KAAK;GAAU,UAAU;aAE7B,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,MACX,qBAAC,QAAD;IAEE,UAAU;MAAE,IAAI,MAAO;KAAK,IAAI;KAAK;IAAC;IACtC,UAAU;KAAC;KAAG;MAAI,IAAI,MAAO;IAAG;cAHlC,CAKE,oBAAC,eAAD,EAAa,MAAM;KAAC;KAAK;KAAM;IAAI,EAAI,CAAA,GACvC,oBAAC,qBAAD;KACE,OAAO,cAAc;KACrB,aAAA;KACA,SAAS;KACT,UAAU,EAAE,aAAa,EAAI;IAC9B,CAAA,CACG;MAXC,CAWD,CACP;EACI,CAAA;EAGX,KAAK,cAAc,oBACjB,OACE,qBAAC,SAAD;GAAO,KAAK;GAAU,UAAU;aAAhC;IAEE,qBAAC,QAAD,EAAA,UAAA,CACE,oBAAC,kBAAD,EAAgB,MAAM;KAAC,MAAO,OAAO;KAAW;KAAI;IAAE,EAAI,CAAA,GAC1D,oBAAC,qBAAD;KACE,OAAO,cAAc;KACrB,aAAA;KACA,SAAS;KACT,UAAU,EAAE,aAAa,GAAI;IAC9B,CAAA,CACG,EAAA,CAAA;IAEL;KAAC;KAAK;KAAK;IAAG,CAAC,CAAC,KAAK,QAAQ,MAC5B,qBAAC,QAAD;KAAc,UAAU;MAAC,CAAC,KAAK,KAAK;MAAG;MAAG;KAAC;eAA3C,CACE,oBAAC,gBAAD,EAAc,MAAM;MAAC;MAAQ,SAAS;MAAM;KAAE,EAAI,CAAA,GAClD,oBAAC,qBAAD;MACE,OAAO,cAAc;MACrB,aAAA;MACA,SAAS;MACT,MAAM,MAAM;MACZ,UAAU,EAAE,aAAa,EAAI;KAC9B,CAAA,CACG;OATK,CASL,CACP;IAED,qBAAC,QAAD;KAAM,UAAU;MAAC;MAAG;MAAG;KAAC;eAAxB,CACE,oBAAC,eAAD,EAAa,MAAM;MAAC;MAAK;MAAM;KAAI,EAAI,CAAA,GACvC,oBAAC,qBAAD;MACE,OAAO,cAAc;MACrB,aAAA;MACA,SAAS;MACT,UAAU,EAAE,aAAa,GAAI;KAC9B,CAAA,CACG;;IACN,qBAAC,QAAD;KAAM,UAAU;MAAC;MAAG;MAAG;KAAC;KAAG,UAAU;MAAC;MAAG;MAAG,KAAK,KAAK;KAAC;eAAvD,CACE,oBAAC,eAAD,EAAa,MAAM;MAAC;MAAK;MAAM;KAAI,EAAI,CAAA,GACvC,oBAAC,qBAAD;MACE,OAAO,cAAc;MACrB,aAAA;MACA,SAAS;MACT,UAAU,EAAE,aAAa,GAAI;KAC9B,CAAA,CACG;;GACD;;EAGX,KAAK,cAAc,OACjB,OACE,qBAAC,SAAD;GAAO,KAAK;GAAU,UAAU;aAAhC,CAEE,qBAAC,QAAD,EAAA,UAAA,CACE,oBAAC,iBAAD,EACE,MAAM;IAAC,MAAO,OAAO;IAAW;IAAM;IAAG;IAAI,KAAK,KAAK;GAAC,EACzD,CAAA,GACD,oBAAC,qBAAD;IACE,OAAO,cAAc;IACrB,aAAA;IACA,SAAS;IACT,UAAU,EAAE,aAAa,EAAI;GAC9B,CAAA,CACG,EAAA,CAAA,GAEL;IAAC;IAAG;IAAG;GAAC,CAAC,CAAC,KAAK,MAAM;IACpB,MAAM,QAAS,KAAK,KAAK,KAAM,IAAI;IACnC,OACE,qBAAC,QAAD;KAEE,UAAU;MAAC,KAAK,IAAI,KAAK,IAAI;MAAK,KAAK,IAAI,KAAK,IAAI;MAAK;KAAC;eAF5D,CAIE,oBAAC,kBAAD,EAAgB,MAAM;MAAC;MAAM;MAAG;KAAC,EAAI,CAAA,GACrC,oBAAC,qBAAD;MACE,OAAO,cAAc;MACrB,aAAA;MACA,SAAS;MACT,UAAU,EAAE,aAAa,GAAI;KAC9B,CAAA,CACG;OAVC,CAUD;GAEV,CAAC,CACI;;EAGX,KAAK,cAAc,SACjB,OACE,oBAAC,SAAD;GAAO,KAAK;GAAU,UAAU;aAE7B;IAAC;IAAG;IAAG;IAAG;GAAC,CAAC,CAAC,KAAK,MACjB,qBAAC,QAAD;IAEE,UAAU;KAAC;KAAI,IAAI,KAAK,KAAM;KAAG;IAAC;IAClC,UAAU;KAAC;KAAG;KAAG;IAAC;cAHpB,CAKE,oBAAC,eAAD,EAAa,MAAM;KAAC;KAAK;KAAM;IAAI,EAAI,CAAA,GACvC,oBAAC,qBAAD;KACE,OAAO,cAAc;KACrB,aAAA;KACA,SAAS;KACT,UAAU,EAAE,aAAa,EAAI;IAC9B,CAAA,CACG;MAXC,CAWD,CACP;EACI,CAAA;EAGX,KAAK,cAAc;EACnB,KAAK,cAAc;EACnB,SACE,OACE,oBAAC,SAAD;GAAO,KAAK;GAAU,UAAU;aAC9B,qBAAC,QAAD,EAAA,UAAA,CACE,oBAAC,kBAAD,EAAgB,MAAM;IAAC,KAAM,OAAO;IAAW;IAAI;GAAE,EAAI,CAAA,GACzD,oBAAC,qBAAD;IACE,OAAO,cAAc;IACrB,aAAA;IACA,SAAS;IACT,UAAU,EAAE,aAAa,GAAI;GAC9B,CAAA,CACG,EAAA,CAAA;EACD,CAAA;CAEb;AACF;;;;;;AAOA,IAAa,gBAA6C,EACxD,SACA,kBACA,kBACI;CACJ,MAAM,gBAAgB,uBAEpB,IAAI,IAAI,CAAC;CACX,MAAM,sBAAsB,uBAAoB,IAAI,IAAI,CAAC;CAEzD,MAAM,CAAC,oBAAoB,yBAAyB,yBAElD,IAAI,IAAI,CAAC;CAEX,gBAAgB;EACd,MAAM,eAAe,IAAI,IAAI,QAAQ,KAAK,MAAM,EAAE,EAAE,CAAC;EACrD,cAAc,QAAQ,SAAS,MAAM,OAAO;GAC1C,IAAI,CAAC,aAAa,IAAI,EAAE,GAAG;IACzB,cAAc,QAAQ,OAAO,EAAE;IAC/B,oBAAoB,QAAQ,OAAO,EAAE;GACvC;EACF,CAAC;EAED,QAAQ,SAAS,WAAW;GAC1B,IAAI,CAAC,cAAc,QAAQ,IAAI,OAAO,EAAE,GACtC,cAAc,QAAQ,IAAI,OAAO,IAAI,EACnC,SAAS;IAAE,GAAG;IAAQ,UAAU;GAAE,EACpC,CAAC;EAEL,CAAC;EAED,sBAAsB,IAAI,IAAI,cAAc,OAAO,CAAC;CACtD,GAAG,CAAC,OAAO,CAAC;CAEZ,eAAe;EACb,MAAM,MAAM,KAAK,IAAI;EAErB,cAAc,QAAQ,SAAS,KAAK,OAAO;GACzC,IAAI,CAAC,IAAI,SAAS;GAElB,MAAM,WAAW,KAAK,KACnB,MAAM,IAAI,QAAQ,aAAa,IAAI,QAAQ,UAC5C,CACF;GACA,IAAI,QAAQ,WAAW;GAGvB,IADkB,YAAY,KAG5B,oBACA,CAAC,oBAAoB,QAAQ,IAAI,EAAE,GACnC;IACA,oBAAoB,QAAQ,IAAI,EAAE;IAClC,iBAAiB,EAAE;GACrB;EACF,CAAC;CACH,CAAC;CAkBD,OACE,oBAAC,SAAD,EAAA,UAjBsB,cAAc;EACpC,OAAO,QACJ,KAAK,WAAW;GAEf,OAAO;IAAE;IAAQ,WADC,mBAAmB,IAAI,OAAO,EAC/B;GAAU;EAC7B,CAAC,CAAC,CACD,QAEG,SAIG,KAAK,cAAc,KAAA,CAC1B;CACJ,GAAG,CAAC,SAAS,kBAAkB,CAI1B,CAAA,CAAgB,KAAK,EAAE,QAAQ,gBAAgB;EAC9C,OACE,oBAAC,iBAAD;GAEU;GACG;GACE;EACd,GAJM,OAAO,EAIb;CAEL,CAAC,EACI,CAAA;AAEX"}
|
|
1
|
+
{"version":3,"file":"HitEffects3D.js","names":[],"sources":["../../../../../src/components/shared/three/effects/HitEffects3D.tsx"],"sourcesContent":["/**\n * HitEffects3D - Three.js particle effects for combat\n *\n * Maintains Korean theming and visual feedback for combat actions\n */\n\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useEffect, useMemo, useRef, useState } from \"react\";\nimport * as THREE from \"three\";\nimport { HitEffect } from \"../../../../systems\";\nimport { HitEffectType } from \"../../../../systems/effects\";\nimport { KOREAN_COLORS } from \"../../../../types/constants\";\nimport { DEFAULT_PHYSICS_ARENA_BOUNDS, type PhysicsArenaBounds } from \"../../../../types/PhysicsTypes\";\n\n/**\n * Props for the HitEffects3D component.\n * Controls which effects are displayed and callbacks for effect lifecycle.\n */\nexport interface HitEffects3DProps {\n /** Array of active hit effects to render in the scene */\n readonly effects: HitEffect[];\n /** Callback invoked when an effect completes its duration */\n readonly onEffectComplete?: (effectId: string) => void;\n /** Arena bounds for accurate coordinate conversion (physics-first with meter dimensions) */\n readonly arenaBounds?: PhysicsArenaBounds;\n}\n\ninterface ActiveEffect extends HitEffect {\n progress: number;\n}\n\n/**\n * Individual Hit Effect Component\n * Renders a single effect with Three.js primitives\n */\nconst HitEffectVisual: React.FC<{\n effect: HitEffect;\n effectRef: React.MutableRefObject<ActiveEffect | null>;\n arenaBounds?: PhysicsArenaBounds;\n}> = ({ effect, effectRef, arenaBounds }) => {\n const groupRef = useRef<THREE.Group>(null);\n const alphaRef = useRef(1);\n\n const position3D: [number, number, number] = useMemo(() => {\n if (!effect.position) return [0, 1, 0];\n\n const bounds = arenaBounds ?? DEFAULT_PHYSICS_ARENA_BOUNDS;\n \n const halfWidth = bounds.worldWidthMeters / 2;\n const halfDepth = bounds.worldDepthMeters / 2;\n \n const clampedX = Math.min(halfWidth, Math.max(-halfWidth, effect.position.x));\n const clampedZ = Math.min(halfDepth, Math.max(-halfDepth, effect.position.y));\n \n const x = clampedX; // Meter position X\n const y = 1.5; // Mid-height for effects\n const z = clampedZ; // Meter position Z (depth)\n\n return [x, y, z];\n }, [effect.position, arenaBounds]);\n\n useFrame(() => {\n if (!groupRef.current || !effectRef.current) return;\n\n const progress = effectRef.current.progress;\n alphaRef.current = 1 - progress;\n\n let sparkIndex = 0;\n groupRef.current.traverse((object) => {\n if (object instanceof THREE.Mesh && object.material instanceof THREE.MeshBasicMaterial) {\n const baseOpacity = object.material.userData.baseOpacity ?? 1;\n object.material.opacity = alphaRef.current * baseOpacity;\n \n if (effect.type === HitEffectType.BLOCK && object.geometry instanceof THREE.SphereGeometry) {\n if (object.geometry.parameters?.radius === 0.05) { // Spark particles have radius 0.05\n const i = sparkIndex++;\n if (i < 3) { // Only update the 3 spark particles\n object.position.y = Math.sin((1 - alphaRef.current) * Math.PI) * 0.3;\n }\n }\n }\n }\n });\n\n if (\n effect.type === HitEffectType.COUNTER ||\n effect.type === HitEffectType.VITAL_POINT_STRIKE\n ) {\n groupRef.current.rotation.y += 0.1;\n }\n\n if (effect.type === HitEffectType.CRITICAL_HIT) {\n const pulse =\n 1 + Math.sin(effectRef.current.progress * Math.PI * 4) * 0.2;\n groupRef.current.scale.set(pulse, pulse, pulse);\n }\n });\n\n switch (effect.type) {\n case HitEffectType.HIT:\n return (\n <group ref={groupRef} position={position3D}>\n {/* Impact flash sphere */}\n <mesh>\n <sphereGeometry args={[0.3 * effect.intensity, 16, 16]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.ACCENT_RED}\n transparent\n opacity={0.5}\n userData={{ baseOpacity: 0.5 }}\n />\n </mesh>\n {/* Expanding ring */}\n <mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0, 0]}>\n <ringGeometry\n args={[0.3 * effect.intensity, 0.35 * effect.intensity, 32]}\n />\n <meshBasicMaterial\n color={KOREAN_COLORS.ACCENT_RED}\n transparent\n opacity={1}\n side={THREE.DoubleSide}\n userData={{ baseOpacity: 1.0 }}\n />\n </mesh>\n </group>\n );\n\n case HitEffectType.CRITICAL_HIT:\n return (\n <group ref={groupRef} position={position3D}>\n {/* Large impact sphere */}\n <mesh>\n <sphereGeometry args={[0.5 * effect.intensity, 16, 16]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.ACCENT_GOLD}\n transparent\n opacity={0.7}\n userData={{ baseOpacity: 0.7 }}\n />\n </mesh>\n {/* Star burst lines */}\n {[0, 1, 2, 3].map((i) => {\n const angle = (i * Math.PI) / 2;\n return (\n <mesh\n key={i}\n position={[Math.cos(angle) * 0.3, 0, Math.sin(angle) * 0.3]}\n rotation={[0, angle, 0]}\n >\n <boxGeometry args={[0.6, 0.05, 0.05]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.ACCENT_RED}\n transparent\n opacity={1}\n userData={{ baseOpacity: 1.0 }}\n />\n </mesh>\n );\n })}\n </group>\n );\n\n case HitEffectType.BLOCK:\n return (\n <group ref={groupRef} position={position3D}>\n {/* Shield arc */}\n <mesh rotation={[0, 0, Math.PI / 2]}>\n <torusGeometry\n args={[0.4 * effect.intensity, 0.05, 8, 16, Math.PI]}\n />\n <meshBasicMaterial\n color={KOREAN_COLORS.ACCENT_CYAN}\n transparent\n opacity={1}\n userData={{ baseOpacity: 1.0 }}\n />\n </mesh>\n {/* Spark particles - positions will be updated in useFrame */}\n {[0, 1, 2].map((i) => (\n <mesh\n key={i}\n position={[\n (i - 1) * 0.2,\n 0,\n 0,\n ]}\n >\n <sphereGeometry args={[0.05, 8, 8]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.ACCENT_CYAN}\n transparent\n opacity={0.8}\n userData={{ baseOpacity: 0.8 }}\n />\n </mesh>\n ))}\n </group>\n );\n\n case HitEffectType.MISS:\n return (\n <group ref={groupRef} position={position3D}>\n {/* Swish trail lines */}\n {[0, 1].map((i) => (\n <mesh\n key={i}\n position={[(i - 0.5) * 0.2, i * 0.1, 0]}\n rotation={[0, 0, (i - 0.5) * 0.3]}\n >\n <boxGeometry args={[0.6, 0.02, 0.02]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.TEXT_TERTIARY}\n transparent\n opacity={1}\n userData={{ baseOpacity: 1.0 }}\n />\n </mesh>\n ))}\n </group>\n );\n\n case HitEffectType.VITAL_POINT_STRIKE:\n return (\n <group ref={groupRef} position={position3D}>\n {/* Pulsing sphere */}\n <mesh>\n <sphereGeometry args={[0.35 * effect.intensity, 16, 16]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.SECONDARY_MAGENTA}\n transparent\n opacity={0.5}\n userData={{ baseOpacity: 0.5 }}\n />\n </mesh>\n {/* Concentric rings */}\n {[0.2, 0.3, 0.4].map((radius, i) => (\n <mesh key={i} rotation={[-Math.PI / 2, 0, 0]}>\n <ringGeometry args={[radius, radius + 0.02, 32]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.SECONDARY_MAGENTA}\n transparent\n opacity={1}\n side={THREE.DoubleSide}\n userData={{ baseOpacity: 1.0 }}\n />\n </mesh>\n ))}\n {/* Crosshair */}\n <mesh position={[0, 0, 0]}>\n <boxGeometry args={[0.8, 0.02, 0.02]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.SECONDARY_MAGENTA}\n transparent\n opacity={0.8}\n userData={{ baseOpacity: 0.8 }}\n />\n </mesh>\n <mesh position={[0, 0, 0]} rotation={[0, 0, Math.PI / 2]}>\n <boxGeometry args={[0.8, 0.02, 0.02]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.SECONDARY_MAGENTA}\n transparent\n opacity={0.8}\n userData={{ baseOpacity: 0.8 }}\n />\n </mesh>\n </group>\n );\n\n case HitEffectType.PARRY:\n return (\n <group ref={groupRef} position={position3D}>\n {/* Deflection arc */}\n <mesh>\n <torusGeometry\n args={[0.35 * effect.intensity, 0.05, 8, 16, Math.PI / 2]}\n />\n <meshBasicMaterial\n color={KOREAN_COLORS.ACCENT_GOLD}\n transparent\n opacity={1}\n userData={{ baseOpacity: 1.0 }}\n />\n </mesh>\n {/* Sparks */}\n {[0, 1, 2].map((i) => {\n const angle = (Math.PI / 6) * (i - 1);\n return (\n <mesh\n key={i}\n position={[Math.cos(angle) * 0.4, Math.sin(angle) * 0.4, 0]}\n >\n <sphereGeometry args={[0.04, 8, 8]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.ACCENT_GOLD}\n transparent\n opacity={0.8}\n userData={{ baseOpacity: 0.8 }}\n />\n </mesh>\n );\n })}\n </group>\n );\n\n case HitEffectType.COUNTER:\n return (\n <group ref={groupRef} position={position3D}>\n {/* Spinning energy blades */}\n {[0, 1, 2, 3].map((i) => (\n <mesh\n key={i}\n rotation={[0, (i * Math.PI) / 2, 0]}\n position={[0, 0, 0]}\n >\n <boxGeometry args={[0.6, 0.05, 0.05]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.PRIMARY_CYAN}\n transparent\n opacity={1}\n userData={{ baseOpacity: 1.0 }}\n />\n </mesh>\n ))}\n </group>\n );\n\n case HitEffectType.GENERAL_DAMAGE:\n case HitEffectType.STATUS_EFFECT:\n default:\n return (\n <group ref={groupRef} position={position3D}>\n <mesh>\n <sphereGeometry args={[0.3 * effect.intensity, 16, 16]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.ACCENT_GREEN}\n transparent\n opacity={0.5}\n userData={{ baseOpacity: 0.5 }}\n />\n </mesh>\n </group>\n );\n }\n};\n\n/**\n * HitEffects3D Component\n * Manages all active hit effects in the combat scene\n * Uses refs to avoid triggering React re-renders at 60fps\n */\nexport const HitEffects3D: React.FC<HitEffects3DProps> = ({\n effects,\n onEffectComplete,\n arenaBounds,\n}) => {\n const effectRefsMap = useRef<\n Map<string, React.MutableRefObject<ActiveEffect | null>>\n >(new Map());\n const completedEffectsRef = useRef<Set<string>>(new Set());\n\n const [effectRefsSnapshot, setEffectRefsSnapshot] = useState<\n Map<string, React.MutableRefObject<ActiveEffect | null>>\n >(new Map());\n\n useEffect(() => {\n const currentIdSet = new Set(effects.map((e) => e.id));\n effectRefsMap.current.forEach((_ref, id) => {\n if (!currentIdSet.has(id)) {\n effectRefsMap.current.delete(id);\n completedEffectsRef.current.delete(id);\n }\n });\n\n effects.forEach((effect) => {\n if (!effectRefsMap.current.has(effect.id)) {\n effectRefsMap.current.set(effect.id, {\n current: { ...effect, progress: 0 },\n });\n }\n });\n\n setEffectRefsSnapshot(new Map(effectRefsMap.current));\n }, [effects]);\n\n useFrame(() => {\n const now = Date.now();\n\n effectRefsMap.current.forEach((ref, id) => {\n if (!ref.current) return;\n\n const progress = Math.min(\n (now - ref.current.startTime) / ref.current.duration,\n 1\n );\n ref.current.progress = progress;\n\n const isExpired = progress >= 1;\n if (\n isExpired &&\n onEffectComplete &&\n !completedEffectsRef.current.has(id)\n ) {\n completedEffectsRef.current.add(id);\n onEffectComplete(id);\n }\n });\n });\n\n const effectsToRender = useMemo(() => {\n return effects\n .map((effect) => {\n const effectRef = effectRefsSnapshot.get(effect.id);\n return { effect, effectRef };\n })\n .filter(\n (\n item\n ): item is {\n effect: HitEffect;\n effectRef: React.MutableRefObject<ActiveEffect | null>;\n } => item.effectRef !== undefined\n );\n }, [effects, effectRefsSnapshot]);\n\n return (\n <group>\n {effectsToRender.map(({ effect, effectRef }) => {\n return (\n <HitEffectVisual\n key={effect.id}\n effect={effect}\n effectRef={effectRef}\n arenaBounds={arenaBounds}\n />\n );\n })}\n </group>\n );\n};\n\nexport default HitEffects3D;\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.
|
|
186
|
+
"aria-label": `Application version 0.7.83`,
|
|
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.
|
|
195
|
+
children: ["v", "0.7.83"]
|
|
196
196
|
})
|
|
197
197
|
]
|
|
198
198
|
});
|
|
@@ -85,7 +85,10 @@ function useMuscleActivation(options) {
|
|
|
85
85
|
}
|
|
86
86
|
});
|
|
87
87
|
} else muscleManager.current.relaxAllMuscles(delta);
|
|
88
|
-
if (frameCounter === 0)
|
|
88
|
+
if (frameCounter === 0) {
|
|
89
|
+
const scratchMap = muscleManager.current.getScratchMapForSync();
|
|
90
|
+
setMuscleStates(scratchMap);
|
|
91
|
+
}
|
|
89
92
|
};
|
|
90
93
|
return {
|
|
91
94
|
muscleStates,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useMuscleActivation.js","names":[],"sources":["../../src/hooks/useMuscleActivation.ts"],"sourcesContent":["/**\n * useMuscleActivation - Shared hook for muscle activation management\n *\n * Manages muscle activation state based on current actions and stamina.\n * Reduces code duplication in skeletal animation components.\n * Includes stance-based leg muscle tension for isometric holds.\n *\n * @module hooks/useMuscleActivation\n * @category Hooks\n * @korean 근육활성화훅\n */\n\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport {\n MuscleActivationManager,\n getMuscleTensionForStance,\n} from \"../systems/animation\";\nimport type { TrigramStance } from \"../types/common\";\nimport { DEFAULT_MUSCLE_CONFIG } from \"../types/muscle\";\nimport type { PlayerAnimation } from \"../types/player-visual\";\n\n/**\n * Simple lerp function for muscle tension interpolation\n * Defined at module level to avoid recreation\n * @korean 선형보간함수\n */\nconst lerp = (start: number, end: number, t: number): number =>\n start + (end - start) * t;\n\n/**\n * Options for useMuscleActivation hook\n * @korean 근육활성화훅옵션\n */\nexport interface UseMuscleActivationOptions {\n /** Current animation name */\n readonly currentAnimation: PlayerAnimation;\n /** Specific attack animation name (for attack state) */\n readonly attackAnimation?: string;\n /** Whether player is blocking */\n readonly isBlocking?: boolean;\n /** Current stamina level (0-100) */\n readonly stamina: number;\n /** Current trigram stance (for leg muscle tension) */\n readonly currentStance?: TrigramStance;\n}\n\n/**\n * Return type for useMuscleActivation hook\n * @korean 근육활성화훅반환타입\n */\nexport interface UseMuscleActivationReturn {\n /** Current muscle activation states (bone name -> activation 0-1) */\n readonly muscleStates: Map<string, number>;\n /** Update muscle activations (call in useFrame) */\n readonly updateMuscleActivations: (\n delta: number,\n frameCounter: number\n ) => void;\n}\n\n/**\n * useMuscleActivation hook\n *\n * Manages muscle activation based on current actions (attack, defend, movement).\n * Updates at 60fps with periodic state syncs to reduce re-renders.\n *\n * @param options - Muscle activation options\n * @returns Muscle states and update function\n *\n * @example\n * ```tsx\n * const { muscleStates, updateMuscleActivations } = useMuscleActivation({\n * currentAnimation: \"attack\",\n * attackAnimation: \"jab\",\n * isBlocking: false,\n * stamina: 85,\n * });\n *\n * // In useFrame callback\n * let frameCounter = 0;\n * useFrame((_, delta) => {\n * frameCounter = (frameCounter + 1) % 10;\n * updateMuscleActivations(delta, frameCounter);\n * });\n *\n * // Use muscle states in rendering\n * <BoneRenderer\n * rig={rig}\n * muscleStates={muscleStates}\n * isExhausted={stamina < 20}\n * />\n * ```\n *\n * @korean 근육활성화훅\n */\nexport function useMuscleActivation(\n options: UseMuscleActivationOptions\n): UseMuscleActivationReturn {\n const {\n currentAnimation,\n attackAnimation,\n isBlocking = false,\n stamina,\n currentStance,\n } = options;\n\n // Muscle activation manager\n const muscleManager = useRef(new MuscleActivationManager());\n const [muscleStates, setMuscleStates] = useState<Map<string, number>>(\n new Map()\n );\n\n // Cleanup muscle manager on unmount\n useEffect(() => {\n const manager = muscleManager.current;\n return () => {\n try {\n manager.reset();\n } catch (error) {\n console.warn(\"MuscleActivationManager reset failed:\", error);\n }\n };\n }, []);\n\n // Memoize stance tension to avoid recalculation at 60fps\n const stanceTension = useMemo(() => {\n return currentStance ? getMuscleTensionForStance(currentStance) : null;\n }, [currentStance]);\n\n // Update muscle activations (called at 60fps in useFrame)\n const updateMuscleActivations = (\n delta: number,\n frameCounter: number\n ): void => {\n // Update muscle system based on current action\n if (currentAnimation === \"attack\" && attackAnimation) {\n muscleManager.current.update(attackAnimation, stamina, delta);\n } else if (currentAnimation === \"defend\" || isBlocking) {\n muscleManager.current.update(\"block\", stamina, delta);\n } else if (\n currentAnimation === \"walk\" ||\n currentAnimation === \"stance_change\"\n ) {\n // Engage stance/leg/core muscles during movement and stance changes\n muscleManager.current.update(\"stance_change\", stamina, delta);\n } else if (currentAnimation === \"idle\" && stanceTension) {\n // Apply stance-based leg muscle tension for idle/guard positions\n // This provides realistic isometric contraction visualization\n\n // Get all activation states once (not in the loop)\n const allActivations = muscleManager.current.getAllActivations();\n\n // Update manager with stance muscle activations\n stanceTension.forEach((tension, muscleGroup) => {\n // Get existing activation state\n const state = allActivations.get(muscleGroup);\n\n if (state) {\n // Set target tension for smooth interpolation\n state.targetTension = tension;\n\n // Smoothly interpolate to target (using config activation speed)\n state.tension = lerp(\n state.tension,\n state.targetTension,\n DEFAULT_MUSCLE_CONFIG.activationSpeed * delta\n );\n }\n });\n } else {\n muscleManager.current.relaxAllMuscles(delta);\n }\n\n // Sync muscle states to React state deterministically\n // (every 10 frames at 60fps = ~6 times/sec)\n // Balances animation smoothness with performance and reduces GC pressure\n if (frameCounter === 0) {\n // Reuse scratch map from manager to avoid repeated allocations\n const scratchMap = muscleManager.current.getScratchMapForSync();\n setMuscleStates(scratchMap);\n }\n };\n\n return {\n muscleStates,\n updateMuscleActivations,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA0BA,IAAM,QAAQ,OAAe,KAAa,MACxC,SAAS,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoE1B,SAAgB,oBACd,SAC2B;CAC3B,MAAM,EACJ,kBACA,iBACA,aAAa,OACb,SACA,kBACE;CAGJ,MAAM,gBAAgB,OAAO,IAAI,wBAAwB,CAAC;CAC1D,MAAM,CAAC,cAAc,mBAAmB,yBACtC,IAAI,IAAI,CACV;CAGA,gBAAgB;EACd,MAAM,UAAU,cAAc;EAC9B,aAAa;GACX,IAAI;IACF,QAAQ,MAAM;GAChB,SAAS,OAAO;IACd,QAAQ,KAAK,yCAAyC,KAAK;GAC7D;EACF;CACF,GAAG,CAAC,CAAC;CAGL,MAAM,gBAAgB,cAAc;EAClC,OAAO,gBAAgB,0BAA0B,aAAa,IAAI;CACpE,GAAG,CAAC,aAAa,CAAC;CAGlB,MAAM,2BACJ,OACA,iBACS;EAET,IAAI,qBAAqB,YAAY,iBACnC,cAAc,QAAQ,OAAO,iBAAiB,SAAS,KAAK;OACvD,IAAI,qBAAqB,YAAY,YAC1C,cAAc,QAAQ,OAAO,SAAS,SAAS,KAAK;OAC/C,IACL,qBAAqB,UACrB,qBAAqB,iBAGrB,cAAc,QAAQ,OAAO,iBAAiB,SAAS,KAAK;OACvD,IAAI,qBAAqB,UAAU,eAAe;GAKvD,MAAM,iBAAiB,cAAc,QAAQ,kBAAkB;GAG/D,cAAc,SAAS,SAAS,gBAAgB;IAE9C,MAAM,QAAQ,eAAe,IAAI,WAAW;IAE5C,IAAI,OAAO;KAET,MAAM,gBAAgB;KAGtB,MAAM,UAAU,KACd,MAAM,SACN,MAAM,eACN,sBAAsB,kBAAkB,KAC1C;IACF;GACF,CAAC;EACH,OACE,cAAc,QAAQ,gBAAgB,KAAK;EAM7C,IAAI,iBAAiB,
|
|
1
|
+
{"version":3,"file":"useMuscleActivation.js","names":[],"sources":["../../src/hooks/useMuscleActivation.ts"],"sourcesContent":["/**\n * useMuscleActivation - Shared hook for muscle activation management\n *\n * Manages muscle activation state based on current actions and stamina.\n * Reduces code duplication in skeletal animation components.\n * Includes stance-based leg muscle tension for isometric holds.\n *\n * @module hooks/useMuscleActivation\n * @category Hooks\n * @korean 근육활성화훅\n */\n\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport {\n MuscleActivationManager,\n getMuscleTensionForStance,\n} from \"../systems/animation\";\nimport type { TrigramStance } from \"../types/common\";\nimport { DEFAULT_MUSCLE_CONFIG } from \"../types/muscle\";\nimport type { PlayerAnimation } from \"../types/player-visual\";\n\n/**\n * Simple lerp function for muscle tension interpolation\n * Defined at module level to avoid recreation\n * @korean 선형보간함수\n */\nconst lerp = (start: number, end: number, t: number): number =>\n start + (end - start) * t;\n\n/**\n * Options for useMuscleActivation hook\n * @korean 근육활성화훅옵션\n */\nexport interface UseMuscleActivationOptions {\n /** Current animation name */\n readonly currentAnimation: PlayerAnimation;\n /** Specific attack animation name (for attack state) */\n readonly attackAnimation?: string;\n /** Whether player is blocking */\n readonly isBlocking?: boolean;\n /** Current stamina level (0-100) */\n readonly stamina: number;\n /** Current trigram stance (for leg muscle tension) */\n readonly currentStance?: TrigramStance;\n}\n\n/**\n * Return type for useMuscleActivation hook\n * @korean 근육활성화훅반환타입\n */\nexport interface UseMuscleActivationReturn {\n /** Current muscle activation states (bone name -> activation 0-1) */\n readonly muscleStates: Map<string, number>;\n /** Update muscle activations (call in useFrame) */\n readonly updateMuscleActivations: (\n delta: number,\n frameCounter: number\n ) => void;\n}\n\n/**\n * useMuscleActivation hook\n *\n * Manages muscle activation based on current actions (attack, defend, movement).\n * Updates at 60fps with periodic state syncs to reduce re-renders.\n *\n * @param options - Muscle activation options\n * @returns Muscle states and update function\n *\n * @example\n * ```tsx\n * const { muscleStates, updateMuscleActivations } = useMuscleActivation({\n * currentAnimation: \"attack\",\n * attackAnimation: \"jab\",\n * isBlocking: false,\n * stamina: 85,\n * });\n *\n * // In useFrame callback\n * let frameCounter = 0;\n * useFrame((_, delta) => {\n * frameCounter = (frameCounter + 1) % 10;\n * updateMuscleActivations(delta, frameCounter);\n * });\n *\n * // Use muscle states in rendering\n * <BoneRenderer\n * rig={rig}\n * muscleStates={muscleStates}\n * isExhausted={stamina < 20}\n * />\n * ```\n *\n * @korean 근육활성화훅\n */\nexport function useMuscleActivation(\n options: UseMuscleActivationOptions\n): UseMuscleActivationReturn {\n const {\n currentAnimation,\n attackAnimation,\n isBlocking = false,\n stamina,\n currentStance,\n } = options;\n\n // Muscle activation manager\n const muscleManager = useRef(new MuscleActivationManager());\n const [muscleStates, setMuscleStates] = useState<Map<string, number>>(\n new Map()\n );\n\n // Cleanup muscle manager on unmount\n useEffect(() => {\n const manager = muscleManager.current;\n return () => {\n try {\n manager.reset();\n } catch (error) {\n console.warn(\"MuscleActivationManager reset failed:\", error);\n }\n };\n }, []);\n\n // Memoize stance tension to avoid recalculation at 60fps\n const stanceTension = useMemo(() => {\n return currentStance ? getMuscleTensionForStance(currentStance) : null;\n }, [currentStance]);\n\n // Update muscle activations (called at 60fps in useFrame)\n const updateMuscleActivations = (\n delta: number,\n frameCounter: number\n ): void => {\n // Update muscle system based on current action\n if (currentAnimation === \"attack\" && attackAnimation) {\n muscleManager.current.update(attackAnimation, stamina, delta);\n } else if (currentAnimation === \"defend\" || isBlocking) {\n muscleManager.current.update(\"block\", stamina, delta);\n } else if (\n currentAnimation === \"walk\" ||\n currentAnimation === \"stance_change\"\n ) {\n // Engage stance/leg/core muscles during movement and stance changes\n muscleManager.current.update(\"stance_change\", stamina, delta);\n } else if (currentAnimation === \"idle\" && stanceTension) {\n // Apply stance-based leg muscle tension for idle/guard positions\n // This provides realistic isometric contraction visualization\n\n // Get all activation states once (not in the loop)\n const allActivations = muscleManager.current.getAllActivations();\n\n // Update manager with stance muscle activations\n stanceTension.forEach((tension, muscleGroup) => {\n // Get existing activation state\n const state = allActivations.get(muscleGroup);\n\n if (state) {\n // Set target tension for smooth interpolation\n state.targetTension = tension;\n\n // Smoothly interpolate to target (using config activation speed)\n state.tension = lerp(\n state.tension,\n state.targetTension,\n DEFAULT_MUSCLE_CONFIG.activationSpeed * delta\n );\n }\n });\n } else {\n muscleManager.current.relaxAllMuscles(delta);\n }\n\n // Sync muscle states to React state deterministically\n // (every 10 frames at 60fps = ~6 times/sec)\n // Balances animation smoothness with performance and reduces GC pressure\n if (frameCounter === 0) {\n // Reuse scratch map from manager to avoid repeated allocations\n const scratchMap = muscleManager.current.getScratchMapForSync();\n setMuscleStates(scratchMap);\n }\n };\n\n return {\n muscleStates,\n updateMuscleActivations,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA0BA,IAAM,QAAQ,OAAe,KAAa,MACxC,SAAS,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoE1B,SAAgB,oBACd,SAC2B;CAC3B,MAAM,EACJ,kBACA,iBACA,aAAa,OACb,SACA,kBACE;CAGJ,MAAM,gBAAgB,OAAO,IAAI,wBAAwB,CAAC;CAC1D,MAAM,CAAC,cAAc,mBAAmB,yBACtC,IAAI,IAAI,CACV;CAGA,gBAAgB;EACd,MAAM,UAAU,cAAc;EAC9B,aAAa;GACX,IAAI;IACF,QAAQ,MAAM;GAChB,SAAS,OAAO;IACd,QAAQ,KAAK,yCAAyC,KAAK;GAC7D;EACF;CACF,GAAG,CAAC,CAAC;CAGL,MAAM,gBAAgB,cAAc;EAClC,OAAO,gBAAgB,0BAA0B,aAAa,IAAI;CACpE,GAAG,CAAC,aAAa,CAAC;CAGlB,MAAM,2BACJ,OACA,iBACS;EAET,IAAI,qBAAqB,YAAY,iBACnC,cAAc,QAAQ,OAAO,iBAAiB,SAAS,KAAK;OACvD,IAAI,qBAAqB,YAAY,YAC1C,cAAc,QAAQ,OAAO,SAAS,SAAS,KAAK;OAC/C,IACL,qBAAqB,UACrB,qBAAqB,iBAGrB,cAAc,QAAQ,OAAO,iBAAiB,SAAS,KAAK;OACvD,IAAI,qBAAqB,UAAU,eAAe;GAKvD,MAAM,iBAAiB,cAAc,QAAQ,kBAAkB;GAG/D,cAAc,SAAS,SAAS,gBAAgB;IAE9C,MAAM,QAAQ,eAAe,IAAI,WAAW;IAE5C,IAAI,OAAO;KAET,MAAM,gBAAgB;KAGtB,MAAM,UAAU,KACd,MAAM,SACN,MAAM,eACN,sBAAsB,kBAAkB,KAC1C;IACF;GACF,CAAC;EACH,OACE,cAAc,QAAQ,gBAAgB,KAAK;EAM7C,IAAI,iBAAiB,GAAG;GAEtB,MAAM,aAAa,cAAc,QAAQ,qBAAqB;GAC9D,gBAAgB,UAAU;EAC5B;CACF;CAEA,OAAO;EACL;EACA;CACF;AACF"}
|
|
@@ -87,7 +87,7 @@ function useRoundTransition(config = {}, onTransitionComplete) {
|
|
|
87
87
|
setRoundWinner(winner);
|
|
88
88
|
setCurrentRoundNumber(roundNumber);
|
|
89
89
|
setTransitionState("announcing");
|
|
90
|
-
|
|
90
|
+
const announcementTimer = setTimeout(() => {
|
|
91
91
|
setTransitionState("countdown");
|
|
92
92
|
countdownActive.current = true;
|
|
93
93
|
countdownTimer.current = setTimeout(() => {
|
|
@@ -99,6 +99,7 @@ function useRoundTransition(config = {}, onTransitionComplete) {
|
|
|
99
99
|
}, mergedConfig.transitionDuration);
|
|
100
100
|
}, mergedConfig.countdownDuration * 1e3);
|
|
101
101
|
}, mergedConfig.announcementDuration * 1e3);
|
|
102
|
+
transitionTimer.current = announcementTimer;
|
|
102
103
|
}, [
|
|
103
104
|
clearTimers,
|
|
104
105
|
mergedConfig,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useRoundTransition.js","names":[],"sources":["../../src/hooks/useRoundTransition.ts"],"sourcesContent":["/**\n * useRoundTransition Hook - Manages round transition state and timing\n *\n * Korean: 라운드 전환 훅 (Round Transition Hook)\n *\n * Handles the state machine for round transitions:\n * - idle: Normal combat state\n * - announcing: Showing round announcement\n * - countdown: Counting down to next round\n * - transitioning: Brief transition to next round\n *\n * @module hooks/useRoundTransition\n * @category Combat Hooks\n */\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { PlayerState } from \"../systems\";\n\n/**\n * Round transition states\n *\n * Korean: 라운드 전환 상태\n */\nexport type RoundTransitionState =\n | \"idle\"\n | \"announcing\"\n | \"countdown\"\n | \"transitioning\";\n\n/**\n * Round transition configuration\n */\nexport interface RoundTransitionConfig {\n /** Duration of announcement display in seconds */\n readonly announcementDuration?: number;\n /** Duration of countdown in seconds */\n readonly countdownDuration?: number;\n /** Duration of transition phase in milliseconds */\n readonly transitionDuration?: number;\n}\n\n/**\n * Round transition hook state\n */\nexport interface UseRoundTransitionResult {\n /** Current transition state */\n readonly transitionState: RoundTransitionState;\n /** Whether announcement should be visible */\n readonly showAnnouncement: boolean;\n /** Start round transition sequence */\n readonly startTransition: (\n winner: PlayerState | null,\n roundNumber: number\n ) => void;\n /** Skip countdown and proceed immediately */\n readonly skipCountdown: () => void;\n /** Reset transition state to idle */\n readonly resetTransition: () => void;\n /** Round winner for current transition */\n readonly roundWinner: PlayerState | null;\n /** Round number for current transition */\n readonly currentRoundNumber: number;\n}\n\n/**\n * Default configuration values\n */\nconst DEFAULT_CONFIG: Required<RoundTransitionConfig> = {\n announcementDuration: 2,\n countdownDuration: 3,\n transitionDuration: 500,\n};\n\n/**\n * useRoundTransition Hook\n *\n * Manages the complete round transition flow:\n * 1. Idle state during normal combat\n * 2. Announcing state shows round results\n * 3. Countdown state counts down to next round\n * 4. Transitioning state briefly transitions to next round\n * 5. Returns to idle for next round\n *\n * @param config - Configuration for transition timings\n * @param onTransitionComplete - Callback when transition completes\n * @returns Round transition state and control functions\n *\n * @example\n * ```typescript\n * const {\n * transitionState,\n * showAnnouncement,\n * startTransition,\n * skipCountdown,\n * } = useRoundTransition(\n * { countdownDuration: 3 },\n * () => {\n * // Start next round\n * initializeNextRound();\n * }\n * );\n *\n * // When round ends\n * startTransition(winner, roundNumber);\n * ```\n */\nexport function useRoundTransition(\n config: RoundTransitionConfig = {},\n onTransitionComplete?: () => void\n): UseRoundTransitionResult {\n const mergedConfig = useMemo(\n () => ({ ...DEFAULT_CONFIG, ...config }),\n [config]\n );\n\n const [transitionState, setTransitionState] =\n useState<RoundTransitionState>(\"idle\");\n const [roundWinner, setRoundWinner] = useState<PlayerState | null>(null);\n const [currentRoundNumber, setCurrentRoundNumber] = useState(0);\n\n // Use ref to track if we should continue countdown\n const countdownActive = useRef(false);\n const countdownTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n const transitionTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n /**\n * Clear all active timers\n */\n const clearTimers = useCallback(() => {\n if (countdownTimer.current) {\n clearInterval(countdownTimer.current);\n countdownTimer.current = null;\n }\n if (transitionTimer.current) {\n clearTimeout(transitionTimer.current);\n transitionTimer.current = null;\n }\n }, []);\n\n /**\n * Start the transition sequence\n */\n const startTransition = useCallback(\n (winner: PlayerState | null, roundNumber: number) => {\n clearTimers();\n setRoundWinner(winner);\n setCurrentRoundNumber(roundNumber);\n setTransitionState(\"announcing\");\n\n // Move to countdown after announcement\n const announcementTimer = setTimeout(() => {\n setTransitionState(\"countdown\");\n countdownActive.current = true;\n\n // Countdown will be managed by the RoundAnnouncement component\n // After countdownDuration, move to transitioning state\n countdownTimer.current = setTimeout(() => {\n countdownActive.current = false;\n setTransitionState(\"transitioning\");\n\n // Complete transition after brief delay\n transitionTimer.current = setTimeout(() => {\n setTransitionState(\"idle\");\n onTransitionComplete?.();\n }, mergedConfig.transitionDuration);\n }, mergedConfig.countdownDuration * 1000);\n }, mergedConfig.announcementDuration * 1000);\n\n // Store timer ref for cleanup\n transitionTimer.current = announcementTimer;\n },\n [clearTimers, mergedConfig, onTransitionComplete]\n );\n\n /**\n * Skip countdown and proceed immediately to next round\n */\n const skipCountdown = useCallback(() => {\n if (transitionState === \"announcing\" || transitionState === \"countdown\") {\n clearTimers();\n countdownActive.current = false;\n setTransitionState(\"transitioning\");\n\n // Complete transition after brief delay\n transitionTimer.current = setTimeout(() => {\n setTransitionState(\"idle\");\n onTransitionComplete?.();\n }, mergedConfig.transitionDuration);\n }\n }, [\n transitionState,\n clearTimers,\n mergedConfig.transitionDuration,\n onTransitionComplete,\n ]);\n\n /**\n * Reset transition to idle state\n */\n const resetTransition = useCallback(() => {\n clearTimers();\n countdownActive.current = false;\n setTransitionState(\"idle\");\n setRoundWinner(null);\n setCurrentRoundNumber(0);\n }, [clearTimers]);\n\n /**\n * Cleanup on unmount\n */\n useEffect(() => {\n return () => {\n clearTimers();\n countdownActive.current = false;\n };\n }, [clearTimers]);\n\n return {\n transitionState,\n showAnnouncement:\n transitionState === \"announcing\" || transitionState === \"countdown\",\n startTransition,\n skipCountdown,\n resetTransition,\n roundWinner,\n currentRoundNumber,\n };\n}\n\nexport default useRoundTransition;\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAmEA,IAAM,iBAAkD;CACtD,sBAAsB;CACtB,mBAAmB;CACnB,oBAAoB;AACtB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCA,SAAgB,mBACd,SAAgC,CAAC,GACjC,sBAC0B;CAC1B,MAAM,eAAe,eACZ;EAAE,GAAG;EAAgB,GAAG;CAAO,IACtC,CAAC,MAAM,CACT;CAEA,MAAM,CAAC,iBAAiB,sBACtB,SAA+B,MAAM;CACvC,MAAM,CAAC,aAAa,kBAAkB,SAA6B,IAAI;CACvE,MAAM,CAAC,oBAAoB,yBAAyB,SAAS,CAAC;CAG9D,MAAM,kBAAkB,OAAO,KAAK;CACpC,MAAM,iBAAiB,OAA6C,IAAI;CACxE,MAAM,kBAAkB,OAA6C,IAAI;;;;CAKzE,MAAM,cAAc,kBAAkB;EACpC,IAAI,eAAe,SAAS;GAC1B,cAAc,eAAe,OAAO;GACpC,eAAe,UAAU;EAC3B;EACA,IAAI,gBAAgB,SAAS;GAC3B,aAAa,gBAAgB,OAAO;GACpC,gBAAgB,UAAU;EAC5B;CACF,GAAG,CAAC,CAAC;;;;CAKL,MAAM,kBAAkB,aACrB,QAA4B,gBAAwB;EACnD,YAAY;EACZ,eAAe,MAAM;EACrB,sBAAsB,WAAW;EACjC,mBAAmB,YAAY;
|
|
1
|
+
{"version":3,"file":"useRoundTransition.js","names":[],"sources":["../../src/hooks/useRoundTransition.ts"],"sourcesContent":["/**\n * useRoundTransition Hook - Manages round transition state and timing\n *\n * Korean: 라운드 전환 훅 (Round Transition Hook)\n *\n * Handles the state machine for round transitions:\n * - idle: Normal combat state\n * - announcing: Showing round announcement\n * - countdown: Counting down to next round\n * - transitioning: Brief transition to next round\n *\n * @module hooks/useRoundTransition\n * @category Combat Hooks\n */\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { PlayerState } from \"../systems\";\n\n/**\n * Round transition states\n *\n * Korean: 라운드 전환 상태\n */\nexport type RoundTransitionState =\n | \"idle\"\n | \"announcing\"\n | \"countdown\"\n | \"transitioning\";\n\n/**\n * Round transition configuration\n */\nexport interface RoundTransitionConfig {\n /** Duration of announcement display in seconds */\n readonly announcementDuration?: number;\n /** Duration of countdown in seconds */\n readonly countdownDuration?: number;\n /** Duration of transition phase in milliseconds */\n readonly transitionDuration?: number;\n}\n\n/**\n * Round transition hook state\n */\nexport interface UseRoundTransitionResult {\n /** Current transition state */\n readonly transitionState: RoundTransitionState;\n /** Whether announcement should be visible */\n readonly showAnnouncement: boolean;\n /** Start round transition sequence */\n readonly startTransition: (\n winner: PlayerState | null,\n roundNumber: number\n ) => void;\n /** Skip countdown and proceed immediately */\n readonly skipCountdown: () => void;\n /** Reset transition state to idle */\n readonly resetTransition: () => void;\n /** Round winner for current transition */\n readonly roundWinner: PlayerState | null;\n /** Round number for current transition */\n readonly currentRoundNumber: number;\n}\n\n/**\n * Default configuration values\n */\nconst DEFAULT_CONFIG: Required<RoundTransitionConfig> = {\n announcementDuration: 2,\n countdownDuration: 3,\n transitionDuration: 500,\n};\n\n/**\n * useRoundTransition Hook\n *\n * Manages the complete round transition flow:\n * 1. Idle state during normal combat\n * 2. Announcing state shows round results\n * 3. Countdown state counts down to next round\n * 4. Transitioning state briefly transitions to next round\n * 5. Returns to idle for next round\n *\n * @param config - Configuration for transition timings\n * @param onTransitionComplete - Callback when transition completes\n * @returns Round transition state and control functions\n *\n * @example\n * ```typescript\n * const {\n * transitionState,\n * showAnnouncement,\n * startTransition,\n * skipCountdown,\n * } = useRoundTransition(\n * { countdownDuration: 3 },\n * () => {\n * // Start next round\n * initializeNextRound();\n * }\n * );\n *\n * // When round ends\n * startTransition(winner, roundNumber);\n * ```\n */\nexport function useRoundTransition(\n config: RoundTransitionConfig = {},\n onTransitionComplete?: () => void\n): UseRoundTransitionResult {\n const mergedConfig = useMemo(\n () => ({ ...DEFAULT_CONFIG, ...config }),\n [config]\n );\n\n const [transitionState, setTransitionState] =\n useState<RoundTransitionState>(\"idle\");\n const [roundWinner, setRoundWinner] = useState<PlayerState | null>(null);\n const [currentRoundNumber, setCurrentRoundNumber] = useState(0);\n\n // Use ref to track if we should continue countdown\n const countdownActive = useRef(false);\n const countdownTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n const transitionTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n /**\n * Clear all active timers\n */\n const clearTimers = useCallback(() => {\n if (countdownTimer.current) {\n clearInterval(countdownTimer.current);\n countdownTimer.current = null;\n }\n if (transitionTimer.current) {\n clearTimeout(transitionTimer.current);\n transitionTimer.current = null;\n }\n }, []);\n\n /**\n * Start the transition sequence\n */\n const startTransition = useCallback(\n (winner: PlayerState | null, roundNumber: number) => {\n clearTimers();\n setRoundWinner(winner);\n setCurrentRoundNumber(roundNumber);\n setTransitionState(\"announcing\");\n\n // Move to countdown after announcement\n const announcementTimer = setTimeout(() => {\n setTransitionState(\"countdown\");\n countdownActive.current = true;\n\n // Countdown will be managed by the RoundAnnouncement component\n // After countdownDuration, move to transitioning state\n countdownTimer.current = setTimeout(() => {\n countdownActive.current = false;\n setTransitionState(\"transitioning\");\n\n // Complete transition after brief delay\n transitionTimer.current = setTimeout(() => {\n setTransitionState(\"idle\");\n onTransitionComplete?.();\n }, mergedConfig.transitionDuration);\n }, mergedConfig.countdownDuration * 1000);\n }, mergedConfig.announcementDuration * 1000);\n\n // Store timer ref for cleanup\n transitionTimer.current = announcementTimer;\n },\n [clearTimers, mergedConfig, onTransitionComplete]\n );\n\n /**\n * Skip countdown and proceed immediately to next round\n */\n const skipCountdown = useCallback(() => {\n if (transitionState === \"announcing\" || transitionState === \"countdown\") {\n clearTimers();\n countdownActive.current = false;\n setTransitionState(\"transitioning\");\n\n // Complete transition after brief delay\n transitionTimer.current = setTimeout(() => {\n setTransitionState(\"idle\");\n onTransitionComplete?.();\n }, mergedConfig.transitionDuration);\n }\n }, [\n transitionState,\n clearTimers,\n mergedConfig.transitionDuration,\n onTransitionComplete,\n ]);\n\n /**\n * Reset transition to idle state\n */\n const resetTransition = useCallback(() => {\n clearTimers();\n countdownActive.current = false;\n setTransitionState(\"idle\");\n setRoundWinner(null);\n setCurrentRoundNumber(0);\n }, [clearTimers]);\n\n /**\n * Cleanup on unmount\n */\n useEffect(() => {\n return () => {\n clearTimers();\n countdownActive.current = false;\n };\n }, [clearTimers]);\n\n return {\n transitionState,\n showAnnouncement:\n transitionState === \"announcing\" || transitionState === \"countdown\",\n startTransition,\n skipCountdown,\n resetTransition,\n roundWinner,\n currentRoundNumber,\n };\n}\n\nexport default useRoundTransition;\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAmEA,IAAM,iBAAkD;CACtD,sBAAsB;CACtB,mBAAmB;CACnB,oBAAoB;AACtB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCA,SAAgB,mBACd,SAAgC,CAAC,GACjC,sBAC0B;CAC1B,MAAM,eAAe,eACZ;EAAE,GAAG;EAAgB,GAAG;CAAO,IACtC,CAAC,MAAM,CACT;CAEA,MAAM,CAAC,iBAAiB,sBACtB,SAA+B,MAAM;CACvC,MAAM,CAAC,aAAa,kBAAkB,SAA6B,IAAI;CACvE,MAAM,CAAC,oBAAoB,yBAAyB,SAAS,CAAC;CAG9D,MAAM,kBAAkB,OAAO,KAAK;CACpC,MAAM,iBAAiB,OAA6C,IAAI;CACxE,MAAM,kBAAkB,OAA6C,IAAI;;;;CAKzE,MAAM,cAAc,kBAAkB;EACpC,IAAI,eAAe,SAAS;GAC1B,cAAc,eAAe,OAAO;GACpC,eAAe,UAAU;EAC3B;EACA,IAAI,gBAAgB,SAAS;GAC3B,aAAa,gBAAgB,OAAO;GACpC,gBAAgB,UAAU;EAC5B;CACF,GAAG,CAAC,CAAC;;;;CAKL,MAAM,kBAAkB,aACrB,QAA4B,gBAAwB;EACnD,YAAY;EACZ,eAAe,MAAM;EACrB,sBAAsB,WAAW;EACjC,mBAAmB,YAAY;EAG/B,MAAM,oBAAoB,iBAAiB;GACzC,mBAAmB,WAAW;GAC9B,gBAAgB,UAAU;GAI1B,eAAe,UAAU,iBAAiB;IACxC,gBAAgB,UAAU;IAC1B,mBAAmB,eAAe;IAGlC,gBAAgB,UAAU,iBAAiB;KACzC,mBAAmB,MAAM;KACzB,uBAAuB;IACzB,GAAG,aAAa,kBAAkB;GACpC,GAAG,aAAa,oBAAoB,GAAI;EAC1C,GAAG,aAAa,uBAAuB,GAAI;EAG3C,gBAAgB,UAAU;CAC5B,GACA;EAAC;EAAa;EAAc;CAAoB,CAClD;;;;CAKA,MAAM,gBAAgB,kBAAkB;EACtC,IAAI,oBAAoB,gBAAgB,oBAAoB,aAAa;GACvE,YAAY;GACZ,gBAAgB,UAAU;GAC1B,mBAAmB,eAAe;GAGlC,gBAAgB,UAAU,iBAAiB;IACzC,mBAAmB,MAAM;IACzB,uBAAuB;GACzB,GAAG,aAAa,kBAAkB;EACpC;CACF,GAAG;EACD;EACA;EACA,aAAa;EACb;CACF,CAAC;;;;CAKD,MAAM,kBAAkB,kBAAkB;EACxC,YAAY;EACZ,gBAAgB,UAAU;EAC1B,mBAAmB,MAAM;EACzB,eAAe,IAAI;EACnB,sBAAsB,CAAC;CACzB,GAAG,CAAC,WAAW,CAAC;;;;CAKhB,gBAAgB;EACd,aAAa;GACX,YAAY;GACZ,gBAAgB,UAAU;EAC5B;CACF,GAAG,CAAC,WAAW,CAAC;CAEhB,OAAO;EACL;EACA,kBACE,oBAAoB,gBAAgB,oBAAoB;EAC1D;EACA;EACA;EACA;EACA;CACF;AACF"}
|
|
@@ -2,9 +2,9 @@ import { getDefensiveAnimation } from "../systems/animation/catalogs/DefensiveAn
|
|
|
2
2
|
import { getAttackAnimation } from "../systems/animation/catalogs/StanceAttackAnimations.js";
|
|
3
3
|
import { getAnimation, getAnimationByName } from "../systems/animation/core/AnimationRegistry.js";
|
|
4
4
|
import { batchUpdateBones, interpolateKeyframeCached, performanceMonitor } from "../systems/animation/core/AnimationOptimizations.js";
|
|
5
|
+
import { applyLaterality } from "../systems/animation/core/LateralityTransform.js";
|
|
5
6
|
import { getStepAnimation } from "../systems/animation/catalogs/StepSkeletalAnimations.js";
|
|
6
7
|
import { getFootworkAnimation } from "../systems/animation/catalogs/FootworkSkeletalAnimations.js";
|
|
7
|
-
import { applyLaterality } from "../systems/animation/core/LateralityTransform.js";
|
|
8
8
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
9
9
|
//#region src/hooks/useSkeletalAnimation.ts
|
|
10
10
|
/**
|
package/lib/index.js
CHANGED
|
@@ -42,7 +42,9 @@ import { DARK_OPS_ARCHETYPE_BONUSES, DARK_OPS_NIGHT_BONUS, DARK_OPS_SPECIAL_EFFE
|
|
|
42
42
|
import { DAMAGE_TYPE_EFFECTIVENESS, KOREAN_TECHNIQUE_CATEGORIES, TECHNIQUE_DIFFICULTY_LEVELS, TECHNIQUE_MODIFIERS, TECHNIQUE_NAMING, TECHNIQUE_PROPERTIES, TRIGRAM_TECHNIQUE_PROPERTIES, calculateDamageEffectiveness, getTrigramKey, getTrigramProperties } from "./systems/trigram/techniques/TechniqueConfig.js";
|
|
43
43
|
import { TRIGRAM_TECHNIQUES, getAllTechniques, getTechniqueById, getTechniqueCountByStance, getTotalTechniqueCount } from "./systems/trigram/techniques/index.js";
|
|
44
44
|
import { KoreanTechniquesSystem, TECHNIQUE_EFFECTIVENESS_MATRIX, getTechniquesByStance } from "./systems/trigram/KoreanTechniques.js";
|
|
45
|
+
import { STANCE_SPEED_MODIFIERS } from "./systems/physics/MovementPhysics.js";
|
|
45
46
|
import { EffectIntensity, HitEffectEnum, HitEffectType } from "./systems/effects.js";
|
|
47
|
+
import { DEFAULT_INJURY_MOVEMENT_CONFIG, InjuryMovementModifier, injuryMovementModifier } from "./systems/movement/InjuryMovementModifier.js";
|
|
46
48
|
import { BodyPartHealthSystem, bodyPartHealthSystem } from "./systems/bodypart/BodyPartHealthSystem.js";
|
|
47
49
|
import { applyDamageToBodyParts, applyVitalPointDamageToBodyParts, getBodyPartCombatEffects, getBodyRegionFromVitalPoint, healBodyPartsProportionally, initializeBodyPartHealthForPlayer, isPlayerIncapacitatedByBodyDamage } from "./systems/bodypart/BodyPartDamageIntegration.js";
|
|
48
50
|
import { MovementPenaltySystem, movementPenaltySystem } from "./systems/bodypart/MovementPenaltySystem.js";
|
|
@@ -76,8 +78,6 @@ import { AIActionType } from "./systems/ai/types.js";
|
|
|
76
78
|
import { ARCHETYPE_ENFORCEMENT, enforceArchetypeBehavior, getActionFrequency, getSignatureMove, isActionProhibited, shouldExecuteSignatureMove } from "./systems/ai/ArchetypeEnforcer.js";
|
|
77
79
|
import { AIDecisionTree } from "./systems/ai/DecisionTree.js";
|
|
78
80
|
import { TrainingAI } from "./systems/ai/TrainingAI.js";
|
|
79
|
-
import { STANCE_SPEED_MODIFIERS } from "./systems/physics/MovementPhysics.js";
|
|
80
|
-
import { DEFAULT_INJURY_MOVEMENT_CONFIG, InjuryMovementModifier, injuryMovementModifier } from "./systems/movement/InjuryMovementModifier.js";
|
|
81
81
|
import { getColorRGB, mixColors, toHexColor } from "./utils/colorHelpers.js";
|
|
82
82
|
import { TrainingCombatSystem } from "./systems/combat/TrainingCombatSystem.js";
|
|
83
83
|
import { isValidArchetype, isVitalPoint, isVitalPointCategory } from "./systems/combat/typeGuards.js";
|
|
@@ -393,7 +393,8 @@ var VitalPointSystem = class {
|
|
|
393
393
|
}
|
|
394
394
|
const meridianStates = {};
|
|
395
395
|
meridians.forEach((meridianId) => {
|
|
396
|
-
|
|
396
|
+
const disruption = this.getMeridianDisruption(meridianId);
|
|
397
|
+
meridianStates[meridianId] = 1 - disruption;
|
|
397
398
|
});
|
|
398
399
|
const vulnerabilityMultiplier = calculateEnhancedVulnerability(vitalPoint.position, hour, defenderStance, meridianStates);
|
|
399
400
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VitalPointSystem.js","names":[],"sources":["../../src/systems/VitalPointSystem.ts"],"sourcesContent":["/**\n * System for managing Korean martial arts vital point (급소) targeting and damage calculation.\n *\n * **Korean**: 급소 시스템 (Vital Point System)\n *\n * The VitalPointSystem implements anatomical targeting mechanics based on traditional\n * Korean martial arts knowledge of 70 vital points (급소). It handles hit detection,\n * damage calculation, and status effect application for precise strikes.\n *\n * ## Vital Point Philosophy\n *\n * Korean martial arts identify specific anatomical locations that, when struck precisely,\n * can cause disproportionate effects. The system categorizes points by:\n *\n * - **Location**: Head, neck, torso, limbs, and core\n * - **Category**: Neurological, vascular, respiratory, muscular, skeletal, etc.\n * - **Severity**: Minor, moderate, major, critical, lethal\n * - **Effects**: Unconsciousness, paralysis, pain, stunning, etc.\n *\n * ## Meridian Integration (경락 통합)\n *\n * The system integrates Traditional Korean Medicine (TKM) meridian theory:\n * - **Time-of-Day Flow**: +30% effectiveness at meridian peak hours\n * - **Meridian Disruption**: Status effects from energy flow blockage\n * - **Elemental Relationships**: 五行 (Wu Xing) elemental advantages\n *\n * ## Key Features\n *\n * - Distance-based hit detection with accuracy falloff\n * - Severity multipliers for damage calculation\n * - Targeted vs. proximity-based strikes\n * - Bilingual Korean-English vital point names\n * - Realistic anatomical positioning\n * - Meridian state tracking and flow calculation\n *\n * @example\n * ```typescript\n * const vitalPointSystem = new VitalPointSystem();\n *\n * // Process a strike at specific position with time-of-day\n * const result = vitalPointSystem.processHit(\n * { x: 100, y: 50 }, // Target position\n * { width: 10, height: 10 }, // Hit box\n * \"GB-20\", // Optional: target specific vital point\n * 14 // Optional: hour of day for meridian flow\n * );\n *\n * if (result.hit && result.vitalPointHit) {\n * console.log(`Hit ${result.vitalPointHit.names.english}!`);\n * console.log(`Damage: ${result.damage}, Severity: ${result.severity}`);\n * console.log(`Meridian bonus: ${result.meridianMultiplier}x`);\n * }\n * ```\n *\n * @category Vital Point System\n * @korean 급소시스템\n */\nimport {\n PlayerArchetype,\n Position,\n TrigramStance,\n VitalPointSeverity,\n} from \"../types/common\";\nimport { convertToStatusEffect } from \"./EffectCalculator\";\nimport { StatusEffect } from \"./types\";\nimport {\n calculateEnhancedVulnerability,\n calculateMeridianFlow,\n generateMeridianEffects,\n} from \"./vitalpoint/KoreanAnatomy\";\nimport { getMeridiansForVitalPoint } from \"./vitalpoint/MeridianVitalPointMapping\";\nimport { VitalPoint, VitalPointHitResult } from \"./vitalpoint/types\";\nimport { VITAL_POINTS_DATA } from \"./vitalpoint/VitalPointsData\";\n\n/**\n * Amount of meridian disruption added per vital point hit (15%)\n */\nconst DISRUPTION_INCREMENT_PER_HIT = 0.15;\n\nexport class VitalPointSystem {\n private vitalPoints: VitalPoint[] = [];\n private meridianStates: Map<string, number> = new Map(); // Tracks disruption level per meridian\n private currentHour: number = 12; // Default to noon\n\n /**\n * Creates a new VitalPointSystem instance.\n *\n * Initializes the system with comprehensive Korean vital points database\n * and meridian state tracking.\n *\n * @param initialHour - Optional initial hour of day (0-23) for meridian flow calculations\n */\n constructor(initialHour: number = 12) {\n this.initializeVitalPoints();\n this.currentHour = initialHour;\n }\n\n /**\n * Sets the current hour of day for meridian flow calculations.\n *\n * **Korean**: 시간 설정\n *\n * @param hour - Hour of day (0-23)\n * @throws Error if hour is not a finite number\n *\n * @example\n * ```typescript\n * vitalPointSystem.setCurrentHour(20); // 8 PM - Pericardium peak time\n * ```\n *\n * @korean 시간설정\n */\n setCurrentHour(hour: number): void {\n if (!Number.isFinite(hour)) {\n throw new Error(\"Hour must be a finite number\");\n }\n this.currentHour = Math.max(0, Math.min(23, Math.floor(hour)));\n }\n\n /**\n * Gets the current hour of day used for meridian calculations.\n *\n * **Korean**: 현재 시간 조회\n *\n * @returns Current hour (0-23)\n *\n * @korean 시간조회\n */\n getCurrentHour(): number {\n return this.currentHour;\n }\n\n /**\n * Updates meridian disruption state for a specific meridian.\n *\n * **Korean**: 경락 차단 상태 업데이트\n *\n * @param meridianId - ID of the meridian\n * @param disruptionLevel - Disruption level (0-1, where 1 is fully blocked)\n *\n * @korean 경락차단업데이트\n */\n setMeridianDisruption(meridianId: string, disruptionLevel: number): void {\n this.meridianStates.set(\n meridianId,\n Math.max(0, Math.min(1, disruptionLevel)),\n );\n }\n\n /**\n * Gets the current disruption level for a meridian.\n *\n * **Korean**: 경락 차단 수준 조회\n *\n * @param meridianId - ID of the meridian\n * @returns Disruption level (0-1)\n *\n * @korean 경락차단조회\n */\n getMeridianDisruption(meridianId: string): number {\n return this.meridianStates.get(meridianId) ?? 0;\n }\n\n /**\n * Clears all meridian disruption states (e.g., after rest or healing).\n *\n * **Korean**: 경락 상태 초기화\n *\n * @korean 경락초기화\n */\n clearMeridianDisruptions(): void {\n this.meridianStates.clear();\n }\n\n /**\n * Processes a hit at a specific position to determine vital point impact.\n *\n * Evaluates whether a strike lands on or near a vital point, calculating\n * damage and effects based on accuracy, severity, and meridian flow.\n * Supports both targeted strikes (specific vital point ID) and proximity-based detection.\n *\n * Enhanced with archetype-specific modifiers for realistic combat simulation.\n *\n * ## Hit Detection Algorithm\n *\n * 1. If targetedVitalPointId provided, validate that specific point\n * 2. Otherwise, find closest vital point to target position\n * 3. Calculate distance from strike to vital point\n * 4. Apply accuracy falloff based on distance (max 50px)\n * 5. Calculate meridian flow bonus based on time of day\n * 6. Apply meridian disruption if applicable\n * 7. Apply archetype offensive/defensive modifiers\n * 8. Apply enhanced vulnerability with stance and anatomical zone\n * 9. Return hit result with damage multipliers and effects\n *\n * @param targetPosition - Position where strike lands\n * @param _hitBox - Size of hit box (currently unused, reserved for future)\n * @param targetedVitalPointId - Optional specific vital point being targeted\n * @param hour - Optional hour of day (0-23) for meridian flow calculation\n * @param attackerArchetype - Optional attacker's archetype (default: MUSA)\n * @param defenderArchetype - Optional defender's archetype (default: MUSA)\n * @param defenderStance - Optional defender's trigram stance (default: GEON)\n * @returns Hit result with damage, effects, severity, and meridian multiplier\n *\n * @example\n * ```typescript\n * // Proximity-based strike\n * const result1 = vitalPointSystem.processHit(\n * { x: 100, y: 50 },\n * { width: 10, height: 10 }\n * );\n *\n * // Targeted strike with archetype modifiers and stance\n * const result2 = vitalPointSystem.processHit(\n * { x: 100, y: 50 },\n * { width: 10, height: 10 },\n * \"head_temple\",\n * 2, // Liver meridian peak hour\n * PlayerArchetype.AMSALJA, // Assassin attacker (+30% effect)\n * PlayerArchetype.MUSA, // Warrior defender (+20% resistance)\n * TrigramStance.GEON // Heaven stance (exposes head +20%)\n * );\n * ```\n *\n * @korean 타격처리\n */\n processHit(\n targetPosition: Position,\n _hitBox: { width: number; height: number }, // Prefixed with underscore to indicate intentionally unused\n targetedVitalPointId?: string | null,\n hour?: number,\n attackerArchetype?: PlayerArchetype,\n defenderArchetype?: PlayerArchetype,\n defenderStance?: TrigramStance,\n ): VitalPointHitResult {\n const effectiveHour = hour ?? this.currentHour;\n const effectiveAttackerArchetype =\n attackerArchetype ?? PlayerArchetype.MUSA;\n const effectiveDefenderArchetype =\n defenderArchetype ?? PlayerArchetype.MUSA;\n const effectiveDefenderStance = defenderStance ?? TrigramStance.GEON;\n\n // If a specific vital point is targeted, check that one\n if (targetedVitalPointId) {\n const targetVitalPoint = this.getVitalPointById(targetedVitalPointId);\n if (targetVitalPoint) {\n return this.calculateVitalPointHit(\n targetVitalPoint,\n targetPosition,\n effectiveHour,\n effectiveAttackerArchetype,\n effectiveDefenderArchetype,\n effectiveDefenderStance,\n );\n }\n }\n\n // Otherwise, find the closest vital point\n const closestVitalPoint = this.findClosestVitalPoint(targetPosition);\n if (!closestVitalPoint) {\n return {\n hit: false,\n damage: 0,\n effects: [],\n severity: VitalPointSeverity.MINOR,\n };\n }\n\n const distance = this.calculateDistance(\n targetPosition,\n closestVitalPoint.position,\n );\n const maxHitDistance = 50; // pixels\n\n if (distance <= maxHitDistance) {\n return this.calculateVitalPointHit(\n closestVitalPoint,\n targetPosition,\n effectiveHour,\n effectiveAttackerArchetype,\n effectiveDefenderArchetype,\n effectiveDefenderStance,\n );\n }\n\n return {\n hit: false,\n damage: 0,\n effects: [],\n severity: VitalPointSeverity.MINOR,\n };\n }\n\n /**\n * Retrieves a vital point by its unique identifier.\n *\n * @param id - Vital point identifier (e.g., \"GB-20\", \"head_temple\")\n * @returns Vital point if found, null otherwise\n *\n * @example\n * ```typescript\n * const temple = vitalPointSystem.getVitalPointById(\"head_temple\");\n * if (temple) {\n * console.log(temple.names.korean); // \"태양혈\"\n * }\n * ```\n *\n * @korean 급소조회\n */\n getVitalPointById(id: string): VitalPoint | null {\n return this.vitalPoints.find((vp) => vp.id === id) ?? null;\n }\n\n /**\n * Gets all registered vital points in the system.\n *\n * @returns Read-only array of all vital points\n *\n * @example\n * ```typescript\n * const allPoints = vitalPointSystem.getVitalPoints();\n * console.log(`${allPoints.length} vital points registered`);\n * ```\n *\n * @korean 급소목록조회\n */\n getVitalPoints(): readonly VitalPoint[] {\n return this.vitalPoints;\n }\n\n /**\n * Calculates hit result for a technique-based attack.\n *\n * Determines whether a technique successfully lands on a vital point,\n * factoring in technique accuracy, attacker position, and randomness.\n * Used for AI and player technique execution.\n *\n * @param technique - Technique being executed with accuracy property\n * @param attackerPosition - Position where attack originates\n * @param _defenderPosition - Defender position (reserved for future use)\n * @param _defenderStance - Defender stance (reserved for future use)\n * @returns Hit result with damage and effects\n *\n * @example\n * ```typescript\n * const technique = {\n * id: \"geon-thunder-strike\",\n * accuracy: 0.85\n * };\n *\n * const result = vitalPointSystem.calculateHit(\n * technique,\n * { x: 100, y: 50 },\n * { x: 120, y: 50 },\n * TrigramStance.GEON\n * );\n * ```\n *\n * @korean 기술타격계산\n */\n calculateHit(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- technique type is dynamically determined by combat system\n technique: any,\n attackerPosition: Position,\n _defenderPosition: Position, // Prefixed with underscore to indicate intentionally unused\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- stance type is dynamically determined by combat system\n _defenderStance: any, // Prefixed with underscore to indicate intentionally unused\n ): VitalPointHitResult {\n const closestVitalPoint = this.findClosestVitalPoint(attackerPosition);\n\n if (!closestVitalPoint) {\n return {\n hit: false,\n damage: 0,\n effects: [],\n severity: VitalPointSeverity.MINOR,\n };\n }\n\n const distance = this.calculateDistance(\n attackerPosition,\n closestVitalPoint.position,\n );\n const hitChance = technique.accuracy * (1 - distance / 100);\n\n if (Math.random() < hitChance) {\n return {\n hit: true,\n vitalPointHit: closestVitalPoint,\n damage: this.calculateBaseDamage(closestVitalPoint, distance),\n effects: [],\n severity: closestVitalPoint.severity,\n };\n }\n\n return {\n hit: false,\n damage: 0,\n effects: [],\n severity: VitalPointSeverity.MINOR,\n };\n }\n\n /**\n * Finds the closest vital point to a given position.\n *\n * Uses Euclidean distance to determine proximity.\n *\n * @param position - Target position\n * @returns Closest vital point or null if none registered\n *\n * @private\n * @korean 최근접급소검색\n */\n private findClosestVitalPoint(position: Position): VitalPoint | null {\n if (this.vitalPoints.length === 0) return null;\n\n let closest = this.vitalPoints[0];\n let minDistance = this.calculateDistance(position, closest.position);\n\n for (const vitalPoint of this.vitalPoints) {\n const distance = this.calculateDistance(position, vitalPoint.position);\n if (distance < minDistance) {\n minDistance = distance;\n closest = vitalPoint;\n }\n }\n\n return closest;\n }\n\n /**\n * Calculates Euclidean distance between two positions.\n *\n * @param pos1 - First position\n * @param pos2 - Second position\n * @returns Distance in pixels\n *\n * @private\n * @korean 거리계산\n */\n private calculateDistance(pos1: Position, pos2: Position): number {\n const dx = pos1.x - pos2.x;\n const dy = pos1.y - pos2.y;\n return Math.sqrt(dx * dx + dy * dy);\n }\n\n /**\n * Calculates vital point hit result with accuracy, damage, and meridian effects.\n *\n * **Korean**: 급소타격결과계산\n *\n * Enhanced with comprehensive effect calculation including:\n * - Duration based on accuracy, severity, and archetype modifiers\n * - Intensity scaling with hit accuracy\n * - Critical hit bonuses for accuracy >= 0.9\n * - Archetype offensive/defensive modifiers\n *\n * @param vitalPoint - Vital point being struck\n * @param hitPosition - Exact position of strike\n * @param hour - Hour of day for meridian flow calculation\n * @param attackerArchetype - Attacker's archetype (default: MUSA)\n * @param defenderArchetype - Defender's archetype (default: MUSA)\n * @returns Hit result with calculated damage, accuracy, and meridian bonuses\n *\n * @private\n * @korean 급소타격결과계산\n */\n private calculateVitalPointHit(\n vitalPoint: VitalPoint,\n hitPosition: Position,\n hour: number,\n attackerArchetype: PlayerArchetype = PlayerArchetype.MUSA,\n defenderArchetype: PlayerArchetype = PlayerArchetype.MUSA,\n defenderStance: TrigramStance = TrigramStance.GEON,\n ): VitalPointHitResult {\n const distance = this.calculateDistance(hitPosition, vitalPoint.position);\n const baseDamage = this.calculateBaseDamage(vitalPoint, distance);\n const now = Date.now(); // Single timestamp for all effects\n\n const accuracy = Math.max(0, 1 - distance / 50);\n\n const meridians = getMeridiansForVitalPoint(vitalPoint.id);\n\n let meridianMultiplier = 1.0;\n const allMeridianEffects: StatusEffect[] = [];\n\n if (meridians.length > 0) {\n // Use the best meridian flow multiplier\n meridianMultiplier = Math.max(\n ...meridians.map((meridianId) =>\n calculateMeridianFlow(meridianId, hour),\n ),\n );\n\n // Generate meridian disruption effects for each affected meridian\n meridians.forEach((meridianId) => {\n const currentDisruption = this.getMeridianDisruption(meridianId);\n const newDisruption = Math.min(\n 1,\n currentDisruption + DISRUPTION_INCREMENT_PER_HIT,\n );\n this.setMeridianDisruption(meridianId, newDisruption);\n\n const effects = generateMeridianEffects(meridianId, newDisruption, now);\n allMeridianEffects.push(...effects);\n });\n }\n\n const meridianStates: Record<string, number> = {};\n meridians.forEach((meridianId) => {\n // Convert disruption (0=normal, 1=blocked) to flow state (1=normal, 0=blocked)\n const disruption = this.getMeridianDisruption(meridianId);\n meridianStates[meridianId] = 1.0 - disruption;\n });\n\n const vulnerabilityMultiplier = calculateEnhancedVulnerability(\n vitalPoint.position,\n hour,\n defenderStance,\n meridianStates,\n );\n\n // Apply all multipliers to damage: base × meridian flow × vulnerability\n const finalDamage = Math.floor(\n baseDamage * meridianMultiplier * vulnerabilityMultiplier,\n );\n\n const vitalPointStatusEffects: StatusEffect[] = vitalPoint.effects.map(\n (effect) =>\n convertToStatusEffect(\n effect,\n accuracy,\n vitalPoint.severity,\n attackerArchetype,\n defenderArchetype,\n vitalPoint.id,\n now,\n ),\n );\n\n const combinedEffects = [...vitalPointStatusEffects, ...allMeridianEffects];\n\n return {\n hit: true,\n vitalPointHit: vitalPoint,\n damage: finalDamage,\n effects: combinedEffects,\n severity: vitalPoint.severity,\n accuracy,\n meridianMultiplier,\n meridianEffects: allMeridianEffects,\n multiplier: vulnerabilityMultiplier,\n };\n }\n\n /**\n * Calculates base damage for a vital point strike.\n *\n * Applies distance-based falloff to vital point's base damage value.\n * Closer strikes deal more damage up to maximum base damage.\n *\n * @param vitalPoint - Vital point with base damage property\n * @param distance - Distance from strike to vital point center\n * @returns Calculated damage value\n *\n * @private\n * @korean 기본피해계산\n */\n private calculateBaseDamage(\n vitalPoint: VitalPoint,\n distance: number,\n ): number {\n const baseDamage = vitalPoint.baseDamage ?? 10;\n const distanceModifier = Math.max(0.1, 1 - distance / 100);\n return Math.floor(baseDamage * distanceModifier);\n }\n\n /**\n * Initializes the system with comprehensive Korean vital points.\n *\n * Loads all 70 Korean vital points from the comprehensive database\n * covering head, torso, arms, and legs with proper categorization.\n *\n * @private\n * @korean 급소초기화\n */\n private initializeVitalPoints(): void {\n this.vitalPoints = [...VITAL_POINTS_DATA];\n }\n}\n\nexport default VitalPointSystem;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"VitalPointSystem.js","names":[],"sources":["../../src/systems/VitalPointSystem.ts"],"sourcesContent":["/**\n * System for managing Korean martial arts vital point (급소) targeting and damage calculation.\n *\n * **Korean**: 급소 시스템 (Vital Point System)\n *\n * The VitalPointSystem implements anatomical targeting mechanics based on traditional\n * Korean martial arts knowledge of 70 vital points (급소). It handles hit detection,\n * damage calculation, and status effect application for precise strikes.\n *\n * ## Vital Point Philosophy\n *\n * Korean martial arts identify specific anatomical locations that, when struck precisely,\n * can cause disproportionate effects. The system categorizes points by:\n *\n * - **Location**: Head, neck, torso, limbs, and core\n * - **Category**: Neurological, vascular, respiratory, muscular, skeletal, etc.\n * - **Severity**: Minor, moderate, major, critical, lethal\n * - **Effects**: Unconsciousness, paralysis, pain, stunning, etc.\n *\n * ## Meridian Integration (경락 통합)\n *\n * The system integrates Traditional Korean Medicine (TKM) meridian theory:\n * - **Time-of-Day Flow**: +30% effectiveness at meridian peak hours\n * - **Meridian Disruption**: Status effects from energy flow blockage\n * - **Elemental Relationships**: 五行 (Wu Xing) elemental advantages\n *\n * ## Key Features\n *\n * - Distance-based hit detection with accuracy falloff\n * - Severity multipliers for damage calculation\n * - Targeted vs. proximity-based strikes\n * - Bilingual Korean-English vital point names\n * - Realistic anatomical positioning\n * - Meridian state tracking and flow calculation\n *\n * @example\n * ```typescript\n * const vitalPointSystem = new VitalPointSystem();\n *\n * // Process a strike at specific position with time-of-day\n * const result = vitalPointSystem.processHit(\n * { x: 100, y: 50 }, // Target position\n * { width: 10, height: 10 }, // Hit box\n * \"GB-20\", // Optional: target specific vital point\n * 14 // Optional: hour of day for meridian flow\n * );\n *\n * if (result.hit && result.vitalPointHit) {\n * console.log(`Hit ${result.vitalPointHit.names.english}!`);\n * console.log(`Damage: ${result.damage}, Severity: ${result.severity}`);\n * console.log(`Meridian bonus: ${result.meridianMultiplier}x`);\n * }\n * ```\n *\n * @category Vital Point System\n * @korean 급소시스템\n */\nimport {\n PlayerArchetype,\n Position,\n TrigramStance,\n VitalPointSeverity,\n} from \"../types/common\";\nimport { convertToStatusEffect } from \"./EffectCalculator\";\nimport { StatusEffect } from \"./types\";\nimport {\n calculateEnhancedVulnerability,\n calculateMeridianFlow,\n generateMeridianEffects,\n} from \"./vitalpoint/KoreanAnatomy\";\nimport { getMeridiansForVitalPoint } from \"./vitalpoint/MeridianVitalPointMapping\";\nimport { VitalPoint, VitalPointHitResult } from \"./vitalpoint/types\";\nimport { VITAL_POINTS_DATA } from \"./vitalpoint/VitalPointsData\";\n\n/**\n * Amount of meridian disruption added per vital point hit (15%)\n */\nconst DISRUPTION_INCREMENT_PER_HIT = 0.15;\n\nexport class VitalPointSystem {\n private vitalPoints: VitalPoint[] = [];\n private meridianStates: Map<string, number> = new Map(); // Tracks disruption level per meridian\n private currentHour: number = 12; // Default to noon\n\n /**\n * Creates a new VitalPointSystem instance.\n *\n * Initializes the system with comprehensive Korean vital points database\n * and meridian state tracking.\n *\n * @param initialHour - Optional initial hour of day (0-23) for meridian flow calculations\n */\n constructor(initialHour: number = 12) {\n this.initializeVitalPoints();\n this.currentHour = initialHour;\n }\n\n /**\n * Sets the current hour of day for meridian flow calculations.\n *\n * **Korean**: 시간 설정\n *\n * @param hour - Hour of day (0-23)\n * @throws Error if hour is not a finite number\n *\n * @example\n * ```typescript\n * vitalPointSystem.setCurrentHour(20); // 8 PM - Pericardium peak time\n * ```\n *\n * @korean 시간설정\n */\n setCurrentHour(hour: number): void {\n if (!Number.isFinite(hour)) {\n throw new Error(\"Hour must be a finite number\");\n }\n this.currentHour = Math.max(0, Math.min(23, Math.floor(hour)));\n }\n\n /**\n * Gets the current hour of day used for meridian calculations.\n *\n * **Korean**: 현재 시간 조회\n *\n * @returns Current hour (0-23)\n *\n * @korean 시간조회\n */\n getCurrentHour(): number {\n return this.currentHour;\n }\n\n /**\n * Updates meridian disruption state for a specific meridian.\n *\n * **Korean**: 경락 차단 상태 업데이트\n *\n * @param meridianId - ID of the meridian\n * @param disruptionLevel - Disruption level (0-1, where 1 is fully blocked)\n *\n * @korean 경락차단업데이트\n */\n setMeridianDisruption(meridianId: string, disruptionLevel: number): void {\n this.meridianStates.set(\n meridianId,\n Math.max(0, Math.min(1, disruptionLevel)),\n );\n }\n\n /**\n * Gets the current disruption level for a meridian.\n *\n * **Korean**: 경락 차단 수준 조회\n *\n * @param meridianId - ID of the meridian\n * @returns Disruption level (0-1)\n *\n * @korean 경락차단조회\n */\n getMeridianDisruption(meridianId: string): number {\n return this.meridianStates.get(meridianId) ?? 0;\n }\n\n /**\n * Clears all meridian disruption states (e.g., after rest or healing).\n *\n * **Korean**: 경락 상태 초기화\n *\n * @korean 경락초기화\n */\n clearMeridianDisruptions(): void {\n this.meridianStates.clear();\n }\n\n /**\n * Processes a hit at a specific position to determine vital point impact.\n *\n * Evaluates whether a strike lands on or near a vital point, calculating\n * damage and effects based on accuracy, severity, and meridian flow.\n * Supports both targeted strikes (specific vital point ID) and proximity-based detection.\n *\n * Enhanced with archetype-specific modifiers for realistic combat simulation.\n *\n * ## Hit Detection Algorithm\n *\n * 1. If targetedVitalPointId provided, validate that specific point\n * 2. Otherwise, find closest vital point to target position\n * 3. Calculate distance from strike to vital point\n * 4. Apply accuracy falloff based on distance (max 50px)\n * 5. Calculate meridian flow bonus based on time of day\n * 6. Apply meridian disruption if applicable\n * 7. Apply archetype offensive/defensive modifiers\n * 8. Apply enhanced vulnerability with stance and anatomical zone\n * 9. Return hit result with damage multipliers and effects\n *\n * @param targetPosition - Position where strike lands\n * @param _hitBox - Size of hit box (currently unused, reserved for future)\n * @param targetedVitalPointId - Optional specific vital point being targeted\n * @param hour - Optional hour of day (0-23) for meridian flow calculation\n * @param attackerArchetype - Optional attacker's archetype (default: MUSA)\n * @param defenderArchetype - Optional defender's archetype (default: MUSA)\n * @param defenderStance - Optional defender's trigram stance (default: GEON)\n * @returns Hit result with damage, effects, severity, and meridian multiplier\n *\n * @example\n * ```typescript\n * // Proximity-based strike\n * const result1 = vitalPointSystem.processHit(\n * { x: 100, y: 50 },\n * { width: 10, height: 10 }\n * );\n *\n * // Targeted strike with archetype modifiers and stance\n * const result2 = vitalPointSystem.processHit(\n * { x: 100, y: 50 },\n * { width: 10, height: 10 },\n * \"head_temple\",\n * 2, // Liver meridian peak hour\n * PlayerArchetype.AMSALJA, // Assassin attacker (+30% effect)\n * PlayerArchetype.MUSA, // Warrior defender (+20% resistance)\n * TrigramStance.GEON // Heaven stance (exposes head +20%)\n * );\n * ```\n *\n * @korean 타격처리\n */\n processHit(\n targetPosition: Position,\n _hitBox: { width: number; height: number }, // Prefixed with underscore to indicate intentionally unused\n targetedVitalPointId?: string | null,\n hour?: number,\n attackerArchetype?: PlayerArchetype,\n defenderArchetype?: PlayerArchetype,\n defenderStance?: TrigramStance,\n ): VitalPointHitResult {\n const effectiveHour = hour ?? this.currentHour;\n const effectiveAttackerArchetype =\n attackerArchetype ?? PlayerArchetype.MUSA;\n const effectiveDefenderArchetype =\n defenderArchetype ?? PlayerArchetype.MUSA;\n const effectiveDefenderStance = defenderStance ?? TrigramStance.GEON;\n\n // If a specific vital point is targeted, check that one\n if (targetedVitalPointId) {\n const targetVitalPoint = this.getVitalPointById(targetedVitalPointId);\n if (targetVitalPoint) {\n return this.calculateVitalPointHit(\n targetVitalPoint,\n targetPosition,\n effectiveHour,\n effectiveAttackerArchetype,\n effectiveDefenderArchetype,\n effectiveDefenderStance,\n );\n }\n }\n\n // Otherwise, find the closest vital point\n const closestVitalPoint = this.findClosestVitalPoint(targetPosition);\n if (!closestVitalPoint) {\n return {\n hit: false,\n damage: 0,\n effects: [],\n severity: VitalPointSeverity.MINOR,\n };\n }\n\n const distance = this.calculateDistance(\n targetPosition,\n closestVitalPoint.position,\n );\n const maxHitDistance = 50; // pixels\n\n if (distance <= maxHitDistance) {\n return this.calculateVitalPointHit(\n closestVitalPoint,\n targetPosition,\n effectiveHour,\n effectiveAttackerArchetype,\n effectiveDefenderArchetype,\n effectiveDefenderStance,\n );\n }\n\n return {\n hit: false,\n damage: 0,\n effects: [],\n severity: VitalPointSeverity.MINOR,\n };\n }\n\n /**\n * Retrieves a vital point by its unique identifier.\n *\n * @param id - Vital point identifier (e.g., \"GB-20\", \"head_temple\")\n * @returns Vital point if found, null otherwise\n *\n * @example\n * ```typescript\n * const temple = vitalPointSystem.getVitalPointById(\"head_temple\");\n * if (temple) {\n * console.log(temple.names.korean); // \"태양혈\"\n * }\n * ```\n *\n * @korean 급소조회\n */\n getVitalPointById(id: string): VitalPoint | null {\n return this.vitalPoints.find((vp) => vp.id === id) ?? null;\n }\n\n /**\n * Gets all registered vital points in the system.\n *\n * @returns Read-only array of all vital points\n *\n * @example\n * ```typescript\n * const allPoints = vitalPointSystem.getVitalPoints();\n * console.log(`${allPoints.length} vital points registered`);\n * ```\n *\n * @korean 급소목록조회\n */\n getVitalPoints(): readonly VitalPoint[] {\n return this.vitalPoints;\n }\n\n /**\n * Calculates hit result for a technique-based attack.\n *\n * Determines whether a technique successfully lands on a vital point,\n * factoring in technique accuracy, attacker position, and randomness.\n * Used for AI and player technique execution.\n *\n * @param technique - Technique being executed with accuracy property\n * @param attackerPosition - Position where attack originates\n * @param _defenderPosition - Defender position (reserved for future use)\n * @param _defenderStance - Defender stance (reserved for future use)\n * @returns Hit result with damage and effects\n *\n * @example\n * ```typescript\n * const technique = {\n * id: \"geon-thunder-strike\",\n * accuracy: 0.85\n * };\n *\n * const result = vitalPointSystem.calculateHit(\n * technique,\n * { x: 100, y: 50 },\n * { x: 120, y: 50 },\n * TrigramStance.GEON\n * );\n * ```\n *\n * @korean 기술타격계산\n */\n calculateHit(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- technique type is dynamically determined by combat system\n technique: any,\n attackerPosition: Position,\n _defenderPosition: Position, // Prefixed with underscore to indicate intentionally unused\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- stance type is dynamically determined by combat system\n _defenderStance: any, // Prefixed with underscore to indicate intentionally unused\n ): VitalPointHitResult {\n const closestVitalPoint = this.findClosestVitalPoint(attackerPosition);\n\n if (!closestVitalPoint) {\n return {\n hit: false,\n damage: 0,\n effects: [],\n severity: VitalPointSeverity.MINOR,\n };\n }\n\n const distance = this.calculateDistance(\n attackerPosition,\n closestVitalPoint.position,\n );\n const hitChance = technique.accuracy * (1 - distance / 100);\n\n if (Math.random() < hitChance) {\n return {\n hit: true,\n vitalPointHit: closestVitalPoint,\n damage: this.calculateBaseDamage(closestVitalPoint, distance),\n effects: [],\n severity: closestVitalPoint.severity,\n };\n }\n\n return {\n hit: false,\n damage: 0,\n effects: [],\n severity: VitalPointSeverity.MINOR,\n };\n }\n\n /**\n * Finds the closest vital point to a given position.\n *\n * Uses Euclidean distance to determine proximity.\n *\n * @param position - Target position\n * @returns Closest vital point or null if none registered\n *\n * @private\n * @korean 최근접급소검색\n */\n private findClosestVitalPoint(position: Position): VitalPoint | null {\n if (this.vitalPoints.length === 0) return null;\n\n let closest = this.vitalPoints[0];\n let minDistance = this.calculateDistance(position, closest.position);\n\n for (const vitalPoint of this.vitalPoints) {\n const distance = this.calculateDistance(position, vitalPoint.position);\n if (distance < minDistance) {\n minDistance = distance;\n closest = vitalPoint;\n }\n }\n\n return closest;\n }\n\n /**\n * Calculates Euclidean distance between two positions.\n *\n * @param pos1 - First position\n * @param pos2 - Second position\n * @returns Distance in pixels\n *\n * @private\n * @korean 거리계산\n */\n private calculateDistance(pos1: Position, pos2: Position): number {\n const dx = pos1.x - pos2.x;\n const dy = pos1.y - pos2.y;\n return Math.sqrt(dx * dx + dy * dy);\n }\n\n /**\n * Calculates vital point hit result with accuracy, damage, and meridian effects.\n *\n * **Korean**: 급소타격결과계산\n *\n * Enhanced with comprehensive effect calculation including:\n * - Duration based on accuracy, severity, and archetype modifiers\n * - Intensity scaling with hit accuracy\n * - Critical hit bonuses for accuracy >= 0.9\n * - Archetype offensive/defensive modifiers\n *\n * @param vitalPoint - Vital point being struck\n * @param hitPosition - Exact position of strike\n * @param hour - Hour of day for meridian flow calculation\n * @param attackerArchetype - Attacker's archetype (default: MUSA)\n * @param defenderArchetype - Defender's archetype (default: MUSA)\n * @returns Hit result with calculated damage, accuracy, and meridian bonuses\n *\n * @private\n * @korean 급소타격결과계산\n */\n private calculateVitalPointHit(\n vitalPoint: VitalPoint,\n hitPosition: Position,\n hour: number,\n attackerArchetype: PlayerArchetype = PlayerArchetype.MUSA,\n defenderArchetype: PlayerArchetype = PlayerArchetype.MUSA,\n defenderStance: TrigramStance = TrigramStance.GEON,\n ): VitalPointHitResult {\n const distance = this.calculateDistance(hitPosition, vitalPoint.position);\n const baseDamage = this.calculateBaseDamage(vitalPoint, distance);\n const now = Date.now(); // Single timestamp for all effects\n\n const accuracy = Math.max(0, 1 - distance / 50);\n\n const meridians = getMeridiansForVitalPoint(vitalPoint.id);\n\n let meridianMultiplier = 1.0;\n const allMeridianEffects: StatusEffect[] = [];\n\n if (meridians.length > 0) {\n // Use the best meridian flow multiplier\n meridianMultiplier = Math.max(\n ...meridians.map((meridianId) =>\n calculateMeridianFlow(meridianId, hour),\n ),\n );\n\n // Generate meridian disruption effects for each affected meridian\n meridians.forEach((meridianId) => {\n const currentDisruption = this.getMeridianDisruption(meridianId);\n const newDisruption = Math.min(\n 1,\n currentDisruption + DISRUPTION_INCREMENT_PER_HIT,\n );\n this.setMeridianDisruption(meridianId, newDisruption);\n\n const effects = generateMeridianEffects(meridianId, newDisruption, now);\n allMeridianEffects.push(...effects);\n });\n }\n\n const meridianStates: Record<string, number> = {};\n meridians.forEach((meridianId) => {\n // Convert disruption (0=normal, 1=blocked) to flow state (1=normal, 0=blocked)\n const disruption = this.getMeridianDisruption(meridianId);\n meridianStates[meridianId] = 1.0 - disruption;\n });\n\n const vulnerabilityMultiplier = calculateEnhancedVulnerability(\n vitalPoint.position,\n hour,\n defenderStance,\n meridianStates,\n );\n\n // Apply all multipliers to damage: base × meridian flow × vulnerability\n const finalDamage = Math.floor(\n baseDamage * meridianMultiplier * vulnerabilityMultiplier,\n );\n\n const vitalPointStatusEffects: StatusEffect[] = vitalPoint.effects.map(\n (effect) =>\n convertToStatusEffect(\n effect,\n accuracy,\n vitalPoint.severity,\n attackerArchetype,\n defenderArchetype,\n vitalPoint.id,\n now,\n ),\n );\n\n const combinedEffects = [...vitalPointStatusEffects, ...allMeridianEffects];\n\n return {\n hit: true,\n vitalPointHit: vitalPoint,\n damage: finalDamage,\n effects: combinedEffects,\n severity: vitalPoint.severity,\n accuracy,\n meridianMultiplier,\n meridianEffects: allMeridianEffects,\n multiplier: vulnerabilityMultiplier,\n };\n }\n\n /**\n * Calculates base damage for a vital point strike.\n *\n * Applies distance-based falloff to vital point's base damage value.\n * Closer strikes deal more damage up to maximum base damage.\n *\n * @param vitalPoint - Vital point with base damage property\n * @param distance - Distance from strike to vital point center\n * @returns Calculated damage value\n *\n * @private\n * @korean 기본피해계산\n */\n private calculateBaseDamage(\n vitalPoint: VitalPoint,\n distance: number,\n ): number {\n const baseDamage = vitalPoint.baseDamage ?? 10;\n const distanceModifier = Math.max(0.1, 1 - distance / 100);\n return Math.floor(baseDamage * distanceModifier);\n }\n\n /**\n * Initializes the system with comprehensive Korean vital points.\n *\n * Loads all 70 Korean vital points from the comprehensive database\n * covering head, torso, arms, and legs with proper categorization.\n *\n * @private\n * @korean 급소초기화\n */\n private initializeVitalPoints(): void {\n this.vitalPoints = [...VITAL_POINTS_DATA];\n }\n}\n\nexport default VitalPointSystem;\n"],"mappings":"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"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { AnimationState, FALL_TO_GROUND_MAP } from "./types.js";
|
|
2
2
|
import { TrigramStance } from "../../../types/common.js";
|
|
3
|
-
import { createMotionPredictionState, predictFutureKeyframe, updateMotionPrediction } from "../builders/KeyframeInterpolation.js";
|
|
4
3
|
import { AnimationQueue, canInterrupt } from "./AnimationPriority.js";
|
|
5
4
|
import { getStanceTransition, isTransitionAllowed } from "./AnimationTransitions.js";
|
|
5
|
+
import { createMotionPredictionState, predictFutureKeyframe, updateMotionPrediction } from "../builders/KeyframeInterpolation.js";
|
|
6
6
|
//#region src/systems/animation/core/AnimationStateMachine.ts
|
|
7
7
|
/**
|
|
8
8
|
* Player Animation State Machine for Black Trigram
|
package/lib/systems/index.js
CHANGED
|
@@ -14,7 +14,9 @@ import { DARK_OPS_ARCHETYPE_BONUSES, DARK_OPS_NIGHT_BONUS, DARK_OPS_SPECIAL_EFFE
|
|
|
14
14
|
import { DAMAGE_TYPE_EFFECTIVENESS, KOREAN_TECHNIQUE_CATEGORIES, TECHNIQUE_DIFFICULTY_LEVELS, TECHNIQUE_MODIFIERS, TECHNIQUE_NAMING, TECHNIQUE_PROPERTIES, TRIGRAM_TECHNIQUE_PROPERTIES, calculateDamageEffectiveness, getTrigramKey, getTrigramProperties } from "./trigram/techniques/TechniqueConfig.js";
|
|
15
15
|
import { TRIGRAM_TECHNIQUES, getAllTechniques, getTechniqueById, getTechniqueCountByStance, getTotalTechniqueCount } from "./trigram/techniques/index.js";
|
|
16
16
|
import { KoreanTechniquesSystem, TECHNIQUE_EFFECTIVENESS_MATRIX, getTechniquesByStance } from "./trigram/KoreanTechniques.js";
|
|
17
|
+
import { STANCE_SPEED_MODIFIERS } from "./physics/MovementPhysics.js";
|
|
17
18
|
import { EffectIntensity, HitEffectEnum, HitEffectType } from "./effects.js";
|
|
19
|
+
import { DEFAULT_INJURY_MOVEMENT_CONFIG, InjuryMovementModifier, injuryMovementModifier } from "./movement/InjuryMovementModifier.js";
|
|
18
20
|
import { BodyPartHealthSystem, bodyPartHealthSystem } from "./bodypart/BodyPartHealthSystem.js";
|
|
19
21
|
import { applyDamageToBodyParts, applyVitalPointDamageToBodyParts, getBodyPartCombatEffects, getBodyRegionFromVitalPoint, healBodyPartsProportionally, initializeBodyPartHealthForPlayer, isPlayerIncapacitatedByBodyDamage } from "./bodypart/BodyPartDamageIntegration.js";
|
|
20
22
|
import { MovementPenaltySystem, movementPenaltySystem } from "./bodypart/MovementPenaltySystem.js";
|
|
@@ -48,8 +50,6 @@ import { AIActionType } from "./ai/types.js";
|
|
|
48
50
|
import { ARCHETYPE_ENFORCEMENT, enforceArchetypeBehavior, getActionFrequency, getSignatureMove, isActionProhibited, shouldExecuteSignatureMove } from "./ai/ArchetypeEnforcer.js";
|
|
49
51
|
import { AIDecisionTree } from "./ai/DecisionTree.js";
|
|
50
52
|
import { TrainingAI } from "./ai/TrainingAI.js";
|
|
51
|
-
import { STANCE_SPEED_MODIFIERS } from "./physics/MovementPhysics.js";
|
|
52
|
-
import { DEFAULT_INJURY_MOVEMENT_CONFIG, InjuryMovementModifier, injuryMovementModifier } from "./movement/InjuryMovementModifier.js";
|
|
53
53
|
import { TrainingCombatSystem } from "./combat/TrainingCombatSystem.js";
|
|
54
54
|
import { isValidArchetype, isVitalPoint, isVitalPointCategory } from "./combat/typeGuards.js";
|
|
55
55
|
import { CombatReadinessState, CombatStateSystem } from "./combat/CombatStateSystem.js";
|
package/lib/utils/inputSystem.js
CHANGED
|
@@ -34,7 +34,8 @@ function usePlayerMovement(config) {
|
|
|
34
34
|
useEffect(() => {
|
|
35
35
|
if (!physicsEngineRef.current) {
|
|
36
36
|
const width = bounds?.worldWidthMeters;
|
|
37
|
-
|
|
37
|
+
const arenaWidth = width != null && Number.isFinite(width) && width > 0 ? width : DEFAULT_PHYSICS_ARENA_BOUNDS.worldWidthMeters;
|
|
38
|
+
physicsEngineRef.current = new MovementPhysics(arenaWidth);
|
|
38
39
|
physicsStateRef.current = {
|
|
39
40
|
position: new THREE.Vector3(initialPositionMeters.x, 0, initialPositionMeters.y),
|
|
40
41
|
velocity: new THREE.Vector3(0, 0, 0),
|