diff --git a/forge-ai/src/main/java/forge/ai/ability/PlayAi.java b/forge-ai/src/main/java/forge/ai/ability/PlayAi.java
index d280ce3e12b..2d64e10bced 100644
--- a/forge-ai/src/main/java/forge/ai/ability/PlayAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/PlayAi.java
@@ -5,6 +5,7 @@ import com.google.common.collect.Iterables;
import forge.ai.*;
import forge.card.CardStateName;
import forge.card.CardTypeView;
+import forge.card.mana.ManaCost;
import forge.game.Game;
import forge.game.GameType;
import forge.game.ability.AbilityUtils;
@@ -26,7 +27,7 @@ public class PlayAi extends SpellAbilityAi {
@Override
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
final String logic = sa.hasParam("AILogic") ? sa.getParam("AILogic") : "";
-
+
final Game game = ai.getGame();
final Card source = sa.getHostCard();
// don't use this as a response (ReplaySpell logic is an exception, might be called from a subability
@@ -95,7 +96,7 @@ public class PlayAi extends SpellAbilityAi {
}
if ("ReplaySpell".equals(logic)) {
- return ComputerUtil.targetPlayableSpellCard(ai, cards, sa, sa.hasParam("WithoutManaCost"));
+ return ComputerUtil.targetPlayableSpellCard(ai, cards, sa, sa.hasParam("WithoutManaCost"));
} else if (logic.startsWith("NeedsChosenCard")) {
int minCMC = 0;
if (sa.getPayCosts().getCostMana() != null) {
@@ -103,6 +104,23 @@ public class PlayAi extends SpellAbilityAi {
}
validOpts = CardLists.filter(validOpts, CardPredicates.greaterCMC(minCMC));
return chooseSingleCard(ai, sa, validOpts, sa.hasParam("Optional"), null, null) != null;
+ } else if ("WithTotalCMC".equals(logic)) {
+ // Try to play only when there are more than three playable cards.
+ if (cards.size() < 3)
+ return false;
+ ManaCost mana = sa.getPayCosts().getTotalMana();
+ if (mana.countX() > 0) {
+ int amount = ComputerUtilCost.getMaxXValue(sa, ai);
+ if (amount < ComputerUtilCard.getBestAI(cards).getCMC())
+ return false;
+ int totalCMC = 0;
+ for (Card c : cards) {
+ totalCMC += c.getCMC();
+ }
+ if (amount > totalCMC)
+ amount = totalCMC;
+ sa.setXManaCostPaid(amount);
+ }
}
if (source != null && source.hasKeyword(Keyword.HIDEAWAY) && source.hasRemembered()) {
@@ -117,7 +135,7 @@ public class PlayAi extends SpellAbilityAi {
return true;
}
-
+
/**
*
* doTriggerAINoCost
@@ -135,7 +153,7 @@ public class PlayAi extends SpellAbilityAi {
if (!sa.hasParam("AILogic")) {
return false;
}
-
+
return checkApiLogic(ai, sa);
}
@@ -164,6 +182,11 @@ public class PlayAi extends SpellAbilityAi {
// timing restrictions still apply
if (!s.getRestrictions().checkTimingRestrictions(c, s))
continue;
+ if (params != null && params.containsKey("CMCLimit")) {
+ Integer cmcLimit = (Integer) params.get("CMCLimit");
+ if (spell.getPayCosts().getTotalMana().getCMC() > cmcLimit)
+ continue;
+ }
if (sa.hasParam("WithoutManaCost")) {
// Try to avoid casting instants and sorceries with X in their cost, since X will be assumed to be 0.
if (!(spell instanceof SpellPermanent)) {
diff --git a/forge-game/src/main/java/forge/game/ability/effects/PlayEffect.java b/forge-game/src/main/java/forge/game/ability/effects/PlayEffect.java
index e69173083a6..90e5dcfd917 100644
--- a/forge-game/src/main/java/forge/game/ability/effects/PlayEffect.java
+++ b/forge-game/src/main/java/forge/game/ability/effects/PlayEffect.java
@@ -1,7 +1,10 @@
package forge.game.ability.effects;
import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
import java.util.List;
+import java.util.Map;
import org.apache.commons.lang3.StringUtils;
@@ -14,6 +17,7 @@ import com.google.common.collect.Lists;
import forge.GameCommand;
import forge.StaticData;
import forge.card.CardRulesPredicates;
+import forge.card.CardStateName;
import forge.game.Game;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityUtils;
@@ -69,6 +73,8 @@ public class PlayEffect extends SpellAbilityEffect {
final boolean optional = sa.hasParam("Optional");
boolean remember = sa.hasParam("RememberPlayed");
int amount = 1;
+ boolean hasTotalCMCLimit = sa.hasParam("WithTotalCMC");
+ int totalCMCLimit = Integer.MAX_VALUE;
if (sa.hasParam("Amount") && !sa.getParam("Amount").equals("All")) {
amount = AbilityUtils.calculateAmount(source, sa.getParam("Amount"), sa);
}
@@ -179,15 +185,46 @@ public class PlayEffect extends SpellAbilityEffect {
amount = tgtCards.size();
}
+ if (hasTotalCMCLimit) {
+ totalCMCLimit = AbilityUtils.calculateAmount(source, sa.getParam("WithTotalCMC"), sa);
+ }
+
if (controlledByPlayer != null) {
activator.addController(controlledByTimeStamp, controlledByPlayer);
}
boolean singleOption = tgtCards.size() == 1 && amount == 1 && optional;
+ Map params = hasTotalCMCLimit ? new HashMap<>() : null;
+
+ while (!tgtCards.isEmpty() && amount > 0 && totalCMCLimit >= 0) {
+ if (hasTotalCMCLimit) {
+ // filter out cars with mana value greater than limit
+ Iterator it = tgtCards.iterator();
+ while (it.hasNext()) {
+ Card c = it.next();
+ if (c.isSplitCard()) {
+ if (c.getState(CardStateName.LeftSplit).getManaCost().getCMC() <= totalCMCLimit)
+ continue;
+ if (c.getState(CardStateName.RightSplit).getManaCost().getCMC() <= totalCMCLimit)
+ continue;
+ it.remove();
+ } else {
+ if (c.getState(CardStateName.Original).getManaCost().getCMC() <= totalCMCLimit)
+ continue;
+ if (c.hasAlternateState() && c.getAlternateState().getManaCost().getCMC() <= totalCMCLimit)
+ continue;
+ // it.remove will only remove item from the list part of CardCollection
+ tgtCards.asSet().remove(c);
+ it.remove();
+ }
+ }
+ if (tgtCards.isEmpty())
+ break;
+ params.put("CMCLimit", totalCMCLimit);
+ }
- while (!tgtCards.isEmpty() && amount > 0) {
activator.getController().tempShowCards(showCards);
- Card tgtCard = controller.getController().chooseSingleEntityForEffect(tgtCards, sa, Localizer.getInstance().getMessage("lblSelectCardToPlay"), !singleOption && optional, null);
+ Card tgtCard = controller.getController().chooseSingleEntityForEffect(tgtCards, sa, Localizer.getInstance().getMessage("lblSelectCardToPlay"), !singleOption && optional, params);
activator.getController().endTempShowCards();
if (tgtCard == null) {
break;
@@ -248,6 +285,14 @@ public class PlayEffect extends SpellAbilityEffect {
final String valid[] = {sa.getParam("ValidSA")};
sas = Lists.newArrayList(Iterables.filter(sas, SpellAbilityPredicates.isValid(valid, controller , source, sa)));
}
+ if (hasTotalCMCLimit) {
+ Iterator it = sas.iterator();
+ while (it.hasNext()) {
+ SpellAbility s = it.next();
+ if (s.getPayCosts().getTotalMana().getCMC() > totalCMCLimit)
+ it.remove();
+ }
+ }
if (sas.isEmpty()) {
continue;
@@ -276,6 +321,8 @@ public class PlayEffect extends SpellAbilityEffect {
continue;
}
+ final int tgtCMC = tgtSA.getPayCosts().getTotalMana().getCMC();
+
if (sa.hasParam("WithoutManaCost")) {
tgtSA = tgtSA.copyWithNoManaCost();
} else if (sa.hasParam("PlayCost")) {
@@ -341,6 +388,7 @@ public class PlayEffect extends SpellAbilityEffect {
}
amount--;
+ totalCMCLimit -= tgtCMC;
}
// Remove controlled by player if any
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 9c660f472f5..f6f8c452aa2 100644
--- a/forge-game/src/main/java/forge/game/card/Card.java
+++ b/forge-game/src/main/java/forge/game/card/Card.java
@@ -1688,7 +1688,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars {
return exiledWith;
}
public final void setExiledWith(final Card e) {
- exiledWith = e;
+ exiledWith = view.setCard(exiledWith, e, TrackableProperty.ExiledWith);
}
public final void cleanupExiledWith() {
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 7ad4251c635..02e54357926 100644
--- a/forge-game/src/main/java/forge/game/card/CardView.java
+++ b/forge-game/src/main/java/forge/game/card/CardView.java
@@ -599,6 +599,10 @@ public class CardView extends GameEntityView {
return get(TrackableProperty.CloneOrigin);
}
+ public CardView getExiledWith() {
+ return get(TrackableProperty.ExiledWith);
+ }
+
public FCollectionView getImprintedCards() {
return get(TrackableProperty.ImprintedCards);
}
diff --git a/forge-game/src/main/java/forge/trackable/TrackableProperty.java b/forge-game/src/main/java/forge/trackable/TrackableProperty.java
index 50ace8e3e31..e378e900ab9 100644
--- a/forge-game/src/main/java/forge/trackable/TrackableProperty.java
+++ b/forge-game/src/main/java/forge/trackable/TrackableProperty.java
@@ -77,6 +77,7 @@ public enum TrackableProperty {
UntilLeavesBattlefield(TrackableTypes.CardViewCollectionType),
GainControlTargets(TrackableTypes.CardViewCollectionType),
CloneOrigin(TrackableTypes.CardViewType),
+ ExiledWith(TrackableTypes.CardViewType),
ImprintedCards(TrackableTypes.CardViewCollectionType),
HauntedBy(TrackableTypes.CardViewCollectionType),
diff --git a/forge-gui/res/cardsfolder/upcoming/rod_of_absorption.txt b/forge-gui/res/cardsfolder/upcoming/rod_of_absorption.txt
new file mode 100644
index 00000000000..9a929ce02ce
--- /dev/null
+++ b/forge-gui/res/cardsfolder/upcoming/rod_of_absorption.txt
@@ -0,0 +1,16 @@
+Name:Rod of Absorption
+ManaCost:2 U
+Types:Artifact
+T:Mode$ ChangesZone | ValidCard$ Card.IsRemembered | Origin$ Exile | Destination$ Any | TriggerZones$ Battlefield | Execute$ TrigForget | Static$ True
+SVar:TrigForget:DB$ Pump | ForgetObjects$ TriggeredCard
+T:Mode$ ChangesZone | ValidCard$ Card.Self | Origin$ Battlefield | Destination$ Any | Execute$ DBCleanup | Static$ True
+SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True
+T:Mode$ SpellCast | ValidCard$ Instant,Sorcery | Execute$ TrigEffect | TriggerZones$ Battlefield | TriggerDescription$ Whenever a player casts an instant or sorcery spell, exile it instead of putting it into a graveyard as it resolves.
+SVar:TrigEffect:DB$ Effect | ReplacementEffects$ ReMoved | RememberObjects$ TriggeredCard
+SVar:ReMoved:Event$ Moved | ValidCard$ Card.IsRemembered | Origin$ Stack | Destination$ Graveyard | Fizzle$ False | ReplaceWith$ DBExile | Description$ Exile it instead of putting it into a graveyard as it resolves.
+SVar:DBExile:DB$ ChangeZone | Defined$ ReplacedCard | Origin$ Stack | Destination$ Exile | SubAbility$ DBRemember
+SVar:DBRemember:DB$ Animate | Defined$ EffectSource | RememberObjects$ ReplacedCard | Duration$ Permanent | SubAbility$ ExileSelf
+SVar:ExileSelf:DB$ ChangeZone | Origin$ Command | Destination$ Exile | Defined$ Self
+A:AB$ Play | Cost$ X T Sac<1/CARDNAME> | Defined$ ValidExile Card.IsRemembered | ValidSA$ Spell | Amount$ All | WithTotalCMC$ X | WithoutManaCost$ True | Optional$ True | AILogic$ WithTotalCMC | SpellDescription$ You may cast any number of spells from among cards exiled with CARDNAME with total mana value X or less without paying their mana costs.
+SVar:X:Count$xPaid
+Oracle:Whenever a player casts an instant or sorcery spell, exile it instead of putting it into a graveyard as it resolves.\n{X}, {T}, Sacrifice Rod of Absorption: You may cast any number of spells from among cards exiled with Rod of Absorption with total mana value X or less without paying their mana costs.