More unit tests, more fixes
This commit is contained in:
@@ -0,0 +1,52 @@
|
|||||||
|
using PkmnLib.Dynamic.Models;
|
||||||
|
using PkmnLib.Plugin.Gen7.Scripts.Moves;
|
||||||
|
|
||||||
|
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests for the <see cref="DarkestLariat"/> move script.
|
||||||
|
/// Gen VII Bulbapedia behavior: "Darkest Lariat inflicts damage, ignoring any changes to the target's
|
||||||
|
/// Defense and evasion stat stages."
|
||||||
|
/// </summary>
|
||||||
|
public class DarkestLariatTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Darkest Lariat inflicts damage, ignoring any changes to the target's Defense [...]
|
||||||
|
/// stat stages." The script bypasses the target's defensive stat boosts.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task BypassDefensiveStatBoosts_Always_SetsBypassToTrue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var script = new DarkestLariat();
|
||||||
|
var move = Substitute.For<IExecutingMove>();
|
||||||
|
var target = Substitute.For<IPokemon>();
|
||||||
|
var bypass = false;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.BypassDefensiveStatBoosts(move, target, 0, ref bypass);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(bypass).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Darkest Lariat inflicts damage, ignoring any changes to the target's [...] evasion
|
||||||
|
/// stat stages." The script bypasses the target's evasion stat boosts.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task BypassEvasionStatBoosts_Always_SetsBypassToTrue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var script = new DarkestLariat();
|
||||||
|
var move = Substitute.For<IExecutingMove>();
|
||||||
|
var target = Substitute.For<IPokemon>();
|
||||||
|
var bypass = false;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.BypassEvasionStatBoosts(move, target, 0, ref bypass);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(bypass).IsTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
164
Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DefogTests.cs
Normal file
164
Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DefogTests.cs
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
using PkmnLib.Dynamic.Models;
|
||||||
|
using PkmnLib.Dynamic.ScriptHandling;
|
||||||
|
using PkmnLib.Dynamic.ScriptHandling.Registry;
|
||||||
|
using PkmnLib.Plugin.Gen7.Scripts.Moves;
|
||||||
|
using PkmnLib.Plugin.Gen7.Scripts.Side;
|
||||||
|
using PkmnLib.Static;
|
||||||
|
using PkmnLib.Static.Utils;
|
||||||
|
|
||||||
|
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests for the <see cref="Defog"/> move script.
|
||||||
|
/// Gen VII Bulbapedia behavior: "Defog lowers the target's evasion by one stage" and clears field effects:
|
||||||
|
/// "It removes Spikes, Toxic Spikes, Stealth Rock, and Sticky Web from both sides, while Light Screen,
|
||||||
|
/// Reflect, Safeguard, Mist, and Aurora Veil are cleared from the target's side only."
|
||||||
|
/// </summary>
|
||||||
|
public class DefogTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a fully mocked test setup where the target is on side 1 and the user on side 0.
|
||||||
|
/// </summary>
|
||||||
|
private static (Defog script, IExecutingMove move, IPokemon target, IScriptSet targetSideScripts, IScriptSet
|
||||||
|
userSideScripts) CreateTestSetup()
|
||||||
|
{
|
||||||
|
var script = new Defog();
|
||||||
|
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 battle = Substitute.For<IBattle>();
|
||||||
|
battle.Sides.Returns(new[] { userSide, targetSide });
|
||||||
|
|
||||||
|
var user = Substitute.For<IPokemon>();
|
||||||
|
var userBattleData = Substitute.For<IPokemonBattleData>();
|
||||||
|
userBattleData.Battle.Returns(battle);
|
||||||
|
userBattleData.SideIndex.Returns((byte)0);
|
||||||
|
user.BattleData.Returns(userBattleData);
|
||||||
|
move.User.Returns(user);
|
||||||
|
|
||||||
|
var target = Substitute.For<IPokemon>();
|
||||||
|
var targetBattleData = Substitute.For<IPokemonBattleData>();
|
||||||
|
targetBattleData.Battle.Returns(battle);
|
||||||
|
targetBattleData.SideIndex.Returns((byte)1);
|
||||||
|
target.BattleData.Returns(targetBattleData);
|
||||||
|
|
||||||
|
return (script, move, target, targetSideScripts, userSideScripts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Light Screen, Reflect, Safeguard, Mist, and Aurora Veil are cleared from the target's
|
||||||
|
/// side only." The screen and protection effects (<see cref="LightScreenEffect"/>,
|
||||||
|
/// <see cref="ReflectEffect"/>, <see cref="SafeguardEffect"/>, <see cref="MistEffect"/>) are removed
|
||||||
|
/// from the target's side.
|
||||||
|
/// </summary>
|
||||||
|
[Test, Arguments("light_screen"), Arguments("reflect"), Arguments("safe_guard"), Arguments("mist")]
|
||||||
|
public async Task OnSecondaryEffect_ScreenOnTargetSide_RemovesEffectFromTargetSide(string scriptName)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, target, targetSideScripts, _) = CreateTestSetup();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
targetSideScripts.Received(1).Remove(new StringKey(scriptName));
|
||||||
|
await Assert.That(targetSideScripts.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Remove")).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "It removes Spikes, Toxic Spikes, Stealth Rock, and Sticky Web" — the entry hazards
|
||||||
|
/// (<see cref="SpikesEffect"/>, <see cref="ToxicSpikesEffect"/>, <see cref="StealthRockEffect"/>,
|
||||||
|
/// <see cref="StickyWebEffect"/>) are removed from the target's side.
|
||||||
|
/// </summary>
|
||||||
|
[Test, Arguments("spikes"), Arguments("toxic_spikes"), Arguments("stealth_rock"), Arguments("sticky_web")]
|
||||||
|
public async Task OnSecondaryEffect_HazardOnTargetSide_RemovesHazardFromTargetSide(string scriptName)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, target, targetSideScripts, _) = CreateTestSetup();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
targetSideScripts.Received(1).Remove(new StringKey(scriptName));
|
||||||
|
await Assert.That(targetSideScripts.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Remove")).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia (Generations VI and VII): "It removes Spikes, Toxic Spikes, Stealth Rock, and Sticky Web
|
||||||
|
/// from both sides" — the entry hazards must also be removed from the user's own side.
|
||||||
|
/// </summary>
|
||||||
|
[Test, Arguments("spikes"), Arguments("toxic_spikes"), Arguments("stealth_rock"), Arguments("sticky_web")]
|
||||||
|
public async Task OnSecondaryEffect_HazardOnUserSide_RemovesHazardFromUserSide(string scriptName)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, target, _, userSideScripts) = CreateTestSetup();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
userSideScripts.Received(1).Remove(new StringKey(scriptName));
|
||||||
|
await Assert.That(userSideScripts.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Remove")).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia (Generations VI and VII): "Light Screen, Reflect, Safeguard, Mist, and Aurora Veil are
|
||||||
|
/// cleared from the target's side only." The <see cref="AuroraVeilEffect"/> must be removed as well.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_AuroraVeilOnTargetSide_RemovesAuroraVeilFromTargetSide()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, target, targetSideScripts, _) = CreateTestSetup();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
targetSideScripts.Received(1).Remove(new StringKey("aurora_veil"));
|
||||||
|
await Assert.That(targetSideScripts.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Remove")).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Defog lowers the target's evasion by one stage". Neither the script nor the move data
|
||||||
|
/// applies the evasion drop, so the target's evasion is never lowered.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_Always_LowersTargetEvasionByOneStage()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, target, _, _) = CreateTestSetup();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(target.ReceivedCalls().Any(c =>
|
||||||
|
c.GetMethodInfo().Name == "ChangeStatBoost" && (Statistic)c.GetArguments()[0]! == Statistic.Evasion &&
|
||||||
|
(sbyte)c.GetArguments()[1]! == -1)).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Technical test: without battle data (outside of battle) the secondary effect does nothing and does
|
||||||
|
/// not throw.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_TargetHasNoBattleData_DoesNotThrow()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var script = new Defog();
|
||||||
|
var move = Substitute.For<IExecutingMove>();
|
||||||
|
var target = Substitute.For<IPokemon>();
|
||||||
|
target.BattleData.Returns((IPokemonBattleData?)null);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
await Assert.That(() => script.OnSecondaryEffect(move, target, 0)).ThrowsNothing();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
using PkmnLib.Dynamic.Models;
|
||||||
|
using PkmnLib.Dynamic.Models.Choices;
|
||||||
|
using PkmnLib.Dynamic.ScriptHandling;
|
||||||
|
using PkmnLib.Dynamic.ScriptHandling.Registry;
|
||||||
|
using PkmnLib.Plugin.Gen7.Scripts.Moves;
|
||||||
|
using PkmnLib.Plugin.Gen7.Scripts.Pokemon;
|
||||||
|
using PkmnLib.Static;
|
||||||
|
|
||||||
|
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests for the <see cref="DestinyBond"/> move script and its <see cref="DestinyBondEffect"/>.
|
||||||
|
/// Gen VII Bulbapedia behavior: "When Destiny Bond is used, the user gains the Destiny Bound status.
|
||||||
|
/// Until the user moves again, if the user faints as the direct result of an attack, the attacking
|
||||||
|
/// Pokémon also faints."
|
||||||
|
/// </summary>
|
||||||
|
public class DestinyBondTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a mocked user whose <see cref="IPokemon.Volatile"/> is a real <see cref="ScriptSet"/> so
|
||||||
|
/// the volatile added by the move can be inspected.
|
||||||
|
/// </summary>
|
||||||
|
private static (DestinyBond script, IExecutingMove move, IPokemon user, ScriptSet userVolatile) CreateTestSetup()
|
||||||
|
{
|
||||||
|
var script = new DestinyBond();
|
||||||
|
var move = Substitute.For<IExecutingMove>();
|
||||||
|
var user = Substitute.For<IPokemon>();
|
||||||
|
var userVolatile = new ScriptSet(user);
|
||||||
|
user.Volatile.Returns(userVolatile);
|
||||||
|
user.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));
|
||||||
|
move.User.Returns(user);
|
||||||
|
return (script, move, user, userVolatile);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a fainting Pokémon whose battle's <see cref="BattleChoiceQueue.LastRanChoice"/> is a move
|
||||||
|
/// choice made by the given attacker.
|
||||||
|
/// </summary>
|
||||||
|
private static IPokemon CreateFaintingPokemonAttackedBy(IPokemon attacker)
|
||||||
|
{
|
||||||
|
var moveChoice = Substitute.For<IMoveChoice>();
|
||||||
|
moveChoice.User.Returns(attacker);
|
||||||
|
var queue = new BattleChoiceQueue([moveChoice]);
|
||||||
|
queue.Dequeue();
|
||||||
|
|
||||||
|
var battle = Substitute.For<IBattle>();
|
||||||
|
battle.ChoiceQueue.Returns(queue);
|
||||||
|
var battleData = Substitute.For<IPokemonBattleData>();
|
||||||
|
battleData.Battle.Returns(battle);
|
||||||
|
var pokemon = Substitute.For<IPokemon>();
|
||||||
|
pokemon.BattleData.Returns(battleData);
|
||||||
|
return pokemon;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "When Destiny Bond is used, the user gains the Destiny Bound status." The move adds the
|
||||||
|
/// <see cref="DestinyBondEffect"/> volatile script to the user.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_Used_AddsDestinyBondEffectToUser()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, _, userVolatile) = CreateTestSetup();
|
||||||
|
var target = Substitute.For<IPokemon>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(userVolatile.Contains(ScriptUtils.ResolveName<DestinyBondEffect>())).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "if the user faints as the direct result of an attack, the attacking Pokémon also
|
||||||
|
/// faints." When the Destiny Bound user faints from move damage, the attacker takes damage far in
|
||||||
|
/// excess of its HP, guaranteeing the faint.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnFaint_FaintsFromMoveDamage_AttackerIsDamagedToFaint()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var effect = new DestinyBondEffect();
|
||||||
|
var attacker = Substitute.For<IPokemon>();
|
||||||
|
attacker.BoostedStats.Returns(new StatisticSet<uint>(100, 1, 1, 1, 1, 1));
|
||||||
|
var pokemon = CreateFaintingPokemonAttackedBy(attacker);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
effect.OnFaint(pokemon, DamageSource.MoveDamage);
|
||||||
|
|
||||||
|
// Assert - the attacker is damaged by well over its max HP (10x) so it always faints
|
||||||
|
var damageCall = attacker.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage");
|
||||||
|
await Assert.That(damageCall).IsNotNull();
|
||||||
|
await Assert.That((uint)damageCall!.GetArguments()[0]!).IsEqualTo(1000u);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: Destiny Bond "bypasses protective abilities like Focus Sash and Sturdy". The faint
|
||||||
|
/// damage is dealt with force, so scripts cannot reduce or prevent it.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnFaint_FaintsFromMoveDamage_DamageIsForced()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var effect = new DestinyBondEffect();
|
||||||
|
var attacker = Substitute.For<IPokemon>();
|
||||||
|
attacker.BoostedStats.Returns(new StatisticSet<uint>(100, 1, 1, 1, 1, 1));
|
||||||
|
var pokemon = CreateFaintingPokemonAttackedBy(attacker);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
effect.OnFaint(pokemon, DamageSource.MoveDamage);
|
||||||
|
|
||||||
|
// Assert - the forceDamage argument (last parameter) is true
|
||||||
|
var damageCall = attacker.ReceivedCalls().First(c => c.GetMethodInfo().Name == "Damage");
|
||||||
|
await Assert.That((bool)damageCall.GetArguments()[3]!).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "The move doesn't activate from indirect damage like weather or Leech Seed."
|
||||||
|
/// Fainting from a non-move damage source does not take the attacker down.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnFaint_FaintsFromIndirectDamage_AttackerIsNotDamaged()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var effect = new DestinyBondEffect();
|
||||||
|
var attacker = Substitute.For<IPokemon>();
|
||||||
|
attacker.BoostedStats.Returns(new StatisticSet<uint>(100, 1, 1, 1, 1, 1));
|
||||||
|
var pokemon = CreateFaintingPokemonAttackedBy(attacker);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
effect.OnFaint(pokemon, DamageSource.Misc);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(attacker.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Damage")).IsFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Until the user moves again" — the Destiny Bound status wears off when the user makes
|
||||||
|
/// its next move, so the effect removes itself before the user's move.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnBeforeMove_UserMovesAgain_RemovesDestinyBondEffect()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var user = Substitute.For<IPokemon>();
|
||||||
|
var userVolatile = new ScriptSet(user);
|
||||||
|
user.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));
|
||||||
|
var effect = new DestinyBondEffect();
|
||||||
|
userVolatile.Add(effect);
|
||||||
|
var move = Substitute.For<IExecutingMove>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
effect.OnBeforeMove(move);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(userVolatile.Contains(ScriptUtils.ResolveName<DestinyBondEffect>())).IsFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Technical test: fainting without battle data (outside of battle) does nothing and does not throw.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnFaint_NoBattleData_DoesNotThrow()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var effect = new DestinyBondEffect();
|
||||||
|
var pokemon = Substitute.For<IPokemon>();
|
||||||
|
pokemon.BattleData.Returns((IPokemonBattleData?)null);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
await Assert.That(() => effect.OnFaint(pokemon, DamageSource.MoveDamage)).ThrowsNothing();
|
||||||
|
}
|
||||||
|
}
|
||||||
290
Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DigTests.cs
Normal file
290
Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DigTests.cs
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
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.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="Dig"/> move script and its <see cref="DigEffect"/>.
|
||||||
|
/// Gen VII Bulbapedia behavior: the user digs underground on the first turn, becoming semi-invulnerable,
|
||||||
|
/// and attacks on the second turn. "Earthquake, Magnitude, and Fissure hit underground targets (dealing
|
||||||
|
/// double damage from Gen II onward)".
|
||||||
|
/// </summary>
|
||||||
|
public class DigTests
|
||||||
|
{
|
||||||
|
private static (Dig script, IExecutingMove move, IPokemon user, ScriptSet userVolatile, IMoveChoice moveChoice)
|
||||||
|
CreateTestSetup()
|
||||||
|
{
|
||||||
|
var script = new Dig();
|
||||||
|
var move = Substitute.For<IExecutingMove>();
|
||||||
|
var user = Substitute.For<IPokemon>();
|
||||||
|
// Use a real script set so the charge volatile added by Dig 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>();
|
||||||
|
battle.EventHook.Returns(new EventHook());
|
||||||
|
var battleData = Substitute.For<IPokemonBattleData>();
|
||||||
|
battleData.Battle.Returns(battle);
|
||||||
|
user.BattleData.Returns(battleData);
|
||||||
|
|
||||||
|
return (script, move, user, userVolatile, moveChoice);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "The user excavates underground, becoming immune to most moves during the initial 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 "a semi-invulnerable state" on the initial turn. The charge turn
|
||||||
|
/// attaches the <see cref="DigEffect"/> volatile script to the user.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task PreventMove_FirstUse_AddsDigEffectToUser()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, _, userVolatile, _) = CreateTestSetup();
|
||||||
|
var prevent = false;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.PreventMove(move, ref prevent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(userVolatile.Contains(ScriptUtils.ResolveName<DigEffect>())).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The charge turn is marked on the <see cref="IMoveChoice"/> so the <see cref="DigEffect"/> knows
|
||||||
|
/// this choice was the charging turn and does not remove itself for it.
|
||||||
|
/// </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("dig_charge"), true);
|
||||||
|
await Assert.That(prevent).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "On the following turn, Dig deals damage." When the user already has the
|
||||||
|
/// <see cref="DigEffect"/> 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 DigEffect(user));
|
||||||
|
var prevent = false;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.PreventMove(move, ref prevent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(prevent).IsFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "On the following turn, Dig deals damage." Once the attack executes, the underground
|
||||||
|
/// state (<see cref="DigEffect"/>) is removed from the user.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnBeforeMove_ChargeCompleted_RemovesDigEffect()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user, userVolatile, _) = CreateTestSetup();
|
||||||
|
userVolatile.Add(new DigEffect(user));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnBeforeMove(move);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(userVolatile.Contains(ScriptUtils.ResolveName<DigEffect>())).IsFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: while underground the user is in "a semi-invulnerable state" and is "immune to most
|
||||||
|
/// moves". The <see cref="DigEffect"/> blocks incoming hits from moves that cannot hit underground
|
||||||
|
/// Pokémon.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task BlockIncomingHit_MoveCannotHitUnderground_BlocksHit()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user, userVolatile, _) = CreateTestSetup();
|
||||||
|
var prevent = false;
|
||||||
|
script.PreventMove(move, ref prevent);
|
||||||
|
await Assert.That(userVolatile.TryGet<DigEffect>(out var effect)).IsTrue();
|
||||||
|
|
||||||
|
var incomingMove = Substitute.For<IExecutingMove>();
|
||||||
|
var incomingMoveData = Substitute.For<IMoveData>();
|
||||||
|
incomingMoveData.HasFlag(new StringKey("hit_underground")).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: "Earthquake, Magnitude, and Fissure hit underground targets". Moves flagged as able to
|
||||||
|
/// hit underground Pokémon are not blocked by the <see cref="DigEffect"/>.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task BlockIncomingHit_EarthquakeLikeMove_DoesNotBlockHit()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user, userVolatile, _) = CreateTestSetup();
|
||||||
|
var prevent = false;
|
||||||
|
script.PreventMove(move, ref prevent);
|
||||||
|
await Assert.That(userVolatile.TryGet<DigEffect>(out var effect)).IsTrue();
|
||||||
|
|
||||||
|
var incomingMove = Substitute.For<IExecutingMove>();
|
||||||
|
var incomingMoveData = Substitute.For<IMoveData>();
|
||||||
|
incomingMoveData.HasFlag(new StringKey("hit_underground")).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: "Earthquake, Magnitude, and Fissure hit underground targets (dealing double damage from
|
||||||
|
/// Gen II onward)". A move flagged as effective against underground Pokémon deals doubled damage to the
|
||||||
|
/// underground user.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task ChangeIncomingMoveDamage_EarthquakeLikeMove_DamageIsDoubled()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user, userVolatile, _) = CreateTestSetup();
|
||||||
|
var prevent = false;
|
||||||
|
script.PreventMove(move, ref prevent);
|
||||||
|
await Assert.That(userVolatile.TryGet<DigEffect>(out var effect)).IsTrue();
|
||||||
|
|
||||||
|
var incomingMove = Substitute.For<IExecutingMove>();
|
||||||
|
var incomingMoveData = Substitute.For<IMoveData>();
|
||||||
|
incomingMoveData.HasFlag(new StringKey("hit_underground")).Returns(true);
|
||||||
|
incomingMoveData.HasFlag(new StringKey("effective_against_underground")).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: only Earthquake and Magnitude deal double damage to underground targets. A move without
|
||||||
|
/// the effective-against-underground flag deals regular damage.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task ChangeIncomingMoveDamage_RegularMove_DamageIsUnchanged()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user, userVolatile, _) = CreateTestSetup();
|
||||||
|
var prevent = false;
|
||||||
|
script.PreventMove(move, ref prevent);
|
||||||
|
await Assert.That(userVolatile.TryGet<DigEffect>(out var effect)).IsTrue();
|
||||||
|
|
||||||
|
var incomingMove = Substitute.For<IExecutingMove>();
|
||||||
|
var incomingMoveData = Substitute.For<IMoveData>();
|
||||||
|
incomingMoveData.HasFlag(new StringKey("effective_against_underground")).Returns(false);
|
||||||
|
incomingMove.UseMove.Returns(incomingMoveData);
|
||||||
|
uint damage = 100;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
effect!.ChangeIncomingMoveDamage(incomingMove, user, 0, ref damage);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(damage).IsEqualTo(100u);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "If disrupted, PP isn't consumed and the move doesn't count as the last used" — when
|
||||||
|
/// the user's executed choice was not the Dig charge (i.e. the move was disrupted or replaced), the
|
||||||
|
/// underground state is removed.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnAfterMoveChoice_ChoiceIsNotDigCharge_RemovesDigEffect()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var user = Substitute.For<IPokemon>();
|
||||||
|
var userVolatile = new ScriptSet(user);
|
||||||
|
user.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));
|
||||||
|
var effect = new DigEffect(user);
|
||||||
|
userVolatile.Add(effect);
|
||||||
|
var choice = Substitute.For<IMoveChoice>();
|
||||||
|
choice.AdditionalData.Returns((Dictionary<StringKey, object?>?)null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
effect.OnAfterMoveChoice(choice);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(userVolatile.Contains(ScriptUtils.ResolveName<DigEffect>())).IsFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The charging turn itself carries the "dig_charge" marker; the <see cref="DigEffect"/> must survive
|
||||||
|
/// that turn so the user stays underground until the attack turn.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnAfterMoveChoice_ChoiceIsDigCharge_KeepsDigEffect()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var user = Substitute.For<IPokemon>();
|
||||||
|
var userVolatile = new ScriptSet(user);
|
||||||
|
user.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));
|
||||||
|
var effect = new DigEffect(user);
|
||||||
|
userVolatile.Add(effect);
|
||||||
|
var choice = Substitute.For<IMoveChoice>();
|
||||||
|
choice.AdditionalData.Returns(new Dictionary<StringKey, object?> { { "dig_charge", true } });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
effect.OnAfterMoveChoice(choice);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(userVolatile.Contains(ScriptUtils.ResolveName<DigEffect>())).IsTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
247
Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DisableTests.cs
Normal file
247
Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DisableTests.cs
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
using PkmnLib.Dynamic.Libraries;
|
||||||
|
using PkmnLib.Dynamic.Models;
|
||||||
|
using PkmnLib.Dynamic.Models.Choices;
|
||||||
|
using PkmnLib.Dynamic.ScriptHandling;
|
||||||
|
using PkmnLib.Dynamic.ScriptHandling.Registry;
|
||||||
|
using PkmnLib.Plugin.Gen7.Scripts.Moves;
|
||||||
|
using PkmnLib.Plugin.Gen7.Scripts.Pokemon;
|
||||||
|
using PkmnLib.Static.Moves;
|
||||||
|
using PkmnLib.Static.Utils;
|
||||||
|
|
||||||
|
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests for the <see cref="Disable"/> move script and its <see cref="DisableEffect"/>.
|
||||||
|
/// Gen VII Bulbapedia behavior: "Disable temporarily prevents the target from using a specific move";
|
||||||
|
/// "Disable now disables the last move that the target used", failing "if the target hasn't used a move
|
||||||
|
/// yet", and "the effect lasts [...] 4 turns from Generation V onward."
|
||||||
|
/// </summary>
|
||||||
|
public class DisableTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a mocked move choice whose chosen move has the given name.
|
||||||
|
/// </summary>
|
||||||
|
private static IMoveChoice CreateMoveChoice(string moveName)
|
||||||
|
{
|
||||||
|
var moveData = Substitute.For<IMoveData>();
|
||||||
|
moveData.Name.Returns(new StringKey(moveName));
|
||||||
|
var learnedMove = Substitute.For<ILearnedMove>();
|
||||||
|
learnedMove.MoveData.Returns(moveData);
|
||||||
|
var choice = Substitute.For<IMoveChoice>();
|
||||||
|
choice.ChosenMove.Returns(learnedMove);
|
||||||
|
return choice;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a fully mocked test setup where the target's volatile scripts are a real
|
||||||
|
/// <see cref="ScriptSet"/> and its last used move is the given one (or none).
|
||||||
|
/// </summary>
|
||||||
|
private static (Disable script, IExecutingMove move, IPokemon target, ScriptSet targetVolatile, IHitData hitData)
|
||||||
|
CreateTestSetup(string? lastUsedMove, bool lastMoveIsStruggle = false)
|
||||||
|
{
|
||||||
|
var script = new Disable();
|
||||||
|
var move = Substitute.For<IExecutingMove>();
|
||||||
|
|
||||||
|
var user = Substitute.For<IPokemon>();
|
||||||
|
var userBattleData = Substitute.For<IPokemonBattleData>();
|
||||||
|
user.BattleData.Returns(userBattleData);
|
||||||
|
move.User.Returns(user);
|
||||||
|
|
||||||
|
var target = Substitute.For<IPokemon>();
|
||||||
|
var targetVolatile = new ScriptSet(target);
|
||||||
|
target.Volatile.Returns(targetVolatile);
|
||||||
|
target.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));
|
||||||
|
var targetBattleData = Substitute.For<IPokemonBattleData>();
|
||||||
|
// Create the choice before the Returns call; configuring substitutes inside Returns is not allowed.
|
||||||
|
var lastMoveChoice = lastUsedMove == null ? null : CreateMoveChoice(lastUsedMove);
|
||||||
|
targetBattleData.LastMoveChoice.Returns(lastMoveChoice);
|
||||||
|
target.BattleData.Returns(targetBattleData);
|
||||||
|
|
||||||
|
// Struggle is recognized through the misc library's replacement choice check, not by name.
|
||||||
|
var miscLibrary = Substitute.For<IMiscLibrary>();
|
||||||
|
var library = Substitute.For<IDynamicLibrary>();
|
||||||
|
library.MiscLibrary.Returns(miscLibrary);
|
||||||
|
target.Library.Returns(library);
|
||||||
|
if (lastMoveChoice != null)
|
||||||
|
miscLibrary.IsReplacementChoice(lastMoveChoice).Returns(lastMoveIsStruggle);
|
||||||
|
|
||||||
|
var hitData = Substitute.For<IHitData>();
|
||||||
|
move.GetHitData(target, 0).Returns(hitData);
|
||||||
|
|
||||||
|
return (script, move, target, targetVolatile, hitData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Disable now disables the last move that the target used." The target gains the
|
||||||
|
/// <see cref="DisableEffect"/> volatile script.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_TargetUsedMove_AddsDisableEffectToTarget()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, target, targetVolatile, _) = CreateTestSetup("tackle");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(targetVolatile.Contains(ScriptUtils.ResolveName<DisableEffect>())).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "It fails if the target hasn't used a move yet". Without a last move choice on the
|
||||||
|
/// target, the hit fails and no disable is applied.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_TargetHasNotUsedMove_FailsHit()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, target, targetVolatile, hitData) = CreateTestSetup(null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
hitData.Received(1).Fail();
|
||||||
|
await Assert.That(targetVolatile.Contains(ScriptUtils.ResolveName<DisableEffect>())).IsFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Disable fails if a move is already disabled". Using Disable on a target that already
|
||||||
|
/// has a disabled move must fail the hit rather than silently doing nothing.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_TargetAlreadyDisabled_FailsHit()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, target, targetVolatile, hitData) = CreateTestSetup("tackle");
|
||||||
|
targetVolatile.Add(new DisableEffect(new StringKey("growl")));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
hitData.Received(1).Fail();
|
||||||
|
await Assert.That(hitData.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Fail")).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "It fails if the target hasn't used a move yet or used Struggle". A target whose last
|
||||||
|
/// move was Struggle (a replacement choice per <see cref="IMiscLibrary.IsReplacementChoice"/>) cannot
|
||||||
|
/// have it disabled.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_TargetLastUsedStruggle_FailsHit()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, target, targetVolatile, hitData) = CreateTestSetup("struggle", true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
hitData.Received(1).Fail();
|
||||||
|
await Assert.That(targetVolatile.Contains(ScriptUtils.ResolveName<DisableEffect>())).IsFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Technical test: without battle data on the user (outside of battle) the effect does nothing and
|
||||||
|
/// does not throw.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_UserHasNoBattleData_DoesNothing()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, target, targetVolatile, _) = CreateTestSetup("tackle");
|
||||||
|
move.User.BattleData.Returns((IPokemonBattleData?)null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(targetVolatile.Contains(ScriptUtils.ResolveName<DisableEffect>())).IsFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Disable temporarily prevents the target from using a specific move." Selecting the
|
||||||
|
/// disabled move is prevented by the <see cref="DisableEffect"/>.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task PreventMoveSelection_ChoiceIsDisabledMove_PreventsSelection()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var effect = new DisableEffect(new StringKey("tackle"));
|
||||||
|
var choice = CreateMoveChoice("tackle");
|
||||||
|
var prevent = false;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
effect.PreventMoveSelection(choice, ref prevent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(prevent).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: only the disabled move is prevented; the target's other moves remain selectable.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task PreventMoveSelection_ChoiceIsOtherMove_DoesNotPreventSelection()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var effect = new DisableEffect(new StringKey("tackle"));
|
||||||
|
var choice = CreateMoveChoice("growl");
|
||||||
|
var prevent = false;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
effect.PreventMoveSelection(choice, ref prevent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(prevent).IsFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "The effect lasts [...] 4 turns from Generation V onward." After three end-of-turn
|
||||||
|
/// ticks the effect is still active.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnEndTurn_ThreeTurnsPassed_EffectStillActive()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var target = Substitute.For<IPokemon>();
|
||||||
|
var targetVolatile = new ScriptSet(target);
|
||||||
|
target.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));
|
||||||
|
var effect = new DisableEffect(new StringKey("tackle"));
|
||||||
|
targetVolatile.Add(effect);
|
||||||
|
var battle = Substitute.For<IBattle>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
for (var i = 0; i < 3; i++)
|
||||||
|
effect.OnEndTurn(target, battle);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(targetVolatile.Contains(ScriptUtils.ResolveName<DisableEffect>())).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "The effect lasts [...] 4 turns from Generation V onward." After the fourth
|
||||||
|
/// end-of-turn tick the effect removes itself.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnEndTurn_FourTurnsPassed_EffectRemovesItself()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var target = Substitute.For<IPokemon>();
|
||||||
|
var targetVolatile = new ScriptSet(target);
|
||||||
|
target.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));
|
||||||
|
var effect = new DisableEffect(new StringKey("tackle"));
|
||||||
|
targetVolatile.Add(effect);
|
||||||
|
var battle = Substitute.For<IBattle>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
for (var i = 0; i < 4; i++)
|
||||||
|
effect.OnEndTurn(target, battle);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(targetVolatile.Contains(ScriptUtils.ResolveName<DisableEffect>())).IsFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
288
Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DiveTests.cs
Normal file
288
Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DiveTests.cs
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
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.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="Dive"/> move script and its <see cref="DiveEffect"/>.
|
||||||
|
/// Gen VII Bulbapedia behavior: "The user gains the Submerged status on the turn this move is used, then
|
||||||
|
/// attacks on the following turn. While submerged, the user is invulnerable to most moves but can still be
|
||||||
|
/// hit by Surf and Whirlpool, dealing twice the regular damage."
|
||||||
|
/// </summary>
|
||||||
|
public class DiveTests
|
||||||
|
{
|
||||||
|
private static (Dive script, IExecutingMove move, IPokemon user, ScriptSet userVolatile, IMoveChoice moveChoice)
|
||||||
|
CreateTestSetup()
|
||||||
|
{
|
||||||
|
var script = new Dive();
|
||||||
|
var move = Substitute.For<IExecutingMove>();
|
||||||
|
var user = Substitute.For<IPokemon>();
|
||||||
|
// Use a real script set so the charge volatile added by Dive 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>();
|
||||||
|
battle.EventHook.Returns(new EventHook());
|
||||||
|
var battleData = Substitute.For<IPokemonBattleData>();
|
||||||
|
battleData.Battle.Returns(battle);
|
||||||
|
user.BattleData.Returns(battleData);
|
||||||
|
|
||||||
|
return (script, move, user, userVolatile, moveChoice);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "The user gains the Submerged 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 Submerged status on the turn this move is used." The charge turn
|
||||||
|
/// attaches the <see cref="DiveEffect"/> volatile script to the user.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task PreventMove_FirstUse_AddsDiveEffectToUser()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, _, userVolatile, _) = CreateTestSetup();
|
||||||
|
var prevent = false;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.PreventMove(move, ref prevent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(userVolatile.Contains(ScriptUtils.ResolveName<DiveEffect>())).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The charge turn is marked on the <see cref="IMoveChoice"/> so the <see cref="DiveEffect"/> knows
|
||||||
|
/// this choice was the charging turn and does not remove itself for it.
|
||||||
|
/// </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("dive_charge"), true);
|
||||||
|
await Assert.That(prevent).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "then attacks on the following turn." When the user already has the
|
||||||
|
/// <see cref="DiveEffect"/> 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 DiveEffect(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 lands, the Submerged state
|
||||||
|
/// (<see cref="DiveEffect"/>) is removed from the user.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_AttackExecuted_RemovesDiveEffect()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user, userVolatile, _) = CreateTestSetup();
|
||||||
|
userVolatile.Add(new DiveEffect(user));
|
||||||
|
var target = Substitute.For<IPokemon>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(userVolatile.Contains(ScriptUtils.ResolveName<DiveEffect>())).IsFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "While submerged, the user is invulnerable to most moves". The <see cref="DiveEffect"/>
|
||||||
|
/// blocks incoming hits from moves that cannot hit underwater Pokémon.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task BlockIncomingHit_MoveCannotHitUnderwater_BlocksHit()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user, userVolatile, _) = CreateTestSetup();
|
||||||
|
var prevent = false;
|
||||||
|
script.PreventMove(move, ref prevent);
|
||||||
|
await Assert.That(userVolatile.TryGet<DiveEffect>(out var effect)).IsTrue();
|
||||||
|
|
||||||
|
var incomingMove = Substitute.For<IExecutingMove>();
|
||||||
|
var incomingMoveData = Substitute.For<IMoveData>();
|
||||||
|
incomingMoveData.HasFlag(new StringKey("hit_underwater")).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 Surf and Whirlpool". Moves flagged as able to hit underwater
|
||||||
|
/// Pokémon are not blocked by the <see cref="DiveEffect"/>.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task BlockIncomingHit_SurfLikeMove_DoesNotBlockHit()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user, userVolatile, _) = CreateTestSetup();
|
||||||
|
var prevent = false;
|
||||||
|
script.PreventMove(move, ref prevent);
|
||||||
|
await Assert.That(userVolatile.TryGet<DiveEffect>(out var effect)).IsTrue();
|
||||||
|
|
||||||
|
var incomingMove = Substitute.For<IExecutingMove>();
|
||||||
|
var incomingMoveData = Substitute.For<IMoveData>();
|
||||||
|
incomingMoveData.HasFlag(new StringKey("hit_underwater")).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: "can still be hit by Surf and Whirlpool, dealing twice the regular damage." A move
|
||||||
|
/// flagged as effective against underwater Pokémon deals doubled damage to the submerged user.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task ChangeIncomingMoveDamage_SurfLikeMove_DamageIsDoubled()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user, userVolatile, _) = CreateTestSetup();
|
||||||
|
var prevent = false;
|
||||||
|
script.PreventMove(move, ref prevent);
|
||||||
|
await Assert.That(userVolatile.TryGet<DiveEffect>(out var effect)).IsTrue();
|
||||||
|
|
||||||
|
var incomingMove = Substitute.For<IExecutingMove>();
|
||||||
|
var incomingMoveData = Substitute.For<IMoveData>();
|
||||||
|
incomingMoveData.HasFlag(new StringKey("hit_underwater")).Returns(true);
|
||||||
|
incomingMoveData.HasFlag(new StringKey("effective_against_underwater")).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: only Surf and Whirlpool deal "twice the regular damage" to the submerged user. A move
|
||||||
|
/// without the effective-against-underwater flag deals regular damage.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task ChangeIncomingMoveDamage_RegularMove_DamageIsUnchanged()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user, userVolatile, _) = CreateTestSetup();
|
||||||
|
var prevent = false;
|
||||||
|
script.PreventMove(move, ref prevent);
|
||||||
|
await Assert.That(userVolatile.TryGet<DiveEffect>(out var effect)).IsTrue();
|
||||||
|
|
||||||
|
var incomingMove = Substitute.For<IExecutingMove>();
|
||||||
|
var incomingMoveData = Substitute.For<IMoveData>();
|
||||||
|
incomingMoveData.HasFlag(new StringKey("effective_against_underwater")).Returns(false);
|
||||||
|
incomingMove.UseMove.Returns(incomingMoveData);
|
||||||
|
uint damage = 100;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
effect!.ChangeIncomingMoveDamage(incomingMove, user, 0, ref damage);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(damage).IsEqualTo(100u);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When the user's executed choice was not the Dive charge (i.e. the move was disrupted or replaced),
|
||||||
|
/// the Submerged state is removed.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnAfterMoveChoice_ChoiceIsNotDiveCharge_RemovesDiveEffect()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var user = Substitute.For<IPokemon>();
|
||||||
|
var userVolatile = new ScriptSet(user);
|
||||||
|
user.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));
|
||||||
|
var effect = new DiveEffect(user);
|
||||||
|
userVolatile.Add(effect);
|
||||||
|
var choice = Substitute.For<IMoveChoice>();
|
||||||
|
choice.AdditionalData.Returns((Dictionary<StringKey, object?>?)null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
effect.OnAfterMoveChoice(choice);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(userVolatile.Contains(ScriptUtils.ResolveName<DiveEffect>())).IsFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The charging turn itself carries the "dive_charge" marker; the <see cref="DiveEffect"/> must
|
||||||
|
/// survive that turn so the user stays submerged until the attack turn.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnAfterMoveChoice_ChoiceIsDiveCharge_KeepsDiveEffect()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var user = Substitute.For<IPokemon>();
|
||||||
|
var userVolatile = new ScriptSet(user);
|
||||||
|
user.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));
|
||||||
|
var effect = new DiveEffect(user);
|
||||||
|
userVolatile.Add(effect);
|
||||||
|
var choice = Substitute.For<IMoveChoice>();
|
||||||
|
choice.AdditionalData.Returns(new Dictionary<StringKey, object?> { { "dive_charge", true } });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
effect.OnAfterMoveChoice(choice);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(userVolatile.Contains(ScriptUtils.ResolveName<DiveEffect>())).IsTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
using PkmnLib.Dynamic.Models;
|
||||||
|
using PkmnLib.Dynamic.ScriptHandling;
|
||||||
|
using PkmnLib.Dynamic.ScriptHandling.Registry;
|
||||||
|
using PkmnLib.Plugin.Gen7.Scripts.Moves;
|
||||||
|
using PkmnLib.Plugin.Gen7.Scripts.Side;
|
||||||
|
|
||||||
|
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests for the <see cref="DoomDesire"/> move script and its <see cref="DoomDesireEffect"/>.
|
||||||
|
/// Gen VII Bulbapedia behavior: "On the turn Doom Desire is selected, this attack will do nothing other
|
||||||
|
/// than state that the user has chosen Doom Desire as its destiny. Two turns later, Doom Desire will do
|
||||||
|
/// damage against the target."
|
||||||
|
/// </summary>
|
||||||
|
public class DoomDesireTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a fully mocked test setup where the target is at the given position on a side whose
|
||||||
|
/// volatile scripts are a real <see cref="ScriptSet"/>.
|
||||||
|
/// </summary>
|
||||||
|
private static (DoomDesire script, IExecutingMove move, IPokemon target, IBattleSide side, ScriptSet sideScripts,
|
||||||
|
IHitData hitData) CreateTestSetup(byte position = 0, uint damage = 100)
|
||||||
|
{
|
||||||
|
var script = new DoomDesire();
|
||||||
|
var move = Substitute.For<IExecutingMove>();
|
||||||
|
|
||||||
|
var side = Substitute.For<IBattleSide>();
|
||||||
|
var sideScripts = new ScriptSet(side);
|
||||||
|
side.VolatileScripts.Returns(sideScripts);
|
||||||
|
side.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));
|
||||||
|
var battle = Substitute.For<IBattle>();
|
||||||
|
battle.Sides.Returns(new[] { side });
|
||||||
|
|
||||||
|
var target = Substitute.For<IPokemon>();
|
||||||
|
var battleData = Substitute.For<IPokemonBattleData>();
|
||||||
|
battleData.Battle.Returns(battle);
|
||||||
|
battleData.SideIndex.Returns((byte)0);
|
||||||
|
battleData.Position.Returns(position);
|
||||||
|
target.BattleData.Returns(battleData);
|
||||||
|
side.Pokemon.Returns(new IPokemon?[] { target });
|
||||||
|
|
||||||
|
var hitData = Substitute.For<IHitData>();
|
||||||
|
hitData.Damage.Returns(damage);
|
||||||
|
move.GetHitData(target, 0).Returns(hitData);
|
||||||
|
|
||||||
|
return (script, move, target, side, sideScripts, hitData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "On the turn Doom Desire is selected, this attack will do nothing" — the immediate hit
|
||||||
|
/// is blocked, and the pending damage is stored as a <see cref="DoomDesireEffect"/> on the target's
|
||||||
|
/// side.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task BlockOutgoingHit_FirstUse_BlocksHitAndAddsSideEffect()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, target, _, sideScripts, _) = CreateTestSetup();
|
||||||
|
var block = false;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.BlockOutgoingHit(move, target, 0, ref block);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(block).IsTrue();
|
||||||
|
await Assert.That(sideScripts.Contains(ScriptUtils.ResolveName<DoomDesireEffect>())).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The pending hit is registered against the target's position, so the effect knows where the damage
|
||||||
|
/// must land two turns later.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task BlockOutgoingHit_FirstUse_RegistersTargetPosition()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, target, _, sideScripts, _) = CreateTestSetup();
|
||||||
|
var block = false;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.BlockOutgoingHit(move, target, 0, ref block);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(sideScripts.TryGet<DoomDesireEffect>(out var effect)).IsTrue();
|
||||||
|
await Assert.That(effect!.HasTarget(0)).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: Doom Desire fails when it is already in effect against the target. The second use
|
||||||
|
/// fails the hit instead of queueing a second strike.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task BlockOutgoingHit_AlreadyPendingForTarget_FailsHit()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, target, _, _, hitData) = CreateTestSetup();
|
||||||
|
var block = false;
|
||||||
|
script.BlockOutgoingHit(move, target, 0, ref block);
|
||||||
|
block = false;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.BlockOutgoingHit(move, target, 0, ref block);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
hitData.Received(1).Fail();
|
||||||
|
await Assert.That(block).IsFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Two turns later, Doom Desire will do damage against the target." One end-of-turn tick
|
||||||
|
/// after selection (the selection turn itself) is not enough for the strike to land.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnEndTurn_TwoTurnTicks_DoesNotDamageTargetYet()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, target, side, sideScripts, _) = CreateTestSetup();
|
||||||
|
var block = false;
|
||||||
|
script.BlockOutgoingHit(move, target, 0, ref block);
|
||||||
|
await Assert.That(sideScripts.TryGet<DoomDesireEffect>(out var effect)).IsTrue();
|
||||||
|
var battle = Substitute.For<IBattle>();
|
||||||
|
|
||||||
|
// Act - end of the selection turn and of the first turn after it
|
||||||
|
effect!.OnEndTurn(side, battle);
|
||||||
|
effect.OnEndTurn(side, battle);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(target.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Damage")).IsFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Two turns later, Doom Desire will do damage against the target." At the end of the
|
||||||
|
/// second turn after selection the stored damage is dealt to the Pokémon at the registered position.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnEndTurn_ThreeTurnTicks_DamagesTargetWithStoredDamage()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, target, side, sideScripts, _) = CreateTestSetup(damage: 123);
|
||||||
|
var block = false;
|
||||||
|
script.BlockOutgoingHit(move, target, 0, ref block);
|
||||||
|
await Assert.That(sideScripts.TryGet<DoomDesireEffect>(out var effect)).IsTrue();
|
||||||
|
var battle = Substitute.For<IBattle>();
|
||||||
|
|
||||||
|
// Act - selection turn plus the two turns after it
|
||||||
|
effect!.OnEndTurn(side, battle);
|
||||||
|
effect.OnEndTurn(side, battle);
|
||||||
|
effect.OnEndTurn(side, battle);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var damageCall = target.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage");
|
||||||
|
await Assert.That(damageCall).IsNotNull();
|
||||||
|
await Assert.That((uint)damageCall!.GetArguments()[0]!).IsEqualTo(123u);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Once the pending strike has landed, the effect has no remaining targets and removes itself from
|
||||||
|
/// the side.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnEndTurn_StrikeLanded_EffectRemovesItself()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, target, side, sideScripts, _) = CreateTestSetup();
|
||||||
|
var block = false;
|
||||||
|
script.BlockOutgoingHit(move, target, 0, ref block);
|
||||||
|
await Assert.That(sideScripts.TryGet<DoomDesireEffect>(out var effect)).IsTrue();
|
||||||
|
var battle = Substitute.For<IBattle>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
effect!.OnEndTurn(side, battle);
|
||||||
|
effect.OnEndTurn(side, battle);
|
||||||
|
effect.OnEndTurn(side, battle);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(sideScripts.Contains(ScriptUtils.ResolveName<DoomDesireEffect>())).IsFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Technical test: without battle data on the target (outside of battle) the script does nothing and
|
||||||
|
/// does not throw.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task BlockOutgoingHit_TargetHasNoBattleData_DoesNotBlock()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var script = new DoomDesire();
|
||||||
|
var move = Substitute.For<IExecutingMove>();
|
||||||
|
var target = Substitute.For<IPokemon>();
|
||||||
|
target.BattleData.Returns((IPokemonBattleData?)null);
|
||||||
|
var block = false;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.BlockOutgoingHit(move, target, 0, ref block);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(block).IsFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using PkmnLib.Dynamic.Models.Choices;
|
||||||
|
using PkmnLib.Plugin.Gen7.Scripts.Moves;
|
||||||
|
|
||||||
|
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests for the <see cref="DoubleHitMove"/> script, which implements Double Hit (and the other
|
||||||
|
/// fixed two-hit moves such as Double Kick, Dual Chop, Bonemerang and Gear Grind).
|
||||||
|
/// Behavior is verified against the Bulbapedia page for Double Hit.
|
||||||
|
/// </summary>
|
||||||
|
public class DoubleHitMoveTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Double Hit inflicts damage, hitting twice per use." The number of hits is always set
|
||||||
|
/// to two, regardless of the default.
|
||||||
|
/// </summary>
|
||||||
|
[Test, Arguments((byte)1), Arguments((byte)2), Arguments((byte)5)]
|
||||||
|
public async Task ChangeNumberOfHits_AnyInitialValue_SetsNumberOfHitsToTwo(byte initialHits)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var script = new DoubleHitMove();
|
||||||
|
var choice = Substitute.For<IMoveChoice>();
|
||||||
|
var numberOfHits = initialHits;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.ChangeNumberOfHits(choice, ref numberOfHits);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(numberOfHits).IsEqualTo((byte)2);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
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.Side;
|
||||||
|
|
||||||
|
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests for the <see cref="DoublePowerIfTargetDamagedInTurn"/> script, which implements Assurance (and
|
||||||
|
/// the other moves whose power doubles when the target was already damaged this turn, such as Avalanche
|
||||||
|
/// and Revenge). Behavior is verified against the Bulbapedia page for Assurance:
|
||||||
|
/// "Assurance inflicts damage and will deal double damage if the target has already received damage on
|
||||||
|
/// the turn it is used."
|
||||||
|
/// </summary>
|
||||||
|
public class DoublePowerIfTargetDamagedInTurnTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a fully mocked test setup where the target sits on side 0 whose volatile scripts are a
|
||||||
|
/// real <see cref="ScriptSet"/>.
|
||||||
|
/// </summary>
|
||||||
|
private static (DoublePowerIfTargetDamagedInTurn script, IExecutingMove move, IPokemon target, ScriptSet sideScripts
|
||||||
|
) CreateTestSetup()
|
||||||
|
{
|
||||||
|
var script = new DoublePowerIfTargetDamagedInTurn();
|
||||||
|
var move = Substitute.For<IExecutingMove>();
|
||||||
|
|
||||||
|
var side = Substitute.For<IBattleSide>();
|
||||||
|
var sideScripts = new ScriptSet(side);
|
||||||
|
side.VolatileScripts.Returns(sideScripts);
|
||||||
|
side.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));
|
||||||
|
var battle = Substitute.For<IBattle>();
|
||||||
|
battle.Sides.Returns(new[] { side });
|
||||||
|
|
||||||
|
var user = Substitute.For<IPokemon>();
|
||||||
|
var userBattleData = Substitute.For<IPokemonBattleData>();
|
||||||
|
userBattleData.Battle.Returns(battle);
|
||||||
|
user.BattleData.Returns(userBattleData);
|
||||||
|
move.User.Returns(user);
|
||||||
|
|
||||||
|
var target = Substitute.For<IPokemon>();
|
||||||
|
var targetBattleData = Substitute.For<IPokemonBattleData>();
|
||||||
|
targetBattleData.Battle.Returns(battle);
|
||||||
|
targetBattleData.SideIndex.Returns((byte)0);
|
||||||
|
target.BattleData.Returns(targetBattleData);
|
||||||
|
|
||||||
|
return (script, move, target, sideScripts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "will deal double damage if the target has already received damage on the turn it is
|
||||||
|
/// used." Before the turn starts, a tracking script is placed on the targeted side to record which
|
||||||
|
/// Pokémon take damage this turn.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnBeforeTurnStart_MoveChoice_AddsTrackingDataToTargetSide()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, _, sideScripts) = CreateTestSetup();
|
||||||
|
var user = move.User;
|
||||||
|
var choice = Substitute.For<IMoveChoice>();
|
||||||
|
choice.User.Returns(user);
|
||||||
|
choice.TargetSide.Returns((byte)0);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnBeforeTurnStart(choice);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(sideScripts.Contains(ScriptUtils.ResolveName<DoublePowerIfTargetDamagedInTurnData>()))
|
||||||
|
.IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Technical test: a non-move turn choice (e.g. a switch) does not place the tracking script.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnBeforeTurnStart_NonMoveChoice_DoesNotAddTrackingData()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, _, _, sideScripts) = CreateTestSetup();
|
||||||
|
var choice = Substitute.For<ITurnChoice>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnBeforeTurnStart(choice);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(sideScripts.Contains(ScriptUtils.ResolveName<DoublePowerIfTargetDamagedInTurnData>()))
|
||||||
|
.IsFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "will deal double damage if the target has already received damage on the turn it is
|
||||||
|
/// used." When the tracking data recorded the target as hit this turn, the base power doubles.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task ChangeBasePower_TargetDamagedThisTurn_BasePowerDoubles()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, target, sideScripts) = CreateTestSetup();
|
||||||
|
var data = new DoublePowerIfTargetDamagedInTurnData();
|
||||||
|
sideScripts.Add(data);
|
||||||
|
data.OnDamage(target, DamageSource.MoveDamage, 100, 80);
|
||||||
|
ushort basePower = 60;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.ChangeBasePower(move, target, 0, ref basePower);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(basePower).IsEqualTo((ushort)120);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: the power only doubles "if the target has already received damage on the turn it is
|
||||||
|
/// used." An undamaged target takes regular base power.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task ChangeBasePower_TargetNotDamagedThisTurn_BasePowerUnchanged()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, target, sideScripts) = CreateTestSetup();
|
||||||
|
sideScripts.Add(new DoublePowerIfTargetDamagedInTurnData());
|
||||||
|
ushort basePower = 60;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.ChangeBasePower(move, target, 0, ref basePower);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(basePower).IsEqualTo((ushort)60);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: only damage to the target itself counts. Damage recorded for a different Pokémon on
|
||||||
|
/// the same side does not double the power against this target.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task ChangeBasePower_OtherPokemonDamagedThisTurn_BasePowerUnchanged()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, target, sideScripts) = CreateTestSetup();
|
||||||
|
var data = new DoublePowerIfTargetDamagedInTurnData();
|
||||||
|
sideScripts.Add(data);
|
||||||
|
data.OnDamage(Substitute.For<IPokemon>(), DamageSource.MoveDamage, 100, 80);
|
||||||
|
ushort basePower = 60;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.ChangeBasePower(move, target, 0, ref basePower);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(basePower).IsEqualTo((ushort)60);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "This includes self-inflicted and indirect damage, such as damage from a Life Orb,
|
||||||
|
/// recoil, crash damage, or hurting itself in confusion." Any damage source recorded by the tracking
|
||||||
|
/// data counts.
|
||||||
|
/// </summary>
|
||||||
|
[Test, Arguments(DamageSource.Misc), Arguments(DamageSource.Confusion)]
|
||||||
|
public async Task ChangeBasePower_TargetTookIndirectDamage_BasePowerDoubles(DamageSource source)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, target, sideScripts) = CreateTestSetup();
|
||||||
|
var data = new DoublePowerIfTargetDamagedInTurnData();
|
||||||
|
sideScripts.Add(data);
|
||||||
|
data.OnDamage(target, source, 100, 90);
|
||||||
|
ushort basePower = 60;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.ChangeBasePower(move, target, 0, ref basePower);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(basePower).IsEqualTo((ushort)120);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The damage tracking only spans a single turn: at the end of the turn the tracking data removes
|
||||||
|
/// itself, so damage from a previous turn cannot double the power.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnEndTurn_TurnEnds_TrackingDataRemovesItself()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (_, _, _, sideScripts) = CreateTestSetup();
|
||||||
|
var data = new DoublePowerIfTargetDamagedInTurnData();
|
||||||
|
sideScripts.Add(data);
|
||||||
|
var battle = Substitute.For<IBattle>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
data.OnEndTurn(Substitute.For<IBattleSide>(), battle);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(sideScripts.Contains(ScriptUtils.ResolveName<DoublePowerIfTargetDamagedInTurnData>()))
|
||||||
|
.IsFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Technical test: without battle data on the user (outside of battle) the base power is unchanged
|
||||||
|
/// and nothing throws.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task ChangeBasePower_UserHasNoBattleData_BasePowerUnchanged()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var script = new DoublePowerIfTargetDamagedInTurn();
|
||||||
|
var move = Substitute.For<IExecutingMove>();
|
||||||
|
var user = Substitute.For<IPokemon>();
|
||||||
|
user.BattleData.Returns((IPokemonBattleData?)null);
|
||||||
|
move.User.Returns(user);
|
||||||
|
var target = Substitute.For<IPokemon>();
|
||||||
|
ushort basePower = 60;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.ChangeBasePower(move, target, 0, ref basePower);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(basePower).IsEqualTo((ushort)60);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
using PkmnLib.Dynamic.Events;
|
||||||
|
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="DragonAscent"/> move script.
|
||||||
|
/// Gen VII Bulbapedia behavior: "Dragon Ascent inflicts damage and then lowers the user's Defense stat
|
||||||
|
/// and Special Defense stat by one stage each."
|
||||||
|
/// </summary>
|
||||||
|
public class DragonAscentTests
|
||||||
|
{
|
||||||
|
private static (DragonAscent script, IExecutingMove move, IPokemon user) CreateTestSetup()
|
||||||
|
{
|
||||||
|
var script = new DragonAscent();
|
||||||
|
var move = Substitute.For<IExecutingMove>();
|
||||||
|
var user = Substitute.For<IPokemon>();
|
||||||
|
move.User.Returns(user);
|
||||||
|
return (script, move, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper checking whether the given Pokémon received a ChangeStatBoost call for the given stat with
|
||||||
|
/// a one-stage self-inflicted, non-forced drop.
|
||||||
|
/// </summary>
|
||||||
|
private static bool ReceivedSelfInflictedDrop(IPokemon pokemon, Statistic stat) =>
|
||||||
|
pokemon.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ChangeStatBoost" &&
|
||||||
|
(Statistic)c.GetArguments()[0]! == stat && (sbyte)c.GetArguments()[1]! == -1 &&
|
||||||
|
(bool)c.GetArguments()[2]! && !(bool)c.GetArguments()[3]!);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "lowers the user's Defense stat [...] by one stage". The stat change is self-inflicted
|
||||||
|
/// and not forced.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_MoveHits_LowersUserDefenseByOneStage()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user) = CreateTestSetup();
|
||||||
|
var target = Substitute.For<IPokemon>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(ReceivedSelfInflictedDrop(user, Statistic.Defense)).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "lowers the user's [...] Special Defense stat by one stage". The stat change is
|
||||||
|
/// self-inflicted and not forced.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_MoveHits_LowersUserSpecialDefenseByOneStage()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user) = CreateTestSetup();
|
||||||
|
var target = Substitute.For<IPokemon>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(ReceivedSelfInflictedDrop(user, Statistic.SpecialDefense)).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: the drop hits the user's stats, not the target's. The target's stat stages are left
|
||||||
|
/// untouched.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_MoveHits_DoesNotChangeTargetStats()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, _) = CreateTestSetup();
|
||||||
|
var target = Substitute.For<IPokemon>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(target.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ChangeStatBoost")).IsFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Both stat drops are reported under the same event batch, so they display as a single combined
|
||||||
|
/// event.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_MoveHits_BothStatDropsShareEventBatch()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, user) = CreateTestSetup();
|
||||||
|
var target = Substitute.For<IPokemon>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var batchIds = user.ReceivedCalls().Where(c => c.GetMethodInfo().Name == "ChangeStatBoost")
|
||||||
|
.Select(c => (EventBatchId)c.GetArguments()[4]!).Distinct().ToList();
|
||||||
|
await Assert.That(batchIds.Count).IsEqualTo(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
using PkmnLib.Dynamic.Models;
|
||||||
|
using PkmnLib.Dynamic.ScriptHandling;
|
||||||
|
using PkmnLib.Plugin.Gen7.Scripts.Abilities;
|
||||||
|
using PkmnLib.Plugin.Gen7.Scripts.Moves;
|
||||||
|
using PkmnLib.Static.Species;
|
||||||
|
using PkmnLib.Static.Utils;
|
||||||
|
|
||||||
|
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests for the <see cref="DreamEater"/> move script.
|
||||||
|
/// Gen VII Bulbapedia behavior: "Dream Eater only works if the target is asleep; otherwise, it does
|
||||||
|
/// nothing", and "50% of the damage dealt to the target is restored to the user as HP".
|
||||||
|
/// </summary>
|
||||||
|
public class DreamEaterTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a fully mocked test setup for Dream Eater tests.
|
||||||
|
/// </summary>
|
||||||
|
private static (DreamEater script, IExecutingMove move, IPokemon target, IPokemon user) CreateTestSetup(uint damage,
|
||||||
|
bool holdsBigRoot = false, Script[]? targetScripts = null)
|
||||||
|
{
|
||||||
|
var script = new DreamEater();
|
||||||
|
var move = Substitute.For<IExecutingMove>();
|
||||||
|
var target = Substitute.For<IPokemon>();
|
||||||
|
var hitData = Substitute.For<IHitData>();
|
||||||
|
hitData.Damage.Returns(damage);
|
||||||
|
move.GetHitData(target, 0).Returns(hitData);
|
||||||
|
|
||||||
|
// RunScriptHook iterates the target's scripts; give the mock a real iterator so the
|
||||||
|
// hook pass runs (empty unless the test attaches scripts such as Liquid Ooze).
|
||||||
|
var containers = (targetScripts ?? []).Select(IEnumerable<ScriptContainer> (s) => new ScriptContainer(s))
|
||||||
|
.ToArray();
|
||||||
|
target.GetScripts().Returns(_ => new ScriptIterator(containers));
|
||||||
|
|
||||||
|
var user = Substitute.For<IPokemon>();
|
||||||
|
if (holdsBigRoot)
|
||||||
|
user.HasHeldItem("big_root").Returns(true);
|
||||||
|
move.User.Returns(user);
|
||||||
|
|
||||||
|
return (script, move, target, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper to extract the heal amount from the user's received Heal calls.
|
||||||
|
/// </summary>
|
||||||
|
private static uint? GetHealAmount(IPokemon user)
|
||||||
|
{
|
||||||
|
var call = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Heal");
|
||||||
|
return call != null ? (uint)call.GetArguments()[0]! : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Dream Eater only works if the target is asleep; otherwise, it does nothing."
|
||||||
|
/// The outgoing hit is blocked when the target does not have the sleep status.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task BlockOutgoingHit_TargetNotAsleep_BlocksHit()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, target, _) = CreateTestSetup(100);
|
||||||
|
target.HasStatus(new StringKey("asleep")).Returns(false);
|
||||||
|
var block = false;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.BlockOutgoingHit(move, target, 0, ref block);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(block).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Dream Eater only works if the target is asleep". A sleeping target can be hit.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task BlockOutgoingHit_TargetAsleep_DoesNotBlockHit()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, target, _) = CreateTestSetup(100);
|
||||||
|
target.HasStatus(new StringKey("asleep")).Returns(true);
|
||||||
|
var block = false;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.BlockOutgoingHit(move, target, 0, ref block);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(block).IsFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "Dream Eater also affects targets with the Comatose Ability, which act as though they
|
||||||
|
/// are asleep without the status condition."
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task BlockOutgoingHit_TargetHasComatose_DoesNotBlockHit()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, target, _) = CreateTestSetup(100);
|
||||||
|
target.HasStatus(new StringKey("asleep")).Returns(false);
|
||||||
|
var comatose = Substitute.For<IAbility>();
|
||||||
|
comatose.Name.Returns(new StringKey("comatose"));
|
||||||
|
target.ActiveAbility.Returns(comatose);
|
||||||
|
var block = false;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.BlockOutgoingHit(move, target, 0, ref block);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(block).IsFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "50% of the damage dealt to the target is restored to the user as HP".
|
||||||
|
/// </summary>
|
||||||
|
[Test, Arguments(100u, 50u), Arguments(200u, 100u), Arguments(51u, 25u), Arguments(99u, 49u)]
|
||||||
|
public async Task OnSecondaryEffect_DamageDealt_UserHealsHalfOfDamage(uint damage, uint expectedHeal)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, target, user) = CreateTestSetup(damage);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(GetHealAmount(user)!.Value).IsEqualTo(expectedHeal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "if the attack deals 1 HP of damage, 1 HP will be restored to the user."
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_DamageOfOne_HealsAtLeastOneHp()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, target, user) = CreateTestSetup(1);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.That(GetHealAmount(user)!.Value).IsEqualTo(1u);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "If the user is holding a Big Root, the HP restored is increased by 30% (making the
|
||||||
|
/// restored HP 65% of the damage dealt)."
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_UserHoldingBigRoot_HealIncreasedByThirtyPercent()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, target, user) = CreateTestSetup(100, true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert - 65% of 100 damage
|
||||||
|
await Assert.That(GetHealAmount(user)!.Value).IsEqualTo(65u);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "When used on a Pokémon with the Liquid Ooze Ability, the user will lose the amount of
|
||||||
|
/// HP it would have gained instead."
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_TargetHasLiquidOoze_UserLosesHpInsteadOfHealing()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, target, user) = CreateTestSetup(100, targetScripts: [new LiquidOoze()]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert - user takes the 50 HP it would have gained, and is not healed
|
||||||
|
var damageCall = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage");
|
||||||
|
await Assert.That(damageCall).IsNotNull();
|
||||||
|
await Assert.That((uint)damageCall!.GetArguments()[0]!).IsEqualTo(50u);
|
||||||
|
await Assert.That(user.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Heal")).IsFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulbapedia: "the user will lose the amount of HP it would have gained instead." The HP loss caused
|
||||||
|
/// by Liquid Ooze is indirect damage, not move damage.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task OnSecondaryEffect_TargetHasLiquidOoze_UsesMiscDamageSource()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (script, move, target, user) = CreateTestSetup(100, targetScripts: [new LiquidOoze()]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
script.OnSecondaryEffect(move, target, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var damageCall = user.ReceivedCalls().First(c => c.GetMethodInfo().Name == "Damage");
|
||||||
|
await Assert.That((DamageSource)damageCall.GetArguments()[1]!).IsEqualTo(DamageSource.Misc);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ public class Defog : Script, IScriptOnSecondaryEffect
|
|||||||
{
|
{
|
||||||
[PublicAPI] public static HashSet<StringKey> DefoggedEffects =
|
[PublicAPI] public static HashSet<StringKey> DefoggedEffects =
|
||||||
[
|
[
|
||||||
|
ScriptUtils.ResolveName<AuroraVeilEffect>(),
|
||||||
ScriptUtils.ResolveName<MistEffect>(),
|
ScriptUtils.ResolveName<MistEffect>(),
|
||||||
ScriptUtils.ResolveName<LightScreenEffect>(),
|
ScriptUtils.ResolveName<LightScreenEffect>(),
|
||||||
ScriptUtils.ResolveName<ReflectEffect>(),
|
ScriptUtils.ResolveName<ReflectEffect>(),
|
||||||
@@ -24,10 +25,14 @@ public class Defog : Script, IScriptOnSecondaryEffect
|
|||||||
if (target.BattleData == null)
|
if (target.BattleData == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var targetSide = target.BattleData.Battle.Sides[target.BattleData.SideIndex];
|
target.ChangeStatBoost(Statistic.Evasion, -1, false, false);
|
||||||
|
|
||||||
|
foreach (var sides in target.BattleData.Battle.Sides)
|
||||||
|
{
|
||||||
foreach (var effect in DefoggedEffects)
|
foreach (var effect in DefoggedEffects)
|
||||||
{
|
{
|
||||||
targetSide.VolatileScripts.Remove(effect);
|
sides.VolatileScripts.Remove(effect);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,12 +11,24 @@ public class Disable : Script, IScriptOnSecondaryEffect
|
|||||||
var battleData = move.User.BattleData;
|
var battleData = move.User.BattleData;
|
||||||
if (battleData == null)
|
if (battleData == null)
|
||||||
return;
|
return;
|
||||||
|
if (target.Volatile.Contains<DisableEffect>())
|
||||||
|
{
|
||||||
|
move.GetHitData(target, hit).Fail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
var lastMove = target.BattleData?.LastMoveChoice;
|
var lastMove = target.BattleData?.LastMoveChoice;
|
||||||
if (lastMove == null)
|
if (lastMove == null)
|
||||||
{
|
{
|
||||||
move.GetHitData(target, hit).Fail();
|
move.GetHitData(target, hit).Fail();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// If the last move was struggle, the move fails and no disable is applied.
|
||||||
|
if (target.Library.MiscLibrary.IsReplacementChoice(lastMove))
|
||||||
|
{
|
||||||
|
move.GetHitData(target, hit).Fail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var lastMoveName = lastMove.ChosenMove.MoveData.Name;
|
var lastMoveName = lastMove.ChosenMove.MoveData.Name;
|
||||||
target.Volatile.Add(new DisableEffect(lastMoveName));
|
target.Volatile.Add(new DisableEffect(lastMoveName));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,13 @@ namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
|
|||||||
[Script(ScriptCategory.Move, "dream_eater")]
|
[Script(ScriptCategory.Move, "dream_eater")]
|
||||||
public class DreamEater : Script, IScriptOnSecondaryEffect, IScriptBlockOutgoingHit
|
public class DreamEater : Script, IScriptOnSecondaryEffect, IScriptBlockOutgoingHit
|
||||||
{
|
{
|
||||||
|
private static readonly StringKey AsleepStatus = new("asleep");
|
||||||
|
private static readonly StringKey ComatoseAbility = new("comatose");
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void BlockOutgoingHit(IExecutingMove executingMove, IPokemon target, byte hitIndex, ref bool block)
|
public void BlockOutgoingHit(IExecutingMove executingMove, IPokemon target, byte hitIndex, ref bool block)
|
||||||
{
|
{
|
||||||
if (!target.HasStatus("asleep"))
|
if (!target.HasStatus(AsleepStatus) && target.ActiveAbility?.Name != ComatoseAbility)
|
||||||
block = true;
|
block = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,6 +26,8 @@ public class DreamEater : Script, IScriptOnSecondaryEffect, IScriptBlockOutgoing
|
|||||||
target.RunScriptHook<IScriptCustomTrigger>(x => x.CustomTrigger(CustomTriggers.ModifyDrain, args));
|
target.RunScriptHook<IScriptCustomTrigger>(x => x.CustomTrigger(CustomTriggers.ModifyDrain, args));
|
||||||
var invert = args.Invert;
|
var invert = args.Invert;
|
||||||
healed = args.Healed;
|
healed = args.Healed;
|
||||||
|
if (healed == 0)
|
||||||
|
healed = 1;
|
||||||
|
|
||||||
if (invert)
|
if (invert)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ public class DigEffect : Script, IScriptForceTurnSelection, IScriptChangeIncomin
|
|||||||
/// <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.EffectiveAgainstUnderground))
|
if (move.UseMove.HasFlag(MoveFlags.EffectiveAgainstUnderground))
|
||||||
damage *= 2;
|
damage *= 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ public class DiveEffect : Script, IScriptForceTurnSelection, IScriptChangeIncomi
|
|||||||
/// <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.EffectiveAgainstUnderwater))
|
if (move.UseMove.HasFlag(MoveFlags.EffectiveAgainstUnderwater))
|
||||||
damage *= 2;
|
damage *= 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user