More unit tests, more fixes

This commit is contained in:
2026-07-05 19:02:00 +02:00
parent 6a82c20cf2
commit 94ddf63861
16 changed files with 1994 additions and 6 deletions

View File

@@ -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();
}
}

View 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();
}
}

View File

@@ -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();
}
}

View 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();
}
}

View 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();
}
}

View 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();
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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));
}

View File

@@ -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)
{

View File

@@ -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;
}

View File

@@ -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;
}