558 lines
36 KiB
C#
558 lines
36 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
||
// WLSlashFlurry.cs — 돌진 베기(Skill_DashSlash) WL 방식: 사라지지 않고 1·2타 빠른 연속 베기 + 앞쪽 여러 번 타격
|
||
//
|
||
// PD 원문(#888 · 2026-10-17): 「한손검 전설 단계의 스킬 시전 시 캐릭터가 사라지지 않고 … 이펙트로 전방의 적을 여러번 공격하는 형태로
|
||
// 구성해줘. (… 이펙트 크기가 너무 크기 때문에 캐릭터 크기를 고려해 자연스럽게 수정해줘)」
|
||
// + 「돌진 베기 → 빠른 찌르기 연타가 아니라 1타와 2타 모션을 이펙트 재생시간 동안 빠르게 재생해 연속으로 빠르게 베는 연속 공격 컨셉으로 바꿔줘.」
|
||
//
|
||
// ■ 원리 — 원본 Skill_DashSlash.Set 의 WL 훅 1줄(Take)이 넘겨받는다: 투명화(Set_Invisible) · 순간이동(Set_Warp) · 카메라 따라가기를 하지 않고,
|
||
// 원본 이펙트(go_effect)와 판정 상자(BoxCollider)를 배율 scale 로 줄여 시전자 앞(forward · height)에 매 프레임 둔다(LateUpdate —
|
||
// 원본 Update 가 대상 위치로 옮긴 뒤에 덮는다). 타격 = 원본 간격 표(list_DamgeDelayTime)대로 상자를 껐다 켠다(원본과 같은 다단 히트).
|
||
// 모션 = 애니메이터 dashslash 사슬(1타 → 2타 → 1타 → 2타 → 대기 · 배속 · AgentScripts/WL888_Flurry.json) — 이 파일은 모션을 건드리지 않는다.
|
||
// ■ 값 = Resources/WL/WLSlashFlurry.json (C45) · 끄기 = enabled 0 → 원본 동작 · 원본 수정 = Skill_DashSlash.Set 1줄
|
||
// 🔴 Debug.LogError 금지(원본 ErrorLogHook 이 팝업을 띄운다).
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using System.Reflection;
|
||
using UnityEngine;
|
||
|
||
namespace WL.Combat.Weapon
|
||
{
|
||
[System.Serializable]
|
||
public class WLSlashFlurryJson
|
||
{
|
||
public int enabled = 0;
|
||
public int[] classes = new int[0]; // 비우면 모든 직업
|
||
public float scale = 1f; // 원본 이펙트 · 판정 상자 배율
|
||
public float forward = 1f; // 시전자 앞 거리(m)
|
||
public float height = 0f; // 시전자 발 기준 높이(m)
|
||
public float duration = 1f; // 이펙트를 켜 두는 시간(s) — 끝나면 투사체 Off
|
||
public float[] hitTimes = new float[0]; // #915 타격 시각(s · 시작 기준) — 비우면 원본 간격 표(list_DamgeDelayTime)
|
||
public float stepKeep = 0f; // #916 걸음(동작이 바뀌는 순간)마다 대상 몸 표면까지 이 거리(m)가 되도록 대상 쪽으로 전진 — 0 = 전진 없음
|
||
public float stepMax = 0.35f; // #916 한 걸음 최대 전진(m)
|
||
public float stepSeconds = 0.1f; // #916 한 걸음에 걸리는 시간(s)
|
||
// #921 PD 「직전 연타 공격 마무리 직후 검에 짧게 모으기 이펙트를 적용한 후 검을 내리치며 Slash Collection 이펙트를 마지막타에」 — 마무리 내려치기
|
||
public int finisher = 0; // 1 = 연타 동작 끝에서 이어서 마무리
|
||
public string finisherLastState = ""; // 연타 사슬의 마지막 상태 — 이 상태가 finisherFromNormalized 에 닿으면 시작
|
||
public float finisherFromNormalized = 0.85f;
|
||
public float finisherMaxAt = 1.0f; // 시작 기준 이 초가 지나도 못 잡으면 그때 시작(스킬 상태일 때만)
|
||
public float flurryFxOff = 1.05f; // 연타 이펙트(찌르기)를 끄는 시각(s · 시작 기준) — 6 번째 찌르기 전
|
||
public string finisherState = ""; // 내려치기 동작 상태
|
||
public float holdNormalized = 0.2f; // 모으기 동안 멈출 자세(검을 머리 위로)
|
||
public float chargeSeconds = 0.3f;
|
||
public float slamSpeed = 1f; // 내려칠 때 애니메이터 배속
|
||
public float impactNormalized = 0.36f; // 검이 앞 아래에 닿는 순간 = 마지막 타격 · 이펙트
|
||
public float endNormalized = 0.55f; // 여기까지 재생하고 대기
|
||
public string swordSocket = "w_onehand";
|
||
public string glowMaterial = "WL/Fx/WL_Mat_WeaponGlow";
|
||
public float[] glowColor = { 0.8f, 1.2f, 1.8f };
|
||
public float glowMax = 0.5f, glowFill = 0.3f, glowRimPower = 2.5f, glowFadeSeconds = 0.2f;
|
||
public string chargeFx = "";
|
||
public float chargeFxScale = 0.06f;
|
||
// #923 PD 「검 각성스킬 마지막 타 공격 모션에서 검에 기가 모이는 연출 및 검이 빛나게(해머 스킬 참고)」 — 모으기 동안 검 크기 1 → growScale(쥔 손 = 소켓 원점 기준 · 해머 각성과 같은 방식) · 내려친 뒤 shrinkSeconds 동안 원래 크기
|
||
public float growScale = 1f;
|
||
public float shrinkSeconds = 0.15f;
|
||
public string slashFx = "";
|
||
public float slashForward = 1f, slashHeight = 0f, slashScale = 0.35f, slashLife = 2f, slashYaw = 0f;
|
||
public int slashFaceCamera = 1; // 1 = 호가 카메라를 정면으로(에셋 데모처럼) · 0 = 시전자 방향
|
||
public float[] slashImpactLocal = new float[3]; // 이펙트 프리팹 안 땅 충격(얼음 가시) 자리(배율 1 · 로컬 m) — 이 자리가 내려친 지점(시전자 앞 slashForward)에 오게 옮긴다
|
||
public int slashFlipRight = 1; // 1 = 시전자가 화면 오른쪽을 보면 호를 180° 돌려 호가 시전자 쪽에서 와 적 쪽(충격 자리)에 꽂히게
|
||
// #923 PD 「검 각성스킬 마지막 공격의 궤적이 검이 내려치는 방향에 맞게」 — 1 = 호를 검이 지나는 면(시전자 앞 · 위)에 세운다(뒤 위에서 앞 바닥으로) · slashFaceCamera 보다 먼저
|
||
public int slashSwingPlane = 0;
|
||
public float slashTilt = 0f; // 그 면을 앞 방향 축으로 기울이는 각(도) — 화면을 더 마주 보는 쪽으로(앞뒤로 보는 각에서 호가 선처럼 얇게 보이지 않게)
|
||
// #924 PD 「검 각성 마지막타에 검이 내려치는 부위에 충격파를 해머 내려치기에 사용한 이펙트처럼 지면에 타격을 준 컨셉으로」 — 내려친 칼끝 아래 지면 충격
|
||
public int strikeAtTip = 0; // 1 = 타격 지점 = 내려친 순간 실제 칼끝(소켓에서 가장 먼 무기 메시 모서리)을 땅으로 내린 곳(베기 호 · 지면 충격 함께) · 0 = 시전자 앞 slashForward
|
||
public string groundFx = "";
|
||
public float groundFxScale = 0.45f, groundFxLife = 2.5f;
|
||
// #924 PD 「검기 각성 스킬 마지막타 모으기 기 이펙트가 좀 더 빛나는 느낌이 나야할 것 같아. (지금은 광택이 느껴지지 않음)」 — 모으기 동안 칼날 위 반짝임(별 반짝 + 빛)
|
||
public string chargeShineFx = "";
|
||
public float chargeShineScale = 0.4f;
|
||
public float chargeShineAlong = 0.6f; // 칼날 가운데(0) → 칼끝(1) 사이 위치
|
||
// #929 PD 「검 각성 스킬 마지막타 사용 직후 바로 다음 공격을 시도하게 하지 말고 0.5초 정도 내려친 상태로 딜레이를 주어 임펙트를 주도록」
|
||
public float impactHoldNormalized = 0.46f; // 마지막 타격 뒤 이 자세(검을 내려친 채)까지 재생하고
|
||
public float impactHoldSeconds = 0f; // 이 시간 동안 멈춘 뒤 대기(0 = 멈추지 않고 endNormalized 까지)
|
||
public int holdBlocksMove = 1; // 1 = 멈춘 동안 이동 입력 무시(구르기는 허용 = 스킵)
|
||
// #930 PD 「검(한손 검) 각성 스킬 중 찌르기 이펙트의 짙은 파란색을 검기 색상과 어울리게 조절해줘.」 — 원본 이펙트(FX_7Blue Stab) 렌더러 재질을 이름으로 찾아 WL 사본(Resources 경로)으로 바꿔 낀다(원본 에셋 그대로 · 넘겨받은 투사체만)
|
||
public WLFxMatSwap[] fxMaterialSwap = new WLFxMatSwap[0];
|
||
public string note = "";
|
||
}
|
||
|
||
[System.Serializable]
|
||
public class WLFxMatSwap { public string from = ""; public string to = ""; }
|
||
|
||
public static class WLSlashFlurry
|
||
{
|
||
public const string ResourcesPath = "WL/WLSlashFlurry";
|
||
public static int Taken, Windows, Errors, Steps, Finishers, FinisherHits, FinisherAborts, GroundFx, Holds, MatSwapped;
|
||
public static float LastHoldSeconds;
|
||
public static Vector3 LastStrike;
|
||
public static float LastFinisherStart, LastFinisherImpact;
|
||
public static float StepDistance;
|
||
public static string LastLog = "";
|
||
static WLSlashFlurryJson s_cfg;
|
||
|
||
internal static WLSlashFlurryJson Cfg
|
||
{
|
||
get
|
||
{
|
||
if (s_cfg != null) return s_cfg;
|
||
s_cfg = new WLSlashFlurryJson();
|
||
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; }
|
||
|
||
/// <summary>원본 Skill_DashSlash.Set 훅 — true 면 WL 이 이 투사체를 맡는다(원본 투명화 · 순간이동 건너뜀).</summary>
|
||
public static bool Take(ProjectileBase pb, GameObject effect, Collider col, List<float> delays)
|
||
{
|
||
try
|
||
{
|
||
var c = Cfg;
|
||
if (c.enabled == 0 || pb == null || effect == null || col == null) return false;
|
||
var data = pb.Get_ProjectTileData();
|
||
var shooter = data != null ? data.shooter : null;
|
||
if (DSUtil.CheckNull(shooter)) return false;
|
||
if (c.classes != null && c.classes.Length > 0)
|
||
{
|
||
var pc = shooter as PCActor;
|
||
int cls = pc != null ? ClassOf(pc) : 0;
|
||
if (System.Array.IndexOf(c.classes, cls) < 0) return false;
|
||
}
|
||
var run = pb.GetComponent<WLSlashFlurryRunner>();
|
||
if (run == null) run = pb.gameObject.AddComponent<WLSlashFlurryRunner>();
|
||
run.Begin(pb, shooter, data.target, effect, col, delays, c);
|
||
Taken++;
|
||
LastLog = "넘겨받음 #" + Taken + " · 배율 " + c.scale + " · 타격 " + (delays != null ? delays.Count : 0) + "회";
|
||
return true;
|
||
}
|
||
catch (System.Exception ex) { Errors++; LastLog = "넘겨받기 실패 — " + ex.Message; return false; }
|
||
}
|
||
|
||
static FieldInfo s_fId;
|
||
static int ClassOf(PCActor pc)
|
||
{
|
||
if (s_fId == null) s_fId = typeof(Actor).GetField("m_ID", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||
return s_fId != null ? (int)s_fId.GetValue(pc) : 0;
|
||
}
|
||
|
||
static MethodInfo s_off;
|
||
internal static void Off(ProjectileBase pb)
|
||
{
|
||
if (s_off == null) s_off = typeof(ProjectileBase).GetMethod("Off", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public, null, new[] { typeof(bool) }, null);
|
||
try { if (s_off != null) s_off.Invoke(pb, new object[] { true }); else pb.gameObject.SetActive(false); }
|
||
catch (System.Exception ex) { Errors++; LastLog = "Off 실패 — " + ex.Message; pb.gameObject.SetActive(false); }
|
||
}
|
||
}
|
||
|
||
/// <summary>넘겨받은 돌진 베기 투사체 1개의 진행(위치 · 배율 · 다단 히트 · 끝). 풀에서 다시 쓰이면 Begin 이 새로 시작한다.</summary>
|
||
internal sealed class WLSlashFlurryRunner : MonoBehaviour
|
||
{
|
||
ProjectileBase _pb; Actor _shooter; GameObject _effect; Collider _col; WLSlashFlurryJson _c;
|
||
Vector3 _baseScale; bool _hasBase, _active;
|
||
Coroutine _co, _fin;
|
||
// #921 마무리 내려치기
|
||
bool _finDone = true, _animOwned, _held; float _t0;
|
||
readonly WLWeaponGlowFx _glow = new WLWeaponGlowFx();
|
||
Transform _socket; Vector3 _swordLocal, _tipLocal; bool _hasTip; GameObject _charge, _shine;
|
||
readonly List<Transform> _parts = new List<Transform>(); readonly List<Vector3> _pos0 = new List<Vector3>(), _scl0 = new List<Vector3>(); float _grow = 1f;
|
||
static readonly Dictionary<string, GameObject> s_fx = new Dictionary<string, GameObject>();
|
||
static readonly Dictionary<string, Material> s_mats = new Dictionary<string, Material>();
|
||
|
||
/// <summary>#930 이펙트 렌더러 재질(본 재질 · 꼬리 재질)을 이름으로 찾아 JSON fxMaterialSwap 의 WL 사본으로 바꾼다(풀에서 다시 쓰여도 같은 결과).</summary>
|
||
static void SwapMats(GameObject effect, WLSlashFlurryJson c)
|
||
{
|
||
if (c.fxMaterialSwap == null || c.fxMaterialSwap.Length == 0) return;
|
||
foreach (var r in effect.GetComponentsInChildren<ParticleSystemRenderer>(true))
|
||
{
|
||
var m = Swap(r.sharedMaterial, c); if (m != null) r.sharedMaterial = m;
|
||
var t = Swap(r.trailMaterial, c); if (t != null) r.trailMaterial = t;
|
||
}
|
||
}
|
||
static Material Swap(Material m, WLSlashFlurryJson c)
|
||
{
|
||
if (m == null) return null;
|
||
foreach (var s in c.fxMaterialSwap)
|
||
{
|
||
if (s == null || s.from != m.name) continue;
|
||
Material to; if (!s_mats.TryGetValue(s.to, out to) || to == null) { to = Resources.Load<Material>(s.to); s_mats[s.to] = to; }
|
||
if (to == null) { WLSlashFlurry.LastLog = "재질 사본 없음 " + s.to; return null; }
|
||
WLSlashFlurry.MatSwapped++; return to;
|
||
}
|
||
return null;
|
||
}
|
||
// #916 걸음 전진
|
||
Actor _target; Animator _an; UnityEngine.AI.NavMeshAgent _agent; int _lastClip; Vector3 _stepVel; float _stepUntil = -1f;
|
||
|
||
public void Begin(ProjectileBase pb, Actor shooter, Actor target, GameObject effect, Collider col, List<float> delays, WLSlashFlurryJson c)
|
||
{
|
||
_pb = pb; _shooter = shooter; _effect = effect; _col = col; _c = c;
|
||
_target = target; var spc = shooter as PCActor; _an = spc != null ? spc.m_animation : null; _agent = shooter.Get_Agent(); _lastClip = 0; _stepUntil = -1f;
|
||
if (!_hasBase) { _baseScale = transform.localScale; _hasBase = true; }
|
||
if (!DSUtil.CheckNull(target))
|
||
{
|
||
var to = target.Get_position() - shooter.Get_position(); to.y = 0f;
|
||
if (to.sqrMagnitude > 1e-4f) shooter.transform.rotation = Quaternion.LookRotation(to);
|
||
}
|
||
foreach (var ps in effect.GetComponentsInChildren<ParticleSystem>(true)) { var m = ps.main; m.scalingMode = ParticleSystemScalingMode.Hierarchy; }
|
||
SwapMats(effect, c);
|
||
transform.localScale = _baseScale * c.scale;
|
||
_active = true;
|
||
Place();
|
||
if (_co != null) StopCoroutine(_co);
|
||
if (_fin != null) { StopCoroutine(_fin); _fin = null; }
|
||
_t0 = Time.time;
|
||
_finDone = !(c.finisher != 0 && !string.IsNullOrEmpty(c.finisherState) && _an != null);
|
||
_co = StartCoroutine(Co(delays));
|
||
if (!_finDone) _fin = StartCoroutine(Fin());
|
||
}
|
||
|
||
// ── #921 마무리 — 연타 동작 끝 → 검을 머리 위로(모으기 · 검 빛 · 모으기 이펙트) → 내려치기 → 마지막 타격 + Slash Collection 이펙트 → 대기 ──
|
||
IEnumerator Fin()
|
||
{
|
||
var c = _c; var an = _an; var pc = _shooter as PCActor;
|
||
// ① 시작 — 연타 사슬 마지막 상태가 finisherFromNormalized 에 닿거나 그 상태를 벗어나거나 finisherMaxAt 이 지나면(그때 스킬 상태여야)
|
||
// 마지막 상태에 들어서는 순간 클립 이벤트를 끈다 — 그 클립의 원본 대기 이벤트(≈ 0.8)가 스킬 상태를 먼저 풀어 마무리가 끊겼다(Play 실측 n 0.85 프레임에 이미 Idle)
|
||
bool seenLast = false;
|
||
while (true)
|
||
{
|
||
if (pc == null || pc.IsDead() || pc.Get_CurAnim() != eAnim.Skill) { FinEnd(false); yield break; }
|
||
var si = an.GetCurrentAnimatorStateInfo(0);
|
||
bool inLast = !string.IsNullOrEmpty(c.finisherLastState) && si.IsName(c.finisherLastState);
|
||
if (inLast && !seenLast) { seenLast = true; an.fireEvents = false; _animOwned = true; }
|
||
if ((inLast && si.normalizedTime >= c.finisherFromNormalized) || (seenLast && !inLast) || Time.time - _t0 >= c.finisherMaxAt) break;
|
||
yield return null;
|
||
}
|
||
WLSlashFlurry.Finishers++; WLSlashFlurry.LastFinisherStart = Time.time - _t0;
|
||
var foe = FrontEnemy();
|
||
if (foe != null) { var to = foe.Get_position() - pc.Get_position(); to.y = 0f; if (to.sqrMagnitude > 1e-4f) pc.transform.rotation = Quaternion.LookRotation(to); }
|
||
an.fireEvents = false; _animOwned = true; // 내려치기 클립의 원본 SkillEvent 등은 끈다(판정은 이 러너가)
|
||
an.Play(c.finisherState, 0, c.holdNormalized); an.speed = 0f; an.Update(0f);
|
||
// ② 모으기 — 검 빛 + 검 가운데 모으기 이펙트
|
||
_socket = null;
|
||
if (pc.tfs_weapon != null) foreach (var s in pc.tfs_weapon) if (s != null && s.name == c.swordSocket && s.gameObject.activeInHierarchy) { _socket = s; break; }
|
||
_hasTip = _socket != null && TipLocal(_socket, out _tipLocal);
|
||
if (_socket != null) { GrowBegin(); _glow.Begin(_socket, c.glowMaterial); _swordLocal = WLWeaponGlowFx.CenterLocal(_socket); }
|
||
_charge = Spawn(c.chargeFx, SwordCenter(), Quaternion.identity, c.chargeFxScale, c.chargeSeconds + 0.5f);
|
||
_shine = Spawn(c.chargeShineFx, ShinePoint(), Quaternion.identity, c.chargeShineScale, c.chargeSeconds + 2f);
|
||
var col = new Color(Get(c.glowColor, 0, 1f), Get(c.glowColor, 1, 1f), Get(c.glowColor, 2, 1f), 1f);
|
||
float t = 0f;
|
||
while (t < c.chargeSeconds)
|
||
{
|
||
if (FinBroken(pc)) { FinEnd(false); yield break; }
|
||
if (an.speed != 0f) an.speed = 0f;
|
||
var si = an.GetCurrentAnimatorStateInfo(0);
|
||
if (!si.IsName(c.finisherState) || Mathf.Abs(si.normalizedTime - c.holdNormalized) > 0.01f) an.Play(c.finisherState, 0, c.holdNormalized);
|
||
float k = Mathf.Clamp01(t / Mathf.Max(0.01f, c.chargeSeconds));
|
||
Grow(Mathf.Lerp(1f, c.growScale, k));
|
||
_glow.Set(c.glowMax * k, col, c.glowFill, c.glowRimPower);
|
||
if (_charge != null) _charge.transform.position = SwordCenter();
|
||
if (_shine != null) _shine.transform.position = ShinePoint();
|
||
t += Time.deltaTime; yield return null;
|
||
}
|
||
Grow(c.growScale);
|
||
_glow.Set(c.glowMax, col, c.glowFill, c.glowRimPower);
|
||
if (_charge != null) { Destroy(_charge); _charge = null; }
|
||
StopShine();
|
||
// ③ 내려치기
|
||
float sp = Mathf.Max(0.1f, c.slamSpeed);
|
||
an.Play(c.finisherState, 0, c.holdNormalized); an.speed = sp;
|
||
yield return null;
|
||
while (an.GetCurrentAnimatorStateInfo(0).normalizedTime < c.impactNormalized)
|
||
{
|
||
if (FinBroken(pc)) { FinEnd(false); yield break; }
|
||
if (an.speed != sp) an.speed = sp;
|
||
if (!an.GetCurrentAnimatorStateInfo(0).IsName(c.finisherState)) an.Play(c.finisherState, 0, c.holdNormalized);
|
||
yield return null;
|
||
}
|
||
// ④ 마지막 타격 + 이펙트
|
||
WLSlashFlurry.LastFinisherImpact = Time.time - _t0;
|
||
if (_effect != null && _effect.activeSelf) _effect.SetActive(false);
|
||
var fwd = pc.transform.forward; fwd.y = 0f; if (fwd.sqrMagnitude < 1e-4f) fwd = Vector3.forward; fwd.Normalize();
|
||
var rot = Quaternion.LookRotation(fwd);
|
||
var cam = Camera.main;
|
||
if (c.slashSwingPlane != 0)
|
||
{
|
||
// 프리팹의 호 = 로컬 XY 면 · 땅 충격 = 로컬 −X 쪽 → 로컬 Z = 시전자 오른쪽이면 −X = 앞(내려친 지점) · +X = 뒤 위(검이 들려 있던 쪽)
|
||
var right = Vector3.Cross(Vector3.up, fwd);
|
||
rot = Quaternion.LookRotation(right, Vector3.up);
|
||
if (c.slashTilt != 0f && cam != null)
|
||
{
|
||
var a = Quaternion.AngleAxis(c.slashTilt, fwd); var b = Quaternion.AngleAxis(-c.slashTilt, fwd); var cf = cam.transform.forward;
|
||
rot = (Mathf.Abs(Vector3.Dot(a * right, cf)) >= Mathf.Abs(Vector3.Dot(b * right, cf)) ? a : b) * rot;
|
||
}
|
||
}
|
||
else if (c.slashFaceCamera != 0 && cam != null)
|
||
{
|
||
var cf = cam.transform.forward; cf.y = 0f;
|
||
if (cf.sqrMagnitude > 1e-4f)
|
||
{
|
||
cf.Normalize();
|
||
bool right = Vector3.Dot(fwd, Vector3.Cross(Vector3.up, cf)) > 0f; // 시전자가 화면 오른쪽을 본다
|
||
rot = Quaternion.LookRotation(c.slashFlipRight != 0 && right ? -cf : cf);
|
||
}
|
||
}
|
||
rot = rot * Quaternion.Euler(0f, c.slashYaw, 0f);
|
||
var strike = pc.Get_position() + fwd * c.slashForward + Vector3.up * c.slashHeight;
|
||
if (c.strikeAtTip != 0 && _hasTip && _socket != null)
|
||
{
|
||
var tip = _socket.TransformPoint(_tipLocal * _grow); // 검이 커져 있으면 커진 칼끝
|
||
strike = new Vector3(tip.x, pc.Get_position().y + c.slashHeight, tip.z);
|
||
}
|
||
var groundAt = new Vector3(strike.x, pc.Get_position().y, strike.z);
|
||
if (!string.IsNullOrEmpty(c.groundFx)) { Spawn(c.groundFx, groundAt, Quaternion.LookRotation(fwd), c.groundFxScale, c.groundFxLife); WLSlashFlurry.GroundFx++; WLSlashFlurry.LastStrike = groundAt - pc.Get_position(); }
|
||
var impactLocal = new Vector3(Get(c.slashImpactLocal, 0, 0f), Get(c.slashImpactLocal, 1, 0f), Get(c.slashImpactLocal, 2, 0f));
|
||
Spawn(c.slashFx, strike - rot * (impactLocal * c.slashScale), rot, c.slashScale, c.slashLife);
|
||
_col.enabled = true; WLSlashFlurry.Windows++; WLSlashFlurry.FinisherHits++;
|
||
yield return new WaitForFixedUpdate();
|
||
yield return new WaitForFixedUpdate();
|
||
_col.enabled = false;
|
||
// ⑤ 끝 자세까지 · 빛 꺼짐 — #929 impactHoldSeconds > 0 이면 내려친 자세(impactHoldNormalized)에서 그 시간 멈춘 뒤 대기(멈춘 동안 이동 입력 무시 · 구르기 = 스킵)
|
||
float f0 = Time.time;
|
||
bool hold = c.impactHoldSeconds > 0f;
|
||
float stopN = hold ? Mathf.Max(c.impactNormalized, c.impactHoldNormalized) : c.endNormalized;
|
||
while (an.GetCurrentAnimatorStateInfo(0).normalizedTime < stopN && Time.time - f0 < 1f)
|
||
{
|
||
if (FinBroken(pc)) { FinEnd(false); yield break; }
|
||
if (an.speed != sp) an.speed = sp;
|
||
_glow.Set(c.glowMax * (1f - Mathf.Clamp01((Time.time - f0) / Mathf.Max(0.01f, c.glowFadeSeconds))), col, c.glowFill, c.glowRimPower);
|
||
Grow(Mathf.Lerp(c.growScale, 1f, Mathf.Clamp01((Time.time - f0) / Mathf.Max(0.01f, c.shrinkSeconds))));
|
||
yield return null;
|
||
}
|
||
if (hold)
|
||
{
|
||
float holdN = an.GetCurrentAnimatorStateInfo(0).normalizedTime, h0 = Time.time;
|
||
an.speed = 0f; _held = true; WLSlashFlurry.Holds++;
|
||
if (c.holdBlocksMove != 0) WLSkillCancel.HoldMove(pc, c.impactHoldSeconds);
|
||
while (Time.time - h0 < c.impactHoldSeconds)
|
||
{
|
||
if (FinBroken(pc)) { FinEnd(false); yield break; }
|
||
if (an.speed != 0f) an.speed = 0f;
|
||
var si = an.GetCurrentAnimatorStateInfo(0);
|
||
if (!si.IsName(c.finisherState) || Mathf.Abs(si.normalizedTime - holdN) > 0.01f) { an.Play(c.finisherState, 0, holdN); an.Update(0f); }
|
||
_glow.Set(c.glowMax * (1f - Mathf.Clamp01((Time.time - f0) / Mathf.Max(0.01f, c.glowFadeSeconds))), col, c.glowFill, c.glowRimPower);
|
||
Grow(Mathf.Lerp(c.growScale, 1f, Mathf.Clamp01((Time.time - f0) / Mathf.Max(0.01f, c.shrinkSeconds))));
|
||
yield return null;
|
||
}
|
||
WLSlashFlurry.LastHoldSeconds = Time.time - h0;
|
||
}
|
||
FinEnd(true);
|
||
}
|
||
|
||
bool FinBroken(PCActor pc) { return pc == null || pc.IsDead() || pc.Get_CurAnim() != eAnim.Skill; }
|
||
|
||
void FinEnd(bool toIdle)
|
||
{
|
||
if (!toIdle) WLSlashFlurry.FinisherAborts++;
|
||
if (_animOwned && _an != null) { _an.speed = 1f; _an.fireEvents = true; }
|
||
_animOwned = false;
|
||
var pc = _shooter as PCActor;
|
||
if (toIdle && pc != null && !pc.IsDead() && pc.Get_CurAnim() == eAnim.Skill) pc.Play_Idle();
|
||
_glow.End(); GrowEnd();
|
||
if (_charge != null) { Destroy(_charge); _charge = null; }
|
||
StopShine();
|
||
if (_held) { _held = false; WLSkillCancel.EndHoldMove(pc, toIdle); }
|
||
_fin = null; _finDone = true;
|
||
}
|
||
|
||
/// <summary>#924 반짝임 자리 — 칼날 가운데 → 칼끝 사이 chargeShineAlong(검이 커져 있으면 커진 자리).</summary>
|
||
Vector3 ShinePoint()
|
||
{
|
||
if (_socket == null) return SwordCenter();
|
||
var local = _hasTip ? Vector3.Lerp(_swordLocal, _tipLocal, Mathf.Clamp01(_c.chargeShineAlong)) : _swordLocal;
|
||
return _socket.TransformPoint(local * _grow);
|
||
}
|
||
|
||
void StopShine()
|
||
{
|
||
if (_shine == null) return;
|
||
foreach (var ps in _shine.GetComponentsInChildren<ParticleSystem>(true)) ps.Stop(true, ParticleSystemStopBehavior.StopEmitting);
|
||
Destroy(_shine, 1.2f); _shine = null;
|
||
}
|
||
|
||
Vector3 SwordCenter()
|
||
{
|
||
if (_socket != null) return _socket.TransformPoint(_swordLocal * _grow);
|
||
return _shooter != null ? _shooter.Get_Center_position() : transform.position;
|
||
}
|
||
|
||
static float Get(float[] a, int i, float d) { return a != null && a.Length > i ? a[i] : d; }
|
||
|
||
/// <summary>#924 칼끝(소켓 로컬) — 소켓 아래 켜진 메시 경계 모서리 중 소켓 원점에서 가장 먼 점(WLImpactRunner.SocketTip 과 같은 규칙 · 해머 각성도 이 방식).</summary>
|
||
static bool TipLocal(Transform socket, out Vector3 tipLocal)
|
||
{
|
||
tipLocal = Vector3.zero; float best = -1f;
|
||
foreach (var r in socket.GetComponentsInChildren<Renderer>(false))
|
||
{
|
||
if (r == null || !r.enabled || r.name == "[WL] Glow") continue;
|
||
var mf = r.GetComponent<MeshFilter>(); if (mf == null || mf.sharedMesh == null) continue;
|
||
var lb = mf.sharedMesh.bounds;
|
||
for (int k = 0; k < 8; k++)
|
||
{
|
||
var corner = lb.center + new Vector3((k & 1) == 0 ? -lb.extents.x : lb.extents.x, (k & 2) == 0 ? -lb.extents.y : lb.extents.y, (k & 4) == 0 ? -lb.extents.z : lb.extents.z);
|
||
var local = socket.InverseTransformPoint(r.transform.TransformPoint(corner)); float d = local.magnitude;
|
||
if (d > best) { best = d; tipLocal = local; }
|
||
}
|
||
}
|
||
return best > 0f;
|
||
}
|
||
|
||
// #923 검 크기 — 소켓 바로 아래 켜진 자식(검 모델)의 위치 · 크기를 함께 배율(쥔 손 = 소켓 원점 기준 · WLHammerQuake.Visual 과 같은 방식)
|
||
void GrowBegin()
|
||
{
|
||
GrowEnd(); if (_socket == null) return;
|
||
foreach (Transform ch in _socket) { if (!ch.gameObject.activeSelf) continue; _parts.Add(ch); _pos0.Add(ch.localPosition); _scl0.Add(ch.localScale); }
|
||
}
|
||
void Grow(float s)
|
||
{
|
||
_grow = s;
|
||
for (int i = 0; i < _parts.Count; i++) { var p = _parts[i]; if (p == null) continue; p.localPosition = _pos0[i] * s; p.localScale = _scl0[i] * s; }
|
||
}
|
||
void GrowEnd()
|
||
{
|
||
for (int i = 0; i < _parts.Count; i++) { var p = _parts[i]; if (p == null) continue; p.localPosition = _pos0[i]; p.localScale = _scl0[i]; }
|
||
_parts.Clear(); _pos0.Clear(); _scl0.Clear(); _grow = 1f;
|
||
}
|
||
|
||
static GameObject Spawn(string path, Vector3 pos, Quaternion rot, float scale, float life)
|
||
{
|
||
if (string.IsNullOrEmpty(path)) return null;
|
||
GameObject prefab;
|
||
if (!s_fx.TryGetValue(path, out prefab)) { prefab = Resources.Load<GameObject>(path); s_fx[path] = prefab; }
|
||
if (prefab == null) { WLSlashFlurry.LastLog = "이펙트 없음 " + path; return null; }
|
||
var go = Instantiate(prefab, pos, rot * prefab.transform.rotation);
|
||
foreach (var ps in go.GetComponentsInChildren<ParticleSystem>(true)) { var m = ps.main; m.scalingMode = ParticleSystemScalingMode.Hierarchy; }
|
||
go.transform.localScale = prefab.transform.localScale * Mathf.Max(0.01f, scale);
|
||
Destroy(go, Mathf.Max(0.1f, life));
|
||
return go;
|
||
}
|
||
|
||
void LateUpdate() { if (_active) { Step(); Place(); } }
|
||
|
||
/// <summary>#916 PD 「플레이어가 공격하며 적이 밀리는 거리를 고려해 스텝을 밟는 타이밍에 전진」 — 걸음 = 동작(클립)이 바뀌는 순간(1타 → 2타 → 1타 → 2타).
|
||
/// 그때 대상 몸 표면까지 거리가 stepKeep 보다 멀면(밀려난 만큼) 그 차이만큼(최대 stepMax) stepSeconds 동안 대상 쪽으로 다가가며 대상을 본다(NavMesh 위 이동).</summary>
|
||
void Step()
|
||
{
|
||
if (_c.stepKeep <= 0f || _an == null || DSUtil.CheckNull(_shooter) || _shooter.IsDead()) return;
|
||
var ci = _an.GetCurrentAnimatorClipInfo(0);
|
||
int clip = ci.Length > 0 && ci[0].clip != null ? ci[0].clip.GetInstanceID() : 0;
|
||
if (clip != 0 && clip != _lastClip)
|
||
{
|
||
_lastClip = clip;
|
||
var foe = _shooter.Get_CurAnim() == eAnim.Skill ? FrontEnemy() : null; // 지금 베는 적 = 정면 가장 가까운 적(투사체 대상과 다를 수 있다 · Play 실측)
|
||
if (foe != null)
|
||
{
|
||
var to = foe.Get_position() - _shooter.Get_position(); to.y = 0f;
|
||
float gap = to.magnitude - WL.Combat.WLCombatMotionSettings.BodyRadius(foe) - _c.stepKeep;
|
||
if (to.sqrMagnitude > 1e-4f) _shooter.transform.rotation = Quaternion.LookRotation(to);
|
||
if (gap > 0.01f)
|
||
{
|
||
float d = Mathf.Min(gap, _c.stepMax);
|
||
_stepVel = to.normalized * (d / Mathf.Max(0.02f, _c.stepSeconds));
|
||
_stepUntil = Time.time + _c.stepSeconds;
|
||
WLSlashFlurry.Steps++; WLSlashFlurry.StepDistance += d;
|
||
}
|
||
}
|
||
}
|
||
if (Time.time < _stepUntil && _agent != null && _agent.isActiveAndEnabled && _agent.isOnNavMesh) _agent.Move(_stepVel * Time.deltaTime);
|
||
}
|
||
|
||
/// <summary>시전자 정면(±60°) stepSearch m 안의 살아 있는 몹 중 가장 가까운 것 — 없으면 투사체 대상.</summary>
|
||
Actor FrontEnemy()
|
||
{
|
||
Actor best = null; float bd = float.MaxValue;
|
||
var info = ActorInfo.Ins;
|
||
if (info != null)
|
||
{
|
||
var me = _shooter.Get_position(); var fwd = _shooter.transform.forward; fwd.y = 0f; fwd.Normalize();
|
||
var list = info.Get_EnemyActors(_shooter.IsEnemy(), me, 3.5f);
|
||
if (list != null)
|
||
{
|
||
for (int i = 0; i < list.Count; i++)
|
||
{
|
||
var a = list[i];
|
||
if (a == null || a.IsDead() || !(a is MobActor)) continue;
|
||
var v = a.Get_position() - me; v.y = 0f; float dist = v.magnitude;
|
||
if (dist > 1e-3f && Vector3.Dot(v / dist, fwd) < 0.5f) continue;
|
||
if (dist < bd) { bd = dist; best = a; }
|
||
}
|
||
info.Release_Actors(list);
|
||
}
|
||
}
|
||
if (best == null && !DSUtil.CheckNull(_target) && !_target.IsDead()) best = _target;
|
||
return best;
|
||
}
|
||
|
||
void Place()
|
||
{
|
||
if (DSUtil.CheckNull(_shooter)) return;
|
||
var fwd = _shooter.transform.forward; fwd.y = 0f;
|
||
if (fwd.sqrMagnitude < 1e-6f) fwd = Vector3.forward;
|
||
fwd.Normalize();
|
||
transform.position = _shooter.Get_position() + fwd * _c.forward + Vector3.up * _c.height;
|
||
transform.rotation = Quaternion.LookRotation(fwd);
|
||
}
|
||
|
||
IEnumerator Co(List<float> delays)
|
||
{
|
||
float t0 = Time.time;
|
||
_effect.SetActive(true);
|
||
var times = _c.hitTimes;
|
||
if (times != null && times.Length > 0)
|
||
{
|
||
// #915 PD 「이펙트나 모션은 유지한 채 공격 타격수를 8회로」 — 원본 간격 표 대신 시작 기준 타격 시각(동작 약 0.84 초 안 · 동작이 끝나면 이펙트가 꺼진다 #914)
|
||
for (int i = 0; i < times.Length; i++)
|
||
{
|
||
_col.enabled = false;
|
||
float wait = t0 + times[i] - Time.time;
|
||
if (wait > 0f) yield return new WaitForSeconds(wait);
|
||
if (DSUtil.CheckNull(_shooter) || _shooter.IsDead()) break;
|
||
_col.enabled = true;
|
||
WLSlashFlurry.Windows++;
|
||
yield return new WaitForFixedUpdate();
|
||
yield return new WaitForFixedUpdate();
|
||
}
|
||
}
|
||
else if (delays != null)
|
||
for (int i = 0; i < delays.Count; i++)
|
||
{
|
||
_col.enabled = false;
|
||
yield return new WaitForSeconds(delays[i]);
|
||
if (DSUtil.CheckNull(_shooter) || _shooter.IsDead()) break;
|
||
_col.enabled = true;
|
||
WLSlashFlurry.Windows++;
|
||
// 원본은 1 프레임만 켰다 → 그 프레임에 물리 계산이 없으면 타격이 빠진다(0.3 배속 실측 7번 중 2~3번) — 물리 계산 2회 동안 켠다
|
||
yield return new WaitForFixedUpdate();
|
||
yield return new WaitForFixedUpdate();
|
||
}
|
||
_col.enabled = false;
|
||
// #921 마무리가 있으면 그 끝까지 기다린다(연타 이펙트는 flurryFxOff 에 끈다 · 마무리 내려치기 이펙트가 대신)
|
||
if (!_finDone)
|
||
{
|
||
while (!_finDone) { if (_effect != null && _effect.activeSelf && Time.time - t0 >= _c.flurryFxOff) _effect.SetActive(false); yield return null; }
|
||
_active = false; transform.localScale = _baseScale; _co = null;
|
||
WLSlashFlurry.Off(_pb);
|
||
yield break;
|
||
}
|
||
float left = _c.duration - (Time.time - t0);
|
||
if (left > 0f) yield return new WaitForSeconds(left);
|
||
_active = false;
|
||
transform.localScale = _baseScale;
|
||
_co = null;
|
||
WLSlashFlurry.Off(_pb);
|
||
}
|
||
|
||
void OnDisable()
|
||
{
|
||
_active = false; if (_hasBase) transform.localScale = _baseScale;
|
||
if (_fin != null) { StopCoroutine(_fin); _fin = null; }
|
||
if (!_finDone) FinEnd(false); // #921 이동 · 구르기 스킵으로 꺼지면 애니메이터 · 검 빛을 되돌린다
|
||
}
|
||
}
|
||
}
|