Many more tests and fixes
All checks were successful
Build / Build (push) Successful in 3m12s

This commit is contained in:
2026-08-27 17:11:12 +02:00
parent 32b3ef9c4a
commit d2a82b5fe3
204 changed files with 20255 additions and 242 deletions

View File

@@ -0,0 +1,294 @@
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.MoveVolatile;
using PkmnLib.Static.Moves;
using PkmnLib.Static.Utils;
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
/// <summary>
/// Tests for the <see cref="MeFirst"/> move script and its <see cref="MeFirstPowerBoost"/> volatile.
/// Gen VII Bulbapedia behavior: "If the target has not made its move this turn, but has selected a
/// damage-dealing move, Me First copies that move preemptively and increases the power by 50%."
/// "Me First will fail if the target selected a non-damaging move, or if the target already executed its
/// move this turn."
/// </summary>
public class MeFirstTests
{
/// <summary>
/// Creates a substitute move choice for the target, queued with the given move name and category.
/// </summary>
private static IMoveChoice CreateTargetMoveChoice(IPokemon target, string moveName,
MoveCategory category = MoveCategory.Physical)
{
var moveData = Substitute.For<IMoveData>();
moveData.Name.Returns(new StringKey(moveName));
moveData.Category.Returns(category);
var learnedMove = Substitute.For<ILearnedMove>();
learnedMove.MoveData.Returns(moveData);
var moveChoice = Substitute.For<IMoveChoice>();
moveChoice.User.Returns(target);
moveChoice.ChosenMove.Returns(learnedMove);
return moveChoice;
}
/// <summary>
/// Creates a fully mocked test setup for Me First tests. The user's move choice targets side 0,
/// position 0, where <paramref name="target"/> is standing (or nothing, when null). The battle's
/// choice queue holds the given remaining choices for this turn.
/// </summary>
private static (MeFirst script, IMoveChoice choice) CreateTestSetup(IPokemon? target, ITurnChoice[] queuedChoices,
bool targetChoiceIsReplacement = false)
{
var script = new MeFirst();
var side = Substitute.For<IBattleSide>();
side.Pokemon.Returns(new List<IPokemon?> { target });
var miscLibrary = Substitute.For<IMiscLibrary>();
miscLibrary.IsReplacementChoice(Arg.Any<ITurnChoice>()).Returns(targetChoiceIsReplacement);
var library = Substitute.For<IDynamicLibrary>();
library.MiscLibrary.Returns(miscLibrary);
var battle = Substitute.For<IBattle>();
battle.Sides.Returns(new[] { side });
battle.ChoiceQueue.Returns(new BattleChoiceQueue(queuedChoices));
battle.Library.Returns(library);
var battleData = Substitute.For<IPokemonBattleData>();
battleData.Battle.Returns(battle);
var user = Substitute.For<IPokemon>();
user.BattleData.Returns(battleData);
var choice = Substitute.For<IMoveChoice>();
choice.User.Returns(user);
choice.TargetSide.Returns((byte)0);
choice.TargetPosition.Returns((byte)0);
// Use a real script set so the volatile script added by Me First can be inspected afterwards.
choice.Volatile.Returns(new ScriptSet(choice));
choice.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));
return (script, choice);
}
/// <summary>
/// Bulbapedia: "If the target has not made its move this turn, but has selected a damage-dealing move,
/// Me First copies that move preemptively".
/// The executed move is replaced by the move the target has queued.
/// </summary>
[Test]
public async Task ChangeMove_TargetSelectedDamagingMove_ChangesMoveToTargetsMove()
{
// Arrange
var target = Substitute.For<IPokemon>();
var targetChoice = CreateTargetMoveChoice(target, "tackle");
var (script, choice) = CreateTestSetup(target, [targetChoice]);
StringKey moveName = "me_first";
// Act
script.ChangeMove(choice, ref moveName);
// Assert
await Assert.That(moveName).IsEqualTo(new StringKey("tackle"));
choice.DidNotReceive().Fail();
}
/// <summary>
/// Bulbapedia: "Me First copies that move preemptively and increases the power by 50%."
/// Copying the move attaches the <see cref="MeFirstPowerBoost"/> volatile to the move choice, which is
/// responsible for the power increase.
/// </summary>
[Test]
public async Task ChangeMove_TargetSelectedDamagingMove_AddsPowerBoostVolatile()
{
// Arrange
var target = Substitute.For<IPokemon>();
var targetChoice = CreateTargetMoveChoice(target, "tackle");
var (script, choice) = CreateTestSetup(target, [targetChoice]);
StringKey moveName = "me_first";
// Act
script.ChangeMove(choice, ref moveName);
// Assert
await Assert.That(choice.Volatile.Contains(ScriptUtils.ResolveName<MeFirstPowerBoost>())).IsTrue();
}
/// <summary>
/// Bulbapedia: "Me First copies that move preemptively and increases the power by 50%."
/// The <see cref="MeFirstPowerBoost"/> volatile multiplies the copied move's base power by 1.5,
/// truncating fractions and capping at <see cref="ushort.MaxValue"/>.
/// </summary>
[Test, Arguments((ushort)100, (ushort)150), Arguments((ushort)60, (ushort)90), Arguments((ushort)5, (ushort)7),
Arguments((ushort)85, (ushort)127), Arguments(ushort.MaxValue, ushort.MaxValue)]
public async Task ChangeBasePower_MeFirstPowerBoost_IncreasesPowerByFiftyPercent(ushort basePower, ushort expected)
{
// Arrange
var boost = new MeFirstPowerBoost();
var move = Substitute.For<IExecutingMove>();
var target = Substitute.For<IPokemon>();
// Act
boost.ChangeBasePower(move, target, 0, ref basePower);
// Assert
await Assert.That(basePower).IsEqualTo(expected);
}
/// <summary>
/// Bulbapedia: "Me First will fail if the target selected a non-damaging move, or if the target already
/// executed its move this turn."
/// The target's choice is no longer in the queue, so the move fails.
/// </summary>
[Test]
public async Task ChangeMove_TargetAlreadyExecutedMove_Fails()
{
// Arrange
var target = Substitute.For<IPokemon>();
var (script, choice) = CreateTestSetup(target, []);
StringKey moveName = "me_first";
// Act
script.ChangeMove(choice, ref moveName);
// Assert
choice.Received(1).Fail();
await Assert.That(moveName).IsEqualTo(new StringKey("me_first"));
}
/// <summary>
/// Bulbapedia: "Unlike other moves, if the target of Me First is a vacant slot or a Pokémon that already
/// fainted before the Me First user's turn, Me First will fail instead of being redirected to an adjacent
/// non-fainted opponent."
/// </summary>
[Test]
public async Task ChangeMove_TargetSlotIsVacant_Fails()
{
// Arrange
var (script, choice) = CreateTestSetup(null, []);
StringKey moveName = "me_first";
// Act
script.ChangeMove(choice, ref moveName);
// Assert
choice.Received(1).Fail();
await Assert.That(moveName).IsEqualTo(new StringKey("me_first"));
}
/// <summary>
/// Bulbapedia: Me First only copies a move the target "has selected". A target that chose to switch out
/// instead of using a move has no selected move to copy, so Me First fails.
/// </summary>
[Test]
public async Task ChangeMove_TargetChoiceIsNotAMoveChoice_Fails()
{
// Arrange
var target = Substitute.For<IPokemon>();
var switchChoice = Substitute.For<ISwitchChoice>();
switchChoice.User.Returns(target);
var (script, choice) = CreateTestSetup(target, [switchChoice]);
StringKey moveName = "me_first";
// Act
script.ChangeMove(choice, ref moveName);
// Assert
choice.Received(1).Fail();
await Assert.That(moveName).IsEqualTo(new StringKey("me_first"));
}
/// <summary>
/// Bulbapedia: "Me First will fail if the target chose a Z-Move or any of the following moves."
/// Struggle is one of those moves; a replacement choice (the forced choice used when a Pokémon cannot
/// select a move, usually Struggle) cannot be copied.
/// </summary>
[Test]
public async Task ChangeMove_TargetChoiceIsReplacementChoice_Fails()
{
// Arrange
var target = Substitute.For<IPokemon>();
var targetChoice = CreateTargetMoveChoice(target, "struggle");
var (script, choice) = CreateTestSetup(target, [targetChoice], true);
StringKey moveName = "me_first";
// Act
script.ChangeMove(choice, ref moveName);
// Assert
choice.Received(1).Fail();
await Assert.That(moveName).IsEqualTo(new StringKey("me_first"));
}
/// <summary>
/// Technical test: outside of battle (no battle data) the script returns without throwing and without
/// changing the move.
/// </summary>
[Test]
public async Task ChangeMove_NoBattleData_DoesNotChangeMove()
{
// Arrange
var script = new MeFirst();
var user = Substitute.For<IPokemon>();
user.BattleData.Returns((IPokemonBattleData?)null);
var choice = Substitute.For<IMoveChoice>();
choice.User.Returns(user);
StringKey moveName = "me_first";
// Act
script.ChangeMove(choice, ref moveName);
// Assert
await Assert.That(moveName).IsEqualTo(new StringKey("me_first"));
}
/// <summary>
/// Bulbapedia: "Me First will fail if the target selected a non-damaging move, or if the target already
/// executed its move this turn."
/// A queued status move must make Me First fail instead of being copied.
/// </summary>
[Test]
public async Task ChangeMove_TargetSelectedNonDamagingMove_Fails()
{
// Arrange
var target = Substitute.For<IPokemon>();
var targetChoice = CreateTargetMoveChoice(target, "swords_dance", MoveCategory.Status);
var (script, choice) = CreateTestSetup(target, [targetChoice]);
StringKey moveName = "me_first";
// Act
script.ChangeMove(choice, ref moveName);
// Assert
choice.Received(1).Fail();
await Assert.That(moveName).IsEqualTo(new StringKey("me_first"));
}
/// <summary>
/// Bulbapedia: "Me First will fail if the target chose a Z-Move or any of the following moves." In
/// Generation VII the listed moves are Beak Blast, Belch, Chatter, Counter, Covet, Focus Punch, Me First,
/// Metal Burst, Mirror Coat, Shell Trap, Struggle and Thief.
/// </summary>
[Test, Arguments("beak_blast"), Arguments("belch"), Arguments("chatter"), Arguments("counter"), Arguments("covet"),
Arguments("focus_punch"), Arguments("metal_burst"), Arguments("mirror_coat"), Arguments("shell_trap"),
Arguments("thief")]
public async Task ChangeMove_TargetSelectedUncopyableMove_Fails(string targetMove)
{
// Arrange
var target = Substitute.For<IPokemon>();
var targetChoice = CreateTargetMoveChoice(target, targetMove);
var (script, choice) = CreateTestSetup(target, [targetChoice]);
StringKey moveName = "me_first";
// Act
script.ChangeMove(choice, ref moveName);
// Assert
choice.Received(1).Fail();
await Assert.That(moveName).IsEqualTo(new StringKey("me_first"));
}
}