Files
PkmnLib.NET/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BindTests.cs
Deukhoofd cc091d5327
Some checks failed
Build / Build (push) Failing after 35s
Update TUnit, warning cleanup
2026-07-05 13:46:57 +02:00

257 lines
10 KiB
C#

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;
/// <summary>
/// Tests for the <see cref="Bind"/> move script and its <see cref="BindEffect"/> 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."
/// </summary>
public class BindTests
{
/// <summary>
/// Test helper script that modifies the Bind duration/damage through the <see cref="CustomTriggers.ModifyBind"/>
/// custom trigger, the same way the Grip Claw and Binding Band item scripts do.
/// </summary>
[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<IExecutingMove>();
var target = Substitute.For<IPokemon>();
var targetVolatile = Substitute.For<IScriptSet>();
target.Volatile.Returns(targetVolatile);
var user = Substitute.For<IPokemon>();
// 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<ScriptContainer> (s) => new ScriptContainer(s)).ToArray();
user.GetScripts().Returns(_ => new ScriptIterator(containers));
move.User.Returns(user);
return (script, move, user, target, targetVolatile);
}
/// <summary>
/// Helper to extract the <see cref="BindEffect"/> instance that was added to the target's volatile scripts.
/// </summary>
private static BindEffect? GetAddedEffect(IScriptSet targetVolatile) => targetVolatile.ReceivedCalls()
.Where(c => c.GetMethodInfo().Name == "Add").Select(c => c.GetArguments()[0]).OfType<BindEffect>()
.FirstOrDefault();
/// <summary>
/// Helper to extract the damage amount from the target's first received Damage call.
/// </summary>
private static uint? GetDamageAmount(IPokemon pokemon)
{
var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage");
return call != null ? (uint)call.GetArguments()[0]! : null;
}
/// <summary>
/// Runs end-of-turn handling on the effect repeatedly and counts how many turns dealt damage to the target.
/// </summary>
private static int CountEndTurnDamageTicks(BindEffect effect, IPokemon target, int maxTurns = 10)
{
var battle = Substitute.For<IBattle>();
for (var i = 0; i < maxTurns; i++)
effect.OnEndTurn(target, battle);
return target.ReceivedCalls().Count(c => c.GetMethodInfo().Name == "Damage");
}
/// <summary>
/// Bulbapedia: "It also traps the target, preventing switching and escape."
/// Using Bind adds the <see cref="BindEffect"/> volatile to the target.
/// </summary>
[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<BindEffect>());
}
/// <summary>
/// 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.
/// </summary>
[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<IBattle>());
// Assert
await Assert.That(GetDamageAmount(target)!.Value).IsEqualTo(expectedDamage);
}
/// <summary>
/// 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.
/// </summary>
[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();
}
/// <summary>
/// Bulbapedia: "If the user of Bind is holding a Grip Claw, the duration will be 7 turns."
/// The duration change flows through the <see cref="CustomTriggers.ModifyBind"/> custom trigger; a script
/// on the user that sets the duration to 7 results in an effect lasting 7 turns.
/// </summary>
[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);
}
/// <summary>
/// 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 <see cref="CustomTriggers.ModifyBind"/> 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.
/// </summary>
[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<IBattle>());
// Assert
await Assert.That(GetDamageAmount(target)!.Value).IsEqualTo(expectedDamage);
}
/// <summary>
/// Bulbapedia: "It also traps the target, preventing switching and escape."
/// While the effect is active, the target cannot switch out.
/// </summary>
[Test]
public async Task BindEffect_WhileActive_PreventsSwitching()
{
// Arrange
var target = Substitute.For<IPokemon>();
var effect = new BindEffect(target, 5, 1f / 8f);
var prevent = false;
// Act
effect.PreventSelfSwitch(Substitute.For<ISwitchChoice>(), ref prevent);
// Assert
await Assert.That(prevent).IsTrue();
}
/// <summary>
/// Bulbapedia: "It also traps the target, preventing switching and escape."
/// While the effect is active, the target cannot flee.
/// </summary>
[Test]
public async Task BindEffect_WhileActive_PreventsRunningAway()
{
// Arrange
var target = Substitute.For<IPokemon>();
var effect = new BindEffect(target, 5, 1f / 8f);
var prevent = false;
// Act
effect.PreventSelfRunAway(Substitute.For<IFleeChoice>(), ref prevent);
// Assert
await Assert.That(prevent).IsTrue();
}
/// <summary>
/// Bulbapedia: "Its effect will last either 4 or 5 turns."
/// Once the duration has run out, the target is no longer prevented from switching.
/// </summary>
[Test]
public async Task BindEffect_AfterDurationExpires_NoLongerPreventsSwitching()
{
// Arrange
var target = Substitute.For<IPokemon>();
target.MaxHealth.Returns(160u);
var effect = new BindEffect(target, 1, 1f / 8f);
// Act - the single remaining turn elapses
effect.OnEndTurn(target, Substitute.For<IBattle>());
var prevent = false;
effect.PreventSelfSwitch(Substitute.For<ISwitchChoice>(), ref prevent);
// Assert
await Assert.That(prevent).IsFalse();
}
}