feat: 보스 공통 패턴 간격 추가
- BossBehaviorRuntimeState에 패턴 종료 후 공통 텀과 준비 판정을 추가\n- UsePatternAction이 런타임 패턴 실행 결과와 공통 텀을 함께 사용하도록 정리\n- 드로그 PlayMode 테스트에 패턴 종료 후 공통 간격 검증 케이스를 추가
This commit is contained in:
@@ -27,6 +27,7 @@ public partial class UsePatternAction : Action
|
|||||||
new System.Collections.Generic.Dictionary<string, float>();
|
new System.Collections.Generic.Dictionary<string, float>();
|
||||||
|
|
||||||
private SkillController skillController;
|
private SkillController skillController;
|
||||||
|
private BossBehaviorRuntimeState runtimeState;
|
||||||
private int currentStepIndex;
|
private int currentStepIndex;
|
||||||
private float waitEndTime;
|
private float waitEndTime;
|
||||||
private bool isWaiting;
|
private bool isWaiting;
|
||||||
@@ -65,6 +66,9 @@ public partial class UsePatternAction : Action
|
|||||||
return Status.Failure;
|
return Status.Failure;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
runtimeState = GameObject.GetComponent<BossBehaviorRuntimeState>();
|
||||||
|
runtimeState?.BeginPatternExecution(Pattern.Value);
|
||||||
|
|
||||||
currentStepIndex = 0;
|
currentStepIndex = 0;
|
||||||
isWaiting = false;
|
isWaiting = false;
|
||||||
isSkillStepExecuting = false;
|
isSkillStepExecuting = false;
|
||||||
@@ -74,7 +78,7 @@ public partial class UsePatternAction : Action
|
|||||||
protected override Status OnUpdate()
|
protected override Status OnUpdate()
|
||||||
{
|
{
|
||||||
if (skillController == null)
|
if (skillController == null)
|
||||||
return Status.Failure;
|
return FinalizePatternFailure(BossPatternExecutionResult.Cancelled);
|
||||||
|
|
||||||
if (isWaiting)
|
if (isWaiting)
|
||||||
{
|
{
|
||||||
@@ -92,7 +96,7 @@ public partial class UsePatternAction : Action
|
|||||||
|
|
||||||
isSkillStepExecuting = false;
|
isSkillStepExecuting = false;
|
||||||
if (skillController.LastExecutionResult != SkillExecutionResult.Completed)
|
if (skillController.LastExecutionResult != SkillExecutionResult.Completed)
|
||||||
return Status.Failure;
|
return FinalizePatternFailure(BossPatternExecutionResult.Cancelled);
|
||||||
}
|
}
|
||||||
else if (skillController.IsPlayingAnimation)
|
else if (skillController.IsPlayingAnimation)
|
||||||
return Status.Running;
|
return Status.Running;
|
||||||
@@ -102,8 +106,7 @@ public partial class UsePatternAction : Action
|
|||||||
|
|
||||||
if (currentStepIndex >= Pattern.Value.Steps.Count)
|
if (currentStepIndex >= Pattern.Value.Steps.Count)
|
||||||
{
|
{
|
||||||
MarkPatternUsed(GameObject, Pattern.Value);
|
return FinalizePatternSuccess();
|
||||||
return Status.Success;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ExecuteCurrentStep();
|
return ExecuteCurrentStep();
|
||||||
@@ -112,6 +115,7 @@ public partial class UsePatternAction : Action
|
|||||||
protected override void OnEnd()
|
protected override void OnEnd()
|
||||||
{
|
{
|
||||||
skillController = null;
|
skillController = null;
|
||||||
|
runtimeState = null;
|
||||||
isSkillStepExecuting = false;
|
isSkillStepExecuting = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,7 +134,7 @@ public partial class UsePatternAction : Action
|
|||||||
if (step.Skill == null)
|
if (step.Skill == null)
|
||||||
{
|
{
|
||||||
Debug.LogWarning($"[UsePatternAction] 스킬이 null입니다. (index {currentStepIndex})");
|
Debug.LogWarning($"[UsePatternAction] 스킬이 null입니다. (index {currentStepIndex})");
|
||||||
return Status.Failure;
|
return FinalizePatternFailure(BossPatternExecutionResult.Failed);
|
||||||
}
|
}
|
||||||
|
|
||||||
GameObject jumpTarget = null;
|
GameObject jumpTarget = null;
|
||||||
@@ -139,7 +143,7 @@ public partial class UsePatternAction : Action
|
|||||||
jumpTarget = ResolveJumpTarget();
|
jumpTarget = ResolveJumpTarget();
|
||||||
if (jumpTarget == null)
|
if (jumpTarget == null)
|
||||||
{
|
{
|
||||||
return Status.Failure;
|
return FinalizePatternFailure(BossPatternExecutionResult.Failed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,7 +154,7 @@ public partial class UsePatternAction : Action
|
|||||||
if (!success)
|
if (!success)
|
||||||
{
|
{
|
||||||
Debug.LogWarning($"[UsePatternAction] 스킬 실행 실패: {step.Skill.SkillName} (index {currentStepIndex})");
|
Debug.LogWarning($"[UsePatternAction] 스킬 실행 실패: {step.Skill.SkillName} (index {currentStepIndex})");
|
||||||
return Status.Failure;
|
return FinalizePatternFailure(BossPatternExecutionResult.Failed);
|
||||||
}
|
}
|
||||||
|
|
||||||
isSkillStepExecuting = true;
|
isSkillStepExecuting = true;
|
||||||
@@ -222,6 +226,10 @@ public partial class UsePatternAction : Action
|
|||||||
if (owner == null || pattern == null)
|
if (owner == null || pattern == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
BossBehaviorRuntimeState context = owner.GetComponent<BossBehaviorRuntimeState>();
|
||||||
|
if (context != null)
|
||||||
|
return context.IsPatternReady(pattern);
|
||||||
|
|
||||||
string cooldownKey = BuildCooldownKey(owner, pattern);
|
string cooldownKey = BuildCooldownKey(owner, pattern);
|
||||||
if (!patternReadyTimes.TryGetValue(cooldownKey, out float readyTime))
|
if (!patternReadyTimes.TryGetValue(cooldownKey, out float readyTime))
|
||||||
return true;
|
return true;
|
||||||
@@ -237,6 +245,14 @@ public partial class UsePatternAction : Action
|
|||||||
if (owner == null || pattern == null)
|
if (owner == null || pattern == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
BossBehaviorRuntimeState context = owner.GetComponent<BossBehaviorRuntimeState>();
|
||||||
|
if (context != null)
|
||||||
|
{
|
||||||
|
context.SetPatternCooldown(pattern);
|
||||||
|
context.CompletePatternExecution(pattern, BossPatternExecutionResult.Succeeded);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
string cooldownKey = BuildCooldownKey(owner, pattern);
|
string cooldownKey = BuildCooldownKey(owner, pattern);
|
||||||
patternReadyTimes[cooldownKey] = Time.time + pattern.Cooldown;
|
patternReadyTimes[cooldownKey] = Time.time + pattern.Cooldown;
|
||||||
}
|
}
|
||||||
@@ -251,4 +267,25 @@ public partial class UsePatternAction : Action
|
|||||||
BossBehaviorRuntimeState context = GameObject.GetComponent<BossBehaviorRuntimeState>();
|
BossBehaviorRuntimeState context = GameObject.GetComponent<BossBehaviorRuntimeState>();
|
||||||
context?.LogDebug(nameof(UsePatternAction), message);
|
context?.LogDebug(nameof(UsePatternAction), message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Status FinalizePatternSuccess()
|
||||||
|
{
|
||||||
|
if (runtimeState != null)
|
||||||
|
{
|
||||||
|
runtimeState.SetPatternCooldown(Pattern.Value);
|
||||||
|
runtimeState.CompletePatternExecution(Pattern.Value, BossPatternExecutionResult.Succeeded);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
MarkPatternUsed(GameObject, Pattern.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Status.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Status FinalizePatternFailure(BossPatternExecutionResult result)
|
||||||
|
{
|
||||||
|
runtimeState?.CompletePatternExecution(Pattern.Value, result);
|
||||||
|
return Status.Failure;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ namespace Colosseum.Enemy
|
|||||||
[Header("Pattern Flow")]
|
[Header("Pattern Flow")]
|
||||||
[Tooltip("대형 패턴(시그니처/기동/조합) 직후 기본 패턴 최소 순환 횟수")]
|
[Tooltip("대형 패턴(시그니처/기동/조합) 직후 기본 패턴 최소 순환 횟수")]
|
||||||
[Min(0)] [SerializeField] protected int basicLoopMinCountAfterBigPattern = 2;
|
[Min(0)] [SerializeField] protected int basicLoopMinCountAfterBigPattern = 2;
|
||||||
|
[Tooltip("패턴 하나가 끝난 뒤 다음 패턴을 시작하기까지의 공통 텀")]
|
||||||
|
[Min(0f)] [SerializeField] protected float commonPatternInterval = 0.35f;
|
||||||
|
|
||||||
[Header("시그니처 효과 설정")]
|
[Header("시그니처 효과 설정")]
|
||||||
[Tooltip("시그니처 패턴 차단에 필요한 누적 피해 비율")]
|
[Tooltip("시그니처 패턴 차단에 필요한 누적 피해 비율")]
|
||||||
@@ -93,6 +95,7 @@ namespace Colosseum.Enemy
|
|||||||
protected int basicLoopCountSinceLastBigPattern;
|
protected int basicLoopCountSinceLastBigPattern;
|
||||||
protected int currentPatternPhase = 1;
|
protected int currentPatternPhase = 1;
|
||||||
protected float currentPhaseStartTime;
|
protected float currentPhaseStartTime;
|
||||||
|
protected float nextPatternReadyTime;
|
||||||
protected BossPatternExecutionResult lastPatternExecutionResult;
|
protected BossPatternExecutionResult lastPatternExecutionResult;
|
||||||
protected BossPatternData lastExecutedPattern;
|
protected BossPatternData lastExecutedPattern;
|
||||||
protected GameObject lastReviveCaster;
|
protected GameObject lastReviveCaster;
|
||||||
@@ -124,6 +127,11 @@ namespace Colosseum.Enemy
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int BasicLoopCountSinceLastBigPattern => basicLoopCountSinceLastBigPattern;
|
public int BasicLoopCountSinceLastBigPattern => basicLoopCountSinceLastBigPattern;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 패턴 종료 후 다음 패턴 시작까지 남은 공통 텀입니다.
|
||||||
|
/// </summary>
|
||||||
|
public float RemainingPatternInterval => Mathf.Max(0f, nextPatternReadyTime - Time.time);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 마지막 패턴 실행 결과
|
/// 마지막 패턴 실행 결과
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -276,6 +284,9 @@ namespace Colosseum.Enemy
|
|||||||
{
|
{
|
||||||
lastExecutedPattern = pattern;
|
lastExecutedPattern = pattern;
|
||||||
lastPatternExecutionResult = result;
|
lastPatternExecutionResult = result;
|
||||||
|
|
||||||
|
if (pattern != null && IsTerminalPatternExecutionResult(result))
|
||||||
|
StartCommonPatternInterval();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -386,6 +397,9 @@ namespace Colosseum.Enemy
|
|||||||
if (pattern == null || pattern.Steps == null || pattern.Steps.Count == 0)
|
if (pattern == null || pattern.Steps == null || pattern.Steps.Count == 0)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
if (!IsCommonPatternIntervalReady())
|
||||||
|
return false;
|
||||||
|
|
||||||
if (!patternCooldownTracker.TryGetValue(pattern, out float readyTime))
|
if (!patternCooldownTracker.TryGetValue(pattern, out float readyTime))
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
@@ -401,6 +415,22 @@ namespace Colosseum.Enemy
|
|||||||
patternCooldownTracker[pattern] = Time.time + pattern.Cooldown;
|
patternCooldownTracker[pattern] = Time.time + pattern.Cooldown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 공통 패턴 텀이 끝났는지 반환합니다.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsCommonPatternIntervalReady()
|
||||||
|
{
|
||||||
|
return Time.time >= nextPatternReadyTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 현재 시점부터 공통 패턴 텀을 다시 시작합니다.
|
||||||
|
/// </summary>
|
||||||
|
public void StartCommonPatternInterval()
|
||||||
|
{
|
||||||
|
nextPatternReadyTime = Time.time + Mathf.Max(0f, commonPatternInterval);
|
||||||
|
}
|
||||||
|
|
||||||
protected void StopMovement()
|
protected void StopMovement()
|
||||||
{
|
{
|
||||||
if (navMeshAgent == null || !navMeshAgent.enabled)
|
if (navMeshAgent == null || !navMeshAgent.enabled)
|
||||||
@@ -435,6 +465,7 @@ namespace Colosseum.Enemy
|
|||||||
{
|
{
|
||||||
currentPatternPhase = 1;
|
currentPatternPhase = 1;
|
||||||
currentPhaseStartTime = Time.time;
|
currentPhaseStartTime = Time.time;
|
||||||
|
nextPatternReadyTime = 0f;
|
||||||
lastPatternExecutionResult = BossPatternExecutionResult.None;
|
lastPatternExecutionResult = BossPatternExecutionResult.None;
|
||||||
lastExecutedPattern = null;
|
lastExecutedPattern = null;
|
||||||
lastReviveCaster = null;
|
lastReviveCaster = null;
|
||||||
@@ -443,6 +474,13 @@ namespace Colosseum.Enemy
|
|||||||
customPhaseConditions.Clear();
|
customPhaseConditions.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsTerminalPatternExecutionResult(BossPatternExecutionResult result)
|
||||||
|
{
|
||||||
|
return result == BossPatternExecutionResult.Succeeded
|
||||||
|
|| result == BossPatternExecutionResult.Failed
|
||||||
|
|| result == BossPatternExecutionResult.Cancelled;
|
||||||
|
}
|
||||||
|
|
||||||
public override void OnNetworkDespawn()
|
public override void OnNetworkDespawn()
|
||||||
{
|
{
|
||||||
if (IsServer)
|
if (IsServer)
|
||||||
|
|||||||
@@ -283,6 +283,44 @@ namespace Colosseum.Tests
|
|||||||
"상위 기본기들이 쿨다운이어도 콤보-기본기1로 폴백하지 않았습니다.");
|
"상위 기본기들이 쿨다운이어도 콤보-기본기1로 폴백하지 않았습니다.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[UnityTest]
|
||||||
|
public IEnumerator Phase1_AfterPatternEnds_WaitsForCommonPatternInterval()
|
||||||
|
{
|
||||||
|
const float commonPatternInterval = 1.2f;
|
||||||
|
|
||||||
|
yield return ResetBossAndPlayerState(phase: 1, phaseElapsedTime: 0f, basicLoopCount: 0);
|
||||||
|
SetRuntimeField("commonPatternInterval", commonPatternInterval);
|
||||||
|
SetPlayerOffset(2f);
|
||||||
|
ResumeBehavior();
|
||||||
|
|
||||||
|
yield return WaitForCondition(
|
||||||
|
() => runtimeState.LastExecutedPattern != null
|
||||||
|
&& runtimeState.LastPatternExecutionResult == BossPatternExecutionResult.Running,
|
||||||
|
DefaultTimeout,
|
||||||
|
"첫 패턴 시작을 확인하지 못했습니다.");
|
||||||
|
|
||||||
|
BossPatternData firstPattern = runtimeState.LastExecutedPattern;
|
||||||
|
|
||||||
|
yield return WaitForCondition(
|
||||||
|
() => runtimeState.LastPatternExecutionResult != BossPatternExecutionResult.Running,
|
||||||
|
LongPatternTimeout,
|
||||||
|
"첫 패턴 종료를 확인하지 못했습니다.");
|
||||||
|
|
||||||
|
Assert.AreSame(firstPattern, runtimeState.LastExecutedPattern, "첫 패턴 종료 전에 다른 패턴이 시작되었습니다.");
|
||||||
|
|
||||||
|
yield return new WaitForSeconds(commonPatternInterval * 0.5f);
|
||||||
|
|
||||||
|
Assert.AreNotEqual(
|
||||||
|
BossPatternExecutionResult.Running,
|
||||||
|
runtimeState.LastPatternExecutionResult,
|
||||||
|
"공통 텀 전에 다음 패턴이 시작되었습니다.");
|
||||||
|
|
||||||
|
yield return WaitForCondition(
|
||||||
|
() => runtimeState.LastPatternExecutionResult == BossPatternExecutionResult.Running,
|
||||||
|
commonPatternInterval,
|
||||||
|
"공통 텀 이후 다음 패턴이 다시 시작되지 않았습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
[UnityTest]
|
[UnityTest]
|
||||||
public IEnumerator Phase3_After90Seconds_ReusesSignature()
|
public IEnumerator Phase3_After90Seconds_ReusesSignature()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user