Initial commit: Tiny Tackle Heroes Unity project
This commit is contained in:
@@ -0,0 +1,457 @@
|
||||
using System.Collections;
|
||||
using System.Reflection;
|
||||
using BumpCombat.Core;
|
||||
using BumpCombat.Enemies;
|
||||
using BumpCombat.Player;
|
||||
using BumpCombat.Progression;
|
||||
using BumpCombat.Spawning;
|
||||
using BumpCombat.UI;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngine.TestTools;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user