This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
using PkmnLib.Dynamic.Events;
|
||||
using PkmnLib.Dynamic.Models;
|
||||
using PkmnLib.Dynamic.Models.Choices;
|
||||
using PkmnLib.Dynamic.ScriptHandling;
|
||||
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="FocusPunch"/> script, which implements Focus Punch.
|
||||
/// Behavior is verified against the Bulbapedia page for Focus Punch (Generation VII).
|
||||
/// </summary>
|
||||
public class FocusPunchTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a fully mocked test setup for the turn start (charging) phase of Focus Punch.
|
||||
/// </summary>
|
||||
private static (FocusPunch focusPunch, ITurnChoice choice, IPokemon user, IScriptSet volatileSet, EventHook
|
||||
eventHook) CreateChargeSetup(bool hasBattleData = true)
|
||||
{
|
||||
var focusPunch = new FocusPunch();
|
||||
var user = Substitute.For<IPokemon>();
|
||||
var volatileSet = Substitute.For<IScriptSet>();
|
||||
user.Volatile.Returns(volatileSet);
|
||||
|
||||
var eventHook = new EventHook();
|
||||
if (hasBattleData)
|
||||
{
|
||||
var battle = Substitute.For<IBattle>();
|
||||
battle.EventHook.Returns(eventHook);
|
||||
var battleData = Substitute.For<IPokemonBattleData>();
|
||||
battleData.Battle.Returns(battle);
|
||||
user.BattleData.Returns(battleData);
|
||||
}
|
||||
else
|
||||
{
|
||||
user.BattleData.Returns((IPokemonBattleData?)null);
|
||||
}
|
||||
|
||||
var choice = Substitute.For<ITurnChoice>();
|
||||
choice.User.Returns(user);
|
||||
return (focusPunch, choice, user, volatileSet, eventHook);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a fully mocked test setup for the execution (PreventMove) phase of Focus Punch.
|
||||
/// </summary>
|
||||
private static (FocusPunch focusPunch, IExecutingMove move, IScriptSet volatileSet) CreateExecutionSetup()
|
||||
{
|
||||
var focusPunch = new FocusPunch();
|
||||
var move = Substitute.For<IExecutingMove>();
|
||||
var user = Substitute.For<IPokemon>();
|
||||
var volatileSet = Substitute.For<IScriptSet>();
|
||||
user.Volatile.Returns(volatileSet);
|
||||
move.User.Returns(user);
|
||||
return (focusPunch, move, volatileSet);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="FocusPunchEffect"/> that has registered an incoming hit, as happens when the user
|
||||
/// is struck by a damaging move while focusing.
|
||||
/// </summary>
|
||||
private static FocusPunchEffect CreateHitEffect()
|
||||
{
|
||||
var effect = new FocusPunchEffect();
|
||||
var hitReceiver = Substitute.For<IPokemon>();
|
||||
hitReceiver.BattleData.Returns((IPokemonBattleData?)null);
|
||||
effect.OnIncomingHit(Substitute.For<IExecutingMove>(), hitReceiver, 0);
|
||||
return effect;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "The user of Focus Punch will start focusing at the beginning of the turn the move is used".
|
||||
/// At turn start the script attaches the <see cref="FocusPunchEffect"/> volatile to the user, which tracks
|
||||
/// whether the user is hit during the turn.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnBeforeTurnStart_Called_AddsFocusPunchEffectToUserVolatile()
|
||||
{
|
||||
// Arrange
|
||||
var (focusPunch, choice, _, volatileSet, _) = CreateChargeSetup();
|
||||
|
||||
// Act
|
||||
focusPunch.OnBeforeTurnStart(choice);
|
||||
|
||||
// Assert
|
||||
var addCall = volatileSet.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Add");
|
||||
await Assert.That(addCall).IsNotNull();
|
||||
await Assert.That(addCall!.GetArguments()[0] is FocusPunchEffect).IsTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "Its focusing message is displayed before any other moves."
|
||||
/// The script fires a <see cref="DialogEvent"/> with the "focus_punch_charge" message at turn start,
|
||||
/// with the charging Pokémon as parameter.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnBeforeTurnStart_Called_FiresFocusPunchChargeDialogEvent()
|
||||
{
|
||||
// Arrange
|
||||
var (focusPunch, choice, user, _, eventHook) = CreateChargeSetup();
|
||||
DialogEvent? capturedEvent = null;
|
||||
eventHook.Handler += (sender, args) =>
|
||||
{
|
||||
if (args is DialogEvent dialogEvent)
|
||||
capturedEvent = dialogEvent;
|
||||
};
|
||||
|
||||
// Act
|
||||
focusPunch.OnBeforeTurnStart(choice);
|
||||
|
||||
// Assert
|
||||
await Assert.That(capturedEvent).IsNotNull();
|
||||
await Assert.That(capturedEvent!.Message).IsEqualTo("focus_punch_charge");
|
||||
await Assert.That(capturedEvent.Parameters).IsNotNull();
|
||||
await Assert.That(capturedEvent.Parameters!.ContainsKey("pokemon")).IsTrue();
|
||||
await Assert.That(ReferenceEquals(capturedEvent.Parameters["pokemon"], user)).IsTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "The user of Focus Punch will start focusing at the beginning of the turn the move is used".
|
||||
/// Technical test: if the user has no <see cref="IPokemon.BattleData"/>, no dialog can be shown, but the
|
||||
/// charging effect is still applied without throwing.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnBeforeTurnStart_UserWithoutBattleData_StillAddsFocusPunchEffect()
|
||||
{
|
||||
// Arrange
|
||||
var (focusPunch, choice, _, volatileSet, _) = CreateChargeSetup(false);
|
||||
|
||||
// Act
|
||||
focusPunch.OnBeforeTurnStart(choice);
|
||||
|
||||
// Assert
|
||||
var addCall = volatileSet.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Add");
|
||||
await Assert.That(addCall).IsNotNull();
|
||||
await Assert.That(addCall!.GetArguments()[0] is FocusPunchEffect).IsTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "then execute the move Focus Punch at a decreased priority, unless it was hit by another
|
||||
/// Pokémon's damaging move before executing Focus Punch".
|
||||
/// If the user was not hit while focusing, the move is not prevented.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task PreventMove_UserNotHitThisTurn_MoveIsNotPrevented()
|
||||
{
|
||||
// Arrange
|
||||
var (focusPunch, move, volatileSet) = CreateExecutionSetup();
|
||||
volatileSet.Get<FocusPunchEffect>().Returns(new FocusPunchEffect());
|
||||
var prevent = false;
|
||||
|
||||
// Act
|
||||
focusPunch.PreventMove(move, ref prevent);
|
||||
|
||||
// Assert
|
||||
await Assert.That(prevent).IsFalse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "The move fails if the user has already taken damage from a move in the same turn."
|
||||
/// Generation V onwards: "PP is no longer consumed if the user loses its focus and fails to execute the
|
||||
/// move." — the script uses the PreventMove hook, which does not consume PP.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task PreventMove_UserWasHitThisTurn_MoveIsPrevented()
|
||||
{
|
||||
// Arrange
|
||||
var (focusPunch, move, volatileSet) = CreateExecutionSetup();
|
||||
// Create the effect before configuring the substitute: CreateHitEffect interacts with other substitutes,
|
||||
// which would make NSubstitute configure the wrong call if evaluated inside Returns().
|
||||
var hitEffect = CreateHitEffect();
|
||||
volatileSet.Get<FocusPunchEffect>().Returns(hitEffect);
|
||||
var prevent = false;
|
||||
|
||||
// Act
|
||||
focusPunch.PreventMove(move, ref prevent);
|
||||
|
||||
// Assert
|
||||
await Assert.That(prevent).IsTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "The user of Focus Punch will start focusing at the beginning of the turn the move is used".
|
||||
/// Technical test: if the user never started focusing (no <see cref="FocusPunchEffect"/> volatile is
|
||||
/// present), the move cannot execute.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task PreventMove_NoFocusPunchEffectPresent_MoveIsPrevented()
|
||||
{
|
||||
// Arrange
|
||||
var (focusPunch, move, volatileSet) = CreateExecutionSetup();
|
||||
volatileSet.Get<FocusPunchEffect>().Returns((FocusPunchEffect?)null);
|
||||
var prevent = false;
|
||||
|
||||
// Act
|
||||
focusPunch.PreventMove(move, ref prevent);
|
||||
|
||||
// Assert
|
||||
await Assert.That(prevent).IsTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia (Generation V onwards): "One-hit knockout moves do not cause Focus Punch to lose focus."
|
||||
/// A one-hit knockout move can land on a focusing Pokémon that survives it (e.g. through Sturdy or a Focus
|
||||
/// Sash), in which case the engine invokes <see cref="FocusPunchEffect"/>'s incoming-hit hook; the effect
|
||||
/// must then recognize the move (Gen7 data marks these with the "one_hit_ko" effect, here real Fissure
|
||||
/// data) and keep its focus.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnIncomingHit_HitByOneHitKnockoutMove_FocusIsNotBroken()
|
||||
{
|
||||
// Arrange
|
||||
var library = LibraryHelpers.LoadLibrary();
|
||||
await Assert.That(library.StaticLibrary.Moves.TryGet("fissure", out var fissure)).IsTrue();
|
||||
var effect = new FocusPunchEffect();
|
||||
var move = Substitute.For<IExecutingMove>();
|
||||
move.UseMove.Returns(fissure!);
|
||||
var target = Substitute.For<IPokemon>();
|
||||
target.BattleData.Returns((IPokemonBattleData?)null);
|
||||
|
||||
// Act
|
||||
effect.OnIncomingHit(move, target, 0);
|
||||
|
||||
// Assert
|
||||
await Assert.That(effect.WasHit).IsFalse();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user