- 보호막을 단일 수치에서 타입별 독립 인스턴스 구조로 리팩터링하고 같은 타입만 갱신되도록 정리 - 플레이어/보스 보호막 상태를 이상상태와 연동해 HUD 및 보스 UI에서 타입별로 식별 가능하게 보강 - 드로그 집행 개시 전조를 집행 준비 이상상태 기반으로 재구성하고 관련 데이터와 보스 컨텍스트를 정리 - 전투 밸런스 계측기와 디버그 메뉴를 추가해 피해, 치유, 보호막, 위협, 패턴 사용량 측정 경로를 마련 - 테스트용 보호막 A/B와 시그니처 전조 자산을 추가하고 기본 포트 7777 원복 후 빌드 및 런타임 검증을 완료
272 lines
8.9 KiB
C#
272 lines
8.9 KiB
C#
using System.Collections.Generic;
|
|
|
|
using UnityEngine;
|
|
|
|
using Colosseum.Abnormalities;
|
|
|
|
namespace Colosseum.Combat
|
|
{
|
|
/// <summary>
|
|
/// 개별 보호막 인스턴스입니다.
|
|
/// 같은 종류의 보호막은 하나로 합쳐지고, 다른 종류의 보호막은 독립적으로 유지됩니다.
|
|
/// </summary>
|
|
[System.Serializable]
|
|
public sealed class ActiveShield
|
|
{
|
|
[SerializeField] private AbnormalityData shieldType;
|
|
[SerializeField] private float remainingAmount;
|
|
[SerializeField] private float remainingDuration;
|
|
[SerializeField] private bool isPermanent;
|
|
[SerializeField] private GameObject source;
|
|
|
|
public AbnormalityData ShieldType => shieldType;
|
|
public float RemainingAmount => remainingAmount;
|
|
public float RemainingDuration => isPermanent ? 0f : remainingDuration;
|
|
public bool IsPermanent => isPermanent;
|
|
public bool IsExpired => !isPermanent && remainingDuration <= 0f;
|
|
public bool IsDepleted => remainingAmount <= 0f;
|
|
public GameObject Source => source;
|
|
|
|
public ActiveShield(AbnormalityData shieldType, float amount, float duration, GameObject source)
|
|
{
|
|
this.shieldType = shieldType;
|
|
remainingAmount = Mathf.Max(0f, amount);
|
|
isPermanent = duration <= 0f;
|
|
remainingDuration = isPermanent ? 0f : duration;
|
|
this.source = source;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 같은 종류 보호막이 다시 적용되었을 때 양을 더하고 지속시간을 갱신합니다.
|
|
/// </summary>
|
|
public float Add(float amount, float duration, GameObject source)
|
|
{
|
|
float appliedAmount = Mathf.Max(0f, amount);
|
|
remainingAmount += appliedAmount;
|
|
|
|
if (duration <= 0f)
|
|
{
|
|
isPermanent = true;
|
|
remainingDuration = 0f;
|
|
}
|
|
else if (!isPermanent)
|
|
{
|
|
remainingDuration = duration;
|
|
}
|
|
|
|
if (source != null)
|
|
{
|
|
this.source = source;
|
|
}
|
|
|
|
return appliedAmount;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 지속시간을 감소시킵니다.
|
|
/// </summary>
|
|
public bool Tick(float deltaTime)
|
|
{
|
|
if (isPermanent || deltaTime <= 0f)
|
|
return false;
|
|
|
|
remainingDuration = Mathf.Max(0f, remainingDuration - deltaTime);
|
|
return IsExpired;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 들어오는 피해를 흡수합니다.
|
|
/// </summary>
|
|
public float Consume(float incomingDamage)
|
|
{
|
|
if (incomingDamage <= 0f || remainingAmount <= 0f)
|
|
return 0f;
|
|
|
|
float absorbed = Mathf.Min(remainingAmount, incomingDamage);
|
|
remainingAmount = Mathf.Max(0f, remainingAmount - absorbed);
|
|
return absorbed;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 대상이 가진 보호막 인스턴스를 관리합니다.
|
|
/// </summary>
|
|
[System.Serializable]
|
|
public sealed class ShieldCollection
|
|
{
|
|
private readonly List<ActiveShield> activeShields = new List<ActiveShield>();
|
|
|
|
public IReadOnlyList<ActiveShield> ActiveShields => activeShields;
|
|
|
|
public float TotalAmount
|
|
{
|
|
get
|
|
{
|
|
float total = 0f;
|
|
for (int i = 0; i < activeShields.Count; i++)
|
|
{
|
|
total += activeShields[i].RemainingAmount;
|
|
}
|
|
|
|
return total;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 보호막을 적용합니다. 같은 종류는 자기 자신만 합산 및 갱신합니다.
|
|
/// </summary>
|
|
public float ApplyShield(AbnormalityData shieldType, float amount, float duration, GameObject source)
|
|
{
|
|
if (amount <= 0f)
|
|
return 0f;
|
|
|
|
ActiveShield existingShield = FindShield(shieldType);
|
|
if (existingShield != null)
|
|
{
|
|
return existingShield.Add(amount, duration, source);
|
|
}
|
|
|
|
activeShields.Add(new ActiveShield(shieldType, amount, duration, source));
|
|
return amount;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 보호막이 적용된 순서대로 피해를 흡수하고 남은 피해를 반환합니다.
|
|
/// </summary>
|
|
public float ConsumeDamage(float incomingDamage)
|
|
{
|
|
if (incomingDamage <= 0f || activeShields.Count == 0)
|
|
return incomingDamage;
|
|
|
|
float remainingDamage = incomingDamage;
|
|
for (int i = 0; i < activeShields.Count && remainingDamage > 0f; i++)
|
|
{
|
|
remainingDamage -= activeShields[i].Consume(remainingDamage);
|
|
}
|
|
|
|
CleanupInactiveShields();
|
|
return Mathf.Max(0f, remainingDamage);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 지속시간 경과를 처리합니다.
|
|
/// </summary>
|
|
public bool Tick(float deltaTime)
|
|
{
|
|
bool changed = false;
|
|
|
|
for (int i = activeShields.Count - 1; i >= 0; i--)
|
|
{
|
|
ActiveShield shield = activeShields[i];
|
|
bool expired = shield.Tick(deltaTime);
|
|
if (!expired && !shield.IsDepleted)
|
|
continue;
|
|
|
|
activeShields.RemoveAt(i);
|
|
changed = true;
|
|
}
|
|
|
|
return changed;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 모든 보호막을 제거합니다.
|
|
/// </summary>
|
|
public void Clear()
|
|
{
|
|
activeShields.Clear();
|
|
}
|
|
|
|
private ActiveShield FindShield(AbnormalityData shieldType)
|
|
{
|
|
for (int i = 0; i < activeShields.Count; i++)
|
|
{
|
|
if (activeShields[i].ShieldType == shieldType)
|
|
return activeShields[i];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private void CleanupInactiveShields()
|
|
{
|
|
for (int i = activeShields.Count - 1; i >= 0; i--)
|
|
{
|
|
if (!activeShields[i].IsExpired && !activeShields[i].IsDepleted)
|
|
continue;
|
|
|
|
activeShields.RemoveAt(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 보호막 수치와 보호막 이상상태를 동기화하는 유틸리티입니다.
|
|
/// </summary>
|
|
public static class ShieldAbnormalityUtility
|
|
{
|
|
/// <summary>
|
|
/// 활성 보호막 목록에 맞춰 보호막 이상상태를 적용하거나 제거합니다.
|
|
/// </summary>
|
|
public static void SyncShieldAbnormalities(
|
|
AbnormalityManager abnormalityManager,
|
|
IReadOnlyList<ActiveShield> activeShields,
|
|
GameObject defaultSource)
|
|
{
|
|
if (abnormalityManager == null)
|
|
return;
|
|
|
|
HashSet<AbnormalityData> desiredShieldStates = new HashSet<AbnormalityData>();
|
|
Dictionary<AbnormalityData, GameObject> shieldSources = new Dictionary<AbnormalityData, GameObject>();
|
|
|
|
if (activeShields != null)
|
|
{
|
|
for (int i = 0; i < activeShields.Count; i++)
|
|
{
|
|
ActiveShield shield = activeShields[i];
|
|
if (shield == null || shield.RemainingAmount <= 0f || shield.ShieldType == null)
|
|
continue;
|
|
|
|
desiredShieldStates.Add(shield.ShieldType);
|
|
|
|
if (!shieldSources.ContainsKey(shield.ShieldType))
|
|
{
|
|
shieldSources.Add(shield.ShieldType, shield.Source != null ? shield.Source : defaultSource);
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach (AbnormalityData shieldState in desiredShieldStates)
|
|
{
|
|
if (abnormalityManager.HasAbnormality(shieldState))
|
|
continue;
|
|
|
|
GameObject source = shieldSources.TryGetValue(shieldState, out GameObject resolvedSource)
|
|
? resolvedSource
|
|
: defaultSource;
|
|
abnormalityManager.ApplyAbnormality(shieldState, source);
|
|
}
|
|
|
|
IReadOnlyList<ActiveAbnormality> activeAbnormalities = abnormalityManager.ActiveAbnormalities;
|
|
List<AbnormalityData> removeList = new List<AbnormalityData>();
|
|
|
|
for (int i = 0; i < activeAbnormalities.Count; i++)
|
|
{
|
|
ActiveAbnormality activeAbnormality = activeAbnormalities[i];
|
|
if (activeAbnormality?.Data == null || !activeAbnormality.Data.isShieldState)
|
|
continue;
|
|
|
|
if (desiredShieldStates.Contains(activeAbnormality.Data))
|
|
continue;
|
|
|
|
removeList.Add(activeAbnormality.Data);
|
|
}
|
|
|
|
for (int i = 0; i < removeList.Count; i++)
|
|
{
|
|
abnormalityManager.RemoveAbnormality(removeList[i]);
|
|
}
|
|
}
|
|
}
|
|
}
|