refactor: 집행개시 시그니처 전용 경로를 BT 일반 패턴 스텝으로 통합
- PatternStepType.ChargeWait 및 ChargeStepData 도입으로 충전/차단 판정을 일반 패턴 스텝으로 표현 - UsePatternByRoleAction에서 IsSignature 분기 완전 제거, 일반 패턴 경로로 통합 - BossCombatBehaviorContext에서 시그니처 전용 메서드 10개 이상 제거 - BossStaggerAction(신규): 충전 차단 성공 시 보스 경직 처리 - SignatureFailureEffectsAction(신규): 차단 실패 시 범위 피해/넉백/다운 적용 - RebuildDrogBehaviorAuthoringGraph에 시그니처 Sequence + outcomeBranch 구조 추가 - 집행개시 에셋 스텝 구성을 ChargeWait(3초) → Skill으로 변경 - BossHealthBarUI 시그니처 UI 비활성화, PlayerSkillDebugMenu 디버그 메서드 제거
This commit is contained in:
@@ -80,7 +80,7 @@ namespace Colosseum.Enemy
|
||||
[Tooltip("대형 패턴(시그니처/기동/조합) 직후 기본 패턴 최소 순환 횟수")]
|
||||
[Min(0)] [SerializeField] protected int basicLoopMinCountAfterBigPattern = 2;
|
||||
|
||||
[Header("Signature Pattern")]
|
||||
[Header("시그니처 효과 설정")]
|
||||
[Tooltip("시그니처 패턴 차단에 필요한 누적 피해 비율")]
|
||||
[Range(0f, 1f)] [SerializeField] protected float signatureRequiredDamageRatio = 0.1f;
|
||||
|
||||
@@ -124,12 +124,6 @@ namespace Colosseum.Enemy
|
||||
protected GameObject currentTarget;
|
||||
protected float nextTargetRefreshTime;
|
||||
protected int meleePatternCounter;
|
||||
protected bool isSignaturePatternActive;
|
||||
protected bool isPreviewingSignatureTelegraph;
|
||||
protected float signatureAccumulatedDamage;
|
||||
protected float signatureRequiredDamage;
|
||||
protected float signatureElapsedTime;
|
||||
protected float signatureTotalDuration;
|
||||
protected int basicLoopCountSinceLastBigPattern;
|
||||
|
||||
/// <summary>
|
||||
@@ -177,28 +171,55 @@ namespace Colosseum.Enemy
|
||||
return highestThreatTarget != null ? highestThreatTarget : FindNearestLivingTarget();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 시그니처 패턴 진행 여부
|
||||
/// </summary>
|
||||
public bool IsSignaturePatternActive => isSignaturePatternActive;
|
||||
public string SignaturePatternName => isSignaturePatternActive && signaturePattern != null ? signaturePattern.PatternName : string.Empty;
|
||||
public float SignatureAccumulatedDamage => signatureAccumulatedDamage;
|
||||
public float SignatureRequiredDamage => signatureRequiredDamage;
|
||||
public float SignatureBreakProgressNormalized => signatureRequiredDamage > 0f ? Mathf.Clamp01(signatureAccumulatedDamage / signatureRequiredDamage) : 0f;
|
||||
public float SignatureElapsedTime => signatureElapsedTime;
|
||||
public float SignatureTotalDuration => signatureTotalDuration;
|
||||
public float SignatureCastProgressNormalized => signatureTotalDuration > 0f ? Mathf.Clamp01(signatureElapsedTime / signatureTotalDuration) : 0f;
|
||||
public float SignatureRemainingTime => Mathf.Max(0f, signatureTotalDuration - signatureElapsedTime);
|
||||
|
||||
/// <summary>
|
||||
/// 디버그 로그 출력 여부
|
||||
/// </summary>
|
||||
public bool DebugModeEnabled => debugMode;
|
||||
|
||||
/// <summary>
|
||||
/// 시그니처 실패 시 모든 플레이어에게 주는 기본 피해
|
||||
/// </summary>
|
||||
public float SignatureFailureDamage => signatureFailureDamage;
|
||||
|
||||
/// <summary>
|
||||
/// 시그니처 실패 시 모든 플레이어에게 적용할 디버프
|
||||
/// </summary>
|
||||
public AbnormalityData SignatureFailureAbnormality => signatureFailureAbnormality;
|
||||
|
||||
/// <summary>
|
||||
/// 시그니처 실패 시 넉백이 적용되는 반경
|
||||
/// </summary>
|
||||
public float SignatureFailureKnockbackRadius => signatureFailureKnockbackRadius;
|
||||
|
||||
/// <summary>
|
||||
/// 시그니처 실패 시 다운이 적용되는 반경
|
||||
/// </summary>
|
||||
public float SignatureFailureDownRadius => signatureFailureDownRadius;
|
||||
|
||||
/// <summary>
|
||||
/// 시그니처 실패 시 넉백 속도
|
||||
/// </summary>
|
||||
public float SignatureFailureKnockbackSpeed => signatureFailureKnockbackSpeed;
|
||||
|
||||
/// <summary>
|
||||
/// 시그니처 실패 시 넉백 지속 시간
|
||||
/// </summary>
|
||||
public float SignatureFailureKnockbackDuration => signatureFailureKnockbackDuration;
|
||||
|
||||
/// <summary>
|
||||
/// 시그니처 실패 시 다운 지속 시간
|
||||
/// </summary>
|
||||
public float SignatureFailureDownDuration => signatureFailureDownDuration;
|
||||
|
||||
/// <summary>
|
||||
/// 마지막 충전 차단 시 설정된 경직 시간 (BossPatternActionBase가 설정)
|
||||
/// </summary>
|
||||
public float LastChargeStaggerDuration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 기절 등으로 인해 보스 전투 로직을 진행할 수 없는 상태인지 여부
|
||||
/// </summary>
|
||||
public bool IsBehaviorSuppressed => isPreviewingSignatureTelegraph || (abnormalityManager != null && abnormalityManager.IsStunned);
|
||||
public bool IsBehaviorSuppressed => abnormalityManager != null && abnormalityManager.IsStunned;
|
||||
|
||||
/// <summary>
|
||||
/// 현재 보스 패턴 페이즈
|
||||
@@ -265,11 +286,7 @@ namespace Colosseum.Enemy
|
||||
if (TryStartPunishPattern())
|
||||
return;
|
||||
|
||||
// 2. 집행 개시 (Phase 3 시그니처)
|
||||
if (TryStartSignaturePatternInLoop())
|
||||
return;
|
||||
|
||||
// 3. 조합 패턴 (Phase 3, 드물게)
|
||||
// 2. 조합 패턴 (Phase 3, 드물게)
|
||||
if (TryStartComboPattern())
|
||||
return;
|
||||
|
||||
@@ -458,28 +475,6 @@ namespace Colosseum.Enemy
|
||||
Debug.Log($"[{source}] {message}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 시그니처 패턴 사용 가능 여부를 반환합니다.
|
||||
/// </summary>
|
||||
public bool IsSignaturePatternReady()
|
||||
{
|
||||
if (!IsServer || bossEnemy == null || skillController == null)
|
||||
return false;
|
||||
|
||||
if (IsBehaviorSuppressed)
|
||||
return false;
|
||||
|
||||
if (activePatternCoroutine != null || isSignaturePatternActive)
|
||||
return false;
|
||||
|
||||
if (bossEnemy.IsDead || bossEnemy.IsTransitioning || skillController.IsPlayingAnimation)
|
||||
return false;
|
||||
|
||||
if (!IsPatternGracePeriodAllowed(signaturePattern))
|
||||
return false;
|
||||
|
||||
return IsPatternReady(signaturePattern);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정 패턴이 grace period를 통과했는지 반환합니다.
|
||||
@@ -510,7 +505,7 @@ namespace Colosseum.Enemy
|
||||
if (IsBehaviorSuppressed)
|
||||
return false;
|
||||
|
||||
if (activePatternCoroutine != null || isSignaturePatternActive)
|
||||
if (activePatternCoroutine != null)
|
||||
return false;
|
||||
|
||||
if (bossEnemy.IsDead || bossEnemy.IsTransitioning || skillController.IsPlayingAnimation)
|
||||
@@ -522,49 +517,6 @@ namespace Colosseum.Enemy
|
||||
return IsPatternReady(comboPattern);
|
||||
}
|
||||
|
||||
/// <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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 디버그 또는 특수 연출에서 시그니처 패턴을 강제로 시작합니다.
|
||||
/// </summary>
|
||||
public bool ForceStartSignaturePattern(GameObject target = null)
|
||||
{
|
||||
if (!IsServer || signaturePattern == null || activePatternCoroutine != null || isSignaturePatternActive)
|
||||
return false;
|
||||
|
||||
GameObject resolvedTarget = IsValidHostileTarget(target) ? target : ResolvePrimaryTarget();
|
||||
activePatternCoroutine = StartCoroutine(RunSignaturePatternCoroutine(signaturePattern, resolvedTarget));
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 네트워크 상태와 무관하게 시그니처 전조 모션만 미리보기로 재생합니다.
|
||||
/// 전조 연출 확인용 디버그 경로입니다.
|
||||
/// </summary>
|
||||
public bool PreviewSignatureTelegraph()
|
||||
{
|
||||
if (signaturePattern == null || skillController == null)
|
||||
return false;
|
||||
|
||||
if (activePatternCoroutine != null || isSignaturePatternActive || isPreviewingSignatureTelegraph)
|
||||
return false;
|
||||
|
||||
StartCoroutine(PreviewSignatureTelegraphCoroutine());
|
||||
return true;
|
||||
}
|
||||
|
||||
protected virtual bool TryStartPrimaryLoopPattern()
|
||||
{
|
||||
if (currentTarget == null)
|
||||
@@ -659,22 +611,6 @@ namespace Colosseum.Enemy
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 시그니처 패턴을 context 루프에서 발동합니다.
|
||||
/// grace period와 Phase 제한을 적용합니다.
|
||||
/// </summary>
|
||||
protected virtual bool TryStartSignaturePatternInLoop()
|
||||
{
|
||||
if (!IsSignaturePatternReady())
|
||||
return false;
|
||||
|
||||
if (!IsPatternGracePeriodAllowed(signaturePattern))
|
||||
return false;
|
||||
|
||||
GameObject target = ResolvePrimaryTarget();
|
||||
return TryStartSignaturePattern(target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase 3 조합 패턴을 발동합니다.
|
||||
/// </summary>
|
||||
@@ -864,298 +800,13 @@ 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;
|
||||
signatureElapsedTime = 0f;
|
||||
signatureTotalDuration = CalculatePatternDuration(pattern);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
signatureElapsedTime += Time.deltaTime;
|
||||
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;
|
||||
}
|
||||
|
||||
signatureElapsedTime += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
if (interrupted || !completed)
|
||||
break;
|
||||
}
|
||||
|
||||
if (interrupted)
|
||||
{
|
||||
skillController?.CancelSkill(SkillCancelReason.Interrupt);
|
||||
UsePatternAction.MarkPatternUsed(gameObject, pattern);
|
||||
LogDebug(GetType().Name, $"시그니처 차단 성공: 누적 피해 {signatureAccumulatedDamage:F1} / 필요 {signatureRequiredDamage:F1}");
|
||||
CombatBalanceTracker.RecordBossEvent("집행 개시 차단 성공");
|
||||
|
||||
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}");
|
||||
CombatBalanceTracker.RecordBossEvent("집행 개시 실패");
|
||||
ExecuteSignatureFailure();
|
||||
}
|
||||
|
||||
if (abnormalityManager != null && signatureTelegraphAbnormality != null)
|
||||
{
|
||||
abnormalityManager.RemoveAbnormality(signatureTelegraphAbnormality);
|
||||
}
|
||||
|
||||
isSignaturePatternActive = false;
|
||||
signatureAccumulatedDamage = 0f;
|
||||
signatureRequiredDamage = 0f;
|
||||
signatureElapsedTime = 0f;
|
||||
signatureTotalDuration = 0f;
|
||||
activePatternCoroutine = null;
|
||||
}
|
||||
|
||||
private IEnumerator PreviewSignatureTelegraphCoroutine()
|
||||
{
|
||||
bool restoreBehaviorGraph = behaviorGraphAgent != null && behaviorGraphAgent.enabled;
|
||||
isPreviewingSignatureTelegraph = true;
|
||||
|
||||
if (restoreBehaviorGraph)
|
||||
{
|
||||
behaviorGraphAgent.enabled = false;
|
||||
}
|
||||
|
||||
StopMovement();
|
||||
|
||||
if (skillController != null && skillController.IsPlayingAnimation)
|
||||
{
|
||||
skillController.CancelSkill(SkillCancelReason.Interrupt);
|
||||
yield return null;
|
||||
}
|
||||
|
||||
bool executed = false;
|
||||
for (int i = 0; i < signaturePattern.Steps.Count; i++)
|
||||
{
|
||||
PatternStep step = signaturePattern.Steps[i];
|
||||
if (step == null || step.Type != PatternStepType.Skill || step.Skill == null)
|
||||
continue;
|
||||
|
||||
executed = skillController.ExecuteSkill(step.Skill);
|
||||
break;
|
||||
}
|
||||
|
||||
if (executed)
|
||||
{
|
||||
while (skillController != null && skillController.IsPlayingAnimation)
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (abnormalityManager != null && signatureTelegraphAbnormality != null)
|
||||
{
|
||||
abnormalityManager.RemoveAbnormality(signatureTelegraphAbnormality);
|
||||
}
|
||||
|
||||
if (restoreBehaviorGraph && behaviorGraphAgent != null)
|
||||
{
|
||||
behaviorGraphAgent.enabled = true;
|
||||
}
|
||||
|
||||
isPreviewingSignatureTelegraph = false;
|
||||
}
|
||||
|
||||
private static float CalculatePatternDuration(BossPatternData pattern)
|
||||
{
|
||||
if (pattern == null || pattern.Steps == null)
|
||||
return 0f;
|
||||
|
||||
float totalDuration = 0f;
|
||||
for (int i = 0; i < pattern.Steps.Count; i++)
|
||||
{
|
||||
PatternStep step = pattern.Steps[i];
|
||||
if (step == null)
|
||||
continue;
|
||||
|
||||
if (step.Type == PatternStepType.Wait)
|
||||
{
|
||||
totalDuration += Mathf.Max(0f, step.Duration);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (step.Skill == null)
|
||||
continue;
|
||||
|
||||
AnimationClip skillClip = step.Skill.SkillClip;
|
||||
if (skillClip != null)
|
||||
{
|
||||
float animationSpeed = Mathf.Max(0.01f, step.Skill.AnimationSpeed);
|
||||
totalDuration += skillClip.length / animationSpeed;
|
||||
}
|
||||
|
||||
if (step.Skill.EndClip != null)
|
||||
{
|
||||
totalDuration += step.Skill.EndClip.length;
|
||||
}
|
||||
}
|
||||
|
||||
return totalDuration;
|
||||
}
|
||||
|
||||
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))
|
||||
@@ -1171,13 +822,5 @@ namespace Colosseum.Enemy
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void HandleBossDamageTaken(float damage)
|
||||
{
|
||||
if (!IsServer || !isSignaturePatternActive || damage <= 0f)
|
||||
return;
|
||||
|
||||
signatureAccumulatedDamage += damage;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user