Files
tiny_tackle_heroes/Assets/_Project/Tests/EditMode/BumpCombatMathTests.cs
T

446 lines
15 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using BumpCombat.Combat;
using BumpCombat.Enemies;
using NUnit.Framework;
using UnityEngine;
namespace BumpCombat.Tests
{
public sealed class BumpCombatMathTests
{
[Test]
public void ClassifySide_WithinFrontNinetyDegrees_ReturnsFront()
{
Vector2 enemyFacing = Vector2.right;
Vector2 enemyToPlayer = DirectionAtDegrees(44f);
Assert.That(BumpCombatMath.ClassifySide(enemyFacing, enemyToPlayer),
Is.EqualTo(HitSide.Front));
}
[Test]
public void ClassifySide_OutsideFrontNinetyDegrees_ReturnsSide()
{
Vector2 enemyFacing = Vector2.right;
Vector2 enemyToPlayer = DirectionAtDegrees(46f);
Assert.That(BumpCombatMath.ClassifySide(enemyFacing, enemyToPlayer),
Is.EqualTo(HitSide.Side));
}
[Test]
public void ClassifySide_WithinBackOneHundredTwentyDegrees_ReturnsBack()
{
Vector2 enemyFacing = Vector2.right;
Vector2 enemyToPlayer = DirectionAtDegrees(121f);
Assert.That(BumpCombatMath.ClassifySide(enemyFacing, enemyToPlayer),
Is.EqualTo(HitSide.Back));
}
[Test]
public void ClassifySide_OutsideBackOneHundredTwentyDegrees_ReturnsSide()
{
Vector2 enemyFacing = Vector2.right;
Vector2 enemyToPlayer = DirectionAtDegrees(119f);
Assert.That(BumpCombatMath.ClassifySide(enemyFacing, enemyToPlayer),
Is.EqualTo(HitSide.Side));
}
[Test]
public void ApplySideMultiplier_BackHit_IsExactlyOnePointFiveTimesDamage()
{
Assert.That(BumpCombatMath.ApplySideMultiplier(10f, HitSide.Back), Is.EqualTo(15f));
}
[Test]
public void ApplySideMultiplier_SideHit_IsExactlyOnePointTwoTimesDamage()
{
Assert.That(BumpCombatMath.ApplySideMultiplier(10f, HitSide.Side), Is.EqualTo(12f));
}
[Test]
public void ApplySideMultiplier_OffsetFrontHit_IsExactlyOnePointTwoTimesDamage()
{
Assert.That(
BumpCombatMath.ApplySideMultiplier(10f, HitSide.Front, true),
Is.EqualTo(12f));
}
[Test]
public void ApplySideMultiplier_CenteredFrontHit_DoesNotChangeDamage()
{
Assert.That(
BumpCombatMath.ApplySideMultiplier(10.5f, HitSide.Front),
Is.EqualTo(10.5f));
}
[TestCase(HitSide.Back, 16.5f)]
[TestCase(HitSide.Side, 13.2f)]
public void ApplySideMultiplier_PreservesFractionalPositionBonus(
HitSide side,
float expectedDamage)
{
Assert.That(
BumpCombatMath.ApplySideMultiplier(11f, side),
Is.EqualTo(expectedDamage).Within(0.001f));
}
[Test]
public void ApplyRepeatedMultiplier_PreservesEveryFractionalChainDepth()
{
Assert.That(
DamageCalculator.ApplyRepeatedMultiplier(10f, 1.2f, 0),
Is.EqualTo(10f));
Assert.That(
DamageCalculator.ApplyRepeatedMultiplier(10f, 1.2f, 1),
Is.EqualTo(12f));
Assert.That(
DamageCalculator.ApplyRepeatedMultiplier(10f, 1.2f, 2),
Is.EqualTo(14.4f).Within(0.001f));
float depthThreeDamage = DamageCalculator.ApplyRepeatedMultiplier(10f, 1.2f, 3);
Assert.That(
depthThreeDamage,
Is.EqualTo(17.28f).Within(0.001f),
"Each link applies ×1.2 to the previous fractional damage value.");
Assert.That(DamageCalculator.FinalizeDamage(depthThreeDamage), Is.EqualTo(17));
}
[Test]
public void ApplyDamageTakenEffects_FinalizesAfterShockAndVulnerability()
{
float damage = DamageCalculator.ApplyDamageTakenEffects(
13f,
0.2f,
true,
0.25f,
true);
Assert.That(damage, Is.EqualTo(19f), "13 × 1.2 × 1.25 = 19.5, then final floor.");
}
[Test]
public void FinalizeDamage_DoesNotDropIntermediateDamageBelowOne()
{
float intermediateDamage = DamageCalculator.ApplyMultiplier(0.9f, 2f);
Assert.That(intermediateDamage, Is.EqualTo(1.8f).Within(0.001f));
Assert.That(DamageCalculator.FinalizeDamage(intermediateDamage), Is.EqualTo(1f));
}
[Test]
public void FormatDamage_FloorsOnlyTheDisplayedDamage()
{
Assert.That(DamageCalculator.FormatDamage(14.5f), Is.EqualTo("14"));
}
[TestCase(HitSide.Front)]
[TestCase(HitSide.Side)]
public void PlayerImpactRecoil_NonBackHit_IsApplied(HitSide side)
{
Assert.That(BumpCombatMath.AppliesPlayerImpactRecoil(side), Is.True);
}
[Test]
public void PlayerImpactRecoil_BackHit_IsNotApplied()
{
Assert.That(
BumpCombatMath.AppliesPlayerImpactRecoil(HitSide.Back),
Is.False);
}
[Test]
public void PlayerImpactRecoil_EventEnemyBackHit_IsApplied()
{
Assert.That(
BumpCombatMath.AppliesPlayerImpactRecoil(HitSide.Back, true),
Is.True);
}
[TestCase(1.2f, 0.625f, false, false)]
[TestCase(0.625f, 0.625f, false, true)]
[TestCase(0f, 0f, false, true)]
[TestCase(0.4f, 0.625f, false, true)]
[TestCase(0.1f, 0.625f, true, false)]
public void AttackAnimationLead_StartsOnceAtLeadTime(
float warningTimeRemaining,
float leadTime,
bool animationStarted,
bool expected)
{
Assert.That(
EnemyController.ShouldStartAttackAnimation(
warningTimeRemaining,
leadTime,
animationStarted),
Is.EqualTo(expected));
}
[Test]
public void HorizontalRootDirection_UsesOnlyTheLockedLeftRightSign()
{
Assert.That(
EnemyAttack.ResolveAttackDirection(
EnemyAttackDirectionMode.HorizontalRoot,
new Vector2(1f, 1f),
Vector2.left),
Is.EqualTo(Vector2.right));
Assert.That(
EnemyAttack.ResolveAttackDirection(
EnemyAttackDirectionMode.HorizontalRoot,
new Vector2(-1f, 1f),
Vector2.right),
Is.EqualTo(Vector2.left));
Assert.That(
EnemyAttack.ResolveAttackDirection(
EnemyAttackDirectionMode.LockedDirection,
new Vector2(1f, 1f),
Vector2.left),
Is.EqualTo(new Vector2(1f, 1f).normalized));
}
[Test]
public void HorizontalRootOrigin_UsesWorldUpForLocalVerticalOffset()
{
Vector2 rightOrigin = EnemyAttack.GetAttackOrigin(
Vector2.zero,
Vector2.right,
new Vector2(0.05f, -0.05f),
EnemyAttackDirectionMode.HorizontalRoot);
Vector2 leftOrigin = EnemyAttack.GetAttackOrigin(
Vector2.zero,
Vector2.left,
new Vector2(0.05f, -0.05f),
EnemyAttackDirectionMode.HorizontalRoot);
Assert.That(rightOrigin, Is.EqualTo(new Vector2(0.05f, -0.05f)));
Assert.That(leftOrigin, Is.EqualTo(new Vector2(-0.05f, -0.05f)));
}
[Test]
public void ContactWindow_UsesHalfOpenActiveInterval()
{
EnemyAttackPattern pattern = EnemyAttackPattern.Create(
"Attack",
EnemyAttackShape.Box,
1f,
0.25f,
1f,
1f,
1f,
1f,
0f);
Assert.That(pattern.IsContactWindowActive(0f, 0.25f), Is.True);
Assert.That(pattern.IsContactWindowActive(0.24999f, 0.25f), Is.True);
Assert.That(pattern.IsContactWindowActive(0.25f, 0.25f), Is.False);
Assert.That(pattern.IsContactWindowActive(0.27f, 0.25f), Is.False);
Assert.That(pattern.IsContactWindowActive(-0.01f, 0.25f), Is.False);
}
[Test]
public void CalculateLateralOffset_CenteredApproach_ReturnsZero()
{
Assert.That(
BumpCombatMath.CalculateLateralOffset(
Vector2.right,
Vector2.zero,
Vector2.right),
Is.EqualTo(0f).Within(0.0001f));
}
[Test]
public void IsOffsetHit_FrontApproachPastThreshold_ReturnsTrue()
{
Assert.That(
BumpCombatMath.IsOffsetHit(
HitSide.Front,
Vector2.right,
Vector2.zero,
new Vector2(0.4f, 0.17f),
0.16f),
Is.True);
}
[Test]
public void IsOffsetHit_FrontApproachBelowThreshold_ReturnsFalse()
{
Assert.That(
BumpCombatMath.IsOffsetHit(
HitSide.Front,
Vector2.right,
Vector2.zero,
new Vector2(0.4f, 0.15f),
0.16f),
Is.False);
}
[TestCase(HitSide.Side)]
[TestCase(HitSide.Back)]
public void IsOffsetHit_NonFrontSide_ReturnsFalse(HitSide side)
{
Assert.That(
BumpCombatMath.IsOffsetHit(
side,
Vector2.right,
Vector2.zero,
new Vector2(0.4f, 0.2f),
0.16f),
Is.False);
}
[Test]
public void WeaponEnemy_AttackStates_DoNotAllowStateMovement()
{
Assert.That(
EnemyController.AllowsStateMovement(EnemyState.Warning, EnemyAttackShape.Box),
Is.False);
Assert.That(
EnemyController.AllowsStateMovement(EnemyState.Active, EnemyAttackShape.Box),
Is.False);
Assert.That(
EnemyController.AllowsStateMovement(EnemyState.Recovery, EnemyAttackShape.Box),
Is.False);
}
[Test]
public void Slime_ActiveState_AllowsLockedDirectionDash()
{
Assert.That(
EnemyController.AllowsStateMovement(EnemyState.Active, EnemyAttackShape.Body),
Is.True);
}
[TestCase(EnemyState.Chase)]
[TestCase(EnemyState.Warning)]
[TestCase(EnemyState.Recovery)]
public void Slime_NonActiveState_DoesNotDealContactDamage(EnemyState state)
{
Assert.That(
EnemyController.AllowsContactDamage(state, EnemyAttackShape.Body),
Is.False);
}
[Test]
public void Slime_ActiveState_DealsContactDamage()
{
Assert.That(
EnemyController.AllowsContactDamage(
EnemyState.Active,
EnemyAttackShape.Body),
Is.True);
}
[TestCase(EnemyState.Warning)]
[TestCase(EnemyState.Active)]
public void BackHit_AttemptedAttackState_IsInterruptible(EnemyState state)
{
Assert.That(EnemyController.CanBackHitInterrupt(state), Is.True);
}
[TestCase(EnemyState.Spawn)]
[TestCase(EnemyState.Chase)]
[TestCase(EnemyState.Recovery)]
[TestCase(EnemyState.Dead)]
public void BackHit_NonAttackState_IsNotInterruptible(EnemyState state)
{
Assert.That(EnemyController.CanBackHitInterrupt(state), Is.False);
}
[TestCase(EnemyAttackShape.Box)]
[TestCase(EnemyAttackShape.Cone)]
[TestCase(EnemyAttackShape.TargetCircle)]
public void WeaponAttackShapes_NeverDealBodyContactDamage(
EnemyAttackShape attackShape)
{
Assert.That(
EnemyController.AllowsContactDamage(
EnemyState.Active,
attackShape),
Is.False);
}
[Test]
public void HeavyEnemies_HaveMorePlayerKnockbackThanSlime()
{
float slimeDistance =
EnemyController.GetPlayerKnockbackDistance(EnemyKind.Slime);
Assert.That(
EnemyController.GetPlayerKnockbackDistance(EnemyKind.Lancer),
Is.GreaterThan(slimeDistance));
Assert.That(
EnemyController.GetPlayerKnockbackDistance(
EnemyKind.GreatswordSkeleton),
Is.GreaterThan(slimeDistance));
}
[Test]
public void AllEnemies_ChaseState_AllowsMovement()
{
Assert.That(
EnemyController.AllowsStateMovement(EnemyState.Chase, EnemyAttackShape.Box),
Is.True);
}
private static Vector2 DirectionAtDegrees(float degrees)
{
float radians = degrees * Mathf.Deg2Rad;
return new Vector2(Mathf.Cos(radians), Mathf.Sin(radians));
}
}
public sealed class LancerAttackMotionTests
{
[Test]
public void WarningReach_RetractsFromSixtyFiveToThirtyFivePercent()
{
Assert.That(
LancerAttackMotion.EvaluateWarningReachFraction(0f),
Is.EqualTo(0.65f).Within(0.0001f));
Assert.That(
LancerAttackMotion.EvaluateWarningReachFraction(1f),
Is.EqualTo(0.35f).Within(0.0001f));
}
[Test]
public void ActiveReach_ExtendsThenReturnsDuringNonDamagingTail()
{
Assert.That(
LancerAttackMotion.EvaluateActiveReachFraction(0f),
Is.EqualTo(0.35f).Within(0.0001f));
Assert.That(
LancerAttackMotion.EvaluateActiveReachFraction(0.65f),
Is.EqualTo(1f).Within(0.0001f));
Assert.That(
LancerAttackMotion.EvaluateActiveReachFraction(1f),
Is.EqualTo(0.35f).Within(0.0001f));
Assert.That(
LancerAttackMotion.IsDamageWindow(0.65f),
Is.True);
Assert.That(
LancerAttackMotion.IsDamageWindow(0.651f),
Is.False);
}
[TestCase(1f, 0f)]
[TestCase(-1f, 0f)]
[TestCase(0f, 1f)]
[TestCase(1f, 1f)]
public void FullReachTip_ProjectsToAttackLengthInLockedDirection(
float directionX,
float directionY)
{
Vector2 direction = new(directionX, directionY);
Vector2 tip = LancerAttackMotion.GetTipPosition(
Vector2.zero,
direction,
1.8f,
1f);
Assert.That(Vector2.Dot(tip, direction.normalized), Is.EqualTo(1.8f).Within(0.0001f));
}
}
}