205 lines
8.0 KiB
C#
205 lines
8.0 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Tests for the <see cref="LeechSeed"/> move script and its volatile effect script
|
|
/// <see cref="LeechSeedEffect"/>.
|
|
/// 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.
|
|
/// </summary>
|
|
public class LeechSeedTests
|
|
{
|
|
/// <summary>
|
|
/// Creates a fully mocked test setup for the <see cref="LeechSeed"/> move script, with a target of the
|
|
/// given types.
|
|
/// </summary>
|
|
private static (LeechSeed script, IExecutingMove move, IBattlePokemon target, IScriptSet targetVolatile, IHitData
|
|
hitData) CreateMoveSetup(params TypeIdentifier[] targetTypes)
|
|
{
|
|
var script = new LeechSeed();
|
|
var move = Substitute.For<IExecutingMove>();
|
|
var user = Substitute.For<IBattlePokemon>();
|
|
move.User.Returns(user);
|
|
var target = Substitute.For<IBattlePokemon>();
|
|
target.Types.Returns(targetTypes);
|
|
var targetVolatile = Substitute.For<IScriptSet>();
|
|
target.Volatile.Returns(targetVolatile);
|
|
var hitData = Substitute.For<IHitData>();
|
|
move.GetHitData(target, 0).Returns(hitData);
|
|
return (script, move, target, targetVolatile, hitData);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a mocked seeded Pokémon and placer for <see cref="LeechSeedEffect"/> tests.
|
|
/// </summary>
|
|
private static (LeechSeedEffect effect, IBattlePokemon seeded, IBattlePokemon placer) CreateEffectSetup(uint maxHp,
|
|
uint currentHp)
|
|
{
|
|
var seeded = Substitute.For<IBattlePokemon>();
|
|
seeded.MaxHealth.Returns(maxHp);
|
|
seeded.CurrentHealth.Returns(currentHp);
|
|
var placer = Substitute.For<IBattlePokemon>();
|
|
var effect = new LeechSeedEffect(seeded, placer);
|
|
return (effect, seeded, placer);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper to extract the heal amount from a substitute's received Heal calls, or null when Heal was
|
|
/// never called.
|
|
/// </summary>
|
|
private static uint? GetHealAmount(IBattlePokemon pokemon)
|
|
{
|
|
var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Heal");
|
|
return call != null ? (uint)call.GetArguments()[0]! : null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper to extract the damage amount from a substitute's received Damage calls, or null when Damage
|
|
/// was never called.
|
|
/// </summary>
|
|
private static uint? GetDamageAmount(IBattlePokemon pokemon)
|
|
{
|
|
var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage");
|
|
return call != null ? (uint)call.GetArguments()[0]! : null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "Leech Seed plants a seed on the target." — using the move places the
|
|
/// <see cref="LeechSeedEffect"/> on the target.
|
|
/// </summary>
|
|
[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<LeechSeedEffect>());
|
|
hitData.DidNotReceive().Fail();
|
|
await Assert.That(targetVolatile.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Add")).IsTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Grass-type Pokémon are unaffected by Leech Seed — against a Grass-type target the move fails and no
|
|
/// seed is planted.
|
|
/// </summary>
|
|
[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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Grass-type Pokémon are unaffected by Leech Seed — this includes dual types where grass is the
|
|
/// secondary type.
|
|
/// </summary>
|
|
[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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
[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<IScriptSource>(), Substitute.For<IBattle>());
|
|
|
|
// Assert
|
|
await Assert.That(GetDamageAmount(seeded)!.Value).IsEqualTo(expectedDrain);
|
|
await Assert.That(GetHealAmount(placer)!.Value).IsEqualTo(expectedDrain);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
[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<IScriptSource>(), Substitute.For<IBattle>());
|
|
|
|
// Assert
|
|
await Assert.That(GetDamageAmount(seeded)!.Value).IsEqualTo(4u);
|
|
await Assert.That(GetHealAmount(placer)!.Value).IsEqualTo(4u);
|
|
}
|
|
|
|
/// <summary>
|
|
/// When the seeded Pokémon has the Liquid Ooze Ability, the placer loses the drained HP instead of
|
|
/// being healed by it.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnEndTurn_SeededPokemonHasLiquidOoze_PlacerDamagedInsteadOfHealed()
|
|
{
|
|
// Arrange
|
|
var (effect, seeded, placer) = CreateEffectSetup(80, 80);
|
|
var ability = Substitute.For<IAbility>();
|
|
ability.Name.Returns(new StringKey("liquid_ooze"));
|
|
seeded.ActiveAbility.Returns(ability);
|
|
|
|
// Act
|
|
effect.OnEndTurn(Substitute.For<IScriptSource>(), Substitute.For<IBattle>());
|
|
|
|
// Assert
|
|
await Assert.That(GetDamageAmount(placer)!.Value).IsEqualTo(10u);
|
|
await Assert.That(placer.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Heal")).IsFalse();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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 <see cref="DamageSource.Misc"/>.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnEndTurn_SeededPokemon_UsesMiscDamageSource()
|
|
{
|
|
// Arrange
|
|
var (effect, seeded, _) = CreateEffectSetup(80, 80);
|
|
|
|
// Act
|
|
effect.OnEndTurn(Substitute.For<IScriptSource>(), Substitute.For<IBattle>());
|
|
|
|
// Assert
|
|
var call = seeded.ReceivedCalls().First(c => c.GetMethodInfo().Name == "Damage");
|
|
await Assert.That((DamageSource)call.GetArguments()[1]!).IsEqualTo(DamageSource.Misc);
|
|
}
|
|
} |