164 lines
11 KiB
C#
164 lines
11 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
||
// 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);
|
||
}
|
||
}
|
||
}
|