Files
Colosseum/Assets/_Game/Scripts/Player/PlayerAbnormalityVerificationRunner.cs

301 lines
11 KiB
C#

using System;
using System.Collections;
using UnityEngine;
using Unity.Netcode;
using Colosseum.Abnormalities;
using Colosseum.Skills;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace Colosseum.Player
{
/// <summary>
/// 플레이어 이상상태와 행동 제어 연동을 자동 검증하는 디버그 러너.
/// 기절, 침묵, 사망, 리스폰 순으로 상태를 검사합니다.
/// </summary>
[DisallowMultipleComponent]
public class PlayerAbnormalityVerificationRunner : NetworkBehaviour
{
[Header("References")]
[SerializeField] private AbnormalityManager abnormalityManager;
[SerializeField] private PlayerActionState actionState;
[SerializeField] private PlayerNetworkController networkController;
[SerializeField] private PlayerSkillInput skillInput;
[SerializeField] private SkillController skillController;
[Header("Test Data")]
[SerializeField] private AbnormalityData stunData;
[SerializeField] private AbnormalityData silenceData;
[Header("Execution")]
[Tooltip("에디터 플레이 시작 시 자동 검증 실행")]
[SerializeField] private bool runOnStartInEditor = false;
[Tooltip("각 검증 단계 사이 대기 시간")]
[Min(0.05f)]
[SerializeField] private float settleDelay = 0.2f;
[Header("Result")]
[SerializeField] private bool isRunning;
[SerializeField] private bool lastRunPassed;
[SerializeField] private int totalChecks;
[SerializeField] private int failedChecks;
[TextArea(5, 12)]
[SerializeField] private string lastReport = string.Empty;
private readonly System.Text.StringBuilder reportBuilder = new System.Text.StringBuilder();
public override void OnNetworkSpawn()
{
if (!IsOwner || !ShouldEnableRunner())
{
enabled = false;
return;
}
ResolveReferences();
LoadDefaultAssetsIfNeeded();
if (runOnStartInEditor)
{
StartCoroutine(RunVerificationRoutine());
}
}
[ContextMenu("Run Verification")]
public void RunVerification()
{
if (!Application.isPlaying || !IsOwner || isRunning)
return;
StartCoroutine(RunVerificationRoutine());
}
private IEnumerator RunVerificationRoutine()
{
if (isRunning)
yield break;
ResolveReferences();
LoadDefaultAssetsIfNeeded();
if (abnormalityManager == null || actionState == null || networkController == null || stunData == null || silenceData == null)
{
Debug.LogWarning("[AbnormalityVerification] Missing references or test data.");
yield break;
}
isRunning = true;
totalChecks = 0;
failedChecks = 0;
lastRunPassed = false;
reportBuilder.Clear();
AppendLine("=== Player Abnormality Verification Start ===");
abnormalityManager.RemoveAllAbnormalities();
RequestRespawnRpc();
yield return new WaitForSeconds(settleDelay);
Verify("초기 상태: 사망 아님", !networkController.IsDead);
Verify("초기 상태: 이동 가능", actionState.CanMove);
Verify("초기 상태: 스킬 사용 가능", actionState.CanUseSkills);
Verify("초기 상태: 무적 상태 아님", !actionState.IsDamageImmune);
yield return RunInvincibilitySkillVerification();
abnormalityManager.ApplyAbnormality(stunData, gameObject);
yield return new WaitForSeconds(settleDelay);
Verify("기절 적용: IsStunned", abnormalityManager.IsStunned);
Verify("기절 적용: ActionState.IsStunned", actionState.IsStunned);
Verify("기절 적용: 이동 불가", !actionState.CanMove);
Verify("기절 적용: 점프 불가", !actionState.CanJump);
Verify("기절 적용: 스킬 사용 불가", !actionState.CanUseSkills);
Verify("기절 적용: 이동속도 0", Mathf.Approximately(actionState.MoveSpeedMultiplier, 0f));
yield return new WaitForSeconds(stunData.duration + settleDelay);
Verify("기절 해제: IsStunned false", !abnormalityManager.IsStunned);
Verify("기절 해제: 이동 가능 복구", actionState.CanMove);
Verify("기절 해제: 스킬 사용 가능 복구", actionState.CanUseSkills);
abnormalityManager.ApplyAbnormality(silenceData, gameObject);
yield return new WaitForSeconds(settleDelay);
Verify("침묵 적용: IsSilenced", abnormalityManager.IsSilenced);
Verify("침묵 적용: 이동 가능 유지", actionState.CanMove);
Verify("침묵 적용: 점프 가능 유지", actionState.CanJump);
Verify("침묵 적용: 스킬 사용 불가", !actionState.CanUseSkills);
yield return new WaitForSeconds(silenceData.duration + settleDelay);
Verify("침묵 해제: IsSilenced false", !abnormalityManager.IsSilenced);
Verify("침묵 해제: 스킬 사용 가능 복구", actionState.CanUseSkills);
abnormalityManager.ApplyAbnormality(stunData, gameObject);
yield return new WaitForSeconds(settleDelay);
networkController.TakeDamageRpc(networkController.Health + 1f);
yield return new WaitForSeconds(settleDelay);
Verify("사망 처리: IsDead", networkController.IsDead);
Verify("사망 처리: 입력 불가", !actionState.CanReceiveInput);
Verify("사망 처리: 이동 불가", !actionState.CanMove);
Verify("사망 처리: 스킬 사용 불가", !actionState.CanUseSkills);
Verify("사망 처리: 활성 이상상태 제거", abnormalityManager.ActiveAbnormalities.Count == 0);
RequestRespawnRpc();
yield return new WaitForSeconds(settleDelay);
Verify("리스폰: IsDead false", !networkController.IsDead);
Verify("리스폰: 활성 이상상태 없음", abnormalityManager.ActiveAbnormalities.Count == 0);
Verify("리스폰: 이동 가능", actionState.CanMove);
Verify("리스폰: 스킬 사용 가능", actionState.CanUseSkills);
lastRunPassed = failedChecks == 0;
AppendLine(lastRunPassed
? "=== Verification Passed ==="
: $"=== Verification Failed: {failedChecks}/{totalChecks} checks failed ===");
lastReport = reportBuilder.ToString();
Debug.Log(lastReport);
isRunning = false;
}
[Rpc(SendTo.Server)]
private void RequestRespawnRpc()
{
if (networkController == null)
return;
networkController.Respawn();
}
private void ResolveReferences()
{
if (abnormalityManager == null)
abnormalityManager = GetComponent<AbnormalityManager>();
if (actionState == null)
actionState = GetComponent<PlayerActionState>();
if (networkController == null)
networkController = GetComponent<PlayerNetworkController>();
if (skillInput == null)
skillInput = GetComponent<PlayerSkillInput>();
if (skillController == null)
skillController = GetComponent<SkillController>();
}
private void LoadDefaultAssetsIfNeeded()
{
#if UNITY_EDITOR
if (stunData == null)
{
stunData = AssetDatabase.LoadAssetAtPath<AbnormalityData>("Assets/_Game/Data/Abnormalities/Data_Abnormality_Player_Stun.asset");
}
if (silenceData == null)
{
silenceData = AssetDatabase.LoadAssetAtPath<AbnormalityData>("Assets/_Game/Data/Abnormalities/Data_Abnormality_Player_Silence.asset");
}
#endif
}
private void Verify(string label, bool condition)
{
totalChecks++;
if (!condition)
{
failedChecks++;
}
AppendLine($"{(condition ? "[PASS]" : "[FAIL]")} {label}");
}
private void AppendLine(string text)
{
if (reportBuilder.Length > 0)
{
reportBuilder.AppendLine();
}
reportBuilder.Append(text);
}
private bool ShouldEnableRunner()
{
#if UNITY_EDITOR
return true;
#else
return Debug.isDebugBuild;
#endif
}
private IEnumerator RunInvincibilitySkillVerification()
{
SkillData invincibilitySkill = skillInput != null ? skillInput.GetSkill(6) : null;
if (invincibilitySkill == null)
{
AppendLine("[SKIP] 무적 스킬 검증: 7번 슬롯 스킬이 없습니다.");
yield break;
}
if (skillController == null || !skillController.ExecuteSkill(invincibilitySkill))
{
Verify("무적 스킬 검증: 스킬 실행 성공", false);
yield break;
}
yield return WaitForConditionOrTimeout(() => actionState.IsDamageImmune, GetSkillDuration(invincibilitySkill) + 0.5f);
float healthBeforeDamage = networkController.Health;
Verify("무적 적용: IsDamageImmune", actionState.IsDamageImmune);
networkController.TakeDamageRpc(15f);
yield return new WaitForSeconds(settleDelay);
Verify("무적 적용: 대미지 무시", Mathf.Approximately(networkController.Health, healthBeforeDamage));
if (silenceData != null)
{
abnormalityManager.ApplyAbnormality(silenceData, gameObject);
yield return WaitForConditionOrTimeout(() => actionState.IsSilenced, settleDelay + 0.5f);
Verify("무적 중 침묵 적용: IsSilenced", actionState.IsSilenced);
Verify("무적 중 침묵 적용: 무적 상태 유지", actionState.IsDamageImmune);
Verify("무적 중 침묵 적용: 스킬 신규 사용 불가", !actionState.CanStartSkill(invincibilitySkill));
abnormalityManager.RemoveAbnormality(silenceData);
yield return new WaitForSeconds(settleDelay);
}
yield return WaitForConditionOrTimeout(() => !actionState.IsDamageImmune, GetSkillDuration(invincibilitySkill) + 1.5f);
Verify("무적 해제: IsDamageImmune false", !actionState.IsDamageImmune);
}
private float GetSkillDuration(SkillData skill)
{
if (skill == null || skill.SkillClip == null)
return settleDelay;
return Mathf.Max(settleDelay, skill.SkillClip.length / Mathf.Max(0.1f, skill.AnimationSpeed));
}
private IEnumerator WaitForConditionOrTimeout(Func<bool> predicate, float timeout)
{
float elapsed = 0f;
while (elapsed < timeout)
{
if (predicate())
yield break;
elapsed += Time.deltaTime;
yield return null;
}
}
}
}