257 lines
10 KiB
C#
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();
|
|
}
|
|
} |