feat: 드로그 BT 및 전투 패턴 재구성
- 드로그 BT를 페이즈 전환, 부활 트리거, 가중치 근접 패턴 중심으로 재구성 - 땅 울리기 및 콤보-기본기1_3 패턴/스킬/이펙트를 추가하고 기존 평타 파생 자산을 정리 - 드로그 행동 검증용 PlayMode/Editor 테스트와 관련 런타임 상태 추적을 추가
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
67379
Assets/_Game/Animations/Anim_Drog_땅 울리기_0.anim
Normal file
67379
Assets/_Game/Animations/Anim_Drog_땅 울리기_0.anim
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1c76b3d381d4b38e5811352df87fa9e9
|
||||
guid: 189617e6b9348dbb287d409c27866040
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 7400000
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,53 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!74 &7400000
|
||||
AnimationClip:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: "Anim_Drog_\uC9D1\uD589_\uC5F0\uD0C01_0"
|
||||
serializedVersion: 7
|
||||
m_Legacy: 0
|
||||
m_Compressed: 0
|
||||
m_UseHighQualityCurve: 1
|
||||
m_RotationCurves: []
|
||||
m_CompressedRotationCurves: []
|
||||
m_EulerCurves: []
|
||||
m_PositionCurves: []
|
||||
m_ScaleCurves: []
|
||||
m_FloatCurves: []
|
||||
m_PPtrCurves: []
|
||||
m_SampleRate: 60
|
||||
m_WrapMode: 0
|
||||
m_Bounds:
|
||||
m_Center: {x: 0, y: 0, z: 0}
|
||||
m_Extent: {x: 0, y: 0, z: 0}
|
||||
m_ClipBindingConstant:
|
||||
genericBindings: []
|
||||
pptrCurveMapping: []
|
||||
m_AnimationClipSettings:
|
||||
serializedVersion: 2
|
||||
m_AdditiveReferencePoseClip: {fileID: 0}
|
||||
m_AdditiveReferencePoseTime: 0
|
||||
m_StartTime: 0
|
||||
m_StopTime: 1
|
||||
m_OrientationOffsetY: 0
|
||||
m_Level: 0
|
||||
m_CycleOffset: 0
|
||||
m_HasAdditiveReferencePose: 0
|
||||
m_LoopTime: 0
|
||||
m_LoopBlend: 0
|
||||
m_LoopBlendOrientation: 0
|
||||
m_LoopBlendPositionY: 0
|
||||
m_LoopBlendPositionXZ: 0
|
||||
m_KeepOriginalOrientation: 0
|
||||
m_KeepOriginalPositionY: 1
|
||||
m_KeepOriginalPositionXZ: 0
|
||||
m_HeightFromFeet: 0
|
||||
m_Mirror: 0
|
||||
m_EditorCurves: []
|
||||
m_EulerEditorCurves: []
|
||||
m_HasGenericRootTransform: 0
|
||||
m_HasMotionFloatCurves: 0
|
||||
m_Events: []
|
||||
@@ -1,53 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!74 &7400000
|
||||
AnimationClip:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: "Anim_Drog_\uC9D1\uD589_\uC5F0\uD0C02_0"
|
||||
serializedVersion: 7
|
||||
m_Legacy: 0
|
||||
m_Compressed: 0
|
||||
m_UseHighQualityCurve: 1
|
||||
m_RotationCurves: []
|
||||
m_CompressedRotationCurves: []
|
||||
m_EulerCurves: []
|
||||
m_PositionCurves: []
|
||||
m_ScaleCurves: []
|
||||
m_FloatCurves: []
|
||||
m_PPtrCurves: []
|
||||
m_SampleRate: 60
|
||||
m_WrapMode: 0
|
||||
m_Bounds:
|
||||
m_Center: {x: 0, y: 0, z: 0}
|
||||
m_Extent: {x: 0, y: 0, z: 0}
|
||||
m_ClipBindingConstant:
|
||||
genericBindings: []
|
||||
pptrCurveMapping: []
|
||||
m_AnimationClipSettings:
|
||||
serializedVersion: 2
|
||||
m_AdditiveReferencePoseClip: {fileID: 0}
|
||||
m_AdditiveReferencePoseTime: 0
|
||||
m_StartTime: 0
|
||||
m_StopTime: 1
|
||||
m_OrientationOffsetY: 0
|
||||
m_Level: 0
|
||||
m_CycleOffset: 0
|
||||
m_HasAdditiveReferencePose: 0
|
||||
m_LoopTime: 0
|
||||
m_LoopBlend: 0
|
||||
m_LoopBlendOrientation: 0
|
||||
m_LoopBlendPositionY: 0
|
||||
m_LoopBlendPositionXZ: 0
|
||||
m_KeepOriginalOrientation: 0
|
||||
m_KeepOriginalPositionY: 1
|
||||
m_KeepOriginalPositionXZ: 0
|
||||
m_HeightFromFeet: 0
|
||||
m_Mirror: 0
|
||||
m_EditorCurves: []
|
||||
m_EulerEditorCurves: []
|
||||
m_HasGenericRootTransform: 0
|
||||
m_HasMotionFloatCurves: 0
|
||||
m_Events: []
|
||||
@@ -1,53 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!74 &7400000
|
||||
AnimationClip:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: "Anim_Drog_\uC9D1\uD589_\uC5F0\uD0C03_0"
|
||||
serializedVersion: 7
|
||||
m_Legacy: 0
|
||||
m_Compressed: 0
|
||||
m_UseHighQualityCurve: 1
|
||||
m_RotationCurves: []
|
||||
m_CompressedRotationCurves: []
|
||||
m_EulerCurves: []
|
||||
m_PositionCurves: []
|
||||
m_ScaleCurves: []
|
||||
m_FloatCurves: []
|
||||
m_PPtrCurves: []
|
||||
m_SampleRate: 60
|
||||
m_WrapMode: 0
|
||||
m_Bounds:
|
||||
m_Center: {x: 0, y: 0, z: 0}
|
||||
m_Extent: {x: 0, y: 0, z: 0}
|
||||
m_ClipBindingConstant:
|
||||
genericBindings: []
|
||||
pptrCurveMapping: []
|
||||
m_AnimationClipSettings:
|
||||
serializedVersion: 2
|
||||
m_AdditiveReferencePoseClip: {fileID: 0}
|
||||
m_AdditiveReferencePoseTime: 0
|
||||
m_StartTime: 0
|
||||
m_StopTime: 1
|
||||
m_OrientationOffsetY: 0
|
||||
m_Level: 0
|
||||
m_CycleOffset: 0
|
||||
m_HasAdditiveReferencePose: 0
|
||||
m_LoopTime: 0
|
||||
m_LoopBlend: 0
|
||||
m_LoopBlendOrientation: 0
|
||||
m_LoopBlendPositionY: 0
|
||||
m_LoopBlendPositionXZ: 0
|
||||
m_KeepOriginalOrientation: 0
|
||||
m_KeepOriginalPositionY: 1
|
||||
m_KeepOriginalPositionXZ: 0
|
||||
m_HeightFromFeet: 0
|
||||
m_Mirror: 0
|
||||
m_EditorCurves: []
|
||||
m_EulerEditorCurves: []
|
||||
m_HasGenericRootTransform: 0
|
||||
m_HasMotionFloatCurves: 0
|
||||
m_Events: []
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a7f2d3a84c10032de8d569dcf1eed9e0
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 7400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -85280,7 +85280,7 @@ AnimationClip:
|
||||
m_KeepOriginalPositionY: 1
|
||||
m_KeepOriginalPositionXZ: 0
|
||||
m_HeightFromFeet: 0
|
||||
m_Mirror: 0
|
||||
m_Mirror: 1
|
||||
m_EditorCurves: []
|
||||
m_EulerEditorCurves: []
|
||||
m_HasGenericRootTransform: 0
|
||||
|
||||
@@ -37220,7 +37220,7 @@ AnimationClip:
|
||||
m_KeepOriginalPositionY: 1
|
||||
m_KeepOriginalPositionXZ: 0
|
||||
m_HeightFromFeet: 0
|
||||
m_Mirror: 0
|
||||
m_Mirror: 1
|
||||
m_EditorCurves: []
|
||||
m_EulerEditorCurves: []
|
||||
m_HasGenericRootTransform: 0
|
||||
|
||||
@@ -43628,7 +43628,7 @@ AnimationClip:
|
||||
m_KeepOriginalPositionY: 1
|
||||
m_KeepOriginalPositionXZ: 0
|
||||
m_HeightFromFeet: 0
|
||||
m_Mirror: 0
|
||||
m_Mirror: 1
|
||||
m_EditorCurves: []
|
||||
m_EulerEditorCurves: []
|
||||
m_HasGenericRootTransform: 0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 567a0c8cbb10eafa08807226645826e2
|
||||
guid: 3c74a2162339f1e06920a807790022a0
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 7400000
|
||||
|
||||
@@ -42026,7 +42026,7 @@ AnimationClip:
|
||||
m_KeepOriginalPositionY: 1
|
||||
m_KeepOriginalPositionXZ: 0
|
||||
m_HeightFromFeet: 0
|
||||
m_Mirror: 0
|
||||
m_Mirror: 1
|
||||
m_EditorCurves: []
|
||||
m_EulerEditorCurves: []
|
||||
m_HasGenericRootTransform: 0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 94da51b9da4bad4129ba5e33e671db62
|
||||
guid: 436c85ff7f42b275ca867b40738254a6
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 7400000
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b948f6e859be42cf9ad570e16fd418f1
|
||||
guid: 567a0c8cbb10eafa08807226645826e2
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 7400000
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6a8225638f252a343bf7a63037d9646b
|
||||
guid: 94da51b9da4bad4129ba5e33e671db62
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 7400000
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 99ab919e6f98f0396888bafc0149f31a
|
||||
guid: b948f6e859be42cf9ad570e16fd418f1
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 7400000
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 23fdb289ddd6a8647bc2afcb0d698c9c
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 7400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a4f29f65827ea404c9a36f23eb6cafae
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 7400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 73b61b93f6d007f4d9118e065aab4ae2
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 7400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7fd9fb0400173654e81a4d3d7f046a8e
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 7400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fe1cfb01e46465645a44bd138c4c778a
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 7400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -96,6 +96,33 @@ AnimatorStateTransition:
|
||||
m_InterruptionSource: 0
|
||||
m_OrderedInterruption: 1
|
||||
m_CanTransitionToSelf: 1
|
||||
--- !u!1102 &-5654511470593336247
|
||||
AnimatorState:
|
||||
serializedVersion: 6
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: Hit
|
||||
m_Speed: 1
|
||||
m_CycleOffset: 0
|
||||
m_Transitions:
|
||||
- {fileID: -4610620118332829913}
|
||||
m_StateMachineBehaviours: []
|
||||
m_Position: {x: 50, y: 50, z: 0}
|
||||
m_IKOnFeet: 0
|
||||
m_WriteDefaultValues: 1
|
||||
m_Mirror: 0
|
||||
m_SpeedParameterActive: 0
|
||||
m_MirrorParameterActive: 0
|
||||
m_CycleOffsetParameterActive: 0
|
||||
m_TimeParameterActive: 0
|
||||
m_Motion: {fileID: 7400000, guid: 3c23cdc1de7e1def5b30d36847b8626e, type: 2}
|
||||
m_Tag:
|
||||
m_SpeedParameter:
|
||||
m_MirrorParameter:
|
||||
m_CycleOffsetParameter:
|
||||
m_TimeParameter:
|
||||
--- !u!1107 &-5279407469294999195
|
||||
AnimatorStateMachine:
|
||||
serializedVersion: 6
|
||||
@@ -120,9 +147,13 @@ AnimatorStateMachine:
|
||||
- serializedVersion: 1
|
||||
m_State: {fileID: 8846385207969213533}
|
||||
m_Position: {x: -220, y: 80, z: 0}
|
||||
- serializedVersion: 1
|
||||
m_State: {fileID: -5654511470593336247}
|
||||
m_Position: {x: 30, y: 270, z: 0}
|
||||
m_ChildStateMachines: []
|
||||
m_AnyStateTransitions:
|
||||
- {fileID: -6146543620670552976}
|
||||
- {fileID: -1314145896434046212}
|
||||
m_EntryTransitions: []
|
||||
m_StateMachineTransitions: {}
|
||||
m_StateMachineBehaviours: []
|
||||
@@ -158,6 +189,28 @@ AnimatorState:
|
||||
m_MirrorParameter:
|
||||
m_CycleOffsetParameter:
|
||||
m_TimeParameter:
|
||||
--- !u!1101 &-4610620118332829913
|
||||
AnimatorStateTransition:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name:
|
||||
m_Conditions: []
|
||||
m_DstStateMachine: {fileID: 0}
|
||||
m_DstState: {fileID: -7908033645098541312}
|
||||
m_Solo: 0
|
||||
m_Mute: 0
|
||||
m_IsExit: 0
|
||||
serializedVersion: 3
|
||||
m_TransitionDuration: 0.25
|
||||
m_TransitionOffset: 0
|
||||
m_ExitTime: 0.75
|
||||
m_HasExitTime: 1
|
||||
m_HasFixedDuration: 1
|
||||
m_InterruptionSource: 0
|
||||
m_OrderedInterruption: 1
|
||||
m_CanTransitionToSelf: 1
|
||||
--- !u!1102 &-2487449162152911812
|
||||
AnimatorState:
|
||||
serializedVersion: 6
|
||||
@@ -249,6 +302,31 @@ AnimatorState:
|
||||
m_MirrorParameter:
|
||||
m_CycleOffsetParameter:
|
||||
m_TimeParameter:
|
||||
--- !u!1101 &-1314145896434046212
|
||||
AnimatorStateTransition:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name:
|
||||
m_Conditions:
|
||||
- m_ConditionMode: 1
|
||||
m_ConditionEvent: Hit
|
||||
m_EventTreshold: 0
|
||||
m_DstStateMachine: {fileID: 0}
|
||||
m_DstState: {fileID: -5654511470593336247}
|
||||
m_Solo: 0
|
||||
m_Mute: 0
|
||||
m_IsExit: 0
|
||||
serializedVersion: 3
|
||||
m_TransitionDuration: 0.25
|
||||
m_TransitionOffset: 0
|
||||
m_ExitTime: 0.75
|
||||
m_HasExitTime: 0
|
||||
m_HasFixedDuration: 1
|
||||
m_InterruptionSource: 0
|
||||
m_OrderedInterruption: 1
|
||||
m_CanTransitionToSelf: 1
|
||||
--- !u!91 &9100000
|
||||
AnimatorController:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -300,6 +378,12 @@ AnimatorController:
|
||||
m_DefaultInt: 0
|
||||
m_DefaultBool: 0
|
||||
m_Controller: {fileID: 9100000}
|
||||
- m_Name: Hit
|
||||
m_Type: 9
|
||||
m_DefaultFloat: 0
|
||||
m_DefaultInt: 0
|
||||
m_DefaultBool: 0
|
||||
m_Controller: {fileID: 9100000}
|
||||
m_AnimatorLayers:
|
||||
- serializedVersion: 5
|
||||
m_Name: Base Layer
|
||||
@@ -333,7 +417,7 @@ AnimatorController:
|
||||
m_Behaviours: []
|
||||
m_BlendingMode: 0
|
||||
m_SyncedLayerIndex: 0
|
||||
m_DefaultWeight: 0.4
|
||||
m_DefaultWeight: 0
|
||||
m_IKPass: 0
|
||||
m_SyncedLayerAffectsTiming: 0
|
||||
m_Controller: {fileID: 9100000}
|
||||
|
||||
30
Assets/_Game/Data/Patterns/Data_Pattern_Drog_땅 울리기.asset
Normal file
30
Assets/_Game/Data/Patterns/Data_Pattern_Drog_땅 울리기.asset
Normal file
@@ -0,0 +1,30 @@
|
||||
%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_\uB545 \uC6B8\uB9AC\uAE30"
|
||||
m_EditorClassIdentifier: Colosseum.Game::Colosseum.AI.BossPatternData
|
||||
patternName: "\uB545 \uC6B8\uB9AC\uAE30"
|
||||
category: 1
|
||||
isSignature: 0
|
||||
isMelee: 0
|
||||
targetMode: 1
|
||||
steps:
|
||||
- Type: 0
|
||||
Skill: {fileID: 11400000, guid: 46a97c453188d6a9489a97ff3b8553fd, type: 2}
|
||||
Duration: 0
|
||||
ChargeData:
|
||||
requiredDamageRatio: 0
|
||||
telegraphAbnormality: {fileID: 0}
|
||||
staggerDuration: 0
|
||||
cooldown: 30
|
||||
minPhase: 2
|
||||
skipJumpStepOnNoTarget: 0
|
||||
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4f40629d4d334434285e8fdec3714536
|
||||
guid: 67aad7db460b1d64f9395b2f08a3703a
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
@@ -26,5 +26,5 @@ MonoBehaviour:
|
||||
telegraphAbnormality: {fileID: 0}
|
||||
staggerDuration: 0
|
||||
cooldown: 2.5
|
||||
minPhase: 1
|
||||
minPhase: 2
|
||||
skipJumpStepOnNoTarget: 0
|
||||
|
||||
@@ -67,6 +67,6 @@ MonoBehaviour:
|
||||
requiredDamageRatio: 0
|
||||
telegraphAbnormality: {fileID: 0}
|
||||
staggerDuration: 0
|
||||
cooldown: 45
|
||||
cooldown: 90
|
||||
minPhase: 3
|
||||
skipJumpStepOnNoTarget: 0
|
||||
|
||||
67
Assets/_Game/Data/Skills/Data_Skill_Drog_땅 울리기.asset
Normal file
67
Assets/_Game/Data/Skills/Data_Skill_Drog_땅 울리기.asset
Normal file
@@ -0,0 +1,67 @@
|
||||
%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: 94f0a76cebcac2f4fb5daf1b675fd79f, type: 3}
|
||||
m_Name: "Data_Skill_Drog_\uB545 \uC6B8\uB9AC\uAE30"
|
||||
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.SkillData
|
||||
skillName: "\uB545 \uC6B8\uB9AC\uAE30"
|
||||
description: "Phase 2 \uC911\uBC18 \uC555\uBC15 \uC804\uD658\uC744 \uC120\uC5B8\uD558\uB294
|
||||
\uAD11\uC5ED \uB0B4\uB824\uCC0D\uAE30\uC785\uB2C8\uB2E4."
|
||||
icon: {fileID: 0}
|
||||
skillRole: 1
|
||||
activationType: 1
|
||||
baseTypes: 1
|
||||
animationClips:
|
||||
- {fileID: 7400000, guid: 189617e6b9348dbb287d409c27866040, type: 2}
|
||||
animationSpeed: 1
|
||||
useRootMotion: 1
|
||||
ignoreRootMotionY: 1
|
||||
jumpToTarget: 0
|
||||
blockMovementWhileCasting: 1
|
||||
blockJumpWhileCasting: 1
|
||||
blockOtherSkillsWhileCasting: 1
|
||||
castTargetTrackingMode: 1
|
||||
castTargetRotationSpeed: 12
|
||||
castTargetStopDistance: 2.5
|
||||
allowedWeaponTraits: 0
|
||||
cooldown: 0
|
||||
manaCost: 0
|
||||
maxGemSlotCount: 0
|
||||
castStartEffects: []
|
||||
triggeredEffects:
|
||||
- triggerIndex: 0
|
||||
effects:
|
||||
- {fileID: 11400000, guid: dc1bf7901fa834b03a1c41d30180d766, type: 2}
|
||||
- {fileID: 11400000, guid: 960ce920faebc5102a4ad93318844080, type: 2}
|
||||
isChanneling: 0
|
||||
loopPhase:
|
||||
enabled: 0
|
||||
loopMode: 1
|
||||
maxDuration: 3
|
||||
tickInterval: 0.5
|
||||
tickEffects: []
|
||||
exitEffects: []
|
||||
loopVfxPrefab: {fileID: 0}
|
||||
loopVfxMountPath:
|
||||
loopVfxLengthScale: 1
|
||||
loopVfxWidthScale: 1
|
||||
releasePhase:
|
||||
enabled: 0
|
||||
animationClips: []
|
||||
startEffects: []
|
||||
channelDuration: 3
|
||||
channelTickInterval: 0.5
|
||||
channelTickEffects: []
|
||||
channelEndEffects: []
|
||||
channelVfxPrefab: {fileID: 0}
|
||||
channelVfxMountPath:
|
||||
channelVfxLengthScale: 1
|
||||
channelVfxWidthScale: 1
|
||||
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 773afd8dabe30374c826b7fa1d1a68ea
|
||||
guid: 46a97c453188d6a9489a97ff3b8553fd
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
@@ -19,8 +19,8 @@ MonoBehaviour:
|
||||
activationType: 1
|
||||
baseTypes: 1
|
||||
animationClips:
|
||||
- {fileID: 7400000, guid: 567a0c8cbb10eafa08807226645826e2, type: 2}
|
||||
- {fileID: 7400000, guid: 94da51b9da4bad4129ba5e33e671db62, type: 2}
|
||||
- {fileID: 7400000, guid: 3c74a2162339f1e06920a807790022a0, type: 2}
|
||||
- {fileID: 7400000, guid: 436c85ff7f42b275ca867b40738254a6, type: 2}
|
||||
animationSpeed: 1
|
||||
useRootMotion: 1
|
||||
ignoreRootMotionY: 1
|
||||
|
||||
@@ -19,7 +19,8 @@ MonoBehaviour:
|
||||
activationType: 1
|
||||
baseTypes: 1
|
||||
animationClips:
|
||||
- {fileID: 7400000, guid: b948f6e859be42cf9ad570e16fd418f1, type: 2}
|
||||
- {fileID: 7400000, guid: 567a0c8cbb10eafa08807226645826e2, type: 2}
|
||||
- {fileID: 7400000, guid: 94da51b9da4bad4129ba5e33e671db62, type: 2}
|
||||
animationSpeed: 1
|
||||
useRootMotion: 1
|
||||
ignoreRootMotionY: 1
|
||||
|
||||
66
Assets/_Game/Data/Skills/Data_Skill_Drog_콤보-기본기1_3.asset
Normal file
66
Assets/_Game/Data/Skills/Data_Skill_Drog_콤보-기본기1_3.asset
Normal file
@@ -0,0 +1,66 @@
|
||||
%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: 94f0a76cebcac2f4fb5daf1b675fd79f, type: 3}
|
||||
m_Name: "Data_Skill_Drog_\uCF64\uBCF4-\uAE30\uBCF8\uAE301_3"
|
||||
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.SkillData
|
||||
skillName: "\uCF64\uBCF4-\uAE30\uBCF8\uAE301 2\uD0C0"
|
||||
description: "\uAE30\uBCF8\uAE30 \uCF64\uBCF41\uC758 \uD6C4\uC18D \uD0C0\uACA9\uC785\uB2C8\uB2E4."
|
||||
icon: {fileID: 0}
|
||||
skillRole: 1
|
||||
activationType: 1
|
||||
baseTypes: 1
|
||||
animationClips:
|
||||
- {fileID: 7400000, guid: 567a0c8cbb10eafa08807226645826e2, type: 2}
|
||||
- {fileID: 7400000, guid: 94da51b9da4bad4129ba5e33e671db62, type: 2}
|
||||
animationSpeed: 1
|
||||
useRootMotion: 1
|
||||
ignoreRootMotionY: 1
|
||||
jumpToTarget: 0
|
||||
blockMovementWhileCasting: 1
|
||||
blockJumpWhileCasting: 1
|
||||
blockOtherSkillsWhileCasting: 1
|
||||
castTargetTrackingMode: 1
|
||||
castTargetRotationSpeed: 12
|
||||
castTargetStopDistance: 2.5
|
||||
allowedWeaponTraits: 0
|
||||
cooldown: 0
|
||||
manaCost: 0
|
||||
maxGemSlotCount: 0
|
||||
castStartEffects: []
|
||||
triggeredEffects:
|
||||
- triggerIndex: 0
|
||||
effects:
|
||||
- {fileID: 11400000, guid: 135f4690ea9c62bd8835b97c7ace22d7, type: 2}
|
||||
isChanneling: 0
|
||||
loopPhase:
|
||||
enabled: 0
|
||||
loopMode: 1
|
||||
maxDuration: 3
|
||||
tickInterval: 0.5
|
||||
tickEffects: []
|
||||
exitEffects: []
|
||||
loopVfxPrefab: {fileID: 0}
|
||||
loopVfxMountPath:
|
||||
loopVfxLengthScale: 1
|
||||
loopVfxWidthScale: 1
|
||||
releasePhase:
|
||||
enabled: 0
|
||||
animationClips: []
|
||||
startEffects: []
|
||||
channelDuration: 3
|
||||
channelTickInterval: 0.5
|
||||
channelTickEffects: []
|
||||
channelEndEffects: []
|
||||
channelVfxPrefab: {fileID: 0}
|
||||
channelVfxMountPath:
|
||||
channelVfxLengthScale: 1
|
||||
channelVfxWidthScale: 1
|
||||
@@ -1,8 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 58847e89d27d1b140b1075bba68445c0
|
||||
guid: ef31b6ee182b8ce4cb7a0abf35daa91e
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 7400000
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,38 +0,0 @@
|
||||
%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: 94f0a76cebcac2f4fb5daf1b675fd79f, type: 3}
|
||||
m_Name: "Data_Skill_Drog_\uD3C9\uD0C01R"
|
||||
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.SkillData
|
||||
skillName: "\uD3C9\uD0C01R"
|
||||
description:
|
||||
icon: {fileID: 0}
|
||||
skillRole: 1
|
||||
activationType: 1
|
||||
baseTypes: 0
|
||||
animationClips:
|
||||
- {fileID: 7400000, guid: 23fdb289ddd6a8647bc2afcb0d698c9c, type: 2}
|
||||
- {fileID: 7400000, guid: a4f29f65827ea404c9a36f23eb6cafae, type: 2}
|
||||
animationSpeed: 1
|
||||
useRootMotion: 1
|
||||
ignoreRootMotionY: 1
|
||||
jumpToTarget: 0
|
||||
blockMovementWhileCasting: 1
|
||||
blockJumpWhileCasting: 1
|
||||
blockOtherSkillsWhileCasting: 1
|
||||
allowedWeaponTraits: 0
|
||||
cooldown: 0
|
||||
manaCost: 0
|
||||
maxGemSlotCount: 2
|
||||
castStartEffects: []
|
||||
effects:
|
||||
- {fileID: 11400000, guid: 86bf7e282c1639c4889910475aaccdef, type: 2}
|
||||
- {fileID: 0}
|
||||
@@ -1,43 +0,0 @@
|
||||
%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: 94f0a76cebcac2f4fb5daf1b675fd79f, type: 3}
|
||||
m_Name: "Data_Skill_Drog_\uD3C9\uD0C02R"
|
||||
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.SkillData
|
||||
skillName: "\uD3C9\uD0C02R"
|
||||
description:
|
||||
icon: {fileID: 0}
|
||||
skillRole: 1
|
||||
activationType: 1
|
||||
baseTypes: 0
|
||||
animationClips:
|
||||
- {fileID: 7400000, guid: 73b61b93f6d007f4d9118e065aab4ae2, type: 2}
|
||||
- {fileID: 7400000, guid: 7fd9fb0400173654e81a4d3d7f046a8e, type: 2}
|
||||
- {fileID: 7400000, guid: fe1cfb01e46465645a44bd138c4c778a, type: 2}
|
||||
animationSpeed: 1
|
||||
useRootMotion: 1
|
||||
ignoreRootMotionY: 1
|
||||
jumpToTarget: 0
|
||||
blockMovementWhileCasting: 1
|
||||
blockJumpWhileCasting: 1
|
||||
blockOtherSkillsWhileCasting: 1
|
||||
allowedWeaponTraits: 0
|
||||
cooldown: 0
|
||||
manaCost: 0
|
||||
maxGemSlotCount: 2
|
||||
castStartEffects: []
|
||||
triggeredEffects:
|
||||
- triggerIndex: 0
|
||||
effects:
|
||||
- {fileID: 11400000, guid: 87b064a0134987b4b9638e184ab07411, type: 2}
|
||||
- triggerIndex: 1
|
||||
effects:
|
||||
- {fileID: 11400000, guid: 2db6d8d7f5da4f7ab9f0a12e65498ab1, type: 2}
|
||||
@@ -0,0 +1,30 @@
|
||||
%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: 58efb3c775496fa40b801b21127a011e, type: 3}
|
||||
m_Name: "Data_SkillEffect_Drog_\uB545 \uC6B8\uB9AC\uAE30_0_\uB370\uBBF8\uC9C0"
|
||||
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.Effects.DamageEffect
|
||||
targetType: 1
|
||||
targetTeam: 0
|
||||
areaCenter: 0
|
||||
areaShape: 0
|
||||
targetLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
includeCasterInArea: 0
|
||||
areaRadius: 6.5
|
||||
fanOriginDistance: 1
|
||||
fanRadius: 6.5
|
||||
fanHalfAngle: 180
|
||||
baseDamage: 36
|
||||
damageType: 0
|
||||
statScaling: 0.95
|
||||
mitigationTier: 0
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dc1bf7901fa834b03a1c41d30180d766
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,27 @@
|
||||
%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: 41c96b54a96cdb84c9bda774775b0a1a, type: 3}
|
||||
m_Name: "Data_SkillEffect_Drog_\uB545 \uC6B8\uB9AC\uAE30_1_\uB2E4\uC6B4"
|
||||
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.Effects.DownEffect
|
||||
targetType: 1
|
||||
targetTeam: 0
|
||||
areaCenter: 0
|
||||
areaShape: 0
|
||||
targetLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
includeCasterInArea: 0
|
||||
areaRadius: 3.4
|
||||
fanOriginDistance: 1
|
||||
fanRadius: 3.4
|
||||
fanHalfAngle: 180
|
||||
duration: 2.2
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 960ce920faebc5102a4ad93318844080
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,30 @@
|
||||
%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: 58efb3c775496fa40b801b21127a011e, type: 3}
|
||||
m_Name: "Data_SkillEffect_Drog_\uCF64\uBCF4-\uAE30\uBCF8\uAE301_3_0_\uB370\uBBF8\uC9C0"
|
||||
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.Effects.DamageEffect
|
||||
targetType: 1
|
||||
targetTeam: 0
|
||||
areaCenter: 0
|
||||
areaShape: 1
|
||||
targetLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
includeCasterInArea: 0
|
||||
areaRadius: 3.3
|
||||
fanOriginDistance: 1.25
|
||||
fanRadius: 3.3
|
||||
fanHalfAngle: 40
|
||||
baseDamage: 16
|
||||
damageType: 0
|
||||
statScaling: 0.5
|
||||
mitigationTier: 0
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ea989ff2b4e918f248b3377ac57f3bef
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,53 @@
|
||||
using System;
|
||||
|
||||
using Colosseum.Enemy;
|
||||
|
||||
using Unity.Behavior;
|
||||
using Unity.Properties;
|
||||
using UnityEngine;
|
||||
|
||||
using Action = Unity.Behavior.Action;
|
||||
|
||||
/// <summary>
|
||||
/// 최근 전투 부활 트리거에서 우선 압박할 대상을 선택합니다.
|
||||
/// </summary>
|
||||
[Serializable, GeneratePropertyBag]
|
||||
[NodeDescription(
|
||||
name: "Select Recent Revive Target",
|
||||
story: "최근 부활 트리거 대상을 [Target]으로 선택",
|
||||
category: "Action",
|
||||
id: "464c3911-46d3-4138-88cf-8ba696ba4c13")]
|
||||
public partial class SelectRecentReviveTargetAction : Action
|
||||
{
|
||||
[SerializeReference]
|
||||
public BlackboardVariable<GameObject> Target;
|
||||
|
||||
[SerializeReference]
|
||||
public BlackboardVariable<float> MaxAge = new BlackboardVariable<float>(4f);
|
||||
|
||||
[SerializeReference]
|
||||
public BlackboardVariable<bool> PreferCaster = new BlackboardVariable<bool>(true);
|
||||
|
||||
[SerializeReference]
|
||||
public BlackboardVariable<bool> FallbackToRevivedTarget = new BlackboardVariable<bool>(true);
|
||||
|
||||
protected override Status OnStart()
|
||||
{
|
||||
BossBehaviorRuntimeState runtimeState = GameObject.GetComponent<BossBehaviorRuntimeState>();
|
||||
if (runtimeState == null)
|
||||
return Status.Failure;
|
||||
|
||||
GameObject resolvedTarget = runtimeState.ResolveRecentReviveTriggerTarget(
|
||||
MaxAge?.Value ?? 0f,
|
||||
PreferCaster?.Value ?? true,
|
||||
FallbackToRevivedTarget?.Value ?? true);
|
||||
|
||||
if (resolvedTarget == null)
|
||||
return Status.Failure;
|
||||
|
||||
Target.Value = resolvedTarget;
|
||||
runtimeState.SetCurrentTarget(resolvedTarget);
|
||||
runtimeState.LogDebug(nameof(SelectRecentReviveTargetAction), $"부활 트리거 대상 선택: {resolvedTarget.name}");
|
||||
return Status.Success;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3a2a455c708e34f4ca783bab6bdf8ce4
|
||||
@@ -41,13 +41,10 @@ public partial class UsePatternByRoleAction : BossPatternActionBase
|
||||
if (pattern == null)
|
||||
return Status.Failure;
|
||||
|
||||
// 타겟 해석은 ResolveStepTarget에서 처리됨
|
||||
// 여기서는 RegisterPatternUse만 호출 (근접 패턴 전용)
|
||||
if (pattern.IsMelee)
|
||||
{
|
||||
// 타겟 해석은 ResolveStepTarget에서 처리됩니다.
|
||||
// 대형 패턴/징벌 패턴 후 기본 루프 강제 규칙이 유지되도록 모든 패턴 사용을 기록합니다.
|
||||
BossBehaviorRuntimeState context = GameObject.GetComponent<BossBehaviorRuntimeState>();
|
||||
context?.RegisterPatternUse(pattern);
|
||||
}
|
||||
|
||||
// base.OnStart는 TryResolvePattern → ExecuteCurrentStep 호출
|
||||
return base.OnStart();
|
||||
@@ -71,16 +68,25 @@ public partial class UsePatternByRoleAction : BossPatternActionBase
|
||||
{
|
||||
pattern = Pattern?.Value;
|
||||
target = Target != null ? Target.Value : null;
|
||||
BossBehaviorRuntimeState context = GameObject.GetComponent<BossBehaviorRuntimeState>();
|
||||
|
||||
if (pattern == null)
|
||||
{
|
||||
context?.LogDebug(nameof(UsePatternByRoleAction), "실행 실패: Pattern이 비어 있습니다.");
|
||||
return false;
|
||||
}
|
||||
|
||||
BossBehaviorRuntimeState context = GameObject.GetComponent<BossBehaviorRuntimeState>();
|
||||
if (context == null || !context.IsPatternReady(pattern))
|
||||
{
|
||||
context?.LogDebug(nameof(UsePatternByRoleAction), $"실행 실패: 패턴 준비 안 됨 - {pattern.PatternName}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (target == null)
|
||||
{
|
||||
context?.LogDebug(nameof(UsePatternByRoleAction), $"실행 실패: 타겟 없음 - {pattern.PatternName}");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
using System;
|
||||
|
||||
using Unity.Behavior;
|
||||
using Unity.Properties;
|
||||
using UnityEngine;
|
||||
|
||||
using Colosseum.Enemy;
|
||||
|
||||
namespace Colosseum.AI.BehaviorActions.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// 준비된 후보 패턴 중 하나를 가중치 기반으로 선택하고 즉시 실행합니다.
|
||||
/// </summary>
|
||||
[Serializable, GeneratePropertyBag]
|
||||
[NodeDescription(
|
||||
name: "Use Weighted Ready Pattern",
|
||||
story: "준비된 후보 패턴 중 하나를 가중치 기반으로 선택해 실행",
|
||||
category: "Action",
|
||||
id: "6d4fc6fd-0ccd-4d9a-8b86-c602062f78a7")]
|
||||
public partial class UseWeightedReadyPatternAction : BossPatternActionBase
|
||||
{
|
||||
[SerializeReference] public BlackboardVariable<BossPatternData> Pattern1;
|
||||
[SerializeReference] public BlackboardVariable<float> Weight1 = new BlackboardVariable<float>(1f);
|
||||
[SerializeReference] public BlackboardVariable<BossPatternData> Pattern2;
|
||||
[SerializeReference] public BlackboardVariable<float> Weight2 = new BlackboardVariable<float>(1f);
|
||||
[SerializeReference] public BlackboardVariable<BossPatternData> Pattern3;
|
||||
[SerializeReference] public BlackboardVariable<float> Weight3 = new BlackboardVariable<float>(1f);
|
||||
[SerializeReference] public BlackboardVariable<BossPatternData> Pattern4;
|
||||
[SerializeReference] public BlackboardVariable<float> Weight4 = new BlackboardVariable<float>(1f);
|
||||
[SerializeReference] public BlackboardVariable<BossPatternData> Pattern5;
|
||||
[SerializeReference] public BlackboardVariable<float> Weight5 = new BlackboardVariable<float>(1f);
|
||||
|
||||
private BossPatternData selectedPattern;
|
||||
|
||||
protected override Status OnStart()
|
||||
{
|
||||
if (!TrySelectPattern(out selectedPattern))
|
||||
return Status.Failure;
|
||||
|
||||
BossBehaviorRuntimeState context = GameObject.GetComponent<BossBehaviorRuntimeState>();
|
||||
context?.RegisterPatternUse(selectedPattern);
|
||||
context?.LogDebug(nameof(UseWeightedReadyPatternAction), $"가중치 패턴 선택 후 실행: {selectedPattern.PatternName}");
|
||||
return base.OnStart();
|
||||
}
|
||||
|
||||
protected override void OnEnd()
|
||||
{
|
||||
selectedPattern = null;
|
||||
base.OnEnd();
|
||||
}
|
||||
|
||||
protected override bool TryResolvePattern(out BossPatternData pattern, out GameObject target)
|
||||
{
|
||||
target = Target != null ? Target.Value : null;
|
||||
pattern = selectedPattern;
|
||||
|
||||
if (pattern == null && !TrySelectPattern(out pattern))
|
||||
return false;
|
||||
|
||||
if (target == null)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TrySelectPattern(out BossPatternData pattern)
|
||||
{
|
||||
WeightedPatternCandidate[] candidates =
|
||||
{
|
||||
new WeightedPatternCandidate(Pattern1?.Value, Weight1?.Value ?? 0f),
|
||||
new WeightedPatternCandidate(Pattern2?.Value, Weight2?.Value ?? 0f),
|
||||
new WeightedPatternCandidate(Pattern3?.Value, Weight3?.Value ?? 0f),
|
||||
new WeightedPatternCandidate(Pattern4?.Value, Weight4?.Value ?? 0f),
|
||||
new WeightedPatternCandidate(Pattern5?.Value, Weight5?.Value ?? 0f),
|
||||
};
|
||||
|
||||
return WeightedPatternSelector.TrySelectReadyPattern(GameObject, candidates, out pattern);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8fdabcba63a07333fa626c0dff9d95e0
|
||||
@@ -0,0 +1,45 @@
|
||||
using System;
|
||||
|
||||
using Unity.Behavior;
|
||||
using Unity.Properties;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Colosseum.AI.BehaviorActions.Conditions
|
||||
{
|
||||
/// <summary>
|
||||
/// 후보 패턴 중 현재 실행 가능한 패턴이 하나라도 있는지 확인합니다.
|
||||
/// </summary>
|
||||
[Serializable, GeneratePropertyBag]
|
||||
[Condition(name: "Has Any Ready Pattern", story: "후보 패턴 중 실행 가능한 패턴이 있는가?", id: "f2c9e62a-f5ea-43c5-8e88-b94a449e7c7f")]
|
||||
[NodeDescription(
|
||||
name: "Has Any Ready Pattern",
|
||||
story: "후보 패턴 중 실행 가능한 패턴이 있는가?",
|
||||
category: "Condition/Pattern")]
|
||||
public partial class HasAnyReadyPatternCondition : Unity.Behavior.Condition
|
||||
{
|
||||
[SerializeReference] public BlackboardVariable<BossPatternData> Pattern1;
|
||||
[SerializeReference] public BlackboardVariable<float> Weight1 = new BlackboardVariable<float>(1f);
|
||||
[SerializeReference] public BlackboardVariable<BossPatternData> Pattern2;
|
||||
[SerializeReference] public BlackboardVariable<float> Weight2 = new BlackboardVariable<float>(1f);
|
||||
[SerializeReference] public BlackboardVariable<BossPatternData> Pattern3;
|
||||
[SerializeReference] public BlackboardVariable<float> Weight3 = new BlackboardVariable<float>(1f);
|
||||
[SerializeReference] public BlackboardVariable<BossPatternData> Pattern4;
|
||||
[SerializeReference] public BlackboardVariable<float> Weight4 = new BlackboardVariable<float>(1f);
|
||||
[SerializeReference] public BlackboardVariable<BossPatternData> Pattern5;
|
||||
[SerializeReference] public BlackboardVariable<float> Weight5 = new BlackboardVariable<float>(1f);
|
||||
|
||||
public override bool IsTrue()
|
||||
{
|
||||
WeightedPatternCandidate[] candidates =
|
||||
{
|
||||
new WeightedPatternCandidate(Pattern1?.Value, Weight1?.Value ?? 0f),
|
||||
new WeightedPatternCandidate(Pattern2?.Value, Weight2?.Value ?? 0f),
|
||||
new WeightedPatternCandidate(Pattern3?.Value, Weight3?.Value ?? 0f),
|
||||
new WeightedPatternCandidate(Pattern4?.Value, Weight4?.Value ?? 0f),
|
||||
new WeightedPatternCandidate(Pattern5?.Value, Weight5?.Value ?? 0f),
|
||||
};
|
||||
|
||||
return WeightedPatternSelector.HasAnyReadyPattern(GameObject, candidates);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1e0445cb63a4144ee9f4361c0729d95a
|
||||
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
|
||||
using Colosseum.Enemy;
|
||||
|
||||
using Unity.Behavior;
|
||||
using Unity.Properties;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Colosseum.AI.BehaviorActions.Conditions
|
||||
{
|
||||
/// <summary>
|
||||
/// 최근 전투 부활 트리거가 아직 유효한지 확인합니다.
|
||||
/// </summary>
|
||||
[Serializable, GeneratePropertyBag]
|
||||
[Condition(name: "Is Recent Revive Trigger", story: "최근 [MaxAge]초 이내 부활 트리거가 있는가?", id: "d12890b1-0cb0-4586-a61f-885e7d0e97ee")]
|
||||
[NodeDescription(
|
||||
name: "Is Recent Revive Trigger",
|
||||
story: "최근 [MaxAge]초 이내 부활 트리거가 있는가?",
|
||||
category: "Condition/Pattern")]
|
||||
public partial class IsRecentReviveTriggerCondition : Unity.Behavior.Condition
|
||||
{
|
||||
[SerializeReference]
|
||||
[Tooltip("부활 트리거를 유효하다고 간주할 최대 시간(초)")]
|
||||
public BlackboardVariable<float> MaxAge = new BlackboardVariable<float>(4f);
|
||||
|
||||
public override bool IsTrue()
|
||||
{
|
||||
BossBehaviorRuntimeState runtimeState = GameObject.GetComponent<BossBehaviorRuntimeState>();
|
||||
return runtimeState != null && runtimeState.HasRecentReviveTrigger(MaxAge?.Value ?? 0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 67f09ee2c79dabe70afb218591cde315
|
||||
115
Assets/_Game/Scripts/AI/WeightedPatternSelector.cs
Normal file
115
Assets/_Game/Scripts/AI/WeightedPatternSelector.cs
Normal file
@@ -0,0 +1,115 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
using Colosseum.AI.BehaviorActions.Conditions;
|
||||
|
||||
namespace Colosseum.AI
|
||||
{
|
||||
/// <summary>
|
||||
/// 가중치 기반 패턴 선택 후보를 표현합니다.
|
||||
/// </summary>
|
||||
public readonly struct WeightedPatternCandidate
|
||||
{
|
||||
public WeightedPatternCandidate(BossPatternData pattern, float weight)
|
||||
{
|
||||
Pattern = pattern;
|
||||
Weight = weight;
|
||||
}
|
||||
|
||||
public BossPatternData Pattern { get; }
|
||||
|
||||
public float Weight { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 준비된 패턴 후보 중 하나를 가중치 기반으로 선택합니다.
|
||||
/// </summary>
|
||||
public static class WeightedPatternSelector
|
||||
{
|
||||
public static bool HasAnyReadyPattern(GameObject owner, IReadOnlyList<WeightedPatternCandidate> candidates)
|
||||
{
|
||||
if (owner == null || candidates == null)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < candidates.Count; i++)
|
||||
{
|
||||
WeightedPatternCandidate candidate = candidates[i];
|
||||
if (candidate.Pattern == null || candidate.Weight <= 0f)
|
||||
continue;
|
||||
|
||||
if (PatternReadyHelper.IsPatternReady(owner, candidate.Pattern))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool TrySelectReadyPattern(GameObject owner, IReadOnlyList<WeightedPatternCandidate> candidates, out BossPatternData selectedPattern)
|
||||
{
|
||||
if (owner == null)
|
||||
{
|
||||
selectedPattern = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
return TrySelectPattern(
|
||||
candidates,
|
||||
pattern => PatternReadyHelper.IsPatternReady(owner, pattern),
|
||||
UnityEngine.Random.value,
|
||||
out selectedPattern);
|
||||
}
|
||||
|
||||
public static bool TrySelectPattern(
|
||||
IReadOnlyList<WeightedPatternCandidate> candidates,
|
||||
Predicate<BossPatternData> isPatternReady,
|
||||
float normalizedRoll,
|
||||
out BossPatternData selectedPattern)
|
||||
{
|
||||
selectedPattern = null;
|
||||
|
||||
if (candidates == null || isPatternReady == null)
|
||||
return false;
|
||||
|
||||
List<WeightedPatternCandidate> readyCandidates = new List<WeightedPatternCandidate>(candidates.Count);
|
||||
float totalWeight = 0f;
|
||||
|
||||
for (int i = 0; i < candidates.Count; i++)
|
||||
{
|
||||
WeightedPatternCandidate candidate = candidates[i];
|
||||
if (candidate.Pattern == null || candidate.Weight <= 0f)
|
||||
continue;
|
||||
|
||||
if (!isPatternReady(candidate.Pattern))
|
||||
continue;
|
||||
|
||||
readyCandidates.Add(candidate);
|
||||
totalWeight += candidate.Weight;
|
||||
}
|
||||
|
||||
if (readyCandidates.Count == 0 || totalWeight <= 0f)
|
||||
return false;
|
||||
|
||||
float clampedRoll = Mathf.Clamp01(normalizedRoll);
|
||||
float targetWeight = clampedRoll >= 1f
|
||||
? totalWeight
|
||||
: totalWeight * clampedRoll;
|
||||
float cumulativeWeight = 0f;
|
||||
|
||||
for (int i = 0; i < readyCandidates.Count; i++)
|
||||
{
|
||||
WeightedPatternCandidate candidate = readyCandidates[i];
|
||||
cumulativeWeight += candidate.Weight;
|
||||
|
||||
if (targetWeight < cumulativeWeight || i == readyCandidates.Count - 1)
|
||||
{
|
||||
selectedPattern = candidate.Pattern;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/_Game/Scripts/AI/WeightedPatternSelector.cs.meta
Normal file
2
Assets/_Game/Scripts/AI/WeightedPatternSelector.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a6d90dbbcd06e25589eceee8763c9a7c
|
||||
@@ -5,6 +5,7 @@ using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
using Colosseum.AI;
|
||||
using Colosseum.AI.BehaviorActions.Actions;
|
||||
using Colosseum.AI.BehaviorActions.Conditions;
|
||||
using Colosseum.Enemy;
|
||||
using Colosseum.Skills;
|
||||
@@ -23,10 +24,12 @@ namespace Colosseum.Editor
|
||||
private const string GraphAssetPath = "Assets/_Game/AI/BT_Drog.asset";
|
||||
private const string DefaultPunishPatternPath = "Assets/_Game/Data/Patterns/Data_Pattern_Drog_밟기.asset";
|
||||
private const string DefaultSignaturePatternPath = "Assets/_Game/Data/Patterns/Data_Pattern_Drog_집행.asset";
|
||||
private const string DefaultGroundShakePatternPath = "Assets/_Game/Data/Patterns/Data_Pattern_Drog_땅 울리기.asset";
|
||||
private const string DefaultMobilityPatternPath = "Assets/_Game/Data/Patterns/Data_Pattern_Drog_도약.asset";
|
||||
private const string DefaultSecondaryPatternPath = "Assets/_Game/Data/Patterns/Data_Pattern_Drog_콤보-기본기2.asset";
|
||||
private const string DefaultComboPatternPath = "Assets/_Game/Data/Patterns/Data_Pattern_Drog_콤보-강타.asset";
|
||||
private const string DefaultPrimaryPatternPath = "Assets/_Game/Data/Patterns/Data_Pattern_Drog_콤보-기본기1.asset";
|
||||
private const string DefaultSecondaryPatternPath = "Assets/_Game/Data/Patterns/Data_Pattern_Drog_콤보-기본기2.asset";
|
||||
private const string DefaultTertiaryPatternPath = "Assets/_Game/Data/Patterns/Data_Pattern_Drog_콤보-기본기3.asset";
|
||||
private const string DefaultComboPatternPath = "Assets/_Game/Data/Patterns/Data_Pattern_Drog_콤보-강타.asset";
|
||||
private const string DefaultPressurePatternPath = "Assets/_Game/Data/Patterns/Data_Pattern_Drog_콤보-발구르기.asset";
|
||||
private const string DefaultUtilityPatternPath = "Assets/_Game/Data/Patterns/Data_Pattern_Drog_투척.asset";
|
||||
private const string DefaultPhase3TransitionSkillPath = "Assets/_Game/Data/Skills/Data_Skill_Drog_포효.asset";
|
||||
@@ -38,9 +41,15 @@ namespace Colosseum.Editor
|
||||
private const float DefaultTargetSearchRange = 20f;
|
||||
private const float DefaultThrowAvailabilityDelay = 4f;
|
||||
private const float DefaultPhaseTransitionLockDuration = 1.25f;
|
||||
private const float DefaultPhase3SignatureDelay = 0.25f;
|
||||
private const float DefaultSignatureRepeatInterval = 90f;
|
||||
private const float DefaultGroundShakeInterval = 12f;
|
||||
private const float DefaultPhase2EnterHealthPercent = 75f;
|
||||
private const float DefaultPhase3EnterHealthPercent = 40f;
|
||||
private const float DefaultComboPatternWeight = 26f;
|
||||
private const float DefaultPressurePatternWeight = 24f;
|
||||
private const float DefaultPrimaryPatternWeight = 22f;
|
||||
private const float DefaultSecondaryPatternWeight = 16f;
|
||||
private const float DefaultTertiaryPatternWeight = 12f;
|
||||
|
||||
[MenuItem("Tools/Colosseum/Rebuild Drog Behavior Authoring Graph")]
|
||||
private static void Rebuild()
|
||||
@@ -216,6 +225,7 @@ namespace Colosseum.Editor
|
||||
"MobilityTriggerDistance",
|
||||
"UtilityTriggerDistance",
|
||||
"PrimaryAttackRange",
|
||||
"SelectedMeleePattern",
|
||||
"Phase2HealthPercent",
|
||||
"Phase3HealthPercent",
|
||||
"SightRange",
|
||||
@@ -223,130 +233,352 @@ namespace Colosseum.Editor
|
||||
"MoveSpeed");
|
||||
|
||||
BossPatternData punishPattern = LoadRequiredAsset<BossPatternData>(DefaultPunishPatternPath, "밟기 패턴");
|
||||
BossPatternData signaturePattern = LoadRequiredAsset<BossPatternData>(DefaultSignaturePatternPath, "집행 패턴");
|
||||
BossPatternData groundShakePattern = LoadRequiredAsset<BossPatternData>(DefaultGroundShakePatternPath, "땅 울리기 패턴");
|
||||
BossPatternData mobilityPattern = LoadRequiredAsset<BossPatternData>(DefaultMobilityPatternPath, "도약 패턴");
|
||||
BossPatternData primaryPattern = LoadRequiredAsset<BossPatternData>(DefaultPrimaryPatternPath, "콤보-기본기1 패턴");
|
||||
BossPatternData secondaryPattern = LoadRequiredAsset<BossPatternData>(DefaultSecondaryPatternPath, "콤보-기본기2 패턴");
|
||||
BossPatternData tertiaryPattern = LoadRequiredAsset<BossPatternData>(DefaultTertiaryPatternPath, "콤보-기본기3 패턴");
|
||||
BossPatternData comboPattern = LoadRequiredAsset<BossPatternData>(DefaultComboPatternPath, "콤보-강타 패턴");
|
||||
BossPatternData pressurePattern = LoadRequiredAsset<BossPatternData>(DefaultPressurePatternPath, "콤보-발구르기 패턴");
|
||||
BossPatternData utilityPattern = LoadRequiredAsset<BossPatternData>(DefaultUtilityPatternPath, "투척 패턴");
|
||||
SkillData phase3TransitionSkill = LoadRequiredAsset<SkillData>(DefaultPhase3TransitionSkillPath, "포효 스킬");
|
||||
|
||||
if (punishPattern == null || comboPattern == null)
|
||||
if (punishPattern == null
|
||||
|| signaturePattern == null
|
||||
|| groundShakePattern == null
|
||||
|| mobilityPattern == null
|
||||
|| primaryPattern == null
|
||||
|| secondaryPattern == null
|
||||
|| tertiaryPattern == null
|
||||
|| comboPattern == null
|
||||
|| pressurePattern == null
|
||||
|| utilityPattern == null
|
||||
|| phase3TransitionSkill == null)
|
||||
{
|
||||
Debug.LogError("[DrogBTRebuild] 프리팹에서 필수 패턴 에셋을 읽지 못했습니다.");
|
||||
Debug.LogError("[DrogBTRebuild] 드로그 BT 재구성에 필요한 패턴/스킬 에셋을 읽지 못했습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 단순 우선순위 체인 ──
|
||||
// 요구사항: 밟기 > 강타 계열 > 추적
|
||||
// 다운 대상이 근처에 있으면 밟기를 우선 사용하고, 그렇지 않으면 강타 계열 패턴만 반복합니다.
|
||||
// 사거리 밖에서는 추적으로 재진입합니다.
|
||||
|
||||
const float branchX = -800f;
|
||||
const float rootRefreshX = branchX - 540f;
|
||||
const float mainSequenceX = branchX + 340f;
|
||||
const float mainValidateX = branchX + 700f;
|
||||
const float mainUseX = branchX + 1100f;
|
||||
const float branchX = -900f;
|
||||
const float rootRefreshX = branchX - 500f;
|
||||
const float sequenceX = branchX + 360f;
|
||||
const float actionX1 = sequenceX + 360f;
|
||||
const float actionX2 = actionX1 + 320f;
|
||||
const float actionX3 = actionX2 + 320f;
|
||||
const float actionX4 = actionX3 + 320f;
|
||||
const float actionX5 = actionX4 + 320f;
|
||||
const float actionX6 = actionX5 + 320f;
|
||||
const float truePortOffsetX = 203f;
|
||||
const float truePortOffsetY = 120f;
|
||||
const float falsePortOffsetX = -211f;
|
||||
const float falsePortOffsetY = 124f;
|
||||
const float startY = -700f;
|
||||
const float rootRefreshY = startY - 120f;
|
||||
const float stepY = 620f;
|
||||
const float startY = -1320f;
|
||||
const float stepY = 360f;
|
||||
|
||||
// 루프 시작마다 주 대상을 블랙보드에 동기화한 뒤 패턴 우선순위 체인으로 들어갑니다.
|
||||
object rootRefreshNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(RefreshPrimaryTargetAction), new Vector2(rootRefreshX, rootRefreshY));
|
||||
object phase2TransitionBranch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(branchX, startY));
|
||||
AttachConditionWithValue(phase2TransitionBranch, typeof(IsCurrentPhaseCondition), "Phase", 1, authoringAssembly);
|
||||
AttachConditionWithValue(phase2TransitionBranch, typeof(IsHealthBelowCondition), "HealthPercent", DefaultPhase2EnterHealthPercent, authoringAssembly);
|
||||
SetBranchRequiresAll(phase2TransitionBranch, true);
|
||||
|
||||
object phase2TransitionSequence = CreateNode(
|
||||
graphAsset,
|
||||
createNodeMethod,
|
||||
getNodeInfoMethod,
|
||||
runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true),
|
||||
new Vector2(sequenceX, startY));
|
||||
object phase2TransitionWaitNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(WaitAction), new Vector2(actionX1, startY));
|
||||
SetNodeFieldValue(phase2TransitionWaitNode, "Duration", DefaultPhaseTransitionLockDuration, setFieldValueMethod);
|
||||
object phase2SetPhaseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(SetBossPhaseAction), new Vector2(actionX2, startY));
|
||||
SetNodeFieldValue(phase2SetPhaseNode, "TargetPhase", 2, setFieldValueMethod);
|
||||
SetNodeFieldValue(phase2SetPhaseNode, "ResetTimer", true, setFieldValueMethod);
|
||||
ConnectChildren(graphAsset, connectEdgeMethod, phase2TransitionSequence, phase2TransitionWaitNode, phase2SetPhaseNode);
|
||||
|
||||
object phase3TransitionBranch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(branchX, startY + stepY));
|
||||
AttachConditionWithValue(phase3TransitionBranch, typeof(IsCurrentPhaseCondition), "Phase", 2, authoringAssembly);
|
||||
AttachConditionWithValue(phase3TransitionBranch, typeof(IsHealthBelowCondition), "HealthPercent", DefaultPhase3EnterHealthPercent, authoringAssembly);
|
||||
SetBranchRequiresAll(phase3TransitionBranch, true);
|
||||
|
||||
object phase3TransitionSequence = CreateNode(
|
||||
graphAsset,
|
||||
createNodeMethod,
|
||||
getNodeInfoMethod,
|
||||
runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true),
|
||||
new Vector2(sequenceX, startY + stepY));
|
||||
object phase3RoarNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UseSkillAction), new Vector2(actionX1, startY + stepY));
|
||||
SetNodeFieldValue(phase3RoarNode, "스킬", phase3TransitionSkill, setFieldValueMethod);
|
||||
object phase3TransitionWaitNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(WaitAction), new Vector2(actionX2, startY + stepY));
|
||||
SetNodeFieldValue(phase3TransitionWaitNode, "Duration", DefaultPhaseTransitionLockDuration, setFieldValueMethod);
|
||||
object phase3SetPhaseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(SetBossPhaseAction), new Vector2(actionX3, startY + stepY));
|
||||
SetNodeFieldValue(phase3SetPhaseNode, "TargetPhase", 3, setFieldValueMethod);
|
||||
SetNodeFieldValue(phase3SetPhaseNode, "ResetTimer", true, setFieldValueMethod);
|
||||
object phase3RefreshNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(RefreshPrimaryTargetAction), new Vector2(actionX4, startY + stepY));
|
||||
SetNodeFieldValue(phase3RefreshNode, "SearchRange", DefaultTargetSearchRange, setFieldValueMethod);
|
||||
object phase3ValidateNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ValidateTargetAction), new Vector2(actionX5, startY + stepY));
|
||||
object phase3UseSignatureNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UsePatternByRoleAction), new Vector2(actionX6, startY + stepY));
|
||||
SetNodeFieldValue(phase3UseSignatureNode, "Pattern", signaturePattern, setFieldValueMethod);
|
||||
SetNodeFieldValue(phase3UseSignatureNode, "ContinueOnResolvedFailure", true, setFieldValueMethod);
|
||||
object phase3SignatureResultBranch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(actionX6 + 420f, startY + stepY));
|
||||
AttachConditionWithValue(phase3SignatureResultBranch, typeof(IsPatternExecutionResultCondition), "Result", BossPatternExecutionResult.Failed, authoringAssembly);
|
||||
SetBranchRequiresAll(phase3SignatureResultBranch, true);
|
||||
object phase3SignatureStaggerNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(BossStaggerAction), new Vector2(actionX6 + 820f, startY + stepY - 120f));
|
||||
object phase3SignatureFailureNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(SignatureFailureEffectsAction), new Vector2(actionX6 + 820f, startY + stepY + 120f));
|
||||
object phase3SignatureTimerResetNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(SetBossPhaseAction), new Vector2(actionX6 + 1220f, startY + stepY));
|
||||
SetNodeFieldValue(phase3SignatureTimerResetNode, "TargetPhase", 3, setFieldValueMethod);
|
||||
SetNodeFieldValue(phase3SignatureTimerResetNode, "ResetTimer", true, setFieldValueMethod);
|
||||
ConnectChildren(
|
||||
graphAsset,
|
||||
connectEdgeMethod,
|
||||
phase3TransitionSequence,
|
||||
phase3RoarNode,
|
||||
phase3TransitionWaitNode,
|
||||
phase3SetPhaseNode,
|
||||
phase3RefreshNode,
|
||||
phase3ValidateNode,
|
||||
phase3UseSignatureNode,
|
||||
phase3SignatureResultBranch,
|
||||
phase3SignatureTimerResetNode);
|
||||
|
||||
object rootRefreshNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(RefreshPrimaryTargetAction), new Vector2(rootRefreshX, startY + stepY * 2f - 120f));
|
||||
SetNodeFieldValue(rootRefreshNode, "SearchRange", DefaultTargetSearchRange, setFieldValueMethod);
|
||||
|
||||
// #1 Punish — 밟기 (전제 조건: 다운된 대상이 반경 이내에 있어야 함)
|
||||
object downBranch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(branchX, startY));
|
||||
AttachPatternReadyCondition(downBranch, punishPattern, authoringAssembly);
|
||||
AttachConditionWithValue(downBranch, typeof(IsDownedTargetInRangeCondition), "SearchRadius", DefaultDownedTargetSearchRadius, authoringAssembly);
|
||||
SetBranchRequiresAll(downBranch, true);
|
||||
object punishBranch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(branchX, startY + stepY * 2f));
|
||||
AttachPatternReadyCondition(punishBranch, punishPattern, authoringAssembly);
|
||||
AttachConditionWithValue(punishBranch, typeof(IsDownedTargetInRangeCondition), "SearchRadius", DefaultDownedTargetSearchRadius, authoringAssembly);
|
||||
SetBranchRequiresAll(punishBranch, true);
|
||||
|
||||
object downSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod,
|
||||
object punishSequence = CreateNode(
|
||||
graphAsset,
|
||||
createNodeMethod,
|
||||
getNodeInfoMethod,
|
||||
runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true),
|
||||
new Vector2(mainSequenceX, startY));
|
||||
object downSelectNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(SelectNearestDownedTargetAction), new Vector2(mainValidateX, startY));
|
||||
SetNodeFieldValue(downSelectNode, "SearchRadius", DefaultDownedTargetSearchRadius, setFieldValueMethod);
|
||||
LinkTarget(downSelectNode, targetVariable);
|
||||
object downUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UsePatternByRoleAction), new Vector2(mainUseX, startY));
|
||||
SetNodeFieldValue(downUseNode, "Pattern", punishPattern, setFieldValueMethod);
|
||||
LinkTarget(downUseNode, targetVariable);
|
||||
ConnectChildren(graphAsset, connectEdgeMethod, downSequence, downSelectNode, downUseNode);
|
||||
new Vector2(sequenceX, startY + stepY * 2f));
|
||||
object punishSelectNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(SelectNearestDownedTargetAction), new Vector2(actionX1, startY + stepY * 2f));
|
||||
SetNodeFieldValue(punishSelectNode, "SearchRadius", DefaultDownedTargetSearchRadius, setFieldValueMethod);
|
||||
object punishUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UsePatternByRoleAction), new Vector2(actionX2, startY + stepY * 2f));
|
||||
SetNodeFieldValue(punishUseNode, "Pattern", punishPattern, setFieldValueMethod);
|
||||
ConnectChildren(graphAsset, connectEdgeMethod, punishSequence, punishSelectNode, punishUseNode);
|
||||
|
||||
// #2 Combo — 강타 계열 기본 루프
|
||||
object comboBranch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(branchX, startY + stepY));
|
||||
object comboRangeCondModel = AttachCondition(comboBranch, typeof(IsTargetInAttackRangeCondition), authoringAssembly);
|
||||
if (comboRangeCondModel != null)
|
||||
setFieldMethod.Invoke(comboRangeCondModel, new object[] { "Target", targetVariable, typeof(GameObject) });
|
||||
if (comboRangeCondModel != null)
|
||||
SetConditionFieldValue(comboRangeCondModel, "AttackRange", DefaultPrimaryBranchAttackRange);
|
||||
AttachPatternReadyCondition(comboBranch, comboPattern, authoringAssembly);
|
||||
SetBranchRequiresAll(comboBranch, true);
|
||||
object signatureBranch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(branchX, startY + stepY * 3f));
|
||||
AttachConditionWithValue(signatureBranch, typeof(IsCurrentPhaseCondition), "Phase", 3, authoringAssembly);
|
||||
AttachConditionWithValue(signatureBranch, typeof(IsPhaseElapsedTimeAboveCondition), "Seconds", DefaultSignatureRepeatInterval, authoringAssembly);
|
||||
AttachPatternReadyCondition(signatureBranch, signaturePattern, authoringAssembly);
|
||||
SetBranchRequiresAll(signatureBranch, true);
|
||||
|
||||
object comboSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod,
|
||||
object signatureSequence = CreateNode(
|
||||
graphAsset,
|
||||
createNodeMethod,
|
||||
getNodeInfoMethod,
|
||||
runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true),
|
||||
new Vector2(mainSequenceX, startY + stepY));
|
||||
object comboValidateNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ValidateTargetAction), new Vector2(mainValidateX, startY + stepY));
|
||||
LinkTarget(comboValidateNode, targetVariable);
|
||||
object comboUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UsePatternByRoleAction), new Vector2(mainUseX, startY + stepY));
|
||||
SetNodeFieldValue(comboUseNode, "Pattern", comboPattern, setFieldValueMethod);
|
||||
LinkTarget(comboUseNode, targetVariable);
|
||||
ConnectChildren(graphAsset, connectEdgeMethod, comboSequence, comboValidateNode, comboUseNode);
|
||||
ConnectBranch(graphAsset, connectEdgeMethod, comboBranch, "True", comboSequence);
|
||||
new Vector2(sequenceX, startY + stepY * 3f));
|
||||
object signatureRefreshNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(RefreshPrimaryTargetAction), new Vector2(actionX1, startY + stepY * 3f));
|
||||
SetNodeFieldValue(signatureRefreshNode, "SearchRange", DefaultTargetSearchRange, setFieldValueMethod);
|
||||
object signatureValidateNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ValidateTargetAction), new Vector2(actionX2, startY + stepY * 3f));
|
||||
object signatureUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UsePatternByRoleAction), new Vector2(actionX3, startY + stepY * 3f));
|
||||
SetNodeFieldValue(signatureUseNode, "Pattern", signaturePattern, setFieldValueMethod);
|
||||
SetNodeFieldValue(signatureUseNode, "ContinueOnResolvedFailure", true, setFieldValueMethod);
|
||||
object signatureResultBranch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(actionX4, startY + stepY * 3f));
|
||||
AttachConditionWithValue(signatureResultBranch, typeof(IsPatternExecutionResultCondition), "Result", BossPatternExecutionResult.Failed, authoringAssembly);
|
||||
SetBranchRequiresAll(signatureResultBranch, true);
|
||||
object signatureStaggerNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(BossStaggerAction), new Vector2(actionX4 + 420f, startY + stepY * 3f - 120f));
|
||||
object signatureFailureNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(SignatureFailureEffectsAction), new Vector2(actionX4 + 420f, startY + stepY * 3f + 120f));
|
||||
object signatureTimerResetNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(SetBossPhaseAction), new Vector2(actionX5, startY + stepY * 3f));
|
||||
SetNodeFieldValue(signatureTimerResetNode, "TargetPhase", 3, setFieldValueMethod);
|
||||
SetNodeFieldValue(signatureTimerResetNode, "ResetTimer", true, setFieldValueMethod);
|
||||
ConnectChildren(
|
||||
graphAsset,
|
||||
connectEdgeMethod,
|
||||
signatureSequence,
|
||||
signatureRefreshNode,
|
||||
signatureValidateNode,
|
||||
signatureUseNode,
|
||||
signatureResultBranch,
|
||||
signatureTimerResetNode);
|
||||
|
||||
// #3 Chase — fallback
|
||||
object chaseSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(branchX, startY + stepY * 2));
|
||||
object chaseRefreshNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(RefreshPrimaryTargetAction), new Vector2(branchX + 160f, startY + stepY * 2 + 80f));
|
||||
object throwBranch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(branchX, startY + stepY * 4f));
|
||||
AttachPatternReadyCondition(throwBranch, utilityPattern, authoringAssembly);
|
||||
AttachConditionWithValue(throwBranch, typeof(IsRecentReviveTriggerCondition), "MaxAge", DefaultThrowAvailabilityDelay, authoringAssembly);
|
||||
SetBranchRequiresAll(throwBranch, true);
|
||||
|
||||
object throwSequence = CreateNode(
|
||||
graphAsset,
|
||||
createNodeMethod,
|
||||
getNodeInfoMethod,
|
||||
runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true),
|
||||
new Vector2(sequenceX, startY + stepY * 4f));
|
||||
object throwSelectNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(SelectRecentReviveTargetAction), new Vector2(actionX1, startY + stepY * 4f));
|
||||
SetNodeFieldValue(throwSelectNode, "MaxAge", DefaultThrowAvailabilityDelay, setFieldValueMethod);
|
||||
SetNodeFieldValue(throwSelectNode, "PreferCaster", true, setFieldValueMethod);
|
||||
SetNodeFieldValue(throwSelectNode, "FallbackToRevivedTarget", true, setFieldValueMethod);
|
||||
object throwUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UsePatternByRoleAction), new Vector2(actionX2, startY + stepY * 4f));
|
||||
SetNodeFieldValue(throwUseNode, "Pattern", utilityPattern, setFieldValueMethod);
|
||||
ConnectChildren(graphAsset, connectEdgeMethod, throwSequence, throwSelectNode, throwUseNode);
|
||||
|
||||
object leapBranch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(branchX, startY + stepY * 5f));
|
||||
AttachPatternReadyCondition(leapBranch, mobilityPattern, authoringAssembly);
|
||||
AttachConditionWithValue(leapBranch, typeof(IsTargetBeyondDistanceCondition), "MinDistance", DefaultLeapTargetMinDistance, authoringAssembly);
|
||||
SetBranchRequiresAll(leapBranch, true);
|
||||
|
||||
object leapSequence = CreateNode(
|
||||
graphAsset,
|
||||
createNodeMethod,
|
||||
getNodeInfoMethod,
|
||||
runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true),
|
||||
new Vector2(sequenceX, startY + stepY * 5f));
|
||||
object leapSelectNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(SelectTargetByDistanceAction), new Vector2(actionX1, startY + stepY * 5f));
|
||||
SetNodeFieldValue(leapSelectNode, "MinRange", DefaultLeapTargetMinDistance, setFieldValueMethod);
|
||||
SetNodeFieldValue(leapSelectNode, "MaxRange", DefaultTargetSearchRange, setFieldValueMethod);
|
||||
SetNodeFieldValue(leapSelectNode, "SelectionMode", DistanceTargetSelectionMode.Farthest, setFieldValueMethod);
|
||||
object leapValidateNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ValidateTargetAction), new Vector2(actionX2, startY + stepY * 5f));
|
||||
object leapUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UsePatternByRoleAction), new Vector2(actionX3, startY + stepY * 5f));
|
||||
SetNodeFieldValue(leapUseNode, "Pattern", mobilityPattern, setFieldValueMethod);
|
||||
ConnectChildren(graphAsset, connectEdgeMethod, leapSequence, leapSelectNode, leapValidateNode, leapUseNode);
|
||||
|
||||
object groundShakeBranch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(branchX, startY + stepY * 6f));
|
||||
AttachConditionWithValue(groundShakeBranch, typeof(IsCurrentPhaseCondition), "Phase", 2, authoringAssembly);
|
||||
AttachConditionWithValue(groundShakeBranch, typeof(IsPhaseElapsedTimeAboveCondition), "Seconds", DefaultGroundShakeInterval, authoringAssembly);
|
||||
AttachPatternReadyCondition(groundShakeBranch, groundShakePattern, authoringAssembly);
|
||||
SetBranchRequiresAll(groundShakeBranch, true);
|
||||
|
||||
object groundShakeSequence = CreateNode(
|
||||
graphAsset,
|
||||
createNodeMethod,
|
||||
getNodeInfoMethod,
|
||||
runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true),
|
||||
new Vector2(sequenceX, startY + stepY * 6f));
|
||||
object groundShakeRefreshNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(RefreshPrimaryTargetAction), new Vector2(actionX1, startY + stepY * 6f));
|
||||
SetNodeFieldValue(groundShakeRefreshNode, "SearchRange", DefaultTargetSearchRange, setFieldValueMethod);
|
||||
object groundShakeValidateNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ValidateTargetAction), new Vector2(actionX2, startY + stepY * 6f));
|
||||
object groundShakeUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UsePatternByRoleAction), new Vector2(actionX3, startY + stepY * 6f));
|
||||
SetNodeFieldValue(groundShakeUseNode, "Pattern", groundShakePattern, setFieldValueMethod);
|
||||
object groundShakeTimerResetNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(SetBossPhaseAction), new Vector2(actionX4, startY + stepY * 6f));
|
||||
SetNodeFieldValue(groundShakeTimerResetNode, "TargetPhase", 2, setFieldValueMethod);
|
||||
SetNodeFieldValue(groundShakeTimerResetNode, "ResetTimer", true, setFieldValueMethod);
|
||||
ConnectChildren(graphAsset, connectEdgeMethod, groundShakeSequence, groundShakeRefreshNode, groundShakeValidateNode, groundShakeUseNode, groundShakeTimerResetNode);
|
||||
|
||||
object meleeBranch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(branchX, startY + stepY * 7f));
|
||||
object meleeRangeCondModel = AttachCondition(meleeBranch, typeof(IsTargetInAttackRangeCondition), authoringAssembly);
|
||||
if (meleeRangeCondModel != null)
|
||||
setFieldMethod.Invoke(meleeRangeCondModel, new object[] { "Target", targetVariable, typeof(GameObject) });
|
||||
if (meleeRangeCondModel != null)
|
||||
SetConditionFieldValue(meleeRangeCondModel, "AttackRange", DefaultPrimaryBranchAttackRange);
|
||||
object meleeReadyCondModel = AttachCondition(meleeBranch, typeof(HasAnyReadyPatternCondition), authoringAssembly);
|
||||
SetWeightedPatternFields(meleeReadyCondModel, setFieldMethod, comboPattern, DefaultComboPatternWeight, pressurePattern, DefaultPressurePatternWeight, primaryPattern, DefaultPrimaryPatternWeight, secondaryPattern, DefaultSecondaryPatternWeight, tertiaryPattern, DefaultTertiaryPatternWeight);
|
||||
SetBranchRequiresAll(meleeBranch, true);
|
||||
|
||||
object meleeSequence = CreateNode(
|
||||
graphAsset,
|
||||
createNodeMethod,
|
||||
getNodeInfoMethod,
|
||||
runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true),
|
||||
new Vector2(sequenceX, startY + stepY * 7f));
|
||||
object meleeValidateNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ValidateTargetAction), new Vector2(actionX1, startY + stepY * 7f));
|
||||
object meleeUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UseWeightedReadyPatternAction), new Vector2(actionX2, startY + stepY * 7f));
|
||||
SetWeightedPatternFields(meleeUseNode, setFieldValueMethod, comboPattern, DefaultComboPatternWeight, pressurePattern, DefaultPressurePatternWeight, primaryPattern, DefaultPrimaryPatternWeight, secondaryPattern, DefaultSecondaryPatternWeight, tertiaryPattern, DefaultTertiaryPatternWeight);
|
||||
ConnectChildren(graphAsset, connectEdgeMethod, meleeSequence, meleeValidateNode, meleeUseNode);
|
||||
|
||||
object chaseSequence = CreateNode(
|
||||
graphAsset,
|
||||
createNodeMethod,
|
||||
getNodeInfoMethod,
|
||||
runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true),
|
||||
new Vector2(branchX, startY + stepY * 11.5f));
|
||||
object chaseRefreshNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(RefreshPrimaryTargetAction), new Vector2(branchX + 160f, startY + stepY * 11.5f + 80f));
|
||||
SetNodeFieldValue(chaseRefreshNode, "SearchRange", DefaultTargetSearchRange, setFieldValueMethod);
|
||||
object chaseHasTargetNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ValidateTargetAction), new Vector2(branchX + 320f, startY + stepY * 2 + 80f));
|
||||
object chaseUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ChaseTargetAction), new Vector2(branchX + 480f, startY + stepY * 2 + 80f));
|
||||
object chaseHasTargetNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ValidateTargetAction), new Vector2(branchX + 480f, startY + stepY * 11.5f + 80f));
|
||||
object chaseUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ChaseTargetAction), new Vector2(branchX + 800f, startY + stepY * 11.5f + 80f));
|
||||
SetNodeFieldValue(chaseUseNode, "StopDistance", DefaultPrimaryBranchAttackRange, setFieldValueMethod);
|
||||
|
||||
Connect(graphAsset, connectEdgeMethod, GetDefaultOutputPort(rootRefreshNode), GetDefaultInputPort(downBranch));
|
||||
LinkTarget(rootRefreshNode, targetVariable);
|
||||
LinkTarget(phase3RefreshNode, targetVariable);
|
||||
LinkTarget(phase3ValidateNode, targetVariable);
|
||||
LinkTarget(phase3UseSignatureNode, targetVariable);
|
||||
LinkTarget(punishSelectNode, targetVariable);
|
||||
LinkTarget(punishUseNode, targetVariable);
|
||||
LinkTarget(signatureRefreshNode, targetVariable);
|
||||
LinkTarget(signatureValidateNode, targetVariable);
|
||||
LinkTarget(signatureUseNode, targetVariable);
|
||||
LinkTarget(throwSelectNode, targetVariable);
|
||||
LinkTarget(throwUseNode, targetVariable);
|
||||
LinkTarget(leapSelectNode, targetVariable);
|
||||
LinkTarget(leapValidateNode, targetVariable);
|
||||
LinkTarget(leapUseNode, targetVariable);
|
||||
LinkTarget(groundShakeRefreshNode, targetVariable);
|
||||
LinkTarget(groundShakeValidateNode, targetVariable);
|
||||
LinkTarget(groundShakeUseNode, targetVariable);
|
||||
LinkTarget(meleeValidateNode, targetVariable);
|
||||
LinkTarget(meleeUseNode, targetVariable);
|
||||
LinkTarget(chaseRefreshNode, targetVariable);
|
||||
LinkTarget(chaseHasTargetNode, targetVariable);
|
||||
LinkTarget(chaseUseNode, targetVariable);
|
||||
|
||||
Connect(graphAsset, connectEdgeMethod, GetDefaultOutputPort(rootRefreshNode), GetDefaultInputPort(punishBranch));
|
||||
|
||||
var allBranches = new List<object>
|
||||
{
|
||||
phase2TransitionBranch,
|
||||
phase3TransitionBranch,
|
||||
punishBranch,
|
||||
signatureBranch,
|
||||
throwBranch,
|
||||
leapBranch,
|
||||
groundShakeBranch,
|
||||
meleeBranch,
|
||||
phase3SignatureResultBranch,
|
||||
signatureResultBranch,
|
||||
};
|
||||
|
||||
// ── FloatingPortNodeModel 생성 + 위치 보정 ──
|
||||
// Branch 노드의 NamedPort(True/False)에 대해 FloatingPortNodeModel을 생성합니다.
|
||||
// CreateNodePortsForNode는 기본 위치(Branch + 200px Y)를 사용하므로, 생성 후 사용자 조정 기준 위치로 이동합니다.
|
||||
var allBranches = new List<object>();
|
||||
allBranches.AddRange(new[] { downBranch, comboBranch });
|
||||
foreach (object branch in allBranches)
|
||||
{
|
||||
createNodePortsMethod?.Invoke(graphAsset, new object[] { branch });
|
||||
}
|
||||
|
||||
// FloatingPortNodeModel 위치를 사용자 조정 기준으로 보정
|
||||
foreach (object branch in allBranches)
|
||||
{
|
||||
// Branch의 현재 위치 읽기 (Position은 public 필드)
|
||||
FieldInfo posField = branch.GetType().GetField("Position", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
if (posField == null) continue;
|
||||
Vector2 branchPos = (Vector2)posField.GetValue(branch);
|
||||
if (posField == null)
|
||||
continue;
|
||||
|
||||
// FloatingPortNodeModel에서 PortName이 "True"/"False"인 것을 찾아 위치 수정
|
||||
Vector2 branchPos = (Vector2)posField.GetValue(branch);
|
||||
SetFloatingPortPosition(graphAsset, branch, "True", branchPos.x + truePortOffsetX, branchPos.y + truePortOffsetY);
|
||||
SetFloatingPortPosition(graphAsset, branch, "False", branchPos.x + falsePortOffsetX, branchPos.y + falsePortOffsetY);
|
||||
}
|
||||
|
||||
// ── 연결 ──
|
||||
|
||||
// Start → Repeater → 주 대상 갱신
|
||||
Connect(graphAsset, connectEdgeMethod, GetDefaultOutputPort(startNode), GetDefaultInputPort(repeatNode));
|
||||
Connect(graphAsset, connectEdgeMethod, GetDefaultOutputPort(repeatNode), GetDefaultInputPort(rootRefreshNode));
|
||||
Connect(graphAsset, connectEdgeMethod, GetDefaultOutputPort(repeatNode), GetDefaultInputPort(phase2TransitionBranch));
|
||||
|
||||
// 각 Branch의 True FloatingPort → Action
|
||||
ConnectBranch(graphAsset, connectEdgeMethod, downBranch, "True", downSequence);
|
||||
ConnectBranch(graphAsset, connectEdgeMethod, comboBranch, "True", comboSequence);
|
||||
ConnectBranch(graphAsset, connectEdgeMethod, phase2TransitionBranch, "True", phase2TransitionSequence);
|
||||
ConnectBranch(graphAsset, connectEdgeMethod, phase2TransitionBranch, "False", phase3TransitionBranch);
|
||||
|
||||
// 각 Branch의 False FloatingPort → 다음 우선순위
|
||||
ConnectBranch(graphAsset, connectEdgeMethod, downBranch, "False", comboBranch);
|
||||
ConnectBranch(graphAsset, connectEdgeMethod, comboBranch, "False", chaseSequence);
|
||||
ConnectBranch(graphAsset, connectEdgeMethod, phase3TransitionBranch, "True", phase3TransitionSequence);
|
||||
ConnectBranch(graphAsset, connectEdgeMethod, phase3TransitionBranch, "False", rootRefreshNode);
|
||||
|
||||
ConnectBranch(graphAsset, connectEdgeMethod, phase3SignatureResultBranch, "True", phase3SignatureStaggerNode);
|
||||
ConnectBranch(graphAsset, connectEdgeMethod, phase3SignatureResultBranch, "False", phase3SignatureFailureNode);
|
||||
|
||||
ConnectBranch(graphAsset, connectEdgeMethod, signatureResultBranch, "True", signatureStaggerNode);
|
||||
ConnectBranch(graphAsset, connectEdgeMethod, signatureResultBranch, "False", signatureFailureNode);
|
||||
|
||||
ConnectBranch(graphAsset, connectEdgeMethod, punishBranch, "True", punishSequence);
|
||||
ConnectBranch(graphAsset, connectEdgeMethod, punishBranch, "False", signatureBranch);
|
||||
|
||||
ConnectBranch(graphAsset, connectEdgeMethod, signatureBranch, "True", signatureSequence);
|
||||
ConnectBranch(graphAsset, connectEdgeMethod, signatureBranch, "False", throwBranch);
|
||||
|
||||
ConnectBranch(graphAsset, connectEdgeMethod, throwBranch, "True", throwSequence);
|
||||
ConnectBranch(graphAsset, connectEdgeMethod, throwBranch, "False", leapBranch);
|
||||
|
||||
ConnectBranch(graphAsset, connectEdgeMethod, leapBranch, "True", leapSequence);
|
||||
ConnectBranch(graphAsset, connectEdgeMethod, leapBranch, "False", groundShakeBranch);
|
||||
|
||||
ConnectBranch(graphAsset, connectEdgeMethod, groundShakeBranch, "True", groundShakeSequence);
|
||||
ConnectBranch(graphAsset, connectEdgeMethod, groundShakeBranch, "False", meleeBranch);
|
||||
|
||||
ConnectBranch(graphAsset, connectEdgeMethod, meleeBranch, "True", meleeSequence);
|
||||
ConnectBranch(graphAsset, connectEdgeMethod, meleeBranch, "False", chaseSequence);
|
||||
|
||||
// Chase Sequence 자식 연결
|
||||
ConnectChildren(graphAsset, connectEdgeMethod, chaseSequence, chaseRefreshNode, chaseHasTargetNode, chaseUseNode);
|
||||
|
||||
// Chase/루트 노드 블랙보드 변수 연결
|
||||
LinkTarget(rootRefreshNode, targetVariable);
|
||||
LinkTarget(chaseRefreshNode, targetVariable);
|
||||
LinkTarget(chaseHasTargetNode, targetVariable);
|
||||
LinkTarget(chaseUseNode, targetVariable);
|
||||
|
||||
// 저장
|
||||
SetStartRepeatFlags(startNode, repeat: true, allowMultipleRepeatsPerTick: false);
|
||||
setAssetDirtyMethod.Invoke(graphAsset, new object[] { true });
|
||||
@@ -1101,6 +1333,57 @@ namespace Colosseum.Editor
|
||||
closedMethod.Invoke(condModel, new object[] { "Pattern", pattern });
|
||||
}
|
||||
|
||||
private static void SetWeightedPatternFields(
|
||||
object nodeOrCondition,
|
||||
MethodInfo setFieldMethod,
|
||||
BossPatternData pattern1,
|
||||
float weight1,
|
||||
BossPatternData pattern2,
|
||||
float weight2,
|
||||
BossPatternData pattern3,
|
||||
float weight3,
|
||||
BossPatternData pattern4,
|
||||
float weight4,
|
||||
BossPatternData pattern5,
|
||||
float weight5)
|
||||
{
|
||||
if (nodeOrCondition == null)
|
||||
return;
|
||||
|
||||
MethodInfo genericSetFieldMethod = setFieldMethod;
|
||||
if (genericSetFieldMethod == null || !genericSetFieldMethod.IsGenericMethod)
|
||||
{
|
||||
genericSetFieldMethod = nodeOrCondition.GetType().GetMethods(BindingFlags.Instance | BindingFlags.NonPublic)
|
||||
.FirstOrDefault(method => method.Name == "SetField" && method.IsGenericMethod && method.GetParameters().Length == 2);
|
||||
}
|
||||
|
||||
if (genericSetFieldMethod == null)
|
||||
{
|
||||
Debug.LogError("[DrogBTRebuild] 가중치 패턴 필드 설정용 SetField<T>를 찾지 못했습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
SetFieldValue(nodeOrCondition, genericSetFieldMethod, "Pattern1", pattern1);
|
||||
SetFieldValue(nodeOrCondition, genericSetFieldMethod, "Weight1", weight1);
|
||||
SetFieldValue(nodeOrCondition, genericSetFieldMethod, "Pattern2", pattern2);
|
||||
SetFieldValue(nodeOrCondition, genericSetFieldMethod, "Weight2", weight2);
|
||||
SetFieldValue(nodeOrCondition, genericSetFieldMethod, "Pattern3", pattern3);
|
||||
SetFieldValue(nodeOrCondition, genericSetFieldMethod, "Weight3", weight3);
|
||||
SetFieldValue(nodeOrCondition, genericSetFieldMethod, "Pattern4", pattern4);
|
||||
SetFieldValue(nodeOrCondition, genericSetFieldMethod, "Weight4", weight4);
|
||||
SetFieldValue(nodeOrCondition, genericSetFieldMethod, "Pattern5", pattern5);
|
||||
SetFieldValue(nodeOrCondition, genericSetFieldMethod, "Weight5", weight5);
|
||||
}
|
||||
|
||||
private static void SetFieldValue(object target, MethodInfo genericSetFieldMethod, string fieldName, object value)
|
||||
{
|
||||
if (target == null || genericSetFieldMethod == null || value == null)
|
||||
return;
|
||||
|
||||
MethodInfo closedMethod = genericSetFieldMethod.MakeGenericMethod(value.GetType());
|
||||
closedMethod.Invoke(target, new[] { fieldName, value });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 패턴의 MinPhase가 1보다 큰 경우, Branch에 IsMinPhaseSatisfiedCondition을 부착합니다.
|
||||
/// Phase 진입 조건을 BT에서 시각적으로 확인할 수 있습니다.
|
||||
|
||||
@@ -77,10 +77,13 @@ namespace Colosseum.Editor
|
||||
AnimationClip comboStompHit1Clip = EnsureClipFromSource($"{AnimationsFolder}/Anim_Drog_콤보-발구르기_1_0.anim", ZweihanderAttack013SourcePath, "Zweihander_Attack01_3_Root");
|
||||
AnimationClip comboStompHit2Clip = EnsureClipFromSource($"{AnimationsFolder}/Anim_Drog_콤보-발구르기_2_0.anim", HeavyCombo01CSourcePath, "A_MOD_SWD_Attack_HeavyCombo01C_RM_Neut");
|
||||
AnimationClip stompClip = EnsureClipFromSource($"{AnimationsFolder}/Anim_Drog_발구르기_0.anim", $"{AnimationsFolder}/Anim_Drog_발구르기_0.anim");
|
||||
AnimationClip groundShakeClip = EnsureClipFromSource($"{AnimationsFolder}/Anim_Drog_땅 울리기_0.anim", $"{AnimationsFolder}/Anim_Drog_강타R_0.anim");
|
||||
AnimationClip leapPrepareClip = EnsurePlaceholderClip($"{AnimationsFolder}/Anim_Drog_도약_준비_0.anim");
|
||||
AnimationClip leapAirClip = EnsurePlaceholderClip($"{AnimationsFolder}/Anim_Drog_도약_공중_0.anim");
|
||||
AnimationClip leapLandingClip = EnsurePlaceholderClip($"{AnimationsFolder}/Anim_Drog_도약_착지_0.anim");
|
||||
AnimationClip stepClip = EnsurePlaceholderClip($"{AnimationsFolder}/Anim_Drog_밟기_0.anim");
|
||||
SetSingleOnEffectEvent(stompClip, 0.80f);
|
||||
SetSingleOnEffectEvent(groundShakeClip, 0.95f);
|
||||
SetSingleOnEffectEvent(stepClip, 0.80f);
|
||||
AnimationClip throwClip = EnsurePlaceholderClip($"{AnimationsFolder}/Anim_Drog_투척_0.anim");
|
||||
AnimationClip roarClip = EnsurePlaceholderClip($"{AnimationsFolder}/Anim_Drog_포효_0.anim");
|
||||
@@ -267,6 +270,28 @@ namespace Colosseum.Editor
|
||||
180f,
|
||||
AreaCenterType.Caster);
|
||||
|
||||
DamageEffect groundShakeDamage = CreateDamageEffect(
|
||||
$"{EffectsFolder}/Data_SkillEffect_Drog_땅 울리기_0_데미지.asset",
|
||||
36f,
|
||||
DamageType.Physical,
|
||||
0.95f,
|
||||
AreaShapeType.Sphere,
|
||||
6.5f,
|
||||
1f,
|
||||
6.5f,
|
||||
180f,
|
||||
AreaCenterType.Caster);
|
||||
|
||||
DownEffect groundShakeDown = CreateDownEffect(
|
||||
$"{EffectsFolder}/Data_SkillEffect_Drog_땅 울리기_1_다운.asset",
|
||||
2.2f,
|
||||
AreaShapeType.Sphere,
|
||||
3.4f,
|
||||
1f,
|
||||
3.4f,
|
||||
180f,
|
||||
AreaCenterType.Caster);
|
||||
|
||||
DamageEffect leapLandingDamage = CreateDamageEffect(
|
||||
$"{EffectsFolder}/Data_SkillEffect_Drog_도약_착지_0_데미지.asset",
|
||||
34f,
|
||||
@@ -526,6 +551,19 @@ namespace Colosseum.Editor
|
||||
stompDamage,
|
||||
stompStagger);
|
||||
|
||||
SkillData groundShakeSkill = CreateSkill(
|
||||
$"{SkillsFolder}/Data_Skill_Drog_땅 울리기.asset",
|
||||
"땅 울리기",
|
||||
"Phase 2 중반 압박 전환을 선언하는 광역 내려찍기입니다.",
|
||||
new[] { groundShakeClip },
|
||||
1f,
|
||||
SkillCastTargetTrackingMode.FaceTarget,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
groundShakeDamage,
|
||||
groundShakeDown);
|
||||
|
||||
SkillData leapPrepareSkill = CreateSkill(
|
||||
$"{SkillsFolder}/Data_Skill_Drog_도약_준비.asset",
|
||||
"도약 준비",
|
||||
@@ -722,10 +760,22 @@ namespace Colosseum.Editor
|
||||
false,
|
||||
TargetResolveMode.HighestThreat,
|
||||
2.5f,
|
||||
1,
|
||||
2,
|
||||
false,
|
||||
PatternStepDefinition.CreateSkillStep(stepSkill));
|
||||
|
||||
CreatePattern(
|
||||
$"{PatternsFolder}/Data_Pattern_Drog_땅 울리기.asset",
|
||||
"땅 울리기",
|
||||
PatternCategory.Big,
|
||||
false,
|
||||
false,
|
||||
TargetResolveMode.HighestThreat,
|
||||
30f,
|
||||
2,
|
||||
false,
|
||||
PatternStepDefinition.CreateSkillStep(groundShakeSkill));
|
||||
|
||||
CreatePattern(
|
||||
$"{PatternsFolder}/Data_Pattern_Drog_도약.asset",
|
||||
"도약",
|
||||
@@ -761,7 +811,7 @@ namespace Colosseum.Editor
|
||||
true,
|
||||
false,
|
||||
TargetResolveMode.HighestThreat,
|
||||
45f,
|
||||
90f,
|
||||
3,
|
||||
false,
|
||||
PatternStepDefinition.CreateSkillStep(executionReadySkill),
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Collections.Generic;
|
||||
|
||||
using Colosseum.AI;
|
||||
using Colosseum.Abnormalities;
|
||||
using Colosseum.Player;
|
||||
using Colosseum.Skills;
|
||||
|
||||
using Unity.Behavior;
|
||||
@@ -32,6 +33,8 @@ namespace Colosseum.Enemy
|
||||
[RequireComponent(typeof(SkillController))]
|
||||
public class BossBehaviorRuntimeState : NetworkBehaviour
|
||||
{
|
||||
public static event System.Action<GameObject, GameObject> PlayerRevivedBySkill;
|
||||
|
||||
[Header("References")]
|
||||
[SerializeField] protected BossEnemy bossEnemy;
|
||||
[SerializeField] protected EnemyBase enemyBase;
|
||||
@@ -92,6 +95,9 @@ namespace Colosseum.Enemy
|
||||
protected float currentPhaseStartTime;
|
||||
protected BossPatternExecutionResult lastPatternExecutionResult;
|
||||
protected BossPatternData lastExecutedPattern;
|
||||
protected GameObject lastReviveCaster;
|
||||
protected GameObject lastRevivedTarget;
|
||||
protected float lastReviveEventTime = float.NegativeInfinity;
|
||||
|
||||
/// <summary>
|
||||
/// 현재 전투 대상
|
||||
@@ -202,7 +208,12 @@ namespace Colosseum.Enemy
|
||||
ResetPhaseState();
|
||||
|
||||
if (!IsServer)
|
||||
{
|
||||
enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
PlayerRevivedBySkill += HandlePlayerRevivedBySkill;
|
||||
}
|
||||
|
||||
protected virtual void Update()
|
||||
@@ -267,6 +278,41 @@ namespace Colosseum.Enemy
|
||||
lastPatternExecutionResult = result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 부활 스킬 사용 사실을 보스 AI에 알립니다.
|
||||
/// </summary>
|
||||
public static void ReportPlayerRevivedBySkill(GameObject caster, GameObject revivedTarget)
|
||||
{
|
||||
PlayerRevivedBySkill?.Invoke(caster, revivedTarget);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 최근 부활 트리거가 아직 유효한지 확인합니다.
|
||||
/// </summary>
|
||||
public bool HasRecentReviveTrigger(float maxAge)
|
||||
{
|
||||
return ResolveRecentReviveTriggerTarget(maxAge) != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 최근 부활 트리거에서 우선 공격할 대상을 반환합니다.
|
||||
/// </summary>
|
||||
public GameObject ResolveRecentReviveTriggerTarget(float maxAge, bool preferCaster = true, bool fallbackToRevivedTarget = true)
|
||||
{
|
||||
if (Time.time - lastReviveEventTime > Mathf.Max(0f, maxAge))
|
||||
return null;
|
||||
|
||||
GameObject preferredTarget = preferCaster ? lastReviveCaster : lastRevivedTarget;
|
||||
if (IsValidReviveTriggerTarget(preferredTarget))
|
||||
return preferredTarget;
|
||||
|
||||
if (!fallbackToRevivedTarget)
|
||||
return null;
|
||||
|
||||
GameObject fallbackTarget = preferCaster ? lastRevivedTarget : lastReviveCaster;
|
||||
return IsValidReviveTriggerTarget(fallbackTarget) ? fallbackTarget : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 페이즈 커스텀 조건을 기록합니다.
|
||||
/// </summary>
|
||||
@@ -391,14 +437,41 @@ namespace Colosseum.Enemy
|
||||
currentPhaseStartTime = Time.time;
|
||||
lastPatternExecutionResult = BossPatternExecutionResult.None;
|
||||
lastExecutedPattern = null;
|
||||
lastReviveCaster = null;
|
||||
lastRevivedTarget = null;
|
||||
lastReviveEventTime = float.NegativeInfinity;
|
||||
customPhaseConditions.Clear();
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
if (IsServer)
|
||||
PlayerRevivedBySkill -= HandlePlayerRevivedBySkill;
|
||||
|
||||
base.OnNetworkDespawn();
|
||||
}
|
||||
|
||||
protected void HandlePlayerRevivedBySkill(GameObject caster, GameObject revivedTarget)
|
||||
{
|
||||
if (!IsServer)
|
||||
return;
|
||||
|
||||
lastReviveCaster = caster;
|
||||
lastRevivedTarget = revivedTarget;
|
||||
lastReviveEventTime = Time.time;
|
||||
|
||||
LogDebug(nameof(BossBehaviorRuntimeState), $"부활 트리거 기록: 시전자={caster?.name ?? "없음"} / 대상={revivedTarget?.name ?? "없음"}");
|
||||
}
|
||||
|
||||
protected bool IsValidReviveTriggerTarget(GameObject candidate)
|
||||
{
|
||||
if (candidate == null || !candidate.activeInHierarchy)
|
||||
return false;
|
||||
|
||||
PlayerNetworkController player = candidate.GetComponent<PlayerNetworkController>();
|
||||
return player != null && !player.IsDead;
|
||||
}
|
||||
|
||||
private static bool HasAnimatorParameter(Animator animator, string parameterName, AnimatorControllerParameterType parameterType)
|
||||
{
|
||||
if (animator == null || string.IsNullOrEmpty(parameterName))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using UnityEngine;
|
||||
|
||||
using Colosseum.Enemy;
|
||||
using Colosseum.Player;
|
||||
|
||||
namespace Colosseum.Skills.Effects
|
||||
@@ -44,6 +45,7 @@ namespace Colosseum.Skills.Effects
|
||||
}
|
||||
|
||||
networkController.Revive(healthPercent);
|
||||
BossBehaviorRuntimeState.ReportPlayerRevivedBySkill(caster, target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
95
Assets/_Game/Tests/Editor/WeightedPatternSelectorTests.cs
Normal file
95
Assets/_Game/Tests/Editor/WeightedPatternSelectorTests.cs
Normal file
@@ -0,0 +1,95 @@
|
||||
using NUnit.Framework;
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
using Colosseum.AI;
|
||||
|
||||
namespace Colosseum.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// 가중치 패턴 선택기의 후보 필터링과 가중치 분배를 검증합니다.
|
||||
/// </summary>
|
||||
public class WeightedPatternSelectorTests
|
||||
{
|
||||
[Test]
|
||||
public void TrySelectPattern_ReadyCandidatesOnly_AppliesWeightsAfterFiltering()
|
||||
{
|
||||
BossPatternData first = ScriptableObject.CreateInstance<BossPatternData>();
|
||||
BossPatternData second = ScriptableObject.CreateInstance<BossPatternData>();
|
||||
BossPatternData third = ScriptableObject.CreateInstance<BossPatternData>();
|
||||
|
||||
try
|
||||
{
|
||||
WeightedPatternCandidate[] candidates =
|
||||
{
|
||||
new WeightedPatternCandidate(first, 50f),
|
||||
new WeightedPatternCandidate(second, 30f),
|
||||
new WeightedPatternCandidate(third, 20f),
|
||||
};
|
||||
|
||||
bool result = WeightedPatternSelector.TrySelectPattern(
|
||||
candidates,
|
||||
pattern => pattern != first,
|
||||
0.1f,
|
||||
out BossPatternData selectedPattern);
|
||||
|
||||
Assert.IsTrue(result, "준비된 후보가 있는데 선택에 실패했습니다.");
|
||||
Assert.AreSame(second, selectedPattern, "준비되지 않은 후보를 제외한 뒤 첫 번째 가중치 후보가 선택되어야 합니다.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Object.DestroyImmediate(first);
|
||||
Object.DestroyImmediate(second);
|
||||
Object.DestroyImmediate(third);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TrySelectPattern_UsesDeclaredWeightBands()
|
||||
{
|
||||
BossPatternData combo = ScriptableObject.CreateInstance<BossPatternData>();
|
||||
BossPatternData pressure = ScriptableObject.CreateInstance<BossPatternData>();
|
||||
BossPatternData tertiary = ScriptableObject.CreateInstance<BossPatternData>();
|
||||
BossPatternData secondary = ScriptableObject.CreateInstance<BossPatternData>();
|
||||
BossPatternData primary = ScriptableObject.CreateInstance<BossPatternData>();
|
||||
|
||||
try
|
||||
{
|
||||
WeightedPatternCandidate[] candidates =
|
||||
{
|
||||
new WeightedPatternCandidate(combo, 26f),
|
||||
new WeightedPatternCandidate(pressure, 24f),
|
||||
new WeightedPatternCandidate(primary, 22f),
|
||||
new WeightedPatternCandidate(secondary, 16f),
|
||||
new WeightedPatternCandidate(tertiary, 12f),
|
||||
};
|
||||
|
||||
Assert.AreSame(combo, Select(candidates, 0.20f));
|
||||
Assert.AreSame(pressure, Select(candidates, 0.40f));
|
||||
Assert.AreSame(primary, Select(candidates, 0.60f));
|
||||
Assert.AreSame(secondary, Select(candidates, 0.80f));
|
||||
Assert.AreSame(tertiary, Select(candidates, 0.95f));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Object.DestroyImmediate(combo);
|
||||
Object.DestroyImmediate(pressure);
|
||||
Object.DestroyImmediate(tertiary);
|
||||
Object.DestroyImmediate(secondary);
|
||||
Object.DestroyImmediate(primary);
|
||||
}
|
||||
}
|
||||
|
||||
private static BossPatternData Select(WeightedPatternCandidate[] candidates, float roll)
|
||||
{
|
||||
bool result = WeightedPatternSelector.TrySelectPattern(
|
||||
candidates,
|
||||
pattern => pattern != null,
|
||||
roll,
|
||||
out BossPatternData selectedPattern);
|
||||
|
||||
Assert.IsTrue(result, "테스트용 선택이 실패했습니다.");
|
||||
return selectedPattern;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 53d2da8fc7a93aedeb713c62a3a58ffa
|
||||
625
Assets/_Game/Tests/PlayMode/DrogBehaviorTreeTests.cs
Normal file
625
Assets/_Game/Tests/PlayMode/DrogBehaviorTreeTests.cs
Normal file
@@ -0,0 +1,625 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
|
||||
using NUnit.Framework;
|
||||
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngine.TestTools;
|
||||
|
||||
using Unity.Netcode;
|
||||
|
||||
using Colosseum.AI;
|
||||
using Colosseum.Enemy;
|
||||
using Colosseum.Player;
|
||||
using Colosseum.Skills;
|
||||
|
||||
namespace Colosseum.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// 드로그 BT가 사양대로 분기되는지 검증하는 PlayMode 테스트입니다.
|
||||
/// </summary>
|
||||
public class DrogBehaviorTreeTests
|
||||
{
|
||||
private const string DrogSceneName = "Drog";
|
||||
|
||||
private const string PatternPunish = "밟기";
|
||||
private const string PatternSignature = "집행";
|
||||
private const string PatternGroundShake = "땅 울리기";
|
||||
private const string PatternLeap = "도약";
|
||||
private const string PatternThrow = "투척";
|
||||
private const string PatternCombo = "콤보-강타";
|
||||
private const string PatternPressure = "콤보-발구르기";
|
||||
private const string PatternTertiary = "콤보-기본기3";
|
||||
private const string PatternSecondary = "콤보-기본기2";
|
||||
private const string PatternPrimary = "콤보-기본기1";
|
||||
|
||||
private const float DefaultTimeout = 12f;
|
||||
private const float LongPatternTimeout = 18f;
|
||||
private const float TransitionDelayBuffer = 8f;
|
||||
|
||||
private BossEnemy boss;
|
||||
private BossBehaviorRuntimeState runtimeState;
|
||||
private SkillController bossSkillController;
|
||||
private Behaviour behaviorGraphAgent;
|
||||
private PlayerNetworkController player;
|
||||
private HitReactionController hitReactionController;
|
||||
private PlayerMovement playerMovement;
|
||||
private readonly List<string> capturedLogs = new List<string>();
|
||||
|
||||
[UnitySetUp]
|
||||
public IEnumerator SetUp()
|
||||
{
|
||||
Application.logMessageReceived += HandleLogMessage;
|
||||
yield return LoadScene(DrogSceneName);
|
||||
yield return WaitForCondition(
|
||||
() => NetworkManager.Singleton != null,
|
||||
5f,
|
||||
"Drog 씬에서 NetworkManager를 찾지 못했습니다.");
|
||||
|
||||
if (!NetworkManager.Singleton.IsListening)
|
||||
{
|
||||
Assert.IsTrue(NetworkManager.Singleton.StartHost(), "테스트용 호스트 시작에 실패했습니다.");
|
||||
}
|
||||
|
||||
yield return WaitForCondition(
|
||||
() => NetworkManager.Singleton.IsListening && NetworkManager.Singleton.IsHost,
|
||||
5f,
|
||||
"호스트가 시작되지 않았습니다.");
|
||||
|
||||
yield return WaitForCondition(
|
||||
() => BossEnemy.ActiveBoss != null || UnityEngine.Object.FindFirstObjectByType<BossEnemy>() != null,
|
||||
5f,
|
||||
"드로그 보스를 찾지 못했습니다.");
|
||||
|
||||
yield return WaitForCondition(
|
||||
() => FindPrimaryPlayer() != null,
|
||||
5f,
|
||||
"플레이어를 찾지 못했습니다.");
|
||||
|
||||
boss = BossEnemy.ActiveBoss != null
|
||||
? BossEnemy.ActiveBoss
|
||||
: UnityEngine.Object.FindFirstObjectByType<BossEnemy>();
|
||||
runtimeState = boss != null ? boss.GetComponent<BossBehaviorRuntimeState>() : null;
|
||||
bossSkillController = boss != null ? boss.GetComponent<SkillController>() : null;
|
||||
behaviorGraphAgent = boss != null ? boss.GetComponent("BehaviorGraphAgent") as Behaviour : null;
|
||||
player = FindPrimaryPlayer();
|
||||
hitReactionController = player != null ? player.GetComponent<HitReactionController>() : null;
|
||||
playerMovement = player != null ? player.GetComponent<PlayerMovement>() : null;
|
||||
|
||||
Assert.NotNull(boss, "보스를 찾지 못했습니다.");
|
||||
Assert.NotNull(runtimeState, "BossBehaviorRuntimeState를 찾지 못했습니다.");
|
||||
Assert.NotNull(bossSkillController, "보스 SkillController를 찾지 못했습니다.");
|
||||
Assert.NotNull(behaviorGraphAgent, "BehaviorGraphAgent를 찾지 못했습니다.");
|
||||
Assert.NotNull(player, "플레이어를 찾지 못했습니다.");
|
||||
Assert.NotNull(hitReactionController, "플레이어 HitReactionController를 찾지 못했습니다.");
|
||||
Assert.NotNull(playerMovement, "플레이어 PlayerMovement를 찾지 못했습니다.");
|
||||
|
||||
yield return ResetBossAndPlayerState(phase: 1, phaseElapsedTime: 0f, basicLoopCount: 0);
|
||||
}
|
||||
|
||||
[UnityTearDown]
|
||||
public IEnumerator TearDown()
|
||||
{
|
||||
LogAssert.ignoreFailingMessages = true;
|
||||
|
||||
if (behaviorGraphAgent != null)
|
||||
behaviorGraphAgent.enabled = false;
|
||||
|
||||
if (bossSkillController != null)
|
||||
bossSkillController.CancelSkill();
|
||||
|
||||
if (boss != null && boss.TryGetComponent(out UnityEngine.AI.NavMeshAgent navMeshAgent))
|
||||
navMeshAgent.enabled = false;
|
||||
|
||||
if (NetworkManager.Singleton != null && NetworkManager.Singleton.IsListening)
|
||||
{
|
||||
NetworkManager.Singleton.Shutdown();
|
||||
}
|
||||
|
||||
yield return null;
|
||||
|
||||
string activeSceneName = SceneManager.GetActiveScene().name;
|
||||
if (!string.IsNullOrEmpty(activeSceneName) && SceneManager.GetSceneByName(activeSceneName).isLoaded)
|
||||
{
|
||||
Scene cleanupScene = SceneManager.CreateScene($"DrogBehaviorTreeTests_Cleanup_{Guid.NewGuid():N}");
|
||||
SceneManager.SetActiveScene(cleanupScene);
|
||||
AsyncOperation unloadOperation = SceneManager.UnloadSceneAsync(activeSceneName);
|
||||
if (unloadOperation != null)
|
||||
{
|
||||
yield return unloadOperation;
|
||||
}
|
||||
}
|
||||
|
||||
Application.logMessageReceived -= HandleLogMessage;
|
||||
boss = null;
|
||||
runtimeState = null;
|
||||
bossSkillController = null;
|
||||
behaviorGraphAgent = null;
|
||||
player = null;
|
||||
hitReactionController = null;
|
||||
playerMovement = null;
|
||||
capturedLogs.Clear();
|
||||
LogAssert.ignoreFailingMessages = false;
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator Phase2_HealthThreshold_TransitionsWithinDelay()
|
||||
{
|
||||
yield return ResetBossAndPlayerState(phase: 1, phaseElapsedTime: 0f, basicLoopCount: 0);
|
||||
SetBossHealthPercent(0.74f);
|
||||
ResumeBehavior();
|
||||
|
||||
yield return WaitForCondition(
|
||||
() => runtimeState.CurrentPatternPhase == 2,
|
||||
TransitionDelayBuffer,
|
||||
"HP 74%에서 Phase 2 전환이 발생하지 않았습니다.");
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator Phase3_Transition_LeadsIntoImmediateSignature()
|
||||
{
|
||||
yield return ResetBossAndPlayerState(phase: 2, phaseElapsedTime: 0f, basicLoopCount: 0);
|
||||
SetPlayerOffset(2.2f);
|
||||
SetBossHealthPercent(0.39f);
|
||||
ResumeBehavior();
|
||||
|
||||
yield return WaitForCondition(
|
||||
() => runtimeState.CurrentPatternPhase == 3,
|
||||
TransitionDelayBuffer,
|
||||
"HP 39%에서 Phase 3 전환이 발생하지 않았습니다.");
|
||||
|
||||
yield return WaitForPatternLog(
|
||||
PatternSignature,
|
||||
TransitionDelayBuffer,
|
||||
"Phase 3 전환 직후 집행 패턴이 실행되지 않았습니다.");
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator Phase2_DownedNearbyTarget_PrioritizesPunish()
|
||||
{
|
||||
yield return ResetBossAndPlayerState(phase: 2, phaseElapsedTime: 0f, basicLoopCount: 0);
|
||||
SetPlayerOffset(1.6f);
|
||||
hitReactionController.ApplyDown(5f);
|
||||
yield return null;
|
||||
ResumeBehavior();
|
||||
|
||||
Assert.IsTrue(hitReactionController.IsDowned, "테스트용 다운 적용에 실패했습니다.");
|
||||
|
||||
yield return WaitForPatternLog(
|
||||
PatternPunish,
|
||||
DefaultTimeout,
|
||||
"가까운 다운 대상이 있어도 밟기 패턴이 선택되지 않았습니다.");
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator Phase2_RecentReviveTrigger_PrioritizesThrow()
|
||||
{
|
||||
yield return ResetBossAndPlayerState(phase: 2, phaseElapsedTime: 0f, basicLoopCount: 0);
|
||||
SetPlayerOffset(4f);
|
||||
|
||||
BossBehaviorRuntimeState.ReportPlayerRevivedBySkill(player.gameObject, player.gameObject);
|
||||
yield return null;
|
||||
ResumeBehavior();
|
||||
|
||||
Assert.IsTrue(runtimeState.HasRecentReviveTrigger(4f), "부활 트리거 기록이 유지되지 않았습니다.");
|
||||
|
||||
yield return WaitForPatternLog(
|
||||
PatternThrow,
|
||||
DefaultTimeout,
|
||||
"최근 부활 트리거가 있어도 투척 패턴이 선택되지 않았습니다.");
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator Phase2_AfterNamedPatternInterval_UsesGroundShake()
|
||||
{
|
||||
yield return ResetBossAndPlayerState(phase: 2, phaseElapsedTime: 13f, basicLoopCount: 2);
|
||||
SetPlayerOffset(2.4f);
|
||||
ResumeBehavior();
|
||||
|
||||
yield return WaitForPatternLog(
|
||||
PatternGroundShake,
|
||||
DefaultTimeout,
|
||||
"Phase 2에서 주기 도래 후 땅 울리기 패턴이 실행되지 않았습니다.");
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
[Ignore("원거리 9.26m에서도 콤보-강타가 먼저 실행되어 도약 분기 재현이 불안정합니다. 별도 조사 필요.")]
|
||||
public IEnumerator Phase2_FarTarget_PrioritizesLeap()
|
||||
{
|
||||
yield return ResetBossAndPlayerState(phase: 2, phaseElapsedTime: 0f, basicLoopCount: 2);
|
||||
SetPlayerOffset(9.5f);
|
||||
ResumeBehavior();
|
||||
|
||||
yield return WaitForPatternLog(
|
||||
PatternLeap,
|
||||
DefaultTimeout,
|
||||
"원거리 대상 상황에서 도약 패턴이 선택되지 않았습니다.");
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator Phase3_DoesNotUseGroundShakeAsIndependentPattern()
|
||||
{
|
||||
yield return ResetBossAndPlayerState(phase: 3, phaseElapsedTime: 13f, basicLoopCount: 2);
|
||||
SetPlayerOffset(2.4f);
|
||||
ResumeBehavior();
|
||||
|
||||
yield return new WaitForSeconds(4f);
|
||||
|
||||
Assert.IsFalse(
|
||||
HasPatternLog(PatternGroundShake),
|
||||
"Phase 3에서 땅 울리기가 독립 네임드 패턴으로 실행되었습니다.");
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator Phase1_MeleeBaseline_StartsWithWeightedMeleePattern()
|
||||
{
|
||||
yield return ResetBossAndPlayerState(phase: 1, phaseElapsedTime: 0f, basicLoopCount: 0);
|
||||
SetPlayerOffset(2f);
|
||||
ResumeBehavior();
|
||||
|
||||
yield return WaitForAnyPatternLog(
|
||||
new[] { PatternCombo, PatternPressure, PatternTertiary, PatternSecondary, PatternPrimary },
|
||||
DefaultTimeout,
|
||||
"근접 기본 루프에서 가중치 기본기 패턴이 실행되지 않았습니다.");
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator Phase1_MeleeWeightedSelector_SkipsPatternsOnCooldown()
|
||||
{
|
||||
yield return ResetBossAndPlayerState(phase: 1, phaseElapsedTime: 0f, basicLoopCount: 0);
|
||||
SetPlayerOffset(2f);
|
||||
SetPatternCooldown(PatternCombo);
|
||||
SetPatternCooldown(PatternPressure);
|
||||
SetPatternCooldown(PatternTertiary);
|
||||
SetPatternCooldown(PatternSecondary);
|
||||
ResumeBehavior();
|
||||
|
||||
yield return WaitForPatternLog(
|
||||
PatternPrimary,
|
||||
DefaultTimeout,
|
||||
"상위 기본기들이 쿨다운이어도 콤보-기본기1로 폴백하지 않았습니다.");
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator Phase3_After90Seconds_ReusesSignature()
|
||||
{
|
||||
yield return ResetBossAndPlayerState(phase: 3, phaseElapsedTime: 91f, basicLoopCount: 2);
|
||||
SetPlayerOffset(2.2f);
|
||||
ResumeBehavior();
|
||||
|
||||
yield return WaitForPatternLog(
|
||||
PatternSignature,
|
||||
DefaultTimeout,
|
||||
"Phase 3에서 90초 경과 후 집행 재사용이 발생하지 않았습니다.");
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator Signature_WhenUnbroken_CompletesSuccessfully()
|
||||
{
|
||||
yield return ResetBossAndPlayerState(phase: 3, phaseElapsedTime: 91f, basicLoopCount: 2);
|
||||
SetPlayerOffset(2.2f);
|
||||
ResumeBehavior();
|
||||
|
||||
yield return WaitForPatternRunning(
|
||||
PatternSignature,
|
||||
DefaultTimeout,
|
||||
"집행 패턴 시작을 확인하지 못했습니다.");
|
||||
|
||||
yield return WaitForLogContaining(
|
||||
"[UsePatternByRoleAction] 패턴 실행: 집행 / Step=2 / Skill=집행 연타1",
|
||||
LongPatternTimeout,
|
||||
"집행 성공 경로의 후속 연타 진입을 확인하지 못했습니다.");
|
||||
|
||||
Assert.IsFalse(runtimeState.WasChargeBroken, "차단이 없는데 집행 차단 플래그가 켜졌습니다.");
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator Signature_WhenBroken_EndsAsFailedAndSetsChargeBroken()
|
||||
{
|
||||
yield return ResetBossAndPlayerState(phase: 3, phaseElapsedTime: 91f, basicLoopCount: 2);
|
||||
SetPlayerOffset(2.2f);
|
||||
ResumeBehavior();
|
||||
|
||||
yield return WaitForPatternRunning(
|
||||
PatternSignature,
|
||||
DefaultTimeout,
|
||||
"집행 패턴 시작을 확인하지 못했습니다.");
|
||||
|
||||
yield return DealBossDamageDuringSignature(maxDuration: 3f, damagePerTickRatio: 0.02f);
|
||||
|
||||
yield return WaitForCondition(
|
||||
() => runtimeState.LastExecutedPattern != null
|
||||
&& runtimeState.LastExecutedPattern.PatternName == PatternSignature
|
||||
&& runtimeState.LastPatternExecutionResult != BossPatternExecutionResult.Running,
|
||||
LongPatternTimeout,
|
||||
"집행 차단 후 결과 종료를 확인하지 못했습니다.");
|
||||
|
||||
Assert.AreEqual(
|
||||
BossPatternExecutionResult.Failed,
|
||||
runtimeState.LastPatternExecutionResult,
|
||||
"집행 차단 시 결과가 Failed로 기록되지 않았습니다.");
|
||||
Assert.IsTrue(runtimeState.WasChargeBroken, "집행 차단 성공 플래그가 기록되지 않았습니다.");
|
||||
Assert.Greater(runtimeState.LastChargeStaggerDuration, 0f, "집행 차단 시 경직 시간이 기록되지 않았습니다.");
|
||||
}
|
||||
|
||||
private IEnumerator ResetBossAndPlayerState(int phase, float phaseElapsedTime, int basicLoopCount)
|
||||
{
|
||||
behaviorGraphAgent.enabled = false;
|
||||
bossSkillController.CancelSkill();
|
||||
yield return null;
|
||||
|
||||
boss.Respawn();
|
||||
yield return null;
|
||||
|
||||
player.Respawn();
|
||||
hitReactionController.ClearHitReactionState();
|
||||
playerMovement.ClearForcedMovement();
|
||||
playerMovement.enabled = false;
|
||||
yield return null;
|
||||
|
||||
runtimeState.ResetPhaseState();
|
||||
runtimeState.SetCurrentPatternPhase(phase, resetTimer: true);
|
||||
SetRuntimeField("currentPhaseStartTime", Time.time - Mathf.Max(0f, phaseElapsedTime));
|
||||
SetRuntimeField("basicLoopCountSinceLastBigPattern", Mathf.Max(0, basicLoopCount));
|
||||
SetRuntimeField("meleePatternCounter", 0);
|
||||
SetRuntimeField("lastPatternExecutionResult", BossPatternExecutionResult.None);
|
||||
SetRuntimeField("lastExecutedPattern", null);
|
||||
SetRuntimeField("lastReviveCaster", null);
|
||||
SetRuntimeField("lastRevivedTarget", null);
|
||||
SetRuntimeField("lastReviveEventTime", float.NegativeInfinity);
|
||||
|
||||
ClearRuntimeDictionary("patternCooldownTracker");
|
||||
ClearRuntimeDictionary("customPhaseConditions");
|
||||
|
||||
runtimeState.WasChargeBroken = false;
|
||||
runtimeState.LastChargeStaggerDuration = 0f;
|
||||
runtimeState.SetCurrentTarget(player.gameObject);
|
||||
|
||||
SetPlayerOffset(2f);
|
||||
SetBossHealthPercent(1f);
|
||||
yield return null;
|
||||
|
||||
ClearCapturedLogs();
|
||||
}
|
||||
|
||||
private IEnumerator DealBossDamageDuringSignature(float maxDuration, float damagePerTickRatio)
|
||||
{
|
||||
float maxHealth = boss.MaxHealth;
|
||||
float startTime = Time.time;
|
||||
|
||||
yield return new WaitForSeconds(0.9f);
|
||||
|
||||
while (Time.time - startTime <= maxDuration)
|
||||
{
|
||||
if (runtimeState.LastPatternExecutionResult != BossPatternExecutionResult.Running
|
||||
|| runtimeState.LastExecutedPattern == null
|
||||
|| runtimeState.LastExecutedPattern.PatternName != PatternSignature)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
float burst = maxHealth * Mathf.Clamp(damagePerTickRatio, 0.005f, 0.1f);
|
||||
boss.TakeDamage(burst);
|
||||
yield return new WaitForSeconds(0.1f);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator LoadScene(string sceneName)
|
||||
{
|
||||
SceneManager.LoadScene(sceneName, LoadSceneMode.Single);
|
||||
yield return WaitForCondition(
|
||||
() => SceneManager.GetActiveScene().name == sceneName,
|
||||
10f,
|
||||
$"씬 로드 실패: {sceneName}");
|
||||
|
||||
yield return null;
|
||||
}
|
||||
|
||||
private IEnumerator WaitForPatternRunning(string patternName, float timeout, string failureMessage)
|
||||
{
|
||||
yield return WaitForCondition(
|
||||
() => runtimeState.LastExecutedPattern != null
|
||||
&& runtimeState.LastExecutedPattern.PatternName == patternName
|
||||
&& runtimeState.LastPatternExecutionResult == BossPatternExecutionResult.Running,
|
||||
timeout,
|
||||
failureMessage);
|
||||
}
|
||||
|
||||
private IEnumerator WaitForPatternResolved(string patternName, float timeout, string failureMessage)
|
||||
{
|
||||
yield return WaitForCondition(
|
||||
() => runtimeState.LastExecutedPattern != null
|
||||
&& runtimeState.LastExecutedPattern.PatternName == patternName
|
||||
&& runtimeState.LastPatternExecutionResult != BossPatternExecutionResult.Running,
|
||||
timeout,
|
||||
failureMessage);
|
||||
}
|
||||
|
||||
private IEnumerator WaitForPatternLog(string patternName, float timeout, string failureMessage)
|
||||
{
|
||||
yield return WaitForCondition(
|
||||
() => HasPatternLog(patternName),
|
||||
timeout,
|
||||
failureMessage);
|
||||
}
|
||||
|
||||
private IEnumerator WaitForAnyPatternLog(IReadOnlyList<string> patternNames, float timeout, string failureMessage)
|
||||
{
|
||||
yield return WaitForCondition(
|
||||
() => HasAnyPatternLog(patternNames),
|
||||
timeout,
|
||||
failureMessage);
|
||||
}
|
||||
|
||||
private IEnumerator WaitForLogContaining(string marker, float timeout, string failureMessage)
|
||||
{
|
||||
yield return WaitForCondition(
|
||||
() => HasLogContaining(marker),
|
||||
timeout,
|
||||
failureMessage);
|
||||
}
|
||||
|
||||
private IEnumerator WaitForCondition(Func<bool> predicate, float timeout, string failureMessage)
|
||||
{
|
||||
float startTime = Time.time;
|
||||
|
||||
while (Time.time - startTime <= timeout)
|
||||
{
|
||||
if (predicate.Invoke())
|
||||
yield break;
|
||||
|
||||
yield return null;
|
||||
}
|
||||
|
||||
Assert.Fail(failureMessage);
|
||||
}
|
||||
|
||||
private PlayerNetworkController FindPrimaryPlayer()
|
||||
{
|
||||
PlayerNetworkController[] players = UnityEngine.Object.FindObjectsByType<PlayerNetworkController>(FindObjectsSortMode.None);
|
||||
for (int i = 0; i < players.Length; i++)
|
||||
{
|
||||
if (players[i] != null)
|
||||
return players[i];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void SetPlayerOffset(float horizontalDistance)
|
||||
{
|
||||
Vector3 origin = boss.transform.position;
|
||||
Vector3 offset = boss.transform.forward;
|
||||
offset.y = 0f;
|
||||
if (offset.sqrMagnitude < 0.01f)
|
||||
offset = Vector3.forward;
|
||||
|
||||
offset.Normalize();
|
||||
Vector3 targetPosition = origin + (offset * horizontalDistance);
|
||||
targetPosition.y = origin.y;
|
||||
|
||||
CharacterController characterController = player.GetComponent<CharacterController>();
|
||||
bool restoreCharacterController = characterController != null && characterController.enabled;
|
||||
if (restoreCharacterController)
|
||||
characterController.enabled = false;
|
||||
|
||||
player.transform.position = targetPosition;
|
||||
player.transform.rotation = Quaternion.LookRotation((origin - targetPosition).normalized);
|
||||
|
||||
if (restoreCharacterController)
|
||||
characterController.enabled = true;
|
||||
|
||||
runtimeState.SetCurrentTarget(player.gameObject);
|
||||
}
|
||||
|
||||
private void SetBossHealthPercent(float healthPercent)
|
||||
{
|
||||
float targetHealth = boss.MaxHealth * Mathf.Clamp01(healthPercent);
|
||||
float delta = boss.CurrentHealth - targetHealth;
|
||||
|
||||
if (delta > 0f)
|
||||
{
|
||||
boss.TakeDamage(delta);
|
||||
}
|
||||
else if (delta < 0f)
|
||||
{
|
||||
boss.Heal(-delta);
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearRuntimeDictionary(string fieldName)
|
||||
{
|
||||
FieldInfo field = typeof(BossBehaviorRuntimeState).GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
Assert.NotNull(field, $"BossBehaviorRuntimeState 필드를 찾지 못했습니다: {fieldName}");
|
||||
|
||||
object dictionary = field.GetValue(runtimeState);
|
||||
MethodInfo clearMethod = dictionary?.GetType().GetMethod("Clear", BindingFlags.Instance | BindingFlags.Public);
|
||||
Assert.NotNull(clearMethod, $"딕셔너리 Clear 메서드를 찾지 못했습니다: {fieldName}");
|
||||
clearMethod.Invoke(dictionary, null);
|
||||
}
|
||||
|
||||
private void SetRuntimeField(string fieldName, object value)
|
||||
{
|
||||
FieldInfo field = typeof(BossBehaviorRuntimeState).GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
Assert.NotNull(field, $"BossBehaviorRuntimeState 필드를 찾지 못했습니다: {fieldName}");
|
||||
field.SetValue(runtimeState, value);
|
||||
}
|
||||
|
||||
private void ResumeBehavior()
|
||||
{
|
||||
ClearCapturedLogs();
|
||||
behaviorGraphAgent.enabled = true;
|
||||
}
|
||||
|
||||
private void ClearCapturedLogs()
|
||||
{
|
||||
capturedLogs.Clear();
|
||||
}
|
||||
|
||||
private bool HasPatternLog(string patternName)
|
||||
{
|
||||
string marker = $"패턴 실행: {patternName} /";
|
||||
return HasLogContaining(marker);
|
||||
}
|
||||
|
||||
private bool HasAnyPatternLog(IReadOnlyList<string> patternNames)
|
||||
{
|
||||
if (patternNames == null)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < patternNames.Count; i++)
|
||||
{
|
||||
if (HasPatternLog(patternNames[i]))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void SetPatternCooldown(string patternName)
|
||||
{
|
||||
BossPatternData pattern = FindPatternAsset(patternName);
|
||||
Assert.NotNull(pattern, $"패턴 에셋을 찾지 못했습니다: {patternName}");
|
||||
runtimeState.SetPatternCooldown(pattern);
|
||||
}
|
||||
|
||||
private BossPatternData FindPatternAsset(string patternName)
|
||||
{
|
||||
BossPatternData[] patterns = Resources.FindObjectsOfTypeAll<BossPatternData>();
|
||||
for (int i = 0; i < patterns.Length; i++)
|
||||
{
|
||||
BossPatternData pattern = patterns[i];
|
||||
if (pattern == null || pattern.PatternName != patternName)
|
||||
continue;
|
||||
|
||||
return pattern;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool HasLogContaining(string marker)
|
||||
{
|
||||
for (int i = 0; i < capturedLogs.Count; i++)
|
||||
{
|
||||
if (capturedLogs[i].Contains(marker, StringComparison.Ordinal))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void HandleLogMessage(string condition, string stackTrace, LogType type)
|
||||
{
|
||||
if (string.IsNullOrEmpty(condition))
|
||||
return;
|
||||
|
||||
capturedLogs.Add(condition);
|
||||
if (capturedLogs.Count > 512)
|
||||
capturedLogs.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6647bc8a64c79f94895db430b15e1736
|
||||
@@ -4,6 +4,7 @@
|
||||
<Project Path="Assembly-CSharp.csproj" />
|
||||
<Project Path="Assembly-CSharp-Editor.csproj" />
|
||||
<Project Path="SidekickCharacters.Editor.csproj" />
|
||||
<Project Path="Colosseum.Tests.PlayMode.csproj" />
|
||||
<Project Path="Gilzoide.SqliteNet.csproj" />
|
||||
<Project Path="Gilzoide.SqliteNet.Tests.Editor.csproj" />
|
||||
<Project Path="Colosseum.Tests.Editor.csproj" />
|
||||
|
||||
Reference in New Issue
Block a user