#if UNITY_EDITOR
using System;
using System.IO;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEngine;
using Object=UnityEngine.Object;
/// Offline source-motion editing; saves assets, never adds runtime correction.
public static class FinalizeEnemyMotions
{
[Serializable] public sealed class Recipe
{
public string id,assetRoot,rootBone,chestBone,rightArm,leftArm,outputDir,profile,attackStyle,attackEvent,usage;
public string headBone,neckBone,frontRightFoot,frontLeftFoot,hindRightFoot,hindLeftFoot,rightFoot,leftFoot;
public float height,idleSeconds,idleBreath,attackSeconds,attackWindup,attackImpact,attackRecover;
public float attackLeanDegrees,attackArmDegrees,attackLunge,attackReach,idleSourceWeight,attackSourceWeight,wingSpreadDegrees;
public float attackHeadReach,attackHeadPitchDegrees,attackSpreadDegrees;
}
sealed class Pose
{
public Vector3[] p,s;
public Quaternion[] q;
public static Pose Read(Transform[] b)=>new Pose { p=b.Select(t=>t.localPosition).ToArray(),s=b.Select(t=>t.localScale).ToArray(),q=b.Select(t=>t.localRotation).ToArray() };
public void Write(Transform[] b) { for(int i=0;iPath.GetFullPath(Path.Combine(Application.dataPath,"../.."));
static float Smooth(float t)=>Mathf.SmoothStep(0,1,Mathf.Clamp01(t));
static float Strike(Recipe r,float t)
{
if(tb.localToWorldMatrix*bind[i]).ToArray();
return mesh.vertices.Select((v,i)=> { var w=weights[i];return matrices[w.boneIndex0].MultiplyPoint3x4(v)*w.weight0
+matrices[w.boneIndex1].MultiplyPoint3x4(v)*w.weight1+matrices[w.boneIndex2].MultiplyPoint3x4(v)*w.weight2
+matrices[w.boneIndex3].MultiplyPoint3x4(v)*w.weight3; }).ToArray();
}
static float Bottom(GameObject model)=>model.GetComponentsInChildren().SelectMany(Vertices).Min(v=>v.y);
static void Save(AnimationClip clip,Transform model,Transform[] bones,Pose[] poses,float seconds,bool loop)
{
Undo.RecordObject(clip,"Author character motion");
clip.ClearCurves();clip.frameRate=30;
int count=poses.Length;
for(int f=1;f(File.ReadAllText(Path.Combine(Repo,recipePath)));
if(string.IsNullOrEmpty(r.attackStyle))r.attackStyle="body_slam";
if(string.IsNullOrEmpty(r.attackEvent))r.attackEvent=r.usage=="companion_visual" ? "OnCompanionAttackImpact" : "OnEnemyAttackImpact";
bool bird=r.profile=="avian_grounded";
bool turtle=r.profile=="quadruped_turtle";
bool heavy=r.profile=="quadruped_heavy";
bool quadruped=turtle || heavy;
bool roar=r.attackStyle=="flame_roar";
if(r.profile!="biped_short" && !bird && !quadruped)throw new ArgumentException("Unsupported anatomy");
if(r.attackStyle!="body_slam" && r.attackStyle!="ranged_cast" && r.attackStyle!="wing_cast" && r.attackStyle!="turtle_bubble" && r.attackStyle!="braced_bash" && !roar)throw new ArgumentException("Unsupported attack style");
if(roar && r.profile!="biped_short")throw new ArgumentException("Flame roar requires an upright biped");
if(bird!=(r.attackStyle=="wing_cast") || (bird && !(r.wingSpreadDegrees>0 && r.wingSpreadDegrees<=60)))
throw new ArgumentException("Invalid grounded-bird wing cast");
if(turtle!=(r.attackStyle=="turtle_bubble"))throw new ArgumentException("Invalid turtle bubble cast");
if(heavy!=(r.attackStyle=="braced_bash"))throw new ArgumentException("Invalid heavy quadruped bash");
if(r.height<=0 || !(0(r.assetRoot+"/Rigged/"+r.id+"_Rigged.fbx"));
try
{
var anim=model.GetComponent();if(anim!=null) anim.enabled=false;
var bones=model.GetComponentsInChildren().Where(t=>t!=model.transform && (t.name=="Armature" || t.name.StartsWith("bone_"))).ToArray();
var root=bones.First(t=>t.name==r.rootBone);
var head=quadruped || roar ? bones.First(t=>t.name==r.headBone) : null;
var neck=quadruped || roar ? bones.First(t=>t.name==r.neckBone) : null;
var feet=(quadruped ? new[]{r.frontRightFoot,r.frontLeftFoot,r.hindRightFoot,r.hindLeftFoot} : roar ? new[]{r.rightFoot,r.leftFoot} : new string[0]).Select(n=>bones.First(t=>t.name==n)).ToArray();
var footPositions=feet.Select(t=>t.position).ToArray();var footRotations=feet.Select(t=>t.rotation).ToArray();
var chest=bones.FirstOrDefault(t=>t.name==r.chestBone);
var arms=new[] {bones.FirstOrDefault(t=>t.name==r.rightArm),bones.FirstOrDefault(t=>t.name==r.leftArm)};
if(!quadruped && (chest==null || arms.Any(t=>t==null) || arms[0]==arms[1])) throw new Exception("Missing/distinct profile bones required");
if((heavy || roar) && (chest==null || chest==head))throw new Exception("Independent chest and head required");
var bind=Pose.Read(bones);var bindRootPosition=root.position;float bindBottom=Bottom(model);
var bindRootRotation=root.rotation;
var wingSides=bird || roar ? arms.Select(a=>Mathf.Sign(a.position.x-root.position.x)).ToArray() : new float[0];
if((bird || roar) && wingSides[0]==wingSides[1])throw new Exception("Arm/wing roots must be on opposite sides");
var skins=model.GetComponentsInChildren();
float unitScale=(skins.SelectMany(Vertices).Max(v=>v.y)-bindBottom)/r.height;
var source=AssetDatabase.LoadAllAssetsAtPath(r.assetRoot+"/Anims/"+r.id+"_"+name+".fbx").OfType().First(c=>!c.name.StartsWith("__preview__"));
source.SampleAnimation(model,0);var reference=Pose.Read(bones);
bool idle=name=="Idle";float seconds=idle ? r.idleSeconds : r.attackSeconds;
int count=Mathf.RoundToInt(seconds*30)+1;var poses=new Pose[count];
for(int f=0;f0 ? r.idleSourceWeight : .2f;
float attackWeight=r.attackSourceWeight>0 ? r.attackSourceWeight : .35f;
float weight=Mathf.Pow(Mathf.Sin(Mathf.PI*t),2)*(idle ? idleWeight : attackWeight);
bind.Write(bones);source.SampleAnimation(model,t*source.length);var raw=Pose.Read(bones);
for(int b=0;b(r.assetRoot+"/Clips/"+name+".anim");
Save(clip,model.transform,bones,poses,seconds,idle);
if(!idle)
{
AnimationUtility.SetAnimationEvents(clip,new[] {new AnimationEvent {
time=r.attackImpact*seconds,functionName=r.attackEvent,messageOptions=SendMessageOptions.DontRequireReceiver
}});
}
sb.AppendLine($"{name}: {seconds:F3}s loop={idle} start/end=bind generated source + offline {r.profile}/{r.attackStyle} authoring");
}
finally {Object.DestroyImmediate(model);}
}
AssetDatabase.SaveAssets();
string report=Path.Combine(Repo,r.outputDir,"motion_processing.txt");Directory.CreateDirectory(Path.GetDirectoryName(report));File.WriteAllText(report,sb.ToString());
return sb.ToString();
}
}
#endif