329 lines
28 KiB
C#
329 lines
28 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
||
// 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); }
|
||
}
|
||
}
|