// WL813w2_Probe.cs — #813w2(런 가이드 2차: 도착 판정 D-24 · 보스 홀드 D-22 · 군중 유지 D-19 · 리쉬 축소) 프로브 // 에디트 모드 전용 · Play 불필요 · 로그인 0 · 씬/에셋 무수정(임시 GameObject·임시 SO 는 즉시 파괴/회수) // // 상주 에디터: // unity command run_script --project-path --file AgentScripts/WL813w2_Probe.cs --entry WL813w2_Probe.RunAll --timeout 300 // 결과: Console + AgentScripts/WL813w2_PROBE.txt (RESULT PASS / RESULT FAIL n) // // 무엇을 증명하나 (발주서 WL-813w2 §1-1~§1-5 · 근거 = qa/W2-5_재채점_v3.md §4) // ① 에셋·SOT — 813w2 신규 9필드 · 값 · 리쉬 0.05 · bossHoldRadius < 리쉬 발동선 // ② D-24 도착 — 🔴 **피격 중에도 도착 판정이 돈다**(813w 은 여기서 통째로 return 했다) · // XZ 평면 판정(y 차이 무시) · 정지 판정 도착 · maxTravelSeconds 초과 → Hold // ③ D-22 보스홀드 — 🔴 **피격 중에도 이탈 되돌림이 즉시** · 되돌림 중 타깃 해제 · // 보스 재타깃은 Hold 에서만(락은 이동 중에도 유지) // ④ 잡몹 타깃 금지 — 보스 생존 중 Hold 에서 잡몹 타깃 → 다음 폴링에 보스로 복귀 // ⑤ D-19 군중 유지 — 존 Hold 에서 zoneHoldRadius 밖 = 즉시 되돌림 · 반경 안 타깃은 유지(교전 계속) // ⑥ 리쉬 0 시뮬 — 보스 구간 240 s 를 0.1 s 스텝으로 돌려 BossArena.EvaluateLeash 리셋 **0** 확인 // ⑦ 리쉬 회복 — EvaluateLeashHeal 판정(부분 → 쿨다운 → 쿨다운 뒤 재회복) · 0.05 // ⑧ GC 0 — ForceTick ×100 이 0 B(대조군으로 측정기 동작 확인) // ⑨ C8 롤백 — 813w2 스위치 4개를 끄면 813w 동작(피격 중 도착·되돌림 0) 그대로 · RuntimeDisabled → 무동작 // // 에디트 모드 한계(보고서 「미확인」) // · NavMesh/NavMeshAgent 가 없어 Steer() 는 에이전트 검사에서 되돌아간다 → 실이동은 못 잰다. // 상태 전이·판정은 PC transform 을 직접 옮겨 검증한다(원본 Get_position() 이 transform 이다). // · Time.unscaledTime 이 스크립트 실행 중에는 멈춰 있다 → 시간 의존 판정은 순수 함수 // (RunGuide.StepStillDetect · BossArena.EvaluateLeash/EvaluateLeashHeal)에 가짜 시각을 먹여 검증한다. // 그래서 「피격 중」 상태는 프로브 내내 유지된다 = ②③ 이 노린 조건 그대로다. using System; using System.IO; using System.Text; using UnityEngine; using WL.Combat.Boss; using WL.Combat.Run; public static class WL813w2_Probe { const string OutCommit = "AgentScripts/WL813w2_PROBE.txt"; static StringBuilder sb; static int s_fail; static GameObject s_pcGo, s_bossGo, s_mobGo; static PCActor s_pc; static Actor s_boss, s_mob; static PCActor s_prevMyPc; public static object RunAll() { sb = new StringBuilder(); s_fail = 0; sb.AppendLine("# WL813w2 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_Arrive(); Sec3_BossHold(); Sec4_NoMobTarget(); Sec5_ZoneHold(); Sec6_LeashZeroSim(); Sec7_LeashHeal(); 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); } static string V(Vector3 v) { return "(" + v.x.ToString("F1") + "," + v.y.ToString("F1") + "," + v.z.ToString("F1") + ")"; } static float DXZ(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("__WL813w2_PC"); s_pc = s_pcGo.AddComponent(); s_pc.m_Role = eRole.PC; s_pc.m_SubRole = eSubRol.None; MyValue.MyPC = s_pc; s_bossGo = new GameObject("__WL813w2_Boss"); s_boss = s_bossGo.AddComponent(); s_boss.m_Role = eRole.Mob; s_boss.m_SubRole = eSubRol.Boss; s_mobGo = new GameObject("__WL813w2_Mob"); s_mob = s_mobGo.AddComponent(); s_mob.m_Role = eRole.Mob; s_mob.m_SubRole = eSubRol.None; RunGuide.ResetDiagnostics(); WLRunGuideSettings.RuntimeDisabled = false; L(""); L("── ⓪ 준비"); L(" " + P(s_pc.IsMainPC()) + " 가짜 메인 PC IsMainPC()=true · MyValue.MyPC 등록"); L(" " + P(RunGuide.Active) + " RunGuide.Active(에셋 · enabled · 런 축)"); L(" Agent=" + (s_pc.Get_Agent() == null ? "없음(에디트 모드 · Steer 는 여기서 되돌아간다)" : "있음")); } static void Teardown() { try { RunGuide.StopGuide("probe teardown"); RunGuide.ResetDiagnostics(); RunGuide.SetSteeringForProbe(false); WLRunGuideSettings.RuntimeDisabled = false; MyValue.MyPC = s_prevMyPc; Kill(s_pcGo, s_bossGo, s_mobGo); } catch { } } static void Kill(params GameObject[] gos) { for (int i = 0; i < gos.Length; i++) if (gos[i] != null) UnityEngine.Object.DestroyImmediate(gos[i]); } static void MovePc(Vector3 p) { s_pcGo.transform.position = p; } // ───────────────────────────────────────── ① 에셋 · SOT static void Sec1_Asset() { L(""); L("── ① 에셋 · SOT (813w2 신규 9필드 · C45)"); var c = WLRunGuideSettings.Instance; L(" " + P(c != null) + " Resources.Load(\"" + WLRunGuideSettings.ResourcesPath + "\")"); if (c == null) return; L(" [813w 값] zoneArriveRadius=" + c.zoneArriveRadius + " zoneHoldRadius=" + c.zoneHoldRadius + " guideLeashRadius=" + c.guideLeashRadius + " bossArriveRadius=" + c.bossArriveRadius + " bossHoldRadius=" + c.bossHoldRadius + " pollSeconds=" + c.pollSeconds + " resumeAfterHitSeconds=" + c.resumeAfterHitSeconds + " maxTravelSeconds=" + c.maxTravelSeconds); L(" [813w2 신규] hitSuspendOnlyInHold=" + c.hitSuspendOnlyInHold + " bossHitSuspendSeconds=" + c.bossHitSuspendSeconds + " arriveStillEnabled=" + c.arriveStillEnabled + " arriveStillSeconds=" + c.arriveStillSeconds + " arriveStillSpeed=" + c.arriveStillSpeed + " arriveStillMaxDistance=" + c.arriveStillMaxDistance + " pullbackReleasesTarget=" + c.pullbackReleasesTarget + " bossRetargetOnlyInHold=" + c.bossRetargetOnlyInHold + " runTickLogSeconds=" + c.runTickLogSeconds); L(" " + P(c.hitSuspendOnlyInHold) + " hitSuspendOnlyInHold=1 — 피격 중단이 도착·되돌림을 막지 않는다(D-24·D-22 원인 차단)"); L(" " + P(c.arriveStillEnabled && c.arriveStillSeconds > 0f) + " 정지 판정 도착 on(" + c.arriveStillSeconds + "s · " + c.arriveStillSpeed + "m/s)"); L(" " + P(c.pullbackReleasesTarget && c.bossRetargetOnlyInHold) + " 되돌림 중 타깃 해제 + 보스 재타깃은 Hold 에서만"); L(" " + P(c.bossHitSuspendSeconds < c.resumeAfterHitSeconds) + " 보스 피격 중단(" + c.bossHitSuspendSeconds + "s) < 존 피격 중단(" + c.resumeAfterHitSeconds + "s) — Q5 D-22 처방"); L(" " + P(c.guideLeashRadius < c.zoneHoldRadius) + " guideLeashRadius(" + c.guideLeashRadius + ") < zoneHoldRadius(" + c.zoneHoldRadius + ") — 존 안의 적은 버리지 않는다(D-19)"); var b = WLBossArenaSettings.Instance; L(" " + P(b != null) + " WLBossArenaSettings 로드"); if (b == null) return; L(" leashHealPercent=" + b.leashHealPercent + " leashHealCooldownSeconds=" + b.leashHealCooldownSeconds + " arenaRadius=" + b.arenaRadius + " playerLeaveMargin=" + b.playerLeaveMargin + " playerLeaveGraceSeconds=" + b.playerLeaveGraceSeconds); L(" " + P(Mathf.Approximately(b.leashHealPercent, 0.05f)) + " 리쉬 회복 0.15 → **0.05**(§1-4)"); L(" " + P(Mathf.Approximately(b.leashHealCooldownSeconds, 10f)) + " 쿨다운 10 s 유지(§1-4)"); float leashLine = b.arenaRadius + b.playerLeaveMargin; L(" " + P(c.bossHoldRadius < leashLine) + " bossHoldRadius(" + c.bossHoldRadius + ") < 리쉬 발동선(" + leashLine + ")"); } // ───────────────────────────────────────── ② D-24 도착 판정 static void Sec2_Arrive() { L(""); L("── ② D-24 도착 판정 (Q5 §4-2 「존 Arrived=0 · State=Travel 고착」)"); var c = WLRunGuideSettings.Instance; if (c == null) return; Vector3 anchor; if (!RunGuide.TryGetAnchor(c, 1, out anchor)) { L(" [FAIL] 존1 앵커 없음"); s_fail++; return; } // ── ②-1 피격 중에도 도착 판정이 돈다 (813w 은 여기서 return 했다) RunGuide.ResetDiagnostics(); MovePc(anchor + new Vector3(40f, 0f, 0f)); RunGuide.BeginZone(1, "probe"); L(" 목적지=" + V(RunGuide.Destination) + " · 시작 상태=" + RunGuide.State); RunGuide.SimulateHit(); // 에디트 모드에선 Time 이 멈춰 있어 「피격 중」이 계속된다 RunGuide.ForceTick(); bool hitStillTravel = RunGuide.State == RunGuideState.Travel; L(" " + P(hitStillTravel) + " 피격 중 · 40 m — Travel 유지(도착 아님)"); // 앵커 위로 옮긴다(y 는 크게 다르게 둔다 = XZ 판정 확인) MovePc(new Vector3(anchor.x + 1.0f, anchor.y + 25f, anchor.z)); RunGuide.ForceTick(); L(" " + P(RunGuide.State == RunGuideState.Hold) + " 🔴 **피격 중에도 도착 → Hold**(813w = Travel 고착 · D-24 원인)"); L(" " + P(RunGuide.Arrived == 1) + " Arrived=1 (실측 " + RunGuide.Arrived + ") · y 차이 25 m 는 무시(XZ 평면 판정)"); L(" LastLog=" + RunGuide.LastLog); // ── ②-2 정지 판정 도착 (순수 함수 · 가짜 시각) float still = -1f; bool a0 = RunGuide.StepStillDetect(c, 0.01f, 0.1f, 10.0f, ref still); // 정지 시작 bool a1 = RunGuide.StepStillDetect(c, 0.01f, 0.1f, 10.3f, ref still); // 0.3 s — 아직 bool a2 = RunGuide.StepStillDetect(c, 0.01f, 0.1f, 10.6f, ref still); // 0.6 s — 도착 L(" " + P(!a0 && !a1 && a2) + " 정지 판정: 0.0s=" + a0 + " 0.3s=" + a1 + " 0.6s=" + a2 + " (arriveStillSeconds=" + c.arriveStillSeconds + ")"); bool a3 = RunGuide.StepStillDetect(c, 2.0f, 0.1f, 10.7f, ref still); // 20 m/s = 이동 중 L(" " + P(!a3 && still < 0f) + " 다시 움직이면 정지 타이머 리셋(still=" + still.ToString("F1") + ")"); L(" " + P(RunGuide.WithinStillRange(c, 12f, 18f) && !RunGuide.WithinStillRange(c, 40f, 18f)) + " 정지 도착은 목적지 반경 안에서만 인정(12 m ok · 40 m 거부)"); // ── ②-3 maxTravelSeconds 초과 → Hold(이탈 감시 유지) RunGuide.ResetDiagnostics(); MovePc(anchor + new Vector3(40f, 0f, 0f)); RunGuide.BeginZone(1, "probe timeout"); RunGuide.SetTravelStartForProbe(Time.unscaledTime - (c.maxTravelSeconds + 1f)); RunGuide.ForceTick(); L(" " + P(RunGuide.State == RunGuideState.Hold && RunGuide.GaveUp == 1) + " maxTravelSeconds(" + c.maxTravelSeconds + "s) 초과 → Hold · GaveUp=" + RunGuide.GaveUp + "(이탈 감시는 유지)"); } // ───────────────────────────────────────── ③ D-22 보스 홀드 static void Sec3_BossHold() { L(""); L("── ③ D-22 보스 홀드 실효화 (Q5 실측 이탈 46.3 m · 되돌림 3~4회 vs 리셋 15~19회)"); var c = WLRunGuideSettings.Instance; if (c == null) return; RunGuide.ResetDiagnostics(); Vector3 center = BossArena.Center; s_bossGo.transform.position = center; RunGuide.SetBossForProbe(s_boss); MovePc(center); RunGuide.BeginBoss("probe"); RunGuide.ForceTick(); // 중심 = 도착(이 틱에서는 아직 Travel → 재타깃 없음) L(" 아레나 중심=" + V(center) + " · " + P(RunGuide.State == RunGuideState.Hold) + " 도착(Hold)"); L(" " + P(RunGuide.BossRetargets == 0) + " 이동(Travel) 중에는 보스 재타깃 0 — 우리 목적지를 덮어쓰지 않는다"); RunGuide.ForceTick(); // Hold 에서 첫 폴링 → 보스 타깃 bool lockedInHold = !DSUtil.CheckNull(s_pc.Get_Target()) && ReferenceEquals(s_pc.Get_Target(), s_boss); L(" " + P(lockedInHold) + " Hold — 타깃 = 보스(BossRetargets=" + RunGuide.BossRetargets + ")"); L(" " + P(s_boss.m_IsLockTarget) + " 원본 락타깃 m_IsLockTarget=true(BossLocks=" + RunGuide.BossLocks + ")"); // 🔴 피격 중에 아레나 밖으로 밀려난다 → 즉시 되돌림이 걸려야 한다 RunGuide.SimulateHit(); MovePc(center + new Vector3(c.bossHoldRadius + 2f, 0f, 0f)); // 16 m — 홀드(14) 밖 · 리쉬선(24) 안 RunGuide.ForceTick(); L(" " + P(RunGuide.State == RunGuideState.Travel) + " 🔴 **피격 중에도 이탈 " + (c.bossHoldRadius + 2f).ToString("F1") + "m 에서 즉시 되돌림**(813w = 되돌림 0 · D-22 원인)"); L(" " + P(RunGuide.HoldPullbacks == 1 && RunGuide.PullbacksWhileHit == 1) + " HoldPullbacks=" + RunGuide.HoldPullbacks + " · 그중 피격 중=" + RunGuide.PullbacksWhileHit); L(" " + P(!s_pc.isTarget) + " 되돌림 중 타깃 해제(보스 타깃도 놓는다 · pullbackReleasesTarget)"); L(" " + P(s_boss.m_IsLockTarget) + " 그래도 보스 락은 유지 → 잡몹이 타깃을 가져갈 수 없다"); L(" " + P(RunGuide.MaxDistFromDest >= c.bossHoldRadius) + " 최대 이탈 실측 " + RunGuide.MaxDistFromDest.ToString("F1") + "m (리쉬 발동선 24 m 미만)"); L(" LastLog=" + RunGuide.LastLog); // 되돌아오면 다시 Hold + 보스 타깃 복귀 MovePc(center + new Vector3(2f, 0f, 0f)); RunGuide.ForceTick(); L(" " + P(RunGuide.State == RunGuideState.Hold) + " 복귀 → Hold"); RunGuide.ForceTick(); L(" " + P(ReferenceEquals(s_pc.Get_Target(), s_boss)) + " Hold 복귀 즉시 타깃 = 보스로 복원"); } // ───────────────────────────────────────── ④ 보스 생존 중 잡몹 타깃 금지 static void Sec4_NoMobTarget() { L(""); L("── ④ 보스 생존 중 잡몹 타깃 금지 (§1-2)"); Vector3 center = BossArena.Center; s_mobGo.transform.position = center + new Vector3(3f, 0f, 0f); // Hold 상태에서 원본 경로가 잡몹으로 타깃을 바꿔치기한 상황을 만든다 s_pc.Change_Target(true, s_mob); bool stolen = ReferenceEquals(s_pc.Get_Target(), s_mob); int before = RunGuide.BossRetargets; RunGuide.ForceTick(); bool back = ReferenceEquals(s_pc.Get_Target(), s_boss); L(" " + P(stolen) + " 잡몹이 타깃을 가져간 상황을 만든다(" + s_mob.name + ")"); L(" " + P(back && RunGuide.BossRetargets == before + 1) + " 다음 폴링(0.1 s)에 보스로 복귀 · BossRetargets " + before + " → " + RunGuide.BossRetargets); // 보스가 죽으면 락·우선 타깃을 놓는다(원본 자동전투 복귀) s_boss.m_IsLockTarget = true; RunGuide.StopGuide("probe boss done"); L(" " + P(!s_boss.m_IsLockTarget) + " 가이드 종료 시 보스 락 해제(원본 타깃 선택 복귀)"); } // ───────────────────────────────────────── ⑤ D-19 군중 유지 (존 반경 이탈 0) static void Sec5_ZoneHold() { L(""); L("── ⑤ D-19 군중 유지 — 존 안에서는 존 반경 밖으로 나가지 않는다"); var c = WLRunGuideSettings.Instance; if (c == null) return; Vector3 anchor; if (!RunGuide.TryGetAnchor(c, 1, out anchor)) { L(" [FAIL] 앵커 없음"); s_fail++; return; } RunGuide.ResetDiagnostics(); MovePc(anchor); RunGuide.BeginZone(1, "probe zone hold"); RunGuide.ForceTick(); L(" " + P(RunGuide.State == RunGuideState.Hold) + " 앵커 위 = Hold(자동전투 재개)"); // 존 반경 안의 적은 유지한다(교전 계속 = 화면 안 적 수) RunGuide.ClearHitSuspendForProbe(); s_mobGo.transform.position = anchor + new Vector3(c.guideLeashRadius - 2f, 0f, 0f); s_pc.Change_Target(true, s_mob); RunGuide.ForceTick(); L(" " + P(ReferenceEquals(s_pc.Get_Target(), s_mob)) + " 반경 안(" + (c.guideLeashRadius - 2f).ToString("F0") + "m < guideLeashRadius " + c.guideLeashRadius + ") 타깃은 유지 — 원본이 계속 싸운다"); // 반경 밖 적은 무시한다(따라 나가지 않는다) s_mobGo.transform.position = anchor + new Vector3(c.guideLeashRadius + 6f, 0f, 0f); s_pc.Change_Target(true, s_mob); int fi = RunGuide.FarTargetIgnored; RunGuide.ForceTick(); L(" " + P(!s_pc.isTarget && RunGuide.FarTargetIgnored == fi + 1) + " 반경 밖 타깃은 무시(Del_Target · FarTargetIgnored " + fi + " → " + RunGuide.FarTargetIgnored + ")"); // 피격 중에는 먼 타깃 무시를 쉬어 준다 = 나를 때린 적에게 즉시 응전(반경 밖으로는 어차피 못 나간다) s_pc.Change_Target(true, s_mob); RunGuide.SimulateHit(); RunGuide.ForceTick(); L(" " + P(ReferenceEquals(s_pc.Get_Target(), s_mob)) + " 피격 중에는 먼 타깃도 유지 = 즉시 응전(이탈 되돌림은 그대로 돈다)"); // 존 반경 밖으로 밀려나면 피격 중이라도 즉시 되돌림 MovePc(anchor + new Vector3(c.zoneHoldRadius + 5f, 0f, 0f)); RunGuide.ForceTick(); L(" " + P(RunGuide.State == RunGuideState.Travel && RunGuide.HoldPullbacks == 1) + " 존 이탈 " + (c.zoneHoldRadius + 5f).ToString("F0") + "m — 피격 중에도 즉시 되돌림(HoldPullbacks=" + RunGuide.HoldPullbacks + ")"); L(" 체류 계측: 존체류=" + RunGuide.ZoneHoldSeconds.ToString("F2") + "s · 이동=" + RunGuide.TravelSeconds.ToString("F2") + "s · 최대이탈=" + RunGuide.MaxDistFromDest.ToString("F1") + "m (에디트 모드는 Time 이 멈춰 있어 초는 0 · 인게임에서 RunTick 로그로 남는다)"); } // ───────────────────────────────────────── ⑥ 리쉬 0 시뮬 (보스 구간 240 s) static void Sec6_LeashZeroSim() { L(""); L("── ⑥ 보스 구간 240 s 시뮬 — 리쉬 리셋 0 (§1-4)"); var c = WLRunGuideSettings.Instance; var b = WLBossArenaSettings.Instance; if (c == null || b == null) return; // 813w2 홀드 규칙만으로 PC 궤적을 만든다(NavMesh 없이 순수 계산): // Hold = 원본 자동전투가 잡몹 쪽으로 끌고 나간다 → 바깥으로 drift // Travel(되돌림) = 우리가 중심으로 민다 → 안쪽으로 pull // 실측 근거: Q5 는 「되돌림이 3~4회뿐」이라 drift 가 46 m 까지 갔다. 되돌림이 매 폴링 걸리면 // PC 는 bossHoldRadius 근처에서 톱니 모양으로 붙잡힌다 → 리쉬 발동선 24 m 에 닿지 않는다. const float step = 0.1f; // 폴링 주기 const float drift = 4.0f; // 잡몹 추격으로 바깥으로 끌리는 속도(m/s · 보수적으로 크게) const float pull = 5.0f; // 되돌림 이동 속도(m/s · PC 이동속도 하한) float d = 0f, maxD = 0f; bool traveling = false; int resets = 0, ticks = 0; BossArena.ResetLeashState(); Vector3 center = BossArena.Center; for (float t = 0f; t < 240f; t += step) { // 813w2 상태 기계와 같은 규칙 if (!traveling && d > c.bossHoldRadius) traveling = true; // 이탈 → 되돌림(피격 무관) else if (traveling && d <= c.bossArriveRadius) traveling = false; // 도착 → Hold d += (traveling ? -pull : drift) * step; if (d < 0f) d = 0f; if (d > maxD) maxD = d; // 0.25 s 마다 813d 리쉬 판정(보스는 중심 · PC 는 d 만큼 떨어져 있다) if (((int)Mathf.Round(t / step)) % 3 != 0) continue; ticks++; int v = BossArena.EvaluateLeash(center, center + new Vector3(d, 0f, 0f), true, center, t); if (v != BossArena.kLeashNone) resets++; } L(" 드리프트 " + drift + " m/s · 되돌림 " + pull + " m/s · 240 s · 판정 " + ticks + "회"); L(" " + P(maxD < b.arenaRadius + b.playerLeaveMargin) + " 최대 이탈 " + maxD.ToString("F1") + " m < 리쉬 발동선 " + (b.arenaRadius + b.playerLeaveMargin) + " m (Q5 실측 46.3 m)"); L(" " + P(resets == 0) + " 🔴 **리쉬 리셋 " + resets + "회**(Q5 실측 15~19회)"); // 대조군: 813w 동작(되돌림이 거의 안 걸린다) → 리셋이 실제로 난다 = 시뮬레이터가 살아 있다는 증거 BossArena.ResetLeashState(); int resets813w = 0; float d2 = 0f; for (float t = 0f; t < 240f; t += step) { d2 += drift * step; // 되돌림 없음 if (((int)Mathf.Round(t / step)) % 3 != 0) continue; if (BossArena.EvaluateLeash(center, center + new Vector3(d2, 0f, 0f), true, center, t) != BossArena.kLeashNone) { resets813w++; d2 = 0f; } // 리셋 = 보스 워프(PC 는 그대로 = 거리 0 로 근사) } L(" " + P(resets813w > 0) + " 대조군(되돌림 0) 리셋 " + resets813w + "회 — 판정기가 실제로 작동한다"); BossArena.ResetLeashState(); } // ───────────────────────────────────────── ⑦ 리쉬 회복 판정 static void Sec7_LeashHeal() { L(""); L("── ⑦ 리쉬 회복 축소 0.15 → 0.05 · 쿨다운 10 s 유지 (§1-4)"); var tmp = ScriptableObject.CreateInstance(); try { var real = WLBossArenaSettings.Instance; tmp.resetHealsBoss = true; tmp.leashHealPercent = real != null ? real.leashHealPercent : 0.05f; tmp.leashHealCooldownSeconds = real != null ? real.leashHealCooldownSeconds : 10f; BossArena.ResetLeashState(); int v1 = BossArena.EvaluateLeashHeal(tmp, 100f); int v2 = BossArena.EvaluateLeashHeal(tmp, 104f); // 쿨다운 안 int v3 = BossArena.EvaluateLeashHeal(tmp, 111f); // 쿨다운 뒤 L(" " + P(v1 == BossArena.kHealPartial) + " 1회차 = 부분 회복(+" + (tmp.leashHealPercent * 100f).ToString("F0") + "%)"); L(" " + P(v2 == BossArena.kHealCooldown) + " 4 s 뒤 = 쿨다운으로 건너뜀"); L(" " + P(v3 == BossArena.kHealPartial) + " 11 s 뒤 = 다시 회복"); // 240 s 동안 최악(매 10 s 리쉬)이라도 회복 총량은 24 × 5 % = 120 % → 0.15 였다면 360 % float worst = (240f / tmp.leashHealCooldownSeconds) * tmp.leashHealPercent; L(" 최악 회복 총량(240 s · 쿨다운마다) = " + (worst * 100f).ToString("F0") + "% (0.15 였다면 " + ((240f / tmp.leashHealCooldownSeconds) * 0.15f * 100f).ToString("F0") + "%)"); L(" " + P(worst < 1.5f) + " 리셋이 남더라도 회복 총량이 보스 HP 1.5배 미만"); tmp.leashHealPercent = 1f; BossArena.ResetLeashState(); L(" " + P(BossArena.EvaluateLeashHeal(tmp, 200f) == BossArena.kHealFull) + " C8: percent>=1 → 813d 만피 리셋 그대로"); } finally { UnityEngine.Object.DestroyImmediate(tmp); BossArena.ResetLeashState(); } } // ───────────────────────────────────────── ⑧ GC static void Sec8_GC() { L(""); L("── ⑧ GC 0 (정상 틱 경로)"); var c = WLRunGuideSettings.Instance; Vector3 anchor; if (c == null || !RunGuide.TryGetAnchor(c, 1, out anchor)) return; RunGuide.ResetDiagnostics(); MovePc(anchor); RunGuide.BeginZone(1, "probe gc"); for (int i = 0; i < 5; i++) RunGuide.ForceTick(); GC.Collect(); GC.WaitForPendingFinalizers(); long a0 = Alloc(); for (int i = 0; i < 100; i++) RunGuide.ForceTick(); long a1 = Alloc(); L(" " + P(a1 - a0 <= 0) + " ForceTick ×100 할당 = " + (a1 - a0) + " B"); long b0 = Alloc(); var junk = new byte[64 * 1024]; junk[0] = 1; long b1 = Alloc(); L(" " + P(b1 - b0 > 0) + " 대조군(64 KB 할당) = " + (b1 - b0) + " B — 측정기가 살아 있다"); RunGuide.StopGuide("probe gc done"); } // ───────────────────────────────────────── ⑨ C8 롤백 static void Sec9_C8() { L(""); L("── ⑨ C8 — 813w2 스위치를 끄면 813w 동작 그대로 · 전체를 끄면 무동작"); var c = WLRunGuideSettings.Instance; Vector3 anchor; if (c == null || !RunGuide.TryGetAnchor(c, 1, out anchor)) return; // ⑨-1 피격 응전 판정 — 실에셋을 건드리지 않고 임시 SO 로 두 동작을 나란히 확인한다 // (Poll 이 부르는 것과 **같은 함수** RunGuide.SuspendDecision 이다) var tmp = ScriptableObject.CreateInstance(); try { tmp.hitSuspendOnlyInHold = false; // 813w bool w_travel = RunGuide.SuspendDecision(tmp, true, RunGuideState.Travel); bool w_hold = RunGuide.SuspendDecision(tmp, true, RunGuideState.Hold); tmp.hitSuspendOnlyInHold = true; // 813w2 bool w2_travel = RunGuide.SuspendDecision(tmp, true, RunGuideState.Travel); bool w2_hold = RunGuide.SuspendDecision(tmp, true, RunGuideState.Hold); bool w2_nohit = RunGuide.SuspendDecision(tmp, false, RunGuideState.Hold); L(" " + P(w_travel && w_hold) + " 813w(hitSuspendOnlyInHold=0): Travel=" + w_travel + " Hold=" + w_hold + " → **이동 중에도 멈춘다 = 도착 판정·되돌림이 폴링에서 빠진다**(D-24·D-22 원인)"); L(" " + P(!w2_travel && w2_hold) + " 813w2(=1): Travel=" + w2_travel + " Hold=" + w2_hold + " → 이동·되돌림은 안 멈추고, 제자리 응전만 남는다"); L(" " + P(!w2_nohit) + " 안 맞으면 언제나 false"); tmp.arriveStillEnabled = false; float stillOff = -1f; bool off = RunGuide.StepStillDetect(tmp, 0f, 0.1f, 50f, ref stillOff) || RunGuide.StepStillDetect(tmp, 0f, 0.1f, 60f, ref stillOff); L(" " + P(!off) + " arriveStillEnabled=0 → 정지 판정 도착 없음(반경 판정만 = 813w)"); } finally { UnityEngine.Object.DestroyImmediate(tmp); } // ⑨-2 RuntimeDisabled → 아무 일도 하지 않는다 RunGuide.ResetDiagnostics(); MovePc(anchor + new Vector3(40f, 0f, 0f)); RunGuide.BeginZone(1, "probe c8-2"); var st = RunGuide.State; WLRunGuideSettings.RuntimeDisabled = true; try { s_pc.Change_Target(true, s_mob); for (int i = 0; i < 10; i++) RunGuide.ForceTick(); L(" " + P(RunGuide.State == st && !RunGuide.Steering && ReferenceEquals(s_pc.Get_Target(), s_mob)) + " RuntimeDisabled=1 → 틱 ×10 에 상태 불변(" + RunGuide.State + ") · 조종 0 · 타깃 무수정"); L(" " + P(s_pc.JoystickStatus == 0) + " JoystickStatus=0(원본 자동전투 100%)"); } finally { WLRunGuideSettings.RuntimeDisabled = false; RunGuide.StopGuide("probe c8 done"); } // 실에셋은 프로브가 한 번도 쓰지 않았다(전부 임시 SO · 읽기만) L(" " + P(c.hitSuspendOnlyInHold && c.arriveStillEnabled && c.pullbackReleasesTarget) + " 실에셋 무수정 확인 — hitSuspendOnlyInHold=" + c.hitSuspendOnlyInHold + " arriveStillEnabled=" + c.arriveStillEnabled + " pullbackReleasesTarget=" + c.pullbackReleasesTarget); } }