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

329 lines
28 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 에셋의 첨부 된 스크린샷과 같은 수리검을 경로 이펙트에 다수 보이게 연출해줘.
// (이때 이펙트 궤적의 색상을 기본 공격처럼 녹색 계열로 바꾸고, 투명도를 조금 부여해서 자연스럽게 구성해줘)」
// ■ 수리검 = 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 = 수리검 면이 카메라를 봄
// ■ #983 mode "scatter" = 숨긴 경로 호(구 반지름 4 m 안 무작위 자리 · 0.5 s 동안 불규칙 생성)처럼 시전자 둘레 무작위 자리 · 무작위 시각에 하나씩 나타나
// 바깥쪽으로 날며 점점 세게 꺾여 나선으로 말려 들어가고 크기가 줄며 사라진다(나타난 자리 기준 · 시전자 이동 무관)
// ■ 값 = 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 float faceTilt = 0f; // #980 PD 「스크린샷과 같은 느낌」 — 카메라 정면에서 카메라 오른쪽 축으로 눕히는 각(°) · 70 ≈ 데모처럼 옆에서 본 납작한 원판 + 초승달
// #983 PD 「기존에 숨긴 궤적과 같이 수리검들이 화면을 불규칙하게 생성되어 날아가며 이때 나선형에 가깝게 회전하듯 사라지는 모습」 — mode "scatter"
public int scatterCount = 12; // 수리검 수(각 구간에 1개씩 = 한쪽에 몰리지 않게 · 구간 안 무작위)
public float scatterWindow = 0.5f; // 나타나는 시각 범위(s · 순서 무작위)
public float scatterMinRadius = 0.8f, scatterMaxRadius = 4f; // 나타나는 자리(시전자 기준 수평 m · 넓이 고르게)
public float scatterMinHeight = 0.6f, scatterMaxHeight = 2f; // 나타나는 높이(발 위 m)
public float scatterMinSeconds = 0.55f, scatterMaxSeconds = 0.85f; // 한 수리검이 나타나서 사라지기까지(s · 무작위)
public float scatterSpeed = 4f, scatterSpeedEnd = 0.3f; // 처음 속도(m/s) · 끝 속도 배율
public float scatterTurnStart = 90f, scatterTurnEnd = 1080f; // 꺾임(°/s) 처음 → 끝(제곱으로 커짐) — 끝이 클수록 촘촘한 나선으로 말려 들어감 · 방향 = spiralDir
public float scatterJitter = 40f; // 바깥 방향에서 흔들림(± °)
public float scatterRise = 0.25f; // 날며 오르는 높이(m)
public float scatterShrinkFrom = 0.35f; // 이 비율부터 크기가 줄어 끝에 0(나선 끝에서 사라짐)
// #985 PD 「비수가 너무 느리게 동작 · 갑자기 사라지는 느낌 · 캐릭터 주위로 퍼져나가야 · 비수 위치를 고려해 궤적 연출을 다시 살리고 · 좀 더 화려하고 타격감」
public int pathFollow = 0; // 1 = 경로 호(layer)를 원래 무작위 자리 대신 날아가는 수리검 자리에서 뿜음(호 자체 방출은 끄고 끝나면 되살림 · hidePath 0 과 함께)
public float pathRate = 30f; // 수리검 1개당 초당 호 수
public float pathSizeMin = 0.6f, pathSizeMax = 1.2f; // 뿜는 호 크기(원본 시작 크기 1~3)
public string endFx = ""; // 수리검이 작아지기 시작할 때(scatterShrinkFrom) 그 자리에 터지는 이펙트(Resources) — 「갑자기 사라짐」 대신 터지며 사라짐
public float endFxScale = 0.2f, endFxLife = 1f;
public string hitFx = ""; // 수리검이 적 몸 hitRadius 안을 지날 때 그 적 몸에(수리검 · 적 쌍마다 1번 · 보이는 타격 · 피해는 원본 스킬 그대로)
public float hitFxScale = 0.2f, hitFxLife = 1f, hitRadius = 0.8f;
public float releaseShake = 0f, releaseShakeSeconds = 0.15f; // 수리검이 처음 나오는 순간 카메라 흔들림(진폭 · 0 = 없음 · 주파수 · 축 = WLHitFeelSettings)
public float hitShake = 0f, hitShakeSeconds = 0.08f; // 명중 섬광마다 카메라 흔들림
public float spinMul = 1f; // 수리검 제 축 회전(사본 AutoRotate) 배율
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 측정용 — 수리검 높이(시전자 발 기준) 최소 · 최대
public static int Scattered; public static float MinR = float.MaxValue, MaxR; // #983 측정용 — scatter 로 나타난 수 · 나타난 자리 반지름 최소 · 최대
public static int PathEmitted, EndBursts, HitBursts, Shakes; public static float MaxReach; // #985 측정용 — 수리검 자리에서 뿜은 호 · 끝 터짐 · 명중 섬광 · 흔들림 · 시전자에서 가장 먼 거리
static WLShurikenRiderJson s_cfg;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] static void Reset() { s_cfg = null; MinY = float.MaxValue; MaxY = float.MinValue; Scattered = 0; MinR = float.MaxValue; MaxR = 0f; PathEmitted = EndBursts = HitBursts = Shakes = 0; MaxReach = 0f; }
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, has ? pd.shooter : null); 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; Actor _shooter;
bool _pathEmOff, _pathEmWas, _shook; float[] _emitAcc; bool[] _endDone; HashSet<long> _hitPairs = new HashSet<long>();
public void Begin(ParticleSystem ps, WLShurikenRiderJson c, float ground, Transform center, Actor shooter)
{
_ps = ps; _c = c; _t0 = Time.time; _on = true; _bySeed.Clear(); _ground = ground; _center = center; _shooter = shooter; _shook = false; _hitPairs.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;
if (c.mode == "scatter" && c.pathFollow != 0) // #985 경로 호를 수리검 자리에서만 — 원래 방출(구 반지름 4 m 무작위)은 끄고 이미 나온 것도 지운다(끝나면 되살림)
{
var em = ps.emission; _pathEmWas = em.enabled; em.enabled = false; _pathEmOff = true; ps.Clear(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.mode == "scatter" ? Mathf.Max(c.maxRiders, c.scatterCount) : c.maxRiders;
while (p != null && _riders.Count < need)
{
var g = Instantiate(p); g.SetActive(false); _riders.Add(g);
if (!Mathf.Approximately(c.spinMul, 1f)) // #985 제 축 회전(사본의 팩 AutoRotate.RotateAngle) 배율 — 만들 때 1번
foreach (var mb in g.GetComponentsInChildren<MonoBehaviour>(true))
{
if (mb == null || mb.GetType().Name != "AutoRotate") continue; var f = mb.GetType().GetField("RotateAngle"); if (f == null) continue;
f.SetValue(mb, (Vector3)f.GetValue(mb) * c.spinMul);
}
}
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;
if (c.mode == "scatter") PlanScatter();
}
// #983 scatter — 수리검마다 나타날 시각 · 자리(각도 · 반지름 · 높이) · 수명 · 첫 방향 흔들림을 미리 정한다(각도는 구간마다 1개 · 시각 순서는 섞음 → 자리와 시각이 따로 논다)
struct Scat { public float t0, ang, r, h, life, jit; public bool on, done; public Vector3 pos; public float head, y0; }
Scat[] _sc;
void PlanScatter()
{
int n = Mathf.Min(Mathf.Max(1, _c.scatterCount), _riders.Count); _sc = new Scat[n]; _emitAcc = new float[n]; _endDone = new bool[n];
var order = new int[n]; for (int i = 0; i < n; i++) order[i] = i;
for (int i = n - 1; i > 0; i--) { int j = Random.Range(0, i + 1); int k = order[i]; order[i] = order[j]; order[j] = k; }
float a0 = Random.Range(0f, 360f), r0 = Mathf.Min(_c.scatterMinRadius, _c.scatterMaxRadius), r1 = Mathf.Max(_c.scatterMinRadius, _c.scatterMaxRadius);
for (int i = 0; i < n; i++)
{
_sc[i].ang = a0 + 360f / n * (i + Random.Range(0.15f, 0.85f));
_sc[i].r = Mathf.Sqrt(Mathf.Lerp(r0 * r0, r1 * r1, Random.value)); // 넓이 고르게(가운데 몰림 없음)
_sc[i].h = Random.Range(_c.scatterMinHeight, _c.scatterMaxHeight);
_sc[i].t0 = _c.scatterWindow * (order[i] + Random.value) / n;
_sc[i].life = Mathf.Max(0.05f, Random.Range(_c.scatterMinSeconds, _c.scatterMaxSeconds));
_sc[i].jit = Random.Range(-_c.scatterJitter, _c.scatterJitter);
}
}
void LateUpdate()
{
if (!_on) return;
if (_ps == null || !_ps.gameObject.activeInHierarchy || Time.time - _t0 > _c.maxSeconds) { Stop(); return; }
if (_c.mode == "spiral") { Spiral(); return; }
if (_c.mode == "scatter") { Scatter(); 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(_c.faceTilt, pos); 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 useFace = _c.faceCamera != 0;
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 = useFace ? Face(_c.faceTilt, pos) : 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();
}
// #983 scatter — 나타날 시각이 되면 그때 시전자 둘레 자리에 켜고 · 바깥쪽(+ 흔들림)으로 날며 꺾임이 처음 → 끝(제곱)으로 커져 나선으로 말려 들어가고 ·
// 속도는 끝 배율까지 줄고 · scatterShrinkFrom 부터 크기가 0 으로(처음 spiralFadeIn 은 0 → 1) · 다 끝나면 멈춤
void Scatter()
{
if (_sc == null) PlanScatter();
float t = Time.time - _t0, dt = Time.deltaTime; bool pending = false; int shown = 0;
var c0 = _center != null ? _center.position : transform.position; c0.y = _ground; var useFace = _c.faceCamera != 0;
for (int i = 0; i < _sc.Length; i++)
{
var g = _riders[i]; if (g == null || _sc[i].done) continue; pending = true;
float ti = t - _sc[i].t0; if (ti < 0f) continue;
if (!_sc[i].on)
{
float a = _sc[i].ang * Mathf.Deg2Rad; var dir = new Vector3(Mathf.Cos(a), 0f, Mathf.Sin(a));
_sc[i].pos = c0 + dir * _sc[i].r + Vector3.up * _sc[i].h; _sc[i].y0 = _sc[i].pos.y; _sc[i].head = _sc[i].ang + _sc[i].jit; _sc[i].on = true; g.SetActive(true);
WLShurikenRider.Scattered++; WLShurikenRider.MaxR = Mathf.Max(WLShurikenRider.MaxR, _sc[i].r); WLShurikenRider.MinR = Mathf.Min(WLShurikenRider.MinR, _sc[i].r);
}
float u = Mathf.Clamp01(ti / _sc[i].life);
if (ti > _sc[i].life) { _sc[i].done = true; g.SetActive(false); continue; }
float turn = Mathf.Lerp(_c.scatterTurnStart, _c.scatterTurnEnd, u * u), spd = _c.scatterSpeed * Mathf.Lerp(1f, _c.scatterSpeedEnd, u);
_sc[i].head += _c.spiralDir * turn * dt; // 각도 증가 = 위에서 볼 때 반시계(spiral 과 같은 규칙)
float h = _sc[i].head * Mathf.Deg2Rad; var vel = new Vector3(Mathf.Cos(h), 0f, Mathf.Sin(h)) * spd;
_sc[i].pos += vel * dt; _sc[i].pos.y = _sc[i].y0 + _c.scatterRise * u;
float k = Mathf.Min(1f, ti / Mathf.Max(0.01f, _c.spiralFadeIn)) * (1f - Mathf.SmoothStep(0f, 1f, Mathf.InverseLerp(_c.scatterShrinkFrom, 1f, u)));
g.transform.position = _sc[i].pos; g.transform.localScale = Vector3.one * (_c.riderScale * k);
g.transform.rotation = useFace ? Face(_c.faceTilt, _sc[i].pos) : Quaternion.LookRotation(vel.sqrMagnitude > 1e-6f ? vel : Vector3.forward);
WLShurikenRider.MinY = Mathf.Min(WLShurikenRider.MinY, _sc[i].pos.y - _ground); WLShurikenRider.MaxY = Mathf.Max(WLShurikenRider.MaxY, _sc[i].pos.y - _ground);
var fl = _sc[i].pos - c0; fl.y = 0f; WLShurikenRider.MaxReach = Mathf.Max(WLShurikenRider.MaxReach, fl.magnitude);
if (_pathEmOff && _ps != null && _c.pathRate > 0f && u < _c.scatterShrinkFrom) // #985 수리검 자리에서 경로 호(궤적)를 뿜는다 — 지나온 길에 남아 원래 수명대로 사라짐
{
_emitAcc[i] += _c.pathRate * dt; if (_emitAcc[i] >= 1f && !_ps.isPlaying) _ps.Play(false); // 호 층 길이(0.5 s)가 지나 멈췄으면 다시 켬(방출은 꺼 둔 채)
while (_emitAcc[i] >= 1f)
{
_emitAcc[i] -= 1f; var ep = new ParticleSystem.EmitParams { applyShapeToPosition = false, startSize = Random.Range(_c.pathSizeMin, _c.pathSizeMax) };
ep.position = _ps.main.simulationSpace == ParticleSystemSimulationSpace.World ? _sc[i].pos : _ps.transform.InverseTransformPoint(_sc[i].pos);
_ps.Emit(ep, 1); WLShurikenRider.PathEmitted++;
}
}
if (!_endDone[i] && u >= _c.scatterShrinkFrom) { _endDone[i] = true; if (Burst(_c.endFx, _sc[i].pos, _c.endFxScale, _c.endFxLife)) WLShurikenRider.EndBursts++; } // #985 작아지기 시작할 때 터짐
shown++;
}
if (shown > 0 && !_shook && _c.releaseShake > 0f) { _shook = true; Shake(_c.releaseShake, _c.releaseShakeSeconds); } // #985 퍼지는 순간 흔들림
if (shown > 0 && !string.IsNullOrEmpty(_c.hitFx)) HitPass();
if (shown > WLShurikenRider.MaxShown) WLShurikenRider.MaxShown = shown;
if (!pending) Stop();
}
// #985 날아가는 수리검이 적 몸(반지름 + hitRadius) 안을 지나면 그 적 몸 앞에 명중 섬광 + 흔들림(수리검 · 적 쌍마다 1번 · 보이는 타격만 · 피해는 원본 스킬 그대로)
void HitPass()
{
var info = ActorInfo.Ins; if (info == null || DSUtil.CheckNull(_shooter)) return;
var c0 = _shooter.Get_position(); var list = info.Get_EnemyActors(_shooter.IsEnemy(), c0, WLShurikenRider.MaxReach + _c.hitRadius + 3f); if (list == null) return;
try
{
for (int k = 0; k < list.Count; k++)
{
var a = list[k]; if (a == null || a.IsDead()) continue; var ap = a.Get_position(); float rad = WL.Combat.WLCombatMotionSettings.BodyRadius(a) + _c.hitRadius;
for (int i = 0; i < _sc.Length; i++)
{
if (!_sc[i].on || _sc[i].done) continue; var v = _sc[i].pos - ap; v.y = 0f; if (v.magnitude > rad) continue;
long key = ((long)a.GetInstanceID() << 8) | (long)(i & 0xff); if (!_hitPairs.Add(key)) continue;
var at = WL.Combat.Impact.WLImpactFx.BodyCenter(a) + WL.Combat.Impact.WLImpactFx.TowardCamera(a);
if (Burst(_c.hitFx, at, _c.hitFxScale * WL.Combat.Impact.WLImpactFx.SizeFactor(a), _c.hitFxLife)) WLShurikenRider.HitBursts++;
if (_c.hitShake > 0f) Shake(_c.hitShake, _c.hitShakeSeconds);
}
}
}
finally { info.Release_Actors(list); }
}
static readonly Dictionary<string, GameObject> s_fx = new Dictionary<string, GameObject>();
static bool Burst(string path, Vector3 pos, float scale, float life)
{
if (string.IsNullOrEmpty(path)) return false; GameObject p;
if (!s_fx.TryGetValue(path, out p) || p == null) { p = Resources.Load<GameObject>(path); s_fx[path] = p; }
if (p == null) { WLShurikenRider.LastLog = "이펙트 없음 " + path; return false; }
var g = Instantiate(p, pos, Quaternion.identity); g.transform.localScale = p.transform.localScale * scale;
foreach (var ps in g.GetComponentsInChildren<ParticleSystem>(true)) { var m = ps.main; m.scalingMode = ParticleSystemScalingMode.Hierarchy; }
Destroy(g, Mathf.Max(0.2f, life)); return true;
}
static void Shake(float amp, float seconds)
{
var st = WL.Feel.WLHitFeel.Settings; var sh = WL.Feel.WLHitFeel.GetShaker(true); if (st == null || sh == null) return;
sh.Shake(amp, seconds, st.shakeFrequency, st.shakeAxisWeight); WLShurikenRider.Shakes++;
}
// 수리검 면(사본 루트 +Y)을 카메라 쪽으로 · 앞(+Z) = 카메라 위쪽 — 위에서 내려다보는 카메라에서 납작한 원판이 정면으로 보이고 제 축(AutoRotate)으로 돈다 · tilt° 만큼 카메라 오른쪽 축으로 눕힘
static Quaternion Face(float tilt, Vector3 pos)
{
var cam = Camera.main; if (cam == null) return Quaternion.identity;
var to = cam.transform.position - pos; if (to.sqrMagnitude < 1e-6f) to = -cam.transform.forward; to.Normalize(); // 원근 — 자기 자리에서 카메라 쪽(화면 가장자리 수리검도 같은 각도로 보이게)
var fwd = Vector3.ProjectOnPlane(cam.transform.up, to); if (fwd.sqrMagnitude < 1e-6f) fwd = Vector3.ProjectOnPlane(Vector3.forward, to);
var face = Quaternion.LookRotation(fwd.normalized, to);
return Quaternion.AngleAxis(tilt, face * Vector3.right) * face;
}
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(); _sc = null; if (_pathEmOff) { _pathEmOff = false; if (_ps != null) { var em = _ps.emission; em.enabled = _pathEmWas; } } 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); }
}
}