228 lines
11 KiB
C#
228 lines
11 KiB
C#
using System;
|
||
using System.Collections;
|
||
using System.Linq;
|
||
using System.Reflection;
|
||
using BumpCombat.Combat;
|
||
using BumpCombat.Constants;
|
||
using BumpCombat.Defense.Comparison;
|
||
using BumpCombat.Player;
|
||
using NUnit.Framework;
|
||
using UnityEditor;
|
||
using UnityEngine;
|
||
using UnityEngine.TestTools;
|
||
using Object = UnityEngine.Object;
|
||
|
||
namespace BumpCombat.Tests
|
||
{
|
||
public sealed class Defense25DCombatPopupPlayModeTests
|
||
{
|
||
private const string SettingsPath =
|
||
"Assets/_Project/Constants/Defense25DComparison/ComparisonSettings.asset";
|
||
private Defense25DComparison world;
|
||
private Defense25DComparisonSettings settings;
|
||
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);
|
||
settings.Enemy = AssetDatabase.LoadAssetAtPath<BumpCombat.Enemies.EnemyDefinition>(
|
||
"Assets/_Project/Constants/Enemies/Slime.asset");
|
||
settings.EnemyArt = ReadCharacter();
|
||
world = new GameObject("2.5D combat popup test world")
|
||
.AddComponent<Defense25DComparison>();
|
||
world.Initialize(settings);
|
||
world.AutoSpawn = false;
|
||
world.ReadInput = false;
|
||
yield return null;
|
||
}
|
||
|
||
[UnityTearDown]
|
||
public IEnumerator TearDown()
|
||
{
|
||
if (world != null) Object.Destroy(world.gameObject);
|
||
if (settings != null) Object.Destroy(settings);
|
||
Time.timeScale = previousTimeScale;
|
||
yield return null;
|
||
}
|
||
|
||
[UnityTest]
|
||
public IEnumerator ValidHitPopups_MatchNormalSideBackAndArtifactContracts()
|
||
{
|
||
GameObject target = SpawnPopupTarget().gameObject;
|
||
RaiseHit(target, HitSide.Front, 31.9f);
|
||
AssertPopup(0, "31", Color.white);
|
||
|
||
world.ResetRun();
|
||
target = SpawnPopupTarget().gameObject;
|
||
RaiseHit(target, HitSide.Side, 22.8f);
|
||
AssertPopup(0, "SIDE ×1.2 22", new Color(0.65f, 1f, 0.3f));
|
||
|
||
world.ResetRun();
|
||
target = SpawnPopupTarget().gameObject;
|
||
RaiseHit(target, HitSide.Back, 9.8f);
|
||
AssertPopup(0, "BACK ×1.5 9", new Color(1f, 0.75f, 0.2f));
|
||
|
||
world.ResetRun();
|
||
target = SpawnPopupTarget().gameObject;
|
||
CombatEvents.RaiseValidHit(new CombatHitResult(
|
||
world.Player.gameObject, target, true, HitSide.Front, 13f,
|
||
Vector2.right, 0f, Vector2.zero, launchSucceeded: true));
|
||
AssertPopup(0, "LAUNCH 13", new Color(0.95f, 0.55f, 1f));
|
||
|
||
world.ResetRun();
|
||
target = SpawnPopupTarget().gameObject;
|
||
ActiveArtifactDefinition dash = world.Loadout.FindArtifact(ActiveArtifactEffect.Dash);
|
||
Assert.That(dash, Is.Not.Null);
|
||
CombatEvents.RaiseValidHit(new CombatHitResult(
|
||
world.Player.gameObject, target, true, HitSide.Front, 15f, Vector2.right,
|
||
0f, Vector2.zero, sourceId: dash.ArtifactId, isArtifactHit: true,
|
||
artifactEffect: ActiveArtifactEffect.Dash));
|
||
AssertPopup(0, $"{dash.DisplayName} 15",
|
||
ActiveArtifactDefinition.GetArtifactPaletteColor(dash.ArtifactColor));
|
||
|
||
world.ResetRun();
|
||
target = SpawnPopupTarget().gameObject;
|
||
ActiveArtifactDefinition cyclone = world.Loadout.FindArtifact(ActiveArtifactEffect.Cyclone);
|
||
Assert.That(cyclone, Is.Not.Null);
|
||
CombatEvents.RaiseValidHit(CombatHitResult.ForArtifactContact(
|
||
world.Player.gameObject, target, ActiveArtifactEffect.Cyclone, false,
|
||
HitSide.Front, Vector2.right, Vector2.zero, cyclone.DamageTag, cyclone.ArtifactId));
|
||
AssertPopup(0, $"{cyclone.DisplayName} 0",
|
||
ActiveArtifactDefinition.GetArtifactPaletteColor(cyclone.ArtifactColor));
|
||
yield return null;
|
||
}
|
||
|
||
[UnityTest]
|
||
public IEnumerator PopupEvents_AreWorldScopedResettableAndRemovedOnDisable()
|
||
{
|
||
Defense25DEnemy unspawnedEnemy = world.Enemies[0];
|
||
Assert.That(unspawnedEnemy.IsAvailable, Is.False);
|
||
Assert.That(unspawnedEnemy.gameObject.activeInHierarchy, Is.False);
|
||
Assert.That(unspawnedEnemy.gameObject.GetComponentInParent<Defense25DEnemy>(), Is.Null,
|
||
"The default parent lookup excludes the inactive, unsupplied enemy fixture.");
|
||
GameObject worldTarget = unspawnedEnemy.gameObject;
|
||
GameObject unrelatedTarget = new("Unrelated popup target");
|
||
GameObject unrelatedAttacker = new("Unrelated popup attacker");
|
||
try
|
||
{
|
||
RaiseHit(unrelatedTarget, HitSide.Front, 10f);
|
||
Assert.That(world.ActiveCombatDamagePopupCount, Is.Zero);
|
||
CombatEvents.RaiseValidHit(new CombatHitResult(
|
||
unrelatedAttacker, worldTarget, false, HitSide.Front, 10f,
|
||
Vector2.right, 0f, Vector2.zero));
|
||
Assert.That(world.ActiveCombatDamagePopupCount, Is.Zero,
|
||
"Hits from another attacker against this world's enemy are ignored.");
|
||
|
||
Assert.That(world.ActiveCombatDamagePopupCount, Is.Zero,
|
||
"An event targeting an inactive, unsupplied enemy cannot create a popup.");
|
||
Defense25DEnemy activeEnemy = SpawnPopupTarget();
|
||
worldTarget = activeEnemy.gameObject;
|
||
Assert.That(worldTarget.GetComponentInParent<Defense25DEnemy>(), Is.SameAs(activeEnemy));
|
||
RaiseHit(worldTarget, HitSide.Front, 10f);
|
||
Assert.That(world.ActiveCombatDamagePopupCount, Is.EqualTo(1),
|
||
"A valid hit against an active, supplied enemy must be received by this world's popup handler.");
|
||
world.ResetRun();
|
||
Assert.That(world.ActiveCombatDamagePopupCount, Is.Zero);
|
||
activeEnemy = SpawnPopupTarget();
|
||
worldTarget = activeEnemy.gameObject;
|
||
|
||
world.enabled = false;
|
||
RaiseHit(worldTarget, HitSide.Front, 10f);
|
||
world.NotifyPlayerDamagePopup(10f, Vector3.zero);
|
||
world.RecordTowerDamagePopup(10f, Color.cyan, Vector3.one);
|
||
world.enabled = true;
|
||
Assert.That(world.ActiveCombatDamagePopupCount, Is.Zero,
|
||
"A disabled comparison must reject event and direct popup additions.");
|
||
RaiseHit(worldTarget, HitSide.Front, 10f);
|
||
Assert.That(world.ActiveCombatDamagePopupCount, Is.EqualTo(1),
|
||
"Re-enabling the comparison must restore one event subscription.");
|
||
}
|
||
finally
|
||
{
|
||
Object.Destroy(unrelatedTarget);
|
||
Object.Destroy(unrelatedAttacker);
|
||
}
|
||
yield return null;
|
||
}
|
||
|
||
[UnityTest]
|
||
public IEnumerator PlayerAndTowerDamagePopups_ShareTwelveReusableUnscaledSlots()
|
||
{
|
||
for (int i = 0; i < 13; i++)
|
||
world.NotifyPlayerDamagePopup(12.9f, Vector3.zero);
|
||
Assert.That(world.ActiveCombatDamagePopupCount, Is.EqualTo(12));
|
||
AssertPopup(0, "-12", new Color(244f / 255f, 117f / 255f, 112f / 255f));
|
||
|
||
world.RecordTowerDamagePopup(8.9f, Color.cyan, Vector3.one);
|
||
Assert.That(world.ActiveTowerDamagePopupCount, Is.Zero,
|
||
"An active-set overflow must be rejected for towers as well.");
|
||
|
||
Time.timeScale = 0f;
|
||
yield return new WaitForSecondsRealtime(0.65f);
|
||
Assert.That(world.ActiveCombatDamagePopupCount, Is.Zero,
|
||
"Popup expiry uses unscaled time, including while paused.");
|
||
|
||
world.NotifyPlayerDamagePopup(14.5f, Vector3.zero);
|
||
world.RecordTowerDamagePopup(8.9f, Color.cyan, Vector3.one);
|
||
Assert.That(world.ActiveCombatDamagePopupCount, Is.EqualTo(2));
|
||
Assert.That(world.ActiveTowerDamagePopupCount, Is.EqualTo(1));
|
||
AssertPopup(0, "-14", new Color(244f / 255f, 117f / 255f, 112f / 255f));
|
||
AssertPopup(1, "-8", Color.cyan);
|
||
world.NotifyPlayerDamagePopup(0f, Vector3.zero);
|
||
Assert.That(world.ActiveCombatDamagePopupCount, Is.EqualTo(2),
|
||
"The public player damage API ignores zero actual HP loss.");
|
||
}
|
||
|
||
private void RaiseHit(GameObject target, HitSide side, float damage)
|
||
{
|
||
CombatEvents.RaiseValidHit(new CombatHitResult(
|
||
world.Player.gameObject, target, false, side, damage, Vector2.right,
|
||
0f, Vector2.zero, isOffsetHit: false));
|
||
}
|
||
|
||
private Defense25DEnemy SpawnPopupTarget()
|
||
{
|
||
Defense25DEnemy enemy = world.SpawnNextEnemy();
|
||
Assert.That(enemy, Is.Not.Null, "The popup fixture needs a supplied enemy from the world factory.");
|
||
Assert.That(enemy.IsAvailable, Is.True);
|
||
Assert.That(enemy.gameObject.GetComponentInParent<Defense25DEnemy>(), Is.SameAs(enemy));
|
||
return enemy;
|
||
}
|
||
|
||
private void AssertPopup(int index, string text, Color color)
|
||
{
|
||
FieldInfo popupArrayField = typeof(Defense25DComparison).GetField(
|
||
"combatDamagePopups", BindingFlags.Instance | BindingFlags.NonPublic);
|
||
Assert.That(popupArrayField, Is.Not.Null);
|
||
Array popupArray = (Array)popupArrayField.GetValue(world);
|
||
object popup = popupArray.GetValue(index);
|
||
Type popupType = popup.GetType();
|
||
Assert.That((string)popupType.GetField("Text").GetValue(popup), Is.EqualTo(text));
|
||
Color actualColor = (Color)popupType.GetField("Color").GetValue(popup);
|
||
Assert.That(actualColor.r, Is.EqualTo(color.r).Within(0.002f));
|
||
Assert.That(actualColor.g, Is.EqualTo(color.g).Within(0.002f));
|
||
Assert.That(actualColor.b, Is.EqualTo(color.b).Within(0.002f));
|
||
}
|
||
|
||
private static ComparisonSpriteFrames ReadCharacter()
|
||
{
|
||
Sprite[] Read(string suffix) => AssetDatabase.LoadAllAssetsAtPath(
|
||
$"Assets/_Project/Art/Characters/Slime/Slime_{suffix}.png")
|
||
.OfType<Sprite>()
|
||
.OrderBy(sprite => int.TryParse(
|
||
sprite.name[(sprite.name.LastIndexOf('_') + 1)..], out int frame) ? frame : 0)
|
||
.ToArray();
|
||
return new ComparisonSpriteFrames
|
||
{
|
||
Idle = Read("Idle"), Walk = Read("Walk"), Attack = Read("Attack"),
|
||
Hurt = Read("Hurt"), Death = Read("Death"),
|
||
};
|
||
}
|
||
}
|
||
}
|