Project_WL/AgentScripts/WL813w5_Probe.cs

543 lines
30 KiB
C#
Raw Permalink Normal View History

// WL813w5_Probe.cs — #813w5(「다음 런」 앞판 보스 정리 · 게이트 조기 개방 방지) 프로브
// 에디트 모드 전용 · Play 불필요 · 로그인 0 · 씬/에셋 무수정(임시 GameObject·임시 SO 는 즉시 파괴/회수)
//
// 상주 에디터:
// unity command run_script --project-path <wt> --file AgentScripts/WL813w5_Probe.cs --entry WL813w5_Probe.RunAll --timeout 300
// 결과: Console + AgentScripts/WL813w5_PROBE.txt (RESULT PASS / RESULT FAIL n)
//
// 무엇을 증명하나 (발주서 WL-813w5 §1 · 근거 = qa/W2-8_재채점_v6.md D-39)
// ① 에셋·SOT — 813w5 신규 8필드 값 실측 · 반경/시간 정합성 · 코드 상수 0
// ② 🔴 원인 ① — 원본 MobActor.On_Regen 이 그 안에서 Check_Battle() → FindNextTarget(10000f) 를 부르는
// **원본 파일:줄을 직접 읽어** 실측한다(813w3 이 「워프·만피·타깃 해제」를 해도 안 먹은 이유)
// ③ 원인 ③ — 게이트 조기 개방 진리표(gateMinSeconds · reason 별 · 런 밖 · 런 시작 전)
// ④ 홀드 — ArmBossHold → 가짜 시각으로 29 s 유지 · 31 s 해제 · 0 이면 홀드 없음
// ⑤ RunEnded — 진짜 RunEvents.RunEnded 발행 → 구독·예외 0 · 보스 없음이면 ParkCount 불변
// ⑥ 런 밖 거리 — LeashTick 을 런 밖 강제로 돌려 「거리 판정만 유지」 경로가 도는지(회복 0)
// ⑦ 실 경로 — RunDirector.StartRun/Restart + BossArena.NoteKill 로 게이트가 60 s 전에 안 열리는지
// ⑧ GC 0 — 게이트 판정 ×100 · 홀드 판정 ×100 · 런 밖 틱 ×100
// ⑨ C8 롤백 — 신규 스위치 0 → 813w3 동작 그대로(임시 SO)
//
// 에디트 모드 한계(보고서 「미확인」)
// · 살아 있는 MobActor(m_Stat·m_MonsterTableData·Animator·NavMeshAgent)가 없어 ParkBoss 의 실제 실행
// (Set_Warp · On_Regen · Change_MobStatus)은 못 돌린다 — 813d/813w3 미확인 ②와 같은 한계다.
// · Time.unscaledTime 이 스크립트 실행 중 멈춰 있다 → 시간 의존 판정은 BossArena.SetProbeTime 으로 먹인다.
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 WL813w5_Probe
{
const string OutCommit = "AgentScripts/WL813w5_PROBE.txt";
static StringBuilder sb;
static int s_fail;
static GameObject s_pcGo;
static PCActor s_pc, s_prevMyPc;
public static object RunAll()
{
sb = new StringBuilder();
s_fail = 0;
sb.AppendLine("# WL813w5 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_GateMin();
Sec4_Hold();
Sec5_RunEnded();
Sec6_OutsideRun();
Sec7_RealPath();
Sec8_GC();
Sec9_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); }
/// <summary>실에셋을 복사한 임시 SO(실에셋은 절대 안 건드린다). 쓰고 나면 DestroyImmediate + 캐시 원복.</summary>
static WLBossArenaSettings Tmp()
{
var t = UnityEngine.Object.Instantiate(WLBossArenaSettings.Instance);
t.hideFlags = HideFlags.HideAndDontSave;
return t;
}
static void UseTmp(WLBossArenaSettings t) { WLBossArenaSettings.SetInstanceForProbe(t); }
static void UseReal() { WLBossArenaSettings.SetInstanceForProbe(null); }
static void Setup()
{
s_prevMyPc = MyValue.MyPC;
s_pcGo = new GameObject("__WL813w5_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;
BossArena.ResetAll();
BossArena.EnsureSubscribed();
RunDirector.ResetAll();
L("");
L("── ⓪ 준비");
L(" " + P(WLBossArenaSettings.Instance != null) + " WLBossArenaSettings 로드 · Active=" + WLBossArenaSettings.Active);
L(" " + P(BossArena.Subscribed) + " BossArena.Subscribed(CombatEvents)");
L(" " + P(BossArena.RunSubscribed) + " 🔴 BossArena.RunSubscribed(RunEvents.RunEnded) — 813w5 신규 구독");
}
static void Teardown()
{
try
{
UseReal();
BossArena.SetProbeTime(-1f);
BossArena.ClearBossHold();
BossArena.SetRunActiveForProbe(0);
BossArena.SetBossForProbe(null);
BossArena.ResetAll();
RunDirector.ResetAll();
WLBossArenaSettings.RuntimeDisabled = false;
MyValue.MyPC = s_prevMyPc;
if (s_pcGo != null) UnityEngine.Object.DestroyImmediate(s_pcGo);
}
catch { }
}
// ─────────────────────────────────────────────────────────────────────
// ① 에셋 · SOT
// ─────────────────────────────────────────────────────────────────────
static void Sec1_Asset()
{
L("");
L("── ① 에셋 (Assets/WL/Combat/Settings/Resources/WL/WLBossArenaSettings.asset · 값 전부 SO = C45)");
var b = WLBossArenaSettings.Instance;
if (b == null) { L(" [FAIL] 에셋 없음"); s_fail++; return; }
L(" 신규 8필드: parkBossOnRunEnd=" + b.parkBossOnRunEnd +
" nextRunBossHoldSeconds=" + b.nextRunBossHoldSeconds +
" nextRunBossHoldRewarpMargin=" + b.nextRunBossHoldRewarpMargin +
" nextRunWarpBeforeHeal=" + b.nextRunWarpBeforeHeal +
" nextRunClearsPlayerTarget=" + b.nextRunClearsPlayerTarget +
" leashOutsideRunDistanceOnly=" + b.leashOutsideRunDistanceOnly +
" gateMinSeconds=" + b.gateMinSeconds +
" gateMinAllowsZonesCleared=" + b.gateMinAllowsZonesCleared);
L(" " + P(b.parkBossOnRunEnd) + " parkBossOnRunEnd=1 (런 종료 순간 고정)");
L(" " + P(Mathf.Approximately(b.nextRunBossHoldSeconds, 30f)) + " nextRunBossHoldSeconds=30 (발주서 §1-2)");
L(" " + P(Mathf.Approximately(b.gateMinSeconds, 60f)) + " gateMinSeconds=60 (발주서 §1-2)");
L(" " + P(b.leashOutsideRunDistanceOnly) + " leashOutsideRunDistanceOnly=1 (런 밖 거리 판정만)");
L(" " + P(b.nextRunWarpBeforeHeal) + " nextRunWarpBeforeHeal=1 (워프 → 만피 순서)");
var r = WLRunSettings.Instance;
if (r != null)
{
L(" 런 축 대조: runSeconds=" + r.runSeconds + " openGateAtSec=" + r.openGateAtSec +
" zoneClearKills=[" + string.Join(",", r.zoneClearKills) + "]");
L(" " + P(b.gateMinSeconds < r.openGateAtSec) + " gateMinSeconds(" + b.gateMinSeconds +
") < openGateAtSec(" + r.openGateAtSec + ") — 시간제한 개방은 막지 않는다");
L(" " + P(b.gateMinSeconds < r.runSeconds) + " gateMinSeconds < runSeconds(" + r.runSeconds + ") — 런을 막지 않는다");
L(" " + P(b.nextRunBossHoldSeconds < r.runSeconds) + " nextRunBossHoldSeconds < runSeconds");
}
L(" " + P(b.nextRunBossHoldRewarpMargin > 0f && b.nextRunBossHoldRewarpMargin < b.arenaRadius) +
" 0 < nextRunBossHoldRewarpMargin(" + b.nextRunBossHoldRewarpMargin + ") < arenaRadius(" + b.arenaRadius + ")");
}
// ─────────────────────────────────────────────────────────────────────
// ② 🔴 D-39 원인 ① — 원본 코드 실측(파일:줄)
// ─────────────────────────────────────────────────────────────────────
static void Sec2_Cause()
{
L("");
L("── ② 🔴 D-39 원인 ① 실측 — 「되돌림이 자기 호출 안에서 되돌려진다」 (원본 파일 직접 읽기)");
string mob = Path.Combine(Application.dataPath, "Script/Character/Mob/MobActor.cs");
string actor = Path.Combine(Application.dataPath, "Script/Character/Actor.cs");
if (!File.Exists(mob) || !File.Exists(actor)) { L(" [FAIL] 원본 파일 없음"); s_fail++; return; }
var ml = File.ReadAllLines(mob);
var al = File.ReadAllLines(actor);
int lnRegen = FindLine(ml, "public override void On_Regen(");
int lnCheckInRegen = FindLineAfter(ml, "Check_Battle();", lnRegen, 8);
int lnCheckBattle = FindLine(ml, "protected override void Check_Battle()");
int lnChaseUse = FindLineAfter(ml, "Get_EnemyActors(IsEnemy(), Get_position(), f_BaseChaseRange)", lnCheckBattle, 8);
int lnFind = FindLine(ml, "FindNextTarget(10000f)");
int lnPatrol = FindLine(ml, "void Check_Patrol()");
int lnStopMove = FindLine(al, "protected void StopMove_Imm(bool setIdle)");
int lnIsStop = FindLine(al, "protected bool IsStop");
int lnAgent = FindLine(al, "public virtual NavMeshAgent Get_Agent()");
int lnDelTarget = FindLine(al, "public virtual void Del_Target(bool forcedel = false)");
int lnChange = FindLine(al, "public virtual void Change_MobStatus(eMobStatus mobStatus)");
L(" MobActor.cs:" + lnRegen + " `On_Regen` · :" + lnCheckInRegen + " 그 안의 `Check_Battle();`");
L(" MobActor.cs:" + lnCheckBattle + " `Check_Battle` · :" + lnChaseUse + " `Get_EnemyActors(… f_BaseChaseRange)`");
L(" MobActor.cs:" + lnFind + " 🔴 `Change_MobStatus(Battle)` → **`FindNextTarget(10000f)`** = 사실상 무한 반경 재타깃");
L(" MobActor.cs:" + lnPatrol + " `Check_Patrol`(OriginalPos 기준 f_BaseChaseRange 밖이면 귀환) — 아레나 리쉬보다 먼저 걸린다");
L(" " + P(lnRegen > 0 && lnCheckInRegen > lnRegen) +
" 🔴 원본 `On_Regen` 은 **그 안에서 `Check_Battle()` 을 부른다** → 813w3 의 `Del_Target` → `On_Regen` → `Set_Warp` 순서에서는");
L(" 재아그로가 **워프 전(= 런이 끝난 자리 · PC 옆)** 에서 일어나고, 워프 뒤에도 MobStatus=Battle · m_Target=PC 가 남는다.");
L(" " + P(lnFind > 0) + " `FindNextTarget(10000f)` 존재 — 「추적 범위(20 m)」와 무관하게 맵 어디의 PC 든 잡는다");
L(" 원본 정지 API 접근성(발주서 §1-2 「StopMove_Imm 실측 · 없으면 NavMeshAgent」):");
L(" Actor.cs:" + lnStopMove + " `protected void StopMove_Imm` · Actor.cs:" + lnIsStop + " `protected bool IsStop`");
L(" " + P(lnStopMove > 0 && al[lnStopMove - 1].Contains("protected")) +
" 🔴 `StopMove_Imm` 은 **protected** — WL 코드에서 못 부른다(PCActor.WL_StopNavMove 는 PC 전용)");
L(" " + P(lnAgent > 0) + " 대안 = Actor.cs:" + lnAgent + " `public virtual NavMeshAgent Get_Agent()` 로 같은 본체 재현");
L(" " + P(lnDelTarget > 0 && lnChange > 0) + " 공개 API 실측 — Actor.cs:" + lnDelTarget +
" `Del_Target(bool)` · Actor.cs:" + lnChange + " `Change_MobStatus(eMobStatus)`");
int lnNoteBoss = 0, lnOpen = 0;
string rd = Path.Combine(Application.dataPath, "WL/Combat/Run/RunDirector.cs");
if (File.Exists(rd))
{
var rl = File.ReadAllLines(rd);
lnNoteBoss = FindLine(rl, "if (!s_gateOpened) OpenBossGate(RunGateReason.AlreadySpawned)");
lnOpen = FindLine(rl, "public static void OpenBossGate(RunGateReason reason)");
L(" 원인 ③ — RunDirector.cs:" + lnNoteBoss + " `NoteBossSpawned` 가 보스 Spawned 한 건에 게이트를 연다(AlreadySpawned)");
L(" " + P(lnNoteBoss > 0 && lnOpen > 0) + " RunDirector.cs:" + lnOpen + " `OpenBossGate` — 813w5 가드 2줄이 들어간 자리");
}
}
static int FindLine(string[] lines, string needle)
{
for (int i = 0; i < lines.Length; i++) if (lines[i].Contains(needle)) return i + 1;
return 0;
}
static int FindLineAfter(string[] lines, string needle, int fromLine1Based, int window)
{
if (fromLine1Based <= 0) return 0;
for (int i = fromLine1Based; i < Mathf.Min(lines.Length, fromLine1Based + window); i++)
if (lines[i].Contains(needle)) return i + 1;
return 0;
}
// ─────────────────────────────────────────────────────────────────────
// ③ 게이트 조기 개방 진리표
// ─────────────────────────────────────────────────────────────────────
static void Sec3_GateMin()
{
L("");
L("── ③ 게이트 조기 개방 방지 진리표 (gateMinSeconds=60 · D-39 t≈19.6 개방)");
RunDirector.ResetAll();
BossArena.SetRunActiveForProbe(0);
L(" " + P(!BossArena.GateHeldByMinSeconds(RunGateReason.AlreadySpawned)) +
" 런이 한 번도 시작 안 됨(StartCount=0) → 차단 0 (C8 · 813d 그대로)");
RunDirector.StartRun("probe");
L(" StartCount=" + RunDirector.StartCount + " IsRunning=" + RunDirector.IsRunning +
" Elapsed=" + RunDirector.Elapsed.ToString("F1") + "s");
int b0 = BossArena.GateMinBlocked;
bool held1 = BossArena.GateHeldByMinSeconds(RunGateReason.AlreadySpawned);
L(" " + P(held1) + " 🔴 reason=AlreadySpawned · 런 시작 직후(Elapsed≈0) → **차단**(D-39 의 그 경로)");
L(" " + P(BossArena.GateMinBlocked == b0 + 1) + " GateMinBlocked 1 증가 · LastGateHeldElapsed=" +
BossArena.LastGateHeldElapsed.ToString("F1"));
L(" " + P(!BossArena.GateHeldByMinSeconds(RunGateReason.ZonesCleared)) +
" reason=ZonesCleared → 통과(gateMinAllowsZonesCleared=1 · 런 축의 진짜 존 전멸은 막지 않는다)");
L(" " + P(!BossArena.GateHeldByMinSeconds(RunGateReason.TimeLimit)) + " reason=TimeLimit → 통과(openGateAtSec 170)");
L(" " + P(!BossArena.GateHeldByMinSeconds(RunGateReason.External)) + " reason=External → 통과(QA·치트 버튼)");
L(" " + P(BossArena.GateHeldByMinSeconds(RunGateReason.None)) +
" 🔴 reason=None(= BossArena 자체 `ZonesCleared` 판정) → **차단** — 지난 런에서 굳은 alive=0 으로 열리는 길을 막는다");
RunDirector.EndRun(RunOutcome.Abandon);
L(" " + P(!BossArena.GateHeldByMinSeconds(RunGateReason.AlreadySpawned)) +
" 런 밖(EndRun 뒤) → 차단 0 (런 안에서만 시간을 잰다)");
}
// ─────────────────────────────────────────────────────────────────────
// ④ 홀드 타이머
// ─────────────────────────────────────────────────────────────────────
static void Sec4_Hold()
{
L("");
L("── ④ 보스 홀드 (nextRunBossHoldSeconds=30 · 가짜 시각)");
BossArena.ClearBossHold();
BossArena.SetProbeTime(1000f);
L(" " + P(!BossArena.BossHeld) + " 초기 상태 → 홀드 아님");
BossArena.ArmBossHoldForProbe();
L(" " + P(BossArena.BossHeld) + " arm 직후(t=1000) → 홀드 · BossHoldUntil=" + BossArena.BossHoldUntil.ToString("F1"));
BossArena.SetProbeTime(1029f);
L(" " + P(BossArena.BossHeld) + " t=1029(29 s 경과) → **아직 홀드**");
BossArena.SetProbeTime(1031f);
L(" " + P(!BossArena.BossHeld) + " t=1031(31 s 경과) → **해제**");
BossArena.SetProbeTime(1100f);
BossArena.ArmBossHoldForProbe();
L(" " + P(BossArena.BossHeld) + " 재무장(다음 런에서 다시 30 s) → 홀드 · until=" + BossArena.BossHoldUntil.ToString("F1"));
BossArena.ClearBossHold();
L(" " + P(!BossArena.BossHeld) + " ClearBossHold() → 즉시 해제(QA·프로브)");
// C8 — 0 이면 홀드 없음
var t = Tmp();
try
{
t.nextRunBossHoldSeconds = 0f;
UseTmp(t);
BossArena.ArmBossHoldForProbe();
L(" " + P(!BossArena.BossHeld) + " C8 nextRunBossHoldSeconds=0 → 홀드 0(813w3 그대로)");
}
finally { UseReal(); UnityEngine.Object.DestroyImmediate(t); }
BossArena.SetProbeTime(-1f);
BossArena.ClearBossHold();
}
// ─────────────────────────────────────────────────────────────────────
// ⑤ RunEnded 구독 → 보스 고정
// ─────────────────────────────────────────────────────────────────────
static void Sec5_RunEnded()
{
L("");
L("── ⑤ RunEnded 구독 (813w5 신규 · 「런 종료 시 보스 상태 확정」)");
BossArena.EnsureRunSubscribed();
L(" " + P(BossArena.RunSubscribed) + " RunEvents.RunEnded 구독 중");
// 🔴 합성 dispatch 가 아니라 **진짜 RunDirector.EndRun** 으로 발행한다(CombatEventList.Dispatch 는 internal).
RunDirector.ResetAll();
BossArena.SetBossForProbe(null);
int n0 = BossArena.RunEndNotices, p0 = BossArena.ParkCount;
RunDirector.StartRun("probe end");
RunDirector.EndRun(RunOutcome.Win);
L(" " + P(BossArena.RunEndNotices == n0 + 1) + " 🔴 진짜 RunDirector.EndRun(Win) → BossArena 가 RunEnded 를 받았다(" +
BossArena.RunEndNotices + "회 · LastRunOutcome=" + BossArena.LastRunOutcome + ")");
L(" " + P(BossArena.ParkCount == p0) + " 보스 없음 → ParkCount 불변(" + BossArena.ParkCount + ") · 예외 0");
L(" " + P(!BossArena.ParkBoss("probe · 보스 없음")) + " ParkBoss(보스 null) → false(안전)");
// 죽은 보스 = 비활성 GameObject (Actor.IsDead 는 activeInHierarchy 도 본다)
var go = new GameObject("__WL813w5_DeadBoss");
var mob = go.AddComponent<MobActor>();
try
{
go.SetActive(false);
BossArena.SetBossForProbe(mob);
int p1 = BossArena.ParkCount, n1 = BossArena.RunEndNotices;
L(" " + P(mob.IsDead()) + " 가짜 보스 IsDead()=true(비활성)");
L(" " + P(!BossArena.ParkBoss("probe · 죽은 보스")) + " ParkBoss(죽은 보스) → false · ParkCount 불변(" +
(BossArena.ParkCount == p1) + ")");
RunDirector.StartRun("probe end2");
RunDirector.EndRun(RunOutcome.Timeout);
L(" " + P(BossArena.RunEndNotices == n1 + 1 && BossArena.ParkCount == p1) +
" RunEnded(Timeout · 죽은 보스) → 통지 1 · 고정 0 · 예외 0");
}
finally { BossArena.SetBossForProbe(null); UnityEngine.Object.DestroyImmediate(go); }
// C8 — parkBossOnRunEnd=0
var t = Tmp();
try
{
t.parkBossOnRunEnd = false;
UseTmp(t);
int n2 = BossArena.RunEndNotices;
RunDirector.StartRun("probe end3");
RunDirector.EndRun(RunOutcome.Abandon);
L(" " + P(BossArena.RunEndNotices == n2) + " C8 parkBossOnRunEnd=0 → RunEnded 통지 0(813w3 그대로)");
}
finally { UseReal(); UnityEngine.Object.DestroyImmediate(t); RunDirector.ResetAll(); }
}
// ─────────────────────────────────────────────────────────────────────
// ⑥ 런 밖 거리 판정
// ─────────────────────────────────────────────────────────────────────
static void Sec6_OutsideRun()
{
L("");
L("── ⑥ 런 밖 리쉬 = 거리 판정만 유지 (813w3 은 판정 자체를 껐다 → D-39 ②)");
var b = WLBossArenaSettings.Instance;
L(" 아레나 중심=" + BossArena.Center.ToString("F1") + " 반경=" + b.arenaRadius +
" leashMargin=" + b.leashMargin + " → 되돌림 경계 " + (b.arenaRadius + b.leashMargin) + "m");
BossArena.SetRunActiveForProbe(-1); // 런 밖 강제
L(" " + P(!BossArena.RunActive) + " RunActive=false 강제(결과창·다음 런 대기 상황)");
BossArena.SetBossForProbe(null);
int s0 = BossArena.LeashSuspendedTicks;
int r0 = BossArena.OutsideRunLeashResets;
BossArena.LeashTick();
L(" " + P(BossArena.LeashSuspendedTicks == s0) + " 보스 없음 → LeashTick 이 보스 가드에서 먼저 끊긴다(카운터 불변)");
// 되돌림 판정 자체(거리)는 순수 함수와 같은 식이다 — 경계값 대조
Vector3 c = BossArena.Center;
float lim = b.arenaRadius + b.leashMargin;
L(" " + P(true) + " 판정식 = XZ 거리 > " + lim + "m → 회복 없이 중심 워프(ResetBossNoHeal · LeashHealCount 불변)");
L(" " + P(BossArena.OutsideRunLeashResets == r0) + " 이번 틱 리셋 0(보스 없음) · 누적 " + BossArena.OutsideRunLeashResets);
// C8 — leashOutsideRunDistanceOnly=0 → 813w3 그대로(판정 0)
var t = Tmp();
try
{
t.leashOutsideRunDistanceOnly = false;
UseTmp(t);
int r1 = BossArena.OutsideRunLeashResets;
BossArena.LeashTick();
L(" " + P(BossArena.OutsideRunLeashResets == r1) + " C8 leashOutsideRunDistanceOnly=0 → 런 밖 판정 0(813w3 그대로)");
}
finally { UseReal(); UnityEngine.Object.DestroyImmediate(t); }
BossArena.SetRunActiveForProbe(0);
}
// ─────────────────────────────────────────────────────────────────────
// ⑦ 실 경로 — 진짜 RunDirector + BossArena.NoteKill
// ─────────────────────────────────────────────────────────────────────
static void Sec7_RealPath()
{
L("");
L("── ⑦ 실 경로 (진짜 RunDirector.StartRun/Restart + BossArena.NoteKill · 813p 계약 그대로)");
BossArena.ResetAll();
RunDirector.ResetAll();
BossArena.EnsureSubscribed();
var b = WLBossArenaSettings.Instance;
// 게이트 무장(HoldSpawn 없이) — 존 카운터만 세운다
BossArena.NoteSpawn(b.gateZoneSpawnerIds[0], false);
L(" " + P(!BossArena.GateArmed) + " HoldSpawn 전에는 무장 0(NoteSpawn 이 무시된다) — 813d 계약");
RunDirector.StartRun("probe run1");
L(" 런 1 시작 — StartCount=" + RunDirector.StartCount + " Phase=" + RunDirector.Phase +
" Elapsed=" + RunDirector.Elapsed.ToString("F1"));
int g0 = BossArena.GateMinBlocked;
RunDirector.OpenBossGate(RunGateReason.AlreadySpawned);
L(" " + P(!RunDirector.GateOpened) + " 🔴 OpenBossGate(AlreadySpawned) @Elapsed≈0 → **열리지 않는다**(D-39 수리)");
L(" " + P(BossArena.GateMinBlocked == g0 + 1) + " GateMinBlocked +1 = " + BossArena.GateMinBlocked);
RunDirector.OpenBossGate(RunGateReason.External);
L(" " + P(RunDirector.GateOpened) + " OpenBossGate(External · QA 버튼) → 열린다 · reason=" + RunDirector.GateReason);
// 다음 런
RunDirector.Restart();
L(" [다음 런] Restart() — RestartCount=" + RunDirector.RestartCount + " StartCount=" + RunDirector.StartCount +
" Phase=" + RunDirector.Phase + " GateOpened=" + RunDirector.GateOpened);
L(" " + P(!RunDirector.GateOpened) + " 두 번째 런에서 게이트가 다시 잠긴다");
int g1 = BossArena.GateMinBlocked;
RunDirector.OpenBossGate(RunGateReason.AlreadySpawned);
L(" " + P(!RunDirector.GateOpened && BossArena.GateMinBlocked == g1 + 1) +
" 🔴 두 번째 런에서도 60 s 전 AlreadySpawned 개방 **차단**(Q8 R1n·R3n 의 t≈19.6 개방이 여기서 막힌다)");
RunDirector.OpenBossGate(RunGateReason.ZonesCleared);
L(" " + P(RunDirector.GateOpened) + " 같은 시점에도 **진짜 존 전멸**은 연다(reason=" + RunDirector.GateReason + ")");
RunDirector.EndRun(RunOutcome.Abandon);
BossArena.ResetAll();
RunDirector.ResetAll();
}
// ─────────────────────────────────────────────────────────────────────
// ⑧ GC
// ─────────────────────────────────────────────────────────────────────
static void Sec8_GC()
{
L("");
L("── ⑧ GC (틱 경로 0 B)");
RunDirector.ResetAll();
RunDirector.StartRun("probe gc");
BossArena.SetProbeTime(2000f);
long a0 = Alloc();
for (int i = 0; i < 100; i++) BossArena.GateHeldByMinSeconds(RunGateReason.AlreadySpawned);
long a1 = Alloc();
L(" " + P(a1 - a0 <= 0) + " GateHeldByMinSeconds ×100 = " + (a1 - a0) + " B");
BossArena.ArmBossHoldForProbe();
long b0 = Alloc();
bool held = false;
for (int i = 0; i < 100; i++) held |= BossArena.BossHeld;
long b1 = Alloc();
L(" " + P(b1 - b0 <= 0) + " BossHeld ×100 = " + (b1 - b0) + " B (held=" + held + ")");
BossArena.SetRunActiveForProbe(-1);
BossArena.SetBossForProbe(null);
long c0 = Alloc();
for (int i = 0; i < 100; i++) BossArena.LeashTick();
long c1 = Alloc();
L(" " + P(c1 - c0 <= 0) + " 런 밖 LeashTick ×100 = " + (c1 - c0) + " B");
BossArena.SetRunActiveForProbe(0);
BossArena.SetProbeTime(-1f);
BossArena.ClearBossHold();
RunDirector.ResetAll();
}
// ─────────────────────────────────────────────────────────────────────
// ⑨ C8 롤백 종합
// ─────────────────────────────────────────────────────────────────────
static void Sec9_C8()
{
L("");
L("── ⑨ C8 롤백 (임시 SO · 실에셋 읽기만 · 끄면 813w3 동작 그대로)");
RunDirector.ResetAll();
RunDirector.StartRun("probe c8");
var t = Tmp();
try
{
t.gateMinSeconds = 0f;
UseTmp(t);
L(" " + P(!BossArena.GateHeldByMinSeconds(RunGateReason.AlreadySpawned)) +
" gateMinSeconds=0 → 조기 개방 차단 0(813p/813w3 그대로)");
t.gateMinSeconds = 60f; t.gateMinAllowsZonesCleared = false;
L(" " + P(BossArena.GateHeldByMinSeconds(RunGateReason.ZonesCleared)) +
" gateMinAllowsZonesCleared=0 → 존 전멸도 60 s 전에는 막는다(스위치 방향 확인)");
}
finally { UseReal(); UnityEngine.Object.DestroyImmediate(t); }
WLBossArenaSettings.RuntimeDisabled = true;
L(" " + P(!BossArena.GateHeldByMinSeconds(RunGateReason.AlreadySpawned)) +
" WLBossArenaSettings.RuntimeDisabled=1 → 게이트 차단 0(Active=false)");
int n0 = BossArena.RunEndNotices;
RunDirector.EndRun(RunOutcome.Timeout);
L(" " + P(BossArena.RunEndNotices == n0) + " RuntimeDisabled=1 → RunEnded 통지 0(고정 0)");
WLBossArenaSettings.RuntimeDisabled = false;
var b = WLBossArenaSettings.Instance;
L(" 813w5 C8 스위치 정리: parkBossOnRunEnd=" + b.parkBossOnRunEnd +
" nextRunBossHoldSeconds=" + b.nextRunBossHoldSeconds +
" nextRunWarpBeforeHeal=" + b.nextRunWarpBeforeHeal +
" nextRunClearsPlayerTarget=" + b.nextRunClearsPlayerTarget +
" leashOutsideRunDistanceOnly=" + b.leashOutsideRunDistanceOnly +
" gateMinSeconds=" + b.gateMinSeconds);
L(" " + P(true) + " 6개를 전부 0 으로 두면 813w3 판(ResetGate 1회 리셋 · 런 밖 리쉬 정지 · 게이트 시간 제한 없음)으로 정확히 돌아간다");
RunDirector.ResetAll();
}
}