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

176 lines
14 KiB
C#
Raw Normal View History

// ─────────────────────────────────────────────────────────────────────────────
// WLShurikenRider.cs — 비수 기본 스킬(무자비한 일격) 경로 이펙트(Multiple Slashes 호) 위에 수리검을 여러 개 태운다 (#977 · 2026-10-24)
//
// PD 원문: 「비수의 "기본 스킬" 시전 시 Master Stylized Projectile 에셋의 첨부 된 스크린샷과 같은 수리검을 경로 이펙트에 다수 보이게 연출해줘.
// (이때 이펙트 궤적의 색상을 기본 공격처럼 녹색 계열로 바꾸고, 투명도를 조금 부여해서 자연스럽게 구성해줘)」
// ■ 수리검 = JSON prefab(#979 = Resources/WL/Fx/WL_Fx_DaggerShurikenMSP = PD 지정 「Master Stylized Projectile」 Shurikens/ShurikenBullet 사본 · 검은 풍차형 수리검 · AutoRotate 회전 · 흰 반짝임)
// ■ 원본 Skill_BrutalSlash.Co_Update 가 베기 이펙트를 켜는 줄 다음 훅 1줄(Begin) → 그 투사체의 경로 층(layer) 입자 중 every 번째(randomSeed)마다
// 수리검 1개를 붙여 매 LateUpdate 호의 가장자리(입자 위치 + 3D 회전 × 오른쪽 × 메시 반지름 × 크기 × rimFraction)로 — 호가 돌면 수리검이 호를 따라 돈다 · 입자가 사라지면 숨김
// ■ #980 mode "spiral" = 시전자 둘레 나선(회전 · 휘몰아치며 퍼져 나감 · 경로 입자 무관) · faceCamera = 수리검 면이 카메라를 봄
// ■ 값 = 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 float minHeight = 0f; // #979 시전자 발 높이 위 최소 높이(m) — PD 「공중을 부양하는 수리검 컨셉」(호가 땅 가까이 돌아도 수리검은 이 높이 위로)
// #980 PD 「회전하는 수리검이 플레이어 주위를 나선형으로 휘몰아치며 퍼져나는 연출」 — mode "spiral" = 경로 입자와 무관하게 시전자 둘레 나선
public string mode = "path";
public int spiralCount = 8; // 수리검 수
public int spiralArms = 1; // 나선 팔 수 — 팔마다 같은 자리에서 stagger 간격으로 차례로 나와 같은 나선을 따라감(팔끼리 360/팔 간격)
public float spiralStartRadius = 0.6f, spiralEndRadius = 4f; // 반지름(m) 시작 → 끝(ease-out)
public float spiralSeconds = 1f; // 한 수리검이 시작 → 끝까지(s)
public float spiralDegPerSec = 540f, spiralDir = 1f; // 공전 속도(°/s) · 방향(1 = 위에서 볼 때 반시계)
public float spiralHeight = 0.8f, spiralRise = 0.2f; // 발 위 높이(m) · 퍼지며 더 오르는 높이(m)
public float spiralStagger = 0.05f, spiralEase = 2f; // 다음 수리검까지(s) · 반지름 ease-out 지수
public float spiralFadeIn = 0.08f, spiralFadeOut = 0.2f; // 처음 · 끝에 크기 0 ↔ 1(s)
public int faceCamera = 0; // #980 PD 「수리검 이펙트 방향은 카메라 시점 기준 가장 자연스럽게」 — 1 = 수리검 면이 카메라를 봄(제 축으로 회전)
public string note = "";
}
public static class WLShurikenRider
{
public const string ResourcesPath = "WL/WLShurikenRider";
public static int Begun, MaxShown; public static string LastLog = "";
public static float MinY = float.MaxValue, MaxY = float.MinValue; // #979 측정용 — 수리검 높이(시전자 발 기준) 최소 · 최대
static WLShurikenRiderJson s_cfg;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] static void Reset() { s_cfg = null; MinY = float.MaxValue; MaxY = float.MinValue; }
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>();
bool has = pd != null && !DSUtil.CheckNull(pd.shooter);
run.Begin(path, c, has ? pd.shooter.Get_position().y : pb.transform.position.y, has ? pd.shooter.transform : pb.transform); 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, _ground; bool _on; Transform _center;
public void Begin(ParticleSystem ps, WLShurikenRiderJson c, float ground, Transform center)
{
_ps = ps; _c = c; _t0 = Time.time; _on = true; _bySeed.Clear(); _ground = ground; _center = center;
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);
int need = c.mode == "spiral" ? Mathf.Max(c.maxRiders, c.spiralCount) : c.maxRiders;
while (p != null && _riders.Count < need) { var g = Instantiate(p); g.SetActive(false); _riders.Add(g); }
foreach (var g in _riders) if (g != null) { g.SetActive(false); g.transform.localScale = Vector3.one * c.riderScale; }
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; }
if (_c.mode == "spiral") { Spiral(); 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);
if (_c.minHeight > 0f) pos.y = Mathf.Max(pos.y, _ground + _c.minHeight); // 공중 부양
WLShurikenRider.MinY = Mathf.Min(WLShurikenRider.MinY, pos.y - _ground); WLShurikenRider.MaxY = Mathf.Max(WLShurikenRider.MaxY, pos.y - _ground);
var mv = pos - g.transform.position; // 투사체 이펙트라 진행 방향(앞 = +Z)으로 돌린다 — 꼬리가 지나온 쪽
if (_c.faceCamera != 0) g.transform.rotation = Face(); else 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();
}
// 나선 — 팔이 1 이면 i 번째 수리검이 i × stagger 뒤 각도 360/n × i 에서 · 팔이 여럿이면 팔마다 같은 각도(360/팔 × 팔)에서 차례로 나와
// spiralSeconds 동안 공전하며 반지름 시작 → 끝(ease-out)으로 퍼지고 · 처음 · 끝은 크기로 페이드
void Spiral()
{
float t = Time.time - _t0; int n = Mathf.Min(_c.spiralCount, _riders.Count); bool pending = false; int shown = 0;
var c0 = _center != null ? _center.position : transform.position; c0.y = _ground; var rot = _c.faceCamera != 0 ? Face() : Quaternion.identity;
for (int i = 0; i < n; i++)
{
int arms = Mathf.Max(1, _c.spiralArms), arm = i % arms, idx = i / arms;
var g = _riders[i]; if (g == null) continue; float ti = t - idx * _c.spiralStagger;
if (ti < 0f) { pending = true; continue; }
if (ti > _c.spiralSeconds) { if (g.activeSelf) g.SetActive(false); continue; }
pending = true; if (!g.activeSelf) g.SetActive(true);
float u = ti / Mathf.Max(0.01f, _c.spiralSeconds), e = 1f - Mathf.Pow(1f - u, Mathf.Max(1f, _c.spiralEase));
float r = Mathf.Lerp(_c.spiralStartRadius, _c.spiralEndRadius, e), ang = ((arms > 1 ? 360f / arms * arm : 360f / Mathf.Max(1, n) * i) + _c.spiralDir * _c.spiralDegPerSec * ti) * Mathf.Deg2Rad;
var dir = new Vector3(Mathf.Cos(ang), 0f, Mathf.Sin(ang));
var pos = c0 + dir * r + Vector3.up * (_c.spiralHeight + _c.spiralRise * e);
float k = Mathf.Min(1f, ti / Mathf.Max(0.01f, _c.spiralFadeIn)) * Mathf.Clamp01((_c.spiralSeconds - ti) / Mathf.Max(0.01f, _c.spiralFadeOut));
g.transform.position = pos; g.transform.localScale = Vector3.one * (_c.riderScale * k);
g.transform.rotation = _c.faceCamera != 0 ? rot : Quaternion.LookRotation(Vector3.Cross(Vector3.up, dir) * _c.spiralDir); // 카메라를 보거나 · 공전 진행 방향
WLShurikenRider.MinY = Mathf.Min(WLShurikenRider.MinY, pos.y - _ground); WLShurikenRider.MaxY = Mathf.Max(WLShurikenRider.MaxY, pos.y - _ground);
shown++;
}
if (shown > WLShurikenRider.MaxShown) WLShurikenRider.MaxShown = shown;
if (!pending) Stop();
}
// 수리검 면(사본 루트 +Y)을 카메라 쪽으로 · 앞(+Z) = 카메라 위쪽 — 위에서 내려다보는 카메라에서 납작한 원판이 정면으로 보이고 제 축(AutoRotate)으로 돈다
static Quaternion Face()
{
var cam = Camera.main; if (cam == null) return Quaternion.identity;
return Quaternion.LookRotation(cam.transform.up, -cam.transform.forward);
}
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); }
}
}