Foretell: add Keyword

This commit is contained in:
Hans Mackowiak
2021-02-02 15:59:46 +00:00
committed by Michael Kamensky
parent 9f4c694c21
commit 4cbeca4cb3
31 changed files with 414 additions and 63 deletions

View File

@@ -153,6 +153,10 @@ public class ForgeScript {
return sa.hasParam("Equip"); return sa.hasParam("Equip");
} else if (property.equals("Boast")) { } else if (property.equals("Boast")) {
return sa.isBoast(); return sa.isBoast();
} else if (property.equals("Foretelling")) {
return sa.isForetelling();
} else if (property.equals("Foretold")) {
return sa.isForetold();
} else if (property.equals("MayPlaySource")) { } else if (property.equals("MayPlaySource")) {
StaticAbility m = sa.getMayPlay(); StaticAbility m = sa.getMayPlay();
if (m == null) { if (m == null) {

View File

@@ -73,6 +73,8 @@ public class GameAction {
// Reset Activations per Turn // Reset Activations per Turn
for (final Card card : game.getCardsInGame()) { for (final Card card : game.getCardsInGame()) {
card.resetActivationsPerTurn(); card.resetActivationsPerTurn();
// need to reset this in exile
card.resetForetoldThisTurn();
} }
} }
@@ -168,7 +170,7 @@ public class GameAction {
// Don't copy Tokens, copy only cards leaving the battlefield // Don't copy Tokens, copy only cards leaving the battlefield
// and returning to hand (to recreate their spell ability information) // and returning to hand (to recreate their spell ability information)
if (suppress || toBattlefield || zoneTo.is(ZoneType.Stack)) { if (suppress || toBattlefield) {
copied = c; copied = c;
if (lastKnownInfo == null) { if (lastKnownInfo == null) {
@@ -193,10 +195,6 @@ public class GameAction {
lastKnownInfo = CardUtil.getLKICopy(c); lastKnownInfo = CardUtil.getLKICopy(c);
} }
if (wasFacedown) {
c.forceTurnFaceUp();
}
if (!c.isToken()) { if (!c.isToken()) {
copied = CardFactory.copyCard(c, false); copied = CardFactory.copyCard(c, false);

View File

@@ -204,8 +204,51 @@ public final class GameActionUtil {
flashback.setPayCosts(new Cost(k[1], false)); flashback.setPayCosts(new Cost(k[1], false));
} }
alternatives.add(flashback); alternatives.add(flashback);
} else if (keyword.startsWith("Foretell")) {
// Fortell cast only from Exile
if (!source.isInZone(ZoneType.Exile) || !source.isForetold() || source.isForetoldThisTurn()) {
continue;
}
// skip this part for fortell by external source
if (keyword.equals("Foretell")) {
continue;
}
final SpellAbility foretold = sa.copy(activator);
foretold.setAlternativeCost(AlternativeCost.Foretold);
foretold.getRestrictions().setZone(ZoneType.Exile);
// Stack Description only for Permanent or it might crash
if (source.isPermanent()) {
final StringBuilder sbStack = new StringBuilder();
sbStack.append(sa.getStackDescription()).append(" (Foretold)");
foretold.setStackDescription(sbStack.toString());
}
final String[] k = keyword.split(":");
foretold.setPayCosts(new Cost(k[1], false));
alternatives.add(foretold);
} }
} }
// foretell by external source
if (source.isForetoldByEffect() && source.isInZone(ZoneType.Exile) && source.isForetold() && !source.isForetoldThisTurn() && !source.getManaCost().isNoCost()) {
// Its foretell cost is equal to its mana cost reduced by {2}.
final SpellAbility foretold = sa.copy(activator);
foretold.putParam("ReduceCost", "2");
foretold.setAlternativeCost(AlternativeCost.Foretold);
foretold.getRestrictions().setZone(ZoneType.Exile);
// Stack Description only for Permanent or it might crash
if (source.isPermanent()) {
final StringBuilder sbStack = new StringBuilder();
sbStack.append(sa.getStackDescription()).append(" (Foretold)");
foretold.setStackDescription(sbStack.toString());
}
alternatives.add(foretold);
}
} }
// reset static abilities // reset static abilities

View File

@@ -701,6 +701,13 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
if (sa.hasParam("ExileFaceDown")) { if (sa.hasParam("ExileFaceDown")) {
movedCard.turnFaceDown(true); movedCard.turnFaceDown(true);
} }
if (sa.hasParam("Foretold")) {
movedCard.setForetold(true);
movedCard.setForetoldThisTurn(true);
movedCard.setForetoldByEffect(true);
// look at the exiled card
movedCard.addMayLookTemp(sa.getActivatingPlayer());
}
if (sa.hasParam("TrackDiscarded")) { if (sa.hasParam("TrackDiscarded")) {
movedCard.setMadnessWithoutCast(true); movedCard.setMadnessWithoutCast(true);
@@ -1240,6 +1247,13 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
if (sa.hasParam("ExileFaceDown")) { if (sa.hasParam("ExileFaceDown")) {
movedCard.turnFaceDown(true); movedCard.turnFaceDown(true);
} }
if (sa.hasParam("Foretold")) {
movedCard.setForetold(true);
movedCard.setForetoldThisTurn(true);
movedCard.setForetoldByEffect(true);
// look at the exiled card
movedCard.addMayLookTemp(sa.getActivatingPlayer());
}
} }
else { else {
movedCard = game.getAction().moveTo(destination, c, 0, cause, moveParams); movedCard = game.getAction().moveTo(destination, c, 0, cause, moveParams);

View File

@@ -177,6 +177,10 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
private boolean manifested = false; private boolean manifested = false;
private boolean foretold = false;
private boolean foretoldThisTurn = false;
private boolean foretoldByEffect = false;
private long bestowTimestamp = -1; private long bestowTimestamp = -1;
private long transformedTimestamp = 0; private long transformedTimestamp = 0;
private boolean tributed = false; private boolean tributed = false;
@@ -1694,7 +1698,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
} else { } else {
sbLong.append(parts[0]).append(" ").append(ManaCostParser.parse(parts[1])).append("\r\n"); sbLong.append(parts[0]).append(" ").append(ManaCostParser.parse(parts[1])).append("\r\n");
} }
} else if (keyword.startsWith("Morph") || keyword.startsWith("Megamorph") || keyword.startsWith("Escape")) { } else if (keyword.startsWith("Morph") || keyword.startsWith("Megamorph") || keyword.startsWith("Escape") || keyword.startsWith("Foretell:")) {
String[] k = keyword.split(":"); String[] k = keyword.split(":");
sbLong.append(k[0]); sbLong.append(k[0]);
if (k.length > 1) { if (k.length > 1) {
@@ -1789,6 +1793,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
|| keyword.equals("Changeling") || keyword.equals("Delve") || keyword.equals("Changeling") || keyword.equals("Delve")
|| keyword.equals("Split second") || keyword.equals("Sunburst") || keyword.equals("Split second") || keyword.equals("Sunburst")
|| keyword.equals("Suspend") // for the ones without amounnt || keyword.equals("Suspend") // for the ones without amounnt
|| keyword.equals("Foretell") // for the ones without cost
|| keyword.equals("Hideaway") || keyword.equals("Ascend") || keyword.equals("Hideaway") || keyword.equals("Ascend")
|| keyword.equals("Totem armor") || keyword.equals("Battle cry") || keyword.equals("Totem armor") || keyword.equals("Battle cry")
|| keyword.equals("Devoid") || keyword.equals("Riot")){ || keyword.equals("Devoid") || keyword.equals("Riot")){
@@ -2236,7 +2241,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
sbBefore.append("\r\n"); sbBefore.append("\r\n");
} else if (keyword.startsWith("Entwine") || keyword.startsWith("Madness") } else if (keyword.startsWith("Entwine") || keyword.startsWith("Madness")
|| keyword.startsWith("Miracle") || keyword.startsWith("Recover") || keyword.startsWith("Miracle") || keyword.startsWith("Recover")
|| keyword.startsWith("Escape")) { || keyword.startsWith("Escape") || keyword.startsWith("Foretell:")) {
final String[] k = keyword.split(":"); final String[] k = keyword.split(":");
final Cost cost = new Cost(k[1], false); final Cost cost = new Cost(k[1], false);
@@ -5319,6 +5324,42 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
} }
} }
public final boolean isForetold() {
// in exile and foretold
if (this.isInZone(ZoneType.Exile)) {
return this.foretold;
}
// cast as foretold, currently only spells
if (this.getCastSA() != null) {
return this.getCastSA().isForetold();
}
return false;
}
public final void setForetold(final boolean foretold) {
this.foretold = foretold;
}
public boolean isForetoldByEffect() {
return foretoldByEffect;
}
public void setForetoldByEffect(final boolean val) {
this.foretoldByEffect = val;
}
public boolean isForetoldThisTurn() {
return foretoldThisTurn;
}
public final void setForetoldThisTurn(final boolean foretoldThisTurn) {
this.foretoldThisTurn = foretoldThisTurn;
}
public void resetForetoldThisTurn() {
foretoldThisTurn = false;
}
public final void animateBestow() { public final void animateBestow() {
animateBestow(true); animateBestow(true);
} }
@@ -6567,11 +6608,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
return numberGameActivations.containsKey(original) ? numberGameActivations.get(original) : 0; return numberGameActivations.containsKey(original) ? numberGameActivations.get(original) : 0;
} }
public void resetTurnActivations() {
numberTurnActivations.clear();
numberTurnActivationsStatic.clear();
}
public List<String> getChosenModesTurn(SpellAbility ability) { public List<String> getChosenModesTurn(SpellAbility ability) {
SpellAbility original = null; SpellAbility original = null;
SpellAbility root = ability.getRootAbility(); SpellAbility root = ability.getRootAbility();

View File

@@ -88,7 +88,7 @@ public class CardFactory {
} }
out.setZone(in.getZone()); out.setZone(in.getZone());
out.setState(in.getCurrentStateName(), true); out.setState(in.getFaceupCardStateName(), true);
out.setBackSide(in.isBackSide()); out.setBackSide(in.isBackSide());
// this's necessary for forge.game.GameAction.unattachCardLeavingBattlefield(Card) // this's necessary for forge.game.GameAction.unattachCardLeavingBattlefield(Card)

View File

@@ -66,7 +66,6 @@ import java.util.Map.Entry;
import io.sentry.Sentry; import io.sentry.Sentry;
import io.sentry.event.BreadcrumbBuilder; import io.sentry.event.BreadcrumbBuilder;
/** /**
* <p> * <p>
* CardFactoryUtil class. * CardFactoryUtil class.
@@ -1404,6 +1403,13 @@ public class CardFactoryUtil {
return doXMath(StringUtils.isNumeric(v) ? Integer.parseInt(v) : xCount(c, c.getSVar(v)), m, c); return doXMath(StringUtils.isNumeric(v) ? Integer.parseInt(v) : xCount(c, c.getSVar(v)), m, c);
} }
// Count$Foretold.<True>.<False>
if (sq[0].startsWith("Foretold")) {
String v = c.isForetold() ? sq[1] : sq[2];
// TODO move this to AbilityUtils
return doXMath(StringUtils.isNumeric(v) ? Integer.parseInt(v) : xCount(c, c.getSVar(v)), m, c);
}
// Count$Presence_<Type>.<True>.<False> // Count$Presence_<Type>.<True>.<False>
if (sq[0].startsWith("Presence")) { if (sq[0].startsWith("Presence")) {
final String type = sq[0].split("_")[1]; final String type = sq[0].split("_")[1];
@@ -1449,7 +1455,6 @@ public class CardFactoryUtil {
return forge.util.MyRandom.getRandom().nextInt(1+max-min) + min; return forge.util.MyRandom.getRandom().nextInt(1+max-min) + min;
} }
// Count$Domain // Count$Domain
if (sq[0].startsWith("Domain")) { if (sq[0].startsWith("Domain")) {
int n = 0; int n = 0;
@@ -1997,7 +2002,6 @@ public class CardFactoryUtil {
final Set<String> hexproofkw = Sets.newHashSet(); final Set<String> hexproofkw = Sets.newHashSet();
final Set<String> allkw = Sets.newHashSet(); final Set<String> allkw = Sets.newHashSet();
for (Card c : CardLists.getValidCards(cardlist, restrictions, p, host, null)) { for (Card c : CardLists.getValidCards(cardlist, restrictions, p, host, null)) {
for (KeywordInterface inst : c.getKeywords()) { for (KeywordInterface inst : c.getKeywords()) {
final String k = inst.getOriginal(); final String k = inst.getOriginal();
@@ -2155,7 +2159,6 @@ public class CardFactoryUtil {
return re; return re;
} }
public static ReplacementEffect makeEtbCounter(final String kw, final Card card, final boolean intrinsic) public static ReplacementEffect makeEtbCounter(final String kw, final Card card, final boolean intrinsic)
{ {
String parse = kw; String parse = kw;
@@ -4095,6 +4098,60 @@ public class CardFactoryUtil {
newSA.setAlternativeCost(AlternativeCost.Evoke); newSA.setAlternativeCost(AlternativeCost.Evoke);
newSA.setIntrinsic(intrinsic); newSA.setIntrinsic(intrinsic);
inst.addSpellAbility(newSA); inst.addSpellAbility(newSA);
} else if (keyword.startsWith("Foretell")) {
final SpellAbility foretell = new AbilityStatic(card, new Cost(ManaCost.TWO, false), null) {
@Override
public boolean canPlay() {
if (!getRestrictions().canPlay(getHostCard(), this)) {
return false;
}
Player activator = this.getActivatingPlayer();
final Game game = activator.getGame();
if (!activator.hasKeyword("Foretell on any players turn") && !game.getPhaseHandler().isPlayerTurn(activator)) {
return false;
}
return true;
}
@Override
public boolean isForetelling() {
return true;
}
@Override
public void resolve() {
final Game game = getHostCard().getGame();
final Card c = game.getAction().exile(getHostCard(), this);
c.setForetold(true);
c.setForetoldThisTurn(true);
c.turnFaceDown(true);
// look at the exiled card
c.addMayLookTemp(getActivatingPlayer());
// only done when the card is foretold by the static ability
getActivatingPlayer().addForetoldThisTurn();
if (!isIntrinsic()) {
// because it doesn't work other wise
c.setForetoldByEffect(true);
}
String sb = TextUtil.concatWithSpace(getActivatingPlayer().toString(),"has foretold.");
game.getGameLog().add(GameLogEntryType.STACK_RESOLVE, sb);
}
};
final StringBuilder sbDesc = new StringBuilder();
sbDesc.append("Foretell (").append(inst.getReminderText()).append(")");
foretell.setDescription(sbDesc.toString());
foretell.putParam("Secondary", "True");
foretell.getRestrictions().setZone(ZoneType.Hand);
foretell.setIntrinsic(intrinsic);
inst.addSpellAbility(foretell);
} else if (keyword.startsWith("Fortify")) { } else if (keyword.startsWith("Fortify")) {
String[] k = keyword.split(":"); String[] k = keyword.split(":");
// Get cost string // Get cost string

View File

@@ -1670,36 +1670,40 @@ public class CardProperty {
if (property.equals("pseudokicked")) { if (property.equals("pseudokicked")) {
if (!card.isOptionalCostPaid(OptionalCost.Generic)) return false; if (!card.isOptionalCostPaid(OptionalCost.Generic)) return false;
} }
} else if (property.startsWith("surged")) { } else if (property.equals("surged")) {
if (card.getCastSA() == null) { if (card.getCastSA() == null) {
return false; return false;
} }
return card.getCastSA().isSurged(); return card.getCastSA().isSurged();
} else if (property.startsWith("dashed")) { } else if (property.equals("dashed")) {
if (card.getCastSA() == null) { if (card.getCastSA() == null) {
return false; return false;
} }
return card.getCastSA().isDash(); return card.getCastSA().isDash();
} else if (property.startsWith("escaped")) { } else if (property.equals("escaped")) {
if (card.getCastSA() == null) { if (card.getCastSA() == null) {
return false; return false;
} }
return card.getCastSA().isEscape(); return card.getCastSA().isEscape();
} else if (property.startsWith("evoked")) { } else if (property.equals("evoked")) {
if (card.getCastSA() == null) { if (card.getCastSA() == null) {
return false; return false;
} }
return card.getCastSA().isEvoke(); return card.getCastSA().isEvoke();
} else if (property.startsWith("prowled")) { } else if (property.equals("prowled")) {
if (card.getCastSA() == null) { if (card.getCastSA() == null) {
return false; return false;
} }
return card.getCastSA().isProwl(); return card.getCastSA().isProwl();
} else if (property.startsWith("spectacle")) { } else if (property.equals("spectacle")) {
if (card.getCastSA() == null) { if (card.getCastSA() == null) {
return false; return false;
} }
return card.getCastSA().isSpectacle(); return card.getCastSA().isSpectacle();
} else if (property.equals("foretold")) {
if (!card.isForetold()) {
return false;
}
} else if (property.equals("HasDevoured")) { } else if (property.equals("HasDevoured")) {
if (card.getDevouredCards().isEmpty()) { if (card.getDevouredCards().isEmpty()) {
return false; return false;

View File

@@ -278,6 +278,10 @@ public final class CardUtil {
newCopy.copyChangedTextFrom(in); newCopy.copyChangedTextFrom(in);
newCopy.setForetold(in.isForetold());
newCopy.setForetoldThisTurn(in.isForetoldThisTurn());
newCopy.setForetoldByEffect(in.isForetoldByEffect());
newCopy.setMeldedWith(getLKICopy(in.getMeldedWith(), cachedMap)); newCopy.setMeldedWith(getLKICopy(in.getMeldedWith(), cachedMap));
newCopy.setTimestamp(in.getTimestamp()); newCopy.setTimestamp(in.getTimestamp());

View File

@@ -70,6 +70,7 @@ public enum Keyword {
FLASH("Flash", SimpleKeyword.class, true, "You may cast this spell any time you could cast an instant."), FLASH("Flash", SimpleKeyword.class, true, "You may cast this spell any time you could cast an instant."),
FLASHBACK("Flashback", KeywordWithCost.class, false, "You may cast this card from your graveyard by paying %s rather than paying its mana cost. If you do, exile it as it resolves."), FLASHBACK("Flashback", KeywordWithCost.class, false, "You may cast this card from your graveyard by paying %s rather than paying its mana cost. If you do, exile it as it resolves."),
FLYING("Flying", SimpleKeyword.class, true, "This creature can't be blocked except by creatures with flying or reach."), FLYING("Flying", SimpleKeyword.class, true, "This creature can't be blocked except by creatures with flying or reach."),
FORETELL("Foretell", KeywordWithCost.class, false, "During your turn, you may pay {2} and exile this card from your hand face down. Cast it on a later turn for its foretell cost."),
FORTIFY("Fortify", KeywordWithCost.class, false, "%s: Attach to target land you control. Fortify only as a sorcery."), FORTIFY("Fortify", KeywordWithCost.class, false, "%s: Attach to target land you control. Fortify only as a sorcery."),
FRENZY("Frenzy", KeywordWithAmount.class, false, "Whenever this creature attacks and isn't blocked, it gets +%d/+0 until end of turn."), FRENZY("Frenzy", KeywordWithAmount.class, false, "Whenever this creature attacks and isn't blocked, it gets +%d/+0 until end of turn."),
GRAFT("Graft", KeywordWithAmount.class, false, "This permanent enters the battlefield with {%d:+1/+1 counter} on it. Whenever another creature enters the battlefield, you may move a +1/+1 counter from this permanent onto it."), GRAFT("Graft", KeywordWithAmount.class, false, "This permanent enters the battlefield with {%d:+1/+1 counter} on it. Whenever another creature enters the battlefield, you may move a +1/+1 counter from this permanent onto it."),

View File

@@ -26,7 +26,6 @@ import forge.game.*;
import forge.game.ability.AbilityKey; import forge.game.ability.AbilityKey;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardCollection; import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists; import forge.game.card.CardLists;
import forge.game.card.CardPredicates.Presets; import forge.game.card.CardPredicates.Presets;
import forge.game.card.CardZoneTable; import forge.game.card.CardZoneTable;
@@ -175,8 +174,7 @@ public class PhaseHandler implements java.io.Serializable {
game.fireEvent(new GameEventTurnBegan(playerTurn, turn)); game.fireEvent(new GameEventTurnBegan(playerTurn, turn));
// Tokens starting game in play should suffer from Sum. Sickness // Tokens starting game in play should suffer from Sum. Sickness
final CardCollectionView list = playerTurn.getCardsIncludePhasingIn(ZoneType.Battlefield); for (final Card c : playerTurn.getCardsIncludePhasingIn(ZoneType.Battlefield)) {
for (final Card c : list) {
if (playerTurn.getTurn() > 0 || !c.isStartsGameInPlay()) { if (playerTurn.getTurn() > 0 || !c.isStartsGameInPlay()) {
c.setSickness(false); c.setSickness(false);
} }

View File

@@ -111,6 +111,7 @@ public class Player extends GameEntity implements Comparable<Player> {
private int numDrawnThisDrawStep = 0; private int numDrawnThisDrawStep = 0;
private int numDiscardedThisTurn = 0; private int numDiscardedThisTurn = 0;
private int numTokenCreatedThisTurn = 0; private int numTokenCreatedThisTurn = 0;
private int numForetoldThisTurn = 0;
private int numCardsInHandStartedThisTurnWith = 0; private int numCardsInHandStartedThisTurnWith = 0;
private final Map<String, FCollection<String>> notes = Maps.newHashMap(); private final Map<String, FCollection<String>> notes = Maps.newHashMap();
@@ -1666,6 +1667,22 @@ public class Player extends GameEntity implements Comparable<Player> {
numTokenCreatedThisTurn = 0; numTokenCreatedThisTurn = 0;
} }
public final int getNumForetoldThisTurn() {
return numForetoldThisTurn;
}
public final void addForetoldThisTurn() {
numForetoldThisTurn++;
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
runParams.put(AbilityKey.Player, this);
runParams.put(AbilityKey.Num, numForetoldThisTurn);
game.getTriggerHandler().runTrigger(TriggerType.Foretell, runParams, false);
}
public final void resetNumForetoldThisTurn() {
numForetoldThisTurn = 0;
}
public final int getNumDiscardedThisTurn() { public final int getNumDiscardedThisTurn() {
return numDiscardedThisTurn; return numDiscardedThisTurn;
} }

View File

@@ -9,6 +9,7 @@ public enum AlternativeCost {
Escape, Escape,
Evoke, Evoke,
Flashback, Flashback,
Foretold,
Madness, Madness,
Offering, Offering,
Outlast, // ActivatedAbility Outlast, // ActivatedAbility

View File

@@ -823,6 +823,14 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
return this.isAlternativeCost(AlternativeCost.Flashback); return this.isAlternativeCost(AlternativeCost.Flashback);
} }
public boolean isForetelling() {
return false;
}
public boolean isForetold() {
return this.isAlternativeCost(AlternativeCost.Foretold);
}
/** /**
* @return the aftermath * @return the aftermath
*/ */
@@ -1780,6 +1788,11 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
return false; return false;
} }
} }
else if (incR[0].equals("Static")) {
if (!(root instanceof AbilityStatic)) {
return false;
}
}
else { //not a spell/ability type else { //not a spell/ability type
return false; return false;
} }

View File

@@ -118,6 +118,10 @@ public class SpellAbilityCondition extends SpellAbilityVariables {
this.optionalCostPaid = true; this.optionalCostPaid = true;
} }
if (value.equals("Foretold")) {
this.foretold = true;
}
if (params.containsKey("ConditionOptionalPaid")) { if (params.containsKey("ConditionOptionalPaid")) {
this.optionalBoolean = Boolean.parseBoolean(params.get("ConditionOptionalPaid")); this.optionalBoolean = Boolean.parseBoolean(params.get("ConditionOptionalPaid"));
} }
@@ -250,6 +254,7 @@ public class SpellAbilityCondition extends SpellAbilityVariables {
if (this.kicked2 && !sa.isOptionalCostPaid(OptionalCost.Kicker2)) return false; if (this.kicked2 && !sa.isOptionalCostPaid(OptionalCost.Kicker2)) return false;
if (this.altCostPaid && !sa.isOptionalCostPaid(OptionalCost.AltCost)) return false; if (this.altCostPaid && !sa.isOptionalCostPaid(OptionalCost.AltCost)) return false;
if (this.surgeCostPaid && !sa.isSurged()) return false; if (this.surgeCostPaid && !sa.isSurged()) return false;
if (this.foretold && !sa.isForetold()) return false;
if (this.optionalCostPaid && this.optionalBoolean && !sa.isOptionalCostPaid(OptionalCost.Generic)) return false; if (this.optionalCostPaid && this.optionalBoolean && !sa.isOptionalCostPaid(OptionalCost.Generic)) return false;
if (this.optionalCostPaid && !this.optionalBoolean && sa.isOptionalCostPaid(OptionalCost.Generic)) return false; if (this.optionalCostPaid && !this.optionalBoolean && sa.isOptionalCostPaid(OptionalCost.Generic)) return false;

View File

@@ -405,6 +405,7 @@ public class SpellAbilityVariables implements Cloneable {
protected boolean optionalCostPaid = false; // Undergrowth other Pseudo-kickers protected boolean optionalCostPaid = false; // Undergrowth other Pseudo-kickers
protected boolean optionalBoolean = true; // Just in case you need to check if something wasn't kicked, etc protected boolean optionalBoolean = true; // Just in case you need to check if something wasn't kicked, etc
protected boolean surgeCostPaid = false; protected boolean surgeCostPaid = false;
protected boolean foretold = false;
/** /**
* @return the allTargetsLegal * @return the allTargetsLegal

View File

@@ -0,0 +1,78 @@
/*
* Forge: Play Magic: the Gathering.
* Copyright (C) 2011 Forge Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package forge.game.trigger;
import forge.game.ability.AbilityKey;
import forge.game.card.Card;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.util.Localizer;
import java.util.Map;
/**
* @author Forge
*/
public class TriggerForetell extends Trigger {
/**
*
* @param params
* a {@link java.util.HashMap} object.
* @param host
* a {@link forge.game.card.Card} object.
* @param intrinsic
* the intrinsic
*/
public TriggerForetell(final Map<String, String> params, final Card host, final boolean intrinsic) {
super(params, host, intrinsic);
}
@Override
public String getImportantStackObjects(SpellAbility sa) {
StringBuilder sb = new StringBuilder();
sb.append(Localizer.getInstance().getMessage("lblPlayer")).append(": ").append(sa.getTriggeringObject(AbilityKey.Player));
return sb.toString();
}
/** {@inheritDoc} */
@Override
public final void setTriggeringObjects(final SpellAbility sa, Map<AbilityKey, Object> runParams) {
sa.setTriggeringObjectsFrom(runParams, AbilityKey.Player);
}
/** {@inheritDoc}
* @param runParams*/
@Override
public final boolean performTest(final Map<AbilityKey, Object> runParams) {
Player p = (Player) runParams.get(AbilityKey.Player);
if (hasParam("ValidPlayer")) {
if (!matchesValid(p, getParam("ValidPlayer").split(","), getHostCard())) {
return false;
}
}
if (hasParam("OnlyFirst")) {
if ((int) runParams.get(AbilityKey.Num) != 1) {
return false;
}
}
return true;
}
}

View File

@@ -64,6 +64,7 @@ public enum TriggerType {
Fight(TriggerFight.class), Fight(TriggerFight.class),
FightOnce(TriggerFightOnce.class), FightOnce(TriggerFightOnce.class),
FlippedCoin(TriggerFlippedCoin.class), FlippedCoin(TriggerFlippedCoin.class),
Foretell(TriggerForetell.class),
Immediate(TriggerImmediate.class), Immediate(TriggerImmediate.class),
Investigated(TriggerInvestigated.class), Investigated(TriggerInvestigated.class),
LandPlayed(TriggerLandPlayed.class), LandPlayed(TriggerLandPlayed.class),

View File

@@ -52,7 +52,6 @@ import forge.game.keyword.Keyword;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.AbilityStatic; import forge.game.spellability.AbilityStatic;
import forge.game.spellability.OptionalCost; import forge.game.spellability.OptionalCost;
import forge.game.spellability.Spell;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance; import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.spellability.TargetChoices; import forge.game.spellability.TargetChoices;
@@ -241,8 +240,7 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
if (sp.isSpell()) { if (sp.isSpell()) {
source.setController(activator, 0); source.setController(activator, 0);
final Spell spell = (Spell) sp; if (sp.isCastFaceDown()) {
if (spell.isCastFaceDown()) {
// Need to override for double faced cards // Need to override for double faced cards
source.turnFaceDown(true); source.turnFaceDown(true);
} else if (source.isFaceDown()) { } else if (source.isFaceDown()) {

View File

@@ -1847,7 +1847,7 @@ public class GameSimulatorTest extends SimulationTestCase {
sim.simulateSpellAbility(gideonSA); sim.simulateSpellAbility(gideonSA);
sim.simulateSpellAbility(sparkDoubleSA); sim.simulateSpellAbility(sparkDoubleSA);
Card simSpark = (Card)sim.getGameCopier().find(sparkDouble); Card simSpark = sim.getSimulatedGameState().findById(sparkDouble.getId());
assertNotNull(simSpark); assertNotNull(simSpark);
assertTrue(simSpark.isInZone(ZoneType.Battlefield)); assertTrue(simSpark.isInZone(ZoneType.Battlefield));

View File

@@ -0,0 +1,11 @@
Name:Cosmos Charger
ManaCost:3 U
Types:Creature Horse Spirit
PT:3/3
K:Flash
K:Flying
S:Mode$ ReduceCost | ValidSpell$ Static.Foretelling | Activator$ You | Amount$ 1 | Description$ Foretelling cards from your hand costs {1} less and can be done on any players turn.
S:Mode$ Continuous | Affected$ You | AddKeyword$ Foretell on any players turn | Secondary$ True | Description$ Foretelling cards from your hand on any players turn.
K:Foretell:2 U
Oracle:Flash\nFlying\nForetelling cards from your hand costs {1} less and can be done on any players turn.\nForetell {2}{U} (During your turn, you may pay {2} and exile this card from your hand face down. Cast it on a later turn for its foretell cost.)

View File

@@ -0,0 +1,8 @@
Name:Dream Devourer
ManaCost:1 B
Types:Creature Demon Cleric
PT:0/3
S:Mode$ Continuous | Affected$ Card.nonLand+YouOwn+withoutForetell | AffectedZone$ Hand | AddKeyword$ Foretell | Description$ Each nonland card in your hand without foretell has foretell. Its foretell cost is equal to its mana cost reduced by {2}. (During your turn, you may pay {2} and exile it from your hand face down. Cast it on a later turn for its foretell cost.)
T:Mode$ Foretell | ValidPlayer$ You | Execute$ TrigPump | TriggerZones$ Battlefield | TriggerDescription$ Whenever you foretell a card, CARDNAME gets +2/+0 until end of turn.
SVar:TrigPump:DB$ Pump | Defined$ Self | NumAtt$ 2
Oracle:Each nonland card in your hand without foretell has foretell. Its foretell cost is equal to its mana cost reduced by {2}. (During your turn, you may pay {2} and exile it from your hand face down. Cast it on a later turn for its foretell cost.)\n Whenever you foretell a card, Dream Devourer gets +2/+0 until end of turn.

View File

@@ -0,0 +1,11 @@
Name:Ethereal Valkyrie
ManaCost:4 U W
Types:Creature Spirit Angel
PT:4/4
K:Flying
T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ DBDraw | TriggerDescription$ Whenever CARDNAME 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}. (On a later turn, you may cast it for its foretell cost, even if this creature has left the battlefield.)
T:Mode$ Attacks | ValidCard$ Card.Self | Execute$ DBDraw | Secondary$ True | TriggerDescription$ Whenever CARDNAME 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}. (On a later turn, you may cast it for its foretell cost, even if this creature has left the battlefield.)
SVar:DBDraw:DB$ Draw | NumCards$ 1 | SubAbility$ DBExile
SVar:DBExile:DB$ ChangeZone | Origin$ Hand | Destination$ Exile | ChangeType$ Card | ChangeNum$ 1 | ExileFaceDown$ True | Mandatory$ True | Foretold$ True
Oracle: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}. (On a later turn, you may cast it for its foretell cost, even if this creature has left the battlefield.)

View File

@@ -0,0 +1,8 @@
Name:Karfell Harbinger
ManaCost:1 U
Types:Creature Zombie Wizard
PT:1/3
A:AB$ Mana | Cost$ T | Produced$ U | RestrictValid$ Static.Foretelling,Spell.Instant,Spell.Sorcery | SpellDescription$ Add {U}. Spend this mana only to foretell a card from your hand or cast an instant or sorcery spell.
DeckHints:Type$Instant|Sorcery
Oracle:{T}: Add {U}. Spend this mana only to foretell a card from your hand or cast an instant or sorcery spell.

View File

@@ -0,0 +1,10 @@
Name:Niko Defies Destiny
ManaCost:1 W U
Types:Enchantment Saga
K:Saga:3:DBGainLife,DBMana,DBChangeZone
SVar:DBGainLife:DB$ GainLife | LifeAmount$ X | References$ X | SpellDescription$ You gain 2 life for each foretold card you own in exile.
SVar:X:Count$ValidExile Card.foretold/Times.2
SVar:DBMana:DB$ Mana | Produced$ W U | RestrictValid$ Static.Foretelling,Card.withForetell | SpellDescription$ Add {W}{U}. Spend this mana only to foretell cards or cast spells that have foretell.
SVar:DBChangeZone:DB$ChangeZone | Origin$ Graveyard | Destination$ Hand | ValidTgts$ Card.YouCtrl+withForetell | SpellDescription$ Return target card with foretell from your graveyard to your hand.
DeckHas:Ability$Graveyard & Ability$GainLife
Oracle:(As this Saga enters and after your draw step, add a lore counter. Sacrifice after III.)\nI — You gain 2 life for each foretold card you own in exile.\nII — Add {W}{U}. Spend this mana only to foretell cards or cast spells that have foretell.\nIII — Return target card with foretell from your graveyard to your hand.

View File

@@ -0,0 +1,8 @@
Name:Poison the Cup
ManaCost:1 B B
Types:Instant
A:SP$ Destroy | Cost$ 1 B B | ValidTgts$ Creature | TgtPrompt$ Select target creature | SubAbility$ DBScry | SpellDescription$ Destroy target creature. If this spell was foretold, scry 2.
SVar:DBScry:DB$ Scry | ScryNum$ 2 | Condition$ Foretold
K:Foretell:1 B
Oracle:Destroy target creature. If this spell was foretold, scry 2.\nForetell {1}{B} (During your turn, you may pay {2} and exile this card from your hand face down. Cast it on a later turn for its foretell cost.)

View File

@@ -0,0 +1,6 @@
Name:Scorn Effigy
ManaCost:3
Types:Artifact Creature Scarecrow
PT:2/3
K:Foretell:0
Oracle:Foretell {0} (During your turn, you may pay {2} and exile this card from your hand face down. Cast it on a later turn for its foretell cost.)

View File

@@ -0,0 +1,9 @@
Name:Starnheim Unleashed
ManaCost:2 W W
Types:Sorcery
A:SP$ Token | Cost$ 2 W W | TokenAmount$ Y | TokenScript$ w_4_4_angel_warrior_flying_vigilance | References$ X,Y | SpellDescription$ Create a 4/4 white Angel Warrior creature token with flying and vigilance. If this spell was foretold, create X of those tokens instead.
SVar:Y:Count$Foretold.X.1
SVar:X:Count$xPaid
K:Foretell:X X W
DeckHas:Ability$Token
Oracle:Create a 4/4 white Angel Warrior creature token with flying and vigilance. If this spell was foretold, create X of those tokens instead.\nForetell {X}{X}{W} (During your turn, you may pay {2} and exile this card from your hand face down. Cast it on a later turn for its foretell cost.)

View File

@@ -0,0 +1,8 @@
Name:Angel Warrior
ManaCost:no cost
Types:Creature Angel Warrior
Colors:white
PT:4/4
K:Flying
K:Vigilance
Oracle:Flying\nVigilance

View File

@@ -65,7 +65,7 @@ public class HumanPlay {
boolean castFaceDown = sa.isCastFaceDown(); boolean castFaceDown = sa.isCastFaceDown();
sa.setActivatingPlayer(p); sa.setActivatingPlayer(p);
boolean flippedToCast = sa instanceof Spell && source.isFaceDown(); boolean flippedToCast = sa.isSpell() && source.isFaceDown();
source.setSplitStateToPlayAbility(sa); source.setSplitStateToPlayAbility(sa);
sa = chooseOptionalAdditionalCosts(p, sa); sa = chooseOptionalAdditionalCosts(p, sa);
@@ -98,7 +98,12 @@ public class HumanPlay {
final HumanPlaySpellAbility req = new HumanPlaySpellAbility(controller, sa); final HumanPlaySpellAbility req = new HumanPlaySpellAbility(controller, sa);
if (!req.playAbility(true, false, false)) { if (!req.playAbility(true, false, false)) {
if (flippedToCast && !castFaceDown) { if (flippedToCast && !castFaceDown) {
source.turnFaceDown(true); // need to get the changed card if able
Card rollback = p.getGame().getCardState(sa.getHostCard());
rollback.turnFaceDown(true);
if (rollback.isInZone(ZoneType.Exile)) {
rollback.addMayLookTemp(p);
}
} }
return false; return false;
} }

View File

@@ -19,7 +19,6 @@ package forge.player;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import forge.card.CardStateName;
import forge.card.CardType; import forge.card.CardType;
import forge.card.MagicColor; import forge.card.MagicColor;
import forge.game.Game; import forge.game.Game;
@@ -64,7 +63,6 @@ public class HumanPlaySpellAbility {
// used to rollback // used to rollback
Zone fromZone = null; Zone fromZone = null;
CardStateName fromState = null;
int zonePosition = 0; int zonePosition = 0;
final ManaPool manapool = human.getManaPool(); final ManaPool manapool = human.getManaPool();
@@ -80,15 +78,9 @@ public class HumanPlaySpellAbility {
if (ability.isSpell() && !c.isCopiedSpell()) { if (ability.isSpell() && !c.isCopiedSpell()) {
fromZone = game.getZoneOf(c); fromZone = game.getZoneOf(c);
fromState = c.getCurrentStateName();
if (fromZone != null) { if (fromZone != null) {
zonePosition = fromZone.getCards().indexOf(c); zonePosition = fromZone.getCards().indexOf(c);
} }
// This is should happen earlier, before the Modal spell is chosen
// Turn face-down card face up (except case of morph spell)
if (ability.isSpell() && !ability.isCastFaceDown() && fromState == CardStateName.FaceDown) {
c.turnFaceUp(null);
}
ability.setHostCard(game.getAction().moveToStack(c, ability)); ability.setHostCard(game.getAction().moveToStack(c, ability));
} }
@@ -155,7 +147,7 @@ public class HumanPlaySpellAbility {
if (!prerequisitesMet) { if (!prerequisitesMet) {
if (!ability.isTrigger()) { if (!ability.isTrigger()) {
rollbackAbility(fromZone, zonePosition, payment); rollbackAbility(fromZone, zonePosition, payment, c);
if (ability.getHostCard().isMadness()) { if (ability.getHostCard().isMadness()) {
// if a player failed to play madness cost, move the card to graveyard // if a player failed to play madness cost, move the card to graveyard
Card newCard = game.getAction().moveToGraveyard(c, null); Card newCard = game.getAction().moveToGraveyard(c, null);
@@ -200,15 +192,17 @@ public class HumanPlaySpellAbility {
return true; return true;
} }
private void rollbackAbility(final Zone fromZone, final int zonePosition, CostPayment payment) { private void rollbackAbility(final Zone fromZone, final int zonePosition, CostPayment payment, Card oldCard) {
// cancel ability during target choosing // cancel ability during target choosing
final Game game = ability.getActivatingPlayer().getGame(); final Game game = ability.getActivatingPlayer().getGame();
if (fromZone != null) { // and not a copy if (fromZone != null) { // and not a copy
ability.getHostCard().setCastSA(null); oldCard.setCastSA(null);
ability.getHostCard().setCastFrom(null); oldCard.setCastFrom(null);
// add back to where it came from // add back to where it came from, hopefully old state
game.getAction().moveTo(fromZone, ability.getHostCard(), zonePosition >= 0 ? Integer.valueOf(zonePosition) : null, null); // skip GameAction
oldCard.getZone().remove(oldCard);
fromZone.add(oldCard, zonePosition >= 0 ? Integer.valueOf(zonePosition) : null);
} }
ability.clearTargets(); ability.clearTargets();