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 temporaryObjects = new(); private float previousTimeScale; [UnitySetUp] public IEnumerator SetUp() { previousTimeScale = Time.timeScale; Time.timeScale = 1f; settings = Object.Instantiate(AssetDatabase.LoadAssetAtPath(SettingsPath)); Assert.That(settings, Is.Not.Null); world = new GameObject("Monster combat adapter under test").AddComponent(); 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(); 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(); 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(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 { [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 }); } } }