// ───────────────────────────────────────────────────────────────────────────── // WL814n_Build.cs — `Assets/Res_Addr/PC/LH_M05.prefab` 조립 (에디터 전용 · 발주서 WL-814n §1-2) // // M05(Little Heroes RedKnight) 모델을 본체로 하고 `Ai01` 의 컴포넌트 구성·직렬화 값을 그대로 이식한다. // 🔴 원본 `Ai01.prefab` 과 `Assets/Suriyun/**` 은 **읽기만** 한다(수정 0). // // 하는 일 // 1. M05 프리팹을 인스턴스화 → 언팩(원본 연결 끊기) → 이름 LH_M05 // 2. 레이어 3 전체 적용(Ai01 실측: 223 중 222 가 L3) // 3. Animator: 아바타 = M05 자신(Humanoid) · 컨트롤러·applyRootMotion·cullingMode 는 Ai01 과 동일 배선 // (런타임에 `PCActor.Set_Obj()` 가 pcanim_* 로 갈아끼운다) // 4. Ai01 루트의 WizardActor · NavMeshAgent · Rigidbody · CapsuleCollider · SkinnedMeshAfterImage 를 // ComponentUtility 로 값 복사 // 5. 무기 소켓 7개를 Ai01 과 **같은 본 경로**에 같은 로컬 TRS 로 생성 → WizardActor.tfs_weapon 재배선 // 6. WLPcScaleCompensator 부착(월드 높이 1.191 m 유지) // 7. 팩 기본 장착 검 `Sword_M05` 비활성(게임의 Change_Weapon 이 소켓에 무기를 꽂는다) // ───────────────────────────────────────────────────────────────────────────── using System; using System.Collections.Generic; using System.Text; using UnityEditor; using UnityEngine; using UnityEditorInternal; using WL.Character; public static class WL814n_Build { const string AI01 = "Assets/Res_Addr/PC/Ai01.prefab"; const string M05 = "Assets/Suriyun/Characters/RedKnight/Prefab/M05/M05.prefab"; const string OUT = "Assets/Res_Addr/PC/LH_M05.prefab"; // Ai01 실측(PROBE1): 소켓 이름 · 부모 본 경로(루트 제외) · 로컬 TRS struct Socket { public string name, parentPath; public Vector3 pos, scale; public Quaternion rot; public int layer; } static Transform FindPath(Transform root, string path) { var t = root; foreach (var seg in path.Split('/')) { t = t.Find(seg); if (t == null) return null; } return t; } static void SetLayerAll(GameObject go, int layer) { foreach (var t in go.GetComponentsInChildren(true)) t.gameObject.layer = layer; } public static void Run() { var sb = new StringBuilder(); var ai01Pf = AssetDatabase.LoadAssetAtPath(AI01); var m05Pf = AssetDatabase.LoadAssetAtPath(M05); if (ai01Pf == null || m05Pf == null) { Debug.LogError("[WL-814n] 원본 프리팹을 찾지 못함"); return; } // ── 1) Ai01 을 읽기 전용으로 인스턴스화해 소켓 정보를 실측 채집 var ai = (GameObject)PrefabUtility.InstantiatePrefab(ai01Pf); ai.transform.localScale = Vector3.one; var aiActor = ai.GetComponent(); var socks = new List(); foreach (var tf in aiActor.tfs_weapon) { // 루트 이름을 뺀 부모 경로 var parts = new List(); var p = tf.parent; while (p != null && p != ai.transform) { parts.Insert(0, p.name); p = p.parent; } socks.Add(new Socket { name = tf.name, parentPath = string.Join("/", parts.ToArray()), pos = tf.localPosition, rot = tf.localRotation, scale = tf.localScale, layer = tf.gameObject.layer }); } sb.AppendLine("Ai01 소켓 채집 " + socks.Count + "/7"); // ── 2) M05 인스턴스화 + 언팩 var go = (GameObject)PrefabUtility.InstantiatePrefab(m05Pf); PrefabUtility.UnpackPrefabInstance(go, PrefabUnpackMode.Completely, InteractionMode.AutomatedAction); go.name = "LH_M05"; go.transform.position = Vector3.zero; go.transform.rotation = Quaternion.identity; go.transform.localScale = Vector3.one; SetLayerAll(go, ai.layer); // Ai01 = Layer 3 go.tag = ai.tag; // Untagged // ── 3) 팩 기본 장착 검 비활성(게임의 Change_Weapon 이 소켓에 꽂는다) var packSword = FindPath(go.transform, "Root_M/RootPart1_M/Spine1_M/Chest_M/Scapula_R/Shoulder_R/Elbow_R/Wrist_R/Sword_parentR/Sword_M05"); if (packSword != null) { packSword.gameObject.SetActive(false); sb.AppendLine("팩 기본 검 Sword_M05 비활성"); } // ── 4) 무기 소켓 7개를 같은 본 경로에 생성 var newSockets = new Transform[socks.Count]; for (int i = 0; i < socks.Count; i++) { var s = socks[i]; var parent = FindPath(go.transform, s.parentPath); if (parent == null) { Debug.LogError("[WL-814n] M05 에 본 경로 없음: " + s.parentPath); sb.AppendLine("MISSING " + s.parentPath); continue; } var n = new GameObject(s.name); n.transform.SetParent(parent, false); n.transform.localPosition = s.pos; n.transform.localRotation = s.rot; n.transform.localScale = s.scale; n.layer = s.layer; newSockets[i] = n.transform; sb.AppendLine(string.Format("소켓 {0} {1} → {2}", i, s.name, s.parentPath)); } // ── 5) Animator 를 Ai01 과 같은 배선으로 (아바타만 M05 자신 것) var aiAnim = ai.GetComponent(); var anim = go.GetComponent(); anim.runtimeAnimatorController = aiAnim.runtimeAnimatorController; // 런타임에 pcanim_* 로 교체된다 anim.applyRootMotion = aiAnim.applyRootMotion; // false anim.updateMode = aiAnim.updateMode; anim.cullingMode = aiAnim.cullingMode; sb.AppendLine("Animator avatar=" + anim.avatar.name + " isHuman=" + anim.avatar.isHuman + " ctrl=" + (anim.runtimeAnimatorController ? anim.runtimeAnimatorController.name : "null") + " rootMotion=" + anim.applyRootMotion); // ── 6) Ai01 루트 컴포넌트 값 복사(Transform/Animator 제외) foreach (var c in ai.GetComponents()) { if (c is Transform || c is Animator) continue; ComponentUtility.CopyComponent(c); var existing = go.GetComponent(c.GetType()); if (existing != null) ComponentUtility.PasteComponentValues(existing); else ComponentUtility.PasteComponentAsNew(go); sb.AppendLine("컴포넌트 이식 " + c.GetType().Name); } // ── 7) tfs_weapon 재배선 (복사된 참조는 Ai01 것이므로 반드시 덮어쓴다) var actor = go.GetComponent(); actor.tfs_weapon = newSockets; int ok = 0; foreach (var t in actor.tfs_weapon) if (t != null && t.IsChildOf(go.transform)) ok++; sb.AppendLine("tfs_weapon 배선 " + ok + "/7 (전부 LH_M05 하위인가)"); // ── 8) 크기 보정 컴포넌트 var comp = go.GetComponent(); if (comp == null) comp = go.AddComponent(); float kComp = comp.Compensation; sb.AppendLine("WLPcScaleCompensator 부착 (보정 " + kComp.ToString("F4") + ")"); // ── 8-b) 🔴 CapsuleCollider 재보정 (프로브 실측으로 발견) // 콜라이더는 루트에 붙어 있어 루트 스케일(0.7 × k)을 그대로 받는다. // Ai01 값을 그대로 쓰면 월드 크기가 k 배(=1.5배) 커진다 → 로컬 값을 k 로 나눠 // 월드 크기를 Ai01(h 1.106 · r 0.385 · centerY 0.553)과 **동일**하게 맞춘다. // (NavMeshAgent 의 radius/height 는 트랜스폼 스케일을 받지 않으므로 Ai01 값 그대로 둔다.) var aiCc = ai.GetComponent(); var cc = go.GetComponent(); if (cc != null && aiCc != null && kComp > 0f) { cc.radius = aiCc.radius / kComp; cc.height = aiCc.height / kComp; cc.center = aiCc.center / kComp; sb.AppendLine(string.Format("CapsuleCollider 재보정 r {0:F4}→{1:F4} h {2:F4}→{3:F4} centerY {4:F4}→{5:F4} (월드는 Ai01 과 동일)", aiCc.radius, cc.radius, aiCc.height, cc.height, aiCc.center.y, cc.center.y)); } // ── 9) 저장 System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(OUT)); var saved = PrefabUtility.SaveAsPrefabAsset(go, OUT, out bool success); sb.AppendLine("저장 " + OUT + " success=" + success); UnityEngine.Object.DestroyImmediate(go); UnityEngine.Object.DestroyImmediate(ai); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); System.IO.File.WriteAllText("AgentScripts/staging/WL814n/BUILD.txt", sb.ToString(), Encoding.UTF8); Debug.Log("[WL-814n] BUILD\n" + sb); } }