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) {
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
// 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()));
@@ -720,7 +725,7 @@ public class AiAttackController {
continue;
}
boolean mustAttack = false;
// TODO for nextTurn check if it was temporary
// TODO this might result into attacking the wrong player
if (attacker.isGoaded()) {
mustAttack = true;
} else if (attacker.getSVar("MustAttack").equals("True")) {
@@ -737,7 +742,7 @@ public class AiAttackController {
mustAttack = true;
}
}
if (mustAttack || (attacker.getController().getMustAttackEntity() != null && nextTurn) || (attacker.getController().getMustAttackEntityThisTurn() != null && !nextTurn)) {
if (mustAttack ||attacker.getController().getMustAttackEntityThisTurn() != null) {
combat.addAttacker(attacker, defender);
attackersLeft.remove(attacker);
numForcedAttackers++;

View File

@@ -426,7 +426,6 @@ public class AiBlockController {
}
attackersLeft = new ArrayList<>(currentAttackers);
currentAttackers = new ArrayList<>(attackersLeft);
boolean considerTripleBlock = true;
@@ -437,6 +436,11 @@ public class AiBlockController {
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);
blockers = getPossibleBlockers(combat, attacker, blockersLeft, false);
@@ -446,11 +450,6 @@ public class AiBlockController {
int currentValue; // The value of the creatures in the blockgang
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
// Don't use blockers without First Strike or Double Strike if attacker has it
usableBlockers = CardLists.filter(blockers, new Predicate<Card>() {
@@ -460,8 +459,7 @@ public class AiBlockController {
&& !ComputerUtilCombat.dealsFirstStrikeDamage(c, false, combat)) {
return false;
}
final boolean randomTrade = wouldLikeToRandomlyTrade(attacker, c, combat);
return lifeInDanger || randomTrade || ComputerUtilCard.evaluateCreature(c) + diff < ComputerUtilCard.evaluateCreature(attacker);
return lifeInDanger || wouldLikeToRandomlyTrade(attacker, c, combat) || ComputerUtilCard.evaluateCreature(c) + diff < ComputerUtilCard.evaluateCreature(attacker);
}
});
if (usableBlockers.size() < 2) {

View File

@@ -1343,7 +1343,7 @@ public class ComputerUtil {
}
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) {
if (c.getSVar("SacMe").equals("6")) {
return true;
@@ -1620,7 +1620,7 @@ public class ComputerUtil {
objects = AbilityUtils.getDefinedObjects(source, topStack.getParam("Defined"), topStack);
} else if (topStack.hasParam("ValidCards")) {
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 {
return threatened;
}
@@ -2822,8 +2822,6 @@ public class ComputerUtil {
pRating /= 5;
}
System.out.println("Board position evaluation for " + p + ": " + pRating);
if (pRating > bestBoardRating) {
bestBoardRating = pRating;
bestBoardPosition = p;

View File

@@ -36,7 +36,7 @@ public class ComputerUtilAbility {
public boolean apply(final Card c) {
if (!c.getSVar("NeedsToPlay").isEmpty()) {
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()) {
return false;
}

View File

@@ -1951,7 +1951,7 @@ public class ComputerUtilCard {
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()) {
return AiPlayDecision.MissingNeededCards;
}

View File

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

View File

@@ -144,7 +144,7 @@ public class ComputerUtilCost {
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()) {
continue;
}

View File

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

View File

@@ -184,7 +184,7 @@ public class ChooseCardAi extends SpellAbilityAi {
choice = ComputerUtilCard.getBestCreatureAI(options);
} else if (logic.equals("Clone")) {
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()) {
options = newOptions;
}
@@ -194,7 +194,7 @@ public class ChooseCardAi extends SpellAbilityAi {
choice = Aggregates.random(options);
} else if (logic.equals("Untap")) {
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()) {
options = newOptions;
}

View File

@@ -198,7 +198,7 @@ public class CloneAi extends SpellAbilityAi {
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()) {
options = newOptions;
}

View File

@@ -248,7 +248,7 @@ public class CopyPermanentAi extends SpellAbilityAi {
final boolean canCopyLegendary = sa.hasParam("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
return CardLists.getValidCards(options, filter.split(","), ctrl, host, sa);
return CardLists.getValidCards(options, filter, ctrl, host, sa);
}
@Override

View File

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

View File

@@ -251,7 +251,7 @@ public class DamageAllAi extends SpellAbilityAi {
// TODO: X may be something different than X paid
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>() {
@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
for (Player opponent: ai.getOpponents()) {
CardCollection opplist = CardLists.getValidCards(opponent.getCardsIn(ZoneType.Battlefield), valid.split(","), source.getController(), source, sa);
CardCollection ailist = CardLists.getValidCards(ai.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, source.getController(), source, sa);
opplist = CardLists.filter(opplist, predicate);
ailist = CardLists.filter(ailist, predicate);

View File

@@ -68,7 +68,7 @@ public class DigUntilAi extends SpellAbilityAi {
} else {
if (sa.hasParam("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;
}
}

View File

@@ -29,7 +29,7 @@ public class RegenerateAllAi extends SpellAbilityAi {
final String valid = sa.getParamOrDefault("ValidCards", "");
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));
if (list.size() == 0) {

View File

@@ -79,7 +79,7 @@ public class SacrificeAi extends SpellAbilityAi {
List<Card> list = null;
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) {
return false;
} finally {
@@ -141,7 +141,7 @@ public class SacrificeAi extends SpellAbilityAi {
List<Card> humanList = null;
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) {
return false;
} finally {
@@ -155,7 +155,7 @@ public class SacrificeAi extends SpellAbilityAi {
} else if (defined.equals("You")) {
List<Card> computerList = null;
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) {
return false;
} finally {

View File

@@ -241,14 +241,14 @@ public class TokenAi extends SpellAbilityAi {
final String valid = topStack.getParamOrDefault("SacValid", "Card.Self");
String num = sa.getParamOrDefault("Amount", "1");
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);
list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(topStack, true));
// only care about saving single creature for now
if (!list.isEmpty() && nTokens > 0 && list.size() == nToSac) {
ComputerUtilCard.sortByEvaluateCreature(list);
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));
return ComputerUtilCard.evaluateCreature(token) < ComputerUtilCard.evaluateCreature(list.get(0))
&& 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);
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
PlayerCollection goodControllers = aiPlayer.getAllies();
goodControllers.add(aiPlayer);
@@ -43,7 +43,7 @@ public class UntapAllAi extends SpellAbilityAi {
if (sa.hasParam("ValidCards")) {
String valid = sa.getParam("ValidCards");
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();
}

View File

@@ -368,7 +368,7 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
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);
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);
int right = AbilityUtils.calculateAmount(getHostCard(), rightString, this);

View File

@@ -346,7 +346,7 @@ public class AbilityUtils {
candidates = game.getCardsIn(ZoneType.smartValueOf(zone));
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;
} else {
CardCollection list = null;
@@ -977,7 +977,7 @@ public class AbilityUtils {
String var = sa.getParam("AbilityCount");
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);
}
}
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")) {
return doXMath(Aggregates.sum(list, CardPredicates.Accessors.fnGetNetToughness), expr, c, ctb);
}
@@ -1911,7 +1911,7 @@ public class AbilityUtils {
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);
}
@@ -1940,14 +1940,14 @@ public class AbilityUtils {
if (sq[0].startsWith("LastStateBattlefield")) {
final String[] k = l[0].split(" ");
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);
}
if (sq[0].startsWith("LastStateGraveyard")) {
final String[] k = l[0].split(" ");
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);
}
@@ -2176,7 +2176,7 @@ public class AbilityUtils {
if (sq[0].startsWith("Devoured")) {
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);
}
@@ -2451,8 +2451,7 @@ public class AbilityUtils {
if (sq[0].startsWith("ColorsCtrl")) {
final String restriction = l[0].substring(11);
final String[] rest = restriction.split(",");
final CardCollection list = CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), rest, player, c, ctb);
final CardCollection list = CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), restriction, player, c, ctb);
byte n = 0;
for (final Card card : list) {
n |= card.getColor().getColor();
@@ -2711,8 +2710,7 @@ public class AbilityUtils {
// Count$SumPower_valid
if (sq[0].startsWith("SumPower")) {
final String[] restrictions = l[0].split("_");
final String[] rest = restrictions[1].split(",");
CardCollection filteredCards = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), rest, player, c, ctb);
CardCollection filteredCards = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), restrictions[1], player, 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"))
zone = ZoneType.Graveyard;
final String[] restrictions = l[0].split("_");
final String[] rest = restrictions[1].split(",");
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);
}
@@ -2821,8 +2818,7 @@ public class AbilityUtils {
if (sq[0].startsWith("GreatestToughness_")) {
final String restriction = l[0].substring(18);
final String[] rest = restriction.split(",");
CardCollection list = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), rest, player, c, ctb);
CardCollection list = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), restriction, player, c, ctb);
int highest = 0;
for (final Card crd : list) {
if (crd.getNetToughness() > highest) {
@@ -2834,8 +2830,7 @@ public class AbilityUtils {
if (sq[0].startsWith("HighestCMC_")) {
final String restriction = l[0].substring(11);
final String[] rest = restriction.split(",");
CardCollection list = CardLists.getValidCards(game.getCardsInGame(), rest, player, c, ctb);
CardCollection list = CardLists.getValidCards(game.getCardsInGame(), restriction, player, c, ctb);
int highest = 0;
for (final Card crd : list) {
// dont check for Split card anymore
@@ -2874,8 +2869,7 @@ public class AbilityUtils {
if (sq[0].startsWith("DifferentCardNames_")) {
final List<String> crdname = Lists.newArrayList();
final String restriction = l[0].substring(19);
final String[] rest = restriction.split(",");
CardCollection list = CardLists.getValidCards(game.getCardsInGame(), rest, player, c, ctb);
CardCollection list = CardLists.getValidCards(game.getCardsInGame(), restriction, player, c, ctb);
for (final Card card : list) {
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
@@ -2889,8 +2883,7 @@ public class AbilityUtils {
if (sq[0].startsWith("DifferentPower_")) {
final List<Integer> powers = Lists.newArrayList();
final String restriction = l[0].substring(15);
final String[] rest = restriction.split(",");
CardCollection list = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), rest, player, c, ctb);
CardCollection list = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), restriction, player, c, ctb);
for (final Card card : list) {
Integer pow = card.getNetPower();
if (!powers.contains(pow)) {
@@ -2915,8 +2908,7 @@ public class AbilityUtils {
if (sq[0].startsWith("ColorsCtrl")) {
final String restriction = l[0].substring(11);
final String[] rest = restriction.split(",");
final CardCollection list = CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), rest, player, c, ctb);
final CardCollection list = CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), restriction, player, c, ctb);
byte n = 0;
for (final Card card : list) {
n |= card.getColor().getColor();
@@ -3417,16 +3409,14 @@ public class AbilityUtils {
String[] lparts = l[0].split(" ", 2);
final List<ZoneType> vZone = ZoneType.listValueOf(lparts[0].split("Valid")[1]);
String restrictions = TextUtil.fastReplace(l[0], TextUtil.addSuffix(lparts[0]," "), "");
final String[] rest = restrictions.split(",");
CardCollection cards = CardLists.getValidCards(game.getCardsIn(vZone), rest, player, source, ctb);
CardCollection cards = CardLists.getValidCards(game.getCardsIn(vZone), restrictions, player, source, ctb);
return doXMath(cards.size(), m, source, ctb);
}
// count valid cards on the battlefield
if (l[0].startsWith("Valid ")) {
final String restrictions = l[0].substring(6);
final String[] rest = restrictions.split(",");
CardCollection cardsonbattlefield = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), rest, player, source, ctb);
CardCollection cardsonbattlefield = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), restrictions, player, 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 = CardLists.getValidCards(list, valid.split(","), host.getController(), host, sa);
list = CardLists.getValidCards(list, valid, host.getController(), host, sa);
for (final Card c : list) {
doAnimate(c, sa, power, toughness, types, removeTypes, finalColors,

View File

@@ -67,7 +67,7 @@ public class CharmEffect extends SpellAbilityEffect {
} else {
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 random = sa.hasParam("Random");
@@ -120,7 +120,7 @@ public class CharmEffect extends SpellAbilityEffect {
}
if (additionalDesc) {
String addDescS = (sa.getParam("AdditionalDescription"));
String addDescS = sa.getParam("AdditionalDescription");
if (optional) {
sb.append(". ").append(addDescS.trim());
} 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);
valid.remove(originalTarget);

View File

@@ -209,7 +209,7 @@ public class DigEffect extends SpellAbilityEffect {
if (changeValid.contains("ChosenType")) {
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) {
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)));
}
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);
if (toBeDiscarded.size() > 1) {
toBeDiscarded = GameActionUtil.orderCardsByTheirOwners(game, toBeDiscarded, ZoneType.Graveyard, sa);
@@ -250,8 +250,7 @@ public class DiscardEffect extends SpellAbilityEffect {
}
final String valid = sa.getParamOrDefault("DiscardValid", "Card");
String[] dValid = valid.split(",");
CardCollection validCards = CardLists.getValidCards(dPHand, dValid, source.getController(), source, sa);
CardCollection validCards = CardLists.getValidCards(dPHand, valid, source.getController(), source, sa);
Player chooser = p;
if (mode.equals("RevealYouChoose")) {

View File

@@ -25,6 +25,9 @@ import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardFactoryUtil;
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.player.Player;
import forge.game.replacement.ReplacementEffect;
@@ -70,7 +73,7 @@ public class PlayEffect extends SpellAbilityEffect {
Player controlledByPlayer = null;
long controlledByTimeStamp = -1;
final Game game = activator.getGame();
final boolean optional = sa.hasParam("Optional");
boolean optional = sa.hasParam("Optional");
boolean remember = sa.hasParam("RememberPlayed");
int amount = 1;
boolean hasTotalCMCLimit = sa.hasParam("WithTotalCMC");
@@ -329,9 +332,19 @@ public class PlayEffect extends SpellAbilityEffect {
tgtSA = tgtSA.copyWithDefinedCost(abCost);
}
if (!optional) {
// 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")) {
// for Kefnet only can reduce colorless cost

View File

@@ -29,7 +29,7 @@ public class RegenerateAllEffect extends RegenerateBaseEffect {
final String valid = sa.getParamOrDefault("ValidCards", "");
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
createRegenerationEffect(sa, list);

View File

@@ -84,7 +84,7 @@ public class RepeatEffect extends SpellAbilityEffect {
} else {
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);
int right = AbilityUtils.calculateAmount(sa.getHostCard(), rightString, sa);

View File

@@ -140,7 +140,7 @@ public class UnattachAllEffect extends SpellAbilityEffect {
String valid = sa.getParam("UnattachValid");
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) {
handleUnattachment((GameEntity) o, c);
}

View File

@@ -37,7 +37,7 @@ public class UntapAllEffect extends SpellAbilityEffect {
}
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");
for (Card c : list) {

View File

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

View File

@@ -1452,7 +1452,7 @@ public class CardFactoryUtil {
final String manacost = k[1];
final String abStrReveal = "DB$ Reveal | Defined$ You | RevealDefined$ Self"
+ " | 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";

View File

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

View File

@@ -22,6 +22,7 @@ import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import forge.card.CardType;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
@@ -834,9 +835,14 @@ public class Cost implements Serializable {
sb.append(Cost.NUM_NAMES[i]);
}
sb.append(" ").append(type);
sb.append(" ");
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();

View File

@@ -138,6 +138,7 @@ public class CostAdjustment {
count = Integer.parseInt(amount);
} else {
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);
} else {
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..."
// The caster is sa.getActivatingPlayer()
// 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 {
value = AbilityUtils.calculateAmount(hostCard, amount, staticAbility);
}

View File

@@ -19,6 +19,7 @@ package forge.game.cost;
import java.io.Serializable;
import forge.card.CardType;
import org.apache.commons.lang3.StringUtils;
import forge.game.CardTraitBase;
@@ -106,7 +107,11 @@ public abstract class CostPart implements Comparable<CostPart>, Cloneable, Seria
public final String getDescriptiveType() {
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;
import forge.card.CardType;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
@@ -87,21 +88,34 @@ public class CostTapType extends CostPartWithList {
@Override
public final String toString() {
final StringBuilder sb = new StringBuilder();
sb.append("Tap ");
final Integer i = this.convertAmount();
final String desc = this.getDescriptiveType();
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")) {
sb.append("two untapped creatures you control that share a creature type");
} else 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 ").append(num).append("or greater");
} else {
sb.append(Cost.convertAmountTypeToWords(i, this.getAmount(), "untapped " + desc));
} else if (type.contains("Other")) {
String rep = type.contains(".Other") ? ".Other" : "+Other";
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 {
sb.append(Cost.convertAmountTypeToWords(i, this.getAmount(), "untapped " + desc)).append(" you control");
}
return sb.toString();
}

View File

@@ -467,7 +467,7 @@ public class StaticAbility extends CardTraitBase implements IIdentifiable, Clone
CardCollectionView list = game.getCardsIn(zone);
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;
final String rightString = compare.substring(2);

View File

@@ -1070,7 +1070,7 @@ public final class StaticAbilityContinuous {
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)
if (affectedCardsOriginal != null) {

View File

@@ -74,7 +74,7 @@ public class TriggerAttackersDeclared extends Trigger {
CardCollection attackers = (CardCollection) runParams.get(AbilityKey.Attackers);
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<>();
for (Card attacker : attackers) {
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 Player p = ((Player)runParams.get(AbilityKey.Player));
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);
if (list.size() == 0) {
return false;

View File

@@ -64,7 +64,7 @@ public class TriggerLifeGained extends Trigger {
final String sIsPresent = this.getParam("ValidPlayerControls");
final Player p = ((Player)runParams.get(AbilityKey.Player));
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);
if (list.size() == 0) {
return false;

View File

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

View File

@@ -3,5 +3,5 @@ ManaCost:2 B
Types:Creature Zombie Treefolk
PT:0/5
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.

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.
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: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
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
Types:Legendary Creature Merfolk Shaman
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$ Draw | Cost$ tapXType<3/Merfolk> | CostDesc$ Tap three untapped Merfolk you control: | 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$ 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> | NumCards$ 1 | AILogic$ AtOppEOT | SpellDescription$ Draw a card.
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
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.

View File

@@ -4,7 +4,7 @@ Types:Legendary Creature Angel
PT:6/4
K:Flying
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
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.)

View File

@@ -3,5 +3,5 @@ ManaCost:W
Types:Creature Bird
PT:1/1
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.)

View File

@@ -2,7 +2,7 @@ Name:Shadow Stinger
ManaCost:2 B
Types:Creature Vampire Rogue
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.)
SVar:TrigMill:DB$ Mill | Defined$ TriggeredTarget | NumCards$ 3
DeckHas:Ability$Mill

View File

@@ -2,7 +2,7 @@ Name:Sure-Footed Infiltrator
ManaCost:3 U
Types:Creature Merfolk Rogue
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.
SVar:TrigDraw:DB$ Draw | Defined$ You | NumCards$ 1
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:*/*
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
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: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.

View File

@@ -463,6 +463,7 @@ ScryfallCode=SLD
588 R Sphere of Safety @Johannes Voss
589 R Arcane Signet @Dan Frazier
591 R Crash Through @Tyler Walpole
596 R Persistent Petitioners @Crom
597 R Persistent Petitioners @Death Burger
598 R Persistent Petitioners @Feifei Ruan
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) {
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);
}

View File

@@ -472,7 +472,7 @@ public class PlayerControllerHuman extends PlayerController implements IGameCont
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()) {
return chooseNumber(ability, localizer.getMessage("lblChooseAnnounceForCard", announceTitle,
CardTranslation.getTranslatedName(ability.getHostCard().getName())) , min, max);