Files
PkmnLib.NET/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/MistyTerrainTests.cs

227 lines
8.5 KiB
C#

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;
using PkmnLib.Static.Utils;
using MistyTerrainScript = PkmnLib.Plugin.Gen7.Scripts.Terrain.MistyTerrain;
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
/// <summary>
/// Tests for the <see cref="MistyTerrain"/> move script and the
/// <see cref="MistyTerrainScript"/> terrain script it activates.
/// Gen VII Bulbapedia behavior: "Misty Terrain creates terrain that envelops the field and replaces the
/// background environment and any other terrain that is already in effect." "This terrain has the following
/// effects on Pokémon that are on the ground [...]: It halves the power of Dragon-type moves used against
/// affected Pokémon [...]. It prevents affected Pokémon from being afflicted by non-volatile status
/// conditions". Generation VII: "Pokémon affected by Misty Terrain can no longer become confused."
/// </summary>
public class MistyTerrainTests
{
private static (MistyTerrain script, IExecutingMove move, IBattle battle) CreateMoveTestSetup()
{
var script = new MistyTerrain();
var move = Substitute.For<IExecutingMove>();
var battle = Substitute.For<IBattle>();
var user = Substitute.For<IBattlePokemon>();
move.User.Returns(user);
user.Battle.Returns(battle);
return (script, move, battle);
}
/// <summary>
/// Creates a mocked setup for the terrain script's base power hook, with a target that is either
/// grounded or floating and a hit of the given type (null for a typeless hit).
/// </summary>
private static (MistyTerrainScript terrain, IExecutingMove move, IBattlePokemon target) CreateTerrainSetup(
bool targetFloating, string? moveType)
{
var terrain = new MistyTerrainScript();
var move = Substitute.For<IExecutingMove>();
var target = Substitute.For<IBattlePokemon>();
target.IsFloating.Returns(targetFloating);
var hitData = Substitute.For<IHitData>();
hitData.Type.Returns(moveType == null ? null : new TypeIdentifier(1, new StringKey(moveType)));
move.GetHitData(target, 0).Returns(hitData);
return (terrain, move, target);
}
/// <summary>
/// Bulbapedia: "Misty Terrain creates terrain that envelops the field and replaces the background
/// environment and any other terrain that is already in effect." — the move sets the battle's terrain to
/// the <see cref="MistyTerrainScript"/> terrain script.
/// </summary>
[Test]
public void OnSecondaryEffect_SetsMistyTerrainOnBattle()
{
// Arrange
var (script, move, battle) = CreateMoveTestSetup();
// Act
script.OnSecondaryEffect(move, Substitute.For<IBattlePokemon>(), 0);
// Assert
battle.Received(1).SetTerrain(ScriptUtils.ResolveName<MistyTerrainScript>());
}
/// <summary>
/// Bulbapedia: "It halves the power of Dragon-type moves used against affected Pokémon (regardless of
/// whether the user of the move is affected by Misty Terrain)." Halving uses integer division.
/// </summary>
[Test, Arguments((ushort)100, (ushort)50), Arguments((ushort)95, (ushort)47), Arguments((ushort)1, (ushort)0),
Arguments((ushort)65535, (ushort)32767)]
public async Task ChangeBasePower_DragonMoveAgainstGroundedPokemon_PowerHalved(ushort basePower, ushort expected)
{
// Arrange
var (terrain, move, target) = CreateTerrainSetup(false, "dragon");
// Act
terrain.ChangeBasePower(move, target, 0, ref basePower);
// Assert
await Assert.That(basePower).IsEqualTo(expected);
}
/// <summary>
/// Bulbapedia: the terrain only affects "Pokémon that are on the ground" — a Dragon-type move used
/// against a floating (airborne) Pokémon keeps its full power.
/// </summary>
[Test]
public async Task ChangeBasePower_DragonMoveAgainstFloatingPokemon_PowerUnchanged()
{
// Arrange
var (terrain, move, target) = CreateTerrainSetup(true, "dragon");
ushort basePower = 100;
// Act
terrain.ChangeBasePower(move, target, 0, ref basePower);
// Assert
await Assert.That(basePower).IsEqualTo((ushort)100);
}
/// <summary>
/// Bulbapedia: only "Dragon-type moves" have their power halved — moves of any other type are
/// unaffected.
/// </summary>
[Test, Arguments("fairy"), Arguments("normal")]
public async Task ChangeBasePower_NonDragonMove_PowerUnchanged(string type)
{
// Arrange
var (terrain, move, target) = CreateTerrainSetup(false, type);
ushort basePower = 100;
// Act
terrain.ChangeBasePower(move, target, 0, ref basePower);
// Assert
await Assert.That(basePower).IsEqualTo((ushort)100);
}
/// <summary>
/// Technical test: a typeless hit (null hit type) is not a Dragon-type move, so its power is unchanged
/// and the script does not throw.
/// </summary>
[Test]
public async Task ChangeBasePower_TypelessMove_PowerUnchanged()
{
// Arrange
var (terrain, move, target) = CreateTerrainSetup(false, null);
ushort basePower = 100;
// Act
terrain.ChangeBasePower(move, target, 0, ref basePower);
// Assert
await Assert.That(basePower).IsEqualTo((ushort)100);
}
/// <summary>
/// Bulbapedia: "It prevents affected Pokémon from being afflicted by non-volatile status conditions" —
/// a grounded Pokémon cannot be given any non-volatile status while Misty Terrain is active.
/// </summary>
[Test, Arguments("burned"), Arguments("paralyzed"), Arguments("poisoned"), Arguments("frozen"), Arguments("sleep")]
public async Task PreventStatusChange_GroundedPokemon_StatusPrevented(string status)
{
// Arrange
var terrain = new MistyTerrainScript();
var pokemon = Substitute.For<IBattlePokemon>();
pokemon.IsFloating.Returns(false);
var preventStatus = false;
// Act
terrain.PreventStatusChange(pokemon, new StringKey(status), false, ref preventStatus);
// Assert
await Assert.That(preventStatus).IsTrue();
}
/// <summary>
/// Bulbapedia: "Rest will fail if used by an affected Pokémon." — even a self-inflicted status (Rest's
/// sleep) is prevented for grounded Pokémon.
/// </summary>
[Test]
public async Task PreventStatusChange_SelfInflictedSleep_Prevented()
{
// Arrange
var terrain = new MistyTerrainScript();
var pokemon = Substitute.For<IBattlePokemon>();
pokemon.IsFloating.Returns(false);
var preventStatus = false;
// Act
terrain.PreventStatusChange(pokemon, new StringKey("sleep"), true, ref preventStatus);
// Assert
await Assert.That(preventStatus).IsTrue();
}
/// <summary>
/// Bulbapedia: the terrain only affects "Pokémon that are on the ground" — a floating (airborne)
/// Pokémon can still be afflicted by non-volatile status conditions.
/// </summary>
[Test]
public async Task PreventStatusChange_FloatingPokemon_StatusNotPrevented()
{
// Arrange
var terrain = new MistyTerrainScript();
var pokemon = Substitute.For<IBattlePokemon>();
pokemon.IsFloating.Returns(true);
var preventStatus = false;
// Act
terrain.PreventStatusChange(pokemon, new StringKey("burned"), false, ref preventStatus);
// Assert
await Assert.That(preventStatus).IsFalse();
}
/// <summary>
/// Bulbapedia (Generation VII): "Pokémon affected by Misty Terrain can no longer become confused." —
/// adding the <see cref="Confusion"/> volatile to a grounded Pokémon is prevented while Misty Terrain
/// is in effect.
/// </summary>
[Test]
public async Task PreventVolatileAdd_ConfusionOnGroundedPokemon_Prevented()
{
// Arrange
var terrain = new MistyTerrainScript();
var pokemon = Substitute.For<IBattlePokemon>();
pokemon.IsFloating.Returns(false);
var preventVolatileAdd = false;
// Act - a correct implementation would prevent the Confusion volatile through this hook.
if (terrain is IScriptPreventVolatileAdd preventHook)
preventHook.PreventVolatileAdd(pokemon, new Confusion(), ref preventVolatileAdd);
// Assert
await Assert.That(preventVolatileAdd).IsTrue();
}
}