509 lines
28 KiB
C#
509 lines
28 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
|
// WLDodgeStyle.cs — 더블 탭 회피의 동작 6가지 = 신발 기본 효과 (#988 · 2026-10-30)
|
|
//
|
|
// PD 원문: 「더블 클릭으로 회피 시 동작을 6가지 형태로 구현해줘. (신발 기본 효과로 사용할 예정이며, 인벤토리에 신발 6종을 배치해서 확인할 수 있게 해줘.)
|
|
// * 한바퀴 돌며 지정한 방향으로 회전하며 회피(현재 상태) * 몸을 둥글게 말며 굴러 지정한 방향으로 회피 * 지정한 방향으로 즉시 순간이동
|
|
// * 지정한 방향과 관계 없이 현재 방형의 반대 방향으로 백덤블링하며 회피 * 회피하지 않고 짧게 실드 이펙트가 발생하며 피해 무효(무적)
|
|
// * 회피하지 않고 즉시 적의 반대 방향으로 잔상과 함께 빠르게 이동」
|
|
//
|
|
// ■ 고르기 — 장착한 신발 GroupNo → WLDodgeStyle.json shoes 행의 style · 신발 없음/표에 없음 = defaultStyle
|
|
// ■ 동작 (spin · roll · blink · backflip 은 원본 PCActor.Play_Dodge 로 회피 상태 · 회피 쿨타임 · 흙먼지를 그대로 쓰고 위를 덮는다)
|
|
// spin = 원본 그대로(탭 방향으로 돌아 dodge 상태 클립 · 3 m)
|
|
// roll = spin 과 같은 이동 + 클립 덮어쓰기(PlayableGraph · rollClip 구간) — 회피 상태가 끝나면 덮어쓰기도 끝
|
|
// blink = 탭 방향 blinkDistance m 를 NavMesh 위로 즉시 Warp · 양 끝 blinkFx(원본 Effect_Teleport 의 WL 사본 · blinkFxHold 뒤 blinkFxFade 초에 투명 소멸 #989) · 흙먼지 없음 · 회피 상태는 다음 프레임에 끝
|
|
// backflip = 탭 방향 무시 · 바라보는 방향 그대로 등 뒤로 flipDistance m · flipClip 을 거꾸로(flipReverse) 덮어쓰기 + 몸(visualChild) 포물선 flipHeight m
|
|
// shield = 이동 · 동작 없음 · shieldFx(원본 Effect_ManaShield) shieldSeconds 초 + 그동안 피해 무효(MyActor.Get_Damage 훅 1줄 · 셈 = ShieldBlocked)
|
|
// ghost = 탭 방향 무시 · 공격 중인 대상(1순위) · 가장 가까운 적(2순위)의 반대 방향 ghostDistance m 를 ghostSeconds 초에 · 둘 다 없으면 바라보는 방향
|
|
// ghostNoTargetDistance m 이내 전진(#989) · 구르기 대신 ghostClip / ghostForwardClip 구간 · 잔상(DashAfterImage) ghostCount 개
|
|
// ■ 값 = Resources/WL/WLDodgeStyle.json · 클립 = Resources/WL/WLDodgeClipBank.asset(이름으로 찾기) (C45) · 원본 수정 = MyActor 훅 1줄
|
|
// 🔴 Debug.LogError 금지(원본 ErrorLogHook 이 팝업을 띄운다).
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
using System.Collections.Generic;
|
|
using System.Reflection;
|
|
using UnityEngine;
|
|
using UnityEngine.AI;
|
|
using UnityEngine.Animations;
|
|
using UnityEngine.Playables;
|
|
using WL.Equip;
|
|
|
|
namespace WL.Combat
|
|
{
|
|
[System.Serializable]
|
|
public class WLDodgeShoe
|
|
{
|
|
public int group;
|
|
public string style = "spin";
|
|
public string note = "";
|
|
}
|
|
|
|
[System.Serializable]
|
|
public class WLDodgeStyleJson
|
|
{
|
|
public int enabled = 1;
|
|
public string defaultStyle = "spin";
|
|
public WLDodgeShoe[] shoes = new WLDodgeShoe[0];
|
|
public int grantShoes = 1; // 1 = 시작 지급(WLStarterKit · 인벤토리만 · 장착 안 함)
|
|
public string clipBank = "WL/WLDodgeClipBank";
|
|
public string visualChild = "Group"; // 포물선 · 회전을 줄 몸 묶음(PC 루트 바로 밑 · 애니메이터는 루트에 있어 이 묶음은 안 움직인다)
|
|
public float accel = 200f; // 덮은 이동 동안 NavMeshAgent 가속(정해진 시간 안에 닿게) · 끝나면 원래 값
|
|
// roll
|
|
public string rollClip = "";
|
|
public float rollSeconds = 0.5f;
|
|
public float rollFrom = 0f, rollTo = 1f; // 클립 정규화 구간
|
|
// blink
|
|
public float blinkDistance = 4f;
|
|
public float blinkSnap = 2f;
|
|
public string blinkFx = "Effect_Teleport"; // Resources 경로(WL 사본)면 hold · fade 로 투명하게 소멸 · 아니면 원본 Addressables 이펙트
|
|
public float blinkFxHold = 0.35f, blinkFxFade = 0.55f;
|
|
// backflip
|
|
public string flipClip = "";
|
|
public int flipReverse = 1;
|
|
public float flipFrom = 0f, flipTo = 1f;
|
|
public float flipSeconds = 0.55f;
|
|
public float flipDistance = 3f;
|
|
public float flipHeight = 0.8f;
|
|
public float flipSpin = 0f; // 클립에 회전이 없을 때 몸 묶음을 등 쪽으로 도는 각(°)
|
|
public float flipCenter = 0.5f; // 회전 중심 높이(m)
|
|
// shield
|
|
public float shieldSeconds = 0.6f;
|
|
public string shieldFx = "Effect_ManaShield";
|
|
public int shieldFxLocation = 1; // eEffectLocation Top 0 · Mid 1 · Bottom 2
|
|
public float shieldFxScale = 1f;
|
|
// ghost
|
|
public float ghostDistance = 4f;
|
|
public float ghostSeconds = 0.18f;
|
|
public int ghostCount = 4;
|
|
public string ghostClip = "";
|
|
public float ghostFrom = 0f, ghostTo = 1f;
|
|
public float ghostNoTargetDistance = 3f; // #989 대상 · 적 없음 = 바라보는 방향으로 이 거리 이내 전진(막히면 그 앞까지)
|
|
public string ghostForwardClip = "";
|
|
public float ghostForwardFrom = 0f, ghostForwardTo = 1f;
|
|
public float ghostRadius = 12f; // 「적」 찾기 반경(m)
|
|
public string note = "";
|
|
}
|
|
|
|
public static class WLDodgeStyle
|
|
{
|
|
public const string ResourcesPath = "WL/WLDodgeStyle";
|
|
public static int Spins, Rolls, Blinks, Flips, Shields, Ghosts, ShieldBlocked, GhostSpawned, ClipMissing;
|
|
public static string LastLog = "", LastStyle = "";
|
|
public static Vector3 LastStart, LastEnd;
|
|
|
|
static WLDodgeStyleJson s_cfg;
|
|
static WLDodgeClipBank s_bank;
|
|
static WLDodgeStyleRunner s_runner;
|
|
|
|
static readonly MethodInfo s_setMove = typeof(Actor).GetMethod("Set_Move", BindingFlags.Instance | BindingFlags.NonPublic);
|
|
static readonly MethodInfo s_isCool = typeof(Actor).GetMethod("IsAvoidCoolTime", BindingFlags.Instance | BindingFlags.NonPublic);
|
|
static readonly MethodInfo s_setCool = typeof(Actor).GetMethod("Set_AvoidCoolTime", BindingFlags.Instance | BindingFlags.NonPublic);
|
|
|
|
public static WLDodgeStyleJson Cfg
|
|
{
|
|
get
|
|
{
|
|
if (s_cfg != null) return s_cfg;
|
|
s_cfg = new WLDodgeStyleJson();
|
|
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;
|
|
}
|
|
}
|
|
|
|
static AnimationClip Clip(string name)
|
|
{
|
|
if (s_bank == null) s_bank = Resources.Load<WLDodgeClipBank>(Cfg.clipBank);
|
|
var c = s_bank != null ? s_bank.Find(name) : null;
|
|
if (c == null && !string.IsNullOrEmpty(name)) ClipMissing++;
|
|
return c;
|
|
}
|
|
|
|
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
|
static void Boot()
|
|
{
|
|
s_cfg = null; s_bank = null; s_invulPc = null; s_invulUntil = 0f;
|
|
Spins = Rolls = Blinks = Flips = Shields = Ghosts = ShieldBlocked = GhostSpawned = ClipMissing = 0;
|
|
}
|
|
|
|
static void EnsureRunner()
|
|
{
|
|
if (s_runner != null) return;
|
|
var go = new GameObject("[WL988] DodgeStyle");
|
|
go.hideFlags = HideFlags.HideAndDontSave;
|
|
s_runner = go.AddComponent<WLDodgeStyleRunner>();
|
|
}
|
|
|
|
// ── 신발 ─────────────────────────────────────────────────────────
|
|
/// <summary>장착 신발 → 동작 이름(신발 없음 · 표에 없음 = defaultStyle).</summary>
|
|
public static string CurrentStyle()
|
|
{
|
|
var c = Cfg;
|
|
if (c.enabled == 0) return "spin";
|
|
var shoe = WLInventory.Loaded ? WLInventory.EquippedInSlot(eFREquipmentType.Shose) : null;
|
|
if (shoe != null && shoe.itemJson != null && c.shoes != null)
|
|
for (int i = 0; i < c.shoes.Length; i++)
|
|
if (c.shoes[i] != null && c.shoes[i].group == shoe.itemJson.GroupNo) return c.shoes[i].style;
|
|
return c.defaultStyle;
|
|
}
|
|
|
|
/// <summary>표의 신발 그룹(순서 그대로) — 시작 지급 · 테스트 씬 순환.</summary>
|
|
public static List<int> ShoeGroups()
|
|
{
|
|
var r = new List<int>();
|
|
var c = Cfg;
|
|
if (c.shoes != null) foreach (var s in c.shoes) if (s != null && s.group > 0 && !r.Contains(s.group)) r.Add(s.group);
|
|
return r;
|
|
}
|
|
|
|
/// <summary>테스트 씬: 표 순서로 다음 신발을 장착(가진 것 중) — 메시지.</summary>
|
|
public static string NextShoe(int step)
|
|
{
|
|
if (!WLInventory.EnsureLoaded()) return "인벤토리 미로드";
|
|
var groups = ShoeGroups();
|
|
if (groups.Count == 0) return "신발 표 없음";
|
|
var cur = WLInventory.EquippedInSlot(eFREquipmentType.Shose);
|
|
int idx = cur != null && cur.itemJson != null ? groups.IndexOf(cur.itemJson.GroupNo) : -1;
|
|
for (int k = 1; k <= groups.Count; k++)
|
|
{
|
|
int g = groups[((idx + step * k) % groups.Count + groups.Count) % groups.Count];
|
|
foreach (var it in WLInventory.SortedItems())
|
|
if (it != null && it.itemJson != null && it.itemJson.GroupNo == g && WLInventory.Equip(it.rawItem._id))
|
|
return "신발 " + it.itemJson.Item_name + "(" + g + ") → 회피 " + CurrentStyle();
|
|
}
|
|
return "가진 신발 없음";
|
|
}
|
|
|
|
/// <summary>#1008 테스트 씬: 표 순서 number 번째(1부터) 신발을 장착(가진 것 중) — 메시지.</summary>
|
|
public static string SelectShoe(int number)
|
|
{
|
|
if (!WLInventory.EnsureLoaded()) return "인벤토리 미로드";
|
|
var groups = ShoeGroups();
|
|
if (number < 1 || number > groups.Count) return "신발 " + number + "번 없음(1~" + groups.Count + ")";
|
|
int g = groups[number - 1];
|
|
var cur = WLInventory.EquippedInSlot(eFREquipmentType.Shose);
|
|
if (cur != null && cur.itemJson != null && cur.itemJson.GroupNo == g) return "이미 신발 " + number + " " + cur.itemJson.Item_name + " → 회피 " + CurrentStyle();
|
|
foreach (var it in WLInventory.SortedItems())
|
|
if (it != null && it.itemJson != null && it.itemJson.GroupNo == g && WLInventory.Equip(it.rawItem._id))
|
|
return "신발 " + number + " " + it.itemJson.Item_name + "(" + g + ") → 회피 " + CurrentStyle();
|
|
return "신발 " + number + "번(" + g + ") 장착 실패 · " + WLInventory.LastMessage;
|
|
}
|
|
|
|
/// <summary>#1008 테스트 씬 화면 목록 한 줄 — 「1 spin 2 roll …」(표 순서 · 장착 중 = [ ]).</summary>
|
|
public static string ShoeLine()
|
|
{
|
|
var groups = ShoeGroups(); var c = Cfg;
|
|
var cur = WLInventory.Loaded ? WLInventory.EquippedInSlot(eFREquipmentType.Shose) : null;
|
|
int curG = cur != null && cur.itemJson != null ? cur.itemJson.GroupNo : -1;
|
|
var sb = new System.Text.StringBuilder();
|
|
for (int i = 0; i < groups.Count; i++)
|
|
{
|
|
string style = c.defaultStyle;
|
|
foreach (var s in c.shoes) if (s != null && s.group == groups[i]) { style = s.style; break; }
|
|
if (i > 0) sb.Append(" ");
|
|
bool on = groups[i] == curG;
|
|
sb.Append(on ? "[" : "").Append(i < 9 ? (i + 1).ToString() : i == 9 ? "0" : "-").Append(' ').Append(style).Append(on ? "]" : "");
|
|
}
|
|
return sb.ToString();
|
|
}
|
|
|
|
// ── 무적(shield) ──────────────────────────────────────────────────
|
|
static Actor s_invulPc;
|
|
static float s_invulUntil;
|
|
|
|
/// <summary>MyActor.Get_Damage 훅 — 실드 동안 피해 무효(막은 수를 센다).</summary>
|
|
public static bool Invulnerable(Actor a)
|
|
{
|
|
if (a == null || a != s_invulPc || Time.time >= s_invulUntil) return false;
|
|
ShieldBlocked++;
|
|
return true;
|
|
}
|
|
|
|
public static bool ShieldActive { get { return s_invulPc != null && Time.time < s_invulUntil; } }
|
|
|
|
// ── 회피 ─────────────────────────────────────────────────────────
|
|
/// <summary>WLDoubleTapDodge 가 부른다 — dir = 탭 방향(수평 · 정규화). 성공 여부 · 기록.</summary>
|
|
public static bool Do(PCActor pc, Vector3 dir, out string log)
|
|
{
|
|
var c = Cfg;
|
|
WLAnimEventSink.Ensure(pc.m_animation != null ? pc.m_animation.gameObject : pc.gameObject); // #989 원본 dodge 클립 'Land' 이벤트 받는 곳
|
|
string style = CurrentStyle();
|
|
LastStyle = style;
|
|
LastStart = pc.Get_position();
|
|
var fwd = pc.transform.forward; fwd.y = 0f; if (fwd.sqrMagnitude < 1e-6f) fwd = Vector3.forward; fwd.Normalize();
|
|
bool ok;
|
|
switch (style)
|
|
{
|
|
case "roll": ok = Dodge(pc, dir, true); if (ok) { Rolls++; StartOverlay(pc, Clip(c.rollClip), c.rollFrom, c.rollTo, false, c.rollSeconds, 0f, 0f); } break;
|
|
case "blink": ok = Blink(pc, dir); break;
|
|
case "backflip": ok = Flip(pc, -fwd); break;
|
|
case "shield": ok = Shield(pc); break;
|
|
case "ghost": ok = Ghost(pc, fwd); break;
|
|
default: style = "spin"; ok = Dodge(pc, dir, true); if (ok) Spins++; break;
|
|
}
|
|
log = style + (ok ? "" : " 안 됨(회피 쿨타임 · 이미 회피 중 등)");
|
|
LastLog = log;
|
|
return ok;
|
|
}
|
|
|
|
/// <summary>원본 회피(쿨타임 · 상태 · 흙먼지 · 앞 3 m). turn = 탭 방향으로 돌기 · 실패면 방향 되돌림(#923).</summary>
|
|
static bool Dodge(PCActor pc, Vector3 dir, bool turn)
|
|
{
|
|
var rot0 = pc.transform.rotation;
|
|
if (turn) pc.transform.rotation = Quaternion.LookRotation(dir);
|
|
pc.Play_Dodge();
|
|
if (pc.Get_CurAnim() == eAnim.Dodge) return true;
|
|
pc.transform.rotation = rot0;
|
|
return false;
|
|
}
|
|
|
|
static bool Blink(PCActor pc, Vector3 dir)
|
|
{
|
|
var c = Cfg;
|
|
s_noDustUntil = Time.time + 0.2f;
|
|
if (!Dodge(pc, dir, true)) { s_noDustUntil = 0f; return false; }
|
|
var start = pc.Get_position();
|
|
var to = start + dir * c.blinkDistance;
|
|
NavMeshHit hit;
|
|
if (NavMesh.SamplePosition(to, out hit, Mathf.Max(0.1f, c.blinkSnap), NavMesh.AllAreas)) to = hit.position; else to = start;
|
|
ShowBlinkFx(start);
|
|
pc.Set_Warp(to);
|
|
if (s_setMove != null) s_setMove.Invoke(pc, new object[] { to }); // 제자리 경로 → 다음 프레임 원본 이동 끝 처리(달리기 → 멈춤) = 회피 상태 끝
|
|
pc.AnimationPlay(MyValue.Get_AnimName(eAnim.Idle));
|
|
ShowBlinkFx(to);
|
|
LastEnd = to;
|
|
Blinks++;
|
|
return true;
|
|
}
|
|
|
|
static bool Flip(PCActor pc, Vector3 back)
|
|
{
|
|
var c = Cfg;
|
|
if (!Dodge(pc, back, false)) return false;
|
|
var to = pc.Get_position() + back * c.flipDistance;
|
|
MoveFor(pc, to, c.flipSeconds);
|
|
StartOverlay(pc, Clip(c.flipClip), c.flipFrom, c.flipTo, c.flipReverse != 0, c.flipSeconds, c.flipHeight, c.flipSpin);
|
|
LastEnd = to;
|
|
Flips++;
|
|
return true;
|
|
}
|
|
|
|
static bool Shield(PCActor pc)
|
|
{
|
|
var c = Cfg;
|
|
if (pc.IsDead()) return false;
|
|
if (s_isCool != null && (bool)s_isCool.Invoke(pc, null)) return false;
|
|
if (s_setCool != null) s_setCool.Invoke(pc, null);
|
|
s_invulPc = pc;
|
|
s_invulUntil = Time.time + c.shieldSeconds;
|
|
if (!string.IsNullOrEmpty(c.shieldFx) && InGameInfo.Ins != null)
|
|
{
|
|
float k = c.shieldFxScale;
|
|
InGameInfo.Ins.Show_Effect(c.shieldFx, pc, (eEffectLocation)Mathf.Clamp(c.shieldFxLocation, 0, 2), c.shieldSeconds, t => { if (t != null) ScaleFx(t.gameObject, k); });
|
|
}
|
|
LastEnd = pc.Get_position();
|
|
Shields++;
|
|
return true;
|
|
}
|
|
|
|
/// <summary>#989 PD 「어느 방향으로 회피를 시도하든 항상 공격 중인 대상(1순위) 또는 가장 가까운 적(2순위)의 반대 방향 · 대상이 없을 경우 3m 이내 전진」</summary>
|
|
static bool Ghost(PCActor pc, Vector3 fwd)
|
|
{
|
|
var c = Cfg;
|
|
var start = pc.Get_position();
|
|
string from;
|
|
var enemy = GhostEnemy(pc, c.ghostRadius, out from); // 원본 Play_Dodge 가 대상을 지우므로(Set_AutoBraking → Del_Target) 그 전에 읽는다
|
|
Vector3 dir = fwd; float dist = c.ghostNoTargetDistance; bool away = false;
|
|
if (enemy != null) { var d = start - enemy.Get_position(); d.y = 0f; if (d.sqrMagnitude > 1e-4f) { dir = d.normalized; dist = c.ghostDistance; away = true; } }
|
|
if (!Dodge(pc, dir, false)) return false;
|
|
var to = start + dir * dist;
|
|
MoveFor(pc, to, c.ghostSeconds * dist / Mathf.Max(0.1f, c.ghostDistance));
|
|
// 구르기 대신 미끄러지는 자세 — 적 반대쪽 = 뒤로 기운 ghostClip 구간 · 대상 없음(전진) = 앞으로 기운 ghostForwardClip 구간
|
|
// (AnimationPlay(idle) 은 상태 머신이 회피 상태를 끝내 이동이 1.4 m 에서 끊겼다 · #988 1차 Play)
|
|
if (away) StartOverlay(pc, Clip(c.ghostClip), c.ghostFrom, c.ghostTo, false, c.ghostSeconds, 0f, 0f);
|
|
else StartOverlay(pc, Clip(c.ghostForwardClip), c.ghostForwardFrom, c.ghostForwardTo, false, c.ghostSeconds, 0f, 0f);
|
|
GhostFrom = enemy != null ? from + " " + enemy.name : "대상 없음 → 전진";
|
|
GhostDir = dir; GhostEnemyPos = enemy != null ? enemy.Get_position() : start;
|
|
s_ghostPc = pc; s_ghostLeft = Mathf.Max(0, c.ghostCount); s_ghostNext = Time.time; s_ghostGap = c.ghostSeconds / Mathf.Max(1, c.ghostCount);
|
|
EnsureRunner();
|
|
LastEnd = to;
|
|
Ghosts++;
|
|
return true;
|
|
}
|
|
|
|
public static string GhostFrom = "";
|
|
public static Vector3 GhostDir, GhostEnemyPos; // 마지막 잔상 이동의 방향 · 그때 기준 적 자리(검증용)
|
|
|
|
/// <summary>1순위 = 공격 중인 대상(m_Target) · 2순위 = 반경 안 가장 가까운 적.</summary>
|
|
static Actor GhostEnemy(PCActor pc, float radius, out string from)
|
|
{
|
|
var t = pc.Get_Target(); // m_Target 은 속성(필드 아님 · #989 1차 Play 에서 리플렉션 필드가 비어 1순위가 안 읽혔다)
|
|
if (t != null && !t.IsDead() && t.gameObject.activeInHierarchy) { from = "공격 대상"; return t; }
|
|
from = "가장 가까운 적";
|
|
return NearestEnemy(pc, radius);
|
|
}
|
|
|
|
static Actor NearestEnemy(PCActor pc, float radius)
|
|
{
|
|
Actor best = null;
|
|
var my = pc.Get_position();
|
|
if (ActorInfo.Ins != null)
|
|
{
|
|
var list = ActorInfo.Ins.Get_EnemyActors(pc.IsEnemy(), my, radius);
|
|
float bd = float.MaxValue;
|
|
for (int i = 0; i < list.Count; i++)
|
|
{
|
|
var a = list[i]; if (a == null || a.IsDead() || !a.gameObject.activeInHierarchy) continue;
|
|
var d = a.Get_position() - my; d.y = 0f;
|
|
if (d.sqrMagnitude < bd) { bd = d.sqrMagnitude; best = a; }
|
|
}
|
|
ActorInfo.Ins.Release_Actors(list);
|
|
}
|
|
return best;
|
|
}
|
|
|
|
/// <summary>원본 회피 이동을 덮는다 — 목적지 · 속도(거리 / 초) · 가속(끝나면 원래 값).</summary>
|
|
static void MoveFor(PCActor pc, Vector3 to, float seconds)
|
|
{
|
|
var agent = pc.GetComponent<NavMeshAgent>();
|
|
if (agent == null || s_setMove == null) return;
|
|
NavMeshHit hit;
|
|
if (NavMesh.Raycast(pc.Get_position(), to, out hit, NavMesh.AllAreas)) to = hit.position; // #989 막힌 곳(섬 가장자리 · 벽) 앞까지만 = 「3 m 이내」
|
|
else if (NavMesh.SamplePosition(to, out hit, 2f, NavMesh.AllAreas)) to = hit.position;
|
|
s_setMove.Invoke(pc, new object[] { to });
|
|
float dist = Vector3.Distance(pc.Get_position(), to);
|
|
agent.speed = dist / Mathf.Max(0.05f, seconds);
|
|
if (s_accelPc != pc) { s_accel0 = agent.acceleration; s_accelPc = pc; }
|
|
agent.acceleration = Cfg.accel;
|
|
EnsureRunner();
|
|
}
|
|
|
|
static PCActor s_accelPc; static float s_accel0;
|
|
|
|
// ── 흙먼지 · 이펙트 ──────────────────────────────────────────────
|
|
static float s_noDustUntil;
|
|
/// <summary>WLDoubleTapDodge.AdjustDust 가 묻는다 — 순간이동은 흙먼지 없음.</summary>
|
|
public static bool SuppressDust { get { return Time.time < s_noDustUntil; } }
|
|
|
|
static readonly Dictionary<string, GameObject> s_fxPrefab = new Dictionary<string, GameObject>();
|
|
|
|
/// <summary>#989 PD 「회피(탄막)는 1초 이내 자연스럽게 소멸(투명해지며 소멸)」 — WL 사본(Resources)을 띄워 blinkFxHold 뒤 blinkFxFade 초 동안 투명하게 지운다(WLFxFade).
|
|
/// 원본 풀 이펙트(Effect_Teleport · 5 s 반복)는 WLFxFade 가 끝에 오브젝트를 지워 풀이 깨지므로 쓰지 않는다 — Resources 경로가 아니면 원본 그대로 띄움.</summary>
|
|
static void ShowBlinkFx(Vector3 pos)
|
|
{
|
|
var c = Cfg;
|
|
if (string.IsNullOrEmpty(c.blinkFx)) return;
|
|
GameObject pf;
|
|
if (!s_fxPrefab.TryGetValue(c.blinkFx, out pf)) { pf = Resources.Load<GameObject>(c.blinkFx); s_fxPrefab[c.blinkFx] = pf; }
|
|
if (pf == null) { if (InGameInfo.Ins != null) InGameInfo.Ins.Show_Effect(c.blinkFx, pos); return; }
|
|
var go = Object.Instantiate(pf, pos, pf.transform.rotation);
|
|
Weapon.WLFxFade.Attach(go, c.blinkFxHold, c.blinkFxFade, true);
|
|
BlinkFxShown++;
|
|
}
|
|
|
|
public static int BlinkFxShown;
|
|
|
|
static void ScaleFx(GameObject fx, float k)
|
|
{
|
|
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;
|
|
}
|
|
|
|
// ── 클립 덮어쓰기 · 몸 포물선 ─────────────────────────────────────
|
|
static PlayableGraph s_graph;
|
|
static AnimationClipPlayable s_play;
|
|
static PCActor s_ovPc, s_ghostPc;
|
|
static AnimationClip s_ovClip;
|
|
static float s_ovT0, s_ovSec, s_ovFrom, s_ovTo, s_ovHeight, s_ovSpin;
|
|
static bool s_ovReverse;
|
|
static Transform s_vis;
|
|
static Vector3 s_visPos0;
|
|
static Quaternion s_visRot0;
|
|
static int s_ghostLeft;
|
|
static float s_ghostNext, s_ghostGap;
|
|
public static float OverlayU { get; private set; }
|
|
public static bool OverlayActive { get { return s_ovPc != null; } }
|
|
|
|
static void StartOverlay(PCActor pc, AnimationClip clip, float from, float to, bool reverse, float seconds, float height, float spin)
|
|
{
|
|
StopOverlay();
|
|
s_ovPc = pc; s_ovClip = clip; s_ovFrom = from; s_ovTo = to; s_ovReverse = reverse; s_ovSec = Mathf.Max(0.05f, seconds); s_ovT0 = Time.time;
|
|
s_ovHeight = height; s_ovSpin = spin; OverlayU = 0f;
|
|
if (clip != null && pc.m_animation != null)
|
|
{
|
|
s_graph = PlayableGraph.Create("WL988 Dodge");
|
|
s_graph.SetTimeUpdateMode(DirectorUpdateMode.GameTime);
|
|
var output = AnimationPlayableOutput.Create(s_graph, "Dodge", pc.m_animation);
|
|
s_play = AnimationClipPlayable.Create(s_graph, clip);
|
|
s_play.SetApplyFootIK(false);
|
|
s_play.SetSpeed(0);
|
|
s_play.SetTime((reverse ? to : from) * clip.length);
|
|
output.SetSourcePlayable(s_play);
|
|
s_graph.Play();
|
|
}
|
|
s_vis = (height != 0f || spin != 0f) ? pc.transform.Find(Cfg.visualChild) : null;
|
|
if (s_vis != null) { s_visPos0 = s_vis.localPosition; s_visRot0 = s_vis.localRotation; }
|
|
EnsureRunner();
|
|
}
|
|
|
|
static void StopOverlay()
|
|
{
|
|
if (s_graph.IsValid()) s_graph.Destroy();
|
|
if (s_vis != null) { s_vis.localPosition = s_visPos0; s_vis.localRotation = s_visRot0; s_vis = null; }
|
|
s_ovPc = null; s_ovClip = null;
|
|
}
|
|
|
|
internal static void Tick()
|
|
{
|
|
if (s_ovPc != null)
|
|
{
|
|
float u = (Time.time - s_ovT0) / s_ovSec;
|
|
bool over = s_ovPc == null || s_ovPc.IsDead() || !s_ovPc.gameObject.activeInHierarchy || u >= 1f || s_ovPc.Get_CurAnim() != eAnim.Dodge;
|
|
if (over) StopOverlay();
|
|
else
|
|
{
|
|
OverlayU = u;
|
|
if (s_ovClip != null && s_play.IsValid())
|
|
{
|
|
float n = Mathf.Lerp(s_ovFrom, s_ovTo, s_ovReverse ? 1f - u : u);
|
|
s_play.SetTime(n * s_ovClip.length);
|
|
}
|
|
if (s_vis != null)
|
|
{
|
|
var pivot = Vector3.up * Cfg.flipCenter;
|
|
var r = Quaternion.AngleAxis(-s_ovSpin * u, Vector3.right);
|
|
s_vis.localRotation = r * s_visRot0;
|
|
s_vis.localPosition = s_visPos0 + pivot - r * pivot + Vector3.up * (4f * s_ovHeight * u * (1f - u));
|
|
}
|
|
}
|
|
}
|
|
if (s_ghostPc != null && s_ghostLeft > 0 && Time.time >= s_ghostNext)
|
|
{
|
|
Reaction.DashAfterImage.ForceSpawn(s_ghostPc);
|
|
GhostSpawned++; s_ghostLeft--; s_ghostNext = Time.time + s_ghostGap;
|
|
if (s_ghostLeft <= 0) s_ghostPc = null;
|
|
}
|
|
if (s_accelPc != null && s_ovPc == null && s_ghostPc == null && s_accelPc.Get_CurAnim() != eAnim.Dodge)
|
|
{
|
|
var agent = s_accelPc.GetComponent<NavMeshAgent>();
|
|
if (agent != null) agent.acceleration = s_accel0;
|
|
s_accelPc = null;
|
|
}
|
|
}
|
|
|
|
internal static void Shutdown() { StopOverlay(); s_ghostPc = null; s_accelPc = null; }
|
|
}
|
|
|
|
/// <summary>WLDodgeStyle 의 프레임 처리(클립 시간 · 몸 포물선 · 잔상 · 가속 복원). 숨김 GameObject 1개.</summary>
|
|
internal sealed class WLDodgeStyleRunner : MonoBehaviour
|
|
{
|
|
void Update() { WLDodgeStyle.Tick(); }
|
|
void OnDestroy() { WLDodgeStyle.Shutdown(); }
|
|
}
|
|
}
|