Project_WL/AgentScripts/WL811def_Probe.cs

681 lines
59 KiB
C#
Raw Normal View History

// 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 // 카운터 전부
[WL-811def-fix] 카메라 리뷰 FIX 3 + Minor (#813) 리뷰 reviews/WL-811def.md 의 Major 3 · Minor 5 반영. Assets/Script/** 0줄 · Core 0 · RealCamera.cs 0(공개 필드/메서드만). FIX-1 보스 킬캠 포기: 처치 스택 뒷부분(MobActor.Set_Die → MonsterSimpleInfo.DeadTarget → Off → Set_LockTarget(null) → RealCamera.Init_Cam)이 궤도 3필드를 덮어써 ZoomStillOurs() 가 항상 false 였다. Phase.Pending 을 추가해 킬캠 시작을 다음 프레임으로 미루고 그때 base 를 재취득한다(KillCamRebased 로 계측). 엘리트/일반은 Set_LockTarget 경로가 없어 회귀 0. FIX-2 arr_cCamData[1] 영구 덮어쓰기: 원값 스냅샷 → 보스 처치(OnKilled)·사라짐/비활성(TickBossPreset)·씬 전환·SO off· ReleaseAll 에 복원(BossPresetRestored/Abandoned). 프리셋 적용을 busy 스킵 검사 뒤로 옮겨 스킵된 스폰은 프리셋도 안 쓴다. FIX-3 Set_Target 누적: 보스 리타겟을 공개 필드 target 직접 대입으로 바꿔 list_tfCamactionTarget 길이 불변(기존 대상이 있을 때만 리타겟 = isTarget 유효). 완전 정합용 Set_Target 오버로드는 811n 요청 대상. Minor: MobHitFlash.OnKilled 에 WLReactionRunner.Ensure() · DamageAggregator 재진입 가드(Harvest/Dispatch 분리 + s_dispatching 백업·복원 · ReentrantHits) · SO 폴백 상수 제거(KillChain/DamageAggregator Tick) · BestChainEver 중복 갱신 1곳으로 · s_burstHeld↔EffectBudget 불변식 주석 + 홀드 만료 시 선반납 · 킬캠 쿨/런 카운터 소모 의도 주석. 프로브: WL811def_Probe.FixCheck 추가(FIX-1 Init_Cam 덮어쓰기 뒤 줌 유지 · FIX-2 프리셋 원복/busy 순서 · FIX-3 리스트 길이 불변). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 16:08:47 +00:00
// 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)); }
[WL-811def-fix] 카메라 리뷰 FIX 3 + Minor (#813) 리뷰 reviews/WL-811def.md 의 Major 3 · Minor 5 반영. Assets/Script/** 0줄 · Core 0 · RealCamera.cs 0(공개 필드/메서드만). FIX-1 보스 킬캠 포기: 처치 스택 뒷부분(MobActor.Set_Die → MonsterSimpleInfo.DeadTarget → Off → Set_LockTarget(null) → RealCamera.Init_Cam)이 궤도 3필드를 덮어써 ZoomStillOurs() 가 항상 false 였다. Phase.Pending 을 추가해 킬캠 시작을 다음 프레임으로 미루고 그때 base 를 재취득한다(KillCamRebased 로 계측). 엘리트/일반은 Set_LockTarget 경로가 없어 회귀 0. FIX-2 arr_cCamData[1] 영구 덮어쓰기: 원값 스냅샷 → 보스 처치(OnKilled)·사라짐/비활성(TickBossPreset)·씬 전환·SO off· ReleaseAll 에 복원(BossPresetRestored/Abandoned). 프리셋 적용을 busy 스킵 검사 뒤로 옮겨 스킵된 스폰은 프리셋도 안 쓴다. FIX-3 Set_Target 누적: 보스 리타겟을 공개 필드 target 직접 대입으로 바꿔 list_tfCamactionTarget 길이 불변(기존 대상이 있을 때만 리타겟 = isTarget 유효). 완전 정합용 Set_Target 오버로드는 811n 요청 대상. Minor: MobHitFlash.OnKilled 에 WLReactionRunner.Ensure() · DamageAggregator 재진입 가드(Harvest/Dispatch 분리 + s_dispatching 백업·복원 · ReentrantHits) · SO 폴백 상수 제거(KillChain/DamageAggregator Tick) · BestChainEver 중복 갱신 1곳으로 · s_burstHeld↔EffectBudget 불변식 주석 + 홀드 만료 시 선반납 · 킬캠 쿨/런 카운터 소모 의도 주석. 프로브: WL811def_Probe.FixCheck 추가(FIX-1 Init_Cam 덮어쓰기 뒤 줌 유지 · FIX-2 프리셋 원복/busy 순서 · FIX-3 리스트 길이 불변). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 16:08:47 +00:00
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
[WL-811def-fix] 카메라 리뷰 FIX 3 + Minor (#813) 리뷰 reviews/WL-811def.md 의 Major 3 · Minor 5 반영. Assets/Script/** 0줄 · Core 0 · RealCamera.cs 0(공개 필드/메서드만). FIX-1 보스 킬캠 포기: 처치 스택 뒷부분(MobActor.Set_Die → MonsterSimpleInfo.DeadTarget → Off → Set_LockTarget(null) → RealCamera.Init_Cam)이 궤도 3필드를 덮어써 ZoomStillOurs() 가 항상 false 였다. Phase.Pending 을 추가해 킬캠 시작을 다음 프레임으로 미루고 그때 base 를 재취득한다(KillCamRebased 로 계측). 엘리트/일반은 Set_LockTarget 경로가 없어 회귀 0. FIX-2 arr_cCamData[1] 영구 덮어쓰기: 원값 스냅샷 → 보스 처치(OnKilled)·사라짐/비활성(TickBossPreset)·씬 전환·SO off· ReleaseAll 에 복원(BossPresetRestored/Abandoned). 프리셋 적용을 busy 스킵 검사 뒤로 옮겨 스킵된 스폰은 프리셋도 안 쓴다. FIX-3 Set_Target 누적: 보스 리타겟을 공개 필드 target 직접 대입으로 바꿔 list_tfCamactionTarget 길이 불변(기존 대상이 있을 때만 리타겟 = isTarget 유효). 완전 정합용 Set_Target 오버로드는 811n 요청 대상. Minor: MobHitFlash.OnKilled 에 WLReactionRunner.Ensure() · DamageAggregator 재진입 가드(Harvest/Dispatch 분리 + s_dispatching 백업·복원 · ReentrantHits) · SO 폴백 상수 제거(KillChain/DamageAggregator Tick) · BestChainEver 중복 갱신 1곳으로 · s_burstHeld↔EffectBudget 불변식 주석 + 홀드 만료 시 선반납 · 킬캠 쿨/런 카운터 소모 의도 주석. 프로브: WL811def_Probe.FixCheck 추가(FIX-1 Init_Cam 덮어쓰기 뒤 줌 유지 · FIX-2 프리셋 원복/busy 순서 · FIX-3 리스트 길이 불변). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 16:08:47 +00:00
+ " 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);
[WL-811def-fix] 카메라 리뷰 FIX 3 + Minor (#813) 리뷰 reviews/WL-811def.md 의 Major 3 · Minor 5 반영. Assets/Script/** 0줄 · Core 0 · RealCamera.cs 0(공개 필드/메서드만). FIX-1 보스 킬캠 포기: 처치 스택 뒷부분(MobActor.Set_Die → MonsterSimpleInfo.DeadTarget → Off → Set_LockTarget(null) → RealCamera.Init_Cam)이 궤도 3필드를 덮어써 ZoomStillOurs() 가 항상 false 였다. Phase.Pending 을 추가해 킬캠 시작을 다음 프레임으로 미루고 그때 base 를 재취득한다(KillCamRebased 로 계측). 엘리트/일반은 Set_LockTarget 경로가 없어 회귀 0. FIX-2 arr_cCamData[1] 영구 덮어쓰기: 원값 스냅샷 → 보스 처치(OnKilled)·사라짐/비활성(TickBossPreset)·씬 전환·SO off· ReleaseAll 에 복원(BossPresetRestored/Abandoned). 프리셋 적용을 busy 스킵 검사 뒤로 옮겨 스킵된 스폰은 프리셋도 안 쓴다. FIX-3 Set_Target 누적: 보스 리타겟을 공개 필드 target 직접 대입으로 바꿔 list_tfCamactionTarget 길이 불변(기존 대상이 있을 때만 리타겟 = isTarget 유효). 완전 정합용 Set_Target 오버로드는 811n 요청 대상. Minor: MobHitFlash.OnKilled 에 WLReactionRunner.Ensure() · DamageAggregator 재진입 가드(Harvest/Dispatch 분리 + s_dispatching 백업·복원 · ReentrantHits) · SO 폴백 상수 제거(KillChain/DamageAggregator Tick) · BestChainEver 중복 갱신 1곳으로 · s_burstHeld↔EffectBudget 불변식 주석 + 홀드 만료 시 선반납 · 킬캠 쿨/런 카운터 소모 의도 주석. 프로브: WL811def_Probe.FixCheck 추가(FIX-1 Init_Cam 덮어쓰기 뒤 줌 유지 · FIX-2 프리셋 원복/busy 순서 · FIX-3 리스트 길이 불변). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 16:08:47 +00:00
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;
}
[WL-811def-fix] 카메라 리뷰 FIX 3 + Minor (#813) 리뷰 reviews/WL-811def.md 의 Major 3 · Minor 5 반영. Assets/Script/** 0줄 · Core 0 · RealCamera.cs 0(공개 필드/메서드만). FIX-1 보스 킬캠 포기: 처치 스택 뒷부분(MobActor.Set_Die → MonsterSimpleInfo.DeadTarget → Off → Set_LockTarget(null) → RealCamera.Init_Cam)이 궤도 3필드를 덮어써 ZoomStillOurs() 가 항상 false 였다. Phase.Pending 을 추가해 킬캠 시작을 다음 프레임으로 미루고 그때 base 를 재취득한다(KillCamRebased 로 계측). 엘리트/일반은 Set_LockTarget 경로가 없어 회귀 0. FIX-2 arr_cCamData[1] 영구 덮어쓰기: 원값 스냅샷 → 보스 처치(OnKilled)·사라짐/비활성(TickBossPreset)·씬 전환·SO off· ReleaseAll 에 복원(BossPresetRestored/Abandoned). 프리셋 적용을 busy 스킵 검사 뒤로 옮겨 스킵된 스폰은 프리셋도 안 쓴다. FIX-3 Set_Target 누적: 보스 리타겟을 공개 필드 target 직접 대입으로 바꿔 list_tfCamactionTarget 길이 불변(기존 대상이 있을 때만 리타겟 = isTarget 유효). 완전 정합용 Set_Target 오버로드는 811n 요청 대상. Minor: MobHitFlash.OnKilled 에 WLReactionRunner.Ensure() · DamageAggregator 재진입 가드(Harvest/Dispatch 분리 + s_dispatching 백업·복원 · ReentrantHits) · SO 폴백 상수 제거(KillChain/DamageAggregator Tick) · BestChainEver 중복 갱신 1곳으로 · s_burstHeld↔EffectBudget 불변식 주석 + 홀드 만료 시 선반납 · 킬캠 쿨/런 카운터 소모 의도 주석. 프로브: WL811def_Probe.FixCheck 추가(FIX-1 Init_Cam 덮어쓰기 뒤 줌 유지 · FIX-2 프리셋 원복/busy 순서 · FIX-3 리스트 길이 불변). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 16:08:47 +00:00
/// <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();
}
[WL-811def-fix] 카메라 리뷰 FIX 3 + Minor (#813) 리뷰 reviews/WL-811def.md 의 Major 3 · Minor 5 반영. Assets/Script/** 0줄 · Core 0 · RealCamera.cs 0(공개 필드/메서드만). FIX-1 보스 킬캠 포기: 처치 스택 뒷부분(MobActor.Set_Die → MonsterSimpleInfo.DeadTarget → Off → Set_LockTarget(null) → RealCamera.Init_Cam)이 궤도 3필드를 덮어써 ZoomStillOurs() 가 항상 false 였다. Phase.Pending 을 추가해 킬캠 시작을 다음 프레임으로 미루고 그때 base 를 재취득한다(KillCamRebased 로 계측). 엘리트/일반은 Set_LockTarget 경로가 없어 회귀 0. FIX-2 arr_cCamData[1] 영구 덮어쓰기: 원값 스냅샷 → 보스 처치(OnKilled)·사라짐/비활성(TickBossPreset)·씬 전환·SO off· ReleaseAll 에 복원(BossPresetRestored/Abandoned). 프리셋 적용을 busy 스킵 검사 뒤로 옮겨 스킵된 스폰은 프리셋도 안 쓴다. FIX-3 Set_Target 누적: 보스 리타겟을 공개 필드 target 직접 대입으로 바꿔 list_tfCamactionTarget 길이 불변(기존 대상이 있을 때만 리타겟 = isTarget 유효). 완전 정합용 Set_Target 오버로드는 811n 요청 대상. Minor: MobHitFlash.OnKilled 에 WLReactionRunner.Ensure() · DamageAggregator 재진입 가드(Harvest/Dispatch 분리 + s_dispatching 백업·복원 · ReentrantHits) · SO 폴백 상수 제거(KillChain/DamageAggregator Tick) · BestChainEver 중복 갱신 1곳으로 · s_burstHeld↔EffectBudget 불변식 주석 + 홀드 만료 시 선반납 · 킬캠 쿨/런 카운터 소모 의도 주석. 프로브: WL811def_Probe.FixCheck 추가(FIX-1 Init_Cam 덮어쓰기 뒤 줌 유지 · FIX-2 프리셋 원복/busy 순서 · FIX-3 리스트 길이 불변). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 16:08:47 +00:00
/// <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);
}
}
}