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

187 lines
13 KiB
C#
Raw Permalink Normal View History

// ─────────────────────────────────────────────────────────────────────────────
// 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)
// ─────────────────────────────────────────────────────────────────────────────
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 = ""; }
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>();
WLBowPoseIK.Ensure(pc); // #967 활 쏘는 자세 두 손 벌리기(IK)
}
}
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; }
/// <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-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);
}
}
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 프리팹 없음)" : "");
}
/// <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);
}
}
}