2026-10-24 11:40:41 +00:00
// ─────────────────────────────────────────────────────────────────────────────
// WLShurikenRider.cs — 비수 기본 스킬(무자비한 일격) 경로 이펙트(Multiple Slashes 호) 위에 수리검을 여러 개 태운다 (#977 · 2026-10-24)
/ /
// PD 원문: 「비수의 "기본 스킬" 시전 시 Master Stylized Projectile 에셋의 첨부 된 스크린샷과 같은 수리검을 경로 이펙트에 다수 보이게 연출해줘.
// (이때 이펙트 궤적의 색상을 기본 공격처럼 녹색 계열로 바꾸고, 투명도를 조금 부여해서 자연스럽게 구성해줘)」
2026-10-24 15:43:32 +00:00
// ■ 수리검 = JSON prefab(#979 = Resources/WL/Fx/WL_Fx_DaggerShurikenMSP = PD 지정 「Master Stylized Projectile」 Shurikens/ShurikenBullet 사본 · 검은 풍차형 수리검 · AutoRotate 회전 · 흰 반짝임)
2026-10-24 11:40:41 +00:00
// ■ 원본 Skill_BrutalSlash.Co_Update 가 베기 이펙트를 켜는 줄 다음 훅 1줄(Begin) → 그 투사체의 경로 층(layer) 입자 중 every 번째(randomSeed)마다
// 수리검 1개를 붙여 매 LateUpdate 호의 가장자리(입자 위치 + 3D 회전 × 오른쪽 × 메시 반지름 × 크기 × rimFraction)로 — 호가 돌면 수리검이 호를 따라 돈다 · 입자가 사라지면 숨김
2026-10-24 16:36:13 +00:00
// ■ #980 mode "spiral" = 시전자 둘레 나선(회전 · 휘몰아치며 퍼져 나감 · 경로 입자 무관) · faceCamera = 수리검 면이 카메라를 봄
2026-10-25 04:53:08 +00:00
// ■ #983 mode "scatter" = 숨긴 경로 호(구 반지름 4 m 안 무작위 자리 · 0.5 s 동안 불규칙 생성)처럼 시전자 둘레 무작위 자리 · 무작위 시각에 하나씩 나타나
// 바깥쪽으로 날며 점점 세게 꺾여 나선으로 말려 들어가고 크기가 줄며 사라진다(나타난 자리 기준 · 시전자 이동 무관)
2026-10-24 11:40:41 +00:00
// ■ 값 = 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 ;
2026-10-24 12:29:37 +00:00
public int hidePath = 0 ; // #978 1 = 경로 층 그리기만 끔(입자는 그대로 돌아 수리검이 따라감 · 끝나면 되살림) — PD 「요청한 이펙트를 확인할 수 있도록 궤적처럼 보이는 이펙트를 잠시 숨겨줘」
2026-10-24 15:43:32 +00:00
public float minHeight = 0f ; // #979 시전자 발 높이 위 최소 높이(m) — PD 「공중을 부양하는 수리검 컨셉」(호가 땅 가까이 돌아도 수리검은 이 높이 위로)
2026-10-24 16:36:13 +00:00
// #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 = 수리검 면이 카메라를 봄(제 축으로 회전)
2026-10-24 16:59:40 +00:00
public float faceTilt = 0f ; // #980 PD 「스크린샷과 같은 느낌」 — 카메라 정면에서 카메라 오른쪽 축으로 눕히는 각(°) · 70 ≈ 데모처럼 옆에서 본 납작한 원판 + 초승달
2026-10-25 04:53:08 +00:00
// #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(나선 끝에서 사라짐)
2026-10-25 16:06:21 +00:00
// #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) 배율
2026-10-24 11:40:41 +00:00
public string note = "" ;
}
public static class WLShurikenRider
{
public const string ResourcesPath = "WL/WLShurikenRider" ;
public static int Begun , MaxShown ; public static string LastLog = "" ;
2026-10-24 15:43:32 +00:00
public static float MinY = float . MaxValue , MaxY = float . MinValue ; // #979 측정용 — 수리검 높이(시전자 발 기준) 최소 · 최대
2026-10-25 04:53:08 +00:00
public static int Scattered ; public static float MinR = float . MaxValue , MaxR ; // #983 측정용 — scatter 로 나타난 수 · 나타난 자리 반지름 최소 · 최대
2026-10-25 16:06:21 +00:00
public static int PathEmitted , EndBursts , HitBursts , Shakes ; public static float MaxReach ; // #985 측정용 — 수리검 자리에서 뿜은 호 · 끝 터짐 · 명중 섬광 · 흔들림 · 시전자에서 가장 먼 거리
2026-10-24 11:40:41 +00:00
static WLShurikenRiderJson s_cfg ;
2026-10-25 16:06:21 +00:00
[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 ; }
2026-10-24 11:40:41 +00:00
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 > ( ) ;
2026-10-24 16:36:13 +00:00
bool has = pd ! = null & & ! DSUtil . CheckNull ( pd . shooter ) ;
2026-10-25 16:06:21 +00:00
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 + + ;
2026-10-24 11:40:41 +00:00
}
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 > ( ) ;
2026-10-25 16:06:21 +00:00
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 > ( ) ;
2026-10-24 11:40:41 +00:00
2026-10-25 16:06:21 +00:00
public void Begin ( ParticleSystem ps , WLShurikenRiderJson c , float ground , Transform center , Actor shooter )
2026-10-24 11:40:41 +00:00
{
2026-10-25 16:06:21 +00:00
_ps = ps ; _c = c ; _t0 = Time . time ; _on = true ; _bySeed . Clear ( ) ; _ground = ground ; _center = center ; _shooter = shooter ; _shook = false ; _hitPairs . Clear ( ) ;
2026-10-24 11:40:41 +00:00
var r = ps . GetComponent < ParticleSystemRenderer > ( ) ; var m = r ! = null ? r . mesh : null ;
2026-10-24 12:29:37 +00:00
_pathRenderer = c . hidePath ! = 0 & & r ! = null & & r . enabled ? r : null ; if ( _pathRenderer ! = null ) _pathRenderer . enabled = false ;
2026-10-25 16:06:21 +00:00
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 ) ;
}
2026-10-24 11:40:41 +00:00
_rim = m ! = null ? Mathf . Max ( m . bounds . extents . x , m . bounds . extents . y ) : 0.5f ; // 호 메시 반지름(입자 크기 1 기준)
var p = Resources . Load < GameObject > ( c . prefab ) ;
2026-10-25 04:53:08 +00:00
int need = c . mode = = "spiral" ? Mathf . Max ( c . maxRiders , c . spiralCount ) : c . mode = = "scatter" ? Mathf . Max ( c . maxRiders , c . scatterCount ) : c . maxRiders ;
2026-10-25 16:06:21 +00:00
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 ) ;
}
}
2026-10-24 16:36:13 +00:00
foreach ( var g in _riders ) if ( g ! = null ) { g . SetActive ( false ) ; g . transform . localScale = Vector3 . one * c . riderScale ; }
2026-10-24 11:40:41 +00:00
if ( p = = null ) WLShurikenRider . LastLog = "수리검 프리팹 없음 " + c . prefab ;
2026-10-25 04:53:08 +00:00
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 ( )
{
2026-10-25 16:06:21 +00:00
int n = Mathf . Min ( Mathf . Max ( 1 , _c . scatterCount ) , _riders . Count ) ; _sc = new Scat [ n ] ; _emitAcc = new float [ n ] ; _endDone = new bool [ n ] ;
2026-10-25 04:53:08 +00:00
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 ) ;
}
2026-10-24 11:40:41 +00:00
}
void LateUpdate ( )
{
if ( ! _on ) return ;
if ( _ps = = null | | ! _ps . gameObject . activeInHierarchy | | Time . time - _t0 > _c . maxSeconds ) { Stop ( ) ; return ; }
2026-10-24 16:36:13 +00:00
if ( _c . mode = = "spiral" ) { Spiral ( ) ; return ; }
2026-10-25 04:53:08 +00:00
if ( _c . mode = = "scatter" ) { Scatter ( ) ; return ; }
2026-10-24 11:40:41 +00:00
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 ) ;
2026-10-24 15:43:32 +00:00
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 ) ;
2026-10-24 11:40:41 +00:00
var mv = pos - g . transform . position ; // 투사체 이펙트라 진행 방향(앞 = +Z)으로 돌린다 — 꼬리가 지나온 쪽
2026-10-24 16:59:40 +00:00
if ( _c . faceCamera ! = 0 ) g . transform . rotation = Face ( _c . faceTilt , pos ) ; else if ( ! fresh & & mv . sqrMagnitude > 1e-6f ) g . transform . rotation = Quaternion . LookRotation ( mv ) ;
2026-10-24 11:40:41 +00:00
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 ( ) ;
}
2026-10-24 16:36:13 +00:00
// 나선 — 팔이 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 ;
2026-10-24 16:59:40 +00:00
var c0 = _center ! = null ? _center . position : transform . position ; c0 . y = _ground ; var useFace = _c . faceCamera ! = 0 ;
2026-10-24 16:36:13 +00:00
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 ) ;
2026-10-24 16:59:40 +00:00
g . transform . rotation = useFace ? Face ( _c . faceTilt , pos ) : Quaternion . LookRotation ( Vector3 . Cross ( Vector3 . up , dir ) * _c . spiralDir ) ; // 카메라를 보거나 · 공전 진행 방향
2026-10-24 16:36:13 +00:00
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 ( ) ;
}
2026-10-25 04:53:08 +00:00
// #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 ) ;
2026-10-25 16:06:21 +00:00
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 작아지기 시작할 때 터짐
2026-10-25 04:53:08 +00:00
shown + + ;
}
2026-10-25 16:06:21 +00:00
if ( shown > 0 & & ! _shook & & _c . releaseShake > 0f ) { _shook = true ; Shake ( _c . releaseShake , _c . releaseShakeSeconds ) ; } // #985 퍼지는 순간 흔들림
if ( shown > 0 & & ! string . IsNullOrEmpty ( _c . hitFx ) ) HitPass ( ) ;
2026-10-25 04:53:08 +00:00
if ( shown > WLShurikenRider . MaxShown ) WLShurikenRider . MaxShown = shown ;
if ( ! pending ) Stop ( ) ;
}
2026-10-25 16:06:21 +00:00
// #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 + + ;
}
2026-10-24 16:59:40 +00:00
// 수리검 면(사본 루트 +Y)을 카메라 쪽으로 · 앞(+Z) = 카메라 위쪽 — 위에서 내려다보는 카메라에서 납작한 원판이 정면으로 보이고 제 축(AutoRotate)으로 돈다 · tilt° 만큼 카메라 오른쪽 축으로 눕힘
static Quaternion Face ( float tilt , Vector3 pos )
2026-10-24 16:36:13 +00:00
{
var cam = Camera . main ; if ( cam = = null ) return Quaternion . identity ;
2026-10-24 16:59:40 +00:00
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 ;
2026-10-24 16:36:13 +00:00
}
2026-10-24 11:40:41 +00:00
int FreeSlot ( ) { for ( int i = 0 ; i < _riders . Count ; i + + ) if ( ! _bySeed . ContainsValue ( i ) ) return i ; return - 1 ; }
2026-10-24 12:29:37 +00:00
ParticleSystemRenderer _pathRenderer ;
2026-10-25 16:06:21 +00:00
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 ; }
2026-10-24 11:40:41 +00:00
void OnDisable ( ) { Stop ( ) ; }
void OnDestroy ( ) { foreach ( var g in _riders ) if ( g ! = null ) Destroy ( g ) ; }
}
}