From ea23bb33b292df334dacad70be9909ca99e4df0c Mon Sep 17 00:00:00 2001 From: elcnesh Date: Wed, 3 Dec 2014 08:49:49 +0000 Subject: [PATCH] Codechanges and lots of fixes related to playing other player's cards from exile. - Cards now keep track of who's allowed to cast them, fixing possible issues in multiplayer. - Face-down cards can now be properly looked at when allowed to by a static ability. - Improve support for casting a face-down exiled card in general. - Add Shared Fate (including AI support). --- .gitattributes | 2 + .../src/main/java/forge/ai/AiController.java | 14 +++++- .../main/java/forge/game/GameActionUtil.java | 44 +++++++++++-------- .../main/java/forge/game/StaticEffects.java | 8 +++- .../game/ability/effects/EffectEffect.java | 2 - .../src/main/java/forge/game/card/Card.java | 42 +++++++++++++++--- .../java/forge/game/card/CardPlayOption.java | 36 +++++++++++++++ .../main/java/forge/game/card/CardView.java | 16 ++++++- .../main/java/forge/game/player/Player.java | 14 +++--- .../java/forge/game/spellability/Spell.java | 3 +- .../spellability/SpellAbilityRestriction.java | 13 +++--- .../game/staticability/StaticAbility.java | 3 +- .../StaticAbilityContinuous.java | 14 +++++- .../forge/game/trigger/TriggerHandler.java | 15 ++++--- .../main/java/forge/game/zone/MagicStack.java | 4 +- .../forge/trackable/TrackableProperty.java | 4 +- .../res/cardsfolder/d/daxos_of_meletis.txt | 5 +-- .../cardsfolder/f/fiend_of_the_shadows.txt | 7 ++- forge-gui/res/cardsfolder/h/havengul_lich.txt | 3 +- .../res/cardsfolder/k/kheru_spellsnatcher.txt | 5 +-- .../res/cardsfolder/k/knacksaw_clique.txt | 9 ++-- forge-gui/res/cardsfolder/m/muse_vessel.txt | 8 ++-- .../res/cardsfolder/n/nightveil_specter.txt | 3 +- .../res/cardsfolder/o/ornate_kanzashi.txt | 7 ++- .../cardsfolder/p/planeswalkers_mischief.txt | 4 +- .../res/cardsfolder/p/praetors_grasp.txt | 8 +++- forge-gui/res/cardsfolder/s/shared_fate.txt | 18 ++++++++ .../forge/player/HumanPlaySpellAbility.java | 33 +++++++++----- 28 files changed, 251 insertions(+), 93 deletions(-) create mode 100644 forge-game/src/main/java/forge/game/card/CardPlayOption.java create mode 100644 forge-gui/res/cardsfolder/s/shared_fate.txt diff --git a/.gitattributes b/.gitattributes index 0645b6040d8..c5056558d0c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -446,6 +446,7 @@ forge-game/src/main/java/forge/game/card/CardDamageHistory.java -text forge-game/src/main/java/forge/game/card/CardFactory.java svneol=native#text/plain forge-game/src/main/java/forge/game/card/CardFactoryUtil.java svneol=native#text/plain forge-game/src/main/java/forge/game/card/CardLists.java svneol=native#text/plain +forge-game/src/main/java/forge/game/card/CardPlayOption.java -text forge-game/src/main/java/forge/game/card/CardPowerToughness.java svneol=native#text/plain forge-game/src/main/java/forge/game/card/CardPredicates.java svneol=native#text/plain forge-game/src/main/java/forge/game/card/CardShields.java -text @@ -12181,6 +12182,7 @@ forge-gui/res/cardsfolder/s/sharding_sphinx.txt svneol=native#text/plain forge-gui/res/cardsfolder/s/shardless_agent.txt -text forge-gui/res/cardsfolder/s/shared_animosity.txt -text forge-gui/res/cardsfolder/s/shared_discovery.txt svneol=native#text/plain +forge-gui/res/cardsfolder/s/shared_fate.txt -text forge-gui/res/cardsfolder/s/shared_trauma.txt -text forge-gui/res/cardsfolder/s/shared_triumph.txt svneol=native#text/plain forge-gui/res/cardsfolder/s/sharpened_pitchfork.txt -text diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index 2ed7fd995ab..ee4233f2147 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -34,6 +34,7 @@ import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.collect.Iterables; +import forge.card.CardStateName; import forge.card.CardType; import forge.card.MagicColor; import forge.card.CardType.Supertype; @@ -335,7 +336,7 @@ public class AiController { sa.setActivatingPlayer(player); //add alternative costs as additional spell abilities newAbilities.add(sa); - newAbilities.addAll(GameActionUtil.getAlternativeCosts(sa)); + newAbilities.addAll(GameActionUtil.getAlternativeCosts(sa, player)); } final ArrayList result = new ArrayList(); @@ -352,6 +353,11 @@ public class AiController { for (final SpellAbility sa : c.getSpellAbilities()) { spellAbilities.add(sa); } + if (c.isFaceDown() && c.isInZone(ZoneType.Exile) && c.mayPlay(player) != null) { + for (final SpellAbility sa : c.getState(CardStateName.Original).getSpellAbilities()) { + spellAbilities.add(sa); + } + } } return spellAbilities; } @@ -392,11 +398,15 @@ public class AiController { }); final CardCollection landsNotInHand = new CardCollection(player.getCardsIn(ZoneType.Graveyard)); + landsNotInHand.addAll(game.getCardsIn(ZoneType.Exile)); if (!player.getCardsIn(ZoneType.Library).isEmpty()) { landsNotInHand.add(player.getCardsIn(ZoneType.Library).get(0)); } for (final Card crd : landsNotInHand) { - if (crd.isLand() && crd.hasKeyword("May be played")) { + if (!(crd.isLand() || (crd.isFaceDown() && crd.getState(CardStateName.Original).getType().isLand()))) { + continue; + } + if (crd.hasKeyword("May be played") || crd.mayPlay(player) != null) { landList.add(crd); } } diff --git a/forge-game/src/main/java/forge/game/GameActionUtil.java b/forge-game/src/main/java/forge/game/GameActionUtil.java index 9451fde067b..eaeb8fa6bd0 100644 --- a/forge-game/src/main/java/forge/game/GameActionUtil.java +++ b/forge-game/src/main/java/forge/game/GameActionUtil.java @@ -29,6 +29,7 @@ import forge.game.ability.ApiType; import forge.game.card.Card; import forge.game.card.CardCollectionView; import forge.game.card.CardLists; +import forge.game.card.CardPlayOption; import forge.game.card.CardPredicates; import forge.game.cost.Cost; import forge.game.mana.ManaCostBeingPaid; @@ -143,20 +144,37 @@ public final class GameActionUtil { /** *

- * getAlternativeCosts. + * Find the alternative costs to a {@link SpellAbility}. *

* * @param sa - * a SpellAbility. - * @return an ArrayList. - * get alternative costs as additional spell abilities + * a {@link SpellAbility}. + * @param activator + * the {@link Player} for which to calculate available + * @return a {@link List} of {@link SpellAbility} objects, each representing + * a possible alternative cost the provided activator can use to pay + * the provided {@link SpellAbility}. */ - public static final ArrayList getAlternativeCosts(SpellAbility sa) { - ArrayList alternatives = new ArrayList(); - Card source = sa.getHostCard(); + public static final List getAlternativeCosts(final SpellAbility sa, final Player activator) { + final List alternatives = new ArrayList(); if (!sa.isBasicSpell()) { return alternatives; } + + final Card source = sa.getHostCard(); + final CardPlayOption playOption = source.mayPlay(activator); + if (sa.isSpell() && playOption != null && playOption.isWithoutManaCost()) { + final SpellAbility newSA = sa.copy(); + final SpellAbilityRestriction sar = new SpellAbilityRestriction(); + sar.setVariables(sa.getRestrictions()); + sar.setZone(null); + newSA.setRestrictions(sar); + newSA.setBasicSpell(false); + newSA.setPayCosts(newSA.getPayCosts().copyWithNoMana()); + newSA.setDescription(sa.getDescription() + " (without paying its mana cost)"); + alternatives.add(newSA); + } + for (final String keyword : source.getKeywords()) { if (sa.isSpell() && keyword.startsWith("Flashback")) { final SpellAbility flashback = sa.copy(); @@ -183,18 +201,6 @@ public final class GameActionUtil { newSA.setDescription(sa.getDescription() + " (without paying its mana cost)"); alternatives.add(newSA); } - if (sa.isSpell() && keyword.equals("May be played by your opponent without paying its mana cost")) { - final SpellAbility newSA = sa.copy(); - SpellAbilityRestriction sar = new SpellAbilityRestriction(); - sar.setVariables(sa.getRestrictions()); - sar.setZone(null); - sar.setOpponentOnly(true); - newSA.setRestrictions(sar); - newSA.setBasicSpell(false); - newSA.setPayCosts(newSA.getPayCosts().copyWithNoMana()); - newSA.setDescription(sa.getDescription() + " (without paying its mana cost)"); - alternatives.add(newSA); - } if (sa.isSpell() && keyword.startsWith("May be played without paying its mana cost and as though it has flash")) { final SpellAbility newSA = sa.copy(); SpellAbilityRestriction sar = new SpellAbilityRestriction(); diff --git a/forge-game/src/main/java/forge/game/StaticEffects.java b/forge-game/src/main/java/forge/game/StaticEffects.java index 68f9050cc25..f2d500c9283 100644 --- a/forge-game/src/main/java/forge/game/StaticEffects.java +++ b/forge-game/src/main/java/forge/game/StaticEffects.java @@ -92,7 +92,7 @@ public class StaticEffects { boolean setPT = false; String[] addHiddenKeywords = null; String addColors = null; - boolean removeMayLookAt = false; + boolean removeMayLookAt = false, removeMayPlay = false; if (params.containsKey("ChangeColorWordsTo")) { changeColorWordsTo = params.get("ChangeColorWordsTo"); @@ -158,6 +158,9 @@ public class StaticEffects { if (params.containsKey("MayLookAt")) { removeMayLookAt = true; } + if (params.containsKey("MayPlay")) { + removeMayPlay = true; + } if (params.containsKey("IgnoreEffectCost")) { for (final SpellAbility s : se.getSource().getSpellAbilities()) { @@ -254,6 +257,9 @@ public class StaticEffects { if (removeMayLookAt) { affectedCard.setMayLookAt(controller, false); } + if (removeMayPlay) { + affectedCard.removeMayPlay(controller); + } } se.clearTimestamps(); return affectedCards; diff --git a/forge-game/src/main/java/forge/game/ability/effects/EffectEffect.java b/forge-game/src/main/java/forge/game/ability/effects/EffectEffect.java index 98b6f6cbc8e..5b78e815d0a 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/EffectEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/EffectEffect.java @@ -112,8 +112,6 @@ public class EffectEffect extends SpellAbilityEffect { eff.setImmutable(true); eff.setEffectSource(hostCard); - // Effects should be Orange or something probably - final Card e = eff; // Grant SVars first in order to give references to granted abilities diff --git a/forge-game/src/main/java/forge/game/card/Card.java b/forge-game/src/main/java/forge/game/card/Card.java index 83a829ef97a..859073e2f3a 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -136,6 +136,8 @@ public class Card extends GameEntity implements Comparable, IIdentifiable // if this card is an Aura, what Entity is it enchanting? private GameEntity enchanting = null; + private final Map mayPlay = Maps.newTreeMap(); + // changes by AF animate and continuous static effects - timestamp is the key of maps private Map changedCardTypes = new ConcurrentSkipListMap(); private Map changedCardKeywords = new ConcurrentSkipListMap(); @@ -1503,10 +1505,22 @@ public class Card extends GameEntity implements Comparable, IIdentifiable public String getAbilityText() { return getAbilityText(currentState); } - public String getAbilityText(CardState state) { + public String getAbilityText(final CardState state) { final CardTypeView type = state.getType(); + + final StringBuilder sb = new StringBuilder(); + if (!mayPlay.isEmpty()) { + sb.append("May be played by: "); + sb.append(Lang.joinHomogenous(mayPlay.entrySet(), new Function, String>() { + @Override public String apply(final Entry entry) { + return entry.getKey().toString() + entry.getValue().toString(); + } + })); + sb.append("\r\n"); + } + if (type.isInstant() || type.isSorcery()) { - final StringBuilder sb = abilityTextInstantSorcery(state); + sb.append(abilityTextInstantSorcery(state)); if (haunting != null) { sb.append("Haunting: ").append(haunting); @@ -1520,8 +1534,6 @@ public class Card extends GameEntity implements Comparable, IIdentifiable return sb.toString().replaceAll("CARDNAME", state.getName()); } - final StringBuilder sb = new StringBuilder(); - if (monstrous) { sb.append("Monstrous\r\n"); } @@ -2141,6 +2153,17 @@ public class Card extends GameEntity implements Comparable, IIdentifiable view.setPlayerMayLook(player, mayLookAt, temp); } + public final CardPlayOption mayPlay(final Player player) { + return mayPlay.get(player); + } + public final void setMayPlay(final Player player, final boolean withoutManaCost, final boolean ignoreColor) { + final CardPlayOption option = this.mayPlay.get(player); + this.mayPlay.put(player, option == null ? new CardPlayOption(withoutManaCost, ignoreColor) : option.add(withoutManaCost, ignoreColor)); + } + public final void removeMayPlay(final Player player) { + this.mayPlay.remove(player); + } + public final CardCollectionView getEquippedBy(boolean allowModify) { return CardCollection.getView(equippedBy, allowModify); } @@ -6246,13 +6269,18 @@ public class Card extends GameEntity implements Comparable, IIdentifiable return null; } - public List getAllPossibleAbilities(Player player, boolean removeUnplayable) { + public List getAllPossibleAbilities(final Player player, final boolean removeUnplayable) { // this can only be called by the Human final List abilities = new ArrayList(); for (SpellAbility sa : getSpellAbilities()) { //add alternative costs as additional spell abilities abilities.add(sa); - abilities.addAll(GameActionUtil.getAlternativeCosts(sa)); + abilities.addAll(GameActionUtil.getAlternativeCosts(sa, player)); + } + if (isFaceDown() && isInZone(ZoneType.Exile) && mayPlay(player) != null) { + for (final SpellAbility sa : getState(CardStateName.Original).getSpellAbilities()) { + abilities.add(sa); + } } for (int i = abilities.size() - 1; i >= 0; i--) { @@ -6266,7 +6294,7 @@ public class Card extends GameEntity implements Comparable, IIdentifiable } } - if (isLand() && player.canPlayLand(this)) { + if (getState(CardStateName.Original).getType().isLand() && player.canPlayLand(this)) { Ability.PLAY_LAND_SURROGATE.setHostCard(this); abilities.add(Ability.PLAY_LAND_SURROGATE); } diff --git a/forge-game/src/main/java/forge/game/card/CardPlayOption.java b/forge-game/src/main/java/forge/game/card/CardPlayOption.java new file mode 100644 index 00000000000..0dd3b6fda7e --- /dev/null +++ b/forge-game/src/main/java/forge/game/card/CardPlayOption.java @@ -0,0 +1,36 @@ +package forge.game.card; + +import org.apache.commons.lang3.StringUtils; + +public final class CardPlayOption { + private final boolean withoutManaCost, ignoreManaCostColor; + + public CardPlayOption(final boolean withoutManaCost, final boolean ignoreManaCostColor) { + this.withoutManaCost = withoutManaCost; + this.ignoreManaCostColor = ignoreManaCostColor; + } + + public CardPlayOption add(final boolean withoutManaCost, final boolean ignoreManaCostColor) { + return new CardPlayOption(isWithoutManaCost() || withoutManaCost, isIgnoreManaCostColor() || ignoreManaCostColor); + } + + public boolean isWithoutManaCost() { + return withoutManaCost; + } + + public boolean isIgnoreManaCostColor() { + return ignoreManaCostColor; + } + + @Override + public String toString() { + if (isWithoutManaCost()) { + return " (without paying its mana cost)"; + } + if (isIgnoreManaCostColor()) { + return " (may spend mana as though it were mana of any color to cast it)"; + } + return StringUtils.EMPTY; + } + +} diff --git a/forge-game/src/main/java/forge/game/card/CardView.java b/forge-game/src/main/java/forge/game/card/CardView.java index 459ee6725ce..09ec7ec6290 100644 --- a/forge-game/src/main/java/forge/game/card/CardView.java +++ b/forge-game/src/main/java/forge/game/card/CardView.java @@ -1,9 +1,12 @@ package forge.game.card; +import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Map; +import com.google.common.collect.Iterables; + import forge.ImageKeys; import forge.card.CardStateName; import forge.card.CardEdition; @@ -102,6 +105,9 @@ public class CardView extends GameEntityView { void updateZone(Card c) { set(TrackableProperty.Zone, c.getZone() == null ? null : c.getZone().getZoneType()); } + public boolean isInZone(final Iterable zones) { + return Iterables.contains(zones, getZone()); + } public boolean isCloned() { return get(TrackableProperty.Cloned); @@ -397,11 +403,17 @@ public class CardView extends GameEntityView { } //if viewer is controlled by another player, also check if face can be shown to that player - PlayerView mindSlaveMaster = viewer.getMindSlaveMaster(); + final PlayerView mindSlaveMaster = viewer.getMindSlaveMaster(); if (mindSlaveMaster != null && canFaceDownBeShownTo(mindSlaveMaster)) { return true; } - return !getController().isOpponentOf(viewer) || getCurrentState().getOpponentMayLook(); + if (isInZone(EnumSet.of(ZoneType.Battlefield, ZoneType.Stack, ZoneType.Sideboard)) && getController().equals(viewer)) { + return true; + } + if (getController().isOpponentOf(viewer) && getCurrentState().getOpponentMayLook()) { + return true; + } + return false; } public CardView getEquipping() { diff --git a/forge-game/src/main/java/forge/game/player/Player.java b/forge-game/src/main/java/forge/game/player/Player.java index a9360e849d9..1d47aef7790 100644 --- a/forge-game/src/main/java/forge/game/player/Player.java +++ b/forge-game/src/main/java/forge/game/player/Player.java @@ -1386,6 +1386,9 @@ public class Player extends GameEntity implements Comparable { // Dakkon Blackblade Avatar will use a similar effect if (canPlayLand(land, ignoreZoneAndTiming)) { land.setController(this, 0); + if (land.isFaceDown()) { + land.turnFaceUp(); + } game.getAction().moveTo(getZone(ZoneType.Battlefield), land); // play a sound @@ -1407,7 +1410,7 @@ public class Player extends GameEntity implements Comparable { public final boolean canPlayLand(final Card land) { return canPlayLand(land, false); } - public final boolean canPlayLand(Card land, final boolean ignoreZoneAndTiming) { + public final boolean canPlayLand(final Card land, final boolean ignoreZoneAndTiming) { if (!ignoreZoneAndTiming && !canCastSorcery()) { return false; } @@ -1423,14 +1426,13 @@ public class Player extends GameEntity implements Comparable { } if (land != null && !ignoreZoneAndTiming) { - if (land.getOwner() != this && !land.hasKeyword("May be played by your opponent")) - return false; - - if (land.getOwner() == this && land.hasKeyword("May be played by your opponent") && !land.hasKeyword("May be played")) + final boolean mayPlay = land.mayPlay(this) != null; + if (land.getOwner() != this && !mayPlay) { return false; + } final Zone zone = game.getZoneOf(land); - if (zone != null && (zone.is(ZoneType.Battlefield) || (!zone.is(ZoneType.Hand) && !land.hasStartOfKeyword("May be played")))) { + if (zone != null && (zone.is(ZoneType.Battlefield) || (!zone.is(ZoneType.Hand) && !(mayPlay || land.hasStartOfKeyword("May be played"))))) { return false; } } diff --git a/forge-game/src/main/java/forge/game/spellability/Spell.java b/forge-game/src/main/java/forge/game/spellability/Spell.java index a4d355a7d3d..51af433abb9 100644 --- a/forge-game/src/main/java/forge/game/spellability/Spell.java +++ b/forge-game/src/main/java/forge/game/spellability/Spell.java @@ -17,6 +17,7 @@ */ package forge.game.spellability; +import forge.card.CardStateName; import forge.game.Game; import forge.game.card.Card; import forge.game.card.CardCollection; @@ -91,7 +92,7 @@ public abstract class Spell extends SpellAbility implements java.io.Serializable } // for uncastables like lotus bloom, check if manaCost is blank (except for morph spells) - if (!isCastFaceDown() && isBasicSpell() && card.getManaCost().isNoCost()) { + if (!isCastFaceDown() && isBasicSpell() && card.getState(card.isFaceDown() ? CardStateName.Original : card.getCurrentStateName()).getManaCost().isNoCost()) { return false; } diff --git a/forge-game/src/main/java/forge/game/spellability/SpellAbilityRestriction.java b/forge-game/src/main/java/forge/game/spellability/SpellAbilityRestriction.java index b9ce60ed6b9..8e6aab3fe23 100644 --- a/forge-game/src/main/java/forge/game/spellability/SpellAbilityRestriction.java +++ b/forge-game/src/main/java/forge/game/spellability/SpellAbilityRestriction.java @@ -200,9 +200,8 @@ public class SpellAbilityRestriction extends SpellAbilityVariables { return true; } - Player activator = sa.getActivatingPlayer(); - - Zone cardZone = activator.getGame().getZoneOf(c); + final Player activator = sa.getActivatingPlayer(); + final Zone cardZone = activator.getGame().getZoneOf(c); if (cardZone == null || !cardZone.is(this.getZone())) { // If Card is not in the default activating zone, do some additional checks // Not a Spell, or on Battlefield, return false @@ -211,12 +210,12 @@ public class SpellAbilityRestriction extends SpellAbilityVariables { return false; } if (cardZone.is(ZoneType.Stack)) { - return false; + return false; } - if (c.hasKeyword("May be played") && activator.equals(c.getController())) { + if (c.mayPlay(activator) != null) { return true; } - if (c.hasKeyword("May be played by your opponent") && !activator.equals(c.getController())) { + if (c.hasKeyword("May be played") && activator.equals(c.getController())) { return true; } return false; @@ -296,7 +295,7 @@ public class SpellAbilityRestriction extends SpellAbilityVariables { return true; } - if (sa.isSpell() && activator.isOpponentOf(c.getController()) && c.hasKeyword("May be played by your opponent")) { + if (sa.isSpell() && c.mayPlay(activator) != null) { return true; } diff --git a/forge-game/src/main/java/forge/game/staticability/StaticAbility.java b/forge-game/src/main/java/forge/game/staticability/StaticAbility.java index d3ab554f28f..bc307654744 100644 --- a/forge-game/src/main/java/forge/game/staticability/StaticAbility.java +++ b/forge-game/src/main/java/forge/game/staticability/StaticAbility.java @@ -257,9 +257,8 @@ public class StaticAbility extends CardTraitBase { return in; } - // apply the ability if it has the right mode /** - * Apply ability. + * Apply ability if it has the right mode. * * @param mode * the mode diff --git a/forge-game/src/main/java/forge/game/staticability/StaticAbilityContinuous.java b/forge-game/src/main/java/forge/game/staticability/StaticAbilityContinuous.java index 995f439935e..aa572a01e73 100644 --- a/forge-game/src/main/java/forge/game/staticability/StaticAbilityContinuous.java +++ b/forge-game/src/main/java/forge/game/staticability/StaticAbilityContinuous.java @@ -113,6 +113,7 @@ public class StaticAbilityContinuous { boolean removeSubTypes = false; boolean removeCreatureTypes = false; boolean controllerMayLookAt = false; + boolean controllerMayPlay = false, mayPlayWithoutManaCost = false, mayPlayIgnoreColor = false; //Global rules changes if (params.containsKey("GlobalRule")) { @@ -328,6 +329,14 @@ public class StaticAbilityContinuous { if (params.containsKey("MayLookAt")) { controllerMayLookAt = true; } + if (params.containsKey("MayPlay")) { + controllerMayPlay = true; + if (params.containsKey("MayPlayWithoutManaCost")) { + mayPlayWithoutManaCost = true; + } else if (params.containsKey("MayPlayIgnoreColor")) { + mayPlayIgnoreColor = true; + } + } if (params.containsKey("IgnoreEffectCost")) { String cost = params.get("IgnoreEffectCost"); @@ -558,8 +567,11 @@ public class StaticAbilityContinuous { if (controllerMayLookAt) { affectedCard.setMayLookAt(controller, true); } + if (controllerMayPlay) { + affectedCard.setMayPlay(controller, mayPlayWithoutManaCost, mayPlayIgnoreColor); + } } - + return affectedCards; } diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerHandler.java b/forge-game/src/main/java/forge/game/trigger/TriggerHandler.java index 7fe7daf928e..c4cc7727ae8 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerHandler.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerHandler.java @@ -39,14 +39,17 @@ import forge.game.zone.ZoneType; import java.util.*; import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ListMultimap; +import com.google.common.collect.Lists; +import com.google.common.collect.Multimaps; public class TriggerHandler { - private final ArrayList suppressedModes = new ArrayList(); - private final ArrayList activeTriggers = new ArrayList(); + private final List suppressedModes = Collections.synchronizedList(new ArrayList()); + private final List activeTriggers = Collections.synchronizedList(new ArrayList()); - private final ArrayList delayedTriggers = new ArrayList(); - private final ArrayListMultimap playerDefinedDelayedTriggers = ArrayListMultimap.create(); - private final List waitingTriggers = new ArrayList(); + private final List delayedTriggers = Collections.synchronizedList(new ArrayList()); + private final ListMultimap playerDefinedDelayedTriggers = Multimaps.synchronizedListMultimap(ArrayListMultimap.create()); + private final List waitingTriggers = Collections.synchronizedList(new ArrayList()); private final Game game; public TriggerHandler(Game gameState) { @@ -268,7 +271,7 @@ public class TriggerHandler { boolean checkStatics = false; // Static triggers - for (final Trigger t : activeTriggers) { + for (final Trigger t : Lists.newArrayList(activeTriggers)) { if (t.isStatic() && canRunTrigger(t, mode, runParams)) { runSingleTrigger(t, runParams); checkStatics = true; diff --git a/forge-game/src/main/java/forge/game/zone/MagicStack.java b/forge-game/src/main/java/forge/game/zone/MagicStack.java index cbd01745a72..f9a440e737d 100644 --- a/forge-game/src/main/java/forge/game/zone/MagicStack.java +++ b/forge-game/src/main/java/forge/game/zone/MagicStack.java @@ -216,9 +216,11 @@ public class MagicStack /* extends MyObservable */ implements Iterable | SpellDescription$ Regenerate CARDNAME. SVar:RemAIDeck:True diff --git a/forge-gui/res/cardsfolder/h/havengul_lich.txt b/forge-gui/res/cardsfolder/h/havengul_lich.txt index 1304c455436..499eecdbdfb 100644 --- a/forge-gui/res/cardsfolder/h/havengul_lich.txt +++ b/forge-gui/res/cardsfolder/h/havengul_lich.txt @@ -5,7 +5,8 @@ PT:4/4 A:AB$ Pump | Cost$ 1 | ValidTgts$ Creature | TgtZone$ Graveyard | TgtPrompt$ Select target creature card | PumpZone$ Graveyard | SubAbility$ PumpYour | RememberObjects$ Targeted | SpellDescription$ You may cast target creature card in a graveyard this turn. When you cast that card this turn, CARDNAME gains all activated abilities of that card until end of turn. SVar:PumpYour:DB$ Pump | Defined$ Remembered | ConditionCheckSVar$ SelectKW | ConditionSVarCompare$ EQ1 | KW$ HIDDEN May be played | PumpZone$ Graveyard | SubAbility$ PumpTheir | References$ SelectKW SVar:PumpTheir:DB$ Pump | Defined$ Remembered | ConditionCheckSVar$ SelectKW | ConditionSVarCompare$ EQ0 | KW$ HIDDEN May be played by your opponent | PumpZone$ Graveyard | SubAbility$ FXCast | References$ SelectKW -SVar:FXCast:DB$ Effect | Name$ Havengul Lich Delayed Trigger | Triggers$ DTCast | SVars$ StealAbs,STSteal,CleanupDT | RememberObjects$ Targeted +SVar:FXCast:DB$ Effect | Name$ Havengul Lich Delayed Trigger | StaticAbilities$ STPlay | Triggers$ DTCast | SVars$ StealAbs,STSteal,CleanupDT | RememberObjects$ Targeted +SVar:STPlay:Mode$ Continuous | MayPlay$ True | EffectZone$ Command | Affected$ Card.IsRemembered | AffectedZone$ Graveyard | Description$ Until end of turn, you may cast a creature card in a graveyard. SVar:DTCast:Mode$ SpellCast | ValidCard$ Card.IsRemembered | Execute$ StealAbs | TriggerDescription$ When you cast that card this turn, Havengul Lich gains all activated abilities of that card until end of turn. SVar:StealAbs:DB$ Effect | Name$ Havengul Lich effect | RememberObjects$ TriggeredCard | StaticAbilities$ STSteal | SubAbility$ CleanupDT SVar:STSteal:Mode$ Continuous | Affected$ EffectSource | EffectZone$ Command | GainsAbilitiesOf$ Creature.IsRemembered | GainsAbilitiesOfZones$ Library,Hand,Stack,Battlefield,Graveyard,Exile,Command diff --git a/forge-gui/res/cardsfolder/k/kheru_spellsnatcher.txt b/forge-gui/res/cardsfolder/k/kheru_spellsnatcher.txt index 6898e51759e..a80c710bc30 100644 --- a/forge-gui/res/cardsfolder/k/kheru_spellsnatcher.txt +++ b/forge-gui/res/cardsfolder/k/kheru_spellsnatcher.txt @@ -5,9 +5,8 @@ PT:3/3 K:Morph:4 U U T:Mode$ TurnFaceUp | ValidCard$ Card.Self | Execute$ TrigCounter | TriggerZones$ Battlefield | TriggerDescription$ When CARDNAME is turned face up, counter target spell. If that spell is countered this way, exile it instead of putting it into its owner's graveyard. You may cast that card without paying its mana cost as long as it remains exiled. SVar:TrigCounter:AB$ Counter | Cost$ 0 | TargetType$ Spell | TgtPrompt$ Select target spell | ValidTgts$ Card | RememberCountered$ True | ForgetOtherTargets$ True | Destination$ Exile | SubAbility$ DBEffect -SVar:DBEffect:DB$ Effect | Name$ Kheru Spellsnatcher Effect | RememberObjects$ Remembered | StaticAbilities$ PlayOpp,PlayYou | Duration$ Permanent | Triggers$ TrigCleanup | SVars$ DBCleanup | References$ PlayOpp,PlayYou,TrigCleanup,DBCleanup -SVar:PlayOpp:Mode$ Continuous | EffectZone$ Command | Affected$ Card.IsRemembered+OppOwn | AffectedZone$ Exile | AddHiddenKeyword$ May be played by your opponent without paying its mana cost | Description$ You may play cards exiled with Kheru Spellsnatcher. -SVar:PlayYou:Mode$ Continuous | EffectZone$ Command | Affected$ Card.IsRemembered+YouOwn | AffectedZone$ Exile | AddHiddenKeyword$ May be played without paying its mana cost +SVar:DBEffect:DB$ Effect | RememberObjects$ Remembered | StaticAbilities$ STPlay | Duration$ Permanent | Triggers$ TrigCleanup | SVars$ DBCleanup | References$ PlayOpp,PlayYou,TrigCleanup,DBCleanup +SVar:STPlay:Mode$ Continuous | MayPlay$ True | MayPlayWithoutManaCost$ True | EffectZone$ Command | Affected$ Card.IsRemembered | AffectedZone$ Exile | Description$ You may cast cards without paying their mana cost as long as they remain exiled. SVar:TrigCleanup:Mode$ ChangesZone | ValidCard$ Card.IsRemembered | Origin$ Exile | Destination$ Any | TriggerZones$ Command | Execute$ DBCleanup | Static$ True SVar:DBCleanup:DB$ ChangeZone | Defined$ Self | Origin$ Command | Destination$ Exile SVar:RemAIDeck:True diff --git a/forge-gui/res/cardsfolder/k/knacksaw_clique.txt b/forge-gui/res/cardsfolder/k/knacksaw_clique.txt index 677aa7b83c8..4fbb8e84f55 100644 --- a/forge-gui/res/cardsfolder/k/knacksaw_clique.txt +++ b/forge-gui/res/cardsfolder/k/knacksaw_clique.txt @@ -3,9 +3,12 @@ ManaCost:3 U Types:Creature Faerie Rogue PT:1/4 K:Flying -A:AB$ Mill | Cost$ 1 U Q | ValidTgts$ Opponent | TgtPrompt$ Select target opponent | NumCards$ 1 | Destination$ Exile | RememberMilled$ True | SubAbility$ DBPump | SpellDescription$ Target opponent exiles the top card of his or her library. Until end of turn, you may play that card. -SVar:DBPump:DB$ Pump | Defined$ Remembered | KW$ May be played by your opponent | PumpZone$ Exile | SubAbility$ DBCleanup -SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True +A:AB$ Mill | Cost$ 1 U Q | ValidTgts$ Opponent | TgtPrompt$ Select target opponent | NumCards$ 1 | Destination$ Exile | RememberMilled$ True | SubAbility$ DBEffect | SpellDescription$ Target opponent exiles the top card of his or her library. Until end of turn, you may play that card. +SVar:DBEffect:DB$ Effect | Duration$ EndOfTurn | RememberObjects$ Remembered | StaticAbilities$ STPlay | Triggers$ TrigCleanup | SVars$ DBExileSelf | SubAbility$ DBCleanup +SVar:STPlay:Mode$ Continuous | MayPlay$ True | EffectZone$ Command | Affected$ Card.IsRemembered | AffectedZone$ Exile | Description$ You may play a card this turn. +SVar:TrigCleanup:Mode$ ChangesZone | ValidCard$ Card.IsRemembered | Origin$ Exile | Destination$ Any | TriggerZones$ Command | Execute$ DBExileSelf | Static$ True +SVar:DBExileSelf:DB$ ChangeZone | Defined$ Self | Origin$ Command | Destination$ Exile +SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ Truek SVar:RemAIDeck:True SVar:Picture:http://www.wizards.com/global/images/magic/general/knacksaw_clique.jpg Oracle:Flying\n{1}{U}, {Q}: Target opponent exiles the top card of his or her library. Until end of turn, you may play that card. ({Q} is the untap symbol.) diff --git a/forge-gui/res/cardsfolder/m/muse_vessel.txt b/forge-gui/res/cardsfolder/m/muse_vessel.txt index 9514eb1958b..be0ee7a46e9 100644 --- a/forge-gui/res/cardsfolder/m/muse_vessel.txt +++ b/forge-gui/res/cardsfolder/m/muse_vessel.txt @@ -2,9 +2,11 @@ Name:Muse Vessel ManaCost:4 Types:Artifact A:AB$ ChangeZone | Cost$ 3 T | ValidTgts$ Player | TgtPrompt$ Select target player | SorcerySpeed$ True | Origin$ Hand | Destination$ Exile | ChangeType$ Card | ChangeNum$ 1 | RememberChanged$ True | Chooser$ Targeted | Hidden$ True | IsCurse$ True | Mandatory$ True | SpellDescription$ Target player exiles a card from his or her hand. Activate this ability only any time you could cast a sorcery. -A:AB$ ChooseCard | Cost$ 1 | ChoiceZone$ Exile | Choices$ Card.IsRemembered | Amount$ 1 | ChoiceTitle$ Choose a card exiled with Muse Vessel | SubAbility$ DBPlayYouOwn | AILogic$ Never | SpellDescription$ Choose a card exiled with CARDNAME. You may play that card this turn. -SVar:DBPlayYouOwn:DB$ Pump | Defined$ ChosenCard | KW$ HIDDEN May be played | PumpZone$ Exile | ConditionDefined$ ChosenCard | ConditionPresent$ Card.YouOwn | ConditionCompare$ EQ1 | SubAbility$ DBPlayOppOwn -SVar:DBPlayOppOwn:DB$ Pump | Defined$ ChosenCard | KW$ HIDDEN May be played by your opponent | PumpZone$ Exile | ConditionDefined$ ChosenCard | ConditionPresent$ Card.OppOwn | ConditionCompare$ EQ1 +A:AB$ ChooseCard | Cost$ 1 | ChoiceZone$ Exile | Choices$ Card.IsRemembered | Amount$ 1 | ChoiceTitle$ Choose a card exiled with Muse Vessel | SubAbility$ DBEffect | AILogic$ Never | SpellDescription$ Choose a card exiled with CARDNAME. You may play that card this turn. +SVar:DBEffect:DB$ Effect | Duration$ EndOfTurn | StaticAbilities$ STPlay | Triggers$ TrigCleanup | SVars$ DBExileSelf | RememberObjects$ ChosenCard | SubAbility$ DBCleanup +SVar:STPlay:Mode$ Continuous | MayPlay$ True | EffectZone$ Command | Affected$ Card.IsRemembered | AffectedZone$ Exile | Description$ You may play a card this turn. +SVar:TrigCleanup:Mode$ ChangesZone | ValidCard$ Card.IsRemembered | Origin$ Exile | Destination$ Any | TriggerZones$ Command | Execute$ DBExileSelf | Static$ True +SVar:DBExileSelf:DB$ ChangeZone | Defined$ Self | Origin$ Command | Destination$ Exile T:Mode$ ChangesZone | Origin$ Exile | Destination$ Any | Static$ True | ValidCard$ Card.IsRemembered | Execute$ DBForget SVar:DBForget:DB$ Pump | Defined$ TriggeredCard | ForgetObjects$ TriggeredCard T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Any | Static$ True | ValidCard$ Card.Self | Execute$ DBCleanup diff --git a/forge-gui/res/cardsfolder/n/nightveil_specter.txt b/forge-gui/res/cardsfolder/n/nightveil_specter.txt index fc837f65ec3..989b9f2984f 100644 --- a/forge-gui/res/cardsfolder/n/nightveil_specter.txt +++ b/forge-gui/res/cardsfolder/n/nightveil_specter.txt @@ -5,8 +5,7 @@ PT:2/3 K:Flying T:Mode$ DamageDone | ValidSource$ Card.Self | ValidTarget$ Player | CombatDamage$ True | Execute$ TrigMill | TriggerZones$ Battlefield | TriggerDescription$ Whenever CARDNAME deals combat damage to a player, that player exiles the top card of his or her library. SVar:TrigMill:AB$ Mill | Cost$ 0 | Defined$ TriggeredTarget | NumCards$ 1 | Destination$ Exile | RememberMilled$ True -S:Mode$ Continuous | Affected$ Card.IsRemembered+OppOwn | AffectedZone$ Exile | AddHiddenKeyword$ May be played by your opponent | Description$ You may play cards exiled with CARDNAME. -S:Mode$ Continuous | Affected$ Card.IsRemembered+YouOwn | AffectedZone$ Exile | AddHiddenKeyword$ May be played +S:Mode$ Continuous | MayPlay$ True | Affected$ Card.IsRemembered+OppOwn | AffectedZone$ Exile | Description$ You may play cards exiled with CARDNAME. T:Mode$ ChangesZone | Origin$ Exile | Destination$ Any | Static$ True | ValidCard$ Card.IsRemembered | Execute$ DBForget SVar:DBForget:DB$ Pump | Defined$ TriggeredCard | ForgetObjects$ TriggeredCard T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Any | Static$ True | ValidCard$ Card.Self | Execute$ DBCleanup diff --git a/forge-gui/res/cardsfolder/o/ornate_kanzashi.txt b/forge-gui/res/cardsfolder/o/ornate_kanzashi.txt index 0a1fd26b019..f306a6a0300 100644 --- a/forge-gui/res/cardsfolder/o/ornate_kanzashi.txt +++ b/forge-gui/res/cardsfolder/o/ornate_kanzashi.txt @@ -1,8 +1,11 @@ Name:Ornate Kanzashi ManaCost:5 Types:Artifact -A:AB$ Mill | Cost$ 2 T | ValidTgts$ Opponent | TgtPrompt$ Select target opponent | NumCards$ 1 | Destination$ Exile | RememberMilled$ True | SubAbility$ DBPump | SpellDescription$ Target opponent exiles the top card of his or her library. You may play that card this turn. -SVar:DBPump:DB$ Pump | Defined$ Remembered | KW$ May be played by your opponent | PumpZone$ Exile | SubAbility$ DBCleanup +A:AB$ Mill | Cost$ 2 T | ValidTgts$ Opponent | TgtPrompt$ Select target opponent | NumCards$ 1 | Destination$ Exile | RememberMilled$ True | SubAbility$ DBEffect | SpellDescription$ Target opponent exiles the top card of his or her library. You may play that card this turn. +SVar:DBEffect:DB$ Effect | Duration$ EndOfTurn | RememberObjects$ Remembered | StaticAbilities$ STPlay | Triggers$ TrigCleanup | SVars$ DBExileSelf | SubAbility$ DBCleanup +SVar:STPlay:Mode$ Continuous | MayPlay$ True | EffectZone$ Command | Affected$ Card.IsRemembered | AffectedZone$ Exile | Description$ You may play a card this turn. +SVar:TrigCleanup:Mode$ ChangesZone | ValidCard$ Card.IsRemembered | Origin$ Exile | Destination$ Any | TriggerZones$ Command | Execute$ DBExileSelf | Static$ True +SVar:DBExileSelf:DB$ ChangeZone | Defined$ Self | Origin$ Command | Destination$ Exile SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True SVar:RemAIDeck:True SVar:Picture:http://www.wizards.com/global/images/magic/general/ornate_kanzashi.jpg diff --git a/forge-gui/res/cardsfolder/p/planeswalkers_mischief.txt b/forge-gui/res/cardsfolder/p/planeswalkers_mischief.txt index bc2d1227d18..29bb01f300e 100644 --- a/forge-gui/res/cardsfolder/p/planeswalkers_mischief.txt +++ b/forge-gui/res/cardsfolder/p/planeswalkers_mischief.txt @@ -6,9 +6,9 @@ SVar:DBChangeZone:DB$ ChangeZoneAll | ChangeType$ Instant.IsRemembered,Sorcery.I SVar:DBForgetOther:DB$ Cleanup | ClearRemembered$ True | ConditionCheckSVar$ X | ConditionSVarCompare$ EQ0 | SubAbility$ DBEffect SVar:DBEffect:DB$ Effect | StaticAbilities$ MischiefPlay | Triggers$ TrigEOT,TrigChangesZone | SVars$ MischiefCleanup,MischiefReturn | RememberObjects$ Remembered | Permanent$ True | ConditionCheckSVar$ X | ConditionSVarCompare$ GE1 | SubAbility$ DBCleanup SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True -SVar:MischiefPlay:Mode$ Continuous | EffectZone$ Command | Affected$ Card.IsRemembered+OppOwn | AffectedZone$ Exile | AddHiddenKeyword$ May be played by your opponent without paying its mana cost | Description$ You may play that exiled card. +SVar:MischiefPlay:Mode$ Continuous | MayPlay$ True | MayPlayWithoutManaCost$ True | EffectZone$ Command | Affected$ Card.IsRemembered+OppOwn | AffectedZone$ Exile | Description$ You may cast a card without paying its mana cost as long as it remains exiled. SVar:TrigEOT:Mode$ Phase | Phase$ End of Turn | ValidPlayer$ You | TriggerZones$ Command | Execute$ MischiefReturn | TriggerDescription$ At the beginning of the next end step, if you haven't cast it, return it to its owner's hand. -SVar:MischiefReturn:DB$ ChangeZone | Defined$ Remembered | Origin$ Exile | Destination$ Hand | +SVar:MischiefReturn:DB$ ChangeZone | Defined$ Remembered | Origin$ Exile | Destination$ Hand SVar:TrigChangesZone:Mode$ ChangesZone | Origin$ Exile | Destination$ Any | Static$ True | ValidCard$ Card.IsRemembered | Execute$ MischiefCleanup SVar:MischiefCleanup:DB$ ChangeZone | Defined$ Self | Origin$ Command | Destination$ Exile SVar:X:Count$ValidExile Instant.IsRemembered,Sorcery.IsRemembered diff --git a/forge-gui/res/cardsfolder/p/praetors_grasp.txt b/forge-gui/res/cardsfolder/p/praetors_grasp.txt index e92bbc57403..d096f616cc7 100644 --- a/forge-gui/res/cardsfolder/p/praetors_grasp.txt +++ b/forge-gui/res/cardsfolder/p/praetors_grasp.txt @@ -1,8 +1,12 @@ Name:Praetor's Grasp ManaCost:1 B B Types:Sorcery -A:SP$ ChangeZone | Cost$ 1 B B | Origin$ Library | Destination$ Exile | ValidTgts$ Opponent | ChangeType$ Card | ChangeNum$ 1 | IsCurse$ True | RememberChanged$ True | SubAbility$ DBPump | StackDescription$ SpellDescription | SpellDescription$ Search target opponent's library for a card and exile it face down. Then that player shuffles his or her library. You may look at and play that card for as long as it remains exiled. -SVar:DBPump:DB$ Pump | Defined$ Remembered | KW$ Your opponent may look at this card. & May be played by your opponent | PumpZone$ Exile | Permanent$ True +A:SP$ ChangeZone | Cost$ 1 B B | Origin$ Library | Destination$ Exile | ExileFaceDown$ True | ValidTgts$ Opponent | ChangeType$ Card | ChangeNum$ 1 | IsCurse$ True | RememberChanged$ True | SubAbility$ DBEffect | StackDescription$ SpellDescription | SpellDescription$ Search target opponent's library for a card and exile it face down. Then that player shuffles his or her library. You may look at and play that card for as long as it remains exiled. +SVar:DBEffect:DB$ Effect | MayLookAt$ True | MayPlay$ True | RememberObjects$ Remembered | StaticAbilities$ STPlay | Duration$ Permanent | Triggers$ TrigCleanup | SVars$ DBExileSelf | SubAbility$ DBCleanup +SVar:STPlay:Mode$ Continuous | MayLookAt$ True | MayPlay$ True | EffectZone$ Command | Affected$ Card.IsRemembered | AffectedZone$ Exile | Description$ You may look at and play a card as long as it remains exiled. +SVar:TrigCleanup:Mode$ ChangesZone | ValidCard$ Card.IsRemembered | Origin$ Exile | Destination$ Any | TriggerZones$ Command | Execute$ DBExileSelf | Static$ True +SVar:DBExileSelf:DB$ ChangeZone | Defined$ Self | Origin$ Command | Destination$ Exile +SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True SVar:RemAIDeck:True SVar:Picture:http://www.wizards.com/global/images/magic/general/praetors_grasp.jpg Oracle:Search target opponent's library for a card and exile it face down. Then that player shuffles his or her library. You may look at and play that card for as long as it remains exiled. diff --git a/forge-gui/res/cardsfolder/s/shared_fate.txt b/forge-gui/res/cardsfolder/s/shared_fate.txt new file mode 100644 index 00000000000..6d1f8c04ec9 --- /dev/null +++ b/forge-gui/res/cardsfolder/s/shared_fate.txt @@ -0,0 +1,18 @@ +Name:Shared Fate +ManaCost:4 U +Types:Enchantment +Text:If a player would draw a card, that player exiles the top card of an opponent's library face down instead. Each player may look at and play cards he or she exiled with CARDNAME. +T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigEffects | Static$ True +#Create an effect for each player. The effect contains both Shared Fate's abilities. +SVar:TrigEffects:DB$ RepeatEach | RepeatPlayers$ Each | RepeatSubAbility$ DBEffect +SVar:DBEffect:DB$ Effect | EffectOwner$ Remembered | StaticAbilities$ STPlay | Triggers$ TrigCleanup | ReplacementEffects$ RDraw | SVars$ DBChooseOpp,DBExile,DBCleanup | Duration$ UntilHostLeavesPlay +SVar:RDraw:Event$ Draw | ActiveZones$ Command | ValidPlayer$ You | ReplaceWith$ DBChooseOpp | Description$ If you would draw a card, exile the top card of an opponent's library face down instead. +SVar:DBChooseOpp:DB$ ChoosePlayer | ChoiceTitle$ Choose an opponent whose top library card to exile | Choices$ Player.Opponent | AILogic$ Curse | SubAbility$ DBExile +SVar:DBExile:DB$ Mill | NumCards$ 1 | Destination$ Exile | ExileFaceDown$ True | Defined$ Player.Chosen | RememberMilled$ True +SVar:STPlay:Mode$ Continuous | MayLookAt$ True | MayPlay$ True | EffectZone$ Command | Affected$ Card.IsRemembered | AffectedZone$ Exile | Description$ You may look at and play cards exiled with Shared Fate. +SVar:TrigCleanup:Mode$ ChangesZone | ValidCard$ Card.IsRemembered | Origin$ Exile | Destination$ Any | TriggerZones$ Command | Execute$ DBCleanup | Static$ True +SVar:DBCleanup:DB$ Cleanup | ForgetDefined$ TriggeredCard +SVar:RemAIDeck:True +SVar:RemRandomDeck:True +SVar:Picture:http://www.wizards.com/global/images/magic/general/shared_fate.jpg +Oracle:If a player would draw a card, that player exiles the top card of an opponent's library face down instead.\nEach player may look at and play cards he or she exiled with Shared Fate. diff --git a/forge-gui/src/main/java/forge/player/HumanPlaySpellAbility.java b/forge-gui/src/main/java/forge/player/HumanPlaySpellAbility.java index fc5885f809f..efd9c4e56f0 100644 --- a/forge-gui/src/main/java/forge/player/HumanPlaySpellAbility.java +++ b/forge-gui/src/main/java/forge/player/HumanPlaySpellAbility.java @@ -17,14 +17,20 @@ */ package forge.player; +import java.util.Collections; + +import org.apache.commons.lang3.StringUtils; + import com.google.common.collect.Iterables; +import forge.card.CardStateName; import forge.card.CardType; import forge.card.MagicColor; import forge.game.Game; import forge.game.GameObject; import forge.game.ability.AbilityUtils; import forge.game.card.Card; +import forge.game.card.CardPlayOption; import forge.game.cost.CostPartMana; import forge.game.cost.CostPayment; import forge.game.mana.ManaPool; @@ -37,10 +43,6 @@ import forge.game.spellability.TargetRestrictions; import forge.game.zone.Zone; import forge.util.FCollection; -import org.apache.commons.lang3.StringUtils; - -import java.util.ArrayList; - /** *

* SpellAbility_Requirements class. @@ -66,17 +68,26 @@ public class HumanPlaySpellAbility { // used to rollback Zone fromZone = null; + CardStateName fromState = null; int zonePosition = 0; final ManaPool manapool = human.getManaPool(); final Card c = ability.getHostCard(); - boolean manaConversion = (ability.isSpell() && c.hasKeyword("May spend mana as though it were mana of any color to cast CARDNAME")); + final CardPlayOption option = c.mayPlay(human); + + boolean manaConversion = (ability.isSpell() && (c.hasKeyword("May spend mana as though it were mana of any color to cast CARDNAME") + || (option != null && option.isIgnoreManaCostColor()))); boolean playerManaConversion = human.hasManaConversion() && human.getController().confirmAction(ability, null, "Do you want to spend mana as though it were mana of any color to pay the cost?"); if (ability instanceof Spell && !c.isCopiedSpell()) { fromZone = game.getZoneOf(c); + fromState = c.getCurrentStateName(); if (fromZone != null) { - zonePosition = fromZone.getCards().indexOf(c); + zonePosition = fromZone.getCards().indexOf(c); + } + // Turn face-down card face up (except case of morph spell) + if (ability instanceof Spell && !((Spell) ability).isCastFaceDown() && fromState == CardStateName.FaceDown) { + c.turnFaceUp(); } ability.setHostCard(game.getAction().moveToStack(c)); } @@ -100,7 +111,7 @@ public class HumanPlaySpellAbility { if (!prerequisitesMet) { if (!ability.isTrigger()) { - rollbackAbility(fromZone, zonePosition); + rollbackAbility(fromZone, fromState, zonePosition); if (ability.getHostCard().isMadness()) { // if a player failed to play madness cost, move the card to graveyard game.getAction().moveToGraveyard(c); @@ -125,8 +136,7 @@ public class HumanPlaySpellAbility { if (skipStack) { AbilityUtils.resolve(ability); - } - else { + } else { enusureAbilityHasDescription(ability); game.getStack().addAndUnfreeze(ability); } @@ -182,13 +192,14 @@ public class HumanPlaySpellAbility { } } - private void rollbackAbility(Zone fromZone, int zonePosition) { + private void rollbackAbility(final Zone fromZone, final CardStateName fromState, final int zonePosition) { // cancel ability during target choosing final Game game = ability.getActivatingPlayer().getGame(); if (fromZone != null) { // and not a copy // add back to where it came from game.getAction().moveTo(fromZone, ability.getHostCard(), zonePosition >= 0 ? Integer.valueOf(zonePosition) : null); + ability.getHostCard().setState(fromState, true); } clearTargets(ability); @@ -255,7 +266,7 @@ public class HumanPlaySpellAbility { for (String aVar : announce.split(",")) { String varName = aVar.trim(); if ("CreatureType".equals(varName)) { - String choice = pc.chooseSomeType("Creature", ability, CardType.Constant.CREATURE_TYPES, new ArrayList()); + final String choice = pc.chooseSomeType("Creature", ability, CardType.Constant.CREATURE_TYPES, Collections.emptyList()); ability.getHostCard().setChosenType(choice); } if ("ChooseNumber".equals(varName)) {