2026-10-18 18:48:42 +00:00
// ─────────────────────────────────────────────────────────────────────────────
// WLShadowSlash.cs — 단검(비수 1043 · Dagger 10202) 주력 「그림자 베기」(Skill_ShadowSlash) — 시전 중 캐릭터 보이기 · 이펙트 크기 배율
/ /
// PD 원문(#943-⑧ · 2026-10-19): 「단검 주력 스킬 시전 시 캐릭터의 모습이 보이도록 수정하고, 이펙트의 크기를 절반으로 줄여줘.」
/ /
// ■ 안 보이던 원인(코드 실측) — 원본 Skill_ShadowSlash.Set 이 shooter.Set_Invisible(2.1 초)를 부른다 → Actor.Co_Invisible 이
// 애니메이터 배속 0 · 무적 · Model_OnOff(false)(파티클 외 렌더러 전부 끔) · Weapon_OnOff(false) · 이동 금지 → 2.1 초 뒤 되돌린 다음 대상 뒤 1.5 m 로 순간이동.
// 이펙트가 덮거나 카메라 문제가 아니라 원본 스킬이 모델을 끈다.
// ■ 처리 — 원본 Set 의 Set_Invisible 뒤 훅 1줄(OnSet): Co_Invisible 이 끈 렌더러 · 무기를 바로 다시 켠다(원본이 2.1 초 뒤 부르는 같은 함수).
// freezeAnim 0 이면 애니메이터 배속도 1 로 되돌려 원본 상태 사슬 DualBlades_Jump_Start → JumpAttack1·2·3 → Jump_End → idle 이 보인다
// (JumpAttack1~3 클립 이벤트 0 개 · Jump_Start 의 SkillEvent 는 시각 0 하나 · 종료 전이 0.7 이라 다시 안 돈다 — 2026-10-19 컨트롤러 · FBX 메타 실측).
// 무적 · 이동 금지 · 2.1 초 뒤 대상 뒤 순간이동 · 판정은 원본 그대로.
// ■ 이펙트 — 베기(go_effect = Slash · 파티클 6 층 · 크기 2.6 ~ 7.7)와 연기(go_invisibleEffect = FX_DarkSkill_SmokeExplosion)의 Transform 을
// 「처음 크기 × 배율」로(풀 재사용 때 누적 없음 · 처음 크기는 WLShadowSlashBase 에 기억) · 파티클 크기 모드 Hierarchy
// (베기 팩 원본이 Local 이라 Transform 배율이 안 먹는다). 베기 판정 상자(BoxCollider 5.25 × 9 × 5.9)는 keepHitBox 1 이면 배율의 역수로 키워 월드 크기 그대로.
// ■ 값 = Resources/WL/WLShadowSlash.json (C45) · 끄기 = enabled 0(원본 그대로)
// 🔴 Debug.LogError 금지(원본 ErrorLogHook 팝업) — 실패는 LastLog
// ─────────────────────────────────────────────────────────────────────────────
using System.Reflection ;
using UnityEngine ;
namespace WL.Combat.Weapon
{
[System.Serializable]
public class WLShadowSlashJson
{
public int enabled = 0 ;
public int keepVisible = 1 ; // 1 = 시전 중 PC 렌더러 · 무기를 켠다(원본 = 2.1 초 숨김)
public int freezeAnim = 0 ; // 1 = 원본처럼 애니메이터 배속 0 유지(보이면 2.1 초 정지 자세) · 0 = 원본 상태 사슬 재생
public float slashScale = 1f ; // 베기(go_effect) 보이는 크기 배율
public float smokeScale = 1f ; // 연기(go_invisibleEffect) 크기 배율
2026-10-21 14:13:56 +00:00
public int smokeHide ; // #965 PD 「비수 주력스킬 사용 때 캐릭터 주위에 생기는 독 연기 이펙트는 제거」 — 1 = 연기(시작 · 끝 순간이동 자리) 렌더러를 끈다(풀 인스턴스라 매 시전 값대로 맞춤)
2026-10-18 18:48:42 +00:00
public int keepHitBox = 1 ; // 1 = 베기 판정 상자 월드 크기 유지(보이는 크기만 바꿈)
2026-10-20 16:54:40 +00:00
// #954 PD 「비수 주력 = Slash Effects FREE 의 녹색 난사(Multiple Slash 2)로」 — 원본 베기 렌더러를 끄고(판정 상자 그대로) WL 사본을 같은 자리·시각에 띄운다
public string fxPrefab = "" ; // Resources 경로(비우면 원본 베기 그대로)
public float fxScale = 1f , fxDelay = 0.5f , fxForward = 1.5f , fxYOffset = 0f , fxLife = 2.5f ; // 배율 · 시전 뒤 시각(원본 go_effect 0.5 s) · PC 정면 거리(원본 1.5) · 높이 · 수명
2026-10-21 04:05:18 +00:00
// #958 PD 「이펙트가 무기 방향 전방에 다른 무기 궤적처럼 하나만 — 전방의 적이 있는 방향에 1개 · 플레이어가 타겟하려는 대상의 무기 공격 범위가 중점」 —
// fxAimTarget 1 = 방향 = 지금 대상 쪽(없으면 PC 정면) · fxRangeFrac > 0 = 중심 거리 = 무기 공격 범위(PCActor.WL_AttackRange) × 이 값(대상이 더 가까우면 대상 자리) ·
// fxRangeWidth > 0 = 보이는 폭 = 공격 범위 × 이 값(배율 = 폭 ÷ fxPrefabWidth · 프리팹 단위 폭 편집 모드 실측) — 0 이면 fxForward · fxScale 그대로
public int fxAimTarget = 0 ;
public float fxRangeFrac = 0f , fxRangeWidth = 0f , fxPrefabWidth = 5.26f ;
2026-10-20 16:54:40 +00:00
public int hideOriginalFx = 0 ; // 1 = 원본 베기(go_effect) 렌더러 끔
2026-10-21 01:28:52 +00:00
// #955 PD 「주력 쓸 때 왜 캐릭터가 공중에 뜬 채 행동하나 — 제자리에서 단검을 난사하도록」 — 원본 상태 사슬(DualBlades_Jump_Start → JumpAttack1·2·3 = 공중 자세)을 쓰지 않고 slashSeconds 동안 slashStates 를 slashInterval 마다 번갈아 재생(배속 slashSpeed · 애니메이션 이벤트 끔 = 추가 타격·궤적 없음)
public string [ ] slashStates = new string [ 0 ] ;
public float slashInterval = 0.28f , slashSpeed = 1.8f , slashSeconds = 2.0f ;
2026-10-21 01:54:38 +00:00
// #956 PD 「주력 이펙트가 너무 먼 위치에 재생 → 플레이어가 휘두르는 무기 위치에 맞게(휘두르는 느낌)」 — fxMain 0 = 정면 1회 사본(fxForward)을 안 띄움 ·
// fxPerSlash 1 = 난사 루프(slashInterval)마다 fxPrefab 의 작은 사본(fxSlashScale)을 무기가 달린 소켓(PCActor.tfs_weapon · 비수 = 양손 · 번갈아)의 손잡이→날 끝 fxSlashFraction 자리 + 위 fxSlashYOffset 에
// 띄우고 fxSlashLife 동안 날 방향(수평 성분)·위치를 매 프레임 따라가게(fxSlashFollow 1 · 0 = 처음 자리 고정)
public int fxMain = 1 , fxPerSlash = 0 , fxSlashFollow = 1 ;
public float fxSlashScale = 0.35f , fxSlashLife = 0.6f , fxSlashFraction = 0.6f , fxSlashYOffset = 0f ;
2026-10-21 04:56:16 +00:00
// #959 PD 「이펙트 크기 2배 · 플레이어 동작에 맞게 재생시간을 늘려 모션만 보이지 않게(캐릭터 전진값 고려해 위치도) · 공격 모션에 잔상(빠르게 공격하는 느낌)」 —
// fxRepeatInterval > 0 = 정면 사본을 fxDelay 뒤 이 간격으로 fxRepeatSeconds(0 = slashSeconds) 까지 되풀이(매번 그때의 PC 위치·대상 방향으로 다시 계산 = 전진 반영) ·
// slashHardRestart 1 = 난사 상태를 CrossFade 대신 Animator.Play 로 강제(Play 실측: 배속 1.8 에선 컨트롤러 exit 전이(2타→3타 0.50)가 0.28 s 틱보다 먼저 와 3타 회전·4타 돌진(0.4 m 전진)이 섞였다) ·
// slashGhosts 1 = 난사 동안 slashGhostInterval 마다 지금 자세 잔상(WLGhostStrike.LiveGhost · 색 RGBA · 유지 · 소멸 초)
public float fxRepeatInterval = 0f , fxRepeatSeconds = 0f ;
public int slashHardRestart = 0 , slashGhosts = 0 ;
public float slashGhostInterval = 0.07f , slashGhostHold = 0.03f , slashGhostFade = 0.14f ; public float [ ] slashGhostColor = new float [ 0 ] ;
2026-10-21 05:29:56 +00:00
// #960 PD 「비수 주력 스킬 공격 시 총 10회 타격이 가능하도록 애니메이션 재생 타이밍에 적절하게 판정」 — slashHits > 0 = 원본 베기 판정(시전 0.5 s 뒤 0.1 s 간격 6회 · go_effect Loop 6 · 난사와 무관)을 끄고
// 난사 휘두름(slashInterval 틱)마다 slashHitDelay 초 뒤(칼이 지나가는 순간) 1회씩 slashHits 회까지 원본 한 대 피해(Skill_ShadowSlash.ProcessCollider · 1회 피해 · 명중 이펙트 · 피격 반응 그대로) ·
// 범위 = PC 발 기준 slashHitRadius(0 = 공격 범위 × slashHitRangeMul) + 적 몸 반지름 · 대상(없으면 정면) 쪽 ±slashHitHalfAngle°
public int slashHits = 0 ;
public float slashHitDelay = 0.1f , slashHitRadius = 0f , slashHitRangeMul = 1.6f , slashHitHalfAngle = 110f ;
2026-10-21 07:56:27 +00:00
// #961 PD 「주력 중 캐릭터가 움직이면 이펙트만 그 자리에 남음 → 못 움직이게 · 구르기로 캔슬하면 이펙트도 같이 소멸」 · 「이펙트 소멸 타이밍에 맞게 모션도 종료 · 최소 0.5 s 딜레이 후 공격」 —
// moveHold 1 = 난사 동안 이동 입력 무시(WLSkillCancel.HoldMove · 구르기는 그대로) · dodgeCancel 1 = 구르기(eAnim.Dodge)가 시작되면 난사 · 이펙트 반복 · 잔상 · 판정을 멈추고 살아 있는 이펙트를 지운다 ·
// fxEndAlign 1 = 정면 사본 반복을 fxVisibleLife(초승달 보이는 시간) 만큼 일찍 끝내 마지막 이펙트가 난사 끝에 함께 사라지게 · endHoldSeconds = 난사가 끝난 뒤 공격 금지(자동 전투 IsStop · 하네스 공격 키)
public int moveHold = 0 , dodgeCancel = 0 , fxEndAlign = 0 ;
public float fxVisibleLife = 0.3f , endHoldSeconds = 0f ;
2026-10-21 08:40:26 +00:00
// #962 PD 「주력 이펙트가 두 번 연속 재생되는 것 같다 → 한 번만 재생하고 모션을 이펙트에 맞춰」 · 「주력 판정이 발생되지 않는다 → 원인 확인 · 수정」 —
// slashMatchFx 1 = 난사 시간 = fxDelay + 사본 1개가 보이는 시간(프리팹 입자계마다 시작 지연 + 마지막 분출 시각 + 수명의 최댓값 · FxDuration) ·
// invisibleMatch 1 = 원본 숨김(Set_Invisible 2.1 s = 이동 금지 · 무적 · 끝 대기 · 대상 뒤 순간이동)도 그 시간에 맞춤(원본 훅 1줄 InvisibleSeconds) ·
// slashHitEven 1 = 판정 slashHits 회를 난사 시간에 고르게(휘두름 수와 무관 · 그전엔 휘두름마다 1회 + 모자란 만큼 끝에 몰아서)
public int slashMatchFx = 0 , invisibleMatch = 0 , slashHitEven = 0 ;
2026-10-21 07:56:27 +00:00
public int endIdle = 0 ; // #961 1 = 난사가 끝나는 순간(= 마지막 이펙트 소멸) 대기 동작으로(원본은 0.1 s 뒤 2.1 s 콜백에서 대기 · 그사이 마지막 휘두름이 이어졌다)
2026-10-22 10:39:16 +00:00
// #970 PD 「비수 주력 스킬 사용 시 적이 갑자기 순간이동 하는 현상 … 순간이동하지 못하게」 — 원인 = 원본 끝 콜백이 PC 를 대상 뒤 1.5 m 로 옮겨(Set_Warp) 카메라가 따라가는 화면에서 적이 반대편으로 튐 ·
// 0 = 끝 순간이동과 그때의 카메라 돌리기(RotateCam_TargetBack)를 건너뛴다(PC 는 난사한 자리 그대로) · 1 = 원본
public int endWarp = 1 ;
2026-10-18 18:48:42 +00:00
}
public static class WLShadowSlash
{
public const string ResourcesPath = "WL/WLShadowSlash" ;
public static int Casts , Shown , Scaled , Errors ;
public static string LastLog = "" ;
static WLShadowSlashJson s_cfg ;
static MethodInfo s_model , s_weapon ;
static WLShadowSlashJson Cfg
{
get
{
if ( s_cfg ! = null ) return s_cfg ;
s_cfg = new WLShadowSlashJson ( ) ;
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 ;
}
}
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
2026-10-21 08:40:26 +00:00
static void Boot ( ) { s_cfg = null ; Casts = Shown = Scaled = Errors = 0 ; SlashLoops = SlashLeaks = FxShown = FxRepeats = FxSlashes = SlashGhostCount = SlashHits = SlashHitTargets = 0 ; DodgeCancels = EndHolds = EndSkips = EndIdles = FxStops = 0 ; s_holdActor = s_castShooter = s_slashActor = null ; s_liveFx . Clear ( ) ; s_fxDur . Clear ( ) ; LastLog = SlashLog = FxRepeatLog = SlashHitLog = "" ; }
2026-10-18 18:48:42 +00:00
2026-10-21 04:56:16 +00:00
public static string Describe ( ) { return "casts " + Casts + " · shown " + Shown + " · scaled " + Scaled + " · err " + Errors + " · fx " + FxShown + "(반복 " + FxRepeats + ") · 잔상 " + SlashGhostCount + " · " + LastLog ; }
2026-10-18 18:48:42 +00:00
/// <summary>원본 Skill_ShadowSlash.Set 훅(Set_Invisible 바로 뒤 1줄) — 캐릭터 보이기 · 베기 · 연기 크기 배율.</summary>
public static void OnSet ( Skill_ShadowSlash skill )
{
try
{
var c = Cfg ;
if ( skill = = null | | c . enabled = = 0 ) return ;
Casts + + ;
var pd = skill . Get_ProjectTileData ( ) ;
var shooter = pd ! = null ? pd . shooter : null ;
if ( c . keepVisible ! = 0 & & ! DSUtil . CheckNull ( shooter ) )
{
bool model = Call ( ref s_model , "Model_OnOff" , shooter ) ;
bool weapon = Call ( ref s_weapon , "Weapon_OnOff" , shooter ) ;
if ( model & & weapon ) Shown + + ;
if ( c . freezeAnim = = 0 & & shooter . m_animation ! = null ) shooter . m_animation . speed = 1f ;
}
if ( skill . go_effect ! = null ) Scale ( skill . go_effect . transform , c . slashScale , c . keepHitBox ! = 0 ) ;
2026-10-21 14:13:56 +00:00
if ( skill . go_invisibleEffect ! = null )
{
Scale ( skill . go_invisibleEffect . transform , c . smokeScale , false ) ;
foreach ( var rd in skill . go_invisibleEffect . GetComponentsInChildren < Renderer > ( true ) ) rd . enabled = c . smokeHide = = 0 ; // #965 독 연기 끄기(원본이 끝에 다시 켜도 렌더러는 꺼진 채)
}
2026-10-21 07:56:27 +00:00
int seq = + + s_castSeq ; s_liveFx . Clear ( ) ; s_castShooter = shooter ; // #961 이번 시전 번호(구르기 캔슬 때 반복 · 잔상을 멈추는 표지)
2026-10-20 16:54:40 +00:00
if ( ! string . IsNullOrEmpty ( c . fxPrefab ) & & ! DSUtil . CheckNull ( shooter ) ) // #954
{
if ( c . hideOriginalFx ! = 0 & & skill . go_effect ! = null ) foreach ( var rd in skill . go_effect . GetComponentsInChildren < Renderer > ( true ) ) rd . enabled = false ; // 풀 인스턴스라 한 번 끄면 유지
2026-10-21 07:56:27 +00:00
if ( c . fxMain ! = 0 ) Runner . StartCoroutine ( CoFx ( shooter , c , seq ) ) ; // #956 fxMain 0 = 정면 사본 없이 난사 사본(fxPerSlash)만
2026-10-20 16:54:40 +00:00
}
2026-10-21 05:29:56 +00:00
HitMode ( skill , c ) ; // #960 원본 6회 판정 끄기(slashHits > 0) / 되돌리기
2026-10-21 08:40:26 +00:00
if ( c . moveHold ! = 0 & & shooter is PCActor mh ) WLSkillCancel . HoldMove ( mh , Mathf . Max ( 0.1f , SlashSec ( c ) ) ) ; // #961 난사 동안 이동 입력 무시(구르기는 됨)
2026-10-21 07:56:27 +00:00
if ( c . slashStates ! = null & & c . slashStates . Length > 0 & & ! DSUtil . CheckNull ( shooter ) & & shooter . m_animation ! = null ) Runner . StartCoroutine ( CoSlash ( shooter , c , skill , seq ) ) ; // #955 · #960 휘두름마다 판정 · #961 구르기 캔슬 · 끝 공격 금지
if ( c . slashGhosts ! = 0 & & ! DSUtil . CheckNull ( shooter ) ) Runner . StartCoroutine ( CoGhosts ( shooter , c , seq ) ) ; // #959 난사 잔상
2026-10-18 18:48:42 +00:00
LastLog = "cast " + Casts + " · shown " + Shown + " · scaled " + Scaled ;
}
catch ( System . Exception ex ) { Errors + + ; LastLog = "실패 — " + ex . Message ; }
}
2026-10-21 08:40:26 +00:00
static readonly System . Collections . Generic . Dictionary < string , float > s_fxDur = new System . Collections . Generic . Dictionary < string , float > ( ) ;
/// <summary>#962 이펙트 프리팹 1회가 보이는 시간 — 입자계마다 (시작 지연 + 마지막 분출 시각 + 수명 최댓값)의 최댓값(반복 입자계 · 끝없는 분출은 빼고 · 비율 분출은 duration). WL_Fx_DaggerMulti = 0.75 + 0.3 = 1.05 s.</summary>
public static float FxDuration ( string path )
{
float v ;
if ( string . IsNullOrEmpty ( path ) ) return 0f ;
if ( s_fxDur . TryGetValue ( path , out v ) ) return v ;
v = 0f ; var p = Resources . Load < GameObject > ( path ) ;
if ( p ! = null )
foreach ( var ps in p . GetComponentsInChildren < ParticleSystem > ( true ) )
{
var m = ps . main ; if ( m . loop ) continue ;
float delay = m . startDelay . mode = = ParticleSystemCurveMode . Constant ? m . startDelay . constant : m . startDelay . constantMax ;
float life = m . startLifetime . mode = = ParticleSystemCurveMode . Constant ? m . startLifetime . constant : ( m . startLifetime . mode = = ParticleSystemCurveMode . TwoConstants ? m . startLifetime . constantMax : m . startLifetime . curveMultiplier ) ;
var em = ps . emission ; float last = - 1f ;
for ( int i = 0 ; i < em . burstCount ; i + + ) { var b = em . GetBurst ( i ) ; if ( b . cycleCount < = 0 ) continue ; last = Mathf . Max ( last , b . time + ( b . cycleCount - 1 ) * b . repeatInterval ) ; }
if ( em . rateOverTime . constantMax > 0f ) last = Mathf . Max ( last , m . duration ) ;
if ( last < 0f ) continue ;
v = Mathf . Max ( v , delay + last + life ) ;
}
s_fxDur [ path ] = v ; return v ;
}
/// <summary>#962 난사 시간 — slashMatchFx 면 fxDelay + 사본 1개가 보이는 시간(없으면 slashSeconds).</summary>
public static float SlashSec ( WLShadowSlashJson c )
{
if ( c . slashMatchFx ! = 0 & & ! string . IsNullOrEmpty ( c . fxPrefab ) ) { float d = FxDuration ( c . fxPrefab ) ; if ( d > 0.05f ) return Mathf . Max ( 0f , c . fxDelay ) + d ; }
return c . slashSeconds ;
}
/// <summary>#962 원본 Skill_ShadowSlash.Set 의 Set_Invisible(2.1 s) 인자 훅 — invisibleMatch 면 난사 시간으로(이동 금지 · 무적 · 끝 대기 · 대상 뒤 순간이동이 모션 끝에 함께).</summary>
public static float InvisibleSeconds ( float orig )
{
try { var c = Cfg ; if ( c . enabled = = 0 | | c . invisibleMatch = = 0 ) return orig ; return Mathf . Max ( 0.2f , SlashSec ( c ) ) ; }
catch ( System . Exception ) { return orig ; }
}
2026-10-20 16:54:40 +00:00
public static int FxShown ;
static WLShadowSlashRunner s_runner ;
static WLShadowSlashRunner Runner { get { if ( s_runner = = null ) { var go = new GameObject ( "[WL954] ShadowSlashFx" ) ; go . hideFlags = HideFlags . HideAndDontSave ; Object . DontDestroyOnLoad ( go ) ; s_runner = go . AddComponent < WLShadowSlashRunner > ( ) ; } return s_runner ; } }
2026-10-21 04:56:16 +00:00
public static int FxRepeats ; public static string FxRepeatLog = "" ;
/// <summary>#954 원본 베기가 켜지는 시각(fxDelay)에 WL 사본(녹색 난사)을 PC 정면 fxForward 에 띄운다(PC 방향 · fxLife 뒤 파괴). #959 fxRepeatInterval > 0 이면 fxRepeatSeconds(0 = slashSeconds)까지 되풀이 — 매번 그때 PC 위치·대상 방향.</summary>
2026-10-21 07:56:27 +00:00
static System . Collections . IEnumerator CoFx ( Actor shooter , WLShadowSlashJson c , int seq )
2026-10-20 16:54:40 +00:00
{
if ( c . fxDelay > 0f ) yield return new WaitForSeconds ( c . fxDelay ) ;
2026-10-21 08:40:26 +00:00
float t0 = Time . time - c . fxDelay , until = c . fxRepeatSeconds > 0f ? c . fxRepeatSeconds : SlashSec ( c ) ; int n = 0 ;
2026-10-21 07:56:27 +00:00
if ( c . fxEndAlign ! = 0 ) until = Mathf . Max ( c . fxDelay + 0.01f , until - Mathf . Max ( 0f , c . fxVisibleLife ) ) ; // #961 마지막 초승달이 난사 끝에 함께 사라지게
2026-10-21 04:56:16 +00:00
while ( true )
{
2026-10-21 07:56:27 +00:00
if ( DSUtil . CheckNull ( shooter ) | | shooter . IsDead ( ) | | s_cancelSeq = = seq ) yield break ;
2026-10-21 04:56:16 +00:00
if ( ! SpawnMainFx ( shooter , c ) ) yield break ;
n + + ; if ( n > 1 ) FxRepeats + + ;
if ( c . fxRepeatInterval < = 0f ) yield break ;
yield return new WaitForSeconds ( c . fxRepeatInterval ) ;
2026-10-21 07:56:27 +00:00
if ( Time . time - t0 + 0.01f > = until )
{
// #961 사본 1개 = 초승달 6개를 0~0.75 s 에 나눠 뿜어 약 1.05 s 보인다(편집 모드 실측) → 반복을 멈추는 이 순간 살아 있는 사본의 분출도 멈춰 이미 나온 초승달(수명 fxVisibleLife)만 끝까지 = 난사 끝에 함께 소멸
if ( c . fxEndAlign ! = 0 ) foreach ( var go in s_liveFx ) if ( go ! = null ) foreach ( var ps in go . GetComponentsInChildren < ParticleSystem > ( true ) ) { ps . Stop ( false , ParticleSystemStopBehavior . StopEmitting ) ; FxStops + + ; }
FxRepeatLog = "정면 사본 " + n + "개 · " + ( Time . time - t0 ) . ToString ( "F2" ) + " s" + ( c . fxEndAlign ! = 0 ? " · 분출 멈춤" : "" ) ; yield break ;
}
2026-10-21 04:56:16 +00:00
}
}
static bool SpawnMainFx ( Actor shooter , WLShadowSlashJson c )
{
var p = Resources . Load < GameObject > ( c . fxPrefab ) ; if ( p = = null ) { LastLog = "fx 프리팹 없음 " + c . fxPrefab ; return false ; }
2026-10-20 16:54:40 +00:00
var fwd = shooter . transform . forward ; fwd . y = 0f ; if ( fwd . sqrMagnitude < 1e-4f ) fwd = Vector3 . forward ; fwd . Normalize ( ) ;
2026-10-21 04:05:18 +00:00
var tgt = shooter . Get_Target ( ) ; float toTarget = - 1f ;
if ( c . fxAimTarget ! = 0 & & ! DSUtil . CheckNull ( tgt ) & & ! tgt . IsDead ( ) ) // #958 대상 쪽
{
var d = tgt . Get_position ( ) - shooter . Get_position ( ) ; d . y = 0f ;
if ( d . sqrMagnitude > 1e-4f ) { toTarget = d . magnitude ; fwd = d / toTarget ; }
}
var pcA = shooter as PCActor ; float range = pcA ! = null ? pcA . WL_AttackRange : 0f ;
float dist = c . fxForward , scale = c . fxScale ;
if ( c . fxRangeFrac > 0f & & range > 0.05f ) { dist = range * c . fxRangeFrac ; if ( toTarget > 0f & & toTarget < dist ) dist = toTarget ; } // #958 공격 범위 가운데(대상이 더 가까우면 대상 자리)
if ( c . fxRangeWidth > 0f & & range > 0.05f ) scale = range * c . fxRangeWidth / Mathf . Max ( 0.1f , c . fxPrefabWidth ) ;
var pos = shooter . Get_Center_position ( ) + fwd * dist + Vector3 . up * c . fxYOffset ;
var go = Object . Instantiate ( p , pos , Quaternion . LookRotation ( fwd , Vector3 . up ) ) ; go . transform . localScale = p . transform . localScale * scale ;
2026-10-20 16:54:40 +00:00
foreach ( var ps in go . GetComponentsInChildren < ParticleSystem > ( true ) ) { var m = ps . main ; m . scalingMode = ParticleSystemScalingMode . Hierarchy ; ps . Play ( true ) ; }
2026-10-21 04:05:18 +00:00
Object . Destroy ( go , Mathf . Max ( 0.5f , c . fxLife ) ) ; FxShown + + ;
2026-10-21 07:56:27 +00:00
s_liveFx . RemoveAll ( x = > x = = null ) ; s_liveFx . Add ( go ) ; // #961 구르기 캔슬 때 지울 목록
2026-10-21 04:05:18 +00:00
LastLog = "fx " + c . fxPrefab + " @" + pos . ToString ( "F2" ) + " × " + scale . ToString ( "F3" ) + " · 거리 " + dist . ToString ( "F2" ) + " · 공격 범위 " + range . ToString ( "F2" ) + " · 대상 " + ( toTarget > 0f ? toTarget . ToString ( "F2" ) + " m" : "없음" ) ;
FxSlashLog = LastLog ;
2026-10-21 04:56:16 +00:00
return true ;
}
public static int SlashGhostCount ;
/// <summary>#959 난사 동안 지금 자세 잔상 — slashGhostInterval 마다 WLGhostStrike.LiveGhost(색 · 유지 · 소멸).</summary>
2026-10-21 07:56:27 +00:00
static System . Collections . IEnumerator CoGhosts ( Actor shooter , WLShadowSlashJson c , int seq )
2026-10-21 04:56:16 +00:00
{
float t0 = Time . time ; var col = c . slashGhostColor ! = null & & c . slashGhostColor . Length > = 4 ? c . slashGhostColor : null ;
2026-10-21 08:40:26 +00:00
float sec = SlashSec ( c ) ;
while ( Time . time - t0 < sec )
2026-10-21 04:56:16 +00:00
{
2026-10-21 07:56:27 +00:00
if ( DSUtil . CheckNull ( shooter ) | | shooter . IsDead ( ) | | s_cancelSeq = = seq ) yield break ;
2026-10-21 04:56:16 +00:00
if ( WLGhostStrike . LiveGhost ( shooter , col , c . slashGhostHold , c . slashGhostFade ) ) SlashGhostCount + + ;
yield return new WaitForSeconds ( Mathf . Max ( 0.02f , c . slashGhostInterval ) ) ;
}
2026-10-20 16:54:40 +00:00
}
2026-10-21 05:29:56 +00:00
public static int SlashHits , SlashHitTargets ; public static string SlashHitLog = "" , LastMissInfo = "" ;
/// <summary>#960 원본 베기 판정(go_effect · Projectile_StopAndColliderControl Loop 6 · 0.1 s)을 slashHits > 0 이면 끄고(Loop 0) · 0 이면 처음 값으로(풀 인스턴스라 값이 남는다).</summary>
static void HitMode ( Skill_ShadowSlash skill , WLShadowSlashJson c )
{
var ge = skill ! = null ? skill . go_effect : null ; if ( ge = = null ) return ;
var mark = ge . GetComponent < WLShadowSlashHits > ( ) ;
if ( c . slashHits > 0 )
{
if ( mark = = null ) { mark = ge . gameObject . AddComponent < WLShadowSlashHits > ( ) ; mark . loop0 = ge . Loop ; mark . loopTime0 = ge . LoopTime ; }
ge . Loop = 0 ; ge . LoopTime = 0f ;
}
else if ( mark ! = null ) { ge . Loop = mark . loop0 ; ge . LoopTime = mark . loopTime0 ; Object . Destroy ( mark ) ; }
}
/// <summary>#960 휘두름 1회 판정 — PC 발 기준 범위 + 적 몸 반지름 · 대상 쪽 ±각 안의 적마다 원본 한 대 피해(대검 각성 WLBladeStorm.Hit 와 같은 방식) · 맞은 수.</summary>
static int SlashHit ( ProjectileBase pb , Actor shooter , WLShadowSlashJson c )
{
var info = ActorInfo . Ins ; if ( info = = null | | pb = = null | | DSUtil . CheckNull ( shooter ) ) return 0 ;
var origin = shooter . Get_position ( ) ;
var fwd = shooter . transform . forward ; fwd . y = 0f ; var tgt = shooter . Get_Target ( ) ;
if ( ! DSUtil . CheckNull ( tgt ) & & ! tgt . IsDead ( ) ) { var d = tgt . Get_position ( ) - origin ; d . y = 0f ; if ( d . sqrMagnitude > 1e-4f ) fwd = d ; }
if ( fwd . sqrMagnitude < 1e-4f ) fwd = Vector3 . forward ; fwd . Normalize ( ) ;
var pcA = shooter as PCActor ; float range = c . slashHitRadius > 0f ? c . slashHitRadius : ( pcA ! = null ? pcA . WL_AttackRange : 1f ) * Mathf . Max ( 0.1f , c . slashHitRangeMul ) ;
var list = info . Get_EnemyActors ( shooter . IsEnemy ( ) , origin , range + 3f ) ; if ( list = = null ) return 0 ;
int n = 0 ; LastMissInfo = "" ;
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 ( k = = 0 ) LastMissInfo = "d" + v . magnitude . ToString ( "F2" ) + "/" + ( range + WL . Combat . WLCombatMotionSettings . BodyRadius ( a ) ) . ToString ( "F2" ) + " a" + ( v . sqrMagnitude > 1e-4f ? Vector3 . Angle ( fwd , v ) : 0f ) . ToString ( "F0" ) ;
if ( v . magnitude > range + WL . Combat . WLCombatMotionSettings . BodyRadius ( a ) ) continue ;
if ( v . sqrMagnitude > 1e-4f & & Vector3 . Angle ( fwd , v ) > c . slashHitHalfAngle ) continue ;
var col = a . GetComponent < Collider > ( ) ; if ( col = = null | | ! col . enabled ) continue ;
if ( pb . ProcessCollider ( col , false ) ) n + + ;
}
info . Release_Actors ( list ) ;
return n ;
}
2026-10-21 04:56:16 +00:00
public static int SlashLoops , SlashLeaks ; public static string SlashLog = "" ;
2026-10-21 05:29:56 +00:00
/// <summary>#955 제자리 난사 — slashSeconds 동안 slashStates 를 번갈아 CrossFade(이벤트 끔 · 배속) · 스킬 상태를 벗어나면 중단 · 끝에 이벤트·배속 원상. #960 휘두름마다 판정(slashHits).</summary>
2026-10-21 07:56:27 +00:00
static System . Collections . IEnumerator CoSlash ( Actor shooter , WLShadowSlashJson c , ProjectileBase pb , int seq )
2026-10-21 01:28:52 +00:00
{
var an = shooter . m_animation ; if ( an = = null ) yield break ;
2026-10-21 07:56:27 +00:00
bool canceled = false ; // #961 구르기 캔슬
if ( c . moveHold ! = 0 ) s_slashActor = shooter ; // #961 난사 중 공격 전진 표 금지(AttackRootMotion.Gate)
2026-10-21 04:56:16 +00:00
bool prevEvents = an . fireEvents ; an . fireEvents = false ; float t0 = Time . time ; int i = - 1 , cur = 0 ; float interval = Mathf . Max ( 0.05f , c . slashInterval ) , tickAt = t0 ;
2026-10-21 05:29:56 +00:00
int hits = 0 , targets = 0 , scheduled = 0 ; var hitQ = new System . Collections . Generic . Queue < float > ( 4 ) ; var hitTimes = new System . Text . StringBuilder ( ) ; // #960 휘두름마다 판정 시각을 쌓는다(프레임 끊김으로 틱 둘이 한 프레임에 와도 빠지지 않게)
2026-10-21 01:28:52 +00:00
try
{
2026-10-21 08:40:26 +00:00
float sec = SlashSec ( c ) ;
if ( c . slashHitEven ! = 0 & & c . slashHits > 0 ) // #962 판정 시각을 미리 고르게
{
float span = Mathf . Max ( 0.05f , sec - Mathf . Max ( 0f , c . slashHitDelay ) ) ;
for ( int k = 0 ; k < c . slashHits ; k + + ) hitQ . Enqueue ( t0 + Mathf . Max ( 0f , c . slashHitDelay ) + k * span / c . slashHits ) ;
scheduled = c . slashHits ;
}
while ( Time . time - t0 < sec )
2026-10-21 01:28:52 +00:00
{
2026-10-21 04:56:16 +00:00
if ( DSUtil . CheckNull ( shooter ) | | shooter . IsDead ( ) ) break ; // CurAnim 조건은 뺀다(실측: 3회 만에 끊겨 원본 콤보 전이 Attack3→4(회전)·Jump 사슬이 이어졌다)
2026-10-21 07:56:27 +00:00
if ( c . dodgeCancel ! = 0 & & shooter . Get_CurAnim ( ) = = eAnim . Dodge ) { canceled = true ; break ; } // #961 구르기 = 캔슬(강제 재생보다 먼저 봐서 구르기 동작을 덮지 않는다)
2026-10-21 04:56:16 +00:00
if ( i < 0 | | Time . time > = tickAt )
{
i + + ; tickAt = t0 + ( i + 1 ) * interval ;
string st = c . slashStates [ i % c . slashStates . Length ] ; cur = Animator . StringToHash ( st ) ;
if ( an . HasState ( 0 , cur ) ) { if ( c . slashHardRestart ! = 0 ) an . Play ( cur , 0 , 0f ) ; else an . CrossFadeInFixedTime ( cur , 0.05f , 0 , 0f ) ; an . speed = Mathf . Max ( 0.1f , c . slashSpeed ) ; SlashLoops + + ; }
else SlashLog = "상태 없음 " + st ;
if ( c . fxPerSlash ! = 0 & & ! string . IsNullOrEmpty ( c . fxPrefab ) ) SlashFx ( shooter , c , i ) ; // #956 휘두르는 손의 날 자리에 작은 사본
2026-10-21 05:29:56 +00:00
if ( c . slashHits > 0 & & scheduled < c . slashHits ) { hitQ . Enqueue ( Time . time + Mathf . Max ( 0f , c . slashHitDelay ) ) ; scheduled + + ; } // #960 이 휘두름의 칼이 지나가는 순간
2026-10-21 04:56:16 +00:00
}
else if ( c . slashHardRestart ! = 0 )
{
// #959 Play 실측: 틱 사이에 다른 상태(원본 콤보 AI 의 Attack3→4 · 원본 Jump 사슬 · exit 전이)가 끼어들어 3타 회전·4타 돌진(1.4 m)이 섞였다 → 매 프레임 지키고 어긋나면 그 프레임에 되돌린다
bool foreign = an . GetCurrentAnimatorStateInfo ( 0 ) . shortNameHash ! = cur | | ( an . IsInTransition ( 0 ) & & an . GetNextAnimatorStateInfo ( 0 ) . shortNameHash ! = cur ) ;
if ( foreign ) { an . Play ( cur , 0 , 0f ) ; an . speed = Mathf . Max ( 0.1f , c . slashSpeed ) ; SlashLeaks + + ; }
}
2026-10-21 05:29:56 +00:00
while ( hitQ . Count > 0 & & Time . time > = hitQ . Peek ( ) ) // #960 판정(이번 프레임까지 온 것 모두)
{
hitQ . Dequeue ( ) ; hits + + ; SlashHits + + ; int n = SlashHit ( pb , shooter , c ) ; targets + = n ; SlashHitTargets + = n ;
hitTimes . Append ( ( Time . time - t0 ) . ToString ( "F2" ) ) . Append ( n > 0 ? "" : "(빗나감 " + LastMissInfo + ")" ) . Append ( ' ' ) ;
}
2026-10-21 04:56:16 +00:00
yield return null ;
2026-10-21 01:28:52 +00:00
}
2026-10-21 07:56:27 +00:00
if ( canceled ) // #961 구르기 캔슬 — 반복 · 잔상 멈춤 + 살아 있는 이펙트 지움 · 이동 잠금 풀기 · 남은 판정 없음
{
s_cancelSeq = seq ; DodgeCancels + + ;
for ( int k = 0 ; k < s_liveFx . Count ; k + + ) if ( s_liveFx [ k ] ! = null ) Object . Destroy ( s_liveFx [ k ] ) ;
s_liveFx . Clear ( ) ; hitQ . Clear ( ) ;
if ( shooter is PCActor cp ) WLSkillCancel . EndHoldMove ( cp , false ) ;
hitTimes . Append ( "(구르기 캔슬 " + ( Time . time - t0 ) . ToString ( "F2" ) + ")" ) ;
}
2026-10-21 05:29:56 +00:00
// 끝 — 남은 판정 + (끊김으로 휘두름이 모자랐으면) 모자란 만큼
2026-10-21 07:56:27 +00:00
if ( ! canceled & & c . slashHits > 0 & & ! DSUtil . CheckNull ( shooter ) & & ! shooter . IsDead ( ) )
2026-10-21 05:29:56 +00:00
while ( hits < c . slashHits & & ( hitQ . Count > 0 | | scheduled < c . slashHits ) )
{
if ( hitQ . Count > 0 ) hitQ . Dequeue ( ) ; else scheduled + + ;
hits + + ; SlashHits + + ; int n = SlashHit ( pb , shooter , c ) ; targets + = n ; SlashHitTargets + = n ; hitTimes . Append ( ( Time . time - t0 ) . ToString ( "F2" ) ) . Append ( "(끝) " ) ;
}
2026-10-21 01:28:52 +00:00
}
2026-10-21 07:56:27 +00:00
finally { if ( ReferenceEquals ( s_slashActor , shooter ) ) s_slashActor = null ; if ( ! DSUtil . CheckNull ( shooter ) & & shooter . m_animation ! = null ) { shooter . m_animation . fireEvents = prevEvents ; shooter . m_animation . speed = 1f ; } }
if ( ! canceled & & c . endIdle ! = 0 & & ! DSUtil . CheckNull ( shooter ) & & ! shooter . IsDead ( ) ) { shooter . Play_Idle ( ) ; EndIdles + + ; } // #961 모션 끝 = 이펙트 끝
SlashLog = "난사 " + ( i + 1 ) + "회 · " + ( Time . time - t0 ) . ToString ( "F2" ) + " s · 되돌림 " + SlashLeaks + ( canceled ? " · 구르기 캔슬" : "" ) ;
2026-10-21 05:29:56 +00:00
if ( c . slashHits > 0 ) SlashHitLog = "판정 " + hits + "회 · 맞힌 적 합 " + targets + " · 시각 " + hitTimes ;
2026-10-21 07:56:27 +00:00
if ( ! canceled & & c . endHoldSeconds > 0f & & ! DSUtil . CheckNull ( shooter ) & & ! shooter . IsDead ( ) ) Runner . StartCoroutine ( CoEndHold ( shooter , c . endHoldSeconds ) ) ; // #961
}
// #961 난사가 끝난 뒤 공격 금지 — 그동안 매 프레임 IsStop(자동 전투 공격 · 접근 멈춤 · 원본 2.1 s 콜백이 풀어도 다시 건다) · 하네스 공격 키는 IsHolding 을 본다
static int s_castSeq , s_cancelSeq = - 1 ; static readonly System . Collections . Generic . List < GameObject > s_liveFx = new System . Collections . Generic . List < GameObject > ( ) ;
public static int DodgeCancels , EndHolds , EndSkips , EndIdles , FxStops ; static Actor s_holdActor , s_castShooter , s_slashActor ; static float s_holdUntil ;
/// <summary>#961 이 PC 가 지금 주력 난사 중(moveHold 1) — AttackRootMotion.Gate 가 공격 전진 표를 막는다.</summary>
public static bool IsSlashing ( Actor a ) { return a ! = null & & ReferenceEquals ( a , s_slashActor ) ; }
/// <summary>#961 원본 Skill_ShadowSlash 의 2.1 s 끝 콜백 훅 — 이 시전이 구르기로 캔슬됐으면 true(끝 Play_Idle · 대상 뒤 순간이동 · 연기 · 카메라 돌리기를 건너뜀 · 이동 허용은 원본이 먼저 부름).</summary>
public static bool SkipEnd ( Actor shooter )
{
if ( Cfg . dodgeCancel = = 0 | | s_castSeq < = 0 | | s_cancelSeq ! = s_castSeq | | ! ReferenceEquals ( shooter , s_castShooter ) ) return false ;
EndSkips + + ; return true ;
}
2026-10-22 10:39:16 +00:00
public static int EndWarps , EndWarpSkips ;
/// <summary>#970 원본 Skill_ShadowSlash 끝 콜백 훅 — false = 대상 뒤 순간이동 · 그때의 카메라 돌리기를 건너뜀(endWarp 0).</summary>
public static bool EndWarp ( )
{
var c = Cfg ;
if ( c . enabled = = 0 | | c . endWarp ! = 0 ) { EndWarps + + ; return true ; }
EndWarpSkips + + ; return false ;
}
2026-10-21 07:56:27 +00:00
static PropertyInfo s_isStop ;
public static bool IsHolding ( Actor a ) { return a ! = null & & ReferenceEquals ( a , s_holdActor ) & & Time . time < s_holdUntil ; }
static void SetStop ( Actor a , bool v )
{
try { if ( s_isStop = = null ) s_isStop = typeof ( Actor ) . GetProperty ( "IsStop" , BindingFlags . Instance | BindingFlags . NonPublic | BindingFlags . Public ) ; if ( s_isStop ! = null & & s_isStop . CanWrite ) s_isStop . SetValue ( a , v , null ) ; } catch ( System . Exception ) { }
}
static System . Collections . IEnumerator CoEndHold ( Actor a , float sec )
{
s_holdActor = a ; s_holdUntil = Time . time + sec ; EndHolds + + ;
while ( Time . time < s_holdUntil )
{
if ( DSUtil . CheckNull ( a ) | | a . IsDead ( ) | | ! ReferenceEquals ( s_holdActor , a ) ) yield break ;
if ( a . Get_CurAnim ( ) = = eAnim . Dodge ) break ; // 구르기 = 대기 끝
SetStop ( a , true ) ; yield return null ;
}
if ( ReferenceEquals ( s_holdActor , a ) ) { s_holdActor = null ; if ( ! DSUtil . CheckNull ( a ) & & ! a . IsDead ( ) ) SetStop ( a , false ) ; }
2026-10-21 01:28:52 +00:00
}
2026-10-21 01:54:38 +00:00
public static int FxSlashes ; public static string FxSlashLog = "" ;
static readonly System . Collections . Generic . List < Transform > s_sock = new System . Collections . Generic . List < Transform > ( 2 ) ;
static readonly System . Collections . Generic . List < Vector3 > s_tip = new System . Collections . Generic . List < Vector3 > ( 2 ) ;
/// <summary>#956 난사 1회마다 fxPrefab 의 작은 사본을 무기 날 자리에 — 소켓은 무기 메시가 달린 것만(비수 = w_bladedancer_l · _r · WLImpactRunner.SocketTipOf) · i 번째 난사는 소켓을 번갈아 · 추종은 WLShadowSlashFxFollow.</summary>
static void SlashFx ( Actor shooter , WLShadowSlashJson c , int i )
{
var pc = shooter as PCActor ; if ( pc = = null | | pc . tfs_weapon = = null ) { FxSlashLog = "PC 아님" ; return ; }
if ( i = = 0 | | s_sock . Count = = 0 )
{
s_sock . Clear ( ) ; s_tip . Clear ( ) ;
foreach ( var s in pc . tfs_weapon ) { Vector3 tl ; if ( WL . Combat . Impact . WLImpactRunner . SocketTipOf ( s , out tl ) > 0.05f ) { s_sock . Add ( s ) ; s_tip . Add ( tl ) ; } }
if ( s_sock . Count = = 0 ) { FxSlashLog = "무기 소켓 없음" ; return ; }
}
int k = i % s_sock . Count ; var sock = s_sock [ k ] ; if ( sock = = null ) return ;
var p = Resources . Load < GameObject > ( c . fxPrefab ) ; if ( p = = null ) { FxSlashLog = "fx 프리팹 없음 " + c . fxPrefab ; return ; }
var go = Object . Instantiate ( p ) ; go . transform . localScale = p . transform . localScale * c . fxSlashScale ;
var f = go . AddComponent < WLShadowSlashFxFollow > ( ) ; f . Set ( pc , sock , s_tip [ k ] , c . fxSlashFraction , c . fxSlashYOffset , c . fxSlashFollow ! = 0 ) ; f . Apply ( ) ;
foreach ( var ps in go . GetComponentsInChildren < ParticleSystem > ( true ) ) { var m = ps . main ; m . scalingMode = ParticleSystemScalingMode . Hierarchy ; ps . Play ( true ) ; }
Object . Destroy ( go , Mathf . Max ( 0.1f , c . fxSlashLife ) ) ; FxSlashes + + ;
FxSlashLog = "난사 fx " + FxSlashes + " · " + sock . name + " 날 " + s_tip [ k ] . magnitude . ToString ( "F2" ) + " m · @" + go . transform . position . ToString ( "F2" ) + " × " + c . fxSlashScale ;
}
2026-10-18 18:48:42 +00:00
/// <summary>Actor 의 protected 켜기 함수(Model_OnOff · Weapon_OnOff(bool))를 true 로 부른다 — 원본 Co_Invisible 끝과 같은 호출(가상 함수는 PCActor 쪽이 불린다).</summary>
static bool Call ( ref MethodInfo m , string name , Actor a )
{
if ( m = = null ) m = typeof ( Actor ) . GetMethod ( name , BindingFlags . Instance | BindingFlags . NonPublic | BindingFlags . Public , null , new [ ] { typeof ( bool ) } , null ) ;
if ( m = = null ) { LastLog = "함수 없음 " + name ; return false ; }
m . Invoke ( a , new object [ ] { true } ) ;
return true ;
}
/// <summary>Transform 을 처음 크기 × k 로 · 파티클 크기 모드 Hierarchy · 판정 상자는 keepBox 면 1/k 로(월드 크기 유지).</summary>
static void Scale ( Transform t , float k , bool keepBox )
{
if ( t = = null | | k < = 0f ) return ;
var b = t . GetComponent < WLShadowSlashBase > ( ) ;
if ( b = = null )
{
if ( Mathf . Approximately ( k , 1f ) ) return ; // 손댄 적 없고 배율 1 = 원본 그대로
b = t . gameObject . AddComponent < WLShadowSlashBase > ( ) ;
b . scale = t . localScale ;
var bc0 = t . GetComponent < BoxCollider > ( ) ;
if ( bc0 ! = null ) { b . hasBox = true ; b . size = bc0 . size ; b . center = bc0 . center ; }
foreach ( var ps in t . GetComponentsInChildren < ParticleSystem > ( true ) ) { var m = ps . main ; m . scalingMode = ParticleSystemScalingMode . Hierarchy ; }
}
t . localScale = b . scale * k ;
if ( b . hasBox )
{
var bc = t . GetComponent < BoxCollider > ( ) ;
if ( bc ! = null ) { float inv = keepBox ? 1f / k : 1f ; bc . size = b . size * inv ; bc . center = b . center * inv ; }
}
Scaled + + ;
}
}
/// <summary>#943-⑧ 이 Transform(원본 스킬 풀 인스턴스)의 처음 크기 · 판정 상자 — 배율 누적 방지.</summary>
public sealed class WLShadowSlashBase : MonoBehaviour { public Vector3 scale , size , center ; public bool hasBox ; }
2026-10-20 16:54:40 +00:00
/// <summary>#954 코루틴 숙주(씬 밖 · 숨김).</summary>
public sealed class WLShadowSlashRunner : MonoBehaviour { }
2026-10-21 05:29:56 +00:00
/// <summary>#960 원본 베기 판정을 끈 표식 — 처음 Loop · LoopTime(풀 인스턴스에 남으므로 slashHits 0 으로 되돌릴 때 쓴다).</summary>
public sealed class WLShadowSlashHits : MonoBehaviour { public int loop0 ; public float loopTime0 ; }
2026-10-21 01:54:38 +00:00
/// <summary>#956 난사 사본 추종 — 자리 = 소켓(손잡이) → 날 끝 사이 fraction + 위 yOffset · 방향 = 날 방향의 수평 성분(없으면 PC 정면) · follow 면 매 프레임(LateUpdate · 애니메이션 평가 뒤) 다시 맞춘다.</summary>
public sealed class WLShadowSlashFxFollow : MonoBehaviour
{
Actor _pc ; Transform _sock ; Vector3 _tip ; float _frac , _yOff ; bool _follow ;
public void Set ( Actor pc , Transform sock , Vector3 tipLocal , float frac , float yOff , bool follow ) { _pc = pc ; _sock = sock ; _tip = tipLocal ; _frac = frac ; _yOff = yOff ; _follow = follow ; }
public void Apply ( )
{
if ( _sock = = null ) { _follow = false ; return ; }
Vector3 hilt = _sock . position , tip = _sock . TransformPoint ( _tip ) , dir = tip - hilt ; dir . y = 0f ;
if ( dir . sqrMagnitude < 1e-4f ) { dir = _pc ! = null ? _pc . transform . forward : Vector3 . forward ; dir . y = 0f ; if ( dir . sqrMagnitude < 1e-4f ) dir = Vector3 . forward ; }
transform . position = Vector3 . Lerp ( hilt , tip , _frac ) + Vector3 . up * _yOff ;
transform . rotation = Quaternion . LookRotation ( dir . normalized , Vector3 . up ) ;
}
void LateUpdate ( ) { if ( _follow ) Apply ( ) ; }
}
2026-10-18 18:48:42 +00:00
}