using PkmnLib.Dynamic.Models; using PkmnLib.Plugin.Gen7.Scripts.Moves; using PkmnLib.Static; using PkmnLib.Static.Species; using PkmnLib.Static.Utils; namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// /// Tests for the move script. /// Gen VII Bulbapedia behavior: "Gear Up raises the Attack and Special Attack stats of allied Pokémon /// (including the user) with the Ability Plus or Minus by one stage each." /// public class GearUpTests { /// /// Creates a fully mocked test setup for Gear Up tests. The user is on side 0; the given side lists /// are installed as the battle's sides. /// private static (GearUp script, IExecutingMove move, IPokemon user) CreateTestSetup( params IReadOnlyList[] sidePokemon) { var script = new GearUp(); var move = Substitute.For(); var battle = Substitute.For(); var sides = sidePokemon.Select(pokemon => { var side = Substitute.For(); side.Pokemon.Returns(pokemon); return side; }).ToArray(); battle.Sides.Returns(sides); var user = Substitute.For(); var battleData = Substitute.For(); battleData.Battle.Returns(battle); battleData.SideIndex.Returns((byte)0); user.BattleData.Returns(battleData); move.User.Returns(user); return (script, move, user); } /// /// Creates a mocked Pokémon whose has the given name, or no /// ability at all when is null. /// private static IPokemon CreatePokemon(string? abilityName) { var pokemon = Substitute.For(); if (abilityName != null) { var ability = Substitute.For(); ability.Name.Returns(new StringKey(abilityName)); pokemon.ActiveAbility.Returns(ability); } else { pokemon.ActiveAbility.Returns((IAbility?)null); } return pokemon; } /// /// Helper to extract the arguments of the received ChangeStatBoost call for the given statistic, or /// null when that stat was not boosted. Received-call inspection is used instead of NSubstitute /// argument matchers because the trailing EventBatchId parameter cannot be bound by Arg.Any. /// private static object?[]? GetStatBoostArgs(IPokemon pokemon, Statistic stat) => pokemon.ReceivedCalls().Where(c => c.GetMethodInfo().Name == "ChangeStatBoost").Select(c => c.GetArguments()) .FirstOrDefault(args => (Statistic)args[0]! == stat); /// /// Bulbapedia: "Gear Up raises the Attack and Special Attack stats of allied Pokémon ... with the /// Ability Plus ... by one stage each." An ally's boost is not self-inflicted. /// [Test] public async Task OnSecondaryEffect_AllyWithPlus_AttackAndSpecialAttackRaisedByOneStage() { // Arrange var ally = CreatePokemon("plus"); var (script, move, _) = CreateTestSetup(new List { ally }); // Act script.OnSecondaryEffect(move, Substitute.For(), 0); // Assert var attackArgs = GetStatBoostArgs(ally, Statistic.Attack); var specialAttackArgs = GetStatBoostArgs(ally, Statistic.SpecialAttack); await Assert.That(attackArgs).IsNotNull(); await Assert.That((sbyte)attackArgs![1]!).IsEqualTo((sbyte)1); await Assert.That((bool)attackArgs[2]!).IsFalse(); // not self-inflicted await Assert.That(specialAttackArgs).IsNotNull(); await Assert.That((sbyte)specialAttackArgs![1]!).IsEqualTo((sbyte)1); } /// /// Bulbapedia: the boost also applies to allies "with the Ability ... Minus". /// [Test] public async Task OnSecondaryEffect_AllyWithMinus_AttackAndSpecialAttackRaisedByOneStage() { // Arrange var ally = CreatePokemon("minus"); var (script, move, _) = CreateTestSetup(new List { ally }); // Act script.OnSecondaryEffect(move, Substitute.For(), 0); // Assert await Assert.That(GetStatBoostArgs(ally, Statistic.Attack)).IsNotNull(); await Assert.That(GetStatBoostArgs(ally, Statistic.SpecialAttack)).IsNotNull(); } /// /// Bulbapedia: the boost applies to allied Pokémon "including the user" — a user with Plus boosts /// itself, and that boost is self-inflicted. /// [Test] public async Task OnSecondaryEffect_UserHasPlus_UserBoostIsSelfInflicted() { // Arrange var (script, move, user) = CreateTestSetup(); var ability = Substitute.For(); ability.Name.Returns(new StringKey("plus")); user.ActiveAbility.Returns(ability); var side = Substitute.For(); side.Pokemon.Returns(new List { user }); user.BattleData!.Battle.Sides.Returns(new[] { side }); // Act script.OnSecondaryEffect(move, Substitute.For(), 0); // Assert var attackArgs = GetStatBoostArgs(user, Statistic.Attack); await Assert.That(attackArgs).IsNotNull(); await Assert.That((bool)attackArgs![2]!).IsTrue(); // self-inflicted for the user itself } /// /// Bulbapedia: only allies "with the Ability Plus or Minus" are raised — an ally with a different /// Ability is unaffected. /// [Test] public async Task OnSecondaryEffect_AllyWithOtherAbility_NotBoosted() { // Arrange var ally = CreatePokemon("levitate"); var (script, move, _) = CreateTestSetup(new List { ally }); // Act script.OnSecondaryEffect(move, Substitute.For(), 0); // Assert await Assert.That(ally.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ChangeStatBoost")).IsFalse(); } /// /// Bulbapedia: the boost applies to "allied Pokémon" — an opposing Pokémon with Plus is not raised. /// [Test] public async Task OnSecondaryEffect_OpponentWithPlus_NotBoosted() { // Arrange var opponent = CreatePokemon("plus"); var (script, move, _) = CreateTestSetup(new List(), new List { opponent }); // Act script.OnSecondaryEffect(move, Substitute.For(), 0); // Assert await Assert.That(opponent.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ChangeStatBoost")).IsFalse(); } /// /// Technical test: an ally without an active Ability is skipped without throwing. /// [Test] public async Task OnSecondaryEffect_AllyWithoutAbility_NotBoosted() { // Arrange var ally = CreatePokemon(null); var (script, move, _) = CreateTestSetup(new List { ally }); // Act script.OnSecondaryEffect(move, Substitute.For(), 0); // Assert await Assert.That(ally.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ChangeStatBoost")).IsFalse(); } /// /// Technical test: empty (null) slots on the user's side are skipped without throwing. /// [Test] public async Task OnSecondaryEffect_EmptyPokemonSlot_IsSkipped() { // Arrange var ally = CreatePokemon("plus"); var (script, move, _) = CreateTestSetup(new List { null, ally }); // Act script.OnSecondaryEffect(move, Substitute.For(), 0); // Assert await Assert.That(GetStatBoostArgs(ally, Statistic.Attack)).IsNotNull(); } /// /// Technical test: when the user has no (it is not in a battle), /// the script does nothing instead of throwing. /// [Test] public async Task OnSecondaryEffect_NullBattleData_DoesNothing() { // Arrange var ally = CreatePokemon("plus"); var (script, move, user) = CreateTestSetup(new List { ally }); user.BattleData.Returns((IPokemonBattleData?)null); // Act script.OnSecondaryEffect(move, Substitute.For(), 0); // Assert await Assert.That(ally.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ChangeStatBoost")).IsFalse(); } }