Project_WL/AgentScripts/WL813w7_Probe.cs

589 lines
33 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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();
}
}