Initial commit: Tiny Tackle Heroes Unity project
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user