186 lines
9.9 KiB
C#
186 lines
9.9 KiB
C#
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|||
|
|
// WLStatusFX.cs — 상태이상 VFX 스폰·풀·추종 (#842). 프리팹 = Resources/WL/Status/<name>(Master Stylized FX 사본 · WL842_StatusPrefabs 가 만든다).
|
|||
|
|
// · 루프(스턴·수면·중독) = 상태가 끝날 때 Stop → 방출 중지 후 남은 파티클이 꺼지면 비활성(풀 복귀).
|
|||
|
|
// · 1회(속도·능력치·출혈) = oneShotSeconds 뒤 방출 중지.
|
|||
|
|
// · 추종 = 매 LateUpdate 액터의 Top/Mid/Bottom 지점(원본 TurnOff_GO 의 eEffectLocation 규칙과 같다).
|
|||
|
|
// · PD 2026-10-11 「몬스터 크기에 맞게」 = 액터 키(Top−발) ÷ referenceHeight 를 크기에 곱한다.
|
|||
|
|
// · PD 2026-10-11 「이미 재생 중이면 중복 재생 금지 · 같은 이펙트가 또 오면 시간을 늘려라」 = (액터, 종류)별 활성 레지스트리.
|
|||
|
|
// 같은 것이 살아 있으면 새로 띄우지 않고 Retrigger: 루프형·지속 방출형은 종료 시각 연장, 1회 폭발형(출혈 베기)은 같은 인스턴스를 되감아 재생.
|
|||
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|||
|
|
using System.Collections.Generic;
|
|||
|
|
using UnityEngine;
|
|||
|
|
|
|||
|
|
namespace WL.Combat.Status
|
|||
|
|
{
|
|||
|
|
public static class WLStatusFX
|
|||
|
|
{
|
|||
|
|
public const string ResourcesDir = "WL/Status/";
|
|||
|
|
public sealed class Handle { public WLStatusType type; public WLStatusFollower follower; public bool Active { get { return follower != null && follower.gameObject.activeSelf && !follower.Ending; } } }
|
|||
|
|
|
|||
|
|
static readonly Dictionary<string, GameObject> s_prefabs = new Dictionary<string, GameObject>();
|
|||
|
|
static readonly Dictionary<string, List<GameObject>> s_pool = new Dictionary<string, List<GameObject>>();
|
|||
|
|
static readonly Dictionary<Actor, WLStatusFollower[]> s_active = new Dictionary<Actor, WLStatusFollower[]>(32); // 액터별 종류별 활성 인스턴스(중복 방지)
|
|||
|
|
static Transform s_root;
|
|||
|
|
static readonly int TypeCount = System.Enum.GetValues(typeof(WLStatusType)).Length;
|
|||
|
|
|
|||
|
|
public static int Spawns, Misses, Retriggers;
|
|||
|
|
public static string LastLog = "";
|
|||
|
|
|
|||
|
|
static GameObject Load(string name)
|
|||
|
|
{
|
|||
|
|
GameObject p;
|
|||
|
|
if (!s_prefabs.TryGetValue(name, out p)) { p = Resources.Load<GameObject>(ResourcesDir + name); s_prefabs[name] = p; }
|
|||
|
|
return p;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static GameObject Get(string name)
|
|||
|
|
{
|
|||
|
|
List<GameObject> list;
|
|||
|
|
if (s_pool.TryGetValue(name, out list))
|
|||
|
|
for (int i = 0; i < list.Count; i++) if (list[i] != null && !list[i].activeSelf) return list[i];
|
|||
|
|
var prefab = Load(name);
|
|||
|
|
if (prefab == null) { Misses++; LastLog = "missing prefab " + ResourcesDir + name; return null; }
|
|||
|
|
if (s_root == null) { var r = new GameObject("WL_StatusFX"); Object.DontDestroyOnLoad(r); s_root = r.transform; }
|
|||
|
|
var go = Object.Instantiate(prefab, s_root);
|
|||
|
|
go.name = name; go.SetActive(false);
|
|||
|
|
if (list == null) { list = new List<GameObject>(4); s_pool[name] = list; }
|
|||
|
|
list.Add(go);
|
|||
|
|
return go;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>액터 키(Top−발 · 원본 tf_Top 기준) ÷ 기준 키 = 크기 배율. PD 「몬스터 크기에 맞게」.</summary>
|
|||
|
|
public static float SizeFactor(Actor actor, WLStatusSettings s)
|
|||
|
|
{
|
|||
|
|
if (actor == null || s == null || s.referenceHeight <= 0f) return 1f;
|
|||
|
|
float h = actor.Get_Top_postion().y - actor.Get_position().y;
|
|||
|
|
if (h < 0.05f) return 1f;
|
|||
|
|
return Mathf.Clamp(h / s.referenceHeight, s.sizeFactorMin, s.sizeFactorMax);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static WLStatusFollower ActiveOf(Actor actor, WLStatusType type)
|
|||
|
|
{
|
|||
|
|
WLStatusFollower[] arr;
|
|||
|
|
if (actor == null || !s_active.TryGetValue(actor, out arr)) return null;
|
|||
|
|
var f = arr[(int)type];
|
|||
|
|
return f != null && f.gameObject.activeSelf && !f.Ending && f.Actor == actor ? f : null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static void SetActive(Actor actor, WLStatusType type, WLStatusFollower f)
|
|||
|
|
{
|
|||
|
|
WLStatusFollower[] arr;
|
|||
|
|
if (!s_active.TryGetValue(actor, out arr)) { arr = new WLStatusFollower[TypeCount]; s_active[actor] = arr; }
|
|||
|
|
arr[(int)type] = f;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
internal static void Unregister(Actor actor, WLStatusType type, WLStatusFollower f)
|
|||
|
|
{
|
|||
|
|
WLStatusFollower[] arr;
|
|||
|
|
if (actor != null && s_active.TryGetValue(actor, out arr) && arr[(int)type] == f) arr[(int)type] = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static Handle Play(Actor actor, WLStatusType type, bool loop)
|
|||
|
|
{
|
|||
|
|
var s = WLStatus.S; if (s == null || actor == null || s.vfx == null) return null;
|
|||
|
|
WLStatusVfx v;
|
|||
|
|
if (!s.vfx.TryGetValue(type.ToString(), out v) || v == null || string.IsNullOrEmpty(v.prefab)) return null;
|
|||
|
|
float seconds = loop ? -1f : s.oneShotSeconds;
|
|||
|
|
var existing = ActiveOf(actor, type);
|
|||
|
|
if (existing != null)
|
|||
|
|
{ // 중복 금지 — 살아 있는 같은 이펙트를 연장/재트리거
|
|||
|
|
existing.Retrigger(seconds);
|
|||
|
|
Retriggers++;
|
|||
|
|
LastLog = "retrigger " + type + " → " + actor.name;
|
|||
|
|
return new Handle { type = type, follower = existing };
|
|||
|
|
}
|
|||
|
|
var go = Get(v.prefab); if (go == null) return null;
|
|||
|
|
var f = go.GetComponent<WLStatusFollower>(); if (f == null) f = go.AddComponent<WLStatusFollower>();
|
|||
|
|
f.Begin(actor, type, v.loc, seconds, v.scale * s.vfxScale * SizeFactor(actor, s), v.yOffset);
|
|||
|
|
SetActive(actor, type, f);
|
|||
|
|
go.SetActive(true);
|
|||
|
|
Spawns++;
|
|||
|
|
LastLog = "play " + type + " (" + v.prefab + ") ×" + f.transform.localScale.x.ToString("F2") + " → " + actor.name;
|
|||
|
|
return new Handle { type = type, follower = f };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static void PlayOneShot(Actor actor, WLStatusType type) { Play(actor, type, false); }
|
|||
|
|
|
|||
|
|
public static void Stop(ref Handle h)
|
|||
|
|
{
|
|||
|
|
if (h != null && h.follower != null) h.follower.End();
|
|||
|
|
h = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>프로브용 — 풀 비우기(설정·프리팹 교체 뒤).</summary>
|
|||
|
|
public static void ClearPool()
|
|||
|
|
{
|
|||
|
|
foreach (var kv in s_pool) foreach (var g in kv.Value) if (g != null) Object.Destroy(g);
|
|||
|
|
s_pool.Clear(); s_prefabs.Clear(); s_active.Clear();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public sealed class WLStatusFollower : MonoBehaviour
|
|||
|
|
{
|
|||
|
|
Actor _actor; WLStatusType _type; string _loc; float _endAt, _offAt, _yOff;
|
|||
|
|
ParticleSystem[] _ps; bool _anyLooping, _loop;
|
|||
|
|
public bool Ending { get; private set; }
|
|||
|
|
public Actor Actor { get { return _actor; } }
|
|||
|
|
/// <summary>남은 파티클이 꺼질 때까지 기다리는 시간(초) — 방출 중지 뒤.</summary>
|
|||
|
|
const float LingerSeconds = 1.5f;
|
|||
|
|
|
|||
|
|
public void Begin(Actor a, WLStatusType type, string loc, float seconds, float scale, float yOff)
|
|||
|
|
{
|
|||
|
|
_actor = a; _type = type; _loc = loc; _yOff = yOff; Ending = false; _loop = seconds <= 0f;
|
|||
|
|
transform.localScale = Vector3.one * Mathf.Max(0.01f, scale);
|
|||
|
|
_endAt = seconds > 0f ? Time.time + seconds : float.MaxValue;
|
|||
|
|
if (_ps == null) { _ps = GetComponentsInChildren<ParticleSystem>(true); _anyLooping = false; for (int i = 0; i < _ps.Length; i++) if (_ps[i].main.loop) _anyLooping = true; }
|
|||
|
|
Follow();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>같은 이펙트가 또 발생 — 지속 방출형은 종료 시각만 연장, 1회 폭발형은 같은 인스턴스를 되감아 재생(인스턴스 1개 유지).</summary>
|
|||
|
|
public void Retrigger(float seconds)
|
|||
|
|
{
|
|||
|
|
_endAt = seconds > 0f ? Time.time + seconds : float.MaxValue;
|
|||
|
|
if (!_anyLooping && _ps != null) for (int i = 0; i < _ps.Length; i++) { _ps[i].Clear(true); _ps[i].Play(true); }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void OnEnable()
|
|||
|
|
{
|
|||
|
|
if (_ps == null) { _ps = GetComponentsInChildren<ParticleSystem>(true); _anyLooping = false; for (int i = 0; i < _ps.Length; i++) if (_ps[i].main.loop) _anyLooping = true; }
|
|||
|
|
for (int i = 0; i < _ps.Length; i++) { _ps[i].Clear(true); _ps[i].Play(true); }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void End()
|
|||
|
|
{
|
|||
|
|
if (Ending) return;
|
|||
|
|
Ending = true;
|
|||
|
|
WLStatusFX.Unregister(_actor, _type, this);
|
|||
|
|
if (_loop)
|
|||
|
|
{ // #843 PD 「스턴 상태에서 몬스터가 이동」 — 상태가 끝나면 별·Z 를 그 자리에서 지운다(남은 파티클이 1.5 s 떠 있으면 걷기 시작한 몹이 스턴처럼 보였다)
|
|||
|
|
if (_ps != null) for (int i = 0; i < _ps.Length; i++) _ps[i].Clear(true);
|
|||
|
|
gameObject.SetActive(false);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (_ps != null) for (int i = 0; i < _ps.Length; i++) _ps[i].Stop(true, ParticleSystemStopBehavior.StopEmitting);
|
|||
|
|
_offAt = Time.time + LingerSeconds;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void OnDisable() { WLStatusFX.Unregister(_actor, _type, this); }
|
|||
|
|
|
|||
|
|
void LateUpdate()
|
|||
|
|
{
|
|||
|
|
if (!Ending && (_actor == null || _actor.IsDead() || !_actor.gameObject.activeInHierarchy)) End();
|
|||
|
|
Follow();
|
|||
|
|
if (!Ending && Time.time >= _endAt) End();
|
|||
|
|
if (Ending && Time.time >= _offAt) gameObject.SetActive(false);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void Follow()
|
|||
|
|
{
|
|||
|
|
if (_actor == null) return;
|
|||
|
|
Vector3 p = _loc == "Top" ? _actor.Get_Top_postion() : _loc == "Bottom" ? _actor.Get_position() : _actor.Get_Center_position();
|
|||
|
|
p.y += _yOff;
|
|||
|
|
transform.position = p;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|