582 lines
38 KiB
C#
582 lines
38 KiB
C#
// 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) ? "있음" : "없음(!)"; }
|
||
}
|