diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/continuous/LoosingAbilitiesTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/LoosingAbilitiesTest.java
index 9516eff7144..a2dd3eb6761 100644
--- a/Mage.Tests/src/test/java/org/mage/test/cards/continuous/LoosingAbilitiesTest.java
+++ b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/LoosingAbilitiesTest.java
@@ -86,4 +86,79 @@ public class LoosingAbilitiesTest extends CardTestPlayerBase {
assertPermanentCount(playerB, "Gravecrawler", 1);
}
+
+ /**
+ * Yixlid Jailer works incorrectly with reanimation spelss - I cast Unearth
+ * targeting Seasoned Pyromancer with a Yixlid Jailer in play, but didnt get
+ * the Pyromancer's ETB trigger. This is a bug as Jailer only affaects cards
+ * when they are on the battle field
+ */
+ @Test
+ public void testYixlidJailerAndETBEffects() {
+ // Cards in graveyards lose all abilities.
+ addCard(Zone.HAND, playerA, "Yixlid Jailer"); // Creature 2/1 - {1}{B}
+ addCard(Zone.BATTLEFIELD, playerA, "Swamp", 2);
+
+ // When Seasoned Pyromancer enters the battlefield, discard two cards, then draw two cards. For each nonland card discarded this way, create a 1/1 red Elemental creature token.
+ // {3}{R}{R}, Exile Seasoned Pyromancer from your graveyard: Create two 1/1 red Elemental creature tokens.
+ addCard(Zone.GRAVEYARD, playerB, "Seasoned Pyromancer");
+ addCard(Zone.HAND, playerB, "Lightning Bolt", 2);
+ // Return target creature card with converted mana cost 3 or less from your graveyard to the battlefield.
+ // Cycling {2}
+ addCard(Zone.HAND, playerB, "Unearth", 1); // Sorcery {B}
+ addCard(Zone.BATTLEFIELD, playerB, "Swamp", 1);
+
+ castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Yixlid Jailer");
+
+ castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Unearth");
+ setChoice(playerB, "Lightning Bolt^Lightning Bolt");
+
+ setStopAt(2, PhaseStep.BEGIN_COMBAT);
+ execute();
+
+ assertPermanentCount(playerA, "Yixlid Jailer", 1);
+
+ assertPermanentCount(playerB, "Seasoned Pyromancer", 1);
+
+ assertGraveyardCount(playerB, "Unearth", 1);
+ assertGraveyardCount(playerB, "Lightning Bolt", 2);
+
+ }
+
+ /**
+ * If an ability triggers when the object that has it is put into a hidden
+ * zone from a graveyard, that ability triggers from the graveyard, (such as
+ * Golgari Brownscale), Yixlid Jailer will prevent that ability from
+ * triggering. (2007-05-01)
+ */
+ @Test
+ public void testYixlidJailerAndPutIntoHandEffect() {
+ // Cards in graveyards lose all abilities.
+ addCard(Zone.HAND, playerA, "Yixlid Jailer"); // Creature 2/1 - {1}{B}
+ addCard(Zone.BATTLEFIELD, playerA, "Swamp", 2);
+
+ // When Golgari Brownscale is put into your hand from your graveyard, you gain 2 life.
+ // Dredge 2 (If you would draw a card, instead you may put exactly X cards from the top of
+ // your library into your graveyard. If you do, return this card from your
+ // graveyard to your hand. Otherwise, draw a card. )
+ addCard(Zone.GRAVEYARD, playerB, "Golgari Brownscale", 1); // Sorcery {B}
+ // Return target creature card from your graveyard to your hand. If it’s a Zombie card, draw a card.
+ addCard(Zone.HAND, playerB, "Cemetery Recruitment", 1); // Sorcery {1}{B}
+ addCard(Zone.BATTLEFIELD, playerB, "Swamp", 2);
+
+ castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Yixlid Jailer");
+
+ castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Cemetery Recruitment");
+
+ setStopAt(2, PhaseStep.BEGIN_COMBAT);
+ execute();
+
+ assertPermanentCount(playerA, "Yixlid Jailer", 1);
+ assertHandCount(playerB, "Golgari Brownscale", 1);
+ assertGraveyardCount(playerB, "Cemetery Recruitment", 1);
+
+ assertLife(playerB, 20); // The trigger of Golgari Brownscale does not work because of Yixlid Jailer
+
+ }
+
}
diff --git a/Mage/src/main/java/mage/abilities/TriggeredAbilityImpl.java b/Mage/src/main/java/mage/abilities/TriggeredAbilityImpl.java
index 86e8b629eb1..2c35128574d 100644
--- a/Mage/src/main/java/mage/abilities/TriggeredAbilityImpl.java
+++ b/Mage/src/main/java/mage/abilities/TriggeredAbilityImpl.java
@@ -1,5 +1,7 @@
package mage.abilities;
+import java.util.Locale;
+import java.util.UUID;
import mage.MageObject;
import mage.abilities.effects.Effect;
import mage.constants.AbilityType;
@@ -10,9 +12,7 @@ import mage.game.events.GameEvent;
import mage.game.events.GameEvent.EventType;
import mage.game.events.ZoneChangeEvent;
import mage.players.Player;
-
-import java.util.Locale;
-import java.util.UUID;
+import mage.util.CardUtil;
/**
* @author BetaSteward_at_googlemail.com
@@ -158,6 +158,17 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge
if (event != null && event.getTargetId() != null && event.getTargetId().equals(getSourceId())) {
switch (event.getType()) {
case ZONE_CHANGE:
+ ZoneChangeEvent zce = (ZoneChangeEvent) event;
+ if (event.getTargetId().equals(getSourceId()) && !zce.getToZone().isPublicZone()) {
+ // If an ability triggers when the object that has it is put into a hidden zone from a graveyard,
+ // that ability triggers from the graveyard, (such as Golgari Brownscale),
+ // Yixlid Jailer will prevent that ability from triggering.
+ if (zce.getFromZone().match(Zone.GRAVEYARD)) {
+ if (!CardUtil.cardHadAbility(this, game.getLastKnownInformationCard(getSourceId(), zce.getFromZone()), getSourceId(), game)) {
+ return false;
+ }
+ }
+ }
case DESTROYED_PERMANENT:
if (isLeavesTheBattlefieldTrigger()) {
if (event.getType() == EventType.DESTROYED_PERMANENT) {
diff --git a/Mage/src/main/java/mage/abilities/common/ZoneChangeTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/ZoneChangeTriggeredAbility.java
index c9203e24c39..cf65b39bf79 100644
--- a/Mage/src/main/java/mage/abilities/common/ZoneChangeTriggeredAbility.java
+++ b/Mage/src/main/java/mage/abilities/common/ZoneChangeTriggeredAbility.java
@@ -1,4 +1,3 @@
-
package mage.abilities.common;
import mage.abilities.TriggeredAbilityImpl;
diff --git a/Mage/src/main/java/mage/abilities/keyword/DredgeAbility.java b/Mage/src/main/java/mage/abilities/keyword/DredgeAbility.java
index 9332fae2270..c2f2b79f2d8 100644
--- a/Mage/src/main/java/mage/abilities/keyword/DredgeAbility.java
+++ b/Mage/src/main/java/mage/abilities/keyword/DredgeAbility.java
@@ -44,7 +44,9 @@ class DredgeEffect extends ReplacementEffectImpl {
public DredgeEffect(int value) {
super(Duration.WhileInGraveyard, Outcome.AIDontUseIt);
this.amount = value;
- this.staticText = new StringBuilder("Dredge ").append(Integer.toString(value)).append(" (If you would draw a card, instead you may put exactly ").append(value).append(" card(s) from the top of your library into your graveyard. If you do, return this card from your graveyard to your hand. Otherwise, draw a card.)").toString();
+ this.staticText = ("Dredge ") + Integer.toString(value) + " (If you would draw a card, instead you may put exactly "
+ + value + " card(s) from the top of your library into your graveyard. If you do, return this card from "
+ + "your graveyard to your hand. Otherwise, draw a card.)";
}
public DredgeEffect(final DredgeEffect effect) {
diff --git a/Mage/src/main/java/mage/game/Game.java b/Mage/src/main/java/mage/game/Game.java
index 2da26d73c92..a38e1f8c111 100644
--- a/Mage/src/main/java/mage/game/Game.java
+++ b/Mage/src/main/java/mage/game/Game.java
@@ -1,5 +1,8 @@
package mage.game;
+import java.io.Serializable;
+import java.util.*;
+import java.util.stream.Collectors;
import mage.MageItem;
import mage.MageObject;
import mage.abilities.Ability;
@@ -41,10 +44,6 @@ import mage.players.Players;
import mage.util.MessageToClient;
import mage.util.functions.ApplyToPermanent;
-import java.io.Serializable;
-import java.util.*;
-import java.util.stream.Collectors;
-
public interface Game extends MageItem, Serializable {
MatchType getGameType();
@@ -208,6 +207,8 @@ public interface Game extends MageItem, Serializable {
MageObject getLastKnownInformation(UUID objectId, Zone zone);
+ CardState getLastKnownInformationCard(UUID objectId, Zone zone);
+
MageObject getLastKnownInformation(UUID objectId, Zone zone, int zoneChangeCounter);
boolean getShortLivingLKI(UUID objectId, Zone zone);
@@ -298,9 +299,9 @@ public interface Game extends MageItem, Serializable {
/**
* Creates and fires an damage prevention event
*
- * @param damageEvent damage event that will be replaced (instanceof check
- * will be done)
- * @param source ability that's the source of the prevention effect
+ * @param damageEvent damage event that will be replaced (instanceof check
+ * will be done)
+ * @param source ability that's the source of the prevention effect
* @param game
* @param amountToPrevent max preventable amount
* @return true prevention was successfull / false prevention was replaced
@@ -310,12 +311,12 @@ public interface Game extends MageItem, Serializable {
/**
* Creates and fires an damage prevention event
*
- * @param event damage event that will be replaced (instanceof check will be
- * done)
- * @param source ability that's the source of the prevention effect
+ * @param event damage event that will be replaced (instanceof check will be
+ * done)
+ * @param source ability that's the source of the prevention effect
* @param game
* @param preventAllDamage true if there is no limit to the damage that can
- * be prevented
+ * be prevented
* @return true prevention was successfull / false prevention was replaced
*/
PreventionEffectData preventDamage(GameEvent event, Ability source, Game game, boolean preventAllDamage);
diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java
index 2bcadb7f8c9..81fc779655b 100644
--- a/Mage/src/main/java/mage/game/GameImpl.java
+++ b/Mage/src/main/java/mage/game/GameImpl.java
@@ -90,6 +90,7 @@ public abstract class GameImpl implements Game, Serializable {
protected Map meldCards = new HashMap<>(0);
protected Map> lki = new EnumMap<>(Zone.class);
+ protected Map> lkiCardState = new EnumMap<>(Zone.class);
protected Map> lkiExtended = new HashMap<>();
// Used to check if an object was moved by the current effect in resolution (so Wrath like effect can be handled correctly)
protected Map> shortLivingLKI = new EnumMap<>(Zone.class);
@@ -168,6 +169,7 @@ public abstract class GameImpl implements Game, Serializable {
this.gameOptions = game.gameOptions;
this.lki.putAll(game.lki);
this.lkiExtended.putAll(game.lkiExtended);
+ this.lkiCardState.putAll(game.lkiCardState);
this.shortLivingLKI.putAll(game.shortLivingLKI);
this.permanentsEntering.putAll(game.permanentsEntering);
@@ -2773,6 +2775,20 @@ public abstract class GameImpl implements Game, Serializable {
return getLastKnownInformation(objectId, zone);
}
+ @Override
+ public CardState getLastKnownInformationCard(UUID objectId, Zone zone) {
+ if (zone == Zone.GRAVEYARD) {
+ Map lkiCardStateMap = lkiCardState.get(zone);
+ if (lkiCardStateMap != null) {
+ CardState cardState = lkiCardStateMap.get(objectId);
+ if (cardState != null) {
+ return cardState;
+ }
+ }
+ }
+ return null;
+ }
+
@Override
public boolean getShortLivingLKI(UUID objectId, Zone zone) {
Set idSet = shortLivingLKI.get(zone);
@@ -2817,16 +2833,28 @@ public abstract class GameImpl implements Game, Serializable {
lkiExtended.put(objectId, lkiExtendedMap);
}
}
+ } else if (Zone.GRAVEYARD.equals(zone)) {
+ // Remember card state in this public zone (mainly removed/gained abilities)
+ Map lkiMap = lkiCardState.get(zone);
+ if (lkiMap != null) {
+ lkiMap.put(objectId, getState().getCardState(objectId));
+ } else {
+ HashMap newMap = new HashMap<>();
+ newMap.put(objectId, getState().getCardState(objectId).copy());
+ lkiCardState.put(zone, newMap);
+ }
}
}
/**
- * Reset objects stored for Last Known Information.
+ * Reset objects stored for Last Known Information. (Happens if all effects
+ * are applied und stack is empty)
*/
@Override
public void resetLKI() {
lki.clear();
lkiExtended.clear();
+ lkiCardState.clear();
infiniteLoopCounter = 0;
stackObjectsCheck.clear();
}
diff --git a/Mage/src/main/java/mage/util/CardUtil.java b/Mage/src/main/java/mage/util/CardUtil.java
index 81ea859d036..c653496503e 100644
--- a/Mage/src/main/java/mage/util/CardUtil.java
+++ b/Mage/src/main/java/mage/util/CardUtil.java
@@ -1,5 +1,11 @@
package mage.util;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.text.SimpleDateFormat;
+import java.util.Objects;
+import java.util.UUID;
import mage.MageObject;
import mage.Mana;
import mage.abilities.Ability;
@@ -9,18 +15,12 @@ import mage.abilities.costs.mana.*;
import mage.cards.Card;
import mage.constants.EmptyNames;
import mage.filter.Filter;
+import mage.game.CardState;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.game.permanent.token.Token;
import mage.util.functions.CopyTokenFunction;
-import java.io.UnsupportedEncodingException;
-import java.net.URLDecoder;
-import java.net.URLEncoder;
-import java.text.SimpleDateFormat;
-import java.util.Objects;
-import java.util.UUID;
-
/**
* @author nantuko
*/
@@ -29,10 +29,10 @@ public final class CardUtil {
private static final String SOURCE_EXILE_ZONE_TEXT = "SourceExileZone";
static final String[] numberStrings = {"zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine",
- "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen", "twenty"};
+ "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen", "twenty"};
static final String[] ordinalStrings = {"first", "second", "third", "fourth", "fifth", "sixth", "seventh", "eightth", "ninth",
- "tenth", "eleventh", "twelfth", "thirteenth", "fourteenth", "fifteenth", "sixteenth", "seventeenth", "eighteenth", "nineteenth", "twentieth"};
+ "tenth", "eleventh", "twelfth", "thirteenth", "fourteenth", "fifteenth", "sixteenth", "seventeenth", "eighteenth", "nineteenth", "twentieth"};
public static final SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS");
@@ -147,8 +147,8 @@ public final class CardUtil {
*
* @param spellAbility
* @param manaCostsToReduce costs to reduce
- * @param convertToGeneric colored mana does reduce generic mana if no
- * appropriate colored mana is in the costs included
+ * @param convertToGeneric colored mana does reduce generic mana if no
+ * appropriate colored mana is in the costs included
*/
public static void adjustCost(SpellAbility spellAbility, ManaCosts manaCostsToReduce, boolean convertToGeneric) {
ManaCosts previousCost = spellAbility.getManaCostsToPay();
@@ -333,7 +333,7 @@ public final class CardUtil {
*
* @param number number to convert to text
* @param forOne if the number is 1, this string will be returnedinstead of
- * "one".
+ * "one".
* @return
*/
public static String numberToText(int number, String forOne) {
@@ -418,7 +418,7 @@ public final class CardUtil {
/**
* Creates and saves a (card + zoneChangeCounter) specific exileId.
*
- * @param game the current game
+ * @param game the current game
* @param source source ability
* @return the specific UUID
*/
@@ -453,9 +453,9 @@ public final class CardUtil {
* be specific to a permanent instance. So they won't match, if a permanent
* was e.g. exiled and came back immediately.
*
- * @param text short value to describe the value
+ * @param text short value to describe the value
* @param cardId id of the card
- * @param game the game
+ * @param game the game
* @return
*/
public static String getCardZoneString(String text, UUID cardId, Game game) {
@@ -605,4 +605,29 @@ public final class CardUtil {
return "";
}
}
+
+ /**
+ * Checks if a card had a given ability depending their historic cardState
+ *
+ * @param ability the ability that is checked
+ * @param cardState the historic cardState (from LKI)
+ * @param cardId the id of the card
+ * @param game
+ * @return
+ */
+ public static boolean cardHadAbility(Ability ability, CardState cardState, UUID cardId, Game game) {
+ Card card = game.getCard(cardId);
+ if (card != null) {
+ if (cardState != null) {
+ if (cardState.getAbilities().contains(ability)) { // Check other abilities (possibly given after lost of abilities)
+ return true;
+ }
+ if (cardState.hasLostAllAbilities()) {
+ return false; // Not allowed to check abilities of original card
+ }
+ }
+ return card.getAbilities().contains(ability); // check if the original card has the ability
+ }
+ return false;
+ }
}