More unit tests, fixes
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
using PkmnLib.Dynamic.Models;
|
||||
using PkmnLib.Dynamic.Models.Choices;
|
||||
using PkmnLib.Dynamic.ScriptHandling;
|
||||
using PkmnLib.Dynamic.ScriptHandling.Registry;
|
||||
using PkmnLib.Plugin.Gen7.Scripts.Moves;
|
||||
using PkmnLib.Plugin.Gen7.Scripts.Side;
|
||||
using PkmnLib.Static.Moves;
|
||||
using PkmnLib.Static.Utils;
|
||||
|
||||
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the <see cref="EchoedVoice"/> move script and its <see cref="EchoedVoiceData"/> side marker.
|
||||
/// Gen VII Bulbapedia behavior: "The move's base power increases by 40 each consecutive turn when used
|
||||
/// repeatedly, reaching a maximum of 200. Power increases only once per round, regardless of how many
|
||||
/// Pokémon deploy it."
|
||||
/// </summary>
|
||||
public class EchoedVoiceTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a mocked move choice whose chosen move has the given name.
|
||||
/// </summary>
|
||||
private static IMoveChoice CreateMoveChoice(string moveName)
|
||||
{
|
||||
var moveData = Substitute.For<IMoveData>();
|
||||
moveData.Name.Returns(new StringKey(moveName));
|
||||
var learnedMove = Substitute.For<ILearnedMove>();
|
||||
learnedMove.MoveData.Returns(moveData);
|
||||
var choice = Substitute.For<IMoveChoice>();
|
||||
choice.ChosenMove.Returns(learnedMove);
|
||||
return choice;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a fully mocked test setup where the user's side has a real <see cref="ScriptSet"/> as its
|
||||
/// volatile scripts.
|
||||
/// </summary>
|
||||
private static (EchoedVoice script, IExecutingMove move, IPokemon target, IScriptSet sideScripts) CreateTestSetup()
|
||||
{
|
||||
var script = new EchoedVoice();
|
||||
var move = Substitute.For<IExecutingMove>();
|
||||
var target = Substitute.For<IPokemon>();
|
||||
|
||||
var side = Substitute.For<IBattleSide>();
|
||||
side.GetScripts().Returns(_ => new ScriptIterator(Array.Empty<IEnumerable<ScriptContainer>>()));
|
||||
IScriptSet sideScripts = new ScriptSet(side);
|
||||
side.VolatileScripts.Returns(sideScripts);
|
||||
|
||||
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);
|
||||
move.User.Returns(user);
|
||||
|
||||
return (script, move, target, sideScripts);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "The move's base power increases by 40 each consecutive turn" — on the first use there
|
||||
/// is no previous turn, so the base power is not modified.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task ChangeBasePower_FirstUse_BasePowerUnchanged()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, target, _) = CreateTestSetup();
|
||||
ushort basePower = 40;
|
||||
|
||||
// Act
|
||||
script.ChangeBasePower(move, target, 0, ref basePower);
|
||||
|
||||
// Assert
|
||||
await Assert.That(basePower).IsEqualTo((ushort)40);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "The move's base power increases by 40 each consecutive turn when used repeatedly,
|
||||
/// reaching a maximum of 200." Each previous consecutive use adds a stack worth 40 power: 80 on the
|
||||
/// second turn, 120 on the third, 160 on the fourth, and 200 on the fifth.
|
||||
/// </summary>
|
||||
[Test, Arguments(1, (ushort)80), Arguments(2, (ushort)120), Arguments(3, (ushort)160), Arguments(4, (ushort)200)]
|
||||
public async Task ChangeBasePower_ConsecutiveUses_PowerIncreasesByFortyPerTurn(int previousUses,
|
||||
ushort expectedPower)
|
||||
{
|
||||
// Arrange - the move hit on the given number of previous consecutive turns
|
||||
var (script, move, target, _) = CreateTestSetup();
|
||||
for (var i = 0; i < previousUses; i++)
|
||||
script.OnSecondaryEffect(move, target, 0);
|
||||
ushort basePower = 40;
|
||||
|
||||
// Act
|
||||
script.ChangeBasePower(move, target, 0, ref basePower);
|
||||
|
||||
// Assert
|
||||
await Assert.That(basePower).IsEqualTo(expectedPower);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: the base power increases "reaching a maximum of 200" — further consecutive uses beyond
|
||||
/// the fifth turn do not push the power past the cap.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task ChangeBasePower_MoreThanFourPreviousUses_PowerCappedAtTwoHundred()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, target, _) = CreateTestSetup();
|
||||
for (var i = 0; i < 6; i++)
|
||||
script.OnSecondaryEffect(move, target, 0);
|
||||
ushort basePower = 40;
|
||||
|
||||
// Act
|
||||
script.ChangeBasePower(move, target, 0, ref basePower);
|
||||
|
||||
// Assert
|
||||
await Assert.That(basePower).IsEqualTo((ushort)200);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: the power increase applies "when used repeatedly" — using the move marks the user's side
|
||||
/// with <see cref="EchoedVoiceData"/> so the next turn's use is recognized as consecutive.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnSecondaryEffect_AddsEchoedVoiceDataToUserSide()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, target, sideScripts) = CreateTestSetup();
|
||||
|
||||
// Act
|
||||
script.OnSecondaryEffect(move, target, 0);
|
||||
|
||||
// Assert
|
||||
await Assert.That(sideScripts.Contains(ScriptUtils.ResolveName<EchoedVoiceData>())).IsTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "The move's base power increases by 40 each consecutive turn" — each additional use
|
||||
/// stacks the side marker, increasing <see cref="EchoedVoiceData.Stacks"/>.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnSecondaryEffect_UsedAgain_StacksEchoedVoiceData()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, target, sideScripts) = CreateTestSetup();
|
||||
|
||||
// Act
|
||||
script.OnSecondaryEffect(move, target, 0);
|
||||
script.OnSecondaryEffect(move, target, 0);
|
||||
|
||||
// Assert
|
||||
await Assert.That(sideScripts.Get<EchoedVoiceData>()!.Stacks).IsEqualTo(2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Technical test: without battle data on the user (outside of battle) the base power hook does
|
||||
/// nothing and does not throw.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task ChangeBasePower_UserHasNoBattleData_BasePowerUnchanged()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, target, _) = CreateTestSetup();
|
||||
move.User.BattleData.Returns((IPokemonBattleData?)null);
|
||||
ushort basePower = 40;
|
||||
|
||||
// Act
|
||||
script.ChangeBasePower(move, target, 0, ref basePower);
|
||||
|
||||
// Assert
|
||||
await Assert.That(basePower).IsEqualTo((ushort)40);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: the boost only applies on "consecutive" turns — when a Pokémon on the side chooses a
|
||||
/// move other than Echoed Voice, the <see cref="EchoedVoiceData"/> marker removes itself, resetting
|
||||
/// the power.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnBeforeMoveChoice_DifferentMoveChosen_MarkerRemovesItself()
|
||||
{
|
||||
// Arrange
|
||||
var side = Substitute.For<IBattleSide>();
|
||||
side.GetScripts().Returns(_ => new ScriptIterator(Array.Empty<IEnumerable<ScriptContainer>>()));
|
||||
IScriptSet sideScripts = new ScriptSet(side);
|
||||
var data = new EchoedVoiceData();
|
||||
sideScripts.Add(data);
|
||||
|
||||
// Act
|
||||
data.OnBeforeMoveChoice(CreateMoveChoice("tackle"));
|
||||
|
||||
// Assert
|
||||
await Assert.That(sideScripts.Contains(ScriptUtils.ResolveName<EchoedVoiceData>())).IsFalse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "The move's base power increases by 40 each consecutive turn when used repeatedly" —
|
||||
/// choosing Echoed Voice again keeps the marker (and its stacks) alive.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnBeforeMoveChoice_EchoedVoiceChosen_MarkerStaysActive()
|
||||
{
|
||||
// Arrange
|
||||
var side = Substitute.For<IBattleSide>();
|
||||
side.GetScripts().Returns(_ => new ScriptIterator(Array.Empty<IEnumerable<ScriptContainer>>()));
|
||||
IScriptSet sideScripts = new ScriptSet(side);
|
||||
var data = new EchoedVoiceData();
|
||||
sideScripts.Add(data);
|
||||
|
||||
// Act
|
||||
data.OnBeforeMoveChoice(CreateMoveChoice("echoed_voice"));
|
||||
|
||||
// Assert
|
||||
await Assert.That(sideScripts.Contains(ScriptUtils.ResolveName<EchoedVoiceData>())).IsTrue();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
using PkmnLib.Dynamic.Models;
|
||||
using PkmnLib.Dynamic.ScriptHandling.Registry;
|
||||
using PkmnLib.Plugin.Gen7.Scripts.Moves;
|
||||
using ElectricTerrainScript = PkmnLib.Plugin.Gen7.Scripts.Terrain.ElectricTerrain;
|
||||
|
||||
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the <see cref="ElectricTerrain"/> move script.
|
||||
/// Gen VII Bulbapedia behavior (Generations VI to VII): Electric Terrain "envelops the field and replaces
|
||||
/// the background environment and any other terrain that is already in effect."
|
||||
/// </summary>
|
||||
public class ElectricTerrainTests
|
||||
{
|
||||
private static (ElectricTerrain script, IExecutingMove move, IBattle battle) CreateTestSetup()
|
||||
{
|
||||
var script = new ElectricTerrain();
|
||||
var move = Substitute.For<IExecutingMove>();
|
||||
|
||||
var battle = Substitute.For<IBattle>();
|
||||
var battleData = Substitute.For<IPokemonBattleData>();
|
||||
battleData.Battle.Returns(battle);
|
||||
|
||||
var user = Substitute.For<IPokemon>();
|
||||
user.BattleData.Returns(battleData);
|
||||
move.User.Returns(user);
|
||||
|
||||
return (script, move, battle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: Electric Terrain "envelops the field and replaces the background environment and any
|
||||
/// other terrain that is already in effect" — the move sets the battle's terrain to the
|
||||
/// <see cref="ElectricTerrainScript"/> terrain script.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void OnSecondaryEffect_SetsElectricTerrainOnBattle()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, battle) = CreateTestSetup();
|
||||
|
||||
// Act
|
||||
script.OnSecondaryEffect(move, Substitute.For<IPokemon>(), 0);
|
||||
|
||||
// Assert
|
||||
battle.Received(1).SetTerrain(ScriptUtils.ResolveName<ElectricTerrainScript>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Technical test: without battle data on the user (outside of battle) the effect does nothing and
|
||||
/// does not throw.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void OnSecondaryEffect_UserHasNoBattleData_DoesNotSetTerrain()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, battle) = CreateTestSetup();
|
||||
move.User.BattleData.Returns((IPokemonBattleData?)null);
|
||||
|
||||
// Act
|
||||
script.OnSecondaryEffect(move, Substitute.For<IPokemon>(), 0);
|
||||
|
||||
// Assert
|
||||
battle.DidNotReceiveWithAnyArgs().SetTerrain(default);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
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.MoveVolatile;
|
||||
using PkmnLib.Static;
|
||||
|
||||
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the <see cref="Electrify"/> move script and its <see cref="ElectrifyEffect"/> move volatile.
|
||||
/// Gen VII Bulbapedia behavior: "If the target has not yet used a move, Electrify makes the target's move
|
||||
/// Electric-type for that turn, even if it is a status move."
|
||||
/// </summary>
|
||||
public class ElectrifyTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a fully mocked test setup where the battle's choice queue is a real
|
||||
/// <see cref="BattleChoiceQueue"/>. When <paramref name="targetStillHasChoice"/> is false, the queue
|
||||
/// only contains another Pokémon's choice, simulating a target that already moved this turn.
|
||||
/// </summary>
|
||||
private static (Electrify script, IExecutingMove move, IPokemon target, IScriptSet choiceVolatile, IHitData hitData)
|
||||
CreateTestSetup(bool targetStillHasChoice = true, bool hasQueue = true)
|
||||
{
|
||||
var script = new Electrify();
|
||||
var move = Substitute.For<IExecutingMove>();
|
||||
var target = Substitute.For<IPokemon>();
|
||||
var hitData = Substitute.For<IHitData>();
|
||||
move.GetHitData(target, 0).Returns(hitData);
|
||||
|
||||
var targetChoice = Substitute.For<IMoveChoice>();
|
||||
targetChoice.GetScripts().Returns(_ => new ScriptIterator(Array.Empty<IEnumerable<ScriptContainer>>()));
|
||||
IScriptSet choiceVolatile = new ScriptSet(targetChoice);
|
||||
targetChoice.Volatile.Returns(choiceVolatile);
|
||||
targetChoice.User.Returns(target);
|
||||
|
||||
var queue = new BattleChoiceQueue(targetStillHasChoice ? [targetChoice] : [Substitute.For<IMoveChoice>()]);
|
||||
|
||||
var battle = Substitute.For<IBattle>();
|
||||
battle.ChoiceQueue.Returns(hasQueue ? queue : null);
|
||||
|
||||
var battleData = Substitute.For<IPokemonBattleData>();
|
||||
battleData.Battle.Returns(battle);
|
||||
target.BattleData.Returns(battleData);
|
||||
|
||||
return (script, move, target, choiceVolatile, hitData);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "If the target has not yet used a move, Electrify makes the target's move Electric-type
|
||||
/// for that turn" — the <see cref="ElectrifyEffect"/> is attached to the target's pending move choice.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnSecondaryEffect_TargetHasNotMovedYet_AddsElectrifyEffectToTargetChoice()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, target, choiceVolatile, hitData) = CreateTestSetup();
|
||||
|
||||
// Act
|
||||
script.OnSecondaryEffect(move, target, 0);
|
||||
|
||||
// Assert
|
||||
await Assert.That(choiceVolatile.Contains(ScriptUtils.ResolveName<ElectrifyEffect>())).IsTrue();
|
||||
hitData.DidNotReceive().Fail();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: the effect only applies "if the target has not yet used a move" — when the target has
|
||||
/// no remaining choice in the queue this turn, the move fails.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void OnSecondaryEffect_TargetAlreadyMoved_FailsHit()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, target, _, hitData) = CreateTestSetup(false);
|
||||
|
||||
// Act
|
||||
script.OnSecondaryEffect(move, target, 0);
|
||||
|
||||
// Assert
|
||||
hitData.Received(1).Fail();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Technical test: when the battle has no active choice queue, the script returns without throwing and
|
||||
/// without failing the hit.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void OnSecondaryEffect_NoChoiceQueue_DoesNothing()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, target, _, hitData) = CreateTestSetup(hasQueue: false);
|
||||
|
||||
// Act
|
||||
script.OnSecondaryEffect(move, target, 0);
|
||||
|
||||
// Assert
|
||||
hitData.DidNotReceive().Fail();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "Electrify makes the target's move Electric-type for that turn, even if it is a status
|
||||
/// move" — the <see cref="ElectrifyEffect"/> changes the executed move's type to Electric.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task ChangeMoveType_ChangesMoveTypeToElectric()
|
||||
{
|
||||
// Arrange
|
||||
var effect = new ElectrifyEffect();
|
||||
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);
|
||||
|
||||
await Assert.That(library.StaticLibrary.Types.TryGetTypeIdentifier("normal", out var normal)).IsTrue();
|
||||
await Assert.That(library.StaticLibrary.Types.TryGetTypeIdentifier("electric", out var electric)).IsTrue();
|
||||
TypeIdentifier? moveType = normal;
|
||||
|
||||
// Act
|
||||
effect.ChangeMoveType(Substitute.For<IExecutingMove>(), target, 0, ref moveType);
|
||||
|
||||
// Assert
|
||||
await Assert.That(moveType).IsNotNull();
|
||||
await Assert.That(moveType!.Value).IsEqualTo(electric);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Technical test: without battle data on the target the move type cannot be resolved and remains
|
||||
/// unchanged.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task ChangeMoveType_TargetHasNoBattleData_TypeUnchanged()
|
||||
{
|
||||
// Arrange
|
||||
var effect = new ElectrifyEffect();
|
||||
var target = Substitute.For<IPokemon>();
|
||||
target.BattleData.Returns((IPokemonBattleData?)null);
|
||||
TypeIdentifier? moveType = null;
|
||||
|
||||
// Act
|
||||
effect.ChangeMoveType(Substitute.For<IExecutingMove>(), target, 0, ref moveType);
|
||||
|
||||
// Assert
|
||||
await Assert.That(moveType).IsNull();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
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="ElectroBall"/> move script.
|
||||
/// Gen VII Bulbapedia behavior: "Electro Ball inflicts more damage the faster the user is compared to the
|
||||
/// target." The power is based on the target's Speed as a percentage of the user's Speed:
|
||||
/// more than 100% (or exactly 0) → 40, 50.01%–100% → 60, 33.34%–50% → 80, 25.01%–33.33% → 120,
|
||||
/// 0.01%–25% → 150. "The Speed values used take all modifiers into account (including stat stages,
|
||||
/// paralysis, held items [...], and Abilities [...])."
|
||||
/// </summary>
|
||||
public class ElectroBallTests
|
||||
{
|
||||
private static (ElectroBall script, IExecutingMove move, IPokemon target) CreateTestSetup(uint userSpeed,
|
||||
uint targetSpeed)
|
||||
{
|
||||
var script = new ElectroBall();
|
||||
var move = Substitute.For<IExecutingMove>();
|
||||
var user = Substitute.For<IPokemon>();
|
||||
user.BoostedStats.Returns(new StatisticSet<uint>(1, 1, 1, 1, 1, userSpeed));
|
||||
move.User.Returns(user);
|
||||
var target = Substitute.For<IPokemon>();
|
||||
target.BoostedStats.Returns(new StatisticSet<uint>(1, 1, 1, 1, 1, targetSpeed));
|
||||
return (script, move, target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: the power scales with the target's Speed as a percentage of the user's Speed —
|
||||
/// more than 100% → 40, 50.01%–100% → 60, 33.34%–50% → 80, 25.01%–33.33% → 120, 0.01%–25% → 150.
|
||||
/// </summary>
|
||||
[Test, Arguments(100u, 150u, (ushort)40), Arguments(100u, 101u, (ushort)40), Arguments(100u, 70u, (ushort)60),
|
||||
Arguments(100u, 40u, (ushort)80), Arguments(100u, 30u, (ushort)120), Arguments(100u, 20u, (ushort)150),
|
||||
Arguments(500u, 100u, (ushort)150)]
|
||||
public async Task ChangeBasePower_ScalesWithSpeedRatio(uint userSpeed, uint targetSpeed, ushort expectedPower)
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, target) = CreateTestSetup(userSpeed, targetSpeed);
|
||||
ushort basePower = 40;
|
||||
|
||||
// Act
|
||||
script.ChangeBasePower(move, target, 0, ref basePower);
|
||||
|
||||
// Assert
|
||||
await Assert.That(basePower).IsEqualTo(expectedPower);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: the power bands are inclusive of their upper boundary — a target at exactly 100% of the
|
||||
/// user's Speed yields 60 power, exactly 50% yields 80, exactly 33.33% yields 120, and exactly 25%
|
||||
/// yields 150.
|
||||
/// </summary>
|
||||
[Test, Arguments(100u, 100u, (ushort)60), Arguments(100u, 50u, (ushort)80), Arguments(300u, 100u, (ushort)120),
|
||||
Arguments(400u, 100u, (ushort)150)]
|
||||
public async Task ChangeBasePower_SpeedRatioAtExactBoundary_UsesHigherTier(uint userSpeed, uint targetSpeed,
|
||||
ushort expectedPower)
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, target) = CreateTestSetup(userSpeed, targetSpeed);
|
||||
ushort basePower = 40;
|
||||
|
||||
// Act
|
||||
script.ChangeBasePower(move, target, 0, ref basePower);
|
||||
|
||||
// Assert
|
||||
await Assert.That(basePower).IsEqualTo(expectedPower);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: the power is 40 when the target's Speed is "more than 100%, or exactly 0" of the
|
||||
/// user's Speed.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task ChangeBasePower_TargetSpeedZero_PowerIsForty()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, target) = CreateTestSetup(100, 0);
|
||||
ushort basePower = 40;
|
||||
|
||||
// Act
|
||||
script.ChangeBasePower(move, target, 0, ref basePower);
|
||||
|
||||
// Assert
|
||||
await Assert.That(basePower).IsEqualTo((ushort)40);
|
||||
}
|
||||
}
|
||||
126
Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/EmbargoTests.cs
Normal file
126
Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/EmbargoTests.cs
Normal file
@@ -0,0 +1,126 @@
|
||||
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="Embargo"/> move script and its <see cref="EmbargoEffect"/>.
|
||||
/// Gen VII Bulbapedia behavior: "For five turns, the target's held item has its effects negated and its
|
||||
/// Trainer cannot use items from the Bag on it."
|
||||
/// </summary>
|
||||
public class EmbargoTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a target Pokémon whose volatile scripts are a real <see cref="ScriptSet"/>.
|
||||
/// </summary>
|
||||
private static (IPokemon target, IScriptSet targetVolatile) CreateTarget()
|
||||
{
|
||||
var target = Substitute.For<IPokemon>();
|
||||
target.GetScripts().Returns(_ => new ScriptIterator(Array.Empty<IEnumerable<ScriptContainer>>()));
|
||||
IScriptSet targetVolatile = new ScriptSet(target);
|
||||
target.Volatile.Returns(targetVolatile);
|
||||
return (target, targetVolatile);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "For five turns, the target's held item has its effects negated" — using the move
|
||||
/// attaches the <see cref="EmbargoEffect"/> to the target.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnSecondaryEffect_AddsEmbargoEffectToTarget()
|
||||
{
|
||||
// Arrange
|
||||
var script = new Embargo();
|
||||
var move = Substitute.For<IExecutingMove>();
|
||||
var (target, targetVolatile) = CreateTarget();
|
||||
|
||||
// Act
|
||||
script.OnSecondaryEffect(move, target, 0);
|
||||
|
||||
// Assert
|
||||
await Assert.That(targetVolatile.Contains(ScriptUtils.ResolveName<EmbargoEffect>())).IsTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: the target's "Trainer cannot use items from the Bag on it" and its held item is
|
||||
/// negated — the effect prevents held item consumption.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task PreventHeldItemConsume_EffectActive_ConsumptionPrevented()
|
||||
{
|
||||
// Arrange
|
||||
var effect = new EmbargoEffect();
|
||||
var prevented = false;
|
||||
|
||||
// Act
|
||||
effect.PreventHeldItemConsume(Substitute.For<IPokemon>(), Substitute.For<IItem>(), ref prevented);
|
||||
|
||||
// Assert
|
||||
await Assert.That(prevented).IsTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: the effect lasts "for five turns" — after four end-of-turn ticks it is still active.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnEndTurn_FourTurnsPassed_EffectStillActive()
|
||||
{
|
||||
// Arrange
|
||||
var (target, targetVolatile) = CreateTarget();
|
||||
var effect = new EmbargoEffect();
|
||||
targetVolatile.Add(effect);
|
||||
|
||||
// Act
|
||||
for (var i = 0; i < 4; i++)
|
||||
effect.OnEndTurn(target, Substitute.For<IBattle>());
|
||||
|
||||
// Assert
|
||||
await Assert.That(targetVolatile.Contains(ScriptUtils.ResolveName<EmbargoEffect>())).IsTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: the effect lasts "for five turns" — after the fifth end-of-turn tick it removes itself.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnEndTurn_FiveTurnsPassed_EffectRemovesItself()
|
||||
{
|
||||
// Arrange
|
||||
var (target, targetVolatile) = CreateTarget();
|
||||
var effect = new EmbargoEffect();
|
||||
targetVolatile.Add(effect);
|
||||
|
||||
// Act
|
||||
for (var i = 0; i < 5; i++)
|
||||
effect.OnEndTurn(target, Substitute.For<IBattle>());
|
||||
|
||||
// Assert
|
||||
await Assert.That(targetVolatile.Contains(ScriptUtils.ResolveName<EmbargoEffect>())).IsFalse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "For five turns" — using Embargo again on an already affected target restarts the
|
||||
/// five turn duration (the effect stacks by resetting its counter).
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Stack_UsedAgain_DurationResetsToFiveTurns()
|
||||
{
|
||||
// Arrange
|
||||
var (target, targetVolatile) = CreateTarget();
|
||||
var effect = new EmbargoEffect();
|
||||
targetVolatile.Add(effect);
|
||||
for (var i = 0; i < 3; i++)
|
||||
effect.OnEndTurn(target, Substitute.For<IBattle>());
|
||||
|
||||
// Act - Embargo is used again, restarting the duration
|
||||
effect.Stack();
|
||||
for (var i = 0; i < 4; i++)
|
||||
effect.OnEndTurn(target, Substitute.For<IBattle>());
|
||||
|
||||
// Assert - only four of the renewed five turns have passed
|
||||
await Assert.That(targetVolatile.Contains(ScriptUtils.ResolveName<EmbargoEffect>())).IsTrue();
|
||||
}
|
||||
}
|
||||
230
Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/EncoreTests.cs
Normal file
230
Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/EncoreTests.cs
Normal file
@@ -0,0 +1,230 @@
|
||||
using PkmnLib.Dynamic.Libraries;
|
||||
using PkmnLib.Dynamic.Models;
|
||||
using PkmnLib.Dynamic.Models.Choices;
|
||||
using PkmnLib.Dynamic.ScriptHandling;
|
||||
using PkmnLib.Dynamic.ScriptHandling.Registry;
|
||||
using PkmnLib.Plugin.Gen7.Common;
|
||||
using PkmnLib.Plugin.Gen7.Scripts.Moves;
|
||||
using PkmnLib.Plugin.Gen7.Scripts.Pokemon;
|
||||
using PkmnLib.Static.Moves;
|
||||
using PkmnLib.Static.Utils;
|
||||
|
||||
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the <see cref="Encore"/> move script and its <see cref="EncoreEffect"/>.
|
||||
/// Gen VII Bulbapedia behavior: "Encore temporarily prevents the target from using any move except the
|
||||
/// last used move." It fails if the target hasn't used a move yet or its last move was Struggle, and
|
||||
/// from Generation V onward the duration is "exactly 3 turns".
|
||||
/// </summary>
|
||||
public class EncoreTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a mocked move choice whose chosen move has the given name, optionally carrying the
|
||||
/// <see cref="MoveFlags.CantRepeat"/> flag.
|
||||
/// </summary>
|
||||
private static IMoveChoice CreateMoveChoice(string moveName, bool cantRepeat = false)
|
||||
{
|
||||
var moveData = Substitute.For<IMoveData>();
|
||||
moveData.Name.Returns(new StringKey(moveName));
|
||||
moveData.HasFlag(MoveFlags.CantRepeat).Returns(cantRepeat);
|
||||
var learnedMove = Substitute.For<ILearnedMove>();
|
||||
learnedMove.MoveData.Returns(moveData);
|
||||
var choice = Substitute.For<IMoveChoice>();
|
||||
choice.ChosenMove.Returns(learnedMove);
|
||||
return choice;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a fully mocked test setup where the target's volatile scripts are a real
|
||||
/// <see cref="ScriptSet"/> and its last used move is the given one (or none).
|
||||
/// </summary>
|
||||
private static (Encore script, IExecutingMove move, IPokemon target, IScriptSet targetVolatile, IHitData hitData)
|
||||
CreateTestSetup(string? lastUsedMove, bool lastMoveIsReplacement = false, bool lastMoveCantRepeat = false)
|
||||
{
|
||||
var script = new Encore();
|
||||
var move = Substitute.For<IExecutingMove>();
|
||||
|
||||
// Struggle is recognized through the misc library's replacement choice check, not by name.
|
||||
var miscLibrary = Substitute.For<IMiscLibrary>();
|
||||
var library = Substitute.For<IDynamicLibrary>();
|
||||
library.MiscLibrary.Returns(miscLibrary);
|
||||
var battle = Substitute.For<IBattle>();
|
||||
battle.Library.Returns(library);
|
||||
|
||||
var target = Substitute.For<IPokemon>();
|
||||
target.GetScripts().Returns(_ => new ScriptIterator(Array.Empty<IEnumerable<ScriptContainer>>()));
|
||||
IScriptSet targetVolatile = new ScriptSet(target);
|
||||
target.Volatile.Returns(targetVolatile);
|
||||
var targetBattleData = Substitute.For<IPokemonBattleData>();
|
||||
// Create the choice before the Returns call; configuring substitutes inside Returns is not allowed.
|
||||
var lastMoveChoice = lastUsedMove == null ? null : CreateMoveChoice(lastUsedMove, lastMoveCantRepeat);
|
||||
targetBattleData.LastMoveChoice.Returns(lastMoveChoice);
|
||||
targetBattleData.Battle.Returns(battle);
|
||||
target.BattleData.Returns(targetBattleData);
|
||||
if (lastMoveChoice != null)
|
||||
miscLibrary.IsReplacementChoice(lastMoveChoice).Returns(lastMoveIsReplacement);
|
||||
|
||||
var hitData = Substitute.For<IHitData>();
|
||||
move.GetHitData(target, 0).Returns(hitData);
|
||||
|
||||
return (script, move, target, targetVolatile, hitData);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "Encore temporarily prevents the target from using any move except the last used
|
||||
/// move." The target gains the <see cref="EncoreEffect"/> volatile script.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnSecondaryEffect_TargetUsedMove_AddsEncoreEffectToTarget()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, target, targetVolatile, hitData) = CreateTestSetup("tackle");
|
||||
|
||||
// Act
|
||||
script.OnSecondaryEffect(move, target, 0);
|
||||
|
||||
// Assert
|
||||
await Assert.That(targetVolatile.Contains(ScriptUtils.ResolveName<EncoreEffect>())).IsTrue();
|
||||
hitData.DidNotReceive().Fail();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "It also fails if the foe hasn't used a move yet." Without a last move choice on the
|
||||
/// target, the hit fails and no encore is applied.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnSecondaryEffect_TargetHasNotUsedMove_FailsHit()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, target, targetVolatile, hitData) = CreateTestSetup(null);
|
||||
|
||||
// Act
|
||||
script.OnSecondaryEffect(move, target, 0);
|
||||
|
||||
// Assert
|
||||
hitData.Received(1).Fail();
|
||||
await Assert.That(targetVolatile.Contains(ScriptUtils.ResolveName<EncoreEffect>())).IsFalse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: Encore fails if the target's last move was Struggle. A last move that is a replacement
|
||||
/// choice (per <see cref="IMiscLibrary.IsReplacementChoice"/>) cannot be encored.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnSecondaryEffect_TargetLastUsedStruggle_FailsHit()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, target, targetVolatile, hitData) = CreateTestSetup("struggle", true);
|
||||
|
||||
// Act
|
||||
script.OnSecondaryEffect(move, target, 0);
|
||||
|
||||
// Assert
|
||||
hitData.Received(1).Fail();
|
||||
await Assert.That(targetVolatile.Contains(ScriptUtils.ResolveName<EncoreEffect>())).IsFalse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: Encore fails "if the opponent's last move was Transform, Mimic, Sketch, Mirror Move,
|
||||
/// Encore itself, Struggle, or a move with no PP remaining." These moves carry the
|
||||
/// <see cref="MoveFlags.CantRepeat"/> flag in the move data, which makes Encore fail.
|
||||
/// </summary>
|
||||
[Test, Arguments("encore"), Arguments("transform"), Arguments("mimic"), Arguments("sketch"),
|
||||
Arguments("mirror_move")]
|
||||
public async Task OnSecondaryEffect_TargetLastUsedUnrepeatableMove_FailsHit(string lastUsedMove)
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, target, targetVolatile, hitData) = CreateTestSetup(lastUsedMove, lastMoveCantRepeat: true);
|
||||
|
||||
// Act
|
||||
script.OnSecondaryEffect(move, target, 0);
|
||||
|
||||
// Assert
|
||||
hitData.Received(1).Fail();
|
||||
await Assert.That(targetVolatile.Contains(ScriptUtils.ResolveName<EncoreEffect>())).IsFalse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: Encore fails "if the opponent's last move was Transform, Mimic, Sketch, Mirror Move,
|
||||
/// Encore itself" — integration check: the real Gen7 move data marks exactly these moves with the
|
||||
/// <see cref="MoveFlags.CantRepeat"/> flag.
|
||||
/// </summary>
|
||||
[Test, Arguments("encore"), Arguments("transform"), Arguments("mimic"), Arguments("sketch"),
|
||||
Arguments("mirror_move")]
|
||||
public async Task MoveData_UnrepeatableMove_HasCantRepeatFlag(string moveName)
|
||||
{
|
||||
// Arrange
|
||||
var library = LibraryHelpers.LoadLibrary();
|
||||
|
||||
// Act
|
||||
var found = library.StaticLibrary.Moves.TryGet(new StringKey(moveName), out var moveData);
|
||||
|
||||
// Assert
|
||||
await Assert.That(found).IsTrue();
|
||||
await Assert.That(moveData!.HasFlag(MoveFlags.CantRepeat)).IsTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Technical test: without battle data on the target (outside of battle) the effect does nothing and
|
||||
/// does not throw.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnSecondaryEffect_TargetHasNoBattleData_DoesNothing()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, target, targetVolatile, hitData) = CreateTestSetup("tackle");
|
||||
target.BattleData.Returns((IPokemonBattleData?)null);
|
||||
|
||||
// Act
|
||||
script.OnSecondaryEffect(move, target, 0);
|
||||
|
||||
// Assert
|
||||
hitData.DidNotReceive().Fail();
|
||||
await Assert.That(targetVolatile.Contains(ScriptUtils.ResolveName<EncoreEffect>())).IsFalse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "Duration standardized to exactly 3 turns" (Generation V onward). After two
|
||||
/// end-of-turn ticks the effect is still active.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnEndTurn_TwoTurnsPassed_EffectStillActive()
|
||||
{
|
||||
// Arrange
|
||||
var owner = Substitute.For<IPokemon>();
|
||||
owner.GetScripts().Returns(_ => new ScriptIterator(Array.Empty<IEnumerable<ScriptContainer>>()));
|
||||
IScriptSet ownerVolatile = new ScriptSet(owner);
|
||||
var effect = new EncoreEffect(owner, new StringKey("tackle"), 3);
|
||||
ownerVolatile.Add(effect);
|
||||
|
||||
// Act
|
||||
for (var i = 0; i < 2; i++)
|
||||
effect.OnEndTurn(owner, Substitute.For<IBattle>());
|
||||
|
||||
// Assert
|
||||
await Assert.That(ownerVolatile.Contains(ScriptUtils.ResolveName<EncoreEffect>())).IsTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "Duration standardized to exactly 3 turns" (Generation V onward). After the third
|
||||
/// end-of-turn tick the effect removes itself.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnEndTurn_ThreeTurnsPassed_EffectRemovesItself()
|
||||
{
|
||||
// Arrange
|
||||
var owner = Substitute.For<IPokemon>();
|
||||
owner.GetScripts().Returns(_ => new ScriptIterator(Array.Empty<IEnumerable<ScriptContainer>>()));
|
||||
IScriptSet ownerVolatile = new ScriptSet(owner);
|
||||
var effect = new EncoreEffect(owner, new StringKey("tackle"), 3);
|
||||
ownerVolatile.Add(effect);
|
||||
|
||||
// Act
|
||||
for (var i = 0; i < 3; i++)
|
||||
effect.OnEndTurn(owner, Substitute.For<IBattle>());
|
||||
|
||||
// Assert
|
||||
await Assert.That(ownerVolatile.Contains(ScriptUtils.ResolveName<EncoreEffect>())).IsFalse();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using PkmnLib.Dynamic.Models;
|
||||
using PkmnLib.Plugin.Gen7.Scripts.Moves;
|
||||
|
||||
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the <see cref="Endeavor"/> move script.
|
||||
/// Gen VII Bulbapedia behavior: "Endeavor deals damage to the target equal to the target's remaining HP
|
||||
/// minus the user's remaining HP, making them both equal. It has no effect if the target's HP is already
|
||||
/// equal to or lower than the user's HP."
|
||||
/// </summary>
|
||||
public class EndeavorTests
|
||||
{
|
||||
private static (Endeavor script, IExecutingMove move, IPokemon target) CreateTestSetup(uint userHealth,
|
||||
uint targetHealth)
|
||||
{
|
||||
var script = new Endeavor();
|
||||
var move = Substitute.For<IExecutingMove>();
|
||||
var user = Substitute.For<IPokemon>();
|
||||
user.CurrentHealth.Returns(userHealth);
|
||||
move.User.Returns(user);
|
||||
var target = Substitute.For<IPokemon>();
|
||||
target.CurrentHealth.Returns(targetHealth);
|
||||
return (script, move, target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "Endeavor deals damage to the target equal to the target's remaining HP minus the
|
||||
/// user's remaining HP, making them both equal."
|
||||
/// </summary>
|
||||
[Test, Arguments(1u, 100u, 99u), Arguments(50u, 100u, 50u), Arguments(99u, 100u, 1u), Arguments(10u, 250u, 240u)]
|
||||
public async Task ChangeMoveDamage_UserHpBelowTarget_DamageEqualsHpDifference(uint userHealth, uint targetHealth,
|
||||
uint expectedDamage)
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, target) = CreateTestSetup(userHealth, targetHealth);
|
||||
var damage = 0u;
|
||||
|
||||
// Act
|
||||
script.ChangeMoveDamage(move, target, 0, ref damage);
|
||||
|
||||
// Assert
|
||||
await Assert.That(damage).IsEqualTo(expectedDamage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "It has no effect if the target's HP is already equal to or lower than the user's HP."
|
||||
/// With equal HP the script does not set any damage.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task ChangeMoveDamage_UserHpEqualToTarget_DamageUnchanged()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, target) = CreateTestSetup(100, 100);
|
||||
var damage = 0u;
|
||||
|
||||
// Act
|
||||
script.ChangeMoveDamage(move, target, 0, ref damage);
|
||||
|
||||
// Assert
|
||||
await Assert.That(damage).IsEqualTo(0u);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "It has no effect if the target's HP is already equal to or lower than the user's HP."
|
||||
/// With the target below the user, the damage is not modified (and does not underflow).
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task ChangeMoveDamage_UserHpAboveTarget_DamageUnchanged()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, target) = CreateTestSetup(100, 50);
|
||||
var damage = 0u;
|
||||
|
||||
// Act
|
||||
script.ChangeMoveDamage(move, target, 0, ref damage);
|
||||
|
||||
// Assert
|
||||
await Assert.That(damage).IsEqualTo(0u);
|
||||
}
|
||||
}
|
||||
208
Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/EndureTests.cs
Normal file
208
Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/EndureTests.cs
Normal file
@@ -0,0 +1,208 @@
|
||||
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;
|
||||
|
||||
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the <see cref="Endure"/> move script and its <see cref="EndureEffect"/>.
|
||||
/// Gen VII Bulbapedia behavior: "For the rest of the turn, Endure allows the user to survive any attack
|
||||
/// that would cause it to faint, leaving the user with 1 HP instead." Like other protection moves, "if it
|
||||
/// or other protection moves are used consecutively, its success rate decreases."
|
||||
/// </summary>
|
||||
public class EndureTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a fully mocked setup for driving <see cref="Endure"/>'s inherited
|
||||
/// <see cref="ProtectionScript.OnSecondaryEffect"/>. The <c>target</c> of the secondary effect is the
|
||||
/// Pokémon using Endure itself, as the move is self-targeted.
|
||||
/// </summary>
|
||||
private static (Endure script, IExecutingMove move, IPokemon target, IHitData hitData, IScriptSet volatileSet)
|
||||
CreateProtectSetup(bool userMovesLast, float randomRoll)
|
||||
{
|
||||
var script = new Endure();
|
||||
var move = Substitute.For<IExecutingMove>();
|
||||
var target = Substitute.For<IPokemon>();
|
||||
var hitData = Substitute.For<IHitData>();
|
||||
move.GetHitData(target, 0).Returns(hitData);
|
||||
|
||||
// A queue with a remaining choice means another Pokémon still has to move after the user.
|
||||
var queue = userMovesLast ? new BattleChoiceQueue([]) : new BattleChoiceQueue([Substitute.For<IMoveChoice>()]);
|
||||
|
||||
var random = Substitute.For<IBattleRandom>();
|
||||
random.GetFloat().Returns(randomRoll);
|
||||
|
||||
var battle = Substitute.For<IBattle>();
|
||||
battle.ChoiceQueue.Returns(queue);
|
||||
battle.Random.Returns(random);
|
||||
|
||||
var battleData = Substitute.For<IPokemonBattleData>();
|
||||
battleData.Battle.Returns(battle);
|
||||
target.BattleData.Returns(battleData);
|
||||
|
||||
target.GetScripts().Returns(_ => new ScriptIterator(Array.Empty<IEnumerable<ScriptContainer>>()));
|
||||
IScriptSet volatileSet = new ScriptSet(target);
|
||||
target.Volatile.Returns(volatileSet);
|
||||
|
||||
return (script, move, target, hitData, volatileSet);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "For the rest of the turn, Endure allows the user to survive any attack that would
|
||||
/// cause it to faint" — using the move attaches the <see cref="EndureEffect"/> to the user.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnSecondaryEffect_FirstUse_AddsEndureEffect()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, target, hitData, volatileSet) = CreateProtectSetup(false, 0.5f);
|
||||
|
||||
// Act
|
||||
script.OnSecondaryEffect(move, target, 0);
|
||||
|
||||
// Assert
|
||||
await Assert.That(volatileSet.Contains(ScriptUtils.ResolveName<EndureEffect>())).IsTrue();
|
||||
hitData.DidNotReceive().Fail();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: Endure "always fails if the user is the last to act in the turn" — with no remaining
|
||||
/// choices in the queue there is nothing left to protect against, and the move fails.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnSecondaryEffect_UserMovesLast_FailsHit()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, target, hitData, volatileSet) = CreateProtectSetup(true, 0.0f);
|
||||
|
||||
// Act
|
||||
script.OnSecondaryEffect(move, target, 0);
|
||||
|
||||
// Assert
|
||||
hitData.Received(1).Fail();
|
||||
await Assert.That(volatileSet.Contains(ScriptUtils.ResolveName<EndureEffect>())).IsFalse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "If it or other protection moves are used consecutively, its success rate decreases."
|
||||
/// On the second consecutive use the success chance is 1/3, so a roll above that fails.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnSecondaryEffect_SecondConsecutiveUseWithHighRoll_FailsHit()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, target, hitData, volatileSet) = CreateProtectSetup(false, 0.5f);
|
||||
volatileSet.Add(new ProtectionFailureScript { ProtectTurns = 1 });
|
||||
|
||||
// Act
|
||||
script.OnSecondaryEffect(move, target, 0);
|
||||
|
||||
// Assert
|
||||
hitData.Received(1).Fail();
|
||||
await Assert.That(volatileSet.Contains(ScriptUtils.ResolveName<EndureEffect>())).IsFalse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "If it or other protection moves are used consecutively, its success rate decreases."
|
||||
/// On the second consecutive use the success chance is 1/3, so a roll below that still succeeds.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnSecondaryEffect_SecondConsecutiveUseWithLowRoll_AddsEndureEffect()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, target, hitData, volatileSet) = CreateProtectSetup(false, 0.2f);
|
||||
volatileSet.Add(new ProtectionFailureScript { ProtectTurns = 1 });
|
||||
|
||||
// Act
|
||||
script.OnSecondaryEffect(move, target, 0);
|
||||
|
||||
// Assert
|
||||
await Assert.That(volatileSet.Contains(ScriptUtils.ResolveName<EndureEffect>())).IsTrue();
|
||||
hitData.DidNotReceive().Fail();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "Endure allows the user to survive any attack that would cause it to faint, leaving
|
||||
/// the user with 1 HP instead." Damage exceeding the user's current HP is reduced to leave 1 HP.
|
||||
/// </summary>
|
||||
[Test, Arguments(100u, 150u, 99u), Arguments(50u, 51u, 49u), Arguments(2u, 9999u, 1u)]
|
||||
public async Task ChangeIncomingDamage_LethalDamage_LeavesUserWithOneHp(uint currentHealth, uint incomingDamage,
|
||||
uint expectedDamage)
|
||||
{
|
||||
// Arrange
|
||||
var effect = new EndureEffect();
|
||||
var pokemon = Substitute.For<IPokemon>();
|
||||
pokemon.CurrentHealth.Returns(currentHealth);
|
||||
var damage = incomingDamage;
|
||||
|
||||
// Act
|
||||
effect.ChangeIncomingDamage(pokemon, DamageSource.MoveDamage, ref damage);
|
||||
|
||||
// Assert
|
||||
await Assert.That(damage).IsEqualTo(expectedDamage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: Endure lets the user "survive any attack that would cause it to faint" — a hit for
|
||||
/// exactly the user's current HP would faint it, so it too must be reduced to leave 1 HP.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task ChangeIncomingDamage_DamageEqualToCurrentHp_LeavesUserWithOneHp()
|
||||
{
|
||||
// Arrange
|
||||
var effect = new EndureEffect();
|
||||
var pokemon = Substitute.For<IPokemon>();
|
||||
pokemon.CurrentHealth.Returns(100u);
|
||||
var damage = 100u;
|
||||
|
||||
// Act
|
||||
effect.ChangeIncomingDamage(pokemon, DamageSource.MoveDamage, ref damage);
|
||||
|
||||
// Assert
|
||||
await Assert.That(damage).IsEqualTo(99u);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: Endure only affects attacks "that would cause it to faint" — non-lethal damage passes
|
||||
/// through unchanged.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task ChangeIncomingDamage_NonLethalDamage_DamageUnchanged()
|
||||
{
|
||||
// Arrange
|
||||
var effect = new EndureEffect();
|
||||
var pokemon = Substitute.For<IPokemon>();
|
||||
pokemon.CurrentHealth.Returns(100u);
|
||||
var damage = 50u;
|
||||
|
||||
// Act
|
||||
effect.ChangeIncomingDamage(pokemon, DamageSource.MoveDamage, ref damage);
|
||||
|
||||
// Assert
|
||||
await Assert.That(damage).IsEqualTo(50u);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: the protection lasts "for the rest of the turn" — the effect removes itself at the end
|
||||
/// of the turn.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnEndTurn_EffectRemovesItself()
|
||||
{
|
||||
// Arrange
|
||||
var owner = Substitute.For<IPokemon>();
|
||||
owner.GetScripts().Returns(_ => new ScriptIterator(Array.Empty<IEnumerable<ScriptContainer>>()));
|
||||
IScriptSet ownerVolatile = new ScriptSet(owner);
|
||||
var effect = new EndureEffect();
|
||||
ownerVolatile.Add(effect);
|
||||
|
||||
// Act
|
||||
effect.OnEndTurn(owner, Substitute.For<IBattle>());
|
||||
|
||||
// Assert
|
||||
await Assert.That(ownerVolatile.Contains(ScriptUtils.ResolveName<EndureEffect>())).IsFalse();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
using PkmnLib.Dynamic.Models;
|
||||
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="Entrainment"/> move script.
|
||||
/// Gen VII Bulbapedia behavior: "Entrainment changes the target's Ability to match the user's." It fails
|
||||
/// if both Pokémon already share the same Ability, if the target's Ability cannot be changed (e.g.
|
||||
/// Truant, Multitype, Stance Change, Comatose, Disguise), or if the user's Ability cannot be copied
|
||||
/// (e.g. Trace, Illusion, Imposter, Power Construct).
|
||||
/// </summary>
|
||||
public class EntrainmentTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a mocked ability that reports the given flags as set.
|
||||
/// </summary>
|
||||
private static IAbility CreateAbility(params string[] flags)
|
||||
{
|
||||
var ability = Substitute.For<IAbility>();
|
||||
foreach (var flag in flags)
|
||||
ability.HasFlag(new StringKey(flag)).Returns(true);
|
||||
return ability;
|
||||
}
|
||||
|
||||
private static (Entrainment script, IExecutingMove move, IPokemon target, IHitData hitData) CreateTestSetup(
|
||||
IAbility? userAbility, IAbility? targetAbility)
|
||||
{
|
||||
var script = new Entrainment();
|
||||
var move = Substitute.For<IExecutingMove>();
|
||||
var target = Substitute.For<IPokemon>();
|
||||
var hitData = Substitute.For<IHitData>();
|
||||
move.GetHitData(target, 0).Returns(hitData);
|
||||
|
||||
var user = Substitute.For<IPokemon>();
|
||||
user.ActiveAbility.Returns(userAbility);
|
||||
move.User.Returns(user);
|
||||
target.ActiveAbility.Returns(targetAbility);
|
||||
|
||||
return (script, move, target, hitData);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "Entrainment changes the target's Ability to match the user's."
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void OnSecondaryEffect_DifferentAbilities_ChangesTargetAbilityToUsers()
|
||||
{
|
||||
// Arrange
|
||||
var userAbility = CreateAbility();
|
||||
var (script, move, target, hitData) = CreateTestSetup(userAbility, CreateAbility());
|
||||
|
||||
// Act
|
||||
script.OnSecondaryEffect(move, target, 0);
|
||||
|
||||
// Assert
|
||||
target.Received(1).ChangeAbility(userAbility);
|
||||
hitData.DidNotReceive().Fail();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "The move fails if both Pokémon already share the same Ability."
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void OnSecondaryEffect_SameAbility_FailsHit()
|
||||
{
|
||||
// Arrange
|
||||
var sharedAbility = CreateAbility();
|
||||
var (script, move, target, hitData) = CreateTestSetup(sharedAbility, sharedAbility);
|
||||
|
||||
// Act
|
||||
script.OnSecondaryEffect(move, target, 0);
|
||||
|
||||
// Assert
|
||||
hitData.Received(1).Fail();
|
||||
target.DidNotReceiveWithAnyArgs().ChangeAbility(default!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "The move fails if the user has: Trace, Forecast, Flower Gift, Zen Mode, Illusion,
|
||||
/// Imposter, [...]" — abilities that cannot be copied prevent Entrainment from working.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void OnSecondaryEffect_UserAbilityCantBeCopied_FailsHit()
|
||||
{
|
||||
// Arrange
|
||||
var userAbility = CreateAbility("cant_be_copied");
|
||||
var (script, move, target, hitData) = CreateTestSetup(userAbility, CreateAbility());
|
||||
|
||||
// Act
|
||||
script.OnSecondaryEffect(move, target, 0);
|
||||
|
||||
// Assert
|
||||
hitData.Received(1).Fail();
|
||||
target.DidNotReceiveWithAnyArgs().ChangeAbility(default!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "The move cannot affect Pokémon with these Abilities: Truant, Multitype, Stance
|
||||
/// Change, [...]" — a target whose ability cannot be changed prevents Entrainment from working.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void OnSecondaryEffect_TargetAbilityCantBeChanged_FailsHit()
|
||||
{
|
||||
// Arrange
|
||||
var targetAbility = CreateAbility("cant_be_changed");
|
||||
var (script, move, target, hitData) = CreateTestSetup(CreateAbility(), targetAbility);
|
||||
|
||||
// Act
|
||||
script.OnSecondaryEffect(move, target, 0);
|
||||
|
||||
// Assert
|
||||
hitData.Received(1).Fail();
|
||||
target.DidNotReceiveWithAnyArgs().ChangeAbility(default!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "Entrainment changes the target's Ability to match the user's" — without a user
|
||||
/// ability there is nothing to copy, so the move fails.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void OnSecondaryEffect_UserHasNoAbility_FailsHit()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, target, hitData) = CreateTestSetup(null, CreateAbility());
|
||||
|
||||
// Act
|
||||
script.OnSecondaryEffect(move, target, 0);
|
||||
|
||||
// Assert
|
||||
hitData.Received(1).Fail();
|
||||
target.DidNotReceiveWithAnyArgs().ChangeAbility(default!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Technical test: a target without an active ability cannot have it validated for replacement, so
|
||||
/// the move fails rather than throwing.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void OnSecondaryEffect_TargetHasNoAbility_FailsHit()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, target, hitData) = CreateTestSetup(CreateAbility(), null);
|
||||
|
||||
// Act
|
||||
script.OnSecondaryEffect(move, target, 0);
|
||||
|
||||
// Assert
|
||||
hitData.Received(1).Fail();
|
||||
target.DidNotReceiveWithAnyArgs().ChangeAbility(default!);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
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="Eruption"/> move script.
|
||||
/// Gen VII Bulbapedia behavior: "Eruption deals damage. It has 150 base power at maximum HP, but its base
|
||||
/// power decreases linearly with the user's remaining HP", following ⌊150 × HPcurrent / HPmax⌋ with a
|
||||
/// minimum of 1.
|
||||
/// </summary>
|
||||
public class EruptionTests
|
||||
{
|
||||
private static (Eruption script, IExecutingMove move, IPokemon target) CreateTestSetup(uint currentHealth,
|
||||
uint maxHealth)
|
||||
{
|
||||
var script = new Eruption();
|
||||
var move = Substitute.For<IExecutingMove>();
|
||||
var user = Substitute.For<IPokemon>();
|
||||
user.CurrentHealth.Returns(currentHealth);
|
||||
user.BoostedStats.Returns(new StatisticSet<uint>(maxHealth, 1, 1, 1, 1, 1));
|
||||
move.User.Returns(user);
|
||||
var target = Substitute.For<IPokemon>();
|
||||
return (script, move, target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "It has 150 base power at maximum HP, but its base power decreases linearly with the
|
||||
/// user's remaining HP" — power = ⌊150 × HPcurrent / HPmax⌋, with integer truncation.
|
||||
/// </summary>
|
||||
[Test, Arguments(100u, 100u, (ushort)150), Arguments(300u, 300u, (ushort)150), Arguments(50u, 100u, (ushort)75),
|
||||
Arguments(75u, 100u, (ushort)112), Arguments(33u, 100u, (ushort)49), Arguments(150u, 300u, (ushort)75)]
|
||||
public async Task ChangeBasePower_ScalesWithUserCurrentHp(uint currentHealth, uint maxHealth, ushort expectedPower)
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, target) = CreateTestSetup(currentHealth, maxHealth);
|
||||
ushort basePower = 150;
|
||||
|
||||
// Act
|
||||
script.ChangeBasePower(move, target, 0, ref basePower);
|
||||
|
||||
// Assert
|
||||
await Assert.That(basePower).IsEqualTo(expectedPower);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "if this calculation yields a value below 1, the move's power becomes 1 instead."
|
||||
/// </summary>
|
||||
[Test, Arguments(1u, 300u), Arguments(1u, 151u)]
|
||||
public async Task ChangeBasePower_UserAtVeryLowHp_PowerIsAtLeastOne(uint currentHealth, uint maxHealth)
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, target) = CreateTestSetup(currentHealth, maxHealth);
|
||||
ushort basePower = 150;
|
||||
|
||||
// Act
|
||||
script.ChangeBasePower(move, target, 0, ref basePower);
|
||||
|
||||
// Assert
|
||||
await Assert.That(basePower).IsEqualTo((ushort)1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using PkmnLib.Dynamic.Models;
|
||||
using PkmnLib.Plugin.Gen7.Scripts.Moves;
|
||||
|
||||
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the <see cref="Explosion"/> move script.
|
||||
/// Gen VII Bulbapedia behavior: "the user faints upon using this move", and from Generation VI onwards
|
||||
/// "Explosion returns to damaging the target before the user faints."
|
||||
/// </summary>
|
||||
public class ExplosionTests
|
||||
{
|
||||
private static (Explosion script, IExecutingMove move, IPokemon user) CreateTestSetup(uint currentHealth)
|
||||
{
|
||||
var script = new Explosion();
|
||||
var move = Substitute.For<IExecutingMove>();
|
||||
var user = Substitute.For<IPokemon>();
|
||||
user.CurrentHealth.Returns(currentHealth);
|
||||
move.User.Returns(user);
|
||||
return (script, move, user);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper to extract the damage amount from the user's received Damage calls.
|
||||
/// </summary>
|
||||
private static uint? GetDamageAmount(IPokemon user)
|
||||
{
|
||||
var call = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage");
|
||||
return call != null ? (uint)call.GetArguments()[0]! : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "the user faints upon using this move" — after the hits are dealt, the user takes at
|
||||
/// least its full current HP in damage, guaranteeing it faints.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnAfterHits_UserTakesAtLeastItsCurrentHealthInDamage()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, user) = CreateTestSetup(100);
|
||||
|
||||
// Act
|
||||
script.OnAfterHits(move, Substitute.For<IPokemon>());
|
||||
|
||||
// Assert
|
||||
var damage = GetDamageAmount(user);
|
||||
await Assert.That(damage).IsNotNull();
|
||||
await Assert.That(damage!.Value).IsGreaterThanOrEqualTo(100u);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "Sturdy, Focus Band, and Focus Sash do not prevent user fainting." The self-inflicted
|
||||
/// faint is indirect damage (<see cref="DamageSource.Misc"/>), not move damage, so hold-on effects
|
||||
/// that watch for move damage do not apply.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnAfterHits_UsesMiscDamageSource()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, user) = CreateTestSetup(100);
|
||||
|
||||
// Act
|
||||
script.OnAfterHits(move, Substitute.For<IPokemon>());
|
||||
|
||||
// Assert
|
||||
var call = user.ReceivedCalls().First(c => c.GetMethodInfo().Name == "Damage");
|
||||
await Assert.That((DamageSource)call.GetArguments()[1]!).IsEqualTo(DamageSource.Misc);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ public static class MoveFlags
|
||||
{
|
||||
public static readonly StringKey Ballistics = "ballistics";
|
||||
public static readonly StringKey Bite = "bite";
|
||||
public static readonly StringKey CantRepeat = "cant_repeat";
|
||||
public static readonly StringKey Charge = "charge";
|
||||
public static readonly StringKey Contact = "contact";
|
||||
public static readonly StringKey Dance = "dance";
|
||||
|
||||
@@ -3196,7 +3196,8 @@
|
||||
"mirror",
|
||||
"ignore_substitute",
|
||||
"mental",
|
||||
"limit_move_choice"
|
||||
"limit_move_choice",
|
||||
"cant_repeat"
|
||||
],
|
||||
"effect": {
|
||||
"name": "encore"
|
||||
@@ -6968,7 +6969,8 @@
|
||||
"category": "status",
|
||||
"flags": [
|
||||
"protect",
|
||||
"ignore_substitute"
|
||||
"ignore_substitute",
|
||||
"cant_repeat"
|
||||
],
|
||||
"effect": {
|
||||
"name": "mimic"
|
||||
@@ -7054,7 +7056,9 @@
|
||||
"priority": 0,
|
||||
"target": "Any",
|
||||
"category": "status",
|
||||
"flags": [],
|
||||
"flags": [
|
||||
"cant_repeat"
|
||||
],
|
||||
"effect": {
|
||||
"name": "mirror_move"
|
||||
}
|
||||
@@ -10033,7 +10037,8 @@
|
||||
"target": "Any",
|
||||
"category": "status",
|
||||
"flags": [
|
||||
"ignore_substitute"
|
||||
"ignore_substitute",
|
||||
"cant_repeat"
|
||||
],
|
||||
"effect": {
|
||||
"name": "sketch"
|
||||
@@ -12100,7 +12105,9 @@
|
||||
"priority": 0,
|
||||
"target": "Any",
|
||||
"category": "status",
|
||||
"flags": [],
|
||||
"flags": [
|
||||
"cant_repeat"
|
||||
],
|
||||
"effect": {
|
||||
"name": "transform"
|
||||
}
|
||||
|
||||
@@ -3,10 +3,9 @@ using PkmnLib.Plugin.Gen7.Scripts.Side;
|
||||
namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
|
||||
|
||||
[Script(ScriptCategory.Move, "echoed_voice")]
|
||||
public class EchoedVoice : Script, IScriptOnSecondaryEffect, IScriptChangeDamageModifier
|
||||
public class EchoedVoice : Script, IScriptOnSecondaryEffect, IScriptChangeBasePower
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public void ChangeDamageModifier(IExecutingMove move, IPokemon target, byte hit, ref float modifier)
|
||||
public void ChangeBasePower(IExecutingMove move, IPokemon target, byte hit, ref ushort basePower)
|
||||
{
|
||||
var battleData = move.User.BattleData;
|
||||
if (battleData == null)
|
||||
@@ -16,7 +15,8 @@ public class EchoedVoice : Script, IScriptOnSecondaryEffect, IScriptChangeDamage
|
||||
if (echoedVoiceData == null)
|
||||
return;
|
||||
|
||||
modifier *= 2;
|
||||
var newBasePower = basePower + (ushort)(echoedVoiceData.Stacks * 40);
|
||||
basePower = (ushort)Math.Min(newBasePower, 200);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -9,14 +9,19 @@ public class ElectroBall : Script, IScriptChangeBasePower
|
||||
var user = move.User;
|
||||
var targetSpeed = target.BoostedStats.Speed;
|
||||
var userSpeed = user.BoostedStats.Speed;
|
||||
if (targetSpeed == 0)
|
||||
{
|
||||
basePower = 40;
|
||||
return;
|
||||
}
|
||||
|
||||
var ratio = (float)userSpeed / targetSpeed;
|
||||
basePower = ratio switch
|
||||
{
|
||||
> 4 => 150,
|
||||
> 3 => 120,
|
||||
> 2 => 80,
|
||||
> 1 => 60,
|
||||
>= 4 => 150,
|
||||
>= 3 => 120,
|
||||
>= 2 => 80,
|
||||
>= 1 => 60,
|
||||
_ => 40,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Frozen;
|
||||
using PkmnLib.Plugin.Gen7.Scripts.Pokemon;
|
||||
|
||||
namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
|
||||
@@ -13,7 +14,8 @@ public class Encore : Script, IScriptOnSecondaryEffect
|
||||
return;
|
||||
|
||||
var lastMove = target.BattleData?.LastMoveChoice;
|
||||
if (lastMove == null || battle.Library.MiscLibrary.IsReplacementChoice(lastMove))
|
||||
if (lastMove == null || battle.Library.MiscLibrary.IsReplacementChoice(lastMove) ||
|
||||
lastMove.ChosenMove.MoveData.HasFlag(MoveFlags.CantRepeat))
|
||||
{
|
||||
move.GetHitData(target, hit).Fail();
|
||||
return;
|
||||
|
||||
@@ -6,7 +6,7 @@ public class EndureEffect : Script, IScriptOnEndTurn, IScriptChangeIncomingDamag
|
||||
/// <inheritdoc />
|
||||
public void ChangeIncomingDamage(IPokemon pokemon, DamageSource source, ref uint damage)
|
||||
{
|
||||
if (damage > pokemon.CurrentHealth)
|
||||
if (damage >= pokemon.CurrentHealth)
|
||||
damage = pokemon.CurrentHealth - 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,11 +4,22 @@ namespace PkmnLib.Plugin.Gen7.Scripts.Side;
|
||||
/// Just here to indicate that a Pokemon on this side has used Echoed Voice.
|
||||
/// </summary>
|
||||
[Script(ScriptCategory.Side, "echoed_voice_data")]
|
||||
public class EchoedVoiceData : Script, IScriptOnEndTurn
|
||||
public class EchoedVoiceData : Script, IScriptOnBeforeMoveChoice, IScriptStack
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public void OnEndTurn(IScriptSource owner, IBattle battle)
|
||||
private int _stacks = 1;
|
||||
|
||||
public int Stacks => _stacks;
|
||||
|
||||
public void OnBeforeMoveChoice(IMoveChoice moveChoice)
|
||||
{
|
||||
RemoveSelf();
|
||||
if (moveChoice.ChosenMove.MoveData.Name != "echoed_voice")
|
||||
{
|
||||
RemoveSelf();
|
||||
}
|
||||
}
|
||||
|
||||
public void Stack()
|
||||
{
|
||||
_stacks++;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user