112 lines
7.0 KiB
C#
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++;
|
|
}
|
|
}
|
|
}
|