mirror of
https://github.com/magefree/mage.git
synced 2026-01-09 20:32:06 -08:00
* 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:
parent
a56cccebf9
commit
f62d3ac227
17 changed files with 231 additions and 44 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 opponent’s 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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())) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue