feat: 서포트 스킬(힐/보호막/버프) 위협 생성 시스템 추가
- ThreatUtility: 공통 위협 생성 유틸리티 (OverlapSphere 기반 반경 내 적 탐색) - OverlapSphereNonAlloc 버퍼 32→256 확장으로 씬 콜라이더 누락 수정 - 위협 배율 체인: SkillGem × ThreatController × Passive - HealEffect: flatThreat + (actualHeal × threatPercent) 공식 적용 - ShieldEffect: flatThreat + (actualShield × threatPercent) 공식 적용 - AbnormalityEffect: flatThreat 고정 위협 생성 - EditMode 유닛 테스트 9/9 통과 (SupportThreatTests) - 테스트 씬 UI 레이아웃 수정 사항 포함
This commit is contained in:
91
Assets/_Game/Scripts/Combat/ThreatUtility.cs
Normal file
91
Assets/_Game/Scripts/Combat/ThreatUtility.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
using Colosseum.Enemy;
|
||||
using Colosseum.Passives;
|
||||
using Colosseum.Skills;
|
||||
|
||||
namespace Colosseum.Combat
|
||||
{
|
||||
/// <summary>
|
||||
/// 힐, 보호막, 버프 등 비공격 스킬에서 위협을 생성할 때 사용하는 공통 유틸리티입니다.
|
||||
/// 시전자 주변의 적을 탐색하고, 위협 배율 체인을 적용하여 위협을 누적합니다.
|
||||
/// </summary>
|
||||
public static class ThreatUtility
|
||||
{
|
||||
private const float DefaultThreatRadius = 50f;
|
||||
|
||||
private static readonly Collider[] overlapBuffer = new Collider[256];
|
||||
private static readonly HashSet<EnemyBase> processedEnemies = new HashSet<EnemyBase>();
|
||||
|
||||
/// <summary>
|
||||
/// 시전자 주변의 적들에게 위협을 분배합니다.
|
||||
/// 위협 공식: baseThreat × (Gem 배율 × ThreatController 배율 × Passive 배율)
|
||||
/// </summary>
|
||||
/// <param name="caster">시전자 (위협을 얻을 주체)</param>
|
||||
/// <param name="baseThreat">배율 적용 전 기본 위협 수치</param>
|
||||
/// <param name="radius">위협을 생성할 반경 (기본 50m)</param>
|
||||
public static void GenerateThreatOnNearbyEnemies(GameObject caster, float baseThreat, float radius = DefaultThreatRadius)
|
||||
{
|
||||
if (caster == null || baseThreat <= 0f)
|
||||
return;
|
||||
|
||||
float multiplier = ResolveThreatMultiplier(caster);
|
||||
float finalThreat = baseThreat * multiplier;
|
||||
|
||||
processedEnemies.Clear();
|
||||
int hitCount = Physics.OverlapSphereNonAlloc(
|
||||
caster.transform.position,
|
||||
Mathf.Max(radius, 1f),
|
||||
overlapBuffer);
|
||||
|
||||
for (int i = 0; i < hitCount; i++)
|
||||
{
|
||||
Collider hit = overlapBuffer[i];
|
||||
if (hit == null)
|
||||
continue;
|
||||
|
||||
EnemyBase enemy = hit.GetComponentInParent<EnemyBase>();
|
||||
if (enemy == null || processedEnemies.Contains(enemy))
|
||||
continue;
|
||||
|
||||
if (!enemy.UseThreatSystem || enemy.IsDead)
|
||||
continue;
|
||||
|
||||
// 같은 팀(적이 아님)은 제외
|
||||
if (Colosseum.Team.IsSameTeam(caster, enemy.gameObject))
|
||||
continue;
|
||||
|
||||
processedEnemies.Add(enemy);
|
||||
enemy.AddThreat(caster, finalThreat);
|
||||
}
|
||||
|
||||
if (processedEnemies.Count > 0)
|
||||
{
|
||||
CombatBalanceTracker.RecordThreat(caster, finalThreat);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 시전자의 전체 위협 배율을 계산합니다.
|
||||
/// 체인: SkillGem 배율 × ThreatController 배율 × Passive 배율
|
||||
/// </summary>
|
||||
public static float ResolveThreatMultiplier(GameObject source)
|
||||
{
|
||||
if (source == null)
|
||||
return 1f;
|
||||
|
||||
float gemMultiplier = SkillRuntimeModifierUtility.GetThreatMultiplier(source);
|
||||
|
||||
ThreatController threatController = source.GetComponent<ThreatController>();
|
||||
float runtimeMultiplier = threatController != null
|
||||
? Mathf.Max(0f, threatController.CurrentThreatMultiplier)
|
||||
: 1f;
|
||||
|
||||
float passiveMultiplier = PassiveRuntimeModifierUtility.GetThreatGeneratedMultiplier(source);
|
||||
|
||||
return gemMultiplier * runtimeMultiplier * passiveMultiplier;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/_Game/Scripts/Combat/ThreatUtility.cs.meta
Normal file
2
Assets/_Game/Scripts/Combat/ThreatUtility.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3e20589b8f84ac54e8417a01d5ca9be0
|
||||
@@ -1,11 +1,15 @@
|
||||
using UnityEngine;
|
||||
|
||||
using Colosseum.Abnormalities;
|
||||
using Colosseum.Combat;
|
||||
using Colosseum.Skills;
|
||||
|
||||
namespace Colosseum.Skills.Effects
|
||||
{
|
||||
/// <summary>
|
||||
/// 이상 상태 효과
|
||||
/// AbnormalityManager를 통해 대상에게 이상 상태를 적용합니다.
|
||||
/// 이상 상태 효과.
|
||||
/// AbnormalityManager를 통해 대상에게 이상 상태를 적용하며,
|
||||
/// 버프 적용 시 적에게 위협을 생성할 수 있습니다.
|
||||
/// </summary>
|
||||
[CreateAssetMenu(fileName = "AbnormalityEffect", menuName = "Colosseum/Skills/Effects/Abnormality")]
|
||||
public class AbnormalityEffect : SkillEffect
|
||||
@@ -14,6 +18,12 @@ namespace Colosseum.Skills.Effects
|
||||
[Tooltip("적용할 이상 상태 데이터")]
|
||||
[SerializeField] private AbnormalityData abnormalityData;
|
||||
|
||||
[Header("Threat")]
|
||||
[Tooltip("버프 적용 시 생성할 고정 위협 수치")]
|
||||
[Min(0f)] [SerializeField] private float flatThreatAmount = 5f;
|
||||
[Tooltip("위협을 생성할 반경 (시전자 기준)")]
|
||||
[Min(0f)] [SerializeField] private float threatRadius = 50f;
|
||||
|
||||
protected override void ApplyEffect(GameObject caster, GameObject target)
|
||||
{
|
||||
if (target == null) return;
|
||||
@@ -24,6 +34,12 @@ namespace Colosseum.Skills.Effects
|
||||
if (abnormalityManager == null) return;
|
||||
|
||||
abnormalityManager.ApplyAbnormality(abnormalityData, caster);
|
||||
|
||||
// 위협 생성: 고정 수치
|
||||
if (flatThreatAmount > 0f)
|
||||
{
|
||||
ThreatUtility.GenerateThreatOnNearbyEnemies(caster, flatThreatAmount, threatRadius);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -8,7 +8,8 @@ using Colosseum.Skills;
|
||||
namespace Colosseum.Skills.Effects
|
||||
{
|
||||
/// <summary>
|
||||
/// 치료 효과
|
||||
/// 치료 효과.
|
||||
/// 회복량에 비례하여 적에게 위협을 생성할 수 있습니다.
|
||||
/// </summary>
|
||||
[CreateAssetMenu(fileName = "HealEffect", menuName = "Colosseum/Skills/Effects/Heal")]
|
||||
public class HealEffect : SkillEffect
|
||||
@@ -18,6 +19,14 @@ namespace Colosseum.Skills.Effects
|
||||
[Tooltip("회복력 계수 (1.0 = 100%)")]
|
||||
[Min(0f)] [SerializeField] private float healScaling = 1f;
|
||||
|
||||
[Header("Threat")]
|
||||
[Tooltip("힐 사용 시 항상 생성할 고정 위협 수치")]
|
||||
[Min(0f)] [SerializeField] private float flatThreatAmount = 5f;
|
||||
[Tooltip("실제 회복량에 대한 위협 비율 (1.0 = 100%)")]
|
||||
[Range(0f, 10f)] [SerializeField] private float threatPercentOfHeal = 0.5f;
|
||||
[Tooltip("위협을 생성할 반경 (시전자 기준)")]
|
||||
[Min(0f)] [SerializeField] private float threatRadius = 50f;
|
||||
|
||||
protected override void ApplyEffect(GameObject caster, GameObject target)
|
||||
{
|
||||
if (target == null) return;
|
||||
@@ -31,6 +40,13 @@ namespace Colosseum.Skills.Effects
|
||||
{
|
||||
float actualHeal = damageable.Heal(totalHeal);
|
||||
CombatBalanceTracker.RecordHeal(caster, target, actualHeal);
|
||||
|
||||
// 위협 생성: 고정 수치 + (실제 회복량 × 비율)
|
||||
float threat = flatThreatAmount + (actualHeal * threatPercentOfHeal);
|
||||
if (threat > 0f)
|
||||
{
|
||||
ThreatUtility.GenerateThreatOnNearbyEnemies(caster, threat, threatRadius);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@ namespace Colosseum.Skills.Effects
|
||||
{
|
||||
/// <summary>
|
||||
/// 보호막 효과입니다.
|
||||
/// 대상에게 일정 시간 동안 피해를 흡수하는 보호막을 부여합니다.
|
||||
/// 대상에게 일정 시간 동안 피해를 흡수하는 보호막을 부여하며,
|
||||
/// 보호막 수치에 비례하여 적에게 위협을 생성할 수 있습니다.
|
||||
/// </summary>
|
||||
[CreateAssetMenu(fileName = "ShieldEffect", menuName = "Colosseum/Skills/Effects/Shield")]
|
||||
public class ShieldEffect : SkillEffect
|
||||
@@ -31,25 +32,43 @@ namespace Colosseum.Skills.Effects
|
||||
[Tooltip("보호막 활성 여부를 나타내는 이상상태 데이터")]
|
||||
[SerializeField] private AbnormalityData shieldStateAbnormality;
|
||||
|
||||
[Header("Threat")]
|
||||
[Tooltip("보호막 사용 시 항상 생성할 고정 위협 수치")]
|
||||
[Min(0f)] [SerializeField] private float flatThreatAmount = 5f;
|
||||
[Tooltip("실제 보호막 수치에 대한 위협 비율 (1.0 = 100%)")]
|
||||
[Range(0f, 10f)] [SerializeField] private float threatPercentOfShield = 0.5f;
|
||||
[Tooltip("위협을 생성할 반경 (시전자 기준)")]
|
||||
[Min(0f)] [SerializeField] private float threatRadius = 50f;
|
||||
|
||||
protected override void ApplyEffect(GameObject caster, GameObject target)
|
||||
{
|
||||
if (target == null)
|
||||
return;
|
||||
|
||||
float totalShield = CalculateShield(caster);
|
||||
float actualShield = 0f;
|
||||
|
||||
PlayerNetworkController playerNetworkController = target.GetComponent<PlayerNetworkController>();
|
||||
if (playerNetworkController != null)
|
||||
{
|
||||
float actualShield = playerNetworkController.ApplyShield(totalShield, duration, shieldStateAbnormality, caster);
|
||||
actualShield = playerNetworkController.ApplyShield(totalShield, duration, shieldStateAbnormality, caster);
|
||||
CombatBalanceTracker.RecordShield(caster, target, actualShield);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
EnemyBase enemyBase = target.GetComponent<EnemyBase>();
|
||||
if (enemyBase != null)
|
||||
{
|
||||
actualShield = enemyBase.ApplyShield(totalShield, duration, shieldStateAbnormality, caster);
|
||||
CombatBalanceTracker.RecordShield(caster, target, actualShield);
|
||||
}
|
||||
}
|
||||
|
||||
EnemyBase enemyBase = target.GetComponent<EnemyBase>();
|
||||
if (enemyBase != null)
|
||||
// 위협 생성: 고정 수치 + (실제 보호막량 × 비율)
|
||||
float threat = flatThreatAmount + (actualShield * threatPercentOfShield);
|
||||
if (threat > 0f)
|
||||
{
|
||||
float actualShield = enemyBase.ApplyShield(totalShield, duration, shieldStateAbnormality, caster);
|
||||
CombatBalanceTracker.RecordShield(caster, target, actualShield);
|
||||
ThreatUtility.GenerateThreatOnNearbyEnemies(caster, threat, threatRadius);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
8
Assets/_Game/Scripts/Tests.meta
Normal file
8
Assets/_Game/Scripts/Tests.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 989936a82b03b3b4eb3b2aac7da092cb
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user