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

122 lines
8.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ─────────────────────────────────────────────────────────────────────────────
// WLShurikenRider.cs — 비수 기본 스킬(무자비한 일격) 경로 이펙트(Multiple Slashes 호) 위에 수리검을 여러 개 태운다 (#977 · 2026-10-24)
//
// PD 원문: 「비수의 "기본 스킬" 시전 시 Master Stylized Projectile 에셋의 첨부 된 스크린샷과 같은 수리검을 경로 이펙트에 다수 보이게 연출해줘.
// (이때 이펙트 궤적의 색상을 기본 공격처럼 녹색 계열로 바꾸고, 투명도를 조금 부여해서 자연스럽게 구성해줘)」
// ■ 수리검 = Resources/WL/Fx/WL_Fx_DaggerShuriken(MasterStylizedFX ShurikensHit 사본 · 녹색 소용돌이 ShurikenSlash + 흰 별 CoreWhite 만 · 반복 · 회전)
// ■ 원본 Skill_BrutalSlash.Co_Update 가 베기 이펙트를 켜는 줄 다음 훅 1줄(Begin) → 그 투사체의 경로 층(layer) 입자 중 every 번째(randomSeed)마다
// 수리검 1개를 붙여 매 LateUpdate 호의 가장자리(입자 위치 + 3D 회전 × 오른쪽 × 메시 반지름 × 크기 × rimFraction)로 — 호가 돌면 수리검이 호를 따라 돈다 · 입자가 사라지면 숨김
// ■ 값 = Resources/WL/WLShurikenRider.json (C45) · 끄기 = enabled 0
// 🔴 Debug.LogError 금지(원본 팝업).
// ─────────────────────────────────────────────────────────────────────────────
using System.Collections.Generic;
using UnityEngine;
namespace WL.Combat.Weapon
{
[System.Serializable]
public class WLShurikenRiderJson
{
public int enabled = 0;
public string[] skills = new string[0];
public int[] classes = new int[0];
public string layer = "Multiple Slashes";
public string prefab = "WL/Fx/WL_Fx_DaggerShuriken";
public int every = 8; // 경로 입자 randomSeed % every == 0 인 것에만
public int maxRiders = 12;
public float riderScale = 0.12f;
public float rimFraction = 1f; // 호 반지름 대비 자리(1 = 가장자리)
public float maxSeconds = 2.5f;
public int hidePath = 0; // #978 1 = 경로 층 그리기만 끔(입자는 그대로 돌아 수리검이 따라감 · 끝나면 되살림) — PD 「요청한 이펙트를 확인할 수 있도록 궤적처럼 보이는 이펙트를 잠시 숨겨줘」
public string note = "";
}
public static class WLShurikenRider
{
public const string ResourcesPath = "WL/WLShurikenRider";
public static int Begun, MaxShown; public static string LastLog = "";
static WLShurikenRiderJson s_cfg;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] static void Reset() { s_cfg = null; }
internal static WLShurikenRiderJson Cfg
{
get
{
if (s_cfg != null) return s_cfg;
s_cfg = new WLShurikenRiderJson();
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; }
return s_cfg;
}
}
/// <summary>원본 스킬이 베기(경로) 이펙트를 켠 직후 훅 — 규칙에 맞으면 그 투사체에 수리검 러너를 붙인다.</summary>
public static void Begin(ProjectileBase pb)
{
try
{
var c = Cfg; if (c.enabled == 0 || pb == null) return;
if (c.skills != null && c.skills.Length > 0 && System.Array.IndexOf(c.skills, pb.GetType().Name) < 0) return;
var pd = pb.Get_ProjectTileData(); int cls = pd != null && !DSUtil.CheckNull(pd.shooter) ? pd.shooter.Get_ID() : 0;
if (c.classes != null && c.classes.Length > 0 && System.Array.IndexOf(c.classes, cls) < 0) return;
ParticleSystem path = null;
foreach (var ps in pb.GetComponentsInChildren<ParticleSystem>(true)) if (ps.name == c.layer) { path = ps; break; }
if (path == null) { LastLog = "경로 층 없음 " + c.layer; return; }
var run = pb.GetComponent<WLShurikenRiderRunner>(); if (run == null) run = pb.gameObject.AddComponent<WLShurikenRiderRunner>();
run.Begin(path, c); Begun++;
}
catch (System.Exception ex) { LastLog = "실패 — " + ex.Message; }
}
}
internal sealed class WLShurikenRiderRunner : MonoBehaviour
{
ParticleSystem _ps; WLShurikenRiderJson _c; ParticleSystem.Particle[] _buf = new ParticleSystem.Particle[256];
readonly List<GameObject> _riders = new List<GameObject>(); readonly Dictionary<uint, int> _bySeed = new Dictionary<uint, int>();
float _t0, _rim = 0.5f; bool _on;
public void Begin(ParticleSystem ps, WLShurikenRiderJson c)
{
_ps = ps; _c = c; _t0 = Time.time; _on = true; _bySeed.Clear();
var r = ps.GetComponent<ParticleSystemRenderer>(); var m = r != null ? r.mesh : null;
_pathRenderer = c.hidePath != 0 && r != null && r.enabled ? r : null; if (_pathRenderer != null) _pathRenderer.enabled = false;
_rim = m != null ? Mathf.Max(m.bounds.extents.x, m.bounds.extents.y) : 0.5f; // 호 메시 반지름(입자 크기 1 기준)
var p = Resources.Load<GameObject>(c.prefab);
while (p != null && _riders.Count < c.maxRiders) { var g = Instantiate(p); g.SetActive(false); g.transform.localScale = Vector3.one * c.riderScale; _riders.Add(g); }
if (p == null) WLShurikenRider.LastLog = "수리검 프리팹 없음 " + c.prefab;
}
void LateUpdate()
{
if (!_on) return;
if (_ps == null || !_ps.gameObject.activeInHierarchy || Time.time - _t0 > _c.maxSeconds) { Stop(); return; }
int n = _ps.GetParticles(_buf); var alive = new HashSet<uint>(); int shown = 0;
for (int i = 0; i < n; i++)
{
var p = _buf[i]; uint seed = p.randomSeed; if (_c.every > 1 && seed % (uint)_c.every != 0) continue;
alive.Add(seed); int idx;
if (!_bySeed.TryGetValue(seed, out idx)) { idx = FreeSlot(); if (idx < 0) continue; _bySeed[seed] = idx; }
var g = _riders[idx]; bool fresh = !g.activeSelf; if (fresh) g.SetActive(true);
var size = p.GetCurrentSize3D(_ps);
var pos = p.position + Quaternion.Euler(p.rotation3D) * (Vector3.right * _rim * size.x * _c.rimFraction);
var mv = pos - g.transform.position; // 투사체 이펙트라 진행 방향(앞 = +Z)으로 돌린다 — 꼬리가 지나온 쪽
if (!fresh && mv.sqrMagnitude > 1e-6f) g.transform.rotation = Quaternion.LookRotation(mv);
g.transform.position = pos;
shown++;
}
var gone = new List<uint>(); foreach (var kv in _bySeed) if (!alive.Contains(kv.Key)) gone.Add(kv.Key);
foreach (var s in gone) { _riders[_bySeed[s]].SetActive(false); _bySeed.Remove(s); }
if (shown > WLShurikenRider.MaxShown) WLShurikenRider.MaxShown = shown;
if (n == 0 && Time.time - _t0 > 0.3f) Stop();
}
int FreeSlot() { for (int i = 0; i < _riders.Count; i++) if (!_bySeed.ContainsValue(i)) return i; return -1; }
ParticleSystemRenderer _pathRenderer;
void Stop() { _on = false; _bySeed.Clear(); if (_pathRenderer != null) { _pathRenderer.enabled = true; _pathRenderer = null; } foreach (var g in _riders) if (g != null) g.SetActive(false); WLShurikenRider.LastLog = "끝 · 최대 동시 " + WLShurikenRider.MaxShown; }
void OnDisable() { Stop(); }
void OnDestroy() { foreach (var g in _riders) if (g != null) Destroy(g); }
}
}