CantExile: add checks for Effects and Costs (#4632)

This commit is contained in:
Hans Mackowiak
2024-02-27 13:25:39 +01:00
committed by GitHub
parent 2c05b5b328
commit 3d08e16ab9
37 changed files with 203 additions and 229 deletions

View File

@@ -59,7 +59,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
@Override
public PaymentDecision visit(CostCollectEvidence cost) {
int c = cost.getAbilityAmount(ability);
CardCollectionView chosen = ComputerUtil.chooseCollectEvidence(player, cost, source, c, ability);
CardCollectionView chosen = ComputerUtil.chooseCollectEvidence(player, cost, source, c, ability, isEffect());
return null == chosen ? null : PaymentDecision.card(chosen);
}
@@ -170,7 +170,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
// TODO Determine exile from same zone for AI
return null;
} else {
CardCollectionView chosen = ComputerUtil.chooseExileFrom(player, cost, source, c, ability);
CardCollectionView chosen = ComputerUtil.chooseExileFrom(player, cost, source, c, ability, isEffect());
return null == chosen ? null : PaymentDecision.card(chosen);
}
}

View File

@@ -665,8 +665,8 @@ public class ComputerUtil {
return sacList;
}
public static CardCollection chooseCollectEvidence(final Player ai, CostCollectEvidence cost, final Card activate, int amount, SpellAbility sa) {
CardCollection typeList = new CardCollection(ai.getCardsIn(ZoneType.Graveyard));
public static CardCollection chooseCollectEvidence(final Player ai, CostCollectEvidence cost, final Card activate, int amount, SpellAbility sa, final boolean effect) {
CardCollection typeList = CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), CardPredicates.canExiledBy(sa, effect));
if (CardLists.getTotalCMC(typeList) < amount) return null;
@@ -692,7 +692,7 @@ public class ComputerUtil {
return exileList;
}
public static CardCollection chooseExileFrom(final Player ai, CostExile cost, final Card activate, final int amount, SpellAbility sa) {
public static CardCollection chooseExileFrom(final Player ai, CostExile cost, final Card activate, final int amount, SpellAbility sa, final boolean effect) {
CardCollection typeList;
if (cost.zoneRestriction != 1) {
typeList = new CardCollection(ai.getGame().getCardsIn(cost.from));
@@ -700,6 +700,7 @@ public class ComputerUtil {
typeList = new CardCollection(ai.getCardsIn(cost.from));
}
typeList = CardLists.getValidCards(typeList, cost.getType().split(";"), activate.getController(), activate, sa);
typeList = CardLists.filter(typeList, CardPredicates.canExiledBy(sa, effect));
// don't exile the card we're pumping
typeList = ComputerUtilCost.paymentChoicesWithoutTargets(typeList, sa, ai);

View File

@@ -62,7 +62,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
int amt = part.getAbilityAmount(sa);
needed += amt;
CardCollection toAdd = ComputerUtil.chooseExileFrom(ai, (CostExile) part, source, amt, sa);
CardCollection toAdd = ComputerUtil.chooseExileFrom(ai, (CostExile) part, source, amt, sa, true);
if (toAdd != null) {
payingCards.addAll(toAdd);
}

View File

@@ -24,7 +24,6 @@ import com.google.common.eventbus.EventBus;
import forge.GameCommand;
import forge.card.CardRarity;
import forge.card.CardStateName;
import forge.card.CardType.Supertype;
import forge.game.ability.AbilityKey;
import forge.game.card.*;
import forge.game.combat.Combat;
@@ -950,36 +949,6 @@ public class Game {
activePlanes = activePlane0;
}
public void archenemy904_10() {
//904.10. If a non-ongoing scheme card is face up in the
//command zone, and it isn't the source of a triggered ability
//that has triggered but not yet left the stack, that scheme card
//is turned face down and put on the bottom of its owner's scheme
//deck the next time a player would receive priority.
//(This is a state-based action. See rule 704.)
for (int i = 0; i < getCardsIn(ZoneType.Command).size(); i++) {
Card c = getCardsIn(ZoneType.Command).get(i);
if (c.isScheme() && !c.getType().hasSupertype(Supertype.Ongoing)) {
boolean foundonstack = false;
for (SpellAbilityStackInstance si : stack) {
if (si.getSourceCard().equals(c)) {
foundonstack = true;
break;
}
}
if (!foundonstack) {
getTriggerHandler().suppressMode(TriggerType.ChangesZone);
c.getController().getZone(ZoneType.Command).remove(c);
i--;
getTriggerHandler().clearSuppression(TriggerType.ChangesZone);
c.getController().getZone(ZoneType.SchemeDeck).add(c);
}
}
}
}
public GameStage getAge() {
return age;
}

View File

@@ -24,6 +24,7 @@ import forge.GameCommand;
import forge.StaticData;
import forge.card.CardStateName;
import forge.card.MagicColor;
import forge.card.CardType.Supertype;
import forge.deck.DeckSection;
import forge.game.ability.*;
import forge.game.card.*;
@@ -747,16 +748,11 @@ public class GameAction {
// use FThreads.invokeInNewThread to run code in a pooled thread
return moveTo(zoneTo, c, null, cause, params);
}
public final Card moveTo(final Zone zoneTo, Card c, Integer position, SpellAbility cause) {
return moveTo(zoneTo, c, position, cause, AbilityKey.newMap());
}
public final Card moveTo(final ZoneType name, final Card c, SpellAbility cause, Map<AbilityKey, Object> params) {
return moveTo(name, c, 0, cause, params);
}
public final Card moveTo(final ZoneType name, final Card c, final int libPosition, SpellAbility cause) {
return moveTo(name, c, libPosition, cause, AbilityKey.newMap());
}
public final Card moveTo(final ZoneType name, final Card c, final int libPosition, SpellAbility cause, Map<AbilityKey, Object> params) {
// Call specific functions to set PlayerZone, then move onto moveTo
switch(name) {
@@ -764,7 +760,11 @@ public class GameAction {
case Library: return moveToLibrary(c, libPosition, cause, params);
case Battlefield: return moveToPlay(c, c.getController(), cause, params);
case Graveyard: return moveToGraveyard(c, cause, params);
case Exile: return exile(c, cause, params);
case Exile:
if (!c.canExiledBy(cause, true)) {
return null;
}
return exile(c, cause, params);
case Stack: return moveToStack(c, cause, params);
case PlanarDeck: return moveToVariantDeck(c, ZoneType.PlanarDeck, libPosition, cause, params);
case SchemeDeck: return moveToVariantDeck(c, ZoneType.SchemeDeck, libPosition, cause, params);
@@ -943,6 +943,20 @@ public class GameAction {
return copied;
}
public final Card exileEffect(final Card effect) {
return exile(effect, null, null);
}
public final void moveToCommand(final Card effect, final SpellAbility sa) {
moveToCommand(effect, sa, AbilityKey.newMap());
}
public final void moveToCommand(final Card effect, final SpellAbility sa, Map<AbilityKey, Object> params) {
game.getTriggerHandler().suppressMode(TriggerType.ChangesZone);
moveTo(ZoneType.Command, effect, sa, params);
effect.updateStateForView();
game.getTriggerHandler().clearSuppression(TriggerType.ChangesZone);
}
public void ceaseToExist(Card c, boolean skipTrig) {
if (c.isInZone(ZoneType.Stack)) {
c.getGame().getStack().remove(c);
@@ -1226,12 +1240,6 @@ public class GameAction {
return false;
}
// Max: I don't know where to put this! - but since it's a state based action, it must be in check state effects
if (game.getRules().hasAppliedVariant(GameType.Archenemy)
|| game.getRules().hasAppliedVariant(GameType.ArchenemyRumble)) {
game.archenemy904_10();
}
final boolean refreeze = game.getStack().isFrozen();
game.getStack().setFrozen(true);
game.getTracker().freeze(); //prevent views flickering during while updating for state-based effects
@@ -1266,6 +1274,7 @@ public class GameAction {
checkAgain |= stateBasedAction704_5d(c);
// Dungeon Card won't affect other cards, so don't need to set checkAgain
stateBasedAction_Dungeon(c);
stateBasedAction_Scheme(c);
}
}
}
@@ -1545,6 +1554,15 @@ public class GameAction {
}
}
private void stateBasedAction_Scheme(Card c) {
if (!c.isScheme() || c.getType().hasSupertype(Supertype.Ongoing)) {
return;
}
if (!game.getStack().hasSourceOnStack(c, null)) {
moveTo(ZoneType.SchemeDeck, c, null, AbilityKey.newMap());
}
}
private boolean stateBasedAction704_attach(Card c, CardCollection unAttachList) {
boolean checkAgain = false;

View File

@@ -48,7 +48,6 @@ import forge.game.replacement.ReplacementLayer;
import forge.game.spellability.*;
import forge.game.staticability.StaticAbilityLayer;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
import forge.game.zone.Zone;
import forge.game.zone.ZoneType;
import forge.util.Lang;
@@ -757,10 +756,7 @@ public final class GameActionUtil {
eff.updateStateForView();
// TODO: Add targeting to the effect so it knows who it's dealing with
game.getTriggerHandler().suppressMode(TriggerType.ChangesZone);
game.getAction().moveTo(ZoneType.Command, eff, null, null);
game.getTriggerHandler().clearSuppression(TriggerType.ChangesZone);
game.getAction().moveToCommand(eff, sa);
return eff;
}

View File

@@ -536,11 +536,7 @@ public abstract class SpellAbilityEffect {
eff.copyChangedTextFrom(card);
}
// TODO: Add targeting to the effect so it knows who it's dealing with
game.getTriggerHandler().suppressMode(TriggerType.ChangesZone);
game.getAction().moveTo(ZoneType.Command, eff, sa, null);
eff.updateStateForView();
game.getTriggerHandler().clearSuppression(TriggerType.ChangesZone);
game.getAction().moveToCommand(eff, sa);
}
protected static void addLeaveBattlefieldReplacement(final Card eff, final String zone) {
@@ -648,22 +644,9 @@ public abstract class SpellAbilityEffect {
eff.copyChangedTextFrom(host);
}
final GameCommand endEffect = new GameCommand() {
private static final long serialVersionUID = -5861759814760561373L;
game.getEndOfTurn().addUntil(exileEffectCommand(game, eff));
@Override
public void run() {
game.getAction().exile(eff, null, null);
}
};
game.getEndOfTurn().addUntil(endEffect);
// TODO: Add targeting to the effect so it knows who it's dealing with
game.getTriggerHandler().suppressMode(TriggerType.ChangesZone);
game.getAction().moveTo(ZoneType.Command, eff, sa, null);
eff.updateStateForView();
game.getTriggerHandler().clearSuppression(TriggerType.ChangesZone);
game.getAction().moveToCommand(eff, sa);
}
}
@@ -991,4 +974,15 @@ public abstract class SpellAbilityEffect {
}
return new CardZoneTable(lastStateBattlefield, lastStateGraveyard);
}
public static GameCommand exileEffectCommand(final Game game, final Card effect) {
return new GameCommand() {
private static final long serialVersionUID = 1L;
@Override
public void run() {
game.getAction().exileEffect(effect);
}
};
}
}

View File

@@ -2,7 +2,6 @@ package forge.game.ability.effects;
import forge.game.Game;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
@@ -11,8 +10,6 @@ import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerHandler;
import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
import forge.util.Lang;
import forge.util.Localizer;
@@ -79,10 +76,7 @@ public class AddTurnEffect extends SpellAbilityEffect {
eff.addStaticAbility(stEffect);
game.getTriggerHandler().suppressMode(TriggerType.ChangesZone);
game.getAction().moveTo(ZoneType.Command, eff, sa, AbilityKey.newMap());
eff.updateStateForView();
game.getTriggerHandler().clearSuppression(TriggerType.ChangesZone);
game.getAction().moveToCommand(eff, sa);
}
}

View File

@@ -197,6 +197,9 @@ public class ChangeZoneAllEffect extends SpellAbilityEffect {
c.setController(sa.getActivatingPlayer(), game.getNextTimestamp());
movedCard = game.getAction().moveToPlay(c, sa.getActivatingPlayer(), sa, moveParams);
} else {
if (destination == ZoneType.Exile && !c.canExiledBy(sa, true)) {
continue;
}
movedCard = game.getAction().moveTo(destination, c, libraryPos, sa, moveParams);
if (destination == ZoneType.Exile) {
handleExiledWith(movedCard, sa);

View File

@@ -702,6 +702,9 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
} else {
// might set before card is moved only for nontoken
if (destination.equals(ZoneType.Exile)) {
if (!gameCard.canExiledBy(sa, true)) {
continue;
}
handleExiledWith(gameCard, sa);
}
@@ -1384,6 +1387,9 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
}
}
else if (destination.equals(ZoneType.Exile)) {
if (!c.canExiledBy(sa, true)) {
continue;
}
movedCard = game.getAction().exile(c, sa, moveParams);
handleExiledWith(movedCard, sa);
@@ -1553,6 +1559,9 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
} else if (srcSA.getParam("Destination").equals("Graveyard")) {
movedCard = game.getAction().moveToGraveyard(tgtHost, srcSA, params);
} else if (srcSA.getParam("Destination").equals("Exile")) {
if (!tgtHost.canExiledBy(srcSA, true)) {
return;
}
movedCard = game.getAction().exile(tgtHost, srcSA, params);
handleExiledWith(movedCard, srcSA);
} else if (srcSA.getParam("Destination").equals("TopOfLibrary")) {

View File

@@ -265,6 +265,9 @@ public class CounterEffect extends SpellAbilityEffect {
} else if (destination.equals("Graveyard")) {
movedCard = game.getAction().moveToGraveyard(c, srcSA, params);
} else if (destination.equals("Exile")) {
if (!c.canExiledBy(srcSA, true)) {
return false;
}
movedCard = game.getAction().exile(c, srcSA, params);
} else if (destination.equals("Hand")) {
movedCard = game.getAction().moveToHand(c, srcSA, params);

View File

@@ -6,7 +6,6 @@ import forge.GameCommand;
import forge.game.Game;
import forge.game.GameEntity;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
@@ -16,8 +15,6 @@ import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementHandler;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
import forge.util.TextUtil;
public abstract class DamagePreventEffectBase extends SpellAbilityEffect {
@@ -66,10 +63,7 @@ public abstract class DamagePreventEffectBase extends SpellAbilityEffect {
addForgetOnMovedTrigger(eff, "Battlefield");
}
game.getTriggerHandler().suppressMode(TriggerType.ChangesZone);
game.getAction().moveTo(ZoneType.Command, eff, sa, AbilityKey.newMap());
eff.updateStateForView();
game.getTriggerHandler().clearSuppression(TriggerType.ChangesZone);
game.getAction().moveToCommand(eff, sa);
o.getView().updatePreventNextDamage(o);
if (o instanceof Player) {
@@ -81,7 +75,7 @@ public abstract class DamagePreventEffectBase extends SpellAbilityEffect {
@Override
public void run() {
game.getAction().exile(eff, null, null);
game.getAction().exileEffect(eff);
o.getView().updatePreventNextDamage(o);
if (o instanceof Player) {
game.fireEvent(new GameEventPlayerStatsChanged((Player) o, false));

View File

@@ -379,8 +379,12 @@ public class DigEffect extends SpellAbilityEffect {
for (Card c : movedCards) {
if (destZone1.equals(ZoneType.Library) || destZone1.equals(ZoneType.PlanarDeck) || destZone1.equals(ZoneType.SchemeDeck)) {
c = game.getAction().moveTo(destZone1, c, libraryPosition, sa);
c = game.getAction().moveTo(destZone1, c, libraryPosition, sa, AbilityKey.newMap());
} else {
if (destZone1.equals(ZoneType.Exile) && !c.canExiledBy(sa, true)) {
continue;
}
if (sa.hasParam("Tapped")) {
c.setTapped(true);
}
@@ -470,6 +474,9 @@ public class DigEffect extends SpellAbilityEffect {
} else {
// just move them randomly
for (Card c : rest) {
if (destZone2 == ZoneType.Exile && !c.canExiledBy(sa, true)) {
continue;
}
c = game.getAction().moveTo(destZone2, c, sa, moveParams);
if (destZone2 == ZoneType.Exile) {
if (sa.hasParam("ExileWithCounter")) {

View File

@@ -6,6 +6,7 @@ import java.util.Map;
import com.google.common.collect.Maps;
import forge.game.Game;
import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
@@ -120,7 +121,7 @@ public class DigMultipleEffect extends SpellAbilityEffect {
if (!sa.hasParam("ChangeLater")) {
if (zone.is(ZoneType.Library) || zone.is(ZoneType.PlanarDeck) || zone.is(ZoneType.SchemeDeck)) {
c = game.getAction().moveTo(destZone1, c, libraryPosition, sa);
c = game.getAction().moveTo(destZone1, c, libraryPosition, sa, AbilityKey.newMap());
} else {
if (destZone1.equals(ZoneType.Battlefield)) {
if (sa.hasParam("Tapped")) {
@@ -164,7 +165,7 @@ public class DigMultipleEffect extends SpellAbilityEffect {
}
for (final Card c : afterOrder) {
final ZoneType origin = c.getZone().getZoneType();
Card m = game.getAction().moveTo(destZone2, c, libraryPosition2, sa);
Card m = game.getAction().moveTo(destZone2, c, libraryPosition2, sa, AbilityKey.newMap());
if (m != null && !origin.equals(m.getZone().getZoneType())) {
table.put(origin, m.getZone().getZoneType(), m);
}

View File

@@ -51,7 +51,6 @@ import java.util.*;
CardCollection drafted = new CardCollection();
for (int i = 0; i < numToDraft; i++) {
String chosen = "";
Collections.shuffle(spellbook);
List<Card> draftOptions = new ArrayList<>();
for (String name : spellbook.subList(0, 3)) {
@@ -69,6 +68,10 @@ import java.util.*;
final CardZoneTable triggerList = new CardZoneTable();
for (final Card c : drafted) {
if (zone.equals(ZoneType.Exile) && !c.canExiledBy(sa, true)) {
continue;
}
Card made = game.getAction().moveTo(zone, c, sa, moveParams);
if (zone.equals(ZoneType.Exile)) {
handleExiledWith(made, sa);

View File

@@ -8,7 +8,6 @@ import java.util.Map;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.GameCommand;
import forge.ImageKeys;
import forge.card.CardRarity;
import forge.game.Game;
@@ -26,7 +25,6 @@ import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbility;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerHandler;
import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
import forge.util.TextUtil;
import forge.util.collect.FCollection;
@@ -307,30 +305,14 @@ public class EffectEffect extends SpellAbilityEffect {
}
if (duration == null || !duration.equals("Permanent")) {
final GameCommand endEffect = new GameCommand() {
private static final long serialVersionUID = -5861759814760561373L;
@Override
public void run() {
game.getAction().exile(eff, null, null);
}
};
addUntilCommand(sa, endEffect, controller);
addUntilCommand(sa, exileEffectCommand(game, eff), controller);
}
if (sa.hasParam("ImprintOnHost")) {
hostCard.addImprintedCard(eff);
}
// TODO: Add targeting to the effect so it knows who it's dealing with
game.getTriggerHandler().suppressMode(TriggerType.ChangesZone);
game.getAction().moveTo(ZoneType.Command, eff, sa, params);
eff.updateStateForView();
game.getTriggerHandler().clearSuppression(TriggerType.ChangesZone);
//if (effectTriggers != null) {
// game.getTriggerHandler().registerActiveTrigger(cmdEffect, false);
//}
game.getAction().moveToCommand(eff, sa);
}
}

View File

@@ -1,15 +1,11 @@
package forge.game.ability.effects;
import forge.GameCommand;
import forge.game.Game;
import forge.game.ability.AbilityKey;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementHandler;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
public class FogEffect extends SpellAbilityEffect {
@@ -32,18 +28,8 @@ public class FogEffect extends SpellAbilityEffect {
ReplacementEffect re = ReplacementHandler.parseReplacement(repeffstr, eff, true);
eff.addReplacementEffect(re);
game.getTriggerHandler().suppressMode(TriggerType.ChangesZone);
game.getAction().moveTo(ZoneType.Command, eff, sa, AbilityKey.newMap());
eff.updateStateForView();
game.getTriggerHandler().clearSuppression(TriggerType.ChangesZone);
game.getAction().moveToCommand(eff, sa);
game.getEndOfTurn().addUntil(new GameCommand() {
private static final long serialVersionUID = -3297629217432253089L;
@Override
public void run() {
game.getAction().exile(eff, null, null);
}
});
game.getEndOfTurn().addUntil(exileEffectCommand(game, eff));
}
}

View File

@@ -39,7 +39,7 @@ public class MeldEffect extends SpellAbilityEffect {
Card secondary = controller.getController().chooseSingleEntityForEffect(field, sa, Localizer.getInstance().getMessage("lblChooseCardToMeld"), null);
CardCollection exiled = new CardCollection(Arrays.asList(hostCard, secondary));
CardCollection exiled = CardLists.filter(Arrays.asList(hostCard, secondary), CardPredicates.canExiledBy(sa, true));
Map<AbilityKey, Object> moveParams = AbilityKey.newMap();
CardZoneTable table = new CardZoneTable(sa.getLastStateBattlefield(), sa.getLastStateGraveyard());
@@ -48,8 +48,12 @@ public class MeldEffect extends SpellAbilityEffect {
exiled = game.getAction().exile(exiled, sa, moveParams);
table.triggerChangesZoneAll(game, sa);
Card primary = exiled.get(0);
secondary = exiled.get(1);
if (exiled.size() < 2) {
return;
}
Card primary = exiled.get(hostCard);
secondary = exiled.get(secondary);
// cards has wrong name in exile
if (!primary.sharesNameWith(primName) || !secondary.sharesNameWith(secName)) {

View File

@@ -80,7 +80,7 @@ public class MutateEffect extends SpellAbilityEffect {
game.getTriggerHandler().clearActiveTriggers(target, null);
game.getTriggerHandler().registerActiveTrigger(target, false);
game.getAction().moveTo(p.getZone(ZoneType.Merged), host, sa);
game.getAction().moveTo(p.getZone(ZoneType.Merged), host, sa, AbilityKey.newMap());
host.setTapped(target.isTapped());
host.setFlipped(target.isFlipped());

View File

@@ -15,7 +15,6 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.GameCommand;
import forge.StaticData;
import forge.card.CardRulesPredicates;
import forge.game.Game;
@@ -42,7 +41,6 @@ import forge.game.spellability.AlternativeCost;
import forge.game.spellability.LandAbility;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityPredicates;
import forge.game.trigger.TriggerType;
import forge.game.zone.Zone;
import forge.game.zone.ZoneType;
import forge.item.PaperCard;
@@ -521,24 +519,11 @@ public class PlayEffect extends SpellAbilityEffect {
eff.copyChangedTextFrom(hostCard);
}
final GameCommand endEffect = new GameCommand() {
private static final long serialVersionUID = -5861759814760561373L;
@Override
public void run() {
game.getAction().exile(eff, null, null);
}
};
game.getEndOfTurn().addUntil(endEffect);
game.getEndOfTurn().addUntil(exileEffectCommand(game, eff));
tgtSA.addRollbackEffect(eff);
// TODO: Add targeting to the effect so it knows who it's dealing with
game.getTriggerHandler().suppressMode(TriggerType.ChangesZone);
game.getAction().moveTo(ZoneType.Command, eff, sa, moveParams);
eff.updateStateForView();
game.getTriggerHandler().clearSuppression(TriggerType.ChangesZone);
game.getAction().moveToCommand(eff, sa);
}
protected void addIllusionaryMaskReplace(Card c, SpellAbility sa, Map<AbilityKey, Object> moveParams) {
@@ -572,9 +557,6 @@ public class PlayEffect extends SpellAbilityEffect {
addExileOnMovedTrigger(eff, "Battlefield");
addExileOnCounteredTrigger(eff);
game.getTriggerHandler().suppressMode(TriggerType.ChangesZone);
game.getAction().moveTo(ZoneType.Command, eff, sa, moveParams);
eff.updateStateForView();
game.getTriggerHandler().clearSuppression(TriggerType.ChangesZone);
game.getAction().moveToCommand(eff, sa);
}
}

View File

@@ -2,10 +2,8 @@ package forge.game.ability.effects;
import java.util.Collection;
import forge.GameCommand;
import forge.game.Game;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
@@ -13,8 +11,6 @@ import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementHandler;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
public abstract class RegenerateBaseEffect extends SpellAbilityEffect {
@@ -67,19 +63,8 @@ public abstract class RegenerateBaseEffect extends SpellAbilityEffect {
for (final Card c : list) {
c.incShieldCount();
}
game.getTriggerHandler().suppressMode(TriggerType.ChangesZone);
game.getAction().moveTo(ZoneType.Command, eff, sa, AbilityKey.newMap());
eff.updateStateForView();
game.getTriggerHandler().clearSuppression(TriggerType.ChangesZone);
game.getAction().moveToCommand(eff, sa);
final GameCommand untilEOT = new GameCommand() {
private static final long serialVersionUID = 259368227093961103L;
@Override
public void run() {
game.getAction().exile(eff, null, null);
}
};
game.getEndOfTurn().addUntil(untilEOT);
game.getEndOfTurn().addUntil(exileEffectCommand(game, eff));
}
}

View File

@@ -48,7 +48,7 @@ public class ReplaceDamageEffect extends SpellAbilityEffect {
if (!StringUtils.isNumeric(varValue) && card.getSVar(varValue).startsWith("Number$")) {
if (card.isImmutable() && prevent <= 0) {
game.getAction().exile(card, null, null);
game.getAction().exileEffect(card);
} else {
card.setSVar(varValue, "Number$" + prevent);
card.updateAbilityTextForView();

View File

@@ -46,7 +46,7 @@ public class ReplaceSplitDamageEffect extends SpellAbilityEffect {
prevent -= n;
if (card.isImmutable() && prevent <= 0) {
game.getAction().exile(card, null, null);
game.getAction().exileEffect(card);
} else if (!StringUtils.isNumeric(varValue)) {
sa.setSVar(varValue, "Number$" + prevent);
card.updateAbilityTextForView();

View File

@@ -12,7 +12,6 @@ import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbilityCantSetSchemesInMotion;
import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
public class SetInMotionEffect extends SpellAbilityEffect {
@@ -40,16 +39,14 @@ public class SetInMotionEffect extends SpellAbilityEffect {
return;
}
game.getTriggerHandler().suppressMode(TriggerType.ChangesZone);
game.getAction().moveTo(ZoneType.Command, controller.getActiveScheme(), null, AbilityKey.newMap());
game.getTriggerHandler().clearSuppression(TriggerType.ChangesZone);
game.getAction().moveToCommand(controller.getActiveScheme(), sa);
// Run triggers
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
runParams.put(AbilityKey.Scheme, controller.getActiveScheme());
game.getTriggerHandler().runTrigger(TriggerType.SetInMotion, runParams, false);
} else {
controller.setSchemeInMotion();
controller.setSchemeInMotion(sa);
}
}
}

View File

@@ -5,7 +5,6 @@ import java.util.List;
import forge.GameCommand;
import forge.game.Game;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityKey;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.player.Player;
@@ -13,8 +12,6 @@ import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementHandler;
import forge.game.replacement.ReplacementLayer;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
public class SkipPhaseEffect extends SpellAbilityEffect {
@@ -102,16 +99,7 @@ public class SkipPhaseEffect extends SpellAbilityEffect {
re.setOverridingAbility(exile);
}
if (duration != null) {
final GameCommand endEffect = new GameCommand() {
private static final long serialVersionUID = -5861759814760561373L;
@Override
public void run() {
game.getAction().exile(eff, null, null);
}
};
addUntilCommand(sa, endEffect);
addUntilCommand(sa, exileEffectCommand(game, eff));
}
eff.addReplacementEffect(re);
@@ -121,18 +109,12 @@ public class SkipPhaseEffect extends SpellAbilityEffect {
@Override
public void run() {
game.getTriggerHandler().suppressMode(TriggerType.ChangesZone);
game.getAction().moveTo(ZoneType.Command, eff, sa, AbilityKey.newMap());
eff.updateStateForView();
game.getTriggerHandler().clearSuppression(TriggerType.ChangesZone);
game.getAction().moveToCommand(eff, sa);
}
};
game.getUpkeep().addUntil(player, startEffect);
} else {
game.getTriggerHandler().suppressMode(TriggerType.ChangesZone);
game.getAction().moveTo(ZoneType.Command, eff, sa, AbilityKey.newMap());
eff.updateStateForView();
game.getTriggerHandler().clearSuppression(TriggerType.ChangesZone);
game.getAction().moveToCommand(eff, sa);
}
}
}

View File

@@ -4,7 +4,6 @@ import java.util.List;
import forge.game.Game;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
@@ -14,8 +13,6 @@ import forge.game.replacement.ReplacementHandler;
import forge.game.replacement.ReplacementLayer;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
import forge.util.Lang;
public class SkipTurnEffect extends SpellAbilityEffect {
@@ -61,10 +58,7 @@ public class SkipTurnEffect extends SpellAbilityEffect {
re.setOverridingAbility(calcTurn);
eff.addReplacementEffect(re);
game.getTriggerHandler().suppressMode(TriggerType.ChangesZone);
game.getAction().moveTo(ZoneType.Command, eff, sa, AbilityKey.newMap());
eff.updateStateForView();
game.getTriggerHandler().clearSuppression(TriggerType.ChangesZone);
game.getAction().moveToCommand(eff, sa);
}
}
}

View File

@@ -59,9 +59,7 @@ public class VentureEffect extends SpellAbilityEffect {
String message = Localizer.getInstance().getMessage("lblChooseDungeon");
Card dungeon = player.getController().chooseDungeon(player, dungeonCards, message);
game.getTriggerHandler().suppressMode(TriggerType.ChangesZone);
game.getAction().moveTo(ZoneType.Command, dungeon, sa, moveParams);
game.getTriggerHandler().clearSuppression(TriggerType.ChangesZone);
game.getAction().moveToCommand(dungeon, sa, moveParams);
return dungeon;
}

View File

@@ -7058,6 +7058,18 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
return !StaticAbilityCantSacrifice.cantSacrifice(this, source, effect);
}
public final boolean canExiledBy(final SpellAbility source, final boolean effect) {
final Card gameCard = game.getCardState(this, null);
// gameCard is LKI in that case, the card is not in game anymore
// or the timestamp did change
// this should check Self too
if (gameCard == null || !this.equalsWithTimestamp(gameCard)) {
return false;
}
return !StaticAbilityCantExile.cantExile(this, source, effect);
}
public CardRules getRules() {
return cardRules;
}

View File

@@ -251,6 +251,15 @@ public final class CardPredicates {
};
}
public static final Predicate<Card> canExiledBy(final SpellAbility sa, final boolean effect) {
return new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
return c.canExiledBy(sa, effect);
}
};
}
public static final Predicate<Card> canBeAttached(final Card aura, final SpellAbility sa) {
return new Predicate<Card>() {
@Override

View File

@@ -7,6 +7,7 @@ import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.TriggerType;
@@ -47,7 +48,7 @@ public class CostCollectEvidence extends CostPartWithList {
// This may need to be updated if we get a card like "Cards in graveyards can't be exiled to pay for costs"
return CardLists.getTotalCMC(payer.getCardsIn(ZoneType.Graveyard)) >= amount;
return CardLists.getTotalCMC(CardLists.filter(payer.getCardsIn(ZoneType.Graveyard), CardPredicates.canExiledBy(ability, effect))) >= amount;
}
@Override

View File

@@ -175,8 +175,8 @@ public class CostExile extends CostPartWithList {
type = TextUtil.fastReplace(type, "FromTopGrave", "");
}
CardCollection list = new CardCollection(zoneRestriction != 1 ? game.getCardsIn(this.from) :
payer.getCardsIn(this.from));
CardCollection list = CardLists.filter(zoneRestriction != 1 ? game.getCardsIn(this.from) :
payer.getCardsIn(this.from), CardPredicates.canExiledBy(ability, effect));
if (this.payCostFromSource()) {
return list.contains(source);
@@ -216,7 +216,6 @@ public class CostExile extends CostPartWithList {
}
if (totalCMC) {
int needed = Integer.parseInt(this.getAmount().split("\\+")[0]);
if (totalM.equals("X") && ability.getXManaCostPaid() == null) { // X hasn't yet been decided, let it pass
return true;
}

View File

@@ -281,7 +281,7 @@ public class PhaseHandler implements java.io.Serializable {
case MAIN1:
{
if (playerTurn.isArchenemy()) {
playerTurn.setSchemeInMotion();
playerTurn.setSchemeInMotion(null);
}
if (playerTurn.hasRadiationEffect()) {
handleRadiation();

View File

@@ -266,7 +266,7 @@ public class Player extends GameEntity implements Comparable<Player> {
return activeScheme;
}
public void setSchemeInMotion() {
public void setSchemeInMotion(SpellAbility cause) {
if (StaticAbilityCantSetSchemesInMotion.any(getGame())) {
return;
}
@@ -280,10 +280,9 @@ public class Player extends GameEntity implements Comparable<Player> {
moveParams.put(AbilityKey.LastStateBattlefield, game.getLastStateBattlefield());
moveParams.put(AbilityKey.LastStateGraveyard, game.getLastStateGraveyard());
game.getTriggerHandler().suppressMode(TriggerType.ChangesZone);
activeScheme = getZone(ZoneType.SchemeDeck).get(0);
game.getAction().moveTo(ZoneType.Command, activeScheme, null, moveParams);
game.getTriggerHandler().clearSuppression(TriggerType.ChangesZone);
game.getAction().moveToCommand(activeScheme, cause);
game.getTriggerHandler().suppressMode(TriggerType.ChangesZone);
// Run triggers
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
@@ -2690,7 +2689,7 @@ public class Player extends GameEntity implements Comparable<Player> {
game.getView().updatePlanarPlayer(getView());
for (Card c : destinations) {
currentPlanes.add(game.getAction().moveTo(getZone(ZoneType.Command), c, sa));
currentPlanes.add(game.getAction().moveTo(getZone(ZoneType.Command), c, sa, AbilityKey.newMap()));
planeswalkedToThisTurn.add(c);
}
@@ -2712,7 +2711,7 @@ public class Player extends GameEntity implements Comparable<Player> {
for (final Card plane : currentPlanes) {
plane.clearControllers();
game.getAction().moveTo(ZoneType.PlanarDeck, plane, -1, null);
game.getAction().moveTo(ZoneType.PlanarDeck, plane, -1, null, AbilityKey.newMap());
}
currentPlanes.clear();
}
@@ -3801,7 +3800,7 @@ public class Player extends GameEntity implements Comparable<Player> {
}
if (c.isInZone(ZoneType.Sideboard)) { // Sideboard Lesson to Hand
game.getAction().reveal(new CardCollection(c), c.getOwner(), true);
Card moved = game.getAction().moveTo(ZoneType.Hand, c, sa, params);
game.getAction().moveTo(ZoneType.Hand, c, sa, params);
} else if (c.isInZone(ZoneType.Hand)) { // Discard and Draw
boolean firstDiscard = getNumDiscardedThisTurn() == 0;
if (discard(c, sa, true, params) != null) {

View File

@@ -250,10 +250,7 @@ public class AbilityManaPart implements java.io.Serializable {
SpellAbilityEffect.addForgetOnMovedTrigger(eff, "Stack");
game.getTriggerHandler().suppressMode(TriggerType.ChangesZone);
game.getAction().moveTo(ZoneType.Command, eff, null, null);
eff.updateStateForView();
game.getTriggerHandler().clearSuppression(TriggerType.ChangesZone);
game.getAction().moveToCommand(eff, null);
}
/**

View File

@@ -0,0 +1,42 @@
package forge.game.staticability;
import forge.game.Game;
import forge.game.card.Card;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
public class StaticAbilityCantExile {
static String MODE = "CantExile";
public static boolean cantExile(final Card card, final SpellAbility cause, final boolean effect) {
final Game game = card.getGame();
for (final Card ca : game.getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) {
for (final StaticAbility stAb : ca.getStaticAbilities()) {
if (!stAb.checkConditions(MODE)) {
continue;
}
if (applyCantExileAbility(stAb, card, cause, effect)) {
return true;
}
}
}
return false;
}
public static boolean applyCantExileAbility(final StaticAbility stAb, final Card card, final SpellAbility cause, final boolean effect) {
if (!stAb.matchesValidParam("ValidCard", card)) {
return false;
}
if (stAb.hasParam("ForCost")) {
if ("True".equalsIgnoreCase(stAb.getParam("ForCost")) == effect) {
return false;
}
}
if (!stAb.matchesValidParam("ValidCause", cause)) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,9 @@
Name:The Master, Multiplied
ManaCost:4 B R
Types:Legendary Creature Time Lord Rogue
PT:4/3
K:Myriad
S:Mode$ IgnoreLegendRule | ValidCard$ Creature.YouCtrl+token | Description$ The "legend rule" doesn't apply to creature tokens you control.
S:Mode$ CantSacrifice | ValidCard$ Creature.YouCtrl+token | ValidCause$ Triggered.YouCtrl | ForCost$ False | Description$ Triggered abilities you control can't cause you to sacrifice or exile creature tokens you control.
S:Mode$ CantExile | ValidCard$ Creature.YouCtrl+token | ValidCause$ Triggered.YouCtrl | ForCost$ False | Secondary$ True | Description$ Triggered abilities you control can't cause you to sacrifice or exile creature tokens you control.
Oracle:Myriad\nThe "legend rule" doesn't apply to creature tokens you control.\nTriggered abilities you control can't cause you to sacrifice or exile creature tokens you control.

View File

@@ -81,7 +81,7 @@ public class HumanCostDecision extends CostDecisionMakerBase {
@Override
public PaymentDecision visit(final CostCollectEvidence cost) {
CardCollection list = new CardCollection(player.getCardsIn(ZoneType.Graveyard));
CardCollection list = CardLists.filter(player.getCardsIn(ZoneType.Graveyard), CardPredicates.canExiledBy(ability, isEffect()));
final int total = AbilityUtils.calculateAmount(source, cost.getAmount(), ability);
final InputSelectCardsFromList inp =
new InputSelectCardsFromList(controller, 0, list.size(), list, ability, total);
@@ -239,6 +239,9 @@ public class HumanCostDecision extends CostDecisionMakerBase {
@Override
public PaymentDecision visit(final CostExile cost) {
if (cost.payCostFromSource()) {
if (!source.canExiledBy(ability, isEffect())) {
return null;
}
return source.getZone() == player.getZone(cost.from.get(0)) && confirmAction(cost, Localizer.getInstance().getMessage("lblExileConfirm", CardTranslation.getTranslatedName(source.getName()))) ? PaymentDecision.card(source) : null;
}
@@ -274,6 +277,7 @@ public class HumanCostDecision extends CostDecisionMakerBase {
return PaymentDecision.card(list);
}
list = CardLists.getValidCards(list, type.split(";"), player, source, ability);
list = CardLists.filter(list, CardPredicates.canExiledBy(ability, isEffect()));
if (totalCMC) {
int needed = Integer.parseInt(cost.getAmount().split("\\+")[0]);