feat: 드로그 BT 및 전투 패턴 재구성
- 드로그 BT를 페이즈 전환, 부활 트리거, 가중치 근접 패턴 중심으로 재구성 - 땅 울리기 및 콤보-기본기1_3 패턴/스킬/이펙트를 추가하고 기존 평타 파생 자산을 정리 - 드로그 행동 검증용 PlayMode/Editor 테스트와 관련 런타임 상태 추적을 추가
This commit is contained in:
625
Assets/_Game/Tests/PlayMode/DrogBehaviorTreeTests.cs
Normal file
625
Assets/_Game/Tests/PlayMode/DrogBehaviorTreeTests.cs
Normal file
@@ -0,0 +1,625 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user