Project_WL/Assets/WL/Combat/WLAttackFacing.cs

112 lines
7.0 KiB
C#

// ─────────────────────────────────────────────────────────────────────────────
// 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)
// #963 PD 「비수 4타는 플레이어가 방향을 바꿔 공격하지 않고 원래 적을 공격하던 방향 그대로」 — 이 클립 이름(조각)인 공격은 들어온 순간의 대상 쪽 방향을 클립이 끝날 때까지 유지
// (매 프레임 대상 쪽으로 돌기 · 판정 순간 맞추기 둘 다 멈춤 · 대상을 뚫고 지나가는 돌진 뒤 뒤돌아 베던 것 · Play 실측 156~162°)
public string[] holdClips = new string[0];
public string note = "";
}
public sealed class WLAttackFacing : MonoBehaviour
{
public const string ResourcesPath = "WL/WLAttackFacing";
public static int Snaps, Turns, Holds; public static float LastSnapDeg;
static bool s_holding; static int s_holdHash; static Quaternion s_holdRot;
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<TextAsset>(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 = Holds = 0; s_holding = false;
if (Cfg.enabled == 0 || s_inst != null) return;
var go = new GameObject("WL_AttackFacing"); DontDestroyOnLoad(go); s_inst = go.AddComponent<WLAttackFacing>();
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;
}
/// <summary>#963 지금(전이 중이면 다음) 클립 이름에 holdClips 조각이 들어 있으면 그 상태 해시.</summary>
static bool HoldClip(PCActor pc, WLAttackFacingJson c, out int hash)
{
hash = 0; var an = pc.m_animation; if (an == null || c.holdClips == null || c.holdClips.Length == 0) return false;
bool tr = an.IsInTransition(0); var ci = tr ? an.GetNextAnimatorClipInfo(0) : an.GetCurrentAnimatorClipInfo(0);
if (ci == null || ci.Length == 0 || ci[0].clip == null) return false;
var nm = ci[0].clip.name;
foreach (var h in c.holdClips) if (!string.IsNullOrEmpty(h) && nm.Contains(h)) { hash = (tr ? an.GetNextAnimatorStateInfo(0) : an.GetCurrentAnimatorStateInfo(0)).fullPathHash; return true; }
return false;
}
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; // 기본 공격만(스킬은 각자 방향을 정한다)
if (s_holding) return; // #963 방향 유지 클립 중엔 판정 순간에도 돌리지 않는다
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) { s_holding = false; return; }
int hh; if (HoldClip(pc, c, out hh)) // #963 들어온 순간 대상 쪽으로 한 번 맞춘 뒤 끝까지 그 방향
{
if (!s_holding || hh != s_holdHash) { s_holding = true; s_holdHash = hh; Vector3 d0; s_holdRot = Dir(pc, out d0) ? Quaternion.LookRotation(d0.normalized) : pc.transform.rotation; Holds++; }
pc.transform.rotation = s_holdRot; return;
}
s_holding = false;
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++;
}
}
}