Project_WL/AgentScripts/WL811s_Probe.cs

668 lines
40 KiB
C#
Raw 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-811s — 이펙트 크기 1차 조정 검증 프로브 (에디트 모드 전용 · Play 0 · 로그인 0 · 에셋 무수정)
//
// 실행 (CLAUDE.md §1: using 금지 · 네임스페이스 풀어 쓰기)
// unity command run_script --file AgentScripts/WL811s_Probe.cs --entry WL811s_Probe.Measure --timeout 1800
// unity command run_script --file AgentScripts/WL811s_Probe.cs --entry WL811s_Probe.Verify --timeout 1800
//
// Measure — 값을 정하기 위한 실측(코드 수정 전)
// A. 액터 높이 : PC 프리팹 렌더러 바운드 × ClassConfig.f_Scale · 존 몹 × MonsterList.f_DefaultScale ·
// 엘리트 × WLEliteSettings.scaleMultiplier · 보스 Anubis
// B. 이펙트 원 크기 : 프리팹 인스턴스화 → ParticleSystem.Simulate(t, withChildren, restart) 를 t 스윕하며
// 모든 렌더러 월드 바운드 합집합의 최대 수평 지름/높이 (= 프리팹 자체 localScale 포함한 「원 크기」)
// C. 화면 점유 : Main Camera 프리팹 fov + RealCamera 궤도식(Quaternion.Euler(h,yaw,pan) * Vector3.one)에
// 811o 오버라이드(h 76 · d 3 · up 2.4)를 넣어 카메라를 세우고 1080×1920 뷰포트에서 세로 점유율
//
// Verify — 코드/값을 넣은 뒤의 검증
// D. 16행 전/후 표 · 슬롯 4 화면 점유 ≤ 50 %
// E. EffectScaleGuard : 가짜 풀 루트에 진짜 이펙트 인스턴스를 넣고 활성/비활성 → 배율 적용·원복·멱등 · 규칙 매칭
// F. GC 0 · C8(enabled=0 → 무동작 · 스케일 원본)
//
// 에디트 모드 주의: 프로브가 만드는 오브젝트는 전부 HideAndDontSave + DestroyImmediate · 어떤 에셋도 쓰지 않는다.
public static class WL811s_Probe
{
const string kOutDir = "AgentScripts/staging/WL811s";
const string kMeasureOut = "AgentScripts/staging/WL811s/MEASURE.txt";
const string kVerifyOut = "AgentScripts/staging/WL811s/VERIFY.txt";
const string kPcPrefab = "Assets/Res_Addr/PC/Ai01.prefab";
const string kCamPrefab = "Assets/ResWork/Prefabs/Ingame/Main Camera.prefab";
const string kEffectDir = "Assets/Res_Addr/Effect/";
static System.Text.StringBuilder s_sb = new System.Text.StringBuilder(1 << 16);
static void L(string s) { s_sb.Append(s).Append('\n'); }
static System.Collections.Generic.List<string> s_err = 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_err.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 void Save(string path)
{
System.IO.Directory.CreateDirectory(kOutDir);
System.IO.File.WriteAllText(path, s_sb.ToString(), new System.Text.UTF8Encoding(false));
}
// ─────────────────────────────────────────────────────────────── 공용 측정
/// <summary>프리팹을 숨은 인스턴스로 띄운다(원 localScale 유지 · 원점 · 무회전).</summary>
static UnityEngine.GameObject Spawn(string path)
{
var pf = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.GameObject>(path);
if (pf == null) return null;
var go = UnityEngine.Object.Instantiate(pf);
go.hideFlags = UnityEngine.HideFlags.HideAndDontSave;
go.transform.position = UnityEngine.Vector3.zero;
go.transform.rotation = UnityEngine.Quaternion.identity;
go.transform.localScale = pf.transform.localScale;
return go;
}
/// <summary>활성 여부와 무관하게 모든 렌더러의 월드 바운드 합집합. 없으면 false.</summary>
static bool RendererBounds(UnityEngine.GameObject go, out UnityEngine.Bounds b, out int count)
{
b = new UnityEngine.Bounds();
count = 0;
var rs = go.GetComponentsInChildren<UnityEngine.Renderer>(true);
for (int i = 0; i < rs.Length; i++)
{
var r = rs[i];
if (r == null) continue;
var rb = r.bounds;
if (rb.size.sqrMagnitude <= 0.0000001f) continue;
if (count == 0) b = rb; else b.Encapsulate(rb);
count++;
}
return count > 0;
}
static UnityEngine.ParticleSystem.Particle[] s_buf = new UnityEngine.ParticleSystem.Particle[8192];
static System.Collections.Generic.List<float> s_radii = new System.Collections.Generic.List<float>(1 << 14);
/// <summary>파티클 실좌표(GetParticles)로 원점 기준 반경 분포를 만든다. 렌더러 바운드(=Unity 보수 컬링 바운드)는 쓰지 않는다.
/// diaMax = 전 파티클 최대 지름 · diaP90 = 90 백분위 지름(스트레이 1개에 흔들리지 않는 「덩치」) · hgt = y 폭.</summary>
static void MeasureEffect(string prefabName, out float diaMax, out float diaP90, out float hgt, out float baseScale, out int psCount, out int pCount, out string note)
{
diaMax = 0f; diaP90 = 0f; hgt = 0f; baseScale = 1f; psCount = 0; pCount = 0; note = "";
var go = Spawn(kEffectDir + prefabName + ".prefab");
if (go == null) { note = "noprefab"; return; }
try
{
baseScale = go.transform.localScale.x;
var pss = go.GetComponentsInChildren<UnityEngine.ParticleSystem>(true);
psCount = pss.Length;
// 🔴 재현성: 자동 랜덤 시드를 끄고 고정 시드를 준다(안 하면 같은 프리팹이 실행마다 ±16 % 다르게 나온다).
for (int i = 0; i < pss.Length; i++)
{
var ps = pss[i]; if (ps == null) continue;
try { ps.useAutoRandomSeed = false; ps.randomSeed = 8115u; } catch (System.Exception) { }
}
go.SetActive(true);
s_radii.Clear();
float yMin = 1e9f, yMax = -1e9f;
// 파티클이 아닌 메시(예: Effect_WLSwingArc 의 슬래시 메시)는 실 바운드가 정확하다
var mrs = go.GetComponentsInChildren<UnityEngine.MeshRenderer>(true);
for (int i = 0; i < mrs.Length; i++)
{
var mr = mrs[i];
if (mr == null || mr.GetComponent<UnityEngine.ParticleSystem>() != null) continue;
var mb = mr.bounds;
if (mb.size.sqrMagnitude <= 0.0000001f) continue;
float rr = new UnityEngine.Vector2(UnityEngine.Mathf.Abs(mb.center.x) + mb.extents.x, UnityEngine.Mathf.Abs(mb.center.z) + mb.extents.z).magnitude;
s_radii.Add(rr);
if (mb.min.y < yMin) yMin = mb.min.y;
if (mb.max.y > yMax) yMax = mb.max.y;
}
if (pss.Length > 0)
{
float[] ts = new float[] { 0.05f, 0.1f, 0.15f, 0.2f, 0.3f, 0.4f, 0.5f, 0.7f, 1.0f, 1.3f, 1.6f, 2.0f };
for (int k = 0; k < ts.Length; k++)
{
for (int i = 0; i < pss.Length; i++)
{
var ps = pss[i];
if (ps == null) continue;
if (ps.transform.parent != null && ps.transform.parent.GetComponentInParent<UnityEngine.ParticleSystem>() != null) continue;
try { ps.Simulate(ts[k], true, true, false); } catch (System.Exception) { }
}
for (int i = 0; i < pss.Length; i++)
{
var ps = pss[i];
if (ps == null) continue;
int n = 0;
try { n = ps.GetParticles(s_buf); } catch (System.Exception) { continue; }
if (n <= 0) continue;
if (n > pCount) pCount = n;
bool local = ps.main.simulationSpace == UnityEngine.ParticleSystemSimulationSpace.Local;
float ls = UnityEngine.Mathf.Abs(ps.transform.lossyScale.x); if (ls <= 0f) ls = 1f;
for (int q = 0; q < n; q++)
{
var p = s_buf[q];
var pos = local ? ps.transform.TransformPoint(p.position) : p.position;
float sz = p.GetCurrentSize(ps) * (local ? ls : 1f);
float rr = new UnityEngine.Vector2(pos.x, pos.z).magnitude + sz * 0.5f;
s_radii.Add(rr);
if (pos.y - sz * 0.5f < yMin) yMin = pos.y - sz * 0.5f;
if (pos.y + sz * 0.5f > yMax) yMax = pos.y + sz * 0.5f;
}
}
}
for (int i = 0; i < pss.Length; i++) { var ps = pss[i]; if (ps != null) { try { ps.Stop(true, UnityEngine.ParticleSystemStopBehavior.StopEmittingAndClear); } catch (System.Exception) { } } }
}
else note = "nops";
if (s_radii.Count == 0) { note = (note.Length > 0 ? note + "," : "") + "noparticle"; return; }
s_radii.Sort();
diaMax = s_radii[s_radii.Count - 1] * 2f;
int idx = UnityEngine.Mathf.Clamp(UnityEngine.Mathf.RoundToInt((s_radii.Count - 1) * 0.90f), 0, s_radii.Count - 1);
diaP90 = s_radii[idx] * 2f;
hgt = (yMax > yMin) ? (yMax - yMin) : 0f;
}
finally { UnityEngine.Object.DestroyImmediate(go); }
}
// ─────────────────────────────────────────────────────────────── A. 액터 높이
class ActorRow { public string kind, name, path; public float tableScale; public float rawH, rawD; public bool ok; }
static System.Collections.Generic.List<ActorRow> MeasureActors()
{
var list = new System.Collections.Generic.List<ActorRow>();
// PC (ClassConfig.f_Scale = 0.7 · 4 근접 클래스 공통 · 프리팹은 코스튬 테이블이 고른다 → 기본 Ai01)
list.Add(new ActorRow { kind = "PC", name = "Ai01 (10101/10102/10501/10502)", path = kPcPrefab, tableScale = 0.7f });
list.Add(new ActorRow { kind = "PC", name = "Ai02", path = "Assets/Res_Addr/PC/Ai02.prefab", tableScale = 0.7f });
list.Add(new ActorRow { kind = "PC", name = "Eri01", path = "Assets/Res_Addr/PC/Eri01.prefab", tableScale = 0.7f });
// 존 몹 (813e 813001~813004)
AddMob(list, "존1", "Mob/Batty_A", 1.5f); AddMob(list, "존1", "Mob/Batty_D", 1.5f);
AddMob(list, "존2", "Mob/Porin_A", 1.3f); AddMob(list, "존2", "Mob/Porin_E", 1.3f);
AddMob(list, "존3", "Mob/Racco_A", 1.1f); AddMob(list, "존3", "Mob/Rabby_Brown", 1.5f);
AddMob(list, "존4", "Mob/Devilu_A", 1.2f); AddMob(list, "존4", "Mob/Devilu_C", 1.2f);
// 엘리트 (813s · × scaleMultiplier)
AddMob(list, "엘리트1", "Elite/Aspethis_D", 2.3f); AddMob(list, "엘리트2", "Elite/Cute_Crab_C_Blue", 2.5f);
AddMob(list, "엘리트3", "Elite/Beeto_A", 3.0f); AddMob(list, "엘리트4", "Elite/Mummy_King", 2.3f);
// 보스
AddMob(list, "보스", "FieldBoss/Anubis", 1.5f);
for (int i = 0; i < list.Count; i++)
{
var r = list[i];
var go = Spawn(r.path);
if (go == null) { r.ok = false; continue; }
try
{
go.transform.localScale = UnityEngine.Vector3.one; // 원 크기(테이블 배율 전) 를 잰다
UnityEngine.Bounds b; int c;
if (RendererBounds(go, out b, out c)) { r.rawH = b.size.y; r.rawD = UnityEngine.Mathf.Max(b.size.x, b.size.z); r.ok = true; }
}
finally { UnityEngine.Object.DestroyImmediate(go); }
}
return list;
}
static void AddMob(System.Collections.Generic.List<ActorRow> l, string kind, string rel, float sc)
{
l.Add(new ActorRow { kind = kind, name = rel, path = "Assets/Res_Addr/Mobs/" + rel + ".prefab", tableScale = sc });
}
// ─────────────────────────────────────────────────────────────── C. 카메라 · 화면 점유
class CamInfo { public float fov, h, dist, up, yaw, pan; public UnityEngine.Vector3 camPos; public UnityEngine.Vector3 lookAt; }
static CamInfo BuildCam()
{
var ci = new CamInfo { fov = 60f, h = 76f, dist = 3f, up = 2.4f, yaw = 347.21f, pan = 0f };
var cam = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.GameObject>(kCamPrefab);
if (cam != null)
{
var c = cam.GetComponent<UnityEngine.Camera>();
if (c != null) ci.fov = c.fieldOfView;
var rc = cam.GetComponent<RealCamera>();
if (rc != null)
{
var t = typeof(RealCamera);
var fy = t.GetField("rotateAround", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (fy != null) ci.yaw = (float)fy.GetValue(rc);
var fp = t.GetField("cameraPan", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (fp != null) ci.pan = (float)fp.GetValue(rc);
}
}
var st = WL.CameraFraming.WLCameraFramingSettings.Instance;
if (st != null && st.framingEnabled) { ci.h = st.cameraHeight; ci.dist = st.distance; ci.up = st.distanceUp; }
// RealCamera 궤도식 그대로: rotateVector = Quaternion.Euler(h, yaw, pan) * Vector3.one
var rot = UnityEngine.Quaternion.Euler(ci.h, ci.yaw, ci.pan);
var rv = rot * UnityEngine.Vector3.one;
ci.camPos = UnityEngine.Vector3.up * ci.up - rv * ci.dist; // 타겟 = 원점
ci.lookAt = UnityEngine.Vector3.zero;
return ci;
}
/// <summary>원점(PC 발밑)에 지름 dia · 높이 hgt 의 구/원기둥이 있을 때 세로 화면 점유율(0~1).</summary>
static float ScreenFraction(CamInfo ci, float dia, float hgt, float anchorY)
{
var go = new UnityEngine.GameObject("__wl811s_cam");
go.hideFlags = UnityEngine.HideFlags.HideAndDontSave;
try
{
var cam = go.AddComponent<UnityEngine.Camera>();
cam.enabled = false;
cam.fieldOfView = ci.fov;
cam.aspect = 1080f / 1920f;
cam.nearClipPlane = 0.1f; cam.farClipPlane = 1000f;
go.transform.position = ci.camPos;
go.transform.LookAt(ci.lookAt);
float r = dia * 0.5f;
float yc = anchorY;
float minY = 1e9f, maxY = -1e9f;
for (int i = 0; i < 8; i++)
{
float a = i * (UnityEngine.Mathf.PI * 2f / 8f);
float x = UnityEngine.Mathf.Cos(a) * r, z = UnityEngine.Mathf.Sin(a) * r;
for (int k = 0; k < 2; k++)
{
float y = yc + (k == 0 ? -hgt * 0.5f : hgt * 0.5f);
var vp = cam.WorldToViewportPoint(new UnityEngine.Vector3(x, y, z));
if (vp.z <= 0f) continue;
if (vp.y < minY) minY = vp.y;
if (vp.y > maxY) maxY = vp.y;
}
}
if (maxY < minY) return 0f;
return maxY - minY;
}
finally { UnityEngine.Object.DestroyImmediate(go); }
}
// ─────────────────────────────────────────────────────────────── Measure 엔트리
public static string Measure()
{
s_sb.Length = 0; s_err.Clear(); Hook();
L("=== WL-811s MEASURE · " + System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " (에디트 모드 · Play 0) ===");
// A
L("");
L("[A] 액터 높이 (프리팹 렌더러 바운드 · 원 크기 → × 테이블 배율)");
L("kind\tname\trawH(m)\trawD(m)\ttableScale\t실높이(m)\t실지름(m)");
var actors = MeasureActors();
float pcH = 0f;
for (int i = 0; i < actors.Count; i++)
{
var r = actors[i];
if (!r.ok) { L(r.kind + "\t" + r.name + "\tMISSING"); continue; }
float h = r.rawH * r.tableScale, d = r.rawD * r.tableScale;
L(r.kind + "\t" + r.name + "\t" + r.rawH.ToString("F3") + "\t" + r.rawD.ToString("F3") + "\t" + r.tableScale.ToString("F2")
+ "\t" + h.ToString("F3") + "\t" + d.ToString("F3"));
if (r.kind == "PC" && pcH <= 0f) pcH = h;
}
float eliteMul = 1.3f;
var est = WL.Combat.Reaction.WLEliteSettings.Instance;
if (est != null) eliteMul = est.scaleMultiplier;
L("엘리트 배수(WLEliteSettings.scaleMultiplier) = " + eliteMul.ToString("F2") + " → 위 「엘리트*」 실높이 × " + eliteMul.ToString("F2"));
L("PC 기준 높이 = " + pcH.ToString("F3") + " m");
// B
L("");
L("[B] 이펙트 원 크기 (프리팹 localScale 포함 · Simulate t 스윕 · 파티클 실좌표 GetParticles)");
L("prefab\tbaseScale\tps\tpMax\tdiaP90(m)\tdiaMax(m)\thgt(m)\tnote");
string[] names = EffectNames();
var dict = new System.Collections.Generic.Dictionary<string, float>();
for (int i = 0; i < names.Length; i++)
{
float diaMax, diaP90, hgt, bs; int ps, pc; string note;
MeasureEffect(names[i], out diaMax, out diaP90, out hgt, out bs, out ps, out pc, out note);
dict[names[i]] = diaP90;
L(names[i] + "\t" + bs.ToString("F3") + "\t" + ps + "\t" + pc + "\t" + diaP90.ToString("F3") + "\t" + diaMax.ToString("F3") + "\t" + hgt.ToString("F3") + "\t" + note);
}
// C
L("");
var ci = BuildCam();
L("[C] 카메라 (Main Camera.prefab fov " + ci.fov.ToString("F1") + " · RealCamera yaw " + ci.yaw.ToString("F2") + " pan " + ci.pan.ToString("F2")
+ " · 811o 오버라이드 h " + ci.h.ToString("F1") + " d " + ci.dist.ToString("F2") + " up " + ci.up.ToString("F2") + ")");
L("camPos(타겟 원점 기준) = " + ci.camPos.ToString("F3") + " · 거리 " + ci.camPos.magnitude.ToString("F3")
+ " m · 내림각 " + (UnityEngine.Mathf.Asin(UnityEngine.Mathf.Clamp(ci.camPos.y / UnityEngine.Mathf.Max(0.0001f, ci.camPos.magnitude), -1f, 1f)) * UnityEngine.Mathf.Rad2Deg).ToString("F1") + "°");
L("세로 1080×1920 (aspect " + (1080f / 1920f).ToString("F4") + ") · 1 m 구가 PC 몸 중심(y=" + (pcH * 0.5f).ToString("F2") + ")에 있을 때 세로 점유 = "
+ (ScreenFraction(ci, 1f, 1f, pcH * 0.5f) * 100f).ToString("F1") + " %");
L("");
L("[C2] 현재 16행 슬롯 4 본체의 화면 세로 점유 (현재 scale · 앵커 Mid = PC 몸 중심)");
L("skill\tprefab\tscale\tdia0(m)\tdia*(m)\t세로점유%");
var so = WL.Combat.Reaction.WLSkillSpectacleSettings.Instance;
if (so != null && so.rows != null)
{
for (int i = 0; i < so.rows.Length; i++)
{
var row = so.rows[i];
if (row.slot != 4) continue;
string p = row.body.prefab;
float d0 = 0f; dict.TryGetValue(p, out d0);
float sc = row.body.scale > 0f ? row.body.scale : 1f;
float d = d0 * sc;
L(row.skillId + "\t" + p + "\t" + sc.ToString("F2") + "\t" + d0.ToString("F3") + "\t" + d.ToString("F3") + "\t"
+ (ScreenFraction(ci, d, d, pcH * 0.5f) * 100f).ToString("F1"));
}
}
else L("WLSkillSpectacleSettings 로드 실패");
L("");
L("콘솔 error/exception = " + s_err.Count);
for (int i = 0; i < s_err.Count && i < 10; i++) L(" " + s_err[i]);
Unhook();
Save(kMeasureOut);
return "MEASURE done · rows=" + actors.Count + " effects=" + names.Length + " err=" + s_err.Count + " → " + kMeasureOut;
}
// ─────────────────────────────────────────────────────────────── Verify 엔트리
// 발주 전 16행 값(main @ 22832ff5d 실측) — (skillId, charge, body, impact) · 1 = 그 단 없음
static float[][] Before()
{
return new float[][] {
new float[]{703101,-1f,1.2f,1.0f}, new float[]{703102,1.0f,1.3f,1.2f},
new float[]{703103,1.0f,1.4f,1.0f}, new float[]{702006,1.2f,1.5f,1.2f},
new float[]{703106,-1f,1.2f,1.0f}, new float[]{703105,1.0f,1.3f,1.0f},
new float[]{703104,1.0f,1.35f,-1f}, new float[]{702009,1.2f,1.6f,1.2f},
new float[]{703502,-1f,1.2f,1.0f}, new float[]{703503,1.0f,1.4f,1.0f},
new float[]{703501,-1f,1.3f,-1f}, new float[]{702005,1.2f,1.8f,1.2f},
new float[]{703505,-1f,1.2f,1.0f}, new float[]{703506,-1f,1.5f,1.0f},
new float[]{703504,1.0f,1.3f,-1f}, new float[]{702012,1.2f,1.5f,1.2f},
};
}
static UnityEngine.GameObject s_fakeInfoGo, s_poolRoot;
static InGameInfo s_prevInfo;
static void PoolBegin()
{
s_prevInfo = InGameInfo.Ins;
s_fakeInfoGo = new UnityEngine.GameObject("__wl811s_info");
s_fakeInfoGo.hideFlags = UnityEngine.HideFlags.HideAndDontSave;
s_poolRoot = new UnityEngine.GameObject("__wl811s_pool");
s_poolRoot.hideFlags = UnityEngine.HideFlags.HideAndDontSave;
var info = s_fakeInfoGo.AddComponent<InGameInfo>(); // 에디트 모드라 Awake 미실행 = 부작용 0
info.tf_Effects = s_poolRoot.transform;
InGameInfo.Ins = info;
}
static void PoolEnd()
{
InGameInfo.Ins = s_prevInfo;
if (s_poolRoot != null) UnityEngine.Object.DestroyImmediate(s_poolRoot);
if (s_fakeInfoGo != null) UnityEngine.Object.DestroyImmediate(s_fakeInfoGo);
s_poolRoot = null; s_fakeInfoGo = null;
}
static UnityEngine.GameObject PoolChild(string prefabName, bool active)
{
var pf = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.GameObject>(kEffectDir + prefabName + ".prefab");
if (pf == null) return null;
var go = UnityEngine.Object.Instantiate(pf, s_poolRoot.transform);
go.name = prefabName + "(Clone)"; // 원본 풀과 같은 이름 규약
go.hideFlags = UnityEngine.HideFlags.HideAndDontSave;
go.transform.localScale = pf.transform.localScale;
go.SetActive(active);
return go;
}
static float s_tick;
static void Tick() { s_tick += 1f; WL.Combat.Reaction.EffectScaleGuard.Tick(s_tick); }
static int s_pass, s_fail;
static void Chk(string name, bool ok, string detail)
{
if (ok) s_pass++; else s_fail++;
L((ok ? "PASS " : "🔴FAIL ") + name + " · " + detail);
}
static bool Near(float a, float b, float eps) { return UnityEngine.Mathf.Abs(a - b) <= eps; }
public static string Verify()
{
s_sb.Length = 0; s_err.Clear(); s_pass = 0; s_fail = 0; Hook();
L("=== WL-811s VERIFY · " + System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " (에디트 모드 · Play 0) ===");
UnityEditor.AssetDatabase.Refresh(); // 디스크에서 손으로 고친 .asset 을 다시 읽는다
UnityEngine.Resources.UnloadUnusedAssets();
WL.Combat.Reaction.WLSkillSpectacleSettings.ClearCache();
WL.Combat.Reaction.WLEffectScaleSettings.ClearCache();
var sk = WL.Combat.Reaction.WLSkillSpectacleSettings.Instance;
var es = WL.Combat.Reaction.WLEffectScaleSettings.Instance;
Chk("SO 로드", sk != null && es != null, "spectacle=" + (sk != null) + " effectScale=" + (es != null) + " rules=" + (es != null && es.rules != null ? es.rules.Length : -1));
// G. guid
L("");
L("[G] 새 에셋 guid → 경로(충돌 0 확인)");
string[][] gp = new string[][] {
new string[]{"8115a0c3e7d24b6f9a1c8e5d3b7f2049","Assets/WL/Combat/Reaction/WLEffectScaleSettings.cs"},
new string[]{"8115b1d4f8e35c709b2d9f6e4c803150","Assets/WL/Combat/Reaction/EffectScaleGuard.cs"},
new string[]{"8115c2e509f46d81ac3ea07f5d914261","Assets/WL/Combat/Reaction/EffectScaleGuardRunner.cs"},
new string[]{"8115d3f61a057e92bd4fb1806ea25372","Assets/WL/Combat/Settings/Resources/WL/WLEffectScaleSettings.asset"},
};
for (int i = 0; i < gp.Length; i++)
{
string got = UnityEditor.AssetDatabase.GUIDToAssetPath(gp[i][0]);
Chk("guid " + gp[i][0].Substring(0, 8), got == gp[i][1], got);
}
// D. 16행 전/후 · 화면 점유
var ci = BuildCam();
float pcH = 1.191f;
// 세로 50 % 가 되는 지름(이분 탐색 · 원 산식 그대로)
float lo = 0.1f, hi = 20f;
for (int i = 0; i < 40; i++) { float mid = (lo + hi) * 0.5f; if (ScreenFraction(ci, mid, mid, pcH * 0.5f) < 0.5f) lo = mid; else hi = mid; }
float capDia = (lo + hi) * 0.5f;
L("");
L("[D] 화면 점유 (fov " + ci.fov.ToString("F0") + " · 카메라 거리 " + ci.camPos.magnitude.ToString("F2") + " m · 1080×1920)");
L("세로 50 % 가 되는 지름 = " + capDia.ToString("F3") + " m (= 슬롯 4 본체 캡)");
var before = Before();
L("");
L("[D2] 16행 전/후 (scale · dia0 = 프리팹 원 지름 실측 · after = dia0 × new)");
L("skill\tslot\tlabel\tphase\tprefab\tdia0\tbefore\tafter\t지름before\t지름after\t세로%before\t세로%after");
var dia = new System.Collections.Generic.Dictionary<string, float>();
for (int i = 0; sk != null && sk.rows != null && i < sk.rows.Length; i++)
{
var row = sk.rows[i];
float[] b = null;
for (int k = 0; k < before.Length; k++) if ((int)before[k][0] == row.skillId) { b = before[k]; break; }
for (int ph = 0; ph < 3; ph++)
{
var v = ph == 0 ? row.charge : (ph == 1 ? row.body : row.impact);
if (string.IsNullOrEmpty(v.prefab)) continue;
float d0;
if (!dia.TryGetValue(v.prefab, out d0))
{
float dm, dp, hg, bs; int ps, pc; string nt;
MeasureEffect(v.prefab, out dm, out dp, out hg, out bs, out ps, out pc, out nt);
d0 = dp; dia[v.prefab] = d0;
}
float ob = b != null ? b[ph + 1] : -1f;
float ns = v.scale > 0f ? v.scale : 1f;
float anchor = v.anchor == eEffectLocation.Bottom ? 0f : (v.anchor == eEffectLocation.Top ? pcH : pcH * 0.5f);
L(row.skillId + "\t" + row.slot + "\t" + row.label + "\t" + (ph == 0 ? "charge" : (ph == 1 ? "body" : "impact")) + "\t" + v.prefab
+ "\t" + d0.ToString("F3") + "\t" + (ob < 0f ? "?" : ob.ToString("F3")) + "\t" + ns.ToString("F3")
+ "\t" + (ob < 0f ? "?" : (d0 * ob).ToString("F3")) + "\t" + (d0 * ns).ToString("F3")
+ "\t" + (ob < 0f ? "?" : (ScreenFraction(ci, d0 * ob, d0 * ob, anchor) * 100f).ToString("F1"))
+ "\t" + (ScreenFraction(ci, d0 * ns, d0 * ns, anchor) * 100f).ToString("F1"));
}
}
L("");
L("[D3] 슬롯 4 본체 캡 판정 (≤ 50 %)");
for (int i = 0; sk != null && sk.rows != null && i < sk.rows.Length; i++)
{
var row = sk.rows[i];
if (row.slot != 4 || string.IsNullOrEmpty(row.body.prefab)) continue;
float d0 = dia.ContainsKey(row.body.prefab) ? dia[row.body.prefab] : 0f;
float d = d0 * (row.body.scale > 0f ? row.body.scale : 1f);
float f = ScreenFraction(ci, d, d, pcH * 0.5f);
Chk("슬롯4 " + row.skillId + " " + row.body.prefab, f <= 0.5f + 0.005f,
"지름 " + d.ToString("F3") + " m · 세로 " + (f * 100f).ToString("F1") + " %");
}
// E. 가드
L("");
L("[E] EffectScaleGuard (가짜 풀 루트 = InGameInfo.tf_Effects · 진짜 이펙트 프리팹 인스턴스)");
WL.Combat.Reaction.EffectScaleGuard.ResetDiagnostics();
WL.Combat.Reaction.WLEffectScaleSettings.RuntimeOverride = 1;
bool prevByHeight = es != null && es.scaleByTargetHeight;
if (es != null) es.scaleByTargetHeight = false;
PoolBegin();
try
{
var stun = PoolChild("Effect_Stun", true);
var hit = PoolChild("Effect_Hit_10101", true);
var none = PoolChild("Effect_AnubisCharging", true); // 보스 패턴 = 규칙 없음 = 원본 유지
var poison = PoolChild("Effect_Poison", false); // 비활성으로 시작
var origStun = stun.transform.localScale;
var origHit = hit.transform.localScale;
var origNone = none.transform.localScale;
var origPoison = poison.transform.localScale;
int rStun = es.FindRule("Effect_Stun(Clone)", WL.Combat.Reaction.WLEffectScaleSettings.CoreLength("Effect_Stun(Clone)"));
int rNone = es.FindRule("Effect_AnubisCharging(Clone)", WL.Combat.Reaction.WLEffectScaleSettings.CoreLength("Effect_AnubisCharging(Clone)"));
Chk("규칙 매칭", rStun >= 0 && rNone < 0, "Effect_Stun → rule " + rStun + " (scale " + (rStun >= 0 ? es.rules[rStun].scale.ToString("F3") : "-") + ") · Effect_AnubisCharging → " + rNone + "(없음 = 원본)");
Tick();
float fStun = es.rules[rStun].scale;
Chk("① 활성 인스턴스 배율 적용", Near(stun.transform.localScale.x, origStun.x * fStun, 0.0005f),
"Effect_Stun " + origStun.x.ToString("F3") + " → " + stun.transform.localScale.x.ToString("F4") + " (기대 " + (origStun.x * fStun).ToString("F4") + ")");
Chk("② 규칙 없는 것은 무변경", Near(none.transform.localScale.x, origNone.x, 0.0001f),
"Effect_AnubisCharging " + none.transform.localScale.x.ToString("F4") + " (원본 " + origNone.x.ToString("F4") + ")");
Chk("③ 비활성은 아직 미적용", Near(poison.transform.localScale.x, origPoison.x, 0.0001f), "Effect_Poison " + poison.transform.localScale.x.ToString("F4"));
int ap1 = WL.Combat.Reaction.EffectScaleGuard.AppliedCount;
var afterFirst = stun.transform.localScale;
Tick(); Tick(); Tick();
Chk("④ 멱등(3회 더 틱)", Near(stun.transform.localScale.x, afterFirst.x, 0.000001f) && WL.Combat.Reaction.EffectScaleGuard.AppliedCount == ap1,
"scale " + stun.transform.localScale.x.ToString("F6") + " · Applied " + ap1 + " → " + WL.Combat.Reaction.EffectScaleGuard.AppliedCount);
stun.SetActive(false); Tick();
Chk("⑤ 반납(SetActive false) → 원 스케일 복귀", Near(stun.transform.localScale.x, origStun.x, 0.0001f),
stun.transform.localScale.x.ToString("F4") + " (원본 " + origStun.x.ToString("F4") + ")");
stun.SetActive(true); Tick();
Chk("⑥ 재활성 → 다시 적용", Near(stun.transform.localScale.x, origStun.x * fStun, 0.0005f), stun.transform.localScale.x.ToString("F4"));
poison.SetActive(true); Tick();
int rP = es.FindRule("Effect_Poison(Clone)", WL.Combat.Reaction.WLEffectScaleSettings.CoreLength("Effect_Poison(Clone)"));
Chk("⑦ 나중에 활성된 인스턴스도 적용", rP >= 0 && Near(poison.transform.localScale.x, origPoison.x * es.rules[rP].scale, 0.0005f),
"Effect_Poison → " + poison.transform.localScale.x.ToString("F4"));
// 대상 높이 옵션
L("");
L("[E2] scaleByTargetHeight (대상 액터 실높이로 재계산)");
var mobPf = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.GameObject>("Assets/Res_Addr/Mobs/Mob/Batty_A.prefab");
var mobGo = mobPf != null ? UnityEngine.Object.Instantiate(mobPf) : null;
if (mobGo != null)
{
mobGo.hideFlags = UnityEngine.HideFlags.HideAndDontSave;
mobGo.transform.position = UnityEngine.Vector3.zero;
var actor = mobGo.GetComponent<Actor>();
var fiTop = typeof(Actor).GetField("tf_Top", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
var fiT2 = typeof(TurnOff_GO).GetField("m_target2", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
Chk("리플렉션 필드 존재", actor != null && fiTop != null && fiT2 != null,
"Actor=" + (actor != null) + " Actor.tf_Top=" + (fiTop != null) + " TurnOff_GO.m_target2=" + (fiT2 != null));
if (actor != null && fiTop != null && fiT2 != null)
{
var topGo = new UnityEngine.GameObject("HUD_Top"); topGo.hideFlags = UnityEngine.HideFlags.HideAndDontSave;
topGo.transform.SetParent(mobGo.transform, false);
topGo.transform.position = new UnityEngine.Vector3(0f, 0.537f, 0f); // 존1 Batty 실높이(MEASURE [A])
fiTop.SetValue(actor, topGo.transform);
var off = stun.GetComponent<TurnOff_GO>();
Chk("Effect_Stun 에 TurnOff_GO 존재", off != null, off != null ? "ok" : "없음");
if (off != null)
{
fiT2.SetValue(off, actor);
es.scaleByTargetHeight = true;
stun.SetActive(false); Tick(); stun.SetActive(true); Tick();
float expect = UnityEngine.Mathf.Clamp((0.537f * es.rules[rStun].targetRatio) / es.rules[rStun].baseDiameter, es.minScale, es.maxScale);
Chk("⑧ 대상 높이 기반 재계산", Near(stun.transform.localScale.x, origStun.x * expect, 0.0005f),
"몹 높이 0.537 m × " + es.rules[rStun].targetRatio + " ÷ " + es.rules[rStun].baseDiameter + " = " + expect.ToString("F4")
+ " → scale " + stun.transform.localScale.x.ToString("F4") + " · 지름 " + (es.rules[rStun].baseDiameter * expect).ToString("F3") + " m"
+ " · HeightResolved " + WL.Combat.Reaction.EffectScaleGuard.HeightResolved);
fiT2.SetValue(off, null);
int miss0 = WL.Combat.Reaction.EffectScaleGuard.HeightMissed;
stun.SetActive(false); Tick(); stun.SetActive(true); Tick();
Chk("⑨ 대상 없으면 고정 배율로 폴백", Near(stun.transform.localScale.x, origStun.x * fStun, 0.0005f) && WL.Combat.Reaction.EffectScaleGuard.HeightMissed > miss0,
"scale " + stun.transform.localScale.x.ToString("F4") + " · HeightMissed " + miss0 + " → " + WL.Combat.Reaction.EffectScaleGuard.HeightMissed);
es.scaleByTargetHeight = false;
}
}
UnityEngine.Object.DestroyImmediate(mobGo);
}
else Chk("몹 프리팹 로드", false, "Batty_A 없음");
// F. GC
L("");
L("[F] GC · C8");
stun.SetActive(true); hit.SetActive(true); Tick();
long m0 = System.GC.GetAllocatedBytesForCurrentThread();
long mono0 = UnityEngine.Profiling.Profiler.GetMonoUsedSizeLong();
for (int i = 0; i < 200000; i++) Tick();
long dAlloc = System.GC.GetAllocatedBytesForCurrentThread() - m0;
long dMono = UnityEngine.Profiling.Profiler.GetMonoUsedSizeLong() - mono0;
Chk("⑩ GC 0 (Tick ×200,000 · 워밍 후)", dAlloc == 0, "GetAllocatedBytesForCurrentThread Δ " + dAlloc + " B · MonoUsed Δ " + dMono + " B");
// C8
WL.Combat.Reaction.WLEffectScaleSettings.RuntimeOverride = -1;
Tick();
Chk("⑪ C8 (enabled = 0) → 전량 원복", Near(stun.transform.localScale.x, origStun.x, 0.0001f) && Near(hit.transform.localScale.x, origHit.x, 0.0001f)
&& WL.Combat.Reaction.EffectScaleGuard.ActiveScaled == 0,
"stun " + stun.transform.localScale.x.ToString("F4") + " · hit " + hit.transform.localScale.x.ToString("F4") + " · ActiveScaled " + WL.Combat.Reaction.EffectScaleGuard.ActiveScaled);
int ap2 = WL.Combat.Reaction.EffectScaleGuard.AppliedCount;
Tick(); Tick();
Chk("⑫ C8 상태에서 무동작", WL.Combat.Reaction.EffectScaleGuard.AppliedCount == ap2, "Applied " + ap2 + " → " + WL.Combat.Reaction.EffectScaleGuard.AppliedCount);
L("카운터: Sweep " + WL.Combat.Reaction.EffectScaleGuard.SweepCount + " · Tracked " + WL.Combat.Reaction.EffectScaleGuard.TrackedCount
+ " · Ruled " + WL.Combat.Reaction.EffectScaleGuard.RuledCount + " · Applied " + WL.Combat.Reaction.EffectScaleGuard.AppliedCount
+ " · Restored " + WL.Combat.Reaction.EffectScaleGuard.RestoredCount + " · Skipped " + WL.Combat.Reaction.EffectScaleGuard.SkippedCount);
}
finally
{
WL.Combat.Reaction.WLEffectScaleSettings.RuntimeOverride = 0;
if (es != null) es.scaleByTargetHeight = prevByHeight;
WL.Combat.Reaction.EffectScaleGuard.ResetDiagnostics();
PoolEnd();
}
L("");
L("RESULT " + (s_fail == 0 ? "PASS" : "FAIL") + " " + s_pass + "/" + (s_pass + s_fail) + " · 콘솔 error/exception = " + s_err.Count);
for (int i = 0; i < s_err.Count && i < 10; i++) L(" " + s_err[i]);
Unhook();
Save(kVerifyOut);
return "VERIFY " + (s_fail == 0 ? "PASS" : "FAIL") + " " + s_pass + "/" + (s_pass + s_fail) + " err=" + s_err.Count + " → " + kVerifyOut;
}
static string[] EffectNames()
{
return new string[]
{
// 16행(811i 매핑) — 차지/본체/착탄
"Effect_Berserk","Effect_PCDash","Effect_Howling","Effect_Casting_Energyball","Effect_Casting_ManaShield","Effect_Casting_Meteor",
"Effect_Slash_10101_1","Effect_Slash_10101_2","Effect_Slash_10101_3","Effect_WLSwingArc",
"Effect_Slash_10102_1","Effect_Slash_10102_2","Effect_SwordShield","Effect_STR_EXTRA_SPLASH",
"Effect_Slash_10501_1","Effect_Slash_10501_2","Effect_Slash_10501_3","Effect_Heal",
"Effect_Slash_10502_1","Effect_Slash_10502_2","Effect_Aggro","ShineBulletFlash",
"Effect_Hit_10101","Effect_Hit_10102","Effect_Hit_10501","Effect_Hit_SweepAttack","Effect_Hit_Skill_EnergySlashFlash",
"Effect_Hit_Energyball","WindBullet_Hit","WaveShotHit","ShineBulletHit","Effect_Hit_Skill_CrossSlash","Effect_Hit_Skill_SpinSlash",
// 원본 풀 상태이상
"Effect_Stun","Effect_Poison","Effect_Burn","Effect_Blind","Effect_Curse","Effect_Slow","Effect_Freezing","Effect_Frost",
"Effect_HPBurn","Effect_ManaBurn","Effect_SilenceCurse","Effect_Blood","Effect_DefenceDecrease","Effect_Barrier","Effect_ManaShield",
"Effect_Cure","Effect_SpeedUp","Effect_StatUp","Effect_Reincarnation","Effect_VampirHeal",
// 원본 풀 히트(클래스 · 몹 · 보스)
"Effect_Hit_10201","Effect_Hit_10202","Effect_Hit_10301","Effect_Hit_10302","Effect_Hit_10303","Effect_Hit_10401","Effect_Hit_10402",
"Effect_Hit_10502","Effect_Hit_10601","Effect_Hit_IceBolt","Effect_Hit_Fireball","Effect_Hit_Firebolt","Effect_Hit_Electronic",
"Effect_Hit_Bead","Effect_Hit_BlackHole","Effect_Hit_AnubisSkill1","Effect_Hit_AnubisSkill2",
"Effect_Hit_BrotherPenguin_Attack","Effect_Hit_FlowerDryad_Attack","Effect_Hit_IceGolem_Attack","Effect_Hit_Shaga_Attack",
"Effect_Hit_Mob_HellfireFlames","Effect_Hit_RookieArcherSkill1","Effect_Hit_SummonGolem","Effect_Hit_SummonWorm",
"Effect_Hit_Skill_DaggerThrow","Effect_Hit_Skill_Frozen"
};
}
}