Project_WL/Assets/WL/Combat/Loot/LootBurstRunner.cs

180 lines
8.4 KiB
C#

// ─────────────────────────────────────────────────────────────────────────────
// LootBurstRunner.cs — LootBurst 의 프레임 루프 · 코루틴 호스트(LevelUpBurstRunner 선례)
//
// PD 지시 #813 · 발주서 WL-813f §1 (2026-09-09)
//
// LootBurst 가 런타임에 1개만 만들고 DontDestroyOnLoad 로 들고 있는다. 씬·프리팹을 건드리지 않는다.
// ① 자동 줍기 — 살아 있는 드랍을 주기적으로 훑어 PC 반경 안이면 원본 자석 상태기를 켠다(LootBurst.EngageMagnet).
// 판정 대상은 **착지가 끝난**(원본이 m_Collider 를 켠) 드랍만 — 날아가는 중에 빨려 들어가지 않게 한다.
// ② 등급 빔 — 드랍이 사라지면(획득·20 s 소멸·AllOff) 빔을 파괴하고 EffectBudget.Heavy 를 정확히 1회 반납한다.
// 러너가 사라져도 OnDisable 이 남은 슬롯을 정산한다.
// ③ 폭발 드랍 — 보스/엘리트 추가 드랍을 간격을 두고 낸다.
//
// 모든 순회는 사전 할당 배열이다(GC 0). 배열은 Register 때만 성장한다.
// ─────────────────────────────────────────────────────────────────────────────
using System;
using System.Collections;
using UnityEngine;
using WL.Combat.Core;
namespace WL.Combat.Loot
{
public class LootBurstRunner : MonoBehaviour
{
struct Entry
{
public DropItem item;
public GameObject beam;
public bool heldBudget;
public float beamDeadline; // unscaled 시각(빔이 있을 때만)
}
Entry[] _entries = new Entry[32];
int _count;
float _nextScan;
/// <summary>살아 있는(등록된) 드랍 수 — 동시 드랍 상한 판정의 기준.</summary>
public int ActiveCount { get { return _count; } }
/// <summary>반납하지 않은 EffectBudget.Heavy 슬롯 수.</summary>
public int HeldBudget
{
get { int n = 0; for (int i = 0; i < _count; i++) if (_entries[i].heldBudget) n++; return n; }
}
/// <summary>진단: 마지막 스캔에서 반경 안이라 자석을 켠 수.</summary>
public int LastScanEngaged;
// ───────────────────────────────────────────────── 등록 · 해제
/// <summary>드랍을 추적 목록에 넣는다(이미 있으면 무시). DropItem.Set 훅에서 부른다.</summary>
public void Register(DropItem item)
{
if (item == null) return;
for (int i = 0; i < _count; i++) if (_entries[i].item == item) return;
if (_count == _entries.Length) Array.Resize(ref _entries, _entries.Length * 2);
_entries[_count++] = new Entry { item = item, beam = null, heldBudget = false, beamDeadline = 0f };
}
/// <summary>이 드랍에 빔을 매단다(같은 드랍에 이미 빔이 있으면 이전 것을 정리한다).</summary>
public void RegisterBeam(DropItem item, GameObject beam, bool heldBudget, float maxLifetime)
{
if (item == null) { DestroyBeam(beam, heldBudget); return; }
Register(item);
for (int i = 0; i < _count; i++)
{
if (_entries[i].item != item) continue;
if (_entries[i].beam != null) DestroyBeam(_entries[i].beam, _entries[i].heldBudget);
_entries[i].beam = beam;
_entries[i].heldBudget = heldBudget;
_entries[i].beamDeadline = Time.unscaledTime + Mathf.Max(0.1f, maxLifetime);
return;
}
}
/// <summary>드랍이 획득·소멸했다. 빔을 파괴하고 슬롯을 반납한 뒤 목록에서 뺀다.</summary>
public void Unregister(DropItem item)
{
for (int i = 0; i < _count; i++)
{
if (_entries[i].item != item) continue;
DestroyBeam(_entries[i].beam, _entries[i].heldBudget);
_entries[i] = _entries[--_count];
_entries[_count] = default(Entry);
return;
}
}
/// <summary>전부 정리(씬 전환 · 프로브 종료). 슬롯을 남기지 않는다.</summary>
public void CleanupAll()
{
for (int i = 0; i < _count; i++)
{
DestroyBeam(_entries[i].beam, _entries[i].heldBudget);
_entries[i] = default(Entry);
}
_count = 0;
}
void DestroyBeam(GameObject beam, bool heldBudget)
{
if (beam != null)
{
if (Application.isPlaying) UnityEngine.Object.Destroy(beam);
else UnityEngine.Object.DestroyImmediate(beam);
}
if (heldBudget) EffectBudget.Release(EffectBudgetKind.Heavy);
}
void OnDisable() { CleanupAll(); }
// ───────────────────────────────────────────────── 프레임 루프
void Update()
{
var s = WLLootSettings.Instance;
if (s == null) { if (_count > 0) CleanupAll(); return; }
float now = Time.unscaledTime;
bool scan = s.autoPickupEnabled && !WLLootSettings.RuntimeDisabled && s.enabled &&
(s.autoPickupScanIntervalSeconds <= 0f || now >= _nextScan);
if (scan) _nextScan = now + Mathf.Max(0f, s.autoPickupScanIntervalSeconds);
Vector3 pcPos = Vector3.zero;
bool pcOk = false;
if (scan)
{
var pc = MyValue.MyPC;
if (pc != null && !pc.IsDead()) { pcPos = pc.Get_Center_position(); pcOk = true; }
}
int engaged = 0;
for (int i = _count - 1; i >= 0; i--)
{
var item = _entries[i].item;
// ① 사라진 드랍(획득 · 20 s 소멸 · AllOff · 풀 파괴) → 빔 파괴 + 슬롯 반납 + 목록에서 제거
if (item == null || !item.gameObject.activeInHierarchy)
{
DestroyBeam(_entries[i].beam, _entries[i].heldBudget);
_entries[i] = _entries[--_count];
_entries[_count] = default(Entry);
continue;
}
// ② 빔 수명 상한(드랍이 어떤 이유로든 오래 남아 있을 때의 2차 방어)
if (_entries[i].beam != null && now >= _entries[i].beamDeadline)
{
DestroyBeam(_entries[i].beam, _entries[i].heldBudget);
_entries[i].beam = null;
_entries[i].heldBudget = false;
}
// ③ 자동 줍기 — 착지가 끝난(콜라이더가 켜진) 드랍만 · 아직 자석이 아닌 것만
if (!scan || !pcOk) continue;
if (!LootBurst.InPickupRadius(item, pcPos, s)) continue;
if (LootBurst.EngageMagnet(item)) engaged++;
}
if (scan) LastScanEngaged = engaged;
}
// ───────────────────────────────────────────────── 폭발 드랍
/// <summary>보스/엘리트 추가 드랍을 간격을 두고 낸다. ClusterBurst 슬롯을 잡았으면 마지막에 반납한다.</summary>
public void RunBurst(int count, int bagId, int baseGold, Vector3 position, float interval, bool heldCluster)
{
StartCoroutine(CoBurst(count, bagId, baseGold, position, interval, heldCluster));
}
IEnumerator CoBurst(int count, int bagId, int baseGold, Vector3 position, float interval, bool heldCluster)
{
for (int i = 0; i < count; i++)
{
LootBurst.SpawnBurstOne(bagId, baseGold, position);
if (i < count - 1 && interval > 0f) yield return new WaitForSeconds(interval);
}
if (heldCluster) EffectBudget.Release(EffectBudgetKind.ClusterBurst);
}
}
}