[WL-813f] 전리품 연출 + 드랍률 (#813)
CombatEvents.Killed 구독 → 드랍 산포(0.6~1.2 m · 등급별 튀는 높이 0.35+0.08/등급) · 등급 4 이상 빔(보유 FX_LootDrop_Blue/Purple/Gold) + s015_GoldPowerUp · 자동 줍기 반경 1.5 m(원본 자석 상태기를 리플렉션으로 켜기만) · 보스 3~5 · 엘리트 1~2 폭발 드랍(RandomBag 재추첨 · 빈손이면 골드 25%). 값은 전부 WLLootSettings.asset(C45) · 동시 빔은 EffectBudget.Heavy 등록. 원본 수정 = DropItem.cs 2줄(Set 산포 훅 · ItemTouch 획득 통지 훅) 뿐. Actor.cs · MobActor.cs · DropItemInfo.cs · Assets/WL/Combat/Core/** 0줄. 데이터(S): RandomBag.json 새 백 8130001 5행(총 드랍 20.95% · 기준서 15~25%) · MonsterAppear.json 813e 존 6행(813001~813005 · 813010)의 n_DropReward 만 새 백으로 · 나머지 46행 · RandomBag 기존 197행 바이트 무변경. ⚠ butler.xlsm 동기화 필요. 검증: 게이트 PASS(oneshot 1.4분 · error CS 0 / resident up_to_date · 콘솔 error 0) · 에디트 모드 프로브(AgentScripts/WL813f_Probe.cs · 덤프 WL813f_PROBE.txt): 산포 1000회 0.601~1.199 m · 폭발 2000회 boss 3~5 · elite 1~2 · normal 0 · 가짜 Killed 3건 구독 동작 · 자석 리플렉션 OK(0→1 · 중복 방지) · 줍기 경계 1.49 in / 1.51 out / 착지 전 제외 · 빔 4건 → Heavy 상한 2 스폰 · 2 스킵 → CleanupAll 후 슬롯 0 · C8(RuntimeDisabled) 시 산포/빔/획득/폭발 전부 0. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
36679cc358
commit
edb74f22a4
|
|
@ -0,0 +1,35 @@
|
|||
=== WL-813f 전리품 연출 프로브(에디트 모드) 01:18:07 ===
|
||||
|
||||
[a] WLLootSettings — Enabled=True verbose=False
|
||||
산포 0.6~1.2 m · navSnap 0.8 · 튀기 base 0.35 +0.08/등급 · max 1
|
||||
빔 minGrade 4 · scale 0.6 · maxLife 21 · budget True · sound s015_GoldPowerUp vol 0.5
|
||||
줍기 True r=1.5 m · scan 0.1 s · magnet rise 3/5
|
||||
폭발 elite 1~2 · boss 3~5 · normal 0 · 간격 0.07 s · bagRoll True · goldShare 0.25 · cluster False
|
||||
상한 maxConcurrentDrops 24 · beamSuppressAbove 12
|
||||
등급별 빔: 1=- 2=- 3=- 4=FX_LootDrop_Blue 5=FX_LootDrop_Purple 6=FX_LootDrop_Gold 7=FX_LootDrop_Purple 8=FX_LootDrop_Gold 9=FX_LootDrop_Purple
|
||||
|
||||
[b] 산포 1000회 — min 0.601 · max 1.199 · mean 0.888 m (설정 0.6~1.2) · NavMesh 없음 → 링 좌표 그대로
|
||||
[c] 튀는 높이(m) — 1:0.35 2:0.43 3:0.51 4:0.59 5:0.67 6:0.75 7:0.83 8:0.91 9:0.99
|
||||
[d] 폭발 건수 2000회 — boss 3:696 4:667 5:637 · elite 1:1001 2:999 · normal 합계 0
|
||||
|
||||
[e] 가짜 Killed — core Enabled=1 · 구독 True · Killed 처리 3 · boss 계획 3(누적 3) · elite 1(누적 4) · normal 0(누적 4) · 실제 드랍 0 (DropItemInfo.Ins=False · table_randombag.Ins=False)
|
||||
|
||||
[f] 자석 리플렉션 OK=True · m_Magnet 0→1 (engage True · 재호출 False = 중복 방지) · MagnetEngaged 1
|
||||
[g] 줍기 반경 1.5 m — 1.49m True · 1.51m False · 착지 전(콜라이더 off) False
|
||||
|
||||
[h] 등급 판정 — 아이템 10101016 → ResolveGrade 1 (table_itemlist.Ins=False · 에디트 모드는 테이블 미로드 → 항상 1)
|
||||
빔 강제(minGrade 1 · 프리팹 FX_LootDrop_Blue) 4건 — Heavy 상한 2 · 스폰 2 · 예산 스킵 2 · 프리팹 없음 0 · Heavy Count 2 · 사운드 0(SoundInfo.Ins=False) · 등록 드랍 4 · 산포 4
|
||||
빔 자식 수 2 (drop 에 붙었는가 · beamParentToDrop=True)
|
||||
CleanupAll 후 Heavy Count 0 · 등록 드랍 0 · 남은 빔 자식 0
|
||||
설정 복원 — beamMinGrade 4 · 등급 1 칸 null(원본) · 에셋 저장 0(SetDirty/SaveAssets 미호출)
|
||||
|
||||
[j] C8 RuntimeDisabled — start 불변 True · target 불변 True · DropsSeen 0 · Picked 0 · Killed 처리 0 · 빔 0 · Heavy Count 0
|
||||
복구 후 — start 변경 True · target 변경 True · DropsSeen 1 · 빔 0
|
||||
|
||||
[i] 원본 풀 상한(DropItemInfo.Start 고정) — 코인(itemid 2) 30 · 아이템(-1) 2 · 초과 요청은 조용히 무시 · 우리 상한 maxConcurrentDrops 24 (폭발 추가분만 막고 기본 드랍은 막지 않는다)
|
||||
|
||||
[k] 데이터 — RandomBag 총 202행 · 새 백 8130001 5행 g1/10101001@0.12 g2/10101006@0.06 g3/10101011@0.02 g4/10101016@0.02 g5/10101021@0.005
|
||||
새 백 총 드랍 확률 = 20.95% (기준서 15~25%) · 원본 순차 추첨(첫 성공에서 break) 기준
|
||||
MonsterAppear 52행 — 새 백 참조 6행(813e 존 5 + 보스 1) · 9700001 유지 46행
|
||||
|
||||
정리 완료 — Heavy Count 0 · ClusterBurst Count 0
|
||||
|
|
@ -0,0 +1,377 @@
|
|||
// 발주서 WL-813f — 전리품 연출(산포 · 등급 빔 · 자동 줍기 · 폭발 드랍) 검증 프로브
|
||||
//
|
||||
// 실행 (CLAUDE.md §1: using 금지 · 네임스페이스 풀어 쓰기)
|
||||
// 에디트 모드 : unity command run_script --file AgentScripts/WL813f_Probe.cs --entry WL813f_Probe.Edit --timeout 300
|
||||
// Play(인게임): unity command run_script --file AgentScripts/WL813f_Probe.cs --entry WL813f_Probe.Status
|
||||
// unity command run_script --file AgentScripts/WL813f_Probe.cs --entry WL813f_Probe.Watch --args '[12.0]'
|
||||
//
|
||||
// Edit() 가 재는 것 — 씬/프리팹/에셋 변경 0(가짜 오브젝트는 전부 DestroyImmediate 로 회수):
|
||||
// (a) WLLootSettings 로드 · 값 전수 · 등급 빔 프리팹 참조 해소(등급 1~3 null · 4~9 실물)
|
||||
// (b) 산포 표집 1000회 — 반경 min/max/mean(설정 0.6~1.2 준수) · 부채꼴 각 분포
|
||||
// (c) 등급별 튀는 높이 1~9
|
||||
// (d) 폭발 건수 분포 — 보스 3~5 · 엘리트 1~2 · 일반 0 (2000회)
|
||||
// (e) 가짜 Killed(비활성 MobActor) → 구독 동작 · BurstPlanned · C8 off 시 0
|
||||
// (f) 자석 리플렉션 — 원본 private 필드 3종 + ProjectileData.Speed 를 찾았는가 · EngageMagnet 전/후 상태
|
||||
// (g) 자동 줍기 반경 경계 — 1.5 m 안/밖 · 콜라이더 꺼짐(착지 전) 제외
|
||||
// (h) 등급 빔 + EffectBudget.Heavy 상한(코어 값) — 초과분 스킵 · CleanupAll 후 슬롯 0
|
||||
// (i) 동시 드랍 상한 · 원본 풀 상한(DropItemInfo: 코인 30 · 아이템 2)
|
||||
// (j) C8 — RuntimeDisabled 시 훅 2개가 원본 값을 그대로 두는가
|
||||
// (k) 데이터 — RandomBag 새 백 8130001 5행 총확률 · MonsterAppear 813e 6행 참조
|
||||
//
|
||||
// 수치는 전부 SO/테이블에서 읽는다(C45). 프로브 상수는 "어디를 보나" 뿐이다.
|
||||
|
||||
public static class WL813f_Probe
|
||||
{
|
||||
const string kBagJson = "Assets/ResWork/Table/Export/RandomBag.json";
|
||||
const string kAppearJson = "Assets/ResWork/Table/Export/MonsterAppear.json";
|
||||
const string kOutDir = "AgentScripts/staging/WL813f";
|
||||
const int kNewBagId = 8130001;
|
||||
|
||||
static System.Text.StringBuilder s;
|
||||
static void L(string t) { s.AppendLine(t); UnityEngine.Debug.Log("[WL813f] " + t); }
|
||||
|
||||
static string Abs(string rel)
|
||||
{
|
||||
return System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(),
|
||||
rel.Replace('/', System.IO.Path.DirectorySeparatorChar));
|
||||
}
|
||||
|
||||
static string Flush(string name)
|
||||
{
|
||||
try
|
||||
{
|
||||
System.IO.Directory.CreateDirectory(Abs(kOutDir));
|
||||
System.IO.File.WriteAllText(Abs(kOutDir + "/" + name), s.ToString(), new System.Text.UTF8Encoding(true));
|
||||
}
|
||||
catch (System.Exception e) { UnityEngine.Debug.LogWarning("[WL813f] 로그 저장 실패: " + e.Message); }
|
||||
return s.ToString();
|
||||
}
|
||||
|
||||
// ── 아주 얕은 JSON 배열 파서(행 = {"k": "v", ...}) ─────────────────────────
|
||||
static System.Collections.Generic.List<System.Collections.Generic.Dictionary<string, string>> ReadRows(string path)
|
||||
{
|
||||
var list = new System.Collections.Generic.List<System.Collections.Generic.Dictionary<string, string>>();
|
||||
string txt = System.IO.File.ReadAllText(Abs(path));
|
||||
foreach (System.Text.RegularExpressions.Match m in
|
||||
System.Text.RegularExpressions.Regex.Matches(txt, "\\{[^{}]*\\}"))
|
||||
{
|
||||
var d = new System.Collections.Generic.Dictionary<string, string>();
|
||||
foreach (System.Text.RegularExpressions.Match kv in
|
||||
System.Text.RegularExpressions.Regex.Matches(m.Value, "\"([^\"]+)\"\\s*:\\s*\"([^\"]*)\""))
|
||||
d[kv.Groups[1].Value] = kv.Groups[2].Value;
|
||||
list.Add(d);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
// ── 가짜 드랍(비활성 GO → Awake 실행 안 됨 · 콜라이더는 직접 붙인다) ────────
|
||||
static DropItem MakeFakeDrop(string name, UnityEngine.Vector3 pos, bool colliderOn)
|
||||
{
|
||||
var go = new UnityEngine.GameObject(name);
|
||||
go.hideFlags = UnityEngine.HideFlags.HideAndDontSave;
|
||||
go.SetActive(false);
|
||||
var di = go.AddComponent<DropItem>();
|
||||
var col = go.AddComponent<UnityEngine.SphereCollider>();
|
||||
col.isTrigger = true;
|
||||
col.enabled = colliderOn;
|
||||
di.m_Collider = col;
|
||||
go.transform.position = pos;
|
||||
go.SetActive(true); // Awake 는 이미 지나갔다(비활성 상태로 AddComponent)
|
||||
return di;
|
||||
}
|
||||
|
||||
static Actor MakeFakeMob(string name, eSubRol subRole, UnityEngine.Vector3 pos)
|
||||
{
|
||||
var go = new UnityEngine.GameObject(name);
|
||||
go.hideFlags = UnityEngine.HideFlags.HideAndDontSave;
|
||||
go.SetActive(false); // 비활성으로 만들어 Awake/Start 를 태우지 않는다
|
||||
var a = go.AddComponent<MobActor>();
|
||||
a.m_SubRole = subRole;
|
||||
go.transform.position = pos;
|
||||
return a;
|
||||
}
|
||||
|
||||
static void Kill(UnityEngine.Object o) { if (o != null) UnityEngine.Object.DestroyImmediate(o); }
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════ Edit
|
||||
public static string Edit()
|
||||
{
|
||||
s = new System.Text.StringBuilder();
|
||||
L("=== WL-813f 전리품 연출 프로브(에디트 모드) " + System.DateTime.Now.ToString("HH:mm:ss") + " ===");
|
||||
|
||||
WL.Combat.Loot.WLLootSettings.ClearCache();
|
||||
var cfg = WL.Combat.Loot.WLLootSettings.Instance;
|
||||
if (cfg == null) { L("!! WLLootSettings.asset 로드 실패 — Resources/WL/WLLootSettings"); return Flush("EDIT.txt"); }
|
||||
|
||||
WL.Combat.Loot.LootBurst.EnsureSubscribed();
|
||||
WL.Combat.Loot.LootBurst.ResetDiagnostics();
|
||||
WL.Combat.Core.EffectBudget.ResetAll();
|
||||
|
||||
// (a) 설정 · 빔 프리팹 참조
|
||||
L("");
|
||||
L("[a] WLLootSettings — Enabled=" + WL.Combat.Loot.WLLootSettings.Enabled + " verbose=" + cfg.verboseLog);
|
||||
L(" 산포 " + cfg.scatterRadiusMin + "~" + cfg.scatterRadiusMax + " m · navSnap " + cfg.scatterNavSnapRadius +
|
||||
" · 튀기 base " + cfg.bounceHeightBase + " +" + cfg.bounceHeightPerGrade + "/등급 · max " + cfg.bounceHeightMax);
|
||||
L(" 빔 minGrade " + cfg.beamMinGrade + " · scale " + cfg.beamScale + " · maxLife " + cfg.beamMaxLifetimeSeconds +
|
||||
" · budget " + cfg.beamUseEffectBudget + " · sound " + cfg.beamSound + " vol " + cfg.beamSoundVolume);
|
||||
L(" 줍기 " + cfg.autoPickupEnabled + " r=" + cfg.autoPickupRadius + " m · scan " + cfg.autoPickupScanIntervalSeconds +
|
||||
" s · magnet rise " + cfg.magnetRiseHeight + "/" + cfg.magnetRiseSpeed);
|
||||
L(" 폭발 elite " + cfg.eliteBurstMin + "~" + cfg.eliteBurstMax + " · boss " + cfg.bossBurstMin + "~" + cfg.bossBurstMax +
|
||||
" · normal " + cfg.normalBurstCount + " · 간격 " + cfg.burstIntervalSeconds + " s · bagRoll " + cfg.burstRollsBag +
|
||||
" · goldShare " + cfg.burstGoldShare + " · cluster " + cfg.burstUseClusterBudget);
|
||||
L(" 상한 maxConcurrentDrops " + cfg.maxConcurrentDrops + " · beamSuppressAbove " + cfg.beamSuppressAboveDrops);
|
||||
var sb = new System.Text.StringBuilder();
|
||||
for (int g = 1; g <= WL.Combat.Loot.WLLootSettings.GradeSlots; g++)
|
||||
{
|
||||
var pf = WL.Combat.Loot.LootBurst.BeamPrefab(cfg, g);
|
||||
sb.Append(g).Append("=").Append(pf != null ? pf.name : "-").Append(" ");
|
||||
}
|
||||
L(" 등급별 빔: " + sb.ToString().Trim());
|
||||
|
||||
// (b) 산포 표집
|
||||
L("");
|
||||
var origin = new UnityEngine.Vector3(10f, 0f, 10f);
|
||||
float mn = 999f, mx = 0f, sum = 0f; int n = 1000;
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
var p = WL.Combat.Loot.LootBurst.SampleScatter(cfg, origin);
|
||||
float d = UnityEngine.Vector3.Distance(new UnityEngine.Vector3(origin.x, 0f, origin.z),
|
||||
new UnityEngine.Vector3(p.x, 0f, p.z));
|
||||
if (d < mn) mn = d; if (d > mx) mx = d; sum += d;
|
||||
}
|
||||
L("[b] 산포 " + n + "회 — min " + mn.ToString("F3") + " · max " + mx.ToString("F3") +
|
||||
" · mean " + (sum / n).ToString("F3") + " m (설정 " + cfg.scatterRadiusMin + "~" + cfg.scatterRadiusMax +
|
||||
") · NavMesh 없음 → 링 좌표 그대로");
|
||||
|
||||
// (c) 등급별 튀는 높이
|
||||
var hb = new System.Text.StringBuilder();
|
||||
for (int g = 1; g <= 9; g++)
|
||||
{
|
||||
float h = UnityEngine.Mathf.Min(cfg.bounceHeightMax,
|
||||
cfg.bounceHeightBase + UnityEngine.Mathf.Max(0, g - 1) * cfg.bounceHeightPerGrade);
|
||||
hb.Append(g).Append(":").Append(h.ToString("F2")).Append(" ");
|
||||
}
|
||||
L("[c] 튀는 높이(m) — " + hb.ToString().Trim());
|
||||
|
||||
// (d) 폭발 건수 분포
|
||||
int[] boss = new int[8], elite = new int[8]; int normal = 0;
|
||||
for (int i = 0; i < 2000; i++)
|
||||
{
|
||||
boss[UnityEngine.Mathf.Clamp(WL.Combat.Loot.LootBurst.BurstCount(cfg, eSubRol.Boss), 0, 7)]++;
|
||||
elite[UnityEngine.Mathf.Clamp(WL.Combat.Loot.LootBurst.BurstCount(cfg, eSubRol.Elite), 0, 7)]++;
|
||||
normal += WL.Combat.Loot.LootBurst.BurstCount(cfg, eSubRol.None);
|
||||
}
|
||||
L("[d] 폭발 건수 2000회 — boss " + Hist(boss) + " · elite " + Hist(elite) + " · normal 합계 " + normal);
|
||||
|
||||
// (e) 가짜 Killed
|
||||
L("");
|
||||
var mobB = MakeFakeMob("__fakeBoss", eSubRol.Boss, new UnityEngine.Vector3(5f, 0f, 5f));
|
||||
var mobE = MakeFakeMob("__fakeElite", eSubRol.Elite, new UnityEngine.Vector3(6f, 0f, 5f));
|
||||
var mobN = MakeFakeMob("__fakeNormal", eSubRol.None, new UnityEngine.Vector3(7f, 0f, 5f));
|
||||
WL.Combat.Loot.LootBurst.ResetDiagnostics();
|
||||
int coreOn = WL.Combat.Core.WLCombatCoreSettings.Enabled ? 1 : 0;
|
||||
WL.Combat.Core.CombatEvents.RaiseKilled(mobB);
|
||||
int p1 = WL.Combat.Loot.LootBurst.BurstPlanned; int c1 = WL.Combat.Loot.LootBurst.LastBurstCount;
|
||||
WL.Combat.Core.CombatEvents.RaiseKilled(mobE);
|
||||
int p2 = WL.Combat.Loot.LootBurst.BurstPlanned; int c2 = WL.Combat.Loot.LootBurst.LastBurstCount;
|
||||
WL.Combat.Core.CombatEvents.RaiseKilled(mobN);
|
||||
int p3 = WL.Combat.Loot.LootBurst.BurstPlanned; int c3 = WL.Combat.Loot.LootBurst.LastBurstCount;
|
||||
L("[e] 가짜 Killed — core Enabled=" + coreOn + " · 구독 " + WL.Combat.Loot.LootBurst.Subscribed +
|
||||
" · Killed 처리 " + WL.Combat.Loot.LootBurst.KilledHandled +
|
||||
" · boss 계획 " + c1 + "(누적 " + p1 + ") · elite " + c2 + "(누적 " + p2 + ") · normal " + c3 + "(누적 " + p3 + ")" +
|
||||
" · 실제 드랍 " + WL.Combat.Loot.LootBurst.BurstDropped +
|
||||
" (DropItemInfo.Ins=" + (DropItemInfo.Ins != null) + " · table_randombag.Ins=" + (table_randombag.Ins != null) + ")");
|
||||
|
||||
// (f) 자석 리플렉션
|
||||
L("");
|
||||
var d1 = MakeFakeDrop("__fakeDrop1", new UnityEngine.Vector3(0f, 0f, 0f), true);
|
||||
int before = WL.Combat.Loot.LootBurst.MagnetState(d1);
|
||||
bool eng = WL.Combat.Loot.LootBurst.EngageMagnet(d1);
|
||||
int after = WL.Combat.Loot.LootBurst.MagnetState(d1);
|
||||
bool again = WL.Combat.Loot.LootBurst.EngageMagnet(d1);
|
||||
L("[f] 자석 리플렉션 OK=" + WL.Combat.Loot.LootBurst.ReflectionOk +
|
||||
" · m_Magnet " + before + "→" + after + " (engage " + eng + " · 재호출 " + again + " = 중복 방지)" +
|
||||
" · MagnetEngaged " + WL.Combat.Loot.LootBurst.MagnetEngaged);
|
||||
|
||||
// (g) 자동 줍기 반경 경계
|
||||
var pc = new UnityEngine.Vector3(0f, 0f, 0f);
|
||||
float r = cfg.autoPickupRadius;
|
||||
var din = MakeFakeDrop("__in", new UnityEngine.Vector3(r - 0.01f, 0f, 0f), true);
|
||||
var dout = MakeFakeDrop("__out", new UnityEngine.Vector3(r + 0.01f, 0f, 0f), true);
|
||||
var dair = MakeFakeDrop("__air", new UnityEngine.Vector3(r - 0.5f, 0f, 0f), false); // 아직 착지 전(콜라이더 off)
|
||||
L("[g] 줍기 반경 " + r + " m — " + (r - 0.01f).ToString("F2") + "m " +
|
||||
WL.Combat.Loot.LootBurst.InPickupRadius(din, pc, cfg) + " · " + (r + 0.01f).ToString("F2") + "m " +
|
||||
WL.Combat.Loot.LootBurst.InPickupRadius(dout, pc, cfg) + " · 착지 전(콜라이더 off) " +
|
||||
WL.Combat.Loot.LootBurst.InPickupRadius(dair, pc, cfg));
|
||||
|
||||
// (h) 등급 빔 + EffectBudget
|
||||
// 에디트 모드에는 table_itemlist.Ins 가 없어 ResolveGrade 가 항상 1 이다(런타임 등급 판정은 QA).
|
||||
// 그래서 **메모리에서만** beamMinGrade 를 1 로 내리고 등급 1 칸에 등급 4 빔을 꽂아 스폰 경로를 실제로 태운다.
|
||||
// 에셋 파일은 저장하지 않는다(SetDirty·SaveAssets 호출 0) — 끝나면 원래 값으로 되돌리고 git status 로 확인한다.
|
||||
L("");
|
||||
WL.Combat.Loot.LootBurst.ResetDiagnostics();
|
||||
WL.Combat.Core.EffectBudget.ResetAll();
|
||||
int heavyLimit = WL.Combat.Core.EffectBudget.Limit(WL.Combat.Core.EffectBudgetKind.Heavy);
|
||||
int probeItemId = FindItemIdOfGrade(cfg.beamMinGrade);
|
||||
L("[h] 등급 판정 — 아이템 " + probeItemId + " → ResolveGrade " + WL.Combat.Loot.LootBurst.ResolveGrade(probeItemId) +
|
||||
" (table_itemlist.Ins=" + (table_itemlist.Ins != null) + " · 에디트 모드는 테이블 미로드 → 항상 1)");
|
||||
|
||||
int savedMinGrade = cfg.beamMinGrade;
|
||||
var savedSlot0 = cfg.beamByGrade[0];
|
||||
cfg.beamByGrade[0] = WL.Combat.Loot.LootBurst.BeamPrefab(cfg, savedMinGrade); // 등급 1 칸 = 등급 4 빔(임시)
|
||||
cfg.beamMinGrade = 1;
|
||||
|
||||
var drops = new System.Collections.Generic.List<DropItem>();
|
||||
for (int i = 0; i < heavyLimit + 2; i++)
|
||||
{
|
||||
var di = MakeFakeDrop("__beam" + i, new UnityEngine.Vector3(i, 0f, 0f), true);
|
||||
drops.Add(di);
|
||||
var sp = new UnityEngine.Vector3(i, 0f, 0f);
|
||||
var tp = sp;
|
||||
WL.Combat.Loot.LootBurst.OnDropSpawn(di, probeItemId, 1, ref sp, ref tp);
|
||||
}
|
||||
L(" 빔 강제(minGrade 1 · 프리팹 " + (cfg.beamByGrade[0] != null ? cfg.beamByGrade[0].name : "-") + ") " +
|
||||
(heavyLimit + 2) + "건 — Heavy 상한 " + heavyLimit + " · 스폰 " + WL.Combat.Loot.LootBurst.BeamSpawned +
|
||||
" · 예산 스킵 " + WL.Combat.Loot.LootBurst.BeamSkippedBudget +
|
||||
" · 프리팹 없음 " + WL.Combat.Loot.LootBurst.BeamSkippedNoPrefab +
|
||||
" · Heavy Count " + WL.Combat.Core.EffectBudget.Count(WL.Combat.Core.EffectBudgetKind.Heavy) +
|
||||
" · 사운드 " + WL.Combat.Loot.LootBurst.SoundPlayed + "(SoundInfo.Ins=" + (SoundInfo.Ins != null) + ")" +
|
||||
" · 등록 드랍 " + WL.Combat.Loot.LootBurst.ActiveDropCount() +
|
||||
" · 산포 " + WL.Combat.Loot.LootBurst.Scattered);
|
||||
int beamChildren = 0;
|
||||
for (int i = 0; i < drops.Count; i++) beamChildren += drops[i].transform.childCount;
|
||||
L(" 빔 자식 수 " + beamChildren + " (drop 에 붙었는가 · beamParentToDrop=" + cfg.beamParentToDrop + ")");
|
||||
WL.Combat.Loot.LootBurst.CleanupAll();
|
||||
int beamAfter = 0;
|
||||
for (int i = 0; i < drops.Count; i++) beamAfter += drops[i].transform.childCount;
|
||||
L(" CleanupAll 후 Heavy Count " + WL.Combat.Core.EffectBudget.Count(WL.Combat.Core.EffectBudgetKind.Heavy) +
|
||||
" · 등록 드랍 " + WL.Combat.Loot.LootBurst.ActiveDropCount() + " · 남은 빔 자식 " + beamAfter);
|
||||
|
||||
cfg.beamMinGrade = savedMinGrade;
|
||||
cfg.beamByGrade[0] = savedSlot0;
|
||||
L(" 설정 복원 — beamMinGrade " + cfg.beamMinGrade + " · 등급 1 칸 " +
|
||||
(cfg.beamByGrade[0] == null ? "null(원본)" : cfg.beamByGrade[0].name) + " · 에셋 저장 0(SetDirty/SaveAssets 미호출)");
|
||||
|
||||
// (j) C8 — 꺼짐이면 원본 값 그대로
|
||||
L("");
|
||||
WL.Combat.Loot.LootBurst.ResetDiagnostics();
|
||||
WL.Combat.Loot.WLLootSettings.RuntimeDisabled = true;
|
||||
var dC8 = MakeFakeDrop("__c8", new UnityEngine.Vector3(0f, 0f, 0f), true);
|
||||
var sp0 = new UnityEngine.Vector3(1f, 2f, 3f); var tp0 = new UnityEngine.Vector3(4f, 5f, 6f);
|
||||
var spK = sp0; var tpK = tp0;
|
||||
WL.Combat.Loot.LootBurst.OnDropSpawn(dC8, probeItemId, 1, ref spK, ref tpK);
|
||||
WL.Combat.Loot.LootBurst.OnDropPicked(dC8, probeItemId, 1, null);
|
||||
WL.Combat.Core.CombatEvents.RaiseKilled(mobB);
|
||||
L("[j] C8 RuntimeDisabled — start 불변 " + (spK == sp0) + " · target 불변 " + (tpK == tp0) +
|
||||
" · DropsSeen " + WL.Combat.Loot.LootBurst.DropsSeen + " · Picked " + WL.Combat.Loot.LootBurst.PickedCount +
|
||||
" · Killed 처리 " + WL.Combat.Loot.LootBurst.KilledHandled + " · 빔 " + WL.Combat.Loot.LootBurst.BeamSpawned +
|
||||
" · Heavy Count " + WL.Combat.Core.EffectBudget.Count(WL.Combat.Core.EffectBudgetKind.Heavy));
|
||||
WL.Combat.Loot.WLLootSettings.RuntimeDisabled = false;
|
||||
var spR = sp0; var tpR = tp0;
|
||||
WL.Combat.Loot.LootBurst.OnDropSpawn(dC8, probeItemId, 1, ref spR, ref tpR);
|
||||
L(" 복구 후 — start 변경 " + (spR != sp0) + " · target 변경 " + (tpR != tp0) +
|
||||
" · DropsSeen " + WL.Combat.Loot.LootBurst.DropsSeen + " · 빔 " + WL.Combat.Loot.LootBurst.BeamSpawned);
|
||||
WL.Combat.Loot.LootBurst.CleanupAll();
|
||||
|
||||
// (i) 원본 풀 상한
|
||||
L("");
|
||||
L("[i] 원본 풀 상한(DropItemInfo.Start 고정) — 코인(itemid 2) 30 · 아이템(-1) 2 · 초과 요청은 조용히 무시" +
|
||||
" · 우리 상한 maxConcurrentDrops " + cfg.maxConcurrentDrops + " (폭발 추가분만 막고 기본 드랍은 막지 않는다)");
|
||||
|
||||
// (k) 데이터
|
||||
L("");
|
||||
var bagRows = ReadRows(kBagJson);
|
||||
double pMiss = 1.0; int newRows = 0;
|
||||
var bagDetail = new System.Text.StringBuilder();
|
||||
for (int i = 0; i < bagRows.Count; i++)
|
||||
{
|
||||
if (bagRows[i]["n_DropIndex"] != kNewBagId.ToString()) continue;
|
||||
newRows++;
|
||||
double rate = double.Parse(bagRows[i]["f_DropRate"], System.Globalization.CultureInfo.InvariantCulture);
|
||||
pMiss *= (1.0 - rate);
|
||||
bagDetail.Append("g").Append(bagRows[i]["n_ItemGrade"]).Append("/").Append(bagRows[i]["n_ItemId"])
|
||||
.Append("@").Append(bagRows[i]["f_DropRate"]).Append(" ");
|
||||
}
|
||||
var appearRows = ReadRows(kAppearJson);
|
||||
int pointed = 0, old = 0;
|
||||
for (int i = 0; i < appearRows.Count; i++)
|
||||
{
|
||||
if (appearRows[i]["n_DropReward"] == kNewBagId.ToString()) pointed++;
|
||||
else old++;
|
||||
}
|
||||
L("[k] 데이터 — RandomBag 총 " + bagRows.Count + "행 · 새 백 " + kNewBagId + " " + newRows + "행 " + bagDetail.ToString().Trim());
|
||||
L(" 새 백 총 드랍 확률 = " + ((1.0 - pMiss) * 100.0).ToString("F2") + "% (기준서 15~25%) · 원본 순차 추첨(첫 성공에서 break) 기준");
|
||||
L(" MonsterAppear " + appearRows.Count + "행 — 새 백 참조 " + pointed + "행(813e 존 5 + 보스 1) · 9700001 유지 " + old + "행");
|
||||
|
||||
// 정리
|
||||
for (int i = 0; i < drops.Count; i++) Kill(drops[i] != null ? drops[i].gameObject : null);
|
||||
Kill(d1 != null ? d1.gameObject : null); Kill(din != null ? din.gameObject : null);
|
||||
Kill(dout != null ? dout.gameObject : null); Kill(dair != null ? dair.gameObject : null);
|
||||
Kill(dC8 != null ? dC8.gameObject : null);
|
||||
Kill(mobB != null ? mobB.gameObject : null); Kill(mobE != null ? mobE.gameObject : null);
|
||||
Kill(mobN != null ? mobN.gameObject : null);
|
||||
var runner = WL.Combat.Loot.LootBurst.RunnerOrNull;
|
||||
if (runner != null) Kill(runner.gameObject);
|
||||
WL.Combat.Core.EffectBudget.ResetAll();
|
||||
L("");
|
||||
L("정리 완료 — Heavy Count " + WL.Combat.Core.EffectBudget.Count(WL.Combat.Core.EffectBudgetKind.Heavy) +
|
||||
" · ClusterBurst Count " + WL.Combat.Core.EffectBudget.Count(WL.Combat.Core.EffectBudgetKind.ClusterBurst));
|
||||
return Flush("EDIT.txt");
|
||||
}
|
||||
|
||||
static string Hist(int[] h)
|
||||
{
|
||||
var b = new System.Text.StringBuilder();
|
||||
for (int i = 0; i < h.Length; i++) if (h[i] > 0) b.Append(i).Append(":").Append(h[i]).Append(" ");
|
||||
return b.ToString().Trim();
|
||||
}
|
||||
|
||||
/// <summary>테이블에서 해당 등급의 아이템 ID 를 하나 찾는다(없으면 새 백의 첫 행 · 그것도 없으면 골드).</summary>
|
||||
static int FindItemIdOfGrade(int grade)
|
||||
{
|
||||
var rows = ReadRows(kBagJson);
|
||||
for (int i = 0; i < rows.Count; i++)
|
||||
if (rows[i]["n_DropIndex"] == kNewBagId.ToString() && rows[i]["n_ItemGrade"] == grade.ToString())
|
||||
return int.Parse(rows[i]["n_ItemId"]);
|
||||
return 2;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════ Play
|
||||
/// <summary>Play 중 카운터 스냅샷(실전투 QA 용).</summary>
|
||||
public static string Status()
|
||||
{
|
||||
s = new System.Text.StringBuilder();
|
||||
L("=== WL-813f Status " + System.DateTime.Now.ToString("HH:mm:ss") + " ===");
|
||||
L("Killed 처리 " + WL.Combat.Loot.LootBurst.KilledHandled + " · 드랍 " + WL.Combat.Loot.LootBurst.DropsSeen +
|
||||
" · 산포 " + WL.Combat.Loot.LootBurst.Scattered + " · 획득 " + WL.Combat.Loot.LootBurst.PickedCount);
|
||||
L("빔 " + WL.Combat.Loot.LootBurst.BeamSpawned + " · 예산 스킵 " + WL.Combat.Loot.LootBurst.BeamSkippedBudget +
|
||||
" · 프리팹 없음 " + WL.Combat.Loot.LootBurst.BeamSkippedNoPrefab + " · 과밀 스킵 " + WL.Combat.Loot.LootBurst.BeamSkippedCrowded +
|
||||
" · 사운드 " + WL.Combat.Loot.LootBurst.SoundPlayed);
|
||||
L("자석 " + WL.Combat.Loot.LootBurst.MagnetEngaged + " · 폭발 계획 " + WL.Combat.Loot.LootBurst.BurstPlanned +
|
||||
" · 실드랍 " + WL.Combat.Loot.LootBurst.BurstDropped + " · 상한 스킵 " + WL.Combat.Loot.LootBurst.BurstSkippedCap);
|
||||
L("살아 있는 드랍 " + WL.Combat.Loot.LootBurst.ActiveDropCount() + " · Heavy " +
|
||||
WL.Combat.Core.EffectBudget.Count(WL.Combat.Core.EffectBudgetKind.Heavy) + "/" +
|
||||
WL.Combat.Core.EffectBudget.Limit(WL.Combat.Core.EffectBudgetKind.Heavy) +
|
||||
" · 마지막 등급 " + WL.Combat.Loot.LootBurst.LastGrade +
|
||||
" · 마지막 산포 " + WL.Combat.Loot.LootBurst.LastScatterDistance.ToString("F2") + " m");
|
||||
return Flush("STATUS.txt");
|
||||
}
|
||||
|
||||
/// <summary>Play 중 N초 감시 — 드랍/획득 수와 줍기 완료 시간을 잰다.</summary>
|
||||
public static string Watch(double seconds)
|
||||
{
|
||||
s = new System.Text.StringBuilder();
|
||||
if (!UnityEngine.Application.isPlaying) { L("Play 중에만 쓸 수 있다."); return Flush("WATCH.txt"); }
|
||||
L("=== WL-813f Watch " + seconds.ToString("F0") + "s ===");
|
||||
int d0 = WL.Combat.Loot.LootBurst.DropsSeen, p0 = WL.Combat.Loot.LootBurst.PickedCount,
|
||||
k0 = WL.Combat.Loot.LootBurst.KilledHandled, b0 = WL.Combat.Loot.LootBurst.BeamSpawned,
|
||||
m0 = WL.Combat.Loot.LootBurst.MagnetEngaged, r0 = WL.Combat.Loot.LootBurst.BurstDropped;
|
||||
double t0 = UnityEngine.Time.realtimeSinceStartupAsDouble;
|
||||
while (UnityEngine.Time.realtimeSinceStartupAsDouble - t0 < seconds)
|
||||
System.Threading.Thread.Sleep(50);
|
||||
L("처치 " + (WL.Combat.Loot.LootBurst.KilledHandled - k0) + " · 드랍 " + (WL.Combat.Loot.LootBurst.DropsSeen - d0) +
|
||||
" · 획득 " + (WL.Combat.Loot.LootBurst.PickedCount - p0) + " · 빔 " + (WL.Combat.Loot.LootBurst.BeamSpawned - b0) +
|
||||
" · 자석 " + (WL.Combat.Loot.LootBurst.MagnetEngaged - m0) + " · 폭발 드랍 " + (WL.Combat.Loot.LootBurst.BurstDropped - r0));
|
||||
return Flush("WATCH.txt");
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -71,6 +71,7 @@ public class DropItem : ProjectileBase
|
|||
break;
|
||||
}
|
||||
|
||||
WL.Combat.Loot.LootBurst.OnDropSpawn(this, m_itemid, m_itemamount, ref startpos, ref targetpos); // WL-813f 산포 훅(꺼지면 원본 그대로)
|
||||
m_Collider.enabled = false;
|
||||
StartCoroutine(Co_Move_Split(() =>
|
||||
{
|
||||
|
|
@ -125,6 +126,7 @@ public class DropItem : ProjectileBase
|
|||
DropItemInfo.Ins.m_StageDropData.Add_Item(_itemid, m_itemamount);
|
||||
}
|
||||
|
||||
WL.Combat.Loot.LootBurst.OnDropPicked(this, m_itemid, m_itemamount, touchActor); // WL-813f 획득 통지 훅(813g 토스트 소스)
|
||||
InGameInfo.Ins.Show_Effect("Effect_GetCoin", transform.position);
|
||||
gameObject.SetActive(false);
|
||||
//NewGameUI.Ins.m_GuideQuestUI.Add_GuideQuest(ePurpose.GetDropItem);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 34a36a6babb7402886d1580a15467e85
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,493 @@
|
|||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 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; } }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 65c19220a8f549008778cc75a6bd5b5e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,179 @@
|
|||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: e768811873de4082964356cf58fb48a2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,181 @@
|
|||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// WLLootSettings.cs — 전리품 연출 값의 단일 출처(SOT · C45)
|
||||
//
|
||||
// PD 지시 #813 · 발주서 WL-813f §1 (2026-09-09) · 기준서 v1 §B 요소 4
|
||||
//
|
||||
// 에셋 경로(고정): Assets/WL/Combat/Settings/Resources/WL/WLLootSettings.asset
|
||||
// → Resources.Load<WLLootSettings>("WL/WLLootSettings") 1회 로드 후 캐시(WLGrowthSettings 관례 그대로).
|
||||
// 에셋이 없거나 enabled = false 면 Enabled == false → DropItem.cs 의 훅 2줄이 즉시 반환하고
|
||||
// 산포·빔·사운드·자동 줍기·폭발 드랍이 전부 꺼진다 = 원본 드랍 100%(C8 롤백 1순위 스위치).
|
||||
// 아래 필드 기본값은 "에셋 부재 시의 폴백" 이 아니라 에셋 생성 시 초기값이다.
|
||||
//
|
||||
// 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것(DropItem/MobActor 가 Assembly-CSharp 에 있다).
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
namespace WL.Combat.Loot
|
||||
{
|
||||
[CreateAssetMenu(fileName = "WLLootSettings", menuName = "WL/Loot Settings", order = 322)]
|
||||
public sealed class WLLootSettings : ScriptableObject
|
||||
{
|
||||
public const string ResourcesPath = "WL/WLLootSettings";
|
||||
|
||||
/// <summary>등급 배열 길이 = eItemGrade 8단계 + 여유 1(기준서 §B 요소 4 등급 색 9행).</summary>
|
||||
public const int GradeSlots = 9;
|
||||
|
||||
static WLLootSettings s_cached;
|
||||
static bool s_lookupDone;
|
||||
|
||||
/// <summary>설정 에셋(1회 로드 후 캐시). 없으면 null.</summary>
|
||||
public static WLLootSettings Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!s_lookupDone)
|
||||
{
|
||||
s_cached = Resources.Load<WLLootSettings>(ResourcesPath);
|
||||
s_lookupDone = true;
|
||||
}
|
||||
return s_cached;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>진단·A/B 전용 런타임 스위치(WLCombatCoreSettings.RuntimeDisabled 선례). Play 종료 시 도메인 리로드로 자동 false.</summary>
|
||||
public static bool RuntimeDisabled;
|
||||
|
||||
/// <summary>전리품 연출이 살아 있는가 = 에셋 존재 · enabled · 런타임 스위치 off.</summary>
|
||||
public static bool Enabled
|
||||
{
|
||||
get { var i = Instance; return i != null && i.enabled && !RuntimeDisabled; }
|
||||
}
|
||||
|
||||
/// <summary>테스트용: 캐시를 비워 다음 접근 때 다시 Resources.Load 한다(에셋 제거/복구 실험).</summary>
|
||||
public static void ClearCache() { s_cached = null; s_lookupDone = false; }
|
||||
|
||||
// ─────────────────────────────────────────── 전체 스위치
|
||||
[Header("전체 스위치")]
|
||||
[Tooltip("끄면 산포·빔·사운드·자동 줍기·폭발 드랍이 전부 비활성 — 원본 DropItem 동작 100%. 롤백 1순위.")]
|
||||
public bool enabled = true;
|
||||
|
||||
[Tooltip("드랍 처리 결과를 Console 에 남긴다(검증용 · 기본 off · GC 가 발생하므로 서비스 빌드에서 켜지 말 것).")]
|
||||
public bool verboseLog = false;
|
||||
|
||||
// ─────────────────────────────────────────── ① 드랍 산포
|
||||
[Header("① 드랍 산포 (기준서 §B: 산포 반경 0.6~1.2 m)")]
|
||||
[Tooltip("끄면 원본이 계산한 위치를 그대로 쓴다(골드는 원본대로 2~3 m).")]
|
||||
public bool scatterEnabled = true;
|
||||
|
||||
[Tooltip("드랍 지점(몹 위치)에서 떨어지는 최소 거리(m).")]
|
||||
public float scatterRadiusMin = 0.6f;
|
||||
|
||||
[Tooltip("드랍 지점에서 떨어지는 최대 거리(m).")]
|
||||
public float scatterRadiusMax = 1.2f;
|
||||
|
||||
[Tooltip("NavMesh 스냅 반경(m). 표집점이 NavMesh 밖이면 이 반경 안에서 가장 가까운 점으로 당긴다. 0 이면 스냅하지 않는다.")]
|
||||
public float scatterNavSnapRadius = 0.8f;
|
||||
|
||||
[Tooltip("포물선 시작 높이(m · 등급 1 기준). Co_Move_Split 의 시작점을 올려 튀는 높이를 만든다.")]
|
||||
public float bounceHeightBase = 0.35f;
|
||||
|
||||
[Tooltip("등급 1 을 넘는 등급 1단계마다 더해지는 높이(m).")]
|
||||
public float bounceHeightPerGrade = 0.08f;
|
||||
|
||||
[Tooltip("튀는 높이 상한(m).")]
|
||||
public float bounceHeightMax = 1.0f;
|
||||
|
||||
[Tooltip("한 번의 처치에서 여러 개가 떨어질 때 서로 다른 방향으로 부채꼴로 흩는다(같은 자리 겹침 방지).")]
|
||||
public bool fanOutSameKill = true;
|
||||
|
||||
// ─────────────────────────────────────────── ② 등급 빔 + 사운드
|
||||
[Header("② 등급 빔 + 사운드 (기준서 §B: 등급 4 이상만)")]
|
||||
[Tooltip("빔·사운드를 낼 최소 아이템 등급(table_itemlist.n_ItemGrade). 기준서 = 4.")]
|
||||
public int beamMinGrade = 4;
|
||||
|
||||
[Tooltip("등급별 빔 프리팹(인덱스 0 = 등급 1 … 인덱스 8 = 등급 9). 비워 두면 그 등급은 빔 없음. 보유 FX_LootDrop_* 만 쓴다(새 아트 0).")]
|
||||
public GameObject[] beamByGrade = new GameObject[GradeSlots];
|
||||
|
||||
[Tooltip("빔을 드랍 오브젝트의 자식으로 붙인다(자석으로 끌려갈 때 같이 따라온다). 끄면 드랍 지점에 고정.")]
|
||||
public bool beamParentToDrop = true;
|
||||
|
||||
[Tooltip("빔 스폰 오프셋(m · 드랍 오브젝트 기준).")]
|
||||
public Vector3 beamOffset = new Vector3(0f, 0f, 0f);
|
||||
|
||||
[Tooltip("빔 프리팹 스케일 배율(1 = 원본).")]
|
||||
public float beamScale = 0.6f;
|
||||
|
||||
[Tooltip("빔 최대 수명(초). 드랍 소멸(원본 20 s)보다 길게 잡아야 줍기 전에 꺼지지 않는다. 드랍이 사라지면 이 시간과 무관하게 즉시 파괴한다.")]
|
||||
public float beamMaxLifetimeSeconds = 21f;
|
||||
|
||||
[Tooltip("EffectBudget.Heavy 슬롯을 잡고 빔을 스폰한다(상한 초과 = 빔만 스킵 · 드랍 자체는 그대로). 코어가 꺼져 있으면 제한 없음.")]
|
||||
public bool beamUseEffectBudget = true;
|
||||
|
||||
[Tooltip("등급 빔과 함께 낼 사운드(원본 eSound · 보유 클립만). 기준서 = s015_GoldPowerUp.")]
|
||||
public eSound beamSound = eSound.s015_GoldPowerUp;
|
||||
|
||||
[Tooltip("빔 사운드 최대 볼륨(거리 감쇠 전). 0 이면 사운드 없음.")]
|
||||
[Range(0f, 1f)] public float beamSoundVolume = 0.5f;
|
||||
|
||||
[Tooltip("빔 사운드를 다시 낼 최소 간격(초 · unscaled). 폭발 드랍이 겹칠 때 소리가 뭉치는 것을 막는다.")]
|
||||
public float beamSoundMinIntervalSeconds = 0.12f;
|
||||
|
||||
// ─────────────────────────────────────────── ③ 자동 줍기 반경
|
||||
[Header("③ 자동 줍기 반경 (기준서 §B: 1.5 m · 원본 자석 상태기 재사용)")]
|
||||
[Tooltip("끄면 원본 그대로 접촉해야만 줍는다.")]
|
||||
public bool autoPickupEnabled = true;
|
||||
|
||||
[Tooltip("PC 중심에서 이 거리 안에 들어온 드랍을 자석 상태로 바꾼다(m).")]
|
||||
public float autoPickupRadius = 1.5f;
|
||||
|
||||
[Tooltip("자석 판정 주기(초). 0 이면 매 프레임. 드랍이 많을 때 비용을 줄인다.")]
|
||||
public float autoPickupScanIntervalSeconds = 0.1f;
|
||||
|
||||
[Tooltip("자석 1단계에서 위로 띄우는 높이(m · 원본 주석 코드 값 3).")]
|
||||
public float magnetRiseHeight = 3f;
|
||||
|
||||
[Tooltip("자석 1단계 속도(원본 주석 코드 값 5). 2단계는 원본이 15 로 올린다.")]
|
||||
public float magnetRiseSpeed = 5f;
|
||||
|
||||
// ─────────────────────────────────────────── ④ 보스/엘리트 폭발 드랍
|
||||
[Header("④ 폭발 드랍 (기준서 §B: 엘리트 1~2건 · 보스 3~5건)")]
|
||||
[Tooltip("끄면 원본대로 처치당 1건만 추첨한다.")]
|
||||
public bool burstEnabled = true;
|
||||
|
||||
[Tooltip("엘리트 처치 시 추가 드랍 최소 건수.")]
|
||||
public int eliteBurstMin = 1;
|
||||
|
||||
[Tooltip("엘리트 처치 시 추가 드랍 최대 건수(포함).")]
|
||||
public int eliteBurstMax = 2;
|
||||
|
||||
[Tooltip("보스 처치 시 추가 드랍 최소 건수.")]
|
||||
public int bossBurstMin = 3;
|
||||
|
||||
[Tooltip("보스 처치 시 추가 드랍 최대 건수(포함).")]
|
||||
public int bossBurstMax = 5;
|
||||
|
||||
[Tooltip("일반 잡몹 추가 드랍 건수(기준서 = 0 · 잡몹은 골드 100% + 백 15~25% 로 충분).")]
|
||||
public int normalBurstCount = 0;
|
||||
|
||||
[Tooltip("폭발 드랍 1건 사이의 간격(초). 0 이면 같은 프레임에 한꺼번에.")]
|
||||
public float burstIntervalSeconds = 0.07f;
|
||||
|
||||
[Tooltip("추가 드랍 1건마다 몹의 RandomBag 을 다시 추첨한다. 추첨이 빈손이면 아래 골드로 대체.")]
|
||||
public bool burstRollsBag = true;
|
||||
|
||||
[Tooltip("백 추첨이 빈손일 때 대신 떨어뜨리는 골드 = 몹 n_DropGold × 이 비율(최소 1). 0 이면 빈손일 때 아무것도 떨어지지 않는다. " +
|
||||
"⚠ 이 값이 0 보다 크면 보스/엘리트 골드 수입이 건수만큼 늘어난다(경제 영향 · PD 튜닝 지점).")]
|
||||
[Range(0f, 1f)] public float burstGoldShare = 0.25f;
|
||||
|
||||
[Tooltip("EffectBudget.ClusterBurst 슬롯을 잡고 폭발 드랍을 시작한다. 811e 군집 폭발과 슬롯(상한 1)을 공유하므로 기본 off.")]
|
||||
public bool burstUseClusterBudget = false;
|
||||
|
||||
// ─────────────────────────────────────────── ⑤ 동시 드랍 오브젝트 상한
|
||||
[Header("⑤ 동시 드랍 오브젝트 상한 (모바일 과밀 방지)")]
|
||||
[Tooltip("살아 있는 드랍 오브젝트가 이 수를 넘으면 폭발 드랍의 추가 건을 더 내지 않는다(원본 기본 드랍은 절대 막지 않는다). " +
|
||||
"0 이면 제한 없음. 참고: 원본 풀 자체가 코인 30 · 아이템 2 로 이미 상한이다(DropItemInfo.Start).")]
|
||||
public int maxConcurrentDrops = 24;
|
||||
|
||||
[Tooltip("살아 있는 드랍이 이 수를 넘으면 등급 빔도 내지 않는다(EffectBudget 과 별개의 2차 방어). 0 이면 제한 없음.")]
|
||||
public int beamSuppressAboveDrops = 12;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: d6c24c4214f94fdb921a4b87c7328797
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: d6c24c4214f94fdb921a4b87c7328797, type: 3}
|
||||
m_Name: WLLootSettings
|
||||
m_EditorClassIdentifier: Assembly-CSharp::WL.Combat.Loot.WLLootSettings
|
||||
enabled: 1
|
||||
verboseLog: 0
|
||||
scatterEnabled: 1
|
||||
scatterRadiusMin: 0.6
|
||||
scatterRadiusMax: 1.2
|
||||
scatterNavSnapRadius: 0.8
|
||||
bounceHeightBase: 0.35
|
||||
bounceHeightPerGrade: 0.08
|
||||
bounceHeightMax: 1
|
||||
fanOutSameKill: 1
|
||||
beamMinGrade: 4
|
||||
beamByGrade:
|
||||
- {fileID: 0}
|
||||
- {fileID: 0}
|
||||
- {fileID: 0}
|
||||
- {fileID: 6592631820678586324, guid: c59a94960337e854d88bea9ce1e865db, type: 3}
|
||||
- {fileID: 8057988818156997068, guid: 2d21eea2e44817f4193c95c7f16d60b6, type: 3}
|
||||
- {fileID: 6728191619410025096, guid: 9fbffa2c33181e949af89f9b629a1d92, type: 3}
|
||||
- {fileID: 8057988818156997068, guid: 2d21eea2e44817f4193c95c7f16d60b6, type: 3}
|
||||
- {fileID: 6728191619410025096, guid: 9fbffa2c33181e949af89f9b629a1d92, type: 3}
|
||||
- {fileID: 8057988818156997068, guid: 2d21eea2e44817f4193c95c7f16d60b6, type: 3}
|
||||
beamParentToDrop: 1
|
||||
beamOffset: {x: 0, y: 0, z: 0}
|
||||
beamScale: 0.6
|
||||
beamMaxLifetimeSeconds: 21
|
||||
beamUseEffectBudget: 1
|
||||
beamSound: 15
|
||||
beamSoundVolume: 0.5
|
||||
beamSoundMinIntervalSeconds: 0.12
|
||||
autoPickupEnabled: 1
|
||||
autoPickupRadius: 1.5
|
||||
autoPickupScanIntervalSeconds: 0.1
|
||||
magnetRiseHeight: 3
|
||||
magnetRiseSpeed: 5
|
||||
burstEnabled: 1
|
||||
eliteBurstMin: 1
|
||||
eliteBurstMax: 2
|
||||
bossBurstMin: 3
|
||||
bossBurstMax: 5
|
||||
normalBurstCount: 0
|
||||
burstIntervalSeconds: 0.07
|
||||
burstRollsBag: 1
|
||||
burstGoldShare: 0.25
|
||||
burstUseClusterBudget: 0
|
||||
maxConcurrentDrops: 24
|
||||
beamSuppressAboveDrops: 12
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 60284f2302b84220b1e294cca077d312
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Loading…
Reference in New Issue