2026-10-18 15:09:05 +00:00
// ─────────────────────────────────────────────────────────────────────────────
// WLRapidShot.cs — 활 주력 「속사」(Skill_RapidShot) WL 방식 (#916 · 2026-10-17)
/ /
// PD 원문: 「활을 쏠 때 플레이어가 뒤로 밀리지 않게」 · 「활의 주력 스킬시전할 때 플레이어의 활이 빛나며 기가 모이는 연출을 추가
// (해머 각성기 참고하되, 해머보다 임펙트가 너무 강하지는 않도록)」 · 「과녁의 크기가 몬스터의 크기를 고려해 유동적이 되도록 ·
// (이펙트가 몬스터에게 가려지지 않도록)」 · 「매우 빠르게 활쏘는 모션을 반복해 3발의 투사체를 연속으로 발사」
// ■ 원본(Skill_RapidShot.Co_Update): 과녁(Effect_BulletTarget · 대상 몸 가운데 · 크기 고정) → 1 초 → 0.25 초 간격 7 발 · 발마다 시전자 넉백
// (원본 넉백 최소 0.1 초 × 속도 4 = 한 발 약 0.4 m 뒤로 · Play 실측).
// ■ WL — 원본 Set 의 훅 1줄(Take)이 넘겨받는다:
// ① 과녁 = 원본 이펙트 그대로 · 대상 중심에서 카메라 쪽으로 몸 반지름 + markFront 앞에(몸에 가려지지 않게) · 배율 = 몹 키 × markPerHeight(markMin~markMax)
// ② 모으기 chargeSeconds — 활 당긴 자세(shotState 의 drawNormalized)로 멈추고, 활에 흰 빛(WLWeaponGlowFx · glowMax) + 활 가운데 모으기 이펙트(chargeFx)
// ③ shots 발 — 발마다 shotState 를 (첫 발 drawNormalized · 다음 fromNormalized) 부터 shotSpeed 배로 재생 → 놓는 순간(클립 Projectile 이벤트)에
// 원본 투사체 1 발(관통 · 원본과 같은 한 발 피해) · 대상을 본다 · 시전자 넉백 없음
// ④ 끝 — endNormalized 까지 재생 → 애니메이터 되돌림 · 대기 · 빛 glowFadeSeconds 동안 꺼짐 · 투사체 Off. 이동 · 구르기로 스킬 상태가 끊기면 즉시 끝(스킵).
// ■ noRecoil = 쏠 때 시전자를 뒤로 미는 다른 활 스킬(원본 Set 훅 1줄 · 속박의 화살 Skill_BindingArrow 0.05 초 × 속도 10)
2026-10-23 15:51:48 +00:00
// ■ #975 multi = 활 기본 스킬(원본 Skill_MultiShot 703404 = 일반 공격 화살 추가 버프) — PD 「주력 스킬과 동일하게 모으기 이펙트 시전 후 전방으로 부채꼴 모양으로
// 퍼지는 화살 10발을 3회 연속으로 발사(멀티샷 형태 · 0.5초 간격)」 — 원본 Set 훅 1줄(TakeMulti)이 넘겨받아 ②모으기는 주력과 같고 ③에서 volleys 번 ·
// interval 초마다(놓는 순간 기준) 대상 쪽(없으면 정면) 부채꼴 fanDegrees 에 arrows 발(화살 = multi.projectile · 겉모습 WLProjectileVisual · 사거리 = 흰 원 × rangeMul)
2026-10-18 15:09:05 +00:00
// ■ 값 = Resources/WL/WLRapidShot.json (C45) · 끄기 = enabled 0 → 원본 동작
// 🔴 Debug.LogError 금지(원본 팝업).
// ─────────────────────────────────────────────────────────────────────────────
using System.Collections ;
using System.Collections.Generic ;
using UnityEngine ;
namespace WL.Combat.Weapon
{
[System.Serializable]
public class WLRapidShotJson
{
public int enabled = 0 ;
public int [ ] classes = new int [ 0 ] ; // 비우면 모든 직업
public int shots = 3 ;
public string shotState = "attack" ;
public string shotClip = "FR_Bow1_10402" ; // 놓는 순간 = 이 클립의 Projectile 이벤트
public float drawNormalized = 0.8f ; // 모으기 동안 멈출 자세(활 당김) · 첫 발 시작
2026-10-21 16:30:22 +00:00
// #967 PD 「주력 스킬 모으기 시 활을 들지 않는 손이 멈춰 있는데 — 활을 쏠 때와 같이 자연스럽게 활시위를 당기는 포즈로 모으도록」 — chargeFromNormalized ≥ 0 이면 모으기 첫 chargeDrawSeconds 동안
// shotState 를 그 자리(활 든 채 시위에 손)부터 drawNormalized(끝까지 당김)까지 재생한 뒤 유지 · < 0 = 이전(처음부터 drawNormalized 정지)
public float chargeFromNormalized = - 1f ;
public float chargeDrawSeconds = 0.35f ;
public float releaseNormalized ; // #967 > 0 = 놓는 순간(발사)을 이 정규화 시각으로 · 0 = shotClip 의 Projectile 이벤트
2026-10-18 15:09:05 +00:00
public float fromNormalized = 0.55f ; // 둘째 발부터 여기서 재생
public float shotSpeed = 2.5f ; // 발 재생 배속
public float endNormalized = 0.97f ; // 마지막 발 뒤 여기까지 재생하고 대기
public float chargeSeconds = 0.6f ;
public string socket = "w_archer_l" ; // 활 소켓
public string chargeFx = "" ;
public float chargeFxScale = 0.06f ;
public string glowMaterial = "WL/Fx/WL_Mat_WeaponGlow" ;
public float [ ] glowColor = { 1.2f , 1.3f , 1.5f } ;
public float glowMax = 0.6f , glowFill = 0.3f , glowRimPower = 2.5f , glowFadeSeconds = 0.2f ;
public float markFront = 0.15f ; // 과녁을 몸 반지름 + 이 거리(m)만큼 카메라 쪽 앞에
public float markPerHeight = 0.35f ; // 과녁 배율 = 몹 키(m) × 이 값
public float markMin = 0.3f , markMax = 2f ;
public float markExtraLife = 0.3f ; // 마지막 발 뒤 과녁 유지(초)
public string [ ] noRecoil = new string [ 0 ] ; // 쏠 때 시전자 넉백을 하지 않을 다른 스킬(eEffect 이름)
// #921 PD 「이펙트 궤적이 타겟 방향으로만 집중해서 날아가도록 · 그 방향으로 화살 기본 공격의 사정거리만큼 직선상 경로상 모든 적에게 피해(레이저 판정)」
public int laser = 0 ; // 1 = 발마다 PC → 대상 방향 직선(길이 = 기본 공격 사정거리 · 흰 원) 위 모든 적에게 그 화살의 한 발 피해를 즉시 · 화살은 판정 없이 사정거리까지만 곧게 날아간다
public float laserWidth = 0.6f ; // 직선 판정 폭(m) — 적 몸 반지름은 따로 더한다
public int aimBody = 1 ; // 1 = 화살이 대상 몸 중심(공중 몹은 그림 가운데) 쪽으로 곧게
public int chargeBlocksMove = 0 ; // #922 PD 「주력 스킬 모으기 이펙트 재생중에는 이동하지 못하게(구르기로 취소는 가능)」 — 1 = 모으기 동안 이동 입력 무시(WLSkillCancel.HoldMove · 구르기 = 스킵)
public int holdThroughShots = 0 ; // #922 1 = 막기를 마지막 발까지(모으기만 막으면 이동 키를 누른 채일 때 모으기 직후 이동이 들어와 한 발도 안 쏘고 끊겼다 · Play 실측)
public float holdMaxSeconds = 3f ; // 막기 상한(초) — 끝나면 어떻게든 푼다
2026-10-23 15:51:48 +00:00
public WLRapidShotMulti multi = new WLRapidShotMulti ( ) ; // #975 활 기본 스킬(멀티샷)
public string note = "" ;
}
/// <summary>#975 활 기본 스킬 — 주력과 같은 모으기 뒤 volleys 번 · interval 초 간격(놓는 순간)으로 앞쪽 부채꼴 fanDegrees 에 화살 arrows 발.</summary>
[System.Serializable]
public class WLRapidShotMulti
{
public int enabled = 0 ;
public string projectile = "Projectile_ArcherSkill2Arrow" ; // 화살(주력 속사와 같은 화살 · 겉모습 = WLProjectileVisual 표)
public int volleys = 3 ;
public float interval = 0.5f ;
public int arrows = 10 ;
public float fanDegrees = 60f ; // 부채꼴 전체 각(°) — 화살 사이 = fanDegrees / (arrows − 1)
public float rangeMul = 1f ; // 나는 거리 = 기본 공격 사정거리(흰 원) × 이 값 · 0 = 투사체 원래 수명
public int pierce = 0 ; // 1 = 관통
public int mark = 0 ; // 1 = 과녁 표시(주력처럼)
2026-10-23 16:28:09 +00:00
public float drawFraction = 0.8f ; // #976 다시 당기는 시간 상한 = interval × 이 값(간격이 당김 동작 0.23 s 보다 짧으면 그만큼 빨리 당긴다)
2026-10-24 11:40:41 +00:00
public int noExtra = 1 ; // #977 1 = 화살에 원본 스킬 부가 효과(703404 방어력 감소 디버프) 없음
2026-10-18 15:09:05 +00:00
public string note = "" ;
}
public static class WLRapidShot
{
public const string ResourcesPath = "WL/WLRapidShot" ;
2026-10-23 15:51:48 +00:00
public static int Taken , Shots , Aborted , RecoilSkipped , Errors , Lasers , LaserHits , TakenMulti , MultiArrows ; public static string LastLog = "" ;
2026-10-18 15:09:05 +00:00
static WLRapidShotJson s_cfg ;
internal static WLRapidShotJson Cfg
{
get
{
if ( s_cfg ! = null ) return s_cfg ;
s_cfg = new WLRapidShotJson ( ) ;
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.SubsystemRegistration)] static void Reset ( ) { s_cfg = null ; }
static int ClassOf ( PCActor pc ) { return pc ! = null ? pc . Get_ID ( ) : 0 ; }
/// <summary>원본 Skill_RapidShot.Set 훅 — true 면 WL 이 이 투사체를 맡는다(원본 Co_Update 건너뜀).</summary>
public static bool Take ( ProjectileBase pb , List < ProjectileData > shots , string projectile )
{
try
{
var c = Cfg ;
if ( c . enabled = = 0 | | pb = = null | | shots = = null | | shots . Count = = 0 ) return false ;
var data = pb . Get_ProjectTileData ( ) ; var pc = data ! = null ? data . shooter as PCActor : null ;
if ( pc = = null | | pc . m_animation = = null ) return false ;
if ( c . classes ! = null & & c . classes . Length > 0 & & System . Array . IndexOf ( c . classes , ClassOf ( pc ) ) < 0 ) return false ;
var run = pb . GetComponent < WLRapidShotRunner > ( ) ;
if ( run = = null ) run = pb . gameObject . AddComponent < WLRapidShotRunner > ( ) ;
run . Begin ( pb , pc , data . target , shots , projectile , c ) ;
Taken + + ;
return true ;
}
catch ( System . Exception ex ) { Errors + + ; LastLog = "넘겨받기 실패 — " + ex . Message ; return false ; }
}
2026-10-23 15:51:48 +00:00
/// <summary>#975 원본 Skill_MultiShot.Set 훅 — true 면 WL 이 맡는다(원본 버프 건너뜀). 화살마다 스킬 투사체 데이터 복사본(시작 = 시전자 가운데).</summary>
public static bool TakeMulti ( ProjectileBase pb , ProjectileData data )
{
try
{
var c = Cfg ; var m = c . multi ;
if ( c . enabled = = 0 | | m = = null | | m . enabled = = 0 | | pb = = null | | data = = null ) return false ;
var pc = data . shooter as PCActor ;
if ( pc = = null | | pc . m_animation = = null ) return false ;
if ( c . classes ! = null & & c . classes . Length > 0 & & System . Array . IndexOf ( c . classes , ClassOf ( pc ) ) < 0 ) return false ;
int n = Mathf . Max ( 1 , m . volleys ) * Mathf . Max ( 1 , m . arrows ) ;
var list = new List < ProjectileData > ( n ) ;
2026-10-24 11:40:41 +00:00
for ( int i = 0 ; i < n ; i + + )
{
var d = new ProjectileData ( ) ; data . Copy ( d ) ; d . eStartPos = eProjectileStartPos . ShooterCenter ;
if ( m . noExtra ! = 0 ) { d . e_SkillExtraType = eSkillExtraType . None ; d . m_skillTypeConfigTable = null ; d . SetSkillExtraType = false ; } // #977 PD 「활 기본 스킬에 명중 된 적에게 발생하는 디버프를 제거」 — Actor.Check_Extra 가 None 이면 바로 돌아간다 · SetSkillExtraType 를 꺼야 ProjectileBase.Set 이 스킬 표 값으로 다시 넣지 않는다(1차 Play 실측 방어 감소 남음)
list . Add ( d ) ;
}
2026-10-23 15:51:48 +00:00
var target = data . target ;
if ( DSUtil . CheckNull ( target ) | | target . IsDead ( ) ) target = pc . Get_Target ( ) ;
var run = pb . GetComponent < WLRapidShotRunner > ( ) ;
if ( run = = null ) run = pb . gameObject . AddComponent < WLRapidShotRunner > ( ) ;
run . BeginMulti ( pb , pc , target , list , c ) ;
TakenMulti + + ;
return true ;
}
catch ( System . Exception ex ) { Errors + + ; LastLog = "멀티샷 넘겨받기 실패 — " + ex . Message ; return false ; }
}
2026-10-18 15:09:05 +00:00
/// <summary>원본 활 스킬 Set 훅 — true 면 시전자 넉백(쏠 때 뒤로 밀림)을 하지 않는다.</summary>
public static bool NoRecoil ( Actor shooter , string skill )
{
var c = Cfg ;
if ( c . enabled = = 0 | | c . noRecoil = = null | | System . Array . IndexOf ( c . noRecoil , skill ) < 0 | | ! ( shooter is PCActor ) ) return false ;
RecoilSkipped + + ;
return true ;
}
}
/// <summary>넘겨받은 속사 투사체 1개의 진행 — 과녁 · 모으기 · 빠른 연사 · 끝.</summary>
internal sealed class WLRapidShotRunner : MonoBehaviour
{
ProjectileBase _pb ; PCActor _pc ; Actor _target ; List < ProjectileData > _shots ; string _proj ; WLRapidShotJson _c ;
Animator _an ; bool _animOwned ; Coroutine _co ;
readonly WLWeaponGlowFx _glow = new WLWeaponGlowFx ( ) ;
Transform _socket ; Vector3 _centerLocal ; GameObject _charge ;
static readonly Dictionary < string , GameObject > s_fx = new Dictionary < string , GameObject > ( ) ;
2026-10-23 15:51:48 +00:00
bool _multi ; // #975 활 기본 스킬(부채꼴 다발)
2026-10-18 15:09:05 +00:00
public void Begin ( ProjectileBase pb , PCActor pc , Actor target , List < ProjectileData > shots , string proj , WLRapidShotJson c )
{
2026-10-23 15:51:48 +00:00
_pb = pb ; _pc = pc ; _target = target ; _shots = shots ; _proj = proj ; _c = c ; _an = pc . m_animation ; _multi = false ;
if ( _co ! = null ) StopCoroutine ( _co ) ;
_co = StartCoroutine ( Run ( ) ) ;
}
/// <summary>#975 활 기본 스킬 — shots = 화살마다 데이터(volleys × arrows).</summary>
public void BeginMulti ( ProjectileBase pb , PCActor pc , Actor target , List < ProjectileData > shots , WLRapidShotJson c )
{
_pb = pb ; _pc = pc ; _target = target ; _shots = shots ; _proj = c . multi . projectile ; _c = c ; _an = pc . m_animation ; _multi = true ;
2026-10-18 15:09:05 +00:00
if ( _co ! = null ) StopCoroutine ( _co ) ;
_co = StartCoroutine ( Run ( ) ) ;
}
IEnumerator Run ( )
{
2026-10-23 15:51:48 +00:00
var c = _c ; var an = _an ; var mc = c . multi ;
2026-10-21 16:30:22 +00:00
float releaseN = c . releaseNormalized > 0f ? c . releaseNormalized : ReleaseNormalized ( an , c . shotClip ) ;
2026-10-18 15:09:05 +00:00
if ( releaseN < = 0f ) { WLRapidShot . LastLog = "놓는 이벤트 없음 " + c . shotClip ; releaseN = 0.92f ; }
2026-10-23 15:51:48 +00:00
int n = _multi ? Mathf . Max ( 1 , mc . volleys ) : Mathf . Clamp ( c . shots , 1 , _shots . Count ) ;
2026-10-18 15:09:05 +00:00
2026-10-23 15:51:48 +00:00
// ① 과녁 — 몹 크기에 맞춘 배율 · 카메라 쪽 앞(몸에 가려지지 않게) · #975 기본 스킬(부채꼴)은 mark 1 일 때만
if ( ! _multi | | mc . mark ! = 0 ) ShowMark ( c . chargeSeconds + n * ( _multi ? mc . interval : 0.2f ) + c . markExtraLife ) ;
2026-10-18 15:09:05 +00:00
// ② 모으기 — 활 당긴 자세 · 활 흰 빛 · 모으기 이펙트
an . fireEvents = false ; _animOwned = true ;
2026-10-21 16:30:22 +00:00
bool drawing = c . chargeFromNormalized > = 0f & & c . chargeFromNormalized < c . drawNormalized ; // #967 시위를 당기며 모은다
float poseN = drawing ? c . chargeFromNormalized : c . drawNormalized ;
an . Play ( c . shotState , 0 , poseN ) ; an . speed = 0f ; an . Update ( 0f ) ;
2026-10-18 15:09:05 +00:00
Face ( ) ;
_socket = FindSocket ( _pc , c . socket ) ;
if ( _socket ! = null ) { _glow . Begin ( _socket , c . glowMaterial ) ; _centerLocal = WLWeaponGlowFx . CenterLocal ( _socket ) ; }
_charge = Spawn ( c . chargeFx , BowCenter ( ) , c . chargeFxScale , c . chargeSeconds + 0.5f ) ;
if ( c . chargeBlocksMove ! = 0 ) { WLSkillCancel . HoldMove ( _pc , c . holdThroughShots ! = 0 ? Mathf . Max ( c . chargeSeconds , c . holdMaxSeconds ) : c . chargeSeconds ) ; _holding = true ; } // #922 모으기(· 마지막 발까지) 이동 무시(구르기 = 스킵)
var col = Rgb ( c . glowColor , 1f ) ;
float t = 0f ;
while ( t < c . chargeSeconds )
{
if ( Broken ( ) ) { End ( false ) ; yield break ; }
if ( an . speed ! = 0f ) an . speed = 0f ;
2026-10-21 16:30:22 +00:00
if ( drawing ) // #967 시위 당기기 — chargeDrawSeconds 동안 부드럽게 끝까지 → 유지
{
float dk = Mathf . SmoothStep ( 0f , 1f , Mathf . Clamp01 ( t / Mathf . Max ( 0.01f , c . chargeDrawSeconds ) ) ) ;
float want = Mathf . Lerp ( c . chargeFromNormalized , c . drawNormalized , dk ) ;
if ( Mathf . Abs ( want - poseN ) > 1e-4f | | ! an . GetCurrentAnimatorStateInfo ( 0 ) . IsName ( c . shotState ) ) { poseN = want ; an . Play ( c . shotState , 0 , poseN ) ; }
}
else if ( ! an . GetCurrentAnimatorStateInfo ( 0 ) . IsName ( c . shotState ) ) an . Play ( c . shotState , 0 , c . drawNormalized ) ; // 다른 연출이 상태를 바꾸면 되돌림
2026-10-18 15:09:05 +00:00
float k = Mathf . Clamp01 ( t / Mathf . Max ( 0.01f , c . chargeSeconds ) ) ;
_glow . Set ( c . glowMax * k , col , c . glowFill , c . glowRimPower ) ;
if ( _charge ! = null ) _charge . transform . position = BowCenter ( ) ;
t + = Time . deltaTime ; yield return null ;
}
_glow . Set ( c . glowMax , col , c . glowFill , c . glowRimPower ) ;
if ( _charge ! = null ) { Destroy ( _charge ) ; _charge = null ; }
if ( _holding & & c . holdThroughShots = = 0 ) { _holding = false ; WLSkillCancel . EndHoldMove ( _pc , true ) ; } // #922 모으기 끝 — 그동안 미룬 이동 입력 적용(마지막 발까지 막을 때는 연사 뒤에)
2026-10-23 15:51:48 +00:00
// ③ 빠른 연사 n 발 · #975 기본 스킬 = n 번 부채꼴 다발(놓는 순간 사이 mc.interval 초)
float lastFire = - 99f ;
float shotSp = c . shotSpeed ; // 발 재생 속도(애니메이터 속도를 직접 씀 = 기본 공격 공속 배율과 무관 · #975 실측 활 컨트롤러 재생 배수 파라미터 없음)
float drawDur = Mathf . Max ( 0f , ( releaseN - c . fromNormalized ) * ClipLen ( an , c . shotClip ) / Mathf . Max ( 0.1f , c . shotSpeed ) ) ; // 다시 당겨 놓기까지(초 · 어림 — 실제 놓는 순간은 아래에서 interval 에 맞춤)
2026-10-23 16:28:09 +00:00
// #976 간격(0.2 s)이 당김 동작보다 짧으면 다시 당기기를 interval × drawFraction 안에 끝나게 빨리 재생
float mDraw = _multi & & mc . drawFraction > 0f ? Mathf . Min ( drawDur , mc . interval * mc . drawFraction ) : drawDur ;
float mSp = mDraw > 1e-3f & & drawDur > 1e-3f ? c . shotSpeed * drawDur / mDraw : c . shotSpeed ;
2026-10-18 15:09:05 +00:00
for ( int i = 0 ; i < n ; i + + )
{
2026-10-23 15:51:48 +00:00
if ( _multi & & i > 0 ) // #975 앞 다발을 놓은 뒤 자세를 이어 재생하다가 interval − 당기는 시간(− 몇 프레임 여유 · 첫 Play 실측 0.54 s = 2 프레임 늦음)에 다시 당긴다
2026-10-23 16:28:09 +00:00
while ( Time . time < lastFire + mc . interval - mDraw - 3f * Time . smoothDeltaTime )
2026-10-23 15:51:48 +00:00
{
if ( Broken ( ) ) { End ( false ) ; yield break ; }
an . speed = an . GetCurrentAnimatorStateInfo ( 0 ) . normalizedTime < c . endNormalized ? shotSp : 0f ; // 다음 상태로 넘어가지 않게(공격 2타 전이 0.98)
yield return null ;
}
2026-10-18 15:09:05 +00:00
Face ( ) ;
float startN = i = = 0 ? c . drawNormalized : c . fromNormalized , s0 = Time . time ;
2026-10-23 16:28:09 +00:00
float spNow = _multi & & i > 0 ? mSp : shotSp ; // #976 멀티샷 다시 당기기 = mSp
an . Play ( c . shotState , 0 , startN ) ; an . speed = Mathf . Max ( 0.1f , spNow ) ;
2026-10-18 15:09:05 +00:00
yield return null ;
while ( an . GetCurrentAnimatorStateInfo ( 0 ) . normalizedTime < releaseN & & Time . time - s0 < 1f )
{
if ( Broken ( ) ) { End ( false ) ; yield break ; }
2026-10-23 16:28:09 +00:00
if ( an . speed ! = spNow ) an . speed = spNow ;
2026-10-18 15:09:05 +00:00
if ( ! an . GetCurrentAnimatorStateInfo ( 0 ) . IsName ( c . shotState ) ) an . Play ( c . shotState , 0 , startN ) ;
yield return null ;
}
2026-10-23 15:51:48 +00:00
if ( _multi )
{
while ( i > 0 & & Time . time < lastFire + mc . interval ) { if ( Broken ( ) ) { End ( false ) ; yield break ; } an . speed = 0f ; yield return null ; } // 놓는 순간을 interval 에 맞춤
an . speed = shotSp ;
Fan ( i , Mathf . Max ( 1 , mc . arrows ) ) ;
}
else
{
var data = _shots [ i ] ;
ProjectileInfo . Ins . Shoot_Projectile ( _proj , data , p = > { p . m_Pierce = true ; WLProjectileVisual . ApplyTo ( p , _proj ) ; if ( _c . laser ! = 0 ) Laser ( p ) ; } ) ; // 원본과 같은 한 발 · 시전자 넉백 없음 · #917 겉모습 · 명중 이펙트(WLProjectileVisual 표) · #921 레이저 판정
}
lastFire = Time . time ;
2026-10-18 15:09:05 +00:00
WLRapidShot . Shots + + ;
}
if ( _holding ) { _holding = false ; WLSkillCancel . EndHoldMove ( _pc , true ) ; } // #922 마지막 발 — 미룬 이동 입력 적용
// ④ 마무리 — 빛 꺼짐 · 끝 자세
float f0 = Time . time ;
while ( an . GetCurrentAnimatorStateInfo ( 0 ) . normalizedTime < c . endNormalized & & Time . time - f0 < 0.5f )
{
if ( Broken ( ) ) { End ( false ) ; yield break ; }
float k = 1f - Mathf . Clamp01 ( ( Time . time - f0 ) / Mathf . Max ( 0.01f , c . glowFadeSeconds ) ) ;
_glow . Set ( c . glowMax * k , col , c . glowFill , c . glowRimPower ) ;
yield return null ;
}
End ( true ) ;
}
/// <summary>#921 레이저 판정 — PC 발에서 대상 쪽 수평 방향으로 기본 공격 사정거리(흰 원 WLRangeRing.AttackReach)만큼의 직선 위(폭 laserWidth + 몸 반지름) 모든 적에게
/// 이 화살의 원본 피해 경로(ProcessCollider · 끄지 않음)를 즉시 · 화살은 대상 몸 중심 쪽으로 곧게 사정거리까지만 날고 자기 판정은 끈다(이중 피해 방지).</summary>
void Laser ( ProjectileBase p )
{
var c = _c ; if ( p = = null | | _pc = = null ) return ;
float range = WL . Combat . RangeRing . WLRangeRing . AttackReach ( _pc ) ;
if ( range < = 0f ) return ;
Vector3 origin = _pc . Get_position ( ) ;
Vector3 dir = _pc . transform . forward ; dir . y = 0f ;
bool hasTarget = ! DSUtil . CheckNull ( _target ) & & ! _target . IsDead ( ) ;
if ( hasTarget ) { var to = _target . Get_position ( ) - origin ; to . y = 0f ; if ( to . sqrMagnitude > 1e-4f ) dir = to ; }
if ( dir . sqrMagnitude < 1e-6f ) return ;
dir . Normalize ( ) ;
// ① 겉모습 — 대상 몸 중심 쪽으로 곧게 · 사정거리 끝에서 사라짐
Vector3 look = dir ;
if ( c . aimBody ! = 0 & & hasTarget )
{
var v = WL . Combat . Impact . WLImpactFx . BodyCenter ( _target ) - p . transform . position ; var vf = v ; vf . y = 0f ;
if ( vf . sqrMagnitude > 0.01f ) look = v ;
}
p . transform . rotation = Quaternion . LookRotation ( look ) ;
var pd = p . Get_ProjectTileData ( ) ;
var start = p . transform . position - origin ; start . y = 0f ;
if ( pd ! = null & & pd . Speed > 0f ) pd . LifeTime = Mathf . Max ( 0.05f , ( range - Vector3 . Dot ( start , dir ) ) / pd . Speed ) ;
// ② 판정 — 직선 위 모든 적
int hits = 0 ; var info = ActorInfo . Ins ;
var list = info ! = null ? info . Get_EnemyActors ( _pc . IsEnemy ( ) , origin , range + 2f ) : null ;
if ( list ! = null )
{
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 ;
float body = WL . Combat . WLCombatMotionSettings . BodyRadius ( a ) , t = Vector3 . Dot ( v , dir ) ;
if ( t < - body | | t > range + body ) continue ;
if ( ( v - dir * t ) . magnitude > c . laserWidth * 0.5f + body ) continue ;
var col = a . GetComponent < Collider > ( ) ;
if ( col = = null | | ! col . enabled ) continue ;
if ( p . ProcessCollider ( col , false ) ) hits + + ;
}
info . Release_Actors ( list ) ;
}
WLLaserMute . Mute ( p ) ;
WLRapidShot . LaserHits + = hits ; WLRapidShot . Lasers + + ;
WLRapidShot . LastLog = "레이저 " + range . ToString ( "F2" ) + " m · 맞음 " + hits ;
}
2026-10-23 15:51:48 +00:00
/// <summary>#975 부채꼴 다발 1번 — 대상 쪽(없으면 정면) 수평 방향을 가운데로 fanDegrees 에 count 발을 고르게 · 화살은 그 방향으로 곧게 · 사거리 = 흰 원 × rangeMul.</summary>
void Fan ( int volley , int count )
{
var mc = _c . multi ; var pc = _pc ; if ( pc = = null | | ProjectileInfo . Ins = = null ) return ;
Vector3 dir = pc . transform . forward ; dir . y = 0f ;
if ( ! DSUtil . CheckNull ( _target ) & & ! _target . IsDead ( ) ) { var to = _target . Get_position ( ) - pc . Get_position ( ) ; to . y = 0f ; if ( to . sqrMagnitude > 1e-4f ) dir = to ; }
if ( dir . sqrMagnitude < 1e-6f ) dir = Vector3 . forward ;
dir . Normalize ( ) ;
float range = mc . rangeMul > 0f ? WL . Combat . RangeRing . WLRangeRing . AttackReach ( pc ) * mc . rangeMul : 0f ;
float step = count > 1 ? mc . fanDegrees / ( count - 1 ) : 0f , a0 = count > 1 ? - mc . fanDegrees * 0.5f : 0f ;
for ( int k = 0 ; k < count ; k + + )
{
int idx = volley * count + k ; if ( idx > = _shots . Count ) break ;
var look = Quaternion . AngleAxis ( a0 + step * k , Vector3 . up ) * dir ;
ProjectileInfo . Ins . Shoot_Projectile ( _proj , _shots [ idx ] , p = >
{
p . m_Pierce = mc . pierce ! = 0 ; WLProjectileVisual . ApplyTo ( p , _proj ) ;
p . transform . rotation = Quaternion . LookRotation ( look ) ;
var pd = p . Get_ProjectTileData ( ) ;
if ( range > 0f & & pd ! = null & & pd . Speed > 0f ) pd . LifeTime = Mathf . Max ( 0.05f , range / pd . Speed ) ;
} ) ;
WLRapidShot . MultiArrows + + ;
}
WLRapidShot . LastLog = "멀티샷 " + ( volley + 1 ) + "번째 · " + count + "발 · 부채 " + mc . fanDegrees . ToString ( "0.#" ) + "° · 사거리 " + range . ToString ( "F1" ) + " m" ;
}
static float ClipLen ( Animator an , string clipName )
{
if ( an ! = null & & an . runtimeAnimatorController ! = null )
foreach ( var clip in an . runtimeAnimatorController . animationClips ) if ( clip ! = null & & clip . name = = clipName & & clip . length > 0f ) return clip . length ;
return 1f ;
}
2026-10-18 15:09:05 +00:00
bool Broken ( ) { return _pc = = null | | _pc . IsDead ( ) | | _pc . Get_CurAnim ( ) ! = eAnim . Skill ; }
bool _holding ;
void End ( bool toIdle )
{
if ( _holding ) { _holding = false ; WLSkillCancel . EndHoldMove ( _pc , toIdle ) ; }
if ( ! toIdle ) { WLRapidShot . Aborted + + ; WLRapidShot . LastLog = "끊김(이동 · 구르기)" ; }
if ( _animOwned & & _an ! = null ) { _an . speed = 1f ; _an . fireEvents = true ; }
_animOwned = false ; _co = null ;
if ( toIdle & & _pc ! = null & & ! _pc . IsDead ( ) & & _pc . Get_CurAnim ( ) = = eAnim . Skill ) _pc . Play_Idle ( ) ;
_glow . End ( ) ;
if ( _charge ! = null ) { Destroy ( _charge ) ; _charge = null ; }
WLSlashFlurry . Off ( _pb ) ;
}
void OnDisable ( )
{
if ( _holding ) { _holding = false ; WLSkillCancel . EndHoldMove ( _pc , false ) ; }
if ( _co ! = null ) { StopCoroutine ( _co ) ; _co = null ; }
if ( _animOwned & & _an ! = null ) { _an . speed = 1f ; _an . fireEvents = true ; _animOwned = false ; }
_glow . End ( ) ;
if ( _charge ! = null ) { Destroy ( _charge ) ; _charge = null ; }
}
void Face ( )
{
if ( _pc = = null | | DSUtil . CheckNull ( _target ) | | _target . IsDead ( ) ) return ;
var to = _target . Get_position ( ) - _pc . Get_position ( ) ; to . y = 0f ;
if ( to . sqrMagnitude > 1e-4f ) _pc . transform . rotation = Quaternion . LookRotation ( to ) ;
}
Vector3 BowCenter ( )
{
if ( _socket ! = null ) return _socket . TransformPoint ( _centerLocal ) ;
return _pc ! = null ? _pc . Get_Center_position ( ) : transform . position ;
}
void ShowMark ( float life )
{
if ( DSUtil . CheckNull ( _target ) | | InGameInfo . Ins = = null ) return ;
var c = _c ; var target = _target ;
float h = Mathf . Max ( 0.1f , target . Get_Top_postion ( ) . y - target . Get_position ( ) . y ) ;
float scale = Mathf . Clamp ( h * c . markPerHeight , c . markMin , c . markMax ) ;
InGameInfo . Ins . Show_Effect ( "Effect_BulletTarget" , target , eEffectLocation . Mid , life , off = >
{
if ( off = = null ) return ;
var fit = off . GetComponent < WLMarkFit > ( ) ; if ( fit = = null ) fit = off . gameObject . AddComponent < WLMarkFit > ( ) ;
fit . Begin ( target , c . markFront , scale , life ) ;
} ) ;
}
static Transform FindSocket ( PCActor pc , string name )
{
if ( pc = = null | | pc . tfs_weapon = = null ) return null ;
foreach ( var s in pc . tfs_weapon ) if ( s ! = null & & s . name = = name & & s . gameObject . activeInHierarchy ) return s ;
return null ;
}
static float ReleaseNormalized ( Animator an , string clipName )
{
if ( an = = null | | an . runtimeAnimatorController = = null ) return - 1f ;
foreach ( var clip in an . runtimeAnimatorController . animationClips )
{
if ( clip = = null | | clip . name ! = clipName | | clip . length < = 0f ) continue ;
foreach ( var ev in clip . events ) if ( ev . functionName = = "Projectile" ) return ev . time / clip . length ;
}
return - 1f ;
}
static GameObject Spawn ( string path , Vector3 pos , float scale , float life )
{
if ( string . IsNullOrEmpty ( path ) ) return null ;
GameObject prefab ;
if ( ! s_fx . TryGetValue ( path , out prefab ) ) { prefab = Resources . Load < GameObject > ( path ) ; s_fx [ path ] = prefab ; }
if ( prefab = = null ) { WLRapidShot . LastLog = "이펙트 없음 " + path ; return null ; }
var go = Instantiate ( prefab , pos , prefab . transform . rotation ) ;
go . transform . localScale = prefab . transform . localScale * Mathf . Max ( 0.01f , scale ) ;
Destroy ( go , Mathf . Max ( 0.1f , life ) ) ;
return go ;
}
static Color Rgb ( float [ ] a , float d ) { return new Color ( a ! = null & & a . Length > 0 ? a [ 0 ] : d , a ! = null & & a . Length > 1 ? a [ 1 ] : d , a ! = null & & a . Length > 2 ? a [ 2 ] : d , 1f ) ; }
}
/// <summary>#916 과녁 맞춤 — 원본 TurnOff_GO 가 Update 에서 대상 가운데로 옮긴 뒤(LateUpdate) 카메라 쪽 앞으로 당기고 몹 크기 배율을 준다. 기간이 지나면 원래 배율.</summary>
public sealed class WLMarkFit : MonoBehaviour
{
Actor _target ; float _front , _scale = 1f , _until ; bool _on ; Vector3 _scale0 = Vector3 . one ; bool _hasScale0 ;
public void Begin ( Actor target , float front , float scale , float life )
{
if ( ! _hasScale0 ) { _scale0 = transform . localScale ; _hasScale0 = true ; }
_target = target ; _front = front ; _scale = scale ; _until = Time . time + life ; _on = true ;
foreach ( var ps in GetComponentsInChildren < ParticleSystem > ( true ) ) { var m = ps . main ; m . scalingMode = ParticleSystemScalingMode . Hierarchy ; }
Apply ( ) ;
}
void LateUpdate ( ) { if ( _on ) Apply ( ) ; }
void Apply ( )
{
if ( Time . time > _until | | DSUtil . CheckNull ( _target ) | | _target . IsDead ( ) ) { Stop ( ) ; return ; }
var mid = WL . Combat . Impact . WLImpactFx . BodyCenter ( _target ) ; // #921 PD 「몸 정중앙 · 공중 몬스터라도」 — 원본 Center_position(등장 때 잰 고정 높이) 대신 지금 몸 중심(떠 있는 몹 = 지금 그림 가운데)
var cam = Camera . main ;
var toCam = cam ! = null ? ( cam . transform . position - mid ) . normalized : Vector3 . up ;
transform . position = mid + toCam * ( WL . Combat . WLCombatMotionSettings . BodyRadius ( _target ) + _front ) ;
transform . localScale = _scale0 * _scale ;
}
void Stop ( ) { _on = false ; if ( _hasScale0 ) transform . localScale = _scale0 ; }
void OnDisable ( ) { Stop ( ) ; }
}
/// <summary>#921 레이저로 피해를 준 속사 화살 — 날아가는 동안 자기 판정을 끈다(이중 피해 방지) · 풀로 돌아갈 때(꺼질 때) 되살린다.</summary>
public sealed class WLLaserMute : MonoBehaviour
{
Collider [ ] _cols ;
public static void Mute ( ProjectileBase p )
{
if ( p = = null ) return ;
var m = p . GetComponent < WLLaserMute > ( ) ; if ( m = = null ) m = p . gameObject . AddComponent < WLLaserMute > ( ) ;
if ( m . _cols = = null ) m . _cols = p . GetComponentsInChildren < Collider > ( true ) ;
for ( int i = 0 ; i < m . _cols . Length ; i + + ) if ( m . _cols [ i ] ! = null ) m . _cols [ i ] . enabled = false ;
m . enabled = true ;
}
void OnDisable ( )
{
if ( _cols ! = null ) for ( int i = 0 ; i < _cols . Length ; i + + ) if ( _cols [ i ] ! = null ) _cols [ i ] . enabled = true ;
enabled = false ;
}
}
}