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; using PkmnLib.Static.Utils; namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// /// Tests for the move script and its attached . /// Behavior is verified against the Bulbapedia page for Beak Blast (Generation VII). /// public class BeakBlastTests { /// /// Creates a fully mocked setup for driving . /// private static (BeakBlast script, ITurnChoice choice, IPokemon user, IScriptSet volatileSet, EventHook eventHook) CreateChargeSetup(bool hasBattleData = true) { var script = new BeakBlast(); var user = Substitute.For(); user.GetScripts().Returns(_ => new ScriptIterator(Array.Empty>())); IScriptSet volatileSet = new ScriptSet(user); 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 (script, choice, user, volatileSet, eventHook); } /// /// Bulbapedia: "The user charges up at the beginning of the turn and executes Beak Blast at -3 priority." /// Before the turn starts, the charging is attached to the user. /// [Test] public async Task OnBeforeTurnStart_UserInBattle_AddsChargeEffect() { // Arrange var (script, choice, _, volatileSet, _) = CreateChargeSetup(); // Act script.OnBeforeTurnStart(choice); // Assert await Assert.That(volatileSet.Get()).IsNotNull(); } /// /// Bulbapedia: "Its charging message is displayed before any other moves." /// The charge fires a dialog event when the turn starts. /// [Test] public async Task OnBeforeTurnStart_UserInBattle_FiresChargingMessage() { // Arrange var (script, choice, user, _, eventHook) = CreateChargeSetup(); DialogEvent? capturedEvent = null; eventHook.Handler += (_, args) => { if (args is DialogEvent dialogEvent) capturedEvent = dialogEvent; }; // Act script.OnBeforeTurnStart(choice); // Assert await Assert.That(capturedEvent).IsNotNull(); await Assert.That(capturedEvent!.Message).IsEqualTo("beak_blast_charge"); await Assert.That(capturedEvent.Parameters!["user"]).IsEqualTo(user); } /// /// Technical test: a Pokémon without is not in battle, so no charging /// phase starts. /// [Test] public async Task OnBeforeTurnStart_NoBattleData_DoesNotAddChargeEffect() { // Arrange var (script, choice, _, volatileSet, _) = CreateChargeSetup(false); // Act script.OnBeforeTurnStart(choice); // Assert await Assert.That(volatileSet.Get()).IsNull(); } /// /// Bulbapedia: "All who make contact with the user during the charging phase are burned." /// The charging phase ends when the move executes, so the is removed again. /// [Test] public async Task OnSecondaryEffect_MoveExecuted_RemovesChargeEffect() { // Arrange var script = new BeakBlast(); var move = Substitute.For(); var target = Substitute.For(); var user = Substitute.For(); user.GetScripts().Returns(_ => new ScriptIterator(Array.Empty>())); IScriptSet volatileSet = new ScriptSet(user); user.Volatile.Returns(volatileSet); move.User.Returns(user); volatileSet.Add(new BeakBlastEffect()); // Act script.OnSecondaryEffect(move, target, 0); // Assert await Assert.That(volatileSet.Get()).IsNull(); } /// /// Creates a fully mocked setup for driving . /// private static (BeakBlastEffect effect, IExecutingMove move, IPokemon target, IPokemon attacker) CreateIncomingHitSetup(bool isContact) { var effect = new BeakBlastEffect(); var move = Substitute.For(); var target = Substitute.For(); var hitData = Substitute.For(); hitData.IsContact.Returns(isContact); move.GetHitData(target, 0).Returns(hitData); var attacker = Substitute.For(); move.User.Returns(attacker); return (effect, move, target, attacker); } /// /// Helper to extract the status name from a Pokémon's received calls. /// private static string? GetStatusSet(IPokemon pokemon) { var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "SetStatus"); return call != null ? ((StringKey)call.GetArguments()[0]!).ToString() : null; } /// /// Bulbapedia: "All who make contact with the user during the charging phase are burned." /// [Test] public async Task OnIncomingHit_ContactMove_BurnsAttacker() { // Arrange var (effect, move, target, attacker) = CreateIncomingHitSetup(true); // Act effect.OnIncomingHit(move, target, 0); // Assert await Assert.That(GetStatusSet(attacker)).IsEqualTo("burned"); } /// /// Bulbapedia: "All who make contact with the user during the charging phase are burned." /// A hit that does not make contact must not burn the attacker. /// [Test] public async Task OnIncomingHit_NonContactMove_DoesNotBurnAttacker() { // Arrange var (effect, move, target, attacker) = CreateIncomingHitSetup(false); // Act effect.OnIncomingHit(move, target, 0); // Assert await Assert.That(attacker.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "SetStatus")).IsFalse(); } }