- 드로그 BT를 페이즈 전환, 부활 트리거, 가중치 근접 패턴 중심으로 재구성 - 땅 울리기 및 콤보-기본기1_3 패턴/스킬/이펙트를 추가하고 기존 평타 파생 자산을 정리 - 드로그 행동 검증용 PlayMode/Editor 테스트와 관련 런타임 상태 추적을 추가
626 lines
24 KiB
C#
626 lines
24 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Reflection;
|
|
|
|
using NUnit.Framework;
|
|
|
|
using UnityEngine;
|
|
using UnityEngine.SceneManagement;
|
|
using UnityEngine.TestTools;
|
|
|
|
using Unity.Netcode;
|
|
|
|
using Colosseum.AI;
|
|
using Colosseum.Enemy;
|
|
using Colosseum.Player;
|
|
using Colosseum.Skills;
|
|
|
|
namespace Colosseum.Tests
|
|
{
|
|
/// <summary>
|
|
/// 드로그 BT가 사양대로 분기되는지 검증하는 PlayMode 테스트입니다.
|
|
/// </summary>
|
|
public class DrogBehaviorTreeTests
|
|
{
|
|
private const string DrogSceneName = "Drog";
|
|
|
|
private const string PatternPunish = "밟기";
|
|
private const string PatternSignature = "집행";
|
|
private const string PatternGroundShake = "땅 울리기";
|
|
private const string PatternLeap = "도약";
|
|
private const string PatternThrow = "투척";
|
|
private const string PatternCombo = "콤보-강타";
|
|
private const string PatternPressure = "콤보-발구르기";
|
|
private const string PatternTertiary = "콤보-기본기3";
|
|
private const string PatternSecondary = "콤보-기본기2";
|
|
private const string PatternPrimary = "콤보-기본기1";
|
|
|
|
private const float DefaultTimeout = 12f;
|
|
private const float LongPatternTimeout = 18f;
|
|
private const float TransitionDelayBuffer = 8f;
|
|
|
|
private BossEnemy boss;
|
|
private BossBehaviorRuntimeState runtimeState;
|
|
private SkillController bossSkillController;
|
|
private Behaviour behaviorGraphAgent;
|
|
private PlayerNetworkController player;
|
|
private HitReactionController hitReactionController;
|
|
private PlayerMovement playerMovement;
|
|
private readonly List<string> capturedLogs = new List<string>();
|
|
|
|
[UnitySetUp]
|
|
public IEnumerator SetUp()
|
|
{
|
|
Application.logMessageReceived += HandleLogMessage;
|
|
yield return LoadScene(DrogSceneName);
|
|
yield return WaitForCondition(
|
|
() => NetworkManager.Singleton != null,
|
|
5f,
|
|
"Drog 씬에서 NetworkManager를 찾지 못했습니다.");
|
|
|
|
if (!NetworkManager.Singleton.IsListening)
|
|
{
|
|
Assert.IsTrue(NetworkManager.Singleton.StartHost(), "테스트용 호스트 시작에 실패했습니다.");
|
|
}
|
|
|
|
yield return WaitForCondition(
|
|
() => NetworkManager.Singleton.IsListening && NetworkManager.Singleton.IsHost,
|
|
5f,
|
|
"호스트가 시작되지 않았습니다.");
|
|
|
|
yield return WaitForCondition(
|
|
() => BossEnemy.ActiveBoss != null || UnityEngine.Object.FindFirstObjectByType<BossEnemy>() != null,
|
|
5f,
|
|
"드로그 보스를 찾지 못했습니다.");
|
|
|
|
yield return WaitForCondition(
|
|
() => FindPrimaryPlayer() != null,
|
|
5f,
|
|
"플레이어를 찾지 못했습니다.");
|
|
|
|
boss = BossEnemy.ActiveBoss != null
|
|
? BossEnemy.ActiveBoss
|
|
: UnityEngine.Object.FindFirstObjectByType<BossEnemy>();
|
|
runtimeState = boss != null ? boss.GetComponent<BossBehaviorRuntimeState>() : null;
|
|
bossSkillController = boss != null ? boss.GetComponent<SkillController>() : null;
|
|
behaviorGraphAgent = boss != null ? boss.GetComponent("BehaviorGraphAgent") as Behaviour : null;
|
|
player = FindPrimaryPlayer();
|
|
hitReactionController = player != null ? player.GetComponent<HitReactionController>() : null;
|
|
playerMovement = player != null ? player.GetComponent<PlayerMovement>() : null;
|
|
|
|
Assert.NotNull(boss, "보스를 찾지 못했습니다.");
|
|
Assert.NotNull(runtimeState, "BossBehaviorRuntimeState를 찾지 못했습니다.");
|
|
Assert.NotNull(bossSkillController, "보스 SkillController를 찾지 못했습니다.");
|
|
Assert.NotNull(behaviorGraphAgent, "BehaviorGraphAgent를 찾지 못했습니다.");
|
|
Assert.NotNull(player, "플레이어를 찾지 못했습니다.");
|
|
Assert.NotNull(hitReactionController, "플레이어 HitReactionController를 찾지 못했습니다.");
|
|
Assert.NotNull(playerMovement, "플레이어 PlayerMovement를 찾지 못했습니다.");
|
|
|
|
yield return ResetBossAndPlayerState(phase: 1, phaseElapsedTime: 0f, basicLoopCount: 0);
|
|
}
|
|
|
|
[UnityTearDown]
|
|
public IEnumerator TearDown()
|
|
{
|
|
LogAssert.ignoreFailingMessages = true;
|
|
|
|
if (behaviorGraphAgent != null)
|
|
behaviorGraphAgent.enabled = false;
|
|
|
|
if (bossSkillController != null)
|
|
bossSkillController.CancelSkill();
|
|
|
|
if (boss != null && boss.TryGetComponent(out UnityEngine.AI.NavMeshAgent navMeshAgent))
|
|
navMeshAgent.enabled = false;
|
|
|
|
if (NetworkManager.Singleton != null && NetworkManager.Singleton.IsListening)
|
|
{
|
|
NetworkManager.Singleton.Shutdown();
|
|
}
|
|
|
|
yield return null;
|
|
|
|
string activeSceneName = SceneManager.GetActiveScene().name;
|
|
if (!string.IsNullOrEmpty(activeSceneName) && SceneManager.GetSceneByName(activeSceneName).isLoaded)
|
|
{
|
|
Scene cleanupScene = SceneManager.CreateScene($"DrogBehaviorTreeTests_Cleanup_{Guid.NewGuid():N}");
|
|
SceneManager.SetActiveScene(cleanupScene);
|
|
AsyncOperation unloadOperation = SceneManager.UnloadSceneAsync(activeSceneName);
|
|
if (unloadOperation != null)
|
|
{
|
|
yield return unloadOperation;
|
|
}
|
|
}
|
|
|
|
Application.logMessageReceived -= HandleLogMessage;
|
|
boss = null;
|
|
runtimeState = null;
|
|
bossSkillController = null;
|
|
behaviorGraphAgent = null;
|
|
player = null;
|
|
hitReactionController = null;
|
|
playerMovement = null;
|
|
capturedLogs.Clear();
|
|
LogAssert.ignoreFailingMessages = false;
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator Phase2_HealthThreshold_TransitionsWithinDelay()
|
|
{
|
|
yield return ResetBossAndPlayerState(phase: 1, phaseElapsedTime: 0f, basicLoopCount: 0);
|
|
SetBossHealthPercent(0.74f);
|
|
ResumeBehavior();
|
|
|
|
yield return WaitForCondition(
|
|
() => runtimeState.CurrentPatternPhase == 2,
|
|
TransitionDelayBuffer,
|
|
"HP 74%에서 Phase 2 전환이 발생하지 않았습니다.");
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator Phase3_Transition_LeadsIntoImmediateSignature()
|
|
{
|
|
yield return ResetBossAndPlayerState(phase: 2, phaseElapsedTime: 0f, basicLoopCount: 0);
|
|
SetPlayerOffset(2.2f);
|
|
SetBossHealthPercent(0.39f);
|
|
ResumeBehavior();
|
|
|
|
yield return WaitForCondition(
|
|
() => runtimeState.CurrentPatternPhase == 3,
|
|
TransitionDelayBuffer,
|
|
"HP 39%에서 Phase 3 전환이 발생하지 않았습니다.");
|
|
|
|
yield return WaitForPatternLog(
|
|
PatternSignature,
|
|
TransitionDelayBuffer,
|
|
"Phase 3 전환 직후 집행 패턴이 실행되지 않았습니다.");
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator Phase2_DownedNearbyTarget_PrioritizesPunish()
|
|
{
|
|
yield return ResetBossAndPlayerState(phase: 2, phaseElapsedTime: 0f, basicLoopCount: 0);
|
|
SetPlayerOffset(1.6f);
|
|
hitReactionController.ApplyDown(5f);
|
|
yield return null;
|
|
ResumeBehavior();
|
|
|
|
Assert.IsTrue(hitReactionController.IsDowned, "테스트용 다운 적용에 실패했습니다.");
|
|
|
|
yield return WaitForPatternLog(
|
|
PatternPunish,
|
|
DefaultTimeout,
|
|
"가까운 다운 대상이 있어도 밟기 패턴이 선택되지 않았습니다.");
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator Phase2_RecentReviveTrigger_PrioritizesThrow()
|
|
{
|
|
yield return ResetBossAndPlayerState(phase: 2, phaseElapsedTime: 0f, basicLoopCount: 0);
|
|
SetPlayerOffset(4f);
|
|
|
|
BossBehaviorRuntimeState.ReportPlayerRevivedBySkill(player.gameObject, player.gameObject);
|
|
yield return null;
|
|
ResumeBehavior();
|
|
|
|
Assert.IsTrue(runtimeState.HasRecentReviveTrigger(4f), "부활 트리거 기록이 유지되지 않았습니다.");
|
|
|
|
yield return WaitForPatternLog(
|
|
PatternThrow,
|
|
DefaultTimeout,
|
|
"최근 부활 트리거가 있어도 투척 패턴이 선택되지 않았습니다.");
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator Phase2_AfterNamedPatternInterval_UsesGroundShake()
|
|
{
|
|
yield return ResetBossAndPlayerState(phase: 2, phaseElapsedTime: 13f, basicLoopCount: 2);
|
|
SetPlayerOffset(2.4f);
|
|
ResumeBehavior();
|
|
|
|
yield return WaitForPatternLog(
|
|
PatternGroundShake,
|
|
DefaultTimeout,
|
|
"Phase 2에서 주기 도래 후 땅 울리기 패턴이 실행되지 않았습니다.");
|
|
}
|
|
|
|
[UnityTest]
|
|
[Ignore("원거리 9.26m에서도 콤보-강타가 먼저 실행되어 도약 분기 재현이 불안정합니다. 별도 조사 필요.")]
|
|
public IEnumerator Phase2_FarTarget_PrioritizesLeap()
|
|
{
|
|
yield return ResetBossAndPlayerState(phase: 2, phaseElapsedTime: 0f, basicLoopCount: 2);
|
|
SetPlayerOffset(9.5f);
|
|
ResumeBehavior();
|
|
|
|
yield return WaitForPatternLog(
|
|
PatternLeap,
|
|
DefaultTimeout,
|
|
"원거리 대상 상황에서 도약 패턴이 선택되지 않았습니다.");
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator Phase3_DoesNotUseGroundShakeAsIndependentPattern()
|
|
{
|
|
yield return ResetBossAndPlayerState(phase: 3, phaseElapsedTime: 13f, basicLoopCount: 2);
|
|
SetPlayerOffset(2.4f);
|
|
ResumeBehavior();
|
|
|
|
yield return new WaitForSeconds(4f);
|
|
|
|
Assert.IsFalse(
|
|
HasPatternLog(PatternGroundShake),
|
|
"Phase 3에서 땅 울리기가 독립 네임드 패턴으로 실행되었습니다.");
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator Phase1_MeleeBaseline_StartsWithWeightedMeleePattern()
|
|
{
|
|
yield return ResetBossAndPlayerState(phase: 1, phaseElapsedTime: 0f, basicLoopCount: 0);
|
|
SetPlayerOffset(2f);
|
|
ResumeBehavior();
|
|
|
|
yield return WaitForAnyPatternLog(
|
|
new[] { PatternCombo, PatternPressure, PatternTertiary, PatternSecondary, PatternPrimary },
|
|
DefaultTimeout,
|
|
"근접 기본 루프에서 가중치 기본기 패턴이 실행되지 않았습니다.");
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator Phase1_MeleeWeightedSelector_SkipsPatternsOnCooldown()
|
|
{
|
|
yield return ResetBossAndPlayerState(phase: 1, phaseElapsedTime: 0f, basicLoopCount: 0);
|
|
SetPlayerOffset(2f);
|
|
SetPatternCooldown(PatternCombo);
|
|
SetPatternCooldown(PatternPressure);
|
|
SetPatternCooldown(PatternTertiary);
|
|
SetPatternCooldown(PatternSecondary);
|
|
ResumeBehavior();
|
|
|
|
yield return WaitForPatternLog(
|
|
PatternPrimary,
|
|
DefaultTimeout,
|
|
"상위 기본기들이 쿨다운이어도 콤보-기본기1로 폴백하지 않았습니다.");
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator Phase3_After90Seconds_ReusesSignature()
|
|
{
|
|
yield return ResetBossAndPlayerState(phase: 3, phaseElapsedTime: 91f, basicLoopCount: 2);
|
|
SetPlayerOffset(2.2f);
|
|
ResumeBehavior();
|
|
|
|
yield return WaitForPatternLog(
|
|
PatternSignature,
|
|
DefaultTimeout,
|
|
"Phase 3에서 90초 경과 후 집행 재사용이 발생하지 않았습니다.");
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator Signature_WhenUnbroken_CompletesSuccessfully()
|
|
{
|
|
yield return ResetBossAndPlayerState(phase: 3, phaseElapsedTime: 91f, basicLoopCount: 2);
|
|
SetPlayerOffset(2.2f);
|
|
ResumeBehavior();
|
|
|
|
yield return WaitForPatternRunning(
|
|
PatternSignature,
|
|
DefaultTimeout,
|
|
"집행 패턴 시작을 확인하지 못했습니다.");
|
|
|
|
yield return WaitForLogContaining(
|
|
"[UsePatternByRoleAction] 패턴 실행: 집행 / Step=2 / Skill=집행 연타1",
|
|
LongPatternTimeout,
|
|
"집행 성공 경로의 후속 연타 진입을 확인하지 못했습니다.");
|
|
|
|
Assert.IsFalse(runtimeState.WasChargeBroken, "차단이 없는데 집행 차단 플래그가 켜졌습니다.");
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator Signature_WhenBroken_EndsAsFailedAndSetsChargeBroken()
|
|
{
|
|
yield return ResetBossAndPlayerState(phase: 3, phaseElapsedTime: 91f, basicLoopCount: 2);
|
|
SetPlayerOffset(2.2f);
|
|
ResumeBehavior();
|
|
|
|
yield return WaitForPatternRunning(
|
|
PatternSignature,
|
|
DefaultTimeout,
|
|
"집행 패턴 시작을 확인하지 못했습니다.");
|
|
|
|
yield return DealBossDamageDuringSignature(maxDuration: 3f, damagePerTickRatio: 0.02f);
|
|
|
|
yield return WaitForCondition(
|
|
() => runtimeState.LastExecutedPattern != null
|
|
&& runtimeState.LastExecutedPattern.PatternName == PatternSignature
|
|
&& runtimeState.LastPatternExecutionResult != BossPatternExecutionResult.Running,
|
|
LongPatternTimeout,
|
|
"집행 차단 후 결과 종료를 확인하지 못했습니다.");
|
|
|
|
Assert.AreEqual(
|
|
BossPatternExecutionResult.Failed,
|
|
runtimeState.LastPatternExecutionResult,
|
|
"집행 차단 시 결과가 Failed로 기록되지 않았습니다.");
|
|
Assert.IsTrue(runtimeState.WasChargeBroken, "집행 차단 성공 플래그가 기록되지 않았습니다.");
|
|
Assert.Greater(runtimeState.LastChargeStaggerDuration, 0f, "집행 차단 시 경직 시간이 기록되지 않았습니다.");
|
|
}
|
|
|
|
private IEnumerator ResetBossAndPlayerState(int phase, float phaseElapsedTime, int basicLoopCount)
|
|
{
|
|
behaviorGraphAgent.enabled = false;
|
|
bossSkillController.CancelSkill();
|
|
yield return null;
|
|
|
|
boss.Respawn();
|
|
yield return null;
|
|
|
|
player.Respawn();
|
|
hitReactionController.ClearHitReactionState();
|
|
playerMovement.ClearForcedMovement();
|
|
playerMovement.enabled = false;
|
|
yield return null;
|
|
|
|
runtimeState.ResetPhaseState();
|
|
runtimeState.SetCurrentPatternPhase(phase, resetTimer: true);
|
|
SetRuntimeField("currentPhaseStartTime", Time.time - Mathf.Max(0f, phaseElapsedTime));
|
|
SetRuntimeField("basicLoopCountSinceLastBigPattern", Mathf.Max(0, basicLoopCount));
|
|
SetRuntimeField("meleePatternCounter", 0);
|
|
SetRuntimeField("lastPatternExecutionResult", BossPatternExecutionResult.None);
|
|
SetRuntimeField("lastExecutedPattern", null);
|
|
SetRuntimeField("lastReviveCaster", null);
|
|
SetRuntimeField("lastRevivedTarget", null);
|
|
SetRuntimeField("lastReviveEventTime", float.NegativeInfinity);
|
|
|
|
ClearRuntimeDictionary("patternCooldownTracker");
|
|
ClearRuntimeDictionary("customPhaseConditions");
|
|
|
|
runtimeState.WasChargeBroken = false;
|
|
runtimeState.LastChargeStaggerDuration = 0f;
|
|
runtimeState.SetCurrentTarget(player.gameObject);
|
|
|
|
SetPlayerOffset(2f);
|
|
SetBossHealthPercent(1f);
|
|
yield return null;
|
|
|
|
ClearCapturedLogs();
|
|
}
|
|
|
|
private IEnumerator DealBossDamageDuringSignature(float maxDuration, float damagePerTickRatio)
|
|
{
|
|
float maxHealth = boss.MaxHealth;
|
|
float startTime = Time.time;
|
|
|
|
yield return new WaitForSeconds(0.9f);
|
|
|
|
while (Time.time - startTime <= maxDuration)
|
|
{
|
|
if (runtimeState.LastPatternExecutionResult != BossPatternExecutionResult.Running
|
|
|| runtimeState.LastExecutedPattern == null
|
|
|| runtimeState.LastExecutedPattern.PatternName != PatternSignature)
|
|
{
|
|
yield break;
|
|
}
|
|
|
|
float burst = maxHealth * Mathf.Clamp(damagePerTickRatio, 0.005f, 0.1f);
|
|
boss.TakeDamage(burst);
|
|
yield return new WaitForSeconds(0.1f);
|
|
}
|
|
}
|
|
|
|
private IEnumerator LoadScene(string sceneName)
|
|
{
|
|
SceneManager.LoadScene(sceneName, LoadSceneMode.Single);
|
|
yield return WaitForCondition(
|
|
() => SceneManager.GetActiveScene().name == sceneName,
|
|
10f,
|
|
$"씬 로드 실패: {sceneName}");
|
|
|
|
yield return null;
|
|
}
|
|
|
|
private IEnumerator WaitForPatternRunning(string patternName, float timeout, string failureMessage)
|
|
{
|
|
yield return WaitForCondition(
|
|
() => runtimeState.LastExecutedPattern != null
|
|
&& runtimeState.LastExecutedPattern.PatternName == patternName
|
|
&& runtimeState.LastPatternExecutionResult == BossPatternExecutionResult.Running,
|
|
timeout,
|
|
failureMessage);
|
|
}
|
|
|
|
private IEnumerator WaitForPatternResolved(string patternName, float timeout, string failureMessage)
|
|
{
|
|
yield return WaitForCondition(
|
|
() => runtimeState.LastExecutedPattern != null
|
|
&& runtimeState.LastExecutedPattern.PatternName == patternName
|
|
&& runtimeState.LastPatternExecutionResult != BossPatternExecutionResult.Running,
|
|
timeout,
|
|
failureMessage);
|
|
}
|
|
|
|
private IEnumerator WaitForPatternLog(string patternName, float timeout, string failureMessage)
|
|
{
|
|
yield return WaitForCondition(
|
|
() => HasPatternLog(patternName),
|
|
timeout,
|
|
failureMessage);
|
|
}
|
|
|
|
private IEnumerator WaitForAnyPatternLog(IReadOnlyList<string> patternNames, float timeout, string failureMessage)
|
|
{
|
|
yield return WaitForCondition(
|
|
() => HasAnyPatternLog(patternNames),
|
|
timeout,
|
|
failureMessage);
|
|
}
|
|
|
|
private IEnumerator WaitForLogContaining(string marker, float timeout, string failureMessage)
|
|
{
|
|
yield return WaitForCondition(
|
|
() => HasLogContaining(marker),
|
|
timeout,
|
|
failureMessage);
|
|
}
|
|
|
|
private IEnumerator WaitForCondition(Func<bool> predicate, float timeout, string failureMessage)
|
|
{
|
|
float startTime = Time.time;
|
|
|
|
while (Time.time - startTime <= timeout)
|
|
{
|
|
if (predicate.Invoke())
|
|
yield break;
|
|
|
|
yield return null;
|
|
}
|
|
|
|
Assert.Fail(failureMessage);
|
|
}
|
|
|
|
private PlayerNetworkController FindPrimaryPlayer()
|
|
{
|
|
PlayerNetworkController[] players = UnityEngine.Object.FindObjectsByType<PlayerNetworkController>(FindObjectsSortMode.None);
|
|
for (int i = 0; i < players.Length; i++)
|
|
{
|
|
if (players[i] != null)
|
|
return players[i];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private void SetPlayerOffset(float horizontalDistance)
|
|
{
|
|
Vector3 origin = boss.transform.position;
|
|
Vector3 offset = boss.transform.forward;
|
|
offset.y = 0f;
|
|
if (offset.sqrMagnitude < 0.01f)
|
|
offset = Vector3.forward;
|
|
|
|
offset.Normalize();
|
|
Vector3 targetPosition = origin + (offset * horizontalDistance);
|
|
targetPosition.y = origin.y;
|
|
|
|
CharacterController characterController = player.GetComponent<CharacterController>();
|
|
bool restoreCharacterController = characterController != null && characterController.enabled;
|
|
if (restoreCharacterController)
|
|
characterController.enabled = false;
|
|
|
|
player.transform.position = targetPosition;
|
|
player.transform.rotation = Quaternion.LookRotation((origin - targetPosition).normalized);
|
|
|
|
if (restoreCharacterController)
|
|
characterController.enabled = true;
|
|
|
|
runtimeState.SetCurrentTarget(player.gameObject);
|
|
}
|
|
|
|
private void SetBossHealthPercent(float healthPercent)
|
|
{
|
|
float targetHealth = boss.MaxHealth * Mathf.Clamp01(healthPercent);
|
|
float delta = boss.CurrentHealth - targetHealth;
|
|
|
|
if (delta > 0f)
|
|
{
|
|
boss.TakeDamage(delta);
|
|
}
|
|
else if (delta < 0f)
|
|
{
|
|
boss.Heal(-delta);
|
|
}
|
|
}
|
|
|
|
private void ClearRuntimeDictionary(string fieldName)
|
|
{
|
|
FieldInfo field = typeof(BossBehaviorRuntimeState).GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic);
|
|
Assert.NotNull(field, $"BossBehaviorRuntimeState 필드를 찾지 못했습니다: {fieldName}");
|
|
|
|
object dictionary = field.GetValue(runtimeState);
|
|
MethodInfo clearMethod = dictionary?.GetType().GetMethod("Clear", BindingFlags.Instance | BindingFlags.Public);
|
|
Assert.NotNull(clearMethod, $"딕셔너리 Clear 메서드를 찾지 못했습니다: {fieldName}");
|
|
clearMethod.Invoke(dictionary, null);
|
|
}
|
|
|
|
private void SetRuntimeField(string fieldName, object value)
|
|
{
|
|
FieldInfo field = typeof(BossBehaviorRuntimeState).GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic);
|
|
Assert.NotNull(field, $"BossBehaviorRuntimeState 필드를 찾지 못했습니다: {fieldName}");
|
|
field.SetValue(runtimeState, value);
|
|
}
|
|
|
|
private void ResumeBehavior()
|
|
{
|
|
ClearCapturedLogs();
|
|
behaviorGraphAgent.enabled = true;
|
|
}
|
|
|
|
private void ClearCapturedLogs()
|
|
{
|
|
capturedLogs.Clear();
|
|
}
|
|
|
|
private bool HasPatternLog(string patternName)
|
|
{
|
|
string marker = $"패턴 실행: {patternName} /";
|
|
return HasLogContaining(marker);
|
|
}
|
|
|
|
private bool HasAnyPatternLog(IReadOnlyList<string> patternNames)
|
|
{
|
|
if (patternNames == null)
|
|
return false;
|
|
|
|
for (int i = 0; i < patternNames.Count; i++)
|
|
{
|
|
if (HasPatternLog(patternNames[i]))
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private void SetPatternCooldown(string patternName)
|
|
{
|
|
BossPatternData pattern = FindPatternAsset(patternName);
|
|
Assert.NotNull(pattern, $"패턴 에셋을 찾지 못했습니다: {patternName}");
|
|
runtimeState.SetPatternCooldown(pattern);
|
|
}
|
|
|
|
private BossPatternData FindPatternAsset(string patternName)
|
|
{
|
|
BossPatternData[] patterns = Resources.FindObjectsOfTypeAll<BossPatternData>();
|
|
for (int i = 0; i < patterns.Length; i++)
|
|
{
|
|
BossPatternData pattern = patterns[i];
|
|
if (pattern == null || pattern.PatternName != patternName)
|
|
continue;
|
|
|
|
return pattern;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private bool HasLogContaining(string marker)
|
|
{
|
|
for (int i = 0; i < capturedLogs.Count; i++)
|
|
{
|
|
if (capturedLogs[i].Contains(marker, StringComparison.Ordinal))
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private void HandleLogMessage(string condition, string stackTrace, LogType type)
|
|
{
|
|
if (string.IsNullOrEmpty(condition))
|
|
return;
|
|
|
|
capturedLogs.Add(condition);
|
|
if (capturedLogs.Count > 512)
|
|
capturedLogs.RemoveAt(0);
|
|
}
|
|
}
|
|
}
|