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,267 @@
using PkmnLib.Dynamic.Models;
using PkmnLib.Dynamic.ScriptHandling;
using PkmnLib.Dynamic.ScriptHandling.Registry;
using PkmnLib.Plugin.Gen7.Scripts;
using PkmnLib.Plugin.Gen7.Scripts.Moves;
using PkmnLib.Plugin.Gen7.Scripts.Side;
using PkmnLib.Static.Moves;
using PkmnLib.Static.Utils;
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
/// <summary>
/// Tests for the <see cref="LightScreen"/> move script and its side effect script
/// <see cref="LightScreenEffect"/>.
/// Gen VII Bulbapedia behavior: Light Screen halves the damage the user's side takes from special moves
/// for 5 turns; "When multiple Pokémon are present on the user's side, special damage reduces by one-third
/// instead of one-half." and "If Light Clay is held when Light Screen is used, it will extend the duration
/// of Light Screen from 5 to 8 turns."
/// </summary>
public class LightScreenTests
{
/// <summary>
/// Test helper script that extends the Light Screen duration through the custom trigger, the same way
/// the Light Clay item script does.
/// </summary>
[Script(ScriptCategory.Pokemon, "test_light_screen_duration_extender")]
private class DurationExtender : Script, IScriptCustomTrigger
{
public void CustomTrigger(StringKey eventName, ICustomTriggerArgs args)
{
if (eventName == CustomTriggers.LightScreenNumberOfTurns &&
args is CustomTriggers.LightScreenNumberOfTurnsArgs d)
d.Duration = 8;
}
}
private static (LightScreen script, IExecutingMove move, IScriptSet sideScripts) CreateMoveSetup()
{
var script = new LightScreen();
var move = Substitute.For<IExecutingMove>();
move.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));
var sideScripts = Substitute.For<IScriptSet>();
// Mimic the real ScriptSet: invoke the factory and hand back a container holding the new script.
sideScripts.StackOrAdd(Arg.Any<StringKey>(), Arg.Any<Func<Script?>>())
.Returns(ci => new ScriptContainer(ci.Arg<Func<Script?>>()()!));
var side = Substitute.For<IBattleSide>();
side.VolatileScripts.Returns(sideScripts);
var battleData = Substitute.For<IPokemonBattleData>();
battleData.BattleSide.Returns(side);
var user = Substitute.For<IPokemon>();
user.BattleData.Returns(battleData);
move.User.Returns(user);
return (script, move, sideScripts);
}
/// <summary>
/// Helper to extract the effect script instance created through StackOrAdd on the side's volatile
/// scripts.
/// </summary>
private static LightScreenEffect? GetAddedEffect(IScriptSet sideScripts)
{
var call = sideScripts.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "StackOrAdd");
if (call == null)
return null;
var factory = (Func<Script?>)call.GetArguments()[1]!;
return factory() as LightScreenEffect;
}
/// <summary>
/// Hosts the given effect in a real <see cref="ScriptSet"/> and counts how many end-of-turn ticks it
/// survives, up to the given maximum.
/// </summary>
private static int CountSurvivedEndTurns(LightScreenEffect effect, int maximum)
{
var owner = Substitute.For<IBattleSide>();
owner.GetScripts().Returns(_ => new ScriptIterator(Array.Empty<IEnumerable<ScriptContainer>>()));
IScriptSet set = new ScriptSet(owner);
set.Add(effect);
for (var turns = 0; turns < maximum; turns++)
{
effect.OnEndTurn(owner, Substitute.For<IBattle>());
if (!set.Contains(ScriptUtils.ResolveName<LightScreenEffect>()))
return turns + 1;
}
return maximum + 1;
}
/// <summary>
/// Creates a mocked incoming move of the given category for <see cref="LightScreenEffect"/> damage
/// tests, with the defending target in a battle with the given amount of positions per side.
/// </summary>
private static (IExecutingMove move, IPokemon target) CreateIncomingMove(MoveCategory category,
byte positionsPerSide, bool isCritical = false)
{
var move = Substitute.For<IExecutingMove>();
var moveData = Substitute.For<IMoveData>();
moveData.Category.Returns(category);
move.UseMove.Returns(moveData);
var target = Substitute.For<IPokemon>();
var hitData = Substitute.For<IHitData>();
hitData.IsCritical.Returns(isCritical);
move.GetHitData(target, 0).Returns(hitData);
var battle = Substitute.For<IBattle>();
battle.PositionsPerSide.Returns(positionsPerSide);
var battleData = Substitute.For<IPokemonBattleData>();
battleData.Battle.Returns(battle);
target.BattleData.Returns(battleData);
return (move, target);
}
/// <summary>
/// Using Light Screen places a <see cref="LightScreenEffect"/> on the user's side of the field.
/// </summary>
[Test]
public async Task OnSecondaryEffect_UserInBattle_AddsLightScreenEffectToUserSide()
{
// Arrange
var (script, move, sideScripts) = CreateMoveSetup();
// Act
script.OnSecondaryEffect(move, Substitute.For<IPokemon>(), 0);
// Assert
await Assert.That(GetAddedEffect(sideScripts)).IsNotNull();
}
/// <summary>
/// Bulbapedia (Generation II): Light Screen "remains in effect for 5 turns" — the effect placed on the
/// side expires at the end of the fifth turn.
/// </summary>
[Test]
public async Task OnSecondaryEffect_UserInBattle_EffectLastsFiveTurns()
{
// Arrange
var (script, move, sideScripts) = CreateMoveSetup();
// Act
script.OnSecondaryEffect(move, Substitute.For<IPokemon>(), 0);
// Assert
var effect = GetAddedEffect(sideScripts);
await Assert.That(CountSurvivedEndTurns(effect!, 10)).IsEqualTo(5);
}
/// <summary>
/// Bulbapedia: "If Light Clay is held when Light Screen is used, it will extend the duration of Light
/// Screen from 5 to 8 turns." The extension flows through the LightScreenNumberOfTurns custom trigger;
/// a script that sets the duration to 8 results in an effect lasting 8 turns.
/// </summary>
[Test]
public async Task OnSecondaryEffect_DurationExtendedByTrigger_EffectLastsEightTurns()
{
// Arrange
var (script, move, sideScripts) = CreateMoveSetup();
move.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>
{
new ScriptContainer(new DurationExtender()),
}));
// Act
script.OnSecondaryEffect(move, Substitute.For<IPokemon>(), 0);
// Assert
var effect = GetAddedEffect(sideScripts);
await Assert.That(CountSurvivedEndTurns(effect!, 10)).IsEqualTo(8);
}
/// <summary>
/// Technical test: outside of battle (no <see cref="IPokemon.BattleData"/>) the hook does nothing.
/// </summary>
[Test]
public async Task OnSecondaryEffect_NoBattleData_DoesNothing()
{
// Arrange
var script = new LightScreen();
var move = Substitute.For<IExecutingMove>();
var user = Substitute.For<IPokemon>();
user.BattleData.Returns((IPokemonBattleData?)null);
move.User.Returns(user);
// Act
script.OnSecondaryEffect(move, Substitute.For<IPokemon>(), 0);
// Assert - the script returns before running the duration trigger over the move's scripts
await Assert.That(move.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "GetScripts")).IsFalse();
}
/// <summary>
/// Bulbapedia (Generation III): Light Screen "halves damage from special attacks" — in a single battle
/// the damage of an incoming special move is halved, with integer truncation.
/// </summary>
[Test, Arguments(100u, 50u), Arguments(101u, 50u), Arguments(99u, 49u)]
public async Task ChangeIncomingMoveDamage_SpecialMoveSingleBattle_DamageHalved(uint damage, uint expectedDamage)
{
// Arrange
var effect = new LightScreenEffect(5);
var (move, target) = CreateIncomingMove(MoveCategory.Special, 1);
// Act
effect.ChangeIncomingMoveDamage(move, target, 0, ref damage);
// Assert
await Assert.That(damage).IsEqualTo(expectedDamage);
}
/// <summary>
/// Bulbapedia: "When multiple Pokémon are present on the user's side, special damage reduces by
/// one-third instead of one-half." — with more than one position per side the damage is multiplied
/// by 2/3.
/// </summary>
[Test, Arguments(90u, 60u), Arguments(100u, 66u)]
public async Task ChangeIncomingMoveDamage_SpecialMoveMultiBattle_DamageReducedByOneThird(uint damage,
uint expectedDamage)
{
// Arrange
var effect = new LightScreenEffect(5);
var (move, target) = CreateIncomingMove(MoveCategory.Special, 2);
// Act
effect.ChangeIncomingMoveDamage(move, target, 0, ref damage);
// Assert
await Assert.That(damage).IsEqualTo(expectedDamage);
}
/// <summary>
/// Light Screen only reduces damage from special moves — physical move damage is unchanged.
/// </summary>
[Test]
public async Task ChangeIncomingMoveDamage_PhysicalMove_DamageUnchanged()
{
// Arrange
var effect = new LightScreenEffect(5);
var (move, target) = CreateIncomingMove(MoveCategory.Physical, 1);
var damage = 100u;
// Act
effect.ChangeIncomingMoveDamage(move, target, 0, ref damage);
// Assert
await Assert.That(damage).IsEqualTo(100u);
}
/// <summary>
/// Critical hits ignore Light Screen — a critical special move deals full damage through the screen.
/// </summary>
[Test]
public async Task ChangeIncomingMoveDamage_CriticalHit_DamageUnchanged()
{
// Arrange
var effect = new LightScreenEffect(5);
var (move, target) = CreateIncomingMove(MoveCategory.Special, 1, true);
var damage = 100u;
// Act
effect.ChangeIncomingMoveDamage(move, target, 0, ref damage);
// Assert
await Assert.That(damage).IsEqualTo(100u);
}
}