2026-10-25 16:51:40 +00:00
// ─────────────────────────────────────────────────────────────────────────────
// WLStaffLaser.cs — 현자의 지팡이(10302) 기본 스킬: 전방 직선 레이저(화염 광선) 3 초 유지 · 0.5 초 간격 다단 피해 · 맞을 때마다 0.2 m 밀침 (#986 · 2026-10-26)
/ /
// PD 원문: 「지팡이 기본 스킬 시전 시 마법 시전 연출 후 지팡이 기본 공격 3타와 동일하게 손을 내지르는 모션과 함께 "RPG Game VFX Collection(URP)" 에셋의
// FX_Dragonfire 이펙트를 사용하게 해줘. 이 스킬은 전방 직선 방향으로 레이저 형태의 마법을 발사해 0.5초간격으로 다단히트 피해를 주며, 0.2m씩 적을
// 밀쳐낼 수 있으며 3초 동안 유지되고, 캐릭터가 방향을 변경할 경우 변경 된 방향으로 지속시간 동안 레이져 공격을 할 수 있어. (단, 구르기를 시도할 경우 마법이 즉시 취소)」
// ■ 그릇 = 원본 Skill_Blizzard(703304 · 시전 skill3 1 s · 지팡이 표 칸 3) — 원본 Set() 훅 1줄(Take)이 원본 눈보라 진행을 건너뛰고 넘겨받는다
// ■ 진행: 이동 막음 + 스킬 상태 → 다음 프레임 3타 자세(holdState · holdNormalized)로 애니메이터 멈춤(다른 상태로 바뀌면 다시 세움) → 손 앞(fxOffset)에 레이저(fx · 반복)를
// 시전자 정면으로 매 프레임 붙임 → interval 마다 정면 직선(length × width + 몸 반지름) 안 적에게 원본 투사체 피해(ProcessCollider) + 레이저 방향으로 push m 밀침 → seconds 뒤 끝
// ■ 방향 전환: 레이저 중엔 원본 CanMove() 가 막혀 조이스틱 이동(Run_byJoystick)이 안 불린다 → 러너가 매 프레임 가상패드 입력(섬 SimpleInput 축 · 테스트 씬 원본 FloatingJoystick · 편집기 방향키)을
// 카메라 정면 기준 방향으로 바꿔 몸만 turnDegPerSec 로 돌린다(이동 없음 · 레이저가 따라감)
// ■ 구르기 = 즉시 취소(시전자 상태가 Dodge 가 되면 레이저를 끄고 애니메이터 · 이동을 되돌린다)
// ■ 값 = Resources/WL/WLStaffLaser.json (C45) · enabled 0 = 원본 눈보라 그대로
// 🔴 Debug.LogError 금지(원본 팝업).
// ─────────────────────────────────────────────────────────────────────────────
using System.Collections ;
using System.Collections.Generic ;
using UnityEngine ;
namespace WL.Combat.Weapon
{
[System.Serializable]
public class WLStaffLaserJson
{
public int enabled = 0 ;
public string skill = "Skill_Blizzard" ; // 넘겨받을 원본 스킬 클래스 이름
public int classId = 10302 ; // 시전자 직업(0 = 모두)
public string fx = "WL/Fx/WL_Fx_StaffLaser" ; // 레이저 이펙트(Resources · 원본 빔 축 = 로컬 -Y)
public float fxScale = 0.24f ;
public float [ ] fxOffset = { 0f , 0.55f , 0.45f } ; // 시전자 정면 기준 (오른쪽, 위, 앞) m — 내민 손 앞
public float [ ] fxEuler = { - 90f , 0f , 0f } ; // 시전자 정면 기준 회전(빔 -Y → 정면 +Z)
public float seconds = 3f , interval = 0.5f ; // 유지 · 다단 간격(s) — 첫 피해는 레이저가 켜지는 순간
public float length = 8f , width = 1.2f ; // 피해 직선(m · 시전자 발에서 정면 · 폭 = 전체 · 적 몸 반지름 더함)
public float push = 0.2f ; // 맞을 때마다 레이저 방향 밀침(m)
public string holdState = "attack3" ; // 손 내지른 자세(애니메이터 상태 · 지팡이 3타)
public float holdNormalized = 0.5f ;
public float turnDegPerSec = 360f ; // 가상패드 방향으로 도는 속도
public float steerDeadzone = 0.2f ; // 가상패드 축 크기가 이보다 작으면 돌지 않음
public float fadeSeconds = 0.4f ; // 끝 · 취소 때 방출을 멈추고 지우기까지
2026-10-31 06:53:50 +00:00
public int noExtra = 0 ; // #994 1 = 원본 스킬 부가 효과(눈보라 = 감속 디버프) 없이 피해만(WLStaffEarth noExtra 와 같은 방식)
2026-10-25 16:51:40 +00:00
public string note = "" ;
}
public static class WLStaffLaser
{
public const string ResourcesPath = "WL/WLStaffLaser" ;
2026-10-31 06:53:50 +00:00
public static int Takes , Ticks , Hits , Pushes , Cancels , Turns , Reasserts , Ends , NoExtra ; public static string LastLog = "" ;
2026-10-25 16:51:40 +00:00
static WLStaffLaserJson s_cfg ; static WLStaffLaserRunner s_runner ;
internal static PCActor Active ; internal static WLStaffLaserJson ActiveCfg ;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] static void Reset ( ) { s_cfg = null ; Active = null ; ActiveCfg = null ; }
internal static WLStaffLaserJson Cfg
{
get
{
if ( s_cfg ! = null ) return s_cfg ;
s_cfg = new WLStaffLaserJson ( ) ;
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>원본 Skill_Blizzard.Set 훅 — 규칙에 맞으면 레이저로 넘겨받는다(true = 원본 진행 건너뜀).</summary>
public static bool Take ( ProjectileBase pb )
{
try
{
var c = Cfg ; if ( c . enabled = = 0 | | pb = = null | | pb . GetType ( ) . Name ! = c . skill ) return false ;
var pd = pb . Get_ProjectTileData ( ) ; var pc = pd ! = null ? pd . shooter as PCActor : null ; if ( pc = = null ) return false ;
if ( c . classId > 0 & & pc . Get_ID ( ) ! = c . classId ) return false ;
if ( s_runner = = null ) { var go = new GameObject ( "[WL986] StaffLaser" ) ; go . hideFlags = HideFlags . HideInHierarchy ; Object . DontDestroyOnLoad ( go ) ; s_runner = go . AddComponent < WLStaffLaserRunner > ( ) ; }
s_runner . Begin ( pb , pd , pc , c ) ; Takes + + ;
return true ;
}
catch ( System . Exception ex ) { LastLog = "넘겨받기 실패 — " + ex . Message ; return false ; }
}
/// <summary>레이저 중 그 방향으로 몸만 돌린다(러너가 가상패드 축을 읽어 부름).</summary>
public static bool Steer ( PCActor pc , Vector3 dir )
{
if ( Active = = null | | ! ReferenceEquals ( pc , Active ) | | ActiveCfg = = null ) return false ;
dir . y = 0f ; if ( dir . sqrMagnitude < 1e-4f ) return true ;
var want = Quaternion . LookRotation ( dir . normalized ) ;
pc . transform . rotation = Quaternion . RotateTowards ( pc . transform . rotation , want , ActiveCfg . turnDegPerSec * Time . deltaTime ) ; Turns + + ;
return true ;
}
}
internal sealed class WLStaffLaserRunner : MonoBehaviour
{
public void Begin ( ProjectileBase pb , ProjectileData pd , PCActor pc , WLStaffLaserJson c ) { StopAllCoroutines ( ) ; StartCoroutine ( Run ( pb , pd , pc , c ) ) ; }
static Vector3 V ( float [ ] a ) { return a ! = null & & a . Length > = 3 ? new Vector3 ( a [ 0 ] , a [ 1 ] , a [ 2 ] ) : Vector3 . zero ; }
IEnumerator Run ( ProjectileBase pb , ProjectileData pd , PCActor pc , WLStaffLaserJson c )
{
// 원본 겉모습 · 판정 상자 · Update 끔(끝나면 되돌림 · 풀 재사용)
var ren = new List < Renderer > ( ) ; foreach ( var x in pb . GetComponentsInChildren < Renderer > ( true ) ) if ( x . enabled ) { x . enabled = false ; ren . Add ( x ) ; }
var col = new List < Collider > ( ) ; foreach ( var x in pb . GetComponentsInChildren < Collider > ( true ) ) if ( x . enabled ) { x . enabled = false ; col . Add ( x ) ; }
bool wasEnabled = pb . enabled ; pb . enabled = false ;
2026-10-31 06:53:50 +00:00
if ( c . noExtra ! = 0 & & pd ! = null ) { pd . e_SkillExtraType = eSkillExtraType . None ; pd . m_skillTypeConfigTable = null ; pd . SetSkillExtraType = false ; WLStaffLaser . NoExtra + + ; } // #994 PD 「지팡이 기본 스킬에 명중 된 적에게 디버프 이펙트가 발생되지 않도록」
2026-10-25 16:51:40 +00:00
if ( OptionInfo . Ins ! = null ) OptionInfo . Ins . Set_PCMove ( pc , false ) ;
pc . Set_CurAnim ( eAnim . Skill ) ;
WLStaffLaser . Active = pc ; WLStaffLaser . ActiveCfg = c ;
yield return null ; // 원본 시전 코루틴이 같은 프레임에 다음 동작을 틀 수 있어 한 프레임 뒤에 자세를 세운다
var an = pc . m_animation ; float sp0 = an ! = null & & an . speed > 0f ? an . speed : 1f ;
Hold ( an , c ) ;
GameObject fx = null ; var p = ! string . IsNullOrEmpty ( c . fx ) ? Resources . Load < GameObject > ( c . fx ) : null ;
if ( p ! = null )
{
fx = Instantiate ( p ) ;
foreach ( var ps in fx . GetComponentsInChildren < ParticleSystem > ( true ) ) { var m = ps . main ; m . scalingMode = ParticleSystemScalingMode . Hierarchy ; m . loop = true ; }
fx . transform . localScale = Vector3 . one * c . fxScale ; Place ( fx , pc , c ) ;
}
else WLStaffLaser . LastLog = "레이저 이펙트 없음 " + c . fx ;
float t = 0f , next = 0f ; bool canceled = false ; int hits0 = WLStaffLaser . Hits ;
while ( t < c . seconds )
{
if ( pc = = null | | pc . IsDead ( ) ) { canceled = true ; break ; }
if ( pc . Get_CurAnim ( ) = = eAnim . Dodge ) { canceled = true ; WLStaffLaser . Cancels + + ; break ; } // PD 「구르기를 시도할 경우 마법이 즉시 취소」
if ( pc . Get_CurAnim ( ) < eAnim . Skill ) pc . Set_CurAnim ( eAnim . Skill ) ;
if ( an ! = null & & ( ! an . GetCurrentAnimatorStateInfo ( 0 ) . IsName ( c . holdState ) | | an . IsInTransition ( 0 ) ) ) { Hold ( an , c ) ; WLStaffLaser . Reasserts + + ; }
else if ( an ! = null & & an . speed ! = 0f ) an . speed = 0f ;
SteerInput ( pc , c ) ;
if ( fx ! = null ) Place ( fx , pc , c ) ;
if ( t > = next ) { next + = Mathf . Max ( 0.05f , c . interval ) ; WLStaffLaser . Ticks + + ; Tick ( pb , pc , c ) ; }
yield return null ; t + = Time . deltaTime ;
}
// 끝 · 취소 — 레이저 방출을 멈추고 흐려지게 · 애니메이터 · 이동 · 스킬 상태 되돌림 · 원본 투사체 끔
if ( fx ! = null ) { foreach ( var ps in fx . GetComponentsInChildren < ParticleSystem > ( true ) ) ps . Stop ( true , ParticleSystemStopBehavior . StopEmitting ) ; Destroy ( fx , Mathf . Max ( 0.05f , c . fadeSeconds ) ) ; }
if ( an ! = null & & an . speed = = 0f ) an . speed = sp0 ;
WLStaffLaser . Active = null ; WLStaffLaser . ActiveCfg = null ;
if ( OptionInfo . Ins ! = null & & pc ! = null ) OptionInfo . Ins . Set_PCMove ( pc , true ) ;
if ( ! canceled & & pc ! = null & & ! pc . IsDead ( ) ) pc . Play_Idle ( ) ;
if ( pb ! = null ) { pb . enabled = wasEnabled ; WLStaffEarth . OffOriginal ( pb ) ; }
foreach ( var x in ren ) if ( x ! = null ) x . enabled = true ;
foreach ( var x in col ) if ( x ! = null ) x . enabled = true ;
WLStaffLaser . Ends + + ;
WLStaffLaser . LastLog = ( canceled ? "취소 " : "끝 " ) + t . ToString ( "F2" ) + " s · 다단 " + WLStaffLaser . Ticks + " · 이번 명중 " + ( WLStaffLaser . Hits - hits0 ) + " · 밀침 " + WLStaffLaser . Pushes + " · 회전 입력 " + WLStaffLaser . Turns + " · 자세 다시 세움 " + WLStaffLaser . Reasserts ;
}
// 가상패드 축(SimpleInput) → 카메라 정면 기준 수평 방향 → Steer(몸만 회전)
static void SteerInput ( PCActor pc , WLStaffLaserJson c )
{
var a = PadAxis ( ) ; float h = a . x , v = a . y ;
if ( h * h + v * v < c . steerDeadzone * c . steerDeadzone ) return ;
var cam = Camera . main ; var fwd = cam ! = null ? cam . transform . forward : Vector3 . forward ; fwd . y = 0f ; if ( fwd . sqrMagnitude < 1e-4f ) fwd = Vector3 . forward ; fwd . Normalize ( ) ;
WLStaffLaser . Steer ( pc , fwd * v + Vector3 . Cross ( Vector3 . up , fwd ) * h ) ;
}
// 가상패드 입력 — 섬 = SimpleInput 축(WLIslandAvatar) · 테스트 씬 = 원본 가상패드(Joystick Pack FloatingJoystick · global::Joystick.Direction) · 편집기 방향키 — 먼저 잡히는 것
static global :: Joystick s_joy ; static float s_joyFind ;
2026-10-31 06:53:50 +00:00
internal static Vector2 PadAxis ( )
2026-10-25 16:51:40 +00:00
{
var a = Vector2 . zero ;
try { a = new Vector2 ( SimpleInput . GetAxis ( "Horizontal" ) , SimpleInput . GetAxis ( "Vertical" ) ) ; } catch { }
if ( a . sqrMagnitude < 1e-4f )
{
if ( s_joy = = null & & Time . unscaledTime > = s_joyFind ) { s_joy = Object . FindAnyObjectByType < global :: Joystick > ( ) ; s_joyFind = Time . unscaledTime + 1f ; }
if ( s_joy ! = null & & s_joy . isActiveAndEnabled ) a = s_joy . Direction ;
}
if ( a . sqrMagnitude < 1e-4f ) a = new Vector2 ( ( Input . GetKey ( KeyCode . RightArrow ) ? 1f : 0f ) - ( Input . GetKey ( KeyCode . LeftArrow ) ? 1f : 0f ) , ( Input . GetKey ( KeyCode . UpArrow ) ? 1f : 0f ) - ( Input . GetKey ( KeyCode . DownArrow ) ? 1f : 0f ) ) ;
return a ;
}
static void Hold ( Animator an , WLStaffLaserJson c )
{
if ( an = = null ) return ;
an . speed = 0f ; an . Play ( c . holdState , 0 , Mathf . Clamp01 ( c . holdNormalized ) ) ; an . Update ( 0f ) ;
}
static void Place ( GameObject fx , PCActor pc , WLStaffLaserJson c )
{
var f = pc . transform . forward ; f . y = 0f ; if ( f . sqrMagnitude < 1e-4f ) f = Vector3 . forward ; var basis = Quaternion . LookRotation ( f . normalized ) ;
fx . transform . SetPositionAndRotation ( pc . transform . position + basis * V ( c . fxOffset ) , basis * Quaternion . Euler ( V ( c . fxEuler ) ) ) ;
}
// 정면 직선(시전자 발에서 length · 폭 width + 적 몸 반지름) 안 적 — 원본 투사체 피해 + 레이저 방향 밀침
static void Tick ( ProjectileBase pb , PCActor pc , WLStaffLaserJson c )
{
var info = ActorInfo . Ins ; if ( info = = null | | pb = = null ) return ;
var o = pc . Get_position ( ) ; var f = pc . transform . forward ; f . y = 0f ; if ( f . sqrMagnitude < 1e-4f ) return ; f . Normalize ( ) ;
var list = info . Get_EnemyActors ( pc . IsEnemy ( ) , o , c . length + 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 v = a . Get_position ( ) - o ; v . y = 0f ; float along = Vector3 . Dot ( v , f ) , side = Mathf . Abs ( Vector3 . Cross ( f , v ) . y ) , br = WL . Combat . WLCombatMotionSettings . BodyRadius ( a ) ;
if ( along < - br | | along > c . length + br | | side > c . width * 0.5f + br ) continue ;
var cl = a . GetComponent < Collider > ( ) ; if ( cl = = null | | ! cl . enabled ) continue ;
if ( ! pb . ProcessCollider ( cl , false ) ) continue ; WLStaffLaser . Hits + + ;
if ( c . push > 0f )
{
var d = f * c . push ; var ag = a . GetComponent < UnityEngine . AI . NavMeshAgent > ( ) ;
if ( ag ! = null & & ag . enabled & & ag . isOnNavMesh ) ag . Move ( d ) ; else a . transform . position + = d ;
WLStaffLaser . Pushes + + ;
}
}
}
finally { info . Release_Actors ( list ) ; }
}
}
}