413 lines
27 KiB
C#
413 lines
27 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
|
// WLBladeStorm.cs — 대검 각성 「칼날 폭풍」(Skill_Whirlwind) WL 방식: 공격 자세로 회전 + 회오리 이펙트 반복 + 간격 피해 (#923 · 2026-10-18)
|
|
//
|
|
// PD 원문(#923): 「대검 각성 스킬 이펙트를 해당 에셋의 첨부한 스크린샷으로 교체하고, 플레이어 캐릭터가 6초 동안 공격 모션을 고정한 상태로
|
|
// 좌/우 반전하는 동안 이펙트를 반복 재생시켜줘. (플레이어는 이펙트 중앙에 배치된 상태로 마치 검을 회전하며 공격하는 형태로 동작해야 해)
|
|
// → 6초가 지나면 자연스럽게 회전을 멈추고 정지해야하며, 구르기를 하거나 스킬 유지 시간이 만료 된 경우 스킬을 종료해야 해.
|
|
// 이 스킬은 시전 중 움직일 수 있으며, 피격 범위에 있는 적은 0.5초 간격으로 피해를 받을 수 있어야 해.」
|
|
//
|
|
// ■ 원리 — 원본 Skill_Whirlwind.Set 의 WL 훅 1줄(Take)이 넘겨받는다: 원본 Co_Update(3 초 · 판정 상자 0.3 초 켬/끔 · 끝에 대기)를 돌리지 않고
|
|
// ① 원본 이펙트(BlueYellow_Sword 등)와 판정 상자를 끈다
|
|
// ② 조이스틱 상태 2(원본과 같음 — 시전 중 조이스틱 이동 · 달리기 동작 없이 · 자동 전투는 WLSkillFollow 가 가까운 적을 따라감)
|
|
// ③ 애니메이터 = poseState 를 poseNormalized 에 멈춘다(클립 이벤트 끔 · 다른 동작이 끼어들면 매 프레임 되돌림)
|
|
// ④ PC 를 spinDegPerSec 로 수평 회전(LateUpdate · 이동 방향으로 돌리는 원본 조이스틱 Update · WLSkillFollow 보다 나중)
|
|
// ⑤ 회오리 이펙트(fx)를 PC 발 중심에 fxRepeat 초마다 다시 띄운다(PC 를 따라감 · 중심 어긋남 fxOffset 보정)
|
|
// ⑥ hitInterval 초마다 PC 발 기준 radius m + 적 몸 반지름 안의 모든 적에게 원본 한 대 피해(ProcessCollider · 명중 이펙트 · 피격 반응 그대로)
|
|
// ⑦ duration 초가 지나면 판정 · 새 이펙트를 멈추고 stopSeconds 동안 회전 감속 → 투사체 끔 → 조이스틱 0 → 대기(원본 끝과 같은 순서)
|
|
// 구르기 등으로 스킬 상태를 벗어나면 즉시 끝(애니메이터 · 조이스틱 상태 되돌림 · 원본은 투사체가 꺼지면 Co_Update 가 함께 멈춰 조이스틱 상태 2 를
|
|
// 되돌리는 줄에 닿지 않는 구조 — 코드 확인 · Play 미재현)
|
|
// ■ #924 PD 「대검 각성 스킬 사용 중에는 이동 속도를 50% 줄이고, 이펙트가 몸을 완전히 가려지도록 이펙트로 캐릭터의 몸을 완전히 감싸줘.
|
|
// 그리고 노란색으로 깜박이며 퍼지는 이펙트는 숨겨줘.」
|
|
// ⑧ moveMul = 이 PC 의 이동 속도 배율(원본 Actor.Get_MoveSpeed 끝 훅 1줄 → 조이스틱 이동 · WLSkillFollow 자동 따라가기 둘 다)
|
|
// ⑨ wraps = 몸을 감싸는 반복 이펙트 목록(회전 검기 기둥 · Hyper Casual 연기 소용돌이 — 시작에 1 개씩 · PC 를 따라감)
|
|
// ⑪ fadeSync 1 = 유지 시간이 지나면 새 이펙트 · 판정을 멈추고, 남은 이펙트 전부(감싸기 · 회오리)를 fadeSeconds 동안 함께 흐려 지우며(WLFxFade = 방출 멈춤 +
|
|
// 입자 알파를 0 으로) 같은 시간 동안 회전을 감속 → 이펙트가 사라지는 그 순간 모션 끝(#924 PD 「이펙트 소멸과 모션 종료가 일치」)
|
|
// — 남은 입자 수명으로 기다리면 보이지 않는 긴 수명 입자(회전 검기 기둥 최상위 이미터 5 초) 때문에 이펙트가 다 사라진 뒤에도 모션이 남았다(Play 캡처)
|
|
// ⑩ hideBody = 시전 동안 PC 몸(무기 소켓 밖 SkinnedMeshRenderer — 팔 · 몸 · 얼굴 · 머리카락 · 머리 · 다리)을 숨긴다(검은 보임 · 끝 · 구르기에 되돌림)
|
|
// — 회오리 · 칼날 기둥은 반투명/가산 이펙트라 겹쳐도 몸이 비쳐 「완전히 가림」이 안 됐다(Play 시안 캡처: 1~3 겹 · 반투명 토네이도 · 반투명 재질판)
|
|
// · 노란 빛살 = 회오리 사본 WL_Fx_BladeStorm 의 floor_ring(시작색 0.98/0.96/0.75 · 초당 8 개 · 0.3 초 방사형 빛살)을 사본에서 끔
|
|
// ■ 값 = Resources/WL/WLBladeStorm.json (C45) · 끄기 = enabled 0 → 원본 동작 · 원본 수정 = Skill_Whirlwind.Set 1줄 · Actor.Get_MoveSpeed 1줄
|
|
// 🔴 Debug.LogError 금지(원본 ErrorLogHook 이 팝업을 띄운다).
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
namespace WL.Combat.Weapon
|
|
{
|
|
[System.Serializable]
|
|
public class WLBladeStormJson
|
|
{
|
|
public int enabled = 0;
|
|
public int[] classes = new int[0]; // 비우면 모든 직업
|
|
public float duration = 6f; // 회전 · 판정 유지(초)
|
|
public float hitInterval = 0.5f; // 피해 간격(초)
|
|
public float firstHit = 0.1f; // 첫 피해 시각(초)
|
|
public float radius = 2f; // 피해 반경(m · PC 발 기준 수평) + 적 몸 반지름
|
|
public string poseState = ""; // 멈출 공격 자세 상태(PC 애니메이터 0 층)
|
|
public float poseNormalized = 0.3f;
|
|
public float spinDegPerSec = 720f;
|
|
public int spinSign = -1; // 1 = 위에서 볼 때 시계 방향 · -1 = 반시계
|
|
public float spinUpSeconds = 0.15f; // 시작 가속(초)
|
|
public float stopSeconds = 0.5f; // 끝 감속(초) — 자연스럽게 멈춤
|
|
public string fx = ""; // 회오리 이펙트(Resources 경로)
|
|
public float fxScale = 0.45f;
|
|
public float fxRepeat = 0.5f; // 다시 띄우는 간격(초)
|
|
public float fxLife = 1.2f;
|
|
public float[] fxOffset = new float[3]; // 프리팹 소용돌이 중심의 원점 어긋남(배율 1 · m) — 빼서 PC 발이 정중앙에
|
|
public int hideOriginal = 1;
|
|
public float moveMul = 1f; // #924 시전 중 이 PC 의 이동 속도 배율
|
|
public WLBladeStormWrap[] wraps = new WLBladeStormWrap[0]; // #924 몸을 감싸는 반복 이펙트들
|
|
public int hideBody = 0; // #924 1 = 시전 동안 몸(무기 소켓 밖 SkinnedMeshRenderer)을 숨김
|
|
public int fadeSync = 0; // #924 1 = 유지 시간 뒤 남은 이펙트를 fadeSeconds 동안 흐려 지우며 같은 시간 감속 · 그 끝에 모션 끝
|
|
public float fadeSeconds = 1.5f;
|
|
public int fadeDimAdditive = 0; // #934 1 = 흐려 지울 때 가산(더하기) 재질 입자는 색도 함께 낮춤(알파만으론 회오리 가산 심이 밝은 채 남던 것)
|
|
// #929 PD 「대검 각성 스킬에 피격 된 적에게 히트 이펙트를 발생시켜 줘.」 — 판정마다 맞은 적 몸 중심에(시전자 → 적 방향)
|
|
public string hitFx = "";
|
|
public float hitFxScale = 0.3f;
|
|
public float hitFxLife = 1.2f;
|
|
// #930 PD 「캐릭터의 색상이 푸른색으로 변하지 않게 해줘.」 — 시전 동안 PC 몸 · 무기 재질을 사본으로 바꿔 렌더 순서를 이만큼 뒤로(툰 2000 → 3050 = 반투명 이펙트 3000 뒤에 그림)
|
|
// → 감싸기 · 회오리 · 칼날 이펙트가 몸 위를 덮어 몸 색이 물들지 않는다(이펙트는 몸 둘레에 그대로) · 끝 · 끊김에 원래 재질로 되돌리고 사본 파기 · 0 = 끔
|
|
public int bodyQueueAdd = 0;
|
|
// #932 PD 「대검 각성 스킬 시전 때 캐릭터의 색상이 바뀌지 않도록 할 수 없을까? (적어도 캐릭터의 상체가 좀 더 잘 보였으면 좋겠어.)」 — 이름이 이 접두어로 시작하는 렌더러만 뒤로(빈 배열 = 전부) · 무기 소켓 안 렌더러는 bodyOnTopWeapon 1 일 때만
|
|
public string[] bodyOnTopNames = new string[0];
|
|
public int bodyOnTopWeapon = 0;
|
|
public string note = "";
|
|
}
|
|
|
|
[System.Serializable]
|
|
public class WLBladeStormWrap
|
|
{
|
|
public string fx = ""; // Resources 경로(반복 재생 프리팹)
|
|
public float scale = 0.45f;
|
|
public float height = 0f; // PC 발 기준 높이(m)
|
|
// #927 위에서 비스듬히 보는 카메라에선 몸 가운데 깊이의 반투명 이펙트가 몸 앞면에 가려 몸이 이펙트 앞에 보인다 →
|
|
// 카메라 쪽 시선 방향으로 이만큼(m) 당겨 화면 위치는 그대로 두고 깊이만 몸보다 앞에(0 = 당기지 않음)
|
|
public float towardCamera = 0f;
|
|
}
|
|
|
|
public static class WLBladeStorm
|
|
{
|
|
public const string ResourcesPath = "WL/WLBladeStorm";
|
|
public static int Taken, Ticks, Hits, FxSpawned, Ends, Aborts, Errors;
|
|
public static float LastSpinTotal, LastSeconds, LastFade;
|
|
public static int HitFxShown, BodyOnTop;
|
|
public static string LastLog = "";
|
|
static WLBladeStormJson s_cfg;
|
|
static Actor s_slowActor; static float s_slowMul = 1f;
|
|
|
|
/// <summary>#924 원본 Actor.Get_MoveSpeed 끝 훅 — 칼날 폭풍 중인 그 PC 만 moveMul(그 밖 = 1).</summary>
|
|
public static float MoveMul(Actor a) { return ReferenceEquals(a, s_slowActor) ? s_slowMul : 1f; }
|
|
internal static void SetSlow(Actor a, float mul) { s_slowActor = a; s_slowMul = Mathf.Clamp(mul, 0.05f, 1f); }
|
|
internal static void ClearSlow(Actor a) { if (ReferenceEquals(a, s_slowActor)) { s_slowActor = null; s_slowMul = 1f; } }
|
|
|
|
internal static WLBladeStormJson Cfg
|
|
{
|
|
get
|
|
{
|
|
if (s_cfg != null) return s_cfg;
|
|
s_cfg = new WLBladeStormJson();
|
|
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) { s_cfg.enabled = 0; LastLog = "설정 파싱 실패 — " + ex.Message; }
|
|
return s_cfg;
|
|
}
|
|
}
|
|
|
|
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
|
static void Boot() { s_cfg = null; s_slowActor = null; s_slowMul = 1f; }
|
|
|
|
/// <summary>원본 Skill_Whirlwind.Set 훅 — true 면 WL 이 이 투사체를 맡는다(원본 Co_Update 건너뜀).</summary>
|
|
public static bool Take(ProjectileBase pb, GameObject hitChecker)
|
|
{
|
|
try
|
|
{
|
|
var c = Cfg;
|
|
if (c.enabled == 0 || pb == null) return false;
|
|
var data = pb.Get_ProjectTileData();
|
|
var pc = data != null ? data.shooter as PCActor : null;
|
|
if (pc == null || !pc.IsMainPC()) return false;
|
|
if (c.classes != null && c.classes.Length > 0 && System.Array.IndexOf(c.classes, pc.Get_ID()) < 0) return false;
|
|
var run = pb.GetComponent<WLBladeStormRunner>();
|
|
if (run == null) run = pb.gameObject.AddComponent<WLBladeStormRunner>();
|
|
run.Begin(pb, pc, hitChecker, c);
|
|
Taken++; LastLog = "넘겨받음 #" + Taken;
|
|
return true;
|
|
}
|
|
catch (System.Exception ex) { Errors++; LastLog = "넘겨받기 실패 — " + ex.Message; return false; }
|
|
}
|
|
}
|
|
|
|
[DefaultExecutionOrder(1000)] // PC 회전을 이동 방향 회전(원본 조이스틱 · WLSkillFollow 의 Update)보다 나중에
|
|
internal sealed class WLBladeStormRunner : MonoBehaviour
|
|
{
|
|
ProjectileBase _pb; PCActor _pc; Animator _an; GameObject _checker; WLBladeStormJson _c;
|
|
bool _running, _animOwned; float _yaw, _spin, _spinTotal, _t0;
|
|
Coroutine _co;
|
|
readonly List<GameObject> _fx = new List<GameObject>();
|
|
readonly List<Renderer> _hidden = new List<Renderer>();
|
|
readonly List<GameObject> _wraps = new List<GameObject>(); readonly List<float> _wrapH = new List<float>(), _wrapT = new List<float>();
|
|
readonly List<float> _fxEnd = new List<float>(); // 회오리 인스턴스가 지워질 시각(_fx 와 같은 순서)
|
|
readonly List<Renderer> _body = new List<Renderer>();
|
|
readonly List<Renderer> _qR = new List<Renderer>(); readonly List<Material[]> _qM = new List<Material[]>(); readonly List<Material> _qNew = new List<Material>();
|
|
static GameObject s_prefab; static string s_prefabPath;
|
|
static readonly Dictionary<string, GameObject> s_wrapPrefabs = new Dictionary<string, GameObject>();
|
|
|
|
public void Begin(ProjectileBase pb, PCActor pc, GameObject checker, WLBladeStormJson c)
|
|
{
|
|
if (_running) End(false, false);
|
|
_pb = pb; _pc = pc; _an = pc.m_animation; _checker = checker; _c = c;
|
|
if (_checker != null) _checker.SetActive(false);
|
|
if (c.hideOriginal != 0) Hide();
|
|
pc.Set_JoystickStatus(2);
|
|
if (c.moveMul < 0.999f) WLBladeStorm.SetSlow(pc, c.moveMul);
|
|
SpawnWraps();
|
|
if (c.hideBody != 0) HideBody();
|
|
BodyOnTop();
|
|
_yaw = pc.transform.eulerAngles.y; _spin = 0f; _spinTotal = 0f; _t0 = Time.time;
|
|
_running = true;
|
|
Pose(true);
|
|
_co = StartCoroutine(Co());
|
|
}
|
|
|
|
IEnumerator Co()
|
|
{
|
|
var c = _c; float nextHit = Mathf.Max(0f, c.firstHit), nextFx = 0f;
|
|
while (Time.time - _t0 < c.duration)
|
|
{
|
|
if (Broken()) { _co = null; End(false, true); yield break; }
|
|
float t = Time.time - _t0;
|
|
_spin = Mathf.Max(0f, c.spinDegPerSec) * Mathf.Clamp01(t / Mathf.Max(0.01f, c.spinUpSeconds));
|
|
Pose(false);
|
|
if (t >= nextFx) { SpawnFx(); nextFx += Mathf.Max(0.05f, c.fxRepeat); }
|
|
if (t >= nextHit) { Hit(); nextHit += Mathf.Max(0.05f, c.hitInterval); }
|
|
yield return null;
|
|
}
|
|
// 자연스럽게 멈춤 — 판정 · 새 이펙트 없이 자세를 둔 채 회전만 감속 · 감싸기 이펙트는 방출만 멈춰 남은 입자가 제 수명대로 사라짐
|
|
// fadeSync 1 = 남은 이펙트(감싸기 · 회오리)를 fadeSeconds 동안 함께 흐려 지우고(WLFxFade) 같은 시간 동안 감속 → 그 끝에 모션 끝
|
|
float dur = Mathf.Max(0.01f, c.stopSeconds);
|
|
if (c.fadeSync != 0)
|
|
{
|
|
dur = Mathf.Max(0.05f, c.fadeSeconds);
|
|
for (int i = 0; i < _wraps.Count; i++) if (_wraps[i] != null) WLFxFade.Attach(_wraps[i], 0f, dur, c.fadeDimAdditive != 0);
|
|
for (int i = 0; i < _fx.Count; i++) if (_fx[i] != null) WLFxFade.Attach(_fx[i], 0f, Mathf.Clamp(_fxEnd[i] - Time.time, 0.05f, dur));
|
|
}
|
|
else StopWraps();
|
|
float s0 = _spin, f0 = Time.time;
|
|
WLBladeStorm.LastFade = dur;
|
|
while (Time.time - f0 < dur)
|
|
{
|
|
if (Broken()) { _co = null; End(false, true); yield break; }
|
|
float k = 1f - Mathf.Clamp01((Time.time - f0) / dur);
|
|
_spin = s0 * k * k;
|
|
Pose(false);
|
|
yield return null;
|
|
}
|
|
_spin = 0f; _co = null;
|
|
End(true, true);
|
|
}
|
|
|
|
bool Broken() { return _pc == null || _pc.IsDead() || _pc.Get_CurAnim() != eAnim.Skill || _pb == null || !_pb.gameObject.activeInHierarchy; }
|
|
|
|
void LateUpdate()
|
|
{
|
|
if (!_running) return;
|
|
if (Broken()) { End(false, true); return; }
|
|
float d = _c.spinSign * _spin * Time.deltaTime; _yaw += d; _spinTotal += Mathf.Abs(d);
|
|
_pc.transform.rotation = Quaternion.Euler(0f, _yaw, 0f);
|
|
var p = _pc.Get_position();
|
|
transform.position = p; // 원본도 시전자 위치로 옮겼다(판정 상자는 끔)
|
|
var at = p - FxOffset();
|
|
for (int i = _fx.Count - 1; i >= 0; i--) { var go = _fx[i]; if (go == null) { _fx.RemoveAt(i); _fxEnd.RemoveAt(i); continue; } go.transform.position = at; }
|
|
var cam = Camera.main;
|
|
for (int i = _wraps.Count - 1; i >= 0; i--)
|
|
{
|
|
var w = _wraps[i]; if (w == null) { _wraps.RemoveAt(i); _wrapH.RemoveAt(i); _wrapT.RemoveAt(i); continue; }
|
|
var wp = p + Vector3.up * _wrapH[i];
|
|
if (_wrapT[i] != 0f && cam != null) wp += (cam.transform.position - wp).normalized * _wrapT[i]; // #927 시선 방향으로 당김
|
|
w.transform.position = wp;
|
|
}
|
|
}
|
|
|
|
/// <summary>몸 숨김 — 무기 소켓 밖의 켜진 SkinnedMeshRenderer(검 · 방패 등 소켓 무기는 그대로 보임).</summary>
|
|
void HideBody()
|
|
{
|
|
ShowBody();
|
|
var sockets = _pc.tfs_weapon;
|
|
foreach (var r in _pc.GetComponentsInChildren<SkinnedMeshRenderer>(false))
|
|
{
|
|
if (r == null || !r.enabled) continue;
|
|
bool inSocket = false;
|
|
if (sockets != null) foreach (var s in sockets) if (s != null && r.transform.IsChildOf(s)) { inSocket = true; break; }
|
|
if (inSocket) continue;
|
|
r.enabled = false; _body.Add(r);
|
|
}
|
|
}
|
|
void ShowBody() { for (int i = 0; i < _body.Count; i++) if (_body[i] != null) _body[i].enabled = true; _body.Clear(); }
|
|
|
|
/// <summary>#930 몸 · 무기 렌더러(파티클 · 꼬리 제외) 재질을 사본으로 바꿔 renderQueue + bodyQueueAdd — 반투명 이펙트 뒤에 그려 몸 색이 이펙트에 물들지 않게.</summary>
|
|
void BodyOnTop()
|
|
{
|
|
BodyRestore();
|
|
if (_c.bodyQueueAdd <= 0) return;
|
|
foreach (var r in _pc.GetComponentsInChildren<Renderer>(false))
|
|
{
|
|
if (r == null || !r.enabled || r is ParticleSystemRenderer || r is TrailRenderer || r is LineRenderer) continue;
|
|
if (!OnTopTarget(r)) continue;
|
|
var src = r.sharedMaterials; var dst = new Material[src.Length];
|
|
for (int i = 0; i < src.Length; i++) { if (src[i] == null) continue; var m = new Material(src[i]) { name = src[i].name }; m.renderQueue = src[i].renderQueue + _c.bodyQueueAdd; dst[i] = m; _qNew.Add(m); }
|
|
_qR.Add(r); _qM.Add(src); r.sharedMaterials = dst;
|
|
}
|
|
WLBladeStorm.BodyOnTop++;
|
|
}
|
|
bool OnTopTarget(Renderer r)
|
|
{
|
|
var names = _c.bodyOnTopNames; if (names == null || names.Length == 0) return true;
|
|
var sockets = _pc.tfs_weapon;
|
|
if (sockets != null) foreach (var s in sockets) if (s != null && r.transform.IsChildOf(s)) return _c.bodyOnTopWeapon != 0;
|
|
foreach (var n in names) if (!string.IsNullOrEmpty(n) && r.name.StartsWith(n, System.StringComparison.OrdinalIgnoreCase)) return true;
|
|
return false;
|
|
}
|
|
void BodyRestore()
|
|
{
|
|
for (int i = 0; i < _qR.Count; i++) if (_qR[i] != null) _qR[i].sharedMaterials = _qM[i];
|
|
for (int i = 0; i < _qNew.Count; i++) if (_qNew[i] != null) Destroy(_qNew[i]);
|
|
_qR.Clear(); _qM.Clear(); _qNew.Clear();
|
|
}
|
|
|
|
void SpawnWraps()
|
|
{
|
|
if (_c.wraps == null) return;
|
|
foreach (var w in _c.wraps)
|
|
{
|
|
if (w == null || string.IsNullOrEmpty(w.fx)) continue;
|
|
GameObject prefab;
|
|
if (!s_wrapPrefabs.TryGetValue(w.fx, out prefab) || prefab == null) { prefab = Resources.Load<GameObject>(w.fx); s_wrapPrefabs[w.fx] = prefab; }
|
|
if (prefab == null) { WLBladeStorm.LastLog = "감싸기 이펙트 없음 " + w.fx; continue; }
|
|
var go = Instantiate(prefab, _pc.Get_position() + Vector3.up * w.height, prefab.transform.rotation);
|
|
go.transform.localScale = prefab.transform.localScale * Mathf.Max(0.01f, w.scale);
|
|
_wraps.Add(go); _wrapH.Add(w.height); _wrapT.Add(w.towardCamera); WLBladeStorm.FxSpawned++;
|
|
}
|
|
}
|
|
|
|
/// <summary>감싸기 이펙트 — 방출만 멈춘다(남은 입자는 제 수명대로 · 다 사라진 뒤 지움) · 가장 긴 남은 수명(초)을 돌려준다.</summary>
|
|
float StopWraps()
|
|
{
|
|
float life = 0f;
|
|
for (int i = 0; i < _wraps.Count; i++)
|
|
{
|
|
var w = _wraps[i]; if (w == null) continue; float l = 0f;
|
|
foreach (var ps in w.GetComponentsInChildren<ParticleSystem>(true)) { ps.Stop(true, ParticleSystemStopBehavior.StopEmitting); l = Mathf.Max(l, ps.main.startLifetime.constantMax); }
|
|
Destroy(w, l + 0.2f); life = Mathf.Max(life, l);
|
|
}
|
|
return life;
|
|
}
|
|
|
|
|
|
|
|
/// <summary>공격 자세 고정 — 다른 동작(원본 시전 다음 동작 등)이 끼어들면 되돌린다 · 클립 이벤트(궤적 · 판정)는 끈다.</summary>
|
|
void Pose(bool force)
|
|
{
|
|
if (_an == null || string.IsNullOrEmpty(_c.poseState)) return;
|
|
if (!_animOwned) { _an.fireEvents = false; _animOwned = true; }
|
|
var si = _an.GetCurrentAnimatorStateInfo(0);
|
|
if (force || _an.IsInTransition(0) || !si.IsName(_c.poseState) || Mathf.Abs(si.normalizedTime - _c.poseNormalized) > 0.01f)
|
|
{
|
|
_an.Play(_c.poseState, 0, _c.poseNormalized);
|
|
if (force) { _an.speed = 0f; _an.Update(0f); }
|
|
}
|
|
if (_an.speed != 0f) _an.speed = 0f;
|
|
}
|
|
|
|
Vector3 FxOffset()
|
|
{
|
|
var a = _c.fxOffset;
|
|
return a != null && a.Length >= 3 ? new Vector3(a[0], a[1], a[2]) * Mathf.Max(0.01f, _c.fxScale) : Vector3.zero;
|
|
}
|
|
|
|
void SpawnFx()
|
|
{
|
|
if (string.IsNullOrEmpty(_c.fx)) return;
|
|
if (s_prefab == null || s_prefabPath != _c.fx) { s_prefab = Resources.Load<GameObject>(_c.fx); s_prefabPath = _c.fx; }
|
|
if (s_prefab == null) { WLBladeStorm.LastLog = "이펙트 없음 " + _c.fx; return; }
|
|
var go = Instantiate(s_prefab, _pc.Get_position() - FxOffset(), s_prefab.transform.rotation);
|
|
go.transform.localScale = s_prefab.transform.localScale * Mathf.Max(0.01f, _c.fxScale);
|
|
Destroy(go, Mathf.Max(0.1f, _c.fxLife));
|
|
_fx.Add(go); _fxEnd.Add(Time.time + Mathf.Max(0.1f, _c.fxLife)); WLBladeStorm.FxSpawned++;
|
|
}
|
|
|
|
/// <summary>PC 발 기준 radius + 적 몸 반지름 안의 모든 적에게 원본 한 대 피해(속사 레이저 WLRapidShot.Laser 와 같은 방식).</summary>
|
|
void Hit()
|
|
{
|
|
WLBladeStorm.Ticks++;
|
|
var info = ActorInfo.Ins; if (info == null || _pb == null) return;
|
|
var origin = _pc.Get_position(); int n = 0;
|
|
var list = info.Get_EnemyActors(_pc.IsEnemy(), origin, _c.radius + 3f);
|
|
if (list == null) return;
|
|
for (int k = 0; k < list.Count; k++)
|
|
{
|
|
var a = list[k];
|
|
if (a == null || a.IsDead()) continue;
|
|
var v = a.Get_position() - origin; v.y = 0f;
|
|
if (v.magnitude > _c.radius + WL.Combat.WLCombatMotionSettings.BodyRadius(a)) continue;
|
|
var col = a.GetComponent<Collider>();
|
|
if (col == null || !col.enabled) continue;
|
|
if (_pb.ProcessCollider(col, false)) { n++; HitFx(a, origin); }
|
|
}
|
|
info.Release_Actors(list);
|
|
WLBladeStorm.Hits += n;
|
|
}
|
|
|
|
static readonly Dictionary<string, GameObject> s_hitPrefabs = new Dictionary<string, GameObject>();
|
|
void HitFx(Actor a, Vector3 from)
|
|
{
|
|
if (string.IsNullOrEmpty(_c.hitFx) || a == null) return;
|
|
GameObject prefab;
|
|
if (!s_hitPrefabs.TryGetValue(_c.hitFx, out prefab) || prefab == null) { prefab = Resources.Load<GameObject>(_c.hitFx); s_hitPrefabs[_c.hitFx] = prefab; }
|
|
if (prefab == null) { WLBladeStorm.LastLog = "히트 이펙트 없음 " + _c.hitFx; return; }
|
|
var at = a.Get_Center_position(); var d = at - from; d.y = 0f; if (d.sqrMagnitude < 1e-4f) d = _pc.transform.forward;
|
|
at += WL.Combat.Impact.WLImpactFx.TowardCamera(a); // #941 몸 앞(카메라 쪽)
|
|
var go = Instantiate(prefab, at, Quaternion.LookRotation(d) * 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, _c.hitFxScale);
|
|
Destroy(go, Mathf.Max(0.1f, _c.hitFxLife)); WLBladeStorm.HitFxShown++;
|
|
}
|
|
|
|
void Hide()
|
|
{
|
|
_hidden.Clear();
|
|
foreach (var ps in GetComponentsInChildren<ParticleSystem>(true)) ps.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear);
|
|
foreach (var r in GetComponentsInChildren<Renderer>(true)) { if (!r.enabled) continue; r.enabled = false; _hidden.Add(r); }
|
|
}
|
|
|
|
/// <summary>끝 — natural = 유지 시간 만료(감속 뒤) · offProjectile = 투사체를 끈다(원본 끝 순서: 끔 → 조이스틱 0 → 대기).</summary>
|
|
void End(bool natural, bool offProjectile)
|
|
{
|
|
if (!_running) return;
|
|
_running = false;
|
|
if (_co != null) { StopCoroutine(_co); _co = null; }
|
|
if (_animOwned && _an != null) { _an.speed = 1f; _an.fireEvents = true; }
|
|
_animOwned = false;
|
|
for (int i = 0; i < _fx.Count; i++) if (_fx[i] != null) foreach (var ps in _fx[i].GetComponentsInChildren<ParticleSystem>(true)) ps.Stop(true, ParticleSystemStopBehavior.StopEmitting); // 남은 입자는 제 수명대로 사라짐
|
|
_fx.Clear(); _fxEnd.Clear();
|
|
StopWraps(); _wraps.Clear(); _wrapH.Clear(); _wrapT.Clear();
|
|
ShowBody();
|
|
BodyRestore();
|
|
WLBladeStorm.ClearSlow(_pc);
|
|
foreach (var r in _hidden) if (r != null) r.enabled = true; // 풀 재사용 · enabled 0 일 때 원본 그대로
|
|
_hidden.Clear();
|
|
WLBladeStorm.LastSpinTotal = _spinTotal; WLBladeStorm.LastSeconds = Time.time - _t0;
|
|
if (natural) WLBladeStorm.Ends++; else WLBladeStorm.Aborts++;
|
|
var pc = _pc;
|
|
if (offProjectile && _pb != null && _pb.gameObject.activeInHierarchy) WLSlashFlurry.Off(_pb);
|
|
if (pc != null)
|
|
{
|
|
pc.Set_JoystickStatus(3);
|
|
if (natural && !pc.IsDead() && pc.Get_CurAnim() == eAnim.Skill) pc.Play_Idle();
|
|
}
|
|
}
|
|
|
|
void OnDisable() { if (_running) End(false, false); }
|
|
}
|
|
}
|