using PkmnLib.Dynamic.Libraries; 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.Moves; using PkmnLib.Static.Utils; namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// /// Tests for the move script and its . /// Gen VII Bulbapedia behavior: "Disable temporarily prevents the target from using a specific move"; /// "Disable now disables the last move that the target used", failing "if the target hasn't used a move /// yet", and "the effect lasts [...] 4 turns from Generation V onward." /// public class DisableTests { /// /// Creates a mocked move choice whose chosen move has the given name. /// private static IMoveChoice CreateMoveChoice(string moveName) { var moveData = Substitute.For(); moveData.Name.Returns(new StringKey(moveName)); var learnedMove = Substitute.For(); learnedMove.MoveData.Returns(moveData); var choice = Substitute.For(); choice.ChosenMove.Returns(learnedMove); return choice; } /// /// Creates a fully mocked test setup where the target's volatile scripts are a real /// and its last used move is the given one (or none). /// private static (Disable script, IExecutingMove move, IPokemon target, ScriptSet targetVolatile, IHitData hitData) CreateTestSetup(string? lastUsedMove, bool lastMoveIsStruggle = false) { var script = new Disable(); var move = Substitute.For(); var user = Substitute.For(); var userBattleData = Substitute.For(); user.BattleData.Returns(userBattleData); move.User.Returns(user); var target = Substitute.For(); var targetVolatile = new ScriptSet(target); target.Volatile.Returns(targetVolatile); target.GetScripts().Returns(_ => new ScriptIterator(new List>())); var targetBattleData = Substitute.For(); // Create the choice before the Returns call; configuring substitutes inside Returns is not allowed. var lastMoveChoice = lastUsedMove == null ? null : CreateMoveChoice(lastUsedMove); targetBattleData.LastMoveChoice.Returns(lastMoveChoice); target.BattleData.Returns(targetBattleData); // Struggle is recognized through the misc library's replacement choice check, not by name. var miscLibrary = Substitute.For(); var library = Substitute.For(); library.MiscLibrary.Returns(miscLibrary); target.Library.Returns(library); if (lastMoveChoice != null) miscLibrary.IsReplacementChoice(lastMoveChoice).Returns(lastMoveIsStruggle); var hitData = Substitute.For(); move.GetHitData(target, 0).Returns(hitData); return (script, move, target, targetVolatile, hitData); } /// /// Bulbapedia: "Disable now disables the last move that the target used." The target gains the /// volatile script. /// [Test] public async Task OnSecondaryEffect_TargetUsedMove_AddsDisableEffectToTarget() { // Arrange var (script, move, target, targetVolatile, _) = CreateTestSetup("tackle"); // Act script.OnSecondaryEffect(move, target, 0); // Assert await Assert.That(targetVolatile.Contains(ScriptUtils.ResolveName())).IsTrue(); } /// /// Bulbapedia: "It fails if the target hasn't used a move yet". Without a last move choice on the /// target, the hit fails and no disable is applied. /// [Test] public async Task OnSecondaryEffect_TargetHasNotUsedMove_FailsHit() { // Arrange var (script, move, target, targetVolatile, hitData) = CreateTestSetup(null); // Act script.OnSecondaryEffect(move, target, 0); // Assert hitData.Received(1).Fail(); await Assert.That(targetVolatile.Contains(ScriptUtils.ResolveName())).IsFalse(); } /// /// Bulbapedia: "Disable fails if a move is already disabled". Using Disable on a target that already /// has a disabled move must fail the hit rather than silently doing nothing. /// [Test] public async Task OnSecondaryEffect_TargetAlreadyDisabled_FailsHit() { // Arrange var (script, move, target, targetVolatile, hitData) = CreateTestSetup("tackle"); targetVolatile.Add(new DisableEffect(new StringKey("growl"))); // Act script.OnSecondaryEffect(move, target, 0); // Assert hitData.Received(1).Fail(); await Assert.That(hitData.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Fail")).IsTrue(); } /// /// Bulbapedia: "It fails if the target hasn't used a move yet or used Struggle". A target whose last /// move was Struggle (a replacement choice per ) cannot /// have it disabled. /// [Test] public async Task OnSecondaryEffect_TargetLastUsedStruggle_FailsHit() { // Arrange var (script, move, target, targetVolatile, hitData) = CreateTestSetup("struggle", true); // Act script.OnSecondaryEffect(move, target, 0); // Assert hitData.Received(1).Fail(); await Assert.That(targetVolatile.Contains(ScriptUtils.ResolveName())).IsFalse(); } /// /// Technical test: without battle data on the user (outside of battle) the effect does nothing and /// does not throw. /// [Test] public async Task OnSecondaryEffect_UserHasNoBattleData_DoesNothing() { // Arrange var (script, move, target, targetVolatile, _) = CreateTestSetup("tackle"); move.User.BattleData.Returns((IPokemonBattleData?)null); // Act script.OnSecondaryEffect(move, target, 0); // Assert await Assert.That(targetVolatile.Contains(ScriptUtils.ResolveName())).IsFalse(); } /// /// Bulbapedia: "Disable temporarily prevents the target from using a specific move." Selecting the /// disabled move is prevented by the . /// [Test] public async Task PreventMoveSelection_ChoiceIsDisabledMove_PreventsSelection() { // Arrange var effect = new DisableEffect(new StringKey("tackle")); var choice = CreateMoveChoice("tackle"); var prevent = false; // Act effect.PreventMoveSelection(choice, ref prevent); // Assert await Assert.That(prevent).IsTrue(); } /// /// Bulbapedia: only the disabled move is prevented; the target's other moves remain selectable. /// [Test] public async Task PreventMoveSelection_ChoiceIsOtherMove_DoesNotPreventSelection() { // Arrange var effect = new DisableEffect(new StringKey("tackle")); var choice = CreateMoveChoice("growl"); var prevent = false; // Act effect.PreventMoveSelection(choice, ref prevent); // Assert await Assert.That(prevent).IsFalse(); } /// /// Bulbapedia: "The effect lasts [...] 4 turns from Generation V onward." After three end-of-turn /// ticks the effect is still active. /// [Test] public async Task OnEndTurn_ThreeTurnsPassed_EffectStillActive() { // Arrange var target = Substitute.For(); var targetVolatile = new ScriptSet(target); target.GetScripts().Returns(_ => new ScriptIterator(new List>())); var effect = new DisableEffect(new StringKey("tackle")); targetVolatile.Add(effect); var battle = Substitute.For(); // Act for (var i = 0; i < 3; i++) effect.OnEndTurn(target, battle); // Assert await Assert.That(targetVolatile.Contains(ScriptUtils.ResolveName())).IsTrue(); } /// /// Bulbapedia: "The effect lasts [...] 4 turns from Generation V onward." After the fourth /// end-of-turn tick the effect removes itself. /// [Test] public async Task OnEndTurn_FourTurnsPassed_EffectRemovesItself() { // Arrange var target = Substitute.For(); var targetVolatile = new ScriptSet(target); target.GetScripts().Returns(_ => new ScriptIterator(new List>())); var effect = new DisableEffect(new StringKey("tackle")); targetVolatile.Add(effect); var battle = Substitute.For(); // Act for (var i = 0; i < 4; i++) effect.OnEndTurn(target, battle); // Assert await Assert.That(targetVolatile.Contains(ScriptUtils.ResolveName())).IsFalse(); } }