Project_WL/AgentScripts/WL811def_Probe.cs

681 lines
59 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.

// WL811def_Probe.cs — #811d/e/f 전투 연출 3종 검증 프로브 (읽기 전용 · 에셋 무수정 · 가짜 이벤트 발행)
//
// 헤드리스에서는 PlayFab 초기화가 실패해 인게임에 못 들어간다(811b 실측) → Play 의존 검증은 **Title Play 에서 가짜 이벤트**로 대체한다.
// 가짜 = RealCamera 1개(빈 GO) · MobActor/PCActor 컴포넌트(비활성 · Awake 만 · 실 몹 머티리얼 T_Aspethis_D = MK Toon) · CombatEvents.Raise* 직접 호출.
//
// unity command run_script --file AgentScripts/WL811def_Probe.cs --entry WL811def_Probe.Dump // 3 SO 값 · 구독 상태 (Edit/Play)
// unity command run_script --file AgentScripts/WL811def_Probe.cs --entry WL811def_Probe.Props // ⓔ 몹 프리팹 299 → 셰이더별 프로퍼티 실측 표 (Edit)
// unity command run_script --file AgentScripts/WL811def_Probe.cs --entry WL811def_Probe.KillCam // ⓑ 줌·슬로모 발동/복원 ms · 쿨 · 히트스톱 스킵 (Title Play)
// unity command run_script --file AgentScripts/WL811def_Probe.cs --entry WL811def_Probe.BossCam // ⓒ Spawned(isBoss) → 오빗 1.2 s (Title Play)
// unity command run_script --file AgentScripts/WL811def_Probe.cs --entry WL811def_Probe.ComboLead // 3타 리드 ×1.3 → 복원 (Title Play)
// unity command run_script --file AgentScripts/WL811def_Probe.cs --entry WL811def_Probe.Chain // ⓓ 3 s 안 2·3·5킬 티어 · 폭발 슬롯 ≤1 (Title Play)
// unity command run_script --file AgentScripts/WL811def_Probe.cs --entry WL811def_Probe.Merge // ⓓ 합산 윈도우 0.25 s (Title Play)
// unity command run_script --file AgentScripts/WL811def_Probe.cs --entry WL811def_Probe.Blink // ⓔ 10회 피격 → MPB on/off 20 · 머티리얼 인스턴스 0 · 잔류 0 (Title Play)
// unity command run_script --file AgentScripts/WL811def_Probe.cs --entry WL811def_Probe.GcCheck // ⓕ 구독자별 100회 GC 증가 (Title Play)
// unity command run_script --file AgentScripts/WL811def_Probe.cs --entry WL811def_Probe.Toggle // ⓖ 3 SO 런타임 off → 연출 0 (Title Play)
// unity command run_script --file AgentScripts/WL811def_Probe.cs --entry WL811def_Probe.Status // 카운터 전부
// unity command run_script --file AgentScripts/WL811def_Probe.cs --entry WL811def_Probe.FixCheck // 리뷰 FIX-1/2/3 회귀 (Title Play)
// unity command run_script --file AgentScripts/WL811def_Probe.cs --entry WL811def_Probe.Budget --args '[5.0]' // 병합 후 실전투(메인 에디터): ON/OFF fps·setPass 비교
// 결과: Screenshots_WL/WL811def/<이름>_<HHmmss>.txt + Console
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using UnityEngine.AI;
using WL.Combat.Core;
using WL.Combat.Reaction;
using WL.CombatCamera;
public static class WL811def_Probe
{
const string OutDir = "Screenshots_WL/WL811def";
const string SampleMat = "Assets/ResWork/Suriyun/Monster Pack Pyramid/Materials/Aspethis/T_Aspethis_D.mat";
const string DissolveMat = "Assets/ThirdParty/Dissolve Edge by Moonflower Carnivore/Materials/mat_DissolveEdge.mat";
public static void KillCam() { Launch(h => h.Co_KillCam()); }
public static void BossCam() { Launch(h => h.Co_BossCam()); }
public static void ComboLead() { Launch(h => h.Co_ComboLead()); }
public static void Chain() { Launch(h => h.Co_Chain()); }
public static void Merge() { Launch(h => h.Co_Merge()); }
public static void Blink() { Launch(h => h.Co_Blink()); }
public static void GcCheck() { Launch(h => h.Co_Gc()); }
public static void Toggle() { Launch(h => h.Co_Toggle()); }
public static void Budget(float seconds) { Launch(h => h.Co_Budget(seconds <= 0f ? 5f : seconds)); }
public static void FixCheck() { Launch(h => h.Co_FixCheck()); }
public static object Dump()
{
var sb = new StringBuilder();
sb.AppendLine("# WL811def Dump " + DateTime.Now.ToString("HH:mm:ss") + " playing=" + Application.isPlaying + " core=" + WLCombatCoreSettings.Enabled);
var cam = WLCombatCameraSettings.Instance; var re = WLReactionSettings.Instance; var bl = WLHitBlinkSettings.Instance;
sb.AppendLine("camera asset=" + (cam != null ? "found" : "NULL") + " Enabled=" + WLCombatCameraSettings.Enabled + " subscribed=" + CombatCameraDirector.Subscribed);
sb.AppendLine("reaction asset=" + (re != null ? "found" : "NULL") + " Enabled=" + WLReactionSettings.Enabled + " chainSub=" + KillChain.Subscribed + " mergeSub=" + DamageAggregator.Subscribed);
sb.AppendLine("blink asset=" + (bl != null ? "found" : "NULL") + " Enabled=" + WLHitBlinkSettings.Enabled + " subscribed=" + MobHitFlash.Subscribed);
sb.AppendLine("subs: Killed=" + CombatEvents.Killed.Count + " Spawned=" + CombatEvents.Spawned.Count + " ComboStage=" + CombatEvents.ComboStage.Count + " HitConfirmed=" + CombatEvents.HitConfirmed.Count);
var core = WLCombatCoreSettings.Instance;
if (core != null) sb.AppendLine("core: cinematicDefaultScale=" + core.cinematicDefaultScale + " killCamCooldownSeconds=" + core.killCamCooldownSeconds + " clusterBurstMax=" + core.clusterBurstMax + " dissolveMax=" + core.dissolveMax);
if (cam != null) sb.AppendLine("## WLCombatCameraSettings\n" + JsonUtility.ToJson(cam, true));
if (re != null) sb.AppendLine("## WLReactionSettings\n" + JsonUtility.ToJson(re, true));
if (bl != null) sb.AppendLine("## WLHitBlinkSettings\n" + JsonUtility.ToJson(bl, true));
Save("dump", sb); return sb.ToString();
}
/// <summary>ⓔ 몹 프리팹 전수(Res_Addr/Mobs) → 비파티클 렌더러의 셰이더별 프로퍼티 존재(Shader.FindPropertyIndex) 표. Edit 모드.</summary>
public static object Props()
{
var sb = new StringBuilder();
sb.AppendLine("# WL811def Props " + DateTime.Now.ToString("HH:mm:ss"));
string[] names = { "_AlbedoColor", "_BaseColor", "_Color", "_EmissionColor", "_EdgeAroundHDR", "_EdgeAround", "_Progress" };
var byShader = new Dictionary<string, int[]>(); // [rendererCount, prefabCount, prop0..]
var lastPrefab = new Dictionary<string, string>();
int prefabs = 0, renderers = 0, particle = 0;
foreach (var folder in new[] { "Assets/Res_Addr/Mobs/Mob", "Assets/Res_Addr/Mobs/Elite", "Assets/Res_Addr/Mobs/FieldBoss" })
{
var guids = UnityEditor.AssetDatabase.FindAssets("t:Prefab", new[] { folder });
foreach (var g in guids)
{
var path = UnityEditor.AssetDatabase.GUIDToAssetPath(g);
var go = UnityEditor.AssetDatabase.LoadAssetAtPath<GameObject>(path);
if (go == null) continue;
prefabs++;
var seen = new HashSet<string>();
foreach (var r in go.GetComponentsInChildren<Renderer>(true))
{
if (r is ParticleSystemRenderer) { particle++; continue; }
renderers++;
var m = r.sharedMaterial; var sh = m != null ? m.shader : null;
string key = sh != null ? sh.name : "(null shader)";
int[] row;
if (!byShader.TryGetValue(key, out row))
{
row = new int[2 + names.Length];
if (sh != null) for (int i = 0; i < names.Length; i++) row[2 + i] = sh.FindPropertyIndex(names[i]) >= 0 ? 1 : 0;
byShader[key] = row;
}
row[0]++;
if (seen.Add(key)) { row[1]++; lastPrefab[key] = System.IO.Path.GetFileNameWithoutExtension(path); }
}
}
}
sb.AppendLine("prefabs=" + prefabs + " renderers(non-particle)=" + renderers + " particleRenderers(skipped)=" + particle);
sb.AppendLine(string.Format("{0,-45} {1,5} {2,5} | {3}", "shader", "rend", "pref", string.Join(" ", names)));
foreach (var kv in byShader)
{
var row = kv.Value; var cols = new StringBuilder();
for (int i = 0; i < names.Length; i++) cols.Append(string.Format("{0,-" + names[i].Length + "} ", row[2 + i] == 1 ? "Y" : "-"));
sb.AppendLine(string.Format("{0,-45} {1,5} {2,5} | {3} e.g. {4}", kv.Key, row[0], row[1], cols.ToString().TrimEnd(), lastPrefab[kv.Key]));
}
var dis = UnityEditor.AssetDatabase.LoadAssetAtPath<Material>(DissolveMat);
if (dis != null && dis.shader != null)
{
var cols = new StringBuilder();
for (int i = 0; i < names.Length; i++) cols.Append(names[i] + "=" + (dis.shader.FindPropertyIndex(names[i]) >= 0 ? "Y" : "-") + " ");
sb.AppendLine("dissolve mat=" + dis.name + " shader=" + dis.shader.name + " | " + cols + "| _EdgeAroundHDR=" + dis.GetFloat("_EdgeAroundHDR") + " _EdgeAround=" + dis.GetFloat("_EdgeAround"));
}
var found = Shader.Find(WLHitBlinkSettings.Instance != null ? WLHitBlinkSettings.Instance.dissolveShaderName : "MoonflowerCarnivore/Dissolve Edge");
sb.AppendLine("Shader.Find(dissolveShaderName)=" + (found != null ? found.name : "NULL"));
Save("props", sb); return sb.ToString();
}
public static object Status()
{
var sb = new StringBuilder();
sb.AppendLine("# WL811def Status " + DateTime.Now.ToString("HH:mm:ss") + " timeScale=" + Time.timeScale.ToString("F2") + " arbiter=" + TimeScaleArbiter.Active);
sb.AppendLine("camera: killCam=" + CombatCameraDirector.KillCamCount + " skip(role/cool/runcap/guard/nocam/busy)=" + CombatCameraDirector.KillCamSkippedRole + "/" + CombatCameraDirector.KillCamSkippedCooldown + "/" + CombatCameraDirector.KillCamSkippedRunCap + "/" + CombatCameraDirector.KillCamSkippedGuard + "/" + CombatCameraDirector.KillCamSkippedNoCam + "/" + CombatCameraDirector.KillCamSkippedBusy
+ " abandoned=" + CombatCameraDirector.KillCamAbandoned + " rebased=" + CombatCameraDirector.KillCamRebased + " pending=" + CombatCameraDirector.KillCamPending
+ " boss=" + CombatCameraDirector.BossCamCount + " bossSkip=" + CombatCameraDirector.BossCamSkipped + " preset(applied/restored/abandoned/active)=" + CombatCameraDirector.BossPresetApplied + "/" + CombatCameraDirector.BossPresetRestored + "/" + CombatCameraDirector.BossPresetAbandoned + "/" + CombatCameraDirector.BossPresetActive + " lead=" + CombatCameraDirector.ComboLeadCount
+ " active(kill/boss)=" + CombatCameraDirector.KillCamActive + "/" + CombatCameraDirector.BossCamActive + " zoom=" + CombatCameraDirector.CurrentZoomScale.ToString("F3") + " lastKillCam=" + CombatCameraDirector.LastKillCamMeasured.ToString("F3") + " lastBoss=" + CombatCameraDirector.LastBossCamMeasured.ToString("F3") + " slowmo=" + CombatCameraDirector.LastSlowMoResult + " run=" + CombatCameraDirector.RunCount + " last=" + CombatCameraDirector.LastInfo);
sb.AppendLine("chain: kills=" + KillChain.KillCount + " tiers=" + KillChain.TierCount + " chains=" + KillChain.ChainCount + " best=" + KillChain.BestChainEver + " cur=" + KillChain.ChainKills + " burst=" + KillChain.BurstCount + " denied=" + KillChain.BurstDeniedBudget + " outOfView=" + KillChain.BurstSkippedView + " noInfo=" + KillChain.BurstNoInGameInfo + " held=" + KillChain.BurstHeld + " budget=" + EffectBudget.Count(EffectBudgetKind.ClusterBurst) + "/" + EffectBudget.Limit(EffectBudgetKind.ClusterBurst) + " last=" + KillChain.LastInfo);
sb.AppendLine("merge: hits=" + DamageAggregator.HitCount + " flush=" + DamageAggregator.FlushCount + " maxMerged=" + DamageAggregator.MaxHitsMerged + " evict=" + DamageAggregator.EvictCount + " active=" + DamageAggregator.ActiveSlots + " reentrant=" + DamageAggregator.ReentrantHits + " dispatching=" + DamageAggregator.Dispatching);
sb.AppendLine("blink: on=" + MobHitFlash.OnCount + " off=" + MobHitFlash.OffCount + " extend=" + MobHitFlash.ExtendCount + " throttled=" + MobHitFlash.ThrottledCount + " tint=" + MobHitFlash.TintCount + " clear=" + MobHitFlash.ClearCount + " skipFilter=" + MobHitFlash.SkippedFilter + " noProp=" + MobHitFlash.SkippedNoProperty + " states=" + MobHitFlash.StateCount + " active=" + MobHitFlash.ActiveCount + " reflect=" + MobHitFlash.RendererFieldViaReflection + " dissolve(resolved/hdr/range)=" + MobHitFlash.DissolveResolved + "/" + MobHitFlash.DissolveHasHdr + "/" + MobHitFlash.DissolveHasRange);
sb.AppendLine("lead multiplier=" + CameraMoveLead.LeadMultiplier.ToString("F2") + " runner(reaction)=" + WLReactionRunner.Exists);
Save("status", sb); return sb.ToString();
}
// ───────────────────────────────────────────── 공용
static void Save(string name, StringBuilder sb)
{
try
{
System.IO.Directory.CreateDirectory(OutDir);
string path = System.IO.Path.Combine(OutDir, name + "_" + DateTime.Now.ToString("HHmmss") + ".txt");
System.IO.File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false));
Debug.Log("[WL811def] 저장 " + path);
}
catch (Exception e) { Debug.LogWarning("[WL811def] 저장 실패 " + e.Message); }
Debug.Log(sb.ToString());
}
static void Launch(Func<Host, IEnumerator> make)
{
if (!Application.isPlaying) { Debug.LogWarning("[WL811def] Play 모드에서만(Title Play). Edit 모드는 Dump/Props/Status 만."); return; }
var old = GameObject.Find("__wl811def_probe");
if (old != null) UnityEngine.Object.DestroyImmediate(old);
var go = new GameObject("__wl811def_probe");
var h = go.AddComponent<Host>();
h.StartCoroutine(make(h));
}
/// <summary>가짜 몹: 비활성 GO 에 컴포넌트를 얹고(NavMeshAgent 는 비활성 → 네비메시 에러 0) 활성화해 Awake(arr_Renderer 캐시)만 돌린다. Start/Update 없음.</summary>
static MobActor MakeMob(string name, eSubRol sub, Vector3 pos, Material mat)
{
var go = new GameObject(name);
go.SetActive(false);
go.transform.position = pos;
go.AddComponent<Rigidbody>().isKinematic = true;
go.AddComponent<BoxCollider>();
go.AddComponent<Animator>();
var agent = go.AddComponent<NavMeshAgent>(); agent.enabled = false;
var cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
cube.name = "Body"; cube.transform.SetParent(go.transform, false);
UnityEngine.Object.Destroy(cube.GetComponent<Collider>());
if (mat != null) cube.GetComponent<MeshRenderer>().sharedMaterial = mat;
var mob = go.AddComponent<MobActor>(); mob.enabled = false;
mob.m_SubRole = sub;
go.SetActive(true);
InitCC(mob);
return mob;
}
/// <summary>실 액터는 Set/On_Regen 이 Init_CC 로 dic_ccData 를 채운다 — 가짜는 리플렉션으로 같은 메서드를 1회 호출(Get_MoveSpeed 의 KeyNotFound 방지 · 1차 실측 76건).</summary>
static void InitCC(Actor a)
{
var m = typeof(Actor).GetMethod("Init_CC", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public);
if (m != null) m.Invoke(a, null); else Debug.LogWarning("[WL811def] Init_CC 미발견");
}
static PCActor MakePC(string name, Vector3 pos)
{
var go = new GameObject(name);
go.SetActive(false);
go.transform.position = pos;
go.AddComponent<Rigidbody>().isKinematic = true;
go.AddComponent<BoxCollider>();
go.AddComponent<Animator>();
var agent = go.AddComponent<NavMeshAgent>(); agent.enabled = false;
var cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
cube.transform.SetParent(go.transform, false);
UnityEngine.Object.Destroy(cube.GetComponent<Collider>());
var pc = go.AddComponent<PCActor>(); pc.enabled = false;
go.SetActive(true);
InitCC(pc);
return pc;
}
/// <summary>진단용 읽기 — RealCamera.list_tfCamactionTarget(private · 인트로 CamStatus 0 에서만 소비)의 길이 확인(FIX-3).</summary>
static List<Transform> CamActionList(RealCamera cam)
{
var f = typeof(RealCamera).GetField("list_tfCamactionTarget", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public);
return f != null && cam != null ? f.GetValue(cam) as List<Transform> : null;
}
static Material LoadSampleMat()
{
return UnityEditor.AssetDatabase.LoadAssetAtPath<Material>(SampleMat); // 에디터 전용 프로브(Roslyn run_script 에 UNITY_EDITOR 심볼이 없을 수 있어 #if 미사용)
}
static DamageInfo MakeDamage(double dmg, bool cri, bool kill)
{
var d = new DamageInfo { Beater = null, Critical = cri ? eStat.CRI : eStat.None, IsDeadByThisDamage = kill };
d.Damage = dmg; d.BaseDmg = dmg;
return d;
}
public class Host : MonoBehaviour
{
StringBuilder _sb; float _t0;
RealCamera _cam; Transform _dummy; bool _camInstalled; RealCamera _prevCam;
readonly List<GameObject> _trash = new List<GameObject>();
string Pre() { return string.Format("{0,7:F0} | f{1,-5} | ts={2:F2} | ", (Time.unscaledTime - _t0) * 1000f, Time.frameCount, Time.timeScale); }
static string N(UnityEngine.Object o) { return o != null ? o.name : "null"; }
IEnumerator Wait(float s) { float t = Time.unscaledTime; while (Time.unscaledTime - t < s) yield return null; }
Vector3 Front(float dist)
{
var c = Camera.main;
return c != null ? c.transform.position + c.transform.forward * dist : Vector3.zero;
}
/// <summary>가짜 RealCamera(빈 GO · 카메라 컴포넌트 없음): 원본 프리팹 [0] 값(3/3/2/78 → 기준서 65) · CamStatus 기본 1 · Set_Target(1인자)로 Update_Cam 만 돈다.</summary>
void InstallCamera()
{
_prevCam = MyValue._RealCamera;
var go = new GameObject("__wl811def_cam"); _trash.Add(go);
_cam = go.AddComponent<RealCamera>();
_cam.arr_cCamData = new[] { new cCamData { minDistance = 3f, maxDistance = 3f, DistanceUp = 2f, cameraHeight = 65f }, new cCamData { minDistance = 2f, maxDistance = 3f, DistanceUp = 3.5f, cameraHeight = 78f } };
_cam.minDistance = 3f; _cam.maxDistance = 3f; _cam.DistanceUp = 2f; _cam.cameraHeight = 65f; _cam.rotateAround = 30f; _cam.smooth = 4f;
var d = new GameObject("__wl811def_pc"); _trash.Add(d); d.transform.position = Front(6f);
_dummy = d.transform;
_cam.Set_Target(_dummy);
MyValue._RealCamera = _cam; _camInstalled = true;
}
void Cleanup()
{
CombatCameraDirector.ReleaseAll();
MobHitFlash.RestoreAll();
KillChain.ClearAll(); DamageAggregator.ClearAll();
if (_camInstalled) { MyValue._RealCamera = _prevCam; _camInstalled = false; }
TimeScaleArbiter.ReleaseAll(); Time.timeScale = 1f;
for (int i = 0; i < _trash.Count; i++) if (_trash[i] != null) Destroy(_trash[i]);
_trash.Clear();
Destroy(gameObject);
}
string CamRow() { return "min=" + _cam.minDistance.ToString("F3") + " max=" + _cam.maxDistance.ToString("F3") + " up=" + _cam.DistanceUp.ToString("F3") + " yaw=" + _cam.rotateAround.ToString("F2") + " target=" + N(_cam.target) + " zoom=" + CombatCameraDirector.CurrentZoomScale.ToString("F3") + " arb=" + TimeScaleArbiter.Active + " camPos=" + _cam.transform.position.ToString("F2"); }
/// <summary>ⓑ 킬캠: 엘리트 Killed → 줌(3→2.25→3)·슬로모(0.35→1) 타임라인 · 히트스톱 SkippedHigher · 쿨 안 재발동 0 · ResetRun 뒤 재발동 · 일반 몹 = 스킵.</summary>
public IEnumerator Co_KillCam()
{
_sb = new StringBuilder(); _t0 = Time.unscaledTime;
_sb.AppendLine("# WL811def KillCam " + DateTime.Now.ToString("HH:mm:ss") + " Enabled=" + WLCombatCameraSettings.Enabled + " core=" + WLCombatCoreSettings.Enabled + " arbiter=" + TimeScaleArbiter.Enabled);
var st = WLCombatCameraSettings.Instance; var core = WLCombatCoreSettings.Instance;
_sb.AppendLine("# 값: zoom=" + st.killCamZoomScale + " in/hold/out=" + st.killCamZoomInSeconds + "/" + st.killCamHoldSeconds + "/" + st.killCamZoomOutSeconds + " slowmo=" + core.cinematicDefaultScale + "×" + st.killCamSlowMoSeconds + " s cool=" + core.killCamCooldownSeconds + " guardY=" + st.killCamBottomGuardViewportY + " maxPerRun=" + st.killCamMaxPerRun);
CombatCameraDirector.ResetDiagnostics(); CombatCameraDirector.ResetRun(); TimeScaleArbiter.ResetDiagnostics();
InstallCamera();
yield return null; yield return null;
var elite = MakeMob("__wl811def_elite", eSubRol.Elite, Front(5f), null); _trash.Add(elite.gameObject);
var normal = MakeMob("__wl811def_normal", eSubRol.None, Front(5f), null); _trash.Add(normal.gameObject);
yield return null;
_sb.AppendLine(Pre() + "before " + CamRow());
// ① 일반 몹 → 역할 스킵
CombatEvents.RaiseKilled(normal);
_sb.AppendLine(Pre() + "① Normal Killed → killCam=" + CombatCameraDirector.KillCamCount + " skipRole=" + CombatCameraDirector.KillCamSkippedRole + " (기대 0 / 1)");
// ② 엘리트 → 발동 · 타임라인
float minSeen = 99f, tsMin = 9f; float zoomReach = -1f, tsRestore = -1f, camRestore = -1f;
float tStart = Time.unscaledTime;
CombatEvents.RaiseKilled(elite);
_sb.AppendLine(Pre() + "② Elite Killed → killCam=" + CombatCameraDirector.KillCamCount + " slowmo=" + CombatCameraDirector.LastSlowMoResult + " viewportY=" + CombatCameraDirector.LastVictimViewportY.ToString("F2") + " last=" + CombatCameraDirector.LastInfo + " " + CamRow());
bool hsTried = false; ArbiterResult hs = ArbiterResult.Invalid;
float tEnd = tStart + st.killCamZoomInSeconds + st.killCamHoldSeconds + st.killCamZoomOutSeconds + 0.4f;
int rows = 0;
while (Time.unscaledTime < tEnd)
{
yield return null;
float t = Time.unscaledTime - tStart;
if (!hsTried && t > 0.08f) { hs = TimeScaleArbiter.Request(TimeLayer.HitStop, 0f, 0.04f); hsTried = true; _sb.AppendLine(Pre() + " 슬로모 중 HitStop 요청 → " + hs + " (" + TimeScaleArbiter.LastInfo + ") 기대=SkippedHigher"); }
minSeen = Mathf.Min(minSeen, _cam.minDistance); tsMin = Mathf.Min(tsMin, Time.timeScale);
if (zoomReach < 0f && Mathf.Approximately(_cam.minDistance, 3f * st.killCamZoomScale)) zoomReach = t;
if (tsRestore < 0f && tsMin < 0.99f && Time.timeScale >= 0.999f) tsRestore = t;
if (camRestore < 0f && zoomReach >= 0f && !CombatCameraDirector.KillCamActive) camRestore = t;
if ((rows++ % 4) == 0 || !CombatCameraDirector.KillCamActive) _sb.AppendLine(Pre() + " " + CamRow());
if (camRestore >= 0f && tsRestore >= 0f && t > 0.9f) break;
}
_sb.AppendLine(Pre() + " 결과: minDistance 최소=" + minSeen.ToString("F3") + " (기대 " + (3f * st.killCamZoomScale).ToString("F3") + ") 줌 도달 " + (zoomReach * 1000f).ToString("F0") + " ms · timeScale 최소=" + tsMin.ToString("F2") + " 복원 " + (tsRestore * 1000f).ToString("F0") + " ms(기대 ≈" + (st.killCamSlowMoSeconds * 1000f).ToString("F0") + ") · 카메라 복원 " + (camRestore * 1000f).ToString("F0") + " ms · measured=" + CombatCameraDirector.LastKillCamMeasured.ToString("F3") + " s · abandoned=" + CombatCameraDirector.KillCamAbandoned + " · 종료 " + CamRow() + " arbiterMeasured=" + TimeScaleArbiter.LastMeasured.ToString("F3"));
// ③ 쿨 안 재발동 → 스킵
CombatEvents.RaiseKilled(elite);
_sb.AppendLine(Pre() + "③ 쿨 안 Elite Killed → killCam=" + CombatCameraDirector.KillCamCount + " skipCooldown=" + CombatCameraDirector.KillCamSkippedCooldown + " (기대 1 / 1) last=" + CombatCameraDirector.LastInfo);
// ④ ResetRun(쿨·런 초기화) → 재발동 · 보스 킬
var boss = MakeMob("__wl811def_boss", eSubRol.Boss, Front(5f), null); _trash.Add(boss.gameObject);
CombatCameraDirector.ResetRun();
CombatEvents.RaiseKilled(boss);
_sb.AppendLine(Pre() + "④ ResetRun 후 Boss Killed → killCam=" + CombatCameraDirector.KillCamCount + " (기대 2) slowmo=" + CombatCameraDirector.LastSlowMoResult + " " + CamRow());
yield return Wait(st.killCamZoomInSeconds + st.killCamHoldSeconds + st.killCamZoomOutSeconds + 0.3f);
_sb.AppendLine(Pre() + " 복원 확인 " + CamRow() + " active=" + CombatCameraDirector.KillCamActive + " measured=" + CombatCameraDirector.LastKillCamMeasured.ToString("F3"));
// ⑤ 런 상한: ResetRun 뒤 쿨만 매번 지우며(ResetCooldown) maxPerRun 회 발동 → 다음은 runcap 스킵
int cap = st.killCamMaxPerRun;
if (cap > 0)
{
CombatCameraDirector.ResetRun();
int before = CombatCameraDirector.KillCamCount, cool0 = CombatCameraDirector.KillCamSkippedCooldown;
for (int i = 0; i < cap; i++) { CombatCameraDirector.ResetCooldown(); CombatEvents.RaiseKilled(elite); CombatCameraDirector.ReleaseAll(); TimeScaleArbiter.ReleaseAll(); yield return null; }
CombatCameraDirector.ResetCooldown();
CombatEvents.RaiseKilled(elite);
_sb.AppendLine(Pre() + "⑤ 런 상한 " + cap + ": 발동+" + (CombatCameraDirector.KillCamCount - before) + " (기대 " + cap + ") skipRunCap=" + CombatCameraDirector.KillCamSkippedRunCap + " (기대 1) skipCool+" + (CombatCameraDirector.KillCamSkippedCooldown - cool0) + " (기대 0) run=" + CombatCameraDirector.RunCount);
}
_sb.AppendLine("# 종료 timeScale=" + Time.timeScale.ToString("F2") + " arbiter=" + TimeScaleArbiter.Active + " " + CamRow());
Save("killcam", _sb); Cleanup();
}
/// <summary>ⓒ 보스 캠: Spawned(isBoss) → Set_Target(보스) · sin 오빗 · 거리 배율 → 1.2 s 뒤 전부 복원 · arr_cCamData[1] 프리셋.</summary>
public IEnumerator Co_BossCam()
{
_sb = new StringBuilder(); _t0 = Time.unscaledTime;
var st = WLCombatCameraSettings.Instance;
_sb.AppendLine("# WL811def BossCam " + DateTime.Now.ToString("HH:mm:ss") + " Enabled=" + WLCombatCameraSettings.Enabled + " sec=" + st.bossCamSeconds + " orbit=" + st.bossCamOrbitDegrees + "° dist×" + st.bossCamDistanceScale + " retarget=" + st.bossCamRetarget + " preset=" + st.bossPresetApply + "(" + st.bossPresetDistance + " m · " + st.bossPresetCameraHeight + "°)");
CombatCameraDirector.ResetDiagnostics();
InstallCamera();
yield return null; yield return null;
var boss = MakeMob("__wl811def_boss", eSubRol.Boss, Front(5f) + Vector3.right * 2f, null); _trash.Add(boss.gameObject);
var normal = MakeMob("__wl811def_normal", eSubRol.None, Front(5f), null); _trash.Add(normal.gameObject);
yield return null;
float yaw0 = _cam.rotateAround; var preset0 = _cam.arr_cCamData[1];
_sb.AppendLine(Pre() + "before " + CamRow() + " preset[1]=" + preset0.minDistance + "/" + preset0.maxDistance + "/" + preset0.cameraHeight + "°");
CombatEvents.RaiseSpawned(normal);
_sb.AppendLine(Pre() + "① Normal Spawned → boss=" + CombatCameraDirector.BossCamCount + " (기대 0)");
float tStart = Time.unscaledTime; float yawMax = -999f, yawMin = 999f; bool sawBossTarget = false;
CombatEvents.RaiseSpawned(boss);
_sb.AppendLine(Pre() + "② Boss Spawned → boss=" + CombatCameraDirector.BossCamCount + " active=" + CombatCameraDirector.BossCamActive + " preset[1]=" + preset0.minDistance + "/" + preset0.maxDistance + "/" + preset0.cameraHeight + "° up=" + preset0.DistanceUp + " " + CamRow());
int rows = 0; float tRestore = -1f;
while (Time.unscaledTime - tStart < st.bossCamSeconds + 0.5f)
{
yield return null;
float rel = Mathf.DeltaAngle(yaw0, _cam.rotateAround);
yawMax = Mathf.Max(yawMax, rel); yawMin = Mathf.Min(yawMin, rel);
if (ReferenceEquals(_cam.target, boss.transform)) sawBossTarget = true;
if (tRestore < 0f && !CombatCameraDirector.BossCamActive) tRestore = Time.unscaledTime - tStart;
if ((rows++ % 6) == 0 || tRestore >= 0f && rows % 6 == 1) _sb.AppendLine(Pre() + " yawRel=" + rel.ToString("F2") + " " + CamRow());
if (tRestore >= 0f && Time.unscaledTime - tStart > st.bossCamSeconds + 0.2f) break;
}
_sb.AppendLine(Pre() + " 결과: yaw 진폭 max=" + yawMax.ToString("F2") + " min=" + yawMin.ToString("F2") + " (기대 ≈+" + st.bossCamOrbitDegrees + ") · 종료 yawRel=" + Mathf.DeltaAngle(yaw0, _cam.rotateAround).ToString("F3") + " (기대 0) · 보스 타겟 관측=" + sawBossTarget + " · 복원 " + (tRestore * 1000f).ToString("F0") + " ms(기대 ≈" + (st.bossCamSeconds * 1000f).ToString("F0") + ") · measured=" + CombatCameraDirector.LastBossCamMeasured.ToString("F3") + " · target 복원=" + ReferenceEquals(_cam.target, _dummy) + " · " + CamRow());
Save("bosscam", _sb); Cleanup();
}
/// <summary>3타 리드: ComboStage(isLast) → LeadMultiplier 1.3 → comboLeadSeconds 뒤 1.</summary>
public IEnumerator Co_ComboLead()
{
_sb = new StringBuilder(); _t0 = Time.unscaledTime;
var st = WLCombatCameraSettings.Instance;
_sb.AppendLine("# WL811def ComboLead " + DateTime.Now.ToString("HH:mm:ss") + " ×" + st.comboLeadMultiplier + " " + st.comboLeadSeconds + " s");
var pc = MakePC("__wl811def_pcactor", Front(5f)); _trash.Add(pc.gameObject);
yield return null;
CombatEvents.RaiseComboStage(pc, 2, "Attack2_S", 0f, false);
_sb.AppendLine(Pre() + "① stage2(isLast=false) → mult=" + CameraMoveLead.LeadMultiplier.ToString("F2") + " (기대 1.00) count=" + CombatCameraDirector.ComboLeadCount);
float t0 = Time.unscaledTime;
CombatEvents.RaiseComboStage(pc, 3, "Attack3_S", 0f, true);
_sb.AppendLine(Pre() + "② stage3(isLast) → mult=" + CameraMoveLead.LeadMultiplier.ToString("F2") + " (기대 " + st.comboLeadMultiplier.ToString("F2") + ") count=" + CombatCameraDirector.ComboLeadCount);
float tBack = -1f;
while (Time.unscaledTime - t0 < st.comboLeadSeconds + 0.3f) { yield return null; if (tBack < 0f && Mathf.Approximately(CameraMoveLead.LeadMultiplier, 1f)) tBack = Time.unscaledTime - t0; }
_sb.AppendLine(Pre() + " 복원 " + (tBack * 1000f).ToString("F0") + " ms (기대 ≈" + (st.comboLeadSeconds * 1000f).ToString("F0") + ") mult=" + CameraMoveLead.LeadMultiplier.ToString("F2"));
Save("combolead", _sb); Cleanup();
}
int _tier, _ended; string _tierLog = "";
void OnTier(in KillChainTierEvent e) { _tier++; _tierLog += Pre() + " TierReached " + e.name + " tier=" + e.tier + " kills=" + e.kills + " centroid=" + e.centroid.ToString("F1") + " burst=" + e.burstSpawned + "\n"; }
void OnEnded(in KillChainEndedEvent e) { _ended++; _tierLog += Pre() + " Ended kills=" + e.kills + " best=" + e.bestTier + " dur=" + e.duration.ToString("F2") + "\n"; }
/// <summary>ⓓ 연쇄: 3 s 안 3킬 → DOUBLE·TRIPLE · 폭발 슬롯 1(홀드) · 윈도우 만료 Ended · 5킬 RAMPAGE · 홀드 중 폭발 거부.</summary>
public IEnumerator Co_Chain()
{
_sb = new StringBuilder(); _t0 = Time.unscaledTime; _tier = 0; _ended = 0; _tierLog = "";
var st = WLReactionSettings.Instance;
_sb.AppendLine("# WL811def Chain " + DateTime.Now.ToString("HH:mm:ss") + " Enabled=" + WLReactionSettings.Enabled + " window=" + st.killChainWindowSeconds + " burstMinTier=" + st.clusterBurstMinTier + " prefab=" + st.clusterBurstPrefab + " hold=" + st.clusterBurstHoldSeconds + " InGameInfo=" + (InGameInfo.Ins != null));
KillChain.ResetDiagnostics(); KillChain.ClearAll(); EffectBudget.ResetAll();
KillChain.TierReached.Add(OnTier); KillChain.Ended.Add(OnEnded);
var mobs = new MobActor[5];
for (int i = 0; i < 5; i++) { mobs[i] = MakeMob("__wl811def_mob" + i, eSubRol.None, Front(5f) + Vector3.right * (i - 2), null); _trash.Add(mobs[i].gameObject); }
yield return null;
for (int i = 0; i < 3; i++) { CombatEvents.RaiseKilled(mobs[i]); yield return Wait(0.15f); }
_sb.AppendLine(Pre() + "① 0.45 s 안 3킬 → tiers=" + _tier + " (기대 2: DOUBLE·TRIPLE) chain=" + KillChain.ChainKills + " burstHeld=" + KillChain.BurstHeld + " budget=" + EffectBudget.Count(EffectBudgetKind.ClusterBurst) + " burst/noInfo/denied=" + KillChain.BurstCount + "/" + KillChain.BurstNoInGameInfo + "/" + KillChain.BurstDeniedBudget);
_sb.Append(_tierLog); _tierLog = "";
yield return Wait(st.clusterBurstHoldSeconds + 0.1f);
_sb.AppendLine(Pre() + " 홀드 만료 → burstHeld=" + KillChain.BurstHeld + " budget=" + EffectBudget.Count(EffectBudgetKind.ClusterBurst) + " (기대 false/0)");
yield return Wait(st.killChainWindowSeconds - st.clusterBurstHoldSeconds);
_sb.AppendLine(Pre() + "② 윈도우 만료 → ended=" + _ended + " (기대 1) chain=" + KillChain.ChainKills + " (기대 0)");
_sb.Append(_tierLog); _tierLog = "";
int denied0 = KillChain.BurstDeniedBudget;
for (int i = 0; i < 5; i++) { CombatEvents.RaiseKilled(mobs[i]); yield return null; }
_sb.AppendLine(Pre() + "③ 5킬 연속 → tiers+=" + _tier + " (누적 기대 5) best=" + KillChain.BestChainEver + " burstDenied+" + (KillChain.BurstDeniedBudget - denied0) + " (TRIPLE 슬롯 홀드 중 RAMPAGE 폭발 거부 = 동시 ≤1) budget=" + EffectBudget.Count(EffectBudgetKind.ClusterBurst));
_sb.Append(_tierLog);
KillChain.TierReached.Remove(OnTier); KillChain.Ended.Remove(OnEnded);
Save("chain", _sb); Cleanup();
}
int _flush; string _flushLog = "";
void OnFlush(in DamageMergeEvent e) { _flush++; _flushLog += Pre() + " Flushed victim=" + N(e.victim) + " total=" + e.total.ToString("F0") + " hits=" + e.hits + " max=" + e.maxSingle.ToString("F0") + " cri=" + e.anyCritical + " kill=" + e.isKill + " window=" + ((e.time - e.firstTime) * 1000f).ToString("F0") + " ms\n"; }
/// <summary>ⓓ 합산: 0.25 s 안 3타(10+20+30 · 2타 크리) → 플러시 1(60 · 3타) · 처치 타격 → 즉시 플러시.</summary>
public IEnumerator Co_Merge()
{
_sb = new StringBuilder(); _t0 = Time.unscaledTime; _flush = 0; _flushLog = "";
var st = WLReactionSettings.Instance;
_sb.AppendLine("# WL811def Merge " + DateTime.Now.ToString("HH:mm:ss") + " window=" + st.damageMergeWindowSeconds + " flushOnKill=" + st.damageMergeFlushOnKill);
DamageAggregator.ResetDiagnostics(); DamageAggregator.ClearAll();
DamageAggregator.Flushed.Add(OnFlush);
var mob = MakeMob("__wl811def_mob", eSubRol.None, Front(5f), null); _trash.Add(mob.gameObject);
yield return null;
CombatEvents.RaiseHitConfirmed(mob, MakeDamage(10, false, false)); yield return null;
CombatEvents.RaiseHitConfirmed(mob, MakeDamage(20, true, false)); yield return null;
CombatEvents.RaiseHitConfirmed(mob, MakeDamage(30, false, false));
double pend; int ph; bool has = DamageAggregator.TryGetPending(mob, out pend, out ph);
_sb.AppendLine(Pre() + "① 3타 → pending=" + has + " total=" + pend.ToString("F0") + " hits=" + ph + " flush=" + _flush + " (기대 true/60/3/0)");
yield return Wait(st.damageMergeWindowSeconds + 0.1f);
_sb.AppendLine(Pre() + " 윈도우 만료 → flush=" + _flush + " (기대 1) active=" + DamageAggregator.ActiveSlots);
_sb.Append(_flushLog); _flushLog = "";
CombatEvents.RaiseHitConfirmed(mob, MakeDamage(5, false, false)); yield return null;
CombatEvents.RaiseHitConfirmed(mob, MakeDamage(999, false, true));
_sb.AppendLine(Pre() + "② 처치 타격 → 즉시 flush=" + _flush + " (기대 2) active=" + DamageAggregator.ActiveSlots);
_sb.Append(_flushLog);
DamageAggregator.Flushed.Remove(OnFlush);
Save("merge", _sb); Cleanup();
}
/// <summary>ⓔ 점멸: MK Toon 실 머티리얼 큐브 몹 10회 피격(0.1 s 간격) → on/off 20 · HasPropertyBlock 잔류 0 · 머티리얼 수 증가 0 · sharedMaterial 불변 · Killed 엣지 → Spawned 정리.</summary>
public IEnumerator Co_Blink()
{
_sb = new StringBuilder(); _t0 = Time.unscaledTime;
var st = WLHitBlinkSettings.Instance;
_sb.AppendLine("# WL811def Blink " + DateTime.Now.ToString("HH:mm:ss") + " Enabled=" + WLHitBlinkSettings.Enabled + " on/off=" + st.blinkOnSeconds + "/" + st.blinkOffSeconds + " color=" + st.blinkColor + " emission=" + st.useEmission);
MobHitFlash.ResetDiagnostics();
var mat = LoadSampleMat();
_sb.AppendLine("# 머티리얼 " + (mat != null ? mat.name + " shader=" + mat.shader.name : "NULL(기본 큐브 머티리얼)"));
var mob = MakeMob("__wl811def_mob", eSubRol.Elite, Front(5f), mat); _trash.Add(mob.gameObject);
yield return null;
var rend = mob.GetComponentInChildren<MeshRenderer>();
int matsBefore = Resources.FindObjectsOfTypeAll<Material>().Length;
int sharedId = rend.sharedMaterial != null ? rend.sharedMaterial.GetInstanceID() : 0;
_sb.AppendLine(Pre() + "before block=" + rend.HasPropertyBlock() + " materials=" + matsBefore + " sharedId=" + sharedId);
int onDuringSeen = 0;
for (int i = 0; i < 10; i++)
{
CombatEvents.RaiseHitConfirmed(mob, MakeDamage(10, false, false));
bool hasNow = rend.HasPropertyBlock();
if (hasNow) onDuringSeen++;
if (i < 3) _sb.AppendLine(Pre() + " hit" + (i + 1) + " on=" + MobHitFlash.OnCount + " off=" + MobHitFlash.OffCount + " block=" + hasNow);
yield return Wait(0.1f);
}
yield return Wait(0.1f);
int matsAfter = Resources.FindObjectsOfTypeAll<Material>().Length;
int sharedAfter = rend.sharedMaterial != null ? rend.sharedMaterial.GetInstanceID() : 0;
_sb.AppendLine(Pre() + "① 10회 피격 → on=" + MobHitFlash.OnCount + " off=" + MobHitFlash.OffCount + " (기대 10/10 = 20) extend=" + MobHitFlash.ExtendCount + " throttled=" + MobHitFlash.ThrottledCount + " 켜진 직후 block 관측=" + onDuringSeen + "/10 · 잔류 block=" + rend.HasPropertyBlock() + " (기대 False) · 머티리얼 " + matsBefore + "→" + matsAfter + " (증가 " + (matsAfter - matsBefore) + " · 기대 0) · sharedId " + sharedId + "→" + sharedAfter + " (기대 동일) · noProp=" + MobHitFlash.SkippedNoProperty + " reflect=" + MobHitFlash.RendererFieldViaReflection);
// 같은 프레임 다중 히트 → 1회 점멸(연장)
int on0 = MobHitFlash.OnCount, ext0 = MobHitFlash.ExtendCount;
for (int i = 0; i < 5; i++) CombatEvents.RaiseHitConfirmed(mob, MakeDamage(10, false, false));
yield return Wait(0.15f);
_sb.AppendLine(Pre() + "② 같은 프레임 5히트 → on+" + (MobHitFlash.OnCount - on0) + " extend+" + (MobHitFlash.ExtendCount - ext0) + " (기대 1 / 4) block=" + rend.HasPropertyBlock());
// Killed → 디졸브 엣지 MPB(Shader.Find 폴백) → Spawned → 정리
CombatEvents.RaiseKilled(mob);
_sb.AppendLine(Pre() + "③ Killed(Elite) → tint=" + MobHitFlash.TintCount + " dissolve resolved/hdr/range=" + MobHitFlash.DissolveResolved + "/" + MobHitFlash.DissolveHasHdr + "/" + MobHitFlash.DissolveHasRange + " block=" + rend.HasPropertyBlock() + " (기대 True) noProp=" + MobHitFlash.SkippedNoProperty);
var mpb = new MaterialPropertyBlock(); rend.GetPropertyBlock(mpb);
_sb.AppendLine(Pre() + " MPB _EdgeAroundHDR=" + mpb.GetFloat("_EdgeAroundHDR").ToString("F2") + " _EdgeAround=" + mpb.GetFloat("_EdgeAround").ToString("F3") + " _Progress 포함=" + mpb.HasFloat("_Progress") + " (기대 False)");
CombatEvents.RaiseSpawned(mob);
_sb.AppendLine(Pre() + "④ Spawned → clear=" + MobHitFlash.ClearCount + " block=" + rend.HasPropertyBlock() + " (기대 False)");
_sb.AppendLine("# GC: 100 피격 → " + GcOf(() => CombatEvents.RaiseHitConfirmed(mob, MakeDamage(10, false, false)), 100) + " (DamageInfo 생성 포함 · 프로브 측 할당)");
Save("blink", _sb); Cleanup();
}
static string GcOf(Action act, int n)
{
for (int i = 0; i < 10; i++) act();
long m0 = GC.GetTotalMemory(false); long p0 = UnityEngine.Profiling.Profiler.GetMonoUsedSizeLong();
for (int i = 0; i < n; i++) act();
long m1 = GC.GetTotalMemory(false); long p1 = UnityEngine.Profiling.Profiler.GetMonoUsedSizeLong();
return "GC.GetTotalMemory delta=" + (m1 - m0) + " B · MonoUsed delta=" + (p1 - p0) + " B";
}
/// <summary>ⓕ 구독자별 100회 GC: DamageInfo 는 미리 1개 만들어 재사용(프로브 측 할당 0). 카메라/반응/점멸을 RuntimeDisabled 로 하나씩 켜서 잰다.</summary>
public IEnumerator Co_Gc()
{
_sb = new StringBuilder(); _t0 = Time.unscaledTime;
_sb.AppendLine("# WL811def GcCheck " + DateTime.Now.ToString("HH:mm:ss") + " (기준 각 < 2048 B)");
InstallCamera(); yield return null; yield return null;
var mat = LoadSampleMat();
var elite = MakeMob("__wl811def_elite", eSubRol.Elite, Front(5f), mat); _trash.Add(elite.gameObject);
var boss = MakeMob("__wl811def_boss", eSubRol.Boss, Front(5f), mat); _trash.Add(boss.gameObject);
var pc = MakePC("__wl811def_pcactor", Front(5f)); _trash.Add(pc.gameObject);
yield return null;
var dmg = MakeDamage(10, false, false);
bool c0 = WLCombatCameraSettings.RuntimeDisabled, r0 = WLReactionSettings.RuntimeDisabled, b0 = WLHitBlinkSettings.RuntimeDisabled;
Action<bool, bool, bool> set = (c, r, b) => { WLCombatCameraSettings.RuntimeDisabled = !c; WLReactionSettings.RuntimeDisabled = !r; WLHitBlinkSettings.RuntimeDisabled = !b; };
// 워밍업(상태·버퍼 1회 할당)
set(true, true, true);
CombatEvents.RaiseKilled(elite); CombatEvents.RaiseHitConfirmed(elite, dmg); CombatEvents.RaiseComboStage(pc, 3, "Attack3_S", 0f, true); CombatEvents.RaiseSpawned(boss);
yield return null; CombatCameraDirector.ReleaseAll(); TimeScaleArbiter.ReleaseAll(); yield return null;
set(true, false, false); CombatCameraDirector.ResetRun();
_sb.AppendLine("camera Killed×100(쿨로 1회 발동+99 스킵): " + GcOf(() => CombatEvents.RaiseKilled(elite), 100));
CombatCameraDirector.ReleaseAll(); TimeScaleArbiter.ReleaseAll(); yield return null;
_sb.AppendLine("camera ComboStage×100: " + GcOf(() => CombatEvents.RaiseComboStage(pc, 3, "Attack3_S", 0f, true), 100));
_sb.AppendLine("camera Spawned(boss)×100(1회 발동+99 busy): " + GcOf(() => CombatEvents.RaiseSpawned(boss), 100));
CombatCameraDirector.ReleaseAll(); yield return null;
set(false, true, false); KillChain.ClearAll(); DamageAggregator.ClearAll();
_sb.AppendLine("reaction Killed×100(연쇄 100 · 티어 4): " + GcOf(() => CombatEvents.RaiseKilled(elite), 100));
_sb.AppendLine("reaction HitConfirmed×100(합산 1슬롯): " + GcOf(() => CombatEvents.RaiseHitConfirmed(elite, dmg), 100));
KillChain.ClearAll(); DamageAggregator.ClearAll(); yield return null;
set(false, false, true);
_sb.AppendLine("blink HitConfirmed×100(on 1+연장 99): " + GcOf(() => CombatEvents.RaiseHitConfirmed(elite, dmg), 100));
_sb.AppendLine("blink Killed×100(엣지 세팅): " + GcOf(() => CombatEvents.RaiseKilled(elite), 100));
_sb.AppendLine("blink Spawned×100(정리): " + GcOf(() => CombatEvents.RaiseSpawned(elite), 100));
yield return null;
set(false, false, false);
_sb.AppendLine("core-only(3 SO off) Killed×100: " + GcOf(() => CombatEvents.RaiseKilled(elite), 100) + " ← CombatEvents.RaiseKilled 의 Log(문자열 결합 · victim.name) 몫");
_sb.AppendLine("core-only(3 SO off) HitConfirmed×100: " + GcOf(() => CombatEvents.RaiseHitConfirmed(elite, dmg), 100));
set(true, true, true); CombatCameraDirector.ResetRun();
_sb.AppendLine("all3 Killed×100: " + GcOf(() => CombatEvents.RaiseKilled(elite), 100));
_sb.AppendLine("all3 HitConfirmed×100: " + GcOf(() => CombatEvents.RaiseHitConfirmed(elite, dmg), 100));
WLCombatCameraSettings.RuntimeDisabled = c0; WLReactionSettings.RuntimeDisabled = r0; WLHitBlinkSettings.RuntimeDisabled = b0;
_sb.AppendLine("# 원복 RuntimeDisabled cam/re/blink=" + c0 + "/" + r0 + "/" + b0);
Save("gc", _sb); Cleanup();
}
/// <summary>ⓖ C8: 3 SO 런타임 off → 같은 이벤트에 카운터 0 · 카메라 필드 불변 · MPB 0 · timeScale 1 → on 복귀.</summary>
public IEnumerator Co_Toggle()
{
_sb = new StringBuilder(); _t0 = Time.unscaledTime;
_sb.AppendLine("# WL811def Toggle " + DateTime.Now.ToString("HH:mm:ss"));
InstallCamera(); yield return null; yield return null;
var mat = LoadSampleMat();
var elite = MakeMob("__wl811def_elite", eSubRol.Elite, Front(5f), mat); _trash.Add(elite.gameObject);
var boss = MakeMob("__wl811def_boss", eSubRol.Boss, Front(5f), mat); _trash.Add(boss.gameObject);
var pc = MakePC("__wl811def_pcactor", Front(5f)); _trash.Add(pc.gameObject);
yield return null;
var rend = elite.GetComponentInChildren<MeshRenderer>();
CombatCameraDirector.ResetDiagnostics(); CombatCameraDirector.ResetRun(); KillChain.ResetDiagnostics(); KillChain.ClearAll(); DamageAggregator.ResetDiagnostics(); DamageAggregator.ClearAll(); MobHitFlash.ResetDiagnostics();
WLCombatCameraSettings.RuntimeDisabled = true; WLReactionSettings.RuntimeDisabled = true; WLHitBlinkSettings.RuntimeDisabled = true;
float minB = _cam.minDistance, yawB = _cam.rotateAround;
CombatEvents.RaiseKilled(elite); CombatEvents.RaiseSpawned(boss); CombatEvents.RaiseComboStage(pc, 3, "Attack3_S", 0f, true);
for (int i = 0; i < 3; i++) CombatEvents.RaiseHitConfirmed(elite, MakeDamage(10, false, false));
yield return null; yield return null;
_sb.AppendLine(Pre() + "OFF: killCam=" + CombatCameraDirector.KillCamCount + " boss=" + CombatCameraDirector.BossCamCount + " lead=" + CombatCameraDirector.ComboLeadCount + " mult=" + CameraMoveLead.LeadMultiplier.ToString("F2") + " chainKills=" + KillChain.KillCount + " tiers=" + KillChain.TierCount + " mergeHits=" + DamageAggregator.HitCount + " blinkOn=" + MobHitFlash.OnCount + " block=" + rend.HasPropertyBlock() + " cam(min/yaw) " + minB.ToString("F2") + "→" + _cam.minDistance.ToString("F2") + " / " + yawB.ToString("F2") + "→" + _cam.rotateAround.ToString("F2") + " timeScale=" + Time.timeScale.ToString("F2") + " (기대 전부 0 · 불변 · 1.00)");
WLCombatCameraSettings.RuntimeDisabled = false; WLReactionSettings.RuntimeDisabled = false; WLHitBlinkSettings.RuntimeDisabled = false;
CombatEvents.RaiseKilled(elite); CombatEvents.RaiseComboStage(pc, 3, "Attack3_S", 0f, true);
for (int i = 0; i < 3; i++) CombatEvents.RaiseHitConfirmed(elite, MakeDamage(10, false, false));
yield return null;
_sb.AppendLine(Pre() + "ON : killCam=" + CombatCameraDirector.KillCamCount + " lead=" + CombatCameraDirector.ComboLeadCount + " mult=" + CameraMoveLead.LeadMultiplier.ToString("F2") + " chainKills=" + KillChain.KillCount + " mergeHits=" + DamageAggregator.HitCount + " blinkOn=" + MobHitFlash.OnCount + " block=" + rend.HasPropertyBlock() + " timeScale=" + Time.timeScale.ToString("F2") + " (기대 1/1/1.30/1/3/1/True/0.35)");
yield return Wait(1.2f);
_sb.AppendLine(Pre() + "복원: timeScale=" + Time.timeScale.ToString("F2") + " block=" + rend.HasPropertyBlock() + " min=" + _cam.minDistance.ToString("F2") + " mult=" + CameraMoveLead.LeadMultiplier.ToString("F2"));
Save("toggle", _sb); Cleanup();
}
/// <summary>리뷰 WL-811def FIX 회귀: ① 처치 스택의 Init_Cam 덮어쓰기 뒤에도 킬캠 줌이 유지되나(FIX-1)
/// ② arr_cCamData[1] 프리셋이 busy 검사 뒤에만 써지고 보스 처치·사라짐에 원복되나(FIX-2) ③ list_tfCamactionTarget 길이가 불변인가(FIX-3).</summary>
public IEnumerator Co_FixCheck()
{
_sb = new StringBuilder(); _t0 = Time.unscaledTime;
var st = WLCombatCameraSettings.Instance;
_sb.AppendLine("# WL811def FixCheck " + DateTime.Now.ToString("HH:mm:ss") + " Enabled=" + WLCombatCameraSettings.Enabled
+ " zoom=" + st.killCamZoomScale + " scaleUp=" + st.killCamScaleDistanceUp + " preset=" + st.bossPresetApply + "(" + st.bossPresetDistance + " m · " + st.bossPresetCameraHeight + "°) retarget=" + st.bossCamRetarget + " bossCam=" + st.bossCamSeconds + " s");
CombatCameraDirector.ResetDiagnostics(); CombatCameraDirector.ResetRun();
InstallCamera();
yield return null; yield return null;
var boss = MakeMob("__wl811def_boss", eSubRol.Boss, Front(5f), null); _trash.Add(boss.gameObject);
var boss2 = MakeMob("__wl811def_boss2", eSubRol.Boss, Front(5f) + Vector3.right * 2f, null); _trash.Add(boss2.gameObject);
var list = CamActionList(_cam);
int list0 = list != null ? list.Count : -1;
var d1 = _cam.arr_cCamData[1];
float p0Min = d1.minDistance, p0Max = d1.maxDistance, p0H = d1.cameraHeight, p0Up = d1.DistanceUp;
yield return null;
_sb.AppendLine(Pre() + "before " + CamRow() + " | [1]=" + p0Min.ToString("F2") + "/" + p0Max.ToString("F2") + "/" + p0H.ToString("F0") + "° up=" + p0Up.ToString("F2") + " · camActionList=" + list0);
// ───── ① FIX-1: 실전투 보스 처치 스택 재현 — RaiseKilled 직후 같은 프레임에 Set_LockTarget(null) → Init_Cam() 이 궤도 3필드를 덮어쓴다
_cam.Set_LockTarget(boss.transform); // 원본 MonsterSimpleInfo.OnClick_Lock 경로(보스만 락타겟) → Init_Cam → arr_cCamData[1]
yield return null;
_sb.AppendLine(Pre() + "① 보스 락타겟 → " + CamRow() + " (Init_Cam 이 [1] 값을 씀)");
float lockedMin = _cam.minDistance;
CombatEvents.RaiseKilled(boss);
_cam.Set_LockTarget(null); // ← 같은 스택 · MonsterSimpleInfo.DeadTarget → Off() 가 하는 일 → Init_Cam → [0] 값
_sb.AppendLine(Pre() + " Killed + 같은 프레임 Set_LockTarget(null) → pending=" + CombatCameraDirector.KillCamPending + " (기대 True · 시작을 미룸) · 궤도 " + lockedMin.ToString("F2") + "→" + _cam.minDistance.ToString("F2") + " (외부 덮어쓰기) killCam=" + CombatCameraDirector.KillCamCount);
float expectZoom = _cam.arr_cCamData[0].minDistance * st.killCamZoomScale;
float zoomMin = 99f; float zoomReach = -1f; float tK = Time.unscaledTime; int rows = 0;
while (Time.unscaledTime - tK < st.killCamZoomInSeconds + st.killCamHoldSeconds + st.killCamZoomOutSeconds + 0.4f)
{
yield return null;
zoomMin = Mathf.Min(zoomMin, _cam.minDistance);
if (zoomReach < 0f && Mathf.Approximately(_cam.minDistance, expectZoom)) zoomReach = (Time.unscaledTime - tK) * 1000f;
if ((rows++ % 8) == 0) _sb.AppendLine(Pre() + " " + CamRow());
if (zoomReach >= 0f && !CombatCameraDirector.KillCamActive) break;
}
_sb.AppendLine(Pre() + " 결과: 줌 최소=" + zoomMin.ToString("F3") + " (기대 " + expectZoom.ToString("F3") + " = [0] " + _cam.arr_cCamData[0].minDistance.ToString("F2") + " × " + st.killCamZoomScale + ") 도달 " + zoomReach.ToString("F0") + " ms · rebased=" + CombatCameraDirector.KillCamRebased
+ " (기대 1 = base 재취득) · abandoned=" + CombatCameraDirector.KillCamAbandoned + " (기대 0 · 수정 전엔 1) · measured=" + CombatCameraDirector.LastKillCamMeasured.ToString("F3") + " s · 복원 " + CamRow());
// ───── ② FIX-2: 프리셋 스냅샷·복원 · busy 검사 뒤 적용
CombatCameraDirector.ResetRun();
CombatEvents.RaiseSpawned(boss);
_sb.AppendLine(Pre() + "② Boss Spawned → bossCam=" + CombatCameraDirector.BossCamCount + " presetApplied=" + CombatCameraDirector.BossPresetApplied + " active=" + CombatCameraDirector.BossPresetActive
+ " [1]=" + d1.minDistance.ToString("F2") + "/" + d1.maxDistance.ToString("F2") + "/" + d1.cameraHeight.ToString("F0") + "° up=" + d1.DistanceUp.ToString("F2") + " (기대 " + st.bossPresetDistance + "/" + st.bossPresetDistance + "/" + st.bossPresetCameraHeight + "°)");
int applied0 = CombatCameraDirector.BossPresetApplied, skip0 = CombatCameraDirector.BossCamSkipped;
yield return null;
CombatEvents.RaiseSpawned(boss2); // 오빗 진행 중 = busy → 스킵 · 프리셋도 추가 적용 0(수정 전엔 프리셋만 다시 써졌다)
_sb.AppendLine(Pre() + " 진행 중 두 번째 Boss Spawned → bossSkip+" + (CombatCameraDirector.BossCamSkipped - skip0) + " (기대 1) presetApplied+" + (CombatCameraDirector.BossPresetApplied - applied0) + " (기대 0 = busy 검사 뒤로 이동) last=" + CombatCameraDirector.LastInfo);
yield return Wait(st.bossCamSeconds + 0.3f);
_sb.AppendLine(Pre() + " 오빗 종료 → bossActive=" + CombatCameraDirector.BossCamActive + " presetActive=" + CombatCameraDirector.BossPresetActive + " (기대 False/True = 프리셋은 보스전 동안 유지) [1]=" + d1.minDistance.ToString("F2") + "/" + d1.cameraHeight.ToString("F0") + "°"
+ " · ③ camActionList " + list0 + "→" + (list != null ? list.Count : -1) + " (기대 동일 = Set_Target 누적 0) · target 복원=" + ReferenceEquals(_cam.target, _dummy) + " · " + CamRow());
CombatCameraDirector.ResetCooldown();
CombatEvents.RaiseKilled(boss); // 보스 처치 → 프리셋 즉시 원복(쿨 스킵과 무관하게 복원 경로가 먼저다)
_sb.AppendLine(Pre() + " 보스 처치 → restored=" + CombatCameraDirector.BossPresetRestored + " (기대 1) abandoned=" + CombatCameraDirector.BossPresetAbandoned + " (기대 0) active=" + CombatCameraDirector.BossPresetActive
+ " [1]=" + d1.minDistance.ToString("F2") + "/" + d1.maxDistance.ToString("F2") + "/" + d1.cameraHeight.ToString("F0") + "° up=" + d1.DistanceUp.ToString("F2") + " (기대 원값 " + p0Min.ToString("F2") + "/" + p0Max.ToString("F2") + "/" + p0H.ToString("F0") + "°/" + p0Up.ToString("F2") + ")");
yield return Wait(st.killCamZoomInSeconds + st.killCamHoldSeconds + st.killCamZoomOutSeconds + 0.3f);
// ②-2 사라짐(풀 반납 = SetActive(false)) 경로도 원복하는가
CombatCameraDirector.ResetRun();
CombatEvents.RaiseSpawned(boss2);
_sb.AppendLine(Pre() + " 재적용(boss2) → active=" + CombatCameraDirector.BossPresetActive + " [1]=" + d1.minDistance.ToString("F2") + "/" + d1.cameraHeight.ToString("F0") + "°");
boss2.gameObject.SetActive(false); // Actor.Off() 와 같은 상태(IsDead() = !activeInHierarchy)
yield return null; yield return null;
_sb.AppendLine(Pre() + " 보스 사라짐(비활성) → restored=" + CombatCameraDirector.BossPresetRestored + " (기대 2) active=" + CombatCameraDirector.BossPresetActive + " (기대 False) [1]=" + d1.minDistance.ToString("F2") + "/" + d1.maxDistance.ToString("F2") + "/" + d1.cameraHeight.ToString("F0") + "° up=" + d1.DistanceUp.ToString("F2"));
yield return Wait(st.bossCamSeconds + 0.2f);
_sb.AppendLine(Pre() + "# 종료 " + CamRow() + " | [1]=" + d1.minDistance.ToString("F2") + "/" + d1.maxDistance.ToString("F2") + "/" + d1.cameraHeight.ToString("F0") + "° up=" + d1.DistanceUp.ToString("F2")
+ " · camActionList=" + (list != null ? list.Count : -1) + " (기대 " + list0 + ") · abandoned=" + CombatCameraDirector.KillCamAbandoned + " rebased=" + CombatCameraDirector.KillCamRebased + " presetApplied/restored/abandoned=" + CombatCameraDirector.BossPresetApplied + "/" + CombatCameraDirector.BossPresetRestored + "/" + CombatCameraDirector.BossPresetAbandoned
+ " · merge reentrant=" + DamageAggregator.ReentrantHits + " dispatching=" + DamageAggregator.Dispatching + " · reactionRunner=" + WLReactionRunner.Exists);
Save("fixcheck", _sb); Cleanup();
}
/// <summary>병합 후 실전투(메인 에디터 · Lead QA): 3 SO ON n초 / OFF n초 UnityStats 비교(WL785_Budget 스타일).</summary>
public IEnumerator Co_Budget(float sec)
{
_sb = new StringBuilder(); _t0 = Time.unscaledTime;
_sb.AppendLine("# WL811def Budget " + DateTime.Now.ToString("HH:mm:ss") + " " + sec + " s ×2");
for (int pass = 0; pass < 2; pass++)
{
bool on = pass == 0;
WLCombatCameraSettings.RuntimeDisabled = !on; WLReactionSettings.RuntimeDisabled = !on; WLHitBlinkSettings.RuntimeDisabled = !on;
yield return Wait(0.3f);
int n = 0, mSet = 0, mDraw = 0; long aSet = 0, aDraw = 0; float worst = 0f, sum = 0f; long gc0 = GC.GetTotalMemory(false);
int kc0 = CombatCameraDirector.KillCamCount, on0 = MobHitFlash.OnCount, fl0 = DamageAggregator.FlushCount, tr0 = KillChain.TierCount;
float t = Time.unscaledTime;
while (Time.unscaledTime - t < sec)
{
yield return null;
float dt = Time.unscaledDeltaTime; worst = Mathf.Max(worst, dt); sum += dt; n++;
int sp = UnityEditor.UnityStats.setPassCalls, d = UnityEditor.UnityStats.drawCalls;
mSet = Mathf.Max(mSet, sp); mDraw = Mathf.Max(mDraw, d); aSet += sp; aDraw += d;
}
_sb.AppendLine((on ? "연출 ON " : "연출 OFF") + " avgFps=" + (n / Mathf.Max(sum, 0.001f)).ToString("F1") + " worstFrame=" + (worst * 1000f).ToString("F1") + " ms · setPass max/avg=" + mSet + "/" + (n > 0 ? aSet / n : 0) + " draw max/avg=" + mDraw + "/" + (n > 0 ? aDraw / n : 0) + " · GC+" + (GC.GetTotalMemory(false) - gc0) + " B · killCam+" + (CombatCameraDirector.KillCamCount - kc0) + " blinkOn+" + (MobHitFlash.OnCount - on0) + " flush+" + (DamageAggregator.FlushCount - fl0) + " tier+" + (KillChain.TierCount - tr0) + " frames=" + n);
}
WLCombatCameraSettings.RuntimeDisabled = false; WLReactionSettings.RuntimeDisabled = false; WLHitBlinkSettings.RuntimeDisabled = false;
Save("budget", _sb); Destroy(gameObject);
}
}
}