Add Rod of Absorption

This commit is contained in:
Alumi
2021-07-20 16:03:25 +00:00
committed by Michael Kamensky
parent 719eb4f64f
commit 3c9462ff34
6 changed files with 99 additions and 7 deletions

View File

@@ -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;
}
/**
* <p>
* 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)) {

View File

@@ -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<String, Object> params = hasTotalCMCLimit ? new HashMap<>() : null;
while (!tgtCards.isEmpty() && amount > 0 && totalCMCLimit >= 0) {
if (hasTotalCMCLimit) {
// filter out cars with mana value greater than limit
Iterator<Card> 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<SpellAbility> 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

View File

@@ -1688,7 +1688,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
return exiledWith;
}
public final void setExiledWith(final Card e) {
exiledWith = e;
exiledWith = view.setCard(exiledWith, e, TrackableProperty.ExiledWith);
}
public final void cleanupExiledWith() {

View File

@@ -599,6 +599,10 @@ public class CardView extends GameEntityView {
return get(TrackableProperty.CloneOrigin);
}
public CardView getExiledWith() {
return get(TrackableProperty.ExiledWith);
}
public FCollectionView<CardView> getImprintedCards() {
return get(TrackableProperty.ImprintedCards);
}

View File

@@ -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),

View File

@@ -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.