Project_WL/Assets/WL/PartSkill/WLPartSkillCombat.cs

146 lines
7.6 KiB
C#
Raw Permalink Normal View History

// ─────────────────────────────────────────────────────────────────────────────
// WLPartSkillCombat.cs — 파츠 스킬 피해 · 상태 · 넉백 · 투사체 막기 (PD 지시 #879 · 2026-10-16)
//
// 피해 = 힘민지 원본 그대로: PC.Get_DamageInfo(가짜 투사체 · 피해 배율) → 치명 판정 포함 → 대상.Cal_Damage(HP · 사망 · 피해 숫자).
// Get_Damage(방어 · HitConfirmed) 를 거치지 않는다 — 파츠 스킬 타격마다 히트스톱 · 카메라 흔들림 · WLStatus 콤보 트리거가 터지지 않게(VS 류 다단 히트).
// WLStatusRunner 도트와 같은 방식(Actor.cs Cal_Damage · Skill_Ignited 가짜 데이터 = 피격 모션 생략). 처치 = Set_Die → CombatEvents.Killed 그대로.
// 상태 = WLStatus.Apply(기절 · 감속 · 중독 · 출혈 — 기존 VFX · 문구 그대로) · 보스에는 기절 · 넉백을 걸지 않는다(원본 KnockBack 규칙과 같음).
// ─────────────────────────────────────────────────────────────────────────────
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
using WL.Combat.Status;
namespace WL.PartSkill
{
public static class WLPartSkillCombat
{
static ProjectileData s_pdHit, s_pdDot;
static readonly Collider[] s_cols = new Collider[48];
public static int Hits, Kills, Blocks, Statuses;
public static double DamageTotal;
public static readonly Dictionary<string, double> DamageByKey = new Dictionary<string, double>();
public static readonly Dictionary<string, int> HitsByKey = new Dictionary<string, int>();
public static PCActor PC { get { return MyValue.bMyPC ? MyValue.MyPC : null; } }
public static bool Valid(Actor a) { return a != null && a.gameObject.activeInHierarchy && !a.IsDead(); }
static ProjectileData Pd(bool anim)
{
if (s_pdHit == null) s_pdHit = new ProjectileData { m_SkillListTableData = new SkillListTableData() };
if (s_pdDot == null) s_pdDot = new ProjectileData { m_SkillListTableData = new SkillListTableData { e_Skill = eEffect.Skill_Ignited } };
return anim ? s_pdHit : s_pdDot;
}
/// <summary>피해 1회. ratio = 공격력 배율(FR 피해 % ÷ 100 × 등급 배율 × 설정). anim = 피격 모션(몹 움찔) 여부.</summary>
public static bool Damage(Actor victim, float ratio, bool anim, string key)
{
var pc = PC;
if (pc == null || !Valid(victim) || ratio <= 0f) return false;
var pd = Pd(anim);
pd.shooter = pc; pd.target = victim; pd.AddDmgPercent = ratio;
DamageInfo d;
try { d = pc.Get_DamageInfo(pd); }
catch (System.Exception ex) { Debug.LogWarning("[WL-879] 피해 계산 실패 — " + ex.Message); return false; }
if (d == null) return false;
d.Beater_pd = pd;
if (d.Damage < 1d) d.Damage = 1d;
double dealt = d.Damage;
victim.Cal_Damage(d);
Hits++; DamageTotal += dealt;
if (!string.IsNullOrEmpty(key))
{
double cur; DamageByKey.TryGetValue(key, out cur); DamageByKey[key] = cur + dealt;
int hc; HitsByKey.TryGetValue(key, out hc); HitsByKey[key] = hc + 1;
}
if (victim.IsDead()) Kills++;
return true;
}
public static double PcAttack()
{
var pc = PC;
try { return pc != null ? pc.Get_StatInfo().Get_Stat(eStat.BaseDmg) : 0d; } catch { return 0d; }
}
static bool IsBoss(Actor a) { return a != null && a.IsSubRole(eSubRol.Boss); }
public static void Stun(Actor v, float sec)
{
if (!Valid(v) || sec <= 0f || IsBoss(v)) return;
if (WLStatus.Apply(v, WLStatusType.Stun, sec, 0f, PC)) Statuses++;
}
public static void Slow(Actor v, float ratio, float sec)
{
if (!Valid(v) || sec <= 0f || ratio <= 0f) return;
if (WLStatus.Apply(v, WLStatusType.SpeedDown, sec, ratio, PC)) Statuses++;
}
/// <summary>중독 — 초당 고정 피해(공격력 × perSecRatio).</summary>
public static void Poison(Actor v, float perSecRatio, float sec)
{
if (!Valid(v) || sec <= 0f) return;
float perSec = (float)(PcAttack() * perSecRatio);
if (WLStatus.Apply(v, WLStatusType.Poison, sec, Mathf.Max(1f, perSec), PC)) Statuses++;
}
/// <summary>출혈 — 초당 공격력 × perSecRatio 피해(WLStatus 출혈은 최대 HP 비율이라 대상 최대 HP 로 나눠 넣는다).</summary>
public static void Bleed(Actor v, float perSecRatio, float sec)
{
if (!Valid(v) || sec <= 0f) return;
double maxHp = v.Get_MaxHP();
if (maxHp <= 0d) return;
float rate = (float)(PcAttack() * perSecRatio / maxHp);
if (WLStatus.Apply(v, WLStatusType.Bleed, sec, Mathf.Max(0.0001f, rate), PC)) Statuses++;
}
/// <summary>from 에서 멀어지게 밀어낸다(원본 Knockback_byHitter · 보스 · OBJ 는 원본이 무시).</summary>
public static void Knock(Actor v, Vector3 from, float dist, float speed)
{
if (!Valid(v) || dist <= 0f || speed <= 0f || IsBoss(v)) return;
try { v.Knockback_byHitter(from, dist / speed, speed, null, true); } catch { }
}
/// <summary>to 쪽으로 끌어당긴다(NavMeshAgent.Move · 내비 밖이면 무시).</summary>
public static void Pull(Actor v, Vector3 to, float speed, float dt)
{
if (!Valid(v) || IsBoss(v)) return;
var ag = v.GetComponent<NavMeshAgent>();
if (ag == null || !ag.enabled || !ag.isOnNavMesh) return;
var d = to - v.Get_position(); d.y = 0f;
float m = d.magnitude;
if (m < 0.35f) return;
ag.Move(d / m * Mathf.Min(m, speed * dt));
}
/// <summary>center 반경 안 적 투사체를 끈다(대지 방벽 · 거인의 손 BlockProjectile). 끈 개수.</summary>
public static int BlockProjectiles(Vector3 center, float radius, string fx)
{
var pc = PC; if (pc == null) return 0;
int n = Physics.OverlapSphereNonAlloc(center, radius, s_cols, ~0, QueryTriggerInteraction.Collide);
int blocked = 0;
for (int i = 0; i < n; i++)
{
var c = s_cols[i]; if (c == null) continue;
var pb = c.GetComponentInParent<ProjectileBase>();
if (pb == null || !pb.gameObject.activeInHierarchy) continue;
var pd = pb.Get_ProjectTileData();
var shooter = pd != null ? pd.shooter : null;
if (shooter == null || shooter == (Actor)pc || shooter.IsEnemy() == pc.IsEnemy()) continue;
var at = pb.transform.position;
pb.gameObject.SetActive(false);
blocked++; Blocks++;
if (!string.IsNullOrEmpty(fx))
{
var r = WLPartSkillRunner.Instance;
WLPartSkillFx.Play(fx, at, r != null ? r.FxScale(fx, WLPartSkillData.S.hitFxScale) : WLPartSkillData.S.hitFxScale, 1f);
}
}
return blocked;
}
}
}