diff --git a/Mage.Sets/src/mage/cards/f/FlockchaserPhantom.java b/Mage.Sets/src/mage/cards/f/FlockchaserPhantom.java index 57424efeb17..1ab23a3a5de 100644 --- a/Mage.Sets/src/mage/cards/f/FlockchaserPhantom.java +++ b/Mage.Sets/src/mage/cards/f/FlockchaserPhantom.java @@ -1,20 +1,15 @@ package mage.cards.f; import mage.MageInt; -import mage.abilities.Ability; import mage.abilities.common.AttacksTriggeredAbility; -import mage.abilities.effects.ContinuousEffectImpl; +import mage.abilities.effects.common.continuous.NextSpellCastHasAbilityEffect; import mage.abilities.keyword.ConvokeAbility; import mage.abilities.keyword.FlyingAbility; import mage.abilities.keyword.VigilanceAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.*; -import mage.game.Game; -import mage.game.stack.Spell; -import mage.game.stack.StackObject; -import mage.players.Player; -import mage.watchers.common.SpellsCastWatcher; +import mage.constants.CardType; +import mage.constants.SubType; import java.util.UUID; @@ -39,7 +34,7 @@ public final class FlockchaserPhantom extends CardImpl { this.addAbility(VigilanceAbility.getInstance()); // Whenever Flockchaser Phantom attacks, the next spell you cast this turn has convoke. - this.addAbility(new AttacksTriggeredAbility(new FlockchaserPhantomEffect())); + this.addAbility(new AttacksTriggeredAbility(new NextSpellCastHasAbilityEffect(new ConvokeAbility()))); } private FlockchaserPhantom(final FlockchaserPhantom card) { @@ -51,60 +46,3 @@ public final class FlockchaserPhantom extends CardImpl { return new FlockchaserPhantom(this); } } - -class FlockchaserPhantomEffect extends ContinuousEffectImpl { - - private int spellsCast; - - FlockchaserPhantomEffect() { - super(Duration.EndOfTurn, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility); - staticText = "the next spell you cast this turn has convoke"; - } - - private FlockchaserPhantomEffect(final FlockchaserPhantomEffect effect) { - super(effect); - this.spellsCast = effect.spellsCast; - } - - @Override - public void init(Ability source, Game game) { - super.init(source, game); - SpellsCastWatcher watcher = game.getState().getWatcher(SpellsCastWatcher.class); - if (watcher != null) { - spellsCast = watcher.getCount(source.getControllerId()); - } - } - - @Override - public boolean apply(Game game, Ability source) { - SpellsCastWatcher watcher = game.getState().getWatcher(SpellsCastWatcher.class); - if (watcher == null) { - return false; - } - - //check if a spell was cast before - if (watcher.getCount(source.getControllerId()) > spellsCast) { - discard(); // only one use - return false; - } - - //check cast for spell and add convoke - Player controller = game.getPlayer(source.getControllerId()); - if (controller != null) { - for (StackObject stackObject : game.getStack()) { - if ((stackObject instanceof Spell) && !stackObject.isCopy() && stackObject.isControlledBy(source.getControllerId())) { - Spell spell = (Spell) stackObject; - game.getState().addOtherAbility(spell.getCard(), new ConvokeAbility()); - return true; - } - } - } - - return false; - } - - @Override - public FlockchaserPhantomEffect copy() { - return new FlockchaserPhantomEffect(this); - } -} diff --git a/Mage.Sets/src/mage/cards/w/WandOfTheWorldsoul.java b/Mage.Sets/src/mage/cards/w/WandOfTheWorldsoul.java index d74c9b75914..eb547b5a742 100644 --- a/Mage.Sets/src/mage/cards/w/WandOfTheWorldsoul.java +++ b/Mage.Sets/src/mage/cards/w/WandOfTheWorldsoul.java @@ -1,20 +1,15 @@ package mage.cards.w; -import mage.abilities.Ability; import mage.abilities.common.EntersBattlefieldTappedAbility; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.costs.common.TapSourceCost; -import mage.abilities.effects.ContinuousEffectImpl; +import mage.abilities.effects.common.continuous.NextSpellCastHasAbilityEffect; import mage.abilities.keyword.ConvokeAbility; import mage.abilities.mana.WhiteManaAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.*; -import mage.game.Game; -import mage.game.stack.Spell; -import mage.game.stack.StackObject; -import mage.players.Player; -import mage.watchers.common.SpellsCastWatcher; +import mage.constants.CardType; +import mage.constants.Zone; import java.util.UUID; @@ -33,7 +28,9 @@ public final class WandOfTheWorldsoul extends CardImpl { this.addAbility(new WhiteManaAbility()); // {T}: The next spell you cast this turn has convoke. - this.addAbility(new SimpleActivatedAbility(Zone.BATTLEFIELD, new WandOfTheWorldsoulEffect(), new TapSourceCost())); + this.addAbility(new SimpleActivatedAbility(Zone.BATTLEFIELD, + new NextSpellCastHasAbilityEffect(new ConvokeAbility()), + new TapSourceCost())); } private WandOfTheWorldsoul(final WandOfTheWorldsoul card) { @@ -45,59 +42,3 @@ public final class WandOfTheWorldsoul extends CardImpl { return new WandOfTheWorldsoul(this); } } -class WandOfTheWorldsoulEffect extends ContinuousEffectImpl { - - private int spellsCast; - - WandOfTheWorldsoulEffect() { - super(Duration.EndOfTurn, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility); - staticText = "the next spell you cast this turn has convoke"; - } - - private WandOfTheWorldsoulEffect(final WandOfTheWorldsoulEffect effect) { - super(effect); - this.spellsCast = effect.spellsCast; - } - - @Override - public void init(Ability source, Game game) { - super.init(source, game); - SpellsCastWatcher watcher = game.getState().getWatcher(SpellsCastWatcher.class); - if (watcher != null) { - spellsCast = watcher.getCount(source.getControllerId()); - } - } - - @Override - public boolean apply(Game game, Ability source) { - SpellsCastWatcher watcher = game.getState().getWatcher(SpellsCastWatcher.class); - if (watcher == null) { - return false; - } - - //check if a spell was cast before - if (watcher.getCount(source.getControllerId()) > spellsCast) { - discard(); // only one use - return false; - } - - //check cast for spell and add convoke - Player controller = game.getPlayer(source.getControllerId()); - if (controller != null) { - for (StackObject stackObject : game.getStack()) { - if ((stackObject instanceof Spell) && !stackObject.isCopy() && stackObject.isControlledBy(source.getControllerId())) { - Spell spell = (Spell) stackObject; - game.getState().addOtherAbility(spell.getCard(), new ConvokeAbility()); - return true; - } - } - } - - return false; - } - - @Override - public WandOfTheWorldsoulEffect copy() { - return new WandOfTheWorldsoulEffect(this); - } -} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/other/GainAbilitiesTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/other/GainAbilitiesTest.java index 89f063efd60..87de4b19a73 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/other/GainAbilitiesTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/other/GainAbilitiesTest.java @@ -132,6 +132,52 @@ public class GainAbilitiesTest extends CardTestPlayerBase { } + @Test + public void nextSpellCastHasConvoke() { + + addCard(Zone.BATTLEFIELD, playerA, "Wand of the Worldsoul"); // to give next spell convoke + addCard(Zone.BATTLEFIELD, playerA, "Runeclaw Bear"); // for convoke + addCard(Zone.HAND, playerA, "Elvish Mystic"); // to cast with convoke + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: The next spell you cast this turn"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Elvish Mystic"); + addTarget(playerA, "Runeclaw Bear"); // tap for G + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertTapped("Runeclaw Bear", true); + assertPermanentCount(playerA, "Elvish Mystic", 1); + + } + + @Test + public void onlyNextSpellCastHasConvoke() { + + addCard(Zone.BATTLEFIELD, playerA, "Wand of the Worldsoul", 2); // to give next spell convoke + addCard(Zone.BATTLEFIELD, playerA, "Goblin Piker"); // for convoke + addCard(Zone.HAND, playerA, "Shock"); // cast first + addCard(Zone.BATTLEFIELD, playerA, "Runeclaw Bear"); // for convoke + addCard(Zone.HAND, playerA, "Elvish Mystic"); // to cast with convoke + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: The next spell you cast this turn"); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: The next spell you cast this turn"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Shock", playerB); + addTarget(playerA, "Goblin Piker"); // tap for R + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkPlayableAbility("no convoke", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Elvish Mystic", false); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertLife(playerB, 20 - 2); + assertTapped("Goblin Piker", true); + } + /** * Reported bug: https://github.com/magefree/mage/issues/9565 * 1. Cast all three of Frondland Felidar, Jubilant Skybonder, and Proud Wildbonder. diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityAttachedEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityAttachedEffect.java index 1c5c9db973e..d07c2b053d1 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityAttachedEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityAttachedEffect.java @@ -12,6 +12,7 @@ import mage.constants.*; import mage.game.Game; import mage.game.permanent.Permanent; import mage.target.targetpointer.FixedTarget; +import mage.util.CardUtil; /** * @author BetaSteward_at_googlemail.com @@ -150,12 +151,7 @@ public class GainAbilityAttachedEffect extends ContinuousEffectImpl { if (quotes) { sb.append('"'); } - String abilityRuleText = ability.getRule("This " + targetObjectName); - if (abilityRuleText.endsWith(")")) { - // remove reminder text for this rule generation - abilityRuleText = abilityRuleText.substring(0, abilityRuleText.indexOf(" (")); - } - sb.append(abilityRuleText); + sb.append(CardUtil.stripReminderText(ability.getRule("This " + targetObjectName))); if (quotes) { sb.append('"'); } diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityControlledSpellsEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityControlledSpellsEffect.java index 26918daff86..e112116e758 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityControlledSpellsEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityControlledSpellsEffect.java @@ -10,6 +10,7 @@ import mage.game.permanent.Permanent; import mage.game.stack.Spell; import mage.game.stack.StackObject; import mage.players.Player; +import mage.util.CardUtil; /** * @author Styxo @@ -23,7 +24,7 @@ public class GainAbilityControlledSpellsEffect extends ContinuousEffectImpl { super(Duration.WhileOnBattlefield, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility); this.ability = ability; this.filter = filter; - staticText = filter.getMessage() + " have " + ability.getRule(); + staticText = filter.getMessage() + " have " + CardUtil.getTextWithFirstCharLowerCase(CardUtil.stripReminderText(ability.getRule())); } private GainAbilityControlledSpellsEffect(final GainAbilityControlledSpellsEffect effect) { @@ -78,10 +79,9 @@ public class GainAbilityControlledSpellsEffect extends ContinuousEffectImpl { } // TODO: Distinguish "you cast" to exclude copies Card card = game.getCard(stackObject.getSourceId()); - if (card == null || !filter.match((Spell) stackObject, game)) { - continue; + if (card != null && filter.match((Spell) stackObject, game)) { + game.getState().addOtherAbility(card, ability); } - game.getState().addOtherAbility(card, ability); } return true; } diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/NextSpellCastHasAbilityEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/NextSpellCastHasAbilityEffect.java new file mode 100644 index 00000000000..e23286715d2 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/NextSpellCastHasAbilityEffect.java @@ -0,0 +1,107 @@ +package mage.abilities.effects.common.continuous; + +import mage.abilities.Ability; +import mage.abilities.effects.ContinuousEffectImpl; +import mage.cards.Card; +import mage.constants.*; +import mage.filter.FilterCard; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.game.stack.Spell; +import mage.game.stack.StackObject; +import mage.players.Player; +import mage.util.CardUtil; +import mage.watchers.common.SpellsCastWatcher; + +/** + * @author xenohedron + */ +public class NextSpellCastHasAbilityEffect extends ContinuousEffectImpl { + + private int spellsCast; + private final Ability ability; + private final FilterCard filter; + + public NextSpellCastHasAbilityEffect(Ability ability) { + this(ability, StaticFilters.FILTER_CARD); + } + + public NextSpellCastHasAbilityEffect(Ability ability, FilterCard filter) { + super(Duration.EndOfTurn, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility); + this.ability = ability; + this.filter = filter; + staticText = "the next spell you cast this turn has " + CardUtil.getTextWithFirstCharLowerCase(CardUtil.stripReminderText(ability.getRule())); + } + + private NextSpellCastHasAbilityEffect(final NextSpellCastHasAbilityEffect effect) { + super(effect); + this.spellsCast = effect.spellsCast; + this.ability = effect.ability; + this.filter = effect.filter; + } + + @Override + public NextSpellCastHasAbilityEffect copy() { + return new NextSpellCastHasAbilityEffect(this); + } + + @Override + public void init(Ability source, Game game) { + super.init(source, game); + SpellsCastWatcher watcher = game.getState().getWatcher(SpellsCastWatcher.class); + if (watcher != null) { + spellsCast = watcher.getCount(source.getControllerId()); + } + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + SpellsCastWatcher watcher = game.getState().getWatcher(SpellsCastWatcher.class); + if (player == null || watcher == null) { + return false; + } + //check if a spell was cast before + if (watcher.getCount(source.getControllerId()) > spellsCast) { + discard(); // only one use + return false; + } + for (Card card : game.getExile().getAllCardsByRange(game, source.getControllerId())) { + if (filter.match(card, game)) { + game.getState().addOtherAbility(card, ability); + } + } + for (Card card : player.getLibrary().getCards(game)) { + if (filter.match(card, game)) { + game.getState().addOtherAbility(card, ability); + } + } + for (Card card : player.getHand().getCards(game)) { + if (filter.match(card, game)) { + game.getState().addOtherAbility(card, ability); + } + } + for (Card card : player.getGraveyard().getCards(game)) { + if (filter.match(card, game)) { + game.getState().addOtherAbility(card, ability); + } + } + // workaround to gain cost reduction abilities to commanders before cast (make it playable) + game.getCommanderCardsFromCommandZone(player, CommanderCardType.ANY) + .stream() + .filter(card -> filter.match(card, game)) + .forEach(card -> game.getState().addOtherAbility(card, ability)); + + for (StackObject stackObject : game.getStack()) { + if (!(stackObject instanceof Spell) || !stackObject.isControlledBy(source.getControllerId())) { + continue; + } + // TODO: Distinguish "you cast" to exclude copies + Card card = game.getCard(stackObject.getSourceId()); + if (card != null && filter.match((Spell) stackObject, game)) { + game.getState().addOtherAbility(card, ability); + } + } + return true; + } +} diff --git a/Mage/src/main/java/mage/util/CardUtil.java b/Mage/src/main/java/mage/util/CardUtil.java index 0652a9baab4..a2f58887cc1 100644 --- a/Mage/src/main/java/mage/util/CardUtil.java +++ b/Mage/src/main/java/mage/util/CardUtil.java @@ -983,6 +983,10 @@ public final class CardUtil { return "" + text + " — "; } + public static String stripReminderText(String text) { + return text.endsWith(")") ? text.substring(0, text.indexOf(" (")) : text; + } + public static Set getAllSelectedTargets(Ability ability, Game game) { return ability.getModes().getSelectedModes() .stream()