112 lines
7.2 KiB
C#
112 lines
7.2 KiB
C#
|
|
// 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<ClipSpec> clips; public List<Step> 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<Spec>(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<string, AnimationClip>();
|
||
|
|
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<AnimationClip>(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<UnityEditor.Animations.AnimatorController>(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<AnimatorState>();
|
||
|
|
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();
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>공격 전진 표 행 추가/갱신. framesCsv = "4,5,6" · cmCsv = "3,5,6" (30fps 프레임 · cm).</summary>
|
||
|
|
public static object AddFrameTable(string clipContains, string framesCsv, string cmCsv)
|
||
|
|
{
|
||
|
|
const string path = "Assets/WL/Settings/Resources/WL/WLCombatMotionSettings.asset";
|
||
|
|
var st = AssetDatabase.LoadAssetAtPath<WL.Combat.WLCombatMotionSettings>(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<WL.Combat.AttackFrameTable>();
|
||
|
|
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 + "행";
|
||
|
|
}
|
||
|
|
}
|