// ───────────────────────────────────────────────────────────────────────────── // 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 DamageByKey = new Dictionary(); public static readonly Dictionary HitsByKey = new Dictionary(); 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; } /// 피해 1회. ratio = 공격력 배율(FR 피해 % ÷ 100 × 등급 배율 × 설정). anim = 피격 모션(몹 움찔) 여부. 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++; } /// 중독 — 초당 고정 피해(공격력 × perSecRatio). 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++; } /// 출혈 — 초당 공격력 × perSecRatio 피해(WLStatus 출혈은 최대 HP 비율이라 대상 최대 HP 로 나눠 넣는다). 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++; } /// from 에서 멀어지게 밀어낸다(원본 Knockback_byHitter · 보스 · OBJ 는 원본이 무시). 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 { } } /// to 쪽으로 끌어당긴다(NavMeshAgent.Move · 내비 밖이면 무시). public static void Pull(Actor v, Vector3 to, float speed, float dt) { if (!Valid(v) || IsBoss(v)) return; var ag = v.GetComponent(); 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)); } /// center 반경 안 적 투사체를 끈다(대지 방벽 · 거인의 손 BlockProjectile). 끈 개수. 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(); 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; } } }