using PkmnLib.Dynamic.Models; using PkmnLib.Dynamic.ScriptHandling; using PkmnLib.Plugin.Gen7.Scripts.Abilities; using PkmnLib.Plugin.Gen7.Scripts.Moves; using PkmnLib.Static.Utils; namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// /// Tests for the move script. /// Gen VII Bulbapedia behavior: "Flare Blitz inflicts damage, and the user receives recoil damage equal to /// ⅓ of the damage done to the target. This move has a 10% chance of burning the target." /// public class FlareBlitzTests { /// /// Creates a fully mocked test setup for Flare Blitz tests, where the hit dealt /// damage to the target. /// private static (FlareBlitz script, IExecutingMove move, IBattlePokemon user, IBattlePokemon target, IBattleRandom random) CreateTestSetup(uint damage, Script[]? moveScripts = null) { var script = new FlareBlitz(); var move = Substitute.For(); var user = Substitute.For(); var target = Substitute.For(); var hitData = Substitute.For(); hitData.Damage.Returns(damage); move.GetHitData(target, 0).Returns(hitData); var battle = Substitute.For(); var random = Substitute.For(); battle.Random.Returns(random); target.Battle.Returns(battle); // RunScriptHook iterates the move's scripts; give the mock a real iterator so the hook pass runs // (empty unless the test attaches scripts such as Rock Head). var containers = (moveScripts ?? []).Select(IEnumerable (s) => new ScriptContainer(s)) .ToArray(); move.GetScripts().Returns(_ => new ScriptIterator(containers)); move.User.Returns(user); return (script, move, user, target, random); } /// /// Helper to extract the damage amount from a Pokémon's received Damage calls, or 0 if none was received. /// private static uint GetDamageAmount(IBattlePokemon pokemon) { var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage"); return call != null ? (uint)call.GetArguments()[0]! : 0; } /// /// Helper to extract the damage source from a Pokémon's received Damage calls. /// private static DamageSource? GetDamageSource(IBattlePokemon pokemon) { var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage"); return call != null ? (DamageSource)call.GetArguments()[1]! : null; } /// /// Helper that checks whether a Pokémon received a SetStatus call for the given status. /// private static bool ReceivedSetStatus(IBattlePokemon pokemon, string status) => pokemon.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "SetStatus" && (StringKey)c.GetArguments()[0]! == new StringKey(status)); /// /// Bulbapedia: "the user receives recoil damage equal to ⅓ of the damage done to the target." /// [Test] public async Task OnSecondaryEffect_DamageDealt_UserTakesOneThirdRecoil() { // Arrange var (script, move, user, target, _) = CreateTestSetup(90); // Act script.OnSecondaryEffect(move, target, 0); // Assert - 90 / 3 = 30 await Assert.That(GetDamageAmount(user)).IsEqualTo(30u); } /// /// Bulbapedia: "the user receives recoil damage equal to ⅓ of the damage done to the target." /// Tests various damage values to ensure proper integer truncation of the one-third amount. /// [Test, Arguments(90u, 30u), Arguments(100u, 33u), Arguments(120u, 40u), Arguments(2u, 0u), Arguments(301u, 100u)] public async Task OnSecondaryEffect_DamageDealt_RecoilCalculation(uint damage, uint expectedRecoil) { // Arrange var (script, move, user, target, _) = CreateTestSetup(damage); // Act script.OnSecondaryEffect(move, target, 0); // Assert await Assert.That(GetDamageAmount(user)).IsEqualTo(expectedRecoil); } /// /// Bulbapedia: "the user receives recoil damage". Recoil is indirect damage, so it is dealt with /// rather than move damage. /// [Test] public async Task OnSecondaryEffect_DamageDealt_RecoilUsesMiscDamageSource() { // Arrange var (script, move, user, target, _) = CreateTestSetup(90); // Act script.OnSecondaryEffect(move, target, 0); // Assert await Assert.That(GetDamageSource(user)!.Value).IsEqualTo(DamageSource.Misc); } /// /// Bulbapedia: "This move has a 10% chance of burning the target." /// When the effect chance roll succeeds, the target is burned. /// [Test] public async Task OnSecondaryEffect_EffectChanceSucceeds_BurnsTarget() { // Arrange var (script, move, _, target, random) = CreateTestSetup(90); random.EffectChance(10f, move, target, 0).Returns(true); // Act script.OnSecondaryEffect(move, target, 0); // Assert await Assert.That(ReceivedSetStatus(target, "burned")).IsTrue(); } /// /// Bulbapedia: "This move has a 10% chance of burning the target." /// When the effect chance roll fails, the target is not burned. /// [Test] public async Task OnSecondaryEffect_EffectChanceFails_DoesNotBurnTarget() { // Arrange var (script, move, _, target, random) = CreateTestSetup(90); random.EffectChance(10f, move, target, 0).Returns(false); // Act script.OnSecondaryEffect(move, target, 0); // Assert await Assert.That(ReceivedSetStatus(target, "burned")).IsFalse(); } /// /// Bulbapedia: "This move has a 10% chance of burning the target." /// The 10% chance is rolled through the battle's so that /// effect-chance-modifying effects (such as Serene Grace) apply. /// [Test] public async Task OnSecondaryEffect_DamageDealt_RollsTenPercentEffectChance() { // Arrange var (script, move, _, target, random) = CreateTestSetup(90); // Act script.OnSecondaryEffect(move, target, 0); // Assert random.Received(1).EffectChance(10f, move, target, 0); } /// /// The recoil can be prevented by recoil-negating effects (Bulbapedia: Rock Head "prevents the Pokémon /// from taking recoil damage from most moves"). When a script such as flags the /// recoil as prevented, the user takes no recoil damage. /// [Test] public async Task OnSecondaryEffect_RecoilPrevented_UserTakesNoRecoilDamage() { // Arrange var (script, move, user, target, _) = CreateTestSetup(90, [new RockHead()]); // Act script.OnSecondaryEffect(move, target, 0); // Assert await Assert.That(user.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Damage")).IsFalse(); } /// /// Bulbapedia: "This move has a 10% chance of burning the target." The burn chance is independent of the /// recoil: Rock Head only "prevents the Pokémon from taking recoil damage", so preventing the recoil must /// not suppress the burn chance. /// [Test] public async Task OnSecondaryEffect_RecoilPrevented_BurnChanceStillApplies() { // Arrange var (script, move, _, target, random) = CreateTestSetup(90, [new RockHead()]); random.EffectChance(10f, move, target, 0).Returns(true); // Act script.OnSecondaryEffect(move, target, 0); // Assert await Assert.That(ReceivedSetStatus(target, "burned")).IsTrue(); } }