blacktrigram 0.7.111 → 0.7.113
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/assets/index.css +0 -1
- package/lib/audio/AudioManager.js +22 -20
- package/lib/audio/AudioManager.js.map +1 -1
- package/lib/audio/AudioPool.js +6 -4
- package/lib/audio/AudioPool.js.map +1 -1
- package/lib/audio/VariantSelector.js +12 -8
- package/lib/audio/VariantSelector.js.map +1 -1
- package/lib/components/screens/intro/IntroScreen3D.js +1 -1
- package/lib/components/screens/training/TrainingScreen3D.js +4 -3
- package/lib/components/screens/training/TrainingScreen3D.js.map +1 -1
- package/lib/components/shared/mobile/HapticController.js +4 -2
- package/lib/components/shared/mobile/HapticController.js.map +1 -1
- package/lib/components/shared/mobile/StanceWheelPure.js +1 -1
- package/lib/components/shared/mobile/StanceWheelPure.js.map +1 -1
- package/lib/components/shared/three/effects/VitalPointMarkers3D.js +7 -5
- package/lib/components/shared/three/effects/VitalPointMarkers3D.js.map +1 -1
- package/lib/components/shared/three/optimization/AdaptiveQuality.js.map +1 -1
- package/lib/components/shared/three/ui/VitalPointOverlayControlsHtml.js +7 -5
- package/lib/components/shared/three/ui/VitalPointOverlayControlsHtml.js.map +1 -1
- package/lib/components/shared/ui/SplashScreen.js +2 -2
- package/lib/components/shared/ui/VitalPointOverlayControlsPure.js +7 -5
- package/lib/components/shared/ui/VitalPointOverlayControlsPure.js.map +1 -1
- package/lib/components/shared/ui/VolumeControl.js +4 -2
- package/lib/components/shared/ui/VolumeControl.js.map +1 -1
- package/lib/hooks/useSkeletalAnimation.js +6 -4
- package/lib/hooks/useSkeletalAnimation.js.map +1 -1
- package/lib/hooks/useTouchControls.js +18 -17
- package/lib/hooks/useTouchControls.js.map +1 -1
- package/lib/systems/animation/builders/AnimationBuilder.js +6 -2
- package/lib/systems/animation/builders/AnimationBuilder.js.map +1 -1
- package/lib/systems/animation/builders/PunchPhaseApplicator.js +8 -4
- package/lib/systems/animation/builders/PunchPhaseApplicator.js.map +1 -1
- package/lib/systems/animation/core/AnimationStateMachine.js +39 -37
- package/lib/systems/animation/core/AnimationStateMachine.js.map +1 -1
- package/lib/systems/bodypart/MovementPenaltySystem.js.map +1 -1
- package/lib/systems/combat/LimbExposureSystem.js.map +1 -1
- package/lib/systems/physics/MovementPhysics.js +25 -24
- package/lib/systems/physics/MovementPhysics.js.map +1 -1
- package/lib/utils/deviceDetection.js +14 -13
- package/lib/utils/deviceDetection.js.map +1 -1
- package/lib/utils/inputSystem.js +4 -2
- package/lib/utils/inputSystem.js.map +1 -1
- package/package.json +6 -6
|
@@ -97,8 +97,10 @@ var VolumeControl = ({ position = "top-right", style, showLabels = true, compact
|
|
|
97
97
|
const handleMuteToggle = useCallback(() => {
|
|
98
98
|
setIsMuted((prevMuted) => {
|
|
99
99
|
const newMuted = !prevMuted;
|
|
100
|
-
if (audio.isAudioReady)
|
|
101
|
-
|
|
100
|
+
if (audio.isAudioReady) {
|
|
101
|
+
if (newMuted) audio.mute();
|
|
102
|
+
else audio.unmute();
|
|
103
|
+
}
|
|
102
104
|
return newMuted;
|
|
103
105
|
});
|
|
104
106
|
}, [audio]);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VolumeControl.js","names":[],"sources":["../../../../src/components/shared/ui/VolumeControl.tsx"],"sourcesContent":["import React, { useCallback, useMemo, useState } from \"react\";\nimport { useAudio } from \"../../../audio/AudioProvider\";\nimport { KOREAN_COLORS } from \"@/types/constants\";\nimport { hexColorToCSS, hexToRgbaString, toHex } from \"../../../utils/colorUtils\";\n\nexport interface VolumeControlProps {\n readonly position?:\n | \"top-right\"\n | \"bottom-right\"\n | \"top-left\"\n | \"bottom-left\"\n | \"custom\";\n readonly style?: React.CSSProperties;\n readonly showLabels?: boolean;\n readonly compact?: boolean;\n}\n\n/**\n * Volume Control Component\n *\n * Provides controls for:\n * - Master volume\n * - Music volume\n * - SFX volume\n * - Mute/unmute toggle\n *\n * Inspired by template game (https://github.com/Hack23/game)\n */\nexport const VolumeControl: React.FC<VolumeControlProps> = ({\n position = \"top-right\",\n style,\n showLabels = true,\n compact = false,\n}) => {\n const audio = useAudio();\n\n const [masterVolume, setMasterVolume] = useState(audio.masterVolume ?? 1.0);\n const [musicVolume, setMusicVolume] = useState(audio.musicVolume ?? 0.7);\n const [sfxVolume, setSfxVolume] = useState(audio.sfxVolume ?? 0.8);\n const [isMuted, setIsMuted] = useState(audio.muted ?? false);\n\n React.useEffect(() => {\n setMasterVolume(audio.masterVolume ?? 1.0);\n setMusicVolume(audio.musicVolume ?? 0.7);\n setSfxVolume(audio.sfxVolume ?? 0.8);\n setIsMuted(audio.muted ?? false);\n }, [audio.masterVolume, audio.musicVolume, audio.sfxVolume, audio.muted]);\n\n const getPositionStyle = useMemo((): React.CSSProperties => {\n if (position === \"custom\") return {};\n\n const baseStyle: React.CSSProperties = {\n position: \"absolute\",\n zIndex: 1000,\n padding: compact ? \"8px 12px\" : \"12px 16px\",\n };\n\n switch (position) {\n case \"top-right\":\n return { ...baseStyle, top: \"20px\", right: \"20px\" };\n case \"bottom-right\":\n return { ...baseStyle, bottom: \"20px\", right: \"20px\" };\n case \"top-left\":\n return { ...baseStyle, top: \"20px\", left: \"20px\" };\n case \"bottom-left\":\n return { ...baseStyle, bottom: \"20px\", left: \"20px\" };\n default:\n return baseStyle;\n }\n }, [position, compact]);\n\n const containerStyle = useMemo(\n (): React.CSSProperties => ({\n ...getPositionStyle,\n display: \"flex\",\n flexDirection: compact ? \"row\" : \"column\",\n alignItems: \"center\",\n gap: compact ? \"12px\" : \"8px\",\n background: \"rgba(33, 38, 45, 0.95)\",\n borderRadius: \"12px\",\n border: `1px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.2)}`,\n pointerEvents: \"auto\", // Enable interaction even when parent has pointerEvents: none\n ...style,\n }),\n [getPositionStyle, compact, style],\n );\n\n const handleMasterVolumeChange = useCallback(\n (event: React.ChangeEvent<HTMLInputElement>) => {\n const value = parseFloat(event.target.value);\n setMasterVolume(value);\n if (audio.isAudioReady) {\n audio.setVolume(\"master\", value);\n }\n },\n [audio],\n );\n\n const handleMusicVolumeChange = useCallback(\n (event: React.ChangeEvent<HTMLInputElement>) => {\n const value = parseFloat(event.target.value);\n setMusicVolume(value);\n if (audio.isAudioReady) {\n audio.setVolume(\"music\", value);\n }\n },\n [audio],\n );\n\n const handleSfxVolumeChange = useCallback(\n (event: React.ChangeEvent<HTMLInputElement>) => {\n const value = parseFloat(event.target.value);\n setSfxVolume(value);\n if (audio.isAudioReady) {\n audio.setVolume(\"sfx\", value);\n }\n },\n [audio],\n );\n\n const handleMuteToggle = useCallback(() => {\n setIsMuted((prevMuted) => {\n const newMuted = !prevMuted;\n if (audio.isAudioReady) {\n if (newMuted) {\n audio.mute();\n } else {\n audio.unmute();\n }\n }\n return newMuted;\n });\n }, [audio]);\n\n const sliderStyle = useMemo(\n (): React.CSSProperties => ({\n width: compact ? \"60px\" : \"100px\",\n cursor: \"pointer\",\n accentColor: `#${toHex(KOREAN_COLORS.PRIMARY_CYAN)}`,\n }),\n [compact],\n );\n\n const labelStyle = useMemo(\n (): React.CSSProperties => ({\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize: compact ? \"11px\" : \"12px\",\n fontWeight: \"bold\",\n minWidth: compact ? \"40px\" : \"50px\",\n textAlign: \"left\",\n }),\n [compact],\n );\n\n const valueStyle = useMemo(\n (): React.CSSProperties => ({\n color: `#${toHex(KOREAN_COLORS.ACCENT_GOLD)}`,\n fontSize: compact ? \"10px\" : \"11px\",\n minWidth: \"35px\",\n textAlign: \"right\",\n }),\n [compact],\n );\n\n const controlRowStyle = useMemo(\n (): React.CSSProperties => ({\n display: \"flex\",\n alignItems: \"center\",\n gap: \"8px\",\n width: \"100%\",\n }),\n [],\n );\n\n if (compact) {\n return (\n <div style={containerStyle} data-testid=\"volume-control\">\n <button\n onClick={handleMuteToggle}\n data-testid=\"mute-toggle-button\"\n aria-label={isMuted ? \"Unmute audio\" : \"Mute audio\"}\n style={{\n background: isMuted\n ? hexColorToCSS(KOREAN_COLORS.UI_DISABLED_TEXT)\n : `#${toHex(KOREAN_COLORS.PRIMARY_CYAN)}`,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n border: \"none\",\n padding: \"6px 12px\",\n borderRadius: \"6px\",\n cursor: \"pointer\",\n fontWeight: \"bold\",\n fontSize: \"14px\",\n }}\n title={isMuted ? \"음소거 해제 | Unmute\" : \"음소거 | Mute\"}\n >\n {isMuted ? \"🔇\" : \"🔊\"}\n </button>\n <input\n type=\"range\"\n min=\"0\"\n max=\"1\"\n step=\"0.01\"\n value={masterVolume}\n onChange={handleMasterVolumeChange}\n data-testid=\"master-volume-slider\"\n aria-label=\"마스터 볼륨 | Master Volume\"\n style={sliderStyle}\n title=\"마스터 볼륨 | Master Volume\"\n />\n <span style={valueStyle}>{Math.round(masterVolume * 100)}%</span>\n </div>\n );\n }\n\n return (\n <div style={containerStyle} data-testid=\"volume-control\">\n {showLabels && (\n <div\n style={{\n color: `#${toHex(KOREAN_COLORS.PRIMARY_CYAN)}`,\n fontSize: \"14px\",\n fontWeight: \"bold\",\n marginBottom: \"4px\",\n textAlign: \"center\",\n }}\n >\n 🎵 음량 | Volume\n </div>\n )}\n\n {/* Master Volume */}\n <div style={controlRowStyle}>\n <label htmlFor=\"master-volume\" style={labelStyle}>\n 전체 | Master\n </label>\n <input\n id=\"master-volume\"\n type=\"range\"\n min=\"0\"\n max=\"1\"\n step=\"0.01\"\n value={masterVolume}\n onChange={handleMasterVolumeChange}\n data-testid=\"master-volume-slider\"\n style={sliderStyle}\n />\n <span style={valueStyle}>{Math.round(masterVolume * 100)}%</span>\n </div>\n\n {/* Music Volume */}\n <div style={controlRowStyle}>\n <label htmlFor=\"music-volume\" style={labelStyle}>\n 음악 | Music\n </label>\n <input\n id=\"music-volume\"\n type=\"range\"\n min=\"0\"\n max=\"1\"\n step=\"0.01\"\n value={musicVolume}\n onChange={handleMusicVolumeChange}\n data-testid=\"music-volume-slider\"\n style={sliderStyle}\n />\n <span style={valueStyle}>{Math.round(musicVolume * 100)}%</span>\n </div>\n\n {/* SFX Volume */}\n <div style={controlRowStyle}>\n <label htmlFor=\"sfx-volume\" style={labelStyle}>\n 효과음 | SFX\n </label>\n <input\n id=\"sfx-volume\"\n type=\"range\"\n min=\"0\"\n max=\"1\"\n step=\"0.01\"\n value={sfxVolume}\n onChange={handleSfxVolumeChange}\n data-testid=\"sfx-volume-slider\"\n style={sliderStyle}\n />\n <span style={valueStyle}>{Math.round(sfxVolume * 100)}%</span>\n </div>\n\n {/* Mute Toggle */}\n <button\n onClick={handleMuteToggle}\n data-testid=\"mute-toggle-button\"\n aria-label={isMuted ? \"Unmute audio\" : \"Mute audio\"}\n style={{\n background: isMuted\n ? hexColorToCSS(KOREAN_COLORS.UI_DISABLED_TEXT)\n : `#${toHex(KOREAN_COLORS.PRIMARY_CYAN)}`,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n border: \"none\",\n padding: \"8px 16px\",\n borderRadius: \"8px\",\n cursor: \"pointer\",\n fontWeight: \"bold\",\n fontSize: \"14px\",\n marginTop: \"4px\",\n width: \"100%\",\n }}\n title={isMuted ? \"음소거 해제 | Unmute\" : \"음소거 | Mute\"}\n >\n {isMuted ? \"🔇 음소거 해제 | Unmute\" : \"🔊 음소거 | Mute\"}\n </button>\n\n {/* Audio Status Indicator */}\n <div\n style={{\n color: audio.isAudioReady\n ? `#${toHex(KOREAN_COLORS.ACCENT_GOLD)}`\n : hexColorToCSS(KOREAN_COLORS.UI_GRAY),\n fontSize: \"10px\",\n marginTop: \"4px\",\n textAlign: \"center\",\n }}\n >\n {audio.isAudioReady\n ? \"✓ 오디오 준비됨 | Audio Ready\"\n : \"⏳ 초기화 중... | Initializing...\"}\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"VolumeControl.js","names":[],"sources":["../../../../src/components/shared/ui/VolumeControl.tsx"],"sourcesContent":["import React, { useCallback, useMemo, useState } from \"react\";\nimport { useAudio } from \"../../../audio/AudioProvider\";\nimport { KOREAN_COLORS } from \"@/types/constants\";\nimport { hexColorToCSS, hexToRgbaString, toHex } from \"../../../utils/colorUtils\";\n\nexport interface VolumeControlProps {\n readonly position?:\n | \"top-right\"\n | \"bottom-right\"\n | \"top-left\"\n | \"bottom-left\"\n | \"custom\";\n readonly style?: React.CSSProperties;\n readonly showLabels?: boolean;\n readonly compact?: boolean;\n}\n\n/**\n * Volume Control Component\n *\n * Provides controls for:\n * - Master volume\n * - Music volume\n * - SFX volume\n * - Mute/unmute toggle\n *\n * Inspired by template game (https://github.com/Hack23/game)\n */\nexport const VolumeControl: React.FC<VolumeControlProps> = ({\n position = \"top-right\",\n style,\n showLabels = true,\n compact = false,\n}) => {\n const audio = useAudio();\n\n const [masterVolume, setMasterVolume] = useState(audio.masterVolume ?? 1.0);\n const [musicVolume, setMusicVolume] = useState(audio.musicVolume ?? 0.7);\n const [sfxVolume, setSfxVolume] = useState(audio.sfxVolume ?? 0.8);\n const [isMuted, setIsMuted] = useState(audio.muted ?? false);\n\n React.useEffect(() => {\n setMasterVolume(audio.masterVolume ?? 1.0);\n setMusicVolume(audio.musicVolume ?? 0.7);\n setSfxVolume(audio.sfxVolume ?? 0.8);\n setIsMuted(audio.muted ?? false);\n }, [audio.masterVolume, audio.musicVolume, audio.sfxVolume, audio.muted]);\n\n const getPositionStyle = useMemo((): React.CSSProperties => {\n if (position === \"custom\") return {};\n\n const baseStyle: React.CSSProperties = {\n position: \"absolute\",\n zIndex: 1000,\n padding: compact ? \"8px 12px\" : \"12px 16px\",\n };\n\n switch (position) {\n case \"top-right\":\n return { ...baseStyle, top: \"20px\", right: \"20px\" };\n case \"bottom-right\":\n return { ...baseStyle, bottom: \"20px\", right: \"20px\" };\n case \"top-left\":\n return { ...baseStyle, top: \"20px\", left: \"20px\" };\n case \"bottom-left\":\n return { ...baseStyle, bottom: \"20px\", left: \"20px\" };\n default:\n return baseStyle;\n }\n }, [position, compact]);\n\n const containerStyle = useMemo(\n (): React.CSSProperties => ({\n ...getPositionStyle,\n display: \"flex\",\n flexDirection: compact ? \"row\" : \"column\",\n alignItems: \"center\",\n gap: compact ? \"12px\" : \"8px\",\n background: \"rgba(33, 38, 45, 0.95)\",\n borderRadius: \"12px\",\n border: `1px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.2)}`,\n pointerEvents: \"auto\", // Enable interaction even when parent has pointerEvents: none\n ...style,\n }),\n [getPositionStyle, compact, style],\n );\n\n const handleMasterVolumeChange = useCallback(\n (event: React.ChangeEvent<HTMLInputElement>) => {\n const value = parseFloat(event.target.value);\n setMasterVolume(value);\n if (audio.isAudioReady) {\n audio.setVolume(\"master\", value);\n }\n },\n [audio],\n );\n\n const handleMusicVolumeChange = useCallback(\n (event: React.ChangeEvent<HTMLInputElement>) => {\n const value = parseFloat(event.target.value);\n setMusicVolume(value);\n if (audio.isAudioReady) {\n audio.setVolume(\"music\", value);\n }\n },\n [audio],\n );\n\n const handleSfxVolumeChange = useCallback(\n (event: React.ChangeEvent<HTMLInputElement>) => {\n const value = parseFloat(event.target.value);\n setSfxVolume(value);\n if (audio.isAudioReady) {\n audio.setVolume(\"sfx\", value);\n }\n },\n [audio],\n );\n\n const handleMuteToggle = useCallback(() => {\n setIsMuted((prevMuted) => {\n const newMuted = !prevMuted;\n if (audio.isAudioReady) {\n if (newMuted) {\n audio.mute();\n } else {\n audio.unmute();\n }\n }\n return newMuted;\n });\n }, [audio]);\n\n const sliderStyle = useMemo(\n (): React.CSSProperties => ({\n width: compact ? \"60px\" : \"100px\",\n cursor: \"pointer\",\n accentColor: `#${toHex(KOREAN_COLORS.PRIMARY_CYAN)}`,\n }),\n [compact],\n );\n\n const labelStyle = useMemo(\n (): React.CSSProperties => ({\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize: compact ? \"11px\" : \"12px\",\n fontWeight: \"bold\",\n minWidth: compact ? \"40px\" : \"50px\",\n textAlign: \"left\",\n }),\n [compact],\n );\n\n const valueStyle = useMemo(\n (): React.CSSProperties => ({\n color: `#${toHex(KOREAN_COLORS.ACCENT_GOLD)}`,\n fontSize: compact ? \"10px\" : \"11px\",\n minWidth: \"35px\",\n textAlign: \"right\",\n }),\n [compact],\n );\n\n const controlRowStyle = useMemo(\n (): React.CSSProperties => ({\n display: \"flex\",\n alignItems: \"center\",\n gap: \"8px\",\n width: \"100%\",\n }),\n [],\n );\n\n if (compact) {\n return (\n <div style={containerStyle} data-testid=\"volume-control\">\n <button\n onClick={handleMuteToggle}\n data-testid=\"mute-toggle-button\"\n aria-label={isMuted ? \"Unmute audio\" : \"Mute audio\"}\n style={{\n background: isMuted\n ? hexColorToCSS(KOREAN_COLORS.UI_DISABLED_TEXT)\n : `#${toHex(KOREAN_COLORS.PRIMARY_CYAN)}`,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n border: \"none\",\n padding: \"6px 12px\",\n borderRadius: \"6px\",\n cursor: \"pointer\",\n fontWeight: \"bold\",\n fontSize: \"14px\",\n }}\n title={isMuted ? \"음소거 해제 | Unmute\" : \"음소거 | Mute\"}\n >\n {isMuted ? \"🔇\" : \"🔊\"}\n </button>\n <input\n type=\"range\"\n min=\"0\"\n max=\"1\"\n step=\"0.01\"\n value={masterVolume}\n onChange={handleMasterVolumeChange}\n data-testid=\"master-volume-slider\"\n aria-label=\"마스터 볼륨 | Master Volume\"\n style={sliderStyle}\n title=\"마스터 볼륨 | Master Volume\"\n />\n <span style={valueStyle}>{Math.round(masterVolume * 100)}%</span>\n </div>\n );\n }\n\n return (\n <div style={containerStyle} data-testid=\"volume-control\">\n {showLabels && (\n <div\n style={{\n color: `#${toHex(KOREAN_COLORS.PRIMARY_CYAN)}`,\n fontSize: \"14px\",\n fontWeight: \"bold\",\n marginBottom: \"4px\",\n textAlign: \"center\",\n }}\n >\n 🎵 음량 | Volume\n </div>\n )}\n\n {/* Master Volume */}\n <div style={controlRowStyle}>\n <label htmlFor=\"master-volume\" style={labelStyle}>\n 전체 | Master\n </label>\n <input\n id=\"master-volume\"\n type=\"range\"\n min=\"0\"\n max=\"1\"\n step=\"0.01\"\n value={masterVolume}\n onChange={handleMasterVolumeChange}\n data-testid=\"master-volume-slider\"\n style={sliderStyle}\n />\n <span style={valueStyle}>{Math.round(masterVolume * 100)}%</span>\n </div>\n\n {/* Music Volume */}\n <div style={controlRowStyle}>\n <label htmlFor=\"music-volume\" style={labelStyle}>\n 음악 | Music\n </label>\n <input\n id=\"music-volume\"\n type=\"range\"\n min=\"0\"\n max=\"1\"\n step=\"0.01\"\n value={musicVolume}\n onChange={handleMusicVolumeChange}\n data-testid=\"music-volume-slider\"\n style={sliderStyle}\n />\n <span style={valueStyle}>{Math.round(musicVolume * 100)}%</span>\n </div>\n\n {/* SFX Volume */}\n <div style={controlRowStyle}>\n <label htmlFor=\"sfx-volume\" style={labelStyle}>\n 효과음 | SFX\n </label>\n <input\n id=\"sfx-volume\"\n type=\"range\"\n min=\"0\"\n max=\"1\"\n step=\"0.01\"\n value={sfxVolume}\n onChange={handleSfxVolumeChange}\n data-testid=\"sfx-volume-slider\"\n style={sliderStyle}\n />\n <span style={valueStyle}>{Math.round(sfxVolume * 100)}%</span>\n </div>\n\n {/* Mute Toggle */}\n <button\n onClick={handleMuteToggle}\n data-testid=\"mute-toggle-button\"\n aria-label={isMuted ? \"Unmute audio\" : \"Mute audio\"}\n style={{\n background: isMuted\n ? hexColorToCSS(KOREAN_COLORS.UI_DISABLED_TEXT)\n : `#${toHex(KOREAN_COLORS.PRIMARY_CYAN)}`,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n border: \"none\",\n padding: \"8px 16px\",\n borderRadius: \"8px\",\n cursor: \"pointer\",\n fontWeight: \"bold\",\n fontSize: \"14px\",\n marginTop: \"4px\",\n width: \"100%\",\n }}\n title={isMuted ? \"음소거 해제 | Unmute\" : \"음소거 | Mute\"}\n >\n {isMuted ? \"🔇 음소거 해제 | Unmute\" : \"🔊 음소거 | Mute\"}\n </button>\n\n {/* Audio Status Indicator */}\n <div\n style={{\n color: audio.isAudioReady\n ? `#${toHex(KOREAN_COLORS.ACCENT_GOLD)}`\n : hexColorToCSS(KOREAN_COLORS.UI_GRAY),\n fontSize: \"10px\",\n marginTop: \"4px\",\n textAlign: \"center\",\n }}\n >\n {audio.isAudioReady\n ? \"✓ 오디오 준비됨 | Audio Ready\"\n : \"⏳ 초기화 중... | Initializing...\"}\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -154,10 +154,12 @@ function useSkeletalAnimation(options) {
|
|
|
154
154
|
const frameStartTime = performance.now();
|
|
155
155
|
let newTime = animTimeRef.current + delta * animState.playbackSpeed;
|
|
156
156
|
let completed = false;
|
|
157
|
-
if (newTime >= animState.currentAnimation.duration)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
157
|
+
if (newTime >= animState.currentAnimation.duration) {
|
|
158
|
+
if (animState.currentAnimation.loop) newTime = newTime % animState.currentAnimation.duration;
|
|
159
|
+
else {
|
|
160
|
+
newTime = animState.currentAnimation.duration;
|
|
161
|
+
completed = true;
|
|
162
|
+
}
|
|
161
163
|
}
|
|
162
164
|
const keyframe = interpolateKeyframeCached(animState.currentAnimation.name, animState.currentAnimation, newTime);
|
|
163
165
|
if (keyframe) batchUpdateBones(targetRig, keyframe);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSkeletalAnimation.js","names":[],"sources":["../../src/hooks/useSkeletalAnimation.ts"],"sourcesContent":["/**\n * useSkeletalAnimation - Shared hook for skeletal animation management\n *\n * Centralizes skeletal animation state and frame updates for player characters.\n * Reduces code duplication across SkeletalPlayer3D, Player3DWithTransitions,\n * and screen components.\n *\n * PHASE 2: Now uses cached interpolation and batch bone updates for 60fps performance\n *\n * @module hooks/useSkeletalAnimation\n * @category Hooks\n * @korean 골격애니메이션훅\n */\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport {\n batchUpdateBones,\n getAnimation,\n getAnimationByName,\n getAttackAnimation,\n getDefensiveAnimation,\n getFootworkAnimation,\n getStepAnimation,\n interpolateKeyframeCached,\n performanceMonitor,\n} from \"../systems/animation\";\nimport { applyLaterality } from \"../systems/animation/core/LateralityTransform\";\nimport type { TrigramStance } from \"../types/common\";\nimport type { PlayerAnimation } from \"../types/player-visual\";\nimport type {\n SkeletalAnimation,\n SkeletalAnimationState,\n SkeletalRig,\n} from \"../types/skeletal\";\n\n/**\n * Options for useSkeletalAnimation hook\n * @korean 골격애니메이션훅옵션\n */\nexport interface UseSkeletalAnimationOptions {\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 player stance for trigram-specific idle animations */\n readonly stance?: TrigramStance;\n /**\n * Stance laterality (left or right foot forward)\n *\n * - \"left\": Left foot forward (왼발서기 - Oenbal Seogi)\n * - \"right\": Right foot forward (오른발서기 - Oreun Bal Seogi)\n *\n * This affects animation mirroring - techniques will be mirrored\n * appropriately based on the laterality, creating 16 distinct stance\n * configurations (8 trigrams × 2 laterality).\n *\n * **Korean**: 측면성 (Cheugmyeonseong - Laterality/Sidedness)\n */\n readonly laterality?: \"left\" | \"right\";\n /** Callback when animation completes */\n readonly onAnimationComplete?: () => void;\n}\n\n/**\n * Return type for useSkeletalAnimation hook\n * @korean 골격애니메이션훅반환타입\n */\nexport interface UseSkeletalAnimationReturn {\n /** Current animation state */\n readonly animState: SkeletalAnimationState;\n /** Animation time reference (seconds) */\n readonly animTimeRef: React.MutableRefObject<number>;\n /** Update animation and apply to rig (call in useFrame) */\n readonly updateRigAnimation: (rig: SkeletalRig, delta: number) => void;\n /** Diagonal rotation override for step animations */\n readonly diagonalRotationY: number | null;\n}\n\n/**\n * Set of diagonal step animations for O(1) lookup\n * @korean 대각선스텝애니메이션집합\n */\nconst DIAGONAL_STEP_ANIMATIONS = new Set([\n \"step_forward_left\",\n \"step_forward_right\",\n \"step_back_left\",\n \"step_back_right\",\n]);\n\n/**\n * useSkeletalAnimation hook\n *\n * Manages skeletal animation state and frame updates for player characters.\n * Handles animation selection based on player state (idle, walk, attack, etc.)\n * and applies keyframes to the skeletal rig.\n *\n * @param options - Animation options\n * @returns Animation state and update function\n *\n * @example\n * ```tsx\n * const { animState, animTimeRef, updateRigAnimation, diagonalRotationY } =\n * useSkeletalAnimation({\n * currentAnimation: \"walk\",\n * isBlocking: false,\n * onAnimationComplete: () => console.log(\"Animation completed\"),\n * });\n *\n * // In useFrame callback\n * useFrame((_, delta) => {\n * updateRigAnimation(rig, delta);\n * });\n * ```\n *\n * @korean 골격애니메이션훅\n */\nexport function useSkeletalAnimation(\n options: UseSkeletalAnimationOptions,\n): UseSkeletalAnimationReturn {\n const {\n currentAnimation,\n attackAnimation,\n isBlocking = false,\n stance,\n laterality = \"right\",\n onAnimationComplete,\n } = options;\n\n // Animation state\n const [animState, setAnimState] = useState<SkeletalAnimationState>({\n currentAnimation: null,\n currentTime: 0,\n isPlaying: false,\n playbackSpeed: 1.0,\n previousKeyframeIndex: 0,\n nextKeyframeIndex: 1,\n });\n\n // Animation time ref (updated at 60fps without triggering re-renders)\n const animTimeRef = useRef(0);\n\n // Diagonal step rotation override (Y-axis rotation in radians)\n const [diagonalRotationY, setDiagonalRotationY] = useState<number | null>(\n null,\n );\n\n // Load animation when currentAnimation, blocking state, or laterality changes\n useEffect(() => {\n // Reset animation time whenever animation changes\n animTimeRef.current = 0;\n\n let selectedAnim: SkeletalAnimation | null = null;\n let playbackSpeed: number;\n let shouldClearDiagonalRotation = true;\n\n if (currentAnimation === \"attack\" && attackAnimation) {\n // Attack animation - first check stance-specific attacks, then generic\n selectedAnim =\n getAttackAnimation(attackAnimation) ??\n getAnimation(attackAnimation) ??\n null;\n playbackSpeed = 1.0;\n } else if (currentAnimation === \"defend\" || isBlocking) {\n // Block/defend animation - check stance-specific defensive animations first\n // If attackAnimation contains a defensive animation name, use it\n if (attackAnimation) {\n selectedAnim = getDefensiveAnimation(attackAnimation) ?? null;\n }\n // Fall back to generic block animation\n selectedAnim ??= getAnimation(\"block\") ?? null;\n playbackSpeed = 1.0;\n } else if (currentAnimation === \"idle\") {\n // Idle animation - use trigram-specific stance idle if stance is provided\n // Otherwise fall back to generic idle breathing animation\n if (stance) {\n const stanceIdleAnim = `stance_${stance}` as PlayerAnimation;\n selectedAnim = getAnimationByName(stanceIdleAnim) ?? null;\n }\n // Fall back to generic idle if no stance or stance animation not found\n selectedAnim ??= getAnimation(\"idle\") ?? null;\n playbackSpeed = 0.5; // Slow breathing animation\n } else if (currentAnimation === \"walk\") {\n // Walking animation - use trigram-specific walk if stance is provided\n if (stance) {\n const stanceWalkAnim = `walk_${stance}` as PlayerAnimation;\n selectedAnim = getAnimationByName(stanceWalkAnim) ?? null;\n }\n // Fall back to generic walk if no stance or stance animation not found\n selectedAnim ??= getAnimation(\"walk\") ?? null;\n playbackSpeed = 1.0;\n } else if (currentAnimation === \"run\") {\n // Running animation - use trigram-specific run if stance is provided\n if (stance) {\n const stanceRunAnim = `run_${stance}` as PlayerAnimation;\n selectedAnim = getAnimationByName(stanceRunAnim) ?? null;\n }\n // Fall back to generic run if no stance or stance animation not found\n selectedAnim ??= getAnimation(\"run\") ?? null;\n playbackSpeed = 1.0;\n } else if (currentAnimation?.startsWith(\"fall_\")) {\n // Fall animations - directional falls from BasicAnimations\n selectedAnim = getAnimation(currentAnimation) ?? null;\n playbackSpeed = 1.0;\n } else if (currentAnimation === \"stance_change\") {\n // Stance change animation\n selectedAnim =\n getAnimation(\"stance_change\") ?? getAnimation(\"idle_stance\") ?? null;\n playbackSpeed = 1.2; // Slightly faster for responsiveness\n } else if (currentAnimation === \"hit\") {\n // Hit reaction - stop animation\n setAnimState((prev) => ({\n ...prev,\n isPlaying: false,\n currentTime: 0,\n }));\n return;\n } else if (currentAnimation?.startsWith(\"step_\")) {\n // Tactical step animation\n selectedAnim = getStepAnimation(currentAnimation) ?? null;\n playbackSpeed = 1.0;\n\n // Handle diagonal step rotation\n if (DIAGONAL_STEP_ANIMATIONS.has(currentAnimation)) {\n shouldClearDiagonalRotation = false;\n // Diagonal rotation will be handled by parent component\n // This hook only manages the flag\n }\n } else if (currentAnimation?.startsWith(\"footwork_\")) {\n // Footwork pattern animation\n selectedAnim = getFootworkAnimation(currentAnimation) ?? null;\n playbackSpeed = 1.0;\n } else if (currentAnimation?.startsWith(\"stance_\")) {\n // Stance-specific idle animation with proper biomechanics\n // Use getAnimationByName which searches ALL_ANIMATIONS (includes STANCE_ANIMATIONS)\n selectedAnim = getAnimationByName(currentAnimation) ?? null;\n playbackSpeed = 0.5; // Slow breathing animation for stance idle\n } else {\n // Idle animation (fallback)\n selectedAnim = getAnimation(\"idle_stance\") ?? null;\n playbackSpeed = 0.5; // Slow breathing animation\n }\n\n // Apply laterality transformation if selectedAnim exists\n // laterality directly affects animation mirroring:\n // \"left\" = left foot forward (왼발서기) → animations mirrored\n // \"right\" = right foot forward (오른발서기) → base animations (default)\n if (selectedAnim) {\n selectedAnim = applyLaterality(selectedAnim, laterality);\n }\n\n // Clear diagonal rotation for non-diagonal animations\n if (shouldClearDiagonalRotation) {\n setDiagonalRotationY(null);\n }\n\n setAnimState({\n currentAnimation: selectedAnim,\n currentTime: 0,\n isPlaying: true,\n playbackSpeed,\n previousKeyframeIndex: 0,\n nextKeyframeIndex: 1,\n });\n }, [currentAnimation, attackAnimation, isBlocking, stance, laterality]);\n\n // Update animation and apply to rig (called at 60fps in useFrame)\n // PHASE 2: Now uses cached interpolation and batch bone updates\n const updateRigAnimation = useCallback(\n (targetRig: SkeletalRig, delta: number) => {\n if (animState.isPlaying && animState.currentAnimation) {\n const frameStartTime = performance.now();\n\n // Advance animation time\n let newTime = animTimeRef.current + delta * animState.playbackSpeed;\n let completed = false;\n\n // Handle looping or completion\n if (newTime >= animState.currentAnimation.duration) {\n if (animState.currentAnimation.loop) {\n newTime = newTime % animState.currentAnimation.duration;\n } else {\n newTime = animState.currentAnimation.duration;\n completed = true;\n }\n }\n\n // Use cached interpolation for 90%+ cache hit rate\n // Use animation.name as the unique identifier\n const keyframe = interpolateKeyframeCached(\n animState.currentAnimation.name,\n animState.currentAnimation,\n newTime,\n );\n\n if (keyframe) {\n // Batch update bones (60% faster than individual updates)\n batchUpdateBones(targetRig, keyframe);\n }\n\n // Update time ref\n animTimeRef.current = newTime;\n\n // Record performance metrics\n const frameTime = performance.now() - frameStartTime;\n performanceMonitor.recordFrame(frameTime);\n\n // Handle animation completion\n if (completed) {\n animTimeRef.current = 0;\n setAnimState((prev) => ({\n ...prev,\n isPlaying: false,\n currentTime: 0,\n }));\n\n // Trigger callback\n if (onAnimationComplete) {\n onAnimationComplete();\n }\n }\n }\n },\n [animState, onAnimationComplete],\n );\n\n return {\n animState,\n animTimeRef,\n updateRigAnimation,\n diagonalRotationY,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useSkeletalAnimation.js","names":[],"sources":["../../src/hooks/useSkeletalAnimation.ts"],"sourcesContent":["/**\n * useSkeletalAnimation - Shared hook for skeletal animation management\n *\n * Centralizes skeletal animation state and frame updates for player characters.\n * Reduces code duplication across SkeletalPlayer3D, Player3DWithTransitions,\n * and screen components.\n *\n * PHASE 2: Now uses cached interpolation and batch bone updates for 60fps performance\n *\n * @module hooks/useSkeletalAnimation\n * @category Hooks\n * @korean 골격애니메이션훅\n */\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport {\n batchUpdateBones,\n getAnimation,\n getAnimationByName,\n getAttackAnimation,\n getDefensiveAnimation,\n getFootworkAnimation,\n getStepAnimation,\n interpolateKeyframeCached,\n performanceMonitor,\n} from \"../systems/animation\";\nimport { applyLaterality } from \"../systems/animation/core/LateralityTransform\";\nimport type { TrigramStance } from \"../types/common\";\nimport type { PlayerAnimation } from \"../types/player-visual\";\nimport type {\n SkeletalAnimation,\n SkeletalAnimationState,\n SkeletalRig,\n} from \"../types/skeletal\";\n\n/**\n * Options for useSkeletalAnimation hook\n * @korean 골격애니메이션훅옵션\n */\nexport interface UseSkeletalAnimationOptions {\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 player stance for trigram-specific idle animations */\n readonly stance?: TrigramStance;\n /**\n * Stance laterality (left or right foot forward)\n *\n * - \"left\": Left foot forward (왼발서기 - Oenbal Seogi)\n * - \"right\": Right foot forward (오른발서기 - Oreun Bal Seogi)\n *\n * This affects animation mirroring - techniques will be mirrored\n * appropriately based on the laterality, creating 16 distinct stance\n * configurations (8 trigrams × 2 laterality).\n *\n * **Korean**: 측면성 (Cheugmyeonseong - Laterality/Sidedness)\n */\n readonly laterality?: \"left\" | \"right\";\n /** Callback when animation completes */\n readonly onAnimationComplete?: () => void;\n}\n\n/**\n * Return type for useSkeletalAnimation hook\n * @korean 골격애니메이션훅반환타입\n */\nexport interface UseSkeletalAnimationReturn {\n /** Current animation state */\n readonly animState: SkeletalAnimationState;\n /** Animation time reference (seconds) */\n readonly animTimeRef: React.MutableRefObject<number>;\n /** Update animation and apply to rig (call in useFrame) */\n readonly updateRigAnimation: (rig: SkeletalRig, delta: number) => void;\n /** Diagonal rotation override for step animations */\n readonly diagonalRotationY: number | null;\n}\n\n/**\n * Set of diagonal step animations for O(1) lookup\n * @korean 대각선스텝애니메이션집합\n */\nconst DIAGONAL_STEP_ANIMATIONS = new Set([\n \"step_forward_left\",\n \"step_forward_right\",\n \"step_back_left\",\n \"step_back_right\",\n]);\n\n/**\n * useSkeletalAnimation hook\n *\n * Manages skeletal animation state and frame updates for player characters.\n * Handles animation selection based on player state (idle, walk, attack, etc.)\n * and applies keyframes to the skeletal rig.\n *\n * @param options - Animation options\n * @returns Animation state and update function\n *\n * @example\n * ```tsx\n * const { animState, animTimeRef, updateRigAnimation, diagonalRotationY } =\n * useSkeletalAnimation({\n * currentAnimation: \"walk\",\n * isBlocking: false,\n * onAnimationComplete: () => console.log(\"Animation completed\"),\n * });\n *\n * // In useFrame callback\n * useFrame((_, delta) => {\n * updateRigAnimation(rig, delta);\n * });\n * ```\n *\n * @korean 골격애니메이션훅\n */\nexport function useSkeletalAnimation(\n options: UseSkeletalAnimationOptions,\n): UseSkeletalAnimationReturn {\n const {\n currentAnimation,\n attackAnimation,\n isBlocking = false,\n stance,\n laterality = \"right\",\n onAnimationComplete,\n } = options;\n\n // Animation state\n const [animState, setAnimState] = useState<SkeletalAnimationState>({\n currentAnimation: null,\n currentTime: 0,\n isPlaying: false,\n playbackSpeed: 1.0,\n previousKeyframeIndex: 0,\n nextKeyframeIndex: 1,\n });\n\n // Animation time ref (updated at 60fps without triggering re-renders)\n const animTimeRef = useRef(0);\n\n // Diagonal step rotation override (Y-axis rotation in radians)\n const [diagonalRotationY, setDiagonalRotationY] = useState<number | null>(\n null,\n );\n\n // Load animation when currentAnimation, blocking state, or laterality changes\n useEffect(() => {\n // Reset animation time whenever animation changes\n animTimeRef.current = 0;\n\n let selectedAnim: SkeletalAnimation | null = null;\n let playbackSpeed: number;\n let shouldClearDiagonalRotation = true;\n\n if (currentAnimation === \"attack\" && attackAnimation) {\n // Attack animation - first check stance-specific attacks, then generic\n selectedAnim =\n getAttackAnimation(attackAnimation) ??\n getAnimation(attackAnimation) ??\n null;\n playbackSpeed = 1.0;\n } else if (currentAnimation === \"defend\" || isBlocking) {\n // Block/defend animation - check stance-specific defensive animations first\n // If attackAnimation contains a defensive animation name, use it\n if (attackAnimation) {\n selectedAnim = getDefensiveAnimation(attackAnimation) ?? null;\n }\n // Fall back to generic block animation\n selectedAnim ??= getAnimation(\"block\") ?? null;\n playbackSpeed = 1.0;\n } else if (currentAnimation === \"idle\") {\n // Idle animation - use trigram-specific stance idle if stance is provided\n // Otherwise fall back to generic idle breathing animation\n if (stance) {\n const stanceIdleAnim = `stance_${stance}` as PlayerAnimation;\n selectedAnim = getAnimationByName(stanceIdleAnim) ?? null;\n }\n // Fall back to generic idle if no stance or stance animation not found\n selectedAnim ??= getAnimation(\"idle\") ?? null;\n playbackSpeed = 0.5; // Slow breathing animation\n } else if (currentAnimation === \"walk\") {\n // Walking animation - use trigram-specific walk if stance is provided\n if (stance) {\n const stanceWalkAnim = `walk_${stance}` as PlayerAnimation;\n selectedAnim = getAnimationByName(stanceWalkAnim) ?? null;\n }\n // Fall back to generic walk if no stance or stance animation not found\n selectedAnim ??= getAnimation(\"walk\") ?? null;\n playbackSpeed = 1.0;\n } else if (currentAnimation === \"run\") {\n // Running animation - use trigram-specific run if stance is provided\n if (stance) {\n const stanceRunAnim = `run_${stance}` as PlayerAnimation;\n selectedAnim = getAnimationByName(stanceRunAnim) ?? null;\n }\n // Fall back to generic run if no stance or stance animation not found\n selectedAnim ??= getAnimation(\"run\") ?? null;\n playbackSpeed = 1.0;\n } else if (currentAnimation?.startsWith(\"fall_\")) {\n // Fall animations - directional falls from BasicAnimations\n selectedAnim = getAnimation(currentAnimation) ?? null;\n playbackSpeed = 1.0;\n } else if (currentAnimation === \"stance_change\") {\n // Stance change animation\n selectedAnim =\n getAnimation(\"stance_change\") ?? getAnimation(\"idle_stance\") ?? null;\n playbackSpeed = 1.2; // Slightly faster for responsiveness\n } else if (currentAnimation === \"hit\") {\n // Hit reaction - stop animation\n setAnimState((prev) => ({\n ...prev,\n isPlaying: false,\n currentTime: 0,\n }));\n return;\n } else if (currentAnimation?.startsWith(\"step_\")) {\n // Tactical step animation\n selectedAnim = getStepAnimation(currentAnimation) ?? null;\n playbackSpeed = 1.0;\n\n // Handle diagonal step rotation\n if (DIAGONAL_STEP_ANIMATIONS.has(currentAnimation)) {\n shouldClearDiagonalRotation = false;\n // Diagonal rotation will be handled by parent component\n // This hook only manages the flag\n }\n } else if (currentAnimation?.startsWith(\"footwork_\")) {\n // Footwork pattern animation\n selectedAnim = getFootworkAnimation(currentAnimation) ?? null;\n playbackSpeed = 1.0;\n } else if (currentAnimation?.startsWith(\"stance_\")) {\n // Stance-specific idle animation with proper biomechanics\n // Use getAnimationByName which searches ALL_ANIMATIONS (includes STANCE_ANIMATIONS)\n selectedAnim = getAnimationByName(currentAnimation) ?? null;\n playbackSpeed = 0.5; // Slow breathing animation for stance idle\n } else {\n // Idle animation (fallback)\n selectedAnim = getAnimation(\"idle_stance\") ?? null;\n playbackSpeed = 0.5; // Slow breathing animation\n }\n\n // Apply laterality transformation if selectedAnim exists\n // laterality directly affects animation mirroring:\n // \"left\" = left foot forward (왼발서기) → animations mirrored\n // \"right\" = right foot forward (오른발서기) → base animations (default)\n if (selectedAnim) {\n selectedAnim = applyLaterality(selectedAnim, laterality);\n }\n\n // Clear diagonal rotation for non-diagonal animations\n if (shouldClearDiagonalRotation) {\n setDiagonalRotationY(null);\n }\n\n setAnimState({\n currentAnimation: selectedAnim,\n currentTime: 0,\n isPlaying: true,\n playbackSpeed,\n previousKeyframeIndex: 0,\n nextKeyframeIndex: 1,\n });\n }, [currentAnimation, attackAnimation, isBlocking, stance, laterality]);\n\n // Update animation and apply to rig (called at 60fps in useFrame)\n // PHASE 2: Now uses cached interpolation and batch bone updates\n const updateRigAnimation = useCallback(\n (targetRig: SkeletalRig, delta: number) => {\n if (animState.isPlaying && animState.currentAnimation) {\n const frameStartTime = performance.now();\n\n // Advance animation time\n let newTime = animTimeRef.current + delta * animState.playbackSpeed;\n let completed = false;\n\n // Handle looping or completion\n if (newTime >= animState.currentAnimation.duration) {\n if (animState.currentAnimation.loop) {\n newTime = newTime % animState.currentAnimation.duration;\n } else {\n newTime = animState.currentAnimation.duration;\n completed = true;\n }\n }\n\n // Use cached interpolation for 90%+ cache hit rate\n // Use animation.name as the unique identifier\n const keyframe = interpolateKeyframeCached(\n animState.currentAnimation.name,\n animState.currentAnimation,\n newTime,\n );\n\n if (keyframe) {\n // Batch update bones (60% faster than individual updates)\n batchUpdateBones(targetRig, keyframe);\n }\n\n // Update time ref\n animTimeRef.current = newTime;\n\n // Record performance metrics\n const frameTime = performance.now() - frameStartTime;\n performanceMonitor.recordFrame(frameTime);\n\n // Handle animation completion\n if (completed) {\n animTimeRef.current = 0;\n setAnimState((prev) => ({\n ...prev,\n isPlaying: false,\n currentTime: 0,\n }));\n\n // Trigger callback\n if (onAnimationComplete) {\n onAnimationComplete();\n }\n }\n }\n },\n [animState, onAnimationComplete],\n );\n\n return {\n animState,\n animTimeRef,\n updateRigAnimation,\n diagonalRotationY,\n };\n}\n"],"mappings":"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"}
|
|
@@ -158,23 +158,24 @@ function useTouchControls({ onGesture, enabled = true, minSwipeDistance = 50, ma
|
|
|
158
158
|
setIsTouching(false);
|
|
159
159
|
if (distance >= minSwipeDistance) {
|
|
160
160
|
e.preventDefault();
|
|
161
|
-
if (Math.abs(deltaX) > Math.abs(deltaY))
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
161
|
+
if (Math.abs(deltaX) > Math.abs(deltaY)) {
|
|
162
|
+
if (deltaX > 0) onGesture({
|
|
163
|
+
type: "swipe-right",
|
|
164
|
+
distance,
|
|
165
|
+
startX: touchStart.clientX,
|
|
166
|
+
startY: touchStart.clientY,
|
|
167
|
+
endX: touchEnd.clientX,
|
|
168
|
+
endY: touchEnd.clientY
|
|
169
|
+
});
|
|
170
|
+
else onGesture({
|
|
171
|
+
type: "swipe-left",
|
|
172
|
+
distance,
|
|
173
|
+
startX: touchStart.clientX,
|
|
174
|
+
startY: touchStart.clientY,
|
|
175
|
+
endX: touchEnd.clientX,
|
|
176
|
+
endY: touchEnd.clientY
|
|
177
|
+
});
|
|
178
|
+
} else if (deltaY > 0) onGesture({
|
|
178
179
|
type: "swipe-down",
|
|
179
180
|
distance,
|
|
180
181
|
startX: touchStart.clientX,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useTouchControls.js","names":[],"sources":["../../src/hooks/useTouchControls.ts"],"sourcesContent":["/**\n * Touch Controls Hook\n * \n * Manages touch event handling and gesture recognition for mobile gameplay\n * Provides swipe detection, multi-touch support, and touch-based movement\n * \n * @module hooks/useTouchControls\n * @category Mobile Controls\n * @korean 터치 컨트롤 훅\n */\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\n/**\n * Gesture types supported by the touch control system\n * \n * Added tactical step gestures for Korean martial arts footwork:\n * - tap-{direction}: Quick tap for tactical 30cm step\n * - hold-{direction}: Hold for continuous walk\n * \n * @korean 제스처타입\n */\nexport type GestureType =\n | 'swipe-right'\n | 'swipe-left'\n | 'swipe-up'\n | 'swipe-down'\n | 'two-finger-tap'\n | 'tap'\n | 'tap-forward'\n | 'tap-back'\n | 'tap-left'\n | 'tap-right'\n | 'tap-forward-left'\n | 'tap-forward-right'\n | 'tap-back-left'\n | 'tap-back-right'\n | 'hold-forward'\n | 'hold-back'\n | 'hold-left'\n | 'hold-right';\n\n/**\n * Gesture event data\n */\nexport interface GestureEvent {\n /** Type of gesture detected */\n readonly type: GestureType;\n /** Distance of swipe in pixels (for swipe gestures) */\n readonly distance?: number;\n /** Coordinates of touch start */\n readonly startX?: number;\n readonly startY?: number;\n /** Coordinates of touch end */\n readonly endX?: number;\n readonly endY?: number;\n}\n\n/**\n * Props for useTouchControls hook\n */\nexport interface UseTouchControlsProps {\n /** Callback when gesture is detected */\n readonly onGesture: (gesture: GestureEvent) => void;\n /** Whether touch input is enabled */\n readonly enabled?: boolean;\n /** Minimum swipe distance in pixels (default: 50) */\n readonly minSwipeDistance?: number;\n /** Maximum time for tap in ms (default: 300) */\n readonly maxTapDuration?: number;\n /** Time threshold for hold vs tap in ms (default: 200) */\n readonly holdThreshold?: number;\n /** Enable haptic feedback for steps (default: true) */\n readonly enableHaptics?: boolean;\n}\n\n/**\n * Return type for useTouchControls hook\n */\nexport interface UseTouchControlsReturn {\n /** Whether a touch is currently active */\n readonly isTouching: boolean;\n}\n\n/**\n * Custom hook for handling touch controls and gesture recognition\n * \n * Features:\n * - Swipe detection (horizontal and vertical)\n * - Two-finger tap detection for vital point mode\n * - Single tap detection\n * - Tactical step gestures (tap) vs continuous walk (hold)\n * - Distance calculation for swipe intensity\n * - Configurable thresholds\n * - Haptic feedback for tactical steps\n * \n * Gesture Mapping:\n * - Swipe Right: Advance toward opponent\n * - Swipe Left: Retreat from opponent\n * - Swipe Up: High stance mode\n * - Swipe Down: Low stance mode\n * - Two-Finger Tap: Activate vital point targeting mode\n * - Single Tap (directional): Tactical 30cm step (전술적 발걸음)\n * - Hold (directional): Continuous walk movement\n * \n * @example\n * ```typescript\n * const { isTouching } = useTouchControls({\n * onGesture: (gesture) => {\n * switch (gesture.type) {\n * case 'tap-forward':\n * handleTacticalStep('forward'); // 전진보법\n * break;\n * case 'hold-forward':\n * handleContinuousWalk('forward');\n * break;\n * case 'two-finger-tap':\n * activateVitalPointMode();\n * break;\n * }\n * },\n * enabled: !isPaused,\n * holdThreshold: 200, // 200ms to distinguish tap from hold\n * enableHaptics: true,\n * });\n * ```\n * \n * @public\n * @korean 터치컨트롤사용\n */\nexport function useTouchControls({\n onGesture,\n enabled = true,\n minSwipeDistance = 50,\n maxTapDuration = 300,\n holdThreshold = 200,\n enableHaptics = true,\n}: UseTouchControlsProps): UseTouchControlsReturn {\n const touchStartRef = useRef<Touch | null>(null);\n const touchStartTimeRef = useRef<number>(0);\n const [isTouching, setIsTouching] = useState<boolean>(false);\n const holdTimerRef = useRef<number | null>(null);\n \n /**\n * Trigger haptic feedback for tactical step\n * Light vibration (10ms) to confirm step input\n * \n * @korean 햅틱피드백\n */\n const triggerStepHaptic = useCallback(() => {\n if (!enableHaptics || !navigator.vibrate) return;\n \n try {\n // Short, light vibration for step (10ms)\n navigator.vibrate(10);\n } catch (error) {\n // Haptic feedback not supported or failed\n console.debug('Haptic feedback not available:', error);\n }\n }, [enableHaptics]);\n \n /**\n * Determine directional gesture from touch position\n * Used for D-pad style controls\n * Returns null for ambiguous/stationary taps\n * \n * @korean 방향제스처감지\n */\n const getDirectionalGesture = useCallback((\n startX: number,\n startY: number,\n endX: number,\n endY: number,\n isTap: boolean\n ): GestureType | null => {\n const deltaX = endX - startX;\n const deltaY = endY - startY;\n const absX = Math.abs(deltaX);\n const absY = Math.abs(deltaY);\n \n // If movement is too small, it's not a directional gesture\n const minDirectionalMovement = 15; // pixels\n if (absX < minDirectionalMovement && absY < minDirectionalMovement) {\n return null; // Ambiguous tap, not directional\n }\n \n // Check for diagonal gestures (45-degree threshold)\n const isDiagonal = absX > 20 && absY > 20 && Math.abs(absX - absY) < 30;\n \n const prefix = isTap ? 'tap' : 'hold';\n \n if (isDiagonal) {\n // Diagonal gestures (only for taps/steps)\n if (isTap) {\n if (deltaY < 0 && deltaX < 0) return 'tap-forward-left';\n if (deltaY < 0 && deltaX > 0) return 'tap-forward-right';\n if (deltaY > 0 && deltaX < 0) return 'tap-back-left';\n if (deltaY > 0 && deltaX > 0) return 'tap-back-right';\n }\n return null;\n }\n \n // Cardinal directions\n if (absX > absY) {\n // Horizontal\n return deltaX > 0 ? `${prefix}-right` as GestureType : `${prefix}-left` as GestureType;\n } else {\n // Vertical\n return deltaY < 0 ? `${prefix}-forward` as GestureType : `${prefix}-back` as GestureType;\n }\n }, []);\n\n /**\n * Handle touch start event\n */\n const handleTouchStart = useCallback((e: TouchEvent) => {\n if (!enabled) return;\n\n const touch = e.touches[0];\n touchStartRef.current = touch;\n touchStartTimeRef.current = Date.now();\n setIsTouching(true);\n\n // Check for two-finger tap immediately\n if (e.touches.length === 2) {\n e.preventDefault();\n onGesture({\n type: 'two-finger-tap',\n startX: touch.clientX,\n startY: touch.clientY,\n });\n return;\n }\n \n // Capture screen dimensions at touch start time to prevent incorrect\n // direction calculation if window is resized during hold\n const screenCenterX = window.innerWidth / 2;\n const screenCenterY = window.innerHeight / 2;\n \n // Set up hold detection timer\n // Note: Hold gesture direction is determined from the initial touch position\n // relative to screen center. This supports D-pad style layouts where each\n // region of the screen (or an overlaid control) corresponds to a cardinal direction.\n holdTimerRef.current = window.setTimeout(() => {\n // Touch held for longer than threshold - trigger hold gesture\n // Check touchStartRef to ensure touch hasn't ended before timer fired\n if (touchStartRef.current) {\n const { clientX, clientY } = touchStartRef.current;\n\n // Use captured screen center coordinates (from touch start time)\n const deltaX = clientX - screenCenterX;\n // Invert Y so that a touch higher on the screen is considered \"forward\"\n const deltaY = screenCenterY - clientY;\n\n const holdGesture: GestureType =\n Math.abs(deltaX) >= Math.abs(deltaY)\n ? (deltaX > 0 ? 'hold-right' : 'hold-left')\n : (deltaY > 0 ? 'hold-forward' : 'hold-back');\n\n onGesture({\n type: holdGesture,\n startX: clientX,\n startY: clientY,\n });\n }\n }, holdThreshold);\n }, [enabled, onGesture, holdThreshold]);\n\n /**\n * Handle touch end event\n */\n const handleTouchEnd = useCallback((e: TouchEvent) => {\n if (!enabled || !touchStartRef.current) return;\n\n const touchEnd = e.changedTouches[0];\n const touchStart = touchStartRef.current;\n const touchDuration = Date.now() - touchStartTimeRef.current;\n\n // Clear hold timer\n if (holdTimerRef.current) {\n clearTimeout(holdTimerRef.current);\n holdTimerRef.current = null;\n }\n\n // Calculate deltas\n const deltaX = touchEnd.clientX - touchStart.clientX;\n const deltaY = touchEnd.clientY - touchStart.clientY;\n const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);\n\n // Reset touch state\n setIsTouching(false);\n\n // Detect gesture type\n if (distance >= minSwipeDistance) {\n // Swipe gesture (for quick directional inputs)\n e.preventDefault();\n\n // Determine primary direction\n if (Math.abs(deltaX) > Math.abs(deltaY)) {\n // Horizontal swipe\n if (deltaX > 0) {\n onGesture({\n type: 'swipe-right',\n distance,\n startX: touchStart.clientX,\n startY: touchStart.clientY,\n endX: touchEnd.clientX,\n endY: touchEnd.clientY,\n });\n } else {\n onGesture({\n type: 'swipe-left',\n distance,\n startX: touchStart.clientX,\n startY: touchStart.clientY,\n endX: touchEnd.clientX,\n endY: touchEnd.clientY,\n });\n }\n } else {\n // Vertical swipe\n if (deltaY > 0) {\n onGesture({\n type: 'swipe-down',\n distance,\n startX: touchStart.clientX,\n startY: touchStart.clientY,\n endX: touchEnd.clientX,\n endY: touchEnd.clientY,\n });\n } else {\n onGesture({\n type: 'swipe-up',\n distance,\n startX: touchStart.clientX,\n startY: touchStart.clientY,\n endX: touchEnd.clientX,\n endY: touchEnd.clientY,\n });\n }\n }\n } else if (touchDuration <= maxTapDuration && touchDuration < holdThreshold) {\n // Quick tap - tactical step gesture\n e.preventDefault();\n \n const tapGesture = getDirectionalGesture(\n touchStart.clientX,\n touchStart.clientY,\n touchEnd.clientX,\n touchEnd.clientY,\n true // Is a tap\n );\n \n if (tapGesture) {\n // Directional step tap\n triggerStepHaptic();\n onGesture({\n type: tapGesture,\n startX: touchStart.clientX,\n startY: touchStart.clientY,\n endX: touchEnd.clientX,\n endY: touchEnd.clientY,\n });\n } else {\n // Generic tap (fallback)\n onGesture({\n type: 'tap',\n startX: touchStart.clientX,\n startY: touchStart.clientY,\n endX: touchEnd.clientX,\n endY: touchEnd.clientY,\n });\n }\n }\n\n // Clear touch start reference\n touchStartRef.current = null;\n }, [enabled, minSwipeDistance, maxTapDuration, holdThreshold, onGesture, getDirectionalGesture, triggerStepHaptic]);\n\n /**\n * Handle touch cancel event\n */\n const handleTouchCancel = useCallback(() => {\n // Clear hold timer\n if (holdTimerRef.current) {\n clearTimeout(holdTimerRef.current);\n holdTimerRef.current = null;\n }\n \n touchStartRef.current = null;\n touchStartTimeRef.current = 0;\n setIsTouching(false);\n }, []);\n\n /**\n * Setup touch event listeners\n */\n useEffect(() => {\n if (!enabled) return;\n\n const options: AddEventListenerOptions = {\n passive: false, // Allow preventDefault for gesture handling\n };\n\n document.addEventListener('touchstart', handleTouchStart, options);\n document.addEventListener('touchend', handleTouchEnd, options);\n document.addEventListener('touchcancel', handleTouchCancel, options);\n\n return () => {\n document.removeEventListener('touchstart', handleTouchStart);\n document.removeEventListener('touchend', handleTouchEnd);\n document.removeEventListener('touchcancel', handleTouchCancel);\n };\n }, [enabled, handleTouchStart, handleTouchEnd, handleTouchCancel]);\n\n return {\n isTouching,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useTouchControls.js","names":[],"sources":["../../src/hooks/useTouchControls.ts"],"sourcesContent":["/**\n * Touch Controls Hook\n * \n * Manages touch event handling and gesture recognition for mobile gameplay\n * Provides swipe detection, multi-touch support, and touch-based movement\n * \n * @module hooks/useTouchControls\n * @category Mobile Controls\n * @korean 터치 컨트롤 훅\n */\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\n/**\n * Gesture types supported by the touch control system\n * \n * Added tactical step gestures for Korean martial arts footwork:\n * - tap-{direction}: Quick tap for tactical 30cm step\n * - hold-{direction}: Hold for continuous walk\n * \n * @korean 제스처타입\n */\nexport type GestureType =\n | 'swipe-right'\n | 'swipe-left'\n | 'swipe-up'\n | 'swipe-down'\n | 'two-finger-tap'\n | 'tap'\n | 'tap-forward'\n | 'tap-back'\n | 'tap-left'\n | 'tap-right'\n | 'tap-forward-left'\n | 'tap-forward-right'\n | 'tap-back-left'\n | 'tap-back-right'\n | 'hold-forward'\n | 'hold-back'\n | 'hold-left'\n | 'hold-right';\n\n/**\n * Gesture event data\n */\nexport interface GestureEvent {\n /** Type of gesture detected */\n readonly type: GestureType;\n /** Distance of swipe in pixels (for swipe gestures) */\n readonly distance?: number;\n /** Coordinates of touch start */\n readonly startX?: number;\n readonly startY?: number;\n /** Coordinates of touch end */\n readonly endX?: number;\n readonly endY?: number;\n}\n\n/**\n * Props for useTouchControls hook\n */\nexport interface UseTouchControlsProps {\n /** Callback when gesture is detected */\n readonly onGesture: (gesture: GestureEvent) => void;\n /** Whether touch input is enabled */\n readonly enabled?: boolean;\n /** Minimum swipe distance in pixels (default: 50) */\n readonly minSwipeDistance?: number;\n /** Maximum time for tap in ms (default: 300) */\n readonly maxTapDuration?: number;\n /** Time threshold for hold vs tap in ms (default: 200) */\n readonly holdThreshold?: number;\n /** Enable haptic feedback for steps (default: true) */\n readonly enableHaptics?: boolean;\n}\n\n/**\n * Return type for useTouchControls hook\n */\nexport interface UseTouchControlsReturn {\n /** Whether a touch is currently active */\n readonly isTouching: boolean;\n}\n\n/**\n * Custom hook for handling touch controls and gesture recognition\n * \n * Features:\n * - Swipe detection (horizontal and vertical)\n * - Two-finger tap detection for vital point mode\n * - Single tap detection\n * - Tactical step gestures (tap) vs continuous walk (hold)\n * - Distance calculation for swipe intensity\n * - Configurable thresholds\n * - Haptic feedback for tactical steps\n * \n * Gesture Mapping:\n * - Swipe Right: Advance toward opponent\n * - Swipe Left: Retreat from opponent\n * - Swipe Up: High stance mode\n * - Swipe Down: Low stance mode\n * - Two-Finger Tap: Activate vital point targeting mode\n * - Single Tap (directional): Tactical 30cm step (전술적 발걸음)\n * - Hold (directional): Continuous walk movement\n * \n * @example\n * ```typescript\n * const { isTouching } = useTouchControls({\n * onGesture: (gesture) => {\n * switch (gesture.type) {\n * case 'tap-forward':\n * handleTacticalStep('forward'); // 전진보법\n * break;\n * case 'hold-forward':\n * handleContinuousWalk('forward');\n * break;\n * case 'two-finger-tap':\n * activateVitalPointMode();\n * break;\n * }\n * },\n * enabled: !isPaused,\n * holdThreshold: 200, // 200ms to distinguish tap from hold\n * enableHaptics: true,\n * });\n * ```\n * \n * @public\n * @korean 터치컨트롤사용\n */\nexport function useTouchControls({\n onGesture,\n enabled = true,\n minSwipeDistance = 50,\n maxTapDuration = 300,\n holdThreshold = 200,\n enableHaptics = true,\n}: UseTouchControlsProps): UseTouchControlsReturn {\n const touchStartRef = useRef<Touch | null>(null);\n const touchStartTimeRef = useRef<number>(0);\n const [isTouching, setIsTouching] = useState<boolean>(false);\n const holdTimerRef = useRef<number | null>(null);\n \n /**\n * Trigger haptic feedback for tactical step\n * Light vibration (10ms) to confirm step input\n * \n * @korean 햅틱피드백\n */\n const triggerStepHaptic = useCallback(() => {\n if (!enableHaptics || !navigator.vibrate) return;\n \n try {\n // Short, light vibration for step (10ms)\n navigator.vibrate(10);\n } catch (error) {\n // Haptic feedback not supported or failed\n console.debug('Haptic feedback not available:', error);\n }\n }, [enableHaptics]);\n \n /**\n * Determine directional gesture from touch position\n * Used for D-pad style controls\n * Returns null for ambiguous/stationary taps\n * \n * @korean 방향제스처감지\n */\n const getDirectionalGesture = useCallback((\n startX: number,\n startY: number,\n endX: number,\n endY: number,\n isTap: boolean\n ): GestureType | null => {\n const deltaX = endX - startX;\n const deltaY = endY - startY;\n const absX = Math.abs(deltaX);\n const absY = Math.abs(deltaY);\n \n // If movement is too small, it's not a directional gesture\n const minDirectionalMovement = 15; // pixels\n if (absX < minDirectionalMovement && absY < minDirectionalMovement) {\n return null; // Ambiguous tap, not directional\n }\n \n // Check for diagonal gestures (45-degree threshold)\n const isDiagonal = absX > 20 && absY > 20 && Math.abs(absX - absY) < 30;\n \n const prefix = isTap ? 'tap' : 'hold';\n \n if (isDiagonal) {\n // Diagonal gestures (only for taps/steps)\n if (isTap) {\n if (deltaY < 0 && deltaX < 0) return 'tap-forward-left';\n if (deltaY < 0 && deltaX > 0) return 'tap-forward-right';\n if (deltaY > 0 && deltaX < 0) return 'tap-back-left';\n if (deltaY > 0 && deltaX > 0) return 'tap-back-right';\n }\n return null;\n }\n \n // Cardinal directions\n if (absX > absY) {\n // Horizontal\n return deltaX > 0 ? `${prefix}-right` as GestureType : `${prefix}-left` as GestureType;\n } else {\n // Vertical\n return deltaY < 0 ? `${prefix}-forward` as GestureType : `${prefix}-back` as GestureType;\n }\n }, []);\n\n /**\n * Handle touch start event\n */\n const handleTouchStart = useCallback((e: TouchEvent) => {\n if (!enabled) return;\n\n const touch = e.touches[0];\n touchStartRef.current = touch;\n touchStartTimeRef.current = Date.now();\n setIsTouching(true);\n\n // Check for two-finger tap immediately\n if (e.touches.length === 2) {\n e.preventDefault();\n onGesture({\n type: 'two-finger-tap',\n startX: touch.clientX,\n startY: touch.clientY,\n });\n return;\n }\n \n // Capture screen dimensions at touch start time to prevent incorrect\n // direction calculation if window is resized during hold\n const screenCenterX = window.innerWidth / 2;\n const screenCenterY = window.innerHeight / 2;\n \n // Set up hold detection timer\n // Note: Hold gesture direction is determined from the initial touch position\n // relative to screen center. This supports D-pad style layouts where each\n // region of the screen (or an overlaid control) corresponds to a cardinal direction.\n holdTimerRef.current = window.setTimeout(() => {\n // Touch held for longer than threshold - trigger hold gesture\n // Check touchStartRef to ensure touch hasn't ended before timer fired\n if (touchStartRef.current) {\n const { clientX, clientY } = touchStartRef.current;\n\n // Use captured screen center coordinates (from touch start time)\n const deltaX = clientX - screenCenterX;\n // Invert Y so that a touch higher on the screen is considered \"forward\"\n const deltaY = screenCenterY - clientY;\n\n const holdGesture: GestureType =\n Math.abs(deltaX) >= Math.abs(deltaY)\n ? (deltaX > 0 ? 'hold-right' : 'hold-left')\n : (deltaY > 0 ? 'hold-forward' : 'hold-back');\n\n onGesture({\n type: holdGesture,\n startX: clientX,\n startY: clientY,\n });\n }\n }, holdThreshold);\n }, [enabled, onGesture, holdThreshold]);\n\n /**\n * Handle touch end event\n */\n const handleTouchEnd = useCallback((e: TouchEvent) => {\n if (!enabled || !touchStartRef.current) return;\n\n const touchEnd = e.changedTouches[0];\n const touchStart = touchStartRef.current;\n const touchDuration = Date.now() - touchStartTimeRef.current;\n\n // Clear hold timer\n if (holdTimerRef.current) {\n clearTimeout(holdTimerRef.current);\n holdTimerRef.current = null;\n }\n\n // Calculate deltas\n const deltaX = touchEnd.clientX - touchStart.clientX;\n const deltaY = touchEnd.clientY - touchStart.clientY;\n const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);\n\n // Reset touch state\n setIsTouching(false);\n\n // Detect gesture type\n if (distance >= minSwipeDistance) {\n // Swipe gesture (for quick directional inputs)\n e.preventDefault();\n\n // Determine primary direction\n if (Math.abs(deltaX) > Math.abs(deltaY)) {\n // Horizontal swipe\n if (deltaX > 0) {\n onGesture({\n type: 'swipe-right',\n distance,\n startX: touchStart.clientX,\n startY: touchStart.clientY,\n endX: touchEnd.clientX,\n endY: touchEnd.clientY,\n });\n } else {\n onGesture({\n type: 'swipe-left',\n distance,\n startX: touchStart.clientX,\n startY: touchStart.clientY,\n endX: touchEnd.clientX,\n endY: touchEnd.clientY,\n });\n }\n } else {\n // Vertical swipe\n if (deltaY > 0) {\n onGesture({\n type: 'swipe-down',\n distance,\n startX: touchStart.clientX,\n startY: touchStart.clientY,\n endX: touchEnd.clientX,\n endY: touchEnd.clientY,\n });\n } else {\n onGesture({\n type: 'swipe-up',\n distance,\n startX: touchStart.clientX,\n startY: touchStart.clientY,\n endX: touchEnd.clientX,\n endY: touchEnd.clientY,\n });\n }\n }\n } else if (touchDuration <= maxTapDuration && touchDuration < holdThreshold) {\n // Quick tap - tactical step gesture\n e.preventDefault();\n \n const tapGesture = getDirectionalGesture(\n touchStart.clientX,\n touchStart.clientY,\n touchEnd.clientX,\n touchEnd.clientY,\n true // Is a tap\n );\n \n if (tapGesture) {\n // Directional step tap\n triggerStepHaptic();\n onGesture({\n type: tapGesture,\n startX: touchStart.clientX,\n startY: touchStart.clientY,\n endX: touchEnd.clientX,\n endY: touchEnd.clientY,\n });\n } else {\n // Generic tap (fallback)\n onGesture({\n type: 'tap',\n startX: touchStart.clientX,\n startY: touchStart.clientY,\n endX: touchEnd.clientX,\n endY: touchEnd.clientY,\n });\n }\n }\n\n // Clear touch start reference\n touchStartRef.current = null;\n }, [enabled, minSwipeDistance, maxTapDuration, holdThreshold, onGesture, getDirectionalGesture, triggerStepHaptic]);\n\n /**\n * Handle touch cancel event\n */\n const handleTouchCancel = useCallback(() => {\n // Clear hold timer\n if (holdTimerRef.current) {\n clearTimeout(holdTimerRef.current);\n holdTimerRef.current = null;\n }\n \n touchStartRef.current = null;\n touchStartTimeRef.current = 0;\n setIsTouching(false);\n }, []);\n\n /**\n * Setup touch event listeners\n */\n useEffect(() => {\n if (!enabled) return;\n\n const options: AddEventListenerOptions = {\n passive: false, // Allow preventDefault for gesture handling\n };\n\n document.addEventListener('touchstart', handleTouchStart, options);\n document.addEventListener('touchend', handleTouchEnd, options);\n document.addEventListener('touchcancel', handleTouchCancel, options);\n\n return () => {\n document.removeEventListener('touchstart', handleTouchStart);\n document.removeEventListener('touchend', handleTouchEnd);\n document.removeEventListener('touchcancel', handleTouchCancel);\n };\n }, [enabled, handleTouchStart, handleTouchEnd, handleTouchCancel]);\n\n return {\n isTouching,\n };\n}\n"],"mappings":"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"}
|
|
@@ -175,7 +175,11 @@ var AnimationBuilder = class AnimationBuilder {
|
|
|
175
175
|
};
|
|
176
176
|
}
|
|
177
177
|
};
|
|
178
|
-
|
|
178
|
+
/**
|
|
179
|
+
* Reusable animation presets for common combat patterns
|
|
180
|
+
* @korean 재사용애니메이션프리셋
|
|
181
|
+
*/
|
|
182
|
+
var AnimationPresets = class {
|
|
179
183
|
/**
|
|
180
184
|
* Standard fighting guard position - protects face and body
|
|
181
185
|
* Both elbows tight, hands at chin/temple level
|
|
@@ -253,7 +257,7 @@ var AnimationBuilder = class AnimationBuilder {
|
|
|
253
257
|
spine: new THREE.Euler(0, .35, 0),
|
|
254
258
|
pelvis: new THREE.Euler(0, .2, 0)
|
|
255
259
|
};
|
|
256
|
-
}
|
|
260
|
+
};
|
|
257
261
|
//#endregion
|
|
258
262
|
export { AnimationBuilder };
|
|
259
263
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AnimationBuilder.js","names":[],"sources":["../../../../src/systems/animation/builders/AnimationBuilder.ts"],"sourcesContent":["/**\n * Animation Builder - Fluent API for creating skeletal animations\n *\n * Provides a cleaner, more maintainable way to define animations with\n * reduced boilerplate and better readability.\n *\n * @module systems/animation/AnimationBuilder\n * @category Animation System\n * @korean 애니메이션빌더\n */\n\nimport * as THREE from \"three\";\nimport type { AnimationKeyframe, SkeletalAnimation } from \"@/types/skeletal\";\nimport { BoneName } from \"@/types/skeletal\";\n\n/**\n * Keyframe builder for fluent keyframe construction\n * @korean 키프레임빌더\n */\nclass KeyframeBuilder {\n private time: number;\n private easing: string;\n private boneRotations: Map<BoneName, THREE.Euler>;\n private bonePositions: Map<BoneName, THREE.Vector3>;\n private parentBuilder: AnimationBuilder | null = null;\n\n constructor(time: number, easing: string = \"linear\") {\n this.time = time;\n this.easing = easing;\n this.boneRotations = new Map();\n this.bonePositions = new Map();\n }\n\n /**\n * Set parent animation builder (for chaining)\n * @internal\n */\n setParent(parent: AnimationBuilder): this {\n this.parentBuilder = parent;\n return this;\n }\n\n /**\n * Add bone rotation to keyframe\n * @param bone - Bone to rotate\n * @param x - X rotation in radians\n * @param y - Y rotation in radians\n * @param z - Z rotation in radians\n * @param order - Rotation order (default: XYZ)\n * @returns This builder for chaining\n */\n rotate(\n bone: BoneName,\n x: number,\n y: number,\n z: number,\n order: THREE.EulerOrder = \"XYZ\",\n ): this {\n this.boneRotations.set(bone, new THREE.Euler(x, y, z, order));\n return this;\n }\n\n /**\n * Add bone position to keyframe\n * @param bone - Bone to position\n * @param x - X position\n * @param y - Y position\n * @param z - Z position\n * @returns This builder for chaining\n */\n position(bone: BoneName, x: number, y: number, z: number): this {\n this.bonePositions.set(bone, new THREE.Vector3(x, y, z));\n return this;\n }\n\n /**\n * Build the keyframe and return to animation builder\n * @returns Animation builder for chaining\n */\n build(): AnimationBuilder {\n const keyframe: AnimationKeyframe = {\n time: this.time,\n easing: this.easing as \"linear\" | \"ease-in\" | \"ease-out\" | \"ease-in-out\",\n boneRotations: this.boneRotations,\n bonePositions: this.bonePositions,\n };\n\n if (this.parentBuilder) {\n this.parentBuilder.addKeyframe(keyframe);\n return this.parentBuilder;\n }\n\n // This indicates incorrect usage of the builder API; parent must be set via setParent(...)\n throw new Error(\n \"KeyframeBuilder.build() called without a parent AnimationBuilder. Ensure setParent(...) is called before build().\",\n );\n }\n}\n\n/**\n * Animation builder for fluent animation construction\n * @korean 애니메이션빌더\n */\nexport class AnimationBuilder {\n private animationName: string;\n private koreanName: string;\n private duration: number;\n private loop: boolean;\n private type: \"attack\" | \"defense\" | \"movement\" | \"idle\";\n private keyframes: AnimationKeyframe[];\n\n private constructor(name: string) {\n this.animationName = name;\n this.koreanName = name;\n this.duration = 1.0;\n this.loop = false;\n this.type = \"idle\";\n this.keyframes = [];\n }\n\n /**\n * Create a new animation builder\n * @param name - Animation name\n * @returns New animation builder\n */\n static create(name: string): AnimationBuilder {\n return new AnimationBuilder(name);\n }\n\n /**\n * Set Korean name for animation\n * @param name - Korean name\n * @returns This builder for chaining\n */\n withKoreanName(name: string): this {\n this.koreanName = name;\n return this;\n }\n\n /**\n * Set animation duration\n * @param seconds - Duration in seconds\n * @returns This builder for chaining\n */\n withDuration(seconds: number): this {\n this.duration = seconds;\n return this;\n }\n\n /**\n * Set animation loop behavior\n * @param shouldLoop - Whether animation should loop\n * @returns This builder for chaining\n */\n withLoop(shouldLoop: boolean): this {\n this.loop = shouldLoop;\n return this;\n }\n\n /**\n * Set animation type\n * @param animType - Animation type\n * @returns This builder for chaining\n */\n withType(animType: \"attack\" | \"defense\" | \"movement\" | \"idle\"): this {\n this.type = animType;\n return this;\n }\n\n /**\n * Add a keyframe to the animation\n * @param time - Time of keyframe in seconds\n * @param easing - Easing function name\n * @returns Keyframe builder for defining keyframe contents\n */\n keyframe(time: number, easing: string = \"linear\"): KeyframeBuilder {\n const kfBuilder = new KeyframeBuilder(time, easing);\n kfBuilder.setParent(this);\n return kfBuilder;\n }\n\n /**\n * Add a pre-built keyframe directly\n * @param keyframe - Complete keyframe\n * @returns This builder for chaining\n */\n addKeyframe(keyframe: AnimationKeyframe): this {\n this.keyframes.push(keyframe);\n return this;\n }\n\n /**\n * Build the complete animation\n * @returns Complete skeletal animation\n */\n build(): SkeletalAnimation {\n return {\n name: this.animationName,\n koreanName: this.koreanName,\n duration: this.duration,\n loop: this.loop,\n type: this.type,\n keyframes: this.keyframes,\n };\n }\n}\n\n/**\n * Common keyframe factories for reusable animation patterns\n * @korean 키프레임팩토리\n */\nexport class KeyframeFactories {\n /**\n * Create a guard return keyframe (return to defensive position)\n * @param time - Time of keyframe\n * @returns Guard position keyframe\n */\n static guardReturn(time: number): AnimationKeyframe {\n return {\n time,\n easing: \"ease-in\",\n boneRotations: new Map([\n [BoneName.SHOULDER_R, new THREE.Euler(-0.35, 0.35, -0.14, \"XYZ\")],\n [BoneName.SHOULDER_L, new THREE.Euler(-0.35, -0.35, 0.14, \"XYZ\")],\n [BoneName.ELBOW_R, new THREE.Euler(0, 0, 1.75, \"XYZ\")],\n [BoneName.ELBOW_L, new THREE.Euler(0, 0, -1.75, \"XYZ\")],\n [BoneName.SPINE_UPPER, new THREE.Euler(0, 0, 0, \"XYZ\")],\n [BoneName.SPINE_MIDDLE, new THREE.Euler(0, 0, 0, \"XYZ\")],\n [BoneName.PELVIS, new THREE.Euler(0, 0, 0, \"XYZ\")],\n ]),\n bonePositions: new Map([\n [BoneName.HAND_R, new THREE.Vector3(0, 0, 0)],\n [BoneName.HAND_L, new THREE.Vector3(0, 0, 0)],\n ]),\n };\n }\n\n /**\n * Create a neutral stance keyframe\n * @param time - Time of keyframe\n * @returns Neutral stance keyframe\n */\n static neutralStance(time: number): AnimationKeyframe {\n return {\n time,\n easing: \"linear\",\n boneRotations: new Map([\n [BoneName.SPINE_UPPER, new THREE.Euler(0, 0, 0, \"XYZ\")],\n [BoneName.SPINE_MIDDLE, new THREE.Euler(0, 0, 0, \"XYZ\")],\n [BoneName.PELVIS, new THREE.Euler(0, 0, 0, \"XYZ\")],\n [BoneName.HIP_L, new THREE.Euler(0, 0, 0, \"XYZ\")],\n [BoneName.HIP_R, new THREE.Euler(0, 0, 0, \"XYZ\")],\n ]),\n bonePositions: new Map(),\n };\n }\n\n /**\n * Create a torso rotation keyframe\n * @param time - Time of keyframe\n * @param angle - Rotation angle in radians (positive = clockwise)\n * @param easing - Easing function\n * @returns Torso rotation keyframe\n */\n static rotateTorso(\n time: number,\n angle: number,\n easing: \"linear\" | \"ease-in\" | \"ease-out\" | \"ease-in-out\" = \"linear\",\n ): AnimationKeyframe {\n return {\n time,\n easing,\n boneRotations: new Map([\n [BoneName.SPINE_UPPER, new THREE.Euler(0, angle, 0, \"XYZ\")],\n [BoneName.SPINE_MIDDLE, new THREE.Euler(0, angle * 0.75, 0, \"XYZ\")],\n [BoneName.PELVIS, new THREE.Euler(0, angle * 0.5, 0, \"XYZ\")],\n ]),\n bonePositions: new Map(),\n };\n }\n}\n\n/**\n * Bone rotation helper utilities\n * @korean 뼈회전헬퍼\n */\nexport class BoneRotationHelpers {\n /**\n * Create shoulder rotation for arm extension\n * @param side - \"L\" or \"R\"\n * @param forward - Forward rotation amount\n * @param up - Upward rotation amount\n * @returns Euler rotation\n */\n static shoulderExtension(\n side: \"L\" | \"R\",\n forward: number,\n up: number = 0,\n ): THREE.Euler {\n const sign = side === \"L\" ? -1 : 1;\n return new THREE.Euler(up, 0, forward * sign, \"XYZ\");\n }\n\n /**\n * Create elbow rotation for arm bend\n * @param side - \"L\" or \"R\"\n * @param bend - Bend amount (0 = straight, PI/2 = 90 degrees)\n * @returns Euler rotation\n */\n static elbowBend(side: \"L\" | \"R\", bend: number): THREE.Euler {\n const sign = side === \"L\" ? -1 : 1;\n return new THREE.Euler(0, 0, bend * sign, \"XYZ\");\n }\n\n /**\n * Create hip rotation for leg movement\n * @param _side - \"L\" or \"R\" (reserved for future asymmetric animations)\n * @param forward - Forward rotation (positive = leg forward)\n * @param outward - Outward rotation (positive = leg out)\n * @returns Euler rotation\n */\n static hipRotation(\n _side: \"L\" | \"R\",\n forward: number,\n outward: number = 0,\n ): THREE.Euler {\n return new THREE.Euler(forward, outward, 0, \"XYZ\");\n }\n\n /**\n * Create knee rotation for leg bend\n * @param _side - \"L\" or \"R\" (reserved for future asymmetric animations)\n * @param bend - Bend amount (positive = knee bends)\n * @returns Euler rotation\n */\n static kneeBend(_side: \"L\" | \"R\", bend: number): THREE.Euler {\n // NOTE: `_side` is intentionally unused: knee bends are currently symmetric for both legs.\n // The parameter is kept to preserve API compatibility for future asymmetric leg animations.\n void _side;\n // Knee flexion is on X axis (legs extend along -Y, X rotation swings shin forward/backward)\n // Negative X = flexion (bend), so negate the positive 'bend' input\n return new THREE.Euler(-bend, 0, 0, \"XYZ\");\n }\n}\n\n/**\n * Reusable animation presets for common combat patterns\n * @korean 재사용애니메이션프리셋\n */\nexport class AnimationPresets {\n /**\n * Standard fighting guard position - protects face and body\n * Both elbows tight, hands at chin/temple level\n * @korean 기본방어자세\n */\n static readonly FIGHTING_GUARD = {\n leftArm: {\n shoulder: new THREE.Euler(-0.35, -0.35, 0.14), // Arms forward, hands near chin\n elbow: new THREE.Euler(0, 0, -1.75), // ~100° bend (중단막기)\n wrist: new THREE.Euler(0.1, 0, 0), // Fist aligned\n },\n rightArm: {\n shoulder: new THREE.Euler(-0.35, 0.35, -0.14), // Mirror\n elbow: new THREE.Euler(0, 0, 1.75), // ~100° bend (중단막기)\n wrist: new THREE.Euler(0.1, 0, 0), // Fist aligned\n },\n } as const;\n\n /**\n * High guard protecting head - both hands at temple level\n * Used during kicks or when expecting high attacks\n * @korean 상단방어자세\n */\n static readonly HIGH_GUARD = {\n leftArm: {\n shoulder: new THREE.Euler(-0.52, -0.44, 0.17), // Raised, arms forward (상단막기)\n elbow: new THREE.Euler(0, 0, -2.09), // ~120° bend tight guard\n wrist: new THREE.Euler(0.1, 0, 0), // Hand near temple\n },\n rightArm: {\n shoulder: new THREE.Euler(-0.52, 0.44, -0.17), // Mirror\n elbow: new THREE.Euler(0, 0, 2.09), // ~120° bend tight guard\n wrist: new THREE.Euler(0.1, 0, 0), // Hand near temple\n },\n } as const;\n\n /**\n * Kick chamber position - leg lifted, hip rotated\n * Shared starting position for most kicks\n * @korean 킥체임버자세\n */\n static readonly KICK_CHAMBER_RIGHT = {\n hip: new THREE.Euler(1.57, 0, 0), // 90° hip flexion\n knee: new THREE.Euler(-2.0, 0, 0), // Tight chamber\n ankle: new THREE.Euler(0, 0, 0), // Relaxed\n supportKnee: new THREE.Euler(-0.25, 0, 0), // Slight bend for balance\n pelvis: new THREE.Euler(-0.1, 0, 0), // Slight backward tilt\n } as const;\n\n /**\n * Kick extension position - leg fully extended\n * @korean 킥확장자세\n */\n static readonly KICK_EXTENSION_RIGHT = {\n hip: new THREE.Euler(1.7, 0, 0), // Hip drives forward\n knee: new THREE.Euler(0.1, 0, 0), // Full extension\n ankle: new THREE.Euler(0.5, 0, 0), // Dorsiflexion for ball strike\n supportKnee: new THREE.Euler(-0.35, 0, 0), // Deeper bend for balance\n pelvis: new THREE.Euler(0.15, 0, 0), // Forward drive\n } as const;\n\n /**\n * Punch wind-up - arm coiled, torso rotated back\n * @korean 펀치준비자세\n */\n static readonly PUNCH_WINDUP_RIGHT = {\n shoulder: new THREE.Euler(0.3, 0, -0.3),\n elbow: new THREE.Euler(0, 0, 1.8),\n spine: new THREE.Euler(0, -0.15, 0),\n pelvis: new THREE.Euler(0, -0.1, 0),\n } as const;\n\n /**\n * Punch extension - arm extended with torso rotation\n * @korean 펀치확장자세\n */\n static readonly PUNCH_EXTENSION_RIGHT = {\n shoulder: new THREE.Euler(-0.7, 0, 0.5),\n elbow: new THREE.Euler(0, 0, 0.05),\n spine: new THREE.Euler(0, 0.35, 0),\n pelvis: new THREE.Euler(0, 0.2, 0),\n } as const;\n}\n\n/**\n * Applies common animation patterns to KeyframeBuilder\n * Allows reusing shared motion patterns across animations\n * @korean 애니메이션패턴헬퍼\n */\nexport class AnimationPatternHelpers {\n /**\n * Apply fighting guard to a keyframe\n * Keeps hands protecting face during kicks\n * @param kf - KeyframeBuilder to modify\n * @returns Modified KeyframeBuilder\n */\n static applyFightingGuard(kf: KeyframeBuilder): KeyframeBuilder {\n const guard = AnimationPresets.FIGHTING_GUARD;\n return kf\n .rotate(\n BoneName.SHOULDER_L,\n guard.leftArm.shoulder.x,\n guard.leftArm.shoulder.y,\n guard.leftArm.shoulder.z,\n )\n .rotate(\n BoneName.ELBOW_L,\n guard.leftArm.elbow.x,\n guard.leftArm.elbow.y,\n guard.leftArm.elbow.z,\n )\n .rotate(\n BoneName.SHOULDER_R,\n guard.rightArm.shoulder.x,\n guard.rightArm.shoulder.y,\n guard.rightArm.shoulder.z,\n )\n .rotate(\n BoneName.ELBOW_R,\n guard.rightArm.elbow.x,\n guard.rightArm.elbow.y,\n guard.rightArm.elbow.z,\n );\n }\n\n /**\n * Apply high guard during kicks\n * @param kf - KeyframeBuilder to modify\n * @returns Modified KeyframeBuilder\n */\n static applyHighGuard(kf: KeyframeBuilder): KeyframeBuilder {\n const guard = AnimationPresets.HIGH_GUARD;\n return kf\n .rotate(\n BoneName.SHOULDER_L,\n guard.leftArm.shoulder.x,\n guard.leftArm.shoulder.y,\n guard.leftArm.shoulder.z,\n )\n .rotate(\n BoneName.ELBOW_L,\n guard.leftArm.elbow.x,\n guard.leftArm.elbow.y,\n guard.leftArm.elbow.z,\n )\n .rotate(\n BoneName.SHOULDER_R,\n guard.rightArm.shoulder.x,\n guard.rightArm.shoulder.y,\n guard.rightArm.shoulder.z,\n )\n .rotate(\n BoneName.ELBOW_R,\n guard.rightArm.elbow.x,\n guard.rightArm.elbow.y,\n guard.rightArm.elbow.z,\n );\n }\n\n /**\n * Apply kick chamber for right leg\n * @param kf - KeyframeBuilder to modify\n * @returns Modified KeyframeBuilder\n */\n static applyKickChamber(kf: KeyframeBuilder): KeyframeBuilder {\n const chamber = AnimationPresets.KICK_CHAMBER_RIGHT;\n return kf\n .rotate(BoneName.HIP_R, chamber.hip.x, chamber.hip.y, chamber.hip.z)\n .rotate(BoneName.KNEE_R, chamber.knee.x, chamber.knee.y, chamber.knee.z)\n .rotate(\n BoneName.KNEE_L,\n chamber.supportKnee.x,\n chamber.supportKnee.y,\n chamber.supportKnee.z,\n )\n .rotate(\n BoneName.PELVIS,\n chamber.pelvis.x,\n chamber.pelvis.y,\n chamber.pelvis.z,\n );\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AnimationBuilder.js","names":[],"sources":["../../../../src/systems/animation/builders/AnimationBuilder.ts"],"sourcesContent":["/**\n * Animation Builder - Fluent API for creating skeletal animations\n *\n * Provides a cleaner, more maintainable way to define animations with\n * reduced boilerplate and better readability.\n *\n * @module systems/animation/AnimationBuilder\n * @category Animation System\n * @korean 애니메이션빌더\n */\n\nimport * as THREE from \"three\";\nimport type { AnimationKeyframe, SkeletalAnimation } from \"@/types/skeletal\";\nimport { BoneName } from \"@/types/skeletal\";\n\n/**\n * Keyframe builder for fluent keyframe construction\n * @korean 키프레임빌더\n */\nclass KeyframeBuilder {\n private time: number;\n private easing: string;\n private boneRotations: Map<BoneName, THREE.Euler>;\n private bonePositions: Map<BoneName, THREE.Vector3>;\n private parentBuilder: AnimationBuilder | null = null;\n\n constructor(time: number, easing: string = \"linear\") {\n this.time = time;\n this.easing = easing;\n this.boneRotations = new Map();\n this.bonePositions = new Map();\n }\n\n /**\n * Set parent animation builder (for chaining)\n * @internal\n */\n setParent(parent: AnimationBuilder): this {\n this.parentBuilder = parent;\n return this;\n }\n\n /**\n * Add bone rotation to keyframe\n * @param bone - Bone to rotate\n * @param x - X rotation in radians\n * @param y - Y rotation in radians\n * @param z - Z rotation in radians\n * @param order - Rotation order (default: XYZ)\n * @returns This builder for chaining\n */\n rotate(\n bone: BoneName,\n x: number,\n y: number,\n z: number,\n order: THREE.EulerOrder = \"XYZ\",\n ): this {\n this.boneRotations.set(bone, new THREE.Euler(x, y, z, order));\n return this;\n }\n\n /**\n * Add bone position to keyframe\n * @param bone - Bone to position\n * @param x - X position\n * @param y - Y position\n * @param z - Z position\n * @returns This builder for chaining\n */\n position(bone: BoneName, x: number, y: number, z: number): this {\n this.bonePositions.set(bone, new THREE.Vector3(x, y, z));\n return this;\n }\n\n /**\n * Build the keyframe and return to animation builder\n * @returns Animation builder for chaining\n */\n build(): AnimationBuilder {\n const keyframe: AnimationKeyframe = {\n time: this.time,\n easing: this.easing as \"linear\" | \"ease-in\" | \"ease-out\" | \"ease-in-out\",\n boneRotations: this.boneRotations,\n bonePositions: this.bonePositions,\n };\n\n if (this.parentBuilder) {\n this.parentBuilder.addKeyframe(keyframe);\n return this.parentBuilder;\n }\n\n // This indicates incorrect usage of the builder API; parent must be set via setParent(...)\n throw new Error(\n \"KeyframeBuilder.build() called without a parent AnimationBuilder. Ensure setParent(...) is called before build().\",\n );\n }\n}\n\n/**\n * Animation builder for fluent animation construction\n * @korean 애니메이션빌더\n */\nexport class AnimationBuilder {\n private animationName: string;\n private koreanName: string;\n private duration: number;\n private loop: boolean;\n private type: \"attack\" | \"defense\" | \"movement\" | \"idle\";\n private keyframes: AnimationKeyframe[];\n\n private constructor(name: string) {\n this.animationName = name;\n this.koreanName = name;\n this.duration = 1.0;\n this.loop = false;\n this.type = \"idle\";\n this.keyframes = [];\n }\n\n /**\n * Create a new animation builder\n * @param name - Animation name\n * @returns New animation builder\n */\n static create(name: string): AnimationBuilder {\n return new AnimationBuilder(name);\n }\n\n /**\n * Set Korean name for animation\n * @param name - Korean name\n * @returns This builder for chaining\n */\n withKoreanName(name: string): this {\n this.koreanName = name;\n return this;\n }\n\n /**\n * Set animation duration\n * @param seconds - Duration in seconds\n * @returns This builder for chaining\n */\n withDuration(seconds: number): this {\n this.duration = seconds;\n return this;\n }\n\n /**\n * Set animation loop behavior\n * @param shouldLoop - Whether animation should loop\n * @returns This builder for chaining\n */\n withLoop(shouldLoop: boolean): this {\n this.loop = shouldLoop;\n return this;\n }\n\n /**\n * Set animation type\n * @param animType - Animation type\n * @returns This builder for chaining\n */\n withType(animType: \"attack\" | \"defense\" | \"movement\" | \"idle\"): this {\n this.type = animType;\n return this;\n }\n\n /**\n * Add a keyframe to the animation\n * @param time - Time of keyframe in seconds\n * @param easing - Easing function name\n * @returns Keyframe builder for defining keyframe contents\n */\n keyframe(time: number, easing: string = \"linear\"): KeyframeBuilder {\n const kfBuilder = new KeyframeBuilder(time, easing);\n kfBuilder.setParent(this);\n return kfBuilder;\n }\n\n /**\n * Add a pre-built keyframe directly\n * @param keyframe - Complete keyframe\n * @returns This builder for chaining\n */\n addKeyframe(keyframe: AnimationKeyframe): this {\n this.keyframes.push(keyframe);\n return this;\n }\n\n /**\n * Build the complete animation\n * @returns Complete skeletal animation\n */\n build(): SkeletalAnimation {\n return {\n name: this.animationName,\n koreanName: this.koreanName,\n duration: this.duration,\n loop: this.loop,\n type: this.type,\n keyframes: this.keyframes,\n };\n }\n}\n\n/**\n * Common keyframe factories for reusable animation patterns\n * @korean 키프레임팩토리\n */\nexport class KeyframeFactories {\n /**\n * Create a guard return keyframe (return to defensive position)\n * @param time - Time of keyframe\n * @returns Guard position keyframe\n */\n static guardReturn(time: number): AnimationKeyframe {\n return {\n time,\n easing: \"ease-in\",\n boneRotations: new Map([\n [BoneName.SHOULDER_R, new THREE.Euler(-0.35, 0.35, -0.14, \"XYZ\")],\n [BoneName.SHOULDER_L, new THREE.Euler(-0.35, -0.35, 0.14, \"XYZ\")],\n [BoneName.ELBOW_R, new THREE.Euler(0, 0, 1.75, \"XYZ\")],\n [BoneName.ELBOW_L, new THREE.Euler(0, 0, -1.75, \"XYZ\")],\n [BoneName.SPINE_UPPER, new THREE.Euler(0, 0, 0, \"XYZ\")],\n [BoneName.SPINE_MIDDLE, new THREE.Euler(0, 0, 0, \"XYZ\")],\n [BoneName.PELVIS, new THREE.Euler(0, 0, 0, \"XYZ\")],\n ]),\n bonePositions: new Map([\n [BoneName.HAND_R, new THREE.Vector3(0, 0, 0)],\n [BoneName.HAND_L, new THREE.Vector3(0, 0, 0)],\n ]),\n };\n }\n\n /**\n * Create a neutral stance keyframe\n * @param time - Time of keyframe\n * @returns Neutral stance keyframe\n */\n static neutralStance(time: number): AnimationKeyframe {\n return {\n time,\n easing: \"linear\",\n boneRotations: new Map([\n [BoneName.SPINE_UPPER, new THREE.Euler(0, 0, 0, \"XYZ\")],\n [BoneName.SPINE_MIDDLE, new THREE.Euler(0, 0, 0, \"XYZ\")],\n [BoneName.PELVIS, new THREE.Euler(0, 0, 0, \"XYZ\")],\n [BoneName.HIP_L, new THREE.Euler(0, 0, 0, \"XYZ\")],\n [BoneName.HIP_R, new THREE.Euler(0, 0, 0, \"XYZ\")],\n ]),\n bonePositions: new Map(),\n };\n }\n\n /**\n * Create a torso rotation keyframe\n * @param time - Time of keyframe\n * @param angle - Rotation angle in radians (positive = clockwise)\n * @param easing - Easing function\n * @returns Torso rotation keyframe\n */\n static rotateTorso(\n time: number,\n angle: number,\n easing: \"linear\" | \"ease-in\" | \"ease-out\" | \"ease-in-out\" = \"linear\",\n ): AnimationKeyframe {\n return {\n time,\n easing,\n boneRotations: new Map([\n [BoneName.SPINE_UPPER, new THREE.Euler(0, angle, 0, \"XYZ\")],\n [BoneName.SPINE_MIDDLE, new THREE.Euler(0, angle * 0.75, 0, \"XYZ\")],\n [BoneName.PELVIS, new THREE.Euler(0, angle * 0.5, 0, \"XYZ\")],\n ]),\n bonePositions: new Map(),\n };\n }\n}\n\n/**\n * Bone rotation helper utilities\n * @korean 뼈회전헬퍼\n */\nexport class BoneRotationHelpers {\n /**\n * Create shoulder rotation for arm extension\n * @param side - \"L\" or \"R\"\n * @param forward - Forward rotation amount\n * @param up - Upward rotation amount\n * @returns Euler rotation\n */\n static shoulderExtension(\n side: \"L\" | \"R\",\n forward: number,\n up: number = 0,\n ): THREE.Euler {\n const sign = side === \"L\" ? -1 : 1;\n return new THREE.Euler(up, 0, forward * sign, \"XYZ\");\n }\n\n /**\n * Create elbow rotation for arm bend\n * @param side - \"L\" or \"R\"\n * @param bend - Bend amount (0 = straight, PI/2 = 90 degrees)\n * @returns Euler rotation\n */\n static elbowBend(side: \"L\" | \"R\", bend: number): THREE.Euler {\n const sign = side === \"L\" ? -1 : 1;\n return new THREE.Euler(0, 0, bend * sign, \"XYZ\");\n }\n\n /**\n * Create hip rotation for leg movement\n * @param _side - \"L\" or \"R\" (reserved for future asymmetric animations)\n * @param forward - Forward rotation (positive = leg forward)\n * @param outward - Outward rotation (positive = leg out)\n * @returns Euler rotation\n */\n static hipRotation(\n _side: \"L\" | \"R\",\n forward: number,\n outward: number = 0,\n ): THREE.Euler {\n return new THREE.Euler(forward, outward, 0, \"XYZ\");\n }\n\n /**\n * Create knee rotation for leg bend\n * @param _side - \"L\" or \"R\" (reserved for future asymmetric animations)\n * @param bend - Bend amount (positive = knee bends)\n * @returns Euler rotation\n */\n static kneeBend(_side: \"L\" | \"R\", bend: number): THREE.Euler {\n // NOTE: `_side` is intentionally unused: knee bends are currently symmetric for both legs.\n // The parameter is kept to preserve API compatibility for future asymmetric leg animations.\n void _side;\n // Knee flexion is on X axis (legs extend along -Y, X rotation swings shin forward/backward)\n // Negative X = flexion (bend), so negate the positive 'bend' input\n return new THREE.Euler(-bend, 0, 0, \"XYZ\");\n }\n}\n\n/**\n * Reusable animation presets for common combat patterns\n * @korean 재사용애니메이션프리셋\n */\nexport class AnimationPresets {\n /**\n * Standard fighting guard position - protects face and body\n * Both elbows tight, hands at chin/temple level\n * @korean 기본방어자세\n */\n static readonly FIGHTING_GUARD = {\n leftArm: {\n shoulder: new THREE.Euler(-0.35, -0.35, 0.14), // Arms forward, hands near chin\n elbow: new THREE.Euler(0, 0, -1.75), // ~100° bend (중단막기)\n wrist: new THREE.Euler(0.1, 0, 0), // Fist aligned\n },\n rightArm: {\n shoulder: new THREE.Euler(-0.35, 0.35, -0.14), // Mirror\n elbow: new THREE.Euler(0, 0, 1.75), // ~100° bend (중단막기)\n wrist: new THREE.Euler(0.1, 0, 0), // Fist aligned\n },\n } as const;\n\n /**\n * High guard protecting head - both hands at temple level\n * Used during kicks or when expecting high attacks\n * @korean 상단방어자세\n */\n static readonly HIGH_GUARD = {\n leftArm: {\n shoulder: new THREE.Euler(-0.52, -0.44, 0.17), // Raised, arms forward (상단막기)\n elbow: new THREE.Euler(0, 0, -2.09), // ~120° bend tight guard\n wrist: new THREE.Euler(0.1, 0, 0), // Hand near temple\n },\n rightArm: {\n shoulder: new THREE.Euler(-0.52, 0.44, -0.17), // Mirror\n elbow: new THREE.Euler(0, 0, 2.09), // ~120° bend tight guard\n wrist: new THREE.Euler(0.1, 0, 0), // Hand near temple\n },\n } as const;\n\n /**\n * Kick chamber position - leg lifted, hip rotated\n * Shared starting position for most kicks\n * @korean 킥체임버자세\n */\n static readonly KICK_CHAMBER_RIGHT = {\n hip: new THREE.Euler(1.57, 0, 0), // 90° hip flexion\n knee: new THREE.Euler(-2.0, 0, 0), // Tight chamber\n ankle: new THREE.Euler(0, 0, 0), // Relaxed\n supportKnee: new THREE.Euler(-0.25, 0, 0), // Slight bend for balance\n pelvis: new THREE.Euler(-0.1, 0, 0), // Slight backward tilt\n } as const;\n\n /**\n * Kick extension position - leg fully extended\n * @korean 킥확장자세\n */\n static readonly KICK_EXTENSION_RIGHT = {\n hip: new THREE.Euler(1.7, 0, 0), // Hip drives forward\n knee: new THREE.Euler(0.1, 0, 0), // Full extension\n ankle: new THREE.Euler(0.5, 0, 0), // Dorsiflexion for ball strike\n supportKnee: new THREE.Euler(-0.35, 0, 0), // Deeper bend for balance\n pelvis: new THREE.Euler(0.15, 0, 0), // Forward drive\n } as const;\n\n /**\n * Punch wind-up - arm coiled, torso rotated back\n * @korean 펀치준비자세\n */\n static readonly PUNCH_WINDUP_RIGHT = {\n shoulder: new THREE.Euler(0.3, 0, -0.3),\n elbow: new THREE.Euler(0, 0, 1.8),\n spine: new THREE.Euler(0, -0.15, 0),\n pelvis: new THREE.Euler(0, -0.1, 0),\n } as const;\n\n /**\n * Punch extension - arm extended with torso rotation\n * @korean 펀치확장자세\n */\n static readonly PUNCH_EXTENSION_RIGHT = {\n shoulder: new THREE.Euler(-0.7, 0, 0.5),\n elbow: new THREE.Euler(0, 0, 0.05),\n spine: new THREE.Euler(0, 0.35, 0),\n pelvis: new THREE.Euler(0, 0.2, 0),\n } as const;\n}\n\n/**\n * Applies common animation patterns to KeyframeBuilder\n * Allows reusing shared motion patterns across animations\n * @korean 애니메이션패턴헬퍼\n */\nexport class AnimationPatternHelpers {\n /**\n * Apply fighting guard to a keyframe\n * Keeps hands protecting face during kicks\n * @param kf - KeyframeBuilder to modify\n * @returns Modified KeyframeBuilder\n */\n static applyFightingGuard(kf: KeyframeBuilder): KeyframeBuilder {\n const guard = AnimationPresets.FIGHTING_GUARD;\n return kf\n .rotate(\n BoneName.SHOULDER_L,\n guard.leftArm.shoulder.x,\n guard.leftArm.shoulder.y,\n guard.leftArm.shoulder.z,\n )\n .rotate(\n BoneName.ELBOW_L,\n guard.leftArm.elbow.x,\n guard.leftArm.elbow.y,\n guard.leftArm.elbow.z,\n )\n .rotate(\n BoneName.SHOULDER_R,\n guard.rightArm.shoulder.x,\n guard.rightArm.shoulder.y,\n guard.rightArm.shoulder.z,\n )\n .rotate(\n BoneName.ELBOW_R,\n guard.rightArm.elbow.x,\n guard.rightArm.elbow.y,\n guard.rightArm.elbow.z,\n );\n }\n\n /**\n * Apply high guard during kicks\n * @param kf - KeyframeBuilder to modify\n * @returns Modified KeyframeBuilder\n */\n static applyHighGuard(kf: KeyframeBuilder): KeyframeBuilder {\n const guard = AnimationPresets.HIGH_GUARD;\n return kf\n .rotate(\n BoneName.SHOULDER_L,\n guard.leftArm.shoulder.x,\n guard.leftArm.shoulder.y,\n guard.leftArm.shoulder.z,\n )\n .rotate(\n BoneName.ELBOW_L,\n guard.leftArm.elbow.x,\n guard.leftArm.elbow.y,\n guard.leftArm.elbow.z,\n )\n .rotate(\n BoneName.SHOULDER_R,\n guard.rightArm.shoulder.x,\n guard.rightArm.shoulder.y,\n guard.rightArm.shoulder.z,\n )\n .rotate(\n BoneName.ELBOW_R,\n guard.rightArm.elbow.x,\n guard.rightArm.elbow.y,\n guard.rightArm.elbow.z,\n );\n }\n\n /**\n * Apply kick chamber for right leg\n * @param kf - KeyframeBuilder to modify\n * @returns Modified KeyframeBuilder\n */\n static applyKickChamber(kf: KeyframeBuilder): KeyframeBuilder {\n const chamber = AnimationPresets.KICK_CHAMBER_RIGHT;\n return kf\n .rotate(BoneName.HIP_R, chamber.hip.x, chamber.hip.y, chamber.hip.z)\n .rotate(BoneName.KNEE_R, chamber.knee.x, chamber.knee.y, chamber.knee.z)\n .rotate(\n BoneName.KNEE_L,\n chamber.supportKnee.x,\n chamber.supportKnee.y,\n chamber.supportKnee.z,\n )\n .rotate(\n BoneName.PELVIS,\n chamber.pelvis.x,\n chamber.pelvis.y,\n chamber.pelvis.z,\n );\n }\n}\n"],"mappings":"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"}
|
|
@@ -57,10 +57,14 @@ function applyPunchPhaseToConfig(kf, phase, hand = "right", options = {}) {
|
|
|
57
57
|
if (includeSpineMiddle) kf.rotate(BoneName.SPINE_MIDDLE, 0, phase.spineY * .7 * spineFlip, 0);
|
|
58
58
|
}
|
|
59
59
|
if (phase.pelvisY !== void 0) kf.rotate(BoneName.PELVIS, 0, phase.pelvisY * spineFlip, 0);
|
|
60
|
-
if (handPose)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
if (handPose) {
|
|
61
|
+
if (hand === "right") kf.setRightHandPose(handPose, handHighlightMode);
|
|
62
|
+
else kf.setLeftHandPose(handPose, handHighlightMode);
|
|
63
|
+
}
|
|
64
|
+
if (oppositeHandPose) {
|
|
65
|
+
if (hand === "right") kf.setLeftHandPose(oppositeHandPose);
|
|
66
|
+
else kf.setRightHandPose(oppositeHandPose);
|
|
67
|
+
}
|
|
64
68
|
}
|
|
65
69
|
//#endregion
|
|
66
70
|
export { applyPunchPhaseToConfig };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PunchPhaseApplicator.js","names":[],"sources":["../../../../src/systems/animation/builders/PunchPhaseApplicator.ts"],"sourcesContent":["/**\n * Punch Phase Application Utilities\n *\n * Utilities for applying punch phase poses to keyframes with integrated\n * anatomy awareness (hand poses and highlighting for strikes).\n * 주먹 단계 적용 유틸리티 (해부학 통합)\n *\n * @module systems/animation/PunchPhaseApplicator\n * @korean 주먹단계적용기\n */\n\nimport { BoneName } from \"@/types/skeletal\";\nimport type { HandHighlightMode, KeyframeConfig } from \"./KeyframeConfig\";\nimport { PUNCH_PHASES } from \"./MartialArtsConstants\";\n\n/**\n * Interface for punch phases with Korean martial arts biomechanics\n * Includes opposite arm hikite (당기기) for power generation\n */\ninterface PunchPhase {\n readonly shoulder: readonly [number, number, number];\n readonly elbow: readonly [number, number, number];\n readonly wrist?: readonly [number, number, number];\n readonly spineY?: number;\n readonly pelvisY?: number;\n // Opposite arm for hikite (pulling hand) - 당기기\n readonly oppositeShoulder?: readonly [number, number, number];\n readonly oppositeElbow?: readonly [number, number, number];\n readonly oppositeWrist?: readonly [number, number, number];\n}\n\n/** Phase name keys */\nexport type PunchPhaseName = keyof typeof PUNCH_PHASES;\n\n/** Punch hand side */\nexport type PunchSide = \"left\" | \"right\";\n\n/**\n * Apply punch phase to a KeyframeConfig with Korean martial arts biomechanics\n * and anatomy integration (hand poses, highlighting).\n *\n * Handles common punch phase bones: shoulder, elbow, wrist, spine, pelvis\n * Now includes opposite arm hikite (당기기) and automatic hand pose/highlight\n *\n * @param kf - KeyframeConfig to apply phase to\n * @param phase - Punch phase data from PUNCH_PHASES\n * @param hand - Which hand is punching (\"left\" | \"right\")\n * @param options - Configuration including anatomy options\n *\n * @example\n * ```typescript\n * // Apply extension with automatic fist pose and knuckle highlight\n * applyPunchPhaseToConfig(kf, PUNCH_PHASES.EXTENSION, \"right\", {\n * handPose: \"fist\",\n * handHighlightMode: \"knuckles\",\n * includeOppositeArm: true\n * });\n * ```\n *\n * @korean KeyframeConfig에주먹단계적용\n */\nexport function applyPunchPhaseToConfig(\n kf: KeyframeConfig,\n phase: PunchPhase,\n hand: PunchSide = \"right\",\n options: {\n readonly includeWrist?: boolean;\n readonly includeSpineMiddle?: boolean;\n readonly includeOppositeArm?: boolean;\n // Anatomy integration\n readonly handPose?: string;\n readonly handHighlightMode?: HandHighlightMode;\n readonly oppositeHandPose?: string;\n } = {},\n): void {\n const {\n includeWrist = false,\n includeSpineMiddle = false,\n includeOppositeArm = true,\n handPose,\n handHighlightMode,\n oppositeHandPose,\n } = options;\n\n // Select bones based on punching hand\n const shoulderBone =\n hand === \"right\" ? BoneName.SHOULDER_R : BoneName.SHOULDER_L;\n const elbowBone = hand === \"right\" ? BoneName.ELBOW_R : BoneName.ELBOW_L;\n const wristBone = hand === \"right\" ? BoneName.WRIST_R : BoneName.WRIST_L;\n\n // Opposite hand bones for hikite (당기기)\n const oppositeShoulderBone =\n hand === \"right\" ? BoneName.SHOULDER_L : BoneName.SHOULDER_R;\n const oppositeElbowBone =\n hand === \"right\" ? BoneName.ELBOW_L : BoneName.ELBOW_R;\n const oppositeWristBone =\n hand === \"right\" ? BoneName.WRIST_L : BoneName.WRIST_R;\n\n // ═══════════════════════════════════════════════════════════════════════════\n // ARM MIRRORING (팔 좌우 반전)\n // ═══════════════════════════════════════════════════════════════════════════\n //\n // PUNCH_PHASES arm values use LEFT-arm convention:\n // Left arm: negative Z = flexion (toward body), negative Z shoulder = pulled back\n // Right arm: positive Z = flexion (toward body), positive Z shoulder = pulled back\n //\n // PUNCH_PHASES opposite arm values use RIGHT-arm convention.\n //\n // When applying to the opposite side, Y and Z must be negated to mirror correctly.\n // The X axis (elevation/flexion) stays the same for both sides.\n //\n // 한국 무술 생체역학: 좌우 대칭 반전으로 정확한 자세 구현\n const armMirror = hand === \"right\" ? -1 : 1;\n\n // Apply punching arm shoulder rotation (with mirroring)\n if (phase.shoulder) {\n kf.rotate(\n shoulderBone,\n phase.shoulder[0],\n phase.shoulder[1] * armMirror,\n phase.shoulder[2] * armMirror,\n );\n }\n\n // Apply punching arm elbow rotation (with mirroring)\n if (phase.elbow) {\n kf.rotate(\n elbowBone,\n phase.elbow[0],\n phase.elbow[1],\n phase.elbow[2] * armMirror,\n );\n }\n\n // Optional punching arm wrist rotation (with mirroring)\n if (includeWrist && phase.wrist) {\n kf.rotate(\n wristBone,\n phase.wrist[0],\n phase.wrist[1],\n phase.wrist[2] * armMirror,\n );\n }\n\n // Apply opposite arm hikite (당기기 - pulling hand for power generation)\n // Opposite arm values are in RIGHT convention; mirror when applying to LEFT arm\n if (includeOppositeArm) {\n if (phase.oppositeShoulder) {\n kf.rotate(\n oppositeShoulderBone,\n phase.oppositeShoulder[0],\n phase.oppositeShoulder[1] * armMirror,\n phase.oppositeShoulder[2] * armMirror,\n );\n }\n\n if (phase.oppositeElbow) {\n kf.rotate(\n oppositeElbowBone,\n phase.oppositeElbow[0],\n phase.oppositeElbow[1],\n phase.oppositeElbow[2] * armMirror,\n );\n }\n\n if (includeWrist && phase.oppositeWrist) {\n kf.rotate(\n oppositeWristBone,\n phase.oppositeWrist[0],\n phase.oppositeWrist[1],\n phase.oppositeWrist[2] * armMirror,\n );\n }\n }\n\n // ═══════════════════════════════════════════════════════════════════════════\n // SPINE/PELVIS Y-AXIS MIRRORING (척추/골반 회전 반전)\n // ═══════════════════════════════════════════════════════════════════════════\n //\n // PUNCH_PHASES spine/pelvis Y values use RIGHT-hand convention:\n // Positive Y = counter-clockwise from above = drives RIGHT shoulder forward\n // For LEFT-hand punches, Y must be negated to drive LEFT shoulder forward\n //\n // 한국 무술: 허리비틀기 (Hip rotation) 좌우 반전\n const spineFlip = hand === \"right\" ? 1 : -1;\n\n if (phase.spineY !== undefined) {\n kf.rotate(BoneName.SPINE_UPPER, 0, phase.spineY * spineFlip, 0);\n if (includeSpineMiddle) {\n kf.rotate(BoneName.SPINE_MIDDLE, 0, phase.spineY * 0.7 * spineFlip, 0);\n }\n }\n if (phase.pelvisY !== undefined) {\n kf.rotate(BoneName.PELVIS, 0, phase.pelvisY * spineFlip, 0);\n }\n\n // ═══════════════════════════════════════════════════════════════════════════\n // ANATOMY INTEGRATION (해부학 통합)\n // ═══════════════════════════════════════════════════════════════════════════\n\n // Set punching hand pose and highlight\n if (handPose) {\n if (hand === \"right\") {\n kf.setRightHandPose(handPose, handHighlightMode);\n } else {\n kf.setLeftHandPose(handPose, handHighlightMode);\n }\n }\n\n // Set opposite hand pose (guard position)\n if (oppositeHandPose) {\n if (hand === \"right\") {\n kf.setLeftHandPose(oppositeHandPose);\n } else {\n kf.setRightHandPose(oppositeHandPose);\n }\n }\n}\n\n/**\n * Get a punch phase by name\n *\n * @param phaseName - Name of the phase from PUNCH_PHASES\n * @returns The punch phase data\n *\n * @korean 주먹단계가져오기\n */\nexport function getPunchPhase(phaseName: PunchPhaseName): PunchPhase {\n return PUNCH_PHASES[phaseName];\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6DA,SAAgB,wBACd,IACA,OACA,OAAkB,SAClB,UAQI,CAAC,GACC;CACN,MAAM,EACJ,eAAe,OACf,qBAAqB,OACrB,qBAAqB,MACrB,UACA,mBACA,qBACE;CAGJ,MAAM,eACJ,SAAS,UAAU,SAAS,aAAa,SAAS;CACpD,MAAM,YAAY,SAAS,UAAU,SAAS,UAAU,SAAS;CACjE,MAAM,YAAY,SAAS,UAAU,SAAS,UAAU,SAAS;CAGjE,MAAM,uBACJ,SAAS,UAAU,SAAS,aAAa,SAAS;CACpD,MAAM,oBACJ,SAAS,UAAU,SAAS,UAAU,SAAS;CACjD,MAAM,oBACJ,SAAS,UAAU,SAAS,UAAU,SAAS;CAgBjD,MAAM,YAAY,SAAS,UAAU,KAAK;CAG1C,IAAI,MAAM,UACR,GAAG,OACD,cACA,MAAM,SAAS,IACf,MAAM,SAAS,KAAK,WACpB,MAAM,SAAS,KAAK,SACtB;CAIF,IAAI,MAAM,OACR,GAAG,OACD,WACA,MAAM,MAAM,IACZ,MAAM,MAAM,IACZ,MAAM,MAAM,KAAK,SACnB;CAIF,IAAI,gBAAgB,MAAM,OACxB,GAAG,OACD,WACA,MAAM,MAAM,IACZ,MAAM,MAAM,IACZ,MAAM,MAAM,KAAK,SACnB;CAKF,IAAI,oBAAoB;EACtB,IAAI,MAAM,kBACR,GAAG,OACD,sBACA,MAAM,iBAAiB,IACvB,MAAM,iBAAiB,KAAK,WAC5B,MAAM,iBAAiB,KAAK,SAC9B;EAGF,IAAI,MAAM,eACR,GAAG,OACD,mBACA,MAAM,cAAc,IACpB,MAAM,cAAc,IACpB,MAAM,cAAc,KAAK,SAC3B;EAGF,IAAI,gBAAgB,MAAM,eACxB,GAAG,OACD,mBACA,MAAM,cAAc,IACpB,MAAM,cAAc,IACpB,MAAM,cAAc,KAAK,SAC3B;CAEJ;CAWA,MAAM,YAAY,SAAS,UAAU,IAAI;CAEzC,IAAI,MAAM,WAAW,KAAA,GAAW;EAC9B,GAAG,OAAO,SAAS,aAAa,GAAG,MAAM,SAAS,WAAW,CAAC;EAC9D,IAAI,oBACF,GAAG,OAAO,SAAS,cAAc,GAAG,MAAM,SAAS,KAAM,WAAW,CAAC;CAEzE;CACA,IAAI,MAAM,YAAY,KAAA,GACpB,GAAG,OAAO,SAAS,QAAQ,GAAG,MAAM,UAAU,WAAW,CAAC;CAQ5D,IAAI,
|
|
1
|
+
{"version":3,"file":"PunchPhaseApplicator.js","names":[],"sources":["../../../../src/systems/animation/builders/PunchPhaseApplicator.ts"],"sourcesContent":["/**\n * Punch Phase Application Utilities\n *\n * Utilities for applying punch phase poses to keyframes with integrated\n * anatomy awareness (hand poses and highlighting for strikes).\n * 주먹 단계 적용 유틸리티 (해부학 통합)\n *\n * @module systems/animation/PunchPhaseApplicator\n * @korean 주먹단계적용기\n */\n\nimport { BoneName } from \"@/types/skeletal\";\nimport type { HandHighlightMode, KeyframeConfig } from \"./KeyframeConfig\";\nimport { PUNCH_PHASES } from \"./MartialArtsConstants\";\n\n/**\n * Interface for punch phases with Korean martial arts biomechanics\n * Includes opposite arm hikite (당기기) for power generation\n */\ninterface PunchPhase {\n readonly shoulder: readonly [number, number, number];\n readonly elbow: readonly [number, number, number];\n readonly wrist?: readonly [number, number, number];\n readonly spineY?: number;\n readonly pelvisY?: number;\n // Opposite arm for hikite (pulling hand) - 당기기\n readonly oppositeShoulder?: readonly [number, number, number];\n readonly oppositeElbow?: readonly [number, number, number];\n readonly oppositeWrist?: readonly [number, number, number];\n}\n\n/** Phase name keys */\nexport type PunchPhaseName = keyof typeof PUNCH_PHASES;\n\n/** Punch hand side */\nexport type PunchSide = \"left\" | \"right\";\n\n/**\n * Apply punch phase to a KeyframeConfig with Korean martial arts biomechanics\n * and anatomy integration (hand poses, highlighting).\n *\n * Handles common punch phase bones: shoulder, elbow, wrist, spine, pelvis\n * Now includes opposite arm hikite (당기기) and automatic hand pose/highlight\n *\n * @param kf - KeyframeConfig to apply phase to\n * @param phase - Punch phase data from PUNCH_PHASES\n * @param hand - Which hand is punching (\"left\" | \"right\")\n * @param options - Configuration including anatomy options\n *\n * @example\n * ```typescript\n * // Apply extension with automatic fist pose and knuckle highlight\n * applyPunchPhaseToConfig(kf, PUNCH_PHASES.EXTENSION, \"right\", {\n * handPose: \"fist\",\n * handHighlightMode: \"knuckles\",\n * includeOppositeArm: true\n * });\n * ```\n *\n * @korean KeyframeConfig에주먹단계적용\n */\nexport function applyPunchPhaseToConfig(\n kf: KeyframeConfig,\n phase: PunchPhase,\n hand: PunchSide = \"right\",\n options: {\n readonly includeWrist?: boolean;\n readonly includeSpineMiddle?: boolean;\n readonly includeOppositeArm?: boolean;\n // Anatomy integration\n readonly handPose?: string;\n readonly handHighlightMode?: HandHighlightMode;\n readonly oppositeHandPose?: string;\n } = {},\n): void {\n const {\n includeWrist = false,\n includeSpineMiddle = false,\n includeOppositeArm = true,\n handPose,\n handHighlightMode,\n oppositeHandPose,\n } = options;\n\n // Select bones based on punching hand\n const shoulderBone =\n hand === \"right\" ? BoneName.SHOULDER_R : BoneName.SHOULDER_L;\n const elbowBone = hand === \"right\" ? BoneName.ELBOW_R : BoneName.ELBOW_L;\n const wristBone = hand === \"right\" ? BoneName.WRIST_R : BoneName.WRIST_L;\n\n // Opposite hand bones for hikite (당기기)\n const oppositeShoulderBone =\n hand === \"right\" ? BoneName.SHOULDER_L : BoneName.SHOULDER_R;\n const oppositeElbowBone =\n hand === \"right\" ? BoneName.ELBOW_L : BoneName.ELBOW_R;\n const oppositeWristBone =\n hand === \"right\" ? BoneName.WRIST_L : BoneName.WRIST_R;\n\n // ═══════════════════════════════════════════════════════════════════════════\n // ARM MIRRORING (팔 좌우 반전)\n // ═══════════════════════════════════════════════════════════════════════════\n //\n // PUNCH_PHASES arm values use LEFT-arm convention:\n // Left arm: negative Z = flexion (toward body), negative Z shoulder = pulled back\n // Right arm: positive Z = flexion (toward body), positive Z shoulder = pulled back\n //\n // PUNCH_PHASES opposite arm values use RIGHT-arm convention.\n //\n // When applying to the opposite side, Y and Z must be negated to mirror correctly.\n // The X axis (elevation/flexion) stays the same for both sides.\n //\n // 한국 무술 생체역학: 좌우 대칭 반전으로 정확한 자세 구현\n const armMirror = hand === \"right\" ? -1 : 1;\n\n // Apply punching arm shoulder rotation (with mirroring)\n if (phase.shoulder) {\n kf.rotate(\n shoulderBone,\n phase.shoulder[0],\n phase.shoulder[1] * armMirror,\n phase.shoulder[2] * armMirror,\n );\n }\n\n // Apply punching arm elbow rotation (with mirroring)\n if (phase.elbow) {\n kf.rotate(\n elbowBone,\n phase.elbow[0],\n phase.elbow[1],\n phase.elbow[2] * armMirror,\n );\n }\n\n // Optional punching arm wrist rotation (with mirroring)\n if (includeWrist && phase.wrist) {\n kf.rotate(\n wristBone,\n phase.wrist[0],\n phase.wrist[1],\n phase.wrist[2] * armMirror,\n );\n }\n\n // Apply opposite arm hikite (당기기 - pulling hand for power generation)\n // Opposite arm values are in RIGHT convention; mirror when applying to LEFT arm\n if (includeOppositeArm) {\n if (phase.oppositeShoulder) {\n kf.rotate(\n oppositeShoulderBone,\n phase.oppositeShoulder[0],\n phase.oppositeShoulder[1] * armMirror,\n phase.oppositeShoulder[2] * armMirror,\n );\n }\n\n if (phase.oppositeElbow) {\n kf.rotate(\n oppositeElbowBone,\n phase.oppositeElbow[0],\n phase.oppositeElbow[1],\n phase.oppositeElbow[2] * armMirror,\n );\n }\n\n if (includeWrist && phase.oppositeWrist) {\n kf.rotate(\n oppositeWristBone,\n phase.oppositeWrist[0],\n phase.oppositeWrist[1],\n phase.oppositeWrist[2] * armMirror,\n );\n }\n }\n\n // ═══════════════════════════════════════════════════════════════════════════\n // SPINE/PELVIS Y-AXIS MIRRORING (척추/골반 회전 반전)\n // ═══════════════════════════════════════════════════════════════════════════\n //\n // PUNCH_PHASES spine/pelvis Y values use RIGHT-hand convention:\n // Positive Y = counter-clockwise from above = drives RIGHT shoulder forward\n // For LEFT-hand punches, Y must be negated to drive LEFT shoulder forward\n //\n // 한국 무술: 허리비틀기 (Hip rotation) 좌우 반전\n const spineFlip = hand === \"right\" ? 1 : -1;\n\n if (phase.spineY !== undefined) {\n kf.rotate(BoneName.SPINE_UPPER, 0, phase.spineY * spineFlip, 0);\n if (includeSpineMiddle) {\n kf.rotate(BoneName.SPINE_MIDDLE, 0, phase.spineY * 0.7 * spineFlip, 0);\n }\n }\n if (phase.pelvisY !== undefined) {\n kf.rotate(BoneName.PELVIS, 0, phase.pelvisY * spineFlip, 0);\n }\n\n // ═══════════════════════════════════════════════════════════════════════════\n // ANATOMY INTEGRATION (해부학 통합)\n // ═══════════════════════════════════════════════════════════════════════════\n\n // Set punching hand pose and highlight\n if (handPose) {\n if (hand === \"right\") {\n kf.setRightHandPose(handPose, handHighlightMode);\n } else {\n kf.setLeftHandPose(handPose, handHighlightMode);\n }\n }\n\n // Set opposite hand pose (guard position)\n if (oppositeHandPose) {\n if (hand === \"right\") {\n kf.setLeftHandPose(oppositeHandPose);\n } else {\n kf.setRightHandPose(oppositeHandPose);\n }\n }\n}\n\n/**\n * Get a punch phase by name\n *\n * @param phaseName - Name of the phase from PUNCH_PHASES\n * @returns The punch phase data\n *\n * @korean 주먹단계가져오기\n */\nexport function getPunchPhase(phaseName: PunchPhaseName): PunchPhase {\n return PUNCH_PHASES[phaseName];\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6DA,SAAgB,wBACd,IACA,OACA,OAAkB,SAClB,UAQI,CAAC,GACC;CACN,MAAM,EACJ,eAAe,OACf,qBAAqB,OACrB,qBAAqB,MACrB,UACA,mBACA,qBACE;CAGJ,MAAM,eACJ,SAAS,UAAU,SAAS,aAAa,SAAS;CACpD,MAAM,YAAY,SAAS,UAAU,SAAS,UAAU,SAAS;CACjE,MAAM,YAAY,SAAS,UAAU,SAAS,UAAU,SAAS;CAGjE,MAAM,uBACJ,SAAS,UAAU,SAAS,aAAa,SAAS;CACpD,MAAM,oBACJ,SAAS,UAAU,SAAS,UAAU,SAAS;CACjD,MAAM,oBACJ,SAAS,UAAU,SAAS,UAAU,SAAS;CAgBjD,MAAM,YAAY,SAAS,UAAU,KAAK;CAG1C,IAAI,MAAM,UACR,GAAG,OACD,cACA,MAAM,SAAS,IACf,MAAM,SAAS,KAAK,WACpB,MAAM,SAAS,KAAK,SACtB;CAIF,IAAI,MAAM,OACR,GAAG,OACD,WACA,MAAM,MAAM,IACZ,MAAM,MAAM,IACZ,MAAM,MAAM,KAAK,SACnB;CAIF,IAAI,gBAAgB,MAAM,OACxB,GAAG,OACD,WACA,MAAM,MAAM,IACZ,MAAM,MAAM,IACZ,MAAM,MAAM,KAAK,SACnB;CAKF,IAAI,oBAAoB;EACtB,IAAI,MAAM,kBACR,GAAG,OACD,sBACA,MAAM,iBAAiB,IACvB,MAAM,iBAAiB,KAAK,WAC5B,MAAM,iBAAiB,KAAK,SAC9B;EAGF,IAAI,MAAM,eACR,GAAG,OACD,mBACA,MAAM,cAAc,IACpB,MAAM,cAAc,IACpB,MAAM,cAAc,KAAK,SAC3B;EAGF,IAAI,gBAAgB,MAAM,eACxB,GAAG,OACD,mBACA,MAAM,cAAc,IACpB,MAAM,cAAc,IACpB,MAAM,cAAc,KAAK,SAC3B;CAEJ;CAWA,MAAM,YAAY,SAAS,UAAU,IAAI;CAEzC,IAAI,MAAM,WAAW,KAAA,GAAW;EAC9B,GAAG,OAAO,SAAS,aAAa,GAAG,MAAM,SAAS,WAAW,CAAC;EAC9D,IAAI,oBACF,GAAG,OAAO,SAAS,cAAc,GAAG,MAAM,SAAS,KAAM,WAAW,CAAC;CAEzE;CACA,IAAI,MAAM,YAAY,KAAA,GACpB,GAAG,OAAO,SAAS,QAAQ,GAAG,MAAM,UAAU,WAAW,CAAC;CAQ5D,IAAI,UAAU;EACZ,IAAI,SAAS,SACX,GAAG,iBAAiB,UAAU,iBAAiB;OAE/C,GAAG,gBAAgB,UAAU,iBAAiB;CAElD;CAGA,IAAI,kBAAkB;EACpB,IAAI,SAAS,SACX,GAAG,gBAAgB,gBAAgB;OAEnC,GAAG,iBAAiB,gBAAgB;CAExC;AACF"}
|
|
@@ -729,24 +729,34 @@ var PlayerAnimationStateMachine = class PlayerAnimationStateMachine {
|
|
|
729
729
|
this.frameIndex++;
|
|
730
730
|
this.timeAccumulator -= frameDuration;
|
|
731
731
|
if (this.events?.onFrame && previousFrame !== this.frameIndex) this.events.onFrame(this.frameIndex, this.currentState);
|
|
732
|
-
if (this.frameIndex >= currentAnim.frames)
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
732
|
+
if (this.frameIndex >= currentAnim.frames) {
|
|
733
|
+
if (currentAnim.loop) this.frameIndex = 0;
|
|
734
|
+
else {
|
|
735
|
+
this.justCompleted = true;
|
|
736
|
+
if (this.events?.onAnimationComplete) this.events.onAnimationComplete(this.currentState);
|
|
737
|
+
if (this.currentState.startsWith("fall_")) {
|
|
738
|
+
const fallType = this.currentState.replace("fall_", "");
|
|
739
|
+
if (fallType === "forward" || fallType === "backward" || fallType === "side_left" || fallType === "side_right") {
|
|
740
|
+
const groundAnimKey = `ground_${FALL_TO_GROUND_MAP[fallType]}`;
|
|
741
|
+
if (DEFAULT_ANIMATION_CONFIGS.has(groundAnimKey)) {
|
|
742
|
+
const groundAnimState = groundAnimKey;
|
|
743
|
+
this.previousState = this.currentState;
|
|
744
|
+
this.currentState = groundAnimState;
|
|
745
|
+
this.frameIndex = 0;
|
|
746
|
+
this.timeAccumulator = 0;
|
|
747
|
+
this.justStarted = true;
|
|
748
|
+
if (this.events?.onAnimationStart) this.events.onAnimationStart(groundAnimState);
|
|
749
|
+
} else {
|
|
750
|
+
console.warn("[AnimationStateMachine] Invalid ground animation mapping for fall type:", fallType, "->", groundAnimKey);
|
|
751
|
+
this.previousState = this.currentState;
|
|
752
|
+
this.currentState = AnimationState.IDLE;
|
|
753
|
+
this.frameIndex = 0;
|
|
754
|
+
this.timeAccumulator = 0;
|
|
755
|
+
this.justStarted = true;
|
|
756
|
+
if (this.events?.onAnimationStart) this.events.onAnimationStart(AnimationState.IDLE);
|
|
757
|
+
}
|
|
748
758
|
} else {
|
|
749
|
-
console.warn("[AnimationStateMachine] Invalid
|
|
759
|
+
console.warn("[AnimationStateMachine] Invalid fall animation state:", this.currentState);
|
|
750
760
|
this.previousState = this.currentState;
|
|
751
761
|
this.currentState = AnimationState.IDLE;
|
|
752
762
|
this.frameIndex = 0;
|
|
@@ -754,32 +764,24 @@ var PlayerAnimationStateMachine = class PlayerAnimationStateMachine {
|
|
|
754
764
|
this.justStarted = true;
|
|
755
765
|
if (this.events?.onAnimationStart) this.events.onAnimationStart(AnimationState.IDLE);
|
|
756
766
|
}
|
|
757
|
-
} else {
|
|
758
|
-
|
|
767
|
+
} else if (this.currentState.startsWith("recovery_")) {
|
|
768
|
+
this.previousState = this.currentState;
|
|
769
|
+
this.currentState = AnimationState.IDLE;
|
|
770
|
+
this.frameIndex = 0;
|
|
771
|
+
this.timeAccumulator = 0;
|
|
772
|
+
this.justStarted = true;
|
|
773
|
+
if (this.events?.onAnimationStart) this.events.onAnimationStart(AnimationState.IDLE);
|
|
774
|
+
} else if (this.currentState !== AnimationState.IDLE && this.currentState !== AnimationState.KO && !this.currentState.startsWith("ground_")) {
|
|
775
|
+
if (this.currentState === AnimationState.STANCE_CHANGE) this.clearStanceTransition();
|
|
759
776
|
this.previousState = this.currentState;
|
|
760
777
|
this.currentState = AnimationState.IDLE;
|
|
761
778
|
this.frameIndex = 0;
|
|
762
779
|
this.timeAccumulator = 0;
|
|
763
780
|
this.justStarted = true;
|
|
764
781
|
if (this.events?.onAnimationStart) this.events.onAnimationStart(AnimationState.IDLE);
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
this.currentState = AnimationState.IDLE;
|
|
769
|
-
this.frameIndex = 0;
|
|
770
|
-
this.timeAccumulator = 0;
|
|
771
|
-
this.justStarted = true;
|
|
772
|
-
if (this.events?.onAnimationStart) this.events.onAnimationStart(AnimationState.IDLE);
|
|
773
|
-
} else if (this.currentState !== AnimationState.IDLE && this.currentState !== AnimationState.KO && !this.currentState.startsWith("ground_")) {
|
|
774
|
-
if (this.currentState === AnimationState.STANCE_CHANGE) this.clearStanceTransition();
|
|
775
|
-
this.previousState = this.currentState;
|
|
776
|
-
this.currentState = AnimationState.IDLE;
|
|
777
|
-
this.frameIndex = 0;
|
|
778
|
-
this.timeAccumulator = 0;
|
|
779
|
-
this.justStarted = true;
|
|
780
|
-
if (this.events?.onAnimationStart) this.events.onAnimationStart(AnimationState.IDLE);
|
|
781
|
-
this.processNextQueuedAnimation();
|
|
782
|
-
} else this.frameIndex = currentAnim.frames - 1;
|
|
782
|
+
this.processNextQueuedAnimation();
|
|
783
|
+
} else this.frameIndex = currentAnim.frames - 1;
|
|
784
|
+
}
|
|
783
785
|
}
|
|
784
786
|
}
|
|
785
787
|
const progress = currentAnim.frames > 0 ? this.frameIndex / currentAnim.frames : 0;
|