201 lines
7.5 KiB
C#
201 lines
7.5 KiB
C#
using PkmnLib.Dynamic.Models;
|
|
using PkmnLib.Plugin.Gen7.Scripts.Moves;
|
|
using PkmnLib.Static;
|
|
using PkmnLib.Static.Utils;
|
|
|
|
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
|
|
|
|
/// <summary>
|
|
/// Tests for the <see cref="ChangeTargetSpecialDefense"/> script, which implements the secondary effect of Acid.
|
|
/// Behavior is verified against the Bulbapedia page for Acid.
|
|
/// Gen VII behavior: "Acid inflicts damage to all adjacent opponents" and "has a 10% chance of lowering the
|
|
/// target's Special Defense stat by one stage". The 10% proc chance and the targeting are handled by the
|
|
/// generic secondary effect data (chance: 10 in Moves.jsonc); this script is responsible for applying the
|
|
/// stat change once the effect procs.
|
|
/// </summary>
|
|
public class ChangeTargetSpecialDefenseTests
|
|
{
|
|
private static ChangeTargetSpecialDefense CreateInitializedScript(int amount = -1)
|
|
{
|
|
var script = new ChangeTargetSpecialDefense();
|
|
script.OnInitialize(new Dictionary<StringKey, object?> { { "amount", amount } });
|
|
return script;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper to extract the arguments of the ChangeStatBoost call received by a substitute target.
|
|
/// </summary>
|
|
private static object?[]? GetStatBoostCallArgs(IPokemon target)
|
|
{
|
|
var call = target.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "ChangeStatBoost");
|
|
return call?.GetArguments();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "Acid now has a 10% chance of lowering the target's Special Defense stat by one stage instead."
|
|
/// (Generation IV onwards.) When the secondary effect procs, the target's Special Defense is lowered by one stage.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSecondaryEffect_TargetIsOpponent_LowersSpecialDefenseByOneStage()
|
|
{
|
|
// Arrange
|
|
var script = CreateInitializedScript();
|
|
var move = Substitute.For<IExecutingMove>();
|
|
var user = Substitute.For<IPokemon>();
|
|
move.User.Returns(user);
|
|
var target = Substitute.For<IPokemon>();
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, target, 0);
|
|
|
|
// Assert
|
|
var args = GetStatBoostCallArgs(target);
|
|
await Assert.That(args).IsNotNull();
|
|
await Assert.That((Statistic)args![0]!).IsEqualTo(Statistic.SpecialDefense);
|
|
await Assert.That((sbyte)args[1]!).IsEqualTo((sbyte)-1);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia (Generation IV onwards): "lowering the target's Special Defense stat by one stage instead."
|
|
/// The word "instead" marks the change from the Gen I/II Defense drop — the script must not touch
|
|
/// the target's (physical) Defense stat.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSecondaryEffect_TargetIsOpponent_DoesNotLowerDefense()
|
|
{
|
|
// Arrange
|
|
var script = CreateInitializedScript();
|
|
var move = Substitute.For<IExecutingMove>();
|
|
var user = Substitute.For<IPokemon>();
|
|
move.User.Returns(user);
|
|
var target = Substitute.For<IPokemon>();
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, target, 0);
|
|
|
|
// Assert - exactly one stat change, and it is not Defense
|
|
var calls = target.ReceivedCalls().Where(c => c.GetMethodInfo().Name == "ChangeStatBoost").ToList();
|
|
await Assert.That(calls.Count).IsEqualTo(1);
|
|
await Assert.That((Statistic)calls[0].GetArguments()[0]!).IsNotEqualTo(Statistic.Defense);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "lowering the target's Special Defense stat".
|
|
/// The stat drop is inflicted on an opponent, so it must not be marked as self-inflicted
|
|
/// (self-inflicted drops bypass effects that prevent stat lowering by opponents, e.g. Clear Body/Mist).
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSecondaryEffect_TargetIsOpponent_NotSelfInflicted()
|
|
{
|
|
// Arrange
|
|
var script = CreateInitializedScript();
|
|
var move = Substitute.For<IExecutingMove>();
|
|
var user = Substitute.For<IPokemon>();
|
|
move.User.Returns(user);
|
|
var target = Substitute.For<IPokemon>();
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, target, 0);
|
|
|
|
// Assert
|
|
var args = GetStatBoostCallArgs(target);
|
|
await Assert.That(args).IsNotNull();
|
|
await Assert.That((bool)args![2]!).IsFalse();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "a 10% chance of lowering the target's Special Defense stat".
|
|
/// The drop is an ordinary stat change, not a forced one — it must respect prevention effects,
|
|
/// so the script may not pass force = true.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSecondaryEffect_TargetIsOpponent_StatDropIsNotForced()
|
|
{
|
|
// Arrange
|
|
var script = CreateInitializedScript();
|
|
var move = Substitute.For<IExecutingMove>();
|
|
var user = Substitute.For<IPokemon>();
|
|
move.User.Returns(user);
|
|
var target = Substitute.For<IPokemon>();
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, target, 0);
|
|
|
|
// Assert
|
|
var args = GetStatBoostCallArgs(target);
|
|
await Assert.That(args).IsNotNull();
|
|
await Assert.That((bool)args![3]!).IsFalse();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Technical test: the shared ChangeTargetStats base marks the stat change as self-inflicted
|
|
/// when the target of the secondary effect is the move's own user.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSecondaryEffect_TargetIsUser_SelfInflicted()
|
|
{
|
|
// Arrange
|
|
var script = CreateInitializedScript();
|
|
var move = Substitute.For<IExecutingMove>();
|
|
var user = Substitute.For<IPokemon>();
|
|
move.User.Returns(user);
|
|
|
|
// Act - the user is hit by its own move (e.g. redirected)
|
|
script.OnSecondaryEffect(move, user, 0);
|
|
|
|
// Assert
|
|
var args = GetStatBoostCallArgs(user);
|
|
await Assert.That(args).IsNotNull();
|
|
await Assert.That((bool)args![2]!).IsTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Technical test: the 'amount' parameter from the move data (amount: -1 for Acid in Moves.jsonc)
|
|
/// is parsed by OnInitialize and passed through to the stat change unmodified.
|
|
/// </summary>
|
|
[Test, Arguments(-1), Arguments(-2)]
|
|
public async Task OnInitialize_AmountParameter_PassedToStatBoost(int amount)
|
|
{
|
|
// Arrange
|
|
var script = CreateInitializedScript(amount);
|
|
var move = Substitute.For<IExecutingMove>();
|
|
var user = Substitute.For<IPokemon>();
|
|
move.User.Returns(user);
|
|
var target = Substitute.For<IPokemon>();
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, target, 0);
|
|
|
|
// Assert
|
|
var args = GetStatBoostCallArgs(target);
|
|
await Assert.That(args).IsNotNull();
|
|
await Assert.That((sbyte)args![1]!).IsEqualTo((sbyte)amount);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Technical test: OnInitialize rejects a null parameter dictionary.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnInitialize_NullParameters_ThrowsArgumentNullException()
|
|
{
|
|
// Arrange
|
|
var script = new ChangeTargetSpecialDefense();
|
|
|
|
// Act & Assert
|
|
await Assert.That(() => script.OnInitialize(null)).ThrowsExactly<ArgumentNullException>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Technical test: OnInitialize rejects parameters without the required 'amount' entry.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnInitialize_MissingAmount_ThrowsArgumentException()
|
|
{
|
|
// Arrange
|
|
var script = new ChangeTargetSpecialDefense();
|
|
|
|
// Act & Assert
|
|
await Assert.That(() => script.OnInitialize(new Dictionary<StringKey, object?>()))
|
|
.ThrowsExactly<ArgumentException>();
|
|
}
|
|
} |