mirror of
https://github.com/magefree/mage.git
synced 2025-12-20 02:30:08 -08:00
Triggered abilities - fixed that it can trigger from hand or library (related to #13089, regression from #13088)
This commit is contained in:
parent
78913ac84b
commit
c6bec887b9
2 changed files with 74 additions and 33 deletions
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 (affectedSourceId == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// emblems/dungeons/planes effects (works all the time, store in command zone)
|
||||||
if (zone == Zone.COMMAND) {
|
if (zone == Zone.COMMAND) {
|
||||||
if (affectedSourceId == null) {
|
if (affectedSourceObject instanceof Emblem || affectedSourceObject instanceof Dungeon || affectedSourceObject instanceof Plane) {
|
||||||
// commander effects
|
|
||||||
return true;
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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"
|
// TODO: research use cases and implement shared logic with "looking zone" instead LKI only
|
||||||
if (isTriggerCanFireAfterLeaveBattlefield(event)) {
|
// in most use cases it's already supported by event (example: saved permanent object in event's target)
|
||||||
// permanents with normal triggers
|
// [x] 603.10a leaves-the-battlefield abilities and other
|
||||||
if (sourceObject instanceof Permanent) { // TODO: use affectedSourceObject here?
|
// [ ] 603.10b Abilities that trigger when a permanent phases out look back in time.
|
||||||
// support leaves-the-battlefield abilities
|
// [ ] 603.10c Abilities that trigger specifically when an object becomes unattached look back in time.
|
||||||
sourceObjectZone = Zone.BATTLEFIELD;
|
// [ ] 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.
|
||||||
// permanents with continues effects like Yixlid Jailer, see related code "isInUseableZone(game, null"
|
// [ ] 603.10f Abilities that trigger when a player loses the game look back in time.
|
||||||
if (sourceObject == null && this instanceof StaticAbility) {
|
// [ ] 603.10g Abilities that trigger when a player planeswalks away from a plane look back in time.
|
||||||
sourceObjectZone = Zone.BATTLEFIELD;
|
|
||||||
|
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?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: research use cases and implement shared logic with "looking zone" instead LKI only
|
return zone.match(affectedObjectZone);
|
||||||
// 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(sourceObjectZone);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isTriggerCanFireAfterLeaveBattlefield(GameEvent event) {
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue