Files
PkmnLib.NET/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/ChangeTargetSpecialDefenseTests.cs
Deukhoofd f9878e76ba
All checks were successful
Build / Build (push) Successful in 1m7s
Adds more tests, fixes
2026-07-04 13:03:54 +02:00

202 lines
7.6 KiB
C#

using PkmnLib.Dynamic.Models;
using PkmnLib.Plugin.Gen7.Scripts.Moves;
using PkmnLib.Static;
using PkmnLib.Static.Utils;
using TUnit.Assertions.AssertConditions.Throws;
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>();
}
}