Project_WL/AgentScripts/WL811gh_Probe.cs

582 lines
38 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// PD 지시 #813 · 발주서 WL-811gh — 임팩트 티어 · PC 피격 림라이트/넉백 · 대시 잔상 검증 프로브
// (에디트 모드 전용 · Play 0 · 로그인 0 · 에셋 무수정)
//
// 실행 (CLAUDE.md §1: using 금지 · 네임스페이스 풀어 쓰기)
// unity command run_script --file AgentScripts/WL811gh_Probe.cs --entry WL811gh_Probe.Edit --timeout 600
//
// 무엇을 재는가
// A. 티어 4단 판정 — 가짜 HitConfirmed 4묶음(평타 · 크리 · 킬 · 보스킬) → 티어 인덱스 + 티어별 값 표
// (히트스톱 ms · 셰이크 배수 · 펀치 배수 · 점멸 색/시간 배수 · 초당 상한)
// B. PC 피격 — 가짜 Damaged(PC) → MPB 플래시 on → 만료 후 off(잔류 0) + 넉백 벡터(방향·거리·NavMesh 가드)
// C. 대시 잔상 — 가짜 Dash(began) → 고스트 3개 스폰 · 알파 페이드 · 풀 반환 · EffectBudget 반환
// D. GC — 각 경로 100회 반복의 GC.GetAllocatedBytesForCurrentThread() 증가분(0 B 목표)
// E. C8 — WLImpactTierSettings.RuntimeDisabled = true(= 에셋 enabled 0 과 같은 Enabled 게이트)
// → 티어 off(기존 WLHitFeel 2단 값) · 림라이트 0 · 고스트 0
// F. 소스 확인 — WLHitFeel.cs 가 티어를 읽는지 · MobHitFlash.cs 가 점멸 배수를 쓰는지 · 원본 diff 0
//
// 에디트 모드 주의
// · [RuntimeInitializeOnLoadMethod] 는 Play 에서만 돈다 → 구독이 없다. 그래서 Probe* 헬퍼로 핸들러를 직접 부른다.
// · 프로브가 만드는 오브젝트는 전부 HideAndDontSave + DestroyImmediate. NavMeshData 는 Add 후 Remove.
// · 게임 수치는 전부 SO 에서 읽는다(C45). 이 파일은 어떤 에셋도 쓰지 않는다.
public static class WL811gh_Probe
{
const string kOut = "AgentScripts/staging/WL811gh/EDIT_PROBE.txt";
const string kHitFeel = "Assets/WL/Feel/WLHitFeel.cs";
const string kBlink = "Assets/WL/Combat/Reaction/MobHitFlash.cs";
static System.Collections.Generic.List<string> s_log = new System.Collections.Generic.List<string>();
static bool s_hooked;
static void OnLog(string cond, string stack, UnityEngine.LogType t)
{
if (t == UnityEngine.LogType.Error || t == UnityEngine.LogType.Exception || t == UnityEngine.LogType.Assert)
s_log.Add(t + " | " + cond);
}
static void Hook() { if (!s_hooked) { UnityEngine.Application.logMessageReceived += OnLog; s_hooked = true; } }
static void Unhook() { if (s_hooked) { UnityEngine.Application.logMessageReceived -= OnLog; s_hooked = false; } }
// ── 프로브가 만든 임시 오브젝트
static UnityEngine.GameObject s_pcGo, s_mobGo, s_attackerGo, s_modelGo;
static PCActor s_pc;
static MobActor s_mob, s_bossMob, s_attacker;
static UnityEngine.AI.NavMeshDataInstance s_navInstance;
static bool s_navAdded;
static string s_navPath = "(none)";
public static string Edit()
{
var sb = new System.Text.StringBuilder();
sb.Append("# WL-811gh 프로브 (에디트 모드 · Play 0 · 로그인 0)\n");
sb.Append("unity=").Append(UnityEngine.Application.unityVersion)
.Append(" isPlaying=").Append(UnityEngine.Application.isPlaying)
.Append(" utc=").Append(System.DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss")).Append("\n\n");
Hook(); s_log.Clear();
try
{
Section0(sb);
BuildFakes(sb);
SectionA(sb);
SectionB(sb);
SectionC(sb);
SectionD(sb);
SectionE(sb);
}
catch (System.Exception ex) { sb.Append("\n!! EXCEPTION ").Append(ex.ToString()).Append("\n"); }
finally { Cleanup(); }
Unhook();
SectionF(sb);
sb.Append("\n## 프로브 실행 중 Error/Exception 로그: ").Append(s_log.Count).Append("\n");
for (int i = 0; i < s_log.Count && i < 20; i++) sb.Append(" ").Append(s_log[i]).Append("\n");
var dir = System.IO.Path.GetDirectoryName(kOut);
if (!System.IO.Directory.Exists(dir)) System.IO.Directory.CreateDirectory(dir);
System.IO.File.WriteAllText(kOut, sb.ToString(), new System.Text.UTF8Encoding(false));
return sb.ToString();
}
// ─────────────────────────────────────────────────────────────────────────
// 0. SO 로드
// ─────────────────────────────────────────────────────────────────────────
static void Section0(System.Text.StringBuilder sb)
{
sb.Append("## 0. 설정 SO\n");
var st = WL.Combat.Reaction.WLImpactTierSettings.Instance;
sb.Append("WLImpactTierSettings = ").Append(st != null ? "found" : "NULL")
.Append(" · Enabled=").Append(WL.Combat.Reaction.WLImpactTierSettings.Enabled)
.Append(" · ImpactTier.Active=").Append(WL.Combat.Reaction.ImpactTier.Active).Append("\n");
var feel = WL.Feel.WLHitFeel.Settings;
sb.Append("WLHitFeelSettings = ").Append(feel != null ? "found" : "NULL");
if (feel != null) sb.Append(" · 기존 2단 hitStop=").Append(feel.hitStopDuration).Append("/").Append(feel.hitStopKillDuration)
.Append(" shakeKillMul=").Append(feel.shakeKillMultiplier).Append(" shakeAmp=").Append(feel.shakeAmplitude);
sb.Append("\n");
var core = WL.Combat.Core.WLCombatCoreSettings.Instance;
sb.Append("WLCombatCoreSettings = ").Append(core != null ? "found" : "NULL");
if (core != null) sb.Append(" · afterImageMax=").Append(core.afterImageMax)
.Append(" hitStopPerSecondMax=").Append(core.hitStopPerSecondMax)
.Append(" layerMaxSeconds=").Append(core.layerMaxSeconds);
sb.Append("\n");
var blink = WL.Combat.Reaction.WLHitBlinkSettings.Instance;
sb.Append("WLHitBlinkSettings = ").Append(blink != null ? "found" : "NULL");
if (blink != null) sb.Append(" · blinkOnSeconds=").Append(blink.blinkOnSeconds).Append(" blinkColor=").Append(blink.blinkColor);
sb.Append("\n\n");
}
// ─────────────────────────────────────────────────────────────────────────
// 가짜 액터 · NavMesh
// ─────────────────────────────────────────────────────────────────────────
static void BuildFakes(System.Text.StringBuilder sb)
{
sb.Append("## 준비: 가짜 액터 · NavMesh\n");
// 비활성 GameObject 에 붙이면 Awake 가 돌지 않아 서버 데이터·테이블 의존이 생기지 않는다.
s_mobGo = new UnityEngine.GameObject("[811gh] mob");
s_mobGo.hideFlags = UnityEngine.HideFlags.HideAndDontSave;
s_mobGo.SetActive(false);
s_mob = s_mobGo.AddComponent<MobActor>();
s_mob.m_Role = eRole.Mob; s_mob.m_SubRole = eSubRol.None;
var bossGo = new UnityEngine.GameObject("[811gh] boss");
bossGo.hideFlags = UnityEngine.HideFlags.HideAndDontSave;
bossGo.SetActive(false);
s_bossMob = bossGo.AddComponent<MobActor>();
s_bossMob.m_Role = eRole.Mob; s_bossMob.m_SubRole = eSubRol.Boss;
s_bossMob.transform.SetParent(s_mobGo.transform, false); // Cleanup 한 번에
s_attackerGo = new UnityEngine.GameObject("[811gh] attacker(boss)");
s_attackerGo.hideFlags = UnityEngine.HideFlags.HideAndDontSave;
s_attackerGo.SetActive(false);
s_attacker = s_attackerGo.AddComponent<MobActor>();
s_attacker.m_Role = eRole.Mob; s_attacker.m_SubRole = eSubRol.Boss;
// PC — SkinnedMeshRenderer 가 있어야 잔상을 구울 수 있다 → 프로젝트의 실제 캐릭터 프리팹을 붙여 실측한다.
s_pcGo = new UnityEngine.GameObject("[811gh] pc");
s_pcGo.hideFlags = UnityEngine.HideFlags.HideAndDontSave;
s_pcGo.SetActive(false);
s_pc = s_pcGo.AddComponent<PCActor>();
s_pc.m_Role = eRole.PC; s_pc.m_SubRole = eSubRol.None;
string modelPath = FindSkinnedPrefab();
if (!string.IsNullOrEmpty(modelPath))
{
var prefab = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.GameObject>(modelPath);
if (prefab != null)
{
s_modelGo = UnityEngine.Object.Instantiate(prefab, s_pcGo.transform);
s_modelGo.hideFlags = UnityEngine.HideFlags.HideAndDontSave;
}
}
var smrs = s_pcGo.GetComponentsInChildren<UnityEngine.SkinnedMeshRenderer>(true);
var rends = s_pcGo.GetComponentsInChildren<UnityEngine.Renderer>(true);
sb.Append("PC 모델 = ").Append(string.IsNullOrEmpty(modelPath) ? "(못 찾음)" : modelPath)
.Append(" · SkinnedMeshRenderer=").Append(smrs.Length).Append(" · Renderer=").Append(rends.Length).Append("\n");
if (rends.Length > 0 && rends[0].sharedMaterial != null && rends[0].sharedMaterial.shader != null)
sb.Append("PC 셰이더[0] = ").Append(rends[0].sharedMaterial.shader.name).Append("\n");
// NavMesh — 넉백의 NavMesh 가드를 실제로 측정하기 위해 프로젝트의 NavMeshData 를 임시로 올린다.
AddNavMesh(sb);
sb.Append("\n");
}
static string FindSkinnedPrefab()
{
string[] roots = new string[] { "Assets/Res_Addr/PC", "Assets/Res_Addr", "Assets/ResWork" };
for (int r = 0; r < roots.Length; r++)
{
if (!UnityEditor.AssetDatabase.IsValidFolder(roots[r])) continue;
var guids = UnityEditor.AssetDatabase.FindAssets("t:Prefab", new string[] { roots[r] });
for (int i = 0; i < guids.Length && i < 400; i++)
{
var p = UnityEditor.AssetDatabase.GUIDToAssetPath(guids[i]);
var go = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.GameObject>(p);
if (go == null) continue;
var smr = go.GetComponentInChildren<UnityEngine.SkinnedMeshRenderer>(true);
if (smr != null && smr.sharedMesh != null) return p;
}
}
return null;
}
static void AddNavMesh(System.Text.StringBuilder sb)
{
var guids = UnityEditor.AssetDatabase.FindAssets("t:NavMeshData", new string[] { "Assets/WL", "Assets" });
for (int i = 0; i < guids.Length && i < 40; i++)
{
var p = UnityEditor.AssetDatabase.GUIDToAssetPath(guids[i]);
var data = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.AI.NavMeshData>(p);
if (data == null) continue;
s_navInstance = UnityEngine.AI.NavMesh.AddNavMeshData(data);
if (s_navInstance.valid) { s_navAdded = true; s_navPath = p; break; }
}
UnityEngine.AI.NavMeshHit hit;
bool any = UnityEngine.AI.NavMesh.SamplePosition(UnityEngine.Vector3.zero, out hit, 2000f, UnityEngine.AI.NavMesh.AllAreas);
sb.Append("NavMeshData = ").Append(s_navPath).Append(" · added=").Append(s_navAdded)
.Append(" · SamplePosition(0,0,0 r=2000)=").Append(any);
if (any)
{
sb.Append(" → ").Append(hit.position.ToString("F2"));
s_pcGo.transform.position = hit.position;
s_attackerGo.transform.position = hit.position + new UnityEngine.Vector3(1f, 0f, 0f); // PC 의 +X 쪽에 서 있다 → 넉백은 -X
}
sb.Append("\n");
}
static void Cleanup()
{
WL.Combat.Reaction.PcHitReaction.RestoreAll();
WL.Combat.Reaction.DashAfterImage.RestoreAll();
WL.Combat.Reaction.DashAfterImage.DestroyPool();
WL.Combat.Reaction.PcHitReaction.ClearAll();
WL.Combat.Reaction.DashAfterImage.ClearAll();
WL.Combat.Reaction.WLImpactTierSettings.RuntimeDisabled = false;
if (s_navAdded) { UnityEngine.AI.NavMesh.RemoveNavMeshData(s_navInstance); s_navAdded = false; }
DestroyGo(ref s_pcGo); DestroyGo(ref s_mobGo); DestroyGo(ref s_attackerGo);
s_modelGo = null; s_pc = null; s_mob = null; s_bossMob = null; s_attacker = null;
}
static void DestroyGo(ref UnityEngine.GameObject go)
{
if (go != null) UnityEngine.Object.DestroyImmediate(go);
go = null;
}
// ─────────────────────────────────────────────────────────────────────────
// A. 티어 4단 + 티어별 값
// ─────────────────────────────────────────────────────────────────────────
static void SectionA(System.Text.StringBuilder sb)
{
sb.Append("## A. 임팩트 티어 4단 (가짜 HitConfirmed 4묶음)\n");
WL.Combat.Reaction.ImpactTier.ResetDiagnostics();
var st = WL.Combat.Reaction.WLImpactTierSettings.Instance;
if (st == null) { sb.Append("SO 없음 — 건너뜀\n\n"); return; }
// 평균 기준선을 만든다(평타만 적재된다 · damageAverageSamples 만큼).
for (int i = 0; i < st.damageAverageSamples; i++)
{
WL.Combat.Reaction.ImpactTier.ClearStamp();
WL.Combat.Reaction.ImpactTier.Resolve(s_mob, MakeDinfo(100d, false, false, false), false);
}
sb.Append("평균 기준선 = ").Append(WL.Combat.Reaction.ImpactTier.Average.ToString("F1"))
.Append(" (표본 ").Append(st.damageAverageSamples).Append("회 × 100 dmg)\n");
var t0 = Tier(s_mob, 100d, false, false, false);
var t1 = Tier(s_mob, 100d, true, false, false);
var t1b = Tier(s_mob, 100d, false, true, false);
var t1c = Tier(s_mob, 400d, false, false, false);
var t2 = Tier(s_mob, 100d, false, false, true);
var t3 = Tier(s_bossMob, 100d, false, false, true);
sb.Append("판정: 평타=").Append(t0).Append(" · 크리=").Append(t1).Append(" · 스킬=").Append(t1b)
.Append(" · 평균×4(400dmg)=").Append(t1c).Append(" · 킬=").Append(t2).Append(" · 보스킬=").Append(t3).Append("\n");
sb.Append("기대: Light · Heavy · Heavy · Heavy · Kill · BossKill → ")
.Append(t0 == WL.Combat.Reaction.ImpactTierKind.Light &&
t1 == WL.Combat.Reaction.ImpactTierKind.Heavy &&
t1b == WL.Combat.Reaction.ImpactTierKind.Heavy &&
t1c == WL.Combat.Reaction.ImpactTierKind.Heavy &&
t2 == WL.Combat.Reaction.ImpactTierKind.Kill &&
t3 == WL.Combat.Reaction.ImpactTierKind.BossKill ? "PASS" : "FAIL").Append("\n\n");
sb.Append("| 티어 | 히트스톱 ms | 셰이크 배수 | 셰이크 진폭(m) | 펀치 배수 | 실펀치 | 점멸 색 배수 | 점멸 시간(s) | 초당 상한 |\n");
sb.Append("|---|---|---|---|---|---|---|---|---|\n");
var feel = WL.Feel.WLHitFeel.Settings;
var blink = WL.Combat.Reaction.WLHitBlinkSettings.Instance;
for (int i = 0; i < 4; i++)
{
var k = (WL.Combat.Reaction.ImpactTierKind)i;
float hs = WL.Combat.Reaction.ImpactTier.HitStopSeconds(k);
float sm = WL.Combat.Reaction.ImpactTier.ShakeMultiplier(k);
float pm = WL.Combat.Reaction.ImpactTier.PunchMultiplier(k);
float bc = WL.Combat.Reaction.ImpactTier.BlinkColorMultiplier(k);
float bt = WL.Combat.Reaction.ImpactTier.BlinkTimeMultiplier(k);
float amp = feel != null ? feel.shakeAmplitude * sm : 0f;
float realPunch = feel != null ? 1f + (UnityEngine.Mathf.Max(1f, feel.scalePunch) - 1f) * pm : 0f;
float bTime = blink != null ? blink.blinkOnSeconds * bt : 0f;
sb.Append("| ").Append(k).Append(" | ").Append((hs * 1000f).ToString("F0"))
.Append(" | ").Append(sm.ToString("F2")).Append(" | ").Append(amp.ToString("F3"))
.Append(" | ").Append(pm.ToString("F2")).Append(" | ").Append(realPunch.ToString("F3"))
.Append(" | ").Append(bc.ToString("F2")).Append(" | ").Append(bTime.ToString("F3"))
.Append(" | ").Append(st.tierMaxPerSecond != null && i < st.tierMaxPerSecond.Length ? st.tierMaxPerSecond[i].ToString("F0") : "-")
.Append(" |\n");
}
sb.Append("티어 카운트 Light/Heavy/Kill/BossKill = ")
.Append(WL.Combat.Reaction.ImpactTier.TierCount[0]).Append("/")
.Append(WL.Combat.Reaction.ImpactTier.TierCount[1]).Append("/")
.Append(WL.Combat.Reaction.ImpactTier.TierCount[2]).Append("/")
.Append(WL.Combat.Reaction.ImpactTier.TierCount[3])
.Append(" · 초당상한 강등=").Append(WL.Combat.Reaction.ImpactTier.DowngradedCount).Append("\n\n");
}
/// <summary>도장을 지우고 1건 판정한다(에디트 모드는 프레임이 멈춰 있어 메모이즈에 걸린다).</summary>
static WL.Combat.Reaction.ImpactTierKind Tier(Actor victim, double dmg, bool crit, bool skill, bool kill)
{
WL.Combat.Reaction.ImpactTier.ClearStamp();
return WL.Combat.Reaction.ImpactTier.Resolve(victim, MakeDinfo(dmg, crit, skill, kill), kill);
}
static DamageInfo MakeDinfo(double dmg, bool critical, bool skill, bool kill)
{
var d = new DamageInfo();
d.Beater = s_pc;
d.Damage = dmg;
d.Critical = critical ? eStat.CRI : eStat.None;
d.IsDeadByThisDamage = kill;
if (skill)
{
var pd = new ProjectileData();
pd.m_SkillListTableData = new SkillListTableData();
d.Beater_pd = pd;
}
return d;
}
// ─────────────────────────────────────────────────────────────────────────
// B. PC 피격 — MPB 플래시 + 넉백
// ─────────────────────────────────────────────────────────────────────────
static void SectionB(System.Text.StringBuilder sb)
{
sb.Append("## B. PC 피격 (가짜 Damaged)\n");
WL.Combat.Reaction.PcHitReaction.ResetDiagnostics();
var st = WL.Combat.Reaction.WLImpactTierSettings.Instance;
if (st == null) { sb.Append("SO 없음 — 건너뜀\n\n"); return; }
var before = s_pcGo.transform.position;
// ① 무적 피격 = 걸러진다(기준서 8-1 · 813tj HitVignette 의 ignoreInvincibleHits 와 같은 기본값)
WL.Combat.Reaction.PcHitReaction.ProbeDamaged(s_pc, s_attacker, 0d, true);
sb.Append("무적 피격 → flashOn=").Append(WL.Combat.Reaction.PcHitReaction.FlashOnCount)
.Append(" (기대 0 · pcHitSkipWhenInvincible=").Append(st.pcHitSkipWhenInvincible).Append(")\n");
// ② 보스 피격 = 림라이트 + 넉백
WL.Combat.Reaction.PcHitReaction.ProbeDamaged(s_pc, s_attacker, 120d, false);
bool blockOn = WL.Combat.Reaction.PcHitReaction.HasAnyBlock(s_pc);
sb.Append("보스 피격 → flashOn=").Append(WL.Combat.Reaction.PcHitReaction.FlashOnCount)
.Append(" · MPB 붙음=").Append(blockOn)
.Append(" · 넉백=").Append(WL.Combat.Reaction.PcHitReaction.KnockbackCount)
.Append(" · NavMesh 스킵=").Append(WL.Combat.Reaction.PcHitReaction.SkippedNoNavMesh).Append("\n");
sb.Append("넉백 벡터 = ").Append(WL.Combat.Reaction.PcHitReaction.LastKnockbackVector.ToString("F3"))
.Append(" · 거리=").Append(WL.Combat.Reaction.PcHitReaction.LastKnockbackDistance.ToString("F3"))
.Append("m (SO ").Append(st.pcHitKnockbackDistance).Append("m) · agentMove=")
.Append(WL.Combat.Reaction.PcHitReaction.LastKnockbackUsedAgent).Append("\n");
sb.Append("방향 검사(계획 벡터): 가해자 반대(-X) 기대 · x=")
.Append(WL.Combat.Reaction.PcHitReaction.LastKnockbackVector.x.ToString("F3"))
.Append(" → ").Append(WL.Combat.Reaction.PcHitReaction.LastKnockbackVector.x < 0f ? "PASS" : "FAIL").Append("\n");
// 위 가짜 PC 는 **비활성**이라 Tick 의 생산 가드(!isActiveAndEnabled)가 이동을 즉시 접는다 → 실이동은 B-2 에서 잰다.
float dur = UnityEngine.Mathf.Max(0.01f, st.pcHitKnockbackSeconds);
for (int i = 0; i < 10; i++) WL.Combat.Reaction.ImpactRunner.ForceTick(dur / 10f);
sb.Append("비활성 PC 실이동 = ").Append((s_pcGo.transform.position - before).magnitude.ToString("F3"))
.Append("m (기대 0 — 비활성 액터는 밀지 않는다 = 생산 가드)\n");
// 림라이트 만료 — flashSeconds 를 넘겨 틱하면 off 되고 MPB 가 떨어진다.
double t0 = UnityEngine.Time.realtimeSinceStartupAsDouble;
int guard = 0;
while (WL.Combat.Reaction.PcHitReaction.FlashOffCount == 0 && guard++ < 2000 &&
UnityEngine.Time.realtimeSinceStartupAsDouble - t0 < st.pcHitFlashSeconds + 1.0)
WL.Combat.Reaction.ImpactRunner.ForceTick(0.016f);
bool blockOff = WL.Combat.Reaction.PcHitReaction.HasAnyBlock(s_pc);
sb.Append("만료 → flashOff=").Append(WL.Combat.Reaction.PcHitReaction.FlashOffCount)
.Append(" · MPB 잔류=").Append(blockOff).Append(" (기대 false)\n");
sb.Append("B-1 판정: ").Append(WL.Combat.Reaction.PcHitReaction.FlashOnCount == 1 &&
WL.Combat.Reaction.PcHitReaction.FlashOffCount >= 1 && !blockOff &&
WL.Combat.Reaction.PcHitReaction.LastKnockbackVector.x < 0f ? "PASS" : "FAIL").Append("\n");
SectionB2(sb, st);
sb.Append("\n");
}
// B-2. 활성 PC 로 넉백 **실이동** 측정(에이전트 경로 · transform 경로 둘 다)
static void SectionB2(System.Text.StringBuilder sb, WL.Combat.Reaction.WLImpactTierSettings st)
{
sb.Append("### B-2. 넉백 실이동 (활성 PC · 최소 구성)\n");
int errBefore = s_log.Count;
UnityEngine.GameObject go = null;
try
{
go = new UnityEngine.GameObject("[811gh] pc2");
go.hideFlags = UnityEngine.HideFlags.HideAndDontSave;
go.SetActive(false);
UnityEngine.AI.NavMeshHit nh;
if (UnityEngine.AI.NavMesh.SamplePosition(UnityEngine.Vector3.zero, out nh, 2000f, UnityEngine.AI.NavMesh.AllAreas))
go.transform.position = nh.position;
var cube = UnityEngine.GameObject.CreatePrimitive(UnityEngine.PrimitiveType.Cube);
cube.transform.SetParent(go.transform, false);
var agent = go.AddComponent<UnityEngine.AI.NavMeshAgent>(); // Actor.Awake 가 m_NavMeshAgent.speed 를 읽는다
var pc2 = go.AddComponent<PCActor>();
pc2.m_Role = eRole.PC; pc2.m_SubRole = eSubRol.None;
go.SetActive(true); // 여기서 원본 Actor.Awake 가 돈다
s_attackerGo.transform.position = go.transform.position + new UnityEngine.Vector3(1f, 0f, 0f);
// ① 에이전트 경로(SO 기본 pcHitKnockbackUseAgentMove=1)
WL.Combat.Reaction.PcHitReaction.ResetDiagnostics();
var p0 = go.transform.position;
WL.Combat.Reaction.PcHitReaction.ProbeDamaged(pc2, s_attacker, 120d, false);
bool usedAgent = WL.Combat.Reaction.PcHitReaction.LastKnockbackUsedAgent;
float plan = WL.Combat.Reaction.PcHitReaction.LastKnockbackDistance;
float step = UnityEngine.Mathf.Max(0.01f, st.pcHitKnockbackSeconds) / 10f;
for (int i = 0; i < 12; i++) WL.Combat.Reaction.ImpactRunner.ForceTick(step);
var mv1 = go.transform.position - p0;
sb.Append("① agentMove=").Append(usedAgent).Append(" · isOnNavMesh=").Append(agent.isOnNavMesh)
.Append(" · 계획=").Append(plan.ToString("F3")).Append("m · 실이동=").Append(mv1.ToString("F3"))
.Append(" 길이=").Append(mv1.magnitude.ToString("F3")).Append("m\n");
// ② transform 경로(에이전트를 끄면 SO 스위치 pcHitKnockbackUseAgentMove=0 과 같은 분기)
agent.enabled = false;
WL.Combat.Reaction.PcHitReaction.ResetDiagnostics();
var p1 = go.transform.position;
WL.Combat.Reaction.PcHitReaction.ProbeDamaged(pc2, s_attacker, 120d, false);
float plan2 = WL.Combat.Reaction.PcHitReaction.LastKnockbackDistance;
for (int i = 0; i < 12; i++) WL.Combat.Reaction.ImpactRunner.ForceTick(step);
var mv2 = go.transform.position - p1;
sb.Append("② agentMove=").Append(WL.Combat.Reaction.PcHitReaction.LastKnockbackUsedAgent)
.Append(" · 계획=").Append(plan2.ToString("F3")).Append("m · 실이동=").Append(mv2.ToString("F3"))
.Append(" 길이=").Append(mv2.magnitude.ToString("F3")).Append("m\n");
sb.Append("B-2 판정(둘 중 하나라도 계획 거리 ±10% 이내 · 방향 -X): ")
.Append(((Near(mv1.magnitude, plan) && mv1.x < 0f) || (Near(mv2.magnitude, plan2) && mv2.x < 0f)) ? "PASS" : "FAIL").Append("\n");
}
catch (System.Exception ex) { sb.Append("B-2 예외: ").Append(ex.Message).Append("\n"); }
finally { if (go != null) UnityEngine.Object.DestroyImmediate(go); }
sb.Append("B-2 구간 Error/Exception 로그 = ").Append(s_log.Count - errBefore)
.Append(" (원본 Actor.Awake 를 최소 구성으로 돌린 구간 · 그 외 구간과 분리해 센다)\n");
}
static bool Near(float a, float b) { return b > 0.0001f && UnityEngine.Mathf.Abs(a - b) <= b * 0.1f; }
// ─────────────────────────────────────────────────────────────────────────
// C. 대시 잔상
// ─────────────────────────────────────────────────────────────────────────
static void SectionC(System.Text.StringBuilder sb)
{
sb.Append("## C. 대시 잔상 (가짜 Dash began)\n");
WL.Combat.Reaction.DashAfterImage.ResetDiagnostics();
WL.Combat.Core.EffectBudget.ResetAll();
var st = WL.Combat.Reaction.WLImpactTierSettings.Instance;
if (st == null) { sb.Append("SO 없음 — 건너뜀\n\n"); return; }
WL.Combat.Reaction.DashAfterImage.ProbeDashBegan(s_pc);
sb.Append("예약 = ").Append(WL.Combat.Reaction.DashAfterImage.PendingCount)
.Append(" (SO ghostCount=").Append(st.ghostCount).Append(")\n");
WL.Combat.Reaction.DashAfterImage.ProbeFlushPending(s_pc);
sb.Append("스폰 = ").Append(WL.Combat.Reaction.DashAfterImage.SpawnCount)
.Append(" · 활성=").Append(WL.Combat.Reaction.DashAfterImage.ActiveCount)
.Append(" · 풀=").Append(WL.Combat.Reaction.DashAfterImage.PoolSize)
.Append(" · BakeMesh=").Append(WL.Combat.Reaction.DashAfterImage.BakeCount).Append("\n");
sb.Append("스킵: 예산=").Append(WL.Combat.Reaction.DashAfterImage.SkippedBudget)
.Append(" 화면밖=").Append(WL.Combat.Reaction.DashAfterImage.SkippedOffscreen)
.Append(" 렌더러없음=").Append(WL.Combat.Reaction.DashAfterImage.SkippedNoRenderer)
.Append(" 셰이더없음=").Append(WL.Combat.Reaction.DashAfterImage.SkippedNoShader)
.Append(" 풀참=").Append(WL.Combat.Reaction.DashAfterImage.SkippedPoolFull).Append("\n");
sb.Append("머티리얼 = ").Append(WL.Combat.Reaction.DashAfterImage.MaterialReady ? WL.Combat.Reaction.DashAfterImage.MaterialShaderName : "(없음)")
.Append(" · 런타임 머티리얼 수=").Append(WL.Combat.Reaction.DashAfterImage.MaterialReady ? 1 : 0)
.Append(" · 색=").Append(WL.Combat.Reaction.DashAfterImage.LastColor.ToString("F2")).Append("\n");
sb.Append("EffectBudget(AfterImage) 사용=").Append(WL.Combat.Core.EffectBudget.Count(WL.Combat.Core.EffectBudgetKind.AfterImage))
.Append("/").Append(WL.Combat.Core.EffectBudget.Limit(WL.Combat.Core.EffectBudgetKind.AfterImage))
.Append(" · 거부=").Append(WL.Combat.Core.EffectBudget.DeniedCount).Append("\n");
// 페이드 → 반환
double t0 = UnityEngine.Time.realtimeSinceStartupAsDouble;
int guard = 0;
while (WL.Combat.Reaction.DashAfterImage.ActiveCount > 0 && guard++ < 4000 &&
UnityEngine.Time.realtimeSinceStartupAsDouble - t0 < st.ghostFadeSeconds + 1.0)
WL.Combat.Reaction.ImpactRunner.ForceTick(0.016f);
sb.Append("페이드 후 활성=").Append(WL.Combat.Reaction.DashAfterImage.ActiveCount)
.Append(" · 반환=").Append(WL.Combat.Reaction.DashAfterImage.ReleasedCount)
.Append(" · 예산 잔여=").Append(WL.Combat.Core.EffectBudget.Count(WL.Combat.Core.EffectBudgetKind.AfterImage)).Append("\n");
int expect = UnityEngine.Mathf.Min(st.ghostCount, WL.Combat.Core.EffectBudget.Limit(WL.Combat.Core.EffectBudgetKind.AfterImage));
sb.Append("판정: 스폰 ").Append(WL.Combat.Reaction.DashAfterImage.SpawnCount).Append("/기대 ").Append(expect)
.Append(" · 활성 0 · 예산 0 → ")
.Append(WL.Combat.Reaction.DashAfterImage.SpawnCount == expect &&
WL.Combat.Reaction.DashAfterImage.ActiveCount == 0 &&
WL.Combat.Core.EffectBudget.Count(WL.Combat.Core.EffectBudgetKind.AfterImage) == 0 ? "PASS" : "FAIL").Append("\n\n");
}
// ─────────────────────────────────────────────────────────────────────────
// D. GC 100회
// ─────────────────────────────────────────────────────────────────────────
static void SectionD(System.Text.StringBuilder sb)
{
sb.Append("## D. GC (100회 반복 · GC.GetAllocatedBytesForCurrentThread)\n");
var dinfo = MakeDinfo(100d, false, false, false);
// 워밍업 — 첫 호출의 캐시(상태 dict · MPB · 셰이더 실측 · 풀·머티리얼)는 제외한다.
for (int i = 0; i < 5; i++) { WL.Combat.Reaction.ImpactTier.ClearStamp(); WL.Combat.Reaction.ImpactTier.Resolve(s_mob, dinfo, false); }
long a0 = System.GC.GetAllocatedBytesForCurrentThread();
for (int i = 0; i < 100; i++) { WL.Combat.Reaction.ImpactTier.ClearStamp(); WL.Combat.Reaction.ImpactTier.Resolve(s_mob, dinfo, false); }
long a1 = System.GC.GetAllocatedBytesForCurrentThread();
sb.Append("ImpactTier.Resolve ×100 = ").Append(a1 - a0).Append(" B\n");
for (int i = 0; i < 5; i++) { WL.Combat.Reaction.PcHitReaction.ProbeDamaged(s_pc, s_attacker, 120d, false); WL.Combat.Reaction.ImpactRunner.ForceTick(0.5f); }
long b0 = System.GC.GetAllocatedBytesForCurrentThread();
for (int i = 0; i < 100; i++) { WL.Combat.Reaction.PcHitReaction.ProbeDamaged(s_pc, s_attacker, 120d, false); WL.Combat.Reaction.ImpactRunner.ForceTick(0.5f); }
long b1 = System.GC.GetAllocatedBytesForCurrentThread();
sb.Append("PcHitReaction 피격+틱 ×100 = ").Append(b1 - b0).Append(" B\n");
long c0 = System.GC.GetAllocatedBytesForCurrentThread();
for (int i = 0; i < 100; i++) WL.Combat.Reaction.ImpactRunner.ForceTick(0.016f);
long c1 = System.GC.GetAllocatedBytesForCurrentThread();
sb.Append("ImpactRunner.ForceTick ×100 = ").Append(c1 - c0).Append(" B\n");
// 잔상 — 풀·머티리얼이 이미 만들어진 뒤(2회차부터)의 스폰·반환 할당
for (int i = 0; i < 3; i++) { WL.Combat.Reaction.DashAfterImage.ForceSpawn(s_pc); WL.Combat.Reaction.DashAfterImage.ProbeExpireAll(); }
long d0 = System.GC.GetAllocatedBytesForCurrentThread();
for (int i = 0; i < 100; i++) { WL.Combat.Reaction.DashAfterImage.ForceSpawn(s_pc); WL.Combat.Reaction.DashAfterImage.ProbeExpireAll(); }
long d1 = System.GC.GetAllocatedBytesForCurrentThread();
sb.Append("DashAfterImage 스폰+반환 ×100 = ").Append(d1 - d0).Append(" B (BakeMesh 내부 할당 포함)\n");
sb.Append("판정(티어·틱 0 B): ").Append((a1 - a0) == 0 && (c1 - c0) == 0 ? "PASS" : "FAIL").Append("\n\n");
}
// ─────────────────────────────────────────────────────────────────────────
// E. C8 롤백
// ─────────────────────────────────────────────────────────────────────────
static void SectionE(System.Text.StringBuilder sb)
{
sb.Append("## E. C8 롤백 (RuntimeDisabled = 에셋 enabled 0 과 같은 Enabled 게이트)\n");
WL.Combat.Reaction.WLImpactTierSettings.RuntimeDisabled = true;
WL.Combat.Reaction.ImpactTier.ResetDiagnostics();
WL.Combat.Reaction.PcHitReaction.ResetDiagnostics();
WL.Combat.Reaction.DashAfterImage.ResetDiagnostics();
WL.Combat.Reaction.DashAfterImage.RestoreAll();
sb.Append("ImpactTier.Active = ").Append(WL.Combat.Reaction.ImpactTier.Active).Append(" (기대 false)\n");
var t = Tier(s_bossMob, 100d, true, true, true);
sb.Append("보스킬 상황 판정 = ").Append(t).Append(" (기대 Light = 티어 미사용)\n");
WL.Combat.Reaction.PcHitReaction.ProbeDamaged(s_pc, s_attacker, 120d, false);
sb.Append("PC 피격 → flashOn=").Append(WL.Combat.Reaction.PcHitReaction.FlashOnCount)
.Append(" 넉백=").Append(WL.Combat.Reaction.PcHitReaction.KnockbackCount).Append(" (기대 0/0)\n");
WL.Combat.Reaction.DashAfterImage.ProbeDashBegan(s_pc);
WL.Combat.Reaction.DashAfterImage.ProbeFlushPending(s_pc);
sb.Append("대시 → 스폰=").Append(WL.Combat.Reaction.DashAfterImage.SpawnCount)
.Append(" 활성=").Append(WL.Combat.Reaction.DashAfterImage.ActiveCount).Append(" (기대 0/0)\n");
// 기존 2단(WLHitFeel) 값이 그대로 살아 있는가
var feel = WL.Feel.WLHitFeel.Settings;
if (feel != null)
sb.Append("기존 2단 유지: hitStop=").Append(feel.hitStopDuration).Append("/").Append(feel.hitStopKillDuration)
.Append(" · shakeKillMul=").Append(feel.shakeKillMultiplier).Append(" · hitStopMax=").Append(feel.hitStopMax).Append("\n");
bool pass = !WL.Combat.Reaction.ImpactTier.Active &&
t == WL.Combat.Reaction.ImpactTierKind.Light &&
WL.Combat.Reaction.PcHitReaction.FlashOnCount == 0 &&
WL.Combat.Reaction.PcHitReaction.KnockbackCount == 0 &&
WL.Combat.Reaction.DashAfterImage.SpawnCount == 0;
sb.Append("판정: ").Append(pass ? "PASS" : "FAIL").Append("\n");
WL.Combat.Reaction.WLImpactTierSettings.RuntimeDisabled = false;
sb.Append("\n");
}
// ─────────────────────────────────────────────────────────────────────────
// F. 소스 확인
// ─────────────────────────────────────────────────────────────────────────
static void SectionF(System.Text.StringBuilder sb)
{
sb.Append("## F. 소스 확인 (원본 훅 0 · 배선)\n");
string feel = ReadAll(kHitFeel);
string blink = ReadAll(kBlink);
sb.Append("WLHitFeel.cs ImpactTier.Resolve 호출 = ").Append(Has(feel, "ImpactTier.Resolve")).Append("\n");
sb.Append("WLHitFeel.cs ImpactTier.HitStopSeconds = ").Append(Has(feel, "ImpactTier.HitStopSeconds")).Append("\n");
sb.Append("WLHitFeel.cs ImpactTier.ShakeMultiplier = ").Append(Has(feel, "ImpactTier.ShakeMultiplier")).Append("\n");
sb.Append("WLHitFeel.cs ImpactTier.PunchMultiplier = ").Append(Has(feel, "ImpactTier.PunchMultiplier")).Append("\n");
sb.Append("WLHitFeel.cs 기존 2단 폴백 보존(hitStopKillDuration) = ").Append(Has(feel, "st.hitStopKillDuration")).Append("\n");
sb.Append("MobHitFlash.cs 점멸 배수 적용(GetBlinkModifiers) = ").Append(Has(blink, "ImpactTier.GetBlinkModifiers")).Append("\n");
string[] originals = new string[] {
"Assets/Script/Character/Actor.cs", "Assets/Script/Character/PCActor.cs",
"Assets/Script/Character/MyActor.cs", "Assets/Script/Character/Mob/MobActor.cs"
};
for (int i = 0; i < originals.Length; i++)
{
string src = ReadAll(originals[i]);
sb.Append(System.IO.Path.GetFileName(originals[i])).Append(": 811gh 문자열 = ")
.Append(src != null && src.Contains("811gh") ? "있음(!)" : "없음").Append("\n");
}
sb.Append("\n");
}
static string ReadAll(string p) { return System.IO.File.Exists(p) ? System.IO.File.ReadAllText(p) : null; }
static string Has(string src, string needle) { return src != null && src.Contains(needle) ? "있음" : "없음(!)"; }
}