193 lines
8.8 KiB
C#
193 lines
8.8 KiB
C#
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using BumpCombat.Constants;
|
|
using BumpCombat.Core;
|
|
using BumpCombat.Defense.Comparison;
|
|
using BumpCombat.Enemies;
|
|
using NUnit.Framework;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
using UnityEngine.TestTools;
|
|
using Object = UnityEngine.Object;
|
|
|
|
namespace BumpCombat.Tests
|
|
{
|
|
public sealed class Defense25DMonsterCombatAdapterPlayModeTests
|
|
{
|
|
private const string SettingsPath = "Assets/_Project/Constants/Defense25DComparison/ComparisonSettings.asset";
|
|
private const string PoolPath = "Assets/_Project/Constants/Defense25DComparison/MonsterPools.asset";
|
|
private Defense25DComparison world;
|
|
private Defense25DComparisonSettings settings;
|
|
private readonly List<Object> temporaryObjects = new();
|
|
private float previousTimeScale;
|
|
|
|
[UnitySetUp]
|
|
public IEnumerator SetUp()
|
|
{
|
|
previousTimeScale = Time.timeScale;
|
|
Time.timeScale = 1f;
|
|
settings = Object.Instantiate(AssetDatabase.LoadAssetAtPath<Defense25DComparisonSettings>(SettingsPath));
|
|
Assert.That(settings, Is.Not.Null);
|
|
world = new GameObject("Monster combat adapter under test").AddComponent<Defense25DComparison>();
|
|
world.Initialize(settings);
|
|
world.AutoSpawn = false;
|
|
world.ReadInput = false;
|
|
world.SetPaused(false);
|
|
Time.timeScale = 0f;
|
|
SetGameTime(0f);
|
|
yield return null;
|
|
}
|
|
|
|
[UnityTearDown]
|
|
public IEnumerator TearDown()
|
|
{
|
|
for (int i = temporaryObjects.Count - 1; i >= 0; i--)
|
|
if (temporaryObjects[i] != null) Object.Destroy(temporaryObjects[i]);
|
|
temporaryObjects.Clear();
|
|
if (world != null) Object.Destroy(world.gameObject);
|
|
if (settings != null) Object.Destroy(settings);
|
|
Time.timeScale = previousTimeScale;
|
|
yield return null;
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator ContactAttackWarnsBeforeApplyingOneRealActorDamageEvent()
|
|
{
|
|
Defense25DMonsterPoolEntry entry = FindEntry("Slime");
|
|
Assert.That(entry.CombatProfile.Archetype, Is.EqualTo(Defense25DMonsterAttackArchetype.Contact));
|
|
Defense25DEnemy enemy = world.Enemies[0];
|
|
enemy.Spawn(Vector2.zero);
|
|
world.Player.ResetAt(enemy.Actor.Center + Vector2.right * .05f);
|
|
Physics.SyncTransforms();
|
|
|
|
Sprite frame = CreateFrame();
|
|
var motion = CreateMotion(entry, frame);
|
|
var warningEvents = new List<Defense25DMonsterCombatWarning>();
|
|
var adapter = new Defense25DMonsterCombatAdapter();
|
|
Assert.That(adapter.Bind(entry, enemy.Actor, motion, () => world.Player, out string error,
|
|
setWarning: warningEvents.Add), Is.True, error);
|
|
|
|
Assert.That(adapter.Step(.001f, moving: false), Is.True);
|
|
Assert.That(warningEvents.Last().Visible, Is.True);
|
|
Assert.That(world.Player.DamageEvents, Is.Zero, "Starting a telegraph cannot apply damage.");
|
|
|
|
adapter.Step(entry.CombatProfile.WarningSeconds - .002f, moving: false);
|
|
Assert.That(world.Player.DamageEvents, Is.Zero, "The configured warning duration must remain safe.");
|
|
|
|
adapter.Step(.003f, moving: false);
|
|
Assert.That(warningEvents.Last().Visible, Is.False);
|
|
Assert.That(world.Player.DamageEvents, Is.EqualTo(1));
|
|
float damagedHealth = world.Player.CurrentHealth;
|
|
Assert.That(damagedHealth, Is.LessThan(world.Player.MaxHealth));
|
|
adapter.Step(.02f, moving: false);
|
|
Assert.That(world.Player.DamageEvents, Is.EqualTo(1), "One contact attack can damage only once.");
|
|
Assert.That(world.Player.CurrentHealth, Is.EqualTo(damagedHealth),
|
|
"The later attack frame cannot reapply its contact damage.");
|
|
|
|
adapter.Cancel();
|
|
yield return null;
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator ContactAttackAppliesTheAttackerAttackDamageModifiers()
|
|
{
|
|
Defense25DMonsterPoolEntry entry = FindEntry("Slime");
|
|
Defense25DEnemy enemy = world.Enemies[0];
|
|
enemy.Spawn(Vector2.zero);
|
|
Assert.That(enemy.Actor.AddModifier(new StatModifier("test-attack-flat",
|
|
CharacterStat.AttackDamage, ModifierOperation.Flat, 7f)), Is.True);
|
|
float expectedDamage = enemy.Actor.CalculateAttackDamage(
|
|
entry.CombatProfile.AttackDamage * entry.Definition.GetAttackPattern(0).DamageMultiplier);
|
|
|
|
world.Player.ResetAt(enemy.Actor.Center + Vector2.right * .05f);
|
|
Physics.SyncTransforms();
|
|
var adapter = new Defense25DMonsterCombatAdapter();
|
|
Assert.That(adapter.Bind(entry, enemy.Actor, CreateMotion(entry, CreateFrame()),
|
|
() => world.Player, out string error), Is.True, error);
|
|
|
|
adapter.Step(.001f, moving: false);
|
|
adapter.Step(entry.CombatProfile.WarningSeconds + .002f, moving: false);
|
|
|
|
Assert.That(world.Player.DamageEvents, Is.EqualTo(1));
|
|
Assert.That(world.Player.MaxHealth - world.Player.CurrentHealth,
|
|
Is.EqualTo(expectedDamage).Within(.01f));
|
|
adapter.Cancel();
|
|
yield return null;
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator ProjectileAttackOnlyDamagesAfterItsTravelSegmentReachesThePlayer()
|
|
{
|
|
Defense25DMonsterPoolEntry entry = FindEntry("SkeletonArcher");
|
|
Defense25DMonsterCombatProfile profile = entry.CombatProfile;
|
|
Assert.That(profile.Archetype, Is.EqualTo(Defense25DMonsterAttackArchetype.Projectile));
|
|
Assert.That(profile.ProjectileSpeed, Is.GreaterThan(0f));
|
|
Defense25DEnemy enemy = world.Enemies[0];
|
|
enemy.Spawn(Vector2.zero);
|
|
float targetDistance = Mathf.Min(profile.AttackRange * .75f, 2f);
|
|
world.Player.ResetAt(enemy.Actor.Center + Vector2.right * targetDistance);
|
|
Physics.SyncTransforms();
|
|
|
|
Sprite frame = CreateFrame();
|
|
var motion = CreateMotion(entry, frame);
|
|
var projectileEvents = new List<Defense25DMonsterProjectileVisual>();
|
|
var adapter = new Defense25DMonsterCombatAdapter();
|
|
Assert.That(adapter.Bind(entry, enemy.Actor, motion, () => world.Player, out string error,
|
|
setProjectile: projectileEvents.Add), Is.True, error);
|
|
|
|
adapter.Step(.001f, moving: false);
|
|
adapter.Step(profile.WarningSeconds + .001f, moving: false);
|
|
Assert.That(projectileEvents.Any(projectile => projectile.Active), Is.True,
|
|
"The authored projectile starts at the end of the warning.");
|
|
Assert.That(world.Player.DamageEvents, Is.Zero, "Firing is not an immediate hit.");
|
|
|
|
adapter.Step(.02f, moving: false);
|
|
Assert.That(world.Player.DamageEvents, Is.Zero,
|
|
"The projectile must not damage before its swept path reaches the target.");
|
|
|
|
adapter.Step(1f, moving: false);
|
|
Assert.That(world.Player.DamageEvents, Is.EqualTo(1));
|
|
Assert.That(projectileEvents.Last().Active, Is.False);
|
|
adapter.Cancel();
|
|
yield return null;
|
|
}
|
|
|
|
private Defense25DMonsterPoolEntry FindEntry(string id)
|
|
{
|
|
Defense25DMonsterPoolDefinition pool = AssetDatabase.LoadAssetAtPath<Defense25DMonsterPoolDefinition>(PoolPath);
|
|
Assert.That(pool, Is.Not.Null);
|
|
Assert.That(pool.TryValidate(out string error), Is.True, error);
|
|
return pool.Tiers.SelectMany(tier => tier.Entries).Single(entry => entry.EnemyDefinitionId == id);
|
|
}
|
|
|
|
private Defense25DMonsterSourceMotion CreateMotion(Defense25DMonsterPoolEntry entry, Sprite frame)
|
|
{
|
|
return new Defense25DMonsterSourceMotion(
|
|
entry.SourceCharacterId,
|
|
new[] { frame },
|
|
new[] { frame },
|
|
new Dictionary<string, Sprite[]> { [entry.CombatProfile.AttackAction] = new[] { frame } },
|
|
entry.CombatProfile.IsFlying ? new[] { frame } : null,
|
|
.1f,
|
|
new[] { frame });
|
|
}
|
|
|
|
private Sprite CreateFrame()
|
|
{
|
|
Texture2D texture = new Texture2D(1, 1);
|
|
temporaryObjects.Add(texture);
|
|
Sprite frame = Sprite.Create(texture, new Rect(0f, 0f, 1f, 1f), new Vector2(.5f, .5f));
|
|
temporaryObjects.Add(frame);
|
|
return frame;
|
|
}
|
|
|
|
private void SetGameTime(float value)
|
|
{
|
|
typeof(Defense25DComparison).GetProperty(nameof(Defense25DComparison.GameTime))
|
|
.GetSetMethod(true).Invoke(world, new object[] { value });
|
|
}
|
|
}
|
|
}
|