// ───────────────────────────────────────────────────────────────────────────── // 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; /// 살아 있는(등록된) 드랍 수 — 동시 드랍 상한 판정의 기준. public int ActiveCount { get { return _count; } } /// 반납하지 않은 EffectBudget.Heavy 슬롯 수. public int HeldBudget { get { int n = 0; for (int i = 0; i < _count; i++) if (_entries[i].heldBudget) n++; return n; } } /// 진단: 마지막 스캔에서 반경 안이라 자석을 켠 수. public int LastScanEngaged; // ───────────────────────────────────────────────── 등록 · 해제 /// 드랍을 추적 목록에 넣는다(이미 있으면 무시). DropItem.Set 훅에서 부른다. 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 }; } /// 이 드랍에 빔을 매단다(같은 드랍에 이미 빔이 있으면 이전 것을 정리한다). 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; } } /// 드랍이 획득·소멸했다. 빔을 파괴하고 슬롯을 반납한 뒤 목록에서 뺀다. 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; } } /// 전부 정리(씬 전환 · 프로브 종료). 슬롯을 남기지 않는다. 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; } // ───────────────────────────────────────────────── 폭발 드랍 /// 보스/엘리트 추가 드랍을 간격을 두고 낸다. ClusterBurst 슬롯을 잡았으면 마지막에 반납한다. public void RunBurst(int count, int bagId, int baseGold, Vector3 position, float interval, bool heldCluster, eSubRol subRole) { StartCoroutine(CoBurst(count, bagId, baseGold, position, interval, heldCluster, subRole)); } IEnumerator CoBurst(int count, int bagId, int baseGold, Vector3 position, float interval, bool heldCluster, eSubRol subRole) { for (int i = 0; i < count; i++) { LootBurst.SpawnBurstOne(LootBurst.BagForBurst(WLLootSettings.Instance, subRole, i, bagId), baseGold, position); if (i < count - 1 && interval > 0f) yield return new WaitForSeconds(interval); } if (heldCluster) EffectBudget.Release(EffectBudgetKind.ClusterBurst); } } }