feat: 방어 시스템과 드로그 검증 경로 정리
- 애니메이션 이벤트 기반 방어/유지/해제 흐름과 HUD 피드백, 방어 디버그 로그를 추가했다. - 드로그 기본기1 테스트 패턴을 정리하고 공격 판정을 OnEffect 기반으로 옮기며 드로그 범위 효과의 타겟 레이어를 정상화했다. - 플레이어 퀵슬롯 테스트 세팅과 적-플레이어 겹침 방지 로직을 조정해 충돌 시 적이 수평 이동을 멈추고 최소 분리만 수행하게 했다.
This commit is contained in:
@@ -26,6 +26,7 @@ namespace Colosseum.Skills
|
||||
Stun,
|
||||
Stagger,
|
||||
HitReaction,
|
||||
ResourceExhausted,
|
||||
Respawn,
|
||||
Revive,
|
||||
}
|
||||
@@ -90,14 +91,18 @@ namespace Colosseum.Skills
|
||||
private int currentIterationIndex = 0;
|
||||
private GameObject currentTargetOverride;
|
||||
private Vector3? currentGroundTargetPosition;
|
||||
private IReadOnlyList<AnimationClip> currentPhaseAnimationClips = Array.Empty<AnimationClip>();
|
||||
private bool isPlayingReleasePhase = false;
|
||||
|
||||
// 채널링 상태
|
||||
private bool isChannelingActive = false;
|
||||
private float channelElapsedTime = 0f;
|
||||
private float channelTickAccumulator = 0f;
|
||||
private GameObject channelVfxInstance;
|
||||
private readonly List<SkillEffect> currentChannelTickEffects = new();
|
||||
private readonly List<SkillEffect> currentChannelEndEffects = new();
|
||||
// 반복 유지 단계 상태
|
||||
private bool isLoopPhaseActive = false;
|
||||
private float loopElapsedTime = 0f;
|
||||
private float loopTickAccumulator = 0f;
|
||||
private GameObject loopVfxInstance;
|
||||
private readonly List<SkillEffect> currentLoopTickEffects = new();
|
||||
private readonly List<SkillEffect> currentLoopExitEffects = new();
|
||||
private readonly List<SkillEffect> currentReleaseStartEffects = new();
|
||||
private bool loopHoldRequested = false;
|
||||
|
||||
// 쿨타임 추적
|
||||
private Dictionary<SkillData, float> cooldownTracker = new Dictionary<SkillData, float>();
|
||||
@@ -114,7 +119,8 @@ namespace Colosseum.Skills
|
||||
public string LastCancelledSkillName => lastCancelledSkillName;
|
||||
public SkillExecutionResult LastExecutionResult => lastExecutionResult;
|
||||
public GameObject CurrentTargetOverride => currentTargetOverride;
|
||||
public bool IsChannelingActive => isChannelingActive;
|
||||
public bool IsChannelingActive => isLoopPhaseActive;
|
||||
public bool IsLoopPhaseActive => isLoopPhaseActive;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
@@ -251,10 +257,10 @@ namespace Colosseum.Skills
|
||||
|
||||
UpdateCastTargetTracking();
|
||||
|
||||
// 채널링 중일 때
|
||||
if (isChannelingActive)
|
||||
// 반복 유지 단계 중일 때
|
||||
if (isLoopPhaseActive)
|
||||
{
|
||||
UpdateChanneling();
|
||||
UpdateLoopPhase();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -267,18 +273,18 @@ namespace Colosseum.Skills
|
||||
if (TryPlayNextClipInSequence())
|
||||
return;
|
||||
|
||||
// 다음 반복 차수가 있으면 시작
|
||||
if (TryStartNextIteration())
|
||||
return;
|
||||
|
||||
// 채널링 스킬이면 채널링 시작
|
||||
if (currentSkill.IsChanneling)
|
||||
if (!isPlayingReleasePhase)
|
||||
{
|
||||
StartChanneling();
|
||||
return;
|
||||
// 다음 반복 차수가 있으면 시작
|
||||
if (TryStartNextIteration())
|
||||
return;
|
||||
|
||||
// 반복 유지 단계가 있으면 시작
|
||||
if (TryStartLoopPhase())
|
||||
return;
|
||||
}
|
||||
|
||||
// 모든 클립과 반복이 끝나면 종료
|
||||
// 모든 클립과 단계가 끝나면 종료
|
||||
if (debugMode) Debug.Log($"[Skill] Animation complete: {currentSkill.SkillName}");
|
||||
RestoreBaseController();
|
||||
CompleteCurrentSkillExecution(SkillExecutionResult.Completed);
|
||||
@@ -376,6 +382,7 @@ namespace Colosseum.Skills
|
||||
BuildResolvedEffects(currentLoadoutEntry);
|
||||
currentRepeatCount = currentLoadoutEntry.GetResolvedRepeatCount();
|
||||
currentIterationIndex = 0;
|
||||
loopHoldRequested = skill.RequiresLoopHold;
|
||||
|
||||
if (debugMode) Debug.Log($"[Skill] Cast: {skill.SkillName}");
|
||||
|
||||
@@ -492,6 +499,9 @@ namespace Colosseum.Skills
|
||||
currentTriggeredEffects.Clear();
|
||||
currentCastStartAbnormalities.Clear();
|
||||
currentTriggeredAbnormalities.Clear();
|
||||
currentLoopTickEffects.Clear();
|
||||
currentLoopExitEffects.Clear();
|
||||
currentReleaseStartEffects.Clear();
|
||||
|
||||
if (loadoutEntry == null)
|
||||
return;
|
||||
@@ -500,8 +510,9 @@ namespace Colosseum.Skills
|
||||
loadoutEntry.CollectTriggeredEffects(currentTriggeredEffects);
|
||||
loadoutEntry.CollectCastStartAbnormalities(currentCastStartAbnormalities);
|
||||
loadoutEntry.CollectTriggeredAbnormalities(currentTriggeredAbnormalities);
|
||||
loadoutEntry.CollectChannelTickEffects(currentChannelTickEffects);
|
||||
loadoutEntry.CollectChannelEndEffects(currentChannelEndEffects);
|
||||
loadoutEntry.CollectLoopTickEffects(currentLoopTickEffects);
|
||||
loadoutEntry.CollectLoopExitEffects(currentLoopExitEffects);
|
||||
loadoutEntry.CollectReleaseStartEffects(currentReleaseStartEffects);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -514,6 +525,8 @@ namespace Colosseum.Skills
|
||||
|
||||
currentIterationIndex++;
|
||||
currentClipSequenceIndex = 0;
|
||||
isPlayingReleasePhase = false;
|
||||
currentPhaseAnimationClips = currentSkill.AnimationClips;
|
||||
|
||||
if (debugMode && currentRepeatCount > 1)
|
||||
{
|
||||
@@ -522,13 +535,13 @@ namespace Colosseum.Skills
|
||||
|
||||
TriggerCastStartEffects();
|
||||
|
||||
if (currentSkill.AnimationClips.Count > 0 && animator != null)
|
||||
if (currentPhaseAnimationClips.Count > 0 && animator != null)
|
||||
{
|
||||
float resolvedAnimationSpeed = currentLoadoutEntry != null
|
||||
? currentLoadoutEntry.GetResolvedAnimationSpeed()
|
||||
: currentSkill.AnimationSpeed;
|
||||
animator.speed = resolvedAnimationSpeed;
|
||||
PlaySkillClip(currentSkill.AnimationClips[0]);
|
||||
PlaySkillClip(currentPhaseAnimationClips[0]);
|
||||
}
|
||||
|
||||
TriggerImmediateSelfEffectsIfNeeded();
|
||||
@@ -542,16 +555,19 @@ namespace Colosseum.Skills
|
||||
if (currentSkill == null)
|
||||
return false;
|
||||
|
||||
if (currentPhaseAnimationClips == null)
|
||||
return false;
|
||||
|
||||
int nextIndex = currentClipSequenceIndex + 1;
|
||||
if (nextIndex >= currentSkill.AnimationClips.Count)
|
||||
if (nextIndex >= currentPhaseAnimationClips.Count)
|
||||
return false;
|
||||
|
||||
currentClipSequenceIndex = nextIndex;
|
||||
PlaySkillClip(currentSkill.AnimationClips[currentClipSequenceIndex]);
|
||||
PlaySkillClip(currentPhaseAnimationClips[currentClipSequenceIndex]);
|
||||
|
||||
if (debugMode)
|
||||
{
|
||||
Debug.Log($"[Skill] Playing clip {currentClipSequenceIndex + 1}/{currentSkill.AnimationClips.Count}: {currentSkill.AnimationClips[currentClipSequenceIndex].name}");
|
||||
Debug.Log($"[Skill] Playing clip {currentClipSequenceIndex + 1}/{currentPhaseAnimationClips.Count}: {currentPhaseAnimationClips[currentClipSequenceIndex].name}");
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -725,6 +741,45 @@ namespace Colosseum.Skills
|
||||
if (debugMode) Debug.Log($"[Skill] End event received: {currentSkill.SkillName}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 애니메이션 이벤트에서 호출. 방어 상태를 시작합니다.
|
||||
/// </summary>
|
||||
public void OnDefenseStateEnter()
|
||||
{
|
||||
PlayerDefenseController defenseController = GetComponent<PlayerDefenseController>();
|
||||
defenseController?.EnterDefenseState();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 애니메이션 이벤트에서 호출. 방어 상태를 종료합니다.
|
||||
/// </summary>
|
||||
public void OnDefenseStateExit()
|
||||
{
|
||||
PlayerDefenseController defenseController = GetComponent<PlayerDefenseController>();
|
||||
defenseController?.ExitDefenseState();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 애니메이션 이벤트에서 호출. 방어 유지 자원 소모를 시작합니다.
|
||||
/// </summary>
|
||||
public void OnDefenseSustainEnter()
|
||||
{
|
||||
PlayerDefenseSustainController sustainController = GetComponent<PlayerDefenseSustainController>();
|
||||
if (sustainController == null)
|
||||
sustainController = gameObject.AddComponent<PlayerDefenseSustainController>();
|
||||
|
||||
sustainController.BeginSustain();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 애니메이션 이벤트에서 호출. 방어 유지 자원 소모를 종료합니다.
|
||||
/// </summary>
|
||||
public void OnDefenseSustainExit()
|
||||
{
|
||||
PlayerDefenseSustainController sustainController = GetComponent<PlayerDefenseSustainController>();
|
||||
sustainController?.EndSustain();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 스킬을 강제 취소합니다.
|
||||
/// </summary>
|
||||
@@ -743,6 +798,33 @@ namespace Colosseum.Skills
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 서버에서 현재 스킬 취소를 확정하고 클라이언트에 동기화합니다.
|
||||
/// </summary>
|
||||
public bool CancelSkillFromServer(SkillCancelReason reason)
|
||||
{
|
||||
bool cancelled = CancelSkill(reason);
|
||||
if (!cancelled)
|
||||
return false;
|
||||
|
||||
if (NetworkManager.Singleton != null && NetworkManager.Singleton.IsServer)
|
||||
{
|
||||
SyncCancelledSkillClientRpc((int)reason);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
[Rpc(SendTo.NotServer)]
|
||||
private void SyncCancelledSkillClientRpc(int reasonValue)
|
||||
{
|
||||
SkillCancelReason reason = System.Enum.IsDefined(typeof(SkillCancelReason), reasonValue)
|
||||
? (SkillCancelReason)reasonValue
|
||||
: SkillCancelReason.Manual;
|
||||
|
||||
CancelSkill(reason);
|
||||
}
|
||||
|
||||
public bool IsOnCooldown(SkillData skill)
|
||||
{
|
||||
if (!cooldownTracker.ContainsKey(skill))
|
||||
@@ -776,49 +858,64 @@ namespace Colosseum.Skills
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 채널링을 시작합니다. 캐스트 애니메이션 종료 후 호출됩니다.
|
||||
/// 반복 유지 단계를 시작합니다. 캐스트 애니메이션 종료 후 호출됩니다.
|
||||
/// </summary>
|
||||
private void StartChanneling()
|
||||
private bool TryStartLoopPhase()
|
||||
{
|
||||
if (currentSkill == null || !currentSkill.IsChanneling)
|
||||
return;
|
||||
if (currentSkill == null || !currentSkill.HasLoopPhase)
|
||||
return false;
|
||||
|
||||
isChannelingActive = true;
|
||||
channelElapsedTime = 0f;
|
||||
channelTickAccumulator = 0f;
|
||||
if (currentSkill.RequiresLoopHold && !loopHoldRequested)
|
||||
{
|
||||
if (debugMode)
|
||||
Debug.Log($"[Skill] 반복 유지 진입 전 버튼 해제됨: {currentSkill.SkillName}");
|
||||
|
||||
SpawnChannelVfx();
|
||||
if (TryStartReleasePhase())
|
||||
return true;
|
||||
|
||||
RestoreBaseController();
|
||||
CompleteCurrentSkillExecution(SkillExecutionResult.Cancelled);
|
||||
return true;
|
||||
}
|
||||
|
||||
isLoopPhaseActive = true;
|
||||
loopElapsedTime = 0f;
|
||||
loopTickAccumulator = 0f;
|
||||
|
||||
SpawnLoopVfx();
|
||||
|
||||
if (debugMode)
|
||||
Debug.Log($"[Skill] 채널링 시작: {currentSkill.SkillName} (duration={currentSkill.ChannelDuration}s, tick={currentSkill.ChannelTickInterval}s)");
|
||||
Debug.Log($"[Skill] 반복 유지 시작: {currentSkill.SkillName} (duration={currentSkill.LoopMaxDuration}s, tick={currentSkill.LoopTickInterval}s)");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 채널링 VFX를 시전자 위치에 생성합니다.
|
||||
/// 반복 유지 VFX를 시전자 위치에 생성합니다.
|
||||
/// </summary>
|
||||
private void SpawnChannelVfx()
|
||||
private void SpawnLoopVfx()
|
||||
{
|
||||
if (currentSkill == null || currentSkill.ChannelVfxPrefab == null)
|
||||
if (currentSkill == null || currentSkill.LoopVfxPrefab == null)
|
||||
return;
|
||||
|
||||
Transform mount = ResolveChannelVfxMount();
|
||||
Transform mount = ResolveLoopVfxMount();
|
||||
Vector3 spawnPos = mount != null ? mount.position : transform.position;
|
||||
|
||||
channelVfxInstance = UnityEngine.Object.Instantiate(
|
||||
currentSkill.ChannelVfxPrefab,
|
||||
loopVfxInstance = UnityEngine.Object.Instantiate(
|
||||
currentSkill.LoopVfxPrefab,
|
||||
spawnPos,
|
||||
transform.rotation);
|
||||
|
||||
if (mount != null)
|
||||
channelVfxInstance.transform.SetParent(mount);
|
||||
loopVfxInstance.transform.SetParent(mount);
|
||||
|
||||
channelVfxInstance.transform.localScale = new Vector3(
|
||||
currentSkill.ChannelVfxWidthScale,
|
||||
currentSkill.ChannelVfxWidthScale,
|
||||
currentSkill.ChannelVfxLengthScale);
|
||||
loopVfxInstance.transform.localScale = new Vector3(
|
||||
currentSkill.LoopVfxWidthScale,
|
||||
currentSkill.LoopVfxWidthScale,
|
||||
currentSkill.LoopVfxLengthScale);
|
||||
|
||||
// 모든 파티클을 루핑 모드로 설정
|
||||
ForceLoopParticleSystems(channelVfxInstance);
|
||||
ForceLoopParticleSystems(loopVfxInstance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -842,24 +939,24 @@ namespace Colosseum.Skills
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// channelVfxMountPath에서 VFX 장착 위치를 찾습니다.
|
||||
/// loopVfxMountPath에서 VFX 장착 위치를 찾습니다.
|
||||
/// </summary>
|
||||
private Transform ResolveChannelVfxMount()
|
||||
private Transform ResolveLoopVfxMount()
|
||||
{
|
||||
if (currentSkill == null || string.IsNullOrEmpty(currentSkill.ChannelVfxMountPath))
|
||||
if (currentSkill == null || string.IsNullOrEmpty(currentSkill.LoopVfxMountPath))
|
||||
return null;
|
||||
|
||||
// Animator 하위에서 이름으로 재귀 검색
|
||||
Animator animator = GetComponentInChildren<Animator>();
|
||||
if (animator != null)
|
||||
{
|
||||
Transform found = FindTransformRecursive(animator.transform, currentSkill.ChannelVfxMountPath);
|
||||
Transform found = FindTransformRecursive(animator.transform, currentSkill.LoopVfxMountPath);
|
||||
if (found != null)
|
||||
return found;
|
||||
}
|
||||
|
||||
// 자식 GameObject에서 경로 검색
|
||||
return transform.Find(currentSkill.ChannelVfxMountPath);
|
||||
return transform.Find(currentSkill.LoopVfxMountPath);
|
||||
}
|
||||
|
||||
private static Transform FindTransformRecursive(Transform parent, string name)
|
||||
@@ -878,60 +975,60 @@ namespace Colosseum.Skills
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 채널링 VFX를 파괴합니다.
|
||||
/// 반복 유지 VFX를 파괴합니다.
|
||||
/// </summary>
|
||||
private void DestroyChannelVfx()
|
||||
private void DestroyLoopVfx()
|
||||
{
|
||||
if (channelVfxInstance != null)
|
||||
if (loopVfxInstance != null)
|
||||
{
|
||||
UnityEngine.Object.Destroy(channelVfxInstance);
|
||||
channelVfxInstance = null;
|
||||
UnityEngine.Object.Destroy(loopVfxInstance);
|
||||
loopVfxInstance = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 채널링을 매 프레임 업데이트합니다. 틱 효과를 주기적으로 발동합니다.
|
||||
/// 반복 유지 단계를 매 프레임 업데이트합니다. 틱 효과를 주기적으로 발동합니다.
|
||||
/// </summary>
|
||||
private void UpdateChanneling()
|
||||
private void UpdateLoopPhase()
|
||||
{
|
||||
if (!isChannelingActive || currentSkill == null)
|
||||
if (!isLoopPhaseActive || currentSkill == null)
|
||||
return;
|
||||
|
||||
channelElapsedTime += Time.deltaTime;
|
||||
channelTickAccumulator += Time.deltaTime;
|
||||
loopElapsedTime += Time.deltaTime;
|
||||
loopTickAccumulator += Time.deltaTime;
|
||||
|
||||
// 틱 효과 발동
|
||||
float tickInterval = currentSkill.ChannelTickInterval;
|
||||
while (channelTickAccumulator >= tickInterval)
|
||||
float tickInterval = currentSkill.LoopTickInterval;
|
||||
while (loopTickAccumulator >= tickInterval)
|
||||
{
|
||||
channelTickAccumulator -= tickInterval;
|
||||
TriggerChannelTick();
|
||||
loopTickAccumulator -= tickInterval;
|
||||
TriggerLoopTick();
|
||||
}
|
||||
|
||||
// 지속 시간 초과 → 채널링 종료
|
||||
if (channelElapsedTime >= currentSkill.ChannelDuration)
|
||||
// 지속 시간 초과 → 반복 유지 종료
|
||||
if (currentSkill.UsesLoopMaxDuration && loopElapsedTime >= currentSkill.LoopMaxDuration)
|
||||
{
|
||||
if (debugMode)
|
||||
Debug.Log($"[Skill] 채널링 지속 시간 만료: {currentSkill.SkillName}");
|
||||
EndChanneling();
|
||||
Debug.Log($"[Skill] 반복 유지 지속 시간 만료: {currentSkill.SkillName}");
|
||||
EndLoopPhase();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 채널링 틱 효과를 발동합니다.
|
||||
/// 반복 유지 틱 효과를 발동합니다.
|
||||
/// </summary>
|
||||
private void TriggerChannelTick()
|
||||
private void TriggerLoopTick()
|
||||
{
|
||||
if (currentChannelTickEffects.Count == 0)
|
||||
if (currentLoopTickEffects.Count == 0)
|
||||
return;
|
||||
|
||||
if (debugMode)
|
||||
Debug.Log($"[Skill] 채널링 틱 발동: {currentSkill.SkillName} (elapsed={channelElapsedTime:F1}s)");
|
||||
Debug.Log($"[Skill] 반복 유지 틱 발동: {currentSkill.SkillName} (elapsed={loopElapsedTime:F1}s)");
|
||||
|
||||
// VFX는 모든 클라이언트에서 로컬 실행
|
||||
for (int i = 0; i < currentChannelTickEffects.Count; i++)
|
||||
for (int i = 0; i < currentLoopTickEffects.Count; i++)
|
||||
{
|
||||
SkillEffect effect = currentChannelTickEffects[i];
|
||||
SkillEffect effect = currentLoopTickEffects[i];
|
||||
if (effect != null && effect.IsVisualOnly)
|
||||
{
|
||||
effect.ExecuteOnCast(gameObject, currentTargetOverride, currentGroundTargetPosition);
|
||||
@@ -942,14 +1039,14 @@ namespace Colosseum.Skills
|
||||
if (NetworkManager.Singleton != null && !NetworkManager.Singleton.IsServer)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < currentChannelTickEffects.Count; i++)
|
||||
for (int i = 0; i < currentLoopTickEffects.Count; i++)
|
||||
{
|
||||
SkillEffect effect = currentChannelTickEffects[i];
|
||||
SkillEffect effect = currentLoopTickEffects[i];
|
||||
if (effect == null || effect.IsVisualOnly)
|
||||
continue;
|
||||
|
||||
if (debugMode)
|
||||
Debug.Log($"[Skill] 채널링 틱 효과: {effect.name}");
|
||||
Debug.Log($"[Skill] 반복 유지 틱 효과: {effect.name}");
|
||||
|
||||
if (showAreaDebug)
|
||||
effect.DrawDebugRange(gameObject, debugDrawDuration, currentGroundTargetPosition);
|
||||
@@ -959,40 +1056,43 @@ namespace Colosseum.Skills
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 채널링을 종료합니다. 종료 효과를 발동하고 스킬 상태를 정리합니다.
|
||||
/// 반복 유지 단계를 종료합니다. 종료 효과를 발동하고 다음 단계를 시작합니다.
|
||||
/// </summary>
|
||||
private void EndChanneling()
|
||||
private void EndLoopPhase()
|
||||
{
|
||||
if (!isChannelingActive)
|
||||
if (!isLoopPhaseActive)
|
||||
return;
|
||||
|
||||
// 채널링 종료 효과 발동
|
||||
TriggerChannelEndEffects();
|
||||
DestroyChannelVfx();
|
||||
// 반복 유지 종료 효과 발동
|
||||
TriggerLoopExitEffects();
|
||||
DestroyLoopVfx();
|
||||
|
||||
isChannelingActive = false;
|
||||
channelElapsedTime = 0f;
|
||||
channelTickAccumulator = 0f;
|
||||
isLoopPhaseActive = false;
|
||||
loopElapsedTime = 0f;
|
||||
loopTickAccumulator = 0f;
|
||||
|
||||
if (debugMode)
|
||||
Debug.Log($"[Skill] 채널링 종료: {currentSkill?.SkillName}");
|
||||
Debug.Log($"[Skill] 반복 유지 종료: {currentSkill?.SkillName}");
|
||||
|
||||
if (TryStartReleasePhase())
|
||||
return;
|
||||
|
||||
RestoreBaseController();
|
||||
CompleteCurrentSkillExecution(SkillExecutionResult.Completed);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 채널링 종료 효과를 발동합니다.
|
||||
/// 반복 유지 종료 효과를 발동합니다.
|
||||
/// </summary>
|
||||
private void TriggerChannelEndEffects()
|
||||
private void TriggerLoopExitEffects()
|
||||
{
|
||||
if (currentChannelEndEffects.Count == 0)
|
||||
if (currentLoopExitEffects.Count == 0)
|
||||
return;
|
||||
|
||||
// VFX는 모든 클라이언트에서 로컬 실행
|
||||
for (int i = 0; i < currentChannelEndEffects.Count; i++)
|
||||
for (int i = 0; i < currentLoopExitEffects.Count; i++)
|
||||
{
|
||||
SkillEffect effect = currentChannelEndEffects[i];
|
||||
SkillEffect effect = currentLoopExitEffects[i];
|
||||
if (effect != null && effect.IsVisualOnly)
|
||||
{
|
||||
effect.ExecuteOnCast(gameObject, currentTargetOverride, currentGroundTargetPosition);
|
||||
@@ -1003,32 +1103,104 @@ namespace Colosseum.Skills
|
||||
if (NetworkManager.Singleton != null && !NetworkManager.Singleton.IsServer)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < currentChannelEndEffects.Count; i++)
|
||||
for (int i = 0; i < currentLoopExitEffects.Count; i++)
|
||||
{
|
||||
SkillEffect effect = currentChannelEndEffects[i];
|
||||
SkillEffect effect = currentLoopExitEffects[i];
|
||||
if (effect == null || effect.IsVisualOnly)
|
||||
continue;
|
||||
|
||||
if (debugMode)
|
||||
Debug.Log($"[Skill] 채널링 종료 효과: {effect.name}");
|
||||
Debug.Log($"[Skill] 반복 유지 종료 효과: {effect.name}");
|
||||
|
||||
effect.ExecuteOnCast(gameObject, currentTargetOverride, currentGroundTargetPosition);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 플레이어가 버튼을 놓았을 때 채널링을 중단합니다.
|
||||
/// 해제 단계를 시작합니다.
|
||||
/// </summary>
|
||||
private bool TryStartReleasePhase()
|
||||
{
|
||||
if (currentSkill == null || !currentSkill.HasReleasePhase)
|
||||
return false;
|
||||
|
||||
currentClipSequenceIndex = 0;
|
||||
isPlayingReleasePhase = true;
|
||||
currentPhaseAnimationClips = currentSkill.ReleaseAnimationClips;
|
||||
|
||||
TriggerReleaseStartEffects();
|
||||
|
||||
if (currentPhaseAnimationClips.Count <= 0 || animator == null)
|
||||
return false;
|
||||
|
||||
float resolvedAnimationSpeed = currentLoadoutEntry != null
|
||||
? currentLoadoutEntry.GetResolvedAnimationSpeed()
|
||||
: currentSkill.AnimationSpeed;
|
||||
animator.speed = resolvedAnimationSpeed;
|
||||
PlaySkillClip(currentPhaseAnimationClips[0]);
|
||||
|
||||
if (debugMode)
|
||||
Debug.Log($"[Skill] 해제 단계 시작: {currentSkill.SkillName}");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 해제 단계 시작 효과를 발동합니다.
|
||||
/// </summary>
|
||||
private void TriggerReleaseStartEffects()
|
||||
{
|
||||
if (currentReleaseStartEffects.Count == 0)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < currentReleaseStartEffects.Count; i++)
|
||||
{
|
||||
SkillEffect effect = currentReleaseStartEffects[i];
|
||||
if (effect != null && effect.IsVisualOnly)
|
||||
{
|
||||
effect.ExecuteOnCast(gameObject, currentTargetOverride, currentGroundTargetPosition);
|
||||
}
|
||||
}
|
||||
|
||||
if (NetworkManager.Singleton != null && !NetworkManager.Singleton.IsServer)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < currentReleaseStartEffects.Count; i++)
|
||||
{
|
||||
SkillEffect effect = currentReleaseStartEffects[i];
|
||||
if (effect == null || effect.IsVisualOnly)
|
||||
continue;
|
||||
|
||||
effect.ExecuteOnCast(gameObject, currentTargetOverride, currentGroundTargetPosition);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 플레이어가 버튼을 놓았을 때 반복 유지 단계를 중단합니다.
|
||||
/// PlayerSkillInput에서 호출됩니다.
|
||||
/// </summary>
|
||||
public void NotifyChannelHoldReleased()
|
||||
public void NotifyLoopHoldReleased()
|
||||
{
|
||||
if (!isChannelingActive)
|
||||
if (currentSkill == null || !currentSkill.RequiresLoopHold)
|
||||
return;
|
||||
|
||||
loopHoldRequested = false;
|
||||
|
||||
if (!isLoopPhaseActive)
|
||||
return;
|
||||
|
||||
if (debugMode)
|
||||
Debug.Log($"[Skill] 채널링 버튼 해제로 중단: {currentSkill?.SkillName}");
|
||||
Debug.Log($"[Skill] 반복 유지 버튼 해제로 중단: {currentSkill?.SkillName}");
|
||||
|
||||
EndChanneling();
|
||||
EndLoopPhase();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 레거시 채널링 입력 해제 경로 호환 메서드입니다.
|
||||
/// </summary>
|
||||
public void NotifyChannelHoldReleased()
|
||||
{
|
||||
NotifyLoopHoldReleased();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1043,17 +1215,21 @@ namespace Colosseum.Skills
|
||||
currentCastStartAbnormalities.Clear();
|
||||
currentTriggeredAbnormalities.Clear();
|
||||
currentTriggeredTargetsBuffer.Clear();
|
||||
currentChannelTickEffects.Clear();
|
||||
currentChannelEndEffects.Clear();
|
||||
isChannelingActive = false;
|
||||
channelElapsedTime = 0f;
|
||||
channelTickAccumulator = 0f;
|
||||
DestroyChannelVfx();
|
||||
currentLoopTickEffects.Clear();
|
||||
currentLoopExitEffects.Clear();
|
||||
currentReleaseStartEffects.Clear();
|
||||
isLoopPhaseActive = false;
|
||||
loopElapsedTime = 0f;
|
||||
loopTickAccumulator = 0f;
|
||||
DestroyLoopVfx();
|
||||
currentTargetOverride = null;
|
||||
currentGroundTargetPosition = null;
|
||||
currentPhaseAnimationClips = Array.Empty<AnimationClip>();
|
||||
isPlayingReleasePhase = false;
|
||||
currentClipSequenceIndex = 0;
|
||||
currentRepeatCount = 1;
|
||||
currentIterationIndex = 0;
|
||||
loopHoldRequested = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1062,9 +1238,19 @@ namespace Colosseum.Skills
|
||||
private void CompleteCurrentSkillExecution(SkillExecutionResult result)
|
||||
{
|
||||
lastExecutionResult = result;
|
||||
NotifyDefenseStateEnded();
|
||||
ClearCurrentSkillState();
|
||||
}
|
||||
|
||||
private void NotifyDefenseStateEnded()
|
||||
{
|
||||
PlayerDefenseController defenseController = GetComponent<PlayerDefenseController>();
|
||||
defenseController?.HandleSkillExecutionEnded();
|
||||
|
||||
PlayerDefenseSustainController sustainController = GetComponent<PlayerDefenseSustainController>();
|
||||
sustainController?.HandleSkillExecutionEnded();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 적 스킬이 시전 중일 때 대상 추적 정책을 적용합니다.
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user