- BossBehaviorRuntimeState에 패턴 종료 후 공통 텀과 준비 판정을 추가\n- UsePatternAction이 런타임 패턴 실행 결과와 공통 텀을 함께 사용하도록 정리\n- 드로그 PlayMode 테스트에 패턴 종료 후 공통 간격 검증 케이스를 추가
292 lines
9.5 KiB
C#
292 lines
9.5 KiB
C#
using System;
|
|
|
|
using Colosseum;
|
|
using Colosseum.AI;
|
|
using Colosseum.Combat;
|
|
using Colosseum.Enemy;
|
|
using Colosseum.Skills;
|
|
|
|
using Unity.Behavior;
|
|
using Unity.Properties;
|
|
using UnityEngine;
|
|
|
|
using Action = Unity.Behavior.Action;
|
|
|
|
/// <summary>
|
|
/// 보스 패턴을 실행하는 Behavior Tree Action.
|
|
/// 패턴 내 스텝(스킬 또는 대기)을 순서대로 실행하며, 패턴 쿨타임을 관리합니다.
|
|
/// </summary>
|
|
[Serializable, GeneratePropertyBag]
|
|
[NodeDescription(name: "Use Pattern", story: "[Pattern] 실행", category: "Action", id: "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6")]
|
|
public partial class UsePatternAction : Action
|
|
{
|
|
[SerializeReference] public BlackboardVariable<BossPatternData> Pattern;
|
|
[SerializeReference] public BlackboardVariable<GameObject> Target;
|
|
|
|
private static readonly System.Collections.Generic.Dictionary<string, float> patternReadyTimes =
|
|
new System.Collections.Generic.Dictionary<string, float>();
|
|
|
|
private SkillController skillController;
|
|
private BossBehaviorRuntimeState runtimeState;
|
|
private int currentStepIndex;
|
|
private float waitEndTime;
|
|
private bool isWaiting;
|
|
private bool isSkillStepExecuting;
|
|
|
|
protected override Status OnStart()
|
|
{
|
|
if (Pattern?.Value == null)
|
|
{
|
|
Debug.LogWarning("[UsePatternAction] 패턴이 null입니다.");
|
|
return Status.Failure;
|
|
}
|
|
|
|
if (!IsPatternReady(GameObject, Pattern.Value))
|
|
{
|
|
LogDebug($"쿨다운 중: {Pattern.Value.PatternName}");
|
|
return Status.Failure;
|
|
}
|
|
|
|
if (Pattern.Value.Steps.Count == 0)
|
|
{
|
|
LogDebug("스텝이 비어 있는 패턴이라 실패합니다.");
|
|
return Status.Failure;
|
|
}
|
|
|
|
if (RequiresJumpTarget(Pattern.Value) && ResolveJumpTarget() == null)
|
|
{
|
|
LogDebug($"점프 타겟을 찾지 못해 실패: {Pattern.Value.PatternName}");
|
|
return Status.Failure;
|
|
}
|
|
|
|
skillController = GameObject.GetComponent<SkillController>();
|
|
if (skillController == null)
|
|
{
|
|
Debug.LogWarning($"[UsePatternAction] SkillController를 찾을 수 없습니다: {GameObject.name}");
|
|
return Status.Failure;
|
|
}
|
|
|
|
runtimeState = GameObject.GetComponent<BossBehaviorRuntimeState>();
|
|
runtimeState?.BeginPatternExecution(Pattern.Value);
|
|
|
|
currentStepIndex = 0;
|
|
isWaiting = false;
|
|
isSkillStepExecuting = false;
|
|
return ExecuteCurrentStep();
|
|
}
|
|
|
|
protected override Status OnUpdate()
|
|
{
|
|
if (skillController == null)
|
|
return FinalizePatternFailure(BossPatternExecutionResult.Cancelled);
|
|
|
|
if (isWaiting)
|
|
{
|
|
if (Time.time < waitEndTime)
|
|
return Status.Running;
|
|
|
|
isWaiting = false;
|
|
}
|
|
else
|
|
{
|
|
if (isSkillStepExecuting)
|
|
{
|
|
if (skillController.IsPlayingAnimation)
|
|
return Status.Running;
|
|
|
|
isSkillStepExecuting = false;
|
|
if (skillController.LastExecutionResult != SkillExecutionResult.Completed)
|
|
return FinalizePatternFailure(BossPatternExecutionResult.Cancelled);
|
|
}
|
|
else if (skillController.IsPlayingAnimation)
|
|
return Status.Running;
|
|
}
|
|
|
|
currentStepIndex++;
|
|
|
|
if (currentStepIndex >= Pattern.Value.Steps.Count)
|
|
{
|
|
return FinalizePatternSuccess();
|
|
}
|
|
|
|
return ExecuteCurrentStep();
|
|
}
|
|
|
|
protected override void OnEnd()
|
|
{
|
|
skillController = null;
|
|
runtimeState = null;
|
|
isSkillStepExecuting = false;
|
|
}
|
|
|
|
private Status ExecuteCurrentStep()
|
|
{
|
|
PatternStep step = Pattern.Value.Steps[currentStepIndex];
|
|
|
|
if (step.Type == PatternStepType.Wait)
|
|
{
|
|
isWaiting = true;
|
|
waitEndTime = Time.time + step.Duration;
|
|
return Status.Running;
|
|
}
|
|
|
|
// PatternStepType.Skill
|
|
if (step.Skill == null)
|
|
{
|
|
Debug.LogWarning($"[UsePatternAction] 스킬이 null입니다. (index {currentStepIndex})");
|
|
return FinalizePatternFailure(BossPatternExecutionResult.Failed);
|
|
}
|
|
|
|
GameObject jumpTarget = null;
|
|
if (step.Skill.JumpToTarget)
|
|
{
|
|
jumpTarget = ResolveJumpTarget();
|
|
if (jumpTarget == null)
|
|
{
|
|
return FinalizePatternFailure(BossPatternExecutionResult.Failed);
|
|
}
|
|
}
|
|
|
|
GameObject skillTarget = step.Skill.JumpToTarget ? jumpTarget : Target?.Value;
|
|
bool success = skillTarget != null
|
|
? skillController.ExecuteSkill(step.Skill, skillTarget)
|
|
: skillController.ExecuteSkill(step.Skill);
|
|
if (!success)
|
|
{
|
|
Debug.LogWarning($"[UsePatternAction] 스킬 실행 실패: {step.Skill.SkillName} (index {currentStepIndex})");
|
|
return FinalizePatternFailure(BossPatternExecutionResult.Failed);
|
|
}
|
|
|
|
isSkillStepExecuting = true;
|
|
LogDebug($"패턴 실행: {Pattern.Value.PatternName} / Step={currentStepIndex} / Skill={step.Skill.SkillName}");
|
|
|
|
// jumpToTarget 스킬이면 타겟 위치 전달
|
|
if (step.Skill.JumpToTarget)
|
|
{
|
|
GameObject.GetComponent<EnemyBase>()?.SetJumpTarget(jumpTarget.transform.position);
|
|
}
|
|
|
|
return Status.Running;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 점프 대상이 근접 상태라면, 더 멀리 있는 유효 타겟으로 재선택합니다.
|
|
/// 도약 패턴이 전방 탱커 대신 원거리 플레이어를 징벌할 수 있도록 합니다.
|
|
/// </summary>
|
|
private GameObject ResolveJumpTarget()
|
|
{
|
|
GameObject currentTarget = Target?.Value;
|
|
EnemyBase enemyBase = GameObject.GetComponent<EnemyBase>();
|
|
float maxJumpDistance = enemyBase?.Data != null ? enemyBase.Data.AggroRange : 20f;
|
|
return IsValidJumpTarget(currentTarget, maxJumpDistance) ? currentTarget : null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 점프 타겟의 생존 여부, 팀, 거리 조건을 검사합니다.
|
|
/// </summary>
|
|
private bool IsValidJumpTarget(GameObject candidate, float maxDistance)
|
|
{
|
|
if (candidate == null || !candidate.activeInHierarchy)
|
|
return false;
|
|
|
|
if (Team.IsSameTeam(GameObject, candidate))
|
|
return false;
|
|
|
|
IDamageable damageable = candidate.GetComponent<IDamageable>();
|
|
if (damageable != null && damageable.IsDead)
|
|
return false;
|
|
|
|
float distance = TargetSurfaceUtility.GetHorizontalSurfaceDistance(GameObject.transform.position, candidate);
|
|
return distance <= maxDistance;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 패턴 안에 점프 타겟이 필요한 스텝이 있는지 확인합니다.
|
|
/// </summary>
|
|
private static bool RequiresJumpTarget(BossPatternData pattern)
|
|
{
|
|
if (pattern == null || pattern.Steps == null)
|
|
return false;
|
|
|
|
for (int i = 0; i < pattern.Steps.Count; i++)
|
|
{
|
|
PatternStep step = pattern.Steps[i];
|
|
if (step.Type == PatternStepType.Skill && step.Skill != null && step.Skill.JumpToTarget)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 동일한 보스 오브젝트가 같은 패턴을 다시 사용할 수 있는지 확인합니다.
|
|
/// </summary>
|
|
public static bool IsPatternReady(GameObject owner, BossPatternData pattern)
|
|
{
|
|
if (owner == null || pattern == null)
|
|
return false;
|
|
|
|
BossBehaviorRuntimeState context = owner.GetComponent<BossBehaviorRuntimeState>();
|
|
if (context != null)
|
|
return context.IsPatternReady(pattern);
|
|
|
|
string cooldownKey = BuildCooldownKey(owner, pattern);
|
|
if (!patternReadyTimes.TryGetValue(cooldownKey, out float readyTime))
|
|
return true;
|
|
|
|
return Time.time >= readyTime;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 패턴 사용 직후 다음 사용 가능 시점을 기록합니다.
|
|
/// </summary>
|
|
public static void MarkPatternUsed(GameObject owner, BossPatternData pattern)
|
|
{
|
|
if (owner == null || pattern == null)
|
|
return;
|
|
|
|
BossBehaviorRuntimeState context = owner.GetComponent<BossBehaviorRuntimeState>();
|
|
if (context != null)
|
|
{
|
|
context.SetPatternCooldown(pattern);
|
|
context.CompletePatternExecution(pattern, BossPatternExecutionResult.Succeeded);
|
|
return;
|
|
}
|
|
|
|
string cooldownKey = BuildCooldownKey(owner, pattern);
|
|
patternReadyTimes[cooldownKey] = Time.time + pattern.Cooldown;
|
|
}
|
|
|
|
private static string BuildCooldownKey(GameObject owner, BossPatternData pattern)
|
|
{
|
|
return $"{owner.GetInstanceID()}_{pattern.GetInstanceID()}";
|
|
}
|
|
|
|
private void LogDebug(string message)
|
|
{
|
|
BossBehaviorRuntimeState context = GameObject.GetComponent<BossBehaviorRuntimeState>();
|
|
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;
|
|
}
|
|
}
|