// 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 s_err = new System.Collections.Generic.List(); 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)); } // ─────────────────────────────────────────────────────────────── 공용 측정 /// 프리팹을 숨은 인스턴스로 띄운다(원 localScale 유지 · 원점 · 무회전). static UnityEngine.GameObject Spawn(string path) { var pf = UnityEditor.AssetDatabase.LoadAssetAtPath(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; } /// 활성 여부와 무관하게 모든 렌더러의 월드 바운드 합집합. 없으면 false. static bool RendererBounds(UnityEngine.GameObject go, out UnityEngine.Bounds b, out int count) { b = new UnityEngine.Bounds(); count = 0; var rs = go.GetComponentsInChildren(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 s_radii = new System.Collections.Generic.List(1 << 14); /// 파티클 실좌표(GetParticles)로 원점 기준 반경 분포를 만든다. 렌더러 바운드(=Unity 보수 컬링 바운드)는 쓰지 않는다. /// diaMax = 전 파티클 최대 지름 · diaP90 = 90 백분위 지름(스트레이 1개에 흔들리지 않는 「덩치」) · hgt = y 폭. 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(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(true); for (int i = 0; i < mrs.Length; i++) { var mr = mrs[i]; if (mr == null || mr.GetComponent() != 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() != 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 MeasureActors() { var list = new System.Collections.Generic.List(); // 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 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(kCamPrefab); if (cam != null) { var c = cam.GetComponent(); if (c != null) ci.fov = c.fieldOfView; var rc = cam.GetComponent(); 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; } /// 원점(PC 발밑)에 지름 dia · 높이 hgt 의 구/원기둥이 있을 때 세로 화면 점유율(0~1). 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(); 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(); 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(); // 에디트 모드라 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(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(); 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("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(); 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(); 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" }; } }