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

263 lines
12 KiB
C#

using System.Collections;
using System.Collections.Generic;
using BumpCombat.Constants;
using BumpCombat.Defense.Comparison;
using NUnit.Framework;
using UnityEditor;
using UnityEngine;
using UnityEngine.TestTools;
namespace BumpCombat.Tests
{
public sealed class Defense25DArtifactDropPlayModeTests
{
private sealed class ScheduleRecord
{
public readonly List<int> SpawnOrder = new();
public readonly List<(string id, float start, float duration)> Reliefs = new();
}
private GameObject worldObject;
private Defense25DComparison world;
private Defense25DArtifactDropConstants settings;
private Defense25DComparisonSettings comparisonSettings;
private Defense25DRunDefinition runDefinition;
private float timeScaleBeforeInteractionTest;
private bool restoreInteractionTestTimeScale;
[SetUp]
public void SetUp()
{
worldObject = new GameObject("Artifact drop test world");
world = worldObject.AddComponent<Defense25DComparison>();
settings = ScriptableObject.CreateInstance<Defense25DArtifactDropConstants>();
}
[TearDown]
public void TearDown()
{
if (restoreInteractionTestTimeScale)
{
Time.timeScale = timeScaleBeforeInteractionTest;
restoreInteractionTestTimeScale = false;
Assert.That(Time.timeScale, Is.EqualTo(timeScaleBeforeInteractionTest));
Debug.Log($"[ArtifactDropInteraction] teardown restored timeScale={Time.timeScale}");
}
if (worldObject != null) Object.DestroyImmediate(worldObject);
if (settings != null) Object.DestroyImmediate(settings);
if (comparisonSettings != null) Object.DestroyImmediate(comparisonSettings);
if (runDefinition != null) Object.DestroyImmediate(runDefinition);
}
[Test]
public void DropsUseFirstActualPortalOpenAndRegisterEachReliefWindowOnlyOnce()
{
Assert.That(Defense25DBattlefieldDefinition.CreateDefault().TryCreateLayout(
8026, out Defense25DRunLayout layout, out string error), Is.True, error);
Defense25DArtifactDrops drops = new();
drops.Bind(world, layout, settings, 419);
List<(string id, float start, float duration)> reliefs = new();
int spawned = 0;
drops.ReliefWindowRequested += (id, start, duration) => reliefs.Add((id, start, duration));
drops.DropSpawned += _ => spawned++;
drops.NotifyPortalOpened(0, 12.5f);
drops.NotifyPortalOpened(1, 20f);
drops.Step(72.49f);
Assert.That(drops.DropStates[0].Status, Is.EqualTo(Defense25DArtifactDropStatus.Scheduled));
drops.Step(72.5f);
Assert.That(drops.DropStates[0].Status, Is.EqualTo(Defense25DArtifactDropStatus.Available));
Assert.That(drops.DropStates[0].AvailableAt, Is.EqualTo(72.5f).Within(.001f));
drops.Step(360f);
drops.Step(360f);
Assert.That(spawned, Is.EqualTo(3));
Assert.That(drops.DropStates.Count, Is.EqualTo(3));
Assert.That(drops.DropStates[1].Status, Is.EqualTo(Defense25DArtifactDropStatus.Available));
Assert.That(drops.DropStates[2].Status, Is.EqualTo(Defense25DArtifactDropStatus.Available));
Assert.That(reliefs, Is.EqualTo(new[]
{
("artifact-drop-0-0", 72.5f, 12f),
("artifact-drop-0-1", 180f, 15f),
("artifact-drop-0-2", 360f, 15f),
}));
}
[Test]
public void LargeDeltaAndOneSecondStepsKeepReliefWindowsAtTheirScheduledTimes()
{
Assert.That(Defense25DBattlefieldDefinition.CreateDefault().TryCreateLayout(
6028, out Defense25DRunLayout layout, out string error), Is.True, error);
ScheduleRecord largeDelta = CaptureSchedule(layout, useOneSecondSteps: false);
ScheduleRecord smallSteps = CaptureSchedule(layout, useOneSecondSteps: true);
Assert.That(largeDelta.SpawnOrder, Is.EqualTo(new[] { 0, 1 }));
Assert.That(largeDelta.Reliefs, Is.EqualTo(new[]
{
("artifact-drop-0-0", 60f, 12f),
("artifact-drop-0-1", 180f, 15f),
}));
Assert.That(smallSteps.SpawnOrder, Is.EqualTo(largeDelta.SpawnOrder));
Assert.That(smallSteps.Reliefs, Is.EqualTo(largeDelta.Reliefs));
Assert.That(largeDelta.Reliefs[0].start, Is.InRange(60f, 72f));
Assert.That(largeDelta.Reliefs[1].start, Is.InRange(180f, 195f));
}
[Test]
public void LateFirstPortalDropIsEmittedAfterEarlierScheduledLaterDrop()
{
Assert.That(Defense25DBattlefieldDefinition.CreateDefault().TryCreateLayout(
6029, out Defense25DRunLayout layout, out string error), Is.True, error);
Defense25DArtifactDrops drops = new();
drops.Bind(world, layout, settings, 419);
ScheduleRecord record = new();
drops.DropSpawned += state => record.SpawnOrder.Add(state.DropId);
drops.ReliefWindowRequested += (id, start, duration) => record.Reliefs.Add((id, start, duration));
drops.NotifyPortalOpened(0, 130f);
drops.Step(200f);
Assert.That(drops.DropStates[0].AvailableAt, Is.EqualTo(190f).Within(.001f));
Assert.That(drops.DropStates[1].AvailableAt, Is.EqualTo(180f).Within(.001f));
Assert.That(record.SpawnOrder, Is.EqualTo(new[] { 1, 0 }));
Assert.That(record.Reliefs, Is.EqualTo(new[]
{
("artifact-drop-0-1", 180f, 15f),
("artifact-drop-0-0", 190f, 12f),
}));
}
[UnityTest]
public IEnumerator InteractedDropSnapshotsCandidatesAndClaimsOnlyTheChosenArtifactOnce()
{
timeScaleBeforeInteractionTest = Time.timeScale;
restoreInteractionTestTimeScale = true;
Debug.Log($"[ArtifactDropInteraction] start frame={Time.frameCount} timeScale={timeScaleBeforeInteractionTest}");
Time.timeScale = 1f;
comparisonSettings = Object.Instantiate(AssetDatabase.LoadAssetAtPath<Defense25DComparisonSettings>(
"Assets/_Project/Constants/Defense25DComparison/ComparisonSettings.asset"));
Assert.That(comparisonSettings, Is.Not.Null);
runDefinition = Object.Instantiate(AssetDatabase.LoadAssetAtPath<Defense25DRunDefinition>(
"Assets/_Project/Constants/Defense25DComparison/LongDefenseRun.asset"));
Assert.That(runDefinition, Is.Not.Null);
runDefinition.PreparationSeconds = 0f;
world.InitializeLongRun(comparisonSettings, runDefinition, layoutSeed: 1741);
world.AutoSpawn = false;
world.ReadInput = false;
Debug.Log($"[ArtifactDropInteraction] before null frame frame={Time.frameCount} timeScale={Time.timeScale}");
yield return null;
Debug.Log($"[ArtifactDropInteraction] before fixed update frame={Time.frameCount} timeScale={Time.timeScale}");
yield return new WaitForFixedUpdate();
Debug.Log($"[ArtifactDropInteraction] after fixed update frame={Time.frameCount} timeScale={Time.timeScale}");
Assert.That(world.RunPhase, Is.EqualTo(Defense25DRunPhase.Running));
Defense25DArtifactDrops drops = new();
drops.Bind(world, world.RunLayout, settings, 508);
world.Player.ResetAt(drops.DropStates[0].Position);
drops.NotifyPortalOpened(0, 0f);
drops.Step(60f);
Assert.That(drops.TryInteract(0), Is.True);
IReadOnlyList<BumpCombat.Player.ActiveArtifactDefinition> snapshot = drops.CurrentCandidates;
int generation = drops.RunGeneration;
int token = drops.CurrentChoiceToken;
Assert.That(snapshot, Has.Count.EqualTo(2));
Assert.That(snapshot[0].Effect, Is.Not.EqualTo(snapshot[1].Effect));
Assert.That(drops.TryInteract(0), Is.False);
Assert.That(drops.TryChoose(0, generation + 1, token), Is.False);
Assert.That(drops.TryChoose(0, generation, token), Is.True);
Assert.That(drops.DropStates[0].Status, Is.EqualTo(Defense25DArtifactDropStatus.Claimed));
Assert.That(world.Loadout.OwnedArtifactCount, Is.EqualTo(1));
Assert.That(drops.TryChoose(1, generation, token), Is.False);
Assert.That(drops.TryInteract(0), Is.False);
}
[Test]
public void SeededDropPositionsUseTheActualHomeAndStayOnClearRouteFreeGround()
{
Assert.That(Defense25DBattlefieldDefinition.CreateDefault().TryCreateLayout(
2718, out Defense25DRunLayout layout, out string error), Is.True, error);
Defense25DArtifactDrops first = new();
Defense25DArtifactDrops replay = new();
first.Bind(world, layout, settings, 9501);
replay.Bind(world, layout, settings, 9501);
Assert.That(first.DropStates.Count, Is.EqualTo(3));
for (int i = 0; i < first.DropStates.Count; i++)
{
Vector2 position = first.DropStates[i].Position;
Assert.That(position, Is.EqualTo(replay.DropStates[i].Position));
Assert.That(layout.Bounds.Contains(position), Is.True);
Assert.That(position.x - layout.Bounds.xMin, Is.GreaterThan(settings.DropFootprintRadius));
Assert.That(layout.Bounds.xMax - position.x, Is.GreaterThan(settings.DropFootprintRadius));
Assert.That(position.y - layout.Bounds.yMin, Is.GreaterThan(settings.DropFootprintRadius));
Assert.That(layout.Bounds.yMax - position.y, Is.GreaterThan(settings.DropFootprintRadius));
Assert.That(DistanceToRoutes(position, layout.Routes), Is.GreaterThan(.75f));
Assert.That(DistanceToPads(position, layout.PadPositions), Is.GreaterThan(1.25f));
Assert.That(DistanceToPads(position, layout.PortalPositions), Is.GreaterThan(1.25f));
float homeDistance = Vector2.Distance(position, layout.HomePosition);
if (i == 0)
Assert.That(homeDistance, Is.InRange(2f, 4f));
else
Assert.That(homeDistance, Is.InRange(18f, 26f));
}
Assert.That(Vector2.Distance(first.DropStates[1].Position, first.DropStates[2].Position),
Is.GreaterThanOrEqualTo(12f));
}
private static float DistanceToRoutes(Vector2 position, IReadOnlyList<IReadOnlyList<Vector2>> routes)
{
float nearest = float.PositiveInfinity;
foreach (IReadOnlyList<Vector2> route in routes)
for (int i = 0; i < route.Count - 1; i++)
nearest = Mathf.Min(nearest, DistanceToSegment(position, route[i], route[i + 1]));
return nearest;
}
private ScheduleRecord CaptureSchedule(Defense25DRunLayout layout, bool useOneSecondSteps)
{
Defense25DArtifactDrops drops = new();
drops.Bind(world, layout, settings, 7401);
ScheduleRecord record = new();
drops.DropSpawned += state => record.SpawnOrder.Add(state.DropId);
drops.ReliefWindowRequested += (id, start, duration) => record.Reliefs.Add((id, start, duration));
drops.NotifyPortalOpened(0, 0f);
drops.Step(59f);
if (useOneSecondSteps)
{
for (int second = 60; second <= 200; second++)
drops.Step(second);
}
else
{
drops.Step(200f);
}
return record;
}
private static float DistanceToPads(Vector2 position, IReadOnlyList<Vector2> pads)
{
float nearest = float.PositiveInfinity;
foreach (Vector2 pad in pads)
nearest = Mathf.Min(nearest, Vector2.Distance(position, pad));
return nearest;
}
private static float DistanceToSegment(Vector2 point, Vector2 start, Vector2 end)
{
Vector2 segment = end - start;
float t = Mathf.Clamp01(Vector2.Dot(point - start, segment) / segment.sqrMagnitude);
return Vector2.Distance(point, start + segment * t);
}
}
}