This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
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 battleData = Substitute.For<IPokemonBattleData>();
|
||||
battleData.Battle.Returns(battle);
|
||||
|
||||
var user = Substitute.For<IPokemon>();
|
||||
user.BattleData.Returns(battleData);
|
||||
move.User.Returns(user);
|
||||
|
||||
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, IPokemon target) CreateTerrainSetup(
|
||||
bool targetFloating, string? moveType)
|
||||
{
|
||||
var terrain = new MistyTerrainScript();
|
||||
var move = Substitute.For<IExecutingMove>();
|
||||
var target = Substitute.For<IPokemon>();
|
||||
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<IPokemon>(), 0);
|
||||
|
||||
// Assert
|
||||
battle.Received(1).SetTerrain(ScriptUtils.ResolveName<MistyTerrainScript>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Technical test: without battle data on the user (outside of battle) the effect does nothing and
|
||||
/// does not throw.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void OnSecondaryEffect_UserHasNoBattleData_DoesNotSetTerrain()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, battle) = CreateMoveTestSetup();
|
||||
move.User.BattleData.Returns((IPokemonBattleData?)null);
|
||||
|
||||
// Act
|
||||
script.OnSecondaryEffect(move, Substitute.For<IPokemon>(), 0);
|
||||
|
||||
// Assert
|
||||
battle.DidNotReceiveWithAnyArgs().SetTerrain(default);
|
||||
}
|
||||
|
||||
/// <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<IPokemon>();
|
||||
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<IPokemon>();
|
||||
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<IPokemon>();
|
||||
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<IPokemon>();
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user