feat: 드로그 집행 개시 패턴 및 낙인 디버프 추가

- 드로그 시그니처 패턴 역할과 집행 개시 패턴 데이터를 추가하고 BT 브랜치에 연결
- 시그니처 차단 성공과 실패 흐름을 BossCombatBehaviorContext에 구현하고 authoring 그래프를 재구성
- 집행자의 낙인 이상상태를 추가하고 받는 피해 배율 증가가 플레이어 대미지 계산에 반영되도록 정리
- 집행 실패 시 광역 피해, 넉백, 다운, 낙인 부여 설정을 드로그 프리팹에 연결
- 성공 경로 검증 중 확인된 보스 Hit 트리거 오류를 방어 로직으로 수정
- Unity 플레이 검증으로 집행 개시 실패와 성공 분기를 모두 확인하고 설계값은 원복
This commit is contained in:
2026-03-23 18:14:18 +09:00
parent 8182258102
commit 0889bb0f25
19 changed files with 1635 additions and 747 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,25 @@
%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: b08cc671f858a3b409170a5356e960a0, type: 3}
m_Name: "Data_Abnormality_Player_\uC9D1\uD589\uC790\uC758\uB099\uC778"
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Abnormalities.AbnormalityData
abnormalityName: "\uC9D1\uD589\uC790\uC758 \uB099\uC778"
icon: {fileID: 0}
duration: 0
level: 1
isDebuff: 1
statModifiers: []
periodicInterval: 0
periodicValue: 0
controlType: 0
slowMultiplier: 0.5
incomingDamageMultiplier: 1.1

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: bc74f1485ad140c28cc14b821e22c127
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,23 @@
%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_\uC9D1\uD589\uAC1C\uC2DC"
m_EditorClassIdentifier: Colosseum.Game::Colosseum.AI.BossPatternData
patternName: "\uC9D1\uD589\uAC1C\uC2DC"
steps:
- Type: 0
Skill: {fileID: 11400000, guid: 99de24df2cb0464d9d4f633efde8dbdb, type: 2}
Duration: 0
- Type: 1
Skill: {fileID: 0}
Duration: 6.5
cooldown: 45

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5e732b41722c45288bb6234f3e3fa638
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,29 @@
%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_\uC9D1\uD589\uAC1C\uC2DC"
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.SkillData
skillName: "\uC9D1\uD589\uAC1C\uC2DC"
description: "\uB4DC\uB85C\uADF8\uAC00 \uD798\uC744 \uB04C\uC5B4\uBAA8\uC73C\uBA70 \uC9D1\uD589\uC744 \uC900\uBE44\uD569\uB2C8\uB2E4."
icon: {fileID: 0}
skillClip: {fileID: -5764696784021583549, guid: 5eaeca917bbeb494eb14ad0e0552c42f, type: 3}
endClip: {fileID: 0}
animationSpeed: 1
useRootMotion: 0
ignoreRootMotionY: 0
jumpToTarget: 0
blockMovementWhileCasting: 1
blockJumpWhileCasting: 1
blockOtherSkillsWhileCasting: 1
cooldown: 0
manaCost: 0
effects: []

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 99de24df2cb0464d9d4f633efde8dbdb
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -2190,6 +2190,7 @@ MonoBehaviour:
slamPattern: {fileID: 11400000, guid: 4a52d59d590b4eaa9ef92b7984eb08c7, type: 2} slamPattern: {fileID: 11400000, guid: 4a52d59d590b4eaa9ef92b7984eb08c7, type: 2}
leapPattern: {fileID: 11400000, guid: 88e6cc7cab28baf4c8f8a742247000ec, type: 2} leapPattern: {fileID: 11400000, guid: 88e6cc7cab28baf4c8f8a742247000ec, type: 2}
downPunishPattern: {fileID: 11400000, guid: fe5100f855d14c0faac44b6d4f2c771e, type: 2} downPunishPattern: {fileID: 11400000, guid: fe5100f855d14c0faac44b6d4f2c771e, type: 2}
signaturePattern: {fileID: 11400000, guid: 5e732b41722c45288bb6234f3e3fa638, type: 2}
phase2HealthThreshold: 0.75 phase2HealthThreshold: 0.75
phase3HealthThreshold: 0.4 phase3HealthThreshold: 0.4
targetRefreshInterval: 0.2 targetRefreshInterval: 0.2
@@ -2198,6 +2199,16 @@ MonoBehaviour:
phase1SlamInterval: 3 phase1SlamInterval: 3
phase2SlamInterval: 2 phase2SlamInterval: 2
phase3SlamInterval: 2 phase3SlamInterval: 2
signatureMinPhase: 2
signatureRequiredDamageRatio: 0.1
signatureSuccessStaggerDuration: 2
signatureFailureAbnormality: {fileID: 11400000, guid: bc74f1485ad140c28cc14b821e22c127, type: 2}
signatureFailureDamage: 40
signatureFailureKnockbackRadius: 8
signatureFailureDownRadius: 3
signatureFailureKnockbackSpeed: 12
signatureFailureKnockbackDuration: 0.35
signatureFailureDownDuration: 2
disableBehaviorGraph: 0 disableBehaviorGraph: 0
debugMode: 1 debugMode: 1
--- !u!114 &7544406269366897481 --- !u!114 &7544406269366897481

View File

@@ -0,0 +1,28 @@
using System;
using Colosseum.Enemy;
using Unity.Behavior;
using Unity.Properties;
using Action = Unity.Behavior.Action;
/// <summary>
/// 시그니처 패턴 사용 가능 여부를 확인하는 체크 액션입니다.
/// </summary>
[Serializable, GeneratePropertyBag]
[NodeDescription(
name: "Check Signature Pattern Ready",
story: "시그니처 패턴 준비 여부 확인",
category: "Action",
id: "b3b2916257134e0eb3a71a5f544a8d6f")]
public partial class CheckSignaturePatternReadyAction : Action
{
protected override Status OnStart()
{
BossCombatBehaviorContext context = GameObject.GetComponent<BossCombatBehaviorContext>();
return context != null && context.IsSignaturePatternReady()
? Status.Success
: Status.Failure;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b27f3137292d5704d802b5cfb58037e4

View File

@@ -0,0 +1,54 @@
using System;
using Colosseum.Enemy;
using Unity.Behavior;
using Unity.Properties;
using UnityEngine;
using Action = Unity.Behavior.Action;
/// <summary>
/// 보스 공통 시그니처 패턴을 실행하는 액션입니다.
/// </summary>
[Serializable, GeneratePropertyBag]
[NodeDescription(
name: "Use Signature Pattern",
story: "시그니처 패턴 실행",
category: "Action",
id: "178f8888d56042c6a75b4d6ee8a7a7d4")]
public partial class UseSignaturePatternAction : Action
{
[SerializeReference]
public BlackboardVariable<GameObject> Target;
private BossCombatBehaviorContext combatBehaviorContext;
private bool started;
protected override Status OnStart()
{
combatBehaviorContext = GameObject.GetComponent<BossCombatBehaviorContext>();
if (combatBehaviorContext == null)
return Status.Failure;
GameObject target = Target != null ? Target.Value : null;
started = combatBehaviorContext.TryStartSignaturePattern(target);
return started ? Status.Running : Status.Failure;
}
protected override Status OnUpdate()
{
if (!started || combatBehaviorContext == null)
return Status.Failure;
return combatBehaviorContext.IsSignaturePatternActive
? Status.Running
: Status.Success;
}
protected override void OnEnd()
{
started = false;
combatBehaviorContext = null;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0680aed4d244d7844918883e06e718d5

View File

@@ -84,6 +84,11 @@ namespace Colosseum.Abnormalities
[Range(0f, 1f)] [Range(0f, 1f)]
public float slowMultiplier = 0.5f; public float slowMultiplier = 0.5f;
[Header("피해 배율")]
[Tooltip("이상 상태가 적용된 동안 받는 피해 배율 (1 = 기본, 1.1 = 10% 증가)")]
[Min(0f)]
public float incomingDamageMultiplier = 1f;
/// <summary> /// <summary>
/// 영구 효과인지 확인 /// 영구 효과인지 확인
/// </summary> /// </summary>
@@ -98,5 +103,10 @@ namespace Colosseum.Abnormalities
/// 제어 효과가 있는지 확인 /// 제어 효과가 있는지 확인
/// </summary> /// </summary>
public bool HasControlEffect => controlType != ControlType.None; public bool HasControlEffect => controlType != ControlType.None;
/// <summary>
/// 받는 피해 배율 변경 여부
/// </summary>
public bool HasIncomingDamageModifier => !Mathf.Approximately(incomingDamageMultiplier, 1f);
} }
} }

View File

@@ -32,6 +32,7 @@ namespace Colosseum.Abnormalities
private int silenceCount; private int silenceCount;
private int invincibleCount; private int invincibleCount;
private float slowMultiplier = 1f; private float slowMultiplier = 1f;
private float incomingDamageMultiplier = 1f;
// 클라이언트 판정용 제어 효과 동기화 변수 // 클라이언트 판정용 제어 효과 동기화 변수
private NetworkVariable<int> syncedStunCount = new NetworkVariable<int>(0); private NetworkVariable<int> syncedStunCount = new NetworkVariable<int>(0);
@@ -62,6 +63,11 @@ namespace Colosseum.Abnormalities
/// </summary> /// </summary>
public float MoveSpeedMultiplier => GetCurrentSlowMultiplier(); public float MoveSpeedMultiplier => GetCurrentSlowMultiplier();
/// <summary>
/// 받는 피해 배율 (1.0 = 기본, 1.1 = 10% 증가)
/// </summary>
public float IncomingDamageMultiplier => incomingDamageMultiplier;
/// <summary> /// <summary>
/// 행동 가능 여부 (기절이 아닐 때) /// 행동 가능 여부 (기절이 아닐 때)
/// </summary> /// </summary>
@@ -238,6 +244,7 @@ namespace Colosseum.Abnormalities
ApplyStatModifiers(newAbnormality); ApplyStatModifiers(newAbnormality);
ApplyControlEffect(data); ApplyControlEffect(data);
RecalculateIncomingDamageMultiplier();
SyncAbnormalityAdd(newAbnormality, source); SyncAbnormalityAdd(newAbnormality, source);
OnAbnormalityAdded?.Invoke(newAbnormality); OnAbnormalityAdded?.Invoke(newAbnormality);
@@ -282,6 +289,7 @@ namespace Colosseum.Abnormalities
{ {
RemoveStatModifiers(abnormality); RemoveStatModifiers(abnormality);
RemoveControlEffect(abnormality.Data); RemoveControlEffect(abnormality.Data);
RecalculateIncomingDamageMultiplier();
SyncAbnormalityRemove(abnormality); SyncAbnormalityRemove(abnormality);
activeAbnormalities.Remove(abnormality); activeAbnormalities.Remove(abnormality);
@@ -488,6 +496,20 @@ namespace Colosseum.Abnormalities
} }
} }
private void RecalculateIncomingDamageMultiplier()
{
incomingDamageMultiplier = 1f;
for (int i = 0; i < activeAbnormalities.Count; i++)
{
AbnormalityData data = activeAbnormalities[i].Data;
if (data == null || !data.HasIncomingDamageModifier)
continue;
incomingDamageMultiplier *= Mathf.Max(0f, data.incomingDamageMultiplier);
}
}
private int GetCurrentStunCount() => IsServer ? stunCount : syncedStunCount.Value; private int GetCurrentStunCount() => IsServer ? stunCount : syncedStunCount.Value;
private int GetCurrentSilenceCount() => IsServer ? silenceCount : syncedSilenceCount.Value; private int GetCurrentSilenceCount() => IsServer ? silenceCount : syncedSilenceCount.Value;

View File

@@ -67,6 +67,7 @@ namespace Colosseum.Editor
object repeatNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.RepeaterModifier", true), new Vector2(420f, -470f)); object repeatNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.RepeaterModifier", true), new Vector2(420f, -470f));
object selectorNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SelectorComposite", true), new Vector2(420f, -280f)); object selectorNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SelectorComposite", true), new Vector2(420f, -280f));
object signatureSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(-1020f, -40f));
object downSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(-620f, -40f)); object downSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(-620f, -40f));
object leapSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(-220f, -40f)); object leapSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(-220f, -40f));
object slamSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(180f, -40f)); object slamSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(180f, -40f));
@@ -74,6 +75,11 @@ namespace Colosseum.Editor
object slamFallbackSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(980f, -40f)); object slamFallbackSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(980f, -40f));
object chaseSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(1380f, -40f)); object chaseSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(1380f, -40f));
object signatureRefreshNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(RefreshPrimaryTargetAction), new Vector2(-1140f, 240f));
object signatureHasTargetNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ValidateTargetAction), new Vector2(-1020f, 240f));
object signatureReadyNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(CheckSignaturePatternReadyAction), new Vector2(-900f, 240f));
object signatureUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UseSignaturePatternAction), new Vector2(-780f, 240f));
object downSelectNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(SelectNearestDownedTargetAction), new Vector2(-740f, 240f)); object downSelectNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(SelectNearestDownedTargetAction), new Vector2(-740f, 240f));
object downReadyNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(CheckPunishPatternReadyAction), new Vector2(-620f, 240f)); object downReadyNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(CheckPunishPatternReadyAction), new Vector2(-620f, 240f));
object downUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UsePunishPatternAction), new Vector2(-500f, 240f)); object downUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UsePunishPatternAction), new Vector2(-500f, 240f));
@@ -107,8 +113,9 @@ namespace Colosseum.Editor
Connect(graphAsset, connectEdgeMethod, GetDefaultOutputPort(startNode), GetDefaultInputPort(repeatNode)); Connect(graphAsset, connectEdgeMethod, GetDefaultOutputPort(startNode), GetDefaultInputPort(repeatNode));
Connect(graphAsset, connectEdgeMethod, GetDefaultOutputPort(repeatNode), GetDefaultInputPort(selectorNode)); Connect(graphAsset, connectEdgeMethod, GetDefaultOutputPort(repeatNode), GetDefaultInputPort(selectorNode));
ConnectChildren(graphAsset, connectEdgeMethod, selectorNode, downSequence, leapSequence, slamSequence, mainSequence, slamFallbackSequence, chaseSequence); ConnectChildren(graphAsset, connectEdgeMethod, selectorNode, signatureSequence, downSequence, leapSequence, slamSequence, mainSequence, slamFallbackSequence, chaseSequence);
ConnectChildren(graphAsset, connectEdgeMethod, signatureSequence, signatureRefreshNode, signatureHasTargetNode, signatureReadyNode, signatureUseNode);
ConnectChildren(graphAsset, connectEdgeMethod, downSequence, downSelectNode, downReadyNode, downUseNode); ConnectChildren(graphAsset, connectEdgeMethod, downSequence, downSelectNode, downReadyNode, downUseNode);
ConnectChildren(graphAsset, connectEdgeMethod, leapSequence, leapSelectNode, leapReadyNode, leapUseNode); ConnectChildren(graphAsset, connectEdgeMethod, leapSequence, leapSelectNode, leapReadyNode, leapUseNode);
ConnectChildren(graphAsset, connectEdgeMethod, slamSequence, slamRefreshNode, slamHasTargetNode, slamRangeNode, slamTurnNode, slamReadyNode, slamUseNode); ConnectChildren(graphAsset, connectEdgeMethod, slamSequence, slamRefreshNode, slamHasTargetNode, slamRangeNode, slamTurnNode, slamReadyNode, slamUseNode);
@@ -116,6 +123,9 @@ namespace Colosseum.Editor
ConnectChildren(graphAsset, connectEdgeMethod, slamFallbackSequence, fallbackRefreshNode, fallbackHasTargetNode, fallbackRangeNode, fallbackReadyNode, fallbackUseNode); ConnectChildren(graphAsset, connectEdgeMethod, slamFallbackSequence, fallbackRefreshNode, fallbackHasTargetNode, fallbackRangeNode, fallbackReadyNode, fallbackUseNode);
ConnectChildren(graphAsset, connectEdgeMethod, chaseSequence, chaseRefreshNode, chaseHasTargetNode, chaseUseNode); ConnectChildren(graphAsset, connectEdgeMethod, chaseSequence, chaseRefreshNode, chaseHasTargetNode, chaseUseNode);
LinkTarget(signatureRefreshNode, targetVariable);
LinkTarget(signatureHasTargetNode, targetVariable);
LinkTarget(signatureUseNode, targetVariable);
LinkTarget(downSelectNode, targetVariable); LinkTarget(downSelectNode, targetVariable);
LinkTarget(downUseNode, targetVariable); LinkTarget(downUseNode, targetVariable);
LinkTarget(leapSelectNode, targetVariable); LinkTarget(leapSelectNode, targetVariable);

View File

@@ -2,6 +2,7 @@ using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using Colosseum.AI; using Colosseum.AI;
using Colosseum.Abnormalities;
using Colosseum.Combat; using Colosseum.Combat;
using Colosseum.Player; using Colosseum.Player;
using Colosseum.Skills; using Colosseum.Skills;
@@ -46,6 +47,9 @@ namespace Colosseum.Enemy
[FormerlySerializedAs("downPunishPattern")] [FormerlySerializedAs("downPunishPattern")]
[SerializeField] protected BossPatternData punishPattern; [SerializeField] protected BossPatternData punishPattern;
[Tooltip("파티 누킹을 시험하는 시그니처 패턴")]
[SerializeField] protected BossPatternData signaturePattern;
[Header("Phase Thresholds")] [Header("Phase Thresholds")]
[Tooltip("2페이즈 진입 체력 비율")] [Tooltip("2페이즈 진입 체력 비율")]
[Range(0f, 1f)] [SerializeField] protected float phase2HealthThreshold = 0.75f; [Range(0f, 1f)] [SerializeField] protected float phase2HealthThreshold = 0.75f;
@@ -79,6 +83,37 @@ namespace Colosseum.Enemy
[FormerlySerializedAs("phase3SlamInterval")] [FormerlySerializedAs("phase3SlamInterval")]
[Min(1)] [SerializeField] protected int phase3SecondaryInterval = 2; [Min(1)] [SerializeField] protected int phase3SecondaryInterval = 2;
[Header("Signature Pattern")]
[Tooltip("시그니처 패턴을 사용하기 시작하는 최소 페이즈")]
[Min(1)] [SerializeField] protected int signatureMinPhase = 2;
[Tooltip("시그니처 패턴 차단에 필요한 누적 피해 비율")]
[Range(0f, 1f)] [SerializeField] protected float signatureRequiredDamageRatio = 0.1f;
[Tooltip("시그니처 차단 성공 시 보스가 멈추는 시간")]
[Min(0f)] [SerializeField] protected float signatureSuccessStaggerDuration = 2f;
[Tooltip("시그니처 실패 시 모든 플레이어에게 적용할 디버프")]
[SerializeField] protected AbnormalityData signatureFailureAbnormality;
[Tooltip("시그니처 실패 시 모든 플레이어에게 주는 기본 피해")]
[Min(0f)] [SerializeField] protected float signatureFailureDamage = 40f;
[Tooltip("시그니처 실패 시 넉백이 적용되는 반경")]
[Min(0f)] [SerializeField] protected float signatureFailureKnockbackRadius = 8f;
[Tooltip("시그니처 실패 시 다운이 적용되는 반경")]
[Min(0f)] [SerializeField] protected float signatureFailureDownRadius = 3f;
[Tooltip("시그니처 실패 시 넉백 속도")]
[Min(0f)] [SerializeField] protected float signatureFailureKnockbackSpeed = 12f;
[Tooltip("시그니처 실패 시 넉백 지속 시간")]
[Min(0f)] [SerializeField] protected float signatureFailureKnockbackDuration = 0.35f;
[Tooltip("시그니처 실패 시 다운 지속 시간")]
[Min(0f)] [SerializeField] protected float signatureFailureDownDuration = 2f;
[Header("Behavior")] [Header("Behavior")]
[Tooltip("전용 컨텍스트 사용 시 기존 BehaviorGraph를 비활성화할지 여부")] [Tooltip("전용 컨텍스트 사용 시 기존 BehaviorGraph를 비활성화할지 여부")]
[SerializeField] protected bool disableBehaviorGraph = true; [SerializeField] protected bool disableBehaviorGraph = true;
@@ -92,6 +127,9 @@ namespace Colosseum.Enemy
protected GameObject currentTarget; protected GameObject currentTarget;
protected float nextTargetRefreshTime; protected float nextTargetRefreshTime;
protected int meleePatternCounter; protected int meleePatternCounter;
protected bool isSignaturePatternActive;
protected float signatureAccumulatedDamage;
protected float signatureRequiredDamage;
/// <summary> /// <summary>
/// 전용 컨텍스트 사용 시 BehaviorGraph를 비활성화할지 여부 /// 전용 컨텍스트 사용 시 BehaviorGraph를 비활성화할지 여부
@@ -108,6 +146,11 @@ namespace Colosseum.Enemy
/// </summary> /// </summary>
public float PunishSearchRadius => punishSearchRadius; public float PunishSearchRadius => punishSearchRadius;
/// <summary>
/// 시그니처 패턴 진행 여부
/// </summary>
public bool IsSignaturePatternActive => isSignaturePatternActive;
/// <summary> /// <summary>
/// 디버그 로그 출력 여부 /// 디버그 로그 출력 여부
/// </summary> /// </summary>
@@ -185,6 +228,7 @@ namespace Colosseum.Enemy
BossCombatPatternRole.Secondary => secondaryPattern, BossCombatPatternRole.Secondary => secondaryPattern,
BossCombatPatternRole.Mobility => mobilityPattern, BossCombatPatternRole.Mobility => mobilityPattern,
BossCombatPatternRole.Punish => punishPattern, BossCombatPatternRole.Punish => punishPattern,
BossCombatPatternRole.Signature => signaturePattern,
_ => null, _ => null,
}; };
} }
@@ -321,6 +365,40 @@ namespace Colosseum.Enemy
Debug.Log($"[{source}] {message}"); Debug.Log($"[{source}] {message}");
} }
/// <summary>
/// 시그니처 패턴 사용 가능 여부를 반환합니다.
/// </summary>
public bool IsSignaturePatternReady()
{
if (!IsServer || bossEnemy == null || skillController == null)
return false;
if (CurrentPatternPhase < signatureMinPhase)
return false;
if (activePatternCoroutine != null || isSignaturePatternActive)
return false;
if (bossEnemy.IsDead || bossEnemy.IsTransitioning || skillController.IsPlayingAnimation)
return false;
return UsePatternAction.IsPatternReady(gameObject, signaturePattern);
}
/// <summary>
/// 시그니처 패턴을 시작합니다.
/// </summary>
public bool TryStartSignaturePattern(GameObject target)
{
if (!IsSignaturePatternReady())
return false;
GameObject resolvedTarget = IsValidHostileTarget(target) ? target : FindNearestLivingTarget();
currentTarget = resolvedTarget;
activePatternCoroutine = StartCoroutine(RunSignaturePatternCoroutine(signaturePattern, resolvedTarget));
return true;
}
protected virtual bool TryStartPrimaryLoopPattern() protected virtual bool TryStartPrimaryLoopPattern()
{ {
if (currentTarget == null) if (currentTarget == null)
@@ -515,6 +593,220 @@ namespace Colosseum.Enemy
if (behaviorGraphAgent == null) if (behaviorGraphAgent == null)
behaviorGraphAgent = GetComponent<BehaviorGraphAgent>(); behaviorGraphAgent = GetComponent<BehaviorGraphAgent>();
if (enemyBase != null)
{
enemyBase.OnDamageTaken -= HandleBossDamageTaken;
enemyBase.OnDamageTaken += HandleBossDamageTaken;
}
}
public override void OnNetworkDespawn()
{
if (enemyBase != null)
{
enemyBase.OnDamageTaken -= HandleBossDamageTaken;
}
base.OnNetworkDespawn();
}
private IEnumerator RunSignaturePatternCoroutine(BossPatternData pattern, GameObject target)
{
StopMovement();
isSignaturePatternActive = true;
signatureAccumulatedDamage = 0f;
signatureRequiredDamage = bossEnemy.MaxHealth * signatureRequiredDamageRatio;
bool interrupted = false;
bool completed = true;
for (int i = 0; i < pattern.Steps.Count; i++)
{
if (HasMetSignatureBreakThreshold())
{
interrupted = true;
break;
}
PatternStep step = pattern.Steps[i];
if (step.Type == PatternStepType.Wait)
{
float remaining = step.Duration;
while (remaining > 0f)
{
if (HasMetSignatureBreakThreshold())
{
interrupted = true;
break;
}
if (bossEnemy == null || bossEnemy.IsDead)
{
completed = false;
break;
}
remaining -= Time.deltaTime;
yield return null;
}
if (interrupted || !completed)
break;
continue;
}
if (step.Skill == null)
{
completed = false;
Debug.LogWarning($"[{GetType().Name}] 시그니처 패턴 스텝 스킬이 비어 있습니다. Pattern={pattern.PatternName}, Index={i}");
break;
}
if (!skillController.ExecuteSkill(step.Skill))
{
completed = false;
LogDebug(GetType().Name, $"시그니처 스킬 실행 실패: {step.Skill.SkillName}");
break;
}
while (skillController != null && skillController.IsPlayingAnimation)
{
if (HasMetSignatureBreakThreshold())
{
interrupted = true;
break;
}
if (bossEnemy == null || bossEnemy.IsDead)
{
completed = false;
break;
}
yield return null;
}
if (interrupted || !completed)
break;
}
if (interrupted)
{
skillController?.CancelSkill(SkillCancelReason.Interrupt);
UsePatternAction.MarkPatternUsed(gameObject, pattern);
LogDebug(GetType().Name, $"시그니처 차단 성공: 누적 피해 {signatureAccumulatedDamage:F1} / 필요 {signatureRequiredDamage:F1}");
if (signatureSuccessStaggerDuration > 0f)
{
if (enemyBase != null && enemyBase.Animator != null &&
HasAnimatorParameter(enemyBase.Animator, "Hit", AnimatorControllerParameterType.Trigger))
{
enemyBase.Animator.SetTrigger("Hit");
}
float endTime = Time.time + signatureSuccessStaggerDuration;
while (Time.time < endTime && bossEnemy != null && !bossEnemy.IsDead)
{
StopMovement();
yield return null;
}
}
}
else if (completed)
{
UsePatternAction.MarkPatternUsed(gameObject, pattern);
LogDebug(GetType().Name, $"시그니처 실패: 누적 피해 {signatureAccumulatedDamage:F1} / 필요 {signatureRequiredDamage:F1}");
ExecuteSignatureFailure();
}
isSignaturePatternActive = false;
signatureAccumulatedDamage = 0f;
signatureRequiredDamage = 0f;
activePatternCoroutine = null;
}
private void ExecuteSignatureFailure()
{
PlayerNetworkController[] players = FindObjectsByType<PlayerNetworkController>(FindObjectsSortMode.None);
for (int i = 0; i < players.Length; i++)
{
PlayerNetworkController player = players[i];
if (player == null || player.IsDead || !player.gameObject.activeInHierarchy)
continue;
GameObject target = player.gameObject;
if (!IsValidHostileTarget(target))
continue;
player.TakeDamage(signatureFailureDamage, gameObject);
AbnormalityManager abnormalityManager = target.GetComponent<AbnormalityManager>();
if (abnormalityManager != null && signatureFailureAbnormality != null)
{
abnormalityManager.ApplyAbnormality(signatureFailureAbnormality, gameObject);
}
HitReactionController hitReactionController = target.GetComponent<HitReactionController>();
if (hitReactionController == null)
continue;
float distance = Vector3.Distance(transform.position, target.transform.position);
if (distance <= signatureFailureDownRadius)
{
hitReactionController.ApplyDown(signatureFailureDownDuration);
continue;
}
if (distance > signatureFailureKnockbackRadius)
continue;
Vector3 knockbackDirection = target.transform.position - transform.position;
knockbackDirection.y = 0f;
if (knockbackDirection.sqrMagnitude < 0.0001f)
{
knockbackDirection = transform.forward;
}
hitReactionController.ApplyKnockback(knockbackDirection.normalized * signatureFailureKnockbackSpeed, signatureFailureKnockbackDuration);
}
}
private bool HasMetSignatureBreakThreshold()
{
if (!isSignaturePatternActive)
return false;
if (signatureRequiredDamage <= 0f)
return true;
return signatureAccumulatedDamage >= signatureRequiredDamage;
}
private static bool HasAnimatorParameter(Animator animator, string parameterName, AnimatorControllerParameterType parameterType)
{
if (animator == null || string.IsNullOrEmpty(parameterName))
return false;
AnimatorControllerParameter[] parameters = animator.parameters;
for (int i = 0; i < parameters.Length; i++)
{
AnimatorControllerParameter parameter = parameters[i];
if (parameter.type == parameterType && parameter.name == parameterName)
return true;
}
return false;
}
private void HandleBossDamageTaken(float damage)
{
if (!IsServer || !isSignaturePatternActive || damage <= 0f)
return;
signatureAccumulatedDamage += damage;
} }
} }
} }

View File

@@ -9,6 +9,7 @@ namespace Colosseum.Enemy
Secondary = 1, Secondary = 1,
Mobility = 2, Mobility = 2,
Punish = 3, Punish = 3,
Signature = 4,
} }
/// <summary> /// <summary>

View File

@@ -103,7 +103,9 @@ namespace Colosseum.Player
{ {
if (isDead.Value || IsDamageImmune()) return; if (isDead.Value || IsDamageImmune()) return;
currentHealth.Value = Mathf.Max(0f, currentHealth.Value - damage); float finalDamage = damage * GetIncomingDamageMultiplier();
float actualDamage = Mathf.Min(finalDamage, currentHealth.Value);
currentHealth.Value = Mathf.Max(0f, currentHealth.Value - actualDamage);
if (currentHealth.Value <= 0f) if (currentHealth.Value <= 0f)
{ {
@@ -272,8 +274,9 @@ namespace Colosseum.Player
{ {
if (!IsServer || isDead.Value || IsDamageImmune()) return 0f; if (!IsServer || isDead.Value || IsDamageImmune()) return 0f;
float actualDamage = Mathf.Min(damage, currentHealth.Value); float finalDamage = damage * GetIncomingDamageMultiplier();
currentHealth.Value = Mathf.Max(0f, currentHealth.Value - damage); float actualDamage = Mathf.Min(finalDamage, currentHealth.Value);
currentHealth.Value = Mathf.Max(0f, currentHealth.Value - actualDamage);
if (currentHealth.Value <= 0f) if (currentHealth.Value <= 0f)
{ {
@@ -300,6 +303,14 @@ namespace Colosseum.Player
{ {
return abnormalityManager != null && abnormalityManager.IsInvincible; return abnormalityManager != null && abnormalityManager.IsInvincible;
} }
private float GetIncomingDamageMultiplier()
{
if (abnormalityManager == null)
return 1f;
return Mathf.Max(0f, abnormalityManager.IncomingDamageMultiplier);
}
#endregion #endregion
} }
} }

View File

@@ -11,6 +11,7 @@ namespace Colosseum.Skills
{ {
None, None,
Manual, Manual,
Interrupt,
Death, Death,
Stun, Stun,
HitReaction, HitReaction,