Files

132 lines
6.6 KiB
C#

using System.Linq;
using System.Reflection;
using BumpCombat.Enemies;
using NUnit.Framework;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
namespace BumpCombat.Tests
{
public sealed class SummonAnimationContractTests
{
[TestCase(false, 1.25f)]
[TestCase(true, 1.875f)]
public void SummonCircle_FollowsShadowCenterAcrossFramesAndMovement(bool flip, float scale)
{
var sprites = AssetDatabase.LoadAllAssetsAtPath(
"Assets/_Project/Art/Characters/Necromancer/Necromancer_Summon-shadow-v2.png")
.OfType<Sprite>().ToArray();
Assert.That(sprites.Length, Is.EqualTo(10));
var owner = new GameObject("Summon anchor test");
StageEnemyEffectVisual effect = null;
try
{
var body = owner.AddComponent<SpriteRenderer>();
body.sprite = sprites[0];
body.flipX = flip;
owner.transform.localScale = new Vector3(scale, scale, 1f);
effect = StageEnemyEffectVisual.PlaySummon(owner, owner.transform.position, 1.25f);
var lateUpdate = typeof(StageEnemyEffectVisual).GetMethod("LateUpdate",
BindingFlags.NonPublic | BindingFlags.Instance);
for (int i = 0; i < sprites.Length; i++)
{
body.sprite = sprites[i];
owner.transform.position = new Vector3(i * .3f, i * -.2f, 0f);
lateUpdate.Invoke(effect, null);
// Measured source shadow center, relative to the imported 50px pivot.
Vector3 expected = owner.transform.position
+ new Vector3((flip ? -.5f : .5f) / 32f, -7.5f / 32f) * scale;
Assert.That(Vector3.Distance(effect.transform.position, expected), Is.LessThan(.0001f));
Assert.That(effect.transform.localScale, Is.EqualTo(Vector3.one));
}
}
finally
{
if (effect != null) Object.DestroyImmediate(effect.gameObject);
Object.DestroyImmediate(owner);
}
}
[TestCase("Skeleton", 5)]
[TestCase("GreatswordSkeleton", 5)]
[TestCase("ArmoredSkeleton", 5)]
[TestCase("SkeletonArcher", 5)]
[TestCase("Necromancer", 10)]
[TestCase("Bat", 4)]
[TestCase("Slime", 4)]
[TestCase("Necrofire", 6)]
[TestCase("Werewolf", 4)]
[TestCase("Werebear", 4)]
[TestCase("NecroGolem", 6)]
public void Summon_PreservesEveryAuthoredFrameAndCannotExitEarly(string name, int frames)
{
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(
$"Assets/_Project/Prefabs/Enemies/{name}.prefab");
var controller = prefab.GetComponent<Animator>().runtimeAnimatorController as AnimatorController;
Assert.That(controller, Is.Not.Null);
var summon = controller.layers[0].stateMachine.states
.Select(child => child.state).Single(state => state.name == "Summon");
var clip = summon.motion as AnimationClip;
Assert.That(clip, Is.Not.Null);
Assert.That(clip.frameRate, Is.EqualTo(8f));
Assert.That(clip.length, Is.EqualTo(frames / 8f).Within(.0001f));
Assert.That(clip.isLooping, Is.False);
var binding = AnimationUtility.GetObjectReferenceCurveBindings(clip)
.Single(b => b.type == typeof(SpriteRenderer) && b.propertyName == "m_Sprite");
var keys = AnimationUtility.GetObjectReferenceCurve(clip, binding);
Assert.That(keys.Length, Is.EqualTo(frames));
for (int i = 0; i < keys.Length; i++)
{
Assert.That(keys[i].value, Is.Not.Null);
Assert.That(keys[i].time, Is.EqualTo(i / 8f).Within(.0001f));
var texturePath = AssetDatabase.GetAssetPath(keys[i].value);
var importer = AssetImporter.GetAtPath(texturePath) as TextureImporter;
Assert.That(importer.filterMode, Is.EqualTo(FilterMode.Point));
Assert.That(importer.spritePixelsPerUnit, Is.EqualTo(32f));
if (name == "GreatswordSkeleton" || name == "ArmoredSkeleton")
{
var sprite = (Sprite)keys[i].value;
var masks = Resources.LoadAll<Sprite>(
"Enemies/Protection-v2/" + sprite.texture.name + "-outline");
Assert.That(masks.Any(mask => mask.rect == sprite.rect), Is.True,
"Protected summons must retain their outline during every rise frame.");
}
}
Assert.That(summon.transitions, Is.Not.Empty);
foreach (var transition in summon.transitions)
{
Assert.That(transition.hasExitTime, Is.True);
Assert.That(transition.exitTime, Is.EqualTo(1f));
Assert.That(transition.duration, Is.Zero);
// Attack exits are deliberately left independent of the summon clock.
foreach (var child in controller.layers[0].stateMachine.states)
if (child.state != summon)
Assert.That(child.state.transitions.Contains(transition), Is.False);
}
}
[TestCase("Bat")]
[TestCase("Slime")]
[TestCase("Necrofire")]
[TestCase("Werewolf")]
[TestCase("Werebear")]
[TestCase("NecroGolem")]
public void DerivedRise_ReusesOriginalDeathSpritesInReverse(string name)
{
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(
$"Assets/_Project/Prefabs/Enemies/{name}.prefab");
var controller = prefab.GetComponent<Animator>().runtimeAnimatorController as AnimatorController;
var states = controller.layers[0].stateMachine.states;
var death = (AnimationClip)states.Single(s => s.state.name == "Death").state.motion;
var rise = (AnimationClip)states.Single(s => s.state.name == "Summon").state.motion;
var deathBinding = AnimationUtility.GetObjectReferenceCurveBindings(death).Single();
var riseBinding = AnimationUtility.GetObjectReferenceCurveBindings(rise).Single();
var deathFrames = AnimationUtility.GetObjectReferenceCurve(death, deathBinding);
var riseFrames = AnimationUtility.GetObjectReferenceCurve(rise, riseBinding);
Assert.That(riseFrames.Select(f => f.value),
Is.EqualTo(deathFrames.Reverse().Select(f => f.value)));
}
}
}