forked from External/mage
582 lines
25 KiB
Java
582 lines
25 KiB
Java
package mage.abilities;
|
||
|
||
import mage.MageObject;
|
||
import mage.abilities.condition.Condition;
|
||
import mage.abilities.effects.Effect;
|
||
import mage.abilities.effects.common.*;
|
||
import mage.constants.AbilityType;
|
||
import mage.constants.AbilityWord;
|
||
import mage.constants.Zone;
|
||
import mage.game.Game;
|
||
import mage.game.events.BatchEvent;
|
||
import mage.game.events.GameEvent;
|
||
import mage.game.events.ZoneChangeEvent;
|
||
import mage.game.permanent.PermanentToken;
|
||
import mage.players.Player;
|
||
import mage.util.CardUtil;
|
||
|
||
import java.util.Locale;
|
||
import java.util.Optional;
|
||
import java.util.Set;
|
||
import java.util.UUID;
|
||
|
||
/**
|
||
* @author BetaSteward_at_googlemail.com
|
||
*/
|
||
public abstract class TriggeredAbilityImpl extends AbilityImpl implements TriggeredAbility {
|
||
|
||
private boolean optional;
|
||
private Condition interveningIfCondition;
|
||
private Condition triggerCondition;
|
||
private boolean leavesTheBattlefieldTrigger;
|
||
private int triggerLimitEachTurn = Integer.MAX_VALUE; // for "triggers only once|twice each turn"
|
||
private int triggerLimitEachGame = Integer.MAX_VALUE; // for "triggers only once|twice"
|
||
private boolean doOnlyOnceEachTurn = false;
|
||
private boolean replaceRuleText = false; // if true, replace "{this}" with "it" in effect text
|
||
private GameEvent triggerEvent = null;
|
||
private String triggerPhrase = null;
|
||
|
||
protected TriggeredAbilityImpl(Zone zone, Effect effect) {
|
||
this(zone, effect, false);
|
||
}
|
||
|
||
protected TriggeredAbilityImpl(Zone zone, Effect effect, boolean optional) {
|
||
super(AbilityType.TRIGGERED_NONMANA, zone);
|
||
setLeavesTheBattlefieldTrigger(false);
|
||
if (effect != null) {
|
||
addEffect(effect);
|
||
}
|
||
this.optional = optional;
|
||
|
||
// verify check: DoIfCostPaid effect already asks about action (optional), so no needs to ask it again in triggered ability
|
||
if (effect instanceof DoIfCostPaid && (this.optional && ((DoIfCostPaid) effect).isOptional())) {
|
||
throw new IllegalArgumentException("DoIfCostPaid effect must have only one optional settings, but it have two (trigger + DoIfCostPaid): " + this.getClass().getSimpleName());
|
||
}
|
||
}
|
||
|
||
protected TriggeredAbilityImpl(final TriggeredAbilityImpl ability) {
|
||
super(ability);
|
||
this.optional = ability.optional;
|
||
this.interveningIfCondition = ability.interveningIfCondition;
|
||
this.triggerCondition = ability.triggerCondition;
|
||
this.leavesTheBattlefieldTrigger = ability.leavesTheBattlefieldTrigger;
|
||
this.triggerLimitEachTurn = ability.triggerLimitEachTurn;
|
||
this.triggerLimitEachGame = ability.triggerLimitEachGame;
|
||
this.doOnlyOnceEachTurn = ability.doOnlyOnceEachTurn;
|
||
this.replaceRuleText = ability.replaceRuleText;
|
||
this.triggerEvent = ability.triggerEvent;
|
||
this.triggerPhrase = ability.triggerPhrase;
|
||
}
|
||
|
||
@Override
|
||
public void trigger(Game game, UUID controllerId, GameEvent triggeringEvent) {
|
||
//20091005 - 603.4
|
||
if (checkInterveningIfClause(game) && checkTriggerCondition(game)) {
|
||
updateTurnCount(game);
|
||
updateGameCount(game);
|
||
game.addTriggeredAbility(this, triggeringEvent);
|
||
}
|
||
}
|
||
|
||
// Used for triggers with a per-turn limit.
|
||
private String getKeyLastTurnTriggered(Game game) {
|
||
return CardUtil.getCardZoneString(
|
||
"lastTurnTriggered|" + getOriginalId(), getSourceId(), game
|
||
);
|
||
}
|
||
|
||
// Used for triggers with a per-turn limit.
|
||
private String getKeyLastTurnTriggeredCount(Game game) {
|
||
return CardUtil.getCardZoneString(
|
||
"lastTurnTriggeredCount|" + getOriginalId(), getSourceId(), game
|
||
);
|
||
}
|
||
|
||
// Used for triggers with a per-game limit.
|
||
private String getKeyGameTriggeredCount(Game game) {
|
||
return CardUtil.getCardZoneString(
|
||
"gameTriggeredCount|" + getOriginalId(), getSourceId(), game
|
||
);
|
||
}
|
||
|
||
private void updateTurnCount(Game game) {
|
||
if (triggerLimitEachTurn == Integer.MAX_VALUE) {
|
||
return;
|
||
}
|
||
String keyLastTurnTriggered = getKeyLastTurnTriggered(game);
|
||
String keyLastTurnTriggeredCount = getKeyLastTurnTriggeredCount(game);
|
||
Integer lastTurn = (Integer) game.getState().getValue(keyLastTurnTriggered);
|
||
int currentTurn = game.getTurnNum();
|
||
if (lastTurn != null && lastTurn == currentTurn) {
|
||
// Ability already triggered this turn, incrementing the count.
|
||
int lastCount = Optional.ofNullable((Integer) game.getState().getValue(keyLastTurnTriggeredCount)).orElse(0);
|
||
game.getState().setValue(keyLastTurnTriggeredCount, lastCount + 1);
|
||
} else {
|
||
// first trigger for Ability this turn.
|
||
game.getState().setValue(keyLastTurnTriggered, currentTurn);
|
||
game.getState().setValue(keyLastTurnTriggeredCount, 1);
|
||
}
|
||
}
|
||
|
||
private void updateGameCount(Game game) {
|
||
if (triggerLimitEachGame == Integer.MAX_VALUE) {
|
||
return;
|
||
}
|
||
String keyGameTriggeredCount = getKeyGameTriggeredCount(game);
|
||
int lastCount = Optional.ofNullable((Integer) game.getState().getValue(keyGameTriggeredCount)).orElse(0);
|
||
// Incrementing the count.
|
||
game.getState().setValue(keyGameTriggeredCount, lastCount + 1);
|
||
}
|
||
|
||
@Override
|
||
public TriggeredAbilityImpl setTriggerPhrase(String triggerPhrase) {
|
||
this.triggerPhrase = triggerPhrase;
|
||
return this;
|
||
}
|
||
|
||
@Override
|
||
public String getTriggerPhrase() {
|
||
return this.triggerPhrase;
|
||
}
|
||
|
||
@Override
|
||
public void setTriggerEvent(GameEvent triggerEvent) {
|
||
this.triggerEvent = triggerEvent;
|
||
}
|
||
|
||
@Override
|
||
public GameEvent getTriggerEvent() {
|
||
return triggerEvent;
|
||
}
|
||
|
||
@Override
|
||
public boolean checkTriggeredLimit(Game game) {
|
||
return getRemainingTriggersLimitEachGame(game) > 0 && getRemainingTriggersLimitEachTurn(game) > 0;
|
||
}
|
||
|
||
@Override
|
||
public boolean checkUsedAlready(Game game) {
|
||
return doOnlyOnceEachTurn && TriggeredAbility.checkDidThisTurn(this, game);
|
||
}
|
||
|
||
@Override
|
||
public TriggeredAbility setTriggersLimitEachTurn(int limit) {
|
||
this.triggerLimitEachTurn = limit;
|
||
return this;
|
||
}
|
||
|
||
@Override
|
||
public TriggeredAbility setTriggersLimitEachGame(int limit) {
|
||
this.triggerLimitEachGame = limit;
|
||
return this;
|
||
}
|
||
|
||
@Override
|
||
public int getRemainingTriggersLimitEachTurn(Game game) {
|
||
if (triggerLimitEachTurn == Integer.MAX_VALUE) {
|
||
return Integer.MAX_VALUE;
|
||
}
|
||
String keyLastTurnTriggered = getKeyLastTurnTriggered(game);
|
||
Integer lastTurn = (Integer) game.getState().getValue(keyLastTurnTriggered);
|
||
int currentTurn = game.getTurnNum();
|
||
if (lastTurn != null && lastTurn == currentTurn) {
|
||
// Ability already triggered this turn, so returning the limit minus the count this turn
|
||
String keyLastTurnTriggeredCount = getKeyLastTurnTriggeredCount(game);
|
||
int count = Optional.ofNullable((Integer) game.getState().getValue(keyLastTurnTriggeredCount)).orElse(0);
|
||
return Math.max(0, triggerLimitEachTurn - count);
|
||
} else {
|
||
// Ability did not trigger this turn, so returning the limit
|
||
return triggerLimitEachTurn;
|
||
}
|
||
}
|
||
|
||
@Override
|
||
public int getRemainingTriggersLimitEachGame(Game game) {
|
||
if (triggerLimitEachGame == Integer.MAX_VALUE) {
|
||
return Integer.MAX_VALUE;
|
||
}
|
||
String keyGameTriggeredCount = getKeyGameTriggeredCount(game);
|
||
int count = Optional.ofNullable((Integer) game.getState().getValue(keyGameTriggeredCount)).orElse(0);
|
||
return Math.max(0, triggerLimitEachGame - count);
|
||
}
|
||
|
||
@Override
|
||
public TriggeredAbility setDoOnlyOnceEachTurn(boolean doOnlyOnce) {
|
||
this.doOnlyOnceEachTurn = doOnlyOnce;
|
||
if (CardUtil.castStream(this.getAllEffects(), DoIfCostPaid.class).noneMatch(DoIfCostPaid::isOptional)) {
|
||
this.optional = true;
|
||
}
|
||
return this;
|
||
}
|
||
|
||
@Override
|
||
public TriggeredAbility setOptional(boolean optional) {
|
||
this.optional = optional;
|
||
return this;
|
||
}
|
||
|
||
@Override
|
||
public TriggeredAbility withRuleTextReplacement(boolean replaceRuleText) {
|
||
this.replaceRuleText = replaceRuleText;
|
||
return this;
|
||
}
|
||
|
||
@Override
|
||
public TriggeredAbility withInterveningIf(Condition interveningIfCondition) {
|
||
this.interveningIfCondition = interveningIfCondition;
|
||
this.replaceRuleText = false;
|
||
return this;
|
||
}
|
||
|
||
@Override
|
||
public boolean checkInterveningIfClause(Game game) {
|
||
return interveningIfCondition == null || interveningIfCondition.apply(game, this);
|
||
}
|
||
|
||
@Override
|
||
public TriggeredAbility withTriggerCondition(Condition condition) {
|
||
this.triggerCondition = condition;
|
||
if (this.triggerPhrase != null && !condition.toString().isEmpty()) {
|
||
this.setTriggerPhrase(
|
||
this.triggerPhrase.substring(0, this.triggerPhrase.length() - 2) + ' ' +
|
||
(condition.toString().startsWith("during") ? "" : "while ") + condition + ", "
|
||
);
|
||
}
|
||
return this;
|
||
}
|
||
|
||
@Override
|
||
public Condition getTriggerCondition() {
|
||
return triggerCondition;
|
||
}
|
||
|
||
@Override
|
||
public boolean checkTriggerCondition(Game game) {
|
||
return triggerCondition == null || triggerCondition.apply(game, this);
|
||
}
|
||
|
||
@Override
|
||
public boolean resolve(Game game) {
|
||
if (!checkInterveningIfClause(game)) {
|
||
return false;
|
||
}
|
||
if (isOptional()) {
|
||
MageObject object = game.getObject(getSourceId());
|
||
Player player = game.getPlayer(this.getControllerId());
|
||
if (player == null || object == null
|
||
|| (doOnlyOnceEachTurn && checkUsedAlready(game))
|
||
|| !player.chooseUse(
|
||
getEffects().getOutcome(this),
|
||
this.getRule(object.getLogName()), this, game
|
||
)) {
|
||
return false;
|
||
}
|
||
if (doOnlyOnceEachTurn) {
|
||
TriggeredAbility.setDidThisTurn(this, game);
|
||
}
|
||
}
|
||
//20091005 - 603.4
|
||
if (!super.resolve(game)) {
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
@Override
|
||
public String getGameLogMessage(Game game) {
|
||
MageObject object = game.getObject(sourceId);
|
||
StringBuilder sb = new StringBuilder();
|
||
if (object != null) {
|
||
sb.append("Ability triggers: ").append(object.getLogName()).append(" - ").append(this.getRule(object.getLogName()));
|
||
} else {
|
||
sb.append("Ability triggers: ").append(this.getRule());
|
||
}
|
||
String targetText = getTargetDescriptionForLog(getTargets(), game);
|
||
if (!targetText.isEmpty()) {
|
||
sb.append(" - ").append(targetText);
|
||
}
|
||
return sb.toString();
|
||
}
|
||
|
||
@Override
|
||
public String getRule() {
|
||
StringBuilder sb = new StringBuilder();
|
||
sb.append(triggerPhrase == null ? "" : triggerPhrase);
|
||
|
||
if (interveningIfCondition != null) {
|
||
String conditionText = interveningIfCondition.toString();
|
||
if (!conditionText.isEmpty()) { // e.g. CaseSolveAbility
|
||
if (replaceRuleText && triggerPhrase != null && triggerPhrase.contains("{this}")) {
|
||
conditionText = conditionText.replace("{this}", "it");
|
||
if (conditionText.startsWith("it is ")) {
|
||
conditionText = conditionText.replace("it is ", "it's ");
|
||
}
|
||
}
|
||
if (!conditionText.startsWith("if ")) {
|
||
sb.append("if ");
|
||
}
|
||
sb.append(conditionText).append(", ");
|
||
}
|
||
}
|
||
|
||
String superRule = super.getRule(true);
|
||
if (!superRule.isEmpty()) {
|
||
String ruleLow = superRule.toLowerCase(Locale.ENGLISH);
|
||
if (isOptional()) {
|
||
if (ruleLow.startsWith("you ")) {
|
||
if (!ruleLow.startsWith("you may")) {
|
||
StringBuilder newRule = new StringBuilder(superRule);
|
||
newRule.insert(4, "may ");
|
||
superRule = newRule.toString();
|
||
}
|
||
} else if (!ruleLow.startsWith("{this}")
|
||
&& (this.getTargets().isEmpty()
|
||
|| startsWithVerb(ruleLow))) {
|
||
sb.append("you may ");
|
||
} else if (!ruleLow.startsWith("its controller may")) {
|
||
sb.append("you may have ");
|
||
superRule = superRule.replaceFirst(" (become|block|deal|discard|gain|get|lose|mill|sacrifice)s? ", " $1 ");
|
||
}
|
||
}
|
||
if (replaceRuleText && triggerPhrase != null) {
|
||
superRule = superRule.replaceFirst("^((?:you may )?sacrifice |(put|remove) [^ ]+ [^ ]+ counters? (on|from) |return |transform |untap |regenerate |attach |exile )?\\{this\\}", "$1it");
|
||
}
|
||
sb.append(superRule);
|
||
if (triggerLimitEachTurn != Integer.MAX_VALUE) {
|
||
sb.append(" This ability triggers only ");
|
||
switch (triggerLimitEachTurn) {
|
||
case 1:
|
||
sb.append("once");
|
||
break;
|
||
case 2:
|
||
sb.append("twice");
|
||
break;
|
||
default:
|
||
// No card with that behavior yet, so feel free to change the text once one exist
|
||
sb.append(CardUtil.numberToText(triggerLimitEachTurn)).append(" times");
|
||
}
|
||
sb.append(" each turn.");
|
||
}
|
||
if (triggerLimitEachGame != Integer.MAX_VALUE) {
|
||
sb.append(" This ability triggers only ");
|
||
switch (triggerLimitEachGame) {
|
||
case 1:
|
||
sb.append("once.");
|
||
break;
|
||
case 2:
|
||
// No card with that behavior yet, so feel free to change the text once one exist
|
||
sb.append("twice.");
|
||
break;
|
||
default:
|
||
// No card with that behavior yet, so feel free to change the text once one exist
|
||
sb.append(CardUtil.numberToText(triggerLimitEachGame)).append(" times.");
|
||
}
|
||
}
|
||
if (doOnlyOnceEachTurn) {
|
||
sb.append(" Do this only once each turn.");
|
||
}
|
||
}
|
||
return addRulePrefix(sb.toString());
|
||
}
|
||
|
||
private static boolean startsWithVerb(String ruleLow) {
|
||
return ruleLow.startsWith("attach")
|
||
|| ruleLow.startsWith("cast")
|
||
|| ruleLow.startsWith("change")
|
||
|| ruleLow.startsWith("counter")
|
||
|| ruleLow.startsWith("create")
|
||
|| ruleLow.startsWith("destroy")
|
||
|| ruleLow.startsWith("distribute")
|
||
|| ruleLow.startsWith("sacrifice")
|
||
|| ruleLow.startsWith("exchange")
|
||
|| ruleLow.startsWith("exile")
|
||
|| ruleLow.startsWith("gain")
|
||
|| ruleLow.startsWith("goad")
|
||
|| ruleLow.startsWith("have")
|
||
|| ruleLow.startsWith("move")
|
||
|| ruleLow.startsWith("prevent")
|
||
|| ruleLow.startsWith("put")
|
||
|| ruleLow.startsWith("remove")
|
||
|| ruleLow.startsWith("return")
|
||
|| ruleLow.startsWith("shuffle")
|
||
|| ruleLow.startsWith("turn")
|
||
|| ruleLow.startsWith("tap")
|
||
|| ruleLow.startsWith("untap");
|
||
}
|
||
|
||
/**
|
||
* For use in generating trigger phrases with correct text
|
||
*
|
||
* @return "When " for an effect that always removes the source from the battlefield, otherwise "Whenever "
|
||
*/
|
||
protected final String getWhen() {
|
||
return (!optional && getAllEffects().stream().anyMatch(
|
||
effect -> effect instanceof SacrificeSourceEffect
|
||
|| effect instanceof ReturnToHandSourceEffect
|
||
|| effect instanceof ShuffleIntoLibrarySourceEffect
|
||
|| effect instanceof ExileSourceEffect
|
||
|| effect instanceof FlipSourceEffect
|
||
|| effect instanceof DestroySourceEffect
|
||
) ? "When " : "Whenever ");
|
||
}
|
||
|
||
@Override
|
||
public boolean isInUseableZone(Game game, MageObject sourceObject, GameEvent event) {
|
||
|
||
// workaround for singleton abilities like Flying
|
||
UUID affectedSourceId = getRealSourceObjectId(this, sourceObject);
|
||
|
||
// 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.
|
||
|
||
// There are possible two different use cases:
|
||
// * look in current game state (normal events):
|
||
// * look back in time (leaves battlefield, dies, etc);
|
||
|
||
// TODO: need sync or shared code with AbilityImpl.isInUseableZone
|
||
MageObject affectedSourceObject = sourceObject;
|
||
if (event == null) {
|
||
// state base triggers - use only actual state
|
||
} else {
|
||
// event triggers - can look back in time for some use cases
|
||
switch (event.getType()) {
|
||
case ZONE_CHANGE:
|
||
ZoneChangeEvent zce = (ZoneChangeEvent) event;
|
||
Set<UUID> eventTargets = CardUtil.getEventTargets(event);
|
||
if (eventTargets.contains(getSourceId()) && !zce.getToZone().isPublicZone()) {
|
||
// TODO: need research and share with AbilityImpl
|
||
// If an ability triggers when the object that has it is put into a hidden zone from a graveyard,
|
||
// that ability triggers from the graveyard, (such as Golgari Brownscale),
|
||
// Yixlid Jailer will prevent that ability from triggering.
|
||
if (zce.getFromZone().match(Zone.GRAVEYARD)) {
|
||
if (!CardUtil.cardHadAbility(this, game.getLastKnownInformationCard(getSourceId(), zce.getFromZone()), getSourceId(), game)) {
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
if (isLeavesTheBattlefieldTrigger() && game.checkShortLivingLKI(affectedSourceId, Zone.BATTLEFIELD)) {
|
||
affectedSourceObject = game.getLastKnownInformation(affectedSourceId, Zone.BATTLEFIELD);
|
||
}
|
||
break;
|
||
case DESTROYED_PERMANENT:
|
||
case EXPLOITED_CREATURE:
|
||
case SACRIFICED_PERMANENT:
|
||
if (isLeavesTheBattlefieldTrigger() && game.checkShortLivingLKI(affectedSourceId, Zone.BATTLEFIELD)) {
|
||
affectedSourceObject = game.getPermanentOrLKIBattlefield(affectedSourceId);
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
return super.isInUseableZone(game, affectedSourceObject, event);
|
||
}
|
||
|
||
@Override
|
||
public boolean isLeavesTheBattlefieldTrigger() {
|
||
return leavesTheBattlefieldTrigger;
|
||
}
|
||
|
||
@Override
|
||
public final void setLeavesTheBattlefieldTrigger(boolean leavesTheBattlefieldTrigger) {
|
||
this.leavesTheBattlefieldTrigger = leavesTheBattlefieldTrigger;
|
||
|
||
// TODO: replace override of isInUseableZone in dies only triggers by like "isDiesOnlyTrigger" here
|
||
}
|
||
|
||
@Override
|
||
public boolean isOptional() {
|
||
return optional;
|
||
}
|
||
|
||
@Override
|
||
public TriggeredAbilityImpl setAbilityWord(AbilityWord abilityWord) {
|
||
super.setAbilityWord(abilityWord);
|
||
return this;
|
||
}
|
||
|
||
/**
|
||
* Looking object in GRAVEYARD zone only. If you need multi zone then use default isInUseableZone
|
||
* - good example: Whenever another creature you control dies
|
||
* - bad example: When {this} dies or is put into exile from the battlefield
|
||
* <p>
|
||
* For triggered abilities that function from the battlefield that must trigger when the source permanent dies
|
||
* and/or for any other events that happen simultaneously to the source permanent dying.
|
||
* (Similar logic must be used for any leaves-the-battlefield, but this method assumes to graveyard only.)
|
||
* NOTE: If your ability functions from another zone (not battlefield) then must use standard logic, not this.
|
||
*/
|
||
public static boolean isInUseableZoneDiesTrigger(TriggeredAbility sourceAbility, MageObject sourceObject, GameEvent event, Game game) {
|
||
// runtime check: wrong trigger settings
|
||
if (!sourceAbility.isLeavesTheBattlefieldTrigger()) {
|
||
throw new IllegalArgumentException("Wrong code usage: all dies triggers must use setLeavesTheBattlefieldTrigger(true) and override isInUseableZone - "
|
||
+ sourceAbility.getSourceObject(game) + " - " + sourceAbility);
|
||
}
|
||
|
||
// runtime check: wrong isInUseableZone for batch related triggers
|
||
if (event instanceof BatchEvent) {
|
||
throw new IllegalArgumentException("Wrong code usage: batch events unsupported here, possible miss of override isInUseableZone - "
|
||
+ sourceAbility.getSourceObject(game) + " - " + sourceAbility);
|
||
}
|
||
|
||
// workaround for singleton abilities like Flying
|
||
UUID affectedSourceId = getRealSourceObjectId(sourceAbility, sourceObject);
|
||
|
||
// on permanent - can use actual or look back in time
|
||
MageObject affectedObject = null;
|
||
if (game.getState().getZone(affectedSourceId) == Zone.BATTLEFIELD) {
|
||
affectedObject = game.getPermanent(affectedSourceId);
|
||
} else {
|
||
// The idea: short living LKI must help to find a moment in the inner of resolve
|
||
// -
|
||
// Example:
|
||
// --!---------------!-------------!-----!-----------!
|
||
// - ! steps ! perm zone ! LKI ! short LKI !
|
||
// --!---------------!-------------!-----!-----------!
|
||
// - ! resolve start ! battlefield ! no ! no !
|
||
// - ! step 1 ! battlefield ! no ! no ! permanent moving to graveyard by step's command
|
||
// - ! step 2 ! graveyard ! yes ! yes ! other commands
|
||
// - ! step 3 ! graveyard ! yes ! yes ! other commands
|
||
// - ! raise triggers! graveyard ! yes ! yes ! handle and put triggers that was raised in resolve steps
|
||
// - ! resolve end ! graveyard ! yes ! no !
|
||
// - ! resolve next ! graveyard ! yes ! no ! resolve next spell
|
||
// - ! empty stack ! graveyard ! no ! no ! no more to resolve
|
||
// --!---------------!-------------!-----!-----------!
|
||
// -
|
||
if (game.checkShortLivingLKI(affectedSourceId, Zone.BATTLEFIELD)) {
|
||
affectedObject = game.getLastKnownInformation(affectedSourceId, Zone.BATTLEFIELD);
|
||
}
|
||
}
|
||
|
||
if (affectedObject == null) {
|
||
affectedObject = game.getObject(sourceAbility);
|
||
if (affectedObject == null || affectedObject.isPermanent(game)) {
|
||
// if it was a permanent, but now removed then ignore
|
||
return false;
|
||
}
|
||
}
|
||
|
||
if (!sourceAbility.hasSourceObjectAbility(game, affectedObject, event)) {
|
||
return false; // the permanent does currently not have or before it dies the ability so no trigger
|
||
}
|
||
|
||
// check now it is in graveyard (only if it is no token and was the target itself)
|
||
// TODO: need research
|
||
if (affectedSourceId.equals(event.getTargetId()) // source is also the target
|
||
&& !(affectedObject instanceof PermanentToken) // it's no token
|
||
&& affectedObject.getZoneChangeCounter(game) + 1 == game.getState().getZoneChangeCounter(affectedSourceId)) { // It's in the next zone
|
||
Zone after = game.getState().getZone(affectedSourceId);
|
||
if (!Zone.GRAVEYARD.match(after)) { // Zone is not the graveyard
|
||
return false; // Moving to graveyard was replaced so no trigger
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
}
|