refactor: 드로그 BT 의사결정 투명화 — 모든 조건을 BT 노드로 표시
- BossCombatPatternRole enum 완전 제거, BossPatternData에 직접 필드 추가 - 14개 패턴별 Check*/Use*Action → CheckPatternReadyCondition + UsePatternByRoleAction으로 통합 - BT 계단식 Branch 체인 구조 도입 (BranchingConditionComposite + FloatingPort) - 패턴별 고유 전제 조건을 BT Condition으로 분리 - Punish: IsDownedTargetInRangeCondition (다운 대상 반경) - Mobility: IsTargetBeyondDistanceCondition (원거리 대상) - Utility: IsTargetBeyondDistanceCondition (원거리 대상) - Primary: IsTargetInAttackRangeCondition (사거리 이내) - Phase 진입 조건을 BT에서 확인 가능하도록 IsMinPhaseSatisfiedCondition 추가 - IsPatternReady()에서 minPhase 체크 분리 → 전용 Condition으로 노출 - Secondary 패턴 개념 제거 (secondaryPattern, 보조 차례, 교대 카운터 로직 전부 삭제) - CanResolvePatternTargetCondition 삭제 (7개 중 5개가 노이즈) - RebuildDrogBehaviorAuthoringGraph로 BT 에셋 자동 재구성 메뉴 제공
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -2001,7 +2001,7 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject
|
||||
GlobalObjectIdHash: 860882280
|
||||
GlobalObjectIdHash: 223369646
|
||||
InScenePlacedSourceGlobalObjectIdHash: 223369646
|
||||
DeferredDespawnTick: 0
|
||||
Ownership: 1
|
||||
@@ -2030,11 +2030,13 @@ MonoBehaviour:
|
||||
characterStats: {fileID: -5454809204868423215}
|
||||
animator: {fileID: 4019041888965840580}
|
||||
navMeshAgent: {fileID: 5153439431748782209}
|
||||
abnormalityManager: {fileID: 0}
|
||||
enemyData: {fileID: 11400000, guid: f21773b42c60c4b40b609966f09146aa, type: 2}
|
||||
useThreatSystem: 1
|
||||
damageThreatMultiplier: 1
|
||||
threatDecayPerSecond: 0
|
||||
retargetThreshold: 0
|
||||
shieldStateAbnormality: {fileID: 0}
|
||||
phases: []
|
||||
initialBehaviorGraph: {fileID: -3933356984444701103, guid: 9ade9280028e9da4aa0151fe8e9ec454, type: 2}
|
||||
phaseTransitionInvincibilityTime: 2
|
||||
@@ -2185,23 +2187,26 @@ MonoBehaviour:
|
||||
bossEnemy: {fileID: 6949205239376088310}
|
||||
enemyBase: {fileID: 6949205239376088310}
|
||||
skillController: {fileID: 8818883032728065057}
|
||||
abnormalityManager: {fileID: 0}
|
||||
navMeshAgent: {fileID: 5153439431748782209}
|
||||
mainPattern: {fileID: 11400000, guid: 5efd8123be76bf844875d386d9d5f73d, type: 2}
|
||||
slamPattern: {fileID: 11400000, guid: 4a52d59d590b4eaa9ef92b7984eb08c7, type: 2}
|
||||
leapPattern: {fileID: 11400000, guid: 88e6cc7cab28baf4c8f8a742247000ec, type: 2}
|
||||
behaviorGraphAgent: {fileID: 0}
|
||||
primaryPattern: {fileID: 11400000, guid: 5efd8123be76bf844875d386d9d5f73d, type: 2}
|
||||
secondaryPattern: {fileID: 11400000, guid: 4a52d59d590b4eaa9ef92b7984eb08c7, type: 2}
|
||||
mobilityPattern: {fileID: 11400000, guid: 88e6cc7cab28baf4c8f8a742247000ec, type: 2}
|
||||
utilityPattern: {fileID: 11400000, guid: 9f7ab8078af64fd9a6ff4c9ce6aa9d3a, type: 2}
|
||||
downPunishPattern: {fileID: 11400000, guid: fe5100f855d14c0faac44b6d4f2c771e, type: 2}
|
||||
comboPattern: {fileID: 0}
|
||||
punishPattern: {fileID: 11400000, guid: fe5100f855d14c0faac44b6d4f2c771e, type: 2}
|
||||
signaturePattern: {fileID: 11400000, guid: 5e732b41722c45288bb6234f3e3fa638, type: 2}
|
||||
phase2HealthThreshold: 0.75
|
||||
phase3HealthThreshold: 0.4
|
||||
targetRefreshInterval: 0.2
|
||||
leapDistanceThreshold: 8
|
||||
downPunishSearchRadius: 6
|
||||
primaryTargetRefreshInterval: 0.2
|
||||
mobilityTriggerDistance: 8
|
||||
punishSearchRadius: 6
|
||||
utilityTriggerDistance: 5
|
||||
phase1SlamInterval: 3
|
||||
phase2SlamInterval: 2
|
||||
phase3SlamInterval: 2
|
||||
signatureMinPhase: 2
|
||||
phase1SecondaryInterval: 3
|
||||
phase2SecondaryInterval: 2
|
||||
phase3SecondaryInterval: 2
|
||||
basicLoopMinCountAfterBigPattern: 2
|
||||
signatureRequiredDamageRatio: 0.1
|
||||
signatureTelegraphAbnormality: {fileID: 11400000, guid: fb1a782e44ff4dc19fd8b3c633360752, type: 2}
|
||||
signatureSuccessStaggerDuration: 2
|
||||
|
||||
@@ -210,6 +210,13 @@ public abstract partial class BossPatternActionBase : Action
|
||||
skillTarget = ResolveStepTarget(activeTarget);
|
||||
if (skillTarget == null)
|
||||
{
|
||||
if (activePattern != null && activePattern.SkipJumpStepOnNoTarget)
|
||||
{
|
||||
UsePatternAction.MarkPatternUsed(GameObject, activePattern);
|
||||
LogDebug($"점프 대상 없음, 조합 패턴 조기 종료: {activePattern.PatternName}");
|
||||
return Status.Success;
|
||||
}
|
||||
|
||||
LogDebug($"점프 타겟을 찾지 못해 실패: {activePattern.PatternName}");
|
||||
return Status.Failure;
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
using System;
|
||||
|
||||
using Colosseum.Enemy;
|
||||
|
||||
using Unity.Behavior;
|
||||
using Unity.Properties;
|
||||
|
||||
/// <summary>
|
||||
/// 기동 패턴 준비 여부를 확인하는 체크 액션입니다.
|
||||
/// </summary>
|
||||
[Serializable, GeneratePropertyBag]
|
||||
[NodeDescription(
|
||||
name: "Check Mobility Pattern Ready",
|
||||
story: "기동 패턴 준비 여부 확인",
|
||||
category: "Action",
|
||||
id: "5b4f133ba50f46759c1c1d3347eb0b0d")]
|
||||
public partial class CheckMobilityPatternReadyAction : CheckPatternReadyActionBase
|
||||
{
|
||||
protected override BossCombatPatternRole PatternRole => BossCombatPatternRole.Mobility;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 489f56d9043e6d24fbe8e5574b6729be
|
||||
@@ -1,40 +0,0 @@
|
||||
using System;
|
||||
|
||||
using Colosseum.AI;
|
||||
using Colosseum.Enemy;
|
||||
|
||||
using Unity.Behavior;
|
||||
using Unity.Properties;
|
||||
|
||||
using Action = Unity.Behavior.Action;
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 공통 패턴 역할의 준비 여부를 확인하는 체크 액션 기반 클래스입니다.
|
||||
/// </summary>
|
||||
[Serializable, GeneratePropertyBag]
|
||||
public abstract partial class CheckPatternReadyActionBase : Action
|
||||
{
|
||||
/// <summary>
|
||||
/// 현재 액션이 검사할 패턴 역할입니다.
|
||||
/// </summary>
|
||||
protected abstract BossCombatPatternRole PatternRole { get; }
|
||||
|
||||
protected override Status OnStart()
|
||||
{
|
||||
BossCombatBehaviorContext context = GameObject.GetComponent<BossCombatBehaviorContext>();
|
||||
if (context == null)
|
||||
return Status.Failure;
|
||||
|
||||
if (context.IsBehaviorSuppressed)
|
||||
return Status.Failure;
|
||||
|
||||
BossPatternData pattern = context.GetPattern(PatternRole);
|
||||
if (pattern == null)
|
||||
return Status.Failure;
|
||||
|
||||
if (context.CurrentPatternPhase < pattern.MinPhase)
|
||||
return Status.Failure;
|
||||
|
||||
return UsePatternAction.IsPatternReady(GameObject, pattern) ? Status.Success : Status.Failure;
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 59a58b1d5cc33f943a1af10764ee11b5
|
||||
@@ -1,20 +0,0 @@
|
||||
using System;
|
||||
|
||||
using Colosseum.Enemy;
|
||||
|
||||
using Unity.Behavior;
|
||||
using Unity.Properties;
|
||||
|
||||
/// <summary>
|
||||
/// 기본 패턴 준비 여부를 확인하는 체크 액션입니다.
|
||||
/// </summary>
|
||||
[Serializable, GeneratePropertyBag]
|
||||
[NodeDescription(
|
||||
name: "Check Primary Pattern Ready",
|
||||
story: "기본 패턴 준비 여부 확인",
|
||||
category: "Action",
|
||||
id: "88626617015e43ef97ea4dd05cce55e0")]
|
||||
public partial class CheckPrimaryPatternReadyAction : CheckPatternReadyActionBase
|
||||
{
|
||||
protected override BossCombatPatternRole PatternRole => BossCombatPatternRole.Primary;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e3b45dc2b81beac44a35a3a6545c0488
|
||||
@@ -1,20 +0,0 @@
|
||||
using System;
|
||||
|
||||
using Colosseum.Enemy;
|
||||
|
||||
using Unity.Behavior;
|
||||
using Unity.Properties;
|
||||
|
||||
/// <summary>
|
||||
/// 징벌 패턴 준비 여부를 확인하는 체크 액션입니다.
|
||||
/// </summary>
|
||||
[Serializable, GeneratePropertyBag]
|
||||
[NodeDescription(
|
||||
name: "Check Punish Pattern Ready",
|
||||
story: "징벌 패턴 준비 여부 확인",
|
||||
category: "Action",
|
||||
id: "e855b3f8bdce44efa85859358d67c7a7")]
|
||||
public partial class CheckPunishPatternReadyAction : CheckPatternReadyActionBase
|
||||
{
|
||||
protected override BossCombatPatternRole PatternRole => BossCombatPatternRole.Punish;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2211d1182dbbf7741b0058718afae162
|
||||
@@ -1,20 +0,0 @@
|
||||
using System;
|
||||
|
||||
using Colosseum.Enemy;
|
||||
|
||||
using Unity.Behavior;
|
||||
using Unity.Properties;
|
||||
|
||||
/// <summary>
|
||||
/// 보조 패턴 준비 여부를 확인하는 체크 액션입니다.
|
||||
/// </summary>
|
||||
[Serializable, GeneratePropertyBag]
|
||||
[NodeDescription(
|
||||
name: "Check Secondary Pattern Ready",
|
||||
story: "보조 패턴 준비 여부 확인",
|
||||
category: "Action",
|
||||
id: "72d4626f97fe4de4aedfda612961957f")]
|
||||
public partial class CheckSecondaryPatternReadyAction : CheckPatternReadyActionBase
|
||||
{
|
||||
protected override BossCombatPatternRole PatternRole => BossCombatPatternRole.Secondary;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e155d7ca234bf8148bef34617a3a8739
|
||||
@@ -1,26 +0,0 @@
|
||||
using System;
|
||||
|
||||
using Colosseum.Enemy;
|
||||
|
||||
using Unity.Behavior;
|
||||
using Unity.Properties;
|
||||
|
||||
using Action = Unity.Behavior.Action;
|
||||
|
||||
/// <summary>
|
||||
/// 현재 근접 패턴 차례가 보조 패턴인지 확인하는 공통 체크 액션입니다.
|
||||
/// </summary>
|
||||
[Serializable, GeneratePropertyBag]
|
||||
[NodeDescription(
|
||||
name: "Check Secondary Pattern Turn",
|
||||
story: "현재 근접 패턴 차례가 보조 패턴인지 확인",
|
||||
category: "Action",
|
||||
id: "e85477bd25894248aeeea8b41efc7f48")]
|
||||
public partial class CheckSecondaryPatternTurnAction : Action
|
||||
{
|
||||
protected override Status OnStart()
|
||||
{
|
||||
BossCombatBehaviorContext context = GameObject.GetComponent<BossCombatBehaviorContext>();
|
||||
return context != null && context.IsNextSecondaryPattern() ? Status.Success : Status.Failure;
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 435950077eea65d43beb6bfaba38dc60
|
||||
@@ -1,31 +0,0 @@
|
||||
using System;
|
||||
|
||||
using Colosseum.Enemy;
|
||||
|
||||
using Unity.Behavior;
|
||||
using Unity.Properties;
|
||||
|
||||
using Action = Unity.Behavior.Action;
|
||||
|
||||
/// <summary>
|
||||
/// 시그니처 패턴 사용 가능 여부를 확인하는 체크 액션입니다.
|
||||
/// </summary>
|
||||
[Serializable, GeneratePropertyBag]
|
||||
[NodeDescription(
|
||||
name: "Check Signature Pattern Ready",
|
||||
story: "시그니처 패턴 준비 여부 확인",
|
||||
category: "Action",
|
||||
id: "b3b2916257134e0eb3a71a5f544a8d6f")]
|
||||
public partial class CheckSignaturePatternReadyAction : Action
|
||||
{
|
||||
protected override Status OnStart()
|
||||
{
|
||||
BossCombatBehaviorContext context = GameObject.GetComponent<BossCombatBehaviorContext>();
|
||||
if (context != null && context.IsBehaviorSuppressed)
|
||||
return Status.Failure;
|
||||
|
||||
return context != null && context.IsSignaturePatternReady()
|
||||
? Status.Success
|
||||
: Status.Failure;
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b27f3137292d5704d802b5cfb58037e4
|
||||
@@ -1,35 +0,0 @@
|
||||
using System;
|
||||
|
||||
using Colosseum.Enemy;
|
||||
|
||||
using Unity.Behavior;
|
||||
using Unity.Properties;
|
||||
using UnityEngine;
|
||||
|
||||
using Action = Unity.Behavior.Action;
|
||||
|
||||
/// <summary>
|
||||
/// 현재 타겟이 보스의 공격 사거리 안에 있는지 확인하는 공통 체크 액션입니다.
|
||||
/// </summary>
|
||||
[Serializable, GeneratePropertyBag]
|
||||
[NodeDescription(
|
||||
name: "Check Target In Attack Range",
|
||||
story: "[Target]이 보스 공격 사거리 안에 있는지 확인",
|
||||
category: "Action",
|
||||
id: "16821bba281d49f699d1ac9ec613dcce")]
|
||||
public partial class CheckTargetInAttackRangeAction : Action
|
||||
{
|
||||
[SerializeReference]
|
||||
public BlackboardVariable<GameObject> Target;
|
||||
|
||||
protected override Status OnStart()
|
||||
{
|
||||
if (Target?.Value == null)
|
||||
return Status.Failure;
|
||||
|
||||
EnemyBase enemyBase = GameObject.GetComponent<EnemyBase>();
|
||||
float attackRange = enemyBase != null && enemyBase.Data != null ? enemyBase.Data.AttackRange : 2f;
|
||||
float distance = Vector3.Distance(GameObject.transform.position, Target.Value.transform.position);
|
||||
return distance <= attackRange + 0.25f ? Status.Success : Status.Failure;
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5b3844411f6dd784089c40c5d4325b45
|
||||
@@ -1,18 +0,0 @@
|
||||
using System;
|
||||
|
||||
using Colosseum.Enemy;
|
||||
|
||||
using Unity.Behavior;
|
||||
using Unity.Properties;
|
||||
|
||||
using Action = Unity.Behavior.Action;
|
||||
|
||||
/// <summary>
|
||||
/// 공통 원거리 견제 패턴의 준비 여부를 확인합니다.
|
||||
/// </summary>
|
||||
[Serializable, GeneratePropertyBag]
|
||||
[NodeDescription(name: "Check Utility Pattern Ready", story: "원거리 견제 패턴 준비 완료", category: "Action", id: "e3a3f4bd4f214efc873109631e5195db")]
|
||||
public partial class CheckUtilityPatternReadyAction : CheckPatternReadyActionBase
|
||||
{
|
||||
protected override BossCombatPatternRole PatternRole => BossCombatPatternRole.Utility;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 15de0eb23ee195a42a07c23c18f9fa9a
|
||||
@@ -1,20 +0,0 @@
|
||||
using System;
|
||||
|
||||
using Colosseum.Enemy;
|
||||
|
||||
using Unity.Behavior;
|
||||
using Unity.Properties;
|
||||
|
||||
/// <summary>
|
||||
/// 기동 패턴을 실행하는 공통 액션입니다.
|
||||
/// </summary>
|
||||
[Serializable, GeneratePropertyBag]
|
||||
[NodeDescription(
|
||||
name: "Use Mobility Pattern",
|
||||
story: "기동 패턴 실행",
|
||||
category: "Action",
|
||||
id: "bb19ca5ae11a4d9586180f7cba9f76cc")]
|
||||
public partial class UseMobilityPatternAction : UsePatternRoleActionBase
|
||||
{
|
||||
protected override BossCombatPatternRole PatternRole => BossCombatPatternRole.Mobility;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 137700b0db09e724899700f0da861132
|
||||
@@ -0,0 +1,153 @@
|
||||
using System;
|
||||
|
||||
using Colosseum.AI;
|
||||
using Colosseum.Enemy;
|
||||
|
||||
using Unity.Behavior;
|
||||
using Unity.Properties;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 패턴을 실행하는 범용 액션 노드입니다.
|
||||
/// Pattern 필드에 BossPatternData 에셋을 직접 할당합니다.
|
||||
/// 타겟 해석과 등록은 Condition에서 처리되므로, 이 액션은 순수하게 패턴만 실행합니다.
|
||||
/// 시그니처 패턴은 내부적으로 TryStartSignaturePattern 경로를 사용합니다.
|
||||
/// </summary>
|
||||
[Serializable, GeneratePropertyBag]
|
||||
[NodeDescription(
|
||||
name: "Use Pattern By Role",
|
||||
story: "[Pattern] 패턴 실행",
|
||||
category: "Action",
|
||||
id: "b2c3d4e5-1111-2222-3333-555566667777")]
|
||||
public partial class UsePatternByRoleAction : BossPatternActionBase
|
||||
{
|
||||
[SerializeReference]
|
||||
[Tooltip("실행할 패턴")]
|
||||
public BlackboardVariable<BossPatternData> Pattern;
|
||||
|
||||
/// <summary>
|
||||
/// 시그니처 패턴 실행 상태 추적
|
||||
/// </summary>
|
||||
private bool signatureStarted;
|
||||
|
||||
protected override Status OnStart()
|
||||
{
|
||||
BossPatternData pattern = Pattern?.Value;
|
||||
if (pattern == null)
|
||||
return Status.Failure;
|
||||
|
||||
if (pattern.IsSignature)
|
||||
return StartSignaturePattern();
|
||||
|
||||
// 타겟 해석은 ResolveStepTarget에서 처리됨
|
||||
// 여기서는 RegisterPatternUse만 호출 (근접 패턴 전용)
|
||||
if (pattern.IsMelee)
|
||||
{
|
||||
BossCombatBehaviorContext context = GameObject.GetComponent<BossCombatBehaviorContext>();
|
||||
context?.RegisterPatternUse(pattern);
|
||||
}
|
||||
|
||||
// base.OnStart는 TryResolvePattern → ExecuteCurrentStep 호출
|
||||
return base.OnStart();
|
||||
}
|
||||
|
||||
protected override Status OnUpdate()
|
||||
{
|
||||
BossPatternData pattern = Pattern?.Value;
|
||||
if (pattern == null)
|
||||
return Status.Failure;
|
||||
|
||||
if (pattern.IsSignature)
|
||||
return UpdateSignaturePattern();
|
||||
|
||||
return base.OnUpdate();
|
||||
}
|
||||
|
||||
protected override void OnEnd()
|
||||
{
|
||||
if (signatureStarted)
|
||||
{
|
||||
signatureStarted = false;
|
||||
return;
|
||||
}
|
||||
|
||||
base.OnEnd();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 시그니처 패턴 시작
|
||||
/// </summary>
|
||||
private Status StartSignaturePattern()
|
||||
{
|
||||
BossCombatBehaviorContext context = GameObject.GetComponent<BossCombatBehaviorContext>();
|
||||
if (context == null)
|
||||
return Status.Failure;
|
||||
|
||||
GameObject target = Target != null ? Target.Value : null;
|
||||
signatureStarted = context.TryStartSignaturePattern(target);
|
||||
return signatureStarted ? Status.Running : Status.Failure;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 시그니처 패턴 업데이트
|
||||
/// </summary>
|
||||
private Status UpdateSignaturePattern()
|
||||
{
|
||||
if (!signatureStarted)
|
||||
return Status.Failure;
|
||||
|
||||
BossCombatBehaviorContext context = GameObject.GetComponent<BossCombatBehaviorContext>();
|
||||
if (context == null)
|
||||
return Status.Failure;
|
||||
|
||||
return context.IsSignaturePatternActive
|
||||
? Status.Running
|
||||
: Status.Success;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// BossPatternActionBase.TryResolvePattern 구현.
|
||||
/// Condition에서 이미 타겟을 해석했으므로, Target.Value를 그대로 사용합니다.
|
||||
/// </summary>
|
||||
protected override bool TryResolvePattern(out BossPatternData pattern, out GameObject target)
|
||||
{
|
||||
pattern = Pattern?.Value;
|
||||
target = Target != null ? Target.Value : null;
|
||||
|
||||
if (pattern == null)
|
||||
return false;
|
||||
|
||||
if (!UsePatternAction.IsPatternReady(GameObject, pattern))
|
||||
return false;
|
||||
|
||||
if (target == null)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override GameObject ResolveStepTarget(GameObject fallbackTarget)
|
||||
{
|
||||
BossPatternData pattern = Pattern?.Value;
|
||||
if (pattern == null)
|
||||
return base.ResolveStepTarget(fallbackTarget);
|
||||
|
||||
BossCombatBehaviorContext context = GameObject.GetComponent<BossCombatBehaviorContext>();
|
||||
if (context == null)
|
||||
return base.ResolveStepTarget(fallbackTarget);
|
||||
|
||||
TargetResolveMode targetMode = pattern.TargetMode;
|
||||
|
||||
if (targetMode == TargetResolveMode.Mobility)
|
||||
return context.IsValidMobilityTarget(fallbackTarget)
|
||||
? fallbackTarget
|
||||
: context.FindMobilityTarget();
|
||||
|
||||
if (targetMode == TargetResolveMode.Utility)
|
||||
return context.IsValidUtilityTarget(fallbackTarget)
|
||||
? fallbackTarget
|
||||
: context.FindUtilityTarget();
|
||||
|
||||
return base.ResolveStepTarget(fallbackTarget);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 330e28c06aa715a4999a2ac322ee7748
|
||||
@@ -1,67 +0,0 @@
|
||||
using System;
|
||||
|
||||
using Colosseum.AI;
|
||||
using Colosseum.Enemy;
|
||||
|
||||
using Unity.Behavior;
|
||||
using Unity.Properties;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 공통 패턴 역할을 실행하는 액션 기반 클래스입니다.
|
||||
/// </summary>
|
||||
[Serializable, GeneratePropertyBag]
|
||||
public abstract partial class UsePatternRoleActionBase : BossPatternActionBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 현재 액션이 실행할 공통 패턴 역할입니다.
|
||||
/// </summary>
|
||||
protected abstract BossCombatPatternRole PatternRole { get; }
|
||||
|
||||
protected override bool TryResolvePattern(out BossPatternData pattern, out GameObject target)
|
||||
{
|
||||
BossCombatBehaviorContext context = GameObject.GetComponent<BossCombatBehaviorContext>();
|
||||
pattern = context != null ? context.GetPattern(PatternRole) : null;
|
||||
target = Target != null ? Target.Value : null;
|
||||
|
||||
if (pattern == null || !UsePatternAction.IsPatternReady(GameObject, pattern))
|
||||
return false;
|
||||
|
||||
if (target == null && PatternRole.IsMeleeRole())
|
||||
target = ResolvePrimaryTarget();
|
||||
|
||||
if (target == null && PatternRole == BossCombatPatternRole.Mobility)
|
||||
target = context != null ? context.FindMobilityTarget() : null;
|
||||
|
||||
if (target == null && PatternRole == BossCombatPatternRole.Utility)
|
||||
target = context != null ? context.FindUtilityTarget() : null;
|
||||
|
||||
if (target == null)
|
||||
return false;
|
||||
|
||||
if (PatternRole.IsMeleeRole() && context != null)
|
||||
context.RegisterPatternUse(PatternRole);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override GameObject ResolveStepTarget(GameObject fallbackTarget)
|
||||
{
|
||||
BossCombatBehaviorContext context = GameObject.GetComponent<BossCombatBehaviorContext>();
|
||||
if (PatternRole == BossCombatPatternRole.Mobility && context != null)
|
||||
{
|
||||
return context.IsValidMobilityTarget(fallbackTarget)
|
||||
? fallbackTarget
|
||||
: context.FindMobilityTarget();
|
||||
}
|
||||
|
||||
if (PatternRole == BossCombatPatternRole.Utility && context != null)
|
||||
{
|
||||
return context.IsValidUtilityTarget(fallbackTarget)
|
||||
? fallbackTarget
|
||||
: context.FindUtilityTarget();
|
||||
}
|
||||
|
||||
return base.ResolveStepTarget(fallbackTarget);
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 33384929fd7ec3c4f9240ac748de185c
|
||||
@@ -1,20 +0,0 @@
|
||||
using System;
|
||||
|
||||
using Colosseum.Enemy;
|
||||
|
||||
using Unity.Behavior;
|
||||
using Unity.Properties;
|
||||
|
||||
/// <summary>
|
||||
/// 기본 패턴을 실행하는 공통 액션입니다.
|
||||
/// </summary>
|
||||
[Serializable, GeneratePropertyBag]
|
||||
[NodeDescription(
|
||||
name: "Use Primary Pattern",
|
||||
story: "기본 패턴 실행",
|
||||
category: "Action",
|
||||
id: "45d71c690f6342bcbbd348b6df5b77f1")]
|
||||
public partial class UsePrimaryPatternAction : UsePatternRoleActionBase
|
||||
{
|
||||
protected override BossCombatPatternRole PatternRole => BossCombatPatternRole.Primary;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2d8de0b13ad776845a14b35e16485f53
|
||||
@@ -1,20 +0,0 @@
|
||||
using System;
|
||||
|
||||
using Colosseum.Enemy;
|
||||
|
||||
using Unity.Behavior;
|
||||
using Unity.Properties;
|
||||
|
||||
/// <summary>
|
||||
/// 징벌 패턴을 실행하는 공통 액션입니다.
|
||||
/// </summary>
|
||||
[Serializable, GeneratePropertyBag]
|
||||
[NodeDescription(
|
||||
name: "Use Punish Pattern",
|
||||
story: "징벌 패턴 실행",
|
||||
category: "Action",
|
||||
id: "55f3c204a22b42dca6ae96e555f11a70")]
|
||||
public partial class UsePunishPatternAction : UsePatternRoleActionBase
|
||||
{
|
||||
protected override BossCombatPatternRole PatternRole => BossCombatPatternRole.Punish;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 16cef0dbbe7946d46b3021b0c1802669
|
||||
@@ -1,20 +0,0 @@
|
||||
using System;
|
||||
|
||||
using Colosseum.Enemy;
|
||||
|
||||
using Unity.Behavior;
|
||||
using Unity.Properties;
|
||||
|
||||
/// <summary>
|
||||
/// 보조 패턴을 실행하는 공통 액션입니다.
|
||||
/// </summary>
|
||||
[Serializable, GeneratePropertyBag]
|
||||
[NodeDescription(
|
||||
name: "Use Secondary Pattern",
|
||||
story: "보조 패턴 실행",
|
||||
category: "Action",
|
||||
id: "5169d341ce0c4400ae7fa3b58dde5b7a")]
|
||||
public partial class UseSecondaryPatternAction : UsePatternRoleActionBase
|
||||
{
|
||||
protected override BossCombatPatternRole PatternRole => BossCombatPatternRole.Secondary;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 79cd3375939c8a244bad9d8e1f02a45d
|
||||
@@ -1,54 +0,0 @@
|
||||
using System;
|
||||
|
||||
using Colosseum.Enemy;
|
||||
|
||||
using Unity.Behavior;
|
||||
using Unity.Properties;
|
||||
using UnityEngine;
|
||||
|
||||
using Action = Unity.Behavior.Action;
|
||||
|
||||
/// <summary>
|
||||
/// 보스 공통 시그니처 패턴을 실행하는 액션입니다.
|
||||
/// </summary>
|
||||
[Serializable, GeneratePropertyBag]
|
||||
[NodeDescription(
|
||||
name: "Use Signature Pattern",
|
||||
story: "시그니처 패턴 실행",
|
||||
category: "Action",
|
||||
id: "178f8888d56042c6a75b4d6ee8a7a7d4")]
|
||||
public partial class UseSignaturePatternAction : Action
|
||||
{
|
||||
[SerializeReference]
|
||||
public BlackboardVariable<GameObject> Target;
|
||||
|
||||
private BossCombatBehaviorContext combatBehaviorContext;
|
||||
private bool started;
|
||||
|
||||
protected override Status OnStart()
|
||||
{
|
||||
combatBehaviorContext = GameObject.GetComponent<BossCombatBehaviorContext>();
|
||||
if (combatBehaviorContext == null)
|
||||
return Status.Failure;
|
||||
|
||||
GameObject target = Target != null ? Target.Value : null;
|
||||
started = combatBehaviorContext.TryStartSignaturePattern(target);
|
||||
return started ? Status.Running : Status.Failure;
|
||||
}
|
||||
|
||||
protected override Status OnUpdate()
|
||||
{
|
||||
if (!started || combatBehaviorContext == null)
|
||||
return Status.Failure;
|
||||
|
||||
return combatBehaviorContext.IsSignaturePatternActive
|
||||
? Status.Running
|
||||
: Status.Success;
|
||||
}
|
||||
|
||||
protected override void OnEnd()
|
||||
{
|
||||
started = false;
|
||||
combatBehaviorContext = null;
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0680aed4d244d7844918883e06e718d5
|
||||
@@ -1,16 +0,0 @@
|
||||
using System;
|
||||
|
||||
using Colosseum.Enemy;
|
||||
|
||||
using Unity.Behavior;
|
||||
using Unity.Properties;
|
||||
|
||||
/// <summary>
|
||||
/// 공통 원거리 견제 패턴을 실행합니다.
|
||||
/// </summary>
|
||||
[Serializable, GeneratePropertyBag]
|
||||
[NodeDescription(name: "Use Utility Pattern", story: "원거리 견제 패턴 실행", category: "Action", id: "f29d4556f2d04f6bb80418f9f9fe2c68")]
|
||||
public partial class UseUtilityPatternAction : UsePatternRoleActionBase
|
||||
{
|
||||
protected override BossCombatPatternRole PatternRole => BossCombatPatternRole.Utility;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 36c98678f964a7447bede88fedc04561
|
||||
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
|
||||
using Colosseum.AI;
|
||||
|
||||
using Unity.Behavior;
|
||||
using Unity.Properties;
|
||||
using UnityEngine;
|
||||
|
||||
using Condition = Unity.Behavior.Condition;
|
||||
|
||||
namespace Colosseum.AI.BehaviorActions.Conditions
|
||||
{
|
||||
/// <summary>
|
||||
/// 지정된 패턴이 준비되었는지 확인하는 범용 조건 노드입니다.
|
||||
/// Pattern 필드에 BossPatternData 에셋을 직접 할당합니다.
|
||||
/// </summary>
|
||||
[Serializable, GeneratePropertyBag]
|
||||
[Condition(name: "Check Pattern Ready", story: "[Pattern] 패턴이 준비되었는가?", id: "a1b2c3d4-1111-2222-3333-444455556666")]
|
||||
[NodeDescription(
|
||||
name: "Check Pattern Ready",
|
||||
story: "Check [Pattern] pattern ready",
|
||||
category: "Condition/Pattern")]
|
||||
public partial class CheckPatternReadyCondition : Condition
|
||||
{
|
||||
[SerializeReference]
|
||||
[Tooltip("준비 여부를 확인할 패턴")]
|
||||
public BlackboardVariable<BossPatternData> Pattern;
|
||||
|
||||
public override bool IsTrue()
|
||||
{
|
||||
BossPatternData pattern = Pattern?.Value;
|
||||
if (pattern == null)
|
||||
return false;
|
||||
|
||||
return PatternReadyHelper.IsPatternReady(GameObject, pattern);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b1a4d12e73d0f4a40a3a1d5a9c1fce6e
|
||||
@@ -0,0 +1,63 @@
|
||||
using System;
|
||||
|
||||
using Colosseum.Combat;
|
||||
using Colosseum.Player;
|
||||
|
||||
using Unity.Behavior;
|
||||
using Unity.Properties;
|
||||
using UnityEngine;
|
||||
|
||||
using Condition = Unity.Behavior.Condition;
|
||||
|
||||
namespace Colosseum.AI.BehaviorActions.Conditions
|
||||
{
|
||||
/// <summary>
|
||||
/// 다운된 적대 대상이 지정 반경 이내에 존재하는지 확인합니다.
|
||||
/// 징벌(Punish) 패턴의 전제 조건으로 사용됩니다.
|
||||
/// </summary>
|
||||
[Serializable, GeneratePropertyBag]
|
||||
[Condition(name: "Downed Target In Range", story: "다운된 대상이 [{SearchRadius}]m 이내에 있는가?", id: "d4e5f6a7-3333-4444-555566667777")]
|
||||
[NodeDescription(
|
||||
name: "Downed Target In Range",
|
||||
story: "Downed target within [{SearchRadius}]m",
|
||||
category: "Condition/Pattern")]
|
||||
public partial class IsDownedTargetInRangeCondition : Condition
|
||||
{
|
||||
[Min(0f)]
|
||||
[Tooltip("다운된 대상을 탐색할 최대 반경")]
|
||||
[SerializeField]
|
||||
private float searchRadius = 6f;
|
||||
|
||||
public override bool IsTrue()
|
||||
{
|
||||
HitReactionController[] controllers = UnityEngine.Object.FindObjectsByType<HitReactionController>(FindObjectsSortMode.None);
|
||||
|
||||
for (int i = 0; i < controllers.Length; i++)
|
||||
{
|
||||
HitReactionController controller = controllers[i];
|
||||
if (controller == null || !controller.IsDowned)
|
||||
continue;
|
||||
|
||||
GameObject candidate = controller.gameObject;
|
||||
if (candidate == null || !candidate.activeInHierarchy)
|
||||
continue;
|
||||
|
||||
if (candidate == GameObject)
|
||||
continue;
|
||||
|
||||
if (Team.IsSameTeam(GameObject, candidate))
|
||||
continue;
|
||||
|
||||
IDamageable damageable = candidate.GetComponent<IDamageable>();
|
||||
if (damageable != null && damageable.IsDead)
|
||||
continue;
|
||||
|
||||
float distance = Vector3.Distance(GameObject.transform.position, candidate.transform.position);
|
||||
if (distance <= searchRadius)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bc4fae13a78a0fb46863950d1c6b5b8d
|
||||
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
|
||||
using Colosseum.Enemy;
|
||||
|
||||
using Unity.Behavior;
|
||||
using Unity.Properties;
|
||||
using UnityEngine;
|
||||
|
||||
using Condition = Unity.Behavior.Condition;
|
||||
|
||||
namespace Colosseum.AI.BehaviorActions.Conditions
|
||||
{
|
||||
/// <summary>
|
||||
/// 현재 보스 페이즈가 지정된 최소 페이즈 이상인지 확인하는 조건 노드입니다.
|
||||
/// 패턴의 Phase 진입 조건을 BT에서 시각적으로 확인할 수 있습니다.
|
||||
/// </summary>
|
||||
[Serializable, GeneratePropertyBag]
|
||||
[Condition(name: "Min Phase Satisfied", story: "현재 페이즈가 [MinPhase] 이상인가?", id: "e3f4a5b6-7777-8888-9999-ddddddddeeee")]
|
||||
[NodeDescription(
|
||||
name: "Min Phase Satisfied",
|
||||
story: "현재 페이즈가 [MinPhase] 이상인가?",
|
||||
category: "Condition/Phase")]
|
||||
public partial class IsMinPhaseSatisfiedCondition : Condition
|
||||
{
|
||||
[SerializeReference]
|
||||
[Tooltip("최소 요구 페이즈 (1=Phase 1부터)")]
|
||||
public BlackboardVariable<int> MinPhase;
|
||||
|
||||
public override bool IsTrue()
|
||||
{
|
||||
int minPhase = MinPhase?.Value ?? 1;
|
||||
if (minPhase <= 1)
|
||||
return true;
|
||||
|
||||
BossCombatBehaviorContext context = GameObject.GetComponent<BossCombatBehaviorContext>();
|
||||
return context != null && context.CurrentPatternPhase >= minPhase;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 46c95824ad6561f44833252a6f25852a
|
||||
@@ -0,0 +1,65 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
using Colosseum.Combat;
|
||||
|
||||
using Unity.Behavior;
|
||||
using Unity.Properties;
|
||||
using UnityEngine;
|
||||
|
||||
using Condition = Unity.Behavior.Condition;
|
||||
|
||||
namespace Colosseum.AI.BehaviorActions.Conditions
|
||||
{
|
||||
/// <summary>
|
||||
/// 지정 거리 이상 떨어진 적대 대상이 존재하는지 확인합니다.
|
||||
/// 기동(도약) 또는 유틸리티(투척) 패턴의 전제 조건으로 사용됩니다.
|
||||
/// </summary>
|
||||
[Serializable, GeneratePropertyBag]
|
||||
[Condition(name: "Target Beyond Distance", story: "[{MinDistance}]m 이상 떨어진 대상이 있는가?", id: "e5f6a7b8-4444-5555-666677778888")]
|
||||
[NodeDescription(
|
||||
name: "Target Beyond Distance",
|
||||
story: "Target beyond [{MinDistance}]m exists",
|
||||
category: "Condition/Pattern")]
|
||||
public partial class IsTargetBeyondDistanceCondition : Condition
|
||||
{
|
||||
[Min(0f)]
|
||||
[Tooltip("이 거리 이상 떨어진 대상이 있는지 확인")]
|
||||
[SerializeField]
|
||||
private float minDistance = 8f;
|
||||
|
||||
public override bool IsTrue()
|
||||
{
|
||||
IDamageable[] targets = UnityEngine.Object.FindObjectsByType<MonoBehaviour>(FindObjectsSortMode.None)
|
||||
.OfType<IDamageable>()
|
||||
.ToArray();
|
||||
|
||||
for (int i = 0; i < targets.Length; i++)
|
||||
{
|
||||
IDamageable target = targets[i];
|
||||
if (target == null)
|
||||
continue;
|
||||
|
||||
Component component = target as Component;
|
||||
if (component == null || !component.gameObject.activeInHierarchy)
|
||||
continue;
|
||||
|
||||
GameObject candidate = component.gameObject;
|
||||
if (candidate == GameObject)
|
||||
continue;
|
||||
|
||||
if (Team.IsSameTeam(GameObject, candidate))
|
||||
continue;
|
||||
|
||||
if (target.IsDead)
|
||||
continue;
|
||||
|
||||
float distance = Vector3.Distance(GameObject.transform.position, candidate.transform.position);
|
||||
if (distance >= minDistance)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2e40fb41bbe354f4dafbe5b94fc6f9da
|
||||
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
|
||||
using Colosseum.Enemy;
|
||||
|
||||
using Unity.Behavior;
|
||||
using Unity.Properties;
|
||||
using UnityEngine;
|
||||
|
||||
using Condition = Unity.Behavior.Condition;
|
||||
|
||||
namespace Colosseum.AI.BehaviorActions.Conditions
|
||||
{
|
||||
/// <summary>
|
||||
/// 현재 타겟이 보스의 공격 사거리 안에 있는지 확인하는 조건 노드입니다.
|
||||
/// </summary>
|
||||
[Serializable, GeneratePropertyBag]
|
||||
[Condition(name: "Is Target In Attack Range", story: "타겟이 공격 사거리 안에 있는가?", id: "57370b5b23f82a54dabc4f189a23286a")]
|
||||
[NodeDescription(
|
||||
name: "Is Target In Attack Range",
|
||||
story: "Is [Target] in attack range",
|
||||
category: "Condition/Combat")]
|
||||
public partial class IsTargetInAttackRangeCondition : Condition
|
||||
{
|
||||
[SerializeReference]
|
||||
public BlackboardVariable<GameObject> Target;
|
||||
|
||||
public override bool IsTrue()
|
||||
{
|
||||
if (Target?.Value == null)
|
||||
return false;
|
||||
|
||||
EnemyBase enemyBase = GameObject.GetComponent<EnemyBase>();
|
||||
float attackRange = enemyBase != null && enemyBase.Data != null ? enemyBase.Data.AttackRange : 2f;
|
||||
float distance = Vector3.Distance(GameObject.transform.position, Target.Value.transform.position);
|
||||
return distance <= attackRange + 0.25f;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 57370b5b23f82a54dabc4f189a23286a
|
||||
@@ -0,0 +1,53 @@
|
||||
using UnityEngine;
|
||||
|
||||
using Colosseum.AI;
|
||||
using Colosseum.Enemy;
|
||||
|
||||
namespace Colosseum.AI.BehaviorActions.Conditions
|
||||
{
|
||||
/// <summary>
|
||||
/// 패턴 준비 여부를 확인하는 공통 헬퍼 메서드를 제공합니다.
|
||||
/// </summary>
|
||||
public static class PatternReadyHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 지정된 패턴이 현재 실행 가능한지 확인합니다.
|
||||
/// 패턴의 특성 필드를 사용하여 grace period 등을 판단합니다.
|
||||
/// </summary>
|
||||
public static bool IsPatternReady(GameObject gameObject, BossPatternData pattern)
|
||||
{
|
||||
if (pattern == null)
|
||||
return false;
|
||||
|
||||
if (pattern.IsSignature)
|
||||
return IsSignatureReady(gameObject);
|
||||
|
||||
BossCombatBehaviorContext context = gameObject.GetComponent<BossCombatBehaviorContext>();
|
||||
if (context == null)
|
||||
return false;
|
||||
|
||||
if (context.IsBehaviorSuppressed)
|
||||
return false;
|
||||
|
||||
if (context.CurrentPatternPhase < pattern.MinPhase)
|
||||
return false;
|
||||
|
||||
if (!context.IsPatternGracePeriodAllowed(pattern))
|
||||
return false;
|
||||
|
||||
return UsePatternAction.IsPatternReady(gameObject, pattern);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 시그니처 패턴 전용 준비 여부 확인.
|
||||
/// </summary>
|
||||
private static bool IsSignatureReady(GameObject gameObject)
|
||||
{
|
||||
BossCombatBehaviorContext context = gameObject.GetComponent<BossCombatBehaviorContext>();
|
||||
if (context == null)
|
||||
return false;
|
||||
|
||||
return context.IsSignaturePatternReady();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7a0f2fd53cb729c4f97223570292e25c
|
||||
@@ -4,6 +4,34 @@ using Colosseum.Skills;
|
||||
|
||||
namespace Colosseum.AI
|
||||
{
|
||||
/// <summary>
|
||||
/// 패턴의 대분류. grace period 판단에 사용됩니다.
|
||||
/// </summary>
|
||||
public enum PatternCategory
|
||||
{
|
||||
/// <summary>기본 패턴 — grace period 제한 없음</summary>
|
||||
Basic,
|
||||
/// <summary>대형 패턴 — basicLoopCount 이후 사용 가능</summary>
|
||||
Big,
|
||||
/// <summary>징벌 패턴 — 항상 허용, bigPattern 카운터 리셋</summary>
|
||||
Punish,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 패턴의 타겟 해석 방식
|
||||
/// </summary>
|
||||
public enum TargetResolveMode
|
||||
{
|
||||
/// <summary>타겟 해석 불필요 (시그니처 등 내부 처리)</summary>
|
||||
None,
|
||||
/// <summary>가장 위협도가 높은 근접 대상</summary>
|
||||
HighestThreat,
|
||||
/// <summary>기동 패턴 전용 타겟</summary>
|
||||
Mobility,
|
||||
/// <summary>유틸리티 패턴 전용 타겟</summary>
|
||||
Utility,
|
||||
}
|
||||
|
||||
public enum PatternStepType { Skill, Wait }
|
||||
|
||||
[System.Serializable]
|
||||
@@ -23,6 +51,16 @@ namespace Colosseum.AI
|
||||
[Header("패턴 정보")]
|
||||
[SerializeField] private string patternName;
|
||||
|
||||
[Header("패턴 특성")]
|
||||
[Tooltip("패턴 분류 — grace period 판단에 사용")]
|
||||
[SerializeField] private PatternCategory category = PatternCategory.Basic;
|
||||
[Tooltip("시그니처 패턴 여부 — 전용 실행 경로 사용")]
|
||||
[SerializeField] private bool isSignature;
|
||||
[Tooltip("근접 패턴 여부 — meleePatternCounter 갱신")]
|
||||
[SerializeField] private bool isMelee;
|
||||
[Tooltip("타겟 해석 방식")]
|
||||
[SerializeField] private TargetResolveMode targetMode = TargetResolveMode.HighestThreat;
|
||||
|
||||
[Header("스텝 순서")]
|
||||
[SerializeField] private List<PatternStep> steps = new List<PatternStep>();
|
||||
|
||||
@@ -36,9 +74,23 @@ namespace Colosseum.AI
|
||||
[Tooltip("이 패턴을 사용하기 시작하는 최소 페이즈 (1=Phase 1부터)")]
|
||||
[SerializeField] private int minPhase = 1;
|
||||
|
||||
[Header("조건부 점프")]
|
||||
[Tooltip("점프 스텝에서 대상을 찾지 못하면 해당 스텝을 스킵하고 패턴을 종료합니다 (조합 패턴용)")]
|
||||
[SerializeField] private bool skipJumpStepOnNoTarget = false;
|
||||
|
||||
public string PatternName => patternName;
|
||||
public PatternCategory Category => category;
|
||||
public bool IsSignature => isSignature;
|
||||
public bool IsMelee => isMelee;
|
||||
public TargetResolveMode TargetMode => targetMode;
|
||||
public IReadOnlyList<PatternStep> Steps => steps;
|
||||
public float Cooldown => cooldown;
|
||||
public int MinPhase => minPhase;
|
||||
public bool SkipJumpStepOnNoTarget => skipJumpStepOnNoTarget;
|
||||
|
||||
/// <summary>
|
||||
/// Big 패턴인지 반환합니다 (grace period 대상).
|
||||
/// </summary>
|
||||
public bool IsBigPattern => category == PatternCategory.Big;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
using Colosseum.AI;
|
||||
using Colosseum.AI.BehaviorActions.Conditions;
|
||||
using Colosseum.Enemy;
|
||||
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -10,6 +15,7 @@ namespace Colosseum.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// 드로그 Behavior Graph authoring 자산을 현재 BT 우선순위 구조로 재생성합니다.
|
||||
/// Check 노드는 ConditionalGuardAction + Condition 조합으로 구현됩니다.
|
||||
/// </summary>
|
||||
public static class RebuildDrogBehaviorAuthoringGraph
|
||||
{
|
||||
@@ -21,18 +27,44 @@ namespace Colosseum.Editor
|
||||
UnityEngine.Object graphAsset = AssetDatabase.LoadMainAssetAtPath(GraphAssetPath);
|
||||
if (graphAsset == null)
|
||||
{
|
||||
Debug.LogError($"[DrogBTRebuild] 그래프 자산을 찾을 수 없습니다: {GraphAssetPath}");
|
||||
// 에셋이 없으면 기존 에셋 경로의 타입을 리플렉션으로 찾아 생성합니다.
|
||||
// BehaviorAuthoringGraph는 Unity.Behavior.Editor 어셈블리에 있습니다.
|
||||
Type authoringGraphType = null;
|
||||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
authoringGraphType = assembly.GetType("Unity.Behavior.Authoring.BehaviorAuthoringGraph");
|
||||
if (authoringGraphType != null)
|
||||
break;
|
||||
}
|
||||
|
||||
if (authoringGraphType == null)
|
||||
{
|
||||
Debug.LogError("[DrogBTRebuild] BehaviorAuthoringGraph 타입을 모든 어셈블리에서 찾지 못했습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
graphAsset = ScriptableObject.CreateInstance(authoringGraphType);
|
||||
if (graphAsset == null)
|
||||
{
|
||||
Debug.LogError("[DrogBTRebuild] BehaviorAuthoringGraph 인스턴스를 생성할 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
AssetDatabase.CreateAsset(graphAsset, GraphAssetPath);
|
||||
AssetDatabase.SaveAssets();
|
||||
Debug.Log("[DrogBTRebuild] 새 그래프 자산을 생성했습니다.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Type authoringGraphType = graphAsset.GetType();
|
||||
Assembly authoringAssembly = authoringGraphType.Assembly;
|
||||
Assembly runtimeAssembly = typeof(Unity.Behavior.BehaviorGraph).Assembly;
|
||||
|
||||
// 기본 리플렉션 메서드
|
||||
MethodInfo createNodeMethod = authoringGraphType.BaseType?.GetMethod("CreateNode", BindingFlags.Instance | BindingFlags.Public);
|
||||
MethodInfo connectEdgeMethod = authoringGraphType.BaseType?.GetMethod("ConnectEdge", BindingFlags.Instance | BindingFlags.Public);
|
||||
MethodInfo createNodePortsMethod = authoringGraphType.BaseType?.GetMethod("CreateNodePortsForNode", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||||
MethodInfo buildRuntimeGraphMethod = authoringGraphType.GetMethod("BuildRuntimeGraph", BindingFlags.Instance | BindingFlags.Public);
|
||||
MethodInfo saveAssetMethod = authoringGraphType.BaseType?.GetMethod("SaveAsset", BindingFlags.Instance | BindingFlags.Public);
|
||||
MethodInfo setAssetDirtyMethod = authoringGraphType.BaseType?.GetMethod("SetAssetDirty", BindingFlags.Instance | BindingFlags.Public);
|
||||
@@ -45,16 +77,100 @@ namespace Colosseum.Editor
|
||||
return;
|
||||
}
|
||||
|
||||
SerializedObject serializedObject = new SerializedObject(graphAsset);
|
||||
SerializedProperty nodesProperty = serializedObject.FindProperty("m_Nodes");
|
||||
if (nodesProperty == null)
|
||||
// ConditionalGuard 리플렉션 타입 (internal)
|
||||
Type conditionalGuardType = runtimeAssembly.GetType("Unity.Behavior.ConditionalGuardAction");
|
||||
Type conditionUtilityType = authoringAssembly.GetType("Unity.Behavior.ConditionUtility");
|
||||
Type conditionModelType = authoringAssembly.GetType("Unity.Behavior.ConditionModel");
|
||||
Type graphNodeModelType = authoringAssembly.GetType("Unity.Behavior.BehaviorGraphNodeModel");
|
||||
Type conditionInfoType = authoringAssembly.GetType("Unity.Behavior.ConditionInfo");
|
||||
|
||||
if (conditionalGuardType == null) { Debug.LogError("[DrogBTRebuild] ConditionalGuardAction 타입을 찾지 못했습니다."); return; }
|
||||
if (conditionUtilityType == null) { Debug.LogError("[DrogBTRebuild] ConditionUtility 타입을 찾지 못했습니다."); return; }
|
||||
if (conditionModelType == null) { Debug.LogError("[DrogBTRebuild] ConditionModel 타입을 찾지 못했습니다."); return; }
|
||||
if (graphNodeModelType == null) { Debug.LogError("[DrogBTRebuild] BehaviorGraphNodeModel 타입을 찾지 못했습니다."); return; }
|
||||
if (conditionInfoType == null)
|
||||
{
|
||||
Debug.LogError("[DrogBTRebuild] m_Nodes 프로퍼티를 찾지 못했습니다.");
|
||||
Debug.LogError("[DrogBTRebuild] ConditionInfo 타입을 찾지 못했습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
nodesProperty.ClearArray();
|
||||
serializedObject.ApplyModifiedPropertiesWithoutUndo();
|
||||
Type branchCompositeType = runtimeAssembly.GetType("Unity.Behavior.BranchingConditionComposite");
|
||||
if (branchCompositeType == null)
|
||||
{
|
||||
Debug.LogError("[DrogBTRebuild] BranchingConditionComposite 타입을 찾지 못했습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
// SetField(string, VariableModel, Type) — 제네릭 버전과 구분하기 위해 파라미터 수로 필터링
|
||||
MethodInfo setFieldMethod = conditionModelType.GetMethods(BindingFlags.Instance | BindingFlags.NonPublic)
|
||||
.FirstOrDefault(m => m.Name == "SetField" && !m.IsGenericMethod && m.GetParameters().Length == 3);
|
||||
|
||||
// SetField<T>(string, T) — BehaviorGraphNodeModel 기반 클래스에서 조회 (ConditionModel과 Action 노드 모두 사용)
|
||||
MethodInfo setFieldValueMethod = graphNodeModelType.GetMethods(BindingFlags.Instance | BindingFlags.NonPublic)
|
||||
.FirstOrDefault(m => m.Name == "SetField" && m.IsGenericMethod && m.GetParameters().Length == 2);
|
||||
|
||||
if (setFieldMethod == null)
|
||||
{
|
||||
Debug.LogError("[DrogBTRebuild] ConditionModel.SetField 메서드를 찾지 못했습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (setFieldValueMethod == null)
|
||||
{
|
||||
Debug.LogError("[DrogBTRebuild] SetField<T> 제네릭 메서드를 찾지 못했습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 기존 에셋의 서브에셋(BehaviorGraph 등)에서 깨진 managed references 클리어
|
||||
Type behaviorGraphType = typeof(Unity.Behavior.BehaviorGraph);
|
||||
UnityEngine.Object[] subAssets = AssetDatabase.LoadAllAssetsAtPath(GraphAssetPath);
|
||||
foreach (var subAsset in subAssets)
|
||||
{
|
||||
if (subAsset != null && subAsset.GetType() == behaviorGraphType)
|
||||
{
|
||||
UnityEditor.SerializationUtility.ClearAllManagedReferencesWithMissingTypes(subAsset);
|
||||
EditorUtility.SetDirty(subAsset);
|
||||
}
|
||||
}
|
||||
|
||||
// AuthoringGraph 자체에서도 깨진 references 클리어
|
||||
UnityEditor.SerializationUtility.ClearAllManagedReferencesWithMissingTypes(graphAsset);
|
||||
|
||||
// 노드 클리어 — 전체 타입 계층에서 필드 찾기
|
||||
FieldInfo nodesField = FindFieldInHierarchy(authoringGraphType, "m_RootNodes");
|
||||
if (nodesField == null)
|
||||
{
|
||||
Debug.LogError("[DrogBTRebuild] m_RootNodes 필드를 타입 계층 전체에서 찾지 못했습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
nodesField.SetValue(graphAsset, Activator.CreateInstance(nodesField.FieldType));
|
||||
|
||||
FieldInfo nodesListField = FindFieldInHierarchy(authoringGraphType, "m_Nodes");
|
||||
if (nodesListField != null)
|
||||
nodesListField.SetValue(graphAsset, Activator.CreateInstance(nodesListField.FieldType));
|
||||
|
||||
FieldInfo nodeModelsInfoField = FindFieldInHierarchy(authoringGraphType, "m_NodeModelsInfo");
|
||||
if (nodeModelsInfoField != null)
|
||||
nodeModelsInfoField.SetValue(graphAsset, Activator.CreateInstance(nodeModelsInfoField.FieldType));
|
||||
|
||||
FieldInfo runtimeGraphField = FindFieldInHierarchy(authoringGraphType, "m_RuntimeGraph");
|
||||
if (runtimeGraphField != null)
|
||||
runtimeGraphField.SetValue(graphAsset, null);
|
||||
|
||||
// 클리어 후 에셋을 저장하고 다시 로드하여 잔류 참조가 메모리에 남지 않게 합니다.
|
||||
EditorUtility.SetDirty(graphAsset);
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
// 에셋을 다시 로드 (직렬화된 상태에서 로드하여 클리어 상태 확보)
|
||||
graphAsset = AssetDatabase.LoadMainAssetAtPath(GraphAssetPath);
|
||||
if (graphAsset == null)
|
||||
{
|
||||
Debug.LogError("[DrogBTRebuild] 에셋 재로드 실패.");
|
||||
return;
|
||||
}
|
||||
authoringGraphType = graphAsset.GetType();
|
||||
|
||||
object targetVariable = FindBlackboardVariableModel("Target");
|
||||
if (targetVariable == null)
|
||||
@@ -63,103 +179,205 @@ namespace Colosseum.Editor
|
||||
return;
|
||||
}
|
||||
|
||||
object startNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.Start", true), new Vector2(420f, -620f));
|
||||
object repeatNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.RepeaterModifier", true), new Vector2(420f, -470f));
|
||||
object selectorNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SelectorComposite", true), new Vector2(420f, -280f));
|
||||
// 구조 노드
|
||||
object startNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.Start", true), new Vector2(420f, -800f));
|
||||
object repeatNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.RepeaterModifier", true), new Vector2(420f, -620f));
|
||||
|
||||
object signatureSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(-1020f, -40f));
|
||||
object downSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(-780f, -40f));
|
||||
object utilitySequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(-380f, -40f));
|
||||
object leapSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(20f, -40f));
|
||||
object slamSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(420f, -40f));
|
||||
object mainSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(820f, -40f));
|
||||
object slamFallbackSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(1220f, -40f));
|
||||
object chaseSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(1620f, -40f));
|
||||
// ── 프리팹에서 패턴 에셋 로드 ──
|
||||
const string prefabPath = "Assets/_Game/Prefabs/Bosses/Prefab_Boss_Drog.prefab";
|
||||
GameObject prefab = AssetDatabase.LoadMainAssetAtPath(prefabPath) as GameObject;
|
||||
BossCombatBehaviorContext context = prefab?.GetComponent<BossCombatBehaviorContext>();
|
||||
if (context == null)
|
||||
{
|
||||
Debug.LogError("[DrogBTRebuild] 드로그 프리팹에서 BossCombatBehaviorContext를 찾지 못했습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
object signatureRefreshNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(RefreshPrimaryTargetAction), new Vector2(-1140f, 240f));
|
||||
object signatureHasTargetNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ValidateTargetAction), new Vector2(-1020f, 240f));
|
||||
object signatureReadyNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(CheckSignaturePatternReadyAction), new Vector2(-900f, 240f));
|
||||
object signatureUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UseSignaturePatternAction), new Vector2(-780f, 240f));
|
||||
// protected 필드에서 BossPatternData 에셋 읽기 (리플렉션)
|
||||
BossPatternData punishPattern = ReadProtectedField<BossPatternData>(context, "punishPattern");
|
||||
BossPatternData signaturePattern = ReadProtectedField<BossPatternData>(context, "signaturePattern");
|
||||
BossPatternData mobilityPattern = ReadProtectedField<BossPatternData>(context, "mobilityPattern");
|
||||
BossPatternData comboPattern = ReadProtectedField<BossPatternData>(context, "comboPattern");
|
||||
BossPatternData primaryPattern = ReadProtectedField<BossPatternData>(context, "primaryPattern");
|
||||
BossPatternData utilityPattern = ReadProtectedField<BossPatternData>(context, "utilityPattern");
|
||||
float punishSearchRadius = ReadProtectedFieldValue<float>(context, "punishSearchRadius", 6f);
|
||||
|
||||
object downSelectNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(SelectNearestDownedTargetAction), new Vector2(-740f, 240f));
|
||||
object downReadyNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(CheckPunishPatternReadyAction), new Vector2(-620f, 240f));
|
||||
object downUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UsePunishPatternAction), new Vector2(-500f, 240f));
|
||||
// 필수 패턴 검증 (combo는 선택 — 할당되지 않은 경우 해당 Branch만 생략)
|
||||
if (punishPattern == null || signaturePattern == null || mobilityPattern == null ||
|
||||
primaryPattern == null || utilityPattern == null)
|
||||
{
|
||||
Debug.LogError("[DrogBTRebuild] 프리팹에서 필수 패턴 에셋을 읽지 못했습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
object utilitySelectNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(SelectAlternateTargetByDistanceAction), new Vector2(-500f, 240f));
|
||||
object utilityReadyNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(CheckUtilityPatternReadyAction), new Vector2(-380f, 240f));
|
||||
object utilityUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UseUtilityPatternAction), new Vector2(-260f, 240f));
|
||||
if (comboPattern == null)
|
||||
Debug.LogWarning("[DrogBTRebuild] comboPattern이 할당되지 않았습니다. 해당 Branch를 생략합니다.");
|
||||
|
||||
object leapSelectNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(SelectTargetByDistanceAction), new Vector2(-100f, 240f));
|
||||
object leapReadyNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(CheckMobilityPatternReadyAction), new Vector2(20f, 240f));
|
||||
object leapUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UseMobilityPatternAction), new Vector2(140f, 240f));
|
||||
// ── 계단식 우선순위 체인 ──
|
||||
// 설계안 우선순위: 다운 추가타 > 도약 > 집행 개시 > 기본 루프 > 조합 > 유틸리티
|
||||
// 각 Branch: CheckPatternReady → true → UsePatternByRole
|
||||
// false → 다음 우선순위 Branch 시도
|
||||
// 마지막까지 모든 조건이 false이면 Chase (fallback)
|
||||
//
|
||||
// 연결 흐름: Branch.True → FloatingPort(True).InputPort → FloatingPort(True).OutputPort → Action.InputPort
|
||||
// CreateNodePortsForNode를 호출하여 FloatingPortNodeModel을 자동 생성해야 합니다.
|
||||
//
|
||||
// 레이아웃 패턴 (사용자 조정 기준):
|
||||
// Branch: (-800, y)
|
||||
// True Floating: (-597, y + 110)
|
||||
// False Floating: (-1011, y + 114)
|
||||
// Action: (-598, y + 199)
|
||||
|
||||
object slamRefreshNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(RefreshPrimaryTargetAction), new Vector2(240f, 240f));
|
||||
object slamHasTargetNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ValidateTargetAction), new Vector2(360f, 240f));
|
||||
object slamRangeNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(CheckTargetInAttackRangeAction), new Vector2(480f, 240f));
|
||||
object slamTurnNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(CheckSecondaryPatternTurnAction), new Vector2(600f, 240f));
|
||||
object slamReadyNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(CheckSecondaryPatternReadyAction), new Vector2(720f, 240f));
|
||||
object slamUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UseSecondaryPatternAction), new Vector2(840f, 240f));
|
||||
const float branchX = -800f;
|
||||
const float truePortOffsetX = 203f;
|
||||
const float truePortOffsetY = 110f;
|
||||
const float falsePortOffsetX = -211f;
|
||||
const float falsePortOffsetY = 114f;
|
||||
const float actionOffsetX = 202f;
|
||||
const float actionOffsetY = 199f;
|
||||
const float startY = -800f;
|
||||
const float stepY = 220f;
|
||||
|
||||
object mainRefreshNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(RefreshPrimaryTargetAction), new Vector2(760f, 240f));
|
||||
object mainHasTargetNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ValidateTargetAction), new Vector2(880f, 240f));
|
||||
object mainRangeNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(CheckTargetInAttackRangeAction), new Vector2(1000f, 240f));
|
||||
object mainReadyNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(CheckPrimaryPatternReadyAction), new Vector2(1120f, 240f));
|
||||
object mainUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UsePrimaryPatternAction), new Vector2(1240f, 240f));
|
||||
// #1 Punish — 다운 추가타 (전제 조건: 다운된 대상이 반경 이내에 있어야 함)
|
||||
object downBranch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(branchX, startY));
|
||||
AttachPatternReadyCondition(downBranch, punishPattern, authoringAssembly);
|
||||
AttachConditionWithValue(downBranch, typeof(IsDownedTargetInRangeCondition), "searchRadius", punishSearchRadius, authoringAssembly);
|
||||
AttachPhaseConditionIfNeeded(downBranch, punishPattern, authoringAssembly);
|
||||
SetBranchRequiresAll(downBranch, true);
|
||||
object downUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UsePatternByRoleAction), new Vector2(branchX + actionOffsetX, startY + actionOffsetY));
|
||||
SetNodeFieldValue(downUseNode, "Pattern", punishPattern, setFieldValueMethod);
|
||||
LinkTarget(downUseNode, targetVariable);
|
||||
|
||||
object fallbackRefreshNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(RefreshPrimaryTargetAction), new Vector2(1160f, 240f));
|
||||
object fallbackHasTargetNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ValidateTargetAction), new Vector2(1280f, 240f));
|
||||
object fallbackRangeNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(CheckTargetInAttackRangeAction), new Vector2(1400f, 240f));
|
||||
object fallbackReadyNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(CheckSecondaryPatternReadyAction), new Vector2(1520f, 240f));
|
||||
object fallbackUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UseSecondaryPatternAction), new Vector2(1640f, 240f));
|
||||
// #2 Mobility — 도약 (전제 조건: 지나치게 먼 대상이 존재해야 함)
|
||||
float mobilityTriggerDistance = ReadProtectedFieldValue<float>(context, "mobilityTriggerDistance", 8f);
|
||||
object leapBranch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(branchX, startY + stepY));
|
||||
AttachPatternReadyCondition(leapBranch, mobilityPattern, authoringAssembly);
|
||||
AttachConditionWithValue(leapBranch, typeof(IsTargetBeyondDistanceCondition), "minDistance", mobilityTriggerDistance, authoringAssembly);
|
||||
AttachPhaseConditionIfNeeded(leapBranch, mobilityPattern, authoringAssembly);
|
||||
SetBranchRequiresAll(leapBranch, true);
|
||||
object leapUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UsePatternByRoleAction), new Vector2(branchX + actionOffsetX, startY + stepY + actionOffsetY));
|
||||
SetNodeFieldValue(leapUseNode, "Pattern", mobilityPattern, setFieldValueMethod);
|
||||
LinkTarget(leapUseNode, targetVariable);
|
||||
|
||||
object chaseRefreshNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(RefreshPrimaryTargetAction), new Vector2(1560f, 240f));
|
||||
object chaseHasTargetNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ValidateTargetAction), new Vector2(1680f, 240f));
|
||||
object chaseUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ChaseTargetAction), new Vector2(1800f, 240f));
|
||||
// #3 Signature — 집행 개시
|
||||
object signatureBranch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(branchX, startY + stepY * 2));
|
||||
AttachPatternReadyCondition(signatureBranch, signaturePattern, authoringAssembly);
|
||||
AttachPhaseConditionIfNeeded(signatureBranch, signaturePattern, authoringAssembly);
|
||||
object signatureUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UsePatternByRoleAction), new Vector2(branchX + actionOffsetX, startY + stepY * 2 + actionOffsetY));
|
||||
SetNodeFieldValue(signatureUseNode, "Pattern", signaturePattern, setFieldValueMethod);
|
||||
LinkTarget(signatureUseNode, targetVariable);
|
||||
|
||||
// #4 Combo — 콤보 패턴 (드문 조합, 선택적)
|
||||
object comboBranch = null;
|
||||
object comboUseNode = null;
|
||||
if (comboPattern != null)
|
||||
{
|
||||
comboBranch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(branchX, startY + stepY * 3));
|
||||
AttachPatternReadyCondition(comboBranch, comboPattern, authoringAssembly);
|
||||
AttachPhaseConditionIfNeeded(comboBranch, comboPattern, authoringAssembly);
|
||||
comboUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UsePatternByRoleAction), new Vector2(branchX + actionOffsetX, startY + stepY * 3 + actionOffsetY));
|
||||
SetNodeFieldValue(comboUseNode, "Pattern", comboPattern, setFieldValueMethod);
|
||||
LinkTarget(comboUseNode, targetVariable);
|
||||
}
|
||||
|
||||
// #5 Primary — 사거리 + 기본 패턴 준비 (모두 충족)
|
||||
object primaryBranch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(branchX, startY + stepY * 4));
|
||||
object primaryRangeCondModel = AttachCondition(primaryBranch, typeof(IsTargetInAttackRangeCondition), authoringAssembly);
|
||||
if (primaryRangeCondModel != null) setFieldMethod.Invoke(primaryRangeCondModel, new object[] { "Target", targetVariable, typeof(GameObject) });
|
||||
AttachPatternReadyCondition(primaryBranch, primaryPattern, authoringAssembly);
|
||||
AttachPhaseConditionIfNeeded(primaryBranch, primaryPattern, authoringAssembly);
|
||||
SetBranchRequiresAll(primaryBranch, true);
|
||||
object primaryUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UsePatternByRoleAction), new Vector2(branchX + actionOffsetX, startY + stepY * 4 + actionOffsetY));
|
||||
SetNodeFieldValue(primaryUseNode, "Pattern", primaryPattern, setFieldValueMethod);
|
||||
LinkTarget(primaryUseNode, targetVariable);
|
||||
|
||||
// #6 Utility — 유틸리티 (전제 조건: 원거리 대상이 존재해야 함)
|
||||
float utilityTriggerDistance = ReadProtectedFieldValue<float>(context, "utilityTriggerDistance", 5f);
|
||||
object utilityBranch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(branchX, startY + stepY * 5));
|
||||
AttachPatternReadyCondition(utilityBranch, utilityPattern, authoringAssembly);
|
||||
AttachConditionWithValue(utilityBranch, typeof(IsTargetBeyondDistanceCondition), "minDistance", utilityTriggerDistance, authoringAssembly);
|
||||
AttachPhaseConditionIfNeeded(utilityBranch, utilityPattern, authoringAssembly);
|
||||
SetBranchRequiresAll(utilityBranch, true);
|
||||
object utilityUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UsePatternByRoleAction), new Vector2(branchX + actionOffsetX, startY + stepY * 5 + actionOffsetY));
|
||||
SetNodeFieldValue(utilityUseNode, "Pattern", utilityPattern, setFieldValueMethod);
|
||||
LinkTarget(utilityUseNode, targetVariable);
|
||||
|
||||
// #7 Chase — fallback (Branch 아님, Sequence 사용)
|
||||
object chaseSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(branchX, startY + stepY * 6));
|
||||
object chaseRefreshNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(RefreshPrimaryTargetAction), new Vector2(branchX + 160f, startY + stepY * 6 + 80f));
|
||||
object chaseHasTargetNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ValidateTargetAction), new Vector2(branchX + 320f, startY + stepY * 6 + 80f));
|
||||
object chaseUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ChaseTargetAction), new Vector2(branchX + 480f, startY + stepY * 6 + 80f));
|
||||
|
||||
// ── FloatingPortNodeModel 생성 + 위치 보정 ──
|
||||
// Branch 노드의 NamedPort(True/False)에 대해 FloatingPortNodeModel을 생성합니다.
|
||||
// CreateNodePortsForNode는 기본 위치(Branch + 200px Y)를 사용하므로, 생성 후 사용자 조정 기준 위치로 이동합니다.
|
||||
var allBranches = new List<object> { downBranch, leapBranch, signatureBranch };
|
||||
if (comboBranch != null) allBranches.Add(comboBranch);
|
||||
allBranches.AddRange(new[] { primaryBranch, utilityBranch });
|
||||
foreach (object branch in allBranches)
|
||||
{
|
||||
createNodePortsMethod?.Invoke(graphAsset, new object[] { branch });
|
||||
}
|
||||
|
||||
// FloatingPortNodeModel 위치를 사용자 조정 기준으로 보정
|
||||
foreach (object branch in allBranches)
|
||||
{
|
||||
// Branch의 현재 위치 읽기 (Position은 public 필드)
|
||||
FieldInfo posField = branch.GetType().GetField("Position", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
if (posField == null) continue;
|
||||
Vector2 branchPos = (Vector2)posField.GetValue(branch);
|
||||
|
||||
// FloatingPortNodeModel에서 PortName이 "True"/"False"인 것을 찾아 위치 수정
|
||||
SetFloatingPortPosition(graphAsset, branch, "True", branchPos.x + truePortOffsetX, branchPos.y + truePortOffsetY);
|
||||
SetFloatingPortPosition(graphAsset, branch, "False", branchPos.x + falsePortOffsetX, branchPos.y + falsePortOffsetY);
|
||||
}
|
||||
|
||||
// ── 연결 ──
|
||||
|
||||
// Start → Repeater → 첫 번째 Branch
|
||||
Connect(graphAsset, connectEdgeMethod, GetDefaultOutputPort(startNode), GetDefaultInputPort(repeatNode));
|
||||
Connect(graphAsset, connectEdgeMethod, GetDefaultOutputPort(repeatNode), GetDefaultInputPort(selectorNode));
|
||||
ConnectChildren(graphAsset, connectEdgeMethod, selectorNode, signatureSequence, downSequence, utilitySequence, leapSequence, slamSequence, mainSequence, slamFallbackSequence, chaseSequence);
|
||||
Connect(graphAsset, connectEdgeMethod, GetDefaultOutputPort(repeatNode), GetDefaultInputPort(downBranch));
|
||||
|
||||
ConnectChildren(graphAsset, connectEdgeMethod, signatureSequence, signatureRefreshNode, signatureHasTargetNode, signatureReadyNode, signatureUseNode);
|
||||
ConnectChildren(graphAsset, connectEdgeMethod, downSequence, downSelectNode, downReadyNode, downUseNode);
|
||||
ConnectChildren(graphAsset, connectEdgeMethod, utilitySequence, utilitySelectNode, utilityReadyNode, utilityUseNode);
|
||||
ConnectChildren(graphAsset, connectEdgeMethod, leapSequence, leapSelectNode, leapReadyNode, leapUseNode);
|
||||
ConnectChildren(graphAsset, connectEdgeMethod, slamSequence, slamRefreshNode, slamHasTargetNode, slamRangeNode, slamTurnNode, slamReadyNode, slamUseNode);
|
||||
ConnectChildren(graphAsset, connectEdgeMethod, mainSequence, mainRefreshNode, mainHasTargetNode, mainRangeNode, mainReadyNode, mainUseNode);
|
||||
ConnectChildren(graphAsset, connectEdgeMethod, slamFallbackSequence, fallbackRefreshNode, fallbackHasTargetNode, fallbackRangeNode, fallbackReadyNode, fallbackUseNode);
|
||||
// 각 Branch의 True FloatingPort → Action
|
||||
ConnectBranch(graphAsset, connectEdgeMethod, downBranch, "True", downUseNode);
|
||||
ConnectBranch(graphAsset, connectEdgeMethod, leapBranch, "True", leapUseNode);
|
||||
ConnectBranch(graphAsset, connectEdgeMethod, signatureBranch, "True", signatureUseNode);
|
||||
if (comboBranch != null)
|
||||
ConnectBranch(graphAsset, connectEdgeMethod, comboBranch, "True", comboUseNode);
|
||||
ConnectBranch(graphAsset, connectEdgeMethod, primaryBranch, "True", primaryUseNode);
|
||||
ConnectBranch(graphAsset, connectEdgeMethod, utilityBranch, "True", utilityUseNode);
|
||||
|
||||
// 각 Branch의 False FloatingPort → 다음 우선순위 (계단식 체인)
|
||||
// combo 유무에 따라 연결 경로가 달라짐
|
||||
object afterSignature = comboBranch ?? primaryBranch;
|
||||
ConnectBranch(graphAsset, connectEdgeMethod, downBranch, "False", leapBranch);
|
||||
ConnectBranch(graphAsset, connectEdgeMethod, leapBranch, "False", signatureBranch);
|
||||
ConnectBranch(graphAsset, connectEdgeMethod, signatureBranch, "False", afterSignature);
|
||||
if (comboBranch != null)
|
||||
ConnectBranch(graphAsset, connectEdgeMethod, comboBranch, "False", primaryBranch);
|
||||
ConnectBranch(graphAsset, connectEdgeMethod, primaryBranch, "False", utilityBranch);
|
||||
ConnectBranch(graphAsset, connectEdgeMethod, utilityBranch, "False", chaseSequence);
|
||||
|
||||
// Chase Sequence 자식 연결
|
||||
ConnectChildren(graphAsset, connectEdgeMethod, chaseSequence, chaseRefreshNode, chaseHasTargetNode, chaseUseNode);
|
||||
|
||||
LinkTarget(signatureRefreshNode, targetVariable);
|
||||
LinkTarget(signatureHasTargetNode, targetVariable);
|
||||
LinkTarget(signatureUseNode, targetVariable);
|
||||
LinkTarget(downSelectNode, targetVariable);
|
||||
LinkTarget(downUseNode, targetVariable);
|
||||
LinkTarget(utilitySelectNode, targetVariable);
|
||||
LinkTarget(utilityUseNode, targetVariable);
|
||||
LinkTarget(leapSelectNode, targetVariable);
|
||||
LinkTarget(leapUseNode, targetVariable);
|
||||
LinkTarget(slamRefreshNode, targetVariable);
|
||||
LinkTarget(slamHasTargetNode, targetVariable);
|
||||
LinkTarget(slamRangeNode, targetVariable);
|
||||
LinkTarget(slamUseNode, targetVariable);
|
||||
LinkTarget(mainRefreshNode, targetVariable);
|
||||
LinkTarget(mainHasTargetNode, targetVariable);
|
||||
LinkTarget(mainRangeNode, targetVariable);
|
||||
LinkTarget(mainUseNode, targetVariable);
|
||||
LinkTarget(fallbackRefreshNode, targetVariable);
|
||||
LinkTarget(fallbackHasTargetNode, targetVariable);
|
||||
LinkTarget(fallbackRangeNode, targetVariable);
|
||||
LinkTarget(fallbackUseNode, targetVariable);
|
||||
// Chase 노드 블랙보드 변수 연결
|
||||
LinkTarget(chaseRefreshNode, targetVariable);
|
||||
LinkTarget(chaseHasTargetNode, targetVariable);
|
||||
LinkTarget(chaseUseNode, targetVariable);
|
||||
|
||||
// 저장
|
||||
SetStartRepeatFlags(startNode, repeat: true, allowMultipleRepeatsPerTick: false);
|
||||
setAssetDirtyMethod.Invoke(graphAsset, new object[] { true });
|
||||
buildRuntimeGraphMethod.Invoke(graphAsset, new object[] { true });
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
// BuildRuntimeGraph는 에셋이 직렬화된 후 AssetDatabase.ImportAsset으로 재임포트하여
|
||||
// OnValidate/AssetPostprocessor에서 자동 빌드되게 합니다.
|
||||
string assetPath = AssetDatabase.GetAssetPath(graphAsset);
|
||||
AssetDatabase.ImportAsset(assetPath, ImportAssetOptions.ForceUpdate);
|
||||
|
||||
saveAssetMethod.Invoke(graphAsset, null);
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
Debug.Log("[DrogBTRebuild] 드로그 Behavior Graph authoring 자산 재구성이 완료되었습니다.");
|
||||
}
|
||||
@@ -169,6 +387,209 @@ namespace Colosseum.Editor
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ConditionalGuardAction 노드를 생성하고 지정된 Condition을 부착합니다.
|
||||
/// </summary>
|
||||
private static object CreateConditionalGuard(
|
||||
UnityEngine.Object graphAsset,
|
||||
MethodInfo createNodeMethod,
|
||||
MethodInfo getNodeInfoMethod,
|
||||
Type conditionalGuardType,
|
||||
Type conditionType,
|
||||
Vector2 position,
|
||||
Assembly authoringAssembly)
|
||||
{
|
||||
object guardNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, conditionalGuardType, position);
|
||||
AttachCondition(guardNode, conditionType, authoringAssembly);
|
||||
return guardNode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ConditionalGuardAction 노드를 생성하고, 블랙보드 변수 참조가 있는 Condition을 부착합니다.
|
||||
/// </summary>
|
||||
private static object CreateConditionalGuardWithField(
|
||||
UnityEngine.Object graphAsset,
|
||||
MethodInfo createNodeMethod,
|
||||
MethodInfo getNodeInfoMethod,
|
||||
Type conditionalGuardType,
|
||||
MethodInfo setFieldMethod,
|
||||
Type conditionType,
|
||||
object targetVariable,
|
||||
Vector2 position,
|
||||
Assembly authoringAssembly)
|
||||
{
|
||||
object guardNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, conditionalGuardType, position);
|
||||
object conditionModel = AttachCondition(guardNode, conditionType, authoringAssembly);
|
||||
|
||||
// Condition의 Target 필드를 블랙보드 변수에 연결
|
||||
// SetField는 GetOrCreateField를 호출하여 m_FieldValues에 FieldModel을 생성합니다.
|
||||
if (conditionModel != null && targetVariable != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
setFieldMethod.Invoke(conditionModel, new object[] { "Target", targetVariable, typeof(GameObject) });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[DrogBTRebuild] SetField 'Target' 실패 for {conditionType.Name}: {ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
return guardNode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 노드에 Condition을 부착합니다.
|
||||
/// ConditionUtility.GetInfoForConditionType를 사용하여 NodeRegistry와 완벽히 동일한
|
||||
/// ConditionInfo를 획득합니다. 이렇게 하면 TypeID가 레지스트리와 일치하여
|
||||
/// EnsureFieldValuesAreUpToDate가 정상 동작하고 UpdateConditionModels가
|
||||
/// ConditionModel을 삭제하지 않습니다.
|
||||
/// </summary>
|
||||
private static object AttachCondition(object guardNode, Type conditionType, Assembly authoringAssembly)
|
||||
{
|
||||
try
|
||||
{
|
||||
// ConditionUtility.GetInfoForConditionType을 사용하여 ConditionInfo 획득
|
||||
// 이 메서드는 ConditionAttribute에서 GUID를 읽고, Variables를 리플렉션으로 수집합니다.
|
||||
Type conditionUtilityType = authoringAssembly.GetType("Unity.Behavior.ConditionUtility");
|
||||
MethodInfo getInfoForTypeMethod = conditionUtilityType?.GetMethod("GetInfoForConditionType", BindingFlags.Static | BindingFlags.NonPublic);
|
||||
|
||||
if (getInfoForTypeMethod == null)
|
||||
{
|
||||
Debug.LogError("[DrogBTRebuild] ConditionUtility.GetInfoForConditionType 메서드를 찾지 못했습니다.");
|
||||
return null;
|
||||
}
|
||||
|
||||
object conditionInfo = getInfoForTypeMethod.Invoke(null, new object[] { conditionType });
|
||||
|
||||
if (conditionInfo == null)
|
||||
{
|
||||
Debug.LogError($"[DrogBTRebuild] GetInfoForConditionType이 null을 반환: {conditionType.Name}");
|
||||
return null;
|
||||
}
|
||||
|
||||
Type conditionModelType = authoringAssembly.GetType("Unity.Behavior.ConditionModel");
|
||||
Type graphNodeModelType = authoringAssembly.GetType("Unity.Behavior.BehaviorGraphNodeModel");
|
||||
|
||||
if (conditionModelType == null || graphNodeModelType == null)
|
||||
{
|
||||
Debug.LogError("[DrogBTRebuild] ConditionModel/BehaviorGraphNodeModel 타입을 찾지 못했습니다.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// ConditionModel 생성자 가져오기 (internal)
|
||||
ConstructorInfo conditionModelCtor = conditionModelType.GetConstructor(
|
||||
BindingFlags.Instance | BindingFlags.NonPublic,
|
||||
null,
|
||||
new[] { graphNodeModelType, typeof(Unity.Behavior.Condition), conditionInfo.GetType() },
|
||||
null);
|
||||
|
||||
if (conditionModelCtor == null)
|
||||
{
|
||||
Debug.LogWarning("[DrogBTRebuild] ConditionModel 생성자를 찾지 못했습니다.");
|
||||
return null;
|
||||
}
|
||||
|
||||
object conditionModel = conditionModelCtor.Invoke(new object[] { guardNode, null, conditionInfo });
|
||||
|
||||
// ConditionModels 리스트에 추가
|
||||
PropertyInfo conditionModelsProp = guardNode.GetType().GetProperty("ConditionModels", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
if (conditionModelsProp != null)
|
||||
{
|
||||
IList conditionModels = conditionModelsProp.GetValue(guardNode) as IList;
|
||||
conditionModels?.Add(conditionModel);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("[DrogBTRebuild] ConditionModels 속성을 찾지 못했습니다.");
|
||||
}
|
||||
|
||||
return conditionModel;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[DrogBTRebuild] AttachCondition 실패 ({conditionType.Name}): {ex.GetType().Name}: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ConditionModel의 필드를 블랙보드 변수에 연결합니다.
|
||||
/// </summary>
|
||||
private static void LinkConditionFieldToVariable(object conditionModel, string fieldName, Type fieldType, object variableModel)
|
||||
{
|
||||
MethodInfo setFieldMethod = conditionModel.GetType().GetMethod("SetField", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
setFieldMethod?.Invoke(conditionModel, new object[] { fieldName, variableModel, fieldType });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Condition을 부착하고, 지정된 enum 필드 값을 설정합니다.
|
||||
/// CheckPatternReadyCondition처럼 필드 값으로 역할을 구분하는 Condition에 사용합니다.
|
||||
/// </summary>
|
||||
private static void AttachConditionWithValue(object guardNode, Type conditionType, string fieldName, object fieldValue, Assembly authoringAssembly)
|
||||
{
|
||||
object conditionModel = AttachCondition(guardNode, conditionType, authoringAssembly);
|
||||
if (conditionModel == null)
|
||||
{
|
||||
Debug.LogWarning($"[DrogBTRebuild] AttachConditionWithValue: Condition 생성 실패 ({conditionType.Name})");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// ConditionModel의 실제 타입에서 SetField<T>를 조회
|
||||
MethodInfo genericSetField = conditionModel.GetType().GetMethods(BindingFlags.Instance | BindingFlags.NonPublic)
|
||||
.FirstOrDefault(m => m.Name == "SetField" && m.IsGenericMethod && m.GetParameters().Length == 2);
|
||||
|
||||
if (genericSetField != null)
|
||||
{
|
||||
MethodInfo closedMethod = genericSetField.MakeGenericMethod(fieldValue.GetType());
|
||||
closedMethod.Invoke(conditionModel, new object[] { fieldName, fieldValue });
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"[DrogBTRebuild] SetField<T>를 찾지 못해 '{fieldName}' 필드를 설정하지 못했습니다.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[DrogBTRebuild] AttachConditionWithValue 실패 ({conditionType.Name}.{fieldName}): {ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 노드 모델의 지정된 enum 필드 값을 설정합니다.
|
||||
/// UsePatternByRoleAction처럼 필드 값으로 역할을 구분하는 Action에 사용합니다.
|
||||
/// </summary>
|
||||
private static void SetNodeFieldValue(object nodeModel, string fieldName, object fieldValue, MethodInfo setFieldValueMethod)
|
||||
{
|
||||
if (setFieldValueMethod == null)
|
||||
{
|
||||
Debug.LogWarning("[DrogBTRebuild] SetNodeFieldValue: setFieldValueMethod이 null입니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 실제 노드 모델 타입에서 SetField<T>를 직접 조회하여 타입 불일치 방지
|
||||
MethodInfo genericMethod = nodeModel.GetType().GetMethods(BindingFlags.Instance | BindingFlags.NonPublic)
|
||||
.FirstOrDefault(m => m.Name == "SetField" && m.IsGenericMethod && m.GetParameters().Length == 2);
|
||||
|
||||
if (genericMethod == null)
|
||||
{
|
||||
Debug.LogWarning($"[DrogBTRebuild] SetNodeFieldValue: SetField<T>를 {nodeModel.GetType().Name}에서 찾지 못했습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
MethodInfo closedMethod = genericMethod.MakeGenericMethod(fieldValue.GetType());
|
||||
closedMethod.Invoke(nodeModel, new object[] { fieldName, fieldValue });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[DrogBTRebuild] SetNodeFieldValue 실패 ({nodeModel.GetType().Name}.{fieldName}): {ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static object CreateNode(UnityEngine.Object graphAsset, MethodInfo createNodeMethod, MethodInfo getNodeInfoMethod, Type runtimeType, Vector2 position)
|
||||
{
|
||||
if (runtimeType == null)
|
||||
@@ -318,5 +739,263 @@ namespace Colosseum.Editor
|
||||
repeatField?.SetValue(startNode, repeat);
|
||||
allowField?.SetValue(startNode, allowMultipleRepeatsPerTick);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GetField()가 null을 반환하는 문제를 회피하기 위해 전체 타입 계층을 순회하며 필드를 검색합니다.
|
||||
/// </summary>
|
||||
private static FieldInfo FindFieldInHierarchy(Type type, string fieldName)
|
||||
{
|
||||
Type current = type;
|
||||
while (current != null)
|
||||
{
|
||||
foreach (FieldInfo fi in current.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
|
||||
{
|
||||
if (fi.Name == fieldName)
|
||||
return fi;
|
||||
}
|
||||
current = current.BaseType;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Branch 노드의 지정된 이름의 출력 포트(PortModel)를 반환합니다.
|
||||
/// BranchingConditionComposite는 NamedChildren(True, False)을 가지므로
|
||||
/// 기본 출력 포트 대신 이름 기반 포트를 사용해야 합니다.
|
||||
/// </summary>
|
||||
private static object GetNamedOutputPort(object node, string portName)
|
||||
{
|
||||
MethodInfo method = node.GetType().GetMethod("FindPortModelByName", BindingFlags.Instance | BindingFlags.Public);
|
||||
if (method == null)
|
||||
throw new InvalidOperationException("[DrogBTRebuild] FindPortModelByName 메서드를 찾지 못했습니다.");
|
||||
object port = method.Invoke(node, new object[] { portName });
|
||||
if (port == null)
|
||||
throw new InvalidOperationException($"[DrogBTRebuild] '{portName}' 포트를 찾지 못했습니다.");
|
||||
return port;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Branch의 NamedPort(True/False)를 FloatingPortNodeModel을 경유하여 대상 노드에 연결합니다.
|
||||
/// 올바른 연결 흐름: Branch.NamedPort → FloatingPort.InputPort → FloatingPort.OutputPort → Target.InputPort
|
||||
/// </summary>
|
||||
private static void ConnectBranch(UnityEngine.Object graphAsset, MethodInfo connectEdgeMethod, object branchNode, string portName, object targetNode)
|
||||
{
|
||||
// Branch의 NamedPort 찾기
|
||||
object branchPort = GetNamedOutputPort(branchNode, portName);
|
||||
|
||||
// FloatingPortNodeModel 찾기 — Branch의 포트에 연결된 FloatingPortNodeModel을 검색
|
||||
// FloatingPortNodeModel은 GraphAsset.Nodes에 별도 노드로 저장됩니다.
|
||||
FieldInfo nodesField = FindFieldInHierarchy(graphAsset.GetType(), "m_Nodes");
|
||||
if (nodesField == null)
|
||||
{
|
||||
// 폴백: 직접 연결 (FloatingPort가 없는 경우)
|
||||
Connect(graphAsset, connectEdgeMethod, branchPort, GetDefaultInputPort(targetNode));
|
||||
return;
|
||||
}
|
||||
|
||||
IEnumerable nodes = nodesField.GetValue(graphAsset) as IEnumerable;
|
||||
if (nodes == null)
|
||||
{
|
||||
Connect(graphAsset, connectEdgeMethod, branchPort, GetDefaultInputPort(targetNode));
|
||||
return;
|
||||
}
|
||||
|
||||
object floatingPortOutput = null;
|
||||
|
||||
foreach (object node in nodes)
|
||||
{
|
||||
if (node == null) continue;
|
||||
Type nodeType = node.GetType();
|
||||
if (!nodeType.Name.Contains("FloatingPortNodeModel")) continue;
|
||||
|
||||
// PortName 확인
|
||||
FieldInfo portNameField = nodeType.GetField("PortName", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
string currentPortName = portNameField?.GetValue(node) as string;
|
||||
if (currentPortName != portName) continue;
|
||||
|
||||
// ParentNodeID 확인 — 이 Branch의 자식인지
|
||||
FieldInfo parentNodeIdField = nodeType.GetField("ParentNodeID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
if (parentNodeIdField == null) continue;
|
||||
|
||||
object parentNodeIdValue = parentNodeIdField.GetValue(node);
|
||||
// Branch의 ID와 비교
|
||||
FieldInfo branchIdField = branchNode.GetType().GetField("ID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
if (branchIdField == null) continue;
|
||||
object branchIdValue = branchIdField.GetValue(branchNode);
|
||||
|
||||
if (!parentNodeIdValue.Equals(branchIdValue)) continue;
|
||||
|
||||
// FloatingPort의 OutputPort 찾기
|
||||
PropertyInfo portModelsProp = nodeType.GetProperty("PortModels", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
FieldInfo portModelsField = nodeType.GetField("PortModels", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
IEnumerable portModels = portModelsProp?.GetValue(node) as IEnumerable ?? portModelsField?.GetValue(node) as IEnumerable;
|
||||
if (portModels == null) continue;
|
||||
|
||||
foreach (object port in portModels)
|
||||
{
|
||||
if (port == null) continue;
|
||||
FieldInfo portNameF = port.GetType().GetField("m_Name", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
string pName = portNameF?.GetValue(port) as string;
|
||||
if (pName == "OutputPort")
|
||||
{
|
||||
floatingPortOutput = port;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (floatingPortOutput != null) break;
|
||||
}
|
||||
|
||||
if (floatingPortOutput != null)
|
||||
{
|
||||
Connect(graphAsset, connectEdgeMethod, floatingPortOutput, GetDefaultInputPort(targetNode));
|
||||
}
|
||||
else
|
||||
{
|
||||
// 폴백: 직접 연결
|
||||
Connect(graphAsset, connectEdgeMethod, branchPort, GetDefaultInputPort(targetNode));
|
||||
Debug.LogWarning($"[DrogBTRebuild] FloatingPortNodeModel을 찾지 못해 '{portName}' 포트를 직접 연결합니다.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Branch 노드의 RequiresAllConditionsTrue 플래그를 설정합니다.
|
||||
/// DefaultNodeTransformer가 model.RequiresAllConditionsTrue → runtime.RequiresAllConditions로 복사합니다.
|
||||
/// </summary>
|
||||
private static void SetBranchRequiresAll(object branchNode, bool requiresAll)
|
||||
{
|
||||
PropertyInfo prop = branchNode.GetType().GetProperty("RequiresAllConditionsTrue", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
prop?.SetValue(branchNode, requiresAll);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Branch에 속한 FloatingPortNodeModel의 위치를 설정합니다.
|
||||
/// Branch의 ID와 PortName으로 FloatingPortNodeModel을 찾아 Position을 변경합니다.
|
||||
/// </summary>
|
||||
private static void SetFloatingPortPosition(UnityEngine.Object graphAsset, object branchNode, string portName, float x, float y)
|
||||
{
|
||||
// m_Nodes 또는 Nodes에서 FloatingPortNodeModel을 검색
|
||||
IEnumerable nodes = null;
|
||||
FieldInfo nodesField = FindFieldInHierarchy(graphAsset.GetType(), "m_Nodes");
|
||||
if (nodesField != null)
|
||||
nodes = nodesField.GetValue(graphAsset) as IEnumerable;
|
||||
if (nodes == null)
|
||||
{
|
||||
PropertyInfo nodesProp = graphAsset.GetType().GetProperty("Nodes", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
if (nodesProp != null)
|
||||
nodes = nodesProp.GetValue(graphAsset) as IEnumerable;
|
||||
}
|
||||
|
||||
if (nodes == null)
|
||||
{
|
||||
Debug.LogWarning("[DrogBTRebuild] SetFloatingPortPosition: Nodes 컬렉션을 찾지 못했습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Branch의 ID 가져오기
|
||||
FieldInfo branchIdField = branchNode.GetType().GetField("ID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
if (branchIdField == null)
|
||||
{
|
||||
Debug.LogWarning("[DrogBTRebuild] SetFloatingPortPosition: ID 필드를 찾지 못했습니다.");
|
||||
return;
|
||||
}
|
||||
object branchIdValue = branchIdField.GetValue(branchNode);
|
||||
|
||||
foreach (object node in nodes)
|
||||
{
|
||||
if (node == null) continue;
|
||||
Type nodeType = node.GetType();
|
||||
if (!nodeType.Name.Contains("FloatingPortNodeModel")) continue;
|
||||
|
||||
FieldInfo portNameField = nodeType.GetField("PortName", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
string currentPortName = portNameField?.GetValue(node) as string;
|
||||
if (currentPortName != portName) continue;
|
||||
|
||||
FieldInfo parentNodeIdField = nodeType.GetField("ParentNodeID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
if (parentNodeIdField == null) continue;
|
||||
object parentNodeIdValue = parentNodeIdField.GetValue(node);
|
||||
|
||||
bool match = parentNodeIdValue != null && parentNodeIdValue.Equals(branchIdValue);
|
||||
if (!match) continue;
|
||||
|
||||
// Position 설정 (Position은 public 필드)
|
||||
FieldInfo posField = nodeType.GetField("Position", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
posField?.SetValue(node, new Vector2(x, y));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 컴포넌트의 protected 필드 값을 읽습니다 (참조 타입용).
|
||||
/// 프리팹에서 BossPatternData 에셋을 로드할 때 사용합니다.
|
||||
/// </summary>
|
||||
private static T ReadProtectedField<T>(object obj, string fieldName) where T : class
|
||||
{
|
||||
Type type = obj.GetType();
|
||||
while (type != null)
|
||||
{
|
||||
FieldInfo field = type.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||||
if (field != null)
|
||||
return field.GetValue(obj) as T;
|
||||
type = type.BaseType;
|
||||
}
|
||||
Debug.LogError($"[DrogBTRebuild] '{fieldName}' 필드를 {obj.GetType().Name}에서 찾지 못했습니다.");
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 컴포넌트의 protected 필드 값을 읽습니다 (값 타입용).
|
||||
/// </summary>
|
||||
private static T ReadProtectedFieldValue<T>(object obj, string fieldName, T defaultValue) where T : struct
|
||||
{
|
||||
Type type = obj.GetType();
|
||||
while (type != null)
|
||||
{
|
||||
FieldInfo field = type.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||||
if (field != null)
|
||||
return (T)field.GetValue(obj);
|
||||
type = type.BaseType;
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Branch에 CheckPatternReadyCondition을 부착하고 BossPatternData 에셋을 설정합니다.
|
||||
/// 노드에 패턴명이 표시됩니다 (story의 [Pattern] 치환).
|
||||
/// </summary>
|
||||
private static void AttachPatternReadyCondition(object branchNode, BossPatternData pattern, Assembly authoringAssembly)
|
||||
{
|
||||
object condModel = AttachCondition(branchNode, typeof(CheckPatternReadyCondition), authoringAssembly);
|
||||
if (condModel == null)
|
||||
{
|
||||
Debug.LogError($"[DrogBTRebuild] CheckPatternReadyCondition 부착 실패: {pattern?.PatternName}");
|
||||
return;
|
||||
}
|
||||
|
||||
// ConditionModel의 실제 타입에서 SetField<T>를 조회하여 BossPatternData 참조 설정
|
||||
MethodInfo genericSetField = condModel.GetType().GetMethods(BindingFlags.Instance | BindingFlags.NonPublic)
|
||||
.FirstOrDefault(m => m.Name == "SetField" && m.IsGenericMethod && m.GetParameters().Length == 2);
|
||||
if (genericSetField != null)
|
||||
{
|
||||
MethodInfo closedMethod = genericSetField.MakeGenericMethod(typeof(BossPatternData));
|
||||
closedMethod.Invoke(condModel, new object[] { "Pattern", pattern });
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("[DrogBTRebuild] CheckPatternReadyCondition에서 SetField<T>를 찾지 못했습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 패턴의 MinPhase가 1보다 큰 경우, Branch에 IsMinPhaseSatisfiedCondition을 부착합니다.
|
||||
/// Phase 진입 조건을 BT에서 시각적으로 확인할 수 있습니다.
|
||||
/// </summary>
|
||||
private static void AttachPhaseConditionIfNeeded(object branchNode, BossPatternData pattern, Assembly authoringAssembly)
|
||||
{
|
||||
if (pattern == null || pattern.MinPhase <= 1)
|
||||
return;
|
||||
|
||||
AttachConditionWithValue(branchNode, typeof(IsMinPhaseSatisfiedCondition), "MinPhase", pattern.MinPhase, authoringAssembly);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,10 +36,6 @@ namespace Colosseum.Enemy
|
||||
[FormerlySerializedAs("mainPattern")]
|
||||
[SerializeField] protected BossPatternData primaryPattern;
|
||||
|
||||
[Tooltip("보조 근접 압박 패턴")]
|
||||
[FormerlySerializedAs("slamPattern")]
|
||||
[SerializeField] protected BossPatternData secondaryPattern;
|
||||
|
||||
[Tooltip("기동 또는 거리 징벌 패턴")]
|
||||
[FormerlySerializedAs("leapPattern")]
|
||||
[SerializeField] protected BossPatternData mobilityPattern;
|
||||
@@ -47,6 +43,9 @@ namespace Colosseum.Enemy
|
||||
[Tooltip("비주 대상 원거리 견제 패턴")]
|
||||
[SerializeField] protected BossPatternData utilityPattern;
|
||||
|
||||
[Tooltip("Phase 3 조합 패턴")]
|
||||
[SerializeField] protected BossPatternData comboPattern;
|
||||
|
||||
[Tooltip("특정 상황에서 우선 발동하는 징벌 패턴")]
|
||||
[FormerlySerializedAs("downPunishPattern")]
|
||||
[SerializeField] protected BossPatternData punishPattern;
|
||||
@@ -77,18 +76,9 @@ namespace Colosseum.Enemy
|
||||
[Tooltip("원거리 견제 패턴을 고려하기 시작하는 최소 거리")]
|
||||
[Min(0f)] [SerializeField] protected float utilityTriggerDistance = 5f;
|
||||
|
||||
[Header("Pattern Cadence")]
|
||||
[Tooltip("1페이즈에서 몇 번의 근접 패턴마다 보조 패턴을 섞을지")]
|
||||
[FormerlySerializedAs("phase1SlamInterval")]
|
||||
[Min(1)] [SerializeField] protected int phase1SecondaryInterval = 3;
|
||||
|
||||
[Tooltip("2페이즈에서 몇 번의 근접 패턴마다 보조 패턴을 섞을지")]
|
||||
[FormerlySerializedAs("phase2SlamInterval")]
|
||||
[Min(1)] [SerializeField] protected int phase2SecondaryInterval = 2;
|
||||
|
||||
[Tooltip("3페이즈에서 몇 번의 근접 패턴마다 보조 패턴을 섞을지")]
|
||||
[FormerlySerializedAs("phase3SlamInterval")]
|
||||
[Min(1)] [SerializeField] protected int phase3SecondaryInterval = 2;
|
||||
[Header("Pattern Flow")]
|
||||
[Tooltip("대형 패턴(시그니처/기동/조합) 직후 기본 패턴 최소 순환 횟수")]
|
||||
[Min(0)] [SerializeField] protected int basicLoopMinCountAfterBigPattern = 2;
|
||||
|
||||
[Header("Signature Pattern")]
|
||||
[Tooltip("시그니처 패턴 차단에 필요한 누적 피해 비율")]
|
||||
@@ -122,8 +112,8 @@ namespace Colosseum.Enemy
|
||||
[Min(0f)] [SerializeField] protected float signatureFailureDownDuration = 2f;
|
||||
|
||||
[Header("Behavior")]
|
||||
[Tooltip("전용 컨텍스트 사용 시 기존 BehaviorGraph를 비활성화할지 여부")]
|
||||
[SerializeField] protected bool disableBehaviorGraph = true;
|
||||
[Tooltip("true면 컨텍스트 코드가 AI를 직접 구동합니다. false면 BehaviorGraph가 모든 의사결정을 담당합니다.")]
|
||||
[SerializeField] protected bool disableBehaviorGraph = false;
|
||||
|
||||
[Tooltip("디버그 로그 출력 여부")]
|
||||
[SerializeField] protected bool debugMode = false;
|
||||
@@ -140,6 +130,7 @@ namespace Colosseum.Enemy
|
||||
protected float signatureRequiredDamage;
|
||||
protected float signatureElapsedTime;
|
||||
protected float signatureTotalDuration;
|
||||
protected int basicLoopCountSinceLastBigPattern;
|
||||
|
||||
/// <summary>
|
||||
/// 전용 컨텍스트 사용 시 BehaviorGraph를 비활성화할지 여부
|
||||
@@ -270,66 +261,50 @@ namespace Colosseum.Enemy
|
||||
if (skillController.IsPlayingAnimation)
|
||||
return;
|
||||
|
||||
// 1. 다운 추가타 (최우선 인터럽트, grace period 면제)
|
||||
if (TryStartPunishPattern())
|
||||
return;
|
||||
|
||||
// 2. 집행 개시 (Phase 3 시그니처)
|
||||
if (TryStartSignaturePatternInLoop())
|
||||
return;
|
||||
|
||||
// 3. 조합 패턴 (Phase 3, 드물게)
|
||||
if (TryStartComboPattern())
|
||||
return;
|
||||
|
||||
// 4. 기동 패턴 (거리 기반 조건부)
|
||||
if (TryStartMobilityPattern())
|
||||
return;
|
||||
|
||||
// 5. 원거리 견제 (보조)
|
||||
if (TryStartUtilityPattern())
|
||||
return;
|
||||
|
||||
// 6. 기본 루프
|
||||
TryStartPrimaryLoopPattern();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 역할의 패턴 데이터를 반환합니다.
|
||||
/// </summary>
|
||||
public BossPatternData GetPattern(BossCombatPatternRole role)
|
||||
{
|
||||
return role switch
|
||||
{
|
||||
BossCombatPatternRole.Primary => primaryPattern,
|
||||
BossCombatPatternRole.Secondary => secondaryPattern,
|
||||
BossCombatPatternRole.Mobility => mobilityPattern,
|
||||
BossCombatPatternRole.Utility => utilityPattern,
|
||||
BossCombatPatternRole.Punish => punishPattern,
|
||||
BossCombatPatternRole.Signature => signaturePattern,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 다음 근접 패턴 차례가 보조 패턴인지 여부
|
||||
/// </summary>
|
||||
public bool IsNextSecondaryPattern()
|
||||
{
|
||||
int secondaryInterval = GetSecondaryIntervalForPhase(CurrentPatternPhase);
|
||||
if (secondaryInterval <= 1)
|
||||
return true;
|
||||
|
||||
return (meleePatternCounter + 1) % secondaryInterval == 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 페이즈 기준의 보조 근접 패턴 주기를 반환합니다.
|
||||
/// </summary>
|
||||
public int GetSecondaryIntervalForPhase(int phase)
|
||||
{
|
||||
return phase switch
|
||||
{
|
||||
1 => Mathf.Max(1, phase1SecondaryInterval),
|
||||
2 => Mathf.Max(1, phase2SecondaryInterval),
|
||||
_ => Mathf.Max(1, phase3SecondaryInterval),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 근접 패턴 사용 카운터를 갱신합니다.
|
||||
/// </summary>
|
||||
public void RegisterPatternUse(BossCombatPatternRole role)
|
||||
public void RegisterPatternUse(BossPatternData pattern)
|
||||
{
|
||||
if (!role.IsMeleeRole())
|
||||
if (pattern == null)
|
||||
return;
|
||||
|
||||
if (pattern.IsMelee)
|
||||
{
|
||||
meleePatternCounter++;
|
||||
basicLoopCountSinceLastBigPattern++;
|
||||
}
|
||||
|
||||
if (pattern.Category == PatternCategory.Punish || pattern.IsBigPattern)
|
||||
{
|
||||
basicLoopCountSinceLastBigPattern = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -500,9 +475,53 @@ namespace Colosseum.Enemy
|
||||
if (bossEnemy.IsDead || bossEnemy.IsTransitioning || skillController.IsPlayingAnimation)
|
||||
return false;
|
||||
|
||||
if (!IsPatternGracePeriodAllowed(signaturePattern))
|
||||
return false;
|
||||
|
||||
return IsPatternReady(signaturePattern);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정 패턴이 grace period를 통과했는지 반환합니다.
|
||||
/// Punish/Melee/Utility는 항상 허용됩니다.
|
||||
/// </summary>
|
||||
public bool IsPatternGracePeriodAllowed(BossPatternData pattern)
|
||||
{
|
||||
if (pattern == null)
|
||||
return false;
|
||||
|
||||
if (pattern.Category == PatternCategory.Punish)
|
||||
return true;
|
||||
|
||||
if (pattern.IsMelee || pattern.TargetMode == TargetResolveMode.Utility)
|
||||
return true;
|
||||
|
||||
return basicLoopCountSinceLastBigPattern >= basicLoopMinCountAfterBigPattern;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 조합 패턴 사용 가능 여부를 반환합니다.
|
||||
/// </summary>
|
||||
public bool IsComboPatternReady()
|
||||
{
|
||||
if (!IsServer || bossEnemy == null || skillController == null)
|
||||
return false;
|
||||
|
||||
if (IsBehaviorSuppressed)
|
||||
return false;
|
||||
|
||||
if (activePatternCoroutine != null || isSignaturePatternActive)
|
||||
return false;
|
||||
|
||||
if (bossEnemy.IsDead || bossEnemy.IsTransitioning || skillController.IsPlayingAnimation)
|
||||
return false;
|
||||
|
||||
if (!IsPatternGracePeriodAllowed(comboPattern))
|
||||
return false;
|
||||
|
||||
return IsPatternReady(comboPattern);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 시그니처 패턴을 시작합니다.
|
||||
/// </summary>
|
||||
@@ -566,7 +585,7 @@ namespace Colosseum.Enemy
|
||||
|
||||
protected virtual bool TryStartMobilityPattern()
|
||||
{
|
||||
BossPatternData pattern = GetPattern(BossCombatPatternRole.Mobility);
|
||||
BossPatternData pattern = mobilityPattern;
|
||||
if (!IsPatternReady(pattern))
|
||||
return false;
|
||||
|
||||
@@ -581,7 +600,7 @@ namespace Colosseum.Enemy
|
||||
|
||||
protected virtual bool TryStartUtilityPattern()
|
||||
{
|
||||
BossPatternData pattern = GetPattern(BossCombatPatternRole.Utility);
|
||||
BossPatternData pattern = utilityPattern;
|
||||
if (!IsPatternReady(pattern))
|
||||
return false;
|
||||
|
||||
@@ -594,33 +613,109 @@ namespace Colosseum.Enemy
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 다운 대상이 존재하면 징벌 패턴을 발동합니다.
|
||||
/// </summary>
|
||||
protected virtual bool TryStartPunishPattern()
|
||||
{
|
||||
BossPatternData pattern = punishPattern;
|
||||
if (!IsPatternReady(pattern))
|
||||
return false;
|
||||
|
||||
HitReactionController[] hitReactionControllers = FindObjectsByType<HitReactionController>(FindObjectsSortMode.None);
|
||||
GameObject nearestDownedTarget = null;
|
||||
float nearestDistance = float.MaxValue;
|
||||
|
||||
for (int i = 0; i < hitReactionControllers.Length; i++)
|
||||
{
|
||||
HitReactionController controller = hitReactionControllers[i];
|
||||
if (controller == null || !controller.IsDowned)
|
||||
continue;
|
||||
|
||||
GameObject candidate = controller.gameObject;
|
||||
if (candidate == null || !candidate.activeInHierarchy)
|
||||
continue;
|
||||
|
||||
if (Team.IsSameTeam(gameObject, candidate))
|
||||
continue;
|
||||
|
||||
IDamageable damageable = candidate.GetComponent<IDamageable>();
|
||||
if (damageable != null && damageable.IsDead)
|
||||
continue;
|
||||
|
||||
float distance = Vector3.Distance(transform.position, candidate.transform.position);
|
||||
if (distance > punishSearchRadius || distance >= nearestDistance)
|
||||
continue;
|
||||
|
||||
nearestDistance = distance;
|
||||
nearestDownedTarget = candidate;
|
||||
}
|
||||
|
||||
if (nearestDownedTarget == null)
|
||||
return false;
|
||||
|
||||
currentTarget = nearestDownedTarget;
|
||||
StartPattern(pattern, nearestDownedTarget);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 시그니처 패턴을 context 루프에서 발동합니다.
|
||||
/// grace period와 Phase 제한을 적용합니다.
|
||||
/// </summary>
|
||||
protected virtual bool TryStartSignaturePatternInLoop()
|
||||
{
|
||||
if (!IsSignaturePatternReady())
|
||||
return false;
|
||||
|
||||
if (!IsPatternGracePeriodAllowed(signaturePattern))
|
||||
return false;
|
||||
|
||||
GameObject target = ResolvePrimaryTarget();
|
||||
return TryStartSignaturePattern(target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase 3 조합 패턴을 발동합니다.
|
||||
/// </summary>
|
||||
protected virtual bool TryStartComboPattern()
|
||||
{
|
||||
if (!IsComboPatternReady())
|
||||
return false;
|
||||
|
||||
currentTarget = ResolvePrimaryTarget();
|
||||
StartPattern(comboPattern, currentTarget);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected virtual BossPatternData SelectPrimaryLoopPattern()
|
||||
{
|
||||
BossPatternData primary = GetPattern(BossCombatPatternRole.Primary);
|
||||
BossPatternData secondary = GetPattern(BossCombatPatternRole.Secondary);
|
||||
|
||||
bool canUsePrimary = IsPatternReady(primary);
|
||||
bool canUseSecondary = IsPatternReady(secondary);
|
||||
|
||||
if (canUseSecondary && IsNextSecondaryPattern())
|
||||
{
|
||||
meleePatternCounter++;
|
||||
return secondary;
|
||||
}
|
||||
|
||||
if (canUsePrimary)
|
||||
{
|
||||
meleePatternCounter++;
|
||||
return primary;
|
||||
}
|
||||
|
||||
if (canUseSecondary)
|
||||
{
|
||||
meleePatternCounter++;
|
||||
return secondary;
|
||||
}
|
||||
|
||||
if (!IsPatternReady(primaryPattern))
|
||||
return null;
|
||||
|
||||
meleePatternCounter++;
|
||||
return primaryPattern;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 기본 패턴을 선택하고 카운터를 갱신합니다.
|
||||
/// </summary>
|
||||
public BossPatternData SelectAndRegisterBasicLoopPattern()
|
||||
{
|
||||
if (!IsPatternReady(primaryPattern))
|
||||
return null;
|
||||
|
||||
RegisterPatternUse(primaryPattern);
|
||||
return primaryPattern;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 기본 패턴이 사용 가능한지 확인합니다.
|
||||
/// 상태 변경 없이 순수 검사만 수행합니다.
|
||||
/// </summary>
|
||||
public bool IsBasicLoopReady()
|
||||
{
|
||||
return IsPatternReady(primaryPattern);
|
||||
}
|
||||
|
||||
protected virtual void StartPattern(BossPatternData pattern, GameObject target)
|
||||
@@ -655,9 +750,18 @@ namespace Colosseum.Enemy
|
||||
break;
|
||||
}
|
||||
|
||||
if (step.Skill.JumpToTarget && target != null)
|
||||
if (step.Skill.JumpToTarget)
|
||||
{
|
||||
enemyBase?.SetJumpTarget(target.transform.position);
|
||||
GameObject jumpTarget = FindMobilityTarget();
|
||||
if (jumpTarget == null)
|
||||
{
|
||||
LogDebug(GetType().Name, $"점프 대상 없음, 패턴 조기 종료: {pattern.PatternName}");
|
||||
break;
|
||||
}
|
||||
|
||||
target = jumpTarget;
|
||||
currentTarget = jumpTarget;
|
||||
enemyBase?.SetJumpTarget(jumpTarget.transform.position);
|
||||
}
|
||||
|
||||
if (!skillController.ExecuteSkill(step.Skill))
|
||||
@@ -686,9 +790,6 @@ namespace Colosseum.Enemy
|
||||
if (pattern == null || pattern.Steps == null || pattern.Steps.Count == 0)
|
||||
return false;
|
||||
|
||||
if (CurrentPatternPhase < pattern.MinPhase)
|
||||
return false;
|
||||
|
||||
if (!patternCooldownTracker.TryGetValue(pattern, out float readyTime))
|
||||
return true;
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
namespace Colosseum.Enemy
|
||||
{
|
||||
/// <summary>
|
||||
/// 보스 전투 BT에서 사용하는 공통 패턴 역할 구분값입니다.
|
||||
/// </summary>
|
||||
public enum BossCombatPatternRole
|
||||
{
|
||||
Primary = 0,
|
||||
Secondary = 1,
|
||||
Mobility = 2,
|
||||
Punish = 3,
|
||||
Signature = 4,
|
||||
Utility = 5,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 공통 패턴 역할 보조 확장 메서드입니다.
|
||||
/// </summary>
|
||||
public static class BossCombatPatternRoleExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 현재 역할이 근접 순환 패턴인지 반환합니다.
|
||||
/// </summary>
|
||||
public static bool IsMeleeRole(this BossCombatPatternRole role)
|
||||
{
|
||||
return role == BossCombatPatternRole.Primary || role == BossCombatPatternRole.Secondary;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ee34c0af35cdfbd45baf0a6b9dcc2dd9
|
||||
2
Assets/_Game/Scripts/Skills/Effects/ReviveEffect.cs.meta
Normal file
2
Assets/_Game/Scripts/Skills/Effects/ReviveEffect.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 848cbb76281c68842a4d00329110b769
|
||||
0
git status
Normal file
0
git status
Normal file
Reference in New Issue
Block a user