using PkmnLib.Dynamic.Models; using PkmnLib.Dynamic.ScriptHandling; using PkmnLib.Dynamic.ScriptHandling.Registry; using PkmnLib.Plugin.Gen7.Scripts.Moves; using PkmnLib.Plugin.Gen7.Scripts.Pokemon; using PkmnLib.Static.Utils; using BattleGravity = PkmnLib.Plugin.Gen7.Scripts.Battle.Gravity; namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// /// Tests for the move script. /// Gen VII Bulbapedia behavior: "Gravity causes the field to undergo intense gravity lasting 5 turns", /// and "Semi-invulnerable Pokémon using Fly or Bounce are immediately canceled." /// public class GravityTests { /// /// Creates a fully mocked test setup for Gravity tests. The battle's volatile script set is a /// substitute so the addition of the battle-wide gravity script can be inspected. /// private static (Gravity script, IExecutingMove move, IBattlePokemon target, IBattle battle, IScriptSet battleVolatile) CreateTestSetup(params IReadOnlyList[] sidePokemon) { var script = new Gravity(); var move = Substitute.For(); move.User.Returns(Substitute.For()); var battle = Substitute.For(); var battleVolatile = Substitute.For(); battle.Volatile.Returns(battleVolatile); var sides = sidePokemon.Select(pokemon => { var side = Substitute.For(); side.Pokemon.Returns(pokemon); return side; }).ToArray(); battle.Sides.Returns(sides); var target = Substitute.For(); target.Battle.Returns(battle); return (script, move, target, battle, battleVolatile); } /// /// Creates a mocked Pokémon with a real as its volatile set, so effects can /// be attached and their removal inspected. /// private static (IBattlePokemon pokemon, ScriptSet volatileSet) CreateFieldPokemon() { var pokemon = Substitute.For(); pokemon.GetScripts().Returns(_ => new ScriptIterator(new List>())); var volatileSet = new ScriptSet(pokemon); pokemon.Volatile.Returns(volatileSet); return (pokemon, volatileSet); } /// /// Bulbapedia: "Gravity causes the field to undergo intense gravity lasting 5 turns." The battle-wide /// gravity effect is added to the battle's volatile script set. /// [Test] public async Task OnSecondaryEffect_Always_AddsGravityToBattleVolatile() { // Arrange var (script, move, target, _, battleVolatile) = CreateTestSetup(); // Act script.OnSecondaryEffect(move, target, 0); // Assert battleVolatile.Received(1).StackOrAdd(new StringKey("gravity"), Arg.Any>()); await Assert.That(battleVolatile.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "StackOrAdd")).IsTrue(); } /// /// Bulbapedia: the field undergoes "intense gravity" — the instantiation function passed to the /// battle's volatile script set creates the battle-wide script. /// [Test] public async Task OnSecondaryEffect_GravityInstantiation_CreatesBattleGravityScript() { // Arrange var (script, move, target, battle, battleVolatile) = CreateTestSetup(); battle.Library.Returns(LibraryHelpers.LoadLibrary()); // Act script.OnSecondaryEffect(move, target, 0); // Assert var call = battleVolatile.ReceivedCalls().First(c => c.GetMethodInfo().Name == "StackOrAdd"); var instantiation = (Func)call.GetArguments()[1]!; await Assert.That(instantiation() is BattleGravity).IsTrue(); } /// /// Bulbapedia: "Semi-invulnerable Pokémon using Fly or Bounce are immediately canceled." A Pokémon in /// the semi-invulnerable turn of Fly has its removed. /// [Test] public async Task OnSecondaryEffect_PokemonFlying_FlyEffectRemoved() { // Arrange var (pokemon, volatileSet) = CreateFieldPokemon(); volatileSet.Add(new ChargeFlyEffect(pokemon)); var (script, move, target, _, _) = CreateTestSetup(new List { pokemon }); // Act script.OnSecondaryEffect(move, target, 0); // Assert await Assert.That(volatileSet.Contains(ScriptUtils.ResolveName())).IsFalse(); } /// /// Bulbapedia: "Semi-invulnerable Pokémon using Fly or Bounce are immediately canceled." A Pokémon in /// the semi-invulnerable turn of Bounce has its removed. /// [Test] public async Task OnSecondaryEffect_PokemonBouncing_BounceEffectRemoved() { // Arrange var (pokemon, volatileSet) = CreateFieldPokemon(); volatileSet.Add(new ChargeBounceEffect(pokemon)); var (script, move, target, _, _) = CreateTestSetup(new List { pokemon }); // Act script.OnSecondaryEffect(move, target, 0); // Assert await Assert.That(volatileSet.Contains(ScriptUtils.ResolveName())).IsFalse(); } /// /// Bulbapedia: Gravity cancels Sky Drop — "Semi-invulnerable Pokémon ... are immediately canceled." A /// Pokémon in the semi-invulnerable turn of Sky Drop has its /// removed. /// [Test] public async Task OnSecondaryEffect_PokemonInSkyDrop_SkyDropEffectRemoved() { // Arrange var (pokemon, volatileSet) = CreateFieldPokemon(); volatileSet.Add(new ChargeSkyDropEffect(pokemon)); var (script, move, target, _, _) = CreateTestSetup(new List { pokemon }); // Act script.OnSecondaryEffect(move, target, 0); // Assert await Assert.That(volatileSet.Contains(ScriptUtils.ResolveName())).IsFalse(); } /// /// Bulbapedia: "The effect grounds all Pokémon, removing ... Telekinesis ... effects." A Pokémon under /// Telekinesis has its removed. /// [Test] public async Task OnSecondaryEffect_PokemonUnderTelekinesis_TelekinesisEffectRemoved() { // Arrange var (pokemon, volatileSet) = CreateFieldPokemon(); volatileSet.Add(new TelekinesisEffect()); var (script, move, target, _, _) = CreateTestSetup(new List { pokemon }); // Act script.OnSecondaryEffect(move, target, 0); // Assert await Assert.That(volatileSet.Contains(ScriptUtils.ResolveName())).IsFalse(); } /// /// Bulbapedia: "The effect grounds all Pokémon, removing ... Magnet Rise ... effects." A Pokémon under /// Magnet Rise should have its removed. /// [Test] public async Task OnSecondaryEffect_PokemonUnderMagnetRise_MagnetRiseEffectRemoved() { // Arrange var (pokemon, volatileSet) = CreateFieldPokemon(); volatileSet.Add(new MagnetRiseEffect()); var (script, move, target, _, _) = CreateTestSetup(new List { pokemon }); // Act script.OnSecondaryEffect(move, target, 0); // Assert await Assert.That(volatileSet.Contains(ScriptUtils.ResolveName())).IsFalse(); } /// /// Bulbapedia: "The effect grounds all Pokémon" — Pokémon on every side of the battle are affected, /// not only those on the target's side. /// [Test] public async Task OnSecondaryEffect_FlyingPokemonOnBothSides_BothCanceled() { // Arrange var (allyPokemon, allyVolatile) = CreateFieldPokemon(); allyVolatile.Add(new ChargeFlyEffect(allyPokemon)); var (opposingPokemon, opposingVolatile) = CreateFieldPokemon(); opposingVolatile.Add(new ChargeFlyEffect(opposingPokemon)); var (script, move, target, _, _) = CreateTestSetup(new List { allyPokemon }, new List { opposingPokemon }); // Act script.OnSecondaryEffect(move, target, 0); // Assert await Assert.That(allyVolatile.Contains(ScriptUtils.ResolveName())).IsFalse(); await Assert.That(opposingVolatile.Contains(ScriptUtils.ResolveName())).IsFalse(); } /// /// Technical test: empty (null) slots on a side are skipped without throwing. /// [Test] public async Task OnSecondaryEffect_EmptyPokemonSlot_IsSkipped() { // Arrange var (script, move, target, _, battleVolatile) = CreateTestSetup(new List { null }); // Act script.OnSecondaryEffect(move, target, 0); // Assert await Assert.That(battleVolatile.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "StackOrAdd")).IsTrue(); } }