349 lines
13 KiB
C#
349 lines
13 KiB
C#
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.Moves;
|
|
using PkmnLib.Plugin.Gen7.Scripts.Pokemon;
|
|
using PkmnLib.Static.Moves;
|
|
using PkmnLib.Static.Utils;
|
|
|
|
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
|
|
|
|
/// <summary>
|
|
/// Tests for the <see cref="Fly"/> move script and its <see cref="ChargeFlyEffect"/>.
|
|
/// Gen VII Bulbapedia behavior: "On the turn that Fly is selected, the user will fly up high and become
|
|
/// semi-invulnerable"; it attacks on the second turn. From Gen II onwards the user "can now be hit by Gust,
|
|
/// Thunder, Twister, and Whirlwind during the semi-invulnerable turn of Fly, and will receive double damage
|
|
/// from Gust and Twister"; from Gen V onwards it "can also be hit with Hurricane, Smack Down, and Thousand
|
|
/// Arrows".
|
|
/// </summary>
|
|
public class FlyTests
|
|
{
|
|
private static (Fly script, IExecutingMove move, IPokemon user, ScriptSet userVolatile, IMoveChoice moveChoice,
|
|
EventHook eventHook) CreateTestSetup()
|
|
{
|
|
var script = new Fly();
|
|
var move = Substitute.For<IExecutingMove>();
|
|
var user = Substitute.For<IPokemon>();
|
|
// Use a real script set so the charge volatile added by Fly can be inspected afterwards.
|
|
var userVolatile = new ScriptSet(user);
|
|
user.Volatile.Returns(userVolatile);
|
|
user.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));
|
|
move.User.Returns(user);
|
|
|
|
var moveChoice = Substitute.For<IMoveChoice>();
|
|
move.MoveChoice.Returns(moveChoice);
|
|
|
|
var eventHook = new EventHook();
|
|
var battle = Substitute.For<IBattle>();
|
|
battle.EventHook.Returns(eventHook);
|
|
var battleData = Substitute.For<IPokemonBattleData>();
|
|
battleData.Battle.Returns(battle);
|
|
user.BattleData.Returns(battleData);
|
|
|
|
return (script, move, user, userVolatile, moveChoice, eventHook);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "On the turn that Fly is selected, the user will fly up high". On the first turn the
|
|
/// move is prevented from executing.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task PreventMove_FirstUse_PreventsMoveForChargeTurn()
|
|
{
|
|
// Arrange
|
|
var (script, move, _, _, _, _) = CreateTestSetup();
|
|
var prevent = false;
|
|
|
|
// Act
|
|
script.PreventMove(move, ref prevent);
|
|
|
|
// Assert
|
|
await Assert.That(prevent).IsTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: the user will "become semi-invulnerable" on the charge turn. The charge turn attaches
|
|
/// the <see cref="ChargeFlyEffect"/> volatile script to the user.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task PreventMove_FirstUse_AddsChargeFlyEffectToUser()
|
|
{
|
|
// Arrange
|
|
var (script, move, _, userVolatile, _, _) = CreateTestSetup();
|
|
var prevent = false;
|
|
|
|
// Act
|
|
script.PreventMove(move, ref prevent);
|
|
|
|
// Assert
|
|
await Assert.That(userVolatile.Contains(ScriptUtils.ResolveName<ChargeFlyEffect>())).IsTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// The charge turn is marked on the <see cref="IMoveChoice"/> so the <see cref="ChargeFlyEffect"/>
|
|
/// knows this choice was the charging turn and does not remove itself for it.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task PreventMove_FirstUse_MarksMoveChoiceAsChargeTurn()
|
|
{
|
|
// Arrange
|
|
var (script, move, _, _, moveChoice, _) = CreateTestSetup();
|
|
var prevent = false;
|
|
|
|
// Act
|
|
script.PreventMove(move, ref prevent);
|
|
|
|
// Assert
|
|
moveChoice.Received(1).SetAdditionalData(new StringKey("fly_charge"), true);
|
|
await Assert.That(prevent).IsTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "the user will fly up high" — the charge turn announces itself through a
|
|
/// <see cref="DialogEvent"/> so the battle log can show the fly-up message.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task PreventMove_FirstUse_FiresFlyChargeDialogEvent()
|
|
{
|
|
// Arrange
|
|
var (script, move, user, _, _, eventHook) = CreateTestSetup();
|
|
DialogEvent? captured = null;
|
|
eventHook.Handler += (_, args) =>
|
|
{
|
|
if (args is DialogEvent dialogEvent)
|
|
captured = dialogEvent;
|
|
};
|
|
var prevent = false;
|
|
|
|
// Act
|
|
script.PreventMove(move, ref prevent);
|
|
|
|
// Assert
|
|
await Assert.That(captured).IsNotNull();
|
|
await Assert.That(captured!.Message).IsEqualTo("fly_charge");
|
|
var userParameter = captured!.Parameters?.GetValueOrDefault("user");
|
|
await Assert.That(ReferenceEquals(userParameter, user)).IsTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: Fly attacks on the second turn. When the user already has the
|
|
/// <see cref="ChargeFlyEffect"/> from the charge turn, the move is not prevented again.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task PreventMove_SecondTurn_DoesNotPreventMove()
|
|
{
|
|
// Arrange
|
|
var (script, move, user, userVolatile, _, _) = CreateTestSetup();
|
|
userVolatile.Add(new ChargeFlyEffect(user));
|
|
var prevent = false;
|
|
|
|
// Act
|
|
script.PreventMove(move, ref prevent);
|
|
|
|
// Assert
|
|
await Assert.That(prevent).IsFalse();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Technical test: a user that is not in a battle (no <see cref="IPokemon.BattleData"/>) still starts
|
|
/// the charge turn without throwing; only the dialog event is skipped.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task PreventMove_NoBattleData_StillPreventsMove()
|
|
{
|
|
// Arrange
|
|
var script = new Fly();
|
|
var move = Substitute.For<IExecutingMove>();
|
|
var user = Substitute.For<IPokemon>();
|
|
var userVolatile = new ScriptSet(user);
|
|
user.Volatile.Returns(userVolatile);
|
|
user.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));
|
|
user.BattleData.Returns((IPokemonBattleData?)null);
|
|
move.User.Returns(user);
|
|
move.MoveChoice.Returns(Substitute.For<IMoveChoice>());
|
|
var prevent = false;
|
|
|
|
// Act
|
|
script.PreventMove(move, ref prevent);
|
|
|
|
// Assert
|
|
await Assert.That(prevent).IsTrue();
|
|
await Assert.That(userVolatile.Contains(ScriptUtils.ResolveName<ChargeFlyEffect>())).IsTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: the attack executes on the second turn, ending the semi-invulnerable state. Once the
|
|
/// attack executes, the <see cref="ChargeFlyEffect"/> is removed from the user.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnBeforeMove_ChargeCompleted_RemovesChargeFlyEffect()
|
|
{
|
|
// Arrange
|
|
var (script, move, user, userVolatile, _, _) = CreateTestSetup();
|
|
userVolatile.Add(new ChargeFlyEffect(user));
|
|
|
|
// Act
|
|
script.OnBeforeMove(move);
|
|
|
|
// Assert
|
|
await Assert.That(userVolatile.Contains(ScriptUtils.ResolveName<ChargeFlyEffect>())).IsFalse();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: while flying up high the user is "semi-invulnerable". The <see cref="ChargeFlyEffect"/>
|
|
/// blocks incoming hits from moves that cannot hit flying Pokémon.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task BlockIncomingHit_MoveCannotHitFlying_BlocksHit()
|
|
{
|
|
// Arrange
|
|
var (script, move, user, userVolatile, _, _) = CreateTestSetup();
|
|
var prevent = false;
|
|
script.PreventMove(move, ref prevent);
|
|
await Assert.That(userVolatile.TryGet<ChargeFlyEffect>(out var effect)).IsTrue();
|
|
|
|
var incomingMove = Substitute.For<IExecutingMove>();
|
|
var incomingMoveData = Substitute.For<IMoveData>();
|
|
incomingMoveData.HasFlag(new StringKey("hit_flying")).Returns(false);
|
|
incomingMove.UseMove.Returns(incomingMoveData);
|
|
|
|
// Act
|
|
var block = false;
|
|
effect!.BlockIncomingHit(incomingMove, user, 0, ref block);
|
|
|
|
// Assert
|
|
await Assert.That(block).IsTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: the user "can now be hit by Gust, Thunder, Twister, and Whirlwind during the
|
|
/// semi-invulnerable turn of Fly" (and from Gen V onwards also "Hurricane, Smack Down, and Thousand
|
|
/// Arrows"). Moves flagged as able to hit flying Pokémon are not blocked.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task BlockIncomingHit_MoveCanHitFlying_DoesNotBlockHit()
|
|
{
|
|
// Arrange
|
|
var (script, move, user, userVolatile, _, _) = CreateTestSetup();
|
|
var prevent = false;
|
|
script.PreventMove(move, ref prevent);
|
|
await Assert.That(userVolatile.TryGet<ChargeFlyEffect>(out var effect)).IsTrue();
|
|
|
|
var incomingMove = Substitute.For<IExecutingMove>();
|
|
var incomingMoveData = Substitute.For<IMoveData>();
|
|
incomingMoveData.HasFlag(new StringKey("hit_flying")).Returns(true);
|
|
incomingMove.UseMove.Returns(incomingMoveData);
|
|
|
|
// Act
|
|
var block = false;
|
|
effect!.BlockIncomingHit(incomingMove, user, 0, ref block);
|
|
|
|
// Assert
|
|
await Assert.That(block).IsFalse();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: the user "will receive double damage from Gust and Twister" while in the
|
|
/// semi-invulnerable turn of Fly. A move flagged as effective against flying Pokémon deals doubled
|
|
/// damage to the flying user.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task ChangeIncomingMoveDamage_GustLikeMove_DamageIsDoubled()
|
|
{
|
|
// Arrange
|
|
var (script, move, user, userVolatile, _, _) = CreateTestSetup();
|
|
var prevent = false;
|
|
script.PreventMove(move, ref prevent);
|
|
await Assert.That(userVolatile.TryGet<ChargeFlyEffect>(out var effect)).IsTrue();
|
|
|
|
var incomingMove = Substitute.For<IExecutingMove>();
|
|
var incomingMoveData = Substitute.For<IMoveData>();
|
|
incomingMoveData.HasFlag(new StringKey("hit_flying")).Returns(true);
|
|
incomingMoveData.HasFlag(new StringKey("effective_against_fly")).Returns(true);
|
|
incomingMove.UseMove.Returns(incomingMoveData);
|
|
uint damage = 100;
|
|
|
|
// Act
|
|
effect!.ChangeIncomingMoveDamage(incomingMove, user, 0, ref damage);
|
|
|
|
// Assert
|
|
await Assert.That(damage).IsEqualTo(200u);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: only Gust and Twister deal "double damage" to the flying user; other moves that can hit
|
|
/// during the semi-invulnerable turn (such as Thunder, Hurricane, and Smack Down) deal regular damage.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task ChangeIncomingMoveDamage_ThunderLikeMove_DamageIsUnchanged()
|
|
{
|
|
// Arrange
|
|
var (script, move, user, userVolatile, _, _) = CreateTestSetup();
|
|
var prevent = false;
|
|
script.PreventMove(move, ref prevent);
|
|
await Assert.That(userVolatile.TryGet<ChargeFlyEffect>(out var effect)).IsTrue();
|
|
|
|
var incomingMove = Substitute.For<IExecutingMove>();
|
|
var incomingMoveData = Substitute.For<IMoveData>();
|
|
incomingMoveData.HasFlag(new StringKey("hit_flying")).Returns(true);
|
|
incomingMoveData.HasFlag(new StringKey("effective_against_fly")).Returns(false);
|
|
incomingMove.UseMove.Returns(incomingMoveData);
|
|
uint damage = 100;
|
|
|
|
// Act
|
|
effect!.ChangeIncomingMoveDamage(incomingMove, user, 0, ref damage);
|
|
|
|
// Assert
|
|
await Assert.That(damage).IsEqualTo(100u);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "If the user of Fly is hit by Smack Down or Thousand Arrows during its
|
|
/// semi-invulnerable turn, it will be knocked down and Fly's execution will be cancelled" — more
|
|
/// generally, when the user's executed choice was not the Fly charge (the move was disrupted or
|
|
/// replaced), the semi-invulnerable state is removed.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnAfterMoveChoice_ChoiceIsNotFlyCharge_RemovesChargeFlyEffect()
|
|
{
|
|
// Arrange
|
|
var user = Substitute.For<IPokemon>();
|
|
var userVolatile = new ScriptSet(user);
|
|
user.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));
|
|
var effect = new ChargeFlyEffect(user);
|
|
userVolatile.Add(effect);
|
|
var choice = Substitute.For<IMoveChoice>();
|
|
choice.AdditionalData.Returns((Dictionary<StringKey, object?>?)null);
|
|
|
|
// Act
|
|
effect.OnAfterMoveChoice(choice);
|
|
|
|
// Assert
|
|
await Assert.That(userVolatile.Contains(ScriptUtils.ResolveName<ChargeFlyEffect>())).IsFalse();
|
|
}
|
|
|
|
/// <summary>
|
|
/// The charging turn itself carries the "fly_charge" marker; the <see cref="ChargeFlyEffect"/> must
|
|
/// survive that turn so the user stays airborne until the attack turn.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnAfterMoveChoice_ChoiceIsFlyCharge_KeepsChargeFlyEffect()
|
|
{
|
|
// Arrange
|
|
var user = Substitute.For<IPokemon>();
|
|
var userVolatile = new ScriptSet(user);
|
|
user.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));
|
|
var effect = new ChargeFlyEffect(user);
|
|
userVolatile.Add(effect);
|
|
var choice = Substitute.For<IMoveChoice>();
|
|
choice.AdditionalData.Returns(new Dictionary<StringKey, object?> { { "fly_charge", true } });
|
|
|
|
// Act
|
|
effect.OnAfterMoveChoice(choice);
|
|
|
|
// Assert
|
|
await Assert.That(userVolatile.Contains(ScriptUtils.ResolveName<ChargeFlyEffect>())).IsTrue();
|
|
}
|
|
} |