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 =
|
||||
[
|
||||
ScriptUtils.ResolveName<AuroraVeilEffect>(),
|
||||
ScriptUtils.ResolveName<MistEffect>(),
|
||||
ScriptUtils.ResolveName<LightScreenEffect>(),
|
||||
ScriptUtils.ResolveName<ReflectEffect>(),
|
||||
@@ -24,10 +25,14 @@ public class Defog : Script, IScriptOnSecondaryEffect
|
||||
if (target.BattleData == null)
|
||||
return;
|
||||
|
||||
var targetSide = target.BattleData.Battle.Sides[target.BattleData.SideIndex];
|
||||
foreach (var effect in DefoggedEffects)
|
||||
target.ChangeStatBoost(Statistic.Evasion, -1, false, false);
|
||||
|
||||
foreach (var sides in target.BattleData.Battle.Sides)
|
||||
{
|
||||
targetSide.VolatileScripts.Remove(effect);
|
||||
foreach (var effect in DefoggedEffects)
|
||||
{
|
||||
sides.VolatileScripts.Remove(effect);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,12 +11,24 @@ public class Disable : Script, IScriptOnSecondaryEffect
|
||||
var battleData = move.User.BattleData;
|
||||
if (battleData == null)
|
||||
return;
|
||||
if (target.Volatile.Contains<DisableEffect>())
|
||||
{
|
||||
move.GetHitData(target, hit).Fail();
|
||||
return;
|
||||
}
|
||||
var lastMove = target.BattleData?.LastMoveChoice;
|
||||
if (lastMove == null)
|
||||
{
|
||||
move.GetHitData(target, hit).Fail();
|
||||
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;
|
||||
target.Volatile.Add(new DisableEffect(lastMoveName));
|
||||
}
|
||||
|
||||
@@ -3,10 +3,13 @@ namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
|
||||
[Script(ScriptCategory.Move, "dream_eater")]
|
||||
public class DreamEater : Script, IScriptOnSecondaryEffect, IScriptBlockOutgoingHit
|
||||
{
|
||||
private static readonly StringKey AsleepStatus = new("asleep");
|
||||
private static readonly StringKey ComatoseAbility = new("comatose");
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -23,6 +26,8 @@ public class DreamEater : Script, IScriptOnSecondaryEffect, IScriptBlockOutgoing
|
||||
target.RunScriptHook<IScriptCustomTrigger>(x => x.CustomTrigger(CustomTriggers.ModifyDrain, args));
|
||||
var invert = args.Invert;
|
||||
healed = args.Healed;
|
||||
if (healed == 0)
|
||||
healed = 1;
|
||||
|
||||
if (invert)
|
||||
{
|
||||
|
||||
@@ -30,7 +30,7 @@ public class DigEffect : Script, IScriptForceTurnSelection, IScriptChangeIncomin
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ public class DiveEffect : Script, IScriptForceTurnSelection, IScriptChangeIncomi
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user