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 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(); settings = ScriptableObject.CreateInstance(); } [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( "Assets/_Project/Constants/Defense25DComparison/ComparisonSettings.asset")); Assert.That(comparisonSettings, Is.Not.Null); runDefinition = Object.Instantiate(AssetDatabase.LoadAssetAtPath( "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 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> routes) { float nearest = float.PositiveInfinity; foreach (IReadOnlyList 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 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); } } }