173 lines
6.8 KiB
C#
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();
|
|
}
|
|
} |