494 lines
25 KiB
C#
494 lines
25 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
||
// LootBurst.cs — 전리품 연출(드랍 산포 · 등급 빔 + 사운드 · 자동 줍기 반경 · 보스/엘리트 폭발 드랍)
|
||
//
|
||
// PD 지시 #813 · 발주서 WL-813f §1 (2026-09-09) · 기준서 v1 §B 요소 4 · §D-1 813f 행
|
||
//
|
||
// ■ 무엇을 하나
|
||
// ① 산포 — DropItem.Set() 의 훅 1줄이 시작점(튀는 높이)·목표점(0.6~1.2 m 링)을 우리 값으로 바꾼다.
|
||
// 원본 포물선 코루틴(ProjectileBase.Co_Move_Split)을 그대로 쓴다 = 새 이동 코드 0.
|
||
// ② 등급 빔 — table_itemlist.n_ItemGrade ≥ beamMinGrade(4) 면 보유 FX_LootDrop_* 프리팹을 드랍에 붙이고
|
||
// eSound.s015_GoldPowerUp 을 낸다. EffectBudget.Heavy 슬롯을 잡고, 드랍이 사라지면 파괴 + 반납.
|
||
// ③ 자동 줍기 — 원본 자석 상태기(DropItem.Update 의 m_Magnet 1→2)를 **리플렉션으로 켜기만** 한다.
|
||
// 실제 획득은 원본 콜라이더 → ProcessCollider → ItemTouch 그대로(획득 로직 무수정).
|
||
// ④ 폭발 드랍 — CombatEvents.Killed 구독. 엘리트 1~2 · 보스 3~5 건을 몹의 RandomBag 재추첨으로 추가한다.
|
||
// 빈손이면 골드(n_DropGold × burstGoldShare)로 대체. 원본 1건 추첨은 그대로 둔다(감소 0).
|
||
// ⑤ 813g 연결 — Dropped / Picked 두 이벤트를 코어의 CombatEventList<T>(제네릭 · GC 0)로 공개한다.
|
||
//
|
||
// ■ 원본 수정량
|
||
// Assets/Script/DropItem/DropItem.cs **2줄**(Set 의 산포 훅 1 · ItemTouch 의 획득 통지 훅 1).
|
||
// Actor.cs · MobActor.cs · DropItemInfo.cs · Assets/WL/Combat/Core/** 는 0줄.
|
||
//
|
||
// ■ 롤백(C8)
|
||
// WLLootSettings 에셋이 없거나 enabled = false(또는 RuntimeDisabled) 면 훅 2줄이 첫 줄에서 반환하고
|
||
// Killed 구독자도 즉시 반환한다 → 산포·빔·줍기·폭발이 전부 사라지고 원본 드랍 100% 로 돌아간다.
|
||
//
|
||
// ■ 에디트 모드(프로브)
|
||
// 코루틴이 돌지 않으므로 폭발 드랍은 즉시(간격 0) 처리하고, 빔은 스폰만 한 뒤 프로브가 CleanupAll() 로 정리한다.
|
||
//
|
||
// 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것.
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
using System;
|
||
using System.Reflection;
|
||
using UnityEngine;
|
||
using UnityEngine.AI;
|
||
using WL.Combat.Core;
|
||
|
||
namespace WL.Combat.Loot
|
||
{
|
||
/// <summary>드랍이 화면에 나온 순간. 813g 토스트는 이걸 구독해도 되고, 획득 시점(Picked)만 써도 된다.</summary>
|
||
public struct LootDropEvent
|
||
{
|
||
public DropItem item; public int itemId; public int amount; public int grade;
|
||
public Vector3 position; public bool fromBurst; public bool beamSpawned;
|
||
public float time; public int frame;
|
||
}
|
||
|
||
/// <summary>드랍을 실제로 주운 순간(원본 ItemTouch 가 서버 지급을 마친 직후). **813g 전리품 토스트의 소스**.</summary>
|
||
public struct LootPickedEvent
|
||
{
|
||
public DropItem item; public Actor picker; public int itemId; public int amount; public int grade;
|
||
public Vector3 position; public bool isGold; public float time; public int frame;
|
||
}
|
||
|
||
public static class LootBurst
|
||
{
|
||
/// <summary>골드 아이템 ID(원본 DropItem.Set / ItemTouch 의 `case 2`).</summary>
|
||
public const int GoldItemId = 2;
|
||
|
||
// ── 813g(U) 연결 API — 구독은 Dropped.Add(handler) / Picked.Add(handler), 해제는 Remove.
|
||
public static readonly CombatEventList<LootDropEvent> Dropped = new CombatEventList<LootDropEvent>(4);
|
||
public static readonly CombatEventList<LootPickedEvent> Picked = new CombatEventList<LootPickedEvent>(4);
|
||
|
||
// ── 진단(프로브가 읽는다)
|
||
public static bool Subscribed;
|
||
public static int KilledHandled, DropsSeen, Scattered, BeamSpawned, BeamSkippedBudget, BeamSkippedNoPrefab,
|
||
BeamSkippedCrowded, SoundPlayed, PickedCount, MagnetEngaged,
|
||
BurstPlanned, BurstDropped, BurstSkippedCap, BurstSkippedBudget;
|
||
public static float LastScatterDistance, LastBounceHeight;
|
||
public static int LastGrade, LastBurstCount;
|
||
public static string LastNote = "";
|
||
|
||
// ── 같은 처치에서 여러 개가 떨어질 때 부채꼴로 흩기 위한 상태
|
||
static int s_fanFrame = -1, s_fanIndex;
|
||
static float s_fanBaseAngle;
|
||
|
||
// ── 폭발 드랍 컨텍스트(Killed → 곧바로 이어지는 원본 드랍이 "이 처치의 것" 임을 알기 위해)
|
||
static int s_lastKillFrame = -1;
|
||
static eSubRol s_lastKillSubRole;
|
||
|
||
static float s_lastSoundTime = -999f;
|
||
static LootBurstRunner s_runner;
|
||
|
||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)]
|
||
static void RegisterOnLoad() { EnsureSubscribed(); }
|
||
|
||
/// <summary>구독 보장(중복 구독은 CombatEventList 가 막는다). 에디트 모드 프로브도 이걸 부른다.</summary>
|
||
public static void EnsureSubscribed()
|
||
{
|
||
CombatEvents.Killed.Add(OnKilled);
|
||
Subscribed = true;
|
||
}
|
||
|
||
/// <summary>구독 해제(프로브 종료용).</summary>
|
||
public static void Unsubscribe()
|
||
{
|
||
CombatEvents.Killed.Remove(OnKilled);
|
||
Subscribed = false;
|
||
}
|
||
|
||
internal static LootBurstRunner Runner
|
||
{
|
||
get
|
||
{
|
||
if (s_runner == null)
|
||
{
|
||
var go = new GameObject("__WLLootBurst");
|
||
// Play 중에만 DontDestroyOnLoad(LevelUpBurst 선례). 에디트 모드(프로브)에서는 씬을 더럽히지 않도록 DontSave.
|
||
if (Application.isPlaying) UnityEngine.Object.DontDestroyOnLoad(go);
|
||
else go.hideFlags = HideFlags.DontSave;
|
||
s_runner = go.AddComponent<LootBurstRunner>();
|
||
}
|
||
return s_runner;
|
||
}
|
||
}
|
||
|
||
/// <summary>러너를 만들지 않고 현재 인스턴스만 본다(에디트 모드 진단용).</summary>
|
||
public static LootBurstRunner RunnerOrNull { get { return s_runner; } }
|
||
|
||
// ═════════════════════════════════════════════════ 원본 훅 ① DropItem.Set (산포)
|
||
/// <summary>
|
||
/// DropItem.Set() 안에서 원본이 계산한 시작점·목표점을 우리 값으로 바꾼다(훅 1줄).
|
||
/// 꺼져 있으면 아무것도 하지 않는다 = 원본 위치 그대로.
|
||
/// </summary>
|
||
public static void OnDropSpawn(DropItem item, int itemId, int amount, ref Vector3 startPos, ref Vector3 targetPos)
|
||
{
|
||
if (!WLLootSettings.Enabled || item == null) return;
|
||
var s = WLLootSettings.Instance;
|
||
|
||
DropsSeen++;
|
||
int grade = ResolveGrade(itemId);
|
||
LastGrade = grade;
|
||
|
||
bool fromBurst = s_lastKillFrame == Time.frameCount;
|
||
|
||
if (s.scatterEnabled)
|
||
{
|
||
Vector3 origin = startPos;
|
||
Vector3 flat = SampleScatter(s, origin);
|
||
float bounce = Mathf.Min(s.bounceHeightMax,
|
||
s.bounceHeightBase + Mathf.Max(0, grade - 1) * s.bounceHeightPerGrade);
|
||
startPos = origin + Vector3.up * bounce;
|
||
targetPos = flat + Vector3.up * 0.25f; // 원본 골드와 같은 착지 오프셋
|
||
LastScatterDistance = Vector3.Distance(new Vector3(origin.x, 0f, origin.z), new Vector3(flat.x, 0f, flat.z));
|
||
LastBounceHeight = bounce;
|
||
Scattered++;
|
||
}
|
||
|
||
bool beam = TrySpawnBeam(s, item, grade);
|
||
|
||
Runner.Register(item);
|
||
|
||
var e = new LootDropEvent
|
||
{
|
||
item = item, itemId = itemId, amount = amount, grade = grade,
|
||
position = targetPos, fromBurst = fromBurst, beamSpawned = beam,
|
||
time = Time.unscaledTime, frame = Time.frameCount
|
||
};
|
||
Dropped.Dispatch(in e);
|
||
|
||
if (s.verboseLog)
|
||
Debug.Log("[LootBurst] drop id=" + itemId + " grade=" + grade +
|
||
" scatter=" + LastScatterDistance.ToString("F2") + "m bounce=" + LastBounceHeight.ToString("F2") +
|
||
"m beam=" + beam + " burst=" + fromBurst);
|
||
}
|
||
|
||
// ═════════════════════════════════════════════════ 원본 훅 ② DropItem.ItemTouch (획득 통지)
|
||
/// <summary>
|
||
/// 원본 ItemTouch 가 서버 지급(Add_Item · StageDropData)을 마친 직후 부른다(훅 1줄).
|
||
/// 813g 전리품 토스트는 이 이벤트만 구독하면 된다 — 획득 수량은 골드 보정까지 끝난 최종값이다.
|
||
/// </summary>
|
||
public static void OnDropPicked(DropItem item, int itemId, int amount, Actor picker)
|
||
{
|
||
if (!WLLootSettings.Enabled) return;
|
||
|
||
PickedCount++;
|
||
int grade = ResolveGrade(itemId);
|
||
var e = new LootPickedEvent
|
||
{
|
||
item = item, picker = picker, itemId = itemId, amount = amount, grade = grade,
|
||
position = item != null ? item.transform.position : Vector3.zero,
|
||
isGold = itemId == GoldItemId, time = Time.unscaledTime, frame = Time.frameCount
|
||
};
|
||
Picked.Dispatch(in e);
|
||
|
||
if (s_runner != null) s_runner.Unregister(item);
|
||
|
||
var s = WLLootSettings.Instance;
|
||
if (s != null && s.verboseLog)
|
||
Debug.Log("[LootBurst] picked id=" + itemId + " x" + amount + " grade=" + grade);
|
||
}
|
||
|
||
// ═════════════════════════════════════════════════ Killed 구독자 (폭발 드랍)
|
||
static void OnKilled(in KilledEvent e)
|
||
{
|
||
var s = WLLootSettings.Instance;
|
||
if (s == null || !s.enabled || WLLootSettings.RuntimeDisabled) return;
|
||
|
||
KilledHandled++;
|
||
s_lastKillFrame = Time.frameCount;
|
||
s_lastKillSubRole = e.subRole;
|
||
NewFanGroup();
|
||
|
||
if (!s.burstEnabled) return;
|
||
if (e.victim == null || e.victim.n_Reincarnation > 0) return; // 원본 Set_Die 와 같은 조건(부활 몹은 드랍 없음)
|
||
|
||
int count = BurstCount(s, e.subRole);
|
||
LastBurstCount = count;
|
||
if (count <= 0) return;
|
||
|
||
if (s.maxConcurrentDrops > 0 && ActiveDropCount() >= s.maxConcurrentDrops)
|
||
{ BurstSkippedCap++; LastNote = "cap"; return; }
|
||
|
||
bool held = false;
|
||
if (s.burstUseClusterBudget)
|
||
{
|
||
if (!EffectBudget.TryAcquire(EffectBudgetKind.ClusterBurst)) { BurstSkippedBudget++; LastNote = "cluster-budget"; return; }
|
||
held = true;
|
||
}
|
||
|
||
BurstPlanned += count;
|
||
var appear = e.victim.Get_MonsterStatTableData();
|
||
int bagId = appear != null ? appear.n_DropReward : 0;
|
||
int baseGold = appear != null ? appear.n_DropGold : 0;
|
||
|
||
if (Application.isPlaying && s.burstIntervalSeconds > 0f)
|
||
Runner.RunBurst(count, bagId, baseGold, e.position, s.burstIntervalSeconds, held);
|
||
else
|
||
{
|
||
for (int i = 0; i < count; i++) SpawnBurstOne(bagId, baseGold, e.position);
|
||
if (held) EffectBudget.Release(EffectBudgetKind.ClusterBurst);
|
||
}
|
||
|
||
if (s.verboseLog)
|
||
Debug.Log("[LootBurst] burst " + e.subRole + " x" + count + " bag=" + bagId + " gold=" + baseGold);
|
||
}
|
||
|
||
/// <summary>등급/역할별 추가 드랍 건수(포함 범위 · min > max 면 min 으로 클램프).</summary>
|
||
public static int BurstCount(WLLootSettings s, eSubRol subRole)
|
||
{
|
||
int min, max;
|
||
switch (subRole)
|
||
{
|
||
case eSubRol.Boss: min = s.bossBurstMin; max = s.bossBurstMax; break;
|
||
case eSubRol.Elite: min = s.eliteBurstMin; max = s.eliteBurstMax; break;
|
||
default: return Mathf.Max(0, s.normalBurstCount);
|
||
}
|
||
min = Mathf.Max(0, min);
|
||
max = Mathf.Max(min, max);
|
||
return UnityEngine.Random.Range(min, max + 1);
|
||
}
|
||
|
||
/// <summary>폭발 드랍 1건. 백을 다시 추첨하고, 빈손이면 골드로 대체한다. 러너가 간격을 두고 부른다.</summary>
|
||
internal static void SpawnBurstOne(int bagId, int baseGold, Vector3 position)
|
||
{
|
||
var s = WLLootSettings.Instance;
|
||
if (s == null || DropItemInfo.Ins == null) return;
|
||
if (s.maxConcurrentDrops > 0 && ActiveDropCount() >= s.maxConcurrentDrops) { BurstSkippedCap++; return; }
|
||
|
||
int itemId = 0, amount = 0;
|
||
if (s.burstRollsBag && bagId > 0 && !RollBag(bagId, out itemId, out amount))
|
||
itemId = 0;
|
||
|
||
if (itemId <= 0)
|
||
{
|
||
if (s.burstGoldShare <= 0f) return;
|
||
itemId = GoldItemId;
|
||
amount = Mathf.Max(1, Mathf.RoundToInt(baseGold * s.burstGoldShare));
|
||
}
|
||
|
||
s_lastKillFrame = Time.frameCount; // 이 드랍도 "이 처치의 것" 으로 표시(부채꼴 유지)
|
||
DropItemInfo.Ins.Drop_DropItem(itemId, amount, position);
|
||
BurstDropped++;
|
||
}
|
||
|
||
/// <summary>원본 MobActor.Set_Die 와 같은 방식(행 순서대로 독립 시행 · 첫 성공에서 멈춤)으로 백을 추첨한다.</summary>
|
||
public static bool RollBag(int bagId, out int itemId, out int amount)
|
||
{
|
||
itemId = 0; amount = 0;
|
||
if (table_randombag.Ins == null) return false;
|
||
System.Collections.Generic.List<RandomBagTableData> rows;
|
||
try { rows = table_randombag.Ins.Get_DataList(bagId); }
|
||
catch (Exception) { return false; }
|
||
if (rows == null) return false;
|
||
|
||
for (int i = 0; i < rows.Count; i++)
|
||
{
|
||
if (DSUtil.RandomTrue(rows[i].f_DropRate))
|
||
{
|
||
itemId = rows[i].n_ItemId;
|
||
amount = rows[i].n_DropCount;
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// ═════════════════════════════════════════════════ 산포
|
||
/// <summary>
|
||
/// 드랍 지점 주위 링(min~max)에서 한 점을 고르고 NavMesh 로 스냅한다.
|
||
/// DSUtil.Get_RandomPos_onNavMesh 를 쓰지 않는 이유: 그 함수는 maxradius 를 스냅 반경으로 그대로 써서
|
||
/// 결과가 원점에서 최대 2×maxradius 까지 튄다(DSUtil.cs:969 주석의 실측). 여기서는 반경을 정확히 지킨다.
|
||
/// alloc 0(NavMeshHit 은 struct · 경로 계산 없음).
|
||
/// </summary>
|
||
public static Vector3 SampleScatter(WLLootSettings s, Vector3 origin)
|
||
{
|
||
float radius = UnityEngine.Random.Range(s.scatterRadiusMin, s.scatterRadiusMax);
|
||
float angle = NextFanAngle(s);
|
||
Vector3 p = origin + new Vector3(Mathf.Cos(angle), 0f, Mathf.Sin(angle)) * radius;
|
||
|
||
if (s.scatterNavSnapRadius > 0f)
|
||
{
|
||
NavMeshHit hit;
|
||
if (NavMesh.SamplePosition(p, out hit, s.scatterNavSnapRadius, NavMesh.AllAreas))
|
||
return hit.position;
|
||
// NavMesh 밖(또는 NavMesh 자체가 없는 씬) → 원점 높이를 유지한 링 위치를 그대로 쓴다.
|
||
p.y = origin.y;
|
||
}
|
||
return p;
|
||
}
|
||
|
||
/// <summary>같은 프레임(= 같은 처치)의 드랍들을 황금각으로 흩는다. 새 프레임이면 기준 각을 새로 뽑는다.</summary>
|
||
static float NextFanAngle(WLLootSettings s)
|
||
{
|
||
if (!s.fanOutSameKill) return UnityEngine.Random.Range(0f, Mathf.PI * 2f);
|
||
if (s_fanFrame != Time.frameCount) NewFanGroup();
|
||
float a = s_fanBaseAngle + s_fanIndex * 2.39996f; // 황금각(rad) — 3~5건이 고르게 퍼진다
|
||
s_fanIndex++;
|
||
return a;
|
||
}
|
||
|
||
static void NewFanGroup()
|
||
{
|
||
s_fanFrame = Time.frameCount;
|
||
s_fanIndex = 0;
|
||
s_fanBaseAngle = UnityEngine.Random.Range(0f, Mathf.PI * 2f);
|
||
}
|
||
|
||
// ═════════════════════════════════════════════════ 등급 빔 + 사운드
|
||
static bool TrySpawnBeam(WLLootSettings s, DropItem item, int grade)
|
||
{
|
||
if (grade < s.beamMinGrade) return false;
|
||
|
||
var prefab = BeamPrefab(s, grade);
|
||
if (prefab == null) { BeamSkippedNoPrefab++; return false; }
|
||
|
||
if (s.beamSuppressAboveDrops > 0 && ActiveDropCount() > s.beamSuppressAboveDrops)
|
||
{ BeamSkippedCrowded++; return false; }
|
||
|
||
bool held = false;
|
||
if (s.beamUseEffectBudget)
|
||
{
|
||
if (!EffectBudget.TryAcquire(EffectBudgetKind.Heavy)) { BeamSkippedBudget++; return false; }
|
||
held = true;
|
||
}
|
||
|
||
Transform parent = s.beamParentToDrop ? item.transform : null;
|
||
var go = UnityEngine.Object.Instantiate(prefab, item.transform.position + s.beamOffset, Quaternion.identity, parent);
|
||
if (s.beamScale > 0f && Mathf.Abs(s.beamScale - 1f) > 0.0001f)
|
||
go.transform.localScale = prefab.transform.localScale * s.beamScale;
|
||
|
||
Runner.RegisterBeam(item, go, held, s.beamMaxLifetimeSeconds);
|
||
BeamSpawned++;
|
||
|
||
PlayBeamSound(s, item.transform.position);
|
||
return true;
|
||
}
|
||
|
||
/// <summary>등급 → 빔 프리팹(인덱스 = 등급 − 1). 배열이 짧거나 칸이 비어 있으면 null.</summary>
|
||
public static GameObject BeamPrefab(WLLootSettings s, int grade)
|
||
{
|
||
if (s.beamByGrade == null) return null;
|
||
int idx = grade - 1;
|
||
if (idx < 0 || idx >= s.beamByGrade.Length) return null;
|
||
return s.beamByGrade[idx];
|
||
}
|
||
|
||
static void PlayBeamSound(WLLootSettings s, Vector3 pos)
|
||
{
|
||
if (s.beamSoundVolume <= 0f) return;
|
||
float now = Time.unscaledTime;
|
||
if (s.beamSoundMinIntervalSeconds > 0f && now - s_lastSoundTime < s.beamSoundMinIntervalSeconds) return;
|
||
if (SoundInfo.Ins == null) return;
|
||
s_lastSoundTime = now;
|
||
SoundInfo.Ins.Play_OneShot_byDistance(s.beamSound, pos, s.beamSoundVolume);
|
||
SoundPlayed++;
|
||
}
|
||
|
||
// ═════════════════════════════════════════════════ 등급 조회
|
||
/// <summary>아이템 등급(table_itemlist.n_ItemGrade). 테이블이 없거나 미등록 ID 면 1.</summary>
|
||
public static int ResolveGrade(int itemId)
|
||
{
|
||
if (itemId == GoldItemId) return 1;
|
||
if (table_itemlist.Ins == null) return 1;
|
||
try
|
||
{
|
||
var d = table_itemlist.Ins.Get_Data(itemId);
|
||
return d != null ? Mathf.Clamp(d.n_ItemGrade, 1, WLLootSettings.GradeSlots) : 1;
|
||
}
|
||
catch (Exception) { return 1; }
|
||
}
|
||
|
||
// ═════════════════════════════════════════════════ 자동 줍기 — 원본 자석 상태기 켜기(리플렉션)
|
||
static FieldInfo s_fMagnet, s_fMagnetUp, s_fMagnetUpTime, s_fProjectileData;
|
||
static FieldInfo s_fSpeed;
|
||
static bool s_reflectionDone, s_reflectionOk;
|
||
|
||
static void EnsureReflection()
|
||
{
|
||
if (s_reflectionDone) return;
|
||
s_reflectionDone = true;
|
||
const BindingFlags F = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public;
|
||
var t = typeof(DropItem);
|
||
s_fMagnet = t.GetField("m_Magnet", F);
|
||
s_fMagnetUp = t.GetField("v3_MagnetUP", F);
|
||
s_fMagnetUpTime = t.GetField("MagnetUpTime", F);
|
||
s_fProjectileData = typeof(ProjectileBase).GetField("_projectileData", F);
|
||
if (s_fProjectileData != null) s_fSpeed = s_fProjectileData.FieldType.GetField("Speed", F);
|
||
s_reflectionOk = s_fMagnet != null && s_fMagnetUp != null;
|
||
if (!s_reflectionOk) LastNote = "reflection-miss";
|
||
}
|
||
|
||
/// <summary>리플렉션으로 원본 자석 필드를 찾았는가(프로브가 읽는다).</summary>
|
||
public static bool ReflectionOk { get { EnsureReflection(); return s_reflectionOk; } }
|
||
|
||
/// <summary>이 드랍이 이미 자석 상태인가(0 = 아님).</summary>
|
||
public static int MagnetState(DropItem item)
|
||
{
|
||
EnsureReflection();
|
||
if (!s_reflectionOk || item == null) return -1;
|
||
return (int)s_fMagnet.GetValue(item);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 자동 줍기 판정(러너와 프로브가 같은 식을 쓴다).
|
||
/// 착지가 끝나(원본이 m_Collider 를 켠) 아직 자석이 아닌 드랍이 PC 중심 반경 안에 있으면 true.
|
||
/// </summary>
|
||
public static bool InPickupRadius(DropItem item, Vector3 pcCenter, WLLootSettings s)
|
||
{
|
||
if (item == null || s == null) return false;
|
||
if (item.m_Collider == null || !item.m_Collider.enabled) return false;
|
||
return (item.transform.position - pcCenter).sqrMagnitude <= s.autoPickupRadius * s.autoPickupRadius;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 원본 자석 상태기를 1단계로 켠다(원본 DropItem.cs:81-91 의 주석 코드와 같은 값 세팅).
|
||
/// 이동·획득은 전부 원본 Update / ProcessCollider 가 한다 — 우리는 스위치만 켠다.
|
||
/// </summary>
|
||
public static bool EngageMagnet(DropItem item)
|
||
{
|
||
EnsureReflection();
|
||
if (!s_reflectionOk || item == null) return false;
|
||
var s = WLLootSettings.Instance;
|
||
if (s == null) return false;
|
||
if ((int)s_fMagnet.GetValue(item) != 0) return false;
|
||
|
||
s_fMagnetUp.SetValue(item, item.transform.position + Vector3.up * s.magnetRiseHeight);
|
||
if (s_fMagnetUpTime != null) s_fMagnetUpTime.SetValue(item, 0f);
|
||
if (s_fProjectileData != null && s_fSpeed != null)
|
||
{
|
||
var pd = s_fProjectileData.GetValue(item);
|
||
if (pd != null) s_fSpeed.SetValue(pd, s.magnetRiseSpeed);
|
||
}
|
||
s_fMagnet.SetValue(item, 1);
|
||
MagnetEngaged++;
|
||
return true;
|
||
}
|
||
|
||
// ═════════════════════════════════════════════════ 진단 · 정리
|
||
/// <summary>지금 살아 있는(활성) 드랍 오브젝트 수. 러너가 세는 값(원본 풀을 훑지 않는다 = alloc 0).</summary>
|
||
public static int ActiveDropCount() { return s_runner != null ? s_runner.ActiveCount : 0; }
|
||
|
||
/// <summary>빔 슬롯을 몇 개 들고 있나(러너 기준).</summary>
|
||
public static int HeldBeamBudget { get { return s_runner != null ? s_runner.HeldBudget : 0; } }
|
||
|
||
/// <summary>프로브·씬 전환용: 빔을 전부 파괴하고 EffectBudget 슬롯을 반납한다.</summary>
|
||
public static void CleanupAll() { if (s_runner != null) s_runner.CleanupAll(); }
|
||
|
||
/// <summary>프로브용: 카운터 초기화(구독·러너는 유지).</summary>
|
||
public static void ResetDiagnostics()
|
||
{
|
||
KilledHandled = DropsSeen = Scattered = BeamSpawned = BeamSkippedBudget = BeamSkippedNoPrefab =
|
||
BeamSkippedCrowded = SoundPlayed = PickedCount = MagnetEngaged =
|
||
BurstPlanned = BurstDropped = BurstSkippedCap = BurstSkippedBudget = 0;
|
||
LastScatterDistance = LastBounceHeight = 0f;
|
||
LastGrade = LastBurstCount = 0; LastNote = "";
|
||
s_lastSoundTime = -999f; s_lastKillFrame = -1; s_lastKillSubRole = eSubRol.None;
|
||
Dropped.Raised = Dropped.Dispatched = 0; Picked.Raised = Picked.Dispatched = 0;
|
||
}
|
||
|
||
/// <summary>마지막으로 통지된 처치의 역할(프로브 검증용).</summary>
|
||
public static eSubRol LastKillSubRole { get { return s_lastKillSubRole; } }
|
||
}
|
||
}
|