Merge branch 'mdfc' into 'master'

Correctly evaluate modal faces

See merge request core-developers/forge!5112
This commit is contained in:
Michael Kamensky
2021-07-27 04:53:19 +00:00
26 changed files with 94 additions and 91 deletions

View File

@@ -750,7 +750,15 @@ public class AiController {
return AiPlayDecision.CantAfford; return AiPlayDecision.CantAfford;
} }
// state needs to be switched here so API checks evaluate the right face
if (sa.getCardState().getStateName() == CardStateName.Modal) {
sa.getHostCard().setState(CardStateName.Modal, false);
}
AiPlayDecision canPlay = canPlaySa(sa); // this is the "heaviest" check, which also sets up targets, defines X, etc. AiPlayDecision canPlay = canPlaySa(sa); // this is the "heaviest" check, which also sets up targets, defines X, etc.
if (sa.getCardState().getStateName() == CardStateName.Modal) {
sa.getHostCard().setState(CardStateName.Original, false);
}
if (canPlay != AiPlayDecision.WillPlay) { if (canPlay != AiPlayDecision.WillPlay) {
return canPlay; return canPlay;
} }
@@ -810,8 +818,7 @@ public class AiController {
if (!canPlay) { if (!canPlay) {
return AiPlayDecision.CantPlayAi; return AiPlayDecision.CantPlayAi;
} }
} } else {
else {
Cost payCosts = sa.getPayCosts(); Cost payCosts = sa.getPayCosts();
if (payCosts != null) { if (payCosts != null) {
ManaCost mana = payCosts.getTotalMana(); ManaCost mana = payCosts.getTotalMana();

View File

@@ -484,7 +484,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
return PaymentDecision.card(source); return PaymentDecision.card(source);
} }
if (cost.getType().equals("OriginalHost")) { if (cost.getType().equals("OriginalHost")) {
return PaymentDecision.card(ability.getHostCard()); return PaymentDecision.card(ability.getOriginalHost());
} }
if (cost.getAmount().equals("All")) { if (cost.getAmount().equals("All")) {
// Does the AI want to use Sacrifice All? // Does the AI want to use Sacrifice All?
@@ -600,7 +600,6 @@ public class AiCostDecision extends CostDecisionMakerBase {
// currently if amount is bigger than one, // currently if amount is bigger than one,
// it tries to remove all counters from one source and type at once // it tries to remove all counters from one source and type at once
int toRemove = 0; int toRemove = 0;
final GameEntityCounterTable table = new GameEntityCounterTable(); final GameEntityCounterTable table = new GameEntityCounterTable();

View File

@@ -2890,10 +2890,10 @@ public class ComputerUtil {
return false; return false;
} }
public static boolean targetPlayableSpellCard(final Player ai, CardCollection options, final SpellAbility sa, final boolean withoutPayingManaCost) { public static boolean targetPlayableSpellCard(final Player ai, CardCollection options, final SpellAbility sa, final boolean withoutPayingManaCost, boolean mandatory) {
// determine and target a card with a SA that the AI can afford and will play // determine and target a card with a SA that the AI can afford and will play
AiController aic = ((PlayerControllerAi) ai.getController()).getAi(); AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
Card targetSpellCard = null; CardCollection targets = new CardCollection();
for (Card c : options) { for (Card c : options) {
if (withoutPayingManaCost && c.getManaCost() != null && c.getManaCost().countX() > 0) { if (withoutPayingManaCost && c.getManaCost() != null && c.getManaCost().countX() > 0) {
// The AI will otherwise cheat with the mana payment, announcing X > 0 for spells like Heat Ray when replaying them // The AI will otherwise cheat with the mana payment, announcing X > 0 for spells like Heat Ray when replaying them
@@ -2913,18 +2913,19 @@ public class ComputerUtil {
// at this point, we're assuming that card will be castable from whichever zone it's in by the AI player. // at this point, we're assuming that card will be castable from whichever zone it's in by the AI player.
abTest.setActivatingPlayer(ai); abTest.setActivatingPlayer(ai);
abTest.getRestrictions().setZone(c.getZone().getZoneType()); abTest.getRestrictions().setZone(c.getZone().getZoneType());
final boolean play = AiPlayDecision.WillPlay == aic.canPlaySa(abTest); if (AiPlayDecision.WillPlay == aic.canPlaySa(abTest) && ComputerUtilCost.canPayCost(abTest, ai)) {
final boolean pay = ComputerUtilCost.canPayCost(abTest, ai); targets.add(c);
if (play && pay) {
targetSpellCard = c;
break;
} }
} }
} }
if (targetSpellCard == null) { if (targets.isEmpty()) {
return false; if (mandatory && !options.isEmpty()) {
targets = options;
} else {
return false;
}
} }
sa.getTargets().add(targetSpellCard); sa.getTargets().add(ComputerUtilCard.getBestAI(targets));
return true; return true;
} }

View File

@@ -633,7 +633,7 @@ public class ComputerUtilCost {
} }
// Check if the AI intends to play the card and if it can pay for it with the mana it has // Check if the AI intends to play the card and if it can pay for it with the mana it has
boolean willPlay = ComputerUtil.hasReasonToPlayCardThisTurn(payer, c); boolean willPlay = ComputerUtil.hasReasonToPlayCardThisTurn(payer, c);
boolean canPay = c.getManaCost().canBePaidWithAvaliable(ColorSet.fromNames(getAvailableManaColors(payer, source)).getColor()); boolean canPay = c.getManaCost().canBePaidWithAvailable(ColorSet.fromNames(getAvailableManaColors(payer, source)).getColor());
return canPay && willPlay; return canPay && willPlay;
} }
} }

View File

@@ -1099,7 +1099,7 @@ public class SpecialCardAi {
for (final SpellAbility testSa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, ai)) { for (final SpellAbility testSa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, ai)) {
ManaCost cost = testSa.getPayCosts().getTotalMana(); ManaCost cost = testSa.getPayCosts().getTotalMana();
boolean canPayWithAvailableColors = cost.canBePaidWithAvaliable(ColorSet.fromNames( boolean canPayWithAvailableColors = cost.canBePaidWithAvailable(ColorSet.fromNames(
ComputerUtilCost.getAvailableManaColors(ai, sa.getHostCard())).getColor()); ComputerUtilCost.getAvailableManaColors(ai, sa.getHostCard())).getColor());
byte colorProfile = cost.getColorProfile(); byte colorProfile = cost.getColorProfile();

View File

@@ -233,10 +233,10 @@ public class EffectAi extends SpellAbilityAi {
return ai.getCreaturesInPlay().size() >= i; return ai.getCreaturesInPlay().size() >= i;
} }
return true; return true;
} else if (logic.equals("CastFromGraveThisTurn")) { } else if (logic.equals("ReplaySpell")) {
CardCollection list = new CardCollection(game.getCardsIn(ZoneType.Graveyard)); CardCollection list = new CardCollection(game.getCardsIn(ZoneType.Graveyard));
list = CardLists.getValidCards(list, sa.getTargetRestrictions().getValidTgts(), ai, sa.getHostCard(), sa); list = CardLists.getValidCards(list, sa.getTargetRestrictions().getValidTgts(), ai, sa.getHostCard(), sa);
if (!ComputerUtil.targetPlayableSpellCard(ai, list, sa, false)) { if (!ComputerUtil.targetPlayableSpellCard(ai, list, sa, false, false)) {
return false; return false;
} }
} else if (logic.equals("Bribe")) { } else if (logic.equals("Bribe")) {
@@ -313,6 +313,5 @@ public class EffectAi extends SpellAbilityAi {
} }
return super.doTriggerAINoCost(aiPlayer, sa, mandatory); return super.doTriggerAINoCost(aiPlayer, sa, mandatory);
} }
} }

View File

@@ -167,7 +167,7 @@ public class ManaEffectAi extends SpellAbilityAi {
List<SpellAbility> all = ComputerUtilAbility.getSpellAbilities(ai.getCardsIn(ZoneType.Hand), ai); List<SpellAbility> all = ComputerUtilAbility.getSpellAbilities(ai.getCardsIn(ZoneType.Hand), ai);
for (final SpellAbility testSa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, ai)) { for (final SpellAbility testSa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, ai)) {
ManaCost cost = testSa.getPayCosts().getTotalMana(); ManaCost cost = testSa.getPayCosts().getTotalMana();
boolean canPayWithAvailableColors = cost.canBePaidWithAvaliable(ColorSet.fromNames( boolean canPayWithAvailableColors = cost.canBePaidWithAvailable(ColorSet.fromNames(
ComputerUtilCost.getAvailableManaColors(ai, (List<Card>)null)).getColor()); ComputerUtilCost.getAvailableManaColors(ai, (List<Card>)null)).getColor());
if (cost.getCMC() == 0 && cost.countX() == 0) { if (cost.getCMC() == 0 && cost.countX() == 0) {

View File

@@ -40,33 +40,9 @@ public class PlayAi extends SpellAbilityAi {
return false; // prevent infinite loop return false; // prevent infinite loop
} }
CardCollection cards = null; CardCollection cards = getPlayableCards(sa, ai);
final TargetRestrictions tgt = sa.getTargetRestrictions(); if (cards.isEmpty()) {
if (tgt != null) { return false;
ZoneType zone = tgt.getZone().get(0);
cards = CardLists.getValidCards(game.getCardsIn(zone), tgt.getValidTgts(), ai, source, sa);
if (cards.isEmpty()) {
return false;
}
} else if (!sa.hasParam("Valid")) {
cards = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
if (cards.isEmpty()) {
return false;
}
}
if (cards != null & sa.hasParam("ValidSA")) {
final String valid[] = sa.getParam("ValidSA").split(",");
final Iterator<Card> itr = cards.iterator();
while (itr.hasNext()) {
final Card c = itr.next();
if (!Iterables.any(AbilityUtils.getBasicSpellsFromPlayEffect(c, ai), SpellAbilityPredicates.isValid(valid, ai , c, sa))) {
itr.remove();
}
}
if (cards.isEmpty()) {
return false;
}
} }
if (game.getRules().hasAppliedVariant(GameType.MoJhoSto) && source.getName().equals("Jhoira of the Ghitu Avatar")) { if (game.getRules().hasAppliedVariant(GameType.MoJhoSto) && source.getName().equals("Jhoira of the Ghitu Avatar")) {
@@ -85,25 +61,15 @@ public class PlayAi extends SpellAbilityAi {
} }
} }
// Ensure that if a ValidZone is specified, there's at least something to choose from in that zone.
CardCollectionView validOpts = new CardCollection();
if (sa.hasParam("ValidZone")) {
validOpts = AbilityUtils.filterListByType(game.getCardsIn(ZoneType.valueOf(sa.getParam("ValidZone"))),
sa.getParam("Valid"), sa);
if (validOpts.isEmpty()) {
return false;
}
}
if ("ReplaySpell".equals(logic)) { if ("ReplaySpell".equals(logic)) {
return ComputerUtil.targetPlayableSpellCard(ai, cards, sa, sa.hasParam("WithoutManaCost")); return ComputerUtil.targetPlayableSpellCard(ai, cards, sa, sa.hasParam("WithoutManaCost"), false);
} else if (logic.startsWith("NeedsChosenCard")) { } else if (logic.startsWith("NeedsChosenCard")) {
int minCMC = 0; int minCMC = 0;
if (sa.getPayCosts().getCostMana() != null) { if (sa.getPayCosts().getCostMana() != null) {
minCMC = sa.getPayCosts().getTotalMana().getCMC(); minCMC = sa.getPayCosts().getTotalMana().getCMC();
} }
validOpts = CardLists.filter(validOpts, CardPredicates.greaterCMC(minCMC)); cards = CardLists.filter(cards, CardPredicates.greaterCMC(minCMC));
return chooseSingleCard(ai, sa, validOpts, sa.hasParam("Optional"), null, null) != null; return chooseSingleCard(ai, sa, cards, sa.hasParam("Optional"), null, null) != null;
} else if ("WithTotalCMC".equals(logic)) { } else if ("WithTotalCMC".equals(logic)) {
// Try to play only when there are more than three playable cards. // Try to play only when there are more than three playable cards.
if (cards.size() < 3) if (cards.size() < 3)
@@ -154,6 +120,10 @@ public class PlayAi extends SpellAbilityAi {
return false; return false;
} }
if ("ReplaySpell".equals(sa.getParam("AILogic"))) {
return ComputerUtil.targetPlayableSpellCard(ai, getPlayableCards(sa, ai), sa, sa.hasParam("WithoutManaCost"), mandatory);
}
return checkApiLogic(ai, sa); return checkApiLogic(ai, sa);
} }
@@ -218,4 +188,36 @@ public class PlayAi extends SpellAbilityAi {
}); });
return ComputerUtilCard.getBestAI(tgtCards); return ComputerUtilCard.getBestAI(tgtCards);
} }
private static CardCollection getPlayableCards(SpellAbility sa, Player ai) {
CardCollection cards = new CardCollection();
final TargetRestrictions tgt = sa.getTargetRestrictions();
final Card source = sa.getHostCard();
if (tgt != null) {
ZoneType zone = tgt.getZone().get(0);
cards = CardLists.getValidCards(ai.getGame().getCardsIn(zone), tgt.getValidTgts(), ai, source, sa);
} else if (!sa.hasParam("Valid")) {
cards = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
}
if (cards != null & sa.hasParam("ValidSA")) {
final String valid[] = sa.getParam("ValidSA").split(",");
final Iterator<Card> itr = cards.iterator();
while (itr.hasNext()) {
final Card c = itr.next();
if (!Iterables.any(AbilityUtils.getBasicSpellsFromPlayEffect(c, ai), SpellAbilityPredicates.isValid(valid, ai , c, sa))) {
itr.remove();
}
}
}
// Ensure that if a ValidZone is specified, there's at least something to choose from in that zone.
if (sa.hasParam("ValidZone")) {
cards = new CardCollection(AbilityUtils.filterListByType(ai.getGame().getCardsIn(ZoneType.valueOf(sa.getParam("ValidZone"))),
sa.getParam("Valid"), sa));
}
return cards;
}
} }

View File

@@ -596,7 +596,7 @@ public class PumpAi extends PumpAiBase {
} }
if ("Snapcaster".equals(sa.getParam("AILogic"))) { if ("Snapcaster".equals(sa.getParam("AILogic"))) {
if (!ComputerUtil.targetPlayableSpellCard(ai, list, sa, false)) { if (!ComputerUtil.targetPlayableSpellCard(ai, list, sa, false, false)) {
return false; return false;
} }
} }

View File

@@ -107,7 +107,7 @@ public class RearrangeTopOfLibraryAi extends SpellAbilityAi {
uncastableCMCThreshold = aic.getIntProperty(AiProps.SCRY_IMMEDIATELY_UNCASTABLE_CMC_DIFF); uncastableCMCThreshold = aic.getIntProperty(AiProps.SCRY_IMMEDIATELY_UNCASTABLE_CMC_DIFF);
} }
Player p = pc.getFirst(); // FIXME: is this always a single target spell? Player p = pc.getFirst(); // currently always a single target spell
Card top = p.getCardsIn(ZoneType.Library).getFirst(); Card top = p.getCardsIn(ZoneType.Library).getFirst();
int landsOTB = CardLists.filter(p.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.LANDS_PRODUCING_MANA).size(); int landsOTB = CardLists.filter(p.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.LANDS_PRODUCING_MANA).size();
int cmc = top.isSplitCard() ? Math.min(top.getCMC(Card.SplitCMCMode.LeftSplitCMC), top.getCMC(Card.SplitCMCMode.RightSplitCMC)) int cmc = top.isSplitCard() ? Math.min(top.getCMC(Card.SplitCMCMode.LeftSplitCMC), top.getCMC(Card.SplitCMCMode.RightSplitCMC))

View File

@@ -182,7 +182,7 @@ public final class CardRules implements ICardCharacteristics {
//if card face has no cost, assume castable only by mana of its defined color //if card face has no cost, assume castable only by mana of its defined color
return face.getColor().hasNoColorsExcept(colorCode); return face.getColor().hasNoColorsExcept(colorCode);
} }
return face.getManaCost().canBePaidWithAvaliable(colorCode); return face.getManaCost().canBePaidWithAvailable(colorCode);
} }
public boolean canCastWithAvailable(byte colorCode) { public boolean canCastWithAvailable(byte colorCode) {

View File

@@ -353,7 +353,7 @@ public final class ManaCost implements Comparable<ManaCost>, Iterable<ManaCostSh
* @param colorCode * @param colorCode
* @return * @return
*/ */
public boolean canBePaidWithAvaliable(byte colorCode) { public boolean canBePaidWithAvailable(byte colorCode) {
for (ManaCostShard shard : shards) { for (ManaCostShard shard : shards) {
if (!shard.isPhyrexian() && !shard.canBePaidWithManaOfColor(colorCode)) { if (!shard.isPhyrexian() && !shard.canBePaidWithManaOfColor(colorCode)) {
return false; return false;

View File

@@ -82,7 +82,6 @@ public class CopySpellAbilityEffect extends SpellAbilityEffect {
boolean isOptional = sa.hasParam("Optional"); boolean isOptional = sa.hasParam("Optional");
for (Player controller : controllers) { for (Player controller : controllers) {
List<SpellAbility> copies = Lists.newArrayList(); List<SpellAbility> copies = Lists.newArrayList();
SpellAbility chosenSA = controller.getController().chooseSingleSpellForEffect(tgtSpells, sa, SpellAbility chosenSA = controller.getController().chooseSingleSpellForEffect(tgtSpells, sa,

View File

@@ -6342,7 +6342,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
} }
if (isFaceDown() && isInZone(ZoneType.Exile)) { if (isFaceDown() && isInZone(ZoneType.Exile)) {
for (final SpellAbility sa : getState(CardStateName.Original).getSpellAbilities()) { for (final SpellAbility sa : oState.getSpellAbilities()) {
abilities.addAll(GameActionUtil.getAlternativeCosts(sa, player)); abilities.addAll(GameActionUtil.getAlternativeCosts(sa, player));
} }
} }

View File

@@ -122,7 +122,7 @@ public class CardLists {
*/ */
public static void sortByCmcDesc(final List<Card> list) { public static void sortByCmcDesc(final List<Card> list) {
Collections.sort(list, CmcComparatorInv); Collections.sort(list, CmcComparatorInv);
} // sortByCmcDesc }
/** /**
* <p> * <p>
@@ -133,7 +133,7 @@ public class CardLists {
*/ */
public static void sortByToughnessAsc(final List<Card> list) { public static void sortByToughnessAsc(final List<Card> list) {
Collections.sort(list, ToughnessComparator); Collections.sort(list, ToughnessComparator);
} // sortByToughnessAsc() }
/** /**
* <p> * <p>
@@ -144,7 +144,7 @@ public class CardLists {
*/ */
public static void sortByToughnessDesc(final List<Card> list) { public static void sortByToughnessDesc(final List<Card> list) {
Collections.sort(list, ToughnessComparatorInv); Collections.sort(list, ToughnessComparatorInv);
} // sortByToughnessDesc() }
/** /**
* <p> * <p>
@@ -155,7 +155,7 @@ public class CardLists {
*/ */
public static void sortByPowerAsc(final List<Card> list) { public static void sortByPowerAsc(final List<Card> list) {
Collections.sort(list, PowerComparator); Collections.sort(list, PowerComparator);
} // sortAttackLowFirst() }
// the higher the attack the better // the higher the attack the better
/** /**
@@ -167,7 +167,7 @@ public class CardLists {
*/ */
public static void sortByPowerDesc(final List<Card> list) { public static void sortByPowerDesc(final List<Card> list) {
Collections.sort(list, Collections.reverseOrder(PowerComparator)); Collections.sort(list, Collections.reverseOrder(PowerComparator));
} // sortAttack() }
/** /**

View File

@@ -63,8 +63,6 @@ public final class AbilitySub extends SpellAbility implements java.io.Serializab
return this.parent; return this.parent;
} }
/** {@inheritDoc} */ /** {@inheritDoc} */
@Override @Override
public boolean canPlay() { public boolean canPlay() {
@@ -72,10 +70,8 @@ public final class AbilitySub extends SpellAbility implements java.io.Serializab
return false; return false;
} }
private final SpellAbilityEffect effect; private final SpellAbilityEffect effect;
public AbilitySub(ApiType api0, final Card ca, final TargetRestrictions tgt, Map<String, String> params0) { public AbilitySub(ApiType api0, final Card ca, final TargetRestrictions tgt, Map<String, String> params0) {
super(ca, Cost.Zero); super(ca, Cost.Zero);
this.setTargetRestrictions(tgt); this.setTargetRestrictions(tgt);

View File

@@ -20,7 +20,7 @@ Colors:blue
Types:Legendary Planeswalker Jace Types:Legendary Planeswalker Jace
Loyalty:5 Loyalty:5
A:AB$ Pump | Cost$ AddCounter<1/LOYALTY> | Planeswalker$ True | NumAtt$ -2 | IsCurse$ True | Duration$ UntilYourNextTurn | TargetMin$ 0 | TargetMax$ 1 | ValidTgts$ Creature | TgtPrompt$ Select target creature | SpellDescription$ Up to one target creature gets -2/-0 until your next turn. A:AB$ Pump | Cost$ AddCounter<1/LOYALTY> | Planeswalker$ True | NumAtt$ -2 | IsCurse$ True | Duration$ UntilYourNextTurn | TargetMin$ 0 | TargetMax$ 1 | ValidTgts$ Creature | TgtPrompt$ Select target creature | SpellDescription$ Up to one target creature gets -2/-0 until your next turn.
A:AB$ Effect | Cost$ SubCounter<3/LOYALTY> | Planeswalker$ True | AILogic$ CastFromGraveThisTurn | ValidTgts$ Instant.YouCtrl,Sorcery.YouCtrl | TgtZone$ Graveyard | TgtPrompt$ Select target instant or sorcery card | RememberObjects$ Targeted | StaticAbilities$ Play | ExileOnMoved$ Graveyard | SubAbility$ DBEffect | SpellDescription$ You may cast target instant or sorcery card from your graveyard this turn. If that spell would be put into your graveyard this turn, exile it instead. A:AB$ Effect | Cost$ SubCounter<3/LOYALTY> | Planeswalker$ True | AILogic$ ReplaySpell | ValidTgts$ Instant.YouCtrl,Sorcery.YouCtrl | TgtZone$ Graveyard | TgtPrompt$ Select target instant or sorcery card | RememberObjects$ Targeted | StaticAbilities$ Play | ExileOnMoved$ Graveyard | SubAbility$ DBEffect | SpellDescription$ You may cast target instant or sorcery card from your graveyard this turn. If that spell would be put into your graveyard this turn, exile it instead.
SVar:Play:Mode$ Continuous | MayPlay$ True | EffectZone$ Command | Affected$ Card.IsRemembered | AffectedZone$ Graveyard | Description$ You may play remembered card. SVar:Play:Mode$ Continuous | MayPlay$ True | EffectZone$ Command | Affected$ Card.IsRemembered | AffectedZone$ Graveyard | Description$ You may play remembered card.
SVar:DBEffect:DB$ Effect | RememberObjects$ Targeted | ExileOnMoved$ Stack | ReplacementEffects$ ReplaceGraveyard SVar:DBEffect:DB$ Effect | RememberObjects$ Targeted | ExileOnMoved$ Stack | ReplacementEffects$ ReplaceGraveyard
SVar:ReplaceGraveyard:Event$ Moved | ValidCard$ Card.IsRemembered | Origin$ Stack | Destination$ Graveyard | ReplaceWith$ MoveExile | Description$ If that card would be put into your graveyard this turn, exile it instead. SVar:ReplaceGraveyard:Event$ Moved | ValidCard$ Card.IsRemembered | Origin$ Stack | Destination$ Graveyard | ReplaceWith$ MoveExile | Description$ If that card would be put into your graveyard this turn, exile it instead.

View File

@@ -2,7 +2,7 @@ Name:Mission Briefing
ManaCost:U U ManaCost:U U
Types:Instant Types:Instant
A:SP$ Surveil | Cost$ U U | Amount$ 2 | SubAbility$ DBChooseCard | SpellDescription$ Surveil 2, then choose an instant or sorcery card in your graveyard. You may cast it this turn. If that spell would be put into your graveyard this turn, exile it instead. (To surveil 2, look at the top two cards of your library, then put any number of them into your graveyard and the rest on top of your library in any order.) A:SP$ Surveil | Cost$ U U | Amount$ 2 | SubAbility$ DBChooseCard | SpellDescription$ Surveil 2, then choose an instant or sorcery card in your graveyard. You may cast it this turn. If that spell would be put into your graveyard this turn, exile it instead. (To surveil 2, look at the top two cards of your library, then put any number of them into your graveyard and the rest on top of your library in any order.)
SVar:DBChooseCard:DB$ ChooseCard | Choices$ Instant.YouCtrl,Sorcery.YouCtrl | ChoiceZone$ Graveyard | AILogic$ CastFromGraveThisTurn | Mandatory$ True | RememberChosen$ True | SubAbility$ DBEffect | SpellDescription$ You may cast that card this turn. If that card would be put into your graveyard this turn, exile it instead. SVar:DBChooseCard:DB$ ChooseCard | Choices$ Instant.YouCtrl,Sorcery.YouCtrl | ChoiceZone$ Graveyard | Mandatory$ True | RememberChosen$ True | SubAbility$ DBEffect | SpellDescription$ You may cast that card this turn. If that card would be put into your graveyard this turn, exile it instead.
SVar:DBEffect:DB$ Effect | RememberObjects$ RememberedCard | StaticAbilities$ Play | SubAbility$ DBCleanup | ExileOnMoved$ Stack | ReplacementEffects$ ReplaceGraveyard SVar:DBEffect:DB$ Effect | RememberObjects$ RememberedCard | StaticAbilities$ Play | SubAbility$ DBCleanup | ExileOnMoved$ Stack | ReplacementEffects$ ReplaceGraveyard
SVar:Play:Mode$ Continuous | MayPlay$ True | EffectZone$ Command | Affected$ Card.IsRemembered | AffectedZone$ Graveyard | Description$ You may play remembered card. SVar:Play:Mode$ Continuous | MayPlay$ True | EffectZone$ Command | Affected$ Card.IsRemembered | AffectedZone$ Graveyard | Description$ You may play remembered card.
SVar:ReplaceGraveyard:Event$ Moved | ValidCard$ Card.IsRemembered | Origin$ Stack | Destination$ Graveyard | ReplaceWith$ MoveExile | Description$ If that card would be put into your graveyard this turn, exile it instead. SVar:ReplaceGraveyard:Event$ Moved | ValidCard$ Card.IsRemembered | Origin$ Stack | Destination$ Graveyard | ReplaceWith$ MoveExile | Description$ If that card would be put into your graveyard this turn, exile it instead.

View File

@@ -4,6 +4,6 @@ Types:Artifact Equipment
K:Equip:2 K:Equip:2
S:Mode$ Continuous | Affected$ Creature.EquippedBy | AddPower$ 1 | AddToughness$ 1 | AddTrigger$ AttackTrigger | Description$ Invoke Duplicity — Equipped creature gets +1/+1 and has "Whenever this creature deals combat damage to a player, you may sacrifice CARDNAME. If you do, create a token that's a copy of this creature." S:Mode$ Continuous | Affected$ Creature.EquippedBy | AddPower$ 1 | AddToughness$ 1 | AddTrigger$ AttackTrigger | Description$ Invoke Duplicity — Equipped creature gets +1/+1 and has "Whenever this creature deals combat damage to a player, you may sacrifice CARDNAME. If you do, create a token that's a copy of this creature."
SVar:AttackTrigger:Mode$ DamageDone | ValidSource$ Card.Self | ValidTarget$ Player | CombatDamage$ True | TriggerZones$ Battlefield | Execute$ TrigCopy | TriggerDescription$ Whenever this creature deals combat damage to a player, you may sacrifice Trickster's Talisman. If you do, create a token that's a copy of this creature. SVar:AttackTrigger:Mode$ DamageDone | ValidSource$ Card.Self | ValidTarget$ Player | CombatDamage$ True | TriggerZones$ Battlefield | Execute$ TrigCopy | TriggerDescription$ Whenever this creature deals combat damage to a player, you may sacrifice Trickster's Talisman. If you do, create a token that's a copy of this creature.
SVar:TrigCopy:AB$ CopyPermanent | Cost$ Sac<1/OriginalHost/Trickster's Talisman> | Defined$ Self | NumCopies$ 1 | AILogic$ DuplicatePerms SVar:TrigCopy:AB$ CopyPermanent | Cost$ Sac<1/OriginalHost/Trickster's Talisman> | Defined$ Self | NumCopies$ 1
DeckHas:Ability$Token DeckHas:Ability$Token
Oracle:Invoke Duplicity — Equipped creature gets +1/+1 and has "Whenever this creature deals combat damage to a player, you may sacrifice Trickster's Talisman. If you do, create a token that's a copy of this creature."\nEquip {2} Oracle:Invoke Duplicity — Equipped creature gets +1/+1 and has "Whenever this creature deals combat damage to a player, you may sacrifice Trickster's Talisman. If you do, create a token that's a copy of this creature."\nEquip {2}

View File

@@ -32,7 +32,7 @@ Strixhaven Stadium|The Mage Tower|And that's game, set, and match!
Test of Endurance|The Test|So... did I pass? Test of Endurance|The Test|So... did I pass?
Thassa's Oracle|The Prophecy of Victory|I see... nothing. We must've won. Thassa's Oracle|The Prophecy of Victory|I see... nothing. We must've won.
The Cheese Stands Alone|The Cheese|It's cheesy, but hey, it works! The Cheese Stands Alone|The Cheese|It's cheesy, but hey, it works!
The Deck of Many Things|Down on the Deck|Lucky draw! The Deck of Many Things's Effect|Down on the Deck|Lucky draw!
Triskaidekaphobia|The Fear of 13|It's just a silly ancient superstition... right? Triskaidekaphobia|The Fear of 13|It's just a silly ancient superstition... right?
Vorpal Sword|Snicker-Snack!|He left it dead, and with its head / He went galumphing back. Vorpal Sword|Snicker-Snack!|He left it dead, and with its head / He went galumphing back.
Emblem - Vraska, Golgari Queen|The Flurry of Assassins|How good is your dodging? Emblem - Vraska, Golgari Queen|The Flurry of Assassins|How good is your dodging?

View File

@@ -697,9 +697,9 @@ public enum ColumnDef {
return !(((IPaperCard) i).getRules().getType().isArtifact() && (toColor(i).isColorless() || return !(((IPaperCard) i).getRules().getType().isArtifact() && (toColor(i).isColorless() ||
//If it isn't colorless, see if it can be paid with only white, only blue, only black. //If it isn't colorless, see if it can be paid with only white, only blue, only black.
//No need to check others since three-color hybrid shards don't exist. //No need to check others since three-color hybrid shards don't exist.
manaCost.canBePaidWithAvaliable(MagicColor.WHITE) && manaCost.canBePaidWithAvailable(MagicColor.WHITE) &&
manaCost.canBePaidWithAvaliable(MagicColor.BLUE) && manaCost.canBePaidWithAvailable(MagicColor.BLUE) &&
manaCost.canBePaidWithAvaliable(MagicColor.BLACK))) manaCost.canBePaidWithAvailable(MagicColor.BLACK)))
? "0" + toSplitLast(i) : "1"; ? "0" + toSplitLast(i) : "1";
} }
@@ -757,9 +757,9 @@ public enum ColumnDef {
private static String toGoldFirst(final InventoryItem i) { private static String toGoldFirst(final InventoryItem i) {
forge.card.mana.ManaCost manaCost = ((IPaperCard) i).getRules().getManaCost(); forge.card.mana.ManaCost manaCost = ((IPaperCard) i).getRules().getManaCost();
return !(manaCost.canBePaidWithAvaliable(MagicColor.WHITE) | manaCost.canBePaidWithAvaliable(MagicColor.BLUE) | return !(manaCost.canBePaidWithAvailable(MagicColor.WHITE) | manaCost.canBePaidWithAvailable(MagicColor.BLUE) |
manaCost.canBePaidWithAvaliable(MagicColor.BLACK) | manaCost.canBePaidWithAvaliable(MagicColor.RED) | manaCost.canBePaidWithAvailable(MagicColor.BLACK) | manaCost.canBePaidWithAvailable(MagicColor.RED) |
manaCost.canBePaidWithAvaliable(MagicColor.GREEN)) ? "0" : "1"; manaCost.canBePaidWithAvailable(MagicColor.GREEN)) ? "0" : "1";
} }
/** /**

View File

@@ -90,12 +90,12 @@ public abstract class AchievementCollection implements Iterable<Achievement> {
filename = filename0; filename = filename0;
isLimitedFormat = isLimitedFormat0; isLimitedFormat = isLimitedFormat0;
path = path0; path = path0;
addSharedAchivements(); addSharedAchievements();
addAchievements(); addAchievements();
load(); load();
} }
protected void addSharedAchivements() { protected void addSharedAchievements() {
add(new GameWinStreak(10, 25, 50, 100)); add(new GameWinStreak(10, 25, 50, 100));
add(new MatchWinStreak(10, 25, 50, 100)); add(new MatchWinStreak(10, 25, 50, 100));
add(new TotalGameWins(250, 500, 1000, 2000)); add(new TotalGameWins(250, 500, 1000, 2000));

View File

@@ -18,7 +18,7 @@ public class AltWinAchievements extends AchievementCollection {
} }
@Override @Override
protected void addSharedAchivements() { protected void addSharedAchievements() {
//prevent including shared achievements //prevent including shared achievements
} }

View File

@@ -15,7 +15,7 @@ public class ChallengeAchievements extends AchievementCollection {
} }
@Override @Override
protected void addSharedAchivements() { protected void addSharedAchievements() {
//prevent including shared achievements //prevent including shared achievements
} }

View File

@@ -23,7 +23,7 @@ public class PlaneswalkerAchievements extends AchievementCollection {
} }
@Override @Override
protected void addSharedAchivements() { protected void addSharedAchievements() {
//prevent including shared achievements //prevent including shared achievements
} }

View File

@@ -8,7 +8,7 @@ public class PuzzleAchievements extends AchievementCollection {
} }
@Override @Override
protected void addSharedAchivements() { protected void addSharedAchievements() {
//prevent including shared achievements //prevent including shared achievements
} }