Project_WL/AgentScripts/WL813w7_Probe.cs

589 lines
33 KiB
C#
Raw Permalink Normal View History

// WL813w7_Probe.cs — #813w7(다음 런 존 재무장 · 결과창 중 PC 정지 D-45) 프로브
// 에디트 모드 전용 · Play 불필요 · 로그인 0 · 씬/에셋 무수정(임시 GameObject·임시 SO 는 즉시 파괴/회수)
//
// 상주 에디터:
// unity command run_script --project-path <wt> --file AgentScripts/WL813w7_Probe.cs --entry WL813w7_Probe.RunAll --timeout 300
// 결과: Console + AgentScripts/WL813w7_PROBE.txt (RESULT PASS / RESULT FAIL n)
//
// 무엇을 증명하나 (발주서 WL-813w7 §1 · 근거 = qa/W2-10_재채점_v8.md D-45)
// ① 에셋·SOT — 813w7 신규 12필드 값 실측 · 이중 스폰 방지 부등식 · 코드 상수 0
// ② 원인 산식 — 존0 앵커 ↔ 아레나 중심 24.1 m ≤ arenaRadius+zoneSpawnHoldMargin 30 m
// = 813w3 리젠 차단의 사정권 안이다(D-45 의 직접 원인 · 값으로 재현)
// ③ 수리 ① — 가짜 `RunEnded` → PC 조종 잡힘(JoystickStatus 1) · 타깃 해제 ·
// 가짜 `RunStarted("restart")` → 조종 원복(0) · 상한 만료 자동 해제
// ④ 수리 ②-a — 리젠 차단 판정(ShouldHoldZoneSpawn) 진리표: 게이트 전 0 · 런 존 0 · 그 밖 1
// ⑤ 수리 ②-b — 빈 존 감시 타이밍(8 s) · Restart 즉시 재무장 · 1웨이브 마리 수(WaveSize)
// ⑥ 수리 ③ — 런 밖 처치는 다음 런 존 진행에 안 센다(StartRun 스냅샷 차감)
// ⑦ GC 0 — 정지 틱 ×100 · 빈 존 감시 ×100 · 차단 판정 ×100 · 런 존 판정 ×100
// ⑧ C8 롤백 — 신규 스위치 0 → 813w6 동작 그대로 · RuntimeDisabled → 무동작
//
// 에디트 모드 한계(보고서 「미확인」)
// · `ActorInfo.Ins` 도 `table_monsterappear.Ins` 도 없다(플레이 모드 전용) → 실제 `On_Regen()` 재무장은
// 못 돌린다. 「언제 재무장을 트리거하는가」와 「1웨이브 마리 수 산정」까지를 증명한다.
// · `Time.unscaledTime` 이 스크립트 실행 중 멈춰 있다 → 시간 판정은 전부 인자로 먹인다.
// · PC 에 살아 있는 타깃(m_Target)을 붙일 수 없다(Set_Outline 이 렌더러를 만진다) → 타깃 해제는
// 「isTarget 플래그가 내려가는가」로 증명한다.
using System;
using System.IO;
using System.Text;
using UnityEngine;
using WL.Combat.Boss;
using WL.Combat.Core;
using WL.Combat.Run;
public static class WL813w7_Probe
{
const string OutCommit = "AgentScripts/WL813w7_PROBE.txt";
static StringBuilder sb;
static int s_fail;
static GameObject s_pcGo, s_fbdGo;
static PCActor s_pc, s_prevMyPc;
static FieldBossData s_fbd;
public static object RunAll()
{
sb = new StringBuilder();
s_fail = 0;
sb.AppendLine("# WL813w7 Probe " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " (edit mode · Play 0 · 로그인 0)");
sb.AppendLine("playMode=" + Application.isPlaying);
try
{
Setup();
Sec1_Asset();
Sec2_Cause();
Sec3_RunEndHold();
Sec4_HoldDecision();
Sec5_ZoneRearm();
Sec6_OutsideRunKills();
Sec7_GC();
Sec8_C8();
}
catch (Exception ex) { L("[FAIL] 예외 — " + ex.GetType().Name + ": " + ex.Message); L(ex.StackTrace); s_fail++; }
finally { Teardown(); }
L("");
L(s_fail == 0 ? "RESULT PASS" : "RESULT FAIL " + s_fail);
var text = sb.ToString();
try { File.WriteAllText(OutCommit, text, new UTF8Encoding(false)); } catch { }
Debug.Log(text);
return text;
}
// ───────────────────────────────────────── 유틸
static void L(string m) { sb.AppendLine(m); }
static string P(bool ok) { if (!ok) s_fail++; return ok ? "[PASS]" : "[FAIL]"; }
static long Alloc() { return GC.GetTotalMemory(false); }
static WLBossArenaSettings TmpB()
{
var t = UnityEngine.Object.Instantiate(WLBossArenaSettings.Instance);
t.hideFlags = HideFlags.HideAndDontSave;
return t;
}
static WLRunGuideSettings TmpG()
{
var t = UnityEngine.Object.Instantiate(WLRunGuideSettings.Instance);
t.hideFlags = HideFlags.HideAndDontSave;
return t;
}
static void UseTmpB(WLBossArenaSettings t) { WLBossArenaSettings.SetInstanceForProbe(t); }
static void UseRealB() { WLBossArenaSettings.SetInstanceForProbe(null); }
static void UseTmpG(WLRunGuideSettings t) { WLRunGuideSettings.SetInstanceForProbe(t); }
static void UseRealG() { WLRunGuideSettings.SetInstanceForProbe(null); }
static float DistXZ(Vector3 a, Vector3 b)
{
float dx = a.x - b.x, dz = a.z - b.z;
return Mathf.Sqrt(dx * dx + dz * dz);
}
static void Setup()
{
s_prevMyPc = MyValue.MyPC;
s_pcGo = new GameObject("__WL813w7_PC");
s_pc = s_pcGo.AddComponent<PCActor>();
s_pc.m_Role = eRole.PC; s_pc.m_SubRole = eSubRol.None;
MyValue.MyPC = s_pc;
WLBossArenaSettings.RuntimeDisabled = false;
WLRunGuideSettings.RuntimeDisabled = false;
BossArena.ResetAll();
BossArena.EnsureSubscribed();
RunDirector.ResetAll();
RunGuide.EnsureSubscribed();
RunGuide.ResetDiagnostics();
// 게이트 무장 — 원본 훅과 같은 진입점(HoldSpawn)으로 Arm 한다(s_zones 에 존 id 가 들어간다).
var cfg = WLBossArenaSettings.Instance;
s_fbdGo = new GameObject("__WL813w7_FBD");
s_fbd = s_fbdGo.AddComponent<FieldBossData>();
s_fbd.n_SpawnerId = cfg != null ? cfg.bossSpawnerId : 813010;
s_fbd.n_MonsterID = cfg != null ? cfg.bossMonsterId : 10006;
bool held = BossArena.HoldSpawn(s_fbd, NoopResume);
L("");
L("── ⓪ 준비");
L(" " + P(WLBossArenaSettings.Instance != null) + " WLBossArenaSettings 로드 · Active=" + WLBossArenaSettings.Active);
L(" " + P(WLRunGuideSettings.Instance != null) + " WLRunGuideSettings 로드 · Enabled=" + WLRunGuideSettings.Enabled);
L(" " + P(WLRunSettings.Instance != null) + " WLRunSettings 로드 · Enabled=" + WLRunSettings.Enabled);
L(" " + P(RunGuide.Subscribed) + " RunGuide.Subscribed(RunEvents)");
L(" " + P(held && BossArena.GateArmed && !BossArena.GateOpen) +
" HoldSpawn(FieldBossData) → 게이트 무장(armed=" + BossArena.GateArmed + " open=" + BossArena.GateOpen + ")");
}
static void NoopResume() { }
static void Teardown()
{
try
{
UseRealB(); UseRealG();
BossArena.SetProbeTime(-1f);
BossArena.SetRunActiveForProbe(0);
BossArena.SetBossForProbe(null);
BossArena.ResetAll();
RunGuide.EndRunEndHold("teardown");
RunGuide.ResetDiagnostics();
RunDirector.ResetAll();
WLBossArenaSettings.RuntimeDisabled = false;
WLRunGuideSettings.RuntimeDisabled = false;
MyValue.MyPC = s_prevMyPc;
if (s_pcGo != null) UnityEngine.Object.DestroyImmediate(s_pcGo);
if (s_fbdGo != null) UnityEngine.Object.DestroyImmediate(s_fbdGo);
}
catch { }
}
// ─────────────────────────────────────────────────────────────────────
// ① 에셋 · SOT (C45 — 값은 전부 .asset)
// ─────────────────────────────────────────────────────────────────────
static void Sec1_Asset()
{
L("");
L("── ① 에셋 (813w7 신규 12필드 · 값 전부 SO = C45)");
var g = WLRunGuideSettings.Instance;
var b = WLBossArenaSettings.Instance;
if (g == null || b == null) { L(" [FAIL] 에셋 없음"); s_fail++; return; }
L(" RunGuide: holdPcOnRunEnd=" + g.holdPcOnRunEnd +
" runEndHoldMaxSeconds=" + g.runEndHoldMaxSeconds +
" runEndHoldClearsTarget=" + g.runEndHoldClearsTarget +
" runEndHoldStopsAgent=" + g.runEndHoldStopsAgent);
L(" " + P(g.holdPcOnRunEnd) + " holdPcOnRunEnd=1 (발주서 §1-2 ① 기본값)");
L(" " + P(g.runEndHoldMaxSeconds > 0f) + " runEndHoldMaxSeconds>0 (결과창을 닫고 방치해도 스스로 풀린다)");
L(" " + P(g.runEndHoldClearsTarget) + " runEndHoldClearsTarget=1 (자동 스킬 루프 차단 · PCActor.cs:93 isTarget 조건)");
L(" " + P(g.runEndHoldStopsAgent) + " runEndHoldStopsAgent=1 (잔여 경로 제거)");
L(" BossArena: holdZoneSpawnOnlyAfterGate=" + b.holdZoneSpawnOnlyAfterGate +
" holdZoneSpawnSkipsRunZones=" + b.holdZoneSpawnSkipsRunZones +
" rearmZoneSpawnsEnabled=" + b.rearmZoneSpawnsEnabled +
" rearmZoneEmptySeconds=" + b.rearmZoneEmptySeconds +
" rearmZoneCheckSeconds=" + b.rearmZoneCheckSeconds +
" rearmZoneRegenMax=" + b.rearmZoneRegenMax +
" rearmZoneRunZonesOnly=" + b.rearmZoneRunZonesOnly +
" rearmZoneSpawnsOnRestart=" + b.rearmZoneSpawnsOnRestart);
L(" " + P(b.holdZoneSpawnOnlyAfterGate) + " holdZoneSpawnOnlyAfterGate=1 (차단을 보스전으로 좁힌다)");
L(" " + P(b.holdZoneSpawnSkipsRunZones) + " holdZoneSpawnSkipsRunZones=1 (런 사냥 존은 대상 제외)");
L(" " + P(b.rearmZoneSpawnsEnabled) + " rearmZoneSpawnsEnabled=1");
L(" " + P(b.rearmZoneSpawnsOnRestart) + " rearmZoneSpawnsOnRestart=1 ([다음 런] 즉시 1회)");
L(" " + P(b.rearmZoneRegenMax > 0) + " rearmZoneRegenMax>0 (테이블 실패 시 폴백·상한)");
// 🔴 이중 스폰 방지 부등식 — 원본 리듬 = 시체 3.0 s(Actor.cs:631) + f_SpawnerDelay 2.0 s(813e2 표 A) = 5.0 s
const float kOriginalRegenRhythm = 5.0f; // 게임 수치가 아니라 813e2 실측 리듬(검증 기준값)
L(" " + P(b.rearmZoneEmptySeconds > kOriginalRegenRhythm) +
" 🔴 rearmZoneEmptySeconds(" + b.rearmZoneEmptySeconds + "s) > 원본 리듬 " + kOriginalRegenRhythm +
"s — 멀쩡한 스포너와 겹치지 않는다(이중 스폰 방지)");
L(" " + P(b.rearmZoneCheckSeconds > 0f && b.rearmZoneCheckSeconds < b.rearmZoneEmptySeconds) +
" 감시 주기(" + b.rearmZoneCheckSeconds + "s) < 임계(" + b.rearmZoneEmptySeconds + "s)");
L(" " + P(b.holdZoneSpawnNearArena) + " 813w3 holdZoneSpawnNearArena=1 유지(차단 경로 자체는 그대로 · 조건만 좁혔다)");
}
// ─────────────────────────────────────────────────────────────────────
// ② 원인 산식 — 존0 이 리젠 차단 사정권 안이다
// ─────────────────────────────────────────────────────────────────────
static void Sec2_Cause()
{
L("");
L("── ② 🔴 D-45 원인 산식 (Q10: R1n 0/18 · R2n 1/18 PC (112.93,0.06,16.60) 고정 무피해 · R3n 14/18 정지)");
var b = WLBossArenaSettings.Instance;
var g = WLRunGuideSettings.Instance;
var rs = WLRunSettings.Instance;
if (b == null || g == null || rs == null) { L(" [FAIL] 에셋 없음"); s_fail++; return; }
Vector3 center = b.arenaCenter;
float lim = b.arenaRadius + b.zoneSpawnHoldMargin;
L(" 아레나 중심 " + center.ToString("F2") + " · 차단 반경 = arenaRadius " + b.arenaRadius +
" + zoneSpawnHoldMargin " + b.zoneSpawnHoldMargin + " = " + lim.ToString("F0") + " m");
var fb = g.zoneAnchorFallbacks;
for (int i = 0; i < rs.zoneSpawnerIds.Length; i++)
{
if (fb == null || i >= fb.Length) continue;
float d = DistXZ(fb[i], center);
L(" 존" + i + "(id " + rs.zoneSpawnerIds[i] + ") 앵커 " + fb[i].ToString("F2") +
" → 중심 " + d.ToString("F1") + " m · 차단 사정권 " + (d <= lim ? "안 🔴" : "밖"));
}
float d0 = fb != null && fb.Length > 0 ? DistXZ(fb[0], center) : -1f;
L(" " + P(d0 >= 0f && d0 <= lim) +
" 🔴 존0 이 차단 사정권 안(" + d0.ToString("F1") + " m ≤ " + lim.ToString("F0") + " m) = D-45 의 직접 원인");
// PC 고정 좌표(Q10 R2n 실측)가 존0 앵커에 도착한 자리인가 = 「PC 는 존에 있는데 몹이 없다」
var pcSeen = new Vector3(112.93f, 0.06f, -16.60f);
float dPc = fb != null && fb.Length > 0 ? DistXZ(pcSeen, fb[0]) : -1f;
L(" " + P(dPc >= 0f && dPc <= g.zoneArriveRadius) +
" Q10 R2n PC 좌표 " + pcSeen.ToString("F2") + " ↔ 존0 앵커 " + dPc.ToString("F1") +
" m ≤ zoneArriveRadius " + g.zoneArriveRadius + " = **도착(Hold)했는데 때릴 몹이 없었다**");
L(" 차단 방식 = MobControlMgr.StopAllCoroutines() → PhaseAllKill 의 `killmobcount = 0`(MobControlMgr.cs:218)이");
L(" 함께 끊겨 카운터가 n_MaxMonsterCount 에 굳는다 → `==` 판정(:214)이 두 번 다시 참이 안 된다 = 영구 소진.");
L(" (MyCoroutine.Set_Coroutine = StartCoroutine(MyCoroutine.cs) 이므로 StopAllCoroutines 에 걸린다 · 실측)");
}
// ─────────────────────────────────────────────────────────────────────
// ③ 수리 ① — 결과창 중 PC 정지
// ─────────────────────────────────────────────────────────────────────
static void Sec3_RunEndHold()
{
L("");
L("── ③ 수리 ① 결과창 중 PC 정지 (D-45 원인 = 결과창 100 s 사이 z0 처치 26 → 47)");
var g = WLRunGuideSettings.Instance;
RunGuide.ResetDiagnostics();
s_pc.Set_JoystickStatus(3);
s_pc.isTarget = false;
int holds0 = RunGuide.RunEndHolds;
RunGuide.ProbeRunEnded(RunOutcome.Win);
RunGuide.SetRunEndHoldUntilForProbe(-1f); // 에디트 모드는 Time 이 안 흘러 상한 판정을 따로 검증한다(아래 ⓑ)
L(" " + P(RunGuide.RunEndHeld && RunGuide.RunEndHolds == holds0 + 1) +
" 가짜 RunEnded(Win) → 정지 시작(RunEndHeld=" + RunGuide.RunEndHeld + " RunEndHolds=" + RunGuide.RunEndHolds + ")");
L(" " + P(RunGuide.State == RunGuideState.Off && RunGuide.DestKind == RunGuideDest.None) +
" 가이드는 꺼진다(State=" + RunGuide.State + " Dest=" + RunGuide.DestKind + ") — 813w6 계약 유지");
// 정지 1틱 — 조종을 잡고 타깃을 뗀다
s_pc.isTarget = true;
int clears0 = RunGuide.RunEndTargetClears;
RunGuide.RunEndHoldTick(g, s_pc, 100f);
L(" " + P(s_pc.JoystickStatus == 1 && RunGuide.Steering) +
" 🔴 정지 틱 → JoystickStatus=" + s_pc.JoystickStatus + "(1 = 원본 자동 교전 접근 루프 차단 · PCActor.cs:139) Steering=" + RunGuide.Steering);
L(" " + P(!s_pc.isTarget && RunGuide.RunEndTargetClears == clears0 + 1) +
" 🔴 정지 틱 → 타깃 해제(isTarget=" + s_pc.isTarget + " RunEndTargetClears=" + RunGuide.RunEndTargetClears +
") = 자동 스킬 루프(PCActor.cs:93)도 돌지 않는다");
int ticks0 = RunGuide.RunEndHoldTicks;
RunGuide.RunEndHoldTick(g, s_pc, 101f);
RunGuide.RunEndHoldTick(g, s_pc, 102f);
L(" " + P(RunGuide.RunEndHoldTicks == ticks0 + 2 && RunGuide.RunEndHeld) +
" 정지가 이어진다(틱 " + RunGuide.RunEndHoldTicks + " · 상한 전)");
// 복귀 ⓐ — 다음 런 시작(RunDirector.Restart → StartRun("restart"))
int rel0 = RunGuide.RunEndHoldReleases;
RunGuide.ProbeRunStarted(2, "restart");
L(" " + P(!RunGuide.RunEndHeld && RunGuide.RunEndHoldReleases == rel0 + 1) +
" 🔴 가짜 RunStarted(\"restart\") → 정지 해제(RunEndHeld=" + RunGuide.RunEndHeld +
" 해제 " + RunGuide.RunEndHoldReleases + " 사유 '" + RunGuide.LastRunEndHoldReason + "')");
L(" " + P(s_pc.JoystickStatus == 0 && !RunGuide.Steering) +
" 🔴 조종 원복 JoystickStatus=" + s_pc.JoystickStatus + "(0 = 원본 그대로) Steering=" + RunGuide.Steering);
L(" " + P(RunGuide.RestartGuides >= 1) +
" 813w4 재유도는 그대로 산다(RestartGuides=" + RunGuide.RestartGuides + ") — 정지 해제가 유도보다 앞");
// 복귀 ⓑ — 상한 만료(결과창을 [닫기]로 닫고 런을 다시 시작하지 않는 경우)
RunGuide.StopGuide("프로브");
RunGuide.BeginRunEndHold("상한 시험");
RunGuide.SetRunEndHoldUntilForProbe(200f);
RunGuide.RunEndHoldTick(g, s_pc, 199f);
bool stillHeld = RunGuide.RunEndHeld;
RunGuide.RunEndHoldTick(g, s_pc, 201f);
L(" " + P(stillHeld && !RunGuide.RunEndHeld) +
" 🔴 상한 만료 자동 해제(199 s 유지 → 201 s 해제 · 사유 '" + RunGuide.LastRunEndHoldReason + "')");
L(" " + P(s_pc.JoystickStatus == 0) + " 만료 해제에서도 조종 원복(JoystickStatus=" + s_pc.JoystickStatus + ")");
// 이미 스킬 이동 잠금(2)이면 건드리지 않는다
RunGuide.ResetDiagnostics();
s_pc.Set_JoystickStatus(2);
RunGuide.BeginRunEndHold("잠금 시험");
RunGuide.SetRunEndHoldUntilForProbe(-1f);
RunGuide.RunEndHoldTick(g, s_pc, 300f);
L(" " + P(s_pc.JoystickStatus == 2 && !RunGuide.Steering) +
" JoystickStatus=2(스킬 이동 잠금)는 건드리지 않는다(=" + s_pc.JoystickStatus + " Steering=" + RunGuide.Steering + ")");
RunGuide.EndRunEndHold("프로브 정리");
s_pc.Set_JoystickStatus(3);
RunGuide.ResetDiagnostics();
}
// ─────────────────────────────────────────────────────────────────────
// ④ 수리 ②-a — 리젠 차단 판정 진리표
// ─────────────────────────────────────────────────────────────────────
static void Sec4_HoldDecision()
{
L("");
L("── ④ 수리 ②-a 리젠 차단 판정(ShouldHoldZoneSpawn · 순수 함수)");
var rs = WLRunSettings.Instance;
int runZone = rs != null && rs.zoneSpawnerIds != null && rs.zoneSpawnerIds.Length > 0 ? rs.zoneSpawnerIds[0] : 813001;
int other = 813005;
L(" " + P(BossArena.IsRunZoneSpawner(runZone)) + " IsRunZoneSpawner(" + runZone + ")=true (런 사냥 존)");
L(" " + P(!BossArena.IsRunZoneSpawner(other)) + " IsRunZoneSpawner(" + other + ")=false (런 밖 존)");
var t = TmpB();
try
{
UseTmpB(t);
t.holdZoneSpawnNearArena = true;
t.holdZoneSpawnOnlyAfterGate = true;
t.holdZoneSpawnSkipsRunZones = true;
L(" " + P(!BossArena.ShouldHoldZoneSpawn(t, other, false)) +
" 🔴 게이트 **닫힘** + 런 밖 존 → 차단 안 함(= 813w6 이 만든 「다음 런 내내 차단」이 사라진다)");
L(" " + P(!BossArena.ShouldHoldZoneSpawn(t, runZone, true)) +
" 🔴 게이트 열림 + **런 사냥 존** → 차단 안 함(D-45 재발 차단)");
L(" " + P(BossArena.ShouldHoldZoneSpawn(t, other, true)) +
" 게이트 열림 + 런 밖 존 → 차단(813w3 잡몹 유입 차단 의도 유지)");
// C8 — 두 스위치를 끄면 813w3 그대로(보스 생존 = 상시 차단)
t.holdZoneSpawnOnlyAfterGate = false;
t.holdZoneSpawnSkipsRunZones = false;
L(" " + P(BossArena.ShouldHoldZoneSpawn(t, runZone, false)) +
" C8: 두 스위치 0 → 게이트 전 + 런 존도 차단 = 813w3 동작 그대로");
t.holdZoneSpawnNearArena = false;
L(" " + P(!BossArena.ShouldHoldZoneSpawn(t, other, true)) + " C8: holdZoneSpawnNearArena=0 → 언제나 차단 0");
}
finally { UseRealB(); UnityEngine.Object.DestroyImmediate(t); }
}
// ─────────────────────────────────────────────────────────────────────
// ⑤ 수리 ②-b — 빈 존 감시 · 재무장
// ─────────────────────────────────────────────────────────────────────
static void Sec5_ZoneRearm()
{
L("");
L("── ⑤ 수리 ②-b 존 재무장(원본 리셋 API 없음 → MobActor.On_Regen = MobControlMgr.cs:221 과 같은 호출)");
var b = WLBossArenaSettings.Instance;
var rs = WLRunSettings.Instance;
int z0 = rs != null && rs.zoneSpawnerIds != null && rs.zoneSpawnerIds.Length > 0 ? rs.zoneSpawnerIds[0] : 813001;
// 감시 목록 — 런 존만
BossArena.RebuildRearmZonesForProbe();
L(" 감시 대상 " + BossArena.RearmZoneCount + "개: " +
(BossArena.RearmZoneCount > 0 ? BossArena.RearmZoneIdAt(0).ToString() : "-") +
(BossArena.RearmZoneCount > 1 ? " / " + BossArena.RearmZoneIdAt(1) : ""));
L(" " + P(rs != null && BossArena.RearmZoneCount == rs.zoneSpawnerIds.Length) +
" 대상 = WLRunSettings.zoneSpawnerIds 전부(rearmZoneRunZonesOnly=1)");
L(" " + P(BossArena.RearmZoneCount > 0 && BossArena.RearmZoneIdAt(0) == z0) + " 첫 대상 = 존0(" + z0 + ")");
// 1웨이브 마리 수 — 에디트 모드엔 table_monsterappear 가 없어 SO 폴백을 쓴다
int wave = BossArena.WaveSize(b, z0);
L(" " + P(wave > 0 && wave <= b.rearmZoneRegenMax) +
" WaveSize(" + z0 + ")=" + wave + " (테이블 n_MaxMonsterCount → 없으면 rearmZoneRegenMax " + b.rearmZoneRegenMax + " · 상한도 그 값)");
// 타이밍 — 살아 있으면 타이머가 안 돈다 / 비면 임계까지 기다렸다 트리거
BossArena.SetRunActiveForProbe(1);
BossArena.NoteSpawn(z0, false); // alive 1
BossArena.ZoneRearmTick(b, 100f);
L(" " + P(BossArena.AliveOf(z0) > 0 && BossArena.RearmZoneEmptySinceAt(0) < 0f) +
" alive=" + BossArena.AliveOf(z0) + " → 빈 시각 미기록(" + BossArena.RearmZoneEmptySinceAt(0) + ")");
BossArena.NoteKill(z0, false); // alive 0
BossArena.ZoneRearmTick(b, 100f);
L(" " + P(BossArena.AliveOf(z0) == 0 && Mathf.Approximately(BossArena.RearmZoneEmptySinceAt(0), 100f)) +
" alive=0 → 빈 시각 " + BossArena.RearmZoneEmptySinceAt(0) + " 기록");
BossArena.ZoneRearmTick(b, 100f + b.rearmZoneEmptySeconds - 1f);
L(" " + P(Mathf.Approximately(BossArena.RearmZoneEmptySinceAt(0), 100f)) +
" 임계(" + b.rearmZoneEmptySeconds + "s) 전 → 트리거 없음(빈 시각 그대로 " + BossArena.RearmZoneEmptySinceAt(0) + ")");
float fire = 100f + b.rearmZoneEmptySeconds + 0.5f;
BossArena.ZoneRearmTick(b, fire);
L(" " + P(Mathf.Approximately(BossArena.RearmZoneEmptySinceAt(0), fire)) +
" 🔴 임계 경과 → 재무장 트리거(빈 시각이 " + BossArena.RearmZoneEmptySinceAt(0) + " 로 갱신 = RearmZone 호출)");
L(" (에디트 모드는 ActorInfo.Ins 가 없어 실제 On_Regen 은 0마리 — 「언제 트리거되는가」까지 증명 · §미확인)");
// 런 밖에서는 감시하지 않는다(결과창 동안 몹을 우리가 늘리지 않는다)
BossArena.SetRunActiveForProbe(-1);
float before = BossArena.RearmZoneEmptySinceAt(0);
BossArena.ZoneRearmTick(b, fire + 100f);
L(" " + P(Mathf.Approximately(BossArena.RearmZoneEmptySinceAt(0), before)) +
" 런 밖(결과창)에서는 감시 정지(빈 시각 " + BossArena.RearmZoneEmptySinceAt(0) + " 불변)");
BossArena.SetRunActiveForProbe(1);
// [다음 런] 즉시 재무장 진입점
int n = BossArena.RearmZoneSpawns("프로브 다음 런");
L(" " + P(BossArena.RearmZoneCount > 0) +
" RearmZoneSpawns() 진입점 동작(재등장 " + n + "마리 · 에디트 모드라 0 · 목록 " + BossArena.RearmZoneCount + "개 재구축)");
BossArena.SetRunActiveForProbe(0);
}
// ─────────────────────────────────────────────────────────────────────
// ⑥ 수리 ③ — 런 밖 처치는 다음 런 존 진행에 안 센다
// ─────────────────────────────────────────────────────────────────────
static void Sec6_OutsideRunKills()
{
L("");
L("── ⑥ 수리 ③ 런 밖 처치가 존 카운터에 안 실린다(Q10: 결과창 동안 z0 26 → 47)");
var rs = WLRunSettings.Instance;
int z0 = rs.zoneSpawnerIds[0];
RunDirector.ResetAll();
RunDirector.EnsureSubscribed();
BossArena.SetRunActiveForProbe(-1); // 런 밖(결과창)
int arena0 = BossArena.KillsOf(z0);
int gateEvals0 = BossArena.GateSuspendedEvals;
for (int i = 0; i < 21; i++) // Q10 실측과 같은 21마리
{
RunDirector.NoteKill(z0, false); // 런 밖 → IsRunning=false → 무시
BossArena.NoteKill(z0, false); // 누적값은 813p 계약대로 계속 센다
}
L(" " + P(BossArena.KillsOf(z0) == arena0 + 21) +
" BossArena 누적은 계속 센다(" + arena0 + " → " + BossArena.KillsOf(z0) + ") = 813p 계약(리셋 금지) 유지");
L(" " + P(BossArena.GateSuspendedEvals > gateEvals0) +
" 런 밖 게이트 평가는 건너뛴다(GateSuspendedEvals " + gateEvals0 + " → " + BossArena.GateSuspendedEvals + ")");
BossArena.SetRunActiveForProbe(1);
RunDirector.StartRun("probe restart"); // ← 여기서 s_zoneBase 를 다시 잡는다
L(" " + P(RunDirector.ZoneProgress(0) == 0) +
" 🔴 다음 런 시작 직후 존0 진행 = " + RunDirector.ZoneProgress(0) + "/" + RunDirector.ZoneRequired(0) +
" — 런 밖 21처치가 한 건도 안 실렸다(StartRun 스냅샷 차감)");
RunDirector.NoteKill(z0, false);
L(" " + P(RunDirector.ZoneProgress(0) == 1) +
" 런 안 처치 1건은 정상 반영(" + RunDirector.ZoneProgress(0) + "/" + RunDirector.ZoneRequired(0) + ")");
RunDirector.ResetAll();
BossArena.SetRunActiveForProbe(0);
RunGuide.ResetDiagnostics();
}
// ─────────────────────────────────────────────────────────────────────
// ⑦ GC 0
// ─────────────────────────────────────────────────────────────────────
static void Sec7_GC()
{
L("");
L("── ⑦ GC 0 (틱 경로 · 각 100회)");
var g = WLRunGuideSettings.Instance;
var b = WLBossArenaSettings.Instance;
var rs = WLRunSettings.Instance;
int z0 = rs.zoneSpawnerIds[0];
int other = 813005;
// ⓐ 결과창 정지 틱
RunGuide.ResetDiagnostics();
s_pc.Set_JoystickStatus(3);
RunGuide.BeginRunEndHold("GC");
RunGuide.SetRunEndHoldUntilForProbe(-1f);
RunGuide.RunEndHoldTick(g, s_pc, 10f); // 워밍업(조종 획득 1회)
long a0 = Alloc();
for (int i = 0; i < 100; i++) RunGuide.RunEndHoldTick(g, s_pc, 10f + i);
long a1 = Alloc();
L(" " + P(a1 - a0 <= 0) + " 정지 틱 ×100 = " + (a1 - a0) + " B");
RunGuide.EndRunEndHold("GC 끝");
s_pc.Set_JoystickStatus(3);
// ⓑ 빈 존 감시
BossArena.SetRunActiveForProbe(1);
BossArena.ZoneRearmTick(b, 500f);
long b0 = Alloc();
for (int i = 0; i < 100; i++) BossArena.ZoneRearmTick(b, 500f + i * 0.01f);
long b1 = Alloc();
L(" " + P(b1 - b0 <= 0) + " 빈 존 감시 ×100 = " + (b1 - b0) + " B");
BossArena.SetRunActiveForProbe(0);
// ⓒ 차단 판정 · 런 존 판정
long c0 = Alloc();
bool acc = false;
for (int i = 0; i < 100; i++) acc ^= BossArena.ShouldHoldZoneSpawn(b, i % 2 == 0 ? z0 : other, i % 2 == 0);
long c1 = Alloc();
L(" " + P(c1 - c0 <= 0) + " 차단 판정 ×100 = " + (c1 - c0) + " B (acc=" + acc + ")");
long d0 = Alloc();
for (int i = 0; i < 100; i++) acc ^= BossArena.IsRunZoneSpawner(i % 2 == 0 ? z0 : other);
long d1 = Alloc();
L(" " + P(d1 - d0 <= 0) + " 런 존 판정 ×100 = " + (d1 - d0) + " B (acc=" + acc + ")");
RunGuide.ResetDiagnostics();
}
// ─────────────────────────────────────────────────────────────────────
// ⑧ C8 롤백
// ─────────────────────────────────────────────────────────────────────
static void Sec8_C8()
{
L("");
L("── ⑧ C8 롤백 (신규 스위치 0 → 813w6 동작 그대로)");
// ⓐ holdPcOnRunEnd = 0 → 런 종료 정지 경로가 통째로 죽는다
var tg = TmpG();
try
{
UseTmpG(tg);
tg.holdPcOnRunEnd = false;
RunGuide.ResetDiagnostics();
s_pc.Set_JoystickStatus(3);
RunGuide.ProbeRunEnded(RunOutcome.Timeout);
L(" " + P(!RunGuide.RunEndHeld && RunGuide.RunEndHolds == 0) +
" holdPcOnRunEnd=0 → 정지 0(RunEndHeld=" + RunGuide.RunEndHeld + " RunEndHolds=" + RunGuide.RunEndHolds + ")");
L(" " + P(s_pc.JoystickStatus == 0) + " 조종도 잡지 않는다(JoystickStatus=" + s_pc.JoystickStatus + ") = 813w6 그대로");
// 정지 중에 스위치를 끄면 그 자리에서 풀린다
tg.holdPcOnRunEnd = true;
RunGuide.BeginRunEndHold("C8");
tg.holdPcOnRunEnd = false;
RunGuide.RunEndHoldTick(tg, s_pc, 900f);
L(" " + P(!RunGuide.RunEndHeld) + " 정지 중 스위치 off → 즉시 해제(사유 '" + RunGuide.LastRunEndHoldReason + "')");
}
finally { UseRealG(); UnityEngine.Object.DestroyImmediate(tg); RunGuide.ResetDiagnostics(); }
// ⓑ rearmZoneSpawnsEnabled = 0 → 감시·재무장 경로가 통째로 죽는다
var tb = TmpB();
try
{
UseTmpB(tb);
BossArena.SetRunActiveForProbe(1);
BossArena.RebuildRearmZonesForProbe();
tb.rearmZoneSpawnsEnabled = false;
BossArena.ZoneRearmTick(tb, 1000f);
L(" " + P(BossArena.RearmZoneEmptySinceAt(0) < 0f) +
" rearmZoneSpawnsEnabled=0 → 감시 무동작(빈 시각 " + BossArena.RearmZoneEmptySinceAt(0) + ")");
L(" " + P(BossArena.RearmZoneSpawns("C8") == 0) + " RearmZoneSpawns() 도 0");
tb.rearmZoneSpawnsEnabled = true;
tb.rearmZoneSpawnsOnRestart = false;
L(" " + P(BossArena.RearmZoneSpawns("C8") == 0) + " rearmZoneSpawnsOnRestart=0 → [다음 런] 즉시 재무장 0");
}
finally { UseRealB(); BossArena.SetRunActiveForProbe(0); UnityEngine.Object.DestroyImmediate(tb); }
// ⓒ RuntimeDisabled — 축 전체 off
WLRunGuideSettings.RuntimeDisabled = true;
RunGuide.ResetDiagnostics();
RunGuide.BeginRunEndHold("RuntimeDisabled");
RunGuide.ForceTick();
L(" " + P(!WLRunGuideSettings.Enabled && !RunGuide.Active) + " RunGuide RuntimeDisabled → Active=false");
L(" " + P(!RunGuide.RunEndHeld) + " Tick 이 정지를 풀고 원본으로 돌려준다(RunEndHeld=" + RunGuide.RunEndHeld + ")");
WLRunGuideSettings.RuntimeDisabled = false;
WLBossArenaSettings.RuntimeDisabled = true;
var b2 = WLBossArenaSettings.Instance;
BossArena.SetRunActiveForProbe(1);
BossArena.RebuildRearmZonesForProbe();
BossArena.ZoneRearmTick(b2, 2000f);
L(" " + P(BossArena.RearmZoneEmptySinceAt(0) < 0f) + " BossArena RuntimeDisabled → 감시 무동작");
WLBossArenaSettings.RuntimeDisabled = false;
BossArena.SetRunActiveForProbe(0);
RunGuide.ResetDiagnostics();
}
}