Fix bugs associated with Foretell ability (#13879)

* add foretell tests

* rework foretell events and watcher

* refactor: not static inner classes

* refactor: move becomes foretold code from Ethereal Valkyrie to ForetellAbility

* add watcher for edge cases

* fix Ethereal Valkyrie to not leak face down card name in log

* fix some access modifiers

* refactor: make copy-pasted code common
This commit is contained in:
xenohedron 2025-07-27 00:26:30 -04:00 committed by GitHub
parent 4a74353b0c
commit e8cd6dbdad
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 784 additions and 601 deletions

View file

@ -4,18 +4,14 @@ import mage.MageInt;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.common.CastSecondSpellTriggeredAbility; import mage.abilities.common.CastSecondSpellTriggeredAbility;
import mage.abilities.common.SimpleStaticAbility; import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.ContinuousEffectImpl;
import mage.abilities.effects.common.combat.GoadTargetEffect; import mage.abilities.effects.common.combat.GoadTargetEffect;
import mage.abilities.keyword.ForetellAbility; import mage.abilities.keyword.ForetellAbility;
import mage.cards.*; import mage.cards.CardImpl;
import mage.constants.*; import mage.cards.CardSetInfo;
import mage.filter.common.FilterNonlandCard; import mage.constants.CardType;
import mage.filter.predicate.Predicates; import mage.constants.SubType;
import mage.filter.predicate.mageobject.AbilityPredicate; import mage.constants.SuperType;
import mage.game.Game;
import mage.players.Player;
import mage.target.common.TargetOpponentsCreaturePermanent; import mage.target.common.TargetOpponentsCreaturePermanent;
import mage.util.CardUtil;
import java.util.UUID; import java.util.UUID;
@ -34,7 +30,7 @@ public final class BohnBeguilingBalladeer extends CardImpl {
this.toughness = new MageInt(3); 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}. // 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. // Whenever you cast your second spell each turn, goad target creature an opponent controls.
Ability ability = new CastSecondSpellTriggeredAbility(new GoadTargetEffect()); Ability ability = new CastSecondSpellTriggeredAbility(new GoadTargetEffect());
@ -51,69 +47,3 @@ public final class BohnBeguilingBalladeer extends CardImpl {
return new BohnBeguilingBalladeer(this); 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;
}
}

View file

@ -1,27 +1,17 @@
package mage.cards.d; package mage.cards.d;
import java.util.UUID;
import mage.MageInt; import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.ForetellSourceControllerTriggeredAbility; import mage.abilities.common.ForetellSourceControllerTriggeredAbility;
import mage.abilities.common.SimpleStaticAbility; import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.ContinuousEffectImpl;
import mage.abilities.effects.common.continuous.BoostSourceEffect; import mage.abilities.effects.common.continuous.BoostSourceEffect;
import mage.abilities.keyword.ForetellAbility; import mage.abilities.keyword.ForetellAbility;
import mage.cards.*; import mage.cards.CardImpl;
import mage.constants.SubType; import mage.cards.CardSetInfo;
import mage.constants.CardType; import mage.constants.CardType;
import mage.constants.Duration; import mage.constants.Duration;
import mage.constants.Layer; import mage.constants.SubType;
import mage.constants.Outcome;
import mage.constants.SubLayer; import java.util.UUID;
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;
/** /**
* *
@ -38,7 +28,7 @@ public final class DreamDevourer extends CardImpl {
this.toughness = new MageInt(3); 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. // 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. // Whenever you foretell a card, Dream Devourer gets +2/+0 until end of turn.
this.addAbility(new ForetellSourceControllerTriggeredAbility(new BoostSourceEffect(2, 0, Duration.EndOfTurn))); this.addAbility(new ForetellSourceControllerTriggeredAbility(new BoostSourceEffect(2, 0, Duration.EndOfTurn)));
@ -54,69 +44,3 @@ public final class DreamDevourer extends CardImpl {
return new DreamDevourer(this); 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;
}
}

View file

@ -1,23 +1,22 @@
package mage.cards.e; package mage.cards.e;
import mage.MageInt; import mage.MageInt;
import mage.MageObjectReference;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.common.EntersBattlefieldOrAttacksSourceTriggeredAbility; import mage.abilities.common.EntersBattlefieldOrAttacksSourceTriggeredAbility;
import mage.abilities.effects.ContinuousEffect;
import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.OneShotEffect;
import mage.abilities.keyword.FlyingAbility; import mage.abilities.keyword.FlyingAbility;
import mage.abilities.keyword.ForetellAbility; 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.CardType;
import mage.constants.Outcome; import mage.constants.Outcome;
import mage.constants.SubType; import mage.constants.SubType;
import mage.filter.FilterCard; import mage.filter.StaticFilters;
import mage.game.Game; import mage.game.Game;
import mage.game.events.GameEvent;
import mage.players.Player; import mage.players.Player;
import mage.target.common.TargetCardInHand; import mage.target.common.TargetCardInHand;
import mage.util.CardUtil; import mage.watchers.common.ForetoldWatcher;
import java.util.UUID; import java.util.UUID;
@ -38,7 +37,7 @@ public final class EtherealValkyrie extends CardImpl {
this.addAbility(FlyingAbility.getInstance()); 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}. // 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) { private EtherealValkyrie(final EtherealValkyrie card) {
@ -75,77 +74,15 @@ class EtherealValkyrieEffect extends OneShotEffect {
if (controller == null) { if (controller == null) {
return false; return false;
} }
controller.drawCards(1, source, game); 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)) { if (!controller.chooseTarget(Outcome.Benefit, targetCard, source, game)) {
return false; return false;
} }
Card card = game.getCard(targetCard.getFirstTarget());
Card exileCard = game.getCard(targetCard.getFirstTarget()); if (card == null) {
if (exileCard == null) {
return false; return false;
} }
return ForetellAbility.doExileBecomesForetold(card, game, source, 2);
// 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;
} }
} }

View file

@ -89,7 +89,7 @@ class RanarTheEverWatchfulCostReductionEffect extends CostModificationEffectImpl
public boolean applies(Ability abilityToModify, Ability source, Game game) { public boolean applies(Ability abilityToModify, Ability source, Game game) {
ForetoldWatcher watcher = game.getState().getWatcher(ForetoldWatcher.class); ForetoldWatcher watcher = game.getState().getWatcher(ForetoldWatcher.class);
return (watcher != null return (watcher != null
&& watcher.countNumberForetellThisTurn() == 0 && watcher.getPlayerForetellCountThisTurn(source.getControllerId()) == 0
&& abilityToModify.isControlledBy(source.getControllerId()) && abilityToModify.isControlledBy(source.getControllerId())
&& abilityToModify instanceof ForetellAbility); && abilityToModify instanceof ForetellAbility);
} }

View file

@ -2,6 +2,7 @@ package org.mage.test.cards.abilities.keywords;
import mage.constants.PhaseStep; import mage.constants.PhaseStep;
import mage.constants.Zone; import mage.constants.Zone;
import mage.counters.CounterType;
import org.junit.Test; import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase; 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 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 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 players 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);
}
} }

View file

@ -7,6 +7,7 @@ import mage.constants.Zone;
import mage.game.Game; import mage.game.Game;
import mage.game.events.GameEvent; import mage.game.events.GameEvent;
import mage.players.Player; import mage.players.Player;
import mage.watchers.common.ForetoldWatcher;
/** /**
* @author jeffwadsworth * @author jeffwadsworth
@ -16,6 +17,7 @@ public class ForetellSourceControllerTriggeredAbility extends TriggeredAbilityIm
public ForetellSourceControllerTriggeredAbility(Effect effect) { public ForetellSourceControllerTriggeredAbility(Effect effect) {
super(Zone.BATTLEFIELD, effect, false); super(Zone.BATTLEFIELD, effect, false);
setTriggerPhrase("Whenever you foretell a card, "); setTriggerPhrase("Whenever you foretell a card, ");
addWatcher(new ForetoldWatcher());
} }
protected ForetellSourceControllerTriggeredAbility(final ForetellSourceControllerTriggeredAbility ability) { protected ForetellSourceControllerTriggeredAbility(final ForetellSourceControllerTriggeredAbility ability) {
@ -24,16 +26,14 @@ public class ForetellSourceControllerTriggeredAbility extends TriggeredAbilityIm
@Override @Override
public boolean checkEventType(GameEvent event, Game game) { public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.FORETELL; return event.getType() == GameEvent.EventType.CARD_FORETOLD;
} }
@Override @Override
public boolean checkTrigger(GameEvent event, Game game) { public boolean checkTrigger(GameEvent event, Game game) {
Card card = game.getCard(event.getTargetId()); Card card = game.getCard(event.getTargetId());
Player player = game.getPlayer(event.getPlayerId()); Player player = game.getPlayer(event.getPlayerId());
return (card != null return event.getFlag() && card != null && player != null && isControlledBy(player.getId());
&& player != null
&& isControlledBy(player.getId()));
} }
@Override @Override

View file

@ -17,7 +17,7 @@ public enum ForetoldCondition implements Condition {
public boolean apply(Game game, Ability source) { public boolean apply(Game game, Ability source) {
ForetoldWatcher watcher = game.getState().getWatcher(ForetoldWatcher.class); ForetoldWatcher watcher = game.getState().getWatcher(ForetoldWatcher.class);
if (watcher != null) { if (watcher != null) {
return watcher.cardWasForetold(source.getSourceId()); return watcher.checkForetold(source.getSourceId(), game);
} }
return false; return false;
} }

View file

@ -11,11 +11,15 @@ import mage.abilities.costs.Costs;
import mage.abilities.costs.mana.GenericManaCost; import mage.abilities.costs.mana.GenericManaCost;
import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.AsThoughEffectImpl; import mage.abilities.effects.AsThoughEffectImpl;
import mage.abilities.effects.ContinuousEffect;
import mage.abilities.effects.ContinuousEffectImpl; import mage.abilities.effects.ContinuousEffectImpl;
import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.ExileTargetEffect; import mage.abilities.effects.common.ExileTargetEffect;
import mage.cards.*; import mage.cards.*;
import mage.constants.*; 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.ExileZone;
import mage.game.Game; import mage.game.Game;
import mage.game.events.GameEvent; import mage.game.events.GameEvent;
@ -90,7 +94,90 @@ public class ForetellAbility extends SpecialAction {
return " foretells a card from hand"; return " foretells a card from hand";
} }
static class ForetellExileEffect extends OneShotEffect { 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);
}
public static ContinuousEffect makeAddForetellEffect() {
return new ForetellAddAbilityEffect();
}
/**
* 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;
}
// 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);
}
}
} 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;
}
}
class ForetellExileEffect extends OneShotEffect {
private final Card card; private final Card card;
String foretellCost; String foretellCost;
@ -103,7 +190,7 @@ public class ForetellAbility extends SpecialAction {
this.foretellSplitCost = foretellSplitCost; this.foretellSplitCost = foretellSplitCost;
} }
protected ForetellExileEffect(final ForetellExileEffect effect) { private ForetellExileEffect(final ForetellExileEffect effect) {
super(effect); super(effect);
this.card = effect.card; this.card = effect.card;
this.foretellCost = effect.foretellCost; this.foretellCost = effect.foretellCost;
@ -143,20 +230,20 @@ public class ForetellAbility extends SpecialAction {
effect.apply(game, source); effect.apply(game, source);
card.setFaceDown(true, game); card.setFaceDown(true, game);
game.addEffect(new ForetellAddCostEffect(new MageObjectReference(card, game)), source); game.addEffect(new ForetellAddCostEffect(new MageObjectReference(card, game)), source);
game.fireEvent(GameEvent.getEvent(GameEvent.EventType.FORETELL, card.getId(), null, source.getControllerId())); game.fireEvent(new GameEvent(GameEvent.EventType.CARD_FORETOLD, card.getId(), source, source.getControllerId(), 0, true));
return true; return true;
} }
return false; return false;
} }
} }
static class ForetellLookAtCardEffect extends AsThoughEffectImpl { class ForetellLookAtCardEffect extends AsThoughEffectImpl {
ForetellLookAtCardEffect() { ForetellLookAtCardEffect() {
super(AsThoughEffectType.LOOK_AT_FACE_DOWN, Duration.EndOfGame, Outcome.AIDontUseIt); super(AsThoughEffectType.LOOK_AT_FACE_DOWN, Duration.EndOfGame, Outcome.AIDontUseIt);
} }
protected ForetellLookAtCardEffect(final ForetellLookAtCardEffect effect) { private ForetellLookAtCardEffect(final ForetellLookAtCardEffect effect) {
super(effect); super(effect);
} }
@ -188,19 +275,19 @@ public class ForetellAbility extends SpecialAction {
} }
return false; return false;
} }
} }
public static class ForetellAddCostEffect extends ContinuousEffectImpl { class ForetellAddCostEffect extends ContinuousEffectImpl {
private final MageObjectReference mor; private final MageObjectReference mor;
public ForetellAddCostEffect(MageObjectReference mor) { ForetellAddCostEffect(MageObjectReference mor) {
super(Duration.EndOfGame, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility); super(Duration.EndOfGame, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility);
this.mor = mor; this.mor = mor;
staticText = "Foretold card"; staticText = "Foretold card";
} }
protected ForetellAddCostEffect(final ForetellAddCostEffect effect) { private ForetellAddCostEffect(final ForetellAddCostEffect effect) {
super(effect); super(effect);
this.mor = effect.mor; this.mor = effect.mor;
} }
@ -295,9 +382,9 @@ public class ForetellAbility extends SpecialAction {
public ForetellAddCostEffect copy() { public ForetellAddCostEffect copy() {
return new ForetellAddCostEffect(this); return new ForetellAddCostEffect(this);
} }
} }
static class ForetellCostAbility extends SpellAbility { class ForetellCostAbility extends SpellAbility {
private String abilityName; private String abilityName;
private SpellAbility spellAbilityToResolve; private SpellAbility spellAbilityToResolve;
@ -315,7 +402,7 @@ public class ForetellAbility extends SpecialAction {
this.addCost(new ManaCostsImpl<>(foretellCost)); this.addCost(new ManaCostsImpl<>(foretellCost));
} }
protected ForetellCostAbility(final ForetellCostAbility ability) { private ForetellCostAbility(final ForetellCostAbility ability) {
super(ability); super(ability);
this.spellAbilityType = ability.spellAbilityType; this.spellAbilityType = ability.spellAbilityType;
this.abilityName = ability.abilityName; this.abilityName = ability.abilityName;
@ -458,14 +545,74 @@ public class ForetellAbility extends SpecialAction {
* Used for split card in PlayerImpl method: * Used for split card in PlayerImpl method:
* getOtherUseableActivatedAbilities * getOtherUseableActivatedAbilities
*/ */
public void setAbilityName(String abilityName) { void setAbilityName(String abilityName) {
this.abilityName = abilityName; this.abilityName = abilityName;
} }
}
class ForetellAddAbilityEffect extends ContinuousEffectImpl {
private static final FilterNonlandCard filter = new FilterNonlandCard();
static {
filter.add(Predicates.not(new AbilityPredicate(ForetellAbility.class)));
} }
public static boolean isCardInForetell(Card card, Game game) { ForetellAddAbilityEffect() {
// searching ForetellCostAbility - it adds for foretelled cards only after exile super(Duration.WhileOnBattlefield, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility);
return card.getAbilities(game).containsClass(ForetellCostAbility.class); 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;
} }
} }

View file

@ -615,8 +615,12 @@ public class GameEvent implements Serializable {
DUNGEON_COMPLETED, DUNGEON_COMPLETED,
TEMPTED_BY_RING, RING_BEARER_CHOSEN, TEMPTED_BY_RING, RING_BEARER_CHOSEN,
REMOVED_FROM_COMBAT, // targetId id of permanent removed from combat REMOVED_FROM_COMBAT, // targetId id of permanent removed from combat
FORETOLD, // targetId id of card foretold /* card foretold
FORETELL, // targetId id of card foretell playerId id of the controller 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 /* villainous choice
targetId player making the choice targetId player making the choice
sourceId sourceId of the ability forcing the choice sourceId sourceId of the ability forcing the choice

View file

@ -1,12 +1,13 @@
package mage.watchers.common; package mage.watchers.common;
import java.util.HashSet; import java.util.*;
import java.util.Set;
import java.util.UUID; import mage.MageObjectReference;
import mage.cards.Card; import mage.cards.Card;
import mage.constants.WatcherScope; import mage.constants.WatcherScope;
import mage.game.Game; import mage.game.Game;
import mage.game.events.GameEvent; import mage.game.events.GameEvent;
import mage.players.Player;
import mage.util.CardUtil; import mage.util.CardUtil;
import mage.watchers.Watcher; import mage.watchers.Watcher;
@ -16,9 +17,12 @@ import mage.watchers.Watcher;
*/ */
public class ForetoldWatcher extends 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<MageObjectReference> foretoldCards = new HashSet<>();
private final Set<UUID> foretellCardsThisTurn = new HashSet<>(); // cards foretold - ZCC stored to reference from stack (exile zone plus 1)
private final Set<UUID> foretoldCards = new HashSet<>();
private final Map<UUID, Integer> playerForetellCount = new HashMap<>();
// map of player id to number of times they foretell a card, cleared each turn
public ForetoldWatcher() { public ForetoldWatcher() {
super(WatcherScope.GAME); super(WatcherScope.GAME);
@ -26,35 +30,32 @@ public class ForetoldWatcher extends Watcher {
@Override @Override
public void watch(GameEvent event, Game game) { public void watch(GameEvent event, Game game) {
if (event.getType() == GameEvent.EventType.FORETELL) { if (event.getType() != GameEvent.EventType.CARD_FORETOLD) {
Card card = game.getCard(event.getTargetId()); return;
if (card != null
&& controllerId == event.getPlayerId()) {
foretellCardsThisTurn.add(card.getId());
foretoldCards.add(card.getId());
} }
}
// Ethereal Valkyrie
if (event.getType() == GameEvent.EventType.FORETOLD) {
Card card = game.getCard(event.getTargetId()); Card card = game.getCard(event.getTargetId());
if (card != null) { if (card != null) {
// Ethereal Valkyrie does not Foretell the card, it becomes Foretold, so don't add it to the Foretell list foretoldCards.add(new MageObjectReference(card, game, 1));
foretoldCards.add(card.getId()); }
if (event.getFlag()) {
Player player = game.getPlayer(event.getPlayerId());
if (player != null) {
playerForetellCount.compute(player.getId(), CardUtil::setOrIncrementValue);
} }
} }
} }
public boolean cardWasForetold(UUID sourceId) { public boolean checkForetold(UUID sourceId, Game game) {
return foretoldCards.contains(sourceId); return foretoldCards.contains(new MageObjectReference(sourceId, game));
} }
public int countNumberForetellThisTurn() { public int getPlayerForetellCountThisTurn(UUID playerId) {
return foretellCardsThisTurn.size(); return playerForetellCount.getOrDefault(playerId, 0);
} }
@Override @Override
public void reset() { public void reset() {
super.reset(); super.reset();
foretellCardsThisTurn.clear(); playerForetellCount.clear();
} }
} }