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.Utils; namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// /// Tests for the move script and its volatile. /// Gen VII Bulbapedia behavior (Gen II base plus Gen V/VI deltas): "It also traps the target, preventing /// switching and escape", "Its effect will last either 4 or 5 turns", and "The end turn damage of Bind is /// increased from 1/16 to 1/8 of the target's maximum HP." /// public class BindTests { /// /// Test helper script that modifies the Bind duration/damage through the /// custom trigger, the same way the Grip Claw and Binding Band item scripts do. /// [Script(ScriptCategory.Pokemon, "test_modify_bind_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; } } private static (Bind script, IExecutingMove move, IPokemon user, IPokemon target, IScriptSet targetVolatile) CreateTestSetup(params Script[] userScripts) { var script = new Bind(); var move = Substitute.For(); var target = Substitute.For(); var targetVolatile = Substitute.For(); target.Volatile.Returns(targetVolatile); var user = Substitute.For(); // RunScriptHook iterates the user's scripts; give the mock a real iterator so the ModifyBind trigger // pass runs (empty unless the test attaches scripts such as the Grip Claw / Binding Band stand-in). var containers = userScripts.Select(IEnumerable (s) => new ScriptContainer(s)).ToArray(); user.GetScripts().Returns(_ => new ScriptIterator(containers)); move.User.Returns(user); return (script, move, user, target, targetVolatile); } /// /// Helper to extract the instance that was added to the target's volatile scripts. /// private static BindEffect? GetAddedEffect(IScriptSet targetVolatile) => targetVolatile.ReceivedCalls() .Where(c => c.GetMethodInfo().Name == "Add").Select(c => c.GetArguments()[0]).OfType() .FirstOrDefault(); /// /// Helper to extract the damage amount from the target's first received Damage call. /// private static uint? GetDamageAmount(IPokemon pokemon) { var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage"); return call != null ? (uint)call.GetArguments()[0]! : null; } /// /// Runs end-of-turn handling on the effect repeatedly and counts how many turns dealt damage to the target. /// private static int CountEndTurnDamageTicks(BindEffect effect, IPokemon target, int maxTurns = 10) { var battle = Substitute.For(); for (var i = 0; i < maxTurns; i++) effect.OnEndTurn(target, battle); return target.ReceivedCalls().Count(c => c.GetMethodInfo().Name == "Damage"); } /// /// Bulbapedia: "It also traps the target, preventing switching and escape." /// Using Bind adds the volatile to the target. /// [Test] public void OnSecondaryEffect_AddsBindEffectToTargetVolatile() { // Arrange var (script, move, _, target, targetVolatile) = CreateTestSetup(); // Act script.OnSecondaryEffect(move, target, 0); // Assert targetVolatile.Received(1).Add(Arg.Any()); } /// /// Bulbapedia: "The end turn damage of Bind is increased from 1/16 to 1/8 of the target's maximum HP." /// Tests several max HP values to ensure proper integer truncation of the 1/8 fraction. /// [Test, Arguments(160u, 20u), Arguments(100u, 12u), Arguments(15u, 1u)] public async Task OnSecondaryEffect_Default_EndTurnDamageIsOneEighthOfTargetMaxHp(uint maxHealth, uint expectedDamage) { // Arrange var (script, move, _, target, targetVolatile) = CreateTestSetup(); target.MaxHealth.Returns(maxHealth); // Act script.OnSecondaryEffect(move, target, 0); var effect = GetAddedEffect(targetVolatile); await Assert.That(effect).IsNotNull(); effect!.OnEndTurn(target, Substitute.For()); // Assert await Assert.That(GetDamageAmount(target)).IsEqualTo(expectedDamage); } /// /// Bulbapedia: "Its effect will last either 4 or 5 turns." /// The added effect deals its end-of-turn damage for 4 or 5 turns and then ends. /// [Test] public async Task OnSecondaryEffect_Default_EffectLastsFourOrFiveTurns() { // Arrange var (script, move, _, target, targetVolatile) = CreateTestSetup(); target.MaxHealth.Returns(160u); // Act script.OnSecondaryEffect(move, target, 0); var effect = GetAddedEffect(targetVolatile); await Assert.That(effect).IsNotNull(); var turnsWithDamage = CountEndTurnDamageTicks(effect!, target); // Assert await Assert.That(turnsWithDamage is 4 or 5).IsTrue(); } /// /// Bulbapedia: "If the user of Bind is holding a Grip Claw, the duration will be 7 turns." /// The duration change flows through the custom trigger; a script /// on the user that sets the duration to 7 results in an effect lasting 7 turns. /// [Test] public async Task OnSecondaryEffect_DurationSetToSevenByTrigger_EffectLastsSevenTurns() { // Arrange var (script, move, _, target, targetVolatile) = CreateTestSetup(new ModifyBindTrigger(7)); target.MaxHealth.Returns(160u); // Act script.OnSecondaryEffect(move, target, 0); var effect = GetAddedEffect(targetVolatile); await Assert.That(effect).IsNotNull(); var turnsWithDamage = CountEndTurnDamageTicks(effect!, target); // Assert await Assert.That(turnsWithDamage).IsEqualTo(7); } /// /// Bulbapedia: "If the user is holding a Binding Band, the end turn damage of Bind will increase to 1/6 of /// the target's 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, Arguments(60u, 10u), Arguments(48u, 8u)] public async Task OnSecondaryEffect_DamageSetToOneSixthByTrigger_EndTurnDamageIsOneSixth(uint maxHealth, uint expectedDamage) { // Arrange var (script, move, _, target, targetVolatile) = CreateTestSetup(new ModifyBindTrigger(damagePercent: 1f / 6f)); target.MaxHealth.Returns(maxHealth); // Act script.OnSecondaryEffect(move, target, 0); var effect = GetAddedEffect(targetVolatile); await Assert.That(effect).IsNotNull(); effect!.OnEndTurn(target, Substitute.For()); // Assert await Assert.That(GetDamageAmount(target)).IsEqualTo(expectedDamage); } /// /// Bulbapedia: "It also traps the target, preventing switching and escape." /// While the effect is active, the target cannot switch out. /// [Test] public async Task BindEffect_WhileActive_PreventsSwitching() { // Arrange var target = Substitute.For(); var effect = new BindEffect(target, 5, 1f / 8f); var prevent = false; // Act effect.PreventSelfSwitch(Substitute.For(), ref prevent); // Assert await Assert.That(prevent).IsTrue(); } /// /// Bulbapedia: "It also traps the target, preventing switching and escape." /// While the effect is active, the target cannot flee. /// [Test] public async Task BindEffect_WhileActive_PreventsRunningAway() { // Arrange var target = Substitute.For(); var effect = new BindEffect(target, 5, 1f / 8f); var prevent = false; // Act effect.PreventSelfRunAway(Substitute.For(), ref prevent); // Assert await Assert.That(prevent).IsTrue(); } /// /// Bulbapedia: "Its effect will last either 4 or 5 turns." /// Once the duration has run out, the target is no longer prevented from switching. /// [Test] public async Task BindEffect_AfterDurationExpires_NoLongerPreventsSwitching() { // Arrange var target = Substitute.For(); target.MaxHealth.Returns(160u); var effect = new BindEffect(target, 1, 1f / 8f); // Act - the single remaining turn elapses effect.OnEndTurn(target, Substitute.For()); var prevent = false; effect.PreventSelfSwitch(Substitute.For(), ref prevent); // Assert await Assert.That(prevent).IsFalse(); } }