// ───────────────────────────────────────────────────────────────────────────── // WLPierceProjectile.cs — 스킬 투사체 관통 + 겹쳐 있는 동안 일정 간격 다단 피해 (#974 · 2026-10-23) // // PD 원문: 「한손검 주력 스킬로 발사하는 검기의 크기를 50% 줄이고, 경로 상 적을 관통하며 모든 적에게 피해를 주도록 구현해줘. // 0.2m 정도 넉백이 적용되어야 하며, 관통하는 동안 0.2초 간격으로 피해를 더 입힐 수 있어야 해. (만약 관통+밀리는 과정에 판정 범위에 있다면 1회 이상 피해를 받을 수 있음)」 // ■ 원본 = ProjectileBase.ProcessCollider(Off_byHit 기본 true) — 처음 맞힌 적에서 투사체를 끈다(Skill_EnergySlash = 루트 BoxCollider 1개 · ProjectileHitChecker 없음 · 코드 실측) // ■ 처리 = 규칙 스킬(맞힌 투사체의 SkillListTableData.e_Skill)이면 원본 명중 줄의 훅(1줄 · 끄기 조건)이 끄지 않는다(관통) + // rehitSeconds > 0 이면 투사체 루트에 WLPierceRehit 을 붙여 겹쳐 있는 적(OnTriggerStay)을 그 간격마다 다시 ProcessCollider(피해 · 명중 이펙트 · 피격 반응 원본 그대로) // ■ 넉백 0.2 m = WLSkillHitFeel 규칙 pushMeters · 크기 50 % = WLProjectileVisual fxScale // ■ 값 = Resources/WL/WLPierceProjectile.json (C45) · 끄기 = enabled 0 // 🔴 Debug.LogError 금지(원본 ErrorLogHook 팝업). // ───────────────────────────────────────────────────────────────────────────── using System.Collections.Generic; using UnityEngine; namespace WL.Combat.Weapon { [System.Serializable] public class WLPierceRule { public string skill = ""; // eEffect 이름(원본 스킬 클래스) public int pierce = 1; // 1 = 맞혀도 끄지 않는다(경로 위 모든 적) public float rehitSeconds; // > 0 = 겹쳐 있는 적을 이 간격(초)마다 다시 맞힌다 public string note = ""; } [System.Serializable] public class WLPierceJson { public int enabled = 1; public WLPierceRule[] rules = new WLPierceRule[0]; public string note = ""; } public static class WLPierceProjectile { public const string ResourcesPath = "WL/WLPierceProjectile"; public static int Pierced, Rehits, Errors; public static string LastLog = ""; static WLPierceJson s_cfg; static Dictionary s_rules; internal static WLPierceJson Cfg { get { if (s_cfg != null) return s_cfg; s_cfg = new WLPierceJson(); s_rules = new Dictionary(); var ta = Resources.Load(ResourcesPath); if (ta == null) { LastLog = "설정 없음 " + ResourcesPath; return s_cfg; } try { JsonUtility.FromJsonOverwrite(ta.text, s_cfg); } catch (System.Exception ex) { LastLog = "설정 파싱 실패 — " + ex.Message; } if (s_cfg.rules != null) foreach (var r in s_cfg.rules) { eEffect ef; if (r != null && System.Enum.TryParse(r.skill, out ef)) s_rules[(int)ef] = r; } return s_cfg; } } [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] static void Boot() { s_cfg = null; s_rules = null; } static WLPierceRule RuleFor(ProjectileData d) { var c = Cfg; if (c.enabled == 0 || d == null || d.m_SkillListTableData == null || s_rules == null) return null; WLPierceRule r; return s_rules.TryGetValue((int)d.m_SkillListTableData.e_Skill, out r) ? r : null; } /// 원본 ProjectileBase.ProcessCollider 명중 줄의 훅 — true = 이 명중에서 투사체를 끄지 않는다(관통) · 다단이면 그 적의 마지막 명중 시각을 기록. public static bool Pierces(ProjectileBase pb, ProjectileData d, Collider other) { try { var r = RuleFor(d); if (r == null || r.pierce == 0 || pb == null) return false; if (r.rehitSeconds > 0f) { var rh = pb.GetComponent(); if (rh == null) rh = pb.gameObject.AddComponent(); rh.Init(pb, r.rehitSeconds); rh.Mark(other); } Pierced++; return true; } catch (System.Exception ex) { Errors++; LastLog = "관통 확인 실패 — " + ex.Message; return false; } } } /// 투사체 루트(충돌체와 같은 오브젝트)에 붙어 겹쳐 있는 적을 간격마다 다시 맞힌다. 처음 닿음은 원본 OnTriggerEnter 가 맡는다. internal sealed class WLPierceRehit : MonoBehaviour { ProjectileBase _pb; float _interval; bool _busy; readonly Dictionary _last = new Dictionary(); public void Init(ProjectileBase pb, float interval) { _pb = pb; _interval = interval; } public void Mark(Collider c) { if (c != null) _last[c.GetInstanceID()] = Time.time; } void OnEnable() { _last.Clear(); } // 풀 재사용 — 이전 발사의 기록을 버린다 void OnTriggerStay(Collider other) { if (_busy || _pb == null || _interval <= 0f || other == null) return; float t; int id = other.GetInstanceID(); if (!_last.TryGetValue(id, out t) || Time.time - t < _interval) return; _busy = true; try { if (_pb.ProcessCollider(other, false)) { _last[id] = Time.time; WLPierceProjectile.Rehits++; } } // 원본 피해 경로(Off_byHit false = 훅을 안 거치므로 여기서 시각 갱신) catch (System.Exception ex) { WLPierceProjectile.Errors++; WLPierceProjectile.LastLog = "다단 실패 — " + ex.Message; } finally { _busy = false; } } } }