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.Common; 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: "Encore temporarily prevents the target from using any move except the /// last used move." It fails if the target hasn't used a move yet or its last move was Struggle, and /// from Generation V onward the duration is "exactly 3 turns". /// public class EncoreTests { /// /// Creates a mocked move choice whose chosen move has the given name, optionally carrying the /// flag. /// private static IMoveChoice CreateMoveChoice(string moveName, bool cantRepeat = false) { var moveData = Substitute.For(); moveData.Name.Returns(new StringKey(moveName)); moveData.HasFlag(MoveFlags.CantRepeat).Returns(cantRepeat); 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 (Encore script, IExecutingMove move, IPokemon target, IScriptSet targetVolatile, IHitData hitData) CreateTestSetup(string? lastUsedMove, bool lastMoveIsReplacement = false, bool lastMoveCantRepeat = false) { var script = new Encore(); var move = Substitute.For(); // 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); var battle = Substitute.For(); battle.Library.Returns(library); var target = Substitute.For(); target.GetScripts().Returns(_ => new ScriptIterator(Array.Empty>())); IScriptSet targetVolatile = new ScriptSet(target); target.Volatile.Returns(targetVolatile); 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, lastMoveCantRepeat); targetBattleData.LastMoveChoice.Returns(lastMoveChoice); targetBattleData.Battle.Returns(battle); target.BattleData.Returns(targetBattleData); if (lastMoveChoice != null) miscLibrary.IsReplacementChoice(lastMoveChoice).Returns(lastMoveIsReplacement); var hitData = Substitute.For(); move.GetHitData(target, 0).Returns(hitData); return (script, move, target, targetVolatile, hitData); } /// /// Bulbapedia: "Encore temporarily prevents the target from using any move except the last used /// move." The target gains the volatile script. /// [Test] public async Task OnSecondaryEffect_TargetUsedMove_AddsEncoreEffectToTarget() { // Arrange var (script, move, target, targetVolatile, hitData) = CreateTestSetup("tackle"); // Act script.OnSecondaryEffect(move, target, 0); // Assert await Assert.That(targetVolatile.Contains(ScriptUtils.ResolveName())).IsTrue(); hitData.DidNotReceive().Fail(); } /// /// Bulbapedia: "It also fails if the foe hasn't used a move yet." Without a last move choice on the /// target, the hit fails and no encore 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: Encore fails if the target's last move was Struggle. A last move that is a replacement /// choice (per ) cannot be encored. /// [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(); } /// /// Bulbapedia: Encore fails "if the opponent's last move was Transform, Mimic, Sketch, Mirror Move, /// Encore itself, Struggle, or a move with no PP remaining." These moves carry the /// flag in the move data, which makes Encore fail. /// [Test, Arguments("encore"), Arguments("transform"), Arguments("mimic"), Arguments("sketch"), Arguments("mirror_move")] public async Task OnSecondaryEffect_TargetLastUsedUnrepeatableMove_FailsHit(string lastUsedMove) { // Arrange var (script, move, target, targetVolatile, hitData) = CreateTestSetup(lastUsedMove, lastMoveCantRepeat: true); // Act script.OnSecondaryEffect(move, target, 0); // Assert hitData.Received(1).Fail(); await Assert.That(targetVolatile.Contains(ScriptUtils.ResolveName())).IsFalse(); } /// /// Bulbapedia: Encore fails "if the opponent's last move was Transform, Mimic, Sketch, Mirror Move, /// Encore itself" — integration check: the real Gen7 move data marks exactly these moves with the /// flag. /// [Test, Arguments("encore"), Arguments("transform"), Arguments("mimic"), Arguments("sketch"), Arguments("mirror_move")] public async Task MoveData_UnrepeatableMove_HasCantRepeatFlag(string moveName) { // Arrange var library = LibraryHelpers.LoadLibrary(); // Act var found = library.StaticLibrary.Moves.TryGet(new StringKey(moveName), out var moveData); // Assert await Assert.That(found).IsTrue(); await Assert.That(moveData!.HasFlag(MoveFlags.CantRepeat)).IsTrue(); } /// /// Technical test: without battle data on the target (outside of battle) the effect does nothing and /// does not throw. /// [Test] public async Task OnSecondaryEffect_TargetHasNoBattleData_DoesNothing() { // Arrange var (script, move, target, targetVolatile, hitData) = CreateTestSetup("tackle"); target.BattleData.Returns((IPokemonBattleData?)null); // Act script.OnSecondaryEffect(move, target, 0); // Assert hitData.DidNotReceive().Fail(); await Assert.That(targetVolatile.Contains(ScriptUtils.ResolveName())).IsFalse(); } /// /// Bulbapedia: "Duration standardized to exactly 3 turns" (Generation V onward). After two /// end-of-turn ticks the effect is still active. /// [Test] public async Task OnEndTurn_TwoTurnsPassed_EffectStillActive() { // Arrange var owner = Substitute.For(); owner.GetScripts().Returns(_ => new ScriptIterator(Array.Empty>())); IScriptSet ownerVolatile = new ScriptSet(owner); var effect = new EncoreEffect(owner, new StringKey("tackle"), 3); ownerVolatile.Add(effect); // Act for (var i = 0; i < 2; i++) effect.OnEndTurn(owner, Substitute.For()); // Assert await Assert.That(ownerVolatile.Contains(ScriptUtils.ResolveName())).IsTrue(); } /// /// Bulbapedia: "Duration standardized to exactly 3 turns" (Generation V onward). After the third /// end-of-turn tick the effect removes itself. /// [Test] public async Task OnEndTurn_ThreeTurnsPassed_EffectRemovesItself() { // Arrange var owner = Substitute.For(); owner.GetScripts().Returns(_ => new ScriptIterator(Array.Empty>())); IScriptSet ownerVolatile = new ScriptSet(owner); var effect = new EncoreEffect(owner, new StringKey("tackle"), 3); ownerVolatile.Add(effect); // Act for (var i = 0; i < 3; i++) effect.OnEndTurn(owner, Substitute.For()); // Assert await Assert.That(ownerVolatile.Contains(ScriptUtils.ResolveName())).IsFalse(); } }