using PkmnLib.Dynamic.Models; using PkmnLib.Dynamic.Models.Choices; using PkmnLib.Dynamic.ScriptHandling; using PkmnLib.Plugin.Gen7.Scripts.Moves; using PkmnLib.Plugin.Gen7.Scripts.Pokemon; using PkmnLib.Static; using PkmnLib.Static.Moves; namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// /// Tests for the move script and its volatile. /// Gen VII Bulbapedia behavior: the user plants roots, restoring 1/16th of its maximum HP each turn while /// becoming unable to switch out; forced-switch moves like Roar fail against it. From Generation IV onwards /// the move grounds the affected Pokémon, making them susceptible to Ground moves even if Flying-type. /// Generation VI onwards: "Ghost-type Pokémon under the effects of Ingrain are now able to switch out." /// public class IngrainTests { /// /// Creates a rooted Pokémon substitute with the given maximum HP, together with the /// that roots it. /// private static (IngrainEffect effect, IPokemon owner) CreateRootedPokemon(uint maxHealth = 100) { var owner = Substitute.For(); owner.BoostedStats.Returns(new StatisticSet(maxHealth, 1, 1, 1, 1, 1)); return (new IngrainEffect(owner), owner); } /// /// Helper to extract the heal amount from a Pokémon's received Heal calls. /// private static uint? GetHealAmount(IPokemon pokemon) { var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Heal"); return call != null ? (uint)call.GetArguments()[0]! : null; } /// /// Bulbapedia: "The user plants roots" — using the move attaches the /// volatile to the user. /// [Test] public async Task OnSecondaryEffect_Used_AddsIngrainEffectToUser() { // Arrange var script = new Ingrain(); var move = Substitute.For(); var user = Substitute.For(); user.GetScripts().Returns(_ => new ScriptIterator(new List>())); var userVolatile = new ScriptSet(user); user.Volatile.Returns(userVolatile); move.User.Returns(user); var target = Substitute.For(); // Act script.OnSecondaryEffect(move, target, 0); // Assert await Assert.That(userVolatile.Get()).IsNotNull(); } /// /// Bulbapedia: the roots restore "1/16th maximum HP each turn". /// [Test, Arguments(160u, 10u), Arguments(100u, 6u), Arguments(17u, 1u)] public async Task OnEndTurn_RootedPokemon_HealsOneSixteenthOfMaxHp(uint maxHealth, uint expectedHeal) { // Arrange var (effect, owner) = CreateRootedPokemon(maxHealth); // Act effect.OnEndTurn(owner, Substitute.For()); // Assert await Assert.That(GetHealAmount(owner)!.Value).IsEqualTo(expectedHeal); } /// /// Bulbapedia: the rooted user becomes "unable to switch out". /// [Test] public async Task PreventSelfSwitch_RootedPokemon_CannotSwitchOut() { // Arrange var (effect, _) = CreateRootedPokemon(); var prevent = false; // Act effect.PreventSelfSwitch(Substitute.For(), ref prevent); // Assert await Assert.That(prevent).IsTrue(); } /// /// Bulbapedia: the user is rooted in place; a rooted wild Pokémon cannot flee the battle. /// [Test] public async Task PreventSelfRunAway_RootedPokemon_CannotFlee() { // Arrange var (effect, _) = CreateRootedPokemon(); var prevent = false; // Act effect.PreventSelfRunAway(Substitute.For(), ref prevent); // Assert await Assert.That(prevent).IsTrue(); } /// /// Bulbapedia: the rooted user cannot be forced out by "forced-switch moves like Roar" — Roar and /// Whirlwind fail against it. /// [Test, Arguments("roar"), Arguments("whirlwind")] public async Task FailIncomingMove_ForcedSwitchMove_MoveFails(string moveName) { // Arrange var (effect, owner) = CreateRootedPokemon(); var incomingMove = Substitute.For(); var moveData = Substitute.For(); moveData.Name.Returns(new Static.Utils.StringKey(moveName)); incomingMove.UseMove.Returns(moveData); var fail = false; // Act effect.FailIncomingMove(incomingMove, owner, ref fail); // Assert await Assert.That(fail).IsTrue(); } /// /// Bulbapedia: only forced-switch moves are blocked by the roots; any other incoming move works /// normally. /// [Test] public async Task FailIncomingMove_RegularMove_MoveDoesNotFail() { // Arrange var (effect, owner) = CreateRootedPokemon(); var incomingMove = Substitute.For(); var moveData = Substitute.For(); moveData.Name.Returns(new Static.Utils.StringKey("tackle")); incomingMove.UseMove.Returns(moveData); var fail = false; // Act effect.FailIncomingMove(incomingMove, owner, ref fail); // Assert await Assert.That(fail).IsFalse(); } /// /// Bulbapedia (Generation IV onwards): the move grounds the affected Pokémon, making them susceptible to /// Ground moves even if Flying-type. For an incoming Ground-type move, the rooted Pokémon's /// Ground-immune types are ignored. /// [Test] public async Task ChangeTypesForIncomingMove_GroundMoveAgainstFlyingType_RemovesGroundImmuneType() { // Arrange var (effect, owner) = CreateRootedPokemon(); var library = LibraryHelpers.LoadLibrary(); owner.Library.Returns(library); var types = library.StaticLibrary.Types; await Assert.That(types.TryGetTypeIdentifier("ground", out var ground)).IsTrue(); await Assert.That(types.TryGetTypeIdentifier("flying", out var flying)).IsTrue(); await Assert.That(types.TryGetTypeIdentifier("grass", out var grass)).IsTrue(); var incomingMove = Substitute.For(); var moveData = Substitute.For(); moveData.MoveType.Returns(ground); incomingMove.UseMove.Returns(moveData); var targetTypes = new List { flying, grass }; // Act effect.ChangeTypesForIncomingMove(incomingMove, owner, 0, targetTypes); // Assert await Assert.That(targetTypes.Contains(flying)).IsFalse(); await Assert.That(targetTypes.Contains(grass)).IsTrue(); } /// /// The grounding only matters for Ground-type moves: any other incoming move sees the rooted Pokémon's /// types unchanged. /// [Test] public async Task ChangeTypesForIncomingMove_NonGroundMove_TypesUnchanged() { // Arrange var (effect, owner) = CreateRootedPokemon(); var library = LibraryHelpers.LoadLibrary(); owner.Library.Returns(library); var types = library.StaticLibrary.Types; await Assert.That(types.TryGetTypeIdentifier("water", out var water)).IsTrue(); await Assert.That(types.TryGetTypeIdentifier("flying", out var flying)).IsTrue(); var incomingMove = Substitute.For(); var moveData = Substitute.For(); moveData.MoveType.Returns(water); incomingMove.UseMove.Returns(moveData); var targetTypes = new List { flying }; // Act effect.ChangeTypesForIncomingMove(incomingMove, owner, 0, targetTypes); // Assert await Assert.That(targetTypes.Contains(flying)).IsTrue(); } /// /// Bulbapedia (Generation VI onwards): "Ghost-type Pokémon under the effects of Ingrain are now able to /// switch out." /// [Test] public async Task PreventSelfSwitch_GhostTypeRooted_CanStillSwitchOut() { // Arrange var (effect, owner) = CreateRootedPokemon(); owner.Types.Returns(new List { new(8, "ghost") }); var prevent = false; var choice = Substitute.For(); choice.User.Returns(owner); // Act effect.PreventSelfSwitch(choice, ref prevent); // Assert await Assert.That(prevent).IsFalse(); } }