313 lines
12 KiB
C#
313 lines
12 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;
|
|
using PkmnLib.Static.Utils;
|
|
|
|
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
|
|
|
|
/// <summary>
|
|
/// Tests for the <see cref="Infestation"/> move script and its <see cref="InfestationEffect"/> 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."
|
|
/// </summary>
|
|
public class InfestationTests
|
|
{
|
|
/// <summary>
|
|
/// Test helper script that modifies the Infestation 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_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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a fully mocked test setup for Infestation tests. The target gets a real
|
|
/// <see cref="ScriptSet"/> as its volatile set, so the applied effect can be inspected and ticked.
|
|
/// Scripts passed as <paramref name="userScripts"/> are attached to the user, so the
|
|
/// <see cref="CustomTriggers.ModifyBind"/> trigger pass runs over them (the Grip Claw / Binding Band
|
|
/// stand-in).
|
|
/// </summary>
|
|
private static (Infestation script, IExecutingMove move, IPokemon user, IPokemon target, ScriptSet targetVolatile,
|
|
IBattleRandom random, IHitData hitData) CreateTestSetup(uint targetMaxHp = 80, params Script[] userScripts)
|
|
{
|
|
var script = new Infestation();
|
|
var move = Substitute.For<IExecutingMove>();
|
|
var user = Substitute.For<IPokemon>();
|
|
var containers = userScripts.Select(IEnumerable<ScriptContainer> (s) => new ScriptContainer(s)).ToArray();
|
|
user.GetScripts().Returns(_ => new ScriptIterator(containers));
|
|
move.User.Returns(user);
|
|
|
|
var random = Substitute.For<IBattleRandom>();
|
|
var battle = Substitute.For<IBattle>();
|
|
battle.Random.Returns(random);
|
|
var battleData = Substitute.For<IPokemonBattleData>();
|
|
battleData.Battle.Returns(battle);
|
|
user.BattleData.Returns(battleData);
|
|
|
|
var target = Substitute.For<IPokemon>();
|
|
target.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));
|
|
var targetVolatile = new ScriptSet(target);
|
|
target.Volatile.Returns(targetVolatile);
|
|
target.BoostedStats.Returns(new StatisticSet<uint>(targetMaxHp, 1, 1, 1, 1, 1));
|
|
|
|
var hitData = Substitute.For<IHitData>();
|
|
move.GetHitData(target, 0).Returns(hitData);
|
|
|
|
return (script, move, user, target, targetVolatile, random, hitData);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper to extract the damage amount from a Pokémon's received Damage calls.
|
|
/// </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>
|
|
/// Bulbapedia: "It also traps the target" — the move script applies the <see cref="InfestationEffect"/>
|
|
/// volatile, which implements the trap, to the target that was hit.
|
|
/// </summary>
|
|
[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<InfestationEffect>()).IsNotNull();
|
|
}
|
|
|
|
/// <summary>
|
|
/// A target that is already afflicted by an infestation cannot be infested again: the hit fails and no
|
|
/// second effect is added.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: the trap lasts "for four to five turns". With the low random roll the trap ticks four
|
|
/// times and is gone afterwards.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSecondaryEffect_LowDurationRoll_TrapLastsFourTurns()
|
|
{
|
|
// Arrange
|
|
var (script, move, _, target, targetVolatile, random, _) = CreateTestSetup();
|
|
random.GetBool().Returns(false);
|
|
var battle = Substitute.For<IBattle>();
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, target, 0);
|
|
var effect = targetVolatile.Get<InfestationEffect>()!;
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: the trap lasts "for four to five turns". With the high random roll the trap ticks five
|
|
/// times and is gone afterwards.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSecondaryEffect_HighDurationRoll_TrapLastsFiveTurns()
|
|
{
|
|
// Arrange
|
|
var (script, move, _, target, targetVolatile, random, _) = CreateTestSetup();
|
|
random.GetBool().Returns(true);
|
|
var battle = Substitute.For<IBattle>();
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, target, 0);
|
|
var effect = targetVolatile.Get<InfestationEffect>()!;
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "Infestation inflicts 1/8 of the target's maximum HP as damage per turn".
|
|
/// </summary>
|
|
[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<IPokemon>();
|
|
owner.BoostedStats.Returns(new StatisticSet<uint>(maxHealth, 1, 1, 1, 1, 1));
|
|
var effect = new InfestationEffect(owner, 4, 1f / 8f);
|
|
|
|
// Act
|
|
effect.OnEndTurn(owner, Substitute.For<IBattle>());
|
|
|
|
// Assert
|
|
await Assert.That(GetDamageAmount(owner)!.Value).IsEqualTo(expectedDamage);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: the per-turn damage is residual trap damage, not move damage, so it uses
|
|
/// <see cref="DamageSource.Misc"/>.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnEndTurn_TrappedPokemon_DamageIsIndirect()
|
|
{
|
|
// Arrange
|
|
var owner = Substitute.For<IPokemon>();
|
|
owner.BoostedStats.Returns(new StatisticSet<uint>(80, 1, 1, 1, 1, 1));
|
|
var effect = new InfestationEffect(owner, 4, 1f / 8f);
|
|
|
|
// Act
|
|
effect.OnEndTurn(owner, Substitute.For<IBattle>());
|
|
|
|
// Assert
|
|
var call = owner.ReceivedCalls().First(c => c.GetMethodInfo().Name == "Damage");
|
|
await Assert.That((DamageSource)call.GetArguments()[1]!).IsEqualTo(DamageSource.Misc);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: Infestation traps the target, "preventing switching".
|
|
/// </summary>
|
|
[Test]
|
|
public async Task PreventSelfSwitch_TrappedPokemon_CannotSwitchOut()
|
|
{
|
|
// Arrange
|
|
var effect = new InfestationEffect(Substitute.For<IPokemon>(), 4, 1f / 8f);
|
|
var prevent = false;
|
|
|
|
// Act
|
|
effect.PreventSelfSwitch(Substitute.For<ISwitchChoice>(), ref prevent);
|
|
|
|
// Assert
|
|
await Assert.That(prevent).IsTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: Infestation traps the target, preventing "escape".
|
|
/// </summary>
|
|
[Test]
|
|
public async Task PreventSelfRunAway_TrappedPokemon_CannotFlee()
|
|
{
|
|
// Arrange
|
|
var effect = new InfestationEffect(Substitute.For<IPokemon>(), 4, 1f / 8f);
|
|
var prevent = false;
|
|
|
|
// Act
|
|
effect.PreventSelfRunAway(Substitute.For<IFleeChoice>(), ref prevent);
|
|
|
|
// Assert
|
|
await Assert.That(prevent).IsTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "Binding Band increases per-turn damage from 1/8 to 1/6 of 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]
|
|
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<InfestationEffect>()!;
|
|
effect.OnEndTurn(target, Substitute.For<IBattle>());
|
|
|
|
// Assert - 1/6 of 120 max HP
|
|
await Assert.That(GetDamageAmount(target)!.Value).IsEqualTo(20u);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "Grip Claw extends duration to 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 a trap that is still active after six end-of-turn ticks.
|
|
/// </summary>
|
|
[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<IBattle>();
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, target, 0);
|
|
var effect = targetVolatile.Get<InfestationEffect>()!;
|
|
for (var i = 0; i < 6; i++)
|
|
effect.OnEndTurn(target, battle);
|
|
|
|
// Assert
|
|
await Assert.That(targetVolatile.Contains(new StringKey("infestation"))).IsTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Technical test: if the user has no <see cref="IPokemon.BattleData"/>, no effect is applied.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSecondaryEffect_UserHasNoBattleData_NoEffectAdded()
|
|
{
|
|
// Arrange
|
|
var (script, move, user, target, targetVolatile, _, _) = CreateTestSetup();
|
|
user.BattleData.Returns((IPokemonBattleData?)null);
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, target, 0);
|
|
|
|
// Assert
|
|
await Assert.That(targetVolatile.Count).IsEqualTo(0);
|
|
}
|
|
} |