This commit is contained in:
@@ -296,7 +296,7 @@ public interface IPokemon : IScriptSource, IDeepCloneable
|
||||
/// <summary>
|
||||
/// Suppresses the ability of the Pokémon.
|
||||
/// </summary>
|
||||
void SuppressAbility();
|
||||
bool SuppressAbility();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the currently active ability.
|
||||
@@ -974,10 +974,14 @@ public class PokemonImpl : ScriptSource, IPokemon
|
||||
public bool AbilitySuppressed { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SuppressAbility()
|
||||
public bool SuppressAbility()
|
||||
{
|
||||
if (ActiveAbility?.CanBeChanged == false)
|
||||
return false;
|
||||
|
||||
AbilitySuppressed = true;
|
||||
AbilityScript.Clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
private (IAbility, AbilityIndex)? _abilityCache;
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the <see cref="Camouflage"/> 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).
|
||||
/// </summary>
|
||||
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<IBattle>();
|
||||
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<IPokemon>();
|
||||
var move = Substitute.For<IExecutingMove>();
|
||||
move.User.Returns(user);
|
||||
move.Battle.Returns(battle);
|
||||
|
||||
return (script, move, user, battle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper that returns the single type the user was changed to, or null when
|
||||
/// <see cref="IPokemon.SetTypes"/> was never called.
|
||||
/// </summary>
|
||||
private static TypeIdentifier? GetSetType(IPokemon user)
|
||||
{
|
||||
var call = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "SetTypes");
|
||||
return call != null ? ((IReadOnlyList<TypeIdentifier>)call.GetArguments()[0]!).Single() : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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<IPokemon>(), 0);
|
||||
|
||||
// Assert
|
||||
await Assert.That(GetSetType(user)!.Value).IsEqualTo(expected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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<IPokemon>(), 0);
|
||||
|
||||
// Assert
|
||||
await Assert.That(GetSetType(user)!.Value).IsEqualTo(expected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "On regular terrain" Camouflage changes the user's type to Normal.
|
||||
/// </summary>
|
||||
[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<IPokemon>(), 0);
|
||||
|
||||
// Assert
|
||||
await Assert.That(GetSetType(user)!.Value).IsEqualTo(expected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: Camouflage changes the type of the user, not the target — the target must be
|
||||
/// left untouched.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnSecondaryEffect_TargetTypes_AreNotChanged()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, _, _) = CreateTestSetup(null, "field");
|
||||
var target = Substitute.For<IPokemon>();
|
||||
|
||||
// Act
|
||||
script.OnSecondaryEffect(move, target, 0);
|
||||
|
||||
// Assert
|
||||
await Assert.That(target.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "SetTypes")).IsFalse();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the <see cref="Captivate"/> 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.
|
||||
/// </summary>
|
||||
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<IExecutingMove>();
|
||||
var user = Substitute.For<IPokemon>();
|
||||
user.Gender.Returns(userGender);
|
||||
move.User.Returns(user);
|
||||
|
||||
var target = Substitute.For<IPokemon>();
|
||||
target.Gender.Returns(targetGender);
|
||||
var hitData = Substitute.For<IHitData>();
|
||||
move.GetHitData(target, 0).Returns(hitData);
|
||||
|
||||
return (script, move, user, target, hitData);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper that checks whether a stat boost change was applied to the given Pokémon.
|
||||
/// </summary>
|
||||
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);
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "Captivate lowers the Special Attack stat of the target by two stages, if it is the
|
||||
/// opposite gender of the user."
|
||||
/// </summary>
|
||||
[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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "Pokémon that are gender-unknown [...] cannot be affected."
|
||||
/// </summary>
|
||||
[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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "those with the Oblivious ability cannot be affected."
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnSecondaryEffect_TargetHasOblivious_Fails()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, _, target, hitData) = CreateTestSetup(Gender.Male, Gender.Female);
|
||||
var ability = Substitute.For<IAbility>();
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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();
|
||||
}
|
||||
}
|
||||
149
Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/ChargeTests.cs
Normal file
149
Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/ChargeTests.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the <see cref="Charge"/> move script and the <see cref="ChargeEffect"/> 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."
|
||||
/// </summary>
|
||||
public class ChargeTests
|
||||
{
|
||||
private static (Charge script, IExecutingMove move, IPokemon user, IScriptSet userVolatile) CreateTestSetup()
|
||||
{
|
||||
var script = new Charge();
|
||||
var move = Substitute.For<IExecutingMove>();
|
||||
var user = Substitute.For<IPokemon>();
|
||||
// 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<IEnumerable<ScriptContainer>>()));
|
||||
move.User.Returns(user);
|
||||
return (script, move, user, userVolatile);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an executing move of the given type whose damage modifier can be changed by
|
||||
/// <see cref="ChargeEffect.ChangeDamageModifier"/>.
|
||||
/// </summary>
|
||||
private static (IExecutingMove move, IPokemon target) CreateExecutingMoveOfType(string typeName)
|
||||
{
|
||||
var library = LibraryHelpers.LoadLibrary();
|
||||
var battle = Substitute.For<IBattle>();
|
||||
battle.Library.Returns(library);
|
||||
var battleData = Substitute.For<IPokemonBattleData>();
|
||||
battleData.Battle.Returns(battle);
|
||||
var target = Substitute.For<IPokemon>();
|
||||
target.BattleData.Returns(battleData);
|
||||
|
||||
library.StaticLibrary.Types.TryGetTypeIdentifier(typeName, out var typeIdentifier);
|
||||
var useMove = Substitute.For<IMoveData>();
|
||||
useMove.MoveType.Returns(typeIdentifier);
|
||||
var move = Substitute.For<IExecutingMove>();
|
||||
move.UseMove.Returns(useMove);
|
||||
|
||||
return (move, target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "It also raises the user's Special Defense stat by one stage."
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnSecondaryEffect_Used_RaisesUserSpecialDefenseByOneStage()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, user, _) = CreateTestSetup();
|
||||
|
||||
// Act
|
||||
script.OnSecondaryEffect(move, Substitute.For<IPokemon>(), 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: Charge doubles the power of the user's next Electric-type move. Using Charge attaches
|
||||
/// the <see cref="ChargeEffect"/> volatile script to the user.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnSecondaryEffect_Used_AddsChargeEffectToUser()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, _, userVolatile) = CreateTestSetup();
|
||||
|
||||
// Act
|
||||
script.OnSecondaryEffect(move, Substitute.For<IPokemon>(), 0);
|
||||
|
||||
// Assert
|
||||
await Assert.That(userVolatile.Contains(ScriptUtils.ResolveName<ChargeEffect>())).IsTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "If the user's next move is Electric-type", its power is doubled.
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: only Electric-type moves benefit — a non-Electric move is not boosted by Charge.
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task ChargeEffect_OnEndTurn_RemovesItselfAfterTheNextTurn()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, user, userVolatile) = CreateTestSetup();
|
||||
script.OnSecondaryEffect(move, Substitute.For<IPokemon>(), 0);
|
||||
await Assert.That(userVolatile.TryGet<ChargeEffect>(out var effect)).IsTrue();
|
||||
var battle = Substitute.For<IBattle>();
|
||||
|
||||
// Act & Assert - end of the turn of use: effect remains
|
||||
effect!.OnEndTurn(user, battle);
|
||||
await Assert.That(userVolatile.Contains(ScriptUtils.ResolveName<ChargeEffect>())).IsTrue();
|
||||
|
||||
// Act & Assert - end of the following turn: effect is removed
|
||||
effect.OnEndTurn(user, battle);
|
||||
await Assert.That(userVolatile.Contains(ScriptUtils.ResolveName<ChargeEffect>())).IsFalse();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using PkmnLib.Dynamic.Models;
|
||||
using PkmnLib.Plugin.Gen7.Scripts.Moves;
|
||||
|
||||
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the <see cref="ChipAway"/> 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."
|
||||
/// </summary>
|
||||
public class ChipAwayTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Bulbapedia: Chip Away inflicts damage "ignoring any changes to the target's Defense [...] stat
|
||||
/// stages".
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task BypassDefensiveStatBoosts_Always_Bypasses()
|
||||
{
|
||||
// Arrange
|
||||
var script = new ChipAway();
|
||||
var bypass = false;
|
||||
|
||||
// Act
|
||||
script.BypassDefensiveStatBoosts(Substitute.For<IExecutingMove>(), Substitute.For<IPokemon>(), 0, ref bypass);
|
||||
|
||||
// Assert
|
||||
await Assert.That(bypass).IsTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: Chip Away inflicts damage "ignoring any changes to the target's [...] evasion stat
|
||||
/// stages".
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task BypassEvasionStatBoosts_Always_Bypasses()
|
||||
{
|
||||
// Arrange
|
||||
var script = new ChipAway();
|
||||
var bypass = false;
|
||||
|
||||
// Act
|
||||
script.BypassEvasionStatBoosts(Substitute.For<IExecutingMove>(), Substitute.For<IPokemon>(), 0, ref bypass);
|
||||
|
||||
// Assert
|
||||
await Assert.That(bypass).IsTrue();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the <see cref="Conversion2"/> 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."
|
||||
/// </summary>
|
||||
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<IBattleRandom>();
|
||||
random.GetInt().Returns(0);
|
||||
var battle = Substitute.For<IBattle>();
|
||||
battle.Library.Returns(Library);
|
||||
battle.Random.Returns(random);
|
||||
|
||||
var userBattleData = Substitute.For<IPokemonBattleData>();
|
||||
userBattleData.Battle.Returns(battle);
|
||||
var user = Substitute.For<IPokemon>();
|
||||
user.BattleData.Returns(userBattleData);
|
||||
|
||||
var targetBattleData = Substitute.For<IPokemonBattleData>();
|
||||
if (lastMoveType == null)
|
||||
{
|
||||
targetBattleData.LastMoveChoice.Returns((IMoveChoice?)null);
|
||||
}
|
||||
else
|
||||
{
|
||||
var moveData = Substitute.For<IMoveData>();
|
||||
moveData.MoveType.Returns(lastMoveType.Value);
|
||||
var learnedMove = Substitute.For<ILearnedMove>();
|
||||
learnedMove.MoveData.Returns(moveData);
|
||||
var lastChoice = Substitute.For<IMoveChoice>();
|
||||
lastChoice.ChosenMove.Returns(learnedMove);
|
||||
targetBattleData.LastMoveChoice.Returns(lastChoice);
|
||||
}
|
||||
|
||||
var target = Substitute.For<IPokemon>();
|
||||
target.BattleData.Returns(targetBattleData);
|
||||
|
||||
var move = Substitute.For<IExecutingMove>();
|
||||
move.User.Returns(user);
|
||||
var hitData = Substitute.For<IHitData>();
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper that returns the single type the user was changed to, or null when
|
||||
/// <see cref="IPokemon.SetTypes"/> was never called.
|
||||
/// </summary>
|
||||
private static TypeIdentifier? GetSetType(IPokemon user)
|
||||
{
|
||||
var call = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "SetTypes");
|
||||
return call != null ? ((IReadOnlyList<TypeIdentifier>)call.GetArguments()[0]!).Single() : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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<IReadOnlyList<TypeIdentifier>>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A last move without a real type (type "none") provides no type to resist, so Conversion 2 fails.
|
||||
/// </summary>
|
||||
[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<IReadOnlyList<TypeIdentifier>>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the <see cref="Conversion"/> 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)."
|
||||
/// </summary>
|
||||
public class ConversionTests
|
||||
{
|
||||
private static ILearnedMove CreateLearnedMoveOfType(TypeIdentifier type)
|
||||
{
|
||||
var moveData = Substitute.For<IMoveData>();
|
||||
moveData.MoveType.Returns(type);
|
||||
var learned = Substitute.For<ILearnedMove>();
|
||||
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<IExecutingMove>();
|
||||
var user = Substitute.For<IPokemon>();
|
||||
user.Moves.Returns(moves);
|
||||
move.User.Returns(user);
|
||||
var target = Substitute.For<IPokemon>();
|
||||
var hitData = Substitute.For<IHitData>();
|
||||
move.GetHitData(target, 0).Returns(hitData);
|
||||
return (script, move, user, target, hitData);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "Conversion now changes the user's current type to match the type of the move the user
|
||||
/// has in its first move slot".
|
||||
/// </summary>
|
||||
[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<IReadOnlyList<TypeIdentifier>>(t => t.Count == 1 && t[0] == fireType));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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<IReadOnlyList<TypeIdentifier>>());
|
||||
}
|
||||
}
|
||||
118
Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CopycatTests.cs
Normal file
118
Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CopycatTests.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the <see cref="Copycat"/> 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.
|
||||
/// </summary>
|
||||
public class CopycatTests
|
||||
{
|
||||
private static (Copycat script, IMoveChoice choice) CreateTestSetup(string? lastMoveName)
|
||||
{
|
||||
var script = new Copycat();
|
||||
var user = Substitute.For<IPokemon>();
|
||||
var battleData = Substitute.For<IPokemonBattleData>();
|
||||
if (lastMoveName == null)
|
||||
{
|
||||
battleData.LastMoveChoice.Returns((IMoveChoice?)null);
|
||||
}
|
||||
else
|
||||
{
|
||||
var moveData = Substitute.For<IMoveData>();
|
||||
moveData.Name.Returns(new StringKey(lastMoveName));
|
||||
var learnedMove = Substitute.For<ILearnedMove>();
|
||||
learnedMove.MoveData.Returns(moveData);
|
||||
var lastChoice = Substitute.For<IMoveChoice>();
|
||||
lastChoice.ChosenMove.Returns(learnedMove);
|
||||
battleData.LastMoveChoice.Returns(lastChoice);
|
||||
}
|
||||
user.BattleData.Returns(battleData);
|
||||
|
||||
var choice = Substitute.For<IMoveChoice>();
|
||||
choice.User.Returns(user);
|
||||
return (script, choice);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "Copycat causes the user to use the last move that was used in the battle".
|
||||
/// </summary>
|
||||
[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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "The move fails if no prior move occurred."
|
||||
/// </summary>
|
||||
[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"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia lists moves Copycat cannot copy (e.g. Counter, Copycat itself, Protect). If the last
|
||||
/// used move is one of those, Copycat fails.
|
||||
/// </summary>
|
||||
[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"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Technical test: outside of battle (no battle data) there is no last move, so Copycat fails.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ChangeMove_NoBattleData_Fails()
|
||||
{
|
||||
// Arrange
|
||||
var script = new Copycat();
|
||||
var user = Substitute.For<IPokemon>();
|
||||
user.BattleData.Returns((IPokemonBattleData?)null);
|
||||
var choice = Substitute.For<IMoveChoice>();
|
||||
choice.User.Returns(user);
|
||||
StringKey moveName = "copycat";
|
||||
|
||||
// Act
|
||||
script.ChangeMove(choice, ref moveName);
|
||||
|
||||
// Assert
|
||||
choice.Received(1).Fail();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the <see cref="CoreEnforcer"/> 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."
|
||||
/// </summary>
|
||||
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<IBattle>();
|
||||
var battleData = Substitute.For<IPokemonBattleData>();
|
||||
battleData.Battle.Returns(battle);
|
||||
var target = Substitute.For<IPokemon>();
|
||||
target.BattleData.Returns(battleData);
|
||||
|
||||
var currentChoice = Substitute.For<IMoveChoice>();
|
||||
var move = Substitute.For<IExecutingMove>();
|
||||
move.MoveChoice.Returns(currentChoice);
|
||||
var hitData = Substitute.For<IHitData>();
|
||||
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<IReadOnlyList<ITurnChoice>> { choices });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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."
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void OnSecondaryEffect_TargetMovedEarlierInTurn_SuppressesTargetAbility()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, currentChoice, target, hitData, battle) = CreateTestSetup();
|
||||
var targetChoice = Substitute.For<IMoveChoice>();
|
||||
targetChoice.User.Returns(target);
|
||||
SetTurnChoices(battle, targetChoice, currentChoice);
|
||||
|
||||
// Act
|
||||
script.OnSecondaryEffect(move, target, 0);
|
||||
|
||||
// Assert
|
||||
target.Received(1).SuppressAbility();
|
||||
hitData.DidNotReceive().Fail();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: the suppression also happens if the target "had a Bag item used on it by its Trainer in
|
||||
/// the same turn".
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void OnSecondaryEffect_ItemUsedEarlierInTurn_SuppressesTargetAbility()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, currentChoice, target, hitData, battle) = CreateTestSetup();
|
||||
var itemChoice = Substitute.For<IItemChoice>();
|
||||
itemChoice.User.Returns(target);
|
||||
SetTurnChoices(battle, itemChoice, currentChoice);
|
||||
|
||||
// Act
|
||||
script.OnSecondaryEffect(move, target, 0);
|
||||
|
||||
// Assert
|
||||
target.Received(1).SuppressAbility();
|
||||
hitData.DidNotReceive().Fail();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void OnSecondaryEffect_TargetActsLaterInTurn_DoesNotSuppressAbility()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, currentChoice, target, hitData, battle) = CreateTestSetup();
|
||||
var targetChoice = Substitute.For<IMoveChoice>();
|
||||
targetChoice.User.Returns(target);
|
||||
SetTurnChoices(battle, currentChoice, targetChoice);
|
||||
|
||||
// Act
|
||||
script.OnSecondaryEffect(move, target, 0);
|
||||
|
||||
// Assert
|
||||
target.DidNotReceive().SuppressAbility();
|
||||
hitData.Received(1).Fail();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: suppression requires the target to have already acted — as the very first action of the
|
||||
/// turn, Core Enforcer never suppresses.
|
||||
/// </summary>
|
||||
[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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Technical test: a target without battle data (not in battle) is left untouched.
|
||||
/// </summary>
|
||||
[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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void OnSecondaryEffect_OnlyOtherPokemonActedEarlier_DoesNotSuppressAbility()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, currentChoice, target, hitData, battle) = CreateTestSetup();
|
||||
var allyChoice = Substitute.For<IMoveChoice>();
|
||||
allyChoice.User.Returns(Substitute.For<IPokemon>());
|
||||
SetTurnChoices(battle, allyChoice, currentChoice);
|
||||
|
||||
// Act
|
||||
script.OnSecondaryEffect(move, target, 0);
|
||||
|
||||
// Assert
|
||||
target.DidNotReceive().SuppressAbility();
|
||||
hitData.Received(1).Fail();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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; <see cref="IPokemon.SuppressAbility"/> refuses
|
||||
/// it when <see cref="IAbility.CanBeChanged"/> is false, so these abilities must carry that flag in the
|
||||
/// Gen7 data.
|
||||
/// </summary>
|
||||
[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();
|
||||
}
|
||||
}
|
||||
194
Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CounterTests.cs
Normal file
194
Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CounterTests.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the <see cref="Counter"/> move script and its <see cref="CounterHelperEffect"/>.
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class CounterTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a user whose volatile scripts contain a <see cref="CounterHelperEffect"/> that has recorded
|
||||
/// an incoming physical hit of the given damage by <paramref name="attacker"/>.
|
||||
/// </summary>
|
||||
private static IPokemon CreateUserHitBy(IPokemon? attacker, uint damage, bool physical = true)
|
||||
{
|
||||
var user = Substitute.For<IPokemon>();
|
||||
var userVolatile = new ScriptSet(user);
|
||||
user.Volatile.Returns(userVolatile);
|
||||
user.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));
|
||||
|
||||
var helper = new CounterHelperEffect();
|
||||
userVolatile.Add(helper);
|
||||
if (attacker != null)
|
||||
{
|
||||
var incomingMove = Substitute.For<IExecutingMove>();
|
||||
var useMove = Substitute.For<IMoveData>();
|
||||
useMove.Category.Returns(physical ? MoveCategory.Physical : MoveCategory.Special);
|
||||
incomingMove.UseMove.Returns(useMove);
|
||||
incomingMove.User.Returns(attacker);
|
||||
var incomingHit = Substitute.For<IHitData>();
|
||||
incomingHit.Damage.Returns(damage);
|
||||
incomingMove.GetHitData(user, 0).Returns(incomingHit);
|
||||
helper.OnIncomingHit(incomingMove, user, 0);
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Counter watches for incoming hits over the whole turn: at the start of the turn it attaches its
|
||||
/// <see cref="CounterHelperEffect"/> to the user.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnBeforeTurnStart_Always_AddsCounterHelperEffectToUser()
|
||||
{
|
||||
// Arrange
|
||||
var script = new Counter();
|
||||
var user = Substitute.For<IPokemon>();
|
||||
var userVolatile = new ScriptSet(user);
|
||||
user.Volatile.Returns(userVolatile);
|
||||
user.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));
|
||||
var choice = Substitute.For<IMoveChoice>();
|
||||
choice.User.Returns(user);
|
||||
|
||||
// Act
|
||||
script.OnBeforeTurnStart(choice);
|
||||
|
||||
// Assert
|
||||
await Assert.That(userVolatile.Contains(ScriptUtils.ResolveName<CounterHelperEffect>())).IsTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: Counter strikes back at the Pokémon that last hit the user with a physical move — the
|
||||
/// move is redirected to that attacker.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task ChangeTargets_UserWasHitByPhysicalMove_TargetsTheAttacker()
|
||||
{
|
||||
// Arrange
|
||||
var script = new Counter();
|
||||
var attacker = Substitute.For<IPokemon>();
|
||||
var user = CreateUserHitBy(attacker, 40);
|
||||
var choice = Substitute.For<IMoveChoice>();
|
||||
choice.User.Returns(user);
|
||||
IReadOnlyList<IPokemon?> targets = new IPokemon?[] { Substitute.For<IPokemon>() };
|
||||
|
||||
// Act
|
||||
script.ChangeTargets(choice, ref targets);
|
||||
|
||||
// Assert
|
||||
await Assert.That(targets.Single()).IsEqualTo(attacker);
|
||||
choice.DidNotReceive().Fail();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: Counter fails when the user has not been hit by a physical move this turn.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ChangeTargets_UserWasNotHit_Fails()
|
||||
{
|
||||
// Arrange
|
||||
var script = new Counter();
|
||||
var user = CreateUserHitBy(null, 0);
|
||||
var choice = Substitute.For<IMoveChoice>();
|
||||
choice.User.Returns(user);
|
||||
IReadOnlyList<IPokemon?> targets = new IPokemon?[] { Substitute.For<IPokemon>() };
|
||||
|
||||
// Act
|
||||
script.ChangeTargets(choice, ref targets);
|
||||
|
||||
// Assert
|
||||
choice.Received(1).Fail();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia (Generation II onwards): "Counter now affects all physical moves, dealing double
|
||||
/// damage." The damage dealt is twice the damage of the recorded hit.
|
||||
/// </summary>
|
||||
[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<IPokemon>();
|
||||
var user = CreateUserHitBy(attacker, damageTaken);
|
||||
var move = Substitute.For<IExecutingMove>();
|
||||
move.User.Returns(user);
|
||||
uint damage = 0;
|
||||
|
||||
// Act
|
||||
script.ChangeMoveDamage(move, attacker, 0, ref damage);
|
||||
|
||||
// Assert
|
||||
await Assert.That(damage).IsEqualTo(expectedDamage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: Counter retaliates against the Pokémon that hit the user — against any other target the
|
||||
/// hit fails.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ChangeMoveDamage_TargetIsNotTheLastAttacker_FailsHit()
|
||||
{
|
||||
// Arrange
|
||||
var script = new Counter();
|
||||
var attacker = Substitute.For<IPokemon>();
|
||||
var user = CreateUserHitBy(attacker, 40);
|
||||
var someoneElse = Substitute.For<IPokemon>();
|
||||
var move = Substitute.For<IExecutingMove>();
|
||||
move.User.Returns(user);
|
||||
var hitData = Substitute.For<IHitData>();
|
||||
move.GetHitData(someoneElse, 0).Returns(hitData);
|
||||
uint damage = 0;
|
||||
|
||||
// Act
|
||||
script.ChangeMoveDamage(move, someoneElse, 0, ref damage);
|
||||
|
||||
// Assert
|
||||
hitData.Received(1).Fail();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: only physical moves can be countered — the helper records physical hits.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task CounterHelperEffect_PhysicalHit_RecordsAttackerAndDamage()
|
||||
{
|
||||
// Arrange
|
||||
var attacker = Substitute.For<IPokemon>();
|
||||
var user = CreateUserHitBy(attacker, 40);
|
||||
|
||||
// Assert
|
||||
var helper = ((IScriptSet)user.Volatile).Get<CounterHelperEffect>();
|
||||
await Assert.That(helper!.LastHitBy).IsEqualTo(attacker);
|
||||
await Assert.That(helper.LastDamage).IsEqualTo(40u);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia (Generation IV onwards): special moves cannot be countered — the helper ignores
|
||||
/// non-physical hits, so Counter fails after a special hit.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task CounterHelperEffect_SpecialHit_IsNotRecorded()
|
||||
{
|
||||
// Arrange
|
||||
var attacker = Substitute.For<IPokemon>();
|
||||
var user = CreateUserHitBy(attacker, 40, false);
|
||||
|
||||
// Assert
|
||||
var helper = ((IScriptSet)user.Volatile).Get<CounterHelperEffect>();
|
||||
await Assert.That(helper!.LastHitBy).IsNull();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
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="Covet"/> 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."
|
||||
/// </summary>
|
||||
public class CovetTests
|
||||
{
|
||||
private static (Covet script, IExecutingMove move, IPokemon user, IPokemon target) CreateTestSetup(
|
||||
IItem? targetItem)
|
||||
{
|
||||
var script = new Covet();
|
||||
var move = Substitute.For<IExecutingMove>();
|
||||
var user = Substitute.For<IPokemon>();
|
||||
// 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<IPokemon>();
|
||||
target.HeldItem.Returns(targetItem);
|
||||
if (targetItem != null)
|
||||
{
|
||||
target.TryStealHeldItem(out Arg.Any<IItem?>()).Returns(x =>
|
||||
{
|
||||
x[0] = targetItem;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
return (script, move, user, target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void OnSecondaryEffect_TargetHoldsItem_UserStealsIt()
|
||||
{
|
||||
// Arrange
|
||||
var item = Substitute.For<IItem>();
|
||||
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<IItem?>());
|
||||
user.Received(1).ForceSetHeldItem(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "If the target is not holding an item [...], Covet cannot steal an item."
|
||||
/// </summary>
|
||||
[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<IItem?>());
|
||||
user.DidNotReceive().ForceSetHeldItem(Arg.Any<IItem?>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "If [...] the user is already holding an item, Covet cannot steal an item."
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void OnSecondaryEffect_UserAlreadyHoldsItem_TargetItemIsNotStolen()
|
||||
{
|
||||
// Arrange
|
||||
var targetItem = Substitute.For<IItem>();
|
||||
var (script, move, user, target) = CreateTestSetup(targetItem);
|
||||
user.HeldItem.Returns(Substitute.For<IItem>());
|
||||
|
||||
// Act
|
||||
script.OnSecondaryEffect(move, target, 0);
|
||||
|
||||
// Assert - the user must not end up with the target's item
|
||||
user.DidNotReceive().ForceSetHeldItem(targetItem);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the <see cref="CraftyShield"/> move script and the <see cref="CraftyShieldEffect"/> it
|
||||
/// applies. Gen VII Bulbapedia behavior: "Crafty Shield protects all Pokémon on the user's side of the
|
||||
/// field from status moves."
|
||||
/// </summary>
|
||||
public class CraftyShieldTests
|
||||
{
|
||||
private static (CraftyShield script, IExecutingMove move, IScriptSet sideVolatile) CreateTestSetup()
|
||||
{
|
||||
var script = new CraftyShield();
|
||||
|
||||
var side = Substitute.For<IBattleSide>();
|
||||
// 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<IEnumerable<ScriptContainer>>()));
|
||||
|
||||
var battle = Substitute.For<IBattle>();
|
||||
battle.Sides.Returns(new[] { side });
|
||||
var battleData = Substitute.For<IPokemonBattleData>();
|
||||
battleData.Battle.Returns(battle);
|
||||
battleData.SideIndex.Returns((byte)0);
|
||||
|
||||
var user = Substitute.For<IPokemon>();
|
||||
user.BattleData.Returns(battleData);
|
||||
var move = Substitute.For<IExecutingMove>();
|
||||
move.User.Returns(user);
|
||||
|
||||
return (script, move, sideVolatile);
|
||||
}
|
||||
|
||||
private static IExecutingMove CreateExecutingMoveOfCategory(MoveCategory category)
|
||||
{
|
||||
var useMove = Substitute.For<IMoveData>();
|
||||
useMove.Category.Returns(category);
|
||||
var move = Substitute.For<IExecutingMove>();
|
||||
move.UseMove.Returns(useMove);
|
||||
return move;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "Crafty Shield protects all Pokémon on the user's side of the field" — using the move
|
||||
/// attaches the <see cref="CraftyShieldEffect"/> to the user's side.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnSecondaryEffect_Used_AddsCraftyShieldEffectToUsersSide()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, sideVolatile) = CreateTestSetup();
|
||||
|
||||
// Act
|
||||
script.OnSecondaryEffect(move, Substitute.For<IPokemon>(), 0);
|
||||
|
||||
// Assert
|
||||
await Assert.That(sideVolatile.Contains(ScriptUtils.ResolveName<CraftyShieldEffect>())).IsTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: the shield protects "from status moves" — an incoming status move is stopped.
|
||||
/// </summary>
|
||||
[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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: Crafty Shield only blocks status moves — damaging moves go through it unhindered.
|
||||
/// </summary>
|
||||
[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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Technical test: outside of battle (no battle data) no shield can be raised and nothing happens.
|
||||
/// </summary>
|
||||
[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<IPokemon>(), 0);
|
||||
|
||||
// Assert - no effect is added
|
||||
await Assert.That(sideVolatile.Contains(ScriptUtils.ResolveName<CraftyShieldEffect>())).IsFalse();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
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="CrushGrip"/> 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.
|
||||
/// </summary>
|
||||
public class CrushGripTests
|
||||
{
|
||||
private static (CrushGrip script, IExecutingMove move, IPokemon target) CreateTestSetup(uint currentHealth,
|
||||
uint maxHealth)
|
||||
{
|
||||
var script = new CrushGrip();
|
||||
var move = Substitute.For<IExecutingMove>();
|
||||
var target = Substitute.For<IPokemon>();
|
||||
target.CurrentHealth.Returns(currentHealth);
|
||||
target.BoostedStats.Returns(new StatisticSet<uint>(maxHealth, 1, 1, 1, 1, 1));
|
||||
return (script, move, target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
}
|
||||
212
Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CurseTests.cs
Normal file
212
Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CurseTests.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the <see cref="Curse"/> move script and the <see cref="GhostCurseEffect"/> 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."
|
||||
/// </summary>
|
||||
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<IBattle>();
|
||||
battle.Library.Returns(library);
|
||||
var battleData = Substitute.For<IPokemonBattleData>();
|
||||
battleData.Battle.Returns(battle);
|
||||
|
||||
var user = Substitute.For<IPokemon>();
|
||||
user.BattleData.Returns(battleData);
|
||||
user.Types.Returns(new[] { userIsGhost ? ghostType : normalType });
|
||||
user.MaxHealth.Returns(userMaxHealth);
|
||||
user.CurrentHealth.Returns(userCurrentHealth);
|
||||
var move = Substitute.For<IExecutingMove>();
|
||||
move.User.Returns(user);
|
||||
|
||||
var target = Substitute.For<IPokemon>();
|
||||
// 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<IEnumerable<ScriptContainer>>()));
|
||||
|
||||
return (script, move, user, target, targetVolatile);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper that checks whether a stat boost change was applied to the given Pokémon.
|
||||
/// </summary>
|
||||
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);
|
||||
|
||||
/// <summary>
|
||||
/// Helper to extract the damage amount from a substitute's received Damage calls.
|
||||
/// </summary>
|
||||
private static uint? GetDamageAmount(IPokemon pokemon)
|
||||
{
|
||||
var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage");
|
||||
return call != null ? (uint)call.GetArguments()[0]! : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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."
|
||||
/// </summary>
|
||||
[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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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<GhostCurseEffect>())).IsFalse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia (Ghost-type user): the user pays HP to "put a curse on the target" — the
|
||||
/// <see cref="GhostCurseEffect"/> is attached to the target.
|
||||
/// </summary>
|
||||
[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<GhostCurseEffect>())).IsTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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<GhostCurseEffect>())).IsTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task GhostCurseEffect_OnEndTurnAtFullHp_CursedPokemonLosesQuarterOfMaximumHp()
|
||||
{
|
||||
// Arrange
|
||||
var cursed = Substitute.For<IPokemon>();
|
||||
cursed.MaxHealth.Returns(100u);
|
||||
cursed.CurrentHealth.Returns(100u);
|
||||
var effect = new GhostCurseEffect(cursed);
|
||||
|
||||
// Act
|
||||
effect.OnEndTurn(Substitute.For<IScriptSource>(), Substitute.For<IBattle>());
|
||||
|
||||
// Assert
|
||||
await Assert.That(GetDamageAmount(cursed)!.Value).IsEqualTo(25u);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task GhostCurseEffect_OnEndTurnAtLowHp_CursedPokemonStillLosesQuarterOfMaximumHp()
|
||||
{
|
||||
// Arrange
|
||||
var cursed = Substitute.For<IPokemon>();
|
||||
cursed.MaxHealth.Returns(100u);
|
||||
cursed.CurrentHealth.Returns(40u);
|
||||
var effect = new GhostCurseEffect(cursed);
|
||||
|
||||
// Act
|
||||
effect.OnEndTurn(Substitute.For<IScriptSource>(), Substitute.For<IBattle>());
|
||||
|
||||
// Assert
|
||||
await Assert.That(GetDamageAmount(cursed)!.Value).IsEqualTo(25u);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -13,12 +13,12 @@ public class GhostCurseEffect : Script, IScriptOnEndTurn, IAIInfoScriptExpectedE
|
||||
/// <inheritdoc />
|
||||
public void OnEndTurn(IScriptSource owner, IBattle battle)
|
||||
{
|
||||
_pokemon.Damage(_pokemon.CurrentHealth / 4, DamageSource.Misc);
|
||||
_pokemon.Damage(_pokemon.MaxHealth / 4, DamageSource.Misc);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ExpectedEndOfTurnDamage(IPokemon pokemon, ref int damage)
|
||||
{
|
||||
damage += (int)(_pokemon.CurrentHealth / 4f);
|
||||
damage += (int)(_pokemon.MaxHealth / 4f);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user