Project_WL/AgentScripts/WL813w2_Probe.cs

525 lines
30 KiB
C#
Raw 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.

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