Merge remote-tracking branch 'core/master' into AdventureModePort

This commit is contained in:
Anthony Calosa
2022-02-10 22:09:24 +08:00
65 changed files with 239 additions and 114 deletions

View File

@@ -153,10 +153,15 @@ public class AiAttackController {
} }
} }
/** Choose opponent for AI to attack here. Expand as necessary. */ /**
* Choose opponent for AI to attack here. Expand as necessary.
* No strategy to secure a second place instead, since Forge has no variant for that
*/
public static Player choosePreferredDefenderPlayer(Player ai) { public static Player choosePreferredDefenderPlayer(Player ai) {
Player defender = ai.getWeakestOpponent(); //Concentrate on opponent within easy kill range Player defender = ai.getWeakestOpponent(); //Concentrate on opponent within easy kill range
// TODO connect with evaluateBoardPosition and only fall back to random when no player is the biggest threat by a fair margin
if (defender.getLife() > 8) { //Otherwise choose a random opponent to ensure no ganging up on players if (defender.getLife() > 8) { //Otherwise choose a random opponent to ensure no ganging up on players
// TODO should we cache the random for each turn? some functions like shouldPumpCard base their decisions on the assumption who will be attacked // TODO should we cache the random for each turn? some functions like shouldPumpCard base their decisions on the assumption who will be attacked
return ai.getOpponents().get(MyRandom.getRandom().nextInt(ai.getOpponents().size())); return ai.getOpponents().get(MyRandom.getRandom().nextInt(ai.getOpponents().size()));
@@ -720,7 +725,7 @@ public class AiAttackController {
continue; continue;
} }
boolean mustAttack = false; boolean mustAttack = false;
// TODO for nextTurn check if it was temporary // TODO this might result into attacking the wrong player
if (attacker.isGoaded()) { if (attacker.isGoaded()) {
mustAttack = true; mustAttack = true;
} else if (attacker.getSVar("MustAttack").equals("True")) { } else if (attacker.getSVar("MustAttack").equals("True")) {
@@ -737,7 +742,7 @@ public class AiAttackController {
mustAttack = true; mustAttack = true;
} }
} }
if (mustAttack || (attacker.getController().getMustAttackEntity() != null && nextTurn) || (attacker.getController().getMustAttackEntityThisTurn() != null && !nextTurn)) { if (mustAttack ||attacker.getController().getMustAttackEntityThisTurn() != null) {
combat.addAttacker(attacker, defender); combat.addAttacker(attacker, defender);
attackersLeft.remove(attacker); attackersLeft.remove(attacker);
numForcedAttackers++; numForcedAttackers++;

View File

@@ -426,7 +426,6 @@ public class AiBlockController {
} }
attackersLeft = new ArrayList<>(currentAttackers); attackersLeft = new ArrayList<>(currentAttackers);
currentAttackers = new ArrayList<>(attackersLeft);
boolean considerTripleBlock = true; boolean considerTripleBlock = true;
@@ -437,6 +436,11 @@ public class AiBlockController {
continue; continue;
} }
// AI can't handle good blocks with more than three creatures yet
if (CombatUtil.getMinNumBlockersForAttacker(attacker, ai) > (considerTripleBlock ? 3 : 2)) {
continue;
}
int evalAttackerValue = ComputerUtilCard.evaluateCreature(attacker); int evalAttackerValue = ComputerUtilCard.evaluateCreature(attacker);
blockers = getPossibleBlockers(combat, attacker, blockersLeft, false); blockers = getPossibleBlockers(combat, attacker, blockersLeft, false);
@@ -446,11 +450,6 @@ public class AiBlockController {
int currentValue; // The value of the creatures in the blockgang int currentValue; // The value of the creatures in the blockgang
boolean foundDoubleBlock = false; // if true, a good double block is found boolean foundDoubleBlock = false; // if true, a good double block is found
// AI can't handle good blocks with more than three creatures yet
if (CombatUtil.getMinNumBlockersForAttacker(attacker, ai) > (considerTripleBlock ? 3 : 2)) {
continue;
}
// Try to add blockers that could be destroyed, but are worth less than the attacker // Try to add blockers that could be destroyed, but are worth less than the attacker
// Don't use blockers without First Strike or Double Strike if attacker has it // Don't use blockers without First Strike or Double Strike if attacker has it
usableBlockers = CardLists.filter(blockers, new Predicate<Card>() { usableBlockers = CardLists.filter(blockers, new Predicate<Card>() {
@@ -460,8 +459,7 @@ public class AiBlockController {
&& !ComputerUtilCombat.dealsFirstStrikeDamage(c, false, combat)) { && !ComputerUtilCombat.dealsFirstStrikeDamage(c, false, combat)) {
return false; return false;
} }
final boolean randomTrade = wouldLikeToRandomlyTrade(attacker, c, combat); return lifeInDanger || wouldLikeToRandomlyTrade(attacker, c, combat) || ComputerUtilCard.evaluateCreature(c) + diff < ComputerUtilCard.evaluateCreature(attacker);
return lifeInDanger || randomTrade || ComputerUtilCard.evaluateCreature(c) + diff < ComputerUtilCard.evaluateCreature(attacker);
} }
}); });
if (usableBlockers.size() < 2) { if (usableBlockers.size() < 2) {

View File

@@ -1343,7 +1343,7 @@ public class ComputerUtil {
} }
final CardCollection typeList = final CardCollection typeList =
CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(","), source.getController(), source, sa); CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type, source.getController(), source, sa);
for (Card c : typeList) { for (Card c : typeList) {
if (c.getSVar("SacMe").equals("6")) { if (c.getSVar("SacMe").equals("6")) {
return true; return true;
@@ -1620,7 +1620,7 @@ public class ComputerUtil {
objects = AbilityUtils.getDefinedObjects(source, topStack.getParam("Defined"), topStack); objects = AbilityUtils.getDefinedObjects(source, topStack.getParam("Defined"), topStack);
} else if (topStack.hasParam("ValidCards")) { } else if (topStack.hasParam("ValidCards")) {
CardCollectionView battleField = aiPlayer.getCardsIn(ZoneType.Battlefield); CardCollectionView battleField = aiPlayer.getCardsIn(ZoneType.Battlefield);
objects = CardLists.getValidCards(battleField, topStack.getParam("ValidCards").split(","), source.getController(), source, topStack); objects = CardLists.getValidCards(battleField, topStack.getParam("ValidCards"), source.getController(), source, topStack);
} else { } else {
return threatened; return threatened;
} }
@@ -2822,8 +2822,6 @@ public class ComputerUtil {
pRating /= 5; pRating /= 5;
} }
System.out.println("Board position evaluation for " + p + ": " + pRating);
if (pRating > bestBoardRating) { if (pRating > bestBoardRating) {
bestBoardRating = pRating; bestBoardRating = pRating;
bestBoardPosition = p; bestBoardPosition = p;

View File

@@ -36,7 +36,7 @@ public class ComputerUtilAbility {
public boolean apply(final Card c) { public boolean apply(final Card c) {
if (!c.getSVar("NeedsToPlay").isEmpty()) { if (!c.getSVar("NeedsToPlay").isEmpty()) {
final String needsToPlay = c.getSVar("NeedsToPlay"); final String needsToPlay = c.getSVar("NeedsToPlay");
CardCollection list = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), needsToPlay.split(","), c.getController(), c, null); CardCollection list = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), needsToPlay, c.getController(), c, null);
if (list.isEmpty()) { if (list.isEmpty()) {
return false; return false;
} }

View File

@@ -1951,7 +1951,7 @@ public class ComputerUtilCard {
CardCollectionView list = game.getCardsIn(ZoneType.Battlefield); CardCollectionView list = game.getCardsIn(ZoneType.Battlefield);
list = CardLists.getValidCards(list, needsToPlay.split(","), card.getController(), card, sa); list = CardLists.getValidCards(list, needsToPlay, card.getController(), card, sa);
if (list.isEmpty()) { if (list.isEmpty()) {
return AiPlayDecision.MissingNeededCards; return AiPlayDecision.MissingNeededCards;
} }

View File

@@ -830,7 +830,7 @@ public class ComputerUtilCombat {
} }
// defender == null means unblocked // defender == null means unblocked
if ((defender == null) && mode == TriggerType.AttackerUnblocked) { if (defender == null && mode == TriggerType.AttackerUnblocked) {
willTrigger = true; willTrigger = true;
if (!trigger.matchesValidParam("ValidCard", attacker)) { if (!trigger.matchesValidParam("ValidCard", attacker)) {
return false; return false;

View File

@@ -144,7 +144,7 @@ public class ComputerUtilCost {
return true; return true;
} }
} }
final CardCollection typeList = CardLists.getValidCards(hand, type.split(","), source.getController(), source, sa); final CardCollection typeList = CardLists.getValidCards(hand, type, source.getController(), source, sa);
if (typeList.size() > ai.getMaxHandSize()) { if (typeList.size() > ai.getMaxHandSize()) {
continue; continue;
} }

View File

@@ -69,14 +69,14 @@ public class AnimateAi extends SpellAbilityAi {
final String valid = topStack.getParamOrDefault("SacValid", "Card.Self"); final String valid = topStack.getParamOrDefault("SacValid", "Card.Self");
String num = topStack.getParamOrDefault("Amount", "1"); String num = topStack.getParamOrDefault("Amount", "1");
final int nToSac = AbilityUtils.calculateAmount(topStack.getHostCard(), num, topStack); final int nToSac = AbilityUtils.calculateAmount(topStack.getHostCard(), num, topStack);
CardCollection list = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid.split(","), CardCollection list = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid,
ai.getWeakestOpponent(), topStack.getHostCard(), topStack); ai.getWeakestOpponent(), topStack.getHostCard(), topStack);
list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(topStack, true)); list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(topStack, true));
ComputerUtilCard.sortByEvaluateCreature(list); ComputerUtilCard.sortByEvaluateCreature(list);
if (!list.isEmpty() && list.size() == nToSac && ComputerUtilCost.canPayCost(sa, ai, sa.isTrigger())) { if (!list.isEmpty() && list.size() == nToSac && ComputerUtilCost.canPayCost(sa, ai, sa.isTrigger())) {
Card animatedCopy = becomeAnimated(source, sa); Card animatedCopy = becomeAnimated(source, sa);
list.add(animatedCopy); list.add(animatedCopy);
list = CardLists.getValidCards(list, valid.split(","), ai.getWeakestOpponent(), topStack.getHostCard(), list = CardLists.getValidCards(list, valid, ai.getWeakestOpponent(), topStack.getHostCard(),
topStack); topStack);
list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(topStack, true)); list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(topStack, true));
if (ComputerUtilCard.evaluateCreature(animatedCopy) < ComputerUtilCard.evaluateCreature(list.get(0)) if (ComputerUtilCard.evaluateCreature(animatedCopy) < ComputerUtilCard.evaluateCreature(list.get(0))

View File

@@ -184,7 +184,7 @@ public class ChooseCardAi extends SpellAbilityAi {
choice = ComputerUtilCard.getBestCreatureAI(options); choice = ComputerUtilCard.getBestCreatureAI(options);
} else if (logic.equals("Clone")) { } else if (logic.equals("Clone")) {
final String filter = "Permanent.YouDontCtrl,Permanent.nonLegendary"; final String filter = "Permanent.YouDontCtrl,Permanent.nonLegendary";
CardCollection newOptions = CardLists.getValidCards(options, filter.split(","), ctrl, host, sa); CardCollection newOptions = CardLists.getValidCards(options, filter, ctrl, host, sa);
if (!newOptions.isEmpty()) { if (!newOptions.isEmpty()) {
options = newOptions; options = newOptions;
} }
@@ -194,7 +194,7 @@ public class ChooseCardAi extends SpellAbilityAi {
choice = Aggregates.random(options); choice = Aggregates.random(options);
} else if (logic.equals("Untap")) { } else if (logic.equals("Untap")) {
final String filter = "Permanent.YouCtrl,Permanent.tapped"; final String filter = "Permanent.YouCtrl,Permanent.tapped";
CardCollection newOptions = CardLists.getValidCards(options, filter.split(","), ctrl, host, sa); CardCollection newOptions = CardLists.getValidCards(options, filter, ctrl, host, sa);
if (!newOptions.isEmpty()) { if (!newOptions.isEmpty()) {
options = newOptions; options = newOptions;
} }

View File

@@ -198,7 +198,7 @@ public class CloneAi extends SpellAbilityAi {
filter = filter.replace(".nonLegendary+", ".").replace(".nonLegendary", ""); filter = filter.replace(".nonLegendary+", ".").replace(".nonLegendary", "");
} }
CardCollection newOptions = CardLists.getValidCards(options, filter.split(","), ctrl, host, sa); CardCollection newOptions = CardLists.getValidCards(options, filter, ctrl, host, sa);
if (!newOptions.isEmpty()) { if (!newOptions.isEmpty()) {
options = newOptions; options = newOptions;
} }

View File

@@ -248,7 +248,7 @@ public class CopyPermanentAi extends SpellAbilityAi {
final boolean canCopyLegendary = sa.hasParam("NonLegendary"); final boolean canCopyLegendary = sa.hasParam("NonLegendary");
final String filter = canCopyLegendary ? "Permanent" : "Permanent.YouDontCtrl,Permanent.nonLegendary"; final String filter = canCopyLegendary ? "Permanent" : "Permanent.YouDontCtrl,Permanent.nonLegendary";
// TODO add filter to not select Legendary from Other Player when ai already have a Legendary with that name // TODO add filter to not select Legendary from Other Player when ai already have a Legendary with that name
return CardLists.getValidCards(options, filter.split(","), ctrl, host, sa); return CardLists.getValidCards(options, filter, ctrl, host, sa);
} }
@Override @Override

View File

@@ -101,6 +101,7 @@ public abstract class CountersAi extends SpellAbilityAi {
Card choice = null; Card choice = null;
if (type.equals("P1P1")) { if (type.equals("P1P1")) {
// TODO look for modified
choice = ComputerUtilCard.getBestCreatureAI(list); choice = ComputerUtilCard.getBestCreatureAI(list);
if (choice == null) { if (choice == null) {

View File

@@ -251,7 +251,7 @@ public class DamageAllAi extends SpellAbilityAi {
// TODO: X may be something different than X paid // TODO: X may be something different than X paid
CardCollection list = CardCollection list =
CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), validC.split(","), source.getController(), source, sa); CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), validC, source.getController(), source, sa);
final Predicate<Card> filterKillable = new Predicate<Card>() { final Predicate<Card> filterKillable = new Predicate<Card>() {
@Override @Override

View File

@@ -92,8 +92,8 @@ public class DestroyAllAi extends SpellAbilityAi {
// TODO should probably sort results when targeted to use on biggest threat instead of first match // TODO should probably sort results when targeted to use on biggest threat instead of first match
for (Player opponent: ai.getOpponents()) { for (Player opponent: ai.getOpponents()) {
CardCollection opplist = CardLists.getValidCards(opponent.getCardsIn(ZoneType.Battlefield), valid.split(","), source.getController(), source, sa); CardCollection opplist = CardLists.getValidCards(opponent.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source, sa);
CardCollection ailist = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid.split(","), source.getController(), source, sa); CardCollection ailist = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source, sa);
opplist = CardLists.filter(opplist, predicate); opplist = CardLists.filter(opplist, predicate);
ailist = CardLists.filter(ailist, predicate); ailist = CardLists.filter(ailist, predicate);

View File

@@ -68,7 +68,7 @@ public class DigUntilAi extends SpellAbilityAi {
} else { } else {
if (sa.hasParam("Valid")) { if (sa.hasParam("Valid")) {
final String valid = sa.getParam("Valid"); final String valid = sa.getParam("Valid");
if (CardLists.getValidCards(ai.getCardsIn(ZoneType.Library), valid.split(","), source.getController(), source, sa).isEmpty()) { if (CardLists.getValidCards(ai.getCardsIn(ZoneType.Library), valid, source.getController(), source, sa).isEmpty()) {
return false; return false;
} }
} }

View File

@@ -29,7 +29,7 @@ public class RegenerateAllAi extends SpellAbilityAi {
final String valid = sa.getParamOrDefault("ValidCards", ""); final String valid = sa.getParamOrDefault("ValidCards", "");
CardCollectionView list = game.getCardsIn(ZoneType.Battlefield); CardCollectionView list = game.getCardsIn(ZoneType.Battlefield);
list = CardLists.getValidCards(list, valid.split(","), hostCard.getController(), hostCard, sa); list = CardLists.getValidCards(list, valid, hostCard.getController(), hostCard, sa);
list = CardLists.filter(list, CardPredicates.isController(ai)); list = CardLists.filter(list, CardPredicates.isController(ai));
if (list.size() == 0) { if (list.size() == 0) {

View File

@@ -79,7 +79,7 @@ public class SacrificeAi extends SpellAbilityAi {
List<Card> list = null; List<Card> list = null;
try { try {
list = CardLists.getValidCards(opp.getCardsIn(ZoneType.Battlefield), valid.split(","), sa.getActivatingPlayer(), source, sa); list = CardLists.getValidCards(opp.getCardsIn(ZoneType.Battlefield), valid, sa.getActivatingPlayer(), source, sa);
} catch (NullPointerException e) { } catch (NullPointerException e) {
return false; return false;
} finally { } finally {
@@ -141,7 +141,7 @@ public class SacrificeAi extends SpellAbilityAi {
List<Card> humanList = null; List<Card> humanList = null;
try { try {
humanList = CardLists.getValidCards(ai.getStrongestOpponent().getCardsIn(ZoneType.Battlefield), valid.split(","), sa.getActivatingPlayer(), source, sa); humanList = CardLists.getValidCards(ai.getStrongestOpponent().getCardsIn(ZoneType.Battlefield), valid, sa.getActivatingPlayer(), source, sa);
} catch (NullPointerException e) { } catch (NullPointerException e) {
return false; return false;
} finally { } finally {
@@ -155,7 +155,7 @@ public class SacrificeAi extends SpellAbilityAi {
} else if (defined.equals("You")) { } else if (defined.equals("You")) {
List<Card> computerList = null; List<Card> computerList = null;
try { try {
computerList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid.split(","), sa.getActivatingPlayer(), source, sa); computerList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid, sa.getActivatingPlayer(), source, sa);
} catch (NullPointerException e) { } catch (NullPointerException e) {
return false; return false;
} finally { } finally {

View File

@@ -241,14 +241,14 @@ public class TokenAi extends SpellAbilityAi {
final String valid = topStack.getParamOrDefault("SacValid", "Card.Self"); final String valid = topStack.getParamOrDefault("SacValid", "Card.Self");
String num = sa.getParamOrDefault("Amount", "1"); String num = sa.getParamOrDefault("Amount", "1");
final int nToSac = AbilityUtils.calculateAmount(topStack.getHostCard(), num, topStack); final int nToSac = AbilityUtils.calculateAmount(topStack.getHostCard(), num, topStack);
CardCollection list = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid.split(","), CardCollection list = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid,
ai.getWeakestOpponent(), topStack.getHostCard(), sa); ai.getWeakestOpponent(), topStack.getHostCard(), sa);
list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(topStack, true)); list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(topStack, true));
// only care about saving single creature for now // only care about saving single creature for now
if (!list.isEmpty() && nTokens > 0 && list.size() == nToSac) { if (!list.isEmpty() && nTokens > 0 && list.size() == nToSac) {
ComputerUtilCard.sortByEvaluateCreature(list); ComputerUtilCard.sortByEvaluateCreature(list);
list.add(token); list.add(token);
list = CardLists.getValidCards(list, valid.split(","), ai.getWeakestOpponent(), topStack.getHostCard(), sa); list = CardLists.getValidCards(list, valid, ai.getWeakestOpponent(), topStack.getHostCard(), sa);
list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(topStack, true)); list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(topStack, true));
return ComputerUtilCard.evaluateCreature(token) < ComputerUtilCard.evaluateCreature(list.get(0)) return ComputerUtilCard.evaluateCreature(token) < ComputerUtilCard.evaluateCreature(list.get(0))
&& list.contains(token); && list.contains(token);

View File

@@ -26,7 +26,7 @@ public class UntapAllAi extends SpellAbilityAi {
} }
CardCollectionView list = CardLists.filter(aiPlayer.getGame().getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.TAPPED); CardCollectionView list = CardLists.filter(aiPlayer.getGame().getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.TAPPED);
final String valid = sa.getParamOrDefault("ValidCards", ""); final String valid = sa.getParamOrDefault("ValidCards", "");
list = CardLists.getValidCards(list, valid.split(","), source.getController(), source, sa); list = CardLists.getValidCards(list, valid, source.getController(), source, sa);
// don't untap if only opponent benefits // don't untap if only opponent benefits
PlayerCollection goodControllers = aiPlayer.getAllies(); PlayerCollection goodControllers = aiPlayer.getAllies();
goodControllers.add(aiPlayer); goodControllers.add(aiPlayer);
@@ -43,7 +43,7 @@ public class UntapAllAi extends SpellAbilityAi {
if (sa.hasParam("ValidCards")) { if (sa.hasParam("ValidCards")) {
String valid = sa.getParam("ValidCards"); String valid = sa.getParam("ValidCards");
CardCollectionView list = CardLists.filter(aiPlayer.getGame().getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.TAPPED); CardCollectionView list = CardLists.filter(aiPlayer.getGame().getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.TAPPED);
list = CardLists.getValidCards(list, valid.split(","), source.getController(), source, sa); list = CardLists.getValidCards(list, valid, source.getController(), source, sa);
return mandatory || !list.isEmpty(); return mandatory || !list.isEmpty();
} }

View File

@@ -368,7 +368,7 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
list.addAll(p.getCardsIn(presentZone)); list.addAll(p.getCardsIn(presentZone));
} }
} }
list = CardLists.getValidCards(list, sIsPresent.split(","), this.getHostCard().getController(), this.getHostCard(), this); list = CardLists.getValidCards(list, sIsPresent, this.getHostCard().getController(), this.getHostCard(), this);
final String rightString = presentCompare.substring(2); final String rightString = presentCompare.substring(2);
int right = AbilityUtils.calculateAmount(getHostCard(), rightString, this); int right = AbilityUtils.calculateAmount(getHostCard(), rightString, this);
@@ -397,7 +397,7 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
} }
} }
list = CardLists.getValidCards(list, sIsPresent.split(","), this.getHostCard().getController(), this.getHostCard(), this); list = CardLists.getValidCards(list, sIsPresent, this.getHostCard().getController(), this.getHostCard(), this);
final String rightString = presentCompare.substring(2); final String rightString = presentCompare.substring(2);
int right = AbilityUtils.calculateAmount(getHostCard(), rightString, this); int right = AbilityUtils.calculateAmount(getHostCard(), rightString, this);

View File

@@ -346,7 +346,7 @@ public class AbilityUtils {
candidates = game.getCardsIn(ZoneType.smartValueOf(zone)); candidates = game.getCardsIn(ZoneType.smartValueOf(zone));
validDefined = s[1]; validDefined = s[1];
} }
cards.addAll(CardLists.getValidCards(candidates, validDefined.split(","), hostCard.getController(), hostCard, sa)); cards.addAll(CardLists.getValidCards(candidates, validDefined, hostCard.getController(), hostCard, sa));
return cards; return cards;
} else { } else {
CardCollection list = null; CardCollection list = null;
@@ -977,7 +977,7 @@ public class AbilityUtils {
String var = sa.getParam("AbilityCount"); String var = sa.getParam("AbilityCount");
valid = TextUtil.fastReplace(valid, var, Integer.toString(calculateAmount(source, var, sa))); valid = TextUtil.fastReplace(valid, var, Integer.toString(calculateAmount(source, var, sa)));
} }
return CardLists.getValidCards(list, valid.split(","), sa.getActivatingPlayer(), source, sa); return CardLists.getValidCards(list, valid, sa.getActivatingPlayer(), source, sa);
} }
/** /**
@@ -1884,7 +1884,7 @@ public class AbilityUtils {
return doXMath(0, expr, c, ctb); return doXMath(0, expr, c, ctb);
} }
} }
list = CardLists.getValidCards(list, k[1].split(","), sa.getActivatingPlayer(), c, sa); list = CardLists.getValidCards(list, k[1], sa.getActivatingPlayer(), c, sa);
if (k[0].contains("TotalToughness")) { if (k[0].contains("TotalToughness")) {
return doXMath(Aggregates.sum(list, CardPredicates.Accessors.fnGetNetToughness), expr, c, ctb); return doXMath(Aggregates.sum(list, CardPredicates.Accessors.fnGetNetToughness), expr, c, ctb);
} }
@@ -1911,7 +1911,7 @@ public class AbilityUtils {
return doXMath(0, expr, c, ctb); return doXMath(0, expr, c, ctb);
} }
} }
list = CardLists.getValidCards(list, k[1].split(","), sa.getActivatingPlayer(), c, sa); list = CardLists.getValidCards(list, k[1], sa.getActivatingPlayer(), c, sa);
return doXMath(list.size(), expr, c, ctb); return doXMath(list.size(), expr, c, ctb);
} }
@@ -1940,14 +1940,14 @@ public class AbilityUtils {
if (sq[0].startsWith("LastStateBattlefield")) { if (sq[0].startsWith("LastStateBattlefield")) {
final String[] k = l[0].split(" "); final String[] k = l[0].split(" ");
CardCollection list = new CardCollection(game.getLastStateBattlefield()); CardCollection list = new CardCollection(game.getLastStateBattlefield());
list = CardLists.getValidCards(list, k[1].split(","), player, c, ctb); list = CardLists.getValidCards(list, k[1], player, c, ctb);
return doXMath(list.size(), expr, c, ctb); return doXMath(list.size(), expr, c, ctb);
} }
if (sq[0].startsWith("LastStateGraveyard")) { if (sq[0].startsWith("LastStateGraveyard")) {
final String[] k = l[0].split(" "); final String[] k = l[0].split(" ");
CardCollection list = new CardCollection(game.getLastStateGraveyard()); CardCollection list = new CardCollection(game.getLastStateGraveyard());
list = CardLists.getValidCards(list, k[1].split(","), player, c, ctb); list = CardLists.getValidCards(list, k[1], player, c, ctb);
return doXMath(list.size(), expr, c, ctb); return doXMath(list.size(), expr, c, ctb);
} }
@@ -2176,7 +2176,7 @@ public class AbilityUtils {
if (sq[0].startsWith("Devoured")) { if (sq[0].startsWith("Devoured")) {
final String validDevoured = sq[0].split(" ")[1]; final String validDevoured = sq[0].split(" ")[1];
CardCollection cl = CardLists.getValidCards(c.getDevouredCards(), validDevoured.split(","), player, c, ctb); CardCollection cl = CardLists.getValidCards(c.getDevouredCards(), validDevoured, player, c, ctb);
return doXMath(cl.size(), expr, c, ctb); return doXMath(cl.size(), expr, c, ctb);
} }
@@ -2451,8 +2451,7 @@ public class AbilityUtils {
if (sq[0].startsWith("ColorsCtrl")) { if (sq[0].startsWith("ColorsCtrl")) {
final String restriction = l[0].substring(11); final String restriction = l[0].substring(11);
final String[] rest = restriction.split(","); final CardCollection list = CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), restriction, player, c, ctb);
final CardCollection list = CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), rest, player, c, ctb);
byte n = 0; byte n = 0;
for (final Card card : list) { for (final Card card : list) {
n |= card.getColor().getColor(); n |= card.getColor().getColor();
@@ -2711,8 +2710,7 @@ public class AbilityUtils {
// Count$SumPower_valid // Count$SumPower_valid
if (sq[0].startsWith("SumPower")) { if (sq[0].startsWith("SumPower")) {
final String[] restrictions = l[0].split("_"); final String[] restrictions = l[0].split("_");
final String[] rest = restrictions[1].split(","); CardCollection filteredCards = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), restrictions[1], player, c, ctb);
CardCollection filteredCards = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), rest, player, c, ctb);
return doXMath(Aggregates.sum(filteredCards, CardPredicates.Accessors.fnGetNetPower), expr, c, ctb); return doXMath(Aggregates.sum(filteredCards, CardPredicates.Accessors.fnGetNetPower), expr, c, ctb);
} }
@@ -2723,9 +2721,8 @@ public class AbilityUtils {
if (sq[0].contains("Graveyard")) if (sq[0].contains("Graveyard"))
zone = ZoneType.Graveyard; zone = ZoneType.Graveyard;
final String[] restrictions = l[0].split("_"); final String[] restrictions = l[0].split("_");
final String[] rest = restrictions[1].split(",");
CardCollectionView cardsonbattlefield = game.getCardsIn(zone); CardCollectionView cardsonbattlefield = game.getCardsIn(zone);
CardCollection filteredCards = CardLists.getValidCards(cardsonbattlefield, rest, player, c, ctb); CardCollection filteredCards = CardLists.getValidCards(cardsonbattlefield, restrictions[1], player, c, ctb);
return Aggregates.sum(filteredCards, CardPredicates.Accessors.fnGetCmc); return Aggregates.sum(filteredCards, CardPredicates.Accessors.fnGetCmc);
} }
@@ -2821,8 +2818,7 @@ public class AbilityUtils {
if (sq[0].startsWith("GreatestToughness_")) { if (sq[0].startsWith("GreatestToughness_")) {
final String restriction = l[0].substring(18); final String restriction = l[0].substring(18);
final String[] rest = restriction.split(","); CardCollection list = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), restriction, player, c, ctb);
CardCollection list = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), rest, player, c, ctb);
int highest = 0; int highest = 0;
for (final Card crd : list) { for (final Card crd : list) {
if (crd.getNetToughness() > highest) { if (crd.getNetToughness() > highest) {
@@ -2834,8 +2830,7 @@ public class AbilityUtils {
if (sq[0].startsWith("HighestCMC_")) { if (sq[0].startsWith("HighestCMC_")) {
final String restriction = l[0].substring(11); final String restriction = l[0].substring(11);
final String[] rest = restriction.split(","); CardCollection list = CardLists.getValidCards(game.getCardsInGame(), restriction, player, c, ctb);
CardCollection list = CardLists.getValidCards(game.getCardsInGame(), rest, player, c, ctb);
int highest = 0; int highest = 0;
for (final Card crd : list) { for (final Card crd : list) {
// dont check for Split card anymore // dont check for Split card anymore
@@ -2874,8 +2869,7 @@ public class AbilityUtils {
if (sq[0].startsWith("DifferentCardNames_")) { if (sq[0].startsWith("DifferentCardNames_")) {
final List<String> crdname = Lists.newArrayList(); final List<String> crdname = Lists.newArrayList();
final String restriction = l[0].substring(19); final String restriction = l[0].substring(19);
final String[] rest = restriction.split(","); CardCollection list = CardLists.getValidCards(game.getCardsInGame(), restriction, player, c, ctb);
CardCollection list = CardLists.getValidCards(game.getCardsInGame(), rest, player, c, ctb);
for (final Card card : list) { for (final Card card : list) {
String name = card.getName(); String name = card.getName();
// CR 201.2b Those objects have different names only if each of them has at least one name and no two objects in that group have a name in common // CR 201.2b Those objects have different names only if each of them has at least one name and no two objects in that group have a name in common
@@ -2889,8 +2883,7 @@ public class AbilityUtils {
if (sq[0].startsWith("DifferentPower_")) { if (sq[0].startsWith("DifferentPower_")) {
final List<Integer> powers = Lists.newArrayList(); final List<Integer> powers = Lists.newArrayList();
final String restriction = l[0].substring(15); final String restriction = l[0].substring(15);
final String[] rest = restriction.split(","); CardCollection list = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), restriction, player, c, ctb);
CardCollection list = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), rest, player, c, ctb);
for (final Card card : list) { for (final Card card : list) {
Integer pow = card.getNetPower(); Integer pow = card.getNetPower();
if (!powers.contains(pow)) { if (!powers.contains(pow)) {
@@ -2915,8 +2908,7 @@ public class AbilityUtils {
if (sq[0].startsWith("ColorsCtrl")) { if (sq[0].startsWith("ColorsCtrl")) {
final String restriction = l[0].substring(11); final String restriction = l[0].substring(11);
final String[] rest = restriction.split(","); final CardCollection list = CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), restriction, player, c, ctb);
final CardCollection list = CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), rest, player, c, ctb);
byte n = 0; byte n = 0;
for (final Card card : list) { for (final Card card : list) {
n |= card.getColor().getColor(); n |= card.getColor().getColor();
@@ -3417,16 +3409,14 @@ public class AbilityUtils {
String[] lparts = l[0].split(" ", 2); String[] lparts = l[0].split(" ", 2);
final List<ZoneType> vZone = ZoneType.listValueOf(lparts[0].split("Valid")[1]); final List<ZoneType> vZone = ZoneType.listValueOf(lparts[0].split("Valid")[1]);
String restrictions = TextUtil.fastReplace(l[0], TextUtil.addSuffix(lparts[0]," "), ""); String restrictions = TextUtil.fastReplace(l[0], TextUtil.addSuffix(lparts[0]," "), "");
final String[] rest = restrictions.split(","); CardCollection cards = CardLists.getValidCards(game.getCardsIn(vZone), restrictions, player, source, ctb);
CardCollection cards = CardLists.getValidCards(game.getCardsIn(vZone), rest, player, source, ctb);
return doXMath(cards.size(), m, source, ctb); return doXMath(cards.size(), m, source, ctb);
} }
// count valid cards on the battlefield // count valid cards on the battlefield
if (l[0].startsWith("Valid ")) { if (l[0].startsWith("Valid ")) {
final String restrictions = l[0].substring(6); final String restrictions = l[0].substring(6);
final String[] rest = restrictions.split(","); CardCollection cardsonbattlefield = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), restrictions, player, source, ctb);
CardCollection cardsonbattlefield = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), rest, player, source, ctb);
return doXMath(cardsonbattlefield.size(), m, source, ctb); return doXMath(cardsonbattlefield.size(), m, source, ctb);
} }

View File

@@ -130,7 +130,7 @@ public class AnimateAllEffect extends AnimateEffectBase {
list = getTargetPlayers(sa).getCardsIn(ZoneType.Battlefield); list = getTargetPlayers(sa).getCardsIn(ZoneType.Battlefield);
} }
list = CardLists.getValidCards(list, valid.split(","), host.getController(), host, sa); list = CardLists.getValidCards(list, valid, host.getController(), host, sa);
for (final Card c : list) { for (final Card c : list) {
doAnimate(c, sa, power, toughness, types, removeTypes, finalColors, doAnimate(c, sa, power, toughness, types, removeTypes, finalColors,

View File

@@ -67,7 +67,7 @@ public class CharmEffect extends SpellAbilityEffect {
} else { } else {
num = Math.min(AbilityUtils.calculateAmount(source, sa.getParamOrDefault("CharmNum", "1"), sa), list.size()); num = Math.min(AbilityUtils.calculateAmount(source, sa.getParamOrDefault("CharmNum", "1"), sa), list.size());
} }
final int min = sa.hasParam("MinCharmNum") ? AbilityUtils.calculateAmount(source, sa.getParamOrDefault("MinCharmNum", "1"), sa) : num; final int min = sa.hasParam("MinCharmNum") ? AbilityUtils.calculateAmount(source, sa.getParam("MinCharmNum"), sa) : num;
boolean repeat = sa.hasParam("CanRepeatModes"); boolean repeat = sa.hasParam("CanRepeatModes");
boolean random = sa.hasParam("Random"); boolean random = sa.hasParam("Random");
@@ -120,7 +120,7 @@ public class CharmEffect extends SpellAbilityEffect {
} }
if (additionalDesc) { if (additionalDesc) {
String addDescS = (sa.getParam("AdditionalDescription")); String addDescS = sa.getParam("AdditionalDescription");
if (optional) { if (optional) {
sb.append(". ").append(addDescS.trim()); sb.append(". ").append(addDescS.trim());
} else if (addDescS.startsWith(("."))) { } else if (addDescS.startsWith(("."))) {

View File

@@ -132,7 +132,7 @@ public class CopySpellAbilityEffect extends SpellAbilityEffect {
} }
} }
} }
valid = CardLists.getValidCards(valid, type.split(","), chosenSA.getActivatingPlayer(), chosenSA.getHostCard(), sa); valid = CardLists.getValidCards(valid, type, chosenSA.getActivatingPlayer(), chosenSA.getHostCard(), sa);
Card originalTarget = Iterables.getFirst(getTargetCards(chosenSA), null); Card originalTarget = Iterables.getFirst(getTargetCards(chosenSA), null);
valid.remove(originalTarget); valid.remove(originalTarget);

View File

@@ -209,7 +209,7 @@ public class DigEffect extends SpellAbilityEffect {
if (changeValid.contains("ChosenType")) { if (changeValid.contains("ChosenType")) {
changeValid = changeValid.replace("ChosenType", host.getChosenType()); changeValid = changeValid.replace("ChosenType", host.getChosenType());
} }
valid = CardLists.getValidCards(top, changeValid.split(","), cont, host, sa); valid = CardLists.getValidCards(top, changeValid, cont, host, sa);
if (totalCMC) { if (totalCMC) {
valid = CardLists.getValidCards(valid, "Card.cmcLE" + totcmc, cont, host, sa); valid = CardLists.getValidCards(valid, "Card.cmcLE" + totcmc, cont, host, sa);
} }

View File

@@ -233,7 +233,7 @@ public class DiscardEffect extends SpellAbilityEffect {
"X", Integer.toString(AbilityUtils.calculateAmount(source, "X", sa))); "X", Integer.toString(AbilityUtils.calculateAmount(source, "X", sa)));
} }
toBeDiscarded = CardLists.getValidCards(dPHand, valid.split(","), source.getController(), source, sa); toBeDiscarded = CardLists.getValidCards(dPHand, valid, source.getController(), source, sa);
toBeDiscarded = CardLists.filter(toBeDiscarded, Presets.NON_TOKEN); toBeDiscarded = CardLists.filter(toBeDiscarded, Presets.NON_TOKEN);
if (toBeDiscarded.size() > 1) { if (toBeDiscarded.size() > 1) {
toBeDiscarded = GameActionUtil.orderCardsByTheirOwners(game, toBeDiscarded, ZoneType.Graveyard, sa); toBeDiscarded = GameActionUtil.orderCardsByTheirOwners(game, toBeDiscarded, ZoneType.Graveyard, sa);
@@ -250,8 +250,7 @@ public class DiscardEffect extends SpellAbilityEffect {
} }
final String valid = sa.getParamOrDefault("DiscardValid", "Card"); final String valid = sa.getParamOrDefault("DiscardValid", "Card");
String[] dValid = valid.split(","); CardCollection validCards = CardLists.getValidCards(dPHand, valid, source.getController(), source, sa);
CardCollection validCards = CardLists.getValidCards(dPHand, dValid, source.getController(), source, sa);
Player chooser = p; Player chooser = p;
if (mode.equals("RevealYouChoose")) { if (mode.equals("RevealYouChoose")) {

View File

@@ -25,6 +25,9 @@ import forge.game.card.Card;
import forge.game.card.CardCollection; import forge.game.card.CardCollection;
import forge.game.card.CardFactoryUtil; import forge.game.card.CardFactoryUtil;
import forge.game.cost.Cost; import forge.game.cost.Cost;
import forge.game.cost.CostDiscard;
import forge.game.cost.CostPart;
import forge.game.cost.CostReveal;
import forge.game.mana.ManaCostBeingPaid; import forge.game.mana.ManaCostBeingPaid;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.replacement.ReplacementEffect; import forge.game.replacement.ReplacementEffect;
@@ -70,7 +73,7 @@ public class PlayEffect extends SpellAbilityEffect {
Player controlledByPlayer = null; Player controlledByPlayer = null;
long controlledByTimeStamp = -1; long controlledByTimeStamp = -1;
final Game game = activator.getGame(); final Game game = activator.getGame();
final boolean optional = sa.hasParam("Optional"); boolean optional = sa.hasParam("Optional");
boolean remember = sa.hasParam("RememberPlayed"); boolean remember = sa.hasParam("RememberPlayed");
int amount = 1; int amount = 1;
boolean hasTotalCMCLimit = sa.hasParam("WithTotalCMC"); boolean hasTotalCMCLimit = sa.hasParam("WithTotalCMC");
@@ -330,7 +333,17 @@ public class PlayEffect extends SpellAbilityEffect {
} }
if (!optional) { if (!optional) {
tgtSA.getPayCosts().setMandatory(true); // 118.8c
for (CostPart cost : tgtSA.getPayCosts().getCostParts()) {
if ((cost instanceof CostDiscard || cost instanceof CostReveal)
&& !cost.getType().equals("Card") && !cost.getType().equals("Random")) {
optional = true;
break;
}
}
if (!optional) {
tgtSA.getPayCosts().setMandatory(true);
}
} }
if (sa.hasParam("PlayReduceCost")) { if (sa.hasParam("PlayReduceCost")) {

View File

@@ -29,7 +29,7 @@ public class RegenerateAllEffect extends RegenerateBaseEffect {
final String valid = sa.getParamOrDefault("ValidCards", ""); final String valid = sa.getParamOrDefault("ValidCards", "");
CardCollectionView list = game.getCardsIn(ZoneType.Battlefield); CardCollectionView list = game.getCardsIn(ZoneType.Battlefield);
list = CardLists.getValidCards(list, valid.split(","), hostCard.getController(), hostCard, sa); list = CardLists.getValidCards(list, valid, hostCard.getController(), hostCard, sa);
// create Effect for Regeneration // create Effect for Regeneration
createRegenerationEffect(sa, list); createRegenerationEffect(sa, list);

View File

@@ -84,7 +84,7 @@ public class RepeatEffect extends SpellAbilityEffect {
} else { } else {
list = game.getCardsIn(ZoneType.Battlefield); list = game.getCardsIn(ZoneType.Battlefield);
} }
list = CardLists.getValidCards(list, repeatPresent.split(","), sa.getActivatingPlayer(), sa.getHostCard(), sa); list = CardLists.getValidCards(list, repeatPresent, sa.getActivatingPlayer(), sa.getHostCard(), sa);
final String rightString = repeatCompare.substring(2); final String rightString = repeatCompare.substring(2);
int right = AbilityUtils.calculateAmount(sa.getHostCard(), rightString, sa); int right = AbilityUtils.calculateAmount(sa.getHostCard(), rightString, sa);

View File

@@ -140,7 +140,7 @@ public class UnattachAllEffect extends SpellAbilityEffect {
String valid = sa.getParam("UnattachValid"); String valid = sa.getParam("UnattachValid");
CardCollectionView unattachList = game.getCardsIn(ZoneType.Battlefield); CardCollectionView unattachList = game.getCardsIn(ZoneType.Battlefield);
unattachList = CardLists.getValidCards(unattachList, valid.split(","), source.getController(), source, sa); unattachList = CardLists.getValidCards(unattachList, valid, source.getController(), source, sa);
for (final Card c : unattachList) { for (final Card c : unattachList) {
handleUnattachment((GameEntity) o, c); handleUnattachment((GameEntity) o, c);
} }

View File

@@ -37,7 +37,7 @@ public class UntapAllEffect extends SpellAbilityEffect {
} }
list = list2; list = list2;
} }
list = CardLists.getValidCards(list, valid.split(","), card.getController(), card, sa); list = CardLists.getValidCards(list, valid, card.getController(), card, sa);
boolean remember = sa.hasParam("RememberUntapped"); boolean remember = sa.hasParam("RememberUntapped");
for (Card c : list) { for (Card c : list) {

View File

@@ -3491,6 +3491,9 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
} }
public final boolean isModified() { public final boolean isModified() {
if (!isCreature()) {
return false;
}
if (this.isEquipped() || this.hasCounters()) { if (this.isEquipped() || this.hasCounters()) {
return true; return true;
} }

View File

@@ -1452,7 +1452,7 @@ public class CardFactoryUtil {
final String manacost = k[1]; final String manacost = k[1];
final String abStrReveal = "DB$ Reveal | Defined$ You | RevealDefined$ Self" final String abStrReveal = "DB$ Reveal | Defined$ You | RevealDefined$ Self"
+ " | MiracleCost$ " + manacost; + " | MiracleCost$ " + manacost;
final String abStrPlay = "DB$ Play | Defined$ Self | PlayCost$ " + manacost; final String abStrPlay = "DB$ Play | Defined$ Self | Optional$ True | PlayCost$ " + manacost;
String revealed = "DB$ ImmediateTrigger | TriggerDescription$ CARDNAME - Miracle"; String revealed = "DB$ ImmediateTrigger | TriggerDescription$ CARDNAME - Miracle";

View File

@@ -90,7 +90,7 @@ public class CardZoneTable extends ForwardingTable<ZoneType, ZoneType, CardColle
} }
if (valid != null) { if (valid != null) {
allCards = CardLists.getValidCards(allCards, valid.split(","), host.getController(), host, sa); allCards = CardLists.getValidCards(allCards, valid, host.getController(), host, sa);
} }
return allCards; return allCards;
} }

View File

@@ -22,6 +22,7 @@ import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import forge.card.CardType;
import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@@ -834,9 +835,14 @@ public class Cost implements Serializable {
sb.append(Cost.NUM_NAMES[i]); sb.append(Cost.NUM_NAMES[i]);
} }
sb.append(" ").append(type); sb.append(" ");
if (1 != i) { if (1 != i) {
sb.append("s"); String [] typewords = type.split(" ");
String lastWord = typewords[typewords.length - 1];
sb.append(CardType.isASubType(lastWord) ? type.replace(lastWord, CardType.getPluralType(lastWord))
: type + "s");
} else {
sb.append(type);
} }
return sb.toString(); return sb.toString();

View File

@@ -138,6 +138,7 @@ public class CostAdjustment {
count = Integer.parseInt(amount); count = Integer.parseInt(amount);
} else { } else {
if (st.hasParam("Relative")) { if (st.hasParam("Relative")) {
// grab SVar here already to avoid potential collision when SA has one with same name
count = AbilityUtils.calculateAmount(hostCard, st.hasSVar(amount) ? st.getSVar(amount) : amount, sa); count = AbilityUtils.calculateAmount(hostCard, st.hasSVar(amount) ? st.getSVar(amount) : amount, sa);
} else { } else {
count = AbilityUtils.calculateAmount(hostCard, amount, st); count = AbilityUtils.calculateAmount(hostCard, amount, st);
@@ -379,7 +380,7 @@ public class CostAdjustment {
// TODO: update cards with "This spell costs X less to cast...if you..." // TODO: update cards with "This spell costs X less to cast...if you..."
// The caster is sa.getActivatingPlayer() // The caster is sa.getActivatingPlayer()
// cards like Hostage Taker can cast spells from other players. // cards like Hostage Taker can cast spells from other players.
value = AbilityUtils.calculateAmount(hostCard, amount, sa); value = AbilityUtils.calculateAmount(hostCard, staticAbility.hasSVar(amount) ? staticAbility.getSVar(amount) : amount, sa);
} else { } else {
value = AbilityUtils.calculateAmount(hostCard, amount, staticAbility); value = AbilityUtils.calculateAmount(hostCard, amount, staticAbility);
} }

View File

@@ -19,6 +19,7 @@ package forge.game.cost;
import java.io.Serializable; import java.io.Serializable;
import forge.card.CardType;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import forge.game.CardTraitBase; import forge.game.CardTraitBase;
@@ -106,7 +107,11 @@ public abstract class CostPart implements Comparable<CostPart>, Cloneable, Seria
public final String getDescriptiveType() { public final String getDescriptiveType() {
String typeDesc = this.getTypeDescription(); String typeDesc = this.getTypeDescription();
return typeDesc == null ? this.getType() : typeDesc; if (typeDesc == null) {
String typeS = this.getType();
typeDesc = CardType.CoreType.isValidEnum(typeS) ? typeS.toLowerCase() : typeS;
}
return typeDesc;
} }
/** /**

View File

@@ -17,6 +17,7 @@
*/ */
package forge.game.cost; package forge.game.cost;
import forge.card.CardType;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardCollection; import forge.game.card.CardCollection;
import forge.game.card.CardLists; import forge.game.card.CardLists;
@@ -87,20 +88,33 @@ public class CostTapType extends CostPartWithList {
@Override @Override
public final String toString() { public final String toString() {
final StringBuilder sb = new StringBuilder(); final StringBuilder sb = new StringBuilder();
sb.append("Tap ");
final Integer i = this.convertAmount(); final Integer i = this.convertAmount();
final String desc = this.getDescriptiveType(); final String desc = this.getDescriptiveType();
final String type = this.getType(); final String type = this.getType();
if (type.contains("+withTotalPowerGE")) {
String num = type.split("\\+withTotalPowerGE")[1];
sb.append("Tap any number of untapped creatures you control other than CARDNAME with total power ");
sb.append(num).append("or greater");
return sb.toString();
}
sb.append("Tap ");
if (type.contains("sharesCreatureTypeWith")) { if (type.contains("sharesCreatureTypeWith")) {
sb.append("two untapped creatures you control that share a creature type"); sb.append("two untapped creatures you control that share a creature type");
} else if (type.contains("+withTotalPowerGE")) { } else if (type.contains("Other")) {
String num = type.split("\\+withTotalPowerGE")[1]; String rep = type.contains(".Other") ? ".Other" : "+Other";
sb.append("Tap any number of untapped creatures you control other than CARDNAME with total power ").append(num).append("or greater"); String descTrim = desc.replace(rep, "");
if (CardType.CoreType.isValidEnum(descTrim)) {
descTrim = descTrim.toLowerCase();
}
sb.append("another untapped ").append(descTrim);
if (!descTrim.contains("you control")) {
sb.append(" you control");
}
} else { } else {
sb.append(Cost.convertAmountTypeToWords(i, this.getAmount(), "untapped " + desc)); sb.append(Cost.convertAmountTypeToWords(i, this.getAmount(), "untapped " + desc)).append(" you control");
sb.append(" you control");
} }
return sb.toString(); return sb.toString();
} }

View File

@@ -172,7 +172,7 @@ public class AbilityManaPart implements java.io.Serializable {
runParams.put(AbilityKey.Activator, root == null ? null : root.getActivatingPlayer()); runParams.put(AbilityKey.Activator, root == null ? null : root.getActivatingPlayer());
player.getGame().getTriggerHandler().runTrigger(TriggerType.TapsForMana, runParams, false); player.getGame().getTriggerHandler().runTrigger(TriggerType.TapsForMana, runParams, false);
if (source.isLand() && sa.getPayCosts() != null && sa.getPayCosts().hasTapCost() ) { if (source.isLand() && sa.getPayCosts() != null && sa.getPayCosts().hasTapCost()) {
player.setTappedLandForManaThisTurn(true); player.setTappedLandForManaThisTurn(true);
} }
} // end produceMana(String) } // end produceMana(String)

View File

@@ -467,7 +467,7 @@ public class StaticAbility extends CardTraitBase implements IIdentifiable, Clone
CardCollectionView list = game.getCardsIn(zone); CardCollectionView list = game.getCardsIn(zone);
final String present = getParam("IsPresent"); final String present = getParam("IsPresent");
list = CardLists.getValidCards(list, present.split(","), controller, hostCard, this); list = CardLists.getValidCards(list, present, controller, hostCard, this);
int right = 1; int right = 1;
final String rightString = compare.substring(2); final String rightString = compare.substring(2);

View File

@@ -1070,7 +1070,7 @@ public final class StaticAbilityContinuous {
affectedCardsOriginal = new CardCollection(affectedCards); affectedCardsOriginal = new CardCollection(affectedCards);
} }
affectedCards = CardLists.getValidCards(affectedCards, stAb.getParam("Affected").split(","), controller, hostCard, stAb); affectedCards = CardLists.getValidCards(affectedCards, stAb.getParam("Affected"), controller, hostCard, stAb);
// Add back all cards that are in other player's graveyard, and meet the restrictions without YouOwn/YouCtrl (treat it as in your graveyard) // Add back all cards that are in other player's graveyard, and meet the restrictions without YouOwn/YouCtrl (treat it as in your graveyard)
if (affectedCardsOriginal != null) { if (affectedCardsOriginal != null) {

View File

@@ -74,7 +74,7 @@ public class TriggerAttackersDeclared extends Trigger {
CardCollection attackers = (CardCollection) runParams.get(AbilityKey.Attackers); CardCollection attackers = (CardCollection) runParams.get(AbilityKey.Attackers);
if (hasParam("ValidAttackers")) { if (hasParam("ValidAttackers")) {
attackers = CardLists.getValidCards(attackers, getParam("ValidAttackers").split(","), getHostCard().getController(), getHostCard(), this); attackers = CardLists.getValidCards(attackers, getParam("ValidAttackers"), getHostCard().getController(), getHostCard(), this);
FCollection<GameEntity> defenders = new FCollection<>(); FCollection<GameEntity> defenders = new FCollection<>();
for (Card attacker : attackers) { for (Card attacker : attackers) {
defenders.add(attacker.getGame().getCombat().getDefenderByAttacker(attacker)); defenders.add(attacker.getGame().getCombat().getDefenderByAttacker(attacker));

View File

@@ -74,7 +74,7 @@ public class TriggerDrawn extends Trigger {
final String sIsPresent = this.getParam("ValidPlayerControls"); final String sIsPresent = this.getParam("ValidPlayerControls");
final Player p = ((Player)runParams.get(AbilityKey.Player)); final Player p = ((Player)runParams.get(AbilityKey.Player));
CardCollection list = (CardCollection) p.getCardsIn(ZoneType.Battlefield); CardCollection list = (CardCollection) p.getCardsIn(ZoneType.Battlefield);
list = CardLists.getValidCards(list, sIsPresent.split(","), this.getHostCard().getController(), list = CardLists.getValidCards(list, sIsPresent, this.getHostCard().getController(),
this.getHostCard(), this); this.getHostCard(), this);
if (list.size() == 0) { if (list.size() == 0) {
return false; return false;

View File

@@ -64,7 +64,7 @@ public class TriggerLifeGained extends Trigger {
final String sIsPresent = this.getParam("ValidPlayerControls"); final String sIsPresent = this.getParam("ValidPlayerControls");
final Player p = ((Player)runParams.get(AbilityKey.Player)); final Player p = ((Player)runParams.get(AbilityKey.Player));
CardCollection list = (CardCollection) p.getCardsIn(ZoneType.Battlefield); CardCollection list = (CardCollection) p.getCardsIn(ZoneType.Battlefield);
list = CardLists.getValidCards(list, sIsPresent.split(","), this.getHostCard().getController(), list = CardLists.getValidCards(list, sIsPresent, this.getHostCard().getController(),
this.getHostCard(), this); this.getHostCard(), this);
if (list.size() == 0) { if (list.size() == 0) {
return false; return false;

View File

@@ -7,6 +7,7 @@ import forge.Graphics;
import forge.assets.FSkinColor; import forge.assets.FSkinColor;
import forge.assets.FSkinColor.Colors; import forge.assets.FSkinColor.Colors;
import forge.assets.FSkinTexture; import forge.assets.FSkinTexture;
import forge.gui.GuiBase;
import forge.screens.FScreen; import forge.screens.FScreen;
import forge.toolbox.FContainer; import forge.toolbox.FContainer;
import forge.toolbox.FDisplayObject; import forge.toolbox.FDisplayObject;
@@ -221,19 +222,22 @@ public abstract class FDropDown extends FScrollPane {
@Override @Override
public boolean pan(float x, float y, float deltaX, float deltaY, boolean moreVertical) { public boolean pan(float x, float y, float deltaX, float deltaY, boolean moreVertical) {
hide(); //always hide if backdrop panned if (!GuiBase.isAndroid())
hide(); //always hide if backdrop panned
return false; //allow pan to pass through to object behind backdrop return false; //allow pan to pass through to object behind backdrop
} }
@Override @Override
public boolean fling(float velocityX, float velocityY) { public boolean fling(float velocityX, float velocityY) {
hide(); //always hide if backdrop flung if (!GuiBase.isAndroid())
hide(); //always hide if backdrop flung
return false; //allow fling to pass through to object behind backdrop return false; //allow fling to pass through to object behind backdrop
} }
@Override @Override
public boolean zoom(float x, float y, float amount) { public boolean zoom(float x, float y, float amount) {
hide(); //always hide if backdrop zoomed if (!GuiBase.isAndroid())
hide(); //always hide if backdrop zoomed
return false; //allow zoom to pass through to object behind backdrop return false; //allow zoom to pass through to object behind backdrop
} }

View File

@@ -3,5 +3,5 @@ ManaCost:2 B
Types:Creature Zombie Treefolk Types:Creature Zombie Treefolk
PT:0/5 PT:0/5
K:Defender K:Defender
A:AB$ Pump | Cost$ B tapXType<1/Creature.Other/another creature> | CostDesc$ {B}, Tap another untapped creature you control: | Defined$ Self | NumAtt$ +1 | NumDef$ +1 | SpellDescription$ CARDNAME gets +1/+1 until end of turn. A:AB$ Pump | Cost$ B tapXType<1/Creature.Other> | Defined$ Self | NumAtt$ +1 | NumDef$ +1 | SpellDescription$ CARDNAME gets +1/+1 until end of turn.
Oracle:Defender\n{B}, Tap another untapped creature you control: Black Oak of Odunos gets +1/+1 until end of turn. Oracle:Defender\n{B}, Tap another untapped creature you control: Black Oak of Odunos gets +1/+1 until end of turn.

View File

@@ -6,6 +6,6 @@ K:Flying
T:Mode$ DamageDone | ValidSource$ Card.Self | ValidTarget$ Player | CombatDamage$ True | Execute$ TrigDigUntil | TriggerZones$ Battlefield | TriggerDescription$ Whenever CARDNAME deals combat damage to a player, that player exiles cards from the top of their library until they exile an instant or sorcery card. You may cast that card without paying its mana cost. Then that player puts the exiled cards that weren't cast this way on the bottom of their library in a random order. T:Mode$ DamageDone | ValidSource$ Card.Self | ValidTarget$ Player | CombatDamage$ True | Execute$ TrigDigUntil | TriggerZones$ Battlefield | TriggerDescription$ Whenever CARDNAME deals combat damage to a player, that player exiles cards from the top of their library until they exile an instant or sorcery card. You may cast that card without paying its mana cost. Then that player puts the exiled cards that weren't cast this way on the bottom of their library in a random order.
SVar:TrigDigUntil:DB$ DigUntil | Defined$ TriggeredTarget | Valid$ Instant,Sorcery | ValidDescription$ instant or sorcery | FoundDestination$ Exile | RevealedDestination$ Exile | RememberFound$ True | RememberRevealed$ True | IsCurse$ True | SubAbility$ DBPlay | SpellDescription$ Whenever CARDNAME deals combat damage to a player, that player exiles cards from the top of their library until they exile an instant or sorcery card. You may cast that card without paying its mana cost. Then that player puts the exiled cards that weren't cast this way on the bottom of their library in a random order. SVar:TrigDigUntil:DB$ DigUntil | Defined$ TriggeredTarget | Valid$ Instant,Sorcery | ValidDescription$ instant or sorcery | FoundDestination$ Exile | RevealedDestination$ Exile | RememberFound$ True | RememberRevealed$ True | IsCurse$ True | SubAbility$ DBPlay | SpellDescription$ Whenever CARDNAME deals combat damage to a player, that player exiles cards from the top of their library until they exile an instant or sorcery card. You may cast that card without paying its mana cost. Then that player puts the exiled cards that weren't cast this way on the bottom of their library in a random order.
SVar:DBPlay:DB$ Play | Defined$ Remembered | ValidZone$ Exile | Valid$ Instant.IsRemembered,Sorcery.IsRemembered | ValidSA$ Spell | WithoutManaCost$ True | RememberObjects$ Remembered | Optional$ True | ForgetTargetRemembered$ True | SubAbility$ DBRestRandomOrder SVar:DBPlay:DB$ Play | Defined$ Remembered | ValidZone$ Exile | Valid$ Instant.IsRemembered,Sorcery.IsRemembered | ValidSA$ Spell | WithoutManaCost$ True | RememberObjects$ Remembered | Optional$ True | ForgetTargetRemembered$ True | SubAbility$ DBRestRandomOrder
SVar:DBRestRandomOrder:DB$ ChangeZoneAll | ChangeType$ Card.IsRemembered | Origin$ Library | Destination$ Library | LibraryPosition$ -1 | RandomOrder$ True | SubAbility$ DBCleanup SVar:DBRestRandomOrder:DB$ ChangeZoneAll | ChangeType$ Card.IsRemembered | Origin$ Exile | Destination$ Library | LibraryPosition$ -1 | RandomOrder$ True | SubAbility$ DBCleanup
SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True
Oracle:Flying\nWhenever Dazzling Sphinx deals combat damage to a player, that player exiles cards from the top of their library until they exile an instant or sorcery card. You may cast that card without paying its mana cost. Then that player puts the exiled cards that weren't cast this way on the bottom of their library in a random order. Oracle:Flying\nWhenever Dazzling Sphinx deals combat damage to a player, that player exiles cards from the top of their library until they exile an instant or sorcery card. You may cast that card without paying its mana cost. Then that player puts the exiled cards that weren't cast this way on the bottom of their library in a random order.

View File

@@ -2,9 +2,10 @@ Name:Kumena, Tyrant of Orazca
ManaCost:1 G U ManaCost:1 G U
Types:Legendary Creature Merfolk Shaman Types:Legendary Creature Merfolk Shaman
PT:2/4 PT:2/4
A:AB$ Pump | Cost$ tapXType<1/Merfolk.Other> | CostDesc$ Tap another untapped Merfolk you control: | Defined$ Self | KW$ HIDDEN Unblockable | AILogic$ BeforeCombat | SpellDescription$ CARDNAME can't be blocked this turn. A:AB$ Pump | Cost$ tapXType<1/Merfolk.Other> | Defined$ Self | KW$ HIDDEN Unblockable | AILogic$ BeforeCombat | SpellDescription$ CARDNAME can't be blocked this turn.
A:AB$ Draw | Cost$ tapXType<3/Merfolk> | CostDesc$ Tap three untapped Merfolk you control: | NumCards$ 1 | AILogic$ AtOppEOT | SpellDescription$ Draw a card. A:AB$ Draw | Cost$ tapXType<3/Merfolk> | NumCards$ 1 | AILogic$ AtOppEOT | SpellDescription$ Draw a card.
A:AB$ PutCounterAll | Cost$ tapXType<5/Merfolk> | CostDesc$ Tap five untapped Merfolk you control: | ValidCards$ Merfolk.YouCtrl | CounterType$ P1P1 | CounterNum$ 1 | AILogic$ AtOppEOT | SpellDescription$ Put a +1/+1 counter on each Merfolk you control. A:AB$ PutCounterAll | Cost$ tapXType<5/Merfolk> | ValidCards$ Merfolk.YouCtrl | CounterType$ P1P1 | CounterNum$ 1 | AILogic$ AtOppEOT | SpellDescription$ Put a +1/+1 counter on each Merfolk you control.
DeckHints:Type$Merfolk DeckHints:Type$Merfolk
SVar:BuffedBy:Merfolk SVar:BuffedBy:Merfolk
DeckHas:Ability$Counters
Oracle:Tap another untapped Merfolk you control: Kumena, Tyrant of Orazca can't be blocked this turn.\nTap three untapped Merfolk you control: Draw a card.\nTap five untapped Merfolk you control: Put a +1/+1 counter on each Merfolk you control. Oracle:Tap another untapped Merfolk you control: Kumena, Tyrant of Orazca can't be blocked this turn.\nTap three untapped Merfolk you control: Draw a card.\nTap five untapped Merfolk you control: Put a +1/+1 counter on each Merfolk you control.

View File

@@ -4,7 +4,7 @@ Types:Legendary Creature Angel
PT:6/4 PT:6/4
K:Flying K:Flying
K:Partner K:Partner
A:AB$ Protection | Cost$ tapXType<1/Creature.untapped+withFlying+Other/another creature you control> | CostDesc$ Tap another untapped creature you control with flying: | Gains$ Choice | Choices$ AnyColor | SpellDescription$ CARDNAME gains protection from the color of your choice until end of turn. A:AB$ Protection | Cost$ tapXType<1/Creature.withFlying+Other/creature you control with flying> | Gains$ Choice | Choices$ AnyColor | SpellDescription$ CARDNAME gains protection from the color of your choice until end of turn.
SVar:BuffedBy:Creature.withFlying SVar:BuffedBy:Creature.withFlying
DeckNeeds:Keyword$Flying DeckNeeds:Keyword$Flying
Oracle:Flying\nTap another untapped creature you control with flying: Radiant, Serra Archangel gains protection from the color of your choice until end of turn.\nPartner (You can have two commanders if both have partner.) Oracle:Flying\nTap another untapped creature you control with flying: Radiant, Serra Archangel gains protection from the color of your choice until end of turn.\nPartner (You can have two commanders if both have partner.)

View File

@@ -3,5 +3,5 @@ ManaCost:W
Types:Creature Bird Types:Creature Bird
PT:1/1 PT:1/1
K:Flying K:Flying
A:AB$ Venture | Cost$ 3 T tapXType<1/Creature/creature> | SorcerySpeed$ True | SpellDescription$ Venture into the dungeon. Activate only as a sorcery. (Enter the first room or advance to the next room.) A:AB$ Venture | Cost$ 3 T tapXType<1/Creature.Other> | SorcerySpeed$ True | SpellDescription$ Venture into the dungeon. Activate only as a sorcery. (Enter the first room or advance to the next room.)
Oracle:Flying\n{3}, {T}, Tap another untapped creature you control: Venture into the dungeon. Activate only as a sorcery. (Enter the first room or advance to the next room.) Oracle:Flying\n{3}, {T}, Tap another untapped creature you control: Venture into the dungeon. Activate only as a sorcery. (Enter the first room or advance to the next room.)

View File

@@ -2,7 +2,7 @@ Name:Shadow Stinger
ManaCost:2 B ManaCost:2 B
Types:Creature Vampire Rogue Types:Creature Vampire Rogue
PT:1/4 PT:1/4
A:AB$ Pump | Cost$ tapXType<1/Rogue> | CostDesc$ Tap another untapped Rogue you control: | Defined$ Self | KW$ Deathtouch | SpellDescription$ CARDNAME gains deathtouch until end of turn. A:AB$ Pump | Cost$ tapXType<1/Rogue> | Defined$ Self | KW$ Deathtouch | SpellDescription$ CARDNAME gains deathtouch until end of turn.
T:Mode$ DamageDone | ValidSource$ Card.Self | ValidTarget$ Player | CombatDamage$ True | Execute$ TrigMill | TriggerZones$ Battlefield | TriggerDescription$ Whenever CARDNAME deals combat damage to a player, that player mills three cards. (They put the top three cards of their library into their graveyard.) T:Mode$ DamageDone | ValidSource$ Card.Self | ValidTarget$ Player | CombatDamage$ True | Execute$ TrigMill | TriggerZones$ Battlefield | TriggerDescription$ Whenever CARDNAME deals combat damage to a player, that player mills three cards. (They put the top three cards of their library into their graveyard.)
SVar:TrigMill:DB$ Mill | Defined$ TriggeredTarget | NumCards$ 3 SVar:TrigMill:DB$ Mill | Defined$ TriggeredTarget | NumCards$ 3
DeckHas:Ability$Mill DeckHas:Ability$Mill

View File

@@ -2,7 +2,7 @@ Name:Sure-Footed Infiltrator
ManaCost:3 U ManaCost:3 U
Types:Creature Merfolk Rogue Types:Creature Merfolk Rogue
PT:2/3 PT:2/3
A:AB$ Pump | Cost$ tapXType<1/Rogue.Other> | CostDesc$ Tap another untapped Rogue you control: | Defined$ Self | KW$ HIDDEN Unblockable | StackDescription$ SpellDescription | SpellDescription$ CARDNAME can't be blocked this turn. A:AB$ Pump | Cost$ tapXType<1/Rogue.Other> | Defined$ Self | KW$ HIDDEN Unblockable | StackDescription$ SpellDescription | SpellDescription$ CARDNAME can't be blocked this turn.
T:Mode$ DamageDone | ValidSource$ Card.Self | ValidTarget$ Player | CombatDamage$ True | Execute$ TrigDraw | TriggerDescription$ Whenever CARDNAME deals combat damage to a player, draw a card. T:Mode$ DamageDone | ValidSource$ Card.Self | ValidTarget$ Player | CombatDamage$ True | Execute$ TrigDraw | TriggerDescription$ Whenever CARDNAME deals combat damage to a player, draw a card.
SVar:TrigDraw:DB$ Draw | Defined$ You | NumCards$ 1 SVar:TrigDraw:DB$ Draw | Defined$ You | NumCards$ 1
DeckNeeds:Type$Rogue DeckNeeds:Type$Rogue

View File

@@ -0,0 +1,9 @@
Name:Iron Apprentice
ManaCost:1
Types:Artifact Creature Construct
PT:0/0
K:etbCounter:P1P1:1
T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Card.Self+HasCounters | Execute$ TrigPutCounter | TriggerDescription$ When CARDNAME dies, if it had counters on it, put those counters on target creature you control.
SVar:TrigPutCounter:DB$ PutCounter | ValidTgts$ Creature.YouCtrl | TgtPrompt$ Select target creature you control | CounterType$ EachFromSource | EachFromSource$ TriggeredCardLKICopy
DeckHas:Ability$Counters
Oracle:Iron Apprentice enters the battlefield with a +1/+1 counter on it.\nWhen Iron Apprentice dies, if it had counters on it, put those counters on target creature you control.

View File

@@ -0,0 +1,7 @@
Name:Papercraft Decoy
ManaCost:2
Types:Artifact Creature Frog
PT:2/1
T:Mode$ ChangesZone | Origin$ Battlefield | ValidCard$ Card.Self | Execute$ TrigDraw1 | TriggerDescription$ When CARDNAME leaves the battlefield, you may pay {2}. If you do, draw a card.
SVar:TrigDraw1:AB$ Draw | Cost$ 2 | Defined$ You | NumCards$ 1
Oracle:When Papercraft Decoy leaves the battlefield, you may pay {2}. If you do, draw a card.

View File

@@ -0,0 +1,10 @@
Name:Patchwork Automaton
ManaCost:2
Types:Artifact Creature Construct
PT:1/1
K:Ward:2
T:Mode$ SpellCast | ValidCard$ Artifact | ValidActivatingPlayer$ You | TriggerZones$ Battlefield | Execute$ TrigPutCounter | TriggerDescription$ Whenever you cast an artifact spell, put a +1/+1 counter on CARDNAME.
SVar:TrigPutCounter:DB$ PutCounter | Defined$ Self | CounterType$ P1P1 | CounterNum$ 1
DeckHas:Ability$Counters
DeckHints:Type$Artifact
Oracle:Ward {2} (Whenever this creature becomes the target of a spell or ability an opponent controls, counter it unless that player pays {2}.)\nWhenever you cast an artifact spell, put a +1/+1 counter on Patchwork Automaton.

View File

@@ -0,0 +1,10 @@
Name:Reito Sentinel
ManaCost:3
Types:Artifact Creature Construct
PT:3/3
K:Defender
T:Mode$ ChangesZone | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigMill | TriggerDescription$ When CARDNAME enters the battlefield, target player mills three cards.
SVar:TrigMill:DB$ Mill | NumCards$ 3 | ValidTgts$ Player | TgtPrompt$ Select target player
A:AB$ ChangeZone | Cost$ 3 | ValidTgts$ Card | TgtPrompt$ Select target card in a graveyard | Origin$ Graveyard | Destination$ Library | LibraryPosition$ -1 | SpellDescription$ Put target card from a graveyard on the bottom of its owner's library.
DeckHas:Ability$Mill|Graveyard
Oracle:Defender\nWhen Reito Sentinel enters the battlefield, target player mills three cards. (They put the top three cards of their library into their graveyard.)\n{3}: Put target card from a graveyard on the bottom of its owner's library.

View File

@@ -0,0 +1,9 @@
Name:Searchlight Companion
ManaCost:3
Types:Artifact Creature Drone
PT:1/1
K:Flying
T:Mode$ ChangesZone | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigToken | TriggerDescription$ When CARDNAME enters the battlefield, create a 1/1 colorless Spirit creature token.
SVar:TrigToken:DB$ Token | TokenAmount$ 1 | TokenScript$ c_1_1_spirit
DeckHas:Ability$Token & Type$Spirit
Oracle:Flying\nWhen Searchlight Companion enters the battlefield, create a 1/1 colorless Spirit creature token.

View File

@@ -0,0 +1,8 @@
Name:Shrine Steward
ManaCost:5
Types:Artifact Creature Construct
PT:3/2
T:Mode$ ChangesZone | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigChange | OptionalDecider$ You | TriggerDescription$ When CARDNAME enters the battlefield, you may search your library for an Aura or Shrine card, reveal it, put it into your hand, then shuffle.
SVar:TrigChange:DB$ ChangeZone | Origin$ Library | Destination$ Hand | ChangeType$ Aura,Shrine | ChangeNum$ 1 | ChangeTypeDesc$ Aura or Shrine card | ShuffleNonMandatory$ True
DeckNeeds:Type$Aura|Shrine
Oracle:When Shrine Steward enters the battlefield, you may search your library for an Aura or Shrine card, reveal it, put it into your hand, then shuffle.

View File

@@ -0,0 +1,8 @@
Name:Thundersteel Colossus
ManaCost:7
Types:Artifact Vehicle
PT:7/7
K:Trample
K:Haste
K:Crew:2
Oracle:Trample, haste\nCrew 2 (Tap any number of creatures you control with total power 2 or more: This Vehicle becomes an artifact creature until end of turn.)

View File

@@ -0,0 +1,11 @@
Name:Towashi Guide-Bot
ManaCost:4
Types:Artifact Creature Construct
PT:3/2
T:Mode$ ChangesZone | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigPutCounter | TriggerDescription$ When CARDNAME enters the battlefield, put a +1/+1 counter on target creature you control.
SVar:TrigPutCounter:DB$ PutCounter | ValidTgts$ Creature.YouCtrl | TgtPrompt$ Select target creature you control | CounterType$ P1P1 | CounterNum$ 1
A:AB$ Draw | Cost$ 4 T | NumCards$ 1 | ReduceCost$ X | SpellDescription$ Draw a card. This ability costs {1} less to activate for each modified creature you control.
SVar:X:Count$Valid Creature.modified+YouCtrl
DeckHas:Ability$Counters
DeckHints:Type$Aura|Equipment & Ability$Counters
Oracle:When Towashi Guide-Bot enters the battlefield, put a +1/+1 counter on target creature you control.\n{4}, {T}: Draw a card. This ability costs {1} less to activate for each modified creature you control. (Equipment, Auras you control, and counters are modifications.)

View File

@@ -4,7 +4,7 @@ Types:Creature Human Soldier Ally
PT:*/* PT:*/*
S:Mode$ Continuous | EffectZone$ All | CharacteristicDefining$ True | SetPower$ X | SetToughness$ X | Description$ CARDNAME's power and toughness are each equal to the number of creatures you control. S:Mode$ Continuous | EffectZone$ All | CharacteristicDefining$ True | SetPower$ X | SetToughness$ X | Description$ CARDNAME's power and toughness are each equal to the number of creatures you control.
SVar:X:Count$Valid Creature.YouCtrl SVar:X:Count$Valid Creature.YouCtrl
A:AB$ GenericChoice | Cost$ tapXType<1/Ally.Other> | CostDesc$ Tap another untapped Ally you control: | Choices$ ChooseFirstStrike,ChooseVigilance,ChooseTrample | SpellDescription$ CARDNAME gains your choice of first strike, vigilance, or trample until end of turn. A:AB$ GenericChoice | Cost$ tapXType<1/Ally.Other> | Choices$ ChooseFirstStrike,ChooseVigilance,ChooseTrample | SpellDescription$ CARDNAME gains your choice of first strike, vigilance, or trample until end of turn.
SVar:ChooseFirstStrike:DB$ Pump | Defined$ Self | KW$ First Strike | SpellDescription$ CARDNAME gains first strike until end of turn. SVar:ChooseFirstStrike:DB$ Pump | Defined$ Self | KW$ First Strike | SpellDescription$ CARDNAME gains first strike until end of turn.
SVar:ChooseVigilance:DB$ Pump | Defined$ Self | KW$ Vigilance | SpellDescription$ CARDNAME gains vigilance until end of turn. SVar:ChooseVigilance:DB$ Pump | Defined$ Self | KW$ Vigilance | SpellDescription$ CARDNAME gains vigilance until end of turn.
SVar:ChooseTrample:DB$ Pump | Defined$ Self | KW$ Trample | SpellDescription$ CARDNAME gains trample until end of turn. SVar:ChooseTrample:DB$ Pump | Defined$ Self | KW$ Trample | SpellDescription$ CARDNAME gains trample until end of turn.

View File

@@ -463,6 +463,7 @@ ScryfallCode=SLD
588 R Sphere of Safety @Johannes Voss 588 R Sphere of Safety @Johannes Voss
589 R Arcane Signet @Dan Frazier 589 R Arcane Signet @Dan Frazier
591 R Crash Through @Tyler Walpole 591 R Crash Through @Tyler Walpole
596 R Persistent Petitioners @Crom
597 R Persistent Petitioners @Death Burger 597 R Persistent Petitioners @Death Burger
598 R Persistent Petitioners @Feifei Ruan 598 R Persistent Petitioners @Feifei Ruan
603 M Eldrazi Monument @Cosmin Podar 603 M Eldrazi Monument @Cosmin Podar

View File

@@ -0,0 +1,13 @@
[metadata]
Code=PL22
Date=2022-02-25
Name=Year of the Tiger 2022
Type=Promo
ScryfallCode=PL22
[cards]
1 R Temur Sabertooth @tswck
2 R Jedit Ojanen @
3 M Yuriko, the Tiger's Shadow @
4 M Snapdax, Apex of the Hunt @
5 R Herald's Horn @tswck

View File

@@ -760,7 +760,8 @@ public class HumanCostDecision extends CostDecisionMakerBase {
if (num == 0) { if (num == 0) {
return PaymentDecision.number(0); return PaymentDecision.number(0);
} }
if (hand.size() == num) { // player might not want to pay if from a trigger
if (!ability.hasSVar("IsCastFromPlayEffect") && hand.size() == num) {
return PaymentDecision.card(hand); return PaymentDecision.card(hand);
} }

View File

@@ -472,7 +472,7 @@ public class PlayerControllerHuman extends PlayerController implements IGameCont
return null; return null;
} }
String announceTitle = ("X".equals(announce)) ? announce : ability.getParamOrDefault("AnnounceTitle", announce); String announceTitle = "X".equals(announce) ? announce : ability.getParamOrDefault("AnnounceTitle", announce);
if (cost.isMandatory()) { if (cost.isMandatory()) {
return chooseNumber(ability, localizer.getMessage("lblChooseAnnounceForCard", announceTitle, return chooseNumber(ability, localizer.getMessage("lblChooseAnnounceForCard", announceTitle,
CardTranslation.getTranslatedName(ability.getHostCard().getName())) , min, max); CardTranslation.getTranslatedName(ability.getHostCard().getName())) , min, max);