From 8c1a4d1fa6fdcd6cf8023de51d894528a2110c98 Mon Sep 17 00:00:00 2001 From: Vivian Greenslade Date: Sat, 9 Sep 2023 17:02:04 -0230 Subject: [PATCH] [WOC] Implement Misleading Signpost (#11084) * [WOC] Implement Misleading Signpost * moved effect to common file * added unit test * added battle to test * updated effect name, fixed list of defenders * fix test text * added text for effect * fixed target * made changes as per PR comments * added unit test for 508.7c --- .../src/mage/cards/m/MisleadingSignpost.java | 53 ++++++++ .../mage/sets/WildsOfEldraineCommander.java | 1 + ...ectDefenderAttackedByTargetEffectTest.java | 105 ++++++++++++++++ ...eselectDefenderAttackedByTargetEffect.java | 117 ++++++++++++++++++ 4 files changed, 276 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/m/MisleadingSignpost.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/targets/attacking/ReselectDefenderAttackedByTargetEffectTest.java create mode 100644 Mage/src/main/java/mage/abilities/effects/common/combat/ReselectDefenderAttackedByTargetEffect.java diff --git a/Mage.Sets/src/mage/cards/m/MisleadingSignpost.java b/Mage.Sets/src/mage/cards/m/MisleadingSignpost.java new file mode 100644 index 00000000000..a50a8ba568b --- /dev/null +++ b/Mage.Sets/src/mage/cards/m/MisleadingSignpost.java @@ -0,0 +1,53 @@ +package mage.cards.m; + +import java.util.UUID; + +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.condition.common.IsStepCondition; +import mage.abilities.decorator.ConditionalTriggeredAbility; +import mage.abilities.effects.common.combat.ReselectDefenderAttackedByTargetEffect; +import mage.abilities.keyword.FlashAbility; +import mage.abilities.mana.BlueManaAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.PhaseStep; +import mage.target.common.TargetAttackingCreature; + + +/** + * + * @author Xanderhall + */ +public final class MisleadingSignpost extends CardImpl { + + public MisleadingSignpost(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{2}{U}"); + + + // Flash + this.addAbility(FlashAbility.getInstance()); + + // When Misleading Signpost enters the battlefield during the declare attackers step, you may reselect which player or permanent target attacking creature is attacking. + Ability ability = new ConditionalTriggeredAbility( + new EntersBattlefieldTriggeredAbility(new ReselectDefenderAttackedByTargetEffect(true), true), + new IsStepCondition(PhaseStep.DECLARE_ATTACKERS, false), + "When {this} enters the battlefield during the declare attackers step, you may reselect which player or permanent target attacking creature is attacking. " + + "(It can't attack its controller or their permanents)"); + ability.addTarget(new TargetAttackingCreature()); + this.addAbility(ability); + + // {T}: Add {U}. + this.addAbility(new BlueManaAbility()); + } + + private MisleadingSignpost(final MisleadingSignpost card) { + super(card); + } + + @Override + public MisleadingSignpost copy() { + return new MisleadingSignpost(this); + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/sets/WildsOfEldraineCommander.java b/Mage.Sets/src/mage/sets/WildsOfEldraineCommander.java index ad64e44c832..54b3f21937a 100644 --- a/Mage.Sets/src/mage/sets/WildsOfEldraineCommander.java +++ b/Mage.Sets/src/mage/sets/WildsOfEldraineCommander.java @@ -91,6 +91,7 @@ public final class WildsOfEldraineCommander extends ExpansionSet { cards.add(new SetCardInfo("Mantle of the Ancients", 70, Rarity.RARE, mage.cards.m.MantleOfTheAncients.class)); cards.add(new SetCardInfo("Midnight Clock", 99, Rarity.RARE, mage.cards.m.MidnightClock.class)); cards.add(new SetCardInfo("Mind Stone", 148, Rarity.COMMON, mage.cards.m.MindStone.class)); + cards.add(new SetCardInfo("Misleading Signpost", 11, Rarity.RARE, mage.cards.m.MisleadingSignpost.class)); cards.add(new SetCardInfo("Myriad Landscape", 164, Rarity.UNCOMMON, mage.cards.m.MyriadLandscape.class)); cards.add(new SetCardInfo("Nettling Nuisance", 15, Rarity.RARE, mage.cards.n.NettlingNuisance.class)); cards.add(new SetCardInfo("Nightmare Unmaking", 114, Rarity.RARE, mage.cards.n.NightmareUnmaking.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/targets/attacking/ReselectDefenderAttackedByTargetEffectTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/targets/attacking/ReselectDefenderAttackedByTargetEffectTest.java new file mode 100644 index 00000000000..e4ac9f83233 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/targets/attacking/ReselectDefenderAttackedByTargetEffectTest.java @@ -0,0 +1,105 @@ +package org.mage.test.cards.targets.attacking; + +import org.junit.Test; +import org.mage.test.serverside.base.CardTestCommander4Players; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; + +public class ReselectDefenderAttackedByTargetEffectTest extends CardTestCommander4Players { + + private static final String SIGNPOST = "Misleading Signpost"; + private static final String LION = "Silvercoat Lion"; + private static final String PLANESWALKER = "Teferi, Master of Time"; + private static final String BATTLE = "Invasion of Segovia"; + private static final String ARCHON = "Blazing Archon"; + + /** + * When Misleading Signpost enters the battlefield during the declare attackers step, you may reselect which player or permanent target attacking creature is attacking. + */ + @Test + public void testSingleTarget() { + addCard(Zone.BATTLEFIELD, playerA, LION); + + addCard(Zone.BATTLEFIELD, playerB, "Island", 3); + addCard(Zone.HAND, playerB, SIGNPOST); + + addCard(Zone.BATTLEFIELD, playerC, PLANESWALKER); + + // Player A declares an attack against Player B + attack(1, playerA, LION, playerB); + + // Player B responds by casting Signpost, redirecting the attack to Player C's planeswalker + castSpell(1, PhaseStep.DECLARE_ATTACKERS, playerB, SIGNPOST); + setChoice(playerB, true); + addTarget(playerB, LION); + addTarget(playerB, PLANESWALKER); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertCounterCount(PLANESWALKER, CounterType.LOYALTY, 1); + assertLife(playerB, 20); + } + + @Test + public void testBattle() { + addCard(Zone.BATTLEFIELD, playerA, LION); + addCard(Zone.BATTLEFIELD, playerA, "Island", 3); + addCard(Zone.HAND, playerA, BATTLE); + + addCard(Zone.BATTLEFIELD, playerB, "Island", 3); + addCard(Zone.HAND, playerB, SIGNPOST); + addCard(Zone.BATTLEFIELD, playerB, BATTLE); + setChoice(playerB, "PlayerC"); + + // Player A declares an attack against Player B + attack(1, playerA, LION, playerB); + + // Player B responds by casting Signpost, redirecting the attack to the battle Player C is protecting + castSpell(1, PhaseStep.DECLARE_ATTACKERS, playerB, SIGNPOST); + setChoice(playerB, true); + addTarget(playerB, LION); + addTarget(playerB, BATTLE); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertCounterCount(BATTLE, CounterType.DEFENSE, 2); + assertLife(playerB, 20); + } + + /** + * Test of 508.7b + */ + @Test + public void testAvoidRestrictions() { + addCard(Zone.BATTLEFIELD, playerA, LION); + + addCard(Zone.BATTLEFIELD, playerB, "Island", 3); + addCard(Zone.HAND, playerB, SIGNPOST); + + addCard(Zone.BATTLEFIELD, playerC, PLANESWALKER); + addCard(Zone.BATTLEFIELD, playerC, ARCHON); + + // Player A attacks Player C's planeswalker, can't attack Player C because of Archon + attack(1, playerA, LION, PLANESWALKER); + + // Player B should be able to redirect to Player C, ignoring Archon + castSpell(1, PhaseStep.DECLARE_ATTACKERS, playerB, SIGNPOST); + setChoice(playerB, true); + addTarget(playerB, LION); + addTarget(playerB, playerC); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + // Player C should be damaged, not planeswalker + assertLife(playerC, 18); + assertCounterCount(PLANESWALKER, CounterType.LOYALTY, 3); + } +} diff --git a/Mage/src/main/java/mage/abilities/effects/common/combat/ReselectDefenderAttackedByTargetEffect.java b/Mage/src/main/java/mage/abilities/effects/common/combat/ReselectDefenderAttackedByTargetEffect.java new file mode 100644 index 00000000000..794f85a7e75 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/effects/common/combat/ReselectDefenderAttackedByTargetEffect.java @@ -0,0 +1,117 @@ +package mage.abilities.effects.common.combat; + +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +import mage.abilities.Ability; +import mage.abilities.Mode; +import mage.abilities.effects.OneShotEffect; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.filter.FilterPermanent; +import mage.filter.predicate.Predicates; +import mage.game.Game; +import mage.game.combat.CombatGroup; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.common.TargetDefender; + +/** + * See 508.7 in the CR for ruling details + * + * @author Xanderhall + */ +public class ReselectDefenderAttackedByTargetEffect extends OneShotEffect { + + private final boolean includePermanents; + private static final FilterPermanent filter = new FilterPermanent("permanent"); + + static { + filter.add(Predicates.or( + CardType.PLANESWALKER.getPredicate(), + CardType.BATTLE.getPredicate() + )); + } + + public ReselectDefenderAttackedByTargetEffect(boolean includePermanents) { + super(Outcome.Benefit); + this.includePermanents = includePermanents; + } + + protected ReselectDefenderAttackedByTargetEffect(final ReselectDefenderAttackedByTargetEffect effect) { + super(effect); + this.includePermanents = effect.includePermanents; + } + + @Override + public ReselectDefenderAttackedByTargetEffect copy() { + return new ReselectDefenderAttackedByTargetEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller == null) { + return false; + } + + for (UUID id : getTargetPointer().getTargets(game, source)) { + + Permanent attackingCreature = game.getPermanent(id); + if (attackingCreature == null) { + continue; + } + + CombatGroup combatGroupTarget = game.getCombat().findGroup(attackingCreature.getId()); + if (combatGroupTarget == null) { + continue; + } + + // 508.7b: While reselecting which player, planeswalker, or battle a creature is attacking, + // that creature isn't affected by requirements or restrictions that apply to the declaration of attackers. + + // 508.7c. The reselected player, planeswalker, or battle must be an opponent of the attacking creature's controller, + // a planeswalker controlled by an opponent of the attacking creature's controller, + // or a battle protected by an opponent of the attacking creature's controller. + + Set defenders = includePermanents ? + game.getCombat().getDefenders() : + game.getCombat().getAttackablePlayers(game).stream().collect(Collectors.toSet()); + + // Select the new defender + TargetDefender defender = new TargetDefender(defenders); + if (controller.chooseTarget(Outcome.Damage, defender, source, game)) { + UUID firstTarget = defender.getFirstTarget(); + if (combatGroupTarget.getDefenderId() != null && !combatGroupTarget.getDefenderId().equals(firstTarget)) { + if (combatGroupTarget.changeDefenderPostDeclaration(firstTarget, game)) { + String attacked = ""; + Player player = game.getPlayer(firstTarget); + if (player != null) { + attacked = player.getLogName(); + } else { + Permanent permanent = game.getPermanent(firstTarget); + if (permanent != null) { + attacked = permanent.getLogName(); + } + } + game.informPlayers(attackingCreature.getLogName() + " now attacks " + attacked); + } + } + } + } + + return true; + } + + @Override + public String getText(Mode mode) { + if (staticText != null && !staticText.isEmpty()) { + return staticText; + } + return "reselect which " + (includePermanents ? "player or permanent " : "player ") + + getTargetPointer().describeTargets(mode.getTargets(), "that creature") + + " is attacking"; + } + +}