// ───────────────────────────────────────────────────────────────────────────── // 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(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(); } } [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(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(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; } } }