132 lines
6.6 KiB
C#
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)));
|
|
}
|
|
}
|
|
}
|