This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the <see cref="MagneticFlux"/> move script.
|
||||
/// Gen VII Bulbapedia behavior: "Magnetic Flux raises the Defense and Special Defense stats of allied
|
||||
/// Pokémon (including the user) with the Ability Plus or Minus by one stage each."
|
||||
/// </summary>
|
||||
public class MagneticFluxTests
|
||||
{
|
||||
private static (MagneticFlux script, IExecutingMove move, IPokemon user) CreateTestSetup(
|
||||
params IPokemon?[] sidePokemon)
|
||||
{
|
||||
var script = new MagneticFlux();
|
||||
|
||||
var side = Substitute.For<IBattleSide>();
|
||||
side.Pokemon.Returns(sidePokemon);
|
||||
|
||||
var battleData = Substitute.For<IPokemonBattleData>();
|
||||
battleData.BattleSide.Returns(side);
|
||||
|
||||
var user = Substitute.For<IPokemon>();
|
||||
user.BattleData.Returns(battleData);
|
||||
var move = Substitute.For<IExecutingMove>();
|
||||
move.User.Returns(user);
|
||||
|
||||
return (script, move, user);
|
||||
}
|
||||
|
||||
private static IPokemon CreatePokemonWithAbility(string? abilityName)
|
||||
{
|
||||
var pokemon = Substitute.For<IPokemon>();
|
||||
if (abilityName is null)
|
||||
{
|
||||
pokemon.ActiveAbility.Returns((IAbility?)null);
|
||||
}
|
||||
else
|
||||
{
|
||||
var ability = Substitute.For<IAbility>();
|
||||
ability.Name.Returns(new StringKey(abilityName));
|
||||
pokemon.ActiveAbility.Returns(ability);
|
||||
}
|
||||
|
||||
return pokemon;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper to extract the arguments of all received ChangeStatBoost calls. Received-call inspection is
|
||||
/// used instead of NSubstitute argument matchers because the trailing <c>EventBatchId</c> parameter
|
||||
/// cannot be bound by <c>Arg.Any</c> (its parameterless constructor initializes a fresh id, so it
|
||||
/// never equals the matcher's default value).
|
||||
/// </summary>
|
||||
private static List<object?[]> GetStatBoostCalls(IPokemon pokemon) =>
|
||||
pokemon.ReceivedCalls().Where(c => c.GetMethodInfo().Name == "ChangeStatBoost").Select(c => c.GetArguments())
|
||||
.ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "Magnetic Flux raises the Defense and Special Defense stats of allied Pokémon
|
||||
/// (including the user) with the Ability Plus or Minus by one stage each." An ally with Plus has both
|
||||
/// its Defense and its Special Defense raised by one stage.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnSecondaryEffect_AllyWithPlus_DefenseAndSpecialDefenseRaisedByOneStage()
|
||||
{
|
||||
// Arrange
|
||||
var ally = CreatePokemonWithAbility("plus");
|
||||
var (script, move, _) = CreateTestSetup(ally);
|
||||
|
||||
// Act
|
||||
script.OnSecondaryEffect(move, Substitute.For<IPokemon>(), 0);
|
||||
|
||||
// Assert
|
||||
var calls = GetStatBoostCalls(ally);
|
||||
await Assert.That(calls.Count).IsEqualTo(2);
|
||||
var boostedStats = calls.Select(args => (Statistic)args[0]!).ToList();
|
||||
await Assert.That(boostedStats.Contains(Statistic.Defense)).IsTrue();
|
||||
await Assert.That(boostedStats.Contains(Statistic.SpecialDefense)).IsTrue();
|
||||
await Assert.That(calls.All(args => (sbyte)args[1]! == 1)).IsTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: the boost applies to allied Pokémon "with the Ability Plus or Minus" — an ally with
|
||||
/// Minus is affected just like one with Plus.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnSecondaryEffect_AllyWithMinus_DefenseAndSpecialDefenseRaisedByOneStage()
|
||||
{
|
||||
// Arrange
|
||||
var ally = CreatePokemonWithAbility("minus");
|
||||
var (script, move, _) = CreateTestSetup(ally);
|
||||
|
||||
// Act
|
||||
script.OnSecondaryEffect(move, Substitute.For<IPokemon>(), 0);
|
||||
|
||||
// Assert
|
||||
var calls = GetStatBoostCalls(ally);
|
||||
await Assert.That(calls.Count).IsEqualTo(2);
|
||||
var boostedStats = calls.Select(args => (Statistic)args[0]!).ToList();
|
||||
await Assert.That(boostedStats.Contains(Statistic.Defense)).IsTrue();
|
||||
await Assert.That(boostedStats.Contains(Statistic.SpecialDefense)).IsTrue();
|
||||
await Assert.That(calls.All(args => (sbyte)args[1]! == 1)).IsTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: only allied Pokémon "with the Ability Plus or Minus" are affected — an ally with a
|
||||
/// different ability does not have its stats raised.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnSecondaryEffect_AllyWithOtherAbility_StatsNotRaised()
|
||||
{
|
||||
// Arrange
|
||||
var ally = CreatePokemonWithAbility("levitate");
|
||||
var (script, move, _) = CreateTestSetup(ally);
|
||||
|
||||
// Act
|
||||
script.OnSecondaryEffect(move, Substitute.For<IPokemon>(), 0);
|
||||
|
||||
// Assert
|
||||
await Assert.That(GetStatBoostCalls(ally).Count).IsEqualTo(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: only allied Pokémon "with the Ability Plus or Minus" are affected — an ally without an
|
||||
/// active ability does not have its stats raised, and the script does not throw.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnSecondaryEffect_AllyWithoutActiveAbility_StatsNotRaised()
|
||||
{
|
||||
// Arrange
|
||||
var ally = CreatePokemonWithAbility(null);
|
||||
var (script, move, _) = CreateTestSetup(ally);
|
||||
|
||||
// Act
|
||||
script.OnSecondaryEffect(move, Substitute.For<IPokemon>(), 0);
|
||||
|
||||
// Assert
|
||||
await Assert.That(GetStatBoostCalls(ally).Count).IsEqualTo(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: the boost applies to allied Pokémon "(including the user)" — when the user itself has
|
||||
/// Plus, its own Defense and Special Defense are raised, and the boost counts as self-inflicted.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnSecondaryEffect_UserWithPlus_BoostIsSelfInflicted()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, user) = CreateTestSetup();
|
||||
var ability = Substitute.For<IAbility>();
|
||||
ability.Name.Returns(new StringKey("plus"));
|
||||
user.ActiveAbility.Returns(ability);
|
||||
user.BattleData!.BattleSide.Pokemon.Returns(new IPokemon?[] { user });
|
||||
|
||||
// Act
|
||||
script.OnSecondaryEffect(move, Substitute.For<IPokemon>(), 0);
|
||||
|
||||
// Assert
|
||||
var calls = GetStatBoostCalls(user);
|
||||
await Assert.That(calls.Count).IsEqualTo(2);
|
||||
await Assert.That(calls.All(args => (bool)args[2]!)).IsTrue(); // self-inflicted for the user itself
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "Magnetic Flux raises the Defense and Special Defense stats of allied Pokémon". A boost
|
||||
/// applied to an ally other than the user is not self-inflicted.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnSecondaryEffect_AllyWithPlus_BoostIsNotSelfInflicted()
|
||||
{
|
||||
// Arrange
|
||||
var ally = CreatePokemonWithAbility("plus");
|
||||
var (script, move, _) = CreateTestSetup(ally);
|
||||
|
||||
// Act
|
||||
script.OnSecondaryEffect(move, Substitute.For<IPokemon>(), 0);
|
||||
|
||||
// Assert
|
||||
var calls = GetStatBoostCalls(ally);
|
||||
await Assert.That(calls.Count).IsEqualTo(2);
|
||||
await Assert.That(calls.All(args => (bool)args[2]!)).IsFalse(); // not self-inflicted
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: the boost applies to "allied Pokémon" with Plus or Minus — when multiple eligible
|
||||
/// allies are on the user's side, every one of them is raised.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnSecondaryEffect_MultipleEligibleAllies_AllAreRaised()
|
||||
{
|
||||
// Arrange
|
||||
var plusAlly = CreatePokemonWithAbility("plus");
|
||||
var minusAlly = CreatePokemonWithAbility("minus");
|
||||
var (script, move, _) = CreateTestSetup(plusAlly, minusAlly);
|
||||
|
||||
// Act
|
||||
script.OnSecondaryEffect(move, Substitute.For<IPokemon>(), 0);
|
||||
|
||||
// Assert
|
||||
await Assert.That(GetStatBoostCalls(plusAlly).Count).IsEqualTo(2);
|
||||
await Assert.That(GetStatBoostCalls(minusAlly).Count).IsEqualTo(2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Technical test: empty (null) slots on the user's side are skipped without throwing, and eligible
|
||||
/// allies in other slots are still raised.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnSecondaryEffect_EmptyPokemonSlot_IsSkipped()
|
||||
{
|
||||
// Arrange
|
||||
var ally = CreatePokemonWithAbility("plus");
|
||||
var (script, move, _) = CreateTestSetup(null, ally);
|
||||
|
||||
// Act
|
||||
script.OnSecondaryEffect(move, Substitute.For<IPokemon>(), 0);
|
||||
|
||||
// Assert
|
||||
await Assert.That(GetStatBoostCalls(ally).Count).IsEqualTo(2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Technical test: when the user has no <see cref="IPokemon.BattleData"/> (it is not in a battle),
|
||||
/// the script does nothing instead of throwing.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnSecondaryEffect_NullBattleData_DoesNothing()
|
||||
{
|
||||
// Arrange
|
||||
var script = new MagneticFlux();
|
||||
var user = Substitute.For<IPokemon>();
|
||||
user.BattleData.Returns((IPokemonBattleData?)null);
|
||||
var move = Substitute.For<IExecutingMove>();
|
||||
move.User.Returns(user);
|
||||
|
||||
// Act & Assert - should not throw
|
||||
script.OnSecondaryEffect(move, Substitute.For<IPokemon>(), 0);
|
||||
await Assert.That(GetStatBoostCalls(user).Count).IsEqualTo(0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user