using PkmnLib.Dynamic.Events; 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.Utils; namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// /// Tests for the move script. /// Gen VII Bulbapedia behavior (Generation IV base text, unchanged through Gen VII): "Ice Fang deals damage /// and has a 10% chance of freezing the opponent. It also has an independent 10% chance of causing the target /// to flinch, if the user attacks before the target." /// public class IceFangTests { /// /// Creates a fully mocked test setup for Ice Fang tests. The target has not yet moved this turn when /// contains a choice for it. /// private static (IceFang script, IExecutingMove move, IBattlePokemon user, IBattlePokemon target, IBattleRandom random, IScriptSet targetVolatile) CreateTestSetup(BattleChoiceQueue? queue) { var script = new IceFang(); var move = Substitute.For(); var user = Substitute.For(); move.User.Returns(user); var random = Substitute.For(); var battle = Substitute.For(); battle.Random.Returns(random); battle.ChoiceQueue.Returns(queue); var target = Substitute.For(); var targetVolatile = Substitute.For(); target.Battle.Returns(battle); target.Volatile.Returns(targetVolatile); return (script, move, user, target, random, targetVolatile); } /// /// Creates a choice queue containing a single yet-to-execute move choice for the given Pokémon. /// private static BattleChoiceQueue CreateQueueWithChoiceFor(IBattlePokemon pokemon) { var choice = Substitute.For(); choice.User.Returns(pokemon); return new BattleChoiceQueue([choice]); } /// /// Helper to check whether a Pokémon received any SetStatus call. (Checked via ReceivedCalls, as arg /// matchers cannot be used for parameters: its parameterless constructor /// generates a random Guid, which breaks NSubstitute's argument specification binding.) /// private static bool ReceivedSetStatus(IBattlePokemon pokemon) => pokemon.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "SetStatus"); /// /// Bulbapedia: "Ice Fang deals damage and has a 10% chance of freezing the opponent." /// When the freeze roll succeeds, the target is frozen by the user. /// [Test] public void OnSecondaryEffect_FreezeRollSucceeds_TargetIsFrozen() { // Arrange var (script, move, user, target, random, _) = CreateTestSetup(new BattleChoiceQueue([])); random.EffectChance(10, move, target, 0).Returns(true); // Act script.OnSecondaryEffect(move, target, 0); // Assert target.Received(1).SetStatus("frozen", user); } /// /// Bulbapedia: "has a 10% chance of freezing the opponent." /// The freeze roll is made with a 10 percent chance. /// [Test] public void OnSecondaryEffect_FreezeRoll_UsesTenPercentChance() { // Arrange var (script, move, _, target, random, _) = CreateTestSetup(new BattleChoiceQueue([])); // Act script.OnSecondaryEffect(move, target, 0); // Assert random.Received(1).EffectChance(10, move, target, 0); } /// /// Bulbapedia: "has a 10% chance of freezing the opponent." /// When the freeze roll fails, the target is not frozen. /// [Test] public async Task OnSecondaryEffect_FreezeRollFails_TargetIsNotFrozen() { // Arrange var (script, move, _, target, random, _) = CreateTestSetup(new BattleChoiceQueue([])); random.EffectChance(10, move, target, 0).Returns(false); // Act script.OnSecondaryEffect(move, target, 0); // Assert await Assert.That(ReceivedSetStatus(target)).IsFalse(); } /// /// Bulbapedia: "It also has an independent 10% chance of causing the target to flinch, if the user /// attacks before the target." /// The target still has a queued choice this turn (so the user attacked first), and the flinch roll /// succeeds: a is added to the target. /// [Test] public void OnSecondaryEffect_UserAttacksBeforeTargetAndFlinchRollSucceeds_TargetFlinches() { // Arrange var (script, move, _, target, random, targetVolatile) = CreateTestSetup(null); var queue = CreateQueueWithChoiceFor(target); target.Battle.ChoiceQueue.Returns(queue); random.EffectChance(10, move, target, 0).Returns(true, true); // Act script.OnSecondaryEffect(move, target, 0); // Assert targetVolatile.Received(1).Add(Arg.Any()); } /// /// Bulbapedia: "It also has an independent 10% chance of causing the target to flinch". /// The flinch chance is independent of the freeze chance: even when the freeze roll fails, a successful /// flinch roll still causes the flinch. /// [Test] public async Task OnSecondaryEffect_FreezeRollFailsButFlinchRollSucceeds_TargetStillFlinches() { // Arrange var (script, move, _, target, random, targetVolatile) = CreateTestSetup(null); var queue = CreateQueueWithChoiceFor(target); target.Battle.ChoiceQueue.Returns(queue); // First roll (freeze) fails, second roll (flinch) succeeds. random.EffectChance(10, move, target, 0).Returns(false, true); // Act script.OnSecondaryEffect(move, target, 0); // Assert await Assert.That(ReceivedSetStatus(target)).IsFalse(); targetVolatile.Received(1).Add(Arg.Any()); } /// /// Bulbapedia: the flinch can only happen "if the user attacks before the target." /// The target has already moved this turn (no queued choice for it remains), so no flinch is applied /// even though the rolls succeed. /// [Test] public void OnSecondaryEffect_TargetAlreadyMoved_TargetDoesNotFlinch() { // Arrange var (script, move, _, target, random, targetVolatile) = CreateTestSetup(null); // The queue only holds a choice for some other Pokémon; the target's choice already executed. var queue = CreateQueueWithChoiceFor(Substitute.For()); target.Battle.ChoiceQueue.Returns(queue); random.EffectChance(10, move, target, 0).Returns(true, true); // Act script.OnSecondaryEffect(move, target, 0); // Assert targetVolatile.DidNotReceive().Add(Arg.Any