775 lines
31 KiB
C#
775 lines
31 KiB
C#
using System.Collections;
|
|
using System.Reflection;
|
|
using BumpCombat.Constants;
|
|
using BumpCombat.Core;
|
|
using BumpCombat.Enemies;
|
|
using BumpCombat.Player;
|
|
using BumpCombat.Progression;
|
|
using BumpCombat.Spawning;
|
|
using BumpCombat.Stage;
|
|
using BumpCombat.UI;
|
|
using NUnit.Framework;
|
|
using UnityEngine;
|
|
using UnityEngine.SceneManagement;
|
|
using UnityEngine.TestTools;
|
|
using UnityEngine.UI;
|
|
|
|
namespace BumpCombat.PlayModeTests
|
|
{
|
|
public sealed class FiniteArenaPlayModeTests
|
|
{
|
|
[TearDown]
|
|
public void Cleanup()
|
|
{
|
|
ArtifactRewardController.GrantCatalogForTests = false;
|
|
Time.timeScale = 1f;
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator PauseFreezesCameraFollowUntilPlayResumes()
|
|
{
|
|
yield return LoadAndStartRun();
|
|
RunManager runManager = RunManager.Instance;
|
|
|
|
PlayerController player = Object.FindAnyObjectByType<PlayerController>();
|
|
ArenaCameraFollow follow = Object.FindAnyObjectByType<ArenaCameraFollow>();
|
|
ArenaBounds bounds = Object.FindAnyObjectByType<ArenaBounds>();
|
|
Assert.That(player, Is.Not.Null);
|
|
Assert.That(follow, Is.Not.Null);
|
|
Assert.That(bounds, Is.Not.Null);
|
|
|
|
player.TryReposition(bounds.SharedActorClampHalfExtents);
|
|
yield return null;
|
|
Assert.That(runManager.TryOpenPause(), Is.True);
|
|
Vector2 pausedCenter = follow.CurrentCenter;
|
|
yield return new WaitForSecondsRealtime(0.3f);
|
|
Assert.That(follow.CurrentCenter, Is.EqualTo(pausedCenter));
|
|
|
|
Assert.That(runManager.ClosePause(), Is.True);
|
|
yield return new WaitForSecondsRealtime(0.8f);
|
|
Assert.That(follow.CurrentCenter.x, Is.GreaterThan(0.1f));
|
|
Assert.That(follow.CurrentCenter.y, Is.GreaterThan(0.1f));
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator RangedEnemyAtCornerApproachesBeforeAttackStart()
|
|
{
|
|
yield return LoadAndStartRun();
|
|
|
|
SpawnDirector director = Object.FindAnyObjectByType<SpawnDirector>();
|
|
director.enabled = false;
|
|
PlayerController player = Object.FindAnyObjectByType<PlayerController>();
|
|
PlayerHealth playerHealth = Object.FindAnyObjectByType<PlayerHealth>();
|
|
Camera camera = Camera.main;
|
|
ArenaCameraFollow follow = Object.FindAnyObjectByType<ArenaCameraFollow>();
|
|
ArenaBounds bounds = Object.FindAnyObjectByType<ArenaBounds>();
|
|
Assert.That(playerHealth, Is.Not.Null);
|
|
Assert.That(camera, Is.Not.Null);
|
|
Assert.That(follow, Is.Not.Null);
|
|
Assert.That(bounds, Is.Not.Null);
|
|
playerHealth.GrantInvulnerability(10f);
|
|
|
|
yield return DestroyActiveEnemies();
|
|
camera.aspect = 16f / 9f;
|
|
EnemyController rangedPrefab = FindRangedPrefab(director);
|
|
Assert.That(rangedPrefab, Is.Not.Null, "A ranged prefab is required for this regression.");
|
|
Vector2 halfExtents = bounds.SharedActorClampHalfExtents;
|
|
Vector2[] edgePositions =
|
|
{
|
|
new(halfExtents.x, 0f),
|
|
new(-halfExtents.x, 0f),
|
|
new(0f, halfExtents.y),
|
|
new(0f, -halfExtents.y),
|
|
};
|
|
for (int edgeIndex = 0; edgeIndex < edgePositions.Length; edgeIndex++)
|
|
{
|
|
Vector2 edgePosition = edgePositions[edgeIndex];
|
|
player.TryReposition(edgePosition);
|
|
// Let the real follow path settle before the enemy is created
|
|
// so the visible gate is measured from that edge camera.
|
|
yield return new WaitForSecondsRealtime(0.9f);
|
|
|
|
EnemyController ranged = Object.Instantiate(
|
|
rangedPrefab,
|
|
edgePosition,
|
|
Quaternion.identity);
|
|
yield return null;
|
|
float gateDeadline = Time.realtimeSinceStartup + 3f;
|
|
while (!ranged.IsRangedAttackStartAllowed
|
|
&& Time.realtimeSinceStartup < gateDeadline)
|
|
{
|
|
yield return new WaitForFixedUpdate();
|
|
}
|
|
|
|
Vector2 position = ranged.GetComponent<Rigidbody2D>().position;
|
|
Assert.That(
|
|
ranged.IsRangedAttackStartAllowed,
|
|
Is.True,
|
|
$"Ranged enemy remained outside its attack gate at edge={edgeIndex}, "
|
|
+ $"position={position}; camera={camera.transform.position}.");
|
|
Vector2 rangedGateHalfExtents = bounds.GetReachableHalfExtents(0.75f);
|
|
Assert.That(
|
|
Mathf.Abs(position.x),
|
|
Is.LessThanOrEqualTo(rangedGateHalfExtents.x + 0.001f));
|
|
Assert.That(
|
|
Mathf.Abs(position.y),
|
|
Is.LessThanOrEqualTo(rangedGateHalfExtents.y + 0.001f));
|
|
|
|
float attackDeadline = Time.realtimeSinceStartup + 1.5f;
|
|
while (ranged.State != EnemyState.Warning
|
|
&& ranged.State != EnemyState.Active
|
|
&& Time.realtimeSinceStartup < attackDeadline)
|
|
{
|
|
yield return null;
|
|
}
|
|
|
|
Assert.That(
|
|
ranged.State == EnemyState.Warning
|
|
|| ranged.State == EnemyState.Active,
|
|
Is.True,
|
|
$"Ranged enemy entered the interior gate but did not start an attack "
|
|
+ $"(edge={edgeIndex}, state={ranged.State}, position={position}).");
|
|
Object.Destroy(ranged.gameObject);
|
|
yield return null;
|
|
}
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator LargeHorizontalMeleeAtFiniteArenaEdges_ReachesActiveAttack()
|
|
{
|
|
yield return LoadAndStartRun();
|
|
|
|
SpawnDirector director = Object.FindAnyObjectByType<SpawnDirector>();
|
|
director.enabled = false;
|
|
PlayerController player = Object.FindAnyObjectByType<PlayerController>();
|
|
PlayerHealth playerHealth = Object.FindAnyObjectByType<PlayerHealth>();
|
|
Rigidbody2D playerBody = player?.GetComponent<Rigidbody2D>();
|
|
ArenaBounds bounds = Object.FindAnyObjectByType<ArenaBounds>();
|
|
Assert.That(player, Is.Not.Null);
|
|
Assert.That(playerHealth, Is.Not.Null);
|
|
Assert.That(playerBody, Is.Not.Null);
|
|
Assert.That(bounds, Is.Not.Null);
|
|
playerHealth.GrantInvulnerability(60f);
|
|
|
|
yield return DestroyActiveEnemies();
|
|
|
|
Vector2 halfExtents = bounds.SharedActorClampHalfExtents;
|
|
Vector2[] playerPositions =
|
|
{
|
|
new(0f, -halfExtents.y),
|
|
new(0f, halfExtents.y),
|
|
new(-halfExtents.x, 0f),
|
|
new(halfExtents.x, 0f),
|
|
new(-halfExtents.x, -halfExtents.y),
|
|
new(halfExtents.x, -halfExtents.y),
|
|
new(-halfExtents.x, halfExtents.y),
|
|
new(halfExtents.x, halfExtents.y),
|
|
};
|
|
EnemyKind[] kinds =
|
|
{
|
|
EnemyKind.ArmoredSkeleton,
|
|
EnemyKind.GreatswordSkeleton,
|
|
};
|
|
RunTimedEvent[] roles =
|
|
{
|
|
RunTimedEvent.Elite,
|
|
RunTimedEvent.MidBoss,
|
|
};
|
|
float[] scaleMultipliers = { 1.25f, 1.6f };
|
|
|
|
for (int kindIndex = 0; kindIndex < kinds.Length; kindIndex++)
|
|
{
|
|
EnemyController prefab = FindCatalogPrefab(
|
|
director,
|
|
kinds[kindIndex]);
|
|
Assert.That(
|
|
prefab,
|
|
Is.Not.Null,
|
|
$"Missing edge melee prefab for {kinds[kindIndex]}.");
|
|
|
|
for (int positionIndex = 0;
|
|
positionIndex < playerPositions.Length;
|
|
positionIndex++)
|
|
{
|
|
Vector2 playerPosition = playerPositions[positionIndex];
|
|
Assert.That(player.TryReposition(playerPosition), Is.True);
|
|
yield return new WaitForFixedUpdate();
|
|
playerHealth.GrantInvulnerability(10f);
|
|
|
|
Vector2 inward = new Vector2(
|
|
Mathf.Sign(-playerPosition.x),
|
|
Mathf.Sign(-playerPosition.y));
|
|
if (inward.sqrMagnitude <= 0.0001f)
|
|
{
|
|
inward = Vector2.up;
|
|
}
|
|
inward.Normalize();
|
|
EnemyController enemy = Object.Instantiate(
|
|
prefab,
|
|
playerPosition + inward * 3f,
|
|
Quaternion.identity);
|
|
enemy.ConfigureRunEventEnemy(
|
|
roles[kindIndex],
|
|
CreateEdgeEventTuning(
|
|
kinds[kindIndex],
|
|
scaleMultipliers[kindIndex]));
|
|
yield return null;
|
|
|
|
float warningDeadline = Time.realtimeSinceStartup + 5f;
|
|
while (enemy.State != EnemyState.Warning
|
|
&& enemy.State != EnemyState.Active
|
|
&& Time.realtimeSinceStartup < warningDeadline)
|
|
{
|
|
yield return new WaitForFixedUpdate();
|
|
}
|
|
|
|
Assert.That(
|
|
enemy.State == EnemyState.Warning
|
|
|| enemy.State == EnemyState.Active,
|
|
Is.True,
|
|
$"{kinds[kindIndex]} stalled before attack at position "
|
|
+ $"{playerPosition}; enemy={enemy.GroundAnchorPosition}, "
|
|
+ $"range={enemy.AttackRange}.");
|
|
|
|
float activeDeadline = Time.realtimeSinceStartup + 2f;
|
|
while (enemy.State != EnemyState.Active
|
|
&& Time.realtimeSinceStartup < activeDeadline)
|
|
{
|
|
yield return new WaitForFixedUpdate();
|
|
}
|
|
|
|
Assert.That(
|
|
enemy.State,
|
|
Is.EqualTo(EnemyState.Active),
|
|
$"{kinds[kindIndex]} did not reach its active hit window "
|
|
+ $"at position {playerPosition}.");
|
|
Assert.That(
|
|
Vector2.Distance(playerBody.position, playerPosition),
|
|
Is.LessThan(0.02f),
|
|
"The stationary edge target moved before the melee hit window.");
|
|
EnemyAttack attack = enemy.GetComponent<EnemyAttack>();
|
|
Assert.That(attack, Is.Not.Null);
|
|
Assert.That(attack.IsContactWindowActive(0f), Is.True);
|
|
|
|
Object.Destroy(enemy.gameObject);
|
|
yield return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator SpawnSampling_StaysReachableSafeAndOutsideVisibleView()
|
|
{
|
|
yield return LoadAndStartRun();
|
|
SpawnDirector director = Object.FindAnyObjectByType<SpawnDirector>();
|
|
director.enabled = false;
|
|
PlayerController player = Object.FindAnyObjectByType<PlayerController>();
|
|
ArenaBounds bounds = Object.FindAnyObjectByType<ArenaBounds>();
|
|
Camera camera = Camera.main;
|
|
Assert.That(bounds, Is.Not.Null);
|
|
Assert.That(camera, Is.Not.Null);
|
|
camera.aspect = 16f / 9f;
|
|
|
|
Vector2 halfExtents = bounds.SharedActorClampHalfExtents;
|
|
Vector2[] corners =
|
|
{
|
|
Vector2.zero,
|
|
new Vector2(-halfExtents.x, halfExtents.y),
|
|
new Vector2(halfExtents.x, halfExtents.y),
|
|
new Vector2(halfExtents.x, -halfExtents.y),
|
|
new Vector2(-halfExtents.x, -halfExtents.y),
|
|
};
|
|
for (int cornerIndex = 0; cornerIndex < corners.Length; cornerIndex++)
|
|
{
|
|
Assert.That(player.TryReposition(corners[cornerIndex]), Is.True);
|
|
yield return new WaitForSecondsRealtime(0.35f);
|
|
Bounds expandedVisible = bounds.GetCameraVisibleBounds(camera);
|
|
expandedVisible.Expand(1f);
|
|
for (int sample = 0; sample < 16; sample++)
|
|
{
|
|
Vector2 position = director.GetSpawnPositionForTests();
|
|
Assert.That(bounds.IsInsideReachableArena(position, 0.25f), Is.True);
|
|
Assert.That(
|
|
Vector2.Distance(position, corners[cornerIndex]),
|
|
Is.GreaterThanOrEqualTo(4f));
|
|
bool insideExpandedVisible = position.x >= expandedVisible.min.x
|
|
&& position.x <= expandedVisible.max.x
|
|
&& position.y >= expandedVisible.min.y
|
|
&& position.y <= expandedVisible.max.y;
|
|
Assert.That(insideExpandedVisible, Is.False);
|
|
}
|
|
}
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator RoomScaleComparison_UsesMatchedCameraAndMinimapForOriginalAndExpanded()
|
|
{
|
|
const int referenceSeed = 20260922;
|
|
RunConstants constants = GameplayConstants.Current.Run;
|
|
RoomScaleComparisonMode previousMode = constants.Stage1FirstCombatRoomScaleComparison;
|
|
UnityEngine.Random.State previousRandomState = UnityEngine.Random.state;
|
|
float previousTimeScale = Time.timeScale;
|
|
bool previousProductionMode = RunManager.ForceProductionModeForTests;
|
|
string captureDirectory = System.Environment.GetEnvironmentVariable(
|
|
"BUMPCOMBAT_COMPARISON_CAPTURE_DIRECTORY");
|
|
|
|
try
|
|
{
|
|
constants.Stage1FirstCombatRoomScaleComparison = RoomScaleComparisonMode.Original;
|
|
yield return LoadAndStartRun();
|
|
|
|
StageManager stageManager = Object.FindAnyObjectByType<StageManager>();
|
|
Camera camera = Camera.main;
|
|
Assert.That(stageManager, Is.Not.Null);
|
|
Assert.That(camera, Is.Not.Null);
|
|
|
|
RoomScaleComparisonMode[] modes =
|
|
{
|
|
RoomScaleComparisonMode.Original,
|
|
RoomScaleComparisonMode.Expanded,
|
|
};
|
|
for (int modeIndex = 0; modeIndex < modes.Length; modeIndex++)
|
|
{
|
|
RoomScaleComparisonMode mode = modes[modeIndex];
|
|
constants.Stage1FirstCombatRoomScaleComparison = mode;
|
|
stageManager.StartRun(referenceSeed);
|
|
yield return new WaitForSecondsRealtime(0.4f);
|
|
|
|
StageNode firstCombat = FindFirstCombat(stageManager.CurrentPlan);
|
|
Assert.That(firstCombat, Is.Not.Null);
|
|
Assert.That(firstCombat.PresetId,
|
|
Is.EqualTo(mode == RoomScaleComparisonMode.Expanded
|
|
? LevelPresetCatalog.S1AComparisonExpandedPresetId
|
|
: "GRAVE_S1_COMBAT_A"));
|
|
|
|
StageDirection entryDirection = GetEntryDirection(
|
|
stageManager.CurrentPlan.StartNode,
|
|
firstCombat);
|
|
|
|
// Reset immediately before the same room activation so the
|
|
// encounter's UnityEngine.Random group rolls match A/B.
|
|
UnityEngine.Random.InitState(referenceSeed);
|
|
stageManager.RequestZoneTransition(firstCombat.Id, entryDirection);
|
|
float deadline = Time.realtimeSinceStartup + 4f;
|
|
while (stageManager.CurrentZone == null
|
|
|| stageManager.CurrentZone.NodeId != firstCombat.Id
|
|
|| stageManager.IsTransitioning)
|
|
{
|
|
Assert.That(Time.realtimeSinceStartup, Is.LessThan(deadline),
|
|
"The comparison fixture did not reach the first combat room.");
|
|
yield return null;
|
|
}
|
|
|
|
// CoTransitionZone sets CurrentZone before its doorway
|
|
// hold. Wait for ActivateZone and encounter spawn to
|
|
// finish before inspecting or capturing the room.
|
|
yield return new WaitForSecondsRealtime(0.4f);
|
|
|
|
ZoneInstance zone = stageManager.CurrentZone;
|
|
Assert.That(stageManager.IsTransitioning, Is.False);
|
|
AssertReferenceEncounter(
|
|
zone,
|
|
mode == RoomScaleComparisonMode.Expanded ? 6 : 3,
|
|
mode == RoomScaleComparisonMode.Expanded ? 4 : 2);
|
|
float expectedOrtho = mode == RoomScaleComparisonMode.Expanded
|
|
? constants.StageComparisonCameraViewHalfExtents.y
|
|
: constants.StageComparisonOriginalCameraViewHalfExtents.y;
|
|
Assert.That(zone.Preset.GridWidth,
|
|
Is.EqualTo(mode == RoomScaleComparisonMode.Expanded ? 36 : 18));
|
|
Assert.That(zone.Preset.GridHeight,
|
|
Is.EqualTo(mode == RoomScaleComparisonMode.Expanded ? 24 : 12));
|
|
|
|
PlayerController comparisonPlayer =
|
|
Object.FindAnyObjectByType<PlayerController>();
|
|
Vector2 returnPosition = comparisonPlayer.transform.position;
|
|
Vector2 explorationPosition = returnPosition;
|
|
bool foundUnseenCell = false;
|
|
for (int y = 0; y < zone.Preset.GridHeight && !foundUnseenCell; y++)
|
|
{
|
|
for (int x = 0; x < zone.Preset.GridWidth; x++)
|
|
{
|
|
if (zone.GetRuntimeCell(x, y) == PresetCellType.Empty
|
|
|| zone.IsCellVisible(x, y))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
explorationPosition = zone.Preset.CellToWorldPosition(
|
|
zone.ZoneCenter,
|
|
new Vector2Int(x, y));
|
|
foundUnseenCell = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
Assert.That(foundUnseenCell, Is.True,
|
|
"The comparison room must have an unseen terrain cell.");
|
|
Assert.That(comparisonPlayer.TryReposition(explorationPosition), Is.True);
|
|
zone.UpdateVisibility(explorationPosition);
|
|
yield return null;
|
|
Assert.That(comparisonPlayer.TryReposition(returnPosition), Is.True);
|
|
zone.UpdateVisibility(returnPosition);
|
|
yield return null;
|
|
|
|
camera.Render();
|
|
Assert.That(camera.orthographicSize, Is.EqualTo(expectedOrtho).Within(0.01f));
|
|
AssertPixelPerfectReference(
|
|
camera,
|
|
mode == RoomScaleComparisonMode.Expanded ? 480 : 320,
|
|
mode == RoomScaleComparisonMode.Expanded ? 270 : 180);
|
|
AssertMinimapTerrainMatches(zone);
|
|
|
|
if (!string.IsNullOrWhiteSpace(captureDirectory))
|
|
{
|
|
CaptureWorldOnlyCamera(
|
|
camera,
|
|
captureDirectory,
|
|
mode == RoomScaleComparisonMode.Expanded ? "expanded" : "original");
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
constants.Stage1FirstCombatRoomScaleComparison = previousMode;
|
|
UnityEngine.Random.state = previousRandomState;
|
|
Time.timeScale = previousTimeScale;
|
|
RunManager.ForceProductionModeForTests = previousProductionMode;
|
|
}
|
|
}
|
|
|
|
private static IEnumerator DestroyActiveEnemies()
|
|
{
|
|
EnemyController[] enemies = Object.FindObjectsByType<EnemyController>(
|
|
FindObjectsSortMode.None);
|
|
for (int i = 0; i < enemies.Length; i++)
|
|
{
|
|
if (enemies[i] != null)
|
|
{
|
|
Object.Destroy(enemies[i].gameObject);
|
|
}
|
|
}
|
|
|
|
yield return null;
|
|
}
|
|
|
|
private static StageNode FindFirstCombat(StagePlan plan)
|
|
{
|
|
StageNode entrance = plan.StartNode;
|
|
for (int i = 0; i < plan.Nodes.Count; i++)
|
|
{
|
|
StageNode node = plan.Nodes[i];
|
|
if (node.NodeType != StageNodeType.Combat)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
for (int direction = 0; direction < 4; direction++)
|
|
{
|
|
if (entrance.GetConnection((StageDirection)direction) == node.Id)
|
|
{
|
|
return node;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static StageDirection GetEntryDirection(StageNode entrance, StageNode target)
|
|
{
|
|
for (int direction = 0; direction < 4; direction++)
|
|
{
|
|
StageDirection sourceDirection = (StageDirection)direction;
|
|
if (entrance.GetConnection(sourceDirection) == target.Id)
|
|
{
|
|
return sourceDirection.Opposite();
|
|
}
|
|
}
|
|
|
|
return StageDirection.West;
|
|
}
|
|
|
|
private static void AssertPixelPerfectReference(
|
|
Camera camera,
|
|
int expectedWidth,
|
|
int expectedHeight)
|
|
{
|
|
Component pixelPerfect = camera.GetComponent(
|
|
"UnityEngine.Rendering.Universal.PixelPerfectCamera");
|
|
Assert.That(pixelPerfect, Is.Not.Null);
|
|
PropertyInfo referenceWidth = pixelPerfect.GetType().GetProperty("refResolutionX");
|
|
PropertyInfo referenceHeight = pixelPerfect.GetType().GetProperty("refResolutionY");
|
|
Assert.That(referenceWidth.GetValue(pixelPerfect), Is.EqualTo(expectedWidth));
|
|
Assert.That(referenceHeight.GetValue(pixelPerfect), Is.EqualTo(expectedHeight));
|
|
}
|
|
|
|
private static void AssertMinimapTerrainMatches(ZoneInstance zone)
|
|
{
|
|
RunHUD hud = Object.FindAnyObjectByType<RunHUD>();
|
|
Assert.That(hud, Is.Not.Null);
|
|
FieldInfo terrainField = typeof(RunHUD).GetField(
|
|
"minimapTerrainRoot",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
RectTransform terrainRoot = terrainField?.GetValue(hud) as RectTransform;
|
|
Assert.That(terrainRoot, Is.Not.Null);
|
|
|
|
int visibleCellCount = 0;
|
|
for (int y = 0; y < zone.Preset.GridHeight; y++)
|
|
{
|
|
for (int x = 0; x < zone.Preset.GridWidth; x++)
|
|
{
|
|
if (zone.GetRuntimeCell(x, y) != PresetCellType.Empty)
|
|
{
|
|
visibleCellCount++;
|
|
}
|
|
}
|
|
}
|
|
|
|
Assert.That(terrainRoot.childCount, Is.EqualTo(visibleCellCount));
|
|
Assert.That(zone.VisibilityRevision, Is.GreaterThan(0));
|
|
|
|
int darkCellCount = 0;
|
|
int dimCellCount = 0;
|
|
for (int y = 0; y < zone.Preset.GridHeight; y++)
|
|
{
|
|
for (int x = 0; x < zone.Preset.GridWidth; x++)
|
|
{
|
|
if (zone.GetRuntimeCell(x, y) == PresetCellType.Empty)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
Image cell = terrainRoot.Find($"Terrain {x},{y}")?.GetComponent<Image>();
|
|
Assert.That(cell, Is.Not.Null);
|
|
Color32 color = cell.color;
|
|
if (zone.IsCellVisible(x, y))
|
|
{
|
|
Assert.That(color, Is.Not.EqualTo(new Color32(2, 4, 5, 255)));
|
|
}
|
|
else if (zone.IsCellExplored(x, y))
|
|
{
|
|
dimCellCount++;
|
|
Assert.That(color.a, Is.EqualTo(255));
|
|
}
|
|
else
|
|
{
|
|
darkCellCount++;
|
|
Assert.That(color, Is.EqualTo(new Color32(2, 4, 5, 255)));
|
|
}
|
|
}
|
|
}
|
|
|
|
Assert.That(dimCellCount, Is.GreaterThan(0));
|
|
Assert.That(darkCellCount, Is.GreaterThan(0));
|
|
}
|
|
|
|
private static void AssertReferenceEncounter(
|
|
ZoneInstance zone,
|
|
int expectedBatCount,
|
|
int expectedSkeletonCount)
|
|
{
|
|
int batCount = 0;
|
|
int skeletonCount = 0;
|
|
for (int i = 0; i < zone.SpawnedEnemies.Count; i++)
|
|
{
|
|
EnemyController enemy = zone.SpawnedEnemies[i];
|
|
if (enemy == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (enemy.name.StartsWith("Bat", System.StringComparison.Ordinal))
|
|
{
|
|
batCount++;
|
|
}
|
|
else if (enemy.name.StartsWith("Skeleton", System.StringComparison.Ordinal))
|
|
{
|
|
skeletonCount++;
|
|
}
|
|
}
|
|
|
|
Assert.That(batCount, Is.EqualTo(expectedBatCount));
|
|
Assert.That(skeletonCount, Is.EqualTo(expectedSkeletonCount));
|
|
}
|
|
|
|
private static void CaptureWorldOnlyCamera(
|
|
Camera camera,
|
|
string directory,
|
|
string variant)
|
|
{
|
|
System.IO.Directory.CreateDirectory(directory);
|
|
RenderTexture previousTarget = camera.targetTexture;
|
|
RenderTexture previousActive = RenderTexture.active;
|
|
RenderTexture target = new(640, 360, 24, RenderTextureFormat.ARGB32);
|
|
Texture2D snapshot = null;
|
|
try
|
|
{
|
|
target.Create();
|
|
camera.targetTexture = target;
|
|
camera.Render();
|
|
RenderTexture.active = target;
|
|
snapshot = new Texture2D(640, 360, TextureFormat.RGB24, false);
|
|
snapshot.ReadPixels(new Rect(0f, 0f, 640f, 360f), 0, 0);
|
|
snapshot.Apply();
|
|
string path = System.IO.Path.Combine(
|
|
directory,
|
|
$"s1a-{variant}-world.png");
|
|
System.IO.File.WriteAllBytes(path, snapshot.EncodeToPNG());
|
|
}
|
|
finally
|
|
{
|
|
camera.targetTexture = previousTarget;
|
|
RenderTexture.active = previousActive;
|
|
if (snapshot != null)
|
|
{
|
|
Object.Destroy(snapshot);
|
|
}
|
|
|
|
target.Release();
|
|
Object.Destroy(target);
|
|
}
|
|
}
|
|
|
|
private static EnemyController FindRangedPrefab(SpawnDirector director)
|
|
{
|
|
FieldInfo field = typeof(SpawnDirector).GetField(
|
|
"enemyPrefabs",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
EnemyController[] prefabs = field?.GetValue(director) as EnemyController[];
|
|
if (prefabs == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
for (int i = 0; i < prefabs.Length; i++)
|
|
{
|
|
if (prefabs[i] != null
|
|
&& prefabs[i].Definition != null
|
|
&& prefabs[i].Definition.IsRanged)
|
|
{
|
|
return prefabs[i];
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static EnemyController FindCatalogPrefab(
|
|
SpawnDirector director,
|
|
EnemyKind kind)
|
|
{
|
|
string[] fieldNames =
|
|
{
|
|
"enemyPrefabs",
|
|
"elitePrefabs",
|
|
"midBossPrefabs",
|
|
"finalBossPrefabs",
|
|
};
|
|
for (int fieldIndex = 0; fieldIndex < fieldNames.Length; fieldIndex++)
|
|
{
|
|
FieldInfo field = typeof(SpawnDirector).GetField(
|
|
fieldNames[fieldIndex],
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
EnemyController[] prefabs = field?.GetValue(director) as EnemyController[];
|
|
if (prefabs == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
for (int prefabIndex = 0; prefabIndex < prefabs.Length; prefabIndex++)
|
|
{
|
|
EnemyController prefab = prefabs[prefabIndex];
|
|
if (prefab != null
|
|
&& prefab.Definition != null
|
|
&& prefab.Definition.Kind == kind)
|
|
{
|
|
return prefab;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static RunEventEnemyTuning CreateEdgeEventTuning(
|
|
EnemyKind kind,
|
|
float scale)
|
|
{
|
|
return RunEventEnemyTuning.Create(
|
|
kind,
|
|
1f,
|
|
0,
|
|
scale,
|
|
Color.white,
|
|
1f,
|
|
1f,
|
|
1f,
|
|
1f,
|
|
1f,
|
|
1f,
|
|
1f,
|
|
1f,
|
|
1f,
|
|
1,
|
|
1f,
|
|
1f,
|
|
1,
|
|
1f,
|
|
false);
|
|
}
|
|
|
|
private static IEnumerator LoadAndStartRun()
|
|
{
|
|
ArtifactRewardController.GrantCatalogForTests = false;
|
|
AsyncOperation load = SceneManager.LoadSceneAsync("CombatPrototype");
|
|
while (!load.isDone)
|
|
{
|
|
yield return null;
|
|
}
|
|
yield return null;
|
|
yield return null;
|
|
|
|
RunManager runManager = RunManager.Instance;
|
|
if (runManager.IsTitleScreen)
|
|
{
|
|
Assert.That(runManager.BeginRun(), Is.True);
|
|
}
|
|
|
|
ArtifactRewardController reward =
|
|
Object.FindAnyObjectByType<ArtifactRewardController>();
|
|
Assert.That(reward, Is.Not.Null);
|
|
for (int selection = 0; selection < 3; selection++)
|
|
{
|
|
float deadline = Time.realtimeSinceStartup + 2f;
|
|
while (!reward.DebugIsSelectionVisible
|
|
&& Time.realtimeSinceStartup < deadline)
|
|
{
|
|
yield return null;
|
|
}
|
|
|
|
Assert.That(reward.DebugIsSelectionVisible, Is.True);
|
|
Assert.That(reward.DebugChooseOption(0), Is.True);
|
|
yield return null;
|
|
}
|
|
|
|
// The first artifact acquisition queues the tutorial modal. This
|
|
// helper is used by gameplay tests, so acknowledge any queued
|
|
// guidance before waiting on gameplay-time coroutines.
|
|
RunTutorialController tutorials =
|
|
Object.FindAnyObjectByType<RunTutorialController>();
|
|
for (int tutorial = 0;
|
|
tutorials != null && tutorials.DebugIsVisible && tutorial < 4;
|
|
tutorial++)
|
|
{
|
|
Assert.That(tutorials.DebugContinue(), Is.True);
|
|
yield return null;
|
|
}
|
|
|
|
Assert.That(
|
|
RunManager.GameplayInputEnabled,
|
|
Is.True,
|
|
"Finite arena tests require startup tutorial guidance to be closed.");
|
|
}
|
|
}
|
|
}
|