From abcd038656d25934c3375a84662173e079c83df5 Mon Sep 17 00:00:00 2001 From: swrring Date: Mon, 7 Sep 2026 14:01:36 +0900 Subject: [PATCH] =?UTF-8?q?WL=20=EC=84=B8=EC=85=98=203=20(#803~#806=20?= =?UTF-8?q?=C2=B7=20PD=20=EC=8A=B9=EC=9D=B8=202026-09-07)=20=E2=80=94=20?= =?UTF-8?q?=EC=84=B8=EB=A1=9C=20=EC=A0=84=EC=9A=A9=20=EC=A0=84=ED=99=98=20?= =?UTF-8?q?=C2=B7=20Attack3=20=EA=B2=80=EA=B8=B0=20=EB=AC=B4=EA=B8=B0=20?= =?UTF-8?q?=EC=A0=95=ED=95=A9(ShowEffect=200.25=20=C2=B7=20editbake=20?= =?UTF-8?q?=EC=BA=98=EB=A6=AC=EB=B8=8C=EB=A0=88=EC=9D=B4=EC=85=98=204?= =?UTF-8?q?=ED=96=89)=20=C2=B7=20=ED=83=80=EA=B2=A9=EA=B0=90=20midplus=20?= =?UTF-8?q?=C2=B7=20=EA=B3=B5=EC=9A=A9=20=ED=8C=9D=EC=97=85=20=EC=84=B8?= =?UTF-8?q?=EB=A1=9C=20=ED=81=AC=EA=B8=B0=20=C2=B7=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EB=8F=84=EA=B5=AC(SuppressPets=20=EB=B3=B5=EC=9B=90=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=C2=B7=20ComboSpeed/Trace3=20=C2=B7=20?= =?UTF-8?q?=EC=8B=A0=EA=B7=9C=20WL803/804/806=20=EB=8F=84=EA=B5=AC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- AgentScripts/WL760_Seq.cs | 17 +- AgentScripts/WL792_Verify.cs | 90 +++++++++ AgentScripts/WL797_MakeAsset.cs | 11 +- AgentScripts/WL803_Portrait.cs | 42 ++++ AgentScripts/WL804_Bake.cs | 191 ++++++++++++++++++ AgentScripts/WL804_TipProbe.cs | 130 ++++++++++++ AgentScripts/WL804_Tools.cs | 112 ++++++++++ AgentScripts/WL806_Popup.cs | 101 +++++++++ AgentScripts/staging/WL_Combat4/WL778_Seq.cs | 17 +- .../UIPrefabs/Title/SortOrder_5.prefab | 18 +- .../OneHand/Knight@Attack3_S.FBX.meta | 8 +- Assets/WL/Combat/CombatAnimMapping.asset | 8 +- .../Resources/WL/WLHitFeelSettings.asset | 8 +- .../Resources/WL/SlashTrailSettings.asset | 102 +++++++--- ProjectSettings/ProjectSettings.asset | 8 +- 15 files changed, 799 insertions(+), 64 deletions(-) create mode 100644 AgentScripts/WL803_Portrait.cs create mode 100644 AgentScripts/WL804_Bake.cs create mode 100644 AgentScripts/WL804_TipProbe.cs create mode 100644 AgentScripts/WL804_Tools.cs create mode 100644 AgentScripts/WL806_Popup.cs diff --git a/AgentScripts/WL760_Seq.cs b/AgentScripts/WL760_Seq.cs index 149d83e84..db7d9dd1b 100644 --- a/AgentScripts/WL760_Seq.cs +++ b/AgentScripts/WL760_Seq.cs @@ -117,10 +117,23 @@ public static class WL760_Seq return t != null ? t.name : "(none)"; } // 펫이 있으면 같은 몹을 때려 데미지 귀속이 오염된다 → 런 동안 매 프레임 꺼 두고 끝나면 켠다(게임이 도중에 다시 켤 수 있음). + // #806(2026-09-07) 수정: 복원(suppress=false) 때 "이 도구가 끈 펫"만 되살린다. + // 이전 버전은 씬의 비활성 PetActor 를 전부 켜서, 게임이 의도적으로 꺼 둔 펫(PCInfo.Make_Pet 의 Off() 잔여 · 펫 메뉴 미리보기 모델)까지 + // 되살렸다 → #792 검증 중 "펫 재등장" 의 유력 원인【추정】. 게임 코드 게이트(WLGameplaySettings.spawnPlayerPet)와 무관한 도구 결함. + static readonly System.Collections.Generic.HashSet s_suppressed = new System.Collections.Generic.HashSet(); static void SuppressPets(bool suppress) { - foreach (var p in UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None)) - if (p.gameObject.activeSelf == suppress) p.gameObject.SetActive(!suppress); + if (suppress) + { + foreach (var p in UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None)) + if (p.gameObject.activeSelf) { p.gameObject.SetActive(false); s_suppressed.Add(p.GetInstanceID()); } + } + else + { + foreach (var p in UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None)) + if (s_suppressed.Contains(p.GetInstanceID()) && !p.gameObject.activeSelf) p.gameObject.SetActive(true); + s_suppressed.Clear(); + } } public IEnumerator Co(MyActor me, int classId, string tag, float dist, bool noPet) diff --git a/AgentScripts/WL792_Verify.cs b/AgentScripts/WL792_Verify.cs index fed55587b..dd409e298 100644 --- a/AgentScripts/WL792_Verify.cs +++ b/AgentScripts/WL792_Verify.cs @@ -29,10 +29,12 @@ public static class WL792_Verify public static void Dirs(int classId) { Launch(h => h.Co_Dirs(classId), "dirs" + classId); } public static void Combo(int classId) { Launch(h => h.Co_Combo(classId), "combo" + classId); } + public static void ComboSpeed(int classId, float speed) { Launch(h => h.Co_ComboSpeed(classId, speed), "comboS" + classId); } // #804 클립 원속 재실측 public static void Moving(int classId) { Launch(h => h.Co_Moving(classId), "moving" + classId); } public static void Budget() { Launch(h => h.Co_Budget(), "budget"); } public static void Regress() { Launch(h => h.Co_Regress(), "regress"); } public static void Trace(int classId) { Launch(h => h.Co_Trace(classId), "trace" + classId); } + public static void Trace3(int classId) { Launch(h => h.Co_Trace3(classId), "trace3_" + classId); } // #804 Attack3 한 클립 프레임 추적 /// 런타임 캘리브레이션·기하 캐시를 비운다(설정/알고리즘을 바꾼 뒤 재실측). public static void Reset() @@ -351,6 +353,94 @@ public static class WL792_Verify Done(sb); } + // ── ②-S 콤보 연속 · 지정 공격 속도 (#804 · 2026-09-07) ────────────────── + // 런타임 공격 속도(FinalAttackSpeed 약 3.5)에서는 Attack3 스윙 창 안 샘플이 6~9 개뿐이라 + // 캘리브레이션 품질이 낮다(실측 path 0.25 m · Stab 오판). 클립 원속(1.0)으로 돌려 샘플을 채운다. + // 베이크된 캘리브레이션은 이벤트 시점에 캐릭터 로컬로 펼치므로 공격 속도와 무관하게 재사용된다. + public IEnumerator Co_ComboSpeed(int classId, float speed) + { + var sb = new StringBuilder(); + sb.AppendLine("# WL #804 콤보 연속(속도 " + speed.ToString("F2") + ") class=" + classId + " " + DateTime.Now.ToString("HH:mm:ss")); + yield return SetClass(sb, classId); + + var me = Me(); + if (me == null) { Done(sb); yield break; } + var anim = me.GetComponentInChildren(); + + int before = WeaponTrailDriver.MappedSpawnCount; + int shots = 0; + float gap = 1.3f / Mathf.Max(speed, 0.1f); + float t0 = Time.time; + me.Play_Attack(0, speed); + float tEnd = Time.time + 6f * gap + 3f; + while (Time.time < tEnd && shots < 6) + { + yield return null; + if (WeaponTrailDriver.MappedSpawnCount > before) + { + before = WeaponTrailDriver.MappedSpawnCount; + var p = WeaponTrailDriver.LastPlacement; + sb.AppendLine(" " + Row("comboS" + shots + " " + ClipName(anim), p)); + yield return new WaitForSeconds(0.05f); + yield return Cap(tag_ + "_seq" + shots); + shots++; + } + if (Time.time - t0 > gap && shots < 6) + { + me.Play_Attack(0, speed); + t0 = Time.time; + } + } + sb.AppendLine("캡처 " + shots + "장"); + Done(sb); + } + + // ── #804 Attack3 한 클립을 프레임마다 추적 — nt · 칼끝 이동 · 스윙 창 on/off · 배치 스폰 ───── + public IEnumerator Co_Trace3(int classId) + { + var sb = new StringBuilder(); + sb.AppendLine("# WL #804 Attack3 프레임 추적 class=" + classId + " " + DateTime.Now.ToString("HH:mm:ss")); + yield return SetClass(sb, classId); + var me = Me(); + if (me == null) { Done(sb); yield break; } + var anim = me.GetComponentInChildren(); + float aspd = (float)ASpd(me); + sb.AppendLine("FinalAttackSpeed=" + aspd.ToString("F3") + " animator.speed=" + anim.speed.ToString("F3")); + + float t0 = Time.time; float tEnd = Time.time + 12f; + me.Play_Attack(0, aspd); + bool recorded = false; + var rows = new List(); + while (Time.time < tEnd && !recorded) + { + yield return null; + if (Time.time - t0 > 1.2f) { me.Play_Attack(0, aspd); t0 = Time.time; } + string c = ClipName(anim); + if (!c.Contains("Attack3")) continue; + // Attack3 진입 — 클립이 끝날 때까지 매 프레임 기록 + var drv = me.GetComponent(); + Vector3 prevTip = drv != null ? drv.GetTipWorld(0) : Vector3.zero; + int spawn0 = WeaponTrailDriver.MappedSpawnCount; + int fi = 0; float path = 0f; + while (ClipName(anim).Contains("Attack3") && fi < 200) + { + var st = anim.GetCurrentAnimatorStateInfo(0); + float nt = st.normalizedTime % 1f; + Vector3 tip = drv != null ? drv.GetTipWorld(0) : Vector3.zero; + float step = Vector3.Distance(tip, prevTip); if (fi > 0) path += step; + rows.Add(string.Format(" f{0,-3} nt={1,5:F3} inTrans={2} speed={3:F2} tipStep={4:F3} path={5:F2} swing={6} spawned={7} localTip={8}", + fi, nt, anim.IsInTransition(0) ? 1 : 0, anim.speed, step, path, drv != null && drv.IsSwinging ? 1 : 0, + WeaponTrailDriver.MappedSpawnCount - spawn0, me.transform.InverseTransformPoint(tip).ToString("F2"))); + prevTip = tip; fi++; + yield return null; + } + recorded = true; + } + sb.AppendLine(recorded ? "Attack3 frames=" + rows.Count : "!! Attack3 not reached in 12 s"); + foreach (var r in rows) sb.AppendLine(r); + Done(sb); + } + // ── ③ 이동 중 공격 ────────────────────────────────────────────────── public IEnumerator Co_Moving(int classId) { diff --git a/AgentScripts/WL797_MakeAsset.cs b/AgentScripts/WL797_MakeAsset.cs index 7c84b77b5..2b7096463 100644 --- a/AgentScripts/WL797_MakeAsset.cs +++ b/AgentScripts/WL797_MakeAsset.cs @@ -62,7 +62,7 @@ public static class WL797_MakeAsset return sb.ToString(); } - /// 세기 프리셋. args: "weak" | "mid" | "strong" | "off" + /// 세기 프리셋. args: "weak" | "mid" | "midplus" | "strong" | "off" public static object Preset(string name) { var so = AssetDatabase.LoadAssetAtPath(Path); @@ -85,6 +85,13 @@ public static class WL797_MakeAsset so.shakeAmplitude = 0.10f; so.shakeDuration = 0.12f; so.scalePunch = 1.12f; break; + case "midplus": // B+ — PD 2026-09-07 "현재보다 미세하게 강하게" (mid 와 strong 의 중간) + case "mid+": + so.enabled = true; + so.hitStopDuration = 0.04f; so.hitStopKillDuration = 0.065f; + so.shakeAmplitude = 0.11f; so.shakeDuration = 0.12f; + so.scalePunch = 1.14f; + break; case "strong": // C — 발주 상한 so.enabled = true; so.hitStopDuration = 0.05f; so.hitStopKillDuration = 0.08f; @@ -92,7 +99,7 @@ public static class WL797_MakeAsset so.scalePunch = 1.16f; break; default: - return "UNKNOWN preset '" + name + "' (weak|mid|strong|off)"; + return "UNKNOWN preset '" + name + "' (weak|mid|midplus|strong|off)"; } EditorUtility.SetDirty(so); diff --git a/AgentScripts/WL803_Portrait.cs b/AgentScripts/WL803_Portrait.cs new file mode 100644 index 000000000..672a57a61 --- /dev/null +++ b/AgentScripts/WL803_Portrait.cs @@ -0,0 +1,42 @@ +// WL803_Portrait.cs — #803 세로(포트레이트) 전용 전환 (에디트 모드 · ProjectSettings.asset 만 수정 · PD 결정 2026-09-07) +// unity command run_script --file AgentScripts/WL803_Portrait.cs --entry WL803_Portrait.Dump +// unity command run_script --file AgentScripts/WL803_Portrait.cs --entry WL803_Portrait.Apply (세로 전용) +// unity command run_script --file AgentScripts/WL803_Portrait.cs --entry WL803_Portrait.Revert (가로 자동회전 원복 = 09-07 이전 값) +// 근거: 이전 값 defaultScreenOrientation 4(AutoRotation) · Portrait 0 · PortraitUpsideDown 0 · LandscapeRight 1 · LandscapeLeft 1. +// 코드의 방향 참조는 Assets/Script/Util/SafeArea.cs 하나(Portrait 기본 · 가로일 때만 분기) → 세로 전용에서 그대로 동작. +using UnityEditor; +using UnityEngine; + +public static class WL803_Portrait +{ + public static object Dump() + { + return string.Format("defaultInterfaceOrientation={0} · autorotate Portrait={1} PortraitUpsideDown={2} LandscapeLeft={3} LandscapeRight={4} · animatedAutorotation={5}", + PlayerSettings.defaultInterfaceOrientation, + PlayerSettings.allowedAutorotateToPortrait, PlayerSettings.allowedAutorotateToPortraitUpsideDown, + PlayerSettings.allowedAutorotateToLandscapeLeft, PlayerSettings.allowedAutorotateToLandscapeRight, + PlayerSettings.useAnimatedAutorotation); + } + + public static object Apply() + { + PlayerSettings.defaultInterfaceOrientation = UIOrientation.Portrait; + PlayerSettings.allowedAutorotateToPortrait = true; + PlayerSettings.allowedAutorotateToPortraitUpsideDown = false; + PlayerSettings.allowedAutorotateToLandscapeLeft = false; + PlayerSettings.allowedAutorotateToLandscapeRight = false; + AssetDatabase.SaveAssets(); + return "APPLIED portrait-only → " + Dump(); + } + + public static object Revert() + { + PlayerSettings.defaultInterfaceOrientation = UIOrientation.AutoRotation; + PlayerSettings.allowedAutorotateToPortrait = false; + PlayerSettings.allowedAutorotateToPortraitUpsideDown = false; + PlayerSettings.allowedAutorotateToLandscapeLeft = true; + PlayerSettings.allowedAutorotateToLandscapeRight = true; + AssetDatabase.SaveAssets(); + return "REVERTED landscape-autorotate → " + Dump(); + } +} diff --git a/AgentScripts/WL804_Bake.cs b/AgentScripts/WL804_Bake.cs new file mode 100644 index 000000000..b1dede1ae --- /dev/null +++ b/AgentScripts/WL804_Bake.cs @@ -0,0 +1,191 @@ +// WL804_Bake.cs — #804 Attack3 검기 캘리브레이션을 **에디트 모드 결정적 샘플링**으로 베이크 (PD 결정 2026-09-07 "3타 휘두르기 · 무기 방향에 정합") +// unity command run_script --file AgentScripts/WL804_Bake.cs --entry WL804_Bake.BakeAll --args "[false]" (dry · 수치만) +// unity command run_script --file AgentScripts/WL804_Bake.cs --entry WL804_Bake.BakeAll --args "[true]" (SlashTrailSettings 에 기록) +// unity command run_script --file AgentScripts/WL804_Bake.cs --entry WL804_Bake.Bake --args "[10101, 7.0, 12.0, 60.0, false]" +// +// 왜 에디트 모드 베이크인가 (실측 2026-09-07) +// · Attack3 클립(Knight@Attack3_S · 32f)의 실제 스윙은 f7~f12(nt 0.22~0.38)에서 칼끝이 약 3 m 움직이는 **가슴 높이 수평 스윙** +// (오른쪽 뒤 → 앞 → 왼쪽 앞)이다 — WL804_TipProbe(에디트 30fps) 와 WL792_Verify.Trace3(런타임 60fps) 두 실측이 일치. +// · 그런데 런타임 MeasureSwingCalibration 은 Attack3 에서만 path 0.20~0.25 m · 샘플 6~9 개를 돌려준다(Attack1/2 는 5 m · 25~29 개). +// 원인은 미규명(【미확인】 — 슬롯 선택/샘플 거리 필터 의심). 이벤트 시각을 f10→f8.5 로 앞당겨도 같았다. +// · 배치는 (클래스,클립) 캘리브레이션 행을 이벤트 시점에 캐릭터 로컬 → 월드로 펼치는 구조라, 행만 정확하면 런타임 실측과 무관하게 정합된다. +// 그래서 같은 모델·무기·클립을 프리뷰 씬에서 60 Hz 로 샘플해 MeasureSwingCalibration 과 **같은 수식**(SlashArcMeasure)으로 행을 만든다. +// · quality 는 런타임 재실측(BakeCalibrations 의 "더 좋을 때만 교체")이 이 행을 덮지 않도록 크게 둔다(source 에 editbake 표기). +using System.Linq; +using System.Text; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; +using WL.Combat; + +public static class WL804_Bake +{ + const string ModelPath = "Assets/Res_Addr/PC/Ai01.prefab"; + const string SettingsPath = "Assets/WL/Settings/Resources/WL/SlashTrailSettings.asset"; + const string ClassConfigJson = "Assets/ResWork/Table/Export/ClassConfig.json"; + const string Fbx3 = "Assets/Res_Addr/Animations/Animation/OneHand/Knight@Attack3_S.FBX"; + const float BakedQuality = 50f; // 런타임 q 는 2~6 → 이 행이 항상 우선 + + public static object BakeAll(bool apply) + { + var sb = new StringBuilder(); + foreach (var cid in new[] { 10101, 10102, 10501, 10502 }) + sb.Append(Bake(cid, 7f, 12f, 60f, apply)).AppendLine(); + return sb.ToString(); + } + + /// frame0~frame1 (30fps 프레임 단위 · 원 클립 기준) 을 hz 로 샘플해 캘리브레이션을 만든다. + public static object Bake(int classId, float frame0, float frame1, float hz, bool apply) + { + // ── 클래스 → 무기 (ClassConfig.json · 런타임 table_classconfig 은 에디트 모드에 없다) + string weaponName = null; int socketIndex = -1; + try + { + var arr = Newtonsoft.Json.Linq.JArray.Parse(System.IO.File.ReadAllText(ClassConfigJson)); + foreach (var row in arr) + { + if ((string)row["n_ClassID"] != classId.ToString()) continue; + weaponName = (string)row["s_WeaponRHPrefab"]; socketIndex = int.Parse((string)row["n_WeaponRHIndex"]); + if (string.IsNullOrEmpty(weaponName)) { weaponName = (string)row["s_WeaponLHPrefab"]; socketIndex = int.Parse((string)row["n_WeaponLHIndex"]); } + break; + } + } + catch (System.Exception ex) { return "ClassConfig parse fail: " + ex.Message; } + if (string.IsNullOrEmpty(weaponName) || socketIndex < 0) return "class " + classId + " weapon not found"; + + var model = AssetDatabase.LoadAssetAtPath(ModelPath); + var clips = AssetDatabase.LoadAllAssetsAtPath(Fbx3).OfType().Where(c => !c.name.StartsWith("__preview")).ToArray(); + var clip = clips.FirstOrDefault(c => c.name == "Attack3_S_" + classId) ?? clips.FirstOrDefault(c => c.name == "Attack3"); + var weapon = AssetDatabase.LoadAssetAtPath("Assets/Res_Addr/Weapon/" + weaponName + ".prefab"); + var settings = AssetDatabase.LoadAssetAtPath(SettingsPath); + if (model == null || clip == null || weapon == null || settings == null) + return "load fail model=" + (model != null) + " clip=" + (clip != null) + " weapon=" + (weapon != null) + "(" + weaponName + ") settings=" + (settings != null); + + var scene = EditorSceneManager.NewPreviewScene(); + var sb = new StringBuilder(); + bool animMode = false; + try + { + var go = (GameObject)PrefabUtility.InstantiatePrefab(model, scene); + go.transform.position = Vector3.zero; go.transform.rotation = Quaternion.identity; + var pc = go.GetComponentInChildren(true); + if (pc == null || pc.tfs_weapon == null || socketIndex >= pc.tfs_weapon.Length) return "tfs_weapon missing"; + var socket = pc.tfs_weapon[socketIndex]; + var w = (GameObject)PrefabUtility.InstantiatePrefab(weapon, scene); + w.transform.SetParent(socket, false); + w.transform.localPosition = Vector3.zero; w.transform.localRotation = Quaternion.identity; w.transform.localScale = Vector3.one; + + // MeasureSlot 규칙과 동일 — 가장 긴 렌더러 바운딩 장축 양끝 · 소켓에 가까운 쪽 = 자루 + Renderer best = null; Mesh bestMesh = null; float bestExtent = 0f; + foreach (var r in socket.GetComponentsInChildren(true)) + { + var mf = r.GetComponent(); var smr = r as SkinnedMeshRenderer; + var mesh = smr != null ? smr.sharedMesh : (mf != null ? mf.sharedMesh : null); + if (mesh == null) continue; + var ws = Vector3.Scale(mesh.bounds.size, Abs(r.transform.lossyScale)); + float longest = Mathf.Max(ws.x, Mathf.Max(ws.y, ws.z)); + if (longest > bestExtent) { bestExtent = longest; best = r; bestMesh = mesh; } + } + if (best == null) return "no renderer under socket"; + var b = bestMesh.bounds; var size = Vector3.Scale(b.size, Abs(best.transform.lossyScale)); + int axis = size.x >= size.y && size.x >= size.z ? 0 : (size.y >= size.z ? 1 : 2); + Vector3 axisDir = Vector3.zero; axisDir[axis] = 1f; + Vector3 eA = best.transform.TransformPoint(b.center - axisDir * b.extents[axis]); + Vector3 eB = best.transform.TransformPoint(b.center + axisDir * b.extents[axis]); + bool aIsHilt = (eA - socket.position).sqrMagnitude <= (eB - socket.position).sqrMagnitude; + Vector3 hiltLocal = socket.InverseTransformPoint(aIsHilt ? eA : eB); + Vector3 tipLocal = socket.InverseTransformPoint(aIsHilt ? eB : eA); + + var anim = go.GetComponentInChildren(); + var animGo = anim.gameObject; anim.applyRootMotion = false; + var root = go.transform; // 런타임 ToLocalPos/Dir 의 기준 = 액터 루트(위치+회전) + float t0 = frame0 / 30f, t1 = frame1 / 30f; + int n = Mathf.Max(3, Mathf.RoundToInt((t1 - t0) * hz) + 1); + var tips = new Vector3[n]; var hilts = new Vector3[n]; + AnimationMode.StartAnimationMode(); animMode = true; + for (int i = 0; i < n; i++) + { + float t = t0 + (t1 - t0) * i / (n - 1); + AnimationMode.BeginSampling(); + AnimationMode.SampleAnimationClip(animGo, clip, t); + AnimationMode.EndSampling(); + tips[i] = root.InverseTransformPoint(socket.TransformPoint(tipLocal)); + hilts[i] = root.InverseTransformPoint(socket.TransformPoint(hiltLocal)); + } + AnimationMode.StopAnimationMode(); animMode = false; + + // ── MeasureSwingCalibration 과 같은 수식 ───────────────────────────── + float fullPath = 0f, hiltPath = 0f, bladeAng = 0f; + Vector3 prevDir = (tips[0] - hilts[0]).normalized; + for (int i = 1; i < n; i++) + { + fullPath += Vector3.Distance(tips[i], tips[i - 1]); + hiltPath += Vector3.Distance(hilts[i], hilts[i - 1]); + var d = (tips[i] - hilts[i]).normalized; bladeAng += Vector3.Angle(prevDir, d); prevDir = d; + } + float tipPerHilt = fullPath / Mathf.Max(hiltPath, 1e-3f); + int kMax = 0; float dMax = 0f; + for (int i = 1; i < n; i++) { float d2 = (tips[i] - tips[0]).sqrMagnitude; if (d2 > dMax) { dMax = d2; kMax = i; } } + float chord = Mathf.Sqrt(dMax); + float pathToMax = 0f; for (int i = 1; i <= kMax; i++) pathToMax += Vector3.Distance(tips[i], tips[i - 1]); + float straight = pathToMax > 1e-5f ? chord / pathToMax : 0f; + bool swingShape = bladeAng >= settings.swingMinSweepDegrees || tipPerHilt >= settings.swingMinTipHiltRatio; + bool stabShape = chord >= settings.stabMinDistance && straight >= settings.stabMinStraightness; + var kind = swingShape ? SwingKind.Swing : (stabShape ? SwingKind.Stab : SwingKind.Swing); + + var fit = SlashArcMeasure.FitArcOrdered(tips, n); + sb.AppendLine(string.Format("== class {0} weapon {1}@{2} clip {3} f{4}~f{5} @{6}Hz n={7} · path={8:F2} hiltPath={9:F2} tip/hilt={10:F2} bladeAng={11:F0}° chord={12:F2} straight={13:F2} → kind={14} · fit.valid={15} sweep={16:F1}° r={17:F3}", + classId, weaponName, socket.name, clip.name, frame0, frame1, hz, n, fullPath, hiltPath, tipPerHilt, bladeAng, chord, straight, kind, fit.valid, fit.sweepDeg, fit.radiusOuter)); + if (!fit.valid) return sb.Append("!! 원 피팅 실패").ToString(); + + Vector3 nrm = fit.normal; Vector3 pivot = fit.center; + float tipR = SlashArcMeasure.MeanRadius(tips, n, pivot, nrm); + float hiltR = SlashArcMeasure.MeanRadius(hilts, n, pivot, nrm); + float dev = SlashArcMeasure.RadiusDeviation(tips, n, pivot, nrm, tipR); + string pivotSrc = "칼끝원"; + bool bad = tipR < 1e-3f || tipR <= hiltR * 1.15f || dev > tipR * 0.25f; + if (bad) + { + Vector3 tipCentroid = SlashArcMeasure.CentroidOnPlane(tips, n, nrm, tips[0]); + pivot = SlashArcMeasure.CentroidOnPlane(hilts, n, nrm, tipCentroid); + tipR = SlashArcMeasure.MeanRadius(tips, n, pivot, nrm); + hiltR = SlashArcMeasure.MeanRadius(hilts, n, pivot, nrm); + pivotSrc = "손잡이중심"; + } + Vector3 sd, bis; float sweep; + if (!SlashArcMeasure.RecomputeAboutPivot(tips, n, pivot, ref nrm, out sd, out bis, out sweep)) + return sb.Append("!! RecomputeAboutPivot 실패").ToString(); + float fitQ = Mathf.Clamp01(1f - (dev / Mathf.Max(tipR, 1e-3f)) * 2f); + float q = fullPath * Mathf.Max(fitQ, 0.05f); + sb.AppendLine(string.Format(" pivot({0})={1} tipR={2:F3} hiltR={3:F3} dev={4:F3} fitQ={5:F2} q={6:F2} · normal={7} bisector={8} startDir={9} sweep={10:F1}°{11}", + pivotSrc, V(pivot), tipR, hiltR, dev, fitQ, q, V(nrm), V(bis), V(sd), sweep, sweep < settings.swingMinSweepDegrees ? " (⚠ 런타임이면 칼날평면 폴백 구간 — 베이크는 원 피팅 그대로 기록)" : "")); + sb.AppendLine(" tips: " + string.Join(" ", tips.Select(V))); + + if (!apply) return sb.Append(" (dry)").ToString(); + + var cal = new SwingCalibration + { + classId = classId, clipName = "Effect_Slash_" + classId + "_3", kind = SwingKind.Swing, + planeNormal = nrm, bisector = bis, startDir = sd, pivot = pivot, tipRadius = tipR, hiltRadius = hiltR, sweepDeg = sweep, + stabAxis = (tips[kMax] - tips[0]).normalized, stabStart = tips[0], stabLength = Mathf.Max(chord, 1e-3f), + quality = Mathf.Max(BakedQuality, q), + source = string.Format("editbake {0:yyyy-MM-dd HH:mm} {1} f{2}~f{3}@{4}Hz n={5} path={6:F2} bladeAng={7:F0}° sweep={8:F1}° pivot={9} tipR={10:F3} hiltR={11:F3} dev={12:F3} fitQ={13:F2} q={14:F2} (WL804_Bake · 런타임 재실측이 덮지 않도록 quality 고정)", + System.DateTime.Now, clip.name, frame0, frame1, hz, n, fullPath, bladeAng, sweep, pivotSrc, tipR, hiltR, dev, fitQ, q) + }; + int removed = settings.swingCalibrations.RemoveAll(c => c != null && c.classId == classId && c.clipName == cal.clipName); + settings.swingCalibrations.Add(cal); + EditorUtility.SetDirty(settings); + AssetDatabase.SaveAssets(); + sb.AppendLine(" APPLIED → " + cal.clipName + " (기존 " + removed + "행 교체 · 총 " + settings.swingCalibrations.Count + "행)"); + } + finally + { + if (animMode) AnimationMode.StopAnimationMode(); + EditorSceneManager.ClosePreviewScene(scene); + } + return sb.ToString(); + } + + static string V(Vector3 v) { return string.Format("({0:F2},{1:F2},{2:F2})", v.x, v.y, v.z); } + static Vector3 Abs(Vector3 v) { return new Vector3(Mathf.Abs(v.x), Mathf.Abs(v.y), Mathf.Abs(v.z)); } +} diff --git a/AgentScripts/WL804_TipProbe.cs b/AgentScripts/WL804_TipProbe.cs new file mode 100644 index 000000000..ab472feed --- /dev/null +++ b/AgentScripts/WL804_TipProbe.cs @@ -0,0 +1,130 @@ +// WL804_TipProbe.cs — #804 공격 클립의 칼끝 궤적 실측 → ShowEffect 이벤트 시각을 "실제 스윙 구간"으로 옮기기 위한 근거 (에디트 모드 · 읽기 전용 · 프리뷰 씬) +// unity command run_script --file AgentScripts/WL804_TipProbe.cs --entry WL804_TipProbe.All +// unity command run_script --file AgentScripts/WL804_TipProbe.cs --entry WL804_TipProbe.Run --args "[\"Assets/Res_Addr/Animations/Animation/OneHand/Knight@Attack3_S.FBX\",\"Attack3\",\"Elven_Sword_01\",2,0.29305]" +// 방법: WL791_FootProbe 와 같은 프리뷰 씬 샘플링. Ai01.prefab(PCActor.tfs_weapon 소켓) 에 클래스 무기 프리팹(ClassConfig s_WeaponRHPrefab)을 +// 런타임 PCActor.Make_Weapon 과 같은 방식(소켓 자식 · 로컬 0/identity/1)으로 붙이고, WeaponTrailDriver.MeasureSlot 과 같은 규칙 +// (가장 긴 렌더러 바운딩의 장축 양끝 · 소켓에 가까운 쪽 = 자루)으로 칼끝/자루를 정한 뒤 30fps 프레임마다 루트 기준 위치를 기록한다. +// 출력: 프레임별 칼끝 이동(cm) · 칼날 방향 변화(deg) · 누적 경로 → "스윙 구간"(이동량 ≥ 최대의 30% 연속 구간) 과 +// 현재 ShowEffect 시각 + 창(swingWindowNormalized 0.35) 이 스윙을 얼마나 덮는지(경로 비율). +using System.Linq; +using System.Text; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; + +public static class WL804_TipProbe +{ + const string ModelPath = "Assets/Res_Addr/PC/Ai01.prefab"; + const float Window = 0.35f; // SlashTrailSettings.swingWindowNormalized (실측값 · 표시용) + + public static object All() + { + var sb = new StringBuilder(); + sb.Append(Run("Assets/Res_Addr/Animations/Animation/OneHand/Knight@Attack1_S.FBX", "Attack1", "Elven_Sword_01", 2, 0.29821f)).AppendLine(); + sb.Append(Run("Assets/Res_Addr/Animations/Animation/OneHand/Knight@Attack2_S.FBX", "Attack2", "Elven_Sword_01", 2, 0.26643f)).AppendLine(); + sb.Append(Run("Assets/Res_Addr/Animations/Animation/OneHand/Knight@Attack3_S.FBX", "Attack3", "Elven_Sword_01", 2, 0.29305f)).AppendLine(); + return sb.ToString(); + } + + public static object Run(string fbxPath, string clipName, string weaponName, int socketIndex, float currentEventNorm) + { + var model = AssetDatabase.LoadAssetAtPath(ModelPath); + var clips = AssetDatabase.LoadAllAssetsAtPath(fbxPath).OfType().Where(c => !c.name.StartsWith("__preview")).ToArray(); + var clip = clips.FirstOrDefault(c => c.name == clipName) ?? clips.FirstOrDefault(c => c.name.Contains(clipName)); + var weapon = AssetDatabase.LoadAssetAtPath("Assets/Res_Addr/Weapon/" + weaponName + ".prefab"); + if (model == null || clip == null || weapon == null) + return "load fail model=" + (model != null) + " clip=" + (clip != null) + " weapon=" + (weapon != null) + " clips=[" + string.Join(",", clips.Select(c => c.name)) + "]"; + + var scene = EditorSceneManager.NewPreviewScene(); + var sb = new StringBuilder(); + bool animMode = false; + try + { + var go = (GameObject)PrefabUtility.InstantiatePrefab(model, scene); + go.transform.position = Vector3.zero; go.transform.rotation = Quaternion.identity; + var pc = go.GetComponentInChildren(true); + if (pc == null || pc.tfs_weapon == null || socketIndex < 0 || socketIndex >= pc.tfs_weapon.Length) return "PCActor/tfs_weapon null or index out of range"; + var socket = pc.tfs_weapon[socketIndex]; + if (socket == null) return "socket null idx=" + socketIndex; + var w = (GameObject)PrefabUtility.InstantiatePrefab(weapon, scene); + w.transform.SetParent(socket, false); + w.transform.localPosition = Vector3.zero; w.transform.localRotation = Quaternion.identity; w.transform.localScale = Vector3.one; + + // MeasureSlot 규칙 + Renderer best = null; Mesh bestMesh = null; float bestExtent = 0f; + foreach (var r in socket.GetComponentsInChildren(true)) + { + var mf = r.GetComponent(); var smr = r as SkinnedMeshRenderer; + var mesh = smr != null ? smr.sharedMesh : (mf != null ? mf.sharedMesh : null); + if (mesh == null) continue; + var ws = Vector3.Scale(mesh.bounds.size, Abs(r.transform.lossyScale)); + float longest = Mathf.Max(ws.x, Mathf.Max(ws.y, ws.z)); + if (longest > bestExtent) { bestExtent = longest; best = r; bestMesh = mesh; } + } + if (best == null) return "no renderer under socket " + socket.name; + var b = bestMesh.bounds; var size = Vector3.Scale(b.size, Abs(best.transform.lossyScale)); + int axis = size.x >= size.y && size.x >= size.z ? 0 : (size.y >= size.z ? 1 : 2); + Vector3 axisDir = Vector3.zero; axisDir[axis] = 1f; + Vector3 eA = best.transform.TransformPoint(b.center - axisDir * b.extents[axis]); + Vector3 eB = best.transform.TransformPoint(b.center + axisDir * b.extents[axis]); + bool aIsHilt = (eA - socket.position).sqrMagnitude <= (eB - socket.position).sqrMagnitude; + Vector3 hiltLocal = socket.InverseTransformPoint(aIsHilt ? eA : eB); + Vector3 tipLocal = socket.InverseTransformPoint(aIsHilt ? eB : eA); + float bladeLen = Vector3.Distance(hiltLocal, tipLocal); + + var anim = go.GetComponentInChildren(); + if (anim == null) return "animator null"; + var animGo = anim.gameObject; anim.applyRootMotion = false; + float fps = clip.frameRate; int n = Mathf.RoundToInt(clip.length * fps); + var T = new Vector3[n + 1]; var H = new Vector3[n + 1]; + AnimationMode.StartAnimationMode(); animMode = true; + for (int i = 0; i <= n; i++) + { + AnimationMode.BeginSampling(); + AnimationMode.SampleAnimationClip(animGo, clip, i / fps); + AnimationMode.EndSampling(); + T[i] = animGo.transform.InverseTransformPoint(socket.TransformPoint(tipLocal)); + H[i] = animGo.transform.InverseTransformPoint(socket.TransformPoint(hiltLocal)); + } + sb.AppendLine("== " + clipName + " (" + clip.name + ") len=" + clip.length.ToString("F3") + "s frames=" + n + " fps=" + fps + + " weapon=" + weaponName + " socket=" + socket.name + " blade=" + best.name + " len=" + bladeLen.ToString("F2") + "m · currentShowEffect=" + currentEventNorm.ToString("F3") + " window=" + Window + " =="); + sb.AppendLine(" f norm | tip x y z | dTip(cm) | dDir(deg) | cum(cm)"); + var d = new float[n + 1]; float accum = 0f; + for (int i = 1; i <= n; i++) + { + d[i] = Vector3.Distance(T[i], T[i - 1]); accum += d[i]; + float ang = Vector3.Angle(T[i] - H[i], T[i - 1] - H[i - 1]); + sb.AppendLine(string.Format(" {0,3} {1,5:F3} | {2,6:F2} {3,5:F2} {4,5:F2} | {5,7:F1} | {6,6:F1} | {7,7:F1}", + i, i / (float)n, T[i].x, T[i].y, T[i].z, d[i] * 100f, ang, accum * 100f)); + } + float dmax = d.Max(); int imax = System.Array.IndexOf(d, dmax); + float thr = dmax * 0.30f; + int s = imax, e = imax; + while (s - 1 >= 1 && d[s - 1] >= thr) s--; + while (e + 1 <= n && d[e + 1] >= thr) e++; + float swingPath = 0f; for (int i = s; i <= e; i++) swingPath += d[i]; + sb.AppendLine(string.Format("SWING(dTip >= 30% of max {0:F1}cm @f{1}) = f{2}~f{3} -> norm {4:F3}~{5:F3} · path {6:F2} m / total {7:F2} m", + dmax * 100f, imax, s, e, s / (float)n, e / (float)n, swingPath, accum)); + float cover = Coverage(d, n, currentEventNorm, Window); + sb.AppendLine(string.Format("CURRENT ShowEffect {0:F3} + window {1:F2} (f{2}~f{3}) covers path {4:F2} m ({5:P0} of total)", + currentEventNorm, Window, Mathf.CeilToInt(currentEventNorm * n), Mathf.Min(n, Mathf.FloorToInt((currentEventNorm + Window) * n)), cover, accum > 0 ? cover / accum : 0f)); + float rec = Mathf.Max(0f, (s - 1) / (float)n); + float recCover = Coverage(d, n, rec, Window); + sb.AppendLine(string.Format("RECOMMEND ShowEffect ~ {0:F3} (f{1}) -> window covers {2:F2} m ({3:P0}) · window end f{4}{5}", + rec, s - 1, recCover, accum > 0 ? recCover / accum : 0f, Mathf.Min(n, Mathf.FloorToInt((rec + Window) * n)), (rec + Window) * n < e ? " !! window shorter than swing end f" + e : "")); + } + finally + { + if (animMode) AnimationMode.StopAnimationMode(); + EditorSceneManager.ClosePreviewScene(scene); + } + return sb.ToString(); + } + + static float Coverage(float[] d, int n, float startNorm, float window) + { + int a = Mathf.Max(1, Mathf.CeilToInt(startNorm * n)), z = Mathf.Min(n, Mathf.FloorToInt((startNorm + window) * n)); + float c = 0f; for (int i = a; i <= z; i++) c += d[i]; return c; + } + static Vector3 Abs(Vector3 v) { return new Vector3(Mathf.Abs(v.x), Mathf.Abs(v.y), Mathf.Abs(v.z)); } +} diff --git a/AgentScripts/WL804_Tools.cs b/AgentScripts/WL804_Tools.cs new file mode 100644 index 000000000..8ca4a3019 --- /dev/null +++ b/AgentScripts/WL804_Tools.cs @@ -0,0 +1,112 @@ +// WL804_Tools.cs — 2026-09-07 세션 보조 도구 (#804 검기 3타 · #806 후속 검증) +// unity command run_script --file AgentScripts/WL804_Tools.cs --entry WL804_Tools.Refresh (에디트 · AssetDatabase.Refresh + 매핑 Attack3 ShowEffect 값 확인) +// unity command run_script --file AgentScripts/WL804_Tools.cs --entry WL804_Tools.FbxEvents (에디트 · Knight@Attack3_S 서브클립 ShowEffect time 확인) +// unity command run_script --file AgentScripts/WL804_Tools.cs --entry WL804_Tools.RuntimeCal (Play · WeaponTrailDriver.RuntimeCalibrations 요약) +// unity command run_script --file AgentScripts/WL804_Tools.cs --entry WL804_Tools.PetCount (Play · PetActor 인스턴스 · ActorInfo 펫 수) +// unity command run_script --file AgentScripts/WL804_Tools.cs --entry WL804_Tools.PopupShow --args "[124]" (Play · 공용 Popup 표시) +// unity command run_script --file AgentScripts/WL804_Tools.cs --entry WL804_Tools.PopupClose (Play) +using System.Linq; +using System.Text; +using UnityEditor; +using UnityEngine; + +public static class WL804_Tools +{ + const string MappingPath = "Assets/WL/Combat/CombatAnimMapping.asset"; + const string Fbx3 = "Assets/Res_Addr/Animations/Animation/OneHand/Knight@Attack3_S.FBX"; + + public static object Refresh() + { + AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); + var so = AssetDatabase.LoadAssetAtPath(MappingPath); + if (so == null) return "mapping null"; + var sb = new StringBuilder("refreshed · mapping Attack3 ShowEffect:\n"); + foreach (var c in so.classes) + foreach (var st in c.states) + if (st.stateName == "Attack3") + foreach (var e in st.events) + if (e.functionName == "ShowEffect") sb.AppendLine(" " + c.classId + " " + st.stateName + " norm=" + e.normalizedTime + " useNorm=" + e.useNormalized); + return sb.ToString(); + } + + public static object FbxEvents() + { + var imp = AssetImporter.GetAtPath(Fbx3) as ModelImporter; + if (imp == null) return "importer null"; + var sb = new StringBuilder(); + foreach (var c in imp.clipAnimations) + { + sb.Append(c.name + " [" + c.firstFrame + "~" + c.lastFrame + "f]:"); + foreach (var e in c.events) sb.Append(" " + e.functionName + "@" + e.time.ToString("F4")); + sb.AppendLine(); + } + sb.AppendLine("motionNodeName='" + imp.motionNodeName + "'"); + return sb.ToString(); + } + + public static object RuntimeCal() + { + var rt = WL.Combat.WeaponTrailDriver.RuntimeCalibrations; + if (rt == null) return "RuntimeCalibrations null"; + var sb = new StringBuilder("runtime calibrations " + rt.Count + "\n"); + foreach (var kv in rt) + { + var c = kv.Value; if (c == null) continue; + sb.AppendLine(string.Format(" {0} {1} kind={2} sweep={3:F1} q={4:F2} | {5}", c.classId, c.clipName, c.kind, c.sweepDeg, c.quality, c.source)); + } + return sb.ToString(); + } + + public static object PetCount() + { + if (!Application.isPlaying) return "not playing"; + var pets = UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None); + var sb = new StringBuilder("PetActor instances=" + pets.Length + "\n"); + foreach (var p in pets) + { + string path = p.name; var t = p.transform.parent; while (t != null) { path = t.name + "/" + path; t = t.parent; } + sb.AppendLine(" " + path + " active=" + p.gameObject.activeInHierarchy + " layer=" + LayerMask.LayerToName(p.gameObject.layer) + " pos=" + p.transform.position); + } + try + { + var list = ActorInfo.Ins.Get_Actors(eRole.Pet); + sb.AppendLine("ActorInfo eRole.Pet count=" + (list != null ? list.Count : -1)); + var my = MyValue.MyPC != null ? MyValue.MyPC.Get_MyPet() : null; + sb.AppendLine("MyPC.Get_MyPet()=" + (my != null ? my.name : "null")); + } + catch (System.Exception ex) { sb.AppendLine("ActorInfo/MyValue err: " + ex.Message); } + sb.AppendLine("spawnPlayerPet=" + WL.Settings.WLGameplaySettings.SpawnPlayerPet); + return sb.ToString(); + } + + public static object PopupShow(int msgKey) + { + if (!Application.isPlaying) return "not playing"; + var pop = UnityEngine.Object.FindFirstObjectByType(FindObjectsInactive.Include); + if (pop == null) return "Popup instance not found"; + pop.Set(ePopupType.One, msgKey); + var rt = pop.m_popup != null ? pop.m_popup.GetComponentInChildren(true) : null; + return "popup shown key=" + msgKey + " text='" + (pop.label_msg != null ? pop.label_msg.text : "") + "' btn=" + (rt != null ? ((RectTransform)rt.transform).rect.size.ToString() : "?") + + " scale=" + (rt != null ? rt.transform.lossyScale.x.ToString("F4") : "?"); + } + + public static object PopupClose() + { + if (!Application.isPlaying) return "not playing"; + var pop = UnityEngine.Object.FindFirstObjectByType(FindObjectsInactive.Include); + if (pop == null) return "Popup instance not found"; + pop.OnClick_OK(); + return "popup closed"; + } + + /// 에디트 모드 · SlashTrailSettings.swingCalibrations 에서 clipName 이 suffix 로 끝나는 행 제거 (재베이크 전 · 예: "_3") + public static object RemoveCal(string clipSuffix) + { + var so = AssetDatabase.LoadAssetAtPath("Assets/WL/Settings/Resources/WL/SlashTrailSettings.asset"); + if (so == null || so.swingCalibrations == null) return "settings null"; + var removed = new StringBuilder(); + int n = so.swingCalibrations.RemoveAll(c => { bool r = c != null && c.clipName != null && c.clipName.EndsWith(clipSuffix); if (r) removed.Append(c.classId + "/" + c.clipName + " q=" + c.quality.ToString("F2") + "; "); return r; }); + EditorUtility.SetDirty(so); AssetDatabase.SaveAssets(); + return "removed " + n + " [" + removed + "] remaining " + so.swingCalibrations.Count; + } +} diff --git a/AgentScripts/WL806_Popup.cs b/AgentScripts/WL806_Popup.cs new file mode 100644 index 000000000..c055a5078 --- /dev/null +++ b/AgentScripts/WL806_Popup.cs @@ -0,0 +1,101 @@ +// WL806_Popup.cs — 후속 B: SortOrder_5.prefab/PopupUI(공용 Popup 싱글톤) 를 세로 화면에서 터치·가독 가능한 크기로 (에디트 모드 · 프리팹 컨텐츠 · Play 불필요) +// unity command run_script --file AgentScripts/WL806_Popup.cs --entry WL806_Popup.Dump +// unity command run_script --file AgentScripts/WL806_Popup.cs --entry WL806_Popup.Apply +// unity command run_script --file AgentScripts/WL806_Popup.cs --entry WL806_Popup.Revert (2026-09-07 이전 원값) +// 배경(실측 Dump 2026-09-07): 캔버스 1920×1080 Expand → 세로 1080×1920 에서 scaleFactor 0.5625. +// 원값: bg 428×311 · msg 400×165 (0,30) fs20 · btn_ok 116×56 (0,-102) fs20 → 실화면 bg 241×175 px · 글자 11 px · 버튼 65×32 px (터치·가독 미달). +// 목표: TitleInfo 팝업(#794 · 1080 기준 bg 860×520 · msg 780×280 (0,70) · btn_ok 340×130 (0,-150)) 과 같은 실크기 → 1920 기준 유닛 = px / 0.5625. +// 글자: 로딩 화면(#800) 과 같은 실화면 ≈27 px → fs 48. +using System.Text; +using UnityEditor; +using UnityEngine; +using UnityEngine.UI; + +public static class WL806_Popup +{ + const string PrefabPath = "Assets/ResWork/UIPrefabs/Title/SortOrder_5.prefab"; + + // 1920×1080 기준 유닛 (px / 0.5625) + static readonly Vector2 BgSize = new Vector2(1529f, 924f); + static readonly Vector2 MsgSize = new Vector2(1387f, 498f); static readonly Vector2 MsgPos = new Vector2(0f, 124f); + static readonly Vector2 BtnSize = new Vector2(604f, 231f); static readonly Vector2 BtnPos = new Vector2(0f, -267f); + const float FontSize = 48f; + // 원값 + static readonly Vector2 BgSize0 = new Vector2(428f, 311f); + static readonly Vector2 MsgSize0 = new Vector2(400f, 165f); static readonly Vector2 MsgPos0 = new Vector2(0f, 30f); + static readonly Vector2 BtnSize0 = new Vector2(116f, 56f); static readonly Vector2 BtnPos0 = new Vector2(0f, -102f); + const float FontSize0 = 20f; + + static void Walk(Transform t, string path, StringBuilder sb, int depth) + { + var rt = t as RectTransform; + if (rt != null) + { + var txt = t.GetComponent(); + var tmp = t.GetComponent(); + string font = txt != null ? (" Text fs=" + txt.fontSize + " bestFit=" + txt.resizeTextForBestFit) + : tmp != null ? (" TMP fs=" + tmp.fontSize + " auto=" + tmp.enableAutoSizing + " wrap=" + tmp.textWrappingMode + " overflow=" + tmp.overflowMode) : ""; + var img = t.GetComponent(); + string im = img != null ? (" Image=" + (img.sprite != null ? img.sprite.name : "null") + " type=" + img.type) : ""; + var btn = t.GetComponent