feat: 드로그 집행 개시 패턴 및 낙인 디버프 추가
- 드로그 시그니처 패턴 역할과 집행 개시 패턴 데이터를 추가하고 BT 브랜치에 연결 - 시그니처 차단 성공과 실패 흐름을 BossCombatBehaviorContext에 구현하고 authoring 그래프를 재구성 - 집행자의 낙인 이상상태를 추가하고 받는 피해 배율 증가가 플레이어 대미지 계산에 반영되도록 정리 - 집행 실패 시 광역 피해, 넉백, 다운, 낙인 부여 설정을 드로그 프리팹에 연결 - 성공 경로 검증 중 확인된 보스 Hit 트리거 오류를 방어 로직으로 수정 - Unity 플레이 검증으로 집행 개시 실패와 성공 분기를 모두 확인하고 설계값은 원복
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user