Initial commit: Tiny Tackle Heroes Unity project
This commit is contained in:
@@ -0,0 +1,325 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user