// ───────────────────────────────────────────────────────────────────────────── // WLOrbFloat.cs — 오브 무기(맹약 · Res_Addr/Weapon/Orb.prefab)를 손 위에 띄운다 (#944 · 2026-10-19) // // PD 원문: 「오브(손에 빛나는 원형 이펙트)의 크기를 눈에 띄게 5개[배] 키워줘」 · 레퍼런스 = 펼친 손바닥 위에 빛나는 고리가 떠서 천천히 흔들림 // 「아트 컨셉은 무시해도 되며 오브의 위치 및 동작 형태만 참고할 것」 // ■ 원리 — 무기는 PCActor.Make_Weapon(Addressables 콜백)으로 손 소켓 자식에 붙고, WLWeaponFitter 가 LateUpdate 소켓 스캔으로 새 자식을 1회 보정한다. // 그 자리(Fit 직후)에서 이름이 weaponNames 에 든 무기에 이 컴포넌트를 1회 붙인다(훅 1줄). 이 컴포넌트는 LateUpdate(애니메이터 뒤)마다 // 월드 위치 = 소켓 위치 + 위 hoverHeight + 흔들림(sin) · 회전 = 월드 위 축 spin · 크기 = 보정된 크기 × scaleMul 로 둔다. // ■ 값 = Resources/WL/WLOrbFloat.json (C45) · enabled 0 = 아무것도 안 함 · 🔴 Debug.LogError 금지(원본 ErrorLogHook 팝업) // ───────────────────────────────────────────────────────────────────────────── using UnityEngine; namespace WL.Combat.Weapon { [System.Serializable] public class WLOrbFloatJson { public int enabled = 1; public string[] weaponNames = new[] { "Orb" }; // 소켓 자식 이름 앞부분(프리팹 이름 · "(Clone)" 무시) public float scaleMul = 5f; // 보정된 크기 × 배수 public float hoverHeight = 0.25f; // 소켓(손) 위 m public float forward = 0f; // 소켓 기준 PC 정면 쪽 m public float bobAmp = 0.04f; // 상하 흔들림 진폭 m public float bobHz = 1.0f; // 흔들림 주기(초당) public float spinDegPerSec = 40f; // 월드 위 축 회전 public float tiltDeg = 0f; // 고리를 눕히는 각(0 = 프리팹 그대로) public int centerChildren = 0; // #948-6 1 = 오브 프리팹 직접 자식(circle 고리 등)의 로컬 위치를 0 으로 — 감싸는 이펙트가 오브 중심을 돈다(원본 circle 은 +0.727 옆에 있어 어긋남) public string note = ""; } public static class WLOrbFloatSettings { public const string ResourcesPath = "WL/WLOrbFloat"; static WLOrbFloatJson s_cfg; static bool s_loaded; public static WLOrbFloatJson Cfg { get { if (!s_loaded) Load(); return s_cfg; } } public static void Reload() { s_loaded = false; } static void Load() { s_loaded = true; try { var ta = Resources.Load(ResourcesPath); s_cfg = ta != null ? JsonUtility.FromJson(ta.text) : null; } catch (System.Exception ex) { WLOrbFloat.LastLog = "설정 읽기 실패 — " + ex.Message; s_cfg = null; } if (s_cfg == null) s_cfg = new WLOrbFloatJson { enabled = 0 }; } } /// 손 소켓 자식(오브)에 붙어 손 위에 띄운다. WLWeaponFitter 훅 1줄 → TryAttach. public sealed class WLOrbFloat : MonoBehaviour { public static int Attached, Frames; public static string LastLog = ""; Transform _socket, _pc; Vector3 _baseScale; float _t0, _spin; public static bool TryAttach(Transform weapon) { try { var c = WLOrbFloatSettings.Cfg; if (c == null || c.enabled == 0 || weapon == null) return false; string n = weapon.name; bool hit = false; if (c.weaponNames != null) for (int i = 0; i < c.weaponNames.Length; i++) if (!string.IsNullOrEmpty(c.weaponNames[i]) && n.StartsWith(c.weaponNames[i])) { hit = true; break; } if (!hit) return false; var f = weapon.GetComponent(); if (f != null) return true; f = weapon.gameObject.AddComponent(); f._socket = weapon.parent; f._pc = f._socket != null ? f._socket.root : null; f._baseScale = weapon.localScale; f._t0 = Time.time; weapon.localScale = f._baseScale * Mathf.Max(0.01f, c.scaleMul); int centered = 0; if (c.centerChildren != 0) for (int i = 0; i < weapon.childCount; i++) { var ch = weapon.GetChild(i); if (ch.localPosition.sqrMagnitude > 1e-6f) { ch.localPosition = Vector3.zero; centered++; } } // #948-6 감싸는 고리를 오브 중심으로 Attached++; LastLog = "오브 부착 " + n + " 소켓=" + (f._socket != null ? f._socket.name : "-") + " 크기 ×" + c.scaleMul + " 높이 " + c.hoverHeight + (centered > 0 ? " · 자식 중심 " + centered : ""); return true; } catch (System.Exception ex) { LastLog = "오브 부착 실패 — " + ex.Message; return false; } } void LateUpdate() { var c = WLOrbFloatSettings.Cfg; if (c == null || c.enabled == 0 || _socket == null) return; float t = Time.time - _t0; Vector3 fwd = _pc != null ? _pc.forward : Vector3.forward; fwd.y = 0f; if (fwd.sqrMagnitude < 1e-4f) fwd = Vector3.forward; fwd.Normalize(); Vector3 pos = _socket.position + Vector3.up * (c.hoverHeight + Mathf.Sin(t * c.bobHz * Mathf.PI * 2f) * c.bobAmp) + fwd * c.forward; _spin += c.spinDegPerSec * Time.deltaTime; var rot = Quaternion.AngleAxis(_spin, Vector3.up) * Quaternion.Euler(c.tiltDeg, 0f, 0f); transform.SetPositionAndRotation(pos, rot); Frames++; } } }