Triggered abilities - fixed that it can trigger from hand or library (related to #13089, regression from #13088)

This commit is contained in:
Oleg Agafonov 2024-12-01 15:28:09 +04:00
parent 78913ac84b
commit c6bec887b9
2 changed files with 74 additions and 33 deletions

View file

@ -73,4 +73,28 @@ public class DauthiVoidwalkerTest extends CardTestPlayerBase {
assertLife(playerA, 20); assertLife(playerA, 20);
assertLife(playerB, 20 - 3); assertLife(playerB, 20 - 3);
} }
@Test
public void test_MakeSureNoTriggerInWrongZones() {
// bug report: it triggered in library
// https://github.com/magefree/mage/issues/13089
// If a card would be put into an opponent's graveyard from anywhere, instead exile it with a void counter on it.
// {T}, Sacrifice Dauthi Voidwalker: Choose an exiled card an opponent owns with a void counter on it. You may play it this turn without paying its mana cost.
addCard(Zone.HAND, playerA, "Dauthi Voidwalker", 1);
//
addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", 1);
//
addCard(Zone.HAND, playerA, "Lightning Bolt");
addCard(Zone.BATTLEFIELD, playerA, "Mountain");
// kill B's creature without triggers
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", "Balduvian Bears");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
checkGraveyardCount("after kill", 1, PhaseStep.PRECOMBAT_MAIN, playerB, "Balduvian Bears", 1);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
}
} }

View file

@ -1182,22 +1182,21 @@ public abstract class AbilityImpl implements Ability {
// workaround for singleton abilities like Flying // workaround for singleton abilities like Flying
UUID affectedSourceId = getRealSourceObjectId(this, sourceObject); UUID affectedSourceId = getRealSourceObjectId(this, sourceObject);
MageObject affectedSourceObject = game.getObject(affectedSourceId);
// in command zone // global game effects (works all the time and don't have sourceId, example: FinalityCounterEffect)
if (zone == Zone.COMMAND) {
if (affectedSourceId == null) { if (affectedSourceId == null) {
// commander effects
return true;
} else {
MageObject object = game.getObject(affectedSourceId);
// emblem/planes are always actual
if (object instanceof Emblem || object instanceof Dungeon || object instanceof Plane) {
return true; return true;
} }
// emblems/dungeons/planes effects (works all the time, store in command zone)
if (zone == Zone.COMMAND) {
if (affectedSourceObject instanceof Emblem || affectedSourceObject instanceof Dungeon || affectedSourceObject instanceof Plane) {
return true;
} }
} }
// on entering permanents - must use static abilities like it already on battlefield // on entering permanents must use static abilities like it already on battlefield
// example: Tatterkite enters without counters from Mikaeus, the Unhallowed // example: Tatterkite enters without counters from Mikaeus, the Unhallowed
if (game.getPermanentEntering(affectedSourceId) != null && zone == Zone.BATTLEFIELD) { if (game.getPermanentEntering(affectedSourceId) != null && zone == Zone.BATTLEFIELD) {
return true; return true;
@ -1208,7 +1207,7 @@ public abstract class AbilityImpl implements Ability {
// any trigger conditions, and continuous effects that exist at that time are used to determine what the // 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. // trigger conditions are and what the objects involved in the event look like.
// ... // ...
Zone sourceObjectZone = game.getState().getZone(affectedSourceId); Zone affectedObjectZone = game.getState().getZone(affectedSourceId);
// 603.10. // 603.10.
// ... // ...
@ -1220,32 +1219,50 @@ public abstract class AbilityImpl implements Ability {
// Some zone-change triggers look back in time. These are leaves-the-battlefield abilities, // 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 // 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. // players can see is put into a hand or library.
// TODO: research "leaves a graveyard"
// TODO: research "put into a hand or library"
if (isTriggerCanFireAfterLeaveBattlefield(event)) {
// permanents with normal triggers
if (sourceObject instanceof Permanent) { // TODO: use affectedSourceObject here?
// support leaves-the-battlefield abilities
sourceObjectZone = Zone.BATTLEFIELD;
}
// permanents with continues effects like Yixlid Jailer, see related code "isInUseableZone(game, null"
if (sourceObject == null && this instanceof StaticAbility) {
sourceObjectZone = Zone.BATTLEFIELD;
}
}
// TODO: research use cases and implement shared logic with "looking zone" instead LKI only // 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. // in most use cases it's already supported by event (example: saved permanent object in event's target)
// 603.10c Abilities that trigger specifically when an object becomes unattached look back in time. // [x] 603.10a leaves-the-battlefield abilities and other
// 603.10d Abilities that trigger when a player loses control of an object look back in time. // [ ] 603.10b Abilities that trigger when a permanent phases out look back in time.
// 603.10e Abilities that trigger when a spell is countered look back in time. // [ ] 603.10c Abilities that trigger specifically when an object becomes unattached look back in time.
// 603.10f Abilities that trigger when a player loses the game look back in time. // [ ] 603.10d Abilities that trigger when a player loses control of an object look back in time.
// 603.10g Abilities that trigger when a player planeswalks away from a plane 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(sourceObjectZone); 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?
}
} }
public static boolean isTriggerCanFireAfterLeaveBattlefield(GameEvent event) { 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) { if (event == null) {
return false; return false;
} }
@ -1300,7 +1317,7 @@ public abstract class AbilityImpl implements Ability {
} }
if (object == null) { if (object == null) {
// replacement and other continues effects can be without source, but active (must return true all time) // global replacement and other continues effects can be without source, but active (must return true all time)
return true; return true;
} }