2026-10-18 15:09:05 +00:00
// ─────────────────────────────────────────────────────────────────────────────
// WLBladeStorm.cs — 대검 각성 「칼날 폭풍」(Skill_Whirlwind) WL 방식: 공격 자세로 회전 + 회오리 이펙트 반복 + 간격 피해 (#923 · 2026-10-18)
/ /
// PD 원문(#923): 「대검 각성 스킬 이펙트를 해당 에셋의 첨부한 스크린샷으로 교체하고, 플레이어 캐릭터가 6초 동안 공격 모션을 고정한 상태로
// 좌/우 반전하는 동안 이펙트를 반복 재생시켜줘. (플레이어는 이펙트 중앙에 배치된 상태로 마치 검을 회전하며 공격하는 형태로 동작해야 해)
// → 6초가 지나면 자연스럽게 회전을 멈추고 정지해야하며, 구르기를 하거나 스킬 유지 시간이 만료 된 경우 스킬을 종료해야 해.
// 이 스킬은 시전 중 움직일 수 있으며, 피격 범위에 있는 적은 0.5초 간격으로 피해를 받을 수 있어야 해.」
/ /
// ■ 원리 — 원본 Skill_Whirlwind.Set 의 WL 훅 1줄(Take)이 넘겨받는다: 원본 Co_Update(3 초 · 판정 상자 0.3 초 켬/끔 · 끝에 대기)를 돌리지 않고
// ① 원본 이펙트(BlueYellow_Sword 등)와 판정 상자를 끈다
// ② 조이스틱 상태 2(원본과 같음 — 시전 중 조이스틱 이동 · 달리기 동작 없이 · 자동 전투는 WLSkillFollow 가 가까운 적을 따라감)
// ③ 애니메이터 = poseState 를 poseNormalized 에 멈춘다(클립 이벤트 끔 · 다른 동작이 끼어들면 매 프레임 되돌림)
// ④ PC 를 spinDegPerSec 로 수평 회전(LateUpdate · 이동 방향으로 돌리는 원본 조이스틱 Update · WLSkillFollow 보다 나중)
// ⑤ 회오리 이펙트(fx)를 PC 발 중심에 fxRepeat 초마다 다시 띄운다(PC 를 따라감 · 중심 어긋남 fxOffset 보정)
// ⑥ hitInterval 초마다 PC 발 기준 radius m + 적 몸 반지름 안의 모든 적에게 원본 한 대 피해(ProcessCollider · 명중 이펙트 · 피격 반응 그대로)
// ⑦ duration 초가 지나면 판정 · 새 이펙트를 멈추고 stopSeconds 동안 회전 감속 → 투사체 끔 → 조이스틱 0 → 대기(원본 끝과 같은 순서)
// 구르기 등으로 스킬 상태를 벗어나면 즉시 끝(애니메이터 · 조이스틱 상태 되돌림 · 원본은 투사체가 꺼지면 Co_Update 가 함께 멈춰 조이스틱 상태 2 를
// 되돌리는 줄에 닿지 않는 구조 — 코드 확인 · Play 미재현)
// ■ #924 PD 「대검 각성 스킬 사용 중에는 이동 속도를 50% 줄이고, 이펙트가 몸을 완전히 가려지도록 이펙트로 캐릭터의 몸을 완전히 감싸줘.
// 그리고 노란색으로 깜박이며 퍼지는 이펙트는 숨겨줘.」
// ⑧ moveMul = 이 PC 의 이동 속도 배율(원본 Actor.Get_MoveSpeed 끝 훅 1줄 → 조이스틱 이동 · WLSkillFollow 자동 따라가기 둘 다)
// ⑨ wraps = 몸을 감싸는 반복 이펙트 목록(회전 검기 기둥 · Hyper Casual 연기 소용돌이 — 시작에 1 개씩 · PC 를 따라감)
// ⑪ fadeSync 1 = 유지 시간이 지나면 새 이펙트 · 판정을 멈추고, 남은 이펙트 전부(감싸기 · 회오리)를 fadeSeconds 동안 함께 흐려 지우며(WLFxFade = 방출 멈춤 +
// 입자 알파를 0 으로) 같은 시간 동안 회전을 감속 → 이펙트가 사라지는 그 순간 모션 끝(#924 PD 「이펙트 소멸과 모션 종료가 일치」)
// — 남은 입자 수명으로 기다리면 보이지 않는 긴 수명 입자(회전 검기 기둥 최상위 이미터 5 초) 때문에 이펙트가 다 사라진 뒤에도 모션이 남았다(Play 캡처)
// ⑩ hideBody = 시전 동안 PC 몸(무기 소켓 밖 SkinnedMeshRenderer — 팔 · 몸 · 얼굴 · 머리카락 · 머리 · 다리)을 숨긴다(검은 보임 · 끝 · 구르기에 되돌림)
// — 회오리 · 칼날 기둥은 반투명/가산 이펙트라 겹쳐도 몸이 비쳐 「완전히 가림」이 안 됐다(Play 시안 캡처: 1~3 겹 · 반투명 토네이도 · 반투명 재질판)
// · 노란 빛살 = 회오리 사본 WL_Fx_BladeStorm 의 floor_ring(시작색 0.98/0.96/0.75 · 초당 8 개 · 0.3 초 방사형 빛살)을 사본에서 끔
// ■ 값 = Resources/WL/WLBladeStorm.json (C45) · 끄기 = enabled 0 → 원본 동작 · 원본 수정 = Skill_Whirlwind.Set 1줄 · Actor.Get_MoveSpeed 1줄
// 🔴 Debug.LogError 금지(원본 ErrorLogHook 이 팝업을 띄운다).
// ─────────────────────────────────────────────────────────────────────────────
using System.Collections ;
using System.Collections.Generic ;
using UnityEngine ;
namespace WL.Combat.Weapon
{
[System.Serializable]
public class WLBladeStormJson
{
public int enabled = 0 ;
public int [ ] classes = new int [ 0 ] ; // 비우면 모든 직업
public float duration = 6f ; // 회전 · 판정 유지(초)
public float hitInterval = 0.5f ; // 피해 간격(초)
public float firstHit = 0.1f ; // 첫 피해 시각(초)
public float radius = 2f ; // 피해 반경(m · PC 발 기준 수평) + 적 몸 반지름
public string poseState = "" ; // 멈출 공격 자세 상태(PC 애니메이터 0 층)
public float poseNormalized = 0.3f ;
public float spinDegPerSec = 720f ;
public int spinSign = - 1 ; // 1 = 위에서 볼 때 시계 방향 · -1 = 반시계
public float spinUpSeconds = 0.15f ; // 시작 가속(초)
public float stopSeconds = 0.5f ; // 끝 감속(초) — 자연스럽게 멈춤
public string fx = "" ; // 회오리 이펙트(Resources 경로)
public float fxScale = 0.45f ;
public float fxRepeat = 0.5f ; // 다시 띄우는 간격(초)
public float fxLife = 1.2f ;
public float [ ] fxOffset = new float [ 3 ] ; // 프리팹 소용돌이 중심의 원점 어긋남(배율 1 · m) — 빼서 PC 발이 정중앙에
public int hideOriginal = 1 ;
public float moveMul = 1f ; // #924 시전 중 이 PC 의 이동 속도 배율
public WLBladeStormWrap [ ] wraps = new WLBladeStormWrap [ 0 ] ; // #924 몸을 감싸는 반복 이펙트들
public int hideBody = 0 ; // #924 1 = 시전 동안 몸(무기 소켓 밖 SkinnedMeshRenderer)을 숨김
public int fadeSync = 0 ; // #924 1 = 유지 시간 뒤 남은 이펙트를 fadeSeconds 동안 흐려 지우며 같은 시간 감속 · 그 끝에 모션 끝
public float fadeSeconds = 1.5f ;
public int fadeDimAdditive = 0 ; // #934 1 = 흐려 지울 때 가산(더하기) 재질 입자는 색도 함께 낮춤(알파만으론 회오리 가산 심이 밝은 채 남던 것)
// #929 PD 「대검 각성 스킬에 피격 된 적에게 히트 이펙트를 발생시켜 줘.」 — 판정마다 맞은 적 몸 중심에(시전자 → 적 방향)
public string hitFx = "" ;
public float hitFxScale = 0.3f ;
public float hitFxLife = 1.2f ;
// #930 PD 「캐릭터의 색상이 푸른색으로 변하지 않게 해줘.」 — 시전 동안 PC 몸 · 무기 재질을 사본으로 바꿔 렌더 순서를 이만큼 뒤로(툰 2000 → 3050 = 반투명 이펙트 3000 뒤에 그림)
// → 감싸기 · 회오리 · 칼날 이펙트가 몸 위를 덮어 몸 색이 물들지 않는다(이펙트는 몸 둘레에 그대로) · 끝 · 끊김에 원래 재질로 되돌리고 사본 파기 · 0 = 끔
public int bodyQueueAdd = 0 ;
// #932 PD 「대검 각성 스킬 시전 때 캐릭터의 색상이 바뀌지 않도록 할 수 없을까? (적어도 캐릭터의 상체가 좀 더 잘 보였으면 좋겠어.)」 — 이름이 이 접두어로 시작하는 렌더러만 뒤로(빈 배열 = 전부) · 무기 소켓 안 렌더러는 bodyOnTopWeapon 1 일 때만
public string [ ] bodyOnTopNames = new string [ 0 ] ;
public int bodyOnTopWeapon = 0 ;
public string note = "" ;
}
[System.Serializable]
public class WLBladeStormWrap
{
public string fx = "" ; // Resources 경로(반복 재생 프리팹)
public float scale = 0.45f ;
public float height = 0f ; // PC 발 기준 높이(m)
// #927 위에서 비스듬히 보는 카메라에선 몸 가운데 깊이의 반투명 이펙트가 몸 앞면에 가려 몸이 이펙트 앞에 보인다 →
// 카메라 쪽 시선 방향으로 이만큼(m) 당겨 화면 위치는 그대로 두고 깊이만 몸보다 앞에(0 = 당기지 않음)
public float towardCamera = 0f ;
2026-10-21 04:05:18 +00:00
// #958 PD 「대검 각성 회오리가 하얗게 뭉개져 보임 → 회오리 느낌이 나도록 회전 방향의 선이나 그라데이션이 보이게(참고 이미지)」 — 층별 재질 값 덮기
public WLFxLayerOverride [ ] layers = new WLFxLayerOverride [ 0 ] ;
}
/// <summary>#958 이펙트 렌더러별 재질 값 덮기(프로퍼티 블록 · 재질 인스턴스 0) — names = 렌더러 이름(| 로 여럿 · * = 나머지 전부) ·
/// emission ≥ 0 = _Emission(HS 셰이더 발광 배율 · 원본 5 는 블룸 없는 화면에서 흰색 포화 = 「하얗게 뭉개짐」 편집 모드 실측) · tint = _Color(r,g,b)</summary>
[System.Serializable]
public class WLFxLayerOverride
{
public string names = "*" ;
public float emission = - 1f ;
public float [ ] tint = new float [ 0 ] ;
2026-10-18 15:09:05 +00:00
}
public static class WLBladeStorm
{
public const string ResourcesPath = "WL/WLBladeStorm" ;
2026-10-21 04:05:18 +00:00
public static int Taken , Ticks , Hits , FxSpawned , Ends , Aborts , Errors , LayerOverrides ;
2026-10-18 15:09:05 +00:00
public static float LastSpinTotal , LastSeconds , LastFade ;
public static int HitFxShown , BodyOnTop ;
public static string LastLog = "" ;
static WLBladeStormJson s_cfg ;
static Actor s_slowActor ; static float s_slowMul = 1f ;
/// <summary>#924 원본 Actor.Get_MoveSpeed 끝 훅 — 칼날 폭풍 중인 그 PC 만 moveMul(그 밖 = 1).</summary>
public static float MoveMul ( Actor a ) { return ReferenceEquals ( a , s_slowActor ) ? s_slowMul : 1f ; }
internal static void SetSlow ( Actor a , float mul ) { s_slowActor = a ; s_slowMul = Mathf . Clamp ( mul , 0.05f , 1f ) ; }
internal static void ClearSlow ( Actor a ) { if ( ReferenceEquals ( a , s_slowActor ) ) { s_slowActor = null ; s_slowMul = 1f ; } }
internal static WLBladeStormJson Cfg
{
get
{
if ( s_cfg ! = null ) return s_cfg ;
s_cfg = new WLBladeStormJson ( ) ;
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 ) { s_cfg . enabled = 0 ; LastLog = "설정 파싱 실패 — " + ex . Message ; }
return s_cfg ;
}
}
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
static void Boot ( ) { s_cfg = null ; s_slowActor = null ; s_slowMul = 1f ; }
/// <summary>원본 Skill_Whirlwind.Set 훅 — true 면 WL 이 이 투사체를 맡는다(원본 Co_Update 건너뜀).</summary>
public static bool Take ( ProjectileBase pb , GameObject hitChecker )
{
try
{
var c = Cfg ;
if ( c . enabled = = 0 | | pb = = null ) return false ;
var data = pb . Get_ProjectTileData ( ) ;
var pc = data ! = null ? data . shooter as PCActor : null ;
if ( pc = = null | | ! pc . IsMainPC ( ) ) return false ;
if ( c . classes ! = null & & c . classes . Length > 0 & & System . Array . IndexOf ( c . classes , pc . Get_ID ( ) ) < 0 ) return false ;
var run = pb . GetComponent < WLBladeStormRunner > ( ) ;
if ( run = = null ) run = pb . gameObject . AddComponent < WLBladeStormRunner > ( ) ;
run . Begin ( pb , pc , hitChecker , c ) ;
Taken + + ; LastLog = "넘겨받음 #" + Taken ;
return true ;
}
catch ( System . Exception ex ) { Errors + + ; LastLog = "넘겨받기 실패 — " + ex . Message ; return false ; }
}
}
[DefaultExecutionOrder(1000)] // PC 회전을 이동 방향 회전(원본 조이스틱 · WLSkillFollow 의 Update)보다 나중에
internal sealed class WLBladeStormRunner : MonoBehaviour
{
ProjectileBase _pb ; PCActor _pc ; Animator _an ; GameObject _checker ; WLBladeStormJson _c ;
bool _running , _animOwned ; float _yaw , _spin , _spinTotal , _t0 ;
Coroutine _co ;
readonly List < GameObject > _fx = new List < GameObject > ( ) ;
readonly List < Renderer > _hidden = new List < Renderer > ( ) ;
readonly List < GameObject > _wraps = new List < GameObject > ( ) ; readonly List < float > _wrapH = new List < float > ( ) , _wrapT = new List < float > ( ) ;
readonly List < float > _fxEnd = new List < float > ( ) ; // 회오리 인스턴스가 지워질 시각(_fx 와 같은 순서)
readonly List < Renderer > _body = new List < Renderer > ( ) ;
readonly List < Renderer > _qR = new List < Renderer > ( ) ; readonly List < Material [ ] > _qM = new List < Material [ ] > ( ) ; readonly List < Material > _qNew = new List < Material > ( ) ;
static GameObject s_prefab ; static string s_prefabPath ;
static readonly Dictionary < string , GameObject > s_wrapPrefabs = new Dictionary < string , GameObject > ( ) ;
public void Begin ( ProjectileBase pb , PCActor pc , GameObject checker , WLBladeStormJson c )
{
if ( _running ) End ( false , false ) ;
_pb = pb ; _pc = pc ; _an = pc . m_animation ; _checker = checker ; _c = c ;
if ( _checker ! = null ) _checker . SetActive ( false ) ;
if ( c . hideOriginal ! = 0 ) Hide ( ) ;
pc . Set_JoystickStatus ( 2 ) ;
if ( c . moveMul < 0.999f ) WLBladeStorm . SetSlow ( pc , c . moveMul ) ;
SpawnWraps ( ) ;
if ( c . hideBody ! = 0 ) HideBody ( ) ;
BodyOnTop ( ) ;
_yaw = pc . transform . eulerAngles . y ; _spin = 0f ; _spinTotal = 0f ; _t0 = Time . time ;
_running = true ;
Pose ( true ) ;
_co = StartCoroutine ( Co ( ) ) ;
}
IEnumerator Co ( )
{
var c = _c ; float nextHit = Mathf . Max ( 0f , c . firstHit ) , nextFx = 0f ;
while ( Time . time - _t0 < c . duration )
{
if ( Broken ( ) ) { _co = null ; End ( false , true ) ; yield break ; }
float t = Time . time - _t0 ;
_spin = Mathf . Max ( 0f , c . spinDegPerSec ) * Mathf . Clamp01 ( t / Mathf . Max ( 0.01f , c . spinUpSeconds ) ) ;
Pose ( false ) ;
if ( t > = nextFx ) { SpawnFx ( ) ; nextFx + = Mathf . Max ( 0.05f , c . fxRepeat ) ; }
if ( t > = nextHit ) { Hit ( ) ; nextHit + = Mathf . Max ( 0.05f , c . hitInterval ) ; }
yield return null ;
}
// 자연스럽게 멈춤 — 판정 · 새 이펙트 없이 자세를 둔 채 회전만 감속 · 감싸기 이펙트는 방출만 멈춰 남은 입자가 제 수명대로 사라짐
// fadeSync 1 = 남은 이펙트(감싸기 · 회오리)를 fadeSeconds 동안 함께 흐려 지우고(WLFxFade) 같은 시간 동안 감속 → 그 끝에 모션 끝
float dur = Mathf . Max ( 0.01f , c . stopSeconds ) ;
if ( c . fadeSync ! = 0 )
{
dur = Mathf . Max ( 0.05f , c . fadeSeconds ) ;
for ( int i = 0 ; i < _wraps . Count ; i + + ) if ( _wraps [ i ] ! = null ) WLFxFade . Attach ( _wraps [ i ] , 0f , dur , c . fadeDimAdditive ! = 0 ) ;
for ( int i = 0 ; i < _fx . Count ; i + + ) if ( _fx [ i ] ! = null ) WLFxFade . Attach ( _fx [ i ] , 0f , Mathf . Clamp ( _fxEnd [ i ] - Time . time , 0.05f , dur ) ) ;
}
else StopWraps ( ) ;
float s0 = _spin , f0 = Time . time ;
WLBladeStorm . LastFade = dur ;
while ( Time . time - f0 < dur )
{
if ( Broken ( ) ) { _co = null ; End ( false , true ) ; yield break ; }
float k = 1f - Mathf . Clamp01 ( ( Time . time - f0 ) / dur ) ;
_spin = s0 * k * k ;
Pose ( false ) ;
yield return null ;
}
_spin = 0f ; _co = null ;
End ( true , true ) ;
}
bool Broken ( ) { return _pc = = null | | _pc . IsDead ( ) | | _pc . Get_CurAnim ( ) ! = eAnim . Skill | | _pb = = null | | ! _pb . gameObject . activeInHierarchy ; }
void LateUpdate ( )
{
if ( ! _running ) return ;
if ( Broken ( ) ) { End ( false , true ) ; return ; }
float d = _c . spinSign * _spin * Time . deltaTime ; _yaw + = d ; _spinTotal + = Mathf . Abs ( d ) ;
_pc . transform . rotation = Quaternion . Euler ( 0f , _yaw , 0f ) ;
var p = _pc . Get_position ( ) ;
transform . position = p ; // 원본도 시전자 위치로 옮겼다(판정 상자는 끔)
var at = p - FxOffset ( ) ;
for ( int i = _fx . Count - 1 ; i > = 0 ; i - - ) { var go = _fx [ i ] ; if ( go = = null ) { _fx . RemoveAt ( i ) ; _fxEnd . RemoveAt ( i ) ; continue ; } go . transform . position = at ; }
var cam = Camera . main ;
for ( int i = _wraps . Count - 1 ; i > = 0 ; i - - )
{
var w = _wraps [ i ] ; if ( w = = null ) { _wraps . RemoveAt ( i ) ; _wrapH . RemoveAt ( i ) ; _wrapT . RemoveAt ( i ) ; continue ; }
var wp = p + Vector3 . up * _wrapH [ i ] ;
if ( _wrapT [ i ] ! = 0f & & cam ! = null ) wp + = ( cam . transform . position - wp ) . normalized * _wrapT [ i ] ; // #927 시선 방향으로 당김
w . transform . position = wp ;
}
}
/// <summary>몸 숨김 — 무기 소켓 밖의 켜진 SkinnedMeshRenderer(검 · 방패 등 소켓 무기는 그대로 보임).</summary>
void HideBody ( )
{
ShowBody ( ) ;
var sockets = _pc . tfs_weapon ;
foreach ( var r in _pc . GetComponentsInChildren < SkinnedMeshRenderer > ( false ) )
{
if ( r = = null | | ! r . enabled ) continue ;
bool inSocket = false ;
if ( sockets ! = null ) foreach ( var s in sockets ) if ( s ! = null & & r . transform . IsChildOf ( s ) ) { inSocket = true ; break ; }
if ( inSocket ) continue ;
r . enabled = false ; _body . Add ( r ) ;
}
}
void ShowBody ( ) { for ( int i = 0 ; i < _body . Count ; i + + ) if ( _body [ i ] ! = null ) _body [ i ] . enabled = true ; _body . Clear ( ) ; }
/// <summary>#930 몸 · 무기 렌더러(파티클 · 꼬리 제외) 재질을 사본으로 바꿔 renderQueue + bodyQueueAdd — 반투명 이펙트 뒤에 그려 몸 색이 이펙트에 물들지 않게.</summary>
void BodyOnTop ( )
{
BodyRestore ( ) ;
if ( _c . bodyQueueAdd < = 0 ) return ;
foreach ( var r in _pc . GetComponentsInChildren < Renderer > ( false ) )
{
if ( r = = null | | ! r . enabled | | r is ParticleSystemRenderer | | r is TrailRenderer | | r is LineRenderer ) continue ;
if ( ! OnTopTarget ( r ) ) continue ;
var src = r . sharedMaterials ; var dst = new Material [ src . Length ] ;
for ( int i = 0 ; i < src . Length ; i + + ) { if ( src [ i ] = = null ) continue ; var m = new Material ( src [ i ] ) { name = src [ i ] . name } ; m . renderQueue = src [ i ] . renderQueue + _c . bodyQueueAdd ; dst [ i ] = m ; _qNew . Add ( m ) ; }
_qR . Add ( r ) ; _qM . Add ( src ) ; r . sharedMaterials = dst ;
}
WLBladeStorm . BodyOnTop + + ;
}
bool OnTopTarget ( Renderer r )
{
var names = _c . bodyOnTopNames ; if ( names = = null | | names . Length = = 0 ) return true ;
var sockets = _pc . tfs_weapon ;
if ( sockets ! = null ) foreach ( var s in sockets ) if ( s ! = null & & r . transform . IsChildOf ( s ) ) return _c . bodyOnTopWeapon ! = 0 ;
foreach ( var n in names ) if ( ! string . IsNullOrEmpty ( n ) & & r . name . StartsWith ( n , System . StringComparison . OrdinalIgnoreCase ) ) return true ;
return false ;
}
void BodyRestore ( )
{
for ( int i = 0 ; i < _qR . Count ; i + + ) if ( _qR [ i ] ! = null ) _qR [ i ] . sharedMaterials = _qM [ i ] ;
for ( int i = 0 ; i < _qNew . Count ; i + + ) if ( _qNew [ i ] ! = null ) Destroy ( _qNew [ i ] ) ;
_qR . Clear ( ) ; _qM . Clear ( ) ; _qNew . Clear ( ) ;
}
2026-10-21 04:05:18 +00:00
static readonly MaterialPropertyBlock s_mpb = new MaterialPropertyBlock ( ) ;
/// <summary>#958 층별 _Emission · _Color 덮기 — 덮은 렌더러 수.</summary>
static int ApplyLayers ( GameObject go , WLFxLayerOverride [ ] layers )
{
if ( go = = null | | layers = = null | | layers . Length = = 0 ) return 0 ;
int n = 0 ;
foreach ( var r in go . GetComponentsInChildren < ParticleSystemRenderer > ( true ) )
{
var m = r . sharedMaterial ; if ( m = = null ) continue ;
WLFxLayerOverride hit = null , def = null ;
foreach ( var L in layers )
{
if ( L = = null | | string . IsNullOrEmpty ( L . names ) ) continue ;
if ( L . names = = "*" ) { if ( def = = null ) def = L ; continue ; }
if ( System . Array . IndexOf ( L . names . Split ( '|' ) , r . name ) > = 0 ) { hit = L ; break ; }
}
var o = hit ? ? def ; if ( o = = null ) continue ;
r . GetPropertyBlock ( s_mpb ) ;
if ( o . emission > = 0f & & m . HasProperty ( "_Emission" ) ) s_mpb . SetFloat ( "_Emission" , o . emission ) ;
if ( o . tint ! = null & & o . tint . Length > = 3 & & m . HasProperty ( "_Color" ) ) s_mpb . SetColor ( "_Color" , new Color ( o . tint [ 0 ] , o . tint [ 1 ] , o . tint [ 2 ] , 1f ) ) ;
r . SetPropertyBlock ( s_mpb ) ; n + + ;
}
return n ;
}
2026-10-18 15:09:05 +00:00
void SpawnWraps ( )
{
if ( _c . wraps = = null ) return ;
foreach ( var w in _c . wraps )
{
if ( w = = null | | string . IsNullOrEmpty ( w . fx ) ) continue ;
GameObject prefab ;
if ( ! s_wrapPrefabs . TryGetValue ( w . fx , out prefab ) | | prefab = = null ) { prefab = Resources . Load < GameObject > ( w . fx ) ; s_wrapPrefabs [ w . fx ] = prefab ; }
if ( prefab = = null ) { WLBladeStorm . LastLog = "감싸기 이펙트 없음 " + w . fx ; continue ; }
var go = Instantiate ( prefab , _pc . Get_position ( ) + Vector3 . up * w . height , prefab . transform . rotation ) ;
go . transform . localScale = prefab . transform . localScale * Mathf . Max ( 0.01f , w . scale ) ;
2026-10-21 04:05:18 +00:00
int ov = ApplyLayers ( go , w . layers ) ; if ( ov > 0 ) WLBladeStorm . LayerOverrides + = ov ; // #958
2026-10-18 15:09:05 +00:00
_wraps . Add ( go ) ; _wrapH . Add ( w . height ) ; _wrapT . Add ( w . towardCamera ) ; WLBladeStorm . FxSpawned + + ;
}
}
/// <summary>감싸기 이펙트 — 방출만 멈춘다(남은 입자는 제 수명대로 · 다 사라진 뒤 지움) · 가장 긴 남은 수명(초)을 돌려준다.</summary>
float StopWraps ( )
{
float life = 0f ;
for ( int i = 0 ; i < _wraps . Count ; i + + )
{
var w = _wraps [ i ] ; if ( w = = null ) continue ; float l = 0f ;
foreach ( var ps in w . GetComponentsInChildren < ParticleSystem > ( true ) ) { ps . Stop ( true , ParticleSystemStopBehavior . StopEmitting ) ; l = Mathf . Max ( l , ps . main . startLifetime . constantMax ) ; }
Destroy ( w , l + 0.2f ) ; life = Mathf . Max ( life , l ) ;
}
return life ;
}
/// <summary>공격 자세 고정 — 다른 동작(원본 시전 다음 동작 등)이 끼어들면 되돌린다 · 클립 이벤트(궤적 · 판정)는 끈다.</summary>
void Pose ( bool force )
{
if ( _an = = null | | string . IsNullOrEmpty ( _c . poseState ) ) return ;
if ( ! _animOwned ) { _an . fireEvents = false ; _animOwned = true ; }
var si = _an . GetCurrentAnimatorStateInfo ( 0 ) ;
if ( force | | _an . IsInTransition ( 0 ) | | ! si . IsName ( _c . poseState ) | | Mathf . Abs ( si . normalizedTime - _c . poseNormalized ) > 0.01f )
{
_an . Play ( _c . poseState , 0 , _c . poseNormalized ) ;
if ( force ) { _an . speed = 0f ; _an . Update ( 0f ) ; }
}
if ( _an . speed ! = 0f ) _an . speed = 0f ;
}
Vector3 FxOffset ( )
{
var a = _c . fxOffset ;
return a ! = null & & a . Length > = 3 ? new Vector3 ( a [ 0 ] , a [ 1 ] , a [ 2 ] ) * Mathf . Max ( 0.01f , _c . fxScale ) : Vector3 . zero ;
}
void SpawnFx ( )
{
if ( string . IsNullOrEmpty ( _c . fx ) ) return ;
if ( s_prefab = = null | | s_prefabPath ! = _c . fx ) { s_prefab = Resources . Load < GameObject > ( _c . fx ) ; s_prefabPath = _c . fx ; }
if ( s_prefab = = null ) { WLBladeStorm . LastLog = "이펙트 없음 " + _c . fx ; return ; }
var go = Instantiate ( s_prefab , _pc . Get_position ( ) - FxOffset ( ) , s_prefab . transform . rotation ) ;
go . transform . localScale = s_prefab . transform . localScale * Mathf . Max ( 0.01f , _c . fxScale ) ;
Destroy ( go , Mathf . Max ( 0.1f , _c . fxLife ) ) ;
_fx . Add ( go ) ; _fxEnd . Add ( Time . time + Mathf . Max ( 0.1f , _c . fxLife ) ) ; WLBladeStorm . FxSpawned + + ;
}
/// <summary>PC 발 기준 radius + 적 몸 반지름 안의 모든 적에게 원본 한 대 피해(속사 레이저 WLRapidShot.Laser 와 같은 방식).</summary>
void Hit ( )
{
WLBladeStorm . Ticks + + ;
var info = ActorInfo . Ins ; if ( info = = null | | _pb = = null ) return ;
var origin = _pc . Get_position ( ) ; int n = 0 ;
var list = info . Get_EnemyActors ( _pc . IsEnemy ( ) , origin , _c . radius + 3f ) ;
if ( list = = null ) return ;
for ( int k = 0 ; k < list . Count ; k + + )
{
var a = list [ k ] ;
if ( a = = null | | a . IsDead ( ) ) continue ;
var v = a . Get_position ( ) - origin ; v . y = 0f ;
if ( v . magnitude > _c . radius + WL . Combat . WLCombatMotionSettings . BodyRadius ( a ) ) continue ;
var col = a . GetComponent < Collider > ( ) ;
if ( col = = null | | ! col . enabled ) continue ;
if ( _pb . ProcessCollider ( col , false ) ) { n + + ; HitFx ( a , origin ) ; }
}
info . Release_Actors ( list ) ;
WLBladeStorm . Hits + = n ;
}
static readonly Dictionary < string , GameObject > s_hitPrefabs = new Dictionary < string , GameObject > ( ) ;
void HitFx ( Actor a , Vector3 from )
{
if ( string . IsNullOrEmpty ( _c . hitFx ) | | a = = null ) return ;
GameObject prefab ;
if ( ! s_hitPrefabs . TryGetValue ( _c . hitFx , out prefab ) | | prefab = = null ) { prefab = Resources . Load < GameObject > ( _c . hitFx ) ; s_hitPrefabs [ _c . hitFx ] = prefab ; }
if ( prefab = = null ) { WLBladeStorm . LastLog = "히트 이펙트 없음 " + _c . hitFx ; return ; }
var at = a . Get_Center_position ( ) ; var d = at - from ; d . y = 0f ; if ( d . sqrMagnitude < 1e-4f ) d = _pc . transform . forward ;
2026-10-18 17:27:34 +00:00
at + = WL . Combat . Impact . WLImpactFx . TowardCamera ( a ) ; // #941 몸 앞(카메라 쪽)
2026-10-18 15:09:05 +00:00
var go = Instantiate ( prefab , at , Quaternion . LookRotation ( d ) * prefab . transform . rotation ) ;
foreach ( var ps in go . GetComponentsInChildren < ParticleSystem > ( true ) ) { var m = ps . main ; m . scalingMode = ParticleSystemScalingMode . Hierarchy ; }
go . transform . localScale = prefab . transform . localScale * Mathf . Max ( 0.01f , _c . hitFxScale ) ;
Destroy ( go , Mathf . Max ( 0.1f , _c . hitFxLife ) ) ; WLBladeStorm . HitFxShown + + ;
}
void Hide ( )
{
_hidden . Clear ( ) ;
foreach ( var ps in GetComponentsInChildren < ParticleSystem > ( true ) ) ps . Stop ( true , ParticleSystemStopBehavior . StopEmittingAndClear ) ;
foreach ( var r in GetComponentsInChildren < Renderer > ( true ) ) { if ( ! r . enabled ) continue ; r . enabled = false ; _hidden . Add ( r ) ; }
}
/// <summary>끝 — natural = 유지 시간 만료(감속 뒤) · offProjectile = 투사체를 끈다(원본 끝 순서: 끔 → 조이스틱 0 → 대기).</summary>
void End ( bool natural , bool offProjectile )
{
if ( ! _running ) return ;
_running = false ;
if ( _co ! = null ) { StopCoroutine ( _co ) ; _co = null ; }
if ( _animOwned & & _an ! = null ) { _an . speed = 1f ; _an . fireEvents = true ; }
_animOwned = false ;
for ( int i = 0 ; i < _fx . Count ; i + + ) if ( _fx [ i ] ! = null ) foreach ( var ps in _fx [ i ] . GetComponentsInChildren < ParticleSystem > ( true ) ) ps . Stop ( true , ParticleSystemStopBehavior . StopEmitting ) ; // 남은 입자는 제 수명대로 사라짐
_fx . Clear ( ) ; _fxEnd . Clear ( ) ;
StopWraps ( ) ; _wraps . Clear ( ) ; _wrapH . Clear ( ) ; _wrapT . Clear ( ) ;
ShowBody ( ) ;
BodyRestore ( ) ;
WLBladeStorm . ClearSlow ( _pc ) ;
foreach ( var r in _hidden ) if ( r ! = null ) r . enabled = true ; // 풀 재사용 · enabled 0 일 때 원본 그대로
_hidden . Clear ( ) ;
WLBladeStorm . LastSpinTotal = _spinTotal ; WLBladeStorm . LastSeconds = Time . time - _t0 ;
if ( natural ) WLBladeStorm . Ends + + ; else WLBladeStorm . Aborts + + ;
var pc = _pc ;
if ( offProjectile & & _pb ! = null & & _pb . gameObject . activeInHierarchy ) WLSlashFlurry . Off ( _pb ) ;
if ( pc ! = null )
{
pc . Set_JoystickStatus ( 3 ) ;
if ( natural & & ! pc . IsDead ( ) & & pc . Get_CurAnim ( ) = = eAnim . Skill ) pc . Play_Idle ( ) ;
}
}
void OnDisable ( ) { if ( _running ) End ( false , false ) ; }
}
}