using PkmnLib.Dynamic.Models; using PkmnLib.Dynamic.Models.Choices; using PkmnLib.Dynamic.ScriptHandling; using PkmnLib.Plugin.Gen7.Common; using PkmnLib.Plugin.Gen7.Scripts.Moves; using PkmnLib.Plugin.Gen7.Scripts.Pokemon; using PkmnLib.Static.Moves; using PkmnLib.Static.Utils; namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// /// Tests for the script, and the it registers on its user. /// Behavior is verified against the Bulbapedia page for Follow Me (Generation VII). /// public class FollowMeTests { /// The side the center of attention and its ally are on. private const byte CenterSide = 0; /// The side the attacking opponents are on. private const byte OpposingSide = 1; /// /// A two-sided battle, each side carrying its own . /// private sealed class Arena { public IBattle Battle { get; } = Substitute.For(); public IBattleSide[] Sides { get; } = [CreateSide(), CreateSide()]; private static IBattleSide CreateSide() { var side = Substitute.For(); side.VolatileScripts.Returns(Substitute.For()); return side; } } /// /// Creates a mocked Pokémon standing on the given side of the given . /// private static IBattlePokemon CreatePokemon(Arena arena, byte sideIndex) { var pokemon = Substitute.For(); pokemon.Volatile.Returns(Substitute.For()); pokemon.SideIndex.Returns(sideIndex); pokemon.BattleSide.Returns(arena.Sides[sideIndex]); pokemon.Battle.Returns(arena.Battle); return pokemon; } /// /// Creates a mocked move choice made by the given Pokémon, using the named move. /// private static IMoveChoice CreateMoveChoice(IBattlePokemon user, string moveName = "tackle", bool noRedirection = false) { var moveData = Substitute.For(); moveData.Name.Returns(new StringKey(moveName)); moveData.HasFlag(MoveFlags.NoRedirection).Returns(noRedirection); var learnedMove = Substitute.For(); learnedMove.MoveData.Returns(moveData); var moveChoice = Substitute.For(); moveChoice.User.Returns(user); moveChoice.ChosenMove.Returns(learnedMove); return moveChoice; } /// /// Returns the script that was handed to or /// on the given set, or null if the set never received one. /// private static Script? RegisteredScript(IScriptSet set) { foreach (var call in set.ReceivedCalls()) { var arguments = call.GetArguments(); switch (call.GetMethodInfo().Name) { case "Add" when arguments[0] is Script script: return script; case "StackOrAdd" when arguments[1] is Func instantiation: return instantiation(); } } return null; } /// /// Executes for the given user and returns the center-of-attention effect it /// registered, regardless of which script set it chose to register it on. /// private static Script? MakeCenterOfAttention(IBattlePokemon user) { var followMe = new FollowMe(); var move = Substitute.For(); move.User.Returns(user); followMe.OnSecondaryEffect(move, user, 0); return RegisteredScript(user.Volatile) ?? RegisteredScript(user.BattleSide.VolatileScripts); } /// /// Drives the registered effect through whichever target-redirection hook it implements, mirroring the two /// hooks the engine runs while resolving a move's targets. /// private static void Redirect(Script effect, IMoveChoice moveChoice, ref IReadOnlyList targets) { switch (effect) { case IScriptChangeIncomingTargets incomingTargets: incomingTargets.ChangeIncomingTargets(moveChoice, ref targets); break; case IScriptChangeTargets changeTargets: changeTargets.ChangeTargets(moveChoice, ref targets); break; } } /// /// Bulbapedia: "Follow Me ... makes the user the center of attention ... for the rest of the turn." /// Opposing Pokémon act in their own, later move choices, so using Follow Me must leave a persistent effect /// behind rather than only altering the targets of the Follow Me choice itself. /// [Test] public async Task OnSecondaryEffect_Used_MakesUserTheCenterOfAttention() { // Arrange var arena = new Arena(); var user = CreatePokemon(arena, CenterSide); // Act var effect = MakeCenterOfAttention(user); // Assert await Assert.That(effect).IsNotNull(); } /// /// Bulbapedia: "forcing opposing Pokémon to use their moves on the user rather than the intended target". /// The engine resolves a move's targets by running over the attacker's /// own script scope, and over each intended target's scope — /// which covers that target's own scripts and its . A redirection /// effect stored on the center of attention's therefore sits in neither /// scope when an opponent attacks the center's ally. It must live on the center's side, the way /// does. /// [Test] public async Task OnSecondaryEffect_Used_RegistersEffectOnUsersSide() { // Arrange var arena = new Arena(); var user = CreatePokemon(arena, CenterSide); // Act MakeCenterOfAttention(user); // Assert await Assert.That(RegisteredScript(arena.Sides[CenterSide].VolatileScripts)).IsNotNull(); } /// /// Bulbapedia: "forcing opposing Pokémon to use their moves on the user rather than the intended target". /// The effect changes the targets of a move aimed at somebody else, so it has to hook /// , which the engine runs on each intended target. /// only ever runs within the attacker's own script scope, so a defending /// Pokémon's effect can never be reached through it. /// [Test] public async Task CenterOfAttentionEffect_ImplementsIncomingTargetsHook() { // Arrange var arena = new Arena(); var user = CreatePokemon(arena, CenterSide); // Act var effect = MakeCenterOfAttention(user); // Assert await Assert.That(effect).IsAssignableTo(); } /// /// Bulbapedia: "makes the user the center of attention, forcing opposing Pokémon to use their moves on the /// user rather than the intended target (even if it was a friendly target ...) for the rest of the turn." /// The core behavior: an opponent's single-target move aimed at the center's ally is drawn to the center. /// [Test] public async Task Redirect_OpposingMoveAimedAtAlly_RedirectsToCenterOfAttention() { // Arrange var arena = new Arena(); var center = CreatePokemon(arena, CenterSide); var ally = CreatePokemon(arena, CenterSide); var opponent = CreatePokemon(arena, OpposingSide); var effect = MakeCenterOfAttention(center)!; var moveChoice = CreateMoveChoice(opponent); IReadOnlyList targets = [ally]; // Act Redirect(effect, moveChoice, ref targets); // Assert await Assert.That(targets.Count).IsEqualTo(1); await Assert.That(targets[0]).IsEqualTo(center); } /// /// Bulbapedia: "Follow Me has no effect on moves which hit multiple Pokémon in a battle." /// [Test] public async Task Redirect_MultiTargetMove_TargetsUnchanged() { // Arrange var arena = new Arena(); var center = CreatePokemon(arena, CenterSide); var ally = CreatePokemon(arena, CenterSide); var opponent = CreatePokemon(arena, OpposingSide); var effect = MakeCenterOfAttention(center)!; var moveChoice = CreateMoveChoice(opponent); IReadOnlyList targets = [center, ally]; // Act Redirect(effect, moveChoice, ref targets); // Assert await Assert.That(targets.Count).IsEqualTo(2); await Assert.That(targets[0]).IsEqualTo(center); await Assert.That(targets[1]).IsEqualTo(ally); } /// /// Bulbapedia: "It does not affect allies." /// A move used by the center of attention's own ally keeps its intended target. /// [Test] public async Task Redirect_AllyMove_TargetsUnchanged() { // Arrange var arena = new Arena(); var center = CreatePokemon(arena, CenterSide); var attackingAlly = CreatePokemon(arena, CenterSide); var targetedAlly = CreatePokemon(arena, CenterSide); var effect = MakeCenterOfAttention(center)!; var moveChoice = CreateMoveChoice(attackingAlly); IReadOnlyList targets = [targetedAlly]; // Act Redirect(effect, moveChoice, ref targets); // Assert await Assert.That(targets.Count).IsEqualTo(1); await Assert.That(targets[0]).IsEqualTo(targetedAlly); } /// /// Bulbapedia: "forcing opposing Pokémon to use their moves on the user rather than the intended target." /// A move already aimed at the center of attention simply stays there. /// [Test] public async Task Redirect_MoveAimedAtCenterOfAttention_TargetsUnchanged() { // Arrange var arena = new Arena(); var center = CreatePokemon(arena, CenterSide); var opponent = CreatePokemon(arena, OpposingSide); var effect = MakeCenterOfAttention(center)!; var moveChoice = CreateMoveChoice(opponent); IReadOnlyList targets = [center]; // Act Redirect(effect, moveChoice, ref targets); // Assert await Assert.That(targets.Count).IsEqualTo(1); await Assert.That(targets[0]).IsEqualTo(center); } /// /// Creates a real hosting the given effect, so that /// calls can be observed through . /// private static IScriptSet CreateHostedSet(Script effect) { var owner = Substitute.For(); owner.GetScripts().Returns(_ => new ScriptIterator(Array.Empty>())); IScriptSet set = new ScriptSet(owner); set.Add(effect); return set; } /// /// Bulbapedia: "If the center of attention faints or switches out, it no longer draws moves." /// The effect removes itself from its host script set when the center of attention faints. /// [Test] public async Task OnFaint_CenterOfAttentionFaints_EffectRemovesItself() { // Arrange var arena = new Arena(); var center = CreatePokemon(arena, CenterSide); var effect = MakeCenterOfAttention(center)!; var set = CreateHostedSet(effect); // Act ((IScriptOnFaint)effect).OnFaint(center, DamageSource.MoveDamage); // Assert await Assert.That(set.Contains(effect.Name)).IsFalse(); } /// /// Bulbapedia: "If the center of attention faints or switches out, it no longer draws moves." /// The effect removes itself from its host script set when the center of attention switches out. /// [Test] public async Task OnSwitchOut_CenterOfAttentionSwitchesOut_EffectRemovesItself() { // Arrange var arena = new Arena(); var center = CreatePokemon(arena, CenterSide); var effect = MakeCenterOfAttention(center)!; var set = CreateHostedSet(effect); // Act ((IScriptOnSwitchOut)effect).OnSwitchOut(center, 0); // Assert await Assert.That(set.Contains(effect.Name)).IsFalse(); } /// /// The effect only concerns its own user: another Pokémon fainting leaves the center of attention in place. /// [Test] public async Task OnFaint_OtherPokemonFaints_EffectStays() { // Arrange var arena = new Arena(); var center = CreatePokemon(arena, CenterSide); var ally = CreatePokemon(arena, CenterSide); var effect = MakeCenterOfAttention(center)!; var set = CreateHostedSet(effect); // Act ((IScriptOnFaint)effect).OnFaint(ally, DamageSource.MoveDamage); // Assert await Assert.That(set.Contains(effect.Name)).IsTrue(); } /// /// Bulbapedia, Generation VI onwards: "Follow Me does not redirect Future Sight or Doom Desire." /// Both moves carry the flag in the move data, which the effect /// checks to leave them alone. /// [Test, Arguments("future_sight"), Arguments("doom_desire")] public async Task Redirect_FutureSightOrDoomDesire_TargetsUnchanged(string moveName) { // Arrange var arena = new Arena(); var center = CreatePokemon(arena, CenterSide); var ally = CreatePokemon(arena, CenterSide); var opponent = CreatePokemon(arena, OpposingSide); var effect = MakeCenterOfAttention(center)!; var moveChoice = CreateMoveChoice(opponent, moveName, true); IReadOnlyList targets = [ally]; // Act Redirect(effect, moveChoice, ref targets); // Assert await Assert.That(targets.Count).IsEqualTo(1); await Assert.That(targets[0]).IsEqualTo(ally); } /// /// Technical test: an empty target list is left unchanged instead of throwing. /// [Test] public async Task Redirect_EmptyTargetList_TargetsUnchanged() { // Arrange var arena = new Arena(); var center = CreatePokemon(arena, CenterSide); var opponent = CreatePokemon(arena, OpposingSide); var effect = MakeCenterOfAttention(center)!; var moveChoice = CreateMoveChoice(opponent); IReadOnlyList targets = []; // Act Redirect(effect, moveChoice, ref targets); // Assert await Assert.That(targets.Count).IsEqualTo(0); } /// /// Bulbapedia: "makes the user the center of attention ... for the rest of the turn." /// The effect has to clear itself once the turn ends, the way does /// through ; otherwise the user stays the center of attention forever. /// [Test] public async Task CenterOfAttentionEffect_WearsOffAtEndOfTurn() { // Arrange var arena = new Arena(); var user = CreatePokemon(arena, CenterSide); // Act var effect = MakeCenterOfAttention(user); // Assert await Assert.That(effect).IsAssignableTo(); } }