189 lines
9.4 KiB
C#
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(); }
|
|
}
|
|
}
|