Project_WL/AgentScripts/WL813w_Probe.cs

560 lines
29 KiB
C#
Raw Normal View History

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