diff --git a/Mage.Sets/src/mage/cards/b/BohnBeguilingBalladeer.java b/Mage.Sets/src/mage/cards/b/BohnBeguilingBalladeer.java index 2bae253674d..825caef29d8 100644 --- a/Mage.Sets/src/mage/cards/b/BohnBeguilingBalladeer.java +++ b/Mage.Sets/src/mage/cards/b/BohnBeguilingBalladeer.java @@ -4,18 +4,14 @@ import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.CastSecondSpellTriggeredAbility; import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.effects.ContinuousEffectImpl; import mage.abilities.effects.common.combat.GoadTargetEffect; import mage.abilities.keyword.ForetellAbility; -import mage.cards.*; -import mage.constants.*; -import mage.filter.common.FilterNonlandCard; -import mage.filter.predicate.Predicates; -import mage.filter.predicate.mageobject.AbilityPredicate; -import mage.game.Game; -import mage.players.Player; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.SuperType; import mage.target.common.TargetOpponentsCreaturePermanent; -import mage.util.CardUtil; import java.util.UUID; @@ -34,7 +30,7 @@ public final class BohnBeguilingBalladeer extends CardImpl { this.toughness = new MageInt(3); // Each nonland card in your hand without foretell has foretell. Its foretell cost is equal to its mana cost reduced by {2}. - this.addAbility(new SimpleStaticAbility(new EdginLarcenousLutenistEffect())); + this.addAbility(new SimpleStaticAbility(ForetellAbility.makeAddForetellEffect())); // Whenever you cast your second spell each turn, goad target creature an opponent controls. Ability ability = new CastSecondSpellTriggeredAbility(new GoadTargetEffect()); @@ -51,69 +47,3 @@ public final class BohnBeguilingBalladeer extends CardImpl { return new BohnBeguilingBalladeer(this); } } - -class EdginLarcenousLutenistEffect extends ContinuousEffectImpl { - - private static final FilterNonlandCard filter = new FilterNonlandCard(); - - static { - filter.add(Predicates.not(new AbilityPredicate(ForetellAbility.class))); - } - - EdginLarcenousLutenistEffect() { - super(Duration.WhileOnBattlefield, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility); - this.staticText = "Each nonland card in your hand without foretell has foretell. Its foretell cost is equal to its mana cost reduced by {2}"; - } - - private EdginLarcenousLutenistEffect(final EdginLarcenousLutenistEffect effect) { - super(effect); - } - - @Override - public EdginLarcenousLutenistEffect copy() { - return new EdginLarcenousLutenistEffect(this); - } - - @Override - public boolean apply(Game game, Ability source) { - Player controller = game.getPlayer(source.getControllerId()); - if (controller == null) { - return false; - } - for (Card card : controller.getHand().getCards(filter, game)) { - ForetellAbility foretellAbility = null; - if (card instanceof SplitCard) { - String leftHalfCost = CardUtil.reduceCost(((SplitCard) card).getLeftHalfCard().getManaCost(), 2).getText(); - String rightHalfCost = CardUtil.reduceCost(((SplitCard) card).getRightHalfCard().getManaCost(), 2).getText(); - foretellAbility = new ForetellAbility(card, leftHalfCost, rightHalfCost); - } else if (card instanceof ModalDoubleFacedCard) { - ModalDoubleFacedCardHalf leftHalfCard = ((ModalDoubleFacedCard) card).getLeftHalfCard(); - // If front side of MDFC is land, do nothing as Dream Devourer does not apply to lands - // MDFC cards in hand are considered lands if front side is land - if (!leftHalfCard.isLand(game)) { - String leftHalfCost = CardUtil.reduceCost(leftHalfCard.getManaCost(), 2).getText(); - ModalDoubleFacedCardHalf rightHalfCard = ((ModalDoubleFacedCard) card).getRightHalfCard(); - if (rightHalfCard.isLand(game)) { - foretellAbility = new ForetellAbility(card, leftHalfCost); - } else { - String rightHalfCost = CardUtil.reduceCost(rightHalfCard.getManaCost(), 2).getText(); - foretellAbility = new ForetellAbility(card, leftHalfCost, rightHalfCost); - } - } - } else if (card instanceof CardWithSpellOption) { - String creatureCost = CardUtil.reduceCost(card.getMainCard().getManaCost(), 2).getText(); - String spellCost = CardUtil.reduceCost(((CardWithSpellOption) card).getSpellCard().getManaCost(), 2).getText(); - foretellAbility = new ForetellAbility(card, creatureCost, spellCost); - } else { - String costText = CardUtil.reduceCost(card.getManaCost(), 2).getText(); - foretellAbility = new ForetellAbility(card, costText); - } - if (foretellAbility != null) { - foretellAbility.setSourceId(card.getId()); - foretellAbility.setControllerId(card.getOwnerId()); - game.getState().addOtherAbility(card, foretellAbility); - } - } - return true; - } -} diff --git a/Mage.Sets/src/mage/cards/d/DreamDevourer.java b/Mage.Sets/src/mage/cards/d/DreamDevourer.java index 0ed63bc1e30..688b87cf66d 100644 --- a/Mage.Sets/src/mage/cards/d/DreamDevourer.java +++ b/Mage.Sets/src/mage/cards/d/DreamDevourer.java @@ -1,27 +1,17 @@ package mage.cards.d; -import java.util.UUID; import mage.MageInt; -import mage.abilities.Ability; import mage.abilities.common.ForetellSourceControllerTriggeredAbility; import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.effects.ContinuousEffectImpl; import mage.abilities.effects.common.continuous.BoostSourceEffect; import mage.abilities.keyword.ForetellAbility; -import mage.cards.*; -import mage.constants.SubType; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Duration; -import mage.constants.Layer; -import mage.constants.Outcome; -import mage.constants.SubLayer; -import mage.constants.Zone; -import mage.filter.common.FilterNonlandCard; -import mage.filter.predicate.Predicates; -import mage.filter.predicate.mageobject.AbilityPredicate; -import mage.game.Game; -import mage.players.Player; -import mage.util.CardUtil; +import mage.constants.SubType; + +import java.util.UUID; /** * @@ -38,7 +28,7 @@ public final class DreamDevourer extends CardImpl { this.toughness = new MageInt(3); // Each nonland card in your hand without foretell has foretell. Its foretell cost is equal to its mana cost reduced by 2. - this.addAbility(new SimpleStaticAbility(new DreamDevourerAddAbilityEffect())); + this.addAbility(new SimpleStaticAbility(ForetellAbility.makeAddForetellEffect())); // Whenever you foretell a card, Dream Devourer gets +2/+0 until end of turn. this.addAbility(new ForetellSourceControllerTriggeredAbility(new BoostSourceEffect(2, 0, Duration.EndOfTurn))); @@ -54,69 +44,3 @@ public final class DreamDevourer extends CardImpl { return new DreamDevourer(this); } } - -class DreamDevourerAddAbilityEffect extends ContinuousEffectImpl { - - private static final FilterNonlandCard filter = new FilterNonlandCard(); - - static { - filter.add(Predicates.not(new AbilityPredicate(ForetellAbility.class))); - } - - DreamDevourerAddAbilityEffect() { - super(Duration.WhileOnBattlefield, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility); - this.staticText = "Each nonland card in your hand without foretell has foretell. Its foretell cost is equal to its mana cost reduced by {2}"; - } - - private DreamDevourerAddAbilityEffect(final DreamDevourerAddAbilityEffect effect) { - super(effect); - } - - @Override - public DreamDevourerAddAbilityEffect copy() { - return new DreamDevourerAddAbilityEffect(this); - } - - @Override - public boolean apply(Game game, Ability source) { - Player controller = game.getPlayer(source.getControllerId()); - if (controller == null) { - return false; - } - for (Card card : controller.getHand().getCards(filter, game)) { - ForetellAbility foretellAbility = null; - if (card instanceof SplitCard) { - String leftHalfCost = CardUtil.reduceCost(((SplitCard) card).getLeftHalfCard().getManaCost(), 2).getText(); - String rightHalfCost = CardUtil.reduceCost(((SplitCard) card).getRightHalfCard().getManaCost(), 2).getText(); - foretellAbility = new ForetellAbility(card, leftHalfCost, rightHalfCost); - } else if (card instanceof ModalDoubleFacedCard) { - ModalDoubleFacedCardHalf leftHalfCard = ((ModalDoubleFacedCard) card).getLeftHalfCard(); - // If front side of MDFC is land, do nothing as Dream Devourer does not apply to lands - // MDFC cards in hand are considered lands if front side is land - if (!leftHalfCard.isLand(game)) { - String leftHalfCost = CardUtil.reduceCost(leftHalfCard.getManaCost(), 2).getText(); - ModalDoubleFacedCardHalf rightHalfCard = ((ModalDoubleFacedCard) card).getRightHalfCard(); - if (rightHalfCard.isLand(game)) { - foretellAbility = new ForetellAbility(card, leftHalfCost); - } else { - String rightHalfCost = CardUtil.reduceCost(rightHalfCard.getManaCost(), 2).getText(); - foretellAbility = new ForetellAbility(card, leftHalfCost, rightHalfCost); - } - } - } else if (card instanceof CardWithSpellOption) { - String creatureCost = CardUtil.reduceCost(card.getMainCard().getManaCost(), 2).getText(); - String spellCost = CardUtil.reduceCost(((CardWithSpellOption) card).getSpellCard().getManaCost(), 2).getText(); - foretellAbility = new ForetellAbility(card, creatureCost, spellCost); - } else { - String costText = CardUtil.reduceCost(card.getManaCost(), 2).getText(); - foretellAbility = new ForetellAbility(card, costText); - } - if (foretellAbility != null) { - foretellAbility.setSourceId(card.getId()); - foretellAbility.setControllerId(card.getOwnerId()); - game.getState().addOtherAbility(card, foretellAbility); - } - } - return true; - } -} diff --git a/Mage.Sets/src/mage/cards/e/EtherealValkyrie.java b/Mage.Sets/src/mage/cards/e/EtherealValkyrie.java index b9f8d113128..5425c44aba2 100644 --- a/Mage.Sets/src/mage/cards/e/EtherealValkyrie.java +++ b/Mage.Sets/src/mage/cards/e/EtherealValkyrie.java @@ -1,23 +1,22 @@ package mage.cards.e; import mage.MageInt; -import mage.MageObjectReference; import mage.abilities.Ability; import mage.abilities.common.EntersBattlefieldOrAttacksSourceTriggeredAbility; -import mage.abilities.effects.ContinuousEffect; import mage.abilities.effects.OneShotEffect; import mage.abilities.keyword.FlyingAbility; import mage.abilities.keyword.ForetellAbility; -import mage.cards.*; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Outcome; import mage.constants.SubType; -import mage.filter.FilterCard; +import mage.filter.StaticFilters; import mage.game.Game; -import mage.game.events.GameEvent; import mage.players.Player; import mage.target.common.TargetCardInHand; -import mage.util.CardUtil; +import mage.watchers.common.ForetoldWatcher; import java.util.UUID; @@ -38,7 +37,7 @@ public final class EtherealValkyrie extends CardImpl { this.addAbility(FlyingAbility.getInstance()); // Whenever Ethereal Valkyrie enters the battlefield or attacks, draw a card, then exile a card from your hand face down. It becomes foretold. Its foretell cost is its mana cost reduced by {2}. - this.addAbility(new EntersBattlefieldOrAttacksSourceTriggeredAbility(new EtherealValkyrieEffect())); + this.addAbility(new EntersBattlefieldOrAttacksSourceTriggeredAbility(new EtherealValkyrieEffect()), new ForetoldWatcher()); } private EtherealValkyrie(final EtherealValkyrie card) { @@ -75,77 +74,15 @@ class EtherealValkyrieEffect extends OneShotEffect { if (controller == null) { return false; } - controller.drawCards(1, source, game); - TargetCardInHand targetCard = new TargetCardInHand(new FilterCard("card to exile face down. It becomes foretold.")); + TargetCardInHand targetCard = new TargetCardInHand(StaticFilters.FILTER_CARD).withChooseHint("to exile face down; it becomes foretold"); if (!controller.chooseTarget(Outcome.Benefit, targetCard, source, game)) { return false; } - - Card exileCard = game.getCard(targetCard.getFirstTarget()); - if (exileCard == null) { + Card card = game.getCard(targetCard.getFirstTarget()); + if (card == null) { return false; } - - // process Split, MDFC, and Adventure cards first - // note that 'Foretell Cost' refers to the main card (left) and 'Foretell Split Cost' refers to the (right) card if it exists - ForetellAbility foretellAbility = null; - if (exileCard instanceof SplitCard) { - String leftHalfCost = CardUtil.reduceCost(((SplitCard) exileCard).getLeftHalfCard().getManaCost(), 2).getText(); - String rightHalfCost = CardUtil.reduceCost(((SplitCard) exileCard).getRightHalfCard().getManaCost(), 2).getText(); - game.getState().setValue(exileCard.getMainCard().getId().toString() + "Foretell Cost", leftHalfCost); - game.getState().setValue(exileCard.getMainCard().getId().toString() + "Foretell Split Cost", rightHalfCost); - foretellAbility = new ForetellAbility(exileCard, leftHalfCost, rightHalfCost); - } else if (exileCard instanceof ModalDoubleFacedCard) { - ModalDoubleFacedCardHalf leftHalfCard = ((ModalDoubleFacedCard) exileCard).getLeftHalfCard(); - if (!leftHalfCard.isLand(game)) { // Only MDFC cards with a left side a land have a land on the right side too - String leftHalfCost = CardUtil.reduceCost(leftHalfCard.getManaCost(), 2).getText(); - game.getState().setValue(exileCard.getMainCard().getId().toString() + "Foretell Cost", leftHalfCost); - ModalDoubleFacedCardHalf rightHalfCard = ((ModalDoubleFacedCard) exileCard).getRightHalfCard(); - if (rightHalfCard.isLand(game)) { - foretellAbility = new ForetellAbility(exileCard, leftHalfCost); - } else { - String rightHalfCost = CardUtil.reduceCost(rightHalfCard.getManaCost(), 2).getText(); - game.getState().setValue(exileCard.getMainCard().getId().toString() + "Foretell Split Cost", rightHalfCost); - foretellAbility = new ForetellAbility(exileCard, leftHalfCost, rightHalfCost); - } - } - } else if (exileCard instanceof CardWithSpellOption) { - String creatureCost = CardUtil.reduceCost(exileCard.getMainCard().getManaCost(), 2).getText(); - String spellCost = CardUtil.reduceCost(((CardWithSpellOption) exileCard).getSpellCard().getManaCost(), 2).getText(); - game.getState().setValue(exileCard.getMainCard().getId().toString() + "Foretell Cost", creatureCost); - game.getState().setValue(exileCard.getMainCard().getId().toString() + "Foretell Split Cost", spellCost); - foretellAbility = new ForetellAbility(exileCard, creatureCost, spellCost); - } else if (!exileCard.isLand(game)) { - // normal card - String costText = CardUtil.reduceCost(exileCard.getManaCost(), 2).getText(); - game.getState().setValue(exileCard.getId().toString() + "Foretell Cost", costText); - foretellAbility = new ForetellAbility(exileCard, costText); - } - - // All card types (including lands) must be exiled - UUID exileId = CardUtil.getExileZoneId(exileCard.getMainCard().getId().toString() + "foretellAbility", game); - controller.moveCardsToExile(exileCard, source, game, true, exileId, " Foretell Turn Number: " + game.getTurnNum()); - exileCard.setFaceDown(true, game); - - // all done pre-processing so stick the foretell cost effect onto the main card - // note that the card is not foretell'd into exile, it is put into exile and made foretold - // If the card is a non-land, it will not be exiled. - if (foretellAbility != null) { - // copy source and use it for the foretold effect on the exiled card - // bug #8673 - Ability copiedSource = source.copy(); - copiedSource.newId(); - copiedSource.setSourceId(exileCard.getId()); - game.getState().setValue(exileCard.getMainCard().getId().toString() + "Foretell Turn Number", game.getTurnNum()); - foretellAbility.setSourceId(exileCard.getId()); - foretellAbility.setControllerId(exileCard.getOwnerId()); - game.getState().addOtherAbility(exileCard, foretellAbility); - foretellAbility.activate(game, true); - ContinuousEffect effect = new ForetellAbility.ForetellAddCostEffect(new MageObjectReference(exileCard, game)); - game.addEffect(effect, copiedSource); - game.fireEvent(GameEvent.getEvent(GameEvent.EventType.FORETOLD, exileCard.getId(), null, null)); - } - return true; + return ForetellAbility.doExileBecomesForetold(card, game, source, 2); } } diff --git a/Mage.Sets/src/mage/cards/r/RanarTheEverWatchful.java b/Mage.Sets/src/mage/cards/r/RanarTheEverWatchful.java index ad16f667ac9..4a63e71307c 100644 --- a/Mage.Sets/src/mage/cards/r/RanarTheEverWatchful.java +++ b/Mage.Sets/src/mage/cards/r/RanarTheEverWatchful.java @@ -89,7 +89,7 @@ class RanarTheEverWatchfulCostReductionEffect extends CostModificationEffectImpl public boolean applies(Ability abilityToModify, Ability source, Game game) { ForetoldWatcher watcher = game.getState().getWatcher(ForetoldWatcher.class); return (watcher != null - && watcher.countNumberForetellThisTurn() == 0 + && watcher.getPlayerForetellCountThisTurn(source.getControllerId()) == 0 && abilityToModify.isControlledBy(source.getControllerId()) && abilityToModify instanceof ForetellAbility); } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ForetellTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ForetellTest.java index d83cbb867ab..56d21bbca97 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ForetellTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ForetellTest.java @@ -2,6 +2,7 @@ package org.mage.test.cards.abilities.keywords; import mage.constants.PhaseStep; import mage.constants.Zone; +import mage.counters.CounterType; import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; @@ -81,4 +82,243 @@ public class ForetellTest extends CardTestPlayerBase { assertExileCount(playerA, "Lightning Bolt", 1); // foretold card in exile assertPowerToughness(playerA, "Dream Devourer", 2, 3); // +2 power boost from trigger due to foretell of Lightning Bolt } + + + // Tests needed to check watcher scope issue (see issue #7493 and issue #13774) + + private static final String scornEffigy = "Scorn Effigy"; // {3} 2/3 foretell {0} + private static final String poisonCup = "Poison the Cup"; // {1}{B}{B} instant destroy target creature + // Foretell {1}{B}, if spell was foretold, scry 2 + private static final String flamespeaker = "Flamespeaker Adept"; // {2}{R} 2/3 + // Whenever you scry, gets +2/+0 and first strike until end of turn + private static final String chancemetElves = "Chance-Met Elves"; // {2}{G} 3/2 + // Whenever you scry, gets a +1/+1 counter, triggers once per turn + + private static final String cardE = "Elite Vanguard"; + private static final String cardD = "Devilthorn Fox"; + private static final String cardC = "Canopy Gorger"; + private static final String cardB = "Barbtooth Wurm"; + private static final String cardA = "Alaborn Trooper"; + + private void setupLibrariesEtc() { + // make a library of 5 cards, bottom : E D C B A : top + skipInitShuffling(); + removeAllCardsFromLibrary(playerA); + addCard(Zone.LIBRARY, playerA, cardE); + addCard(Zone.LIBRARY, playerA, cardD); + addCard(Zone.LIBRARY, playerA, cardC); + addCard(Zone.LIBRARY, playerA, cardB); + addCard(Zone.LIBRARY, playerA, cardA); + removeAllCardsFromLibrary(playerB); + addCard(Zone.LIBRARY, playerB, cardE); + addCard(Zone.LIBRARY, playerB, cardD); + addCard(Zone.LIBRARY, playerB, cardC); + addCard(Zone.LIBRARY, playerB, cardB); + addCard(Zone.LIBRARY, playerB, cardA); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 5); + addCard(Zone.BATTLEFIELD, playerB, "Swamp", 5); + addCard(Zone.BATTLEFIELD, playerA, flamespeaker); + addCard(Zone.BATTLEFIELD, playerB, chancemetElves); + addCard(Zone.HAND, playerA, scornEffigy); + } + + @Test + public void testForetellWatcherPlayerA() { + setupLibrariesEtc(); + addCard(Zone.HAND, playerA, poisonCup); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, scornEffigy); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Foretell"); + checkExileCount("foretold in exile", 2, PhaseStep.PRECOMBAT_MAIN, playerA, poisonCup, 1); + // turn 3, draw card A + activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Foretell {1}{B}", chancemetElves); + // foretold, so scry 2 (cards B and C) + addTarget(playerA, cardB); // scrying B bottom (C remains on top) + + setStrictChooseMode(true); + setStopAt(3, PhaseStep.END_TURN); + execute(); + + assertPowerToughness(playerA, scornEffigy, 2, 3); + assertGraveyardCount(playerA, poisonCup, 1); + assertGraveyardCount(playerB, chancemetElves, 1); + assertPowerToughness(playerA, flamespeaker, 4, 3); + } + + @Test + public void testForetellWatcherPlayerB() { + setupLibrariesEtc(); + addCard(Zone.HAND, playerB, poisonCup); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, scornEffigy); + // turn 2, draw card A + activateAbility(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Foretell"); + checkExileCount("foretold in exile", 2, PhaseStep.PRECOMBAT_MAIN, playerB, poisonCup, 1); + // turn 4, draw card B + activateAbility(4, PhaseStep.PRECOMBAT_MAIN, playerB, "Foretell {1}{B}", flamespeaker); + // foretold, so scry 2 (cards C and D) + addTarget(playerB, cardD); // scrying D bottom (C remains on top) + + setStrictChooseMode(true); + setStopAt(4, PhaseStep.END_TURN); + execute(); + + assertPowerToughness(playerA, scornEffigy, 2, 3); + assertGraveyardCount(playerB, poisonCup, 1); + assertGraveyardCount(playerA, flamespeaker, 1); + assertPowerToughness(playerB, chancemetElves, 4, 3); + } + + @Test + public void testRanar() { + skipInitShuffling(); + String ranar = "Ranar the Ever-Watchful"; // 2WU 2/3 Flying Vigilance + // The first card you foretell each turn costs {0} to foretell. + // Whenever one or more cards are put into exile from your hand or a spell or ability you control exiles + // one or more permanents from the battlefield, create a 1/1 white Spirit creature token with flying. + addCard(Zone.BATTLEFIELD, playerA, ranar); + addCard(Zone.BATTLEFIELD, playerA, "Sage of the Falls"); // may loot on creature ETB + addCard(Zone.HAND, playerA, poisonCup); + addCard(Zone.LIBRARY, playerA, scornEffigy); + addCard(Zone.HAND, playerA, "Wastes"); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Foretell"); // poison the cup + setChoice(playerA, true); // yes to loot + setChoice(playerA, "Wastes"); // discard + + checkExileCount("Poison the Cup foretold", 1, PhaseStep.BEGIN_COMBAT, playerA, poisonCup, 1); + checkHandCardCount("scorn effigy drawn", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, scornEffigy, 1); + checkPlayableAbility("can't foretell another for free", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Foretell", false); + + activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Foretell"); // scorn effigy + setChoice(playerA, false); // no loot + + setStrictChooseMode(true); + setStopAt(3, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, "Spirit Token", 2); + assertExileCount(playerA, 2); + assertGraveyardCount(playerA, "Wastes", 1); + + } + + @Test + public void testCosmosCharger() { + addCard(Zone.BATTLEFIELD, playerA, "Cosmos Charger"); + // Foretelling cards from your hand costs {1} less and can be done on any player’s turn. + + addCard(Zone.HAND, playerA, scornEffigy); + addCard(Zone.BATTLEFIELD, playerA, "Wastes"); + + activateAbility(2, PhaseStep.UPKEEP, playerA, "Foretell"); + + setStrictChooseMode(true); + setStopAt(2, PhaseStep.END_TURN); + execute(); + + assertExileCount(playerA, scornEffigy, 1); + } + + @Test + public void testAlrund() { + String alrund = "Alrund, God of the Cosmos"; + // Alrund gets +1/+1 for each card in your hand and each foretold card you own in exile. + + addCard(Zone.BATTLEFIELD, playerA, alrund); // 1/1 + addCard(Zone.HAND, playerA, scornEffigy); + addCard(Zone.HAND, playerA, "Lightning Bolt"); + addCard(Zone.BATTLEFIELD, playerA, "Cadaverous Bloom"); + // Exile a card from your hand: Add {B}{B} or {G}{G}. + + activateAbility(1, PhaseStep.BEGIN_COMBAT, playerA, "Exile a card from your hand: Add {B}{B}"); + setChoice(playerA, "Lightning Bolt"); + activateAbility(1, PhaseStep.BEGIN_COMBAT, playerA, "Foretell"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertHandCount(playerA, 0); + assertExileCount(playerA, scornEffigy, 1); + assertPowerToughness(playerA, alrund, 2, 2); + } + + private static final String valkyrie = "Ethereal Valkyrie"; // 4/4 flying + // Whenever this creature enters or attacks, draw a card, then exile a card from your hand face down. + // It becomes foretold. Its foretell cost is its mana cost reduced by {2}. + + @Test + public void testEtherealValkyrie() { + skipInitShuffling(); + removeAllCardsFromLibrary(playerA); + String saga = "Niko Defies Destiny"; + // I - You gain 2 life for each foretold card you own in exile. + // II - Add {W}{U}. Spend this mana only to foretell cards or cast spells that have foretell. + String crab = "Fortress Crab"; // 3U 1/6 + String puma = "Stonework Puma"; // {3} 2/2 + addCard(Zone.BATTLEFIELD, playerA, valkyrie); + addCard(Zone.HAND, playerA, saga); + addCard(Zone.HAND, playerA, crab); + addCard(Zone.BATTLEFIELD, playerA, "Tundra", 5); + addCard(Zone.LIBRARY, playerA, "Wastes"); + addCard(Zone.LIBRARY, playerA, puma); + + attack(1, playerA, valkyrie, playerB); + addTarget(playerA, crab); // exile becomes foretold + + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, saga); // gain 2 life + waitStackResolved(1, PhaseStep.POSTCOMBAT_MAIN); + checkExileCount("crab foretold", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, crab, 1); + checkPlayableAbility("can't cast foretold same turn", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Foretell", false); + + waitStackResolved(3, PhaseStep.PRECOMBAT_MAIN); + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, puma); + + activateAbility(3, PhaseStep.POSTCOMBAT_MAIN, playerA, "Foretell"); + + setStrictChooseMode(true); + setStopAt(3, PhaseStep.END_TURN); + execute(); + + assertLife(playerA, 22); + assertLife(playerB, 16); + assertCounterCount(saga, CounterType.LORE, 2); + assertPowerToughness(playerA, crab, 1, 6); + assertPowerToughness(playerA, valkyrie, 4, 4); + assertPowerToughness(playerA, puma, 2, 2); + assertHandCount(playerA, 1); + assertHandCount(playerA, "Wastes", 1); + assertTappedCount("Tundra", true, 5); + } + + @Test + public void testForetoldNotForetell() { + skipInitShuffling(); + removeAllCardsFromLibrary(playerA); + addCard(Zone.LIBRARY, playerA, "Wastes"); + addCard(Zone.LIBRARY, playerA, "Darksteel Citadel"); + addCard(Zone.BATTLEFIELD, playerA, valkyrie); + addCard(Zone.BATTLEFIELD, playerA, "Dream Devourer"); + addCard(Zone.HAND, playerA, "Papercraft Decoy"); + + attack(1, playerA, valkyrie, playerB); + addTarget(playerA, "Papercraft Decoy"); // exile becomes foretold + + checkPT("Dream Devourer not boosted", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Dream Devourer", 0, 3); + checkPlayableAbility("Can't cast this turn", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Foretell", false); + checkHandCardCount("card drawn", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Darksteel Citadel", 1); + + activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Foretell"); + + setStrictChooseMode(true); + setStopAt(3, PhaseStep.END_TURN); + execute(); + + assertLife(playerA, 20); + assertLife(playerB, 16); + assertPowerToughness(playerA, "Papercraft Decoy", 2, 1); + assertPowerToughness(playerA, "Dream Devourer", 0, 3); + assertHandCount(playerA, 2); + } + + } diff --git a/Mage/src/main/java/mage/abilities/common/ForetellSourceControllerTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/ForetellSourceControllerTriggeredAbility.java index 975f5b725ec..81672a10fe9 100644 --- a/Mage/src/main/java/mage/abilities/common/ForetellSourceControllerTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/ForetellSourceControllerTriggeredAbility.java @@ -7,6 +7,7 @@ import mage.constants.Zone; import mage.game.Game; import mage.game.events.GameEvent; import mage.players.Player; +import mage.watchers.common.ForetoldWatcher; /** * @author jeffwadsworth @@ -16,6 +17,7 @@ public class ForetellSourceControllerTriggeredAbility extends TriggeredAbilityIm public ForetellSourceControllerTriggeredAbility(Effect effect) { super(Zone.BATTLEFIELD, effect, false); setTriggerPhrase("Whenever you foretell a card, "); + addWatcher(new ForetoldWatcher()); } protected ForetellSourceControllerTriggeredAbility(final ForetellSourceControllerTriggeredAbility ability) { @@ -24,16 +26,14 @@ public class ForetellSourceControllerTriggeredAbility extends TriggeredAbilityIm @Override public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.FORETELL; + return event.getType() == GameEvent.EventType.CARD_FORETOLD; } @Override public boolean checkTrigger(GameEvent event, Game game) { Card card = game.getCard(event.getTargetId()); Player player = game.getPlayer(event.getPlayerId()); - return (card != null - && player != null - && isControlledBy(player.getId())); + return event.getFlag() && card != null && player != null && isControlledBy(player.getId()); } @Override diff --git a/Mage/src/main/java/mage/abilities/condition/common/ForetoldCondition.java b/Mage/src/main/java/mage/abilities/condition/common/ForetoldCondition.java index c0e63fc8663..9e3159b298f 100644 --- a/Mage/src/main/java/mage/abilities/condition/common/ForetoldCondition.java +++ b/Mage/src/main/java/mage/abilities/condition/common/ForetoldCondition.java @@ -17,7 +17,7 @@ public enum ForetoldCondition implements Condition { public boolean apply(Game game, Ability source) { ForetoldWatcher watcher = game.getState().getWatcher(ForetoldWatcher.class); if (watcher != null) { - return watcher.cardWasForetold(source.getSourceId()); + return watcher.checkForetold(source.getSourceId(), game); } return false; } diff --git a/Mage/src/main/java/mage/abilities/keyword/ForetellAbility.java b/Mage/src/main/java/mage/abilities/keyword/ForetellAbility.java index d094a49f537..421a25858d3 100644 --- a/Mage/src/main/java/mage/abilities/keyword/ForetellAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/ForetellAbility.java @@ -11,11 +11,15 @@ import mage.abilities.costs.Costs; import mage.abilities.costs.mana.GenericManaCost; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.AsThoughEffectImpl; +import mage.abilities.effects.ContinuousEffect; import mage.abilities.effects.ContinuousEffectImpl; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.ExileTargetEffect; import mage.cards.*; import mage.constants.*; +import mage.filter.common.FilterNonlandCard; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.mageobject.AbilityPredicate; import mage.game.ExileZone; import mage.game.Game; import mage.game.events.GameEvent; @@ -90,132 +94,236 @@ public class ForetellAbility extends SpecialAction { return " foretells a card from hand"; } - static class ForetellExileEffect extends OneShotEffect { - - private final Card card; - String foretellCost; - String foretellSplitCost; - - ForetellExileEffect(Card card, String foretellCost, String foretellSplitCost) { - super(Outcome.Neutral); - this.card = card; - this.foretellCost = foretellCost; - this.foretellSplitCost = foretellSplitCost; - } - - protected ForetellExileEffect(final ForetellExileEffect effect) { - super(effect); - this.card = effect.card; - this.foretellCost = effect.foretellCost; - this.foretellSplitCost = effect.foretellSplitCost; - } - - @Override - public ForetellExileEffect copy() { - return new ForetellExileEffect(this); - } - - @Override - public boolean apply(Game game, Ability source) { - Player controller = game.getPlayer(source.getControllerId()); - if (controller != null - && card != null) { - - // get main card id - UUID mainCardId = card.getMainCard().getId(); - - // retrieve the exileId of the foretold card - UUID exileId = CardUtil.getExileZoneId(mainCardId.toString() + "foretellAbility", game); - - // foretell turn number shows up on exile window - ExileTargetEffect effect = new ExileTargetEffect(exileId, " Foretell Turn Number: " + game.getTurnNum()); - - // remember turn number it was cast - game.getState().setValue(mainCardId.toString() + "Foretell Turn Number", game.getTurnNum()); - - // remember the foretell cost - game.getState().setValue(mainCardId.toString() + "Foretell Cost", foretellCost); - game.getState().setValue(mainCardId.toString() + "Foretell Split Cost", foretellSplitCost); - - // exile the card face-down - effect.setWithName(false); - effect.setTargetPointer(new FixedTarget(card.getId(), game)); - effect.apply(game, source); - card.setFaceDown(true, game); - game.addEffect(new ForetellAddCostEffect(new MageObjectReference(card, game)), source); - game.fireEvent(GameEvent.getEvent(GameEvent.EventType.FORETELL, card.getId(), null, source.getControllerId())); - return true; - } - return false; - } + public static boolean isCardInForetell(Card card, Game game) { + // searching ForetellCostAbility - it adds for foretelled cards only after exile + return card.getAbilities(game).containsClass(ForetellCostAbility.class); } - static class ForetellLookAtCardEffect extends AsThoughEffectImpl { + public static ContinuousEffect makeAddForetellEffect() { + return new ForetellAddAbilityEffect(); + } - ForetellLookAtCardEffect() { - super(AsThoughEffectType.LOOK_AT_FACE_DOWN, Duration.EndOfGame, Outcome.AIDontUseIt); + /** + * For use in apply() method of OneShotEffect + * Exile the target card. It becomes foretold. + * Its foretell cost is its mana cost reduced by [amountToReduceCost] + */ + public static boolean doExileBecomesForetold(Card card, Game game, Ability source, int amountToReduceCost) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller == null) { + return false; } - protected ForetellLookAtCardEffect(final ForetellLookAtCardEffect effect) { - super(effect); - } - - @Override - public boolean apply(Game game, Ability source) { - return true; - } - - @Override - public ForetellLookAtCardEffect copy() { - return new ForetellLookAtCardEffect(this); - } - - @Override - public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) { - if (affectedControllerId.equals(source.getControllerId())) { - Card card = game.getCard(objectId); - if (card != null) { - MageObject sourceObject = game.getObject(source); - if (sourceObject == null) { - return false; - } - UUID mainCardId = card.getMainCard().getId(); - UUID exileId = CardUtil.getExileZoneId(mainCardId.toString() + "foretellAbility", game); - ExileZone exile = game.getExile().getExileZone(exileId); - return exile != null - && exile.contains(mainCardId); + // process Split, MDFC, and Adventure cards first + // note that 'Foretell Cost' refers to the main card (left) and 'Foretell Split Cost' refers to the (right) card if it exists + ForetellAbility foretellAbility = null; + if (card instanceof SplitCard) { + String leftHalfCost = CardUtil.reduceCost(((SplitCard) card).getLeftHalfCard().getManaCost(), amountToReduceCost).getText(); + String rightHalfCost = CardUtil.reduceCost(((SplitCard) card).getRightHalfCard().getManaCost(), amountToReduceCost).getText(); + game.getState().setValue(card.getMainCard().getId().toString() + "Foretell Cost", leftHalfCost); + game.getState().setValue(card.getMainCard().getId().toString() + "Foretell Split Cost", rightHalfCost); + foretellAbility = new ForetellAbility(card, leftHalfCost, rightHalfCost); + } else if (card instanceof ModalDoubleFacedCard) { + ModalDoubleFacedCardHalf leftHalfCard = ((ModalDoubleFacedCard) card).getLeftHalfCard(); + if (!leftHalfCard.isLand(game)) { // Only MDFC cards with a left side a land have a land on the right side too + String leftHalfCost = CardUtil.reduceCost(leftHalfCard.getManaCost(), amountToReduceCost).getText(); + game.getState().setValue(card.getMainCard().getId().toString() + "Foretell Cost", leftHalfCost); + ModalDoubleFacedCardHalf rightHalfCard = ((ModalDoubleFacedCard) card).getRightHalfCard(); + if (rightHalfCard.isLand(game)) { + foretellAbility = new ForetellAbility(card, leftHalfCost); + } else { + String rightHalfCost = CardUtil.reduceCost(rightHalfCard.getManaCost(), amountToReduceCost).getText(); + game.getState().setValue(card.getMainCard().getId().toString() + "Foretell Split Cost", rightHalfCost); + foretellAbility = new ForetellAbility(card, leftHalfCost, rightHalfCost); } } - return false; + } else if (card instanceof CardWithSpellOption) { + String creatureCost = CardUtil.reduceCost(card.getMainCard().getManaCost(), amountToReduceCost).getText(); + String spellCost = CardUtil.reduceCost(((CardWithSpellOption) card).getSpellCard().getManaCost(), amountToReduceCost).getText(); + game.getState().setValue(card.getMainCard().getId().toString() + "Foretell Cost", creatureCost); + game.getState().setValue(card.getMainCard().getId().toString() + "Foretell Split Cost", spellCost); + foretellAbility = new ForetellAbility(card, creatureCost, spellCost); + } else if (!card.isLand(game)) { + // normal card + String costText = CardUtil.reduceCost(card.getManaCost(), amountToReduceCost).getText(); + game.getState().setValue(card.getId().toString() + "Foretell Cost", costText); + foretellAbility = new ForetellAbility(card, costText); } + + // All card types (including lands) must be exiled + UUID exileId = CardUtil.getExileZoneId(card.getMainCard().getId().toString() + "foretellAbility", game); + controller.moveCardsToExile(card, source, game, false, exileId, " Foretell Turn Number: " + game.getTurnNum()); + card.setFaceDown(true, game); + + // all done pre-processing so stick the foretell cost effect onto the main card + // note that the card is not foretell'd into exile, it is put into exile and made foretold + // If the card is a non-land, it will not be exiled. + if (foretellAbility != null) { + // copy source and use it for the foretold effect on the exiled card + // bug #8673 + Ability copiedSource = source.copy(); + copiedSource.newId(); + copiedSource.setSourceId(card.getId()); + game.getState().setValue(card.getMainCard().getId().toString() + "Foretell Turn Number", game.getTurnNum()); + foretellAbility.setSourceId(card.getId()); + foretellAbility.setControllerId(card.getOwnerId()); + game.getState().addOtherAbility(card, foretellAbility); + foretellAbility.activate(game, true); + game.addEffect(new ForetellAddCostEffect(new MageObjectReference(card, game)), copiedSource); + game.fireEvent(new GameEvent(GameEvent.EventType.CARD_FORETOLD, card.getId(), copiedSource, copiedSource.getControllerId(), 0, false)); + } + return true; } - public static class ForetellAddCostEffect extends ContinuousEffectImpl { +} - private final MageObjectReference mor; +class ForetellExileEffect extends OneShotEffect { - public ForetellAddCostEffect(MageObjectReference mor) { - super(Duration.EndOfGame, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility); - this.mor = mor; - staticText = "Foretold card"; + private final Card card; + String foretellCost; + String foretellSplitCost; + + ForetellExileEffect(Card card, String foretellCost, String foretellSplitCost) { + super(Outcome.Neutral); + this.card = card; + this.foretellCost = foretellCost; + this.foretellSplitCost = foretellSplitCost; + } + + private ForetellExileEffect(final ForetellExileEffect effect) { + super(effect); + this.card = effect.card; + this.foretellCost = effect.foretellCost; + this.foretellSplitCost = effect.foretellSplitCost; + } + + @Override + public ForetellExileEffect copy() { + return new ForetellExileEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller != null + && card != null) { + + // get main card id + UUID mainCardId = card.getMainCard().getId(); + + // retrieve the exileId of the foretold card + UUID exileId = CardUtil.getExileZoneId(mainCardId.toString() + "foretellAbility", game); + + // foretell turn number shows up on exile window + ExileTargetEffect effect = new ExileTargetEffect(exileId, " Foretell Turn Number: " + game.getTurnNum()); + + // remember turn number it was cast + game.getState().setValue(mainCardId.toString() + "Foretell Turn Number", game.getTurnNum()); + + // remember the foretell cost + game.getState().setValue(mainCardId.toString() + "Foretell Cost", foretellCost); + game.getState().setValue(mainCardId.toString() + "Foretell Split Cost", foretellSplitCost); + + // exile the card face-down + effect.setWithName(false); + effect.setTargetPointer(new FixedTarget(card.getId(), game)); + effect.apply(game, source); + card.setFaceDown(true, game); + game.addEffect(new ForetellAddCostEffect(new MageObjectReference(card, game)), source); + game.fireEvent(new GameEvent(GameEvent.EventType.CARD_FORETOLD, card.getId(), source, source.getControllerId(), 0, true)); + return true; } + return false; + } +} - protected ForetellAddCostEffect(final ForetellAddCostEffect effect) { - super(effect); - this.mor = effect.mor; - } +class ForetellLookAtCardEffect extends AsThoughEffectImpl { - @Override - public boolean apply(Game game, Ability source) { - Card card = mor.getCard(game); + ForetellLookAtCardEffect() { + super(AsThoughEffectType.LOOK_AT_FACE_DOWN, Duration.EndOfGame, Outcome.AIDontUseIt); + } + + private ForetellLookAtCardEffect(final ForetellLookAtCardEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + return true; + } + + @Override + public ForetellLookAtCardEffect copy() { + return new ForetellLookAtCardEffect(this); + } + + @Override + public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) { + if (affectedControllerId.equals(source.getControllerId())) { + Card card = game.getCard(objectId); if (card != null) { + MageObject sourceObject = game.getObject(source); + if (sourceObject == null) { + return false; + } UUID mainCardId = card.getMainCard().getId(); - if (game.getState().getZone(mainCardId) == Zone.EXILED) { - String foretellCost = (String) game.getState().getValue(mainCardId.toString() + "Foretell Cost"); - String foretellSplitCost = (String) game.getState().getValue(mainCardId.toString() + "Foretell Split Cost"); - if (card instanceof SplitCard) { - if (foretellCost != null) { - SplitCardHalf leftHalfCard = ((SplitCard) card).getLeftHalfCard(); + UUID exileId = CardUtil.getExileZoneId(mainCardId.toString() + "foretellAbility", game); + ExileZone exile = game.getExile().getExileZone(exileId); + return exile != null + && exile.contains(mainCardId); + } + } + return false; + } +} + +class ForetellAddCostEffect extends ContinuousEffectImpl { + + private final MageObjectReference mor; + + ForetellAddCostEffect(MageObjectReference mor) { + super(Duration.EndOfGame, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility); + this.mor = mor; + staticText = "Foretold card"; + } + + private ForetellAddCostEffect(final ForetellAddCostEffect effect) { + super(effect); + this.mor = effect.mor; + } + + @Override + public boolean apply(Game game, Ability source) { + Card card = mor.getCard(game); + if (card != null) { + UUID mainCardId = card.getMainCard().getId(); + if (game.getState().getZone(mainCardId) == Zone.EXILED) { + String foretellCost = (String) game.getState().getValue(mainCardId.toString() + "Foretell Cost"); + String foretellSplitCost = (String) game.getState().getValue(mainCardId.toString() + "Foretell Split Cost"); + if (card instanceof SplitCard) { + if (foretellCost != null) { + SplitCardHalf leftHalfCard = ((SplitCard) card).getLeftHalfCard(); + ForetellCostAbility ability = new ForetellCostAbility(foretellCost); + ability.setSourceId(leftHalfCard.getId()); + ability.setControllerId(source.getControllerId()); + ability.setSpellAbilityType(leftHalfCard.getSpellAbility().getSpellAbilityType()); + ability.setAbilityName(leftHalfCard.getName()); + game.getState().addOtherAbility(leftHalfCard, ability); + } + if (foretellSplitCost != null) { + SplitCardHalf rightHalfCard = ((SplitCard) card).getRightHalfCard(); + ForetellCostAbility ability = new ForetellCostAbility(foretellSplitCost); + ability.setSourceId(rightHalfCard.getId()); + ability.setControllerId(source.getControllerId()); + ability.setSpellAbilityType(rightHalfCard.getSpellAbility().getSpellAbilityType()); + ability.setAbilityName(rightHalfCard.getName()); + game.getState().addOtherAbility(rightHalfCard, ability); + } + } else if (card instanceof ModalDoubleFacedCard) { + if (foretellCost != null) { + ModalDoubleFacedCardHalf leftHalfCard = ((ModalDoubleFacedCard) card).getLeftHalfCard(); + // some MDFC's are land IE: sea gate restoration + if (!leftHalfCard.isLand(game)) { ForetellCostAbility ability = new ForetellCostAbility(foretellCost); ability.setSourceId(leftHalfCard.getId()); ability.setControllerId(source.getControllerId()); @@ -223,8 +331,11 @@ public class ForetellAbility extends SpecialAction { ability.setAbilityName(leftHalfCard.getName()); game.getState().addOtherAbility(leftHalfCard, ability); } - if (foretellSplitCost != null) { - SplitCardHalf rightHalfCard = ((SplitCard) card).getRightHalfCard(); + } + if (foretellSplitCost != null) { + ModalDoubleFacedCardHalf rightHalfCard = ((ModalDoubleFacedCard) card).getRightHalfCard(); + // some MDFC's are land IE: sea gate restoration + if (!rightHalfCard.isLand(game)) { ForetellCostAbility ability = new ForetellCostAbility(foretellSplitCost); ability.setSourceId(rightHalfCard.getId()); ability.setControllerId(source.getControllerId()); @@ -232,240 +343,276 @@ public class ForetellAbility extends SpecialAction { ability.setAbilityName(rightHalfCard.getName()); game.getState().addOtherAbility(rightHalfCard, ability); } - } else if (card instanceof ModalDoubleFacedCard) { - if (foretellCost != null) { - ModalDoubleFacedCardHalf leftHalfCard = ((ModalDoubleFacedCard) card).getLeftHalfCard(); - // some MDFC's are land IE: sea gate restoration - if (!leftHalfCard.isLand(game)) { - ForetellCostAbility ability = new ForetellCostAbility(foretellCost); - ability.setSourceId(leftHalfCard.getId()); - ability.setControllerId(source.getControllerId()); - ability.setSpellAbilityType(leftHalfCard.getSpellAbility().getSpellAbilityType()); - ability.setAbilityName(leftHalfCard.getName()); - game.getState().addOtherAbility(leftHalfCard, ability); - } - } - if (foretellSplitCost != null) { - ModalDoubleFacedCardHalf rightHalfCard = ((ModalDoubleFacedCard) card).getRightHalfCard(); - // some MDFC's are land IE: sea gate restoration - if (!rightHalfCard.isLand(game)) { - ForetellCostAbility ability = new ForetellCostAbility(foretellSplitCost); - ability.setSourceId(rightHalfCard.getId()); - ability.setControllerId(source.getControllerId()); - ability.setSpellAbilityType(rightHalfCard.getSpellAbility().getSpellAbilityType()); - ability.setAbilityName(rightHalfCard.getName()); - game.getState().addOtherAbility(rightHalfCard, ability); - } - } - } else if (card instanceof CardWithSpellOption) { - if (foretellCost != null) { - Card creatureCard = card.getMainCard(); - ForetellCostAbility ability = new ForetellCostAbility(foretellCost); - ability.setSourceId(creatureCard.getId()); - ability.setControllerId(source.getControllerId()); - ability.setSpellAbilityType(creatureCard.getSpellAbility().getSpellAbilityType()); - ability.setAbilityName(creatureCard.getName()); - game.getState().addOtherAbility(creatureCard, ability); - } - if (foretellSplitCost != null) { - Card spellCard = ((CardWithSpellOption) card).getSpellCard(); - ForetellCostAbility ability = new ForetellCostAbility(foretellSplitCost); - ability.setSourceId(spellCard.getId()); - ability.setControllerId(source.getControllerId()); - ability.setSpellAbilityType(spellCard.getSpellAbility().getSpellAbilityType()); - ability.setAbilityName(spellCard.getName()); - game.getState().addOtherAbility(spellCard, ability); - } - } else if (foretellCost != null) { + } + } else if (card instanceof CardWithSpellOption) { + if (foretellCost != null) { + Card creatureCard = card.getMainCard(); ForetellCostAbility ability = new ForetellCostAbility(foretellCost); - ability.setSourceId(card.getId()); + ability.setSourceId(creatureCard.getId()); ability.setControllerId(source.getControllerId()); - ability.setSpellAbilityType(card.getSpellAbility().getSpellAbilityType()); - ability.setAbilityName(card.getName()); - game.getState().addOtherAbility(card, ability); + ability.setSpellAbilityType(creatureCard.getSpellAbility().getSpellAbilityType()); + ability.setAbilityName(creatureCard.getName()); + game.getState().addOtherAbility(creatureCard, ability); } - return true; + if (foretellSplitCost != null) { + Card spellCard = ((CardWithSpellOption) card).getSpellCard(); + ForetellCostAbility ability = new ForetellCostAbility(foretellSplitCost); + ability.setSourceId(spellCard.getId()); + ability.setControllerId(source.getControllerId()); + ability.setSpellAbilityType(spellCard.getSpellAbility().getSpellAbilityType()); + ability.setAbilityName(spellCard.getName()); + game.getState().addOtherAbility(spellCard, ability); + } + } else if (foretellCost != null) { + ForetellCostAbility ability = new ForetellCostAbility(foretellCost); + ability.setSourceId(card.getId()); + ability.setControllerId(source.getControllerId()); + ability.setSpellAbilityType(card.getSpellAbility().getSpellAbilityType()); + ability.setAbilityName(card.getName()); + game.getState().addOtherAbility(card, ability); } + return true; } - discard(); - return true; - } - - @Override - public ForetellAddCostEffect copy() { - return new ForetellAddCostEffect(this); } + discard(); + return true; } - static class ForetellCostAbility extends SpellAbility { - - private String abilityName; - private SpellAbility spellAbilityToResolve; - - ForetellCostAbility(String foretellCost) { - super(null, "Testing", Zone.EXILED, SpellAbilityType.BASE_ALTERNATE, SpellAbilityCastMode.NORMAL); - // Needed for Dream Devourer and Ethereal Valkyrie reducing the cost of a colorless CMC 2 or less spell to 0 - // CardUtil.reduceCost returns an empty string in that case so we add a cost of 0 here - // https://github.com/magefree/mage/issues/7607 - if (foretellCost != null && foretellCost.isEmpty()) { - foretellCost = "{0}"; - } - this.setAdditionalCostsRuleVisible(false); - this.name = "Foretell " + foretellCost; - this.addCost(new ManaCostsImpl<>(foretellCost)); - } - - protected ForetellCostAbility(final ForetellCostAbility ability) { - super(ability); - this.spellAbilityType = ability.spellAbilityType; - this.abilityName = ability.abilityName; - this.spellAbilityToResolve = ability.spellAbilityToResolve; - } - - @Override - public ActivationStatus canActivate(UUID playerId, Game game) { - if (super.canActivate(playerId, game).canActivate()) { - Card card = game.getCard(getSourceId()); - if (card != null) { - UUID mainCardId = card.getMainCard().getId(); - // Card must be in the exile zone - if (game.getState().getZone(mainCardId) != Zone.EXILED) { - return ActivationStatus.getFalse(); - } - Integer foretoldTurn = (Integer) game.getState().getValue(mainCardId.toString() + "Foretell Turn Number"); - UUID exileId = (UUID) game.getState().getValue(mainCardId.toString() + "foretellAbility"); - // Card must be Foretold - if (foretoldTurn == null || exileId == null) { - return ActivationStatus.getFalse(); - } - // Can't be cast if the turn it was Foretold is the same - if (foretoldTurn == game.getTurnNum()) { - return ActivationStatus.getFalse(); - } - // Check that the card is actually in the exile zone (ex: Oblivion Ring exiles it after it was Foretold, etc) - ExileZone exileZone = game.getState().getExile().getExileZone(exileId); - if (exileZone != null - && exileZone.isEmpty()) { - return ActivationStatus.getFalse(); - } - if (card instanceof SplitCard) { - if (((SplitCard) card).getLeftHalfCard().getName().equals(abilityName)) { - return ((SplitCard) card).getLeftHalfCard().getSpellAbility().canActivate(playerId, game); - } else if (((SplitCard) card).getRightHalfCard().getName().equals(abilityName)) { - return ((SplitCard) card).getRightHalfCard().getSpellAbility().canActivate(playerId, game); - } - } else if (card instanceof ModalDoubleFacedCard) { - if (((ModalDoubleFacedCard) card).getLeftHalfCard().getName().equals(abilityName)) { - return ((ModalDoubleFacedCard) card).getLeftHalfCard().getSpellAbility().canActivate(playerId, game); - } else if (((ModalDoubleFacedCard) card).getRightHalfCard().getName().equals(abilityName)) { - return ((ModalDoubleFacedCard) card).getRightHalfCard().getSpellAbility().canActivate(playerId, game); - } - } else if (card instanceof CardWithSpellOption) { - if (card.getMainCard().getName().equals(abilityName)) { - return card.getMainCard().getSpellAbility().canActivate(playerId, game); - } else if (((CardWithSpellOption) card).getSpellCard().getName().equals(abilityName)) { - return ((CardWithSpellOption) card).getSpellCard().getSpellAbility().canActivate(playerId, game); - } - } - return card.getSpellAbility().canActivate(playerId, game); - } - } - return ActivationStatus.getFalse(); - } - - @Override - public SpellAbility getSpellAbilityToResolve(Game game) { - Card card = game.getCard(getSourceId()); - if (card != null) { - if (spellAbilityToResolve == null) { - SpellAbility spellAbilityCopy = null; - if (card instanceof SplitCard) { - if (((SplitCard) card).getLeftHalfCard().getName().equals(abilityName)) { - spellAbilityCopy = ((SplitCard) card).getLeftHalfCard().getSpellAbility().copy(); - } else if (((SplitCard) card).getRightHalfCard().getName().equals(abilityName)) { - spellAbilityCopy = ((SplitCard) card).getRightHalfCard().getSpellAbility().copy(); - } - } else if (card instanceof ModalDoubleFacedCard) { - if (((ModalDoubleFacedCard) card).getLeftHalfCard().getName().equals(abilityName)) { - spellAbilityCopy = ((ModalDoubleFacedCard) card).getLeftHalfCard().getSpellAbility().copy(); - } else if (((ModalDoubleFacedCard) card).getRightHalfCard().getName().equals(abilityName)) { - spellAbilityCopy = ((ModalDoubleFacedCard) card).getRightHalfCard().getSpellAbility().copy(); - } - } else if (card instanceof CardWithSpellOption) { - if (card.getMainCard().getName().equals(abilityName)) { - spellAbilityCopy = card.getMainCard().getSpellAbility().copy(); - } else if (((CardWithSpellOption) card).getSpellCard().getName().equals(abilityName)) { - spellAbilityCopy = ((CardWithSpellOption) card).getSpellCard().getSpellAbility().copy(); - } - } else { - spellAbilityCopy = card.getSpellAbility().copy(); - } - if (spellAbilityCopy == null) { - return null; - } - spellAbilityCopy.setId(this.getId()); - spellAbilityCopy.clearManaCosts(); - spellAbilityCopy.clearManaCostsToPay(); - spellAbilityCopy.addCost(this.getCosts().copy()); - spellAbilityCopy.addCost(this.getManaCosts().copy()); - spellAbilityCopy.setSpellAbilityCastMode(this.getSpellAbilityCastMode()); - spellAbilityToResolve = spellAbilityCopy; - } - } - return spellAbilityToResolve; - } - - @Override - public Costs getCosts() { - if (spellAbilityToResolve == null) { - return super.getCosts(); - } - return spellAbilityToResolve.getCosts(); - } - - @Override - public ForetellCostAbility copy() { - return new ForetellCostAbility(this); - } - - @Override - public String getRule(boolean all) { - StringBuilder sbRule = new StringBuilder("Foretell"); - if (!getCosts().isEmpty()) { - sbRule.append("—"); - } else { - sbRule.append(' '); - } - if (!getManaCosts().isEmpty()) { - sbRule.append(getManaCosts().getText()); - } - if (!getCosts().isEmpty()) { - if (!getManaCosts().isEmpty()) { - sbRule.append(", "); - } - sbRule.append(getCosts().getText()); - sbRule.append('.'); - } - if (abilityName != null) { - sbRule.append(' '); - sbRule.append(abilityName); - } - sbRule.append(" (You may cast this card from exile for its foretell cost.)"); - return sbRule.toString(); - } - - /** - * Used for split card in PlayerImpl method: - * getOtherUseableActivatedAbilities - */ - public void setAbilityName(String abilityName) { - this.abilityName = abilityName; - } - - } - - public static boolean isCardInForetell(Card card, Game game) { - // searching ForetellCostAbility - it adds for foretelled cards only after exile - return card.getAbilities(game).containsClass(ForetellCostAbility.class); + @Override + public ForetellAddCostEffect copy() { + return new ForetellAddCostEffect(this); + } +} + +class ForetellCostAbility extends SpellAbility { + + private String abilityName; + private SpellAbility spellAbilityToResolve; + + ForetellCostAbility(String foretellCost) { + super(null, "Testing", Zone.EXILED, SpellAbilityType.BASE_ALTERNATE, SpellAbilityCastMode.NORMAL); + // Needed for Dream Devourer and Ethereal Valkyrie reducing the cost of a colorless CMC 2 or less spell to 0 + // CardUtil.reduceCost returns an empty string in that case so we add a cost of 0 here + // https://github.com/magefree/mage/issues/7607 + if (foretellCost != null && foretellCost.isEmpty()) { + foretellCost = "{0}"; + } + this.setAdditionalCostsRuleVisible(false); + this.name = "Foretell " + foretellCost; + this.addCost(new ManaCostsImpl<>(foretellCost)); + } + + private ForetellCostAbility(final ForetellCostAbility ability) { + super(ability); + this.spellAbilityType = ability.spellAbilityType; + this.abilityName = ability.abilityName; + this.spellAbilityToResolve = ability.spellAbilityToResolve; + } + + @Override + public ActivationStatus canActivate(UUID playerId, Game game) { + if (super.canActivate(playerId, game).canActivate()) { + Card card = game.getCard(getSourceId()); + if (card != null) { + UUID mainCardId = card.getMainCard().getId(); + // Card must be in the exile zone + if (game.getState().getZone(mainCardId) != Zone.EXILED) { + return ActivationStatus.getFalse(); + } + Integer foretoldTurn = (Integer) game.getState().getValue(mainCardId.toString() + "Foretell Turn Number"); + UUID exileId = (UUID) game.getState().getValue(mainCardId.toString() + "foretellAbility"); + // Card must be Foretold + if (foretoldTurn == null || exileId == null) { + return ActivationStatus.getFalse(); + } + // Can't be cast if the turn it was Foretold is the same + if (foretoldTurn == game.getTurnNum()) { + return ActivationStatus.getFalse(); + } + // Check that the card is actually in the exile zone (ex: Oblivion Ring exiles it after it was Foretold, etc) + ExileZone exileZone = game.getState().getExile().getExileZone(exileId); + if (exileZone != null + && exileZone.isEmpty()) { + return ActivationStatus.getFalse(); + } + if (card instanceof SplitCard) { + if (((SplitCard) card).getLeftHalfCard().getName().equals(abilityName)) { + return ((SplitCard) card).getLeftHalfCard().getSpellAbility().canActivate(playerId, game); + } else if (((SplitCard) card).getRightHalfCard().getName().equals(abilityName)) { + return ((SplitCard) card).getRightHalfCard().getSpellAbility().canActivate(playerId, game); + } + } else if (card instanceof ModalDoubleFacedCard) { + if (((ModalDoubleFacedCard) card).getLeftHalfCard().getName().equals(abilityName)) { + return ((ModalDoubleFacedCard) card).getLeftHalfCard().getSpellAbility().canActivate(playerId, game); + } else if (((ModalDoubleFacedCard) card).getRightHalfCard().getName().equals(abilityName)) { + return ((ModalDoubleFacedCard) card).getRightHalfCard().getSpellAbility().canActivate(playerId, game); + } + } else if (card instanceof CardWithSpellOption) { + if (card.getMainCard().getName().equals(abilityName)) { + return card.getMainCard().getSpellAbility().canActivate(playerId, game); + } else if (((CardWithSpellOption) card).getSpellCard().getName().equals(abilityName)) { + return ((CardWithSpellOption) card).getSpellCard().getSpellAbility().canActivate(playerId, game); + } + } + return card.getSpellAbility().canActivate(playerId, game); + } + } + return ActivationStatus.getFalse(); + } + + @Override + public SpellAbility getSpellAbilityToResolve(Game game) { + Card card = game.getCard(getSourceId()); + if (card != null) { + if (spellAbilityToResolve == null) { + SpellAbility spellAbilityCopy = null; + if (card instanceof SplitCard) { + if (((SplitCard) card).getLeftHalfCard().getName().equals(abilityName)) { + spellAbilityCopy = ((SplitCard) card).getLeftHalfCard().getSpellAbility().copy(); + } else if (((SplitCard) card).getRightHalfCard().getName().equals(abilityName)) { + spellAbilityCopy = ((SplitCard) card).getRightHalfCard().getSpellAbility().copy(); + } + } else if (card instanceof ModalDoubleFacedCard) { + if (((ModalDoubleFacedCard) card).getLeftHalfCard().getName().equals(abilityName)) { + spellAbilityCopy = ((ModalDoubleFacedCard) card).getLeftHalfCard().getSpellAbility().copy(); + } else if (((ModalDoubleFacedCard) card).getRightHalfCard().getName().equals(abilityName)) { + spellAbilityCopy = ((ModalDoubleFacedCard) card).getRightHalfCard().getSpellAbility().copy(); + } + } else if (card instanceof CardWithSpellOption) { + if (card.getMainCard().getName().equals(abilityName)) { + spellAbilityCopy = card.getMainCard().getSpellAbility().copy(); + } else if (((CardWithSpellOption) card).getSpellCard().getName().equals(abilityName)) { + spellAbilityCopy = ((CardWithSpellOption) card).getSpellCard().getSpellAbility().copy(); + } + } else { + spellAbilityCopy = card.getSpellAbility().copy(); + } + if (spellAbilityCopy == null) { + return null; + } + spellAbilityCopy.setId(this.getId()); + spellAbilityCopy.clearManaCosts(); + spellAbilityCopy.clearManaCostsToPay(); + spellAbilityCopy.addCost(this.getCosts().copy()); + spellAbilityCopy.addCost(this.getManaCosts().copy()); + spellAbilityCopy.setSpellAbilityCastMode(this.getSpellAbilityCastMode()); + spellAbilityToResolve = spellAbilityCopy; + } + } + return spellAbilityToResolve; + } + + @Override + public Costs getCosts() { + if (spellAbilityToResolve == null) { + return super.getCosts(); + } + return spellAbilityToResolve.getCosts(); + } + + @Override + public ForetellCostAbility copy() { + return new ForetellCostAbility(this); + } + + @Override + public String getRule(boolean all) { + StringBuilder sbRule = new StringBuilder("Foretell"); + if (!getCosts().isEmpty()) { + sbRule.append("—"); + } else { + sbRule.append(' '); + } + if (!getManaCosts().isEmpty()) { + sbRule.append(getManaCosts().getText()); + } + if (!getCosts().isEmpty()) { + if (!getManaCosts().isEmpty()) { + sbRule.append(", "); + } + sbRule.append(getCosts().getText()); + sbRule.append('.'); + } + if (abilityName != null) { + sbRule.append(' '); + sbRule.append(abilityName); + } + sbRule.append(" (You may cast this card from exile for its foretell cost.)"); + return sbRule.toString(); + } + + /** + * Used for split card in PlayerImpl method: + * getOtherUseableActivatedAbilities + */ + void setAbilityName(String abilityName) { + this.abilityName = abilityName; + } + +} + +class ForetellAddAbilityEffect extends ContinuousEffectImpl { + + private static final FilterNonlandCard filter = new FilterNonlandCard(); + + static { + filter.add(Predicates.not(new AbilityPredicate(ForetellAbility.class))); + } + + ForetellAddAbilityEffect() { + super(Duration.WhileOnBattlefield, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility); + this.staticText = "Each nonland card in your hand without foretell has foretell. Its foretell cost is equal to its mana cost reduced by {2}"; + } + + private ForetellAddAbilityEffect(final ForetellAddAbilityEffect effect) { + super(effect); + } + + @Override + public ForetellAddAbilityEffect copy() { + return new ForetellAddAbilityEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller == null) { + return false; + } + for (Card card : controller.getHand().getCards(filter, game)) { + ForetellAbility foretellAbility = null; + if (card instanceof SplitCard) { + String leftHalfCost = CardUtil.reduceCost(((SplitCard) card).getLeftHalfCard().getManaCost(), 2).getText(); + String rightHalfCost = CardUtil.reduceCost(((SplitCard) card).getRightHalfCard().getManaCost(), 2).getText(); + foretellAbility = new ForetellAbility(card, leftHalfCost, rightHalfCost); + } else if (card instanceof ModalDoubleFacedCard) { + ModalDoubleFacedCardHalf leftHalfCard = ((ModalDoubleFacedCard) card).getLeftHalfCard(); + // If front side of MDFC is land, do nothing as Dream Devourer does not apply to lands + // MDFC cards in hand are considered lands if front side is land + if (!leftHalfCard.isLand(game)) { + String leftHalfCost = CardUtil.reduceCost(leftHalfCard.getManaCost(), 2).getText(); + ModalDoubleFacedCardHalf rightHalfCard = ((ModalDoubleFacedCard) card).getRightHalfCard(); + if (rightHalfCard.isLand(game)) { + foretellAbility = new ForetellAbility(card, leftHalfCost); + } else { + String rightHalfCost = CardUtil.reduceCost(rightHalfCard.getManaCost(), 2).getText(); + foretellAbility = new ForetellAbility(card, leftHalfCost, rightHalfCost); + } + } + } else if (card instanceof CardWithSpellOption) { + String creatureCost = CardUtil.reduceCost(card.getMainCard().getManaCost(), 2).getText(); + String spellCost = CardUtil.reduceCost(((CardWithSpellOption) card).getSpellCard().getManaCost(), 2).getText(); + foretellAbility = new ForetellAbility(card, creatureCost, spellCost); + } else { + String costText = CardUtil.reduceCost(card.getManaCost(), 2).getText(); + foretellAbility = new ForetellAbility(card, costText); + } + if (foretellAbility != null) { + foretellAbility.setSourceId(card.getId()); + foretellAbility.setControllerId(card.getOwnerId()); + game.getState().addOtherAbility(card, foretellAbility); + } + } + return true; } } diff --git a/Mage/src/main/java/mage/game/events/GameEvent.java b/Mage/src/main/java/mage/game/events/GameEvent.java index 632c2ede065..21ad27b5d95 100644 --- a/Mage/src/main/java/mage/game/events/GameEvent.java +++ b/Mage/src/main/java/mage/game/events/GameEvent.java @@ -615,8 +615,12 @@ public class GameEvent implements Serializable { DUNGEON_COMPLETED, TEMPTED_BY_RING, RING_BEARER_CHOSEN, REMOVED_FROM_COMBAT, // targetId id of permanent removed from combat - FORETOLD, // targetId id of card foretold - FORETELL, // targetId id of card foretell playerId id of the controller + /* card foretold + targetId id of card foretold + playerId id of player foretelling card + flag true if player did foretell, false if became foretold without foretell + */ + CARD_FORETOLD, /* villainous choice targetId player making the choice sourceId sourceId of the ability forcing the choice diff --git a/Mage/src/main/java/mage/watchers/common/ForetoldWatcher.java b/Mage/src/main/java/mage/watchers/common/ForetoldWatcher.java index 3213e8b76de..b37b0edf9f3 100644 --- a/Mage/src/main/java/mage/watchers/common/ForetoldWatcher.java +++ b/Mage/src/main/java/mage/watchers/common/ForetoldWatcher.java @@ -1,12 +1,13 @@ package mage.watchers.common; -import java.util.HashSet; -import java.util.Set; -import java.util.UUID; +import java.util.*; + +import mage.MageObjectReference; import mage.cards.Card; import mage.constants.WatcherScope; import mage.game.Game; import mage.game.events.GameEvent; +import mage.players.Player; import mage.util.CardUtil; import mage.watchers.Watcher; @@ -16,9 +17,12 @@ import mage.watchers.Watcher; */ public class ForetoldWatcher extends Watcher { - // If foretell was activated or a card was Foretold by the controller this turn, this list stores it. Cleared at the end of the turn. - private final Set foretellCardsThisTurn = new HashSet<>(); - private final Set foretoldCards = new HashSet<>(); + private final Set foretoldCards = new HashSet<>(); + // cards foretold - ZCC stored to reference from stack (exile zone plus 1) + + private final Map playerForetellCount = new HashMap<>(); + // map of player id to number of times they foretell a card, cleared each turn + public ForetoldWatcher() { super(WatcherScope.GAME); @@ -26,35 +30,32 @@ public class ForetoldWatcher extends Watcher { @Override public void watch(GameEvent event, Game game) { - if (event.getType() == GameEvent.EventType.FORETELL) { - Card card = game.getCard(event.getTargetId()); - if (card != null - && controllerId == event.getPlayerId()) { - foretellCardsThisTurn.add(card.getId()); - foretoldCards.add(card.getId()); - } + if (event.getType() != GameEvent.EventType.CARD_FORETOLD) { + return; } - // Ethereal Valkyrie - if (event.getType() == GameEvent.EventType.FORETOLD) { - Card card = game.getCard(event.getTargetId()); - if (card != null) { - // Ethereal Valkyrie does not Foretell the card, it becomes Foretold, so don't add it to the Foretell list - foretoldCards.add(card.getId()); + Card card = game.getCard(event.getTargetId()); + if (card != null) { + foretoldCards.add(new MageObjectReference(card, game, 1)); + } + if (event.getFlag()) { + Player player = game.getPlayer(event.getPlayerId()); + if (player != null) { + playerForetellCount.compute(player.getId(), CardUtil::setOrIncrementValue); } } } - public boolean cardWasForetold(UUID sourceId) { - return foretoldCards.contains(sourceId); + public boolean checkForetold(UUID sourceId, Game game) { + return foretoldCards.contains(new MageObjectReference(sourceId, game)); } - public int countNumberForetellThisTurn() { - return foretellCardsThisTurn.size(); + public int getPlayerForetellCountThisTurn(UUID playerId) { + return playerForetellCount.getOrDefault(playerId, 0); } @Override public void reset() { super.reset(); - foretellCardsThisTurn.clear(); + playerForetellCount.clear(); } }