2026-09-06 23:24:25 +00:00
// ─────────────────────────────────────────────────────────────────────────────
// AttackRootMotion.cs — 공격 클립의 이동을 액터 이동으로 재현 (PD #789 · #791 · 2026-09-07)
/ /
// 모드(WLCombatMotionSettings.attackMoveMode):
// · FrameTable(기본 · #791): 클립별 "프레임 → 전진(cm)" 표(발 접지 실측으로 도출)를 클립 정규화 시간으로 재생.
// 발이 땅에 붙어 있는 구간엔 0, 발이 공중에 있거나 내딛는 구간에만 전진 → 발 슬라이딩이 없다.
// 힘민지 원본 `Interpolation` 이벤트(프레임 목록 + 프레임당 값)와 같은 개념을 WL 설정 SO 로 옮긴 것(FBX 이벤트 무수정).
// · ClipRootMotion(#789): animator.deltaPosition(RootT) 재생. Knight 클립은 상체 러닝(hips 33 cm 전후)이 루트에 섞여
// 접지 발이 30 cm 미끄러진다(2026-09-07 실측) → 기본에서 제외, 비교용으로 보존.
// · FixedStep(#762): AttackStepDriver + 원본 Set_Move.
// 적용: 공격 상태에서만 · NavMeshAgent.Move · 대상 stopDistance 관통 방지 · 조이스틱 입력 시 중단 없음(공격 중엔 입력 무시가 원본 동작).
// ─────────────────────────────────────────────────────────────────────────────
using System.Collections.Generic ;
using UnityEngine ;
using UnityEngine.AI ;
namespace WL.Combat
{
[DisallowMultipleComponent]
public sealed class AttackRootMotion : MonoBehaviour
{
private PCActor _pc ;
private Animator _anim ;
private NavMeshAgent _agent ;
private WLCombatMotionSettings _cfg ;
// FrameTable 상태
private int _tableClipHash ; // 현재 재생 중인 표의 클립(상태) 식별
private int _tableLastFrame = - 1 ; // 마지막으로 처리한 프레임 인덱스
private float _tableLastNorm = - 1f ;
// ── 검증용 정적 카운터 ──
public static int EntryCount , ApplyCount ;
public static float LastFrameDelta , SwingAccum , RawAccum ;
public static string LastInfo = "" ;
public static bool HandlesAttackMove ( PCActor pc )
{
var cfg = WLCombatMotionSettings . Instance ;
return pc ! = null & & cfg ! = null & & cfg . attackMoveMode ! = AttackMoveMode . FixedStep ;
}
public static void Ensure ( PCActor pc )
{
if ( pc = = null | | pc . m_animation = = null ) return ;
var cfg = WLCombatMotionSettings . Instance ;
if ( cfg = = null ) return ;
var host = pc . m_animation . gameObject ;
var rm = host . GetComponent < AttackRootMotion > ( ) ;
if ( rm = = null ) rm = host . AddComponent < AttackRootMotion > ( ) ;
rm . _pc = pc ; rm . _anim = pc . m_animation ; rm . _agent = pc . Get_Agent ( ) ; rm . _cfg = cfg ;
rm . _anim . applyRootMotion = cfg . attackMoveMode = = AttackMoveMode . ClipRootMotion ;
}
2026-10-18 15:09:05 +00:00
private void OnDisable ( ) { RestoreSpeed ( ) ; if ( _anim ! = null ) _anim . applyRootMotion = false ; }
// #899 앞 프레임 배속(방패 치기 전 숙이기) — 내가 바꾼 배속 그대로일 때만 되돌린다(그 사이 다른 코드가 바꾼 값은 건드리지 않는다).
private bool _speedOn ; private float _speedSet , _speedBase ;
private void SpeedWindow ( AttackFrameTable t , int frame )
{
bool want = t ! = null & & t . speedUntilFrame > 0 & & t . speedScale > 0f & & frame < = t . speedUntilFrame ;
if ( want & & ! _speedOn ) { _speedBase = _anim . speed ; _speedSet = _speedBase * t . speedScale ; _anim . speed = _speedSet ; _speedOn = true ; }
else if ( ! want ) RestoreSpeed ( ) ;
}
private void RestoreSpeed ( )
{
if ( ! _speedOn ) return ;
_speedOn = false ;
if ( _anim ! = null & & Mathf . Approximately ( _anim . speed , _speedSet ) ) _anim . speed = _speedBase ;
}
2026-09-06 23:24:25 +00:00
private void OnAnimatorMove ( )
{
EntryCount + + ;
if ( _pc = = null | | _anim = = null ) return ;
var cfg = _cfg ! = null ? _cfg : WLCombatMotionSettings . Instance ;
if ( cfg = = null ) return ;
if ( cfg . attackMoveMode ! = AttackMoveMode . ClipRootMotion )
{
if ( _anim . applyRootMotion ) _anim . applyRootMotion = false ;
return ;
}
if ( ! _anim . applyRootMotion ) _anim . applyRootMotion = true ;
if ( ! Gate ( cfg ) ) return ;
Vector3 d = _anim . deltaPosition ; d . y = 0f ;
RawAccum + = d . magnitude ;
if ( d . sqrMagnitude < 1e-12f ) return ;
Apply ( d * Mathf . Max ( cfg . rootMotionScale , 0f ) , cfg , true ) ;
}
private void LateUpdate ( )
{
if ( _pc = = null | | _anim = = null ) return ;
var cfg = _cfg ! = null ? _cfg : WLCombatMotionSettings . Instance ;
if ( cfg = = null | | cfg . attackMoveMode ! = AttackMoveMode . FrameTable ) return ;
2026-10-18 15:09:05 +00:00
if ( ! Gate ( cfg ) ) { _tableLastFrame = - 1 ; _tableClipHash = 0 ; RestoreSpeed ( ) ; return ; }
2026-09-06 23:24:25 +00:00
var st = _anim . GetCurrentAnimatorStateInfo ( 0 ) ;
var ci = _anim . GetCurrentAnimatorClipInfo ( 0 ) ;
2026-10-18 15:09:05 +00:00
// #841 전이 중에는 다음 상태의 표를 미리 재생한다 — 크로스페이드(0.15 s) 동안 소비되는 앞 프레임이 통째로 빠졌다
// (2026-10-11 PassLog 실측: 비수 4타 첫 적용 프레임 = f7 → f1~f6 의 51 cm 유실 · 돌진 1.16 m 가 0.65 m 로 끝남).
if ( _anim . IsInTransition ( 0 ) )
{
var nci = _anim . GetNextAnimatorClipInfo ( 0 ) ;
if ( nci . Length > 0 & & nci [ 0 ] . clip ! = null & & cfg . FindFrameTable ( nci [ 0 ] . clip . name ) ! = null ) { st = _anim . GetNextAnimatorStateInfo ( 0 ) ; ci = nci ; }
}
if ( ci . Length = = 0 | | ci [ 0 ] . clip = = null ) { RestoreSpeed ( ) ; return ; }
2026-09-06 23:24:25 +00:00
var clip = ci [ 0 ] . clip ;
var table = cfg . FindFrameTable ( clip . name ) ;
2026-10-18 15:09:05 +00:00
if ( table = = null ) { RestoreSpeed ( ) ; return ; }
2026-09-06 23:24:25 +00:00
float norm = st . normalizedTime - Mathf . Floor ( st . normalizedTime ) ;
int totalFrames = Mathf . Max ( 1 , Mathf . RoundToInt ( clip . frameRate * clip . length ) ) ;
int frame = Mathf . Clamp ( ( int ) ( norm * totalFrames ) , 0 , totalFrames ) ;
2026-10-18 15:09:05 +00:00
SpeedWindow ( table , frame ) ; // #899 방패 치기 전 숙이기 배속
2026-09-06 23:24:25 +00:00
int hash = st . fullPathHash ;
if ( hash ! = _tableClipHash | | norm < _tableLastNorm - 0.5f ) { _tableClipHash = hash ; _tableLastFrame = frame - 1 ; }
_tableLastNorm = norm ;
if ( frame < = _tableLastFrame ) return ;
// 놓친 프레임까지 합산(프레임 드랍 보정 · 원본 Co_Interpolation 과 같은 규약)
float cm = 0f ;
for ( int f = _tableLastFrame + 1 ; f < = frame ; f + + ) cm + = table . ValueAt ( f ) ;
_tableLastFrame = frame ;
if ( cm < = 0f ) return ;
Vector3 fwd = _pc . transform . forward ; fwd . y = 0f ;
if ( fwd . sqrMagnitude < 1e-6f ) return ;
2026-10-18 15:09:05 +00:00
_passThrough = table . passThrough ; // #840 비수 4타 돌진베기 — 적 앞 멈춤 무시(통과)
_stopOverride = table . stopDistance ; // #899 방패 치기 앞 한 걸음 — 표 고유 멈춤 거리
_lastFrame = frame ;
2026-09-06 23:24:25 +00:00
Apply ( fwd . normalized * ( cm * 0.01f * Mathf . Max ( cfg . frameTableScale , 0f ) ) , cfg , false ) ;
2026-10-18 15:09:05 +00:00
_passThrough = false ; _stopOverride = 0f ;
2026-09-06 23:24:25 +00:00
}
2026-10-18 15:09:05 +00:00
private bool _passThrough ; private int _lastFrame ; private float _stopOverride ;
/// <summary>검증용 — 통과 돌진(passThrough) 표의 프레임별 요청/실제 이동(NavMeshAgent.Move 는 적 에이전트와 겹쳐도 깎이지 않는다 · 2026-10-11 실측). 4 KB 순환.</summary>
public static readonly System . Text . StringBuilder PassLog = new System . Text . StringBuilder ( ) ;
2026-09-06 23:24:25 +00:00
private bool Gate ( WLCombatMotionSettings cfg )
{
if ( _agent = = null | | ! _agent . enabled | | ! _agent . isOnNavMesh ) return false ;
if ( ! _pc . WL_CanAttackStep ( ) ) return false ;
if ( DashDriver . IsDashingNow ( _pc ) ) return false ;
2026-10-21 05:29:56 +00:00
if ( WL . Combat . Weapon . WLGhostStrike . IsLanding ( _pc ) ) return false ; // #960 비수 각성 착지(양손 베기 = 4타 클립)는 옆 진입 뒤 멈춰야 한다 — 4타 전진 표가 착지 뒤 0.65 m 더 밀었다(실측 이동 0.75 m)
2026-10-21 07:56:27 +00:00
if ( WL . Combat . Weapon . WLShadowSlash . IsSlashing ( _pc ) ) return false ; // #961 비수 주력 난사(1·2타 클립 강제 재생) 중 전진 표 금지 — 휘두를 때마다 밀려 10회에 약 1 m 전진 · 이펙트만 그 자리에 남았다(Play 실측)
2026-09-06 23:24:25 +00:00
return true ;
}
private void Apply ( Vector3 d , WLCombatMotionSettings cfg , bool clampSpeed )
{
if ( clampSpeed )
{
float maxStep = Mathf . Max ( cfg . rootMotionMaxSpeed , 0.1f ) * Mathf . Max ( Time . deltaTime , 1e-4f ) ;
if ( d . magnitude > maxStep ) d = d . normalized * maxStep ;
}
if ( ! cfg . rootMotionAllowBackward )
{
Vector3 f = _pc . transform . forward ; f . y = 0f ;
if ( f . sqrMagnitude > 1e-6f ) { f . Normalize ( ) ; float along = Vector3 . Dot ( d , f ) ; if ( along < 0f ) d - = f * along ; }
}
var target = _pc . Get_Target ( ) ;
2026-10-18 15:09:05 +00:00
float stopAt = _stopOverride > 0f ? _stopOverride : WLCombatMotionSettings . ApproachStop ( _pc , cfg . attackStepStopDistance ) ; // #893 공격 가능 거리의 절반 · #899 표 고유 값 우선
if ( ! _passThrough & & target ! = null & & ! target . IsDead ( ) & & stopAt > 0f )
2026-09-06 23:24:25 +00:00
{
Vector3 to = target . Get_position ( ) - _pc . transform . position ; to . y = 0f ;
float dist = to . magnitude ;
if ( dist > 1e-4f )
{
Vector3 n = to / dist ; float along = Vector3 . Dot ( d , n ) ;
2026-10-18 15:09:05 +00:00
if ( along > 0f & & dist - along < stopAt )
{ float allowed = Mathf . Max ( dist - stopAt , 0f ) ; d - = n * ( along - allowed ) ; }
2026-09-06 23:24:25 +00:00
}
}
if ( d . sqrMagnitude < 1e-12f ) return ;
2026-10-18 15:09:05 +00:00
Vector3 before = _pc . transform . position ;
2026-09-06 23:24:25 +00:00
_agent . Move ( d ) ;
2026-10-18 15:09:05 +00:00
if ( _passThrough )
{
if ( PassLog . Length > 4000 ) PassLog . Length = 0 ;
PassLog . Append ( string . Format ( "f{0} req={1:F3} {2}->{3}\n" , _lastFrame , d . magnitude , before . ToString ( "F2" ) , _pc . transform . position . ToString ( "F2" ) ) ) ;
}
2026-09-06 23:24:25 +00:00
ApplyCount + + ; LastFrameDelta = d . magnitude ; SwingAccum + = d . magnitude ;
}
}
}