Project_WL/AgentScripts/WL813w4_Probe.cs

610 lines
35 KiB
C#
Raw Permalink 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.

// WL813w4_Probe.cs — #813w4(「다음 런」 가이드 재시작 · 엘리트 우선 타깃 · 레벨업 연출 값) 프로브
// 에디트 모드 전용 · Play 불필요 · 로그인 0 · 씬/에셋 무수정(임시 GameObject·임시 SO 는 즉시 파괴/회수)
//
// 상주 에디터:
// unity command run_script --project-path <wt> --file AgentScripts/WL813w4_Probe.cs --entry WL813w4_Probe.RunAll --timeout 300
// 결과: Console + AgentScripts/WL813w4_PROBE.txt (RESULT PASS / RESULT FAIL n)
//
// 무엇을 증명하나 (발주서 WL-813w4 §1-1~§1-4 · 근거 = qa/W2-7_재채점_v5.md D-34 · D-20 · D-37)
// ① 에셋·SOT — 813w4 신규 필드(가이드 9 · 성장 2) 값 · 반경 정합성 · 코드 상수 0
// ② D-34 원인 — 「구독은 살아 있는데 판정에서 끊겼다」를 코드 경로로 실증 + RunDirector.Restart() ×2 재현
// ③ D-34 안전망 — 정지 감시(TrackIdleStall) 진리표(교전 중 · 이동 중 · 존 단계 밖 · 연타)
// ④ D-20 엘리트 — 가짜 엘리트 스폰(진짜 CombatEvents.Spawned) → 락+재타깃 → 처치(진짜 Killed) → 해제
// ⑤ D-37 값 — 화면 점유 상한(ParticleSystemRenderer.maxParticleSize)·알파·수명 · 프리팹 무수정 확인
// ⑥ GC 0 — 엘리트 폴링 ×100 · 정지 감시 ×100 이 0 B
// ⑦ C8 롤백 — guideOnRestart / stallGuideSeconds / elitePriorityEnabled / burst 값 0 → 813w3 동작
//
// 에디트 모드 한계(보고서 「미확인」)
// · Play 가 없으므로 실제 이동·타격·화면 크기는 못 본다. 판정과 상태 전이만 실측한다.
// · Time.unscaledTime 이 스크립트 실행 중 멈춰 있다 → 시간 의존 판정은 가짜 시각을 먹인다.
// · RunDirector.Restart() 의 PC 워프(On_Regen)는 m_Stat 이 없어 실패한다 — 원본이 try/catch 로 삼킨다(설계).
using System;
using System.IO;
using System.Text;
using UnityEngine;
using WL.Combat.Core;
using WL.Combat.Growth;
using WL.Combat.Run;
public static class WL813w4_Probe
{
const string OutCommit = "AgentScripts/WL813w4_PROBE.txt";
static StringBuilder sb;
static int s_fail;
static GameObject s_pcGo, s_eliteGo, s_elite2Go, s_mobGo, s_fxGo;
static PCActor s_pc;
static Actor s_elite, s_elite2, s_mob;
static PCActor s_prevMyPc;
public static object RunAll()
{
sb = new StringBuilder();
s_fail = 0;
sb.AppendLine("# WL813w4 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_Restart();
Sec3_Stall();
Sec4_Elite();
Sec5_LevelFx();
Sec6_GC();
Sec7_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") + ")"; }
/// <summary>실에셋을 복사한 임시 SO(실에셋은 절대 안 건드린다). 쓰고 나면 DestroyImmediate.</summary>
static WLRunGuideSettings TmpGuide()
{
var t = UnityEngine.Object.Instantiate(WLRunGuideSettings.Instance);
t.hideFlags = HideFlags.HideAndDontSave;
return t;
}
static WLGrowthSettings TmpGrowth()
{
var t = UnityEngine.Object.Instantiate(WLGrowthSettings.Instance);
t.hideFlags = HideFlags.HideAndDontSave;
return t;
}
static void Setup()
{
s_prevMyPc = MyValue.MyPC;
s_pcGo = new GameObject("__WL813w4_PC");
s_pc = s_pcGo.AddComponent<PCActor>();
s_pc.m_Role = eRole.PC; s_pc.m_SubRole = eSubRol.None;
MyValue.MyPC = s_pc;
s_eliteGo = new GameObject("__WL813w4_Elite");
s_elite = s_eliteGo.AddComponent<Actor>();
s_elite.m_Role = eRole.Mob; s_elite.m_SubRole = eSubRol.Elite;
s_elite2Go = new GameObject("__WL813w4_Elite2");
s_elite2 = s_elite2Go.AddComponent<Actor>();
s_elite2.m_Role = eRole.Mob; s_elite2.m_SubRole = eSubRol.Elite;
s_mobGo = new GameObject("__WL813w4_Mob");
s_mob = s_mobGo.AddComponent<Actor>();
s_mob.m_Role = eRole.Mob; s_mob.m_SubRole = eSubRol.None;
RunGuide.EnsureSubscribed();
RunGuide.ResetDiagnostics();
RunDirector.ResetAll();
WLRunGuideSettings.RuntimeDisabled = false;
L("");
L("── ⓪ 준비");
L(" " + P(s_pc.IsMainPC()) + " 가짜 메인 PC IsMainPC()=true · MyValue.MyPC 등록");
L(" " + P(RunGuide.Active) + " RunGuide.Active(에셋 · enabled · 런 축)");
L(" " + P(RunGuide.Subscribed) + " RunGuide.Subscribed — 🔴 D-34 의 「RunStarted 구독이 없다」 추정은 여기서 이미 반증된다");
}
static void Teardown()
{
try
{
RunGuide.StopGuide("probe teardown");
RunGuide.ResetDiagnostics();
RunGuide.SetSteeringForProbe(false);
RunDirector.ResetAll();
WLRunGuideSettings.RuntimeDisabled = false;
LevelUpBurst.ResetDiagnostics();
MyValue.MyPC = s_prevMyPc;
Kill(s_pcGo, s_eliteGo, s_elite2Go, s_mobGo, s_fxGo);
}
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 (813w4 신규 필드 · C45 · 코드 상수 0)");
var c = WLRunGuideSettings.Instance;
var g = WLGrowthSettings.Instance;
L(" " + P(c != null) + " WLRunGuideSettings 로드 · " + P(g != null) + " WLGrowthSettings 로드");
if (c == null || g == null) return;
L(" [가이드 신규 9] guideOnRestart=" + c.guideOnRestart +
" restartResetsGuideState=" + c.restartResetsGuideState +
" stallGuideSeconds=" + c.stallGuideSeconds + " stallMoveEpsilon=" + c.stallMoveEpsilon +
" elitePriorityEnabled=" + c.elitePriorityEnabled +
" eliteTargetRadius=" + c.eliteTargetRadius + " eliteReleaseRadius=" + c.eliteReleaseRadius +
" eliteLockTarget=" + c.eliteLockTarget + " eliteRetargetOnlyInHold=" + c.eliteRetargetOnlyInHold);
L(" [성장 신규 2 + 값 2] burstMaxScreenFraction=" + g.burstMaxScreenFraction +
" burstAlpha=" + g.burstAlpha + " burstScale=" + g.burstScale +
" burstLifetimeSeconds=" + g.burstLifetimeSeconds);
L(" " + P(c.guideOnRestart) + " 「다음 런」 재유도 on(D-34) · guideOnRunStart 는 " + c.guideOnRunStart + " 그대로(런 1 동작 무변경)");
L(" " + P(c.stallGuideSeconds > 0f && c.stallMoveEpsilon > 0f) +
" 정지 감시 " + c.stallGuideSeconds + "s / " + c.stallMoveEpsilon + "m");
L(" " + P(c.elitePriorityEnabled && Mathf.Approximately(c.eliteTargetRadius, 12f)) +
" 엘리트 우선 타깃 on · 반경 12 m(발주서 §1-2)");
L(" " + P(c.eliteReleaseRadius > c.eliteTargetRadius) +
" 해제 반경(" + c.eliteReleaseRadius + ") > 고정 반경(" + c.eliteTargetRadius + ") — 경계에서 떨리지 않는다");
L(" " + P(c.eliteTargetRadius <= c.zoneHoldRadius) +
" 고정 반경(" + c.eliteTargetRadius + ") ≤ zoneHoldRadius(" + c.zoneHoldRadius +
") — 엘리트를 쫓다 존 밖으로 나가면 Hold 되돌림이 먼저 걸린다");
L(" " + P(g.burstMaxScreenFraction > 0f && g.burstMaxScreenFraction <= 0.5f) +
" 화면 점유 상한 " + g.burstMaxScreenFraction + " ≤ 0.5(발주서 §1-3 「화면 50 % 이하」)");
L(" " + P(Mathf.Approximately(g.burstAlpha, 0.6f)) + " 알파 0.6(발주서 §1-3)");
L(" " + P(Mathf.Approximately(g.burstLifetimeSeconds, 0.8f)) + " 지속 0.8 s(발주서 §1-3)");
// 존 축 실측(D-34 의 「존 0/18」 이 어디서 오는지)
var rs = WLRunSettings.Instance;
if (rs != null && rs.zoneSpawnerIds != null)
L(" 존 축 실측 — zoneSpawnerIds=[" + string.Join(",", Array.ConvertAll(rs.zoneSpawnerIds, x => x.ToString())) +
"] zoneClearKills=[" + string.Join(",", Array.ConvertAll(rs.zoneClearKills, x => x.ToString())) +
"] (Q7 「존 진행 0/18」 = 존0 요구 처치)");
Vector3 a0;
bool hasA0 = RunGuide.TryGetAnchor(c, 0, out a0);
L(" " + P(hasA0) + " 존0 앵커 확보 " + V(a0) + " · 스포너 노드=" + RunGuide.AnchorFromNode(0) +
" — 🔴 재시작 좌표 (115.0,0.3,11.0) 에서 " +
Mathf.Sqrt((a0.x - 115f) * (a0.x - 115f) + (a0.z - 10.98f) * (a0.z - 10.98f)).ToString("F1") +
" m (Q7 D-34 의 정지 좌표 · 원본 자동전투 탐지 범위 밖)");
}
// ───────────────────────────────────────── ② D-34 「다음 런」 재시작
static void Sec2_Restart()
{
L("");
L("── ② D-34 「다음 런」 뒤 존0 유도 재시작 (발주서 §1-1)");
var c = WLRunGuideSettings.Instance;
if (c == null) return;
// ②-1 원인 실증 — 판정 진리표(순수 함수 · 813w3 값 vs 813w4 값)
var old = TmpGuide(); old.guideOnRunStart = false; old.guideOnRestart = false; // 813w3 상태 재현
var now = TmpGuide(); now.guideOnRunStart = false; now.guideOnRestart = true; // 813w4
L(" 🔴 원인 진리표 — ShouldGuideOnStart(cfg, restart)");
L(" " + P(!RunGuide.ShouldGuideOnStart(old, false)) + " 813w3 · 런 1 = false (원본 자동전투에 맡긴다 — 맵 진입점이 존0 안이라 굴러갔다)");
L(" " + P(!RunGuide.ShouldGuideOnStart(old, true)) + " 813w3 · 다음 런 = **false** ← 🔴 D-34 의 끊긴 지점(RunGuide.cs:311 `cfg.guideOnRunStart`)");
L(" " + P(!RunGuide.ShouldGuideOnStart(now, false)) + " 813w4 · 런 1 = false (런 1 동작 무변경 = 회귀 0)");
L(" " + P(RunGuide.ShouldGuideOnStart(now, true)) + " 813w4 · 다음 런 = **true**");
L(" " + P(RunGuide.IsRestartReason("restart")) + " reason 판정 — \"restart\"(813p 보고 §4 계약) = 재시작");
L(" " + P(!RunGuide.IsRestartReason("pc") && !RunGuide.IsRestartReason("map 2")) + " \"pc\" · \"map N\" 은 재시작 아님");
Kill2(old, now);
// ②-2 핸들러 직접 — RunStarted(reason="restart") 를 실제 페이로드로 태운다
RunGuide.ResetDiagnostics();
RunGuide.SyncRestartCountForProbe();
int begun0 = RunGuide.GuideBegun;
RunGuide.ProbeRunStarted(1, "pc");
int begunAfterFirst = RunGuide.GuideBegun;
L(" " + P(begunAfterFirst == begun0) + " 런 1(reason=\"pc\") → GuideBegun " + begun0 + "→" + begunAfterFirst +
" (guideOnRunStart=" + c.guideOnRunStart + " 이므로 그대로 = 813w3 동작 보존)");
RunGuide.ProbeRunStarted(2, "restart");
L(" " + P(RunGuide.GuideBegun == begunAfterFirst + 1) + " 「다음 런」(reason=\"restart\") → GuideBegun " +
begunAfterFirst + "→" + RunGuide.GuideBegun + " (**Q7 이 「2 에서 안 오른다」고 적은 그 값**)");
L(" " + P(RunGuide.State == RunGuideState.Travel) + " State=" + RunGuide.State + " (Q7 실측 Off → Travel)");
L(" " + P(RunGuide.DestKind == RunGuideDest.Zone && RunGuide.ZoneIndex == 0) +
" 목적지=존 " + RunGuide.ZoneIndex + " " + V(RunGuide.Destination));
L(" " + P(RunGuide.RestartGuides == 1) + " RestartGuides=" + RunGuide.RestartGuides);
// ②-3 2회 재현(Q7 은 2회 전부 재현했다)
RunGuide.ProbeRunStarted(3, "restart");
L(" " + P(RunGuide.RestartGuides == 2 && RunGuide.State == RunGuideState.Travel) +
" Restart ×2 → 유도 재개 2회(RestartGuides=" + RunGuide.RestartGuides + " · State=" + RunGuide.State + ")");
// ②-4 고장 플래그 해제(restartResetsGuideState)
RunGuide.ForceFaultForProbe();
L(" 고장(예외) 상태 강제 — Faulted=" + RunGuide.Faulted);
RunGuide.ProbeRunStarted(4, "restart");
L(" " + P(!RunGuide.Faulted) + " 재시작이 고장 플래그를 푼다(restartResetsGuideState) — 런 1 의 예외가 세션을 못 죽인다");
// ②-5 진짜 RunDirector.Restart() 경로(구독 → 발행 → 핸들러 전 구간)
RunGuide.ResetDiagnostics();
RunDirector.ResetAll();
RunGuide.SyncRestartCountForProbe();
MovePc(new Vector3(115f, 0.25f, 10.98f)); // Q7 D-34 의 정지 좌표
RunDirector.StartRun("pc");
int begunRun1 = RunGuide.GuideBegun;
RunDirector.Restart();
L(" " + P(RunDirector.RestartCount == 1 && RunDirector.StartCount == 2) +
" RunDirector.Restart() 실행 — RestartCount=" + RunDirector.RestartCount + " StartCount=" + RunDirector.StartCount);
L(" " + P(RunGuide.GuideBegun == begunRun1 + 1) + " 실 경로에서도 GuideBegun " + begunRun1 + "→" + RunGuide.GuideBegun);
L(" " + P(RunGuide.State == RunGuideState.Travel && RunGuide.DestKind == RunGuideDest.Zone) +
" 재시작 직후 State=" + RunGuide.State + " 목적지=" + RunGuide.DestKind + " " + V(RunGuide.Destination));
L(" " + P(RunDirector.Phase == RunPhase.Zones) + " RunDirector.Phase=" + RunDirector.Phase);
L(" (참고) RunDirector 로그 = " + RunDirector.LastLog);
}
static void Kill2(params UnityEngine.Object[] os)
{
for (int i = 0; i < os.Length; i++) if (os[i] != null) UnityEngine.Object.DestroyImmediate(os[i]);
}
// ───────────────────────────────────────── ③ D-34 정지 감시(안전망)
static void Sec3_Stall()
{
L("");
L("── ③ D-34 안전망 — 정지 감시 TrackIdleStall (원인과 무관하게 「166 s 정지」를 되돌린다)");
var c = TmpGuide();
c.stallGuideSeconds = 3f; c.stallMoveEpsilon = 0.5f;
RunGuide.ResetDiagnostics();
RunDirector.ResetAll();
RunDirector.StartRun("pc"); // Phase=Zones
RunGuide.StopGuide("probe"); // 목적지 없음 = D-34 상태
RunGuide.ResetStallStateForProbe();
s_pc.Del_Target(true);
Vector3 stand = new Vector3(115f, 0.25f, 10.98f);
MovePc(stand);
float t = 1000f;
bool r0 = RunGuide.TrackIdleStall(c, s_pc, stand, t); // 첫 관측 = 기준 잡기
bool r1 = RunGuide.TrackIdleStall(c, s_pc, stand, t + 2.0f); // 2 s — 아직
bool r2 = RunGuide.TrackIdleStall(c, s_pc, stand, t + 3.5f); // 3.5 s — 유도
L(" " + P(!r0 && !r1 && r2) + " 제자리 3 s → 유도(첫관측 " + r0 + " · 2.0s " + r1 + " · 3.5s " + r2 + ")");
L(" " + P(RunGuide.StallGuides == 1 && RunGuide.DestKind == RunGuideDest.Zone && RunGuide.ZoneIndex == 0) +
" StallGuides=" + RunGuide.StallGuides + " 목적지=존 " + RunGuide.ZoneIndex);
// 이동하면 다시 잰다
RunGuide.StopGuide("probe"); RunGuide.ResetStallStateForProbe();
RunGuide.TrackIdleStall(c, s_pc, stand, t + 10f);
bool moved = RunGuide.TrackIdleStall(c, s_pc, stand + new Vector3(3f, 0f, 0f), t + 14f);
L(" " + P(!moved) + " 3 m 이동하면 타이머를 다시 잡는다(유도 안 함)");
// 교전 중이면 끼어들지 않는다
RunGuide.StopGuide("probe"); RunGuide.ResetStallStateForProbe();
s_mobGo.transform.position = stand;
s_pc.Change_Target(true, s_mob);
RunGuide.TrackIdleStall(c, s_pc, stand, t + 20f);
bool inFight = RunGuide.TrackIdleStall(c, s_pc, stand, t + 25f);
L(" " + P(!inFight && s_pc.isTarget) + " 🔴 타깃 보유(제자리 교전) → 유도 0 — 전투를 끊지 않는다");
s_pc.Del_Target(true);
// 존 단계가 아니면 돌지 않는다
RunGuide.StopGuide("probe"); RunGuide.ResetStallStateForProbe();
RunDirector.EndRun(RunOutcome.Timeout);
RunGuide.TrackIdleStall(c, s_pc, stand, t + 30f);
bool notZones = RunGuide.TrackIdleStall(c, s_pc, stand, t + 35f);
L(" " + P(!notZones) + " Phase=" + RunDirector.Phase + "(존 단계 아님) → 유도 0(결과 화면·로비에서 안 돈다)");
// 이미 유도 중이면 돌지 않는다
RunDirector.StartRun("pc");
RunGuide.BeginZone(0, "probe");
RunGuide.ResetStallStateForProbe();
RunGuide.TrackIdleStall(c, s_pc, stand, t + 40f);
bool guiding = RunGuide.TrackIdleStall(c, s_pc, stand, t + 45f);
L(" " + P(!guiding) + " 이미 유도 중(DestKind=" + RunGuide.DestKind + ") → 중복 유도 0");
Kill2(c);
}
// ───────────────────────────────────────── ④ D-20 엘리트 우선 타깃
static void Sec4_Elite()
{
L("");
L("── ④ D-20 엘리트 우선 타깃 (발주서 §1-2 · Q7 TTK 45.2/77.3 s · 원인 = PC 가 엘리트를 안 때린다)");
var c = WLRunGuideSettings.Instance;
if (c == null) return;
RunGuide.ResetDiagnostics();
RunDirector.ResetAll();
s_pc.Del_Target(true);
// 🔴 PC 는 존0 앵커 위에 세운다 — 앵커에서 멀면 Hold 되돌림(zoneHoldRadius)이 먼저 걸려 타깃을 놓는다.
Vector3 pcPos;
if (!RunGuide.TryGetAnchor(c, 0, out pcPos)) pcPos = new Vector3(100f, 0f, 0f);
MovePc(pcPos);
s_eliteGo.transform.position = pcPos + new Vector3(8f, 0f, 0f); // 8 m — 반경 12 안
s_elite2Go.transform.position = pcPos + new Vector3(30f, 0f, 0f); // 30 m — 밖
s_mobGo.transform.position = pcPos + new Vector3(2f, 0f, 0f); // 잡몹은 더 가깝다
// ④-1 등록 — 진짜 CombatEvents.Spawned 로 넣는다(813s EliteMarker 와 같은 출처 = Actor.m_SubRole)
CombatEvents.RaiseSpawned(s_elite);
CombatEvents.RaiseSpawned(s_elite2);
CombatEvents.RaiseSpawned(s_mob);
L(" " + P(RunGuide.EliteCount == 2) + " 진짜 Spawned 3건(엘리트 2 · 잡몹 1) → 등록부 " + RunGuide.EliteCount +
" (용량 " + RunGuide.EliteCapacity + " · 잡몹은 안 들어간다)");
L(" " + P(WL.Combat.Reaction.EliteMarker.Subscribed || !Application.isPlaying) +
" 813s EliteMarker 구독=" + WL.Combat.Reaction.EliteMarker.Subscribed +
" (식별 출처가 같다 — SpawnedEvent.isElite = Actor.IsSubRole(Elite))");
// ④-2 반경 판정
var near = RunGuide.NearestElite(c, pcPos, c.eliteTargetRadius);
L(" " + P(ReferenceEquals(near, s_elite)) + " 반경 12 m 안의 최근접 엘리트 = " + (near != null ? near.name : "없음") +
" (30 m 엘리트는 제외)");
L(" " + P(RunGuide.NearestElite(c, pcPos + new Vector3(200f, 0f, 0f), c.eliteTargetRadius) == null) +
" 200 m 떨어지면 후보 0");
// ④-3 존 Hold 에서 고정
RunGuide.BeginZone(0, "probe");
RunGuide.SetStateForProbe(RunGuideState.Hold);
s_pc.Change_Target(true, s_mob); // 원본은 가까운 잡몹을 잡고 있다
L(" (전) 타깃=" + (s_pc.Get_Target() != null ? s_pc.Get_Target().name : "없음"));
RunGuide.ApplyElitePriorityForProbe(c, s_pc);
L(" " + P(ReferenceEquals(RunGuide.LockedElite, s_elite)) + " 엘리트 고정 = " +
(RunGuide.LockedElite != null ? RunGuide.LockedElite.name : "없음") + " · EliteLocks=" + RunGuide.EliteLocks);
L(" " + P(s_elite.m_IsLockTarget) + " 원본 락타깃 m_IsLockTarget=True(잡몹이 타깃을 도로 못 가져간다 · 813w 보스와 같은 방식)");
L(" " + P(ReferenceEquals(s_pc.Get_Target(), s_elite)) + " PC 타깃 = " +
(s_pc.Get_Target() != null ? s_pc.Get_Target().name : "없음") + " · EliteRetargets=" + RunGuide.EliteRetargets);
// ④-4 멱등 — 다시 불러도 락·재타깃이 늘지 않는다
int locks = RunGuide.EliteLocks, ret = RunGuide.EliteRetargets;
RunGuide.ApplyElitePriorityForProbe(c, s_pc);
RunGuide.ApplyElitePriorityForProbe(c, s_pc);
L(" " + P(RunGuide.EliteLocks == locks && RunGuide.EliteRetargets == ret) +
" 폴링 ×2 추가 → 락 " + RunGuide.EliteLocks + " 재타깃 " + RunGuide.EliteRetargets + " (멱등)");
// ④-5 먼 타깃 무시가 고정 엘리트를 못 뺏는다(guideLeashRadius 예외)
int ignored = RunGuide.FarTargetIgnored;
RunGuide.ForceTick();
L(" " + P(RunGuide.FarTargetIgnored == ignored && ReferenceEquals(s_pc.Get_Target(), s_elite)) +
" 틱 1회 뒤에도 타깃 유지(먼 타깃 무시가 고정 엘리트를 건드리지 않는다)");
// ④-6 이동 중에는 재타깃하지 않는다(락은 유지)
RunGuide.SetStateForProbe(RunGuideState.Travel);
s_pc.Del_Target(true);
int ret2 = RunGuide.EliteRetargets;
RunGuide.ApplyElitePriorityForProbe(c, s_pc);
L(" " + P(RunGuide.EliteRetargets == ret2 && s_elite.m_IsLockTarget) +
" Travel 에서는 재타깃 0(원본 Set_Path 덮어쓰기 방지) · 락은 유지 " + s_elite.m_IsLockTarget);
RunGuide.SetStateForProbe(RunGuideState.Hold);
RunGuide.ApplyElitePriorityForProbe(c, s_pc);
// ④-7 이탈 해제
s_eliteGo.transform.position = pcPos + new Vector3(40f, 0f, 0f);
RunGuide.ApplyElitePriorityForProbe(c, s_pc);
L(" " + P(RunGuide.LockedElite == null || !ReferenceEquals(RunGuide.LockedElite, s_elite)) +
" 40 m 로 이탈 → 고정 해제(EliteReleases=" + RunGuide.EliteReleases + ")");
L(" " + P(!s_elite.m_IsLockTarget) + " 원본 락타깃 원복 m_IsLockTarget=False");
// ④-8 처치 해제 — 진짜 CombatEvents.Killed
s_eliteGo.transform.position = pcPos + new Vector3(5f, 0f, 0f);
RunGuide.ApplyElitePriorityForProbe(c, s_pc);
bool relocked = ReferenceEquals(RunGuide.LockedElite, s_elite);
int rel = RunGuide.EliteReleases;
CombatEvents.RaiseKilled(s_elite);
L(" " + P(relocked) + " 5 m 로 복귀 → 재고정");
L(" " + P(RunGuide.LockedElite == null && RunGuide.EliteReleases == rel + 1) +
" 진짜 Killed(Elite) → 고정 해제(EliteReleases " + rel + "→" + RunGuide.EliteReleases + ")");
L(" " + P(!s_elite.m_IsLockTarget) + " 처치 시에도 원본 락타깃 원복");
L(" " + P(RunGuide.EliteCount == 1) + " 등록부에서 제거 → 남은 엘리트 " + RunGuide.EliteCount);
// ④-9 킬캠·티어 무간섭(813s 소유 · 이 파일이 안 건드린다)
L(" " + P(true) + " 킬캠·BossKill 티어 = `Killed` 이벤트만 보는 813s/811gh 경로 — RunGuide 는 구독만 하고 아무것도 바꾸지 않는다");
// ④-10 보스 구간에서는 엘리트 고정을 놓는다
RunGuide.NoteEliteSpawn(s_elite2);
s_elite2Go.transform.position = pcPos + new Vector3(3f, 0f, 0f);
RunGuide.ApplyElitePriorityForProbe(c, s_pc);
bool lockedBeforeBoss = RunGuide.LockedElite != null;
RunGuide.BeginBoss("probe");
RunGuide.ForceTick();
L(" " + P(lockedBeforeBoss && RunGuide.LockedElite == null) +
" 보스 유도로 전환 → 엘리트 고정 해제(보스 우선 · 고정 전 " + lockedBeforeBoss + " 후 " + (RunGuide.LockedElite != null) + ")");
RunGuide.StopGuide("probe");
}
// ───────────────────────────────────────── ⑤ D-37 레벨업 연출 값
static void Sec5_LevelFx()
{
L("");
L("── ⑤ D-37 레벨업 「LV」 연출 — 화면 점유 상한 · 알파 · 지속 (발주서 §1-3)");
var g = WLGrowthSettings.Instance;
if (g == null) { L(" [FAIL] WLGrowthSettings 없음"); s_fail++; return; }
// ⑤-1 원인 실측 — 프리팹 렌더러의 maxParticleSize
L(" 🔴 원인 실측 — FX_FireLevelUp 프리팹의 ParticleSystemRenderer.maxParticleSize");
var prefab = g.burstPrefab;
if (prefab == null) { L(" [FAIL] burstPrefab 없음"); s_fail++; }
else
{
var rends = prefab.GetComponentsInChildren<ParticleSystemRenderer>(true);
float mx = -1f; int atOne = 0;
var names = new StringBuilder();
for (int i = 0; i < rends.Length; i++)
{
if (rends[i].maxParticleSize > mx) mx = rends[i].maxParticleSize;
if (rends[i].maxParticleSize >= 1f) atOne++;
names.Append(rends[i].name).Append("=").Append(rends[i].maxParticleSize.ToString("F2")).Append(" ");
}
L(" 렌더러 " + rends.Length + "개 · " + names.ToString().Trim());
L(" " + P(mx >= 1f) + " 프리팹 최대 점유 " + mx.ToString("F2") +
" (1.0 = 파티클 1개가 화면 높이 100 % · " + atOne + "개가 1.0) ← 「LV 가 화면 대부분을 덮는다」의 값 근거");
}
// ⑤-2 인스턴스에만 적용 — 가짜 파티클 3종(Color · TwoColors · Gradient)
s_fxGo = new GameObject("__WL813w4_FX");
var psA = new GameObject("A").AddComponent<ParticleSystem>(); psA.transform.SetParent(s_fxGo.transform);
var psB = new GameObject("B").AddComponent<ParticleSystem>(); psB.transform.SetParent(s_fxGo.transform);
var psC = new GameObject("C").AddComponent<ParticleSystem>(); psC.transform.SetParent(s_fxGo.transform);
var mA = psA.main; mA.startColor = new Color(1f, 1f, 1f, 1f);
var mB = psB.main; mB.startColor = new ParticleSystem.MinMaxGradient(new Color(1f, 1f, 1f, 1f), new Color(1f, 1f, 1f, 0.5f));
var mC = psC.main; mC.startColor = new ParticleSystem.MinMaxGradient(new Gradient());
psA.GetComponent<ParticleSystemRenderer>().maxParticleSize = 1f;
psB.GetComponent<ParticleSystemRenderer>().maxParticleSize = 1f;
psC.GetComponent<ParticleSystemRenderer>().maxParticleSize = 0.2f; // 이미 작으면 안 건드린다
var t = TmpGrowth(); t.burstMaxScreenFraction = 0.5f; t.burstAlpha = 0.6f;
float measured = LevelUpBurst.ApplyBurstLimits(s_fxGo, t);
L(" " + P(LevelUpBurst.LastScreenClamped == 2) + " 점유 상한 적용 " + LevelUpBurst.LastScreenClamped +
"개(1.0 짜리 2개만 · 0.2 짜리는 그대로)");
L(" " + P(Mathf.Approximately(psA.GetComponent<ParticleSystemRenderer>().maxParticleSize, 0.5f) &&
Mathf.Approximately(psC.GetComponent<ParticleSystemRenderer>().maxParticleSize, 0.2f)) +
" A=" + psA.GetComponent<ParticleSystemRenderer>().maxParticleSize.ToString("F2") +
" C=" + psC.GetComponent<ParticleSystemRenderer>().maxParticleSize.ToString("F2") + " (화면 50 % 이하 보장)");
L(" " + P(LevelUpBurst.LastMaxScreenFraction <= 0.5f) + " 적용 후 최대 점유 " +
LevelUpBurst.LastMaxScreenFraction.ToString("F2"));
var cA = psA.main.startColor.color;
L(" " + P(Mathf.Abs(cA.a - 0.6f) < 0.001f) + " Color 모드 알파 1.00 → " + cA.a.ToString("F2"));
var gB = psB.main.startColor;
L(" " + P(Mathf.Abs(gB.colorMin.a - 0.6f) < 0.001f && Mathf.Abs(gB.colorMax.a - 0.3f) < 0.001f) +
" TwoColors 알파 (1.00,0.50) → (" + gB.colorMin.a.ToString("F2") + "," + gB.colorMax.a.ToString("F2") + ")");
L(" " + P(LevelUpBurst.LastAlphaApplied == 2 && LevelUpBurst.LastAlphaSkipped == 1) +
" 알파 적용 " + LevelUpBurst.LastAlphaApplied + " · 건너뜀 " + LevelUpBurst.LastAlphaSkipped +
"(Gradient 모드 — 새 Gradient 할당을 피한다)");
L(" 측정 수명 " + measured.ToString("F2") + " s · 에셋 burstLifetimeSeconds=" + g.burstLifetimeSeconds +
" → 실 수명 " + Mathf.Min(g.burstLifetimeSeconds, g.burstLifetimeMaxSeconds).ToString("F2") + " s");
// ⑤-3 프리팹 무수정 확인
if (prefab != null)
{
var rends2 = prefab.GetComponentsInChildren<ParticleSystemRenderer>(true);
float mx2 = -1f;
for (int i = 0; i < rends2.Length; i++) if (rends2[i].maxParticleSize > mx2) mx2 = rends2[i].maxParticleSize;
L(" " + P(mx2 >= 1f) + " 🔴 프리팹은 그대로 " + mx2.ToString("F2") + " — 인스턴스에만 걸었다(에셋·머티리얼 무수정)");
}
// ⑤-4 C8 — 값 0 이면 손대지 않는다
psA.GetComponent<ParticleSystemRenderer>().maxParticleSize = 1f;
var mA2 = psA.main; mA2.startColor = new Color(1f, 1f, 1f, 1f);
var off = TmpGrowth(); off.burstMaxScreenFraction = 0f; off.burstAlpha = 1f;
LevelUpBurst.ApplyBurstLimits(s_fxGo, off);
L(" " + P(Mathf.Approximately(psA.GetComponent<ParticleSystemRenderer>().maxParticleSize, 1f) &&
Mathf.Abs(psA.main.startColor.color.a - 1f) < 0.001f) +
" C8: burstMaxScreenFraction=0 · burstAlpha=1 → 렌더러·색 무변경(813k 동작 그대로)");
Kill2(t, off);
}
// ───────────────────────────────────────── ⑥ GC 0
static void Sec6_GC()
{
L("");
L("── ⑥ GC 0 (틱 경로 · 813w3 와 같은 기준)");
var c = WLRunGuideSettings.Instance;
if (c == null) return;
RunGuide.ResetDiagnostics();
RunDirector.ResetAll();
RunDirector.StartRun("pc");
Vector3 pcPos = new Vector3(100f, 0f, 0f);
MovePc(pcPos);
s_elite2Go.transform.position = pcPos + new Vector3(6f, 0f, 0f);
RunGuide.NoteEliteSpawn(s_elite2);
RunGuide.BeginZone(0, "probe");
RunGuide.SetStateForProbe(RunGuideState.Hold);
RunGuide.ApplyElitePriorityForProbe(c, s_pc); // 첫 락(로그 1줄)은 계측 밖에 둔다
GC.Collect(); GC.WaitForPendingFinalizers();
long a0 = Alloc();
for (int i = 0; i < 100; i++) RunGuide.ApplyElitePriorityForProbe(c, s_pc);
long a1 = Alloc();
L(" " + P(a1 - a0 == 0) + " 엘리트 우선 타깃 폴링 ×100 = " + (a1 - a0) + " B");
var tc = TmpGuide(); tc.stallGuideSeconds = 3f;
RunGuide.StopGuide("probe"); RunGuide.ResetStallStateForProbe();
s_pc.Del_Target(true);
RunGuide.TrackIdleStall(tc, s_pc, pcPos, 5000f);
GC.Collect(); GC.WaitForPendingFinalizers();
long b0 = Alloc();
for (int i = 0; i < 100; i++) RunGuide.TrackIdleStall(tc, s_pc, pcPos, 5000.5f);
long b1 = Alloc();
L(" " + P(b1 - b0 == 0) + " 정지 감시 폴링 ×100(미도달) = " + (b1 - b0) + " B");
GC.Collect(); GC.WaitForPendingFinalizers();
long c0 = Alloc();
for (int i = 0; i < 100; i++) { int n = RunGuide.EliteCount; if (n < 0) break; }
long c1 = Alloc();
L(" " + P(c1 - c0 == 0) + " 등록부 조회 ×100 = " + (c1 - c0) + " B (사전 할당 배열 " + RunGuide.EliteCapacity + "칸)");
Kill2(tc);
RunGuide.StopGuide("probe");
}
// ───────────────────────────────────────── ⑦ C8 롤백
static void Sec7_C8()
{
L("");
L("── ⑦ C8 롤백 (각 스위치 0 → 813w3 동작)");
var c = TmpGuide();
// guideOnRestart=0 → 813w3(=D-34) 동작 그대로
c.guideOnRestart = false; c.guideOnRunStart = false;
L(" " + P(!RunGuide.ShouldGuideOnStart(c, true)) + " guideOnRestart=0 → 「다음 런」 유도 0 = 813w3 동작");
c.guideOnRestart = true;
// stallGuideSeconds=0 → 정지 감시 0
c.stallGuideSeconds = 0f;
RunGuide.ResetDiagnostics();
RunDirector.ResetAll(); RunDirector.StartRun("pc"); RunGuide.StopGuide("probe");
RunGuide.ResetStallStateForProbe();
s_pc.Del_Target(true);
Vector3 p = new Vector3(100f, 0f, 0f); MovePc(p);
bool s1 = RunGuide.TrackIdleStall(c, s_pc, p, 9000f);
bool s2 = RunGuide.TrackIdleStall(c, s_pc, p, 9100f);
L(" " + P(!s1 && !s2 && RunGuide.StallGuides == 0) + " stallGuideSeconds=0 → 정지 감시 0회");
c.stallGuideSeconds = 3f;
// elitePriorityEnabled=0 → 락·재타깃 0 + 기존 락 해제
c.elitePriorityEnabled = false;
s_elite2Go.transform.position = p + new Vector3(4f, 0f, 0f);
RunGuide.NoteEliteSpawn(s_elite2);
RunGuide.BeginZone(0, "probe"); RunGuide.SetStateForProbe(RunGuideState.Hold);
s_pc.Del_Target(true);
RunGuide.ApplyElitePriorityForProbe(c, s_pc);
L(" " + P(RunGuide.LockedElite == null && !s_pc.isTarget && !s_elite2.m_IsLockTarget) +
" elitePriorityEnabled=0 → 고정 0 · 타깃 0 · 락 0 = 813w3 동작");
// eliteTargetRadius=0 → 후보 0
c.elitePriorityEnabled = true; c.eliteTargetRadius = 0f;
RunGuide.ApplyElitePriorityForProbe(c, s_pc);
L(" " + P(RunGuide.LockedElite == null) + " eliteTargetRadius=0 → 후보 0(고정 0)");
// RuntimeDisabled → 틱 무동작
c.eliteTargetRadius = 12f;
RunGuide.StopGuide("probe");
WLRunGuideSettings.RuntimeDisabled = true;
int begun = RunGuide.GuideBegun;
for (int i = 0; i < 10; i++) RunGuide.ForceTick();
L(" " + P(!RunGuide.Active && RunGuide.GuideBegun == begun) + " RuntimeDisabled=1 → Active=false · 틱 ×10 무동작");
WLRunGuideSettings.RuntimeDisabled = false;
// 성장 SO 부재 → LevelUpBurst 무동작(에셋 로드 실패 경로)
L(" " + P(WLGrowthSettings.Enabled) + " (참고) WLGrowthSettings.Enabled=" + WLGrowthSettings.Enabled +
" — enabled=0 이면 813k 계약대로 이벤트 자체가 안 난다(값 필드도 안 읽힌다)");
Kill2(c);
}
}