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; /// /// Tests for the move script and the /// 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." /// public class MistyTerrainTests { private static (MistyTerrain script, IExecutingMove move, IBattle battle) CreateMoveTestSetup() { var script = new MistyTerrain(); var move = Substitute.For(); var battle = Substitute.For(); var battleData = Substitute.For(); battleData.Battle.Returns(battle); var user = Substitute.For(); user.BattleData.Returns(battleData); move.User.Returns(user); return (script, move, battle); } /// /// 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). /// private static (MistyTerrainScript terrain, IExecutingMove move, IPokemon target) CreateTerrainSetup( bool targetFloating, string? moveType) { var terrain = new MistyTerrainScript(); var move = Substitute.For(); var target = Substitute.For(); target.IsFloating.Returns(targetFloating); var hitData = Substitute.For(); hitData.Type.Returns(moveType == null ? null : new TypeIdentifier(1, new StringKey(moveType))); move.GetHitData(target, 0).Returns(hitData); return (terrain, move, target); } /// /// 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 terrain script. /// [Test] public void OnSecondaryEffect_SetsMistyTerrainOnBattle() { // Arrange var (script, move, battle) = CreateMoveTestSetup(); // Act script.OnSecondaryEffect(move, Substitute.For(), 0); // Assert battle.Received(1).SetTerrain(ScriptUtils.ResolveName()); } /// /// Technical test: without battle data on the user (outside of battle) the effect does nothing and /// does not throw. /// [Test] public void OnSecondaryEffect_UserHasNoBattleData_DoesNotSetTerrain() { // Arrange var (script, move, battle) = CreateMoveTestSetup(); move.User.BattleData.Returns((IPokemonBattleData?)null); // Act script.OnSecondaryEffect(move, Substitute.For(), 0); // Assert battle.DidNotReceiveWithAnyArgs().SetTerrain(default); } /// /// 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. /// [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); } /// /// 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. /// [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); } /// /// Bulbapedia: only "Dragon-type moves" have their power halved — moves of any other type are /// unaffected. /// [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); } /// /// 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. /// [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); } /// /// 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. /// [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(); pokemon.IsFloating.Returns(false); var preventStatus = false; // Act terrain.PreventStatusChange(pokemon, new StringKey(status), false, ref preventStatus); // Assert await Assert.That(preventStatus).IsTrue(); } /// /// 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. /// [Test] public async Task PreventStatusChange_SelfInflictedSleep_Prevented() { // Arrange var terrain = new MistyTerrainScript(); var pokemon = Substitute.For(); pokemon.IsFloating.Returns(false); var preventStatus = false; // Act terrain.PreventStatusChange(pokemon, new StringKey("sleep"), true, ref preventStatus); // Assert await Assert.That(preventStatus).IsTrue(); } /// /// 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. /// [Test] public async Task PreventStatusChange_FloatingPokemon_StatusNotPrevented() { // Arrange var terrain = new MistyTerrainScript(); var pokemon = Substitute.For(); pokemon.IsFloating.Returns(true); var preventStatus = false; // Act terrain.PreventStatusChange(pokemon, new StringKey("burned"), false, ref preventStatus); // Assert await Assert.That(preventStatus).IsFalse(); } /// /// Bulbapedia (Generation VII): "Pokémon affected by Misty Terrain can no longer become confused." — /// adding the volatile to a grounded Pokémon is prevented while Misty Terrain /// is in effect. /// [Test] public async Task PreventVolatileAdd_ConfusionOnGroundedPokemon_Prevented() { // Arrange var terrain = new MistyTerrainScript(); var pokemon = Substitute.For(); 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(); } }