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:
2026-03-30 15:34:21 +09:00
parent dea7fd39ec
commit c6fc56e9c6
56 changed files with 3287 additions and 3541 deletions

View File

@@ -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;
meleePatternCounter++;
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);
if (!IsPatternReady(primaryPattern))
return null;
bool canUsePrimary = IsPatternReady(primary);
bool canUseSecondary = IsPatternReady(secondary);
meleePatternCounter++;
return primaryPattern;
}
if (canUseSecondary && IsNextSecondaryPattern())
{
meleePatternCounter++;
return secondary;
}
/// <summary>
/// 기본 패턴을 선택하고 카운터를 갱신합니다.
/// </summary>
public BossPatternData SelectAndRegisterBasicLoopPattern()
{
if (!IsPatternReady(primaryPattern))
return null;
if (canUsePrimary)
{
meleePatternCounter++;
return primary;
}
RegisterPatternUse(primaryPattern);
return primaryPattern;
}
if (canUseSecondary)
{
meleePatternCounter++;
return secondary;
}
return null;
/// <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;