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-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-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-24 11:40:41 +00:00
static WLShurikenRiderJson s_cfg ;
2026-10-24 15:43:32 +00:00
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] static void Reset ( ) { s_cfg = null ; MinY = float . MaxValue ; MaxY = float . MinValue ; }
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 ) ;
run . Begin ( path , c , has ? pd . shooter . Get_position ( ) . y : pb . transform . position . y , has ? pd . shooter . transform : pb . transform ) ; 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-24 16:36:13 +00:00
float _t0 , _rim = 0.5f , _ground ; bool _on ; Transform _center ;
2026-10-24 11:40:41 +00:00
2026-10-24 16:36:13 +00:00
public void Begin ( ParticleSystem ps , WLShurikenRiderJson c , float ground , Transform center )
2026-10-24 11:40:41 +00:00
{
2026-10-24 16:36:13 +00:00
_ps = ps ; _c = c ; _t0 = Time . time ; _on = true ; _bySeed . Clear ( ) ; _ground = ground ; _center = center ;
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-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-24 16:36:13 +00:00
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 ; }
2026-10-24 11:40:41 +00:00
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 ; }
2026-10-24 16:36:13 +00:00
if ( _c . mode = = "spiral" ) { Spiral ( ) ; 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-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 ;
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 ; }
2026-10-24 11:40:41 +00:00
void OnDisable ( ) { Stop ( ) ; }
void OnDestroy ( ) { foreach ( var g in _riders ) if ( g ! = null ) Destroy ( g ) ; }
}
}