From ebdba3c57e65c6d03f3cbda383701cefe804860b Mon Sep 17 00:00:00 2001 From: LevelX2 Date: Sat, 4 Jan 2020 23:53:47 +0100 Subject: [PATCH] * Added logic to check if a card had a triggered ability in the graveyard if it was moved from graveyard to a hidden zone. Because if not, the ability does not trigger. --- .../continuous/LoosingAbilitiesTest.java | 75 +++++++++++++++++++ .../mage/abilities/TriggeredAbilityImpl.java | 17 ++++- .../common/ZoneChangeTriggeredAbility.java | 1 - .../mage/abilities/keyword/DredgeAbility.java | 4 +- Mage/src/main/java/mage/game/Game.java | 23 +++--- Mage/src/main/java/mage/game/GameImpl.java | 30 +++++++- Mage/src/main/java/mage/util/CardUtil.java | 55 ++++++++++---- 7 files changed, 173 insertions(+), 32 deletions(-) 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; + } }