using PkmnLib.Dynamic.Models; using PkmnLib.Dynamic.ScriptHandling; using PkmnLib.Dynamic.ScriptHandling.Registry; using PkmnLib.Plugin.Gen7.Scripts.Moves; using PkmnLib.Plugin.Gen7.Scripts.Side; namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// /// Tests for the move script and its . /// Gen VII Bulbapedia behavior: "On the turn Doom Desire is selected, this attack will do nothing other /// than state that the user has chosen Doom Desire as its destiny. Two turns later, Doom Desire will do /// damage against the target." /// public class DoomDesireTests { /// /// Creates a fully mocked test setup where the target is at the given position on a side whose /// volatile scripts are a real . /// private static (DoomDesire script, IExecutingMove move, IPokemon target, IBattleSide side, ScriptSet sideScripts, IHitData hitData) CreateTestSetup(byte position = 0, uint damage = 100) { var script = new DoomDesire(); var move = Substitute.For(); var side = Substitute.For(); var sideScripts = new ScriptSet(side); side.VolatileScripts.Returns(sideScripts); side.GetScripts().Returns(_ => new ScriptIterator(new List>())); var battle = Substitute.For(); battle.Sides.Returns(new[] { side }); var target = Substitute.For(); var battleData = Substitute.For(); battleData.Battle.Returns(battle); battleData.SideIndex.Returns((byte)0); battleData.Position.Returns(position); target.BattleData.Returns(battleData); side.Pokemon.Returns(new IPokemon?[] { target }); var hitData = Substitute.For(); hitData.Damage.Returns(damage); move.GetHitData(target, 0).Returns(hitData); return (script, move, target, side, sideScripts, hitData); } /// /// Bulbapedia: "On the turn Doom Desire is selected, this attack will do nothing" — the immediate hit /// is blocked, and the pending damage is stored as a on the target's /// side. /// [Test] public async Task BlockOutgoingHit_FirstUse_BlocksHitAndAddsSideEffect() { // Arrange var (script, move, target, _, sideScripts, _) = CreateTestSetup(); var block = false; // Act script.BlockOutgoingHit(move, target, 0, ref block); // Assert await Assert.That(block).IsTrue(); await Assert.That(sideScripts.Contains(ScriptUtils.ResolveName())).IsTrue(); } /// /// The pending hit is registered against the target's position, so the effect knows where the damage /// must land two turns later. /// [Test] public async Task BlockOutgoingHit_FirstUse_RegistersTargetPosition() { // Arrange var (script, move, target, _, sideScripts, _) = CreateTestSetup(); var block = false; // Act script.BlockOutgoingHit(move, target, 0, ref block); // Assert await Assert.That(sideScripts.TryGet(out var effect)).IsTrue(); await Assert.That(effect!.HasTarget(0)).IsTrue(); } /// /// Bulbapedia: Doom Desire fails when it is already in effect against the target. The second use /// fails the hit instead of queueing a second strike. /// [Test] public async Task BlockOutgoingHit_AlreadyPendingForTarget_FailsHit() { // Arrange var (script, move, target, _, _, hitData) = CreateTestSetup(); var block = false; script.BlockOutgoingHit(move, target, 0, ref block); block = false; // Act script.BlockOutgoingHit(move, target, 0, ref block); // Assert hitData.Received(1).Fail(); await Assert.That(block).IsFalse(); } /// /// Bulbapedia: "Two turns later, Doom Desire will do damage against the target." One end-of-turn tick /// after selection (the selection turn itself) is not enough for the strike to land. /// [Test] public async Task OnEndTurn_TwoTurnTicks_DoesNotDamageTargetYet() { // Arrange var (script, move, target, side, sideScripts, _) = CreateTestSetup(); var block = false; script.BlockOutgoingHit(move, target, 0, ref block); await Assert.That(sideScripts.TryGet(out var effect)).IsTrue(); var battle = Substitute.For(); // Act - end of the selection turn and of the first turn after it effect!.OnEndTurn(side, battle); effect.OnEndTurn(side, battle); // Assert await Assert.That(target.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Damage")).IsFalse(); } /// /// Bulbapedia: "Two turns later, Doom Desire will do damage against the target." At the end of the /// second turn after selection the stored damage is dealt to the Pokémon at the registered position. /// [Test] public async Task OnEndTurn_ThreeTurnTicks_DamagesTargetWithStoredDamage() { // Arrange var (script, move, target, side, sideScripts, _) = CreateTestSetup(damage: 123); var block = false; script.BlockOutgoingHit(move, target, 0, ref block); await Assert.That(sideScripts.TryGet(out var effect)).IsTrue(); var battle = Substitute.For(); // Act - selection turn plus the two turns after it effect!.OnEndTurn(side, battle); effect.OnEndTurn(side, battle); effect.OnEndTurn(side, battle); // Assert var damageCall = target.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage"); await Assert.That(damageCall).IsNotNull(); await Assert.That((uint)damageCall!.GetArguments()[0]!).IsEqualTo(123u); } /// /// Once the pending strike has landed, the effect has no remaining targets and removes itself from /// the side. /// [Test] public async Task OnEndTurn_StrikeLanded_EffectRemovesItself() { // Arrange var (script, move, target, side, sideScripts, _) = CreateTestSetup(); var block = false; script.BlockOutgoingHit(move, target, 0, ref block); await Assert.That(sideScripts.TryGet(out var effect)).IsTrue(); var battle = Substitute.For(); // Act effect!.OnEndTurn(side, battle); effect.OnEndTurn(side, battle); effect.OnEndTurn(side, battle); // Assert await Assert.That(sideScripts.Contains(ScriptUtils.ResolveName())).IsFalse(); } /// /// Technical test: without battle data on the target (outside of battle) the script does nothing and /// does not throw. /// [Test] public async Task BlockOutgoingHit_TargetHasNoBattleData_DoesNotBlock() { // Arrange var script = new DoomDesire(); var move = Substitute.For(); var target = Substitute.For(); target.BattleData.Returns((IPokemonBattleData?)null); var block = false; // Act script.BlockOutgoingHit(move, target, 0, ref block); // Assert await Assert.That(block).IsFalse(); } }