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; /// /// Tests for the move script and its 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". /// 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(); var target = Substitute.For(); var hitData = Substitute.For(); move.GetHitData(target, 0).Returns(hitData); var user = Substitute.For(); user.GetScripts().Returns(_ => new ScriptIterator(new List>())); // 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(); var battleData = Substitute.For(); battleData.IsOnBattlefield.Returns(onBattlefield); attacker.BattleData.Returns(battleData); return attacker; } /// /// Adds a to the user's volatile scripts, as if Bide has already been storing /// energy for the given number of executed turns. /// 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; } /// /// Helper to check whether a Pokémon received any Damage call. /// private static bool ReceivedDamage(IPokemon pokemon) => pokemon.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Damage"); /// /// Helper to extract the damage amount from a Pokémon's received Damage calls. /// private static uint? GetDamageAmount(IPokemon pokemon) { var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage"); return call != null ? (uint)call.GetArguments()[0]! : null; } /// /// Helper to extract the damage source from a Pokémon's received Damage calls. /// private static DamageSource? GetDamageSource(IPokemon pokemon) { var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage"); return call != null ? (DamageSource)call.GetArguments()[1]! : null; } /// /// Bulbapedia: "After Bide is selected, the user will be unable to select a move for an idling period". /// The first use adds the volatile to the user, which forces the Bide choice on /// the following turns and records incoming damage. /// [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()).IsNotNull(); } /// /// 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. /// [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(); } /// /// Bulbapedia: "Bide will do damage equal to twice the damage received during the idling period." /// [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); } /// /// 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. /// [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); } /// /// Bulbapedia: "Bide hits the last Pokémon to attack the user, even if this is an ally." /// [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(); } /// /// 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. /// [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(); } /// /// Bulbapedia: "If the user is not directly attacked during the biding period, it will fail on the turn it /// would have released." /// [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(); } /// /// 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 volatile must /// be removed from the user; otherwise the user would remain locked into Bide indefinitely through /// . /// [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()).IsNull(); } /// /// Bulbapedia: "Bide will do damage equal to twice the damage received during the idling period." /// The volatile accumulates every chunk of damage the user takes while storing /// energy. /// [Test] public async Task BideEffect_OnDamage_AccumulatesDamageTaken() { // Arrange var user = Substitute.For(); 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); } /// /// Bulbapedia: "Bide hits the last Pokémon to attack the user, even if this is an ally." /// The volatile records every Pokémon that hits the user, in order, so the last /// attacker can be targeted on release. /// [Test] public async Task BideEffect_OnIncomingHit_RecordsAttacker() { // Arrange var user = Substitute.For(); var effect = new BideEffect(user); var incomingMove = Substitute.For(); var attacker = Substitute.For(); incomingMove.User.Returns(attacker); // Act effect.OnIncomingHit(incomingMove, user, 0); // Assert await Assert.That(effect.HitBy.LastOrDefault()).IsEqualTo(attacker); } }