feat: 드로그 공통 보스 BT 프레임워크 정리
- 보스 공통 전투 컨텍스트와 패턴 역할 기반 BT 액션을 추가 - 드로그 패턴 선택을 다운 추가타, 도약, 기본 및 보조 패턴 우선순위 브랜치로 이관 - BT_Drog authoring 그래프를 공통 구조에 맞게 재구성 - 드로그 전용 BT 헬퍼를 정리하고 공통 베이스 액션으로 통합 - 플레이 검증으로 도약, 기본 패턴, 내려찍기, 다운 추가타 루프를 확인
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -62,6 +62,13 @@ ModelImporter:
|
||||
floatParameter: 0
|
||||
intParameter: 0
|
||||
messageOptions: 0
|
||||
- time: 0.53048134
|
||||
functionName: OnEffect
|
||||
data:
|
||||
objectReferenceParameter: {instanceID: 0}
|
||||
floatParameter: 0
|
||||
intParameter: 1
|
||||
messageOptions: 0
|
||||
- time: 0.99549305
|
||||
functionName: OnSkillEnd
|
||||
data:
|
||||
|
||||
@@ -55,13 +55,6 @@ ModelImporter:
|
||||
bodyMask: 01000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000
|
||||
curves: []
|
||||
events:
|
||||
- time: 0.35211262
|
||||
functionName: OnEffect
|
||||
data:
|
||||
objectReferenceParameter: {instanceID: 0}
|
||||
floatParameter: 0
|
||||
intParameter: 1
|
||||
messageOptions: 0
|
||||
- time: 0.3636364
|
||||
functionName: OnEffect
|
||||
data:
|
||||
@@ -69,6 +62,13 @@ ModelImporter:
|
||||
floatParameter: 0
|
||||
intParameter: 0
|
||||
messageOptions: 0
|
||||
- time: 0.37272727
|
||||
functionName: OnEffect
|
||||
data:
|
||||
objectReferenceParameter: {instanceID: 0}
|
||||
floatParameter: 0
|
||||
intParameter: 1
|
||||
messageOptions: 0
|
||||
- time: 0.9887324
|
||||
functionName: OnSkillEnd
|
||||
data:
|
||||
|
||||
@@ -471,6 +471,13 @@ ModelImporter:
|
||||
floatParameter: 0
|
||||
intParameter: 0
|
||||
messageOptions: 0
|
||||
- time: 0.06148243
|
||||
functionName: OnEffect
|
||||
data:
|
||||
objectReferenceParameter: {instanceID: 0}
|
||||
floatParameter: 0
|
||||
intParameter: 1
|
||||
messageOptions: 0
|
||||
transformMask: []
|
||||
maskType: 3
|
||||
maskSource: {fileID: 31900000, guid: e8e1ad9aea8c740458a8550aa77c27b0, type: 2}
|
||||
|
||||
20
Assets/_Game/Data/Patterns/Data_Pattern_Drog_내려찍기.asset
Normal file
20
Assets/_Game/Data/Patterns/Data_Pattern_Drog_내려찍기.asset
Normal file
@@ -0,0 +1,20 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 0ce956e0878565343974c31b8111c0c6, type: 3}
|
||||
m_Name: "Data_Pattern_Drog_\uB0B4\uB824\uCC0D\uAE30"
|
||||
m_EditorClassIdentifier: Colosseum.Game::Colosseum.AI.BossPatternData
|
||||
patternName: "\uB0B4\uB824\uCC0D\uAE30"
|
||||
steps:
|
||||
- Type: 0
|
||||
Skill: {fileID: 11400000, guid: 773afd8dabe30374c826b7fa1d1a68ea, type: 2}
|
||||
Duration: 0
|
||||
cooldown: 4
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4a52d59d590b4eaa9ef92b7984eb08c7
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -17,9 +17,6 @@ MonoBehaviour:
|
||||
- Type: 0
|
||||
Skill: {fileID: 11400000, guid: 4f40629d4d334434285e8fdec3714536, type: 2}
|
||||
Duration: 0
|
||||
- Type: 0
|
||||
Skill: {fileID: 11400000, guid: 773afd8dabe30374c826b7fa1d1a68ea, type: 2}
|
||||
Duration: 0
|
||||
- Type: 0
|
||||
Skill: {fileID: 11400000, guid: 3acbf1c5ec71bef4bb13f8534605d554, type: 2}
|
||||
Duration: 0.5
|
||||
|
||||
@@ -12,7 +12,7 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: 0ce956e0878565343974c31b8111c0c6, type: 3}
|
||||
m_Name: "Data_Pattern_Drog_\uC810\uD504"
|
||||
m_EditorClassIdentifier: Colosseum.Game::Colosseum.AI.BossPatternData
|
||||
patternName: "\uAE30\uBCF8 \uD328\uD134"
|
||||
patternName: "\uB3C4\uC57D"
|
||||
steps:
|
||||
- Type: 0
|
||||
Skill: {fileID: 11400000, guid: 0e22d4b1dc395a04fb00ca4f82aeb838, type: 2}
|
||||
|
||||
@@ -25,3 +25,4 @@ MonoBehaviour:
|
||||
manaCost: 0
|
||||
effects:
|
||||
- {fileID: 11400000, guid: 0f134a897a7e4d0e98c8d9058b1d79d1, type: 2}
|
||||
- {fileID: 11400000, guid: 216d4b5f6ce9479e94e0d306399f4891, type: 2}
|
||||
|
||||
@@ -23,4 +23,5 @@ MonoBehaviour:
|
||||
manaCost: 0
|
||||
effects:
|
||||
- {fileID: 11400000, guid: 94b0f3305cea88c458a56783a486340e, type: 2}
|
||||
- {fileID: 11400000, guid: e9d73ea9d8f4462ab5d2f2d844eef2ab, type: 2}
|
||||
|
||||
|
||||
@@ -23,5 +23,5 @@ MonoBehaviour:
|
||||
manaCost: 0
|
||||
effects:
|
||||
- {fileID: 11400000, guid: 87b064a0134987b4b9638e184ab07411, type: 2}
|
||||
- {fileID: 11400000, guid: 0dba6dca651743bc84b0df42f9dbd290, type: 2}
|
||||
- {fileID: 11400000, guid: 2db6d8d7f5da4f7ab9f0a12e65498ab1, type: 2}
|
||||
|
||||
|
||||
@@ -25,4 +25,5 @@ MonoBehaviour:
|
||||
manaCost: 0
|
||||
effects:
|
||||
- {fileID: 11400000, guid: 11bd2d1ebdbfc2f4abf5a9d886615eb3, type: 2}
|
||||
- {fileID: 11400000, guid: 74cc72abcf5f49d0aed2d0a2258c73d8, type: 2}
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 27cd4e4eb6a485845953db2a108a37f8, type: 3}
|
||||
m_Name: "Data_SkillEffect_Drog_\uB2E4\uC6B4\uCD94\uAC00\uD0C0_1_\uB109\uBC31"
|
||||
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.Effects.KnockbackEffect
|
||||
targetType: 1
|
||||
targetTeam: 0
|
||||
areaCenter: 0
|
||||
areaShape: 0
|
||||
targetLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
areaRadius: 3.25
|
||||
fanOriginDistance: 1
|
||||
fanRadius: 3
|
||||
fanHalfAngle: 45
|
||||
force: 6
|
||||
upwardForce: 1
|
||||
duration: 0.25
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 216d4b5f6ce9479e94e0d306399f4891
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,28 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 27cd4e4eb6a485845953db2a108a37f8, type: 3}
|
||||
m_Name: "Data_SkillEffect_Drog_\uC2A4\uC719_1_\uB109\uBC31"
|
||||
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.Effects.KnockbackEffect
|
||||
targetType: 1
|
||||
targetTeam: 0
|
||||
areaCenter: 0
|
||||
areaShape: 1
|
||||
targetLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
areaRadius: 3
|
||||
fanOriginDistance: 0
|
||||
fanRadius: 3
|
||||
fanHalfAngle: 45
|
||||
force: 7
|
||||
upwardForce: 1.5
|
||||
duration: 0.3
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e9d73ea9d8f4462ab5d2f2d844eef2ab
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -23,4 +23,4 @@ MonoBehaviour:
|
||||
fanOriginDistance: 0
|
||||
fanRadius: 3
|
||||
fanHalfAngle: 45
|
||||
duration: 2.5
|
||||
duration: 1.25
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0dba6dca651743bc84b0df42f9dbd290
|
||||
guid: 2db6d8d7f5da4f7ab9f0a12e65498ab1
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 27cd4e4eb6a485845953db2a108a37f8, type: 3}
|
||||
m_Name: "Data_SkillEffect_Drog_\uC810\uD504_1_\uB109\uBC31"
|
||||
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.Effects.KnockbackEffect
|
||||
targetType: 1
|
||||
targetTeam: 0
|
||||
areaCenter: 0
|
||||
areaShape: 0
|
||||
targetLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
areaRadius: 5
|
||||
fanOriginDistance: 1
|
||||
fanRadius: 3
|
||||
fanHalfAngle: 45
|
||||
force: 7
|
||||
upwardForce: 1.25
|
||||
duration: 0.35
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 74cc72abcf5f49d0aed2d0a2258c73d8
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -2187,6 +2187,7 @@ MonoBehaviour:
|
||||
skillController: {fileID: 8818883032728065057}
|
||||
navMeshAgent: {fileID: 5153439431748782209}
|
||||
mainPattern: {fileID: 11400000, guid: 5efd8123be76bf844875d386d9d5f73d, type: 2}
|
||||
slamPattern: {fileID: 11400000, guid: 4a52d59d590b4eaa9ef92b7984eb08c7, type: 2}
|
||||
leapPattern: {fileID: 11400000, guid: 88e6cc7cab28baf4c8f8a742247000ec, type: 2}
|
||||
downPunishPattern: {fileID: 11400000, guid: fe5100f855d14c0faac44b6d4f2c771e, type: 2}
|
||||
phase2HealthThreshold: 0.75
|
||||
@@ -2194,7 +2195,10 @@ MonoBehaviour:
|
||||
targetRefreshInterval: 0.2
|
||||
leapDistanceThreshold: 8
|
||||
downPunishSearchRadius: 6
|
||||
disableBehaviorGraph: 1
|
||||
phase1SlamInterval: 3
|
||||
phase2SlamInterval: 2
|
||||
phase3SlamInterval: 2
|
||||
disableBehaviorGraph: 0
|
||||
debugMode: 1
|
||||
--- !u!114 &7544406269366897481
|
||||
MonoBehaviour:
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
using System;
|
||||
|
||||
using Colosseum;
|
||||
using Colosseum.AI;
|
||||
using Colosseum.Combat;
|
||||
using Colosseum.Enemy;
|
||||
using Colosseum.Player;
|
||||
using Colosseum.Skills;
|
||||
|
||||
using Unity.Behavior;
|
||||
using Unity.Properties;
|
||||
using UnityEngine;
|
||||
|
||||
using Action = Unity.Behavior.Action;
|
||||
|
||||
/// <summary>
|
||||
/// 보스 공통 패턴 실행용 Behavior Action 기반 클래스입니다.
|
||||
/// </summary>
|
||||
[Serializable, GeneratePropertyBag]
|
||||
public abstract partial class BossPatternActionBase : Action
|
||||
{
|
||||
[SerializeReference]
|
||||
public BlackboardVariable<GameObject> Target;
|
||||
|
||||
protected BossEnemy bossEnemy;
|
||||
protected EnemyBase enemyBase;
|
||||
protected SkillController skillController;
|
||||
protected BossCombatBehaviorContext combatBehaviorContext;
|
||||
protected UnityEngine.AI.NavMeshAgent navMeshAgent;
|
||||
|
||||
private BossPatternData activePattern;
|
||||
private GameObject activeTarget;
|
||||
private int currentStepIndex;
|
||||
private bool isWaiting;
|
||||
private float waitEndTime;
|
||||
|
||||
/// <summary>
|
||||
/// 액션 시작 시 실제로 실행할 패턴과 대상을 결정합니다.
|
||||
/// </summary>
|
||||
protected abstract bool TryResolvePattern(out BossPatternData pattern, out GameObject target);
|
||||
|
||||
protected override Status OnStart()
|
||||
{
|
||||
ResolveReferences();
|
||||
ClearRuntimeState();
|
||||
|
||||
if (!IsReady())
|
||||
return Status.Failure;
|
||||
|
||||
if (bossEnemy.IsDead || bossEnemy.IsTransitioning)
|
||||
return Status.Failure;
|
||||
|
||||
if (skillController.IsPlayingAnimation)
|
||||
return Status.Failure;
|
||||
|
||||
if (!TryResolvePattern(out BossPatternData pattern, out GameObject target))
|
||||
return Status.Failure;
|
||||
|
||||
activePattern = pattern;
|
||||
activeTarget = target;
|
||||
|
||||
if (Target != null)
|
||||
Target.Value = target;
|
||||
|
||||
StopMovement();
|
||||
return ExecuteCurrentStep();
|
||||
}
|
||||
|
||||
protected override Status OnUpdate()
|
||||
{
|
||||
if (!IsReady() || activePattern == null)
|
||||
return Status.Failure;
|
||||
|
||||
if (bossEnemy.IsDead || bossEnemy.IsTransitioning)
|
||||
return Status.Failure;
|
||||
|
||||
if (isWaiting)
|
||||
{
|
||||
if (Time.time < waitEndTime)
|
||||
return Status.Running;
|
||||
|
||||
isWaiting = false;
|
||||
}
|
||||
else if (skillController.IsPlayingAnimation)
|
||||
{
|
||||
return Status.Running;
|
||||
}
|
||||
|
||||
currentStepIndex++;
|
||||
if (currentStepIndex >= activePattern.Steps.Count)
|
||||
{
|
||||
UsePatternAction.MarkPatternUsed(GameObject, activePattern);
|
||||
return Status.Success;
|
||||
}
|
||||
|
||||
return ExecuteCurrentStep();
|
||||
}
|
||||
|
||||
protected override void OnEnd()
|
||||
{
|
||||
ClearRuntimeState();
|
||||
}
|
||||
|
||||
protected virtual GameObject ResolveStepTarget(GameObject fallbackTarget)
|
||||
{
|
||||
return fallbackTarget;
|
||||
}
|
||||
|
||||
protected GameObject FindNearestLivingPlayer()
|
||||
{
|
||||
PlayerNetworkController[] players = UnityEngine.Object.FindObjectsByType<PlayerNetworkController>(FindObjectsSortMode.None);
|
||||
GameObject nearestTarget = null;
|
||||
float nearestDistance = float.MaxValue;
|
||||
float maxDistance = enemyBase != null && enemyBase.Data != null ? enemyBase.Data.AggroRange : Mathf.Infinity;
|
||||
|
||||
for (int i = 0; i < players.Length; i++)
|
||||
{
|
||||
PlayerNetworkController player = players[i];
|
||||
if (player == null || player.IsDead || !player.gameObject.activeInHierarchy)
|
||||
continue;
|
||||
|
||||
GameObject candidate = player.gameObject;
|
||||
if (Team.IsSameTeam(GameObject, candidate))
|
||||
continue;
|
||||
|
||||
float distance = Vector3.Distance(GameObject.transform.position, candidate.transform.position);
|
||||
if (distance > maxDistance || distance >= nearestDistance)
|
||||
continue;
|
||||
|
||||
nearestDistance = distance;
|
||||
nearestTarget = candidate;
|
||||
}
|
||||
|
||||
return nearestTarget;
|
||||
}
|
||||
|
||||
protected GameObject ResolvePrimaryTarget()
|
||||
{
|
||||
GameObject highestThreatTarget = enemyBase != null
|
||||
? enemyBase.GetHighestThreatTarget(Target?.Value, null, enemyBase.Data != null ? enemyBase.Data.AggroRange : Mathf.Infinity)
|
||||
: null;
|
||||
|
||||
GameObject target = highestThreatTarget != null ? highestThreatTarget : FindNearestLivingPlayer();
|
||||
if (Target != null)
|
||||
Target.Value = target;
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
protected bool IsValidHostileTarget(GameObject candidate)
|
||||
{
|
||||
if (candidate == null || !candidate.activeInHierarchy)
|
||||
return false;
|
||||
|
||||
if (Team.IsSameTeam(GameObject, candidate))
|
||||
return false;
|
||||
|
||||
IDamageable damageable = candidate.GetComponent<IDamageable>();
|
||||
return damageable == null || !damageable.IsDead;
|
||||
}
|
||||
|
||||
protected void StopMovement()
|
||||
{
|
||||
if (navMeshAgent == null || !navMeshAgent.enabled)
|
||||
return;
|
||||
|
||||
navMeshAgent.isStopped = true;
|
||||
navMeshAgent.ResetPath();
|
||||
}
|
||||
|
||||
protected void LogDebug(string message)
|
||||
{
|
||||
combatBehaviorContext?.LogDebug(GetType().Name, message);
|
||||
}
|
||||
|
||||
private Status ExecuteCurrentStep()
|
||||
{
|
||||
if (activePattern == null || currentStepIndex < 0 || currentStepIndex >= activePattern.Steps.Count)
|
||||
return Status.Failure;
|
||||
|
||||
PatternStep step = activePattern.Steps[currentStepIndex];
|
||||
if (step.Type == PatternStepType.Wait)
|
||||
{
|
||||
isWaiting = true;
|
||||
waitEndTime = Time.time + step.Duration;
|
||||
return Status.Running;
|
||||
}
|
||||
|
||||
if (step.Skill == null)
|
||||
{
|
||||
Debug.LogWarning($"[{GetType().Name}] 스킬이 비어 있는 패턴 스텝입니다: {activePattern.PatternName} / Step={currentStepIndex}");
|
||||
return Status.Failure;
|
||||
}
|
||||
|
||||
GameObject skillTarget = activeTarget;
|
||||
if (step.Skill.JumpToTarget)
|
||||
{
|
||||
skillTarget = ResolveStepTarget(activeTarget);
|
||||
if (skillTarget == null)
|
||||
{
|
||||
LogDebug($"점프 타겟을 찾지 못해 실패: {activePattern.PatternName}");
|
||||
return Status.Failure;
|
||||
}
|
||||
|
||||
enemyBase?.SetJumpTarget(skillTarget.transform.position);
|
||||
}
|
||||
|
||||
if (!skillController.ExecuteSkill(step.Skill))
|
||||
{
|
||||
Debug.LogWarning($"[{GetType().Name}] 스킬 실행 실패: {step.Skill.SkillName}");
|
||||
return Status.Failure;
|
||||
}
|
||||
|
||||
LogDebug($"패턴 실행: {activePattern.PatternName} / Step={currentStepIndex} / Skill={step.Skill.SkillName}");
|
||||
return Status.Running;
|
||||
}
|
||||
|
||||
private bool IsReady()
|
||||
{
|
||||
return bossEnemy != null && enemyBase != null && skillController != null && combatBehaviorContext != null;
|
||||
}
|
||||
|
||||
private void ResolveReferences()
|
||||
{
|
||||
if (bossEnemy == null)
|
||||
bossEnemy = GameObject.GetComponent<BossEnemy>();
|
||||
|
||||
if (enemyBase == null)
|
||||
enemyBase = GameObject.GetComponent<EnemyBase>();
|
||||
|
||||
if (skillController == null)
|
||||
skillController = GameObject.GetComponent<SkillController>();
|
||||
|
||||
if (combatBehaviorContext == null)
|
||||
combatBehaviorContext = GameObject.GetComponent<BossCombatBehaviorContext>();
|
||||
|
||||
if (navMeshAgent == null)
|
||||
navMeshAgent = GameObject.GetComponent<UnityEngine.AI.NavMeshAgent>();
|
||||
}
|
||||
|
||||
private void ClearRuntimeState()
|
||||
{
|
||||
activePattern = null;
|
||||
activeTarget = null;
|
||||
currentStepIndex = 0;
|
||||
isWaiting = false;
|
||||
waitEndTime = 0f;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 44fff80e86b16f6419b0e952efbebf2a
|
||||
@@ -0,0 +1,20 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 489f56d9043e6d24fbe8e5574b6729be
|
||||
@@ -0,0 +1,31 @@
|
||||
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;
|
||||
|
||||
BossPatternData pattern = context.GetPattern(PatternRole);
|
||||
return UsePatternAction.IsPatternReady(GameObject, pattern) ? Status.Success : Status.Failure;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 59a58b1d5cc33f943a1af10764ee11b5
|
||||
@@ -0,0 +1,20 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e3b45dc2b81beac44a35a3a6545c0488
|
||||
@@ -0,0 +1,20 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2211d1182dbbf7741b0058718afae162
|
||||
@@ -0,0 +1,20 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e155d7ca234bf8148bef34617a3a8739
|
||||
@@ -0,0 +1,26 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 435950077eea65d43beb6bfaba38dc60
|
||||
@@ -0,0 +1,35 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5b3844411f6dd784089c40c5d4325b45
|
||||
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
|
||||
using Colosseum.Enemy;
|
||||
|
||||
using Unity.Behavior;
|
||||
using Unity.Properties;
|
||||
using UnityEngine;
|
||||
|
||||
using Action = Unity.Behavior.Action;
|
||||
|
||||
/// <summary>
|
||||
/// 보스의 주 대상을 갱신하는 공통 Behavior Action입니다.
|
||||
/// </summary>
|
||||
[Serializable, GeneratePropertyBag]
|
||||
[NodeDescription(
|
||||
name: "Refresh Primary Target",
|
||||
story: "보스 주 대상을 [Target]으로 갱신",
|
||||
category: "Action",
|
||||
id: "b7dbb1fc0c0d451795e9f02d6f4d3930")]
|
||||
public partial class RefreshPrimaryTargetAction : Action
|
||||
{
|
||||
[SerializeReference]
|
||||
public BlackboardVariable<GameObject> Target;
|
||||
|
||||
protected override Status OnStart()
|
||||
{
|
||||
EnemyBase enemyBase = GameObject.GetComponent<EnemyBase>();
|
||||
if (enemyBase == null)
|
||||
return Status.Failure;
|
||||
|
||||
GameObject currentTarget = Target != null ? Target.Value : null;
|
||||
float aggroRange = enemyBase.Data != null ? enemyBase.Data.AggroRange : Mathf.Infinity;
|
||||
GameObject resolvedTarget = enemyBase.GetHighestThreatTarget(currentTarget, null, aggroRange);
|
||||
|
||||
if (resolvedTarget == null)
|
||||
{
|
||||
BossCombatBehaviorContext context = GameObject.GetComponent<BossCombatBehaviorContext>();
|
||||
resolvedTarget = context != null ? context.FindNearestLivingTarget() : null;
|
||||
}
|
||||
|
||||
if (Target != null)
|
||||
Target.Value = resolvedTarget;
|
||||
|
||||
return resolvedTarget != null ? Status.Success : Status.Failure;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 47bff77f5775ca3409b7f9676596d561
|
||||
@@ -0,0 +1,93 @@
|
||||
using System;
|
||||
|
||||
using Colosseum;
|
||||
using Colosseum.Combat;
|
||||
using Colosseum.Enemy;
|
||||
using Colosseum.Player;
|
||||
|
||||
using Unity.Behavior;
|
||||
using Unity.Properties;
|
||||
using UnityEngine;
|
||||
|
||||
using Action = Unity.Behavior.Action;
|
||||
|
||||
/// <summary>
|
||||
/// 일정 반경 내에서 가장 가까운 다운 대상 플레이어를 선택하는 Behavior Action입니다.
|
||||
/// </summary>
|
||||
[Serializable, GeneratePropertyBag]
|
||||
[NodeDescription(
|
||||
name: "Select Nearest Downed Target",
|
||||
story: "[SearchRadius] 반경 내 가장 가까운 다운 대상을 [Target]으로 선택",
|
||||
category: "Action",
|
||||
id: "ee1146ad46ec4730acb4d6c883a5a771")]
|
||||
public partial class SelectNearestDownedTargetAction : Action
|
||||
{
|
||||
[SerializeReference]
|
||||
public BlackboardVariable<GameObject> Target;
|
||||
|
||||
[SerializeReference]
|
||||
public BlackboardVariable<float> SearchRadius = new BlackboardVariable<float>(0f);
|
||||
|
||||
protected override Status OnStart()
|
||||
{
|
||||
float searchRadius = ResolveSearchRadius();
|
||||
HitReactionController[] hitReactionControllers = UnityEngine.Object.FindObjectsByType<HitReactionController>(FindObjectsSortMode.None);
|
||||
|
||||
GameObject nearestTarget = 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(GameObject.transform.position, candidate.transform.position);
|
||||
if (distance > searchRadius || distance >= nearestDistance)
|
||||
continue;
|
||||
|
||||
nearestDistance = distance;
|
||||
nearestTarget = candidate;
|
||||
}
|
||||
|
||||
if (nearestTarget == null)
|
||||
return Status.Failure;
|
||||
|
||||
Target.Value = nearestTarget;
|
||||
LogDebug($"다운 대상 선택: {nearestTarget.name}");
|
||||
return Status.Success;
|
||||
}
|
||||
|
||||
private float ResolveSearchRadius()
|
||||
{
|
||||
if (SearchRadius.Value > 0f)
|
||||
return SearchRadius.Value;
|
||||
|
||||
BossCombatBehaviorContext context = GameObject.GetComponent<BossCombatBehaviorContext>();
|
||||
if (context != null)
|
||||
return context.PunishSearchRadius;
|
||||
|
||||
EnemyBase enemyBase = GameObject.GetComponent<EnemyBase>();
|
||||
if (enemyBase != null && enemyBase.Data != null)
|
||||
return enemyBase.Data.AttackRange + 4f;
|
||||
|
||||
return 6f;
|
||||
}
|
||||
|
||||
private void LogDebug(string message)
|
||||
{
|
||||
BossCombatBehaviorContext context = GameObject.GetComponent<BossCombatBehaviorContext>();
|
||||
context?.LogDebug(nameof(SelectNearestDownedTargetAction), message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f7453c48fa1599a44a10be23750ade75
|
||||
@@ -0,0 +1,172 @@
|
||||
using System;
|
||||
|
||||
using Unity.Behavior;
|
||||
using Unity.Properties;
|
||||
using UnityEngine;
|
||||
|
||||
using Colosseum;
|
||||
using Colosseum.Combat;
|
||||
using Colosseum.Enemy;
|
||||
|
||||
using Action = Unity.Behavior.Action;
|
||||
|
||||
/// <summary>
|
||||
/// 거리 조건에 맞는 대상을 선택하는 Behavior Action입니다.
|
||||
/// 가장 가까운 대상, 가장 먼 대상, 후보 중 랜덤 선택을 지원합니다.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public enum DistanceTargetSelectionMode
|
||||
{
|
||||
Nearest = 0,
|
||||
Farthest = 1,
|
||||
Random = 2,
|
||||
}
|
||||
|
||||
[Serializable, GeneratePropertyBag]
|
||||
[NodeDescription(
|
||||
name: "Select Target By Distance",
|
||||
story: "[Tag] 대상 중 [MinRange] 이상 [MaxRange] 이하 범위에서 [SelectionMode] 방식으로 [Target] 선택",
|
||||
category: "Action",
|
||||
id: "4f6a830df3ff4ff5bf8bd2c8b433aa41")]
|
||||
public partial class SelectTargetByDistanceAction : Action
|
||||
{
|
||||
[SerializeReference]
|
||||
public BlackboardVariable<GameObject> Target;
|
||||
|
||||
[SerializeReference]
|
||||
public BlackboardVariable<string> Tag = new BlackboardVariable<string>("Player");
|
||||
|
||||
[SerializeReference]
|
||||
public BlackboardVariable<float> MinRange = new BlackboardVariable<float>(0f);
|
||||
|
||||
[SerializeReference]
|
||||
public BlackboardVariable<float> MaxRange = new BlackboardVariable<float>(20f);
|
||||
|
||||
[SerializeReference]
|
||||
public BlackboardVariable<DistanceTargetSelectionMode> SelectionMode =
|
||||
new BlackboardVariable<DistanceTargetSelectionMode>(DistanceTargetSelectionMode.Farthest);
|
||||
|
||||
protected override Status OnStart()
|
||||
{
|
||||
if (string.IsNullOrEmpty(Tag.Value))
|
||||
return Status.Failure;
|
||||
|
||||
GameObject[] candidates = GameObject.FindGameObjectsWithTag(Tag.Value);
|
||||
if (candidates == null || candidates.Length == 0)
|
||||
return Status.Failure;
|
||||
|
||||
float minRange = Mathf.Max(0f, MinRange.Value);
|
||||
float maxRange = Mathf.Max(minRange, MaxRange.Value);
|
||||
BossCombatBehaviorContext context = GameObject.GetComponent<BossCombatBehaviorContext>();
|
||||
|
||||
if (minRange <= 0f && context != null)
|
||||
minRange = context.MobilityTriggerDistance;
|
||||
|
||||
if (maxRange <= minRange)
|
||||
{
|
||||
EnemyBase enemyBase = GameObject.GetComponent<EnemyBase>();
|
||||
maxRange = enemyBase != null && enemyBase.Data != null ? enemyBase.Data.AggroRange : 20f;
|
||||
}
|
||||
|
||||
GameObject selectedTarget = SelectionMode.Value switch
|
||||
{
|
||||
DistanceTargetSelectionMode.Nearest => FindNearestTarget(candidates, minRange, maxRange),
|
||||
DistanceTargetSelectionMode.Random => FindRandomTarget(candidates, minRange, maxRange),
|
||||
_ => FindFarthestTarget(candidates, minRange, maxRange),
|
||||
};
|
||||
|
||||
if (selectedTarget == null)
|
||||
return Status.Failure;
|
||||
|
||||
Target.Value = selectedTarget;
|
||||
return Status.Success;
|
||||
}
|
||||
|
||||
private GameObject FindNearestTarget(GameObject[] candidates, float minRange, float maxRange)
|
||||
{
|
||||
GameObject bestTarget = null;
|
||||
float bestDistance = float.MaxValue;
|
||||
|
||||
for (int i = 0; i < candidates.Length; i++)
|
||||
{
|
||||
GameObject candidate = candidates[i];
|
||||
if (!IsValidTarget(candidate, minRange, maxRange, out float distance))
|
||||
continue;
|
||||
|
||||
if (distance >= bestDistance)
|
||||
continue;
|
||||
|
||||
bestDistance = distance;
|
||||
bestTarget = candidate;
|
||||
}
|
||||
|
||||
return bestTarget;
|
||||
}
|
||||
|
||||
private GameObject FindFarthestTarget(GameObject[] candidates, float minRange, float maxRange)
|
||||
{
|
||||
GameObject bestTarget = null;
|
||||
float bestDistance = minRange;
|
||||
|
||||
for (int i = 0; i < candidates.Length; i++)
|
||||
{
|
||||
GameObject candidate = candidates[i];
|
||||
if (!IsValidTarget(candidate, minRange, maxRange, out float distance))
|
||||
continue;
|
||||
|
||||
if (distance <= bestDistance)
|
||||
continue;
|
||||
|
||||
bestDistance = distance;
|
||||
bestTarget = candidate;
|
||||
}
|
||||
|
||||
return bestTarget;
|
||||
}
|
||||
|
||||
private GameObject FindRandomTarget(GameObject[] candidates, float minRange, float maxRange)
|
||||
{
|
||||
System.Collections.Generic.List<GameObject> validTargets = new System.Collections.Generic.List<GameObject>();
|
||||
|
||||
for (int i = 0; i < candidates.Length; i++)
|
||||
{
|
||||
GameObject candidate = candidates[i];
|
||||
if (!IsValidTarget(candidate, minRange, maxRange, out _))
|
||||
continue;
|
||||
|
||||
validTargets.Add(candidate);
|
||||
}
|
||||
|
||||
if (validTargets.Count == 0)
|
||||
return null;
|
||||
|
||||
int randomIndex = UnityEngine.Random.Range(0, validTargets.Count);
|
||||
return validTargets[randomIndex];
|
||||
}
|
||||
|
||||
private bool IsValidTarget(GameObject candidate, float minRange, float maxRange, out float distance)
|
||||
{
|
||||
distance = 0f;
|
||||
|
||||
if (candidate == null || !candidate.activeInHierarchy)
|
||||
return false;
|
||||
|
||||
if (Team.IsSameTeam(GameObject, candidate))
|
||||
return false;
|
||||
|
||||
IDamageable damageable = candidate.GetComponent<IDamageable>();
|
||||
if (damageable != null && damageable.IsDead)
|
||||
return false;
|
||||
|
||||
EnemyBase enemyBase = GameObject.GetComponent<EnemyBase>();
|
||||
float sightRange = enemyBase != null && enemyBase.Data != null
|
||||
? enemyBase.Data.AggroRange
|
||||
: maxRange;
|
||||
|
||||
distance = Vector3.Distance(GameObject.transform.position, candidate.transform.position);
|
||||
if (distance < minRange || distance > maxRange || distance > sightRange)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ce75af43cd1bd434495f187749228a63
|
||||
@@ -0,0 +1,20 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 137700b0db09e724899700f0da861132
|
||||
@@ -1,9 +1,15 @@
|
||||
using System;
|
||||
|
||||
using Colosseum;
|
||||
using Colosseum.AI;
|
||||
using Colosseum.Combat;
|
||||
using Colosseum.Enemy;
|
||||
using Colosseum.Skills;
|
||||
|
||||
using Unity.Behavior;
|
||||
using Unity.Properties;
|
||||
using UnityEngine;
|
||||
|
||||
using Action = Unity.Behavior.Action;
|
||||
|
||||
/// <summary>
|
||||
@@ -17,11 +23,13 @@ public partial class UsePatternAction : Action
|
||||
[SerializeReference] public BlackboardVariable<BossPatternData> Pattern;
|
||||
[SerializeReference] public BlackboardVariable<GameObject> Target;
|
||||
|
||||
private static readonly System.Collections.Generic.Dictionary<string, float> patternReadyTimes =
|
||||
new System.Collections.Generic.Dictionary<string, float>();
|
||||
|
||||
private SkillController skillController;
|
||||
private int currentStepIndex;
|
||||
private float waitEndTime;
|
||||
private bool isWaiting;
|
||||
private float lastUsedTime = float.MinValue;
|
||||
|
||||
protected override Status OnStart()
|
||||
{
|
||||
@@ -31,13 +39,21 @@ public partial class UsePatternAction : Action
|
||||
return Status.Failure;
|
||||
}
|
||||
|
||||
if (Time.time - lastUsedTime < Pattern.Value.Cooldown)
|
||||
if (!IsPatternReady(GameObject, Pattern.Value))
|
||||
{
|
||||
LogDebug($"쿨다운 중: {Pattern.Value.PatternName}");
|
||||
return Status.Failure;
|
||||
}
|
||||
|
||||
if (Pattern.Value.Steps.Count == 0)
|
||||
{
|
||||
LogDebug("스텝이 비어 있는 패턴이라 실패합니다.");
|
||||
return Status.Failure;
|
||||
}
|
||||
|
||||
if (RequiresJumpTarget(Pattern.Value) && ResolveJumpTarget() == null)
|
||||
{
|
||||
LogDebug($"점프 타겟을 찾지 못해 실패: {Pattern.Value.PatternName}");
|
||||
return Status.Failure;
|
||||
}
|
||||
|
||||
@@ -76,7 +92,7 @@ public partial class UsePatternAction : Action
|
||||
|
||||
if (currentStepIndex >= Pattern.Value.Steps.Count)
|
||||
{
|
||||
lastUsedTime = Time.time;
|
||||
MarkPatternUsed(GameObject, Pattern.Value);
|
||||
return Status.Success;
|
||||
}
|
||||
|
||||
@@ -106,6 +122,16 @@ public partial class UsePatternAction : Action
|
||||
return Status.Failure;
|
||||
}
|
||||
|
||||
GameObject jumpTarget = null;
|
||||
if (step.Skill.JumpToTarget)
|
||||
{
|
||||
jumpTarget = ResolveJumpTarget();
|
||||
if (jumpTarget == null)
|
||||
{
|
||||
return Status.Failure;
|
||||
}
|
||||
}
|
||||
|
||||
bool success = skillController.ExecuteSkill(step.Skill);
|
||||
if (!success)
|
||||
{
|
||||
@@ -113,15 +139,101 @@ public partial class UsePatternAction : Action
|
||||
return Status.Failure;
|
||||
}
|
||||
|
||||
LogDebug($"패턴 실행: {Pattern.Value.PatternName} / Step={currentStepIndex} / Skill={step.Skill.SkillName}");
|
||||
|
||||
// jumpToTarget 스킬이면 타겟 위치 전달
|
||||
if (step.Skill.JumpToTarget)
|
||||
{
|
||||
if (Target?.Value == null)
|
||||
Debug.LogWarning($"[UsePatternAction] '{step.Skill.SkillName}'은 JumpToTarget 스킬이지만 Target이 바인딩되지 않았습니다.");
|
||||
else
|
||||
GameObject.GetComponent<Colosseum.Enemy.EnemyBase>()?.SetJumpTarget(Target.Value.transform.position);
|
||||
GameObject.GetComponent<EnemyBase>()?.SetJumpTarget(jumpTarget.transform.position);
|
||||
}
|
||||
|
||||
return Status.Running;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 점프 대상이 근접 상태라면, 더 멀리 있는 유효 타겟으로 재선택합니다.
|
||||
/// 도약 패턴이 전방 탱커 대신 원거리 플레이어를 징벌할 수 있도록 합니다.
|
||||
/// </summary>
|
||||
private GameObject ResolveJumpTarget()
|
||||
{
|
||||
GameObject currentTarget = Target?.Value;
|
||||
EnemyBase enemyBase = GameObject.GetComponent<EnemyBase>();
|
||||
float maxJumpDistance = enemyBase?.Data != null ? enemyBase.Data.AggroRange : 20f;
|
||||
return IsValidJumpTarget(currentTarget, maxJumpDistance) ? currentTarget : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 점프 타겟의 생존 여부, 팀, 거리 조건을 검사합니다.
|
||||
/// </summary>
|
||||
private bool IsValidJumpTarget(GameObject candidate, float maxDistance)
|
||||
{
|
||||
if (candidate == null || !candidate.activeInHierarchy)
|
||||
return false;
|
||||
|
||||
if (Team.IsSameTeam(GameObject, candidate))
|
||||
return false;
|
||||
|
||||
IDamageable damageable = candidate.GetComponent<IDamageable>();
|
||||
if (damageable != null && damageable.IsDead)
|
||||
return false;
|
||||
|
||||
float distance = Vector3.Distance(GameObject.transform.position, candidate.transform.position);
|
||||
return distance <= maxDistance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 패턴 안에 점프 타겟이 필요한 스텝이 있는지 확인합니다.
|
||||
/// </summary>
|
||||
private static bool RequiresJumpTarget(BossPatternData pattern)
|
||||
{
|
||||
if (pattern == null || pattern.Steps == null)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < pattern.Steps.Count; i++)
|
||||
{
|
||||
PatternStep step = pattern.Steps[i];
|
||||
if (step.Type == PatternStepType.Skill && step.Skill != null && step.Skill.JumpToTarget)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 동일한 보스 오브젝트가 같은 패턴을 다시 사용할 수 있는지 확인합니다.
|
||||
/// </summary>
|
||||
public static bool IsPatternReady(GameObject owner, BossPatternData pattern)
|
||||
{
|
||||
if (owner == null || pattern == null)
|
||||
return false;
|
||||
|
||||
string cooldownKey = BuildCooldownKey(owner, pattern);
|
||||
if (!patternReadyTimes.TryGetValue(cooldownKey, out float readyTime))
|
||||
return true;
|
||||
|
||||
return Time.time >= readyTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 패턴 사용 직후 다음 사용 가능 시점을 기록합니다.
|
||||
/// </summary>
|
||||
public static void MarkPatternUsed(GameObject owner, BossPatternData pattern)
|
||||
{
|
||||
if (owner == null || pattern == null)
|
||||
return;
|
||||
|
||||
string cooldownKey = BuildCooldownKey(owner, pattern);
|
||||
patternReadyTimes[cooldownKey] = Time.time + pattern.Cooldown;
|
||||
}
|
||||
|
||||
private static string BuildCooldownKey(GameObject owner, BossPatternData pattern)
|
||||
{
|
||||
return $"{owner.GetInstanceID()}_{pattern.GetInstanceID()}";
|
||||
}
|
||||
|
||||
private void LogDebug(string message)
|
||||
{
|
||||
BossCombatBehaviorContext context = GameObject.GetComponent<BossCombatBehaviorContext>();
|
||||
context?.LogDebug(nameof(UsePatternAction), message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
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)
|
||||
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();
|
||||
}
|
||||
|
||||
return base.ResolveStepTarget(fallbackTarget);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 33384929fd7ec3c4f9240ac748de185c
|
||||
@@ -0,0 +1,20 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2d8de0b13ad776845a14b35e16485f53
|
||||
@@ -0,0 +1,20 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 16cef0dbbe7946d46b3021b0c1802669
|
||||
@@ -0,0 +1,20 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 79cd3375939c8a244bad9d8e1f02a45d
|
||||
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
|
||||
using Colosseum.Combat;
|
||||
|
||||
using Unity.Behavior;
|
||||
using Unity.Properties;
|
||||
using UnityEngine;
|
||||
|
||||
using Action = Unity.Behavior.Action;
|
||||
|
||||
/// <summary>
|
||||
/// 현재 타겟이 살아 있는 유효 대상인지 확인하는 공통 체크 액션입니다.
|
||||
/// </summary>
|
||||
[Serializable, GeneratePropertyBag]
|
||||
[NodeDescription(
|
||||
name: "Validate Target",
|
||||
story: "[Target]이 살아 있는 유효 대상인지 확인",
|
||||
category: "Action",
|
||||
id: "e9ec7a3b5a5447138ecf85ab0c57b21f")]
|
||||
public partial class ValidateTargetAction : Action
|
||||
{
|
||||
[SerializeReference]
|
||||
public BlackboardVariable<GameObject> Target;
|
||||
|
||||
protected override Status OnStart()
|
||||
{
|
||||
if (Target?.Value == null || !Target.Value.activeInHierarchy)
|
||||
return Status.Failure;
|
||||
|
||||
IDamageable damageable = Target.Value.GetComponent<IDamageable>();
|
||||
return damageable != null && damageable.IsDead ? Status.Failure : Status.Success;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4108d806232628e43839703188adeae8
|
||||
304
Assets/_Game/Scripts/Editor/RebuildDrogBehaviorAuthoringGraph.cs
Normal file
304
Assets/_Game/Scripts/Editor/RebuildDrogBehaviorAuthoringGraph.cs
Normal file
@@ -0,0 +1,304 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Colosseum.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// 드로그 Behavior Graph authoring 자산을 현재 BT 우선순위 구조로 재생성합니다.
|
||||
/// </summary>
|
||||
public static class RebuildDrogBehaviorAuthoringGraph
|
||||
{
|
||||
private const string GraphAssetPath = "Assets/_Game/AI/BT_Drog.asset";
|
||||
|
||||
[MenuItem("Tools/Colosseum/Rebuild Drog Behavior Authoring Graph")]
|
||||
private static void Rebuild()
|
||||
{
|
||||
UnityEngine.Object graphAsset = AssetDatabase.LoadMainAssetAtPath(GraphAssetPath);
|
||||
if (graphAsset == null)
|
||||
{
|
||||
Debug.LogError($"[DrogBTRebuild] 그래프 자산을 찾을 수 없습니다: {GraphAssetPath}");
|
||||
return;
|
||||
}
|
||||
|
||||
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 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);
|
||||
MethodInfo getNodeInfoMethod = authoringAssembly.GetType("Unity.Behavior.NodeRegistry", true)
|
||||
?.GetMethod("GetInfo", BindingFlags.Static | BindingFlags.NonPublic);
|
||||
|
||||
if (createNodeMethod == null || connectEdgeMethod == null || buildRuntimeGraphMethod == null || saveAssetMethod == null || setAssetDirtyMethod == null || getNodeInfoMethod == null)
|
||||
{
|
||||
Debug.LogError("[DrogBTRebuild] Behavior Authoring 리플렉션 메서드를 찾지 못했습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
SerializedObject serializedObject = new SerializedObject(graphAsset);
|
||||
SerializedProperty nodesProperty = serializedObject.FindProperty("m_Nodes");
|
||||
if (nodesProperty == null)
|
||||
{
|
||||
Debug.LogError("[DrogBTRebuild] m_Nodes 프로퍼티를 찾지 못했습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
nodesProperty.ClearArray();
|
||||
serializedObject.ApplyModifiedPropertiesWithoutUndo();
|
||||
|
||||
object targetVariable = FindBlackboardVariableModel("Target");
|
||||
if (targetVariable == null)
|
||||
{
|
||||
Debug.LogError("[DrogBTRebuild] Target 블랙보드 변수를 찾지 못했습니다.");
|
||||
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 downSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(-620f, -40f));
|
||||
object leapSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(-220f, -40f));
|
||||
object slamSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(180f, -40f));
|
||||
object mainSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(580f, -40f));
|
||||
object slamFallbackSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(980f, -40f));
|
||||
object chaseSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(1380f, -40f));
|
||||
|
||||
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));
|
||||
|
||||
object leapSelectNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(SelectTargetByDistanceAction), new Vector2(-340f, 240f));
|
||||
object leapReadyNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(CheckMobilityPatternReadyAction), new Vector2(-220f, 240f));
|
||||
object leapUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UseMobilityPatternAction), new Vector2(-100f, 240f));
|
||||
|
||||
object slamRefreshNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(RefreshPrimaryTargetAction), new Vector2(0f, 240f));
|
||||
object slamHasTargetNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ValidateTargetAction), new Vector2(120f, 240f));
|
||||
object slamRangeNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(CheckTargetInAttackRangeAction), new Vector2(240f, 240f));
|
||||
object slamTurnNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(CheckSecondaryPatternTurnAction), new Vector2(360f, 240f));
|
||||
object slamReadyNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(CheckSecondaryPatternReadyAction), new Vector2(480f, 240f));
|
||||
object slamUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UseSecondaryPatternAction), new Vector2(600f, 240f));
|
||||
|
||||
object mainRefreshNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(RefreshPrimaryTargetAction), new Vector2(520f, 240f));
|
||||
object mainHasTargetNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ValidateTargetAction), new Vector2(640f, 240f));
|
||||
object mainRangeNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(CheckTargetInAttackRangeAction), new Vector2(760f, 240f));
|
||||
object mainReadyNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(CheckPrimaryPatternReadyAction), new Vector2(880f, 240f));
|
||||
object mainUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UsePrimaryPatternAction), new Vector2(1000f, 240f));
|
||||
|
||||
object fallbackRefreshNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(RefreshPrimaryTargetAction), new Vector2(920f, 240f));
|
||||
object fallbackHasTargetNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ValidateTargetAction), new Vector2(1040f, 240f));
|
||||
object fallbackRangeNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(CheckTargetInAttackRangeAction), new Vector2(1160f, 240f));
|
||||
object fallbackReadyNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(CheckSecondaryPatternReadyAction), new Vector2(1280f, 240f));
|
||||
object fallbackUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UseSecondaryPatternAction), new Vector2(1400f, 240f));
|
||||
|
||||
object chaseRefreshNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(RefreshPrimaryTargetAction), new Vector2(1320f, 240f));
|
||||
object chaseHasTargetNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ValidateTargetAction), new Vector2(1440f, 240f));
|
||||
object chaseUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ChaseTargetAction), new Vector2(1560f, 240f));
|
||||
|
||||
Connect(graphAsset, connectEdgeMethod, GetDefaultOutputPort(startNode), GetDefaultInputPort(repeatNode));
|
||||
Connect(graphAsset, connectEdgeMethod, GetDefaultOutputPort(repeatNode), GetDefaultInputPort(selectorNode));
|
||||
ConnectChildren(graphAsset, connectEdgeMethod, selectorNode, downSequence, leapSequence, slamSequence, mainSequence, slamFallbackSequence, chaseSequence);
|
||||
|
||||
ConnectChildren(graphAsset, connectEdgeMethod, downSequence, downSelectNode, downReadyNode, downUseNode);
|
||||
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);
|
||||
ConnectChildren(graphAsset, connectEdgeMethod, chaseSequence, chaseRefreshNode, chaseHasTargetNode, chaseUseNode);
|
||||
|
||||
LinkTarget(downSelectNode, targetVariable);
|
||||
LinkTarget(downUseNode, 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);
|
||||
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 });
|
||||
saveAssetMethod.Invoke(graphAsset, null);
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
Debug.Log("[DrogBTRebuild] 드로그 Behavior Graph authoring 자산 재구성이 완료되었습니다.");
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.LogException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static object CreateNode(UnityEngine.Object graphAsset, MethodInfo createNodeMethod, MethodInfo getNodeInfoMethod, Type runtimeType, Vector2 position)
|
||||
{
|
||||
if (runtimeType == null)
|
||||
throw new InvalidOperationException("[DrogBTRebuild] 런타임 타입이 null입니다.");
|
||||
|
||||
object nodeInfo = getNodeInfoMethod.Invoke(null, new object[] { runtimeType });
|
||||
if (nodeInfo == null)
|
||||
throw new InvalidOperationException($"[DrogBTRebuild] NodeInfo를 찾지 못했습니다: {runtimeType.FullName}");
|
||||
|
||||
Type nodeInfoType = nodeInfo.GetType();
|
||||
FieldInfo modelTypeField = nodeInfoType.GetField("ModelType", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
object serializableType = modelTypeField?.GetValue(nodeInfo);
|
||||
PropertyInfo serializableTypeValueProperty = serializableType?.GetType().GetProperty("Type", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
Type modelType = serializableTypeValueProperty?.GetValue(serializableType) as Type;
|
||||
if (modelType == null)
|
||||
throw new InvalidOperationException($"[DrogBTRebuild] ModelType을 찾지 못했습니다: {runtimeType.FullName}");
|
||||
|
||||
return createNodeMethod.Invoke(graphAsset, new object[] { modelType, position, null, new object[] { nodeInfo } });
|
||||
}
|
||||
|
||||
private static void ConnectChildren(UnityEngine.Object graphAsset, MethodInfo connectEdgeMethod, object parentNode, params object[] children)
|
||||
{
|
||||
object outputPort = GetDefaultOutputPort(parentNode);
|
||||
for (int i = 0; i < children.Length; i++)
|
||||
{
|
||||
Connect(graphAsset, connectEdgeMethod, outputPort, GetDefaultInputPort(children[i]));
|
||||
}
|
||||
}
|
||||
|
||||
private static void Connect(UnityEngine.Object graphAsset, MethodInfo connectEdgeMethod, object outputPort, object inputPort)
|
||||
{
|
||||
if (outputPort == null || inputPort == null)
|
||||
throw new InvalidOperationException("[DrogBTRebuild] 포트 연결 대상이 null입니다.");
|
||||
|
||||
connectEdgeMethod.Invoke(graphAsset, new[] { outputPort, inputPort });
|
||||
}
|
||||
|
||||
private static object GetDefaultInputPort(object node)
|
||||
{
|
||||
return GetDefaultPort(node, "TryDefaultInputPortModel");
|
||||
}
|
||||
|
||||
private static object GetDefaultOutputPort(object node)
|
||||
{
|
||||
return GetDefaultPort(node, "TryDefaultOutputPortModel");
|
||||
}
|
||||
|
||||
private static object GetDefaultPort(object node, string methodName)
|
||||
{
|
||||
MethodInfo method = node.GetType().GetMethod(methodName, BindingFlags.Instance | BindingFlags.Public);
|
||||
object[] parameters = { null };
|
||||
bool success = method != null && (bool)method.Invoke(node, parameters);
|
||||
return success ? parameters[0] : null;
|
||||
}
|
||||
|
||||
private static object FindBlackboardVariableModel(string variableName)
|
||||
{
|
||||
UnityEngine.Object blackboardAsset = AssetDatabase.LoadAllAssetsAtPath(GraphAssetPath)
|
||||
.FirstOrDefault(asset => asset != null && asset.GetType().Name.Contains("BehaviorBlackboardAuthoringAsset", StringComparison.Ordinal));
|
||||
|
||||
if (blackboardAsset == null)
|
||||
return null;
|
||||
|
||||
PropertyInfo variablesProperty = blackboardAsset.GetType().GetProperty("Variables", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
FieldInfo variablesField = blackboardAsset.GetType().GetField("m_Variables", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
IEnumerable variables = variablesProperty?.GetValue(blackboardAsset) as IEnumerable ?? variablesField?.GetValue(blackboardAsset) as IEnumerable;
|
||||
if (variables == null)
|
||||
return null;
|
||||
|
||||
foreach (object variable in variables)
|
||||
{
|
||||
if (variable == null)
|
||||
continue;
|
||||
|
||||
PropertyInfo nameProperty = variable.GetType().GetProperty("Name", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
FieldInfo nameField = variable.GetType().GetField("Name", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
string name = nameProperty?.GetValue(variable) as string ?? nameField?.GetValue(variable) as string;
|
||||
if (name == variableName)
|
||||
return variable;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void LinkTarget(object node, object targetVariable)
|
||||
{
|
||||
LinkFieldToVariable(node, "Target", typeof(GameObject), targetVariable);
|
||||
}
|
||||
|
||||
private static void LinkFieldToVariable(object node, string fieldName, Type fieldType, object variableModel)
|
||||
{
|
||||
MethodInfo getVariableLinkMethod = node.GetType().GetMethod("GetVariableLink", BindingFlags.Instance | BindingFlags.Public);
|
||||
object variableLink = getVariableLinkMethod?.Invoke(node, new object[] { fieldName, fieldType });
|
||||
if (variableLink != null)
|
||||
{
|
||||
PropertyInfo blackboardVariableProperty = variableLink.GetType().GetProperty("BlackboardVariable", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
blackboardVariableProperty?.SetValue(variableLink, variableModel);
|
||||
}
|
||||
|
||||
object fieldModel = FindFieldModel(node, fieldName);
|
||||
if (fieldModel == null)
|
||||
return;
|
||||
|
||||
PropertyInfo linkedVariableProperty = fieldModel.GetType().GetProperty("LinkedVariable", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
FieldInfo linkedVariableField = fieldModel.GetType().GetField("LinkedVariable", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
|
||||
if (linkedVariableProperty != null && linkedVariableProperty.CanWrite)
|
||||
{
|
||||
linkedVariableProperty.SetValue(fieldModel, variableModel);
|
||||
}
|
||||
else
|
||||
{
|
||||
linkedVariableField?.SetValue(fieldModel, variableModel);
|
||||
}
|
||||
}
|
||||
|
||||
private static object FindFieldModel(object node, string fieldName)
|
||||
{
|
||||
FieldInfo fieldValuesField = node.GetType().GetField("m_FieldValues", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
PropertyInfo fieldValuesProperty = node.GetType().GetProperty("FieldValues", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
|
||||
IEnumerable fieldValues = fieldValuesField?.GetValue(node) as IEnumerable ?? fieldValuesProperty?.GetValue(node) as IEnumerable;
|
||||
if (fieldValues == null)
|
||||
return null;
|
||||
|
||||
foreach (object fieldModel in fieldValues)
|
||||
{
|
||||
if (fieldModel == null)
|
||||
continue;
|
||||
|
||||
PropertyInfo fieldNameProperty = fieldModel.GetType().GetProperty("FieldName", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
FieldInfo fieldNameField = fieldModel.GetType().GetField("FieldName", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
string currentFieldName = fieldNameProperty?.GetValue(fieldModel) as string ?? fieldNameField?.GetValue(fieldModel) as string;
|
||||
if (currentFieldName == fieldName)
|
||||
return fieldModel;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void SetStartRepeatFlags(object startNode, bool repeat, bool allowMultipleRepeatsPerTick)
|
||||
{
|
||||
Type startNodeType = startNode.GetType();
|
||||
FieldInfo repeatField = startNodeType.GetField("Repeat", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
FieldInfo allowField = startNodeType.GetField("AllowMultipleRepeatsPerTick", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
|
||||
repeatField?.SetValue(startNode, repeat);
|
||||
allowField?.SetValue(startNode, allowMultipleRepeatsPerTick);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 32898a7b79779e04d8dea1e3530df132
|
||||
520
Assets/_Game/Scripts/Enemy/BossCombatBehaviorContext.cs
Normal file
520
Assets/_Game/Scripts/Enemy/BossCombatBehaviorContext.cs
Normal file
@@ -0,0 +1,520 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using Colosseum.AI;
|
||||
using Colosseum.Combat;
|
||||
using Colosseum.Player;
|
||||
using Colosseum.Skills;
|
||||
|
||||
using Unity.Behavior;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
namespace Colosseum.Enemy
|
||||
{
|
||||
/// <summary>
|
||||
/// 보스 공통 전투 BT가 참조하는 전투 컨텍스트입니다.
|
||||
/// 패턴 슬롯, 거리 기준, 페이즈별 주기, 공통 타겟 판정 정보를 제공합니다.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
[RequireComponent(typeof(BossEnemy))]
|
||||
[RequireComponent(typeof(SkillController))]
|
||||
public abstract class BossCombatBehaviorContext : NetworkBehaviour
|
||||
{
|
||||
[Header("References")]
|
||||
[SerializeField] protected BossEnemy bossEnemy;
|
||||
[SerializeField] protected EnemyBase enemyBase;
|
||||
[SerializeField] protected SkillController skillController;
|
||||
[SerializeField] protected UnityEngine.AI.NavMeshAgent navMeshAgent;
|
||||
[SerializeField] protected BehaviorGraphAgent behaviorGraphAgent;
|
||||
|
||||
[Header("Pattern Data")]
|
||||
[Tooltip("기본 근접 압박 패턴")]
|
||||
[FormerlySerializedAs("mainPattern")]
|
||||
[SerializeField] protected BossPatternData primaryPattern;
|
||||
|
||||
[Tooltip("보조 근접 압박 패턴")]
|
||||
[FormerlySerializedAs("slamPattern")]
|
||||
[SerializeField] protected BossPatternData secondaryPattern;
|
||||
|
||||
[Tooltip("기동 또는 거리 징벌 패턴")]
|
||||
[FormerlySerializedAs("leapPattern")]
|
||||
[SerializeField] protected BossPatternData mobilityPattern;
|
||||
|
||||
[Tooltip("특정 상황에서 우선 발동하는 징벌 패턴")]
|
||||
[FormerlySerializedAs("downPunishPattern")]
|
||||
[SerializeField] protected BossPatternData punishPattern;
|
||||
|
||||
[Header("Phase Thresholds")]
|
||||
[Tooltip("2페이즈 진입 체력 비율")]
|
||||
[Range(0f, 1f)] [SerializeField] protected float phase2HealthThreshold = 0.75f;
|
||||
|
||||
[Tooltip("3페이즈 진입 체력 비율")]
|
||||
[Range(0f, 1f)] [SerializeField] protected float phase3HealthThreshold = 0.4f;
|
||||
|
||||
[Header("Targeting")]
|
||||
[Tooltip("타겟 재탐색 주기")]
|
||||
[FormerlySerializedAs("targetRefreshInterval")]
|
||||
[Min(0.05f)] [SerializeField] protected float primaryTargetRefreshInterval = 0.2f;
|
||||
|
||||
[Tooltip("기동 패턴을 고려하기 시작하는 거리")]
|
||||
[FormerlySerializedAs("leapDistanceThreshold")]
|
||||
[Min(0f)] [SerializeField] protected float mobilityTriggerDistance = 8f;
|
||||
|
||||
[Tooltip("징벌 패턴을 고려할 최대 반경")]
|
||||
[FormerlySerializedAs("downPunishSearchRadius")]
|
||||
[Min(0f)] [SerializeField] protected float punishSearchRadius = 6f;
|
||||
|
||||
[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("Behavior")]
|
||||
[Tooltip("전용 컨텍스트 사용 시 기존 BehaviorGraph를 비활성화할지 여부")]
|
||||
[SerializeField] protected bool disableBehaviorGraph = true;
|
||||
|
||||
[Tooltip("디버그 로그 출력 여부")]
|
||||
[SerializeField] protected bool debugMode = false;
|
||||
|
||||
protected readonly Dictionary<BossPatternData, float> patternCooldownTracker = new Dictionary<BossPatternData, float>();
|
||||
|
||||
protected Coroutine activePatternCoroutine;
|
||||
protected GameObject currentTarget;
|
||||
protected float nextTargetRefreshTime;
|
||||
protected int meleePatternCounter;
|
||||
|
||||
/// <summary>
|
||||
/// 전용 컨텍스트 사용 시 BehaviorGraph를 비활성화할지 여부
|
||||
/// </summary>
|
||||
public bool DisableBehaviorGraph => disableBehaviorGraph;
|
||||
|
||||
/// <summary>
|
||||
/// 기동 패턴을 고려하는 최소 거리
|
||||
/// </summary>
|
||||
public float MobilityTriggerDistance => mobilityTriggerDistance;
|
||||
|
||||
/// <summary>
|
||||
/// 징벌 패턴을 고려하는 최대 반경
|
||||
/// </summary>
|
||||
public float PunishSearchRadius => punishSearchRadius;
|
||||
|
||||
/// <summary>
|
||||
/// 디버그 로그 출력 여부
|
||||
/// </summary>
|
||||
public bool DebugModeEnabled => debugMode;
|
||||
|
||||
/// <summary>
|
||||
/// 현재 보스 패턴 페이즈
|
||||
/// </summary>
|
||||
public int CurrentPatternPhase
|
||||
{
|
||||
get
|
||||
{
|
||||
float healthRatio = bossEnemy != null && bossEnemy.MaxHealth > 0f
|
||||
? bossEnemy.CurrentHealth / bossEnemy.MaxHealth
|
||||
: 1f;
|
||||
|
||||
if (healthRatio <= phase3HealthThreshold)
|
||||
return 3;
|
||||
|
||||
if (healthRatio <= phase2HealthThreshold)
|
||||
return 2;
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void Awake()
|
||||
{
|
||||
ResolveReferences();
|
||||
}
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
ResolveReferences();
|
||||
|
||||
if (!IsServer)
|
||||
enabled = false;
|
||||
}
|
||||
|
||||
protected virtual void Update()
|
||||
{
|
||||
if (!IsServer)
|
||||
return;
|
||||
|
||||
ResolveReferences();
|
||||
if (bossEnemy == null || enemyBase == null || skillController == null)
|
||||
return;
|
||||
|
||||
if (bossEnemy.IsDead || bossEnemy.IsTransitioning)
|
||||
return;
|
||||
|
||||
if (!disableBehaviorGraph)
|
||||
return;
|
||||
|
||||
RefreshTargetIfNeeded();
|
||||
UpdateMovement();
|
||||
|
||||
if (skillController.IsPlayingAnimation)
|
||||
return;
|
||||
|
||||
if (TryStartMobilityPattern())
|
||||
return;
|
||||
|
||||
TryStartPrimaryLoopPattern();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 역할의 패턴 데이터를 반환합니다.
|
||||
/// </summary>
|
||||
public BossPatternData GetPattern(BossCombatPatternRole role)
|
||||
{
|
||||
return role switch
|
||||
{
|
||||
BossCombatPatternRole.Primary => primaryPattern,
|
||||
BossCombatPatternRole.Secondary => secondaryPattern,
|
||||
BossCombatPatternRole.Mobility => mobilityPattern,
|
||||
BossCombatPatternRole.Punish => punishPattern,
|
||||
_ => 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)
|
||||
{
|
||||
if (!role.IsMeleeRole())
|
||||
return;
|
||||
|
||||
meleePatternCounter++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 살아 있는 적대 대상인지 확인합니다.
|
||||
/// </summary>
|
||||
public bool IsValidHostileTarget(GameObject candidate)
|
||||
{
|
||||
if (candidate == null || !candidate.activeInHierarchy)
|
||||
return false;
|
||||
|
||||
if (Team.IsSameTeam(gameObject, candidate))
|
||||
return false;
|
||||
|
||||
IDamageable damageable = candidate.GetComponent<IDamageable>();
|
||||
return damageable == null || !damageable.IsDead;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 기동 패턴 대상으로 유효한지 확인합니다.
|
||||
/// </summary>
|
||||
public bool IsValidMobilityTarget(GameObject candidate)
|
||||
{
|
||||
if (!IsValidHostileTarget(candidate))
|
||||
return false;
|
||||
|
||||
float maxDistance = enemyBase != null && enemyBase.Data != null ? enemyBase.Data.AggroRange : 20f;
|
||||
float distance = Vector3.Distance(transform.position, candidate.transform.position);
|
||||
return distance >= mobilityTriggerDistance && distance <= maxDistance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 기동 패턴 대상으로 사용할 수 있는 가장 먼 유효 타겟을 찾습니다.
|
||||
/// </summary>
|
||||
public GameObject FindMobilityTarget()
|
||||
{
|
||||
GameObject[] candidates = GameObject.FindGameObjectsWithTag("Player");
|
||||
GameObject farthestTarget = null;
|
||||
float bestDistance = mobilityTriggerDistance;
|
||||
float maxDistance = enemyBase != null && enemyBase.Data != null ? enemyBase.Data.AggroRange : 20f;
|
||||
|
||||
for (int i = 0; i < candidates.Length; i++)
|
||||
{
|
||||
GameObject candidate = candidates[i];
|
||||
if (!IsValidMobilityTarget(candidate))
|
||||
continue;
|
||||
|
||||
float distance = Vector3.Distance(transform.position, candidate.transform.position);
|
||||
if (distance > maxDistance || distance <= bestDistance)
|
||||
continue;
|
||||
|
||||
bestDistance = distance;
|
||||
farthestTarget = candidate;
|
||||
}
|
||||
|
||||
return farthestTarget;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 가장 가까운 생존 플레이어를 찾습니다.
|
||||
/// </summary>
|
||||
public GameObject FindNearestLivingTarget()
|
||||
{
|
||||
PlayerNetworkController[] players = FindObjectsByType<PlayerNetworkController>(FindObjectsSortMode.None);
|
||||
|
||||
GameObject nearestTarget = null;
|
||||
float nearestDistance = float.MaxValue;
|
||||
float aggroRange = enemyBase != null && enemyBase.Data != null ? enemyBase.Data.AggroRange : Mathf.Infinity;
|
||||
|
||||
for (int i = 0; i < players.Length; i++)
|
||||
{
|
||||
PlayerNetworkController player = players[i];
|
||||
if (player == null || player.IsDead || !player.gameObject.activeInHierarchy)
|
||||
continue;
|
||||
|
||||
GameObject candidate = player.gameObject;
|
||||
if (!IsValidHostileTarget(candidate))
|
||||
continue;
|
||||
|
||||
float distance = Vector3.Distance(transform.position, candidate.transform.position);
|
||||
if (distance > aggroRange || distance >= nearestDistance)
|
||||
continue;
|
||||
|
||||
nearestDistance = distance;
|
||||
nearestTarget = candidate;
|
||||
}
|
||||
|
||||
return nearestTarget;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 로그를 출력합니다.
|
||||
/// </summary>
|
||||
public void LogDebug(string source, string message)
|
||||
{
|
||||
if (debugMode)
|
||||
Debug.Log($"[{source}] {message}");
|
||||
}
|
||||
|
||||
protected virtual bool TryStartPrimaryLoopPattern()
|
||||
{
|
||||
if (currentTarget == null)
|
||||
return false;
|
||||
|
||||
float distanceToTarget = Vector3.Distance(transform.position, currentTarget.transform.position);
|
||||
float attackRange = enemyBase != null && enemyBase.Data != null ? enemyBase.Data.AttackRange : 2f;
|
||||
if (distanceToTarget > attackRange + 0.25f)
|
||||
return false;
|
||||
|
||||
BossPatternData selectedPattern = SelectPrimaryLoopPattern();
|
||||
if (selectedPattern == null)
|
||||
return false;
|
||||
|
||||
StartPattern(selectedPattern, currentTarget);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected virtual bool TryStartMobilityPattern()
|
||||
{
|
||||
BossPatternData pattern = GetPattern(BossCombatPatternRole.Mobility);
|
||||
if (!IsPatternReady(pattern))
|
||||
return false;
|
||||
|
||||
GameObject target = FindMobilityTarget();
|
||||
if (target == null)
|
||||
return false;
|
||||
|
||||
currentTarget = target;
|
||||
StartPattern(pattern, target);
|
||||
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;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected virtual void StartPattern(BossPatternData pattern, GameObject target)
|
||||
{
|
||||
if (pattern == null || activePatternCoroutine != null)
|
||||
return;
|
||||
|
||||
LogDebug(GetType().Name, $"패턴 시작: {pattern.PatternName} / Target={(target != null ? target.name : "None")} / Phase={CurrentPatternPhase}");
|
||||
activePatternCoroutine = StartCoroutine(RunPatternCoroutine(pattern, target));
|
||||
}
|
||||
|
||||
protected virtual IEnumerator RunPatternCoroutine(BossPatternData pattern, GameObject target)
|
||||
{
|
||||
StopMovement();
|
||||
bool completed = true;
|
||||
|
||||
for (int i = 0; i < pattern.Steps.Count; i++)
|
||||
{
|
||||
PatternStep step = pattern.Steps[i];
|
||||
if (step.Type == PatternStepType.Wait)
|
||||
{
|
||||
yield return new WaitForSeconds(step.Duration);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (step.Skill == null)
|
||||
{
|
||||
completed = false;
|
||||
Debug.LogWarning($"[{GetType().Name}] 패턴 스텝 스킬이 비어 있습니다. Pattern={pattern.PatternName}, Index={i}");
|
||||
break;
|
||||
}
|
||||
|
||||
if (step.Skill.JumpToTarget && target != null)
|
||||
{
|
||||
enemyBase?.SetJumpTarget(target.transform.position);
|
||||
}
|
||||
|
||||
if (!skillController.ExecuteSkill(step.Skill))
|
||||
{
|
||||
completed = false;
|
||||
LogDebug(GetType().Name, $"스킬 실행 실패: {step.Skill.SkillName}");
|
||||
break;
|
||||
}
|
||||
|
||||
yield return new WaitUntil(() => skillController == null || !skillController.IsPlayingAnimation || bossEnemy == null || bossEnemy.IsDead);
|
||||
if (bossEnemy == null || bossEnemy.IsDead)
|
||||
break;
|
||||
}
|
||||
|
||||
if (completed)
|
||||
{
|
||||
patternCooldownTracker[pattern] = Time.time + pattern.Cooldown;
|
||||
}
|
||||
|
||||
activePatternCoroutine = null;
|
||||
currentTarget = target;
|
||||
}
|
||||
|
||||
protected bool IsPatternReady(BossPatternData pattern)
|
||||
{
|
||||
if (pattern == null || pattern.Steps == null || pattern.Steps.Count == 0)
|
||||
return false;
|
||||
|
||||
if (!patternCooldownTracker.TryGetValue(pattern, out float readyTime))
|
||||
return true;
|
||||
|
||||
return Time.time >= readyTime;
|
||||
}
|
||||
|
||||
protected virtual void RefreshTargetIfNeeded()
|
||||
{
|
||||
if (Time.time < nextTargetRefreshTime)
|
||||
return;
|
||||
|
||||
nextTargetRefreshTime = Time.time + primaryTargetRefreshInterval;
|
||||
GameObject highestThreatTarget = enemyBase.GetHighestThreatTarget(currentTarget, null, enemyBase.Data != null ? enemyBase.Data.AggroRange : Mathf.Infinity);
|
||||
currentTarget = highestThreatTarget != null ? highestThreatTarget : FindNearestLivingTarget();
|
||||
}
|
||||
|
||||
protected virtual void UpdateMovement()
|
||||
{
|
||||
if (navMeshAgent == null || !navMeshAgent.enabled)
|
||||
return;
|
||||
|
||||
if (skillController != null && skillController.IsPlayingAnimation)
|
||||
{
|
||||
StopMovement();
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentTarget == null)
|
||||
{
|
||||
StopMovement();
|
||||
return;
|
||||
}
|
||||
|
||||
float attackRange = enemyBase != null && enemyBase.Data != null ? enemyBase.Data.AttackRange : 2f;
|
||||
float distanceToTarget = Vector3.Distance(transform.position, currentTarget.transform.position);
|
||||
if (distanceToTarget <= attackRange)
|
||||
{
|
||||
StopMovement();
|
||||
return;
|
||||
}
|
||||
|
||||
navMeshAgent.isStopped = false;
|
||||
navMeshAgent.stoppingDistance = attackRange;
|
||||
navMeshAgent.SetDestination(currentTarget.transform.position);
|
||||
}
|
||||
|
||||
protected void StopMovement()
|
||||
{
|
||||
if (navMeshAgent == null || !navMeshAgent.enabled)
|
||||
return;
|
||||
|
||||
navMeshAgent.isStopped = true;
|
||||
navMeshAgent.ResetPath();
|
||||
}
|
||||
|
||||
protected virtual void ResolveReferences()
|
||||
{
|
||||
if (bossEnemy == null)
|
||||
bossEnemy = GetComponent<BossEnemy>();
|
||||
|
||||
if (enemyBase == null)
|
||||
enemyBase = GetComponent<EnemyBase>();
|
||||
|
||||
if (skillController == null)
|
||||
skillController = GetComponent<SkillController>();
|
||||
|
||||
if (navMeshAgent == null)
|
||||
navMeshAgent = GetComponent<UnityEngine.AI.NavMeshAgent>();
|
||||
|
||||
if (behaviorGraphAgent == null)
|
||||
behaviorGraphAgent = GetComponent<BehaviorGraphAgent>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3568a8ab7f49c5242a8f7c4bc655b68d
|
||||
27
Assets/_Game/Scripts/Enemy/BossCombatPatternRole.cs
Normal file
27
Assets/_Game/Scripts/Enemy/BossCombatPatternRole.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
namespace Colosseum.Enemy
|
||||
{
|
||||
/// <summary>
|
||||
/// 보스 전투 BT에서 사용하는 공통 패턴 역할 구분값입니다.
|
||||
/// </summary>
|
||||
public enum BossCombatPatternRole
|
||||
{
|
||||
Primary = 0,
|
||||
Secondary = 1,
|
||||
Mobility = 2,
|
||||
Punish = 3,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 공통 패턴 역할 보조 확장 메서드입니다.
|
||||
/// </summary>
|
||||
public static class BossCombatPatternRoleExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 현재 역할이 근접 순환 패턴인지 반환합니다.
|
||||
/// </summary>
|
||||
public static bool IsMeleeRole(this BossCombatPatternRole role)
|
||||
{
|
||||
return role == BossCombatPatternRole.Primary || role == BossCombatPatternRole.Secondary;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/_Game/Scripts/Enemy/BossCombatPatternRole.cs.meta
Normal file
2
Assets/_Game/Scripts/Enemy/BossCombatPatternRole.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ee34c0af35cdfbd45baf0a6b9dcc2dd9
|
||||
@@ -33,7 +33,7 @@ namespace Colosseum.Enemy
|
||||
|
||||
// 컴포넌트
|
||||
private BehaviorGraphAgent behaviorAgent;
|
||||
private DrogPatternController drogPatternController;
|
||||
private BossCombatBehaviorContext combatBehaviorContext;
|
||||
|
||||
// 페이즈 상태
|
||||
private int currentPhaseIndex = 0;
|
||||
@@ -78,10 +78,10 @@ namespace Colosseum.Enemy
|
||||
behaviorAgent = gameObject.AddComponent<BehaviorGraphAgent>();
|
||||
}
|
||||
|
||||
drogPatternController = GetComponent<DrogPatternController>();
|
||||
combatBehaviorContext = GetComponent<BossCombatBehaviorContext>();
|
||||
|
||||
// 초기 AI 설정
|
||||
if (IsServer && drogPatternController != null && drogPatternController.DisableBehaviorGraph)
|
||||
if (IsServer && combatBehaviorContext != null && combatBehaviorContext.DisableBehaviorGraph)
|
||||
{
|
||||
behaviorAgent.enabled = false;
|
||||
behaviorAgent.Graph = null;
|
||||
|
||||
@@ -1,408 +1,10 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using UnityEngine;
|
||||
using Unity.Netcode;
|
||||
|
||||
using Colosseum.AI;
|
||||
using Colosseum.Player;
|
||||
using Colosseum.Skills;
|
||||
|
||||
namespace Colosseum.Enemy
|
||||
{
|
||||
/// <summary>
|
||||
/// 드로그 전용 패턴 선택 컨트롤러입니다.
|
||||
/// 기본 루프, 도약, 다운 추가타 같은 고우선 패턴을 직접 선택합니다.
|
||||
/// 드로그가 사용하는 보스 전투 컨텍스트 컴포넌트입니다.
|
||||
/// 현재는 공통 보스 전투 BT 프레임워크를 그대로 사용합니다.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
[RequireComponent(typeof(BossEnemy))]
|
||||
[RequireComponent(typeof(SkillController))]
|
||||
public class DrogPatternController : NetworkBehaviour
|
||||
public class DrogPatternController : BossCombatBehaviorContext
|
||||
{
|
||||
[Header("References")]
|
||||
[SerializeField] private BossEnemy bossEnemy;
|
||||
[SerializeField] private EnemyBase enemyBase;
|
||||
[SerializeField] private SkillController skillController;
|
||||
[SerializeField] private UnityEngine.AI.NavMeshAgent navMeshAgent;
|
||||
|
||||
[Header("Pattern Data")]
|
||||
[Tooltip("기본 근접 압박 패턴")]
|
||||
[SerializeField] private BossPatternData mainPattern;
|
||||
|
||||
[Tooltip("먼 대상 징벌용 도약 패턴")]
|
||||
[SerializeField] private BossPatternData leapPattern;
|
||||
|
||||
[Tooltip("다운 대상이 있을 때 우선 발동하는 광역 추가타 패턴")]
|
||||
[SerializeField] private BossPatternData downPunishPattern;
|
||||
|
||||
[Header("Phase Thresholds")]
|
||||
[Tooltip("2페이즈 진입 체력 비율")]
|
||||
[Range(0f, 1f)] [SerializeField] private float phase2HealthThreshold = 0.75f;
|
||||
|
||||
[Tooltip("3페이즈 진입 체력 비율")]
|
||||
[Range(0f, 1f)] [SerializeField] private float phase3HealthThreshold = 0.4f;
|
||||
|
||||
[Header("Targeting")]
|
||||
[Tooltip("타겟 재탐색 주기")]
|
||||
[Min(0.05f)] [SerializeField] private float targetRefreshInterval = 0.2f;
|
||||
|
||||
[Tooltip("도약 패턴을 고려하기 시작하는 거리")]
|
||||
[Min(0f)] [SerializeField] private float leapDistanceThreshold = 8f;
|
||||
|
||||
[Tooltip("다운 추가타를 고려할 최대 반경")]
|
||||
[Min(0f)] [SerializeField] private float downPunishSearchRadius = 6f;
|
||||
|
||||
[Header("Behavior")]
|
||||
[Tooltip("드로그 전용 컨트롤러 사용 시 기존 BehaviorGraph를 비활성화할지 여부")]
|
||||
[SerializeField] private bool disableBehaviorGraph = true;
|
||||
|
||||
[Tooltip("디버그 로그 출력 여부")]
|
||||
[SerializeField] private bool debugMode = false;
|
||||
|
||||
private readonly Dictionary<BossPatternData, float> patternCooldownTracker = new Dictionary<BossPatternData, float>();
|
||||
|
||||
private Coroutine activePatternCoroutine;
|
||||
private GameObject currentTarget;
|
||||
private float nextTargetRefreshTime;
|
||||
|
||||
/// <summary>
|
||||
/// 드로그 컨트롤러 사용 시 BehaviorGraph를 비활성화할지 여부
|
||||
/// </summary>
|
||||
public bool DisableBehaviorGraph => disableBehaviorGraph;
|
||||
|
||||
/// <summary>
|
||||
/// 현재 전용 패턴 실행 중인지 여부
|
||||
/// </summary>
|
||||
public bool IsExecutingPattern => activePatternCoroutine != null;
|
||||
|
||||
/// <summary>
|
||||
/// 현재 드로그 패턴 페이즈
|
||||
/// </summary>
|
||||
public int CurrentPatternPhase
|
||||
{
|
||||
get
|
||||
{
|
||||
float healthRatio = bossEnemy != null && bossEnemy.MaxHealth > 0f
|
||||
? bossEnemy.CurrentHealth / bossEnemy.MaxHealth
|
||||
: 1f;
|
||||
|
||||
if (healthRatio <= phase3HealthThreshold)
|
||||
return 3;
|
||||
|
||||
if (healthRatio <= phase2HealthThreshold)
|
||||
return 2;
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
ResolveReferences();
|
||||
}
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
ResolveReferences();
|
||||
|
||||
if (!IsServer)
|
||||
{
|
||||
enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!IsServer)
|
||||
return;
|
||||
|
||||
ResolveReferences();
|
||||
|
||||
if (bossEnemy == null || enemyBase == null || skillController == null)
|
||||
return;
|
||||
|
||||
if (bossEnemy.IsDead || bossEnemy.IsTransitioning)
|
||||
return;
|
||||
|
||||
RefreshTargetIfNeeded();
|
||||
UpdateMovement();
|
||||
|
||||
if (activePatternCoroutine != null || skillController.IsPlayingAnimation)
|
||||
return;
|
||||
|
||||
if (TryStartDownPunishPattern())
|
||||
return;
|
||||
|
||||
TryStartMainPattern();
|
||||
}
|
||||
|
||||
private bool TryStartDownPunishPattern()
|
||||
{
|
||||
if (!IsPatternReady(downPunishPattern))
|
||||
return false;
|
||||
|
||||
GameObject downedTarget = FindNearestDownedTarget();
|
||||
if (downedTarget == null)
|
||||
return false;
|
||||
|
||||
StartPattern(downPunishPattern, downedTarget);
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryStartMainPattern()
|
||||
{
|
||||
if (currentTarget == null)
|
||||
return false;
|
||||
|
||||
float distanceToTarget = Vector3.Distance(transform.position, currentTarget.transform.position);
|
||||
|
||||
if (distanceToTarget >= leapDistanceThreshold && IsPatternReady(leapPattern))
|
||||
{
|
||||
StartPattern(leapPattern, currentTarget);
|
||||
return true;
|
||||
}
|
||||
|
||||
float attackRange = enemyBase.Data != null ? enemyBase.Data.AttackRange : 2f;
|
||||
if (distanceToTarget <= attackRange + 0.25f && IsPatternReady(mainPattern))
|
||||
{
|
||||
StartPattern(mainPattern, currentTarget);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void StartPattern(BossPatternData pattern, GameObject target)
|
||||
{
|
||||
if (pattern == null || activePatternCoroutine != null)
|
||||
return;
|
||||
|
||||
if (debugMode)
|
||||
{
|
||||
string targetName = target != null ? target.name : "None";
|
||||
Debug.Log($"[DrogPattern] 패턴 시작: {pattern.PatternName} / Target={targetName} / Phase={CurrentPatternPhase}");
|
||||
}
|
||||
|
||||
activePatternCoroutine = StartCoroutine(RunPatternCoroutine(pattern, target));
|
||||
}
|
||||
|
||||
private IEnumerator RunPatternCoroutine(BossPatternData pattern, GameObject target)
|
||||
{
|
||||
StopMovement();
|
||||
|
||||
bool completed = true;
|
||||
BossPatternData chainedPattern = null;
|
||||
GameObject chainedTarget = null;
|
||||
|
||||
for (int i = 0; i < pattern.Steps.Count; i++)
|
||||
{
|
||||
PatternStep step = pattern.Steps[i];
|
||||
|
||||
if (step.Type == PatternStepType.Wait)
|
||||
{
|
||||
yield return new WaitForSeconds(step.Duration);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (step.Skill == null)
|
||||
{
|
||||
completed = false;
|
||||
Debug.LogWarning($"[DrogPattern] 패턴 스텝 스킬이 비어 있습니다. Pattern={pattern.PatternName}, Index={i}");
|
||||
break;
|
||||
}
|
||||
|
||||
if (step.Skill.JumpToTarget && target != null)
|
||||
{
|
||||
enemyBase?.SetJumpTarget(target.transform.position);
|
||||
}
|
||||
|
||||
if (!skillController.ExecuteSkill(step.Skill))
|
||||
{
|
||||
completed = false;
|
||||
if (debugMode)
|
||||
{
|
||||
Debug.LogWarning($"[DrogPattern] 스킬 실행 실패: {step.Skill.SkillName}");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
yield return new WaitUntil(() => skillController == null || !skillController.IsPlayingAnimation || bossEnemy == null || bossEnemy.IsDead);
|
||||
|
||||
if (bossEnemy == null || bossEnemy.IsDead)
|
||||
{
|
||||
completed = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (pattern != downPunishPattern && TryPrepareDownPunishChain(out chainedTarget))
|
||||
{
|
||||
chainedPattern = downPunishPattern;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (completed)
|
||||
{
|
||||
patternCooldownTracker[pattern] = Time.time + pattern.Cooldown;
|
||||
}
|
||||
|
||||
activePatternCoroutine = null;
|
||||
|
||||
if (chainedPattern != null && chainedTarget != null && bossEnemy != null && !bossEnemy.IsDead)
|
||||
{
|
||||
StartPattern(chainedPattern, chainedTarget);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsPatternReady(BossPatternData pattern)
|
||||
{
|
||||
if (pattern == null || pattern.Steps == null || pattern.Steps.Count == 0)
|
||||
return false;
|
||||
|
||||
if (!patternCooldownTracker.TryGetValue(pattern, out float readyTime))
|
||||
return true;
|
||||
|
||||
return Time.time >= readyTime;
|
||||
}
|
||||
|
||||
private void RefreshTargetIfNeeded()
|
||||
{
|
||||
if (Time.time < nextTargetRefreshTime)
|
||||
return;
|
||||
|
||||
nextTargetRefreshTime = Time.time + targetRefreshInterval;
|
||||
|
||||
GameObject highestThreatTarget = enemyBase.GetHighestThreatTarget(currentTarget, null, enemyBase.Data != null ? enemyBase.Data.AggroRange : Mathf.Infinity);
|
||||
currentTarget = highestThreatTarget != null ? highestThreatTarget : FindNearestLivingPlayer();
|
||||
}
|
||||
|
||||
private void UpdateMovement()
|
||||
{
|
||||
if (navMeshAgent == null || !navMeshAgent.enabled)
|
||||
return;
|
||||
|
||||
if (activePatternCoroutine != null || (skillController != null && skillController.IsPlayingAnimation))
|
||||
{
|
||||
StopMovement();
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentTarget == null)
|
||||
{
|
||||
StopMovement();
|
||||
return;
|
||||
}
|
||||
|
||||
float attackRange = enemyBase.Data != null ? enemyBase.Data.AttackRange : 2f;
|
||||
float distanceToTarget = Vector3.Distance(transform.position, currentTarget.transform.position);
|
||||
if (distanceToTarget <= attackRange)
|
||||
{
|
||||
StopMovement();
|
||||
return;
|
||||
}
|
||||
|
||||
navMeshAgent.isStopped = false;
|
||||
navMeshAgent.SetDestination(currentTarget.transform.position);
|
||||
}
|
||||
|
||||
private void StopMovement()
|
||||
{
|
||||
if (navMeshAgent == null || !navMeshAgent.enabled)
|
||||
return;
|
||||
|
||||
navMeshAgent.isStopped = true;
|
||||
navMeshAgent.ResetPath();
|
||||
}
|
||||
|
||||
private GameObject FindNearestDownedTarget()
|
||||
{
|
||||
HitReactionController[] hitReactionControllers = FindObjectsByType<HitReactionController>(FindObjectsSortMode.None);
|
||||
|
||||
GameObject bestTarget = null;
|
||||
float bestDistance = 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 || Team.IsSameTeam(gameObject, candidate))
|
||||
continue;
|
||||
|
||||
float distance = Vector3.Distance(transform.position, candidate.transform.position);
|
||||
if (distance > downPunishSearchRadius || distance >= bestDistance)
|
||||
continue;
|
||||
|
||||
bestDistance = distance;
|
||||
bestTarget = candidate;
|
||||
}
|
||||
|
||||
return bestTarget;
|
||||
}
|
||||
|
||||
private bool TryPrepareDownPunishChain(out GameObject downedTarget)
|
||||
{
|
||||
downedTarget = null;
|
||||
|
||||
if (!IsPatternReady(downPunishPattern))
|
||||
return false;
|
||||
|
||||
downedTarget = FindNearestDownedTarget();
|
||||
if (downedTarget == null)
|
||||
return false;
|
||||
|
||||
if (debugMode)
|
||||
{
|
||||
Debug.Log($"[DrogPattern] 다운 대상 감지, 다운 추가타 연계 준비: {downedTarget.name}");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private GameObject FindNearestLivingPlayer()
|
||||
{
|
||||
PlayerNetworkController[] players = FindObjectsByType<PlayerNetworkController>(FindObjectsSortMode.None);
|
||||
|
||||
GameObject bestTarget = null;
|
||||
float bestDistance = float.MaxValue;
|
||||
float aggroRange = enemyBase.Data != null ? enemyBase.Data.AggroRange : Mathf.Infinity;
|
||||
|
||||
for (int i = 0; i < players.Length; i++)
|
||||
{
|
||||
PlayerNetworkController player = players[i];
|
||||
if (player == null || player.IsDead || !player.gameObject.activeInHierarchy)
|
||||
continue;
|
||||
|
||||
GameObject candidate = player.gameObject;
|
||||
if (Team.IsSameTeam(gameObject, candidate))
|
||||
continue;
|
||||
|
||||
float distance = Vector3.Distance(transform.position, candidate.transform.position);
|
||||
if (distance > aggroRange || distance >= bestDistance)
|
||||
continue;
|
||||
|
||||
bestDistance = distance;
|
||||
bestTarget = candidate;
|
||||
}
|
||||
|
||||
return bestTarget;
|
||||
}
|
||||
|
||||
private void ResolveReferences()
|
||||
{
|
||||
if (bossEnemy == null)
|
||||
bossEnemy = GetComponent<BossEnemy>();
|
||||
|
||||
if (enemyBase == null)
|
||||
enemyBase = GetComponent<EnemyBase>();
|
||||
|
||||
if (skillController == null)
|
||||
skillController = GetComponent<SkillController>();
|
||||
|
||||
if (navMeshAgent == null)
|
||||
navMeshAgent = GetComponent<UnityEngine.AI.NavMeshAgent>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
13
Tools/Reload-Obsidian.ps1
Normal file
13
Tools/Reload-Obsidian.ps1
Normal file
@@ -0,0 +1,13 @@
|
||||
param(
|
||||
[string]$VaultName = "Obsidian Vault"
|
||||
)
|
||||
|
||||
$obsidianPath = "C:\Program Files\Obsidian\Obsidian.exe"
|
||||
|
||||
if (-not (Test-Path $obsidianPath))
|
||||
{
|
||||
Write-Error "Obsidian.exe를 찾을 수 없습니다: $obsidianPath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
& $obsidianPath reload vault=$VaultName
|
||||
Reference in New Issue
Block a user