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; /// /// Tests for the script, which implements Focus Punch. /// Behavior is verified against the Bulbapedia page for Focus Punch (Generation VII). /// public class FocusPunchTests { /// /// Creates a fully mocked test setup for the turn start (charging) phase of Focus Punch. /// private static (FocusPunch focusPunch, ITurnChoice choice, IPokemon user, IScriptSet volatileSet, EventHook eventHook) CreateChargeSetup(bool hasBattleData = true) { var focusPunch = new FocusPunch(); var user = Substitute.For(); var volatileSet = Substitute.For(); user.Volatile.Returns(volatileSet); var eventHook = new EventHook(); if (hasBattleData) { var battle = Substitute.For(); battle.EventHook.Returns(eventHook); var battleData = Substitute.For(); battleData.Battle.Returns(battle); user.BattleData.Returns(battleData); } else { user.BattleData.Returns((IPokemonBattleData?)null); } var choice = Substitute.For(); choice.User.Returns(user); return (focusPunch, choice, user, volatileSet, eventHook); } /// /// Creates a fully mocked test setup for the execution (PreventMove) phase of Focus Punch. /// private static (FocusPunch focusPunch, IExecutingMove move, IScriptSet volatileSet) CreateExecutionSetup() { var focusPunch = new FocusPunch(); var move = Substitute.For(); var user = Substitute.For(); var volatileSet = Substitute.For(); user.Volatile.Returns(volatileSet); move.User.Returns(user); return (focusPunch, move, volatileSet); } /// /// Creates a that has registered an incoming hit, as happens when the user /// is struck by a damaging move while focusing. /// private static FocusPunchEffect CreateHitEffect() { var effect = new FocusPunchEffect(); var hitReceiver = Substitute.For(); hitReceiver.BattleData.Returns((IPokemonBattleData?)null); effect.OnIncomingHit(Substitute.For(), hitReceiver, 0); return effect; } /// /// 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 volatile to the user, which tracks /// whether the user is hit during the turn. /// [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(); } /// /// Bulbapedia: "Its focusing message is displayed before any other moves." /// The script fires a with the "focus_punch_charge" message at turn start, /// with the charging Pokémon as parameter. /// [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(); } /// /// 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 , no dialog can be shown, but the /// charging effect is still applied without throwing. /// [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(); } /// /// 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. /// [Test] public async Task PreventMove_UserNotHitThisTurn_MoveIsNotPrevented() { // Arrange var (focusPunch, move, volatileSet) = CreateExecutionSetup(); volatileSet.Get().Returns(new FocusPunchEffect()); var prevent = false; // Act focusPunch.PreventMove(move, ref prevent); // Assert await Assert.That(prevent).IsFalse(); } /// /// 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. /// [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().Returns(hitEffect); var prevent = false; // Act focusPunch.PreventMove(move, ref prevent); // Assert await Assert.That(prevent).IsTrue(); } /// /// 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 volatile is /// present), the move cannot execute. /// [Test] public async Task PreventMove_NoFocusPunchEffectPresent_MoveIsPrevented() { // Arrange var (focusPunch, move, volatileSet) = CreateExecutionSetup(); volatileSet.Get().Returns((FocusPunchEffect?)null); var prevent = false; // Act focusPunch.PreventMove(move, ref prevent); // Assert await Assert.That(prevent).IsTrue(); } /// /// 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 '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. /// [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(); move.UseMove.Returns(fissure!); var target = Substitute.For(); target.BattleData.Returns((IPokemonBattleData?)null); // Act effect.OnIncomingHit(move, target, 0); // Assert await Assert.That(effect.WasHit).IsFalse(); } }