* Arashin Sovereign - Fixed that the player had no option to let the card go to the graveyard.

This commit is contained in:
LevelX2 2015-04-03 16:28:40 +02:00
parent 5dc6f1a7c4
commit dbb9be6703
173 changed files with 873 additions and 506 deletions

View file

@ -32,7 +32,6 @@ import java.util.UUID;
import mage.cards.Card;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.game.stack.Spell;
/**
* A object reference that takes zone changes into account.
@ -65,10 +64,11 @@ public class MageObjectReference implements Comparable<MageObjectReference> {
public MageObjectReference(UUID sourceId, Game game) {
this.sourceId = sourceId;
MageObject mageObject = game.getObject(sourceId);
if (mageObject != null)
if (mageObject != null) {
this.zoneChangeCounter = mageObject.getZoneChangeCounter(game);
else
this.zoneChangeCounter = 0;
} else {
throw new IllegalArgumentException("The provided sourceId is not connected to an object in the game");
}
}
public UUID getSourceId() {
@ -109,7 +109,10 @@ public class MageObjectReference implements Comparable<MageObjectReference> {
}
public boolean refersTo(MageObject mageObject, Game game) {
return mageObject.getId().equals(sourceId) && this.zoneChangeCounter == mageObject.getZoneChangeCounter(game);
if (mageObject != null) {
return mageObject.getId().equals(sourceId) && this.zoneChangeCounter == mageObject.getZoneChangeCounter(game);
}
return false;
}
public Permanent getPermanent(Game game) {

View file

@ -499,8 +499,9 @@ public interface Ability extends Controllable, Serializable {
* an ability was activated.
*
* @param mageObject
* @param game
*/
void setSourceObject(MageObject mageObject);
void setSourceObject(MageObject mageObject, Game game);
/**
* Returns the object that actually existed while a ability triggerd or
@ -511,4 +512,17 @@ public interface Ability extends Controllable, Serializable {
* @return
*/
MageObject getSourceObject(Game game);
int getSourceObjectZoneChangeCounter();
/**
* Returns the object that actually existed while a ability triggerd or
* an ability was activated only if it has not changed zone meanwhile.
* If not set yet, the current object will be retrieved from the game.
*
* @param game
* @return
*/
MageObject getSourceObjectIfItStillExists(Game game);
}

View file

@ -32,6 +32,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import mage.MageObject;
import mage.MageObjectReference;
import mage.Mana;
import mage.abilities.costs.AdjustingSourceCosts;
import mage.abilities.costs.AlternativeCost;
@ -109,6 +110,7 @@ public abstract class AbilityImpl implements Ability {
protected boolean activated = false;
protected boolean worksFaceDown = false;
protected MageObject sourceObject;
protected int sourceObjectZoneChangeCounter;
protected List<Watcher> watchers = null;
protected List<Ability> subAbilities = null;
@ -160,6 +162,7 @@ public abstract class AbilityImpl implements Ability {
this.worksFaceDown = ability.worksFaceDown;
this.abilityWord = ability.abilityWord;
this.sourceObject = ability.sourceObject;
this.sourceObjectZoneChangeCounter = ability.sourceObjectZoneChangeCounter;
}
@Override
@ -213,6 +216,13 @@ public abstract class AbilityImpl implements Ability {
else {
game.addEffect((ContinuousEffect) effect, this);
}
/**
* game.applyEffects() has to be done at least for every effect that moves cards/permanent between zones,
* so Static effects work as intened if dependant from the moved objects zone it is in
* Otherwise for example were static abilities with replacement effects deactivated to late
* Example: {@link org.mage.test.cards.replacement.DryadMilitantTest#testDiesByDestroy testDiesByDestroy}
*/
// game.applyEffects();
// some effects must be applied before next effect is resolved, because effect is dependend.
if (effect.applyEffectsAfter()) {
game.applyEffects();
@ -236,6 +246,8 @@ public abstract class AbilityImpl implements Ability {
return false;
}
getSourceObject(game);
/* 20130201 - 601.2b
* If the player wishes to splice any cards onto the spell (see rule 702.45), he
* or she reveals those cards in his or her hand.
@ -243,13 +255,13 @@ public abstract class AbilityImpl implements Ability {
if (this.abilityType.equals(AbilityType.SPELL)) {
game.getContinuousEffects().applySpliceEffects(this, game);
}
// TODO: Because all (non targeted) choices have to be done during resolution
// this has to be removed, if all using effects are changed
sourceObject = this.getSourceObject(game);
if (sourceObject != null) {
sourceObject.adjustChoices(this, game);
}
// TODO: Because all (non targeted) choices have to be done during resolution
// this has to be removed, if all using effects are changed
for (UUID modeId :this.getModes().getSelectedModes()) {
this.getModes().setMode(this.getModes().get(modeId));
if (getChoices().size() > 0 && getChoices().choose(game, this) == false) {
@ -660,16 +672,18 @@ public abstract class AbilityImpl implements Ability {
@Override
public List<Watcher> getWatchers() {
if (watchers != null)
if (watchers != null) {
return watchers;
else
} else {
return emptyWatchers;
}
}
@Override
public void addWatcher(Watcher watcher) {
if (watchers == null)
if (watchers == null) {
watchers = new ArrayList<>();
}
watcher.setSourceId(this.sourceId);
watcher.setControllerId(this.controllerId);
watchers.add(watcher);
@ -677,16 +691,18 @@ public abstract class AbilityImpl implements Ability {
@Override
public List<Ability> getSubAbilities() {
if (subAbilities != null)
if (subAbilities != null) {
return subAbilities;
else
} else {
return emptyAbilities;
}
}
@Override
public void addSubAbility(Ability ability) {
if (subAbilities == null)
if (subAbilities == null) {
subAbilities = new ArrayList<>();
}
ability.setSourceId(this.sourceId);
ability.setControllerId(this.controllerId);
subAbilities.add(ability);
@ -842,8 +858,15 @@ public abstract class AbilityImpl implements Ability {
return false;
}
/**
*
* @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 checkLKI) {
public boolean isInUseableZone(Game game, MageObject source, boolean checkShortLivingLKI) {
if (zone.equals(Zone.COMMAND)) {
if (this.getSourceId() == null) { // commander effects
return true;
@ -855,10 +878,13 @@ public abstract class AbilityImpl implements Ability {
}
}
// try LKI first
if (checkLKI) {
MageObject lkiTest = game.getShortLivingLKI(getSourceId(), zone);
if (lkiTest != null) {
// 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;
}
}
@ -1097,16 +1123,38 @@ public abstract class AbilityImpl implements Ability {
@Override
public MageObject getSourceObject(Game game) {
if (sourceObject != null) {
return sourceObject;
} else {
return game.getObject(sourceId);
if (sourceObject == null) {
setSourceObject(null, game);
}
return sourceObject;
}
@Override
public void setSourceObject(MageObject sourceObject) {
this.sourceObject = sourceObject;
public MageObject getSourceObjectIfItStillExists(Game game) {
MageObject currentObject = game.getObject(getSourceId());
if (currentObject != null) {
MageObjectReference mor = new MageObjectReference(currentObject, game);
if (mor.getZoneChangeCounter() == getSourceObjectZoneChangeCounter()) {
// source object has meanwhile not changed zone
return sourceObject;
}
}
return null;
}
@Override
public int getSourceObjectZoneChangeCounter() {
return sourceObjectZoneChangeCounter;
}
@Override
public void setSourceObject(MageObject sourceObject, Game game) {
if (sourceObject == null) {
this.sourceObject = game.getObject(sourceId);
} else {
this.sourceObject = sourceObject;
}
this.sourceObjectZoneChangeCounter = game.getState().getZoneChangeCounter(sourceId);
}

View file

@ -1,32 +1,31 @@
/*
* 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.
*/
* 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 mage.abilities;
import java.util.ArrayList;
@ -38,6 +37,7 @@ import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import mage.MageObject;
import mage.cards.Card;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.GameEvent;
@ -45,17 +45,18 @@ import mage.game.events.GameEvent.EventType;
import mage.game.permanent.Permanent;
/**
*
* @author BetaSteward_at_googlemail.com
*/
*
* @author BetaSteward_at_googlemail.com
*/
public class TriggeredAbilities extends ConcurrentHashMap<String, TriggeredAbility> {
private final Map<String, List<UUID>> sources = new HashMap<>();
public TriggeredAbilities() {}
public TriggeredAbilities() {
}
public TriggeredAbilities(final TriggeredAbilities abilities) {
for (Map.Entry<String, TriggeredAbility> entry: abilities.entrySet()) {
for (Map.Entry<String, TriggeredAbility> entry : abilities.entrySet()) {
this.put(entry.getKey(), entry.getValue().copy());
}
for (Map.Entry<String, List<UUID>> entry : abilities.sources.entrySet()) {
@ -70,29 +71,34 @@ public class TriggeredAbilities extends ConcurrentHashMap<String, TriggeredAbili
continue;
}
// 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)
if (ability.isInUseableZone(game, ability.getSourceObject(game), event.getType().equals(EventType.ZONE_CHANGE) || event.getType().equals(EventType.DESTROYED_PERMANENT))) {
MageObject object = game.getObject(ability.getSourceId());
if (ability.isInUseableZone(game, object, false)) {
if (!game.getContinuousEffects().preventedByRuleModification(event, ability, game, false)) {
MageObject object = null;
if (!ability.getZone().equals(Zone.COMMAND) && !game.getState().getZone(ability.getSourceId()).equals(ability.getZone())) {
object = game.getShortLivingLKI(ability.getSourceId(), ability.getZone());
}
if (object == null) {
object = getMageObject(event, game, ability);
if (object != null) {
boolean controllerSet = false;
if (!ability.getZone().equals(Zone.COMMAND) && (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 Card) {
ability.setControllerId(((Card) object).getOwnerId());
}
}
}
if (object != null) {
if (object instanceof Permanent) {
if (((Permanent)object).isFaceDown(game) && !ability.getWorksFaceDown()) {
continue;
}
ability.setControllerId(((Permanent) object).getControllerId());
}
ability.setSourceObject(object);
if (ability.checkTrigger(event, game)) {
UUID controllerId = ability.getControllerId();
ability.trigger(game, controllerId);
}
// ability.setSourceObject(object);
if (ability.checkTrigger(event, game)) {
ability.trigger(game, ability.getControllerId());
}
}
}
@ -120,8 +126,7 @@ public class TriggeredAbilities extends ConcurrentHashMap<String, TriggeredAbili
public void add(TriggeredAbility ability, UUID sourceId, MageObject attachedTo) {
if (sourceId == null) {
add(ability, attachedTo);
}
else {
} else {
this.add(ability, attachedTo);
List<UUID> uuidList = new LinkedList<>();
uuidList.add(sourceId);
@ -145,18 +150,18 @@ public class TriggeredAbilities extends ConcurrentHashMap<String, TriggeredAbili
public void removeAbilitiesOfSource(UUID sourceId) {
List<String> keysToRemove = new ArrayList<>();
for (String key: this.keySet()) {
if(key.endsWith(sourceId.toString())) {
for (String key : this.keySet()) {
if (key.endsWith(sourceId.toString())) {
keysToRemove.add(key);
}
}
for(String key: keysToRemove) {
for (String key : keysToRemove) {
remove(key);
}
}
public void removeAllGainedAbilities() {
for(String key: sources.keySet()) {
for (String key : sources.keySet()) {
this.remove(key);
}
sources.clear();
@ -166,4 +171,4 @@ public class TriggeredAbilities extends ConcurrentHashMap<String, TriggeredAbili
return new TriggeredAbilities(this);
}
}
}

View file

@ -66,7 +66,7 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge
public void trigger(Game game, UUID controllerId) {
//20091005 - 603.4
if (checkInterveningIfClause(game)) {
this.controllerId = controllerId;
setSourceObject(null, game); // set the source object the time the trigger goes off
game.addTriggeredAbility(this);
}
}

View file

@ -49,7 +49,7 @@ public class ZoneChangeTriggeredAbility extends TriggeredAbilityImpl {
protected String rule;
public ZoneChangeTriggeredAbility(Zone fromZone, Zone toZone, Effect effect, String rule, boolean optional) {
this(fromZone, fromZone, toZone, effect, rule, optional);
this(toZone == null ? Zone.ALL : toZone, fromZone, toZone, effect, rule, optional);
}
public ZoneChangeTriggeredAbility(Zone worksInZone, Zone fromZone, Zone toZone, Effect effect, String rule, boolean optional) {

View file

@ -41,6 +41,7 @@ import mage.game.ExileZone;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.events.ZoneChangeEvent;
import mage.game.permanent.PermanentToken;
import mage.players.Player;
import mage.util.CardUtil;
@ -107,7 +108,8 @@ class ReturnExiledPermanentsEffect extends OneShotEffect {
Player controller = game.getPlayer(source.getControllerId());
MageObject sourceObject = source.getSourceObject(game);
if (sourceObject != null && controller != null) {
UUID exileZone = CardUtil.getObjectExileZoneId(game, sourceObject);
int zoneChangeCounter = (sourceObject instanceof PermanentToken) ? source.getSourceObjectZoneChangeCounter() : source.getSourceObjectZoneChangeCounter() -1;
UUID exileZone = CardUtil.getExileZoneId(game, source.getSourceId(), zoneChangeCounter);
if (exileZone != null) {
ExileZone exile = game.getExile().getExileZone(exileZone);
if (exile != null) {

View file

@ -29,11 +29,14 @@
package mage.abilities.costs.common;
import java.util.UUID;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.costs.CostImpl;
import mage.cards.Card;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.util.CardUtil;
/**
*
@ -41,19 +44,38 @@ import mage.game.permanent.Permanent;
*/
public class ExileSourceCost extends CostImpl {
private boolean toUniqueExileZone;
public ExileSourceCost() {
this.text = "Exile {this}";
}
/**
*
* @param toUniqueExileZone moves the card to a source object dependant unique exile zone, so another
* effect of the same source object (e.g. Deadeye Navigator) can identify the card
*/
public ExileSourceCost(boolean toUniqueExileZone) {
this.text = "Exile {this}";
this.toUniqueExileZone = toUniqueExileZone;
}
public ExileSourceCost(ExileSourceCost cost) {
super(cost);
this.toUniqueExileZone = cost.toUniqueExileZone;
}
@Override
public boolean pay(Ability ability, Game game, UUID sourceId, UUID controllerId, boolean noMana) {
Permanent permanent = game.getPermanent(sourceId);
if (permanent != null) {
paid = permanent.moveToExile(null, "", sourceId, game);
MageObject sourceObject = ability.getSourceObject(game);
Player controller = game.getPlayer(controllerId);
if (controller != null && sourceObject != null && (sourceObject instanceof Card)) {
UUID exileZoneId = null;
String exileZoneName = "";
if (toUniqueExileZone) {
exileZoneId = CardUtil.getExileZoneId(game, ability.getSourceId(), ability.getSourceObjectZoneChangeCounter());
exileZoneName = sourceObject.getLogName();
}
paid = controller.moveCardToExileWithInfo((Card) sourceObject, exileZoneId, exileZoneName, sourceId, game, game.getState().getZone(sourceObject.getId()));
}
return paid;
}
@ -61,10 +83,7 @@ public class ExileSourceCost extends CostImpl {
@Override
public boolean canPay(Ability ability, UUID sourceId, UUID controllerId, Game game) {
Permanent permanent = game.getPermanent(sourceId);
if (permanent != null) {
return true;
}
return false;
return permanent != null;
}
@Override

View file

@ -78,7 +78,11 @@ public class GenericManaCost extends ManaCostImpl {
@Override
public GenericManaCost getUnpaid() {
return new GenericManaCost(mana - this.payment.count());
GenericManaCost unpaid = new GenericManaCost(mana - this.payment.count());
if (sourceFilter != null) {
unpaid.setSourceFilter(sourceFilter);
}
return unpaid;
}
@Override

View file

@ -49,6 +49,7 @@ import mage.abilities.StaticAbility;
import mage.abilities.keyword.SpliceOntoArcaneAbility;
import mage.cards.Cards;
import mage.cards.CardsImpl;
import mage.constants.AbilityType;
import mage.constants.AsThoughEffectType;
import mage.constants.CostModificationType;
import mage.constants.Duration;
@ -213,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, false)) {
if (!(ability instanceof StaticAbility) || ability.isInUseableZone(game, null, true)) {
layerEffects.add(effect);
break;
}
@ -332,7 +333,7 @@ public class ContinuousEffects implements Serializable {
if(auraReplacementEffect.checksEventType(event, game) && auraReplacementEffect.applies(event, null, game)){
replaceEffects.put(auraReplacementEffect, null);
}
boolean checkLKI = event.getType().equals(EventType.ZONE_CHANGE) || event.getType().equals(EventType.DESTROYED_PERMANENT);
// boolean checkLKI = event.getType().equals(EventType.ZONE_CHANGE) || event.getType().equals(EventType.DESTROYED_PERMANENT);
//get all applicable transient Replacement effects
for (ReplacementEffect effect: replacementEffects) {
if (!effect.checksEventType(event, game)) {
@ -346,10 +347,11 @@ public class ContinuousEffects implements Serializable {
HashSet<Ability> abilities = replacementEffects.getAbility(effect.getId());
HashSet<Ability> applicableAbilities = new HashSet<>();
for (Ability ability : abilities) {
if (!(ability instanceof StaticAbility) || ability.isInUseableZone(game, null, checkLKI)) {
// 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 (effect.getDuration() != Duration.OneUse || !effect.isUsed()) {
if (!game.getScopeRelevant() || effect.hasSelfScope() || !event.getTargetId().equals(ability.getSourceId())) {
if (checkAbilityStillExists(ability, effect, event, game)) {
if (checkAbilityStillExists(ability, effect, event, game)) { // TODO: This is really needed???
if (effect.applies(event, ability, game)) {
applicableAbilities.add(ability);
}
@ -374,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 instanceof StaticAbility) || ability.isInUseableZone(game, null, false)) {
if (ability.getAbilityType() != AbilityType.STATIC || ability.isInUseableZone(game, null, true)) {
if (effect.getDuration() != Duration.OneUse || !effect.isUsed()) {
if (effect.applies(event, ability, game)) {
applicableAbilities.add(ability);
@ -658,7 +660,7 @@ public class ContinuousEffects implements Serializable {
continue;
}
for (Ability sourceAbility : continuousRuleModifyingEffects.getAbility(effect.getId())) {
if (!(sourceAbility instanceof StaticAbility) || sourceAbility.isInUseableZone(game, null, false)) {
if (!(sourceAbility instanceof StaticAbility) || sourceAbility.isInUseableZone(game, null, true)) {
if (checkAbilityStillExists(sourceAbility, effect, event, game)) {
if (effect.getDuration() != Duration.OneUse || !effect.isUsed()) {
effect.setValue("targetAbility", targetAbility);

View file

@ -74,6 +74,11 @@ public abstract class ReplacementEffectImpl extends ContinuousEffectImpl impleme
return selfScope;
}
@Override
public boolean apply(Game game, Ability source) {
throw new UnsupportedOperationException("Not used for replacemnt effect.");
}
@Override
public boolean checksEventType(GameEvent event, Game game) {
return true;

View file

@ -70,7 +70,7 @@ public class CreateDelayedTriggeredAbilityEffect extends OneShotEffect {
DelayedTriggeredAbility delayedAbility = ability.copy();
delayedAbility.setSourceId(source.getSourceId());
delayedAbility.setControllerId(source.getControllerId());
delayedAbility.setSourceObject(source.getSourceObject(game));
delayedAbility.setSourceObject(source.getSourceObject(game), game);
if (this.copyTargets) {
if (source.getTargets().isEmpty()) {
for(Effect effect : delayedAbility.getEffects()) {

View file

@ -28,14 +28,15 @@
package mage.abilities.effects.common;
import java.util.UUID;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.effects.OneShotEffect;
import mage.cards.Card;
import mage.constants.Outcome;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.util.CardUtil;
/**
*
@ -43,21 +44,26 @@ import mage.players.Player;
*/
public class ExileSourceEffect extends OneShotEffect {
private Zone onlyfromZone;
private boolean toUniqueExileZone;
public ExileSourceEffect() {
this(Zone.ALL);
this(false);
}
public ExileSourceEffect(Zone onlyFromZone) {
/**
*
* @param toUniqueExileZone moves the card to a source object dependant unique exile zone, so another
* effect of the same source object (e.g. Deadeye Navigator) can identify the card
*/
public ExileSourceEffect(boolean toUniqueExileZone) {
super(Outcome.Exile);
staticText = "Exile {this}";
this.onlyfromZone = onlyFromZone;
this.toUniqueExileZone = toUniqueExileZone;
}
public ExileSourceEffect(final ExileSourceEffect effect) {
super(effect);
this.onlyfromZone = effect.onlyfromZone;
this.toUniqueExileZone = effect.toUniqueExileZone;
}
@Override
@ -66,22 +72,22 @@ public class ExileSourceEffect extends OneShotEffect {
}
@Override
public boolean apply(Game game, Ability source) {
Zone zone = game.getState().getZone(source.getSourceId());
if (!zone.match(onlyfromZone)) {
return false;
}
Permanent permanent = game.getPermanent(source.getSourceId());
public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId());
if (permanent != null) {
return controller.moveCardToExileWithInfo(permanent, null, null, source.getSourceId(), game, zone);
} else {
Card card = game.getCard(source.getSourceId());
if (card != null) {
return controller.moveCardToExileWithInfo(card, null, null, source.getSourceId(), game, zone);
if (controller != null) {
MageObject sourceObject = source.getSourceObjectIfItStillExists(game);
if (sourceObject instanceof Card) {
UUID exileZoneId = null;
String exileZoneName = "";
if (toUniqueExileZone) {
exileZoneId = CardUtil.getExileZoneId(game, source.getSourceId(), source.getSourceObjectZoneChangeCounter());
exileZoneName = sourceObject.getLogName();
}
Card sourceCard = (Card) sourceObject;
return controller.moveCardToExileWithInfo(sourceCard, exileZoneId, exileZoneName, source.getSourceId(), game, game.getState().getZone(sourceCard.getId()));
}
return true;
}
return false;
}
}

View file

@ -69,7 +69,7 @@ public class ExileTargetForSourceEffect extends OneShotEffect {
MageObject sourceObject = source.getSourceObject(game);
if (controller != null && sourceObject != null) {
Permanent permanent = game.getPermanent(getTargetPointer().getFirst(game, source));
UUID exileId = CardUtil.getObjectExileZoneId(game, sourceObject);
UUID exileId = CardUtil.getExileZoneId(game, source.getSourceId(), source.getSourceObjectZoneChangeCounter());
if (permanent != null) {
return controller.moveCardToExileWithInfo(permanent, exileId, sourceObject.getLogName(), source.getSourceId(), game, Zone.BATTLEFIELD);
} else {

View file

@ -50,24 +50,41 @@ import mage.util.CardUtil;
*/
public class ReturnFromExileForSourceEffect extends OneShotEffect {
private Zone zone;
private Zone returnToZone;
private boolean tapped;
private boolean previousZone;
/**
*
* @param zone Zone the card should return to
*/
public ReturnFromExileForSourceEffect(Zone zone) {
this(zone, false);
}
public ReturnFromExileForSourceEffect(Zone zone, boolean tapped) {
this(zone, tapped, true);
}
/**
*
* @param zone
* @param tapped
* @param previousZone if this is used from a dies leave battlefield or destroyed trigger, the exile zone is based on previous zone of the object
*/
public ReturnFromExileForSourceEffect(Zone zone, boolean tapped, boolean previousZone) {
super(Outcome.PutCardInPlay);
this.zone = zone;
this.returnToZone = zone;
this.tapped = tapped;
this.previousZone = previousZone;
setText();
}
public ReturnFromExileForSourceEffect(final ReturnFromExileForSourceEffect effect) {
super(effect);
this.zone = effect.zone;
this.returnToZone = effect.returnToZone;
this.tapped = effect.tapped;
this.previousZone = effect.previousZone;
}
@Override
@ -78,9 +95,9 @@ public class ReturnFromExileForSourceEffect extends OneShotEffect {
@Override
public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId());
MageObject sourceObject = source.getSourceObject(game);
if (controller != null && sourceObject != null) {
UUID exileId = CardUtil.getObjectExileZoneId(game, sourceObject);
if (controller != null) {
int zoneChangeCounter = source.getSourceObjectZoneChangeCounter() - (previousZone ? 1:0);
UUID exileId = CardUtil.getExileZoneId(game, source.getSourceId(), zoneChangeCounter);
ExileZone exile = game.getExile().getExileZone(exileId);
if (exile != null) { // null is valid if source left battlefield before enters the battlefield effect resolved
LinkedList<UUID> cards = new LinkedList<>(exile);
@ -89,8 +106,8 @@ public class ReturnFromExileForSourceEffect extends OneShotEffect {
if (card == null) {
return false;
}
game.informPlayers(controller.getName() + " moves " + card.getLogName() + " from exile to " + zone.toString().toLowerCase());
card.moveToZone(zone, source.getSourceId(), game, tapped);
game.informPlayers(controller.getName() + " moves " + card.getLogName() + " from exile to " + returnToZone.toString().toLowerCase());
card.moveToZone(returnToZone, source.getSourceId(), game, tapped);
}
exile.clear();
}
@ -102,7 +119,7 @@ public class ReturnFromExileForSourceEffect extends OneShotEffect {
private void setText() {
StringBuilder sb = new StringBuilder();
sb.append("return the exiled cards ");
switch(zone) {
switch(returnToZone) {
case BATTLEFIELD:
sb.append("to the battlefield under its owner's control");
if (tapped) {

View file

@ -28,13 +28,16 @@
package mage.abilities.effects.common;
import mage.MageObjectReference;
import java.util.UUID;
import mage.MageObject;
import mage.constants.Outcome;
import mage.constants.Zone;
import mage.abilities.Ability;
import mage.abilities.effects.OneShotEffect;
import mage.cards.Card;
import mage.constants.Zone;
import mage.game.ExileZone;
import mage.game.Game;
import mage.util.CardUtil;
/**
*
@ -42,16 +45,13 @@ import mage.game.Game;
*/
public class ReturnToBattlefieldUnderYourControlSourceEffect extends OneShotEffect {
private final Zone onlyFromZone;
public ReturnToBattlefieldUnderYourControlSourceEffect(Zone fromZone) {
public ReturnToBattlefieldUnderYourControlSourceEffect() {
super(Outcome.Benefit);
this.onlyFromZone = fromZone;
staticText = "return that card to the battlefield under your control";
}
public ReturnToBattlefieldUnderYourControlSourceEffect(final ReturnToBattlefieldUnderYourControlSourceEffect effect) {
super(effect);
this.onlyFromZone = effect.onlyFromZone;
}
@Override
@ -61,11 +61,11 @@ public class ReturnToBattlefieldUnderYourControlSourceEffect extends OneShotEffe
@Override
public boolean apply(Game game, Ability source) {
MageObjectReference mor = new MageObjectReference(source.getSourceObject(game), game);
Card card = game.getCard(source.getSourceId());
if (card != null && game.getState().getZone(source.getSourceId()) == onlyFromZone && mor.getZoneChangeCounter() == card.getZoneChangeCounter(game) + 1) {
Zone currentZone = game.getState().getZone(card.getId());
if (card.putOntoBattlefield(game, currentZone, source.getSourceId(), source.getControllerId())) {
UUID exileZoneId = CardUtil.getExileZoneId(game, source.getSourceId(), source.getSourceObjectZoneChangeCounter());
ExileZone exileZone = game.getExile().getExileZone(exileZoneId);
if (exileZone != null && exileZone.contains(source.getSourceId())) {
Card card = game.getCard(source.getSourceId());
if (card != null && card.putOntoBattlefield(game, Zone.EXILED, source.getSourceId(), source.getControllerId())) {
return true;
}
}

View file

@ -53,7 +53,7 @@ public class ReturnToBattlefieldUnderYourControlTargetEffect extends OneShotEffe
/**
*
* @param fromExileZone - the card will only be retunred if it's still in the sour obect specific exile zone
* @param fromExileZone - the card will only be returned if it's still in the sour obect specific exile zone
*/
public ReturnToBattlefieldUnderYourControlTargetEffect(boolean fromExileZone) {
super(Outcome.Benefit);
@ -74,11 +74,10 @@ public class ReturnToBattlefieldUnderYourControlTargetEffect extends OneShotEffe
@Override
public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId());
MageObject sourceObject = source.getSourceObject(game);
if (controller != null && sourceObject != null) {
if (controller != null) {
Card card = null;
if (fromExileZone) {
UUID exilZoneId = CardUtil.getObjectExileZoneId(game, sourceObject);
UUID exilZoneId = CardUtil.getExileZoneId(game, source.getSourceId(), source.getSourceObjectZoneChangeCounter());
if (exilZoneId != null) {
card = game.getExile().getExileZone(exilZoneId).get(getTargetPointer().getFirst(game, source), game);
}

View file

@ -155,7 +155,7 @@ class ChampionExileCost extends CostImpl {
MageObject sourceObject = ability.getSourceObject(game);
if (controller != null && sourceObject != null) {
if (targets.choose(Outcome.Exile, controllerId, sourceId, game)) {
UUID exileId = CardUtil.getObjectExileZoneId(game, sourceObject);
UUID exileId = CardUtil.getExileZoneId(game, ability.getSourceId(), ability.getSourceObjectZoneChangeCounter());
for (UUID targetId: targets.get(0).getTargets()) {
Permanent permanent = game.getPermanent(targetId);
if (permanent == null) {

View file

@ -29,7 +29,6 @@
package mage.abilities.keyword;
import java.util.UUID;
import mage.MageObjectReference;
import mage.abilities.Ability;
import mage.abilities.Mode;
import mage.abilities.TriggeredAbilityImpl;
@ -53,7 +52,7 @@ public class EchoAbility extends TriggeredAbilityImpl {
protected UUID lastController;
protected boolean echoPaid;
protected Costs echoCosts = new CostsImpl();
protected Costs<Cost> echoCosts = new CostsImpl<>();
private boolean manaEcho = true;
public EchoAbility(String manaString) {
@ -94,6 +93,7 @@ public class EchoAbility extends TriggeredAbilityImpl {
// reset the echo paid state back, if creature enteres the battlefield
if (event.getType().equals(GameEvent.EventType.ENTERS_THE_BATTLEFIELD)
&& event.getTargetId().equals(this.getSourceId())) {
this.echoPaid = false;
}
if (event.getType().equals(GameEvent.EventType.UPKEEP_STEP_PRE)) {
@ -144,15 +144,17 @@ class EchoEffect extends OneShotEffect {
@Override
public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId());
MageObjectReference mor = new MageObjectReference(source.getSourceId(), game);
if (controller != null && mor.refersTo(source.getSourceObject(game), game)) {
if (controller != null && source.getSourceObjectIfItStillExists(game) != null) {
if (controller.chooseUse(Outcome.Benefit, "Pay " + cost.getText() /* + " or sacrifice " + permanent.getName() */ + "?", game)) {
cost.clearPaid();
if (cost.pay(source, game, source.getSourceId(), source.getControllerId(), false)) {
return true;
}
}
mor.getPermanent(game).sacrifice(source.getSourceId(), game);
Permanent permanent = game.getPermanent(source.getSourceId());
if (permanent != null) {
permanent.sacrifice(source.getSourceId(), game);
}
return true;
}
return false;

View file

@ -35,10 +35,13 @@ import mage.abilities.costs.Cost;
import mage.abilities.costs.VariableCost;
import mage.abilities.costs.mana.VariableManaCost;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.ReplacementEffectImpl;
import mage.abilities.effects.common.CreateDelayedTriggeredAbilityEffect;
import mage.abilities.effects.common.ExileSourceEffect;
import mage.cards.Card;
import mage.cards.SplitCard;
import mage.constants.CardType;
import mage.constants.Duration;
import mage.constants.Outcome;
import mage.constants.SpellAbilityType;
import mage.constants.TimingRule;
@ -74,7 +77,6 @@ public class FlashbackAbility extends SpellAbility {
this.timing = timingRule;
this.usesStack = false;
this.spellAbilityType = SpellAbilityType.BASE_ALTERNATE;
this.addEffect(new CreateDelayedTriggeredAbilityEffect(new FlashbackTriggeredAbility()));
}
public FlashbackAbility(final FlashbackAbility ability) {
@ -211,48 +213,59 @@ class FlashbackEffect extends OneShotEffect {
}
game.informPlayers(new StringBuilder(controller.getName()).append(" flashbacks ").append(card.getName()).toString());
spellAbility.setCostModificationActive(false); // prevents to apply cost modification twice for flashbacked spells
return controller.cast(spellAbility, game, true);
if (controller.cast(spellAbility, game, true)) {
game.addEffect(new FlashbackReplacementEffect(), source);
return true;
}
return false;
}
}
return false;
}
}
class FlashbackTriggeredAbility extends DelayedTriggeredAbility {
class FlashbackReplacementEffect extends ReplacementEffectImpl {
public FlashbackTriggeredAbility() {
super(new ExileSourceEffect());
usesStack = false;
public FlashbackReplacementEffect() {
super(Duration.OneUse, Outcome.Exile);
staticText = "(If the flashback cost was paid, exile this card instead of putting it anywhere else any time it would leave the stack)";
}
public FlashbackTriggeredAbility(final FlashbackTriggeredAbility ability) {
super(ability);
public FlashbackReplacementEffect(final FlashbackReplacementEffect effect) {
super(effect);
}
@Override
public FlashbackTriggeredAbility copy() {
return new FlashbackTriggeredAbility(this);
public FlashbackReplacementEffect copy() {
return new FlashbackReplacementEffect(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
public boolean apply(Game game, Ability source) {
return true;
}
@Override
public boolean replaceEvent(GameEvent event, Ability source, Game game) {
Player controller = game.getPlayer(source.getControllerId());
if (controller != null) {
Card card = game.getCard(event.getTargetId());
if (card != null) {
return controller.moveCardToExileWithInfo(card, null, "", source.getSourceId(), game, game.getState().getZone(card.getId()));
}
}
return false;
}
@Override
public boolean checksEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.ZONE_CHANGE;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
if (event.getTargetId().equals(this.sourceId)) {
ZoneChangeEvent zEvent = (ZoneChangeEvent) event;
if (zEvent.getFromZone() == Zone.STACK) {
return true;
}
}
return false;
public boolean applies(GameEvent event, Ability source, Game game) {
return event.getTargetId().equals(source.getSourceId())
&& ((ZoneChangeEvent)event).getFromZone() == Zone.STACK
&& ((ZoneChangeEvent)event).getToZone() != Zone.EXILED;
}
@Override
public String getRule() {
return "(If the flashback cost was paid, exile this card instead of putting it anywhere else any time it would leave the stack)";
}
}
}

View file

@ -124,7 +124,7 @@ class UnearthDelayedTriggeredAbility extends DelayedTriggeredAbility {
class UnearthLeavesBattlefieldEffect extends ReplacementEffectImpl {
public UnearthLeavesBattlefieldEffect() {
super(Duration.WhileOnBattlefield, Outcome.Exile);
super(Duration.OneUse, Outcome.Exile);
staticText = "When {this} leaves the battlefield, exile it";
}
@ -144,7 +144,7 @@ class UnearthLeavesBattlefieldEffect extends ReplacementEffectImpl {
@Override
public boolean applies(GameEvent event, Ability source, Game game) {
if (event.getType() == EventType.ZONE_CHANGE && event.getTargetId().equals(source.getSourceId())) {
if (source.getSourceObjectIfItStillExists(game) != null) {
ZoneChangeEvent zEvent = (ZoneChangeEvent)event;
if (zEvent.getFromZone() == Zone.BATTLEFIELD && zEvent.getToZone() != Zone.EXILED) {
return true;
@ -153,14 +153,9 @@ class UnearthLeavesBattlefieldEffect extends ReplacementEffectImpl {
return false;
}
@Override
public boolean apply(Game game, Ability source) {
ExileSourceEffect effect = new ExileSourceEffect();
return effect.apply(game, source);
}
@Override
public boolean replaceEvent(GameEvent event, Ability source, Game game) {
return apply(game, source);
new ExileSourceEffect().apply(game, source);
return true;
}
}

View file

@ -142,7 +142,7 @@ public interface Game extends MageItem, Serializable {
void setSimulation(boolean simulation);
MageObject getLastKnownInformation(UUID objectId, Zone zone);
MageObject getLastKnownInformation(UUID objectId, Zone zone, int zoneChangeCounter);
MageObject getShortLivingLKI(UUID objectId, Zone zone);
boolean getShortLivingLKI(UUID objectId, Zone zone);
void rememberLKI(UUID objectId, Zone zone, MageObject object);
void resetLKI();
void resetShortLivingLKI();

View file

@ -159,9 +159,11 @@ public abstract class GameImpl implements Game, Serializable {
protected transient PlayerQueryEventSource playerQueryEventSource = new PlayerQueryEventSource();
protected Map<UUID, Card> gameCards = new HashMap<>();
protected Map<Zone,HashMap<UUID, MageObject>> lki = new EnumMap<>(Zone.class);
protected Map<UUID, Map<Integer, MageObject>> lkiExtended = new HashMap<>();
protected Map<Zone,HashMap<UUID, MageObject>> shortLivingLKI = new EnumMap<>(Zone.class);
// Used to check if an object was moved by the current effect in resolution (so Wrath like effect can be handled correctly)
protected Map<Zone, Set<UUID>> shortLivingLKI = new EnumMap<>(Zone.class);
protected GameState state;
private transient Stack<Integer> savedStates = new Stack<>();
@ -1131,6 +1133,9 @@ public abstract class GameImpl implements Game, Serializable {
// 603.3. Once an ability has triggered, its controller puts it on the stack as an object that’s not a card the next time a player would receive priority
checkStateAndTriggered();
applyEffects();
if (state.getStack().isEmpty()) {
resetLKI();
}
saveState(false);
if (isPaused() || gameOver(null)) {
return;
@ -1158,7 +1163,7 @@ public abstract class GameImpl implements Game, Serializable {
state.getPlayers().resetPassed();
fireUpdatePlayersEvent();
state.getRevealed().reset();
resetShortLivingLKI();
resetShortLivingLKI();
break;
} else {
resetLKI();
@ -2195,15 +2200,12 @@ public abstract class GameImpl implements Game, Serializable {
}
@Override
public MageObject getShortLivingLKI(UUID objectId, Zone zone) {
Map<UUID, MageObject> shortLivingLkiMap = shortLivingLKI.get(zone);
if (shortLivingLkiMap != null) {
MageObject object = shortLivingLkiMap.get(objectId);
if (object != null) {
return object.copy();
}
public boolean getShortLivingLKI(UUID objectId, Zone zone) {
Set<UUID> idSet = shortLivingLKI.get(zone);
if (idSet != null) {
return idSet.contains(objectId);
}
return null;
return false;
}
/**
@ -2226,16 +2228,15 @@ public abstract class GameImpl implements Game, Serializable {
newMap.put(objectId, copy);
lki.put(zone, newMap);
}
Map<UUID, MageObject> shortLivingLkiMap = shortLivingLKI.get(zone);
if (shortLivingLkiMap != null) {
shortLivingLkiMap.put(objectId, copy);
} else {
HashMap<UUID, MageObject> newMap = new HashMap<>();
newMap.put(objectId, copy);
shortLivingLKI.put(zone, newMap);
// remembers if a object was in a zone during the resolution of an effect
// e.g. Wrath destroys all and you the question is is the replacement effect to apply because the source was also moved by the same effect
// because it ahppens all at the same time the replcaement effect has still to be applied
Set<UUID> idSet = shortLivingLKI.get(zone);
if (idSet == null) {
idSet = new HashSet<>();
shortLivingLKI.put(zone, idSet);
}
idSet.add(objectId);
if (object instanceof Permanent) {
Map<Integer, MageObject> lkiExtendedMap = lkiExtended.get(objectId);
if (lkiExtendedMap != null) {

View file

@ -394,6 +394,7 @@ public class GameState implements Serializable, Copyable<GameState> {
}
public void applyEffects(Game game) {
game.resetShortLivingLKI();
for (Player player: players.values()) {
player.reset();
}

View file

@ -198,6 +198,7 @@ public class Spell implements StackObject, Card {
result |= spellAbility.resolve(game);
}
}
game.resetShortLivingLKI();
index++;
}
}

View file

@ -499,12 +499,22 @@ public class StackAbility implements StackObject, Ability {
@Override
public MageObject getSourceObject(Game game) {
throw new UnsupportedOperationException("Not supported."); //To change body of generated methods, choose Tools | Templates.
throw new UnsupportedOperationException("Not supported.");
}
@Override
public void setSourceObject(MageObject sourceObject) {
throw new UnsupportedOperationException("Not supported."); //To change body of generated methods, choose Tools | Templates.
public MageObject getSourceObjectIfItStillExists(Game game) {
throw new UnsupportedOperationException("Not supported.");
}
@Override
public int getSourceObjectZoneChangeCounter() {
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
@Override
public void setSourceObject(MageObject sourceObject, Game game) {
throw new UnsupportedOperationException("Not supported.");
}
@Override

View file

@ -111,20 +111,23 @@ public class ManaPool implements Serializable {
lockManaType(); // pay only one mana if mana payment is set to manually
return true;
}
MageObject sourceObject = ability.getSourceObject(game);
for (ManaPoolItem mana : manaItems) {
if (filter == null || filter.match(sourceObject, game)) {
boolean spendAnyMana = spendAnyMana(ability, game);
if (mana.get(manaType) > 0 || (spendAnyMana && mana.count() > 0)) {
game.fireEvent(new GameEvent(GameEvent.EventType.MANA_PAYED, ability.getId(), mana.getSourceId(), ability.getControllerId(), 0, mana.getFlag()));
if (spendAnyMana) {
mana.removeAny();
} else {
mana.remove(manaType);
}
lockManaType(); // pay only one mana if mana payment is set to manually
return true;
if (filter != null) {
MageObject sourceObject = game.getObject(mana.getSourceId());
if (!filter.match(sourceObject, game)) {
continue;
}
}
boolean spendAnyMana = spendAnyMana(ability, game);
if (mana.get(manaType) > 0 || (spendAnyMana && mana.count() > 0)) {
game.fireEvent(new GameEvent(GameEvent.EventType.MANA_PAYED, ability.getId(), mana.getSourceId(), ability.getControllerId(), 0, mana.getFlag()));
if (spendAnyMana) {
mana.removeAny();
} else {
mana.remove(manaType);
}
lockManaType(); // pay only one mana if mana payment is set to manually
return true;
}
}
return false;