fix: 드로그 강타 다운 및 밟기 연계 복구

- 드로그 BT를 밟기 우선, 콤보-강타, 추적 순서로 유지하도록 관련 자산을 정리
- 강타/밟기 클립에 효과 이벤트를 추가하고 밟기 패턴의 Phase 1 진입을 복구
- 플레이어 다운 시간을 DownBegin 이후 루프 구간 기준으로 계산하도록 조정
This commit is contained in:
2026-04-08 22:33:25 +09:00
parent 3c59f35fae
commit 81d2f4a5a1
9 changed files with 6141 additions and 5577 deletions

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

View File

@@ -26,5 +26,5 @@ MonoBehaviour:
telegraphAbnormality: {fileID: 0} telegraphAbnormality: {fileID: 0}
staggerDuration: 0 staggerDuration: 0
cooldown: 2.5 cooldown: 2.5
minPhase: 2 minPhase: 1
skipJumpStepOnNoTarget: 0 skipJumpStepOnNoTarget: 0

View File

@@ -223,36 +223,18 @@ namespace Colosseum.Editor
"MoveSpeed"); "MoveSpeed");
BossPatternData punishPattern = LoadRequiredAsset<BossPatternData>(DefaultPunishPatternPath, "밟기 패턴"); BossPatternData punishPattern = LoadRequiredAsset<BossPatternData>(DefaultPunishPatternPath, "밟기 패턴");
BossPatternData signaturePattern = LoadRequiredAsset<BossPatternData>(DefaultSignaturePatternPath, "집행 개시 패턴");
BossPatternData mobilityPattern = LoadRequiredAsset<BossPatternData>(DefaultMobilityPatternPath, "점프 패턴");
BossPatternData secondaryPattern = LoadRequiredAsset<BossPatternData>(DefaultSecondaryPatternPath, "콤보-기본기2 패턴");
BossPatternData comboPattern = LoadRequiredAsset<BossPatternData>(DefaultComboPatternPath, "콤보-강타 패턴"); BossPatternData comboPattern = LoadRequiredAsset<BossPatternData>(DefaultComboPatternPath, "콤보-강타 패턴");
BossPatternData primaryPattern = LoadRequiredAsset<BossPatternData>(DefaultPrimaryPatternPath, "콤보-기본기1 패턴");
BossPatternData pressurePattern = LoadRequiredAsset<BossPatternData>(DefaultPressurePatternPath, "콤보-발구르기 패턴");
BossPatternData utilityPattern = LoadRequiredAsset<BossPatternData>(DefaultUtilityPatternPath, "투척 패턴");
SkillData phase3TransitionSkill = LoadRequiredAsset<SkillData>(DefaultPhase3TransitionSkillPath, "Phase 3 포효 스킬");
if (punishPattern == null || signaturePattern == null || mobilityPattern == null || if (punishPattern == null || comboPattern == null)
secondaryPattern == null || comboPattern == null || primaryPattern == null || pressurePattern == null ||
utilityPattern == null || phase3TransitionSkill == null)
{ {
Debug.LogError("[DrogBTRebuild] 프리팹에서 필수 패턴 에셋을 읽지 못했습니다."); Debug.LogError("[DrogBTRebuild] 프리팹에서 필수 패턴 에셋을 읽지 못했습니다.");
return; return;
} }
// ── 계단식 우선순위 체인 ── // ── 단순 우선순위 체인 ──
// 설계안 우선순위: 밟기 > 집행 개시 > 조합 > 도약 > 기본 루프 > 유틸리티 // 요구사항: 밟기 > 강타 계열 > 추적
// 각 Branch는 조건만 판정하고, 실제 대상 선택/검증/실행은 Sequence 내부 노드로 드러냅니다. // 다운 대상이 근처에 있으면 밟기를 우선 사용하고, 그렇지 않으면 강타 계열 패턴만 반복합니다.
// 마지막까지 모든 조건이 false이면 Chase (fallback) // 사거리 밖에서는 추적으로 재진입합니다.
//
// 연결 흐름: Branch.True → FloatingPort(True).InputPort → FloatingPort(True).OutputPort → Action.InputPort
// CreateNodePortsForNode를 호출하여 FloatingPortNodeModel을 자동 생성해야 합니다.
//
// 레이아웃 패턴 (사용자 조정 기준):
// Branch: (-800, y)
// True Floating: (-597, y + 110)
// False Floating: (-1011, y + 114)
// Action: (-598, y + 199)
const float branchX = -800f; const float branchX = -800f;
const float rootRefreshX = branchX - 540f; const float rootRefreshX = branchX - 540f;
@@ -266,8 +248,6 @@ namespace Colosseum.Editor
const float startY = -700f; const float startY = -700f;
const float rootRefreshY = startY - 120f; const float rootRefreshY = startY - 120f;
const float stepY = 620f; const float stepY = 620f;
const float nestedBranchOffsetY = 180f;
const float nestedActionOffsetY = 360f;
// 루프 시작마다 주 대상을 블랙보드에 동기화한 뒤 패턴 우선순위 체인으로 들어갑니다. // 루프 시작마다 주 대상을 블랙보드에 동기화한 뒤 패턴 우선순위 체인으로 들어갑니다.
object rootRefreshNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(RefreshPrimaryTargetAction), new Vector2(rootRefreshX, rootRefreshY)); object rootRefreshNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(RefreshPrimaryTargetAction), new Vector2(rootRefreshX, rootRefreshY));
@@ -277,7 +257,6 @@ namespace Colosseum.Editor
object downBranch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(branchX, startY)); object downBranch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(branchX, startY));
AttachPatternReadyCondition(downBranch, punishPattern, authoringAssembly); AttachPatternReadyCondition(downBranch, punishPattern, authoringAssembly);
AttachConditionWithValue(downBranch, typeof(IsDownedTargetInRangeCondition), "SearchRadius", DefaultDownedTargetSearchRadius, authoringAssembly); AttachConditionWithValue(downBranch, typeof(IsDownedTargetInRangeCondition), "SearchRadius", DefaultDownedTargetSearchRadius, authoringAssembly);
AttachPhaseConditionIfNeeded(downBranch, punishPattern, authoringAssembly);
SetBranchRequiresAll(downBranch, true); SetBranchRequiresAll(downBranch, true);
object downSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, object downSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod,
@@ -291,221 +270,42 @@ namespace Colosseum.Editor
LinkTarget(downUseNode, targetVariable); LinkTarget(downUseNode, targetVariable);
ConnectChildren(graphAsset, connectEdgeMethod, downSequence, downSelectNode, downUseNode); ConnectChildren(graphAsset, connectEdgeMethod, downSequence, downSelectNode, downUseNode);
// #2 Signature — 집행 개시 (Sequence: 패턴 실행 → 결과 분기) // #2 Combo — 강타 계열 기본 루프
// signatureBranch.True → Sequence: object comboBranch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(branchX, startY + stepY));
// Child 1: 현재 주 대상 검증 object comboRangeCondModel = AttachCondition(comboBranch, typeof(IsTargetInAttackRangeCondition), authoringAssembly);
// Child 2: 집행개시 패턴 실행 (ChargeWait 포함) if (comboRangeCondModel != null)
// Child 3: Branch(차단 성공 여부) → 보스 경직 또는 범위 효과 setFieldMethod.Invoke(comboRangeCondModel, new object[] { "Target", targetVariable, typeof(GameObject) });
object signatureBranch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(branchX, startY + stepY)); if (comboRangeCondModel != null)
AttachPatternReadyCondition(signatureBranch, signaturePattern, authoringAssembly); SetConditionFieldValue(comboRangeCondModel, "AttackRange", DefaultPrimaryBranchAttackRange);
AttachPhaseConditionIfNeeded(signatureBranch, signaturePattern, authoringAssembly);
AttachConditionWithValue(signatureBranch, typeof(IsPhaseElapsedTimeAboveCondition), "Seconds", DefaultPhase3SignatureDelay, authoringAssembly);
SetBranchRequiresAll(signatureBranch, true);
// Sequence: 패턴 실행 → 결과 분기
object signatureSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod,
runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true),
new Vector2(mainSequenceX, startY + stepY));
object signatureValidateNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ValidateTargetAction), new Vector2(mainValidateX, startY + stepY));
LinkTarget(signatureValidateNode, targetVariable);
// Child 2: 집행 패턴 실행
object signatureUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UsePatternByRoleAction), new Vector2(mainUseX, startY + stepY));
SetNodeFieldValue(signatureUseNode, "Pattern", signaturePattern, setFieldValueMethod);
SetNodeFieldValue(signatureUseNode, "ContinueOnResolvedFailure", true, setFieldValueMethod);
LinkTarget(signatureUseNode, targetVariable);
// Child 3: 패턴 완료 시 결과 분기
// 패턴이 실패 결과로 끝나면 True → 보스 경직, 성공적으로 완수되면 False → 범위 효과 적용
object outcomeBranch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(mainUseX + 320f, startY + stepY));
AttachConditionWithValue(outcomeBranch, typeof(IsPatternExecutionResultCondition), "Result", BossPatternExecutionResult.Failed, authoringAssembly);
object staggerNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(BossStaggerAction), new Vector2(mainUseX + 520f, startY + stepY + nestedBranchOffsetY));
object failureEffectsNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(SignatureFailureEffectsAction), new Vector2(mainUseX + 520f, startY + stepY + nestedActionOffsetY));
// outcomeBranch True → 보스 경직 (패턴 실패 결과)
ConnectBranch(graphAsset, connectEdgeMethod, outcomeBranch, "True", staggerNode);
// outcomeBranch False → 실패 효과 (패턴 성공 완수)
ConnectBranch(graphAsset, connectEdgeMethod, outcomeBranch, "False", failureEffectsNode);
// Sequence에 자식 연결
ConnectChildren(graphAsset, connectEdgeMethod, signatureSequence, signatureValidateNode, signatureUseNode, outcomeBranch);
// 메인 체인: signatureBranch.True → Sequence
ConnectBranch(graphAsset, connectEdgeMethod, signatureBranch, "True", signatureSequence);
// #3 Combo — 콤보-강타
object comboBranch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(branchX, startY + stepY * 2));
AttachPatternReadyCondition(comboBranch, comboPattern, authoringAssembly); AttachPatternReadyCondition(comboBranch, comboPattern, authoringAssembly);
AttachPhaseConditionIfNeeded(comboBranch, comboPattern, authoringAssembly);
SetBranchRequiresAll(comboBranch, true); SetBranchRequiresAll(comboBranch, true);
object comboSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, object comboSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod,
runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true),
new Vector2(mainSequenceX, startY + stepY * 2)); new Vector2(mainSequenceX, startY + stepY));
object comboValidateNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ValidateTargetAction), new Vector2(mainValidateX, startY + stepY * 2)); object comboValidateNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ValidateTargetAction), new Vector2(mainValidateX, startY + stepY));
LinkTarget(comboValidateNode, targetVariable); LinkTarget(comboValidateNode, targetVariable);
object comboUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UsePatternByRoleAction), new Vector2(mainUseX, startY + stepY * 2)); object comboUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UsePatternByRoleAction), new Vector2(mainUseX, startY + stepY));
SetNodeFieldValue(comboUseNode, "Pattern", comboPattern, setFieldValueMethod); SetNodeFieldValue(comboUseNode, "Pattern", comboPattern, setFieldValueMethod);
LinkTarget(comboUseNode, targetVariable); LinkTarget(comboUseNode, targetVariable);
ConnectChildren(graphAsset, connectEdgeMethod, comboSequence, comboValidateNode, comboUseNode); ConnectChildren(graphAsset, connectEdgeMethod, comboSequence, comboValidateNode, comboUseNode);
ConnectBranch(graphAsset, connectEdgeMethod, comboBranch, "True", comboSequence); ConnectBranch(graphAsset, connectEdgeMethod, comboBranch, "True", comboSequence);
// #4 Mobility — 도약 (전제 조건: 지나치게 먼 대상이 존재해야 함) // #3 Chase — fallback
object leapBranch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(branchX, startY + stepY * 3)); object chaseSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(branchX, startY + stepY * 2));
AttachPatternReadyCondition(leapBranch, mobilityPattern, authoringAssembly); object chaseRefreshNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(RefreshPrimaryTargetAction), new Vector2(branchX + 160f, startY + stepY * 2 + 80f));
AttachConditionWithValue(leapBranch, typeof(IsTargetBeyondDistanceCondition), "MinDistance", DefaultLeapTargetMinDistance, authoringAssembly);
AttachPhaseConditionIfNeeded(leapBranch, mobilityPattern, authoringAssembly);
SetBranchRequiresAll(leapBranch, true);
object leapSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod,
runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true),
new Vector2(mainSequenceX, startY + stepY * 3));
object leapSelectNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(SelectTargetByDistanceAction), new Vector2(mainValidateX, startY + stepY * 3));
SetNodeFieldValue(leapSelectNode, "MinRange", DefaultLeapTargetMinDistance, setFieldValueMethod);
SetNodeFieldValue(leapSelectNode, "MaxRange", DefaultTargetSearchRange, setFieldValueMethod);
SetNodeFieldValue(leapSelectNode, "SelectionMode", DistanceTargetSelectionMode.Farthest, setFieldValueMethod);
LinkTarget(leapSelectNode, targetVariable);
object leapUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UsePatternByRoleAction), new Vector2(mainUseX, startY + stepY * 3));
SetNodeFieldValue(leapUseNode, "Pattern", mobilityPattern, setFieldValueMethod);
LinkTarget(leapUseNode, targetVariable);
ConnectChildren(graphAsset, connectEdgeMethod, leapSequence, leapSelectNode, leapUseNode);
// #5 Primary — 콤보-기본기1
object primaryBranch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(branchX, startY + stepY * 4));
object primaryRangeCondModel = AttachCondition(primaryBranch, typeof(IsTargetInAttackRangeCondition), authoringAssembly);
if (primaryRangeCondModel != null) setFieldMethod.Invoke(primaryRangeCondModel, new object[] { "Target", targetVariable, typeof(GameObject) });
if (primaryRangeCondModel != null) SetConditionFieldValue(primaryRangeCondModel, "AttackRange", DefaultPrimaryBranchAttackRange);
AttachPatternReadyCondition(primaryBranch, primaryPattern, authoringAssembly);
AttachPhaseConditionIfNeeded(primaryBranch, primaryPattern, authoringAssembly);
SetBranchRequiresAll(primaryBranch, true);
object primarySequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod,
runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true),
new Vector2(mainSequenceX, startY + stepY * 4));
object primaryValidateNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ValidateTargetAction), new Vector2(mainValidateX, startY + stepY * 4));
LinkTarget(primaryValidateNode, targetVariable);
object primaryUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UsePatternByRoleAction), new Vector2(mainUseX, startY + stepY * 4));
SetNodeFieldValue(primaryUseNode, "Pattern", primaryPattern, setFieldValueMethod);
LinkTarget(primaryUseNode, targetVariable);
ConnectChildren(graphAsset, connectEdgeMethod, primarySequence, primaryValidateNode, primaryUseNode);
// #6 Secondary Basic — 콤보-기본기2
object secondaryBranch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(branchX, startY + stepY * 5));
object secondaryRangeCondModel = AttachCondition(secondaryBranch, typeof(IsTargetInAttackRangeCondition), authoringAssembly);
if (secondaryRangeCondModel != null) setFieldMethod.Invoke(secondaryRangeCondModel, new object[] { "Target", targetVariable, typeof(GameObject) });
if (secondaryRangeCondModel != null) SetConditionFieldValue(secondaryRangeCondModel, "AttackRange", DefaultPrimaryBranchAttackRange);
AttachPatternReadyCondition(secondaryBranch, secondaryPattern, authoringAssembly);
AttachPhaseConditionIfNeeded(secondaryBranch, secondaryPattern, authoringAssembly);
SetBranchRequiresAll(secondaryBranch, true);
object secondarySequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod,
runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true),
new Vector2(mainSequenceX, startY + stepY * 5));
object secondaryValidateNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ValidateTargetAction), new Vector2(mainValidateX, startY + stepY * 5));
LinkTarget(secondaryValidateNode, targetVariable);
object secondaryUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UsePatternByRoleAction), new Vector2(mainUseX, startY + stepY * 5));
SetNodeFieldValue(secondaryUseNode, "Pattern", secondaryPattern, setFieldValueMethod);
LinkTarget(secondaryUseNode, targetVariable);
ConnectChildren(graphAsset, connectEdgeMethod, secondarySequence, secondaryValidateNode, secondaryUseNode);
// #7 Pressure — 콤보-발구르기
object pressureBranch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(branchX, startY + stepY * 6));
object pressureRangeCondModel = AttachCondition(pressureBranch, typeof(IsTargetInAttackRangeCondition), authoringAssembly);
if (pressureRangeCondModel != null) setFieldMethod.Invoke(pressureRangeCondModel, new object[] { "Target", targetVariable, typeof(GameObject) });
if (pressureRangeCondModel != null) SetConditionFieldValue(pressureRangeCondModel, "AttackRange", DefaultPrimaryBranchAttackRange);
AttachPatternReadyCondition(pressureBranch, pressurePattern, authoringAssembly);
AttachPhaseConditionIfNeeded(pressureBranch, pressurePattern, authoringAssembly);
SetBranchRequiresAll(pressureBranch, true);
object pressureSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod,
runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true),
new Vector2(mainSequenceX, startY + stepY * 6));
object pressureValidateNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ValidateTargetAction), new Vector2(mainValidateX, startY + stepY * 6));
LinkTarget(pressureValidateNode, targetVariable);
object pressureUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UsePatternByRoleAction), new Vector2(mainUseX, startY + stepY * 6));
SetNodeFieldValue(pressureUseNode, "Pattern", pressurePattern, setFieldValueMethod);
LinkTarget(pressureUseNode, targetVariable);
ConnectChildren(graphAsset, connectEdgeMethod, pressureSequence, pressureValidateNode, pressureUseNode);
// #8 Utility — 유틸리티 (전제 조건: 원거리 대상이 존재해야 함)
object utilityBranch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(branchX, startY + stepY * 7));
AttachPatternReadyCondition(utilityBranch, utilityPattern, authoringAssembly);
AttachConditionWithValue(utilityBranch, typeof(IsTargetBeyondDistanceCondition), "MinDistance", DefaultThrowTargetMinDistance, authoringAssembly);
AttachConditionWithValue(utilityBranch, typeof(IsPhaseElapsedTimeAboveCondition), "Seconds", DefaultThrowAvailabilityDelay, authoringAssembly);
AttachPhaseConditionIfNeeded(utilityBranch, utilityPattern, authoringAssembly);
SetBranchRequiresAll(utilityBranch, true);
object utilitySequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod,
runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true),
new Vector2(mainSequenceX, startY + stepY * 7));
object utilitySelectNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(SelectAlternateTargetByDistanceAction), new Vector2(mainValidateX, startY + stepY * 7));
SetNodeFieldValue(utilitySelectNode, "MinRange", DefaultThrowTargetMinDistance, setFieldValueMethod);
SetNodeFieldValue(utilitySelectNode, "MaxRange", DefaultTargetSearchRange, setFieldValueMethod);
LinkTarget(utilitySelectNode, targetVariable);
object utilityUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UsePatternByRoleAction), new Vector2(mainUseX, startY + stepY * 7));
SetNodeFieldValue(utilityUseNode, "Pattern", utilityPattern, setFieldValueMethod);
LinkTarget(utilityUseNode, targetVariable);
ConnectChildren(graphAsset, connectEdgeMethod, utilitySequence, utilitySelectNode, utilityUseNode);
// #9 Chase — fallback (Branch 아님, Sequence 사용)
object chaseSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(branchX, startY + stepY * 8));
object chaseRefreshNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(RefreshPrimaryTargetAction), new Vector2(branchX + 160f, startY + stepY * 8 + 80f));
SetNodeFieldValue(chaseRefreshNode, "SearchRange", DefaultTargetSearchRange, setFieldValueMethod); SetNodeFieldValue(chaseRefreshNode, "SearchRange", DefaultTargetSearchRange, setFieldValueMethod);
object chaseHasTargetNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ValidateTargetAction), new Vector2(branchX + 320f, startY + stepY * 8 + 80f)); 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 * 8 + 80f)); object chaseUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ChaseTargetAction), new Vector2(branchX + 480f, startY + stepY * 2 + 80f));
SetNodeFieldValue(chaseUseNode, "StopDistance", DefaultPrimaryBranchAttackRange, setFieldValueMethod); SetNodeFieldValue(chaseUseNode, "StopDistance", DefaultPrimaryBranchAttackRange, setFieldValueMethod);
Connect(graphAsset, connectEdgeMethod, GetDefaultOutputPort(rootRefreshNode), GetDefaultInputPort(downBranch)); Connect(graphAsset, connectEdgeMethod, GetDefaultOutputPort(rootRefreshNode), GetDefaultInputPort(downBranch));
List<object> phaseBranches = new List<object>();
object phaseEntryNode = null;
object previousPhaseBranch = null;
object phase2Branch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(branchX, -1320f));
AttachConditionWithValue(phase2Branch, typeof(IsCurrentPhaseCondition), "Phase", 1, authoringAssembly);
AttachConditionWithValue(phase2Branch, typeof(IsHealthBelowCondition), "HealthPercent", DefaultPhase2EnterHealthPercent, authoringAssembly);
SetBranchRequiresAll(phase2Branch, true);
phaseBranches.Add(phase2Branch);
phaseEntryNode = phase2Branch;
object phase2Sequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod,
runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true),
new Vector2(mainSequenceX, -1320f));
object phase2TransitionWaitNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(WaitAction), new Vector2(mainValidateX, -1320f));
SetNodeFieldValue(phase2TransitionWaitNode, "Duration", DefaultPhaseTransitionLockDuration, setFieldValueMethod);
object phase2SetNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(SetBossPhaseAction), new Vector2(mainUseX, -1320f));
SetNodeFieldValue(phase2SetNode, "TargetPhase", 2, setFieldValueMethod);
ConnectChildren(graphAsset, connectEdgeMethod, phase2Sequence, phase2TransitionWaitNode, phase2SetNode);
ConnectBranch(graphAsset, connectEdgeMethod, phase2Branch, "True", phase2Sequence);
object phase3Branch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(branchX, -1040f));
AttachConditionWithValue(phase3Branch, typeof(IsCurrentPhaseCondition), "Phase", 2, authoringAssembly);
AttachConditionWithValue(phase3Branch, typeof(IsHealthBelowCondition), "HealthPercent", DefaultPhase3EnterHealthPercent, authoringAssembly);
SetBranchRequiresAll(phase3Branch, true);
phaseBranches.Add(phase3Branch);
object phase3Sequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod,
runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true),
new Vector2(mainSequenceX, -1040f));
object phase3TransitionWaitNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(WaitAction), new Vector2(mainValidateX, -1040f));
SetNodeFieldValue(phase3TransitionWaitNode, "Duration", DefaultPhaseTransitionLockDuration, setFieldValueMethod);
object phase3RoarNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UseSkillAction), new Vector2(mainValidateX + 180f, -1040f));
SetNodeFieldValue(phase3RoarNode, "스킬", phase3TransitionSkill, setFieldValueMethod);
object phase3SetNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(SetBossPhaseAction), new Vector2(mainUseX, -1040f));
SetNodeFieldValue(phase3SetNode, "TargetPhase", 3, setFieldValueMethod);
ConnectChildren(graphAsset, connectEdgeMethod, phase3Sequence, phase3TransitionWaitNode, phase3RoarNode, phase3SetNode);
ConnectBranch(graphAsset, connectEdgeMethod, phase3Branch, "True", phase3Sequence);
ConnectBranch(graphAsset, connectEdgeMethod, phase2Branch, "False", phase3Branch);
previousPhaseBranch = phase3Branch;
if (previousPhaseBranch != null)
ConnectBranch(graphAsset, connectEdgeMethod, previousPhaseBranch, "False", rootRefreshNode);
// ── FloatingPortNodeModel 생성 + 위치 보정 ── // ── FloatingPortNodeModel 생성 + 위치 보정 ──
// Branch 노드의 NamedPort(True/False)에 대해 FloatingPortNodeModel을 생성합니다. // Branch 노드의 NamedPort(True/False)에 대해 FloatingPortNodeModel을 생성합니다.
// CreateNodePortsForNode는 기본 위치(Branch + 200px Y)를 사용하므로, 생성 후 사용자 조정 기준 위치로 이동합니다. // CreateNodePortsForNode는 기본 위치(Branch + 200px Y)를 사용하므로, 생성 후 사용자 조정 기준 위치로 이동합니다.
var allBranches = new List<object>(); var allBranches = new List<object>();
allBranches.AddRange(phaseBranches); allBranches.AddRange(new[] { downBranch, comboBranch });
allBranches.AddRange(new[] { downBranch, leapBranch, signatureBranch, outcomeBranch });
allBranches.AddRange(new[] { comboBranch, primaryBranch, secondaryBranch, pressureBranch, utilityBranch });
foreach (object branch in allBranches) foreach (object branch in allBranches)
{ {
createNodePortsMethod?.Invoke(graphAsset, new object[] { branch }); createNodePortsMethod?.Invoke(graphAsset, new object[] { branch });
@@ -526,29 +326,17 @@ namespace Colosseum.Editor
// ── 연결 ── // ── 연결 ──
// Start → Repeater → phaseEntry(페이즈 전환 조건 -> 전투 의사결정 체인) // Start → Repeater → 주 대상 갱신
Connect(graphAsset, connectEdgeMethod, GetDefaultOutputPort(startNode), GetDefaultInputPort(repeatNode)); Connect(graphAsset, connectEdgeMethod, GetDefaultOutputPort(startNode), GetDefaultInputPort(repeatNode));
Connect(graphAsset, connectEdgeMethod, GetDefaultOutputPort(repeatNode), GetDefaultInputPort(phaseEntryNode)); Connect(graphAsset, connectEdgeMethod, GetDefaultOutputPort(repeatNode), GetDefaultInputPort(rootRefreshNode));
// 각 Branch의 True FloatingPort → Action (combo, signature는 내부에서 Sequence로 연결됨) // 각 Branch의 True FloatingPort → Action
ConnectBranch(graphAsset, connectEdgeMethod, downBranch, "True", downSequence); ConnectBranch(graphAsset, connectEdgeMethod, downBranch, "True", downSequence);
ConnectBranch(graphAsset, connectEdgeMethod, leapBranch, "True", leapSequence);
// signatureBranch.True는 signatureSequence에 이미 연결됨
ConnectBranch(graphAsset, connectEdgeMethod, comboBranch, "True", comboSequence); ConnectBranch(graphAsset, connectEdgeMethod, comboBranch, "True", comboSequence);
ConnectBranch(graphAsset, connectEdgeMethod, primaryBranch, "True", primarySequence);
ConnectBranch(graphAsset, connectEdgeMethod, secondaryBranch, "True", secondarySequence);
ConnectBranch(graphAsset, connectEdgeMethod, pressureBranch, "True", pressureSequence);
ConnectBranch(graphAsset, connectEdgeMethod, utilityBranch, "True", utilitySequence);
// 각 Branch의 False FloatingPort → 다음 우선순위 (계단식 체인) // 각 Branch의 False FloatingPort → 다음 우선순위
ConnectBranch(graphAsset, connectEdgeMethod, downBranch, "False", signatureBranch); ConnectBranch(graphAsset, connectEdgeMethod, downBranch, "False", comboBranch);
ConnectBranch(graphAsset, connectEdgeMethod, signatureBranch, "False", comboBranch); ConnectBranch(graphAsset, connectEdgeMethod, comboBranch, "False", chaseSequence);
ConnectBranch(graphAsset, connectEdgeMethod, comboBranch, "False", leapBranch);
ConnectBranch(graphAsset, connectEdgeMethod, leapBranch, "False", primaryBranch);
ConnectBranch(graphAsset, connectEdgeMethod, primaryBranch, "False", secondaryBranch);
ConnectBranch(graphAsset, connectEdgeMethod, secondaryBranch, "False", pressureBranch);
ConnectBranch(graphAsset, connectEdgeMethod, pressureBranch, "False", utilityBranch);
ConnectBranch(graphAsset, connectEdgeMethod, utilityBranch, "False", chaseSequence);
// Chase Sequence 자식 연결 // Chase Sequence 자식 연결
ConnectChildren(graphAsset, connectEdgeMethod, chaseSequence, chaseRefreshNode, chaseHasTargetNode, chaseUseNode); ConnectChildren(graphAsset, connectEdgeMethod, chaseSequence, chaseRefreshNode, chaseHasTargetNode, chaseUseNode);

View File

@@ -70,6 +70,9 @@ namespace Colosseum.Editor
AnimationClip comboSlamHit1Clip = EnsureClipFromSource($"{AnimationsFolder}/Anim_Drog_콤보-강타_1_0.anim", HeavyCombo01BSourcePath, "A_MOD_SWD_Attack_HeavyCombo01B_RM_Neut"); AnimationClip comboSlamHit1Clip = EnsureClipFromSource($"{AnimationsFolder}/Anim_Drog_콤보-강타_1_0.anim", HeavyCombo01BSourcePath, "A_MOD_SWD_Attack_HeavyCombo01B_RM_Neut");
AnimationClip comboSlamHit2Clip = EnsureClipFromSource($"{AnimationsFolder}/Anim_Drog_콤보-강타_2_0.anim", LightCombo01BSourcePath, "A_MOD_SWD_Attack_LightCombo01B_RM_Neut"); AnimationClip comboSlamHit2Clip = EnsureClipFromSource($"{AnimationsFolder}/Anim_Drog_콤보-강타_2_0.anim", LightCombo01BSourcePath, "A_MOD_SWD_Attack_LightCombo01B_RM_Neut");
AnimationClip slamClip = EnsureClipFromSource($"{AnimationsFolder}/Anim_Drog_강타_0.anim", $"{AnimationsFolder}/Anim_Drog_강타R_0.anim"); AnimationClip slamClip = EnsureClipFromSource($"{AnimationsFolder}/Anim_Drog_강타_0.anim", $"{AnimationsFolder}/Anim_Drog_강타R_0.anim");
SetSingleOnEffectEvent(comboSlamHit1Clip, 0.30f);
SetSingleOnEffectEvent(comboSlamHit2Clip, 0.28f);
SetSingleOnEffectEvent(slamClip, 0.95f);
AnimationClip comboStompHit1Clip = EnsureClipFromSource($"{AnimationsFolder}/Anim_Drog_콤보-발구르기_1_0.anim", ZweihanderAttack013SourcePath, "Zweihander_Attack01_3_Root"); 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 comboStompHit2Clip = EnsureClipFromSource($"{AnimationsFolder}/Anim_Drog_콤보-발구르기_2_0.anim", HeavyCombo01CSourcePath, "A_MOD_SWD_Attack_HeavyCombo01C_RM_Neut");
@@ -78,6 +81,7 @@ namespace Colosseum.Editor
AnimationClip leapAirClip = EnsurePlaceholderClip($"{AnimationsFolder}/Anim_Drog_도약_공중_0.anim"); AnimationClip leapAirClip = EnsurePlaceholderClip($"{AnimationsFolder}/Anim_Drog_도약_공중_0.anim");
AnimationClip leapLandingClip = EnsurePlaceholderClip($"{AnimationsFolder}/Anim_Drog_도약_착지_0.anim"); AnimationClip leapLandingClip = EnsurePlaceholderClip($"{AnimationsFolder}/Anim_Drog_도약_착지_0.anim");
AnimationClip stepClip = EnsurePlaceholderClip($"{AnimationsFolder}/Anim_Drog_밟기_0.anim"); AnimationClip stepClip = EnsurePlaceholderClip($"{AnimationsFolder}/Anim_Drog_밟기_0.anim");
SetSingleOnEffectEvent(stepClip, 0.80f);
AnimationClip throwClip = EnsurePlaceholderClip($"{AnimationsFolder}/Anim_Drog_투척_0.anim"); AnimationClip throwClip = EnsurePlaceholderClip($"{AnimationsFolder}/Anim_Drog_투척_0.anim");
AnimationClip roarClip = EnsurePlaceholderClip($"{AnimationsFolder}/Anim_Drog_포효_0.anim"); AnimationClip roarClip = EnsurePlaceholderClip($"{AnimationsFolder}/Anim_Drog_포효_0.anim");
AnimationClip executionReadyClip = EnsurePlaceholderClip($"{AnimationsFolder}/Anim_Drog_집행_준비_0.anim"); AnimationClip executionReadyClip = EnsurePlaceholderClip($"{AnimationsFolder}/Anim_Drog_집행_준비_0.anim");
@@ -718,7 +722,7 @@ namespace Colosseum.Editor
false, false,
TargetResolveMode.HighestThreat, TargetResolveMode.HighestThreat,
2.5f, 2.5f,
2, 1,
false, false,
PatternStepDefinition.CreateSkillStep(stepSkill)); PatternStepDefinition.CreateSkillStep(stepSkill));

View File

@@ -61,6 +61,7 @@ namespace Colosseum.Player
private float knockbackRemainingTime; private float knockbackRemainingTime;
private float staggerRemainingTime; private float staggerRemainingTime;
private bool isDownRecoveryAnimating; private bool isDownRecoveryAnimating;
private bool isDownLoopTimingActive;
/// <summary> /// <summary>
/// 다운 상태 여부 /// 다운 상태 여부
@@ -197,6 +198,7 @@ namespace Colosseum.Player
isDowned.Value = true; isDowned.Value = true;
isDownRecoverable.Value = false; isDownRecoverable.Value = false;
isDownRecoveryAnimating = false; isDownRecoveryAnimating = false;
isDownLoopTimingActive = false;
downRecoverableDelayRemaining = -1f; downRecoverableDelayRemaining = -1f;
ClearKnockbackState(); ClearKnockbackState();
ClearStaggerState(); ClearStaggerState();
@@ -213,6 +215,7 @@ namespace Colosseum.Player
if (!IsServer || !isDowned.Value || isDownRecoveryAnimating) if (!IsServer || !isDowned.Value || isDownRecoveryAnimating)
return; return;
isDownLoopTimingActive = true;
downRecoverableDelayRemaining = downRecoverableDelayAfterBeginExit; downRecoverableDelayRemaining = downRecoverableDelayAfterBeginExit;
} }
@@ -372,7 +375,10 @@ namespace Colosseum.Player
if (!isDowned.Value) if (!isDowned.Value)
return; return;
downRemainingTime -= deltaTime; if (isDownLoopTimingActive)
{
downRemainingTime -= deltaTime;
}
if (!isDownRecoverable.Value && downRecoverableDelayRemaining >= 0f) if (!isDownRecoverable.Value && downRecoverableDelayRemaining >= 0f)
{ {
@@ -405,6 +411,7 @@ namespace Colosseum.Player
EnterDownRecoverableState(); EnterDownRecoverableState();
isDownRecoveryAnimating = true; isDownRecoveryAnimating = true;
isDownLoopTimingActive = false;
downRemainingTime = 0f; downRemainingTime = 0f;
TriggerAnimationRpc(recoverTriggerParam); TriggerAnimationRpc(recoverTriggerParam);
} }
@@ -414,6 +421,7 @@ namespace Colosseum.Player
isDowned.Value = false; isDowned.Value = false;
isDownRecoverable.Value = false; isDownRecoverable.Value = false;
isDownRecoveryAnimating = false; isDownRecoveryAnimating = false;
isDownLoopTimingActive = false;
downRemainingTime = 0f; downRemainingTime = 0f;
downRecoverableDelayRemaining = -1f; downRecoverableDelayRemaining = -1f;
} }