2026-10-18 15:09:05 +00:00
// ─────────────────────────────────────────────────────────────────────────────
// WLBowCharge.cs — 활 3타 「기 모으기 → 큰 화살」 (#839 · 2026-10-11 · PD 「3발 발사 후 마지막 타 공격 시 살짝 임팩트 있게 모았다가 발사 · 활에 기가 모이는 임팩트 후 좀 더 큰 화살」)
/ /
// ■ 신호 — 3타 클립(FR_Bow3_{CID}.anim)에 매핑 도구가 굽는 애니메이션 이벤트 WLBowCharge(float = 지속 초).
// Animator 는 PC 루트(Actor 와 같은 GameObject)에 있으므로 루트에 붙은 이 수신기가 받는다(원본 Actor.cs 0줄).
// ■ 동작 — ① 활 소켓(이름에 Bow 가 든 무기 노드)에 Resources/WL/WL_BowChargeVisual 을 지속 시간만큼 띄운다
// ② 「강화 창」(ArmedUntil)을 연다 → 그 안에 스폰되는 화살(WLProjectileVisual 가 붙인 WLChargedMark)이 크기 × chargedScale + 글로우.
// ■ 부착 — WLBowChargeBoot 가 0.5 s 마다 MyValue.MyPC 루트에 수신기를 보장한다(씬 전환·캐릭터 교체 대응).
// ■ #919 PD 「마지막 타의 모으기 이펙트가 활 중심부에 모이도록 · 3타 모으기 시간을 절반으로 · 발사되는 투사체를 지금 기본 공격 투사체로」
// = 이펙트 프리팹(WL_BowChargeVisual)의 모으기 파티클 3 개를 루트(= 활 그림 중심 · 활 피벗이 그림 중심과 같음 실측 0.00 m)로 옮기고 재생 속도 × 2(Level_up 배치 그대로라 0.37~0.52 m 위에서 모였다 · 실측)
// · 매핑(CombatAnimMapping · FR_Bow3_1040x.anim) WLBowCharge 0.6 → 0.3 초 · 당김 유지 속도 0.35 → 0.69(모으기 이벤트 → 발사 이벤트 클립 0.208 초를 0.3 초에 — 모으기 끝에 쏜다)
// · 강화 화살 겉모습 = WLProjectileVisualRule.chargedReplaces(기본 겉모습을 끄고 chargedVisual 로 바꿈 · normal)
// ■ #923 PD 「활 공격 중 구 버전의 화살 이펙트가 날아가는 경우가 간헐적으로」 — 3타 모으기 뒤 3타가 끊기면(구르기 등) 창(모으기 + 1.3 초)이
// 남아 다음 1타 화살이 3타 겉모습(1·2타였던 노란 탄 · #921 맞바꿈)으로 날았다(Play 재현: 3타 모으기 중 구르기 → 1.24 초 뒤 다음 1타 화살이 3타 겉모습)
// → 창을 연 PC 의 애니메이터가 창을 연 상태(3타 클립)가 아닐 때 쏜 화살이면 강화하지 않고 창을 닫는다(Dropped) · 다른 쏜 이의 화살은 창을 쓰지 않는다
2026-10-31 06:53:50 +00:00
// ■ #1005 모으기 이펙트 = Resources/WL/WLBowCharge.json(대검 모으기 사본 WL_Fx_BowChargeWhite · 대검과 같은 그라데이션 · 숨김) · 끝 = 방출 멈춤 + 흐려짐(WLFxFade)
2026-10-18 15:09:05 +00:00
// ─────────────────────────────────────────────────────────────────────────────
using UnityEngine ;
namespace WL.Combat.Weapon
{
2026-10-31 06:53:50 +00:00
// #1005 PD 「활 기본 공격 마지막타의 에너지 모이는 연출이 갑자기 끊기는 느낌이 들지 않도록 수정해줘. (대검 모으기 이펙트를 그대로 복사해서 바꾸고, 타이밍을 조절해줘)」
// prefab = Resources 경로 · scale = 월드 배율 · speed = 입자 재생 속도 배율 · fade = 모으기 끝 뒤 흐려지는 초 · gradient · hide = 대검 WLGreatWave.json chargeFxGradient · chargeFxHide 와 같은 형식
[System.Serializable] public class WLBowChargeJson { public string prefab = "WL/WL_BowChargeVisual" ; public float scale = 1f , speed = 1f , fade = 0.25f ; public float [ ] gradient = new float [ 0 ] ; public string [ ] hide = new string [ 0 ] ; public string note = "" ; }
2026-10-18 15:09:05 +00:00
public sealed class WLBowChargeBoot : MonoBehaviour
{
float _next ;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
static void Boot ( )
{
if ( FindFirstObjectByType < WLBowChargeBoot > ( ) ! = null ) return ;
var go = new GameObject ( "WL_BowChargeBoot" ) ;
DontDestroyOnLoad ( go ) ;
go . AddComponent < WLBowChargeBoot > ( ) ;
}
void Update ( )
{
if ( Time . unscaledTime < _next ) return ;
_next = Time . unscaledTime + 0.5f ;
var pc = MyValue . MyPC ;
if ( pc = = null ) return ;
if ( pc . GetComponent < WLBowChargeReceiver > ( ) = = null ) pc . gameObject . AddComponent < WLBowChargeReceiver > ( ) ;
2026-10-21 16:30:22 +00:00
WLBowPoseIK . Ensure ( pc ) ; // #967 활 쏘는 자세 두 손 벌리기(IK)
2026-10-18 15:09:05 +00:00
}
}
public sealed class WLBowChargeReceiver : MonoBehaviour
{
2026-10-31 06:53:50 +00:00
public const string ResourcesPath = "WL/WLBowCharge" ; // #1005 모으기 이펙트 값(이전 = 상수 WL/WL_BowChargeVisual)
static WLBowChargeJson s_cfg ; static bool s_missing ;
static WLBowChargeJson Cfg
{
get
{
if ( s_cfg ! = null ) return s_cfg ;
s_cfg = new WLBowChargeJson ( ) ;
var ta = Resources . Load < TextAsset > ( ResourcesPath ) ;
if ( ta ! = null ) try { JsonUtility . FromJsonOverwrite ( ta . text , s_cfg ) ; } catch ( System . Exception ex ) { LastLog = "설정 파싱 실패 — " + ex . Message ; }
return s_cfg ;
}
}
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] static void BootCfg ( ) { s_cfg = null ; }
2026-10-18 15:09:05 +00:00
/// <summary>이 시각 전에 스폰되는 투사체는 강화된다(0 = 닫힘).</summary>
public static float ArmedUntil ;
/// <summary>#923 창을 연 PC 와 그때의 애니메이터 상태(3타 클립) — 그 PC 가 다른 상태(다음 콤보 1타 · 구르기 등)에서 쏜 화살이면 창을 닫는다
/// (3타가 끊겨 남은 창이 다음 1타 화살을 3타 겉모습으로 바꾸던 것 · 활 콤보는 1~3타 모두 CurAnim = Attack 이라 동작 값으로는 못 가른다 · Play 실측).</summary>
public static Actor ArmedShooter ;
public static Animator ArmedAnimator ;
public static int ArmedState ;
public static int Charges , Consumed , Dropped ;
public static string LastLog = "" ;
// 애니메이션 이벤트 진입점 — functionName "WLBowCharge" · floatParameter = 기 모으기(활시위 유지) 초 · stringParameter = 유지 중 애니메이터 속도(예 "0.35" · 비면 유지 없음)
// #840 PD 「투사체 발사 전까지 활시위를 당기는 모션이 유지되도록」 — 이벤트 시점부터 floatParameter 초 동안 애니메이터를 느리게(당김 자세 유지) 돌리고 원래 속도로 되돌린다.
// 발사(Projectile 이벤트 0.92)는 그 뒤 클립 진행에 따라 온다 → 당김이 끝나기 전에 쏘는 일이 없다.
Coroutine _hold ;
public void WLBowCharge ( AnimationEvent e )
{
float dur = e ! = null & & e . floatParameter > 0f ? e . floatParameter : 0.45f ;
float holdSpeed = 0f ;
if ( e ! = null & & ! string . IsNullOrEmpty ( e . stringParameter ) ) float . TryParse ( e . stringParameter , System . Globalization . NumberStyles . Float , System . Globalization . CultureInfo . InvariantCulture , out holdSpeed ) ;
if ( holdSpeed > 0f )
{
var an = GetComponent < Animator > ( ) ;
if ( an ! = null ) { if ( _hold ! = null ) StopCoroutine ( _hold ) ; _hold = StartCoroutine ( CoHold ( an , holdSpeed , dur ) ) ; }
}
Transform anchor = FindBow ( transform ) ;
2026-10-31 06:53:50 +00:00
var c = Cfg ;
if ( ! string . IsNullOrEmpty ( c . prefab ) )
2026-10-18 15:09:05 +00:00
{
2026-10-31 06:53:50 +00:00
// #1005 대검 모으기와 같게 — 월드 배율 scale · 대검 그라데이션 · 숨김 층 · 모으기가 끝나면 방출을 멈추고 fade 초 동안 흐려짐(이전 = dur + 0.25 s 에 통째로 지움 = 끊김)
var at = anchor ! = null ? anchor . position : transform . position + Vector3 . up * 0.9f ;
var vfx = WLGreatWave . Spawn ( c . prefab , at , Quaternion . identity , c . scale , 0f ) ;
s_missing = vfx = = null ;
if ( vfx ! = null )
{
WLGreatWave . TintGradient ( vfx , c . gradient ) ; WLGreatWave . HideLayers ( vfx , c . hide ) ; // 숨김은 루트 이름(프리팹 이름)으로 찾으므로 이름 바꾸기 전에
vfx . name = "~WL_BowCharge" ;
if ( anchor ! = null ) vfx . transform . SetParent ( anchor , true ) ;
foreach ( var ps in vfx . GetComponentsInChildren < ParticleSystem > ( true ) ) { if ( c . speed > 0f ) { var m = ps . main ; m . simulationSpeed * = c . speed ; } ps . Play ( true ) ; }
WLFxFade . Attach ( vfx , dur , c . fade , true ) ;
}
2026-10-18 15:09:05 +00:00
}
ArmedUntil = Time . time + dur + 1.0f ;
ArmedShooter = GetComponent < Actor > ( ) ; ArmedAnimator = GetComponent < Animator > ( ) ; ArmedState = StateHash ( ArmedAnimator , true ) ;
Charges + + ;
2026-10-31 06:53:50 +00:00
LastLog = "charge " + dur . ToString ( "0.00" ) + "s @" + ( anchor ! = null ? anchor . name : "pc" ) + " · " + c . prefab + " × " + c . scale . ToString ( "0.00" ) + ( s_missing ? " (VFX 프리팹 없음)" : "" ) ;
2026-10-18 15:09:05 +00:00
}
/// <summary>지금 상태(전이 중이면 preferNext 일 때 들어가는 상태)의 fullPathHash · 없으면 0.</summary>
internal static int StateHash ( Animator an , bool preferNext )
{
if ( an = = null | | ! an . isActiveAndEnabled ) return 0 ;
if ( preferNext & & an . IsInTransition ( 0 ) ) return an . GetNextAnimatorStateInfo ( 0 ) . fullPathHash ;
return an . GetCurrentAnimatorStateInfo ( 0 ) . fullPathHash ;
}
System . Collections . IEnumerator CoHold ( Animator an , float holdSpeed , float dur )
{
float prev = an . speed ;
an . speed = Mathf . Clamp ( holdSpeed , 0.05f , 1f ) ;
float t = 0f ;
while ( t < dur & & an ! = null & & an . isActiveAndEnabled ) { t + = Time . deltaTime ; yield return null ; }
if ( an ! = null ) an . speed = prev < = 0f ? 1f : prev ;
_hold = null ;
}
static Transform FindBow ( Transform root )
{
Transform best = null ;
foreach ( var t in root . GetComponentsInChildren < Transform > ( false ) )
{
if ( ! t . gameObject . activeInHierarchy ) continue ;
if ( t . name . IndexOf ( "Bow" , System . StringComparison . OrdinalIgnoreCase ) > = 0 & & t . name . IndexOf ( "Elbow" , System . StringComparison . OrdinalIgnoreCase ) < 0 ) { best = t ; break ; }
}
return best ;
}
}
/// <summary>풀 투사체 인스턴스에 붙어, 켜질 때 강화 창이 열려 있으면 크기·글로우를 적용하고 꺼질 때 되돌린다.</summary>
public sealed class WLChargedMark : MonoBehaviour
{
public float scale = 1.6f ;
public GameObject visual ;
public GameObject normal ; // #919 강화 동안 끌 기본 겉모습(WLProjectileVisualRule.chargedReplaces) — null 이면 덧붙이기
Vector3 _orig ; bool _hasOrig ; bool _charged ;
void Awake ( ) { if ( ! _hasOrig ) { _orig = transform . localScale ; _hasOrig = true ; } }
void OnEnable ( ) { ApplyNow ( ) ; }
void OnDisable ( ) { Restore ( ) ; }
public void ApplyNow ( )
{
if ( ! _hasOrig ) { _orig = transform . localScale ; _hasOrig = true ; }
if ( ! _charged & & WLBowChargeReceiver . ArmedUntil > 0f & & Time . time < WLBowChargeReceiver . ArmedUntil )
{
// #923 3타 모으기 뒤 3타 화살이 안 나가고(구르기 등으로 끊김) 창이 남아 있으면 — 같은 PC 의 다른 동작 중 화살은 강화하지 않고 창을 닫는다
var armed = WLBowChargeReceiver . ArmedShooter ;
if ( armed ! = null )
{
var pb = GetComponent < ProjectileBase > ( ) ; var pd = pb ! = null ? pb . Get_ProjectTileData ( ) : null ;
if ( pd = = null | | ! ReferenceEquals ( pd . shooter , armed ) ) return ;
var an = WLBowChargeReceiver . ArmedAnimator ; int want = WLBowChargeReceiver . ArmedState ;
if ( an ! = null & & want ! = 0 & & WLBowChargeReceiver . StateHash ( an , false ) ! = want & & WLBowChargeReceiver . StateHash ( an , true ) ! = want )
{ WLBowChargeReceiver . ArmedUntil = 0f ; WLBowChargeReceiver . Dropped + + ; return ; }
}
WLBowChargeReceiver . ArmedUntil = 0f ;
_charged = true ;
transform . localScale = _orig * Mathf . Max ( scale , 0.01f ) ;
if ( normal ! = null ) normal . SetActive ( false ) ;
if ( visual ! = null ) { visual . SetActive ( true ) ; foreach ( var ps in visual . GetComponentsInChildren < ParticleSystem > ( true ) ) ps . Play ( true ) ; }
WLBowChargeReceiver . Consumed + + ;
}
}
void Restore ( )
{
if ( ! _charged ) return ;
_charged = false ;
transform . localScale = _orig ;
if ( visual ! = null ) visual . SetActive ( false ) ;
if ( normal ! = null ) normal . SetActive ( true ) ;
}
}
}