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

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

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

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

View File

@@ -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에서 시각적으로 확인할 수 있습니다.

View File

@@ -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),