Card: refactor hidden keywords into Table Structure

This commit is contained in:
Hans Mackowiak
2021-09-12 15:48:34 +00:00
committed by Michael Kamensky
parent 83a7cc8a3e
commit bfc938be57
60 changed files with 424 additions and 420 deletions

View File

@@ -42,7 +42,6 @@ import forge.game.combat.Combat;
import forge.game.combat.CombatUtil; import forge.game.combat.CombatUtil;
import forge.game.combat.GlobalAttackRestrictions; import forge.game.combat.GlobalAttackRestrictions;
import forge.game.keyword.Keyword; import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordInterface;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.player.PlayerPredicates; import forge.game.player.PlayerPredicates;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
@@ -742,14 +741,9 @@ public class AiAttackController {
// TODO: if there are other ways to tap this creature (like mana creature), then don't need to attack // TODO: if there are other ways to tap this creature (like mana creature), then don't need to attack
mustAttack = true; mustAttack = true;
} else { } else {
for (KeywordInterface inst : attacker.getKeywords()) { // TODO move to static Ability
String s = inst.getOriginal(); if (attacker.hasKeyword("CARDNAME attacks each combat if able.") || attacker.hasStartOfKeyword("CARDNAME attacks specific player each combat if able")) {
if (s.equals("CARDNAME attacks each turn if able.") mustAttack = true;
|| s.startsWith("CARDNAME attacks specific player each combat if able")
|| s.equals("CARDNAME attacks each combat if able.")) {
mustAttack = true;
break;
}
} }
} }
if (mustAttack || attacker.getController().getMustAttackEntity() != null || attacker.getController().getMustAttackEntityThisTurn() != null) { if (mustAttack || attacker.getController().getMustAttackEntity() != null || attacker.getController().getMustAttackEntityThisTurn() != null) {
@@ -1201,13 +1195,9 @@ public class AiAttackController {
int defPower = CardLists.getTotalPower(validBlockers, true, false); int defPower = CardLists.getTotalPower(validBlockers, true, false);
if (!hasCombatEffect) { if (!hasCombatEffect) {
for (KeywordInterface inst : attacker.getKeywords()) { if (attacker.hasKeyword(Keyword.WITHER) || attacker.hasKeyword(Keyword.INFECT)
String keyword = inst.getOriginal(); || attacker.hasKeyword(Keyword.LIFELINK) || attacker.hasKeyword(Keyword.AFFLICT)) {
if (keyword.equals("Wither") || keyword.equals("Infect") hasCombatEffect = true;
|| keyword.equals("Lifelink") || keyword.startsWith("Afflict")) {
hasCombatEffect = true;
break;
}
} }
} }

View File

@@ -38,6 +38,7 @@ import forge.game.combat.Combat;
import forge.game.combat.CombatUtil; import forge.game.combat.CombatUtil;
import forge.game.keyword.Keyword; import forge.game.keyword.Keyword;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.staticability.StaticAbilityCantAttackBlock;
import forge.game.trigger.Trigger; import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType; import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
@@ -183,9 +184,7 @@ public class AiBlockController {
List<Card> currentAttackers = new ArrayList<>(attackersLeft); List<Card> currentAttackers = new ArrayList<>(attackersLeft);
for (final Card attacker : attackersLeft) { for (final Card attacker : attackersLeft) {
if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT") if (StaticAbilityCantAttackBlock.getMinMaxBlocker(attacker, combat.getDefenderPlayerByAttacker(attacker)).getLeft() > 1) {
|| attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")
|| attacker.hasKeyword(Keyword.MENACE)) {
continue; continue;
} }
@@ -296,8 +295,7 @@ public class AiBlockController {
// 6. Blockers that don't survive until the next turn anyway // 6. Blockers that don't survive until the next turn anyway
for (final Card attacker : attackersLeft) { for (final Card attacker : attackersLeft) {
if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT") || attacker.hasKeyword(Keyword.MENACE) if (StaticAbilityCantAttackBlock.getMinMaxBlocker(attacker, combat.getDefenderPlayerByAttacker(attacker)).getLeft() > 1) {
|| attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) {
continue; continue;
} }
@@ -323,7 +321,17 @@ public class AiBlockController {
attackersLeft = (new ArrayList<>(currentAttackers)); attackersLeft = (new ArrayList<>(currentAttackers));
} }
static final Predicate<Card> rampagesOrNeedsManyToBlock = Predicates.or(CardPredicates.containsKeyword("Rampage"), CardPredicates.containsKeyword("CantBeBlockedByAmount GT")); private Predicate<Card> rampagesOrNeedsManyToBlock(final Combat combat) {
return Predicates.or(CardPredicates.hasKeyword(Keyword.RAMPAGE), new Predicate<Card>() {
@Override
public boolean apply(Card input) {
// select creature that has a max blocker
return StaticAbilityCantAttackBlock.getMinMaxBlocker(input, combat.getDefenderPlayerByAttacker(input)).getRight() < Integer.MAX_VALUE;
}
});
}
// Good Gang Blocks means a good trade or no trade // Good Gang Blocks means a good trade or no trade
/** /**
@@ -334,7 +342,7 @@ public class AiBlockController {
* @param combat a {@link forge.game.combat.Combat} object. * @param combat a {@link forge.game.combat.Combat} object.
*/ */
private void makeGangBlocks(final Combat combat) { private void makeGangBlocks(final Combat combat) {
List<Card> currentAttackers = CardLists.filter(attackersLeft, Predicates.not(rampagesOrNeedsManyToBlock)); List<Card> currentAttackers = CardLists.filter(attackersLeft, Predicates.not(rampagesOrNeedsManyToBlock(combat)));
List<Card> blockers; List<Card> blockers;
// Try to block an attacker without first strike with a gang of first strikers // Try to block an attacker without first strike with a gang of first strikers
@@ -528,7 +536,7 @@ public class AiBlockController {
// Try to block a Menace attacker with two blockers, neither of which will die // Try to block a Menace attacker with two blockers, neither of which will die
for (final Card attacker : attackersLeft) { for (final Card attacker : attackersLeft) {
if (!attacker.hasKeyword(Keyword.MENACE) && !attacker.hasStartOfKeyword("CantBeBlockedByAmount LT2")) { if (StaticAbilityCantAttackBlock.getMinMaxBlocker(attacker, combat.getDefenderPlayerByAttacker(attacker)).getLeft() <= 1) {
continue; continue;
} }
@@ -584,9 +592,7 @@ public class AiBlockController {
List<Card> killingBlockers; List<Card> killingBlockers;
for (final Card attacker : attackersLeft) { for (final Card attacker : attackersLeft) {
if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT") if (StaticAbilityCantAttackBlock.getMinMaxBlocker(attacker, combat.getDefenderPlayerByAttacker(attacker)).getLeft() > 1) {
|| attacker.hasKeyword(Keyword.MENACE)
|| attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) {
continue; continue;
} }
if (ComputerUtilCombat.attackerHasThreateningAfflict(attacker, ai)) { if (ComputerUtilCombat.attackerHasThreateningAfflict(attacker, ai)) {
@@ -635,10 +641,8 @@ public class AiBlockController {
Card attacker = attackers.get(0); Card attacker = attackers.get(0);
if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT") if (StaticAbilityCantAttackBlock.getMinMaxBlocker(attacker, combat.getDefenderPlayerByAttacker(attacker)).getLeft() > 1
|| attacker.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.") || attacker.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")
|| attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")
|| attacker.hasKeyword(Keyword.MENACE)
|| ComputerUtilCombat.attackerHasThreateningAfflict(attacker, ai)) { || ComputerUtilCombat.attackerHasThreateningAfflict(attacker, ai)) {
attackers.remove(0); attackers.remove(0);
makeChumpBlocks(combat, attackers); makeChumpBlocks(combat, attackers);
@@ -686,9 +690,7 @@ public class AiBlockController {
List<Card> currentAttackers = new ArrayList<>(attackersLeft); List<Card> currentAttackers = new ArrayList<>(attackersLeft);
for (final Card attacker : currentAttackers) { for (final Card attacker : currentAttackers) {
if (!attacker.hasStartOfKeyword("CantBeBlockedByAmount LT") if (StaticAbilityCantAttackBlock.getMinMaxBlocker(attacker, combat.getDefenderPlayerByAttacker(attacker)).getLeft() <= 1) {
&& !attacker.hasKeyword(Keyword.MENACE)
&& !attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) {
continue; continue;
} }
List<Card> possibleBlockers = getPossibleBlockers(combat, attacker, blockersLeft, true); List<Card> possibleBlockers = getPossibleBlockers(combat, attacker, blockersLeft, true);
@@ -720,14 +722,14 @@ public class AiBlockController {
List<Card> chumpBlockers; List<Card> chumpBlockers;
List<Card> tramplingAttackers = CardLists.getKeyword(attackers, Keyword.TRAMPLE); List<Card> tramplingAttackers = CardLists.getKeyword(attackers, Keyword.TRAMPLE);
tramplingAttackers = CardLists.filter(tramplingAttackers, Predicates.not(rampagesOrNeedsManyToBlock)); tramplingAttackers = CardLists.filter(tramplingAttackers, Predicates.not(rampagesOrNeedsManyToBlock(combat)));
// TODO - should check here for a "rampage-like" trigger that replaced the keyword: // TODO - should check here for a "rampage-like" trigger that replaced the keyword:
// "Whenever CARDNAME becomes blocked, it gets +1/+1 until end of turn for each creature blocking it." // "Whenever CARDNAME becomes blocked, it gets +1/+1 until end of turn for each creature blocking it."
for (final Card attacker : tramplingAttackers) { for (final Card attacker : tramplingAttackers) {
if (((attacker.hasStartOfKeyword("CantBeBlockedByAmount LT") || attacker.hasKeyword(Keyword.MENACE)) && !combat.isBlocked(attacker)) if (((StaticAbilityCantAttackBlock.getMinMaxBlocker(attacker, combat.getDefenderPlayerByAttacker(attacker)).getLeft() > 1) && !combat.isBlocked(attacker))
|| attacker.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.") || attacker.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")
|| attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) { || attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) {
continue; continue;
@@ -751,7 +753,7 @@ public class AiBlockController {
private void reinforceBlockersToKill(final Combat combat) { private void reinforceBlockersToKill(final Combat combat) {
List<Card> safeBlockers; List<Card> safeBlockers;
List<Card> blockers; List<Card> blockers;
List<Card> targetAttackers = CardLists.filter(blockedButUnkilled, Predicates.not(rampagesOrNeedsManyToBlock)); List<Card> targetAttackers = CardLists.filter(blockedButUnkilled, Predicates.not(rampagesOrNeedsManyToBlock(combat)));
// TODO - should check here for a "rampage-like" trigger that replaced // TODO - should check here for a "rampage-like" trigger that replaced
// the keyword: "Whenever CARDNAME becomes blocked, it gets +1/+1 until end of turn for each creature blocking it." // the keyword: "Whenever CARDNAME becomes blocked, it gets +1/+1 until end of turn for each creature blocking it."
@@ -1076,8 +1078,7 @@ public class AiBlockController {
} }
// assign blockers that have to block // assign blockers that have to block
chumpBlockers = CardLists.getKeyword(blockersLeft, "CARDNAME blocks each turn if able."); chumpBlockers = CardLists.getKeyword(blockersLeft, "CARDNAME blocks each combat if able.");
chumpBlockers.addAll(CardLists.getKeyword(blockersLeft, "CARDNAME blocks each combat if able."));
// if an attacker with lure attacks - all that can block // if an attacker with lure attacks - all that can block
for (final Card blocker : blockersLeft) { for (final Card blocker : blockersLeft) {
if (CombatUtil.mustBlockAnAttacker(blocker, combat, null)) { if (CombatUtil.mustBlockAnAttacker(blocker, combat, null)) {
@@ -1091,7 +1092,6 @@ public class AiBlockController {
for (final Card blocker : blockers) { for (final Card blocker : blockers) {
if (CombatUtil.canBlock(attacker, blocker, combat) && blockersLeft.contains(blocker) if (CombatUtil.canBlock(attacker, blocker, combat) && blockersLeft.contains(blocker)
&& (CombatUtil.mustBlockAnAttacker(blocker, combat, null) && (CombatUtil.mustBlockAnAttacker(blocker, combat, null)
|| blocker.hasKeyword("CARDNAME blocks each turn if able.")
|| blocker.hasKeyword("CARDNAME blocks each combat if able."))) { || blocker.hasKeyword("CARDNAME blocks each combat if able."))) {
combat.addBlocker(attacker, blocker); combat.addBlocker(attacker, blocker);
if (blocker.getMustBlockCards() != null) { if (blocker.getMustBlockCards() != null) {

View File

@@ -1654,10 +1654,11 @@ public class ComputerUtilCard {
Card pumped = CardFactory.copyCard(c, false); Card pumped = CardFactory.copyCard(c, false);
pumped.setSickness(c.hasSickness()); pumped.setSickness(c.hasSickness());
final long timestamp = c.getGame().getNextTimestamp(); final long timestamp = c.getGame().getNextTimestamp();
final List<String> kws = new ArrayList<>(); final List<String> kws = Lists.newArrayList();
final List<String> hiddenKws = Lists.newArrayList();
for (String kw : keywords) { for (String kw : keywords) {
if (kw.startsWith("HIDDEN")) { if (kw.startsWith("HIDDEN")) {
pumped.addHiddenExtrinsicKeyword(kw); hiddenKws.add(kw.substring(7));
} else { } else {
kws.add(kw); kws.add(kw);
} }
@@ -1686,7 +1687,13 @@ public class ComputerUtilCard {
pumped.addNewPT(c.getCurrentPower(), c.getCurrentToughness(), timestamp); pumped.addNewPT(c.getCurrentPower(), c.getCurrentToughness(), timestamp);
pumped.setPTBoost(c.getPTBoostTable()); pumped.setPTBoost(c.getPTBoostTable());
pumped.addPTBoost(power + berserkPower, toughness, timestamp, 0); pumped.addPTBoost(power + berserkPower, toughness, timestamp, 0);
pumped.addChangedCardKeywords(kws, null, false, false, timestamp, 0);
if (!kws.isEmpty()) {
pumped.addChangedCardKeywords(kws, null, false, false, timestamp, 0);
}
if (!hiddenKws.isEmpty()) {
pumped.addHiddenExtrinsicKeywords(timestamp, 0, hiddenKws);
}
Set<CounterType> types = c.getCounters().keySet(); Set<CounterType> types = c.getCounters().keySet();
for(CounterType ct : types) { for(CounterType ct : types) {
pumped.addCounterFireNoEvents(ct, c.getCounters(ct), ai, sa, true, null); pumped.addCounterFireNoEvents(ct, c.getCounters(ct), ai, sa, true, null);
@@ -1699,14 +1706,10 @@ public class ComputerUtilCard {
KeywordCollection copiedKeywords = new KeywordCollection(); KeywordCollection copiedKeywords = new KeywordCollection();
copiedKeywords.insertAll(pumped.getKeywords()); copiedKeywords.insertAll(pumped.getKeywords());
List<KeywordInterface> toCopy = Lists.newArrayList(); List<KeywordInterface> toCopy = Lists.newArrayList();
for (KeywordInterface k : c.getKeywords()) { for (KeywordInterface k : c.getUnhiddenKeywords()) {
KeywordInterface copiedKI = k.copy(c, true); KeywordInterface copiedKI = k.copy(c, true);
if (!copiedKeywords.contains(copiedKI.getOriginal())) { if (!copiedKeywords.contains(copiedKI.getOriginal())) {
if (copiedKI.getHidden()) { toCopy.add(copiedKI);
pumped.addHiddenExtrinsicKeyword(copiedKI);
} else {
toCopy.add(copiedKI);
}
} }
} }
final long timestamp2 = c.getGame().getNextTimestamp(); //is this necessary or can the timestamp be re-used? final long timestamp2 = c.getGame().getNextTimestamp(); //is this necessary or can the timestamp be re-used?
@@ -1744,7 +1747,7 @@ public class ComputerUtilCard {
if (!stAb.hasParam("AddPower") && !stAb.hasParam("AddToughness")) { if (!stAb.hasParam("AddPower") && !stAb.hasParam("AddToughness")) {
continue; continue;
} }
if (!vCard.isValid(stAb.getParam("Affected").split(","), c.getController(), c, stAb)) { if (!stAb.matchesValidParam("Affected", vCard)) {
continue; continue;
} }
int att = 0; int att = 0;

View File

@@ -110,7 +110,17 @@ public class ComputerUtilCombat {
return false; return false;
} }
for (final KeywordInterface inst : attacker.getKeywords()) { // TODO replace with Static Ability
for (final String keyword : attacker.getHiddenExtrinsicKeywords()) {
if (keyword.startsWith("CARDNAME attacks specific player each combat if able")) {
final String defined = keyword.split(":")[1];
final Player player = AbilityUtils.getDefinedPlayers(attacker, defined, null).get(0);
if (!defender.equals(player)) {
return false;
}
}
}
for (final KeywordInterface inst : attacker.getKeywords(Keyword.UNDEFINED)) {
final String keyword = inst.getOriginal(); final String keyword = inst.getOriginal();
if (keyword.startsWith("CARDNAME attacks specific player each combat if able")) { if (keyword.startsWith("CARDNAME attacks specific player each combat if able")) {
final String defined = keyword.split(":")[1]; final String defined = keyword.split(":")[1];

View File

@@ -8,7 +8,6 @@ import forge.game.card.Card;
import forge.game.card.CounterEnumType; import forge.game.card.CounterEnumType;
import forge.game.cost.CostPayEnergy; import forge.game.cost.CostPayEnergy;
import forge.game.keyword.Keyword; import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordInterface;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
public class CreatureEvaluator implements Function<Card, Integer> { public class CreatureEvaluator implements Function<Card, Integer> {
@@ -35,16 +34,15 @@ public class CreatureEvaluator implements Function<Card, Integer> {
} }
int power = getEffectivePower(c); int power = getEffectivePower(c);
final int toughness = getEffectiveToughness(c); final int toughness = getEffectiveToughness(c);
for (KeywordInterface kw : c.getKeywords()) {
String keyword = kw.getOriginal(); // TODO replace with ReplacementEffect checks
if (keyword.equals("Prevent all combat damage that would be dealt by CARDNAME.") if (c.hasKeyword("Prevent all combat damage that would be dealt by CARDNAME.")
|| keyword.equals("Prevent all damage that would be dealt by CARDNAME.") || c.hasKeyword("Prevent all damage that would be dealt by CARDNAME.")
|| keyword.equals("Prevent all combat damage that would be dealt to and dealt by CARDNAME.") || c.hasKeyword("Prevent all combat damage that would be dealt to and dealt by CARDNAME.")
|| keyword.equals("Prevent all damage that would be dealt to and dealt by CARDNAME.")) { || c.hasKeyword("Prevent all damage that would be dealt to and dealt by CARDNAME.")) {
power = 0; power = 0;
break;
}
} }
if (considerPT) { if (considerPT) {
value += addValue(power * 15, "power"); value += addValue(power * 15, "power");
value += addValue(toughness * 10, "toughness: " + toughness); value += addValue(toughness * 10, "toughness: " + toughness);
@@ -157,8 +155,7 @@ public class CreatureEvaluator implements Function<Card, Integer> {
} }
if (c.hasKeyword("CARDNAME can't block.")) { if (c.hasKeyword("CARDNAME can't block.")) {
value -= subValue(10, "cant-block"); value -= subValue(10, "cant-block");
} else if (c.hasKeyword("CARDNAME attacks each turn if able.") } else if (c.hasKeyword("CARDNAME attacks each combat if able.")) {
|| c.hasKeyword("CARDNAME attacks each combat if able.")) {
value -= subValue(10, "must-attack"); value -= subValue(10, "must-attack");
} else if (c.hasStartOfKeyword("CARDNAME attacks specific player each combat if able")) { } else if (c.hasStartOfKeyword("CARDNAME attacks specific player each combat if able")) {
value -= subValue(10, "must-attack-player"); value -= subValue(10, "must-attack-player");

View File

@@ -16,7 +16,6 @@ import com.google.common.collect.Lists;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import forge.ai.AiAttackController; import forge.ai.AiAttackController;
import forge.ai.AiBlockController;
import forge.ai.AiCardMemory; import forge.ai.AiCardMemory;
import forge.ai.AiController; import forge.ai.AiController;
import forge.ai.AiProps; import forge.ai.AiProps;

View File

@@ -24,7 +24,6 @@ import forge.game.card.CardFactory;
import forge.game.card.CounterType; import forge.game.card.CounterType;
import forge.game.card.token.TokenInfo; import forge.game.card.token.TokenInfo;
import forge.game.combat.Combat; import forge.game.combat.Combat;
import forge.game.keyword.KeywordInterface;
import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
@@ -295,8 +294,8 @@ public class GameCopier {
newCard.setChangedCardNames(c.getChangedCardNames()); newCard.setChangedCardNames(c.getChangedCardNames());
// TODO: Is this correct? Does it not duplicate keywords from enchantments and such? // TODO: Is this correct? Does it not duplicate keywords from enchantments and such?
for (KeywordInterface kw : c.getHiddenExtrinsicKeywords()) //for (KeywordInterface kw : c.getHiddenExtrinsicKeywords())
newCard.addHiddenExtrinsicKeyword(kw); // newCard.addHiddenExtrinsicKeyword(kw);
if (c.isTapped()) { if (c.isTapped()) {
newCard.setTapped(true); newCard.setTapped(true);
} }

View File

@@ -74,6 +74,7 @@ import forge.game.replacement.ReplacementType;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityPredicates; import forge.game.spellability.SpellAbilityPredicates;
import forge.game.staticability.StaticAbility; import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityCantAttackBlock;
import forge.game.staticability.StaticAbilityLayer; import forge.game.staticability.StaticAbilityLayer;
import forge.game.trigger.TriggerType; import forge.game.trigger.TriggerType;
import forge.game.zone.PlayerZone; import forge.game.zone.PlayerZone;
@@ -682,17 +683,25 @@ public class GameAction {
// need to refresh ability text for affected cards // need to refresh ability text for affected cards
for (final StaticAbility stAb : c.getStaticAbilities()) { for (final StaticAbility stAb : c.getStaticAbilities()) {
if (stAb.isSecondary() || if (stAb.isSuppressed() || !stAb.checkConditions()) {
!stAb.getParam("Mode").equals("CantBlockBy") ||
stAb.isSuppressed() || !stAb.checkConditions() ||
!stAb.hasParam("ValidAttacker") ||
(stAb.hasParam("ValidBlocker") && stAb.getParam("ValidBlocker").equals("Creature.Self"))) {
continue; continue;
} }
final Card host = stAb.getHostCard();
for (Card creature : Iterables.filter(game.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.CREATURES)) { if (stAb.getParam("Mode").equals("CantBlockBy")) {
if (creature.isValid(stAb.getParam("ValidAttacker").split(","), host.getController(), host, stAb)) { if (!stAb.hasParam("ValidAttacker") || (stAb.hasParam("ValidBlocker") && stAb.getParam("ValidBlocker").equals("Creature.Self"))) {
creature.updateAbilityTextForView(); continue;
}
for (Card creature : Iterables.filter(game.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.CREATURES)) {
if (stAb.matchesValidParam("ValidAttacker", creature)) {
creature.updateAbilityTextForView();
}
}
}
if (stAb.getParam("Mode").equals(StaticAbilityCantAttackBlock.MinMaxBlockerMode)) {
for (Card creature : Iterables.filter(game.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.CREATURES)) {
if (stAb.matchesValidParam("ValidCard", creature)) {
creature.updateAbilityTextForView();
}
} }
} }
} }

View File

@@ -174,18 +174,8 @@ public class StaticEffect {
final CardCollectionView affectedCards = getAffectedCards(); final CardCollectionView affectedCards = getAffectedCards();
final List<Player> affectedPlayers = getAffectedPlayers(); final List<Player> affectedPlayers = getAffectedPlayers();
boolean setPT = false;
String[] addHiddenKeywords = null;
boolean removeMayPlay = false; boolean removeMayPlay = false;
if (hasParam("SetPower") || hasParam("SetToughness")) {
setPT = true;
}
if (hasParam("AddHiddenKeyword")) {
addHiddenKeywords = getParam("AddHiddenKeyword").split(" & ");
}
if (hasParam("MayPlay")) { if (hasParam("MayPlay")) {
removeMayPlay = true; removeMayPlay = true;
} }
@@ -220,7 +210,7 @@ public class StaticEffect {
affectedCard.removeChangedTextColorWord(getTimestamp(), ability.getId()); affectedCard.removeChangedTextColorWord(getTimestamp(), ability.getId());
// remove set P/T // remove set P/T
if (setPT) { if (hasParam("SetPower") || hasParam("SetToughness")) {
affectedCard.removeNewPT(getTimestamp()); affectedCard.removeNewPT(getTimestamp());
} }
@@ -240,10 +230,8 @@ public class StaticEffect {
affectedCard.removeCantHaveKeyword(getTimestamp()); affectedCard.removeCantHaveKeyword(getTimestamp());
} }
if (addHiddenKeywords != null) { if (hasParam("AddHiddenKeyword")) {
for (final String k : addHiddenKeywords) { affectedCard.removeHiddenExtrinsicKeywords(timestamp, ability.getId());
affectedCard.removeHiddenExtrinsicKeyword(k);
}
} }
// remove abilities // remove abilities

View File

@@ -154,7 +154,7 @@ public class AnimateAllEffect extends AnimateEffectBase {
@Override @Override
public void run() { public void run() {
doUnanimate(c, sa, hiddenKeywords, timestamp); doUnanimate(c, timestamp);
game.fireEvent(new GameEventCardStatsChanged(c)); game.fireEvent(new GameEventCardStatsChanged(c));
} }

View File

@@ -103,8 +103,8 @@ public abstract class AnimateEffectBase extends SpellAbilityEffect {
c.addCantHaveKeyword(timestamp, Keyword.setValueOf(sa.getParam("CantHaveKeyword"))); c.addCantHaveKeyword(timestamp, Keyword.setValueOf(sa.getParam("CantHaveKeyword")));
} }
for (final String k : hiddenKeywords) { if (!hiddenKeywords.isEmpty()) {
c.addHiddenExtrinsicKeyword(k); c.addHiddenExtrinsicKeywords(timestamp, 0, hiddenKeywords);
} }
c.addColor(colors, !sa.hasParam("OverwriteColors"), timestamp, false); c.addColor(colors, !sa.hasParam("OverwriteColors"), timestamp, false);
@@ -157,7 +157,7 @@ public abstract class AnimateEffectBase extends SpellAbilityEffect {
@Override @Override
public void run() { public void run() {
doUnanimate(c, sa, hiddenKeywords, timestamp); doUnanimate(c, timestamp);
c.removeChangedName(timestamp); c.removeChangedName(timestamp);
c.updateStateForView(); c.updateStateForView();
@@ -217,8 +217,7 @@ public abstract class AnimateEffectBase extends SpellAbilityEffect {
* @param timestamp * @param timestamp
* a long. * a long.
*/ */
static void doUnanimate(final Card c, SpellAbility sa, static void doUnanimate(final Card c, final long timestamp) {
final List<String> hiddenKeywords, final long timestamp) {
c.removeNewPT(timestamp); c.removeNewPT(timestamp);
@@ -231,9 +230,7 @@ public abstract class AnimateEffectBase extends SpellAbilityEffect {
c.removeCantHaveKeyword(timestamp); c.removeCantHaveKeyword(timestamp);
for (final String k : hiddenKeywords) { c.removeHiddenExtrinsicKeywords(timestamp, 0);
c.removeHiddenExtrinsicKeyword(k);
}
// any other unanimate cleanup // any other unanimate cleanup
if (!c.isCreature()) { if (!c.isCreature()) {

View File

@@ -13,6 +13,7 @@ import forge.game.combat.Combat;
import forge.game.combat.CombatUtil; import forge.game.combat.CombatUtil;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbilityCantAttackBlock;
import forge.util.Localizer; import forge.util.Localizer;
public class CamouflageEffect extends SpellAbilityEffect { public class CamouflageEffect extends SpellAbilityEffect {
@@ -36,7 +37,7 @@ public class CamouflageEffect extends SpellAbilityEffect {
continue; continue;
} }
if (attacker.hasKeyword("CantBeBlockedByAmount GT1") && blockers.size() > 1) { if (StaticAbilityCantAttackBlock.getMinMaxBlocker(attacker, defender).getRight() < blockers.size()) {
// If no more than one creature can block, order the player to choose one to block // If no more than one creature can block, order the player to choose one to block
Card chosen = declarer.getController().chooseCardsForEffect(blockers, sa, Card chosen = declarer.getController().chooseCardsForEffect(blockers, sa,
Localizer.getInstance().getMessage("lblChooseBlockerForAttacker", attacker.toString()), 1, 1, false, null).get(0); Localizer.getInstance().getMessage("lblChooseBlockerForAttacker", attacker.toString()), 1, 1, false, null).get(0);

View File

@@ -128,10 +128,11 @@ public class ControlGainEffect extends SpellAbilityEffect {
} }
final List<String> kws = Lists.newArrayList(); final List<String> kws = Lists.newArrayList();
final List<String> hiddenKws = Lists.newArrayList();
if (null != keywords) { if (null != keywords) {
for (final String kw : keywords) { for (final String kw : keywords) {
if (kw.startsWith("HIDDEN")) { if (kw.startsWith("HIDDEN")) {
tgtC.addHiddenExtrinsicKeyword(kw); hiddenKws.add(kw.substring(7));
} else { } else {
kws.add(kw); kws.add(kw);
} }
@@ -142,6 +143,9 @@ public class ControlGainEffect extends SpellAbilityEffect {
tgtC.addChangedCardKeywords(kws, Lists.newArrayList(), false, false, tStamp, 0); tgtC.addChangedCardKeywords(kws, Lists.newArrayList(), false, false, tStamp, 0);
game.fireEvent(new GameEventCardStatsChanged(tgtC)); game.fireEvent(new GameEventCardStatsChanged(tgtC));
} }
if (hiddenKws.isEmpty()) {
tgtC.addHiddenExtrinsicKeywords(tStamp, 0, hiddenKws);
}
if (remember && !source.isRemembered(tgtC)) { if (remember && !source.isRemembered(tgtC)) {
source.addRemembered(tgtC); source.addRemembered(tgtC);
@@ -191,14 +195,8 @@ public class ControlGainEffect extends SpellAbilityEffect {
@Override @Override
public void run() { public void run() {
if (keywords.size() > 0) { tgtC.removeHiddenExtrinsicKeywords(tStamp, 0);
for (String kw : keywords) { tgtC.removeChangedCardKeywords(tStamp, 0);
if (kw.startsWith("HIDDEN")) {
tgtC.removeHiddenExtrinsicKeyword(kw);
}
}
tgtC.removeChangedCardKeywords(tStamp, 0);
}
} }
}; };
game.getEndOfTurn().addUntil(untilKeywordEOT); game.getEndOfTurn().addUntil(untilKeywordEOT);

View File

@@ -32,6 +32,7 @@ import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode; import forge.game.player.PlayerActionConfirmMode;
import forge.game.player.PlayerController; import forge.game.player.PlayerController;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbilityAdapt;
import forge.game.trigger.Trigger; import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerHandler; import forge.game.trigger.TriggerHandler;
import forge.game.trigger.TriggerType; import forge.game.trigger.TriggerType;
@@ -288,8 +289,7 @@ public class CountersPutEffect extends SpellAbilityEffect {
// Adapt need extra logic // Adapt need extra logic
if (sa.hasParam("Adapt")) { if (sa.hasParam("Adapt")) {
if (!(gameCard.getCounters(CounterEnumType.P1P1) == 0 if (!(gameCard.getCounters(CounterEnumType.P1P1) == 0 || StaticAbilityAdapt.anyWithAdapt(sa, gameCard))) {
|| gameCard.hasKeyword("CARDNAME adapts as though it had no +1/+1 counters"))) {
continue; continue;
} }
} }
@@ -362,8 +362,6 @@ public class CountersPutEffect extends SpellAbilityEffect {
game.getTriggerHandler().runTrigger(TriggerType.BecomeRenowned, AbilityKey.mapFromCard(gameCard), false); game.getTriggerHandler().runTrigger(TriggerType.BecomeRenowned, AbilityKey.mapFromCard(gameCard), false);
} }
if (sa.hasParam("Adapt")) { if (sa.hasParam("Adapt")) {
// need to remove special keyword
gameCard.removeHiddenExtrinsicKeyword("CARDNAME adapts as though it had no +1/+1 counters");
game.getTriggerHandler().runTrigger(TriggerType.Adapt, AbilityKey.mapFromCard(gameCard), false); game.getTriggerHandler().runTrigger(TriggerType.Adapt, AbilityKey.mapFromCard(gameCard), false);
} }
} else { } else {
@@ -422,7 +420,7 @@ public class CountersPutEffect extends SpellAbilityEffect {
List<String> keywords = Arrays.asList(sa.getParam("SharedKeywords").split(" & ")); List<String> keywords = Arrays.asList(sa.getParam("SharedKeywords").split(" & "));
List<ZoneType> zones = ZoneType.listValueOf(sa.getParam("SharedKeywordsZone")); List<ZoneType> zones = ZoneType.listValueOf(sa.getParam("SharedKeywordsZone"));
String[] restrictions = sa.hasParam("SharedRestrictions") ? sa.getParam("SharedRestrictions").split(",") : new String[]{"Card"}; String[] restrictions = sa.hasParam("SharedRestrictions") ? sa.getParam("SharedRestrictions").split(",") : new String[]{"Card"};
keywords = CardFactoryUtil.sharedKeywords(keywords, restrictions, zones, card); keywords = CardFactoryUtil.sharedKeywords(keywords, restrictions, zones, card, sa);
for (String k : keywords) { for (String k : keywords) {
resolvePerType(sa, placer, CounterType.getType(k), counterAmount, table); resolvePerType(sa, placer, CounterType.getType(k), counterAmount, table);
} }

View File

@@ -13,6 +13,7 @@ import forge.card.MagicColor;
import forge.game.Game; import forge.game.Game;
import forge.game.ability.SpellAbilityEffect; import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordInterface; import forge.game.keyword.KeywordInterface;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
@@ -72,17 +73,16 @@ public class DebuffEffect extends SpellAbilityEffect {
final List<String> removedKW = Lists.newArrayList(); final List<String> removedKW = Lists.newArrayList();
if (tgtC.isInPlay() && (!sa.usesTargeting() || tgtC.canBeTargetedBy(sa))) { if (tgtC.isInPlay() && (!sa.usesTargeting() || tgtC.canBeTargetedBy(sa))) {
if (sa.hasParam("AllSuffixKeywords")) { if (sa.hasParam("AllSuffixKeywords")) {
String suffix = sa.getParam("AllSuffixKeywords"); // this only for walk abilities, may to try better
for (final KeywordInterface kw : tgtC.getKeywords()) { if (sa.getParam("AllSuffixKeywords").equals("walk")) {
String keyword = kw.getOriginal(); for (final KeywordInterface kw : tgtC.getKeywords(Keyword.LANDWALK)) {
if (keyword.endsWith(suffix)) { removedKW.add(kw.getOriginal());
kws.add(keyword);
} }
} }
} }
// special for Protection:Card.<color>:Protection from <color>:* // special for Protection:Card.<color>:Protection from <color>:*
for (final KeywordInterface inst : tgtC.getKeywords()) { for (final KeywordInterface inst : tgtC.getUnhiddenKeywords()) {
String keyword = inst.getOriginal(); String keyword = inst.getOriginal();
if (keyword.startsWith("Protection:")) { if (keyword.startsWith("Protection:")) {
for (final String kw : kws) { for (final String kw : kws) {

View File

@@ -31,7 +31,7 @@ public class PumpAllEffect extends SpellAbilityEffect {
for (String kw : keywords) { for (String kw : keywords) {
if (kw.startsWith("HIDDEN")) { if (kw.startsWith("HIDDEN")) {
hiddenkws.add(kw); hiddenkws.add(kw.substring(7));
} else { } else {
kws.add(kw); kws.add(kw);
} }
@@ -57,13 +57,15 @@ public class PumpAllEffect extends SpellAbilityEffect {
redrawPT = true; redrawPT = true;
} }
tgtC.addChangedCardKeywords(kws, null, false, false, timestamp, 0); if (!kws.isEmpty()) {
tgtC.addChangedCardKeywords(kws, null, false, false, timestamp, 0);
}
if (redrawPT) { if (redrawPT) {
tgtC.updatePowerToughnessForView(); tgtC.updatePowerToughnessForView();
} }
for (String kw : hiddenkws) { if (!hiddenkws.isEmpty()) {
tgtC.addHiddenExtrinsicKeyword(kw); tgtC.addHiddenExtrinsicKeywords(timestamp, 0, hiddenkws);
} }
if (sa.hasParam("RememberAllPumped")) { if (sa.hasParam("RememberAllPumped")) {
@@ -79,10 +81,8 @@ public class PumpAllEffect extends SpellAbilityEffect {
public void run() { public void run() {
tgtC.removePTBoost(timestamp, 0); tgtC.removePTBoost(timestamp, 0);
tgtC.removeChangedCardKeywords(timestamp, 0); tgtC.removeChangedCardKeywords(timestamp, 0);
tgtC.removeHiddenExtrinsicKeywords(timestamp, 0);
for (String kw : hiddenkws) {
tgtC.removeHiddenExtrinsicKeyword(kw);
}
tgtC.updatePowerToughnessForView(); tgtC.updatePowerToughnessForView();
game.fireEvent(new GameEventCardStatsChanged(tgtC)); game.fireEvent(new GameEventCardStatsChanged(tgtC));
@@ -156,7 +156,7 @@ public class PumpAllEffect extends SpellAbilityEffect {
String[] restrictions = new String[] {"Card"}; String[] restrictions = new String[] {"Card"};
if (sa.hasParam("SharedRestrictions")) if (sa.hasParam("SharedRestrictions"))
restrictions = sa.getParam("SharedRestrictions").split(","); restrictions = sa.getParam("SharedRestrictions").split(",");
keywords = CardFactoryUtil.sharedKeywords(keywords, restrictions, zones, sa.getHostCard()); keywords = CardFactoryUtil.sharedKeywords(keywords, restrictions, zones, sa.getHostCard(), sa);
} }
applyPumpAll(sa, list, a, d, keywords, affectedZones); applyPumpAll(sa, list, a, d, keywords, affectedZones);

View File

@@ -20,7 +20,6 @@ import forge.game.card.CardCollection;
import forge.game.card.CardFactoryUtil; import forge.game.card.CardFactoryUtil;
import forge.game.card.CardUtil; import forge.game.card.CardUtil;
import forge.game.event.GameEventCardStatsChanged; import forge.game.event.GameEventCardStatsChanged;
import forge.game.keyword.KeywordInterface;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.player.PlayerCollection; import forge.game.player.PlayerCollection;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
@@ -50,11 +49,12 @@ public class PumpEffect extends SpellAbilityEffect {
return; return;
} }
final List<String> kws = Lists.newArrayList(); final List<String> kws = Lists.newArrayList();
final List<String> hiddenKws = Lists.newArrayList();
boolean redrawPT = false; boolean redrawPT = false;
for (String kw : keywords) { for (String kw : keywords) {
if (kw.startsWith("HIDDEN")) { if (kw.startsWith("HIDDEN")) {
gameCard.addHiddenExtrinsicKeyword(kw); hiddenKws.add(kw.substring(7));
redrawPT |= kw.contains("CARDNAME's power and toughness are switched"); redrawPT |= kw.contains("CARDNAME's power and toughness are switched");
} else { } else {
kws.add(kw); kws.add(kw);
@@ -66,7 +66,12 @@ public class PumpEffect extends SpellAbilityEffect {
redrawPT = true; redrawPT = true;
} }
gameCard.addChangedCardKeywords(kws, Lists.newArrayList(), false, false, timestamp, 0); if (!kws.isEmpty()) {
gameCard.addChangedCardKeywords(kws, Lists.newArrayList(), false, false, timestamp, 0);
}
if (!hiddenKws.isEmpty()) {
gameCard.addHiddenExtrinsicKeywords(timestamp, 0, hiddenKws);
}
if (redrawPT) { if (redrawPT) {
gameCard.updatePowerToughnessForView(); gameCard.updatePowerToughnessForView();
} }
@@ -95,12 +100,7 @@ public class PumpEffect extends SpellAbilityEffect {
updateText |= gameCard.removeCanBlockAdditional(timestamp); updateText |= gameCard.removeCanBlockAdditional(timestamp);
if (keywords.size() > 0) { if (keywords.size() > 0) {
gameCard.removeHiddenExtrinsicKeywords(timestamp, 0);
for (String kw : keywords) {
if (kw.startsWith("HIDDEN")) {
gameCard.removeHiddenExtrinsicKeyword(kw);
}
}
gameCard.removeChangedCardKeywords(timestamp, 0); gameCard.removeChangedCardKeywords(timestamp, 0);
} }
gameCard.updatePowerToughnessForView(); gameCard.updatePowerToughnessForView();
@@ -244,7 +244,7 @@ public class PumpEffect extends SpellAbilityEffect {
if (sa.hasParam("SharedKeywordsZone")) { if (sa.hasParam("SharedKeywordsZone")) {
List<ZoneType> zones = ZoneType.listValueOf(sa.getParam("SharedKeywordsZone")); List<ZoneType> zones = ZoneType.listValueOf(sa.getParam("SharedKeywordsZone"));
String[] restrictions = sa.hasParam("SharedRestrictions") ? sa.getParam("SharedRestrictions").split(",") : new String[]{"Card"}; String[] restrictions = sa.hasParam("SharedRestrictions") ? sa.getParam("SharedRestrictions").split(",") : new String[]{"Card"};
keywords = CardFactoryUtil.sharedKeywords(keywords, restrictions, zones, sa.getHostCard()); keywords = CardFactoryUtil.sharedKeywords(keywords, restrictions, zones, sa.getHostCard(), sa);
} }
List<GameEntity> tgts = Lists.newArrayList(); List<GameEntity> tgts = Lists.newArrayList();
@@ -290,9 +290,10 @@ public class PumpEffect extends SpellAbilityEffect {
List<String> choice = Lists.newArrayList(); List<String> choice = Lists.newArrayList();
List<String> total = Lists.newArrayList(keywords); List<String> total = Lists.newArrayList(keywords);
if (sa.hasParam("NoRepetition")) { if (sa.hasParam("NoRepetition")) {
for (KeywordInterface inst : tgtCards.get(0).getKeywords()) { for (String kw : keywords) {
final String kws = inst.getOriginal(); if (tgtCards.get(0).hasKeyword(kw)) {
total.remove(kws); total.remove(kw);
}
} }
} }
final int min = Math.min(total.size(), numkw); final int min = Math.min(total.size(), numkw);

View File

@@ -55,6 +55,7 @@ import forge.game.replacement.ReplacementResult;
import forge.game.replacement.ReplacementType; import forge.game.replacement.ReplacementType;
import forge.game.spellability.*; import forge.game.spellability.*;
import forge.game.staticability.StaticAbility; import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityCantAttackBlock;
import forge.game.trigger.Trigger; import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType; import forge.game.trigger.TriggerType;
import forge.game.zone.Zone; import forge.game.zone.Zone;
@@ -99,7 +100,8 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
private CardDamageHistory damageHistory = new CardDamageHistory(); private CardDamageHistory damageHistory = new CardDamageHistory();
// Hidden keywords won't be displayed on the card // Hidden keywords won't be displayed on the card
private final KeywordCollection hiddenExtrinsicKeyword = new KeywordCollection(); // x=timestamp y=StaticAbility id
private final Table<Long, Long, List<String>> hiddenExtrinsicKeywords = TreeBasedTable.create();
// cards attached or otherwise linked to this card // cards attached or otherwise linked to this card
private CardCollection hauntedBy, devouredCards, exploitedCards, delvedCards, convokedCards, imprintedCards, encodedCards; private CardCollection hauntedBy, devouredCards, exploitedCards, delvedCards, convokedCards, imprintedCards, encodedCards;
@@ -1867,7 +1869,37 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
// get the text that does not belong to a cards abilities (and is not really // get the text that does not belong to a cards abilities (and is not really
// there rules-wise) // there rules-wise)
public final String getNonAbilityText() { public final String getNonAbilityText() {
return keywordsToText(getHiddenExtrinsicKeywords()); final StringBuilder sb = new StringBuilder();
final StringBuilder sbLong = new StringBuilder();
for (String keyword : getHiddenExtrinsicKeywords()) {
if (keyword.startsWith("CantBeCounteredBy")) {
final String[] p = keyword.split(":");
sbLong.append(p[2]).append("\r\n");
} else if (keyword.equals("Unblockable")) {
sbLong.append(getName()).append(" can't be blocked.\r\n");
} else if (keyword.equals("AllNonLegendaryCreatureNames")) {
sbLong.append(getName()).append(" has all names of nonlegendary creature cards.\r\n");
} else if (keyword.startsWith("IfReach")) {
String[] k = keyword.split(":");
sbLong.append(getName()).append(" can block ")
.append(CardType.getPluralType(k[1]))
.append(" as though it had reach.\r\n");
} else {
sbLong.append(keyword).append("\r\n");
}
}
if (sb.length() > 0) {
sb.append("\r\n");
if (sbLong.length() > 0) {
sb.append("\r\n");
}
}
if (sbLong.length() > 0) {
sbLong.append("\r\n");
}
sb.append(sbLong);
return CardTranslation.translateMultipleDescriptionText(sb.toString(), getName());
} }
// convert a keyword list to the String that should be displayed in game // convert a keyword list to the String that should be displayed in game
@@ -2120,9 +2152,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
|| keyword.startsWith("Encore") || keyword.startsWith("Mutate") || keyword.startsWith("Dungeon") || keyword.startsWith("Encore") || keyword.startsWith("Mutate") || keyword.startsWith("Dungeon")
|| keyword.startsWith("Class") || keyword.startsWith("Saga")) { || keyword.startsWith("Class") || keyword.startsWith("Saga")) {
// keyword parsing takes care of adding a proper description // keyword parsing takes care of adding a proper description
} else if (keyword.startsWith("CantBeBlockedByAmount")) {
sbLong.append(getName()).append(" can't be blocked ");
sbLong.append(getTextForKwCantBeBlockedByAmount(keyword));
} else if (keyword.equals("Unblockable")) { } else if (keyword.equals("Unblockable")) {
sbLong.append(getName()).append(" can't be blocked.\r\n"); sbLong.append(getName()).append(" can't be blocked.\r\n");
} else if (keyword.equals("AllNonLegendaryCreatureNames")) { } else if (keyword.equals("AllNonLegendaryCreatureNames")) {
@@ -2173,14 +2202,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
return CardTranslation.translateMultipleDescriptionText(sb.toString(), getName()); return CardTranslation.translateMultipleDescriptionText(sb.toString(), getName());
} }
private static String getTextForKwCantBeBlockedByAmount(final String keyword) {
final String restriction = keyword.split(" ", 2)[1];
final boolean isLT = "LT".equals(restriction.substring(0,2));
final String byClause = isLT ? "except by " : "by more than ";
final int cnt = Integer.parseInt(restriction.substring(2));
return byClause + Lang.nounWithNumeral(cnt, isLT ? "or more creature" : "creature");
}
// get the text of the abilities of a card // get the text of the abilities of a card
public String getAbilityText() { public String getAbilityText() {
return getAbilityText(currentState); return getAbilityText(currentState);
@@ -2367,15 +2388,27 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
continue; continue;
} }
for (final StaticAbility stAb : ca.getStaticAbilities()) { for (final StaticAbility stAb : ca.getStaticAbilities()) {
if (stAb.isSecondary() || if (stAb.isSuppressed() || !stAb.checkConditions()) {
!stAb.getParam("Mode").equals("CantBlockBy") ||
stAb.isSuppressed() || !stAb.checkConditions() ||
!stAb.hasParam("ValidAttacker") ||
(stAb.hasParam("ValidBlocker") && stAb.getParam("ValidBlocker").equals("Creature.Self"))) {
continue; continue;
} }
final Card host = stAb.getHostCard();
if (isValid(stAb.getParam("ValidAttacker").split(","), host.getController(), host, stAb)) { boolean found = false;
if (stAb.getParam("Mode").equals("CantBlockBy")) {
if (!stAb.hasParam("ValidAttacker") || (stAb.hasParam("ValidBlocker") && stAb.getParam("ValidBlocker").equals("Creature.Self"))) {
continue;
}
if (stAb.matchesValidParam("ValidAttacker", this)) {
found = true;
}
} else if (stAb.getParam("Mode").equals(StaticAbilityCantAttackBlock.MinMaxBlockerMode)) {
if (stAb.matchesValidParam("ValidCard", this)) {
found = true;
}
}
if (found) {
final Card host = stAb.getHostCard();
String currentName = host.getName(); String currentName = host.getName();
String desc1 = TextUtil.fastReplace(stAb.toString(), "CARDNAME", currentName); String desc1 = TextUtil.fastReplace(stAb.toString(), "CARDNAME", currentName);
String desc = TextUtil.fastReplace(desc1,"NICKNAME", currentName.split(",")[0]); String desc = TextUtil.fastReplace(desc1,"NICKNAME", currentName.split(",")[0]);
@@ -4119,7 +4152,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
// of lists. Optimizes common operations such as hasKeyword(). // of lists. Optimizes common operations such as hasKeyword().
public final void visitKeywords(CardState state, Visitor<KeywordInterface> visitor) { public final void visitKeywords(CardState state, Visitor<KeywordInterface> visitor) {
visitUnhiddenKeywords(state, visitor); visitUnhiddenKeywords(state, visitor);
visitHiddenExtrinsicKeywords(visitor);
} }
@Override @Override
@@ -4140,8 +4172,10 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
} }
// shortcut for hidden keywords // shortcut for hidden keywords
if (this.hiddenExtrinsicKeyword.contains(keyword)) { for (List<String> kw : this.hiddenExtrinsicKeywords.values()) {
return true; if (kw.contains(keyword)) {
return true;
}
} }
HasKeywordVisitor visitor = new HasKeywordVisitor(keyword, false); HasKeywordVisitor visitor = new HasKeywordVisitor(keyword, false);
@@ -4418,41 +4452,33 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
} }
// Hidden Keywords will be returned without the indicator HIDDEN // Hidden Keywords will be returned without the indicator HIDDEN
public final List<KeywordInterface> getHiddenExtrinsicKeywords() { public final Iterable<String> getHiddenExtrinsicKeywords() {
ListKeywordVisitor visitor = new ListKeywordVisitor(); return Iterables.concat(this.hiddenExtrinsicKeywords.values());
visitHiddenExtrinsicKeywords(visitor);
return visitor.getKeywords();
}
private void visitHiddenExtrinsicKeywords(Visitor<KeywordInterface> visitor) {
for (KeywordInterface inst : hiddenExtrinsicKeyword.getValues()) {
if (!visitor.visit(inst)) {
return;
}
}
} }
public final void addHiddenExtrinsicKeyword(String s) { public final void addHiddenExtrinsicKeywords(long timestamp, long staticId, Iterable<String> keywords) {
if (s.startsWith("HIDDEN")) { // TODO if some keywords aren't removed anymore, then no need for extra Array List
s = s.substring(7); hiddenExtrinsicKeywords.put(timestamp, staticId, Lists.newArrayList(keywords));
}
if (hiddenExtrinsicKeyword.add(s) != null) { view.updateNonAbilityText(this);
view.updateNonAbilityText(this); updateKeywords();
updateKeywords();
}
} }
public final void addHiddenExtrinsicKeyword(KeywordInterface k) { public final void removeHiddenExtrinsicKeywords(long timestamp, long staticId) {
if (hiddenExtrinsicKeyword.insert(k)) { if (hiddenExtrinsicKeywords.remove(timestamp, staticId) != null) {
view.updateNonAbilityText(this); view.updateNonAbilityText(this);
updateKeywords(); updateKeywords();
} }
} }
public final void removeHiddenExtrinsicKeyword(String s) { public final void removeHiddenExtrinsicKeyword(String s) {
if (s.startsWith("HIDDEN")) { boolean updated = false;
s = s.substring(7); for (List<String> list : hiddenExtrinsicKeywords.values()) {
if (list.remove(s)) {
updated = true;
}
} }
if (hiddenExtrinsicKeyword.remove(s)) { if (updated) {
view.updateNonAbilityText(this); view.updateNonAbilityText(this);
updateKeywords(); updateKeywords();
} }
@@ -4710,6 +4736,12 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
return hasStartOfKeyword(keyword, currentState); return hasStartOfKeyword(keyword, currentState);
} }
public final boolean hasStartOfKeyword(String keyword, CardState state) { public final boolean hasStartOfKeyword(String keyword, CardState state) {
for (String s : this.getHiddenExtrinsicKeywords()) {
if (s.startsWith(keyword)) {
return true;
}
}
HasKeywordVisitor visitor = new HasKeywordVisitor(keyword, true); HasKeywordVisitor visitor = new HasKeywordVisitor(keyword, true);
visitKeywords(state, visitor); visitKeywords(state, visitor);
return visitor.getResult(); return visitor.getResult();
@@ -4741,9 +4773,10 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
return getAmountOfKeyword(k, currentState); return getAmountOfKeyword(k, currentState);
} }
public final int getAmountOfKeyword(final String k, CardState state) { public final int getAmountOfKeyword(final String k, CardState state) {
int count = Iterables.frequency(this.getHiddenExtrinsicKeywords(), k);
CountKeywordVisitor visitor = new CountKeywordVisitor(k); CountKeywordVisitor visitor = new CountKeywordVisitor(k);
visitKeywords(state, visitor); visitKeywords(state, visitor);
return visitor.getCount(); return count + visitor.getCount();
} }
public final int getAmountOfKeyword(final Keyword k) { public final int getAmountOfKeyword(final Keyword k) {
@@ -5619,11 +5652,8 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
final boolean colorlessDamage = damageSource && source.hasKeyword("Colorless Damage Source"); final boolean colorlessDamage = damageSource && source.hasKeyword("Colorless Damage Source");
for (final KeywordInterface inst : getKeywords()) { for (final KeywordInterface inst : getKeywords(Keyword.PROTECTION)) {
String kw = inst.getOriginal(); String kw = inst.getOriginal();
if (!kw.startsWith("Protection")) {
continue;
}
if (kw.equals("Protection from white")) { if (kw.equals("Protection from white")) {
if (source.isWhite() && !colorlessDamage) { if (source.isWhite() && !colorlessDamage) {
return true; return true;
@@ -5698,11 +5728,8 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
public String getProtectionKey() { public String getProtectionKey() {
String protectKey = ""; String protectKey = "";
boolean pR = false; boolean pG = false; boolean pB = false; boolean pU = false; boolean pW = false; boolean pR = false; boolean pG = false; boolean pB = false; boolean pU = false; boolean pW = false;
for (final KeywordInterface inst : getKeywords()) { for (final KeywordInterface inst : getKeywords(Keyword.PROTECTION)) {
String kw = inst.getOriginal(); String kw = inst.getOriginal();
if (!kw.startsWith("Protection")) {
continue;
}
if (kw.contains("Protection from red")) { if (kw.contains("Protection from red")) {
if (!pR) { if (!pR) {
pR = true; pR = true;
@@ -5755,11 +5782,8 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
public String getHexproofKey() { public String getHexproofKey() {
String hexproofKey = ""; String hexproofKey = "";
boolean hR = false; boolean hG = false; boolean hB = false; boolean hU = false; boolean hW = false; boolean hR = false; boolean hG = false; boolean hB = false; boolean hU = false; boolean hW = false;
for (final KeywordInterface inst : getKeywords()) { for (final KeywordInterface inst : getKeywords(Keyword.HEXPROOF)) {
String kw = inst.getOriginal(); String kw = inst.getOriginal();
if (!kw.startsWith("Hexproof")) {
continue;
}
if (kw.equals("Hexproof")) { if (kw.equals("Hexproof")) {
hexproofKey += "generic:"; hexproofKey += "generic:";
} }
@@ -5859,6 +5883,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
if (sa.isSpell()) { if (sa.isSpell()) {
// TODO replace with Static Ability
for(KeywordInterface inst : source.getKeywords()) { for(KeywordInterface inst : source.getKeywords()) {
String kw = inst.getOriginal(); String kw = inst.getOriginal();
if(!kw.startsWith("SpellCantTarget")) { if(!kw.startsWith("SpellCantTarget")) {
@@ -6506,23 +6531,16 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
private static final class CountKeywordVisitor extends Visitor<KeywordInterface> { private static final class CountKeywordVisitor extends Visitor<KeywordInterface> {
private String keyword; private String keyword;
private int count; private int count;
private boolean startOf;
private CountKeywordVisitor(String keyword) { private CountKeywordVisitor(String keyword) {
this.keyword = keyword; this.keyword = keyword;
this.count = 0; this.count = 0;
this.startOf = false;
}
private CountKeywordVisitor(String keyword, boolean startOf) {
this(keyword);
this.startOf = startOf;
} }
@Override @Override
public boolean visit(KeywordInterface inst) { public boolean visit(KeywordInterface inst) {
final String kw = inst.getOriginal(); final String kw = inst.getOriginal();
if ((startOf && kw.startsWith(keyword)) || kw.equals(keyword)) { if (kw.equals(keyword)) {
count++; count++;
} }
return true; return true;
@@ -6551,7 +6569,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
} }
return result.isFalse(); return result.isFalse();
} }
public boolean getResult() { public boolean getResult() {
return result.isTrue(); return result.isTrue();
} }

View File

@@ -42,6 +42,7 @@ import forge.card.ICardFace;
import forge.card.MagicColor; import forge.card.MagicColor;
import forge.card.mana.ManaCost; import forge.card.mana.ManaCost;
import forge.card.mana.ManaCostParser; import forge.card.mana.ManaCostParser;
import forge.game.CardTraitBase;
import forge.game.Game; import forge.game.Game;
import forge.game.GameEntityCounterTable; import forge.game.GameEntityCounterTable;
import forge.game.GameLogEntryType; import forge.game.GameLogEntryType;
@@ -264,8 +265,7 @@ public class CardFactoryUtil {
return false; return false;
} }
for (KeywordInterface k : c.getKeywords()) { for (String o : c.getHiddenExtrinsicKeywords()) {
final String o = k.getOriginal();
if (o.startsWith("CantBeCounteredBy")) { if (o.startsWith("CantBeCounteredBy")) {
final String[] m = o.split(":"); final String[] m = o.split(":");
if (sa.isValid(m[1].split(","), c.getController(), c, null)) { if (sa.isValid(m[1].split(","), c.getController(), c, null)) {
@@ -510,7 +510,7 @@ public class CardFactoryUtil {
* @return a List<String>. * @return a List<String>.
*/ */
public static List<String> sharedKeywords(final Iterable<String> kw, final String[] restrictions, public static List<String> sharedKeywords(final Iterable<String> kw, final String[] restrictions,
final Iterable<ZoneType> zones, final Card host) { final Iterable<ZoneType> zones, final Card host, CardTraitBase ctb) {
final List<String> filteredkw = Lists.newArrayList(); final List<String> filteredkw = Lists.newArrayList();
final Player p = host.getController(); final Player p = host.getController();
CardCollectionView cardlist = p.getGame().getCardsIn(zones); CardCollectionView cardlist = p.getGame().getCardsIn(zones);
@@ -521,7 +521,7 @@ public class CardFactoryUtil {
final Set<String> tramplekw = Sets.newHashSet(); final Set<String> tramplekw = Sets.newHashSet();
final Set<String> allkw = Sets.newHashSet(); final Set<String> allkw = Sets.newHashSet();
for (Card c : CardLists.getValidCards(cardlist, restrictions, p, host, null)) { for (Card c : CardLists.getValidCards(cardlist, restrictions, p, host, ctb)) {
for (KeywordInterface inst : c.getKeywords()) { for (KeywordInterface inst : c.getKeywords()) {
final String k = inst.getOriginal(); final String k = inst.getOriginal();
if (k.endsWith("walk")) { if (k.endsWith("walk")) {

View File

@@ -21,6 +21,7 @@ import java.util.Comparator;
import com.google.common.base.Function; import com.google.common.base.Function;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import forge.card.CardStateName; import forge.card.CardStateName;
import forge.game.CardTraitBase; import forge.game.CardTraitBase;
@@ -31,6 +32,7 @@ import forge.game.player.Player;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.zone.Zone; import forge.game.zone.Zone;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.PredicateString;
import forge.util.collect.FCollectionView; import forge.util.collect.FCollectionView;
@@ -109,6 +111,10 @@ public final class CardPredicates {
return new Predicate<Card>() { return new Predicate<Card>() {
@Override @Override
public boolean apply(final Card c) { public boolean apply(final Card c) {
if (Iterables.any(c.getHiddenExtrinsicKeywords(), PredicateString.contains(keyword))) {
return true;
}
for (KeywordInterface k : c.getKeywords()) { for (KeywordInterface k : c.getKeywords()) {
if (k.getOriginal().contains(keyword)) { if (k.getOriginal().contains(keyword)) {
return true; return true;

View File

@@ -52,18 +52,27 @@ public class AttackRequirement {
nAttackAnything += attacker.getGoaded().size(); nAttackAnything += attacker.getGoaded().size();
} }
// remove it when all of them are HIDDEN or static
for (final KeywordInterface inst : attacker.getKeywords()) { for (final KeywordInterface inst : attacker.getKeywords()) {
final String keyword = inst.getOriginal(); final String keyword = inst.getOriginal();
if (keyword.startsWith("CARDNAME attacks specific player each combat if able")) { if (keyword.startsWith("CARDNAME attacks specific player each combat if able")) {
final String defined = keyword.split(":")[1]; final String defined = keyword.split(":")[1];
final GameEntity mustAttack2 = AbilityUtils.getDefinedPlayers(attacker, defined, null).get(0); final GameEntity mustAttack2 = AbilityUtils.getDefinedPlayers(attacker, defined, null).get(0);
defenderSpecific.add(mustAttack2); defenderSpecific.add(mustAttack2);
} else if (keyword.equals("CARDNAME attacks each combat if able.") || } else if (keyword.equals("CARDNAME attacks each combat if able.")) {
(keyword.equals("CARDNAME attacks each turn if able.")
&& !attacker.getDamageHistory().getCreatureAttackedThisTurn())) {
nAttackAnything++; nAttackAnything++;
} }
} }
for (final String keyword : attacker.getHiddenExtrinsicKeywords()) {
if (keyword.startsWith("CARDNAME attacks specific player each combat if able")) {
final String defined = keyword.split(":")[1];
final GameEntity mustAttack2 = AbilityUtils.getDefinedPlayers(attacker, defined, null).get(0);
defenderSpecific.add(mustAttack2);
} else if (keyword.equals("CARDNAME attacks each combat if able.")) {
nAttackAnything++;
}
}
final GameEntity mustAttack3 = attacker.getMustAttackEntity(); final GameEntity mustAttack3 = attacker.getMustAttackEntity();
if (mustAttack3 != null) { if (mustAttack3 != null) {
defenderSpecific.add(mustAttack3); defenderSpecific.add(mustAttack3);
@@ -149,7 +158,7 @@ public class AttackRequirement {
int violations = 0; int violations = 0;
// first. check to see if "must attack X or Y with at least one creature" requirements are satisfied // first. check to see if "must attack X or Y with at least one creature" requirements are satisfied
List<GameEntity> toRemoveFromDefSpecific = Lists.newArrayList(); //List<GameEntity> toRemoveFromDefSpecific = Lists.newArrayList();
if (!defenderOrPWSpecific.isEmpty()) { if (!defenderOrPWSpecific.isEmpty()) {
for (GameEntity def : defenderOrPWSpecific.keySet()) { for (GameEntity def : defenderOrPWSpecific.keySet()) {
if (defenderSpecificAlternatives.containsKey(def)) { if (defenderSpecificAlternatives.containsKey(def)) {

View File

@@ -47,9 +47,9 @@ import forge.game.player.Player;
import forge.game.player.PlayerController.ManaPaymentPurpose; import forge.game.player.PlayerController.ManaPaymentPurpose;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbility; import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityCantAttackBlock;
import forge.game.trigger.TriggerType; import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.Expressions;
import forge.util.TextUtil; import forge.util.TextUtil;
import forge.util.collect.FCollection; import forge.util.collect.FCollection;
import forge.util.collect.FCollectionView; import forge.util.collect.FCollectionView;
@@ -251,12 +251,9 @@ public class CombatUtil {
} }
// Keywords // Keywords
for (final KeywordInterface keyword : attacker.getKeywords()) { // replace with Static Ability if able
switch (keyword.getOriginal()) { if (attacker.hasKeyword("CARDNAME can't attack.") || attacker.hasKeyword("CARDNAME can't attack or block.")) {
case "CARDNAME can't attack.": return false;
case "CARDNAME can't attack or block.":
return false;
}
} }
// CantAttack static abilities // CantAttack static abilities
@@ -505,7 +502,7 @@ public class CombatUtil {
} }
if ( combat != null ) { if ( combat != null ) {
if (attacker.hasStartOfKeyword("CantBeBlockedByAmount GT") && !combat.getBlockers(attacker).isEmpty()) { if (StaticAbilityCantAttackBlock.getMinMaxBlocker(attacker, defendingPlayer).getRight() == combat.getBlockers(attacker).size()) {
return false; return false;
} }
@@ -587,7 +584,7 @@ public class CombatUtil {
} }
} }
for (final KeywordInterface inst : attacker.getKeywords()) { for (final KeywordInterface inst : attacker.getKeywords(Keyword.LANDWALK)) {
String keyword = inst.getOriginal(); String keyword = inst.getOriginal();
if (keyword.equals("Legendary landwalk")) { if (keyword.equals("Legendary landwalk")) {
walkTypes.add("Land.Legendary"); walkTypes.add("Land.Legendary");
@@ -762,11 +759,11 @@ public class CombatUtil {
} }
// "CARDNAME blocks each turn/combat if able." // "CARDNAME blocks each turn/combat if able."
if (!blockers.contains(blocker) && (blocker.hasKeyword("CARDNAME blocks each turn if able.") || blocker.hasKeyword("CARDNAME blocks each combat if able."))) { if (!blockers.contains(blocker) && (blocker.hasKeyword("CARDNAME blocks each combat if able."))) {
for (final Card attacker : attackers) { for (final Card attacker : attackers) {
if (canBlock(attacker, blocker, combat)) { if (canBlock(attacker, blocker, combat)) {
boolean must = true; boolean must = true;
if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT") || attacker.hasKeyword(Keyword.MENACE)) { if (StaticAbilityCantAttackBlock.getMinMaxBlocker(attacker, defending).getLeft() > 1) {
final List<Card> possibleBlockers = Lists.newArrayList(defendersArmy); final List<Card> possibleBlockers = Lists.newArrayList(defendersArmy);
possibleBlockers.remove(blocker); possibleBlockers.remove(blocker);
if (!canBeBlocked(attacker, possibleBlockers, combat)) { if (!canBeBlocked(attacker, possibleBlockers, combat)) {
@@ -774,8 +771,7 @@ public class CombatUtil {
} }
} }
if (must) { if (must) {
String unit = blocker.hasKeyword("CARDNAME blocks each combat if able.") ? "combat," : "turn,"; return TextUtil.concatWithSpace(blocker.toString(),"must block each combat but was not assigned to block any attacker now.");
return TextUtil.concatWithSpace(blocker.toString(),"must block each", unit, "but was not assigned to block any attacker now.");
} }
} }
} }
@@ -848,6 +844,7 @@ public class CombatUtil {
&& combat.getBlockers(attacker).size() < 2)) { && combat.getBlockers(attacker).size() < 2)) {
attackersWithLure.add(attacker); attackersWithLure.add(attacker);
} else { } else {
// TODO replace with Hidden Keyword or Static Ability
for (KeywordInterface inst : attacker.getKeywords()) { for (KeywordInterface inst : attacker.getKeywords()) {
String keyword = inst.getOriginal(); String keyword = inst.getOriginal();
// MustBeBlockedBy <valid> // MustBeBlockedBy <valid>
@@ -875,8 +872,11 @@ public class CombatUtil {
for (final Card attacker : attackersWithLure) { for (final Card attacker : attackersWithLure) {
if (canBeBlocked(attacker, combat, defender) && canBlock(attacker, blocker)) { if (canBeBlocked(attacker, combat, defender) && canBlock(attacker, blocker)) {
boolean canBe = true; boolean canBe = true;
if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT") || attacker.hasKeyword(Keyword.MENACE)) {
final List<Card> blockers = combat.getDefenderPlayerByAttacker(attacker).getCreaturesInPlay(); Player defendingPlayer = combat.getDefenderPlayerByAttacker(attacker);
if (StaticAbilityCantAttackBlock.getMinMaxBlocker(attacker, defendingPlayer).getLeft() > 1) {
final List<Card> blockers = defendingPlayer.getCreaturesInPlay();
blockers.remove(blocker); blockers.remove(blocker);
if (!canBeBlocked(attacker, blockers, combat)) { if (!canBeBlocked(attacker, blockers, combat)) {
canBe = false; canBe = false;
@@ -893,8 +893,9 @@ public class CombatUtil {
if (canBeBlocked(attacker, combat, defender) && canBlock(attacker, blocker) if (canBeBlocked(attacker, combat, defender) && canBlock(attacker, blocker)
&& combat.isAttacking(attacker)) { && combat.isAttacking(attacker)) {
boolean canBe = true; boolean canBe = true;
if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT") || attacker.hasKeyword(Keyword.MENACE)) { Player defendingPlayer = combat.getDefenderPlayerByAttacker(attacker);
final List<Card> blockers = freeBlockers != null ? new CardCollection(freeBlockers) : combat.getDefenderPlayerByAttacker(attacker).getCreaturesInPlay(); if (StaticAbilityCantAttackBlock.getMinMaxBlocker(attacker, defendingPlayer).getLeft() > 1) {
final List<Card> blockers = freeBlockers != null ? new CardCollection(freeBlockers) : defendingPlayer.getCreaturesInPlay();
blockers.remove(blocker); blockers.remove(blocker);
if (!canBeBlocked(attacker, blockers, combat)) { if (!canBeBlocked(attacker, blockers, combat)) {
canBe = false; canBe = false;
@@ -968,6 +969,7 @@ public class CombatUtil {
return false; return false;
} }
// TODO remove with HiddenKeyword or Static Ability
boolean mustBeBlockedBy = false; boolean mustBeBlockedBy = false;
for (KeywordInterface inst : attacker.getKeywords()) { for (KeywordInterface inst : attacker.getKeywords()) {
String keyword = inst.getOriginal(); String keyword = inst.getOriginal();
@@ -1085,60 +1087,16 @@ public class CombatUtil {
if (amount == 0) if (amount == 0)
return false; // no block return false; // no block
List<String> restrictions = Lists.newArrayList(); Pair<Integer, Integer> minMaxBlock = StaticAbilityCantAttackBlock.getMinMaxBlocker(attacker, defender);
for (KeywordInterface inst : attacker.getKeywords()) {
String kw = inst.getOriginal(); if (minMaxBlock.getLeft() > amount || minMaxBlock.getRight() < amount) {
if (kw.startsWith("CantBeBlockedByAmount")) { return false;
restrictions.add(TextUtil.split(kw, ' ', 2)[1]);
}
if (kw.equals("Menace")) {
restrictions.add("LT2");
}
}
for (String res : restrictions) {
int operand = Integer.parseInt(res.substring(2));
String operator = res.substring(0,2);
if (Expressions.compare(amount, operator, operand) )
return false;
}
if (defender != null && attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) {
return amount >= defender.getCreaturesInPlay().size();
} }
return true; return true;
} }
public static int getMinNumBlockersForAttacker(Card attacker, Player defender) { public static int getMinNumBlockersForAttacker(Card attacker, Player defender) {
List<String> restrictions = Lists.newArrayList(); return StaticAbilityCantAttackBlock.getMinMaxBlocker(attacker, defender).getLeft();
for (KeywordInterface inst : attacker.getKeywords()) {
String kw = inst.getOriginal();
if (kw.startsWith("CantBeBlockedByAmount")) {
restrictions.add(TextUtil.split(kw, ' ', 2)[1]);
}
if (kw.equals("Menace")) {
restrictions.add("LT2");
}
}
int minBlockers = 1;
for (String res : restrictions) {
int operand = Integer.parseInt(res.substring(2));
String operator = res.substring(0, 2);
if (operator.equals("LT") || operator.equals("GE")) {
if (minBlockers < operand) {
minBlockers = operand;
}
} else if (operator.equals("LE") || operator.equals("GT") || operator.equals("EQ")) {
if (minBlockers < operand + 1) {
minBlockers = operand + 1;
}
}
}
if (attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) {
if (defender != null) {
minBlockers = defender.getCreaturesInPlay().size();
}
}
return minBlockers;
} }
} // end class CombatUtil } // end class CombatUtil

View File

@@ -284,12 +284,10 @@ public class CostAdjustment {
private static void adjustCostByOffering(final ManaCostBeingPaid cost, final SpellAbility sa) { private static void adjustCostByOffering(final ManaCostBeingPaid cost, final SpellAbility sa) {
String offeringType = ""; String offeringType = "";
for (KeywordInterface inst : sa.getHostCard().getKeywords()) { for (KeywordInterface inst : sa.getHostCard().getKeywords(Keyword.OFFERING)) {
final String kw = inst.getOriginal(); final String kw = inst.getOriginal();
if (kw.endsWith(" offering")) { offeringType = kw.split(" ")[0];
offeringType = kw.split(" ")[0]; break;
break;
}
} }
Card toSac = null; Card toSac = null;

View File

@@ -12,7 +12,6 @@ import forge.game.card.Card;
public class KeywordCollection implements Iterable<KeywordInterface> { public class KeywordCollection implements Iterable<KeywordInterface> {
private boolean hidden = false;
private transient KeywordCollectionView view; private transient KeywordCollectionView view;
// don't use enumKeys it causes a slow down // don't use enumKeys it causes a slow down
@@ -21,11 +20,6 @@ public class KeywordCollection implements Iterable<KeywordInterface> {
public KeywordCollection() { public KeywordCollection() {
super(); super();
this.hidden = false;
}
public KeywordCollection(boolean hidden) {
super();
this.hidden = hidden;
} }
public boolean contains(Keyword keyword) { public boolean contains(Keyword keyword) {
@@ -50,7 +44,6 @@ public class KeywordCollection implements Iterable<KeywordInterface> {
public KeywordInterface add(String k) { public KeywordInterface add(String k) {
KeywordInterface inst = Keyword.getInstance(k); KeywordInterface inst = Keyword.getInstance(k);
inst.setHidden(hidden);
if (insert(inst)) { if (insert(inst)) {
return inst; return inst;
} }

View File

@@ -23,8 +23,6 @@ public abstract class KeywordInstance<T extends KeywordInstance<?>> implements K
private Keyword keyword; private Keyword keyword;
private String original; private String original;
private boolean hidden;
private List<Trigger> triggers = Lists.newArrayList(); private List<Trigger> triggers = Lists.newArrayList();
private List<ReplacementEffect> replacements = Lists.newArrayList(); private List<ReplacementEffect> replacements = Lists.newArrayList();
private List<SpellAbility> abilities = Lists.newArrayList(); private List<SpellAbility> abilities = Lists.newArrayList();
@@ -205,21 +203,6 @@ public abstract class KeywordInstance<T extends KeywordInstance<?>> implements K
staticAbilities.add(st); staticAbilities.add(st);
} }
/* (non-Javadoc)
* @see forge.game.keyword.KeywordInterface#getHidden()
*/
@Override
public boolean getHidden() {
return hidden;
}
/* (non-Javadoc)
* @see forge.game.keyword.KeywordInterface#setHidden(boolean)
*/
@Override
public void setHidden(boolean val) {
hidden = val;
}
/* /*
* (non-Javadoc) * (non-Javadoc)
* @see forge.game.keyword.KeywordInterface#getTriggers() * @see forge.game.keyword.KeywordInterface#getTriggers()

View File

@@ -19,9 +19,6 @@ public interface KeywordInterface extends Cloneable {
int getAmount(); int getAmount();
boolean getHidden();
void setHidden(boolean val);
void createTraits(final Card host, final boolean intrinsic); void createTraits(final Card host, final boolean intrinsic);
void createTraits(final Card host, final boolean intrinsic, final boolean clear); void createTraits(final Card host, final boolean intrinsic, final boolean clear);

View File

@@ -205,11 +205,12 @@ public class Untap extends Phase {
} }
// Remove temporary keywords // Remove temporary keywords
// TODO Replace with Static Abilities
for (final Card c : player.getCardsIn(ZoneType.Battlefield)) { for (final Card c : player.getCardsIn(ZoneType.Battlefield)) {
c.removeHiddenExtrinsicKeyword("This card doesn't untap during your next untap step."); c.removeHiddenExtrinsicKeyword("This card doesn't untap during your next untap step.");
if (c.hasKeyword("This card doesn't untap during your next two untap steps.")) { if (c.hasKeyword("This card doesn't untap during your next two untap steps.")) {
c.removeHiddenExtrinsicKeyword("This card doesn't untap during your next two untap steps."); c.removeHiddenExtrinsicKeyword("This card doesn't untap during your next two untap steps.");
c.addHiddenExtrinsicKeyword("This card doesn't untap during your next untap step."); c.addHiddenExtrinsicKeywords(game.getNextTimestamp(), 0, Lists.newArrayList("This card doesn't untap during your next untap step."));
} }
} }

View File

@@ -3014,7 +3014,7 @@ public class Player extends GameEntity implements Comparable<Player> {
game.getAction().checkStaticAbilities(false); game.getAction().checkStaticAbilities(false);
for (final Card c : getCardsIn(ZoneType.Sideboard)) { for (final Card c : getCardsIn(ZoneType.Sideboard)) {
for (KeywordInterface inst : c.getKeywords()) { for (KeywordInterface inst : c.getKeywords(Keyword.COMPANION)) {
if (!(inst instanceof Companion)) { if (!(inst instanceof Companion)) {
continue; continue;
} }

View File

@@ -0,0 +1,38 @@
package forge.game.staticability;
import forge.game.Game;
import forge.game.card.Card;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
public class StaticAbilityAdapt {
static String MODE = "CanAdapt";
public static boolean anyWithAdapt(final SpellAbility sa, final Card card) {
final Game game = card.getGame();
for (final Card ca : game.getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) {
for (final StaticAbility stAb : ca.getStaticAbilities()) {
if (!stAb.getParam("Mode").equals(MODE) || stAb.isSuppressed() || !stAb.checkConditions()) {
continue;
}
if (applyWithAdapt(stAb, sa, card)) {
return true;
}
}
}
return false;
}
public static boolean applyWithAdapt(final StaticAbility stAb, final SpellAbility sa, final Card card) {
if (!stAb.matchesValidParam("ValidCard", card)) {
return false;
}
if (!stAb.matchesValidParam("ValidSA", sa)) {
return false;
}
return true;
}
}

View File

@@ -17,14 +17,19 @@
*/ */
package forge.game.staticability; package forge.game.staticability;
import org.apache.commons.lang3.tuple.MutablePair;
import org.apache.commons.lang3.tuple.Pair;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import forge.game.Game;
import forge.game.GameEntity; import forge.game.GameEntity;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardCollectionView; import forge.game.card.CardCollectionView;
import forge.game.card.CardPredicates; import forge.game.card.CardPredicates;
import forge.game.cost.Cost; import forge.game.cost.Cost;
import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordInterface; import forge.game.keyword.KeywordInterface;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
@@ -34,6 +39,8 @@ import forge.game.zone.ZoneType;
*/ */
public class StaticAbilityCantAttackBlock { public class StaticAbilityCantAttackBlock {
public static String MinMaxBlockerMode = "MinMaxBlocker";
/** /**
* TODO Write javadoc for this method. * TODO Write javadoc for this method.
* *
@@ -221,4 +228,43 @@ public class StaticAbilityCantAttackBlock {
} }
return true; return true;
} }
public static Pair<Integer, Integer> getMinMaxBlocker(final Card attacker, final Player defender) {
MutablePair<Integer, Integer> result = MutablePair.of(1, Integer.MAX_VALUE);
// Menace keyword
if (attacker.hasKeyword(Keyword.MENACE)) {
result.setLeft(2);
}
final Game game = attacker.getGame();
for (final Card ca : game.getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) {
for (final StaticAbility stAb : ca.getStaticAbilities()) {
if (!stAb.getParam("Mode").equals(MinMaxBlockerMode) || stAb.isSuppressed() || !stAb.checkConditions()) {
continue;
}
applyMinMaxBlockerAbility(stAb, attacker, defender, result);
}
}
if (attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) {
if (defender != null) {
result.setLeft(defender.getCreaturesInPlay().size());
}
}
return result;
}
public static void applyMinMaxBlockerAbility(final StaticAbility stAb, final Card attacker, final Player defender, MutablePair<Integer, Integer> result) {
if (!stAb.matchesValidParam("ValidCard", attacker)) {
return;
}
if (stAb.hasParam("Min")) {
result.setLeft(AbilityUtils.calculateAmount(stAb.getHostCard(), stAb.getParam("Min"), stAb));
}
if (stAb.hasParam("Max")) {
result.setRight(AbilityUtils.calculateAmount(stAb.getHostCard(), stAb.getParam("Max"), stAb));
}
}
} }

View File

@@ -326,7 +326,7 @@ public final class StaticAbilityContinuous {
if (params.containsKey("SharedKeywordsZone")) { if (params.containsKey("SharedKeywordsZone")) {
List<ZoneType> zones = ZoneType.listValueOf(params.get("SharedKeywordsZone")); List<ZoneType> zones = ZoneType.listValueOf(params.get("SharedKeywordsZone"));
String[] restrictions = params.containsKey("SharedRestrictions") ? params.get("SharedRestrictions").split(",") : new String[] {"Card"}; String[] restrictions = params.containsKey("SharedRestrictions") ? params.get("SharedRestrictions").split(",") : new String[] {"Card"};
addKeywords = CardFactoryUtil.sharedKeywords(addKeywords, restrictions, zones, hostCard); addKeywords = CardFactoryUtil.sharedKeywords(addKeywords, restrictions, zones, hostCard, stAb);
} }
} }
@@ -708,9 +708,7 @@ public final class StaticAbilityContinuous {
// add HIDDEN keywords // add HIDDEN keywords
if (!addHiddenKeywords.isEmpty()) { if (!addHiddenKeywords.isEmpty()) {
for (final String k : addHiddenKeywords) { affectedCard.addHiddenExtrinsicKeywords(hostCard.getTimestamp(), stAb.getId(), addHiddenKeywords);
affectedCard.addHiddenExtrinsicKeyword(k);
}
} }
// add SVars // add SVars

View File

@@ -3,6 +3,6 @@ ManaCost:1 G
Types:Enchantment Aura Types:Enchantment Aura
K:Enchant creature K:Enchant creature
A:SP$ Attach | Cost$ 1 G | ValidTgts$ Creature | AILogic$ Pump A:SP$ Attach | Cost$ 1 G | ValidTgts$ Creature | AILogic$ Pump
S:Mode$ Continuous | Affected$ Creature.EnchantedBy | AddKeyword$ Hexproof | AddHiddenKeyword$ CantBeBlockedByAmount GT1 | Description$ Enchanted creature has hexproof and can't be blocked by more than one creature. S:Mode$ Continuous | Affected$ Creature.EnchantedBy | AddKeyword$ Hexproof | Description$ Enchanted creature has hexproof and can't be blocked by more than one creature.
SVar:Picture:http://www.wizards.com/global/images/magic/general/alpha_authority.jpg S:Mode$ MinMaxBlocker | ValidCard$ Creature.EnchantedBy | Max$ 1 | Secondary$ True | Description$ Enchanted creature can't be blocked by more than one creature.
Oracle:Enchant creature\nEnchanted creature has hexproof and can't be blocked by more than one creature. Oracle:Enchant creature\nEnchanted creature has hexproof and can't be blocked by more than one creature.

View File

@@ -2,10 +2,9 @@ Name:Battlefront Krushok
ManaCost:4 G ManaCost:4 G
Types:Creature Beast Types:Creature Beast
PT:3/4 PT:3/4
K:CantBeBlockedByAmount GT1 S:Mode$ MinMaxBlocker | ValidCard$ Card.Self | Max$ 1 | Description$ CARDNAME can't be blocked by more than one creature.
S:Mode$ Continuous | Affected$ Creature.YouCtrl+counters_GE1_P1P1 | AddHiddenKeyword$ CantBeBlockedByAmount GT1 | Description$ Each creature you control with a +1/+1 counter on it can't be blocked by more than one creature. S:Mode$ MinMaxBlocker | ValidCard$ Creature.YouCtrl+counters_GE1_P1P1 | Max$ 1 | Description$ Each creature you control with a +1/+1 counter on it can't be blocked by more than one creature.
SVar:NonStackingEffect:True SVar:NonStackingEffect:True
SVar:PlayMain1:TRUE SVar:PlayMain1:TRUE
DeckHints:Ability$Counters DeckHints:Ability$Counters
SVar:Picture:http://www.wizards.com/global/images/magic/general/battlefront_krushok.jpg
Oracle:Battlefront Krushok can't be blocked by more than one creature.\nEach creature you control with a +1/+1 counter on it can't be blocked by more than one creature. Oracle:Battlefront Krushok can't be blocked by more than one creature.\nEach creature you control with a +1/+1 counter on it can't be blocked by more than one creature.

View File

@@ -3,6 +3,9 @@ ManaCost:G U
Types:Creature Mutant Types:Creature Mutant
PT:2/2 PT:2/2
S:Mode$ ReduceCost | ValidCard$ Creature.YouCtrl | Type$ Ability | Amount$ 2 | MinMana$ 1 | AffectedZone$ Battlefield | Description$ Activated abilities of creatures you control cost {2} less to activate. This effect can't reduce the mana in that cost to less than one mana. S:Mode$ ReduceCost | ValidCard$ Creature.YouCtrl | Type$ Ability | Amount$ 2 | MinMana$ 1 | AffectedZone$ Battlefield | Description$ Activated abilities of creatures you control cost {2} less to activate. This effect can't reduce the mana in that cost to less than one mana.
A:AB$ Pump | Cost$ T | ValidTgts$ Creature | KW$ HIDDEN CARDNAME adapts as though it had no +1/+1 counters | TgtPrompt$ Select target creature. | StackDescription$ SpellDescription | SpellDescription$ The next time target creature adapts this turn, it adapts as though it had no +1/+1 counters on it. A:AB$ Effect | Cost$ T | ValidTgts$ Creature | RememberObjects$ ThisTargetedCard | StaticAbilities$ StaticAllowAdapt | Triggers$ TriggerClearAdapt | TgtPrompt$ Select target creature. | StackDescription$ SpellDescription | SpellDescription$ The next time target creature adapts this turn, it adapts as though it had no +1/+1 counters on it.
SVar:StaticAllowAdapt:Mode$ CanAdapt | ValidCard$ Card.IsRemembered | Description$ Remembered adapts as though it had no +1/+1 counters on it.
SVar:TriggerClearAdapt:Mode$ Adapt | ValidCard$ Card.IsRemembered | Execute$ ExileSelf | Static$ True
SVar:ExileSelf:DB$ ChangeZone | Defined$ Self | Origin$ Command | Destination$ Exile
DeckHints:Keyword$Adapt DeckHints:Keyword$Adapt
Oracle:Activated abilities of creatures you control cost {2} less to activate. This effect can't reduce the mana in that cost to less than one mana.\n{T}: The next time target creature adapts this turn, it adapts as though it had no +1/+1 counters on it. Oracle:Activated abilities of creatures you control cost {2} less to activate. This effect can't reduce the mana in that cost to less than one mana.\n{T}: The next time target creature adapts this turn, it adapts as though it had no +1/+1 counters on it.

View File

@@ -1,6 +1,6 @@
Name:Bristling Boar Name:Bristling Boar
ManaCost:3 G ManaCost:3 G
Types:Creature Boar Types:Creature Boar
K:CantBeBlockedByAmount GT1
Oracle:Bristling Boar can't be blocked by more than one creature.
PT:4/3 PT:4/3
S:Mode$ MinMaxBlocker | ValidCard$ Card.Self | Max$ 1 | Description$ CARDNAME can't be blocked by more than one creature.
Oracle:Bristling Boar can't be blocked by more than one creature.

View File

@@ -2,7 +2,7 @@ Name:Challenger Troll
ManaCost:4 G ManaCost:4 G
Types:Creature Troll Types:Creature Troll
PT:6/5 PT:6/5
S:Mode$ Continuous | Affected$ Creature.YouCtrl+powerGE4 | AddHiddenKeyword$ CantBeBlockedByAmount GT1 | Description$ Each creature you control with power 4 or greater can't be blocked by more than one creature. S:Mode$ MinMaxBlocker | ValidCard$ Creature.YouCtrl+powerGE4 | Max$ 1 | Description$ Each creature you control with power 4 or greater can't be blocked by more than one creature.
SVar:PlayMain1:TRUE SVar:PlayMain1:TRUE
AI:RemoveDeck:Random AI:RemoveDeck:Random
Oracle:Each creature you control with power 4 or greater can't be blocked by more than one creature. Oracle:Each creature you control with power 4 or greater can't be blocked by more than one creature.

View File

@@ -2,6 +2,5 @@ Name:Charging Rhino
ManaCost:3 G G ManaCost:3 G G
Types:Creature Rhino Types:Creature Rhino
PT:4/4 PT:4/4
K:CantBeBlockedByAmount GT1 S:Mode$ MinMaxBlocker | ValidCard$ Card.Self | Max$ 1 | Description$ CARDNAME can't be blocked by more than one creature.
SVar:Picture:http://www.wizards.com/global/images/magic/general/charging_rhino.jpg
Oracle:Charging Rhino can't be blocked by more than one creature. Oracle:Charging Rhino can't be blocked by more than one creature.

View File

@@ -1,8 +1,7 @@
Name:Familiar Ground Name:Familiar Ground
ManaCost:2 G ManaCost:2 G
Types:Enchantment Types:Enchantment
S:Mode$ Continuous | Affected$ Creature.YouCtrl | AddHiddenKeyword$ CantBeBlockedByAmount GT1 | Description$ Each creature you control can't be blocked by more than one creature. S:Mode$ MinMaxBlocker | ValidCard$ Creature.YouCtrl | Max$ 1 | Description$ Each creature you control can't be blocked by more than one creature.
SVar:NonStackingEffect:True SVar:NonStackingEffect:True
SVar:PlayMain1:TRUE SVar:PlayMain1:TRUE
SVar:Picture:http://www.wizards.com/global/images/magic/general/familiar_ground.jpg
Oracle:Each creature you control can't be blocked by more than one creature. Oracle:Each creature you control can't be blocked by more than one creature.

View File

@@ -4,6 +4,5 @@ Types:Creature Ape Berserker
PT:2/3 PT:2/3
K:Trample K:Trample
K:Rampage:2 K:Rampage:2
K:CantBeBlockedByAmount LT3 S:Mode$ MinMaxBlocker | ValidCard$ Creature.Self | Min$ 3 | Description$ CARDNAME can't be blocked except by three or more creatures.
SVar:Picture:http://www.wizards.com/global/images/magic/general/gorilla_berserkers.jpg
Oracle:Trample; rampage 2 (Whenever this creature becomes blocked, it gets +2/+2 until end of turn for each creature blocking it beyond the first.)\nGorilla Berserkers can't be blocked except by three or more creatures. Oracle:Trample; rampage 2 (Whenever this creature becomes blocked, it gets +2/+2 until end of turn for each creature blocking it beyond the first.)\nGorilla Berserkers can't be blocked except by three or more creatures.

View File

@@ -2,11 +2,10 @@ Name:Guile
ManaCost:3 U U U ManaCost:3 U U U
Types:Creature Elemental Incarnation Types:Creature Elemental Incarnation
PT:6/6 PT:6/6
K:CantBeBlockedByAmount LT3 S:Mode$ MinMaxBlocker | ValidCard$ Creature.Self | Min$ 3 | Description$ CARDNAME can't be blocked except by three or more creatures.
R:Event$ Counter | ActiveZones$ Battlefield | ValidType$ Spell | ValidCause$ Card.YouCtrl | ReplaceWith$ DBRemove | Description$ If a spell or ability you control would counter a spell, instead exile that spell and you may play that card without paying its mana cost. R:Event$ Counter | ActiveZones$ Battlefield | ValidType$ Spell | ValidCause$ Card.YouCtrl | ReplaceWith$ DBRemove | Description$ If a spell or ability you control would counter a spell, instead exile that spell and you may play that card without paying its mana cost.
SVar:DBRemove:DB$ ChangeZone | Defined$ ReplacedCard | Origin$ Stack | Destination$ Exile | Fizzle$ True | SubAbility$ DBPlay SVar:DBRemove:DB$ ChangeZone | Defined$ ReplacedCard | Origin$ Stack | Destination$ Exile | Fizzle$ True | SubAbility$ DBPlay
SVar:DBPlay:DB$ Play | Defined$ ReplacedCard | WithoutManaCost$ True | Optional$ True SVar:DBPlay:DB$ Play | Defined$ ReplacedCard | WithoutManaCost$ True | Optional$ True
T:Mode$ ChangesZone | Origin$ Any | Destination$ Graveyard | ValidCard$ Card.Self | Execute$ TrigShuffle | TriggerDescription$ When CARDNAME is put into a graveyard from anywhere, shuffle it into its owner's library. T:Mode$ ChangesZone | Origin$ Any | Destination$ Graveyard | ValidCard$ Card.Self | Execute$ TrigShuffle | TriggerDescription$ When CARDNAME is put into a graveyard from anywhere, shuffle it into its owner's library.
SVar:TrigShuffle:DB$ ChangeZone | Origin$ Graveyard | Destination$ Library | Shuffle$ True | Defined$ TriggeredCardLKICopy SVar:TrigShuffle:DB$ ChangeZone | Origin$ Graveyard | Destination$ Library | Shuffle$ True | Defined$ TriggeredCardLKICopy
SVar:Picture:http://www.wizards.com/global/images/magic/general/guile.jpg
Oracle:Guile can't be blocked except by three or more creatures.\nIf a spell or ability you control would counter a spell, instead exile that spell and you may play that card without paying its mana cost.\nWhen Guile is put into a graveyard from anywhere, shuffle it into its owner's library. Oracle:Guile can't be blocked except by three or more creatures.\nIf a spell or ability you control would counter a spell, instead exile that spell and you may play that card without paying its mana cost.\nWhen Guile is put into a graveyard from anywhere, shuffle it into its owner's library.

View File

@@ -2,6 +2,5 @@ Name:Huang Zhong, Shu General
ManaCost:2 W W ManaCost:2 W W
Types:Legendary Creature Human Soldier Types:Legendary Creature Human Soldier
PT:2/3 PT:2/3
K:CantBeBlockedByAmount GT1 S:Mode$ MinMaxBlocker | ValidCard$ Card.Self | Max$ 1 | Description$ CARDNAME can't be blocked by more than one creature.
SVar:Picture:http://www.wizards.com/global/images/magic/general/huang_zhong_shu_general.jpg
Oracle:Huang Zhong, Shu General can't be blocked by more than one creature. Oracle:Huang Zhong, Shu General can't be blocked by more than one creature.

View File

@@ -2,7 +2,7 @@ Name:Hungering Hydra
ManaCost:X G ManaCost:X G
Types:Creature Hydra Types:Creature Hydra
PT:0/0 PT:0/0
K:CantBeBlockedByAmount GT1 S:Mode$ MinMaxBlocker | ValidCard$ Card.Self | Max$ 1 | Description$ CARDNAME can't be blocked by more than one creature.
K:etbCounter:P1P1:X K:etbCounter:P1P1:X
SVar:X:Count$xPaid SVar:X:Count$xPaid
T:Mode$ DamageDoneOnce | ValidTarget$ Card.Self | TriggerZones$ Battlefield | Execute$ TrigPutCounter | TriggerDescription$ Whenever CARDNAME is dealt damage, put that many +1/+1 counters on CARDNAME. T:Mode$ DamageDoneOnce | ValidTarget$ Card.Self | TriggerZones$ Battlefield | Execute$ TrigPutCounter | TriggerDescription$ Whenever CARDNAME is dealt damage, put that many +1/+1 counters on CARDNAME.

View File

@@ -2,6 +2,5 @@ Name:Ironhoof Ox
ManaCost:3 G G ManaCost:3 G G
Types:Creature Ox Types:Creature Ox
PT:4/4 PT:4/4
K:CantBeBlockedByAmount GT1 S:Mode$ MinMaxBlocker | ValidCard$ Card.Self | Max$ 1 | Description$ CARDNAME can't be blocked by more than one creature.
SVar:Picture:http://resources.wizards.com/magic/cards/p2/en-us/card6628.jpg
Oracle:Ironhoof Ox can't be blocked by more than one creature. Oracle:Ironhoof Ox can't be blocked by more than one creature.

View File

@@ -1,36 +1,18 @@
Name:Kessig Prowler Name:Kessig Prowler
ManaCost:G ManaCost:G
Types:Creature Werewolf Horror Types:Creature Werewolf Horror
PT:2/1 PT:2/1
A:AB$SetState | Cost$ 4 G | Defined$ Self | Mode$ Transform | SpellDescription$ Transform CARDNAME. A:AB$SetState | Cost$ 4 G | Defined$ Self | Mode$ Transform | SpellDescription$ Transform CARDNAME.
SVar:Picture:http://www.wizards.com/global/images/magic/general/kessig_prowler.jpg
AlternateMode:DoubleFaced AlternateMode:DoubleFaced
Oracle:{4}{G}: Transform Kessig Prowler. Oracle:{4}{G}: Transform Kessig Prowler.
ALTERNATE ALTERNATE
Name:Sinuous Predator Name:Sinuous Predator
ManaCost:no cost ManaCost:no cost
Types:Creature Eldrazi Werewolf Types:Creature Eldrazi Werewolf
PT:4/4 PT:4/4
S:Mode$ MinMaxBlocker | ValidCard$ Card.Self | Max$ 1 | Description$ CARDNAME can't be blocked by more than one creature.
K:CantBeBlockedByAmount GT1
SVar:Picture:http://www.wizards.com/global/images/magic/general/sinuous_predator.jpg
Oracle:Sinuous Predator can't be blocked by more than one creature. Oracle:Sinuous Predator can't be blocked by more than one creature.

View File

@@ -3,6 +3,5 @@ ManaCost:3 G
Types:Creature Cat Beast Types:Creature Cat Beast
PT:3/2 PT:3/2
K:Provoke K:Provoke
K:CantBeBlockedByAmount GT1 S:Mode$ MinMaxBlocker | ValidCard$ Card.Self | Max$ 1 | Description$ CARDNAME can't be blocked by more than one creature.
SVar:Picture:http://www.wizards.com/global/images/magic/general/krosan_vorine.jpg
Oracle:Provoke (Whenever this creature attacks, you may have target creature defending player controls untap and block it if able.)\nKrosan Vorine can't be blocked by more than one creature. Oracle:Provoke (Whenever this creature attacks, you may have target creature defending player controls untap and block it if able.)\nKrosan Vorine can't be blocked by more than one creature.

View File

@@ -2,6 +2,5 @@ Name:Norwood Riders
ManaCost:3 G ManaCost:3 G
Types:Creature Elf Types:Creature Elf
PT:3/3 PT:3/3
K:CantBeBlockedByAmount GT1 S:Mode$ MinMaxBlocker | ValidCard$ Card.Self | Max$ 1 | Description$ CARDNAME can't be blocked by more than one creature.
SVar:Picture:http://resources.wizards.com/magic/cards/p2/en-us/card6615.jpg
Oracle:Norwood Riders can't be blocked by more than one creature. Oracle:Norwood Riders can't be blocked by more than one creature.

View File

@@ -3,7 +3,6 @@ ManaCost:3 G G
Types:Creature Giant Types:Creature Giant
PT:6/6 PT:6/6
K:Renown:6 K:Renown:6
K:CantBeBlockedByAmount GT1 S:Mode$ MinMaxBlocker | ValidCard$ Card.Self | Max$ 1 | Description$ CARDNAME can't be blocked by more than one creature.
DeckHas:Ability$Counters DeckHas:Ability$Counters
SVar:Picture:http://www.wizards.com/global/images/magic/general/outland_colossus.jpg
Oracle:Renown 6 (When this creature deals combat damage to a player, if it isn't renowned, put six +1/+1 counters on it and it becomes renowned.)\nOutland Colossus can't be blocked by more than one creature. Oracle:Renown 6 (When this creature deals combat damage to a player, if it isn't renowned, put six +1/+1 counters on it and it becomes renowned.)\nOutland Colossus can't be blocked by more than one creature.

View File

@@ -3,6 +3,5 @@ ManaCost:11
Types:Creature Eldrazi Types:Creature Eldrazi
PT:9/9 PT:9/9
K:Annihilator:3 K:Annihilator:3
K:CantBeBlockedByAmount LT3 S:Mode$ MinMaxBlocker | ValidCard$ Creature.Self | Min$ 3 | Description$ CARDNAME can't be blocked except by three or more creatures.
SVar:Picture:http://www.wizards.com/global/images/magic/general/pathrazer_of_ulamog.jpg
Oracle:Annihilator 3 (Whenever this creature attacks, defending player sacrifices three permanents.)\nPathrazer of Ulamog can't be blocked except by three or more creatures. Oracle:Annihilator 3 (Whenever this creature attacks, defending player sacrifices three permanents.)\nPathrazer of Ulamog can't be blocked except by three or more creatures.

View File

@@ -4,7 +4,6 @@ Types:Artifact Creature Phyrexian Golem
PT:8/8 PT:8/8
K:CARDNAME doesn't untap during your untap step. K:CARDNAME doesn't untap during your untap step.
A:AB$ Untap | Cost$ PayLife<8> | SpellDescription$ Untap CARDNAME. A:AB$ Untap | Cost$ PayLife<8> | SpellDescription$ Untap CARDNAME.
K:CantBeBlockedByAmount LT3 S:Mode$ MinMaxBlocker | ValidCard$ Creature.Self | Min$ 3 | Description$ CARDNAME can't be blocked except by three or more creatures.
AI:RemoveDeck:All AI:RemoveDeck:All
SVar:Picture:http://www.wizards.com/global/images/magic/general/phyrexian_colossus.jpg
Oracle:Phyrexian Colossus doesn't untap during your untap step.\nPay 8 life: Untap Phyrexian Colossus.\nPhyrexian Colossus can't be blocked except by three or more creatures. Oracle:Phyrexian Colossus doesn't untap during your untap step.\nPay 8 life: Untap Phyrexian Colossus.\nPhyrexian Colossus can't be blocked except by three or more creatures.

View File

@@ -3,6 +3,6 @@ ManaCost:1 B/R B/R
Types:Creature Human Warrior Types:Creature Human Warrior
PT:2/2 PT:2/2
K:Menace K:Menace
S:Mode$ Continuous | Affected$ Creature.YouCtrl+withMenace | AddHiddenKeyword$ CantBeBlockedByAmount LT3 | Description$ Each creature you control with menace can't be blocked except by three or more creatures. S:Mode$ MinMaxBlocker | ValidCard$ Creature.YouCtrl+withMenace | Min$ 3 | Description$ Each creature you control with menace can't be blocked except by three or more creatures.
SVar:PlayMain1:TRUE SVar:PlayMain1:TRUE
Oracle:Menace\nEach creature you control with menace can't be blocked except by three or more creatures. Oracle:Menace\nEach creature you control with menace can't be blocked except by three or more creatures.

View File

@@ -2,6 +2,5 @@ Name:Stalking Tiger
ManaCost:3 G ManaCost:3 G
Types:Creature Cat Types:Creature Cat
PT:3/3 PT:3/3
K:CantBeBlockedByAmount GT1 S:Mode$ MinMaxBlocker | ValidCard$ Card.Self | Max$ 1 | Description$ CARDNAME can't be blocked by more than one creature.
SVar:Picture:http://www.wizards.com/global/images/magic/general/stalking_tiger.jpg
Oracle:Stalking Tiger can't be blocked by more than one creature. Oracle:Stalking Tiger can't be blocked by more than one creature.

View File

@@ -2,7 +2,7 @@ Name:Sunder Shaman
ManaCost:R R G G ManaCost:R R G G
Types:Creature Giant Shaman Types:Creature Giant Shaman
PT:5/5 PT:5/5
K:CantBeBlockedByAmount GT1 S:Mode$ MinMaxBlocker | ValidCard$ Card.Self | Max$ 1 | Description$ CARDNAME can't be blocked by more than one creature.
T:Mode$ DamageDone | ValidSource$ Card.Self | ValidTarget$ Player | CombatDamage$ True | Execute$ TrigDestroy | TriggerZones$ Battlefield | TriggerDescription$ Whenever CARDNAME deals combat damage to a player, destroy target artifact or enchantment that player controls. T:Mode$ DamageDone | ValidSource$ Card.Self | ValidTarget$ Player | CombatDamage$ True | Execute$ TrigDestroy | TriggerZones$ Battlefield | TriggerDescription$ Whenever CARDNAME deals combat damage to a player, destroy target artifact or enchantment that player controls.
SVar:TrigDestroy:DB$ Destroy | ValidTgts$ Artifact.ControlledBy TriggeredDefendingPlayer,Enchantment.ControlledBy TriggeredDefendingPlayer | TgtPrompt$ Select target artifact or enchantment that player controls. SVar:TrigDestroy:DB$ Destroy | ValidTgts$ Artifact.ControlledBy TriggeredDefendingPlayer,Enchantment.ControlledBy TriggeredDefendingPlayer | TgtPrompt$ Select target artifact or enchantment that player controls.
Oracle:Sunder Shaman can't be blocked by more than one creature.\nWhenever Sunder Shaman deals combat damage to a player, destroy target artifact or enchantment that player controls. Oracle:Sunder Shaman can't be blocked by more than one creature.\nWhenever Sunder Shaman deals combat damage to a player, destroy target artifact or enchantment that player controls.

View File

@@ -2,7 +2,7 @@ Name:Tahngarth, First Mate
ManaCost:2 R G ManaCost:2 R G
Types:Legendary Creature Minotaur Warrior Types:Legendary Creature Minotaur Warrior
PT:5/5 PT:5/5
K:CantBeBlockedByAmount GT1 S:Mode$ MinMaxBlocker | ValidCard$ Card.Self | Max$ 1 | Description$ CARDNAME can't be blocked by more than one creature.
T:Mode$ AttackersDeclared | AttackingPlayer$ Player.Opponent | Execute$ TrigGainControl | TriggerZones$ Battlefield | OptionalDecider$ You | IsPresent$ Card.Self+tapped | TriggerDescription$ Whenever an opponent attacks with one or more creatures, if CARDNAME is tapped, you may have that opponent gain control of CARDNAME until end of combat. If you do, choose a player or planeswalker that opponent is attacking. CARDNAME is attacking that player or planeswalker. T:Mode$ AttackersDeclared | AttackingPlayer$ Player.Opponent | Execute$ TrigGainControl | TriggerZones$ Battlefield | OptionalDecider$ You | IsPresent$ Card.Self+tapped | TriggerDescription$ Whenever an opponent attacks with one or more creatures, if CARDNAME is tapped, you may have that opponent gain control of CARDNAME until end of combat. If you do, choose a player or planeswalker that opponent is attacking. CARDNAME is attacking that player or planeswalker.
SVar:TrigGainControl:DB$ GainControl | Defined$ Self | NewController$ TriggeredAttackingPlayer | LoseControl$ EndOfCombat | Attacking$ Player.Defending | Chooser$ You | ChoosePlayerOrPlaneswalker$ True SVar:TrigGainControl:DB$ GainControl | Defined$ Self | NewController$ TriggeredAttackingPlayer | LoseControl$ EndOfCombat | Attacking$ Player.Defending | Chooser$ You | ChoosePlayerOrPlaneswalker$ True
AI:RemoveDeck:All AI:RemoveDeck:All

View File

@@ -3,7 +3,9 @@ ManaCost:3 R
Types:Enchantment Saga Types:Enchantment Saga
K:Saga:3:DBGainControl,DBAllAttack,DBDamageTapped K:Saga:3:DBGainControl,DBAllAttack,DBDamageTapped
SVar:DBGainControl:DB$ GainControl | ValidTgts$ Creature | TgtPrompt$ Select target creature | LoseControl$ LeavesPlay | SpellDescription$ Gain control of target creature for as long as CARDNAME remains on the battlefield. SVar:DBGainControl:DB$ GainControl | ValidTgts$ Creature | TgtPrompt$ Select target creature | LoseControl$ LeavesPlay | SpellDescription$ Gain control of target creature for as long as CARDNAME remains on the battlefield.
SVar:DBAllAttack:DB$ PumpAll | ValidCards$ Creature.OppCtrl | Duration$ UntilYourNextTurn | KW$ HIDDEN CARDNAME attacks each combat if able. | SpellDescription$ Until your next turn, creatures your opponents control attack each turn if able.
# Refactor as Effect
SVar:DBAllAttack:DB$ PumpAll | ValidCards$ Creature.OppCtrl | Duration$ UntilYourNextTurn | KW$ HIDDEN CARDNAME attacks each combat if able. | SpellDescription$ Until your next turn, creatures your opponents control attack each combat if able.
SVar:DBDamageTapped:DB$ EachDamage | ValidCards$ Creature.tapped | NumDmg$ X | DamageDesc$ damage equal to its power | DefinedCards$ Self | SpellDescription$ Each tapped creature deals damage to itself equal to its power. SVar:DBDamageTapped:DB$ EachDamage | ValidCards$ Creature.tapped | NumDmg$ X | DamageDesc$ damage equal to its power | DefinedCards$ Self | SpellDescription$ Each tapped creature deals damage to itself equal to its power.
SVar:X:Count$CardPower SVar:X:Count$CardPower
Oracle:(As this Saga enters and after your draw step, add a lore counter. Sacrifice after III.)\nI — Gain control of target creature for as long as The Akroan War remains on the battlefield.\nII — Until your next turn, creatures your opponents control attack each combat if able.\nIII — Each tapped creature deals damage to itself equal to its power. Oracle:(As this Saga enters and after your draw step, add a lore counter. Sacrifice after III.)\nI — Gain control of target creature for as long as The Akroan War remains on the battlefield.\nII — Until your next turn, creatures your opponents control attack each combat if able.\nIII — Each tapped creature deals damage to itself equal to its power.

View File

@@ -2,7 +2,7 @@ Name:Underworld Cerberus
ManaCost:3 B R ManaCost:3 B R
Types:Creature Dog Types:Creature Dog
PT:6/6 PT:6/6
K:CantBeBlockedByAmount LT3 S:Mode$ MinMaxBlocker | ValidCard$ Creature.Self | Min$ 3 | Description$ CARDNAME can't be blocked except by three or more creatures.
S:Mode$ CantTarget | AffectedZone$ Graveyard | Description$ Cards in graveyards can't be the targets of spells or abilities. S:Mode$ CantTarget | AffectedZone$ Graveyard | Description$ Cards in graveyards can't be the targets of spells or abilities.
T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Card.Self | Execute$ TrigExile | TriggerController$ TriggeredCardController | TriggerDescription$ When CARDNAME dies, exile it and each player returns all creature cards from their graveyard to their hand. T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Card.Self | Execute$ TrigExile | TriggerController$ TriggeredCardController | TriggerDescription$ When CARDNAME dies, exile it and each player returns all creature cards from their graveyard to their hand.
SVar:TrigExile:DB$ ChangeZone | Origin$ Graveyard | Destination$ Exile | Defined$ TriggeredNewCardLKICopy | SubAbility$ DBChangeZoneAll SVar:TrigExile:DB$ ChangeZone | Origin$ Graveyard | Destination$ Exile | Defined$ TriggeredNewCardLKICopy | SubAbility$ DBChangeZoneAll

View File

@@ -2,7 +2,7 @@ Name:Vigorspore Wurm
ManaCost:5 G ManaCost:5 G
Types:Creature Wurm Types:Creature Wurm
PT:6/4 PT:6/4
K:CantBeBlockedByAmount GT1 S:Mode$ MinMaxBlocker | ValidCard$ Card.Self | Max$ 1 | Description$ CARDNAME can't be blocked by more than one creature.
T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigPump | TriggerDescription$ Undergrowth - When CARDNAME enters the battlefield, target creature gains vigilance and gets +X/+X until end of turn, where X is the number of creature cards in your graveyard. T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigPump | TriggerDescription$ Undergrowth - When CARDNAME enters the battlefield, target creature gains vigilance and gets +X/+X until end of turn, where X is the number of creature cards in your graveyard.
SVar:TrigPump:DB$ Pump | ValidTgts$ Creature | TgtPrompt$ Select target creature | NumAtt$ X | NumDef$ X | KW$ Vigilance SVar:TrigPump:DB$ Pump | ValidTgts$ Creature | TgtPrompt$ Select target creature | NumAtt$ X | NumDef$ X | KW$ Vigilance
SVar:X:Count$TypeInYourYard.Creature SVar:X:Count$TypeInYourYard.Creature

View File

@@ -2,6 +2,6 @@ Name:Vorrac Battlehorns
ManaCost:2 ManaCost:2
Types:Artifact Equipment Types:Artifact Equipment
K:Equip:1 K:Equip:1
S:Mode$ Continuous | Affected$ Creature.EquippedBy | AddKeyword$ Trample | AddHiddenKeyword$ CantBeBlockedByAmount GT1 | Description$ Equipped creature has trample and can't be blocked by more than one creature. S:Mode$ Continuous | Affected$ Creature.EquippedBy | AddKeyword$ Trample | Description$ Equipped creature has trample and can't be blocked by more than one creature.
SVar:Picture:http://www.wizards.com/global/images/magic/general/vorrac_battlehorns.jpg S:Mode$ MinMaxBlocker | ValidCard$ Creature.EquippedBy | Max$ 1 | Secondary$ True | Description$ Equipped creature can't be blocked by more than one creature.
Oracle:Equipped creature has trample and can't be blocked by more than one creature.\nEquip {1} ({1}: Attach to target creature you control. Equip only as a sorcery. This card enters the battlefield unattached and stays on the battlefield if the creature leaves.) Oracle:Equipped creature has trample and can't be blocked by more than one creature.\nEquip {1} ({1}: Attach to target creature you control. Equip only as a sorcery. This card enters the battlefield unattached and stays on the battlefield if the creature leaves.)

View File

@@ -6,6 +6,7 @@ SVar:TrigToken:DB$ Token | LegacyImage$ g 2 2 wolf m20 | TokenAmount$ 1 | TokenS
SVar:DBAttach:DB$ Attach | Defined$ Remembered | SubAbility$ DBCleanup SVar:DBAttach:DB$ Attach | Defined$ Remembered | SubAbility$ DBCleanup
SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True
DeckHas:Ability$Token DeckHas:Ability$Token
S:Mode$ Continuous | Affected$ Creature.EquippedBy | AddPower$ 1 | AddToughness$ 1 | AddHiddenKeyword$ CantBeBlockedByAmount GT1 | Description$ Equipped creature gets +1/+1 and can't be blocked by more than one creature. S:Mode$ Continuous | Affected$ Creature.EquippedBy | AddPower$ 1 | AddToughness$ 1 | Description$ Equipped creature gets +1/+1 and can't be blocked by more than one creature.
S:Mode$ MinMaxBlocker | ValidCard$ Creature.EquippedBy | Max$ 1 | Secondary$ True | Description$ Equipped creature can't be blocked by more than one creature.
K:Equip:3 K:Equip:3
Oracle:When Wolfrider's Saddle enters the battlefield, create a 2/2 green Wolf creature token, then attach Wolfrider's Saddle to it.\nEquipped creature gets +1/+1 and can't be blocked by more than one creature.\nEquip {3} ({3}: Attach to target creature you control. Equip only as a sorcery.) Oracle:When Wolfrider's Saddle enters the battlefield, create a 2/2 green Wolf creature token, then attach Wolfrider's Saddle to it.\nEquipped creature gets +1/+1 and can't be blocked by more than one creature.\nEquip {3} ({3}: Attach to target creature you control. Equip only as a sorcery.)

View File

@@ -3,6 +3,5 @@ ManaCost:4 R
Types:Legendary Creature Human Soldier Types:Legendary Creature Human Soldier
PT:2/3 PT:2/3
K:Horsemanship K:Horsemanship
S:Mode$ Continuous | Affected$ Creature.YouCtrl | AddHiddenKeyword$ CantBeBlockedByAmount GT1 | Description$ Each creature you control can't be blocked by more than one creature. S:Mode$ MinMaxBlocker | ValidCard$ Creature.YouCtrl | Max$ 1 | Description$ Each creature you control can't be blocked by more than one creature.
SVar:Picture:http://www.wizards.com/global/images/magic/general/yuan_shao_the_indecisive.jpg
Oracle:Horsemanship (This creature can't be blocked except by creatures with horsemanship.)\nEach creature you control can't be blocked by more than one creature. Oracle:Horsemanship (This creature can't be blocked except by creatures with horsemanship.)\nEach creature you control can't be blocked by more than one creature.

View File

@@ -1,11 +1,7 @@
All creatures able to block CARDNAME do so. All creatures able to block CARDNAME do so.
Banding Banding
CantBeBlockedByAmount LT2
CantBeBlockedByAmount LT3
CARDNAME's activated abilities can't be activated. CARDNAME's activated abilities can't be activated.
CARDNAME attacks each turn if able.
CARDNAME attacks each combat if able. CARDNAME attacks each combat if able.
CARDNAME blocks each turn if able.
CARDNAME blocks each combat if able. CARDNAME blocks each combat if able.
CARDNAME can attack as though it didn't have defender. CARDNAME can attack as though it didn't have defender.
CARDNAME can block any number of creatures. CARDNAME can block any number of creatures.