using PkmnLib.Dynamic.Models; using PkmnLib.Dynamic.Models.Choices; using PkmnLib.Dynamic.ScriptHandling; using PkmnLib.Plugin.Gen7.Scripts.Moves; using PkmnLib.Plugin.Gen7.Scripts.Pokemon; using PkmnLib.Static.Moves; using PkmnLib.Static.Utils; namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// /// Tests for the move script and its attached . /// Behavior is verified against the Bulbapedia page for Baneful Bunker (Generation VII). /// public class BanefulBunkerTests { /// /// Creates a fully mocked setup for driving 's inherited /// . The target of the secondary effect is the /// Pokémon using Baneful Bunker itself, as the move is self-targeted. /// private static (BanefulBunker script, IExecutingMove move, IPokemon target, IHitData hitData, IScriptSet volatileSet ) CreateProtectSetup(bool userMovesLast, float randomRoll) { var script = new BanefulBunker(); var move = Substitute.For(); var target = Substitute.For(); var hitData = Substitute.For(); move.GetHitData(target, 0).Returns(hitData); // A queue with a remaining choice means another Pokémon still has to move after the user. var queue = userMovesLast ? new BattleChoiceQueue([]) : new BattleChoiceQueue([Substitute.For()]); var random = Substitute.For(); random.GetFloat().Returns(randomRoll); var battle = Substitute.For(); battle.ChoiceQueue.Returns(queue); battle.Random.Returns(random); var battleData = Substitute.For(); battleData.Battle.Returns(battle); target.BattleData.Returns(battleData); // Give the mock a real script iterator (used by the volatile add hook) and a real volatile script set. target.GetScripts().Returns(_ => new ScriptIterator(Array.Empty>())); IScriptSet volatileSet = new ScriptSet(target); target.Volatile.Returns(volatileSet); return (script, move, target, hitData, volatileSet); } /// /// Creates a fully mocked setup for driving , the /// volatile script that attaches to its user. /// private static (BanefulBunkerEffect effect, IExecutingMove move, IPokemon target, IPokemon attacker) CreateBlockSetup(bool isContact, bool hasProtectFlag, MoveCategory category = MoveCategory.Physical) { var effect = new BanefulBunkerEffect(); var move = Substitute.For(); var target = Substitute.For(); var hitData = Substitute.For(); hitData.IsContact.Returns(isContact); move.GetHitData(target, 0).Returns(hitData); var useMove = Substitute.For(); useMove.HasFlag(new StringKey("protect")).Returns(hasProtectFlag); useMove.Category.Returns(category); move.UseMove.Returns(useMove); var attacker = Substitute.For(); attacker.GetScripts().Returns(_ => new ScriptIterator(Array.Empty>())); move.User.Returns(attacker); target.BattleData.Returns(Substitute.For()); return (effect, move, target, attacker); } /// /// Helper to extract the status name from a Pokémon's received calls. /// private static string? GetStatusSet(IPokemon pokemon) { var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "SetStatus"); return call != null ? ((StringKey)call.GetArguments()[0]!).ToString() : null; } /// /// Bulbapedia: "Baneful Bunker protects the user from all effects of moves that target it during the turn /// it is used, including damage." /// Using the move attaches the volatile script to the user. /// [Test] public async Task OnSecondaryEffect_FirstUse_AddsBanefulBunkerEffect() { // Arrange var (script, move, target, hitData, volatileSet) = CreateProtectSetup(false, 0.0f); // Act script.OnSecondaryEffect(move, target, 0); // Assert await Assert.That(volatileSet.Get()).IsNotNull(); hitData.DidNotReceive().Fail(); } /// /// Bulbapedia: "If the user goes last in the turn, the move will fail." /// [Test] public async Task OnSecondaryEffect_UserMovesLast_Fails() { // Arrange var (script, move, target, hitData, volatileSet) = CreateProtectSetup(true, 0.0f); // Act script.OnSecondaryEffect(move, target, 0); // Assert hitData.Received(1).Fail(); await Assert.That(volatileSet.Get()).IsNull(); } /// /// Bulbapedia: "The chance that Baneful Bunker will succeed also drops each time the user successfully and /// consecutively uses Endure, any protection move that only affects the user, Quick Guard, or Wide Guard." /// A successful use registers a consecutive protection turn on the . /// [Test] public async Task OnSecondaryEffect_FirstUse_RegistersConsecutiveProtectionUse() { // Arrange var (script, move, target, _, volatileSet) = CreateProtectSetup(false, 0.0f); // Act script.OnSecondaryEffect(move, target, 0); // Assert var failure = volatileSet.Get(); await Assert.That(failure).IsNotNull(); await Assert.That(failure!.ProtectTurns).IsEqualTo(1); await Assert.That(failure.UsedProtect).IsTrue(); } /// /// Bulbapedia: "Each time, the chance of success is divided by 3." /// After one consecutive protection use the chance is 1/3, so a roll above 1/3 makes the move fail. /// [Test] public async Task OnSecondaryEffect_SecondConsecutiveUse_RollAboveOneThird_Fails() { // Arrange var (script, move, target, hitData, volatileSet) = CreateProtectSetup(false, 0.5f); volatileSet.Add(new ProtectionFailureScript { ProtectTurns = 1 }); // Act script.OnSecondaryEffect(move, target, 0); // Assert hitData.Received(1).Fail(); await Assert.That(volatileSet.Get()).IsNull(); } /// /// Bulbapedia: "Each time, the chance of success is divided by 3." /// After one consecutive protection use the chance is 1/3, so a roll below 1/3 still succeeds and /// increments the consecutive use counter. /// [Test] public async Task OnSecondaryEffect_SecondConsecutiveUse_RollBelowOneThird_Succeeds() { // Arrange var (script, move, target, hitData, volatileSet) = CreateProtectSetup(false, 0.2f); var failure = new ProtectionFailureScript { ProtectTurns = 1 }; volatileSet.Add(failure); // Act script.OnSecondaryEffect(move, target, 0); // Assert hitData.DidNotReceive().Fail(); await Assert.That(volatileSet.Get()).IsNotNull(); await Assert.That(failure.ProtectTurns).IsEqualTo(2); } /// /// Bulbapedia: "Baneful Bunker protects the user from all effects of moves that target it during the turn /// it is used, including damage." /// A move that can be protected against (it has the protect flag) is blocked by the /// . /// [Test] public async Task BlockIncomingHit_MoveWithProtectFlag_BlocksHit() { // Arrange var (effect, move, target, _) = CreateBlockSetup(false, true); var block = false; // Act effect.BlockIncomingHit(move, target, 0, ref block); // Assert await Assert.That(block).IsTrue(); } /// /// Bulbapedia: "Baneful Bunker protects the user from all effects of moves that target it"; moves that /// bypass protection (they lack the protect flag) are not blocked by the . /// [Test] public async Task BlockIncomingHit_MoveWithoutProtectFlag_DoesNotBlock() { // Arrange var (effect, move, target, _) = CreateBlockSetup(false, false); var block = false; // Act effect.BlockIncomingHit(move, target, 0, ref block); // Assert await Assert.That(block).IsFalse(); } /// /// Bulbapedia: "If Baneful Bunker blocks an attack that would make contact with the user, the attacker /// becomes poisoned (unless they are immune)." /// [Test] public async Task BlockIncomingHit_BlockedContactMove_PoisonsAttacker() { // Arrange var (effect, move, target, attacker) = CreateBlockSetup(true, true); var block = false; // Act effect.BlockIncomingHit(move, target, 0, ref block); // Assert await Assert.That(GetStatusSet(attacker)).IsEqualTo("poisoned"); } /// /// Bulbapedia: "If Baneful Bunker blocks an attack that would make contact with the user, the attacker /// becomes poisoned (unless they are immune)." /// A blocked attack that does not make contact must not poison the attacker. /// [Test] public async Task BlockIncomingHit_BlockedNonContactMove_DoesNotPoisonAttacker() { // Arrange var (effect, move, target, attacker) = CreateBlockSetup(false, true); var block = false; // Act effect.BlockIncomingHit(move, target, 0, ref block); // Assert await Assert.That(attacker.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "SetStatus")).IsFalse(); } /// /// Bulbapedia: "If Baneful Bunker blocks an attack that would make contact with the user, the attacker /// becomes poisoned (unless they are immune)." /// A contact move that bypasses the protection (it lacks the protect flag, e.g. Shadow Force) is not /// blocked, so the attacker must not be poisoned. /// [Test] public async Task BlockIncomingHit_ContactMoveThatBypassesProtection_DoesNotPoisonAttacker() { // Arrange var (effect, move, target, attacker) = CreateBlockSetup(true, false); var block = false; // Act effect.BlockIncomingHit(move, target, 0, ref block); // Assert - the hit was not blocked, so no poison is applied await Assert.That(block).IsFalse(); await Assert.That(attacker.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "SetStatus")).IsFalse(); } }