diff --git a/Mage.Sets/src/mage/cards/a/AssembleThePlayers.java b/Mage.Sets/src/mage/cards/a/AssembleThePlayers.java index 18b2dddf2d3..a52007b7c0f 100644 --- a/Mage.Sets/src/mage/cards/a/AssembleThePlayers.java +++ b/Mage.Sets/src/mage/cards/a/AssembleThePlayers.java @@ -3,21 +3,23 @@ package mage.cards.a; import mage.MageIdentifier; import mage.MageObjectReference; import mage.abilities.Ability; +import mage.abilities.SpellAbility; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.AsThoughEffectImpl; import mage.abilities.effects.common.continuous.LookAtTopCardOfLibraryAnyTimeEffect; -import mage.abilities.hint.Hint; import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.*; +import mage.constants.AsThoughEffectType; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.Outcome; import mage.game.Game; -import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.players.Player; -import mage.watchers.Watcher; +import mage.watchers.common.OnceEachTurnCastWatcher; -import java.util.*; +import java.util.UUID; /** * @author xenohedron @@ -33,9 +35,9 @@ public final class AssembleThePlayers extends CardImpl { // Once each turn, you may cast a creature spell with power 2 or less from the top of your library. this.addAbility( new SimpleStaticAbility(new AssembleThePlayersPlayTopEffect()) - .setIdentifier(MageIdentifier.AssembleThePlayersWatcher) - .addHint(AssembleThePlayersHint.instance), - new AssembleThePlayersWatcher() + .setIdentifier(MageIdentifier.OnceEachTurnCastWatcher) + .addHint(OnceEachTurnCastWatcher.getHint()), + new OnceEachTurnCastWatcher() // all based on Johann, Apprentice Sorcerer ); @@ -51,30 +53,6 @@ public final class AssembleThePlayers extends CardImpl { } } -enum AssembleThePlayersHint implements Hint { - instance; - - @Override - public String getText(Game game, Ability ability) { - AssembleThePlayersWatcher watcher = game.getState().getWatcher(AssembleThePlayersWatcher.class); - if (watcher != null) { - boolean used = watcher.isAbilityUsed(ability.getControllerId(), new MageObjectReference(ability.getSourceId(), game)); - if (used) { - Player player = game.getPlayer(ability.getControllerId()); - if (player != null) { - return "A spell has been cast by " + player.getLogName() + " with {this} this turn."; - } - } - } - return ""; - } - - @Override - public AssembleThePlayersHint copy() { - return this; - } -} - class AssembleThePlayersPlayTopEffect extends AsThoughEffectImpl { AssembleThePlayersPlayTopEffect() { @@ -98,62 +76,41 @@ class AssembleThePlayersPlayTopEffect extends AsThoughEffectImpl { @Override public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) { - // Only applies for the controller of the ability. - if (!affectedControllerId.equals(source.getControllerId())) { - return false; - } + throw new IllegalArgumentException("Wrong code usage: can't call applies method on empty affectedAbility"); + } + @Override + public boolean applies(UUID objectId, Ability affectedAbility, Ability source, Game game, UUID playerId) { Player controller = game.getPlayer(source.getControllerId()); - AssembleThePlayersWatcher watcher = game.getState().getWatcher(AssembleThePlayersWatcher.class); - Permanent sourceObject = game.getPermanent(source.getSourceId()); - if (controller == null || watcher == null || sourceObject == null) { + OnceEachTurnCastWatcher watcher = game.getState().getWatcher(OnceEachTurnCastWatcher.class); + Permanent sourcePermanent = source.getSourcePermanentIfItStillExists(game); + if (controller == null || sourcePermanent == null || watcher == null) { + return false; + } + // Only applies for the controller of the ability. + if (!playerId.equals(source.getControllerId())) { return false; } - // Has the ability already been used this turn by the player? - if (watcher.isAbilityUsed(controller.getId(), new MageObjectReference(sourceObject, game))) { + if (watcher.isAbilityUsed(controller.getId(), new MageObjectReference(sourcePermanent, game))) { return false; } - Card card = game.getCard(objectId); Card topCard = controller.getLibrary().getFromTop(game); // Is the card attempted to be played the top card of the library? if (card == null || topCard == null || !topCard.getId().equals(card.getMainCard().getId())) { return false; } - - // Only works for creatures with power 2 or less - return card.isCreature(game) && card.getPower().getValue() <=2; - } -} - -class AssembleThePlayersWatcher extends Watcher { - - // player -> set of all permanent's mor that already used their once per turn Approval. - private final Map> usedFrom = new HashMap<>(); - - public AssembleThePlayersWatcher() { - super(WatcherScope.GAME); - } - - @Override - public void watch(GameEvent event, Game game) { - UUID playerId = event.getPlayerId(); - if (event.getType() == GameEvent.EventType.SPELL_CAST - && event.hasApprovingIdentifier(MageIdentifier.AssembleThePlayersWatcher) - && playerId != null) { - usedFrom.computeIfAbsent(playerId, k -> new HashSet<>()) - .add(event.getAdditionalReference().getApprovingMageObjectReference()); + if (affectedAbility instanceof SpellAbility) { + SpellAbility spellAbility = (SpellAbility) affectedAbility; + if (spellAbility.getManaCosts().isEmpty() + || !spellAbility.spellCanBeActivatedRegularlyNow(playerId, game)) { + return false; + } + Card cardToCheck = spellAbility.getCharacteristics(game); + // Only works for creatures with power 2 or less + return cardToCheck.isCreature(game) && cardToCheck.getPower().getValue() <= 2; } - } - - @Override - public void reset() { - super.reset(); - usedFrom.clear(); - } - - public boolean isAbilityUsed(UUID playerId, MageObjectReference mor) { - return usedFrom.getOrDefault(playerId, Collections.emptySet()).contains(mor); + return false; } } diff --git a/Mage.Sets/src/mage/cards/a/AugurOfAutumn.java b/Mage.Sets/src/mage/cards/a/AugurOfAutumn.java index 26e26f2d42b..ce363acd115 100644 --- a/Mage.Sets/src/mage/cards/a/AugurOfAutumn.java +++ b/Mage.Sets/src/mage/cards/a/AugurOfAutumn.java @@ -8,7 +8,7 @@ import mage.abilities.condition.common.CovenCondition; import mage.abilities.decorator.ConditionalAsThoughEffect; import mage.abilities.effects.Effect; import mage.abilities.effects.common.continuous.LookAtTopCardOfLibraryAnyTimeEffect; -import mage.abilities.effects.common.continuous.PlayTheTopCardEffect; +import mage.abilities.effects.common.continuous.PlayFromTopOfLibraryEffect; import mage.abilities.hint.common.CovenHint; import mage.constants.AbilityWord; import mage.constants.SubType; @@ -41,11 +41,11 @@ public final class AugurOfAutumn extends CardImpl { this.addAbility(new SimpleStaticAbility(new LookAtTopCardOfLibraryAnyTimeEffect())); // You may play lands from the top of your library. - this.addAbility(new SimpleStaticAbility(new PlayTheTopCardEffect(TargetController.YOU, filter, false))); + this.addAbility(new SimpleStaticAbility(new PlayFromTopOfLibraryEffect(filter))); // Coven — As long as you control three or more creatures with different powers, you may cast creature spells from the top of your library. Effect effect = new ConditionalAsThoughEffect( - new PlayTheTopCardEffect(TargetController.YOU, filter2, false), + new PlayFromTopOfLibraryEffect(filter2), CovenCondition.instance ); effect.setText("As long as you control three or more creatures with different powers, you may cast creature spells from the top of your library"); diff --git a/Mage.Sets/src/mage/cards/c/CaseOfTheLockedHothouse.java b/Mage.Sets/src/mage/cards/c/CaseOfTheLockedHothouse.java index 9cf8c65397c..bd852204977 100644 --- a/Mage.Sets/src/mage/cards/c/CaseOfTheLockedHothouse.java +++ b/Mage.Sets/src/mage/cards/c/CaseOfTheLockedHothouse.java @@ -12,7 +12,7 @@ import mage.abilities.decorator.ConditionalAsThoughEffect; import mage.abilities.decorator.ConditionalContinuousEffect; import mage.abilities.effects.common.continuous.LookAtTopCardOfLibraryAnyTimeEffect; import mage.abilities.effects.common.continuous.PlayAdditionalLandsControllerEffect; -import mage.abilities.effects.common.continuous.PlayTheTopCardEffect; +import mage.abilities.effects.common.continuous.PlayFromTopOfLibraryEffect; import mage.abilities.hint.common.CaseSolvedHint; import mage.constants.ComparisonType; import mage.constants.Duration; @@ -64,7 +64,7 @@ public final class CaseOfTheLockedHothouse extends CardImpl { Ability solvedAbility = new SimpleStaticAbility(new ConditionalContinuousEffect( new LookAtTopCardOfLibraryAnyTimeEffect(), SolvedSourceCondition.SOLVED, "")); solvedAbility.addEffect(new ConditionalAsThoughEffect( - new PlayTheTopCardEffect(TargetController.YOU, filter2, false), + new PlayFromTopOfLibraryEffect(filter2), SolvedSourceCondition.SOLVED) .setText(", and you may play lands and cast creature and enchantment spells from the top of your library.")); diff --git a/Mage.Sets/src/mage/cards/c/CemeteryIlluminator.java b/Mage.Sets/src/mage/cards/c/CemeteryIlluminator.java index 72d499c61c3..0ee1e5920cb 100644 --- a/Mage.Sets/src/mage/cards/c/CemeteryIlluminator.java +++ b/Mage.Sets/src/mage/cards/c/CemeteryIlluminator.java @@ -1,32 +1,33 @@ package mage.cards.c; -import java.util.HashSet; -import java.util.Set; -import java.util.UUID; - import mage.MageIdentifier; import mage.MageInt; import mage.MageObject; import mage.MageObjectReference; import mage.abilities.Ability; +import mage.abilities.SpellAbility; import mage.abilities.common.EntersBattlefieldOrAttacksSourceTriggeredAbility; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.AsThoughEffectImpl; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.continuous.LookAtTopCardOfLibraryAnyTimeEffect; -import mage.cards.Card; -import mage.constants.*; import mage.abilities.keyword.FlyingAbility; +import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; +import mage.constants.*; import mage.game.ExileZone; import mage.game.Game; -import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.players.Player; import mage.target.common.TargetCardInGraveyard; import mage.util.CardUtil; -import mage.watchers.Watcher; +import mage.watchers.common.OnceEachTurnCastWatcher; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; /** * @@ -52,8 +53,9 @@ public final class CemeteryIlluminator extends CardImpl { // Once each turn, you may cast a spell from the top of your library if it shares a card type with a card exiled with Cemetery Illuminator. this.addAbility(new SimpleStaticAbility(new CemeteryIlluminatorPlayTopEffect()) - .setIdentifier(MageIdentifier.CemeteryIlluminatorWatcher), - new CemeteryIlluminatorWatcher()); + .setIdentifier(MageIdentifier.OnceEachTurnCastWatcher) + .addHint(OnceEachTurnCastWatcher.getHint()), + new OnceEachTurnCastWatcher()); } private CemeteryIlluminator(final CemeteryIlluminator card) { @@ -124,62 +126,46 @@ class CemeteryIlluminatorPlayTopEffect extends AsThoughEffectImpl { @Override public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) { - // Same checks as in PlayTheTopCardEffect - // Once per turn clause checked by Watcher same as Lurrus of the Dream Den - if (affectedControllerId.equals(source.getControllerId())) { - Player controller = game.getPlayer(source.getControllerId()); - CemeteryIlluminatorWatcher watcher = game.getState().getWatcher(CemeteryIlluminatorWatcher.class); - Permanent sourceObject = game.getPermanent(source.getSourceId()); - if (controller != null && watcher != null && sourceObject != null && !watcher.isAbilityUsed(new MageObjectReference(sourceObject, game))) { - Card card = game.getCard(objectId); - Card topCard = controller.getLibrary().getFromTop(game); - if (card != null && topCard != null && topCard.getId().equals(card.getMainCard().getId()) && !card.isLand(game) && !card.getManaCost().isEmpty()) { - // Check if it shares a card type with exiled cards - UUID exileId = CardUtil.getExileZoneId(game, source.getSourceId(), game.getState().getZoneChangeCounter(source.getSourceId())); - ExileZone exileZone = game.getExile().getExileZone(exileId); - if (exileZone != null) { - HashSet cardTypes = new HashSet<>(card.getCardType(game)); - for (UUID exileCardId : exileZone) { - Card exileCard = game.getCard(exileCardId); - if (exileCard != null) { - for (CardType exileType : exileCard.getCardType(game)) { - if (cardTypes.contains(exileType)) { - return true; - } - } - } - } - } - } - } - } - return false; - } -} - -class CemeteryIlluminatorWatcher extends Watcher { - - private final Set usedFrom = new HashSet<>(); - - public CemeteryIlluminatorWatcher() { - super(WatcherScope.GAME); + throw new IllegalArgumentException("Wrong code usage: can't call applies method on empty affectedAbility"); } @Override - public void watch(GameEvent event, Game game) { - if (event.getType() == GameEvent.EventType.SPELL_CAST - && event.hasApprovingIdentifier(MageIdentifier.CemeteryIlluminatorWatcher)) { - usedFrom.add(event.getAdditionalReference().getApprovingMageObjectReference()); + public boolean applies(UUID objectId, Ability affectedAbility, Ability source, Game game, UUID playerId) { + Player controller = game.getPlayer(source.getControllerId()); + OnceEachTurnCastWatcher watcher = game.getState().getWatcher(OnceEachTurnCastWatcher.class); + Permanent sourceObject = source.getSourcePermanentIfItStillExists(game); + if (controller == null || watcher == null || sourceObject == null) { + return false; } - } - - @Override - public void reset() { - super.reset(); - usedFrom.clear(); - } - - public boolean isAbilityUsed(MageObjectReference mor) { - return usedFrom.contains(mor); + // Reference logic from PlayFromTopOfLibraryEffect and CastFromGraveyardOnceEachTurnAbility + if (!playerId.equals(controller.getId()) || watcher.isAbilityUsed(playerId, new MageObjectReference(sourceObject, game))) { + return false; + } + Card card = game.getCard(objectId); + Card topCard = controller.getLibrary().getFromTop(game); + if (card == null || topCard == null || !topCard.getId().equals(card.getMainCard().getId())) { + return false; + } + if (!(affectedAbility instanceof SpellAbility)) { + return false; + } + // need to check characteristics of spell rather than card (e.g. adventure, morph, etc.) + Card cardToCast = ((SpellAbility) affectedAbility).getCharacteristics(game); + if (cardToCast.getManaCost().isEmpty()) { + return false; + } + Set cardTypes = new HashSet<>(cardToCast.getCardType(game)); + // Check if it shares a card type with exiled cards + UUID exileId = CardUtil.getExileZoneId(game, source.getSourceId(), game.getState().getZoneChangeCounter(source.getSourceId())); + ExileZone exileZone = game.getExile().getExileZone(exileId); + if (exileZone == null) { + return false; + } + return exileZone + .stream() + .map(game::getCard) + .filter(Objects::nonNull) + .flatMap(c -> c.getCardType(game).stream()) + .anyMatch(cardTypes::contains); } } diff --git a/Mage.Sets/src/mage/cards/c/ConspicuousSnoop.java b/Mage.Sets/src/mage/cards/c/ConspicuousSnoop.java index a790c80537f..fd371f10dab 100644 --- a/Mage.Sets/src/mage/cards/c/ConspicuousSnoop.java +++ b/Mage.Sets/src/mage/cards/c/ConspicuousSnoop.java @@ -3,7 +3,7 @@ package mage.cards.c; import mage.MageInt; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.common.GainActivatedAbilitiesOfTopCardEffect; -import mage.abilities.effects.common.continuous.PlayTheTopCardEffect; +import mage.abilities.effects.common.continuous.PlayFromTopOfLibraryEffect; import mage.abilities.effects.common.continuous.PlayWithTheTopCardRevealedEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -37,7 +37,7 @@ public final class ConspicuousSnoop extends CardImpl { this.addAbility(new SimpleStaticAbility(new PlayWithTheTopCardRevealedEffect())); // You may cast Goblin spells from the top of your library. - this.addAbility(new SimpleStaticAbility(new PlayTheTopCardEffect(TargetController.YOU, filter, false))); + this.addAbility(new SimpleStaticAbility(new PlayFromTopOfLibraryEffect(filter))); // As long as the top card of your library is a Goblin card, Conspicuous Snoop has all activated abilities of that card. this.addAbility(new SimpleStaticAbility(new GainActivatedAbilitiesOfTopCardEffect(filter.copy().withMessage("a Goblin card")))); diff --git a/Mage.Sets/src/mage/cards/c/CourserOfKruphix.java b/Mage.Sets/src/mage/cards/c/CourserOfKruphix.java index 9ed69c46a19..d782911fcbb 100644 --- a/Mage.Sets/src/mage/cards/c/CourserOfKruphix.java +++ b/Mage.Sets/src/mage/cards/c/CourserOfKruphix.java @@ -4,7 +4,7 @@ import mage.MageInt; import mage.abilities.common.LandfallAbility; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.common.GainLifeEffect; -import mage.abilities.effects.common.continuous.PlayTheTopCardEffect; +import mage.abilities.effects.common.continuous.PlayFromTopOfLibraryEffect; import mage.abilities.effects.common.continuous.PlayWithTheTopCardRevealedEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -34,7 +34,7 @@ public final class CourserOfKruphix extends CardImpl { this.addAbility(new SimpleStaticAbility(new PlayWithTheTopCardRevealedEffect())); // You may play lands from the top of your library. - this.addAbility(new SimpleStaticAbility(new PlayTheTopCardEffect(TargetController.YOU, filter, false))); + this.addAbility(new SimpleStaticAbility(new PlayFromTopOfLibraryEffect(filter))); // Whenever a land enters the battlefield under your control, you gain 1 life. this.addAbility(new LandfallAbility(new GainLifeEffect(1))); diff --git a/Mage.Sets/src/mage/cards/d/DanithaNewBenaliasLight.java b/Mage.Sets/src/mage/cards/d/DanithaNewBenaliasLight.java index ccb6f61a633..b458bd99339 100644 --- a/Mage.Sets/src/mage/cards/d/DanithaNewBenaliasLight.java +++ b/Mage.Sets/src/mage/cards/d/DanithaNewBenaliasLight.java @@ -1,25 +1,18 @@ package mage.cards.d; -import mage.MageIdentifier; import mage.MageInt; -import mage.MageObjectReference; -import mage.abilities.Ability; -import mage.abilities.SpellAbility; -import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.effects.AsThoughEffectImpl; +import mage.abilities.common.CastFromGraveyardOnceEachTurnAbility; import mage.abilities.keyword.LifelinkAbility; import mage.abilities.keyword.TrampleAbility; import mage.abilities.keyword.VigilanceAbility; -import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.*; -import mage.game.Game; -import mage.game.events.GameEvent; -import mage.watchers.Watcher; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.filter.FilterCard; +import mage.filter.predicate.Predicates; -import java.util.HashSet; -import java.util.Set; import java.util.UUID; /** @@ -27,6 +20,13 @@ import java.util.UUID; */ public final class DanithaNewBenaliasLight extends CardImpl { + private static final FilterCard filter = new FilterCard("an Aura or Equipment spell"); + static { + filter.add(Predicates.or( + SubType.AURA.getPredicate(), SubType.EQUIPMENT.getPredicate() + )); + } + public DanithaNewBenaliasLight(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{G}{W}"); @@ -46,9 +46,7 @@ public final class DanithaNewBenaliasLight extends CardImpl { this.addAbility(LifelinkAbility.getInstance()); // Once during each of your turns, you may cast an Aura or Equipment spell from your graveyard. - this.addAbility(new SimpleStaticAbility( - new DanithaNewBenaliasLightCastFromGraveyardEffect() - ).setIdentifier(MageIdentifier.DanithaNewBenaliasLightWatcher), new DanithaNewBenaliasLightWatcher()); + this.addAbility(new CastFromGraveyardOnceEachTurnAbility(filter)); } private DanithaNewBenaliasLight(final DanithaNewBenaliasLight card) { @@ -60,74 +58,3 @@ public final class DanithaNewBenaliasLight extends CardImpl { return new DanithaNewBenaliasLight(this); } } - -class DanithaNewBenaliasLightCastFromGraveyardEffect extends AsThoughEffectImpl { - - DanithaNewBenaliasLightCastFromGraveyardEffect() { - super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.PutCardInPlay); - staticText = "once during each of your turns, you may cast an Aura or Equipment spell from your graveyard"; - } - - private DanithaNewBenaliasLightCastFromGraveyardEffect(final DanithaNewBenaliasLightCastFromGraveyardEffect effect) { - super(effect); - } - - @Override - public boolean apply(Game game, Ability source) { - return true; - } - - @Override - public DanithaNewBenaliasLightCastFromGraveyardEffect copy() { - return new DanithaNewBenaliasLightCastFromGraveyardEffect(this); - } - - @Override - public boolean applies(UUID objectId, Ability affectedAbility, Ability source, Game game, UUID playerId) { - if (!source.isControlledBy(playerId) || game.getState().getZone(objectId) != Zone.GRAVEYARD || !(affectedAbility instanceof SpellAbility)) { - return false; - } - Card objectCard = ((SpellAbility) affectedAbility).getCharacteristics(game); - return objectCard != null - && objectCard.isOwnedBy(source.getControllerId()) - && (objectCard.hasSubtype(SubType.AURA, game) || objectCard.hasSubtype(SubType.EQUIPMENT, game)) - && objectCard.getSpellAbility() != null - && objectCard.getSpellAbility().spellCanBeActivatedRegularlyNow(playerId, game) - && !DanithaNewBenaliasLightWatcher.isAbilityUsed(source, game); - } - @Override - public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) { - throw new IllegalArgumentException("Wrong code usage: can't call applies method on empty affectedAbility"); - } -} - -class DanithaNewBenaliasLightWatcher extends Watcher { - - private final Set usedFrom = new HashSet<>(); - - DanithaNewBenaliasLightWatcher() { - super(WatcherScope.GAME); - } - - @Override - public void watch(GameEvent event, Game game) { - if (event.getType() == GameEvent.EventType.SPELL_CAST - && event.hasApprovingIdentifier(MageIdentifier.DanithaNewBenaliasLightWatcher)) { - usedFrom.add(event.getAdditionalReference().getApprovingMageObjectReference()); - } - } - - @Override - public void reset() { - super.reset(); - usedFrom.clear(); - } - - public static boolean isAbilityUsed(Ability source, Game game) { - return game - .getState() - .getWatcher(DanithaNewBenaliasLightWatcher.class) - .usedFrom - .contains(new MageObjectReference(source.getSourceId(), game)); - } -} diff --git a/Mage.Sets/src/mage/cards/e/ElshaOfTheInfinite.java b/Mage.Sets/src/mage/cards/e/ElshaOfTheInfinite.java index 6430b69db40..675c3ec7ee9 100644 --- a/Mage.Sets/src/mage/cards/e/ElshaOfTheInfinite.java +++ b/Mage.Sets/src/mage/cards/e/ElshaOfTheInfinite.java @@ -5,7 +5,7 @@ import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.common.continuous.CastAsThoughItHadFlashAllEffect; import mage.abilities.effects.common.continuous.LookAtTopCardOfLibraryAnyTimeEffect; -import mage.abilities.effects.common.continuous.PlayTheTopCardEffect; +import mage.abilities.effects.common.continuous.PlayFromTopOfLibraryEffect; import mage.abilities.keyword.ProwessAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -45,7 +45,7 @@ public final class ElshaOfTheInfinite extends CardImpl { this.addAbility(new SimpleStaticAbility(new LookAtTopCardOfLibraryAnyTimeEffect())); // You may cast noncreature spells from the top of your library. If you cast a spell this way, you may cast it as though it had flash. - Ability ability = new SimpleStaticAbility(new PlayTheTopCardEffect(TargetController.YOU, filter, false)); + Ability ability = new SimpleStaticAbility(new PlayFromTopOfLibraryEffect(filter)); ability.addEffect(new CastAsThoughItHadFlashAllEffect( Duration.WhileOnBattlefield, filter ).setText("If you cast a spell this way, you may cast it as though it had flash.")); @@ -61,4 +61,3 @@ public final class ElshaOfTheInfinite extends CardImpl { return new ElshaOfTheInfinite(this); } } - diff --git a/Mage.Sets/src/mage/cards/e/ElvenChorus.java b/Mage.Sets/src/mage/cards/e/ElvenChorus.java index a30afc59b44..364bb38b32d 100644 --- a/Mage.Sets/src/mage/cards/e/ElvenChorus.java +++ b/Mage.Sets/src/mage/cards/e/ElvenChorus.java @@ -3,13 +3,12 @@ package mage.cards.e; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.common.continuous.GainAbilityControlledEffect; import mage.abilities.effects.common.continuous.LookAtTopCardOfLibraryAnyTimeEffect; -import mage.abilities.effects.common.continuous.PlayTheTopCardEffect; +import mage.abilities.effects.common.continuous.PlayFromTopOfLibraryEffect; import mage.abilities.mana.AnyColorManaAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Duration; -import mage.constants.TargetController; import mage.filter.FilterCard; import mage.filter.StaticFilters; import mage.filter.common.FilterCreatureCard; @@ -30,9 +29,7 @@ public final class ElvenChorus extends CardImpl { this.addAbility(new SimpleStaticAbility(new LookAtTopCardOfLibraryAnyTimeEffect())); // You may cast creature spells from the top of your library. - this.addAbility(new SimpleStaticAbility(new PlayTheTopCardEffect( - TargetController.YOU, filter, false - ))); + this.addAbility(new SimpleStaticAbility(new PlayFromTopOfLibraryEffect(filter))); // Creatures you control have "{T}: Add one mana of any color." this.addAbility(new SimpleStaticAbility(new GainAbilityControlledEffect( diff --git a/Mage.Sets/src/mage/cards/e/EmperorMihailII.java b/Mage.Sets/src/mage/cards/e/EmperorMihailII.java index 01523fcafe9..202eb5da35b 100644 --- a/Mage.Sets/src/mage/cards/e/EmperorMihailII.java +++ b/Mage.Sets/src/mage/cards/e/EmperorMihailII.java @@ -7,13 +7,12 @@ import mage.abilities.costs.mana.GenericManaCost; import mage.abilities.effects.common.CreateTokenEffect; import mage.abilities.effects.common.DoIfCostPaid; import mage.abilities.effects.common.continuous.LookAtTopCardOfLibraryAnyTimeEffect; -import mage.abilities.effects.common.continuous.PlayTheTopCardEffect; +import mage.abilities.effects.common.continuous.PlayFromTopOfLibraryEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; import mage.constants.SuperType; -import mage.constants.TargetController; import mage.filter.FilterCard; import mage.filter.FilterSpell; import mage.game.permanent.token.MerfolkToken; @@ -46,9 +45,7 @@ public final class EmperorMihailII extends CardImpl { this.addAbility(new SimpleStaticAbility(new LookAtTopCardOfLibraryAnyTimeEffect())); // You may cast Merfolk spells from the top of your library. - this.addAbility(new SimpleStaticAbility(new PlayTheTopCardEffect( - TargetController.YOU, filter, false - ))); + this.addAbility(new SimpleStaticAbility(new PlayFromTopOfLibraryEffect(filter))); // Whenever you cast a Merfolk spell, you may pay {1}. If you do, create a 1/1 blue Merfolk creature token. this.addAbility(new SpellCastControllerTriggeredAbility(new DoIfCostPaid( diff --git a/Mage.Sets/src/mage/cards/e/ErrantAndGiada.java b/Mage.Sets/src/mage/cards/e/ErrantAndGiada.java index a2d410626e6..441dd9d1125 100644 --- a/Mage.Sets/src/mage/cards/e/ErrantAndGiada.java +++ b/Mage.Sets/src/mage/cards/e/ErrantAndGiada.java @@ -3,7 +3,7 @@ package mage.cards.e; import mage.MageInt; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.common.continuous.LookAtTopCardOfLibraryAnyTimeEffect; -import mage.abilities.effects.common.continuous.PlayTheTopCardEffect; +import mage.abilities.effects.common.continuous.PlayFromTopOfLibraryEffect; import mage.abilities.keyword.FlashAbility; import mage.abilities.keyword.FlyingAbility; import mage.cards.CardImpl; @@ -51,7 +51,7 @@ public final class ErrantAndGiada extends CardImpl { this.addAbility(new SimpleStaticAbility(new LookAtTopCardOfLibraryAnyTimeEffect())); // You may cast spells with flash or flying from the top of your library. - this.addAbility(new SimpleStaticAbility(new PlayTheTopCardEffect(TargetController.YOU, filter, false))); + this.addAbility(new SimpleStaticAbility(new PlayFromTopOfLibraryEffect(filter))); } private ErrantAndGiada(final ErrantAndGiada card) { diff --git a/Mage.Sets/src/mage/cards/e/ExperimentalFrenzy.java b/Mage.Sets/src/mage/cards/e/ExperimentalFrenzy.java index bfe6f2c1586..b962100625f 100644 --- a/Mage.Sets/src/mage/cards/e/ExperimentalFrenzy.java +++ b/Mage.Sets/src/mage/cards/e/ExperimentalFrenzy.java @@ -7,7 +7,7 @@ import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.ContinuousRuleModifyingEffectImpl; import mage.abilities.effects.common.DestroySourceEffect; import mage.abilities.effects.common.continuous.LookAtTopCardOfLibraryAnyTimeEffect; -import mage.abilities.effects.common.continuous.PlayTheTopCardEffect; +import mage.abilities.effects.common.continuous.PlayFromTopOfLibraryEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -31,7 +31,7 @@ public final class ExperimentalFrenzy extends CardImpl { this.addAbility(new SimpleStaticAbility(new LookAtTopCardOfLibraryAnyTimeEffect())); // You may play lands and cast spells from the top of your library. - this.addAbility(new SimpleStaticAbility(new PlayTheTopCardEffect())); + this.addAbility(new SimpleStaticAbility(new PlayFromTopOfLibraryEffect())); // You can't play cards from your hand. this.addAbility(new SimpleStaticAbility(new ExperimentalFrenzyRestrictionEffect())); diff --git a/Mage.Sets/src/mage/cards/f/FutureSight.java b/Mage.Sets/src/mage/cards/f/FutureSight.java index b8427292165..fa7cda7e9c3 100644 --- a/Mage.Sets/src/mage/cards/f/FutureSight.java +++ b/Mage.Sets/src/mage/cards/f/FutureSight.java @@ -1,7 +1,7 @@ package mage.cards.f; import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.effects.common.continuous.PlayTheTopCardEffect; +import mage.abilities.effects.common.continuous.PlayFromTopOfLibraryEffect; import mage.abilities.effects.common.continuous.PlayWithTheTopCardRevealedEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -22,7 +22,7 @@ public final class FutureSight extends CardImpl { this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new PlayWithTheTopCardRevealedEffect())); // You may play lands and cast spells from the top of your library. - this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new PlayTheTopCardEffect())); + this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new PlayFromTopOfLibraryEffect())); } private FutureSight(final FutureSight card) { diff --git a/Mage.Sets/src/mage/cards/g/GaleaKindlerOfHope.java b/Mage.Sets/src/mage/cards/g/GaleaKindlerOfHope.java index 33d9f9cd119..55a9e824ab2 100644 --- a/Mage.Sets/src/mage/cards/g/GaleaKindlerOfHope.java +++ b/Mage.Sets/src/mage/cards/g/GaleaKindlerOfHope.java @@ -9,7 +9,7 @@ import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.ContinuousEffectImpl; import mage.abilities.effects.common.InfoEffect; import mage.abilities.effects.common.continuous.LookAtTopCardOfLibraryAnyTimeEffect; -import mage.abilities.effects.common.continuous.PlayTheTopCardEffect; +import mage.abilities.effects.common.continuous.PlayFromTopOfLibraryEffect; import mage.abilities.keyword.VigilanceAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -53,7 +53,7 @@ public final class GaleaKindlerOfHope extends CardImpl { this.addAbility(new SimpleStaticAbility(new LookAtTopCardOfLibraryAnyTimeEffect())); // You may cast Aura and Equipment spells from the top of your library. When you cast an Equipment spell this way, it gains "When this Equipment enters the battlefield, attach it to target creature you control." - Ability ability = new SimpleStaticAbility(new PlayTheTopCardEffect(TargetController.YOU, filter, false)); + Ability ability = new SimpleStaticAbility(new PlayFromTopOfLibraryEffect(filter)); ability.addEffect(new InfoEffect("When you cast an Equipment spell this way, it gains " + "\"When this Equipment enters the battlefield, attach it to target creature you control.\"")); this.addAbility(ability); diff --git a/Mage.Sets/src/mage/cards/g/GarruksHorde.java b/Mage.Sets/src/mage/cards/g/GarruksHorde.java index 7b00eca127f..0f5d11934e0 100644 --- a/Mage.Sets/src/mage/cards/g/GarruksHorde.java +++ b/Mage.Sets/src/mage/cards/g/GarruksHorde.java @@ -2,7 +2,7 @@ package mage.cards.g; import mage.MageInt; import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.effects.common.continuous.PlayTheTopCardEffect; +import mage.abilities.effects.common.continuous.PlayFromTopOfLibraryEffect; import mage.abilities.effects.common.continuous.PlayWithTheTopCardRevealedEffect; import mage.abilities.keyword.TrampleAbility; import mage.cards.CardImpl; @@ -36,7 +36,7 @@ public final class GarruksHorde extends CardImpl { this.addAbility(new SimpleStaticAbility(new PlayWithTheTopCardRevealedEffect())); // You may cast creature spells from the top of your library. - this.addAbility(new SimpleStaticAbility(new PlayTheTopCardEffect(TargetController.YOU, filter, false))); + this.addAbility(new SimpleStaticAbility(new PlayFromTopOfLibraryEffect(filter))); } private GarruksHorde(final GarruksHorde card) { diff --git a/Mage.Sets/src/mage/cards/g/GisaAndGeralf.java b/Mage.Sets/src/mage/cards/g/GisaAndGeralf.java index 8cab6cf72f9..b3520a300a4 100644 --- a/Mage.Sets/src/mage/cards/g/GisaAndGeralf.java +++ b/Mage.Sets/src/mage/cards/g/GisaAndGeralf.java @@ -1,25 +1,17 @@ package mage.cards.g; -import java.util.HashSet; -import java.util.Set; import mage.MageInt; -import mage.abilities.Ability; +import mage.abilities.common.CastFromGraveyardOnceEachTurnAbility; import mage.abilities.common.EntersBattlefieldTriggeredAbility; -import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.effects.AsThoughEffectImpl; import mage.abilities.effects.common.MillCardsControllerEffect; -import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.*; -import mage.game.Game; -import mage.game.events.GameEvent; -import mage.watchers.Watcher; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.filter.common.FilterCreatureCard; import java.util.UUID; -import mage.MageIdentifier; -import mage.MageObjectReference; -import mage.game.permanent.Permanent; /** * @@ -27,6 +19,11 @@ import mage.game.permanent.Permanent; */ public final class GisaAndGeralf extends CardImpl { + private static final FilterCreatureCard filter = new FilterCreatureCard("a Zombie creature spell"); + static { + filter.add(SubType.ZOMBIE.getPredicate()); + } + public GisaAndGeralf(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{U}{B}"); this.supertype.add(SuperType.LEGENDARY); @@ -38,10 +35,8 @@ public final class GisaAndGeralf extends CardImpl { // When Gisa and Geralf enters the battlefield, put the top four cards of your library into your graveyard. this.addAbility(new EntersBattlefieldTriggeredAbility(new MillCardsControllerEffect(4))); - // During each of your turns, you may cast a Zombie creature card from your graveyard. - this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new GisaAndGeralfCastFromGraveyardEffect()) - .setIdentifier(MageIdentifier.GisaAndGeralfWatcher), - new GisaAndGeralfWatcher()); + // Once during each of your turns, you may cast a Zombie creature spell from your graveyard + this.addAbility(new CastFromGraveyardOnceEachTurnAbility(filter)); } private GisaAndGeralf(final GisaAndGeralf card) { @@ -53,71 +48,3 @@ public final class GisaAndGeralf extends CardImpl { return new GisaAndGeralf(this); } } - -class GisaAndGeralfCastFromGraveyardEffect extends AsThoughEffectImpl { - - GisaAndGeralfCastFromGraveyardEffect() { - super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.PutCreatureInPlay); - staticText = "Once during each of your turns, you may cast a Zombie creature spell from your graveyard"; - } - - private GisaAndGeralfCastFromGraveyardEffect(final GisaAndGeralfCastFromGraveyardEffect effect) { - super(effect); - } - - @Override - public boolean apply(Game game, Ability source) { - return true; - } - - @Override - public GisaAndGeralfCastFromGraveyardEffect copy() { - return new GisaAndGeralfCastFromGraveyardEffect(this); - } - - @Override - public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) { - if (source.isControlledBy(affectedControllerId) - && Zone.GRAVEYARD.equals(game.getState().getZone(objectId))) { - Card objectCard = game.getCard(objectId); - Permanent sourceObject = game.getPermanent(source.getSourceId()); - if (sourceObject != null && objectCard != null - && objectCard.isOwnedBy(source.getControllerId()) - && objectCard.isCreature(game) - && objectCard.hasSubtype(SubType.ZOMBIE, game) - && objectCard.getSpellAbility() != null - && objectCard.getSpellAbility().spellCanBeActivatedRegularlyNow(affectedControllerId, game)) { - GisaAndGeralfWatcher watcher = game.getState().getWatcher(GisaAndGeralfWatcher.class); - return watcher != null && !watcher.isAbilityUsed(new MageObjectReference(sourceObject, game)); - } - } - return false; - } -} - -class GisaAndGeralfWatcher extends Watcher { - - private final Set usedFrom = new HashSet<>(); - - GisaAndGeralfWatcher() { - super(WatcherScope.GAME); - } - - @Override - public void watch(GameEvent event, Game game) { - if (GameEvent.EventType.SPELL_CAST.equals(event.getType()) - && event.hasApprovingIdentifier(MageIdentifier.GisaAndGeralfWatcher)) { - usedFrom.add(event.getAdditionalReference().getApprovingMageObjectReference()); - } - } - - @Override - public void reset() { - super.reset(); - usedFrom.clear(); - } - - public boolean isAbilityUsed(MageObjectReference mor) { - return usedFrom.contains(mor); - } -} diff --git a/Mage.Sets/src/mage/cards/i/IsuTheAbominable.java b/Mage.Sets/src/mage/cards/i/IsuTheAbominable.java index 9797bb855b6..0b4c0b05c51 100644 --- a/Mage.Sets/src/mage/cards/i/IsuTheAbominable.java +++ b/Mage.Sets/src/mage/cards/i/IsuTheAbominable.java @@ -7,14 +7,13 @@ import mage.abilities.costs.OrCost; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.common.DoIfCostPaid; import mage.abilities.effects.common.continuous.LookAtTopCardOfLibraryAnyTimeEffect; -import mage.abilities.effects.common.continuous.PlayTheTopCardEffect; +import mage.abilities.effects.common.continuous.PlayFromTopOfLibraryEffect; import mage.abilities.effects.common.counter.AddCountersSourceEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; import mage.constants.SuperType; -import mage.constants.TargetController; import mage.counters.CounterType; import mage.filter.FilterCard; import mage.filter.FilterPermanent; @@ -49,9 +48,7 @@ public final class IsuTheAbominable extends CardImpl { this.addAbility(new SimpleStaticAbility(new LookAtTopCardOfLibraryAnyTimeEffect())); // You may play snow lands and cast snow spells from the top of your library. - this.addAbility(new SimpleStaticAbility(new PlayTheTopCardEffect( - TargetController.YOU, filter, false - ))); + this.addAbility(new SimpleStaticAbility(new PlayFromTopOfLibraryEffect(filter))); // Whenever another snow permanent enters the battlefield under your control, you may pay {G}, {W}, or {U}. If you do, put a +1/+1 counter on Isu the Abominable. this.addAbility(new EntersBattlefieldControlledTriggeredAbility( diff --git a/Mage.Sets/src/mage/cards/j/JohannApprenticeSorcerer.java b/Mage.Sets/src/mage/cards/j/JohannApprenticeSorcerer.java index 227cb3bcded..dbc462f6484 100644 --- a/Mage.Sets/src/mage/cards/j/JohannApprenticeSorcerer.java +++ b/Mage.Sets/src/mage/cards/j/JohannApprenticeSorcerer.java @@ -4,21 +4,20 @@ import mage.MageIdentifier; import mage.MageInt; import mage.MageObjectReference; import mage.abilities.Ability; +import mage.abilities.SpellAbility; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.AsThoughEffectImpl; import mage.abilities.effects.common.continuous.LookAtTopCardOfLibraryAnyTimeEffect; -import mage.abilities.hint.Hint; import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; import mage.game.Game; -import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.players.Player; -import mage.watchers.Watcher; +import mage.watchers.common.OnceEachTurnCastWatcher; -import java.util.*; +import java.util.UUID; /** * @author Susucr @@ -40,9 +39,9 @@ public final class JohannApprenticeSorcerer extends CardImpl { // Once each turn, you may cast an instant or sorcery spell from the top of your library. this.addAbility( new SimpleStaticAbility(new JohannApprenticeSorcererPlayTopEffect()) - .setIdentifier(MageIdentifier.JohannApprenticeSorcererWatcher) - .addHint(JohannApprenticeSorcererHint.instance), - new JohannApprenticeSorcererWatcher() + .setIdentifier(MageIdentifier.OnceEachTurnCastWatcher) + .addHint(OnceEachTurnCastWatcher.getHint()), + new OnceEachTurnCastWatcher() ); } @@ -56,31 +55,6 @@ public final class JohannApprenticeSorcerer extends CardImpl { } } -enum JohannApprenticeSorcererHint implements Hint { - instance; - - @Override - public String getText(Game game, Ability ability) { - JohannApprenticeSorcererWatcher watcher = game.getState().getWatcher(JohannApprenticeSorcererWatcher.class); - if (watcher != null) { - boolean used = watcher.isAbilityUsed(ability.getControllerId(), new MageObjectReference(ability.getSourceId(), game)); - if (used) { - Player player = game.getPlayer(ability.getControllerId()); - if (player != null) { - return "A spell has been cast by " + player.getLogName() + " with {this} this turn."; - } - } - } - - return ""; - } - - @Override - public JohannApprenticeSorcererHint copy() { - return this; - } -} - class JohannApprenticeSorcererPlayTopEffect extends AsThoughEffectImpl { JohannApprenticeSorcererPlayTopEffect() { @@ -104,62 +78,42 @@ class JohannApprenticeSorcererPlayTopEffect extends AsThoughEffectImpl { @Override public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) { - // Only applies for the controller of the ability. - if (!affectedControllerId.equals(source.getControllerId())) { - return false; - } + throw new IllegalArgumentException("Wrong code usage: can't call applies method on empty affectedAbility"); + } + @Override + public boolean applies(UUID objectId, Ability affectedAbility, Ability source, Game game, UUID playerId) { Player controller = game.getPlayer(source.getControllerId()); - JohannApprenticeSorcererWatcher watcher = game.getState().getWatcher(JohannApprenticeSorcererWatcher.class); - Permanent sourceObject = game.getPermanent(source.getSourceId()); - if (controller == null || watcher == null || sourceObject == null) { + OnceEachTurnCastWatcher watcher = game.getState().getWatcher(OnceEachTurnCastWatcher.class); + Permanent sourcePermanent = source.getSourcePermanentIfItStillExists(game); + if (controller == null || sourcePermanent == null || watcher == null) { + return false; + } + // Only applies for the controller of the ability. + if (!playerId.equals(source.getControllerId())) { return false; } - // Has the ability already been used this turn by the player? - if (watcher.isAbilityUsed(controller.getId(), new MageObjectReference(sourceObject, game))) { + if (watcher.isAbilityUsed(controller.getId(), new MageObjectReference(sourcePermanent, game))) { return false; } - Card card = game.getCard(objectId); Card topCard = controller.getLibrary().getFromTop(game); // Is the card attempted to be played the top card of the library? if (card == null || topCard == null || !topCard.getId().equals(card.getMainCard().getId())) { return false; } - - // Only works for instant & sorcery. - return card.isInstantOrSorcery(game); - } -} - -class JohannApprenticeSorcererWatcher extends Watcher { - - // player -> set of all permanent's mor that already used their once per turn Approval. - private final Map> usedFrom = new HashMap<>(); - - public JohannApprenticeSorcererWatcher() { - super(WatcherScope.GAME); - } - - @Override - public void watch(GameEvent event, Game game) { - UUID playerId = event.getPlayerId(); - if (event.getType() == GameEvent.EventType.SPELL_CAST - && event.hasApprovingIdentifier(MageIdentifier.JohannApprenticeSorcererWatcher) - && playerId != null) { - usedFrom.computeIfAbsent(playerId, k -> new HashSet<>()) - .add(event.getAdditionalReference().getApprovingMageObjectReference()); + if (affectedAbility instanceof SpellAbility) { + SpellAbility spellAbility = (SpellAbility) affectedAbility; + if (spellAbility.getManaCosts().isEmpty() + || !spellAbility.spellCanBeActivatedRegularlyNow(playerId, game)) { + return false; + } + Card cardToCheck = spellAbility.getCharacteristics(game); + // Only works for instant & sorcery. + return cardToCheck.isInstantOrSorcery(game); } + return false; } - @Override - public void reset() { - super.reset(); - usedFrom.clear(); - } - - public boolean isAbilityUsed(UUID playerId, MageObjectReference mor) { - return usedFrom.getOrDefault(playerId, Collections.emptySet()).contains(mor); - } } diff --git a/Mage.Sets/src/mage/cards/k/KaradorGhostChieftain.java b/Mage.Sets/src/mage/cards/k/KaradorGhostChieftain.java index 7f7a8fa882d..37455d3b7ab 100644 --- a/Mage.Sets/src/mage/cards/k/KaradorGhostChieftain.java +++ b/Mage.Sets/src/mage/cards/k/KaradorGhostChieftain.java @@ -1,27 +1,21 @@ package mage.cards.k; -import java.util.HashSet; -import java.util.Set; -import java.util.UUID; -import mage.MageIdentifier; import mage.MageInt; -import mage.MageObjectReference; import mage.abilities.Ability; import mage.abilities.SpellAbility; +import mage.abilities.common.CastFromGraveyardOnceEachTurnAbility; import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.effects.AsThoughEffectImpl; import mage.abilities.effects.common.cost.CostModificationEffectImpl; -import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; import mage.filter.StaticFilters; +import mage.filter.common.FilterCreatureCard; import mage.game.Game; -import mage.game.events.GameEvent; -import mage.game.permanent.Permanent; import mage.players.Player; import mage.util.CardUtil; -import mage.watchers.Watcher; + +import java.util.UUID; /** * @@ -29,6 +23,8 @@ import mage.watchers.Watcher; */ public final class KaradorGhostChieftain extends CardImpl { + private static final FilterCreatureCard filter = new FilterCreatureCard("a creature spell"); + public KaradorGhostChieftain(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{5}{W}{B}{G}"); this.supertype.add(SuperType.LEGENDARY); @@ -42,10 +38,8 @@ public final class KaradorGhostChieftain extends CardImpl { this.addAbility(new SimpleStaticAbility(Zone.ALL, new KaradorGhostChieftainCostReductionEffect())); - // During each of your turns, you may cast one creature card from your graveyard. - this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, - new KaradorGhostChieftainCastFromGraveyardEffect()).setIdentifier(MageIdentifier.KaradorGhostChieftainWatcher), - new KaradorGhostChieftainWatcher()); + // Once during each of your turns, you may cast a creature spell from your graveyard. + this.addAbility(new CastFromGraveyardOnceEachTurnAbility(filter)); } private KaradorGhostChieftain(final KaradorGhostChieftain card) { @@ -94,73 +88,3 @@ class KaradorGhostChieftainCostReductionEffect extends CostModificationEffectImp return new KaradorGhostChieftainCostReductionEffect(this); } } - -class KaradorGhostChieftainCastFromGraveyardEffect extends AsThoughEffectImpl { - - KaradorGhostChieftainCastFromGraveyardEffect() { - super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.PutCreatureInPlay); - staticText = "During each of your turns, you may cast a creature spell from your graveyard"; - } - - private KaradorGhostChieftainCastFromGraveyardEffect(final KaradorGhostChieftainCastFromGraveyardEffect effect) { - super(effect); - } - - @Override - public boolean apply(Game game, Ability source) { - return true; - } - - @Override - public KaradorGhostChieftainCastFromGraveyardEffect copy() { - return new KaradorGhostChieftainCastFromGraveyardEffect(this); - } - - @Override - public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) { - if (source.isControlledBy(affectedControllerId) - && Zone.GRAVEYARD.equals(game.getState().getZone(objectId))) { - Card objectCard = game.getCard(objectId); - Permanent sourceObject = game.getPermanent(source.getSourceId()); // needs to be onto the battlefield - if (objectCard != null - && sourceObject != null - && objectCard.isOwnedBy(source.getControllerId()) - && objectCard.isCreature(game) - && objectCard.getSpellAbility() != null - && objectCard.getSpellAbility().spellCanBeActivatedRegularlyNow(affectedControllerId, game)) { - KaradorGhostChieftainWatcher watcher - = game.getState().getWatcher(KaradorGhostChieftainWatcher.class); - return watcher != null - && !watcher.isAbilityUsed(new MageObjectReference(sourceObject, game)); - } - } - return false; - } -} - -class KaradorGhostChieftainWatcher extends Watcher { - - private final Set usedFrom = new HashSet<>(); - - KaradorGhostChieftainWatcher() { - super(WatcherScope.GAME); - } - - @Override - public void watch(GameEvent event, Game game) { - if (GameEvent.EventType.SPELL_CAST.equals(event.getType()) - && event.hasApprovingIdentifier(MageIdentifier.KaradorGhostChieftainWatcher)) { - usedFrom.add(event.getAdditionalReference().getApprovingMageObjectReference()); - } - } - - @Override - public void reset() { - super.reset(); - usedFrom.clear(); - } - - public boolean isAbilityUsed(MageObjectReference mor) { - return usedFrom.contains(mor); - } -} diff --git a/Mage.Sets/src/mage/cards/k/KorlessaScaleSinger.java b/Mage.Sets/src/mage/cards/k/KorlessaScaleSinger.java index a400536ed69..da44063bc9e 100644 --- a/Mage.Sets/src/mage/cards/k/KorlessaScaleSinger.java +++ b/Mage.Sets/src/mage/cards/k/KorlessaScaleSinger.java @@ -3,7 +3,7 @@ package mage.cards.k; import mage.MageInt; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.common.continuous.LookAtTopCardOfLibraryAnyTimeEffect; -import mage.abilities.effects.common.continuous.PlayTheTopCardEffect; +import mage.abilities.effects.common.continuous.PlayFromTopOfLibraryEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -38,7 +38,7 @@ public final class KorlessaScaleSinger extends CardImpl { this.addAbility(new SimpleStaticAbility(new LookAtTopCardOfLibraryAnyTimeEffect())); // You may cast Dragon spells from the top of your library. - this.addAbility(new SimpleStaticAbility(new PlayTheTopCardEffect(TargetController.YOU, filter, false))); + this.addAbility(new SimpleStaticAbility(new PlayFromTopOfLibraryEffect(filter))); } private KorlessaScaleSinger(final KorlessaScaleSinger card) { diff --git a/Mage.Sets/src/mage/cards/l/LilaHospitalityHostess.java b/Mage.Sets/src/mage/cards/l/LilaHospitalityHostess.java index fc945da042c..dd5703ff43e 100644 --- a/Mage.Sets/src/mage/cards/l/LilaHospitalityHostess.java +++ b/Mage.Sets/src/mage/cards/l/LilaHospitalityHostess.java @@ -4,7 +4,7 @@ import mage.MageInt; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.common.continuous.BoostControlledEffect; import mage.abilities.effects.common.continuous.LookAtTopCardOfLibraryAnyTimeEffect; -import mage.abilities.effects.common.continuous.PlayTheTopCardEffect; +import mage.abilities.effects.common.continuous.PlayFromTopOfLibraryEffect; import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -41,9 +41,7 @@ public final class LilaHospitalityHostess extends CardImpl { this.addAbility(new SimpleStaticAbility(new LookAtTopCardOfLibraryAnyTimeEffect())); // You may cast common spells from the top of your library. - this.addAbility(new SimpleStaticAbility(new PlayTheTopCardEffect( - TargetController.YOU, filter, false - ))); + this.addAbility(new SimpleStaticAbility(new PlayFromTopOfLibraryEffect(filter))); // Guests you control get +1/+1. this.addAbility(new SimpleStaticAbility(new BoostControlledEffect( diff --git a/Mage.Sets/src/mage/cards/l/LurrusOfTheDreamDen.java b/Mage.Sets/src/mage/cards/l/LurrusOfTheDreamDen.java index 2cf85ac35c8..dece80f0725 100644 --- a/Mage.Sets/src/mage/cards/l/LurrusOfTheDreamDen.java +++ b/Mage.Sets/src/mage/cards/l/LurrusOfTheDreamDen.java @@ -2,7 +2,7 @@ package mage.cards.l; import mage.MageInt; import mage.MageObject; -import mage.abilities.common.CastFromGraveyardOnceStaticAbility; +import mage.abilities.common.CastFromGraveyardOnceEachTurnAbility; import mage.abilities.keyword.CompanionAbility; import mage.abilities.keyword.CompanionCondition; import mage.abilities.keyword.LifelinkAbility; @@ -24,7 +24,7 @@ import java.util.UUID; */ public final class LurrusOfTheDreamDen extends CardImpl { - private static final FilterPermanentCard filter = new FilterPermanentCard(); + private static final FilterPermanentCard filter = new FilterPermanentCard("a permanent spell with mana value 2 or less"); static { filter.add(new ManaValuePredicate(ComparisonType.FEWER_THAN, 3)); @@ -46,7 +46,7 @@ public final class LurrusOfTheDreamDen extends CardImpl { this.addAbility(LifelinkAbility.getInstance()); // During each of your turns, you may cast one permanent spell with converted mana cost 2 or less from your graveyard. - this.addAbility(new CastFromGraveyardOnceStaticAbility(filter, "During each of your turns, you may cast one permanent spell with mana value 2 or less from your graveyard")); + this.addAbility(new CastFromGraveyardOnceEachTurnAbility(filter)); } private LurrusOfTheDreamDen(final LurrusOfTheDreamDen card) { diff --git a/Mage.Sets/src/mage/cards/m/MagusOfTheFuture.java b/Mage.Sets/src/mage/cards/m/MagusOfTheFuture.java index f03135109c7..a5988407338 100644 --- a/Mage.Sets/src/mage/cards/m/MagusOfTheFuture.java +++ b/Mage.Sets/src/mage/cards/m/MagusOfTheFuture.java @@ -2,7 +2,7 @@ package mage.cards.m; import mage.MageInt; import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.effects.common.continuous.PlayTheTopCardEffect; +import mage.abilities.effects.common.continuous.PlayFromTopOfLibraryEffect; import mage.abilities.effects.common.continuous.PlayWithTheTopCardRevealedEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -29,7 +29,7 @@ public final class MagusOfTheFuture extends CardImpl { this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new PlayWithTheTopCardRevealedEffect())); // You may play lands and cast spells from the top of your library. - this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new PlayTheTopCardEffect())); + this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new PlayFromTopOfLibraryEffect())); } private MagusOfTheFuture(final MagusOfTheFuture card) { diff --git a/Mage.Sets/src/mage/cards/m/MelekIzzetParagon.java b/Mage.Sets/src/mage/cards/m/MelekIzzetParagon.java index e878280b28e..a98f111a506 100644 --- a/Mage.Sets/src/mage/cards/m/MelekIzzetParagon.java +++ b/Mage.Sets/src/mage/cards/m/MelekIzzetParagon.java @@ -5,7 +5,7 @@ import mage.abilities.TriggeredAbilityImpl; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.Effect; import mage.abilities.effects.common.CopyTargetSpellEffect; -import mage.abilities.effects.common.continuous.PlayTheTopCardEffect; +import mage.abilities.effects.common.continuous.PlayFromTopOfLibraryEffect; import mage.abilities.effects.common.continuous.PlayWithTheTopCardRevealedEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -46,7 +46,7 @@ public final class MelekIzzetParagon extends CardImpl { this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new PlayWithTheTopCardRevealedEffect())); // You may cast instant and sorcery spells from the top of your library. - this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new PlayTheTopCardEffect(TargetController.YOU, filter, false))); + this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new PlayFromTopOfLibraryEffect(filter))); // Whenever you cast an instant or sorcery spell from your library, copy it. You may choose new targets for the copy. this.addAbility(new MelekIzzetParagonTriggeredAbility()); diff --git a/Mage.Sets/src/mage/cards/m/MysticForge.java b/Mage.Sets/src/mage/cards/m/MysticForge.java index 158399d614a..bb3f3c3b2fa 100644 --- a/Mage.Sets/src/mage/cards/m/MysticForge.java +++ b/Mage.Sets/src/mage/cards/m/MysticForge.java @@ -7,7 +7,7 @@ import mage.abilities.costs.common.PayLifeCost; import mage.abilities.costs.common.TapSourceCost; import mage.abilities.effects.common.ExileCardsFromTopOfLibraryControllerEffect; import mage.abilities.effects.common.continuous.LookAtTopCardOfLibraryAnyTimeEffect; -import mage.abilities.effects.common.continuous.PlayTheTopCardEffect; +import mage.abilities.effects.common.continuous.PlayFromTopOfLibraryEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -40,7 +40,7 @@ public final class MysticForge extends CardImpl { this.addAbility(new SimpleStaticAbility(new LookAtTopCardOfLibraryAnyTimeEffect())); // You may cast artifact spells and colorless spells from the top of your library. - this.addAbility(new SimpleStaticAbility(new PlayTheTopCardEffect(TargetController.YOU, filter, false))); + this.addAbility(new SimpleStaticAbility(new PlayFromTopOfLibraryEffect(filter))); // {T}, Pay 1 life: Exile the top card of your library. Ability ability = new SimpleActivatedAbility(new ExileCardsFromTopOfLibraryControllerEffect(1), new TapSourceCost()); diff --git a/Mage.Sets/src/mage/cards/n/NaliaDeArnise.java b/Mage.Sets/src/mage/cards/n/NaliaDeArnise.java index 72195adfafd..ab9c8e933e1 100644 --- a/Mage.Sets/src/mage/cards/n/NaliaDeArnise.java +++ b/Mage.Sets/src/mage/cards/n/NaliaDeArnise.java @@ -8,7 +8,7 @@ import mage.abilities.condition.common.FullPartyCondition; import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; import mage.abilities.effects.common.continuous.GainAbilityAllEffect; import mage.abilities.effects.common.continuous.LookAtTopCardOfLibraryAnyTimeEffect; -import mage.abilities.effects.common.continuous.PlayTheTopCardEffect; +import mage.abilities.effects.common.continuous.PlayFromTopOfLibraryEffect; import mage.abilities.effects.common.counter.AddCountersAllEffect; import mage.abilities.hint.common.PartyCountHint; import mage.abilities.keyword.DeathtouchAbility; @@ -51,9 +51,7 @@ public final class NaliaDeArnise extends CardImpl { this.addAbility(new SimpleStaticAbility(new LookAtTopCardOfLibraryAnyTimeEffect())); // You may cast Cleric, Rogue, Warrior, and Wizard spells from the top of your library. - this.addAbility(new SimpleStaticAbility(new PlayTheTopCardEffect( - TargetController.YOU, filter, false - ))); + this.addAbility(new SimpleStaticAbility(new PlayFromTopOfLibraryEffect(filter))); // At the beginning of combat on your turn, if you have a full party, put a +1/+1 counter on each creature you control and those creatures gain deathtouch until end of turn. Ability ability = new ConditionalInterveningIfTriggeredAbility( diff --git a/Mage.Sets/src/mage/cards/o/OneWithTheMultiverse.java b/Mage.Sets/src/mage/cards/o/OneWithTheMultiverse.java index dc7ac80b6b1..fbffad823ab 100644 --- a/Mage.Sets/src/mage/cards/o/OneWithTheMultiverse.java +++ b/Mage.Sets/src/mage/cards/o/OneWithTheMultiverse.java @@ -7,7 +7,7 @@ import mage.abilities.common.SimpleStaticAbility; import mage.abilities.condition.Condition; import mage.abilities.effects.AsThoughEffectImpl; import mage.abilities.effects.common.continuous.LookAtTopCardOfLibraryAnyTimeEffect; -import mage.abilities.effects.common.continuous.PlayTheTopCardEffect; +import mage.abilities.effects.common.continuous.PlayFromTopOfLibraryEffect; import mage.abilities.hint.common.ConditionPermanentHint; import mage.cards.Card; import mage.cards.CardImpl; @@ -36,7 +36,7 @@ public final class OneWithTheMultiverse extends CardImpl { this.addAbility(new SimpleStaticAbility(new LookAtTopCardOfLibraryAnyTimeEffect())); // You may play lands and cast spells from the top of your library. - this.addAbility(new SimpleStaticAbility(new PlayTheTopCardEffect())); + this.addAbility(new SimpleStaticAbility(new PlayFromTopOfLibraryEffect())); // Once during each of your turns, you may cast a spell from your hand or the top of your library without paying its mana cost. this.addAbility( diff --git a/Mage.Sets/src/mage/cards/o/OracleOfMulDaya.java b/Mage.Sets/src/mage/cards/o/OracleOfMulDaya.java index a5a6bc5d02a..c59a1fce09c 100644 --- a/Mage.Sets/src/mage/cards/o/OracleOfMulDaya.java +++ b/Mage.Sets/src/mage/cards/o/OracleOfMulDaya.java @@ -3,7 +3,7 @@ package mage.cards.o; import mage.MageInt; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.common.continuous.PlayAdditionalLandsControllerEffect; -import mage.abilities.effects.common.continuous.PlayTheTopCardEffect; +import mage.abilities.effects.common.continuous.PlayFromTopOfLibraryEffect; import mage.abilities.effects.common.continuous.PlayWithTheTopCardRevealedEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -40,7 +40,7 @@ public final class OracleOfMulDaya extends CardImpl { this.addAbility(new SimpleStaticAbility(new PlayWithTheTopCardRevealedEffect())); // You may play lands from the top of your library. - this.addAbility(new SimpleStaticAbility(new PlayTheTopCardEffect(TargetController.YOU, filter, false))); + this.addAbility(new SimpleStaticAbility(new PlayFromTopOfLibraryEffect(filter))); } private OracleOfMulDaya(final OracleOfMulDaya card) { diff --git a/Mage.Sets/src/mage/cards/p/PrecognitionField.java b/Mage.Sets/src/mage/cards/p/PrecognitionField.java index 1641ba976c8..d233fcbb1e1 100644 --- a/Mage.Sets/src/mage/cards/p/PrecognitionField.java +++ b/Mage.Sets/src/mage/cards/p/PrecognitionField.java @@ -5,11 +5,10 @@ import mage.abilities.common.SimpleStaticAbility; import mage.abilities.costs.mana.GenericManaCost; import mage.abilities.effects.common.ExileCardsFromTopOfLibraryControllerEffect; import mage.abilities.effects.common.continuous.LookAtTopCardOfLibraryAnyTimeEffect; -import mage.abilities.effects.common.continuous.PlayTheTopCardEffect; +import mage.abilities.effects.common.continuous.PlayFromTopOfLibraryEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.TargetController; import mage.filter.FilterCard; import mage.filter.predicate.Predicates; @@ -36,7 +35,7 @@ public final class PrecognitionField extends CardImpl { this.addAbility(new SimpleStaticAbility(new LookAtTopCardOfLibraryAnyTimeEffect())); // You may cast instant and sorcery spells from the top of your library. - this.addAbility(new SimpleStaticAbility(new PlayTheTopCardEffect(TargetController.YOU, filter, false))); + this.addAbility(new SimpleStaticAbility(new PlayFromTopOfLibraryEffect(filter))); // {3}: Exile the top card of your library. this.addAbility(new SimpleActivatedAbility(new ExileCardsFromTopOfLibraryControllerEffect(1), new GenericManaCost(3))); diff --git a/Mage.Sets/src/mage/cards/r/RadhaHeartOfKeld.java b/Mage.Sets/src/mage/cards/r/RadhaHeartOfKeld.java index daedc26b920..4d15b5f9f8a 100644 --- a/Mage.Sets/src/mage/cards/r/RadhaHeartOfKeld.java +++ b/Mage.Sets/src/mage/cards/r/RadhaHeartOfKeld.java @@ -11,7 +11,7 @@ import mage.abilities.dynamicvalue.common.PermanentsOnBattlefieldCount; import mage.abilities.effects.common.continuous.BoostSourceEffect; import mage.abilities.effects.common.continuous.GainAbilitySourceEffect; import mage.abilities.effects.common.continuous.LookAtTopCardOfLibraryAnyTimeEffect; -import mage.abilities.effects.common.continuous.PlayTheTopCardEffect; +import mage.abilities.effects.common.continuous.PlayFromTopOfLibraryEffect; import mage.abilities.hint.common.MyTurnHint; import mage.abilities.keyword.FirstStrikeAbility; import mage.cards.CardImpl; @@ -48,7 +48,7 @@ public final class RadhaHeartOfKeld extends CardImpl { // You may look at the top card of your library any time, and you may play lands from the top of your library. LookAtTopCardOfLibraryAnyTimeEffect lookEffect = new LookAtTopCardOfLibraryAnyTimeEffect(); lookEffect.setText("You may look at the top card of your library any time"); - PlayTheTopCardEffect playEffect = new PlayTheTopCardEffect(TargetController.YOU, filter, false); + PlayFromTopOfLibraryEffect playEffect = new PlayFromTopOfLibraryEffect(filter); playEffect.setText(", and you may play lands from the top of your library"); SimpleStaticAbility lookAndPlayAbility = new SimpleStaticAbility(lookEffect); diff --git a/Mage.Sets/src/mage/cards/r/RangerClass.java b/Mage.Sets/src/mage/cards/r/RangerClass.java index b570eb499b8..bdb5a16b0a5 100644 --- a/Mage.Sets/src/mage/cards/r/RangerClass.java +++ b/Mage.Sets/src/mage/cards/r/RangerClass.java @@ -7,7 +7,7 @@ import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.common.CreateTokenEffect; import mage.abilities.effects.common.continuous.GainClassAbilitySourceEffect; import mage.abilities.effects.common.continuous.LookAtTopCardOfLibraryAnyTimeEffect; -import mage.abilities.effects.common.continuous.PlayTheTopCardEffect; +import mage.abilities.effects.common.continuous.PlayFromTopOfLibraryEffect; import mage.abilities.effects.common.counter.AddCountersTargetEffect; import mage.abilities.keyword.ClassLevelAbility; import mage.abilities.keyword.ClassReminderAbility; @@ -62,7 +62,7 @@ public final class RangerClass extends CardImpl { // You may cast creature spells from the top of your library. this.addAbility(new SimpleStaticAbility(new GainClassAbilitySourceEffect( - new PlayTheTopCardEffect(TargetController.YOU, filter, false), 3 + new PlayFromTopOfLibraryEffect(filter), 3 ))); } diff --git a/Mage.Sets/src/mage/cards/r/Realmwalker.java b/Mage.Sets/src/mage/cards/r/Realmwalker.java index 75fc454b9a8..a746a2743d4 100644 --- a/Mage.Sets/src/mage/cards/r/Realmwalker.java +++ b/Mage.Sets/src/mage/cards/r/Realmwalker.java @@ -5,7 +5,7 @@ import mage.abilities.common.AsEntersBattlefieldAbility; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.common.ChooseCreatureTypeEffect; import mage.abilities.effects.common.continuous.LookAtTopCardOfLibraryAnyTimeEffect; -import mage.abilities.effects.common.continuous.PlayTheTopCardEffect; +import mage.abilities.effects.common.continuous.PlayFromTopOfLibraryEffect; import mage.abilities.keyword.ChangelingAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -46,7 +46,7 @@ public final class Realmwalker extends CardImpl { this.addAbility(new SimpleStaticAbility(new LookAtTopCardOfLibraryAnyTimeEffect())); // You may cast creature spells of the chosen type from the top of your library. - this.addAbility(new SimpleStaticAbility(new PlayTheTopCardEffect(TargetController.YOU, filter, false))); + this.addAbility(new SimpleStaticAbility(new PlayFromTopOfLibraryEffect(filter))); } private Realmwalker(final Realmwalker card) { diff --git a/Mage.Sets/src/mage/cards/r/RivazOfTheClaw.java b/Mage.Sets/src/mage/cards/r/RivazOfTheClaw.java index b95474add03..6bc09194c35 100644 --- a/Mage.Sets/src/mage/cards/r/RivazOfTheClaw.java +++ b/Mage.Sets/src/mage/cards/r/RivazOfTheClaw.java @@ -1,7 +1,7 @@ package mage.cards.r; import mage.MageInt; -import mage.abilities.common.CastFromGraveyardOnceStaticAbility; +import mage.abilities.common.CastFromGraveyardOnceEachTurnAbility; import mage.abilities.common.DiesSourceTriggeredAbility; import mage.abilities.common.SpellCastControllerTriggeredAbility; import mage.abilities.effects.common.ExileSourceEffect; @@ -24,7 +24,7 @@ import java.util.UUID; public final class RivazOfTheClaw extends CardImpl { private static final FilterCreatureSpell manaAbilityFilter = new FilterCreatureSpell("Dragon creature spells"); - private static final FilterCreatureCard staticAbilityFilter = new FilterCreatureCard(); + private static final FilterCreatureCard staticAbilityFilter = new FilterCreatureCard("a Dragon creature spell"); private static final FilterCreatureSpell spellCastFilter = new FilterCreatureSpell("a Dragon creature spell from your graveyard"); static { @@ -50,7 +50,7 @@ public final class RivazOfTheClaw extends CardImpl { this.addAbility(new ConditionalAnyColorManaAbility(2, new ConditionalSpellManaBuilder(manaAbilityFilter))); // Once during each of your turns, you may cast a Dragon creature spell from your graveyard. - this.addAbility(new CastFromGraveyardOnceStaticAbility(staticAbilityFilter, "Once during each of your turns, you may cast a Dragon creature spell from your graveyard")); + this.addAbility(new CastFromGraveyardOnceEachTurnAbility(staticAbilityFilter)); // Whenever you cast a Dragon creature spell from your graveyard, it gains "When this creature dies, exile it." this.addAbility(new SpellCastControllerTriggeredAbility( diff --git a/Mage.Sets/src/mage/cards/s/SigardaFontOfBlessings.java b/Mage.Sets/src/mage/cards/s/SigardaFontOfBlessings.java index 3e79b06a8e7..8144e940147 100644 --- a/Mage.Sets/src/mage/cards/s/SigardaFontOfBlessings.java +++ b/Mage.Sets/src/mage/cards/s/SigardaFontOfBlessings.java @@ -4,7 +4,7 @@ import mage.MageInt; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.common.continuous.GainAbilityControlledEffect; import mage.abilities.effects.common.continuous.LookAtTopCardOfLibraryAnyTimeEffect; -import mage.abilities.effects.common.continuous.PlayTheTopCardEffect; +import mage.abilities.effects.common.continuous.PlayFromTopOfLibraryEffect; import mage.abilities.keyword.FlyingAbility; import mage.abilities.keyword.HexproofAbility; import mage.cards.CardImpl; @@ -51,9 +51,7 @@ public final class SigardaFontOfBlessings extends CardImpl { this.addAbility(new SimpleStaticAbility(new LookAtTopCardOfLibraryAnyTimeEffect())); // You may cast Angel spells and Human spells from the top of your library. - this.addAbility(new SimpleStaticAbility(new PlayTheTopCardEffect( - TargetController.YOU, filter, false - ))); + this.addAbility(new SimpleStaticAbility(new PlayFromTopOfLibraryEffect(filter))); } private SigardaFontOfBlessings(final SigardaFontOfBlessings card) { diff --git a/Mage.Sets/src/mage/cards/t/TheBelligerent.java b/Mage.Sets/src/mage/cards/t/TheBelligerent.java index dae76d87340..6f05a6f5a7f 100644 --- a/Mage.Sets/src/mage/cards/t/TheBelligerent.java +++ b/Mage.Sets/src/mage/cards/t/TheBelligerent.java @@ -5,7 +5,7 @@ import mage.abilities.Ability; import mage.abilities.common.AttacksTriggeredAbility; import mage.abilities.effects.common.CreateTokenEffect; import mage.abilities.effects.common.continuous.LookAtTopCardOfLibraryAnyTimeEffect; -import mage.abilities.effects.common.continuous.PlayTheTopCardEffect; +import mage.abilities.effects.common.continuous.PlayFromTopOfLibraryEffect; import mage.abilities.keyword.CrewAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -32,10 +32,8 @@ public final class TheBelligerent extends CardImpl { // Whenever The Belligerent attacks, create a Treasure token. Until end of turn, you may look at the top card of your library any time, and you may play lands and cast spells from the top of your library. Ability ability = new AttacksTriggeredAbility(new CreateTokenEffect(new TreasureToken())); - ability.addEffect(new LookAtTopCardOfLibraryAnyTimeEffect(TargetController.YOU, Duration.EndOfTurn)); - ability.addEffect(new PlayTheTopCardEffect( - TargetController.YOU, filter, false - ).concatBy(", and")); + ability.addEffect(new LookAtTopCardOfLibraryAnyTimeEffect(Duration.EndOfTurn)); + ability.addEffect(new PlayFromTopOfLibraryEffect(filter).setDuration(Duration.EndOfTurn).concatBy(", and")); this.addAbility(ability); // Crew 3 diff --git a/Mage.Sets/src/mage/cards/t/TheRealityChip.java b/Mage.Sets/src/mage/cards/t/TheRealityChip.java index 21d03266524..360e3213c3a 100644 --- a/Mage.Sets/src/mage/cards/t/TheRealityChip.java +++ b/Mage.Sets/src/mage/cards/t/TheRealityChip.java @@ -6,7 +6,7 @@ import mage.abilities.condition.Condition; import mage.abilities.condition.common.AttachedToMatchesFilterCondition; import mage.abilities.decorator.ConditionalAsThoughEffect; import mage.abilities.effects.common.continuous.LookAtTopCardOfLibraryAnyTimeEffect; -import mage.abilities.effects.common.continuous.PlayTheTopCardEffect; +import mage.abilities.effects.common.continuous.PlayFromTopOfLibraryEffect; import mage.abilities.keyword.ReconfigureAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -37,7 +37,7 @@ public final class TheRealityChip extends CardImpl { this.addAbility(new SimpleStaticAbility(new LookAtTopCardOfLibraryAnyTimeEffect())); // As long as The Reality Chip is attached to a creature, you may play lands and cast spells from the top of your library. - this.addAbility(new SimpleStaticAbility(new ConditionalAsThoughEffect(new PlayTheTopCardEffect(), condition) + this.addAbility(new SimpleStaticAbility(new ConditionalAsThoughEffect(new PlayFromTopOfLibraryEffect(), condition) .setText("as long as {this} is attached to a creature, you may play lands and cast spells from the top of your library"))); // Reconfigure {2}{U} diff --git a/Mage.Sets/src/mage/cards/v/VergeRangers.java b/Mage.Sets/src/mage/cards/v/VergeRangers.java index 09ddb5644ff..b3b44007daa 100644 --- a/Mage.Sets/src/mage/cards/v/VergeRangers.java +++ b/Mage.Sets/src/mage/cards/v/VergeRangers.java @@ -4,14 +4,12 @@ import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.common.continuous.LookAtTopCardOfLibraryAnyTimeEffect; -import mage.abilities.effects.common.continuous.PlayTheTopCardEffect; +import mage.abilities.effects.common.continuous.PlayFromTopOfLibraryEffect; import mage.abilities.keyword.FirstStrikeAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; -import mage.constants.TargetController; -import mage.constants.Zone; import mage.filter.FilterCard; import mage.filter.StaticFilters; import mage.filter.common.FilterLandCard; @@ -41,7 +39,7 @@ public final class VergeRangers extends CardImpl { this.addAbility(new SimpleStaticAbility(new LookAtTopCardOfLibraryAnyTimeEffect())); // As long as an opponent controls more lands than you, you may play lands from the top of your library. - this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new VergeRangersEffect())); + this.addAbility(new SimpleStaticAbility(new VergeRangersEffect())); } private VergeRangers(final VergeRangers card) { @@ -54,12 +52,12 @@ public final class VergeRangers extends CardImpl { } } -class VergeRangersEffect extends PlayTheTopCardEffect { +class VergeRangersEffect extends PlayFromTopOfLibraryEffect { private static final FilterCard filter = new FilterLandCard("play lands"); - public VergeRangersEffect() { - super(TargetController.YOU, filter, false); + VergeRangersEffect() { + super(filter); staticText = "As long as an opponent controls more lands than you, you may play lands from the top of your library"; } @@ -85,4 +83,4 @@ class VergeRangersEffect extends PlayTheTopCardEffect { .orElse(0); return maxOpponentLandCount > myLandCount; } -} \ No newline at end of file +} diff --git a/Mage.Sets/src/mage/cards/v/VivienMonstersAdvocate.java b/Mage.Sets/src/mage/cards/v/VivienMonstersAdvocate.java index 164bef33b1b..47138bda3a1 100644 --- a/Mage.Sets/src/mage/cards/v/VivienMonstersAdvocate.java +++ b/Mage.Sets/src/mage/cards/v/VivienMonstersAdvocate.java @@ -7,7 +7,7 @@ import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.CreateDelayedTriggeredAbilityEffect; import mage.abilities.effects.common.continuous.LookAtTopCardOfLibraryAnyTimeEffect; -import mage.abilities.effects.common.continuous.PlayTheTopCardEffect; +import mage.abilities.effects.common.continuous.PlayFromTopOfLibraryEffect; import mage.abilities.effects.common.search.SearchLibraryPutInPlayEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -47,7 +47,7 @@ public final class VivienMonstersAdvocate extends CardImpl { this.addAbility(new SimpleStaticAbility(new LookAtTopCardOfLibraryAnyTimeEffect())); // You may cast creature spells from the top of your library. - this.addAbility(new SimpleStaticAbility(new PlayTheTopCardEffect(TargetController.YOU, filter, false))); + this.addAbility(new SimpleStaticAbility(new PlayFromTopOfLibraryEffect(filter))); // +1: Create a 3/3 green Beast creature token. Put your choice of a vigilance // counter, a reach counter, or a trample counter on it. diff --git a/Mage.Sets/src/mage/cards/v/VizierOfTheMenagerie.java b/Mage.Sets/src/mage/cards/v/VizierOfTheMenagerie.java index 52c6f708348..b560d5ff305 100644 --- a/Mage.Sets/src/mage/cards/v/VizierOfTheMenagerie.java +++ b/Mage.Sets/src/mage/cards/v/VizierOfTheMenagerie.java @@ -7,7 +7,7 @@ import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.AsThoughEffectImpl; import mage.abilities.effects.AsThoughManaEffect; import mage.abilities.effects.common.continuous.LookAtTopCardOfLibraryAnyTimeEffect; -import mage.abilities.effects.common.continuous.PlayTheTopCardEffect; +import mage.abilities.effects.common.continuous.PlayFromTopOfLibraryEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; @@ -37,9 +37,7 @@ public final class VizierOfTheMenagerie extends CardImpl { this.addAbility(new SimpleStaticAbility(new LookAtTopCardOfLibraryAnyTimeEffect())); // You may cast creature spells from the top of your library. - this.addAbility(new SimpleStaticAbility(new PlayTheTopCardEffect( - TargetController.YOU, filter, false - ))); + this.addAbility(new SimpleStaticAbility(new PlayFromTopOfLibraryEffect(filter))); // You may spend mana as though it were mana of any type to cast creature spells. this.addAbility(new SimpleStaticAbility(new VizierOfTheMenagerieManaEffect())); diff --git a/Mage.Sets/src/mage/cards/x/XanatharGuildKingpin.java b/Mage.Sets/src/mage/cards/x/XanatharGuildKingpin.java index 3cae01e63d0..cc0445d5f83 100644 --- a/Mage.Sets/src/mage/cards/x/XanatharGuildKingpin.java +++ b/Mage.Sets/src/mage/cards/x/XanatharGuildKingpin.java @@ -3,13 +3,9 @@ package mage.cards.x; import mage.MageInt; import mage.MageObject; import mage.abilities.Ability; +import mage.abilities.SpellAbility; import mage.abilities.common.BeginningOfUpkeepTriggeredAbility; -import mage.abilities.effects.AsThoughEffectImpl; -import mage.abilities.effects.AsThoughManaEffect; -import mage.abilities.effects.ContinuousRuleModifyingEffectImpl; -import mage.abilities.effects.OneShotEffect; -import mage.abilities.effects.common.continuous.LookAtTopCardOfLibraryAnyTimeTargetEffect; -import mage.abilities.effects.common.continuous.PlayTheTopCardTargetEffect; +import mage.abilities.effects.*; import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -44,9 +40,9 @@ public final class XanatharGuildKingpin extends CardImpl { .setText("choose target opponent. Until end of turn, that player can't cast spells,"), TargetController.YOU, false ); - ability.addEffect(new LookAtTopCardOfLibraryAnyTimeTargetEffect(Duration.EndOfTurn) + ability.addEffect(new XanatharLookAtTopCardOfLibraryEffect() .setText(" you may look at the top card of their library any time,")); - ability.addEffect(new PlayTheTopCardTargetEffect() + ability.addEffect(new XanatharPlayFromTopOfTargetLibraryEffect() .setText(" you may play the top card of their library,")); ability.addEffect(new XanatharGuildKingpinSpendManaAsAnyColorOneShotEffect() .setText(" and you may spend mana as thought it were mana of any color to cast spells this way")); @@ -102,6 +98,90 @@ class XanatharGuildKingpinRuleModifyingEffect extends ContinuousRuleModifyingEff } } +class XanatharLookAtTopCardOfLibraryEffect extends ContinuousEffectImpl { + + XanatharLookAtTopCardOfLibraryEffect() { + super(Duration.EndOfTurn, Layer.PlayerEffects, SubLayer.NA, Outcome.Benefit); + } + + private XanatharLookAtTopCardOfLibraryEffect(final XanatharLookAtTopCardOfLibraryEffect effect) { + super(effect); + } + + @Override + public XanatharLookAtTopCardOfLibraryEffect copy() { + return new XanatharLookAtTopCardOfLibraryEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + if (game.inCheckPlayableState()) { // Ignored - see https://github.com/magefree/mage/issues/6994 + return false; + } + Player controller = game.getPlayer(source.getControllerId()); + Player opponent = game.getPlayer(getTargetPointer().getFirst(game, source)); + if (controller == null || opponent == null) { + return false; + } + if (!canLookAtNextTopLibraryCard(game)) { + return false; + } + Card topCard = opponent.getLibrary().getFromTop(game); + if (topCard == null) { + return false; + } + controller.lookAtCards("Top card of " + opponent.getName() + "'s library", topCard, game); + return true; + } + +} + +class XanatharPlayFromTopOfTargetLibraryEffect extends AsThoughEffectImpl { + + XanatharPlayFromTopOfTargetLibraryEffect() { + super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.EndOfTurn, Outcome.Benefit); + } + + private XanatharPlayFromTopOfTargetLibraryEffect(final XanatharPlayFromTopOfTargetLibraryEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + return true; + } + + @Override + public XanatharPlayFromTopOfTargetLibraryEffect copy() { + return new XanatharPlayFromTopOfTargetLibraryEffect(this); + } + + @Override + public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) { + throw new IllegalArgumentException("Wrong code usage: can't call applies method on empty affectedAbility"); + } + + @Override + public boolean applies(UUID objectId, Ability affectedAbility, Ability source, Game game, UUID playerId) { + if (!(affectedAbility instanceof SpellAbility) || !playerId.equals(source.getControllerId())) { + return false; + } + SpellAbility spell = (SpellAbility) affectedAbility; + Card cardToCheck = spell.getCharacteristics(game); + if (spell.getManaCosts().isEmpty()) { + return false; + } + Player controller = game.getPlayer(source.getControllerId()); + Player opponent = game.getPlayer(getTargetPointer().getFirst(game, source)); + if (controller == null || opponent == null) { + return false; + } + // main card of spell must be on top of the opponent's library + Card topCard = opponent.getLibrary().getFromTop(game); + return topCard != null && topCard.getId().equals(cardToCheck.getMainCard().getId()); + } +} + class XanatharGuildKingpinSpendManaAsAnyColorOneShotEffect extends OneShotEffect { XanatharGuildKingpinSpendManaAsAnyColorOneShotEffect() { diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/other/CastFromGraveyardOnceTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/other/CastFromGraveyardOnceTest.java new file mode 100644 index 00000000000..125d65438b2 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/other/CastFromGraveyardOnceTest.java @@ -0,0 +1,148 @@ +package org.mage.test.cards.abilities.other; + +import mage.abilities.keyword.IndestructibleAbility; +import mage.constants.EmptyNames; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author xenohedron + */ +public class CastFromGraveyardOnceTest extends CardTestPlayerBase { + + private static final String danitha = "Danitha, New Benalia's Light"; // 2/2 + // Vigilance, trample, lifelink + // Once during each of your turns, you may cast an Aura or Equipment spell from your graveyard. + + private static final String bonesplitter = "Bonesplitter"; // 1 mana equip 1 for +2/+0 + private static final String kitesail = "Kitesail"; // 2 mana equip 2 for +1/+0 and flying + private static final String creature = "Field Creeper"; // 2 mana 2/1 + private static final String halvar = "Halvar, God of Battle"; // MDFC front side - creature 2WW + private static final String sword = "Sword of the Realms"; // MDFC back side - equipment 1W + private static final String auraMorph = "Gift of Doom"; // 4B enchant creature, or morph + private static final String saguArcher = "Sagu Archer"; // 4G 2/5 reach, or morph + private static final String unicorn = "Lonesome Unicorn"; // 4W 3/3 with adventure 2W 2/2 token + private static final String rider = "Rider in Need"; + + private static final String karador = "Karador, Ghost Chieftain"; + // Once during each of your turns, you may cast a creature spell from your graveyard. + + @Test + public void testDanithaAllowsOneCast() { + addCard(Zone.BATTLEFIELD, playerA, danitha); + addCard(Zone.GRAVEYARD, playerA, bonesplitter); + addCard(Zone.GRAVEYARD, playerA, kitesail); + addCard(Zone.GRAVEYARD, playerA, creature); + addCard(Zone.BATTLEFIELD, playerA, "Wastes", 3); + addCard(Zone.BATTLEFIELD, playerA, "Raff Capashen, Ship's Mage"); // historic spells have flash + + checkPlayableAbility("bonesplitter your turn", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + bonesplitter, true); + checkPlayableAbility("kitesail your turn", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + kitesail, true); + checkPlayableAbility("creature not permitted", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + creature, false); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, kitesail); + + checkPermanentCount("kitesail on battlefield", 1, PhaseStep.BEGIN_COMBAT, playerA, kitesail, 1); + checkPlayableAbility("no second cast", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Cast " + bonesplitter, false); + + checkPlayableAbility("not during opponent's turn", 2, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + bonesplitter, false); + + checkPlayableAbility("available next turn", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + bonesplitter, true); + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, bonesplitter); + + setStrictChooseMode(true); + setStopAt(3, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, bonesplitter, 1); + } + + @Test + public void testDanithaMDFC() { + addCard(Zone.BATTLEFIELD, playerA, danitha); + addCard(Zone.GRAVEYARD, playerA, halvar); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 4); + + checkPlayableAbility("front not allowed", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + halvar, false); + checkPlayableAbility("back allowed", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + sword, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, sword); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, sword, 1); + } + + @Test + public void testDanithaMorph() { + addCard(Zone.BATTLEFIELD, playerA, danitha); + addCard(Zone.GRAVEYARD, playerA, auraMorph); + addCard(Zone.BATTLEFIELD, playerA, creature); // to enchant + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 5); + + checkPlayableAbility("morph not allowed", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + auraMorph + " using Morph", false); + checkPlayableAbility("aura allowed", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + auraMorph, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, auraMorph, creature); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, auraMorph, 1); + assertAbility(playerA, creature, IndestructibleAbility.getInstance(), true); + } + + @Test + public void testKaradorCastWithoutMorph() { + addCard(Zone.BATTLEFIELD, playerA, karador); + addCard(Zone.GRAVEYARD, playerA, saguArcher); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 5); + + checkPlayableAbility("with morph", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + saguArcher + " using Morph", true); + checkPlayableAbility("without morph", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + saguArcher, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, saguArcher); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, saguArcher, 1); + } + + @Test + public void testKaradorCastWithMorph() { + addCard(Zone.BATTLEFIELD, playerA, karador); + addCard(Zone.GRAVEYARD, playerA, saguArcher); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 5); + + checkPlayableAbility("with morph", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + saguArcher + " using Morph", true); + checkPlayableAbility("without morph", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + saguArcher, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, saguArcher + " using Morph"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); + } + + @Test + public void testKaradorCastAdventure() { + addCard(Zone.BATTLEFIELD, playerA, karador); + addCard(Zone.GRAVEYARD, playerA, unicorn); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 5); + + checkPlayableAbility("creature", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + unicorn, true); + checkPlayableAbility("sorcery", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + rider, false); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, unicorn); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, unicorn, 1); + } + +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/other/MultipleAsThoughEffects.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/other/MultipleAsThoughEffectsTest.java similarity index 87% rename from Mage.Tests/src/test/java/org/mage/test/cards/abilities/other/MultipleAsThoughEffects.java rename to Mage.Tests/src/test/java/org/mage/test/cards/abilities/other/MultipleAsThoughEffectsTest.java index c8d72b56cec..8a52fb685a5 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/other/MultipleAsThoughEffects.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/other/MultipleAsThoughEffectsTest.java @@ -8,16 +8,16 @@ import org.mage.test.serverside.base.CardTestPlayerBase; /** * @author Alex-Vasile, Susucr */ -public class MultipleAsThoughEffects extends CardTestPlayerBase { +public class MultipleAsThoughEffectsTest extends CardTestPlayerBase { /** - * Reported bug: https://github.com/magefree/mage/issues/8584 - * + * Reported bug #8584 + *

* If there are multiple effects which allow a player to cast a spell, - * they should be able to choose which one they whish to use. + * they should be able to choose which one they wish to use. */ @Test - public void ChoosingAlternateCastingMethod() { + public void testChoosingAlternateCastingMethod() { setStrictChooseMode(true); skipInitShuffling(); @@ -51,13 +51,13 @@ public class MultipleAsThoughEffects extends CardTestPlayerBase { } /** - * Reported bug: https://github.com/magefree/mage/issues/2087 - * + * Reported bug: #2087 + *

* If there are multiple effects which allow a player to cast a spell, - * they should be able to choose which one they whish to use, even if one is single-use. + * they should be able to choose which one they wish to use, even if one is single-use. */ @Test - public void RisenExecutioner() { + public void testRisenExecutioner() { setStrictChooseMode(true); // You may cast Risen Executioner from your graveyard if you pay {1} more to cast it for each other creature card in your graveyard. @@ -78,4 +78,4 @@ public class MultipleAsThoughEffects extends CardTestPlayerBase { assertPermanentCount(playerA, "Risen Executioner", 2); assertTappedCount("Swamp", true, 4 + 5); } -} \ No newline at end of file +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/c17/KessDissidentMageTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/c17/KessDissidentMageTest.java index bbd572d20f9..36684010cc0 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/c17/KessDissidentMageTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/c17/KessDissidentMageTest.java @@ -2,6 +2,7 @@ package org.mage.test.cards.single.c17; import mage.constants.PhaseStep; import mage.constants.Zone; +import org.junit.Ignore; import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; @@ -97,4 +98,68 @@ public class KessDissidentMageTest extends CardTestPlayerBase { setStopAt(1, PhaseStep.END_TURN); execute(); } + + private static final String unicorn = "Lonesome Unicorn"; // 4W 3/3 with adventure 2W 2/2 token + private static final String rider = "Rider in Need"; + private static final String lifegain = "Chaplain's Blessing"; + private static final String kess = "Kess, Dissident Mage"; + // Once during each of your turns, you may cast an instant or sorcery spell from your graveyard. + // If a spell cast this way would be put into your graveyard, exile it instead. + + @Test + public void testKessCastAdventure() { + addCard(Zone.BATTLEFIELD, playerA, kess); + addCard(Zone.GRAVEYARD, playerA, lifegain); + addCard(Zone.GRAVEYARD, playerA, unicorn); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 6); + + checkPlayableAbility("lifegain", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + lifegain, true); + checkPlayableAbility("creature", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + unicorn, false); + checkPlayableAbility("adventure", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + rider, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, rider); + + checkPlayableAbility("already used", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Cast " + lifegain, false); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, "Knight Token", 1); + assertExileCount(playerA, unicorn, 1); + } + + @Test + @Ignore("failing, see issue #11924") + public void testKessCastAdventureAfterDeath() { + addCard(Zone.BATTLEFIELD, playerA, kess); + addCard(Zone.GRAVEYARD, playerA, lifegain); + addCard(Zone.HAND, playerA, unicorn); + addCard(Zone.BATTLEFIELD, playerA, "Blood Bairn"); // for sacrificing creature + addCard(Zone.BATTLEFIELD, playerA, "Plains", 6); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, unicorn); + + checkPlayableAbility("lifegain", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Cast " + lifegain, true); + checkPlayableAbility("creature", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Cast " + unicorn, false); + checkPlayableAbility("adventure", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Cast " + rider, false); + activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Sacrifice another"); + setChoice(playerA, unicorn); + + checkGraveyardCount("sacrificed", 2, PhaseStep.PRECOMBAT_MAIN, playerA, unicorn, 1); + + checkPlayableAbility("lifegain", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + lifegain, true); + checkPlayableAbility("creature", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + unicorn, false); + checkPlayableAbility("adventure", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + rider, true); + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, rider); + + checkPlayableAbility("already used", 3, PhaseStep.POSTCOMBAT_MAIN, playerA, "Cast " + lifegain, false); + + setStrictChooseMode(true); + setStopAt(3, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, "Knight Token", 1); + assertExileCount(playerA, unicorn, 1); + } + } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/mkm/AssembleThePlayersTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/mkm/AssembleThePlayersTest.java new file mode 100644 index 00000000000..569eb5850d0 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/mkm/AssembleThePlayersTest.java @@ -0,0 +1,55 @@ +package org.mage.test.cards.single.mkm; + +import mage.constants.EmptyNames; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author xenohedron + */ +public class AssembleThePlayersTest extends CardTestPlayerBase { + + /* + Assemble the Players {1}{W} Enchantment + You may look at the top card of your library any time. + Once each turn, you may cast a creature spell with power 2 or less from the top of your library. + */ + private static final String assemble = "Assemble the Players"; + private static final String merfolk = "Coral Merfolk"; + private static final String drake = "Seacoast Drake"; + private static final String glacial = "Glacial Stalker"; + + @Test + public void testNormalAndMorph() { + skipInitShuffling(); + addCard(Zone.BATTLEFIELD, playerA, "Island", 3); + addCard(Zone.BATTLEFIELD, playerA, assemble); + addCard(Zone.LIBRARY, playerA, glacial); + addCard(Zone.LIBRARY, playerA, drake); + addCard(Zone.LIBRARY, playerA, merfolk); + + checkHandCardCount("not in hand", 1, PhaseStep.PRECOMBAT_MAIN, playerA, merfolk, 0); + checkHandCardCount("not in hand", 1, PhaseStep.PRECOMBAT_MAIN, playerA, drake, 0); + checkHandCardCount("not in hand", 1, PhaseStep.PRECOMBAT_MAIN, playerA, glacial, 0); + checkPlayableAbility("from library", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + merfolk, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, merfolk); + + checkPlayableAbility("once per turn", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + drake, false); + + checkHandCardCount("not in hand", 3, PhaseStep.PRECOMBAT_MAIN, playerA, merfolk, 0); + checkHandCardCount("in hand", 3, PhaseStep.PRECOMBAT_MAIN, playerA, drake, 1); + checkHandCardCount("not in hand", 3, PhaseStep.PRECOMBAT_MAIN, playerA, glacial, 0); + + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, glacial + " using Morph"); + + setStrictChooseMode(true); + setStopAt(3, PhaseStep.END_TURN); + execute(); + + assertPowerToughness(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 2, 2); + assertPowerToughness(playerA, merfolk, 2, 1); + + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/vow/CemeteryIlluminatorTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/vow/CemeteryIlluminatorTest.java new file mode 100644 index 00000000000..01107363d6e --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/vow/CemeteryIlluminatorTest.java @@ -0,0 +1,105 @@ +package org.mage.test.cards.single.vow; + +import mage.constants.EmptyNames; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author xenohedron + */ +public class CemeteryIlluminatorTest extends CardTestPlayerBase { + + /* + Cemetery Illuminator {1}{U}{U} + Creature — Spirit + Flying + Whenever Cemetery Illuminator enters the battlefield or attacks, exile a card from a graveyard. + You may look at the top card of your library any time. + Once each turn, you may cast a spell from the top of your library if it shares a card type with a card exiled with Cemetery Illuminator. + */ + private static final String ci = "Cemetery Illuminator"; + + private static final String unicorn = "Lonesome Unicorn"; // 4W 3/3 with adventure 2W 2/2 token + private static final String rider = "Rider in Need"; + private static final String knight = "Knight Token"; + private static final String sorcery = "Sign in Blood"; + private static final String creature = "Walking Corpse"; + + @Test + public void testCreature() { + skipInitShuffling(); + addCard(Zone.LIBRARY, playerA, unicorn); + addCard(Zone.GRAVEYARD, playerA, creature); + addCard(Zone.HAND, playerA, ci); + addCard(Zone.BATTLEFIELD, playerA, "Tundra", 8); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, ci); + setChoice(playerA, creature); // to exile on ETB + + checkPlayableAbility("creature", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Cast " + unicorn, true); + checkPlayableAbility("sorcery", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Cast " + rider, false); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, unicorn); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertExileCount(playerA, creature, 1); + assertPermanentCount(playerA, ci, 1); + assertPermanentCount(playerA, unicorn, 1); + } + + @Test + public void testSorcery() { + skipInitShuffling(); + addCard(Zone.LIBRARY, playerA, unicorn); + addCard(Zone.GRAVEYARD, playerA, sorcery); + addCard(Zone.HAND, playerA, ci); + addCard(Zone.BATTLEFIELD, playerA, "Tundra", 8); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, ci); + setChoice(playerA, sorcery); // to exile on ETB + + checkPlayableAbility("creature", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Cast " + unicorn, false); + checkPlayableAbility("sorcery", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Cast " + rider, true); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, rider); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertExileCount(playerA, sorcery, 1); + assertPermanentCount(playerA, ci, 1); + assertPermanentCount(playerA, knight, 1); + } + + @Test + public void testMorph() { + String whetwheel = "Whetwheel"; // Artifact + skipInitShuffling(); + addCard(Zone.LIBRARY, playerA, whetwheel); + addCard(Zone.GRAVEYARD, playerA, creature); + addCard(Zone.HAND, playerA, ci); + addCard(Zone.BATTLEFIELD, playerA, "Tundra", 8); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, ci); + setChoice(playerA, creature); // to exile on ETB + + // test framework can't pick this up, but it works correctly + // checkPlayableAbility("artifact", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Cast " + whetwheel, false); + checkPlayableAbility("creature", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Cast " + whetwheel + " using Morph", true); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, whetwheel + " using Morph"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertExileCount(playerA, creature, 1); + assertPermanentCount(playerA, ci, 1); + assertPermanentCount(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); + assertPermanentCount(playerA, whetwheel, 0); + } + +} diff --git a/Mage/src/main/java/mage/MageIdentifier.java b/Mage/src/main/java/mage/MageIdentifier.java index 63592d375c1..cc10c5eb0ca 100644 --- a/Mage/src/main/java/mage/MageIdentifier.java +++ b/Mage/src/main/java/mage/MageIdentifier.java @@ -20,12 +20,9 @@ public enum MageIdentifier { // "Once each turn, you may cast an instant or sorcery spell from the top of your library." // CastFromGraveyardOnceWatcher, - CemeteryIlluminatorWatcher, - GisaAndGeralfWatcher, - DanithaNewBenaliasLightWatcher, + OnceEachTurnCastWatcher, HaukensInsightWatcher, IntrepidPaleontologistWatcher, - KaradorGhostChieftainWatcher, KessDissidentMageWatcher, MuldrothaTheGravetideWatcher, ShareTheSpoilsWatcher, @@ -33,8 +30,6 @@ public enum MageIdentifier { GlimpseTheCosmosWatcher, SerraParagonWatcher, OneWithTheMultiverseWatcher("Without paying manacost"), - JohannApprenticeSorcererWatcher, - AssembleThePlayersWatcher, KaghaShadowArchdruidWatcher, CourtOfLocthwainWatcher("Without paying manacost"), LaraCroftTombRaiderWatcher, diff --git a/Mage/src/main/java/mage/abilities/common/CastFromGraveyardOnceStaticAbility.java b/Mage/src/main/java/mage/abilities/common/CastFromGraveyardOnceEachTurnAbility.java similarity index 53% rename from Mage/src/main/java/mage/abilities/common/CastFromGraveyardOnceStaticAbility.java rename to Mage/src/main/java/mage/abilities/common/CastFromGraveyardOnceEachTurnAbility.java index cef0a26ddc2..4405e6f0038 100644 --- a/Mage/src/main/java/mage/abilities/common/CastFromGraveyardOnceStaticAbility.java +++ b/Mage/src/main/java/mage/abilities/common/CastFromGraveyardOnceEachTurnAbility.java @@ -3,6 +3,7 @@ package mage.abilities.common; import mage.MageIdentifier; import mage.MageObjectReference; import mage.abilities.Ability; +import mage.abilities.SpellAbility; import mage.abilities.effects.AsThoughEffectImpl; import mage.cards.Card; import mage.constants.*; @@ -10,6 +11,7 @@ import mage.filter.FilterCard; import mage.game.Game; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; +import mage.players.Player; import mage.watchers.Watcher; import java.util.HashSet; @@ -18,26 +20,26 @@ import java.util.UUID; /** * Once during each of your turns, you may cast... from your graveyard - * + *

* See Lurrus of the Dream Den and Rivaz of the Claw * * @author weirddan455 */ -public class CastFromGraveyardOnceStaticAbility extends SimpleStaticAbility { +public class CastFromGraveyardOnceEachTurnAbility extends SimpleStaticAbility { - public CastFromGraveyardOnceStaticAbility(FilterCard filter, String text) { - super(new CastFromGraveyardOnceEffect(filter, text)); + public CastFromGraveyardOnceEachTurnAbility(FilterCard filter) { + super(new CastFromGraveyardOnceEffect(filter)); this.addWatcher(new CastFromGraveyardOnceWatcher()); this.setIdentifier(MageIdentifier.CastFromGraveyardOnceWatcher); } - private CastFromGraveyardOnceStaticAbility(final CastFromGraveyardOnceStaticAbility ability) { + private CastFromGraveyardOnceEachTurnAbility(final CastFromGraveyardOnceEachTurnAbility ability) { super(ability); } @Override - public CastFromGraveyardOnceStaticAbility copy() { - return new CastFromGraveyardOnceStaticAbility(this); + public CastFromGraveyardOnceEachTurnAbility copy() { + return new CastFromGraveyardOnceEachTurnAbility(this); } } @@ -45,10 +47,10 @@ class CastFromGraveyardOnceEffect extends AsThoughEffectImpl { private final FilterCard filter; - CastFromGraveyardOnceEffect(FilterCard filter, String text) { + CastFromGraveyardOnceEffect(FilterCard filter) { super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.Benefit); this.filter = filter; - this.staticText = text; + this.staticText = "Once during each of your turns, you may cast " + filter.getMessage() + " from your graveyard"; } private CastFromGraveyardOnceEffect(final CastFromGraveyardOnceEffect effect) { @@ -68,19 +70,30 @@ class CastFromGraveyardOnceEffect extends AsThoughEffectImpl { @Override public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) { - if (source.isControlledBy(affectedControllerId) - && Zone.GRAVEYARD.equals(game.getState().getZone(objectId)) - && game.isActivePlayer(affectedControllerId)) { - Card card = game.getCard(objectId); - Permanent sourceObject = source.getSourcePermanentIfItStillExists(game); - if (card != null && sourceObject != null - && card.isOwnedBy(affectedControllerId) - && card.getSpellAbility() != null - && card.getSpellAbility().spellCanBeActivatedRegularlyNow(affectedControllerId, game) - && filter.match(card, affectedControllerId, source, game)) { - CastFromGraveyardOnceWatcher watcher = game.getState().getWatcher(CastFromGraveyardOnceWatcher.class); - return watcher != null && watcher.abilityNotUsed(new MageObjectReference(sourceObject, game)); + throw new IllegalArgumentException("Wrong code usage: can't call applies method on empty affectedAbility"); + } + + @Override + public boolean applies(UUID objectId, Ability affectedAbility, Ability source, Game game, UUID playerId) { + Player controller = game.getPlayer(source.getControllerId()); + Permanent sourcePermanent = source.getSourcePermanentIfItStillExists(game); + CastFromGraveyardOnceWatcher watcher = game.getState().getWatcher(CastFromGraveyardOnceWatcher.class); + if (controller == null || sourcePermanent == null || watcher == null) { + return false; + } + if (game.isActivePlayer(playerId) // only during your turn + && source.isControlledBy(playerId) // only you may cast + && Zone.GRAVEYARD.equals(game.getState().getZone(objectId)) // from graveyard + && affectedAbility instanceof SpellAbility // characteristics to check + && watcher.abilityNotUsed(new MageObjectReference(sourcePermanent, game)) // once per turn + ) { + SpellAbility spellAbility = (SpellAbility) affectedAbility; + Card cardToCheck = spellAbility.getCharacteristics(game); + if (spellAbility.getManaCosts().isEmpty()) { + return false; } + return spellAbility.spellCanBeActivatedRegularlyNow(playerId, game) + && filter.match(cardToCheck, playerId, source, game); } return false; } diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/LookAtTopCardOfLibraryAnyTimeEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/LookAtTopCardOfLibraryAnyTimeEffect.java index 4cb4694c505..71d6c634dcd 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/LookAtTopCardOfLibraryAnyTimeEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/LookAtTopCardOfLibraryAnyTimeEffect.java @@ -1,63 +1,32 @@ package mage.abilities.effects.common.continuous; -import mage.MageObject; import mage.abilities.Ability; import mage.abilities.effects.ContinuousEffectImpl; import mage.cards.Card; -import mage.constants.*; +import mage.constants.Duration; +import mage.constants.Layer; +import mage.constants.Outcome; +import mage.constants.SubLayer; import mage.game.Game; import mage.players.Player; -import mage.util.CardUtil; - -import java.util.HashSet; -import java.util.Objects; -import java.util.Set; -import java.util.UUID; /** * @author TheElk801 */ public class LookAtTopCardOfLibraryAnyTimeEffect extends ContinuousEffectImpl { - private final TargetController targetLibrary; - public LookAtTopCardOfLibraryAnyTimeEffect() { - this(TargetController.YOU, Duration.WhileOnBattlefield); + this(Duration.WhileOnBattlefield); } - public LookAtTopCardOfLibraryAnyTimeEffect(TargetController targetLibrary, Duration duration) { + public LookAtTopCardOfLibraryAnyTimeEffect(Duration duration) { super(duration, Layer.PlayerEffects, SubLayer.NA, Outcome.Benefit); - this.targetLibrary = targetLibrary; - - String libInfo; - switch (this.targetLibrary) { - case YOU: - libInfo = "your library"; - break; - case OPPONENT: - libInfo = "opponents libraries"; - break; - case SOURCE_TARGETS: - libInfo = "target player's library"; - break; - default: - throw new IllegalArgumentException("Unknown target library type: " + targetLibrary); - } - StringBuilder sb = new StringBuilder(); - String durationString = duration.toString(); - if (durationString != null && !durationString.isEmpty()) { - sb.append(durationString); - sb.append(", "); - } - sb.append("you may look at the top card of "); - sb.append(libInfo); - sb.append(" any time"); - staticText = sb.toString(); + staticText = (duration.toString().isEmpty() ? "" : duration.toString() + ", ") + + "you may look at the top care of your library any time"; } protected LookAtTopCardOfLibraryAnyTimeEffect(final LookAtTopCardOfLibraryAnyTimeEffect effect) { super(effect); - this.targetLibrary = effect.targetLibrary; } @Override @@ -72,43 +41,11 @@ public class LookAtTopCardOfLibraryAnyTimeEffect extends ContinuousEffectImpl { if (!canLookAtNextTopLibraryCard(game)) { return false; } - MageObject obj = source.getSourceObject(game); - if (obj == null) { + Card topCard = controller.getLibrary().getFromTop(game); + if (topCard == null) { return false; } - - Set needPlayers = new HashSet<>(); - switch (this.targetLibrary) { - case YOU: { - needPlayers.add(source.getControllerId()); - break; - } - case OPPONENT: { - needPlayers.addAll(game.getOpponents(source.getControllerId())); - break; - } - case SOURCE_TARGETS: { - needPlayers.addAll(CardUtil.getAllSelectedTargets(source, game)); - break; - } - } - - Set needCards = new HashSet<>(); - needPlayers.stream() - .map(game::getPlayer) - .filter(Objects::nonNull) - .map(player -> player.getLibrary().getFromTop(game)) - .filter(Objects::nonNull) - .forEach(needCards::add); - if (needCards.isEmpty()) { - return false; - } - - // all fine, can show top card - needCards.forEach(topCard -> { - Player owner = game.getPlayer(topCard.getOwnerId()); - controller.lookAtCards(String.format("%s: top card of %s", obj.getName(), owner == null ? "error" : owner.getName()), topCard, game); - }); + controller.lookAtCards("Top card of your library", topCard, game); return true; } diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/LookAtTopCardOfLibraryAnyTimeTargetEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/LookAtTopCardOfLibraryAnyTimeTargetEffect.java deleted file mode 100644 index 4af3cf4d5d7..00000000000 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/LookAtTopCardOfLibraryAnyTimeTargetEffect.java +++ /dev/null @@ -1,23 +0,0 @@ -package mage.abilities.effects.common.continuous; - -import mage.constants.Duration; -import mage.constants.TargetController; - -/** - * @author JayDi85 - */ -public class LookAtTopCardOfLibraryAnyTimeTargetEffect extends LookAtTopCardOfLibraryAnyTimeEffect { - - public LookAtTopCardOfLibraryAnyTimeTargetEffect(Duration duration) { - super(TargetController.SOURCE_TARGETS, duration); - } - - private LookAtTopCardOfLibraryAnyTimeTargetEffect(final LookAtTopCardOfLibraryAnyTimeTargetEffect effect) { - super(effect); - } - - @Override - public LookAtTopCardOfLibraryAnyTimeTargetEffect copy() { - return new LookAtTopCardOfLibraryAnyTimeTargetEffect(this); - } -} diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/PlayFromTopOfLibraryEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/PlayFromTopOfLibraryEffect.java new file mode 100644 index 00000000000..e1c58df4cbc --- /dev/null +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/PlayFromTopOfLibraryEffect.java @@ -0,0 +1,99 @@ +package mage.abilities.effects.common.continuous; + +import mage.abilities.Ability; +import mage.abilities.SpellAbility; +import mage.abilities.effects.AsThoughEffectImpl; +import mage.cards.Card; +import mage.constants.AsThoughEffectType; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.filter.FilterCard; +import mage.game.Game; +import mage.players.Player; + +import java.util.Locale; +import java.util.UUID; + +/** + * @author nantuko, JayDi85, xenohedron + */ +public class PlayFromTopOfLibraryEffect extends AsThoughEffectImpl { + + private final FilterCard filter; + + private static final FilterCard defaultFilter = new FilterCard("play lands and cast spells"); + + /** + * You may play lands and cast spells from the top of your library + */ + public PlayFromTopOfLibraryEffect() { + this(defaultFilter); + } + + /** + * You may [play lands and/or cast spells, according to filter] from the top of your library + */ + public PlayFromTopOfLibraryEffect(FilterCard filter) { + super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.Benefit); + this.filter = filter; + this.staticText = "you may " + filter.getMessage() + " from the top of your library"; + + // verify check: this ability is to allow playing lands or casting spells, not playing a "card" + if (filter.getMessage().toLowerCase(Locale.ENGLISH).contains("card")) { + throw new IllegalArgumentException("Wrong code usage or wrong filter text: PlayTheTopCardEffect"); + } + } + + protected PlayFromTopOfLibraryEffect(final PlayFromTopOfLibraryEffect effect) { + super(effect); + this.filter = effect.filter; + } + + @Override + public boolean apply(Game game, Ability source) { + return true; + } + + @Override + public PlayFromTopOfLibraryEffect copy() { + return new PlayFromTopOfLibraryEffect(this); + } + + @Override + public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) { + throw new IllegalArgumentException("Wrong code usage: can't call applies method on empty affectedAbility"); + } + + @Override + public boolean applies(UUID objectId, Ability affectedAbility, Ability source, Game game, UUID playerId) { + // can play lands/spells (must check specific part and allows specific part) + + Card cardToCheck = game.getCard(objectId); // maybe this should be removed and only check SpellAbility characteristics + if (cardToCheck == null) { + return false; + } + if (affectedAbility instanceof SpellAbility) { + SpellAbility spell = (SpellAbility) affectedAbility; + cardToCheck = spell.getCharacteristics(game); + if (spell.getManaCosts().isEmpty()){ + return false; + } + } + // only permits you to cast + if (!playerId.equals(source.getControllerId())) { + return false; + } + Player cardOwner = game.getPlayer(cardToCheck.getOwnerId()); + Player controller = game.getPlayer(source.getControllerId()); + if (cardOwner == null || controller == null) { + return false; + } + // main card of spell must be on top of your library + Card topCard = controller.getLibrary().getFromTop(game); + if (topCard == null || !topCard.getId().equals(cardToCheck.getMainCard().getId())) { + return false; + } + // spell characteristics must match filter + return filter.match(cardToCheck, playerId, source, game); + } +} diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/PlayTheTopCardEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/PlayTheTopCardEffect.java deleted file mode 100644 index 3eb76dd81b7..00000000000 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/PlayTheTopCardEffect.java +++ /dev/null @@ -1,170 +0,0 @@ -package mage.abilities.effects.common.continuous; - -import mage.abilities.Ability; -import mage.abilities.SpellAbility; -import mage.abilities.effects.AsThoughEffectImpl; -import mage.cards.Card; -import mage.constants.AsThoughEffectType; -import mage.constants.Duration; -import mage.constants.Outcome; -import mage.constants.TargetController; -import mage.filter.FilterCard; -import mage.game.Game; -import mage.players.Player; -import mage.util.CardUtil; - -import java.util.Locale; -import java.util.Objects; -import java.util.UUID; - -/** - * @author nantuko, JayDi85 - */ -public class PlayTheTopCardEffect extends AsThoughEffectImpl { - - private final FilterCard filter; - private final TargetController targetLibrary; - - // can play card or can play lands/cast spells, see two modes below - private final boolean canPlayCardOnly; - - - /** - * Support targets, use TargetController.SOURCE_TARGETS - */ - public PlayTheTopCardEffect() { - this(TargetController.YOU); - } - - public PlayTheTopCardEffect(TargetController targetLibrary) { - this(targetLibrary, new FilterCard("play lands and cast spells"), false); - } - - public PlayTheTopCardEffect(TargetController targetLibrary, FilterCard filter, boolean canPlayCardOnly) { - super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.Benefit); - this.filter = filter; - this.targetLibrary = targetLibrary; - this.canPlayCardOnly = canPlayCardOnly; - - String libInfo; - switch (this.targetLibrary) { - case YOU: - libInfo = "your library"; - break; - case OPPONENT: - libInfo = "opponents libraries"; - break; - case SOURCE_TARGETS: - libInfo = "target player's library"; - break; - default: - throw new IllegalArgumentException("Unknown target library type: " + targetLibrary); - } - this.staticText = "you may " + filter.getMessage() + " from the top of " + libInfo; - - // verify check: if you see "card" text in the rules then use card mode - // (there aren't any real cards after oracle update, but can be added in the future) - if (this.canPlayCardOnly != filter.getMessage().toLowerCase(Locale.ENGLISH).contains("card")) { - throw new IllegalArgumentException("Wrong usage of card mode settings"); - } - } - - protected PlayTheTopCardEffect(final PlayTheTopCardEffect effect) { - super(effect); - this.filter = effect.filter; - this.targetLibrary = effect.targetLibrary; - this.canPlayCardOnly = effect.canPlayCardOnly; - } - - @Override - public boolean apply(Game game, Ability source) { - return true; - } - - @Override - public PlayTheTopCardEffect copy() { - return new PlayTheTopCardEffect(this); - } - - @Override - public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) { - throw new IllegalArgumentException("Wrong code usage: can't call applies method on empty affectedAbility"); - } - @Override - public boolean applies(UUID objectId, Ability affectedAbility, Ability source, Game game, UUID playerId) { - // main card and all parts are checks in different calls. - // two modes: - // * can play cards (must check main card and allows any parts) - // * can play lands/spells (must check specific part and allows specific part) - - // current card's part - Card cardToCheck = game.getCard(objectId); - if (cardToCheck == null) { - return false; - } - - if (this.canPlayCardOnly) { - // check whole card instead part - cardToCheck = cardToCheck.getMainCard(); - } else if (affectedAbility instanceof SpellAbility) { - SpellAbility spell = (SpellAbility) affectedAbility; - cardToCheck = spell.getCharacteristics(game); - if (spell.getManaCosts().isEmpty()){ - return false; - } - } - // must be you - if (!playerId.equals(source.getControllerId())) { - return false; - } - - Player cardOwner = game.getPlayer(cardToCheck.getOwnerId()); - Player controller = game.getPlayer(source.getControllerId()); - if (cardOwner == null || controller == null) { - return false; - } - - // must be your or opponents library - switch (this.targetLibrary) { - case YOU: { - Card topCard = controller.getLibrary().getFromTop(game); - if (topCard == null || !topCard.getId().equals(cardToCheck.getMainCard().getId())) { - return false; - } - break; - } - - case OPPONENT: { - if (!game.getOpponents(controller.getId()).contains(cardOwner.getId())) { - return false; - } - Card topCard = cardOwner.getLibrary().getFromTop(game); - if (topCard == null || !topCard.getId().equals(cardToCheck.getMainCard().getId())) { - return false; - } - break; - } - - case SOURCE_TARGETS: { - UUID needCardId = cardToCheck.getMainCard().getId(); - if (CardUtil.getAllSelectedTargets(source, game).stream() - .map(game::getPlayer) - .filter(Objects::nonNull) - .noneMatch(player -> { - Card topCard = player.getLibrary().getFromTop(game); - return topCard != null && topCard.getId().equals(needCardId); - })) { - return false; - } - break; - } - - default: { - return false; - } - } - - // must be correct card - return filter.match(cardToCheck, playerId, source, game); - } -} diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/PlayTheTopCardTargetEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/PlayTheTopCardTargetEffect.java deleted file mode 100644 index 642083049a7..00000000000 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/PlayTheTopCardTargetEffect.java +++ /dev/null @@ -1,22 +0,0 @@ -package mage.abilities.effects.common.continuous; - -import mage.constants.TargetController; - -/** - * @author JayDi85 - */ -public class PlayTheTopCardTargetEffect extends PlayTheTopCardEffect { - - public PlayTheTopCardTargetEffect() { - super(TargetController.SOURCE_TARGETS); - } - - protected PlayTheTopCardTargetEffect(final PlayTheTopCardTargetEffect effect) { - super(effect); - } - - @Override - public PlayTheTopCardTargetEffect copy() { - return new PlayTheTopCardTargetEffect(this); - } -} diff --git a/Mage/src/main/java/mage/watchers/common/OnceEachTurnCastWatcher.java b/Mage/src/main/java/mage/watchers/common/OnceEachTurnCastWatcher.java new file mode 100644 index 00000000000..2ac2b578add --- /dev/null +++ b/Mage/src/main/java/mage/watchers/common/OnceEachTurnCastWatcher.java @@ -0,0 +1,77 @@ +package mage.watchers.common; + +import mage.MageIdentifier; +import mage.MageObjectReference; +import mage.abilities.Ability; +import mage.abilities.hint.Hint; +import mage.constants.WatcherScope; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.players.Player; +import mage.watchers.Watcher; + +import java.util.*; + +/** + * @author xenohedron + */ +public class OnceEachTurnCastWatcher extends Watcher { + + private final Map> usedFrom = new HashMap<>(); + + /** + * For abilities that permit the casting of a spell from not own hand zone once each turn (per player) + */ + public OnceEachTurnCastWatcher() { + super(WatcherScope.GAME); + } + + @Override + public void watch(GameEvent event, Game game) { + if (event.getType() == GameEvent.EventType.SPELL_CAST + && event.getPlayerId() != null + && event.hasApprovingIdentifier(MageIdentifier.OnceEachTurnCastWatcher)) { + usedFrom.computeIfAbsent(event.getPlayerId(), k -> new HashSet<>()) + .add(event.getAdditionalReference().getApprovingMageObjectReference()); + } + } + + @Override + public void reset() { + super.reset(); + usedFrom.clear(); + } + + public boolean isAbilityUsed(UUID playerId, MageObjectReference mor) { + return usedFrom.getOrDefault(playerId, Collections.emptySet()).contains(mor); + } + + public static Hint getHint() { + return OnceEachTurnCastHint.instance; + } + +} + +enum OnceEachTurnCastHint implements Hint { + instance; + + @Override + public String getText(Game game, Ability ability) { + OnceEachTurnCastWatcher watcher = game.getState().getWatcher(OnceEachTurnCastWatcher.class); + if (watcher != null) { + boolean used = watcher.isAbilityUsed(ability.getControllerId(), new MageObjectReference(ability.getSourceId(), game)); + if (used) { + Player player = game.getPlayer(ability.getControllerId()); + if (player != null) { + return "A spell has been cast by " + player.getLogName() + " with {this} this turn."; + } + } + } + return ""; + } + + @Override + public OnceEachTurnCastHint copy() { + return this; + } +}