diff --git a/forge-core/src/main/java/forge/card/mana/ManaCost.java b/forge-core/src/main/java/forge/card/mana/ManaCost.java index ab286301886..f9c123f99f8 100644 --- a/forge-core/src/main/java/forge/card/mana/ManaCost.java +++ b/forge-core/src/main/java/forge/card/mana/ManaCost.java @@ -91,14 +91,18 @@ private void sealClass(List shards0) { */ public ManaCost(final IParserManaCost parser) { final List shardsTemp = Lists.newArrayList(); + boolean xMana = false; while (parser.hasNext()) { final ManaCostShard shard = parser.next(); if (shard != null && shard != ManaCostShard.GENERIC) { + if (shard.toString().equals("{X}")) { + xMana = true; + } shardsTemp.add(shard); } // null is OK - that was generic mana } int generic = parser.getTotalGenericCost(); // collect generic mana here - this.hasNoCost = generic == -1; + this.hasNoCost = !xMana && generic == -1; this.genericCost = hasNoCost ? 0 : generic; sealClass(shardsTemp); } @@ -117,11 +121,15 @@ public String getSimpleString() { } for (final ManaCostShard s : this.shards) { if (s == ManaCostShard.X) { - sb.insert(0, s.toString()); + sb.insert(0, s); } else { sb.append(s.toString()); } } + // If the generic cost has been reduced below 0, display the reduction. (Only set for X cost spells) + if (this.genericCost < 0) { + sb.append(' ').append(this.genericCost); + } return sb.toString(); } @@ -295,6 +303,10 @@ public String getShortString() { sb.append(' '); sb.append(s); } + // If the generic cost has been reduced below 0, display the reduction. (Only set for X cost spells) + if (generic < 0) { + sb.append(' ').append(generic); + } return sb.toString().trim(); } @@ -399,6 +411,12 @@ public int getGlyphCount() { // counts all colored shards or 1 for {0} costs if (genericCost > 0 || (genericCost == 0 && width == 0)) { width++; } + // If the generic cost has been reduced below 0 (due to perpetual cost decrease effects) + // and there is an X cost (so the below 0 generic cost actually does something) then + // add space for an additional symbol to display the extra cost reduction. + if (genericCost < 0 && countX() > 0) { + width++; + } return width; } } 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 9104c097b73..dfaa6646d38 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 @@ -175,9 +175,13 @@ public static void doAnimate(final Card c, final SpellAbility sa, final Integer // give static abilities (should only be used by cards to give // itself a static ability) + boolean costChange = false; final List addedStaticAbilities = Lists.newArrayList(); for (final String s : stAbs) { addedStaticAbilities.add(StaticAbility.create(AbilityUtils.getSVar(sa, s), c, sa.getCardState(), false)); + if ("ReduceCost".equals(s) || "RaiseCost".equals(s)) { + costChange = true; + } } final GameCommand unanimate = new GameCommand() { @@ -230,6 +234,9 @@ public String getDescription() { addUntilCommand(sa, unanimate); } } + if (costChange) { + c.calculatePerpetualAdjustedManaCost(); + } } /** 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 3c3777d88e1..9acaa140857 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -440,6 +440,7 @@ public void updateNonAbilityTextForView() { public void updateManaCostForView() { currentState.getView().updateManaCost(this); + currentState.calculatePerpetualAdjustedManaCost(); // TODO re-factor Spell ManaCost fallback to CardState ManaCost if (getFirstSpellAbility() != null) { @@ -2074,6 +2075,14 @@ public final ManaCost getManaCost() { return result; } + public void calculatePerpetualAdjustedManaCost() { + currentState.calculatePerpetualAdjustedManaCost(); + } + + public ManaCost getPerpetualAdjustedManaCost() { + return currentState.getPerpetualAdjustedManaCost(); + } + public void addChangedManaCost(ManaCost cost, boolean additional, long timestamp, long staticId) { changedCardManaCost.put(timestamp, staticId, new CardManaCost(cost, additional)); updateManaCostForView(); diff --git a/forge-game/src/main/java/forge/game/card/CardState.java b/forge-game/src/main/java/forge/game/card/CardState.java index 7c14bce77da..ea5e4e82f04 100644 --- a/forge-game/src/main/java/forge/game/card/CardState.java +++ b/forge-game/src/main/java/forge/game/card/CardState.java @@ -19,9 +19,11 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; import com.google.common.collect.Maps; import forge.card.*; import forge.card.mana.ManaCost; +import forge.card.mana.ManaCostParser; import forge.game.CardTraitBase; import forge.game.ForgeScript; import forge.game.GameObject; @@ -39,6 +41,7 @@ import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellPermanent; import forge.game.staticability.StaticAbility; +import forge.game.staticability.StaticAbilityMode; import forge.game.trigger.Trigger; import forge.util.CardTranslation; import forge.util.ITranslatable; @@ -52,6 +55,7 @@ import org.apache.commons.lang3.StringUtils; import java.util.Collection; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Predicate; @@ -62,6 +66,8 @@ public class CardState implements GameObject, IHasSVars, ITranslatable { private CardType type = new CardType(false); private CardTypeView changedType = null; private ManaCost manaCost = ManaCost.NO_COST; + // Track mana cost after adjustments from perpetual cost-changing effects for display + private ManaCost perpetualAdjustedManaCost = null; private ColorSet color = ColorSet.C; private String oracleText = ""; private String functionalVariantName = null; @@ -207,9 +213,81 @@ public final ManaCost getManaCost() { } public final void setManaCost(final ManaCost manaCost0) { manaCost = manaCost0; + calculatePerpetualAdjustedManaCost(); + } + + /** + * Calculate and save the value of the mana cost adjusted by any perpetual raise/lower cost + * effects for display. + */ + public void calculatePerpetualAdjustedManaCost() { + final List raiseAbilities = Lists.newArrayList(); + final List reduceAbilities = Lists.newArrayList(); + // Separate abilities to apply them in proper order + if (getCard() == null || getCard().getGame() == null) { + setPerpetualAdjustedManaCost(manaCost); + return; + } + ManaCost manaCost = getCard().getManaCost(); + // I don't know why refetching the card data like in this next line is necessary but in some cases (when the cost reduction + // wasn't added when the card was in the current zone, I think) the static abilities are missing. There's probably + // a better way to do this. + for (final StaticAbility stAb : getCard().getGame().getCardsInGame().get(getCard()).getStaticAbilities()) { +// for (final StaticAbility stAb : getStaticAbilities()) { // For some reason the current state sometimes doesn't have static abilities + // Only collect perpetual cost changes + if ("Card.Self".equals(stAb.getParam("ValidCard")) && "DBCleanup".equals(stAb.getParam("SubAbility"))) { + if (stAb.checkMode(StaticAbilityMode.ReduceCost)) { + reduceAbilities.add(stAb); + } else if (stAb.checkMode(StaticAbilityMode.RaiseCost)) { + raiseAbilities.add(stAb); + } + } + } + + int totalGenericCostAdjustment = 0; + for (final StaticAbility stAb : raiseAbilities) { + int amount = Integer.parseInt(stAb.getParamOrDefault("Amount", "1")); + totalGenericCostAdjustment = totalGenericCostAdjustment + amount; + } + for (final StaticAbility stAb : reduceAbilities) { + int amount = Integer.parseInt(stAb.getParamOrDefault("Amount", "1")); + totalGenericCostAdjustment = totalGenericCostAdjustment - amount; + } + + if (totalGenericCostAdjustment != 0) { + int genericCost = manaCost.getGenericCost(); + int genericCostAdjustment; + int remainingGenericCostAdjustment; + if (genericCost + totalGenericCostAdjustment < 0) { + genericCostAdjustment = -genericCost; + remainingGenericCostAdjustment = totalGenericCostAdjustment + genericCost; + } else { + genericCostAdjustment = totalGenericCostAdjustment; + remainingGenericCostAdjustment = 0; + } + if (genericCostAdjustment != 0) { + String manaCostString = manaCost.getShortString(); + manaCostString = manaCostString.replace("" + genericCost, "" + (genericCost + genericCostAdjustment)); + manaCost = new ManaCost(new ManaCostParser(manaCostString)); + } + if (remainingGenericCostAdjustment != 0 && manaCost.getShortString().contains("X")) { + // Store extra cost adjustment for X cost spells as a negative generic value for display + String manaCostString = manaCost.getShortString(); + manaCost = new ManaCost(new ManaCostParser(manaCostString + " " + remainingGenericCostAdjustment)); + } + } + setPerpetualAdjustedManaCost(manaCost); view.updateManaCost(this); } + public ManaCost getPerpetualAdjustedManaCost() { + return perpetualAdjustedManaCost; + } + + private void setPerpetualAdjustedManaCost(ManaCost manaCost) { + perpetualAdjustedManaCost = manaCost; + } + public final ColorSet getColor() { return color; } 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 6e1f748e342..3b506501c45 100644 --- a/forge-game/src/main/java/forge/game/card/CardView.java +++ b/forge-game/src/main/java/forge/game/card/CardView.java @@ -1388,11 +1388,16 @@ void updateType(CardState c) { public ManaCost getManaCost() { return get(TrackableProperty.ManaCost); } + public ManaCost getOriginalManaCost() { + return get(TrackableProperty.OriginalManaCost); + } void updateManaCost(CardState c) { - set(TrackableProperty.ManaCost, c.getManaCost()); + set(TrackableProperty.ManaCost, c.getPerpetualAdjustedManaCost()); + set(TrackableProperty.OriginalManaCost, c.getManaCost()); } void updateManaCost(Card c) { - set(TrackableProperty.ManaCost, c.getManaCost()); + set(TrackableProperty.ManaCost, c.getPerpetualAdjustedManaCost()); + set(TrackableProperty.OriginalManaCost, c.getManaCost()); } public String getOracleText() { diff --git a/forge-game/src/main/java/forge/trackable/TrackableProperty.java b/forge-game/src/main/java/forge/trackable/TrackableProperty.java index 12ef4f8e4d9..03e9818c3e6 100644 --- a/forge-game/src/main/java/forge/trackable/TrackableProperty.java +++ b/forge-game/src/main/java/forge/trackable/TrackableProperty.java @@ -126,6 +126,7 @@ public enum TrackableProperty { ImageKey(TrackableTypes.StringType), Type(TrackableTypes.CardTypeViewType), ManaCost(TrackableTypes.ManaCostType), + OriginalManaCost(TrackableTypes.ManaCostType), SetCode(TrackableTypes.StringType), Rarity(TrackableTypes.EnumType(CardRarity.class)), FunctionalVariant(TrackableTypes.StringType), diff --git a/forge-gui-desktop/src/main/java/forge/gui/CardDetailPanel.java b/forge-gui-desktop/src/main/java/forge/gui/CardDetailPanel.java index 4a0650500b4..613a3c72d01 100644 --- a/forge-gui-desktop/src/main/java/forge/gui/CardDetailPanel.java +++ b/forge-gui-desktop/src/main/java/forge/gui/CardDetailPanel.java @@ -194,14 +194,14 @@ public final void setCard(final CardView card, final boolean mayView, final bool } final String name = CardDetailUtil.formatCardName(card, mayView, isInAltState), nameCost; - if (state.getManaCost().isNoCost() || !mayView) { + if (state.getOriginalManaCost().isNoCost() || !mayView) { nameCost = name; } else { final String manaCost; if (card.isSplitCard() && card.hasAlternateState() && !card.isFaceDown() && card.getZone() != ZoneType.Stack && card.getZone() != ZoneType.Battlefield) { //only display current state's mana cost when on stack - manaCost = card.getLeftSplitState().getManaCost() + " // " + card.getAlternateState().getManaCost(); + manaCost = card.getLeftSplitState().getOriginalManaCost() + " // " + card.getAlternateState().getOriginalManaCost(); } else { - manaCost = state.getManaCost().toString(); + manaCost = state.getOriginalManaCost().toString(); } nameCost = String.format("%s - %s", name, manaCost); } diff --git a/forge-gui-desktop/src/main/java/forge/toolbox/CardFaceSymbols.java b/forge-gui-desktop/src/main/java/forge/toolbox/CardFaceSymbols.java index 59dcd09375f..ff5b54c687c 100644 --- a/forge-gui-desktop/src/main/java/forge/toolbox/CardFaceSymbols.java +++ b/forge-gui-desktop/src/main/java/forge/toolbox/CardFaceSymbols.java @@ -1,6 +1,6 @@ package forge.toolbox; -import java.awt.Graphics; +import java.awt.*; import java.util.HashMap; import java.util.Map; import java.util.StringTokenizer; @@ -202,6 +202,19 @@ public static void draw(final Graphics g, final ManaCost manaCost, final int x, xpos += offset; } } + // Show "negative" mana cost caused by perpetual cost reduction effects + // This is only relevant for cards with an "X" in the cost + if (genericManaCost < 0) { + final String sGenericAdjust = Integer.toString(Math.abs(genericManaCost)); + drawSymbol(sGenericAdjust, g, xpos, y, size); + // Give it a yellow border so it doesn't look like the regular generic mana symbol + Stroke oldStroke = ((Graphics2D) g).getStroke(); + ((Graphics2D) g).setStroke(new BasicStroke(2)); + g.setColor(Color.YELLOW); + g.drawOval(xpos, y, size, size); + ((Graphics2D) g).setStroke(oldStroke); + xpos += offset; + } } public static void drawColorSet(Graphics g, ColorSet colorSet, int x, int y, int imageSize, boolean vertical) { @@ -284,7 +297,7 @@ public static void drawAbilitySymbol(final String imageName, final Graphics g, f /** *

- * getWidth. + * Return width needed to draw mana symbols *

* * @param manaCost diff --git a/forge-gui-desktop/src/main/java/forge/toolbox/imaging/FCardImageRenderer.java b/forge-gui-desktop/src/main/java/forge/toolbox/imaging/FCardImageRenderer.java index cebe2a8dcaa..c07635afbf4 100644 --- a/forge-gui-desktop/src/main/java/forge/toolbox/imaging/FCardImageRenderer.java +++ b/forge-gui-desktop/src/main/java/forge/toolbox/imaging/FCardImageRenderer.java @@ -675,7 +675,7 @@ private static void drawHeader(Graphics2D g, CardStateView state, Color[] colors //draw mana cost for card if (drawMana) { - ManaCost manaCost = state.getManaCost(); + ManaCost manaCost = state.getOriginalManaCost(); int manaCostWidth = manaCost.getGlyphCount() * NAME_SIZE + HEADER_PADDING; CardFaceSymbols.draw(g, manaCost, x + w - manaCostWidth, y + (h - NAME_SIZE) / 2 + 1, NAME_SIZE - 1); w -= padding + manaCostWidth; diff --git a/forge-gui-mobile/src/forge/card/CardFaceSymbols.java b/forge-gui-mobile/src/forge/card/CardFaceSymbols.java index 5d182b3a907..dacb3aabf0b 100644 --- a/forge-gui-mobile/src/forge/card/CardFaceSymbols.java +++ b/forge-gui-mobile/src/forge/card/CardFaceSymbols.java @@ -17,10 +17,7 @@ */ package forge.card; -import java.util.Map; -import java.util.Set; -import java.util.StringTokenizer; - +import com.badlogic.gdx.graphics.Color; import forge.Forge; import forge.Graphics; import forge.assets.FSkinImage; @@ -30,6 +27,10 @@ import forge.gui.error.BugReporter; import forge.localinstance.skin.FSkinProp; +import java.util.Map; +import java.util.Set; +import java.util.StringTokenizer; + public class CardFaceSymbols { public static final float FONT_SIZE_FACTOR = 0.85f; @@ -178,6 +179,15 @@ public static void drawManaCost(Graphics g, ManaCost manaCost, float x, float y, x += dx; } } + // Show "negative" mana cost caused by perpetual cost reduction effects + // This is only relevant for cards with an "X" in the cost + if (genericManaCost < 0) { + final String sGenericAdjust = Integer.toString(Math.abs(genericManaCost)); + drawSymbol(sGenericAdjust, g, x, y, imageSize, imageSize); + // Give it a yellow border so it doesn't look like the regular generic mana symbol + g.drawCircle(3, Color.YELLOW, x + dx / 2, y + dx / 2, imageSize / 2 - 1); + x += dx; + } } public static void drawColorSet(Graphics g, ColorSet colorSet, float x, float y, final float imageSize) { @@ -235,6 +245,9 @@ public static void drawSymbol(final String imageName, final Graphics g, final fl g.drawImage(Forge.getAssets().manaImages().get(imageName), x, y, w, h); } + /** + * Return width needed to draw mana symbols + */ public static float getWidth(final ManaCost manaCost, float imageSize) { return manaCost.getGlyphCount() * imageSize; } diff --git a/forge-gui-mobile/src/forge/card/CardImageRenderer.java b/forge-gui-mobile/src/forge/card/CardImageRenderer.java index 6df633e82be..c77c2896484 100644 --- a/forge-gui-mobile/src/forge/card/CardImageRenderer.java +++ b/forge-gui-mobile/src/forge/card/CardImageRenderer.java @@ -306,7 +306,7 @@ private static void drawHeader(Graphics g, CardView card, CardStateView state, C float manaSymbolSize = isAdventure ? MANA_SYMBOL_SIZE * 0.75f : MANA_SYMBOL_SIZE; if (!noText && state != null) { //draw mana cost for card - ManaCost mainManaCost = state.getManaCost(); + ManaCost mainManaCost = state.getOriginalManaCost(); if (card.isSplitCard() && card.getAlternateState() != null && !card.isFaceDown() && card.getZone() != ZoneType.Stack && card.getZone() != ZoneType.Battlefield) { //handle rendering both parts of split card mainManaCost = card.getLeftSplitState().getManaCost();