using PkmnLib.Dynamic.Models; using PkmnLib.Dynamic.ScriptHandling; using PkmnLib.Plugin.Gen7.Scripts.Moves; using PkmnLib.Plugin.Gen7.Scripts.Pokemon; using PkmnLib.Static; using PkmnLib.Static.Species; using PkmnLib.Static.Utils; namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// /// Tests for the move script and its volatile effect script /// . /// Bulbapedia: "Leech Seed plants a seed on the target.", with the in-game description "A seed is planted /// on the target. It steals some HP from the target every turn." In Generation VII the seed drains 1/8 of /// the seeded Pokémon's maximum HP at the end of each turn and restores it to the placer; Grass-type /// Pokémon are unaffected by Leech Seed. /// public class LeechSeedTests { /// /// Creates a fully mocked test setup for the move script, with a target of the /// given types. /// private static (LeechSeed script, IExecutingMove move, IBattlePokemon target, IScriptSet targetVolatile, IHitData hitData) CreateMoveSetup(params TypeIdentifier[] targetTypes) { var script = new LeechSeed(); var move = Substitute.For(); var user = Substitute.For(); move.User.Returns(user); var target = Substitute.For(); target.Types.Returns(targetTypes); var targetVolatile = Substitute.For(); target.Volatile.Returns(targetVolatile); var hitData = Substitute.For(); move.GetHitData(target, 0).Returns(hitData); return (script, move, target, targetVolatile, hitData); } /// /// Creates a mocked seeded Pokémon and placer for tests. /// private static (LeechSeedEffect effect, IBattlePokemon seeded, IBattlePokemon placer) CreateEffectSetup(uint maxHp, uint currentHp) { var seeded = Substitute.For(); seeded.MaxHealth.Returns(maxHp); seeded.CurrentHealth.Returns(currentHp); var placer = Substitute.For(); var effect = new LeechSeedEffect(seeded, placer); return (effect, seeded, placer); } /// /// Helper to extract the heal amount from a substitute's received Heal calls, or null when Heal was /// never called. /// private static uint? GetHealAmount(IBattlePokemon pokemon) { var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Heal"); return call != null ? (uint)call.GetArguments()[0]! : null; } /// /// Helper to extract the damage amount from a substitute's received Damage calls, or null when Damage /// was never called. /// private static uint? GetDamageAmount(IBattlePokemon pokemon) { var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage"); return call != null ? (uint)call.GetArguments()[0]! : null; } /// /// Bulbapedia: "Leech Seed plants a seed on the target." — using the move places the /// on the target. /// [Test] public async Task OnSecondaryEffect_NonGrassTarget_AddsLeechSeedEffect() { // Arrange var (script, move, target, targetVolatile, hitData) = CreateMoveSetup(new TypeIdentifier(11, "water")); // Act script.OnSecondaryEffect(move, target, 0); // Assert targetVolatile.Received(1).Add(Arg.Any()); hitData.DidNotReceive().Fail(); await Assert.That(targetVolatile.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Add")).IsTrue(); } /// /// Grass-type Pokémon are unaffected by Leech Seed — against a Grass-type target the move fails and no /// seed is planted. /// [Test] public async Task OnSecondaryEffect_GrassTarget_MoveFailsAndNoSeedPlanted() { // Arrange var (script, move, target, targetVolatile, hitData) = CreateMoveSetup(new TypeIdentifier(4, "grass")); // Act script.OnSecondaryEffect(move, target, 0); // Assert hitData.Received(1).Fail(); await Assert.That(targetVolatile.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Add")).IsFalse(); } /// /// Grass-type Pokémon are unaffected by Leech Seed — this includes dual types where grass is the /// secondary type. /// [Test] public async Task OnSecondaryEffect_DualTypeGrassTarget_MoveFails() { // Arrange var (script, move, target, _, hitData) = CreateMoveSetup(new TypeIdentifier(7, "poison"), new TypeIdentifier(4, "grass")); // Act script.OnSecondaryEffect(move, target, 0); // Assert hitData.Received(1).Fail(); await Assert.That(hitData.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Fail")).IsTrue(); } /// /// Bulbapedia: "It steals some HP from the target every turn." — at the end of each turn the seeded /// Pokémon loses 1/8 of its maximum HP (integer division) and the placer is healed by the same amount. /// [Test, Arguments(80u, 10u), Arguments(100u, 12u), Arguments(1000u, 125u)] public async Task OnEndTurn_SeededPokemon_DrainsOneEighthOfMaxHpToPlacer(uint maxHp, uint expectedDrain) { // Arrange var (effect, seeded, placer) = CreateEffectSetup(maxHp, maxHp); // Act effect.OnEndTurn(Substitute.For(), Substitute.For()); // Assert await Assert.That(GetDamageAmount(seeded)!.Value).IsEqualTo(expectedDrain); await Assert.That(GetHealAmount(placer)!.Value).IsEqualTo(expectedDrain); } /// /// The drained amount cannot exceed the seeded Pokémon's remaining HP — with less HP left than the 1/8 /// drain, only the remaining HP is drained and transferred. /// [Test] public async Task OnEndTurn_SeededPokemonHpBelowDrain_DrainCappedAtRemainingHp() { // Arrange - 1/8 of 80 is 10, but only 4 HP remains var (effect, seeded, placer) = CreateEffectSetup(80, 4); // Act effect.OnEndTurn(Substitute.For(), Substitute.For()); // Assert await Assert.That(GetDamageAmount(seeded)!.Value).IsEqualTo(4u); await Assert.That(GetHealAmount(placer)!.Value).IsEqualTo(4u); } /// /// When the seeded Pokémon has the Liquid Ooze Ability, the placer loses the drained HP instead of /// being healed by it. /// [Test] public async Task OnEndTurn_SeededPokemonHasLiquidOoze_PlacerDamagedInsteadOfHealed() { // Arrange var (effect, seeded, placer) = CreateEffectSetup(80, 80); var ability = Substitute.For(); ability.Name.Returns(new StringKey("liquid_ooze")); seeded.ActiveAbility.Returns(ability); // Act effect.OnEndTurn(Substitute.For(), Substitute.For()); // Assert await Assert.That(GetDamageAmount(placer)!.Value).IsEqualTo(10u); await Assert.That(placer.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Heal")).IsFalse(); } /// /// The end-of-turn drain is indirect damage, not move damage — both the seeded Pokémon's HP loss and a /// Liquid Ooze recoil use . /// [Test] public async Task OnEndTurn_SeededPokemon_UsesMiscDamageSource() { // Arrange var (effect, seeded, _) = CreateEffectSetup(80, 80); // Act effect.OnEndTurn(Substitute.For(), Substitute.For()); // Assert var call = seeded.ReceivedCalls().First(c => c.GetMethodInfo().Name == "Damage"); await Assert.That((DamageSource)call.GetArguments()[1]!).IsEqualTo(DamageSource.Misc); } }