Project_WL/AgentScripts/WL813w2_Probe.cs

525 lines
30 KiB
C#
Raw Permalink Normal View History

// WL813w2_Probe.cs — #813w2(런 가이드 2차: 도착 판정 D-24 · 보스 홀드 D-22 · 군중 유지 D-19 · 리쉬 축소) 프로브
// 에디트 모드 전용 · Play 불필요 · 로그인 0 · 씬/에셋 무수정(임시 GameObject·임시 SO 는 즉시 파괴/회수)
//
// 상주 에디터:
// unity command run_script --project-path <wt> --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<PCActor>();
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<Actor>();
s_boss.m_Role = eRole.Mob; s_boss.m_SubRole = eSubRol.Boss;
s_mobGo = new GameObject("__WL813w2_Mob");
s_mob = s_mobGo.AddComponent<Actor>();
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<WLBossArenaSettings>();
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<WLRunGuideSettings>();
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);
}
}