dies triggers improves:

* tests: added additional tests and verify/runtime checks for wrong die trigger settings;
* refactor: removed some usage of short LKI ;
* fixed dies events support in "or trigger" and "conditional trigger" (use cases like sacrifice cost);
* fixed dies events support in shared triggered abilities (use cases like sacrifice cost);
This commit is contained in:
Oleg Agafonov 2024-11-04 23:55:14 +04:00
parent a2ed52b8de
commit 66b338c6fc
18 changed files with 233 additions and 63 deletions

View file

@ -668,4 +668,66 @@ public class PhantasmalImageTest extends CardTestPlayerBase {
assertTrue("Cloak and Dagger should be a Rogue", cloakB.hasSubtype(SubType.ROGUE, currentGame)); assertTrue("Cloak and Dagger should be a Rogue", cloakB.hasSubtype(SubType.ROGUE, currentGame));
assertTrue("Cloak and Dagger should be an Equipment", cloakB.hasSubtype(SubType.EQUIPMENT, currentGame)); assertTrue("Cloak and Dagger should be an Equipment", cloakB.hasSubtype(SubType.EQUIPMENT, currentGame));
} }
@Test
public void test_SelfExploit_SidisiUndeadVizier_Normal() {
// bug https://github.com/magefree/mage/issues/5925
// You may have Phantasmal Image enter the battlefield as a copy of any creature on the battlefield,
// except it's an Illusion in addition to its other types and it has "When this creature becomes the
// target of a spell or ability, sacrifice it."
addCard(Zone.HAND, playerA, "Phantasmal Image"); // {1}{U}
addCard(Zone.BATTLEFIELD, playerA, "Island", 2);
//
// Exploit (When this creature enters the battlefield, you may sacrifice a creature.)
// When Sidisi, Undead Vizier exploits a creature, you may search your library for a card, put it into your hand, then shuffle your library.
addCard(Zone.BATTLEFIELD, playerA, "Sidisi, Undead Vizier");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Phantasmal Image");
setChoice(playerA, true); // use copy
setChoice(playerA, "Sidisi, Undead Vizier"); // to copy
setChoice(playerA, "Sidisi, Undead Vizier[only copy]"); // legendary rule, keep copy
setChoice(playerA, true); // use exploit on etb
setChoice(playerA, "Sidisi, Undead Vizier[only copy]"); // sacrifice itself on exploit
setChoice(playerA, true); // use exploit trigger (search lib)
addTarget(playerA, "Mountain"); // tutor mountain
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertHandCount(playerA, "Mountain", 1);
}
@Test
public void test_SelfExploit_SidisiUndeadVizier_Exile() {
// exploit look for sacrifice only, not a dies conditional - so it must work with exile replace
// You may have Phantasmal Image enter the battlefield as a copy of any creature on the battlefield,
// except it's an Illusion in addition to its other types and it has "When this creature becomes the
// target of a spell or ability, sacrifice it."
addCard(Zone.HAND, playerA, "Phantasmal Image"); // {1}{U}
addCard(Zone.BATTLEFIELD, playerA, "Island", 2);
//
// Exploit (When this creature enters the battlefield, you may sacrifice a creature.)
// When Sidisi, Undead Vizier exploits a creature, you may search your library for a card, put it into your hand, then shuffle your library.
addCard(Zone.BATTLEFIELD, playerA, "Sidisi, Undead Vizier");
//
// If a card or token would be put into a graveyard from anywhere, exile it instead.
addCard(Zone.BATTLEFIELD, playerA, "Rest in Peace");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Phantasmal Image");
setChoice(playerA, true); // use copy
setChoice(playerA, "Sidisi, Undead Vizier"); // to copy
setChoice(playerA, "Sidisi, Undead Vizier[only copy]"); // legendary rule, keep copy
setChoice(playerA, true); // use exploit on etb
setChoice(playerA, "Sidisi, Undead Vizier[only copy]"); // sacrifice itself on exploit
setChoice(playerA, true); // use exploit trigger (search lib)
addTarget(playerA, "Mountain"); // tutor mountain
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertHandCount(playerA, "Mountain", 1);
}
} }

View file

@ -116,4 +116,25 @@ public class VerrakWarpedSengirTest extends CardTestPlayerBase {
assertLife(playerA, 20 - 2 * 2 + 2 * 2); // x2 pays, x2 gains assertLife(playerA, 20 - 2 * 2 + 2 * 2); // x2 pays, x2 gains
assertLife(playerB, 20 - 2 * 2); // x2 lose assertLife(playerB, 20 - 2 * 2); // x2 lose
} }
@Test
public void test_MustNotTriggerOnDiscardCost() {
// bug: https://github.com/magefree/mage/issues/12089
// Whenever you activate an ability that isnt a mana ability, if life was paid to activate it,
// you may pay that much life again. If you do, copy that ability. You may choose new targets for the copy.
addCard(Zone.BATTLEFIELD, playerA, "Verrak, Warped Sengir");
//
// Pay 2 life, Sacrifice another creature: Search your library for a card, put that card into your hand, then shuffle.
addCard(Zone.BATTLEFIELD, playerA, "Razaketh, the Foulblooded");
// activate without copy trigger (discard cost pay will remove Verrak before activate the ability)
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Pay 2 life, Sacrifice");
setChoice(playerA, "Verrak, Warped Sengir"); // sacrifice cost
addTarget(playerA, "Mountain"); // search lib
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
setStrictChooseMode(true);
execute();
}
} }

View file

@ -23,7 +23,7 @@ public class ChronozoaTest extends CardTestPlayerBase {
addCard(Zone.BATTLEFIELD, playerA, "Island", 4); addCard(Zone.BATTLEFIELD, playerA, "Island", 4);
// Flying // Flying
// Vanishing 3 (This permanent enters the battlefield with three time counters on it. At the beginning of your upkeep, remove a time counter from it. When the last is removed, sacrifice it.) // Vanishing 3 (This permanent enters the battlefield with three time counters on it. At the beginning of your upkeep, remove a time counter from it. When the last is removed, sacrifice it.)
// When Chronozoa is put into a graveyard from play, if it had no time counters on it, create two tokens that are copies of it. // When Chronozoa dies, if it had no time counters on it, create two tokens that are copies of it.
addCard(Zone.HAND, playerA, "Chronozoa"); // {3}{U} addCard(Zone.HAND, playerA, "Chronozoa"); // {3}{U}
addCard(Zone.GRAVEYARD, playerA, "Chronozoa"); addCard(Zone.GRAVEYARD, playerA, "Chronozoa");
// Sacrifice a creature: Scry 1. (To scry 1, look at the top card of your library, then you may put that card on the bottom of your library.) // Sacrifice a creature: Scry 1. (To scry 1, look at the top card of your library, then you may put that card on the bottom of your library.)

View file

@ -7,6 +7,7 @@ import mage.ObjectColor;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.AbilityImpl; import mage.abilities.AbilityImpl;
import mage.abilities.Mode; import mage.abilities.Mode;
import mage.abilities.TriggeredAbility;
import mage.abilities.common.*; import mage.abilities.common.*;
import mage.abilities.condition.Condition; import mage.abilities.condition.Condition;
import mage.abilities.costs.Cost; import mage.abilities.costs.Cost;
@ -1979,10 +1980,23 @@ public class VerifyCardDataTest {
fail(card, "abilities", "legendary nonpermanent cards need to have LegendarySpellAbility"); fail(card, "abilities", "legendary nonpermanent cards need to have LegendarySpellAbility");
} }
// special check: mutate is not supported yet, so must be removed from sets
if (card.getAbilities().containsClass(MutateAbility.class)) { if (card.getAbilities().containsClass(MutateAbility.class)) {
fail(card, "abilities", "mutate cards aren't implemented and shouldn't be available"); fail(card, "abilities", "mutate cards aren't implemented and shouldn't be available");
} }
// special check: wrong dies triggers
card.getAbilities().stream()
.filter(a -> a instanceof TriggeredAbility)
.map(a -> (TriggeredAbility) a)
.filter(a -> a.getRule().contains("whenever") || a.getRule().contains("Whenever"))
.filter(a -> a.getRule().contains("dies"))
.filter(a -> !a.getRule().contains("with \"When")) // ignore token creating effects
.filter(a -> !a.isLeavesTheBattlefieldTrigger())
.forEach(a -> {
fail(card, "abilities", "dies trigger must use setLeavesTheBattlefieldTrigger(true) and override isInUseableZone - " + a.getClass().getSimpleName());
});
// special check: duplicated words in ability text (wrong target/filter usage) // special check: duplicated words in ability text (wrong target/filter usage)
// example: You may exile __two two__ blue cards // example: You may exile __two two__ blue cards
// possible fixes: // possible fixes:

View file

@ -351,7 +351,12 @@ public interface Ability extends Controllable, Serializable {
void addWatcher(Watcher watcher); void addWatcher(Watcher watcher);
/** /**
* Returns true if this abilities source is in the zone for the ability * Allow to control ability/trigger's lifecycle
* <p>
* How-to use:
* - for normal abilities and triggers - keep default
* - for leave battlefield triggers - keep default + set setLeavesTheBattlefieldTrigger(true)
* - for dies triggers - override and use TriggeredAbilityImpl.isInUseableZoneDiesTrigger inside + set setLeavesTheBattlefieldTrigger(true)
*/ */
boolean isInUseableZone(Game game, MageObject source, GameEvent event); boolean isInUseableZone(Game game, MageObject source, GameEvent event);

View file

@ -29,7 +29,9 @@ import mage.game.Game;
import mage.game.command.Dungeon; import mage.game.command.Dungeon;
import mage.game.command.Emblem; import mage.game.command.Emblem;
import mage.game.command.Plane; import mage.game.command.Plane;
import mage.game.events.BatchEvent;
import mage.game.events.GameEvent; import mage.game.events.GameEvent;
import mage.game.events.ZoneChangeEvent;
import mage.game.permanent.Permanent; import mage.game.permanent.Permanent;
import mage.game.stack.Spell; import mage.game.stack.Spell;
import mage.game.stack.StackAbility; import mage.game.stack.StackAbility;
@ -1172,11 +1174,6 @@ public abstract class AbilityImpl implements Ability {
return false; return false;
} }
/**
* @param game
* @param source
* @return
*/
@Override @Override
public boolean isInUseableZone(Game game, MageObject source, GameEvent event) { public boolean isInUseableZone(Game game, MageObject source, GameEvent event) {
if (!this.hasSourceObjectAbility(game, source, event)) { if (!this.hasSourceObjectAbility(game, source, event)) {
@ -1201,13 +1198,74 @@ public abstract class AbilityImpl implements Ability {
} else { } else {
parameterSourceId = getSourceId(); parameterSourceId = getSourceId();
} }
// old code:
// TODO: delete after dies fix
// check against shortLKI for effects that move multiple object at the same time (e.g. destroy all) // check against shortLKI for effects that move multiple object at the same time (e.g. destroy all)
if (game.checkShortLivingLKI(getSourceId(), getZone())) { if (game.checkShortLivingLKI(getSourceId(), getZone())) {
return true; //return true; // fix 1
} }
// check against current state
Zone test = game.getState().getZone(parameterSourceId);
return zone.match(test); // 603.10.
// Normally, objects that exist immediately after an event are checked to see if the event matched
// any trigger conditions, and continuous effects that exist at that time are used to determine what the
// trigger conditions are and what the objects involved in the event look like.
// ...
Zone lookingInZone = game.getState().getZone(parameterSourceId);
// 603.10.
// ...
// However, some triggered abilities are exceptions to this rule; the game looks back in time to determine
// if those abilities trigger, using the existence of those abilities and the appearance of objects
// immediately prior to the event. The list of exceptions is as follows:
// 603.10a
// Some zone-change triggers look back in time. These are leaves-the-battlefield abilities,
// abilities that trigger when a card leaves a graveyard, and abilities that trigger when an object that all
// players can see is put into a hand or library.
// TODO: research "leaves a graveyard"
// TODO: research "put into a hand or library"
if (source instanceof Permanent && isTriggerCanFireAfterLeaveBattlefield(event)) {
// support leaves-the-battlefield abilities
lookingInZone = Zone.BATTLEFIELD;
}
// TODO: research use cases and implement shared logic with "looking zone" instead LKI only
// 603.10b Abilities that trigger when a permanent phases out look back in time.
// 603.10c Abilities that trigger specifically when an object becomes unattached look back in time.
// 603.10d Abilities that trigger when a player loses control of an object look back in time.
// 603.10e Abilities that trigger when a spell is countered look back in time.
// 603.10f Abilities that trigger when a player loses the game look back in time.
// 603.10g Abilities that trigger when a player planeswalks away from a plane look back in time.
return zone.match(lookingInZone);
}
public static boolean isTriggerCanFireAfterLeaveBattlefield(GameEvent event) {
if (event == null) {
return false;
}
List<GameEvent> allEvents = new ArrayList<>();
if (event instanceof BatchEvent) {
allEvents.addAll(((BatchEvent) event).getEvents());
} else {
allEvents.add(event);
}
return allEvents.stream().anyMatch(e -> {
// TODO: add more events with zone change logic (or make it even't param)?
switch (e.getType()) {
case DESTROYED_PERMANENT:
case EXPLOITED_CREATURE:
return true;
case ZONE_CHANGE:
return ((ZoneChangeEvent) event).getFromZone() == Zone.BATTLEFIELD;
default:
return false;
}
});
} }
@Override @Override

View file

@ -27,7 +27,7 @@ public abstract class StaticAbility extends AbilityImpl {
@Override @Override
public boolean isInUseableZone(Game game, MageObject source, GameEvent event) { public boolean isInUseableZone(Game game, MageObject source, GameEvent event) {
if (game.checkShortLivingLKI(getSourceId(), zone)) { if (game.checkShortLivingLKI(getSourceId(), zone)) { // TODO: can be deleted? Need research
return true; // maybe this can be a problem if effects removed the ability from the object return true; // maybe this can be a problem if effects removed the ability from the object
} }
if (game.getPermanentEntering(getSourceId()) != null && zone == Zone.BATTLEFIELD) { if (game.getPermanentEntering(getSourceId()) != null && zone == Zone.BATTLEFIELD) {

View file

@ -62,8 +62,17 @@ public interface TriggeredAbility extends Ability {
TriggeredAbility setOptional(); TriggeredAbility setOptional();
/**
* Allow trigger to fire after source leave the battlefield (example: will use LKI on itself sacrifice)
*/
boolean isLeavesTheBattlefieldTrigger(); boolean isLeavesTheBattlefieldTrigger();
/**
* 603.6c,603.6d
* If true the game looks back in time to determine if those abilities trigger
* This has to be set, if the triggered ability has to check back in time if the permanent the ability is connected
* to had the ability on the battlefield while the trigger is checked
*/
void setLeavesTheBattlefieldTrigger(boolean leavesTheBattlefieldTrigger); void setLeavesTheBattlefieldTrigger(boolean leavesTheBattlefieldTrigger);
@Override @Override

View file

@ -49,7 +49,6 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge
// verify check: DoIfCostPaid effect already asks about action (optional), so no needs to ask it again in triggered ability // verify check: DoIfCostPaid effect already asks about action (optional), so no needs to ask it again in triggered ability
if (effect instanceof DoIfCostPaid && (this.optional && ((DoIfCostPaid) effect).isOptional())) { if (effect instanceof DoIfCostPaid && (this.optional && ((DoIfCostPaid) effect).isOptional())) {
throw new IllegalArgumentException("DoIfCostPaid effect must have only one optional settings, but it have two (trigger + DoIfCostPaid): " + this.getClass().getSimpleName()); throw new IllegalArgumentException("DoIfCostPaid effect must have only one optional settings, but it have two (trigger + DoIfCostPaid): " + this.getClass().getSimpleName());
} }
} }
@ -400,6 +399,7 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge
} }
break; break;
case DESTROYED_PERMANENT: case DESTROYED_PERMANENT:
case EXPLOITED_CREATURE:
if (isLeavesTheBattlefieldTrigger()) { if (isLeavesTheBattlefieldTrigger()) {
source = game.getLastKnownInformation(getSourceId(), Zone.BATTLEFIELD); source = game.getLastKnownInformation(getSourceId(), Zone.BATTLEFIELD);
} }
@ -408,20 +408,11 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge
return super.isInUseableZone(game, source, event); return super.isInUseableZone(game, source, event);
} }
/*
603.6c Leaves-the-battlefield abilities, 603.6d
if true the game looks back in time to determine if those abilities trigger,
using the existence of those abilities and the appearance of objects immediately prior to the event (603.10)
*/
@Override @Override
public boolean isLeavesTheBattlefieldTrigger() { public boolean isLeavesTheBattlefieldTrigger() {
return leavesTheBattlefieldTrigger; return leavesTheBattlefieldTrigger;
} }
/*
603.6c,603.6d
This has to be set, if the triggered ability has to check back in time if the permanent the ability is connected to had the ability on the battlefield while the trigger is checked
*/
@Override @Override
public final void setLeavesTheBattlefieldTrigger(boolean leavesTheBattlefieldTrigger) { public final void setLeavesTheBattlefieldTrigger(boolean leavesTheBattlefieldTrigger) {
this.leavesTheBattlefieldTrigger = leavesTheBattlefieldTrigger; this.leavesTheBattlefieldTrigger = leavesTheBattlefieldTrigger;
@ -453,12 +444,22 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge
} }
/** /**
* Looking object in GRAVEYARD zone only. If you need multi zone then use default isInUseableZone
* - good example: Whenever another creature you control dies
* - bad example: When {this} dies or is put into exile from the battlefield
* <p>
* For triggered abilities that function from the battlefield that must trigger when the source permanent dies * For triggered abilities that function from the battlefield that must trigger when the source permanent dies
* and/or for any other events that happen simultaneously to the source permanent dying. * and/or for any other events that happen simultaneously to the source permanent dying.
* (Similar logic must be used for any leaves-the-battlefield, but this method assumes to graveyard only.) * (Similar logic must be used for any leaves-the-battlefield, but this method assumes to graveyard only.)
* NOTE: If your ability functions from another zone (not battlefield) then must use standard logic, not this. * NOTE: If your ability functions from another zone (not battlefield) then must use standard logic, not this.
*/ */
public static boolean isInUseableZoneDiesTrigger(TriggeredAbility source, GameEvent event, Game game) { public static boolean isInUseableZoneDiesTrigger(TriggeredAbility source, GameEvent event, Game game) {
// runtime check: wrong trigger settings
if (!source.isLeavesTheBattlefieldTrigger()) {
// TODO: enable after fix
// throw new IllegalArgumentException("Wrong code usage: all dies triggers must use setLeavesTheBattlefieldTrigger(true)");
}
// Get the source permanent of the ability // Get the source permanent of the ability
MageObject sourceObject = null; MageObject sourceObject = null;
if (game.getState().getZone(source.getSourceId()) == Zone.BATTLEFIELD) { if (game.getState().getZone(source.getSourceId()) == Zone.BATTLEFIELD) {
@ -481,7 +482,7 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge
// --!---------------!-------------!-----!-----------! // --!---------------!-------------!-----!-----------!
// - // -
if (game.checkShortLivingLKI(source.getSourceId(), Zone.BATTLEFIELD)) { if (game.checkShortLivingLKI(source.getSourceId(), Zone.BATTLEFIELD)) {
sourceObject = (Permanent) game.getLastKnownInformation(source.getSourceId(), Zone.BATTLEFIELD); sourceObject = game.getLastKnownInformation(source.getSourceId(), Zone.BATTLEFIELD);
} }
} }
if (sourceObject == null) { // source is no permanent if (sourceObject == null) { // source is no permanent

View file

@ -48,6 +48,7 @@ public class DiesCreatureTriggeredAbility extends TriggeredAbilityImpl {
super(zone, effect, optional); super(zone, effect, optional);
this.filter = filter; this.filter = filter;
this.setTargetPointer = setTargetPointer; this.setTargetPointer = setTargetPointer;
setLeavesTheBattlefieldTrigger(true);
setTriggerPhrase("Whenever " + filter.getMessage() + (filter.getMessage().startsWith("one or more") ? " die, " : " dies, ")); setTriggerPhrase("Whenever " + filter.getMessage() + (filter.getMessage().startsWith("one or more") ? " die, " : " dies, "));
} }

View file

@ -30,6 +30,7 @@ public class DiesThisOrAnotherTriggeredAbility extends TriggeredAbilityImpl {
filterMessage = filterMessage.substring(2); filterMessage = filterMessage.substring(2);
} }
setTriggerPhrase("Whenever {this} or another " + filterMessage + " dies, "); setTriggerPhrase("Whenever {this} or another " + filterMessage + " dies, ");
setLeavesTheBattlefieldTrigger(true);
} }
protected DiesThisOrAnotherTriggeredAbility(final DiesThisOrAnotherTriggeredAbility ability) { protected DiesThisOrAnotherTriggeredAbility(final DiesThisOrAnotherTriggeredAbility ability) {

View file

@ -48,22 +48,6 @@ public class ExploitCreatureTriggeredAbility extends TriggeredAbilityImpl {
return event.getType() == GameEvent.EventType.EXPLOITED_CREATURE; return event.getType() == GameEvent.EventType.EXPLOITED_CREATURE;
} }
@Override
public boolean isInUseableZone(Game game, MageObject source, GameEvent event) {
Permanent sourcePermanent = null;
if (game.getState().getZone(getSourceId()) == Zone.BATTLEFIELD) {
sourcePermanent = game.getPermanent(getSourceId());
} else {
if (game.checkShortLivingLKI(getSourceId(), Zone.BATTLEFIELD)) {
sourcePermanent = (Permanent) game.getLastKnownInformation(getSourceId(), Zone.BATTLEFIELD);
}
}
if (sourcePermanent == null) {
return false;
}
return hasSourceObjectAbility(game, sourcePermanent, event);
}
@Override @Override
public boolean checkTrigger(GameEvent event, Game game) { public boolean checkTrigger(GameEvent event, Game game) {
if (event.getSourceId().equals(getSourceId())) { if (event.getSourceId().equals(getSourceId())) {

View file

@ -21,6 +21,7 @@ public class GodEternalDiesTriggeredAbility extends TriggeredAbilityImpl {
public GodEternalDiesTriggeredAbility() { public GodEternalDiesTriggeredAbility() {
super(Zone.ALL, null, true); super(Zone.ALL, null, true);
this.setLeavesTheBattlefieldTrigger(true);
} }
private GodEternalDiesTriggeredAbility(GodEternalDiesTriggeredAbility ability) { private GodEternalDiesTriggeredAbility(GodEternalDiesTriggeredAbility ability) {
@ -49,22 +50,6 @@ public class GodEternalDiesTriggeredAbility extends TriggeredAbilityImpl {
return false; return false;
} }
@Override
public boolean isInUseableZone(Game game, MageObject source, GameEvent event) {
Permanent sourcePermanent = null;
if (game.getState().getZone(getSourceId()) == Zone.BATTLEFIELD) {
sourcePermanent = game.getPermanent(getSourceId());
} else {
if (game.checkShortLivingLKI(getSourceId(), Zone.BATTLEFIELD)) {
sourcePermanent = (Permanent) game.getLastKnownInformation(getSourceId(), Zone.BATTLEFIELD);
}
}
if (sourcePermanent == null) {
return false;
}
return hasSourceObjectAbility(game, sourcePermanent, event);
}
@Override @Override
public GodEternalDiesTriggeredAbility copy() { public GodEternalDiesTriggeredAbility copy() {
return new GodEternalDiesTriggeredAbility(this); return new GodEternalDiesTriggeredAbility(this);

View file

@ -259,8 +259,8 @@ public abstract class ManaCostImpl extends CostImpl implements ManaCost {
return false; return false;
} }
// TODO: is it require Phyrexian stile effects here for single payment? // no needs to call
//AbilityImpl.preparePhyrexianCost(game, source, player, ability, this); //AbilityImpl.handlePhyrexianLikeEffects(game, source, ability, this);
if (!player.getManaPool().isForcedToPay()) { if (!player.getManaPool().isForcedToPay()) {
assignPayment(game, ability, player.getManaPool(), costToPay != null ? costToPay : this); assignPayment(game, ability, player.getManaPool(), costToPay != null ? costToPay : this);

View file

@ -1,5 +1,6 @@
package mage.abilities.decorator; package mage.abilities.decorator;
import mage.MageObject;
import mage.abilities.Modes; import mage.abilities.Modes;
import mage.abilities.TriggeredAbility; import mage.abilities.TriggeredAbility;
import mage.abilities.TriggeredAbilityImpl; import mage.abilities.TriggeredAbilityImpl;
@ -7,6 +8,7 @@ import mage.abilities.condition.Condition;
import mage.abilities.effects.Effect; import mage.abilities.effects.Effect;
import mage.abilities.effects.Effects; import mage.abilities.effects.Effects;
import mage.constants.EffectType; import mage.constants.EffectType;
import mage.constants.Zone;
import mage.game.Game; import mage.game.Game;
import mage.game.events.GameEvent; import mage.game.events.GameEvent;
import mage.util.CardUtil; import mage.util.CardUtil;
@ -133,4 +135,14 @@ public class ConditionalInterveningIfTriggeredAbility extends TriggeredAbilityIm
public boolean caresAboutManaColor() { public boolean caresAboutManaColor() {
return condition.caresAboutManaColor(); return condition.caresAboutManaColor();
} }
@Override
public boolean isInUseableZone(Game game, MageObject source, GameEvent event) {
if (isLeavesTheBattlefieldTrigger()) {
// TODO: leaves battlefield and die are not same! Is it possible make a diff logic?
return TriggeredAbilityImpl.isInUseableZoneDiesTrigger(this, event, game);
} else {
return super.isInUseableZone(game, source, event);
}
}
} }

View file

@ -1,5 +1,6 @@
package mage.abilities.meta; package mage.abilities.meta;
import mage.MageObject;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.TriggeredAbility; import mage.abilities.TriggeredAbility;
import mage.abilities.TriggeredAbilityImpl; import mage.abilities.TriggeredAbilityImpl;
@ -10,10 +11,7 @@ import mage.game.events.GameEvent;
import mage.util.CardUtil; import mage.util.CardUtil;
import mage.watchers.Watcher; import mage.watchers.Watcher;
import java.util.ArrayList; import java.util.*;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@ -46,6 +44,10 @@ public class OrTriggeredAbility extends TriggeredAbilityImpl {
for(Watcher watcher : ability.getWatchers()) { for(Watcher watcher : ability.getWatchers()) {
super.addWatcher(watcher); super.addWatcher(watcher);
} }
if (ability.isLeavesTheBattlefieldTrigger()) {
this.setLeavesTheBattlefieldTrigger(true);
}
} }
setTriggerPhrase(generateTriggerPhrase()); setTriggerPhrase(generateTriggerPhrase());
} }
@ -123,4 +125,19 @@ public class OrTriggeredAbility extends TriggeredAbilityImpl {
ability.addWatcher(watcher); ability.addWatcher(watcher);
} }
} }
@Override
public boolean isInUseableZone(Game game, MageObject source, GameEvent event) {
boolean res = false;
for (TriggeredAbility ability : triggeredAbilities) {
// TODO: call full inner trigger instead like ability.isInUseableZone()?! Need research why it fails
if (ability.isLeavesTheBattlefieldTrigger()) {
// TODO: leaves battlefield and die are not same! Is it possible make a diff logic?
res |= TriggeredAbilityImpl.isInUseableZoneDiesTrigger(this, event, game);
} else {
res |= super.isInUseableZone(game, source, event);
}
}
return res;
}
} }

View file

@ -63,7 +63,7 @@ class DackFaydenEmblemTriggeredAbility extends TriggeredAbilityImpl {
@Override @Override
public boolean checkTrigger(GameEvent event, Game game) { public boolean checkTrigger(GameEvent event, Game game) {
boolean returnValue = false; boolean returnValue = false;
List<UUID> targetedPermanentIds = new ArrayList<>(0); List<UUID> targetedPermanentIds = new ArrayList<>();
Player player = game.getPlayer(this.getControllerId()); Player player = game.getPlayer(this.getControllerId());
if (player != null) { if (player != null) {
if (event.getPlayerId().equals(this.getControllerId())) { if (event.getPlayerId().equals(this.getControllerId())) {

View file

@ -15,7 +15,7 @@ import java.util.*;
*/ */
public abstract class NthTargetPointer extends TargetPointerImpl { public abstract class NthTargetPointer extends TargetPointerImpl {
private static final List<UUID> emptyTargets = Collections.unmodifiableList(new ArrayList<>(0)); private static final List<UUID> emptyTargets = Collections.unmodifiableList(new ArrayList<>());
// TODO: rework to list of MageObjectReference instead zcc // TODO: rework to list of MageObjectReference instead zcc
private final Map<UUID, Integer> zoneChangeCounter = new HashMap<>(); private final Map<UUID, Integer> zoneChangeCounter = new HashMap<>();