* WARNING, MageSingleton abilities contains dirty data here, so you can't use sourceId with it - * - * @return The {@link java.util.UUID} of the object this ability is - * associated with. */ UUID getSourceId(); @@ -351,16 +348,24 @@ public interface Ability extends Controllable, Serializable { void addWatcher(Watcher watcher); /** - * Returns true if this abilities source is in the zone for the ability + * Allow to control ability/trigger's lifecycle + *
+ * 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)
+ *
+ * @param sourceObject can be null for static continues effects checking like rules modification (example: Yixlid Jailer)
+ * @param event can be null for state base effects checking like "when you control seven or more" (example: Endrek Sahr, Master Breeder)
*/
- boolean isInUseableZone(Game game, MageObject source, GameEvent event);
+ boolean isInUseableZone(Game game, MageObject sourceObject, GameEvent event);
/**
* Returns true if the source object has currently the ability (e.g. The
* object can have lost all or some abilities for some time (e.g. Turn to
* Frog)
*/
- boolean hasSourceObjectAbility(Game game, MageObject source, GameEvent event);
+ boolean hasSourceObjectAbility(Game game, MageObject sourceObject, GameEvent event);
/**
* Returns true if the ability has a tap itself in their costs
@@ -478,6 +483,7 @@ public interface Ability extends Controllable, Serializable {
/**
* Finds the source object regardless of its zcc. Can be LKI from battlefield in some cases.
+ * Warning, do not use with singleton abilities
*/
MageObject getSourceObject(Game game);
diff --git a/Mage/src/main/java/mage/abilities/AbilityImpl.java b/Mage/src/main/java/mage/abilities/AbilityImpl.java
index 0830467cbdb..242a25f863e 100644
--- a/Mage/src/main/java/mage/abilities/AbilityImpl.java
+++ b/Mage/src/main/java/mage/abilities/AbilityImpl.java
@@ -29,7 +29,9 @@ import mage.game.Game;
import mage.game.command.Dungeon;
import mage.game.command.Emblem;
import mage.game.command.Plane;
+import mage.game.events.BatchEvent;
import mage.game.events.GameEvent;
+import mage.game.events.ZoneChangeEvent;
import mage.game.permanent.Permanent;
import mage.game.stack.Spell;
import mage.game.stack.StackAbility;
@@ -1172,69 +1174,162 @@ public abstract class AbilityImpl implements Ability {
return false;
}
- /**
- * @param game
- * @param source
- * @return
- */
@Override
- public boolean isInUseableZone(Game game, MageObject source, GameEvent event) {
- if (!this.hasSourceObjectAbility(game, source, event)) {
+ public boolean isInUseableZone(Game game, MageObject sourceObject, GameEvent event) {
+ if (!this.hasSourceObjectAbility(game, sourceObject, event)) {
return false;
}
+
+ // workaround for singleton abilities like Flying
+ UUID affectedSourceId = getRealSourceObjectId(this, sourceObject);
+ MageObject affectedSourceObject = game.getObject(affectedSourceId);
+
+ // global game effects (works all the time and don't have sourceId, example: FinalityCounterEffect)
+ if (affectedSourceId == null) {
+ return true;
+ }
+
+ // emblems/dungeons/planes effects (works all the time, store in command zone)
if (zone == Zone.COMMAND) {
- if (this.getSourceId() == null) { // commander effects
- return true;
- }
- MageObject object = game.getObject(this.getSourceId());
- // emblem/planes are always actual
- if (object instanceof Emblem || object instanceof Dungeon || object instanceof Plane) {
+ if (affectedSourceObject instanceof Emblem || affectedSourceObject instanceof Dungeon || affectedSourceObject instanceof Plane) {
return true;
}
}
- UUID parameterSourceId;
- // for singleton abilities like Flying we can't rely on abilities' source because it's only once in continuous effects
- // so will use the sourceId of the object itself that came as a parameter if it is not null
- if (this instanceof MageSingleton && source != null) {
- parameterSourceId = source.getId();
- } else {
- parameterSourceId = getSourceId();
- }
- // check against shortLKI for effects that move multiple object at the same time (e.g. destroy all)
- if (game.checkShortLivingLKI(getSourceId(), getZone())) {
+ // on entering permanents must use static abilities like it already on battlefield
+ // example: Tatterkite enters without counters from Mikaeus, the Unhallowed
+ if (game.getPermanentEntering(affectedSourceId) != null && zone == Zone.BATTLEFIELD) {
return true;
}
- // 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 affectedObjectZone = game.getState().getZone(affectedSourceId);
+
+ // 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 use cases and implement shared logic with "looking zone" instead LKI only
+ // in most use cases it's already supported by event (example: saved permanent object in event's target)
+ // [x] 603.10a leaves-the-battlefield abilities and other
+ // [ ] 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.
+
+ if (event == null) {
+ // state base triggers - use only actual state
+ } else {
+ // event triggers and continues effects - can look back in time
+ if (isAbilityCanLookBackInTime(this) && isEventCanLookBackInTime(event)) {
+ // 603.10a leaves-the-battlefield
+ if (game.checkShortLivingLKI(affectedSourceId, Zone.BATTLEFIELD)) {
+ affectedObjectZone = Zone.BATTLEFIELD;
+ }
+ // 603.10a leaves a graveyard
+ // TODO: need tests
+ if (game.checkShortLivingLKI(affectedSourceId, Zone.GRAVEYARD)) {
+ affectedObjectZone = Zone.GRAVEYARD;
+ }
+ // 603.10a put into a hand or library
+ // TODO: need tests and implementation?
+ }
+ }
+
+ return zone.match(affectedObjectZone);
+ }
+
+ public static boolean isAbilityCanLookBackInTime(Ability ability) {
+ if (ability instanceof StaticAbility) {
+ return true;
+ }
+ if (ability instanceof TriggeredAbility) {
+ return ((TriggeredAbility) ability).isLeavesTheBattlefieldTrigger();
+ }
+ return false;
+ }
+
+ public static boolean isEventCanLookBackInTime(GameEvent event) {
+ if (event == null) {
+ return false;
+ }
+
+ List
+ * 603.6c
+ * Leaves-the-battlefield abilities trigger when a permanent moves from the battlefield to another zone,
+ * or when a phased-in permanent leaves the game because its owner leaves the game. These are written as,
+ * but aren’t limited to, “When [this object] leaves the battlefield, . . .” or “Whenever [something] is put
+ * into a graveyard from the battlefield, . . . .” (See also rule 603.10.) An ability that attempts to do
+ * something to the card that left the battlefield checks for it only in the first zone that it went to.
+ * An ability that triggers when a card is put into a certain zone “from anywhere” is never treated as a
+ * leaves-the-battlefield ability, even if an object is put into that zone from the battlefield.
+ */
void setLeavesTheBattlefieldTrigger(boolean leavesTheBattlefieldTrigger);
@Override
diff --git a/Mage/src/main/java/mage/abilities/TriggeredAbilityImpl.java b/Mage/src/main/java/mage/abilities/TriggeredAbilityImpl.java
index 29030e2bf3f..a119c31c1bb 100644
--- a/Mage/src/main/java/mage/abilities/TriggeredAbilityImpl.java
+++ b/Mage/src/main/java/mage/abilities/TriggeredAbilityImpl.java
@@ -8,7 +8,9 @@ import mage.constants.AbilityType;
import mage.constants.AbilityWord;
import mage.constants.Zone;
import mage.game.Game;
+import mage.game.events.BatchEvent;
import mage.game.events.GameEvent;
+import mage.game.events.ZoneChangeBatchEvent;
import mage.game.events.ZoneChangeEvent;
import mage.game.permanent.Permanent;
import mage.game.permanent.PermanentToken;
@@ -49,7 +51,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
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());
-
}
}
@@ -347,84 +348,73 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge
}
@Override
- public boolean isInUseableZone(Game game, MageObject source, GameEvent event) {
+ public boolean isInUseableZone(Game game, MageObject sourceObject, GameEvent event) {
- /**
- * 603.6. Trigger events that involve objects changing zones are called
- * “zone-change triggers.” Many abilities with zone-change triggers
- * attempt to do something to that object after it changes zones. During
- * resolution, these abilities look for the object in the zone that it
- * moved to. If the object is unable to be found in the zone it went to,
- * the part of the ability attempting to do something to the object will
- * fail to do anything. The ability could be unable to find the object
- * because the object never entered the specified zone, because it left
- * the zone before the ability resolved, or because it is in a zone that
- * is hidden from a player, such as a library or an opponent's hand.
- * (This rule applies even if the object leaves the zone and returns
- * again before the ability resolves.) The most common zone-change
- * triggers are enters-the-battlefield triggers and
- * leaves-the-battlefield triggers.
- *
- * from:
- * http://www.mtgsalvation.com/forums/magic-fundamentals/magic-rulings/magic-rulings-archives/537065-ixidron-and-kozilek
- * There are two types of triggers that involve the graveyard: dies
- * triggers (which are a subset of leave-the-battlefield triggers) and
- * put into the graveyard from anywhere triggers.
- *
- * The former triggers trigger based on the game state prior to the move
- * where the Kozilek permanent is face down and has no abilities. The
- * latter triggers trigger from the game state after the move where the
- * Kozilek card is itself and has the ability.
- */
+ // workaround for singleton abilities like Flying
+ UUID affectedSourceId = getRealSourceObjectId(this, sourceObject);
- Set
* 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.
* (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.
*/
- public static boolean isInUseableZoneDiesTrigger(TriggeredAbility source, GameEvent event, Game game) {
- // Get the source permanent of the ability
- MageObject sourceObject = null;
- if (game.getState().getZone(source.getSourceId()) == Zone.BATTLEFIELD) {
- sourceObject = game.getPermanent(source.getSourceId());
+ public static boolean isInUseableZoneDiesTrigger(TriggeredAbility sourceAbility, MageObject sourceObject, GameEvent event, Game game) {
+ // runtime check: wrong trigger settings
+ if (!sourceAbility.isLeavesTheBattlefieldTrigger()) {
+ throw new IllegalArgumentException("Wrong code usage: all dies triggers must use setLeavesTheBattlefieldTrigger(true) and override isInUseableZone - "
+ + sourceAbility.getSourceObject(game) + " - " + sourceAbility);
+ }
+
+ // runtime check: wrong isInUseableZone for batch related triggers
+ if (event instanceof BatchEvent) {
+ throw new IllegalArgumentException("Wrong code usage: batch events unsupported here, possible miss of override isInUseableZone - "
+ + sourceAbility.getSourceObject(game) + " - " + sourceAbility);
+ }
+
+ // workaround for singleton abilities like Flying
+ UUID affectedSourceId = getRealSourceObjectId(sourceAbility, sourceObject);
+
+ // on permanent - can use actual or look back in time
+ MageObject affectedObject = null;
+ if (game.getState().getZone(affectedSourceId) == Zone.BATTLEFIELD) {
+ affectedObject = game.getPermanent(affectedSourceId);
} else {
// The idea: short living LKI must help to find a moment in the inner of resolve
// -
@@ -480,26 +489,29 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge
// - ! empty stack ! graveyard ! no ! no ! no more to resolve
// --!---------------!-------------!-----!-----------!
// -
- if (game.checkShortLivingLKI(source.getSourceId(), Zone.BATTLEFIELD)) {
- sourceObject = (Permanent) game.getLastKnownInformation(source.getSourceId(), Zone.BATTLEFIELD);
- }
- }
- if (sourceObject == null) { // source is no permanent
- sourceObject = game.getObject(source);
- if (sourceObject == null || sourceObject.isPermanent(game)) {
- return false; // No source object found => ability is not valid
+ if (game.checkShortLivingLKI(affectedSourceId, Zone.BATTLEFIELD)) {
+ affectedObject = game.getLastKnownInformation(affectedSourceId, Zone.BATTLEFIELD);
}
}
- if (!source.hasSourceObjectAbility(game, sourceObject, event)) {
+ if (affectedObject == null) {
+ affectedObject = game.getObject(sourceAbility);
+ if (affectedObject == null || affectedObject.isPermanent(game)) {
+ // if it was a permanent, but now removed then ignore
+ return false;
+ }
+ }
+
+ if (!sourceAbility.hasSourceObjectAbility(game, affectedObject, event)) {
return false; // the permanent does currently not have or before it dies the ability so no trigger
}
// check now it is in graveyard (only if it is no token and was the target itself)
- if (source.getSourceId().equals(event.getTargetId()) // source is also the target
- && !(sourceObject instanceof PermanentToken) // it's no token
- && sourceObject.getZoneChangeCounter(game) + 1 == game.getState().getZoneChangeCounter(source.getSourceId())) { // It's in the next zone
- Zone after = game.getState().getZone(source.getSourceId());
+ // TODO: need research
+ if (affectedSourceId.equals(event.getTargetId()) // source is also the target
+ && !(affectedObject instanceof PermanentToken) // it's no token
+ && affectedObject.getZoneChangeCounter(game) + 1 == game.getState().getZoneChangeCounter(affectedSourceId)) { // It's in the next zone
+ Zone after = game.getState().getZone(affectedSourceId);
if (!Zone.GRAVEYARD.match(after)) { // Zone is not the graveyard
return false; // Moving to graveyard was replaced so no trigger
}
diff --git a/Mage/src/main/java/mage/abilities/common/CastFromGraveyardOnceEachTurnAbility.java b/Mage/src/main/java/mage/abilities/common/CastFromGraveyardOnceEachTurnAbility.java
index 6ee178ca922..17ddc9a8bd5 100644
--- a/Mage/src/main/java/mage/abilities/common/CastFromGraveyardOnceEachTurnAbility.java
+++ b/Mage/src/main/java/mage/abilities/common/CastFromGraveyardOnceEachTurnAbility.java
@@ -116,7 +116,7 @@ class CastFromGraveyardOnceWatcher extends Watcher {
public void watch(GameEvent event, Game game) {
if (GameEvent.EventType.SPELL_CAST.equals(event.getType())
&& event.hasApprovingIdentifier(MageIdentifier.CastFromGraveyardOnceWatcher)) {
- usedFrom.add(event.getAdditionalReference().getApprovingMageObjectReference());
+ usedFrom.add(event.getApprovingObject().getApprovingMageObjectReference());
}
}
diff --git a/Mage/src/main/java/mage/abilities/common/DealtDamageAndDiedTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/DealtDamageAndDiedTriggeredAbility.java
index e2903456db2..6afca9b9c41 100644
--- a/Mage/src/main/java/mage/abilities/common/DealtDamageAndDiedTriggeredAbility.java
+++ b/Mage/src/main/java/mage/abilities/common/DealtDamageAndDiedTriggeredAbility.java
@@ -1,5 +1,6 @@
package mage.abilities.common;
+import mage.MageObject;
import mage.MageObjectReference;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.Effect;
@@ -35,6 +36,7 @@ public class DealtDamageAndDiedTriggeredAbility extends TriggeredAbilityImpl {
this.filter = filter;
this.setTargetPointer = setTargetPointer;
setTriggerPhrase(getWhen() + CardUtil.addArticle(filter.getMessage()) + " dealt damage by {this} this turn dies, ");
+ setLeavesTheBattlefieldTrigger(true);
}
protected DealtDamageAndDiedTriggeredAbility(final DealtDamageAndDiedTriggeredAbility ability) {
@@ -55,6 +57,9 @@ public class DealtDamageAndDiedTriggeredAbility extends TriggeredAbilityImpl {
@Override
public boolean checkTrigger(GameEvent event, Game game) {
+ // If Axelrod Gunnarson and a creature it dealt damage to are both put into a graveyard at the same time,
+ // Axelrod Gunnarson’s second ability will trigger.
+ // (2009-10-01)
if (((ZoneChangeEvent) event).isDiesEvent()) {
ZoneChangeEvent zEvent = (ZoneChangeEvent) event;
if (filter.match(zEvent.getTarget(), game)) {
@@ -78,4 +83,9 @@ public class DealtDamageAndDiedTriggeredAbility extends TriggeredAbilityImpl {
}
return false;
}
+
+ @Override
+ public boolean isInUseableZone(Game game, MageObject sourceObject, GameEvent event) {
+ return TriggeredAbilityImpl.isInUseableZoneDiesTrigger(this, sourceObject, event, game);
+ }
}
diff --git a/Mage/src/main/java/mage/abilities/common/DealtDamageAttachedAndDiedTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/DealtDamageAttachedAndDiedTriggeredAbility.java
index 1ad19fa0246..04a36d16338 100644
--- a/Mage/src/main/java/mage/abilities/common/DealtDamageAttachedAndDiedTriggeredAbility.java
+++ b/Mage/src/main/java/mage/abilities/common/DealtDamageAttachedAndDiedTriggeredAbility.java
@@ -1,5 +1,6 @@
package mage.abilities.common;
+import mage.MageObject;
import mage.MageObjectReference;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.Effect;
@@ -32,6 +33,7 @@ public class DealtDamageAttachedAndDiedTriggeredAbility extends TriggeredAbility
setTriggerPhrase(getWhen() + CardUtil.addArticle(filter.getMessage()) + " dealt damage by "
+ CardUtil.getTextWithFirstCharLowerCase(attachmentType.verb()) +
" creature this turn dies, ");
+ setLeavesTheBattlefieldTrigger(true);
}
protected DealtDamageAttachedAndDiedTriggeredAbility(final DealtDamageAttachedAndDiedTriggeredAbility ability) {
@@ -75,4 +77,9 @@ public class DealtDamageAttachedAndDiedTriggeredAbility extends TriggeredAbility
}
return true;
}
+
+ @Override
+ public boolean isInUseableZone(Game game, MageObject sourceObject, GameEvent event) {
+ return TriggeredAbilityImpl.isInUseableZoneDiesTrigger(this, sourceObject, event, game);
+ }
}
diff --git a/Mage/src/main/java/mage/abilities/common/DiesAttachedTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/DiesAttachedTriggeredAbility.java
index d88602941b5..b291486ec39 100644
--- a/Mage/src/main/java/mage/abilities/common/DiesAttachedTriggeredAbility.java
+++ b/Mage/src/main/java/mage/abilities/common/DiesAttachedTriggeredAbility.java
@@ -1,5 +1,6 @@
package mage.abilities.common;
+import mage.MageObject;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.Effect;
import mage.cards.Card;
@@ -48,6 +49,7 @@ public class DiesAttachedTriggeredAbility extends TriggeredAbilityImpl {
this.setTargetPointer = setTargetPointer;
this.rememberSource = rememberSource;
setTriggerPhrase(generateTriggerPhrase());
+ setLeavesTheBattlefieldTrigger(true);
}
protected DiesAttachedTriggeredAbility(final DiesAttachedTriggeredAbility ability) {
@@ -157,4 +159,9 @@ public class DiesAttachedTriggeredAbility extends TriggeredAbilityImpl {
}
return sb.toString();
}
+
+ @Override
+ public boolean isInUseableZone(Game game, MageObject sourceObject, GameEvent event) {
+ return TriggeredAbilityImpl.isInUseableZoneDiesTrigger(this, sourceObject, event, game);
+ }
}
diff --git a/Mage/src/main/java/mage/abilities/common/DiesCreatureTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/DiesCreatureTriggeredAbility.java
index cc6f9fc8624..9b885b1bf6e 100644
--- a/Mage/src/main/java/mage/abilities/common/DiesCreatureTriggeredAbility.java
+++ b/Mage/src/main/java/mage/abilities/common/DiesCreatureTriggeredAbility.java
@@ -48,6 +48,7 @@ public class DiesCreatureTriggeredAbility extends TriggeredAbilityImpl {
super(zone, effect, optional);
this.filter = filter;
this.setTargetPointer = setTargetPointer;
+ setLeavesTheBattlefieldTrigger(true);
setTriggerPhrase("Whenever " + filter.getMessage() + (filter.getMessage().startsWith("one or more") ? " die, " : " dies, "));
}
@@ -81,11 +82,11 @@ public class DiesCreatureTriggeredAbility extends TriggeredAbilityImpl {
}
@Override
- public boolean isInUseableZone(Game game, MageObject source, GameEvent event) {
+ public boolean isInUseableZone(Game game, MageObject sourceObject, GameEvent event) {
if (this.zone == Zone.BATTLEFIELD) {
- return TriggeredAbilityImpl.isInUseableZoneDiesTrigger(this, event, game);
+ return TriggeredAbilityImpl.isInUseableZoneDiesTrigger(this, sourceObject, event, game);
} else {
- return super.isInUseableZone(game, source, event);
+ return super.isInUseableZone(game, sourceObject, event);
}
}
}
diff --git a/Mage/src/main/java/mage/abilities/common/DiesOneOrMoreTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/DiesOneOrMoreTriggeredAbility.java
index 655a7d57839..a537a79f359 100644
--- a/Mage/src/main/java/mage/abilities/common/DiesOneOrMoreTriggeredAbility.java
+++ b/Mage/src/main/java/mage/abilities/common/DiesOneOrMoreTriggeredAbility.java
@@ -23,6 +23,7 @@ public class DiesOneOrMoreTriggeredAbility extends TriggeredAbilityImpl implemen
super(Zone.BATTLEFIELD, effect, optional);
this.filter = filter;
this.setTriggerPhrase("Whenever one or more " + filter.getMessage() + " die, ");
+ setLeavesTheBattlefieldTrigger(true);
}
private DiesOneOrMoreTriggeredAbility(final DiesOneOrMoreTriggeredAbility ability) {
@@ -55,10 +56,10 @@ public class DiesOneOrMoreTriggeredAbility extends TriggeredAbilityImpl implemen
}
@Override
- public boolean isInUseableZone(Game game, MageObject source, GameEvent event) {
+ public boolean isInUseableZone(Game game, MageObject sourceObject, GameEvent event) {
return ((ZoneChangeBatchEvent) event)
.getEvents()
.stream()
- .allMatch(e -> TriggeredAbilityImpl.isInUseableZoneDiesTrigger(this, e, game));
+ .anyMatch(e -> TriggeredAbilityImpl.isInUseableZoneDiesTrigger(this, sourceObject, e, game));
}
}
diff --git a/Mage/src/main/java/mage/abilities/common/DiesSourceTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/DiesSourceTriggeredAbility.java
index 790e94ae41e..fdadfae2743 100644
--- a/Mage/src/main/java/mage/abilities/common/DiesSourceTriggeredAbility.java
+++ b/Mage/src/main/java/mage/abilities/common/DiesSourceTriggeredAbility.java
@@ -42,7 +42,7 @@ public class DiesSourceTriggeredAbility extends ZoneChangeTriggeredAbility {
}
@Override
- public boolean isInUseableZone(Game game, MageObject source, GameEvent event) {
- return TriggeredAbilityImpl.isInUseableZoneDiesTrigger(this, event, game);
+ public boolean isInUseableZone(Game game, MageObject sourceObject, GameEvent event) {
+ return TriggeredAbilityImpl.isInUseableZoneDiesTrigger(this, sourceObject, event, game);
}
}
diff --git a/Mage/src/main/java/mage/abilities/common/DiesThisOrAnotherTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/DiesThisOrAnotherTriggeredAbility.java
index 8a19656b323..e55160ee720 100644
--- a/Mage/src/main/java/mage/abilities/common/DiesThisOrAnotherTriggeredAbility.java
+++ b/Mage/src/main/java/mage/abilities/common/DiesThisOrAnotherTriggeredAbility.java
@@ -30,6 +30,7 @@ public class DiesThisOrAnotherTriggeredAbility extends TriggeredAbilityImpl {
filterMessage = filterMessage.substring(2);
}
setTriggerPhrase("Whenever {this} or another " + filterMessage + " dies, ");
+ setLeavesTheBattlefieldTrigger(true);
}
protected DiesThisOrAnotherTriggeredAbility(final DiesThisOrAnotherTriggeredAbility ability) {
@@ -65,7 +66,7 @@ public class DiesThisOrAnotherTriggeredAbility extends TriggeredAbilityImpl {
}
@Override
- public boolean isInUseableZone(Game game, MageObject source, GameEvent event) {
- return TriggeredAbilityImpl.isInUseableZoneDiesTrigger(this, event, game);
+ public boolean isInUseableZone(Game game, MageObject sourceObject, GameEvent event) {
+ return TriggeredAbilityImpl.isInUseableZoneDiesTrigger(this, sourceObject, event, game);
}
}
diff --git a/Mage/src/main/java/mage/abilities/common/ExploitCreatureTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/ExploitCreatureTriggeredAbility.java
index 9989905a53f..1a380412bc9 100644
--- a/Mage/src/main/java/mage/abilities/common/ExploitCreatureTriggeredAbility.java
+++ b/Mage/src/main/java/mage/abilities/common/ExploitCreatureTriggeredAbility.java
@@ -48,22 +48,6 @@ public class ExploitCreatureTriggeredAbility extends TriggeredAbilityImpl {
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
public boolean checkTrigger(GameEvent event, Game game) {
if (event.getSourceId().equals(getSourceId())) {
diff --git a/Mage/src/main/java/mage/abilities/common/GodEternalDiesTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/GodEternalDiesTriggeredAbility.java
index f8cc58d95b0..26a89a93904 100644
--- a/Mage/src/main/java/mage/abilities/common/GodEternalDiesTriggeredAbility.java
+++ b/Mage/src/main/java/mage/abilities/common/GodEternalDiesTriggeredAbility.java
@@ -21,6 +21,7 @@ public class GodEternalDiesTriggeredAbility extends TriggeredAbilityImpl {
public GodEternalDiesTriggeredAbility() {
super(Zone.ALL, null, true);
+ setLeavesTheBattlefieldTrigger(true);
}
private GodEternalDiesTriggeredAbility(GodEternalDiesTriggeredAbility ability) {
@@ -49,22 +50,6 @@ public class GodEternalDiesTriggeredAbility extends TriggeredAbilityImpl {
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
public GodEternalDiesTriggeredAbility copy() {
return new GodEternalDiesTriggeredAbility(this);
diff --git a/Mage/src/main/java/mage/abilities/common/PutIntoGraveFromAnywhereSourceTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/PutIntoGraveFromAnywhereSourceTriggeredAbility.java
index a50025271ad..cbf4ccd4da2 100644
--- a/Mage/src/main/java/mage/abilities/common/PutIntoGraveFromAnywhereSourceTriggeredAbility.java
+++ b/Mage/src/main/java/mage/abilities/common/PutIntoGraveFromAnywhereSourceTriggeredAbility.java
@@ -41,7 +41,7 @@ public class PutIntoGraveFromAnywhereSourceTriggeredAbility extends ZoneChangeTr
// * @return
// */
// @Override
-// public boolean isInUseableZone(Game game, MageObject source, GameEvent event) {
+// public boolean isInUseableZone(Game game, MageObject sourceObject, GameEvent event) {
// if (game.getState().getZone(source.getId()).equals(Zone.GRAVEYARD)) {
// return this.hasSourceObjectAbility(game, source, event);
// }
diff --git a/Mage/src/main/java/mage/abilities/common/PutIntoGraveFromBattlefieldAllTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/PutIntoGraveFromBattlefieldAllTriggeredAbility.java
index b86baa07a3a..f67a6bdba61 100644
--- a/Mage/src/main/java/mage/abilities/common/PutIntoGraveFromBattlefieldAllTriggeredAbility.java
+++ b/Mage/src/main/java/mage/abilities/common/PutIntoGraveFromBattlefieldAllTriggeredAbility.java
@@ -26,7 +26,7 @@ public class PutIntoGraveFromBattlefieldAllTriggeredAbility extends TriggeredAbi
public PutIntoGraveFromBattlefieldAllTriggeredAbility(Effect effect, boolean optional, FilterPermanent filter, boolean setTargetPointer, boolean onlyToControllerGraveyard) {
super(Zone.BATTLEFIELD, effect, optional);
- this.setLeavesTheBattlefieldTrigger(true);
+ setLeavesTheBattlefieldTrigger(true);
this.filter = filter;
this.onlyToControllerGraveyard = onlyToControllerGraveyard;
this.setTargetPointer = setTargetPointer;
@@ -66,7 +66,7 @@ public class PutIntoGraveFromBattlefieldAllTriggeredAbility extends TriggeredAbi
}
@Override
- public boolean isInUseableZone(Game game, MageObject source, GameEvent event) {
- return TriggeredAbilityImpl.isInUseableZoneDiesTrigger(this, event, game);
+ public boolean isInUseableZone(Game game, MageObject sourceObject, GameEvent event) {
+ return TriggeredAbilityImpl.isInUseableZoneDiesTrigger(this, sourceObject, event, game);
}
}
diff --git a/Mage/src/main/java/mage/abilities/common/PutIntoGraveFromBattlefieldSourceTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/PutIntoGraveFromBattlefieldSourceTriggeredAbility.java
index 629f3cd1937..fbff5dc43d1 100644
--- a/Mage/src/main/java/mage/abilities/common/PutIntoGraveFromBattlefieldSourceTriggeredAbility.java
+++ b/Mage/src/main/java/mage/abilities/common/PutIntoGraveFromBattlefieldSourceTriggeredAbility.java
@@ -58,7 +58,7 @@ public class PutIntoGraveFromBattlefieldSourceTriggeredAbility extends Triggered
}
@Override
- public boolean isInUseableZone(Game game, MageObject source, GameEvent event) {
- return TriggeredAbilityImpl.isInUseableZoneDiesTrigger(this, event, game);
+ public boolean isInUseableZone(Game game, MageObject sourceObject, GameEvent event) {
+ return TriggeredAbilityImpl.isInUseableZoneDiesTrigger(this, sourceObject, event, game);
}
}
diff --git a/Mage/src/main/java/mage/abilities/common/ZoneChangeTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/ZoneChangeTriggeredAbility.java
index 848ef2bc366..05629813f62 100644
--- a/Mage/src/main/java/mage/abilities/common/ZoneChangeTriggeredAbility.java
+++ b/Mage/src/main/java/mage/abilities/common/ZoneChangeTriggeredAbility.java
@@ -21,7 +21,9 @@ public class ZoneChangeTriggeredAbility extends TriggeredAbilityImpl {
protected final Zone toZone;
public ZoneChangeTriggeredAbility(Zone fromZone, Zone toZone, Effect effect, String triggerPhrase, boolean optional) {
+ // fix 3
this(toZone == null ? Zone.ALL : toZone, fromZone, toZone, effect, triggerPhrase, optional);
+ //this(fromZone == null ? Zone.ALL : fromZone, fromZone, toZone, effect, triggerPhrase, optional);
}
public ZoneChangeTriggeredAbility(Zone worksInZone, Zone fromZone, Zone toZone, Effect effect, String triggerPhrase, boolean optional) {
diff --git a/Mage/src/main/java/mage/abilities/common/delayed/UntilYourNextTurnDelayedTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/delayed/UntilYourNextTurnDelayedTriggeredAbility.java
index 136431ae8e0..33f9a066f40 100644
--- a/Mage/src/main/java/mage/abilities/common/delayed/UntilYourNextTurnDelayedTriggeredAbility.java
+++ b/Mage/src/main/java/mage/abilities/common/delayed/UntilYourNextTurnDelayedTriggeredAbility.java
@@ -1,9 +1,11 @@
package mage.abilities.common.delayed;
+import mage.MageObject;
import mage.abilities.DelayedTriggeredAbility;
import mage.abilities.Modes;
import mage.abilities.TriggeredAbility;
+import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.Effect;
import mage.abilities.effects.Effects;
import mage.constants.Duration;
@@ -27,7 +29,7 @@ public class UntilYourNextTurnDelayedTriggeredAbility extends DelayedTriggeredAb
public UntilYourNextTurnDelayedTriggeredAbility(TriggeredAbility ability) {
super(null, Duration.UntilYourNextTurn, false);
if (ability.isLeavesTheBattlefieldTrigger()) {
- this.setLeavesTheBattlefieldTrigger(true);
+ setLeavesTheBattlefieldTrigger(true);
}
this.ability = ability;
}
@@ -103,4 +105,14 @@ public class UntilYourNextTurnDelayedTriggeredAbility extends DelayedTriggeredAb
public int getSourceObjectZoneChangeCounter() {
return ability.getSourceObjectZoneChangeCounter();
}
+
+ @Override
+ public boolean isInUseableZone(Game game, MageObject sourceObject, GameEvent event) {
+ if (isLeavesTheBattlefieldTrigger()) {
+ // TODO: leaves battlefield and die are not same! Is it possible make a diff logic?
+ return TriggeredAbilityImpl.isInUseableZoneDiesTrigger(this, sourceObject, event, game);
+ } else {
+ return super.isInUseableZone(game, sourceObject, event);
+ }
+ }
}
diff --git a/Mage/src/main/java/mage/abilities/costs/common/UntapSourceCost.java b/Mage/src/main/java/mage/abilities/costs/common/UntapSourceCost.java
index 09ec8fe5b03..2e379faac2a 100644
--- a/Mage/src/main/java/mage/abilities/costs/common/UntapSourceCost.java
+++ b/Mage/src/main/java/mage/abilities/costs/common/UntapSourceCost.java
@@ -6,6 +6,7 @@ import mage.abilities.Ability;
import mage.abilities.costs.Cost;
import mage.abilities.costs.CostImpl;
import mage.constants.AsThoughEffectType;
+import mage.counters.CounterType;
import mage.game.Game;
import mage.game.permanent.Permanent;
@@ -27,7 +28,13 @@ public class UntapSourceCost extends CostImpl {
public boolean pay(Ability ability, Game game, Ability source, UUID controllerId, boolean noMana, Cost costToPay) {
Permanent permanent = game.getPermanent(source.getSourceId());
if (permanent != null) {
+ int stunCount = permanent.getCounters(game).getCount(CounterType.STUN);
paid = permanent.untap(game);
+ // 118.11 - if a stun counter replaces the untap, the cost has still been paid.
+ // Fear of Sleep Paralysis ruling - if the stun counter can't be removed, the untap cost hasn't been paid.
+ if (stunCount > 0) {
+ paid = permanent.getCounters(game).getCount(CounterType.STUN) < stunCount;
+ }
}
return paid;
}
diff --git a/Mage/src/main/java/mage/abilities/costs/common/UntapTargetCost.java b/Mage/src/main/java/mage/abilities/costs/common/UntapTargetCost.java
index 75f498e9705..8e0506c2f5c 100644
--- a/Mage/src/main/java/mage/abilities/costs/common/UntapTargetCost.java
+++ b/Mage/src/main/java/mage/abilities/costs/common/UntapTargetCost.java
@@ -4,6 +4,7 @@ import mage.abilities.Ability;
import mage.abilities.costs.Cost;
import mage.abilities.costs.CostImpl;
import mage.constants.Outcome;
+import mage.counters.CounterType;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.target.TargetPermanent;
@@ -45,7 +46,10 @@ public class UntapTargetCost extends CostImpl {
return false;
}
- if (permanent.untap(game)) {
+ // 118.11 - if a stun counter replaces the untap, the cost has still been paid.
+ // Fear of Sleep Paralysis ruling - if the stun counter can't be removed, the untap cost hasn't been paid.
+ int stunCount = permanent.getCounters(game).getCount(CounterType.STUN);
+ if (permanent.untap(game) || (stunCount > 0 && permanent.getCounters(game).getCount(CounterType.STUN) < stunCount)) {
untapped.add(targetId);
}
diff --git a/Mage/src/main/java/mage/abilities/costs/mana/ManaCostImpl.java b/Mage/src/main/java/mage/abilities/costs/mana/ManaCostImpl.java
index 2244f6f8123..1fd9bdd5f90 100644
--- a/Mage/src/main/java/mage/abilities/costs/mana/ManaCostImpl.java
+++ b/Mage/src/main/java/mage/abilities/costs/mana/ManaCostImpl.java
@@ -259,8 +259,8 @@ public abstract class ManaCostImpl extends CostImpl implements ManaCost {
return false;
}
- // TODO: is it require Phyrexian stile effects here for single payment?
- //AbilityImpl.preparePhyrexianCost(game, source, player, ability, this);
+ // no needs to call
+ //AbilityImpl.handlePhyrexianLikeEffects(game, source, ability, this);
if (!player.getManaPool().isForcedToPay()) {
assignPayment(game, ability, player.getManaPool(), costToPay != null ? costToPay : this);
diff --git a/Mage/src/main/java/mage/abilities/decorator/ConditionalInterveningIfTriggeredAbility.java b/Mage/src/main/java/mage/abilities/decorator/ConditionalInterveningIfTriggeredAbility.java
index b8e0b6f57d3..ab4e86f5d4d 100644
--- a/Mage/src/main/java/mage/abilities/decorator/ConditionalInterveningIfTriggeredAbility.java
+++ b/Mage/src/main/java/mage/abilities/decorator/ConditionalInterveningIfTriggeredAbility.java
@@ -1,5 +1,6 @@
package mage.abilities.decorator;
+import mage.MageObject;
import mage.abilities.Modes;
import mage.abilities.TriggeredAbility;
import mage.abilities.TriggeredAbilityImpl;
@@ -7,6 +8,7 @@ import mage.abilities.condition.Condition;
import mage.abilities.effects.Effect;
import mage.abilities.effects.Effects;
import mage.constants.EffectType;
+import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.util.CardUtil;
@@ -38,7 +40,7 @@ public class ConditionalInterveningIfTriggeredAbility extends TriggeredAbilityIm
public ConditionalInterveningIfTriggeredAbility(TriggeredAbility ability, Condition condition, String text) {
super(ability.getZone(), null);
if (ability.isLeavesTheBattlefieldTrigger()) {
- this.setLeavesTheBattlefieldTrigger(true);
+ setLeavesTheBattlefieldTrigger(true);
}
this.ability = ability;
this.condition = condition;
@@ -133,4 +135,14 @@ public class ConditionalInterveningIfTriggeredAbility extends TriggeredAbilityIm
public boolean caresAboutManaColor() {
return condition.caresAboutManaColor();
}
+
+ @Override
+ public boolean isInUseableZone(Game game, MageObject sourceObject, GameEvent event) {
+ if (isLeavesTheBattlefieldTrigger()) {
+ // TODO: leaves battlefield and die are not same! Is it possible make a diff logic?
+ return TriggeredAbilityImpl.isInUseableZoneDiesTrigger(this, sourceObject, event, game);
+ } else {
+ return super.isInUseableZone(game, sourceObject, event);
+ }
+ }
}
diff --git a/Mage/src/main/java/mage/abilities/decorator/ConditionalTriggeredAbility.java b/Mage/src/main/java/mage/abilities/decorator/ConditionalTriggeredAbility.java
index cb12f7b2b66..4a2b89ce8f5 100644
--- a/Mage/src/main/java/mage/abilities/decorator/ConditionalTriggeredAbility.java
+++ b/Mage/src/main/java/mage/abilities/decorator/ConditionalTriggeredAbility.java
@@ -1,5 +1,6 @@
package mage.abilities.decorator;
+import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.Modes;
import mage.abilities.TriggeredAbility;
@@ -41,6 +42,9 @@ public class ConditionalTriggeredAbility extends TriggeredAbilityImpl {
this.ability = ability;
this.condition = condition;
this.abilityText = text;
+ if (ability.isLeavesTheBattlefieldTrigger()) {
+ this.setLeavesTheBattlefieldTrigger(true);
+ }
}
protected ConditionalTriggeredAbility(final ConditionalTriggeredAbility triggered) {
@@ -118,4 +122,13 @@ public class ConditionalTriggeredAbility extends TriggeredAbilityImpl {
return this;
}
+ @Override
+ public boolean isInUseableZone(Game game, MageObject sourceObject, GameEvent event) {
+ if (isLeavesTheBattlefieldTrigger()) {
+ // TODO: leaves battlefield and die are not same! Is it possible make a diff logic?
+ return TriggeredAbilityImpl.isInUseableZoneDiesTrigger(this, sourceObject, event, game);
+ } else {
+ return super.isInUseableZone(game, sourceObject, event);
+ }
+ }
}
diff --git a/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java b/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java
index 8dd4e6cf9ea..5ad74ac0fb1 100644
--- a/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java
+++ b/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java
@@ -325,7 +325,7 @@ public class ContinuousEffects implements Serializable {
if (effect instanceof PayCostToAttackBlockEffect) {
Set