Files
PkmnLib.NET/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BideTests.cs
2026-07-05 13:01:12 +02:00

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);
}
}