More unit tests, fixes
This commit is contained in:
@@ -0,0 +1,283 @@
|
|||||||
|
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.Moves;
|
||||||
|
using PkmnLib.Static.Utils;
|
||||||
|
|
||||||
|
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests for the <see cref="BanefulBunker"/> move script and its attached <see cref="BanefulBunkerEffect"/>.
|
||||||
|
/// Behavior is verified against the Bulbapedia page for Baneful Bunker (Generation VII).
|
||||||
|
/// </summary>
|
||||||
|
public class BanefulBunkerTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a fully mocked setup for driving <see cref="BanefulBunker"/>'s inherited
|
||||||
|
/// <see cref="ProtectionScript.OnSecondaryEffect"/>. The <c>target</c> of the secondary effect is the
|
||||||
|
/// Pokémon using Baneful Bunker itself, as the move is self-targeted.
|
||||||
|
/// </summary>
|
||||||
|
private static (BanefulBunker script, IExecutingMove move, IPokemon target, IHitData hitData, IScriptSet volatileSet
|
||||||
|
) CreateProtectSetup(bool userMovesLast, float randomRoll)
|
||||||
|
{
|
||||||
|
var script = new BanefulBunker();
|
||||||
|
var move = Substitute.For<IExecutingMove>();
|
||||||
|
var target = Substitute.For<IPokemon>();
|
||||||
|
var hitData = Substitute.For<IHitData>();
|
||||||
|
move.GetHitData(target, 0).Returns(hitData);
|
||||||
|
|
||||||
|
// A queue with a remaining choice means another Pokémon still has to move after the user.
|
||||||
|
var queue = userMovesLast ? new BattleChoiceQueue([]) : new BattleChoiceQueue([Substitute.For<IMoveChoice>()]);
|
||||||
|
|
||||||
|
var random = Substitute.For<IBattleRandom>();
|
||||||
|
random.GetFloat().Returns(randomRoll);
|
||||||
|
|
||||||
|
var battle = Substitute.For<IBattle>();
|
||||||
|
battle.ChoiceQueue.Returns(queue);
|
||||||
|
battle.Random.Returns(random);
|
||||||
|
|
||||||
|
var battleData = Substitute.For<IPokemonBattleData>();
|
||||||
|
battleData.Battle.Returns(battle);
|
||||||
|
target.BattleData.Returns(battleData);
|
||||||
|
|
||||||
|
// Give the mock a real script iterator (used by the volatile add hook) and a real volatile script set.
|
||||||
|
target.GetScripts().Returns(_ => new ScriptIterator(Array.Empty<IEnumerable<ScriptContainer>>()));
|
||||||
|
IScriptSet volatileSet = new ScriptSet(target);
|
||||||
|
target.Volatile.Returns(volatileSet);
|
||||||
|
|
||||||
|
return (script, move, target, hitData, volatileSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a fully mocked setup for driving <see cref="BanefulBunkerEffect.BlockIncomingHit"/>, the
|
||||||
|
/// volatile script that <see cref="BanefulBunker"/> attaches to its user.
|
||||||
|
/// </summary>
|
||||||
|
private static (BanefulBunkerEffect effect, IExecutingMove move, IPokemon target, IPokemon attacker)
|
||||||
|
CreateBlockSetup(bool isContact, bool hasProtectFlag, MoveCategory category = MoveCategory.Physical)
|
||||||
|
{
|
||||||
|
var effect = new BanefulBunkerEffect();
|
||||||
|
var move = Substitute.For<IExecutingMove>();
|
||||||
|
var target = Substitute.For<IPokemon>();
|
||||||
|
var hitData = Substitute.For<IHitData>();
|
||||||
|
hitData.IsContact.Returns(isContact);
|
||||||
|
move.GetHitData(target, 0).Returns(hitData);
|
||||||
|
|
||||||
|
var useMove = Substitute.For<IMoveData>();
|
||||||
|
useMove.HasFlag(new StringKey("protect")).Returns(hasProtectFlag);
|
||||||
|
useMove.Category.Returns(category);
|
||||||
|
move.UseMove.Returns(useMove);
|
||||||
|
|
||||||
|
var attacker = Substitute.For<IPokemon>();
|
||||||
|
attacker.GetScripts().Returns(_ => new ScriptIterator(Array.Empty<IEnumerable<ScriptContainer>>()));
|
||||||
|
move.User.Returns(attacker);
|
||||||
|
|
||||||
|
target.BattleData.Returns(Substitute.For<IPokemonBattleData>());
|
||||||
|
|
||||||
|
return (effect, move, target, attacker);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper to extract the status name from a Pokémon's received <see cref="IPokemon.SetStatus"/> calls.
|
||||||
|
/// </summary>
|
||||||
|
private static string? GetStatusSet(IPokemon pokemon)
|
||||||
|
{
|
||||||
|
var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "SetStatus");
|
||||||
|
return call != null ? ((StringKey)call.GetArguments()[0]!).ToString() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Baneful Bunker protects the user from all effects of moves that target it during the turn
|
||||||
|
/// it is used, including damage."
|
||||||
|
/// Using the move attaches the <see cref="BanefulBunkerEffect"/> volatile script to the user.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_FirstUse_AddsBanefulBunkerEffect()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, target, hitData, volatileSet) = CreateProtectSetup(false, 0.0f);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(volatileSet.Get<BanefulBunkerEffect>()).IsNotNull();
|
||||||
|
hitData.DidNotReceive().Fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "If the user goes last in the turn, the move will fail."
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_UserMovesLast_Fails()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, target, hitData, volatileSet) = CreateProtectSetup(true, 0.0f);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
hitData.Received(1).Fail();
|
||||||
|
await Assert.That(volatileSet.Get<BanefulBunkerEffect>()).IsNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "The chance that Baneful Bunker will succeed also drops each time the user successfully and
|
||||||
|
/// consecutively uses Endure, any protection move that only affects the user, Quick Guard, or Wide Guard."
|
||||||
|
/// A successful use registers a consecutive protection turn on the <see cref="ProtectionFailureScript"/>.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_FirstUse_RegistersConsecutiveProtectionUse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, target, _, volatileSet) = CreateProtectSetup(false, 0.0f);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var failure = volatileSet.Get<ProtectionFailureScript>();
|
||||||
|
await Assert.That(failure).IsNotNull();
|
||||||
|
await Assert.That(failure!.ProtectTurns).IsEqualTo(1);
|
||||||
|
await Assert.That(failure.UsedProtect).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Each time, the chance of success is divided by 3."
|
||||||
|
/// After one consecutive protection use the chance is 1/3, so a roll above 1/3 makes the move fail.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_SecondConsecutiveUse_RollAboveOneThird_Fails()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, target, hitData, volatileSet) = CreateProtectSetup(false, 0.5f);
|
||||||
|
volatileSet.Add(new ProtectionFailureScript { ProtectTurns = 1 });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
hitData.Received(1).Fail();
|
||||||
|
await Assert.That(volatileSet.Get<BanefulBunkerEffect>()).IsNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Each time, the chance of success is divided by 3."
|
||||||
|
/// After one consecutive protection use the chance is 1/3, so a roll below 1/3 still succeeds and
|
||||||
|
/// increments the consecutive use counter.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_SecondConsecutiveUse_RollBelowOneThird_Succeeds()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, target, hitData, volatileSet) = CreateProtectSetup(false, 0.2f);
|
||||||
|
var failure = new ProtectionFailureScript { ProtectTurns = 1 };
|
||||||
|
volatileSet.Add(failure);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
hitData.DidNotReceive().Fail();
|
||||||
|
await Assert.That(volatileSet.Get<BanefulBunkerEffect>()).IsNotNull();
|
||||||
|
await Assert.That(failure.ProtectTurns).IsEqualTo(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Baneful Bunker protects the user from all effects of moves that target it during the turn
|
||||||
|
/// it is used, including damage."
|
||||||
|
/// A move that can be protected against (it has the protect flag) is blocked by the
|
||||||
|
/// <see cref="BanefulBunkerEffect"/>.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task BlockIncomingHit_MoveWithProtectFlag_BlocksHit()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (effect, move, target, _) = CreateBlockSetup(false, true);
|
||||||
|
var block = false;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
effect.BlockIncomingHit(move, target, 0, ref block);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(block).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Baneful Bunker protects the user from all effects of moves that target it"; moves that
|
||||||
|
/// bypass protection (they lack the protect flag) are not blocked by the <see cref="BanefulBunkerEffect"/>.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task BlockIncomingHit_MoveWithoutProtectFlag_DoesNotBlock()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (effect, move, target, _) = CreateBlockSetup(false, false);
|
||||||
|
var block = false;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
effect.BlockIncomingHit(move, target, 0, ref block);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(block).IsFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "If Baneful Bunker blocks an attack that would make contact with the user, the attacker
|
||||||
|
/// becomes poisoned (unless they are immune)."
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task BlockIncomingHit_BlockedContactMove_PoisonsAttacker()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (effect, move, target, attacker) = CreateBlockSetup(true, true);
|
||||||
|
var block = false;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
effect.BlockIncomingHit(move, target, 0, ref block);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(GetStatusSet(attacker)).IsEqualTo("poisoned");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "If Baneful Bunker blocks an attack that would make contact with the user, the attacker
|
||||||
|
/// becomes poisoned (unless they are immune)."
|
||||||
|
/// A blocked attack that does not make contact must not poison the attacker.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task BlockIncomingHit_BlockedNonContactMove_DoesNotPoisonAttacker()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (effect, move, target, attacker) = CreateBlockSetup(false, true);
|
||||||
|
var block = false;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
effect.BlockIncomingHit(move, target, 0, ref block);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(attacker.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "SetStatus")).IsFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "If Baneful Bunker blocks an attack that would make contact with the user, the attacker
|
||||||
|
/// becomes poisoned (unless they are immune)."
|
||||||
|
/// A contact move that bypasses the protection (it lacks the protect flag, e.g. Shadow Force) is not
|
||||||
|
/// blocked, so the attacker must not be poisoned.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task BlockIncomingHit_ContactMoveThatBypassesProtection_DoesNotPoisonAttacker()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (effect, move, target, attacker) = CreateBlockSetup(true, false);
|
||||||
|
var block = false;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
effect.BlockIncomingHit(move, target, 0, ref block);
|
||||||
|
|
||||||
|
// Assert - the hit was not blocked, so no poison is applied
|
||||||
|
await Assert.That(block).IsFalse();
|
||||||
|
await Assert.That(attacker.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "SetStatus")).IsFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
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.Plugin.Gen7.Scripts.Utils;
|
||||||
|
using PkmnLib.Static;
|
||||||
|
using PkmnLib.Static.Utils;
|
||||||
|
|
||||||
|
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests for the <see cref="BatonPass"/> move script.
|
||||||
|
/// Behavior is verified against the Bulbapedia page for Baton Pass (Generation VII).
|
||||||
|
/// In this codebase, volatile scripts that are transferred by Baton Pass are marked with
|
||||||
|
/// <see cref="IBatonPassException"/> (e.g. <see cref="AutotomizeEffect"/>).
|
||||||
|
/// </summary>
|
||||||
|
public class BatonPassTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a mocked Pokémon with a real volatile <see cref="ScriptSet"/> and a real
|
||||||
|
/// <see cref="StatBoostStatisticSet"/>.
|
||||||
|
/// </summary>
|
||||||
|
private static IPokemon CreateMockPokemon(out IScriptSet volatileSet)
|
||||||
|
{
|
||||||
|
var pokemon = Substitute.For<IPokemon>();
|
||||||
|
pokemon.GetScripts().Returns(_ => new ScriptIterator(Array.Empty<IEnumerable<ScriptContainer>>()));
|
||||||
|
var set = new ScriptSet(pokemon);
|
||||||
|
pokemon.Volatile.Returns(set);
|
||||||
|
pokemon.StatBoost.Returns(new StatBoostStatisticSet());
|
||||||
|
volatileSet = set;
|
||||||
|
return pokemon;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a fully mocked test setup for Baton Pass tests. The Pokémon to switch in is stored in the
|
||||||
|
/// move choice's <see cref="IMoveChoice.AdditionalData"/> under the <c>to_switch</c> key.
|
||||||
|
/// </summary>
|
||||||
|
private static (BatonPass script, IExecutingMove move, IPokemon user, IPokemon toSwitch, IBattleSide side,
|
||||||
|
IScriptSet userVolatile, IScriptSet switchInVolatile) CreateTestSetup()
|
||||||
|
{
|
||||||
|
var script = new BatonPass();
|
||||||
|
var move = Substitute.For<IExecutingMove>();
|
||||||
|
var user = CreateMockPokemon(out var userVolatile);
|
||||||
|
var toSwitch = CreateMockPokemon(out var switchInVolatile);
|
||||||
|
|
||||||
|
var moveChoice = Substitute.For<IMoveChoice>();
|
||||||
|
moveChoice.AdditionalData.Returns(new Dictionary<StringKey, object?> { { "to_switch", toSwitch } });
|
||||||
|
move.MoveChoice.Returns(moveChoice);
|
||||||
|
|
||||||
|
var side = Substitute.For<IBattleSide>();
|
||||||
|
var battle = Substitute.For<IBattle>();
|
||||||
|
battle.Sides.Returns(new[] { side });
|
||||||
|
|
||||||
|
var battleData = Substitute.For<IPokemonBattleData>();
|
||||||
|
battleData.Battle.Returns(battle);
|
||||||
|
battleData.SideIndex.Returns((byte)0);
|
||||||
|
battleData.Position.Returns((byte)1);
|
||||||
|
user.BattleData.Returns(battleData);
|
||||||
|
|
||||||
|
move.User.Returns(user);
|
||||||
|
return (script, move, user, toSwitch, side, userVolatile, switchInVolatile);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Baton Pass switches out the user and passes several effects to the Pokémon switched in
|
||||||
|
/// its place."
|
||||||
|
/// The user is swapped out for the chosen Pokémon on its own field position.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void OnSecondaryEffect_SwitchTargetChosen_SwitchesOutUser()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user, toSwitch, side, _, _) = CreateTestSetup();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, user, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
side.Received(1).SwapPokemon((byte)1, toSwitch);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Baton Pass transfers all temporary stat stage increases and decreases".
|
||||||
|
/// Each stat stage (including evasion and accuracy) is copied to the Pokémon that is switched in.
|
||||||
|
/// </summary>
|
||||||
|
[Test, Arguments(Statistic.Attack, (sbyte)6), Arguments(Statistic.Defense, (sbyte)-6),
|
||||||
|
Arguments(Statistic.SpecialAttack, (sbyte)2), Arguments(Statistic.SpecialDefense, (sbyte)-1),
|
||||||
|
Arguments(Statistic.Speed, (sbyte)3), Arguments(Statistic.Evasion, (sbyte)1),
|
||||||
|
Arguments(Statistic.Accuracy, (sbyte)-2)]
|
||||||
|
public async Task OnSecondaryEffect_StatBoost_TransferredToSwitchIn(Statistic stat, sbyte boost)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user, toSwitch, _, _, _) = CreateTestSetup();
|
||||||
|
user.StatBoost.SetStatistic(stat, boost);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, user, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(toSwitch.StatBoost.GetStatistic(stat)).IsEqualTo(boost);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Baton Pass transfers all temporary stat stage increases and decreases".
|
||||||
|
/// After the stat stages are copied, the switched-in Pokémon's boosted stats are recalculated.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void OnSecondaryEffect_StatBoostsTransferred_RecalculatesSwitchInStats()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user, toSwitch, _, _, _) = CreateTestSetup();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, user, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
toSwitch.Received(1).RecalculateBoostedStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Baton Pass switches out the user and passes several effects to the Pokémon switched in
|
||||||
|
/// its place."
|
||||||
|
/// A volatile script that is marked as transferable (<see cref="IBatonPassException"/>, such as
|
||||||
|
/// <see cref="AutotomizeEffect"/>) is passed to the switched-in Pokémon.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_TransferableVolatile_PassedToSwitchIn()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user, _, _, userVolatile, switchInVolatile) = CreateTestSetup();
|
||||||
|
userVolatile.Add(new AutotomizeEffect());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, user, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(switchInVolatile.Get<AutotomizeEffect>()).IsNotNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Baton Pass also passes some volatile status conditions, namely confusion, getting pumped,
|
||||||
|
/// escape prevention, seeding, Curse, Substitute, Ingrain, Aqua Ring" — effects outside that list are not
|
||||||
|
/// passed. The consecutive-protection counter (<see cref="ProtectionFailureScript"/>) is not transferable,
|
||||||
|
/// so it must not end up on the switched-in Pokémon.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_NonTransferableVolatile_NotPassedToSwitchIn()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user, _, _, userVolatile, switchInVolatile) = CreateTestSetup();
|
||||||
|
userVolatile.Add(new ProtectionFailureScript());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, user, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(switchInVolatile.Get<ProtectionFailureScript>()).IsNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Baton Pass switches out the user and passes several effects to the Pokémon switched in
|
||||||
|
/// its place."
|
||||||
|
/// A transferable volatile must be passed even when the user also has a non-transferable volatile that
|
||||||
|
/// was added before it.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_TransferableVolatileAfterNonTransferable_StillPassedToSwitchIn()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user, _, _, userVolatile, switchInVolatile) = CreateTestSetup();
|
||||||
|
userVolatile.Add(new ProtectionFailureScript());
|
||||||
|
userVolatile.Add(new AutotomizeEffect());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, user, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(switchInVolatile.Get<AutotomizeEffect>()).IsNotNull();
|
||||||
|
await Assert.That(switchInVolatile.Get<ProtectionFailureScript>()).IsNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Baton Pass switches out the user".
|
||||||
|
/// The user's volatile scripts leave the field with it: after the switch its volatile containers are empty.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_UserSwitchedOut_UserVolatilesCleared()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user, _, _, userVolatile, _) = CreateTestSetup();
|
||||||
|
userVolatile.Add(new AutotomizeEffect());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, user, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(userVolatile.Get<AutotomizeEffect>()).IsNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Technical test: without any <see cref="IMoveChoice.AdditionalData"/> there is no Pokémon to switch to,
|
||||||
|
/// so no switch happens.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void OnSecondaryEffect_NoAdditionalData_DoesNotSwitch()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user, _, side, _, _) = CreateTestSetup();
|
||||||
|
move.MoveChoice.AdditionalData.Returns((Dictionary<StringKey, object?>?)null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, user, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
side.DidNotReceive().SwapPokemon(Arg.Any<byte>(), Arg.Any<IPokemon?>());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Technical test: if the stored switch target is not a Pokémon, no switch happens.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void OnSecondaryEffect_SwitchTargetNotAPokemon_DoesNotSwitch()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user, _, side, _, _) = CreateTestSetup();
|
||||||
|
move.MoveChoice.AdditionalData.Returns(new Dictionary<StringKey, object?> { { "to_switch", null } });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, user, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
side.DidNotReceive().SwapPokemon(Arg.Any<byte>(), Arg.Any<IPokemon?>());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Technical test: if the user has no <see cref="IPokemon.BattleData"/>, the script returns without
|
||||||
|
/// switching and without clearing the user's volatile scripts.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_NoBattleData_DoesNotSwitch()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user, _, side, userVolatile, _) = CreateTestSetup();
|
||||||
|
user.BattleData.Returns((IPokemonBattleData?)null);
|
||||||
|
userVolatile.Add(new AutotomizeEffect());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, user, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
side.DidNotReceive().SwapPokemon(Arg.Any<byte>(), Arg.Any<IPokemon?>());
|
||||||
|
await Assert.That(userVolatile.Get<AutotomizeEffect>()).IsNotNull();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
using PkmnLib.Dynamic.Events;
|
||||||
|
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.Utils;
|
||||||
|
|
||||||
|
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests for the <see cref="BeakBlast"/> move script and its attached <see cref="BeakBlastEffect"/>.
|
||||||
|
/// Behavior is verified against the Bulbapedia page for Beak Blast (Generation VII).
|
||||||
|
/// </summary>
|
||||||
|
public class BeakBlastTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a fully mocked setup for driving <see cref="BeakBlast.OnBeforeTurnStart"/>.
|
||||||
|
/// </summary>
|
||||||
|
private static (BeakBlast script, ITurnChoice choice, IPokemon user, IScriptSet volatileSet, EventHook eventHook)
|
||||||
|
CreateChargeSetup(bool hasBattleData = true)
|
||||||
|
{
|
||||||
|
var script = new BeakBlast();
|
||||||
|
var user = Substitute.For<IPokemon>();
|
||||||
|
user.GetScripts().Returns(_ => new ScriptIterator(Array.Empty<IEnumerable<ScriptContainer>>()));
|
||||||
|
IScriptSet volatileSet = new ScriptSet(user);
|
||||||
|
user.Volatile.Returns(volatileSet);
|
||||||
|
|
||||||
|
var eventHook = new EventHook();
|
||||||
|
if (hasBattleData)
|
||||||
|
{
|
||||||
|
var battle = Substitute.For<IBattle>();
|
||||||
|
battle.EventHook.Returns(eventHook);
|
||||||
|
var battleData = Substitute.For<IPokemonBattleData>();
|
||||||
|
battleData.Battle.Returns(battle);
|
||||||
|
user.BattleData.Returns(battleData);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
user.BattleData.Returns((IPokemonBattleData?)null);
|
||||||
|
}
|
||||||
|
|
||||||
|
var choice = Substitute.For<ITurnChoice>();
|
||||||
|
choice.User.Returns(user);
|
||||||
|
|
||||||
|
return (script, choice, user, volatileSet, eventHook);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "The user charges up at the beginning of the turn and executes Beak Blast at -3 priority."
|
||||||
|
/// Before the turn starts, the charging <see cref="BeakBlastEffect"/> is attached to the user.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnBeforeTurnStart_UserInBattle_AddsChargeEffect()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, choice, _, volatileSet, _) = CreateChargeSetup();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnBeforeTurnStart(choice);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(volatileSet.Get<BeakBlastEffect>()).IsNotNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Its charging message is displayed before any other moves."
|
||||||
|
/// The charge fires a dialog event when the turn starts.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnBeforeTurnStart_UserInBattle_FiresChargingMessage()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, choice, user, _, eventHook) = CreateChargeSetup();
|
||||||
|
DialogEvent? capturedEvent = null;
|
||||||
|
eventHook.Handler += (_, args) =>
|
||||||
|
{
|
||||||
|
if (args is DialogEvent dialogEvent)
|
||||||
|
capturedEvent = dialogEvent;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnBeforeTurnStart(choice);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(capturedEvent).IsNotNull();
|
||||||
|
await Assert.That(capturedEvent!.Message).IsEqualTo("beak_blast_charge");
|
||||||
|
await Assert.That(capturedEvent.Parameters!["user"]).IsEqualTo(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Technical test: a Pokémon without <see cref="IPokemon.BattleData"/> is not in battle, so no charging
|
||||||
|
/// phase starts.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnBeforeTurnStart_NoBattleData_DoesNotAddChargeEffect()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, choice, _, volatileSet, _) = CreateChargeSetup(false);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnBeforeTurnStart(choice);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(volatileSet.Get<BeakBlastEffect>()).IsNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "All who make contact with the user during the charging phase are burned."
|
||||||
|
/// The charging phase ends when the move executes, so the <see cref="BeakBlastEffect"/> is removed again.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_MoveExecuted_RemovesChargeEffect()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var script = new BeakBlast();
|
||||||
|
var move = Substitute.For<IExecutingMove>();
|
||||||
|
var target = Substitute.For<IPokemon>();
|
||||||
|
var user = Substitute.For<IPokemon>();
|
||||||
|
user.GetScripts().Returns(_ => new ScriptIterator(Array.Empty<IEnumerable<ScriptContainer>>()));
|
||||||
|
IScriptSet volatileSet = new ScriptSet(user);
|
||||||
|
user.Volatile.Returns(volatileSet);
|
||||||
|
move.User.Returns(user);
|
||||||
|
volatileSet.Add(new BeakBlastEffect());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(volatileSet.Get<BeakBlastEffect>()).IsNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a fully mocked setup for driving <see cref="BeakBlastEffect.OnIncomingHit"/>.
|
||||||
|
/// </summary>
|
||||||
|
private static (BeakBlastEffect effect, IExecutingMove move, IPokemon target, IPokemon attacker)
|
||||||
|
CreateIncomingHitSetup(bool isContact)
|
||||||
|
{
|
||||||
|
var effect = new BeakBlastEffect();
|
||||||
|
var move = Substitute.For<IExecutingMove>();
|
||||||
|
var target = Substitute.For<IPokemon>();
|
||||||
|
var hitData = Substitute.For<IHitData>();
|
||||||
|
hitData.IsContact.Returns(isContact);
|
||||||
|
move.GetHitData(target, 0).Returns(hitData);
|
||||||
|
|
||||||
|
var attacker = Substitute.For<IPokemon>();
|
||||||
|
move.User.Returns(attacker);
|
||||||
|
|
||||||
|
return (effect, move, target, attacker);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper to extract the status name from a Pokémon's received <see cref="IPokemon.SetStatus"/> calls.
|
||||||
|
/// </summary>
|
||||||
|
private static string? GetStatusSet(IPokemon pokemon)
|
||||||
|
{
|
||||||
|
var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "SetStatus");
|
||||||
|
return call != null ? ((StringKey)call.GetArguments()[0]!).ToString() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "All who make contact with the user during the charging phase are burned."
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnIncomingHit_ContactMove_BurnsAttacker()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (effect, move, target, attacker) = CreateIncomingHitSetup(true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
effect.OnIncomingHit(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(GetStatusSet(attacker)).IsEqualTo("burned");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "All who make contact with the user during the charging phase are burned."
|
||||||
|
/// A hit that does not make contact must not burn the attacker.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnIncomingHit_NonContactMove_DoesNotBurnAttacker()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (effect, move, target, attacker) = CreateIncomingHitSetup(false);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
effect.OnIncomingHit(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(attacker.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "SetStatus")).IsFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
229
Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BeatUpTests.cs
Normal file
229
Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BeatUpTests.cs
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
using PkmnLib.Dynamic.Models;
|
||||||
|
using PkmnLib.Dynamic.Models.Choices;
|
||||||
|
using PkmnLib.Dynamic.ScriptHandling;
|
||||||
|
using PkmnLib.Plugin.Gen7.Scripts.Moves;
|
||||||
|
using PkmnLib.Plugin.Gen7.Scripts.Status;
|
||||||
|
using PkmnLib.Static;
|
||||||
|
using PkmnLib.Static.Species;
|
||||||
|
|
||||||
|
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests for the <see cref="BeatUp"/> move script.
|
||||||
|
/// Gen VII Bulbapedia behavior: "Beat Up inflicts damage on the target from the user and from each conscious
|
||||||
|
/// Pokémon in the user's party that does not have a non-volatile status." Since Generation V, "the base power
|
||||||
|
/// per strike is no longer 10, but instead individually based on the Attack base stats of the party Pokémon:
|
||||||
|
/// BasePower = BaseAttack(PartyMember)/10 + 5".
|
||||||
|
/// </summary>
|
||||||
|
public class BeatUpTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a mocked party member with the given base Attack stat, usability, and optional non-volatile
|
||||||
|
/// status script in its <see cref="IPokemon.StatusScript"/>.
|
||||||
|
/// </summary>
|
||||||
|
private static IPokemon CreatePartyMember(ushort baseAttack = 100, bool usable = true, Script? status = null)
|
||||||
|
{
|
||||||
|
var pokemon = Substitute.For<IPokemon>();
|
||||||
|
pokemon.IsUsable.Returns(usable);
|
||||||
|
pokemon.StatusScript.Returns(status == null ? new ScriptContainer() : new ScriptContainer(status));
|
||||||
|
var form = Substitute.For<IForm>();
|
||||||
|
form.BaseStats.Returns(new ImmutableStatisticSet<ushort>(100, baseAttack, 100, 100, 100, 100));
|
||||||
|
pokemon.Form.Returns(form);
|
||||||
|
return pokemon;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a fully mocked test setup where the user and the given other Pokémon form a party in a battle.
|
||||||
|
/// </summary>
|
||||||
|
private static (BeatUp script, IMoveChoice choice, IExecutingMove move) CreateTestSetup(IPokemon user,
|
||||||
|
params IPokemon?[] otherPartyMembers)
|
||||||
|
{
|
||||||
|
var script = new BeatUp();
|
||||||
|
|
||||||
|
var members = new List<IPokemon?> { user };
|
||||||
|
members.AddRange(otherPartyMembers);
|
||||||
|
|
||||||
|
var party = Substitute.For<IPokemonParty>();
|
||||||
|
party.GetEnumerator().Returns(_ => members.GetEnumerator());
|
||||||
|
var battleParty = Substitute.For<IBattleParty>();
|
||||||
|
battleParty.Party.Returns(party);
|
||||||
|
|
||||||
|
var battle = Substitute.For<IBattle>();
|
||||||
|
battle.Parties.Returns(new[] { battleParty });
|
||||||
|
var battleData = Substitute.For<IPokemonBattleData>();
|
||||||
|
battleData.Battle.Returns(battle);
|
||||||
|
user.BattleData.Returns(battleData);
|
||||||
|
|
||||||
|
var choice = Substitute.For<IMoveChoice>();
|
||||||
|
choice.User.Returns(user);
|
||||||
|
var move = Substitute.For<IExecutingMove>();
|
||||||
|
move.User.Returns(user);
|
||||||
|
|
||||||
|
return (script, choice, move);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Beat Up inflicts damage on the target from the user and from each conscious Pokémon in the
|
||||||
|
/// user's party that does not have a non-volatile status."
|
||||||
|
/// With the user and two healthy allies, the move strikes three times.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task ChangeNumberOfHits_AllPartyMembersEligible_OneHitPerMember()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var user = CreatePartyMember();
|
||||||
|
var (script, choice, _) = CreateTestSetup(user, CreatePartyMember(), CreatePartyMember());
|
||||||
|
byte numberOfHits = 1;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.ChangeNumberOfHits(choice, ref numberOfHits);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(numberOfHits).IsEqualTo((byte)3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Beat Up inflicts damage on the target from the user and from each conscious Pokémon in the
|
||||||
|
/// user's party".
|
||||||
|
/// A fainted (not conscious) party member does not contribute a strike.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task ChangeNumberOfHits_FaintedPartyMember_Excluded()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var user = CreatePartyMember();
|
||||||
|
var (script, choice, _) = CreateTestSetup(user, CreatePartyMember(usable: false), CreatePartyMember());
|
||||||
|
byte numberOfHits = 1;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.ChangeNumberOfHits(choice, ref numberOfHits);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(numberOfHits).IsEqualTo((byte)2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "each conscious Pokémon in the user's party that does not have a non-volatile status".
|
||||||
|
/// A party member with a non-volatile status (here <see cref="Burned"/>) does not contribute a strike.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task ChangeNumberOfHits_StatusedPartyMember_Excluded()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var user = CreatePartyMember();
|
||||||
|
var (script, choice, _) = CreateTestSetup(user, CreatePartyMember(status: new Burned()));
|
||||||
|
byte numberOfHits = 1;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.ChangeNumberOfHits(choice, ref numberOfHits);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(numberOfHits).IsEqualTo((byte)1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Technical test: outside of battle (no <see cref="IPokemon.BattleData"/>) there are no relevant party
|
||||||
|
/// members, and the number of hits falls back to a single strike.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task ChangeNumberOfHits_NoBattleData_SingleHit()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var script = new BeatUp();
|
||||||
|
var user = Substitute.For<IPokemon>();
|
||||||
|
user.BattleData.Returns((IPokemonBattleData?)null);
|
||||||
|
var choice = Substitute.For<IMoveChoice>();
|
||||||
|
choice.User.Returns(user);
|
||||||
|
byte numberOfHits = 3;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.ChangeNumberOfHits(choice, ref numberOfHits);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(numberOfHits).IsEqualTo((byte)1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "the base power per strike is no longer 10, but instead individually based on the Attack
|
||||||
|
/// base stats of the party Pokémon: BasePower = BaseAttack(PartyMember)/10 + 5".
|
||||||
|
/// Verifies the formula, including integer truncation of the division.
|
||||||
|
/// </summary>
|
||||||
|
[Test, Arguments((ushort)10, (ushort)6), Arguments((ushort)55, (ushort)10), Arguments((ushort)59, (ushort)10),
|
||||||
|
Arguments((ushort)100, (ushort)15), Arguments((ushort)255, (ushort)30)]
|
||||||
|
public async Task ChangeBasePower_UsesBaseAttackFormula(ushort baseAttack, ushort expectedBasePower)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var user = CreatePartyMember(baseAttack);
|
||||||
|
var (script, _, move) = CreateTestSetup(user);
|
||||||
|
var target = Substitute.For<IPokemon>();
|
||||||
|
ushort basePower = 10;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.ChangeBasePower(move, target, 0, ref basePower);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(basePower).IsEqualTo(expectedBasePower);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "the base power per strike is [...] individually based on the Attack base stats of the party
|
||||||
|
/// Pokémon". The second strike uses the second eligible party member's base Attack, not the user's.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task ChangeBasePower_SecondHit_UsesSecondPartyMembersBaseAttack()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var user = CreatePartyMember(100);
|
||||||
|
var (script, _, move) = CreateTestSetup(user, CreatePartyMember(250));
|
||||||
|
var target = Substitute.For<IPokemon>();
|
||||||
|
ushort basePower = 10;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.ChangeBasePower(move, target, 1, ref basePower);
|
||||||
|
|
||||||
|
// Assert - 250 / 10 + 5 = 30
|
||||||
|
await Assert.That(basePower).IsEqualTo((ushort)30);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: strikes only come from party members "that [do] not have a non-volatile status".
|
||||||
|
/// An ineligible member is skipped entirely, so the strike after the user's uses the next eligible
|
||||||
|
/// member's base Attack.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task ChangeBasePower_StatusedMemberSkipped_UsesNextEligibleMember()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var user = CreatePartyMember(100);
|
||||||
|
var (script, _, move) = CreateTestSetup(user, CreatePartyMember(250, status: new Burned()),
|
||||||
|
CreatePartyMember(60));
|
||||||
|
var target = Substitute.For<IPokemon>();
|
||||||
|
ushort basePower = 10;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.ChangeBasePower(move, target, 1, ref basePower);
|
||||||
|
|
||||||
|
// Assert - the burned member is skipped: 60 / 10 + 5 = 11
|
||||||
|
await Assert.That(basePower).IsEqualTo((ushort)11);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Technical test: a hit index beyond the number of eligible party members leaves the base power unchanged
|
||||||
|
/// instead of throwing.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task ChangeBasePower_HitIndexBeyondEligibleMembers_BasePowerUnchanged()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var user = CreatePartyMember(100);
|
||||||
|
var (script, _, move) = CreateTestSetup(user);
|
||||||
|
var target = Substitute.For<IPokemon>();
|
||||||
|
ushort basePower = 10;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.ChangeBasePower(move, target, 3, ref basePower);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(basePower).IsEqualTo((ushort)10);
|
||||||
|
}
|
||||||
|
}
|
||||||
133
Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BelchTests.cs
Normal file
133
Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BelchTests.cs
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
using PkmnLib.Dynamic.Models;
|
||||||
|
using PkmnLib.Dynamic.Models.Choices;
|
||||||
|
using PkmnLib.Plugin.Gen7.Scripts.Moves;
|
||||||
|
using PkmnLib.Static;
|
||||||
|
|
||||||
|
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests for the <see cref="Belch"/> move script.
|
||||||
|
/// Gen VII Bulbapedia behavior: "Belch deals damage. It cannot be selected unless the user has previously eaten
|
||||||
|
/// a Berry in the current battle."
|
||||||
|
/// </summary>
|
||||||
|
public class BelchTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a fully mocked test setup where the user's <see cref="IPokemonBattleData.ConsumedItems"/>
|
||||||
|
/// contains one item per given <see cref="ItemCategory"/>.
|
||||||
|
/// </summary>
|
||||||
|
private static (Belch script, IMoveChoice choice) CreateTestSetup(params ItemCategory[] consumedItemCategories)
|
||||||
|
{
|
||||||
|
var script = new Belch();
|
||||||
|
var user = Substitute.For<IPokemon>();
|
||||||
|
|
||||||
|
var items = consumedItemCategories.Select(category =>
|
||||||
|
{
|
||||||
|
var item = Substitute.For<IItem>();
|
||||||
|
item.Category.Returns(category);
|
||||||
|
return item;
|
||||||
|
}).ToArray();
|
||||||
|
var battleData = Substitute.For<IPokemonBattleData>();
|
||||||
|
battleData.ConsumedItems.Returns(items);
|
||||||
|
user.BattleData.Returns(battleData);
|
||||||
|
|
||||||
|
var choice = Substitute.For<IMoveChoice>();
|
||||||
|
choice.User.Returns(user);
|
||||||
|
|
||||||
|
return (script, choice);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "It cannot be selected unless the user has previously eaten a Berry in the current battle."
|
||||||
|
/// A user that has eaten a Berry may select Belch.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task PreventMoveSelection_UserAteBerry_SelectionAllowed()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, choice) = CreateTestSetup(ItemCategory.Berry);
|
||||||
|
var prevent = false;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.PreventMoveSelection(choice, ref prevent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(prevent).IsFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "It cannot be selected unless the user has previously eaten a Berry in the current battle."
|
||||||
|
/// A user that has not consumed anything cannot select Belch.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task PreventMoveSelection_NoItemsConsumed_SelectionPrevented()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, choice) = CreateTestSetup();
|
||||||
|
var prevent = false;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.PreventMoveSelection(choice, ref prevent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(prevent).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "It cannot be selected unless the user has previously eaten a Berry in the current battle."
|
||||||
|
/// Having consumed only non-Berry items does not satisfy the requirement.
|
||||||
|
/// </summary>
|
||||||
|
[Test, Arguments(ItemCategory.MiscItem), Arguments(ItemCategory.Medicine)]
|
||||||
|
public async Task PreventMoveSelection_OnlyNonBerryItemConsumed_SelectionPrevented(ItemCategory category)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, choice) = CreateTestSetup(category);
|
||||||
|
var prevent = false;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.PreventMoveSelection(choice, ref prevent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(prevent).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "It cannot be selected unless the user has previously eaten a Berry in the current battle."
|
||||||
|
/// The Berry does not have to be the only consumed item: a Berry among other consumed items is enough.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task PreventMoveSelection_BerryAmongOtherConsumedItems_SelectionAllowed()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, choice) = CreateTestSetup(ItemCategory.MiscItem, ItemCategory.Berry);
|
||||||
|
var prevent = false;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.PreventMoveSelection(choice, ref prevent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(prevent).IsFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Technical test: outside of battle (no <see cref="IPokemon.BattleData"/>) the script does not prevent
|
||||||
|
/// selection.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task PreventMoveSelection_NoBattleData_SelectionAllowed()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var script = new Belch();
|
||||||
|
var user = Substitute.For<IPokemon>();
|
||||||
|
user.BattleData.Returns((IPokemonBattleData?)null);
|
||||||
|
var choice = Substitute.For<IMoveChoice>();
|
||||||
|
choice.User.Returns(user);
|
||||||
|
var prevent = false;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.PreventMoveSelection(choice, ref prevent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(prevent).IsFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
using PkmnLib.Dynamic.Models;
|
||||||
|
using PkmnLib.Plugin.Gen7.Scripts.Moves;
|
||||||
|
using PkmnLib.Static;
|
||||||
|
|
||||||
|
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests for the <see cref="BellyDrum"/> move script.
|
||||||
|
/// Gen VII Bulbapedia behavior: "Belly Drum deducts half of the user's maximum HP (rounded down) from its
|
||||||
|
/// current HP and, in return, it maximizes the user's Attack stat by raising it to +6 stages".
|
||||||
|
/// </summary>
|
||||||
|
public class BellyDrumTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a fully mocked test setup for Belly Drum. The target of the secondary effect is the user itself.
|
||||||
|
/// </summary>
|
||||||
|
private static (BellyDrum script, IExecutingMove move, IPokemon user, IHitData hitData) CreateTestSetup(uint maxHp,
|
||||||
|
uint currentHp, sbyte attackBoost = 0)
|
||||||
|
{
|
||||||
|
var script = new BellyDrum();
|
||||||
|
var user = Substitute.For<IPokemon>();
|
||||||
|
user.BoostedStats.Returns(new StatisticSet<uint>(maxHp, 10, 10, 10, 10, 10));
|
||||||
|
user.CurrentHealth.Returns(currentHp);
|
||||||
|
user.StatBoost.Returns(new StatBoostStatisticSet(0, attackBoost, 0, 0, 0, 0));
|
||||||
|
|
||||||
|
var hitData = Substitute.For<IHitData>();
|
||||||
|
var move = Substitute.For<IExecutingMove>();
|
||||||
|
move.GetHitData(user, 0).Returns(hitData);
|
||||||
|
move.User.Returns(user);
|
||||||
|
|
||||||
|
return (script, move, user, hitData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper to extract the received Damage call from the user, if any.
|
||||||
|
/// </summary>
|
||||||
|
private static (uint damage, DamageSource source, bool forceDamage)? GetDamageCall(IPokemon user)
|
||||||
|
{
|
||||||
|
var call = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage");
|
||||||
|
if (call == null)
|
||||||
|
return null;
|
||||||
|
var args = call.GetArguments();
|
||||||
|
return ((uint)args[0]!, (DamageSource)args[1]!, (bool)args[3]!);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper to extract the received ChangeStatBoost call from the user, if any.
|
||||||
|
/// </summary>
|
||||||
|
private static (Statistic stat, sbyte change, bool selfInflicted)? GetStatBoostCall(IPokemon user)
|
||||||
|
{
|
||||||
|
var call = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "ChangeStatBoost");
|
||||||
|
if (call == null)
|
||||||
|
return null;
|
||||||
|
var args = call.GetArguments();
|
||||||
|
return ((Statistic)args[0]!, (sbyte)args[1]!, (bool)args[2]!);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Belly Drum deducts half of the user's maximum HP (rounded down) from its current HP".
|
||||||
|
/// Verifies the deduction amount, including the rounding down for odd maximum HP values. The deduction is
|
||||||
|
/// a cost rather than regular damage, so it is forced <see cref="DamageSource.Misc"/> damage.
|
||||||
|
/// </summary>
|
||||||
|
[Test, Arguments(100u, 50u), Arguments(101u, 50u), Arguments(99u, 49u), Arguments(255u, 127u)]
|
||||||
|
public async Task OnSecondaryEffect_SufficientHp_DeductsHalfMaxHpRoundedDown(uint maxHp, uint expectedDeduction)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user, _) = CreateTestSetup(maxHp, maxHp);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, user, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(GetDamageCall(user)).IsEqualTo((expectedDeduction, DamageSource.Misc, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "it maximizes the user's Attack stat by raising it to +6 stages".
|
||||||
|
/// The user receives a self-inflicted Attack boost large enough to reach the +6 cap.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_SufficientHp_MaximizesAttack()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user, _) = CreateTestSetup(100, 100);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, user, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var boostCall = GetStatBoostCall(user);
|
||||||
|
await Assert.That(boostCall).IsNotNull();
|
||||||
|
await Assert.That(boostCall!.Value.stat).IsEqualTo(Statistic.Attack);
|
||||||
|
await Assert.That(boostCall.Value.change).IsGreaterThanOrEqualTo((sbyte)6);
|
||||||
|
await Assert.That(boostCall.Value.selfInflicted).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "it maximizes the user's Attack stat by raising it to +6 stages, even if the user's
|
||||||
|
/// temporary Attack bonus stages were below 0 prior to using Belly Drum."
|
||||||
|
/// From -6 stages only a raise of at least 12 stages reaches +6.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_AttackStagesBelowZero_StillRaisedToPlusSix()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user, _) = CreateTestSetup(100, 100, -6);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, user, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var boostCall = GetStatBoostCall(user);
|
||||||
|
await Assert.That(boostCall).IsNotNull();
|
||||||
|
await Assert.That(boostCall!.Value.change).IsGreaterThanOrEqualTo((sbyte)12);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Belly Drum fails if the user's current HP is less than half its maximum".
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void OnSecondaryEffect_CurrentHpBelowHalf_Fails()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user, hitData) = CreateTestSetup(100, 49);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, user, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
hitData.Received(1).Fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Belly Drum fails if the user's current HP is less than half its maximum".
|
||||||
|
/// When the move fails, no HP is deducted and the Attack stat is not changed.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_CurrentHpBelowHalf_NoHpDeductedAndNoAttackChange()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user, _) = CreateTestSetup(100, 49);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, user, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(GetDamageCall(user)).IsNull();
|
||||||
|
await Assert.That(GetStatBoostCall(user)).IsNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Belly Drum fails if the user's current HP is less than half its maximum".
|
||||||
|
/// At exactly half its maximum HP the user cannot pay the cost without fainting (deducting half of the
|
||||||
|
/// maximum HP would reduce it to 0 HP), so the move fails as well.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void OnSecondaryEffect_CurrentHpExactlyHalf_Fails()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user, hitData) = CreateTestSetup(100, 50);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, user, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
hitData.Received(1).Fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Belly Drum fails if the user's current HP is less than half its maximum, or if the user's
|
||||||
|
/// Attack is already at +6 (even if the user has Contrary)."
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void OnSecondaryEffect_AttackAlreadyAtPlusSix_Fails()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user, hitData) = CreateTestSetup(100, 100, 6);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, user, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
hitData.Received(1).Fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Belly Drum fails if [...] the user's Attack is already at +6".
|
||||||
|
/// A failed Belly Drum does not deduct HP: the HP cost only applies when the move works (the only
|
||||||
|
/// exception Bulbapedia lists is a Contrary user already at -6 stages).
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_AttackAlreadyAtPlusSix_NoHpDeducted()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user, _) = CreateTestSetup(100, 100, 6);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, user, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(GetDamageCall(user)).IsNull();
|
||||||
|
}
|
||||||
|
}
|
||||||
175
Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BestowTests.cs
Normal file
175
Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BestowTests.cs
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
using PkmnLib.Dynamic.Models;
|
||||||
|
using PkmnLib.Plugin.Gen7.Scripts.Moves;
|
||||||
|
using PkmnLib.Static;
|
||||||
|
|
||||||
|
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests for the <see cref="Bestow"/> move script.
|
||||||
|
/// Gen VII Bulbapedia behavior: "The user transfers its held item to the target. It fails if the user has no
|
||||||
|
/// held item, the target already has a held item, or the target is behind a substitute. [...] Items given away
|
||||||
|
/// in Trainer battles return to the original Pokémon after the battle."
|
||||||
|
/// </summary>
|
||||||
|
public class BestowTests
|
||||||
|
{
|
||||||
|
private static (Bestow script, IExecutingMove move, IPokemon user, IPokemon target, IHitData hitData)
|
||||||
|
CreateTestSetup(IItem? userItem, IItem? targetItem)
|
||||||
|
{
|
||||||
|
var script = new Bestow();
|
||||||
|
var move = Substitute.For<IExecutingMove>();
|
||||||
|
var target = Substitute.For<IPokemon>();
|
||||||
|
var hitData = Substitute.For<IHitData>();
|
||||||
|
move.GetHitData(target, 0).Returns(hitData);
|
||||||
|
target.HeldItem.Returns(targetItem);
|
||||||
|
|
||||||
|
var user = Substitute.For<IPokemon>();
|
||||||
|
user.HeldItem.Returns(userItem);
|
||||||
|
user.RemoveHeldItemForBattle().Returns(userItem);
|
||||||
|
move.User.Returns(user);
|
||||||
|
|
||||||
|
return (script, move, user, target, hitData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "The user transfers its held item to the target."
|
||||||
|
/// The target ends up holding the user's item.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void OnSecondaryEffect_TargetHasNoItem_TargetReceivesUsersItem()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var userItem = Substitute.For<IItem>();
|
||||||
|
var (script, move, _, target, _) = CreateTestSetup(userItem, null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
target.Received(1).ForceSetHeldItem(userItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "The user transfers its held item to the target."
|
||||||
|
/// A successful transfer does not fail the move.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void OnSecondaryEffect_TargetHasNoItem_MoveDoesNotFail()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var userItem = Substitute.For<IItem>();
|
||||||
|
var (script, move, _, target, hitData) = CreateTestSetup(userItem, null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
hitData.DidNotReceive().Fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Items given away in Trainer battles return to the original Pokémon after the battle."
|
||||||
|
/// The item is taken from the user through <see cref="IPokemon.RemoveHeldItemForBattle"/>, which only
|
||||||
|
/// removes the item for the duration of the battle.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void OnSecondaryEffect_TargetHasNoItem_UserItemIsRemovedForBattleOnly()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var userItem = Substitute.For<IItem>();
|
||||||
|
var (script, move, user, target, _) = CreateTestSetup(userItem, null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
user.Received(1).RemoveHeldItemForBattle();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "It fails if the user has no held item".
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void OnSecondaryEffect_UserHasNoHeldItem_MoveFails()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, _, target, hitData) = CreateTestSetup(null, null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
hitData.Received(1).Fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "It fails if the user has no held item".
|
||||||
|
/// When the move fails, no item is given to the target.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void OnSecondaryEffect_UserHasNoHeldItem_TargetDoesNotReceiveAnItem()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, _, target, _) = CreateTestSetup(null, null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
target.DidNotReceive().ForceSetHeldItem(Arg.Any<IItem>());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "It fails if [...] the target already has a held item".
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void OnSecondaryEffect_TargetAlreadyHasHeldItem_MoveFails()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, _, target, hitData) = CreateTestSetup(Substitute.For<IItem>(), Substitute.For<IItem>());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
hitData.Received(1).Fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "It fails if [...] the target already has a held item".
|
||||||
|
/// The target's own held item is left untouched by the failed move.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void OnSecondaryEffect_TargetAlreadyHasHeldItem_TargetItemIsNotReplaced()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, _, target, _) = CreateTestSetup(Substitute.For<IItem>(), Substitute.For<IItem>());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
target.DidNotReceive().ForceSetHeldItem(Arg.Any<IItem>());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "It fails if [...] the target already has a held item".
|
||||||
|
/// A failed Bestow must not cost the user its held item: the item is either never taken from the user, or
|
||||||
|
/// it is given back after the failure check.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_TargetAlreadyHasHeldItem_UserKeepsItsHeldItem()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var userItem = Substitute.For<IItem>();
|
||||||
|
var (script, move, user, target, _) = CreateTestSetup(userItem, Substitute.For<IItem>());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert - the user still holds its item: it was either never removed, or restored after the failure.
|
||||||
|
var removed = user.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "RemoveHeldItemForBattle");
|
||||||
|
var restored = user.ReceivedCalls().Any(c =>
|
||||||
|
c.GetMethodInfo().Name == "ForceSetHeldItem" && ReferenceEquals(c.GetArguments()[0], userItem));
|
||||||
|
await Assert.That(!removed || restored).IsTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
280
Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BideTests.cs
Normal file
280
Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BideTests.cs
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
using PkmnLib.Dynamic.Models;
|
||||||
|
using PkmnLib.Dynamic.ScriptHandling;
|
||||||
|
using PkmnLib.Plugin.Gen7.Scripts.Moves;
|
||||||
|
using PkmnLib.Plugin.Gen7.Scripts.Pokemon;
|
||||||
|
|
||||||
|
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests for the <see cref="Bide"/> move script and its <see cref="BideEffect"/> volatile.
|
||||||
|
/// Gen VII Bulbapedia behavior (base plus Gen III-VII deltas): the user idles while storing the damage it
|
||||||
|
/// receives, then "Bide will do damage equal to twice the damage received during the idling period", enduring
|
||||||
|
/// "attacks for two turns" and hitting "the last Pokémon to attack the user, even if this is an ally".
|
||||||
|
/// </summary>
|
||||||
|
public class BideTests
|
||||||
|
{
|
||||||
|
private static (Bide script, IExecutingMove move, IPokemon user, IPokemon target, IScriptSet userVolatile, IHitData
|
||||||
|
hitData) CreateTestSetup()
|
||||||
|
{
|
||||||
|
var script = new Bide();
|
||||||
|
var move = Substitute.For<IExecutingMove>();
|
||||||
|
var target = Substitute.For<IPokemon>();
|
||||||
|
var hitData = Substitute.For<IHitData>();
|
||||||
|
move.GetHitData(target, 0).Returns(hitData);
|
||||||
|
|
||||||
|
var user = Substitute.For<IPokemon>();
|
||||||
|
user.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));
|
||||||
|
// A real script set so the volatile Bide effect can actually be added, retrieved and removed.
|
||||||
|
var userVolatile = new ScriptSet(user);
|
||||||
|
user.Volatile.Returns(userVolatile);
|
||||||
|
move.User.Returns(user);
|
||||||
|
|
||||||
|
return (script, move, user, target, userVolatile, hitData);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IPokemon CreateAttacker(bool onBattlefield = true)
|
||||||
|
{
|
||||||
|
var attacker = Substitute.For<IPokemon>();
|
||||||
|
var battleData = Substitute.For<IPokemonBattleData>();
|
||||||
|
battleData.IsOnBattlefield.Returns(onBattlefield);
|
||||||
|
attacker.BattleData.Returns(battleData);
|
||||||
|
return attacker;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a <see cref="BideEffect"/> to the user's volatile scripts, as if Bide has already been storing
|
||||||
|
/// energy for the given number of executed turns.
|
||||||
|
/// </summary>
|
||||||
|
private static BideEffect AddStoredBideEffect(IScriptSet userVolatile, IPokemon user, byte turns, uint damageTaken,
|
||||||
|
params IPokemon[] hitBy)
|
||||||
|
{
|
||||||
|
var effect = new BideEffect(user)
|
||||||
|
{
|
||||||
|
Turns = turns,
|
||||||
|
DamageTaken = damageTaken,
|
||||||
|
};
|
||||||
|
effect.HitBy.AddRange(hitBy);
|
||||||
|
userVolatile.Add(effect);
|
||||||
|
return effect;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper to check whether a Pokémon received any Damage call.
|
||||||
|
/// </summary>
|
||||||
|
private static bool ReceivedDamage(IPokemon pokemon) =>
|
||||||
|
pokemon.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Damage");
|
||||||
|
|
||||||
|
/// <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>
|
||||||
|
/// Helper to extract the damage source from a Pokémon's received Damage calls.
|
||||||
|
/// </summary>
|
||||||
|
private static DamageSource? GetDamageSource(IPokemon pokemon)
|
||||||
|
{
|
||||||
|
var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage");
|
||||||
|
return call != null ? (DamageSource)call.GetArguments()[1]! : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "After Bide is selected, the user will be unable to select a move for an idling period".
|
||||||
|
/// The first use adds the <see cref="BideEffect"/> volatile to the user, which forces the Bide choice on
|
||||||
|
/// the following turns and records incoming damage.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_FirstUse_AddsBideEffectToUserVolatile()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, _, target, userVolatile, _) = CreateTestSetup();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(userVolatile.Get<BideEffect>()).IsNotNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Bide will now only endure attacks for two turns."
|
||||||
|
/// The turn Bide is selected begins the storing period, which spans two more turns; the stored damage is
|
||||||
|
/// only released on the third execution, so on the second execution nothing is released yet.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_SecondExecution_DoesNotReleaseStoredDamage()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user, target, userVolatile, _) = CreateTestSetup();
|
||||||
|
var attacker = CreateAttacker();
|
||||||
|
_ = AddStoredBideEffect(userVolatile, user, 1, 50, attacker);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert - still storing, so the attacker has not been damaged
|
||||||
|
await Assert.That(ReceivedDamage(attacker)).IsFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Bide will do damage equal to twice the damage received during the idling period."
|
||||||
|
/// </summary>
|
||||||
|
[Test, Arguments(50u, 100u), Arguments(1u, 2u), Arguments(123u, 246u)]
|
||||||
|
public async Task OnSecondaryEffect_Release_DealsTwiceTheStoredDamage(uint damageTaken, uint expectedDamage)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user, target, userVolatile, _) = CreateTestSetup();
|
||||||
|
var attacker = CreateAttacker();
|
||||||
|
_ = AddStoredBideEffect(userVolatile, user, 2, damageTaken, attacker);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(GetDamageAmount(attacker)).IsEqualTo(expectedDamage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Bide will do damage equal to twice the damage received during the idling period."
|
||||||
|
/// The released damage is dealt as move damage, not as indirect damage.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_Release_DealsMoveDamage()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user, target, userVolatile, _) = CreateTestSetup();
|
||||||
|
var attacker = CreateAttacker();
|
||||||
|
_ = AddStoredBideEffect(userVolatile, user, 2, 50, attacker);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(GetDamageSource(attacker)).IsEqualTo(DamageSource.MoveDamage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Bide hits the last Pokémon to attack the user, even if this is an ally."
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_Release_HitsLastPokemonThatAttackedUser()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user, target, userVolatile, _) = CreateTestSetup();
|
||||||
|
var firstAttacker = CreateAttacker();
|
||||||
|
var lastAttacker = CreateAttacker();
|
||||||
|
_ = AddStoredBideEffect(userVolatile, user, 2, 50, firstAttacker, lastAttacker);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert - only the last attacker is hit
|
||||||
|
await Assert.That(ReceivedDamage(lastAttacker)).IsTrue();
|
||||||
|
await Assert.That(ReceivedDamage(firstAttacker)).IsFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "this attack will hit the last Pokémon to attack the user".
|
||||||
|
/// A Pokémon that has since left the battlefield cannot be hit by the released damage.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_Release_AttackerNoLongerOnBattlefield_IsNotDamaged()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user, target, userVolatile, _) = CreateTestSetup();
|
||||||
|
var departedAttacker = CreateAttacker(false);
|
||||||
|
_ = AddStoredBideEffect(userVolatile, user, 2, 50, departedAttacker);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(ReceivedDamage(departedAttacker)).IsFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "If the user is not directly attacked during the biding period, it will fail on the turn it
|
||||||
|
/// would have released."
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void OnSecondaryEffect_ReleaseWithoutBeingAttacked_MoveFails()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user, target, userVolatile, hitData) = CreateTestSetup();
|
||||||
|
_ = AddStoredBideEffect(userVolatile, user, 2, 0);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
hitData.Received(1).Fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Once a Pokémon has started biding, it cannot choose to switch out until the attack is
|
||||||
|
/// complete."
|
||||||
|
/// Once the stored damage is released the attack is complete, so the <see cref="BideEffect"/> volatile must
|
||||||
|
/// be removed from the user; otherwise the user would remain locked into Bide indefinitely through
|
||||||
|
/// <see cref="BideEffect.ForceTurnSelection"/>.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_Release_RemovesBideEffectFromUser()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user, target, userVolatile, _) = CreateTestSetup();
|
||||||
|
var attacker = CreateAttacker();
|
||||||
|
_ = AddStoredBideEffect(userVolatile, user, 2, 50, attacker);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(userVolatile.Get<BideEffect>()).IsNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Bide will do damage equal to twice the damage received during the idling period."
|
||||||
|
/// The <see cref="BideEffect"/> volatile accumulates every chunk of damage the user takes while storing
|
||||||
|
/// energy.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task BideEffect_OnDamage_AccumulatesDamageTaken()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var user = Substitute.For<IPokemon>();
|
||||||
|
var effect = new BideEffect(user);
|
||||||
|
|
||||||
|
// Act - the user drops from 100 to 60 HP, then from 60 to 50 HP
|
||||||
|
effect.OnDamage(user, DamageSource.MoveDamage, 100, 60);
|
||||||
|
effect.OnDamage(user, DamageSource.MoveDamage, 60, 50);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(effect.DamageTaken).IsEqualTo(50u);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Bide hits the last Pokémon to attack the user, even if this is an ally."
|
||||||
|
/// The <see cref="BideEffect"/> volatile records every Pokémon that hits the user, in order, so the last
|
||||||
|
/// attacker can be targeted on release.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task BideEffect_OnIncomingHit_RecordsAttacker()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var user = Substitute.For<IPokemon>();
|
||||||
|
var effect = new BideEffect(user);
|
||||||
|
var incomingMove = Substitute.For<IExecutingMove>();
|
||||||
|
var attacker = Substitute.For<IPokemon>();
|
||||||
|
incomingMove.User.Returns(attacker);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
effect.OnIncomingHit(incomingMove, user, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(effect.HitBy.LastOrDefault()).IsEqualTo(attacker);
|
||||||
|
}
|
||||||
|
}
|
||||||
257
Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BindTests.cs
Normal file
257
Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BindTests.cs
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
using PkmnLib.Dynamic.Models;
|
||||||
|
using PkmnLib.Dynamic.Models.Choices;
|
||||||
|
using PkmnLib.Dynamic.ScriptHandling;
|
||||||
|
using PkmnLib.Dynamic.ScriptHandling.Registry;
|
||||||
|
using PkmnLib.Plugin.Gen7.Scripts;
|
||||||
|
using PkmnLib.Plugin.Gen7.Scripts.Moves;
|
||||||
|
using PkmnLib.Plugin.Gen7.Scripts.Pokemon;
|
||||||
|
using PkmnLib.Static.Utils;
|
||||||
|
|
||||||
|
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests for the <see cref="Bind"/> move script and its <see cref="BindEffect"/> volatile.
|
||||||
|
/// Gen VII Bulbapedia behavior (Gen II base plus Gen V/VI deltas): "It also traps the target, preventing
|
||||||
|
/// switching and escape", "Its effect will last either 4 or 5 turns", and "The end turn damage of Bind is
|
||||||
|
/// increased from 1/16 to 1/8 of the target's maximum HP."
|
||||||
|
/// </summary>
|
||||||
|
public class BindTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Test helper script that modifies the Bind duration/damage through the <see cref="CustomTriggers.ModifyBind"/>
|
||||||
|
/// custom trigger, the same way the Grip Claw and Binding Band item scripts do.
|
||||||
|
/// </summary>
|
||||||
|
[Script(ScriptCategory.Pokemon, "test_modify_bind_trigger")]
|
||||||
|
private class ModifyBindTrigger : Script, IScriptCustomTrigger
|
||||||
|
{
|
||||||
|
private readonly int? _duration;
|
||||||
|
private readonly float? _damagePercent;
|
||||||
|
|
||||||
|
public ModifyBindTrigger(int? duration = null, float? damagePercent = null)
|
||||||
|
{
|
||||||
|
_duration = duration;
|
||||||
|
_damagePercent = damagePercent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CustomTrigger(StringKey eventName, ICustomTriggerArgs args)
|
||||||
|
{
|
||||||
|
if (eventName != CustomTriggers.ModifyBind || args is not CustomTriggers.ModifyBindArgs bindArgs)
|
||||||
|
return;
|
||||||
|
if (_duration.HasValue)
|
||||||
|
bindArgs.Duration = _duration.Value;
|
||||||
|
if (_damagePercent.HasValue)
|
||||||
|
bindArgs.DamagePercent = _damagePercent.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (Bind script, IExecutingMove move, IPokemon user, IPokemon target, IScriptSet targetVolatile)
|
||||||
|
CreateTestSetup(params Script[] userScripts)
|
||||||
|
{
|
||||||
|
var script = new Bind();
|
||||||
|
var move = Substitute.For<IExecutingMove>();
|
||||||
|
var target = Substitute.For<IPokemon>();
|
||||||
|
var targetVolatile = Substitute.For<IScriptSet>();
|
||||||
|
target.Volatile.Returns(targetVolatile);
|
||||||
|
|
||||||
|
var user = Substitute.For<IPokemon>();
|
||||||
|
// RunScriptHook iterates the user's scripts; give the mock a real iterator so the ModifyBind trigger
|
||||||
|
// pass runs (empty unless the test attaches scripts such as the Grip Claw / Binding Band stand-in).
|
||||||
|
var containers = userScripts.Select(IEnumerable<ScriptContainer> (s) => new ScriptContainer(s)).ToArray();
|
||||||
|
user.GetScripts().Returns(_ => new ScriptIterator(containers));
|
||||||
|
move.User.Returns(user);
|
||||||
|
|
||||||
|
return (script, move, user, target, targetVolatile);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper to extract the <see cref="BindEffect"/> instance that was added to the target's volatile scripts.
|
||||||
|
/// </summary>
|
||||||
|
private static BindEffect? GetAddedEffect(IScriptSet targetVolatile) => targetVolatile.ReceivedCalls()
|
||||||
|
.Where(c => c.GetMethodInfo().Name == "Add").Select(c => c.GetArguments()[0]).OfType<BindEffect>()
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper to extract the damage amount from the target's first received Damage call.
|
||||||
|
/// </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>
|
||||||
|
/// Runs end-of-turn handling on the effect repeatedly and counts how many turns dealt damage to the target.
|
||||||
|
/// </summary>
|
||||||
|
private static int CountEndTurnDamageTicks(BindEffect effect, IPokemon target, int maxTurns = 10)
|
||||||
|
{
|
||||||
|
var battle = Substitute.For<IBattle>();
|
||||||
|
for (var i = 0; i < maxTurns; i++)
|
||||||
|
effect.OnEndTurn(target, battle);
|
||||||
|
return target.ReceivedCalls().Count(c => c.GetMethodInfo().Name == "Damage");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "It also traps the target, preventing switching and escape."
|
||||||
|
/// Using Bind adds the <see cref="BindEffect"/> volatile to the target.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void OnSecondaryEffect_AddsBindEffectToTargetVolatile()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, _, target, targetVolatile) = CreateTestSetup();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
targetVolatile.Received(1).Add(Arg.Any<BindEffect>());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "The end turn damage of Bind is increased from 1/16 to 1/8 of the target's maximum HP."
|
||||||
|
/// Tests several max HP values to ensure proper integer truncation of the 1/8 fraction.
|
||||||
|
/// </summary>
|
||||||
|
[Test, Arguments(160u, 20u), Arguments(100u, 12u), Arguments(15u, 1u)]
|
||||||
|
public async Task OnSecondaryEffect_Default_EndTurnDamageIsOneEighthOfTargetMaxHp(uint maxHealth,
|
||||||
|
uint expectedDamage)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, _, target, targetVolatile) = CreateTestSetup();
|
||||||
|
target.MaxHealth.Returns(maxHealth);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
var effect = GetAddedEffect(targetVolatile);
|
||||||
|
await Assert.That(effect).IsNotNull();
|
||||||
|
effect!.OnEndTurn(target, Substitute.For<IBattle>());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(GetDamageAmount(target)).IsEqualTo(expectedDamage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Its effect will last either 4 or 5 turns."
|
||||||
|
/// The added effect deals its end-of-turn damage for 4 or 5 turns and then ends.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_Default_EffectLastsFourOrFiveTurns()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, _, target, targetVolatile) = CreateTestSetup();
|
||||||
|
target.MaxHealth.Returns(160u);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
var effect = GetAddedEffect(targetVolatile);
|
||||||
|
await Assert.That(effect).IsNotNull();
|
||||||
|
var turnsWithDamage = CountEndTurnDamageTicks(effect!, target);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(turnsWithDamage is 4 or 5).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "If the user of Bind is holding a Grip Claw, the duration will be 7 turns."
|
||||||
|
/// The duration change flows through the <see cref="CustomTriggers.ModifyBind"/> custom trigger; a script
|
||||||
|
/// on the user that sets the duration to 7 results in an effect lasting 7 turns.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_DurationSetToSevenByTrigger_EffectLastsSevenTurns()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, _, target, targetVolatile) = CreateTestSetup(new ModifyBindTrigger(7));
|
||||||
|
target.MaxHealth.Returns(160u);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
var effect = GetAddedEffect(targetVolatile);
|
||||||
|
await Assert.That(effect).IsNotNull();
|
||||||
|
var turnsWithDamage = CountEndTurnDamageTicks(effect!, target);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(turnsWithDamage).IsEqualTo(7);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "If the user is holding a Binding Band, the end turn damage of Bind will increase to 1/6 of
|
||||||
|
/// the target's maximum HP."
|
||||||
|
/// The damage change flows through the <see cref="CustomTriggers.ModifyBind"/> custom trigger; a script on
|
||||||
|
/// the user that sets the damage to 1/6 results in end-of-turn damage of 1/6 max HP.
|
||||||
|
/// </summary>
|
||||||
|
[Test, Arguments(60u, 10u), Arguments(48u, 8u)]
|
||||||
|
public async Task OnSecondaryEffect_DamageSetToOneSixthByTrigger_EndTurnDamageIsOneSixth(uint maxHealth,
|
||||||
|
uint expectedDamage)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, _, target, targetVolatile) = CreateTestSetup(new ModifyBindTrigger(damagePercent: 1f / 6f));
|
||||||
|
target.MaxHealth.Returns(maxHealth);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
var effect = GetAddedEffect(targetVolatile);
|
||||||
|
await Assert.That(effect).IsNotNull();
|
||||||
|
effect!.OnEndTurn(target, Substitute.For<IBattle>());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(GetDamageAmount(target)).IsEqualTo(expectedDamage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "It also traps the target, preventing switching and escape."
|
||||||
|
/// While the effect is active, the target cannot switch out.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task BindEffect_WhileActive_PreventsSwitching()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var target = Substitute.For<IPokemon>();
|
||||||
|
var effect = new BindEffect(target, 5, 1f / 8f);
|
||||||
|
var prevent = false;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
effect.PreventSelfSwitch(Substitute.For<ISwitchChoice>(), ref prevent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(prevent).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "It also traps the target, preventing switching and escape."
|
||||||
|
/// While the effect is active, the target cannot flee.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task BindEffect_WhileActive_PreventsRunningAway()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var target = Substitute.For<IPokemon>();
|
||||||
|
var effect = new BindEffect(target, 5, 1f / 8f);
|
||||||
|
var prevent = false;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
effect.PreventSelfRunAway(Substitute.For<IFleeChoice>(), ref prevent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(prevent).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Its effect will last either 4 or 5 turns."
|
||||||
|
/// Once the duration has run out, the target is no longer prevented from switching.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task BindEffect_AfterDurationExpires_NoLongerPreventsSwitching()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var target = Substitute.For<IPokemon>();
|
||||||
|
target.MaxHealth.Returns(160u);
|
||||||
|
var effect = new BindEffect(target, 1, 1f / 8f);
|
||||||
|
|
||||||
|
// Act - the single remaining turn elapses
|
||||||
|
effect.OnEndTurn(target, Substitute.For<IBattle>());
|
||||||
|
var prevent = false;
|
||||||
|
effect.PreventSelfSwitch(Substitute.For<ISwitchChoice>(), ref prevent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(prevent).IsFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
119
Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BlockTests.cs
Normal file
119
Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BlockTests.cs
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests for the <see cref="Block"/> move script.
|
||||||
|
/// Gen VII Bulbapedia behavior: "Block prevents the target from switching out or fleeing (including via
|
||||||
|
/// Teleport)." From Generation VI onward: "Block no longer affects Ghost-type Pokémon."
|
||||||
|
/// </summary>
|
||||||
|
public class BlockTests
|
||||||
|
{
|
||||||
|
private static (Block script, IExecutingMove move, IPokemon target, ScriptSet targetVolatile) CreateTestSetup()
|
||||||
|
{
|
||||||
|
var script = new Block();
|
||||||
|
var move = Substitute.For<IExecutingMove>();
|
||||||
|
var user = Substitute.For<IPokemon>();
|
||||||
|
move.User.Returns(user);
|
||||||
|
|
||||||
|
var target = Substitute.For<IPokemon>();
|
||||||
|
// Use a real script set so the volatile script added by Block can be inspected afterwards.
|
||||||
|
var targetVolatile = new ScriptSet(target);
|
||||||
|
target.Volatile.Returns(targetVolatile);
|
||||||
|
target.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));
|
||||||
|
move.GetHitData(target, 0).Returns(Substitute.For<IHitData>());
|
||||||
|
|
||||||
|
return (script, move, target, targetVolatile);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Block prevents the target from switching out or fleeing (including via Teleport)."
|
||||||
|
/// Hitting the target attaches the <see cref="BlockEffect"/> volatile script to it.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_Hit_AddsBlockEffectToTarget()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, target, targetVolatile) = CreateTestSetup();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(targetVolatile.Contains(ScriptUtils.ResolveName<BlockEffect>())).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Block prevents the target from switching out".
|
||||||
|
/// The <see cref="BlockEffect"/> applied by the move prevents the target's switch choices through
|
||||||
|
/// <see cref="BlockEffect.PreventSelfSwitch"/>.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_AppliedEffect_PreventsTargetFromSwitchingOut()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, target, targetVolatile) = CreateTestSetup();
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
await Assert.That(targetVolatile.TryGet<BlockEffect>(out var effect)).IsTrue();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var prevent = false;
|
||||||
|
effect!.PreventSelfSwitch(Substitute.For<ISwitchChoice>(), ref prevent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(prevent).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Block prevents the target from [...] fleeing (including via Teleport)."
|
||||||
|
/// The <see cref="BlockEffect"/> applied by the move prevents the target's flee choices through
|
||||||
|
/// <see cref="BlockEffect.PreventSelfRunAway"/>.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_AppliedEffect_PreventsTargetFromFleeing()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, target, targetVolatile) = CreateTestSetup();
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
await Assert.That(targetVolatile.TryGet<BlockEffect>(out var effect)).IsTrue();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var prevent = false;
|
||||||
|
effect!.PreventSelfRunAway(Substitute.For<IFleeChoice>(), ref prevent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(prevent).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia (Generation VI onward): "Block no longer affects Ghost-type Pokémon."
|
||||||
|
/// Using Block on a Ghost-type target should not trap it, so no <see cref="BlockEffect"/> may be added.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_GhostTypeTarget_DoesNotTrapTarget()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, target, targetVolatile) = CreateTestSetup();
|
||||||
|
var library = LibraryHelpers.LoadLibrary();
|
||||||
|
await Assert.That(library.StaticLibrary.Types.TryGetTypeIdentifier("ghost", out var ghostType)).IsTrue();
|
||||||
|
target.Types.Returns(new[] { ghostType });
|
||||||
|
|
||||||
|
var battle = Substitute.For<IBattle>();
|
||||||
|
battle.Library.Returns(library);
|
||||||
|
var battleData = Substitute.For<IPokemonBattleData>();
|
||||||
|
battleData.Battle.Returns(battle);
|
||||||
|
move.User.BattleData.Returns(battleData);
|
||||||
|
target.BattleData.Returns(battleData);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(targetVolatile.Contains(ScriptUtils.ResolveName<BlockEffect>())).IsFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
327
Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BounceTests.cs
Normal file
327
Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BounceTests.cs
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
using PkmnLib.Dynamic.Events;
|
||||||
|
using PkmnLib.Dynamic.Models;
|
||||||
|
using PkmnLib.Dynamic.Models.Choices;
|
||||||
|
using PkmnLib.Dynamic.ScriptHandling;
|
||||||
|
using PkmnLib.Dynamic.ScriptHandling.Registry;
|
||||||
|
using PkmnLib.Plugin.Gen7.Common;
|
||||||
|
using PkmnLib.Plugin.Gen7.Scripts.Moves;
|
||||||
|
using PkmnLib.Plugin.Gen7.Scripts.Pokemon;
|
||||||
|
using PkmnLib.Static.Moves;
|
||||||
|
using PkmnLib.Static.Utils;
|
||||||
|
|
||||||
|
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests for the <see cref="Bounce"/> move script.
|
||||||
|
/// Gen VII Bulbapedia behavior: "The user gains the Sky-High status on the turn this move is used, then
|
||||||
|
/// attacks on the following turn, inflicting damage with a 30% chance of paralyzing the target. While in
|
||||||
|
/// Sky-High status, the user is invulnerable to most moves, but can still be hit by Gust, Twister, Thunder,
|
||||||
|
/// and Sky Uppercut", where (from Generation V onward) for Gust and Twister "the damage dealt will be doubled".
|
||||||
|
/// </summary>
|
||||||
|
public class BounceTests
|
||||||
|
{
|
||||||
|
private static (Bounce script, IExecutingMove move, IPokemon user, ScriptSet userVolatile, IMoveChoice moveChoice,
|
||||||
|
IBattleRandom random) CreateTestSetup()
|
||||||
|
{
|
||||||
|
var script = new Bounce();
|
||||||
|
var move = Substitute.For<IExecutingMove>();
|
||||||
|
var user = Substitute.For<IPokemon>();
|
||||||
|
// Use a real script set so the charge volatile added by Bounce can be inspected afterwards.
|
||||||
|
var userVolatile = new ScriptSet(user);
|
||||||
|
user.Volatile.Returns(userVolatile);
|
||||||
|
user.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));
|
||||||
|
move.User.Returns(user);
|
||||||
|
|
||||||
|
var moveChoice = Substitute.For<IMoveChoice>();
|
||||||
|
move.MoveChoice.Returns(moveChoice);
|
||||||
|
|
||||||
|
var battle = Substitute.For<IBattle>();
|
||||||
|
var random = Substitute.For<IBattleRandom>();
|
||||||
|
battle.Random.Returns(random);
|
||||||
|
battle.EventHook.Returns(new EventHook());
|
||||||
|
var battleData = Substitute.For<IPokemonBattleData>();
|
||||||
|
battleData.Battle.Returns(battle);
|
||||||
|
user.BattleData.Returns(battleData);
|
||||||
|
|
||||||
|
return (script, move, user, userVolatile, moveChoice, random);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "The user gains the Sky-High status on the turn this move is used, then attacks on the
|
||||||
|
/// following turn". On the first turn the move is prevented from executing.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task PreventMove_FirstUse_PreventsMoveForChargeTurn()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, _, _, _, _) = CreateTestSetup();
|
||||||
|
var prevent = false;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.PreventMove(move, ref prevent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(prevent).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "The user gains the Sky-High status on the turn this move is used".
|
||||||
|
/// The charge turn attaches the <see cref="ChargeBounceEffect"/> volatile script to the user.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task PreventMove_FirstUse_AddsChargeBounceEffectToUser()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, _, userVolatile, _, _) = CreateTestSetup();
|
||||||
|
var prevent = false;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.PreventMove(move, ref prevent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(userVolatile.Contains(ScriptUtils.ResolveName<ChargeBounceEffect>())).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "The user gains the Sky-High status on the turn this move is used".
|
||||||
|
/// The charge turn is marked on the <see cref="IMoveChoice"/> so the <see cref="ChargeBounceEffect"/>
|
||||||
|
/// knows this choice was the charging turn.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task PreventMove_FirstUse_MarksMoveChoiceAsChargeTurn()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, _, _, moveChoice, _) = CreateTestSetup();
|
||||||
|
var prevent = false;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.PreventMove(move, ref prevent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
moveChoice.Received(1).SetAdditionalData(new StringKey("bounce_charge"), true);
|
||||||
|
await Assert.That(prevent).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "then attacks on the following turn". When the user already has the
|
||||||
|
/// <see cref="ChargeBounceEffect"/> from the charge turn, the move is not prevented again.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task PreventMove_SecondTurn_DoesNotPreventMove()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user, userVolatile, _, _) = CreateTestSetup();
|
||||||
|
userVolatile.Add(new ChargeBounceEffect(user));
|
||||||
|
var prevent = false;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.PreventMove(move, ref prevent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(prevent).IsFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "then attacks on the following turn". Once the attack executes, the Sky-High state
|
||||||
|
/// (<see cref="ChargeBounceEffect"/>) is removed from the user.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnBeforeMove_ChargeCompleted_RemovesChargeBounceEffect()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user, userVolatile, _, _) = CreateTestSetup();
|
||||||
|
userVolatile.Add(new ChargeBounceEffect(user));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnBeforeMove(move);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(userVolatile.Contains(ScriptUtils.ResolveName<ChargeBounceEffect>())).IsFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "inflicting damage with a 30% chance of paralyzing the target."
|
||||||
|
/// When the effect chance roll succeeds, the target is paralyzed.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_EffectChanceSucceeds_ParalyzesTarget()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user, _, _, random) = CreateTestSetup();
|
||||||
|
var target = Substitute.For<IPokemon>();
|
||||||
|
random.EffectChance(30, move, target, 0).Returns(true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
target.Received(1).SetStatus(new StringKey("paralyzed"), user);
|
||||||
|
await Assert.That(target.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "SetStatus")).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "inflicting damage with a 30% chance of paralyzing the target."
|
||||||
|
/// When the effect chance roll fails, the target is not paralyzed.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_EffectChanceFails_DoesNotParalyzeTarget()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, _, _, _, random) = CreateTestSetup();
|
||||||
|
var target = Substitute.For<IPokemon>();
|
||||||
|
random.EffectChance(30, move, target, 0).Returns(false);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(target.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "SetStatus")).IsFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "a 30% chance of paralyzing the target."
|
||||||
|
/// The paralysis roll is made with exactly a 30% chance.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_ParalysisRoll_UsesThirtyPercentChance()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, _, _, _, random) = CreateTestSetup();
|
||||||
|
var target = Substitute.For<IPokemon>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
random.Received(1).EffectChance(30, move, target, 0);
|
||||||
|
await Assert.That(random.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "EffectChance")).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Technical test: outside of battle (no battle data) the secondary effect does nothing and does not throw.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_NoBattleData_DoesNotParalyzeTarget()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user, _, _, _) = CreateTestSetup();
|
||||||
|
user.BattleData.Returns((IPokemonBattleData?)null);
|
||||||
|
var target = Substitute.For<IPokemon>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(target.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "SetStatus")).IsFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "While in Sky-High status, the user is invulnerable to most moves".
|
||||||
|
/// The <see cref="ChargeBounceEffect"/> added by the charge turn blocks incoming hits from moves that
|
||||||
|
/// cannot hit semi-invulnerable flying Pokémon.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task BlockIncomingHit_MoveCannotHitSkyHighUser_BlocksHit()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user, userVolatile, _, _) = CreateTestSetup();
|
||||||
|
var prevent = false;
|
||||||
|
script.PreventMove(move, ref prevent);
|
||||||
|
await Assert.That(userVolatile.TryGet<ChargeBounceEffect>(out var effect)).IsTrue();
|
||||||
|
|
||||||
|
var incomingMove = Substitute.For<IExecutingMove>();
|
||||||
|
var incomingMoveData = Substitute.For<IMoveData>();
|
||||||
|
incomingMoveData.HasFlag(MoveFlags.HitFlying).Returns(false);
|
||||||
|
incomingMove.UseMove.Returns(incomingMoveData);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var block = false;
|
||||||
|
effect!.BlockIncomingHit(incomingMove, user, 0, ref block);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(block).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "but can still be hit by Gust, Twister, Thunder, and Sky Uppercut".
|
||||||
|
/// Moves flagged as able to hit semi-invulnerable flying Pokémon are not blocked by the
|
||||||
|
/// <see cref="ChargeBounceEffect"/>.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task BlockIncomingHit_MoveCanHitSkyHighUser_DoesNotBlockHit()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user, userVolatile, _, _) = CreateTestSetup();
|
||||||
|
var prevent = false;
|
||||||
|
script.PreventMove(move, ref prevent);
|
||||||
|
await Assert.That(userVolatile.TryGet<ChargeBounceEffect>(out var effect)).IsTrue();
|
||||||
|
|
||||||
|
var incomingMove = Substitute.For<IExecutingMove>();
|
||||||
|
var incomingMoveData = Substitute.For<IMoveData>();
|
||||||
|
incomingMoveData.HasFlag(MoveFlags.HitFlying).Returns(true);
|
||||||
|
incomingMove.UseMove.Returns(incomingMoveData);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var block = false;
|
||||||
|
effect!.BlockIncomingHit(incomingMove, user, 0, ref block);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(block).IsFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia (Generation V onward): "If the user is hit by Gust or Twister while the user has the
|
||||||
|
/// Sky-High status, the damage dealt will be doubled". Gust and Twister carry the
|
||||||
|
/// <see cref="MoveFlags.EffectiveAgainstFly"/> flag, so their damage against the Sky-High user doubles.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task ChangeIncomingMoveDamage_GustLikeMove_DamageIsDoubled()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user, userVolatile, _, _) = CreateTestSetup();
|
||||||
|
var prevent = false;
|
||||||
|
script.PreventMove(move, ref prevent);
|
||||||
|
await Assert.That(userVolatile.TryGet<ChargeBounceEffect>(out var effect)).IsTrue();
|
||||||
|
|
||||||
|
var incomingMove = Substitute.For<IExecutingMove>();
|
||||||
|
var incomingMoveData = Substitute.For<IMoveData>();
|
||||||
|
incomingMoveData.HasFlag(MoveFlags.HitFlying).Returns(true);
|
||||||
|
incomingMoveData.HasFlag(MoveFlags.EffectiveAgainstFly).Returns(true);
|
||||||
|
incomingMove.UseMove.Returns(incomingMoveData);
|
||||||
|
uint damage = 100;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
effect!.ChangeIncomingMoveDamage(incomingMove, user, 0, ref damage);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(damage).IsEqualTo(200u);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia (Generation V onward): only for Gust and Twister "the damage dealt will be doubled".
|
||||||
|
/// Other moves that can hit the Sky-High user (e.g. Thunder, Sky Uppercut) deal regular damage; they do
|
||||||
|
/// not carry the <see cref="MoveFlags.EffectiveAgainstFly"/> flag.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task ChangeIncomingMoveDamage_ThunderLikeMove_DamageIsUnchanged()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user, userVolatile, _, _) = CreateTestSetup();
|
||||||
|
var prevent = false;
|
||||||
|
script.PreventMove(move, ref prevent);
|
||||||
|
await Assert.That(userVolatile.TryGet<ChargeBounceEffect>(out var effect)).IsTrue();
|
||||||
|
|
||||||
|
var incomingMove = Substitute.For<IExecutingMove>();
|
||||||
|
var incomingMoveData = Substitute.For<IMoveData>();
|
||||||
|
incomingMoveData.HasFlag(MoveFlags.HitFlying).Returns(true);
|
||||||
|
incomingMoveData.HasFlag(MoveFlags.EffectiveAgainstFly).Returns(false);
|
||||||
|
incomingMove.UseMove.Returns(incomingMoveData);
|
||||||
|
uint damage = 100;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
effect!.ChangeIncomingMoveDamage(incomingMove, user, 0, ref damage);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(damage).IsEqualTo(100u);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
using PkmnLib.Dynamic.Models;
|
||||||
|
using PkmnLib.Dynamic.ScriptHandling;
|
||||||
|
using PkmnLib.Plugin.Gen7.Scripts.Moves;
|
||||||
|
using PkmnLib.Plugin.Gen7.Scripts.Side;
|
||||||
|
using PkmnLib.Static.Utils;
|
||||||
|
using TUnit.Assertions.AssertConditions.Throws;
|
||||||
|
|
||||||
|
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests for the <see cref="BrickBreak"/> move script.
|
||||||
|
/// Gen VII Bulbapedia behavior: "Brick Break removes Light Screen and Reflect", from Generation IV onward
|
||||||
|
/// "from the target's side of the field, even if the target is an ally", and from Generation V onwards
|
||||||
|
/// "Brick Break can also remove Aurora Veil."
|
||||||
|
/// </summary>
|
||||||
|
public class BrickBreakTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a mocked Pokémon whose <see cref="IPokemonBattleData.BattleSide"/> is the given side.
|
||||||
|
/// </summary>
|
||||||
|
private static IPokemon CreatePokemonOnSide(IBattleSide side)
|
||||||
|
{
|
||||||
|
var pokemon = Substitute.For<IPokemon>();
|
||||||
|
var battleData = Substitute.For<IPokemonBattleData>();
|
||||||
|
battleData.BattleSide.Returns(side);
|
||||||
|
pokemon.BattleData.Returns(battleData);
|
||||||
|
return pokemon;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a fully mocked test setup where the user targets a Pokémon on the opposing side.
|
||||||
|
/// </summary>
|
||||||
|
private static (BrickBreak script, IExecutingMove move, IPokemon user, IScriptSet userSideScripts, IScriptSet
|
||||||
|
targetSideScripts) CreateTestSetup()
|
||||||
|
{
|
||||||
|
var script = new BrickBreak();
|
||||||
|
var move = Substitute.For<IExecutingMove>();
|
||||||
|
|
||||||
|
var userSideScripts = Substitute.For<IScriptSet>();
|
||||||
|
var userSide = Substitute.For<IBattleSide>();
|
||||||
|
userSide.VolatileScripts.Returns(userSideScripts);
|
||||||
|
var targetSideScripts = Substitute.For<IScriptSet>();
|
||||||
|
var targetSide = Substitute.For<IBattleSide>();
|
||||||
|
targetSide.VolatileScripts.Returns(targetSideScripts);
|
||||||
|
|
||||||
|
var user = CreatePokemonOnSide(userSide);
|
||||||
|
move.User.Returns(user);
|
||||||
|
var target = CreatePokemonOnSide(targetSide);
|
||||||
|
move.Targets.Returns(new IPokemon?[] { target });
|
||||||
|
|
||||||
|
return (script, move, user, userSideScripts, targetSideScripts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia (Generation IV onward): "Brick Break now removes Light Screen and Reflect from the target's
|
||||||
|
/// side of the field, even if the target is an ally." The screen effects are removed from the target's
|
||||||
|
/// side before the move hits. The script names match <see cref="ReflectEffect"/> and
|
||||||
|
/// <see cref="LightScreenEffect"/>.
|
||||||
|
/// </summary>
|
||||||
|
[Test, Arguments("reflect"), Arguments("light_screen")]
|
||||||
|
public async Task OnBeforeMove_ScreenOnTargetSide_RemovesScreenFromTargetSide(string screenScriptName)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, _, _, targetSideScripts) = CreateTestSetup();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnBeforeMove(move);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
targetSideScripts.Received(1).Remove(new StringKey(screenScriptName));
|
||||||
|
await Assert.That(targetSideScripts.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Remove")).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia (Generation V onwards): "Brick Break can also remove Aurora Veil."
|
||||||
|
/// The <see cref="AuroraVeilEffect"/> is removed from the target's side as well.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnBeforeMove_AuroraVeilOnTargetSide_RemovesAuroraVeilFromTargetSide()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, _, _, targetSideScripts) = CreateTestSetup();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnBeforeMove(move);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
targetSideScripts.Received(1).Remove(new StringKey("aurora_veil"));
|
||||||
|
await Assert.That(targetSideScripts.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Remove")).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia (Generation IV onward): "Brick Break now removes Light Screen and Reflect from the target's
|
||||||
|
/// side of the field". When the target is an opponent, screens on the user's own side stay up.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnBeforeMove_ScreensOnUserSide_DoesNotRemoveScreensFromUserSide()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, _, userSideScripts, _) = CreateTestSetup();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnBeforeMove(move);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(userSideScripts.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Remove")).IsFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia (Generation IV onward): "Brick Break now removes Light Screen and Reflect from the target's
|
||||||
|
/// side of the field, even if the target is an ally." An ally shares the user's side, so targeting an ally
|
||||||
|
/// removes the screens from the user's own side.
|
||||||
|
/// </summary>
|
||||||
|
[Test,
|
||||||
|
TestFailing("BrickBreak.OnBeforeMove excludes the user's own side unconditionally, so targeting an " +
|
||||||
|
"ally removes nothing (Bulbapedia: 'removes Light Screen and Reflect from the target's " +
|
||||||
|
"side of the field, even if the target is an ally')")]
|
||||||
|
public async Task OnBeforeMove_AllyTargetOnUserSide_RemovesScreensFromUserSide()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user, userSideScripts, _) = CreateTestSetup();
|
||||||
|
var ally = CreatePokemonOnSide(user.BattleData!.BattleSide);
|
||||||
|
move.Targets.Returns(new IPokemon?[] { ally });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnBeforeMove(move);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(userSideScripts.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Remove")).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Brick Break removes Light Screen and Reflect [...], then inflicts damage."
|
||||||
|
/// When no screens are up there is nothing to remove; the move simply proceeds without error and the
|
||||||
|
/// sides remain unaffected.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnBeforeMove_NoScreensPresent_LeavesSidesUnchanged()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var script = new BrickBreak();
|
||||||
|
var move = Substitute.For<IExecutingMove>();
|
||||||
|
// Real, empty script sets so the removals (if any wrongly happen) would be observable as mutations.
|
||||||
|
var userSideScripts = new ScriptSet(Substitute.For<IBattleSide>());
|
||||||
|
var targetSideScripts = new ScriptSet(Substitute.For<IBattleSide>());
|
||||||
|
var userSide = Substitute.For<IBattleSide>();
|
||||||
|
userSide.VolatileScripts.Returns(userSideScripts);
|
||||||
|
var targetSide = Substitute.For<IBattleSide>();
|
||||||
|
targetSide.VolatileScripts.Returns(targetSideScripts);
|
||||||
|
var user = CreatePokemonOnSide(userSide);
|
||||||
|
move.User.Returns(user);
|
||||||
|
var target = CreatePokemonOnSide(targetSide);
|
||||||
|
move.Targets.Returns(new IPokemon?[] { target });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnBeforeMove(move);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(userSideScripts.Count).IsEqualTo(0);
|
||||||
|
await Assert.That(targetSideScripts.Count).IsEqualTo(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Technical test: without any targets (e.g. outside of battle) the script does nothing and does not throw.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnBeforeMove_NoTargets_DoesNotThrow()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var script = new BrickBreak();
|
||||||
|
var move = Substitute.For<IExecutingMove>();
|
||||||
|
var user = Substitute.For<IPokemon>();
|
||||||
|
user.BattleData.Returns((IPokemonBattleData?)null);
|
||||||
|
move.User.Returns(user);
|
||||||
|
move.Targets.Returns(Array.Empty<IPokemon?>());
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
await Assert.That(() => script.OnBeforeMove(move)).ThrowsNothing();
|
||||||
|
}
|
||||||
|
}
|
||||||
101
Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BrineTests.cs
Normal file
101
Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BrineTests.cs
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
using PkmnLib.Dynamic.Models;
|
||||||
|
using PkmnLib.Plugin.Gen7.Scripts.Moves;
|
||||||
|
using PkmnLib.Static;
|
||||||
|
|
||||||
|
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests for the <see cref="Brine"/> move script.
|
||||||
|
/// Behavior is verified against the Bulbapedia page for Brine.
|
||||||
|
/// </summary>
|
||||||
|
public class BrineTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a fully mocked test setup for Brine tests, with a target whose max HP
|
||||||
|
/// (<see cref="IPokemon.BoostedStats"/>) and <see cref="IPokemon.CurrentHealth"/> are configured.
|
||||||
|
/// </summary>
|
||||||
|
private static (Brine brine, IExecutingMove move, IPokemon target) CreateTestSetup(uint maxHp, uint currentHp)
|
||||||
|
{
|
||||||
|
var brine = new Brine();
|
||||||
|
var move = Substitute.For<IExecutingMove>();
|
||||||
|
var target = Substitute.For<IPokemon>();
|
||||||
|
target.BoostedStats.Returns(new StatisticSet<uint>(maxHp, 0, 0, 0, 0, 0));
|
||||||
|
target.CurrentHealth.Returns(currentHp);
|
||||||
|
return (brine, move, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Brine inflicts damage, and its power doubles to 130 when the target is at or below
|
||||||
|
/// 50% health."
|
||||||
|
/// At exactly half HP, the base power of 65 doubles to 130.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task ChangeBasePower_TargetAtExactlyHalfHp_BasePowerDoubles()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (brine, move, target) = CreateTestSetup(100, 50);
|
||||||
|
ushort basePower = 65;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
brine.ChangeBasePower(move, target, 0, ref basePower);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(basePower).IsEqualTo((ushort)130);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "its power doubles to 130 when the target is at or below 50% health."
|
||||||
|
/// Tests several HP fractions at or below half, including odd max HP values where 50% is not
|
||||||
|
/// a whole number (50/101 ≈ 49.5% is below half).
|
||||||
|
/// </summary>
|
||||||
|
[Test, Arguments(100u, 49u), Arguments(100u, 1u), Arguments(200u, 100u), Arguments(101u, 50u)]
|
||||||
|
public async Task ChangeBasePower_TargetAtOrBelowHalfHp_BasePowerDoubles(uint maxHp, uint currentHp)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (brine, move, target) = CreateTestSetup(maxHp, currentHp);
|
||||||
|
ushort basePower = 65;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
brine.ChangeBasePower(move, target, 0, ref basePower);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(basePower).IsEqualTo((ushort)130);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "its power doubles to 130 when the target is at or below 50% health."
|
||||||
|
/// Above 50% health the power must remain unchanged, including one HP above half and odd max HP
|
||||||
|
/// values where 51/101 ≈ 50.5% is just above half.
|
||||||
|
/// </summary>
|
||||||
|
[Test, Arguments(100u, 51u), Arguments(100u, 100u), Arguments(101u, 51u), Arguments(200u, 101u)]
|
||||||
|
public async Task ChangeBasePower_TargetAboveHalfHp_BasePowerUnchanged(uint maxHp, uint currentHp)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (brine, move, target) = CreateTestSetup(maxHp, currentHp);
|
||||||
|
ushort basePower = 65;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
brine.ChangeBasePower(move, target, 0, ref basePower);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(basePower).IsEqualTo((ushort)65);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Technical test: doubling an already very high base power must clamp to
|
||||||
|
/// <see cref="ushort.MaxValue"/> instead of overflowing.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task ChangeBasePower_DoublingWouldOverflow_ClampsToMax()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (brine, move, target) = CreateTestSetup(100, 50);
|
||||||
|
ushort basePower = ushort.MaxValue - 100;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
brine.ChangeBasePower(move, target, 0, ref basePower);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(basePower).IsEqualTo(ushort.MaxValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
243
Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BugBiteTests.cs
Normal file
243
Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BugBiteTests.cs
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
using PkmnLib.Dynamic.Events;
|
||||||
|
using PkmnLib.Dynamic.Libraries;
|
||||||
|
using PkmnLib.Dynamic.Models;
|
||||||
|
using PkmnLib.Dynamic.ScriptHandling;
|
||||||
|
using PkmnLib.Plugin.Gen7.Scripts.Moves;
|
||||||
|
using PkmnLib.Static;
|
||||||
|
using PkmnLib.Static.Moves;
|
||||||
|
using PkmnLib.Static.Utils;
|
||||||
|
|
||||||
|
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests for the <see cref="BugBite"/> move script.
|
||||||
|
/// Behavior is verified against the Bulbapedia page for Bug Bite.
|
||||||
|
/// </summary>
|
||||||
|
public class BugBiteTests
|
||||||
|
{
|
||||||
|
private const string BerryEffectName = "test_berry_effect";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Minimal <see cref="ItemScript"/> that records whether its effect was applied through
|
||||||
|
/// <see cref="ItemScript.OnUse"/>.
|
||||||
|
/// </summary>
|
||||||
|
private class RecordingItemScript : ItemScript
|
||||||
|
{
|
||||||
|
public RecordingItemScript(IItem item) : base(item)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool WasUsed { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override bool IsItemUsable => true;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void OnUse(EventHook eventHook) => WasUsed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a fully mocked test setup for BugBite tests. The battle's <see cref="ScriptResolver"/> is a
|
||||||
|
/// real resolver with a single registered item script constructor for <see cref="BerryEffectName"/>, so
|
||||||
|
/// eating a Berry runs a <see cref="RecordingItemScript"/> that the test can inspect.
|
||||||
|
/// </summary>
|
||||||
|
private static (BugBite bugBite, IExecutingMove move, IPokemon target, IHitData hitData, List<RecordingItemScript>
|
||||||
|
createdItemScripts) CreateTestSetup(IItem? targetHeldItem, bool canSteal = true)
|
||||||
|
{
|
||||||
|
var bugBite = new BugBite();
|
||||||
|
|
||||||
|
var createdItemScripts = new List<RecordingItemScript>();
|
||||||
|
var resolver = new ScriptResolver(new Dictionary<(ScriptCategory, StringKey), Func<Script>>(),
|
||||||
|
new Dictionary<StringKey, Func<IItem, ItemScript>>
|
||||||
|
{
|
||||||
|
{
|
||||||
|
new StringKey(BerryEffectName), item =>
|
||||||
|
{
|
||||||
|
var script = new RecordingItemScript(item);
|
||||||
|
createdItemScripts.Add(script);
|
||||||
|
return script;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
var battle = Substitute.For<IBattle>();
|
||||||
|
battle.EventHook.Returns(new EventHook());
|
||||||
|
var dynamicLibrary = Substitute.For<IDynamicLibrary>();
|
||||||
|
dynamicLibrary.ScriptResolver.Returns(resolver);
|
||||||
|
battle.Library.Returns(dynamicLibrary);
|
||||||
|
|
||||||
|
var battleData = Substitute.For<IPokemonBattleData>();
|
||||||
|
battleData.Battle.Returns(battle);
|
||||||
|
|
||||||
|
var user = Substitute.For<IPokemon>();
|
||||||
|
user.BattleData.Returns(battleData);
|
||||||
|
|
||||||
|
var move = Substitute.For<IExecutingMove>();
|
||||||
|
move.User.Returns(user);
|
||||||
|
move.Battle.Returns(battle);
|
||||||
|
|
||||||
|
var target = Substitute.For<IPokemon>();
|
||||||
|
target.HeldItem.Returns(targetHeldItem);
|
||||||
|
if (targetHeldItem != null && canSteal)
|
||||||
|
{
|
||||||
|
target.TryStealHeldItem(out Arg.Any<IItem?>()).Returns(x =>
|
||||||
|
{
|
||||||
|
x[0] = targetHeldItem;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
target.TryStealHeldItem(out Arg.Any<IItem?>()).Returns(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
var hitData = Substitute.For<IHitData>();
|
||||||
|
move.GetHitData(target, 0).Returns(hitData);
|
||||||
|
|
||||||
|
return (bugBite, move, target, hitData, createdItemScripts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a mocked Berry item whose battle effect is <see cref="BerryEffectName"/>.
|
||||||
|
/// </summary>
|
||||||
|
private static IItem CreateBerry()
|
||||||
|
{
|
||||||
|
var berry = Substitute.For<IItem>();
|
||||||
|
berry.Category.Returns(ItemCategory.Berry);
|
||||||
|
berry.BattleEffect.Returns(new SecondaryEffectImpl(-1, new StringKey(BerryEffectName),
|
||||||
|
new Dictionary<StringKey, object?>()));
|
||||||
|
return berry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "If the target is holding a Berry, the user will eat the Berry and gain its effect."
|
||||||
|
/// Eating the Berry removes it from the target via <see cref="IPokemon.ForceSetHeldItem"/>.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_TargetHoldsBerry_BerryIsRemovedFromTarget()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (bugBite, move, target, _, _) = CreateTestSetup(CreateBerry());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
bugBite.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(target.ReceivedCalls().Any(c =>
|
||||||
|
c.GetMethodInfo().Name == "ForceSetHeldItem" && c.GetArguments()[0] == null)).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "If the target is holding a Berry, the user will eat the Berry and gain its effect."
|
||||||
|
/// The Berry's item script is executed for the user, applying its effect.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_TargetHoldsBerry_UserGainsBerryEffect()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (bugBite, move, target, _, createdItemScripts) = CreateTestSetup(CreateBerry());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
bugBite.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert - exactly one berry script was resolved and its effect was applied
|
||||||
|
await Assert.That(createdItemScripts.Count).IsEqualTo(1);
|
||||||
|
await Assert.That(createdItemScripts[0].WasUsed).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "If the target is holding a Berry, the user will eat the Berry and gain its effect."
|
||||||
|
/// When the Berry is successfully eaten, the secondary effect must not be marked as failed.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_TargetHoldsBerry_HitDoesNotFail()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (bugBite, move, target, hitData, _) = CreateTestSetup(CreateBerry());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
bugBite.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(hitData.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Fail")).IsFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "If the target is holding a Berry, the user will eat the Berry and gain its effect."
|
||||||
|
/// A held item that is not a Berry is not eaten: the secondary effect fails and the target keeps
|
||||||
|
/// its item.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_TargetHoldsNonBerryItem_EffectFailsAndItemIsKept()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var nonBerry = Substitute.For<IItem>();
|
||||||
|
nonBerry.Category.Returns(ItemCategory.MiscItem);
|
||||||
|
var (bugBite, move, target, hitData, _) = CreateTestSetup(nonBerry);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
bugBite.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(hitData.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Fail")).IsTrue();
|
||||||
|
await Assert.That(target.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ForceSetHeldItem")).IsFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "If the target is holding a Berry, the user will eat the Berry and gain its effect."
|
||||||
|
/// With no held item at all there is nothing to eat, so the secondary effect fails.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_TargetHoldsNoItem_EffectFails()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (bugBite, move, target, hitData, _) = CreateTestSetup(null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
bugBite.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(hitData.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Fail")).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Bug Bite will not consume the Berry of a target that has the Ability Sticky Hold."
|
||||||
|
/// When the Berry cannot be stolen (<see cref="IPokemon.TryStealHeldItem"/> returns false, as with
|
||||||
|
/// Sticky Hold), the target keeps its Berry and the effect fails.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_BerryCannotBeStolen_TargetKeepsBerry()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (bugBite, move, target, hitData, createdItemScripts) = CreateTestSetup(CreateBerry(), false);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
bugBite.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert - the berry is not removed, its effect is not gained, and the effect fails
|
||||||
|
await Assert.That(target.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ForceSetHeldItem")).IsFalse();
|
||||||
|
await Assert.That(createdItemScripts.Count).IsEqualTo(0);
|
||||||
|
await Assert.That(hitData.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Fail")).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If the user has no <see cref="IPokemon.BattleData"/>, the script does nothing: no Berry is eaten
|
||||||
|
/// and the hit is not failed.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_UserHasNoBattleData_DoesNothing()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (bugBite, move, target, hitData, _) = CreateTestSetup(CreateBerry());
|
||||||
|
var user = Substitute.For<IPokemon>();
|
||||||
|
user.BattleData.Returns((IPokemonBattleData?)null);
|
||||||
|
move.User.Returns(user);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
bugBite.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(target.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ForceSetHeldItem")).IsFalse();
|
||||||
|
await Assert.That(hitData.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Fail")).IsFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
199
Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BurnUpTests.cs
Normal file
199
Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BurnUpTests.cs
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
using PkmnLib.Dynamic.Libraries;
|
||||||
|
using PkmnLib.Dynamic.Models;
|
||||||
|
using PkmnLib.Plugin.Gen7.Scripts.Moves;
|
||||||
|
using PkmnLib.Static;
|
||||||
|
using PkmnLib.Static.Libraries;
|
||||||
|
using PkmnLib.Static.Utils;
|
||||||
|
|
||||||
|
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests for the <see cref="BurnUp"/> move script.
|
||||||
|
/// Behavior is verified against the Bulbapedia page for Burn Up.
|
||||||
|
/// </summary>
|
||||||
|
public class BurnUpTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a fully mocked test setup for Burn Up tests. The static library's type library is a real
|
||||||
|
/// <see cref="TypeLibrary"/> with "fire" and "water" registered. The user is a Water-type, optionally
|
||||||
|
/// also carrying the Fire type.
|
||||||
|
/// </summary>
|
||||||
|
private static (BurnUp burnUp, IExecutingMove move, IPokemon user, IPokemon target, IHitData hitData, TypeIdentifier
|
||||||
|
fireType) CreateTestSetup(bool userIsFireType, bool userIsFrozen = false)
|
||||||
|
{
|
||||||
|
var burnUp = new BurnUp();
|
||||||
|
|
||||||
|
var typeLibrary = new TypeLibrary();
|
||||||
|
var fireType = typeLibrary.RegisterType(new StringKey("fire"));
|
||||||
|
var waterType = typeLibrary.RegisterType(new StringKey("water"));
|
||||||
|
|
||||||
|
var staticLibrary = Substitute.For<IStaticLibrary>();
|
||||||
|
staticLibrary.Types.Returns(typeLibrary);
|
||||||
|
var dynamicLibrary = Substitute.For<IDynamicLibrary>();
|
||||||
|
dynamicLibrary.StaticLibrary.Returns(staticLibrary);
|
||||||
|
var battle = Substitute.For<IBattle>();
|
||||||
|
battle.Library.Returns(dynamicLibrary);
|
||||||
|
var battleData = Substitute.For<IPokemonBattleData>();
|
||||||
|
battleData.Battle.Returns(battle);
|
||||||
|
|
||||||
|
var user = Substitute.For<IPokemon>();
|
||||||
|
user.BattleData.Returns(battleData);
|
||||||
|
user.Types.Returns(userIsFireType
|
||||||
|
? new List<TypeIdentifier> { fireType, waterType }
|
||||||
|
: new List<TypeIdentifier> { waterType });
|
||||||
|
user.HasStatus(new StringKey("frozen")).Returns(userIsFrozen);
|
||||||
|
|
||||||
|
var move = Substitute.For<IExecutingMove>();
|
||||||
|
move.User.Returns(user);
|
||||||
|
|
||||||
|
var target = Substitute.For<IPokemon>();
|
||||||
|
var hitData = Substitute.For<IHitData>();
|
||||||
|
move.GetHitData(target, 0).Returns(hitData);
|
||||||
|
|
||||||
|
return (burnUp, move, user, target, hitData, fireType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper that checks whether <see cref="IPokemon.RemoveType"/> was called with the given type.
|
||||||
|
/// </summary>
|
||||||
|
private static bool ReceivedRemoveType(IPokemon user, TypeIdentifier type) =>
|
||||||
|
user.ReceivedCalls().Any(c =>
|
||||||
|
c.GetMethodInfo().Name == "RemoveType" && type.Equals((TypeIdentifier)c.GetArguments()[0]!));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "If used by a Fire-type Pokémon, Burn Up will thaw out the user if it is frozen, and
|
||||||
|
/// then inflict damage to the target and cause the user to lose its Fire type."
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_FireTypeUser_UserLosesFireType()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (burnUp, move, user, target, _, fireType) = CreateTestSetup(true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
burnUp.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(ReceivedRemoveType(user, fireType)).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "If used by a Fire-type Pokémon, Burn Up will ... inflict damage to the target".
|
||||||
|
/// For a Fire-type user, the hit is not marked as failed.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_FireTypeUser_HitDoesNotFail()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (burnUp, move, _, target, hitData, _) = CreateTestSetup(true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
burnUp.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(hitData.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Fail")).IsFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Burn Up fails if the user is not Fire-type, making the move always fail after the
|
||||||
|
/// first use until the Pokémon regains its Fire type".
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_NonFireTypeUser_HitFails()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (burnUp, move, _, target, hitData, _) = CreateTestSetup(false);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
burnUp.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(hitData.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Fail")).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Burn Up fails if the user is not Fire-type".
|
||||||
|
/// A failed Burn Up does not remove any type from the user.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_NonFireTypeUser_NoTypeIsRemoved()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (burnUp, move, user, target, _, _) = CreateTestSetup(false);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
burnUp.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(user.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "RemoveType")).IsFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "If used by a Fire-type Pokémon, Burn Up will thaw out the user if it is frozen".
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_FrozenFireTypeUser_UserIsThawed()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (burnUp, move, user, target, _, _) = CreateTestSetup(true, true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
burnUp.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(user.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ClearStatus")).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Burn Up cannot thaw the user if the move would fail (due to the user not being
|
||||||
|
/// Fire-type)."
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_FrozenNonFireTypeUser_UserIsNotThawed()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (burnUp, move, user, target, _, _) = CreateTestSetup(false, true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
burnUp.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(user.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ClearStatus")).IsFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Burn Up will thaw out the user if it is frozen".
|
||||||
|
/// A Fire-type user that is not frozen has no status cleared.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_NonFrozenFireTypeUser_StatusIsNotCleared()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (burnUp, move, user, target, _, _) = CreateTestSetup(true, false);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
burnUp.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(user.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ClearStatus")).IsFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If the user has no <see cref="IPokemon.BattleData"/>, the script does nothing: no type is removed
|
||||||
|
/// and the hit is not failed.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_UserHasNoBattleData_DoesNothing()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (burnUp, move, user, target, hitData, _) = CreateTestSetup(true);
|
||||||
|
user.BattleData.Returns((IPokemonBattleData?)null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
burnUp.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(user.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "RemoveType")).IsFalse();
|
||||||
|
await Assert.That(hitData.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Fail")).IsFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,7 +37,7 @@ public class BatonPass : Script, IScriptOnSecondaryEffect
|
|||||||
foreach (var script in volatileScripts)
|
foreach (var script in volatileScripts)
|
||||||
{
|
{
|
||||||
if (script is not IBatonPassException)
|
if (script is not IBatonPassException)
|
||||||
return;
|
continue;
|
||||||
toSwitch.Volatile.Add(script);
|
toSwitch.Volatile.Add(script);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ public class BellyDrum : Script, IScriptOnSecondaryEffect
|
|||||||
move.GetHitData(target, hit).Fail();
|
move.GetHitData(target, hit).Fail();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (target.StatBoost.Attack >= 6)
|
||||||
|
{
|
||||||
|
move.GetHitData(target, hit).Fail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
target.Damage(maxHealthHalved, DamageSource.Misc, forceDamage: true);
|
target.Damage(maxHealthHalved, DamageSource.Misc, forceDamage: true);
|
||||||
// Raising the user's Attack by 12 stages should always set it to +6.
|
// Raising the user's Attack by 12 stages should always set it to +6.
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ public class Bestow : Script, IScriptOnSecondaryEffect
|
|||||||
public void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
|
public void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
|
||||||
{
|
{
|
||||||
var user = move.User;
|
var user = move.User;
|
||||||
var userHeldItem = user.RemoveHeldItemForBattle();
|
var userHeldItem = user.HeldItem;
|
||||||
var targetHeldItem = target.HeldItem;
|
var targetHeldItem = target.HeldItem;
|
||||||
|
|
||||||
if (userHeldItem == null || targetHeldItem != null)
|
if (userHeldItem == null || targetHeldItem != null)
|
||||||
@@ -15,7 +15,7 @@ public class Bestow : Script, IScriptOnSecondaryEffect
|
|||||||
move.GetHitData(target, hit).Fail();
|
move.GetHitData(target, hit).Fail();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
userHeldItem = user.RemoveHeldItemForBattle();
|
||||||
_ = target.ForceSetHeldItem(userHeldItem);
|
_ = target.ForceSetHeldItem(userHeldItem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -16,11 +16,18 @@ public class Bide : Script, IScriptOnSecondaryEffect
|
|||||||
}
|
}
|
||||||
|
|
||||||
bideEffect.Turns += 1;
|
bideEffect.Turns += 1;
|
||||||
if (bideEffect.Turns < 2)
|
if (bideEffect.Turns < 3)
|
||||||
return;
|
return;
|
||||||
|
if (bideEffect.DamageTaken == 0)
|
||||||
|
{
|
||||||
|
move.GetHitData(target, hit).Fail();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
var lastPokemon = bideEffect.HitBy.LastOrDefault(x => x.BattleData?.IsOnBattlefield == true);
|
var lastPokemon = bideEffect.HitBy.LastOrDefault(x => x.BattleData?.IsOnBattlefield == true);
|
||||||
lastPokemon?.Damage(bideEffect.DamageTaken, DamageSource.MoveDamage);
|
lastPokemon?.Damage(bideEffect.DamageTaken * 2, DamageSource.MoveDamage);
|
||||||
|
}
|
||||||
|
|
||||||
RemoveSelf();
|
move.User.Volatile.Remove<BideEffect>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,9 +5,13 @@ namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
|
|||||||
[Script(ScriptCategory.Move, "block")]
|
[Script(ScriptCategory.Move, "block")]
|
||||||
public class Block : Script, IScriptOnSecondaryEffect
|
public class Block : Script, IScriptOnSecondaryEffect
|
||||||
{
|
{
|
||||||
|
private static StringKey GhostTypeName => new("ghost");
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
|
public void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
|
||||||
{
|
{
|
||||||
|
if (target.Types.Any(x => x.Name == GhostTypeName))
|
||||||
|
return;
|
||||||
target.Volatile.Add(new BlockEffect());
|
target.Volatile.Add(new BlockEffect());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,10 +8,8 @@ public class BrickBreak : Script, IScriptOnBeforeMove
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void OnBeforeMove(IExecutingMove move)
|
public void OnBeforeMove(IExecutingMove move)
|
||||||
{
|
{
|
||||||
var sides = move.User.BattleData?.Battle.Sides;
|
var targets = move.Targets.WhereNotNull();
|
||||||
if (sides == null)
|
var sides = targets.Select(x => x.BattleData!.BattleSide).Distinct();
|
||||||
return;
|
|
||||||
|
|
||||||
foreach (var side in sides)
|
foreach (var side in sides)
|
||||||
{
|
{
|
||||||
side.VolatileScripts.Remove(ScriptUtils.ResolveName<ReflectEffect>());
|
side.VolatileScripts.Remove(ScriptUtils.ResolveName<ReflectEffect>());
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ public class BanefulBunkerEffect : ProtectionEffectScript
|
|||||||
public override void BlockIncomingHit(IExecutingMove executingMove, IPokemon target, byte hitIndex, ref bool block)
|
public override void BlockIncomingHit(IExecutingMove executingMove, IPokemon target, byte hitIndex, ref bool block)
|
||||||
{
|
{
|
||||||
base.BlockIncomingHit(executingMove, target, hitIndex, ref block);
|
base.BlockIncomingHit(executingMove, target, hitIndex, ref block);
|
||||||
if (executingMove.UseMove.Category != MoveCategory.Status &&
|
if (block && executingMove.UseMove.Category != MoveCategory.Status &&
|
||||||
executingMove.GetHitData(target, hitIndex).IsContact)
|
executingMove.GetHitData(target, hitIndex).IsContact)
|
||||||
{
|
{
|
||||||
executingMove.User.SetStatus(ScriptUtils.ResolveName<Status.Poisoned>(), executingMove.User);
|
executingMove.User.SetStatus(ScriptUtils.ResolveName<Status.Poisoned>(), executingMove.User);
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ public class ChargeBounceEffect : Script, IScriptForceTurnSelection, IScriptChan
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void ChangeIncomingMoveDamage(IExecutingMove move, IPokemon target, byte hit, ref uint damage)
|
public void ChangeIncomingMoveDamage(IExecutingMove move, IPokemon target, byte hit, ref uint damage)
|
||||||
{
|
{
|
||||||
if (!move.UseMove.HasFlag(MoveFlags.EffectiveAgainstFly))
|
if (move.UseMove.HasFlag(MoveFlags.EffectiveAgainstFly))
|
||||||
damage *= 2;
|
damage *= 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user