2.5d test

This commit is contained in:
2026-09-27 21:08:05 +09:00
parent 84dd12414a
commit abd2da2057
201 changed files with 24297 additions and 295 deletions
@@ -0,0 +1,205 @@
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 = world.Enemies[0].gameObject;
RaiseHit(target, HitSide.Front, 31.9f);
AssertPopup(0, "31", Color.white);
world.ResetRun();
target = world.Enemies[0].gameObject;
RaiseHit(target, HitSide.Side, 22.8f);
AssertPopup(0, "SIDE ×1.2 22", new Color(0.65f, 1f, 0.3f));
world.ResetRun();
target = world.Enemies[0].gameObject;
RaiseHit(target, HitSide.Back, 9.8f);
AssertPopup(0, "BACK ×1.5 9", new Color(1f, 0.75f, 0.2f));
world.ResetRun();
target = world.Enemies[0].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 = world.Enemies[0].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 = world.Enemies[0].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()
{
GameObject worldTarget = world.Enemies[0].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.");
RaiseHit(worldTarget, HitSide.Front, 10f);
Assert.That(world.ActiveCombatDamagePopupCount, Is.EqualTo(1));
world.ResetRun();
Assert.That(world.ActiveCombatDamagePopupCount, Is.Zero);
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(world.Enemies[0].gameObject, 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 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"),
};
}
}
}