This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user