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();
}
}