using PkmnLib.Dynamic.Models; using PkmnLib.Dynamic.Models.Choices; using PkmnLib.Dynamic.ScriptHandling; using PkmnLib.Dynamic.ScriptHandling.Registry; using PkmnLib.Plugin.Gen7.Scripts; using PkmnLib.Plugin.Gen7.Scripts.Moves; using PkmnLib.Plugin.Gen7.Scripts.Pokemon; using PkmnLib.Static; using PkmnLib.Static.Utils; namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// /// Tests for the move script and its volatile. /// Gen VII Bulbapedia behavior: "Infestation inflicts 1/8 of the target's maximum HP as damage per turn for /// four to five turns upon use, in addition to the damage dealt when it is used. It also traps the target, /// preventing switching and escape." "Grip Claw extends duration to 7 turns" and "Binding Band increases /// per-turn damage from 1/8 to 1/6 of maximum HP." /// public class InfestationTests { /// /// Test helper script that modifies the Infestation duration/damage through the /// custom trigger, the same way the Grip Claw and Binding Band /// item scripts do. /// [Script(ScriptCategory.Pokemon, "test_modify_infestation_trigger")] private class ModifyBindTrigger : Script, IScriptCustomTrigger { private readonly int? _duration; private readonly float? _damagePercent; public ModifyBindTrigger(int? duration = null, float? damagePercent = null) { _duration = duration; _damagePercent = damagePercent; } public void CustomTrigger(StringKey eventName, ICustomTriggerArgs args) { if (eventName != CustomTriggers.ModifyBind || args is not CustomTriggers.ModifyBindArgs bindArgs) return; if (_duration.HasValue) bindArgs.Duration = _duration.Value; if (_damagePercent.HasValue) bindArgs.DamagePercent = _damagePercent.Value; } } /// /// Creates a fully mocked test setup for Infestation tests. The target gets a real /// as its volatile set, so the applied effect can be inspected and ticked. /// Scripts passed as are attached to the user, so the /// trigger pass runs over them (the Grip Claw / Binding Band /// stand-in). /// private static (Infestation script, IExecutingMove move, IBattlePokemon user, IBattlePokemon target, ScriptSet targetVolatile, IBattleRandom random, IHitData hitData) CreateTestSetup(uint targetMaxHp = 80, params Script[] userScripts) { var script = new Infestation(); var move = Substitute.For(); var user = Substitute.For(); var containers = userScripts.Select(IEnumerable (s) => new ScriptContainer(s)).ToArray(); user.GetScripts().Returns(_ => new ScriptIterator(containers)); move.User.Returns(user); var random = Substitute.For(); var battle = Substitute.For(); battle.Random.Returns(random); user.Battle.Returns(battle); var target = Substitute.For(); target.GetScripts().Returns(_ => new ScriptIterator(new List>())); var targetVolatile = new ScriptSet(target); target.Volatile.Returns(targetVolatile); target.BoostedStats.Returns(new StatisticSet(targetMaxHp, 1, 1, 1, 1, 1)); var hitData = Substitute.For(); move.GetHitData(target, 0).Returns(hitData); return (script, move, user, target, targetVolatile, random, hitData); } /// /// Helper to extract the damage amount from a Pokémon's received Damage calls. /// private static uint? GetDamageAmount(IBattlePokemon pokemon) { var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage"); return call != null ? (uint)call.GetArguments()[0]! : null; } /// /// Bulbapedia: "It also traps the target" — the move script applies the /// volatile, which implements the trap, to the target that was hit. /// [Test] public async Task OnSecondaryEffect_MoveHits_AddsInfestationEffectToTarget() { // Arrange var (script, move, _, target, targetVolatile, _, _) = CreateTestSetup(); // Act script.OnSecondaryEffect(move, target, 0); // Assert await Assert.That(targetVolatile.Get()).IsNotNull(); } /// /// A target that is already afflicted by an infestation cannot be infested again: the hit fails and no /// second effect is added. /// [Test] public async Task OnSecondaryEffect_TargetAlreadyInfested_HitFails() { // Arrange var (script, move, _, target, targetVolatile, _, hitData) = CreateTestSetup(); targetVolatile.Add(new InfestationEffect(target, 4, 1f / 8f)); // Act script.OnSecondaryEffect(move, target, 0); // Assert hitData.Received(1).Fail(); await Assert.That(targetVolatile.Count).IsEqualTo(1); } /// /// Bulbapedia: the trap lasts "for four to five turns". With the low random roll the trap ticks four /// times and is gone afterwards. /// [Test] public async Task OnSecondaryEffect_LowDurationRoll_TrapLastsFourTurns() { // Arrange var (script, move, _, target, targetVolatile, random, _) = CreateTestSetup(); random.GetBool().Returns(false); var battle = Substitute.For(); // Act script.OnSecondaryEffect(move, target, 0); var effect = targetVolatile.Get()!; for (var i = 0; i < 3; i++) effect.OnEndTurn(target, battle); var presentAfterThreeTurns = targetVolatile.Contains(new StringKey("infestation")); effect.OnEndTurn(target, battle); // Assert await Assert.That(presentAfterThreeTurns).IsTrue(); await Assert.That(targetVolatile.Contains(new StringKey("infestation"))).IsFalse(); } /// /// Bulbapedia: the trap lasts "for four to five turns". With the high random roll the trap ticks five /// times and is gone afterwards. /// [Test] public async Task OnSecondaryEffect_HighDurationRoll_TrapLastsFiveTurns() { // Arrange var (script, move, _, target, targetVolatile, random, _) = CreateTestSetup(); random.GetBool().Returns(true); var battle = Substitute.For(); // Act script.OnSecondaryEffect(move, target, 0); var effect = targetVolatile.Get()!; for (var i = 0; i < 4; i++) effect.OnEndTurn(target, battle); var presentAfterFourTurns = targetVolatile.Contains(new StringKey("infestation")); effect.OnEndTurn(target, battle); // Assert await Assert.That(presentAfterFourTurns).IsTrue(); await Assert.That(targetVolatile.Contains(new StringKey("infestation"))).IsFalse(); } /// /// Bulbapedia: "Infestation inflicts 1/8 of the target's maximum HP as damage per turn". /// [Test, Arguments(96u, 12u), Arguments(100u, 12u), Arguments(8u, 1u)] public async Task OnEndTurn_TrappedPokemon_TakesOneEighthOfMaxHpAsDamage(uint maxHealth, uint expectedDamage) { // Arrange var owner = Substitute.For(); owner.BoostedStats.Returns(new StatisticSet(maxHealth, 1, 1, 1, 1, 1)); var effect = new InfestationEffect(owner, 4, 1f / 8f); // Act effect.OnEndTurn(owner, Substitute.For()); // Assert await Assert.That(GetDamageAmount(owner)!.Value).IsEqualTo(expectedDamage); } /// /// Bulbapedia: the per-turn damage is residual trap damage, not move damage, so it uses /// . /// [Test] public async Task OnEndTurn_TrappedPokemon_DamageIsIndirect() { // Arrange var owner = Substitute.For(); owner.BoostedStats.Returns(new StatisticSet(80, 1, 1, 1, 1, 1)); var effect = new InfestationEffect(owner, 4, 1f / 8f); // Act effect.OnEndTurn(owner, Substitute.For()); // Assert var call = owner.ReceivedCalls().First(c => c.GetMethodInfo().Name == "Damage"); await Assert.That((DamageSource)call.GetArguments()[1]!).IsEqualTo(DamageSource.Misc); } /// /// Bulbapedia: Infestation traps the target, "preventing switching". /// [Test] public async Task PreventSelfSwitch_TrappedPokemon_CannotSwitchOut() { // Arrange var effect = new InfestationEffect(Substitute.For(), 4, 1f / 8f); var prevent = false; // Act effect.PreventSelfSwitch(Substitute.For(), ref prevent); // Assert await Assert.That(prevent).IsTrue(); } /// /// Bulbapedia: Infestation traps the target, preventing "escape". /// [Test] public async Task PreventSelfRunAway_TrappedPokemon_CannotFlee() { // Arrange var effect = new InfestationEffect(Substitute.For(), 4, 1f / 8f); var prevent = false; // Act effect.PreventSelfRunAway(Substitute.For(), ref prevent); // Assert await Assert.That(prevent).IsTrue(); } /// /// Bulbapedia: "Binding Band increases per-turn damage from 1/8 to 1/6 of maximum HP." /// The damage change flows through the custom trigger; a script /// on the user that sets the damage to 1/6 results in end-of-turn damage of 1/6 max HP. /// [Test] public async Task OnEndTurn_DamageSetToOneSixthByTrigger_DamageIsOneSixthOfMaxHp() { // Arrange var (script, move, _, target, targetVolatile, _, _) = CreateTestSetup(120, new ModifyBindTrigger(damagePercent: 1f / 6f)); // Act script.OnSecondaryEffect(move, target, 0); var effect = targetVolatile.Get()!; effect.OnEndTurn(target, Substitute.For()); // Assert - 1/6 of 120 max HP await Assert.That(GetDamageAmount(target)!.Value).IsEqualTo(20u); } /// /// Bulbapedia: "Grip Claw extends duration to 7 turns." The duration change flows through the /// custom trigger; a script on the user that sets the duration /// to 7 results in a trap that is still active after six end-of-turn ticks. /// [Test] public async Task OnSecondaryEffect_DurationSetToSevenByTrigger_TrapLastsSevenTurns() { // Arrange var (script, move, _, target, targetVolatile, random, _) = CreateTestSetup(80, new ModifyBindTrigger(7)); random.GetBool().Returns(false); var battle = Substitute.For(); // Act script.OnSecondaryEffect(move, target, 0); var effect = targetVolatile.Get()!; for (var i = 0; i < 6; i++) effect.OnEndTurn(target, battle); // Assert await Assert.That(targetVolatile.Contains(new StringKey("infestation"))).IsTrue(); } }