* Exploit - Fixed that Exploit also triggered if the creature with Exploit left the battlefield before the first ability of Exploit resolved.

This commit is contained in:
LevelX2 2015-04-07 00:27:07 +02:00
parent a56cccebf9
commit f62d3ac227
17 changed files with 231 additions and 44 deletions

View file

@ -47,6 +47,7 @@ import mage.constants.EffectType;
import mage.constants.Zone;
import mage.game.Controllable;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.players.Player;
import mage.target.Target;
import mage.target.Targets;
@ -382,10 +383,10 @@ public interface Ability extends Controllable, Serializable {
*
* @param game
* @param source
* @param checkLKI
* @param event
* @return
*/
boolean isInUseableZone(Game game, MageObject source, boolean checkLKI);
boolean isInUseableZone(Game game, MageObject source, GameEvent event);
/**
* Returns true if this ability has to be shown as topmost of all the rules of the object

View file

@ -67,6 +67,7 @@ import mage.constants.Zone;
import mage.game.Game;
import mage.game.command.Emblem;
import mage.game.events.GameEvent;
import mage.game.events.GameEvent.EventType;
import mage.game.events.ManaEvent;
import mage.game.permanent.Permanent;
import mage.game.permanent.PermanentCard;
@ -864,11 +865,10 @@ public abstract class AbilityImpl implements Ability {
*
* @param game
* @param source
* @param checkShortLivingLKI if the object was in the needed zone as the effect that's currently applied started, the check returns true
* @return
*/
@Override
public boolean isInUseableZone(Game game, MageObject source, boolean checkShortLivingLKI) {
public boolean isInUseableZone(Game game, MageObject source, GameEvent event) {
if (zone.equals(Zone.COMMAND)) {
if (this.getSourceId() == null) { // commander effects
return true;
@ -880,17 +880,6 @@ public abstract class AbilityImpl implements Ability {
}
}
// try LKI first (was the object with the id in the needed zone before)
if (checkShortLivingLKI) {
if (game.getShortLivingLKI(getSourceId(), zone)) {
return true;
}
} else {
if (game.getLastKnownInformation(getSourceId(), zone) != null) {
return true;
}
}
MageObject object;
UUID parameterSourceId;
// for singleton abilities like Flying we can't rely on abilities' source because it's only once in continuous effects

View file

@ -28,9 +28,13 @@
package mage.abilities;
import mage.MageObject;
import mage.abilities.effects.Effect;
import mage.constants.AbilityType;
import mage.constants.Zone;
import mage.abilities.effects.Effect;
import mage.game.Game;
import mage.game.events.GameEvent;
/**
*
@ -49,6 +53,14 @@ public abstract class StaticAbility extends AbilityImpl {
}
}
@Override
public boolean isInUseableZone(Game game, MageObject source, GameEvent event) {
if (game.getShortLivingLKI(getSourceId(), zone)) {
return true;
}
return super.isInUseableZone(game, source, event);
}
public StaticAbility(StaticAbility ability) {
super(ability);
}

View file

@ -72,7 +72,7 @@ public class TriggeredAbilities extends ConcurrentHashMap<String, TriggeredAbili
}
// 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, false)) {
if (ability.isInUseableZone(game, object, event)) {
if (!game.getContinuousEffects().preventedByRuleModification(event, ability, game, false)) {
if (object != null) {
boolean controllerSet = false;

View file

@ -35,6 +35,7 @@ import mage.constants.AbilityType;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.events.ZoneChangeEvent;
import mage.players.Player;
/**
@ -158,4 +159,36 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge
return sb.toString();
}
@Override
public boolean isInUseableZone(Game game, MageObject source, 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 opponents 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.
*/
if (event != null) {
switch (event.getType()) {
case ZONE_CHANGE:
if (source == null && ((ZoneChangeEvent)event).getTarget() != null) {
source = ((ZoneChangeEvent)event).getTarget();
}
case DESTROYED_PERMANENT:
// case LOST_CONTROL:
case PHASED_OUT:
case PHASED_IN:
if (game.getLastKnownInformation(getSourceId(), zone) != null) {
return true;
}
}
}
return super.isInUseableZone(game, source, event);
}
}

View file

@ -34,6 +34,7 @@ import mage.abilities.keyword.CyclingAbility;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.events.GameEvent.EventType;
import mage.game.stack.StackObject;
/**
@ -55,7 +56,7 @@ public class CycleTriggeredAbility extends ZoneChangeTriggeredAbility {
}
@Override
public boolean isInUseableZone(Game game, MageObject source, boolean checkLKI) {
public boolean isInUseableZone(Game game, MageObject source, GameEvent event) {
return true;
}

View file

@ -55,7 +55,7 @@ public class DiesTriggeredAbility extends ZoneChangeTriggeredAbility {
}
@Override
public boolean isInUseableZone(Game game, MageObject source, boolean checkLKI) {
public boolean isInUseableZone(Game game, MageObject source, GameEvent event) {
// check it was previously on battlefield
Permanent before = (Permanent) game.getLastKnownInformation(sourceId, Zone.BATTLEFIELD);
// check now it is in graveyard

View file

@ -27,6 +27,7 @@
*/
package mage.abilities.common;
import mage.MageObject;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.Effect;
import mage.constants.SetTargetPointer;
@ -48,7 +49,7 @@ public class ExploitCreatureTriggeredAbility extends TriggeredAbilityImpl {
}
public ExploitCreatureTriggeredAbility(Effect effect, boolean optional, SetTargetPointer setTargetPointer) {
super(Zone.ALL, effect, optional);
super(Zone.BATTLEFIELD, effect, optional);
this.setTargetPointer = setTargetPointer;
}
@ -62,11 +63,20 @@ public class ExploitCreatureTriggeredAbility extends TriggeredAbilityImpl {
return new ExploitCreatureTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.EXPLOITED_CREATURE;
}
@Override
public boolean isInUseableZone(Game game, MageObject source, GameEvent event) {
if (event.getTargetId().equals(getSourceId()) && event.getSourceId().equals(getSourceId())) {
return true; // if Exploits creature sacrifices itself, exploit triggers
}
return super.isInUseableZone(game, source, event);
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
if (event.getSourceId().equals(getSourceId())) {

View file

@ -214,7 +214,7 @@ public class ContinuousEffects implements Serializable {
HashSet<Ability> abilities = layeredEffects.getAbility(effect.getId());
for (Ability ability: abilities) {
// If e.g. triggerd abilities (non static) created the effect, the ability must not be in usable zone (e.g. Unearth giving Haste effect)
if (!(ability instanceof StaticAbility) || ability.isInUseableZone(game, null, true)) {
if (!(ability instanceof StaticAbility) || ability.isInUseableZone(game, null, null)) {
layerEffects.add(effect);
break;
}
@ -268,7 +268,7 @@ public class ContinuousEffects implements Serializable {
HashSet<Ability> abilities = requirementEffects.getAbility(effect.getId());
HashSet<Ability> applicableAbilities = new HashSet<>();
for (Ability ability : abilities) {
if (!(ability instanceof StaticAbility) || ability.isInUseableZone(game, ability instanceof MageSingleton ? permanent : null, false)) {
if (!(ability instanceof StaticAbility) || ability.isInUseableZone(game, ability instanceof MageSingleton ? permanent : null, null)) {
if (effect.applies(permanent, ability, game)) {
applicableAbilities.add(ability);
}
@ -287,7 +287,7 @@ public class ContinuousEffects implements Serializable {
HashSet<Ability> abilities = restrictionEffects.getAbility(effect.getId());
HashSet<Ability> applicableAbilities = new HashSet<>();
for (Ability ability : abilities) {
if (!(ability instanceof StaticAbility) || ability.isInUseableZone(game, ability instanceof MageSingleton ? permanent : null, false)) {
if (!(ability instanceof StaticAbility) || ability.isInUseableZone(game, ability instanceof MageSingleton ? permanent : null, null)) {
if (effect.applies(permanent, ability, game)) {
applicableAbilities.add(ability);
}
@ -306,7 +306,7 @@ public class ContinuousEffects implements Serializable {
HashSet<Ability> abilities = restrictionUntapNotMoreThanEffects.getAbility(effect.getId());
HashSet<Ability> applicableAbilities = new HashSet<>();
for (Ability ability : abilities) {
if (!(ability instanceof StaticAbility) || ability.isInUseableZone(game, null, false)) {
if (!(ability instanceof StaticAbility) || ability.isInUseableZone(game, null, null)) {
if (effect.applies(player, ability, game)) {
applicableAbilities.add(ability);
}
@ -348,7 +348,7 @@ public class ContinuousEffects implements Serializable {
HashSet<Ability> applicableAbilities = new HashSet<>();
for (Ability ability : abilities) {
// for replacment effects of static abilities do not use LKI to check if to apply
if (ability.getAbilityType() != AbilityType.STATIC || ability.isInUseableZone(game, null, true)) {
if (ability.getAbilityType() != AbilityType.STATIC || ability.isInUseableZone(game, null, event)) {
if (effect.getDuration() != Duration.OneUse || !effect.isUsed()) {
if (!game.getScopeRelevant() || effect.hasSelfScope() || !event.getTargetId().equals(ability.getSourceId())) {
if (checkAbilityStillExists(ability, effect, event, game)) { // TODO: This is really needed???
@ -376,7 +376,7 @@ public class ContinuousEffects implements Serializable {
HashSet<Ability> abilities = preventionEffects.getAbility(effect.getId());
HashSet<Ability> applicableAbilities = new HashSet<>();
for (Ability ability : abilities) {
if (ability.getAbilityType() != AbilityType.STATIC || ability.isInUseableZone(game, null, true)) {
if (ability.getAbilityType() != AbilityType.STATIC || ability.isInUseableZone(game, null, event)) {
if (effect.getDuration() != Duration.OneUse || !effect.isUsed()) {
if (effect.applies(event, ability, game)) {
applicableAbilities.add(ability);
@ -443,7 +443,7 @@ public class ContinuousEffects implements Serializable {
for (CostModificationEffect effect: costModificationEffects) {
HashSet<Ability> abilities = costModificationEffects.getAbility(effect.getId());
for (Ability ability : abilities) {
if (!(ability instanceof StaticAbility) || ability.isInUseableZone(game, null, false)) {
if (!(ability instanceof StaticAbility) || ability.isInUseableZone(game, null, null)) {
if (effect.getDuration() != Duration.OneUse || !effect.isUsed()) {
costEffects.add(effect);
break;
@ -466,7 +466,7 @@ public class ContinuousEffects implements Serializable {
for (SpliceCardEffect effect: spliceCardEffects) {
HashSet<Ability> abilities = spliceCardEffects.getAbility(effect.getId());
for (Ability ability : abilities) {
if (ability.getControllerId().equals(playerId) && (!(ability instanceof StaticAbility) || ability.isInUseableZone(game, null, false))) {
if (ability.getControllerId().equals(playerId) && (!(ability instanceof StaticAbility) || ability.isInUseableZone(game, null, null))) {
if (effect.getDuration() != Duration.OneUse || !effect.isUsed()) {
spliceEffects.add(effect);
break;
@ -516,7 +516,7 @@ public class ContinuousEffects implements Serializable {
for (AsThoughEffect effect: asThoughEffectsMap.get(type)) {
HashSet<Ability> abilities = asThoughEffectsMap.get(type).getAbility(effect.getId());
for (Ability ability : abilities) {
if (!(ability instanceof StaticAbility) || ability.isInUseableZone(game, null, false)) {
if (!(ability instanceof StaticAbility) || ability.isInUseableZone(game, null, null)) {
if (effect.getDuration() != Duration.OneUse || !effect.isUsed()) {
asThoughEffectsList.add(effect);
break;
@ -660,7 +660,7 @@ public class ContinuousEffects implements Serializable {
continue;
}
for (Ability sourceAbility : continuousRuleModifyingEffects.getAbility(effect.getId())) {
if (!(sourceAbility instanceof StaticAbility) || sourceAbility.isInUseableZone(game, null, true)) {
if (!(sourceAbility instanceof StaticAbility) || sourceAbility.isInUseableZone(game, null, event)) {
if (checkAbilityStillExists(sourceAbility, effect, event, game)) {
if (effect.getDuration() != Duration.OneUse || !effect.isUsed()) {
effect.setValue("targetAbility", targetAbility);

View file

@ -40,6 +40,7 @@ import mage.constants.Outcome;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.events.GameEvent.EventType;
import mage.game.events.ZoneChangeEvent;
import mage.game.permanent.Permanent;
import mage.target.common.TargetCreaturePermanent;
@ -158,7 +159,7 @@ class HauntExileAbility extends ZoneChangeTriggeredAbility {
}
@Override
public boolean isInUseableZone(Game game, MageObject source, boolean checkLKI) {
public boolean isInUseableZone(Game game, MageObject source, GameEvent event) {
boolean fromOK = true;
if (creatureHaunt) {
// check it was previously on battlefield
@ -209,8 +210,9 @@ class HauntEffect extends OneShotEffect {
game.getState().setValue(key, new FixedTarget(targetPointer.getFirst(game, source)));
card.addInfo("hauntinfo", new StringBuilder("Haunting ").append(hauntedCreature.getLogName()).toString(), game);
hauntedCreature.addInfo("hauntinfo", new StringBuilder("Haunted by ").append(card.getLogName()).toString(), game);
if (!game.isSimulation())
if (!game.isSimulation()) {
game.informPlayers(new StringBuilder(card.getName()).append(" haunting ").append(hauntedCreature.getLogName()).toString());
}
}
return true;
}

View file

@ -56,6 +56,7 @@ import java.util.List;
import java.util.UUID;
import mage.cards.Card;
import mage.constants.AbilityWord;
import mage.game.events.GameEvent;
import mage.players.Player;
import mage.watchers.Watcher;
@ -100,8 +101,9 @@ public class StackAbility implements StackObject, Ability {
if (ability.getTargets().stillLegal(ability, game)) {
return ability.resolve(game);
}
if (!game.isSimulation())
if (!game.isSimulation()) {
game.informPlayers("Ability has been fizzled: " + getRule());
}
counter(null, game);
return false;
}
@ -400,7 +402,7 @@ public class StackAbility implements StackObject, Ability {
}
@Override
public boolean isInUseableZone(Game game, MageObject source, boolean checkLKI) {
public boolean isInUseableZone(Game game, MageObject source, GameEvent event) {
throw new UnsupportedOperationException("Not supported yet.");
}

View file

@ -189,10 +189,10 @@ public class TraceUtil {
for (RestrictionEffect effect: restrictionEffects) {
log.error(uuid+" effect=" + effect.toString() + " id=" + effect.getId());
for (Ability ability : restrictionEffects.getAbility(effect.getId())) {
if (!(ability instanceof StaticAbility) || ability.isInUseableZone(game, permanent, false)) {
if (!(ability instanceof StaticAbility) || ability.isInUseableZone(game, permanent, null)) {
log.error(uuid+" ability=" + ability + ", applies_to_attacker=" + effect.applies(permanent, ability, game));
} else {
boolean usable = ability.isInUseableZone(game, permanent, false);
boolean usable = ability.isInUseableZone(game, permanent, null);
log.error(uuid+" instanceof StaticAbility: " + (ability instanceof StaticAbility) + ", ability=" + ability);
log.error(uuid+" usable zone: " + usable + ", ability=" + ability);
if (!usable) {