Project_WL/AgentScripts/WL813w_Probe.cs

560 lines
29 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.

// WL813w_Probe.cs — #813w(런 진행 가이드 · 보스 우선 타깃 · 리쉬 완화 · 보스 바 이름) 에디트 모드 검증 프로브
// 에디트 모드 전용 · Play 불필요 · 로그인 0 · 씬/에셋 무수정(임시 GameObject·임시 SO 는 즉시 파괴)
//
// 상주 에디터: unity command run_script --project-path <wt> --file AgentScripts/WL813w_Probe.cs --entry WL813w_Probe.RunAll --timeout 300
// 결과: Console + AgentScripts/WL813w_PROBE.txt (RESULT PASS / RESULT FAIL n)
//
// 검사 (발주서 WL-813w §1-5)
// ① 에셋·SOT: WLRunGuideSettings 로드·값 전수 · WLBossArenaSettings 신규 3필드
// ② 앵커: 스포너 노드(MobControlMgr.SpawnerID) 실좌표 우선 · 노드가 없으면 SO 폴백 · WL_Nature.prefab 좌표 대조
// ③ 존 이동: 가짜 ZoneCleared(0) → 목적지 = 존2 앵커 · Travel → 도착(Hold) → 이탈 → 되돌림
// ④ 타깃 규칙: Travel 중 타깃 해제 · Hold 중 「앵커에서 guideLeashRadius 밖」만 무시(가까운 적은 유지)
// ⑤ 보스: 가짜 BossGateOpened → 아레나 중심 · BossStarted(boss) → 타깃 고정 + 원본 락타깃 · 사망 → 해제
// ⑥ 피격: SimulateHit → 가이드 중단(조종 0) · 해제 → 재개
// ⑦ 리쉬 회복 판정(임시 SO): 부분 회복 → 쿨다운 → 쿨다운 뒤 다시 회복 · percent>=1 이면 만피(813d 레거시)
// ⑧ 보스 바 이름: StripClone · ResolveBossLabel · MonsterList/Localization 실측 행 대조
// ⑨ GC: ForceTick 100회 0 B(대조군 = 의도적 할당)
// ⑩ C8: WLRunGuideSettings.RuntimeDisabled → 아무 일도 하지 않는다(조종 해제 · 상태 불변)
//
// 에디트 모드 한계(보고서 「미확인」으로 올린다)
// · NavMesh 가 없어 NavMeshAgent.SetDestination 실이동은 못 잰다 → Steer 는 agent 검사에서 되돌아간다.
// 상태 전이·타깃 규칙은 PC transform 을 직접 옮겨 검증한다(원본 Get_position() 이 transform 이다).
// · table_monsterlist/localtext 가 없어 「테이블 이름」 실경로는 JSON 행 대조로만 확인한다.
// · 실제 보스 Heal 은 살아 있는 m_Stat 이 필요해(813d 미확인 ②와 같은 뿌리) 판정만 검증한다.
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 WL813w_Probe
{
const string OutCommit = "AgentScripts/WL813w_PROBE.txt";
const string MonsterListJson = "Assets/ResWork/Table/Export/MonsterList.json";
const string LocalizationJson = "Assets/ResWork/Table/Export/Localization.json";
const string NaturePrefab = "Assets/Res_Addr/Map/WL_Nature.prefab";
static StringBuilder sb;
static int s_fail;
static GameObject s_pcGo, s_bossGo, s_mobFarGo, s_mobNearGo;
static GameObject[] s_spawnerGo;
static PCActor s_pc;
static Actor s_boss, s_mobFar, s_mobNear;
static PCActor s_prevMyPc;
public static object RunAll()
{
sb = new StringBuilder();
s_fail = 0;
sb.AppendLine("# WL813w 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_Anchor();
Sec3_ZoneMove();
Sec4_TargetRule();
Sec5_Boss();
Sec6_Hit();
Sec7_LeashHeal();
Sec8_BossName();
Sec9_GC();
Sec10_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 Actor MakeActor(string name, eRole role, eSubRol sub, Vector3 pos, out GameObject go)
{
go = new GameObject("__WL813w_" + name);
go.transform.position = pos;
var a = go.AddComponent<Actor>();
a.m_Role = role; a.m_SubRole = sub;
return a;
}
static void Setup()
{
s_prevMyPc = MyValue.MyPC;
s_pcGo = new GameObject("__WL813w_PC");
s_pcGo.transform.position = Vector3.zero;
s_pc = s_pcGo.AddComponent<PCActor>();
s_pc.m_Role = eRole.PC; s_pc.m_SubRole = eSubRol.None; // m_Enemy 기본 false → IsMainPC() true
MyValue.MyPC = s_pc;
s_boss = MakeActor("Anubis(Clone)", eRole.Mob, eSubRol.Boss, Vector3.zero, out s_bossGo);
s_mobFar = MakeActor("mobFar", eRole.Mob, eSubRol.None, Vector3.zero, out s_mobFarGo);
s_mobNear = MakeActor("mobNear", eRole.Mob, eSubRol.None, Vector3.zero, out s_mobNearGo);
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();
WLRunGuideSettings.RuntimeDisabled = false;
MyValue.MyPC = s_prevMyPc;
Kill(s_pcGo, s_bossGo, s_mobFarGo, s_mobNearGo);
if (s_spawnerGo != null) for (int i = 0; i < s_spawnerGo.Length; i++) Kill(s_spawnerGo[i]);
s_spawnerGo = null;
}
catch { }
}
static void Kill(params GameObject[] gos)
{
for (int i = 0; i < gos.Length; i++)
if (gos[i] != null) UnityEngine.Object.DestroyImmediate(gos[i]);
}
// ───────────────────────────────────────── ① 에셋 · SOT
static void Sec1_Asset()
{
L("");
L("── ① 에셋 · SOT");
var c = WLRunGuideSettings.Instance;
L(" " + P(c != null) + " Resources.Load(\"" + WLRunGuideSettings.ResourcesPath + "\")");
if (c == null) return;
L(" enabled=" + c.enabled + " verboseLog=" + c.verboseLog + " pollSeconds=" + c.pollSeconds);
L(" zoneGuideEnabled=" + c.zoneGuideEnabled + " guideOnRunStart=" + c.guideOnRunStart +
" zoneArriveRadius=" + c.zoneArriveRadius + " zoneHoldRadius=" + c.zoneHoldRadius);
L(" guideLeashRadius=" + c.guideLeashRadius + " guideStopDistance=" + c.guideStopDistance +
" repathIntervalSeconds=" + c.repathIntervalSeconds);
L(" resumeAfterHitSeconds=" + c.resumeAfterHitSeconds + " maxTravelSeconds=" + c.maxTravelSeconds);
L(" bossGuideEnabled=" + c.bossGuideEnabled + " bossPriorityEnabled=" + c.bossPriorityEnabled +
" bossArriveRadius=" + c.bossArriveRadius + " bossHoldRadius=" + c.bossHoldRadius +
" bossLockTarget=" + c.bossLockTarget);
L(" zoneAnchorFallbacks=" + (c.zoneAnchorFallbacks == null ? 0 : c.zoneAnchorFallbacks.Length) + "개");
var b = WLBossArenaSettings.Instance;
L(" " + P(b != null) + " WLBossArenaSettings 로드");
if (b == null) return;
L(" leashHealPercent=" + b.leashHealPercent + " leashHealCooldownSeconds=" + b.leashHealCooldownSeconds +
" bossNameFromTable=" + b.bossNameFromTable + " (기존 resetHealsBoss=" + b.resetHealsBoss + " resetHealRate=" + b.resetHealRate + ")");
L(" " + P(b.leashHealPercent > 0f && b.leashHealPercent < 1f) + " 리쉬 부분 회복 구간(0<p<1) — 만피 리셋 아님");
float leashLine = b.arenaRadius + b.playerLeaveMargin;
L(" " + P(c.bossHoldRadius < leashLine) + " bossHoldRadius(" + c.bossHoldRadius + ") < 리쉬 발동선(" +
leashLine + " = arenaRadius+playerLeaveMargin) → 이탈 전에 되돌린다");
}
// ───────────────────────────────────────── ② 앵커
static void Sec2_Anchor()
{
L("");
L("── ② 존 앵커 (스포너 노드 우선 · SO 폴백)");
var c = WLRunGuideSettings.Instance;
var rs = WLRunSettings.Instance;
if (c == null || rs == null || rs.zoneSpawnerIds == null) { L(" [FAIL] 설정 없음"); s_fail++; return; }
int n = rs.zoneSpawnerIds.Length;
if (rs.zoneClearKills != null && rs.zoneClearKills.Length < n) n = rs.zoneClearKills.Length;
// (a) 씬에 노드가 없을 때 = SO 폴백
Vector3 a0;
bool got0 = RunGuide.TryGetAnchor(c, 0, out a0);
L(" " + P(got0) + " 존0 앵커 = " + V(a0) + " · 노드=" + RunGuide.AnchorFromNode(0) + "(씬에 스포너 없음 → 폴백 기대)");
if (c.zoneAnchorFallbacks != null && c.zoneAnchorFallbacks.Length > 0)
L(" " + P(!RunGuide.AnchorFromNode(0) && a0 == c.zoneAnchorFallbacks[0]) + " 폴백 값 일치");
// (b) 가짜 스포너 노드를 심고 다시 읽는다 = 노드 실좌표 우선
s_spawnerGo = new GameObject[n];
for (int i = 0; i < n; i++)
{
s_spawnerGo[i] = new GameObject("__WL813w_Spawner" + rs.zoneSpawnerIds[i]);
s_spawnerGo[i].transform.position = new Vector3(1000f + i, 0f, 2000f + i);
var m = s_spawnerGo[i].AddComponent<MobControlMgr>();
m.SpawnerID = rs.zoneSpawnerIds[i];
}
RunGuide.ResetDiagnostics();
Vector3 a1;
bool got1 = RunGuide.TryGetAnchor(c, 1, out a1);
L(" " + P(got1 && RunGuide.AnchorFromNode(1) && a1 == s_spawnerGo[1].transform.position) +
" 존1 앵커 = " + V(a1) + " · 노드 실좌표 우선(스캔 " + RunGuide.AnchorScans + "회)");
// (c) WL_Nature.prefab 실좌표와 SO 폴백 대조 (원본 프리팹은 읽기만 한다)
L(" 프리팹 실측 대조(" + NaturePrefab + "):");
for (int i = 0; i < n && i < (c.zoneAnchorFallbacks == null ? 0 : c.zoneAnchorFallbacks.Length); i++)
{
Vector3 pf;
bool found = PrefabSpawnerPos(rs.zoneSpawnerIds[i], out pf);
bool same = found && DXZ(pf, c.zoneAnchorFallbacks[i]) < 0.5f;
L(" " + P(found && same) + " " + rs.zoneSpawnerIds[i] + " 프리팹 " + (found ? V(pf) : "없음") +
" ↔ SO 폴백 " + V(c.zoneAnchorFallbacks[i]));
}
}
/// <summary>WL_Nature.prefab 을 텍스트로 읽어 SpawnerID 노드의 월드 좌표를 구한다(프리팹 무수정).</summary>
static bool PrefabSpawnerPos(int spawnerId, out Vector3 pos)
{
pos = Vector3.zero;
try
{
var go = UnityEditor.AssetDatabase.LoadAssetAtPath<GameObject>(NaturePrefab);
if (go == null) return false;
var mgrs = go.GetComponentsInChildren<MobControlMgr>(true);
for (int i = 0; i < mgrs.Length; i++)
if (mgrs[i].SpawnerID == spawnerId) { pos = mgrs[i].transform.position; return true; }
}
catch { }
return false;
}
// ───────────────────────────────────────── ③ 존 이동
static void Sec3_ZoneMove()
{
L("");
L("── ③ 존 이동 (ZoneCleared → 다음 존 · 도착 · 이탈 복귀)");
var c = WLRunGuideSettings.Instance;
RunGuide.ResetDiagnostics();
Vector3 anchor1;
RunGuide.TryGetAnchor(c, 1, out anchor1);
// 존0 클리어 → 존1 유도
RunGuide.BeginZone(1, "probe ZoneCleared(0)");
L(" " + P(RunGuide.DestKind == RunGuideDest.Zone && RunGuide.ZoneIndex == 1) + " 목적지 = 존1(Zone)");
L(" " + P(DXZ(RunGuide.Destination, anchor1) < 0.01f) + " 목적지 좌표 = " + V(RunGuide.Destination));
L(" " + P(RunGuide.State == RunGuideState.Travel) + " 상태 Travel · GuideBegun=" + RunGuide.GuideBegun);
// 멀리 있을 때 = Travel 유지
s_pcGo.transform.position = anchor1 + new Vector3(0f, 0f, 30f);
RunGuide.ForceTick();
L(" " + P(RunGuide.State == RunGuideState.Travel) + " 30 m — Travel 유지(조종 시도 · Repaths=" + RunGuide.Repaths + ")");
// 도착
s_pcGo.transform.position = anchor1 + new Vector3(0f, 0f, c.zoneArriveRadius - 1f);
RunGuide.ForceTick();
L(" " + P(RunGuide.State == RunGuideState.Hold && RunGuide.Arrived == 1) +
" " + (c.zoneArriveRadius - 1f).ToString("F1") + " m — 도착 → Hold(자동전투 재개) · Arrived=" + RunGuide.Arrived);
L(" " + P(!RunGuide.Steering) + " 도착 뒤 조종 해제(JoystickStatus=" + s_pc.JoystickStatus + ")");
// Hold 유지 범위
s_pcGo.transform.position = anchor1 + new Vector3(0f, 0f, c.zoneHoldRadius - 2f);
RunGuide.ForceTick();
L(" " + P(RunGuide.State == RunGuideState.Hold && RunGuide.HoldPullbacks == 0) +
" " + (c.zoneHoldRadius - 2f).ToString("F1") + " m — Hold 유지(끌어오지 않는다)");
// 이탈 → 되돌림
s_pcGo.transform.position = anchor1 + new Vector3(0f, 0f, c.zoneHoldRadius + 5f);
RunGuide.ForceTick();
L(" " + P(RunGuide.State == RunGuideState.Travel && RunGuide.HoldPullbacks == 1) +
" " + (c.zoneHoldRadius + 5f).ToString("F1") + " m — 이탈 → 다시 유도 · HoldPullbacks=" + RunGuide.HoldPullbacks);
L(" 로그=" + RunGuide.LastLog);
}
// ───────────────────────────────────────── ④ 타깃 규칙
static void Sec4_TargetRule()
{
L("");
L("── ④ 타깃 규칙 (Travel 전면 해제 · Hold 는 앵커에서 먼 것만 무시)");
var c = WLRunGuideSettings.Instance;
RunGuide.ResetDiagnostics();
Vector3 anchor1;
RunGuide.TryGetAnchor(c, 1, out anchor1);
RunGuide.BeginZone(1, "probe target rule");
// Travel 중: 어떤 타깃이든 놓는다
s_pcGo.transform.position = anchor1 + new Vector3(0f, 0f, 25f);
s_mobNearGo.transform.position = anchor1 + new Vector3(0f, 0f, 24f); // PC 코앞이지만 이동 중
s_pc.Set_Target(s_mobNear);
RunGuide.ForceTick();
L(" " + P(!s_pc.isTarget && RunGuide.FarTargetIgnored == 1) +
" Travel 중 타깃 해제(무시 " + RunGuide.FarTargetIgnored + " · 마지막=" + RunGuide.LastIgnoredTarget + ")");
// 도착 → Hold
s_pcGo.transform.position = anchor1;
RunGuide.ForceTick();
L(" " + P(RunGuide.State == RunGuideState.Hold) + " 도착 → Hold");
// Hold: 앵커에서 먼 타깃 = 무시
int before = RunGuide.FarTargetIgnored;
s_mobFarGo.transform.position = anchor1 + new Vector3(0f, 0f, c.guideLeashRadius + 8f);
s_pc.Set_Target(s_mobFar);
RunGuide.ForceTick();
L(" " + P(!s_pc.isTarget && RunGuide.FarTargetIgnored == before + 1) +
" 앵커 +" + (c.guideLeashRadius + 8f).ToString("F0") + " m 타깃 무시(Del_Target)");
// Hold: 앵커 가까이의 타깃 = 유지(원본 자동전투가 잡는다)
before = RunGuide.FarTargetIgnored;
s_mobNearGo.transform.position = anchor1 + new Vector3(0f, 0f, c.guideLeashRadius - 4f);
s_pc.Set_Target(s_mobNear);
RunGuide.ForceTick();
L(" " + P(s_pc.isTarget && RunGuide.FarTargetIgnored == before) +
" 앵커 +" + (c.guideLeashRadius - 4f).ToString("F0") + " m 타깃 유지(원본 자동전투)");
s_pc.Del_Target(true);
}
// ───────────────────────────────────────── ⑤ 보스
static void Sec5_Boss()
{
L("");
L("── ⑤ 보스 (아레나 유도 · 우선 타깃 고정)");
var c = WLRunGuideSettings.Instance;
RunGuide.ResetDiagnostics();
Vector3 center = BossArena.Center;
RunGuide.BeginBoss("probe BossGateOpened");
L(" " + P(RunGuide.DestKind == RunGuideDest.Boss) + " 목적지 = 보스 아레나 " + V(RunGuide.Destination) +
" (BossArena.Center=" + V(center) + ")");
// 보스 등장 → 아레나 밖에서 시작
s_bossGo.transform.position = center;
RunGuide.SetBossForProbe(s_boss);
s_pcGo.transform.position = center + new Vector3(0f, 0f, 25f);
s_pc.Set_Target(s_mobFar);
s_mobFarGo.transform.position = center + new Vector3(0f, 0f, 40f);
RunGuide.ForceTick();
L(" " + P(RunGuide.State == RunGuideState.Travel) + " 25 m — Travel(아레나로 유도)");
L(" " + P(ReferenceEquals(s_pc.Get_Target(), s_boss) && RunGuide.BossRetargets >= 1) +
" 타깃 = 보스 고정(BossRetargets=" + RunGuide.BossRetargets + ")");
L(" " + P(!c.bossLockTarget || s_boss.m_IsLockTarget) +
" 원본 락타깃 m_IsLockTarget=" + s_boss.m_IsLockTarget + "(BossLocks=" + RunGuide.BossLocks + ")");
// 잡몹으로 타깃이 새면 폴링이 되돌린다
s_pc.Set_Target(s_mobFar);
RunGuide.ForceTick();
L(" " + P(ReferenceEquals(s_pc.Get_Target(), s_boss)) + " 잡몹 타깃 전환 → 다음 폴링에서 보스로 복귀");
// 아레나 도착
s_pcGo.transform.position = center + new Vector3(0f, 0f, c.bossArriveRadius - 2f);
RunGuide.ForceTick();
L(" " + P(RunGuide.State == RunGuideState.Hold) + " " + (c.bossArriveRadius - 2f).ToString("F1") + " m — 도착 → Hold");
// 아레나 이탈(리쉬 발동선 전에) → 되돌림
var b = WLBossArenaSettings.Instance;
float leashLine = b != null ? b.arenaRadius + b.playerLeaveMargin : 24f;
s_pcGo.transform.position = center + new Vector3(0f, 0f, c.bossHoldRadius + 2f);
RunGuide.ForceTick();
L(" " + P(RunGuide.State == RunGuideState.Travel && RunGuide.HoldPullbacks == 1) +
" " + (c.bossHoldRadius + 2f).ToString("F1") + " m 이탈 → 되돌림(리쉬 발동선 " + leashLine.ToString("F0") + " m 前)");
L(" " + P(c.bossHoldRadius + 2f < leashLine) + " 되돌림 지점 < 리쉬 발동선 = 리셋이 아예 안 걸린다(D-11)");
// 보스 사망 → 락 해제
s_bossGo.SetActive(false); // IsDead() = !activeInHierarchy
RunGuide.ForceTick();
L(" " + P(!s_boss.m_IsLockTarget) + " 보스 사망/비활성 → 락타깃 해제");
s_bossGo.SetActive(true);
RunGuide.StopGuide("probe ⑤ 종료");
}
// ───────────────────────────────────────── ⑥ 피격 응전
static void Sec6_Hit()
{
L("");
L("── ⑥ 피격 → 즉시 응전(가이드 일시 중단)");
var c = WLRunGuideSettings.Instance;
RunGuide.ResetDiagnostics();
Vector3 anchor1;
RunGuide.TryGetAnchor(c, 1, out anchor1);
RunGuide.BeginZone(1, "probe hit");
s_pcGo.transform.position = anchor1 + new Vector3(0f, 0f, 25f);
s_mobFarGo.transform.position = anchor1 + new Vector3(0f, 0f, 40f);
RunGuide.SimulateHit();
s_pc.Set_Target(s_mobFar);
int before = RunGuide.FarTargetIgnored;
RunGuide.ForceTick();
L(" " + P(s_pc.isTarget && RunGuide.FarTargetIgnored == before && !RunGuide.Steering) +
" 피격 중 — 타깃 유지 · 조종 0(원본 자동전투가 응전) · HitSuspends=" + RunGuide.HitSuspends);
L(" " + P(RunGuide.DestKind == RunGuideDest.Zone) + " 목적지는 유지(중단일 뿐 취소 아님)");
RunGuide.ClearHitSuspendForProbe();
RunGuide.ForceTick();
L(" " + P(!s_pc.isTarget && RunGuide.FarTargetIgnored == before + 1) + " 무피격 구간 복귀 → 가이드 재개");
RunGuide.StopGuide("probe ⑥ 종료");
}
// ───────────────────────────────────────── ⑦ 리쉬 회복
static void Sec7_LeashHeal()
{
L("");
L("── ⑦ 리쉬 회복 판정 (임시 SO · 에셋 무수정)");
var tmp = ScriptableObject.CreateInstance<WLBossArenaSettings>();
try
{
tmp.resetHealsBoss = true;
tmp.leashHealPercent = 0.15f;
tmp.leashHealCooldownSeconds = 10f;
BossArena.ResetLeashState();
int v1 = BossArena.EvaluateLeashHeal(tmp, 100f);
int v2 = BossArena.EvaluateLeashHeal(tmp, 105f);
int v3 = BossArena.EvaluateLeashHeal(tmp, 111f);
L(" " + P(v1 == BossArena.kHealPartial) + " t=100 첫 리쉬 → 부분 회복(+15 %)");
L(" " + P(v2 == BossArena.kHealCooldown) + " t=105 (5 s 뒤) → 쿨다운으로 건너뜀 = 만피 복구 없음");
L(" " + P(v3 == BossArena.kHealPartial) + " t=111 (11 s 뒤) → 다시 부분 회복 · 최대 1회/10 s");
tmp.leashHealPercent = 1f;
BossArena.ResetLeashState();
L(" " + P(BossArena.EvaluateLeashHeal(tmp, 200f) == BossArena.kHealFull) +
" leashHealPercent=1 → 813d 원래 동작(On_Regen 만피) = C8 롤백 경로");
tmp.leashHealPercent = 0.15f; tmp.resetHealsBoss = false;
BossArena.ResetLeashState();
L(" " + P(BossArena.EvaluateLeashHeal(tmp, 300f) == BossArena.kHealNone) + " resetHealsBoss=0 → 회복 없음(워프만)");
// Q4b 실측 대비: 69회 리쉬 · 90 s 보스전이면 회복은 최대 몇 회인가
var real = WLBossArenaSettings.Instance;
if (real != null && real.leashHealCooldownSeconds > 0f)
{
int maxHeals = Mathf.FloorToInt(90f / real.leashHealCooldownSeconds) + 1;
L(" 참고: Q4b 런① = 리쉬 69회 / 보스전 90 s → 이 값으로는 최대 " + maxHeals +
"회 × " + (real.leashHealPercent * 100f).ToString("F0") + "% (예전 = 69회 × 100 %)");
}
}
finally { UnityEngine.Object.DestroyImmediate(tmp); BossArena.ResetLeashState(); }
}
// ───────────────────────────────────────── ⑧ 보스 바 이름
static void Sec8_BossName()
{
L("");
L("── ⑧ 보스 HP 바 이름 (D-12 · Anubis(Clone))");
L(" " + P(WL.UI.BossHpBar.StripClone("Anubis(Clone)") == "Anubis") + " StripClone(\"Anubis(Clone)\") = \"" +
WL.UI.BossHpBar.StripClone("Anubis(Clone)") + "\"");
L(" " + P(WL.UI.BossHpBar.StripClone("Anubis (Clone)") == "Anubis") + " StripClone(\"Anubis (Clone)\") = \"" +
WL.UI.BossHpBar.StripClone("Anubis (Clone)") + "\"");
// 가짜 보스 오브젝트 이름 = "__WL813w_Anubis(Clone)"(프로브 접두사 + QA 가 본 이름)
string objName = s_boss.name;
string label = WL.UI.BossHpBar.ResolveBossLabel(s_boss);
L(" " + P(label == objName.Replace("(Clone)", "") && !label.Contains("(Clone)")) +
" 표가 없는 에디트 모드 → 폴백 = \"" + label + "\" (원본 오브젝트 이름 \"" + objName +
"\" · 경로=" + WL.UI.BossHpBar.LastNameSource + ")");
var tmp = ScriptableObject.CreateInstance<WLBossArenaSettings>();
try
{
tmp.bossNameFromTable = false;
string raw = WL.UI.BossHpBar.ResolveBossLabel(s_boss, tmp);
L(" " + P(raw == objName) + " C8 롤백(bossNameFromTable=0) → 원본 이름 그대로 \"" + raw + "\"");
}
finally { UnityEngine.Object.DestroyImmediate(tmp); }
// 인게임에서 실제로 나올 이름 = MonsterList → Localization 실측
var b = WLBossArenaSettings.Instance;
int mid = b != null ? b.bossMonsterId : 10006;
string nameId = JsonField(MonsterListJson, "n_MonsterID", mid.ToString(), "n_MonsterName");
string kor = nameId != null ? JsonField(LocalizationJson, "ID", nameId, "s_Korean") : null;
L(" " + P(!string.IsNullOrEmpty(kor)) + " MonsterList " + mid + " → n_MonsterName=" + nameId +
" → Localization s_Korean=\"" + (kor ?? "없음") + "\" (인게임 라벨 기대값 · 813tj 배너와 동일)");
}
/// <summary>Export JSON 에서 keyField=keyValue 인 행의 outField 를 읽는다(표 로드 없이 실측).</summary>
static string JsonField(string path, string keyField, string keyValue, string outField)
{
try
{
string txt = File.ReadAllText(path);
string key = "\"" + keyField + "\":\"" + keyValue + "\"";
int i = txt.IndexOf(key, StringComparison.Ordinal);
if (i < 0) { key = "\"" + keyField + "\": \"" + keyValue + "\""; i = txt.IndexOf(key, StringComparison.Ordinal); }
if (i < 0) return null;
int rowStart = txt.LastIndexOf('{', i);
int rowEnd = txt.IndexOf('}', i);
if (rowStart < 0 || rowEnd < 0) return null;
string row = txt.Substring(rowStart, rowEnd - rowStart);
int j = row.IndexOf("\"" + outField + "\"", StringComparison.Ordinal);
if (j < 0) return null;
int q1 = row.IndexOf('"', row.IndexOf(':', j) + 1);
int q2 = row.IndexOf('"', q1 + 1);
if (q1 < 0 || q2 < 0) return null;
return row.Substring(q1 + 1, q2 - q1 - 1);
}
catch { return null; }
}
// ───────────────────────────────────────── ⑨ GC
static void Sec9_GC()
{
L("");
L("── ⑨ GC (ForceTick 100회)");
var c = WLRunGuideSettings.Instance;
RunGuide.ResetDiagnostics();
Vector3 anchor1;
RunGuide.TryGetAnchor(c, 1, out anchor1);
RunGuide.BeginZone(1, "probe gc");
s_pcGo.transform.position = anchor1 + new Vector3(0f, 0f, 8f); // Hold 도 Travel 도 아닌 중간 = 매 틱 판정
RunGuide.ForceTick();
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
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 string[256];
for (int i = 0; i < junk.Length; i++) junk[i] = new string('x', 4096);
long b1 = Alloc();
L(" 대조군(의도적 할당 256×4096자) = " + (b1 - b0) + " B · " + P(b1 - b0 > 0) + " 측정기 동작 확인");
GC.KeepAlive(junk);
RunGuide.StopGuide("probe ⑨ 종료");
}
// ───────────────────────────────────────── ⑩ C8
static void Sec10_C8()
{
L("");
L("── ⑩ C8 롤백 (RuntimeDisabled → 원본 자동전투 100%)");
var c = WLRunGuideSettings.Instance;
RunGuide.ResetDiagnostics();
Vector3 anchor1;
RunGuide.TryGetAnchor(c, 1, out anchor1);
RunGuide.BeginZone(1, "probe c8");
s_pcGo.transform.position = anchor1 + new Vector3(0f, 0f, 30f);
s_mobFarGo.transform.position = anchor1 + new Vector3(0f, 0f, 60f);
s_pc.Set_Target(s_mobFar);
WLRunGuideSettings.RuntimeDisabled = true;
int before = RunGuide.FarTargetIgnored;
var st = RunGuide.State;
for (int i = 0; i < 10; i++) RunGuide.ForceTick();
L(" " + P(!RunGuide.Active) + " RuntimeDisabled → Active=false");
L(" " + P(RunGuide.FarTargetIgnored == before && s_pc.isTarget) + " 틱 ×10 — 타깃 무수정(원본 그대로)");
L(" " + P(!RunGuide.Steering && s_pc.JoystickStatus == 0) + " 조종 0(JoystickStatus=" + s_pc.JoystickStatus + ")");
L(" " + P(RunGuide.State == st) + " 상태 불변");
WLRunGuideSettings.RuntimeDisabled = false;
RunGuide.ForceTick();
L(" " + P(RunGuide.FarTargetIgnored == before + 1) + " 스위치 복귀 → 다시 동작");
RunGuide.StopGuide("probe ⑩ 종료");
s_pc.Del_Target(true);
}
}