From 01bf10c719fb90400dede469e0fa863d6d1ddf06 Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Mon, 3 Feb 2020 11:35:28 +0000 Subject: [PATCH] refactor Keywords using Chosen Values --- .../main/java/forge/game/StaticEffect.java | 4 + .../ability/effects/AnimateEffectBase.java | 8 +- .../src/main/java/forge/game/card/Card.java | 144 +++++---- .../java/forge/game/card/CardFactoryUtil.java | 287 +++++++++--------- .../main/java/forge/game/card/CardUtil.java | 8 + .../main/java/forge/game/card/CardView.java | 28 ++ .../main/java/forge/game/keyword/Keyword.java | 11 + .../forge/game/keyword/KeywordCollection.java | 17 +- .../forge/game/player/PlayerProperty.java | 4 + .../game/staticability/StaticAbility.java | 7 +- .../StaticAbilityContinuous.java | 174 ++++++++--- .../forge/trackable/TrackableProperty.java | 2 + .../res/cardsfolder/a/arcane_lighthouse.txt | 2 +- .../cardsfolder/a/archetype_of_aggression.txt | 2 +- .../cardsfolder/a/archetype_of_courage.txt | 2 +- .../cardsfolder/a/archetype_of_endurance.txt | 2 +- .../cardsfolder/a/archetype_of_finality.txt | 2 +- .../a/archetype_of_imagination.txt | 2 +- .../res/cardsfolder/e/earnest_fellowship.txt | 7 +- .../cardsfolder/e/empty_shrine_kannushi.txt | 11 +- .../cardsfolder/e/escaped_shapeshifter.txt | 18 +- .../cardsfolder/h/haktos_the_unscarred.txt | 4 +- .../res/cardsfolder/p/pledge_of_loyalty.txt | 11 +- .../res/cardsfolder/t/true_name_nemesis.txt | 2 +- 24 files changed, 440 insertions(+), 319 deletions(-) diff --git a/forge-game/src/main/java/forge/game/StaticEffect.java b/forge-game/src/main/java/forge/game/StaticEffect.java index 98838b84ff9..dc9d51094a0 100644 --- a/forge-game/src/main/java/forge/game/StaticEffect.java +++ b/forge-game/src/main/java/forge/game/StaticEffect.java @@ -276,6 +276,10 @@ public class StaticEffect { affectedCard.removeChangedCardKeywords(getTimestamp()); } + if (hasParam("CantHaveKeyword")) { + affectedCard.removeCantHaveKeyword(getTimestamp()); + } + if (addHiddenKeywords != null) { for (final String k : addHiddenKeywords) { affectedCard.removeHiddenExtrinsicKeyword(k); diff --git a/forge-game/src/main/java/forge/game/ability/effects/AnimateEffectBase.java b/forge-game/src/main/java/forge/game/ability/effects/AnimateEffectBase.java index 7e3311e1f40..0762a16835a 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/AnimateEffectBase.java +++ b/forge-game/src/main/java/forge/game/ability/effects/AnimateEffectBase.java @@ -20,7 +20,7 @@ package forge.game.ability.effects; import forge.card.CardType; import forge.game.ability.SpellAbilityEffect; import forge.game.card.Card; - +import forge.game.keyword.Keyword; import forge.game.spellability.SpellAbility; import java.util.List; @@ -101,6 +101,10 @@ public abstract class AnimateEffectBase extends SpellAbilityEffect { c.addChangedCardKeywords(keywords, removeKeywords, sa.hasParam("RemoveAllAbilities"), sa.hasParam("RemoveIntrinsicAbilities"), timestamp); + if (sa.hasParam("CantHaveKeyword")) { + c.addCantHaveKeyword(timestamp, Keyword.setValueOf(sa.getParam("CantHaveKeyword"))); + } + for (final String k : hiddenKeywords) { c.addHiddenExtrinsicKeyword(k); } @@ -138,6 +142,8 @@ public abstract class AnimateEffectBase extends SpellAbilityEffect { c.removeChangedCardTraits(timestamp); + c.removeCantHaveKeyword(timestamp); + for (final String k : hiddenKeywords) { c.removeHiddenExtrinsicKeyword(k); } diff --git a/forge-game/src/main/java/forge/game/card/Card.java b/forge-game/src/main/java/forge/game/card/Card.java index 5c8a9eca9dc..bf91cfabb11 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -127,6 +127,8 @@ public class Card extends GameEntity implements Comparable { private final NavigableMap clonedStates = Maps.newTreeMap(); private final NavigableMap textChangeStates = Maps.newTreeMap(); + private final Multimap cantHaveKeywords = MultimapBuilder.hashKeys().enumSetValues(Keyword.class).build(); + private final Map canBlockAdditional = Maps.newTreeMap(); private final Set canBlockAny = Sets.newHashSet(); @@ -232,8 +234,8 @@ public class Card extends GameEntity implements Comparable { private String originalText = "", text = ""; private String chosenType = ""; private List chosenColors; - private String namedCard = ""; - private int chosenNumber; + private String chosenName = ""; + private Integer chosenNumber; private Player chosenPlayer; private Direction chosenDirection = null; private String chosenMode = ""; @@ -1445,6 +1447,9 @@ public class Card extends GameEntity implements Comparable { return currentState.getManaCost(); } + public final boolean hasChosenPlayer() { + return chosenPlayer != null; + } public final Player getChosenPlayer() { return chosenPlayer; } @@ -1454,7 +1459,11 @@ public class Card extends GameEntity implements Comparable { view.updateChosenPlayer(this); } - public final int getChosenNumber() { + public final boolean hasChosenNumber() { + return chosenNumber != null; + } + + public final Integer getChosenNumber() { return chosenNumber; } public final void setChosenNumber(final int i) { @@ -1472,6 +1481,7 @@ public class Card extends GameEntity implements Comparable { public final String getChosenType() { return chosenType; } + public final void setChosenType(final String s) { chosenType = s; view.updateChosenType(this); @@ -1537,13 +1547,24 @@ public class Card extends GameEntity implements Comparable { view.updateChosenMode(this); } + public boolean hasChosenName() { + return chosenName != null; + } + + public String getChosenName() { + return chosenName; + } + public final void setChosenName(final String s) { + chosenName = s; + view.updateNamedCard(this); + } + // used for cards like Meddling Mage... public final String getNamedCard() { - return namedCard; + return getChosenName(); } public final void setNamedCard(final String s) { - namedCard = s; - view.updateNamedCard(this); + setChosenName(s); } public final boolean getDrawnThisTurn() { @@ -3677,7 +3698,6 @@ public class Card extends GameEntity implements Comparable { public final void addChangedCardKeywords(final List keywords, final List removeKeywords, final boolean removeAllKeywords, final boolean removeIntrinsicKeywords, final long timestamp, final boolean updateView) { - keywords.removeAll(getCantHaveOrGainKeyword()); // if the key already exists - merge entries final KeywordsChange cks = changedCardKeywords.get(timestamp); if (cks != null) { @@ -3706,7 +3726,6 @@ public class Card extends GameEntity implements Comparable { final long timestamp, final boolean updateView) { KeywordCollection list = new KeywordCollection(); list.insertAll(keywords); - list.removeAll(getCantHaveOrGainKeyword()); // if the key already exists - merge entries final KeywordsChange cks = changedCardKeywords.get(timestamp); if (cks != null) { @@ -3727,22 +3746,6 @@ public class Card extends GameEntity implements Comparable { } } - public final void addChangedCardKeywords(final String[] keywords, final String[] removeKeywords, - final boolean removeAllKeywords, final boolean removeIntrinsicKeywords, final long timestamp) { - List keywordsList = Lists.newArrayList(); - List removeKeywordsList = Lists.newArrayList(); - if (keywords != null) { - keywordsList = Lists.newArrayList(Arrays.asList(keywords)); - } - - if (removeKeywords != null) { - removeKeywordsList = Lists.newArrayList(Arrays.asList(removeKeywords)); - } - - addChangedCardKeywords(keywordsList, removeKeywordsList, - removeAllKeywords, removeIntrinsicKeywords, timestamp); - } - public final KeywordsChange removeChangedCardKeywords(final long timestamp) { return removeChangedCardKeywords(timestamp, true); } @@ -3817,21 +3820,17 @@ public class Card extends GameEntity implements Comparable { } } + // remove Can't have keywords + for (Keyword k : getCantHaveKeyword()) { + keywords.removeAll(k); + } + state.setCachedKeywords(keywords); } private void visitUnhiddenKeywords(CardState state, Visitor visitor) { - if (changedCardKeywords.isEmpty()) { - // Fast path that doesn't involve temp allocations. - for (KeywordInterface kw : state.getIntrinsicKeywords()) { - if (!visitor.visit(kw)) { - return; - } - } - } else { - for (KeywordInterface kw : getUnhiddenKeywords(state)) { - if (!visitor.visit(kw)) { - return; - } + for (KeywordInterface kw : getUnhiddenKeywords(state)) { + if (!visitor.visit(kw)) { + return; } } } @@ -4035,14 +4034,32 @@ public class Card extends GameEntity implements Comparable { } } - public final List getCantHaveOrGainKeyword() { - final List cantGain = Lists.newArrayList(); - for (String s : hiddenExtrinsicKeyword) { - if (s.contains("can't have or gain")) { - cantGain.add(s.split("can't have or gain ")[1]); - } + public void addCantHaveKeyword(Keyword keyword, Long timestamp) { + cantHaveKeywords.put(timestamp, keyword); + getView().updateCantHaveKeyword(this); + } + + public void addCantHaveKeyword(Long timestamp, Iterable keywords) { + cantHaveKeywords.putAll(timestamp, keywords); + getView().updateCantHaveKeyword(this); + } + + public boolean removeCantHaveKeyword(Long timestamp) { + return removeCantHaveKeyword(timestamp, true); + } + public boolean removeCantHaveKeyword(Long timestamp, boolean updateView) { + boolean change = !cantHaveKeywords.removeAll(timestamp).isEmpty(); + if (change && updateView) { + getView().updateCantHaveKeyword(this); + updateKeywords(); + if (isToken()) + game.fireEvent(new GameEventTokenStateUpdate(this)); } - return cantGain; + return change; + } + + public Collection getCantHaveKeyword() { + return cantHaveKeywords.values(); } public final void setStaticAbilities(final List a) { @@ -5403,34 +5420,33 @@ public class Card extends GameEntity implements Comparable { final String[] kws = kw.split(":"); String characteristic = kws[1]; - // if colorlessDamage then it does only check damage color.. - if (colorlessDamage) { - if (characteristic.endsWith("White") || characteristic.endsWith("Blue") - || characteristic.endsWith("Black") || characteristic.endsWith("Red") - || characteristic.endsWith("Green") || characteristic.endsWith("Colorless") - || characteristic.endsWith("ChosenColor")) { - characteristic += "Source"; + if (characteristic.startsWith("Player")) { + // TODO need to handle that better in CardProperty + if (source.getController().isValid(characteristic.split(","), getController(), this, null)) { + return true; + } + } else { + // if colorlessDamage then it does only check damage color.. + if (colorlessDamage) { + if (characteristic.endsWith("White") || characteristic.endsWith("Blue") + || characteristic.endsWith("Black") || characteristic.endsWith("Red") + || characteristic.endsWith("Green") || characteristic.endsWith("Colorless") + || characteristic.endsWith("ChosenColor")) { + characteristic += "Source"; + } } - } - final String[] characteristics = characteristic.split(","); - final String exception = kws.length > 3 ? kws[3] : null; // check "This effect cannot remove sth" - if (source.isValid(characteristics, getController(), this, null) - && (!checkSBA || exception == null || !source.isValid(exception, getController(), this, null))) { - return true; + final String[] characteristics = characteristic.split(","); + final String exception = kws.length > 3 ? kws[3] : null; // check "This effect cannot remove sth" + if (source.isValid(characteristics, getController(), this, null) + && (!checkSBA || exception == null || !source.isValid(exception, getController(), this, null))) { + return true; + } } } else if (kw.equals("Protection from colored spells")) { if (source.isSpell() && !source.isColorless()) { return true; } - } else if (kw.equals("Protection from the chosen player")) { - if (source.getController().equals(chosenPlayer)) { - return true; - } - } else if (kw.equals("Protection from each converted mana cost other than the chosen number")) { - if (source.getCMC() != chosenNumber) { - return true; - } } else if (kw.startsWith("Protection from opponent of ")) { final String playerName = kw.substring("Protection from opponent of ".length()); if (source.getController().isOpponentOf(playerName)) { diff --git a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java index 9f34a544f75..e60f3e16bd1 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java +++ b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java @@ -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 . */ @@ -72,7 +72,7 @@ import io.sentry.event.BreadcrumbBuilder; *

* CardFactoryUtil class. *

- * + * * @author Forge * @version $Id$ */ @@ -92,7 +92,7 @@ public class CardFactoryUtil { *

* abilityMorphDown. *

- * + * * @param sourceCard * a {@link forge.game.card.Card} object. * @return a {@link forge.game.spellability.SpellAbility} object. @@ -134,7 +134,7 @@ public class CardFactoryUtil { *

* abilityMorphUp. *

- * + * * @param sourceCard * a {@link forge.game.card.Card} object. * @return a {@link forge.game.spellability.AbilityActivated} object. @@ -223,7 +223,7 @@ public class CardFactoryUtil { *

* isTargetStillValid. *

- * + * * @param ability * a {@link forge.game.spellability.SpellAbility} object. * @param target @@ -268,7 +268,7 @@ public class CardFactoryUtil { *

* hasProtectionFrom. *

- * + * * @param card * a {@link forge.game.card.Card} object. * @param target @@ -287,7 +287,7 @@ public class CardFactoryUtil { *

* isCounterable. *

- * + * * @param c * a {@link forge.game.card.Card} object. * @return a boolean. @@ -300,7 +300,7 @@ public class CardFactoryUtil { *

* isCounterableBy. *

- * + * * @param c * a {@link forge.game.card.Card} object. * @param sa @@ -328,7 +328,7 @@ public class CardFactoryUtil { *

* countOccurrences. *

- * + * * @param arg1 * a {@link java.lang.String} object. * @param arg2 @@ -349,7 +349,7 @@ public class CardFactoryUtil { *

* parseMath. *

- * + * * @param expression * a {@link java.lang.String} object. * @return an array of {@link java.lang.String} objects. @@ -363,7 +363,7 @@ public class CardFactoryUtil { *

* Parse player targeted X variables. *

- * + * * @param objects * a {@link java.util.ArrayList} object. * @param s @@ -395,7 +395,7 @@ public class CardFactoryUtil { *

* Parse player targeted X variables. *

- * + * * @param players * a {@link java.util.ArrayList} object. * @param s @@ -518,9 +518,9 @@ public class CardFactoryUtil { public static int playerXProperty(final Player player, final String s, final Card source) { final String[] l = s.split("/"); final String m = extractOperators(s); - + final Game game = player.getGame(); - + // count valid cards in any specified zone/s if (l[0].startsWith("Valid") && !l[0].contains("Valid ")) { String[] lparts = l[0].split(" ", 2); @@ -619,7 +619,7 @@ public class CardFactoryUtil { if (value.contains("CardsDrawn")) { return doXMath(player.getNumDrawnThisTurn(), m, source); } - + if (value.contains("CardsDiscardedThisTurn")) { return doXMath(player.getNumDiscardedThisTurn(), m, source); } @@ -643,7 +643,7 @@ public class CardFactoryUtil { if (value.equals("OpponentsAttackedThisTurn")) { return doXMath(player.getAttackedOpponentsThisTurn().size(), m, source); } - + return doXMath(0, m, source); } @@ -651,7 +651,7 @@ public class CardFactoryUtil { *

* Parse non-mana X variables. *

- * + * * @param c * a {@link forge.game.card.Card} object. * @param expression @@ -677,7 +677,8 @@ public class CardFactoryUtil { if (l[0].startsWith("Number$")) { final String number = l[0].substring(7); if (number.equals("ChosenNumber")) { - return doXMath(c.getChosenNumber(), m, c); + int x = c.getChosenNumber() == null ? 0 : c.getChosenNumber(); + return doXMath(x, m, c); } return doXMath(Integer.parseInt(number), m, c); } @@ -708,7 +709,7 @@ public class CardFactoryUtil { String[] lparts = l[0].split(" ", 2); final String[] rest = lparts[1].split(","); - final CardCollectionView cardsInZones = lparts[0].length() > 5 + final CardCollectionView cardsInZones = lparts[0].length() > 5 ? game.getCardsIn(ZoneType.listValueOf(lparts[0].substring(5))) : game.getCardsIn(ZoneType.Battlefield); @@ -764,7 +765,7 @@ public class CardFactoryUtil { String[] lparts = l[0].split(" ", 2); final String[] rest = lparts[1].split(","); - final CardCollectionView cardsInZones = lparts[0].length() > 12 + final CardCollectionView cardsInZones = lparts[0].length() > 12 ? game.getCardsIn(ZoneType.listValueOf(lparts[0].substring(12))) : game.getCardsIn(ZoneType.Battlefield); @@ -950,7 +951,7 @@ public class CardFactoryUtil { if (sq[0].equals("RegeneratedThisTurn")) { return doXMath(c.getRegeneratedThisTurn(), m, c); } - + // TriggeringObjects if (sq[0].startsWith("Triggered")) { return doXMath(xCount((Card) c.getTriggeringObject(AbilityKey.Card), sq[0].substring(9)), m, c); @@ -1045,7 +1046,7 @@ public class CardFactoryUtil { CardCollection filtered = CardLists.getValidCards(allSrc, restriction, cc, c); return doXMath(filtered.size(), m, c); } - + if (sq[0].contains("YourLandsPlayed")) { return doXMath(cc.getLandsPlayedThisTurn(), m, c); } @@ -1056,7 +1057,7 @@ public class CardFactoryUtil { cc.getCardsIn(ZoneType.Library).getFirst().getCMC(); return doXMath(cmc, m, c); } - + // Count$EnchantedControllerCreatures if (sq[0].contains("EnchantedControllerCreatures")) { if (c.getEnchantingCard() != null) { @@ -1080,7 +1081,7 @@ public class CardFactoryUtil { byte colorCode = ManaAtom.fromName(sq[1]); for (Card c0 : cards) { for (ManaCostShard sh : c0.getManaCost()){ - if ((sh.getColorMask() & colorCode) != 0) + if ((sh.getColorMask() & colorCode) != 0) colorOcurrencices++; } } @@ -1192,7 +1193,7 @@ public class CardFactoryUtil { // Count$wasCastFrom.. if (sq[0].startsWith("wasCastFrom")) { - boolean zonesMatch = c.getCastFrom() == ZoneType.smartValueOf(sq[0].substring(11)); + boolean zonesMatch = c.getCastFrom() == ZoneType.smartValueOf(sq[0].substring(11)); return doXMath(Integer.parseInt(sq[zonesMatch ? 1 : 2]), m, c); } @@ -1307,7 +1308,7 @@ public class CardFactoryUtil { // Count$ThisTurnEntered [from ] if (sq[0].contains("ThisTurnEntered")) { final String[] workingCopy = l[0].split("_"); - + ZoneType destination = ZoneType.smartValueOf(workingCopy[1]); final boolean hasFrom = workingCopy[2].equals("from"); ZoneType origin = hasFrom ? ZoneType.smartValueOf(workingCopy[3]) : null; @@ -1324,7 +1325,7 @@ public class CardFactoryUtil { // Count$LastTurnEntered [from ] if (sq[0].contains("LastTurnEntered")) { final String[] workingCopy = l[0].split("_"); - + ZoneType destination = ZoneType.smartValueOf(workingCopy[1]); final boolean hasFrom = workingCopy[2].equals("from"); ZoneType origin = hasFrom ? ZoneType.smartValueOf(workingCopy[3]) : null; @@ -1422,7 +1423,7 @@ public class CardFactoryUtil { // Sorry for the Singleton use, replace this once this function has game passed into it return doXMath(game.getPhaseHandler().getTurn(), m, c); } - + //Count$Random.. if (sq[0].equals("Random")) { int min = StringUtils.isNumeric(sq[1]) ? Integer.parseInt(sq[1]) : xCount(c, c.getSVar(sq[1])); @@ -1475,16 +1476,16 @@ public class CardFactoryUtil { } return doXMath(ColorSet.fromMask(mask).countColors(), m, c); } - + // Count$CardMulticolor.. if (sq[0].contains("CardMulticolor")) { - final boolean isMulti = CardUtil.getColors(c).isMulticolor(); + final boolean isMulti = CardUtil.getColors(c).isMulticolor(); return doXMath(Integer.parseInt(sq[isMulti ? 1 : 2]), m, c); } - + // Complex counting methods CardCollectionView someCards = getCardListForXCount(c, cc, sq); - + // 1/10 - Count$MaxCMCYouCtrl if (sq[0].contains("MaxCMC")) { int mmc = Aggregates.max(someCards, CardPredicates.Accessors.fnGetCmc); @@ -1498,7 +1499,7 @@ public class CardFactoryUtil { final List opps = cc.getOpponents(); CardCollection someCards = new CardCollection(); final Game game = c.getGame(); - + // Generic Zone-based counting // Count$QualityAndZones.Subquality @@ -1621,7 +1622,7 @@ public class CardFactoryUtil { someCards.addAll(controller.getCardsIn(ZoneType.Graveyard)); } } - + // filter lists based on the specified quality // "Clerics you control" - Count$TypeYouCtrl.Cleric @@ -1747,7 +1748,7 @@ public class CardFactoryUtil { *

* handlePaid. *

- * + * * @param paidList * a {@link forge.game.card.CardCollectionView} object. * @param string @@ -1792,7 +1793,7 @@ public class CardFactoryUtil { } if (string.startsWith("Valid")) { - + final String[] splitString = string.split("/", 2); String valid = splitString[0].substring(6); final CardCollection list = CardLists.getValidCards(paidList, valid, source.getController(), source); @@ -1823,7 +1824,7 @@ public class CardFactoryUtil { *

* isMostProminentColor. *

- * + * * @param list * a {@link Iterable} object. * @return a boolean. @@ -1845,12 +1846,12 @@ public class CardFactoryUtil { byte mask = 0; int nMax = -1; - for (int i = 0; i < cntColors; i++) { + for (int i = 0; i < cntColors; i++) { if (map[i] > nMax) mask = MagicColor.WUBRG[i]; else if (map[i] == nMax) mask |= MagicColor.WUBRG[i]; - else + else continue; nMax = map[i]; } @@ -1861,7 +1862,7 @@ public class CardFactoryUtil { *

* SortColorsFromList. *

- * + * * @param list * a {@link forge.game.card.CardCollection} object. * @return a List. @@ -1888,7 +1889,7 @@ public class CardFactoryUtil { *

* getMostProminentColorsFromList. *

- * + * * @param list * a {@link forge.game.card.CardCollectionView} object. * @return a boolean. @@ -1915,12 +1916,12 @@ public class CardFactoryUtil { byte mask = 0; int nMax = -1; - for (int i = 0; i < cntColors; i++) { + for (int i = 0; i < cntColors; i++) { if (map[i] > nMax) mask = colorRestrictions.get(i); else if (map[i] == nMax) mask |= colorRestrictions.get(i); - else + else continue; nMax = map[i]; } @@ -1931,7 +1932,7 @@ public class CardFactoryUtil { *

* getMostProminentCreatureType. *

- * + * * @param list * a {@link forge.game.card.CardCollection} object. * @return an int. @@ -1970,7 +1971,7 @@ public class CardFactoryUtil { *

* sharedKeywords. *

- * + * * @param kw * a String arry. * @return a List. @@ -1980,35 +1981,37 @@ public class CardFactoryUtil { final List filteredkw = Lists.newArrayList(); final Player p = host.getController(); CardCollectionView cardlist = p.getGame().getCardsIn(zones); - final List landkw = Lists.newArrayList(); - final List protectionkw = Lists.newArrayList(); - final List hexproofkw = Lists.newArrayList(); - final List allkw = Lists.newArrayList(); + final Set landkw = Sets.newHashSet(); + final Set protectionkw = Sets.newHashSet(); + final Set protectionColorkw = Sets.newHashSet(); + final Set hexproofkw = Sets.newHashSet(); + final Set allkw = Sets.newHashSet(); + for (Card c : CardLists.getValidCards(cardlist, restrictions, p, host, null)) { for (KeywordInterface inst : c.getKeywords()) { final String k = inst.getOriginal(); if (k.endsWith("walk")) { - if (!landkw.contains(k)) { - landkw.add(k); - } + landkw.add(k); } else if (k.startsWith("Protection")) { - if (!protectionkw.contains(k)) { - protectionkw.add(k); + protectionkw.add(k); + for(byte col : MagicColor.WUBRG) { + final String colString = "Protection from " + MagicColor.toLongString(col).toLowerCase(); + if (k.contains(colString)) { + protectionColorkw.add(colString); + } } } else if (k.startsWith("Hexproof")) { - if (!hexproofkw.contains(k)) { - hexproofkw.add(k); - } - } - if (!allkw.contains(k)) { - allkw.add(k); + hexproofkw.add(k); } + allkw.add(k); } } for (String keyword : kw) { if (keyword.equals("Protection")) { filteredkw.addAll(protectionkw); + } else if (keyword.equals("ProtectionColor")) { + filteredkw.addAll(protectionColorkw); } else if (keyword.equals("Landwalk")) { filteredkw.addAll(landkw); } else if (keyword.equals("Hexproof")) { @@ -2032,7 +2035,7 @@ public class CardFactoryUtil { *

* getNeededXDamage. *

- * + * * @param ability * a {@link forge.game.spellability.SpellAbility} object. * @return a int. @@ -2052,7 +2055,7 @@ public class CardFactoryUtil { /** * Adds the ability factory abilities. - * + * * @param card * the card */ @@ -2081,7 +2084,7 @@ public class CardFactoryUtil { *

* postFactoryKeywords. *

- * + * * @param card * a {@link forge.game.card.Card} object. */ @@ -2186,7 +2189,7 @@ public class CardFactoryUtil { return re; } - + public static void addTriggerAbility(final KeywordInterface inst, final Card card, final boolean intrinsic) { String keyword = inst.getOriginal(); @@ -2222,13 +2225,13 @@ public class CardFactoryUtil { } else if (keyword.startsWith("Annihilator")) { final String[] k = keyword.split(":"); final String n = k[1]; - + final String trig = "Mode$ Attacks | ValidCard$ Card.Self | " + "TriggerZones$ Battlefield | Secondary$ True | TriggerDescription$ " + "Annihilator " + n + " (" + inst.getReminderText() + ")"; final String effect = "DB$ Sacrifice | Defined$ DefendingPlayer | SacValid$ Permanent | Amount$ " + k[1]; - + final Trigger trigger = TriggerHandler.parseTrigger(trig, card, intrinsic); trigger.setOverridingAbility(AbilityFactory.getAbility(effect, card)); @@ -2239,12 +2242,12 @@ public class CardFactoryUtil { final String trig = "Mode$ Always | TriggerZones$ Battlefield | Secondary$ True" + " | Static$ True | Blessing$ False | IsPresent$ Permanent.YouCtrl | PresentCompare$ GE10 " + " | TriggerDescription$ Ascend (" + inst.getReminderText() + ")"; - + final String effect = "DB$ Ascend | Defined$ You"; - + final Trigger trigger = TriggerHandler.parseTrigger(trig, card, intrinsic); trigger.setOverridingAbility(AbilityFactory.getAbility(effect, card)); - + inst.addTrigger(trigger); } else { SpellAbility sa = card.getFirstSpellAbility(); @@ -2283,7 +2286,7 @@ public class CardFactoryUtil { final Trigger bushidoTrigger1 = TriggerHandler.parseTrigger(trigBlock, card, intrinsic); final Trigger bushidoTrigger2 = TriggerHandler.parseTrigger(trigBlocked, card, intrinsic); - + bushidoTrigger1.setOverridingAbility(pump); bushidoTrigger2.setOverridingAbility(pump); @@ -2449,7 +2452,7 @@ public class CardFactoryUtil { + "TriggerDescription$ Evolve (" + inst.getReminderText()+ ")"; final String effect = "DB$ PutCounter | Defined$ Self | CounterType$ P1P1 | " + "CounterNum$ 1 | Evolve$ True"; - + final Trigger trigger = TriggerHandler.parseTrigger(trigStr, card, intrinsic); trigger.setOverridingAbility(AbilityFactory.getAbility(effect, card)); @@ -2529,7 +2532,7 @@ public class CardFactoryUtil { card.setSVar("FadingCleanup","DB$ Cleanup | ClearRemembered$ True"); card.setSVar("FadingCheckSVar","Count$RememberedSize"); final Trigger trigger = TriggerHandler.parseTrigger(upkeepTrig, card, intrinsic); - + inst.addTrigger(trigger); } else if (keyword.equals("Flanking")) { final StringBuilder trigFlanking = new StringBuilder( @@ -2617,7 +2620,7 @@ public class CardFactoryUtil { final Trigger trigHauntRemoved = TriggerHandler.parseTrigger(sbHauntRemoved.toString(), card, intrinsic); trigHauntRemoved.setOverridingAbility(unhaunt); - + triggers.add(trigHauntRemoved); // Fifth, add all triggers and abilities to the card. @@ -2662,7 +2665,7 @@ public class CardFactoryUtil { sb.append("Mode$ ChangesZone | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigHideawayDig"); sb.append("| Secondary$ True | TriggerDescription$ When CARDNAME enters the battlefield, "); sb.append("look at the top four cards of your library, exile one face down"); - sb.append(", then put the rest on the bottom of your library."); + sb.append(", then put the rest on the bottom of your library."); final Trigger hideawayTrigger = TriggerHandler.parseTrigger(sb.toString(), card, intrinsic); triggers.add(hideawayTrigger); card.setSVar("TrigHideawayDig", "DB$ Dig | Defined$ You | DigNum$ 4 | DestinationZone$ Exile | ExileFaceDown$ True | RememberChanged$ True | SubAbility$ DBHideawayEffect"); @@ -2676,7 +2679,7 @@ public class CardFactoryUtil { triggers.add(changeZoneTrigger); card.setSVar("DBHideawayRemember", "DB$ Animate | Defined$ Imprinted | RememberObjects$ Remembered | Permanent$ True"); card.setSVar("DBHideawayCleanup", "DB$ Cleanup | ClearRemembered$ True"); - + for (final Trigger trigger : triggers) { inst.addTrigger(trigger); } @@ -2728,7 +2731,7 @@ public class CardFactoryUtil { "Play Madness " + ManaCostParser.parse(manacost) + " - " + card.getName(); final String playMadness = "AB$ Play | Cost$ 0 | Defined$ Self | PlayCost$ " + manacost + - " | ConditionDefined$ Self | ConditionPresent$ Card.StrictlySelf+inZoneExile" + + " | ConditionDefined$ Self | ConditionPresent$ Card.StrictlySelf+inZoneExile" + " | Optional$ True | SubAbility$ DBWasNotPlayMadness | RememberPlayed$ True | Madness$ True"; final String moveToYard = "DB$ ChangeZone | Defined$ Self.StrictlySelf | Origin$ Exile | " + "Destination$ Graveyard | TrackDiscarded$ True | ConditionDefined$ Remembered | ConditionPresent$" + @@ -2747,7 +2750,7 @@ public class CardFactoryUtil { final String effect = "DB$ Pump | Defined$ TriggeredAttackerLKICopy | NumAtt$ MeleeX | NumDef$ MeleeX"; final Trigger trigger = TriggerHandler.parseTrigger(trigStr, card, intrinsic); - + SpellAbility sa = AbilityFactory.getAbility(effect, card); sa.setSVar("MeleeX", "TriggeredPlayersDefenders$Amount"); sa.setIntrinsic(intrinsic); @@ -2797,20 +2800,20 @@ public class CardFactoryUtil { } else if (keyword.startsWith("Modular")) { final String abStr = "DB$ PutCounter | ValidTgts$ Artifact.Creature | " + "TgtPrompt$ Select target artifact creature | CounterType$ P1P1 | CounterNum$ ModularX"; - + String trigStr = "Mode$ ChangesZone | ValidCard$ Card.Self | Origin$ Battlefield | Destination$ Graveyard" + " | OptionalDecider$ TriggeredCardController | TriggerController$ TriggeredCardController" + " | Secondary$ True | TriggerDescription$ When CARDNAME dies, " + "you may put a +1/+1 counter on target artifact creature for each +1/+1 counter on CARDNAME"; final Trigger trigger = TriggerHandler.parseTrigger(trigStr, card, intrinsic); - + SpellAbility ab = AbilityFactory.getAbility(abStr, card); - + ab.setSVar("ModularX", "TriggeredCard$CardCounters.P1P1"); - + trigger.setOverridingAbility(ab); - + inst.addTrigger(trigger); } else if (keyword.equals("Myriad")) { final String actualTrigger = "Mode$ Attacks | ValidCard$ Card.Self | Execute$ " @@ -2834,7 +2837,7 @@ public class CardFactoryUtil { card.setSVar("MyriadDelTrig", dbString2); card.setSVar("MyriadExile", dbString3); card.setSVar("MyriadCleanup", dbString4); - + inst.addTrigger(parsedTrigger); } else if (keyword.startsWith("Partner:")) { // Partner With @@ -2855,7 +2858,7 @@ public class CardFactoryUtil { inst.addTrigger(trigger); } else if (keyword.equals("Persist")) { final String trigStr = "Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | OncePerEffect$ True " + - " | ValidCard$ Card.Self+counters_EQ0_M1M1 | Secondary$ True" + + " | ValidCard$ Card.Self+counters_EQ0_M1M1 | Secondary$ True" + " | TriggerDescription$ Persist (" + inst.getReminderText() + ")"; final String effect = "DB$ ChangeZone | Defined$ TriggeredCard | Origin$ Graveyard | Destination$ Battlefield | WithCounters$ M1M1_1"; @@ -2892,7 +2895,7 @@ public class CardFactoryUtil { inst.addTrigger(parsedTrigger); } else if (keyword.equals("Prowess")) { - final String trigProwess = "Mode$ SpellCast | ValidCard$ Card.nonCreature" + final String trigProwess = "Mode$ SpellCast | ValidCard$ Card.nonCreature" + " | ValidActivatingPlayer$ You | TriggerZones$ Battlefield | TriggerDescription$ " + "Prowess (" + inst.getReminderText() + ")"; @@ -2978,30 +2981,30 @@ public class CardFactoryUtil { final String actualTrigger = "Mode$ SpellCast | ValidCard$ Card.Self | OptionalDecider$ You | " + " Secondary$ True | TriggerDescription$ Ripple " + num + " - CARDNAME"; - + final String abString = "DB$ Dig | NoMove$ True | DigNum$ " + num + " | Reveal$ True | RememberRevealed$ True"; final String dbCast = "DB$ Play | Valid$ Card.IsRemembered+sameName | " + "ValidZone$ Library | WithoutManaCost$ True | Optional$ True | " + "Amount$ All"; - + final String toBottom = "DB$ ChangeZoneAll | ChangeType$ Card.IsRemembered " + "| Origin$ Library | Destination$ Library | LibraryPosition$ -1"; final String cleanuptxt = "DB$ Cleanup | ClearRemembered$ True"; - + SpellAbility sa = AbilityFactory.getAbility(abString, card); AbilitySub saCast = (AbilitySub)AbilityFactory.getAbility(dbCast, card); AbilitySub saBottom = (AbilitySub)AbilityFactory.getAbility(toBottom, card); AbilitySub saCleanup = (AbilitySub)AbilityFactory.getAbility(cleanuptxt, card); - + saBottom.setSubAbility(saCleanup); saCast.setSubAbility(saBottom); sa.setSubAbility(saCast); - + sa.setIntrinsic(intrinsic); - + final Trigger parsedTrigger = TriggerHandler.parseTrigger(actualTrigger, card, intrinsic); parsedTrigger.setOverridingAbility(sa); @@ -3071,13 +3074,13 @@ public class CardFactoryUtil { } else if (keyword.equals("Storm")) { final String actualTrigger = "Mode$ SpellCast | ValidCard$ Card.Self | Secondary$ True" + "| TriggerDescription$ Storm (" + inst.getReminderText() + ")"; - + String effect = "DB$ CopySpellAbility | Defined$ TriggeredSpellAbility | Amount$ StormCount"; - + final Trigger parsedTrigger = TriggerHandler.parseTrigger(actualTrigger, card, intrinsic); final SpellAbility sa = AbilityFactory.getAbility(effect, card); - + sa.setSVar("StormCount", "TriggerCount$CurrentStormCount/Minus.1"); parsedTrigger.setOverridingAbility(sa); @@ -3142,7 +3145,7 @@ public class CardFactoryUtil { inst.addTrigger(parsedTrigger); } else if (keyword.equals("Undying")) { final String trigStr = "Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | OncePerEffect$ True " + - " | Execute$ UndyingReturn | ValidCard$ Card.Self+counters_EQ0_P1P1 | TriggerZones$ Battlefield | Secondary$ True" + + " | Execute$ UndyingReturn | ValidCard$ Card.Self+counters_EQ0_P1P1 | TriggerZones$ Battlefield | Secondary$ True" + " | TriggerDescription$ Undying (" + inst.getReminderText() + ")"; final String effect = "DB$ ChangeZone | Defined$ TriggeredCard | Origin$ Graveyard | Destination$ Battlefield | WithCounters$ P1P1_1"; @@ -3210,10 +3213,10 @@ public class CardFactoryUtil { String strTrig = "Mode$ SpellCast | ValidCard$ Card.Self | ValidSA$ Spell.MayPlaySource | Static$ True | Secondary$ True " + " | TriggerDescription$ If you cast it any time a sorcery couldn't have been cast, " + " the controller of the permanent it becomes sacrifices it at the beginning of the next cleanup step."; - + final String strDelay = "DB$ DelayedTrigger | Mode$ Phase | Phase$ Cleanup | TriggerDescription$ At the beginning of the next cleanup step, sacrifice CARDNAME."; final String strSac = "DB$ SacrificeAll | Defined$ Self"; - + SpellAbility saDelay = AbilityFactory.getAbility(strDelay, card); saDelay.setAdditionalAbility("Execute", (AbilitySub) AbilityFactory.getAbility(strSac, card)); final Trigger trigger = TriggerHandler.parseTrigger(strTrig, card, intrinsic); @@ -3422,9 +3425,9 @@ public class CardFactoryUtil { final String[] k = keyword.split(":"); final Cost cost = new Cost(k[1], false); sb.append( cost.isOnlyManaCost() ? " " : "—"); - + sb.append(cost.toSimpleString()); - + if (!cost.isOnlyManaCost()) { sb.append("."); } @@ -3521,7 +3524,7 @@ public class CardFactoryUtil { + " | Description$ Rebound (" + inst.getReminderText() + ")"; String abExile = "DB$ ChangeZone | Defined$ Self | Origin$ Stack | Destination$ Exile"; - String delTrig = "DB$ DelayedTrigger | Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You " + + String delTrig = "DB$ DelayedTrigger | Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You " + " | OptionalDecider$ You | RememberObjects$ Self | TriggerDescription$" + " At the beginning of your next upkeep, you may cast " + card.toString() + " without paying its mana cost."; // TODO add check for still in exile @@ -3647,7 +3650,7 @@ public class CardFactoryUtil { inst.addReplacement(re); } - + // extra part for the Damage Prevention keywords if (keyword.startsWith("Prevent all ")) { // TODO add intrinsic warning @@ -3689,10 +3692,10 @@ public class CardFactoryUtil { inst.addReplacement(re); } } - + else if (keyword.startsWith("If CARDNAME would be put into a graveyard " + "from anywhere, reveal CARDNAME and shuffle it into its owner's library instead.")) { - + StringBuilder sb = new StringBuilder("Event$ Moved | Destination$ Graveyard | ValidCard$ Card.Self "); // to show it on Nexus @@ -3713,10 +3716,10 @@ public class CardFactoryUtil { re.setLayer(ReplacementLayer.Other); re.setOverridingAbility(sa); - + inst.addReplacement(re); } - + if (keyword.equals("CARDNAME enters the battlefield tapped.") || keyword.equals("Hideaway")) { String effect = "DB$ Tap | Defined$ Self | ETB$ True " + " | SpellDescription$ CARDNAME enters the battlefield tapped."; @@ -3727,11 +3730,11 @@ public class CardFactoryUtil { inst.addReplacement(re); } - + if (keyword.startsWith("ETBReplacement")) { String[] splitkw = keyword.split(":"); ReplacementLayer layer = ReplacementLayer.smartValueOf(splitkw[1]); - + final boolean optional = splitkw.length >= 4 && splitkw[3].contains("Optional"); final String valid = splitkw.length >= 6 ? splitkw[5] : "Card.Self"; @@ -3765,7 +3768,7 @@ public class CardFactoryUtil { newSA.setIntrinsic(intrinsic); inst.addSpellAbility(newSA); - + } } else if (keyword.startsWith("Adapt")) { final String[] k = keyword.split(":"); @@ -3838,7 +3841,7 @@ public class CardFactoryUtil { inst.addSpellAbility(awakenSpell); } else if (keyword.startsWith("Bestow")) { final String[] params = keyword.split(":"); - final String cost = params[1]; + final String cost = params[1]; final StringBuilder sbAttach = new StringBuilder(); sbAttach.append("SP$ Attach | Cost$ "); @@ -3892,10 +3895,10 @@ public class CardFactoryUtil { String costStr = kw[1]; String effect = "AB$ CopyPermanent | Cost$ " + costStr + " ExileFromGrave<1/CARDNAME> " + - "| ActivationZone$ Graveyard | SorcerySpeed$ True | Embalm$ True " + - "| PrecostDesc$ Embalm | CostDesc$ " + ManaCostParser.parse(costStr) + " | Defined$ Self " + + "| ActivationZone$ Graveyard | SorcerySpeed$ True | Embalm$ True " + + "| PrecostDesc$ Embalm | CostDesc$ " + ManaCostParser.parse(costStr) + " | Defined$ Self " + "| StackDescription$ Embalm - CARDNAME "+ - "| SpellDescription$ (" + inst.getReminderText() + ")" ; + "| SpellDescription$ (" + inst.getReminderText() + ")" ; final SpellAbility sa = AbilityFactory.getAbility(effect, card); sa.setIntrinsic(intrinsic); inst.addSpellAbility(sa); @@ -3957,7 +3960,7 @@ public class CardFactoryUtil { final String[] kw = keyword.split(":"); String costStr = kw[1]; Cost cost = new Cost(costStr, true); - + StringBuilder sb = new StringBuilder(); sb.append("AB$ CopyPermanent | Cost$ ").append(costStr).append(" ExileFromGrave<1/CARDNAME>") @@ -3978,7 +3981,7 @@ public class CardFactoryUtil { } sb.append(" | StackDescription$ Eternalize - CARDNAME ") - .append("| SpellDescription$ (").append(inst.getReminderText()).append(")"); + .append("| SpellDescription$ (").append(inst.getReminderText()).append(")"); final SpellAbility sa = AbilityFactory.getAbility(sb.toString(), card); sa.setIntrinsic(intrinsic); inst.addSpellAbility(sa); @@ -4018,7 +4021,7 @@ public class CardFactoryUtil { abilityStr.append("| CostDesc$ ").append(cost.toSimpleString()).append(" "); abilityStr.append("| SpellDescription$ ("); abilityStr.append(inst.getReminderText()).append(")"); - + // instantiate attach ability final SpellAbility sa = AbilityFactory.getAbility(abilityStr.toString(), card); inst.addSpellAbility(sa); @@ -4026,10 +4029,10 @@ public class CardFactoryUtil { final SpellAbility sa = AbilityFactory.buildFusedAbility(card); card.addSpellAbility(sa); inst.addSpellAbility(sa); - } else if (keyword.startsWith("Haunt")) { + } else if (keyword.startsWith("Haunt")) { if (!card.isCreature() && intrinsic) { final String[] k = keyword.split(":"); - final String hauntSVarName = k[1]; + final String hauntSVarName = k[1]; // no nice way to get the cost String abString = TextUtil.concatNoSpace( @@ -4062,9 +4065,9 @@ public class CardFactoryUtil { final String[] k = keyword.split(":"); final String magnitude = k[1]; final String manacost = k[2]; - + final String reduceCost = k.length > 3 ? k[3] : null; - + Set references = Sets.newHashSet(); String desc = "Monstrosity " + magnitude; @@ -4082,17 +4085,17 @@ public class CardFactoryUtil { if (!references.isEmpty()) { effect += "| References$ " + TextUtil.join(references, ","); } - + if (card.hasSVar("MonstrosityAILogic")) { effect += "| AILogic$ " + card.getSVar("MonstrosityAILogic"); } - + effect += "| SpellDescription$ " + desc + " (" + inst.getReminderText() + ")"; final SpellAbility sa = AbilityFactory.getAbility(effect, card); sa.setIntrinsic(intrinsic); inst.addSpellAbility(sa); - + } else if (keyword.startsWith("Morph")) { final String[] k = keyword.split(":"); @@ -4124,10 +4127,10 @@ public class CardFactoryUtil { } String effect = "AB$ ChangeZone | Cost$ " + manacost + - " Return<1/Creature.attacking+unblocked/unblocked attacker> " + + " Return<1/Creature.attacking+unblocked/unblocked attacker> " + "| PrecostDesc$ " + desc + " | CostDesc$ " + ManaCostParser.parse(manacost) + "| ActivationZone$ Hand | Origin$ Hand | Ninjutsu$ True " + - "| Destination$ Battlefield | Defined$ Self " + + "| Destination$ Battlefield | Defined$ Self " + "| SpellDescription$ (" + inst.getReminderText() + ")"; SpellAbility sa = AbilityFactory.getAbility(effect, card); @@ -4137,10 +4140,10 @@ public class CardFactoryUtil { // extra secondary effect for Commander Ninjutsu if (commander) { effect = "AB$ ChangeZone | Cost$ " + manacost + - " Return<1/Creature.attacking+unblocked/unblocked attacker> " + + " Return<1/Creature.attacking+unblocked/unblocked attacker> " + "| PrecostDesc$ " + desc + " | CostDesc$ " + ManaCostParser.parse(manacost) + "| ActivationZone$ Command | Origin$ Command | Ninjutsu$ True " + - "| Destination$ Battlefield | Defined$ Self | Secondary$ True " + + "| Destination$ Battlefield | Defined$ Self | Secondary$ True " + "| SpellDescription$ (" + inst.getReminderText() + ")"; sa = AbilityFactory.getAbility(effect, card); @@ -4196,7 +4199,7 @@ public class CardFactoryUtil { final String[] k = keyword.split(":"); final String n = k[1]; final String manacost = k[2]; - + StringBuilder sb = new StringBuilder(); sb.append("AB$ PutCounter | CounterType$ P1P1 | ActivationZone$ Hand"); sb.append("| ValidTgts$ Creature | TgtPrompt$ Select target creature"); @@ -4208,7 +4211,7 @@ public class CardFactoryUtil { final SpellAbility sa = AbilityFactory.getAbility(sb.toString(), card); sa.setIntrinsic(intrinsic); - + if (n.equals("X")) { sa.setSVar("X", "Count$xPaid"); } @@ -4220,7 +4223,7 @@ public class CardFactoryUtil { String effect = "AB$ PutCounter | Cost$ " + manacost + " ExileFromGrave<1/CARDNAME> " + "| ActivationZone$ Graveyard | ValidTgts$ Creature | CounterType$ P1P1 " + "| CounterNum$ ScavengeX | SorcerySpeed$ True " + - "| PrecostDesc$ Scavenge | CostDesc$ " + ManaCostParser.parse(manacost) + + "| PrecostDesc$ Scavenge | CostDesc$ " + ManaCostParser.parse(manacost) + "| SpellDescription$ (" + inst.getReminderText() + ")"; final SpellAbility sa = AbilityFactory.getAbility(effect, card); @@ -4252,10 +4255,10 @@ public class CardFactoryUtil { String desc = "Surge " + surgeCost.toSimpleString() + " (" + inst.getReminderText() + ")"; newSA.setDescription(desc); - + newSA.setIntrinsic(intrinsic); inst.addSpellAbility(newSA); - + } else if (keyword.startsWith("Suspend") && !keyword.equals("Suspend")) { // only add it if suspend has counter and cost final String[] k = keyword.split(":"); @@ -4285,7 +4288,7 @@ public class CardFactoryUtil { GameEntityCounterTable table = new GameEntityCounterTable(); c.addCounter(CounterType.TIME, counters, getActivatingPlayer(), true, table); table.triggerCountersPutAll(game); - + String sb = TextUtil.concatWithSpace(getActivatingPlayer().toString(),"has suspended", c.getName(), "with", String.valueOf(counters),"time counters on it."); game.getGameLog().add(GameLogEntryType.STACK_RESOLVE, sb); } @@ -4342,11 +4345,11 @@ public class CardFactoryUtil { " | PrecostDesc$ Unearth | StackDescription$ " + "Unearth: Return CARDNAME to the battlefield. | SpellDescription$" + " (" + inst.getReminderText() + ")"; - + final SpellAbility sa = AbilityFactory.getAbility(effect, card); sa.setIntrinsic(intrinsic); inst.addSpellAbility(sa); - + } else if (keyword.endsWith(" offering")) { final String offeringType = keyword.split(" ")[0]; final SpellAbility sa = card.getFirstSpellAbility(); @@ -4363,7 +4366,7 @@ public class CardFactoryUtil { newSA.setDescription(sa.getDescription() + " (" + offeringType + " offering)"); newSA.setIntrinsic(intrinsic); inst.addSpellAbility(newSA); - + } else if (keyword.startsWith("Crew")) { final String[] k = keyword.split(":"); final String power = k[1]; @@ -4378,7 +4381,7 @@ public class CardFactoryUtil { final SpellAbility sa = AbilityFactory.getAbility(effect, card); sa.setIntrinsic(intrinsic); inst.addSpellAbility(sa); - + } else if (keyword.startsWith("Cycling")) { final String[] k = keyword.split(":"); final String manacost = k[1]; @@ -4396,7 +4399,7 @@ public class CardFactoryUtil { sa.setAlternativeCost(AlternativeCost.Cycling); sa.setIntrinsic(intrinsic); inst.addSpellAbility(sa); - + } else if (keyword.startsWith("TypeCycling")) { final String[] k = keyword.split(":"); final String type = k[1]; @@ -4420,7 +4423,7 @@ public class CardFactoryUtil { sa.setAlternativeCost(AlternativeCost.Cycling); sa.setIntrinsic(intrinsic); inst.addSpellAbility(sa); - + } } @@ -4551,7 +4554,7 @@ public class CardFactoryUtil { * @param card * @param altCost * @param sa - * @return + * @return */ private static SpellAbility makeAltCostAbility(final Card card, final String altCost, final SpellAbility sa) { final Map params = AbilityFactory.getMapParams(altCost); @@ -4569,9 +4572,9 @@ public class CardFactoryUtil { } altCostSA.setRestrictions(restriction); - final String costDescription = params.containsKey("Description") ? params.get("Description") + final String costDescription = params.containsKey("Description") ? params.get("Description") : TextUtil.concatWithSpace("You may", abCost.toStringAlt(),"rather than pay", TextUtil.addSuffix(card.getName(),"'s mana cost.")); - + altCostSA.setDescription(costDescription); if (params.containsKey("References")) { for (String svar : params.get("References").split(",")) { diff --git a/forge-game/src/main/java/forge/game/card/CardUtil.java b/forge-game/src/main/java/forge/game/card/CardUtil.java index 8365f7cb3b0..d3173a2ad1b 100644 --- a/forge-game/src/main/java/forge/game/card/CardUtil.java +++ b/forge-game/src/main/java/forge/game/card/CardUtil.java @@ -319,6 +319,14 @@ public final class CardUtil { return res; } + public static ColorSet getColorsYouCtrl(final Player p) { + byte b = 0; + for (Card c : p.getCardsIn(ZoneType.Battlefield)) { + b |= c.determineColor().getColor(); + } + return ColorSet.fromMask(b); + } + public static CardState getFaceDownCharacteristic(Card c) { final CardType type = new CardType(); type.add("Creature"); diff --git a/forge-game/src/main/java/forge/game/card/CardView.java b/forge-game/src/main/java/forge/game/card/CardView.java index 8e6654474bf..f4cf26a6699 100644 --- a/forge-game/src/main/java/forge/game/card/CardView.java +++ b/forge-game/src/main/java/forge/game/card/CardView.java @@ -2,6 +2,8 @@ package forge.game.card; import com.google.common.base.Predicate; import com.google.common.collect.Iterables; +import com.google.common.collect.Sets; + import forge.ImageKeys; import forge.card.*; import forge.card.mana.ManaCost; @@ -26,6 +28,7 @@ import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; public class CardView extends GameEntityView { private static final long serialVersionUID = -3624090829028979255L; @@ -422,6 +425,8 @@ public class CardView extends GameEntityView { case SchemeDeck: // true for now, to actually see the Scheme cards (can't see deck anyway) return true; + default: + break; } // special viewing permissions for viewer @@ -649,6 +654,17 @@ public class CardView extends GameEntityView { } + Set cantHaveKeyword = this.getCantHaveKeyword(); + if (cantHaveKeyword != null && !cantHaveKeyword.isEmpty()) { + sb.append("\r\n\r\n"); + for(String k : cantHaveKeyword) { + sb.append("CARDNAME can't have or gain ".replaceAll("CARDNAME", getName())); + sb.append(k); + sb.append("."); + sb.append("\r\n"); + } + } + String cloner = get(TrackableProperty.Cloner); if (!cloner.isEmpty()) { sb.append("\r\nCloned by: ").append(cloner); @@ -768,6 +784,18 @@ public class CardView extends GameEntityView { set(TrackableProperty.BlockAny, c.canBlockAny()); } + Set getCantHaveKeyword() { + return get(TrackableProperty.CantHaveKeyword); + } + + void updateCantHaveKeyword(Card c) { + Set keywords = Sets.newTreeSet(); + for (Keyword k : c.getCantHaveKeyword()) { + keywords.add(k.toString()); + } + set(TrackableProperty.CantHaveKeyword, keywords); + } + @Override public String toString() { String name = getName(); diff --git a/forge-game/src/main/java/forge/game/keyword/Keyword.java b/forge-game/src/main/java/forge/game/keyword/Keyword.java index 2959b6baf50..5a6e3c33c6c 100644 --- a/forge-game/src/main/java/forge/game/keyword/Keyword.java +++ b/forge-game/src/main/java/forge/game/keyword/Keyword.java @@ -287,4 +287,15 @@ public enum Keyword { return UNDEFINED; } + + public static Set setValueOf(String value) { + Set result = EnumSet.noneOf(Keyword.class); + for (String s : value.split(" & ")) { + Keyword k = smartValueOf(s); + if (!UNDEFINED.equals(k)) { + result.add(k); + } + } + return result; + } } diff --git a/forge-game/src/main/java/forge/game/keyword/KeywordCollection.java b/forge-game/src/main/java/forge/game/keyword/KeywordCollection.java index e7c8f0ea745..de9f14c2ecf 100644 --- a/forge-game/src/main/java/forge/game/keyword/KeywordCollection.java +++ b/forge-game/src/main/java/forge/game/keyword/KeywordCollection.java @@ -12,7 +12,7 @@ import forge.game.card.Card; public class KeywordCollection implements Iterable, Serializable { private static final long serialVersionUID = -2882986558147844702L; - + private boolean hidden = false; private transient KeywordCollectionView view; @@ -64,7 +64,7 @@ public class KeywordCollection implements Iterable, Serializable { return true; } return false; - + } public void addAll(Iterable keywords) { @@ -82,10 +82,10 @@ public class KeywordCollection implements Iterable, Serializable { } return result; } - + public boolean remove(String keyword) { Iterator it = map.values().iterator(); - + boolean result = false; while (it.hasNext()) { KeywordInterface k = it.next(); @@ -94,7 +94,7 @@ public class KeywordCollection implements Iterable, Serializable { result = true; } } - + return result; } @@ -102,6 +102,10 @@ public class KeywordCollection implements Iterable, Serializable { return map.remove(keyword.getKeyword(), keyword); } + public boolean removeAll(Keyword kenum) { + return !map.removeAll(kenum).isEmpty(); + } + public boolean removeAll(Iterable keywords) { boolean result = false; for (String k : keywords) { @@ -144,7 +148,7 @@ public class KeywordCollection implements Iterable, Serializable { } return amount; } - + public Collection getValues() { return map.values(); } @@ -163,7 +167,6 @@ public class KeywordCollection implements Iterable, Serializable { public Iterator iterator() { return new Iterator() { private final Iterator iterator = map.values().iterator(); - @Override public boolean hasNext() { diff --git a/forge-game/src/main/java/forge/game/player/PlayerProperty.java b/forge-game/src/main/java/forge/game/player/PlayerProperty.java index 5970a266813..18593cbb8aa 100644 --- a/forge-game/src/main/java/forge/game/player/PlayerProperty.java +++ b/forge-game/src/main/java/forge/game/player/PlayerProperty.java @@ -38,6 +38,10 @@ public class PlayerProperty { return false; } } + } else if (property.startsWith("PlayerUID_")) { + if (player.getId() != Integer.parseInt(property.split("PlayerUID_")[1])) { + return false; + } } else if (property.equals("YourTeam")) { if (!player.sameTeam(sourceController)) { return false; diff --git a/forge-game/src/main/java/forge/game/staticability/StaticAbility.java b/forge-game/src/main/java/forge/game/staticability/StaticAbility.java index 1dd7ab9f55f..df855c9a8c1 100644 --- a/forge-game/src/main/java/forge/game/staticability/StaticAbility.java +++ b/forge-game/src/main/java/forge/game/staticability/StaticAbility.java @@ -165,7 +165,8 @@ public class StaticAbility extends CardTraitBase implements IIdentifiable, Clone if (hasParam("AddKeyword") || hasParam("AddAbility") || hasParam("AddTrigger") || hasParam("RemoveTriggers") || hasParam("RemoveKeyword") || hasParam("AddReplacementEffects") - || hasParam("AddStaticAbility") || hasParam("AddSVar")) { + || hasParam("AddStaticAbility") || hasParam("AddSVar") + || hasParam("CantHaveKeyword")) { layers.add(StaticAbilityLayer.ABILITIES); } @@ -181,10 +182,6 @@ public class StaticAbility extends CardTraitBase implements IIdentifiable, Clone } if (hasParam("AddHiddenKeyword")) { - // special rule for can't have or gain - if (getParam("AddHiddenKeyword").contains("can't have or gain")) { - layers.add(StaticAbilityLayer.ABILITIES); - } layers.add(StaticAbilityLayer.RULES); } diff --git a/forge-game/src/main/java/forge/game/staticability/StaticAbilityContinuous.java b/forge-game/src/main/java/forge/game/staticability/StaticAbilityContinuous.java index fe5206d6768..5fb038ab9a9 100644 --- a/forge-game/src/main/java/forge/game/staticability/StaticAbilityContinuous.java +++ b/forge-game/src/main/java/forge/game/staticability/StaticAbilityContinuous.java @@ -17,11 +17,13 @@ */ package forge.game.staticability; +import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import forge.GameCommand; +import forge.card.ColorSet; import forge.card.CardType; import forge.card.MagicColor; import forge.game.Game; @@ -33,6 +35,7 @@ import forge.game.ability.AbilityUtils; import forge.game.ability.ApiType; import forge.game.card.*; import forge.game.cost.Cost; +import forge.game.keyword.Keyword; import forge.game.player.Player; import forge.game.replacement.ReplacementEffect; import forge.game.replacement.ReplacementHandler; @@ -113,9 +116,9 @@ public final class StaticAbilityContinuous { String setT = ""; int setToughness = Integer.MAX_VALUE; - String[] addKeywords = null; + List addKeywords = null; List addHiddenKeywords = Lists.newArrayList(); - String[] removeKeywords = null; + List removeKeywords = null; String[] addAbilities = null; String[] addReplacements = null; String[] addSVars = null; @@ -137,6 +140,8 @@ public final class StaticAbilityContinuous { boolean overwriteColors = false; + Set cantHaveKeyword = null; + List mayLookAt = null; List withFlash = null; @@ -189,50 +194,96 @@ public final class StaticAbilityContinuous { } if (layer == StaticAbilityLayer.ABILITIES && params.containsKey("AddKeyword")) { - addKeywords = params.get("AddKeyword").split(" & "); - final Iterable chosencolors = hostCard.getChosenColors(); - for (final String color : chosencolors) { - for (int w = 0; w < addKeywords.length; w++) { - addKeywords[w] = addKeywords[w].replaceAll("ChosenColor", StringUtils.capitalize(color)); - } - } - final String chosenType = hostCard.getChosenType(); - for (int w = 0; w < addKeywords.length; w++) { - addKeywords[w] = addKeywords[w].replaceAll("ChosenType", chosenType); - } - final String chosenName = hostCard.getNamedCard().replace(",", ";"); + addKeywords = Lists.newArrayList(Arrays.asList(params.get("AddKeyword").split(" & "))); + final List newKeywords = Lists.newArrayList(); + + // update keywords with Chosen parts final String hostCardUID = Integer.toString(hostCard.getId()); // Protection with "doesn't remove" effect - final String hostCardPower = Integer.toString(hostCard.getNetPower()); - for (int w = 0; w < addKeywords.length; w++) { - if (addKeywords[w].startsWith("Protection:")) { - String k = addKeywords[w]; - k = k.replaceAll("ChosenName", "Card.named" + chosenName); - k = k.replace("HostCardUID", hostCardUID); - addKeywords[w] = k; - } else if (addKeywords[w].startsWith("CantBeBlockedBy")) { - String k = addKeywords[w]; - k = k.replaceAll("HostCardPower", hostCardPower); - addKeywords[w] = k; + + final ColorSet colorsYouCtrl = CardUtil.getColorsYouCtrl(controller); + + Iterables.removeIf(addKeywords, new Predicate() { + @Override + public boolean apply(String input) { + if (!hostCard.hasChosenColor() && input.contains("ChosenColor")) { + return true; + } + if (!hostCard.hasChosenType() && input.contains("ChosenType")) { + return true; + } + if (!hostCard.hasChosenNumber() && input.contains("ChosenNumber")) { + return true; + } + if (!hostCard.hasChosenPlayer() && input.contains("ChosenPlayer")) { + return true; + } + if (!hostCard.hasChosenName() && input.contains("ChosenName")) { + return true; + } + + // two variants for Red vs. red in keyword + if (input.contains("ColorsYouCtrl") || input.contains("colorsYouCtrl")) { + for (byte color : colorsYouCtrl) { + final String colorWord = MagicColor.toLongString(color); + String y = input.replaceAll("ColorsYouCtrl", StringUtils.capitalize(colorWord)); + y = y.replaceAll("colorsYouCtrl", colorWord); + newKeywords.add(y); + } + return true; + } + + return false; } - } + + }); + + addKeywords.addAll(newKeywords); + + addKeywords = Lists.transform(addKeywords, new Function() { + + @Override + public String apply(String input) { + if (hostCard.hasChosenColor()) { + input = input.replaceAll("ChosenColor", StringUtils.capitalize(hostCard.getChosenColor())); + } + if (hostCard.hasChosenType()) { + input = input.replaceAll("ChosenType", hostCard.getChosenType()); + } + if (hostCard.hasChosenNumber()) { + input = input.replaceAll("ChosenNumber", String.valueOf(hostCard.getChosenNumber())); + } + if (hostCard.hasChosenPlayer()) { + Player cp = hostCard.getChosenPlayer(); + input = input.replaceAll("ChosenPlayerUID", String.valueOf(cp.getId())); + input = input.replaceAll("ChosenPlayerName", cp.getName()); + } + if (hostCard.hasChosenName()) { + final String chosenName = hostCard.getChosenName().replace(",", ";"); + input = input.replaceAll("ChosenName", "Card.named" + chosenName); + } + input = input.replace("HostCardUID", hostCardUID); + return input; + } + + }); + if (params.containsKey("SharedKeywordsZone")) { List zones = ZoneType.listValueOf(params.get("SharedKeywordsZone")); String[] restrictions = params.containsKey("SharedRestrictions") ? params.get("SharedRestrictions").split(",") : new String[] {"Card"}; - List kw = CardFactoryUtil.sharedKeywords(Arrays.asList(addKeywords), restrictions, zones, hostCard); - addKeywords = kw.toArray(new String[kw.size()]); + addKeywords = CardFactoryUtil.sharedKeywords(addKeywords, restrictions, zones, hostCard); } } - if ((layer == StaticAbilityLayer.RULES || layer == StaticAbilityLayer.ABILITIES) && params.containsKey("AddHiddenKeyword")) { - // can't have or gain, need to be applyed in ABILITIES1 - for (String k : params.get("AddHiddenKeyword").split(" & ")) { - if ( (k.contains("can't have or gain")) == (layer == StaticAbilityLayer.ABILITIES)) - addHiddenKeywords.add(k); - } + if (layer == StaticAbilityLayer.ABILITIES && params.containsKey("CantHaveKeyword")) { + cantHaveKeyword = Keyword.setValueOf(params.get("CantHaveKeyword")); + } + + if ((layer == StaticAbilityLayer.RULES) && params.containsKey("AddHiddenKeyword")) { + addHiddenKeywords.addAll(Arrays.asList(params.get("AddHiddenKeyword").split(" & "))); } if (layer == StaticAbilityLayer.ABILITIES && params.containsKey("RemoveKeyword")) { - removeKeywords = params.get("RemoveKeyword").split(" & "); + removeKeywords = Arrays.asList(params.get("RemoveKeyword").split(" & ")); } if (layer == StaticAbilityLayer.ABILITIES && params.containsKey("RemoveAllAbilities")) { @@ -419,7 +470,7 @@ public final class StaticAbilityContinuous { // add keywords if (addKeywords != null) { - p.addChangedKeywords(addKeywords, removeKeywords == null ? new String[0] : removeKeywords, se.getTimestamp()); + p.addChangedKeywords(addKeywords, removeKeywords, se.getTimestamp()); } // add static abilities @@ -530,21 +581,44 @@ public final class StaticAbilityContinuous { // TODO regular keywords currently don't try to use keyword multiplier // (Although nothing uses it at this time) if ((addKeywords != null) || (removeKeywords != null) || removeAllAbilities || removeIntrinsicAbilities) { - String[] newKeywords = null; + List newKeywords = null; if (addKeywords != null) { - newKeywords = Arrays.copyOf(addKeywords, addKeywords.length); - for (int j = 0; j < newKeywords.length; ++j) { - if (newKeywords[j].contains("CardManaCost")) { - if (affectedCard.getManaCost().isNoCost()) { - newKeywords[j] = ""; // prevent a crash (varolz the scar-striped + dryad arbor) - } else { - newKeywords[j] = newKeywords[j].replace("CardManaCost", affectedCard.getManaCost().getShortString()); + newKeywords = Lists.newArrayList(addKeywords); + final List extraKeywords = Lists.newArrayList(); + + Iterables.removeIf(newKeywords, new Predicate() { + @Override + public boolean apply(String input) { + if (input.contains("CardManaCost")) { + if (affectedCard.getManaCost().isNoCost()) { + return true; + } } - } else if (newKeywords[j].contains("ConvertedManaCost")) { - final String costcmc = Integer.toString(affectedCard.getCMC()); - newKeywords[j] = newKeywords[j].replace("ConvertedManaCost", costcmc); + // replace one Keyword with list of keywords + if (input.startsWith("Protection") && input.contains("CardColors")) { + for (Byte color : affectedCard.determineColor()) { + extraKeywords.add(input.replace("CardColors", MagicColor.toLongString(color))); + } + return true; + } + return false; } - } + }); + newKeywords.addAll(extraKeywords); + + newKeywords = Lists.transform(newKeywords, new Function() { + + @Override + public String apply(String input) { + if (input.contains("CardManaCost")) { + input = input.replace("CardManaCost", affectedCard.getManaCost().getShortString()); + } else if (input.contains("ConvertedManaCost")) { + final String costcmc = Integer.toString(affectedCard.getCMC()); + input = input.replace("ConvertedManaCost", costcmc); + } + return input; + } + }); } affectedCard.addChangedCardKeywords(newKeywords, removeKeywords, @@ -682,6 +756,10 @@ public final class StaticAbilityContinuous { || removeAllAbilities) { affectedCard.addChangedCardTraits(addedAbilities, null, addedTrigger, addedReplacementEffects, addedStaticAbility, removeAllAbilities, removeNonMana, false, hostCard.getTimestamp()); } + + if (cantHaveKeyword != null) { + affectedCard.addCantHaveKeyword(hostCard.getTimestamp(), cantHaveKeyword); + } } if (layer == StaticAbilityLayer.TYPE && removeIntrinsicAbilities) { diff --git a/forge-game/src/main/java/forge/trackable/TrackableProperty.java b/forge-game/src/main/java/forge/trackable/TrackableProperty.java index dc2eb3317e6..742ab344d9f 100644 --- a/forge-game/src/main/java/forge/trackable/TrackableProperty.java +++ b/forge-game/src/main/java/forge/trackable/TrackableProperty.java @@ -117,6 +117,8 @@ public enum TrackableProperty { NonAbilityText(TrackableTypes.StringType), FoilIndex(TrackableTypes.IntegerType), + CantHaveKeyword(TrackableTypes.StringListType), + //Player IsAI(TrackableTypes.BooleanType), LobbyPlayerName(TrackableTypes.StringType), diff --git a/forge-gui/res/cardsfolder/a/arcane_lighthouse.txt b/forge-gui/res/cardsfolder/a/arcane_lighthouse.txt index ca54e7930b6..30f41b1cd7e 100644 --- a/forge-gui/res/cardsfolder/a/arcane_lighthouse.txt +++ b/forge-gui/res/cardsfolder/a/arcane_lighthouse.txt @@ -2,6 +2,6 @@ Name:Arcane Lighthouse ManaCost:no cost Types:Land A:AB$ Mana | Cost$ T | Produced$ C | SpellDescription$ Add {C}. -A:AB$ AnimateAll | Cost$ 1 T | ValidCards$ Creature.OppCtrl | RemoveKeywords$ Hexproof & Shroud | HiddenKeywords$ CARDNAME can't have or gain Hexproof & CARDNAME can't have or gain Shroud | SpellDescription$ Until end of turn, creatures your opponents control lose hexproof and shroud and can't have hexproof or shroud. +A:AB$ AnimateAll | Cost$ 1 T | ValidCards$ Creature.OppCtrl | RemoveKeywords$ Hexproof & Shroud | CantHaveKeyword$ Hexproof & Shroud | StackDescription$ SpellDescription | SpellDescription$ Until end of turn, creatures your opponents control lose hexproof and shroud and can't have hexproof or shroud. AI:RemoveDeck:All Oracle:{T}: Add {C}.\n{1}, {T}: Until end of turn, creatures your opponents control lose hexproof and shroud and can't have hexproof or shroud. diff --git a/forge-gui/res/cardsfolder/a/archetype_of_aggression.txt b/forge-gui/res/cardsfolder/a/archetype_of_aggression.txt index e204c0cd46c..95990663573 100644 --- a/forge-gui/res/cardsfolder/a/archetype_of_aggression.txt +++ b/forge-gui/res/cardsfolder/a/archetype_of_aggression.txt @@ -3,7 +3,7 @@ ManaCost:1 R R Types:Enchantment Creature Human Warrior PT:3/2 S:Mode$ Continuous | Affected$ Creature.YouCtrl | AddKeyword$ Trample | Description$ Creatures you control have trample. -S:Mode$ Continuous | Affected$ Creature.OppCtrl | RemoveKeyword$ Trample | AddHiddenKeyword$ CARDNAME can't have or gain Trample | Description$ Creatures your opponents control lose trample and can't have or gain trample. +S:Mode$ Continuous | Affected$ Creature.OppCtrl | RemoveKeyword$ Trample | CantHaveKeyword$ Trample | Description$ Creatures your opponents control lose trample and can't have or gain trample. SVar:PlayMain1:TRUE SVar:Picture:http://www.wizards.com/global/images/magic/general/archetype_of_aggression.jpg Oracle:Creatures you control have trample.\nCreatures your opponents control lose trample and can't have or gain trample. diff --git a/forge-gui/res/cardsfolder/a/archetype_of_courage.txt b/forge-gui/res/cardsfolder/a/archetype_of_courage.txt index 2301b6e23a9..8d9e81399fa 100644 --- a/forge-gui/res/cardsfolder/a/archetype_of_courage.txt +++ b/forge-gui/res/cardsfolder/a/archetype_of_courage.txt @@ -3,7 +3,7 @@ ManaCost:1 W W Types:Enchantment Creature Human Soldier PT:2/2 S:Mode$ Continuous | Affected$ Creature.YouCtrl | AddKeyword$ First Strike | Description$ Creatures you control have first strike. -S:Mode$ Continuous | Affected$ Creature.OppCtrl | RemoveKeyword$ First Strike | AddHiddenKeyword$ CARDNAME can't have or gain First Strike | Description$ Creatures your opponents control lose first strike and can't have or gain first strike. +S:Mode$ Continuous | Affected$ Creature.OppCtrl | RemoveKeyword$ First Strike | CantHaveKeyword$ First Strike | Description$ Creatures your opponents control lose first strike and can't have or gain first strike. SVar:PlayMain1:TRUE SVar:Picture:http://www.wizards.com/global/images/magic/general/archetype_of_courage.jpg Oracle:Creatures you control have first strike.\nCreatures your opponents control lose first strike and can't have or gain first strike. diff --git a/forge-gui/res/cardsfolder/a/archetype_of_endurance.txt b/forge-gui/res/cardsfolder/a/archetype_of_endurance.txt index d3315c3dc4e..d0a065a50e6 100644 --- a/forge-gui/res/cardsfolder/a/archetype_of_endurance.txt +++ b/forge-gui/res/cardsfolder/a/archetype_of_endurance.txt @@ -3,7 +3,7 @@ ManaCost:6 G G Types:Enchantment Creature Boar PT:6/5 S:Mode$ Continuous | Affected$ Creature.YouCtrl | AddKeyword$ Hexproof | Description$ Creatures you control have hexproof. -S:Mode$ Continuous | Affected$ Creature.OppCtrl | RemoveKeyword$ Hexproof | AddHiddenKeyword$ CARDNAME can't have or gain Hexproof | Description$ Creatures your opponents control lose hexproof and can't have or gain hexproof. +S:Mode$ Continuous | Affected$ Creature.OppCtrl | RemoveKeyword$ Hexproof | CantHaveKeyword$ Hexproof | Description$ Creatures your opponents control lose hexproof and can't have or gain hexproof. SVar:PlayMain1:TRUE SVar:Picture:http://www.wizards.com/global/images/magic/general/archetype_of_endurance.jpg Oracle:Creatures you control have hexproof.\nCreatures your opponents control lose hexproof and can't have or gain hexproof. diff --git a/forge-gui/res/cardsfolder/a/archetype_of_finality.txt b/forge-gui/res/cardsfolder/a/archetype_of_finality.txt index 81b9f0f2fde..e13573520c9 100644 --- a/forge-gui/res/cardsfolder/a/archetype_of_finality.txt +++ b/forge-gui/res/cardsfolder/a/archetype_of_finality.txt @@ -3,7 +3,7 @@ ManaCost:4 B B Types:Enchantment Creature Gorgon PT:2/3 S:Mode$ Continuous | Affected$ Creature.YouCtrl | AddKeyword$ Deathtouch | Description$ Creatures you control have deathtouch. -S:Mode$ Continuous | Affected$ Creature.OppCtrl | RemoveKeyword$ Deathtouch | AddHiddenKeyword$ CARDNAME can't have or gain Deathtouch | Description$ Creatures your opponents control lose deathtouch and can't have or gain deathtouch. +S:Mode$ Continuous | Affected$ Creature.OppCtrl | RemoveKeyword$ Deathtouch | CantHaveKeyword$ Deathtouch | Description$ Creatures your opponents control lose deathtouch and can't have or gain deathtouch. SVar:PlayMain1:TRUE SVar:Picture:http://www.wizards.com/global/images/magic/general/archetype_of_finality.jpg Oracle:Creatures you control have deathtouch.\nCreatures your opponents control lose deathtouch and can't have or gain deathtouch. diff --git a/forge-gui/res/cardsfolder/a/archetype_of_imagination.txt b/forge-gui/res/cardsfolder/a/archetype_of_imagination.txt index 7d0c22f4d9e..8b6d94bc4e3 100644 --- a/forge-gui/res/cardsfolder/a/archetype_of_imagination.txt +++ b/forge-gui/res/cardsfolder/a/archetype_of_imagination.txt @@ -3,7 +3,7 @@ ManaCost:4 U U Types:Enchantment Creature Human Wizard PT:3/2 S:Mode$ Continuous | Affected$ Creature.YouCtrl | AddKeyword$ Flying | Description$ Creatures you control have flying. -S:Mode$ Continuous | Affected$ Creature.OppCtrl | RemoveKeyword$ Flying | AddHiddenKeyword$ CARDNAME can't have or gain Flying | Description$ Creatures your opponents control lose flying and can't have or gain flying. +S:Mode$ Continuous | Affected$ Creature.OppCtrl | RemoveKeyword$ Flying | CantHaveKeyword$ Flying | Description$ Creatures your opponents control lose flying and can't have or gain flying. SVar:PlayMain1:TRUE SVar:Picture:http://www.wizards.com/global/images/magic/general/archetype_of_imagination.jpg Oracle:Creatures you control have flying.\nCreatures your opponents control lose flying and can't have or gain flying. diff --git a/forge-gui/res/cardsfolder/e/earnest_fellowship.txt b/forge-gui/res/cardsfolder/e/earnest_fellowship.txt index 02b8e47277c..63b4e7a18bf 100644 --- a/forge-gui/res/cardsfolder/e/earnest_fellowship.txt +++ b/forge-gui/res/cardsfolder/e/earnest_fellowship.txt @@ -1,12 +1,7 @@ Name:Earnest Fellowship ManaCost:1 W Types:Enchantment -Text:Each creature has protection from its colors. -S:Mode$ Continuous | Affected$ Creature.White | AddKeyword$ Protection from white -S:Mode$ Continuous | Affected$ Creature.Blue | AddKeyword$ Protection from blue -S:Mode$ Continuous | Affected$ Creature.Black | AddKeyword$ Protection from black -S:Mode$ Continuous | Affected$ Creature.Red | AddKeyword$ Protection from red -S:Mode$ Continuous | Affected$ Creature.Green | AddKeyword$ Protection from green +S:Mode$ Continuous | Affected$ Creature | AddKeyword$ Protection from CardColors | Description$ Each creature has protection from its colors. SVar:NonStackingEffect:True SVar:PlayMain1:TRUE AI:RemoveDeck:Random diff --git a/forge-gui/res/cardsfolder/e/empty_shrine_kannushi.txt b/forge-gui/res/cardsfolder/e/empty_shrine_kannushi.txt index a64f851eaa8..cb96bce0d68 100644 --- a/forge-gui/res/cardsfolder/e/empty_shrine_kannushi.txt +++ b/forge-gui/res/cardsfolder/e/empty_shrine_kannushi.txt @@ -2,15 +2,6 @@ Name:Empty-Shrine Kannushi ManaCost:W Types:Creature Human Cleric PT:1/1 -S:Mode$ Continuous | Affected$ Card.Self | AddKeyword$ Protection from white | CheckSVar$ WHITE | SVarCompare$ GE1 | Description$ CARDNAME has protection from the colors of permanents you control. -S:Mode$ Continuous | Affected$ Card.Self | AddKeyword$ Protection from blue | CheckSVar$ BLUE | SVarCompare$ GE1 -S:Mode$ Continuous | Affected$ Card.Self | AddKeyword$ Protection from black | CheckSVar$ BLACK | SVarCompare$ GE1 -S:Mode$ Continuous | Affected$ Card.Self | AddKeyword$ Protection from red | CheckSVar$ RED | SVarCompare$ GE1 -S:Mode$ Continuous | Affected$ Card.Self | AddKeyword$ Protection from green | CheckSVar$ GREEN | SVarCompare$ GE1 -SVar:WHITE:Count$Valid Permanent.White+YouCtrl -SVar:BLUE:Count$Valid Permanent.Blue+YouCtrl -SVar:BLACK:Count$Valid Permanent.Black+YouCtrl -SVar:RED:Count$Valid Permanent.Red+YouCtrl -SVar:GREEN:Count$Valid Permanent.Green+YouCtrl +S:Mode$ Continuous | Affected$ Card.Self | AddKeyword$ Protection from colorsYouCtrl | Description$ CARDNAME has protection from the colors of permanents you control. SVar:Picture:http://www.wizards.com/global/images/magic/general/empty_shrine_kannushi.jpg Oracle:Empty-Shrine Kannushi has protection from the colors of permanents you control. diff --git a/forge-gui/res/cardsfolder/e/escaped_shapeshifter.txt b/forge-gui/res/cardsfolder/e/escaped_shapeshifter.txt index ef18b116bb5..27c1b001a4a 100644 --- a/forge-gui/res/cardsfolder/e/escaped_shapeshifter.txt +++ b/forge-gui/res/cardsfolder/e/escaped_shapeshifter.txt @@ -2,21 +2,5 @@ Name:Escaped Shapeshifter ManaCost:3 U U Types:Creature Shapeshifter PT:3/4 -S:Mode$ Continuous | Affected$ Card.Self | AddKeyword$ Flying | CheckSVar$ FLYING | SVarCompare$ GE1 | Description$ As long as an opponent controls a creature with flying not named CARDNAME, CARDNAME has flying. The same is true for first strike, trample, and protection from any color. -S:Mode$ Continuous | Affected$ Card.Self | AddKeyword$ First Strike | CheckSVar$ FIRSTSTRIKE | SVarCompare$ GE1 -S:Mode$ Continuous | Affected$ Card.Self | AddKeyword$ Trample | CheckSVar$ TRAMPLE | SVarCompare$ GE1 -S:Mode$ Continuous | Affected$ Card.Self | AddKeyword$ Protection from white | CheckSVar$ WHITE | SVarCompare$ GE1 -S:Mode$ Continuous | Affected$ Card.Self | AddKeyword$ Protection from blue | CheckSVar$ BLUE | SVarCompare$ GE1 -S:Mode$ Continuous | Affected$ Card.Self | AddKeyword$ Protection from black | CheckSVar$ BLACK | SVarCompare$ GE1 -S:Mode$ Continuous | Affected$ Card.Self | AddKeyword$ Protection from red | CheckSVar$ RED | SVarCompare$ GE1 -S:Mode$ Continuous | Affected$ Card.Self | AddKeyword$ Protection from green | CheckSVar$ GREEN | SVarCompare$ GE1 -SVar:FLYING:Count$Valid Creature.withFlying+notnamed Escaped Shapeshifter+OppCtrl -SVar:FIRSTSTRIKE:Count$Valid Creature.withFirst Strike+notnamed Escaped Shapeshifter+OppCtrl -SVar:TRAMPLE:Count$Valid Creature.withTrample+notnamed Escaped Shapeshifter+OppCtrl -SVar:WHITE:Count$Valid Creature.withProtection from white+notnamed Escaped Shapeshifter+OppCtrl -SVar:BLUE:Count$Valid Creature.withProtection from blue+notnamed Escaped Shapeshifter+OppCtrl -SVar:BLACK:Count$Valid Creature.withProtection from black+notnamed Escaped Shapeshifter+OppCtrl -SVar:RED:Count$Valid Creature.withProtection from red+notnamed Escaped Shapeshifter+OppCtrl -SVar:GREEN:Count$Valid Creature.withProtection from green+notnamed Escaped Shapeshifter+OppCtrl -SVar:Picture:http://www.wizards.com/global/images/magic/general/escaped_shapeshifter.jpg +S:Mode$ Continuous | Affected$ Card.Self | AddKeyword$ Flying & First Strike & Trample & ProtectionColor | SharedKeywordsZone$ Battlefield | SharedRestrictions$ Creature.notnamed Escaped Shapeshifter+OppCtrl | Description$ As long as an opponent controls a creature with flying not named Escaped Shapeshifter, CARDNAME has flying. The same is true for first strike, trample, and protection from any color. Oracle:As long as an opponent controls a creature with flying not named Escaped Shapeshifter, Escaped Shapeshifter has flying. The same is true for first strike, trample, and protection from any color. diff --git a/forge-gui/res/cardsfolder/h/haktos_the_unscarred.txt b/forge-gui/res/cardsfolder/h/haktos_the_unscarred.txt index 874c021ccb5..a287e3381e4 100644 --- a/forge-gui/res/cardsfolder/h/haktos_the_unscarred.txt +++ b/forge-gui/res/cardsfolder/h/haktos_the_unscarred.txt @@ -4,6 +4,6 @@ Types:Legendary Creature Human Warrior PT:6/1 K:CARDNAME attacks each combat if able. K:ETBReplacement:Other:ChooseNum -SVar:ChooseNum:DB$ ChooseNumber | Min$ 2 | Max$ 4 | Defined$ You | Random$ True -S:Mode$ Continuous | Affected$ Card.Self | AddKeyword$ Protection from each converted mana cost other than the chosen number | Description$ CARDNAME has protection from each converted mana cost other than the chosen number. +SVar:ChooseNum:DB$ ChooseNumber | Min$ 2 | Max$ 4 | Defined$ You | Random$ True | SpellDescription$ As Haktos enters the battlefield, choose 2, 3, or 4 at random. +S:Mode$ Continuous | Affected$ Card.Self | AddKeyword$ Protection:Card.cmcNEChosenNumber:Protection from each converted mana cost other than ChosenNumber | Description$ CARDNAME has protection from each converted mana cost other than the chosen number. Oracle:Haktos the Unscarred attacks each combat if able.\nAs Haktos enters the battlefield, choose 2, 3, or 4 at random.\nHaktos has protection from each converted mana cost other than the chosen number. diff --git a/forge-gui/res/cardsfolder/p/pledge_of_loyalty.txt b/forge-gui/res/cardsfolder/p/pledge_of_loyalty.txt index 542346100ee..2ff511602d5 100644 --- a/forge-gui/res/cardsfolder/p/pledge_of_loyalty.txt +++ b/forge-gui/res/cardsfolder/p/pledge_of_loyalty.txt @@ -3,15 +3,6 @@ ManaCost:1 W Types:Enchantment Aura K:Enchant creature A:SP$ Attach | Cost$ 1 W | ValidTgts$ Creature | AILogic$ Pump -S:Mode$ Continuous | Affected$ Card.EnchantedBy | AddKeyword$ Protection:Card.White:Protection from white:Card.CardUID_HostCardUID | CheckSVar$ WHITE | SVarCompare$ GE1 | Description$ Enchanted creature has protection from the colors of permanents you control. This effect doesn't remove CARDNAME. -S:Mode$ Continuous | Affected$ Card.EnchantedBy | AddKeyword$ Protection:Card.Blue:Protection from blue:Card.CardUID_HostCardUID | CheckSVar$ BLUE | SVarCompare$ GE1 -S:Mode$ Continuous | Affected$ Card.EnchantedBy | AddKeyword$ Protection:Card.Black:Protection from black:Card.CardUID_HostCardUID | CheckSVar$ BLACK | SVarCompare$ GE1 -S:Mode$ Continuous | Affected$ Card.EnchantedBy | AddKeyword$ Protection:Card.Red:Protection from red:Card.CardUID_HostCardUID | CheckSVar$ RED | SVarCompare$ GE1 -S:Mode$ Continuous | Affected$ Card.EnchantedBy | AddKeyword$ Protection:Card.Green:Protection from green:Card.CardUID_HostCardUID | CheckSVar$ GREEN | SVarCompare$ GE1 -SVar:WHITE:Count$Valid Permanent.White+YouCtrl -SVar:BLUE:Count$Valid Permanent.Blue+YouCtrl -SVar:BLACK:Count$Valid Permanent.Black+YouCtrl -SVar:RED:Count$Valid Permanent.Red+YouCtrl -SVar:GREEN:Count$Valid Permanent.Green+YouCtrl +S:Mode$ Continuous | Affected$ Card.EnchantedBy | AddKeyword$ Protection:Card.ColorsYouCtrl:Protection from colorsYouCtrl:Card.CardUID_HostCardUID | Description$ Enchanted creature has protection from the colors of permanents you control. This effect doesn't remove CARDNAME. SVar:Picture:http://www.wizards.com/global/images/magic/general/pledge_of_loyalty.jpg Oracle:Enchant creature\nEnchanted creature has protection from the colors of permanents you control. This effect doesn't remove Pledge of Loyalty. diff --git a/forge-gui/res/cardsfolder/t/true_name_nemesis.txt b/forge-gui/res/cardsfolder/t/true_name_nemesis.txt index 192a4e99ba5..33de606f51e 100644 --- a/forge-gui/res/cardsfolder/t/true_name_nemesis.txt +++ b/forge-gui/res/cardsfolder/t/true_name_nemesis.txt @@ -4,6 +4,6 @@ Types:Creature Merfolk Rogue PT:3/1 K:ETBReplacement:Other:ChooseP SVar:ChooseP:DB$ ChoosePlayer | Defined$ You | Choices$ Player | AILogic$ Curse | SpellDescription$ As CARDNAME enters the battlefield, choose a player. -S:Mode$ Continuous | AddKeyword$ Protection from the chosen player | Affected$ Card.Self | Description$ CARDNAME has protection from that player. (This creature can't be blocked, targeted, dealt damage, or enchanted by anything controlled by that player.) +S:Mode$ Continuous | AddKeyword$ Protection:Player.PlayerUID_ChosenPlayerUID:Protection from ChosenPlayerName | Affected$ Card.Self | Description$ CARDNAME has protection from that player. (This creature can't be blocked, targeted, dealt damage, or enchanted by anything controlled by that player.) SVar:Picture:http://www.wizards.com/global/images/magic/general/true_name_nemesis.jpg Oracle:As True-Name Nemesis enters the battlefield, choose a player.\nTrue-Name Nemesis has protection from the chosen player. (This creature can't be blocked, targeted, dealt damage, or enchanted by anything controlled by that player.)