280 lines
11 KiB
C#
280 lines
11 KiB
C#
using PkmnLib.Dynamic.Models;
|
|
using PkmnLib.Dynamic.ScriptHandling;
|
|
using PkmnLib.Plugin.Gen7.Scripts.Moves;
|
|
using PkmnLib.Plugin.Gen7.Scripts.Pokemon;
|
|
|
|
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
|
|
|
|
/// <summary>
|
|
/// Tests for the <see cref="Bide"/> move script and its <see cref="BideEffect"/> volatile.
|
|
/// Gen VII Bulbapedia behavior (base plus Gen III-VII deltas): the user idles while storing the damage it
|
|
/// receives, then "Bide will do damage equal to twice the damage received during the idling period", enduring
|
|
/// "attacks for two turns" and hitting "the last Pokémon to attack the user, even if this is an ally".
|
|
/// </summary>
|
|
public class BideTests
|
|
{
|
|
private static (Bide script, IExecutingMove move, IPokemon user, IPokemon target, IScriptSet userVolatile, IHitData
|
|
hitData) CreateTestSetup()
|
|
{
|
|
var script = new Bide();
|
|
var move = Substitute.For<IExecutingMove>();
|
|
var target = Substitute.For<IPokemon>();
|
|
var hitData = Substitute.For<IHitData>();
|
|
move.GetHitData(target, 0).Returns(hitData);
|
|
|
|
var user = Substitute.For<IPokemon>();
|
|
user.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));
|
|
// A real script set so the volatile Bide effect can actually be added, retrieved and removed.
|
|
var userVolatile = new ScriptSet(user);
|
|
user.Volatile.Returns(userVolatile);
|
|
move.User.Returns(user);
|
|
|
|
return (script, move, user, target, userVolatile, hitData);
|
|
}
|
|
|
|
private static IPokemon CreateAttacker(bool onBattlefield = true)
|
|
{
|
|
var attacker = Substitute.For<IPokemon>();
|
|
var battleData = Substitute.For<IPokemonBattleData>();
|
|
battleData.IsOnBattlefield.Returns(onBattlefield);
|
|
attacker.BattleData.Returns(battleData);
|
|
return attacker;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a <see cref="BideEffect"/> to the user's volatile scripts, as if Bide has already been storing
|
|
/// energy for the given number of executed turns.
|
|
/// </summary>
|
|
private static BideEffect AddStoredBideEffect(IScriptSet userVolatile, IPokemon user, byte turns, uint damageTaken,
|
|
params IPokemon[] hitBy)
|
|
{
|
|
var effect = new BideEffect(user)
|
|
{
|
|
Turns = turns,
|
|
DamageTaken = damageTaken,
|
|
};
|
|
effect.HitBy.AddRange(hitBy);
|
|
userVolatile.Add(effect);
|
|
return effect;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper to check whether a Pokémon received any Damage call.
|
|
/// </summary>
|
|
private static bool ReceivedDamage(IPokemon pokemon) =>
|
|
pokemon.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Damage");
|
|
|
|
/// <summary>
|
|
/// Helper to extract the damage amount from a Pokémon's received Damage calls.
|
|
/// </summary>
|
|
private static uint? GetDamageAmount(IPokemon pokemon)
|
|
{
|
|
var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage");
|
|
return call != null ? (uint)call.GetArguments()[0]! : null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper to extract the damage source from a Pokémon's received Damage calls.
|
|
/// </summary>
|
|
private static DamageSource? GetDamageSource(IPokemon pokemon)
|
|
{
|
|
var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage");
|
|
return call != null ? (DamageSource)call.GetArguments()[1]! : null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "After Bide is selected, the user will be unable to select a move for an idling period".
|
|
/// The first use adds the <see cref="BideEffect"/> volatile to the user, which forces the Bide choice on
|
|
/// the following turns and records incoming damage.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSecondaryEffect_FirstUse_AddsBideEffectToUserVolatile()
|
|
{
|
|
// Arrange
|
|
var (script, move, _, target, userVolatile, _) = CreateTestSetup();
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, target, 0);
|
|
|
|
// Assert
|
|
await Assert.That(userVolatile.Get<BideEffect>()).IsNotNull();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "Bide will now only endure attacks for two turns."
|
|
/// The turn Bide is selected begins the storing period, which spans two more turns; the stored damage is
|
|
/// only released on the third execution, so on the second execution nothing is released yet.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSecondaryEffect_SecondExecution_DoesNotReleaseStoredDamage()
|
|
{
|
|
// Arrange
|
|
var (script, move, user, target, userVolatile, _) = CreateTestSetup();
|
|
var attacker = CreateAttacker();
|
|
_ = AddStoredBideEffect(userVolatile, user, 1, 50, attacker);
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, target, 0);
|
|
|
|
// Assert - still storing, so the attacker has not been damaged
|
|
await Assert.That(ReceivedDamage(attacker)).IsFalse();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "Bide will do damage equal to twice the damage received during the idling period."
|
|
/// </summary>
|
|
[Test, Arguments(50u, 100u), Arguments(1u, 2u), Arguments(123u, 246u)]
|
|
public async Task OnSecondaryEffect_Release_DealsTwiceTheStoredDamage(uint damageTaken, uint expectedDamage)
|
|
{
|
|
// Arrange
|
|
var (script, move, user, target, userVolatile, _) = CreateTestSetup();
|
|
var attacker = CreateAttacker();
|
|
_ = AddStoredBideEffect(userVolatile, user, 2, damageTaken, attacker);
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, target, 0);
|
|
|
|
// Assert
|
|
await Assert.That(GetDamageAmount(attacker)).IsEqualTo(expectedDamage);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "Bide will do damage equal to twice the damage received during the idling period."
|
|
/// The released damage is dealt as move damage, not as indirect damage.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSecondaryEffect_Release_DealsMoveDamage()
|
|
{
|
|
// Arrange
|
|
var (script, move, user, target, userVolatile, _) = CreateTestSetup();
|
|
var attacker = CreateAttacker();
|
|
_ = AddStoredBideEffect(userVolatile, user, 2, 50, attacker);
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, target, 0);
|
|
|
|
// Assert
|
|
await Assert.That(GetDamageSource(attacker)).IsEqualTo(DamageSource.MoveDamage);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "Bide hits the last Pokémon to attack the user, even if this is an ally."
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSecondaryEffect_Release_HitsLastPokemonThatAttackedUser()
|
|
{
|
|
// Arrange
|
|
var (script, move, user, target, userVolatile, _) = CreateTestSetup();
|
|
var firstAttacker = CreateAttacker();
|
|
var lastAttacker = CreateAttacker();
|
|
_ = AddStoredBideEffect(userVolatile, user, 2, 50, firstAttacker, lastAttacker);
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, target, 0);
|
|
|
|
// Assert - only the last attacker is hit
|
|
await Assert.That(ReceivedDamage(lastAttacker)).IsTrue();
|
|
await Assert.That(ReceivedDamage(firstAttacker)).IsFalse();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "this attack will hit the last Pokémon to attack the user".
|
|
/// A Pokémon that has since left the battlefield cannot be hit by the released damage.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSecondaryEffect_Release_AttackerNoLongerOnBattlefield_IsNotDamaged()
|
|
{
|
|
// Arrange
|
|
var (script, move, user, target, userVolatile, _) = CreateTestSetup();
|
|
var departedAttacker = CreateAttacker(false);
|
|
_ = AddStoredBideEffect(userVolatile, user, 2, 50, departedAttacker);
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, target, 0);
|
|
|
|
// Assert
|
|
await Assert.That(ReceivedDamage(departedAttacker)).IsFalse();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "If the user is not directly attacked during the biding period, it will fail on the turn it
|
|
/// would have released."
|
|
/// </summary>
|
|
[Test]
|
|
public void OnSecondaryEffect_ReleaseWithoutBeingAttacked_MoveFails()
|
|
{
|
|
// Arrange
|
|
var (script, move, user, target, userVolatile, hitData) = CreateTestSetup();
|
|
_ = AddStoredBideEffect(userVolatile, user, 2, 0);
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, target, 0);
|
|
|
|
// Assert
|
|
hitData.Received(1).Fail();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "Once a Pokémon has started biding, it cannot choose to switch out until the attack is
|
|
/// complete."
|
|
/// Once the stored damage is released the attack is complete, so the <see cref="BideEffect"/> volatile must
|
|
/// be removed from the user; otherwise the user would remain locked into Bide indefinitely through
|
|
/// <see cref="BideEffect.ForceTurnSelection"/>.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSecondaryEffect_Release_RemovesBideEffectFromUser()
|
|
{
|
|
// Arrange
|
|
var (script, move, user, target, userVolatile, _) = CreateTestSetup();
|
|
var attacker = CreateAttacker();
|
|
_ = AddStoredBideEffect(userVolatile, user, 2, 50, attacker);
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, target, 0);
|
|
|
|
// Assert
|
|
await Assert.That(userVolatile.Get<BideEffect>()).IsNull();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "Bide will do damage equal to twice the damage received during the idling period."
|
|
/// The <see cref="BideEffect"/> volatile accumulates every chunk of damage the user takes while storing
|
|
/// energy.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task BideEffect_OnDamage_AccumulatesDamageTaken()
|
|
{
|
|
// Arrange
|
|
var user = Substitute.For<IPokemon>();
|
|
var effect = new BideEffect(user);
|
|
|
|
// Act - the user drops from 100 to 60 HP, then from 60 to 50 HP
|
|
effect.OnDamage(user, DamageSource.MoveDamage, 100, 60);
|
|
effect.OnDamage(user, DamageSource.MoveDamage, 60, 50);
|
|
|
|
// Assert
|
|
await Assert.That(effect.DamageTaken).IsEqualTo(50u);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "Bide hits the last Pokémon to attack the user, even if this is an ally."
|
|
/// The <see cref="BideEffect"/> volatile records every Pokémon that hits the user, in order, so the last
|
|
/// attacker can be targeted on release.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task BideEffect_OnIncomingHit_RecordsAttacker()
|
|
{
|
|
// Arrange
|
|
var user = Substitute.For<IPokemon>();
|
|
var effect = new BideEffect(user);
|
|
var incomingMove = Substitute.For<IExecutingMove>();
|
|
var attacker = Substitute.For<IPokemon>();
|
|
incomingMove.User.Returns(attacker);
|
|
|
|
// Act
|
|
effect.OnIncomingHit(incomingMove, user, 0);
|
|
|
|
// Assert
|
|
await Assert.That(effect.HitBy.LastOrDefault()).IsEqualTo(attacker);
|
|
}
|
|
} |