Project_WL/Assets/WL/Combat/Weapon/WLBowCharge.cs

164 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ─────────────────────────────────────────────────────────────────────────────
// 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) · 다른 쏜 이의 화살은 창을 쓰지 않는다
// ─────────────────────────────────────────────────────────────────────────────
using UnityEngine;
namespace WL.Combat.Weapon
{
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>();
}
}
public sealed class WLBowChargeReceiver : MonoBehaviour
{
public const string VisualPath = "WL/WL_BowChargeVisual";
/// <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 = "";
static GameObject s_prefab;
// 애니메이션 이벤트 진입점 — 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);
if (s_prefab == null) s_prefab = Resources.Load<GameObject>(VisualPath);
if (s_prefab != null)
{
var vfx = Instantiate(s_prefab, anchor != null ? anchor : transform);
vfx.name = "~WL_BowCharge";
vfx.transform.localPosition = Vector3.zero;
vfx.transform.localRotation = Quaternion.identity;
if (anchor == null) vfx.transform.localPosition = Vector3.up * 0.9f;
foreach (var ps in vfx.GetComponentsInChildren<ParticleSystem>(true)) ps.Play(true);
Destroy(vfx, dur + 0.25f);
}
ArmedUntil = Time.time + dur + 1.0f;
ArmedShooter = GetComponent<Actor>(); ArmedAnimator = GetComponent<Animator>(); ArmedState = StateHash(ArmedAnimator, true);
Charges++;
LastLog = "charge " + dur.ToString("0.00") + "s @" + (anchor != null ? anchor.name : "pc") + (s_prefab == null ? " (VFX 프리팹 없음)" : "");
}
/// <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);
}
}
}