// ───────────────────────────────────────────────────────────────────────────── // WLSkillYaw.cs — 스킬 동작 상태 동안 보이는 몸(엉덩이 뼈)을 위 축으로 일정 각만큼 돌려 공격 대상 쪽에 맞춘다 (#974 · 2026-10-23) // // PD 원문: 「한손검 주력 스킬 시 회전할 때 공격 대상의 방향과 일치하도록 수정해줘 (현재는 공격 투사체 방향보다 더 회전하고 있어.)」 // ■ 실측(#974 진단 Play · 974_probe Spin): 검기 분출 동작 energyslash(월광 클립 combo09_3)는 몸을 한 바퀴 반 돌리고, // 검기를 쏘는 순간(정규화 0.41~0.45) 몸이 대상 · 검기 방향과 어긋난다(PC 루트 · 검기는 대상 쪽 · 어긋난 각 = 규칙 yaw 의 근거 · JSON 메모) // ■ 처리 = 규칙 상태(지금 상태 또는 넘어가는 중인 다음 상태)일 때 LateUpdate(애니메이션 평가 뒤)에서 엉덩이 뼈를 PC 루트 둘레 위 축으로 yaw° — // 들어갈 때 blendIn · 나갈 때 blendOut 초에 걸쳐 · 애니메이터가 매 프레임 뼈를 다시 쓰므로 누적 없음 · 루트 · 판정 · 투사체 방향 · 카메라는 그대로(보이는 몸만) // (WLImpactFx 비수 3타 회전 #956 과 같은 방식) // ■ 값 = Resources/WL/WLSkillYaw.json (C45) · 끄기 = enabled 0 // 🔴 Debug.LogError 금지(원본 ErrorLogHook 팝업). // ───────────────────────────────────────────────────────────────────────────── using UnityEngine; namespace WL.Combat.Weapon { [System.Serializable] public class WLSkillYawRule { public string state = ""; // 애니메이터 0층 상태 이름(예: energyslash) public float yaw; // 보이는 몸을 돌릴 각(°) · + = 위에서 볼 때 시계 방향 public float blendIn = 0.08f, blendOut = 0.15f; public string note = ""; } [System.Serializable] public class WLSkillYawJson { public int enabled = 1; public WLSkillYawRule[] rules = new WLSkillYawRule[0]; public string note = ""; } public static class WLSkillYaw { public const string ResourcesPath = "WL/WLSkillYaw"; public static int Frames; public static float LastYaw; public static string LastLog = ""; static WLSkillYawJson s_cfg; static GameObject s_runner; internal static WLSkillYawJson Cfg { get { if (s_cfg != null) return s_cfg; s_cfg = new WLSkillYawJson(); 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("[WL974] SkillYaw"); s_runner.hideFlags = HideFlags.HideAndDontSave; Object.DontDestroyOnLoad(s_runner); s_runner.AddComponent(); } internal static WLSkillYawRule Match(AnimatorStateInfo st) { var rules = Cfg.rules; if (rules == null) return null; for (int i = 0; i < rules.Length; i++) { var r = rules[i]; if (r != null && !string.IsNullOrEmpty(r.state) && st.IsName(r.state)) return r; } return null; } } [DefaultExecutionOrder(-100)] // 손 · 무기를 따라가는 궤적 · 이펙트(LateUpdate)보다 먼저 몸을 돌린다 internal sealed class WLSkillYawRunner : MonoBehaviour { float _w; WLSkillYawRule _rule; void LateUpdate() { try { if (WLSkillYaw.Cfg.enabled == 0) return; var pc = MyValue.MyPC as PCActor; if (pc == null || pc.m_animation == null || !pc.m_animation.isHuman) { _w = 0f; _rule = null; return; } var an = pc.m_animation; var hit = WLSkillYaw.Match(an.GetCurrentAnimatorStateInfo(0)); if (hit == null && an.IsInTransition(0)) hit = WLSkillYaw.Match(an.GetNextAnimatorStateInfo(0)); if (hit != null) _rule = hit; if (_rule == null) return; float target = hit != null ? 1f : 0f; float dur = target > _w ? _rule.blendIn : _rule.blendOut; _w = Mathf.MoveTowards(_w, target, Time.deltaTime / Mathf.Max(0.01f, dur)); if (_w <= 0f) { _rule = null; WLSkillYaw.LastYaw = 0f; return; } var hips = an.GetBoneTransform(HumanBodyBones.Hips); if (hips == null) return; float d = _rule.yaw * _w; hips.RotateAround(pc.transform.position, pc.transform.up, d); WLSkillYaw.Frames++; WLSkillYaw.LastYaw = d; } catch (System.Exception ex) { WLSkillYaw.LastLog = "회전 실패 — " + ex.Message; _w = 0f; _rule = null; } } } }