Project_WL/AgentScripts/WL813w4_Probe.cs

610 lines
35 KiB
C#
Raw Normal View History

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