434 lines
24 KiB
C#
434 lines
24 KiB
C#
// PD 지시 #813 · 발주서 WL-813d §2 ⓒ — 보스전 게이트 · 아레나 에디트 모드 프로브
|
|
//
|
|
// 실행 (CLAUDE.md §1: using 금지 · 네임스페이스 풀어 쓰기 · 첫 Roslyn 컴파일은 --timeout 60)
|
|
// unity command run_script --file AgentScripts/WL813d_Probe.cs --entry WL813d_Probe.Run --timeout 300
|
|
//
|
|
// 무엇을 확인하나 (발주서 §2 ⓒ ⓓ)
|
|
// ① 설정 에셋 로드 · Active · 값 전수 덤프
|
|
// ② 게이트 A — 가짜 Spawned/Killed 로 존 카운트 → 게이트 존(813004·813005) 전멸 → resume() 호출(= 보스 활성)
|
|
// ③ 게이트 B — 누적 처치 ≥ requiredKills 경로(리젠형 존에서 전멸이 안 잡힐 때)
|
|
// ④ 추적 반경 데이터화 — MonsterList 10006 행(실 JSON 값)에 SO 값이 실제로 쓰이는지(원 코드 경로)
|
|
// ⑤ 리쉬 판정표 — 보스 이탈 · PC 이탈(유예 포함) 가짜 좌표
|
|
// ⑥ 보스 처치 → 정리 — RegenTime 코루틴 차단 · 반경 원복
|
|
// ⑦ C8 — RuntimeDisabled / 코어 off 면 HoldSpawn 이 false(= 813e 상시 배치 100%)
|
|
//
|
|
// Play 하지 않는다. 프리팹·씬·에셋을 저장하지 않는다(만든 GameObject 는 DestroyImmediate 로 되돌린다).
|
|
// 수치는 전부 .asset / MonsterList.json / MonsterAppear.json 에서 읽는다(C45).
|
|
|
|
public static class WL813d_Probe
|
|
{
|
|
const string kLog = "AgentScripts/WL813d_PROBE.txt";
|
|
const string kMonsterList = "Assets/ResWork/Table/Export/MonsterList.json";
|
|
const string kMonsterAppear = "Assets/ResWork/Table/Export/MonsterAppear.json";
|
|
|
|
static System.Text.StringBuilder s;
|
|
static int s_resumeCount;
|
|
static UnityEngine.GameObject s_fbdGo, s_tableGo;
|
|
static FieldBossData s_fbd;
|
|
|
|
public static string Run()
|
|
{
|
|
s = new System.Text.StringBuilder();
|
|
s_resumeCount = 0;
|
|
|
|
L("===== WL-813d 보스전 게이트 · 아레나 프로브 =====");
|
|
L("playMode=" + UnityEngine.Application.isPlaying + " time=" + System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
|
|
|
|
WL.Combat.Core.WLCombatCoreSettings.RuntimeDisabled = false;
|
|
WL.Combat.Boss.WLBossArenaSettings.RuntimeDisabled = false;
|
|
WL.Combat.Boss.WLBossArenaSettings.ClearCache();
|
|
WL.Combat.Boss.BossArena.ResetAll();
|
|
|
|
try
|
|
{
|
|
Setup();
|
|
Section1_Settings();
|
|
Section2_GateZoneCleared();
|
|
Section3_GateKillCount();
|
|
Section5_Leash();
|
|
Section6_Cleanup();
|
|
Section7_Rollback();
|
|
}
|
|
catch (System.Exception ex) { L("!! 예외: " + ex); }
|
|
finally { Teardown(); }
|
|
|
|
L("");
|
|
L("===== 끝 =====");
|
|
return Flush();
|
|
}
|
|
|
|
// ───────────────────────────────────────── 준비
|
|
static void Setup()
|
|
{
|
|
// 가짜 FieldBossData — 실제 훅과 같은 계약으로 HoldSpawn 을 부른다(원본 Start 는 부르지 않는다).
|
|
var cfg = WL.Combat.Boss.WLBossArenaSettings.Instance;
|
|
s_fbdGo = new UnityEngine.GameObject("WL813d_FakeFieldBoss");
|
|
s_fbdGo.transform.position = cfg != null ? cfg.arenaCenter : UnityEngine.Vector3.zero;
|
|
s_fbd = s_fbdGo.AddComponent<FieldBossData>();
|
|
s_fbd.isChapterBoss = false;
|
|
s_fbd.n_MonsterID = cfg != null ? cfg.bossMonsterId : 10006;
|
|
s_fbd.n_SpawnerId = cfg != null ? cfg.bossSpawnerId : 813010;
|
|
s_fbd.RegenTime = 60f;
|
|
|
|
// 가짜 MonsterList 싱글턴 — 실 JSON 의 10006 행 값을 그대로 넣어 「원 코드 경로」로 반경 적용을 측정한다.
|
|
// (에디트 모드에는 table_monsterlist 인스턴스가 없다. Ins 는 public static, dic_Data 만 private 이라 리플렉션 1회.)
|
|
float chaseFromJson = ReadFloatField(kMonsterList, "n_MonsterID", s_fbd.n_MonsterID.ToString(), "f_BaseChaseRange", -1f);
|
|
var row = new MonsterTableData();
|
|
row.n_MonsterID = s_fbd.n_MonsterID;
|
|
row.f_BaseChaseRange = chaseFromJson;
|
|
var dic = new System.Collections.Generic.Dictionary<int, MonsterTableData>();
|
|
dic.Add(row.n_MonsterID, row);
|
|
|
|
s_tableGo = new UnityEngine.GameObject("WL813d_FakeMonsterList");
|
|
var tbl = s_tableGo.AddComponent<table_monsterlist>();
|
|
var fi = typeof(table_monsterlist).GetField("dic_Data",
|
|
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
|
if (fi != null) fi.SetValue(tbl, dic);
|
|
table_monsterlist.Ins = tbl;
|
|
|
|
L("");
|
|
L("── ⓪ 준비");
|
|
L(" 가짜 FieldBossData = spawnerId " + s_fbd.n_SpawnerId + " · monsterId " + s_fbd.n_MonsterID +
|
|
" · pos " + s_fbdGo.transform.position.ToString("F1") + " · RegenTime " + s_fbd.RegenTime);
|
|
L(" MonsterList 10006 행 f_BaseChaseRange (실 JSON) = " + chaseFromJson.ToString("F1") +
|
|
" dic_Data 주입 = " + (fi != null));
|
|
}
|
|
|
|
static void Teardown()
|
|
{
|
|
WL.Combat.Boss.BossArena.Unsubscribe();
|
|
WL.Combat.Boss.BossArena.ResetAll();
|
|
WL.Combat.Boss.WLBossArenaSettings.RuntimeDisabled = false;
|
|
WL.Combat.Core.WLCombatCoreSettings.RuntimeDisabled = false;
|
|
table_monsterlist.Ins = null;
|
|
if (s_fbdGo != null) UnityEngine.Object.DestroyImmediate(s_fbdGo);
|
|
if (s_tableGo != null) UnityEngine.Object.DestroyImmediate(s_tableGo);
|
|
}
|
|
|
|
// ───────────────────────────────────────── ① 설정
|
|
static void Section1_Settings()
|
|
{
|
|
var cfg = WL.Combat.Boss.WLBossArenaSettings.Instance;
|
|
L("");
|
|
L("── ① 설정 에셋");
|
|
L(" Instance = " + (cfg != null ? "OK" : "null (Resources/WL/WLBossArenaSettings 없음)"));
|
|
L(" 코어 Enabled = " + WL.Combat.Core.WLCombatCoreSettings.Enabled);
|
|
L(" 게이트 Active = " + WL.Combat.Boss.WLBossArenaSettings.Active);
|
|
if (cfg == null) return;
|
|
L(" enabled/verboseLog = " + cfg.enabled + " / " + cfg.verboseLog);
|
|
L(" bossSpawnerId/Monster = " + cfg.bossSpawnerId + " / " + cfg.bossMonsterId);
|
|
L(" gateEnabled/mode = " + cfg.gateEnabled + " / " + cfg.gateMode);
|
|
L(" zoneSpawnerIds = " + Join(cfg.zoneSpawnerIds));
|
|
L(" gateZoneSpawnerIds = " + Join(cfg.gateZoneSpawnerIds));
|
|
L(" requiredKills = " + cfg.requiredKills + " requireZoneSpawnedOnce=" + cfg.requireZoneSpawnedOnce);
|
|
L(" gateOpenDelaySeconds = " + cfg.gateOpenDelaySeconds);
|
|
L(" arena center/radius = " + (cfg.useFieldBossPositionAsCenter ? "FieldBossData 좌표" : cfg.arenaCenter.ToString("F1")) +
|
|
" / " + cfg.arenaRadius);
|
|
L(" bossChaseRange = " + cfg.bossChaseRange + " restoreOnCleanup=" + cfg.restoreChaseRangeOnCleanup);
|
|
L(" leash margin/PC margin= " + cfg.leashMargin + " / " + cfg.playerLeaveMargin +
|
|
" grace=" + cfg.playerLeaveGraceSeconds + "s interval=" + cfg.leashCheckIntervalSeconds + "s");
|
|
L(" resetHeals/rate = " + cfg.resetHealsBoss + " / " + cfg.resetHealRate);
|
|
L(" cleanup delay/regen = " + cfg.cleanupDelaySeconds + "s / suppressRegen=" + cfg.suppressRegenAfterBossKill);
|
|
L(" deactivateZones/rearm = " + cfg.deactivateZonesAfterBossKill + " / " + cfg.rearmGateAfterBossKill);
|
|
}
|
|
|
|
// ───────────────────────────────────────── ② 게이트 A (존 전멸)
|
|
static void Section2_GateZoneCleared()
|
|
{
|
|
var cfg = WL.Combat.Boss.WLBossArenaSettings.Instance;
|
|
L("");
|
|
L("── ② 게이트 A — 게이트 존 전멸 → 보스 등장");
|
|
if (cfg == null) { L(" 설정 없음 — 건너뜀"); return; }
|
|
|
|
WL.Combat.Boss.BossArena.ResetAll();
|
|
s_resumeCount = 0;
|
|
|
|
int killedBefore = WL.Combat.Core.CombatEvents.Killed.Count;
|
|
int spawnedBefore = WL.Combat.Core.CombatEvents.Spawned.Count;
|
|
|
|
bool held = WL.Combat.Boss.BossArena.HoldSpawn(s_fbd, FakeResume);
|
|
L(" HoldSpawn() = " + held + " (true = 원본 Start 가 return · 보스 미등장)");
|
|
L(" GateArmed/Subscribed = " + WL.Combat.Boss.BossArena.GateArmed + " / " + WL.Combat.Boss.BossArena.Subscribed);
|
|
L(" CombatEvents 구독 수 = Killed " + killedBefore + "→" + WL.Combat.Core.CombatEvents.Killed.Count +
|
|
" · Spawned " + spawnedBefore + "→" + WL.Combat.Core.CombatEvents.Spawned.Count);
|
|
L(" resume 호출 수 = " + s_resumeCount + " (게이트가 닫혀 있으므로 0 이어야 한다)");
|
|
L(" 반경 적용 여부 = " + WL.Combat.Boss.BossArena.ChaseRangeApplied + " (등장 직전에만 적용)");
|
|
|
|
// 존 스폰 — 마릿수는 MonsterAppear.json 의 n_MaxMonsterCount 실값
|
|
L("");
|
|
L(" [스폰] 존별 n_MaxMonsterCount 만큼 가짜 Spawned");
|
|
for (int i = 0; i < cfg.zoneSpawnerIds.Length; i++)
|
|
{
|
|
int id = cfg.zoneSpawnerIds[i];
|
|
int n = (int)ReadFloatField(kMonsterAppear, "n_SpawnerID", id.ToString(), "n_MaxMonsterCount", 0f);
|
|
for (int k = 0; k < n; k++) WL.Combat.Boss.BossArena.NoteSpawn(id, false);
|
|
L(" 존 " + id + " : 스폰 " + n + " → alive " + WL.Combat.Boss.BossArena.AliveOf(id));
|
|
}
|
|
L(" GateOpen = " + WL.Combat.Boss.BossArena.GateOpen + " (아직 닫혀 있어야 한다)");
|
|
|
|
// 게이트 존만 전멸시킨다(비게이트 존은 살려 둔다 → ZoneCleared 경로만 검증)
|
|
L("");
|
|
L(" [처치] 게이트 존만 전멸 — 비게이트 존은 살려 둔다(KillCount 경로와 분리)");
|
|
for (int i = 0; i < cfg.gateZoneSpawnerIds.Length; i++)
|
|
{
|
|
int id = cfg.gateZoneSpawnerIds[i];
|
|
int alive = WL.Combat.Boss.BossArena.AliveOf(id);
|
|
for (int k = 0; k < alive; k++)
|
|
{
|
|
WL.Combat.Boss.BossArena.NoteKill(id, false);
|
|
if (k == alive - 1)
|
|
L(" 존 " + id + " 마지막 처치 → alive " + WL.Combat.Boss.BossArena.AliveOf(id) +
|
|
" · 누적처치 " + WL.Combat.Boss.BossArena.TotalZoneKills + "/" + cfg.requiredKills +
|
|
" · GateOpen " + WL.Combat.Boss.BossArena.GateOpen + " · resume " + s_resumeCount);
|
|
else if (k == 0)
|
|
L(" 존 " + id + " 첫 처치 → alive " + WL.Combat.Boss.BossArena.AliveOf(id) +
|
|
" · GateOpen " + WL.Combat.Boss.BossArena.GateOpen);
|
|
}
|
|
}
|
|
|
|
L("");
|
|
L(" 결과 GateOpen = " + WL.Combat.Boss.BossArena.GateOpen);
|
|
L(" 결과 resume 호출 수 = " + s_resumeCount + " (1 = 원본 FieldBossData.Start 재진입 = 보스 활성)");
|
|
L(" ReleaseCount = " + WL.Combat.Boss.BossArena.ReleaseCount);
|
|
L(" 누적 처치 = " + WL.Combat.Boss.BossArena.TotalZoneKills + " / requiredKills " + cfg.requiredKills +
|
|
" → KillCount 조건은 아직 미충족(= ZoneCleared 단독 개방)");
|
|
L(" LastLog = " + WL.Combat.Boss.BossArena.LastLog);
|
|
|
|
// ④ 추적 반경 — Release 안에서 원 코드 경로로 적용된다
|
|
L("");
|
|
L("── ④ 추적 반경 데이터화 (등장 직전 · MonsterList 행)");
|
|
L(" ChaseRangeApplied = " + WL.Combat.Boss.BossArena.ChaseRangeApplied);
|
|
L(" before → after = " + WL.Combat.Boss.BossArena.ChaseRangeBefore.ToString("F1") + " → " +
|
|
WL.Combat.Boss.BossArena.ChaseRangeAfter.ToString("F1") + " (SO bossChaseRange=" + cfg.bossChaseRange + ")");
|
|
var live = table_monsterlist.Ins != null ? table_monsterlist.Ins.Get_Data_orNull(cfg.bossMonsterId) : null;
|
|
L(" 테이블 행 실측 = " + (live != null ? live.f_BaseChaseRange.ToString("F1") : "행 없음") +
|
|
" (MobActor.cs:92 Check_Battle · :356 Check_Patrol 이 이 값을 읽는다)");
|
|
|
|
// 두 번째 진입(resume → Start 재진입)에서 통과하는지
|
|
bool second = WL.Combat.Boss.BossArena.HoldSpawn(s_fbd, FakeResume);
|
|
L(" 재진입 HoldSpawn() = " + second + " (false = 원본 스폰 코드 진행 · 이중 스폰 없음)");
|
|
L(" 재진입 후 resume 호출 = " + s_resumeCount + " (증가하지 않아야 한다)");
|
|
}
|
|
|
|
// ───────────────────────────────────────── ③ 게이트 B (누적 처치)
|
|
static void Section3_GateKillCount()
|
|
{
|
|
var cfg = WL.Combat.Boss.WLBossArenaSettings.Instance;
|
|
L("");
|
|
L("── ③ 게이트 B — 누적 처치 ≥ requiredKills (리젠형 존에서 전멸이 안 잡힐 때)");
|
|
if (cfg == null) { L(" 설정 없음 — 건너뜀"); return; }
|
|
|
|
WL.Combat.Boss.BossArena.ResetAll();
|
|
s_resumeCount = 0;
|
|
WL.Combat.Boss.BossArena.HoldSpawn(s_fbd, FakeResume);
|
|
|
|
// 모든 존을 스폰시켜 두되, 게이트 존은 계속 리젠되는 상황을 흉내낸다(처치할 때마다 다시 스폰).
|
|
int zone0 = cfg.zoneSpawnerIds.Length > 0 ? cfg.zoneSpawnerIds[0] : 813001;
|
|
for (int i = 0; i < cfg.zoneSpawnerIds.Length; i++)
|
|
{
|
|
int id = cfg.zoneSpawnerIds[i];
|
|
int n = (int)ReadFloatField(kMonsterAppear, "n_SpawnerID", id.ToString(), "n_MaxMonsterCount", 0f);
|
|
for (int k = 0; k < n; k++) WL.Combat.Boss.BossArena.NoteSpawn(id, false);
|
|
}
|
|
|
|
int guard = 0;
|
|
while (!WL.Combat.Boss.BossArena.GateOpen && guard++ < 500)
|
|
{
|
|
WL.Combat.Boss.BossArena.NoteKill(zone0, false);
|
|
WL.Combat.Boss.BossArena.NoteSpawn(zone0, false); // 리젠 — alive 가 0 이 되지 않는다
|
|
}
|
|
|
|
L(" 리젠 흉내 (존 " + zone0 + " 처치↔리젠 반복 · 어떤 존도 전멸하지 않음)");
|
|
L(" 게이트 존 alive = " + Join(cfg.gateZoneSpawnerIds) + " → " + AliveList(cfg.gateZoneSpawnerIds));
|
|
L(" 누적 처치 / 임계 = " + WL.Combat.Boss.BossArena.TotalZoneKills + " / " + cfg.requiredKills);
|
|
L(" GateOpen / resume = " + WL.Combat.Boss.BossArena.GateOpen + " / " + s_resumeCount);
|
|
L(" 반복 횟수(안전 상한 500) = " + guard);
|
|
L(" LastLog = " + WL.Combat.Boss.BossArena.LastLog);
|
|
}
|
|
|
|
// ───────────────────────────────────────── ⑤ 리쉬
|
|
static void Section5_Leash()
|
|
{
|
|
var cfg = WL.Combat.Boss.WLBossArenaSettings.Instance;
|
|
L("");
|
|
L("── ⑤ 리쉬 판정표 (EvaluateLeash · 순수 함수 · Actor 접근 0)");
|
|
if (cfg == null) { L(" 설정 없음 — 건너뜀"); return; }
|
|
|
|
var c = cfg.arenaCenter;
|
|
float r = cfg.arenaRadius;
|
|
L(" 중심 " + c.ToString("F1") + " · 반경 " + r + " · leashMargin " + cfg.leashMargin +
|
|
" · playerLeaveMargin " + cfg.playerLeaveMargin + " · grace " + cfg.playerLeaveGraceSeconds + "s");
|
|
L(" " + Pad("보스 거리", 12) + Pad("PC 거리", 10) + Pad("경과(s)", 9) + "판정");
|
|
|
|
LeashCase(c, r, 0f, 0f, 0f); // 둘 다 안
|
|
LeashCase(c, r, r - 1f, 0f, 0f); // 보스 경계 안
|
|
LeashCase(c, r, r + cfg.leashMargin + 1f, 0f, 0f); // 보스 이탈
|
|
LeashCase(c, r, 0f, r + cfg.playerLeaveMargin + 1f, 0f); // PC 이탈 · 유예 전
|
|
LeashCase(c, r, 0f, r + cfg.playerLeaveMargin + 1f, cfg.playerLeaveGraceSeconds + 0.1f); // PC 이탈 · 유예 후
|
|
WL.Combat.Boss.BossArena.ResetLeashState();
|
|
LeashCase(c, r, 0f, r + cfg.playerLeaveMargin - 1f, 0f); // PC 여유 안 (이탈 아님)
|
|
|
|
L(" ※ 실제 리셋 실행(Del_Target · On_Regen(reincarnation:true) · Set_Warp)은 살아 있는 MobActor 가 필요해");
|
|
L(" 에디트 모드에서 측정 불가 = 「미확인」. Boss 참조 없음 → LeashTick() 는 무동작:");
|
|
WL.Combat.Boss.BossArena.SetBossForProbe(null);
|
|
int before = WL.Combat.Boss.BossArena.ResetCount;
|
|
WL.Combat.Boss.BossArena.LeashTick();
|
|
L(" LeashTick() ResetCount " + before + " → " + WL.Combat.Boss.BossArena.ResetCount);
|
|
}
|
|
|
|
static void LeashCase(UnityEngine.Vector3 c, float r, float bossDist, float pcDist, float elapsed)
|
|
{
|
|
WL.Combat.Boss.BossArena.ResetLeashState();
|
|
var bossPos = c + new UnityEngine.Vector3(bossDist, 0f, 0f);
|
|
var pcPos = c + new UnityEngine.Vector3(pcDist, 0f, 0f);
|
|
bool hasPc = pcDist > 0f;
|
|
int v = WL.Combat.Boss.BossArena.EvaluateLeash(bossPos, pcPos, hasPc, c, 0f);
|
|
if (v == WL.Combat.Boss.BossArena.kLeashNone && elapsed > 0f)
|
|
v = WL.Combat.Boss.BossArena.EvaluateLeash(bossPos, pcPos, hasPc, c, elapsed); // 유예 경과 후 재판정
|
|
L(" " + Pad(bossDist.ToString("F1") + "m", 12) + Pad(hasPc ? pcDist.ToString("F1") + "m" : "-", 10) +
|
|
Pad(elapsed.ToString("F1"), 9) + Verdict(v));
|
|
}
|
|
|
|
static string Verdict(int v)
|
|
{
|
|
if (v == WL.Combat.Boss.BossArena.kLeashPlayerOut) return "리셋 (PC 이탈)";
|
|
if (v == WL.Combat.Boss.BossArena.kLeashBossOut) return "리셋 (보스 이탈)";
|
|
return "유지";
|
|
}
|
|
|
|
// ───────────────────────────────────────── ⑥ 처치 후 정리
|
|
static void Section6_Cleanup()
|
|
{
|
|
var cfg = WL.Combat.Boss.WLBossArenaSettings.Instance;
|
|
L("");
|
|
L("── ⑥ 보스 처치 → 정리");
|
|
if (cfg == null) { L(" 설정 없음 — 건너뜀"); return; }
|
|
|
|
WL.Combat.Boss.BossArena.ResetAll();
|
|
s_resumeCount = 0;
|
|
WL.Combat.Boss.BossArena.HoldSpawn(s_fbd, FakeResume);
|
|
WL.Combat.Boss.BossArena.ForceOpenGate();
|
|
L(" 강제 개방 후 resume = " + s_resumeCount + " · 반경 " +
|
|
WL.Combat.Boss.BossArena.ChaseRangeBefore.ToString("F1") + " → " +
|
|
WL.Combat.Boss.BossArena.ChaseRangeAfter.ToString("F1"));
|
|
|
|
WL.Combat.Boss.BossArena.NoteSpawn(cfg.bossSpawnerId, true);
|
|
L(" 보스 Spawned(isBoss) → BossSpawned=" + WL.Combat.Boss.BossArena.BossSpawned +
|
|
" (811d 보스 캠 · 813c HP 바 · 813t 배너가 구독하는 그 이벤트)");
|
|
|
|
WL.Combat.Boss.BossArena.NoteKill(cfg.bossSpawnerId, true);
|
|
L(" 보스 Killed(Boss) → BossKilled=" + WL.Combat.Boss.BossArena.BossKilled +
|
|
" · CleanupDone=" + WL.Combat.Boss.BossArena.CleanupDone +
|
|
" (에디트 모드라 cleanupDelaySeconds 무시 · 즉시 실행)");
|
|
var live = table_monsterlist.Ins != null ? table_monsterlist.Ins.Get_Data_orNull(cfg.bossMonsterId) : null;
|
|
L(" 반경 원복 후 테이블 값 = " + (live != null ? live.f_BaseChaseRange.ToString("F1") : "행 없음") +
|
|
" restoreChaseRangeOnCleanup=" + cfg.restoreChaseRangeOnCleanup);
|
|
L(" ChaseRangeApplied = " + WL.Combat.Boss.BossArena.ChaseRangeApplied);
|
|
L(" LastLog = " + WL.Combat.Boss.BossArena.LastLog);
|
|
L(" ※ 리젠 차단은 FieldBossData.StopAllCoroutines() — 코루틴은 Play 에서만 돌아 「미확인」.");
|
|
L(" 가짜 FieldBossData 생존 = " + (s_fbd != null) + " (정리가 오브젝트를 파괴하지 않는다)");
|
|
}
|
|
|
|
// ───────────────────────────────────────── ⑦ C8
|
|
static void Section7_Rollback()
|
|
{
|
|
var cfg = WL.Combat.Boss.WLBossArenaSettings.Instance;
|
|
L("");
|
|
L("── ⑦ C8 롤백 — 어느 스위치든 끄면 813e 상시 배치 100%");
|
|
if (cfg == null) { L(" 설정 없음 — 건너뜀"); return; }
|
|
|
|
// ⑦-1 게이트 SO 런타임 스위치
|
|
WL.Combat.Boss.BossArena.ResetAll();
|
|
s_resumeCount = 0;
|
|
WL.Combat.Boss.WLBossArenaSettings.RuntimeDisabled = true;
|
|
bool a = WL.Combat.Boss.BossArena.HoldSpawn(s_fbd, FakeResume);
|
|
L(" WLBossArenaSettings.RuntimeDisabled=true → HoldSpawn=" + a +
|
|
" · Active=" + WL.Combat.Boss.WLBossArenaSettings.Active +
|
|
" · PassThrough=" + WL.Combat.Boss.BossArena.PassThroughCount);
|
|
WL.Combat.Boss.WLBossArenaSettings.RuntimeDisabled = false;
|
|
|
|
// ⑦-2 코어 off — 이벤트가 0 이라 게이트를 걸면 보스가 영영 안 나온다 → 걸지 않는다
|
|
WL.Combat.Boss.BossArena.ResetAll();
|
|
WL.Combat.Core.WLCombatCoreSettings.RuntimeDisabled = true;
|
|
bool b = WL.Combat.Boss.BossArena.HoldSpawn(s_fbd, FakeResume);
|
|
L(" WLCombatCoreSettings.RuntimeDisabled=true → HoldSpawn=" + b +
|
|
" · 코어 Enabled=" + WL.Combat.Core.WLCombatCoreSettings.Enabled +
|
|
" · 게이트 Active=" + WL.Combat.Boss.WLBossArenaSettings.Active);
|
|
WL.Combat.Core.WLCombatCoreSettings.RuntimeDisabled = false;
|
|
|
|
// ⑦-3 다른 FieldBossData(다른 spawnerId)는 손대지 않는다
|
|
WL.Combat.Boss.BossArena.ResetAll();
|
|
var otherGo = new UnityEngine.GameObject("WL813d_OtherFieldBoss");
|
|
var other = otherGo.AddComponent<FieldBossData>();
|
|
other.n_MonsterID = 10001; other.n_SpawnerId = 999999;
|
|
bool c = WL.Combat.Boss.BossArena.HoldSpawn(other, FakeResume);
|
|
L(" 다른 FieldBossData(spawnerId 999999) → HoldSpawn=" + c + " (false = 원본 즉시 배치 그대로)");
|
|
UnityEngine.Object.DestroyImmediate(otherGo);
|
|
|
|
// ⑦-4 gateEnabled=false 는 에셋 값이라 여기서 바꾸지 않는다(에셋 무변경) — 코드 경로만 명시
|
|
L(" gateEnabled=false / enabled=false / 에셋 삭제도 같은 경로(HoldSpawn 첫 3줄) — 에셋을 건드리지 않아 미실행");
|
|
L(" resume 호출 총합 = " + s_resumeCount + " (C8 구간에서 0 이어야 한다)");
|
|
}
|
|
|
|
// ───────────────────────────────────────── 유틸
|
|
static void FakeResume()
|
|
{
|
|
s_resumeCount++;
|
|
L(" ▶ resume() 호출 = 원본 FieldBossData.Start 재진입(보스 활성) #" + s_resumeCount);
|
|
}
|
|
|
|
/// <summary>Export 테이블(1줄 minified · 값은 문자열)에서 keyField=keyValue 인 행의 field 를 float 로 읽는다.</summary>
|
|
static float ReadFloatField(string path, string keyField, string keyValue, string field, float fallback)
|
|
{
|
|
try
|
|
{
|
|
string txt = System.IO.File.ReadAllText(path);
|
|
string anchor = "\"" + keyField + "\": \"" + keyValue + "\"";
|
|
int at = txt.IndexOf(anchor, System.StringComparison.Ordinal);
|
|
if (at < 0) return fallback;
|
|
int end = txt.IndexOf('}', at);
|
|
if (end < 0) end = txt.Length;
|
|
string row = txt.Substring(at, end - at);
|
|
var m = System.Text.RegularExpressions.Regex.Match(row, "\"" + field + "\"\\s*:\\s*\"([^\"]*)\"");
|
|
if (!m.Success) return fallback;
|
|
float v;
|
|
return float.TryParse(m.Groups[1].Value, out v) ? v : fallback;
|
|
}
|
|
catch (System.Exception) { return fallback; }
|
|
}
|
|
|
|
static string AliveList(int[] ids)
|
|
{
|
|
if (ids == null) return "";
|
|
var sb = new System.Text.StringBuilder();
|
|
for (int i = 0; i < ids.Length; i++)
|
|
{
|
|
if (i > 0) sb.Append(", ");
|
|
sb.Append(WL.Combat.Boss.BossArena.AliveOf(ids[i]));
|
|
}
|
|
return sb.ToString();
|
|
}
|
|
|
|
static string Join(int[] a)
|
|
{
|
|
if (a == null) return "(null)";
|
|
var sb = new System.Text.StringBuilder();
|
|
for (int i = 0; i < a.Length; i++) { if (i > 0) sb.Append(", "); sb.Append(a[i]); }
|
|
return sb.ToString();
|
|
}
|
|
|
|
static string Pad(string t, int n) { return t.Length >= n ? t + " " : t + new string(' ', n - t.Length); }
|
|
|
|
static void L(string line) { s.AppendLine(line); }
|
|
|
|
static string Flush()
|
|
{
|
|
try
|
|
{
|
|
System.IO.File.WriteAllText(kLog, s.ToString(), new System.Text.UTF8Encoding(true));
|
|
}
|
|
catch (System.Exception ex) { s.AppendLine("!! 로그 저장 실패: " + ex.Message); }
|
|
return s.ToString();
|
|
}
|
|
}
|