feat: 드로그 BT 및 전투 패턴 재구성

- 드로그 BT를 페이즈 전환, 부활 트리거, 가중치 근접 패턴 중심으로 재구성

- 땅 울리기 및 콤보-기본기1_3 패턴/스킬/이펙트를 추가하고 기존 평타 파생 자산을 정리

- 드로그 행동 검증용 PlayMode/Editor 테스트와 관련 런타임 상태 추적을 추가
This commit is contained in:
2026-04-09 23:21:38 +09:00
parent 1307123029
commit 7776f7ed05
77 changed files with 449522 additions and 345357 deletions

View File

@@ -0,0 +1,53 @@
using System;
using Colosseum.Enemy;
using Unity.Behavior;
using Unity.Properties;
using UnityEngine;
using Action = Unity.Behavior.Action;
/// <summary>
/// 최근 전투 부활 트리거에서 우선 압박할 대상을 선택합니다.
/// </summary>
[Serializable, GeneratePropertyBag]
[NodeDescription(
name: "Select Recent Revive Target",
story: "최근 부활 트리거 대상을 [Target] ",
category: "Action",
id: "464c3911-46d3-4138-88cf-8ba696ba4c13")]
public partial class SelectRecentReviveTargetAction : Action
{
[SerializeReference]
public BlackboardVariable<GameObject> Target;
[SerializeReference]
public BlackboardVariable<float> MaxAge = new BlackboardVariable<float>(4f);
[SerializeReference]
public BlackboardVariable<bool> PreferCaster = new BlackboardVariable<bool>(true);
[SerializeReference]
public BlackboardVariable<bool> FallbackToRevivedTarget = new BlackboardVariable<bool>(true);
protected override Status OnStart()
{
BossBehaviorRuntimeState runtimeState = GameObject.GetComponent<BossBehaviorRuntimeState>();
if (runtimeState == null)
return Status.Failure;
GameObject resolvedTarget = runtimeState.ResolveRecentReviveTriggerTarget(
MaxAge?.Value ?? 0f,
PreferCaster?.Value ?? true,
FallbackToRevivedTarget?.Value ?? true);
if (resolvedTarget == null)
return Status.Failure;
Target.Value = resolvedTarget;
runtimeState.SetCurrentTarget(resolvedTarget);
runtimeState.LogDebug(nameof(SelectRecentReviveTargetAction), $"부활 트리거 대상 선택: {resolvedTarget.name}");
return Status.Success;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3a2a455c708e34f4ca783bab6bdf8ce4

View File

@@ -41,13 +41,10 @@ public partial class UsePatternByRoleAction : BossPatternActionBase
if (pattern == null)
return Status.Failure;
// 타겟 해석은 ResolveStepTarget에서 처리
// 여기서는 RegisterPatternUse만 호출 (근접 패턴 전용)
if (pattern.IsMelee)
{
BossBehaviorRuntimeState context = GameObject.GetComponent<BossBehaviorRuntimeState>();
context?.RegisterPatternUse(pattern);
}
// 타겟 해석은 ResolveStepTarget에서 처리됩니다.
// 대형 패턴/징벌 패턴 후 기본 루프 강제 규칙이 유지되도록 모든 패턴 사용을 기록합니다.
BossBehaviorRuntimeState context = GameObject.GetComponent<BossBehaviorRuntimeState>();
context?.RegisterPatternUse(pattern);
// base.OnStart는 TryResolvePattern → ExecuteCurrentStep 호출
return base.OnStart();
@@ -71,16 +68,25 @@ public partial class UsePatternByRoleAction : BossPatternActionBase
{
pattern = Pattern?.Value;
target = Target != null ? Target.Value : null;
BossBehaviorRuntimeState context = GameObject.GetComponent<BossBehaviorRuntimeState>();
if (pattern == null)
{
context?.LogDebug(nameof(UsePatternByRoleAction), "실행 실패: Pattern이 비어 있습니다.");
return false;
}
BossBehaviorRuntimeState context = GameObject.GetComponent<BossBehaviorRuntimeState>();
if (context == null || !context.IsPatternReady(pattern))
{
context?.LogDebug(nameof(UsePatternByRoleAction), $"실행 실패: 패턴 준비 안 됨 - {pattern.PatternName}");
return false;
}
if (target == null)
{
context?.LogDebug(nameof(UsePatternByRoleAction), $"실행 실패: 타겟 없음 - {pattern.PatternName}");
return false;
}
return true;
}

View File

@@ -0,0 +1,80 @@
using System;
using Unity.Behavior;
using Unity.Properties;
using UnityEngine;
using Colosseum.Enemy;
namespace Colosseum.AI.BehaviorActions.Actions
{
/// <summary>
/// 준비된 후보 패턴 중 하나를 가중치 기반으로 선택하고 즉시 실행합니다.
/// </summary>
[Serializable, GeneratePropertyBag]
[NodeDescription(
name: "Use Weighted Ready Pattern",
story: "준비된 후보 패턴 중 하나를 가중치 기반으로 선택해 실행",
category: "Action",
id: "6d4fc6fd-0ccd-4d9a-8b86-c602062f78a7")]
public partial class UseWeightedReadyPatternAction : BossPatternActionBase
{
[SerializeReference] public BlackboardVariable<BossPatternData> Pattern1;
[SerializeReference] public BlackboardVariable<float> Weight1 = new BlackboardVariable<float>(1f);
[SerializeReference] public BlackboardVariable<BossPatternData> Pattern2;
[SerializeReference] public BlackboardVariable<float> Weight2 = new BlackboardVariable<float>(1f);
[SerializeReference] public BlackboardVariable<BossPatternData> Pattern3;
[SerializeReference] public BlackboardVariable<float> Weight3 = new BlackboardVariable<float>(1f);
[SerializeReference] public BlackboardVariable<BossPatternData> Pattern4;
[SerializeReference] public BlackboardVariable<float> Weight4 = new BlackboardVariable<float>(1f);
[SerializeReference] public BlackboardVariable<BossPatternData> Pattern5;
[SerializeReference] public BlackboardVariable<float> Weight5 = new BlackboardVariable<float>(1f);
private BossPatternData selectedPattern;
protected override Status OnStart()
{
if (!TrySelectPattern(out selectedPattern))
return Status.Failure;
BossBehaviorRuntimeState context = GameObject.GetComponent<BossBehaviorRuntimeState>();
context?.RegisterPatternUse(selectedPattern);
context?.LogDebug(nameof(UseWeightedReadyPatternAction), $"가중치 패턴 선택 후 실행: {selectedPattern.PatternName}");
return base.OnStart();
}
protected override void OnEnd()
{
selectedPattern = null;
base.OnEnd();
}
protected override bool TryResolvePattern(out BossPatternData pattern, out GameObject target)
{
target = Target != null ? Target.Value : null;
pattern = selectedPattern;
if (pattern == null && !TrySelectPattern(out pattern))
return false;
if (target == null)
return false;
return true;
}
private bool TrySelectPattern(out BossPatternData pattern)
{
WeightedPatternCandidate[] candidates =
{
new WeightedPatternCandidate(Pattern1?.Value, Weight1?.Value ?? 0f),
new WeightedPatternCandidate(Pattern2?.Value, Weight2?.Value ?? 0f),
new WeightedPatternCandidate(Pattern3?.Value, Weight3?.Value ?? 0f),
new WeightedPatternCandidate(Pattern4?.Value, Weight4?.Value ?? 0f),
new WeightedPatternCandidate(Pattern5?.Value, Weight5?.Value ?? 0f),
};
return WeightedPatternSelector.TrySelectReadyPattern(GameObject, candidates, out pattern);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8fdabcba63a07333fa626c0dff9d95e0

View File

@@ -0,0 +1,45 @@
using System;
using Unity.Behavior;
using Unity.Properties;
using UnityEngine;
namespace Colosseum.AI.BehaviorActions.Conditions
{
/// <summary>
/// 후보 패턴 중 현재 실행 가능한 패턴이 하나라도 있는지 확인합니다.
/// </summary>
[Serializable, GeneratePropertyBag]
[Condition(name: "Has Any Ready Pattern", story: "후보 패턴 중 실행 가능한 패턴이 있는가?", id: "f2c9e62a-f5ea-43c5-8e88-b94a449e7c7f")]
[NodeDescription(
name: "Has Any Ready Pattern",
story: "후보 패턴 중 실행 가능한 패턴이 있는가?",
category: "Condition/Pattern")]
public partial class HasAnyReadyPatternCondition : Unity.Behavior.Condition
{
[SerializeReference] public BlackboardVariable<BossPatternData> Pattern1;
[SerializeReference] public BlackboardVariable<float> Weight1 = new BlackboardVariable<float>(1f);
[SerializeReference] public BlackboardVariable<BossPatternData> Pattern2;
[SerializeReference] public BlackboardVariable<float> Weight2 = new BlackboardVariable<float>(1f);
[SerializeReference] public BlackboardVariable<BossPatternData> Pattern3;
[SerializeReference] public BlackboardVariable<float> Weight3 = new BlackboardVariable<float>(1f);
[SerializeReference] public BlackboardVariable<BossPatternData> Pattern4;
[SerializeReference] public BlackboardVariable<float> Weight4 = new BlackboardVariable<float>(1f);
[SerializeReference] public BlackboardVariable<BossPatternData> Pattern5;
[SerializeReference] public BlackboardVariable<float> Weight5 = new BlackboardVariable<float>(1f);
public override bool IsTrue()
{
WeightedPatternCandidate[] candidates =
{
new WeightedPatternCandidate(Pattern1?.Value, Weight1?.Value ?? 0f),
new WeightedPatternCandidate(Pattern2?.Value, Weight2?.Value ?? 0f),
new WeightedPatternCandidate(Pattern3?.Value, Weight3?.Value ?? 0f),
new WeightedPatternCandidate(Pattern4?.Value, Weight4?.Value ?? 0f),
new WeightedPatternCandidate(Pattern5?.Value, Weight5?.Value ?? 0f),
};
return WeightedPatternSelector.HasAnyReadyPattern(GameObject, candidates);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1e0445cb63a4144ee9f4361c0729d95a

View File

@@ -0,0 +1,32 @@
using System;
using Colosseum.Enemy;
using Unity.Behavior;
using Unity.Properties;
using UnityEngine;
namespace Colosseum.AI.BehaviorActions.Conditions
{
/// <summary>
/// 최근 전투 부활 트리거가 아직 유효한지 확인합니다.
/// </summary>
[Serializable, GeneratePropertyBag]
[Condition(name: "Is Recent Revive Trigger", story: "최근 [MaxAge] ?", id: "d12890b1-0cb0-4586-a61f-885e7d0e97ee")]
[NodeDescription(
name: "Is Recent Revive Trigger",
story: "최근 [MaxAge] ?",
category: "Condition/Pattern")]
public partial class IsRecentReviveTriggerCondition : Unity.Behavior.Condition
{
[SerializeReference]
[Tooltip("부활 트리거를 유효하다고 간주할 최대 시간(초)")]
public BlackboardVariable<float> MaxAge = new BlackboardVariable<float>(4f);
public override bool IsTrue()
{
BossBehaviorRuntimeState runtimeState = GameObject.GetComponent<BossBehaviorRuntimeState>();
return runtimeState != null && runtimeState.HasRecentReviveTrigger(MaxAge?.Value ?? 0f);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 67f09ee2c79dabe70afb218591cde315