Project_WL/Assets/WL/Combat/Dodge/WLDoubleTapDodge.cs

195 lines
10 KiB
C#

// ─────────────────────────────────────────────────────────────────────────────
// WLDoubleTapDodge.cs — 화면 두 번 터치 = 그 방향으로 구르기(회피) · 모든 무기 공통
//
// PD 원문(#889 · 2026-10-17): 「화면을 두번 터치하면 해당 방향으로 구르기 동작(회피)을 하도록 구현해줘. (모든 무기 공통)」
//
// ■ 원리 — 짧은 탭(누름 → 뗌이 tapMaxSeconds 안 · 움직임 tapMaxMove 안) 뒤 gapMaxSeconds 안에 가까운 자리(gapMaxDistance)를 다시 누르면
// 그 화면 지점을 PC 발 높이 평면에 비춰 방향을 구하고, PC 를 그쪽으로 돌린 뒤 원본 회피 PCActor.Play_Dodge()(앞으로 3 m · 회피 쿨타임 ·
// 직업마다 애니메이터 dodge 상태 — 14개 컨트롤러 모두 있음)를 부른다. 탭이 PC 바로 위(minDirDistance 안)면 지금 바라보는 쪽.
// ■ 막는 경우 — 버튼 등 눌리는 UI(Selectable) 위 탭 · 스킬 동작 중(allowDuringSkill 0) · 시간 정지(timeScale 0) · PC 없음/사망/꺼짐.
// ■ 입력 = 레거시 Input(터치 우선 · 없으면 마우스 = 에디터) · 값 = Resources/WL/WLDoubleTapDodge.json (C45) · 끄기 = enabled 0(오브젝트 0) · 원본 수정 0줄
// 🔴 Debug.LogError 금지(원본 ErrorLogHook 이 팝업을 띄운다).
// ─────────────────────────────────────────────────────────────────────────────
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace WL.Combat
{
[System.Serializable]
public class WLDoubleTapDodgeJson
{
public int enabled = 0;
public float tapMaxSeconds = 0.25f; // 한 번 탭: 누름 → 뗌 최대 시간
public float tapMaxMove = 0.03f; // 한 번 탭: 최대 움직임(화면 높이 비율)
public float gapMaxSeconds = 0.3f; // 첫 탭 뗌 → 두 번째 누름 최대 간격
public float gapMaxDistance = 0.12f; // 두 탭 사이 최대 거리(화면 높이 비율)
public int ignoreOverButtons = 1;
public int allowDuringSkill = 0;
public float minDirDistance = 0.3f; // 탭 지점이 PC 에서 이만큼(m) 안이면 바라보는 쪽
public float dustScale = 1f; // #892 구르기 흙먼지(원본 Effect_PCDash) 배율
public string note = "";
}
/// <summary>흙먼지 이펙트 인스턴스의 원래 크기(풀에서 다시 쓰여도 같은 기준으로 줄인다).</summary>
internal sealed class WLDustScaleMark : MonoBehaviour { public Vector3 baseScale = Vector3.one; }
public static class WLDoubleTapDodge
{
public const string ResourcesPath = "WL/WLDoubleTapDodge";
public static int Taps, DoubleTaps, Dodges, Blocked;
public static string LastLog = "";
static WLDoubleTapDodgeJson s_cfg;
static WLDoubleTapDodgeRunner s_runner;
static bool s_down, s_downOnButton;
static int s_finger;
static Vector2 s_downPos, s_lastTapPos;
static float s_downTime, s_lastTapTime = -10f, s_maxMove;
static readonly List<RaycastResult> s_hits = new List<RaycastResult>();
internal static WLDoubleTapDodgeJson Cfg
{
get
{
if (s_cfg != null) return s_cfg;
s_cfg = new WLDoubleTapDodgeJson();
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; }
return s_cfg;
}
}
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
static void Boot()
{
s_cfg = null; s_down = false; s_lastTapTime = -10f;
if (Cfg.enabled == 0 || !DSUtil.CheckNull(s_runner)) return; // 꺼져 있으면 오브젝트 0
var go = new GameObject("[WL889] DoubleTapDodge");
go.hideFlags = HideFlags.HideAndDontSave;
s_runner = go.AddComponent<WLDoubleTapDodgeRunner>();
}
internal static void Tick()
{
float now = Time.unscaledTime;
if (Input.touchCount > 0)
{
for (int i = 0; i < Input.touchCount; i++)
{
var t = Input.GetTouch(i);
if (t.phase == TouchPhase.Began) { if (!s_down) Down(t.position, now, t.fingerId); }
else if (s_down && t.fingerId == s_finger)
{
if (t.phase == TouchPhase.Moved) Move(t.position);
else if (t.phase == TouchPhase.Ended || t.phase == TouchPhase.Canceled) Up(t.position, now, t.phase == TouchPhase.Canceled);
}
}
return;
}
if (Input.GetMouseButtonDown(0)) Down(Input.mousePosition, now, -1);
else if (s_down && Input.GetMouseButton(0)) Move(Input.mousePosition);
else if (s_down && Input.GetMouseButtonUp(0)) Up(Input.mousePosition, now, false);
}
/// <summary>누름(시험 도구도 부른다). 두 번째 누름이 조건을 채우면 바로 구른다.</summary>
public static void Down(Vector2 p, float now, int finger)
{
var c = Cfg;
s_down = true; s_finger = finger; s_downPos = p; s_downTime = now; s_maxMove = 0f;
s_downOnButton = c.ignoreOverButtons != 0 && OverButton(p);
if (s_downOnButton) return;
if (now - s_lastTapTime <= c.gapMaxSeconds && (p - s_lastTapPos).magnitude <= c.gapMaxDistance * Screen.height)
{
DoubleTaps++;
s_lastTapTime = -10f;
TryDodge(p);
}
}
public static void Move(Vector2 p) { s_maxMove = Mathf.Max(s_maxMove, (p - s_downPos).magnitude); }
/// <summary>뗌(시험 도구도 부른다). 짧고 거의 안 움직였으면 「한 번 탭」으로 기억한다.</summary>
public static void Up(Vector2 p, float now, bool canceled)
{
if (!s_down) return;
s_down = false;
var c = Cfg;
Move(p);
bool tap = !canceled && !s_downOnButton && now - s_downTime <= c.tapMaxSeconds && s_maxMove <= c.tapMaxMove * Screen.height;
if (tap) { Taps++; s_lastTapPos = p; s_lastTapTime = now; }
else s_lastTapTime = -10f;
}
static void TryDodge(Vector2 screen)
{
var c = Cfg;
var pc = MyValue.MyPC;
if (DSUtil.CheckNull(pc) || pc.IsDead() || !pc.gameObject.activeInHierarchy) { Blocked++; LastLog = "PC 없음"; return; }
if (Time.timeScale <= 0f) { Blocked++; LastLog = "시간 정지"; return; }
if (c.allowDuringSkill == 0 && pc.Get_CurAnim() == eAnim.Skill) { Blocked++; LastLog = "스킬 중"; return; }
if (pc is PCActor wpc && WL.Combat.Weapon.WLSkillCancel.BlockDodge(wpc)) { Blocked++; LastLog = "점프 스킬 중(#908 이동 · 구르기 무시)"; return; }
var cam = Camera.main;
var pos = pc.transform.position;
Vector3 dir = Vector3.zero;
if (cam != null)
{
var ray = cam.ScreenPointToRay(screen);
var plane = new Plane(Vector3.up, pos);
float enter;
if (plane.Raycast(ray, out enter)) { dir = ray.GetPoint(enter) - pos; dir.y = 0f; }
}
if (dir.magnitude < c.minDirDistance) { dir = pc.transform.forward; dir.y = 0f; }
if (dir.sqrMagnitude < 1e-6f) dir = Vector3.forward;
var rot0 = pc.transform.rotation;
pc.transform.rotation = Quaternion.LookRotation(dir.normalized);
pc.Play_Dodge();
if (pc.Get_CurAnim() == eAnim.Dodge) { Dodges++; LastLog = "구르기 #" + Dodges + " 방향 " + dir.normalized.ToString("F2"); }
else { pc.transform.rotation = rot0; Blocked++; LastLog = "구르기 안 됨(회피 쿨타임 · 이미 구르는 중 등) · 방향 그대로"; } // #923 못 구르면 몸을 탭 쪽으로 돌려 둔 채 남기지 않는다(공격 중이면 등 뒤 대상에게 반대로 쏘던 것)
}
public static int DustScaled;
/// <summary>원본 Actor.Play_Dodge 이펙트 콜백 훅(#892) — 흙먼지를 dustScale 배로(파티클 크기 모드 Hierarchy).</summary>
public static void AdjustDust(GameObject fx)
{
if (fx == null) return;
float k = Cfg.dustScale;
if (k <= 0f || Mathf.Approximately(k, 1f)) return;
var mark = fx.GetComponent<WLDustScaleMark>();
if (mark == null)
{
mark = fx.AddComponent<WLDustScaleMark>();
mark.baseScale = fx.transform.localScale;
foreach (var ps in fx.GetComponentsInChildren<ParticleSystem>(true)) { var m = ps.main; m.scalingMode = ParticleSystemScalingMode.Hierarchy; }
}
fx.transform.localScale = mark.baseScale * k;
DustScaled++;
}
static bool OverButton(Vector2 p)
{
var es = EventSystem.current;
if (es == null) return false;
var ped = new PointerEventData(es) { position = p };
s_hits.Clear();
es.RaycastAll(ped, s_hits);
for (int i = 0; i < s_hits.Count; i++)
{
var go = s_hits[i].gameObject; if (go == null) continue;
var sel = go.GetComponentInParent<Selectable>();
if (sel != null && sel.enabled && sel.IsInteractable()) return true;
}
return false;
}
}
/// <summary>WLDoubleTapDodge 의 입력 읽기. 숨김 GameObject 1개.</summary>
internal sealed class WLDoubleTapDodgeRunner : MonoBehaviour
{
void Update() { WLDoubleTapDodge.Tick(); }
}
}