diff --git a/.gitattributes b/.gitattributes index f015049affb..53b9c7dce07 100644 --- a/.gitattributes +++ b/.gitattributes @@ -307,6 +307,7 @@ forge-game/src/main/java/forge/game/ability/effects/BecomesBlockedEffect.java -t forge-game/src/main/java/forge/game/ability/effects/BidLifeEffect.java -text forge-game/src/main/java/forge/game/ability/effects/BondEffect.java -text forge-game/src/main/java/forge/game/ability/effects/ChangeTargetsEffect.java -text +forge-game/src/main/java/forge/game/ability/effects/ChangeTextEffect.java -text forge-game/src/main/java/forge/game/ability/effects/ChangeZoneAllEffect.java -text forge-game/src/main/java/forge/game/ability/effects/ChangeZoneEffect.java -text forge-game/src/main/java/forge/game/ability/effects/CharmEffect.java -text @@ -422,6 +423,8 @@ forge-game/src/main/java/forge/game/ability/effects/VoteEffect.java -text forge-game/src/main/java/forge/game/ability/effects/ZoneExchangeEffect.java -text forge-game/src/main/java/forge/game/ability/package-info.java svneol=native#text/plain forge-game/src/main/java/forge/game/card/Card.java svneol=native#text/plain +forge-game/src/main/java/forge/game/card/CardChangedWord.java -text +forge-game/src/main/java/forge/game/card/CardChangedWords.java -text forge-game/src/main/java/forge/game/card/CardCharacteristics.java -text forge-game/src/main/java/forge/game/card/CardColor.java svneol=native#text/plain forge-game/src/main/java/forge/game/card/CardDamageHistory.java -text diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index a45ea73d7fc..cafb8ba1704 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -481,7 +481,7 @@ public class AiController { // don't play the land if it has cycling and enough lands are // available - final ArrayList spellAbilities = c.getSpellAbilities(); + final List spellAbilities = c.getSpellAbilities(); final List hand = player.getCardsIn(ZoneType.Hand); List lands = player.getCardsIn(ZoneType.Battlefield); diff --git a/forge-core/src/main/java/forge/util/TextUtil.java b/forge-core/src/main/java/forge/util/TextUtil.java index a2196c8210e..e479f64707e 100644 --- a/forge-core/src/main/java/forge/util/TextUtil.java +++ b/forge-core/src/main/java/forge/util/TextUtil.java @@ -178,4 +178,9 @@ public class TextUtil { } return builder.toString(); } + + public static String capitalize(final String s) { + return s.substring(0, 1).toUpperCase() + + s.substring(1); + } } diff --git a/forge-game/src/main/java/forge/game/CardTraitBase.java b/forge-game/src/main/java/forge/game/CardTraitBase.java index 12d5ad5772e..aa2a623b880 100644 --- a/forge-game/src/main/java/forge/game/CardTraitBase.java +++ b/forge-game/src/main/java/forge/game/CardTraitBase.java @@ -12,6 +12,8 @@ import forge.util.Expressions; import java.util.*; +import com.google.common.collect.Maps; + /** * Base class for Triggers,ReplacementEffects and StaticAbilities. * @@ -22,7 +24,8 @@ public abstract class CardTraitBase extends GameObject { protected Card hostCard; /** The map params. */ - protected final Map mapParams = new HashMap(); + protected final Map originalMapParams = Maps.newHashMap(), + mapParams = Maps.newHashMap(); /** The is intrinsic. */ protected boolean intrinsic; @@ -66,11 +69,6 @@ public abstract class CardTraitBase extends GameObject { return this.mapParams; } - public final void setMapParams(Map params) { - this.mapParams.clear(); - this.mapParams.putAll(params); - } - /** * Checks if is intrinsic. * @@ -350,4 +348,15 @@ public abstract class CardTraitBase extends GameObject { } return true; } + + public void changeText() { + for (final String key : this.mapParams.keySet()) { + // don't change literal SVar names! + if (!this.getHostCard().hasSVar(key)) { + final String value = this.originalMapParams.get(key), + newValue = AbilityUtils.applyTextChangeEffects(value, this.getHostCard()); + this.mapParams.put(key, newValue); + } + } + } } diff --git a/forge-game/src/main/java/forge/game/ability/AbilityApiBased.java b/forge-game/src/main/java/forge/game/ability/AbilityApiBased.java index 87f2a443815..702e45f67db 100644 --- a/forge-game/src/main/java/forge/game/ability/AbilityApiBased.java +++ b/forge-game/src/main/java/forge/game/ability/AbilityApiBased.java @@ -20,6 +20,7 @@ public class AbilityApiBased extends AbilityActivated { public AbilityApiBased(ApiType api0, Card sourceCard, Cost abCost, TargetRestrictions tgt, Map params0) { super(sourceCard, abCost, tgt); + originalMapParams.putAll(params0); mapParams.putAll(params0); api = api0; effect = api.getSpellEffect(); diff --git a/forge-game/src/main/java/forge/game/ability/AbilityUtils.java b/forge-game/src/main/java/forge/game/ability/AbilityUtils.java index d834571233c..6a3b9332f4f 100644 --- a/forge-game/src/main/java/forge/game/ability/AbilityUtils.java +++ b/forge-game/src/main/java/forge/game/ability/AbilityUtils.java @@ -25,6 +25,7 @@ import org.apache.commons.lang3.text.WordUtils; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -79,7 +80,7 @@ public class AbilityUtils { @SuppressWarnings("unchecked") public static List getDefinedCards(final Card hostCard, final String def, final SpellAbility sa) { final List cards = new ArrayList(); - final String defined = (def == null) ? "Self" : def; // default to Self + final String defined = (def == null) ? "Self" : applyTextChangeEffects(def, hostCard); // default to Self final Game game = hostCard.getGame(); Card c = null; @@ -351,34 +352,37 @@ public class AbilityUtils { // return result soon for plain numbers if (StringUtils.isNumeric(amount)) { return Integer.parseInt(amount) * multiplier; } + // modify amount string for text changes + final String amount2 = AbilityUtils.applyTextChangeEffects(amount, card); + // Try to fetch variable, try ability first, then card. String svarval = null; - if (amount.indexOf('$') > 0) { // when there is a dollar sign, it's not a reference, it's a raw value! - svarval = amount; + if (amount2.indexOf('$') > 0) { // when there is a dollar sign, it's not a reference, it's a raw value! + svarval = amount2; } else if (ability != null) { - svarval = ability.getSVar(amount); + svarval = ability.getSVar(amount2); } if (StringUtils.isBlank(svarval)) { if (ability != null) { - System.err.printf("SVar '%s' not found in ability, fallback to Card (%s). Ability is (%s)%n", amount, card.getName(), ability); + System.err.printf("SVar '%s' not found in ability, fallback to Card (%s). Ability is (%s)%n", amount2, card.getName(), ability); } - svarval = card.getSVar(amount); + svarval = card.getSVar(amount2); } if (StringUtils.isBlank(svarval)) { // Some variables may be not chosen yet at this moment // So return 0 and don't issue an error. - if (amount.equals("ChosenX")) { + if (amount2.equals("ChosenX")) { // isn't made yet return 0; } // cost hasn't been paid yet - if (amount.startsWith("Cost")) { + if (amount2.startsWith("Cost")) { return 0; } // Nothing to do here if value is missing or blank - System.err.printf("SVar '%s' not defined in Card (%s)%n", amount, card.getName()); + System.err.printf("SVar '%s' not defined in Card (%s)%n", amount2, card.getName()); return 0; } @@ -395,6 +399,9 @@ public class AbilityUtils { return 0; } + // modify amount string for text changes + calcX[1] = AbilityUtils.applyTextChangeEffects(calcX[1], card); + if (calcX[0].startsWith("Count")) { return AbilityUtils.xCount(card, calcX[1], ability) * multiplier; } @@ -806,7 +813,7 @@ public class AbilityUtils { */ public static List getDefinedPlayers(final Card card, final String def, final SpellAbility sa) { final List players = new ArrayList(); - final String defined = (def == null) ? "You" : def; + final String defined = (def == null) ? "You" : applyTextChangeEffects(def, card); final Game game = card == null ? null : card.getGame(); if (defined.equals("Targeted") || defined.equals("TargetedPlayer")) { @@ -1125,7 +1132,7 @@ public class AbilityUtils { public static ArrayList getDefinedSpellAbilities(final Card card, final String def, final SpellAbility sa) { final ArrayList sas = new ArrayList(); - final String defined = (def == null) ? "Self" : def; // default to Self + final String defined = (def == null) ? "Self" : applyTextChangeEffects(def, card); // default to Self final Game game = sa.getActivatingPlayer().getGame(); SpellAbility s = null; @@ -1398,9 +1405,9 @@ public class AbilityUtils { * @return a int. */ public static int xCount(final Card c, final String s, final SpellAbility sa) { - - final String[] l = s.split("/"); - final String expr = CardFactoryUtil.extractOperators(s); + final String s2 = AbilityUtils.applyTextChangeEffects(s, c); + final String[] l = s2.split("/"); + final String expr = CardFactoryUtil.extractOperators(s2); final String[] sq; sq = l[0].split("\\."); @@ -1459,7 +1466,7 @@ public class AbilityUtils { } } } - return CardFactoryUtil.xCount(c, s); + return CardFactoryUtil.xCount(c, s2); } public static final void applyManaColorConversion(final Player p, final Map params) { @@ -1506,4 +1513,15 @@ public class AbilityUtils { } return sas; } + + public static final String applyTextChangeEffects(final String def, final Card hostCard) { + String replaced = def; + for (final Entry e : hostCard.getChangedTextColorWords().entrySet()) { + replaced = replaced.replace(e.getKey(), e.getValue()); + } + for (final Entry e : hostCard.getChangedTextTypeWords().entrySet()) { + replaced = replaced.replace(e.getKey(), e.getValue()); + } + return replaced; + } } diff --git a/forge-game/src/main/java/forge/game/ability/ApiType.java b/forge-game/src/main/java/forge/game/ability/ApiType.java index dc21c12e0c4..864f89ef219 100644 --- a/forge-game/src/main/java/forge/game/ability/ApiType.java +++ b/forge-game/src/main/java/forge/game/ability/ApiType.java @@ -25,9 +25,9 @@ public enum ApiType { BidLife (BidLifeEffect.class), Bond (BondEffect.class), ChangeTargets (ChangeTargetsEffect.class), + ChangeText (ChangeTextEffect.class), ChangeZone (ChangeZoneEffect.class), ChangeZoneAll (ChangeZoneAllEffect.class), - Charm (CharmEffect.class), ChooseCard (ChooseCardEffect.class), ChooseColor (ChooseColorEffect.class), diff --git a/forge-game/src/main/java/forge/game/ability/SpellApiBased.java b/forge-game/src/main/java/forge/game/ability/SpellApiBased.java index ba4c3020548..d8dcc87be93 100644 --- a/forge-game/src/main/java/forge/game/ability/SpellApiBased.java +++ b/forge-game/src/main/java/forge/game/ability/SpellApiBased.java @@ -19,7 +19,8 @@ public class SpellApiBased extends Spell { public SpellApiBased(ApiType api0, Card sourceCard, Cost abCost, TargetRestrictions tgt, Map params0) { super(sourceCard, abCost); this.setTargetRestrictions(tgt); - + + originalMapParams.putAll(params0); mapParams.putAll(params0); api = api0; effect = api.getSpellEffect(); diff --git a/forge-game/src/main/java/forge/game/ability/StaticAbilityApiBased.java b/forge-game/src/main/java/forge/game/ability/StaticAbilityApiBased.java index d85057207d8..4487536d5db 100644 --- a/forge-game/src/main/java/forge/game/ability/StaticAbilityApiBased.java +++ b/forge-game/src/main/java/forge/game/ability/StaticAbilityApiBased.java @@ -15,6 +15,7 @@ public class StaticAbilityApiBased extends AbilityStatic { public StaticAbilityApiBased(ApiType api0, Card sourceCard, Cost abCost, TargetRestrictions tgt, Map params0) { super(sourceCard, abCost, tgt); + originalMapParams.putAll(params0); mapParams.putAll(params0); api = api0; effect = api.getSpellEffect(); diff --git a/forge-game/src/main/java/forge/game/ability/effects/ChangeTextEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ChangeTextEffect.java new file mode 100644 index 00000000000..e0d4cdbacac --- /dev/null +++ b/forge-game/src/main/java/forge/game/ability/effects/ChangeTextEffect.java @@ -0,0 +1,235 @@ +package forge.game.ability.effects; + +import java.util.List; + +import com.google.common.collect.Lists; + +import forge.GameCommand; +import forge.card.CardType; +import forge.card.ColorSet; +import forge.card.MagicColor; +import forge.game.Game; +import forge.game.ability.SpellAbilityEffect; +import forge.game.card.Card; +import forge.game.event.GameEventCardStatsChanged; +import forge.game.spellability.SpellAbility; +import forge.util.TextUtil; + +public class ChangeTextEffect extends SpellAbilityEffect { + + /* (non-Javadoc) + * @see forge.card.abilityfactory.SpellEffect#resolve(forge.card.spellability.SpellAbility) + */ + @Override + public void resolve(final SpellAbility sa) { + final Card source = sa.getHostCard(); + final Game game = source.getGame(); + final boolean permanent = sa.hasParam("Permanent"); + + final String changedColorWordOriginal, changedColorWordNew; + if (sa.hasParam("ChangeColorWord")) { + byte originalColor = 0; + final String[] changedColorWordsArray = sa.getParam("ChangeColorWord").split(" "); + if (changedColorWordsArray[0].equals("Choose")) { + originalColor = sa.getActivatingPlayer().getController().chooseColor( + "Choose a color word to replace", sa, ColorSet.fromMask(MagicColor.ALL_COLORS)); + changedColorWordOriginal = TextUtil.capitalize(MagicColor.toLongString(originalColor)); + } else { + changedColorWordOriginal = changedColorWordsArray[0]; + originalColor = MagicColor.fromName(changedColorWordOriginal); + } + + if (changedColorWordsArray[1].equals("Choose")) { + final ColorSet possibleNewColors; + if (originalColor == 0) { // no original color (ie. any or absent) + possibleNewColors = ColorSet.fromMask(MagicColor.ALL_COLORS); + } else { // may choose any except original color + possibleNewColors = ColorSet.fromMask(originalColor).inverse(); + } + final byte newColor = sa.getActivatingPlayer().getController().chooseColor( + "Choose a new color word", sa, possibleNewColors); + changedColorWordNew = TextUtil.capitalize(MagicColor.toLongString(newColor)); + } else { + changedColorWordNew = changedColorWordsArray[1]; + } + } else { + changedColorWordOriginal = null; + changedColorWordNew = null; + } + + final String changedTypeWordOriginal, changedTypeWordNew; + if (sa.hasParam("ChangeTypeWord")) { + String kindOfType = ""; + final List validTypes = Lists.newArrayList(); + final String[] changedTypeWordsArray = sa.getParam("ChangeTypeWord").split(" "); + if (changedTypeWordsArray[0].equals("ChooseBasicLandType") || changedTypeWordsArray[0].equals("ChooseCreatureType")) { + if (changedTypeWordsArray[0].equals("ChooseBasicLandType")) { + validTypes.addAll(CardType.getBasicTypes()); + kindOfType = "basic land"; + } else if (changedTypeWordsArray[0].equals("ChooseCreatureType")) { + validTypes.addAll(CardType.getCreatureTypes()); + kindOfType = "creature"; + } + changedTypeWordOriginal = sa.getActivatingPlayer().getController().chooseSomeType(kindOfType, sa, validTypes, Lists.newArrayList()); + } else { + changedTypeWordOriginal = changedTypeWordsArray[0]; + } + + validTypes.clear(); + final List forbiddenTypes = sa.hasParam("ForbiddenNewTypes") ? Lists.newArrayList(sa.getParam("ForbiddenNewTypes").split(",")) : Lists.newArrayList(); + forbiddenTypes.add(changedTypeWordOriginal); + if (changedTypeWordsArray[0].startsWith("Choose")) { + if (changedTypeWordsArray[0].equals("ChooseBasicLandType")) { + validTypes.addAll(CardType.getBasicTypes()); + kindOfType = "basic land"; + } else if (changedTypeWordsArray[0].equals("ChooseCreatureType")) { + validTypes.addAll(CardType.getCreatureTypes()); + kindOfType = "creature"; + } + changedTypeWordNew = sa.getActivatingPlayer().getController().chooseSomeType(kindOfType, sa, validTypes, forbiddenTypes); + } else { + changedTypeWordNew = changedTypeWordsArray[1]; + } + } else { + changedTypeWordOriginal = null; + changedTypeWordNew = null; + } + + final List tgts = getTargetCards(sa); + for (final Card c : tgts) { + final Long colorTimestamp; + if (changedColorWordNew != null) { + colorTimestamp = c.addChangedTextColorWord(changedColorWordOriginal, changedColorWordNew); + } else { + colorTimestamp = null; + } + + final Long typeTimestamp; + if (changedTypeWordNew != null) { + typeTimestamp = c.addChangedTextTypeWord(changedTypeWordOriginal, changedTypeWordNew); + } else { + typeTimestamp = null; + } + + if (!permanent) { + final GameCommand revert = new GameCommand() { + private static final long serialVersionUID = -7802388880114360593L; + @Override + public void run() { + if (changedColorWordNew != null) { + c.removeChangedTextColorWord(colorTimestamp); + } + if (changedTypeWordNew != null) { + c.removeChangedTextTypeWord(typeTimestamp); + } + } + }; + game.getEndOfTurn().addUntil(revert); + } + + game.fireEvent(new GameEventCardStatsChanged(c)); + } + } + + /* (non-Javadoc) + * @see forge.card.abilityfactory.SpellEffect#getStackDescription(java.util.Map, forge.card.spellability.SpellAbility) + */ + @Override + protected String getStackDescription(SpellAbility sa) { + final String changedColorWordOriginal, changedColorWordNew; + if (sa.hasParam("ChangeColorWord")) { + final String[] changedColorWordsArray = sa.getParam("ChangeColorWord").split(" "); + changedColorWordOriginal = changedColorWordsArray[0]; + changedColorWordNew = changedColorWordsArray[1]; + } else { + changedColorWordOriginal = null; + changedColorWordNew = null; + } + + final String changedTypeWordOriginal, changedTypeWordNew; + if (sa.hasParam("ChangeTypeWord")) { + final String[] changedTypeWordsArray = sa.getParam("ChangeTypeWord").split(" "); + changedTypeWordOriginal = changedTypeWordsArray[0]; + changedTypeWordNew = changedTypeWordsArray[1]; + } else { + changedTypeWordOriginal = null; + changedTypeWordNew = null; + } + + final boolean permanent = sa.hasParam("Permanent"); + + final StringBuilder sb = new StringBuilder(); + sb.append("Change the text of "); + + final List tgts = getTargetCards(sa); + for (final Card c : tgts) { + sb.append(c).append(" "); + } + + if (changedColorWordOriginal != null) { + sb.append(" by replacing all instances of "); + if (changedColorWordOriginal.equals("Choose")) { + sb.append("one color word"); + } else if (changedColorWordOriginal.equals("Any")) { + sb.append("each color word"); + } else { + sb.append(changedColorWordOriginal); + } + sb.append(" with "); + if (changedColorWordNew.equals("Choose")) { + if (changedColorWordOriginal.equals("Choose")) { + sb.append("another"); + } else { + sb.append("a color word of your choice"); + } + } else { + sb.append(changedColorWordNew); + } + } + + if (changedTypeWordOriginal != null) { + sb.append(" by replacing all instances of "); + if (changedTypeWordOriginal.equals("ChooseBasicLandType")) { + sb.append("one basic land type"); + } else if (changedTypeWordOriginal.equals("ChooseCreatureType")) { + sb.append("one creature type"); + } else { + sb.append(changedTypeWordOriginal); + } + sb.append(" with "); + if (changedTypeWordNew.equals("ChooseBasicLandType")) { + if (changedTypeWordOriginal.equals("ChooseBasicLandType")) { + sb.append("another"); + } else { + sb.append("a basic land type of your choice"); + } + } else if (changedTypeWordNew.equals("ChooseCreatureType")) { + if (changedTypeWordOriginal.equals("ChooseCreatureType")) { + sb.append("another"); + } else { + sb.append("a creature type of your choice"); + } + } else { + sb.append(changedTypeWordNew); + } + } + + if (!permanent) { + sb.append(" until end of turn"); + } + sb.append('.'); + + if (sa.hasParam("ForbiddenNewTypes")) { + sb.append(" The new creature type can't be "); + sb.append(sa.getParam("ForbiddenNewTypes")); + sb.append('.'); + } + + if (permanent) { + sb.append(" (This effect lasts indefinitely.)"); + } + + return sb.toString(); + } + +} diff --git a/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneEffect.java index 80929059a70..f8dcd0f837a 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneEffect.java @@ -518,7 +518,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect { movedCard = game.getAction().moveTo(destination, tgtC); // If a card is Exiled from the stack, remove its spells from the stack if (sa.hasParam("Fizzle")) { - ArrayList spells = tgtC.getSpellAbilities(); + final List spells = tgtC.getSpellAbilities(); for (SpellAbility spell : spells) { if (tgtC.isInZone(ZoneType.Exile) || tgtC.isInZone(ZoneType.Hand)) { final SpellAbilityStackInstance si = game.getStack().getInstanceFromSpellAbility(spell); 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 ad6c7f975df..88f3929d65c 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -20,7 +20,9 @@ package forge.game.card; import com.esotericsoftware.minlog.Log; import com.google.common.base.Function; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; +import com.google.common.collect.Sets; import forge.GameCommand; import forge.StaticData; @@ -118,6 +120,12 @@ public class Card extends GameEntity implements Comparable { private Map changedCardTypes = new ConcurrentSkipListMap(); private Map changedCardKeywords = new ConcurrentSkipListMap(); + // changes that say "replace each instance of one [color,type] by another - timestamp is the key of maps + private final CardChangedWords changedTextColors = new CardChangedWords(); + private final CardChangedWords changedTextTypes = new CardChangedWords(); + /** List of the keywords that have been added by text changes. */ + private final List keywordsGrantedByTextChanges = Lists.newArrayList(); + private final ArrayList rememberedObjects = new ArrayList(); private final MapOfLists rememberMap = new HashMapOfLists(CollectionSuppliers.arrayLists()); private final ArrayList imprintedCards = new ArrayList(); @@ -2389,6 +2397,18 @@ public class Card extends GameEntity implements Comparable { } } + for (final Entry e : Sets.union(this.changedTextColors.toMap().entrySet(), + this.changedTextTypes.toMap().entrySet())) { + // only the upper case ones, to avoid duplicity + if (Character.isUpperCase(e.getKey().charAt(0))) { + sb.append("Text changed: all instances of "); + sb.append(e.getKey()); + sb.append(" are replaced by "); + sb.append(e.getValue()); + sb.append(".\r\n"); + } + } + // NOTE: if (sb.toString().contains(" (NOTE: ")) { sb.insert(sb.indexOf("(NOTE: "), "\r\n"); @@ -2594,7 +2614,6 @@ public class Card extends GameEntity implements Comparable { return Collections.unmodifiableList(this.getCharacteristics().getManaAbility()); } - public final boolean canProduceSameManaTypeWith(final Card c) { final List manaAb = this.getManaAbility(); if (manaAb.isEmpty()) { @@ -2691,7 +2710,6 @@ public class Card extends GameEntity implements Comparable { * a {@link forge.game.spellability.SpellAbility} object. */ public final void addSpellAbility(final SpellAbility a) { - a.setHostCard(this); if (a.isManaAbility()) { this.getCharacteristics().getManaAbility().add(a); @@ -2700,7 +2718,6 @@ public class Card extends GameEntity implements Comparable { } } - /** *

* removeSpellAbility. @@ -2726,7 +2743,7 @@ public class Card extends GameEntity implements Comparable { * * @return a {@link java.util.ArrayList} object. */ - public final ArrayList getSpellAbilities() { + public final List getSpellAbilities() { final ArrayList res = new ArrayList(this.getManaAbility()); res.addAll(this.getCharacteristics().getSpellAbility()); return res; @@ -4483,9 +4500,10 @@ public class Card extends GameEntity implements Comparable { * * @param timestamp * the timestamp + * @return the removed {@link CardKeywords}. */ - public final void removeChangedCardKeywords(final long timestamp) { - changedCardKeywords.remove(Long.valueOf(timestamp)); + public final CardKeywords removeChangedCardKeywords(final long timestamp) { + return changedCardKeywords.remove(Long.valueOf(timestamp)); } // Hidden keywords will be left out @@ -4518,6 +4536,97 @@ public class Card extends GameEntity implements Comparable { return keywords; } + /** + * Replace all instances of one color word in this card's text by another. + * @param originalWord the original color word. + * @param newWord the new color word. + * @return the timestamp. + * @throws RuntimeException if either of the strings is not a valid Magic + * color. + */ + public final Long addChangedTextColorWord(final String originalWord, final String newWord) { + if (MagicColor.fromName(newWord) == 0) { + throw new RuntimeException("Not a color: " + newWord); + } + final Long timestamp = this.changedTextColors.add(this.getGame().getNextTimestamp(), originalWord, newWord); + this.updateKeywordsChangedText(originalWord, newWord, timestamp); + this.updateChangedText(); + return timestamp; + } + + public final void removeChangedTextColorWord(final Long timestamp) { + this.changedTextColors.remove(timestamp); + this.updateKeywordsOnRemoveChangedText( + this.removeChangedCardKeywords(timestamp.longValue())); + this.updateChangedText(); + } + + /** + * Replace all instances of one type in this card's text by another. + * @param originalWord the original type word. + * @param newWord the new type word. + */ + public final Long addChangedTextTypeWord(final String originalWord, final String newWord) { + final Long timestamp = this.changedTextTypes.add(this.getGame().getNextTimestamp(), originalWord, newWord); + if (this.getType().contains(originalWord)) { + this.addChangedCardTypes(Lists.newArrayList(newWord), Lists.newArrayList(originalWord), false, false, false, false, timestamp); + } + this.updateKeywordsChangedText(originalWord, newWord, timestamp); + this.updateChangedText(); + return timestamp; + } + + public final void removeChangedTextTypeWord(final Long timestamp) { + this.changedTextTypes.remove(timestamp); + this.removeChangedCardTypes(timestamp); + this.updateKeywordsOnRemoveChangedText( + this.removeChangedCardKeywords(timestamp.longValue())); + this.updateChangedText(); + } + + private final void updateKeywordsChangedText(final String originalWord, final String newWord, final Long timestamp) { + final List addKeywords = Lists.newArrayList(), + removeKeywords = Lists.newArrayList(this.keywordsGrantedByTextChanges); + + for (final String kw : this.getIntrinsicKeyword()) { + final String newKw = AbilityUtils.applyTextChangeEffects(kw, this); + if (!newKw.equals(kw)) { + addKeywords.add(newKw); + removeKeywords.add(kw); + this.keywordsGrantedByTextChanges.add(newKw); + } + } + this.addChangedCardKeywords(addKeywords, removeKeywords, false, timestamp.longValue()); + } + + private final void updateKeywordsOnRemoveChangedText(final CardKeywords k) { + this.keywordsGrantedByTextChanges.removeAll(k.getKeywords()); + } + + /** + * Update the changed text of the intrinsic spell abilities and keywords. + */ + private final void updateChangedText() { + final List allAbs = Lists.newLinkedList(); + allAbs.addAll(this.getSpellAbilities()); + allAbs.addAll(this.getStaticAbilities()); + allAbs.addAll(this.getReplacementEffects()); + allAbs.addAll(this.getTriggers()); + for (final CardTraitBase ctb : allAbs) { + if (ctb.isIntrinsic()) { + ctb.changeText(); + } + } + } + + public final ImmutableMap getChangedTextColorWords() { + return ImmutableMap.copyOf(changedTextColors.toMap()); + } + + public final ImmutableMap getChangedTextTypeWords() { + return ImmutableMap.copyOf(changedTextTypes.toMap()); + } + /** *

* getIntrinsicAbilities. @@ -4542,19 +4651,6 @@ public class Card extends GameEntity implements Comparable { return this.getCharacteristics().getIntrinsicKeyword(); } - /** - *

- * Setter for the field intrinsicKeyword. - *

- * - * @param a - * a {@link java.util.ArrayList} object. - */ - public final void setIntrinsicKeyword(final List a) { - this.getCharacteristics().setIntrinsicKeyword(new ArrayList(a)); - } - - /** *

* setIntrinsicAbilities. diff --git a/forge-game/src/main/java/forge/game/card/CardChangedWord.java b/forge-game/src/main/java/forge/game/card/CardChangedWord.java new file mode 100644 index 00000000000..d1a1590bcca --- /dev/null +++ b/forge-game/src/main/java/forge/game/card/CardChangedWord.java @@ -0,0 +1,21 @@ +package forge.game.card; + +public class CardChangedWord { + + private final String originalWord, + newWord; + + public CardChangedWord(final String originalWord, final String newWord) { + this.originalWord = originalWord; + this.newWord = newWord; + } + + public String getOriginalWord() { + return originalWord; + } + + public String getNewWord() { + return newWord; + } + +} diff --git a/forge-game/src/main/java/forge/game/card/CardChangedWords.java b/forge-game/src/main/java/forge/game/card/CardChangedWords.java new file mode 100644 index 00000000000..0a7ba531a90 --- /dev/null +++ b/forge-game/src/main/java/forge/game/card/CardChangedWords.java @@ -0,0 +1,61 @@ +package forge.game.card; + +import java.util.Map; +import java.util.Map.Entry; +import java.util.SortedMap; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Maps; + +public final class CardChangedWords { + + private final SortedMap map = Maps.newTreeMap(); + + private boolean isDirty = false; + private Map resultCache = Maps.newHashMap(); + + public CardChangedWords() { + } + + public Long add(final long timestamp, final String originalWord, final String newWord) { + final Long stamp = Long.valueOf(timestamp); + map.put(stamp, new CardChangedWord(originalWord, newWord)); + isDirty = true; + return stamp; + } + + public void remove(final Long timestamp) { + map.remove(timestamp); + isDirty = true; + } + + /** + * Converts this object to a {@link Map}. + * + * @return a map of strings to strings, where each changed word in this + * object is mapped to its corresponding replacement word. + */ + public Map toMap() { + refreshCache(); + return resultCache; + } + + private void refreshCache() { + if (isDirty) { + resultCache = Maps.newHashMap(); + for (final CardChangedWord ccw : this.map.values()) { + for (final Entry e : resultCache.entrySet()) { + if (e.getValue().equals(ccw.getOriginalWord())) { + e.setValue(ccw.getNewWord()); + } + } + + resultCache.put(ccw.getOriginalWord(), ccw.getNewWord()); + } + for (final String key : ImmutableList.copyOf(resultCache.keySet())) { + resultCache.put(key.toLowerCase(), resultCache.get(key).toLowerCase()); + } + isDirty = false; + } + } +} diff --git a/forge-game/src/main/java/forge/game/card/CardFactory.java b/forge-game/src/main/java/forge/game/card/CardFactory.java index 62e6eee34ef..79ce7037cbc 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactory.java +++ b/forge-game/src/main/java/forge/game/card/CardFactory.java @@ -31,6 +31,7 @@ import forge.game.cost.Cost; import forge.game.player.Player; import forge.game.replacement.ReplacementHandler; import forge.game.spellability.*; +import forge.game.staticability.StaticAbility; import forge.game.trigger.Trigger; import forge.game.trigger.TriggerHandler; import forge.game.trigger.WrappedAbility; @@ -288,11 +289,11 @@ public class CardFactory { card.setState(state); CardFactoryUtil.addAbilityFactoryAbilities(card); for (String stAb : card.getStaticAbilityStrings()) { - card.addStaticAbility(stAb); + final StaticAbility s = card.addStaticAbility(stAb); + s.setIntrinsic(true); } - - if ( state == CardCharacteristicName.LeftSplit || state == CardCharacteristicName.RightSplit ) + if (state == CardCharacteristicName.LeftSplit || state == CardCharacteristicName.RightSplit) { CardCharacteristics original = card.getState(CardCharacteristicName.Original); original.getSpellAbility().addAll(card.getCharacteristics().getSpellAbility()); 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 57637d90e5f..c97f4a96bcf 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java +++ b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java @@ -681,8 +681,8 @@ public class CardFactoryUtil { return doXMath(n, m, source); } - - public static int playerXProperty(Player player, String s, Card source) { + + public static int playerXProperty(final Player player, final String s, final Card source) { final String[] l = s.split("/"); final String m = extractOperators(s); @@ -824,7 +824,7 @@ public class CardFactoryUtil { final Player cc = c.getController(); final Game game = c.getGame(); final Player activePlayer = game.getPhaseHandler().getPlayerTurn(); - + final String[] l = expression.split("/"); final String m = extractOperators(expression); @@ -845,7 +845,7 @@ public class CardFactoryUtil { if (l[0].startsWith("SVar$")) { return doXMath(xCount(c, c.getSVar(l[0].substring(5))), m, c); } - + if (l[0].startsWith("Controller$")) return playerXProperty(cc, l[0].substring(11), c); @@ -1975,7 +1975,9 @@ public class CardFactoryUtil { // ************************************************** // AbilityFactory cards for (String rawAbility : card.getUnparsedAbilities()) { - card.addSpellAbility(AbilityFactory.getAbility(rawAbility, card)); + final SpellAbility intrinsicAbility = AbilityFactory.getAbility(rawAbility, card); + card.addSpellAbility(intrinsicAbility); + intrinsicAbility.setIntrinsic(true); } } /* diff --git a/forge-game/src/main/java/forge/game/replacement/ReplacementEffect.java b/forge-game/src/main/java/forge/game/replacement/ReplacementEffect.java index 84390282669..a4c2484be31 100644 --- a/forge-game/src/main/java/forge/game/replacement/ReplacementEffect.java +++ b/forge-game/src/main/java/forge/game/replacement/ReplacementEffect.java @@ -58,6 +58,7 @@ public abstract class ReplacementEffect extends TriggerReplacementBase { */ public ReplacementEffect(final Map map, final Card host, final boolean intrinsic) { this.intrinsic = intrinsic; + originalMapParams.putAll(map); mapParams.putAll(map); this.setHostCard(host); } diff --git a/forge-game/src/main/java/forge/game/replacement/ReplacementHandler.java b/forge-game/src/main/java/forge/game/replacement/ReplacementHandler.java index 1f6b49b4102..664e7f3db22 100644 --- a/forge-game/src/main/java/forge/game/replacement/ReplacementHandler.java +++ b/forge-game/src/main/java/forge/game/replacement/ReplacementHandler.java @@ -192,6 +192,10 @@ public class ReplacementHandler implements IGameStateObject { } while(tailend != null); } + if (effectSA.isIntrinsic()) { + effectSA.changeText(); + } + // Decider gets to choose whether or not to apply the replacement. if (replacementEffect.getMapParams().containsKey("Optional")) { Player optDecider = decider; diff --git a/forge-game/src/main/java/forge/game/spellability/AbilitySub.java b/forge-game/src/main/java/forge/game/spellability/AbilitySub.java index d6093763047..52e1fe459fd 100644 --- a/forge-game/src/main/java/forge/game/spellability/AbilitySub.java +++ b/forge-game/src/main/java/forge/game/spellability/AbilitySub.java @@ -86,8 +86,11 @@ public final class AbilitySub extends SpellAbility implements java.io.Serializab this.setTargetRestrictions(tgt); api = api0; - if (params0 != null) + if (params0 != null) { + originalMapParams.putAll(params0); mapParams.putAll(params0); + } + effect = api.getSpellEffect(); if (effect instanceof ManaEffect || effect instanceof ManaReflectedEffect) { diff --git a/forge-game/src/main/java/forge/game/spellability/SpellAbility.java b/forge-game/src/main/java/forge/game/spellability/SpellAbility.java index e933ed356ca..6c6bd07c36f 100644 --- a/forge-game/src/main/java/forge/game/spellability/SpellAbility.java +++ b/forge-game/src/main/java/forge/game/spellability/SpellAbility.java @@ -1773,4 +1773,17 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit public void setChosenList(List choices) { this.chosenList = choices; } + + @Override + public void changeText() { + super.changeText(); + + if (this.targetRestricions != null) { + this.targetRestricions.applyTargetTextChanges(this); + } + + if (this.subAbility != null) { + this.subAbility.changeText(); + } + } } \ No newline at end of file diff --git a/forge-game/src/main/java/forge/game/spellability/TargetRestrictions.java b/forge-game/src/main/java/forge/game/spellability/TargetRestrictions.java index 2f9994ed48a..88c905c2020 100644 --- a/forge-game/src/main/java/forge/game/spellability/TargetRestrictions.java +++ b/forge-game/src/main/java/forge/game/spellability/TargetRestrictions.java @@ -24,6 +24,7 @@ import forge.game.ability.AbilityUtils; import forge.game.card.Card; import forge.game.player.Player; import forge.game.zone.ZoneType; + import org.apache.commons.lang3.StringUtils; import java.util.ArrayList; @@ -47,7 +48,8 @@ public class TargetRestrictions { // What this Object is restricted to targeting private boolean tgtValid = false; - private String[] validTgts; + private String[] originalValidTgts, + validTgts; private String uiPrompt = ""; private List tgtZone = Arrays.asList(ZoneType.Battlefield); @@ -86,7 +88,8 @@ public class TargetRestrictions { public TargetRestrictions(final TargetRestrictions target) { this.tgtValid = true; this.uiPrompt = target.getVTSelection(); - this.validTgts = target.getValidTgts(); + this.originalValidTgts = target.getValidTgts(); + this.validTgts = this.originalValidTgts.clone(); this.minTargets = target.getMinTargets(); this.maxTargets = target.getMaxTargets(); this.tgtZone = target.getZone(); @@ -120,7 +123,8 @@ public class TargetRestrictions { public TargetRestrictions(final String prompt, final String[] valid, final String min, final String max) { this.tgtValid = true; this.uiPrompt = prompt; - this.validTgts = valid; + this.originalValidTgts = valid; + this.validTgts = this.originalValidTgts.clone(); this.minTargets = min; this.maxTargets = max; } @@ -418,7 +422,9 @@ public class TargetRestrictions { return true; } } - + + this.applyTargetTextChanges(sa); + final Card srcCard = sa.getHostCard(); // should there be OrginalHost at any moment? if (this.tgtZone.contains(ZoneType.Stack)) { // Stack Zone targets are considered later @@ -476,6 +482,8 @@ public class TargetRestrictions { } } + this.applyTargetTextChanges(sa); + final Card srcCard = sa.getHostCard(); // should there be OrginalHost at any moment? if (this.tgtZone.contains(ZoneType.Stack)) { for (final Card c : game.getStackZone().getCards()) { @@ -655,7 +663,7 @@ public class TargetRestrictions { public void setStillToDivide(final int remaining) { this.stillToDivide = remaining; } - + public void calculateStillToDivide(String toDistribute, Card source, SpellAbility sa) { // Recalculate this value just in case it's variable if (!this.dividedAsYouChoose) { @@ -698,4 +706,10 @@ public class TargetRestrictions { return this.dividedMap; } + public final void applyTargetTextChanges(final SpellAbility sa) { + for (int i = 0; i < validTgts.length; i++) { + validTgts[i] = AbilityUtils.applyTextChangeEffects(originalValidTgts[i], sa.getHostCard()); + } + } + } 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 65b89cda6d5..0cb17ce94f3 100644 --- a/forge-game/src/main/java/forge/game/staticability/StaticAbility.java +++ b/forge-game/src/main/java/forge/game/staticability/StaticAbility.java @@ -111,6 +111,10 @@ public class StaticAbility extends CardTraitBase { return 2; } + if (this.mapParams.containsKey("ChangeText")) { + return 3; + } + if (this.mapParams.containsKey("AddType") || this.mapParams.containsKey("RemoveType") || this.mapParams.containsKey("RemoveCardTypes") || this.mapParams.containsKey("RemoveSubTypes") || this.mapParams.containsKey("RemoveSuperTypes") || this.mapParams.containsKey("RemoveCreatureTypes")) { @@ -170,7 +174,9 @@ public class StaticAbility extends CardTraitBase { * the host */ public StaticAbility(final String params, final Card host) { - this.mapParams.putAll(this.parseParams(params, host)); + final Map parsedParams = this.parseParams(params, host); + this.originalMapParams.putAll(parsedParams); + this.mapParams.putAll(parsedParams); this.hostCard = host; this.layer = this.generateLayer(); } @@ -183,10 +189,9 @@ public class StaticAbility extends CardTraitBase { * @param host * the host */ - public StaticAbility(final HashMap params, final Card host) { - for (final Map.Entry entry : params.entrySet()) { - this.mapParams.put(entry.getKey(), entry.getValue()); - } + public StaticAbility(final Map params, final Card host) { + this.originalMapParams.putAll(params); + this.mapParams.putAll(params); this.layer = this.generateLayer(); this.hostCard = host; } diff --git a/forge-game/src/main/java/forge/game/trigger/Trigger.java b/forge-game/src/main/java/forge/game/trigger/Trigger.java index b505767c63c..3b1047e5f35 100644 --- a/forge-game/src/main/java/forge/game/trigger/Trigger.java +++ b/forge-game/src/main/java/forge/game/trigger/Trigger.java @@ -124,6 +124,7 @@ public abstract class Trigger extends TriggerReplacementBase { this.intrinsic = intrinsic; this.setRunParams(new HashMap()); + this.originalMapParams.putAll(params); this.mapParams.putAll(params); this.setHostCard(host); } @@ -441,8 +442,8 @@ public abstract class Trigger extends TriggerReplacementBase { public final Trigger getCopyForHostCard(Card newHost) { - TriggerType tt = TriggerType.getTypeFor(this); - Trigger copy = tt.createTrigger(mapParams, newHost, intrinsic); + final TriggerType tt = TriggerType.getTypeFor(this); + final Trigger copy = tt.createTrigger(originalMapParams, newHost, intrinsic); if (this.getOverridingAbility() != null) { copy.setOverridingAbility(this.getOverridingAbility()); diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerHandler.java b/forge-game/src/main/java/forge/game/trigger/TriggerHandler.java index 99e0e8de232..f6ea6cc17fb 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerHandler.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerHandler.java @@ -390,6 +390,10 @@ public class TriggerHandler implements IGameStateObject { host.addRemembered(sa.getActivatingPlayer()); } + if (regtrig.isIntrinsic()) { + sa.changeText(); + } + sa.setStackDescription(sa.toString()); if (sa.getApi() == ApiType.Charm && !sa.isWrapper()) { CharmEffect.makeChoices(sa);