fix: 드로그 패턴 애니메이션 재생 끊김 수정

- BT 재평가 중에도 패턴 실행 상태를 보존하도록 보스 패턴 액션과 런타임 상태를 조정했다.
- 스킬 컨트롤러에서 동일 프레임 종료 판정을 막아 패턴 내 다음 스킬이 즉시 잘리는 문제를 수정했다.
- 드로그 BT, 패턴/스킬 데이터, 애니메이션 클립과 컨트롤러를 현재 검증된 재생 구성으로 정리했다.
- 자연 발동 기준으로 콤보-기본기2 재생 시간을 재검증해 클립 길이와 실제 재생 간격이 맞는 것을 확인했다.
This commit is contained in:
2026-04-12 05:44:54 +09:00
parent 12a481b596
commit 9fd231626b
40 changed files with 598072 additions and 425361 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: e35d6eb3ae2c5a146801c9dd399acd52
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 7400000
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@@ -43628,7 +43628,7 @@ AnimationClip:
m_KeepOriginalPositionY: 1
m_KeepOriginalPositionXZ: 0
m_HeightFromFeet: 0
m_Mirror: 1
m_Mirror: 0
m_EditorCurves: []
m_EulerEditorCurves: []
m_HasGenericRootTransform: 0

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 94da51b9da4bad4129ba5e33e671db62
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 7400000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 4ff85a68bb491e143a001f3af82639ed
guid: 2ca3044b7e7a3ff4eb8d3d6bf75f5e92
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 7400000

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 8cfd11750543a4484bae82462b7c0351
guid: 6b11f2b53d826bba3ab9f8086a42de14
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 7400000

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: b8206d6d914a86cd9a169763c82f273e
guid: 156c90879bba0d7649e8b29224daa390
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 7400000

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 16c44854334d767d3af2fd774b89a809
guid: 6439cde8bc726bd1caad7d6e18a31416
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 7400000

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 82cf3119cd7b56e3e9d579cac94fc09d
guid: 606aec780e456217687074cbbf23a2c8
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 7400000

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 18b092b0aae73b9219db20623b0c3427
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 7400000
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 8f572193078ff9e229c1d39038620857
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 7400000
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 0b70d6464b876144c84f2410c0359a4f
guid: 16c44854334d767d3af2fd774b89a809
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 7400000

View File

@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 9678ad326a270e9aa9cb5ebf5fa00279
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 7400000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -161,7 +161,7 @@ AnimatorStateMachine:
m_ChildStates:
- serializedVersion: 1
m_State: {fileID: -4652545120162758660}
m_Position: {x: 290, y: 180, z: 0}
m_Position: {x: 480, y: 230, z: 0}
- serializedVersion: 1
m_State: {fileID: -2487449162152911812}
m_Position: {x: -220, y: 350, z: 0}
@@ -454,30 +454,6 @@ AnimatorController:
m_IKPass: 0
m_SyncedLayerAffectsTiming: 0
m_Controller: {fileID: 9100000}
- serializedVersion: 5
m_Name: Override
m_StateMachine: {fileID: -8724618498758723359}
m_Mask: {fileID: 31900000, guid: d878cc26c0d6449d2ad2c0ed02c73eca, type: 2}
m_Motions:
- serializedVersion: 2
m_State: {fileID: 0}
m_Motion: {fileID: -1601098376034012026, guid: 86ec0c0d77d3fcd47b879e206e9b492c, type: 3}
- serializedVersion: 2
m_State: {fileID: -4652545120162758660}
m_Motion: {fileID: 691025228808474260, guid: f24af721b97fadb4ba0f82937853e9f7, type: 3}
- serializedVersion: 2
m_State: {fileID: -2487449162152911812}
m_Motion: {fileID: 8640942340808133859, guid: 235e9fb2014c3044ba826dd0e71987a4, type: 3}
- serializedVersion: 2
m_State: {fileID: 0}
m_Motion: {fileID: 9143937929783346939, guid: 92dc2765bff93f540a4433ac13713d72, type: 3}
m_Behaviours: []
m_BlendingMode: 0
m_SyncedLayerIndex: 0
m_DefaultWeight: 0
m_IKPass: 0
m_SyncedLayerAffectsTiming: 0
m_Controller: {fileID: 9100000}
--- !u!1102 &1582555298472611487
AnimatorState:
serializedVersion: 6
@@ -493,7 +469,7 @@ AnimatorState:
- {fileID: 2459631991574308566}
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_IKOnFeet: 1
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0

View File

@@ -22,16 +22,16 @@ MonoBehaviour:
Skill: {fileID: 11400000, guid: ae7fc1b970b770680b95f69111f2b08a, type: 2}
Duration: 0
ChargeData:
requiredDamageRatio: 0
requiredDamageRatio: 0.1
telegraphAbnormality: {fileID: 0}
staggerDuration: 0
staggerDuration: 2
- Type: 0
Skill: {fileID: 11400000, guid: e666c41a932cdd478a62552e12c64801, type: 2}
Duration: 0
ChargeData:
requiredDamageRatio: 0
requiredDamageRatio: 0.1
telegraphAbnormality: {fileID: 0}
staggerDuration: 0
staggerDuration: 2
- Type: 1
Skill: {fileID: 0}
Duration: 0.1

View File

@@ -22,16 +22,16 @@ MonoBehaviour:
Skill: {fileID: 11400000, guid: d8008b7d595f832798f900b884fb6ac2, type: 2}
Duration: 0
ChargeData:
requiredDamageRatio: 0
requiredDamageRatio: 0.1
telegraphAbnormality: {fileID: 0}
staggerDuration: 0
staggerDuration: 2
- Type: 0
Skill: {fileID: 11400000, guid: a42c075b82a2b40b3b1c4540bea4bd03, type: 2}
Duration: 0
ChargeData:
requiredDamageRatio: 0
requiredDamageRatio: 0.1
telegraphAbnormality: {fileID: 0}
staggerDuration: 0
staggerDuration: 2
cooldown: 3
minPhase: 1
skipJumpStepOnNoTarget: 0

View File

@@ -22,23 +22,23 @@ MonoBehaviour:
Skill: {fileID: 11400000, guid: 78fa18c15c0ea5248bc6966b4b2c4e04, type: 2}
Duration: 0
ChargeData:
requiredDamageRatio: 0
requiredDamageRatio: 0.1
telegraphAbnormality: {fileID: 0}
staggerDuration: 0
staggerDuration: 2
- Type: 0
Skill: {fileID: 11400000, guid: 03c4971dcffb2ea0eb36ac997ee2a1a0, type: 2}
Duration: 0
ChargeData:
requiredDamageRatio: 0
requiredDamageRatio: 0.1
telegraphAbnormality: {fileID: 0}
staggerDuration: 0
staggerDuration: 2
- Type: 0
Skill: {fileID: 11400000, guid: fc435c80b8f1348f889910629f8eec51, type: 2}
Duration: 0
ChargeData:
requiredDamageRatio: 0
requiredDamageRatio: 0.1
telegraphAbnormality: {fileID: 0}
staggerDuration: 0
staggerDuration: 2
cooldown: 3.25
minPhase: 1
skipJumpStepOnNoTarget: 0

View File

@@ -22,16 +22,16 @@ MonoBehaviour:
Skill: {fileID: 11400000, guid: a3d01db588247bc93861ea39572489f5, type: 2}
Duration: 0
ChargeData:
requiredDamageRatio: 0
requiredDamageRatio: 0.1
telegraphAbnormality: {fileID: 0}
staggerDuration: 0
staggerDuration: 2
- Type: 0
Skill: {fileID: 11400000, guid: 4d2a845524d535769b4e2583c6321ffe, type: 2}
Duration: 0
ChargeData:
requiredDamageRatio: 0
requiredDamageRatio: 0.1
telegraphAbnormality: {fileID: 0}
staggerDuration: 0
staggerDuration: 2
- Type: 1
Skill: {fileID: 0}
Duration: 0.1

View File

@@ -20,7 +20,6 @@ MonoBehaviour:
baseTypes: 1
animationClips:
- {fileID: 7400000, guid: 567a0c8cbb10eafa08807226645826e2, type: 2}
- {fileID: 7400000, guid: 94da51b9da4bad4129ba5e33e671db62, type: 2}
animationSpeed: 1
useRootMotion: 1
ignoreRootMotionY: 1

View File

@@ -12,15 +12,15 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 94f0a76cebcac2f4fb5daf1b675fd79f, type: 3}
m_Name: "Data_Skill_Drog_\uCF64\uBCF4-\uAE30\uBCF8\uAE301_3"
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.SkillData
skillName: "\uCF64\uBCF4-\uAE30\uBCF8\uAE301 2\uD0C0"
skillName: "\uCF64\uBCF4-\uAE30\uBCF8\uAE301 3"
description: "\uAE30\uBCF8\uAE30 \uCF64\uBCF41\uC758 \uD6C4\uC18D \uD0C0\uACA9\uC785\uB2C8\uB2E4."
icon: {fileID: 0}
skillRole: 1
activationType: 1
baseTypes: 1
animationClips:
- {fileID: 7400000, guid: 567a0c8cbb10eafa08807226645826e2, type: 2}
- {fileID: 7400000, guid: 94da51b9da4bad4129ba5e33e671db62, type: 2}
- {fileID: 7400000, guid: b948f6e859be42cf9ad570e16fd418f1, type: 2}
- {fileID: 7400000, guid: 2ca3044b7e7a3ff4eb8d3d6bf75f5e92, type: 2}
animationSpeed: 1
useRootMotion: 1
ignoreRootMotionY: 1

View File

@@ -19,9 +19,9 @@ MonoBehaviour:
activationType: 1
baseTypes: 1
animationClips:
- {fileID: 7400000, guid: 8cfd11750543a4484bae82462b7c0351, type: 2}
- {fileID: 7400000, guid: b8206d6d914a86cd9a169763c82f273e, type: 2}
- {fileID: 7400000, guid: 16c44854334d767d3af2fd774b89a809, type: 2}
- {fileID: 7400000, guid: 6b11f2b53d826bba3ab9f8086a42de14, type: 2}
- {fileID: 7400000, guid: 156c90879bba0d7649e8b29224daa390, type: 2}
- {fileID: 7400000, guid: 6439cde8bc726bd1caad7d6e18a31416, type: 2}
animationSpeed: 1
useRootMotion: 1
ignoreRootMotionY: 1

View File

@@ -19,7 +19,7 @@ MonoBehaviour:
activationType: 1
baseTypes: 1
animationClips:
- {fileID: 7400000, guid: 82cf3119cd7b56e3e9d579cac94fc09d, type: 2}
- {fileID: 7400000, guid: 606aec780e456217687074cbbf23a2c8, type: 2}
animationSpeed: 1
useRootMotion: 1
ignoreRootMotionY: 1

View File

@@ -19,7 +19,7 @@ MonoBehaviour:
activationType: 1
baseTypes: 1
animationClips:
- {fileID: 7400000, guid: 8f572193078ff9e229c1d39038620857, type: 2}
- {fileID: 7400000, guid: 16c44854334d767d3af2fd774b89a809, type: 2}
animationSpeed: 1
useRootMotion: 1
ignoreRootMotionY: 1

View File

@@ -43,6 +43,12 @@ public abstract partial class BossPatternActionBase : Action
private ChargeStepData activeChargeData;
private bool chargeTelegraphApplied;
/// <summary>
/// 현재 액션 인스턴스가 진행 중인 패턴 실행 상태를 이미 보유하고 있는지 여부입니다.
/// BT 재평가 중 재진입할 때 기존 실행을 이어가기 위한 가드로 사용합니다.
/// </summary>
protected bool HasActivePatternExecutionState => activePattern != null;
/// <summary>
/// 액션 시작 시 실제로 실행할 패턴과 대상을 결정합니다.
/// </summary>
@@ -56,6 +62,10 @@ public abstract partial class BossPatternActionBase : Action
protected override Status OnStart()
{
ResolveReferences();
if (ShouldPreserveExecutionState())
return Status.Running;
ClearRuntimeState();
if (!IsReady())
@@ -153,6 +163,9 @@ public abstract partial class BossPatternActionBase : Action
protected override void OnEnd()
{
if (ShouldPreserveExecutionState())
return;
ClearRuntimeState();
}
@@ -389,6 +402,20 @@ public abstract partial class BossPatternActionBase : Action
waitEndTime = 0f;
}
/// <summary>
/// BT 관찰자 재평가로 노드가 다시 시작될 때 현재 패턴 실행 상태를 유지해야 하는지 판단합니다.
/// </summary>
private bool ShouldPreserveExecutionState()
{
if (!IsReady() || activePattern == null || runtimeState == null || !runtimeState.IsExecutingPattern)
return false;
if (runtimeState.IsBehaviorSuppressed || bossEnemy.IsDead)
return false;
return true;
}
private bool IsFirstSkillStep(int stepIndex)
{
if (activePattern == null || activePattern.Steps == null)

View File

@@ -34,6 +34,9 @@ namespace Colosseum.AI.BehaviorActions.Actions
protected override Status OnStart()
{
if (HasActivePatternExecutionState)
return base.OnStart();
if (!TrySelectPattern(out selectedPattern))
return Status.Failure;
@@ -44,7 +47,9 @@ namespace Colosseum.AI.BehaviorActions.Actions
protected override void OnEnd()
{
selectedPattern = null;
if (!HasActivePatternExecutionState)
selectedPattern = null;
base.OnEnd();
}

View File

@@ -45,6 +45,8 @@ namespace Colosseum.Enemy
[Header("Pattern Flow")]
[Tooltip("패턴 하나가 끝난 뒤 다음 패턴을 시작하기까지의 공통 텀")]
[Min(0f)] [SerializeField] protected float commonPatternInterval = 0.35f;
[Tooltip("패턴 종료 후 Idle 자세가 잠깐 안착할 수 있도록 추가로 확보하는 시간")]
[Min(0f)] [SerializeField] protected float postPatternIdleSettleDuration = 0.12f;
[Header("Phase State")]
[Tooltip("BT가 관리하는 최대 페이즈 수")]
@@ -62,6 +64,7 @@ namespace Colosseum.Enemy
protected int currentPatternPhase = 1;
protected float currentPhaseStartTime;
protected float nextPatternReadyTime;
protected float lastPatternCompletedTime = float.NegativeInfinity;
protected BossPatternExecutionResult lastPatternExecutionResult;
protected BossPatternData lastExecutedPattern;
protected BossPatternData activePattern;
@@ -100,6 +103,7 @@ namespace Colosseum.Enemy
/// 패턴 종료 후 다음 패턴 시작까지 남은 공통 텀입니다.
/// </summary>
public float RemainingPatternInterval => Mathf.Max(0f, nextPatternReadyTime - Time.time);
public float RemainingPatternIdleSettleTime => Mathf.Max(0f, (lastPatternCompletedTime + Mathf.Max(0f, postPatternIdleSettleDuration)) - Time.time);
/// <summary>
/// 마지막 패턴 실행 결과
@@ -239,6 +243,7 @@ namespace Colosseum.Enemy
activePattern = null;
currentPatternSkillStartsFromIdle = false;
currentPatternSkillReturnsToIdle = false;
lastPatternCompletedTime = Time.time;
if (pattern != null && IsTerminalPatternExecutionResult(result))
StartCommonPatternInterval();
@@ -338,6 +343,9 @@ namespace Colosseum.Enemy
if (pattern == null || pattern.Steps == null || pattern.Steps.Count == 0)
return false;
if (Time.time < lastPatternCompletedTime + Mathf.Max(0f, postPatternIdleSettleDuration))
return false;
if (!IsCommonPatternIntervalReady())
return false;
@@ -407,6 +415,7 @@ namespace Colosseum.Enemy
currentPatternPhase = 1;
currentPhaseStartTime = Time.time;
nextPatternReadyTime = 0f;
lastPatternCompletedTime = float.NegativeInfinity;
lastPatternExecutionResult = BossPatternExecutionResult.None;
lastExecutedPattern = null;
lastReviveCaster = null;

View File

@@ -127,6 +127,10 @@ namespace Colosseum.Skills
private Dictionary<SkillData, float> cooldownTracker = new Dictionary<SkillData, float>();
private AnimatorOverrideController runtimeOverrideController;
private int cachedRecoveryStateHash;
private float currentClipStartTime = -1f;
private float currentClipExpectedDuration = 0f;
private string currentClipDebugName = string.Empty;
private int currentClipStartFrame = -1;
public bool IsExecutingSkill => currentSkill != null;
@@ -292,7 +296,13 @@ namespace Colosseum.Skills
// 애니메이션 종료 시 처리
if (stateInfo.normalizedTime >= 1f)
{
if (Time.frameCount <= currentClipStartFrame)
return;
// 같은 반복 차수 내에서 다음 클립이 있으면 재생
if (HasNextClipInSequence())
LogCurrentClipTiming("Advance");
if (TryPlayNextClipInSequence())
return;
@@ -308,6 +318,7 @@ namespace Colosseum.Skills
}
// 모든 클립과 단계가 끝나면 종료
LogCurrentClipTiming("Complete");
if (debugMode) Debug.Log($"[Skill] Animation complete: {currentSkill.SkillName}");
RestoreBaseControllerIfNeeded();
CompleteCurrentSkillExecution(SkillExecutionResult.Completed);
@@ -566,7 +577,8 @@ namespace Colosseum.Skills
? currentLoadoutEntry.GetResolvedAnimationSpeed()
: currentSkill.AnimationSpeed;
animator.speed = resolvedAnimationSpeed;
PlaySkillClip(currentPhaseAnimationClips[0], ShouldBlendIntoClip());
float enterTransitionDuration = ResolveSkillEnterTransitionDuration();
PlaySkillClip(currentPhaseAnimationClips[0], enterTransitionDuration > 0f, enterTransitionDuration);
}
TriggerImmediateSelfEffectsIfNeeded();
@@ -588,7 +600,7 @@ namespace Colosseum.Skills
return false;
currentClipSequenceIndex = nextIndex;
PlaySkillClip(currentPhaseAnimationClips[currentClipSequenceIndex], blendIn: false);
PlaySkillClip(currentPhaseAnimationClips[currentClipSequenceIndex], blendIn: false, transitionDuration: 0f);
if (debugMode)
{
@@ -598,6 +610,17 @@ namespace Colosseum.Skills
return true;
}
/// <summary>
/// 현재 시퀀스에 다음 클립이 남아 있는지 반환합니다.
/// </summary>
private bool HasNextClipInSequence()
{
if (currentSkill == null || currentPhaseAnimationClips == null)
return false;
return currentClipSequenceIndex + 1 < currentPhaseAnimationClips.Count;
}
/// <summary>
/// 반복 시전이 남아 있으면 다음 차수를 시작합니다.
/// </summary>
@@ -616,7 +639,7 @@ namespace Colosseum.Skills
/// <summary>
/// 스킬 클립으로 Override Controller 생성 후 재생
/// </summary>
private void PlaySkillClip(AnimationClip clip, bool blendIn)
private void PlaySkillClip(AnimationClip clip, bool blendIn, float transitionDuration)
{
if (baseSkillClip == null)
{
@@ -630,19 +653,21 @@ namespace Colosseum.Skills
return;
}
RecordCurrentClipTiming(clip);
if (debugMode)
{
Debug.Log($"[Skill] PlaySkillClip: {clip.name}, BaseClip: {baseSkillClip.name}");
Debug.Log($"[Skill] PlaySkillClip: {clip.name}, BaseClip: {baseSkillClip.name}, Blend={blendIn}, Transition={transitionDuration:F2}");
}
if (blendIn)
animator.CrossFadeInFixedTime(GetSkillStateHash(), GetSkillEnterTransitionDuration(), 0, 0f);
animator.CrossFadeInFixedTime(GetSkillStateHash(), transitionDuration, 0, 0f);
else
animator.Play(GetSkillStateHash(), 0, 0f);
// 클라이언트에 클립 동기화
if (IsServer && IsSpawned)
PlaySkillClipClientRpc(registeredClips.IndexOf(clip), blendIn);
PlaySkillClipClientRpc(registeredClips.IndexOf(clip), blendIn, transitionDuration);
}
/// <summary>
@@ -673,7 +698,7 @@ namespace Colosseum.Skills
/// 클라이언트: override 컨트롤러 적용 + 스킬 상태 재생 (원자적 실행으로 타이밍 문제 해결)
/// </summary>
[Rpc(SendTo.NotServer)]
private void PlaySkillClipClientRpc(int clipIndex, bool blendIn)
private void PlaySkillClipClientRpc(int clipIndex, bool blendIn, float transitionDuration)
{
if (baseSkillClip == null || animator == null || baseController == null) return;
if (clipIndex < 0 || clipIndex >= registeredClips.Count || registeredClips[clipIndex] == null)
@@ -685,7 +710,7 @@ namespace Colosseum.Skills
if (!ApplyOverrideClip(registeredClips[clipIndex]))
return;
if (blendIn)
animator.CrossFadeInFixedTime(GetSkillStateHash(), GetSkillEnterTransitionDuration(), 0, 0f);
animator.CrossFadeInFixedTime(GetSkillStateHash(), transitionDuration, 0, 0f);
else
animator.Play(GetSkillStateHash(), 0, 0f);
}
@@ -825,6 +850,7 @@ namespace Colosseum.Skills
lastCancelledSkillName = currentSkill.SkillName;
lastCancelReason = reason;
LogCurrentClipTiming("Cancelled");
Debug.Log($"[Skill] Cancelled: {currentSkill.SkillName} / reason={reason}");
RestoreBaseController();
@@ -1171,7 +1197,7 @@ namespace Colosseum.Skills
? currentLoadoutEntry.GetResolvedAnimationSpeed()
: currentSkill.AnimationSpeed;
animator.speed = resolvedAnimationSpeed;
PlaySkillClip(currentPhaseAnimationClips[0], blendIn: false);
PlaySkillClip(currentPhaseAnimationClips[0], blendIn: false, transitionDuration: 0f);
if (debugMode)
Debug.Log($"[Skill] 해제 단계 시작: {currentSkill.SkillName}");
@@ -1265,6 +1291,10 @@ namespace Colosseum.Skills
currentIterationIndex = 0;
loopHoldRequested = false;
cachedRecoveryStateHash = 0;
currentClipStartTime = -1f;
currentClipExpectedDuration = 0f;
currentClipDebugName = string.Empty;
currentClipStartFrame = -1;
shouldBlendIntoCurrentSkill = true;
shouldRestoreToIdleAfterCurrentSkill = true;
}
@@ -1288,6 +1318,38 @@ namespace Colosseum.Skills
sustainController?.HandleSkillExecutionEnded();
}
/// <summary>
/// 현재 클립의 예상 재생 시간을 기록합니다.
/// </summary>
private void RecordCurrentClipTiming(AnimationClip clip)
{
if (clip == null)
return;
currentClipStartTime = Time.time;
currentClipExpectedDuration = clip.length / Mathf.Max(0.0001f, animator != null ? animator.speed : 1f);
currentClipDebugName = clip.name;
currentClipStartFrame = Time.frameCount;
if (!debugMode)
return;
Debug.Log($"[SkillTiming] Start: skill={currentSkill?.SkillName ?? "<null>"}, clip={currentClipDebugName}, t={currentClipStartTime:F3}, expected={currentClipExpectedDuration:F3}");
}
/// <summary>
/// 현재 클립의 실제 경과 시간을 로그로 남깁니다.
/// </summary>
private void LogCurrentClipTiming(string phase)
{
if (!debugMode || string.IsNullOrEmpty(currentClipDebugName) || currentClipStartTime < 0f)
return;
float elapsed = Time.time - currentClipStartTime;
float delta = elapsed - currentClipExpectedDuration;
Debug.Log($"[SkillTiming] {phase}: skill={currentSkill?.SkillName ?? "<null>"}, clip={currentClipDebugName}, t={Time.time:F3}, elapsed={elapsed:F3}, expected={currentClipExpectedDuration:F3}, delta={delta:F3}");
}
/// <summary>
/// 적 스킬이 시전 중일 때 대상 추적 정책을 적용합니다.
/// </summary>
@@ -1460,29 +1522,46 @@ namespace Colosseum.Skills
/// <summary>
/// 현재 재생할 클립이 스킬 시작 블렌드 대상인지 반환합니다.
/// </summary>
private bool ShouldBlendIntoClip()
private float ResolveSkillEnterTransitionDuration()
{
if (isBossPatternBoundarySkill)
return false;
if (!shouldBlendIntoCurrentSkill)
return false;
return 0f;
if (isPlayingReleasePhase)
return false;
return 0f;
return currentClipSequenceIndex == 0 && currentIterationIndex == 1;
if (currentClipSequenceIndex != 0 || currentIterationIndex != 1)
return 0f;
if (!isBossPatternBoundarySkill)
return skillEnterTransitionDuration;
bool shouldBlendBossEntry = ShouldBlendBossPatternEntryFromCurrentState();
return shouldBlendBossEntry
? Mathf.Max(skillEnterTransitionDuration, bossPatternEnterTransitionDuration)
: 0f;
}
/// <summary>
/// 현재 스킬 진입에 사용할 전환 시간을 반환합니다.
/// 보스 패턴 첫 스킬이 이동 중 진입인지 판단합니다.
/// </summary>
private float GetSkillEnterTransitionDuration()
private bool ShouldBlendBossPatternEntryFromCurrentState()
{
if (isBossPatternBoundarySkill)
return bossPatternEnterTransitionDuration;
if (animator == null)
return false;
return skillEnterTransitionDuration;
if (animator.IsInTransition(0))
return true;
AnimatorStateInfo currentState = animator.GetCurrentAnimatorStateInfo(0);
int moveStateHash = Animator.StringToHash($"{BaseLayerName}.{MoveStateName}");
if (currentState.fullPathHash == moveStateHash)
return true;
UnityEngine.AI.NavMeshAgent agent = GetComponent<UnityEngine.AI.NavMeshAgent>();
return agent != null
&& agent.enabled
&& agent.velocity.sqrMagnitude > 0.0025f;
}
/// <summary>