diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/cost/alternate/BolassCitadelTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/cost/alternate/BolassCitadelTest.java index 9eb185896e1..8fba1058dff 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/cost/alternate/BolassCitadelTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/cost/alternate/BolassCitadelTest.java @@ -67,7 +67,6 @@ public class BolassCitadelTest extends CardTestPlayerBase { addCard(Zone.LIBRARY, playerA, "Fellwar Stone"); // cast from top library for 2 life - //showAvaileableAbilities("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Fellwar Stone"); setStrictChooseMode(true); @@ -78,4 +77,56 @@ public class BolassCitadelTest extends CardTestPlayerBase { assertPermanentCount(playerA, "Fellwar Stone", 1); assertLife(playerA, 20 - 2); } + + @Test + public void testCardWithCycling() { + // bug: can't cast cards with cycling, see https://github.com/magefree/mage/issues/6215 + // bug active only in GUI (HumanPlayer) + removeAllCardsFromLibrary(playerA); + + // You may play the top card of your library. If you cast a spell this way, pay life equal to its converted mana cost rather than pay its mana cost. + addCard(Zone.BATTLEFIELD, playerA, "Bolas's Citadel"); + // Cycling {2} ({2}, Discard this card: Draw a card.) + addCard(Zone.LIBRARY, playerA, "Archfiend of Ifnir"); // {3}{B}{B} + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Archfiend of Ifnir"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Archfiend of Ifnir", 1); + assertLife(playerA, 20 - 5); + } + + @Test + public void testCardWithAdventure() { + removeAllCardsFromLibrary(playerA); + + // You may play the top card of your library. If you cast a spell this way, pay life equal to its converted mana cost rather than pay its mana cost. + addCard(Zone.BATTLEFIELD, playerA, "Bolas's Citadel"); + // Adventure spell: Chop Down {2}{W} - Destroy target creature with power 4 or greater. + addCard(Zone.LIBRARY, playerA, "Giant Killer"); // {W} + addCard(Zone.BATTLEFIELD, playerA, "Ferocious Zheng"); // 4/4 + addCard(Zone.BATTLEFIELD, playerA, "Plains", 1); + + // cast adventure + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Chop Down", "Ferocious Zheng"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkPermanentCount("must kill", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Ferocious Zheng", 0); + checkExileCount("on adventure", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Giant Killer", 1); + + // cast normal + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Giant Killer"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Giant Killer", 1); + assertGraveyardCount(playerA, "Ferocious Zheng", 1); + assertLife(playerA, 20 - 3); + } } diff --git a/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java b/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java index c8a42c6a05e..c29b6c58dac 100644 --- a/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java +++ b/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java @@ -1,12 +1,11 @@ package mage.abilities.effects; -import java.io.Serializable; -import java.util.*; -import java.util.Map.Entry; -import java.util.stream.Collectors; import mage.MageObject; import mage.MageObjectReference; -import mage.abilities.*; +import mage.abilities.Ability; +import mage.abilities.MageSingleton; +import mage.abilities.SpellAbility; +import mage.abilities.StaticAbility; import mage.abilities.effects.common.continuous.BecomesFaceDownCreatureEffect; import mage.abilities.effects.common.continuous.CommanderReplacementEffect; import mage.cards.*; @@ -27,6 +26,11 @@ import mage.players.Player; import mage.target.common.TargetCardInHand; import org.apache.log4j.Logger; +import java.io.Serializable; +import java.util.*; +import java.util.Map.Entry; +import java.util.stream.Collectors; + /** * @author BetaSteward_at_googlemail.com */ @@ -334,7 +338,7 @@ public class ContinuousEffects implements Serializable { } // boolean checkLKI = event.getType().equals(EventType.ZONE_CHANGE) || event.getType().equals(EventType.DESTROYED_PERMANENT); //get all applicable transient Replacement effects - for (Iterator iterator = replacementEffects.iterator(); iterator.hasNext();) { + for (Iterator iterator = replacementEffects.iterator(); iterator.hasNext(); ) { ReplacementEffect effect = iterator.next(); if (!effect.checksEventType(event, game)) { continue; @@ -367,7 +371,7 @@ public class ContinuousEffects implements Serializable { } } - for (Iterator iterator = preventionEffects.iterator(); iterator.hasNext();) { + for (Iterator iterator = preventionEffects.iterator(); iterator.hasNext(); ) { PreventionEffect effect = iterator.next(); if (!effect.checksEventType(event, game)) { continue; @@ -507,8 +511,7 @@ public class ContinuousEffects implements Serializable { if (affectedAbility != null && affectedAbility.getSourceObject(game) instanceof SplitCardHalf) { idToCheck = ((SplitCardHalf) affectedAbility.getSourceObject(game)).getParentCard().getId(); } else if (affectedAbility != null && affectedAbility.getSourceObject(game) instanceof AdventureCardSpell - && type != AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE - && type != AsThoughEffectType.CAST_AS_INSTANT) { + && !type.needPlayCardAbility()) { // adventure spell uses alternative characteristics for spell/stack idToCheck = ((AdventureCardSpell) affectedAbility.getSourceObject(game)).getParentCard().getId(); } else { @@ -516,23 +519,33 @@ public class ContinuousEffects implements Serializable { if (card instanceof SplitCardHalf) { idToCheck = ((SplitCardHalf) card).getParentCard().getId(); } else if (card instanceof AdventureCardSpell - && type != AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE - && type != AsThoughEffectType.CAST_AS_INSTANT) { + && !type.needPlayCardAbility()) { // adventure spell uses alternative characteristics for spell/stack idToCheck = ((AdventureCardSpell) card).getParentCard().getId(); } else { idToCheck = objectId; } } + for (AsThoughEffect effect : asThoughEffectsList) { Set abilities = asThoughEffectsMap.get(type).getAbility(effect.getId()); for (Ability ability : abilities) { if (affectedAbility == null) { + // applies to own ability (one effect can be used in multiple abilities) if (effect.applies(idToCheck, ability, controllerId, game)) { return new MageObjectReference(ability.getSourceObject(game), game); } - } else if (effect.applies(idToCheck, affectedAbility, ability, game, controllerId)) { - return new MageObjectReference(ability.getSourceObject(game), game); + } else { + // applies to affected ability + + // filter play abilities (no need to check it in every effect's code) + if (type.needPlayCardAbility() && !affectedAbility.getAbilityType().isPlayCardAbility()) { + continue; + } + + if (effect.applies(idToCheck, affectedAbility, ability, game, controllerId)) { + return new MageObjectReference(ability.getSourceObject(game), game); + } } } } @@ -725,10 +738,10 @@ public class ContinuousEffects implements Serializable { * Checks if an event won't happen because of an rule modifying effect * * @param event - * @param targetAbility ability the event is attached to. can be null. + * @param targetAbility ability the event is attached to. can be null. * @param game * @param checkPlayableMode true if the event does not really happen but - * it's checked if the event would be replaced + * it's checked if the event would be replaced * @return */ public boolean preventedByRuleModification(GameEvent event, Ability targetAbility, Game game, boolean checkPlayableMode) { @@ -772,7 +785,7 @@ public class ContinuousEffects implements Serializable { do { Map> rEffects = getApplicableReplacementEffects(event, game); // Remove all consumed effects (ability dependant) - for (Iterator it1 = rEffects.keySet().iterator(); it1.hasNext();) { + for (Iterator it1 = rEffects.keySet().iterator(); it1.hasNext(); ) { ReplacementEffect entry = it1.next(); if (consumed.containsKey(entry.getId()) /*&& !(entry instanceof CommanderReplacementEffect) */) { // 903.9. Set consumedAbilitiesIds = consumed.get(entry.getId()); @@ -963,7 +976,7 @@ public class ContinuousEffects implements Serializable { if (!waitingEffects.isEmpty()) { // check if waiting effects can be applied now - for (Iterator>> iterator = waitingEffects.entrySet().iterator(); iterator.hasNext();) { + for (Iterator>> iterator = waitingEffects.entrySet().iterator(); iterator.hasNext(); ) { Map.Entry> entry = iterator.next(); if (appliedEffects.containsAll(entry.getValue())) { // all dependent to effects are applied now so apply the effect itself appliedAbilities = appliedEffectAbilities.get(entry.getKey()); diff --git a/Mage/src/main/java/mage/constants/AbilityType.java b/Mage/src/main/java/mage/constants/AbilityType.java index cd65522ddef..b0956e98ecd 100644 --- a/Mage/src/main/java/mage/constants/AbilityType.java +++ b/Mage/src/main/java/mage/constants/AbilityType.java @@ -1,29 +1,33 @@ package mage.constants; /** - * * @author North */ public enum AbilityType { - - PLAY_LAND("Play land"), - MANA("Mana"), - SPELL("Spell"), - ACTIVATED("Activated"), - STATIC("Static"), - TRIGGERED("Triggered"), - EVASION("Evasion"), - LOYALTY("Loyalty"), - SPECIAL_ACTION("Special Action"); + PLAY_LAND("Play land", true), + MANA("Mana", false), + SPELL("Spell", true), + ACTIVATED("Activated", false), + STATIC("Static", false), + TRIGGERED("Triggered", false), + EVASION("Evasion", false), + LOYALTY("Loyalty", false), + SPECIAL_ACTION("Special Action", false); private final String text; + private final boolean playCardAbility; - AbilityType(String text) { + AbilityType(String text, boolean playCardAbility) { this.text = text; + this.playCardAbility = playCardAbility; } @Override public String toString() { return text; } + + public boolean isPlayCardAbility() { + return playCardAbility; + } } diff --git a/Mage/src/main/java/mage/constants/AsThoughEffectType.java b/Mage/src/main/java/mage/constants/AsThoughEffectType.java index 808a99277d8..c4f1ace53b0 100644 --- a/Mage/src/main/java/mage/constants/AsThoughEffectType.java +++ b/Mage/src/main/java/mage/constants/AsThoughEffectType.java @@ -24,8 +24,8 @@ public enum AsThoughEffectType { // 2. All effects in "applies" must checks affectedControllerId.equals(source.getControllerId()) (if not then all players will be able to play it) // 3. Target points to mainCard, but card's characteristics from objectId (split, adventure) // TODO: search all PLAY_FROM_NOT_OWN_HAND_ZONE and CAST_AS_INSTANT effects and add support of mainCard and objectId - PLAY_FROM_NOT_OWN_HAND_ZONE, - CAST_AS_INSTANT, + PLAY_FROM_NOT_OWN_HAND_ZONE(true), + CAST_AS_INSTANT(true), ACTIVATE_AS_INSTANT, DAMAGE, @@ -42,5 +42,19 @@ public enum AsThoughEffectType { SPEND_OTHER_MANA, SPEND_ONLY_MANA, - TARGET + TARGET; + + private final boolean needPlayCardAbility; // mark effect type as compatible with play/cast abilities + + AsThoughEffectType() { + this(false); + } + + AsThoughEffectType(boolean needPlayCardAbility) { + this.needPlayCardAbility = needPlayCardAbility; + } + + public boolean needPlayCardAbility() { + return needPlayCardAbility; + } }