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

This commit is contained in:
LevelX2 2020-01-04 23:53:47 +01:00
parent 9110f70e2d
commit ebdba3c57e
7 changed files with 173 additions and 32 deletions

View file

@ -86,4 +86,79 @@ public class LoosingAbilitiesTest extends CardTestPlayerBase {
assertPermanentCount(playerB, "Gravecrawler", 1); 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 its 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
}
} }

View file

@ -1,5 +1,7 @@
package mage.abilities; package mage.abilities;
import java.util.Locale;
import java.util.UUID;
import mage.MageObject; import mage.MageObject;
import mage.abilities.effects.Effect; import mage.abilities.effects.Effect;
import mage.constants.AbilityType; import mage.constants.AbilityType;
@ -10,9 +12,7 @@ import mage.game.events.GameEvent;
import mage.game.events.GameEvent.EventType; import mage.game.events.GameEvent.EventType;
import mage.game.events.ZoneChangeEvent; import mage.game.events.ZoneChangeEvent;
import mage.players.Player; import mage.players.Player;
import mage.util.CardUtil;
import java.util.Locale;
import java.util.UUID;
/** /**
* @author BetaSteward_at_googlemail.com * @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())) { if (event != null && event.getTargetId() != null && event.getTargetId().equals(getSourceId())) {
switch (event.getType()) { switch (event.getType()) {
case ZONE_CHANGE: 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: case DESTROYED_PERMANENT:
if (isLeavesTheBattlefieldTrigger()) { if (isLeavesTheBattlefieldTrigger()) {
if (event.getType() == EventType.DESTROYED_PERMANENT) { if (event.getType() == EventType.DESTROYED_PERMANENT) {

View file

@ -1,4 +1,3 @@
package mage.abilities.common; package mage.abilities.common;
import mage.abilities.TriggeredAbilityImpl; import mage.abilities.TriggeredAbilityImpl;

View file

@ -44,7 +44,9 @@ class DredgeEffect extends ReplacementEffectImpl {
public DredgeEffect(int value) { public DredgeEffect(int value) {
super(Duration.WhileInGraveyard, Outcome.AIDontUseIt); super(Duration.WhileInGraveyard, Outcome.AIDontUseIt);
this.amount = value; this.amount = value;
this.staticText = new StringBuilder("Dredge ").append(Integer.toString(value)).append(" <i>(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.)</i>").toString(); this.staticText = ("Dredge ") + Integer.toString(value) + " <i>(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.)</i>";
} }
public DredgeEffect(final DredgeEffect effect) { public DredgeEffect(final DredgeEffect effect) {

View file

@ -1,5 +1,8 @@
package mage.game; package mage.game;
import java.io.Serializable;
import java.util.*;
import java.util.stream.Collectors;
import mage.MageItem; import mage.MageItem;
import mage.MageObject; import mage.MageObject;
import mage.abilities.Ability; import mage.abilities.Ability;
@ -41,10 +44,6 @@ import mage.players.Players;
import mage.util.MessageToClient; import mage.util.MessageToClient;
import mage.util.functions.ApplyToPermanent; import mage.util.functions.ApplyToPermanent;
import java.io.Serializable;
import java.util.*;
import java.util.stream.Collectors;
public interface Game extends MageItem, Serializable { public interface Game extends MageItem, Serializable {
MatchType getGameType(); MatchType getGameType();
@ -208,6 +207,8 @@ public interface Game extends MageItem, Serializable {
MageObject getLastKnownInformation(UUID objectId, Zone zone); MageObject getLastKnownInformation(UUID objectId, Zone zone);
CardState getLastKnownInformationCard(UUID objectId, Zone zone);
MageObject getLastKnownInformation(UUID objectId, Zone zone, int zoneChangeCounter); MageObject getLastKnownInformation(UUID objectId, Zone zone, int zoneChangeCounter);
boolean getShortLivingLKI(UUID objectId, Zone zone); boolean getShortLivingLKI(UUID objectId, Zone zone);

View file

@ -90,6 +90,7 @@ public abstract class GameImpl implements Game, Serializable {
protected Map<UUID, MeldCard> meldCards = new HashMap<>(0); protected Map<UUID, MeldCard> meldCards = new HashMap<>(0);
protected Map<Zone, HashMap<UUID, MageObject>> lki = new EnumMap<>(Zone.class); protected Map<Zone, HashMap<UUID, MageObject>> lki = new EnumMap<>(Zone.class);
protected Map<Zone, HashMap<UUID, CardState>> lkiCardState = new EnumMap<>(Zone.class);
protected Map<UUID, Map<Integer, MageObject>> lkiExtended = new HashMap<>(); protected Map<UUID, Map<Integer, MageObject>> 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) // Used to check if an object was moved by the current effect in resolution (so Wrath like effect can be handled correctly)
protected Map<Zone, Set<UUID>> shortLivingLKI = new EnumMap<>(Zone.class); protected Map<Zone, Set<UUID>> shortLivingLKI = new EnumMap<>(Zone.class);
@ -168,6 +169,7 @@ public abstract class GameImpl implements Game, Serializable {
this.gameOptions = game.gameOptions; this.gameOptions = game.gameOptions;
this.lki.putAll(game.lki); this.lki.putAll(game.lki);
this.lkiExtended.putAll(game.lkiExtended); this.lkiExtended.putAll(game.lkiExtended);
this.lkiCardState.putAll(game.lkiCardState);
this.shortLivingLKI.putAll(game.shortLivingLKI); this.shortLivingLKI.putAll(game.shortLivingLKI);
this.permanentsEntering.putAll(game.permanentsEntering); this.permanentsEntering.putAll(game.permanentsEntering);
@ -2773,6 +2775,20 @@ public abstract class GameImpl implements Game, Serializable {
return getLastKnownInformation(objectId, zone); return getLastKnownInformation(objectId, zone);
} }
@Override
public CardState getLastKnownInformationCard(UUID objectId, Zone zone) {
if (zone == Zone.GRAVEYARD) {
Map<UUID, CardState> lkiCardStateMap = lkiCardState.get(zone);
if (lkiCardStateMap != null) {
CardState cardState = lkiCardStateMap.get(objectId);
if (cardState != null) {
return cardState;
}
}
}
return null;
}
@Override @Override
public boolean getShortLivingLKI(UUID objectId, Zone zone) { public boolean getShortLivingLKI(UUID objectId, Zone zone) {
Set<UUID> idSet = shortLivingLKI.get(zone); Set<UUID> idSet = shortLivingLKI.get(zone);
@ -2817,16 +2833,28 @@ public abstract class GameImpl implements Game, Serializable {
lkiExtended.put(objectId, lkiExtendedMap); lkiExtended.put(objectId, lkiExtendedMap);
} }
} }
} else if (Zone.GRAVEYARD.equals(zone)) {
// Remember card state in this public zone (mainly removed/gained abilities)
Map<UUID, CardState> lkiMap = lkiCardState.get(zone);
if (lkiMap != null) {
lkiMap.put(objectId, getState().getCardState(objectId));
} else {
HashMap<UUID, CardState> 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 @Override
public void resetLKI() { public void resetLKI() {
lki.clear(); lki.clear();
lkiExtended.clear(); lkiExtended.clear();
lkiCardState.clear();
infiniteLoopCounter = 0; infiniteLoopCounter = 0;
stackObjectsCheck.clear(); stackObjectsCheck.clear();
} }

View file

@ -1,5 +1,11 @@
package mage.util; 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.MageObject;
import mage.Mana; import mage.Mana;
import mage.abilities.Ability; import mage.abilities.Ability;
@ -9,18 +15,12 @@ import mage.abilities.costs.mana.*;
import mage.cards.Card; import mage.cards.Card;
import mage.constants.EmptyNames; import mage.constants.EmptyNames;
import mage.filter.Filter; import mage.filter.Filter;
import mage.game.CardState;
import mage.game.Game; import mage.game.Game;
import mage.game.permanent.Permanent; import mage.game.permanent.Permanent;
import mage.game.permanent.token.Token; import mage.game.permanent.token.Token;
import mage.util.functions.CopyTokenFunction; 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 * @author nantuko
*/ */
@ -605,4 +605,29 @@ public final class CardUtil {
return ""; 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;
}
} }