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

117 lines
6.9 KiB
C#

// ─────────────────────────────────────────────────────────────────────────────
// WLNockArrow.cs — 활 시위를 당기는 동안 화살(기본 화살 투사체와 같은 메시)을 당기는 손 ~ 활 사이에 보인다 (#977 · 2026-10-24)
//
// PD 원문: 「활 시위를 당길 때 화살 오브젝트가 보이게 해줘.(보유중인 리소스 중 화살을 찾아볼 것)」
// ■ 화살 = Resources/WL/Fx/WL_NockArrow(루트 +Z = 촉 · #979 = Stylized Shoot & Hit Vol.2 FX_Shoot_Arrow 의 화살 메시 층을 정적 메시로 · 납작한 면을 위로 눕힘)
// ■ 활 직업(classes)의 애니메이터 상태가 규칙(state · 정규화 from~to = 시위를 당겨 놓기 전까지)에 들면 매 LateUpdate(IK 뒤)에
// 꼬리 = 오른손 · 촉 방향 = 왼손(활 잡은 손) 쪽 · 길이 lengthMeters 로 놓고, 아니면 숨긴다 · 스킬 러너(WLRapidShot)도 같은 attack 상태라 함께
// ■ 값 = Resources/WL/WLNockArrow.json (C45) · 끄기 = enabled 0
// 🔴 Debug.LogError 금지(원본 팝업).
// ─────────────────────────────────────────────────────────────────────────────
using UnityEngine;
namespace WL.Combat.Weapon
{
[System.Serializable]
public class WLNockArrowRule { public string state = ""; public float from, to = 1f; }
[System.Serializable]
public class WLNockArrowJson
{
public int enabled = 0;
public int[] classes = new int[0];
public string prefab = "WL/Fx/WL_NockArrow";
public float lengthMeters = 0.55f; // 보이는 화살 길이(m)
public float widthMul = 1f; // #979 굵기(촉 · 깃 폭) 배율 — 길이와 따로(위 카메라에서 실처럼 보이지 않게)
public float nockOffset = 0f; // 꼬리를 오른손에서 촉 쪽으로 더 미는 거리(m)
public WLNockArrowRule[] rules = new WLNockArrowRule[0];
public string note = "";
}
public static class WLNockArrow
{
public const string ResourcesPath = "WL/WLNockArrow";
public static int ShownFrames; public static string LastState = "", LastLog = "";
static WLNockArrowJson s_cfg; static GameObject s_runner;
internal static WLNockArrowJson Cfg
{
get
{
if (s_cfg != null) return s_cfg;
s_cfg = new WLNockArrowJson();
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.AfterSceneLoad)]
static void Boot()
{
s_cfg = null;
if (Cfg.enabled == 0 || s_runner != null) return;
s_runner = new GameObject("[WL977] NockArrow"); s_runner.hideFlags = HideFlags.HideAndDontSave;
Object.DontDestroyOnLoad(s_runner); s_runner.AddComponent<WLNockArrowRunner>();
}
}
[DefaultExecutionOrder(1000)] // 애니메이션 · 활 손 IK(WLBowPoseIK) 뒤
internal sealed class WLNockArrowRunner : MonoBehaviour
{
GameObject _arrow; float _tailZ, _len = 1f;
void LateUpdate()
{
bool show = false;
try
{
var c = WLNockArrow.Cfg; var pc = MyValue.MyPC as PCActor;
if (c.enabled != 0 && pc != null && pc.m_animation != null && pc.m_animation.isHuman && !pc.IsDead() &&
(c.classes == null || c.classes.Length == 0 || System.Array.IndexOf(c.classes, pc.Get_ID()) >= 0))
{
var an = pc.m_animation; var st = an.GetCurrentAnimatorStateInfo(0); float n = st.normalizedTime;
if (c.rules != null) foreach (var r in c.rules) if (r != null && st.IsName(r.state) && n >= r.from && n <= r.to) { show = true; WLNockArrow.LastState = r.state; break; }
if (show)
{
var rh = an.GetBoneTransform(HumanBodyBones.RightHand); var lh = an.GetBoneTransform(HumanBodyBones.LeftHand);
if (rh == null || lh == null || !Ensure(c)) show = false;
else
{
var d = lh.position - rh.position;
if (d.sqrMagnitude < 1e-6f) show = false;
else
{
float s = c.lengthMeters / Mathf.Max(1e-3f, _len); var rot = Quaternion.LookRotation(d.normalized);
var t = _arrow.transform; t.localScale = new Vector3(s * c.widthMul, s * c.widthMul, s); t.rotation = rot;
t.position = rh.position + rot * new Vector3(0f, 0f, c.nockOffset - _tailZ * s); // 꼬리(_tailZ) = 오른손
WLNockArrow.ShownFrames++;
}
}
}
}
}
catch (System.Exception ex) { WLNockArrow.LastLog = "실패 — " + ex.Message; show = false; }
if (_arrow != null && _arrow.activeSelf != show) _arrow.SetActive(show);
}
bool Ensure(WLNockArrowJson c)
{
if (_arrow != null) return true;
var p = Resources.Load<GameObject>(c.prefab); if (p == null) { WLNockArrow.LastLog = "화살 프리팹 없음 " + c.prefab; return false; }
_arrow = Instantiate(p); _arrow.name = "[WL977] NockArrow"; DontDestroyOnLoad(_arrow);
float zmin = float.MaxValue, zmax = float.MinValue; // 루트 기준 메시 경계(꼬리 · 촉)
foreach (var mf in _arrow.GetComponentsInChildren<MeshFilter>(true))
{
if (mf.sharedMesh == null) continue; var b = mf.sharedMesh.bounds; var m = _arrow.transform.worldToLocalMatrix * mf.transform.localToWorldMatrix;
for (int i = 0; i < 8; i++) { var v = m.MultiplyPoint3x4(new Vector3((i & 1) == 0 ? b.min.x : b.max.x, (i & 2) == 0 ? b.min.y : b.max.y, (i & 4) == 0 ? b.min.z : b.max.z)); zmin = Mathf.Min(zmin, v.z); zmax = Mathf.Max(zmax, v.z); }
}
_tailZ = zmin < zmax ? zmin : 0f; _len = zmin < zmax ? zmax - zmin : 1f;
WLNockArrow.LastLog = "화살 길이(원본) " + _len.ToString("F2") + " m · 꼬리 z " + _tailZ.ToString("F2");
_arrow.SetActive(false);
return true;
}
}
}