feat: 젬 반복 시전 로직 및 테스트 프리셋 추가

- SkillGemData에 카테고리, 시전 속도 배율, 추가 반복 횟수 필드를 추가함
- SkillLoadoutEntry가 젬 합산 기준 최종 속도와 반복 횟수를 계산하도록 확장함
- SkillController가 반복 횟수만큼 스킬을 재시전하고 시작 효과와 OnEffect를 매 반복에 다시 적용하도록 수정함
- 연속 젬과 반복 젬 테스트 프리셋을 추가하고 디버그 메뉴에 적용 및 계산 로그 경로를 보강함
- 공격형 테스트 젬 자산과 추가 대미지 이펙트를 정리하고 무젬 35, 반복 젬 70 피해를 검증함
This commit is contained in:
2026-03-26 12:36:03 +09:00
parent dedfb60a4c
commit b4475ea77f
21 changed files with 726 additions and 91 deletions

View File

@@ -56,14 +56,15 @@ namespace Colosseum.Skills
private SkillLoadoutEntry currentLoadoutEntry;
private readonly List<SkillEffect> currentCastStartEffects = new();
private readonly Dictionary<int, List<SkillEffect>> currentTriggeredEffects = new();
private bool skillEndRequested; // OnSkillEnd 이벤트 호출 여부
private bool waitingForEndAnimation; // EndAnimation 종료 대기 중
private int currentRepeatCount = 1;
private int currentIterationIndex = 0;
// 쿨타임 추적
private Dictionary<SkillData, float> cooldownTracker = new Dictionary<SkillData, float>();
public bool IsExecutingSkill => currentSkill != null && !skillEndRequested;
public bool IsExecutingSkill => currentSkill != null;
public bool IsPlayingAnimation => currentSkill != null;
public bool IsInEndAnimation => waitingForEndAnimation;
public bool UsesRootMotion => currentSkill != null && currentSkill.UseRootMotion;
@@ -105,7 +106,7 @@ namespace Colosseum.Skills
{
if (debugMode) Debug.Log($"[Skill] EndAnimation complete: {currentSkill.SkillName}");
RestoreBaseController();
currentSkill = null;
ClearCurrentSkillState();
}
return;
}
@@ -113,6 +114,9 @@ namespace Colosseum.Skills
// 애니메이션 종료 시 처리 (OnSkillEnd 여부와 관계없이 애니메이션 끝까지 재생)
if (stateInfo.normalizedTime >= 1f)
{
if (TryStartNextIteration())
return;
if (currentSkill.EndClip != null)
{
// EndAnimation 재생 후 종료 대기
@@ -125,7 +129,7 @@ namespace Colosseum.Skills
// EndAnimation 없으면 바로 종료
if (debugMode) Debug.Log($"[Skill] Animation complete: {currentSkill.SkillName}");
RestoreBaseController();
currentSkill = null;
ClearCurrentSkillState();
}
}
}
@@ -172,26 +176,18 @@ namespace Colosseum.Skills
currentLoadoutEntry = loadoutEntry != null ? loadoutEntry.CreateCopy() : SkillLoadoutEntry.CreateTemporary(skill);
currentSkill = skill;
skillEndRequested = false;
waitingForEndAnimation = false;
lastCancelReason = SkillCancelReason.None;
BuildResolvedEffects(currentLoadoutEntry);
currentRepeatCount = currentLoadoutEntry.GetResolvedRepeatCount();
currentIterationIndex = 0;
if (debugMode) Debug.Log($"[Skill] Cast: {skill.SkillName}");
// 쿨타임 시작
StartCooldown(skill, currentLoadoutEntry.GetResolvedCooldown());
TriggerCastStartEffects();
// 스킬 애니메이션 재생
if (skill.SkillClip != null && animator != null)
{
animator.speed = skill.AnimationSpeed;
PlaySkillClip(skill.SkillClip);
}
TriggerImmediateSelfEffectsIfNeeded();
StartCurrentIteration();
return true;
}
@@ -263,6 +259,51 @@ namespace Colosseum.Skills
loadoutEntry.CollectTriggeredEffects(currentTriggeredEffects);
}
/// <summary>
/// 현재 스킬의 반복 차수 하나를 시작합니다.
/// </summary>
private void StartCurrentIteration()
{
if (currentSkill == null)
return;
currentIterationIndex++;
waitingForEndAnimation = false;
if (debugMode && currentRepeatCount > 1)
{
Debug.Log($"[Skill] Iteration {currentIterationIndex}/{currentRepeatCount}: {currentSkill.SkillName}");
}
TriggerCastStartEffects();
if (currentSkill.SkillClip != null && animator != null)
{
float resolvedAnimationSpeed = currentLoadoutEntry != null
? currentLoadoutEntry.GetResolvedAnimationSpeed()
: currentSkill.AnimationSpeed;
animator.speed = resolvedAnimationSpeed;
PlaySkillClip(currentSkill.SkillClip);
}
TriggerImmediateSelfEffectsIfNeeded();
}
/// <summary>
/// 반복 시전이 남아 있으면 다음 차수를 시작합니다.
/// </summary>
private bool TryStartNextIteration()
{
if (currentSkill == null)
return false;
if (currentIterationIndex >= currentRepeatCount)
return false;
StartCurrentIteration();
return true;
}
/// <summary>
/// 스킬 클립으로 Override Controller 생성 후 재생
/// </summary>
@@ -426,9 +467,7 @@ namespace Colosseum.Skills
return;
}
skillEndRequested = true;
if (debugMode) Debug.Log($"[Skill] End requested: {currentSkill.SkillName} (will complete after animation)");
if (debugMode) Debug.Log($"[Skill] End event received: {currentSkill.SkillName}");
}
/// <summary>
@@ -445,12 +484,7 @@ namespace Colosseum.Skills
Debug.Log($"[Skill] Cancelled: {currentSkill.SkillName} / reason={reason}");
RestoreBaseController();
currentSkill = null;
currentLoadoutEntry = null;
currentCastStartEffects.Clear();
currentTriggeredEffects.Clear();
skillEndRequested = false;
waitingForEndAnimation = false;
ClearCurrentSkillState();
return true;
}
@@ -485,5 +519,19 @@ namespace Colosseum.Skills
{
cooldownTracker.Clear();
}
/// <summary>
/// 현재 실행 중인 스킬 상태를 정리합니다.
/// </summary>
private void ClearCurrentSkillState()
{
currentSkill = null;
currentLoadoutEntry = null;
currentCastStartEffects.Clear();
currentTriggeredEffects.Clear();
waitingForEndAnimation = false;
currentRepeatCount = 1;
currentIterationIndex = 0;
}
}
}

View File

@@ -5,6 +5,20 @@ using UnityEngine;
namespace Colosseum.Skills
{
/// <summary>
/// 젬의 주 역할 분류입니다.
/// </summary>
public enum SkillGemCategory
{
Common,
Attack,
Threat,
Defense,
Support,
Control,
Efficiency,
}
/// <summary>
/// 젬 효과가 발동될 애니메이션 이벤트 인덱스와 효과 목록입니다.
/// </summary>
@@ -31,12 +45,18 @@ namespace Colosseum.Skills
[TextArea(2, 4)]
[SerializeField] private string description;
[SerializeField] private Sprite icon;
[Tooltip("젬의 주 역할 분류")]
[SerializeField] private SkillGemCategory category = SkillGemCategory.Common;
[Header("기본 수치 보정")]
[Tooltip("장착 시 마나 비용 배율")]
[Min(0f)] [SerializeField] private float manaCostMultiplier = 1f;
[Tooltip("장착 시 쿨타임 배율")]
[Min(0f)] [SerializeField] private float cooldownMultiplier = 1f;
[Tooltip("장착 시 스킬 애니메이션 재생 속도 배율")]
[Min(0.1f)] [SerializeField] private float castSpeedMultiplier = 1f;
[Tooltip("기반 스킬 시전을 몇 회 더 반복할지 정의합니다. 현재는 계산/표시용으로만 사용됩니다.")]
[Min(0)] [SerializeField] private int additionalRepeatCount = 0;
[Header("추가 효과")]
[Tooltip("시전 시작 시 즉시 발동하는 추가 효과")]
@@ -47,8 +67,11 @@ namespace Colosseum.Skills
public string GemName => gemName;
public string Description => description;
public Sprite Icon => icon;
public SkillGemCategory Category => category;
public float ManaCostMultiplier => manaCostMultiplier;
public float CooldownMultiplier => cooldownMultiplier;
public float CastSpeedMultiplier => castSpeedMultiplier;
public int AdditionalRepeatCount => additionalRepeatCount;
public IReadOnlyList<SkillEffect> CastStartEffects => castStartEffects;
public IReadOnlyList<SkillGemTriggeredEffectEntry> TriggeredEffects => triggeredEffects;
}

View File

@@ -135,6 +135,48 @@ namespace Colosseum.Skills
return resolved;
}
public float GetResolvedAnimationSpeed()
{
if (baseSkill == null)
return 0f;
float resolved = baseSkill.AnimationSpeed;
if (socketedGems == null)
return resolved;
for (int i = 0; i < socketedGems.Length; i++)
{
SkillGemData gem = socketedGems[i];
if (gem == null)
continue;
resolved *= gem.CastSpeedMultiplier;
}
return Mathf.Max(0.05f, resolved);
}
public int GetResolvedRepeatCount()
{
if (baseSkill == null)
return 0;
int resolved = 1;
if (socketedGems == null)
return resolved;
for (int i = 0; i < socketedGems.Length; i++)
{
SkillGemData gem = socketedGems[i];
if (gem == null)
continue;
resolved += gem.AdditionalRepeatCount;
}
return Mathf.Max(1, resolved);
}
public void CollectCastStartEffects(List<SkillEffect> destination)
{
if (destination == null)