This commit is contained in:
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user