// WL787_Anim.cs — PD 지시 #787 대쉬 상태 추가 (에디트 모드 · UnityEditor.Animations API) // unity command run_script --file AgentScripts/WL787_Anim.cs --entry WL787_Anim.Dump // unity command run_script --file AgentScripts/WL787_Anim.cs --entry WL787_Anim.Apply // unity command run_script --file AgentScripts/WL787_Anim.cs --entry WL787_Anim.Remove (롤백) // // 하는 일: 근접 4컨트롤러(ClassConfig 의 OneHand·Shield)의 Base Layer 에 // · 상태 `dash` 를 추가하고 Stander@Chase_Start 의 클립을 물린다 // · `dash -> idle` 전이(hasExitTime 1.0 · 0.15s)를 하나 건다 — 대쉬가 그냥 끝났을 때 자세가 굳지 않게 // 진입은 전이가 아니라 DashDriver 의 Animator.CrossFadeInFixedTime 이 한다. // → Any State 전이를 만들지 않으므로 기존 상태 전이망을 전혀 건드리지 않는다(회귀 위험 최소). // // 🔴 이름 충돌 주의: 이 레포에는 전역 네임스페이스에 MonoBehaviour `AnimatorController` 가 있다. // 반드시 UnityEditor.Animations.AnimatorController 로 **완전 한정**해서 쓴다. using System.Collections.Generic; using System.Linq; using System.Text; using UnityEditor; using UnityEngine; public static class WL787_Anim { const string ClassTable = "Assets/ResWork/Table/Export/ClassConfig.json"; const string ChaseFbx = "Assets/Res_Addr/Animations/Animation/Common_PlatformAnim/Stander@Chase_Start.FBX"; const string ChaseClip = "Chase_Start"; const string DashState = "dash"; const string IdleState = "idle"; static string[] TargetControllers() { var txt = System.IO.File.ReadAllText(ClassTable).TrimStart(''); var list = new List(); foreach (System.Text.RegularExpressions.Match m in System.Text.RegularExpressions.Regex.Matches(txt, "\\{[^{}]*\\}")) { string row = m.Value; var bt = System.Text.RegularExpressions.Regex.Match(row, "\"e_BattleType\":\\s*\"([^\"]*)\""); var ac = System.Text.RegularExpressions.Regex.Match(row, "\"s_AnimationController\":\\s*\"([^\"]*)\""); if (!bt.Success || !ac.Success) continue; if (bt.Groups[1].Value != "OneHand" && bt.Groups[1].Value != "Shield") continue; if (!list.Contains(ac.Groups[1].Value)) list.Add(ac.Groups[1].Value); } return list.ToArray(); } static AnimationClip LoadChaseClip() { foreach (var o in AssetDatabase.LoadAllAssetsAtPath(ChaseFbx)) { var c = o as AnimationClip; if (c != null && c.name == ChaseClip) return c; } return null; } static UnityEditor.Animations.AnimatorState Find( UnityEditor.Animations.AnimatorController c, string name) { foreach (var layer in c.layers) foreach (var cs in layer.stateMachine.states) if (cs.state != null && cs.state.name == name) return cs.state; return null; } public static object Dump() { var sb = new StringBuilder(); var clip = LoadChaseClip(); sb.AppendLine("clip " + ChaseClip + " = " + (clip != null ? clip.name + " len=" + clip.length.ToString("0.000") + "s" : "(NOT FOUND)")); foreach (var path in TargetControllers()) { var c = AssetDatabase.LoadAssetAtPath(path); if (c == null) { sb.AppendLine("MISSING " + path); continue; } var dash = Find(c, DashState); sb.AppendLine("## " + System.IO.Path.GetFileName(path) + " states=" + c.layers[0].stateMachine.states.Length + " dash=" + (dash == null ? "(없음)" : (dash.motion != null ? dash.motion.name : "(motion null)") + " out=" + dash.transitions.Length)); } return sb.ToString(); } public static object Apply() { if (EditorApplication.isPlaying) return "ABORT: Play 중"; if (EditorApplication.isCompiling) return "ABORT: 컴파일 중"; var clip = LoadChaseClip(); if (clip == null) return "ABORT: 클립을 못 찾았다 — " + ChaseFbx + " / " + ChaseClip; var sb = new StringBuilder("clip=" + clip.name + " len=" + clip.length.ToString("0.000") + "s\n"); int added = 0, wired = 0; foreach (var path in TargetControllers()) { var c = AssetDatabase.LoadAssetAtPath(path); if (c == null) { sb.AppendLine("MISSING " + path); continue; } var sm = c.layers[0].stateMachine; var dash = Find(c, DashState); if (dash == null) { // idle 아래쪽에 눈에 띄게 놓는다(에디터에서 사람이 찾기 쉽게). var idleForPos = Find(c, IdleState); Vector3 pos = new Vector3(-260f, 260f, 0f); foreach (var cs in sm.states) if (idleForPos != null && cs.state == idleForPos) pos = cs.position + new Vector3(0f, 130f, 0f); dash = sm.AddState(DashState, pos); added++; sb.AppendLine(" + 상태 추가 dash (" + System.IO.Path.GetFileName(path) + ")"); } dash.motion = clip; dash.speed = 1f; dash.writeDefaultValues = false; // dash -> idle : 대쉬가 공격으로 이어지지 않고 그냥 끝났을 때의 안전 출구. var idle = Find(c, IdleState); if (idle != null && !dash.transitions.Any(t => t.destinationState == idle)) { var tr = dash.AddTransition(idle); tr.hasExitTime = true; tr.exitTime = 1f; tr.hasFixedDuration = true; tr.duration = 0.15f; tr.offset = 0f; wired++; sb.AppendLine(" + 전이 dash -> idle exit=1.00 dur=0.15"); } else if (idle == null) sb.AppendLine(" [WARN] idle 상태를 못 찾음 — 출구 전이 생략 " + path); EditorUtility.SetDirty(c); } AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); sb.AppendLine("결과: 상태추가=" + added + " 전이추가=" + wired); return sb.ToString(); } /// /// WLCombatMotionSettings.asset 을 다시 직렬화해 #787 신규 필드를 YAML 에 실제로 적어 둔다. /// (없어도 C# 필드 초기값으로 동작하지만, PD·PM 이 파일에서 값을 읽고 고칠 수 있어야 한다 · C45) /// public static object TouchSettings() { const string p = "Assets/WL/Settings/Resources/WL/WLCombatMotionSettings.asset"; var s = AssetDatabase.LoadAssetAtPath(p); if (s == null) return "ABORT: " + p + " 없음"; EditorUtility.SetDirty(s); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); return "OK dashEnabled=" + s.dashEnabled + " min=" + s.dashMinDistance + " max=" + s.dashMaxDistance + " stop=" + s.dashStopDistance + " speed=" + s.dashMaxSpeed + " maxSec=" + s.dashMaxSeconds + " look=" + s.dashLookAtTarget + " cool=" + s.dashCooldownSeconds + " state=" + s.dashStateName + " clip=" + s.dashClipName; } /// 롤백 — dash 상태를 지운다(전이도 함께 사라진다). public static object Remove() { if (EditorApplication.isPlaying) return "ABORT: Play 중"; var sb = new StringBuilder(); foreach (var path in TargetControllers()) { var c = AssetDatabase.LoadAssetAtPath(path); if (c == null) continue; var dash = Find(c, DashState); if (dash == null) { sb.AppendLine(" (없음) " + System.IO.Path.GetFileName(path)); continue; } c.layers[0].stateMachine.RemoveState(dash); EditorUtility.SetDirty(c); sb.AppendLine(" - 삭제 dash " + System.IO.Path.GetFileName(path)); } AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); return sb.ToString(); } }