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

228 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"),
};
}
}
}