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,132 +94,236 @@ 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
private final Card card; return card.getAbilities(game).containsClass(ForetellCostAbility.class);
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;
}
} }
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) { // process Split, MDFC, and Adventure cards first
super(effect); // 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) {
@Override String leftHalfCost = CardUtil.reduceCost(((SplitCard) card).getLeftHalfCard().getManaCost(), amountToReduceCost).getText();
public boolean apply(Game game, Ability source) { String rightHalfCost = CardUtil.reduceCost(((SplitCard) card).getRightHalfCard().getManaCost(), amountToReduceCost).getText();
return true; 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);
@Override } else if (card instanceof ModalDoubleFacedCard) {
public ForetellLookAtCardEffect copy() { ModalDoubleFacedCardHalf leftHalfCard = ((ModalDoubleFacedCard) card).getLeftHalfCard();
return new ForetellLookAtCardEffect(this); 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);
@Override ModalDoubleFacedCardHalf rightHalfCard = ((ModalDoubleFacedCard) card).getRightHalfCard();
public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) { if (rightHalfCard.isLand(game)) {
if (affectedControllerId.equals(source.getControllerId())) { foretellAbility = new ForetellAbility(card, leftHalfCost);
Card card = game.getCard(objectId); } else {
if (card != null) { String rightHalfCost = CardUtil.reduceCost(rightHalfCard.getManaCost(), amountToReduceCost).getText();
MageObject sourceObject = game.getObject(source); game.getState().setValue(card.getMainCard().getId().toString() + "Foretell Split Cost", rightHalfCost);
if (sourceObject == null) { foretellAbility = new ForetellAbility(card, leftHalfCost, rightHalfCost);
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);
} }
} }
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) { private final Card card;
super(Duration.EndOfGame, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility); String foretellCost;
this.mor = mor; String foretellSplitCost;
staticText = "Foretold card";
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) { class ForetellLookAtCardEffect extends AsThoughEffectImpl {
super(effect);
this.mor = effect.mor;
}
@Override ForetellLookAtCardEffect() {
public boolean apply(Game game, Ability source) { super(AsThoughEffectType.LOOK_AT_FACE_DOWN, Duration.EndOfGame, Outcome.AIDontUseIt);
Card card = mor.getCard(game); }
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) { if (card != null) {
MageObject sourceObject = game.getObject(source);
if (sourceObject == null) {
return false;
}
UUID mainCardId = card.getMainCard().getId(); UUID mainCardId = card.getMainCard().getId();
if (game.getState().getZone(mainCardId) == Zone.EXILED) { UUID exileId = CardUtil.getExileZoneId(mainCardId.toString() + "foretellAbility", game);
String foretellCost = (String) game.getState().getValue(mainCardId.toString() + "Foretell Cost"); ExileZone exile = game.getExile().getExileZone(exileId);
String foretellSplitCost = (String) game.getState().getValue(mainCardId.toString() + "Foretell Split Cost"); return exile != null
if (card instanceof SplitCard) { && exile.contains(mainCardId);
if (foretellCost != null) { }
SplitCardHalf leftHalfCard = ((SplitCard) card).getLeftHalfCard(); }
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); ForetellCostAbility ability = new ForetellCostAbility(foretellCost);
ability.setSourceId(leftHalfCard.getId()); ability.setSourceId(leftHalfCard.getId());
ability.setControllerId(source.getControllerId()); ability.setControllerId(source.getControllerId());
@ -223,8 +331,11 @@ public class ForetellAbility extends SpecialAction {
ability.setAbilityName(leftHalfCard.getName()); ability.setAbilityName(leftHalfCard.getName());
game.getState().addOtherAbility(leftHalfCard, ability); 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); ForetellCostAbility ability = new ForetellCostAbility(foretellSplitCost);
ability.setSourceId(rightHalfCard.getId()); ability.setSourceId(rightHalfCard.getId());
ability.setControllerId(source.getControllerId()); ability.setControllerId(source.getControllerId());
@ -232,240 +343,276 @@ public class ForetellAbility extends SpecialAction {
ability.setAbilityName(rightHalfCard.getName()); ability.setAbilityName(rightHalfCard.getName());
game.getState().addOtherAbility(rightHalfCard, ability); game.getState().addOtherAbility(rightHalfCard, ability);
} }
} else if (card instanceof ModalDoubleFacedCard) { }
if (foretellCost != null) { } else if (card instanceof CardWithSpellOption) {
ModalDoubleFacedCardHalf leftHalfCard = ((ModalDoubleFacedCard) card).getLeftHalfCard(); if (foretellCost != null) {
// some MDFC's are land IE: sea gate restoration Card creatureCard = card.getMainCard();
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) {
ForetellCostAbility ability = new ForetellCostAbility(foretellCost); ForetellCostAbility ability = new ForetellCostAbility(foretellCost);
ability.setSourceId(card.getId()); ability.setSourceId(creatureCard.getId());
ability.setControllerId(source.getControllerId()); ability.setControllerId(source.getControllerId());
ability.setSpellAbilityType(card.getSpellAbility().getSpellAbilityType()); ability.setSpellAbilityType(creatureCard.getSpellAbility().getSpellAbilityType());
ability.setAbilityName(card.getName()); ability.setAbilityName(creatureCard.getName());
game.getState().addOtherAbility(card, ability); 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 { @Override
public ForetellAddCostEffect copy() {
private String abilityName; return new ForetellAddCostEffect(this);
private SpellAbility spellAbilityToResolve; }
}
ForetellCostAbility(String foretellCost) {
super(null, "Testing", Zone.EXILED, SpellAbilityType.BASE_ALTERNATE, SpellAbilityCastMode.NORMAL); class ForetellCostAbility extends SpellAbility {
// 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 private String abilityName;
// https://github.com/magefree/mage/issues/7607 private SpellAbility spellAbilityToResolve;
if (foretellCost != null && foretellCost.isEmpty()) {
foretellCost = "{0}"; ForetellCostAbility(String foretellCost) {
} super(null, "Testing", Zone.EXILED, SpellAbilityType.BASE_ALTERNATE, SpellAbilityCastMode.NORMAL);
this.setAdditionalCostsRuleVisible(false); // Needed for Dream Devourer and Ethereal Valkyrie reducing the cost of a colorless CMC 2 or less spell to 0
this.name = "Foretell " + foretellCost; // CardUtil.reduceCost returns an empty string in that case so we add a cost of 0 here
this.addCost(new ManaCostsImpl<>(foretellCost)); // https://github.com/magefree/mage/issues/7607
} if (foretellCost != null && foretellCost.isEmpty()) {
foretellCost = "{0}";
protected ForetellCostAbility(final ForetellCostAbility ability) { }
super(ability); this.setAdditionalCostsRuleVisible(false);
this.spellAbilityType = ability.spellAbilityType; this.name = "Foretell " + foretellCost;
this.abilityName = ability.abilityName; this.addCost(new ManaCostsImpl<>(foretellCost));
this.spellAbilityToResolve = ability.spellAbilityToResolve; }
}
private ForetellCostAbility(final ForetellCostAbility ability) {
@Override super(ability);
public ActivationStatus canActivate(UUID playerId, Game game) { this.spellAbilityType = ability.spellAbilityType;
if (super.canActivate(playerId, game).canActivate()) { this.abilityName = ability.abilityName;
Card card = game.getCard(getSourceId()); this.spellAbilityToResolve = ability.spellAbilityToResolve;
if (card != null) { }
UUID mainCardId = card.getMainCard().getId();
// Card must be in the exile zone @Override
if (game.getState().getZone(mainCardId) != Zone.EXILED) { public ActivationStatus canActivate(UUID playerId, Game game) {
return ActivationStatus.getFalse(); if (super.canActivate(playerId, game).canActivate()) {
} Card card = game.getCard(getSourceId());
Integer foretoldTurn = (Integer) game.getState().getValue(mainCardId.toString() + "Foretell Turn Number"); if (card != null) {
UUID exileId = (UUID) game.getState().getValue(mainCardId.toString() + "foretellAbility"); UUID mainCardId = card.getMainCard().getId();
// Card must be Foretold // Card must be in the exile zone
if (foretoldTurn == null || exileId == null) { if (game.getState().getZone(mainCardId) != Zone.EXILED) {
return ActivationStatus.getFalse(); return ActivationStatus.getFalse();
} }
// Can't be cast if the turn it was Foretold is the same Integer foretoldTurn = (Integer) game.getState().getValue(mainCardId.toString() + "Foretell Turn Number");
if (foretoldTurn == game.getTurnNum()) { UUID exileId = (UUID) game.getState().getValue(mainCardId.toString() + "foretellAbility");
return ActivationStatus.getFalse(); // Card must be Foretold
} if (foretoldTurn == null || exileId == null) {
// Check that the card is actually in the exile zone (ex: Oblivion Ring exiles it after it was Foretold, etc) return ActivationStatus.getFalse();
ExileZone exileZone = game.getState().getExile().getExileZone(exileId); }
if (exileZone != null // Can't be cast if the turn it was Foretold is the same
&& exileZone.isEmpty()) { if (foretoldTurn == game.getTurnNum()) {
return ActivationStatus.getFalse(); return ActivationStatus.getFalse();
} }
if (card instanceof SplitCard) { // Check that the card is actually in the exile zone (ex: Oblivion Ring exiles it after it was Foretold, etc)
if (((SplitCard) card).getLeftHalfCard().getName().equals(abilityName)) { ExileZone exileZone = game.getState().getExile().getExileZone(exileId);
return ((SplitCard) card).getLeftHalfCard().getSpellAbility().canActivate(playerId, game); if (exileZone != null
} else if (((SplitCard) card).getRightHalfCard().getName().equals(abilityName)) { && exileZone.isEmpty()) {
return ((SplitCard) card).getRightHalfCard().getSpellAbility().canActivate(playerId, game); return ActivationStatus.getFalse();
} }
} else if (card instanceof ModalDoubleFacedCard) { if (card instanceof SplitCard) {
if (((ModalDoubleFacedCard) card).getLeftHalfCard().getName().equals(abilityName)) { if (((SplitCard) card).getLeftHalfCard().getName().equals(abilityName)) {
return ((ModalDoubleFacedCard) card).getLeftHalfCard().getSpellAbility().canActivate(playerId, game); return ((SplitCard) card).getLeftHalfCard().getSpellAbility().canActivate(playerId, game);
} else if (((ModalDoubleFacedCard) card).getRightHalfCard().getName().equals(abilityName)) { } else if (((SplitCard) card).getRightHalfCard().getName().equals(abilityName)) {
return ((ModalDoubleFacedCard) card).getRightHalfCard().getSpellAbility().canActivate(playerId, game); return ((SplitCard) card).getRightHalfCard().getSpellAbility().canActivate(playerId, game);
} }
} else if (card instanceof CardWithSpellOption) { } else if (card instanceof ModalDoubleFacedCard) {
if (card.getMainCard().getName().equals(abilityName)) { if (((ModalDoubleFacedCard) card).getLeftHalfCard().getName().equals(abilityName)) {
return card.getMainCard().getSpellAbility().canActivate(playerId, game); return ((ModalDoubleFacedCard) card).getLeftHalfCard().getSpellAbility().canActivate(playerId, game);
} else if (((CardWithSpellOption) card).getSpellCard().getName().equals(abilityName)) { } else if (((ModalDoubleFacedCard) card).getRightHalfCard().getName().equals(abilityName)) {
return ((CardWithSpellOption) card).getSpellCard().getSpellAbility().canActivate(playerId, game); return ((ModalDoubleFacedCard) card).getRightHalfCard().getSpellAbility().canActivate(playerId, game);
} }
} } else if (card instanceof CardWithSpellOption) {
return card.getSpellAbility().canActivate(playerId, game); if (card.getMainCard().getName().equals(abilityName)) {
} return card.getMainCard().getSpellAbility().canActivate(playerId, game);
} } else if (((CardWithSpellOption) card).getSpellCard().getName().equals(abilityName)) {
return ActivationStatus.getFalse(); return ((CardWithSpellOption) card).getSpellCard().getSpellAbility().canActivate(playerId, game);
} }
}
@Override return card.getSpellAbility().canActivate(playerId, game);
public SpellAbility getSpellAbilityToResolve(Game game) { }
Card card = game.getCard(getSourceId()); }
if (card != null) { return ActivationStatus.getFalse();
if (spellAbilityToResolve == null) { }
SpellAbility spellAbilityCopy = null;
if (card instanceof SplitCard) { @Override
if (((SplitCard) card).getLeftHalfCard().getName().equals(abilityName)) { public SpellAbility getSpellAbilityToResolve(Game game) {
spellAbilityCopy = ((SplitCard) card).getLeftHalfCard().getSpellAbility().copy(); Card card = game.getCard(getSourceId());
} else if (((SplitCard) card).getRightHalfCard().getName().equals(abilityName)) { if (card != null) {
spellAbilityCopy = ((SplitCard) card).getRightHalfCard().getSpellAbility().copy(); if (spellAbilityToResolve == null) {
} SpellAbility spellAbilityCopy = null;
} else if (card instanceof ModalDoubleFacedCard) { if (card instanceof SplitCard) {
if (((ModalDoubleFacedCard) card).getLeftHalfCard().getName().equals(abilityName)) { if (((SplitCard) card).getLeftHalfCard().getName().equals(abilityName)) {
spellAbilityCopy = ((ModalDoubleFacedCard) card).getLeftHalfCard().getSpellAbility().copy(); spellAbilityCopy = ((SplitCard) card).getLeftHalfCard().getSpellAbility().copy();
} else if (((ModalDoubleFacedCard) card).getRightHalfCard().getName().equals(abilityName)) { } else if (((SplitCard) card).getRightHalfCard().getName().equals(abilityName)) {
spellAbilityCopy = ((ModalDoubleFacedCard) card).getRightHalfCard().getSpellAbility().copy(); spellAbilityCopy = ((SplitCard) card).getRightHalfCard().getSpellAbility().copy();
} }
} else if (card instanceof CardWithSpellOption) { } else if (card instanceof ModalDoubleFacedCard) {
if (card.getMainCard().getName().equals(abilityName)) { if (((ModalDoubleFacedCard) card).getLeftHalfCard().getName().equals(abilityName)) {
spellAbilityCopy = card.getMainCard().getSpellAbility().copy(); spellAbilityCopy = ((ModalDoubleFacedCard) card).getLeftHalfCard().getSpellAbility().copy();
} else if (((CardWithSpellOption) card).getSpellCard().getName().equals(abilityName)) { } else if (((ModalDoubleFacedCard) card).getRightHalfCard().getName().equals(abilityName)) {
spellAbilityCopy = ((CardWithSpellOption) card).getSpellCard().getSpellAbility().copy(); spellAbilityCopy = ((ModalDoubleFacedCard) card).getRightHalfCard().getSpellAbility().copy();
} }
} else { } else if (card instanceof CardWithSpellOption) {
spellAbilityCopy = card.getSpellAbility().copy(); if (card.getMainCard().getName().equals(abilityName)) {
} spellAbilityCopy = card.getMainCard().getSpellAbility().copy();
if (spellAbilityCopy == null) { } else if (((CardWithSpellOption) card).getSpellCard().getName().equals(abilityName)) {
return null; spellAbilityCopy = ((CardWithSpellOption) card).getSpellCard().getSpellAbility().copy();
} }
spellAbilityCopy.setId(this.getId()); } else {
spellAbilityCopy.clearManaCosts(); spellAbilityCopy = card.getSpellAbility().copy();
spellAbilityCopy.clearManaCostsToPay(); }
spellAbilityCopy.addCost(this.getCosts().copy()); if (spellAbilityCopy == null) {
spellAbilityCopy.addCost(this.getManaCosts().copy()); return null;
spellAbilityCopy.setSpellAbilityCastMode(this.getSpellAbilityCastMode()); }
spellAbilityToResolve = spellAbilityCopy; spellAbilityCopy.setId(this.getId());
} spellAbilityCopy.clearManaCosts();
} spellAbilityCopy.clearManaCostsToPay();
return spellAbilityToResolve; spellAbilityCopy.addCost(this.getCosts().copy());
} spellAbilityCopy.addCost(this.getManaCosts().copy());
spellAbilityCopy.setSpellAbilityCastMode(this.getSpellAbilityCastMode());
@Override spellAbilityToResolve = spellAbilityCopy;
public Costs<Cost> getCosts() { }
if (spellAbilityToResolve == null) { }
return super.getCosts(); return spellAbilityToResolve;
} }
return spellAbilityToResolve.getCosts();
} @Override
public Costs<Cost> getCosts() {
@Override if (spellAbilityToResolve == null) {
public ForetellCostAbility copy() { return super.getCosts();
return new ForetellCostAbility(this); }
} return spellAbilityToResolve.getCosts();
}
@Override
public String getRule(boolean all) { @Override
StringBuilder sbRule = new StringBuilder("Foretell"); public ForetellCostAbility copy() {
if (!getCosts().isEmpty()) { return new ForetellCostAbility(this);
sbRule.append("&mdash;"); }
} else {
sbRule.append(' '); @Override
} public String getRule(boolean all) {
if (!getManaCosts().isEmpty()) { StringBuilder sbRule = new StringBuilder("Foretell");
sbRule.append(getManaCosts().getText()); if (!getCosts().isEmpty()) {
} sbRule.append("&mdash;");
if (!getCosts().isEmpty()) { } else {
if (!getManaCosts().isEmpty()) { sbRule.append(' ');
sbRule.append(", "); }
} if (!getManaCosts().isEmpty()) {
sbRule.append(getCosts().getText()); sbRule.append(getManaCosts().getText());
sbRule.append('.'); }
} if (!getCosts().isEmpty()) {
if (abilityName != null) { if (!getManaCosts().isEmpty()) {
sbRule.append(' '); sbRule.append(", ");
sbRule.append(abilityName); }
} sbRule.append(getCosts().getText());
sbRule.append(" <i>(You may cast this card from exile for its foretell cost.)</i>"); sbRule.append('.');
return sbRule.toString(); }
} if (abilityName != null) {
sbRule.append(' ');
/** sbRule.append(abilityName);
* Used for split card in PlayerImpl method: }
* getOtherUseableActivatedAbilities sbRule.append(" <i>(You may cast this card from exile for its foretell cost.)</i>");
*/ return sbRule.toString();
public void setAbilityName(String abilityName) { }
this.abilityName = abilityName;
} /**
* Used for split card in PlayerImpl method:
} * getOtherUseableActivatedAbilities
*/
public static boolean isCardInForetell(Card card, Game game) { void setAbilityName(String abilityName) {
// searching ForetellCostAbility - it adds for foretelled cards only after exile this.abilityName = abilityName;
return card.getAbilities(game).containsClass(ForetellCostAbility.class); }
}
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;
} }
} }

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 Card card = game.getCard(event.getTargetId());
if (event.getType() == GameEvent.EventType.FORETOLD) { if (card != null) {
Card card = game.getCard(event.getTargetId()); foretoldCards.add(new MageObjectReference(card, game, 1));
if (card != null) { }
// Ethereal Valkyrie does not Foretell the card, it becomes Foretold, so don't add it to the Foretell list if (event.getFlag()) {
foretoldCards.add(card.getId()); 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();
} }
} }