// ───────────────────────────────────────────────────────────────────────────── // WLAttackFacing.cs — 공격할 때는 대상 쪽으로 돌아선 뒤 쏜다/벤다 (#923 · 2026-10-18) // // PD 원문: 「구르기 직 후 캐릭터가 바라보는 방향과 반대 방향으로 활을 쏘는 경우가 있는데 활을 쏘거나 무기로 공격할 때는 // 반드시 바라보는 방향으로 전환한 후 공격하도록 해줘.」 // ■ 원본 Actor.Play_Attack 은 공격 시작 때 한 번 대상을 본다(LookAt) — 그 뒤 구르기 끝 회전 등이 몸을 돌려 놓으면 // 화살은 대상(뒤)으로 날아가는데 몸은 반대를 본 채 쏘았다. // ■ ① 기본 공격(CurAnim Attack ~ Attack4) 동안 매 프레임 대상 쪽으로 빠르게 돈다(turnDegPerSec · 수평) // ② 판정 · 투사체가 나가는 순간(CombatEvents.HitboxSpawned = 원본 Shoot_Projectile 첫 줄 · 투사체 생성 전) 대상 쪽으로 바로 맞춘다 // → 투사체 시작 위치(몸 앞) · 근접 판정 상자도 대상 쪽에서 나온다. // ■ 값 = Resources/WL/WLAttackFacing.json (C45) · 끄기 = enabled 0 · 내 PC 만(몹 · 펫 무관) // 🔴 원본 수정 0줄 · Debug.LogError 금지. // ───────────────────────────────────────────────────────────────────────────── using UnityEngine; using WL.Combat.Core; namespace WL.Combat { [System.Serializable] public class WLAttackFacingJson { public int enabled = 1; public float turnDegPerSec = 1440f; // 공격 동작 중 대상 쪽으로 도는 속도(도/초) public int snapAtHitbox = 1; // 1 = 판정 · 투사체가 나가는 순간 바로 대상 쪽으로 public float minDistance = 0.05f; // 대상이 이보다 가까우면(겹침) 돌지 않는다(m) public string note = ""; } public sealed class WLAttackFacing : MonoBehaviour { public const string ResourcesPath = "WL/WLAttackFacing"; public static int Snaps, Turns; public static float LastSnapDeg; static WLAttackFacingJson s_cfg; static WLAttackFacing s_inst; static WLAttackFacingJson Cfg { get { if (s_cfg != null) return s_cfg; s_cfg = new WLAttackFacingJson(); var ta = Resources.Load(ResourcesPath); if (ta != null) { try { JsonUtility.FromJsonOverwrite(ta.text, s_cfg); } catch (System.Exception) { s_cfg.enabled = 0; } } return s_cfg; } } [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] static void Boot() { s_cfg = null; Snaps = Turns = 0; if (Cfg.enabled == 0 || s_inst != null) return; var go = new GameObject("WL_AttackFacing"); DontDestroyOnLoad(go); s_inst = go.AddComponent(); CombatEvents.HitboxSpawned.Remove(OnHitbox); CombatEvents.HitboxSpawned.Add(OnHitbox); } void OnDestroy() { if (s_inst == this) { CombatEvents.HitboxSpawned.Remove(OnHitbox); s_inst = null; } } static bool Dir(PCActor pc, out Vector3 dir) { dir = Vector3.zero; if (pc == null || pc.IsDead() || !pc.isTarget) return false; var t = pc.Get_Target(); if (DSUtil.CheckNull(t) || t.IsDead()) return false; dir = t.Get_position() - pc.Get_position(); dir.y = 0f; return dir.magnitude > Cfg.minDistance; } static void OnHitbox(in HitboxSpawnedEvent e) { var c = Cfg; if (c.enabled == 0 || c.snapAtHitbox == 0) return; var pc = e.shooter as PCActor; if (pc == null || !ReferenceEquals(pc, MyValue.MyPC)) return; var cur = pc.Get_CurAnim(); if (cur < eAnim.Attack || cur >= eAnim.Skill) return; // 기본 공격만(스킬은 각자 방향을 정한다) Vector3 dir; if (!Dir(pc, out dir)) return; var want = Quaternion.LookRotation(dir.normalized); LastSnapDeg = Quaternion.Angle(pc.transform.rotation, want); pc.transform.rotation = want; Snaps++; } void LateUpdate() { var c = Cfg; if (c.enabled == 0) return; var pc = MyValue.MyPC as PCActor; if (pc == null) return; var cur = pc.Get_CurAnim(); if (cur < eAnim.Attack || cur >= eAnim.Skill) return; Vector3 dir; if (!Dir(pc, out dir)) return; var want = Quaternion.LookRotation(dir.normalized); if (Quaternion.Angle(pc.transform.rotation, want) < 0.5f) return; pc.transform.rotation = Quaternion.RotateTowards(pc.transform.rotation, want, c.turnDegPerSec * Time.deltaTime); Turns++; } } }