This commit is contained in:
245
Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/FireSpinTests.cs
Normal file
245
Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/FireSpinTests.cs
Normal file
@@ -0,0 +1,245 @@
|
||||
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;
|
||||
|
||||
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the <see cref="FireSpin"/> move script and its <see cref="FireSpinEffect"/> volatile.
|
||||
/// Gen VII Bulbapedia behavior: Fire Spin inflicts damage and traps the target. Generation V: "it now lasts
|
||||
/// for four to five turns." Generation VI onwards: "The end turn damage of Fire Spin is increased from 1/16
|
||||
/// to 1/8 of the target's maximum HP."
|
||||
/// The trapping and end-of-turn damage themselves are implemented by the <see cref="FireSpinEffect"/>
|
||||
/// volatile; the move script is responsible for applying that volatile to the target.
|
||||
/// </summary>
|
||||
public class FireSpinTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a fully mocked test setup for Fire Spin tests. The target gets a real
|
||||
/// <see cref="ScriptSet"/> as its volatile set, so the applied effect can be inspected.
|
||||
/// The battle random is stubbed to return 5, the maximum of Fire Spin's four-to-five turn duration.
|
||||
/// </summary>
|
||||
private static (FireSpin script, IExecutingMove move, IPokemon target, IScriptSet targetVolatile) CreateTestSetup()
|
||||
{
|
||||
var script = new FireSpin();
|
||||
var move = Substitute.For<IExecutingMove>();
|
||||
var target = Substitute.For<IPokemon>();
|
||||
target.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));
|
||||
var targetVolatile = new ScriptSet(target);
|
||||
target.Volatile.Returns(targetVolatile);
|
||||
|
||||
var random = Substitute.For<IBattleRandom>();
|
||||
random.GetInt(Arg.Any<int>(), Arg.Any<int>()).Returns(5);
|
||||
var battle = Substitute.For<IBattle>();
|
||||
battle.Random.Returns(random);
|
||||
var battleData = Substitute.For<IPokemonBattleData>();
|
||||
battleData.Battle.Returns(battle);
|
||||
target.BattleData.Returns(battleData);
|
||||
|
||||
return (script, move, target, targetVolatile);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: Fire Spin traps the target and deals "1/8 of the target's maximum HP" at the end of each
|
||||
/// turn. The move script applies the <see cref="FireSpinEffect"/> volatile, which implements the trap,
|
||||
/// to the target that was hit.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnSecondaryEffect_MoveHits_AddsFireSpinEffectToTargetVolatile()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, target, targetVolatile) = CreateTestSetup();
|
||||
|
||||
// Act
|
||||
script.OnSecondaryEffect(move, target, 0);
|
||||
|
||||
// Assert
|
||||
await Assert.That(targetVolatile.Get<FireSpinEffect>()).IsNotNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Technical test: hitting a target that is already trapped by Fire Spin does not add a second
|
||||
/// <see cref="FireSpinEffect"/>; the existing volatile is reused.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnSecondaryEffect_TargetAlreadyTrapped_DoesNotAddSecondEffect()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, target, targetVolatile) = CreateTestSetup();
|
||||
|
||||
// Act
|
||||
script.OnSecondaryEffect(move, target, 0);
|
||||
script.OnSecondaryEffect(move, target, 0);
|
||||
|
||||
// Assert
|
||||
await Assert.That(targetVolatile.Count).IsEqualTo(1);
|
||||
}
|
||||
|
||||
// ----- FireSpinEffect behavior (the volatile the move applies) -----
|
||||
|
||||
/// <summary>
|
||||
/// Creates a trapped Pokémon substitute with the given maximum HP, together with the
|
||||
/// <see cref="FireSpinEffect"/> that traps it.
|
||||
/// </summary>
|
||||
private static (FireSpinEffect effect, IPokemon owner) CreateTrappedPokemon(uint maxHealth, IPokemon? user = null)
|
||||
{
|
||||
var owner = Substitute.For<IPokemon>();
|
||||
owner.BoostedStats.Returns(new StatisticSet<uint>(maxHealth, 1, 1, 1, 1, 1));
|
||||
owner.MaxHealth.Returns(maxHealth);
|
||||
owner.Types.Returns(new List<TypeIdentifier> { new(10, "fire") });
|
||||
return (new FireSpinEffect(owner, 5, user ?? Substitute.For<IPokemon>()), owner);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper to extract the damage amount from a Pokémon's received Damage calls.
|
||||
/// </summary>
|
||||
private static uint? GetDamageAmount(IPokemon pokemon)
|
||||
{
|
||||
var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage");
|
||||
return call != null ? (uint)call.GetArguments()[0]! : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia (Generation VI onwards): "The end turn damage of Fire Spin is increased from 1/16 to 1/8
|
||||
/// of the target's maximum HP."
|
||||
/// </summary>
|
||||
[Test, Arguments(96u, 12u), Arguments(100u, 12u), Arguments(120u, 15u), Arguments(8u, 1u)]
|
||||
public async Task OnEndTurn_TrappedPokemon_TakesOneEighthOfMaxHpAsDamage(uint maxHealth, uint expectedDamage)
|
||||
{
|
||||
// Arrange
|
||||
var (effect, owner) = CreateTrappedPokemon(maxHealth);
|
||||
|
||||
// Act
|
||||
effect.OnEndTurn(owner, Substitute.For<IBattle>());
|
||||
|
||||
// Assert
|
||||
await Assert.That(GetDamageAmount(owner)!.Value).IsEqualTo(expectedDamage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: the trap deals "end turn damage"; this is indirect damage, not move damage, so it uses
|
||||
/// <see cref="DamageSource.Misc"/>.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnEndTurn_TrappedPokemon_DamageIsIndirect()
|
||||
{
|
||||
// Arrange
|
||||
var (effect, owner) = CreateTrappedPokemon(100);
|
||||
|
||||
// Act
|
||||
effect.OnEndTurn(owner, Substitute.For<IBattle>());
|
||||
|
||||
// Assert
|
||||
var call = owner.ReceivedCalls().First(c => c.GetMethodInfo().Name == "Damage");
|
||||
await Assert.That((DamageSource)call.GetArguments()[1]!).IsEqualTo(DamageSource.Misc);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia (Generation II onwards): Fire Spin "trapped the opponent, preventing switching."
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task PreventSelfSwitch_TrappedPokemon_CannotSwitchOut()
|
||||
{
|
||||
// Arrange
|
||||
var (effect, owner) = CreateTrappedPokemon(100);
|
||||
var choice = Substitute.For<ISwitchChoice>();
|
||||
choice.User.Returns(owner);
|
||||
var prevent = false;
|
||||
|
||||
// Act
|
||||
effect.PreventSelfSwitch(choice, ref prevent);
|
||||
|
||||
// Assert
|
||||
await Assert.That(prevent).IsTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: Fire Spin traps the target; a trapped wild Pokémon cannot flee (the Generation III-IV
|
||||
/// text lists Run Away and a held Smoke Ball as the only ways a wild Pokémon can still escape).
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task PreventSelfRunAway_TrappedPokemon_CannotFlee()
|
||||
{
|
||||
// Arrange
|
||||
var (effect, owner) = CreateTrappedPokemon(100);
|
||||
var choice = Substitute.For<IFleeChoice>();
|
||||
choice.User.Returns(owner);
|
||||
var prevent = false;
|
||||
|
||||
// Act
|
||||
effect.PreventSelfRunAway(choice, ref prevent);
|
||||
|
||||
// Assert
|
||||
await Assert.That(prevent).IsTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia (Generation VI onwards): "Ghost-type Pokémon cannot be trapped by Fire Spin." A trapped
|
||||
/// Ghost-type Pokémon must remain free to switch out.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task PreventSelfSwitch_GhostTypeOwner_CanStillSwitchOut()
|
||||
{
|
||||
// Arrange
|
||||
var (effect, owner) = CreateTrappedPokemon(100);
|
||||
owner.Types.Returns(new List<TypeIdentifier> { new(8, "ghost") });
|
||||
var choice = Substitute.For<ISwitchChoice>();
|
||||
choice.User.Returns(owner);
|
||||
var prevent = false;
|
||||
|
||||
// Act
|
||||
effect.PreventSelfSwitch(choice, ref prevent);
|
||||
|
||||
// Assert
|
||||
await Assert.That(prevent).IsFalse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia (Generation VI onwards): "If the user is holding a Binding Band, the end turn damage of
|
||||
/// Fire Spin will increase to 1/6 of the target's maximum HP."
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnEndTurn_UserHoldingBindingBand_DamageIsOneSixthOfMaxHp()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, target, targetVolatile) = CreateTestSetup();
|
||||
var user = Substitute.For<IPokemon>();
|
||||
user.HasHeldItem("binding_band").Returns(true);
|
||||
move.User.Returns(user);
|
||||
target.BoostedStats.Returns(new StatisticSet<uint>(120, 1, 1, 1, 1, 1));
|
||||
target.MaxHealth.Returns(120u);
|
||||
|
||||
// Act
|
||||
script.OnSecondaryEffect(move, target, 0);
|
||||
var effect = targetVolatile.Get<FireSpinEffect>();
|
||||
effect!.OnEndTurn(target, Substitute.For<IBattle>());
|
||||
|
||||
// Assert - 1/6 of 120 max HP
|
||||
await Assert.That(GetDamageAmount(target)!.Value).IsEqualTo(20u);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia (Generation V onwards): "it now lasts for four to five turns." After at most five
|
||||
/// end-of-turn ticks the trap must have removed itself from the target.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnEndTurn_AfterFiveEndOfTurns_TrapHasEnded()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, target, targetVolatile) = CreateTestSetup();
|
||||
target.BoostedStats.Returns(new StatisticSet<uint>(80, 1, 1, 1, 1, 1));
|
||||
script.OnSecondaryEffect(move, target, 0);
|
||||
var effect = targetVolatile.Get<FireSpinEffect>()!;
|
||||
var battle = Substitute.For<IBattle>();
|
||||
|
||||
// Act - five end-of-turn ticks, the maximum duration of the trap
|
||||
for (var i = 0; i < 5; i++)
|
||||
effect.OnEndTurn(target, battle);
|
||||
|
||||
// Assert
|
||||
await Assert.That(targetVolatile.Contains("fire_spin")).IsFalse();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user