This commit is contained in:
@@ -0,0 +1,327 @@
|
||||
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="MagmaStorm"/> move script and its <see cref="MagmaStormEffect"/> volatile.
|
||||
/// Gen VII Bulbapedia behavior: Magma Storm deals damage and binds the target. Generation IV: "A bound target
|
||||
/// cannot switch out or flee, and takes damage ... at the end of each turn." Generation V: the trapping
|
||||
/// duration changed to four to five turns. Generations VI and VII: the per-turn damage is increased to 1/8 of
|
||||
/// the target's maximum HP, a Binding Band boosts it to 1/6, and "Ghost-type Pokémon cannot be trapped by
|
||||
/// Magma Storm."
|
||||
/// The trapping and end-of-turn damage themselves are implemented by the <see cref="MagmaStormEffect"/>
|
||||
/// volatile; the move script determines the duration and damage fraction through the
|
||||
/// <see cref="CustomTriggers.ModifyBind"/> custom trigger and applies the volatile to the target.
|
||||
/// </summary>
|
||||
public class MagmaStormTests
|
||||
{
|
||||
/// <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_magma_storm_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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a fully mocked test setup for Magma Storm tests. The target gets a real
|
||||
/// <see cref="ScriptSet"/> as its volatile set, so the applied effect can be inspected. The user's script
|
||||
/// iterator is real but empty unless the test attaches scripts (such as the Grip Claw / Binding Band
|
||||
/// stand-in), so the <see cref="CustomTriggers.ModifyBind"/> trigger pass runs.
|
||||
/// </summary>
|
||||
private static (MagmaStorm script, IExecutingMove move, IPokemon target, IScriptSet targetVolatile) CreateTestSetup(
|
||||
params Script[] userScripts)
|
||||
{
|
||||
var script = new MagmaStorm();
|
||||
var move = Substitute.For<IExecutingMove>();
|
||||
var target = Substitute.For<IPokemon>();
|
||||
target.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));
|
||||
var targetVolatile = new ScriptSet(target);
|
||||
target.Volatile.Returns(targetVolatile);
|
||||
|
||||
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);
|
||||
|
||||
return (script, move, target, targetVolatile);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia: "Magma Storm deals damage and binds the target". The move script applies the
|
||||
/// <see cref="MagmaStormEffect"/> volatile, which implements the trap, to the target that was hit.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnSecondaryEffect_MoveHits_AddsMagmaStormEffectToTargetVolatile()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, target, targetVolatile) = CreateTestSetup();
|
||||
|
||||
// Act
|
||||
script.OnSecondaryEffect(move, target, 0);
|
||||
|
||||
// Assert
|
||||
await Assert.That(targetVolatile.Get<MagmaStormEffect>()).IsNotNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Technical test: hitting a target that is already trapped by Magma Storm does not add a second
|
||||
/// <see cref="MagmaStormEffect"/>; the existing volatile is reused.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnSecondaryEffect_TargetAlreadyTrapped_DoesNotAddSecondEffect()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, target, targetVolatile) = CreateTestSetup();
|
||||
|
||||
// Act
|
||||
script.OnSecondaryEffect(move, target, 0);
|
||||
script.OnSecondaryEffect(move, target, 0);
|
||||
|
||||
// Assert
|
||||
await Assert.That(targetVolatile.Count).IsEqualTo(1);
|
||||
}
|
||||
|
||||
// ----- MagmaStormEffect behavior (the volatile the move applies) -----
|
||||
|
||||
/// <summary>
|
||||
/// Creates a trapped Pokémon substitute with the given maximum HP, together with the
|
||||
/// <see cref="MagmaStormEffect"/> that traps it.
|
||||
/// </summary>
|
||||
private static (MagmaStormEffect effect, IPokemon owner) CreateTrappedPokemon(uint maxHealth,
|
||||
float percentOfMaxHealth = 1f / 8f)
|
||||
{
|
||||
var owner = Substitute.For<IPokemon>();
|
||||
owner.MaxHealth.Returns(maxHealth);
|
||||
owner.Types.Returns(new List<TypeIdentifier> { new(10, "fire") });
|
||||
return (new MagmaStormEffect(owner, 5, percentOfMaxHealth), owner);
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// Runs end-of-turn handling on the effect repeatedly and counts how many turns dealt damage to the target.
|
||||
/// </summary>
|
||||
private static int CountEndTurnDamageTicks(MagmaStormEffect 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 (Generations VI and VII): the damage taken by the bound target at the end of each turn is
|
||||
/// increased from 1/16 to 1/8 of its maximum HP.
|
||||
/// </summary>
|
||||
[Test, Arguments(96u, 12u), Arguments(100u, 12u), Arguments(120u, 15u), Arguments(8u, 1u)]
|
||||
public async Task OnEndTurn_TrappedPokemon_TakesOneEighthOfMaxHpAsDamage(uint maxHealth, uint expectedDamage)
|
||||
{
|
||||
// Arrange
|
||||
var (effect, owner) = CreateTrappedPokemon(maxHealth);
|
||||
|
||||
// Act
|
||||
effect.OnEndTurn(owner, Substitute.For<IBattle>());
|
||||
|
||||
// Assert
|
||||
await Assert.That(GetDamageAmount(owner)!.Value).IsEqualTo(expectedDamage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia (Generation IV): the bound target "takes damage ... at the end of each turn"; the damage is
|
||||
/// dealt once per turn, not more.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnEndTurn_TrappedPokemon_TakesDamageOncePerTurn()
|
||||
{
|
||||
// Arrange
|
||||
var (effect, owner) = CreateTrappedPokemon(96);
|
||||
|
||||
// Act
|
||||
effect.OnEndTurn(owner, Substitute.For<IBattle>());
|
||||
|
||||
// Assert
|
||||
var damageCalls = owner.ReceivedCalls().Count(c => c.GetMethodInfo().Name == "Damage");
|
||||
await Assert.That(damageCalls).IsEqualTo(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia (Generation IV): the bound target "takes damage ... at the end of each turn"; this is
|
||||
/// indirect damage, not move damage, so it uses <see cref="DamageSource.Misc"/>.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnEndTurn_TrappedPokemon_DamageIsIndirect()
|
||||
{
|
||||
// Arrange
|
||||
var (effect, owner) = CreateTrappedPokemon(100);
|
||||
|
||||
// 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 (Generation IV): "A bound target cannot switch out".
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task PreventSelfSwitch_TrappedPokemon_CannotSwitchOut()
|
||||
{
|
||||
// Arrange
|
||||
var (effect, owner) = CreateTrappedPokemon(100);
|
||||
var choice = Substitute.For<ISwitchChoice>();
|
||||
choice.User.Returns(owner);
|
||||
var prevent = false;
|
||||
|
||||
// Act
|
||||
effect.PreventSelfSwitch(choice, ref prevent);
|
||||
|
||||
// Assert
|
||||
await Assert.That(prevent).IsTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia (Generation IV): "A bound target cannot switch out or flee".
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task PreventSelfRunAway_TrappedPokemon_CannotFlee()
|
||||
{
|
||||
// Arrange
|
||||
var (effect, owner) = CreateTrappedPokemon(100);
|
||||
var choice = Substitute.For<IFleeChoice>();
|
||||
choice.User.Returns(owner);
|
||||
var prevent = false;
|
||||
|
||||
// Act
|
||||
effect.PreventSelfRunAway(choice, ref prevent);
|
||||
|
||||
// Assert
|
||||
await Assert.That(prevent).IsTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia (Generations VI and VII): "Ghost-type Pokémon cannot be trapped by Magma Storm." A trapped
|
||||
/// Ghost-type Pokémon must remain free to switch out.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task PreventSelfSwitch_GhostTypeOwner_CanStillSwitchOut()
|
||||
{
|
||||
// Arrange
|
||||
var (effect, owner) = CreateTrappedPokemon(100);
|
||||
owner.Types.Returns(new List<TypeIdentifier> { new(8, "ghost") });
|
||||
var choice = Substitute.For<ISwitchChoice>();
|
||||
choice.User.Returns(owner);
|
||||
var prevent = false;
|
||||
|
||||
// Act
|
||||
effect.PreventSelfSwitch(choice, ref prevent);
|
||||
|
||||
// Assert
|
||||
await Assert.That(prevent).IsFalse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia (Generations VI and VII): if the user is holding a Binding Band, the end-of-turn damage of
|
||||
/// Magma Storm is increased 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(120u, 20u), 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 = targetVolatile.Get<MagmaStormEffect>();
|
||||
await Assert.That(effect).IsNotNull();
|
||||
effect!.OnEndTurn(target, Substitute.For<IBattle>());
|
||||
|
||||
// Assert
|
||||
await Assert.That(GetDamageAmount(target)!.Value).IsEqualTo(expectedDamage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia (Generation V onwards): if the user is holding a Grip Claw, the trapping lasts seven 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 = targetVolatile.Get<MagmaStormEffect>();
|
||||
await Assert.That(effect).IsNotNull();
|
||||
var turnsWithDamage = CountEndTurnDamageTicks(effect!, target);
|
||||
|
||||
// Assert
|
||||
await Assert.That(turnsWithDamage).IsEqualTo(7);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulbapedia (Generation V onwards): the trapping lasts for four to five turns. After at most five
|
||||
/// end-of-turn ticks the trap must have removed itself from the target.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task OnEndTurn_AfterFiveEndOfTurns_TrapHasEnded()
|
||||
{
|
||||
// Arrange
|
||||
var (script, move, target, targetVolatile) = CreateTestSetup();
|
||||
target.MaxHealth.Returns(80u);
|
||||
script.OnSecondaryEffect(move, target, 0);
|
||||
var effect = targetVolatile.Get<MagmaStormEffect>()!;
|
||||
var battle = Substitute.For<IBattle>();
|
||||
|
||||
// Act - five end-of-turn ticks, the maximum duration of the trap
|
||||
for (var i = 0; i < 5; i++)
|
||||
effect.OnEndTurn(target, battle);
|
||||
|
||||
// Assert
|
||||
await Assert.That(targetVolatile.Contains("magma_storm")).IsFalse();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user