diff --git a/PkmnLib.Dynamic/Models/Pokemon.cs b/PkmnLib.Dynamic/Models/Pokemon.cs
index ddf8544..fb1013a 100644
--- a/PkmnLib.Dynamic/Models/Pokemon.cs
+++ b/PkmnLib.Dynamic/Models/Pokemon.cs
@@ -296,7 +296,7 @@ public interface IPokemon : IScriptSource, IDeepCloneable
///
/// Suppresses the ability of the Pokémon.
///
- void SuppressAbility();
+ bool SuppressAbility();
///
/// Returns the currently active ability.
@@ -974,10 +974,14 @@ public class PokemonImpl : ScriptSource, IPokemon
public bool AbilitySuppressed { get; private set; }
///
- public void SuppressAbility()
+ public bool SuppressAbility()
{
+ if (ActiveAbility?.CanBeChanged == false)
+ return false;
+
AbilitySuppressed = true;
AbilityScript.Clear();
+ return true;
}
private (IAbility, AbilityIndex)? _abilityCache;
diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CamouflageTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CamouflageTests.cs
new file mode 100644
index 0000000..6d8dd87
--- /dev/null
+++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CamouflageTests.cs
@@ -0,0 +1,118 @@
+using PkmnLib.Dynamic.Models;
+using PkmnLib.Plugin.Gen7.Scripts.Moves;
+using PkmnLib.Static;
+using PkmnLib.Static.Utils;
+
+namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
+
+///
+/// Tests for the move script.
+/// Gen VII Bulbapedia behavior: Camouflage changes the user's type based on the current battle
+/// environment, and the terrain in effect takes precedence (Electric Terrain → Electric,
+/// Misty Terrain → Fairy, Grassy Terrain → Grass, Psychic Terrain → Psychic).
+///
+public class CamouflageTests
+{
+ private static (Camouflage script, IExecutingMove move, IPokemon user, IBattle battle) CreateTestSetup(
+ string? terrainName, string environmentName)
+ {
+ var script = new Camouflage();
+ var library = LibraryHelpers.LoadLibrary();
+
+ var battle = Substitute.For();
+ battle.Library.Returns(library);
+ battle.TerrainName.Returns(terrainName == null ? null : new StringKey?(new StringKey(terrainName)));
+ battle.EnvironmentName.Returns(new StringKey(environmentName));
+
+ var user = Substitute.For();
+ var move = Substitute.For();
+ move.User.Returns(user);
+ move.Battle.Returns(battle);
+
+ return (script, move, user, battle);
+ }
+
+ ///
+ /// Helper that returns the single type the user was changed to, or null when
+ /// was never called.
+ ///
+ private static TypeIdentifier? GetSetType(IPokemon user)
+ {
+ var call = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "SetTypes");
+ return call != null ? ((IReadOnlyList)call.GetArguments()[0]!).Single() : null;
+ }
+
+ ///
+ /// Bulbapedia (Generation VI onwards): terrain effects determine the type Camouflage grants —
+ /// Electric Terrain makes the user Electric-type, Misty Terrain makes it Fairy-type,
+ /// Grassy Terrain makes it Grass-type, and (Generation VII) Psychic Terrain makes it Psychic-type.
+ ///
+ [Test, Arguments("electric_terrain", "electric"), Arguments("misty_terrain", "fairy"),
+ Arguments("grassy_terrain", "grass"), Arguments("psychic_terrain", "psychic")]
+ public async Task OnSecondaryEffect_TerrainActive_UserBecomesTerrainType(string terrainName, string expectedType)
+ {
+ // Arrange
+ var (script, move, user, battle) = CreateTestSetup(terrainName, "field");
+ battle.Library.StaticLibrary.Types.TryGetTypeIdentifier(expectedType, out var expected);
+
+ // Act
+ script.OnSecondaryEffect(move, Substitute.For(), 0);
+
+ // Assert
+ await Assert.That(GetSetType(user)!.Value).IsEqualTo(expected);
+ }
+
+ ///
+ /// Bulbapedia: without a terrain in effect, the battle environment decides the type — caves make
+ /// the user Rock-type, mountains and beaches Ground-type, snow Ice-type, and water Water-type.
+ ///
+ [Test, Arguments("cave", "rock"), Arguments("mountain", "ground"), Arguments("beach", "ground"),
+ Arguments("snow", "ice"), Arguments("sea", "water"), Arguments("lake", "water")]
+ public async Task OnSecondaryEffect_NoTerrain_UserBecomesEnvironmentType(string environment, string expectedType)
+ {
+ // Arrange
+ var (script, move, user, battle) = CreateTestSetup(null, environment);
+ battle.Library.StaticLibrary.Types.TryGetTypeIdentifier(expectedType, out var expected);
+
+ // Act
+ script.OnSecondaryEffect(move, Substitute.For(), 0);
+
+ // Assert
+ await Assert.That(GetSetType(user)!.Value).IsEqualTo(expected);
+ }
+
+ ///
+ /// Bulbapedia: "On regular terrain" Camouflage changes the user's type to Normal.
+ ///
+ [Test, Arguments("field"), Arguments("forest"), Arguments("building")]
+ public async Task OnSecondaryEffect_RegularEnvironment_UserBecomesNormalType(string environment)
+ {
+ // Arrange
+ var (script, move, user, battle) = CreateTestSetup(null, environment);
+ battle.Library.StaticLibrary.Types.TryGetTypeIdentifier("normal", out var expected);
+
+ // Act
+ script.OnSecondaryEffect(move, Substitute.For(), 0);
+
+ // Assert
+ await Assert.That(GetSetType(user)!.Value).IsEqualTo(expected);
+ }
+
+ ///
+ /// Bulbapedia: Camouflage changes the type of the user, not the target — the target must be
+ /// left untouched.
+ ///
+ [Test]
+ public async Task OnSecondaryEffect_TargetTypes_AreNotChanged()
+ {
+ // Arrange
+ var (script, move, _, _) = CreateTestSetup(null, "field");
+ var target = Substitute.For();
+
+ // Act
+ script.OnSecondaryEffect(move, target, 0);
+
+ // Assert
+ await Assert.That(target.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "SetTypes")).IsFalse();
+ }
+}
\ No newline at end of file
diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CaptivateTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CaptivateTests.cs
new file mode 100644
index 0000000..5957dca
--- /dev/null
+++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CaptivateTests.cs
@@ -0,0 +1,133 @@
+using PkmnLib.Dynamic.Models;
+using PkmnLib.Plugin.Gen7.Scripts.Moves;
+using PkmnLib.Static;
+using PkmnLib.Static.Species;
+using PkmnLib.Static.Utils;
+
+namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
+
+///
+/// Tests for the move script.
+/// Gen VII Bulbapedia behavior: "Captivate lowers the Special Attack stat of the target by two stages,
+/// if it is the opposite gender of the user." Gender-unknown Pokémon and Pokémon with the Oblivious
+/// Ability cannot be affected, and the move fails when used by a gender-unknown Pokémon.
+///
+public class CaptivateTests
+{
+ private static (Captivate script, IExecutingMove move, IPokemon user, IPokemon target, IHitData hitData)
+ CreateTestSetup(Gender userGender, Gender targetGender)
+ {
+ var script = new Captivate();
+ var move = Substitute.For();
+ var user = Substitute.For();
+ user.Gender.Returns(userGender);
+ move.User.Returns(user);
+
+ var target = Substitute.For();
+ target.Gender.Returns(targetGender);
+ var hitData = Substitute.For();
+ move.GetHitData(target, 0).Returns(hitData);
+
+ return (script, move, user, target, hitData);
+ }
+
+ ///
+ /// Helper that checks whether a stat boost change was applied to the given Pokémon.
+ ///
+ private static bool ReceivedStatBoost(IPokemon pokemon, Statistic stat, sbyte amount) =>
+ pokemon.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ChangeStatBoost" &&
+ (Statistic)c.GetArguments()[0]! == stat &&
+ (sbyte)c.GetArguments()[1]! == amount);
+
+ ///
+ /// Bulbapedia: "Captivate lowers the Special Attack stat of the target by two stages, if it is the
+ /// opposite gender of the user."
+ ///
+ [Test, Arguments(Gender.Male, Gender.Female), Arguments(Gender.Female, Gender.Male)]
+ public async Task OnSecondaryEffect_OppositeGender_LowersTargetSpecialAttackByTwoStages(Gender userGender,
+ Gender targetGender)
+ {
+ // Arrange
+ var (script, move, _, target, hitData) = CreateTestSetup(userGender, targetGender);
+
+ // Act
+ script.OnSecondaryEffect(move, target, 0);
+
+ // Assert
+ await Assert.That(ReceivedStatBoost(target, Statistic.SpecialAttack, -2)).IsTrue();
+ hitData.DidNotReceive().Fail();
+ }
+
+ ///
+ /// Bulbapedia: Captivate only affects a target "if it is the opposite gender of the user" — against a
+ /// same-gender target the move fails and no stat is lowered.
+ ///
+ [Test, Arguments(Gender.Male), Arguments(Gender.Female)]
+ public async Task OnSecondaryEffect_SameGender_Fails(Gender gender)
+ {
+ // Arrange
+ var (script, move, _, target, hitData) = CreateTestSetup(gender, gender);
+
+ // Act
+ script.OnSecondaryEffect(move, target, 0);
+
+ // Assert
+ hitData.Received(1).Fail();
+ await Assert.That(target.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ChangeStatBoost")).IsFalse();
+ }
+
+ ///
+ /// Bulbapedia: "Pokémon that are gender-unknown [...] cannot be affected."
+ ///
+ [Test]
+ public async Task OnSecondaryEffect_GenderlessTarget_Fails()
+ {
+ // Arrange
+ var (script, move, _, target, hitData) = CreateTestSetup(Gender.Male, Gender.Genderless);
+
+ // Act
+ script.OnSecondaryEffect(move, target, 0);
+
+ // Assert
+ hitData.Received(1).Fail();
+ await Assert.That(target.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ChangeStatBoost")).IsFalse();
+ }
+
+ ///
+ /// Bulbapedia: "those with the Oblivious ability cannot be affected."
+ ///
+ [Test]
+ public async Task OnSecondaryEffect_TargetHasOblivious_Fails()
+ {
+ // Arrange
+ var (script, move, _, target, hitData) = CreateTestSetup(Gender.Male, Gender.Female);
+ var ability = Substitute.For();
+ ability.Name.Returns(new StringKey("oblivious"));
+ target.ActiveAbility.Returns(ability);
+
+ // Act
+ script.OnSecondaryEffect(move, target, 0);
+
+ // Assert
+ hitData.Received(1).Fail();
+ await Assert.That(target.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ChangeStatBoost")).IsFalse();
+ }
+
+ ///
+ /// Bulbapedia: "The move fails automatically if used by a gender-unknown Pokémon." A genderless user
+ /// against a gendered target must fail rather than lower the target's Special Attack.
+ ///
+ [Test]
+ public async Task OnSecondaryEffect_GenderlessUser_Fails()
+ {
+ // Arrange
+ var (script, move, _, target, hitData) = CreateTestSetup(Gender.Genderless, Gender.Female);
+
+ // Act
+ script.OnSecondaryEffect(move, target, 0);
+
+ // Assert
+ hitData.Received(1).Fail();
+ await Assert.That(target.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ChangeStatBoost")).IsFalse();
+ }
+}
\ No newline at end of file
diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/ChargeTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/ChargeTests.cs
new file mode 100644
index 0000000..46549d2
--- /dev/null
+++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/ChargeTests.cs
@@ -0,0 +1,149 @@
+using PkmnLib.Dynamic.Models;
+using PkmnLib.Dynamic.ScriptHandling;
+using PkmnLib.Dynamic.ScriptHandling.Registry;
+using PkmnLib.Plugin.Gen7.Scripts.Moves;
+using PkmnLib.Plugin.Gen7.Scripts.Pokemon;
+using PkmnLib.Static;
+using PkmnLib.Static.Moves;
+
+namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
+
+///
+/// Tests for the move script and the it applies.
+/// Gen VII Bulbapedia behavior: Charge doubles the power of the user's next Electric-type move,
+/// and "It also raises the user's Special Defense stat by one stage."
+///
+public class ChargeTests
+{
+ private static (Charge script, IExecutingMove move, IPokemon user, IScriptSet userVolatile) CreateTestSetup()
+ {
+ var script = new Charge();
+ var move = Substitute.For();
+ var user = Substitute.For();
+ // Use a real script set so the volatile script added by Charge can be inspected afterwards.
+ var userVolatile = new ScriptSet(user);
+ user.Volatile.Returns(userVolatile);
+ user.GetScripts().Returns(_ => new ScriptIterator(new List>()));
+ move.User.Returns(user);
+ return (script, move, user, userVolatile);
+ }
+
+ ///
+ /// Creates an executing move of the given type whose damage modifier can be changed by
+ /// .
+ ///
+ private static (IExecutingMove move, IPokemon target) CreateExecutingMoveOfType(string typeName)
+ {
+ var library = LibraryHelpers.LoadLibrary();
+ var battle = Substitute.For();
+ battle.Library.Returns(library);
+ var battleData = Substitute.For();
+ battleData.Battle.Returns(battle);
+ var target = Substitute.For();
+ target.BattleData.Returns(battleData);
+
+ library.StaticLibrary.Types.TryGetTypeIdentifier(typeName, out var typeIdentifier);
+ var useMove = Substitute.For();
+ useMove.MoveType.Returns(typeIdentifier);
+ var move = Substitute.For();
+ move.UseMove.Returns(useMove);
+
+ return (move, target);
+ }
+
+ ///
+ /// Bulbapedia: "It also raises the user's Special Defense stat by one stage."
+ ///
+ [Test]
+ public async Task OnSecondaryEffect_Used_RaisesUserSpecialDefenseByOneStage()
+ {
+ // Arrange
+ var (script, move, user, _) = CreateTestSetup();
+
+ // Act
+ script.OnSecondaryEffect(move, Substitute.For(), 0);
+
+ // Assert
+ var boost = user.ReceivedCalls().SingleOrDefault(c => c.GetMethodInfo().Name == "ChangeStatBoost");
+ await Assert.That(boost).IsNotNull();
+ await Assert.That((Statistic)boost!.GetArguments()[0]!).IsEqualTo(Statistic.SpecialDefense);
+ await Assert.That((sbyte)boost.GetArguments()[1]!).IsEqualTo((sbyte)1);
+ await Assert.That((bool)boost.GetArguments()[2]!).IsTrue();
+ }
+
+ ///
+ /// Bulbapedia: Charge doubles the power of the user's next Electric-type move. Using Charge attaches
+ /// the volatile script to the user.
+ ///
+ [Test]
+ public async Task OnSecondaryEffect_Used_AddsChargeEffectToUser()
+ {
+ // Arrange
+ var (script, move, _, userVolatile) = CreateTestSetup();
+
+ // Act
+ script.OnSecondaryEffect(move, Substitute.For(), 0);
+
+ // Assert
+ await Assert.That(userVolatile.Contains(ScriptUtils.ResolveName())).IsTrue();
+ }
+
+ ///
+ /// Bulbapedia: "If the user's next move is Electric-type", its power is doubled.
+ ///
+ [Test]
+ public async Task ChargeEffect_ElectricMove_DoublesDamageModifier()
+ {
+ // Arrange
+ var effect = new ChargeEffect();
+ var (move, target) = CreateExecutingMoveOfType("electric");
+ var modifier = 1.0f;
+
+ // Act
+ effect.ChangeDamageModifier(move, target, 0, ref modifier);
+
+ // Assert
+ await Assert.That(modifier).IsEqualTo(2.0f);
+ }
+
+ ///
+ /// Bulbapedia: only Electric-type moves benefit — a non-Electric move is not boosted by Charge.
+ ///
+ [Test]
+ public async Task ChargeEffect_NonElectricMove_DoesNotChangeDamageModifier()
+ {
+ // Arrange
+ var effect = new ChargeEffect();
+ var (move, target) = CreateExecutingMoveOfType("water");
+ var modifier = 1.0f;
+
+ // Act
+ effect.ChangeDamageModifier(move, target, 0, ref modifier);
+
+ // Assert
+ await Assert.That(modifier).IsEqualTo(1.0f);
+ }
+
+ ///
+ /// Bulbapedia: Charge boosts the user's *next* Electric-type move — the effect lasts through the end
+ /// of the following turn and then wears off. The effect survives the end of the turn Charge was used
+ /// on and removes itself at the end of the next turn.
+ ///
+ [Test]
+ public async Task ChargeEffect_OnEndTurn_RemovesItselfAfterTheNextTurn()
+ {
+ // Arrange
+ var (script, move, user, userVolatile) = CreateTestSetup();
+ script.OnSecondaryEffect(move, Substitute.For(), 0);
+ await Assert.That(userVolatile.TryGet(out var effect)).IsTrue();
+ var battle = Substitute.For();
+
+ // Act & Assert - end of the turn of use: effect remains
+ effect!.OnEndTurn(user, battle);
+ await Assert.That(userVolatile.Contains(ScriptUtils.ResolveName())).IsTrue();
+
+ // Act & Assert - end of the following turn: effect is removed
+ effect.OnEndTurn(user, battle);
+ await Assert.That(userVolatile.Contains(ScriptUtils.ResolveName())).IsFalse();
+ }
+}
\ No newline at end of file
diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/ChipAwayTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/ChipAwayTests.cs
new file mode 100644
index 0000000..23e7625
--- /dev/null
+++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/ChipAwayTests.cs
@@ -0,0 +1,48 @@
+using PkmnLib.Dynamic.Models;
+using PkmnLib.Plugin.Gen7.Scripts.Moves;
+
+namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
+
+///
+/// Tests for the move script.
+/// Gen VII Bulbapedia behavior: "Chip Away inflicts damage, ignoring any changes to the target's Defense
+/// and evasion stat stages — positive and negative."
+///
+public class ChipAwayTests
+{
+ ///
+ /// Bulbapedia: Chip Away inflicts damage "ignoring any changes to the target's Defense [...] stat
+ /// stages".
+ ///
+ [Test]
+ public async Task BypassDefensiveStatBoosts_Always_Bypasses()
+ {
+ // Arrange
+ var script = new ChipAway();
+ var bypass = false;
+
+ // Act
+ script.BypassDefensiveStatBoosts(Substitute.For(), Substitute.For(), 0, ref bypass);
+
+ // Assert
+ await Assert.That(bypass).IsTrue();
+ }
+
+ ///
+ /// Bulbapedia: Chip Away inflicts damage "ignoring any changes to the target's [...] evasion stat
+ /// stages".
+ ///
+ [Test]
+ public async Task BypassEvasionStatBoosts_Always_Bypasses()
+ {
+ // Arrange
+ var script = new ChipAway();
+ var bypass = false;
+
+ // Act
+ script.BypassEvasionStatBoosts(Substitute.For(), Substitute.For(), 0, ref bypass);
+
+ // Assert
+ await Assert.That(bypass).IsTrue();
+ }
+}
\ No newline at end of file
diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/Conversion2Tests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/Conversion2Tests.cs
new file mode 100644
index 0000000..bea78b9
--- /dev/null
+++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/Conversion2Tests.cs
@@ -0,0 +1,164 @@
+using PkmnLib.Dynamic.Libraries;
+using PkmnLib.Dynamic.Models;
+using PkmnLib.Dynamic.Models.Choices;
+using PkmnLib.Plugin.Gen7.Scripts.Moves;
+using PkmnLib.Static;
+using PkmnLib.Static.Moves;
+using PkmnLib.Static.Utils;
+
+namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
+
+///
+/// Tests for the move script.
+/// Gen VII Bulbapedia behavior: "Conversion 2 will randomly change the user's type to any type that
+/// either resists or is immune to the type of the last damaging move it was hit by." From Generation V
+/// onwards "Conversion 2 now targets an adjacent Pokémon" and considers that target's last used move.
+/// From Generation IV onwards, "Conversion 2 will no longer change the user to any of its current types."
+///
+public class Conversion2Tests
+{
+ private static readonly IDynamicLibrary Library = LibraryHelpers.LoadLibrary();
+
+ private static (Conversion2 script, IExecutingMove move, IPokemon user, IPokemon target, IHitData hitData)
+ CreateTestSetup(TypeIdentifier? lastMoveType)
+ {
+ var script = new Conversion2();
+
+ var random = Substitute.For();
+ random.GetInt().Returns(0);
+ var battle = Substitute.For();
+ battle.Library.Returns(Library);
+ battle.Random.Returns(random);
+
+ var userBattleData = Substitute.For();
+ userBattleData.Battle.Returns(battle);
+ var user = Substitute.For();
+ user.BattleData.Returns(userBattleData);
+
+ var targetBattleData = Substitute.For();
+ if (lastMoveType == null)
+ {
+ targetBattleData.LastMoveChoice.Returns((IMoveChoice?)null);
+ }
+ else
+ {
+ var moveData = Substitute.For();
+ moveData.MoveType.Returns(lastMoveType.Value);
+ var learnedMove = Substitute.For();
+ learnedMove.MoveData.Returns(moveData);
+ var lastChoice = Substitute.For();
+ lastChoice.ChosenMove.Returns(learnedMove);
+ targetBattleData.LastMoveChoice.Returns(lastChoice);
+ }
+
+ var target = Substitute.For();
+ target.BattleData.Returns(targetBattleData);
+
+ var move = Substitute.For();
+ move.User.Returns(user);
+ var hitData = Substitute.For();
+ move.GetHitData(target, 0).Returns(hitData);
+
+ return (script, move, user, target, hitData);
+ }
+
+ private static TypeIdentifier GetType(string name)
+ {
+ Library.StaticLibrary.Types.TryGetTypeIdentifier(name, out var type);
+ return type;
+ }
+
+ ///
+ /// Helper that returns the single type the user was changed to, or null when
+ /// was never called.
+ ///
+ private static TypeIdentifier? GetSetType(IPokemon user)
+ {
+ var call = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "SetTypes");
+ return call != null ? ((IReadOnlyList)call.GetArguments()[0]!).Single() : null;
+ }
+
+ ///
+ /// Bulbapedia: Conversion 2 changes "the user's type to any type that either resists or is immune to
+ /// the type of the last damaging move" the target used. The chosen type must take less than neutral
+ /// damage from the target's last move type.
+ ///
+ [Test]
+ public async Task OnSecondaryEffect_TargetUsedMove_UserBecomesTypeResistingThatMove()
+ {
+ // Arrange
+ var normalType = GetType("normal");
+ var (script, move, user, target, _) = CreateTestSetup(normalType);
+
+ // Act
+ script.OnSecondaryEffect(move, target, 0);
+
+ // Assert - the new type resists (or is immune to) Normal
+ var newType = GetSetType(user);
+ await Assert.That(newType).IsNotNull();
+ var effectiveness = Library.StaticLibrary.Types.GetAllEffectivenessFromAttacking(normalType)
+ .Single(x => x.Item1 == newType!.Value).Item2;
+ await Assert.That(effectiveness).IsLessThan(1f);
+ }
+
+ ///
+ /// Bulbapedia: the move works off "the type of the last damaging move it was hit by" (the target's
+ /// last used move from Generation V onward) — if the target has not used a move, Conversion 2 fails.
+ ///
+ [Test]
+ public void OnSecondaryEffect_TargetHasNoLastMove_Fails()
+ {
+ // Arrange
+ var (script, move, user, target, hitData) = CreateTestSetup(null);
+
+ // Act
+ script.OnSecondaryEffect(move, target, 0);
+
+ // Assert
+ hitData.Received(1).Fail();
+ user.DidNotReceive().SetTypes(Arg.Any>());
+ }
+
+ ///
+ /// A last move without a real type (type "none") provides no type to resist, so Conversion 2 fails.
+ ///
+ [Test]
+ public void OnSecondaryEffect_LastMoveHasNoType_Fails()
+ {
+ // Arrange
+ var (script, move, user, target, hitData) = CreateTestSetup(new TypeIdentifier(0, new StringKey("none")));
+
+ // Act
+ script.OnSecondaryEffect(move, target, 0);
+
+ // Assert
+ hitData.Received(1).Fail();
+ user.DidNotReceive().SetTypes(Arg.Any>());
+ }
+
+ ///
+ /// Bulbapedia (Generation IV onwards): "Conversion 2 will no longer change the user to any of its
+ /// current types." When the type that would be rolled is one the user already has, a different
+ /// resisting type must be chosen.
+ ///
+ [Test]
+ public async Task OnSecondaryEffect_RolledTypeIsUsersCurrentType_PicksDifferentType()
+ {
+ // Arrange
+ var normalType = GetType("normal");
+ var (script, move, user, target, _) = CreateTestSetup(normalType);
+ // With the mocked random always returning 0, the script deterministically picks the resisting type
+ // with the lowest type identifier. Give the user exactly that type.
+ var predictedPick = Library.StaticLibrary.Types.GetAllEffectivenessFromAttacking(normalType)
+ .Where(x => x.effectiveness < 1).OrderBy(x => x.type.Value).First().Item1;
+ user.Types.Returns([predictedPick]);
+
+ // Act
+ script.OnSecondaryEffect(move, target, 0);
+
+ // Assert - the user must not be "changed" to a type it already has
+ var newType = GetSetType(user);
+ await Assert.That(newType).IsNotNull();
+ await Assert.That(newType!.Value).IsNotEqualTo(predictedPick);
+ }
+}
\ No newline at end of file
diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/ConversionTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/ConversionTests.cs
new file mode 100644
index 0000000..ff2e80c
--- /dev/null
+++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/ConversionTests.cs
@@ -0,0 +1,76 @@
+using PkmnLib.Dynamic.Models;
+using PkmnLib.Plugin.Gen7.Scripts.Moves;
+using PkmnLib.Static;
+using PkmnLib.Static.Moves;
+using PkmnLib.Static.Utils;
+
+namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
+
+///
+/// Tests for the move script.
+/// Gen VII Bulbapedia behavior: "Conversion now changes the user's current type to match the type of the
+/// move the user has in its first move slot (even if that move cannot be selected in battle)."
+///
+public class ConversionTests
+{
+ private static ILearnedMove CreateLearnedMoveOfType(TypeIdentifier type)
+ {
+ var moveData = Substitute.For();
+ moveData.MoveType.Returns(type);
+ var learned = Substitute.For();
+ learned.MoveData.Returns(moveData);
+ return learned;
+ }
+
+ private static (Conversion script, IExecutingMove move, IPokemon user, IPokemon target, IHitData hitData)
+ CreateTestSetup(params ILearnedMove?[] moves)
+ {
+ var script = new Conversion();
+ var move = Substitute.For();
+ var user = Substitute.For();
+ user.Moves.Returns(moves);
+ move.User.Returns(user);
+ var target = Substitute.For();
+ var hitData = Substitute.For();
+ move.GetHitData(target, 0).Returns(hitData);
+ return (script, move, user, target, hitData);
+ }
+
+ ///
+ /// Bulbapedia: "Conversion now changes the user's current type to match the type of the move the user
+ /// has in its first move slot".
+ ///
+ [Test]
+ public void OnSecondaryEffect_UserHasMoves_SetsUserTypeToFirstMoveSlotType()
+ {
+ // Arrange
+ var fireType = new TypeIdentifier(3, new StringKey("fire"));
+ var waterType = new TypeIdentifier(4, new StringKey("water"));
+ var (script, move, user, target, _) =
+ CreateTestSetup(CreateLearnedMoveOfType(fireType), CreateLearnedMoveOfType(waterType));
+
+ // Act
+ script.OnSecondaryEffect(move, target, 0);
+
+ // Assert - the first slot's type (fire) is used, not the second
+ user.Received(1).SetTypes(Arg.Is>(t => t.Count == 1 && t[0] == fireType));
+ }
+
+ ///
+ /// Bulbapedia: the type comes from "the move the user has in its first move slot" — with no known
+ /// moves there is no type to copy and the move fails.
+ ///
+ [Test]
+ public void OnSecondaryEffect_UserHasNoMoves_Fails()
+ {
+ // Arrange
+ var (script, move, user, target, hitData) = CreateTestSetup(null, null, null, null);
+
+ // Act
+ script.OnSecondaryEffect(move, target, 0);
+
+ // Assert
+ hitData.Received(1).Fail();
+ user.DidNotReceive().SetTypes(Arg.Any>());
+ }
+}
\ No newline at end of file
diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CopycatTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CopycatTests.cs
new file mode 100644
index 0000000..c315171
--- /dev/null
+++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CopycatTests.cs
@@ -0,0 +1,118 @@
+using PkmnLib.Dynamic.Models;
+using PkmnLib.Dynamic.Models.Choices;
+using PkmnLib.Plugin.Gen7.Scripts.Moves;
+using PkmnLib.Static.Moves;
+using PkmnLib.Static.Utils;
+
+namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
+
+///
+/// Tests for the move script.
+/// Gen VII Bulbapedia behavior: "Copycat causes the user to use the last move that was used in the battle
+/// (even if the last move was by the user)." The move fails if no prior move occurred, and certain moves
+/// (such as Counter or Copycat itself) cannot be copied.
+///
+public class CopycatTests
+{
+ private static (Copycat script, IMoveChoice choice) CreateTestSetup(string? lastMoveName)
+ {
+ var script = new Copycat();
+ var user = Substitute.For();
+ var battleData = Substitute.For();
+ if (lastMoveName == null)
+ {
+ battleData.LastMoveChoice.Returns((IMoveChoice?)null);
+ }
+ else
+ {
+ var moveData = Substitute.For();
+ moveData.Name.Returns(new StringKey(lastMoveName));
+ var learnedMove = Substitute.For();
+ learnedMove.MoveData.Returns(moveData);
+ var lastChoice = Substitute.For();
+ lastChoice.ChosenMove.Returns(learnedMove);
+ battleData.LastMoveChoice.Returns(lastChoice);
+ }
+ user.BattleData.Returns(battleData);
+
+ var choice = Substitute.For();
+ choice.User.Returns(user);
+ return (script, choice);
+ }
+
+ ///
+ /// Bulbapedia: "Copycat causes the user to use the last move that was used in the battle".
+ ///
+ [Test]
+ public async Task ChangeMove_LastMoveIsCopyable_ChangesMoveToLastMove()
+ {
+ // Arrange
+ var (script, choice) = CreateTestSetup("tackle");
+ StringKey moveName = "copycat";
+
+ // Act
+ script.ChangeMove(choice, ref moveName);
+
+ // Assert
+ await Assert.That(moveName).IsEqualTo(new StringKey("tackle"));
+ choice.DidNotReceive().Fail();
+ }
+
+ ///
+ /// Bulbapedia: "The move fails if no prior move occurred."
+ ///
+ [Test]
+ public async Task ChangeMove_NoMoveUsedYet_Fails()
+ {
+ // Arrange
+ var (script, choice) = CreateTestSetup(null);
+ StringKey moveName = "copycat";
+
+ // Act
+ script.ChangeMove(choice, ref moveName);
+
+ // Assert
+ choice.Received(1).Fail();
+ await Assert.That(moveName).IsEqualTo(new StringKey("copycat"));
+ }
+
+ ///
+ /// Bulbapedia lists moves Copycat cannot copy (e.g. Counter, Copycat itself, Protect). If the last
+ /// used move is one of those, Copycat fails.
+ ///
+ [Test, Arguments("counter"), Arguments("copycat"), Arguments("protect")]
+ public async Task ChangeMove_LastMoveNotCopyable_Fails(string lastMove)
+ {
+ // Arrange
+ var (script, choice) = CreateTestSetup(lastMove);
+ StringKey moveName = "copycat";
+
+ // Act
+ script.ChangeMove(choice, ref moveName);
+
+ // Assert
+ choice.Received(1).Fail();
+ await Assert.That(moveName).IsEqualTo(new StringKey("copycat"));
+ }
+
+ ///
+ /// Technical test: outside of battle (no battle data) there is no last move, so Copycat fails.
+ ///
+ [Test]
+ public void ChangeMove_NoBattleData_Fails()
+ {
+ // Arrange
+ var script = new Copycat();
+ var user = Substitute.For();
+ user.BattleData.Returns((IPokemonBattleData?)null);
+ var choice = Substitute.For();
+ choice.User.Returns(user);
+ StringKey moveName = "copycat";
+
+ // Act
+ script.ChangeMove(choice, ref moveName);
+
+ // Assert
+ choice.Received(1).Fail();
+ }
+}
\ No newline at end of file
diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CoreEnforcerTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CoreEnforcerTests.cs
new file mode 100644
index 0000000..cd1fc0e
--- /dev/null
+++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CoreEnforcerTests.cs
@@ -0,0 +1,181 @@
+using PkmnLib.Dynamic.Models;
+using PkmnLib.Dynamic.Models.Choices;
+using PkmnLib.Plugin.Gen7.Scripts.Moves;
+using PkmnLib.Static.Species;
+using PkmnLib.Static.Utils;
+
+namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
+
+///
+/// Tests for the move script.
+/// Gen VII Bulbapedia behavior: "Core Enforcer deals damage. If the target has already used a move or had
+/// a Bag item used on it by its Trainer in the same turn, Core Enforcer also suppresses the target's
+/// Ability while it remains in battle."
+///
+public class CoreEnforcerTests
+{
+ private static (CoreEnforcer script, IExecutingMove move, IMoveChoice currentChoice, IPokemon target, IHitData
+ hitData, IBattle battle) CreateTestSetup()
+ {
+ var script = new CoreEnforcer();
+ var battle = Substitute.For();
+ var battleData = Substitute.For();
+ battleData.Battle.Returns(battle);
+ var target = Substitute.For();
+ target.BattleData.Returns(battleData);
+
+ var currentChoice = Substitute.For();
+ var move = Substitute.For();
+ move.MoveChoice.Returns(currentChoice);
+ var hitData = Substitute.For();
+ move.GetHitData(target, 0).Returns(hitData);
+
+ return (script, move, currentChoice, target, hitData, battle);
+ }
+
+ private static void SetTurnChoices(IBattle battle, params ITurnChoice[] choices)
+ {
+ battle.PreviousTurnChoices.Returns(new List> { choices });
+ }
+
+ ///
+ /// Bulbapedia: "If the target has already used a move [...] in the same turn, Core Enforcer also
+ /// suppresses the target's Ability while it remains in battle."
+ ///
+ [Test]
+ public void OnSecondaryEffect_TargetMovedEarlierInTurn_SuppressesTargetAbility()
+ {
+ // Arrange
+ var (script, move, currentChoice, target, hitData, battle) = CreateTestSetup();
+ var targetChoice = Substitute.For();
+ targetChoice.User.Returns(target);
+ SetTurnChoices(battle, targetChoice, currentChoice);
+
+ // Act
+ script.OnSecondaryEffect(move, target, 0);
+
+ // Assert
+ target.Received(1).SuppressAbility();
+ hitData.DidNotReceive().Fail();
+ }
+
+ ///
+ /// Bulbapedia: the suppression also happens if the target "had a Bag item used on it by its Trainer in
+ /// the same turn".
+ ///
+ [Test]
+ public void OnSecondaryEffect_ItemUsedEarlierInTurn_SuppressesTargetAbility()
+ {
+ // Arrange
+ var (script, move, currentChoice, target, hitData, battle) = CreateTestSetup();
+ var itemChoice = Substitute.For();
+ itemChoice.User.Returns(target);
+ SetTurnChoices(battle, itemChoice, currentChoice);
+
+ // Act
+ script.OnSecondaryEffect(move, target, 0);
+
+ // Assert
+ target.Received(1).SuppressAbility();
+ hitData.DidNotReceive().Fail();
+ }
+
+ ///
+ /// Bulbapedia: the Ability is only suppressed "If the target has already used a move [...] in the same
+ /// turn" — when Core Enforcer strikes before the target has acted, no suppression happens.
+ ///
+ [Test]
+ public void OnSecondaryEffect_TargetActsLaterInTurn_DoesNotSuppressAbility()
+ {
+ // Arrange
+ var (script, move, currentChoice, target, hitData, battle) = CreateTestSetup();
+ var targetChoice = Substitute.For();
+ targetChoice.User.Returns(target);
+ SetTurnChoices(battle, currentChoice, targetChoice);
+
+ // Act
+ script.OnSecondaryEffect(move, target, 0);
+
+ // Assert
+ target.DidNotReceive().SuppressAbility();
+ hitData.Received(1).Fail();
+ }
+
+ ///
+ /// Bulbapedia: suppression requires the target to have already acted — as the very first action of the
+ /// turn, Core Enforcer never suppresses.
+ ///
+ [Test]
+ public void OnSecondaryEffect_CoreEnforcerIsFirstActionOfTurn_DoesNotSuppressAbility()
+ {
+ // Arrange
+ var (script, move, currentChoice, target, hitData, battle) = CreateTestSetup();
+ SetTurnChoices(battle, currentChoice);
+
+ // Act
+ script.OnSecondaryEffect(move, target, 0);
+
+ // Assert
+ target.DidNotReceive().SuppressAbility();
+ hitData.Received(1).Fail();
+ }
+
+ ///
+ /// Technical test: a target without battle data (not in battle) is left untouched.
+ ///
+ [Test]
+ public void OnSecondaryEffect_TargetHasNoBattleData_DoesNothing()
+ {
+ // Arrange
+ var (script, move, _, target, hitData, _) = CreateTestSetup();
+ target.BattleData.Returns((IPokemonBattleData?)null);
+
+ // Act
+ script.OnSecondaryEffect(move, target, 0);
+
+ // Assert
+ target.DidNotReceive().SuppressAbility();
+ hitData.DidNotReceive().Fail();
+ }
+
+ ///
+ /// Bulbapedia: the condition is that "the target has already used a move" — an action by a different
+ /// Pokémon (e.g. the user's ally in a Double Battle) does not count as the target having acted.
+ ///
+ [Test]
+ public void OnSecondaryEffect_OnlyOtherPokemonActedEarlier_DoesNotSuppressAbility()
+ {
+ // Arrange
+ var (script, move, currentChoice, target, hitData, battle) = CreateTestSetup();
+ var allyChoice = Substitute.For();
+ allyChoice.User.Returns(Substitute.For());
+ SetTurnChoices(battle, allyChoice, currentChoice);
+
+ // Act
+ script.OnSecondaryEffect(move, target, 0);
+
+ // Assert
+ target.DidNotReceive().SuppressAbility();
+ hitData.Received(1).Fail();
+ }
+
+ ///
+ /// Bulbapedia: "The move cannot suppress certain signature abilities including Multitype, Stance
+ /// Change, Schooling, Comatose, Shields Down, Disguise, RKS System, Battle Bond, Power Construct".
+ /// The script requests the suppression unconditionally; refuses
+ /// it when is false, so these abilities must carry that flag in the
+ /// Gen7 data.
+ ///
+ [Test, Arguments("multitype"), Arguments("stance_change"), Arguments("schooling"), Arguments("comatose"),
+ Arguments("shields_down"), Arguments("disguise"), Arguments("rks_system"), Arguments("battle_bond"),
+ Arguments("power_construct")]
+ public async Task OnSecondaryEffect_UnsuppressableAbility_IsProtectedByAbilityData(string abilityName)
+ {
+ // Arrange
+ var library = LibraryHelpers.LoadLibrary();
+
+ // Assert
+ await Assert.That(library.StaticLibrary.Abilities.TryGet(new StringKey(abilityName), out var ability)).IsTrue();
+ await Assert.That(ability!.CanBeChanged).IsFalse();
+ }
+}
\ No newline at end of file
diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CounterTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CounterTests.cs
new file mode 100644
index 0000000..fa04d89
--- /dev/null
+++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CounterTests.cs
@@ -0,0 +1,194 @@
+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;
+
+namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
+
+///
+/// Tests for the move script and its .
+/// Gen VII Bulbapedia behavior: Counter retaliates against the last physical move that damaged the user
+/// this turn, dealing twice the damage the user received to the Pokémon that dealt it. Since Generation II
+/// "Counter now affects all physical moves, dealing double damage"; it fails when the user was not hit by
+/// a physical move first.
+///
+public class CounterTests
+{
+ ///
+ /// Creates a user whose volatile scripts contain a that has recorded
+ /// an incoming physical hit of the given damage by .
+ ///
+ private static IPokemon CreateUserHitBy(IPokemon? attacker, uint damage, bool physical = true)
+ {
+ var user = Substitute.For();
+ var userVolatile = new ScriptSet(user);
+ user.Volatile.Returns(userVolatile);
+ user.GetScripts().Returns(_ => new ScriptIterator(new List>()));
+
+ var helper = new CounterHelperEffect();
+ userVolatile.Add(helper);
+ if (attacker != null)
+ {
+ var incomingMove = Substitute.For();
+ var useMove = Substitute.For();
+ useMove.Category.Returns(physical ? MoveCategory.Physical : MoveCategory.Special);
+ incomingMove.UseMove.Returns(useMove);
+ incomingMove.User.Returns(attacker);
+ var incomingHit = Substitute.For();
+ incomingHit.Damage.Returns(damage);
+ incomingMove.GetHitData(user, 0).Returns(incomingHit);
+ helper.OnIncomingHit(incomingMove, user, 0);
+ }
+
+ return user;
+ }
+
+ ///
+ /// Counter watches for incoming hits over the whole turn: at the start of the turn it attaches its
+ /// to the user.
+ ///
+ [Test]
+ public async Task OnBeforeTurnStart_Always_AddsCounterHelperEffectToUser()
+ {
+ // Arrange
+ var script = new Counter();
+ var user = Substitute.For();
+ var userVolatile = new ScriptSet(user);
+ user.Volatile.Returns(userVolatile);
+ user.GetScripts().Returns(_ => new ScriptIterator(new List>()));
+ var choice = Substitute.For();
+ choice.User.Returns(user);
+
+ // Act
+ script.OnBeforeTurnStart(choice);
+
+ // Assert
+ await Assert.That(userVolatile.Contains(ScriptUtils.ResolveName())).IsTrue();
+ }
+
+ ///
+ /// Bulbapedia: Counter strikes back at the Pokémon that last hit the user with a physical move — the
+ /// move is redirected to that attacker.
+ ///
+ [Test]
+ public async Task ChangeTargets_UserWasHitByPhysicalMove_TargetsTheAttacker()
+ {
+ // Arrange
+ var script = new Counter();
+ var attacker = Substitute.For();
+ var user = CreateUserHitBy(attacker, 40);
+ var choice = Substitute.For();
+ choice.User.Returns(user);
+ IReadOnlyList targets = new IPokemon?[] { Substitute.For() };
+
+ // Act
+ script.ChangeTargets(choice, ref targets);
+
+ // Assert
+ await Assert.That(targets.Single()).IsEqualTo(attacker);
+ choice.DidNotReceive().Fail();
+ }
+
+ ///
+ /// Bulbapedia: Counter fails when the user has not been hit by a physical move this turn.
+ ///
+ [Test]
+ public void ChangeTargets_UserWasNotHit_Fails()
+ {
+ // Arrange
+ var script = new Counter();
+ var user = CreateUserHitBy(null, 0);
+ var choice = Substitute.For();
+ choice.User.Returns(user);
+ IReadOnlyList targets = new IPokemon?[] { Substitute.For() };
+
+ // Act
+ script.ChangeTargets(choice, ref targets);
+
+ // Assert
+ choice.Received(1).Fail();
+ }
+
+ ///
+ /// Bulbapedia (Generation II onwards): "Counter now affects all physical moves, dealing double
+ /// damage." The damage dealt is twice the damage of the recorded hit.
+ ///
+ [Test, Arguments(40u, 80u), Arguments(1u, 2u), Arguments(123u, 246u)]
+ public async Task ChangeMoveDamage_UserWasHitByPhysicalMove_DealsDoubleTheDamageTaken(uint damageTaken,
+ uint expectedDamage)
+ {
+ // Arrange
+ var script = new Counter();
+ var attacker = Substitute.For();
+ var user = CreateUserHitBy(attacker, damageTaken);
+ var move = Substitute.For();
+ move.User.Returns(user);
+ uint damage = 0;
+
+ // Act
+ script.ChangeMoveDamage(move, attacker, 0, ref damage);
+
+ // Assert
+ await Assert.That(damage).IsEqualTo(expectedDamage);
+ }
+
+ ///
+ /// Bulbapedia: Counter retaliates against the Pokémon that hit the user — against any other target the
+ /// hit fails.
+ ///
+ [Test]
+ public void ChangeMoveDamage_TargetIsNotTheLastAttacker_FailsHit()
+ {
+ // Arrange
+ var script = new Counter();
+ var attacker = Substitute.For();
+ var user = CreateUserHitBy(attacker, 40);
+ var someoneElse = Substitute.For();
+ var move = Substitute.For();
+ move.User.Returns(user);
+ var hitData = Substitute.For();
+ move.GetHitData(someoneElse, 0).Returns(hitData);
+ uint damage = 0;
+
+ // Act
+ script.ChangeMoveDamage(move, someoneElse, 0, ref damage);
+
+ // Assert
+ hitData.Received(1).Fail();
+ }
+
+ ///
+ /// Bulbapedia: only physical moves can be countered — the helper records physical hits.
+ ///
+ [Test]
+ public async Task CounterHelperEffect_PhysicalHit_RecordsAttackerAndDamage()
+ {
+ // Arrange
+ var attacker = Substitute.For();
+ var user = CreateUserHitBy(attacker, 40);
+
+ // Assert
+ var helper = ((IScriptSet)user.Volatile).Get();
+ await Assert.That(helper!.LastHitBy).IsEqualTo(attacker);
+ await Assert.That(helper.LastDamage).IsEqualTo(40u);
+ }
+
+ ///
+ /// Bulbapedia (Generation IV onwards): special moves cannot be countered — the helper ignores
+ /// non-physical hits, so Counter fails after a special hit.
+ ///
+ [Test]
+ public async Task CounterHelperEffect_SpecialHit_IsNotRecorded()
+ {
+ // Arrange
+ var attacker = Substitute.For();
+ var user = CreateUserHitBy(attacker, 40, false);
+
+ // Assert
+ var helper = ((IScriptSet)user.Volatile).Get();
+ await Assert.That(helper!.LastHitBy).IsNull();
+ }
+}
\ No newline at end of file
diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CovetTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CovetTests.cs
new file mode 100644
index 0000000..ed10133
--- /dev/null
+++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CovetTests.cs
@@ -0,0 +1,92 @@
+using PkmnLib.Dynamic.Models;
+using PkmnLib.Plugin.Gen7.Scripts.Moves;
+using PkmnLib.Static;
+
+namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
+
+///
+/// Tests for the move script.
+/// Gen VII Bulbapedia behavior: "Covet inflicts damage and steals the target's held item if it is holding
+/// any. If the target is not holding an item or the user is already holding an item, Covet cannot steal an
+/// item."
+///
+public class CovetTests
+{
+ private static (Covet script, IExecutingMove move, IPokemon user, IPokemon target) CreateTestSetup(
+ IItem? targetItem)
+ {
+ var script = new Covet();
+ var move = Substitute.For();
+ var user = Substitute.For();
+ // Explicitly return null to suppress NSubstitute's auto-substitution; Covet must see an empty-handed user.
+ user.HeldItem.Returns((IItem?)null);
+ move.User.Returns(user);
+
+ var target = Substitute.For();
+ target.HeldItem.Returns(targetItem);
+ if (targetItem != null)
+ {
+ target.TryStealHeldItem(out Arg.Any()).Returns(x =>
+ {
+ x[0] = targetItem;
+ return true;
+ });
+ }
+
+ return (script, move, user, target);
+ }
+
+ ///
+ /// Bulbapedia: "Covet inflicts damage and steals the target's held item if it is holding any." The
+ /// item is removed from the target and ends up held by the user.
+ ///
+ [Test]
+ public void OnSecondaryEffect_TargetHoldsItem_UserStealsIt()
+ {
+ // Arrange
+ var item = Substitute.For();
+ var (script, move, user, target) = CreateTestSetup(item);
+
+ // Act
+ script.OnSecondaryEffect(move, target, 0);
+
+ // Assert - the item is taken from the target and given to the user
+ target.Received(1).TryStealHeldItem(out Arg.Any());
+ user.Received(1).ForceSetHeldItem(item);
+ }
+
+ ///
+ /// Bulbapedia: "If the target is not holding an item [...], Covet cannot steal an item."
+ ///
+ [Test]
+ public void OnSecondaryEffect_TargetHoldsNoItem_NothingIsStolen()
+ {
+ // Arrange
+ var (script, move, user, target) = CreateTestSetup(null);
+
+ // Act
+ script.OnSecondaryEffect(move, target, 0);
+
+ // Assert
+ target.DidNotReceive().TryStealHeldItem(out Arg.Any());
+ user.DidNotReceive().ForceSetHeldItem(Arg.Any());
+ }
+
+ ///
+ /// Bulbapedia: "If [...] the user is already holding an item, Covet cannot steal an item."
+ ///
+ [Test]
+ public void OnSecondaryEffect_UserAlreadyHoldsItem_TargetItemIsNotStolen()
+ {
+ // Arrange
+ var targetItem = Substitute.For();
+ var (script, move, user, target) = CreateTestSetup(targetItem);
+ user.HeldItem.Returns(Substitute.For());
+
+ // Act
+ script.OnSecondaryEffect(move, target, 0);
+
+ // Assert - the user must not end up with the target's item
+ user.DidNotReceive().ForceSetHeldItem(targetItem);
+ }
+}
\ No newline at end of file
diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CraftyShieldTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CraftyShieldTests.cs
new file mode 100644
index 0000000..5b5c30d
--- /dev/null
+++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CraftyShieldTests.cs
@@ -0,0 +1,119 @@
+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.Moves;
+
+namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
+
+///
+/// Tests for the move script and the it
+/// applies. Gen VII Bulbapedia behavior: "Crafty Shield protects all Pokémon on the user's side of the
+/// field from status moves."
+///
+public class CraftyShieldTests
+{
+ private static (CraftyShield script, IExecutingMove move, IScriptSet sideVolatile) CreateTestSetup()
+ {
+ var script = new CraftyShield();
+
+ var side = Substitute.For();
+ // Use a real script set so the side script added by Crafty Shield can be inspected afterwards.
+ var sideVolatile = new ScriptSet(side);
+ side.VolatileScripts.Returns(sideVolatile);
+ side.GetScripts().Returns(_ => new ScriptIterator(new List>()));
+
+ var battle = Substitute.For();
+ battle.Sides.Returns(new[] { side });
+ var battleData = Substitute.For();
+ battleData.Battle.Returns(battle);
+ battleData.SideIndex.Returns((byte)0);
+
+ var user = Substitute.For();
+ user.BattleData.Returns(battleData);
+ var move = Substitute.For();
+ move.User.Returns(user);
+
+ return (script, move, sideVolatile);
+ }
+
+ private static IExecutingMove CreateExecutingMoveOfCategory(MoveCategory category)
+ {
+ var useMove = Substitute.For();
+ useMove.Category.Returns(category);
+ var move = Substitute.For();
+ move.UseMove.Returns(useMove);
+ return move;
+ }
+
+ ///
+ /// Bulbapedia: "Crafty Shield protects all Pokémon on the user's side of the field" — using the move
+ /// attaches the to the user's side.
+ ///
+ [Test]
+ public async Task OnSecondaryEffect_Used_AddsCraftyShieldEffectToUsersSide()
+ {
+ // Arrange
+ var (script, move, sideVolatile) = CreateTestSetup();
+
+ // Act
+ script.OnSecondaryEffect(move, Substitute.For(), 0);
+
+ // Assert
+ await Assert.That(sideVolatile.Contains(ScriptUtils.ResolveName())).IsTrue();
+ }
+
+ ///
+ /// Bulbapedia: the shield protects "from status moves" — an incoming status move is stopped.
+ ///
+ [Test]
+ public async Task CraftyShieldEffect_StatusMove_IsStopped()
+ {
+ // Arrange
+ var effect = new CraftyShieldEffect();
+ var move = CreateExecutingMoveOfCategory(MoveCategory.Status);
+ var stop = false;
+
+ // Act
+ effect.StopBeforeMove(move, ref stop);
+
+ // Assert
+ await Assert.That(stop).IsTrue();
+ }
+
+ ///
+ /// Bulbapedia: Crafty Shield only blocks status moves — damaging moves go through it unhindered.
+ ///
+ [Test, Arguments(MoveCategory.Physical), Arguments(MoveCategory.Special)]
+ public async Task CraftyShieldEffect_DamagingMove_IsNotStopped(MoveCategory category)
+ {
+ // Arrange
+ var effect = new CraftyShieldEffect();
+ var move = CreateExecutingMoveOfCategory(category);
+ var stop = false;
+
+ // Act
+ effect.StopBeforeMove(move, ref stop);
+
+ // Assert
+ await Assert.That(stop).IsFalse();
+ }
+
+ ///
+ /// Technical test: outside of battle (no battle data) no shield can be raised and nothing happens.
+ ///
+ [Test]
+ public async Task OnSecondaryEffect_NoBattleData_DoesNothing()
+ {
+ // Arrange
+ var (script, move, sideVolatile) = CreateTestSetup();
+ move.User.BattleData.Returns((IPokemonBattleData?)null);
+
+ // Act
+ script.OnSecondaryEffect(move, Substitute.For(), 0);
+
+ // Assert - no effect is added
+ await Assert.That(sideVolatile.Contains(ScriptUtils.ResolveName())).IsFalse();
+ }
+}
\ No newline at end of file
diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CrushGripTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CrushGripTests.cs
new file mode 100644
index 0000000..2a6214e
--- /dev/null
+++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CrushGripTests.cs
@@ -0,0 +1,62 @@
+using PkmnLib.Dynamic.Models;
+using PkmnLib.Plugin.Gen7.Scripts.Moves;
+using PkmnLib.Static;
+
+namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
+
+///
+/// Tests for the move script.
+/// Gen VII Bulbapedia behavior: "Crush Grip deals damage. Its power is greater the more HP the target
+/// has." Power = 120 × (Current HP target / Max HP target), with a minimum of 1.
+///
+public class CrushGripTests
+{
+ private static (CrushGrip script, IExecutingMove move, IPokemon target) CreateTestSetup(uint currentHealth,
+ uint maxHealth)
+ {
+ var script = new CrushGrip();
+ var move = Substitute.For();
+ var target = Substitute.For();
+ target.CurrentHealth.Returns(currentHealth);
+ target.BoostedStats.Returns(new StatisticSet(maxHealth, 1, 1, 1, 1, 1));
+ return (script, move, target);
+ }
+
+ ///
+ /// Bulbapedia: "Power = 120 × (Current HP target / Max HP target)" — at full HP the power is 120, and
+ /// it scales down (with integer truncation) as the target's HP drops.
+ ///
+ [Test, Arguments(100u, 100u, (ushort)120), Arguments(200u, 200u, (ushort)120), Arguments(50u, 100u, (ushort)60),
+ Arguments(75u, 100u, (ushort)90), Arguments(33u, 100u, (ushort)39), Arguments(1u, 4u, (ushort)30)]
+ public async Task ChangeBasePower_ScalesWithTargetCurrentHp(uint currentHealth, uint maxHealth,
+ ushort expectedPower)
+ {
+ // Arrange
+ var (script, move, target) = CreateTestSetup(currentHealth, maxHealth);
+ ushort basePower = 120;
+
+ // Act
+ script.ChangeBasePower(move, target, 0, ref basePower);
+
+ // Assert
+ await Assert.That(basePower).IsEqualTo(expectedPower);
+ }
+
+ ///
+ /// Bulbapedia: the power varies "between 1 and 120 [...] with a minimum of 1" — even against a target
+ /// with almost no HP left, the power cannot drop below 1.
+ ///
+ [Test, Arguments(1u, 200u), Arguments(1u, 121u)]
+ public async Task ChangeBasePower_TargetAtVeryLowHp_PowerIsAtLeastOne(uint currentHealth, uint maxHealth)
+ {
+ // Arrange
+ var (script, move, target) = CreateTestSetup(currentHealth, maxHealth);
+ ushort basePower = 120;
+
+ // Act
+ script.ChangeBasePower(move, target, 0, ref basePower);
+
+ // Assert
+ await Assert.That(basePower).IsEqualTo((ushort)1);
+ }
+}
\ No newline at end of file
diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CurseTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CurseTests.cs
new file mode 100644
index 0000000..da8a6db
--- /dev/null
+++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CurseTests.cs
@@ -0,0 +1,212 @@
+using PkmnLib.Dynamic.Models;
+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;
+
+///
+/// Tests for the move script and the it applies.
+/// Gen VII Bulbapedia behavior — non-Ghost user: "The user's Speed stat will drop by one stage and its
+/// Attack stat and Defense stat will rise by one stage each." Ghost-type user: "The user will lose half of
+/// its maximum HP (rounded down) and put a curse on the target", and "A cursed Pokémon will lose ¼ of its
+/// maximum HP at the end of each turn."
+///
+public class CurseTests
+{
+ private static (Curse script, IExecutingMove move, IPokemon user, IPokemon target, IScriptSet targetVolatile)
+ CreateTestSetup(bool userIsGhost, uint userMaxHealth = 100, uint userCurrentHealth = 100)
+ {
+ var script = new Curse();
+ var library = LibraryHelpers.LoadLibrary();
+ library.StaticLibrary.Types.TryGetTypeIdentifier("ghost", out var ghostType);
+ library.StaticLibrary.Types.TryGetTypeIdentifier("normal", out var normalType);
+
+ var battle = Substitute.For();
+ battle.Library.Returns(library);
+ var battleData = Substitute.For();
+ battleData.Battle.Returns(battle);
+
+ var user = Substitute.For();
+ user.BattleData.Returns(battleData);
+ user.Types.Returns(new[] { userIsGhost ? ghostType : normalType });
+ user.MaxHealth.Returns(userMaxHealth);
+ user.CurrentHealth.Returns(userCurrentHealth);
+ var move = Substitute.For();
+ move.User.Returns(user);
+
+ var target = Substitute.For();
+ // Use a real script set so the curse applied to the target can be inspected afterwards.
+ var targetVolatile = new ScriptSet(target);
+ target.Volatile.Returns(targetVolatile);
+ target.GetScripts().Returns(_ => new ScriptIterator(new List>()));
+
+ return (script, move, user, target, targetVolatile);
+ }
+
+ ///
+ /// Helper that checks whether a stat boost change was applied to the given Pokémon.
+ ///
+ private static bool ReceivedStatBoost(IPokemon pokemon, Statistic stat, sbyte amount) =>
+ pokemon.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ChangeStatBoost" &&
+ (Statistic)c.GetArguments()[0]! == stat &&
+ (sbyte)c.GetArguments()[1]! == amount);
+
+ ///
+ /// Helper to extract the damage amount from a substitute's received Damage calls.
+ ///
+ private static uint? GetDamageAmount(IPokemon pokemon)
+ {
+ var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage");
+ return call != null ? (uint)call.GetArguments()[0]! : null;
+ }
+
+ ///
+ /// Bulbapedia (non-Ghost user): "The user's Speed stat will drop by one stage and its Attack stat and
+ /// Defense stat will rise by one stage each."
+ ///
+ [Test]
+ public async Task OnSecondaryEffect_NonGhostUser_LowersSpeedAndRaisesAttackAndDefense()
+ {
+ // Arrange
+ var (script, move, user, target, _) = CreateTestSetup(false);
+
+ // Act
+ script.OnSecondaryEffect(move, target, 0);
+
+ // Assert
+ await Assert.That(ReceivedStatBoost(user, Statistic.Speed, -1)).IsTrue();
+ await Assert.That(ReceivedStatBoost(user, Statistic.Attack, 1)).IsTrue();
+ await Assert.That(ReceivedStatBoost(user, Statistic.Defense, 1)).IsTrue();
+ }
+
+ ///
+ /// Bulbapedia: only a Ghost-type user pays HP and curses the target — a non-Ghost user takes no damage
+ /// and does not curse the target.
+ ///
+ [Test]
+ public async Task OnSecondaryEffect_NonGhostUser_DoesNotDamageUserOrCurseTarget()
+ {
+ // Arrange
+ var (script, move, user, target, targetVolatile) = CreateTestSetup(false);
+
+ // Act
+ script.OnSecondaryEffect(move, target, 0);
+
+ // Assert
+ await Assert.That(GetDamageAmount(user)).IsNull();
+ await Assert.That(targetVolatile.Contains(ScriptUtils.ResolveName())).IsFalse();
+ }
+
+ ///
+ /// Bulbapedia (Ghost-type user): "The user will lose half of its maximum HP (rounded down)". At full
+ /// HP that is half of its current HP as well.
+ ///
+ [Test]
+ public async Task OnSecondaryEffect_GhostUserAtFullHp_UserLosesHalfItsMaximumHp()
+ {
+ // Arrange
+ var (script, move, user, target, _) = CreateTestSetup(true, 100, 100);
+
+ // Act
+ script.OnSecondaryEffect(move, target, 0);
+
+ // Assert
+ await Assert.That(GetDamageAmount(user)!.Value).IsEqualTo(50u);
+ }
+
+ ///
+ /// Bulbapedia (Ghost-type user): the user pays HP to "put a curse on the target" — the
+ /// is attached to the target.
+ ///
+ [Test]
+ public async Task OnSecondaryEffect_GhostUser_CursesTarget()
+ {
+ // Arrange
+ var (script, move, _, target, targetVolatile) = CreateTestSetup(true);
+
+ // Act
+ script.OnSecondaryEffect(move, target, 0);
+
+ // Assert
+ await Assert.That(targetVolatile.Contains(ScriptUtils.ResolveName())).IsTrue();
+ }
+
+ ///
+ /// Bulbapedia (Ghost-type user): "The user will lose half of its maximum HP (rounded down)" — the cost
+ /// is based on maximum HP, not on the HP the user has left.
+ ///
+ [Test]
+ public async Task OnSecondaryEffect_GhostUserAtHalfHp_UserStillLosesHalfItsMaximumHp()
+ {
+ // Arrange
+ var (script, move, user, target, _) = CreateTestSetup(true, 100, 50);
+
+ // Act
+ script.OnSecondaryEffect(move, target, 0);
+
+ // Assert
+ await Assert.That(GetDamageAmount(user)!.Value).IsEqualTo(50u);
+ }
+
+ ///
+ /// Bulbapedia (Ghost-type user): "If this causes the user's HP to drop to 0, the move will execute
+ /// fully but cause the user to faint." — the curse is still put on the target.
+ ///
+ [Test]
+ public async Task OnSecondaryEffect_GhostUserFaintsFromHpCost_TargetIsStillCursed()
+ {
+ // Arrange
+ var (script, move, user, target, targetVolatile) = CreateTestSetup(true);
+ // The first read of CurrentHealth computes the HP cost; afterwards the user has fainted.
+ user.CurrentHealth.Returns(100u, 0u);
+
+ // Act
+ script.OnSecondaryEffect(move, target, 0);
+
+ // Assert
+ await Assert.That(targetVolatile.Contains(ScriptUtils.ResolveName())).IsTrue();
+ }
+
+ ///
+ /// Bulbapedia: "A cursed Pokémon will lose ¼ of its maximum HP at the end of each turn." At full HP
+ /// that is a quarter of its current HP as well.
+ ///
+ [Test]
+ public async Task GhostCurseEffect_OnEndTurnAtFullHp_CursedPokemonLosesQuarterOfMaximumHp()
+ {
+ // Arrange
+ var cursed = Substitute.For();
+ cursed.MaxHealth.Returns(100u);
+ cursed.CurrentHealth.Returns(100u);
+ var effect = new GhostCurseEffect(cursed);
+
+ // Act
+ effect.OnEndTurn(Substitute.For(), Substitute.For());
+
+ // Assert
+ await Assert.That(GetDamageAmount(cursed)!.Value).IsEqualTo(25u);
+ }
+
+ ///
+ /// Bulbapedia: "A cursed Pokémon will lose ¼ of its maximum HP at the end of each turn." — the drain
+ /// is based on maximum HP, not on the HP the Pokémon has left.
+ ///
+ [Test]
+ public async Task GhostCurseEffect_OnEndTurnAtLowHp_CursedPokemonStillLosesQuarterOfMaximumHp()
+ {
+ // Arrange
+ var cursed = Substitute.For();
+ cursed.MaxHealth.Returns(100u);
+ cursed.CurrentHealth.Returns(40u);
+ var effect = new GhostCurseEffect(cursed);
+
+ // Act
+ effect.OnEndTurn(Substitute.For(), Substitute.For());
+
+ // Assert
+ await Assert.That(GetDamageAmount(cursed)!.Value).IsEqualTo(25u);
+ }
+}
\ No newline at end of file
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Captivate.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Captivate.cs
index 162c037..72f3f56 100644
--- a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Captivate.cs
+++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Captivate.cs
@@ -9,7 +9,7 @@ public class Captivate : Script, IScriptOnSecondaryEffect
public void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
{
var user = move.User;
- if (target.Gender == Gender.Genderless || target.Gender == user.Gender)
+ if (target.Gender == Gender.Genderless || target.Gender == user.Gender || user.Gender == Gender.Genderless)
{
move.GetHitData(target, hit).Fail();
return;
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Conversion2.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Conversion2.cs
index d5e1b7f..8ff8858 100644
--- a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Conversion2.cs
+++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Conversion2.cs
@@ -16,7 +16,7 @@ public class Conversion2 : Script, IScriptOnSecondaryEffect
var typeLibrary = move.User.BattleData!.Battle.Library.StaticLibrary.Types;
// Get all types against which the last move would be not very effective
var type = typeLibrary.GetAllEffectivenessFromAttacking(lastMoveByTarget.ChosenMove.MoveData.MoveType)
- .Where(x => x.effectiveness < 1)
+ .Where(x => x.effectiveness < 1 && !move.User.Types.Contains(x.type))
// Shuffle them randomly, but deterministically
.OrderBy(_ => move.User.BattleData.Battle.Random.GetInt()).ThenBy(x => x.type.Value)
// And grab the first one
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/CoreEnforcer.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/CoreEnforcer.cs
index f34a4aa..802015c 100644
--- a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/CoreEnforcer.cs
+++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/CoreEnforcer.cs
@@ -11,8 +11,8 @@ public class CoreEnforcer : Script, IScriptOnSecondaryEffect
return;
var turnChoices = battleData.Battle.PreviousTurnChoices.Last();
var currentChoiceIndex = turnChoices.IndexOf(move.MoveChoice);
- if (currentChoiceIndex == -1 ||
- !turnChoices.Take(currentChoiceIndex).Any(choice => choice is IMoveChoice or IItemChoice))
+ if (currentChoiceIndex == -1 || !turnChoices.Take(currentChoiceIndex)
+ .Any(choice => choice is IMoveChoice or IItemChoice && choice.User == target))
{
move.GetHitData(target, hit).Fail();
return;
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Covet.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Covet.cs
index d085ebd..3b04f5c 100644
--- a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Covet.cs
+++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Covet.cs
@@ -8,7 +8,9 @@ public class Covet : Script, IScriptOnSecondaryEffect
{
if (target.HeldItem == null)
return;
- if (!move.User.TryStealHeldItem(out var item))
+ if (move.User.HeldItem is not null)
+ return;
+ if (!target.TryStealHeldItem(out var item))
return;
_ = move.User.ForceSetHeldItem(item);
}
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Curse.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Curse.cs
index 6889e8e..e763780 100644
--- a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Curse.cs
+++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Curse.cs
@@ -16,9 +16,7 @@ public class Curse : Script, IScriptOnSecondaryEffect
return;
if (move.User.Types.Contains(ghostType))
{
- move.User.Damage(move.User.CurrentHealth / 2, DamageSource.Misc, forceDamage: true);
- if (move.User.CurrentHealth == 0)
- return;
+ move.User.Damage(move.User.MaxHealth / 2, DamageSource.Misc, forceDamage: true);
target.Volatile.Add(new GhostCurseEffect(target));
}
else
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/GhostCurseEffect.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/GhostCurseEffect.cs
index 6347d75..2e48d8e 100644
--- a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/GhostCurseEffect.cs
+++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/GhostCurseEffect.cs
@@ -13,12 +13,12 @@ public class GhostCurseEffect : Script, IScriptOnEndTurn, IAIInfoScriptExpectedE
///
public void OnEndTurn(IScriptSource owner, IBattle battle)
{
- _pokemon.Damage(_pokemon.CurrentHealth / 4, DamageSource.Misc);
+ _pokemon.Damage(_pokemon.MaxHealth / 4, DamageSource.Misc);
}
///
public void ExpectedEndOfTurnDamage(IPokemon pokemon, ref int damage)
{
- damage += (int)(_pokemon.CurrentHealth / 4f);
+ damage += (int)(_pokemon.MaxHealth / 4f);
}
}
\ No newline at end of file