2026-10-18 15:09:05 +00:00
// ─────────────────────────────────────────────────────────────────────────────
// WLSkillVfx.cs — 스킬 이펙트 교체(휘두를 때 · 맞은 대상) — 표 기반
/ /
// PD 원문(#887 · 2026-10-17): 「방패 치기 모션일 때 이펙트를 교체하고 싶어. (Anime Stylized VFX URP 에셋의 Slash를
// 방패를 휘두를 때 발생시키고, 히트 된 대상에게 Aroow Shot Impact를 발생시켜줘)」
/ /
// ■ 원리 — 원본 SkillEvent 훅(CombatEvents.SkillFired = 스킬 투사체가 나오는 순간)에서 표의 스킬이면 swing 이펙트를 시전자 앞에,
// 타격 확정 훅(CombatEvents.HitConfirmed · 피해 정보 Beater_pd 의 스킬)에서 hit 이펙트를 맞은 대상 중심에 띄운다(시전자 → 대상 방향).
// 원본 스킬 프리팹의 기존 이펙트(hideChildren)는 그 투사체가 켜진 같은 프레임(타격 확정)에 끈다 — 원본 프리팹 무수정.
// ■ 이펙트 = Resources/WL/Fx/ 래퍼(자식 = 팩 프리팹 인스턴스) · 팩 파티클은 크기 모드가 Local 이라 scale ≠ 1 이면 Hierarchy 로 바꿔 키운다.
// ■ 값 = Resources/WL/WLSkillVfx.json (C45) · 끄기 = enabled 0(구독 0) · 원본 수정 0줄
// 🔴 Debug.LogError 금지(원본 ErrorLogHook 이 팝업을 띄운다) — 구독자 예외도 LastLog 로만 남긴다.
// ─────────────────────────────────────────────────────────────────────────────
using System.Collections.Generic ;
using UnityEngine ;
using WL.Combat.Core ;
namespace WL.Combat.Weapon
{
[System.Serializable]
public class WLSkillVfxFx
{
public string prefab = "" ;
public float [ ] offset = new float [ 3 ] ; // 기준 회전 기준 (오른쪽, 위, 앞) m
public float [ ] euler = new float [ 3 ] ;
public float scale = 1f ;
public float life = 1f ;
public float fade = 0f ; // #921 0 보다 크면 life 의 마지막 fade 초 동안 투명해지며 사라진다(방출 멈춤 + 입자 알파 → 0) — 0 = life 에 바로 지움
}
/// <summary>#921 PD 「플레이어 위치에 생성 된 이펙트가 갑자기 사라지지 않고 … 투명해지며 자연스럽게 소멸」 — hold 초 뒤 fade 초 동안 방출을 멈추고 입자 알파를 0 으로 · 끝나면 지운다(셰이더 무관 · 입자 색).</summary>
public sealed class WLFxFade : MonoBehaviour
{
float _start , _fade ; bool _began ; ParticleSystem [ ] _ps ; ParticleSystem . Particle [ ] _buf = new ParticleSystem . Particle [ 64 ] ;
readonly Dictionary < ParticleSystem , float > _a0 = new Dictionary < ParticleSystem , float > ( ) ;
// #934 PD 「(회오리가) 소멸할 때 뭉쳐진 회오리만 남아있어서 어색해 … 스킬 종료 타이밍에 맞게 자연스럽게 소멸」 — 가산(더하기) 재질은 알파만 낮추면 밝기가 남는다:
// dimAdditive 면 가산 재질 입자는 처음 색(입자별 · randomSeed) × 남은 비율로 색 · 알파를 함께 낮춘다(알파 블렌드 입자는 그대로 알파만)
bool _dimAdd ; bool [ ] _add ; readonly Dictionary < ParticleSystem , Dictionary < uint , Color32 > > _c0 = new Dictionary < ParticleSystem , Dictionary < uint , Color32 > > ( ) ;
2026-10-31 06:53:50 +00:00
Light [ ] _lights = new Light [ 0 ] ; float [ ] _l0 = new float [ 0 ] ;
2026-10-18 15:09:05 +00:00
public static int DimmedSystems ;
public static void Attach ( GameObject go , float hold , float fade ) { Attach ( go , hold , fade , false ) ; }
public static void Attach ( GameObject go , float hold , float fade , bool dimAdditive )
{
var f = go . AddComponent < WLFxFade > ( ) ; f . _start = Time . time + Mathf . Max ( 0f , hold ) ; f . _fade = Mathf . Max ( 0.05f , fade ) ; f . _dimAdd = dimAdditive ;
}
static bool IsAdditive ( ParticleSystem ps )
{
var r = ps . GetComponent < ParticleSystemRenderer > ( ) ; var m = r ! = null ? r . sharedMaterial : null ; if ( m = = null ) return false ;
if ( m . HasProperty ( "_DstBlend" ) ) return Mathf . RoundToInt ( m . GetFloat ( "_DstBlend" ) ) = = ( int ) UnityEngine . Rendering . BlendMode . One ;
return m . shader ! = null & & m . shader . name . IndexOf ( "Add" , System . StringComparison . OrdinalIgnoreCase ) > = 0 ;
}
void LateUpdate ( )
{
if ( Time . time < _start ) return ;
if ( ! _began )
{
_began = true ; _ps = GetComponentsInChildren < ParticleSystem > ( true ) ; _add = new bool [ _ps . Length ] ;
for ( int j = 0 ; j < _ps . Length ; j + + ) { _ps [ j ] . Stop ( false , ParticleSystemStopBehavior . StopEmitting ) ; _add [ j ] = _dimAdd & & IsAdditive ( _ps [ j ] ) ; if ( _add [ j ] ) DimmedSystems + + ; }
2026-10-31 06:53:50 +00:00
_lights = GetComponentsInChildren < Light > ( true ) ; _l0 = new float [ _lights . Length ] ; for ( int j = 0 ; j < _lights . Length ; j + + ) _l0 [ j ] = _lights [ j ] . intensity ; // #1005 이펙트에 붙은 점광원도 같이 어두워짐
2026-10-18 15:09:05 +00:00
}
float k = Mathf . Clamp01 ( ( Time . time - _start ) / _fade ) , keep = 1f - k ;
2026-10-31 06:53:50 +00:00
for ( int j = 0 ; j < _lights . Length ; j + + ) if ( _lights [ j ] ! = null ) _lights [ j ] . intensity = _l0 [ j ] * keep ;
2026-10-18 15:09:05 +00:00
for ( int j = 0 ; j < _ps . Length ; j + + )
{
var ps = _ps [ j ] ;
if ( ps = = null ) continue ;
int n = ps . particleCount ; if ( n = = 0 ) continue ;
if ( _buf . Length < n ) _buf = new ParticleSystem . Particle [ n ] ;
n = ps . GetParticles ( _buf , n ) ;
if ( _add [ j ] )
{
Dictionary < uint , Color32 > c0 ; if ( ! _c0 . TryGetValue ( ps , out c0 ) ) { c0 = new Dictionary < uint , Color32 > ( ) ; _c0 [ ps ] = c0 ; }
for ( int i = 0 ; i < n ; i + + ) { Color32 o ; uint key = _buf [ i ] . randomSeed ; if ( ! c0 . TryGetValue ( key , out o ) ) { o = _buf [ i ] . startColor ; c0 [ key ] = o ; } _buf [ i ] . startColor = new Color32 ( ( byte ) ( o . r * keep ) , ( byte ) ( o . g * keep ) , ( byte ) ( o . b * keep ) , ( byte ) ( o . a * keep ) ) ; }
}
else
for ( int i = 0 ; i < n ; i + + ) { var c = _buf [ i ] . startColor ; float a0 ; if ( ! _a0 . TryGetValue ( ps , out a0 ) ) { a0 = c . a ; _a0 [ ps ] = a0 ; } c . a = ( byte ) Mathf . RoundToInt ( Mathf . Min ( c . a , a0 * keep ) ) ; _buf [ i ] . startColor = c ; }
ps . SetParticles ( _buf , n ) ;
}
if ( k > = 1f ) Destroy ( gameObject ) ;
}
}
/// <summary>#914 원본 스킬 프리팹 안 파티클 색 · 투명도 바꾸기(이름별) — 풀 인스턴스마다 1 회(표시 컴포넌트 WLSkillVfxTinted).</summary>
[System.Serializable]
public class WLSkillVfxTint
{
public string [ ] names = new string [ 0 ] ; // 파티클 오브젝트 이름
public float [ ] rgb = new float [ 0 ] ; // 시작색 RGB(비우면 그대로)
public float alpha = - 1f ; // 시작색 알파(-1 = 그대로)
public float alphaMul = 1f ; // 시작색 알파 배율
public float [ ] colKeys = new float [ 0 ] ; // 수명색 색 키 t,r,g,b…(비우면 그대로)
public float [ ] colAlpha = new float [ 0 ] ; // 수명색 알파 키 t,a…(비우면 그대로)
}
[System.Serializable]
public class WLSkillVfxRule
{
public string skill = "" ; // eEffect 이름 = 원본 스킬 클래스 이름
public string [ ] hideChildren = new string [ 0 ] ; // 원본 스킬 프리팹에서 끌 자식 이름
public WLSkillVfxFx swing = new WLSkillVfxFx ( ) ;
public WLSkillVfxFx hit = new WLSkillVfxFx ( ) ;
public float effectScale = 1f ; // #899 원본 스킬 프리팹의 보이는 이펙트(크기 노드) 배율 — 피해 판정(콜라이더)은 그대로
// #902 PD 「해머 점프 내려치기 — 내려친 위치에 스크린샷 이펙트 · 충격 지점 중심 8방향 충격파 · 맞은 적 1m 띄웠다 떨어뜨린 뒤 스턴 1.5초」
public float hitBoxScale = 1f ; // 원본 판정 상자 가로·세로 배율(8방향 충격파까지 맞게 · 각성 성장분 유지)
public WLSkillVfxFx centerFx = new WLSkillVfxFx ( ) ; // 원본 Set 때 스킬 자리(내려친 위치)에 띄움
public WLSkillVfxFx radialFx = new WLSkillVfxFx ( ) ; // 스킬 자리 둘레 radialCount 방향(시전자 정면부터 등간격)
public int radialCount ;
public float radialRadius = 1f ;
// #904 PD 「충격 지점부터 빠르게 퍼지며 폭파(중간 범위 광역)」 — radialSteps ≥ 2 면 방향마다 radialStartRadius → radialRadius 사이를 radialSteps 번에 나눠
// radialStepDelay 초 간격으로 radialFx 를 잇달아 띄우고(퍼짐), 끝 자리에 radialEndFx 를 띄운다(폭발). 1 이하 = 끝 자리 한 번에(#902).
public int radialSteps ;
public float radialStartRadius = 0.4f , radialStepDelay = 0.04f ;
public WLSkillVfxFx radialEndFx = new WLSkillVfxFx ( ) ;
public float launchHeight , launchSeconds = 0.5f , stunSeconds ; // 이 스킬에 맞은 적: 띄우기 높이(m) · 걸리는 시간(초) · 내려온 뒤 기절(초)
2026-10-31 06:53:50 +00:00
public int [ ] noLaunchClassIds = new int [ 0 ] ; // #1001 이 직업이 쓴 이 스킬 피격엔 띄움 · 기절을 걸지 않는다(대검 기본 = 같은 그릇 강력한 일격 · 회전 연타마다 1 m 떠오름 실측 → 띄움은 WLStaffEarth 내려치기만)
2026-10-18 15:09:05 +00:00
public WLSkillVfxTint [ ] tint = new WLSkillVfxTint [ 0 ] ; // #914 원본 스킬 프리팹 파티클 색 · 투명도(검은 갈라짐 · 연기 → 반투명)
// #917 원본 Set 때 바로 숨김(hideChildren 을 첫 타격까지 기다리지 않음) · 멈춰 서서 판정 상자를 켜는 스킬(Projectile_StopAndColliderControl)의 다단 히트 시각
public int hideOnSet ;
public float multiHitDelay = - 1f ; // 첫 판정까지(초 · ColliderActiveDealyTime) — 음수 = 원본 그대로
public int multiHitCount ; // 판정 횟수(Loop) — 0 = 원본 그대로
public float multiHitInterval = - 1f ; // 판정 간격(초 · DelayTime) — 음수 = 원본 그대로
public float lifeSeconds = - 1f ; // 화살 비(Skill_ArrowRain.lifetime) 유지 시간 — 음수 = 원본 그대로
public WLSkillVfxFx casterFx = new WLSkillVfxFx ( ) ; // 원본 Set 때 시전자 발밑(몸)에 띄움
}
[System.Serializable]
public class WLSkillVfxJson
{
public int enabled = 0 ;
public WLSkillVfxRule [ ] rules = new WLSkillVfxRule [ 0 ] ;
public string note = "" ;
}
public static class WLSkillVfx
{
public const string ResourcesPath = "WL/WLSkillVfx" ;
public static int Swings , Hits , Hidden , Errors ;
public static string LastLog = "" ;
static WLSkillVfxJson s_cfg ;
static readonly Dictionary < string , WLSkillVfxRule > s_rules = new Dictionary < string , WLSkillVfxRule > ( ) ;
static readonly Dictionary < string , GameObject > s_prefabs = new Dictionary < string , GameObject > ( ) ;
static readonly Dictionary < string , System . Type > s_types = new Dictionary < string , System . Type > ( ) ;
static WLSkillVfxJson Cfg
{
get
{
if ( s_cfg ! = null ) return s_cfg ;
s_cfg = new WLSkillVfxJson ( ) ;
s_rules . Clear ( ) ;
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 ; }
if ( s_cfg . rules ! = null ) foreach ( var r in s_cfg . rules ) if ( r ! = null & & ! string . IsNullOrEmpty ( r . skill ) ) s_rules [ r . skill ] = r ;
return s_cfg ;
}
}
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
static void Boot ( )
{
s_cfg = null ; s_prefabs . Clear ( ) ;
if ( Cfg . enabled = = 0 ) { CombatEvents . SkillFired . Remove ( OnSkillFired ) ; CombatEvents . HitConfirmed . Remove ( OnHitConfirmed ) ; return ; }
CombatEvents . SkillFired . Add ( OnSkillFired ) ;
CombatEvents . HitConfirmed . Add ( OnHitConfirmed ) ;
}
/// <summary>#899 PD 「강력한 일격 이펙트가 너무 과도 — 캐릭터 크기·비율을 고려해 이펙트 크기 조절」 — 원본 스킬 Set() 끝 훅 1줄이 부른다.
/// 표(skill = 클래스 이름)의 effectScale 을 보이는 크기 노드(sizeNode)에만 곱한다(원본이 매번 1 로 되돌린 뒤라 반복 시전에도 누적되지 않음). 꺼져 있으면(enabled 0) 아무것도 안 한다.</summary>
public static void ScaleOriginal ( ProjectileBase skill , Transform sizeNode )
{
try
{
if ( skill = = null | | Cfg . enabled = = 0 ) return ;
WLSkillVfxRule r ;
if ( ! s_rules . TryGetValue ( skill . GetType ( ) . Name , out r ) ) return ;
if ( sizeNode ! = null & & r . effectScale > 0f & & ! Mathf . Approximately ( r . effectScale , 1f ) ) { sizeNode . localScale * = r . effectScale ; Scaled + + ; }
if ( r . tint ! = null & & r . tint . Length > 0 & & skill . GetComponent < WLSkillVfxTinted > ( ) = = null ) { Tinted + = Tint ( skill , r . tint ) ; skill . gameObject . AddComponent < WLSkillVfxTinted > ( ) ; }
// #917 바로 숨김 · 다단 히트 시각 · 유지 시간 · 시전자 이펙트
if ( r . hideOnSet ! = 0 & & r . hideChildren ! = null )
foreach ( var n in r . hideChildren ) { var c = skill . transform . Find ( n ) ; if ( c ! = null & & c . gameObject . activeSelf ) { c . gameObject . SetActive ( false ) ; Hidden + + ; } }
var sc = skill as Projectile_StopAndColliderControl ;
if ( sc ! = null )
{
if ( r . multiHitDelay > = 0f ) sc . ColliderActiveDealyTime = r . multiHitDelay ;
if ( r . multiHitCount > 0 ) { sc . Loop = r . multiHitCount ; sc . LoopTime = 0f ; }
if ( r . multiHitInterval > = 0f ) sc . DelayTime = r . multiHitInterval ;
}
var rain = skill as Skill_ArrowRain ;
if ( rain ! = null & & r . lifeSeconds > 0f ) rain . lifetime = r . lifeSeconds ;
var pdc = skill . Get_ProjectTileData ( ) ; var caster = pdc ! = null ? pdc . shooter : null ;
if ( r . casterFx ! = null & & ! string . IsNullOrEmpty ( r . casterFx . prefab ) & & ! DSUtil . CheckNull ( caster ) )
{
var cf = caster . transform . forward ; cf . y = 0f ; if ( cf . sqrMagnitude < 1e-4f ) cf = Vector3 . forward ;
if ( Spawn ( r . casterFx , caster . Get_position ( ) , Quaternion . LookRotation ( cf . normalized ) ) ) Casters + + ;
}
// #902 판정 상자 · 내려친 위치 이펙트 · 8방향 충격파
if ( r . hitBoxScale > 0f & & ! Mathf . Approximately ( r . hitBoxScale , 1f ) )
{
var bc = skill . GetComponent < BoxCollider > ( ) ;
if ( bc ! = null )
{
// #917 풀 재사용 때 배율이 누적되지 않게 — 원본 크기를 기억(원본이 Set 에서 크기를 다시 정했으면 그 값이 새 기준)
var bs = skill . GetComponent < WLSkillVfxBase > ( ) ;
if ( bs = = null ) { bs = skill . gameObject . AddComponent < WLSkillVfxBase > ( ) ; bs . box = bc . size ; bs . written = new Vector3 ( - 1f , - 1f , - 1f ) ; }
if ( bc . size ! = bs . written ) bs . box = bc . size ;
bc . size = new Vector3 ( bs . box . x * r . hitBoxScale , bs . box . y , bs . box . z * r . hitBoxScale ) ;
bs . written = bc . size ;
}
}
var pd = skill . Get_ProjectTileData ( ) ;
var shooter = pd ! = null ? pd . shooter : null ;
Vector3 fwd = ! DSUtil . CheckNull ( shooter ) ? shooter . transform . forward : skill . transform . forward ; fwd . y = 0f ;
if ( fwd . sqrMagnitude < 1e-4f ) fwd = Vector3 . forward ; fwd . Normalize ( ) ;
Vector3 at = skill . transform . position ;
if ( Spawn ( r . centerFx , at , Quaternion . LookRotation ( fwd ) ) ) Centers + + ;
for ( int k = 0 ; k < r . radialCount ; k + + )
{
var dir = Quaternion . Euler ( 0f , k * 360f / r . radialCount , 0f ) * fwd ;
var rot = Quaternion . LookRotation ( dir ) ;
if ( r . radialSteps < = 1 ) { if ( Spawn ( r . radialFx , at + dir * r . radialRadius , rot ) ) Radials + + ; continue ; }
for ( int i = 0 ; i < r . radialSteps ; i + + ) // #904 중심에서 바깥으로 퍼짐 → 끝에서 폭발
{
float rr = Mathf . Lerp ( r . radialStartRadius , r . radialRadius , i / ( float ) ( r . radialSteps - 1 ) ) ;
Later ( i * r . radialStepDelay , r . radialFx , at + dir * rr , rot ) ;
}
Later ( r . radialSteps * r . radialStepDelay , string . IsNullOrEmpty ( r . radialEndFx . prefab ) ? r . radialFx : r . radialEndFx , at + dir * r . radialRadius , rot ) ;
}
}
catch ( System . Exception ex ) { Errors + + ; LastLog = "크기 조절 실패 — " + ex . Message ; }
}
public static int Scaled , Centers , Radials , Launched , Tinted , Casters ;
/// <summary>#914 이름이 맞는 파티클의 시작색(RGB · 알파 · 알파 배율) · 수명색(색 키 · 알파 키)을 바꾼다 — 바꾼 파티클 수.</summary>
static int Tint ( ProjectileBase skill , WLSkillVfxTint [ ] tints )
{
int n = 0 ;
foreach ( var ps in skill . GetComponentsInChildren < ParticleSystem > ( true ) )
foreach ( var t in tints )
{
if ( t = = null | | t . names = = null | | System . Array . IndexOf ( t . names , ps . name ) < 0 ) continue ;
var m = ps . main ; var c = m . startColor ;
if ( c . mode = = ParticleSystemGradientMode . Color ) c . color = TintColor ( c . color , t ) ;
else if ( c . mode = = ParticleSystemGradientMode . TwoColors ) { c . colorMin = TintColor ( c . colorMin , t ) ; c . colorMax = TintColor ( c . colorMax , t ) ; }
m . startColor = c ;
if ( ( t . colKeys ! = null & & t . colKeys . Length > = 4 ) | | ( t . colAlpha ! = null & & t . colAlpha . Length > = 2 ) )
{
var col = ps . colorOverLifetime ; col . enabled = true ;
var old = col . color . mode = = ParticleSystemGradientMode . Gradient ? col . color . gradient : new Gradient ( ) ;
var ck = old . colorKeys ; var ak = old . alphaKeys ;
if ( t . colKeys ! = null & & t . colKeys . Length > = 4 )
{
ck = new GradientColorKey [ t . colKeys . Length / 4 ] ;
for ( int i = 0 ; i < ck . Length ; i + + ) ck [ i ] = new GradientColorKey ( new Color ( t . colKeys [ i * 4 + 1 ] , t . colKeys [ i * 4 + 2 ] , t . colKeys [ i * 4 + 3 ] ) , t . colKeys [ i * 4 ] ) ;
}
if ( t . colAlpha ! = null & & t . colAlpha . Length > = 2 )
{
ak = new GradientAlphaKey [ t . colAlpha . Length / 2 ] ;
for ( int i = 0 ; i < ak . Length ; i + + ) ak [ i ] = new GradientAlphaKey ( t . colAlpha [ i * 2 + 1 ] , t . colAlpha [ i * 2 ] ) ;
}
var g = new Gradient ( ) ; g . SetKeys ( ck , ak ) ; col . color = new ParticleSystem . MinMaxGradient ( g ) ;
}
n + + ;
break ;
}
return n ;
}
static Color TintColor ( Color k , WLSkillVfxTint t )
{
if ( t . rgb ! = null & & t . rgb . Length > = 3 ) { k . r = t . rgb [ 0 ] ; k . g = t . rgb [ 1 ] ; k . b = t . rgb [ 2 ] ; }
if ( t . alpha > = 0f ) k . a = t . alpha ;
k . a * = t . alphaMul ;
return k ;
}
static WLSkillVfxRule RuleOf ( SkillListTableData s )
{
if ( s = = null ) return null ;
WLSkillVfxRule r ;
return s_rules . TryGetValue ( s . e_Skill . ToString ( ) , out r ) ? r : null ;
}
static void OnSkillFired ( in SkillFiredEvent e )
{
try
{
var r = RuleOf ( e . skill ) ;
if ( r = = null | | DSUtil . CheckNull ( e . actor ) ) return ;
var t = e . actor . transform ;
if ( Spawn ( r . swing , t . position , t . rotation ) ) Swings + + ;
}
catch ( System . Exception ex ) { Errors + + ; LastLog = "swing 실패 — " + ex . Message ; }
}
static void OnHitConfirmed ( in HitConfirmedEvent e )
{
try
{
var pd = e . projectile ;
var r = pd ! = null ? RuleOf ( pd . m_SkillListTableData ) : null ;
if ( r = = null | | DSUtil . CheckNull ( e . victim ) ) return ;
HideOriginal ( r , pd ) ;
var at = e . victim . Get_Center_position ( ) ;
var dir = ! DSUtil . CheckNull ( e . attacker ) ? at - e . attacker . Get_Center_position ( ) : e . victim . transform . forward ;
dir . y = 0f ;
var rot = dir . sqrMagnitude > 1e-4f ? Quaternion . LookRotation ( dir ) : Quaternion . identity ;
2026-10-18 17:27:34 +00:00
if ( Spawn ( r . hit , at + WL . Combat . Impact . WLImpactFx . TowardCamera ( e . victim ) , rot ) ) Hits + + ; // #941 몸 앞(카메라 쪽)
2026-10-31 06:53:50 +00:00
var apc = e . attacker as PCActor ; bool noLaunch = apc ! = null & & r . noLaunchClassIds ! = null & & System . Array . IndexOf ( r . noLaunchClassIds , apc . Get_ID ( ) ) > = 0 ; // #1001
if ( ! noLaunch & & ( r . launchHeight > 0f | | r . stunSeconds > 0f ) & & WLLaunch . Apply ( e . victim , r . launchHeight , r . launchSeconds , r . stunSeconds ) ) Launched + + ; // #902
2026-10-18 15:09:05 +00:00
}
catch ( System . Exception ex ) { Errors + + ; LastLog = "hit 실패 — " + ex . Message ; }
}
/// <summary>그 스킬의 켜진 원본 투사체(이번 피해의 투사체 데이터)에서 hideChildren 자식을 끈다.</summary>
static void HideOriginal ( WLSkillVfxRule r , ProjectileData pd )
{
if ( r . hideChildren = = null | | r . hideChildren . Length = = 0 ) return ;
System . Type type ;
if ( ! s_types . TryGetValue ( r . skill , out type ) ) { type = System . Type . GetType ( r . skill + ", Assembly-CSharp" ) ; s_types [ r . skill ] = type ; }
if ( type = = null ) return ;
foreach ( var o in Object . FindObjectsByType ( type , FindObjectsInactive . Exclude , FindObjectsSortMode . None ) )
{
var pb = o as ProjectileBase ;
if ( pb = = null | | pb . Get_ProjectTileData ( ) ! = pd ) continue ;
foreach ( var n in r . hideChildren )
{
var c = pb . transform . Find ( n ) ;
if ( c ! = null & & c . gameObject . activeSelf ) { c . gameObject . SetActive ( false ) ; Hidden + + ; }
}
}
}
static bool Spawn ( WLSkillVfxFx fx , Vector3 pos , Quaternion rot )
{
if ( fx = = null | | string . IsNullOrEmpty ( fx . prefab ) ) return false ;
var p = Prefab ( fx . prefab ) ;
if ( p = = null ) return false ;
var go = Object . Instantiate ( p , pos + rot * V ( fx . offset ) , rot * Quaternion . Euler ( V ( fx . euler ) ) ) ;
if ( ! Mathf . Approximately ( fx . scale , 1f ) )
{
foreach ( var ps in go . GetComponentsInChildren < ParticleSystem > ( true ) ) { var m = ps . main ; m . scalingMode = ParticleSystemScalingMode . Hierarchy ; }
go . transform . localScale = Vector3 . one * fx . scale ;
}
if ( fx . fade > 0f ) WLFxFade . Attach ( go , Mathf . Max ( 0f , fx . life - fx . fade ) , fx . fade ) ; // #921 투명해지며 소멸
else Object . Destroy ( go , Mathf . Max ( 0.1f , fx . life ) ) ;
LastLog = "생성 " + fx . prefab + " @" + pos . ToString ( "F1" ) ;
return true ;
}
// ── #904 지연 생성(퍼지는 충격파) — 숨김 러너 1개가 시각이 된 것을 띄운다 ──
struct Pending { public float at ; public WLSkillVfxFx fx ; public Vector3 pos ; public Quaternion rot ; }
static readonly List < Pending > s_pending = new List < Pending > ( 64 ) ;
static void Later ( float delay , WLSkillVfxFx fx , Vector3 pos , Quaternion rot )
{
if ( fx = = null | | string . IsNullOrEmpty ( fx . prefab ) ) return ;
if ( delay < = 0f ) { if ( Spawn ( fx , pos , rot ) ) Radials + + ; return ; }
s_pending . Add ( new Pending { at = Time . time + delay , fx = fx , pos = pos , rot = rot } ) ;
if ( s_runner = = null ) { var go = new GameObject ( "[WL904] SkillVfxLater" ) ; go . hideFlags = HideFlags . HideAndDontSave ; Object . DontDestroyOnLoad ( go ) ; s_runner = go . AddComponent < WLSkillVfxLater > ( ) ; }
}
static WLSkillVfxLater s_runner ;
internal static void TickLater ( )
{
float now = Time . time ;
for ( int i = s_pending . Count - 1 ; i > = 0 ; i - - )
{
var p = s_pending [ i ] ;
if ( now < p . at ) continue ;
s_pending . RemoveAt ( i ) ;
try { if ( Spawn ( p . fx , p . pos , p . rot ) ) Radials + + ; } catch ( System . Exception ex ) { Errors + + ; LastLog = "지연 생성 실패 — " + ex . Message ; }
}
}
static GameObject Prefab ( string path )
{
GameObject p ;
if ( s_prefabs . TryGetValue ( path , out p ) ) return p ;
p = Resources . Load < GameObject > ( path ) ;
if ( p = = null ) LastLog = "이펙트 없음 " + path ;
s_prefabs [ path ] = p ;
return p ;
}
static Vector3 V ( float [ ] a ) { return a ! = null & & a . Length > = 3 ? new Vector3 ( a [ 0 ] , a [ 1 ] , a [ 2 ] ) : Vector3 . zero ; }
}
/// <summary>#904 지연 생성 러너(숨김 GameObject 1개).</summary>
public sealed class WLSkillVfxLater : MonoBehaviour { void Update ( ) { WLSkillVfx . TickLater ( ) ; } }
/// <summary>#914 이 원본 스킬 인스턴스(풀)는 tint 를 이미 적용함 — 다시 쓰여도 알파 배율이 누적되지 않게.</summary>
public sealed class WLSkillVfxTinted : MonoBehaviour { }
/// <summary>#917 이 원본 스킬 인스턴스(풀)의 판정 상자 원래 크기 · 마지막으로 쓴 크기(hitBoxScale 누적 방지).</summary>
public sealed class WLSkillVfxBase : MonoBehaviour { public Vector3 box , written ; }
}