feat: 젬 테스트 경로 및 보스 기절 디버그 추가

- 다중 젬 슬롯용 타입을 별도 스크립트로 분리하고 테스트 젬/로드아웃 자산 생성 경로를 정리

- 젬 테스트 전용 공격 스킬과 분리된 애니메이션 자산을 추가해 베이스 스킬 검증 경로를 마련

- PlayerSkillDebugMenu와 MPP 디버그 메뉴를 보강해 젬 프리셋 적용, 원격 테스트, 보스 기절 디버그 메뉴를 추가

- BossCombatBehaviorContext와 공통 BT 액션이 기절 상태를 존중하도록 수정해 보스 추적과 패턴 실행을 중단

- Unity 리프레시와 외부 빌드 통과를 확인하고 드로그전 및 MPP 기준 젬 프리셋 적용 흐름을 검증
This commit is contained in:
2026-03-25 18:38:12 +09:00
parent 35a5b272cb
commit 24b284ad7e
39 changed files with 4443 additions and 463 deletions

View File

@@ -53,6 +53,9 @@ namespace Colosseum.Skills
// 현재 실행 중인 스킬
private SkillData currentSkill;
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 종료 대기 중
@@ -66,6 +69,7 @@ namespace Colosseum.Skills
public bool UsesRootMotion => currentSkill != null && currentSkill.UseRootMotion;
public bool IgnoreRootMotionY => currentSkill != null && currentSkill.IgnoreRootMotionY;
public SkillData CurrentSkill => currentSkill;
public SkillLoadoutEntry CurrentLoadoutEntry => currentLoadoutEntry;
public Animator Animator => animator;
public SkillCancelReason LastCancelReason => lastCancelReason;
public string LastCancelledSkillName => lastCancelledSkillName;
@@ -131,6 +135,15 @@ namespace Colosseum.Skills
/// </summary>
public bool ExecuteSkill(SkillData skill)
{
return ExecuteSkill(SkillLoadoutEntry.CreateTemporary(skill));
}
/// <summary>
/// 슬롯 엔트리 기준으로 스킬 시전
/// </summary>
public bool ExecuteSkill(SkillLoadoutEntry loadoutEntry)
{
SkillData skill = loadoutEntry != null ? loadoutEntry.BaseSkill : null;
if (skill == null)
{
Debug.LogWarning("Skill is null!");
@@ -157,17 +170,19 @@ namespace Colosseum.Skills
return false;
}
currentLoadoutEntry = loadoutEntry != null ? loadoutEntry.CreateCopy() : SkillLoadoutEntry.CreateTemporary(skill);
currentSkill = skill;
skillEndRequested = false;
waitingForEndAnimation = false;
lastCancelReason = SkillCancelReason.None;
BuildResolvedEffects(currentLoadoutEntry);
if (debugMode) Debug.Log($"[Skill] Cast: {skill.SkillName}");
// 쿨타임 시작
StartCooldown(skill);
StartCooldown(skill, currentLoadoutEntry.GetResolvedCooldown());
TriggerCastStartEffects(skill);
TriggerCastStartEffects();
// 스킬 애니메이션 재생
if (skill.SkillClip != null && animator != null)
@@ -176,7 +191,7 @@ namespace Colosseum.Skills
PlaySkillClip(skill.SkillClip);
}
TriggerImmediateSelfEffectsIfNeeded(skill);
TriggerImmediateSelfEffectsIfNeeded();
return true;
}
@@ -185,17 +200,17 @@ namespace Colosseum.Skills
/// 시전 시작 즉시 발동하는 효과를 실행합니다.
/// 서버 권한으로만 처리해 실제 게임플레이 효과가 한 번만 적용되게 합니다.
/// </summary>
private void TriggerCastStartEffects(SkillData skill)
private void TriggerCastStartEffects()
{
if (skill == null || skill.CastStartEffects == null || skill.CastStartEffects.Count == 0)
if (currentSkill == null || currentCastStartEffects.Count == 0)
return;
if (NetworkManager.Singleton != null && !NetworkManager.Singleton.IsServer)
return;
for (int i = 0; i < skill.CastStartEffects.Count; i++)
for (int i = 0; i < currentCastStartEffects.Count; i++)
{
SkillEffect effect = skill.CastStartEffects[i];
SkillEffect effect = currentCastStartEffects[i];
if (effect == null)
continue;
@@ -208,20 +223,23 @@ namespace Colosseum.Skills
/// 애니메이션 이벤트가 없는 자기 자신 대상 효과는 시전 즉시 발동합니다.
/// 버프/무적 같은 자기 강화 스킬이 이벤트 누락으로 동작하지 않는 상황을 막기 위한 보정입니다.
/// </summary>
private void TriggerImmediateSelfEffectsIfNeeded(SkillData skill)
private void TriggerImmediateSelfEffectsIfNeeded()
{
if (skill == null || skill.Effects == null || skill.Effects.Count == 0)
if (currentSkill == null || currentTriggeredEffects.Count == 0)
return;
if (NetworkManager.Singleton != null && !NetworkManager.Singleton.IsServer)
return;
if (skill.SkillClip != null && skill.SkillClip.events != null && skill.SkillClip.events.Length > 0)
if (currentSkill.SkillClip != null && currentSkill.SkillClip.events != null && currentSkill.SkillClip.events.Length > 0)
return;
for (int i = 0; i < skill.Effects.Count; i++)
if (!currentTriggeredEffects.TryGetValue(0, out List<SkillEffect> effectsAtZero))
return;
for (int i = 0; i < effectsAtZero.Count; i++)
{
SkillEffect effect = skill.Effects[i];
SkillEffect effect = effectsAtZero[i];
if (effect == null || effect.TargetType != TargetType.Self)
continue;
@@ -230,6 +248,21 @@ namespace Colosseum.Skills
}
}
/// <summary>
/// 현재 슬롯 엔트리 기준으로 시전 시작/트리거 효과를 합성합니다.
/// </summary>
private void BuildResolvedEffects(SkillLoadoutEntry loadoutEntry)
{
currentCastStartEffects.Clear();
currentTriggeredEffects.Clear();
if (loadoutEntry == null)
return;
loadoutEntry.CollectCastStartEffects(currentCastStartEffects);
loadoutEntry.CollectTriggeredEffects(currentTriggeredEffects);
}
/// <summary>
/// 스킬 클립으로 Override Controller 생성 후 재생
/// </summary>
@@ -354,23 +387,28 @@ namespace Colosseum.Skills
return;
}
var effects = currentSkill.Effects;
if (index < 0 || index >= effects.Count)
if (!currentTriggeredEffects.TryGetValue(index, out List<SkillEffect> effects) || effects == null || effects.Count == 0)
{
if (debugMode) Debug.LogWarning($"[Effect] Invalid index: {index}");
return;
}
var effect = effects[index];
if (debugMode) Debug.Log($"[Effect] {effect.name} (index {index})");
// 공격 범위 시각화
if (showAreaDebug)
for (int i = 0; i < effects.Count; i++)
{
effect.DrawDebugRange(gameObject, debugDrawDuration);
}
SkillEffect effect = effects[i];
if (effect == null)
continue;
effect.ExecuteOnCast(gameObject);
if (debugMode) Debug.Log($"[Effect] {effect.name} (index {index})");
// 공격 범위 시각화
if (showAreaDebug)
{
effect.DrawDebugRange(gameObject, debugDrawDuration);
}
effect.ExecuteOnCast(gameObject);
}
}
/// <summary>
@@ -408,6 +446,9 @@ namespace Colosseum.Skills
RestoreBaseController();
currentSkill = null;
currentLoadoutEntry = null;
currentCastStartEffects.Clear();
currentTriggeredEffects.Clear();
skillEndRequested = false;
waitingForEndAnimation = false;
return true;
@@ -430,9 +471,9 @@ namespace Colosseum.Skills
return Mathf.Max(0f, remaining);
}
private void StartCooldown(SkillData skill)
private void StartCooldown(SkillData skill, float cooldownDuration)
{
cooldownTracker[skill] = Time.time + skill.Cooldown;
cooldownTracker[skill] = Time.time + cooldownDuration;
}
public void ResetCooldown(SkillData skill)