using PkmnLib.Dynamic.Models; using PkmnLib.Dynamic.Models.Choices; using PkmnLib.Dynamic.ScriptHandling; using PkmnLib.Dynamic.ScriptHandling.Registry; using PkmnLib.Plugin.Gen7.Scripts.Moves; using PkmnLib.Plugin.Gen7.Scripts.Pokemon; using PkmnLib.Static; namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// /// Tests for the move script and its . /// Gen VII Bulbapedia behavior: "When Destiny Bond is used, the user gains the Destiny Bound status. /// Until the user moves again, if the user faints as the direct result of an attack, the attacking /// Pokémon also faints." /// public class DestinyBondTests { /// /// Creates a mocked user whose is a real so /// the volatile added by the move can be inspected. /// private static (DestinyBond script, IExecutingMove move, IPokemon user, ScriptSet userVolatile) CreateTestSetup() { var script = new DestinyBond(); var move = Substitute.For(); var user = Substitute.For(); var userVolatile = new ScriptSet(user); user.Volatile.Returns(userVolatile); user.GetScripts().Returns(_ => new ScriptIterator(new List>())); move.User.Returns(user); return (script, move, user, userVolatile); } /// /// Creates a fainting Pokémon whose battle's is a move /// choice made by the given attacker. /// private static IPokemon CreateFaintingPokemonAttackedBy(IPokemon attacker) { var moveChoice = Substitute.For(); moveChoice.User.Returns(attacker); var queue = new BattleChoiceQueue([moveChoice]); queue.Dequeue(); var battle = Substitute.For(); battle.ChoiceQueue.Returns(queue); var battleData = Substitute.For(); battleData.Battle.Returns(battle); var pokemon = Substitute.For(); pokemon.BattleData.Returns(battleData); return pokemon; } /// /// Bulbapedia: "When Destiny Bond is used, the user gains the Destiny Bound status." The move adds the /// volatile script to the user. /// [Test] public async Task OnSecondaryEffect_Used_AddsDestinyBondEffectToUser() { // Arrange var (script, move, _, userVolatile) = CreateTestSetup(); var target = Substitute.For(); // Act script.OnSecondaryEffect(move, target, 0); // Assert await Assert.That(userVolatile.Contains(ScriptUtils.ResolveName())).IsTrue(); } /// /// Bulbapedia: "if the user faints as the direct result of an attack, the attacking Pokémon also /// faints." When the Destiny Bound user faints from move damage, the attacker takes damage far in /// excess of its HP, guaranteeing the faint. /// [Test] public async Task OnFaint_FaintsFromMoveDamage_AttackerIsDamagedToFaint() { // Arrange var effect = new DestinyBondEffect(); var attacker = Substitute.For(); attacker.BoostedStats.Returns(new StatisticSet(100, 1, 1, 1, 1, 1)); var pokemon = CreateFaintingPokemonAttackedBy(attacker); // Act effect.OnFaint(pokemon, DamageSource.MoveDamage); // Assert - the attacker is damaged by well over its max HP (10x) so it always faints var damageCall = attacker.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage"); await Assert.That(damageCall).IsNotNull(); await Assert.That((uint)damageCall!.GetArguments()[0]!).IsEqualTo(1000u); } /// /// Bulbapedia: Destiny Bond "bypasses protective abilities like Focus Sash and Sturdy". The faint /// damage is dealt with force, so scripts cannot reduce or prevent it. /// [Test] public async Task OnFaint_FaintsFromMoveDamage_DamageIsForced() { // Arrange var effect = new DestinyBondEffect(); var attacker = Substitute.For(); attacker.BoostedStats.Returns(new StatisticSet(100, 1, 1, 1, 1, 1)); var pokemon = CreateFaintingPokemonAttackedBy(attacker); // Act effect.OnFaint(pokemon, DamageSource.MoveDamage); // Assert - the forceDamage argument (last parameter) is true var damageCall = attacker.ReceivedCalls().First(c => c.GetMethodInfo().Name == "Damage"); await Assert.That((bool)damageCall.GetArguments()[3]!).IsTrue(); } /// /// Bulbapedia: "The move doesn't activate from indirect damage like weather or Leech Seed." /// Fainting from a non-move damage source does not take the attacker down. /// [Test] public async Task OnFaint_FaintsFromIndirectDamage_AttackerIsNotDamaged() { // Arrange var effect = new DestinyBondEffect(); var attacker = Substitute.For(); attacker.BoostedStats.Returns(new StatisticSet(100, 1, 1, 1, 1, 1)); var pokemon = CreateFaintingPokemonAttackedBy(attacker); // Act effect.OnFaint(pokemon, DamageSource.Misc); // Assert await Assert.That(attacker.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Damage")).IsFalse(); } /// /// Bulbapedia: "Until the user moves again" — the Destiny Bound status wears off when the user makes /// its next move, so the effect removes itself before the user's move. /// [Test] public async Task OnBeforeMove_UserMovesAgain_RemovesDestinyBondEffect() { // Arrange var user = Substitute.For(); var userVolatile = new ScriptSet(user); user.GetScripts().Returns(_ => new ScriptIterator(new List>())); var effect = new DestinyBondEffect(); userVolatile.Add(effect); var move = Substitute.For(); // Act effect.OnBeforeMove(move); // Assert await Assert.That(userVolatile.Contains(ScriptUtils.ResolveName())).IsFalse(); } /// /// Technical test: fainting without battle data (outside of battle) does nothing and does not throw. /// [Test] public async Task OnFaint_NoBattleData_DoesNotThrow() { // Arrange var effect = new DestinyBondEffect(); var pokemon = Substitute.For(); pokemon.BattleData.Returns((IPokemonBattleData?)null); // Act & Assert await Assert.That(() => effect.OnFaint(pokemon, DamageSource.MoveDamage)).ThrowsNothing(); } }