Files
tiny_tackle_heroes/Assets/_Project/Tests/PlayMode/Defense25DMonsterCombatAdapterPlayModeTests.cs
T
2026-09-28 11:07:17 +09:00

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 });
}
}
}