1074 lines
44 KiB
C#
1074 lines
44 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using Newtonsoft.Json.Linq;
|
|
using NUnit.Framework;
|
|
using UnityEditor;
|
|
using UnityEditor.Animations;
|
|
using UnityEngine;
|
|
using MCPForUnity.Editor.Tools.Animation;
|
|
using static MCPForUnityTests.Editor.TestUtilities;
|
|
|
|
namespace MCPForUnityTests.Editor.Tools
|
|
{
|
|
public class ManageAnimationTests
|
|
{
|
|
private const string TempRoot = "Assets/Temp/ManageAnimationTests";
|
|
|
|
[SetUp]
|
|
public void SetUp()
|
|
{
|
|
EnsureFolder(TempRoot);
|
|
}
|
|
|
|
[TearDown]
|
|
public void TearDown()
|
|
{
|
|
// Clean up scene objects
|
|
foreach (var go in UnityEngine.Object.FindObjectsOfType<GameObject>())
|
|
{
|
|
if (go.name.StartsWith("AnimTest_"))
|
|
{
|
|
UnityEngine.Object.DestroyImmediate(go);
|
|
}
|
|
}
|
|
|
|
if (AssetDatabase.IsValidFolder(TempRoot))
|
|
{
|
|
AssetDatabase.DeleteAsset(TempRoot);
|
|
}
|
|
CleanupEmptyParentFolders(TempRoot);
|
|
}
|
|
|
|
// =============================================================================
|
|
// Dispatch / Error Handling
|
|
// =============================================================================
|
|
|
|
[Test]
|
|
public void HandleCommand_MissingAction_ReturnsError()
|
|
{
|
|
var paramsObj = new JObject();
|
|
var result = ToJObject(ManageAnimation.HandleCommand(paramsObj));
|
|
Assert.IsFalse(result.Value<bool>("success"));
|
|
Assert.That(result["message"].ToString(), Does.Contain("Action is required"));
|
|
}
|
|
|
|
[Test]
|
|
public void HandleCommand_UnknownAction_ReturnsError()
|
|
{
|
|
var paramsObj = new JObject { ["action"] = "bogus_action" };
|
|
var result = ToJObject(ManageAnimation.HandleCommand(paramsObj));
|
|
Assert.IsFalse(result.Value<bool>("success"));
|
|
Assert.That(result["message"].ToString(), Does.Contain("Unknown action"));
|
|
}
|
|
|
|
[Test]
|
|
public void HandleCommand_UnknownAnimatorAction_ReturnsError()
|
|
{
|
|
var paramsObj = new JObject { ["action"] = "animator_nonexistent" };
|
|
var result = ToJObject(ManageAnimation.HandleCommand(paramsObj));
|
|
Assert.IsFalse(result.Value<bool>("success"));
|
|
Assert.That(result["message"].ToString(), Does.Contain("Unknown animator action"));
|
|
}
|
|
|
|
[Test]
|
|
public void HandleCommand_UnknownClipAction_ReturnsError()
|
|
{
|
|
var paramsObj = new JObject { ["action"] = "clip_nonexistent" };
|
|
var result = ToJObject(ManageAnimation.HandleCommand(paramsObj));
|
|
Assert.IsFalse(result.Value<bool>("success"));
|
|
Assert.That(result["message"].ToString(), Does.Contain("Unknown clip action"));
|
|
}
|
|
|
|
// =============================================================================
|
|
// Animator: Get Info
|
|
// =============================================================================
|
|
|
|
[Test]
|
|
public void AnimatorGetInfo_NoTarget_ReturnsError()
|
|
{
|
|
var paramsObj = new JObject { ["action"] = "animator_get_info" };
|
|
var result = ToJObject(ManageAnimation.HandleCommand(paramsObj));
|
|
Assert.IsFalse(result.Value<bool>("success"));
|
|
}
|
|
|
|
[Test]
|
|
public void AnimatorGetInfo_NoAnimator_ReturnsError()
|
|
{
|
|
var go = new GameObject("AnimTest_NoAnimator");
|
|
try
|
|
{
|
|
var paramsObj = new JObject
|
|
{
|
|
["action"] = "animator_get_info",
|
|
["target"] = "AnimTest_NoAnimator"
|
|
};
|
|
var result = ToJObject(ManageAnimation.HandleCommand(paramsObj));
|
|
Assert.IsFalse(result.Value<bool>("success"));
|
|
Assert.That(result["message"].ToString(), Does.Contain("No Animator"));
|
|
}
|
|
finally
|
|
{
|
|
UnityEngine.Object.DestroyImmediate(go);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void AnimatorGetInfo_WithAnimator_ReturnsData()
|
|
{
|
|
var go = new GameObject("AnimTest_WithAnimator");
|
|
go.AddComponent<Animator>();
|
|
try
|
|
{
|
|
var paramsObj = new JObject
|
|
{
|
|
["action"] = "animator_get_info",
|
|
["target"] = "AnimTest_WithAnimator"
|
|
};
|
|
var result = ToJObject(ManageAnimation.HandleCommand(paramsObj));
|
|
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
|
var data = result["data"] as JObject;
|
|
Assert.IsNotNull(data);
|
|
Assert.AreEqual("AnimTest_WithAnimator", data["gameObject"].ToString());
|
|
Assert.IsNotNull(data["enabled"]);
|
|
Assert.IsNotNull(data["speed"]);
|
|
}
|
|
finally
|
|
{
|
|
UnityEngine.Object.DestroyImmediate(go);
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Animator: Set Speed / Set Enabled
|
|
// =============================================================================
|
|
|
|
[Test]
|
|
public void AnimatorSetSpeed_ChangesSpeed()
|
|
{
|
|
var go = new GameObject("AnimTest_Speed");
|
|
var animator = go.AddComponent<Animator>();
|
|
try
|
|
{
|
|
var paramsObj = new JObject
|
|
{
|
|
["action"] = "animator_set_speed",
|
|
["target"] = "AnimTest_Speed",
|
|
["speed"] = 2.5f
|
|
};
|
|
var result = ToJObject(ManageAnimation.HandleCommand(paramsObj));
|
|
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
|
Assert.AreEqual(2.5f, animator.speed, 0.001f);
|
|
}
|
|
finally
|
|
{
|
|
UnityEngine.Object.DestroyImmediate(go);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void AnimatorSetEnabled_DisablesAnimator()
|
|
{
|
|
var go = new GameObject("AnimTest_Enabled");
|
|
var animator = go.AddComponent<Animator>();
|
|
Assert.IsTrue(animator.enabled);
|
|
try
|
|
{
|
|
var paramsObj = new JObject
|
|
{
|
|
["action"] = "animator_set_enabled",
|
|
["target"] = "AnimTest_Enabled",
|
|
["enabled"] = false
|
|
};
|
|
var result = ToJObject(ManageAnimation.HandleCommand(paramsObj));
|
|
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
|
Assert.IsFalse(animator.enabled);
|
|
}
|
|
finally
|
|
{
|
|
UnityEngine.Object.DestroyImmediate(go);
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Clip: Create
|
|
// =============================================================================
|
|
|
|
[Test]
|
|
public void ClipCreate_CreatesAsset()
|
|
{
|
|
string clipPath = $"{TempRoot}/TestClip_{Guid.NewGuid():N}.anim";
|
|
|
|
var paramsObj = new JObject
|
|
{
|
|
["action"] = "clip_create",
|
|
["clipPath"] = clipPath,
|
|
["length"] = 2.0f,
|
|
["loop"] = true
|
|
};
|
|
var result = ToJObject(ManageAnimation.HandleCommand(paramsObj));
|
|
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
|
|
|
var clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(clipPath);
|
|
Assert.IsNotNull(clip, "Clip asset should exist");
|
|
|
|
var settings = AnimationUtility.GetAnimationClipSettings(clip);
|
|
Assert.IsTrue(settings.loopTime, "Clip should be looping");
|
|
Assert.AreEqual(2.0f, settings.stopTime, 0.01f);
|
|
}
|
|
|
|
[Test]
|
|
public void ClipCreate_DuplicatePath_ReturnsError()
|
|
{
|
|
string clipPath = $"{TempRoot}/DuplicateClip.anim";
|
|
|
|
// Create first
|
|
var clip = new AnimationClip { name = "DuplicateClip" };
|
|
AssetDatabase.CreateAsset(clip, clipPath);
|
|
AssetDatabase.SaveAssets();
|
|
|
|
// Try to create again
|
|
var paramsObj = new JObject
|
|
{
|
|
["action"] = "clip_create",
|
|
["clipPath"] = clipPath,
|
|
};
|
|
var result = ToJObject(ManageAnimation.HandleCommand(paramsObj));
|
|
Assert.IsFalse(result.Value<bool>("success"));
|
|
Assert.That(result["message"].ToString(), Does.Contain("already exists"));
|
|
}
|
|
|
|
[Test]
|
|
public void ClipCreate_MissingPath_ReturnsError()
|
|
{
|
|
var paramsObj = new JObject { ["action"] = "clip_create" };
|
|
var result = ToJObject(ManageAnimation.HandleCommand(paramsObj));
|
|
Assert.IsFalse(result.Value<bool>("success"));
|
|
Assert.That(result["message"].ToString(), Does.Contain("clipPath"));
|
|
}
|
|
|
|
// =============================================================================
|
|
// Clip: Get Info
|
|
// =============================================================================
|
|
|
|
[Test]
|
|
public void ClipGetInfo_ReturnsClipData()
|
|
{
|
|
string clipName = $"InfoClip_{Guid.NewGuid():N}";
|
|
string clipPath = $"{TempRoot}/{clipName}.anim";
|
|
var clip = new AnimationClip { name = clipName, frameRate = 30f };
|
|
var settings = AnimationUtility.GetAnimationClipSettings(clip);
|
|
settings.loopTime = true;
|
|
settings.stopTime = 1.5f;
|
|
AnimationUtility.SetAnimationClipSettings(clip, settings);
|
|
AssetDatabase.CreateAsset(clip, clipPath);
|
|
AssetDatabase.SaveAssets();
|
|
|
|
var paramsObj = new JObject
|
|
{
|
|
["action"] = "clip_get_info",
|
|
["clipPath"] = clipPath
|
|
};
|
|
var result = ToJObject(ManageAnimation.HandleCommand(paramsObj));
|
|
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
|
|
|
var data = result["data"] as JObject;
|
|
Assert.IsNotNull(data);
|
|
Assert.AreEqual(clipName, data["name"].ToString());
|
|
Assert.AreEqual(30f, data.Value<float>("frameRate"), 0.01f);
|
|
Assert.IsTrue(data.Value<bool>("isLooping"));
|
|
}
|
|
|
|
[Test]
|
|
public void ClipGetInfo_NotFound_ReturnsError()
|
|
{
|
|
var paramsObj = new JObject
|
|
{
|
|
["action"] = "clip_get_info",
|
|
["clipPath"] = "Assets/Nonexistent.anim"
|
|
};
|
|
var result = ToJObject(ManageAnimation.HandleCommand(paramsObj));
|
|
Assert.IsFalse(result.Value<bool>("success"));
|
|
Assert.That(result["message"].ToString(), Does.Contain("not found"));
|
|
}
|
|
|
|
// =============================================================================
|
|
// Clip: Add / Set Curve
|
|
// =============================================================================
|
|
|
|
[Test]
|
|
public void ClipAddCurve_AddsKeyframes()
|
|
{
|
|
string clipPath = $"{TempRoot}/CurveClip_{Guid.NewGuid():N}.anim";
|
|
var clip = new AnimationClip { name = "CurveClip" };
|
|
AssetDatabase.CreateAsset(clip, clipPath);
|
|
AssetDatabase.SaveAssets();
|
|
|
|
var paramsObj = new JObject
|
|
{
|
|
["action"] = "clip_add_curve",
|
|
["clipPath"] = clipPath,
|
|
["propertyPath"] = "localPosition.y",
|
|
["type"] = "Transform",
|
|
["keys"] = new JArray(
|
|
new JArray(0f, 0f),
|
|
new JArray(0.5f, 2f),
|
|
new JArray(1f, 0f)
|
|
)
|
|
};
|
|
var result = ToJObject(ManageAnimation.HandleCommand(paramsObj));
|
|
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
|
|
|
// Verify curve was added
|
|
clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(clipPath);
|
|
var bindings = AnimationUtility.GetCurveBindings(clip);
|
|
Assert.AreEqual(1, bindings.Length);
|
|
Assert.AreEqual("localPosition.y", bindings[0].propertyName);
|
|
|
|
var curve = AnimationUtility.GetEditorCurve(clip, bindings[0]);
|
|
Assert.AreEqual(3, curve.length);
|
|
}
|
|
|
|
[Test]
|
|
public void ClipSetCurve_ReplacesKeyframes()
|
|
{
|
|
string clipPath = $"{TempRoot}/SetCurveClip_{Guid.NewGuid():N}.anim";
|
|
var clip = new AnimationClip { name = "SetCurveClip" };
|
|
|
|
// Add initial curve
|
|
var initialCurve = new AnimationCurve(new Keyframe(0, 0), new Keyframe(1, 1));
|
|
var binding = EditorCurveBinding.FloatCurve("", typeof(Transform), "localPosition.x");
|
|
AnimationUtility.SetEditorCurve(clip, binding, initialCurve);
|
|
|
|
AssetDatabase.CreateAsset(clip, clipPath);
|
|
AssetDatabase.SaveAssets();
|
|
|
|
// Replace with new keyframes
|
|
var paramsObj = new JObject
|
|
{
|
|
["action"] = "clip_set_curve",
|
|
["clipPath"] = clipPath,
|
|
["propertyPath"] = "localPosition.x",
|
|
["type"] = "Transform",
|
|
["keys"] = new JArray(
|
|
new JArray(0f, 5f),
|
|
new JArray(2f, 10f)
|
|
)
|
|
};
|
|
var result = ToJObject(ManageAnimation.HandleCommand(paramsObj));
|
|
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
|
|
|
clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(clipPath);
|
|
var curve = AnimationUtility.GetEditorCurve(clip, binding);
|
|
Assert.AreEqual(2, curve.length);
|
|
Assert.AreEqual(5f, curve.keys[0].value, 0.01f);
|
|
Assert.AreEqual(10f, curve.keys[1].value, 0.01f);
|
|
}
|
|
|
|
[Test]
|
|
public void ClipAddCurve_WithObjectFormat_ParsesKeyframes()
|
|
{
|
|
string clipPath = $"{TempRoot}/ObjFormatClip_{Guid.NewGuid():N}.anim";
|
|
var clip = new AnimationClip { name = "ObjFormatClip" };
|
|
AssetDatabase.CreateAsset(clip, clipPath);
|
|
AssetDatabase.SaveAssets();
|
|
|
|
var paramsObj = new JObject
|
|
{
|
|
["action"] = "clip_add_curve",
|
|
["clipPath"] = clipPath,
|
|
["propertyPath"] = "localPosition.z",
|
|
["type"] = "Transform",
|
|
["keys"] = new JArray(
|
|
new JObject { ["time"] = 0f, ["value"] = 0f, ["inTangent"] = 0f, ["outTangent"] = 1f },
|
|
new JObject { ["time"] = 1f, ["value"] = 5f }
|
|
)
|
|
};
|
|
var result = ToJObject(ManageAnimation.HandleCommand(paramsObj));
|
|
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
|
|
|
clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(clipPath);
|
|
var bindings = AnimationUtility.GetCurveBindings(clip);
|
|
Assert.AreEqual(1, bindings.Length);
|
|
var curve = AnimationUtility.GetEditorCurve(clip, bindings[0]);
|
|
Assert.AreEqual(2, curve.length);
|
|
Assert.AreEqual(1f, curve.keys[0].outTangent, 0.01f);
|
|
}
|
|
|
|
[Test]
|
|
public void ClipAddCurve_MissingKeys_ReturnsError()
|
|
{
|
|
string clipPath = $"{TempRoot}/NoKeysClip_{Guid.NewGuid():N}.anim";
|
|
var clip = new AnimationClip { name = "NoKeysClip" };
|
|
AssetDatabase.CreateAsset(clip, clipPath);
|
|
AssetDatabase.SaveAssets();
|
|
|
|
var paramsObj = new JObject
|
|
{
|
|
["action"] = "clip_add_curve",
|
|
["clipPath"] = clipPath,
|
|
["propertyPath"] = "localPosition.y",
|
|
["type"] = "Transform",
|
|
};
|
|
var result = ToJObject(ManageAnimation.HandleCommand(paramsObj));
|
|
Assert.IsFalse(result.Value<bool>("success"));
|
|
Assert.That(result["message"].ToString(), Does.Contain("keys"));
|
|
}
|
|
|
|
// =============================================================================
|
|
// Clip: Assign
|
|
// =============================================================================
|
|
|
|
[Test]
|
|
public void ClipAssign_AddsAnimationComponent()
|
|
{
|
|
string clipPath = $"{TempRoot}/AssignClip_{Guid.NewGuid():N}.anim";
|
|
var clip = new AnimationClip { name = "AssignClip" };
|
|
clip.legacy = true;
|
|
AssetDatabase.CreateAsset(clip, clipPath);
|
|
AssetDatabase.SaveAssets();
|
|
|
|
var go = new GameObject("AnimTest_Assign");
|
|
try
|
|
{
|
|
Assert.IsNull(go.GetComponent<UnityEngine.Animation>());
|
|
Assert.IsNull(go.GetComponent<Animator>());
|
|
|
|
var paramsObj = new JObject
|
|
{
|
|
["action"] = "clip_assign",
|
|
["target"] = "AnimTest_Assign",
|
|
["clipPath"] = clipPath
|
|
};
|
|
var result = ToJObject(ManageAnimation.HandleCommand(paramsObj));
|
|
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
|
|
|
var anim = go.GetComponent<UnityEngine.Animation>();
|
|
Assert.IsNotNull(anim, "Should have added Animation component");
|
|
Assert.IsNotNull(anim.clip, "Should have assigned clip");
|
|
}
|
|
finally
|
|
{
|
|
UnityEngine.Object.DestroyImmediate(go);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void ClipAssign_MissingClip_ReturnsError()
|
|
{
|
|
var go = new GameObject("AnimTest_AssignMissing");
|
|
try
|
|
{
|
|
var paramsObj = new JObject
|
|
{
|
|
["action"] = "clip_assign",
|
|
["target"] = "AnimTest_AssignMissing",
|
|
["clipPath"] = "Assets/Nonexistent.anim"
|
|
};
|
|
var result = ToJObject(ManageAnimation.HandleCommand(paramsObj));
|
|
Assert.IsFalse(result.Value<bool>("success"));
|
|
Assert.That(result["message"].ToString(), Does.Contain("not found"));
|
|
}
|
|
finally
|
|
{
|
|
UnityEngine.Object.DestroyImmediate(go);
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Parameter Normalization
|
|
// =============================================================================
|
|
|
|
[Test]
|
|
public void HandleCommand_SnakeCaseParams_Normalized()
|
|
{
|
|
// Test that snake_case parameters like clip_path get normalized to clipPath
|
|
string clipPath = $"{TempRoot}/SnakeCase_{Guid.NewGuid():N}.anim";
|
|
var paramsObj = new JObject
|
|
{
|
|
["action"] = "clip_create",
|
|
["clip_path"] = clipPath,
|
|
["length"] = 1.0f
|
|
};
|
|
var result = ToJObject(ManageAnimation.HandleCommand(paramsObj));
|
|
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
|
|
|
var clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(clipPath);
|
|
Assert.IsNotNull(clip);
|
|
}
|
|
|
|
[Test]
|
|
public void HandleCommand_PropertiesDict_Flattened()
|
|
{
|
|
// Test that properties dict is flattened into top-level params
|
|
string clipPath = $"{TempRoot}/PropsFlat_{Guid.NewGuid():N}.anim";
|
|
var paramsObj = new JObject
|
|
{
|
|
["action"] = "clip_create",
|
|
["properties"] = new JObject
|
|
{
|
|
["clipPath"] = clipPath,
|
|
["length"] = 1.5f,
|
|
["loop"] = true
|
|
}
|
|
};
|
|
var result = ToJObject(ManageAnimation.HandleCommand(paramsObj));
|
|
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
|
|
|
var clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(clipPath);
|
|
Assert.IsNotNull(clip);
|
|
var settings = AnimationUtility.GetAnimationClipSettings(clip);
|
|
Assert.IsTrue(settings.loopTime);
|
|
}
|
|
|
|
// =============================================================================
|
|
// Controller: Dispatch
|
|
// =============================================================================
|
|
|
|
[Test]
|
|
public void HandleCommand_UnknownControllerAction_ReturnsError()
|
|
{
|
|
var paramsObj = new JObject { ["action"] = "controller_nonexistent" };
|
|
var result = ToJObject(ManageAnimation.HandleCommand(paramsObj));
|
|
Assert.IsFalse(result.Value<bool>("success"));
|
|
Assert.That(result["message"].ToString(), Does.Contain("Unknown controller action"));
|
|
}
|
|
|
|
// =============================================================================
|
|
// Controller: Create
|
|
// =============================================================================
|
|
|
|
[Test]
|
|
public void ControllerCreate_CreatesAsset()
|
|
{
|
|
string controllerPath = $"{TempRoot}/TestController_{Guid.NewGuid():N}.controller";
|
|
|
|
var paramsObj = new JObject
|
|
{
|
|
["action"] = "controller_create",
|
|
["controllerPath"] = controllerPath
|
|
};
|
|
var result = ToJObject(ManageAnimation.HandleCommand(paramsObj));
|
|
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
|
|
|
var controller = AssetDatabase.LoadAssetAtPath<AnimatorController>(controllerPath);
|
|
Assert.IsNotNull(controller, "Controller asset should exist");
|
|
}
|
|
|
|
[Test]
|
|
public void ControllerCreate_DuplicatePath_ReturnsError()
|
|
{
|
|
string controllerPath = $"{TempRoot}/DuplicateController.controller";
|
|
|
|
var controller = AnimatorController.CreateAnimatorControllerAtPath(controllerPath);
|
|
AssetDatabase.SaveAssets();
|
|
|
|
var paramsObj = new JObject
|
|
{
|
|
["action"] = "controller_create",
|
|
["controllerPath"] = controllerPath
|
|
};
|
|
var result = ToJObject(ManageAnimation.HandleCommand(paramsObj));
|
|
Assert.IsFalse(result.Value<bool>("success"));
|
|
Assert.That(result["message"].ToString(), Does.Contain("already exists"));
|
|
}
|
|
|
|
[Test]
|
|
public void ControllerCreate_MissingPath_ReturnsError()
|
|
{
|
|
var paramsObj = new JObject { ["action"] = "controller_create" };
|
|
var result = ToJObject(ManageAnimation.HandleCommand(paramsObj));
|
|
Assert.IsFalse(result.Value<bool>("success"));
|
|
Assert.That(result["message"].ToString(), Does.Contain("controllerPath"));
|
|
}
|
|
|
|
// =============================================================================
|
|
// Controller: Add State
|
|
// =============================================================================
|
|
|
|
[Test]
|
|
public void ControllerAddState_AddsState()
|
|
{
|
|
string controllerPath = $"{TempRoot}/StateController_{Guid.NewGuid():N}.controller";
|
|
AnimatorController.CreateAnimatorControllerAtPath(controllerPath);
|
|
AssetDatabase.SaveAssets();
|
|
|
|
var paramsObj = new JObject
|
|
{
|
|
["action"] = "controller_add_state",
|
|
["controllerPath"] = controllerPath,
|
|
["stateName"] = "Walk"
|
|
};
|
|
var result = ToJObject(ManageAnimation.HandleCommand(paramsObj));
|
|
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
|
|
|
var controller = AssetDatabase.LoadAssetAtPath<AnimatorController>(controllerPath);
|
|
var states = controller.layers[0].stateMachine.states;
|
|
Assert.IsTrue(states.Any(s => s.state.name == "Walk"), "State 'Walk' should exist");
|
|
}
|
|
|
|
[Test]
|
|
public void ControllerAddState_DuplicateName_ReturnsError()
|
|
{
|
|
string controllerPath = $"{TempRoot}/DupStateController_{Guid.NewGuid():N}.controller";
|
|
var controller = AnimatorController.CreateAnimatorControllerAtPath(controllerPath);
|
|
controller.layers[0].stateMachine.AddState("Idle");
|
|
AssetDatabase.SaveAssets();
|
|
|
|
var paramsObj = new JObject
|
|
{
|
|
["action"] = "controller_add_state",
|
|
["controllerPath"] = controllerPath,
|
|
["stateName"] = "Idle"
|
|
};
|
|
var result = ToJObject(ManageAnimation.HandleCommand(paramsObj));
|
|
Assert.IsFalse(result.Value<bool>("success"));
|
|
Assert.That(result["message"].ToString(), Does.Contain("already exists"));
|
|
}
|
|
|
|
[Test]
|
|
public void ControllerAddState_WithClip_AssignsMotion()
|
|
{
|
|
string controllerPath = $"{TempRoot}/MotionController_{Guid.NewGuid():N}.controller";
|
|
AnimatorController.CreateAnimatorControllerAtPath(controllerPath);
|
|
|
|
string clipPath = $"{TempRoot}/MotionClip_{Guid.NewGuid():N}.anim";
|
|
var clip = new AnimationClip { name = "MotionClip" };
|
|
AssetDatabase.CreateAsset(clip, clipPath);
|
|
AssetDatabase.SaveAssets();
|
|
|
|
var paramsObj = new JObject
|
|
{
|
|
["action"] = "controller_add_state",
|
|
["controllerPath"] = controllerPath,
|
|
["stateName"] = "Run",
|
|
["clipPath"] = clipPath
|
|
};
|
|
var result = ToJObject(ManageAnimation.HandleCommand(paramsObj));
|
|
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
|
|
|
var controller = AssetDatabase.LoadAssetAtPath<AnimatorController>(controllerPath);
|
|
var state = controller.layers[0].stateMachine.states.First(s => s.state.name == "Run").state;
|
|
Assert.IsNotNull(state.motion, "State should have motion assigned");
|
|
}
|
|
|
|
// =============================================================================
|
|
// Controller: Add Transition
|
|
// =============================================================================
|
|
|
|
[Test]
|
|
public void ControllerAddTransition_AddsTransition()
|
|
{
|
|
string controllerPath = $"{TempRoot}/TransController_{Guid.NewGuid():N}.controller";
|
|
var controller = AnimatorController.CreateAnimatorControllerAtPath(controllerPath);
|
|
var sm = controller.layers[0].stateMachine;
|
|
sm.AddState("Idle");
|
|
sm.AddState("Walk");
|
|
AssetDatabase.SaveAssets();
|
|
|
|
var paramsObj = new JObject
|
|
{
|
|
["action"] = "controller_add_transition",
|
|
["controllerPath"] = controllerPath,
|
|
["fromState"] = "Idle",
|
|
["toState"] = "Walk",
|
|
["hasExitTime"] = false,
|
|
["duration"] = 0.1f
|
|
};
|
|
var result = ToJObject(ManageAnimation.HandleCommand(paramsObj));
|
|
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
|
|
|
controller = AssetDatabase.LoadAssetAtPath<AnimatorController>(controllerPath);
|
|
var idleState = controller.layers[0].stateMachine.states.First(s => s.state.name == "Idle").state;
|
|
Assert.AreEqual(1, idleState.transitions.Length);
|
|
Assert.AreEqual("Walk", idleState.transitions[0].destinationState.name);
|
|
Assert.IsFalse(idleState.transitions[0].hasExitTime);
|
|
}
|
|
|
|
[Test]
|
|
public void ControllerAddTransition_WithConditions_AddsConditions()
|
|
{
|
|
string controllerPath = $"{TempRoot}/CondController_{Guid.NewGuid():N}.controller";
|
|
var controller = AnimatorController.CreateAnimatorControllerAtPath(controllerPath);
|
|
controller.AddParameter("Speed", AnimatorControllerParameterType.Float);
|
|
var sm = controller.layers[0].stateMachine;
|
|
sm.AddState("Idle");
|
|
sm.AddState("Walk");
|
|
AssetDatabase.SaveAssets();
|
|
|
|
var paramsObj = new JObject
|
|
{
|
|
["action"] = "controller_add_transition",
|
|
["controllerPath"] = controllerPath,
|
|
["fromState"] = "Idle",
|
|
["toState"] = "Walk",
|
|
["conditions"] = new JArray(
|
|
new JObject
|
|
{
|
|
["parameter"] = "Speed",
|
|
["mode"] = "greater",
|
|
["threshold"] = 0.1f
|
|
}
|
|
)
|
|
};
|
|
var result = ToJObject(ManageAnimation.HandleCommand(paramsObj));
|
|
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
|
|
|
controller = AssetDatabase.LoadAssetAtPath<AnimatorController>(controllerPath);
|
|
var idleState = controller.layers[0].stateMachine.states.First(s => s.state.name == "Idle").state;
|
|
Assert.AreEqual(1, idleState.transitions[0].conditions.Length);
|
|
Assert.AreEqual("Speed", idleState.transitions[0].conditions[0].parameter);
|
|
}
|
|
|
|
[Test]
|
|
public void ControllerAddTransition_MissingState_ReturnsError()
|
|
{
|
|
string controllerPath = $"{TempRoot}/MissStateController_{Guid.NewGuid():N}.controller";
|
|
var controller = AnimatorController.CreateAnimatorControllerAtPath(controllerPath);
|
|
controller.layers[0].stateMachine.AddState("Idle");
|
|
AssetDatabase.SaveAssets();
|
|
|
|
var paramsObj = new JObject
|
|
{
|
|
["action"] = "controller_add_transition",
|
|
["controllerPath"] = controllerPath,
|
|
["fromState"] = "Idle",
|
|
["toState"] = "Nonexistent"
|
|
};
|
|
var result = ToJObject(ManageAnimation.HandleCommand(paramsObj));
|
|
Assert.IsFalse(result.Value<bool>("success"));
|
|
Assert.That(result["message"].ToString(), Does.Contain("not found"));
|
|
}
|
|
|
|
// =============================================================================
|
|
// Controller: Add Parameter
|
|
// =============================================================================
|
|
|
|
[Test]
|
|
public void ControllerAddParameter_AddsParameter()
|
|
{
|
|
string controllerPath = $"{TempRoot}/ParamController_{Guid.NewGuid():N}.controller";
|
|
AnimatorController.CreateAnimatorControllerAtPath(controllerPath);
|
|
AssetDatabase.SaveAssets();
|
|
|
|
var paramsObj = new JObject
|
|
{
|
|
["action"] = "controller_add_parameter",
|
|
["controllerPath"] = controllerPath,
|
|
["parameterName"] = "Speed",
|
|
["parameterType"] = "float",
|
|
["defaultValue"] = 1.5f
|
|
};
|
|
var result = ToJObject(ManageAnimation.HandleCommand(paramsObj));
|
|
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
|
|
|
var controller = AssetDatabase.LoadAssetAtPath<AnimatorController>(controllerPath);
|
|
Assert.IsTrue(controller.parameters.Any(p => p.name == "Speed"), "Parameter 'Speed' should exist");
|
|
var param = controller.parameters.First(p => p.name == "Speed");
|
|
Assert.AreEqual(AnimatorControllerParameterType.Float, param.type);
|
|
Assert.AreEqual(1.5f, param.defaultFloat, 0.01f);
|
|
}
|
|
|
|
[Test]
|
|
public void ControllerAddParameter_DuplicateName_ReturnsError()
|
|
{
|
|
string controllerPath = $"{TempRoot}/DupParamController_{Guid.NewGuid():N}.controller";
|
|
var controller = AnimatorController.CreateAnimatorControllerAtPath(controllerPath);
|
|
controller.AddParameter("Speed", AnimatorControllerParameterType.Float);
|
|
AssetDatabase.SaveAssets();
|
|
|
|
var paramsObj = new JObject
|
|
{
|
|
["action"] = "controller_add_parameter",
|
|
["controllerPath"] = controllerPath,
|
|
["parameterName"] = "Speed",
|
|
["parameterType"] = "float"
|
|
};
|
|
var result = ToJObject(ManageAnimation.HandleCommand(paramsObj));
|
|
Assert.IsFalse(result.Value<bool>("success"));
|
|
Assert.That(result["message"].ToString(), Does.Contain("already exists"));
|
|
}
|
|
|
|
[Test]
|
|
public void ControllerAddParameter_AllTypes()
|
|
{
|
|
string controllerPath = $"{TempRoot}/AllTypesController_{Guid.NewGuid():N}.controller";
|
|
AnimatorController.CreateAnimatorControllerAtPath(controllerPath);
|
|
AssetDatabase.SaveAssets();
|
|
|
|
string[] types = { "float", "int", "bool", "trigger" };
|
|
foreach (var t in types)
|
|
{
|
|
var paramsObj = new JObject
|
|
{
|
|
["action"] = "controller_add_parameter",
|
|
["controllerPath"] = controllerPath,
|
|
["parameterName"] = $"Param_{t}",
|
|
["parameterType"] = t
|
|
};
|
|
var result = ToJObject(ManageAnimation.HandleCommand(paramsObj));
|
|
Assert.IsTrue(result.Value<bool>("success"), $"Failed for type {t}: {result}");
|
|
}
|
|
|
|
var controller = AssetDatabase.LoadAssetAtPath<AnimatorController>(controllerPath);
|
|
Assert.AreEqual(4, controller.parameters.Length);
|
|
}
|
|
|
|
// =============================================================================
|
|
// Controller: Get Info
|
|
// =============================================================================
|
|
|
|
[Test]
|
|
public void ControllerGetInfo_ReturnsData()
|
|
{
|
|
string controllerPath = $"{TempRoot}/InfoController_{Guid.NewGuid():N}.controller";
|
|
var controller = AnimatorController.CreateAnimatorControllerAtPath(controllerPath);
|
|
controller.AddParameter("Speed", AnimatorControllerParameterType.Float);
|
|
var sm = controller.layers[0].stateMachine;
|
|
sm.AddState("Idle");
|
|
sm.AddState("Walk");
|
|
AssetDatabase.SaveAssets();
|
|
|
|
var paramsObj = new JObject
|
|
{
|
|
["action"] = "controller_get_info",
|
|
["controllerPath"] = controllerPath
|
|
};
|
|
var result = ToJObject(ManageAnimation.HandleCommand(paramsObj));
|
|
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
|
|
|
var data = result["data"] as JObject;
|
|
Assert.IsNotNull(data);
|
|
Assert.AreEqual(1, data.Value<int>("parameterCount"));
|
|
Assert.AreEqual(1, data.Value<int>("layerCount"));
|
|
|
|
var layers = data["layers"] as JArray;
|
|
Assert.IsNotNull(layers);
|
|
Assert.AreEqual(1, layers.Count);
|
|
}
|
|
|
|
[Test]
|
|
public void ControllerGetInfo_NotFound_ReturnsError()
|
|
{
|
|
var paramsObj = new JObject
|
|
{
|
|
["action"] = "controller_get_info",
|
|
["controllerPath"] = "Assets/Nonexistent.controller"
|
|
};
|
|
var result = ToJObject(ManageAnimation.HandleCommand(paramsObj));
|
|
Assert.IsFalse(result.Value<bool>("success"));
|
|
}
|
|
|
|
// =============================================================================
|
|
// Controller: Assign
|
|
// =============================================================================
|
|
|
|
[Test]
|
|
public void ControllerAssign_AddsAnimatorAndAssigns()
|
|
{
|
|
string controllerPath = $"{TempRoot}/AssignController_{Guid.NewGuid():N}.controller";
|
|
AnimatorController.CreateAnimatorControllerAtPath(controllerPath);
|
|
AssetDatabase.SaveAssets();
|
|
|
|
var go = new GameObject("AnimTest_ControllerAssign");
|
|
try
|
|
{
|
|
Assert.IsNull(go.GetComponent<Animator>());
|
|
|
|
var paramsObj = new JObject
|
|
{
|
|
["action"] = "controller_assign",
|
|
["controllerPath"] = controllerPath,
|
|
["target"] = "AnimTest_ControllerAssign"
|
|
};
|
|
var result = ToJObject(ManageAnimation.HandleCommand(paramsObj));
|
|
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
|
|
|
var animator = go.GetComponent<Animator>();
|
|
Assert.IsNotNull(animator, "Should have added Animator component");
|
|
Assert.IsNotNull(animator.runtimeAnimatorController, "Should have assigned controller");
|
|
}
|
|
finally
|
|
{
|
|
UnityEngine.Object.DestroyImmediate(go);
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Clip: Set Vector Curve
|
|
// =============================================================================
|
|
|
|
[Test]
|
|
public void ClipSetVectorCurve_Sets3Curves()
|
|
{
|
|
string clipPath = $"{TempRoot}/VectorClip_{Guid.NewGuid():N}.anim";
|
|
var clip = new AnimationClip { name = "VectorClip" };
|
|
AssetDatabase.CreateAsset(clip, clipPath);
|
|
AssetDatabase.SaveAssets();
|
|
|
|
var paramsObj = new JObject
|
|
{
|
|
["action"] = "clip_set_vector_curve",
|
|
["clipPath"] = clipPath,
|
|
["property"] = "localPosition",
|
|
["keys"] = new JArray(
|
|
new JObject { ["time"] = 0f, ["value"] = new JArray(0f, 1f, -10f) },
|
|
new JObject { ["time"] = 1f, ["value"] = new JArray(2f, 1f, -10f) }
|
|
)
|
|
};
|
|
var result = ToJObject(ManageAnimation.HandleCommand(paramsObj));
|
|
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
|
|
|
clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(clipPath);
|
|
var bindings = AnimationUtility.GetCurveBindings(clip);
|
|
// clip.SetCurve doesn't populate EditorCurve bindings — it uses legacy runtime curves
|
|
// Verify via the data response
|
|
var data = result["data"] as JObject;
|
|
Assert.IsNotNull(data);
|
|
Assert.AreEqual(2, data.Value<int>("keyframeCount"));
|
|
var curves = data["curves"] as JArray;
|
|
Assert.IsNotNull(curves);
|
|
Assert.AreEqual(3, curves.Count);
|
|
}
|
|
|
|
[Test]
|
|
public void ClipSetVectorCurve_MissingProperty_ReturnsError()
|
|
{
|
|
string clipPath = $"{TempRoot}/NoPropertyClip_{Guid.NewGuid():N}.anim";
|
|
var clip = new AnimationClip { name = "NoPropertyClip" };
|
|
AssetDatabase.CreateAsset(clip, clipPath);
|
|
AssetDatabase.SaveAssets();
|
|
|
|
var paramsObj = new JObject
|
|
{
|
|
["action"] = "clip_set_vector_curve",
|
|
["clipPath"] = clipPath,
|
|
["keys"] = new JArray(
|
|
new JObject { ["time"] = 0f, ["value"] = new JArray(0f, 0f, 0f) }
|
|
)
|
|
};
|
|
var result = ToJObject(ManageAnimation.HandleCommand(paramsObj));
|
|
Assert.IsFalse(result.Value<bool>("success"));
|
|
Assert.That(result["message"].ToString(), Does.Contain("property"));
|
|
}
|
|
|
|
[Test]
|
|
public void ClipSetVectorCurve_InvalidValueFormat_ReturnsError()
|
|
{
|
|
string clipPath = $"{TempRoot}/BadValueClip_{Guid.NewGuid():N}.anim";
|
|
var clip = new AnimationClip { name = "BadValueClip" };
|
|
AssetDatabase.CreateAsset(clip, clipPath);
|
|
AssetDatabase.SaveAssets();
|
|
|
|
var paramsObj = new JObject
|
|
{
|
|
["action"] = "clip_set_vector_curve",
|
|
["clipPath"] = clipPath,
|
|
["property"] = "localPosition",
|
|
["keys"] = new JArray(
|
|
new JObject { ["time"] = 0f, ["value"] = new JArray(0f, 1f) } // Only 2 elements
|
|
)
|
|
};
|
|
var result = ToJObject(ManageAnimation.HandleCommand(paramsObj));
|
|
Assert.IsFalse(result.Value<bool>("success"));
|
|
Assert.That(result["message"].ToString(), Does.Contain("3-element"));
|
|
}
|
|
|
|
// =============================================================================
|
|
// Clip: Create Preset
|
|
// =============================================================================
|
|
|
|
[Test]
|
|
public void ClipCreatePreset_Bounce_CreatesClip()
|
|
{
|
|
string clipPath = $"{TempRoot}/BouncePreset_{Guid.NewGuid():N}.anim";
|
|
var paramsObj = new JObject
|
|
{
|
|
["action"] = "clip_create_preset",
|
|
["clipPath"] = clipPath,
|
|
["preset"] = "bounce",
|
|
["duration"] = 2.0f,
|
|
["amplitude"] = 0.5f,
|
|
["loop"] = true
|
|
};
|
|
var result = ToJObject(ManageAnimation.HandleCommand(paramsObj));
|
|
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
|
|
|
var clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(clipPath);
|
|
Assert.IsNotNull(clip, "Bounce preset clip should exist");
|
|
|
|
var settings = AnimationUtility.GetAnimationClipSettings(clip);
|
|
Assert.IsTrue(settings.loopTime, "Should be looping");
|
|
}
|
|
|
|
[Test]
|
|
public void ClipCreatePreset_AllPresetsCreateSuccessfully()
|
|
{
|
|
string[] presets = { "bounce", "rotate", "pulse", "fade", "shake", "hover", "spin" };
|
|
foreach (var preset in presets)
|
|
{
|
|
string clipPath = $"{TempRoot}/{preset}Preset_{Guid.NewGuid():N}.anim";
|
|
var paramsObj = new JObject
|
|
{
|
|
["action"] = "clip_create_preset",
|
|
["clipPath"] = clipPath,
|
|
["preset"] = preset
|
|
};
|
|
var result = ToJObject(ManageAnimation.HandleCommand(paramsObj));
|
|
Assert.IsTrue(result.Value<bool>("success"), $"Preset '{preset}' failed: {result}");
|
|
|
|
var clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(clipPath);
|
|
Assert.IsNotNull(clip, $"Clip for preset '{preset}' should exist");
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void ClipCreatePreset_InvalidPreset_ReturnsError()
|
|
{
|
|
string clipPath = $"{TempRoot}/BadPreset_{Guid.NewGuid():N}.anim";
|
|
var paramsObj = new JObject
|
|
{
|
|
["action"] = "clip_create_preset",
|
|
["clipPath"] = clipPath,
|
|
["preset"] = "nonexistent"
|
|
};
|
|
var result = ToJObject(ManageAnimation.HandleCommand(paramsObj));
|
|
Assert.IsFalse(result.Value<bool>("success"));
|
|
Assert.That(result["message"].ToString(), Does.Contain("Unknown preset"));
|
|
}
|
|
|
|
[Test]
|
|
public void ClipCreatePreset_MissingPreset_ReturnsError()
|
|
{
|
|
string clipPath = $"{TempRoot}/NoPreset_{Guid.NewGuid():N}.anim";
|
|
var paramsObj = new JObject
|
|
{
|
|
["action"] = "clip_create_preset",
|
|
["clipPath"] = clipPath
|
|
};
|
|
var result = ToJObject(ManageAnimation.HandleCommand(paramsObj));
|
|
Assert.IsFalse(result.Value<bool>("success"));
|
|
Assert.That(result["message"].ToString(), Does.Contain("preset"));
|
|
}
|
|
|
|
[Test]
|
|
public void ClipCreatePreset_DuplicatePath_ReturnsError()
|
|
{
|
|
string clipPath = $"{TempRoot}/ExistingPreset.anim";
|
|
var clip = new AnimationClip { name = "ExistingPreset" };
|
|
AssetDatabase.CreateAsset(clip, clipPath);
|
|
AssetDatabase.SaveAssets();
|
|
|
|
var paramsObj = new JObject
|
|
{
|
|
["action"] = "clip_create_preset",
|
|
["clipPath"] = clipPath,
|
|
["preset"] = "bounce"
|
|
};
|
|
var result = ToJObject(ManageAnimation.HandleCommand(paramsObj));
|
|
Assert.IsFalse(result.Value<bool>("success"));
|
|
Assert.That(result["message"].ToString(), Does.Contain("already exists"));
|
|
}
|
|
}
|
|
}
|