using PkmnLib.Dynamic.Models; using PkmnLib.Plugin.Gen7.Scripts.Moves; using PkmnLib.Static; namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// /// Tests for the move script. /// Gen VII Bulbapedia behavior: "Belly Drum deducts half of the user's maximum HP (rounded down) from its /// current HP and, in return, it maximizes the user's Attack stat by raising it to +6 stages". /// public class BellyDrumTests { /// /// Creates a fully mocked test setup for Belly Drum. The target of the secondary effect is the user itself. /// private static (BellyDrum script, IExecutingMove move, IPokemon user, IHitData hitData) CreateTestSetup(uint maxHp, uint currentHp, sbyte attackBoost = 0) { var script = new BellyDrum(); var user = Substitute.For(); user.BoostedStats.Returns(new StatisticSet(maxHp, 10, 10, 10, 10, 10)); user.CurrentHealth.Returns(currentHp); user.StatBoost.Returns(new StatBoostStatisticSet(0, attackBoost, 0, 0, 0, 0)); var hitData = Substitute.For(); var move = Substitute.For(); move.GetHitData(user, 0).Returns(hitData); move.User.Returns(user); return (script, move, user, hitData); } /// /// Helper to extract the received Damage call from the user, if any. /// private static (uint damage, DamageSource source, bool forceDamage)? GetDamageCall(IPokemon user) { var call = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage"); if (call == null) return null; var args = call.GetArguments(); return ((uint)args[0]!, (DamageSource)args[1]!, (bool)args[3]!); } /// /// Helper to extract the received ChangeStatBoost call from the user, if any. /// private static (Statistic stat, sbyte change, bool selfInflicted)? GetStatBoostCall(IPokemon user) { var call = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "ChangeStatBoost"); if (call == null) return null; var args = call.GetArguments(); return ((Statistic)args[0]!, (sbyte)args[1]!, (bool)args[2]!); } /// /// Bulbapedia: "Belly Drum deducts half of the user's maximum HP (rounded down) from its current HP". /// Verifies the deduction amount, including the rounding down for odd maximum HP values. The deduction is /// a cost rather than regular damage, so it is forced damage. /// [Test, Arguments(100u, 50u), Arguments(101u, 50u), Arguments(99u, 49u), Arguments(255u, 127u)] public async Task OnSecondaryEffect_SufficientHp_DeductsHalfMaxHpRoundedDown(uint maxHp, uint expectedDeduction) { // Arrange var (script, move, user, _) = CreateTestSetup(maxHp, maxHp); // Act script.OnSecondaryEffect(move, user, 0); // Assert await Assert.That(GetDamageCall(user)).IsEqualTo((expectedDeduction, DamageSource.Misc, true)); } /// /// Bulbapedia: "it maximizes the user's Attack stat by raising it to +6 stages". /// The user receives a self-inflicted Attack boost large enough to reach the +6 cap. /// [Test] public async Task OnSecondaryEffect_SufficientHp_MaximizesAttack() { // Arrange var (script, move, user, _) = CreateTestSetup(100, 100); // Act script.OnSecondaryEffect(move, user, 0); // Assert var boostCall = GetStatBoostCall(user); await Assert.That(boostCall).IsNotNull(); await Assert.That(boostCall!.Value.stat).IsEqualTo(Statistic.Attack); await Assert.That(boostCall.Value.change).IsGreaterThanOrEqualTo((sbyte)6); await Assert.That(boostCall.Value.selfInflicted).IsTrue(); } /// /// Bulbapedia: "it maximizes the user's Attack stat by raising it to +6 stages, even if the user's /// temporary Attack bonus stages were below 0 prior to using Belly Drum." /// From -6 stages only a raise of at least 12 stages reaches +6. /// [Test] public async Task OnSecondaryEffect_AttackStagesBelowZero_StillRaisedToPlusSix() { // Arrange var (script, move, user, _) = CreateTestSetup(100, 100, -6); // Act script.OnSecondaryEffect(move, user, 0); // Assert var boostCall = GetStatBoostCall(user); await Assert.That(boostCall).IsNotNull(); await Assert.That(boostCall!.Value.change).IsGreaterThanOrEqualTo((sbyte)12); } /// /// Bulbapedia: "Belly Drum fails if the user's current HP is less than half its maximum". /// [Test] public void OnSecondaryEffect_CurrentHpBelowHalf_Fails() { // Arrange var (script, move, user, hitData) = CreateTestSetup(100, 49); // Act script.OnSecondaryEffect(move, user, 0); // Assert hitData.Received(1).Fail(); } /// /// Bulbapedia: "Belly Drum fails if the user's current HP is less than half its maximum". /// When the move fails, no HP is deducted and the Attack stat is not changed. /// [Test] public async Task OnSecondaryEffect_CurrentHpBelowHalf_NoHpDeductedAndNoAttackChange() { // Arrange var (script, move, user, _) = CreateTestSetup(100, 49); // Act script.OnSecondaryEffect(move, user, 0); // Assert await Assert.That(GetDamageCall(user)).IsNull(); await Assert.That(GetStatBoostCall(user)).IsNull(); } /// /// Bulbapedia: "Belly Drum fails if the user's current HP is less than half its maximum". /// At exactly half its maximum HP the user cannot pay the cost without fainting (deducting half of the /// maximum HP would reduce it to 0 HP), so the move fails as well. /// [Test] public void OnSecondaryEffect_CurrentHpExactlyHalf_Fails() { // Arrange var (script, move, user, hitData) = CreateTestSetup(100, 50); // Act script.OnSecondaryEffect(move, user, 0); // Assert hitData.Received(1).Fail(); } /// /// Bulbapedia: "Belly Drum fails if the user's current HP is less than half its maximum, or if the user's /// Attack is already at +6 (even if the user has Contrary)." /// [Test] public void OnSecondaryEffect_AttackAlreadyAtPlusSix_Fails() { // Arrange var (script, move, user, hitData) = CreateTestSetup(100, 100, 6); // Act script.OnSecondaryEffect(move, user, 0); // Assert hitData.Received(1).Fail(); } /// /// Bulbapedia: "Belly Drum fails if [...] the user's Attack is already at +6". /// A failed Belly Drum does not deduct HP: the HP cost only applies when the move works (the only /// exception Bulbapedia lists is a Contrary user already at -6 stages). /// [Test] public async Task OnSecondaryEffect_AttackAlreadyAtPlusSix_NoHpDeducted() { // Arrange var (script, move, user, _) = CreateTestSetup(100, 100, 6); // Act script.OnSecondaryEffect(move, user, 0); // Assert await Assert.That(GetDamageCall(user)).IsNull(); } }