Files
PkmnLib.NET/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DestinyBondTests.cs

173 lines
6.8 KiB
C#

using PkmnLib.Dynamic.Models;
using PkmnLib.Dynamic.Models.Choices;
using PkmnLib.Dynamic.ScriptHandling;
using PkmnLib.Dynamic.ScriptHandling.Registry;
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="DestinyBond"/> move script and its <see cref="DestinyBondEffect"/>.
/// Gen VII Bulbapedia behavior: "When Destiny Bond is used, the user gains the Destiny Bound status.
/// Until the user moves again, if the user faints as the direct result of an attack, the attacking
/// Pokémon also faints."
/// </summary>
public class DestinyBondTests
{
/// <summary>
/// Creates a mocked user whose <see cref="IPokemon.Volatile"/> is a real <see cref="ScriptSet"/> so
/// the volatile added by the move can be inspected.
/// </summary>
private static (DestinyBond script, IExecutingMove move, IPokemon user, ScriptSet userVolatile) CreateTestSetup()
{
var script = new DestinyBond();
var move = Substitute.For<IExecutingMove>();
var user = Substitute.For<IPokemon>();
var userVolatile = new ScriptSet(user);
user.Volatile.Returns(userVolatile);
user.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));
move.User.Returns(user);
return (script, move, user, userVolatile);
}
/// <summary>
/// Creates a fainting Pokémon whose battle's <see cref="BattleChoiceQueue.LastRanChoice"/> is a move
/// choice made by the given attacker.
/// </summary>
private static IPokemon CreateFaintingPokemonAttackedBy(IPokemon attacker)
{
var moveChoice = Substitute.For<IMoveChoice>();
moveChoice.User.Returns(attacker);
var queue = new BattleChoiceQueue([moveChoice]);
queue.Dequeue();
var battle = Substitute.For<IBattle>();
battle.ChoiceQueue.Returns(queue);
var battleData = Substitute.For<IPokemonBattleData>();
battleData.Battle.Returns(battle);
var pokemon = Substitute.For<IPokemon>();
pokemon.BattleData.Returns(battleData);
return pokemon;
}
/// <summary>
/// Bulbapedia: "When Destiny Bond is used, the user gains the Destiny Bound status." The move adds the
/// <see cref="DestinyBondEffect"/> volatile script to the user.
/// </summary>
[Test]
public async Task OnSecondaryEffect_Used_AddsDestinyBondEffectToUser()
{
// Arrange
var (script, move, _, userVolatile) = CreateTestSetup();
var target = Substitute.For<IPokemon>();
// Act
script.OnSecondaryEffect(move, target, 0);
// Assert
await Assert.That(userVolatile.Contains(ScriptUtils.ResolveName<DestinyBondEffect>())).IsTrue();
}
/// <summary>
/// Bulbapedia: "if the user faints as the direct result of an attack, the attacking Pokémon also
/// faints." When the Destiny Bound user faints from move damage, the attacker takes damage far in
/// excess of its HP, guaranteeing the faint.
/// </summary>
[Test]
public async Task OnFaint_FaintsFromMoveDamage_AttackerIsDamagedToFaint()
{
// Arrange
var effect = new DestinyBondEffect();
var attacker = Substitute.For<IPokemon>();
attacker.BoostedStats.Returns(new StatisticSet<uint>(100, 1, 1, 1, 1, 1));
var pokemon = CreateFaintingPokemonAttackedBy(attacker);
// Act
effect.OnFaint(pokemon, DamageSource.MoveDamage);
// Assert - the attacker is damaged by well over its max HP (10x) so it always faints
var damageCall = attacker.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage");
await Assert.That(damageCall).IsNotNull();
await Assert.That((uint)damageCall!.GetArguments()[0]!).IsEqualTo(1000u);
}
/// <summary>
/// Bulbapedia: Destiny Bond "bypasses protective abilities like Focus Sash and Sturdy". The faint
/// damage is dealt with force, so scripts cannot reduce or prevent it.
/// </summary>
[Test]
public async Task OnFaint_FaintsFromMoveDamage_DamageIsForced()
{
// Arrange
var effect = new DestinyBondEffect();
var attacker = Substitute.For<IPokemon>();
attacker.BoostedStats.Returns(new StatisticSet<uint>(100, 1, 1, 1, 1, 1));
var pokemon = CreateFaintingPokemonAttackedBy(attacker);
// Act
effect.OnFaint(pokemon, DamageSource.MoveDamage);
// Assert - the forceDamage argument (last parameter) is true
var damageCall = attacker.ReceivedCalls().First(c => c.GetMethodInfo().Name == "Damage");
await Assert.That((bool)damageCall.GetArguments()[3]!).IsTrue();
}
/// <summary>
/// Bulbapedia: "The move doesn't activate from indirect damage like weather or Leech Seed."
/// Fainting from a non-move damage source does not take the attacker down.
/// </summary>
[Test]
public async Task OnFaint_FaintsFromIndirectDamage_AttackerIsNotDamaged()
{
// Arrange
var effect = new DestinyBondEffect();
var attacker = Substitute.For<IPokemon>();
attacker.BoostedStats.Returns(new StatisticSet<uint>(100, 1, 1, 1, 1, 1));
var pokemon = CreateFaintingPokemonAttackedBy(attacker);
// Act
effect.OnFaint(pokemon, DamageSource.Misc);
// Assert
await Assert.That(attacker.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Damage")).IsFalse();
}
/// <summary>
/// Bulbapedia: "Until the user moves again" — the Destiny Bound status wears off when the user makes
/// its next move, so the effect removes itself before the user's move.
/// </summary>
[Test]
public async Task OnBeforeMove_UserMovesAgain_RemovesDestinyBondEffect()
{
// Arrange
var user = Substitute.For<IPokemon>();
var userVolatile = new ScriptSet(user);
user.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));
var effect = new DestinyBondEffect();
userVolatile.Add(effect);
var move = Substitute.For<IExecutingMove>();
// Act
effect.OnBeforeMove(move);
// Assert
await Assert.That(userVolatile.Contains(ScriptUtils.ResolveName<DestinyBondEffect>())).IsFalse();
}
/// <summary>
/// Technical test: fainting without battle data (outside of battle) does nothing and does not throw.
/// </summary>
[Test]
public async Task OnFaint_NoBattleData_DoesNotThrow()
{
// Arrange
var effect = new DestinyBondEffect();
var pokemon = Substitute.For<IPokemon>();
pokemon.BattleData.Returns((IPokemonBattleData?)null);
// Act & Assert
await Assert.That(() => effect.OnFaint(pokemon, DamageSource.MoveDamage)).ThrowsNothing();
}
}