This commit is contained in:
@@ -0,0 +1,360 @@
|
||||
using PkmnLib.Dynamic.Events;
|
||||
using PkmnLib.Dynamic.Models;
|
||||
using PkmnLib.Dynamic.Models.Choices;
|
||||
using PkmnLib.Dynamic.ScriptHandling;
|
||||
using PkmnLib.Dynamic.ScriptHandling.Registry;
|
||||
using PkmnLib.Plugin.Gen7.Scripts.Battle;
|
||||
using PkmnLib.Plugin.Gen7.Scripts.Moves;
|
||||
using PkmnLib.Plugin.Gen7.Scripts.Side;
|
||||
using PkmnLib.Static.Moves;
|
||||
|
||||
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the <see cref="FutureSight"/> move script and its <see cref="FutureSightEffect"/>.
|
||||
/// Gen VII Bulbapedia behavior: "On the turn Future Sight is selected, this attack will do nothing other
|
||||
/// than say that the user has foreseen an attack. Two turns later, Future Sight will do damage against the
|
||||
/// target", where (from Generation V onwards) the damage calculation occurs "when it hits rather than when
|
||||
/// it is selected".
|
||||
/// </summary>
|
||||
public class FutureSightTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a fully mocked test setup for the <see cref="FutureSight"/> script where the battle's
|
||||
/// volatile scripts are a real <see cref="ScriptSet"/>.
|
||||
/// </summary>
|
||||
private static (FutureSight script, IExecutingMove move, IPokemon user, IScriptSet battleVolatile)
|
||||
CreateScriptSetup()
|
||||
{
|
||||
var script = new FutureSight();
|
||||
var move = Substitute.For<IExecutingMove>();
|
||||
|
||||
var battle = Substitute.For<IBattle>();
|
||||
battle.GetScripts().Returns(_ => new ScriptIterator(Array.Empty<IEnumerable<ScriptContainer>>()));
|
||||
IScriptSet battleVolatile = new ScriptSet(battle);
|
||||
battle.Volatile.Returns(battleVolatile);
|
||||
|
||||
var battleData = Substitute.For<IPokemonBattleData>();
|
||||
battleData.Battle.Returns(battle);
|
||||
var user = Substitute.For<IPokemon>();
|
||||
user.BattleData.Returns(battleData);
|
||||
move.User.Returns(user);
|
||||
|
||||
var moveChoice = Substitute.For<IMoveChoice>();
|
||||
move.MoveChoice.Returns(moveChoice);
|
||||
|
||||
return (script, move, user, battleVolatile);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a fully mocked test setup for the <see cref="FutureSightEffect"/>, with a target at side 1,
|
||||
/// position 0 and a damage calculator that computes the given damage when the delayed strike lands.
|
||||
/// </summary>
|
||||
private static (FutureSightEffect effect, IBattle battle, IPokemon target, EventHook eventHook) CreateEffectSetup(
|
||||
uint damage = 100, bool targetUsable = true)
|
||||
{
|
||||
var moveData = Substitute.For<IMoveData>();
|
||||
var learnedMove = Substitute.For<ILearnedMove>();
|
||||
learnedMove.MoveData.Returns(moveData);
|
||||
var moveChoice = Substitute.For<IMoveChoice>();
|
||||
moveChoice.ChosenMove.Returns(learnedMove);
|
||||
moveChoice.TargetSide.Returns((byte)1);
|
||||
moveChoice.TargetPosition.Returns((byte)0);
|
||||
|
||||
var battle = Substitute.For<IBattle>();
|
||||
var eventHook = new EventHook();
|
||||
battle.EventHook.Returns(eventHook);
|
||||
|
||||
var target = Substitute.For<IPokemon>();
|
||||
target.IsUsable.Returns(targetUsable);
|
||||
battle.GetPokemon(1, 0).Returns(target);
|
||||
|
||||
battle.Library.DamageCalculator.GetDamage(Arg.Any<IExecutingMove?>(), Arg.Any<MoveCategory>(),
|
||||
Arg.Any<IPokemon>(), Arg.Any<IPokemon>(), Arg.Any<int>(), Arg.Any<byte>(), Arg.Any<IHitData>())
|
||||
.Returns(damage);
|
||||
|
||||
var effect = new FutureSightEffect(moveChoice);
|
||||
return (effect, battle, target, eventHook);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "On the turn Future Sight is selected, this attack will do nothing other than say that
|
||||
/// the user has foreseen an attack." — the immediate execution of the move is stopped.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task StopBeforeMove_UserInBattle_StopsImmediateExecution()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, _, _) = CreateScriptSetup();
|
||||
var stop = false;
|
||||
|
||||
// Act
|
||||
script.StopBeforeMove(move, ref stop);
|
||||
|
||||
// Assert
|
||||
await Assert.That(stop).IsTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "Two turns later, Future Sight will do damage against the target" — selecting the move
|
||||
/// queues a pending <see cref="FutureSightEffect"/> on the battle.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task StopBeforeMove_UserInBattle_AddsFutureSightEffectToBattleVolatile()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, _, battleVolatile) = CreateScriptSetup();
|
||||
var stop = false;
|
||||
|
||||
// Act
|
||||
script.StopBeforeMove(move, ref stop);
|
||||
|
||||
// Assert
|
||||
await Assert.That(battleVolatile.Contains(ScriptUtils.ResolveName<FutureSightEffect>())).IsTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Technical test: outside of battle (no battle data on the user) the hook does nothing and does not
|
||||
/// stop the move.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task StopBeforeMove_UserHasNoBattleData_DoesNotStopMove()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, user, _) = CreateScriptSetup();
|
||||
user.BattleData.Returns((IPokemonBattleData?)null);
|
||||
var stop = false;
|
||||
|
||||
// Act
|
||||
script.StopBeforeMove(move, ref stop);
|
||||
|
||||
// Assert
|
||||
await Assert.That(stop).IsFalse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "Two turns later, Future Sight will do damage against the target." One end-of-turn tick
|
||||
/// after selection (the selection turn itself) plus one more is not enough for the strike to land.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnEndTurn_TwoTurnTicks_DoesNotDamageTargetYet()
|
||||
{
|
||||
// Arrange
|
||||
var (effect, battle, target, _) = CreateEffectSetup();
|
||||
|
||||
// Act - end of the selection turn and of the first turn after it
|
||||
effect.OnEndTurn(battle, battle);
|
||||
effect.OnEndTurn(battle, battle);
|
||||
|
||||
// Assert
|
||||
await Assert.That(target.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Damage")).IsFalse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "Two turns later, Future Sight will do damage against the target." At the end of the
|
||||
/// second turn after selection the delayed strike lands on the Pokémon in the targeted spot.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnEndTurn_ThreeTurnTicks_DamagesTarget()
|
||||
{
|
||||
// Arrange
|
||||
var (effect, battle, target, _) = CreateEffectSetup(123);
|
||||
|
||||
// Act - selection turn plus the two turns after it
|
||||
effect.OnEndTurn(battle, battle);
|
||||
effect.OnEndTurn(battle, battle);
|
||||
effect.OnEndTurn(battle, battle);
|
||||
|
||||
// Assert
|
||||
var damageCall = target.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage");
|
||||
await Assert.That(damageCall).IsNotNull();
|
||||
await Assert.That((uint)damageCall!.GetArguments()[0]!).IsEqualTo(123u);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia (Generation V onwards): the damage calculation "occurs when it hits rather than when it
|
||||
/// is selected" — the damage calculator is only consulted on the turn the strike lands, not before.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnEndTurn_BeforeStrikeLands_DamageCalculatorNotConsulted()
|
||||
{
|
||||
// Arrange
|
||||
var (effect, battle, _, _) = CreateEffectSetup();
|
||||
|
||||
// Act
|
||||
effect.OnEndTurn(battle, battle);
|
||||
effect.OnEndTurn(battle, battle);
|
||||
|
||||
// Assert
|
||||
await Assert.That(battle.Library.DamageCalculator.ReceivedCalls()
|
||||
.Any(c => c.GetMethodInfo().Name == "GetDamage")).IsFalse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: the delayed attack does damage "against the target" — when the targeted spot no longer
|
||||
/// holds a usable Pokémon, the move fails and no damage is dealt.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnEndTurn_TargetNotUsable_MoveFailsWithoutDamage()
|
||||
{
|
||||
// Arrange
|
||||
var (effect, battle, target, eventHook) = CreateEffectSetup(targetUsable: false);
|
||||
DialogEvent? capturedDialog = null;
|
||||
eventHook.Handler += (_, args) =>
|
||||
{
|
||||
if (args is DialogEvent dialogEvent)
|
||||
capturedDialog = dialogEvent;
|
||||
};
|
||||
|
||||
// Act
|
||||
effect.OnEndTurn(battle, battle);
|
||||
effect.OnEndTurn(battle, battle);
|
||||
effect.OnEndTurn(battle, battle);
|
||||
|
||||
// Assert
|
||||
await Assert.That(target.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Damage")).IsFalse();
|
||||
await Assert.That(capturedDialog).IsNotNull();
|
||||
await Assert.That(capturedDialog!.Message).IsEqualTo("move_failed");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a mocked battle whose volatile scripts are a real <see cref="ScriptSet"/>, for tests that
|
||||
/// exercise multiple Future Sight uses against the same battle.
|
||||
/// </summary>
|
||||
private static (IBattle battle, IScriptSet battleVolatile) CreateBattleWithRealVolatile()
|
||||
{
|
||||
var battle = Substitute.For<IBattle>();
|
||||
battle.GetScripts().Returns(_ => new ScriptIterator(Array.Empty<IEnumerable<ScriptContainer>>()));
|
||||
IScriptSet battleVolatile = new ScriptSet(battle);
|
||||
battle.Volatile.Returns(battleVolatile);
|
||||
return (battle, battleVolatile);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a mocked executing Future Sight use in the given battle, whose move choice targets the given
|
||||
/// side and position.
|
||||
/// </summary>
|
||||
private static IExecutingMove CreateFutureSightUse(IBattle battle, byte targetSide, byte targetPosition)
|
||||
{
|
||||
var moveData = Substitute.For<IMoveData>();
|
||||
var learnedMove = Substitute.For<ILearnedMove>();
|
||||
learnedMove.MoveData.Returns(moveData);
|
||||
var moveChoice = Substitute.For<IMoveChoice>();
|
||||
moveChoice.ChosenMove.Returns(learnedMove);
|
||||
moveChoice.TargetSide.Returns(targetSide);
|
||||
moveChoice.TargetPosition.Returns(targetPosition);
|
||||
|
||||
var battleData = Substitute.For<IPokemonBattleData>();
|
||||
battleData.Battle.Returns(battle);
|
||||
var user = Substitute.For<IPokemon>();
|
||||
user.BattleData.Returns(battleData);
|
||||
|
||||
var move = Substitute.For<IExecutingMove>();
|
||||
move.User.Returns(user);
|
||||
move.MoveChoice.Returns(moveChoice);
|
||||
return move;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "Future Sight fails when used if the target is already set to be hit by Future Sight or
|
||||
/// Doom Desire." — a second Future Sight against a spot that already has one queued fails instead of
|
||||
/// being silently swallowed.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task StopBeforeMove_FutureSightAlreadyQueuedOnTarget_SecondUseFails()
|
||||
{
|
||||
// Arrange - a Future Sight is already queued against side 1, position 0
|
||||
var script = new FutureSight();
|
||||
var (battle, _) = CreateBattleWithRealVolatile();
|
||||
var firstUse = CreateFutureSightUse(battle, 1, 0);
|
||||
var secondUse = CreateFutureSightUse(battle, 1, 0);
|
||||
var stop = false;
|
||||
script.StopBeforeMove(firstUse, ref stop);
|
||||
stop = false;
|
||||
|
||||
// Act - a second Future Sight is used against the same spot
|
||||
script.StopBeforeMove(secondUse, ref stop);
|
||||
|
||||
// Assert - the second use fails
|
||||
secondUse.MoveChoice.Received(1).Fail();
|
||||
await Assert.That(secondUse.MoveChoice.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Fail")).IsTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "Future Sight fails when used if the target is already set to be hit by Future Sight or
|
||||
/// Doom Desire." — a pending <see cref="DoomDesireEffect"/> against the targeted spot also makes Future
|
||||
/// Sight fail.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task StopBeforeMove_DoomDesireAlreadyQueuedOnTarget_MoveFails()
|
||||
{
|
||||
// Arrange - a Doom Desire strike is pending against side 1, position 0
|
||||
var script = new FutureSight();
|
||||
var (battle, _) = CreateBattleWithRealVolatile();
|
||||
|
||||
var opposingSide = Substitute.For<IBattleSide>();
|
||||
opposingSide.GetScripts().Returns(_ => new ScriptIterator(Array.Empty<IEnumerable<ScriptContainer>>()));
|
||||
IScriptSet sideScripts = new ScriptSet(opposingSide);
|
||||
opposingSide.VolatileScripts.Returns(sideScripts);
|
||||
var doomDesire = new DoomDesireEffect(opposingSide);
|
||||
doomDesire.AddTarget(0, 100);
|
||||
sideScripts.Add(doomDesire);
|
||||
battle.Sides.Returns(new[] { Substitute.For<IBattleSide>(), opposingSide });
|
||||
|
||||
var move = CreateFutureSightUse(battle, 1, 0);
|
||||
var stop = false;
|
||||
|
||||
// Act
|
||||
script.StopBeforeMove(move, ref stop);
|
||||
|
||||
// Assert - the move fails
|
||||
move.MoveChoice.Received(1).Fail();
|
||||
await Assert.That(move.MoveChoice.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Fail")).IsTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "Two turns later, Future Sight will do damage against the target." — the failure clause
|
||||
/// only covers a target that is "already set to be hit", so two Future Sights queued by different users
|
||||
/// against different spots are both tracked and both delayed strikes land.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task StopBeforeMove_TwoUsersTargetDifferentSpots_BothDelayedStrikesLand()
|
||||
{
|
||||
// Arrange - two users queue Future Sight against different spots on side 1
|
||||
var script = new FutureSight();
|
||||
var (battle, battleVolatile) = CreateBattleWithRealVolatile();
|
||||
battle.EventHook.Returns(new EventHook());
|
||||
|
||||
var targetA = Substitute.For<IPokemon>();
|
||||
targetA.IsUsable.Returns(true);
|
||||
battle.GetPokemon(1, 0).Returns(targetA);
|
||||
var targetB = Substitute.For<IPokemon>();
|
||||
targetB.IsUsable.Returns(true);
|
||||
battle.GetPokemon(1, 1).Returns(targetB);
|
||||
|
||||
battle.Library.DamageCalculator.GetDamage(Arg.Any<IExecutingMove?>(), Arg.Any<MoveCategory>(),
|
||||
Arg.Any<IPokemon>(), Arg.Any<IPokemon>(), Arg.Any<int>(), Arg.Any<byte>(), Arg.Any<IHitData>())
|
||||
.Returns(100u);
|
||||
|
||||
var useA = CreateFutureSightUse(battle, 1, 0);
|
||||
var useB = CreateFutureSightUse(battle, 1, 1);
|
||||
var stop = false;
|
||||
script.StopBeforeMove(useA, ref stop);
|
||||
stop = false;
|
||||
script.StopBeforeMove(useB, ref stop);
|
||||
|
||||
// Act - tick the end of the selection turn and of the two turns after it for all pending strikes
|
||||
for (var turn = 0; turn < 3; turn++)
|
||||
{
|
||||
foreach (var container in battleVolatile.ToList())
|
||||
{
|
||||
if (container.Script is IScriptOnEndTurn onEndTurn)
|
||||
onEndTurn.OnEndTurn(battle, battle);
|
||||
}
|
||||
}
|
||||
|
||||
// Assert - both delayed strikes land
|
||||
await Assert.That(targetA.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Damage")).IsTrue();
|
||||
await Assert.That(targetB.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Damage")).IsTrue();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user