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,221 @@
using PkmnLib.Dynamic.Libraries;
using PkmnLib.Dynamic.Models;
using PkmnLib.Dynamic.ScriptHandling;
using PkmnLib.Plugin.Gen7.Scripts.Moves;
using PkmnLib.Plugin.Gen7.Scripts.Pokemon;
using PkmnLib.Static;
using PkmnLib.Static.Libraries;
using PkmnLib.Static.Utils;
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
/// <summary>
/// Tests for the <see cref="Foresight"/> move script.
/// Behavior is verified against the Bulbapedia page for Foresight (Generation VII behavior: the
/// Generation II base effect with the Generation III, IV and V-to-VII changes applied).
/// The lasting part of the effect is implemented by the <see cref="ForesightEffect"/> volatile script,
/// which the move script is responsible for attaching to the target.
/// </summary>
public class ForesightTests
{
/// <summary>
/// Creates a fully mocked test setup for Foresight tests. The target is in battle, with a mocked
/// battle library chain and the given evasion stat stage.
/// </summary>
private static (Foresight script, IExecutingMove move, IPokemon target, IScriptSet volatileSet, IHitData hitData)
CreateTestSetup(sbyte evasionStage = 0)
{
var script = new Foresight();
var move = Substitute.For<IExecutingMove>();
var target = Substitute.For<IPokemon>();
var hitData = Substitute.For<IHitData>();
move.GetHitData(target, 0).Returns(hitData);
var typeLibrary = Substitute.For<IReadOnlyTypeLibrary>();
var staticLibrary = Substitute.For<IStaticLibrary>();
staticLibrary.Types.Returns(typeLibrary);
var library = Substitute.For<IDynamicLibrary>();
library.StaticLibrary.Returns(staticLibrary);
var battle = Substitute.For<IBattle>();
battle.Library.Returns(library);
var battleData = Substitute.For<IPokemonBattleData>();
battleData.Battle.Returns(battle);
target.BattleData.Returns(battleData);
var volatileSet = Substitute.For<IScriptSet>();
target.Volatile.Returns(volatileSet);
target.StatBoost.Returns(new StatBoostStatisticSet
{
Evasion = evasionStage,
});
return (script, move, target, volatileSet, hitData);
}
/// <summary>
/// Helper to extract all stat boost changes requested on the target through
/// <see cref="IPokemon.ChangeStatBoost"/>.
/// </summary>
private static IReadOnlyList<(Statistic stat, sbyte amount)> GetStatBoostChanges(IPokemon target) =>
target.ReceivedCalls().Where(c => c.GetMethodInfo().Name == "ChangeStatBoost")
.Select(c => ((Statistic)c.GetArguments()[0]!, (sbyte)c.GetArguments()[1]!)).ToList();
/// <summary>
/// Bulbapedia: "Foresight also removes the Ghost type's immunity to Fighting and Normal moves for any
/// moves used against the target. This effect remains until the target switches out".
/// The lasting effect is implemented by attaching a <see cref="ForesightEffect"/> volatile script
/// to the target.
/// </summary>
[Test]
public async Task OnSecondaryEffect_TargetInBattle_AddsForesightEffectToTargetVolatile()
{
// Arrange
var (script, move, target, volatileSet, _) = CreateTestSetup();
// Act
script.OnSecondaryEffect(move, target, 0);
// Assert
var addCall = volatileSet.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Add");
await Assert.That(addCall).IsNotNull();
await Assert.That(addCall!.GetArguments()[0] is ForesightEffect).IsTrue();
}
/// <summary>
/// Bulbapedia (Generation III): "Foresight now causes all accuracy checks against the target to ignore
/// any changes to the target's evasion stat stages."
/// The engine models this by resetting a raised evasion stat stage back to zero (and preventing further
/// changes through <see cref="ForesightEffect"/>).
/// </summary>
[Test, Arguments((sbyte)1, (sbyte)-1), Arguments((sbyte)3, (sbyte)-3), Arguments((sbyte)6, (sbyte)-6)]
public async Task OnSecondaryEffect_PositiveEvasionStage_EvasionStageResetToZero(sbyte evasionStage,
sbyte expectedChange)
{
// Arrange
var (script, move, target, _, _) = CreateTestSetup(evasionStage);
// Act
script.OnSecondaryEffect(move, target, 0);
// Assert
var changes = GetStatBoostChanges(target);
await Assert.That(changes.Count).IsEqualTo(1);
await Assert.That(changes[0].stat).IsEqualTo(Statistic.Evasion);
await Assert.That(changes[0].amount).IsEqualTo(expectedChange);
}
/// <summary>
/// Bulbapedia (Generation IV): "Foresight now only causes accuracy checks against the target to ignore
/// changes to its evasion stat stages if its evasion stat stage is greater than 0."
/// A lowered evasion stat stage therefore remains in effect: Foresight must not raise it back to zero.
/// </summary>
[Test, Arguments((sbyte)-1), Arguments((sbyte)-6)]
public async Task OnSecondaryEffect_NegativeEvasionStage_EvasionNotRaised(sbyte evasionStage)
{
// Arrange
var (script, move, target, _, _) = CreateTestSetup(evasionStage);
// Act
script.OnSecondaryEffect(move, target, 0);
// Assert - a negative evasion stage may not be raised back towards zero
var changes = GetStatBoostChanges(target);
await Assert.That(changes.Any(c => c.stat == Statistic.Evasion && c.amount > 0)).IsFalse();
}
/// <summary>
/// Bulbapedia (Generation III): "Foresight now causes all accuracy checks against the target to ignore
/// any changes to the target's evasion stat stages."
/// With an unchanged (zero) evasion stat stage there is nothing to reset, so no effective stat change
/// may be requested.
/// </summary>
[Test]
public async Task OnSecondaryEffect_ZeroEvasionStage_NoEffectiveEvasionChange()
{
// Arrange
var (script, move, target, _, _) = CreateTestSetup();
// Act
script.OnSecondaryEffect(move, target, 0);
// Assert
var changes = GetStatBoostChanges(target);
await Assert.That(changes.Any(c => c.amount != 0)).IsFalse();
}
/// <summary>
/// If the target has no <see cref="IPokemon.BattleData"/>, the script cannot resolve the type library,
/// so nothing may happen: no volatile script is added and no stat stage is changed.
/// </summary>
[Test]
public async Task OnSecondaryEffect_NullBattleData_NoEffect()
{
// Arrange
var (script, move, target, volatileSet, _) = CreateTestSetup(2);
target.BattleData.Returns((IPokemonBattleData?)null);
// Act
script.OnSecondaryEffect(move, target, 0);
// Assert
await Assert.That(volatileSet.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Add")).IsFalse();
await Assert.That(target.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ChangeStatBoost")).IsFalse();
}
/// <summary>
/// Bulbapedia (Generations V to VII): "Foresight will once again fail if used against a Pokémon already
/// under its effect."
/// When the target already has the <see cref="ForesightEffect"/> volatile script, the hit must fail.
/// </summary>
[Test]
public async Task OnSecondaryEffect_TargetAlreadyUnderEffect_HitFails()
{
// Arrange
var (script, move, target, volatileSet, hitData) = CreateTestSetup();
volatileSet.Contains<ForesightEffect>().Returns(true);
volatileSet.Contains(Arg.Any<StringKey>()).Returns(true);
// Act
script.OnSecondaryEffect(move, target, 0);
// Assert
await Assert.That(hitData.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Fail")).IsTrue();
}
/// <summary>
/// Bulbapedia (Generation III): "Foresight now causes all accuracy checks against the target to ignore
/// any changes to the target's evasion stat stages."
/// For the evasion reset to actually take effect in a real battle, it must not be blocked by the move's
/// own <see cref="ForesightEffect"/>: that script's <c>PreventStatBoostChange</c> hook prevents all
/// evasion changes once attached. The reset must therefore either happen before the volatile script is
/// added to the target, or be requested with <c>force: true</c>.
/// </summary>
[Test]
public async Task OnSecondaryEffect_PositiveEvasionStage_ResetNotBlockedByOwnEffect()
{
// Arrange
var (script, move, target, volatileSet, _) = CreateTestSetup(2);
var callOrder = new List<string>();
volatileSet.Add(null!).ReturnsForAnyArgs(ci =>
{
callOrder.Add("Add");
return (ScriptContainer?)null;
});
target.ChangeStatBoost(default, default, default, default).ReturnsForAnyArgs(ci =>
{
callOrder.Add(ci.ArgAt<bool>(3) ? "ChangeStatBoost:forced" : "ChangeStatBoost");
return true;
});
// Act
script.OnSecondaryEffect(move, target, 0);
// Assert - the evasion reset must be able to take effect: either it happens before the
// ForesightEffect volatile (which prevents evasion changes) is attached, or it is forced.
var boostIndex = callOrder.FindIndex(c => c.StartsWith("ChangeStatBoost"));
var addIndex = callOrder.IndexOf("Add");
await Assert.That(boostIndex).IsNotEqualTo(-1);
await Assert.That(addIndex).IsNotEqualTo(-1);
await Assert.That(callOrder.Contains("ChangeStatBoost:forced") || boostIndex < addIndex).IsTrue();
}
}