// WL849_WeaponModels.cs — 무기 래퍼 프리팹 생성(손잡이 기준 확대) + Addressables 등록 · 월드 크기 측정 (에디터 · 멱등) // PD 2026-10-12 #849 「해머·창·대검을 훨씬 크게 · 900 Low Poly Fantasy RPG Weapons 팩으로 모델링 개량」 // 값 = AgentScripts/WL849_WeaponModels.json (C45 · 모델 경로·배율은 여기 없음) // unity command run_script --file AgentScripts/WL849_WeaponModels.cs --entry WL849_WeaponModels.Build --timeout 180 // unity command run_script --file AgentScripts/WL849_WeaponModels.cs --entry WL849_WeaponModels.Measure --args '["PP_Theme_01_Hammer_001|WL_W_Hammer_T05_003", 1]' --timeout 120 // 🔴 원본 PCActor.Make_Weapon 은 인스턴스 루트에 (위치 0 · 크기 1)을 덮어쓴다 → 배율은 루트가 아니라 자식(팩 프리팹 인스턴스)에 둔다. // 팩 모델의 기준점 = 손잡이 → 자식을 원점에 두고 키우면 손에 쥔 자리는 그대로다. using System.Collections.Generic; using System.Text; using UnityEditor; using UnityEditor.AddressableAssets; using UnityEditor.AddressableAssets.Settings; using UnityEditor.SceneManagement; using UnityEngine; public static class WL849_WeaponModels { const string SpecPath = "AgentScripts/WL849_WeaponModels.json"; const string ClassConfigJson = "Assets/ResWork/Table/Export/ClassConfig.json"; class Spec { public string outDir; public List models; } class Model { public string name; public int[] classes; public string source; public float[] scale; public float fitRatio; } const string FitSettingsPath = "Assets/WL/Settings/Resources/WL/WLWeaponFitSettings.asset"; /// 스펙의 fitRatio 를 WLWeaponFitSettings.perWeapon 에 넣는다(같은 이름이면 갱신 · 다른 무기 행은 그대로). public static string ApplyFitRatios() { var spec = Load(); var sb = new StringBuilder(); var st = AssetDatabase.LoadAssetAtPath(FitSettingsPath); if (st == null) return "fit settings 없음"; if (st.perWeapon == null) st.perWeapon = new List(); foreach (var m in spec.models) { if (m.fitRatio <= 0f) continue; var row = st.perWeapon.Find(r => r != null && string.Equals(r.weaponName, m.name, System.StringComparison.OrdinalIgnoreCase)); if (row == null) { row = new WL.Character.WLWeaponFitSettings.WeaponRatio { weaponName = m.name }; st.perWeapon.Add(row); } row.ratio = m.fitRatio; sb.Append(m.name).Append(" ratio ").Append(m.fitRatio).Append('\n'); } EditorUtility.SetDirty(st); AssetDatabase.SaveAssets(); WL.Character.WLWeaponFitSettings.ClearCache(); return sb.ToString() + "perWeapon " + st.perWeapon.Count + "행 · targetRatio " + st.targetRatio + " · maxScale " + st.maxScale; } static Spec Load() { return Newtonsoft.Json.JsonConvert.DeserializeObject(System.IO.File.ReadAllText(SpecPath)); } public static string Build() { var spec = Load(); var sb = new StringBuilder(); var settings = AddressableAssetSettingsDefaultObject.Settings; foreach (var m in spec.models) { var src = AssetDatabase.LoadAssetAtPath(m.source); if (src == null) { sb.Append("원본 없음 ").Append(m.source).Append('\n'); continue; } string path = spec.outDir + "/" + m.name + ".prefab"; var root = new GameObject(m.name); try { var child = (GameObject)PrefabUtility.InstantiatePrefab(src); child.transform.SetParent(root.transform, false); child.transform.localPosition = Vector3.zero; child.transform.localRotation = Quaternion.identity; child.transform.localScale = new Vector3(m.scale[0], m.scale[1], m.scale[2]); PrefabUtility.SaveAsPrefabAsset(root, path); } finally { Object.DestroyImmediate(root); } if (settings != null) { var e = settings.CreateOrMoveEntry(AssetDatabase.AssetPathToGUID(path), settings.DefaultGroup, false, false); e.address = path; } sb.Append("프리팹 ").Append(path).Append(" ← ").Append(System.IO.Path.GetFileNameWithoutExtension(m.source)).Append(" × (").Append(m.scale[0]).Append(", ").Append(m.scale[1]).Append(", ").Append(m.scale[2]).Append(") · 주소 등록\n"); } if (settings != null) settings.SetDirty(AddressableAssetSettings.ModificationEvent.EntryMoved, null, true, true); AssetDatabase.SaveAssets(); return sb.ToString(); } /// 런타임과 같은 PC(이름·키 보정)를 미리보기 씬에 세워 소켓에 무기를 붙이고 월드 크기를 잰다. names = "a|b|c" · socket = tfs_weapon 번호. public static string Measure(string names, int socket) { var sb = new StringBuilder(); float fScale = 1f; try { foreach (var row in Newtonsoft.Json.Linq.JArray.Parse(System.IO.File.ReadAllText(ClassConfigJson))) if ((string)row["n_ClassID"] == "10101") { float.TryParse((string)row["f_Scale"], out fScale); break; } } catch (System.Exception) { } string pcName = WL.Character.WLCharacterSwapSettings.Resolve("Ai01"); float rootScale = Mathf.Max(fScale, 0.01f) * Mathf.Max(WL.Character.WLCharacterSwapSettings.HeightCompensation, 0.01f); var model = AssetDatabase.LoadAssetAtPath("Assets/Res_Addr/PC/" + pcName + ".prefab"); if (model == null) return "pc 없음 " + pcName; var scene = EditorSceneManager.NewPreviewScene(); try { var go = (GameObject)PrefabUtility.InstantiatePrefab(model, scene); go.transform.position = Vector3.zero; go.transform.rotation = Quaternion.identity; go.transform.localScale = Vector3.one * rootScale; var pc = go.GetComponentInChildren(true); float h = 0f; foreach (var r in go.GetComponentsInChildren(true)) h = Mathf.Max(h, r.bounds.max.y); var tf = pc != null && pc.tfs_weapon != null && socket < pc.tfs_weapon.Length ? pc.tfs_weapon[socket] : null; if (tf == null) return "socket 없음"; sb.Append("PC ").Append(pcName).Append(" rootScale=").Append(rootScale.ToString("F3")).Append(" 키(렌더러 최고점)=").Append(h.ToString("F2")).Append(" · socket ").Append(tf.name).Append(" lossy=").Append(tf.lossyScale.ToString("F3")).Append('\n'); foreach (var n in names.Split('|')) { var wp = AssetDatabase.LoadAssetAtPath("Assets/Res_Addr/Weapon/" + n + ".prefab"); if (wp == null) { sb.Append(n).Append(" 없음\n"); continue; } var w = (GameObject)PrefabUtility.InstantiatePrefab(wp, scene); w.transform.SetParent(tf, false); w.transform.localPosition = Vector3.zero; w.transform.localRotation = Quaternion.identity; w.transform.localScale = Vector3.one; // 런타임과 같은 크기 보정(WLWeaponFitter · 무기별 비율) 적용 뒤 잰다 var fitter = go.GetComponent(); if (fitter == null) fitter = go.AddComponent(); float k = fitter.Fit(w.transform, tf); sb.Append(" [보정 ×").Append(k.ToString("F3")).Append(" · 비율 ").Append(WL.Character.WLWeaponFitSettings.ResolveRatio(n).ToString("F2")).Append("] "); // 무기 길이 축 = 소켓 로컬 Y(팩 모델 규약) — 소켓 공간 bounds 로 길이·폭·두께 Vector3 mn = Vector3.one * 1e9f, mx = -Vector3.one * 1e9f; foreach (var mf in w.GetComponentsInChildren(true)) { if (mf.sharedMesh == null) continue; var b = mf.sharedMesh.bounds; for (int c = 0; c < 8; c++) { var corner = b.center + new Vector3((c & 1) == 0 ? -b.extents.x : b.extents.x, (c & 2) == 0 ? -b.extents.y : b.extents.y, (c & 4) == 0 ? -b.extents.z : b.extents.z); var p = tf.InverseTransformPoint(mf.transform.TransformPoint(corner)); mn = Vector3.Min(mn, p); mx = Vector3.Max(mx, p); } } var size = Vector3.Scale(mx - mn, tf.lossyScale); sb.Append(n).Append(" 월드 [폭 ").Append(size.x.ToString("F2")).Append(" · 길이 ").Append(size.y.ToString("F2")).Append(" · 두께 ").Append(size.z.ToString("F2")).Append("] m · 손잡이 아래 ").Append((-mn.y * tf.lossyScale.y).ToString("F2")).Append(" m · 키 대비 길이 ").Append(h > 0.01f ? (size.y / h).ToString("F2") : "-").Append('\n'); Object.DestroyImmediate(w); } } finally { EditorSceneManager.ClosePreviewScene(scene); } return sb.ToString(); } }