// WL888_Tools.cs — PD 지시 #888 에디터 도구(Play 아님 · 멱등) // BuildFlurry : 돌진 베기 → 1·2타 빠른 연속 베기 클립 복사 + 상태 사슬 (값 = AgentScripts/WL888_Flurry.json) // AddFrameTable: WLCombatMotionSettings 공격 전진 표에 클립 이름 행 추가/갱신 (방패 치기 살짝 전진) // unity command run_script --file AgentScripts/WL888_Tools.cs --entry WL888_Tools.BuildFlurry --timeout 180 using System.Collections.Generic; using System.Linq; using System.Text; using UnityEditor; using UnityEditor.Animations; using UnityEngine; public static class WL888_Tools { class Spec { public string controller, outDir, endState; public float speed, exitTime, blend, endExitTime, endBlend; public Src skillEventFrom; public List clips; public List sequence; } class Src { public string src, clip; } class ClipSpec { public string name, src, srcClip; public string[] keepEvents; public float skillEventAt; } class Step { public string state, clip; } static AnimationClip FindClip(string path, string name) { foreach (var o in AssetDatabase.LoadAllAssetsAtPath(path)) { var c = o as AnimationClip; if (c != null && c.name == name) return c; } return null; } public static object BuildFlurry() { var spec = Newtonsoft.Json.JsonConvert.DeserializeObject(System.IO.File.ReadAllText("AgentScripts/WL888_Flurry.json")); var sb = new StringBuilder(); var evSrc = FindClip(spec.skillEventFrom.src, spec.skillEventFrom.clip); if (evSrc == null) return "SkillEvent 원본 클립 없음"; var skillEv = AnimationUtility.GetAnimationEvents(evSrc).FirstOrDefault(e => e.functionName == "SkillEvent"); if (skillEv == null) return "원본에 SkillEvent 없음"; System.IO.Directory.CreateDirectory(spec.outDir); var made = new Dictionary(); foreach (var cs in spec.clips) { var src = FindClip(cs.src, cs.srcClip); if (src == null) return "원본 클립 없음 " + cs.srcClip; string path = spec.outDir + "/" + cs.name + ".anim"; var dst = AssetDatabase.LoadAssetAtPath(path); bool created = dst == null; if (created) dst = new AnimationClip(); dst.ClearCurves(); dst.frameRate = src.frameRate; foreach (var b in AnimationUtility.GetCurveBindings(src)) AnimationUtility.SetEditorCurve(dst, b, AnimationUtility.GetEditorCurve(src, b)); foreach (var b in AnimationUtility.GetObjectReferenceCurveBindings(src)) AnimationUtility.SetObjectReferenceCurve(dst, b, AnimationUtility.GetObjectReferenceCurve(src, b)); var st = AnimationUtility.GetAnimationClipSettings(src); st.loopTime = false; AnimationUtility.SetAnimationClipSettings(dst, st); var evs = AnimationUtility.GetAnimationEvents(src).Where(e => cs.keepEvents != null && cs.keepEvents.Contains(e.functionName)).ToList(); if (cs.skillEventAt >= 0f) evs.Add(new AnimationEvent { functionName = skillEv.functionName, intParameter = skillEv.intParameter, floatParameter = skillEv.floatParameter, stringParameter = skillEv.stringParameter, objectReferenceParameter = skillEv.objectReferenceParameter, time = cs.skillEventAt }); AnimationUtility.SetAnimationEvents(dst, evs.OrderBy(e => e.time).ToArray()); if (created) AssetDatabase.CreateAsset(dst, path); else EditorUtility.SetDirty(dst); made[cs.name] = dst; sb.Append(created ? "클립 생성 " : "클립 갱신 ").Append(cs.name).Append(" ← ").Append(src.name).Append(" len=").Append(src.length.ToString("F2")).Append(" human=").Append(src.humanMotion) .Append(" 곡선=").Append(AnimationUtility.GetCurveBindings(dst).Length).Append(" 이벤트=").Append(string.Join(",", evs.Select(e => e.functionName + "@" + e.time.ToString("F2")).ToArray())).Append('\n'); } var ctrl = AssetDatabase.LoadAssetAtPath(spec.controller); if (ctrl == null) return sb + "컨트롤러 없음"; var sm = ctrl.layers[0].stateMachine; AnimatorState Find(string n) { foreach (var c in sm.states) if (c.state.name == n) return c.state; return null; } var end = Find(spec.endState); if (end == null) return sb + "끝 상태 없음 " + spec.endState; var first = Find(spec.sequence[0].state); if (first == null) return sb + "시작 상태 없음 " + spec.sequence[0].state; var basePos = sm.states.First(x => x.state == first).position; var chain = new List(); for (int i = 0; i < spec.sequence.Count; i++) { var s = Find(spec.sequence[i].state); if (s == null) s = sm.AddState(spec.sequence[i].state, basePos + new Vector3(220f * i, 70f, 0f)); s.motion = made[spec.sequence[i].clip]; s.speed = spec.speed; s.speedParameterActive = false; s.writeDefaultValues = first.writeDefaultValues; foreach (var t in s.transitions.ToArray()) s.RemoveTransition(t); chain.Add(s); } for (int i = 0; i < chain.Count; i++) { bool last = i == chain.Count - 1; var t = chain[i].AddTransition(last ? end : chain[i + 1]); t.hasExitTime = true; t.exitTime = last ? spec.endExitTime : spec.exitTime; t.hasFixedDuration = true; t.duration = last ? spec.endBlend : spec.blend; t.offset = 0f; } EditorUtility.SetDirty(ctrl); AssetDatabase.SaveAssets(); sb.Append("사슬 ").Append(string.Join(" → ", chain.Select(x => x.name + "(" + x.motion.name + ")").ToArray())).Append(" → ").Append(end.name).Append(" · 배속 ").Append(spec.speed).Append(" · 넘김 ").Append(spec.exitTime); return sb.ToString(); } /// 공격 전진 표 행 추가/갱신. framesCsv = "4,5,6" · cmCsv = "3,5,6" (30fps 프레임 · cm). public static object AddFrameTable(string clipContains, string framesCsv, string cmCsv) { const string path = "Assets/WL/Settings/Resources/WL/WLCombatMotionSettings.asset"; var st = AssetDatabase.LoadAssetAtPath(path); if (st == null) return "설정 없음"; var frames = framesCsv.Split(',').Select(x => int.Parse(x.Trim())).ToArray(); var cm = cmCsv.Split(',').Select(x => float.Parse(x.Trim())).ToArray(); if (frames.Length != cm.Length) return "길이 다름"; if (st.attackFrameTables == null) st.attackFrameTables = new List(); var row = st.attackFrameTables.Find(r => r != null && r.clipContains == clipContains); bool added = row == null; if (added) { row = new WL.Combat.AttackFrameTable { clipContains = clipContains }; st.attackFrameTables.Add(row); } row.frames = frames; row.cm = cm; row.passThrough = false; EditorUtility.SetDirty(st); AssetDatabase.SaveAssets(); return (added ? "추가 " : "갱신 ") + clipContains + " 프레임 " + framesCsv + " · cm " + cmCsv + " = 총 " + row.Total() + " cm · 표 " + st.attackFrameTables.Count + "행"; } }