feat: 드로그 집행 개시 패턴 및 낙인 디버프 추가
- 드로그 시그니처 패턴 역할과 집행 개시 패턴 데이터를 추가하고 BT 브랜치에 연결 - 시그니처 차단 성공과 실패 흐름을 BossCombatBehaviorContext에 구현하고 authoring 그래프를 재구성 - 집행자의 낙인 이상상태를 추가하고 받는 피해 배율 증가가 플레이어 대미지 계산에 반영되도록 정리 - 집행 실패 시 광역 피해, 넉백, 다운, 낙인 부여 설정을 드로그 프리팹에 연결 - 성공 경로 검증 중 확인된 보스 Hit 트리거 오류를 방어 로직으로 수정 - Unity 플레이 검증으로 집행 개시 실패와 성공 분기를 모두 확인하고 설계값은 원복
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b27f3137292d5704d802b5cfb58037e4
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0680aed4d244d7844918883e06e718d5
|
||||
@@ -84,6 +84,11 @@ namespace Colosseum.Abnormalities
|
||||
[Range(0f, 1f)]
|
||||
public float slowMultiplier = 0.5f;
|
||||
|
||||
[Header("피해 배율")]
|
||||
[Tooltip("이상 상태가 적용된 동안 받는 피해 배율 (1 = 기본, 1.1 = 10% 증가)")]
|
||||
[Min(0f)]
|
||||
public float incomingDamageMultiplier = 1f;
|
||||
|
||||
/// <summary>
|
||||
/// 영구 효과인지 확인
|
||||
/// </summary>
|
||||
@@ -98,5 +103,10 @@ namespace Colosseum.Abnormalities
|
||||
/// 제어 효과가 있는지 확인
|
||||
/// </summary>
|
||||
public bool HasControlEffect => controlType != ControlType.None;
|
||||
|
||||
/// <summary>
|
||||
/// 받는 피해 배율 변경 여부
|
||||
/// </summary>
|
||||
public bool HasIncomingDamageModifier => !Mathf.Approximately(incomingDamageMultiplier, 1f);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ namespace Colosseum.Abnormalities
|
||||
private int silenceCount;
|
||||
private int invincibleCount;
|
||||
private float slowMultiplier = 1f;
|
||||
private float incomingDamageMultiplier = 1f;
|
||||
|
||||
// 클라이언트 판정용 제어 효과 동기화 변수
|
||||
private NetworkVariable<int> syncedStunCount = new NetworkVariable<int>(0);
|
||||
@@ -62,6 +63,11 @@ namespace Colosseum.Abnormalities
|
||||
/// </summary>
|
||||
public float MoveSpeedMultiplier => GetCurrentSlowMultiplier();
|
||||
|
||||
/// <summary>
|
||||
/// 받는 피해 배율 (1.0 = 기본, 1.1 = 10% 증가)
|
||||
/// </summary>
|
||||
public float IncomingDamageMultiplier => incomingDamageMultiplier;
|
||||
|
||||
/// <summary>
|
||||
/// 행동 가능 여부 (기절이 아닐 때)
|
||||
/// </summary>
|
||||
@@ -238,6 +244,7 @@ namespace Colosseum.Abnormalities
|
||||
|
||||
ApplyStatModifiers(newAbnormality);
|
||||
ApplyControlEffect(data);
|
||||
RecalculateIncomingDamageMultiplier();
|
||||
SyncAbnormalityAdd(newAbnormality, source);
|
||||
|
||||
OnAbnormalityAdded?.Invoke(newAbnormality);
|
||||
@@ -282,6 +289,7 @@ namespace Colosseum.Abnormalities
|
||||
{
|
||||
RemoveStatModifiers(abnormality);
|
||||
RemoveControlEffect(abnormality.Data);
|
||||
RecalculateIncomingDamageMultiplier();
|
||||
SyncAbnormalityRemove(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 GetCurrentSilenceCount() => IsServer ? silenceCount : syncedSilenceCount.Value;
|
||||
|
||||
@@ -67,6 +67,7 @@ namespace Colosseum.Editor
|
||||
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 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 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));
|
||||
@@ -74,6 +75,11 @@ namespace Colosseum.Editor
|
||||
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 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 downReadyNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(CheckPunishPatternReadyAction), new Vector2(-620f, 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(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, leapSequence, leapSelectNode, leapReadyNode, leapUseNode);
|
||||
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, chaseSequence, chaseRefreshNode, chaseHasTargetNode, chaseUseNode);
|
||||
|
||||
LinkTarget(signatureRefreshNode, targetVariable);
|
||||
LinkTarget(signatureHasTargetNode, targetVariable);
|
||||
LinkTarget(signatureUseNode, targetVariable);
|
||||
LinkTarget(downSelectNode, targetVariable);
|
||||
LinkTarget(downUseNode, targetVariable);
|
||||
LinkTarget(leapSelectNode, targetVariable);
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using Colosseum.AI;
|
||||
using Colosseum.Abnormalities;
|
||||
using Colosseum.Combat;
|
||||
using Colosseum.Player;
|
||||
using Colosseum.Skills;
|
||||
@@ -46,6 +47,9 @@ namespace Colosseum.Enemy
|
||||
[FormerlySerializedAs("downPunishPattern")]
|
||||
[SerializeField] protected BossPatternData punishPattern;
|
||||
|
||||
[Tooltip("파티 누킹을 시험하는 시그니처 패턴")]
|
||||
[SerializeField] protected BossPatternData signaturePattern;
|
||||
|
||||
[Header("Phase Thresholds")]
|
||||
[Tooltip("2페이즈 진입 체력 비율")]
|
||||
[Range(0f, 1f)] [SerializeField] protected float phase2HealthThreshold = 0.75f;
|
||||
@@ -79,6 +83,37 @@ namespace Colosseum.Enemy
|
||||
[FormerlySerializedAs("phase3SlamInterval")]
|
||||
[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")]
|
||||
[Tooltip("전용 컨텍스트 사용 시 기존 BehaviorGraph를 비활성화할지 여부")]
|
||||
[SerializeField] protected bool disableBehaviorGraph = true;
|
||||
@@ -92,6 +127,9 @@ namespace Colosseum.Enemy
|
||||
protected GameObject currentTarget;
|
||||
protected float nextTargetRefreshTime;
|
||||
protected int meleePatternCounter;
|
||||
protected bool isSignaturePatternActive;
|
||||
protected float signatureAccumulatedDamage;
|
||||
protected float signatureRequiredDamage;
|
||||
|
||||
/// <summary>
|
||||
/// 전용 컨텍스트 사용 시 BehaviorGraph를 비활성화할지 여부
|
||||
@@ -108,6 +146,11 @@ namespace Colosseum.Enemy
|
||||
/// </summary>
|
||||
public float PunishSearchRadius => punishSearchRadius;
|
||||
|
||||
/// <summary>
|
||||
/// 시그니처 패턴 진행 여부
|
||||
/// </summary>
|
||||
public bool IsSignaturePatternActive => isSignaturePatternActive;
|
||||
|
||||
/// <summary>
|
||||
/// 디버그 로그 출력 여부
|
||||
/// </summary>
|
||||
@@ -185,6 +228,7 @@ namespace Colosseum.Enemy
|
||||
BossCombatPatternRole.Secondary => secondaryPattern,
|
||||
BossCombatPatternRole.Mobility => mobilityPattern,
|
||||
BossCombatPatternRole.Punish => punishPattern,
|
||||
BossCombatPatternRole.Signature => signaturePattern,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
@@ -321,6 +365,40 @@ namespace Colosseum.Enemy
|
||||
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()
|
||||
{
|
||||
if (currentTarget == null)
|
||||
@@ -515,6 +593,220 @@ namespace Colosseum.Enemy
|
||||
|
||||
if (behaviorGraphAgent == null)
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace Colosseum.Enemy
|
||||
Secondary = 1,
|
||||
Mobility = 2,
|
||||
Punish = 3,
|
||||
Signature = 4,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -103,7 +103,9 @@ namespace Colosseum.Player
|
||||
{
|
||||
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)
|
||||
{
|
||||
@@ -272,8 +274,9 @@ namespace Colosseum.Player
|
||||
{
|
||||
if (!IsServer || isDead.Value || IsDamageImmune()) return 0f;
|
||||
|
||||
float actualDamage = Mathf.Min(damage, currentHealth.Value);
|
||||
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)
|
||||
{
|
||||
@@ -300,6 +303,14 @@ namespace Colosseum.Player
|
||||
{
|
||||
return abnormalityManager != null && abnormalityManager.IsInvincible;
|
||||
}
|
||||
|
||||
private float GetIncomingDamageMultiplier()
|
||||
{
|
||||
if (abnormalityManager == null)
|
||||
return 1f;
|
||||
|
||||
return Mathf.Max(0f, abnormalityManager.IncomingDamageMultiplier);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace Colosseum.Skills
|
||||
{
|
||||
None,
|
||||
Manual,
|
||||
Interrupt,
|
||||
Death,
|
||||
Stun,
|
||||
HitReaction,
|
||||
|
||||
Reference in New Issue
Block a user