[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>
This commit is contained in:
parent
e5980770c6
commit
902c77992a
|
|
@ -14,6 +14,7 @@
|
|||
// 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
|
||||
|
||||
|
|
@ -42,6 +43,7 @@ public static class WL811def_Probe
|
|||
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()
|
||||
{
|
||||
|
|
@ -122,10 +124,11 @@ public static class WL811def_Probe
|
|||
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 + " boss=" + CombatCameraDirector.BossCamCount + " bossSkip=" + CombatCameraDirector.BossCamSkipped + " preset=" + CombatCameraDirector.BossPresetApplied + " lead=" + CombatCameraDirector.ComboLeadCount
|
||||
+ " 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);
|
||||
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();
|
||||
|
|
@ -201,6 +204,13 @@ public static class WL811def_Probe
|
|||
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 미사용)
|
||||
|
|
@ -568,6 +578,79 @@ public static class WL811def_Probe
|
|||
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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@
|
|||
// Killed(Elite/Boss) → TimeScaleArbiter.Request(Cinematic · cinematicDefaultScale · killCamSlowMoSeconds)
|
||||
// + RealCamera 궤도 거리(minDistance/maxDistance[/DistanceUp]) × killCamZoomScale 램프 → 유지 → 복원
|
||||
// 쿨 = 코어 killCamCooldownSeconds · 런당 killCamMaxPerRun · 하단 40% 가드(처치 위치 뷰포트 y)
|
||||
// Spawned(isBoss) → arr_cCamData[1] 보스 프리셋 덮어쓰기(옵션) · Set_Target(보스) · Add_Rot 로 sin 오빗 · 거리 배율 · 끝나면 전부 복원
|
||||
// Spawned(isBoss) → arr_cCamData[1] 보스 프리셋 덮어쓰기(옵션 · 원값 스냅샷 후 · 보스가 죽거나 사라지면 원복) · 추적 대상을 보스로
|
||||
// (공개 필드 target 직접 대입 = list_tfCamactionTarget 무접촉) · Add_Rot 로 sin 오빗 · 거리 배율 · 끝나면 전부 복원
|
||||
// ComboStage(isLast) → CameraMoveLead.LeadMultiplier = comboLeadMultiplier (comboLeadSeconds 뒤 1 로)
|
||||
//
|
||||
// ■ 왜 RealCamera 공개 필드를 매 프레임 쓰는가 (사후 가산 기각 근거 = CameraMoveLead.cs / WLHitFeelCameraShake.cs 주석)
|
||||
|
|
@ -21,6 +22,12 @@
|
|||
// · 보스 캠 중 킬캠 줌은 스킵(슬로모만) · 킬캠 중 보스 스폰은 킬캠을 즉시 복원하고 보스 캠 시작
|
||||
// · 씬 전환 시 상태·런 카운터 초기화 · 러너 파괴/앱 종료 시 전부 복원
|
||||
// · 시간은 전부 unscaled(슬로모 중에도 램프가 늘어지지 않는다) · 핸들러 GC 0(verboseLog 때만 문자열)
|
||||
//
|
||||
// ■ 리뷰 WL-811def FIX 3건 반영 (2026-09-09 · #813)
|
||||
// FIX-1 킬캠 시작을 **다음 프레임**으로 미루고 그때 base 궤도 값을 재취득 — 처치 스택 뒷부분(MonsterSimpleInfo.Off → Set_LockTarget(null)
|
||||
// → RealCamera.Init_Cam)이 min/max/DistanceUp 을 덮어써 보스 킬캠 줌이 항상 포기되던 문제(Phase.Pending · KillCamRebased 로 계측)
|
||||
// FIX-2 arr_cCamData[1](보스/락타겟 프리셋)을 덮어쓸 때 **원값 스냅샷** → 보스 처치·사라짐·씬 전환·SO off 에 복원 · 적용을 busy 스킵 검사 뒤로
|
||||
// FIX-3 보스 리타겟을 Set_Target(→ list_tfCamactionTarget 누적) 대신 **공개 필드 target 직접 대입**으로 — 리스트 길이 불변
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
using UnityEngine;
|
||||
|
|
@ -31,17 +38,19 @@ namespace WL.CombatCamera
|
|||
{
|
||||
public static class CombatCameraDirector
|
||||
{
|
||||
enum Phase { None = 0, ZoomIn = 1, Hold = 2, ZoomOut = 3 }
|
||||
enum Phase { None = 0, Pending = 1, ZoomIn = 2, Hold = 3, ZoomOut = 4 }
|
||||
|
||||
// ── 킬캠 상태
|
||||
static Phase s_phase;
|
||||
static float s_phaseStart, s_killStart;
|
||||
static float s_scale = 1f;
|
||||
static float s_baseMin, s_baseMax, s_baseUp; // 발동 시점 스냅샷
|
||||
static float s_baseMin, s_baseMax, s_baseUp; // 줌 시작 시점 스냅샷(FIX-1 = 처치 다음 프레임에 재취득)
|
||||
static float s_preMin, s_preMax, s_preUp; // 처치 이벤트 시점 값 — 외부 덮어쓰기 계측용(KillCamRebased)
|
||||
static float s_wMin, s_wMax, s_wUp; // 마지막으로 우리가 쓴 값(외부 덮어쓰기 감지)
|
||||
static bool s_scaleUp;
|
||||
static float s_lastKillCam = -999f;
|
||||
static int s_runCount;
|
||||
static int s_pendingFrame; // FIX-1: 킬캠을 예약한 프레임(이 프레임에는 시작하지 않는다)
|
||||
static RealCamera s_cam;
|
||||
|
||||
// ── 보스 캠 상태
|
||||
|
|
@ -52,6 +61,13 @@ namespace WL.CombatCamera
|
|||
static float s_bossYawApplied;
|
||||
static float s_bossBaseMin, s_bossBaseMax, s_bossWMin, s_bossWMax;
|
||||
|
||||
// ── 보스 프리셋(arr_cCamData[1]) 상태 — FIX-2: 원값 스냅샷 → 보스 처치·사라짐·씬 전환·SO off 에 복원
|
||||
static bool s_presetApplied;
|
||||
static cCamData s_presetData; // 우리가 값을 쓴 프리셋(참조 · 원본 [Serializable] 클래스라 fake-null 아님)
|
||||
static Actor s_presetBoss; // 프리셋을 건 보스(처치·풀 반납 감지)
|
||||
static float s_presetOMin, s_presetOMax, s_presetOHeight, s_presetOUp; // 원값
|
||||
static float s_presetWMin, s_presetWMax, s_presetWHeight, s_presetWUp; // 우리가 쓴 값
|
||||
|
||||
// ── 3타 리드 상태
|
||||
static float s_leadUntil = -1f;
|
||||
|
||||
|
|
@ -60,7 +76,8 @@ namespace WL.CombatCamera
|
|||
// ── 진단(프로브가 읽는다)
|
||||
public static bool Subscribed;
|
||||
public static int KillCamCount, KillCamSkippedRole, KillCamSkippedCooldown, KillCamSkippedRunCap, KillCamSkippedGuard, KillCamSkippedNoCam, KillCamSkippedBusy, KillCamAbandoned;
|
||||
public static int BossCamCount, BossCamSkipped, BossPresetApplied, ComboLeadCount;
|
||||
public static int KillCamRebased; // FIX-1: 미룬 사이에 외부(Init_Cam 등)가 궤도 값을 바꿔 base 를 다시 잡은 횟수
|
||||
public static int BossCamCount, BossCamSkipped, BossPresetApplied, BossPresetRestored, BossPresetAbandoned, ComboLeadCount;
|
||||
public static ArbiterResult LastSlowMoResult;
|
||||
public static float LastVictimViewportY = -1f;
|
||||
public static float LastKillCamMeasured; // 줌 시작 → 완전 복원까지(초 · unscaled)
|
||||
|
|
@ -68,7 +85,9 @@ namespace WL.CombatCamera
|
|||
public static string LastInfo = "";
|
||||
|
||||
public static bool KillCamActive { get { return s_phase != Phase.None; } }
|
||||
public static bool KillCamPending { get { return s_phase == Phase.Pending; } }
|
||||
public static bool BossCamActive { get { return s_bossActive; } }
|
||||
public static bool BossPresetActive { get { return s_presetApplied; } }
|
||||
public static float CurrentZoomScale { get { return s_scale; } }
|
||||
public static int RunCount { get { return s_runCount; } }
|
||||
public static float LeadUntil { get { return s_leadUntil; } }
|
||||
|
|
@ -114,6 +133,11 @@ namespace WL.CombatCamera
|
|||
{
|
||||
if (!WLCombatCameraSettings.Enabled) return;
|
||||
var st = St;
|
||||
|
||||
// FIX-2: 프리셋을 건 보스가 죽었으면 즉시 원복(틱 폴백보다 한 프레임 빠르다).
|
||||
// Set_Die 는 이 뒤에 Set_LockTarget(null) → Init_Cam() 으로 인덱스 0 을 읽으므로 여기서 되돌려도 이번 프레임 연출에 영향 0.
|
||||
if (s_presetApplied && ReferenceEquals(e.victim, s_presetBoss)) RestoreBossPreset();
|
||||
|
||||
if (!st.killCamEnabled) return;
|
||||
|
||||
bool roleOk = e.subRole == eSubRol.Boss ? st.killCamOnBoss : e.subRole == eSubRol.Elite ? st.killCamOnElite : st.killCamOnNormal;
|
||||
|
|
@ -125,6 +149,8 @@ namespace WL.CombatCamera
|
|||
if (now - s_lastKillCam < cooldown) { KillCamSkippedCooldown++; LastInfo = "cooldown"; if (Verbose) Log("킬캠 스킵(쿨) " + (now - s_lastKillCam).ToString("F2") + " < " + cooldown); return; }
|
||||
if (st.killCamMaxPerRun > 0 && s_runCount >= st.killCamMaxPerRun) { KillCamSkippedRunCap++; LastInfo = "runcap"; if (Verbose) Log("킬캠 스킵(런 상한 " + st.killCamMaxPerRun + ")"); return; }
|
||||
|
||||
// 쿨·런 카운터는 여기서 소모한다 — 아래 스킵(카메라 없음·인트로·하단 가드)으로 슬로모만 나가도 "킬캠 1회를 썼다"로 친다.
|
||||
// 의도: 연출 밀도(런당 ≤6 · 최소 간격 8 s)는 슬로모까지 포함한 체감 기준이다(리뷰 Minor-6 확인 사항).
|
||||
s_lastKillCam = now;
|
||||
s_runCount++;
|
||||
KillCamCount++;
|
||||
|
|
@ -155,14 +181,39 @@ namespace WL.CombatCamera
|
|||
}
|
||||
|
||||
if (s_phase != Phase.None) EndZoom(true); // (쿨 안이라 사실상 없음) 안전
|
||||
|
||||
// ── FIX-1(리뷰 Major-1): 여기서 바로 스냅샷·줌을 걸면 같은 호출 스택 뒷부분이 그 값을 덮어쓴다.
|
||||
// MobActor.Set_Die 는 RaiseKilled(:435) 다음에 MonsterSimpleInfo.DeadTarget(:444) → Off() → 보스면 Set_LockTarget(null)
|
||||
// → RealCamera.Init_Cam() 으로 minDistance/maxDistance/cameraHeight 와 Set_DistanceUp(false) 로 DistanceUp 을 프리셋 값으로 다시 쓴다.
|
||||
// → 다음 틱의 ZoomStillOurs() 가 false → KillCamAbandoned · 보스 킬캠은 슬로모만 남았다.
|
||||
// 그래서 **시작을 다음 프레임으로 미루고 그때 base 를 재취득**한다(엘리트/일반은 Set_LockTarget 경로가 없어 값이 같다 = 회귀 0 · 지연 1프레임).
|
||||
s_cam = cam;
|
||||
s_baseMin = cam.minDistance; s_baseMax = cam.maxDistance; s_baseUp = cam.DistanceUp;
|
||||
s_preMin = cam.minDistance; s_preMax = cam.maxDistance; s_preUp = cam.DistanceUp;
|
||||
s_scaleUp = st.killCamScaleDistanceUp;
|
||||
s_phase = Phase.ZoomIn; s_phaseStart = s_killStart = now;
|
||||
WriteZoom(1f);
|
||||
s_scale = 1f;
|
||||
s_phase = Phase.Pending; s_pendingFrame = Time.frameCount; s_phaseStart = s_killStart = now;
|
||||
Runner.enabled = true;
|
||||
LastInfo = "killcam";
|
||||
if (Verbose) Log("킬캠 발동 victim=" + (e.victim != null ? e.victim.name : "?") + " sub=" + e.subRole + " 슬로모=" + LastSlowMoResult + " base=" + s_baseMin.ToString("F2") + "/" + s_baseMax.ToString("F2") + "/" + s_baseUp.ToString("F2"));
|
||||
if (Verbose) Log("킬캠 예약(다음 프레임 base 재취득) victim=" + (e.victim != null ? e.victim.name : "?") + " sub=" + e.subRole + " 슬로모=" + LastSlowMoResult + " pre=" + s_preMin.ToString("F2") + "/" + s_preMax.ToString("F2") + "/" + s_preUp.ToString("F2"));
|
||||
}
|
||||
|
||||
/// <summary>FIX-1: 예약해 둔 킬캠을 다음 프레임에 시작한다 — 처치 프레임의 나머지 로직(Init_Cam 등)이 다 쓴 뒤의 값을 base 로 잡는다.</summary>
|
||||
static void BeginZoom(float now)
|
||||
{
|
||||
if (Time.frameCount <= s_pendingFrame) return; // 처치와 같은 프레임에는 시작하지 않는다
|
||||
var cam = s_cam;
|
||||
if (cam.IsEnter() || s_bossActive) // 미룬 사이에 인트로/보스 캠이 시작됐으면 슬로모만
|
||||
{
|
||||
KillCamSkippedBusy++; LastInfo = "slowmo-only(busy)";
|
||||
s_phase = Phase.None; s_cam = null; LastKillCamMeasured = -1f;
|
||||
if (Verbose) Log("킬캠 줌 취소(예약 중 인트로/보스 캠)");
|
||||
return;
|
||||
}
|
||||
s_baseMin = cam.minDistance; s_baseMax = cam.maxDistance; s_baseUp = cam.DistanceUp;
|
||||
if (!Mathf.Approximately(s_baseMin, s_preMin) || !Mathf.Approximately(s_baseMax, s_preMax) || !Mathf.Approximately(s_baseUp, s_preUp)) KillCamRebased++;
|
||||
s_phase = Phase.ZoomIn; s_phaseStart = now;
|
||||
WriteZoom(1f);
|
||||
if (Verbose) Log("킬캠 시작 base=" + s_baseMin.ToString("F2") + "/" + s_baseMax.ToString("F2") + "/" + s_baseUp.ToString("F2") + " rebased=" + KillCamRebased);
|
||||
}
|
||||
|
||||
static void WriteZoom(float k)
|
||||
|
|
@ -186,6 +237,12 @@ namespace WL.CombatCamera
|
|||
static void EndZoom(bool restore)
|
||||
{
|
||||
if (s_phase == Phase.None) return;
|
||||
if (s_phase == Phase.Pending) // 아직 아무것도 쓰지 않았다 — 복원할 것도, 포기로 셀 것도 없다
|
||||
{
|
||||
s_phase = Phase.None; s_scale = 1f; s_cam = null;
|
||||
if (Verbose) Log("킬캠 예약 취소");
|
||||
return;
|
||||
}
|
||||
if (restore && ZoomStillOurs())
|
||||
{
|
||||
var cam = s_cam;
|
||||
|
|
@ -203,6 +260,7 @@ namespace WL.CombatCamera
|
|||
if (s_phase == Phase.None) return;
|
||||
var st = St;
|
||||
if (s_cam == null || st == null || !WLCombatCameraSettings.Enabled) { EndZoom(true); return; }
|
||||
if (s_phase == Phase.Pending) { BeginZoom(now); return; } // FIX-1: 예약 → 다음 프레임 시작(base 재취득)
|
||||
if (!ZoomStillOurs()) { KillCamAbandoned++; s_phase = Phase.None; s_scale = 1f; s_cam = null; LastKillCamMeasured = -1f; if (Verbose) Log("킬캠 포기(외부가 거리 필드를 바꿈)"); return; }
|
||||
|
||||
float t = now - s_phaseStart;
|
||||
|
|
@ -232,25 +290,27 @@ namespace WL.CombatCamera
|
|||
var cam = ResolveCamera();
|
||||
if (cam == null) { BossCamSkipped++; LastInfo = "bosscam(no cam)"; return; }
|
||||
|
||||
if (st.bossPresetApply && cam.arr_cCamData != null && cam.arr_cCamData.Length > 1 && cam.arr_cCamData[1] != null)
|
||||
{
|
||||
var d = cam.arr_cCamData[1];
|
||||
d.minDistance = d.maxDistance = st.bossPresetDistance;
|
||||
d.cameraHeight = st.bossPresetCameraHeight;
|
||||
if (st.bossPresetDistanceUp >= 0f) d.DistanceUp = st.bossPresetDistanceUp;
|
||||
BossPresetApplied++;
|
||||
}
|
||||
|
||||
// FIX-2: 프리셋 적용은 busy 스킵 검사 **뒤**로 — 보스 캠이 스킵되는데 프리셋만 써지는 일이 없게 한다.
|
||||
if (cam.IsEnter() || s_bossActive) { BossCamSkipped++; LastInfo = "bosscam(busy)"; return; }
|
||||
if (s_phase != Phase.None) EndZoom(true);
|
||||
|
||||
ApplyBossPreset(cam, st, e.actor);
|
||||
|
||||
float now = Time.unscaledTime;
|
||||
s_cam = cam;
|
||||
s_bossActive = true; s_bossStart = now;
|
||||
s_bossTf = e.actor.transform;
|
||||
|
||||
// ── FIX-3(리뷰 Major-3): RealCamera.Set_Target 은 호출마다 list_tfCamactionTarget 에 항목을 쌓고(원본 :77-86),
|
||||
// 그 리스트는 인트로(CamStatus 0) 분기에서만 소비된다(:197·212) → 보스 캠 1회당 2건이 영구히 남아 다음 인트로/컷 경로가
|
||||
// 우리 잔재를 첫 목표로 삼고 Start_Game() 이 지연된다. 그래서 **공개 필드 target 을 직접 대입**해 리스트를 건드리지 않는다.
|
||||
// · isTarget(private)은 target 이 이미 있을 때만 true 이므로 기존 대상이 없으면 아예 리타겟하지 않는다(카메라 정지 방지).
|
||||
// · m_Target(private Actor)은 이전 대상(PC)인 채로 남는다 = CameraMoveLead 의 "기준 속도" 출처만 PC 값
|
||||
// (리드 자체는 target 의 이동 차분으로 계산 · speedReference > 0 이면 애초에 안 쓴다) · 오빗이 끝나면 다시 정합.
|
||||
// · 완전 정합이 필요하면 811n 에 Add_CamActionTarget 없는 Set_Target 오버로드를 요청한다(완료보고 기재).
|
||||
s_prevTarget = cam.target;
|
||||
s_bossRetargeted = false;
|
||||
if (st.bossCamRetarget) { cam.Set_Target(s_bossTf); s_bossRetargeted = true; }
|
||||
if (st.bossCamRetarget && s_prevTarget != null && s_bossTf != null) { cam.target = s_bossTf; s_bossRetargeted = true; }
|
||||
s_bossBaseMin = cam.minDistance; s_bossBaseMax = cam.maxDistance;
|
||||
cam.minDistance = s_bossWMin = s_bossBaseMin * st.bossCamDistanceScale;
|
||||
cam.maxDistance = s_bossWMax = s_bossBaseMax * st.bossCamDistanceScale;
|
||||
|
|
@ -272,13 +332,62 @@ namespace WL.CombatCamera
|
|||
{
|
||||
cam.minDistance = s_bossBaseMin; cam.maxDistance = s_bossBaseMax;
|
||||
}
|
||||
if (s_bossRetargeted && ReferenceEquals(cam.target, s_bossTf)) cam.Set_Target(s_prevTarget); // 외부가 대상을 바꿨으면 손대지 않음
|
||||
// FIX-3: 복원도 공개 필드 대입(리스트 무접촉) · 외부가 대상을 바꿨으면 손대지 않음
|
||||
if (s_bossRetargeted && s_prevTarget != null && ReferenceEquals(cam.target, s_bossTf)) cam.target = s_prevTarget;
|
||||
LastBossCamMeasured = Time.unscaledTime - s_bossStart;
|
||||
}
|
||||
s_bossActive = false; s_bossTf = null; s_prevTarget = null; s_bossYawApplied = 0f; s_cam = null;
|
||||
if (Verbose) Log("보스 캠 종료 restore=" + restore + " measured=" + LastBossCamMeasured.ToString("F3"));
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────── 보스 프리셋(arr_cCamData[1]) — FIX-2
|
||||
/// <summary>보스/락타겟 프리셋을 덮어쓰기 전에 원값을 스냅샷한다. 이미 걸려 있으면 스냅샷을 덮어쓰지 않는다(중첩 보스).</summary>
|
||||
static void ApplyBossPreset(RealCamera cam, WLCombatCameraSettings st, Actor boss)
|
||||
{
|
||||
if (!st.bossPresetApply || s_presetApplied) return;
|
||||
var arr = cam.arr_cCamData;
|
||||
if (arr == null || arr.Length <= 1 || arr[1] == null) return;
|
||||
var d = arr[1];
|
||||
s_presetData = d; s_presetBoss = boss;
|
||||
s_presetOMin = d.minDistance; s_presetOMax = d.maxDistance; s_presetOHeight = d.cameraHeight; s_presetOUp = d.DistanceUp;
|
||||
d.minDistance = s_presetWMin = st.bossPresetDistance;
|
||||
d.maxDistance = s_presetWMax = st.bossPresetDistance;
|
||||
d.cameraHeight = s_presetWHeight = st.bossPresetCameraHeight;
|
||||
if (st.bossPresetDistanceUp >= 0f) d.DistanceUp = st.bossPresetDistanceUp;
|
||||
s_presetWUp = d.DistanceUp;
|
||||
s_presetApplied = true;
|
||||
BossPresetApplied++;
|
||||
if (Verbose) Log("보스 프리셋 적용 [1] " + s_presetOMin.ToString("F2") + "/" + s_presetOMax.ToString("F2") + "/" + s_presetOHeight.ToString("F0") + "° → " + s_presetWMin.ToString("F2") + "/" + s_presetWMax.ToString("F2") + "/" + s_presetWHeight.ToString("F0") + "°");
|
||||
}
|
||||
|
||||
/// <summary>프리셋을 원값으로 되돌린다(보스 처치·사라짐·씬 전환·SO off·러너 파괴). 파일 규약대로 "우리가 쓴 값이 그대로 남아 있을 때만" 되돌린다.</summary>
|
||||
static void RestoreBossPreset()
|
||||
{
|
||||
if (!s_presetApplied) return;
|
||||
var d = s_presetData;
|
||||
s_presetApplied = false; s_presetData = null; s_presetBoss = null;
|
||||
if (d == null) return;
|
||||
if (!Mathf.Approximately(d.minDistance, s_presetWMin) || !Mathf.Approximately(d.maxDistance, s_presetWMax)
|
||||
|| !Mathf.Approximately(d.cameraHeight, s_presetWHeight) || !Mathf.Approximately(d.DistanceUp, s_presetWUp))
|
||||
{
|
||||
BossPresetAbandoned++;
|
||||
if (Verbose) Log("보스 프리셋 복원 포기(외부가 값을 바꿈)");
|
||||
return;
|
||||
}
|
||||
d.minDistance = s_presetOMin; d.maxDistance = s_presetOMax; d.cameraHeight = s_presetOHeight; d.DistanceUp = s_presetOUp;
|
||||
BossPresetRestored++;
|
||||
if (Verbose) Log("보스 프리셋 복원 [1] " + s_presetOMin.ToString("F2") + "/" + s_presetOMax.ToString("F2") + "/" + s_presetOHeight.ToString("F0") + "°");
|
||||
}
|
||||
|
||||
/// <summary>프리셋 수명 감시 — 보스가 죽거나(IsDead) 풀로 돌아가거나(비활성) 파괴되면, SO 가 꺼지면 원값으로 돌린다.</summary>
|
||||
static void TickBossPreset()
|
||||
{
|
||||
if (!s_presetApplied) return;
|
||||
if (!WLCombatCameraSettings.Enabled) { RestoreBossPreset(); return; } // C8 — 끄면 즉시 원본 값
|
||||
var b = s_presetBoss;
|
||||
if (b == null || b.IsDead()) RestoreBossPreset(); // IsDead() = DeadStatus || !activeInHierarchy(풀 반납)
|
||||
}
|
||||
|
||||
static void TickBossCam(float now)
|
||||
{
|
||||
if (!s_bossActive) return;
|
||||
|
|
@ -316,6 +425,7 @@ namespace WL.CombatCamera
|
|||
float now = Time.unscaledTime;
|
||||
TickKillCam(now);
|
||||
TickBossCam(now);
|
||||
TickBossPreset();
|
||||
TickLead(now);
|
||||
}
|
||||
|
||||
|
|
@ -324,6 +434,7 @@ namespace WL.CombatCamera
|
|||
{
|
||||
EndZoom(true);
|
||||
EndBossCam(true);
|
||||
RestoreBossPreset();
|
||||
CameraMoveLead.LeadMultiplier = 1f; s_leadUntil = -1f;
|
||||
}
|
||||
|
||||
|
|
@ -332,6 +443,7 @@ namespace WL.CombatCamera
|
|||
// 카메라 오브젝트가 사라졌을 수 있다 — 복원 없이 상태만 버린다(복원은 ReleaseAll 이 러너 OnDisable 에서 시도)
|
||||
s_phase = Phase.None; s_scale = 1f; s_cam = null;
|
||||
s_bossActive = false; s_bossTf = null; s_prevTarget = null; s_bossYawApplied = 0f;
|
||||
RestoreBossPreset(); // FIX-2: 프리셋은 순수 데이터(cCamData)라 카메라가 파괴돼도 되돌리는 쪽이 안전하다(살아 있으면 원값 복귀)
|
||||
CameraMoveLead.LeadMultiplier = 1f; s_leadUntil = -1f;
|
||||
s_runCount = 0; s_lastKillCam = -999f;
|
||||
}
|
||||
|
|
@ -345,8 +457,8 @@ namespace WL.CombatCamera
|
|||
/// <summary>프로브용 카운터 초기화.</summary>
|
||||
public static void ResetDiagnostics()
|
||||
{
|
||||
KillCamCount = KillCamSkippedRole = KillCamSkippedCooldown = KillCamSkippedRunCap = KillCamSkippedGuard = KillCamSkippedNoCam = KillCamSkippedBusy = KillCamAbandoned = 0;
|
||||
BossCamCount = BossCamSkipped = BossPresetApplied = ComboLeadCount = 0;
|
||||
KillCamCount = KillCamSkippedRole = KillCamSkippedCooldown = KillCamSkippedRunCap = KillCamSkippedGuard = KillCamSkippedNoCam = KillCamSkippedBusy = KillCamAbandoned = KillCamRebased = 0;
|
||||
BossCamCount = BossCamSkipped = BossPresetApplied = BossPresetRestored = BossPresetAbandoned = ComboLeadCount = 0;
|
||||
LastKillCamMeasured = LastBossCamMeasured = 0f; LastVictimViewportY = -1f; LastInfo = "";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,12 +34,14 @@ namespace WL.Combat.Reaction
|
|||
|
||||
static Slot[] s_slots;
|
||||
static int s_used;
|
||||
static bool s_dispatching; // 리뷰 Minor-5(811b FIX-5 계열) 재진입 가드 — Flushed 구독자가 새 피해를 일으켜도 슬롯이 깨지지 않게
|
||||
|
||||
// ── 진단
|
||||
public static bool Subscribed;
|
||||
public static int HitCount, FlushCount, MaxHitsMerged, EvictCount;
|
||||
public static int HitCount, FlushCount, MaxHitsMerged, EvictCount, ReentrantHits;
|
||||
public static string LastInfo = "";
|
||||
public static int ActiveSlots { get { return s_used; } }
|
||||
public static bool Dispatching { get { return s_dispatching; } }
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)]
|
||||
static void Register()
|
||||
|
|
@ -60,6 +62,7 @@ namespace WL.Combat.Reaction
|
|||
|
||||
static void EnsureSlots(WLReactionSettings st)
|
||||
{
|
||||
if (s_slots != null && s_dispatching) return; // Dispatch 중에는 배열을 갈아끼우지 않는다 — 바깥 순회가 보던 배열이 그대로 유지된다
|
||||
int n = Mathf.Max(4, st.damageMergeSlots);
|
||||
if (s_slots == null || s_slots.Length != n) { s_slots = new Slot[n]; s_used = 0; }
|
||||
}
|
||||
|
|
@ -75,6 +78,7 @@ namespace WL.Combat.Reaction
|
|||
WLReactionRunner.Ensure();
|
||||
float now = Time.unscaledTime;
|
||||
HitCount++;
|
||||
if (s_dispatching) ReentrantHits++; // 구독자가 Flushed 안에서 새 피해를 냈다(연쇄 피해 등) — 아래 슬롯 조작은 재진입 안전해야 한다
|
||||
|
||||
int idx = -1, free = -1, oldest = -1; float oldestStart = float.MaxValue;
|
||||
for (int i = 0; i < s_slots.Length; i++)
|
||||
|
|
@ -83,9 +87,12 @@ namespace WL.Combat.Reaction
|
|||
if (ReferenceEquals(s_slots[i].victim, e.victim)) { idx = i; break; }
|
||||
if (s_slots[i].start < oldestStart) { oldestStart = s_slots[i].start; oldest = i; }
|
||||
}
|
||||
// 리뷰 Minor-5: 슬롯을 잡기 **전에** Dispatch 하면 구독자가 그 빈 슬롯을 먼저 가져가 s_used 가 어긋난다
|
||||
// → 밀어내기는 Harvest(슬롯 비우기)만 먼저 하고, Dispatch 는 슬롯 조작이 다 끝난 뒤에 한다.
|
||||
DamageMergeEvent evicted = default(DamageMergeEvent); bool hasEvicted = false;
|
||||
if (idx < 0)
|
||||
{
|
||||
if (free < 0) { Flush(oldest, now); EvictCount++; free = oldest; }
|
||||
if (free < 0) { hasEvicted = Harvest(oldest, now, out evicted); if (hasEvicted) EvictCount++; free = oldest; }
|
||||
idx = free;
|
||||
s_slots[idx].used = true; s_slots[idx].victim = e.victim; s_slots[idx].total = 0d; s_slots[idx].maxSingle = 0d;
|
||||
s_slots[idx].hits = 0; s_slots[idx].anyCritical = false; s_slots[idx].isKill = false; s_slots[idx].byMainPC = e.byMainPC; s_slots[idx].start = now;
|
||||
|
|
@ -99,13 +106,22 @@ namespace WL.Combat.Reaction
|
|||
s_slots[idx].byMainPC |= e.byMainPC;
|
||||
s_slots[idx].pos = e.victim.Get_position();
|
||||
|
||||
// 슬롯 조작 완료 — 여기서부터 Dispatch(재진입해도 안전)
|
||||
if (hasEvicted) Dispatch(in evicted);
|
||||
if (e.isKill && st.damageMergeFlushOnKill) Flush(idx, now);
|
||||
}
|
||||
|
||||
static void Flush(int i, float now)
|
||||
{
|
||||
if (i < 0 || !s_slots[i].used) return;
|
||||
var ev = new DamageMergeEvent
|
||||
DamageMergeEvent ev;
|
||||
if (Harvest(i, now, out ev)) Dispatch(in ev);
|
||||
}
|
||||
|
||||
/// <summary>슬롯을 비우고 결과 이벤트를 만든다(Dispatch 없음 = 재진입 안전).</summary>
|
||||
static bool Harvest(int i, float now, out DamageMergeEvent ev)
|
||||
{
|
||||
if (i < 0 || s_slots == null || i >= s_slots.Length || !s_slots[i].used) { ev = default(DamageMergeEvent); return false; }
|
||||
ev = new DamageMergeEvent
|
||||
{
|
||||
victim = s_slots[i].victim, total = s_slots[i].total, hits = s_slots[i].hits, maxSingle = s_slots[i].maxSingle,
|
||||
anyCritical = s_slots[i].anyCritical, isKill = s_slots[i].isKill, byMainPC = s_slots[i].byMainPC,
|
||||
|
|
@ -113,8 +129,16 @@ namespace WL.Combat.Reaction
|
|||
};
|
||||
if (ev.hits > MaxHitsMerged) MaxHitsMerged = ev.hits;
|
||||
s_slots[i].used = false; s_slots[i].victim = null; s_used--;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>재진입 가드(811b FIX-5 계열 = 이전 값을 백업하고 복원)를 걸고 구독자에게 알린다.</summary>
|
||||
static void Dispatch(in DamageMergeEvent ev)
|
||||
{
|
||||
FlushCount++;
|
||||
Flushed.Dispatch(in ev);
|
||||
bool prev = s_dispatching; s_dispatching = true;
|
||||
try { Flushed.Dispatch(in ev); }
|
||||
finally { s_dispatching = prev; }
|
||||
if (Verbose) Log("플러시 victim=" + (ev.victim != null ? ev.victim.name : "?") + " total=" + ev.total.ToString("F0") + " hits=" + ev.hits + " cri=" + ev.anyCritical + " kill=" + ev.isKill);
|
||||
}
|
||||
|
||||
|
|
@ -122,9 +146,14 @@ namespace WL.Combat.Reaction
|
|||
{
|
||||
if (s_used <= 0 || s_slots == null) return;
|
||||
var st = St;
|
||||
float win = st != null ? st.damageMergeWindowSeconds : 0.25f;
|
||||
if (st == null) return; // 리뷰 Minor-7: 폴백 상수(값 이중 출처) 대신 그냥 반환 — 에셋이 없으면 Enabled 가 false 라 슬롯도 없다
|
||||
float win = st.damageMergeWindowSeconds;
|
||||
// Dispatch 안에서 새 피해가 들어와도 EnsureSlots 가 배열 재할당을 막으므로 s_slots 는 순회 내내 같은 배열이다.
|
||||
for (int i = 0; i < s_slots.Length; i++)
|
||||
if (s_slots[i].used && now - s_slots[i].start >= win) Flush(i, now);
|
||||
{
|
||||
DamageMergeEvent ev;
|
||||
if (s_slots[i].used && now - s_slots[i].start >= win && Harvest(i, now, out ev)) Dispatch(in ev);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>데이터 API — 대상의 합산 중 값(813g 가 미리 읽고 싶을 때). 슬롯이 없으면 false.</summary>
|
||||
|
|
@ -148,7 +177,7 @@ namespace WL.Combat.Reaction
|
|||
/// <summary>프로브용 카운터 초기화.</summary>
|
||||
public static void ResetDiagnostics()
|
||||
{
|
||||
HitCount = FlushCount = MaxHitsMerged = EvictCount = 0; LastInfo = "";
|
||||
HitCount = FlushCount = MaxHitsMerged = EvictCount = ReentrantHits = 0; LastInfo = "";
|
||||
Flushed.Raised = Flushed.Dispatched = 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ namespace WL.Combat.Reaction
|
|||
s_chainKills++;
|
||||
s_lastKill = now;
|
||||
KillCount++;
|
||||
if (s_chainKills > BestChainEver) BestChainEver = s_chainKills; // 리뷰 Minor-7: 티어 루프 안팎 중복 갱신을 여기 1곳으로
|
||||
|
||||
// 위치 링 + 무게중심(연쇄 안 최근 N개)
|
||||
s_pos[s_head] = e.position;
|
||||
|
|
@ -105,7 +106,6 @@ namespace WL.Combat.Reaction
|
|||
{
|
||||
if (tiers[i].kills != s_chainKills) continue;
|
||||
s_bestTier = i;
|
||||
if (s_chainKills > BestChainEver) BestChainEver = s_chainKills;
|
||||
bool burst = i >= st.clusterBurstMinTier && TrySpawnBurst(st, now);
|
||||
var ev = new KillChainTierEvent { tier = i, name = tiers[i].name, kills = s_chainKills, centroid = s_centroid, lastVictim = e.victim, burstSpawned = burst, time = now, frame = Time.frameCount };
|
||||
TierCount++;
|
||||
|
|
@ -114,14 +114,20 @@ namespace WL.Combat.Reaction
|
|||
if (Verbose) Log("티어 " + tiers[i].name + " kills=" + s_chainKills + " centroid=" + s_centroid.ToString("F1") + " burst=" + burst);
|
||||
break;
|
||||
}
|
||||
if (s_chainKills > BestChainEver) BestChainEver = s_chainKills;
|
||||
}
|
||||
|
||||
// ■ 불변식(리뷰 Minor-8ⓐ): s_burstHeld == true ⟺ EffectBudget.ClusterBurst 슬롯 1개를 우리가 잡고 있다.
|
||||
// 획득은 여기(TryAcquire 성공 직후 s_burstHeld = true)뿐이고 반납은 ReleaseBurst 뿐이다. 홀드가 만료됐는데 Tick 이 아직
|
||||
// 돌지 않은 사이에 다음 처치가 오면 **먼저 반납하고** 다시 잡는다 — 그러지 않으면 상한을 2 이상으로 올렸을 때 획득 2 : 반납 1 로 샌다.
|
||||
static bool TrySpawnBurst(WLReactionSettings st, float now)
|
||||
{
|
||||
if (!st.clusterBurstEnabled) return false;
|
||||
if (st.clusterBurstRequireInView && !EffectBudget.IsInView(s_centroid)) { BurstSkippedView++; return false; }
|
||||
if (s_burstHeld && now < s_burstReleaseAt) { BurstDeniedBudget++; return false; } // 우리 슬롯이 아직 살아 있음(동시 ≤ 1)
|
||||
if (s_burstHeld)
|
||||
{
|
||||
if (now < s_burstReleaseAt) { BurstDeniedBudget++; return false; } // 우리 슬롯이 아직 살아 있음(동시 ≤ 1)
|
||||
ReleaseBurst(); // 홀드 만료 · Tick 보다 먼저 도착 → 반납 후 재획득
|
||||
}
|
||||
if (!EffectBudget.TryAcquire(EffectBudgetKind.ClusterBurst)) { BurstDeniedBudget++; return false; }
|
||||
s_burstHeld = true;
|
||||
s_burstReleaseAt = now + Mathf.Max(0.05f, st.clusterBurstHoldSeconds);
|
||||
|
|
@ -154,8 +160,9 @@ namespace WL.Combat.Reaction
|
|||
if (s_chainKills > 0)
|
||||
{
|
||||
var st = St;
|
||||
float win = st != null ? st.killChainWindowSeconds : 3f;
|
||||
if (now - s_lastKill >= win) EndChain(now);
|
||||
// 리뷰 Minor-7: 폴백 상수(SO 기본값의 사본 = 값 이중 출처)를 두지 않는다 — 에셋이 없으면 Enabled 가 false 라 연쇄가 시작되지도 않는다.
|
||||
if (st == null) EndChain(now);
|
||||
else if (now - s_lastKill >= st.killChainWindowSeconds) EndChain(now);
|
||||
}
|
||||
if (s_burstHeld && now >= s_burstReleaseAt) ReleaseBurst();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,6 +119,7 @@ namespace WL.Combat.Reaction
|
|||
if (!s_dissolveHasHdr && !s_dissolveHasRange) { SkippedNoProperty++; return; }
|
||||
var s = GetState(victim, st);
|
||||
if (s == null) return;
|
||||
WLReactionRunner.Ensure(); // 리뷰 Minor-4: 첫 이벤트가 Killed 여도 씬 전환 훅이 붙어 s_states(파괴 액터 참조)가 씬을 넘지 않게
|
||||
float hdr = e.subRole == eSubRol.Boss ? st.dissolveEdgeHdrBoss : e.subRole == eSubRol.Elite ? st.dissolveEdgeHdrElite : st.dissolveEdgeHdrNormal;
|
||||
float range = e.subRole == eSubRol.Boss ? st.dissolveEdgeRangeBoss : e.subRole == eSubRol.Elite ? st.dissolveEdgeRangeElite : st.dissolveEdgeRangeNormal;
|
||||
if (hdr < 0f && range < 0f) return;
|
||||
|
|
@ -136,6 +137,8 @@ namespace WL.Combat.Reaction
|
|||
if (!s.blinkOn && !s.tint) return;
|
||||
s.blinkOn = false; s.tint = false; s.blinkReadyAt = 0f;
|
||||
var st = St;
|
||||
// 리뷰 Minor-8ⓒ: st == null(에셋 자체가 없음)이면 Apply 를 못 부르지만, 그 상태에서는 Enabled 가 false 라 애초에 MPB 를 쓴 적이 없다
|
||||
// (블록이 있다면 RestoreAll 의 st == null 경로가 SetPropertyBlock(null) 로 뗀다) → 잔류 0. ⓑ s_states 는 씬 전환(ClearAll)까지 유지 = 풀 크기 상한.
|
||||
if (st != null) Apply(s, st);
|
||||
Deactivate(s);
|
||||
ClearCount++;
|
||||
|
|
|
|||
Loading…
Reference in New Issue