* Fixed that state triggered abilities were not checked at the correct times.

This commit is contained in:
LevelX2 2015-09-09 00:51:41 +02:00
parent 92f30f3f2f
commit 340398fb74
12 changed files with 217 additions and 97 deletions

View file

@ -27,8 +27,6 @@
*/
package mage.sets.alliances;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import mage.MageInt;
import mage.abilities.Ability;
@ -100,10 +98,7 @@ class PhyrexianDevourerStateTriggeredAbility extends StateTriggeredAbility {
@Override
public boolean checkTrigger(GameEvent event, Game game) {
Permanent permanent = game.getPermanent(getSourceId());
if(permanent != null && permanent.getPower().getValue() >= 7){
return true;
}
return false;
return permanent != null && permanent.getPower().getValue() >= 7;
}
@Override
@ -134,9 +129,9 @@ class PhyrexianDevourerEffect extends OneShotEffect {
Player controller = game.getPlayer(source.getControllerId());
if (controller != null) {
Card card = null;
for (Cost cost: source.getCosts()) {
for (Cost cost : source.getCosts()) {
if (cost instanceof ExileTopCardLibraryCost) {
card = ((ExileTopCardLibraryCost)cost).getCard();
card = ((ExileTopCardLibraryCost) cost).getCard();
}
}
if (card != null) {
@ -170,7 +165,7 @@ class ExileTopCardLibraryCost extends CostImpl {
if (controller != null) {
card = controller.getLibrary().getFromTop(game);
if (card != null) {
paid = controller.moveCardToExileWithInfo(card, null, "", sourceId, game, Zone.LIBRARY, true);
paid = controller.moveCards(card, null, Zone.EXILED, ability, game);
}
}
return paid;

View file

@ -27,15 +27,15 @@
*/
package mage.sets.avacynrestored;
import mage.constants.CardType;
import mage.constants.Rarity;
import java.util.UUID;
import mage.abilities.effects.Effect;
import mage.abilities.effects.common.ExileTargetForSourceEffect;
import mage.abilities.effects.common.ReturnToBattlefieldUnderYourControlTargetEffect;
import mage.cards.CardImpl;
import mage.constants.CardType;
import mage.constants.Rarity;
import mage.target.common.TargetControlledCreaturePermanent;
import java.util.UUID;
/**
*
* @author noxx
@ -46,10 +46,11 @@ public class Cloudshift extends CardImpl {
super(ownerId, 12, "Cloudshift", Rarity.COMMON, new CardType[]{CardType.INSTANT}, "{W}");
this.expansionSetCode = "AVR";
// Exile target creature you control, then return that card to the battlefield under your control.
this.getSpellAbility().addTarget(new TargetControlledCreaturePermanent());
this.getSpellAbility().addEffect(new ExileTargetForSourceEffect());
Effect effect = new ExileTargetForSourceEffect();
effect.setApplyEffectsAfter();
this.getSpellAbility().addEffect(effect);
this.getSpellAbility().addEffect(new ReturnToBattlefieldUnderYourControlTargetEffect(true));
}

View file

@ -35,7 +35,6 @@ import mage.abilities.StateTriggeredAbility;
import mage.abilities.common.AsEntersBattlefieldAbility;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.SacrificeSourceEffect;
import mage.cards.Card;
import mage.cards.CardImpl;
import mage.choices.ChoiceColor;
import mage.constants.CardType;
@ -127,20 +126,16 @@ class LureboundScarecrowTriggeredAbility extends StateTriggeredAbility {
@Override
public boolean checkTrigger(GameEvent event, Game game) {
if (event.getType() == GameEvent.EventType.ZONE_CHANGE || event.getType() == GameEvent.EventType.LOST_CONTROL
|| event.getType() == GameEvent.EventType.COLOR_CHANGED
|| event.getType() == GameEvent.EventType.SPELL_CAST) {
Card card = game.getCard(this.getSourceId());
if (card != null) {
ObjectColor color = (ObjectColor) game.getState().getValue(card.getId() + "_color");
if (color != null) {
for (Permanent perm : game.getBattlefield().getAllActivePermanents(controllerId)) {
if (perm.getColor(game).contains(color)) {
return false;
}
Permanent permanent = game.getPermanent(getSourceId());
if (permanent != null) {
ObjectColor color = (ObjectColor) game.getState().getValue(getSourceId() + "_color");
if (color != null) {
for (Permanent perm : game.getBattlefield().getAllActivePermanents(controllerId)) {
if (perm.getColor(game).contains(color)) {
return false;
}
return true;
}
return true;
}
}
return false;

View file

@ -28,8 +28,6 @@
package mage.sets.shardsofalara;
import java.util.UUID;
import mage.constants.*;
import mage.abilities.Ability;
import mage.abilities.StateTriggeredAbility;
import mage.abilities.common.SimpleActivatedAbility;
@ -42,6 +40,11 @@ import mage.abilities.effects.common.DrawCardSourceControllerEffect;
import mage.abilities.effects.common.SacrificeSourceEffect;
import mage.cards.Card;
import mage.cards.CardImpl;
import mage.constants.CardType;
import mage.constants.Duration;
import mage.constants.Outcome;
import mage.constants.Rarity;
import mage.constants.Zone;
import mage.filter.FilterCard;
import mage.game.Game;
import mage.game.events.GameEvent;
@ -58,7 +61,6 @@ public class ImmortalCoil extends CardImpl {
super(ownerId, 79, "Immortal Coil", Rarity.RARE, new CardType[]{CardType.ARTIFACT}, "{2}{B}{B}");
this.expansionSetCode = "ALA";
// {tap}, Exile two cards from your graveyard: Draw a card.
Ability ability = new SimpleActivatedAbility(Zone.BATTLEFIELD, new DrawCardSourceControllerEffect(1), new TapSourceCost());
ability.addCost(new ExileFromGraveCost(new TargetCardInYourGraveyard(2, new FilterCard("cards from your graveyard"))));
@ -98,10 +100,7 @@ class ImmortalCoilAbility extends StateTriggeredAbility {
@Override
public boolean checkTrigger(GameEvent event, Game game) {
Player player = game.getPlayer(this.getControllerId());
if(player != null && player.getGraveyard().size() == 0){
return true;
}
return false;
return player != null && player.getGraveyard().size() == 0;
}
@Override
@ -140,7 +139,6 @@ class LoseGameEffect extends OneShotEffect {
class PreventAllDamageToControllerEffect extends PreventionEffectImpl {
public PreventAllDamageToControllerEffect() {
super(Duration.WhileOnBattlefield);
staticText = "If damage would be dealt to you, prevent that damage. Exile a card from your graveyard for each 1 damage prevented this way";
@ -166,10 +164,10 @@ class PreventAllDamageToControllerEffect extends PreventionEffectImpl {
if (!game.replaceEvent(preventEvent)) {
int damage = event.getAmount();
Player player = game.getPlayer(source.getControllerId());
if(player != null){
if (player != null) {
TargetCardInYourGraveyard target = new TargetCardInYourGraveyard(Math.min(damage, player.getGraveyard().size()), new FilterCard());
if (target.choose(Outcome.Exile, source.getControllerId(), source.getSourceId(), game)) {
for (UUID targetId: target.getTargets()) {
for (UUID targetId : target.getTargets()) {
Card card = player.getGraveyard().get(targetId, game);
if (card != null) {
card.moveToZone(Zone.EXILED, source.getSourceId(), game, false);
@ -186,7 +184,7 @@ class PreventAllDamageToControllerEffect extends PreventionEffectImpl {
@Override
public boolean applies(GameEvent event, Ability source, Game game) {
if (super.applies(event, source, game)) {
if (event.getTargetId().equals(source.getControllerId())){
if (event.getTargetId().equals(source.getControllerId())) {
return true;
}

View file

@ -0,0 +1,71 @@
/*
* Copyright 2010 BetaSteward_at_googlemail.com. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other materials
* provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY BetaSteward_at_googlemail.com ``AS IS'' AND ANY EXPRESS OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BetaSteward_at_googlemail.com OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* The views and conclusions contained in the software and documentation are those of the
* authors and should not be interpreted as representing official policies, either expressed
* or implied, of BetaSteward_at_googlemail.com.
*/
package org.mage.test.cards.triggers.state;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
*
* @author LevelX2
*/
public class PhyrexianDevourerTest extends CardTestPlayerBase {
/**
* Check that Phyrexian Devourer is sacrifriced as soon as the counters are
* added
*
*/
@Test
public void testBoostChecked() {
// When Phyrexian Devourer's power is 7 or greater, sacrifice it.
// Exile the top card of your library: Put X +1/+1 counters on Phyrexian Devourer, where X is the exiled card's converted mana cost.
addCard(Zone.BATTLEFIELD, playerA, "Phyrexian Devourer");
addCard(Zone.LIBRARY, playerA, "Phyrexian Devourer");
skipInitShuffling();
addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion");
attack(2, playerB, "Silvercoat Lion");
block(2, playerA, "Phyrexian Devourer", "Silvercoat Lion");
activateAbility(2, PhaseStep.DECLARE_BLOCKERS, playerA, "Exile the top card of your library");
setStopAt(2, PhaseStep.POSTCOMBAT_MAIN);
execute();
assertGraveyardCount(playerA, "Phyrexian Devourer", 1);
assertExileCount("Phyrexian Devourer", 1);
assertPermanentCount(playerB, "Silvercoat Lion", 1);
}
}

View file

@ -92,4 +92,51 @@ public class AnafenzaTest extends CardTestCommanderDuelBase {
}
/**
* Token don't go to exile because they are no creature cards
*/
@Test
public void testAnafenzaExileInCombatOmnathToken() {
addCard(Zone.BATTLEFIELD, playerA, "Plains", 1);
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1);
addCard(Zone.BATTLEFIELD, playerA, "Forest", 1);
addCard(Zone.BATTLEFIELD, playerA, "Acidic Slime", 1);
addCard(Zone.HAND, playerB, "Forest", 2);
// <i>Landfall</i> - Whenever a land enters the battlefield under your control, put a 5/5 red and green Elemental creature token onto the battlefield.
// Whenever Omnath, Locus of Rage or another Elemental you control dies, Omnath deals 3 damage to target creature or player.
addCard(Zone.BATTLEFIELD, playerB, "Omnath, Locus of Rage", 1);
// Whenever Anafenza, the Foremost attacks, put a +1/+1 counter on another target tapped creature you control.
// If a creature card would be put into an opponent's graveyard from anywhere, exile it instead.
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Anafenza, the Foremost");
playLand(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Forest");
playLand(4, PhaseStep.PRECOMBAT_MAIN, playerB, "Forest");
attack(5, playerA, "Acidic Slime");
block(5, playerB, "Elemental", "Acidic Slime");
attack(5, playerA, "Anafenza, the Foremost");
block(5, playerB, "Elemental", "Anafenza, the Foremost");
addTarget(playerB, playerA);
setStopAt(5, PhaseStep.POSTCOMBAT_MAIN);
execute();
assertExileCount(playerA, 0);
assertExileCount(playerB, 0);
assertPermanentCount(playerB, "Elemental", 1);
assertGraveyardCount(playerA, "Acidic Slime", 1);
assertGraveyardCount(playerA, "Anafenza, the Foremost", 0);
assertCommandZoneCount(playerA, "Anafenza, the Foremost", 1);
assertHandCount(playerA, 2); // turn 3 + 5 draw
assertHandCount(playerB, 2); // turn 2 + 4 draw
assertLife(playerA, 37);
assertLife(playerB, 40);
}
}

View file

@ -585,7 +585,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
actualCount++;
}
}
Assert.assertEquals("(Battlefield) Card counts are not equal (" + commandZoneObjectName + ")", count, actualCount);
Assert.assertEquals("(Command Zone) Card counts are not equal (" + commandZoneObjectName + ")", count, actualCount);
}
/**
@ -777,7 +777,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
*/
public void assertHandCount(Player player, int count) throws AssertionError {
int actual = currentGame.getPlayer(player.getId()).getHand().size();
Assert.assertEquals("(Hand) Card counts are not equal ", count, actual);
Assert.assertEquals("(Hand " + player.getName() + ") Card counts are not equal ", count, actual);
}
/**

View file

@ -237,6 +237,7 @@ public abstract class AbilityImpl implements Ability {
*/
if (effect.applyEffectsAfter()) {
game.applyEffects();
game.getState().getTriggers().checkStateTriggers(game);
}
}
}

View file

@ -25,7 +25,6 @@
* authors and should not be interpreted as representing official policies, either expressed
* or implied, of BetaSteward_at_googlemail.com.
*/
package mage.abilities;
import java.util.UUID;
@ -48,8 +47,7 @@ public abstract class StateTriggeredAbility extends TriggeredAbilityImpl {
super(ability);
}
@Override
public final boolean checkEventType(GameEvent event, Game game) {
public boolean canTrigger(Game game) {
//20100716 - 603.8
Boolean triggered = (Boolean) game.getState().getValue(getSourceId().toString() + "triggered");
if (triggered == null) {
@ -58,6 +56,10 @@ public abstract class StateTriggeredAbility extends TriggeredAbilityImpl {
return !triggered;
}
@Override
public final boolean checkEventType(GameEvent event, Game game) {
return false;
}
@Override
public void trigger(Game game, UUID controllerId) {

View file

@ -69,45 +69,57 @@ public class TriggeredAbilities extends ConcurrentHashMap<String, TriggeredAbili
}
}
public void checkStateTriggers(Game game) {
for (Iterator<TriggeredAbility> it = this.values().iterator(); it.hasNext();) {
TriggeredAbility ability = it.next();
if (ability instanceof StateTriggeredAbility && ((StateTriggeredAbility) ability).canTrigger(game)) {
checkTrigger(ability, null, game);
}
}
}
public void checkTriggers(GameEvent event, Game game) {
for (Iterator<TriggeredAbility> it = this.values().iterator(); it.hasNext();) {
TriggeredAbility ability = it.next();
if (!ability.checkEventType(event, game)) {
continue;
if (ability.checkEventType(event, game)) {
checkTrigger(ability, event, game);
}
// for effects like when leaves battlefield or destroyed use ShortLKI to check if permanent was in the correct zone before (e.g. Oblivion Ring or Karmic Justice)
MageObject object = game.getObject(ability.getSourceId());
if (ability.isInUseableZone(game, object, event)) {
if (!game.getContinuousEffects().preventedByRuleModification(event, ability, game, false)) {
if (object != null) {
boolean controllerSet = false;
if (!ability.getZone().equals(Zone.COMMAND) && event.getTargetId() != null && event.getTargetId().equals(ability.getSourceId())
&& (event.getType().equals(EventType.ZONE_CHANGE) || event.getType().equals(EventType.DESTROYED_PERMANENT))) {
// need to check if object was face down for dies and destroy events because the ability triggers in the new zone, zone counter -1 is used
Permanent permanent = (Permanent) game.getLastKnownInformation(ability.getSourceId(), Zone.BATTLEFIELD, ability.getSourceObjectZoneChangeCounter() - 1);
if (permanent != null) {
if (!ability.getWorksFaceDown() && permanent.isFaceDown(game)) {
continue;
}
controllerSet = true;
ability.setControllerId(permanent.getControllerId());
}
}
if (!controllerSet) {
if (object instanceof Permanent) {
ability.setControllerId(((Permanent) object).getControllerId());
} else if (object instanceof Spell) {
// needed so that cast triggered abilities have to correct controller (e.g. Ulamog, the Infinite Gyre).
ability.setControllerId(((Spell) object).getControllerId());
} else if (object instanceof Card) {
ability.setControllerId(((Card) object).getOwnerId());
}
}
}
}
}
if (ability.checkTrigger(event, game)) {
ability.trigger(game, ability.getControllerId());
private void checkTrigger(TriggeredAbility ability, GameEvent event, Game game) {
// for effects like when leaves battlefield or destroyed use ShortLKI to check if permanent was in the correct zone before (e.g. Oblivion Ring or Karmic Justice)
MageObject object = game.getObject(ability.getSourceId());
if (ability.isInUseableZone(game, object, event)) {
if (event == null || !game.getContinuousEffects().preventedByRuleModification(event, ability, game, false)) {
if (object != null) {
boolean controllerSet = false;
if (!ability.getZone().equals(Zone.COMMAND) && event != null && event.getTargetId() != null && event.getTargetId().equals(ability.getSourceId())
&& (event.getType().equals(EventType.ZONE_CHANGE) || event.getType().equals(EventType.DESTROYED_PERMANENT))) {
// need to check if object was face down for dies and destroy events because the ability triggers in the new zone, zone counter -1 is used
Permanent permanent = (Permanent) game.getLastKnownInformation(ability.getSourceId(), Zone.BATTLEFIELD, ability.getSourceObjectZoneChangeCounter() - 1);
if (permanent != null) {
if (!ability.getWorksFaceDown() && permanent.isFaceDown(game)) {
return;
}
controllerSet = true;
ability.setControllerId(permanent.getControllerId());
}
}
if (!controllerSet) {
if (object instanceof Permanent) {
ability.setControllerId(((Permanent) object).getControllerId());
} else if (object instanceof Spell) {
// needed so that cast triggered abilities have to correct controller (e.g. Ulamog, the Infinite Gyre).
ability.setControllerId(((Spell) object).getControllerId());
} else if (object instanceof Card) {
ability.setControllerId(((Card) object).getOwnerId());
}
}
}
if (ability.checkTrigger(event, game)) {
ability.trigger(game, ability.getControllerId());
}
}
}

View file

@ -28,9 +28,9 @@
package mage.abilities.common;
import mage.MageObject;
import mage.constants.Zone;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.Effect;
import mage.constants.Zone;
import mage.filter.common.FilterCreaturePermanent;
import mage.game.Game;
import mage.game.events.GameEvent;
@ -86,19 +86,16 @@ public class DiesThisOrAnotherCreatureTriggeredAbility extends TriggeredAbilityI
public boolean checkTrigger(GameEvent event, Game game) {
ZoneChangeEvent zEvent = (ZoneChangeEvent) event;
if (game.getPermanent(sourceId) == null) {
if (game.getLastKnownInformation(sourceId, Zone.BATTLEFIELD) == null) {
return false;
}
if (game.getPermanentOrLKIBattlefield(getSourceId()) == null) {
return false;
}
if (zEvent.getFromZone() == Zone.BATTLEFIELD && zEvent.getToZone() == Zone.GRAVEYARD) {
Permanent permanent = (Permanent) game.getLastKnownInformation(event.getTargetId(), Zone.BATTLEFIELD);
if (permanent != null) {
if (permanent.getId().equals(this.getSourceId())) {
if (zEvent.getTarget() != null) {
if (zEvent.getTarget().getId().equals(this.getSourceId())) {
return true;
} else {
if (filter.match(permanent, sourceId, controllerId, game)) {
if (filter.match(zEvent.getTarget(), sourceId, controllerId, game)) {
return true;
}
}

View file

@ -1464,6 +1464,7 @@ public abstract class GameImpl implements Game, Serializable {
*/
public boolean checkTriggered() {
boolean played = false;
state.getTriggers().checkStateTriggers(this);
for (UUID playerId : state.getPlayerList(state.getActivePlayerId())) {
Player player = getPlayer(playerId);
while (player.isInGame()) { // player can die or win caused by triggered abilities or leave the game