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;
///
/// Tests for the move script and its .
/// 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".
///
public class FutureSightTests
{
///
/// Creates a fully mocked test setup for the script where the battle's
/// volatile scripts are a real .
///
private static (FutureSight script, IExecutingMove move, IBattlePokemon user, IScriptSet battleVolatile)
CreateScriptSetup()
{
var script = new FutureSight();
var move = Substitute.For();
var battle = Substitute.For();
battle.GetScripts().Returns(_ => new ScriptIterator(Array.Empty>()));
IScriptSet battleVolatile = new ScriptSet(battle);
battle.Volatile.Returns(battleVolatile);
var user = Substitute.For();
move.User.Returns(user);
user.Battle.Returns(battle);
var moveChoice = Substitute.For();
move.MoveChoice.Returns(moveChoice);
return (script, move, user, battleVolatile);
}
///
/// Creates a fully mocked test setup for the , with a target at side 1,
/// position 0 and a damage calculator that computes the given damage when the delayed strike lands.
///
private static (FutureSightEffect effect, IBattle battle, IBattlePokemon target, EventHook eventHook)
CreateEffectSetup(uint damage = 100, bool targetUsable = true)
{
var moveData = Substitute.For();
var learnedMove = Substitute.For();
learnedMove.MoveData.Returns(moveData);
var moveChoice = Substitute.For();
moveChoice.ChosenMove.Returns(learnedMove);
moveChoice.TargetSide.Returns((byte)1);
moveChoice.TargetPosition.Returns((byte)0);
var battle = Substitute.For();
var eventHook = new EventHook();
battle.EventHook.Returns(eventHook);
var target = Substitute.For();
target.IsUsable.Returns(targetUsable);
battle.GetPokemon(1, 0).Returns(target);
battle.Library.DamageCalculator.GetDamage(Arg.Any(), Arg.Any(),
Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(),
Arg.Any())
.Returns(damage);
var effect = new FutureSightEffect(moveChoice);
return (effect, battle, target, eventHook);
}
///
/// 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.
///
[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();
}
///
/// Bulbapedia: "Two turns later, Future Sight will do damage against the target" — selecting the move
/// queues a pending on the battle.
///
[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())).IsTrue();
}
///
/// 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.
///
[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();
}
///
/// 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.
///
[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);
}
///
/// 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.
///
[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();
}
///
/// 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.
///
[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");
}
///
/// Creates a mocked battle whose volatile scripts are a real , for tests that
/// exercise multiple Future Sight uses against the same battle.
///
private static (IBattle battle, IScriptSet battleVolatile) CreateBattleWithRealVolatile()
{
var battle = Substitute.For();
battle.GetScripts().Returns(_ => new ScriptIterator(Array.Empty>()));
IScriptSet battleVolatile = new ScriptSet(battle);
battle.Volatile.Returns(battleVolatile);
return (battle, battleVolatile);
}
///
/// Creates a mocked executing Future Sight use in the given battle, whose move choice targets the given
/// side and position.
///
private static IExecutingMove CreateFutureSightUse(IBattle battle, byte targetSide, byte targetPosition)
{
var moveData = Substitute.For();
var learnedMove = Substitute.For();
learnedMove.MoveData.Returns(moveData);
var moveChoice = Substitute.For();
moveChoice.ChosenMove.Returns(learnedMove);
moveChoice.TargetSide.Returns(targetSide);
moveChoice.TargetPosition.Returns(targetPosition);
var user = Substitute.For();
user.Battle.Returns(battle);
var move = Substitute.For();
move.User.Returns(user);
move.MoveChoice.Returns(moveChoice);
return move;
}
///
/// 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.
///
[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();
}
///
/// Bulbapedia: "Future Sight fails when used if the target is already set to be hit by Future Sight or
/// Doom Desire." — a pending against the targeted spot also makes Future
/// Sight fail.
///
[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();
opposingSide.GetScripts().Returns(_ => new ScriptIterator(Array.Empty>()));
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(), 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();
}
///
/// 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.
///
[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();
targetA.IsUsable.Returns(true);
battle.GetPokemon(1, 0).Returns(targetA);
var targetB = Substitute.For();
targetB.IsUsable.Returns(true);
battle.GetPokemon(1, 1).Returns(targetB);
battle.Library.DamageCalculator.GetDamage(Arg.Any(), Arg.Any(),
Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(),
Arg.Any())
.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();
}
}