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