Project_WL/Assets/WL/Combat/Status/WLStatusFX.cs

186 lines
9.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ─────────────────────────────────────────────────────────────────────────────
// 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;
}
}
}