Project_WL/Assets/WL/Combat/Weapon/WLPierceProjectile.cs

118 lines
6.4 KiB
C#

// ─────────────────────────────────────────────────────────────────────────────
// 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<int, WLPierceRule> s_rules;
internal static WLPierceJson Cfg
{
get
{
if (s_cfg != null) return s_cfg;
s_cfg = new WLPierceJson(); s_rules = new Dictionary<int, WLPierceRule>();
var ta = Resources.Load<TextAsset>(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;
}
/// <summary>원본 ProjectileBase.ProcessCollider 명중 줄의 훅 — true = 이 명중에서 투사체를 끄지 않는다(관통) · 다단이면 그 적의 마지막 명중 시각을 기록.</summary>
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<WLPierceRehit>();
if (rh == null) rh = pb.gameObject.AddComponent<WLPierceRehit>();
rh.Init(pb, r.rehitSeconds); rh.Mark(other);
}
Pierced++;
return true;
}
catch (System.Exception ex) { Errors++; LastLog = "관통 확인 실패 — " + ex.Message; return false; }
}
}
/// <summary>투사체 루트(충돌체와 같은 오브젝트)에 붙어 겹쳐 있는 적을 간격마다 다시 맞힌다. 처음 닿음은 원본 OnTriggerEnter 가 맡는다.</summary>
internal sealed class WLPierceRehit : MonoBehaviour
{
ProjectileBase _pb;
float _interval;
bool _busy;
readonly Dictionary<int, float> _last = new Dictionary<int, float>();
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; }
}
}
}