Project_WL/Assets/WL/Combat/Weapon/WLSkillFollow.cs

189 lines
9.4 KiB
C#

// ─────────────────────────────────────────────────────────────────────────────
// WLSkillFollow.cs — 회전하며 이동하는 스킬(칼날 폭풍)이 자동 전투 중에도 가까운 적을 따라가게 한다
//
// PD 원문(#885 · 2026-10-17): 「자동 전투 중에도 가까운 적을 따라가며 돌게 해줘.」
//
// ■ 원리 — 원본 Skill_Whirlwind 는 지속시간 동안 PC 조이스틱 상태를 2(조이스틱만)로 둔다 → 자동 접근(PCActor.Update)이 멈춰
// 조이스틱을 잡지 않으면 제자리에서 돈다. 원본 SkillEvent 훅(CombatEvents.SkillFired)에서 표의 스킬이면 따라가기를 켜고,
// 조이스틱 상태가 2 인 동안 플레이어가 조이스틱을 잡지 않은 프레임마다 가까운 적 쪽으로 NavMeshAgent.Move(원본 조이스틱 이동과 같은 방식).
// 조이스틱 상태가 2 에서 풀리면(스킬 끝) 끈다.
// ■ 손 뗌 보류 — 조이스틱 상태 2 중에 손을 떼면 원본 Set_AutoBraking(true) 의 Play_Idle 이 회전 동작을 끊고 대기 자세로 미끄러진다
// (섬 WLIslandAvatar · 힘민지 FloatingJoystick 둘 다). PCActor.Set_AutoBraking 첫 줄 훅(DeferBrake)이 그 호출을 스킬 끝까지 미루고,
// 끝났을 때 손을 안 잡고 있으면 다시 부른다(IsStop 해제 → 자동 전투 재개). 값 deferBrake.
// ■ 값 = Resources/WL/WLSkillFollow.json (C45) · 끄기 = enabled 0(구독 · 오브젝트 0) · 원본 수정 = PCActor.Set_AutoBraking 1줄
// 🔴 Debug.LogError 금지(원본 ErrorLogHook 이 팝업을 띄운다).
// ─────────────────────────────────────────────────────────────────────────────
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
using WL.Combat.Core;
namespace WL.Combat.Weapon
{
[System.Serializable]
public class WLSkillFollowJson
{
public int enabled = 0;
public string[] skills = new string[0];
public float speedMul = 1f;
public float stopDistance = 1f;
public float searchRadius = 8f;
public float retargetSeconds = 0.25f;
public float startGraceSeconds = 0.5f;
public float inputDeadzone = 0.2f;
public int faceTarget = 1;
public int deferBrake = 0;
public string note = "";
}
public static class WLSkillFollow
{
public const string ResourcesPath = "WL/WLSkillFollow";
public static int Started, Ended, SteerFrames, NoTargetFrames, InRangeFrames, BrakeDeferred, BrakeApplied, BrakeDropped;
public static float MovedMeters;
public static string LastLog = "";
public static bool Active { get { return s_pc != null; } }
static WLSkillFollowJson s_cfg;
static readonly HashSet<string> s_skills = new HashSet<string>();
static PCActor s_pc;
static NavMeshAgent s_agent;
static Actor s_target;
static float s_armedAt, s_retargetAt;
static bool s_locked; // 조이스틱 상태 2 를 한 번이라도 봤다
static PCActor s_brakePc; // 손 뗌 보류 중인 PC
static Joystick s_joy; // 힘민지 FloatingJoystick — 섬은 WLIslandAvatar(SimpleInput)도 같이 본다
static int s_joyLookFrame;
static WLSkillFollowRunner s_runner;
static WLSkillFollowJson Cfg
{
get
{
if (s_cfg != null) return s_cfg;
s_cfg = new WLSkillFollowJson();
var ta = Resources.Load<TextAsset>(ResourcesPath);
if (ta == null) { LastLog = "설정 없음 " + ResourcesPath; return s_cfg; }
try { JsonUtility.FromJsonOverwrite(ta.text, s_cfg); } catch (System.Exception ex) { LastLog = "설정 파싱 실패 — " + ex.Message; }
s_skills.Clear();
if (s_cfg.skills != null) foreach (var s in s_cfg.skills) if (!string.IsNullOrEmpty(s)) s_skills.Add(s);
return s_cfg;
}
}
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
static void Boot()
{
s_cfg = null; s_pc = null; s_brakePc = null;
if (Cfg.enabled == 0) { CombatEvents.SkillFired.Remove(OnSkillFired); return; } // 꺼져 있으면 구독 · 오브젝트 0
CombatEvents.SkillFired.Add(OnSkillFired);
}
static void OnSkillFired(in SkillFiredEvent e)
{
var pc = e.actor as PCActor;
if (pc == null || e.skill == null || !pc.IsMainPC()) return;
var c = Cfg;
if (c.enabled == 0 || !s_skills.Contains(e.skill.e_Skill.ToString())) return;
EnsureRunner();
s_pc = pc; s_agent = pc.GetComponent<NavMeshAgent>(); s_target = null;
s_armedAt = Time.time; s_retargetAt = 0f; s_locked = false;
Started++;
LastLog = "켜짐 " + e.skill.e_Skill + " #" + Started;
}
static void EnsureRunner()
{
if (!DSUtil.CheckNull(s_runner)) return;
var go = new GameObject("[WL885] SkillFollowRunner");
go.hideFlags = HideFlags.HideAndDontSave;
s_runner = go.AddComponent<WLSkillFollowRunner>();
}
/// <summary>PCActor.Set_AutoBraking(true) 첫 줄 훅 — 조이스틱 상태 2(회전하며 이동) 중이면 호출을 스킬 끝까지 미룬다(true = 원본은 지금 아무것도 안 함).</summary>
public static bool DeferBrake(PCActor pc)
{
if (pc == null || pc.JoystickStatus != 2 || !pc.IsMainPC()) return false;
var c = Cfg;
if (c.enabled == 0 || c.deferBrake == 0) return false;
EnsureRunner();
s_brakePc = pc;
BrakeDeferred++;
return true;
}
internal static void Tick()
{
TickBrake();
var pc = s_pc;
if (pc == null) return;
var c = Cfg;
if (DSUtil.CheckNull(pc) || pc.IsDead()) { Stop("PC 없음"); return; }
if (pc.JoystickStatus != 2)
{
// 원본 Skill_Whirlwind 가 SkillEvent 직후 상태 2 로 바꾼다 — 아직 못 봤으면 잠깐 기다리고, 본 뒤에 풀리면 스킬 끝
if (s_locked || Time.time - s_armedAt > c.startGraceSeconds) Stop(s_locked ? "스킬 끝" : "상태 2 못 봄");
return;
}
s_locked = true;
if (PlayerSteering(c)) { SteerFrames++; return; }
if (pc.IsCC() || s_agent == null || !s_agent.isOnNavMesh) return;
var pos = pc.Get_position();
if (Time.time >= s_retargetAt || DSUtil.CheckNull(s_target) || s_target.IsDead())
{
s_retargetAt = Time.time + c.retargetSeconds;
s_target = ActorInfo.Ins != null ? ActorInfo.Ins.Get_Nearest_EnemyActor_orNull(pc.IsEnemy(), pos, c.searchRadius) : null;
}
if (DSUtil.CheckNull(s_target)) { NoTargetFrames++; return; }
var to = s_target.Get_position() - pos; to.y = 0f;
float d = to.magnitude;
if (d <= c.stopDistance) { InRangeFrames++; return; }
var dir = to / d;
float step = Mathf.Min(d - c.stopDistance, pc.Get_MoveSpeed() * c.speedMul * Time.deltaTime);
s_agent.Move(dir * step);
if (c.faceTarget != 0) pc.transform.rotation = Quaternion.LookRotation(dir);
MovedMeters += step;
}
static void TickBrake()
{
var pc = s_brakePc;
if (pc == null) return;
if (DSUtil.CheckNull(pc)) { s_brakePc = null; return; }
if (pc.JoystickStatus == 2) return; // 아직 스킬 중
s_brakePc = null;
if (PlayerSteering(Cfg)) { BrakeDropped++; return; } // 다시 잡고 있다 — 손을 뗄 때 원래 경로가 부른다
pc.Set_AutoBraking(true); // 상태 2 가 풀렸으니 이번엔 원본 그대로 돈다
BrakeApplied++;
}
static bool PlayerSteering(WLSkillFollowJson c)
{
var av = WL.IslandMob.WLIslandAvatar.Instance;
if (av != null && av.PcMoving) return true;
if (DSUtil.CheckNull(s_joy) && Time.frameCount >= s_joyLookFrame)
{
s_joy = Object.FindAnyObjectByType<Joystick>();
s_joyLookFrame = Time.frameCount + 60;
}
return !DSUtil.CheckNull(s_joy) && s_joy.isActiveAndEnabled && s_joy.Direction.sqrMagnitude > c.inputDeadzone * c.inputDeadzone;
}
static void Stop(string why)
{
s_pc = null; s_agent = null; s_target = null;
Ended++;
LastLog = "꺼짐(" + why + ") · 이동 누적 " + MovedMeters.ToString("F1") + " m · 조이스틱 " + SteerFrames + " · 적 없음 " + NoTargetFrames + " · 붙음 " + InRangeFrames;
}
}
/// <summary>WLSkillFollow 의 시간 축. 숨김 GameObject 1개 · 따라가기 켜졌을 때만 일한다.</summary>
internal sealed class WLSkillFollowRunner : MonoBehaviour
{
void Update() { WLSkillFollow.Tick(); }
}
}