Files

326 lines
14 KiB
C#

using System;
using System.Collections;
using System.Reflection;
using BumpCombat.Enemies;
using BumpCombat.Core;
using BumpCombat.Player;
using BumpCombat.Progression;
using BumpCombat.Spawning;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.TestTools;
namespace BumpCombat.PlayModeTests
{
public sealed class LancerThrustPlayModeTests
{
[SetUp]
public void SetUp()
{
ArtifactRewardController.GrantCatalogForTests = true;
RunManager.ForceProductionModeForTests = false;
Time.timeScale = 1f;
}
[TearDown]
public void TearDown()
{
ArtifactRewardController.GrantCatalogForTests = false;
RunManager.ForceProductionModeForTests = false;
Time.timeScale = 1f;
}
[UnityTest]
public IEnumerator LancerThrust_VisualUsesLockedHandAndCleansUpAcrossDirections()
{
yield return LoadCombatPrototype();
EnemyController lancer = SpawnLancer();
EnemyAttack attack = lancer.GetComponent<EnemyAttack>();
LancerThrustVisual visual = lancer.GetComponent<LancerThrustVisual>();
SpriteRenderer source = lancer.GetComponent<SpriteRenderer>();
lancer.enabled = false;
Vector2[] directions =
{
Vector2.right,
Vector2.left,
Vector2.up,
new Vector2(-1f, 1f).normalized,
};
foreach (Vector2 direction in directions)
{
attack.BeginWarning(direction, (Vector2)lancer.transform.position + direction);
SetState(lancer, EnemyState.Warning, lancer.WarningDuration);
yield return null;
Assert.That(visual, Is.Not.Null);
Assert.That(visual.IsVisible, Is.True);
Assert.That(source.enabled, Is.False);
Vector2 expectedHand = (Vector2)lancer.transform.position
+ LancerAttackMotion.GetHandAnchorOffset(
direction,
Mathf.Abs(lancer.transform.lossyScale.x));
Assert.That(
Vector2.Distance(visual.SpearRenderer.transform.position, expectedHand),
Is.LessThan(0.0001f));
Assert.That(
Mathf.Abs(Mathf.DeltaAngle(
visual.SpearRenderer.transform.eulerAngles.z,
Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg)),
Is.LessThan(0.001f));
attack.EndAttack(true);
yield return null;
Assert.That(visual.IsVisible, Is.False);
Assert.That(source.enabled, Is.True);
}
}
[UnityTest]
public IEnumerator LancerThrust_SweepsTipAndRetriesBlockedHitOnce()
{
yield return LoadCombatPrototype();
PlayerHealth health = UnityEngine.Object.FindAnyObjectByType<PlayerHealth>();
Rigidbody2D playerBody = health.GetComponent<Rigidbody2D>();
Collider2D playerCollider = health.GetComponent<Collider2D>();
EnemyController lancer = SpawnLancer();
EnemyAttack attack = lancer.GetComponent<EnemyAttack>();
lancer.enabled = false;
lancer.transform.position = Vector2.zero;
lancer.GetComponent<Rigidbody2D>().position = Vector2.zero;
Vector2 direction = Vector2.right;
float scale = Mathf.Abs(lancer.transform.lossyScale.x);
Vector2 futureTip = LancerAttackMotion.GetTipPosition(
lancer.transform.position,
direction,
lancer.AttackLength,
1f,
scale);
PlacePlayerAtColliderCenter(playerBody, playerCollider, futureTip);
Physics2D.SyncTransforms();
attack.BeginWarning(direction, futureTip);
SetState(lancer, EnemyState.Warning, lancer.WarningDuration);
float healthBefore = health.CurrentHealth;
attack.Activate();
SetState(lancer, EnemyState.Active, lancer.ActiveDuration);
Assert.That(health.CurrentHealth, Is.EqualTo(healthBefore));
attack.TickActive(lancer.ActiveDuration * 0.1f);
Assert.That(health.CurrentHealth, Is.EqualTo(healthBefore));
health.GrantInvulnerability(0.25f);
attack.TickActive(lancer.ActiveDuration
* LancerAttackMotion.ActiveExtensionEndFraction);
Assert.That(health.CurrentHealth, Is.EqualTo(healthBefore));
yield return new WaitForSecondsRealtime(0.3f);
PlacePlayerAtColliderCenter(playerBody, playerCollider, futureTip);
Physics2D.SyncTransforms();
Assert.That(health.IsInvulnerable, Is.False);
attack.TickActive(lancer.ActiveDuration
* LancerAttackMotion.ActiveExtensionEndFraction);
Assert.That(
health.CurrentHealth,
Is.EqualTo(healthBefore - lancer.AttackDamage).Within(0.0001f));
float immunityDeadline = Time.realtimeSinceStartup
+ health.InvulnerabilityDuration + 1f;
while (health.IsInvulnerable
&& Time.realtimeSinceStartup < immunityDeadline)
{
yield return new WaitForSecondsRealtime(0.05f);
}
Assert.That(health.IsInvulnerable, Is.False,
"The same thrust is retried only after the prior hit's real invulnerability ends.");
PlacePlayerAtColliderCenter(playerBody, playerCollider, futureTip);
Physics2D.SyncTransforms();
attack.TickActive(lancer.ActiveDuration
* LancerAttackMotion.ActiveExtensionEndFraction);
Assert.That(
health.CurrentHealth,
Is.EqualTo(healthBefore - lancer.AttackDamage).Within(0.0001f));
// A completed extension must not retry the stale tip during return.
attack.EndAttack(false);
attack.BeginWarning(direction, futureTip);
SetState(lancer, EnemyState.Warning, lancer.WarningDuration);
attack.Activate();
SetState(lancer, EnemyState.Active, lancer.ActiveDuration);
health.GrantInvulnerability(0.15f);
float afterFirstThrust = health.CurrentHealth;
attack.TickActive(lancer.ActiveDuration
* LancerAttackMotion.ActiveExtensionEndFraction);
yield return new WaitForSecondsRealtime(0.2f);
Assert.That(health.IsInvulnerable, Is.False);
attack.TickActive(lancer.ActiveDuration * 0.8f);
Assert.That(health.CurrentHealth, Is.EqualTo(afterFirstThrust));
attack.EndAttack(false);
SetState(lancer, EnemyState.Recovery, lancer.RecoveryDuration);
attack.TickActive(lancer.ActiveDuration);
Assert.That(
health.CurrentHealth,
Is.EqualTo(healthBefore - lancer.AttackDamage).Within(0.0001f));
}
[UnityTest]
public IEnumerator LancerThrust_KnockbackRebaseDoesNotSweepDisplacement()
{
yield return LoadCombatPrototype();
PlayerHealth health = UnityEngine.Object.FindAnyObjectByType<PlayerHealth>();
Rigidbody2D playerBody = health.GetComponent<Rigidbody2D>();
Collider2D playerCollider = health.GetComponent<Collider2D>();
EnemyController lancer = SpawnLancer();
EnemyAttack attack = lancer.GetComponent<EnemyAttack>();
lancer.enabled = false;
Rigidbody2D lancerBody = lancer.GetComponent<Rigidbody2D>();
Vector2 direction = Vector2.right;
Vector2 oldRoot = Vector2.zero;
lancer.transform.position = oldRoot;
lancerBody.position = oldRoot;
Vector2 farFromPath = new Vector2(5f, 5f);
PlacePlayerAtColliderCenter(playerBody, playerCollider, farFromPath);
Physics2D.SyncTransforms();
attack.BeginWarning(direction, farFromPath);
SetState(lancer, EnemyState.Warning, lancer.WarningDuration);
attack.Activate();
SetState(lancer, EnemyState.Active, lancer.ActiveDuration);
attack.TickActive(lancer.ActiveDuration * 0.1f);
float scale = Mathf.Abs(lancer.transform.lossyScale.x);
float firstProgress = 0.1f;
Vector2 oldTip = LancerAttackMotion.GetTipPosition(
oldRoot,
direction,
lancer.AttackLength,
LancerAttackMotion.EvaluateActiveReachFraction(firstProgress),
scale);
Vector2 newRoot = new Vector2(0f, 3f);
lancer.transform.position = newRoot;
lancerBody.position = newRoot;
Physics2D.SyncTransforms();
Vector2 newTipAtFirstProgress = LancerAttackMotion.GetTipPosition(
newRoot,
direction,
lancer.AttackLength,
LancerAttackMotion.EvaluateActiveReachFraction(firstProgress),
scale);
attack.RebaseActiveTipAfterExternalMotion();
float healthBefore = health.CurrentHealth;
PlacePlayerAtColliderCenter(
playerBody,
playerCollider,
Vector2.Lerp(oldTip, newTipAtFirstProgress, 0.5f));
Physics2D.SyncTransforms();
attack.TickActive(lancer.ActiveDuration * 0.2f);
Assert.That(health.CurrentHealth, Is.EqualTo(healthBefore));
Vector2 resumedTip = LancerAttackMotion.GetTipPosition(
newRoot,
direction,
lancer.AttackLength,
LancerAttackMotion.EvaluateActiveReachFraction(0.5f),
scale);
PlacePlayerAtColliderCenter(playerBody, playerCollider, resumedTip);
Physics2D.SyncTransforms();
attack.TickActive(lancer.ActiveDuration * 0.5f);
Assert.That(
health.CurrentHealth,
Is.EqualTo(healthBefore - lancer.AttackDamage).Within(0.0001f));
}
private static EnemyController SpawnLancer()
{
SpawnDirector director = UnityEngine.Object.FindAnyObjectByType<SpawnDirector>();
Assert.That(director, Is.Not.Null);
director.enabled = false;
director.DebugSpawnImmediate(3);
// Lancer remains a legacy motion regression fixture even though
// the stage catalog now contains only the requested ambient and
// event roster. Load the preserved prefab directly in the Editor
// test process instead of coupling this helper to wave contents.
GameObject lancerPrefab = LoadLegacyPrefab(
"Assets/_Project/Prefabs/Enemies/Lancer.prefab");
Assert.That(lancerPrefab, Is.Not.Null);
EnemyController spawnedLancer = UnityEngine.Object.Instantiate(
lancerPrefab,
Vector3.zero,
Quaternion.identity)
.GetComponent<EnemyController>();
Assert.That(spawnedLancer, Is.Not.Null);
EnemyController[] enemies = UnityEngine.Object.FindObjectsByType<EnemyController>(
FindObjectsSortMode.InstanceID);
foreach (EnemyController enemy in enemies)
{
if (enemy != spawnedLancer)
{
enemy.gameObject.SetActive(false);
}
}
return spawnedLancer;
}
private static GameObject LoadLegacyPrefab(string assetPath)
{
Type assetDatabaseType = Type.GetType("UnityEditor.AssetDatabase, UnityEditor");
Assert.That(assetDatabaseType, Is.Not.Null, "UnityEditor.AssetDatabase is unavailable.");
MethodInfo loadAssetAtPath = null;
foreach (MethodInfo method in assetDatabaseType.GetMethods(
BindingFlags.Public | BindingFlags.Static))
{
if (method.Name == "LoadAssetAtPath"
&& method.IsGenericMethodDefinition
&& method.GetGenericArguments().Length == 1)
{
loadAssetAtPath = method;
break;
}
}
Assert.That(loadAssetAtPath, Is.Not.Null);
MethodInfo loadPrefab = loadAssetAtPath.MakeGenericMethod(typeof(GameObject));
return loadPrefab.Invoke(null, new object[] { assetPath }) as GameObject;
}
private static void PlacePlayerAtColliderCenter(
Rigidbody2D body,
Collider2D collider,
Vector2 desiredCenter)
{
Vector2 offset = (Vector2)collider.bounds.center - body.position;
body.position = desiredCenter - offset;
}
private static void SetState(
EnemyController controller,
EnemyState state,
float duration)
{
typeof(EnemyController)
.GetMethod("EnterState", BindingFlags.Instance | BindingFlags.NonPublic)
?.Invoke(controller, new object[] { state, duration });
}
private static IEnumerator LoadCombatPrototype()
{
AsyncOperation load = SceneManager.LoadSceneAsync("CombatPrototype");
while (!load.isDone)
{
yield return null;
}
yield return null;
yield return null;
Assert.That(Time.timeScale, Is.EqualTo(1f));
}
}
}