spells = watcher.getSpellsCastThisTurn(source.getControllerId());
+ return spells != null && spells
+ .stream()
+ .filter(Objects::nonNull)
+ .anyMatch(spell -> !spell.getSourceId().equals(source.getSourceId()) || spell.getZoneChangeCounter(game) != source.getSourceObjectZoneChangeCounter());
+ }
+
+ public Hint getHint() {
+ return hint;
+ }
+
+ @Override
+ public String toString() {
+ return "you've cast another spell this turn";
+ }
+}
diff --git a/Mage/src/main/java/mage/abilities/condition/common/IsBeingCastFromHandCondition.java b/Mage/src/main/java/mage/abilities/condition/common/IsBeingCastFromHandCondition.java
index fe3ca1e23ce..6e0e5b65174 100644
--- a/Mage/src/main/java/mage/abilities/condition/common/IsBeingCastFromHandCondition.java
+++ b/Mage/src/main/java/mage/abilities/condition/common/IsBeingCastFromHandCondition.java
@@ -4,7 +4,7 @@ package mage.abilities.condition.common;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.condition.Condition;
-import mage.cards.AdventureCardSpell;
+import mage.cards.SpellOptionCard;
import mage.cards.Card;
import mage.cards.ModalDoubleFacedCardHalf;
import mage.cards.SplitCardHalf;
@@ -20,7 +20,7 @@ public enum IsBeingCastFromHandCondition implements Condition {
@Override
public boolean apply(Game game, Ability source) {
MageObject object = game.getObject(source);
- if (object instanceof SplitCardHalf || object instanceof AdventureCardSpell || object instanceof ModalDoubleFacedCardHalf) {
+ if (object instanceof SplitCardHalf || object instanceof SpellOptionCard || object instanceof ModalDoubleFacedCardHalf) {
UUID mainCardId = ((Card) object).getMainCard().getId();
object = game.getObject(mainCardId);
}
diff --git a/Mage/src/main/java/mage/abilities/condition/common/ModeChoiceSourceCondition.java b/Mage/src/main/java/mage/abilities/condition/common/ModeChoiceSourceCondition.java
deleted file mode 100644
index 8ce77f24633..00000000000
--- a/Mage/src/main/java/mage/abilities/condition/common/ModeChoiceSourceCondition.java
+++ /dev/null
@@ -1,26 +0,0 @@
-
-package mage.abilities.condition.common;
-
-import mage.abilities.Ability;
-import mage.abilities.condition.Condition;
-import mage.game.Game;
-
-/**
- *
- * @author LevelX2
- */
-public class ModeChoiceSourceCondition implements Condition {
-
- private final String mode;
-
-
- public ModeChoiceSourceCondition(String mode) {
- this.mode = mode;
- }
-
- @Override
- public boolean apply(Game game, Ability source) {
- String chosenMode = (String) game.getState().getValue(source.getSourceId() + "_modeChoice");
- return chosenMode != null && chosenMode.equals(mode);
- }
-}
diff --git a/Mage/src/main/java/mage/abilities/condition/common/YouCastExactOneSpellThisTurnCondition.java b/Mage/src/main/java/mage/abilities/condition/common/YouCastExactOneSpellThisTurnCondition.java
new file mode 100644
index 00000000000..98aa5c75dc0
--- /dev/null
+++ b/Mage/src/main/java/mage/abilities/condition/common/YouCastExactOneSpellThisTurnCondition.java
@@ -0,0 +1,19 @@
+package mage.abilities.condition.common;
+
+import mage.abilities.Ability;
+import mage.abilities.condition.Condition;
+import mage.game.Game;
+import mage.watchers.common.SpellsCastWatcher;
+
+/**
+ * @author androosss
+ */
+public enum YouCastExactOneSpellThisTurnCondition implements Condition {
+ instance;
+
+ @Override
+ public boolean apply(Game game, Ability source) {
+ SpellsCastWatcher watcher = game.getState().getWatcher(SpellsCastWatcher.class);
+ return watcher != null && watcher.getSpellsCastThisTurn(source.getControllerId()).size() == 1;
+ }
+}
diff --git a/Mage/src/main/java/mage/abilities/costs/CostAdjuster.java b/Mage/src/main/java/mage/abilities/costs/CostAdjuster.java
index ea1d77449d0..86bc96ee985 100644
--- a/Mage/src/main/java/mage/abilities/costs/CostAdjuster.java
+++ b/Mage/src/main/java/mage/abilities/costs/CostAdjuster.java
@@ -1,24 +1,86 @@
package mage.abilities.costs;
import mage.abilities.Ability;
+import mage.constants.CostModificationType;
import mage.game.Game;
import java.io.Serializable;
/**
- * @author TheElk801
+ * Dynamic costs implementation to control {X} or other costs, can be used in spells and abilities
+ *
+ * Possible use cases:
+ * - define {X} costs like X cards to discard (mana and non-mana values);
+ * - define {X} limits before announce (to help in UX and AI logic);
+ * - define any dynamic costs;
+ * - use as simple cost increase/reduce effect;
+ *
+ * Calls order by game engine:
+ * - ... early cost target selection for EarlyTargetCost ...
+ * - prepareX
+ * - ... x announce ...
+ * - prepareCost
+ * - increaseCost
+ * - reduceCost
+ * - ... normal target selection and payment ...
+ *
+ * @author TheElk801, JayDi85
*/
-@FunctionalInterface
public interface CostAdjuster extends Serializable {
/**
- * Must check playable and real cast states.
- * Example: if it need stack related info (like real targets) then must check two states (game.inCheckPlayableState):
- * 1. In playable state it must check all possible use cases (e.g. allow to reduce on any available target and modes)
- * 2. In real cast state it must check current use case (e.g. real selected targets and modes)
- *
- * @param ability
- * @param game
+ * Prepare {X} costs settings or define auto-announced mana values
+ *
+ * Usage example:
+ * - define auto-announced mana value {X} by ability.setVariableCostsValue
+ * - define possible {X} settings by ability.setVariableCostsMinMax
*/
- void adjustCosts(Ability ability, Game game);
+ default void prepareX(Ability ability, Game game) {
+ // do nothing
+ }
+
+ /**
+ * Prepare any dynamic costs
+ *
+ * Usage example:
+ * - add real cost after {X} mana value announce by CardUtil.getSourceCostsTagX
+ * - add dynamic cost from game data
+ */
+ default void prepareCost(Ability ability, Game game) {
+ // do nothing
+ }
+
+ /**
+ * Simple cost reduction effect
+ */
+ default void reduceCost(Ability ability, Game game) {
+ // do nothing
+ }
+
+ /**
+ * Simple cost increase effect
+ */
+ default void increaseCost(Ability ability, Game game) {
+ // do nothing
+ }
+
+ /**
+ * Default implementation. Override reduceCost or increaseCost instead
+ * TODO: make it private after java 9+ migrate
+ */
+ default void modifyCost(Ability ability, Game game, CostModificationType costModificationType) {
+ switch (costModificationType) {
+ case REDUCE_COST:
+ reduceCost(ability, game);
+ break;
+ case INCREASE_COST:
+ increaseCost(ability, game);
+ break;
+ case SET_COST:
+ // do nothing
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown mod type: " + costModificationType);
+ }
+ }
}
diff --git a/Mage/src/main/java/mage/abilities/costs/EarlyTargetCost.java b/Mage/src/main/java/mage/abilities/costs/EarlyTargetCost.java
index 359188f7fdc..23e71faa203 100644
--- a/Mage/src/main/java/mage/abilities/costs/EarlyTargetCost.java
+++ b/Mage/src/main/java/mage/abilities/costs/EarlyTargetCost.java
@@ -6,20 +6,11 @@ import mage.players.Player;
/**
* @author Grath
- * Costs which extend this class need to have targets chosen, and those targets must be chosen during 601.2b step.
+ *
+ * Support 601.2b rules for ealry target choice before X announce and other actions
*/
-public abstract class EarlyTargetCost extends CostImpl {
+public interface EarlyTargetCost {
- protected EarlyTargetCost() {
- super();
- }
+ void chooseTarget(Game game, Ability source, Player controller);
- protected EarlyTargetCost(final EarlyTargetCost cost) {
- super(cost);
- }
-
- @Override
- public abstract EarlyTargetCost copy();
-
- public abstract void chooseTarget(Game game, Ability source, Player controller);
}
diff --git a/Mage/src/main/java/mage/abilities/costs/MinMaxVariableCost.java b/Mage/src/main/java/mage/abilities/costs/MinMaxVariableCost.java
new file mode 100644
index 00000000000..80b8a1d12ed
--- /dev/null
+++ b/Mage/src/main/java/mage/abilities/costs/MinMaxVariableCost.java
@@ -0,0 +1,15 @@
+package mage.abilities.costs;
+
+/**
+ * @author jayDi85
+ */
+public interface MinMaxVariableCost extends VariableCost {
+
+ int getMinX();
+
+ void setMinX(int minX);
+
+ int getMaxX();
+
+ void setMaxX(int maxX);
+}
diff --git a/Mage/src/main/java/mage/abilities/costs/common/DiscardXTargetCost.java b/Mage/src/main/java/mage/abilities/costs/common/DiscardXTargetCost.java
index 8fc089b12de..ac863a0e819 100644
--- a/Mage/src/main/java/mage/abilities/costs/common/DiscardXTargetCost.java
+++ b/Mage/src/main/java/mage/abilities/costs/common/DiscardXTargetCost.java
@@ -10,11 +10,20 @@ import mage.players.Player;
import mage.target.common.TargetCardInHand;
/**
+ * Used to setup discard cost WITHOUT {X} mana cost
+ *
+ * If you have {X} in spell's mana cost then use DiscardXCardsCostAdjuster instead
+ *
+ * Example:
+ * - {2}{U}{R}
+ * - As an additional cost to cast this spell, discard X cards.
+ *
* @author LevelX2
*/
public class DiscardXTargetCost extends VariableCostImpl {
protected FilterCard filter;
+ protected boolean isRandom = false;
public DiscardXTargetCost(FilterCard filter) {
this(filter, false);
@@ -30,6 +39,12 @@ public class DiscardXTargetCost extends VariableCostImpl {
protected DiscardXTargetCost(final DiscardXTargetCost cost) {
super(cost);
this.filter = cost.filter;
+ this.isRandom = cost.isRandom;
+ }
+
+ public DiscardXTargetCost withRandom() {
+ this.isRandom = true;
+ return this;
}
@Override
@@ -49,6 +64,6 @@ public class DiscardXTargetCost extends VariableCostImpl {
@Override
public Cost getFixedCostsFromAnnouncedValue(int xValue) {
TargetCardInHand target = new TargetCardInHand(xValue, filter);
- return new DiscardTargetCost(target);
+ return new DiscardTargetCost(target, this.isRandom);
}
}
diff --git a/Mage/src/main/java/mage/abilities/costs/common/PayLoyaltyCost.java b/Mage/src/main/java/mage/abilities/costs/common/PayLoyaltyCost.java
index 2b8885471c0..fff0baae6bb 100644
--- a/Mage/src/main/java/mage/abilities/costs/common/PayLoyaltyCost.java
+++ b/Mage/src/main/java/mage/abilities/costs/common/PayLoyaltyCost.java
@@ -35,10 +35,10 @@ public class PayLoyaltyCost extends CostImpl {
int loyaltyCost = amount;
- // apply cost modification
+ // apply dynamic costs and cost modification
if (ability instanceof LoyaltyAbility) {
LoyaltyAbility copiedAbility = ((LoyaltyAbility) ability).copy();
- copiedAbility.adjustCosts(game);
+ copiedAbility.adjustX(game);
game.getContinuousEffects().costModification(copiedAbility, game);
loyaltyCost = 0;
for (Cost cost : copiedAbility.getCosts()) {
diff --git a/Mage/src/main/java/mage/abilities/costs/common/PayVariableLoyaltyCost.java b/Mage/src/main/java/mage/abilities/costs/common/PayVariableLoyaltyCost.java
index 42bca28413b..4806e359f85 100644
--- a/Mage/src/main/java/mage/abilities/costs/common/PayVariableLoyaltyCost.java
+++ b/Mage/src/main/java/mage/abilities/costs/common/PayVariableLoyaltyCost.java
@@ -59,10 +59,10 @@ public class PayVariableLoyaltyCost extends VariableCostImpl {
int maxValue = permanent.getCounters(game).getCount(CounterType.LOYALTY);
- // apply cost modification
+ // apply dynamic costs and cost modification
if (source instanceof LoyaltyAbility) {
LoyaltyAbility copiedAbility = ((LoyaltyAbility) source).copy();
- copiedAbility.adjustCosts(game);
+ copiedAbility.adjustX(game);
game.getContinuousEffects().costModification(copiedAbility, game);
for (Cost cost : copiedAbility.getCosts()) {
if (cost instanceof PayVariableLoyaltyCost) {
diff --git a/Mage/src/main/java/mage/abilities/costs/costadjusters/CommanderManaValueAdjuster.java b/Mage/src/main/java/mage/abilities/costs/costadjusters/CommanderManaValueAdjuster.java
index cd018459433..d168aa46850 100644
--- a/Mage/src/main/java/mage/abilities/costs/costadjusters/CommanderManaValueAdjuster.java
+++ b/Mage/src/main/java/mage/abilities/costs/costadjusters/CommanderManaValueAdjuster.java
@@ -13,7 +13,7 @@ public enum CommanderManaValueAdjuster implements CostAdjuster {
instance;
@Override
- public void adjustCosts(Ability ability, Game game) {
+ public void reduceCost(Ability ability, Game game) {
CardUtil.reduceCost(ability, GreatestCommanderManaValue.instance.calculate(game, ability, null));
}
}
diff --git a/Mage/src/main/java/mage/abilities/costs/costadjusters/DiscardXCardsCostAdjuster.java b/Mage/src/main/java/mage/abilities/costs/costadjusters/DiscardXCardsCostAdjuster.java
new file mode 100644
index 00000000000..302d4d7e354
--- /dev/null
+++ b/Mage/src/main/java/mage/abilities/costs/costadjusters/DiscardXCardsCostAdjuster.java
@@ -0,0 +1,73 @@
+package mage.abilities.costs.costadjusters;
+
+import mage.abilities.Ability;
+import mage.abilities.common.SimpleStaticAbility;
+import mage.abilities.costs.CostAdjuster;
+import mage.abilities.costs.common.DiscardTargetCost;
+import mage.abilities.effects.common.InfoEffect;
+import mage.cards.Card;
+import mage.constants.Zone;
+import mage.filter.FilterCard;
+import mage.game.Game;
+import mage.players.Player;
+import mage.target.common.TargetCardInHand;
+import mage.util.CardUtil;
+
+/**
+ * Used to setup discard cost with {X} mana cost
+ *
+ * If you don't have {X} then use DiscardXTargetCost instead
+ *
+ * Example:
+ * - {X}{1}{B}
+ * - As an additional cost to cast this spell, discard X cards.
+ *
+ * @author JayDi85
+ */
+public class DiscardXCardsCostAdjuster implements CostAdjuster {
+
+ private final FilterCard filter;
+
+ private DiscardXCardsCostAdjuster(FilterCard filter) {
+ this.filter = filter;
+ }
+
+ @Override
+ public void prepareX(Ability ability, Game game) {
+ Player controller = game.getPlayer(ability.getControllerId());
+ if (controller == null) {
+ return;
+ }
+
+ int minX = 0;
+ int maxX = controller.getHand().getCards(this.filter, ability.getControllerId(), ability, game).size();
+ ability.setVariableCostsMinMax(minX, maxX);
+ }
+
+ @Override
+ public void prepareCost(Ability ability, Game game) {
+ int x = CardUtil.getSourceCostsTagX(game, ability, -1);
+ if (x >= 0) {
+ ability.addCost(new DiscardTargetCost(new TargetCardInHand(x, x, this.filter)));
+ }
+ }
+
+ public static void addAdjusterAndMessage(Card card, FilterCard filter) {
+ addAdjusterAndMessage(card, filter, false);
+ }
+
+ public static void addAdjusterAndMessage(Card card, FilterCard filter, boolean isRandom) {
+ if (card.getSpellAbility().getManaCosts().getVariableCosts().isEmpty()) {
+ // how to fix: use DiscardXTargetCost
+ throw new IllegalArgumentException("Wrong code usage: that's cost adjuster must be used with {X} in mana costs only - " + card);
+ }
+
+ Ability ability = new SimpleStaticAbility(
+ Zone.ALL, new InfoEffect("As an additional cost to cast this spell, discard X " + filter.getMessage())
+ );
+ ability.setRuleAtTheTop(true);
+ card.addAbility(ability);
+
+ card.getSpellAbility().setCostAdjuster(new DiscardXCardsCostAdjuster(filter));
+ }
+}
diff --git a/Mage/src/main/java/mage/abilities/costs/costadjusters/DomainAdjuster.java b/Mage/src/main/java/mage/abilities/costs/costadjusters/DomainAdjuster.java
index 37cf5e704a2..5037eedb47d 100644
--- a/Mage/src/main/java/mage/abilities/costs/costadjusters/DomainAdjuster.java
+++ b/Mage/src/main/java/mage/abilities/costs/costadjusters/DomainAdjuster.java
@@ -13,7 +13,7 @@ public enum DomainAdjuster implements CostAdjuster {
instance;
@Override
- public void adjustCosts(Ability ability, Game game) {
+ public void reduceCost(Ability ability, Game game) {
CardUtil.reduceCost(ability, DomainValue.REGULAR.calculate(game, ability, null));
}
}
diff --git a/Mage/src/main/java/mage/abilities/costs/costadjusters/ExileCardsFromHandAdjuster.java b/Mage/src/main/java/mage/abilities/costs/costadjusters/ExileCardsFromHandAdjuster.java
index 742e315efcb..9d9d287eae2 100644
--- a/Mage/src/main/java/mage/abilities/costs/costadjusters/ExileCardsFromHandAdjuster.java
+++ b/Mage/src/main/java/mage/abilities/costs/costadjusters/ExileCardsFromHandAdjuster.java
@@ -25,22 +25,29 @@ public class ExileCardsFromHandAdjuster implements CostAdjuster {
}
@Override
- public void adjustCosts(Ability ability, Game game) {
- if (game.inCheckPlayableState()) {
- return;
- }
+ public void reduceCost(Ability ability, Game game) {
Player player = game.getPlayer(ability.getControllerId());
if (player == null) {
return;
}
+
int cardCount = player.getHand().count(filter, game);
- int toExile = cardCount > 0 ? player.getAmount(
- 0, cardCount, "Choose how many " + filter.getMessage() + " to exile", game
- ) : 0;
- if (toExile > 0) {
- ability.addCost(new ExileFromHandCost(new TargetCardInHand(toExile, filter)));
- CardUtil.reduceCost(ability, 2 * toExile);
+ int reduceCount;
+ if (game.inCheckPlayableState()) {
+ // possible
+ reduceCount = 2 * cardCount;
+ } else {
+ // real - need to choose
+ // TODO: need early target cost instead dialog here
+ int toExile = cardCount == 0 ? 0 : player.getAmount(
+ 0, cardCount, "Choose how many " + filter.getMessage() + " to exile", game
+ );
+ reduceCount = 2 * toExile;
+ if (toExile > 0) {
+ ability.addCost(new ExileFromHandCost(new TargetCardInHand(toExile, filter)));
+ }
}
+ CardUtil.reduceCost(ability, 2 * reduceCount);
}
public static final void addAdjusterAndMessage(Card card, FilterCard filter) {
diff --git a/Mage/src/main/java/mage/abilities/costs/costadjusters/ImprintedManaValueXCostAdjuster.java b/Mage/src/main/java/mage/abilities/costs/costadjusters/ImprintedManaValueXCostAdjuster.java
new file mode 100644
index 00000000000..76ac9ca2a0a
--- /dev/null
+++ b/Mage/src/main/java/mage/abilities/costs/costadjusters/ImprintedManaValueXCostAdjuster.java
@@ -0,0 +1,37 @@
+package mage.abilities.costs.costadjusters;
+
+import mage.abilities.Ability;
+import mage.abilities.costs.CostAdjuster;
+import mage.cards.Card;
+import mage.game.Game;
+import mage.game.permanent.Permanent;
+
+/**
+ * Used for {X} mana cost that must be replaced by imprinted mana value
+ *
+ * Example:
+ * - Elite Arcanist
+ * - {X}, {T}: Copy the exiled card. ... X is the converted mana cost of the exiled card.
+ *
+ * @author JayDi85
+ */
+public enum ImprintedManaValueXCostAdjuster implements CostAdjuster {
+ instance;
+
+ @Override
+ public void prepareX(Ability ability, Game game) {
+ int manaValue = Integer.MAX_VALUE;
+
+ Permanent sourcePermanent = game.getPermanent(ability.getSourceId());
+ if (sourcePermanent != null
+ && sourcePermanent.getImprinted() != null
+ && !sourcePermanent.getImprinted().isEmpty()) {
+ Card imprintedInstant = game.getCard(sourcePermanent.getImprinted().get(0));
+ if (imprintedInstant != null) {
+ manaValue = imprintedInstant.getManaValue();
+ }
+ }
+
+ ability.setVariableCostsValue(manaValue);
+ }
+}
diff --git a/Mage/src/main/java/mage/abilities/costs/costadjusters/LegendaryCreatureCostAdjuster.java b/Mage/src/main/java/mage/abilities/costs/costadjusters/LegendaryCreatureCostAdjuster.java
index cd2406ed5f3..3d4f08fc785 100644
--- a/Mage/src/main/java/mage/abilities/costs/costadjusters/LegendaryCreatureCostAdjuster.java
+++ b/Mage/src/main/java/mage/abilities/costs/costadjusters/LegendaryCreatureCostAdjuster.java
@@ -29,10 +29,8 @@ public enum LegendaryCreatureCostAdjuster implements CostAdjuster {
);
@Override
- public void adjustCosts(Ability ability, Game game) {
- int count = game.getBattlefield().count(
- filter, ability.getControllerId(), ability, game
- );
+ public void reduceCost(Ability ability, Game game) {
+ int count = game.getBattlefield().count(filter, ability.getControllerId(), ability, game);
if (count > 0) {
CardUtil.reduceCost(ability, count);
}
diff --git a/Mage/src/main/java/mage/abilities/costs/mana/ManaCost.java b/Mage/src/main/java/mage/abilities/costs/mana/ManaCost.java
index b0d51dca448..f72b7fa0a86 100644
--- a/Mage/src/main/java/mage/abilities/costs/mana/ManaCost.java
+++ b/Mage/src/main/java/mage/abilities/costs/mana/ManaCost.java
@@ -32,7 +32,7 @@ public interface ManaCost extends Cost {
ManaOptions getOptions();
/**
- * Return all options for paying the mana cost (this) while taking into accoutn if the player can pay life.
+ * Return all options for paying the mana cost (this) while taking into account if the player can pay life.
* Used to correctly highlight (or not) spells with Phyrexian mana depending on if the player can pay life costs.
*
* E.g. Tezzeret's Gambit has a cost of {3}{U/P}.
diff --git a/Mage/src/main/java/mage/abilities/costs/mana/ManaCostImpl.java b/Mage/src/main/java/mage/abilities/costs/mana/ManaCostImpl.java
index 1fd9bdd5f90..c557d260326 100644
--- a/Mage/src/main/java/mage/abilities/costs/mana/ManaCostImpl.java
+++ b/Mage/src/main/java/mage/abilities/costs/mana/ManaCostImpl.java
@@ -72,7 +72,7 @@ public abstract class ManaCostImpl extends CostImpl implements ManaCost {
}
@Override
- public ManaOptions getOptions() {
+ public final ManaOptions getOptions() {
return getOptions(true);
}
diff --git a/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java b/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java
index 118379ccef8..70ba8044ff5 100644
--- a/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java
+++ b/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java
@@ -439,7 +439,7 @@ public class ManaCostsImpl extends ArrayList implements M
this.add(new ColoredManaCost(ColoredManaSymbol.lookup(symbol.charAt(0))));
} else // check X wasn't added before
if (modifierForX == 0) {
- // count X occurence
+ // count X occurrence
for (String s : symbols) {
if (s.equals("X")) {
modifierForX++;
diff --git a/Mage/src/main/java/mage/abilities/costs/mana/VariableManaCost.java b/Mage/src/main/java/mage/abilities/costs/mana/VariableManaCost.java
index d2fa2b16f75..8a8d3e25a00 100644
--- a/Mage/src/main/java/mage/abilities/costs/mana/VariableManaCost.java
+++ b/Mage/src/main/java/mage/abilities/costs/mana/VariableManaCost.java
@@ -3,17 +3,19 @@ package mage.abilities.costs.mana;
import mage.Mana;
import mage.abilities.Ability;
import mage.abilities.costs.Cost;
-import mage.abilities.costs.VariableCost;
+import mage.abilities.costs.MinMaxVariableCost;
import mage.abilities.costs.VariableCostType;
+import mage.abilities.mana.ManaOptions;
import mage.constants.ColoredManaSymbol;
import mage.filter.FilterMana;
import mage.game.Game;
import mage.players.ManaPool;
+import mage.util.CardUtil;
/**
* @author BetaSteward_at_googlemail.com, JayDi85
*/
-public final class VariableManaCost extends ManaCostImpl implements VariableCost {
+public class VariableManaCost extends ManaCostImpl implements MinMaxVariableCost {
// variable mana cost usage on 2019-06-20:
// 1. as X value in spell/ability cast (announce X, set VariableManaCost as paid and add generic mana to pay instead)
@@ -23,7 +25,7 @@ public final class VariableManaCost extends ManaCostImpl implements VariableCost
protected int xInstancesCount; // number of {X} instances in cost like {X} or {X}{X}
protected int xValue = 0; // final X value after announce and replace events
protected int xPay = 0; // final/total need pay after announce and replace events (example: {X}{X}, X=3, xPay = 6)
- protected boolean wasAnnounced = false;
+ protected boolean wasAnnounced = false; // X was announced by player or auto-defined by CostAdjuster
protected FilterMana filter; // mana filter that can be used for that cost
protected int minX = 0;
@@ -59,6 +61,18 @@ public final class VariableManaCost extends ManaCostImpl implements VariableCost
return 0;
}
+ @Override
+ public ManaOptions getOptions(boolean canPayLifeCost) {
+ ManaOptions res = new ManaOptions();
+
+ // limit mana options for better performance
+ CardUtil.distributeValues(10, getMinX(), getMaxX()).forEach(value -> {
+ res.add(Mana.GenericMana(value));
+ });
+
+ return res;
+ }
+
@Override
public void assignPayment(Game game, Ability ability, ManaPool pool, Cost costToPay) {
// X mana cost always pays as generic mana
@@ -86,6 +100,10 @@ public final class VariableManaCost extends ManaCostImpl implements VariableCost
return this.isColorlessPaid(xPay);
}
+ public boolean wasAnnounced() {
+ return this.wasAnnounced;
+ }
+
@Override
public VariableManaCost getUnpaid() {
return this;
@@ -122,18 +140,22 @@ public final class VariableManaCost extends ManaCostImpl implements VariableCost
return this.xInstancesCount;
}
+ @Override
public int getMinX() {
return minX;
}
+ @Override
public void setMinX(int minX) {
this.minX = minX;
}
+ @Override
public int getMaxX() {
return maxX;
}
+ @Override
public void setMaxX(int maxX) {
this.maxX = maxX;
}
diff --git a/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardsInControllerHandCount.java b/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardsInControllerHandCount.java
index ea8cde0572d..6163ffdf6a2 100644
--- a/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardsInControllerHandCount.java
+++ b/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardsInControllerHandCount.java
@@ -3,35 +3,58 @@ package mage.abilities.dynamicvalue.common;
import mage.abilities.Ability;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.effects.Effect;
+import mage.abilities.hint.Hint;
+import mage.abilities.hint.ValueHint;
+import mage.filter.FilterCard;
+import mage.filter.StaticFilters;
+import mage.game.Controllable;
import mage.game.Game;
import mage.players.Player;
+import java.util.Optional;
+import java.util.Set;
+
public enum CardsInControllerHandCount implements DynamicValue {
- instance;
+
+ ANY(StaticFilters.FILTER_CARD_CARDS),
+ CREATURES(StaticFilters.FILTER_CARD_CREATURES),
+ LANDS(StaticFilters.FILTER_CARD_LANDS);
+
+ private final FilterCard filter;
+ private final ValueHint hint;
+
+ CardsInControllerHandCount(FilterCard filter) {
+ this.filter = filter;
+ this.hint = new ValueHint(filter.getMessage() + " in your hand", this);
+ }
@Override
public int calculate(Game game, Ability sourceAbility, Effect effect) {
- if (sourceAbility != null) {
- Player controller = game.getPlayer(sourceAbility.getControllerId());
- if (controller != null) {
- return controller.getHand().size();
- }
- }
- return 0;
+ return Optional
+ .ofNullable(sourceAbility)
+ .map(Controllable::getControllerId)
+ .map(game::getPlayer)
+ .map(Player::getHand)
+ .map(Set::size)
+ .orElse(0);
}
@Override
public CardsInControllerHandCount copy() {
- return CardsInControllerHandCount.instance;
+ return this;
}
@Override
public String getMessage() {
- return "cards in your hand";
+ return this.filter.getMessage() + " in your hand";
}
@Override
public String toString() {
return "1";
}
+
+ public Hint getHint() {
+ return this.hint;
+ }
}
diff --git a/Mage/src/main/java/mage/abilities/dynamicvalue/common/GreatestToughnessAmongControlledCreaturesValue.java b/Mage/src/main/java/mage/abilities/dynamicvalue/common/GreatestToughnessAmongControlledCreaturesValue.java
index bd60ddb158f..4ec730c0f08 100644
--- a/Mage/src/main/java/mage/abilities/dynamicvalue/common/GreatestToughnessAmongControlledCreaturesValue.java
+++ b/Mage/src/main/java/mage/abilities/dynamicvalue/common/GreatestToughnessAmongControlledCreaturesValue.java
@@ -1,4 +1,3 @@
-
package mage.abilities.dynamicvalue.common;
import mage.MageInt;
@@ -6,6 +5,9 @@ import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.effects.Effect;
+import mage.abilities.hint.Hint;
+import mage.abilities.hint.ValueHint;
+import mage.filter.FilterPermanent;
import mage.filter.StaticFilters;
import mage.game.Game;
@@ -13,16 +15,21 @@ import mage.game.Game;
* @author TheElk801
*/
public enum GreatestToughnessAmongControlledCreaturesValue implements DynamicValue {
- instance;
+ ALL(StaticFilters.FILTER_CONTROLLED_CREATURES),
+ OTHER(StaticFilters.FILTER_OTHER_CONTROLLED_CREATURES);
+ private final FilterPermanent filter;
+ private final Hint hint;
+
+ GreatestToughnessAmongControlledCreaturesValue(FilterPermanent filter) {
+ this.filter = filter;
+ this.hint = new ValueHint("The greatest toughness among " + filter.getMessage(), this);
+ }
@Override
public int calculate(Game game, Ability sourceAbility, Effect effect) {
return game
.getBattlefield()
- .getActivePermanents(
- StaticFilters.FILTER_CONTROLLED_CREATURE,
- sourceAbility.getControllerId(), game
- )
+ .getActivePermanents(filter, sourceAbility.getControllerId(), game)
.stream()
.map(MageObject::getToughness)
.mapToInt(MageInt::getValue)
@@ -32,12 +39,12 @@ public enum GreatestToughnessAmongControlledCreaturesValue implements DynamicVal
@Override
public GreatestToughnessAmongControlledCreaturesValue copy() {
- return GreatestToughnessAmongControlledCreaturesValue.instance;
+ return this;
}
@Override
public String getMessage() {
- return "the greatest toughness among creatures you control";
+ return "the greatest toughness among " + filter.getMessage();
}
@Override
@@ -45,4 +52,7 @@ public enum GreatestToughnessAmongControlledCreaturesValue implements DynamicVal
return "X";
}
+ public Hint getHint() {
+ return hint;
+ }
}
diff --git a/Mage/src/main/java/mage/abilities/effects/AsThoughEffectImpl.java b/Mage/src/main/java/mage/abilities/effects/AsThoughEffectImpl.java
index 09f1c74629b..6d8e2d6fea4 100644
--- a/Mage/src/main/java/mage/abilities/effects/AsThoughEffectImpl.java
+++ b/Mage/src/main/java/mage/abilities/effects/AsThoughEffectImpl.java
@@ -6,14 +6,13 @@ import mage.abilities.ActivatedAbility;
import mage.cards.Card;
import mage.cards.ModalDoubleFacedCard;
import mage.cards.SplitCard;
+import mage.cards.CardWithSpellOption;
import mage.constants.*;
import mage.game.Game;
import mage.players.Player;
import java.util.UUID;
-import mage.cards.AdventureCard;
-
/**
* @author BetaSteward_at_googlemail.com
*/
@@ -103,9 +102,9 @@ public abstract class AsThoughEffectImpl extends ContinuousEffectImpl implements
if (!rightCard.isLand(game)) {
player.setCastSourceIdWithAlternateMana(rightCard.getId(), null, rightCard.getSpellAbility().getCosts(), identifier);
}
- } else if (card instanceof AdventureCard) {
+ } else if (card instanceof CardWithSpellOption) {
Card creatureCard = card.getMainCard();
- Card spellCard = ((AdventureCard) card).getSpellCard();
+ Card spellCard = ((CardWithSpellOption) card).getSpellCard();
player.setCastSourceIdWithAlternateMana(creatureCard.getId(), null, creatureCard.getSpellAbility().getCosts(), identifier);
player.setCastSourceIdWithAlternateMana(spellCard.getId(), null, spellCard.getSpellAbility().getCosts(), identifier);
}
diff --git a/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java b/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java
index ea47fe4f39e..a60322ba64a 100644
--- a/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java
+++ b/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java
@@ -549,9 +549,9 @@ public class ContinuousEffects implements Serializable {
// rules:
// 708.4. In every zone except the stack, the characteristics of a split card are those of its two halves combined.
idToCheck = ((SplitCardHalf) objectToCheck).getMainCard().getId();
- } else if (!type.needPlayCardAbility() && objectToCheck instanceof AdventureCardSpell) {
- // adventure spell uses alternative characteristics for spell/stack, all other cases must use main card
- idToCheck = ((AdventureCardSpell) objectToCheck).getMainCard().getId();
+ } else if (!type.needPlayCardAbility() && objectToCheck instanceof CardWithSpellOption) {
+ // adventure/omen spell uses alternative characteristics for spell/stack, all other cases must use main card
+ idToCheck = ((CardWithSpellOption) objectToCheck).getMainCard().getId();
} else if (!type.needPlayCardAbility() && objectToCheck instanceof ModalDoubleFacedCardHalf) {
// each mdf side uses own characteristics to check for playing, all other cases must use main card
// rules:
@@ -688,6 +688,8 @@ public class ContinuousEffects implements Serializable {
* the battlefield for
* {@link CostModificationEffect cost modification effects} and applies them
* if necessary.
+ *
+ * Warning, don't forget to call ability.adjustX before any cost modifications
*
* @param abilityToModify
* @param game
@@ -695,6 +697,10 @@ public class ContinuousEffects implements Serializable {
public void costModification(Ability abilityToModify, Game game) {
List costEffects = getApplicableCostModificationEffects(game);
+ // add dynamic costs from X and other places
+ abilityToModify.adjustCostsPrepare(game);
+
+ abilityToModify.adjustCostsModify(game, CostModificationType.INCREASE_COST);
for (CostModificationEffect effect : costEffects) {
if (effect.getModificationType() == CostModificationType.INCREASE_COST) {
Set abilities = costModificationEffects.getAbility(effect.getId());
@@ -706,6 +712,7 @@ public class ContinuousEffects implements Serializable {
}
}
+ abilityToModify.adjustCostsModify(game, CostModificationType.REDUCE_COST);
for (CostModificationEffect effect : costEffects) {
if (effect.getModificationType() == CostModificationType.REDUCE_COST) {
Set abilities = costModificationEffects.getAbility(effect.getId());
@@ -717,6 +724,7 @@ public class ContinuousEffects implements Serializable {
}
}
+ abilityToModify.adjustCostsModify(game, CostModificationType.SET_COST);
for (CostModificationEffect effect : costEffects) {
if (effect.getModificationType() == CostModificationType.SET_COST) {
Set abilities = costModificationEffects.getAbility(effect.getId());
diff --git a/Mage/src/main/java/mage/abilities/effects/common/ChooseModeEffect.java b/Mage/src/main/java/mage/abilities/effects/common/ChooseModeEffect.java
index 5ca8e531c68..978e0cb8f2e 100644
--- a/Mage/src/main/java/mage/abilities/effects/common/ChooseModeEffect.java
+++ b/Mage/src/main/java/mage/abilities/effects/common/ChooseModeEffect.java
@@ -1,39 +1,41 @@
package mage.abilities.effects.common;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
import mage.abilities.Ability;
import mage.abilities.effects.OneShotEffect;
import mage.choices.Choice;
import mage.choices.ChoiceImpl;
+import mage.constants.ModeChoice;
import mage.constants.Outcome;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.util.CardUtil;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
/**
* @author LevelX2
*/
public class ChooseModeEffect extends OneShotEffect {
- protected final List modes = new ArrayList<>();
- protected final String choiceMessage;
+ protected final List modes = new ArrayList<>();
+ protected final String message;
- public ChooseModeEffect(String choiceMessage, String... modes) {
+ public ChooseModeEffect(ModeChoice... modes) {
super(Outcome.Neutral);
- this.choiceMessage = choiceMessage;
this.modes.addAll(Arrays.asList(modes));
- this.staticText = setText();
+ this.message = makeMessage(this.modes);
+ this.staticText = "choose " + this.message;
}
protected ChooseModeEffect(final ChooseModeEffect effect) {
super(effect);
this.modes.addAll(effect.modes);
- this.choiceMessage = effect.choiceMessage;
+ this.message = effect.message;
}
@Override
@@ -48,34 +50,27 @@ public class ChooseModeEffect extends OneShotEffect {
if (sourcePermanent == null) {
sourcePermanent = game.getPermanentEntering(source.getSourceId());
}
- if (controller != null && sourcePermanent != null) {
- Choice choice = new ChoiceImpl(true);
- choice.setMessage(choiceMessage + CardUtil.getSourceLogName(game, source));
- choice.getChoices().addAll(modes);
- if (controller.choose(Outcome.Neutral, choice, game)) {
- if (!game.isSimulation()) {
- game.informPlayers(sourcePermanent.getLogName() + ": " + controller.getLogName() + " has chosen " + choice.getChoice());
- }
- game.getState().setValue(source.getSourceId() + "_modeChoice", choice.getChoice());
- sourcePermanent.addInfo("_modeChoice", "Chosen mode: " + choice.getChoice() + "", game);
- return true;
- }
+ if (controller == null || sourcePermanent == null) {
+ return false;
}
- return false;
+ Choice choice = new ChoiceImpl(true);
+ choice.setMessage(message + "? (" + CardUtil.getSourceLogName(game, source) + ')');
+ choice.getChoices().addAll(modes.stream().map(ModeChoice::toString).collect(Collectors.toList()));
+ if (!controller.choose(Outcome.Neutral, choice, game)) {
+ return false;
+ }
+ game.informPlayers(sourcePermanent.getLogName() + ": " + controller.getLogName() + " has chosen " + choice.getChoice());
+ game.getState().setValue(source.getSourceId() + "_modeChoice", choice.getChoice());
+ sourcePermanent.addInfo("_modeChoice", "Chosen mode: " + choice.getChoice() + "", game);
+ return true;
}
- private String setText() {
- StringBuilder sb = new StringBuilder("choose ");
- int count = 0;
- for (String choice : modes) {
- count++;
- sb.append(choice);
- if (count + 1 < modes.size()) {
- sb.append(", ");
- } else if (count < modes.size()) {
- sb.append(" or ");
- }
- }
- return sb.toString();
+ private static String makeMessage(List modeChoices) {
+ return CardUtil.concatWithOr(
+ modeChoices
+ .stream()
+ .map(ModeChoice::toString)
+ .collect(Collectors.toList())
+ );
}
}
diff --git a/Mage/src/main/java/mage/abilities/effects/common/CreateXXTokenExiledEffectManaValueEffect.java b/Mage/src/main/java/mage/abilities/effects/common/CreateXXTokenExiledEffectManaValueEffect.java
new file mode 100644
index 00000000000..3b58617a27c
--- /dev/null
+++ b/Mage/src/main/java/mage/abilities/effects/common/CreateXXTokenExiledEffectManaValueEffect.java
@@ -0,0 +1,74 @@
+package mage.abilities.effects.common;
+
+import mage.MageObject;
+import mage.abilities.Ability;
+import mage.abilities.effects.OneShotEffect;
+import mage.constants.Outcome;
+import mage.game.ExileZone;
+import mage.game.Game;
+import mage.game.permanent.Permanent;
+import mage.game.permanent.token.Token;
+import mage.util.CardUtil;
+
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+
+/**
+ * @author TheElk801
+ */
+public class CreateXXTokenExiledEffectManaValueEffect extends OneShotEffect {
+
+ private final Function tokenMaker;
+
+ public CreateXXTokenExiledEffectManaValueEffect(Function tokenMaker, String description) {
+ super(Outcome.Benefit);
+ this.tokenMaker = tokenMaker;
+ staticText = "the exiled card's owner creates an X/X " + description +
+ "creature token, where X is the mana value of the exiled card";
+ }
+
+ private CreateXXTokenExiledEffectManaValueEffect(final CreateXXTokenExiledEffectManaValueEffect effect) {
+ super(effect);
+ this.tokenMaker = effect.tokenMaker;
+ }
+
+ @Override
+ public CreateXXTokenExiledEffectManaValueEffect copy() {
+ return new CreateXXTokenExiledEffectManaValueEffect(this);
+ }
+
+ @Override
+ public boolean apply(Game game, Ability source) {
+ Permanent permanentLeftBattlefield = (Permanent) getValue("permanentLeftBattlefield");
+ ExileZone exile = game.getExile().getExileZone(
+ CardUtil.getExileZoneId(game, source.getSourceId(), permanentLeftBattlefield.getZoneChangeCounter(game))
+ );
+ if (exile == null || exile.isEmpty()) {
+ return false;
+ }
+ // From ZNR Release Notes:
+ // https://magic.wizards.com/en/articles/archive/feature/zendikar-rising-release-notes-2020-09-10
+ // If Skyclave Apparition's first ability exiled more than one card owned by a single player,
+ // that player creates a token with power and toughness equal to the sum of those cards' converted mana costs.
+ // If the first ability exiled cards owned by more than one player, each of those players creates a token
+ // with power and toughness equal to the sum of the converted mana costs of all cards exiled by the first ability.
+ Set owners = new HashSet<>();
+ int totalCMC = exile
+ .getCards(game)
+ .stream()
+ .filter(Objects::nonNull)
+ .map(card -> {
+ owners.add(card.getOwnerId());
+ return card;
+ })
+ .mapToInt(MageObject::getManaValue)
+ .sum();
+ for (UUID playerId : owners) {
+ tokenMaker.apply(totalCMC).putOntoBattlefield(1, game, source, playerId);
+ }
+ return true;
+ }
+}
diff --git a/Mage/src/main/java/mage/abilities/effects/common/ExileAdventureSpellEffect.java b/Mage/src/main/java/mage/abilities/effects/common/ExileAdventureSpellEffect.java
index 907881dea21..706af3cd62a 100644
--- a/Mage/src/main/java/mage/abilities/effects/common/ExileAdventureSpellEffect.java
+++ b/Mage/src/main/java/mage/abilities/effects/common/ExileAdventureSpellEffect.java
@@ -5,7 +5,7 @@ import mage.abilities.MageSingleton;
import mage.abilities.effects.AsThoughEffectImpl;
import mage.abilities.effects.ContinuousEffect;
import mage.abilities.effects.OneShotEffect;
-import mage.cards.AdventureCardSpell;
+import mage.cards.AdventureSpellCard;
import mage.cards.Card;
import mage.constants.AsThoughEffectType;
import mage.constants.Duration;
@@ -51,10 +51,10 @@ public class ExileAdventureSpellEffect extends OneShotEffect implements MageSing
Spell spell = game.getStack().getSpell(source.getId());
if (spell != null) {
Card spellCard = spell.getCard();
- if (spellCard instanceof AdventureCardSpell) {
+ if (spellCard instanceof AdventureSpellCard) {
UUID exileId = adventureExileId(controller.getId(), game);
game.getExile().createZone(exileId, "On an Adventure from " + controller.getName());
- AdventureCardSpell adventureSpellCard = (AdventureCardSpell) spellCard;
+ AdventureSpellCard adventureSpellCard = (AdventureSpellCard) spellCard;
Card parentCard = adventureSpellCard.getParentCard();
if (controller.moveCardsToExile(parentCard, source, game, true, exileId, "On an Adventure from " + controller.getName())) {
ContinuousEffect effect = new AdventureCastFromExileEffect();
diff --git a/Mage/src/main/java/mage/abilities/effects/common/PermanentsEnterBattlefieldTappedEffect.java b/Mage/src/main/java/mage/abilities/effects/common/PermanentsEnterBattlefieldTappedEffect.java
index e9d949ac4ec..7eb63a0be7f 100644
--- a/Mage/src/main/java/mage/abilities/effects/common/PermanentsEnterBattlefieldTappedEffect.java
+++ b/Mage/src/main/java/mage/abilities/effects/common/PermanentsEnterBattlefieldTappedEffect.java
@@ -63,7 +63,7 @@ public class PermanentsEnterBattlefieldTappedEffect extends ReplacementEffectImp
return staticText;
}
return filter.getMessage()
- + " enter tapped"
- + (duration == Duration.EndOfTurn ? " this turn" : "");
+ + " enter" + (filter.getMessage().startsWith("each") ? "s" : "")
+ + " tapped" + (duration == Duration.EndOfTurn ? " this turn" : "");
}
}
diff --git a/Mage/src/main/java/mage/abilities/effects/common/ReturnFromGraveyardToBattlefieldWithCounterTargetEffect.java b/Mage/src/main/java/mage/abilities/effects/common/ReturnFromGraveyardToBattlefieldWithCounterTargetEffect.java
index 2bf4ce8bc1f..7d85a7a1a30 100644
--- a/Mage/src/main/java/mage/abilities/effects/common/ReturnFromGraveyardToBattlefieldWithCounterTargetEffect.java
+++ b/Mage/src/main/java/mage/abilities/effects/common/ReturnFromGraveyardToBattlefieldWithCounterTargetEffect.java
@@ -7,6 +7,8 @@ import mage.counters.Counters;
import mage.game.Game;
import mage.util.CardUtil;
+import java.util.ArrayList;
+import java.util.List;
import java.util.UUID;
/**
@@ -17,15 +19,17 @@ public class ReturnFromGraveyardToBattlefieldWithCounterTargetEffect extends Ret
private final Counters counters;
private final String counterText;
- public ReturnFromGraveyardToBattlefieldWithCounterTargetEffect(Counter counter) {
- this(counter, false);
+ public ReturnFromGraveyardToBattlefieldWithCounterTargetEffect(Counter... counters) {
+ this(false, counters);
}
- public ReturnFromGraveyardToBattlefieldWithCounterTargetEffect(Counter counter, boolean additional) {
+ public ReturnFromGraveyardToBattlefieldWithCounterTargetEffect(boolean additional, Counter... counters) {
super(false);
this.counters = new Counters();
- this.counters.addCounter(counter);
- this.counterText = makeText(counter, additional);
+ for (Counter counter : counters) {
+ this.counters.addCounter(counter);
+ }
+ this.counterText = makeText(additional, counters);
}
protected ReturnFromGraveyardToBattlefieldWithCounterTargetEffect(final ReturnFromGraveyardToBattlefieldWithCounterTargetEffect effect) {
@@ -47,22 +51,26 @@ public class ReturnFromGraveyardToBattlefieldWithCounterTargetEffect extends Ret
return super.apply(game, source);
}
- private String makeText(Counter counter, boolean additional) {
- StringBuilder sb = new StringBuilder(" with ");
- if (counter.getCount() == 1) {
- if (additional) {
- sb.append("an additional ").append(counter.getName());
+ private static String makeText(boolean additional, Counter... counters) {
+ List strings = new ArrayList<>();
+ for (Counter counter : counters) {
+ StringBuilder sb = new StringBuilder();
+ if (counter.getCount() == 1) {
+ if (additional) {
+ sb.append("an additional ").append(counter.getName());
+ } else {
+ sb.append(CardUtil.addArticle(counter.getName()));
+ }
+ sb.append(" counter");
} else {
- sb.append(CardUtil.addArticle(counter.getName()));
+ sb.append(CardUtil.numberToText(counter.getCount()));
+ sb.append(additional ? " additional " : " ");
+ sb.append(counter.getName());
+ sb.append(" counters");
}
- sb.append(" counter");
- } else {
- sb.append(CardUtil.numberToText(counter.getCount()));
- sb.append(additional ? " additional " : " ");
- sb.append(counter.getName());
- sb.append(" counters");
+ strings.add(sb.toString());
}
- return sb.toString();
+ return " with " + CardUtil.concatWithAnd(strings);
}
@Override
diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAnchorWordAbilitySourceEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAnchorWordAbilitySourceEffect.java
new file mode 100644
index 00000000000..f4af6963691
--- /dev/null
+++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAnchorWordAbilitySourceEffect.java
@@ -0,0 +1,52 @@
+package mage.abilities.effects.common.continuous;
+
+import mage.abilities.Ability;
+import mage.abilities.common.SimpleStaticAbility;
+import mage.abilities.effects.ContinuousEffectImpl;
+import mage.abilities.effects.Effect;
+import mage.constants.*;
+import mage.game.Game;
+import mage.game.permanent.Permanent;
+
+/**
+ * @author TheElk801
+ */
+public class GainAnchorWordAbilitySourceEffect extends ContinuousEffectImpl {
+
+ private final Ability ability;
+ private final ModeChoice modeChoice;
+
+ public GainAnchorWordAbilitySourceEffect(Effect effect, ModeChoice modeChoice) {
+ this(new SimpleStaticAbility(effect), modeChoice);
+ }
+
+ public GainAnchorWordAbilitySourceEffect(Ability ability, ModeChoice modeChoice) {
+ super(Duration.WhileOnBattlefield, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility);
+ this.staticText = "&bull " + modeChoice + " — " + ability.getRule();
+ this.ability = ability;
+ this.modeChoice = modeChoice;
+ this.ability.setRuleVisible(false);
+ this.generateGainAbilityDependencies(ability, null);
+ }
+
+ private GainAnchorWordAbilitySourceEffect(final GainAnchorWordAbilitySourceEffect effect) {
+ super(effect);
+ this.modeChoice = effect.modeChoice;
+ this.ability = effect.ability;
+ }
+
+ @Override
+ public boolean apply(Game game, Ability source) {
+ Permanent permanent = source.getSourcePermanentIfItStillExists(game);
+ if (permanent == null || !modeChoice.checkMode(game, source)) {
+ return false;
+ }
+ permanent.addAbility(ability, source.getSourceId(), game);
+ return true;
+ }
+
+ @Override
+ public GainAnchorWordAbilitySourceEffect copy() {
+ return new GainAnchorWordAbilitySourceEffect(this);
+ }
+}
diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/PlayerCanOnlyAttackInDirectionRestrictionEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/PlayerCanOnlyAttackInDirectionRestrictionEffect.java
index b74ab65717c..8aaefded6da 100644
--- a/Mage/src/main/java/mage/abilities/effects/common/continuous/PlayerCanOnlyAttackInDirectionRestrictionEffect.java
+++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/PlayerCanOnlyAttackInDirectionRestrictionEffect.java
@@ -6,6 +6,7 @@ import mage.abilities.effects.RestrictionEffect;
import mage.abilities.effects.common.ChooseModeEffect;
import mage.constants.CardType;
import mage.constants.Duration;
+import mage.constants.ModeChoice;
import mage.constants.Outcome;
import mage.game.Game;
import mage.game.permanent.Permanent;
@@ -19,9 +20,6 @@ import java.util.UUID;
*/
public class PlayerCanOnlyAttackInDirectionRestrictionEffect extends RestrictionEffect {
- public static final String ALLOW_ATTACKING_LEFT = "Allow attacking left";
- public static final String ALLOW_ATTACKING_RIGHT = "Allow attacking right";
-
public PlayerCanOnlyAttackInDirectionRestrictionEffect(Duration duration, String directionText) {
super(duration, Outcome.Neutral);
staticText = duration + (duration.toString().isEmpty() ? "" : ", ")
@@ -39,10 +37,7 @@ public class PlayerCanOnlyAttackInDirectionRestrictionEffect extends Restriction
}
public static Effect choiceEffect() {
- return new ChooseModeEffect(
- "Choose a direction to allow attacking in.",
- ALLOW_ATTACKING_LEFT, ALLOW_ATTACKING_RIGHT
- ).setText("choose left or right");
+ return new ChooseModeEffect(ModeChoice.LEFT, ModeChoice.RIGHT);
}
@Override
@@ -55,10 +50,13 @@ public class PlayerCanOnlyAttackInDirectionRestrictionEffect extends Restriction
if (defenderId == null) {
return true;
}
-
- String allowedDirection = (String) game.getState().getValue(source.getSourceId() + "_modeChoice");
- if (allowedDirection == null) {
- return true; // If no choice was made, the ability has no effect.
+ boolean left;
+ if (ModeChoice.LEFT.checkMode(game, source)) {
+ left = true;
+ } else if (ModeChoice.RIGHT.checkMode(game, source)) {
+ left = false;
+ } else {
+ return false; // If no choice was made, the ability has no effect.
}
Player playerAttacking = game.getPlayer(attacker.getControllerId());
@@ -83,18 +81,7 @@ public class PlayerCanOnlyAttackInDirectionRestrictionEffect extends Restriction
}
PlayerList playerList = game.getState().getPlayerList(playerAttacking.getId());
- if (allowedDirection.equals(ALLOW_ATTACKING_LEFT)
- && !playerList.getNext().equals(playerDefending.getId())) {
- // the defender is not the player to the left
- return false;
- }
- if (allowedDirection.equals(ALLOW_ATTACKING_RIGHT)
- && !playerList.getPrevious().equals(playerDefending.getId())) {
- // the defender is not the player to the right
- return false;
- }
-
- return true;
+ return (!left || playerList.getNext().equals(playerDefending.getId()))
+ && (left || playerList.getPrevious().equals(playerDefending.getId()));
}
-
}
diff --git a/Mage/src/main/java/mage/abilities/effects/common/discard/LookTargetHandChooseDiscardEffect.java b/Mage/src/main/java/mage/abilities/effects/common/discard/LookTargetHandChooseDiscardEffect.java
index e8dc09bd813..7cacad39c33 100644
--- a/Mage/src/main/java/mage/abilities/effects/common/discard/LookTargetHandChooseDiscardEffect.java
+++ b/Mage/src/main/java/mage/abilities/effects/common/discard/LookTargetHandChooseDiscardEffect.java
@@ -62,6 +62,9 @@ public class LookTargetHandChooseDiscardEffect extends OneShotEffect {
}
TargetCard target = new TargetCardInHand(upTo ? 0 : num, num, filter);
if (controller.choose(Outcome.Discard, player.getHand(), target, source, game)) {
+ // TODO: must fizzle discard effect on not full choice
+ // - tests: affected (allow to choose and discard 1 instead 2)
+ // - real game: need to check
player.discard(new CardsImpl(target.getTargets()), false, source, game);
}
return true;
diff --git a/Mage/src/main/java/mage/abilities/effects/common/replacement/AdditionalTriggersAttackingReplacementEffect.java b/Mage/src/main/java/mage/abilities/effects/common/replacement/AdditionalTriggersAttackingReplacementEffect.java
new file mode 100644
index 00000000000..85516648f74
--- /dev/null
+++ b/Mage/src/main/java/mage/abilities/effects/common/replacement/AdditionalTriggersAttackingReplacementEffect.java
@@ -0,0 +1,80 @@
+package mage.abilities.effects.common.replacement;
+
+import mage.abilities.Ability;
+import mage.abilities.effects.ReplacementEffectImpl;
+import mage.constants.Duration;
+import mage.constants.Outcome;
+import mage.game.Controllable;
+import mage.game.Game;
+import mage.game.events.DefenderAttackedEvent;
+import mage.game.events.GameEvent;
+import mage.game.events.NumberOfTriggersEvent;
+import mage.game.permanent.Permanent;
+
+/**
+ * @author TheElk801
+ */
+public class AdditionalTriggersAttackingReplacementEffect extends ReplacementEffectImpl {
+
+ private final boolean onlyControlled;
+
+ public AdditionalTriggersAttackingReplacementEffect(boolean onlyControlled) {
+ super(Duration.WhileOnBattlefield, Outcome.Benefit);
+ this.onlyControlled = onlyControlled;
+ staticText = "if a creature " + (onlyControlled ? "you control " : "") + "attacking causes a triggered ability " +
+ "of a permanent you control to trigger, that ability triggers an additional time";
+ }
+
+ private AdditionalTriggersAttackingReplacementEffect(final AdditionalTriggersAttackingReplacementEffect effect) {
+ super(effect);
+ this.onlyControlled = effect.onlyControlled;
+ }
+
+ @Override
+ public AdditionalTriggersAttackingReplacementEffect copy() {
+ return new AdditionalTriggersAttackingReplacementEffect(this);
+ }
+
+ @Override
+ public boolean checksEventType(GameEvent event, Game game) {
+ return event.getType() == GameEvent.EventType.NUMBER_OF_TRIGGERS;
+ }
+
+ @Override
+ public boolean applies(GameEvent event, Ability source, Game game) {
+ NumberOfTriggersEvent numberOfTriggersEvent = (NumberOfTriggersEvent) event;
+ Permanent sourcePermanent = game.getPermanent(numberOfTriggersEvent.getSourceId());
+ if (sourcePermanent == null || !sourcePermanent.isControlledBy(source.getControllerId())) {
+ return false;
+ }
+ GameEvent sourceEvent = numberOfTriggersEvent.getSourceEvent();
+ if (sourceEvent == null) {
+ return false;
+ }
+
+ switch (sourceEvent.getType()) {
+ case ATTACKER_DECLARED:
+ return !onlyControlled || source.isControlledBy(sourceEvent.getPlayerId());
+ case DECLARED_ATTACKERS:
+ return !onlyControlled || game
+ .getCombat()
+ .getAttackers()
+ .stream()
+ .map(game::getControllerId)
+ .anyMatch(source::isControlledBy);
+ case DEFENDER_ATTACKED:
+ return !onlyControlled || ((DefenderAttackedEvent) sourceEvent)
+ .getAttackers(game)
+ .stream()
+ .map(Controllable::getControllerId)
+ .anyMatch(source::isControlledBy);
+ }
+ return false;
+ }
+
+ @Override
+ public boolean replaceEvent(GameEvent event, Ability source, Game game) {
+ event.setAmount(event.getAmount() + 1);
+ return false;
+ }
+}
diff --git a/Mage/src/main/java/mage/abilities/effects/keyword/EndureSourceEffect.java b/Mage/src/main/java/mage/abilities/effects/keyword/EndureSourceEffect.java
index d6a8b4e9fea..a9f23f7bcea 100644
--- a/Mage/src/main/java/mage/abilities/effects/keyword/EndureSourceEffect.java
+++ b/Mage/src/main/java/mage/abilities/effects/keyword/EndureSourceEffect.java
@@ -1,6 +1,8 @@
package mage.abilities.effects.keyword;
import mage.abilities.Ability;
+import mage.abilities.dynamicvalue.DynamicValue;
+import mage.abilities.dynamicvalue.common.StaticValue;
import mage.abilities.effects.OneShotEffect;
import mage.constants.Outcome;
import mage.counters.CounterType;
@@ -15,13 +17,17 @@ import mage.util.CardUtil;
*/
public class EndureSourceEffect extends OneShotEffect {
- private final int amount;
+ private final DynamicValue amount;
public EndureSourceEffect(int amount) {
this(amount, "it");
}
public EndureSourceEffect(int amount, String selfText) {
+ this(StaticValue.get(amount), selfText);
+ }
+
+ public EndureSourceEffect(DynamicValue amount, String selfText) {
super(Outcome.Benefit);
staticText = selfText + " endures " + amount;
this.amount = amount;
@@ -39,12 +45,23 @@ public class EndureSourceEffect extends OneShotEffect {
@Override
public boolean apply(Game game, Ability source) {
- Player player = game.getPlayer(source.getControllerId());
- if (player == null) {
+ return doEndure(
+ source.getSourcePermanentOrLKI(game),
+ amount.calculate(game, source, this),
+ game, source
+ );
+ }
+
+ public static boolean doEndure(Permanent permanent, int amount, Game game, Ability source) {
+ if (permanent == null || amount < 1) {
return false;
}
- Permanent permanent = source.getSourcePermanentIfItStillExists(game);
- if (permanent != null && player.chooseUse(
+ Player controller = game.getPlayer(permanent.getControllerId());
+ if (controller == null) {
+ return false;
+ }
+ if (permanent.getZoneChangeCounter(game) == game.getState().getZoneChangeCounter(permanent.getId())
+ && controller.chooseUse(
Outcome.BoostCreature, "Put " + CardUtil.numberToText(amount, "a") + " +1/+1 counter" +
(amount > 1 ? "s" : "") + " on " + permanent.getName() + " or create " +
CardUtil.addArticle("" + amount) + ' ' + amount + '/' + amount + " Spirit token?",
diff --git a/Mage/src/main/java/mage/abilities/keyword/CastFromGraveyardAbility.java b/Mage/src/main/java/mage/abilities/keyword/CastFromGraveyardAbility.java
new file mode 100644
index 00000000000..73de96cf5e7
--- /dev/null
+++ b/Mage/src/main/java/mage/abilities/keyword/CastFromGraveyardAbility.java
@@ -0,0 +1,197 @@
+package mage.abilities.keyword;
+
+import mage.abilities.Ability;
+import mage.abilities.SpellAbility;
+import mage.abilities.costs.Cost;
+import mage.abilities.costs.Costs;
+import mage.abilities.effects.ContinuousEffect;
+import mage.abilities.effects.ReplacementEffectImpl;
+import mage.cards.Card;
+import mage.cards.ModalDoubleFacedCard;
+import mage.cards.SplitCard;
+import mage.constants.*;
+import mage.game.Game;
+import mage.game.events.GameEvent;
+import mage.game.events.ZoneChangeEvent;
+import mage.players.Player;
+import mage.target.targetpointer.FixedTarget;
+import mage.util.CardUtil;
+
+import java.util.UUID;
+
+/**
+ * Base class for Flashback and Harmonize and any future ability which works similarly
+ *
+ * @author TheElk801
+ */
+public abstract class CastFromGraveyardAbility extends SpellAbility {
+
+ protected String abilityName;
+ private SpellAbility spellAbilityToResolve;
+
+ protected CastFromGraveyardAbility(Card card, Cost cost, SpellAbilityCastMode spellAbilityCastMode) {
+ super(null, "", Zone.GRAVEYARD, SpellAbilityType.BASE_ALTERNATE, spellAbilityCastMode);
+ this.setAdditionalCostsRuleVisible(false);
+ this.name = spellAbilityCastMode + " " + cost.getText();
+ this.addCost(cost);
+ this.timing = card.isSorcery() ? TimingRule.SORCERY : TimingRule.INSTANT;
+ }
+
+ protected CastFromGraveyardAbility(final CastFromGraveyardAbility ability) {
+ super(ability);
+ this.abilityName = ability.abilityName;
+ this.spellAbilityToResolve = ability.spellAbilityToResolve;
+ }
+
+ @Override
+ public ActivationStatus canActivate(UUID playerId, Game game) {
+ // flashback ability dynamicly added to all card's parts (split cards)
+ if (!super.canActivate(playerId, game).canActivate()) {
+ return ActivationStatus.getFalse();
+ }
+ Card card = game.getCard(getSourceId());
+ if (card == null) {
+ return ActivationStatus.getFalse();
+ }
+ // Card must be in the graveyard zone
+ if (game.getState().getZone(card.getId()) != Zone.GRAVEYARD) {
+ return ActivationStatus.getFalse();
+ }
+ // Cards with no Mana Costs cant't be flashbacked (e.g. Ancestral Vision)
+ if (card.getManaCost().isEmpty()) {
+ return ActivationStatus.getFalse();
+ }
+ // CastFromGraveyard can never cast a split card by Fuse, because Fuse only works from hand
+ // https://tappedout.net/mtg-questions/snapcaster-mage-and-flashback-on-a-fuse-card-one-or-both-halves-legal-targets/
+ if (card instanceof SplitCard) {
+ if (((SplitCard) card).getLeftHalfCard().getName().equals(abilityName)) {
+ return ((SplitCard) card).getLeftHalfCard().getSpellAbility().canActivate(playerId, game);
+ } else if (((SplitCard) card).getRightHalfCard().getName().equals(abilityName)) {
+ return ((SplitCard) card).getRightHalfCard().getSpellAbility().canActivate(playerId, game);
+ }
+ } else if (card instanceof ModalDoubleFacedCard) {
+ if (((ModalDoubleFacedCard) card).getLeftHalfCard().getName().equals(abilityName)) {
+ return ((ModalDoubleFacedCard) card).getLeftHalfCard().getSpellAbility().canActivate(playerId, game);
+ } else if (((ModalDoubleFacedCard) card).getRightHalfCard().getName().equals(abilityName)) {
+ return ((ModalDoubleFacedCard) card).getRightHalfCard().getSpellAbility().canActivate(playerId, game);
+ }
+ }
+ return card.getSpellAbility().canActivate(playerId, game);
+ }
+
+ @Override
+ public SpellAbility getSpellAbilityToResolve(Game game) {
+ Card card = game.getCard(getSourceId());
+ if (card == null || spellAbilityToResolve != null) {
+ return spellAbilityToResolve;
+ }
+ SpellAbility spellAbilityCopy;
+ if (card instanceof SplitCard) {
+ if (((SplitCard) card).getLeftHalfCard().getName().equals(abilityName)) {
+ spellAbilityCopy = ((SplitCard) card).getLeftHalfCard().getSpellAbility().copy();
+ } else if (((SplitCard) card).getRightHalfCard().getName().equals(abilityName)) {
+ spellAbilityCopy = ((SplitCard) card).getRightHalfCard().getSpellAbility().copy();
+ } else {
+ spellAbilityCopy = null;
+ }
+ } else if (card instanceof ModalDoubleFacedCard) {
+ if (((ModalDoubleFacedCard) card).getLeftHalfCard().getName().equals(abilityName)) {
+ spellAbilityCopy = ((ModalDoubleFacedCard) card).getLeftHalfCard().getSpellAbility().copy();
+ } else if (((ModalDoubleFacedCard) card).getRightHalfCard().getName().equals(abilityName)) {
+ spellAbilityCopy = ((ModalDoubleFacedCard) card).getRightHalfCard().getSpellAbility().copy();
+ } else {
+ spellAbilityCopy = null;
+ }
+ } else {
+ spellAbilityCopy = card.getSpellAbility().copy();
+ }
+ if (spellAbilityCopy == null) {
+ return null;
+ }
+ spellAbilityCopy.setId(this.getId());
+ spellAbilityCopy.clearManaCosts();
+ spellAbilityCopy.clearManaCostsToPay();
+ spellAbilityCopy.addCost(this.getCosts().copy());
+ spellAbilityCopy.addCost(this.getManaCosts().copy());
+ spellAbilityCopy.setSpellAbilityCastMode(this.getSpellAbilityCastMode());
+ spellAbilityToResolve = spellAbilityCopy;
+ ContinuousEffect effect = new CastFromGraveyardReplacementEffect();
+ effect.setTargetPointer(new FixedTarget(getSourceId(), game.getState().getZoneChangeCounter(getSourceId())));
+ game.addEffect(effect, this);
+ return spellAbilityToResolve;
+ }
+
+ @Override
+ public Costs getCosts() {
+ if (spellAbilityToResolve == null) {
+ return super.getCosts();
+ }
+ return spellAbilityToResolve.getCosts();
+ }
+
+ @Override
+ public String getRule(boolean all) {
+ return this.getRule();
+ }
+
+ /**
+ * Used for split card in PlayerImpl method:
+ * getOtherUseableActivatedAbilities
+ *
+ * @param abilityName
+ */
+ public CastFromGraveyardAbility setAbilityName(String abilityName) {
+ this.abilityName = abilityName;
+ return this;
+ }
+}
+
+class CastFromGraveyardReplacementEffect extends ReplacementEffectImpl {
+
+ public CastFromGraveyardReplacementEffect() {
+ super(Duration.OneUse, Outcome.Exile);
+ }
+
+ protected CastFromGraveyardReplacementEffect(final CastFromGraveyardReplacementEffect effect) {
+ super(effect);
+ }
+
+ @Override
+ public CastFromGraveyardReplacementEffect copy() {
+ return new CastFromGraveyardReplacementEffect(this);
+ }
+
+ @Override
+ public boolean replaceEvent(GameEvent event, Ability source, Game game) {
+ Player controller = game.getPlayer(source.getControllerId());
+ if (controller == null) {
+ return false;
+ }
+ Card card = game.getCard(event.getTargetId());
+ if (card == null) {
+ return false;
+ }
+ discard();
+ return controller.moveCards(
+ card, Zone.EXILED, source, game, false,
+ false, false, event.getAppliedEffects()
+ );
+ }
+
+ @Override
+ public boolean checksEventType(GameEvent event, Game game) {
+ return event.getType() == GameEvent.EventType.ZONE_CHANGE;
+ }
+
+ @Override
+ public boolean applies(GameEvent event, Ability source, Game game) {
+ UUID cardId = CardUtil.getMainCardId(game, source.getSourceId()); // for split cards
+ if (!cardId.equals(event.getTargetId())
+ || ((ZoneChangeEvent) event).getFromZone() != Zone.STACK
+ || ((ZoneChangeEvent) event).getToZone() == Zone.EXILED) {
+ return false;
+ }
+ int zcc = game.getState().getZoneChangeCounter(cardId);
+ return ((FixedTarget) getTargetPointer()).getZoneChangeCounter() + 1 == zcc;
+ }
+}
diff --git a/Mage/src/main/java/mage/abilities/keyword/FlashbackAbility.java b/Mage/src/main/java/mage/abilities/keyword/FlashbackAbility.java
index e88cf333684..4b7dae1d709 100644
--- a/Mage/src/main/java/mage/abilities/keyword/FlashbackAbility.java
+++ b/Mage/src/main/java/mage/abilities/keyword/FlashbackAbility.java
@@ -1,23 +1,8 @@
package mage.abilities.keyword;
-import mage.abilities.Ability;
-import mage.abilities.SpellAbility;
import mage.abilities.costs.Cost;
-import mage.abilities.costs.Costs;
-import mage.abilities.effects.ContinuousEffect;
-import mage.abilities.effects.ReplacementEffectImpl;
import mage.cards.Card;
-import mage.cards.ModalDoubleFacedCard;
-import mage.cards.SplitCard;
-import mage.constants.*;
-import mage.game.Game;
-import mage.game.events.GameEvent;
-import mage.game.events.ZoneChangeEvent;
-import mage.players.Player;
-import mage.target.targetpointer.FixedTarget;
-import mage.util.CardUtil;
-
-import java.util.UUID;
+import mage.constants.SpellAbilityCastMode;
/**
* 702.32. Flashback
@@ -33,106 +18,14 @@ import java.util.UUID;
*
* @author nantuko
*/
-public class FlashbackAbility extends SpellAbility {
-
- private String abilityName;
- private SpellAbility spellAbilityToResolve;
+public class FlashbackAbility extends CastFromGraveyardAbility {
public FlashbackAbility(Card card, Cost cost) {
- super(null, "", Zone.GRAVEYARD, SpellAbilityType.BASE_ALTERNATE, SpellAbilityCastMode.FLASHBACK);
- this.setAdditionalCostsRuleVisible(false);
- this.name = "Flashback " + cost.getText();
- this.addCost(cost);
- this.timing = card.isSorcery() ? TimingRule.SORCERY : TimingRule.INSTANT;
+ super(card, cost, SpellAbilityCastMode.FLASHBACK);
}
protected FlashbackAbility(final FlashbackAbility ability) {
super(ability);
- this.spellAbilityType = ability.spellAbilityType;
- this.abilityName = ability.abilityName;
- this.spellAbilityToResolve = ability.spellAbilityToResolve;
- }
-
- @Override
- public ActivationStatus canActivate(UUID playerId, Game game) {
- // flashback ability dynamicly added to all card's parts (split cards)
- if (super.canActivate(playerId, game).canActivate()) {
- Card card = game.getCard(getSourceId());
- if (card != null) {
- // Card must be in the graveyard zone
- if (game.getState().getZone(card.getId()) != Zone.GRAVEYARD) {
- return ActivationStatus.getFalse();
- }
- // Cards with no Mana Costs cant't be flashbacked (e.g. Ancestral Vision)
- if (card.getManaCost().isEmpty()) {
- return ActivationStatus.getFalse();
- }
- // Flashback can never cast a split card by Fuse, because Fuse only works from hand
- // https://tappedout.net/mtg-questions/snapcaster-mage-and-flashback-on-a-fuse-card-one-or-both-halves-legal-targets/
- if (card instanceof SplitCard) {
- if (((SplitCard) card).getLeftHalfCard().getName().equals(abilityName)) {
- return ((SplitCard) card).getLeftHalfCard().getSpellAbility().canActivate(playerId, game);
- } else if (((SplitCard) card).getRightHalfCard().getName().equals(abilityName)) {
- return ((SplitCard) card).getRightHalfCard().getSpellAbility().canActivate(playerId, game);
- }
- } else if (card instanceof ModalDoubleFacedCard) {
- if (((ModalDoubleFacedCard) card).getLeftHalfCard().getName().equals(abilityName)) {
- return ((ModalDoubleFacedCard) card).getLeftHalfCard().getSpellAbility().canActivate(playerId, game);
- } else if (((ModalDoubleFacedCard) card).getRightHalfCard().getName().equals(abilityName)) {
- return ((ModalDoubleFacedCard) card).getRightHalfCard().getSpellAbility().canActivate(playerId, game);
- }
- }
- return card.getSpellAbility().canActivate(playerId, game);
- }
- }
- return ActivationStatus.getFalse();
- }
-
- @Override
- public SpellAbility getSpellAbilityToResolve(Game game) {
- Card card = game.getCard(getSourceId());
- if (card != null) {
- if (spellAbilityToResolve == null) {
- SpellAbility spellAbilityCopy = null;
- if (card instanceof SplitCard) {
- if (((SplitCard) card).getLeftHalfCard().getName().equals(abilityName)) {
- spellAbilityCopy = ((SplitCard) card).getLeftHalfCard().getSpellAbility().copy();
- } else if (((SplitCard) card).getRightHalfCard().getName().equals(abilityName)) {
- spellAbilityCopy = ((SplitCard) card).getRightHalfCard().getSpellAbility().copy();
- }
- } else if (card instanceof ModalDoubleFacedCard) {
- if (((ModalDoubleFacedCard) card).getLeftHalfCard().getName().equals(abilityName)) {
- spellAbilityCopy = ((ModalDoubleFacedCard) card).getLeftHalfCard().getSpellAbility().copy();
- } else if (((ModalDoubleFacedCard) card).getRightHalfCard().getName().equals(abilityName)) {
- spellAbilityCopy = ((ModalDoubleFacedCard) card).getRightHalfCard().getSpellAbility().copy();
- }
- } else {
- spellAbilityCopy = card.getSpellAbility().copy();
- }
- if (spellAbilityCopy == null) {
- return null;
- }
- spellAbilityCopy.setId(this.getId());
- spellAbilityCopy.clearManaCosts();
- spellAbilityCopy.clearManaCostsToPay();
- spellAbilityCopy.addCost(this.getCosts().copy());
- spellAbilityCopy.addCost(this.getManaCosts().copy());
- spellAbilityCopy.setSpellAbilityCastMode(this.getSpellAbilityCastMode());
- spellAbilityToResolve = spellAbilityCopy;
- ContinuousEffect effect = new FlashbackReplacementEffect();
- effect.setTargetPointer(new FixedTarget(getSourceId(), game.getState().getZoneChangeCounter(getSourceId())));
- game.addEffect(effect, this);
- }
- }
- return spellAbilityToResolve;
- }
-
- @Override
- public Costs getCosts() {
- if (spellAbilityToResolve == null) {
- return super.getCosts();
- }
- return spellAbilityToResolve.getCosts();
}
@Override
@@ -140,11 +33,6 @@ public class FlashbackAbility extends SpellAbility {
return new FlashbackAbility(this);
}
- @Override
- public String getRule(boolean all) {
- return this.getRule();
- }
-
@Override
public String getRule() {
StringBuilder sbRule = new StringBuilder("Flashback");
@@ -170,66 +58,4 @@ public class FlashbackAbility extends SpellAbility {
sbRule.append(" (You may cast this card from your graveyard for its flashback cost. Then exile it.)");
return sbRule.toString();
}
-
- /**
- * Used for split card in PlayerImpl method:
- * getOtherUseableActivatedAbilities
- *
- * @param abilityName
- */
- public FlashbackAbility setAbilityName(String abilityName) {
- this.abilityName = abilityName;
- return this;
- }
-
-}
-
-class FlashbackReplacementEffect extends ReplacementEffectImpl {
-
- public FlashbackReplacementEffect() {
- super(Duration.OneUse, Outcome.Exile);
- staticText = "(If the flashback cost was paid, exile this card instead of putting it anywhere else any time it would leave the stack)";
- }
-
- protected FlashbackReplacementEffect(final FlashbackReplacementEffect effect) {
- super(effect);
- }
-
- @Override
- public FlashbackReplacementEffect copy() {
- return new FlashbackReplacementEffect(this);
- }
-
- @Override
- public boolean replaceEvent(GameEvent event, Ability source, Game game) {
- Player controller = game.getPlayer(source.getControllerId());
- if (controller != null) {
- Card card = game.getCard(event.getTargetId());
- if (card != null) {
- discard();
- return controller.moveCards(
- card, Zone.EXILED, source, game, false, false, false, event.getAppliedEffects());
- }
- }
- return false;
- }
-
- @Override
- public boolean checksEventType(GameEvent event, Game game) {
- return event.getType() == GameEvent.EventType.ZONE_CHANGE;
- }
-
- @Override
- public boolean applies(GameEvent event, Ability source, Game game) {
- UUID cardId = CardUtil.getMainCardId(game, source.getSourceId()); // for split cards
- if (cardId.equals(event.getTargetId())
- && ((ZoneChangeEvent) event).getFromZone() == Zone.STACK
- && ((ZoneChangeEvent) event).getToZone() != Zone.EXILED) {
-
- int zcc = game.getState().getZoneChangeCounter(cardId);
- return ((FixedTarget) getTargetPointer()).getZoneChangeCounter() + 1 == zcc;
-
- }
- return false;
- }
}
diff --git a/Mage/src/main/java/mage/abilities/keyword/ForetellAbility.java b/Mage/src/main/java/mage/abilities/keyword/ForetellAbility.java
index 9287ae99694..d094a49f537 100644
--- a/Mage/src/main/java/mage/abilities/keyword/ForetellAbility.java
+++ b/Mage/src/main/java/mage/abilities/keyword/ForetellAbility.java
@@ -257,7 +257,7 @@ public class ForetellAbility extends SpecialAction {
game.getState().addOtherAbility(rightHalfCard, ability);
}
}
- } else if (card instanceof AdventureCard) {
+ } else if (card instanceof CardWithSpellOption) {
if (foretellCost != null) {
Card creatureCard = card.getMainCard();
ForetellCostAbility ability = new ForetellCostAbility(foretellCost);
@@ -268,7 +268,7 @@ public class ForetellAbility extends SpecialAction {
game.getState().addOtherAbility(creatureCard, ability);
}
if (foretellSplitCost != null) {
- Card spellCard = ((AdventureCard) card).getSpellCard();
+ Card spellCard = ((CardWithSpellOption) card).getSpellCard();
ForetellCostAbility ability = new ForetellCostAbility(foretellSplitCost);
ability.setSourceId(spellCard.getId());
ability.setControllerId(source.getControllerId());
@@ -360,11 +360,11 @@ public class ForetellAbility extends SpecialAction {
} else if (((ModalDoubleFacedCard) card).getRightHalfCard().getName().equals(abilityName)) {
return ((ModalDoubleFacedCard) card).getRightHalfCard().getSpellAbility().canActivate(playerId, game);
}
- } else if (card instanceof AdventureCard) {
+ } else if (card instanceof CardWithSpellOption) {
if (card.getMainCard().getName().equals(abilityName)) {
return card.getMainCard().getSpellAbility().canActivate(playerId, game);
- } else if (((AdventureCard) card).getSpellCard().getName().equals(abilityName)) {
- return ((AdventureCard) card).getSpellCard().getSpellAbility().canActivate(playerId, game);
+ } else if (((CardWithSpellOption) card).getSpellCard().getName().equals(abilityName)) {
+ return ((CardWithSpellOption) card).getSpellCard().getSpellAbility().canActivate(playerId, game);
}
}
return card.getSpellAbility().canActivate(playerId, game);
@@ -391,11 +391,11 @@ public class ForetellAbility extends SpecialAction {
} else if (((ModalDoubleFacedCard) card).getRightHalfCard().getName().equals(abilityName)) {
spellAbilityCopy = ((ModalDoubleFacedCard) card).getRightHalfCard().getSpellAbility().copy();
}
- } else if (card instanceof AdventureCard) {
+ } else if (card instanceof CardWithSpellOption) {
if (card.getMainCard().getName().equals(abilityName)) {
spellAbilityCopy = card.getMainCard().getSpellAbility().copy();
- } else if (((AdventureCard) card).getSpellCard().getName().equals(abilityName)) {
- spellAbilityCopy = ((AdventureCard) card).getSpellCard().getSpellAbility().copy();
+ } else if (((CardWithSpellOption) card).getSpellCard().getName().equals(abilityName)) {
+ spellAbilityCopy = ((CardWithSpellOption) card).getSpellCard().getSpellAbility().copy();
}
} else {
spellAbilityCopy = card.getSpellAbility().copy();
diff --git a/Mage/src/main/java/mage/abilities/keyword/HarmonizeAbility.java b/Mage/src/main/java/mage/abilities/keyword/HarmonizeAbility.java
index 92f6310401c..d18de35de8c 100644
--- a/Mage/src/main/java/mage/abilities/keyword/HarmonizeAbility.java
+++ b/Mage/src/main/java/mage/abilities/keyword/HarmonizeAbility.java
@@ -1,19 +1,43 @@
package mage.abilities.keyword;
+import mage.MageInt;
+import mage.MageObject;
+import mage.abilities.Ability;
import mage.abilities.SpellAbility;
+import mage.abilities.common.SimpleStaticAbility;
+import mage.abilities.costs.Cost;
+import mage.abilities.costs.VariableCostImpl;
+import mage.abilities.costs.VariableCostType;
+import mage.abilities.costs.common.TapTargetCost;
import mage.abilities.costs.mana.ManaCostsImpl;
+import mage.abilities.effects.common.cost.CostModificationEffectImpl;
import mage.cards.Card;
-import mage.constants.Zone;
+import mage.constants.*;
+import mage.filter.StaticFilters;
+import mage.filter.common.FilterControlledPermanent;
+import mage.filter.predicate.permanent.PermanentIdPredicate;
+import mage.game.Game;
+import mage.game.permanent.Permanent;
+import mage.players.Player;
+import mage.target.TargetPermanent;
+import mage.target.common.TargetControlledPermanent;
+import mage.util.CardUtil;
+
+import java.util.Objects;
+import java.util.UUID;
/**
- * TODO: Implement this
- *
* @author TheElk801
*/
-public class HarmonizeAbility extends SpellAbility {
+public class HarmonizeAbility extends CastFromGraveyardAbility {
+
+ private String abilityName;
+ private SpellAbility spellAbilityToResolve;
public HarmonizeAbility(Card card, String manaString) {
- super(new ManaCostsImpl<>(manaString), card.getName(), Zone.GRAVEYARD);
+ super(card, new ManaCostsImpl<>(manaString), SpellAbilityCastMode.HARMONIZE);
+ this.addCost(new HarmonizeCost());
+ this.addSubAbility(new SimpleStaticAbility(Zone.ALL, new HarmonizeCostReductionEffect()).setRuleVisible(false));
}
private HarmonizeAbility(final HarmonizeAbility ability) {
@@ -24,4 +48,132 @@ public class HarmonizeAbility extends SpellAbility {
public HarmonizeAbility copy() {
return new HarmonizeAbility(this);
}
+
+ @Override
+ public String getRule() {
+ return name + " (You may cast this card from your graveyard for its harmonize cost. " +
+ "You may tap a creature you control to reduce that cost by {X}, " +
+ "where X is its power. Then exile this spell.)";
+ }
+}
+
+class HarmonizeCostReductionEffect extends CostModificationEffectImpl {
+
+ HarmonizeCostReductionEffect() {
+ super(Duration.WhileOnStack, Outcome.Benefit, CostModificationType.REDUCE_COST);
+ }
+
+ private HarmonizeCostReductionEffect(final HarmonizeCostReductionEffect effect) {
+ super(effect);
+ }
+
+ @Override
+ public boolean apply(Game game, Ability source, Ability abilityToModify) {
+ SpellAbility spellAbility = (SpellAbility) abilityToModify;
+ int power;
+ if (game.inCheckPlayableState()) {
+ power = game
+ .getBattlefield()
+ .getActivePermanents(
+ StaticFilters.FILTER_CONTROLLED_UNTAPPED_CREATURE,
+ source.getControllerId(), source, game
+ ).stream()
+ .map(MageObject::getPower)
+ .mapToInt(MageInt::getValue)
+ .max()
+ .orElse(0);
+ } else {
+ power = CardUtil
+ .castStream(spellAbility.getCosts().stream(), HarmonizeCost.class)
+ .map(HarmonizeCost::getChosenCreature)
+ .map(game::getPermanent)
+ .filter(Objects::nonNull)
+ .map(MageObject::getPower)
+ .mapToInt(MageInt::getValue)
+ .map(x -> Math.max(x, 0))
+ .sum();
+ }
+ if (power > 0) {
+ CardUtil.adjustCost(spellAbility, power);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean applies(Ability abilityToModify, Ability source, Game game) {
+ return abilityToModify instanceof SpellAbility
+ && abilityToModify.getSourceId().equals(source.getSourceId());
+ }
+
+ @Override
+ public HarmonizeCostReductionEffect copy() {
+ return new HarmonizeCostReductionEffect(this);
+ }
+}
+
+class HarmonizeCost extends VariableCostImpl {
+
+ private UUID chosenCreature = null;
+
+ HarmonizeCost() {
+ super(VariableCostType.ADDITIONAL, "", "");
+ }
+
+ private HarmonizeCost(final HarmonizeCost cost) {
+ super(cost);
+ this.chosenCreature = cost.chosenCreature;
+ }
+
+ @Override
+ public HarmonizeCost copy() {
+ return new HarmonizeCost(this);
+ }
+
+ @Override
+ public void clearPaid() {
+ super.clearPaid();
+ chosenCreature = null;
+ }
+
+ @Override
+ public int getMaxValue(Ability source, Game game) {
+ return game.getBattlefield().contains(StaticFilters.FILTER_CONTROLLED_UNTAPPED_CREATURE, source, game, 1) ? 1 : 0;
+ }
+
+ @Override
+ public int announceXValue(Ability source, Game game) {
+ Player player = game.getPlayer(source.getControllerId());
+ if (player == null || !game.getBattlefield().contains(
+ StaticFilters.FILTER_CONTROLLED_UNTAPPED_CREATURE, source, game, 1
+ ) || !player.chooseUse(
+ Outcome.Benefit, "Tap an untapped creature you control for harmonize?", source, game
+ )) {
+ return 0;
+ }
+ TargetPermanent target = new TargetPermanent(StaticFilters.FILTER_CONTROLLED_UNTAPPED_CREATURE);
+ target.withNotTarget(true);
+ target.withChooseHint("for harmonize");
+ player.choose(Outcome.PlayForFree, target, source, game);
+ Permanent permanent = game.getPermanent(target.getFirstTarget());
+ if (permanent == null) {
+ return 0;
+ }
+ chosenCreature = permanent.getId();
+ return 1;
+ }
+
+ private FilterControlledPermanent makeFilter() {
+ FilterControlledPermanent filter = new FilterControlledPermanent("tap the chosen creature");
+ filter.add(new PermanentIdPredicate(chosenCreature));
+ return filter;
+ }
+
+ @Override
+ public Cost getFixedCostsFromAnnouncedValue(int xValue) {
+ return new TapTargetCost(new TargetControlledPermanent(xValue, xValue, makeFilter(), true));
+ }
+
+ public UUID getChosenCreature() {
+ return chosenCreature;
+ }
}
diff --git a/Mage/src/main/java/mage/abilities/keyword/PlotAbility.java b/Mage/src/main/java/mage/abilities/keyword/PlotAbility.java
index c91a6f2cbbd..93375e704b3 100644
--- a/Mage/src/main/java/mage/abilities/keyword/PlotAbility.java
+++ b/Mage/src/main/java/mage/abilities/keyword/PlotAbility.java
@@ -268,11 +268,11 @@ class PlotSpellAbility extends SpellAbility {
} else if (((CardWithHalves) mainCard).getRightHalfCard().getName().equals(faceCardName)) {
return ((CardWithHalves) mainCard).getRightHalfCard().getSpellAbility().canActivate(playerId, game);
}
- } else if (card instanceof AdventureCard) {
+ } else if (card instanceof CardWithSpellOption) {
if (card.getMainCard().getName().equals(faceCardName)) {
return card.getMainCard().getSpellAbility().canActivate(playerId, game);
- } else if (((AdventureCard) card).getSpellCard().getName().equals(faceCardName)) {
- return ((AdventureCard) card).getSpellCard().getSpellAbility().canActivate(playerId, game);
+ } else if (((CardWithSpellOption) card).getSpellCard().getName().equals(faceCardName)) {
+ return ((CardWithSpellOption) card).getSpellCard().getSpellAbility().canActivate(playerId, game);
}
}
return card.getSpellAbility().canActivate(playerId, game);
@@ -294,11 +294,11 @@ class PlotSpellAbility extends SpellAbility {
} else if (((CardWithHalves) card).getRightHalfCard().getName().equals(faceCardName)) {
spellAbilityCopy = ((CardWithHalves) card).getRightHalfCard().getSpellAbility().copy();
}
- } else if (card instanceof AdventureCard) {
+ } else if (card instanceof CardWithSpellOption) {
if (card.getMainCard().getName().equals(faceCardName)) {
spellAbilityCopy = card.getMainCard().getSpellAbility().copy();
- } else if (((AdventureCard) card).getSpellCard().getName().equals(faceCardName)) {
- spellAbilityCopy = ((AdventureCard) card).getSpellCard().getSpellAbility().copy();
+ } else if (((CardWithSpellOption) card).getSpellCard().getName().equals(faceCardName)) {
+ spellAbilityCopy = ((CardWithSpellOption) card).getSpellCard().getSpellAbility().copy();
}
} else {
spellAbilityCopy = card.getSpellAbility().copy();
diff --git a/Mage/src/main/java/mage/abilities/keyword/SuspendAbility.java b/Mage/src/main/java/mage/abilities/keyword/SuspendAbility.java
index 94972323c83..1114ecebc64 100644
--- a/Mage/src/main/java/mage/abilities/keyword/SuspendAbility.java
+++ b/Mage/src/main/java/mage/abilities/keyword/SuspendAbility.java
@@ -1,6 +1,5 @@
package mage.abilities.keyword;
-import mage.ApprovingObject;
import mage.MageIdentifier;
import mage.abilities.Ability;
import mage.abilities.SpecialAction;
@@ -17,8 +16,11 @@ import mage.abilities.effects.ContinuousEffectImpl;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.counter.RemoveCounterSourceEffect;
import mage.cards.Card;
+import mage.cards.CardsImpl;
+import mage.cards.ModalDoubleFacedCard;
import mage.constants.*;
import mage.counters.CounterType;
+import mage.filter.StaticFilters;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
@@ -26,8 +28,6 @@ import mage.players.Player;
import mage.target.targetpointer.FixedTarget;
import mage.util.CardUtil;
-import java.util.ArrayList;
-import java.util.List;
import java.util.Set;
import java.util.UUID;
@@ -112,7 +112,6 @@ import java.util.UUID;
public class SuspendAbility extends SpecialAction {
private final String ruleText;
- private boolean gainedTemporary;
/**
* Gives the card the SuspendAbility
@@ -132,6 +131,8 @@ public class SuspendAbility extends SpecialAction {
this.addEffect(new SuspendExileEffect(suspend));
this.usesStack = false;
if (suspend == Integer.MAX_VALUE) {
+ // example: Suspend X-{X}{W}{W}. X can't be 0.
+ // TODO: replace by costAdjuster for shared logic
VariableManaCost xCosts = new VariableManaCost(VariableCostType.ALTERNATIVE);
xCosts.setMinX(1);
this.addCost(xCosts);
@@ -175,6 +176,11 @@ public class SuspendAbility extends SpecialAction {
* or added by Jhoira of the Ghitu
*/
public static void addSuspendTemporaryToCard(Card card, Ability source, Game game) {
+ if (card instanceof ModalDoubleFacedCard) {
+ // Need to ensure the suspend ability gets put on the left side card
+ // since counters get added to this card.
+ card = ((ModalDoubleFacedCard) card).getLeftHalfCard();
+ }
SuspendAbility ability = new SuspendAbility(0, null, card, false);
ability.setSourceId(card.getId());
ability.setControllerId(card.getOwnerId());
@@ -204,7 +210,6 @@ public class SuspendAbility extends SpecialAction {
private SuspendAbility(final SuspendAbility ability) {
super(ability);
this.ruleText = ability.ruleText;
- this.gainedTemporary = ability.gainedTemporary;
}
@Override
@@ -230,10 +235,6 @@ public class SuspendAbility extends SpecialAction {
return ruleText;
}
- public boolean isGainedTemporary() {
- return gainedTemporary;
- }
-
@Override
public SuspendAbility copy() {
return new SuspendAbility(this);
@@ -343,40 +344,16 @@ class SuspendPlayCardEffect extends OneShotEffect {
if (player == null || card == null) {
return false;
}
- if (!player.chooseUse(Outcome.Benefit, "Play " + card.getLogName() + " without paying its mana cost?", source, game)) {
+ // ensure we're getting the main card when passing to CardUtil to check all parts of card
+ // MDFC points to left half card
+ card = card.getMainCard();
+ // cast/play the card for free
+ if (!CardUtil.castSpellWithAttributesForFree(player, source, game, new CardsImpl(card),
+ StaticFilters.FILTER_CARD, null, true)) {
return true;
}
- // remove temporary suspend ability (used e.g. for Epochrasite)
- // TODO: isGainedTemporary is not set or use in other places, so it can be deleted?!
- List abilitiesToRemove = new ArrayList<>();
- for (Ability ability : card.getAbilities(game)) {
- if (ability instanceof SuspendAbility && (((SuspendAbility) ability).isGainedTemporary())) {
- abilitiesToRemove.add(ability);
- }
- }
- if (!abilitiesToRemove.isEmpty()) {
- for (Ability ability : card.getAbilities(game)) {
- if (ability instanceof SuspendBeginningOfUpkeepInterveningIfTriggeredAbility
- || ability instanceof SuspendPlayCardAbility) {
- abilitiesToRemove.add(ability);
- }
- }
- // remove the abilities from the card
- // TODO: will not work with Adventure Cards and another auto-generated abilities list
- // TODO: is it work after blink or return to hand?
- /*
- bug example:
- Epochrasite bug: It comes out of suspend, is cast and enters the battlefield. THEN if it's returned to
- its owner's hand from battlefield, the bounced Epochrasite can't be cast for the rest of the game.
- */
- card.getAbilities().removeAll(abilitiesToRemove);
- }
- // cast the card for free
- game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), Boolean.TRUE);
- boolean cardWasCast = player.cast(player.chooseAbilityForCast(card, game, true),
- game, true, new ApprovingObject(source, game));
- game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), null);
- if (cardWasCast && (card.isCreature(game))) {
+ // creatures cast from suspend gain haste
+ if ((card.isCreature(game))) {
ContinuousEffect effect = new GainHasteEffect();
effect.setTargetPointer(new FixedTarget(card.getId(), card.getZoneChangeCounter(game) + 1));
game.addEffect(effect, source);
diff --git a/Mage/src/main/java/mage/abilities/keyword/WardAbility.java b/Mage/src/main/java/mage/abilities/keyword/WardAbility.java
index b6bebf87769..c68ff475567 100644
--- a/Mage/src/main/java/mage/abilities/keyword/WardAbility.java
+++ b/Mage/src/main/java/mage/abilities/keyword/WardAbility.java
@@ -77,7 +77,7 @@ public class WardAbility extends TriggeredAbilityImpl {
if (!getSourceId().equals(event.getTargetId())) {
return false;
}
- StackObject targetingObject = CardUtil.getTargetingStackObject(event, game);
+ StackObject targetingObject = CardUtil.getTargetingStackObject(this.getId().toString(), event, game);
if (targetingObject == null || !game.getOpponents(getControllerId()).contains(targetingObject.getControllerId())) {
return false;
}
diff --git a/Mage/src/main/java/mage/cards/AdventureCard.java b/Mage/src/main/java/mage/cards/AdventureCard.java
index 413d9d6e026..af6e67ca7c1 100644
--- a/Mage/src/main/java/mage/cards/AdventureCard.java
+++ b/Mage/src/main/java/mage/cards/AdventureCard.java
@@ -1,98 +1,29 @@
package mage.cards;
-import mage.abilities.Abilities;
-import mage.abilities.AbilitiesImpl;
-import mage.abilities.Ability;
import mage.abilities.SpellAbility;
import mage.constants.CardType;
import mage.constants.SpellAbilityType;
import mage.constants.Zone;
import mage.game.Game;
-import mage.game.events.ZoneChangeEvent;
-import mage.util.CardUtil;
-import java.util.List;
import java.util.UUID;
/**
* @author phulin
*/
-public abstract class AdventureCard extends CardImpl {
-
- /* The adventure spell card, i.e. Swift End. */
- protected AdventureCardSpell spellCard;
+public abstract class AdventureCard extends CardWithSpellOption {
public AdventureCard(UUID ownerId, CardSetInfo setInfo, CardType[] types, CardType[] typesSpell, String costs, String adventureName, String costsSpell) {
super(ownerId, setInfo, types, costs);
- this.spellCard = new AdventureCardSpellImpl(ownerId, setInfo, adventureName, typesSpell, costsSpell, this);
+ this.spellCard = new AdventureSpellCard(ownerId, setInfo, adventureName, typesSpell, costsSpell, this);
+ }
+
+ public AdventureCard(AdventureCard card) {
+ super(card);
}
public void finalizeAdventure() {
- spellCard.finalizeAdventure();
- }
-
- protected AdventureCard(final AdventureCard card) {
- super(card);
- this.spellCard = card.getSpellCard().copy();
- this.spellCard.setParentCard(this);
- }
-
- public AdventureCardSpell getSpellCard() {
- return spellCard;
- }
-
- public void setParts(AdventureCardSpell cardSpell) {
- // for card copy only - set new parts
- this.spellCard = cardSpell;
- cardSpell.setParentCard(this);
- }
-
- @Override
- public void assignNewId() {
- super.assignNewId();
- spellCard.assignNewId();
- }
-
- @Override
- public boolean moveToZone(Zone toZone, Ability source, Game game, boolean flag, List appliedEffects) {
- if (super.moveToZone(toZone, source, game, flag, appliedEffects)) {
- Zone currentZone = game.getState().getZone(getId());
- game.getState().setZone(getSpellCard().getId(), currentZone);
- return true;
- }
- return false;
- }
-
- @Override
- public void setZone(Zone zone, Game game) {
- super.setZone(zone, game);
- game.setZone(getSpellCard().getId(), zone);
- }
-
- @Override
- public boolean moveToExile(UUID exileId, String name, Ability source, Game game, List appliedEffects) {
- if (super.moveToExile(exileId, name, source, game, appliedEffects)) {
- Zone currentZone = game.getState().getZone(getId());
- game.getState().setZone(getSpellCard().getId(), currentZone);
- return true;
- }
- return false;
- }
-
- @Override
- public boolean removeFromZone(Game game, Zone fromZone, Ability source) {
- // zone contains only one main card
- return super.removeFromZone(game, fromZone, source);
- }
-
- @Override
- public void updateZoneChangeCounter(Game game, ZoneChangeEvent event) {
- if (isCopy()) { // same as meld cards
- super.updateZoneChangeCounter(game, event);
- return;
- }
- super.updateZoneChangeCounter(game, event);
- getSpellCard().updateZoneChangeCounter(game, event);
+ spellCard.finalizeSpell();
}
@Override
@@ -103,45 +34,4 @@ public abstract class AdventureCard extends CardImpl {
this.getSpellCard().getSpellAbility().setControllerId(controllerId);
return super.cast(game, fromZone, ability, controllerId);
}
-
- @Override
- public Abilities getAbilities() {
- Abilities allAbilities = new AbilitiesImpl<>();
- allAbilities.addAll(spellCard.getAbilities());
- allAbilities.addAll(super.getAbilities());
- return allAbilities;
- }
-
- @Override
- public Abilities getInitAbilities() {
- // must init only parent related abilities, spell card must be init separately
- return super.getAbilities();
- }
-
- @Override
- public Abilities getAbilities(Game game) {
- Abilities allAbilities = new AbilitiesImpl<>();
- allAbilities.addAll(spellCard.getAbilities(game));
- allAbilities.addAll(super.getAbilities(game));
- return allAbilities;
- }
-
- public Abilities getSharedAbilities(Game game) {
- // abilities without spellcard
- return super.getAbilities(game);
- }
-
- public List getSharedRules(Game game) {
- // rules without spellcard
- Abilities sourceAbilities = this.getSharedAbilities(game);
- return CardUtil.getCardRulesWithAdditionalInfo(game, this, sourceAbilities, sourceAbilities);
- }
-
- @Override
- public void setOwnerId(UUID ownerId) {
- super.setOwnerId(ownerId);
- abilities.setControllerId(ownerId);
- spellCard.getAbilities().setControllerId(ownerId);
- spellCard.setOwnerId(ownerId);
- }
}
diff --git a/Mage/src/main/java/mage/cards/AdventureCardSpell.java b/Mage/src/main/java/mage/cards/AdventureCardSpell.java
deleted file mode 100644
index 27f14c5493a..00000000000
--- a/Mage/src/main/java/mage/cards/AdventureCardSpell.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package mage.cards;
-
-/**
- * @author phulin
- */
-public interface AdventureCardSpell extends SubCard {
-
- @Override
- AdventureCardSpell copy();
-
- void finalizeAdventure();
-}
diff --git a/Mage/src/main/java/mage/cards/AdventureCardSpellImpl.java b/Mage/src/main/java/mage/cards/AdventureSpellCard.java
similarity index 88%
rename from Mage/src/main/java/mage/cards/AdventureCardSpellImpl.java
rename to Mage/src/main/java/mage/cards/AdventureSpellCard.java
index 2062b27ca9e..ed6efa5f1f1 100644
--- a/Mage/src/main/java/mage/cards/AdventureCardSpellImpl.java
+++ b/Mage/src/main/java/mage/cards/AdventureSpellCard.java
@@ -19,11 +19,11 @@ import java.util.stream.Collectors;
/**
* @author phulin
*/
-public class AdventureCardSpellImpl extends CardImpl implements AdventureCardSpell {
+public class AdventureSpellCard extends CardImpl implements SpellOptionCard {
private AdventureCard adventureCardParent;
- public AdventureCardSpellImpl(UUID ownerId, CardSetInfo setInfo, String adventureName, CardType[] cardTypes, String costs, AdventureCard adventureCardParent) {
+ public AdventureSpellCard(UUID ownerId, CardSetInfo setInfo, String adventureName, CardType[] cardTypes, String costs, AdventureCard adventureCardParent) {
super(ownerId, setInfo, cardTypes, costs, SpellAbilityType.ADVENTURE_SPELL);
this.subtype.add(SubType.ADVENTURE);
@@ -35,13 +35,13 @@ public class AdventureCardSpellImpl extends CardImpl implements AdventureCardSpe
this.adventureCardParent = adventureCardParent;
}
- public void finalizeAdventure() {
+ public void finalizeSpell() {
if (spellAbility instanceof AdventureCardSpellAbility) {
((AdventureCardSpellAbility) spellAbility).finalizeAdventure();
}
}
- protected AdventureCardSpellImpl(final AdventureCardSpellImpl card) {
+ protected AdventureSpellCard(final AdventureSpellCard card) {
super(card);
this.adventureCardParent = card.adventureCardParent;
}
@@ -83,13 +83,13 @@ public class AdventureCardSpellImpl extends CardImpl implements AdventureCardSpe
}
@Override
- public AdventureCardSpellImpl copy() {
- return new AdventureCardSpellImpl(this);
+ public AdventureSpellCard copy() {
+ return new AdventureSpellCard(this);
}
@Override
- public void setParentCard(AdventureCard card) {
- this.adventureCardParent = card;
+ public void setParentCard(CardWithSpellOption card) {
+ this.adventureCardParent = (AdventureCard) card;
}
@Override
@@ -102,6 +102,11 @@ public class AdventureCardSpellImpl extends CardImpl implements AdventureCardSpe
// id must send to main card (popup card hint in game logs)
return getName() + " [" + adventureCardParent.getId().toString().substring(0, 3) + ']';
}
+
+ @Override
+ public String getSpellType() {
+ return "Adventure";
+ }
}
class AdventureCardSpellAbility extends SpellAbility {
@@ -141,8 +146,8 @@ class AdventureCardSpellAbility extends SpellAbility {
public ActivationStatus canActivate(UUID playerId, Game game) {
ExileZone adventureExileZone = game.getExile().getExileZone(ExileAdventureSpellEffect.adventureExileId(playerId, game));
Card spellCard = game.getCard(this.getSourceId());
- if (spellCard instanceof AdventureCardSpell) {
- Card card = ((AdventureCardSpell) spellCard).getParentCard();
+ if (spellCard instanceof AdventureSpellCard) {
+ Card card = ((AdventureSpellCard) spellCard).getParentCard();
if (adventureExileZone != null && adventureExileZone.contains(card.getId())) {
return ActivationStatus.getFalse();
}
diff --git a/Mage/src/main/java/mage/cards/CardImpl.java b/Mage/src/main/java/mage/cards/CardImpl.java
index e6c7aa4b9ae..2515d21c1fd 100644
--- a/Mage/src/main/java/mage/cards/CardImpl.java
+++ b/Mage/src/main/java/mage/cards/CardImpl.java
@@ -530,8 +530,8 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
}
}
- if (stackObject == null && (this instanceof AdventureCard)) {
- stackObject = game.getStack().getSpell(((AdventureCard) this).getSpellCard().getId(), false);
+ if (stackObject == null && (this instanceof CardWithSpellOption)) {
+ stackObject = game.getStack().getSpell(((CardWithSpellOption) this).getSpellCard().getId(), false);
}
if (stackObject == null) {
diff --git a/Mage/src/main/java/mage/cards/CardWithSpellOption.java b/Mage/src/main/java/mage/cards/CardWithSpellOption.java
new file mode 100644
index 00000000000..b1470f7dedc
--- /dev/null
+++ b/Mage/src/main/java/mage/cards/CardWithSpellOption.java
@@ -0,0 +1,131 @@
+package mage.cards;
+
+import mage.abilities.Abilities;
+import mage.abilities.AbilitiesImpl;
+import mage.abilities.Ability;
+import mage.constants.CardType;
+import mage.constants.Zone;
+import mage.game.Game;
+import mage.game.events.ZoneChangeEvent;
+import mage.util.CardUtil;
+
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * @author phulin, jmlundeen
+ */
+public abstract class CardWithSpellOption extends CardImpl {
+
+ /* The adventure/omen spell card, i.e. Swift End. */
+ protected SpellOptionCard spellCard;
+
+ public CardWithSpellOption(UUID ownerId, CardSetInfo setInfo, CardType[] types, String costs) {
+ super(ownerId, setInfo, types, costs);
+ }
+
+ public CardWithSpellOption(CardWithSpellOption card) {
+ super(card);
+ this.spellCard = card.getSpellCard().copy();
+ this.spellCard.setParentCard(this);
+ }
+
+ public SpellOptionCard getSpellCard() {
+ return spellCard;
+ }
+
+ public void setParts(SpellOptionCard cardSpell) {
+ // for card copy only - set new parts
+ this.spellCard = cardSpell;
+ cardSpell.setParentCard(this);
+ }
+
+ @Override
+ public void assignNewId() {
+ super.assignNewId();
+ spellCard.assignNewId();
+ }
+
+ @Override
+ public boolean moveToZone(Zone toZone, Ability source, Game game, boolean flag, List appliedEffects) {
+ if (super.moveToZone(toZone, source, game, flag, appliedEffects)) {
+ Zone currentZone = game.getState().getZone(getId());
+ game.getState().setZone(getSpellCard().getId(), currentZone);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void setZone(Zone zone, Game game) {
+ super.setZone(zone, game);
+ game.setZone(getSpellCard().getId(), zone);
+ }
+
+ @Override
+ public boolean moveToExile(UUID exileId, String name, Ability source, Game game, List appliedEffects) {
+ if (super.moveToExile(exileId, name, source, game, appliedEffects)) {
+ Zone currentZone = game.getState().getZone(getId());
+ game.getState().setZone(getSpellCard().getId(), currentZone);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean removeFromZone(Game game, Zone fromZone, Ability source) {
+ // zone contains only one main card
+ return super.removeFromZone(game, fromZone, source);
+ }
+
+ @Override
+ public void updateZoneChangeCounter(Game game, ZoneChangeEvent event) {
+ if (isCopy()) { // same as meld cards
+ super.updateZoneChangeCounter(game, event);
+ return;
+ }
+ super.updateZoneChangeCounter(game, event);
+ getSpellCard().updateZoneChangeCounter(game, event);
+ }
+
+ @Override
+ public Abilities getAbilities() {
+ Abilities allAbilities = new AbilitiesImpl<>();
+ allAbilities.addAll(spellCard.getAbilities());
+ allAbilities.addAll(super.getAbilities());
+ return allAbilities;
+ }
+
+ @Override
+ public Abilities getInitAbilities() {
+ // must init only parent related abilities, spell card must be init separately
+ return super.getAbilities();
+ }
+
+ @Override
+ public Abilities getAbilities(Game game) {
+ Abilities allAbilities = new AbilitiesImpl<>();
+ allAbilities.addAll(spellCard.getAbilities(game));
+ allAbilities.addAll(super.getAbilities(game));
+ return allAbilities;
+ }
+
+ public Abilities getSharedAbilities(Game game) {
+ // abilities without spellCard
+ return super.getAbilities(game);
+ }
+
+ public List getSharedRules(Game game) {
+ // rules without spellCard
+ Abilities sourceAbilities = this.getSharedAbilities(game);
+ return CardUtil.getCardRulesWithAdditionalInfo(game, this, sourceAbilities, sourceAbilities);
+ }
+
+ @Override
+ public void setOwnerId(UUID ownerId) {
+ super.setOwnerId(ownerId);
+ abilities.setControllerId(ownerId);
+ spellCard.getAbilities().setControllerId(ownerId);
+ spellCard.setOwnerId(ownerId);
+ }
+}
diff --git a/Mage/src/main/java/mage/cards/OmenCard.java b/Mage/src/main/java/mage/cards/OmenCard.java
new file mode 100644
index 00000000000..82401304f12
--- /dev/null
+++ b/Mage/src/main/java/mage/cards/OmenCard.java
@@ -0,0 +1,34 @@
+package mage.cards;
+
+import mage.abilities.SpellAbility;
+import mage.constants.CardType;
+import mage.constants.SpellAbilityType;
+import mage.constants.Zone;
+import mage.game.Game;
+
+import java.util.UUID;
+
+public abstract class OmenCard extends CardWithSpellOption {
+
+ public OmenCard(UUID ownerId, CardSetInfo setInfo, CardType[] types, CardType[] typesSpell, String costs, String omenName, String costsSpell) {
+ super(ownerId, setInfo, types, costs);
+ this.spellCard = new OmenSpellCard(ownerId, setInfo, omenName, typesSpell, costsSpell, this);
+ }
+
+ public OmenCard(OmenCard card) {
+ super(card);
+ }
+
+ public void finalizeOmen() {
+ spellCard.finalizeSpell();
+ }
+
+ @Override
+ public boolean cast(Game game, Zone fromZone, SpellAbility ability, UUID controllerId) {
+ if (ability.getSpellAbilityType() == SpellAbilityType.OMEN_SPELL) {
+ return this.getSpellCard().cast(game, fromZone, ability, controllerId);
+ }
+ this.getSpellCard().getSpellAbility().setControllerId(controllerId);
+ return super.cast(game, fromZone, ability, controllerId);
+ }
+}
diff --git a/Mage/src/main/java/mage/cards/OmenSpellCard.java b/Mage/src/main/java/mage/cards/OmenSpellCard.java
new file mode 100644
index 00000000000..040bba51217
--- /dev/null
+++ b/Mage/src/main/java/mage/cards/OmenSpellCard.java
@@ -0,0 +1,174 @@
+package mage.cards;
+
+import mage.abilities.Ability;
+import mage.abilities.Modes;
+import mage.abilities.SpellAbility;
+import mage.abilities.effects.Effect;
+import mage.abilities.effects.common.ShuffleIntoLibrarySourceEffect;
+import mage.constants.CardType;
+import mage.constants.SpellAbilityType;
+import mage.constants.SubType;
+import mage.constants.Zone;
+import mage.game.Game;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+public class OmenSpellCard extends CardImpl implements SpellOptionCard {
+
+ private OmenCard omenCardParent;
+
+ public OmenSpellCard(UUID ownerId, CardSetInfo setInfo, String omenName, CardType[] cardTypes, String costs, OmenCard omenCard) {
+ super(ownerId, setInfo, cardTypes, costs, SpellAbilityType.OMEN_SPELL);
+ this.subtype.add(SubType.OMEN);
+
+ OmenCardSpellAbility newSpellAbility = new OmenCardSpellAbility(getSpellAbility(), omenName, cardTypes, costs);
+ this.replaceSpellAbility(newSpellAbility);
+ spellAbility = newSpellAbility;
+
+ this.setName(omenName);
+ this.omenCardParent = omenCard;
+ }
+
+ public void finalizeSpell() {
+ if (spellAbility instanceof OmenCardSpellAbility) {
+ ((OmenCardSpellAbility) spellAbility).finalizeOmen();
+ }
+ }
+
+ protected OmenSpellCard(final OmenSpellCard card) {
+ super(card);
+ this.omenCardParent = card.omenCardParent;
+ }
+
+ @Override
+ public UUID getOwnerId() {
+ return omenCardParent.getOwnerId();
+ }
+
+ @Override
+ public String getExpansionSetCode() {
+ return omenCardParent.getExpansionSetCode();
+ }
+
+ @Override
+ public String getCardNumber() {
+ return omenCardParent.getCardNumber();
+ }
+
+ @Override
+ public boolean moveToZone(Zone toZone, Ability source, Game game, boolean flag, List appliedEffects) {
+ return omenCardParent.moveToZone(toZone, source, game, flag, appliedEffects);
+ }
+
+ @Override
+ public boolean moveToExile(UUID exileId, String name, Ability source, Game game, List appliedEffects) {
+ return omenCardParent.moveToExile(exileId, name, source, game, appliedEffects);
+ }
+
+ @Override
+ public OmenCard getMainCard() {
+ return omenCardParent;
+ }
+
+ @Override
+ public void setZone(Zone zone, Game game) {
+ game.setZone(omenCardParent.getId(), zone);
+ game.setZone(omenCardParent.getSpellCard().getId(), zone);
+ }
+
+ @Override
+ public OmenSpellCard copy() {
+ return new OmenSpellCard(this);
+ }
+
+ @Override
+ public void setParentCard(CardWithSpellOption card) {
+ this.omenCardParent = (OmenCard) card;
+ }
+
+ @Override
+ public OmenCard getParentCard() {
+ return this.omenCardParent;
+ }
+
+ @Override
+ public String getIdName() {
+ // id must send to main card (popup card hint in game logs)
+ return getName() + " [" + omenCardParent.getId().toString().substring(0, 3) + ']';
+ }
+
+ @Override
+ public String getSpellType() {
+ return "Omen";
+ }
+}
+
+class OmenCardSpellAbility extends SpellAbility {
+
+ private String nameFull;
+ private boolean finalized = false;
+
+ public OmenCardSpellAbility(final SpellAbility baseSpellAbility, String omenName, CardType[] cardTypes, String costs) {
+ super(baseSpellAbility);
+ this.setName(cardTypes, omenName, costs);
+ this.setCardName(omenName);
+ }
+
+ public void finalizeOmen() {
+ if (finalized) {
+ throw new IllegalStateException("Wrong code usage. "
+ + "Omen (" + cardName + ") "
+ + "need to call finalizeOmen() exactly once.");
+ }
+ Effect effect = new ShuffleIntoLibrarySourceEffect();
+ effect.setText("");
+ this.addEffect(effect);
+ this.finalized = true;
+ }
+
+ protected OmenCardSpellAbility(final OmenCardSpellAbility ability) {
+ super(ability);
+ this.nameFull = ability.nameFull;
+ if (!ability.finalized) {
+ throw new IllegalStateException("Wrong code usage. "
+ + "Omen (" + cardName + ") "
+ + "need to call finalizeOmen() at the very end of the card's constructor.");
+ }
+ this.finalized = true;
+ }
+
+ public void setName(CardType[] cardTypes, String omenName, String costs) {
+ this.nameFull = "Omen " + Arrays.stream(cardTypes).map(CardType::toString).collect(Collectors.joining(" ")) + " — " + omenName;
+ this.name = this.nameFull + " " + costs;
+ }
+
+ @Override
+ public String getRule(boolean all) {
+ return this.getRule();
+ }
+
+ @Override
+ public String getRule() {
+ StringBuilder sbRule = new StringBuilder();
+ sbRule.append(this.nameFull);
+ sbRule.append(" ");
+ sbRule.append(getManaCosts().getText());
+ sbRule.append(" — ");
+ Modes modes = this.getModes();
+ if (modes.size() <= 1) {
+ sbRule.append(modes.getMode().getEffects().getTextStartingUpperCase(modes.getMode()));
+ } else {
+ sbRule.append(getModes().getText());
+ }
+ sbRule.append(" (Then shuffle this card into its owner's library.)");
+ return sbRule.toString();
+ }
+
+ @Override
+ public OmenCardSpellAbility copy() {
+ return new OmenCardSpellAbility(this);
+ }
+}
diff --git a/Mage/src/main/java/mage/cards/SpellOptionCard.java b/Mage/src/main/java/mage/cards/SpellOptionCard.java
new file mode 100644
index 00000000000..0007d70fcbc
--- /dev/null
+++ b/Mage/src/main/java/mage/cards/SpellOptionCard.java
@@ -0,0 +1,18 @@
+package mage.cards;
+
+public interface SpellOptionCard extends SubCard {
+
+ @Override
+ SpellOptionCard copy();
+
+ /**
+ * Adds the final shared ability to the card. e.g. Adventure exile effect / Omen shuffle effect
+ */
+ void finalizeSpell();
+
+ /**
+ * Used to get the card type text such as Adventure. Currently only used in {@link mage.game.stack.Spell#getSpellCastText Spell} for logging the spell
+ * being cast as part of the two part card.
+ */
+ String getSpellType();
+}
diff --git a/Mage/src/main/java/mage/cards/mock/MockCard.java b/Mage/src/main/java/mage/cards/mock/MockCard.java
index ae406a39dca..6d768ef8b67 100644
--- a/Mage/src/main/java/mage/cards/mock/MockCard.java
+++ b/Mage/src/main/java/mage/cards/mock/MockCard.java
@@ -20,7 +20,7 @@ import java.util.List;
*/
public class MockCard extends CardImpl implements MockableCard {
- public static String ADVENTURE_NAME_SEPARATOR = " // ";
+ public static String CARD_WITH_SPELL_OPTION_NAME_SEPARATOR = " // ";
public static String MODAL_DOUBLE_FACES_NAME_SEPARATOR = " // ";
// Needs to be here, as it is normally calculated from the
@@ -34,7 +34,7 @@ public class MockCard extends CardImpl implements MockableCard {
protected List manaCostLeftStr;
protected List manaCostRightStr;
protected List manaCostStr;
- protected String adventureSpellName;
+ protected String spellOptionName; // adventure/omen spell name
protected boolean isModalDoubleFacedCard;
protected int manaValue;
@@ -71,8 +71,8 @@ public class MockCard extends CardImpl implements MockableCard {
this.secondSideCard = new MockCard(CardRepository.instance.findCardWithPreferredSetAndNumber(card.getSecondSideName(), card.getSetCode(), card.getCardNumber()));
}
- if (card.isAdventureCard()) {
- this.adventureSpellName = card.getAdventureSpellName();
+ if (card.isCardWithSpellOption()) {
+ this.spellOptionName = card.getSpellOptionCardName();
}
if (card.isModalDoubleFacedCard()) {
@@ -101,7 +101,7 @@ public class MockCard extends CardImpl implements MockableCard {
this.manaCostLeftStr = new ArrayList<>(card.manaCostLeftStr);
this.manaCostRightStr = new ArrayList<>(card.manaCostRightStr);
this.manaCostStr = new ArrayList<>(card.manaCostStr);
- this.adventureSpellName = card.adventureSpellName;
+ this.spellOptionName = card.spellOptionName;
this.isModalDoubleFacedCard = card.isModalDoubleFacedCard;
this.manaValue = card.manaValue;
}
@@ -155,8 +155,8 @@ public class MockCard extends CardImpl implements MockableCard {
return getName();
}
- if (adventureSpellName != null) {
- return getName() + ADVENTURE_NAME_SEPARATOR + adventureSpellName;
+ if (spellOptionName != null) {
+ return getName() + CARD_WITH_SPELL_OPTION_NAME_SEPARATOR + spellOptionName;
} else if (isModalDoubleFacedCard) {
return getName() + MODAL_DOUBLE_FACES_NAME_SEPARATOR + this.getSecondCardFace().getName();
} else {
diff --git a/Mage/src/main/java/mage/cards/repository/CardInfo.java b/Mage/src/main/java/mage/cards/repository/CardInfo.java
index 7477dc9bee3..d870673e76d 100644
--- a/Mage/src/main/java/mage/cards/repository/CardInfo.java
+++ b/Mage/src/main/java/mage/cards/repository/CardInfo.java
@@ -106,9 +106,9 @@ public class CardInfo {
@DatabaseField
protected String secondSideName;
@DatabaseField
- protected boolean adventureCard;
+ protected boolean cardWithSpellOption;
@DatabaseField
- protected String adventureSpellName;
+ protected String spellOptionCardName;
@DatabaseField
protected boolean modalDoubleFacedCard;
@DatabaseField
@@ -157,9 +157,9 @@ public class CardInfo {
this.secondSideName = secondSide.getName();
}
- if (card instanceof AdventureCard) {
- this.adventureCard = true;
- this.adventureSpellName = ((AdventureCard) card).getSpellCard().getName();
+ if (card instanceof CardWithSpellOption) {
+ this.cardWithSpellOption = true;
+ this.spellOptionCardName = ((CardWithSpellOption) card).getSpellCard().getName();
}
if (card instanceof ModalDoubleFacedCard) {
@@ -189,8 +189,8 @@ public class CardInfo {
List manaCostLeft = ((ModalDoubleFacedCard) card).getLeftHalfCard().getManaCostSymbols();
List manaCostRight = ((ModalDoubleFacedCard) card).getRightHalfCard().getManaCostSymbols();
this.setManaCosts(CardUtil.concatManaSymbols(SPLIT_MANA_SEPARATOR_FULL, manaCostLeft, manaCostRight));
- } else if (card instanceof AdventureCard) {
- List manaCostLeft = ((AdventureCard) card).getSpellCard().getManaCostSymbols();
+ } else if (card instanceof CardWithSpellOption) {
+ List manaCostLeft = ((CardWithSpellOption) card).getSpellCard().getManaCostSymbols();
List manaCostRight = card.getManaCostSymbols();
this.setManaCosts(CardUtil.concatManaSymbols(SPLIT_MANA_SEPARATOR_FULL, manaCostLeft, manaCostRight));
} else {
@@ -469,12 +469,16 @@ public class CardInfo {
return secondSideName;
}
- public boolean isAdventureCard() {
- return adventureCard;
+ public boolean isCardWithSpellOption() {
+ return cardWithSpellOption;
}
- public String getAdventureSpellName() {
- return adventureSpellName;
+ /**
+ * used for spell card portion of adventure/omen cards
+ * @return name of the spell
+ */
+ public String getSpellOptionCardName() {
+ return spellOptionCardName;
}
public boolean isModalDoubleFacedCard() {
diff --git a/Mage/src/main/java/mage/cards/repository/CardRepository.java b/Mage/src/main/java/mage/cards/repository/CardRepository.java
index 24484308724..88cb2fbadda 100644
--- a/Mage/src/main/java/mage/cards/repository/CardRepository.java
+++ b/Mage/src/main/java/mage/cards/repository/CardRepository.java
@@ -147,8 +147,8 @@ public enum CardRepository {
if (card.getMeldsToCardName() != null && !card.getMeldsToCardName().isEmpty()) {
namesList.add(card.getMeldsToCardName());
}
- if (card.getAdventureSpellName() != null && !card.getAdventureSpellName().isEmpty()) {
- namesList.add(card.getAdventureSpellName());
+ if (card.getSpellOptionCardName() != null && !card.getSpellOptionCardName().isEmpty()) {
+ namesList.add(card.getSpellOptionCardName());
}
}
@@ -160,7 +160,7 @@ public enum CardRepository {
Set names = new TreeSet<>();
try {
QueryBuilder qb = cardsDao.queryBuilder();
- qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName");
+ qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName");
List results = cardsDao.query(qb.prepare());
for (CardInfo card : results) {
addNewNames(card, names);
@@ -176,7 +176,7 @@ public enum CardRepository {
Set names = new TreeSet<>();
try {
QueryBuilder qb = cardsDao.queryBuilder();
- qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName");
+ qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName");
qb.where().not().like("types", new SelectArg('%' + CardType.LAND.name() + '%'));
List results = cardsDao.query(qb.prepare());
for (CardInfo card : results) {
@@ -193,7 +193,7 @@ public enum CardRepository {
Set names = new TreeSet<>();
try {
QueryBuilder qb = cardsDao.queryBuilder();
- qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName");
+ qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName");
Where where = qb.where();
where.and(
where.not().like("supertypes", '%' + SuperType.BASIC.name() + '%'),
@@ -214,7 +214,7 @@ public enum CardRepository {
Set names = new TreeSet<>();
try {
QueryBuilder qb = cardsDao.queryBuilder();
- qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName");
+ qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName");
qb.where().not().like("supertypes", new SelectArg('%' + SuperType.BASIC.name() + '%'));
List results = cardsDao.query(qb.prepare());
for (CardInfo card : results) {
@@ -231,7 +231,7 @@ public enum CardRepository {
Set names = new TreeSet<>();
try {
QueryBuilder qb = cardsDao.queryBuilder();
- qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName");
+ qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName");
qb.where().like("types", new SelectArg('%' + CardType.CREATURE.name() + '%'));
List results = cardsDao.query(qb.prepare());
for (CardInfo card : results) {
@@ -248,7 +248,7 @@ public enum CardRepository {
Set names = new TreeSet<>();
try {
QueryBuilder qb = cardsDao.queryBuilder();
- qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName");
+ qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName");
qb.where().like("types", new SelectArg('%' + CardType.ARTIFACT.name() + '%'));
List results = cardsDao.query(qb.prepare());
for (CardInfo card : results) {
@@ -265,7 +265,7 @@ public enum CardRepository {
Set names = new TreeSet<>();
try {
QueryBuilder qb = cardsDao.queryBuilder();
- qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName");
+ qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName");
Where where = qb.where();
where.and(
where.not().like("types", '%' + CardType.CREATURE.name() + '%'),
@@ -286,7 +286,7 @@ public enum CardRepository {
Set names = new TreeSet<>();
try {
QueryBuilder qb = cardsDao.queryBuilder();
- qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName");
+ qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName");
Where where = qb.where();
where.and(
where.not().like("types", '%' + CardType.ARTIFACT.name() + '%'),
@@ -511,7 +511,7 @@ public enum CardRepository {
queryBuilder.where()
.eq("flipCardName", new SelectArg(name)).or()
.eq("secondSideName", new SelectArg(name)).or()
- .eq("adventureSpellName", new SelectArg(name)).or()
+ .eq("spellOptionCardName", new SelectArg(name)).or()
.eq("modalDoubleFacedSecondSideName", new SelectArg(name));
results = cardsDao.query(queryBuilder.prepare());
} else {
diff --git a/Mage/src/main/java/mage/constants/ModeChoice.java b/Mage/src/main/java/mage/constants/ModeChoice.java
new file mode 100644
index 00000000000..1759d2dece8
--- /dev/null
+++ b/Mage/src/main/java/mage/constants/ModeChoice.java
@@ -0,0 +1,75 @@
+package mage.constants;
+
+import mage.abilities.Ability;
+import mage.abilities.condition.Condition;
+import mage.game.Game;
+
+import java.util.Objects;
+
+public enum ModeChoice {
+
+ KHANS("Khans"),
+ DRAGONS("Dragons"),
+
+ MARDU("Mardu"),
+ TEMUR("Temur"),
+ ABZAN("Abzan"),
+ JESKAI("Jeskai"),
+ SULTAI("Sultai"),
+
+ MIRRAN("Mirran"),
+ PHYREXIAN("Phyrexian "),
+
+ ODD("odd"),
+ EVEN("even"),
+
+ BELIEVE("Believe"),
+ DOUBT("Doubt"),
+
+ NCR("NCR"),
+ LEGION("Legion"),
+
+ BROTHERHOOD("Brotherhood"),
+ ENCLAVE("Enclave"),
+
+ ISLAND("Island"),
+ SWAMP("Swamp"),
+
+ LEFT("left"),
+ RIGHT("right");
+
+ private static class ModeChoiceCondition implements Condition {
+
+ private final ModeChoice modeChoice;
+
+ ModeChoiceCondition(ModeChoice modeChoice) {
+ this.modeChoice = modeChoice;
+ }
+
+ @Override
+ public boolean apply(Game game, Ability source) {
+ return modeChoice.checkMode(game, source);
+ }
+ }
+
+ private final String name;
+ private final ModeChoiceCondition condition;
+
+ ModeChoice(String name) {
+ this.name = name;
+ this.condition = new ModeChoiceCondition(this);
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+
+ public ModeChoiceCondition getCondition() {
+ return condition;
+ }
+
+ public boolean checkMode(Game game, Ability source) {
+ return Objects.equals(game.getState().getValue(source.getSourceId() + "_modeChoice"), name);
+ }
+}
diff --git a/Mage/src/main/java/mage/constants/SpellAbilityCastMode.java b/Mage/src/main/java/mage/constants/SpellAbilityCastMode.java
index a57e28325f6..6eedb18741f 100644
--- a/Mage/src/main/java/mage/constants/SpellAbilityCastMode.java
+++ b/Mage/src/main/java/mage/constants/SpellAbilityCastMode.java
@@ -15,6 +15,7 @@ public enum SpellAbilityCastMode {
NORMAL("Normal"),
MADNESS("Madness"),
FLASHBACK("Flashback"),
+ HARMONIZE("Harmonize"),
BESTOW("Bestow"),
PROTOTYPE("Prototype"),
MORPH("Morph", false, true), // and megamorph
@@ -91,6 +92,7 @@ public enum SpellAbilityCastMode {
case NORMAL:
case MADNESS:
case FLASHBACK:
+ case HARMONIZE:
case DISTURB:
case PLOT:
case MORE_THAN_MEETS_THE_EYE:
diff --git a/Mage/src/main/java/mage/constants/SpellAbilityType.java b/Mage/src/main/java/mage/constants/SpellAbilityType.java
index cb95677e060..fac7a218aef 100644
--- a/Mage/src/main/java/mage/constants/SpellAbilityType.java
+++ b/Mage/src/main/java/mage/constants/SpellAbilityType.java
@@ -15,7 +15,8 @@ public enum SpellAbilityType {
MODAL_LEFT("LeftModal SpellAbility"),
MODAL_RIGHT("RightModal SpellAbility"),
SPLICE("Spliced SpellAbility"),
- ADVENTURE_SPELL("Adventure SpellAbility");
+ ADVENTURE_SPELL("Adventure SpellAbility"),
+ OMEN_SPELL("Omen SpellAbility");
private final String text;
diff --git a/Mage/src/main/java/mage/constants/SubType.java b/Mage/src/main/java/mage/constants/SubType.java
index 5037f60d44a..cbf9f006c19 100644
--- a/Mage/src/main/java/mage/constants/SubType.java
+++ b/Mage/src/main/java/mage/constants/SubType.java
@@ -13,6 +13,7 @@ public enum SubType {
ADVENTURE("Adventure", SubTypeSet.SpellType),
ARCANE("Arcane", SubTypeSet.SpellType),
LESSON("Lesson", SubTypeSet.SpellType),
+ OMEN("Omen", SubTypeSet.SpellType),
TRAP("Trap", SubTypeSet.SpellType),
// Battle subtypes
@@ -274,6 +275,7 @@ public enum SubType {
MONGOOSE("Mongoose", SubTypeSet.CreatureType),
MONK("Monk", SubTypeSet.CreatureType),
MONKEY("Monkey", SubTypeSet.CreatureType),
+ MOOGLE("Moogle", SubTypeSet.CreatureType),
MOONFOLK("Moonfolk", SubTypeSet.CreatureType),
MOUNT("Mount", SubTypeSet.CreatureType),
MOUSE("Mouse", SubTypeSet.CreatureType),
diff --git a/Mage/src/main/java/mage/counters/CounterType.java b/Mage/src/main/java/mage/counters/CounterType.java
index dcb5353e8b3..0afd7f62cb5 100644
--- a/Mage/src/main/java/mage/counters/CounterType.java
+++ b/Mage/src/main/java/mage/counters/CounterType.java
@@ -54,6 +54,7 @@ public enum CounterType {
CURRENCY("currency"),
DEATH("death"),
DEATHTOUCH("deathtouch"),
+ DECAYED("decayed"),
DEFENSE("defense"),
DELAY("delay"),
DEPLETION("depletion"),
@@ -184,6 +185,7 @@ public enum CounterType {
PREY("prey"),
PUPA("pupa"),
RAD("rad"),
+ RALLY("rally"),
REACH("reach"),
REJECTION("rejection"),
REPAIR("repair"),
@@ -314,6 +316,8 @@ public enum CounterType {
return new BoostCounter(-2, -2, amount);
case DEATHTOUCH:
return new AbilityCounter(DeathtouchAbility.getInstance(), amount);
+ case DECAYED:
+ return new AbilityCounter(new DecayedAbility(), amount);
case DOUBLE_STRIKE:
return new AbilityCounter(DoubleStrikeAbility.getInstance(), amount);
case EXALTED:
diff --git a/Mage/src/main/java/mage/filter/predicate/card/CardTextPredicate.java b/Mage/src/main/java/mage/filter/predicate/card/CardTextPredicate.java
index 7f412b0e45f..dc15882ba86 100644
--- a/Mage/src/main/java/mage/filter/predicate/card/CardTextPredicate.java
+++ b/Mage/src/main/java/mage/filter/predicate/card/CardTextPredicate.java
@@ -1,9 +1,6 @@
package mage.filter.predicate.card;
-import mage.cards.AdventureCard;
-import mage.cards.Card;
-import mage.cards.ModalDoubleFacedCard;
-import mage.cards.SplitCard;
+import mage.cards.*;
import mage.cards.mock.MockCard;
import mage.constants.SubType;
import mage.constants.SuperType;
@@ -55,8 +52,8 @@ public class CardTextPredicate implements Predicate {
fullName = ((MockCard) input).getFullName(true);
} else if (input instanceof ModalDoubleFacedCard) {
fullName = input.getName() + MockCard.MODAL_DOUBLE_FACES_NAME_SEPARATOR + ((ModalDoubleFacedCard) input).getRightHalfCard().getName();
- } else if (input instanceof AdventureCard) {
- fullName = input.getName() + MockCard.ADVENTURE_NAME_SEPARATOR + ((AdventureCard) input).getSpellCard().getName();
+ } else if (input instanceof CardWithSpellOption) {
+ fullName = input.getName() + MockCard.CARD_WITH_SPELL_OPTION_NAME_SEPARATOR + ((CardWithSpellOption) input).getSpellCard().getName();
}
if (fullName.toLowerCase(Locale.ENGLISH).contains(text.toLowerCase(Locale.ENGLISH))) {
@@ -107,8 +104,8 @@ public class CardTextPredicate implements Predicate {
}
}
- if (input instanceof AdventureCard) {
- for (String rule : ((AdventureCard) input).getSpellCard().getRules(game)) {
+ if (input instanceof CardWithSpellOption) {
+ for (String rule : ((CardWithSpellOption) input).getSpellCard().getRules(game)) {
if (rule.toLowerCase(Locale.ENGLISH).contains(token)) {
found = true;
break;
diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java
index 17238d3c262..7ff0e1610a7 100644
--- a/Mage/src/main/java/mage/game/GameImpl.java
+++ b/Mage/src/main/java/mage/game/GameImpl.java
@@ -334,8 +334,8 @@ public abstract class GameImpl implements Game {
Card rightCard = ((ModalDoubleFacedCard) card).getRightHalfCard();
rightCard.setOwnerId(ownerId);
addCardToState(rightCard);
- } else if (card instanceof AdventureCard) {
- Card spellCard = ((AdventureCard) card).getSpellCard();
+ } else if (card instanceof CardWithSpellOption) {
+ Card spellCard = ((CardWithSpellOption) card).getSpellCard();
spellCard.setOwnerId(ownerId);
addCardToState(spellCard);
} else if (card.isTransformable() && card.getSecondCardFace() != null) {
diff --git a/Mage/src/main/java/mage/game/GameState.java b/Mage/src/main/java/mage/game/GameState.java
index e0c3d41d452..3f13573471d 100644
--- a/Mage/src/main/java/mage/game/GameState.java
+++ b/Mage/src/main/java/mage/game/GameState.java
@@ -1639,14 +1639,14 @@ public class GameState implements Serializable, Copyable {
copiedParts.add(rightCopied);
// sync parts
((ModalDoubleFacedCard) copiedCard).setParts(leftCopied, rightCopied);
- } else if (copiedCard instanceof AdventureCard) {
+ } else if (copiedCard instanceof CardWithSpellOption) {
// right
- AdventureCardSpell rightOriginal = ((AdventureCard) copiedCard).getSpellCard();
- AdventureCardSpell rightCopied = rightOriginal.copy();
+ SpellOptionCard rightOriginal = ((CardWithSpellOption) copiedCard).getSpellCard();
+ SpellOptionCard rightCopied = rightOriginal.copy();
prepareCardForCopy(rightOriginal, rightCopied, newController);
copiedParts.add(rightCopied);
// sync parts
- ((AdventureCard) copiedCard).setParts(rightCopied);
+ ((CardWithSpellOption) copiedCard).setParts(rightCopied);
}
// main part prepare (must be called after other parts cause it change ids for all)
diff --git a/Mage/src/main/java/mage/game/permanent/token/AlienRhinoToken.java b/Mage/src/main/java/mage/game/permanent/token/AlienRhinoToken.java
new file mode 100644
index 00000000000..43b6fbd5da4
--- /dev/null
+++ b/Mage/src/main/java/mage/game/permanent/token/AlienRhinoToken.java
@@ -0,0 +1,34 @@
+package mage.game.permanent.token;
+
+import mage.MageInt;
+import mage.abilities.common.SimpleStaticAbility;
+import mage.abilities.effects.common.PreventDamageToSourceEffect;
+import mage.abilities.keyword.VanishingAbility;
+import mage.constants.CardType;
+import mage.constants.SubType;
+import mage.constants.Duration;
+
+/**
+ * @author padfoothelix
+ */
+public final class AlienRhinoToken extends TokenImpl {
+
+ public AlienRhinoToken() {
+ super("Alien Rhino Token", "4/4 white Alien Rhino creature token");
+ cardType.add(CardType.CREATURE);
+ color.setWhite(true);
+ subtype.add(SubType.ALIEN);
+ subtype.add(SubType.RHINO);
+ power = new MageInt(4);
+ toughness = new MageInt(4);
+ }
+
+ private AlienRhinoToken(final AlienRhinoToken token) {
+ super(token);
+ }
+
+ @Override
+ public AlienRhinoToken copy() {
+ return new AlienRhinoToken(this);
+ }
+}
diff --git a/Mage/src/main/java/mage/game/permanent/token/Human11WithWard2Token.java b/Mage/src/main/java/mage/game/permanent/token/Human11WithWard2Token.java
new file mode 100644
index 00000000000..a39848fcb5c
--- /dev/null
+++ b/Mage/src/main/java/mage/game/permanent/token/Human11WithWard2Token.java
@@ -0,0 +1,33 @@
+package mage.game.permanent.token;
+
+import mage.MageInt;
+import mage.abilities.costs.mana.GenericManaCost;
+import mage.abilities.keyword.WardAbility;
+import mage.constants.CardType;
+import mage.constants.SubType;
+import mage.constants.Duration;
+
+/**
+ * @author padfoothelix
+ */
+public final class Human11WithWard2Token extends TokenImpl {
+
+ public Human11WithWard2Token() {
+ super("Human Token", "1/1 white Human creature token with ward {2}");
+ cardType.add(CardType.CREATURE);
+ color.setWhite(true);
+ subtype.add(SubType.HUMAN);
+ power = new MageInt(1);
+ toughness = new MageInt(1);
+ this.addAbility(new WardAbility(new GenericManaCost(2)));
+ }
+
+ private Human11WithWard2Token(final Human11WithWard2Token token) {
+ super(token);
+ }
+
+ @Override
+ public Human11WithWard2Token copy() {
+ return new Human11WithWard2Token(this);
+ }
+}
diff --git a/Mage/src/main/java/mage/game/stack/Spell.java b/Mage/src/main/java/mage/game/stack/Spell.java
index c234e901643..149d8a6e92b 100644
--- a/Mage/src/main/java/mage/game/stack/Spell.java
+++ b/Mage/src/main/java/mage/game/stack/Spell.java
@@ -209,10 +209,11 @@ public class Spell extends StackObjectImpl implements Card {
+ " using " + this.getSpellAbility().getSpellAbilityCastMode();
}
- if (card instanceof AdventureCardSpell) {
- AdventureCard adventureCard = ((AdventureCardSpell) card).getParentCard();
- return GameLog.replaceNameByColoredName(card, getSpellAbility().toString(), adventureCard)
- + " as Adventure spell of " + GameLog.getColoredObjectIdName(adventureCard);
+ if (card instanceof SpellOptionCard) {
+ CardWithSpellOption parentCard = ((SpellOptionCard) card).getParentCard();
+ String type = ((SpellOptionCard) card).getSpellType();
+ return GameLog.replaceNameByColoredName(card, getSpellAbility().toString(), parentCard)
+ + " as " + type + " spell of " + GameLog.getColoredObjectIdName(parentCard);
}
if (card instanceof ModalDoubleFacedCardHalf) {
@@ -539,8 +540,8 @@ public class Spell extends StackObjectImpl implements Card {
public String getIdName() {
String idName;
if (card != null) {
- if (card instanceof AdventureCardSpell) {
- idName = ((AdventureCardSpell) card).getParentCard().getId().toString().substring(0, 3);
+ if (card instanceof SpellOptionCard) {
+ idName = ((SpellOptionCard) card).getParentCard().getId().toString().substring(0, 3);
} else {
idName = card.getId().toString().substring(0, 3);
}
diff --git a/Mage/src/main/java/mage/game/stack/StackAbility.java b/Mage/src/main/java/mage/game/stack/StackAbility.java
index 4f9244234ba..34fb4333ab9 100644
--- a/Mage/src/main/java/mage/game/stack/StackAbility.java
+++ b/Mage/src/main/java/mage/game/stack/StackAbility.java
@@ -426,6 +426,16 @@ public class StackAbility extends StackObjectImpl implements Ability {
// Do nothing
}
+ @Override
+ public void setVariableCostsMinMax(int min, int max) {
+ ability.setVariableCostsMinMax(min, max);
+ }
+
+ @Override
+ public void setVariableCostsValue(int xValue) {
+ ability.setVariableCostsValue(xValue);
+ }
+
@Override
public Map getCostsTagMap() {
return ability.getCostsTagMap();
@@ -778,14 +788,23 @@ public class StackAbility extends StackObjectImpl implements Ability {
}
@Override
- public CostAdjuster getCostAdjuster() {
- return costAdjuster;
+ public void adjustX(Game game) {
+ if (costAdjuster != null) {
+ costAdjuster.prepareX(this, game);
+ }
}
@Override
- public void adjustCosts(Game game) {
+ public void adjustCostsPrepare(Game game) {
if (costAdjuster != null) {
- costAdjuster.adjustCosts(this, game);
+ costAdjuster.prepareCost(this, game);
+ }
+ }
+
+ @Override
+ public void adjustCostsModify(Game game, CostModificationType costModificationType) {
+ if (costAdjuster != null) {
+ costAdjuster.modifyCost(this, game, costModificationType);
}
}
diff --git a/Mage/src/main/java/mage/players/Player.java b/Mage/src/main/java/mage/players/Player.java
index c6c7d14039c..987b47e37f8 100644
--- a/Mage/src/main/java/mage/players/Player.java
+++ b/Mage/src/main/java/mage/players/Player.java
@@ -743,10 +743,14 @@ public interface Player extends MageItem, Copyable {
boolean shuffleCardsToLibrary(Card card, Game game, Ability source);
- // set the value for X mana spells and abilities
+ /**
+ * Set the value for X mana spells and abilities
+ */
int announceXMana(int min, int max, String message, Game game, Ability ability);
- // set the value for non mana X costs
+ /**
+ * Set the value for non mana X costs
+ */
int announceXCost(int min, int max, String message, Game game, Ability ability, VariableCost variableCost);
// TODO: rework to use pair's list of effect + ability instead string's map
diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java
index a6f3ac1dd88..88e36b7c3fb 100644
--- a/Mage/src/main/java/mage/players/PlayerImpl.java
+++ b/Mage/src/main/java/mage/players/PlayerImpl.java
@@ -3679,8 +3679,11 @@ public abstract class PlayerImpl implements Player, Serializable {
if (!copy.canActivate(playerId, game).canActivate()) {
return false;
}
+
+ // apply dynamic costs and cost modification
+ copy.adjustX(game);
if (availableMana != null) {
- copy.adjustCosts(game);
+ // TODO: need research, why it look at availableMana here - can delete condition?
game.getContinuousEffects().costModification(copy, game);
}
boolean canBeCastRegularly = true;
@@ -3891,7 +3894,8 @@ public abstract class PlayerImpl implements Player, Serializable {
copyAbility = ability.copy();
copyAbility.clearManaCostsToPay();
copyAbility.addManaCostsToPay(manaCosts.copy());
- copyAbility.adjustCosts(game);
+ // apply dynamic costs and cost modification
+ copyAbility.adjustX(game);
game.getContinuousEffects().costModification(copyAbility, game);
// reduced all cost
@@ -3963,12 +3967,9 @@ public abstract class PlayerImpl implements Player, Serializable {
// alternative cost reduce
copyAbility = ability.copy();
copyAbility.clearManaCostsToPay();
- // TODO: IDE warning:
- // Unchecked assignment: 'mage.abilities.costs.mana.ManaCosts' to
- // 'java.util.Collection extends mage.abilities.costs.mana.ManaCost>'.
- // Reason: 'manaCosts' has raw type, so result of copy is erased
copyAbility.addManaCostsToPay(manaCosts.copy());
- copyAbility.adjustCosts(game);
+ // apply dynamic costs and cost modification
+ copyAbility.adjustX(game);
game.getContinuousEffects().costModification(copyAbility, game);
// reduced all cost
@@ -4068,11 +4069,11 @@ public abstract class PlayerImpl implements Player, Serializable {
getPlayableFromObjectSingle(game, fromZone, mainCard.getLeftHalfCard(), mainCard.getLeftHalfCard().getAbilities(game), availableMana, output);
getPlayableFromObjectSingle(game, fromZone, mainCard.getRightHalfCard(), mainCard.getRightHalfCard().getAbilities(game), availableMana, output);
getPlayableFromObjectSingle(game, fromZone, mainCard, mainCard.getSharedAbilities(game), availableMana, output);
- } else if (object instanceof AdventureCard) {
+ } else if (object instanceof CardWithSpellOption) {
// adventure must use different card characteristics for different spells (main or adventure)
- AdventureCard adventureCard = (AdventureCard) object;
- getPlayableFromObjectSingle(game, fromZone, adventureCard.getSpellCard(), adventureCard.getSpellCard().getAbilities(game), availableMana, output);
- getPlayableFromObjectSingle(game, fromZone, adventureCard, adventureCard.getSharedAbilities(game), availableMana, output);
+ CardWithSpellOption cardWithSpellOption = (CardWithSpellOption) object;
+ getPlayableFromObjectSingle(game, fromZone, cardWithSpellOption.getSpellCard(), cardWithSpellOption.getSpellCard().getAbilities(game), availableMana, output);
+ getPlayableFromObjectSingle(game, fromZone, cardWithSpellOption, cardWithSpellOption.getSharedAbilities(game), availableMana, output);
} else if (object instanceof Card) {
getPlayableFromObjectSingle(game, fromZone, object, ((Card) object).getAbilities(game), availableMana, output);
} else if (object instanceof StackObject) {
diff --git a/Mage/src/main/java/mage/target/targetadjustment/TargetsCountAdjuster.java b/Mage/src/main/java/mage/target/targetadjustment/TargetsCountAdjuster.java
index c84fbd949d5..c8ffbea7271 100644
--- a/Mage/src/main/java/mage/target/targetadjustment/TargetsCountAdjuster.java
+++ b/Mage/src/main/java/mage/target/targetadjustment/TargetsCountAdjuster.java
@@ -25,8 +25,13 @@ public class TargetsCountAdjuster extends GenericTargetAdjuster {
@Override
public void adjustTargets(Ability ability, Game game) {
- Target newTarget = blueprintTarget.copy();
int count = dynamicValue.calculate(game, ability, ability.getEffects().get(0));
+ ability.getTargets().clear();
+ if (count <= 0) {
+ return;
+ }
+
+ Target newTarget = blueprintTarget.copy();
newTarget.setMaxNumberOfTargets(count);
Filter filter = newTarget.getFilter();
if (blueprintTarget.getMinNumberOfTargets() != 0) {
@@ -35,7 +40,6 @@ public class TargetsCountAdjuster extends GenericTargetAdjuster {
} else {
newTarget.withTargetName(filter.getMessage() + " (up to " + count + " targets)");
}
- ability.getTargets().clear();
ability.addTarget(newTarget);
}
}
diff --git a/Mage/src/main/java/mage/target/targetpointer/FixedTargets.java b/Mage/src/main/java/mage/target/targetpointer/FixedTargets.java
index af3d80dbb31..1a3b3d926bd 100644
--- a/Mage/src/main/java/mage/target/targetpointer/FixedTargets.java
+++ b/Mage/src/main/java/mage/target/targetpointer/FixedTargets.java
@@ -22,14 +22,7 @@ public class FixedTargets extends TargetPointerImpl {
final ArrayList targets = new ArrayList<>();
- public FixedTargets(List objects, Game game) {
- this(objects
- .stream()
- .map(o -> new MageObjectReference(o.getId(), game))
- .collect(Collectors.toList()));
- }
-
- public FixedTargets(Set objects, Game game) {
+ public FixedTargets(Collection extends Card> objects, Game game) {
this(objects
.stream()
.map(o -> new MageObjectReference(o.getId(), game))
@@ -50,7 +43,7 @@ public class FixedTargets extends TargetPointerImpl {
.collect(Collectors.toList()), game);
}
- public FixedTargets(List morList) {
+ public FixedTargets(Collection morList) {
super();
targets.addAll(morList);
this.setInitialized(); // no need dynamic init
diff --git a/Mage/src/main/java/mage/util/CardUtil.java b/Mage/src/main/java/mage/util/CardUtil.java
index 7ba17e4036f..eb2a2e8d513 100644
--- a/Mage/src/main/java/mage/util/CardUtil.java
+++ b/Mage/src/main/java/mage/util/CardUtil.java
@@ -1079,6 +1079,53 @@ public final class CardUtil {
.collect(Collectors.toSet());
}
+ public static Set getAllPossibleTargets(Cost cost, Game game, Ability source) {
+ return cost.getTargets()
+ .stream()
+ .map(t -> t.possibleTargets(source.getControllerId(), source, game))
+ .flatMap(Collection::stream)
+ .collect(Collectors.toSet());
+ }
+
+ /**
+ * Distribute values between min and max and make sure that the values will be evenly distributed
+ * Use it to limit possible values list like mana options
+ */
+ public static List distributeValues(int count, int min, int max) {
+ List res = new ArrayList<>();
+ if (count <= 0 || min > max) {
+ return res;
+ }
+
+ if (min == max) {
+ res.add(min);
+ return res;
+ }
+
+ int range = max - min + 1;
+
+ // low possible amount
+ if (range <= count) {
+ for (int i = 0; i < range; i++) {
+ res.add(min + i);
+ }
+ return res;
+ }
+
+ // big possible amount, so skip some values
+ double step = (double) (max - min) / (count - 1);
+ for (int i = 0; i < count; i++) {
+ res.add(min + (int) Math.round(i * step));
+ }
+ // make sure first and last elements are good
+ res.set(0, min);
+ if (res.size() > 1) {
+ res.set(res.size() - 1, max);
+ }
+
+ return res;
+ }
+
/**
* For finding the spell or ability on the stack for "becomes the target" triggers.
*
@@ -1086,13 +1133,22 @@ public final class CardUtil {
* @param game the Game from checkTrigger() or watch()
* @return the StackObject which targeted the source, or null if not found
*/
- public static StackObject getTargetingStackObject(GameEvent event, Game game) {
+ public static StackObject getTargetingStackObject(String checkingReference, GameEvent event, Game game) {
// In case of multiple simultaneous triggered abilities from the same source,
// need to get the actual one that targeted, see #8026, #8378
// Also avoids triggering on cancelled selections, see #8802
+ String stateKey = "targetedMap" + checkingReference;
+ Map> targetMap = (Map>) game.getState().getValue(stateKey);
+ // targetMap: key - targetId; value - Set of stackObject Ids
+ if (targetMap == null) {
+ targetMap = new HashMap<>();
+ } else {
+ targetMap = new HashMap<>(targetMap); // must have new object reference if saved back to game state
+ }
+ Set targetingObjects = targetMap.computeIfAbsent(event.getTargetId(), k -> new HashSet<>());
for (StackObject stackObject : game.getStack()) {
Ability stackAbility = stackObject.getStackAbility();
- if (stackAbility == null || !stackAbility.getSourceId().equals(event.getSourceId())) {
+ if (stackAbility == null || !stackAbility.getSourceId().equals(event.getSourceId()) || targetingObjects.contains(stackObject.getId())) {
continue;
}
if (CardUtil.getAllSelectedTargets(stackAbility, game).contains(event.getTargetId())) {
@@ -1263,7 +1319,7 @@ public final class CardUtil {
Card permCard;
if (card instanceof SplitCard) {
permCard = card;
- } else if (card instanceof AdventureCard) {
+ } else if (card instanceof CardWithSpellOption) {
permCard = card;
} else if (card instanceof ModalDoubleFacedCard) {
permCard = ((ModalDoubleFacedCard) card).getLeftHalfCard();
@@ -1451,9 +1507,9 @@ public final class CardUtil {
if (cardToCast instanceof CardWithHalves) {
cards.add(((CardWithHalves) cardToCast).getLeftHalfCard());
cards.add(((CardWithHalves) cardToCast).getRightHalfCard());
- } else if (cardToCast instanceof AdventureCard) {
+ } else if (cardToCast instanceof CardWithSpellOption) {
cards.add(cardToCast);
- cards.add(((AdventureCard) cardToCast).getSpellCard());
+ cards.add(((CardWithSpellOption) cardToCast).getSpellCard());
} else {
cards.add(cardToCast);
}
@@ -1642,9 +1698,9 @@ public final class CardUtil {
}
// handle adventure cards
- if (card instanceof AdventureCard) {
+ if (card instanceof CardWithSpellOption) {
Card creatureCard = card.getMainCard();
- Card spellCard = ((AdventureCard) card).getSpellCard();
+ Card spellCard = ((CardWithSpellOption) card).getSpellCard();
if (manaCost != null) {
// get additional cost if any
Costs additionalCostsCreature = creatureCard.getSpellAbility().getCosts();
@@ -1682,9 +1738,9 @@ public final class CardUtil {
game.getState().setValue("PlayFromNotOwnHandZone" + leftHalfCard.getId(), null);
game.getState().setValue("PlayFromNotOwnHandZone" + rightHalfCard.getId(), null);
}
- if (card instanceof AdventureCard) {
+ if (card instanceof CardWithSpellOption) {
Card creatureCard = card.getMainCard();
- Card spellCard = ((AdventureCard) card).getSpellCard();
+ Card spellCard = ((CardWithSpellOption) card).getSpellCard();
game.getState().setValue("PlayFromNotOwnHandZone" + creatureCard.getId(), null);
game.getState().setValue("PlayFromNotOwnHandZone" + spellCard.getId(), null);
}
@@ -1899,6 +1955,10 @@ public final class CardUtil {
return defaultValue;
}
+ public static int getSourceCostsTagX(Game game, Ability source, int defaultValue) {
+ return getSourceCostsTag(game, source, "X", defaultValue);
+ }
+
public static String addCostVerb(String text) {
if (costWords.stream().anyMatch(text.toLowerCase(Locale.ENGLISH)::startsWith)) {
return text;
@@ -2069,8 +2129,8 @@ public final class CardUtil {
res.add(mainCard);
res.add(mainCard.getLeftHalfCard());
res.add(mainCard.getRightHalfCard());
- } else if (object instanceof AdventureCard || object instanceof AdventureCardSpell) {
- AdventureCard mainCard = (AdventureCard) ((Card) object).getMainCard();
+ } else if (object instanceof CardWithSpellOption || object instanceof SpellOptionCard) {
+ CardWithSpellOption mainCard = (CardWithSpellOption) ((Card) object).getMainCard();
res.add(mainCard);
res.add(mainCard.getSpellCard());
} else if (object instanceof Spell) {
@@ -2176,6 +2236,19 @@ public final class CardUtil {
return sb.toString();
}
+ public static T getEffectValueFromAbility(Ability ability, String key, Class clazz) {
+ return getEffectValueFromAbility(ability, key, clazz, null);
+ }
+
+ public static T getEffectValueFromAbility(Ability ability, String key, Class clazz, T defaultValue) {
+ return castStream(
+ ability.getAllEffects()
+ .stream()
+ .map(effect -> effect.getValue(key)),
+ clazz
+ ).findFirst().orElse(defaultValue);
+ }
+
public static Stream castStream(Collection> collection, Class clazz) {
return castStream(collection.stream(), clazz);
}
diff --git a/Mage/src/main/java/mage/util/CircularList.java b/Mage/src/main/java/mage/util/CircularList.java
index 5fb6a09c6d6..45890ad68f1 100644
--- a/Mage/src/main/java/mage/util/CircularList.java
+++ b/Mage/src/main/java/mage/util/CircularList.java
@@ -82,8 +82,8 @@ public class CircularList implements List, Iterable, Serializable {
*/
@Override
public E get(int index) {
- if (list.size() > this.index) {
- return list.get(this.index);
+ if (list.size() > index) {
+ return list.get(index);
}
return null;
}
diff --git a/Mage/src/main/java/mage/util/ManaUtil.java b/Mage/src/main/java/mage/util/ManaUtil.java
index 08b478d6e23..816353e13e9 100644
--- a/Mage/src/main/java/mage/util/ManaUtil.java
+++ b/Mage/src/main/java/mage/util/ManaUtil.java
@@ -13,10 +13,7 @@ import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.dynamicvalue.common.GetXValue;
import mage.abilities.effects.Effect;
import mage.abilities.mana.*;
-import mage.cards.AdventureCard;
-import mage.cards.Card;
-import mage.cards.ModalDoubleFacedCard;
-import mage.cards.SplitCard;
+import mage.cards.*;
import mage.choices.Choice;
import mage.constants.ColoredManaSymbol;
import mage.constants.ManaType;
@@ -644,8 +641,8 @@ public final class ManaUtil {
Card secondSide;
if (card instanceof SplitCard) {
secondSide = ((SplitCard) card).getRightHalfCard();
- } else if (card instanceof AdventureCard) {
- secondSide = ((AdventureCard) card).getSpellCard();
+ } else if (card instanceof CardWithSpellOption) {
+ secondSide = ((CardWithSpellOption) card).getSpellCard();
} else if (card instanceof ModalDoubleFacedCard) {
secondSide = ((ModalDoubleFacedCard) card).getRightHalfCard();
} else {
diff --git a/Mage/src/main/java/mage/watchers/common/NumberOfTimesPermanentTargetedATurnWatcher.java b/Mage/src/main/java/mage/watchers/common/NumberOfTimesPermanentTargetedATurnWatcher.java
index c97cf9436f3..accc6b4628c 100644
--- a/Mage/src/main/java/mage/watchers/common/NumberOfTimesPermanentTargetedATurnWatcher.java
+++ b/Mage/src/main/java/mage/watchers/common/NumberOfTimesPermanentTargetedATurnWatcher.java
@@ -29,7 +29,7 @@ public class NumberOfTimesPermanentTargetedATurnWatcher extends Watcher {
if (event.getType() != GameEvent.EventType.TARGETED) {
return;
}
- StackObject targetingObject = CardUtil.getTargetingStackObject(event, game);
+ StackObject targetingObject = CardUtil.getTargetingStackObject(this.getKey(), event, game);
if (targetingObject == null || CardUtil.checkTargetedEventAlreadyUsed(this.getKey(), targetingObject, event, game)) {
return;
}
diff --git a/Mage/src/main/resources/tokens-database.txt b/Mage/src/main/resources/tokens-database.txt
index 0b77e47469c..8dc1ecf4e63 100644
--- a/Mage/src/main/resources/tokens-database.txt
+++ b/Mage/src/main/resources/tokens-database.txt
@@ -2235,6 +2235,7 @@
# WHO
|Generate|TOK:WHO|Alien|||AlienToken|
|Generate|TOK:WHO|Alien Insect|||AlienInsectToken|
+|Generate|TOK:WHO|Alien Rhino|||AlienRhinoToken|
|Generate|TOK:WHO|Alien Salamander|||AlienSalamanderToken|
|Generate|TOK:WHO|Alien Warrior|||AlienWarriorToken|
|Generate|TOK:WHO|Beast|||BeastToken|
@@ -2248,7 +2249,8 @@
|Generate|TOK:WHO|Food|2||FoodToken|
|Generate|TOK:WHO|Food|3||FoodToken|
|Generate|TOK:WHO|Horse|||TheGirlInTheFireplaceHorseToken|
-|Generate|TOK:WHO|Human|||TheEleventhHourToken|
+|Generate|TOK:WHO|Human|1||Human11WithWard2Token|
+|Generate|TOK:WHO|Human|2||TheEleventhHourToken|
|Generate|TOK:WHO|Human Noble|||TheGirlInTheFireplaceHumanNobleToken|
|Generate|TOK:WHO|Mark of the Rani|||MarkOfTheRaniToken|
|Generate|TOK:WHO|Soldier|||SoldierToken|
diff --git a/Utils/keywords.txt b/Utils/keywords.txt
index a3eea683be2..fb6f00141ca 100644
--- a/Utils/keywords.txt
+++ b/Utils/keywords.txt
@@ -25,6 +25,7 @@ Cycling|cost|
Dash|manaString|
Daybound|new|
Deathtouch|instance|
+Decayed|new|
Demonstrate|new|
Delve|new|
Dethrone|new|
diff --git a/pom.xml b/pom.xml
index a9f8e251668..f61deeb6eff 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.mage
mage-root
- 1.4.56
+ 1.4.57
pom
Mage Root
Mage Root POM
@@ -16,7 +16,7 @@
${project.basedir}
- 1.4.56
+ 1.4.57
-Dfile.encoding=UTF-8
UTF-8
yyyy-MM-dd'T'HH:mm:ss'Z'