From 3c9462ff34e4b00b25565c99ab14a0a5fa95c849 Mon Sep 17 00:00:00 2001 From: Alumi Date: Tue, 20 Jul 2021 16:03:25 +0000 Subject: [PATCH] Add Rod of Absorption --- .../main/java/forge/ai/ability/PlayAi.java | 31 +++++++++-- .../game/ability/effects/PlayEffect.java | 52 ++++++++++++++++++- .../src/main/java/forge/game/card/Card.java | 2 +- .../main/java/forge/game/card/CardView.java | 4 ++ .../forge/trackable/TrackableProperty.java | 1 + .../upcoming/rod_of_absorption.txt | 16 ++++++ 6 files changed, 99 insertions(+), 7 deletions(-) create mode 100644 forge-gui/res/cardsfolder/upcoming/rod_of_absorption.txt 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.