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

@@ -6,12 +6,12 @@
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
@@ -42,7 +42,6 @@ import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.combat.GlobalAttackRestrictions;
import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordInterface;
import forge.game.player.Player;
import forge.game.player.PlayerPredicates;
import forge.game.spellability.SpellAbility;
@@ -60,7 +59,7 @@ import forge.util.collect.FCollectionView;
* <p>
* ComputerUtil_Attack2 class.
* </p>
*
*
* @author Forge
* @version $Id$
*/
@@ -72,10 +71,10 @@ public class AiAttackController {
private List<Card> oppList; // holds human player creatures
private List<Card> myList; // holds computer creatures
private final Player ai;
private Player defendingOpponent;
private int aiAggression = 0; // added by Masher, how aggressive the ai is attack will be depending on circumstances
private final boolean nextTurn;
@@ -108,7 +107,7 @@ public class AiAttackController {
public AiAttackController(final Player ai, Card attacker) {
this.ai = ai;
this.defendingOpponent = choosePreferredDefenderPlayer(ai);
this.defendingOpponent = choosePreferredDefenderPlayer(ai);
this.oppList = getOpponentCreatures(this.defendingOpponent);
this.myList = ai.getCreaturesInPlay();
this.attackers = new ArrayList<>();
@@ -118,7 +117,7 @@ public class AiAttackController {
this.blockers = getPossibleBlockers(oppList, this.attackers);
this.nextTurn = false;
} // overloaded constructor to evaluate single specified attacker
public static List<Card> getOpponentCreatures(final Player defender) {
List<Card> defenders = new ArrayList<>(defender.getCreaturesInPlay());
Predicate<Card> canAnimate = new Predicate<Card>() {
@@ -133,7 +132,7 @@ public class AiAttackController {
}
for (SpellAbility sa : c.getSpellAbilities()) {
if (sa.getApi() == ApiType.Animate) {
if (ComputerUtilCost.canPayCost(sa, defender)
if (ComputerUtilCost.canPayCost(sa, defender)
&& sa.getRestrictions().checkOtherRestrictions(c, sa, defender)) {
Card animatedCopy = AnimateAi.becomeAnimated(c, sa);
defenders.add(animatedCopy);
@@ -143,7 +142,7 @@ public class AiAttackController {
}
return defenders;
}
public void removeBlocker(Card blocker) {
this.oppList.remove(blocker);
}
@@ -200,7 +199,7 @@ public class AiAttackController {
* <p>
* isEffectiveAttacker.
* </p>
*
*
* @param attacker
* a {@link forge.game.card.Card} object.
* @param combat
@@ -501,7 +500,7 @@ public class AiAttackController {
// Conservative prediction for vehicles: the AI tries to acknowledge the fact that
// at least one creature will tap to crew a blocking vehicle when predicting if an
// alpha strike for lethal is viable
int maxBlockersAfterCrew = remainingBlockers.size();
int maxBlockersAfterCrew = remainingBlockers.size();
for (Card c : this.blockers) {
CardTypeView cardType = c.getCurrentState().getType();
CardCollectionView oppBattlefield = c.getController().getCardsIn(ZoneType.Battlefield);
@@ -514,7 +513,7 @@ public class AiAttackController {
} else if (c.getName().equals("Peacewalker Colossus")) {
// can activate other vehicles for {1}{W}
// TODO: the AI should ideally predict how many times it can activate
// for now, unless the opponent is tapped out, break at this point
// for now, unless the opponent is tapped out, break at this point
// and do not predict the blocker limit (which is safer)
if (!CardLists.filter(oppBattlefield, Predicates.and(CardPredicates.Presets.UNTAPPED, CardPredicates.Presets.LANDS)).isEmpty()) {
maxBlockersAfterCrew = Integer.MAX_VALUE;
@@ -600,7 +599,7 @@ public class AiAttackController {
maxBlockersAfterCrew--;
}
unblockedAttackers.addAll(remainingAttackers);
int trampleDamage = 0;
for (Card attacker : blockedAttackers) {
if (attacker.hasKeyword(Keyword.TRAMPLE)) {
@@ -674,7 +673,7 @@ public class AiAttackController {
* <p>
* Getter for the field <code>attackers</code>.
* </p>
*
*
* @return a {@link forge.game.combat.Combat} object.
*/
public final void declareAttackers(final Combat combat) {
@@ -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
mustAttack = true;
} else {
for (KeywordInterface inst : attacker.getKeywords()) {
String s = inst.getOriginal();
if (s.equals("CARDNAME attacks each turn if able.")
|| s.startsWith("CARDNAME attacks specific player each combat if able")
|| s.equals("CARDNAME attacks each combat if able.")) {
mustAttack = true;
break;
}
// TODO move to static Ability
if (attacker.hasKeyword("CARDNAME attacks each combat if able.") || attacker.hasStartOfKeyword("CARDNAME attacks specific player each combat if able")) {
mustAttack = true;
}
}
if (mustAttack || attacker.getController().getMustAttackEntity() != null || attacker.getController().getMustAttackEntityThisTurn() != null) {
@@ -799,7 +793,7 @@ public class AiAttackController {
}
}
}
// Exalted
if (combat.getAttackers().isEmpty()) {
boolean exalted = ai.countExaltedBonus() > 2;
@@ -857,7 +851,7 @@ public class AiAttackController {
// examine the potential forces
final List<Card> nextTurnAttackers = new ArrayList<>();
int candidateCounterAttackDamage = 0;
final Player opp = this.defendingOpponent;
// get the potential damage and strength of the AI forces
final List<Card> candidateAttackers = new ArrayList<>();
@@ -1115,7 +1109,7 @@ public class AiAttackController {
* <p>
* getAttack.
* </p>
*
*
* @param c
* a {@link forge.game.card.Card} object.
* @return a int.
@@ -1134,7 +1128,7 @@ public class AiAttackController {
* <p>
* shouldAttack.
* </p>
*
*
* @param attacker
* a {@link forge.game.card.Card} object.
* @param defenders
@@ -1179,7 +1173,7 @@ public class AiAttackController {
}
boolean hasAttackEffect = attacker.getSVar("HasAttackEffect").equals("TRUE") || attacker.hasStartOfKeyword("Annihilator");
// is there a gain in attacking even when the blocker is not killed (Lifelink, Wither,...)
boolean hasCombatEffect = attacker.getSVar("HasCombatEffect").equals("TRUE")
boolean hasCombatEffect = attacker.getSVar("HasCombatEffect").equals("TRUE")
|| "Blocked".equals(attacker.getSVar("HasAttackEffect"));
// contains only the defender's blockers that can actually block the attacker
@@ -1201,13 +1195,9 @@ public class AiAttackController {
int defPower = CardLists.getTotalPower(validBlockers, true, false);
if (!hasCombatEffect) {
for (KeywordInterface inst : attacker.getKeywords()) {
String keyword = inst.getOriginal();
if (keyword.equals("Wither") || keyword.equals("Infect")
|| keyword.equals("Lifelink") || keyword.startsWith("Afflict")) {
hasCombatEffect = true;
break;
}
if (attacker.hasKeyword(Keyword.WITHER) || attacker.hasKeyword(Keyword.INFECT)
|| attacker.hasKeyword(Keyword.LIFELINK) || attacker.hasKeyword(Keyword.AFFLICT)) {
hasCombatEffect = true;
}
}
@@ -1262,7 +1252,7 @@ public class AiAttackController {
}
}
}
if (!attacker.hasKeyword(Keyword.VIGILANCE) && ComputerUtilCard.canBeKilledByRoyalAssassin(ai, attacker)) {
canKillAllDangerous = false;
canBeKilled = true;
@@ -1272,7 +1262,7 @@ public class AiAttackController {
} else if ((canKillAllDangerous || !canBeKilled) && ComputerUtilCard.canBeBlockedProfitably(defendingOpponent, attacker)) {
canKillAllDangerous = false;
canBeKilled = true;
}
}
// if the creature cannot block and can kill all opponents they might as
// well attack, they do nothing staying back
@@ -1286,7 +1276,7 @@ public class AiAttackController {
return true;
}
if (numberOfPossibleBlockers > 2
if (numberOfPossibleBlockers > 2
|| (numberOfPossibleBlockers >= 1 && CombatUtil.canAttackerBeBlockedWithAmount(attacker, 1, this.defendingOpponent))
|| (numberOfPossibleBlockers == 2 && CombatUtil.canAttackerBeBlockedWithAmount(attacker, 2, this.defendingOpponent))) {
canBeBlocked = true;
@@ -1416,7 +1406,7 @@ public class AiAttackController {
return exerters;
}
/**
* Find a protection type that will make an attacker unblockable.
* @param sa ability belonging to ApiType.Protection
@@ -1485,10 +1475,10 @@ public class AiAttackController {
CardCollection attSorted = new CardCollection(attackersLeft);
CardCollection attUnsafe = new CardCollection();
CardLists.sortByToughnessDesc(attSorted);
int i = numForcedAttackers;
int refPowerValue = 0; // Aggro profiles do not account for the possible blockers' power, conservative profiles do.
if (!playAggro && this.blockers.size() > 0) {
// Conservative play: check to ensure that the card can't be killed off while damaged
// TODO: currently sorting a copy of this.blockers, but it looks safe to operate on this.blockers directly?
@@ -1498,7 +1488,7 @@ public class AiAttackController {
CardLists.sortByPowerDesc(blkSorted);
refPowerValue += blkSorted.get(0).getCurrentPower();
}
for (Card cre : attSorted) {
i++;
if (i + refPowerValue >= cre.getCurrentToughness()) {
@@ -1507,7 +1497,7 @@ public class AiAttackController {
continue;
}
}
attackersLeft.removeAll(attUnsafe);
}

View File

@@ -38,6 +38,7 @@ import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.keyword.Keyword;
import forge.game.player.Player;
import forge.game.staticability.StaticAbilityCantAttackBlock;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
@@ -183,9 +184,7 @@ public class AiBlockController {
List<Card> currentAttackers = new ArrayList<>(attackersLeft);
for (final Card attacker : attackersLeft) {
if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT")
|| attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")
|| attacker.hasKeyword(Keyword.MENACE)) {
if (StaticAbilityCantAttackBlock.getMinMaxBlocker(attacker, combat.getDefenderPlayerByAttacker(attacker)).getLeft() > 1) {
continue;
}
@@ -296,8 +295,7 @@ public class AiBlockController {
// 6. Blockers that don't survive until the next turn anyway
for (final Card attacker : attackersLeft) {
if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT") || attacker.hasKeyword(Keyword.MENACE)
|| attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) {
if (StaticAbilityCantAttackBlock.getMinMaxBlocker(attacker, combat.getDefenderPlayerByAttacker(attacker)).getLeft() > 1) {
continue;
}
@@ -323,7 +321,17 @@ public class AiBlockController {
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
/**
@@ -334,7 +342,7 @@ public class AiBlockController {
* @param combat a {@link forge.game.combat.Combat} object.
*/
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;
// 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
for (final Card attacker : attackersLeft) {
if (!attacker.hasKeyword(Keyword.MENACE) && !attacker.hasStartOfKeyword("CantBeBlockedByAmount LT2")) {
if (StaticAbilityCantAttackBlock.getMinMaxBlocker(attacker, combat.getDefenderPlayerByAttacker(attacker)).getLeft() <= 1) {
continue;
}
@@ -584,9 +592,7 @@ public class AiBlockController {
List<Card> killingBlockers;
for (final Card attacker : attackersLeft) {
if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT")
|| attacker.hasKeyword(Keyword.MENACE)
|| attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) {
if (StaticAbilityCantAttackBlock.getMinMaxBlocker(attacker, combat.getDefenderPlayerByAttacker(attacker)).getLeft() > 1) {
continue;
}
if (ComputerUtilCombat.attackerHasThreateningAfflict(attacker, ai)) {
@@ -635,10 +641,8 @@ public class AiBlockController {
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("CARDNAME can't be blocked unless all creatures defending player controls block it.")
|| attacker.hasKeyword(Keyword.MENACE)
|| ComputerUtilCombat.attackerHasThreateningAfflict(attacker, ai)) {
attackers.remove(0);
makeChumpBlocks(combat, attackers);
@@ -686,9 +690,7 @@ public class AiBlockController {
List<Card> currentAttackers = new ArrayList<>(attackersLeft);
for (final Card attacker : currentAttackers) {
if (!attacker.hasStartOfKeyword("CantBeBlockedByAmount LT")
&& !attacker.hasKeyword(Keyword.MENACE)
&& !attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) {
if (StaticAbilityCantAttackBlock.getMinMaxBlocker(attacker, combat.getDefenderPlayerByAttacker(attacker)).getLeft() <= 1) {
continue;
}
List<Card> possibleBlockers = getPossibleBlockers(combat, attacker, blockersLeft, true);
@@ -720,14 +722,14 @@ public class AiBlockController {
List<Card> chumpBlockers;
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:
// "Whenever CARDNAME becomes blocked, it gets +1/+1 until end of turn for each creature blocking it."
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("CARDNAME can't be blocked unless all creatures defending player controls block it.")) {
continue;
@@ -751,7 +753,7 @@ public class AiBlockController {
private void reinforceBlockersToKill(final Combat combat) {
List<Card> safeBlockers;
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
// 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
chumpBlockers = CardLists.getKeyword(blockersLeft, "CARDNAME blocks each turn if able.");
chumpBlockers.addAll(CardLists.getKeyword(blockersLeft, "CARDNAME blocks each combat if able."));
chumpBlockers = CardLists.getKeyword(blockersLeft, "CARDNAME blocks each combat if able.");
// if an attacker with lure attacks - all that can block
for (final Card blocker : blockersLeft) {
if (CombatUtil.mustBlockAnAttacker(blocker, combat, null)) {
@@ -1091,7 +1092,6 @@ public class AiBlockController {
for (final Card blocker : blockers) {
if (CombatUtil.canBlock(attacker, blocker, combat) && blockersLeft.contains(blocker)
&& (CombatUtil.mustBlockAnAttacker(blocker, combat, null)
|| blocker.hasKeyword("CARDNAME blocks each turn if able.")
|| blocker.hasKeyword("CARDNAME blocks each combat if able."))) {
combat.addBlocker(attacker, blocker);
if (blocker.getMustBlockCards() != null) {

View File

@@ -1654,10 +1654,11 @@ public class ComputerUtilCard {
Card pumped = CardFactory.copyCard(c, false);
pumped.setSickness(c.hasSickness());
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) {
if (kw.startsWith("HIDDEN")) {
pumped.addHiddenExtrinsicKeyword(kw);
hiddenKws.add(kw.substring(7));
} else {
kws.add(kw);
}
@@ -1686,7 +1687,13 @@ public class ComputerUtilCard {
pumped.addNewPT(c.getCurrentPower(), c.getCurrentToughness(), timestamp);
pumped.setPTBoost(c.getPTBoostTable());
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();
for(CounterType ct : types) {
pumped.addCounterFireNoEvents(ct, c.getCounters(ct), ai, sa, true, null);
@@ -1699,14 +1706,10 @@ public class ComputerUtilCard {
KeywordCollection copiedKeywords = new KeywordCollection();
copiedKeywords.insertAll(pumped.getKeywords());
List<KeywordInterface> toCopy = Lists.newArrayList();
for (KeywordInterface k : c.getKeywords()) {
for (KeywordInterface k : c.getUnhiddenKeywords()) {
KeywordInterface copiedKI = k.copy(c, true);
if (!copiedKeywords.contains(copiedKI.getOriginal())) {
if (copiedKI.getHidden()) {
pumped.addHiddenExtrinsicKeyword(copiedKI);
} else {
toCopy.add(copiedKI);
}
toCopy.add(copiedKI);
}
}
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")) {
continue;
}
if (!vCard.isValid(stAb.getParam("Affected").split(","), c.getController(), c, stAb)) {
if (!stAb.matchesValidParam("Affected", vCard)) {
continue;
}
int att = 0;

View File

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

View File

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

View File

@@ -74,6 +74,7 @@ import forge.game.replacement.ReplacementType;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityPredicates;
import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityCantAttackBlock;
import forge.game.staticability.StaticAbilityLayer;
import forge.game.trigger.TriggerType;
import forge.game.zone.PlayerZone;
@@ -682,17 +683,25 @@ public class GameAction {
// need to refresh ability text for affected cards
for (final StaticAbility stAb : c.getStaticAbilities()) {
if (stAb.isSecondary() ||
!stAb.getParam("Mode").equals("CantBlockBy") ||
stAb.isSuppressed() || !stAb.checkConditions() ||
!stAb.hasParam("ValidAttacker") ||
(stAb.hasParam("ValidBlocker") && stAb.getParam("ValidBlocker").equals("Creature.Self"))) {
if (stAb.isSuppressed() || !stAb.checkConditions()) {
continue;
}
final Card host = stAb.getHostCard();
for (Card creature : Iterables.filter(game.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.CREATURES)) {
if (creature.isValid(stAb.getParam("ValidAttacker").split(","), host.getController(), host, stAb)) {
creature.updateAbilityTextForView();
if (stAb.getParam("Mode").equals("CantBlockBy")) {
if (!stAb.hasParam("ValidAttacker") || (stAb.hasParam("ValidBlocker") && stAb.getParam("ValidBlocker").equals("Creature.Self"))) {
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 List<Player> affectedPlayers = getAffectedPlayers();
boolean setPT = false;
String[] addHiddenKeywords = null;
boolean removeMayPlay = false;
if (hasParam("SetPower") || hasParam("SetToughness")) {
setPT = true;
}
if (hasParam("AddHiddenKeyword")) {
addHiddenKeywords = getParam("AddHiddenKeyword").split(" & ");
}
if (hasParam("MayPlay")) {
removeMayPlay = true;
}
@@ -220,7 +210,7 @@ public class StaticEffect {
affectedCard.removeChangedTextColorWord(getTimestamp(), ability.getId());
// remove set P/T
if (setPT) {
if (hasParam("SetPower") || hasParam("SetToughness")) {
affectedCard.removeNewPT(getTimestamp());
}
@@ -240,10 +230,8 @@ public class StaticEffect {
affectedCard.removeCantHaveKeyword(getTimestamp());
}
if (addHiddenKeywords != null) {
for (final String k : addHiddenKeywords) {
affectedCard.removeHiddenExtrinsicKeyword(k);
}
if (hasParam("AddHiddenKeyword")) {
affectedCard.removeHiddenExtrinsicKeywords(timestamp, ability.getId());
}
// remove abilities

View File

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

View File

@@ -13,6 +13,7 @@ import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbilityCantAttackBlock;
import forge.util.Localizer;
public class CamouflageEffect extends SpellAbilityEffect {
@@ -36,7 +37,7 @@ public class CamouflageEffect extends SpellAbilityEffect {
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
Card chosen = declarer.getController().chooseCardsForEffect(blockers, sa,
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> hiddenKws = Lists.newArrayList();
if (null != keywords) {
for (final String kw : keywords) {
if (kw.startsWith("HIDDEN")) {
tgtC.addHiddenExtrinsicKeyword(kw);
hiddenKws.add(kw.substring(7));
} else {
kws.add(kw);
}
@@ -142,6 +143,9 @@ public class ControlGainEffect extends SpellAbilityEffect {
tgtC.addChangedCardKeywords(kws, Lists.newArrayList(), false, false, tStamp, 0);
game.fireEvent(new GameEventCardStatsChanged(tgtC));
}
if (hiddenKws.isEmpty()) {
tgtC.addHiddenExtrinsicKeywords(tStamp, 0, hiddenKws);
}
if (remember && !source.isRemembered(tgtC)) {
source.addRemembered(tgtC);
@@ -191,14 +195,8 @@ public class ControlGainEffect extends SpellAbilityEffect {
@Override
public void run() {
if (keywords.size() > 0) {
for (String kw : keywords) {
if (kw.startsWith("HIDDEN")) {
tgtC.removeHiddenExtrinsicKeyword(kw);
}
}
tgtC.removeChangedCardKeywords(tStamp, 0);
}
tgtC.removeHiddenExtrinsicKeywords(tStamp, 0);
tgtC.removeChangedCardKeywords(tStamp, 0);
}
};
game.getEndOfTurn().addUntil(untilKeywordEOT);

View File

@@ -32,6 +32,7 @@ import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.player.PlayerController;
import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbilityAdapt;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerHandler;
import forge.game.trigger.TriggerType;
@@ -288,8 +289,7 @@ public class CountersPutEffect extends SpellAbilityEffect {
// Adapt need extra logic
if (sa.hasParam("Adapt")) {
if (!(gameCard.getCounters(CounterEnumType.P1P1) == 0
|| gameCard.hasKeyword("CARDNAME adapts as though it had no +1/+1 counters"))) {
if (!(gameCard.getCounters(CounterEnumType.P1P1) == 0 || StaticAbilityAdapt.anyWithAdapt(sa, gameCard))) {
continue;
}
}
@@ -362,8 +362,6 @@ public class CountersPutEffect extends SpellAbilityEffect {
game.getTriggerHandler().runTrigger(TriggerType.BecomeRenowned, AbilityKey.mapFromCard(gameCard), false);
}
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);
}
} else {
@@ -422,7 +420,7 @@ public class CountersPutEffect extends SpellAbilityEffect {
List<String> keywords = Arrays.asList(sa.getParam("SharedKeywords").split(" & "));
List<ZoneType> zones = ZoneType.listValueOf(sa.getParam("SharedKeywordsZone"));
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) {
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.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordInterface;
import forge.game.spellability.SpellAbility;
@@ -72,17 +73,16 @@ public class DebuffEffect extends SpellAbilityEffect {
final List<String> removedKW = Lists.newArrayList();
if (tgtC.isInPlay() && (!sa.usesTargeting() || tgtC.canBeTargetedBy(sa))) {
if (sa.hasParam("AllSuffixKeywords")) {
String suffix = sa.getParam("AllSuffixKeywords");
for (final KeywordInterface kw : tgtC.getKeywords()) {
String keyword = kw.getOriginal();
if (keyword.endsWith(suffix)) {
kws.add(keyword);
// this only for walk abilities, may to try better
if (sa.getParam("AllSuffixKeywords").equals("walk")) {
for (final KeywordInterface kw : tgtC.getKeywords(Keyword.LANDWALK)) {
removedKW.add(kw.getOriginal());
}
}
}
// special for Protection:Card.<color>:Protection from <color>:*
for (final KeywordInterface inst : tgtC.getKeywords()) {
for (final KeywordInterface inst : tgtC.getUnhiddenKeywords()) {
String keyword = inst.getOriginal();
if (keyword.startsWith("Protection:")) {
for (final String kw : kws) {

View File

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

View File

@@ -20,7 +20,6 @@ import forge.game.card.CardCollection;
import forge.game.card.CardFactoryUtil;
import forge.game.card.CardUtil;
import forge.game.event.GameEventCardStatsChanged;
import forge.game.keyword.KeywordInterface;
import forge.game.player.Player;
import forge.game.player.PlayerCollection;
import forge.game.spellability.SpellAbility;
@@ -50,11 +49,12 @@ public class PumpEffect extends SpellAbilityEffect {
return;
}
final List<String> kws = Lists.newArrayList();
final List<String> hiddenKws = Lists.newArrayList();
boolean redrawPT = false;
for (String kw : keywords) {
if (kw.startsWith("HIDDEN")) {
gameCard.addHiddenExtrinsicKeyword(kw);
hiddenKws.add(kw.substring(7));
redrawPT |= kw.contains("CARDNAME's power and toughness are switched");
} else {
kws.add(kw);
@@ -66,7 +66,12 @@ public class PumpEffect extends SpellAbilityEffect {
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) {
gameCard.updatePowerToughnessForView();
}
@@ -95,12 +100,7 @@ public class PumpEffect extends SpellAbilityEffect {
updateText |= gameCard.removeCanBlockAdditional(timestamp);
if (keywords.size() > 0) {
for (String kw : keywords) {
if (kw.startsWith("HIDDEN")) {
gameCard.removeHiddenExtrinsicKeyword(kw);
}
}
gameCard.removeHiddenExtrinsicKeywords(timestamp, 0);
gameCard.removeChangedCardKeywords(timestamp, 0);
}
gameCard.updatePowerToughnessForView();
@@ -244,7 +244,7 @@ public class PumpEffect extends SpellAbilityEffect {
if (sa.hasParam("SharedKeywordsZone")) {
List<ZoneType> zones = ZoneType.listValueOf(sa.getParam("SharedKeywordsZone"));
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();
@@ -290,9 +290,10 @@ public class PumpEffect extends SpellAbilityEffect {
List<String> choice = Lists.newArrayList();
List<String> total = Lists.newArrayList(keywords);
if (sa.hasParam("NoRepetition")) {
for (KeywordInterface inst : tgtCards.get(0).getKeywords()) {
final String kws = inst.getOriginal();
total.remove(kws);
for (String kw : keywords) {
if (tgtCards.get(0).hasKeyword(kw)) {
total.remove(kw);
}
}
}
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.spellability.*;
import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityCantAttackBlock;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
import forge.game.zone.Zone;
@@ -99,7 +100,8 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
private CardDamageHistory damageHistory = new CardDamageHistory();
// 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
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
// there rules-wise)
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
@@ -2120,9 +2152,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
|| keyword.startsWith("Encore") || keyword.startsWith("Mutate") || keyword.startsWith("Dungeon")
|| keyword.startsWith("Class") || keyword.startsWith("Saga")) {
// 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")) {
sbLong.append(getName()).append(" can't be blocked.\r\n");
} else if (keyword.equals("AllNonLegendaryCreatureNames")) {
@@ -2173,14 +2202,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
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
public String getAbilityText() {
return getAbilityText(currentState);
@@ -2367,15 +2388,27 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
continue;
}
for (final StaticAbility stAb : ca.getStaticAbilities()) {
if (stAb.isSecondary() ||
!stAb.getParam("Mode").equals("CantBlockBy") ||
stAb.isSuppressed() || !stAb.checkConditions() ||
!stAb.hasParam("ValidAttacker") ||
(stAb.hasParam("ValidBlocker") && stAb.getParam("ValidBlocker").equals("Creature.Self"))) {
if (stAb.isSuppressed() || !stAb.checkConditions()) {
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 desc1 = TextUtil.fastReplace(stAb.toString(), "CARDNAME", currentName);
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().
public final void visitKeywords(CardState state, Visitor<KeywordInterface> visitor) {
visitUnhiddenKeywords(state, visitor);
visitHiddenExtrinsicKeywords(visitor);
}
@Override
@@ -4140,8 +4172,10 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
}
// shortcut for hidden keywords
if (this.hiddenExtrinsicKeyword.contains(keyword)) {
return true;
for (List<String> kw : this.hiddenExtrinsicKeywords.values()) {
if (kw.contains(keyword)) {
return true;
}
}
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
public final List<KeywordInterface> getHiddenExtrinsicKeywords() {
ListKeywordVisitor visitor = new ListKeywordVisitor();
visitHiddenExtrinsicKeywords(visitor);
return visitor.getKeywords();
}
private void visitHiddenExtrinsicKeywords(Visitor<KeywordInterface> visitor) {
for (KeywordInterface inst : hiddenExtrinsicKeyword.getValues()) {
if (!visitor.visit(inst)) {
return;
}
}
public final Iterable<String> getHiddenExtrinsicKeywords() {
return Iterables.concat(this.hiddenExtrinsicKeywords.values());
}
public final void addHiddenExtrinsicKeyword(String s) {
if (s.startsWith("HIDDEN")) {
s = s.substring(7);
}
if (hiddenExtrinsicKeyword.add(s) != null) {
view.updateNonAbilityText(this);
updateKeywords();
}
public final void addHiddenExtrinsicKeywords(long timestamp, long staticId, Iterable<String> keywords) {
// TODO if some keywords aren't removed anymore, then no need for extra Array List
hiddenExtrinsicKeywords.put(timestamp, staticId, Lists.newArrayList(keywords));
view.updateNonAbilityText(this);
updateKeywords();
}
public final void addHiddenExtrinsicKeyword(KeywordInterface k) {
if (hiddenExtrinsicKeyword.insert(k)) {
public final void removeHiddenExtrinsicKeywords(long timestamp, long staticId) {
if (hiddenExtrinsicKeywords.remove(timestamp, staticId) != null) {
view.updateNonAbilityText(this);
updateKeywords();
}
}
public final void removeHiddenExtrinsicKeyword(String s) {
if (s.startsWith("HIDDEN")) {
s = s.substring(7);
boolean updated = false;
for (List<String> list : hiddenExtrinsicKeywords.values()) {
if (list.remove(s)) {
updated = true;
}
}
if (hiddenExtrinsicKeyword.remove(s)) {
if (updated) {
view.updateNonAbilityText(this);
updateKeywords();
}
@@ -4710,6 +4736,12 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
return hasStartOfKeyword(keyword, currentState);
}
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);
visitKeywords(state, visitor);
return visitor.getResult();
@@ -4741,9 +4773,10 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
return getAmountOfKeyword(k, currentState);
}
public final int getAmountOfKeyword(final String k, CardState state) {
int count = Iterables.frequency(this.getHiddenExtrinsicKeywords(), k);
CountKeywordVisitor visitor = new CountKeywordVisitor(k);
visitKeywords(state, visitor);
return visitor.getCount();
return count + visitor.getCount();
}
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");
for (final KeywordInterface inst : getKeywords()) {
for (final KeywordInterface inst : getKeywords(Keyword.PROTECTION)) {
String kw = inst.getOriginal();
if (!kw.startsWith("Protection")) {
continue;
}
if (kw.equals("Protection from white")) {
if (source.isWhite() && !colorlessDamage) {
return true;
@@ -5698,11 +5728,8 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
public String getProtectionKey() {
String protectKey = "";
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();
if (!kw.startsWith("Protection")) {
continue;
}
if (kw.contains("Protection from red")) {
if (!pR) {
pR = true;
@@ -5755,11 +5782,8 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
public String getHexproofKey() {
String hexproofKey = "";
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();
if (!kw.startsWith("Hexproof")) {
continue;
}
if (kw.equals("Hexproof")) {
hexproofKey += "generic:";
}
@@ -5859,6 +5883,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
final Card source = sa.getHostCard();
if (sa.isSpell()) {
// TODO replace with Static Ability
for(KeywordInterface inst : source.getKeywords()) {
String kw = inst.getOriginal();
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 String keyword;
private int count;
private boolean startOf;
private CountKeywordVisitor(String keyword) {
this.keyword = keyword;
this.count = 0;
this.startOf = false;
}
private CountKeywordVisitor(String keyword, boolean startOf) {
this(keyword);
this.startOf = startOf;
}
@Override
public boolean visit(KeywordInterface inst) {
final String kw = inst.getOriginal();
if ((startOf && kw.startsWith(keyword)) || kw.equals(keyword)) {
if (kw.equals(keyword)) {
count++;
}
return true;
@@ -6551,7 +6569,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
}
return result.isFalse();
}
public boolean getResult() {
return result.isTrue();
}

View File

@@ -42,6 +42,7 @@ import forge.card.ICardFace;
import forge.card.MagicColor;
import forge.card.mana.ManaCost;
import forge.card.mana.ManaCostParser;
import forge.game.CardTraitBase;
import forge.game.Game;
import forge.game.GameEntityCounterTable;
import forge.game.GameLogEntryType;
@@ -264,8 +265,7 @@ public class CardFactoryUtil {
return false;
}
for (KeywordInterface k : c.getKeywords()) {
final String o = k.getOriginal();
for (String o : c.getHiddenExtrinsicKeywords()) {
if (o.startsWith("CantBeCounteredBy")) {
final String[] m = o.split(":");
if (sa.isValid(m[1].split(","), c.getController(), c, null)) {
@@ -510,7 +510,7 @@ public class CardFactoryUtil {
* @return a List<String>.
*/
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 Player p = host.getController();
CardCollectionView cardlist = p.getGame().getCardsIn(zones);
@@ -521,7 +521,7 @@ public class CardFactoryUtil {
final Set<String> tramplekw = 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()) {
final String k = inst.getOriginal();
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.Predicate;
import com.google.common.collect.Iterables;
import forge.card.CardStateName;
import forge.game.CardTraitBase;
@@ -31,6 +32,7 @@ import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.zone.Zone;
import forge.game.zone.ZoneType;
import forge.util.PredicateString;
import forge.util.collect.FCollectionView;
@@ -109,6 +111,10 @@ public final class CardPredicates {
return new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
if (Iterables.any(c.getHiddenExtrinsicKeywords(), PredicateString.contains(keyword))) {
return true;
}
for (KeywordInterface k : c.getKeywords()) {
if (k.getOriginal().contains(keyword)) {
return true;

View File

@@ -52,18 +52,27 @@ public class AttackRequirement {
nAttackAnything += attacker.getGoaded().size();
}
// remove it when all of them are HIDDEN or static
for (final KeywordInterface inst : attacker.getKeywords()) {
final String keyword = inst.getOriginal();
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.") ||
(keyword.equals("CARDNAME attacks each turn if able.")
&& !attacker.getDamageHistory().getCreatureAttackedThisTurn())) {
} else if (keyword.equals("CARDNAME attacks each combat if able.")) {
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();
if (mustAttack3 != null) {
defenderSpecific.add(mustAttack3);
@@ -149,7 +158,7 @@ public class AttackRequirement {
int violations = 0;
// 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()) {
for (GameEntity def : defenderOrPWSpecific.keySet()) {
if (defenderSpecificAlternatives.containsKey(def)) {

View File

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

View File

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

View File

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

View File

@@ -23,8 +23,6 @@ public abstract class KeywordInstance<T extends KeywordInstance<?>> implements K
private Keyword keyword;
private String original;
private boolean hidden;
private List<Trigger> triggers = Lists.newArrayList();
private List<ReplacementEffect> replacements = Lists.newArrayList();
private List<SpellAbility> abilities = Lists.newArrayList();
@@ -205,21 +203,6 @@ public abstract class KeywordInstance<T extends KeywordInstance<?>> implements K
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)
* @see forge.game.keyword.KeywordInterface#getTriggers()

View File

@@ -18,10 +18,7 @@ public interface KeywordInterface extends Cloneable {
String getReminderText();
int getAmount();
boolean getHidden();
void setHidden(boolean val);
void createTraits(final Card host, final boolean intrinsic);
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
// TODO Replace with Static Abilities
for (final Card c : player.getCardsIn(ZoneType.Battlefield)) {
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.")) {
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);
for (final Card c : getCardsIn(ZoneType.Sideboard)) {
for (KeywordInterface inst : c.getKeywords()) {
for (KeywordInterface inst : c.getKeywords(Keyword.COMPANION)) {
if (!(inst instanceof Companion)) {
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;
import org.apache.commons.lang3.tuple.MutablePair;
import org.apache.commons.lang3.tuple.Pair;
import com.google.common.collect.Iterables;
import forge.game.Game;
import forge.game.GameEntity;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardCollectionView;
import forge.game.card.CardPredicates;
import forge.game.cost.Cost;
import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordInterface;
import forge.game.player.Player;
import forge.game.zone.ZoneType;
@@ -34,6 +39,8 @@ import forge.game.zone.ZoneType;
*/
public class StaticAbilityCantAttackBlock {
public static String MinMaxBlockerMode = "MinMaxBlocker";
/**
* TODO Write javadoc for this method.
*
@@ -221,4 +228,43 @@ public class StaticAbilityCantAttackBlock {
}
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")) {
List<ZoneType> zones = ZoneType.listValueOf(params.get("SharedKeywordsZone"));
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
if (!addHiddenKeywords.isEmpty()) {
for (final String k : addHiddenKeywords) {
affectedCard.addHiddenExtrinsicKeyword(k);
}
affectedCard.addHiddenExtrinsicKeywords(hostCard.getTimestamp(), stAb.getId(), addHiddenKeywords);
}
// add SVars

View File

@@ -3,6 +3,6 @@ ManaCost:1 G
Types:Enchantment Aura
K:Enchant creature
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.
SVar:Picture:http://www.wizards.com/global/images/magic/general/alpha_authority.jpg
S:Mode$ Continuous | Affected$ Creature.EnchantedBy | AddKeyword$ Hexproof | Description$ Enchanted creature has hexproof and can't be blocked by more than one creature.
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.

View File

@@ -2,10 +2,9 @@ Name:Battlefront Krushok
ManaCost:4 G
Types:Creature Beast
PT:3/4
K:CantBeBlockedByAmount GT1
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$ Card.Self | Max$ 1 | Description$ CARDNAME 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:PlayMain1:TRUE
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.

View File

@@ -3,6 +3,9 @@ ManaCost:G U
Types:Creature Mutant
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.
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
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
ManaCost:3 G
Types:Creature Boar
K:CantBeBlockedByAmount GT1
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.
PT:4/3

View File

@@ -2,7 +2,7 @@ Name:Challenger Troll
ManaCost:4 G
Types:Creature Troll
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
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
Types:Creature Rhino
PT:4/4
K:CantBeBlockedByAmount GT1
SVar:Picture:http://www.wizards.com/global/images/magic/general/charging_rhino.jpg
S:Mode$ MinMaxBlocker | ValidCard$ Card.Self | Max$ 1 | Description$ CARDNAME 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
ManaCost:2 G
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: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.

View File

@@ -4,6 +4,5 @@ Types:Creature Ape Berserker
PT:2/3
K:Trample
K:Rampage:2
K:CantBeBlockedByAmount LT3
SVar:Picture:http://www.wizards.com/global/images/magic/general/gorilla_berserkers.jpg
S:Mode$ MinMaxBlocker | ValidCard$ Creature.Self | Min$ 3 | Description$ CARDNAME 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
Types:Creature Elemental Incarnation
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.
SVar:DBRemove:DB$ ChangeZone | Defined$ ReplacedCard | Origin$ Stack | Destination$ Exile | Fizzle$ True | SubAbility$ DBPlay
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.
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.

View File

@@ -2,6 +2,5 @@ Name:Huang Zhong, Shu General
ManaCost:2 W W
Types:Legendary Creature Human Soldier
PT:2/3
K:CantBeBlockedByAmount GT1
SVar:Picture:http://www.wizards.com/global/images/magic/general/huang_zhong_shu_general.jpg
S:Mode$ MinMaxBlocker | ValidCard$ Card.Self | Max$ 1 | Description$ CARDNAME 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
Types:Creature Hydra
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
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.

View File

@@ -2,6 +2,5 @@ Name:Ironhoof Ox
ManaCost:3 G G
Types:Creature Ox
PT:4/4
K:CantBeBlockedByAmount GT1
SVar:Picture:http://resources.wizards.com/magic/cards/p2/en-us/card6628.jpg
S:Mode$ MinMaxBlocker | ValidCard$ Card.Self | Max$ 1 | Description$ CARDNAME 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
ManaCost:G
Types:Creature Werewolf Horror
PT:2/1
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
Oracle:{4}{G}: Transform Kessig Prowler.
ALTERNATE
Name:Sinuous Predator
ManaCost:no cost
Types:Creature Eldrazi Werewolf
PT:4/4
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.
S:Mode$ MinMaxBlocker | ValidCard$ Card.Self | Max$ 1 | Description$ CARDNAME 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
PT:3/2
K:Provoke
K:CantBeBlockedByAmount GT1
SVar:Picture:http://www.wizards.com/global/images/magic/general/krosan_vorine.jpg
S:Mode$ MinMaxBlocker | ValidCard$ Card.Self | Max$ 1 | Description$ CARDNAME 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
Types:Creature Elf
PT:3/3
K:CantBeBlockedByAmount GT1
SVar:Picture:http://resources.wizards.com/magic/cards/p2/en-us/card6615.jpg
S:Mode$ MinMaxBlocker | ValidCard$ Card.Self | Max$ 1 | Description$ CARDNAME 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
PT:6/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
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.

View File

@@ -3,6 +3,5 @@ ManaCost:11
Types:Creature Eldrazi
PT:9/9
K:Annihilator:3
K:CantBeBlockedByAmount LT3
SVar:Picture:http://www.wizards.com/global/images/magic/general/pathrazer_of_ulamog.jpg
S:Mode$ MinMaxBlocker | ValidCard$ Creature.Self | Min$ 3 | Description$ CARDNAME 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
K:CARDNAME doesn't untap during your untap step.
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
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.

View File

@@ -3,6 +3,6 @@ ManaCost:1 B/R B/R
Types:Creature Human Warrior
PT:2/2
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
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
Types:Creature Cat
PT:3/3
K:CantBeBlockedByAmount GT1
SVar:Picture:http://www.wizards.com/global/images/magic/general/stalking_tiger.jpg
S:Mode$ MinMaxBlocker | ValidCard$ Card.Self | Max$ 1 | Description$ CARDNAME 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
Types:Creature Giant Shaman
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.
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.

View File

@@ -2,7 +2,7 @@ Name:Tahngarth, First Mate
ManaCost:2 R G
Types:Legendary Creature Minotaur Warrior
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.
SVar:TrigGainControl:DB$ GainControl | Defined$ Self | NewController$ TriggeredAttackingPlayer | LoseControl$ EndOfCombat | Attacking$ Player.Defending | Chooser$ You | ChoosePlayerOrPlaneswalker$ True
AI:RemoveDeck:All

View File

@@ -3,7 +3,9 @@ ManaCost:3 R
Types:Enchantment Saga
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: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: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.

View File

@@ -2,7 +2,7 @@ Name:Underworld Cerberus
ManaCost:3 B R
Types:Creature Dog
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.
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

View File

@@ -2,7 +2,7 @@ Name:Vigorspore Wurm
ManaCost:5 G
Types:Creature Wurm
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.
SVar:TrigPump:DB$ Pump | ValidTgts$ Creature | TgtPrompt$ Select target creature | NumAtt$ X | NumDef$ X | KW$ Vigilance
SVar:X:Count$TypeInYourYard.Creature

View File

@@ -2,6 +2,6 @@ Name:Vorrac Battlehorns
ManaCost:2
Types:Artifact Equipment
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.
SVar:Picture:http://www.wizards.com/global/images/magic/general/vorrac_battlehorns.jpg
S:Mode$ Continuous | Affected$ Creature.EquippedBy | AddKeyword$ Trample | Description$ Equipped creature has trample 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.
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:DBCleanup:DB$ Cleanup | ClearRemembered$ True
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
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
PT:2/3
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.
SVar:Picture:http://www.wizards.com/global/images/magic/general/yuan_shao_the_indecisive.jpg
S:Mode$ MinMaxBlocker | ValidCard$ Creature.YouCtrl | Max$ 1 | Description$ Each 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.
Banding
CantBeBlockedByAmount LT2
CantBeBlockedByAmount LT3
CARDNAME's activated abilities can't be activated.
CARDNAME attacks each turn if able.
CARDNAME attacks each combat if able.
CARDNAME blocks each turn if able.
CARDNAME blocks each combat if able.
CARDNAME can attack as though it didn't have defender.
CARDNAME can block any number of creatures.