mirror of
https://github.com/magefree/mage.git
synced 2025-12-20 10:40:06 -08:00
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:
parent
a2ed52b8de
commit
66b338c6fc
18 changed files with 233 additions and 63 deletions
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 isn’t 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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.)
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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, "));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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())) {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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())) {
|
||||||
|
|
|
||||||
|
|
@ -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<>();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue