diff --git a/Mage.Client/src/main/java/mage/client/cards/Cards.java b/Mage.Client/src/main/java/mage/client/cards/Cards.java index d79ed7c1e9..cff58b5614 100644 --- a/Mage.Client/src/main/java/mage/client/cards/Cards.java +++ b/Mage.Client/src/main/java/mage/client/cards/Cards.java @@ -183,6 +183,7 @@ if (card instanceof StackAbilityView) { // replace ability by original card CardView tmp = ((StackAbilityView) card).getSourceCard(); + // sync settings tmp.overrideRules(card.getRules()); tmp.setChoosable(card.isChoosable()); tmp.setPlayableStats(card.getPlayableStats().copy()); @@ -191,6 +192,9 @@ tmp.overrideTargets(card.getTargets()); tmp.overrideId(card.getId()); tmp.setAbilityType(card.getAbilityType()); + // sync card icons + tmp.getCardIcons().clear(); + tmp.getCardIcons().addAll(card.getCardIcons()); card = tmp; } else { card.setAbilityType(null); diff --git a/Mage.Client/src/main/resources/card/icons/original/bootstrap/reply-fill.svg b/Mage.Client/src/main/resources/card/icons/original/bootstrap/reply-fill.svg new file mode 100644 index 0000000000..b5a87228d8 --- /dev/null +++ b/Mage.Client/src/main/resources/card/icons/original/bootstrap/reply-fill.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/Mage.Client/src/main/resources/card/icons/prepared/hexagon-fill.svg b/Mage.Client/src/main/resources/card/icons/prepared/hexagon-fill.svg new file mode 100644 index 0000000000..2f310a22d6 --- /dev/null +++ b/Mage.Client/src/main/resources/card/icons/prepared/hexagon-fill.svg @@ -0,0 +1,7 @@ + + + + + diff --git a/Mage.Client/src/main/resources/card/icons/prepared/reply-fill.svg b/Mage.Client/src/main/resources/card/icons/prepared/reply-fill.svg new file mode 100644 index 0000000000..daa1b3d23e --- /dev/null +++ b/Mage.Client/src/main/resources/card/icons/prepared/reply-fill.svg @@ -0,0 +1,7 @@ + + + + + diff --git a/Mage.Common/src/main/java/mage/view/CardView.java b/Mage.Common/src/main/java/mage/view/CardView.java index 79afbb0a8a..dca13d2c2c 100644 --- a/Mage.Common/src/main/java/mage/view/CardView.java +++ b/Mage.Common/src/main/java/mage/view/CardView.java @@ -7,9 +7,12 @@ import mage.abilities.Abilities; import mage.abilities.Ability; import mage.abilities.Mode; import mage.abilities.SpellAbility; +import mage.abilities.dynamicvalue.common.ManacostVariableValue; import mage.abilities.effects.Effect; import mage.abilities.effects.Effects; import mage.abilities.icon.CardIcon; +import mage.abilities.icon.other.FaceDownCardIcon; +import mage.abilities.icon.other.VariableCostCardIcon; import mage.abilities.keyword.AftermathAbility; import mage.cards.*; import mage.cards.mock.MockCard; @@ -368,7 +371,7 @@ public class CardView extends SimpleCardView { this.manaCostRightStr = String.join("", mainCard.getRightHalfCard().getManaCostSymbols()); } else if (card instanceof AdventureCard) { AdventureCard adventureCard = ((AdventureCard) card); - AdventureCardSpell adventureCardSpell = ((AdventureCardSpell) adventureCard.getSpellCard()); + AdventureCardSpell adventureCardSpell = adventureCard.getSpellCard(); fullCardName = adventureCard.getName() + MockCard.ADVENTURE_NAME_SEPARATOR + adventureCardSpell.getName(); this.manaCostLeftStr = String.join("", adventureCardSpell.getManaCostSymbols()); this.manaCostRightStr = String.join("", adventureCard.getManaCostSymbols()); @@ -415,9 +418,14 @@ public class CardView extends SimpleCardView { } // card icons for permanents on battlefield + // abilities permanent.getAbilities(game).forEach(ability -> { - this.cardIcons.addAll(ability.getIcons()); + this.cardIcons.addAll(ability.getIcons(game)); }); + // face down + if (permanent.isFaceDown(game)) { + this.cardIcons.add(FaceDownCardIcon.instance); + } } else { if (card.isCopy()) { this.mageObjectType = MageObjectType.COPY_CARD; @@ -432,6 +440,25 @@ public class CardView extends SimpleCardView { } } } + + // card icons for any permanents and cards + if (game != null) { + // x cost + Zone cardZone = game.getState().getZone(card.getId()); + if (card.getManaCost().containsX() + && (cardZone.match(Zone.BATTLEFIELD) || cardZone.match(Zone.STACK))) { + int costX; + if (card instanceof Permanent) { + // permanent on battlefield + costX = ManacostVariableValue.ETB.calculate(game, card.getSpellAbility(), null); + } else { + // other like Stack + costX = ManacostVariableValue.REGULAR.calculate(game, card.getSpellAbility(), null); + } + this.cardIcons.add(new VariableCostCardIcon(costX)); + } + } + this.power = Integer.toString(card.getPower().getValue()); this.toughness = Integer.toString(card.getToughness().getValue()); this.cardTypes = card.getCardType(game); diff --git a/Mage.Common/src/main/java/mage/view/StackAbilityView.java b/Mage.Common/src/main/java/mage/view/StackAbilityView.java index c9de06d7f6..74142d11d9 100644 --- a/Mage.Common/src/main/java/mage/view/StackAbilityView.java +++ b/Mage.Common/src/main/java/mage/view/StackAbilityView.java @@ -3,9 +3,11 @@ package mage.view; import mage.MageObject; import mage.abilities.Mode; import mage.abilities.Modes; +import mage.abilities.dynamicvalue.common.ManacostVariableValue; import mage.abilities.effects.Effect; import mage.abilities.hint.Hint; import mage.abilities.hint.HintUtils; +import mage.abilities.icon.other.VariableCostCardIcon; import mage.cards.Card; import mage.constants.AbilityType; import mage.constants.CardType; @@ -28,7 +30,7 @@ public class StackAbilityView extends CardView { private static final long serialVersionUID = 1L; // in GUI: that's view will be replaced by sourceCard, so don't forget to sync settings like - // selectable, chooseable, etc. Search by getSourceCard + // selectable, chooseable, card icons etc. Search by getSourceCard private final CardView sourceCard; public StackAbilityView(Game game, StackAbility ability, String sourceName, CardView sourceCard) { @@ -73,6 +75,13 @@ public class StackAbilityView extends CardView { this.counters = sourceCard.getCounters(); updateTargets(game, ability); + + // card icons (warning, it must be synced in gui dialogs with replaced card, see comments at the start of the file) + // cost x + if (ability.getManaCostsToPay().containsX()) { + int costX = ManacostVariableValue.REGULAR.calculate(game, ability, null); + this.cardIcons.add(new VariableCostCardIcon(costX)); + } } private void updateTargets(Game game, StackAbility ability) { @@ -108,7 +117,7 @@ public class StackAbilityView extends CardView { } } if (!names.isEmpty()) { - getRules().add("Related objects: " + names.toString() + ""); + getRules().add("Related objects: " + names + ""); } // show for modal ability, which mode was choosen diff --git a/Mage.Server/src/main/java/mage/server/game/GameSessionPlayer.java b/Mage.Server/src/main/java/mage/server/game/GameSessionPlayer.java index 0939c26eb6..dfa6bf947f 100644 --- a/Mage.Server/src/main/java/mage/server/game/GameSessionPlayer.java +++ b/Mage.Server/src/main/java/mage/server/game/GameSessionPlayer.java @@ -11,8 +11,8 @@ import mage.interfaces.callback.ClientCallback; import mage.interfaces.callback.ClientCallbackMethod; import mage.players.Player; import mage.server.User; -import mage.server.managers.UserManager; import mage.server.managers.ManagerFactory; +import mage.server.managers.UserManager; import mage.view.*; import org.apache.log4j.Logger; @@ -195,6 +195,18 @@ public class GameSessionPlayer extends GameSessionWatcher { @Override public GameView getGameView() { + return prepareGameView(game, playerId, userId); + } + + /** + * Prepare client-server data. Can be used in real games or in unit tests + * + * @param game + * @param playerId + * @param userId can be null for tests + * @return + */ + public static GameView prepareGameView(Game game, UUID playerId, UUID userId) { Player player = game.getPlayer(playerId); GameView gameView = new GameView(game.getState(), game, playerId, null); gameView.setHand(new CardsView(game, player.getHand().getCards(game))); @@ -202,8 +214,8 @@ public class GameSessionPlayer extends GameSessionWatcher { gameView.setCanPlayObjects(player.getPlayableObjects(game, Zone.ALL)); } - processControlledPlayers(player, gameView); - processWatchedHands(userId, gameView); + processControlledPlayers(game, player, gameView); + processWatchedHands(game, userId, gameView); //TODO: should player who controls another player's turn be able to look at all these cards? List list = new ArrayList<>(); @@ -215,7 +227,7 @@ public class GameSessionPlayer extends GameSessionWatcher { return gameView; } - private void processControlledPlayers(Player player, GameView gameView) { + private static void processControlledPlayers(Game game, Player player, GameView gameView) { if (!player.getPlayersUnderYourControl().isEmpty()) { Map handCards = new HashMap<>(); for (UUID controlledPlayerId : player.getPlayersUnderYourControl()) { @@ -258,7 +270,7 @@ public class GameSessionPlayer extends GameSessionWatcher { if (ex.getCause() != null) { logger.debug("- Cause: " + (ex.getCause().getMessage() == null ? "null" : ex.getCause().getMessage()), ex); } else { - logger.debug("- ex: " + ex.toString(), ex); + logger.debug("- ex: " + ex, ex); } } else { logger.fatal("Game session game quit exception - null gameId:" + game.getId() + " playerId: " + playerId); diff --git a/Mage.Server/src/main/java/mage/server/game/GameSessionWatcher.java b/Mage.Server/src/main/java/mage/server/game/GameSessionWatcher.java index 6aafddbf67..4bcd69d57c 100644 --- a/Mage.Server/src/main/java/mage/server/game/GameSessionWatcher.java +++ b/Mage.Server/src/main/java/mage/server/game/GameSessionWatcher.java @@ -99,11 +99,11 @@ public class GameSessionWatcher { public GameView getGameView() { GameView gameView = new GameView(game.getState(), game, null, userId); - processWatchedHands(userId, gameView); + processWatchedHands(game, userId, gameView); return gameView; } - protected void processWatchedHands(UUID userId, GameView gameView) { + protected static void processWatchedHands(Game game, UUID userId, GameView gameView) { Map handCards = new HashMap<>(); for (Player player : game.getPlayers().values()) { if (player.hasUserPermissionToSeeHand(userId)) { diff --git a/Mage.Server/src/main/java/mage/server/util/SystemUtil.java b/Mage.Server/src/main/java/mage/server/util/SystemUtil.java index 1f8d5988b8..38cef7431f 100644 --- a/Mage.Server/src/main/java/mage/server/util/SystemUtil.java +++ b/Mage.Server/src/main/java/mage/server/util/SystemUtil.java @@ -23,6 +23,7 @@ import mage.game.GameCommanderImpl; import mage.game.command.CommandObject; import mage.game.command.Plane; import mage.game.permanent.Permanent; +import mage.game.permanent.token.Token; import mage.players.Player; import mage.util.CardUtil; import mage.util.RandomUtil; @@ -267,8 +268,8 @@ public final class SystemUtil { public static void addCardsForTesting(Game game, String fileSource, Player feedbackPlayer) { // fake test ability for triggers and events - Ability fakeSourceAbility = new SimpleStaticAbility(Zone.OUTSIDE, new InfoEffect("adding testing cards")); - fakeSourceAbility.setControllerId(feedbackPlayer.getId()); + Ability fakeSourceAbilityTemplate = new SimpleStaticAbility(Zone.OUTSIDE, new InfoEffect("adding testing cards")); + fakeSourceAbilityTemplate.setControllerId(feedbackPlayer.getId()); try { String fileName = fileSource; @@ -496,9 +497,12 @@ public final class SystemUtil { // eg: token:Human:HippoToken:1 Class c = Class.forName("mage.game.permanent.token." + command.cardName); Constructor cons = c.getConstructor(); - Object token = cons.newInstance(); - if (token instanceof mage.game.permanent.token.Token) { - ((mage.game.permanent.token.Token) token).putOntoBattlefield(command.Amount, game, fakeSourceAbility, player.getId(), false, false); + Object obj = cons.newInstance(); + if (obj instanceof Token) { + Token token = (Token) obj; + Ability fakeSourceAbility = fakeSourceAbilityTemplate.copy(); + fakeSourceAbility.setSourceId(token.getId()); + token.putOntoBattlefield(command.Amount, game, fakeSourceAbility, player.getId(), false, false); continue; } } else if ("emblem".equalsIgnoreCase(command.zone)) { @@ -518,6 +522,8 @@ public final class SystemUtil { } else if ("loyalty".equalsIgnoreCase(command.zone)) { for (Permanent perm : game.getBattlefield().getAllActivePermanents(player.getId())) { if (perm.getName().equals(command.cardName) && perm.getCardType(game).contains(CardType.PLANESWALKER)) { + Ability fakeSourceAbility = fakeSourceAbilityTemplate.copy(); + fakeSourceAbility.setSourceId(perm.getId()); perm.addCounters(CounterType.LOYALTY.createInstance(command.Amount), fakeSourceAbility.getControllerId(), fakeSourceAbility, game); } } @@ -541,6 +547,8 @@ public final class SystemUtil { // move card from exile to stack for (Card card : cardsToLoad) { + Ability fakeSourceAbility = fakeSourceAbilityTemplate.copy(); + fakeSourceAbility.setSourceId(card.getId()); putCardToZone(fakeSourceAbility, game, player, card, Zone.STACK); } @@ -616,6 +624,8 @@ public final class SystemUtil { } else { // as other card for (Card card : cardsToLoad) { + Ability fakeSourceAbility = fakeSourceAbilityTemplate.copy(); + fakeSourceAbility.setSourceId(card.getId()); putCardToZone(fakeSourceAbility, game, player, card, gameZone); } } diff --git a/Mage.Sets/src/mage/cards/a/AdmiralsOrder.java b/Mage.Sets/src/mage/cards/a/AdmiralsOrder.java index b0331a3e62..225eea9cbb 100644 --- a/Mage.Sets/src/mage/cards/a/AdmiralsOrder.java +++ b/Mage.Sets/src/mage/cards/a/AdmiralsOrder.java @@ -24,12 +24,11 @@ public final class AdmiralsOrder extends CardImpl { // Raid - If you attacked with a creature this turn, you may pay {U} rather than pay this spell's mana cost. this.addAbility(new AlternativeCostSourceAbility(new ManaCostsImpl("{U}"), RaidCondition.instance, - "

Raid — If you attacked with a creature this turn, you may pay {U} rather than pay this spell's mana cost"), + "
Raid — If you attacked this turn, you may pay {U} rather than pay this spell's mana cost"), new PlayerAttackedWatcher()); // Counter target spell. this.getSpellAbility().addEffect(new CounterTargetEffect()); this.getSpellAbility().addTarget(new TargetSpell()); - this.getSpellAbility().setAbilityWord(AbilityWord.RAID); this.getSpellAbility().addHint(RaidHint.instance); } diff --git a/Mage.Sets/src/mage/cards/a/AidFromTheCowl.java b/Mage.Sets/src/mage/cards/a/AidFromTheCowl.java index d22ba61bc2..adfcd640ca 100644 --- a/Mage.Sets/src/mage/cards/a/AidFromTheCowl.java +++ b/Mage.Sets/src/mage/cards/a/AidFromTheCowl.java @@ -25,7 +25,7 @@ import mage.watchers.common.RevoltWatcher; public final class AidFromTheCowl extends CardImpl { private static final String ruleText = "Revolt — At the beginning of your end step, if a permanent you controlled left the battlefield this turn, " - + "you may reveal the top card of your library. If it's a permanent card, you may put it onto the battlefield. Otherwise, put it on the bottom of your library."; + + "reveal the top card of your library. If it's a permanent card, you may put it onto the battlefield. Otherwise, put it on the bottom of your library."; public AidFromTheCowl(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{3}{G}{G}"); diff --git a/Mage.Sets/src/mage/cards/a/AirCultElemental.java b/Mage.Sets/src/mage/cards/a/AirCultElemental.java index 210a6fa390..173b62f090 100644 --- a/Mage.Sets/src/mage/cards/a/AirCultElemental.java +++ b/Mage.Sets/src/mage/cards/a/AirCultElemental.java @@ -20,7 +20,7 @@ import mage.target.common.TargetCreaturePermanent; */ public final class AirCultElemental extends CardImpl { - private static final FilterCreaturePermanent filter = new FilterCreaturePermanent("other creature"); + private static final FilterCreaturePermanent filter = new FilterCreaturePermanent("other target creature"); static { filter.add(AnotherPredicate.instance); diff --git a/Mage.Sets/src/mage/cards/a/AkoumFirebird.java b/Mage.Sets/src/mage/cards/a/AkoumFirebird.java index f233a9f061..c64e50ab76 100644 --- a/Mage.Sets/src/mage/cards/a/AkoumFirebird.java +++ b/Mage.Sets/src/mage/cards/a/AkoumFirebird.java @@ -44,7 +44,7 @@ public final class AkoumFirebird extends CardImpl { // Landfall-Whenever a land enters the battlefield under your control, you may pay {4}{R}{R}. // If you do, return Akoum Firebird from your graveyard to the battlefield. this.addAbility(new AkoumFirebirdLandfallAbility(new DoIfCostPaid( - new ReturnSourceFromGraveyardToBattlefieldEffect(), new ManaCostsImpl("{4}{R}{R}")), false)); + new ReturnSourceFromGraveyardToBattlefieldEffect(false, false), new ManaCostsImpl("{4}{R}{R}")), false)); } private AkoumFirebird(final AkoumFirebird card) { diff --git a/Mage.Sets/src/mage/cards/a/AngelicArbiter.java b/Mage.Sets/src/mage/cards/a/AngelicArbiter.java index 81f26520e8..64b74a7824 100644 --- a/Mage.Sets/src/mage/cards/a/AngelicArbiter.java +++ b/Mage.Sets/src/mage/cards/a/AngelicArbiter.java @@ -86,7 +86,7 @@ class AngelicArbiterEffect2 extends ContinuousRuleModifyingEffectImpl { public AngelicArbiterEffect2() { super(Duration.WhileOnBattlefield, Outcome.Benefit); - staticText = "Each opponent who attacked with a creature this turn can't cast spells"; + staticText = "Each opponent who attacked this turn can't cast spells"; } public AngelicArbiterEffect2(final AngelicArbiterEffect2 effect) { diff --git a/Mage.Sets/src/mage/cards/a/ArrowStorm.java b/Mage.Sets/src/mage/cards/a/ArrowStorm.java index ad7273389b..7d68830e13 100644 --- a/Mage.Sets/src/mage/cards/a/ArrowStorm.java +++ b/Mage.Sets/src/mage/cards/a/ArrowStorm.java @@ -32,9 +32,8 @@ public final class ArrowStorm extends CardImpl { this.getSpellAbility().addEffect(new ConditionalOneShotEffect( new DamageTargetEffect(5, false), RaidCondition.instance, - "

Raid — If you attacked with a creature this turn, instead {this} deals 5 damage to that permanent or player and the damage can't be prevented")); + "

Raid — If you attacked this turn, instead {this} deals 5 damage to that permanent or player and the damage can't be prevented")); this.getSpellAbility().addWatcher(new PlayerAttackedWatcher()); - this.getSpellAbility().setAbilityWord(AbilityWord.RAID); this.getSpellAbility().addHint(RaidHint.instance); } diff --git a/Mage.Sets/src/mage/cards/a/AuriokSunchaser.java b/Mage.Sets/src/mage/cards/a/AuriokSunchaser.java index 1f71e3f393..1a483827ca 100644 --- a/Mage.Sets/src/mage/cards/a/AuriokSunchaser.java +++ b/Mage.Sets/src/mage/cards/a/AuriokSunchaser.java @@ -1,17 +1,20 @@ package mage.cards.a; import mage.MageInt; +import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.condition.common.MetalcraftCondition; import mage.abilities.decorator.ConditionalContinuousEffect; -import mage.abilities.effects.ContinuousEffect; import mage.abilities.effects.common.continuous.BoostSourceEffect; import mage.abilities.effects.common.continuous.GainAbilitySourceEffect; import mage.abilities.hint.common.MetalcraftHint; import mage.abilities.keyword.FlyingAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.*; +import mage.constants.AbilityWord; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; import java.util.UUID; @@ -20,9 +23,6 @@ import java.util.UUID; */ public final class AuriokSunchaser extends CardImpl { - protected static String effect1Text = "Metalcraft — As long as you control three or more artifacts, Auriok Sunchaser gets +2/+2"; - protected static String effect2Text = "Metalcraft — As long as you control three or more artifacts, Auriok Sunchaser has flying"; - public AuriokSunchaser(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{W}"); this.subtype.add(SubType.HUMAN); @@ -31,19 +31,14 @@ public final class AuriokSunchaser extends CardImpl { this.power = new MageInt(1); this.toughness = new MageInt(1); - ContinuousEffect effect1 = new BoostSourceEffect(2, 2, Duration.WhileOnBattlefield); - this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, - new ConditionalContinuousEffect(effect1, MetalcraftCondition.instance, effect1Text)) - .setAbilityWord(AbilityWord.METALCRAFT) - .addHint(MetalcraftHint.instance) - ); - - ContinuousEffect effect2 = new GainAbilitySourceEffect(FlyingAbility.getInstance(), Duration.WhileOnBattlefield); - this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, - new ConditionalContinuousEffect(effect2, MetalcraftCondition.instance, effect2Text)) - .setAbilityWord(AbilityWord.METALCRAFT) - .addHint(MetalcraftHint.instance) - ); + Ability ability = new SimpleStaticAbility(new ConditionalContinuousEffect( + new BoostSourceEffect(2, 2, Duration.WhileOnBattlefield), + MetalcraftCondition.instance, "as long as you control three or more artifacts, {this} gets +2/+2" + )); + ability.addEffect(new ConditionalContinuousEffect(new GainAbilitySourceEffect( + FlyingAbility.getInstance(), Duration.WhileOnBattlefield + ), MetalcraftCondition.instance, "and has flying")); + this.addAbility(ability.setAbilityWord(AbilityWord.METALCRAFT).addHint(MetalcraftHint.instance)); } private AuriokSunchaser(final AuriokSunchaser card) { diff --git a/Mage.Sets/src/mage/cards/b/BalothWoodcrasher.java b/Mage.Sets/src/mage/cards/b/BalothWoodcrasher.java index 6c94ac1628..1c1879adf8 100644 --- a/Mage.Sets/src/mage/cards/b/BalothWoodcrasher.java +++ b/Mage.Sets/src/mage/cards/b/BalothWoodcrasher.java @@ -26,8 +26,8 @@ public final class BalothWoodcrasher extends CardImpl { this.power = new MageInt(4); this.toughness = new MageInt(4); - LandfallAbility ability = new LandfallAbility(new BoostSourceEffect(4, 4, Duration.EndOfTurn), false); - ability.addEffect(new GainAbilitySourceEffect(TrampleAbility.getInstance(), Duration.EndOfTurn)); + LandfallAbility ability = new LandfallAbility(new BoostSourceEffect(4, 4, Duration.EndOfTurn).setText("{this} gets +4/+4"), false); + ability.addEffect(new GainAbilitySourceEffect(TrampleAbility.getInstance(), Duration.EndOfTurn).setText("and gains trample until end of turn")); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/b/BardClass.java b/Mage.Sets/src/mage/cards/b/BardClass.java index 1ec0ae71e2..12329fc085 100644 --- a/Mage.Sets/src/mage/cards/b/BardClass.java +++ b/Mage.Sets/src/mage/cards/b/BardClass.java @@ -52,7 +52,7 @@ public final class BardClass extends CardImpl { // Legendary spells you cast cost {R}{G} less to cast. This effect reduces only the amount of colored mana you pay. this.addAbility(new SimpleStaticAbility(new GainClassAbilitySourceEffect( - new SpellsCostReductionControllerEffect(filter, new ManaCostsImpl<>("{W}{B}")), 2 + new SpellsCostReductionControllerEffect(filter, new ManaCostsImpl<>("{R}{G}")), 2 ))); // {3}{R}{G}: Level 3 diff --git a/Mage.Sets/src/mage/cards/b/BellowingSaddlebrute.java b/Mage.Sets/src/mage/cards/b/BellowingSaddlebrute.java index bdd745139e..83c8dfd375 100644 --- a/Mage.Sets/src/mage/cards/b/BellowingSaddlebrute.java +++ b/Mage.Sets/src/mage/cards/b/BellowingSaddlebrute.java @@ -32,7 +32,7 @@ public final class BellowingSaddlebrute extends CardImpl { this.addAbility(new ConditionalInterveningIfTriggeredAbility( new EntersBattlefieldTriggeredAbility(new LoseLifeSourceControllerEffect(4)), new InvertCondition(RaidCondition.instance), - "Raid — When {this} enters the battlefield, you lose 4 life unless you attacked with a creature this turn") + "Raid — When {this} enters the battlefield, you lose 4 life unless you attacked this turn.") .setAbilityWord(AbilityWord.RAID) .addHint(RaidHint.instance), new PlayerAttackedWatcher()); diff --git a/Mage.Sets/src/mage/cards/b/BlazingSunsteel.java b/Mage.Sets/src/mage/cards/b/BlazingSunsteel.java index eb9218ad7f..36990b5141 100644 --- a/Mage.Sets/src/mage/cards/b/BlazingSunsteel.java +++ b/Mage.Sets/src/mage/cards/b/BlazingSunsteel.java @@ -15,6 +15,8 @@ import mage.constants.Outcome; import mage.constants.SubType; import mage.constants.Zone; import mage.game.Game; +import mage.game.events.DamagedEvent; +import mage.game.events.DamagedPermanentBatchEvent; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.players.Player; @@ -28,16 +30,16 @@ import java.util.UUID; public final class BlazingSunsteel extends CardImpl { public BlazingSunsteel(UUID ownerId, CardSetInfo setInfo) { - super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{1}{R}"); + super(ownerId, setInfo, new CardType[] { CardType.ARTIFACT }, "{1}{R}"); this.subtype.add(SubType.EQUIPMENT); // Equipped creature gets +1/+0 for each opponent you have. - this.addAbility(new SimpleStaticAbility(new BoostEquippedEffect( - OpponentsCount.instance, StaticValue.get(0) - ).setText("equipped creature gets +1/+0 for each opponent you have"))); + this.addAbility(new SimpleStaticAbility(new BoostEquippedEffect(OpponentsCount.instance, StaticValue.get(0)) + .setText("equipped creature gets +1/+0 for each opponent you have"))); - // Whenever equipped creature is dealt damage, it deals that much damage to any target. + // Whenever equipped creature is dealt damage, it deals that much damage to any + // target. this.addAbility(new BlazingSunsteelTriggeredAbility()); // Equip {4} @@ -72,20 +74,40 @@ class BlazingSunsteelTriggeredAbility extends TriggeredAbilityImpl { @Override public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.DAMAGED_PERMANENT; + return event.getType() == GameEvent.EventType.DAMAGED_PERMANENT_BATCH; } @Override public boolean checkTrigger(GameEvent event, Game game) { Permanent equipment = game.getPermanent(this.getSourceId()); - if (equipment == null - || equipment.getAttachedTo() == null - || !event.getTargetId().equals(equipment.getAttachedTo())) { + if (equipment == null) { return false; } - this.getEffects().setValue("equipped", game.getPermanent(equipment.getAttachedTo())); - this.getEffects().setValue("damage", event.getAmount()); - return true; + + UUID attachedCreature = equipment.getAttachedTo(); + if (attachedCreature == null) { + return false; + } + + int damage = 0; + DamagedPermanentBatchEvent dEvent = (DamagedPermanentBatchEvent) event; + for (DamagedEvent damagedEvent : dEvent.getEvents()) { + UUID targetID = damagedEvent.getTargetId(); + if (targetID == null) { + continue; + } + + if (targetID == attachedCreature) { + damage += damagedEvent.getAmount(); + } + } + + if (damage > 0) { + this.getEffects().setValue("equipped", attachedCreature); + this.getEffects().setValue("damage", damage); + return true; + } + return false; } @Override @@ -111,11 +133,13 @@ class BlazingSunsteelEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { - Permanent creature = (Permanent) getValue("equipped"); - Integer damage = (Integer) getValue("damage"); - if (creature == null || damage == null || damage < 1) { + Permanent creature = game.getPermanentOrLKIBattlefield((UUID) getValue("equipped")); + Integer damage = (Integer)getValue("damage"); + + if (creature == null || damage == null || damage < 1) { return false; } + Permanent permanent = game.getPermanent(source.getFirstTarget()); if (permanent != null) { permanent.damage(damage, creature.getId(), source, game); diff --git a/Mage.Sets/src/mage/cards/b/BonePicker.java b/Mage.Sets/src/mage/cards/b/BonePicker.java index 86a6574fa2..7101ae17f7 100644 --- a/Mage.Sets/src/mage/cards/b/BonePicker.java +++ b/Mage.Sets/src/mage/cards/b/BonePicker.java @@ -7,6 +7,7 @@ import mage.abilities.SpellAbility; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.condition.common.MorbidCondition; import mage.abilities.effects.common.cost.CostModificationEffectImpl; +import mage.abilities.hint.common.MorbidHint; import mage.abilities.keyword.DeathtouchAbility; import mage.abilities.keyword.FlyingAbility; import mage.cards.CardImpl; @@ -19,7 +20,6 @@ import mage.constants.SubType; import mage.constants.Zone; import mage.game.Game; import mage.util.CardUtil; -import mage.watchers.common.MorbidWatcher; /** * @@ -35,7 +35,7 @@ public final class BonePicker extends CardImpl { this.toughness = new MageInt(2); // Bone Picker costs {3} less to cast if a creature died this turn. - this.addAbility(new SimpleStaticAbility(Zone.ALL, new BonePickerAdjustingCostsEffect()), new MorbidWatcher()); + this.addAbility(new SimpleStaticAbility(Zone.ALL, new BonePickerAdjustingCostsEffect()).addHint(MorbidHint.instance)); // Flying this.addAbility(FlyingAbility.getInstance()); diff --git a/Mage.Sets/src/mage/cards/b/BreakOfDay.java b/Mage.Sets/src/mage/cards/b/BreakOfDay.java index 2bb5a09bd1..02502d868e 100644 --- a/Mage.Sets/src/mage/cards/b/BreakOfDay.java +++ b/Mage.Sets/src/mage/cards/b/BreakOfDay.java @@ -32,7 +32,7 @@ public final class BreakOfDay extends CardImpl { StaticFilters.FILTER_PERMANENT_CREATURES, false ), new LockedInCondition(FatefulHourCondition.instance), "
Fateful hour — If you have 5 or less life, " + - "those creatures also are indestructible this turn" + "those creatures gain indestructible until end of turn" )); } diff --git a/Mage.Sets/src/mage/cards/b/BrilliantSpectrum.java b/Mage.Sets/src/mage/cards/b/BrilliantSpectrum.java index 352f72ef6d..f3ffee2a35 100644 --- a/Mage.Sets/src/mage/cards/b/BrilliantSpectrum.java +++ b/Mage.Sets/src/mage/cards/b/BrilliantSpectrum.java @@ -25,7 +25,7 @@ public final class BrilliantSpectrum extends CardImpl { Effect effect = new DrawCardSourceControllerEffect(ColorsOfManaSpentToCastCount.getInstance()); effect.setText("Draw X cards, where X is the number of colors of mana spent to cast this spell"); this.getSpellAbility().addEffect(effect); - this.getSpellAbility().addEffect(new DiscardControllerEffect(2)); + this.getSpellAbility().addEffect(new DiscardControllerEffect(2).concatBy("Then")); } private BrilliantSpectrum(final BrilliantSpectrum card) { diff --git a/Mage.Sets/src/mage/cards/b/BrimstoneVolley.java b/Mage.Sets/src/mage/cards/b/BrimstoneVolley.java index 201317ac06..c67f824590 100644 --- a/Mage.Sets/src/mage/cards/b/BrimstoneVolley.java +++ b/Mage.Sets/src/mage/cards/b/BrimstoneVolley.java @@ -3,11 +3,11 @@ package mage.cards.b; import mage.abilities.condition.common.MorbidCondition; import mage.abilities.decorator.ConditionalOneShotEffect; import mage.abilities.effects.common.DamageTargetEffect; +import mage.abilities.hint.common.MorbidHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.target.common.TargetAnyTarget; -import mage.watchers.common.MorbidWatcher; import java.util.UUID; /** @@ -26,7 +26,7 @@ public final class BrimstoneVolley extends CardImpl { "
Morbid — {this} deals 5 damage instead if a creature died this turn." )); this.getSpellAbility().addTarget(new TargetAnyTarget()); - this.getSpellAbility().addWatcher(new MorbidWatcher()); + this.getSpellAbility().addHint(MorbidHint.instance); } private BrimstoneVolley(final BrimstoneVolley card) { diff --git a/Mage.Sets/src/mage/cards/b/Bulette.java b/Mage.Sets/src/mage/cards/b/Bulette.java index 850a60afb0..2338b591f3 100644 --- a/Mage.Sets/src/mage/cards/b/Bulette.java +++ b/Mage.Sets/src/mage/cards/b/Bulette.java @@ -6,6 +6,7 @@ import mage.abilities.common.BeginningOfYourEndStepTriggeredAbility; import mage.abilities.condition.common.MorbidCondition; import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.abilities.hint.common.MorbidHint; import mage.constants.SubType; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -30,7 +31,7 @@ public final class Bulette extends CardImpl { new BeginningOfYourEndStepTriggeredAbility(new AddCountersSourceEffect(CounterType.P1P1.createInstance()), false), MorbidCondition.instance, "At the beginning of your end step, if a creature died this turn, put a +1/+1 counter on {this}." - )); + ).addHint(MorbidHint.instance)); } private Bulette(final Bulette card) { diff --git a/Mage.Sets/src/mage/cards/c/CacklingFlames.java b/Mage.Sets/src/mage/cards/c/CacklingFlames.java index 5ef7543cb0..501f8cb4bd 100644 --- a/Mage.Sets/src/mage/cards/c/CacklingFlames.java +++ b/Mage.Sets/src/mage/cards/c/CacklingFlames.java @@ -22,7 +22,7 @@ public final class CacklingFlames extends CardImpl { // Hellbent - Cackling Flames deals 5 damage to that creature or player instead if you have no cards in hand. this.getSpellAbility().addEffect(new ConditionalOneShotEffect( new DamageTargetEffect(5), new DamageTargetEffect(3), HellbentCondition.instance, - "{this} deals 3 damage to any target
Hellbent " + + "{this} deals 3 damage to any target.
Hellbent " + "— {this} deals 5 damage instead if you have no cards in hand." )); this.getSpellAbility().addTarget(new TargetAnyTarget()); diff --git a/Mage.Sets/src/mage/cards/c/CagedZombie.java b/Mage.Sets/src/mage/cards/c/CagedZombie.java index c3fffd077e..675a0a4c19 100644 --- a/Mage.Sets/src/mage/cards/c/CagedZombie.java +++ b/Mage.Sets/src/mage/cards/c/CagedZombie.java @@ -7,12 +7,12 @@ import mage.abilities.condition.common.MorbidCondition; import mage.abilities.costs.common.TapSourceCost; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.common.LoseLifeOpponentsEffect; +import mage.abilities.hint.common.MorbidHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; import mage.constants.Zone; -import mage.watchers.common.MorbidWatcher; import java.util.UUID; @@ -34,7 +34,7 @@ public final class CagedZombie extends CardImpl { new ManaCostsImpl("{1}{B}"), MorbidCondition.instance ); ability.addCost(new TapSourceCost()); - this.addAbility(ability, new MorbidWatcher()); + this.addAbility(ability.addHint(MorbidHint.instance)); } private CagedZombie(final CagedZombie card) { diff --git a/Mage.Sets/src/mage/cards/c/CarapaceForger.java b/Mage.Sets/src/mage/cards/c/CarapaceForger.java index 06c196070b..40e48b860a 100644 --- a/Mage.Sets/src/mage/cards/c/CarapaceForger.java +++ b/Mage.Sets/src/mage/cards/c/CarapaceForger.java @@ -8,7 +8,10 @@ import mage.abilities.effects.common.continuous.BoostSourceEffect; import mage.abilities.hint.common.MetalcraftHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.*; +import mage.constants.AbilityWord; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; import java.util.UUID; @@ -25,15 +28,11 @@ public final class CarapaceForger extends CardImpl { this.power = new MageInt(2); this.toughness = new MageInt(2); - this.addAbility(new SimpleStaticAbility( - Zone.BATTLEFIELD, - new ConditionalContinuousEffect( - new BoostSourceEffect(2, 2, Duration.WhileOnBattlefield), - MetalcraftCondition.instance, "Metalcraft — {this} gets " + - "+2/+2 as long as you control three or more artifacts" - )) - .setAbilityWord(AbilityWord.METALCRAFT) - .addHint(MetalcraftHint.instance)); + this.addAbility(new SimpleStaticAbility(new ConditionalContinuousEffect( + new BoostSourceEffect(2, 2, Duration.WhileOnBattlefield), + MetalcraftCondition.instance, "{this} gets " + + "+2/+2 as long as you control three or more artifacts" + )).setAbilityWord(AbilityWord.METALCRAFT).addHint(MetalcraftHint.instance)); } private CarapaceForger(final CarapaceForger card) { diff --git a/Mage.Sets/src/mage/cards/c/CaravanVigil.java b/Mage.Sets/src/mage/cards/c/CaravanVigil.java index fd702a6e0e..9806506813 100644 --- a/Mage.Sets/src/mage/cards/c/CaravanVigil.java +++ b/Mage.Sets/src/mage/cards/c/CaravanVigil.java @@ -6,6 +6,7 @@ import mage.MageObject; import mage.abilities.Ability; import mage.abilities.condition.common.MorbidCondition; import mage.abilities.effects.OneShotEffect; +import mage.abilities.hint.common.MorbidHint; import mage.cards.*; import mage.constants.CardType; import mage.constants.Outcome; @@ -27,6 +28,7 @@ public final class CaravanVigil extends CardImpl { // Search your library for a basic land card, reveal it, put it into your hand, then shuffle your library. // Morbid — You may put that card onto the battlefield instead of putting it into your hand if a creature died this turn. this.getSpellAbility().addEffect(new CaravanVigilEffect()); + this.getSpellAbility().addHint(MorbidHint.instance); } private CaravanVigil(final CaravanVigil card) { diff --git a/Mage.Sets/src/mage/cards/c/ChromeMox.java b/Mage.Sets/src/mage/cards/c/ChromeMox.java index 52d284f598..0b7ccdc18c 100644 --- a/Mage.Sets/src/mage/cards/c/ChromeMox.java +++ b/Mage.Sets/src/mage/cards/c/ChromeMox.java @@ -14,6 +14,7 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.choices.Choice; import mage.choices.ChoiceColor; +import mage.constants.AbilityWord; import mage.constants.CardType; import mage.constants.Outcome; import mage.constants.Zone; @@ -39,7 +40,7 @@ public final class ChromeMox extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{0}"); // Imprint - When Chrome Mox enters the battlefield, you may exile a nonartifact, nonland card from your hand. - this.addAbility(new EntersBattlefieldTriggeredAbility(new ChromeMoxEffect(), true)); + this.addAbility(new EntersBattlefieldTriggeredAbility(new ChromeMoxEffect(), true).setAbilityWord(AbilityWord.IMPRINT)); // {T}: Add one mana of any of the exiled card's colors. this.addAbility(new SimpleManaAbility(Zone.BATTLEFIELD, new ChromeMoxManaEffect(), new TapSourceCost())); } diff --git a/Mage.Sets/src/mage/cards/c/ChromeSteed.java b/Mage.Sets/src/mage/cards/c/ChromeSteed.java index e65cd74a3c..26f35224a0 100644 --- a/Mage.Sets/src/mage/cards/c/ChromeSteed.java +++ b/Mage.Sets/src/mage/cards/c/ChromeSteed.java @@ -8,7 +8,10 @@ import mage.abilities.effects.common.continuous.BoostSourceEffect; import mage.abilities.hint.common.MetalcraftHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.*; +import mage.constants.AbilityWord; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; import java.util.UUID; @@ -23,15 +26,11 @@ public final class ChromeSteed extends CardImpl { this.power = new MageInt(2); this.toughness = new MageInt(2); - this.addAbility(new SimpleStaticAbility( - Zone.BATTLEFIELD, - new ConditionalContinuousEffect( - new BoostSourceEffect(2, 2, Duration.WhileOnBattlefield), - MetalcraftCondition.instance, "Metalcraft — {this} gets " + - "+2/+2 as long as you control three or more artifacts" - )) - .setAbilityWord(AbilityWord.METALCRAFT) - .addHint(MetalcraftHint.instance)); + this.addAbility(new SimpleStaticAbility(new ConditionalContinuousEffect( + new BoostSourceEffect(2, 2, Duration.WhileOnBattlefield), + MetalcraftCondition.instance, "{this} gets " + + "+2/+2 as long as you control three or more artifacts" + )).setAbilityWord(AbilityWord.METALCRAFT).addHint(MetalcraftHint.instance)); } private ChromeSteed(final ChromeSteed card) { @@ -42,5 +41,4 @@ public final class ChromeSteed extends CardImpl { public ChromeSteed copy() { return new ChromeSteed(this); } - } diff --git a/Mage.Sets/src/mage/cards/c/ClayGolem.java b/Mage.Sets/src/mage/cards/c/ClayGolem.java new file mode 100644 index 0000000000..84ba183bfe --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/ClayGolem.java @@ -0,0 +1,140 @@ +package mage.cards.c; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.BecomesMonstrousSourceTriggeredAbility; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.Cost; +import mage.abilities.costs.CostImpl; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.DestroyTargetEffect; +import mage.abilities.hint.common.MonstrousHint; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.counters.CounterType; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.TargetPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class ClayGolem extends CardImpl { + + public ClayGolem(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT, CardType.CREATURE}, "{4}"); + + this.subtype.add(SubType.GOLEM); + this.power = new MageInt(4); + this.toughness = new MageInt(4); + + // {6}, Roll a d8: Monstrosity X, where X is the result. + Ability ability = new SimpleActivatedAbility(new ClayGolemEffect(), new GenericManaCost(6)); + ability.addCost(new ClayGolemCost()); + this.addAbility(ability.addHint(MonstrousHint.instance)); + + // Berserk — When Clay Golem becomes monstrous, destroy target permanent. + ability = new BecomesMonstrousSourceTriggeredAbility(new DestroyTargetEffect()); + ability.addTarget(new TargetPermanent()); + this.addAbility(ability.withFlavorWord("Berserk")); + } + + private ClayGolem(final ClayGolem card) { + super(card); + } + + @Override + public ClayGolem copy() { + return new ClayGolem(this); + } +} + +class ClayGolemCost extends CostImpl { + + private int lastRoll = 0; + + ClayGolemCost() { + super(); + text = "roll a d8"; + } + + private ClayGolemCost(final ClayGolemCost cost) { + super(cost); + this.lastRoll = cost.lastRoll; + } + + @Override + public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { + return true; + } + + @Override + public boolean pay(Ability ability, Game game, Ability source, UUID controllerId, boolean noMana, Cost costToPay) { + Player player = game.getPlayer(controllerId); + if (player == null) { + return paid; + } + lastRoll = player.rollDice(source, game, 8); + paid = true; + return paid; + } + + @Override + public ClayGolemCost copy() { + return new ClayGolemCost(this); + } + + public int getLastRoll() { + return lastRoll; + } +} + +class ClayGolemEffect extends OneShotEffect { + ClayGolemEffect() { + super(Outcome.Benefit); + staticText = "monstrosity X, where X is the result"; + } + + private ClayGolemEffect(final ClayGolemEffect effect) { + super(effect); + } + + @Override + public ClayGolemEffect copy() { + return new ClayGolemEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = source.getSourcePermanentIfItStillExists(game); + if (permanent == null || permanent.isMonstrous()) { + return false; + } + int monstrosityValue = source + .getCosts() + .stream() + .filter(ClayGolemCost.class::isInstance) + .map(ClayGolemCost.class::cast) + .mapToInt(ClayGolemCost::getLastRoll) + .findFirst() + .orElse(0); + permanent.addCounters( + CounterType.P1P1.createInstance(monstrosityValue), + source.getControllerId(), source, game + ); + permanent.setMonstrous(true); + game.fireEvent(GameEvent.getEvent( + GameEvent.EventType.BECOMES_MONSTROUS, source.getSourceId(), + source, source.getControllerId(), monstrosityValue + )); + return true; + } +} diff --git a/Mage.Sets/src/mage/cards/c/CloneShell.java b/Mage.Sets/src/mage/cards/c/CloneShell.java index d31fd85d05..6ce2cfdaa2 100644 --- a/Mage.Sets/src/mage/cards/c/CloneShell.java +++ b/Mage.Sets/src/mage/cards/c/CloneShell.java @@ -6,10 +6,7 @@ import mage.abilities.common.DiesSourceTriggeredAbility; import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.effects.OneShotEffect; import mage.cards.*; -import mage.constants.CardType; -import mage.constants.Outcome; -import mage.constants.SubType; -import mage.constants.Zone; +import mage.constants.*; import mage.filter.FilterCard; import mage.game.Game; import mage.game.permanent.Permanent; @@ -33,7 +30,7 @@ public final class CloneShell extends CardImpl { this.toughness = new MageInt(2); // Imprint - When Clone Shell enters the battlefield, look at the top four cards of your library, exile one face down, then put the rest on the bottom of your library in any order. - this.addAbility(new EntersBattlefieldTriggeredAbility(new CloneShellEffect(), false)); + this.addAbility(new EntersBattlefieldTriggeredAbility(new CloneShellEffect(), false).setAbilityWord(AbilityWord.IMPRINT)); // When Clone Shell dies, turn the exiled card face up. If it's a creature card, put it onto the battlefield under your control. this.addAbility(new DiesSourceTriggeredAbility(new CloneShellDiesEffect())); diff --git a/Mage.Sets/src/mage/cards/c/CodeOfConstraint.java b/Mage.Sets/src/mage/cards/c/CodeOfConstraint.java index f548ade278..a88990cfc8 100644 --- a/Mage.Sets/src/mage/cards/c/CodeOfConstraint.java +++ b/Mage.Sets/src/mage/cards/c/CodeOfConstraint.java @@ -29,7 +29,7 @@ public final class CodeOfConstraint extends CardImpl { this.getSpellAbility().addTarget(new TargetCreaturePermanent()); // Draw a card. - this.getSpellAbility().addEffect(new DrawCardSourceControllerEffect(1)); + this.getSpellAbility().addEffect(new DrawCardSourceControllerEffect(1).concatBy("
")); // Addendum — If you cast this spell during your main phase, tap that creature and it doesn't untap during its controller's next untap step. this.getSpellAbility().addEffect(new CodeOfConstraintEffect()); diff --git a/Mage.Sets/src/mage/cards/c/ConcussiveBolt.java b/Mage.Sets/src/mage/cards/c/ConcussiveBolt.java index ba0d5f960e..8f2ed7780f 100644 --- a/Mage.Sets/src/mage/cards/c/ConcussiveBolt.java +++ b/Mage.Sets/src/mage/cards/c/ConcussiveBolt.java @@ -34,7 +34,6 @@ public final class ConcussiveBolt extends CardImpl { // Metalcraft — If you control three or more artifacts, creatures that player controls can't block this turn. this.getSpellAbility().addEffect(new ConcussiveBoltEffect()); this.getSpellAbility().addEffect(new ConcussiveBoltRestrictionEffect()); - this.getSpellAbility().setAbilityWord(AbilityWord.METALCRAFT); this.getSpellAbility().addHint(MetalcraftHint.instance); } @@ -52,7 +51,7 @@ class ConcussiveBoltEffect extends OneShotEffect { public ConcussiveBoltEffect() { super(Outcome.Benefit); - this.staticText = "Metalcraft — If you control three or more artifacts, creatures controlled by that player or by that planeswalker's controller can't block this turn."; + this.staticText = "
Metalcraft — If you control three or more artifacts, creatures controlled by that player or by that planeswalker's controller can't block this turn."; } public ConcussiveBoltEffect(final ConcussiveBoltEffect effect) { diff --git a/Mage.Sets/src/mage/cards/c/ContactOtherPlane.java b/Mage.Sets/src/mage/cards/c/ContactOtherPlane.java index 3a8ceee6d4..c172cb58e1 100644 --- a/Mage.Sets/src/mage/cards/c/ContactOtherPlane.java +++ b/Mage.Sets/src/mage/cards/c/ContactOtherPlane.java @@ -33,7 +33,7 @@ public final class ContactOtherPlane extends CardImpl { // 20 | Scry 3, then draw three cards. effect.addTableEntry( 20, 20, new ScryEffect(3, false), - new DrawCardSourceControllerEffect(2).concatBy(", then") + new DrawCardSourceControllerEffect(3).concatBy(", then") ); } diff --git a/Mage.Sets/src/mage/cards/c/CropSigil.java b/Mage.Sets/src/mage/cards/c/CropSigil.java index bb9d2af446..1efa3cb98b 100644 --- a/Mage.Sets/src/mage/cards/c/CropSigil.java +++ b/Mage.Sets/src/mage/cards/c/CropSigil.java @@ -43,7 +43,7 @@ public final class CropSigil extends CardImpl { Ability ability = new ConditionalActivatedAbility(Zone.BATTLEFIELD, new ReturnToHandTargetEffect(true), new ManaCostsImpl<>("{2}{G}"), DeliriumCondition.instance, "Delirium — {2}{G}, Sacrifice {this}: Return up to one target creature card and up to one target land card from your graveyard to your hand. " - + "Activate only if there are four or more card types among cards in your graveyard"); + + "Activate only if there are four or more card types among cards in your graveyard."); ability.addCost(new SacrificeSourceCost()); ability.addTarget(new TargetCardInYourGraveyard(0, 1, filterCreature)); ability.addTarget(new TargetCardInYourGraveyard(0, 1, filterLand)); diff --git a/Mage.Sets/src/mage/cards/d/DarkDabbling.java b/Mage.Sets/src/mage/cards/d/DarkDabbling.java index bab340e52d..8337340f6f 100644 --- a/Mage.Sets/src/mage/cards/d/DarkDabbling.java +++ b/Mage.Sets/src/mage/cards/d/DarkDabbling.java @@ -48,7 +48,7 @@ class DarkDabblingEffect extends OneShotEffect { public DarkDabblingEffect() { super(Outcome.Benefit); - this.staticText = "Spell mastery — If there are two or more instant and/or sorcery cards in your graveyard, also regenerate each other creature you control"; + this.staticText = "
Spell mastery — If there are two or more instant and/or sorcery cards in your graveyard, also regenerate each other creature you control"; } public DarkDabblingEffect(final DarkDabblingEffect effect) { diff --git a/Mage.Sets/src/mage/cards/d/DawnbringerCleric.java b/Mage.Sets/src/mage/cards/d/DawnbringerCleric.java index 3999e94d0d..746cef4f2e 100644 --- a/Mage.Sets/src/mage/cards/d/DawnbringerCleric.java +++ b/Mage.Sets/src/mage/cards/d/DawnbringerCleric.java @@ -31,7 +31,7 @@ public final class DawnbringerCleric extends CardImpl { // When Dawnbringer Cleric enters the battlefield, choose one — // • Cure Wounds — You gain 2 life. - Ability ability = new EntersBattlefieldTriggeredAbility(new GainLifeEffect(1)); + Ability ability = new EntersBattlefieldTriggeredAbility(new GainLifeEffect(2)); ability.getModes().getMode().withFlavorWord("Cure Wounds"); // • Dispel Magic — Destroy target enchantment. diff --git a/Mage.Sets/src/mage/cards/d/DeathPriestOfMyrkul.java b/Mage.Sets/src/mage/cards/d/DeathPriestOfMyrkul.java index 11222e13ae..51a906c2b8 100644 --- a/Mage.Sets/src/mage/cards/d/DeathPriestOfMyrkul.java +++ b/Mage.Sets/src/mage/cards/d/DeathPriestOfMyrkul.java @@ -3,18 +3,18 @@ package mage.cards.d; import mage.MageInt; import mage.abilities.common.BeginningOfEndStepTriggeredAbility; import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.condition.common.CompletedDungeonCondition; +import mage.abilities.condition.common.MorbidCondition; import mage.abilities.costs.mana.GenericManaCost; import mage.abilities.effects.common.CreateTokenEffect; import mage.abilities.effects.common.DoIfCostPaid; import mage.abilities.effects.common.continuous.BoostControlledEffect; +import mage.abilities.hint.common.MorbidHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; import mage.filter.common.FilterCreaturePermanent; import mage.filter.predicate.Predicates; import mage.game.permanent.token.SkeletonToken; -import mage.watchers.common.CompletedDungeonWatcher; import java.util.UUID; @@ -51,8 +51,8 @@ public final class DeathPriestOfMyrkul extends CardImpl { this.addAbility(new BeginningOfEndStepTriggeredAbility( Zone.BATTLEFIELD, new DoIfCostPaid(new CreateTokenEffect(new SkeletonToken()), new GenericManaCost(1)), - TargetController.YOU, CompletedDungeonCondition.instance, false - ).addHint(CompletedDungeonCondition.getHint()), new CompletedDungeonWatcher()); + TargetController.YOU, MorbidCondition.instance, false + ).addHint(MorbidHint.instance)); } private DeathPriestOfMyrkul(final DeathPriestOfMyrkul card) { diff --git a/Mage.Sets/src/mage/cards/d/DeathreapRitual.java b/Mage.Sets/src/mage/cards/d/DeathreapRitual.java index 74e471fdc7..a4a9f34b93 100644 --- a/Mage.Sets/src/mage/cards/d/DeathreapRitual.java +++ b/Mage.Sets/src/mage/cards/d/DeathreapRitual.java @@ -5,8 +5,10 @@ import java.util.UUID; import mage.abilities.common.BeginningOfEndStepTriggeredAbility; import mage.abilities.condition.common.MorbidCondition; import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.hint.common.MorbidHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; +import mage.constants.AbilityWord; import mage.constants.CardType; import mage.constants.TargetController; import mage.constants.Zone; @@ -22,7 +24,7 @@ public final class DeathreapRitual extends CardImpl { // Morbid — At the beginning of each end step, if a creature died this turn, you may draw a card. this.addAbility(new BeginningOfEndStepTriggeredAbility(Zone.BATTLEFIELD, new DrawCardSourceControllerEffect(1), - TargetController.ANY, MorbidCondition.instance, true)); + TargetController.ANY, MorbidCondition.instance, true).addHint(MorbidHint.instance).setAbilityWord(AbilityWord.MORBID)); } private DeathreapRitual(final DeathreapRitual card) { diff --git a/Mage.Sets/src/mage/cards/d/Demilich.java b/Mage.Sets/src/mage/cards/d/Demilich.java new file mode 100644 index 0000000000..cb64df7b96 --- /dev/null +++ b/Mage.Sets/src/mage/cards/d/Demilich.java @@ -0,0 +1,168 @@ +package mage.cards.d; + +import java.util.UUID; + +import mage.ApprovingObject; +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.AttacksTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.costs.Cost; +import mage.abilities.costs.Costs; +import mage.abilities.costs.CostsImpl; +import mage.abilities.costs.common.ExileFromGraveCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.effects.AsThoughEffectImpl; +import mage.abilities.effects.Effect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.cost.SpellCostReductionForEachSourceEffect; +import mage.abilities.hint.ValueHint; +import mage.cards.Card; +import mage.constants.*; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.game.stack.Spell; +import mage.players.Player; +import mage.target.common.TargetCardInYourGraveyard; +import mage.watchers.common.SpellsCastWatcher; + +/** + * + * @author weirddan455 + */ +public final class Demilich extends CardImpl { + + public Demilich(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{U}{U}{U}{U}"); + + this.subtype.add(SubType.SKELETON); + this.subtype.add(SubType.WIZARD); + this.power = new MageInt(4); + this.toughness = new MageInt(3); + + // This spell costs {U} less to cast for each instant and sorcery you've cast this turn. + this.addAbility(new SimpleStaticAbility(Zone.ALL, new SpellCostReductionForEachSourceEffect( + new ManaCostsImpl<>("{U}"), DemilichValue.instance + )).addHint(new ValueHint("Instants and sorceries you've cast this turn", DemilichValue.instance)), new SpellsCastWatcher()); + + // Whenever Demilich attacks, exile up to one target instant or sorcery card from your graveyard. Copy it. You may cast the copy. + Ability ability = new AttacksTriggeredAbility(new DemilichCopyEffect()); + ability.addTarget(new TargetCardInYourGraveyard(0, 1, StaticFilters.FILTER_CARD_INSTANT_OR_SORCERY_FROM_YOUR_GRAVEYARD)); + this.addAbility(ability); + + // You may cast Demilich from your graveyard by exiling four instants and/or sorcery cards from your graveyard in addition to paying its other costs. + this.addAbility(new SimpleStaticAbility(Zone.ALL, new DemilichPlayEffect())); + } + + private Demilich(final Demilich card) { + super(card); + } + + @Override + public Demilich copy() { + return new Demilich(this); + } +} + +enum DemilichValue implements DynamicValue { + instance; + + @Override + public int calculate(Game game, Ability sourceAbility, Effect effect) { + int spells = 0; + SpellsCastWatcher watcher = game.getState().getWatcher(SpellsCastWatcher.class); + if (watcher != null) { + for (Spell spell : watcher.getSpellsCastThisTurn(sourceAbility.getControllerId())) { + if (spell.isInstantOrSorcery()) { + spells++; + } + } + } + return spells; + } + + @Override + public DemilichValue copy() { + return instance; + } + + @Override + public String getMessage() { + return "instant and sorcery you've cast this turn"; + } +} + +class DemilichCopyEffect extends OneShotEffect { + + public DemilichCopyEffect() { + super(Outcome.Benefit); + this.staticText = "exile up to one target instant or sorcery card from your graveyard. Copy it. You may cast the copy"; + } + + private DemilichCopyEffect(final DemilichCopyEffect effect) { + super(effect); + } + + @Override + public DemilichCopyEffect copy() { + return new DemilichCopyEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + Card card = game.getCard(targetPointer.getFirst(game, source)); + if (controller == null || card == null) { + return false; + } + controller.moveCards(card, Zone.EXILED, source, game); + if (controller.chooseUse(outcome, "Cast copy of " + card.getName() + '?', source, game)) { + Card copiedCard = game.copyCard(card, source, controller.getId()); + game.getState().setValue("PlayFromNotOwnHandZone" + copiedCard.getId(), Boolean.TRUE); + controller.cast(controller.chooseAbilityForCast(copiedCard, game, false), + game, false, new ApprovingObject(source, game)); + game.getState().setValue("PlayFromNotOwnHandZone" + copiedCard.getId(), null); + } + return true; + } +} + +class DemilichPlayEffect extends AsThoughEffectImpl { + + public DemilichPlayEffect() { + super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.EndOfGame, Outcome.Benefit); + this.staticText = "You may cast {this} from your graveyard by exiling four instants and/or sorcery cards from your graveyard in addition to paying its other costs"; + } + + private DemilichPlayEffect(final DemilichPlayEffect effect) { + super(effect); + } + + @Override + public DemilichPlayEffect copy() { + return new DemilichPlayEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + return true; + } + + @Override + public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) { + if (source.getSourceId().equals(objectId) && source.isControlledBy(affectedControllerId) + && game.getState().getZone(objectId) == Zone.GRAVEYARD) { + Player controller = game.getPlayer(affectedControllerId); + if (controller != null) { + Costs costs = new CostsImpl<>(); + costs.add(new ExileFromGraveCost(new TargetCardInYourGraveyard(4, StaticFilters.FILTER_CARD_INSTANT_OR_SORCERY_FROM_YOUR_GRAVEYARD))); + controller.setCastSourceIdWithAlternateMana(objectId, new ManaCostsImpl<>("{U}{U}{U}{U}"), costs); + return true; + } + } + return false; + } +} diff --git a/Mage.Sets/src/mage/cards/d/DispenseJustice.java b/Mage.Sets/src/mage/cards/d/DispenseJustice.java index 84b1e3e496..8a09c91c82 100644 --- a/Mage.Sets/src/mage/cards/d/DispenseJustice.java +++ b/Mage.Sets/src/mage/cards/d/DispenseJustice.java @@ -28,7 +28,6 @@ public final class DispenseJustice extends CardImpl { // Metalcraft — That player sacrifices two attacking creatures instead if you control three or more artifacts. this.getSpellAbility().addEffect(new DispenseJusticeEffect()); this.getSpellAbility().addTarget(new TargetPlayer()); - this.getSpellAbility().setAbilityWord(AbilityWord.METALCRAFT); this.getSpellAbility().addHint(MetalcraftHint.instance); } diff --git a/Mage.Sets/src/mage/cards/d/DreadhordeArcanist.java b/Mage.Sets/src/mage/cards/d/DreadhordeArcanist.java index 5c1f0edb6d..8ba2508b57 100644 --- a/Mage.Sets/src/mage/cards/d/DreadhordeArcanist.java +++ b/Mage.Sets/src/mage/cards/d/DreadhordeArcanist.java @@ -76,7 +76,7 @@ enum DreadhordeArcanistPredicate implements ObjectSourcePlayerPredicate input, Game game) { Permanent sourcePermanent = game.getPermanentOrLKIBattlefield(input.getSourceId()); return sourcePermanent != null - & input.getObject().getManaValue() <= sourcePermanent.getPower().getValue(); + && input.getObject().getManaValue() <= sourcePermanent.getPower().getValue(); } } diff --git a/Mage.Sets/src/mage/cards/e/EbonyFly.java b/Mage.Sets/src/mage/cards/e/EbonyFly.java new file mode 100644 index 0000000000..8949eb3277 --- /dev/null +++ b/Mage.Sets/src/mage/cards/e/EbonyFly.java @@ -0,0 +1,110 @@ +package mage.cards.e; + +import mage.abilities.Ability; +import mage.abilities.common.AttacksTriggeredAbility; +import mage.abilities.common.EntersBattlefieldTappedAbility; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.continuous.BecomesCreatureSourceEffect; +import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.abilities.mana.ColorlessManaAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterAttackingCreature; +import mage.filter.predicate.mageobject.AnotherPredicate; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.game.permanent.token.custom.CreatureToken; +import mage.players.Player; +import mage.target.TargetPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class EbonyFly extends CardImpl { + + private static final FilterPermanent filter + = new FilterAttackingCreature("another target attacking creature"); + + static { + filter.add(AnotherPredicate.instance); + } + + public EbonyFly(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{2}"); + + // Ebony Fly enters the battlefield tapped. + this.addAbility(new EntersBattlefieldTappedAbility()); + + // {T}: Add {C}. + this.addAbility(new ColorlessManaAbility()); + + // {4}: Roll a d6. Until end of turn, you may have Ebony Fly become an X/X Insect artifact creature with flying, where X is the result. + this.addAbility(new SimpleActivatedAbility(new EbonyFlyEffect(), new GenericManaCost(4))); + + // Whenever Ebony Fly attacks, another target attacking creature gains flying until end of turn. + Ability ability = new AttacksTriggeredAbility(new GainAbilityTargetEffect( + FlyingAbility.getInstance(), Duration.EndOfTurn + )); + ability.addTarget(new TargetPermanent(filter)); + this.addAbility(ability); + } + + private EbonyFly(final EbonyFly card) { + super(card); + } + + @Override + public EbonyFly copy() { + return new EbonyFly(this); + } +} + +class EbonyFlyEffect extends OneShotEffect { + + EbonyFlyEffect() { + super(Outcome.Benefit); + staticText = "roll a d6. Until end of turn, you may have {this} " + + "become an X/X Insect artifact creature with flying, where X is the result"; + } + + private EbonyFlyEffect(final EbonyFlyEffect effect) { + super(effect); + } + + @Override + public EbonyFlyEffect copy() { + return new EbonyFlyEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + int result = player.rollDice(source, game, 6); + Permanent permanent = source.getSourcePermanentIfItStillExists(game); + if (permanent == null || !player.chooseUse( + outcome, "Have " + permanent.getName() + " become a " + + result + '/' + result + " creature until end of turn?", source, game + )) { + return true; + } + game.addEffect(new BecomesCreatureSourceEffect( + new CreatureToken(result, result) + .withType(CardType.ARTIFACT) + .withAbility(FlyingAbility.getInstance()), + "", Duration.EndOfTurn, false, false + ), source); + return true; + } +} diff --git a/Mage.Sets/src/mage/cards/e/EmissaryOfTheSleepless.java b/Mage.Sets/src/mage/cards/e/EmissaryOfTheSleepless.java index 9d5e817ab6..48e73d741f 100644 --- a/Mage.Sets/src/mage/cards/e/EmissaryOfTheSleepless.java +++ b/Mage.Sets/src/mage/cards/e/EmissaryOfTheSleepless.java @@ -8,6 +8,7 @@ import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.condition.common.MorbidCondition; import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.hint.common.MorbidHint; import mage.abilities.keyword.FlyingAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -32,7 +33,7 @@ public final class EmissaryOfTheSleepless extends CardImpl { // When Emissary of the Sleepless enters the battlefield, if a creature died this turn, create a 1/1 white Spirit creature token with flying. TriggeredAbility ability = new EntersBattlefieldTriggeredAbility(new CreateTokenEffect(new SpiritWhiteToken())); - this.addAbility(new ConditionalInterveningIfTriggeredAbility(ability, MorbidCondition.instance, "When {this} enters the battlefield, if a creature died this turn, create a 1/1 white Spirit creature token with flying.")); + this.addAbility(new ConditionalInterveningIfTriggeredAbility(ability, MorbidCondition.instance, "When {this} enters the battlefield, if a creature died this turn, create a 1/1 white Spirit creature token with flying.").addHint(MorbidHint.instance)); } private EmissaryOfTheSleepless(final EmissaryOfTheSleepless card) { diff --git a/Mage.Sets/src/mage/cards/e/EtchedChampion.java b/Mage.Sets/src/mage/cards/e/EtchedChampion.java index 176017ef35..872ae67025 100644 --- a/Mage.Sets/src/mage/cards/e/EtchedChampion.java +++ b/Mage.Sets/src/mage/cards/e/EtchedChampion.java @@ -22,7 +22,7 @@ import java.util.UUID; * @author North */ public final class EtchedChampion extends CardImpl { - private static final String ruleText = "Metalcraft — Etched Champion has protection from all colors as long as you control three or more artifacts"; + private static final String ruleText = "{this} has protection from all colors as long as you control three or more artifacts"; private static final FilterCard filter = new FilterCard("all colors"); diff --git a/Mage.Sets/src/mage/cards/e/ExclusionRitual.java b/Mage.Sets/src/mage/cards/e/ExclusionRitual.java index d94793960c..c6ab1c28ee 100644 --- a/Mage.Sets/src/mage/cards/e/ExclusionRitual.java +++ b/Mage.Sets/src/mage/cards/e/ExclusionRitual.java @@ -8,10 +8,7 @@ import mage.abilities.effects.OneShotEffect; import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.CardType; -import mage.constants.Duration; -import mage.constants.Outcome; -import mage.constants.Zone; +import mage.constants.*; import mage.filter.FilterPermanent; import mage.filter.predicate.Predicates; import mage.game.Game; @@ -40,7 +37,7 @@ public final class ExclusionRitual extends CardImpl { // Imprint - When Exclusion Ritual enters the battlefield, exile target nonland permanent. Ability ability = new EntersBattlefieldTriggeredAbility(new ExclusionRitualImprintEffect(), false); ability.addTarget(new TargetPermanent(filter)); - this.addAbility(ability); + this.addAbility(ability.setAbilityWord(AbilityWord.IMPRINT)); // Players can't cast spells with the same name as the exiled card. this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new ExclusionRitualReplacementEffect())); } diff --git a/Mage.Sets/src/mage/cards/e/ExertInfluence.java b/Mage.Sets/src/mage/cards/e/ExertInfluence.java index c071c37ad0..c56cb65785 100644 --- a/Mage.Sets/src/mage/cards/e/ExertInfluence.java +++ b/Mage.Sets/src/mage/cards/e/ExertInfluence.java @@ -48,7 +48,7 @@ class ExertInfluenceEffect extends OneShotEffect { public ExertInfluenceEffect() { super(Outcome.GainControl); - this.staticText = "Gain control of target creature if its power is less than or equal to the number of colors spent to cast this spell"; + this.staticText = "Gain control of target creature if its power is less than or equal to the number of colors of mana spent to cast this spell"; } public ExertInfluenceEffect(final ExertInfluenceEffect effect) { diff --git a/Mage.Sets/src/mage/cards/e/EzurisBrigade.java b/Mage.Sets/src/mage/cards/e/EzurisBrigade.java index 888af884a0..70c285bf42 100644 --- a/Mage.Sets/src/mage/cards/e/EzurisBrigade.java +++ b/Mage.Sets/src/mage/cards/e/EzurisBrigade.java @@ -20,7 +20,7 @@ import java.util.UUID; * @author Loki */ public final class EzurisBrigade extends CardImpl { - private static final String rule = "Metalcraft — As long as you control three or more artifacts, Ezuri's Brigade gets +4/+4 and has trample"; + private static final String rule = "As long as you control three or more artifacts, {this} gets +4/+4"; public EzurisBrigade(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{G}{G}"); @@ -33,7 +33,7 @@ public final class EzurisBrigade extends CardImpl { ContinuousEffect boostSource = new BoostSourceEffect(4, 4, Duration.WhileOnBattlefield); ConditionalContinuousEffect effect = new ConditionalContinuousEffect(boostSource, MetalcraftCondition.instance, rule); Ability ability = new SimpleStaticAbility(Zone.BATTLEFIELD, effect); - ability.addEffect(new ConditionalContinuousEffect(new GainAbilitySourceEffect(TrampleAbility.getInstance(), Duration.WhileOnBattlefield), MetalcraftCondition.instance, "")); + ability.addEffect(new ConditionalContinuousEffect(new GainAbilitySourceEffect(TrampleAbility.getInstance(), Duration.WhileOnBattlefield), MetalcraftCondition.instance, "and has trample")); ability.setAbilityWord(AbilityWord.METALCRAFT); ability.addHint(MetalcraftHint.instance); this.addAbility(ability); diff --git a/Mage.Sets/src/mage/cards/f/FesterhideBoar.java b/Mage.Sets/src/mage/cards/f/FesterhideBoar.java index 916f9a6da4..9714339aae 100644 --- a/Mage.Sets/src/mage/cards/f/FesterhideBoar.java +++ b/Mage.Sets/src/mage/cards/f/FesterhideBoar.java @@ -7,9 +7,11 @@ import mage.abilities.common.EntersBattlefieldAbility; import mage.abilities.condition.common.MorbidCondition; import mage.abilities.decorator.ConditionalOneShotEffect; import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.abilities.hint.common.MorbidHint; import mage.abilities.keyword.TrampleAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; +import mage.constants.AbilityWord; import mage.constants.CardType; import mage.constants.SubType; import mage.counters.CounterType; @@ -30,7 +32,7 @@ public final class FesterhideBoar extends CardImpl { this.addAbility(TrampleAbility.getInstance()); // Morbid — Festerhide Boar enters the battlefield with two +1/+1 counters on it if a creature died this turn. this.addAbility(new EntersBattlefieldAbility(new ConditionalOneShotEffect(new AddCountersSourceEffect(CounterType.P1P1.createInstance(2)), - MorbidCondition.instance, ""), "with two +1/+1 counters on it if a creature died this turn")); + MorbidCondition.instance, ""), "with two +1/+1 counters on it if a creature died this turn").addHint(MorbidHint.instance).setAbilityWord(AbilityWord.MORBID)); } private FesterhideBoar(final FesterhideBoar card) { diff --git a/Mage.Sets/src/mage/cards/f/FeySteed.java b/Mage.Sets/src/mage/cards/f/FeySteed.java new file mode 100644 index 0000000000..6266d63078 --- /dev/null +++ b/Mage.Sets/src/mage/cards/f/FeySteed.java @@ -0,0 +1,110 @@ +package mage.cards.f; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.AttacksTriggeredAbility; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; +import mage.abilities.keyword.IndestructibleAbility; +import mage.constants.SubType; +import mage.constants.TargetController; +import mage.constants.Zone; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.mageobject.AnotherPredicate; +import mage.filter.predicate.permanent.AttackingPredicate; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.common.TargetCreaturePermanent; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; + +/** + * + * @author anonymous + */ +public final class FeySteed extends CardImpl { + + private static final FilterCreaturePermanent filter = new FilterCreaturePermanent( + "another target attacking creature you control"); + + static { + filter.add(TargetController.YOU.getControllerPredicate()); + filter.add(AnotherPredicate.instance); + filter.add(AttackingPredicate.instance); + } + + public FeySteed(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[] { CardType.CREATURE }, "{2}{W}{W}"); + + this.subtype.add(SubType.ELK); + this.power = new MageInt(4); + this.toughness = new MageInt(4); + + // Whenever Fey Steed attacks, another target attacking creature you control + // gains indestructible until end of turn. + Ability ability = new AttacksTriggeredAbility( + new GainAbilityTargetEffect(IndestructibleAbility.getInstance(), Duration.EndOfTurn), false); + ability.addTarget(new TargetCreaturePermanent(filter)); + this.addAbility(ability); + + // Whenever a creature or planeswalker you control becomes the target of a spell + // or ability an opponent controls, you may draw a card. + this.addAbility(new FeySteedTriggeredAbility()); + } + + private FeySteed(final FeySteed card) { + super(card); + } + + @Override + public FeySteed copy() { + return new FeySteed(this); + } +} + +class FeySteedTriggeredAbility extends TriggeredAbilityImpl { + + public FeySteedTriggeredAbility() { + super(Zone.BATTLEFIELD, new DrawCardSourceControllerEffect(1), true); + } + + public FeySteedTriggeredAbility(FeySteedTriggeredAbility ability) { + super(ability); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.TARGETED; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + Player targetter = game.getPlayer(event.getPlayerId()); + if (targetter == null || !targetter.hasOpponent(this.controllerId, game)) { + return false; + } + + Permanent permanent = game.getPermanentOrLKIBattlefield(event.getTargetId()); + if (permanent == null || !permanent.isControlledBy(this.getControllerId()) + || (!permanent.isCreature(game) && !permanent.isPlaneswalker(game))) { + return false; + } + return true; + } + + @Override + public String getRule() { + return "Whenever a creature or planeswalker you control becomes the target of a spell or ability an opponent controls, you may draw a card"; + } + + @Override + public FeySteedTriggeredAbility copy() { + return new FeySteedTriggeredAbility(this); + } +} diff --git a/Mage.Sets/src/mage/cards/f/Fiendlash.java b/Mage.Sets/src/mage/cards/f/Fiendlash.java new file mode 100644 index 0000000000..11ab580dcb --- /dev/null +++ b/Mage.Sets/src/mage/cards/f/Fiendlash.java @@ -0,0 +1,162 @@ +package mage.cards.f; + +import java.util.UUID; + +import mage.abilities.Ability; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.continuous.BoostEquippedEffect; +import mage.abilities.effects.common.continuous.GainAbilityAttachedEffect; +import mage.abilities.keyword.EquipAbility; +import mage.abilities.keyword.ReachAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.AttachmentType; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.events.DamagedEvent; +import mage.game.events.DamagedPermanentBatchEvent; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.common.TargetPlayerOrPlaneswalker; + +/** + * + * @author zeffirojoe + */ +public final class Fiendlash extends CardImpl { + + public Fiendlash(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[] { CardType.ARTIFACT }, "{1}{R}"); + + this.subtype.add(SubType.EQUIPMENT); + + // Equipped creature gets +2/+0 and has reach. + Ability staticAbility = new SimpleStaticAbility(new BoostEquippedEffect(2, 0)); + staticAbility.addEffect(new GainAbilityAttachedEffect(ReachAbility.getInstance(), AttachmentType.EQUIPMENT) + .setText("and has reach")); + this.addAbility(staticAbility); + + // Whenever equipped creature is dealt damage, it deals damage equal to its + // power to target player or planeswalker. + this.addAbility(new FiendlashTriggeredAbility()); + + // Equip {2}{R} + this.addAbility(new EquipAbility(Outcome.AddAbility, new ManaCostsImpl<>("{2}{R}"))); + } + + private Fiendlash(final Fiendlash card) { + super(card); + } + + @Override + public Fiendlash copy() { + return new Fiendlash(this); + } +} + +class FiendlashTriggeredAbility extends TriggeredAbilityImpl { + + FiendlashTriggeredAbility() { + super(Zone.BATTLEFIELD, new FiendlashEffect(), false); + this.addTarget(new TargetPlayerOrPlaneswalker()); + } + + private FiendlashTriggeredAbility(final FiendlashTriggeredAbility ability) { + super(ability); + } + + @Override + public FiendlashTriggeredAbility copy() { + return new FiendlashTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.DAMAGED_PERMANENT_BATCH; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + Permanent equipment = game.getPermanent(this.getSourceId()); + if (equipment == null) { + return false; + } + + UUID attachedCreature = equipment.getAttachedTo(); + if (attachedCreature == null) { + return false; + } + + game.getState().setValue("Fiendlash" + equipment.getId(), attachedCreature); + + DamagedPermanentBatchEvent dEvent = (DamagedPermanentBatchEvent) event; + for (DamagedEvent damagedEvent : dEvent.getEvents()) { + UUID targetID = damagedEvent.getTargetId(); + if (targetID == null) { + continue; + } + + if (targetID == attachedCreature) { + return true; + } + } + + return false; + } + + @Override + public String getRule() { + return "Whenever equipped creature is dealt damage, it deals damage equal to its power to target player or planeswalker."; + } +} + +class FiendlashEffect extends OneShotEffect { + + FiendlashEffect() { + super(Outcome.Benefit); + } + + private FiendlashEffect(final FiendlashEffect effect) { + super(effect); + } + + @Override + public FiendlashEffect copy() { + return new FiendlashEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent creature = game + .getPermanentOrLKIBattlefield((UUID) game.getState().getValue("Fiendlash" + source.getSourceId())); + if (creature == null) { + return false; + } + + int damage = creature.getPower().getValue(); + if (damage < 1) { + return false; + } + + Permanent permanent = game.getPermanent(source.getFirstTarget()); + if (permanent != null) { + if (permanent.isPlaneswalker()) { + permanent.damage(damage, creature.getId(), source, game); + return true; + } + } + Player player = game.getPlayer(source.getFirstTarget()); + if (player != null) { + player.damage(damage, creature.getId(), source, game); + return true; + } + return false; + } +} diff --git a/Mage.Sets/src/mage/cards/f/FighterClass.java b/Mage.Sets/src/mage/cards/f/FighterClass.java index 29e81e8eea..183f415693 100644 --- a/Mage.Sets/src/mage/cards/f/FighterClass.java +++ b/Mage.Sets/src/mage/cards/f/FighterClass.java @@ -49,7 +49,7 @@ public final class FighterClass extends CardImpl { // When Fighter Class enters the battlefield, search your library for an Equipment card, reveal it, put it into your hand, then shuffle. this.addAbility(new EntersBattlefieldTriggeredAbility( - new SearchLibraryPutInHandEffect(new TargetCardInLibrary(filter)) + new SearchLibraryPutInHandEffect(new TargetCardInLibrary(filter), true) )); // {1}{R}{W}: Level 2 @@ -58,7 +58,7 @@ public final class FighterClass extends CardImpl { // Equip abilities you activate cost {2} less to activate. this.addAbility(new SimpleStaticAbility(new GainClassAbilitySourceEffect( new AbilitiesCostReductionControllerEffect(EquipAbility.class, "Equip") - .setText("\"equip abilities you activate cost {2} less to activate\""), + .setText("equip abilities you activate cost {2} less to activate"), 2 ))); diff --git a/Mage.Sets/src/mage/cards/f/FindThePath.java b/Mage.Sets/src/mage/cards/f/FindThePath.java index b3d68b92d6..647f735b84 100644 --- a/Mage.Sets/src/mage/cards/f/FindThePath.java +++ b/Mage.Sets/src/mage/cards/f/FindThePath.java @@ -43,7 +43,7 @@ public final class FindThePath extends CardImpl { new SimpleManaAbility( Zone.BATTLEFIELD, new Mana(ManaType.GREEN, 2), new TapSourceCost() ), AttachmentType.AURA - ))); + ).setText("enchanted land has \"{T}: Add {G}{G}.\""))); } private FindThePath(final FindThePath card) { diff --git a/Mage.Sets/src/mage/cards/f/Flameskull.java b/Mage.Sets/src/mage/cards/f/Flameskull.java new file mode 100644 index 0000000000..7d3ac303d8 --- /dev/null +++ b/Mage.Sets/src/mage/cards/f/Flameskull.java @@ -0,0 +1,156 @@ +package mage.cards.f; + +import mage.MageInt; +import mage.MageObject; +import mage.MageObjectReference; +import mage.abilities.Ability; +import mage.abilities.common.CantBlockAbility; +import mage.abilities.common.DiesSourceTriggeredAbility; +import mage.abilities.effects.AsThoughEffectImpl; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.*; +import mage.constants.*; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.stack.Spell; +import mage.players.Player; +import mage.util.CardUtil; +import mage.watchers.Watcher; + +import java.util.*; + +/** + * @author TheElk801 + */ +public final class Flameskull extends CardImpl { + + public Flameskull(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{R}{R}"); + + this.subtype.add(SubType.SKELETON); + this.power = new MageInt(3); + this.toughness = new MageInt(1); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Flameskull can't block. + this.addAbility(new CantBlockAbility()); + + // Rejuvenation — When Flameskull dies, exile it. If you do, exile the top card of your library. Until the end of your next turn, you may play one of those cards. + this.addAbility(new DiesSourceTriggeredAbility(new FlameskullEffect()) + .withFlavorWord("Rejuventation"), new FlameskullWatcher()); + } + + private Flameskull(final Flameskull card) { + super(card); + } + + @Override + public Flameskull copy() { + return new Flameskull(this); + } +} + +class FlameskullEffect extends OneShotEffect { + + FlameskullEffect() { + super(Outcome.Benefit); + staticText = "exile it. If you do, exile the top card of your library. " + + "Until the end of your next turn, you may play one of those cards"; + } + + private FlameskullEffect(final FlameskullEffect effect) { + super(effect); + } + + @Override + public FlameskullEffect copy() { + return new FlameskullEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + MageObject sourceObject = source.getSourceObjectIfItStillExists(game); + if (player == null || !(sourceObject instanceof Card)) { + return false; + } + Cards cards = new CardsImpl(player.getLibrary().getFromTop(game)); + cards.add((Card) sourceObject); + player.moveCards(cards, Zone.EXILED, source, game); + game.addEffect(new FlameskullPlayEffect(cards, game), source); + return true; + } +} + +class FlameskullPlayEffect extends AsThoughEffectImpl { + + private final Set morSet = new HashSet<>(); + + FlameskullPlayEffect(Cards cards, Game game) { + super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.UntilEndOfYourNextTurn, Outcome.Benefit); + cards.stream() + .map(uuid -> new MageObjectReference(uuid, game)) + .forEach(morSet::add); + } + + private FlameskullPlayEffect(final FlameskullPlayEffect effect) { + super(effect); + this.morSet.addAll(effect.morSet); + } + + @Override + public FlameskullPlayEffect copy() { + return new FlameskullPlayEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + return true; + } + + @Override + public boolean applies(UUID sourceId, Ability source, UUID affectedControllerId, Game game) { + UUID objectIdToCast = CardUtil.getMainCardId(game, sourceId); + return source.isControlledBy(affectedControllerId) + && morSet.stream().anyMatch(mor -> mor.refersTo(objectIdToCast, game)) + && FlameskullWatcher.checkRef(source, morSet, game); + } +} + +class FlameskullWatcher extends Watcher { + + private final Map> morMap = new HashMap<>(); + private static final Set emptySet = new HashSet<>(); + + FlameskullWatcher() { + super(WatcherScope.GAME); + } + + @Override + public void watch(GameEvent event, Game game) { + if (event.getType() != GameEvent.EventType.SPELL_CAST + || event.getAdditionalReference() == null) { + return; + } + MageObjectReference mor = event.getAdditionalReference().getApprovingMageObjectReference(); + Spell spell = game.getSpell(event.getTargetId()); + if (mor == null || spell == null) { + return; + } + morMap.computeIfAbsent(mor, x -> new HashSet<>()) + .add(new MageObjectReference(spell.getMainCard(), game, -1)); + } + + static boolean checkRef(Ability source, Set morSet, Game game) { + FlameskullWatcher watcher = game.getState().getWatcher(FlameskullWatcher.class); + return watcher != null + && watcher + .morMap + .getOrDefault(new MageObjectReference(source.getSourceObject(game), game), emptySet) + .stream() + .noneMatch(morSet::contains); + } +} diff --git a/Mage.Sets/src/mage/cards/f/FungalRebirth.java b/Mage.Sets/src/mage/cards/f/FungalRebirth.java index 801d277449..3fc2a1b1aa 100644 --- a/Mage.Sets/src/mage/cards/f/FungalRebirth.java +++ b/Mage.Sets/src/mage/cards/f/FungalRebirth.java @@ -4,13 +4,13 @@ import mage.abilities.condition.common.MorbidCondition; import mage.abilities.decorator.ConditionalOneShotEffect; import mage.abilities.effects.common.CreateTokenEffect; import mage.abilities.effects.common.ReturnFromGraveyardToHandTargetEffect; +import mage.abilities.hint.common.MorbidHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.filter.StaticFilters; import mage.game.permanent.token.SaprolingToken; import mage.target.common.TargetCardInYourGraveyard; -import mage.watchers.common.MorbidWatcher; import java.util.UUID; @@ -27,12 +27,12 @@ public final class FungalRebirth extends CardImpl { getSpellAbility().addEffect( new ReturnFromGraveyardToHandTargetEffect().setText("Return target permanent card from your graveyard to your hand") ); - getSpellAbility().addWatcher(new MorbidWatcher()); getSpellAbility().addEffect(new ConditionalOneShotEffect( new CreateTokenEffect(new SaprolingToken(), 2), MorbidCondition.instance, "If a creature died this turn, create two 1/1 green Saproling creature tokens")); getSpellAbility().addTarget(new TargetCardInYourGraveyard(StaticFilters.FILTER_CARD_PERMANENT)); + getSpellAbility().addHint(MorbidHint.instance); } private FungalRebirth(final FungalRebirth card) { diff --git a/Mage.Sets/src/mage/cards/f/FunnelWebRecluse.java b/Mage.Sets/src/mage/cards/f/FunnelWebRecluse.java index 226041ff3d..2a4c822f59 100644 --- a/Mage.Sets/src/mage/cards/f/FunnelWebRecluse.java +++ b/Mage.Sets/src/mage/cards/f/FunnelWebRecluse.java @@ -5,6 +5,7 @@ import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.condition.common.MorbidCondition; import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; import mage.abilities.effects.keyword.InvestigateEffect; +import mage.abilities.hint.common.MorbidHint; import mage.abilities.keyword.ReachAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -34,7 +35,7 @@ public final class FunnelWebRecluse extends CardImpl { MorbidCondition.instance, "Morbid — When {this} enters the battlefield, " + "if a creature died this turn, investigate. (Create a colorless Clue artifact token " + "with \"{2}, Sacrifice this artifact: Draw a card.\")" - )); + ).addHint(MorbidHint.instance)); } private FunnelWebRecluse(final FunnelWebRecluse card) { diff --git a/Mage.Sets/src/mage/cards/g/GalvanicBlast.java b/Mage.Sets/src/mage/cards/g/GalvanicBlast.java index d0962d36bd..3f715e6070 100644 --- a/Mage.Sets/src/mage/cards/g/GalvanicBlast.java +++ b/Mage.Sets/src/mage/cards/g/GalvanicBlast.java @@ -31,7 +31,6 @@ public final class GalvanicBlast extends CardImpl { MetalcraftCondition.instance, effectText )); this.getSpellAbility().addTarget(new TargetAnyTarget()); - this.getSpellAbility().setAbilityWord(AbilityWord.METALCRAFT); this.getSpellAbility().addHint(MetalcraftHint.instance); } diff --git a/Mage.Sets/src/mage/cards/g/GelatinousCube.java b/Mage.Sets/src/mage/cards/g/GelatinousCube.java new file mode 100644 index 0000000000..e13ea9e46d --- /dev/null +++ b/Mage.Sets/src/mage/cards/g/GelatinousCube.java @@ -0,0 +1,106 @@ +package mage.cards.g; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.common.delayed.OnLeaveReturnExiledToBattlefieldAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.CreateDelayedTriggeredAbilityEffect; +import mage.abilities.effects.common.ExileUntilSourceLeavesEffect; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.filter.FilterCard; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterCreatureCard; +import mage.filter.common.FilterOpponentsCreaturePermanent; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.mageobject.ManaValuePredicate; +import mage.game.Game; +import mage.players.Player; +import mage.target.TargetPermanent; +import mage.target.common.TargetCardInExile; +import mage.target.targetadjustment.TargetAdjuster; +import mage.util.CardUtil; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class GelatinousCube extends CardImpl { + + private static final FilterPermanent filter = new FilterOpponentsCreaturePermanent("non-Ooze creature an opponent controls"); + + static { + filter.add(Predicates.not(SubType.OOZE.getPredicate())); + } + + public GelatinousCube(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{B}{B}"); + + this.subtype.add(SubType.OOZE); + this.power = new MageInt(4); + this.toughness = new MageInt(3); + + // Engulf — When Gelatinous Cube enters the battlefield, exile target non-Ooze creature an opponent controls until Gelatinous Cube leaves the battlefield. + Ability ability = new EntersBattlefieldTriggeredAbility(new ExileUntilSourceLeavesEffect(filter.getMessage())); + ability.addTarget(new TargetPermanent(filter)); + ability.addEffect(new CreateDelayedTriggeredAbilityEffect(new OnLeaveReturnExiledToBattlefieldAbility())); + this.addAbility(ability.withFlavorWord("Engulf")); + + // Dissolve — {X}{B}: Put target creature card with mana value X exiled with Gelatinous Cube into its owner's graveyard. + ability = new SimpleActivatedAbility(new GelatinousCubeEffect(), new ManaCostsImpl<>("{X}{B}")); + ability.setTargetAdjuster(GelatinousCubeAdjuster.instance); + this.addAbility(ability.withFlavorWord("Dissolve")); + } + + private GelatinousCube(final GelatinousCube card) { + super(card); + } + + @Override + public GelatinousCube copy() { + return new GelatinousCube(this); + } +} + +enum GelatinousCubeAdjuster implements TargetAdjuster { + instance; + + @Override + public void adjustTargets(Ability ability, Game game) { + ability.getTargets().clear(); + int xValue = ability.getManaCostsToPay().getX(); + FilterCard filter = new FilterCreatureCard("creature card with mana value " + xValue); + filter.add(new ManaValuePredicate(ComparisonType.EQUAL_TO, xValue)); + ability.addTarget(new TargetCardInExile(filter, CardUtil.getExileZoneId(game, ability))); + } +} + +class GelatinousCubeEffect extends OneShotEffect { + + GelatinousCubeEffect() { + super(Outcome.Benefit); + staticText = "put target creature card with mana value X exiled with {this} into its owner's graveyard"; + } + + private GelatinousCubeEffect(final GelatinousCubeEffect effect) { + super(effect); + } + + @Override + public GelatinousCubeEffect copy() { + return new GelatinousCubeEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + Card card = game.getCard(getTargetPointer().getFirst(game, source)); + return player != null && card != null && player.moveCards(card, Zone.GRAVEYARD, source, game); + } +} diff --git a/Mage.Sets/src/mage/cards/g/GhalmasWarden.java b/Mage.Sets/src/mage/cards/g/GhalmasWarden.java index 98f11973e5..b38ebfa651 100644 --- a/Mage.Sets/src/mage/cards/g/GhalmasWarden.java +++ b/Mage.Sets/src/mage/cards/g/GhalmasWarden.java @@ -18,7 +18,7 @@ import java.util.UUID; */ public final class GhalmasWarden extends CardImpl { - private static final String rule = "Metalcraft — Ghalma's Warden gets +2/+2 as long as you control three or more artifacts"; + private static final String rule = "{this} gets +2/+2 as long as you control three or more artifacts"; public GhalmasWarden(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{W}"); diff --git a/Mage.Sets/src/mage/cards/g/GoblinMorningstar.java b/Mage.Sets/src/mage/cards/g/GoblinMorningstar.java index 7d1364b6ec..fd14b0a394 100644 --- a/Mage.Sets/src/mage/cards/g/GoblinMorningstar.java +++ b/Mage.Sets/src/mage/cards/g/GoblinMorningstar.java @@ -33,7 +33,9 @@ public final class GoblinMorningstar extends CardImpl { // Equipped creature gets +1/+0 and has trample. Ability ability = new SimpleStaticAbility(new BoostEquippedEffect(1, 0)); - ability.addEffect(new GainAbilityAttachedEffect(TrampleAbility.getInstance(), AttachmentType.EQUIPMENT)); + ability.addEffect(new GainAbilityAttachedEffect( + TrampleAbility.getInstance(), AttachmentType.EQUIPMENT + ).setText("and has trample")); this.addAbility(ability); // Equip {1}{R} diff --git a/Mage.Sets/src/mage/cards/g/GravetillerWurm.java b/Mage.Sets/src/mage/cards/g/GravetillerWurm.java index 3c99e86d75..3ae7150b9f 100644 --- a/Mage.Sets/src/mage/cards/g/GravetillerWurm.java +++ b/Mage.Sets/src/mage/cards/g/GravetillerWurm.java @@ -7,9 +7,11 @@ import mage.abilities.common.EntersBattlefieldAbility; import mage.abilities.condition.common.MorbidCondition; import mage.abilities.decorator.ConditionalOneShotEffect; import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.abilities.hint.common.MorbidHint; import mage.abilities.keyword.TrampleAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; +import mage.constants.AbilityWord; import mage.constants.CardType; import mage.constants.SubType; import mage.counters.CounterType; @@ -30,7 +32,7 @@ public final class GravetillerWurm extends CardImpl { this.addAbility(TrampleAbility.getInstance()); // Morbid — Gravetiller Wurm enters the battlefield with four +1/+1 counters on it if a creature died this turn. this.addAbility(new EntersBattlefieldAbility(new ConditionalOneShotEffect(new AddCountersSourceEffect(CounterType.P1P1.createInstance(4)), - MorbidCondition.instance, ""), "with four +1/+1 counters on it if a creature died this turn")); + MorbidCondition.instance, ""), "with four +1/+1 counters on it if a creature died this turn").addHint(MorbidHint.instance).setAbilityWord(AbilityWord.MORBID)); } private GravetillerWurm(final GravetillerWurm card) { diff --git a/Mage.Sets/src/mage/cards/g/GreenDragon.java b/Mage.Sets/src/mage/cards/g/GreenDragon.java index 620d99f265..c05ff73f20 100644 --- a/Mage.Sets/src/mage/cards/g/GreenDragon.java +++ b/Mage.Sets/src/mage/cards/g/GreenDragon.java @@ -36,7 +36,7 @@ public final class GreenDragon extends CardImpl { // Poison Breath — When Green Dragon enters the battlefield, until end of turn, whenever a creature an opponent controls is dealt damage, destroy it. this.addAbility(new EntersBattlefieldTriggeredAbility(new CreateDelayedTriggeredAbilityEffect( new GreenDragonDelayedTriggeredAbility(), false - ))); + )).withFlavorWord("Poison Breath")); } private GreenDragon(final GreenDragon card) { diff --git a/Mage.Sets/src/mage/cards/g/GreenwheelLiberator.java b/Mage.Sets/src/mage/cards/g/GreenwheelLiberator.java index 7ced58150c..560f85dc0d 100644 --- a/Mage.Sets/src/mage/cards/g/GreenwheelLiberator.java +++ b/Mage.Sets/src/mage/cards/g/GreenwheelLiberator.java @@ -31,7 +31,7 @@ public final class GreenwheelLiberator extends CardImpl { // permanent you controlled left the battlefield this turn. Ability ability = new EntersBattlefieldAbility( new AddCountersSourceEffect(CounterType.P1P1.createInstance(2)), false, RevoltCondition.instance, - "Revolt — {this} enters the battlefield with two +1/+1 counters on it if a permanent you controlled left the battlefield this turn", null); + "Revolt — {this} enters the battlefield with two +1/+1 counters on it if a permanent you controlled left the battlefield this turn.", null); ability.addWatcher(new RevoltWatcher()); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/g/GrimHireling.java b/Mage.Sets/src/mage/cards/g/GrimHireling.java new file mode 100644 index 0000000000..0df709311e --- /dev/null +++ b/Mage.Sets/src/mage/cards/g/GrimHireling.java @@ -0,0 +1,85 @@ +package mage.cards.g; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.ActivateAsSorceryActivatedAbility; +import mage.abilities.common.ControlledCreaturesDealCombatDamagePlayerTriggeredAbility; +import mage.abilities.costs.common.SacrificeXTargetCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.dynamicvalue.common.GetXValue; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.continuous.BoostTargetEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.filter.common.FilterControlledPermanent; +import mage.game.Game; +import mage.game.permanent.token.TreasureToken; +import mage.target.common.TargetCreaturePermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class GrimHireling extends CardImpl { + + private static final FilterControlledPermanent filter + = new FilterControlledPermanent(SubType.TREASURE, "Treasures"); + + public GrimHireling(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{B}"); + + this.subtype.add(SubType.TIEFLING); + this.subtype.add(SubType.ROGUE); + this.power = new MageInt(3); + this.toughness = new MageInt(2); + + // Whenever one or more creatures you control deal combat damage to a player, create two Treasure tokens. + this.addAbility(new ControlledCreaturesDealCombatDamagePlayerTriggeredAbility( + new CreateTokenEffect(new TreasureToken(), 2) + )); + + // {B}, Sacrifice X Treasures: Target creature gets -X/-X until end of turn. Activate only as a sorcery. + Ability ability = new ActivateAsSorceryActivatedAbility(new GrimHirelingEffect(), new ManaCostsImpl<>("{B}")); + ability.addCost(new SacrificeXTargetCost(filter)); + ability.addTarget(new TargetCreaturePermanent()); + this.addAbility(ability); + } + + private GrimHireling(final GrimHireling card) { + super(card); + } + + @Override + public GrimHireling copy() { + return new GrimHireling(this); + } +} + +class GrimHirelingEffect extends OneShotEffect { + + GrimHirelingEffect() { + super(Outcome.Benefit); + staticText = "target creature gets -X/-X until end of turn"; + } + + private GrimHirelingEffect(final GrimHirelingEffect effect) { + super(effect); + } + + @Override + public GrimHirelingEffect copy() { + return new GrimHirelingEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + int xValue = GetXValue.instance.calculate(game, source, this); + game.addEffect(new BoostTargetEffect(-xValue, -xValue), source); + return true; + } +} diff --git a/Mage.Sets/src/mage/cards/g/GrimWanderer.java b/Mage.Sets/src/mage/cards/g/GrimWanderer.java index f579914e16..490d0ce9bd 100644 --- a/Mage.Sets/src/mage/cards/g/GrimWanderer.java +++ b/Mage.Sets/src/mage/cards/g/GrimWanderer.java @@ -4,6 +4,7 @@ import java.util.UUID; import mage.MageInt; import mage.abilities.common.CastOnlyIfConditionIsTrueAbility; import mage.abilities.condition.common.MorbidCondition; +import mage.abilities.hint.common.MorbidHint; import mage.constants.SubType; import mage.abilities.keyword.FlashAbility; import mage.cards.CardImpl; @@ -28,7 +29,7 @@ public final class GrimWanderer extends CardImpl { this.addAbility(FlashAbility.getInstance()); // Tragic Backstory — Cast this spell only if a creature died this turn. - this.addAbility(new CastOnlyIfConditionIsTrueAbility(MorbidCondition.instance).withFlavorWord("Tragic Backstory")); + this.addAbility(new CastOnlyIfConditionIsTrueAbility(MorbidCondition.instance).withFlavorWord("Tragic Backstory").addHint(MorbidHint.instance)); } private GrimWanderer(final GrimWanderer card) { diff --git a/Mage.Sets/src/mage/cards/g/GruesomeDiscovery.java b/Mage.Sets/src/mage/cards/g/GruesomeDiscovery.java index 7c3a9bde9d..87b6f5fcfb 100644 --- a/Mage.Sets/src/mage/cards/g/GruesomeDiscovery.java +++ b/Mage.Sets/src/mage/cards/g/GruesomeDiscovery.java @@ -5,6 +5,7 @@ import mage.abilities.condition.common.MorbidCondition; import mage.abilities.decorator.ConditionalOneShotEffect; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.discard.DiscardTargetEffect; +import mage.abilities.hint.common.MorbidHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.cards.CardsImpl; @@ -36,6 +37,7 @@ public final class GruesomeDiscovery extends CardImpl { "you choose two cards from it, then that player discards those cards" )); this.getSpellAbility().addTarget(new TargetPlayer()); + this.getSpellAbility().addHint(MorbidHint.instance); } private GruesomeDiscovery(final GruesomeDiscovery card) { diff --git a/Mage.Sets/src/mage/cards/g/GuildThief.java b/Mage.Sets/src/mage/cards/g/GuildThief.java index b66a7e2b43..c0b4636cc7 100644 --- a/Mage.Sets/src/mage/cards/g/GuildThief.java +++ b/Mage.Sets/src/mage/cards/g/GuildThief.java @@ -1,12 +1,9 @@ package mage.cards.g; import mage.MageInt; -import mage.abilities.Ability; import mage.abilities.common.DealsCombatDamageToAPlayerTriggeredAbility; -import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.costs.mana.ManaCostsImpl; -import mage.abilities.effects.common.DestroyTargetEffect; import mage.abilities.effects.common.combat.CantBeBlockedSourceEffect; import mage.abilities.effects.common.counter.AddCountersSourceEffect; import mage.cards.CardImpl; @@ -15,7 +12,6 @@ import mage.constants.CardType; import mage.constants.Duration; import mage.constants.SubType; import mage.counters.CounterType; -import mage.target.common.TargetCreaturePermanent; import java.util.UUID; @@ -34,7 +30,9 @@ public final class GuildThief extends CardImpl { // Whenever Guild Thief deals combat damage to a player, put a +1/+1 counter on it. this.addAbility(new DealsCombatDamageToAPlayerTriggeredAbility( - new AddCountersSourceEffect(CounterType.P1P1.createInstance(1)), false + new AddCountersSourceEffect(CounterType.P1P1.createInstance(1)) + .setText("put a +1/+1 counter on it"), + false )); // Cunning Action — {3}{U}: Guild Thief can't be blocked this turn. diff --git a/Mage.Sets/src/mage/cards/h/HeartlessPillage.java b/Mage.Sets/src/mage/cards/h/HeartlessPillage.java index 9c366f5a7d..0bb94d9f38 100644 --- a/Mage.Sets/src/mage/cards/h/HeartlessPillage.java +++ b/Mage.Sets/src/mage/cards/h/HeartlessPillage.java @@ -31,9 +31,8 @@ public final class HeartlessPillage extends CardImpl { this.getSpellAbility().addEffect(new ConditionalOneShotEffect( new CreateTokenEffect(new TreasureToken()), RaidCondition.instance, - "

Raid — If you attacked with a creature this turn, create a colorless Treasure artifact token with \"{T}, Sacrifice this artifact: Add one mana of any color.\"")); + "

Raid — If you attacked this turn, create a colorless Treasure artifact token with \"{T}, Sacrifice this artifact: Add one mana of any color.\"")); this.getSpellAbility().addWatcher(new PlayerAttackedWatcher()); - this.getSpellAbility().setAbilityWord(AbilityWord.RAID); this.getSpellAbility().addHint(RaidHint.instance); } diff --git a/Mage.Sets/src/mage/cards/h/HiddenStockpile.java b/Mage.Sets/src/mage/cards/h/HiddenStockpile.java index 3bca1b1a4e..9d50a5e0e5 100644 --- a/Mage.Sets/src/mage/cards/h/HiddenStockpile.java +++ b/Mage.Sets/src/mage/cards/h/HiddenStockpile.java @@ -32,7 +32,7 @@ public final class HiddenStockpile extends CardImpl { // Revolt — At the beginning of your end step, if a permanent you controlled left the battlefield this turn, create a 1/1 colorless Servo artifact creature token. Ability ability = new ConditionalInterveningIfTriggeredAbility(new BeginningOfYourEndStepTriggeredAbility(new CreateTokenEffect(new ServoToken()), false), RevoltCondition.instance, - "Revolt — At the beginning of your end step, if a permanent you controlled left the battlefield this turn, create a 1/1 colorless Servo artifact creature token"); + "Revolt — At the beginning of your end step, if a permanent you controlled left the battlefield this turn, create a 1/1 colorless Servo artifact creature token."); ability.setAbilityWord(AbilityWord.REVOLT); ability.addWatcher(new RevoltWatcher()); this.addAbility(ability); diff --git a/Mage.Sets/src/mage/cards/h/HollowhengeScavenger.java b/Mage.Sets/src/mage/cards/h/HollowhengeScavenger.java index d525206fd7..a65ab1f936 100644 --- a/Mage.Sets/src/mage/cards/h/HollowhengeScavenger.java +++ b/Mage.Sets/src/mage/cards/h/HollowhengeScavenger.java @@ -8,6 +8,7 @@ import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.condition.common.MorbidCondition; import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; import mage.abilities.effects.common.GainLifeEffect; +import mage.abilities.hint.common.MorbidHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -30,7 +31,7 @@ public final class HollowhengeScavenger extends CardImpl { // Morbid — When Hollowhenge Scavenger enters the battlefield, if a creature died this turn, you gain 5 life. TriggeredAbility ability = new EntersBattlefieldTriggeredAbility(new GainLifeEffect(5)); - this.addAbility(new ConditionalInterveningIfTriggeredAbility(ability, MorbidCondition.instance, staticText)); + this.addAbility(new ConditionalInterveningIfTriggeredAbility(ability, MorbidCondition.instance, staticText).addHint(MorbidHint.instance)); } private HollowhengeScavenger(final HollowhengeScavenger card) { diff --git a/Mage.Sets/src/mage/cards/h/HowlOfTheHorde.java b/Mage.Sets/src/mage/cards/h/HowlOfTheHorde.java index 011b9b4bc0..121d8d766b 100644 --- a/Mage.Sets/src/mage/cards/h/HowlOfTheHorde.java +++ b/Mage.Sets/src/mage/cards/h/HowlOfTheHorde.java @@ -38,10 +38,9 @@ public final class HowlOfTheHorde extends CardImpl { this.getSpellAbility().addEffect(new ConditionalOneShotEffect( new CreateDelayedTriggeredAbilityEffect(new HowlOfTheHordeDelayedTriggeredAbility()), RaidCondition.instance, - "

Raid — If you attacked with a creature this turn, when you cast your next instant or sorcery spell this turn, copy that spell an additional time. You may choose new targets for the copy.") + "

Raid — If you attacked this turn, when you cast your next instant or sorcery spell this turn, copy that spell an additional time. You may choose new targets for the copy.") ); this.getSpellAbility().addWatcher(new PlayerAttackedWatcher()); - this.getSpellAbility().setAbilityWord(AbilityWord.RAID); this.getSpellAbility().addHint(RaidHint.instance); } diff --git a/Mage.Sets/src/mage/cards/h/HungerOfTheHowlpack.java b/Mage.Sets/src/mage/cards/h/HungerOfTheHowlpack.java index fcc3a39a89..173ca7df69 100644 --- a/Mage.Sets/src/mage/cards/h/HungerOfTheHowlpack.java +++ b/Mage.Sets/src/mage/cards/h/HungerOfTheHowlpack.java @@ -5,6 +5,7 @@ import java.util.UUID; import mage.abilities.condition.common.MorbidCondition; import mage.abilities.decorator.ConditionalOneShotEffect; import mage.abilities.effects.common.counter.AddCountersTargetEffect; +import mage.abilities.hint.common.MorbidHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -28,8 +29,9 @@ public final class HungerOfTheHowlpack extends CardImpl { new AddCountersTargetEffect(CounterType.P1P1.createInstance(3)), new AddCountersTargetEffect(CounterType.P1P1.createInstance()), MorbidCondition.instance, - "Put a +1/+1 counter on target creature. Morbid — Put three +1/+1 counters on that creature instead if a creature died this turn")); + "Put a +1/+1 counter on target creature.
Morbid — Put three +1/+1 counters on that creature instead if a creature died this turn")); this.getSpellAbility().addTarget(new TargetCreaturePermanent()); + this.getSpellAbility().addHint(MorbidHint.instance); } private HungerOfTheHowlpack(final HungerOfTheHowlpack card) { diff --git a/Mage.Sets/src/mage/cards/i/IndomitableArchangel.java b/Mage.Sets/src/mage/cards/i/IndomitableArchangel.java index 2d2b06ae03..a89daa819a 100644 --- a/Mage.Sets/src/mage/cards/i/IndomitableArchangel.java +++ b/Mage.Sets/src/mage/cards/i/IndomitableArchangel.java @@ -21,7 +21,7 @@ import java.util.UUID; */ public final class IndomitableArchangel extends CardImpl { - private static final String rule = "Metalcraft — Artifacts you control have shroud as long as you control three or more artifacts."; + private static final String rule = "Artifacts you control have shroud as long as you control three or more artifacts."; private static final FilterPermanent filter = new FilterPermanent("Artifacts"); diff --git a/Mage.Sets/src/mage/cards/i/InexorableBlob.java b/Mage.Sets/src/mage/cards/i/InexorableBlob.java index 0500f61cef..1c708a27b9 100644 --- a/Mage.Sets/src/mage/cards/i/InexorableBlob.java +++ b/Mage.Sets/src/mage/cards/i/InexorableBlob.java @@ -29,8 +29,8 @@ public final class InexorableBlob extends CardImpl { // in your graveyard, create a 3/3 green Ooze creature token that’s tapped and attacking. this.addAbility(new ConditionalInterveningIfTriggeredAbility(new AttacksTriggeredAbility(new CreateTokenEffect(new InexorableBlobOozeToken(), 1, true, true), false), DeliriumCondition.instance, - "Delirium — Whenever {this} attacks and there are at least four card types among cards in your graveyard, " - + "create a 3/3 green Ooze creature token tapped and attacking.") + "Delirium — Whenever {this} attacks, if there are four or more card types among cards in your graveyard, " + + "create a 3/3 green Ooze creature token that's tapped and attacking.") .addHint(CardTypesInGraveyardHint.YOU)); } diff --git a/Mage.Sets/src/mage/cards/i/IronGolem.java b/Mage.Sets/src/mage/cards/i/IronGolem.java index e9c5921272..76bf1480d0 100644 --- a/Mage.Sets/src/mage/cards/i/IronGolem.java +++ b/Mage.Sets/src/mage/cards/i/IronGolem.java @@ -31,7 +31,7 @@ public final class IronGolem extends CardImpl { // Iron Golem attacks or blocks each combat if able. Ability ability = new SimpleStaticAbility(new AttacksIfAbleSourceEffect(Duration.WhileOnBattlefield).setText("{this} attacks")); - ability.addEffect(new BlocksIfAbleSourceEffect(Duration.WhileOnBattlefield).setText("or blocks each combat if able")); + ability.addEffect(new BlocksIfAbleSourceEffect(Duration.WhileOnBattlefield).setText("blocks each combat if able").concatBy("or")); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/k/KnowledgePool.java b/Mage.Sets/src/mage/cards/k/KnowledgePool.java index 06d3300b3d..c4af838462 100644 --- a/Mage.Sets/src/mage/cards/k/KnowledgePool.java +++ b/Mage.Sets/src/mage/cards/k/KnowledgePool.java @@ -9,6 +9,7 @@ import mage.abilities.effects.OneShotEffect; import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; +import mage.constants.AbilityWord; import mage.constants.CardType; import mage.constants.Outcome; import mage.constants.Zone; @@ -37,7 +38,7 @@ public final class KnowledgePool extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{6}"); // Imprint - When Knowledge Pool enters the battlefield, each player exiles the top three cards of their library - this.addAbility(new EntersBattlefieldTriggeredAbility(new KnowledgePoolEffect1(), false)); + this.addAbility(new EntersBattlefieldTriggeredAbility(new KnowledgePoolEffect1(), false).setAbilityWord(AbilityWord.IMPRINT)); // Whenever a player casts a spell from their hand, that player exiles it. If the player does, they may cast another nonland card exiled with Knowledge Pool without paying that card's mana cost. this.addAbility(new KnowledgePoolAbility()); diff --git a/Mage.Sets/src/mage/cards/l/LifeAndLimb.java b/Mage.Sets/src/mage/cards/l/LifeAndLimb.java index 4c1a1c1d39..c7ac029561 100644 --- a/Mage.Sets/src/mage/cards/l/LifeAndLimb.java +++ b/Mage.Sets/src/mage/cards/l/LifeAndLimb.java @@ -49,8 +49,9 @@ class LifeAndLimbEffect extends ContinuousEffectImpl { LifeAndLimbEffect() { super(Duration.WhileOnBattlefield, Outcome.Neutral); staticText = "All Forests and all Saprolings are 1/1 green Saproling creatures and Forest lands in addition to their other types"; - this.dependencyTypes.add(DependencyType.BecomeForest); - this.dependencyTypes.add(DependencyType.BecomeCreature); + + this.dependendToTypes.add(DependencyType.BecomeForest); + this.dependendToTypes.add(DependencyType.BecomeCreature); } LifeAndLimbEffect(final LifeAndLimbEffect effect) { diff --git a/Mage.Sets/src/mage/cards/l/LifeGoesOn.java b/Mage.Sets/src/mage/cards/l/LifeGoesOn.java index 134a50429f..316b92ad52 100644 --- a/Mage.Sets/src/mage/cards/l/LifeGoesOn.java +++ b/Mage.Sets/src/mage/cards/l/LifeGoesOn.java @@ -5,10 +5,10 @@ import java.util.UUID; import mage.abilities.condition.common.MorbidCondition; import mage.abilities.decorator.ConditionalOneShotEffect; import mage.abilities.effects.common.GainLifeEffect; +import mage.abilities.hint.common.MorbidHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.watchers.common.MorbidWatcher; /** * @@ -21,8 +21,8 @@ public final class LifeGoesOn extends CardImpl { // You gain 4 life. If a creature died this turn, you gain 8 life instead. - getSpellAbility().addWatcher(new MorbidWatcher()); getSpellAbility().addEffect(new ConditionalOneShotEffect(new GainLifeEffect(8), new GainLifeEffect(4), MorbidCondition.instance, "You gain 4 life. If a creature died this turn, you gain 8 life instead")); + this.getSpellAbility().addHint(MorbidHint.instance); } private LifeGoesOn(final LifeGoesOn card) { diff --git a/Mage.Sets/src/mage/cards/l/LifecraftCavalry.java b/Mage.Sets/src/mage/cards/l/LifecraftCavalry.java index bdba958ee0..d66cc678d5 100644 --- a/Mage.Sets/src/mage/cards/l/LifecraftCavalry.java +++ b/Mage.Sets/src/mage/cards/l/LifecraftCavalry.java @@ -37,7 +37,7 @@ public final class LifecraftCavalry extends CardImpl { new AddCountersSourceEffect(CounterType.P1P1.createInstance(2)), false, RevoltCondition.instance, - "Revolt — {this} enters the battlefield with two +1/+1 counter on it if a permanent you controlled left the battlefield this turn", null), + "Revolt — {this} enters the battlefield with two +1/+1 counters on it if a permanent you controlled left the battlefield this turn.", null), new RevoltWatcher() ); } diff --git a/Mage.Sets/src/mage/cards/l/LightfootRogue.java b/Mage.Sets/src/mage/cards/l/LightfootRogue.java index f646f20ac4..a88196de0d 100644 --- a/Mage.Sets/src/mage/cards/l/LightfootRogue.java +++ b/Mage.Sets/src/mage/cards/l/LightfootRogue.java @@ -56,7 +56,7 @@ public final class LightfootRogue extends CardImpl { 20, 20, new BoostSourceEffect( 3, 0, Duration.EndOfTurn - ).setText("it gets +1/+0"), + ).setText("it gets +3/+0"), new GainAbilitySourceEffect( FirstStrikeAbility.getInstance(), Duration.EndOfTurn ).setText("and gains first strike"), diff --git a/Mage.Sets/src/mage/cards/l/LilianasDevotee.java b/Mage.Sets/src/mage/cards/l/LilianasDevotee.java index 32a0236aeb..5ec2241310 100644 --- a/Mage.Sets/src/mage/cards/l/LilianasDevotee.java +++ b/Mage.Sets/src/mage/cards/l/LilianasDevotee.java @@ -9,6 +9,7 @@ import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; import mage.abilities.effects.common.CreateTokenEffect; import mage.abilities.effects.common.DoIfCostPaid; import mage.abilities.effects.common.continuous.BoostControlledEffect; +import mage.abilities.hint.common.MorbidHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -17,7 +18,6 @@ import mage.constants.SubType; import mage.constants.TargetController; import mage.filter.common.FilterCreaturePermanent; import mage.game.permanent.token.ZombieToken; -import mage.watchers.common.MorbidWatcher; import java.util.UUID; @@ -48,7 +48,7 @@ public final class LilianasDevotee extends CardImpl { ), TargetController.YOU, false), MorbidCondition.instance, "At the beginning of your end step, if a creature died this turn, " + "you may pay {1}{B}. If you do, create a 2/2 black Zombie creature token." - ), new MorbidWatcher()); + ).addHint(MorbidHint.instance)); } private LilianasDevotee(final LilianasDevotee card) { diff --git a/Mage.Sets/src/mage/cards/l/LilianasScrounger.java b/Mage.Sets/src/mage/cards/l/LilianasScrounger.java index e6a762f294..bea115fe3a 100644 --- a/Mage.Sets/src/mage/cards/l/LilianasScrounger.java +++ b/Mage.Sets/src/mage/cards/l/LilianasScrounger.java @@ -6,6 +6,7 @@ import mage.abilities.common.BeginningOfEndStepTriggeredAbility; import mage.abilities.condition.common.MorbidCondition; import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; import mage.abilities.effects.OneShotEffect; +import mage.abilities.hint.common.MorbidHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -19,7 +20,6 @@ import mage.game.Game; import mage.game.permanent.Permanent; import mage.players.Player; import mage.target.TargetPermanent; -import mage.watchers.common.MorbidWatcher; import java.util.UUID; @@ -41,7 +41,7 @@ public final class LilianasScrounger extends CardImpl { new LilianasScroungerEffect(), TargetController.ANY, false ), MorbidCondition.instance, "At the beginning of each end step, " + "if a creature died this turn, you may put a loyalty counter on a Liliana planeswalker you control." - ), new MorbidWatcher()); + ).addHint(MorbidHint.instance)); } private LilianasScrounger(final LilianasScrounger card) { diff --git a/Mage.Sets/src/mage/cards/l/LoathsomeTroll.java b/Mage.Sets/src/mage/cards/l/LoathsomeTroll.java index 6c24781cd0..11d7b68c40 100644 --- a/Mage.Sets/src/mage/cards/l/LoathsomeTroll.java +++ b/Mage.Sets/src/mage/cards/l/LoathsomeTroll.java @@ -39,7 +39,7 @@ public final class LoathsomeTroll extends CardImpl { )); // 10-19 | Return Loathsome Troll to your hand. - effect.addTableEntry(10, 19, new ReturnToHandSourceEffect().setText("retun {this} to your hand")); + effect.addTableEntry(10, 19, new ReturnToHandSourceEffect().setText("return {this} to your hand")); // 20 | Return Loathsome Troll to the battlefield tapped. effect.addTableEntry(20, 20, new ReturnSourceFromGraveyardToBattlefieldEffect(true) diff --git a/Mage.Sets/src/mage/cards/m/MalakirSoothsayer.java b/Mage.Sets/src/mage/cards/m/MalakirSoothsayer.java index 184bdfc754..3e05b674b7 100644 --- a/Mage.Sets/src/mage/cards/m/MalakirSoothsayer.java +++ b/Mage.Sets/src/mage/cards/m/MalakirSoothsayer.java @@ -42,7 +42,7 @@ public final class MalakirSoothsayer extends CardImpl { // Cohort — {T}, Tap an untapped Ally you control: You draw a card and you lose a life. SimpleActivatedAbility ability = new SimpleActivatedAbility(Zone.BATTLEFIELD, - new DrawCardSourceControllerEffect(1), + new DrawCardSourceControllerEffect(1).setText("you draw a card"), new TapSourceCost()); ability.setAbilityWord(AbilityWord.COHORT); ability.addCost(new TapTargetCost(new TargetControlledCreaturePermanent(1, 1, filter, false))); diff --git a/Mage.Sets/src/mage/cards/m/MaliciousAffliction.java b/Mage.Sets/src/mage/cards/m/MaliciousAffliction.java index 2922c55323..7f1534ab42 100644 --- a/Mage.Sets/src/mage/cards/m/MaliciousAffliction.java +++ b/Mage.Sets/src/mage/cards/m/MaliciousAffliction.java @@ -7,6 +7,7 @@ import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; import mage.abilities.effects.common.CastSourceTriggeredAbility; import mage.abilities.effects.common.CopySourceSpellEffect; import mage.abilities.effects.common.DestroyTargetEffect; +import mage.abilities.hint.common.MorbidHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -35,10 +36,10 @@ public final class MaliciousAffliction extends CardImpl { Ability ability = new ConditionalInterveningIfTriggeredAbility( new CastSourceTriggeredAbility(new CopySourceSpellEffect(), true), MorbidCondition.instance, "Morbid — When you cast this spell, " + - "if a creature died this turn, you may copy {this} and may choose a new target for the copy" + "if a creature died this turn, you may copy {this} and may choose a new target for the copy." ); ability.setRuleAtTheTop(true); - this.addAbility(ability); + this.addAbility(ability.addHint(MorbidHint.instance)); // Destroy target nonblack creature. this.getSpellAbility().addEffect(new DestroyTargetEffect()); diff --git a/Mage.Sets/src/mage/cards/m/MercadianAtlas.java b/Mage.Sets/src/mage/cards/m/MercadianAtlas.java index 113c9a3bf3..a8c2b1cfa3 100644 --- a/Mage.Sets/src/mage/cards/m/MercadianAtlas.java +++ b/Mage.Sets/src/mage/cards/m/MercadianAtlas.java @@ -58,7 +58,7 @@ enum MercadianAtlasCondition implements Condition { @Override public String toString() { - return "{this} is attacking"; + return "you didn't play a land this turn"; } } diff --git a/Mage.Sets/src/mage/cards/m/MidnightPathlighter.java b/Mage.Sets/src/mage/cards/m/MidnightPathlighter.java new file mode 100644 index 0000000000..a4d2c8773a --- /dev/null +++ b/Mage.Sets/src/mage/cards/m/MidnightPathlighter.java @@ -0,0 +1,58 @@ +package mage.cards.m; + +import mage.MageInt; +import mage.abilities.common.ControlledCreaturesDealCombatDamagePlayerTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.common.combat.CantBeBlockedByCreaturesAllEffect; +import mage.abilities.effects.keyword.VentureIntoTheDungeonEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.filter.StaticFilters; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.Predicates; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class MidnightPathlighter extends CardImpl { + + private static final FilterCreaturePermanent filter + = new FilterCreaturePermanent("except by legendary creatures"); + + static { + filter.add(Predicates.not(SuperType.LEGENDARY.getPredicate())); + } + + public MidnightPathlighter(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{W}{U}"); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.WIZARD); + this.power = new MageInt(2); + this.toughness = new MageInt(3); + + // Creatures you control can't be blocked except by legendary creatures. + this.addAbility(new SimpleStaticAbility(new CantBeBlockedByCreaturesAllEffect( + StaticFilters.FILTER_PERMANENT_CREATURES_CONTROLLED, + filter, Duration.WhileOnBattlefield + ))); + + // Whenever one or more creatures you control deal combat damage to a player, venture into the dungeon. + this.addAbility(new ControlledCreaturesDealCombatDamagePlayerTriggeredAbility(new VentureIntoTheDungeonEffect())); + } + + private MidnightPathlighter(final MidnightPathlighter card) { + super(card); + } + + @Override + public MidnightPathlighter copy() { + return new MidnightPathlighter(this); + } +} diff --git a/Mage.Sets/src/mage/cards/m/MightBeyondReason.java b/Mage.Sets/src/mage/cards/m/MightBeyondReason.java index a4f1bef316..eced211738 100644 --- a/Mage.Sets/src/mage/cards/m/MightBeyondReason.java +++ b/Mage.Sets/src/mage/cards/m/MightBeyondReason.java @@ -27,7 +27,7 @@ public final class MightBeyondReason extends CardImpl { new AddCountersTargetEffect(CounterType.P1P1.createInstance(2)), DeliriumCondition.instance, "Put two +1/+1 counter on target creature.
" - + "Delirium — Put three +1/+1 counter on that creature instead if there are four or more card types among cards in your graveyard" + + "Delirium — Put three +1/+1 counters on that creature instead if there are four or more card types among cards in your graveyard" )); getSpellAbility().addTarget(new TargetCreaturePermanent()); getSpellAbility().addHint(CardTypesInGraveyardHint.YOU); diff --git a/Mage.Sets/src/mage/cards/m/MimicVat.java b/Mage.Sets/src/mage/cards/m/MimicVat.java index 116f55e005..a951e44e4b 100644 --- a/Mage.Sets/src/mage/cards/m/MimicVat.java +++ b/Mage.Sets/src/mage/cards/m/MimicVat.java @@ -17,6 +17,7 @@ import mage.abilities.effects.common.ExileTargetEffect; import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; +import mage.constants.AbilityWord; import mage.constants.CardType; import mage.constants.Outcome; import mage.constants.Zone; @@ -102,7 +103,7 @@ class MimicVatTriggeredAbility extends TriggeredAbilityImpl { @Override public String getRule() { - return "Whenever a nontoken creature dies, you may exile that card. If you do, return each other card exiled with {this} to its owner's graveyard."; + return AbilityWord.IMPRINT.formatWord() + "Whenever a nontoken creature dies, you may exile that card. If you do, return each other card exiled with {this} to its owner's graveyard."; } } diff --git a/Mage.Sets/src/mage/cards/m/MirranMettle.java b/Mage.Sets/src/mage/cards/m/MirranMettle.java index 77beffebf7..b87643552f 100644 --- a/Mage.Sets/src/mage/cards/m/MirranMettle.java +++ b/Mage.Sets/src/mage/cards/m/MirranMettle.java @@ -19,7 +19,7 @@ import java.util.UUID; */ public final class MirranMettle extends CardImpl { - private static final String effectText = "Metalcraft — That creature gets +4/+4 until end of turn instead if you control three or more artifacts."; + private static final String effectText = "
Metalcraft — That creature gets +4/+4 until end of turn instead if you control three or more artifacts."; public MirranMettle(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{G}"); @@ -31,7 +31,6 @@ public final class MirranMettle extends CardImpl { // Metalcraft — That creature gets +4/+4 until end of turn instead if you control three or more artifacts. this.getSpellAbility().addEffect(new ConditionalContinuousEffect(new BoostTargetEffect(2, 2, Duration.EndOfTurn), new LockedInCondition(MetalcraftCondition.instance), effectText)); - this.getSpellAbility().setAbilityWord(AbilityWord.METALCRAFT); this.getSpellAbility().addHint(MetalcraftHint.instance); } diff --git a/Mage.Sets/src/mage/cards/m/MoltenPsyche.java b/Mage.Sets/src/mage/cards/m/MoltenPsyche.java index bbec0aa34e..ac66622c24 100644 --- a/Mage.Sets/src/mage/cards/m/MoltenPsyche.java +++ b/Mage.Sets/src/mage/cards/m/MoltenPsyche.java @@ -29,7 +29,6 @@ public final class MoltenPsyche extends CardImpl { // Metalcraft — If you control three or more artifacts, Molten Psyche deals damage to each opponent equal to the number of cards that player has drawn this turn. this.getSpellAbility().addEffect(new MoltenPsycheEffect()); this.getSpellAbility().addWatcher(new MoltenPsycheWatcher()); - this.getSpellAbility().setAbilityWord(AbilityWord.METALCRAFT); this.getSpellAbility().addHint(MetalcraftHint.instance); } diff --git a/Mage.Sets/src/mage/cards/m/MonkClass.java b/Mage.Sets/src/mage/cards/m/MonkClass.java new file mode 100644 index 0000000000..b603c85c54 --- /dev/null +++ b/Mage.Sets/src/mage/cards/m/MonkClass.java @@ -0,0 +1,155 @@ +package mage.cards.m; + +import mage.abilities.Ability; +import mage.abilities.common.BecomesClassLevelTriggeredAbility; +import mage.abilities.common.BeginningOfUpkeepTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.condition.Condition; +import mage.abilities.decorator.ConditionalCostModificationEffect; +import mage.abilities.effects.AsThoughEffectImpl; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.ReturnToHandTargetEffect; +import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; +import mage.abilities.effects.common.continuous.GainClassAbilitySourceEffect; +import mage.abilities.effects.common.cost.SpellsCostReductionControllerEffect; +import mage.abilities.keyword.ClassLevelAbility; +import mage.abilities.keyword.ClassReminderAbility; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.players.Player; +import mage.target.common.TargetNonlandPermanent; +import mage.target.targetpointer.FixedTarget; +import mage.watchers.common.SpellsCastWatcher; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class MonkClass extends CardImpl { + + public MonkClass(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{W}{U}"); + + this.subtype.add(SubType.CLASS); + + // (Gain the next level as a sorcery to add its ability.) + this.addAbility(new ClassReminderAbility()); + + // The second spell you cast each turn costs {1} less to cast. + this.addAbility(new SimpleStaticAbility(new ConditionalCostModificationEffect( + new SpellsCostReductionControllerEffect(StaticFilters.FILTER_CARD, 1), + MonkClassCondition.instance, "the second spell you cast each turn costs {1} less to cast" + )), new SpellsCastWatcher()); + + // {W}{U}: Level 2 + this.addAbility(new ClassLevelAbility(2, "{W}{U}")); + + // When this Class becomes level 2, return up to one target nonland permanent to its owner's hand. + Ability ability = new BecomesClassLevelTriggeredAbility(new ReturnToHandTargetEffect(), 2); + ability.addTarget(new TargetNonlandPermanent(0, 1, false)); + this.addAbility(ability); + + // {1}{W}{U}: Level 3 + this.addAbility(new ClassLevelAbility(3, "{1}{W}{U}")); + + // At the beginning of your upkeep, exile the top card of your library. For as long as it remains exiled, it has "You may cast this card from exile as long as you've cast another spell this turn." + this.addAbility(new SimpleStaticAbility(new GainClassAbilitySourceEffect( + new BeginningOfUpkeepTriggeredAbility( + new MonkClassEffect(), TargetController.YOU, false + ), 3 + ))); + } + + private MonkClass(final MonkClass card) { + super(card); + } + + @Override + public MonkClass copy() { + return new MonkClass(this); + } +} + +enum MonkClassCondition implements Condition { + instance; + + @Override + public boolean apply(Game game, Ability source) { + SpellsCastWatcher watcher = game.getState().getWatcher(SpellsCastWatcher.class); + return watcher != null && watcher.getSpellsCastThisTurn(source.getControllerId()).size() == 1; + } +} + +class MonkClassEffect extends OneShotEffect { + + MonkClassEffect() { + super(Outcome.Benefit); + staticText = "exile the top card of your library. For as long as it remains exiled, " + + "it has \"You may cast this card from exile as long as you've cast another spell this turn.\""; + } + + private MonkClassEffect(final MonkClassEffect effect) { + super(effect); + } + + @Override + public MonkClassEffect copy() { + return new MonkClassEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + Card card = player.getLibrary().getFromTop(game); + if (card == null) { + return false; + } + player.moveCards(card, Zone.EXILED, source, game); + game.addEffect(new GainAbilityTargetEffect( + new SimpleStaticAbility(new MonkClassCastEffect()), + Duration.Custom, null, true + ).setTargetPointer(new FixedTarget(card, game)), source); + return true; + } +} + +class MonkClassCastEffect extends AsThoughEffectImpl { + + MonkClassCastEffect() { + super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.Custom, Outcome.Benefit); + staticText = "you may cast this card from exile as long as you've cast another spell this turn"; + } + + private MonkClassCastEffect(final MonkClassCastEffect effect) { + super(effect); + } + + @Override + public boolean applies(UUID sourceId, Ability source, UUID affectedControllerId, Game game) { + if (!sourceId.equals(source.getSourceId()) || !source.isControlledBy(affectedControllerId)) { + return false; + } + Card card = game.getCard(source.getSourceId()); + SpellsCastWatcher watcher = game.getState().getWatcher(SpellsCastWatcher.class); + return card != null && watcher != null + && watcher.getSpellsCastThisTurn(affectedControllerId).size() > 0; + } + + @Override + public MonkClassCastEffect copy() { + return new MonkClassCastEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + return true; + } +} diff --git a/Mage.Sets/src/mage/cards/m/MorkrutBanshee.java b/Mage.Sets/src/mage/cards/m/MorkrutBanshee.java index d03414edf6..34b0d72ef0 100644 --- a/Mage.Sets/src/mage/cards/m/MorkrutBanshee.java +++ b/Mage.Sets/src/mage/cards/m/MorkrutBanshee.java @@ -8,6 +8,7 @@ import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.condition.common.MorbidCondition; import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; import mage.abilities.effects.common.continuous.BoostTargetEffect; +import mage.abilities.hint.common.MorbidHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -34,7 +35,7 @@ public final class MorkrutBanshee extends CardImpl { TriggeredAbility triggeredAbility = new EntersBattlefieldTriggeredAbility(new BoostTargetEffect(-4, -4, Duration.EndOfTurn)); TriggeredAbility ability = new ConditionalInterveningIfTriggeredAbility(triggeredAbility, MorbidCondition.instance, staticText); ability.addTarget(new TargetCreaturePermanent()); - this.addAbility(ability); + this.addAbility(ability.addHint(MorbidHint.instance)); } private MorkrutBanshee(final MorkrutBanshee card) { diff --git a/Mage.Sets/src/mage/cards/m/MyrWelder.java b/Mage.Sets/src/mage/cards/m/MyrWelder.java index 77ca9e7042..d8b71b1512 100644 --- a/Mage.Sets/src/mage/cards/m/MyrWelder.java +++ b/Mage.Sets/src/mage/cards/m/MyrWelder.java @@ -12,13 +12,7 @@ import mage.abilities.effects.OneShotEffect; import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.CardType; -import mage.constants.Duration; -import mage.constants.Layer; -import mage.constants.Outcome; -import mage.constants.SubLayer; -import mage.constants.SubType; -import mage.constants.Zone; +import mage.constants.*; import mage.filter.common.FilterArtifactCard; import mage.game.Game; import mage.game.permanent.Permanent; @@ -39,7 +33,7 @@ public final class MyrWelder extends CardImpl { // Imprint - {tap}: Exile target artifact card from a graveyard SimpleActivatedAbility ability = new SimpleActivatedAbility(Zone.BATTLEFIELD, new MyrWelderEffect(), new TapSourceCost()); ability.addTarget(new TargetCardInGraveyard(new FilterArtifactCard("artifact card from a graveyard"))); - this.addAbility(ability); + this.addAbility(ability.setAbilityWord(AbilityWord.IMPRINT)); // Myr Welder has all activated abilities of all cards exiled with it this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new MyrWelderContinuousEffect())); diff --git a/Mage.Sets/src/mage/cards/o/ObNixilisTheFallen.java b/Mage.Sets/src/mage/cards/o/ObNixilisTheFallen.java index fc2ec9a3c0..3a68864fa3 100644 --- a/Mage.Sets/src/mage/cards/o/ObNixilisTheFallen.java +++ b/Mage.Sets/src/mage/cards/o/ObNixilisTheFallen.java @@ -32,7 +32,7 @@ public final class ObNixilisTheFallen extends CardImpl { // Landfall - Whenever a land enters the battlefield under your control, you may have target player lose 3 life. // If you do, put three +1/+1 counters on Ob Nixilis, the Fallen. Ability ability = new LandfallAbility(new LoseLifeTargetEffect(3), true); - ability.addEffect(new AddCountersSourceEffect(CounterType.P1P1.createInstance(3))); + ability.addEffect(new AddCountersSourceEffect(CounterType.P1P1.createInstance(3)).concatBy("If you do,")); ability.addTarget(new TargetPlayer()); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/o/OchreJelly.java b/Mage.Sets/src/mage/cards/o/OchreJelly.java new file mode 100644 index 0000000000..131240fe55 --- /dev/null +++ b/Mage.Sets/src/mage/cards/o/OchreJelly.java @@ -0,0 +1,107 @@ +package mage.cards.o; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.DiesSourceTriggeredAbility; +import mage.abilities.common.EntersBattlefieldAbility; +import mage.abilities.common.delayed.AtTheBeginOfNextEndStepDelayedTriggeredAbility; +import mage.abilities.condition.Condition; +import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.CreateDelayedTriggeredAbilityEffect; +import mage.abilities.effects.common.CreateTokenCopyTargetEffect; +import mage.abilities.effects.common.EntersBattlefieldWithXCountersEffect; +import mage.abilities.keyword.TrampleAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.counters.CounterType; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.util.CardUtil; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class OchreJelly extends CardImpl { + + public OchreJelly(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{X}{G}"); + + this.subtype.add(SubType.OOZE); + this.power = new MageInt(0); + this.toughness = new MageInt(0); + + // Trample + this.addAbility(TrampleAbility.getInstance()); + + // Ochre Jelly enters the battlefield with X +1/+1 counters on it. + this.addAbility(new EntersBattlefieldAbility(new EntersBattlefieldWithXCountersEffect(CounterType.P1P1.createInstance()))); + + // Split — When Ochre Jelly dies, if it had two or more +1/+1 counters on it, create a token that's a copy of it at the beginning of the next end step. That token enters the battlefield with half that many +1/+1 counters on it, rounded down. + this.addAbility(new ConditionalInterveningIfTriggeredAbility( + new DiesSourceTriggeredAbility(new CreateDelayedTriggeredAbilityEffect( + new AtTheBeginOfNextEndStepDelayedTriggeredAbility(new OchreJellyEffect()) + )), OchreJellyCondition.instance, CardUtil.italicizeWithEmDash("Split") + + "When {this} dies, if it had two or more +1/+1 counters on it, " + + "create a token that's a copy of it at the beginning of the next end step. " + + "That token enters the battlefield with half that many +1/+1 counters on it, rounded down." + )); + } + + private OchreJelly(final OchreJelly card) { + super(card); + } + + @Override + public OchreJelly copy() { + return new OchreJelly(this); + } +} + +enum OchreJellyCondition implements Condition { + instance; + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = (Permanent) source.getEffects().get(0).getValue("permanentLeftBattlefield"); + return permanent != null && permanent.getCounters(game).getCount(CounterType.P1P1) >= 2; + } +} + +class OchreJellyEffect extends OneShotEffect { + + OchreJellyEffect() { + super(Outcome.Benefit); + staticText = "create a token that's a copy of {this}"; + } + + private OchreJellyEffect(final OchreJellyEffect effect) { + super(effect); + } + + @Override + public OchreJellyEffect copy() { + return new OchreJellyEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = (Permanent) getValue("permanentLeftBattlefield"); + if (permanent == null) { + return false; + } + CreateTokenCopyTargetEffect effect = new CreateTokenCopyTargetEffect(); + effect.setSavedPermanent(permanent); + effect.apply(game, source); + int counters = permanent.getCounters(game).getCount(CounterType.P1P1) / 2; + for (Permanent token : effect.getAddedPermanent()) { + permanent.addCounters(CounterType.P1P1.createInstance(counters), source.getControllerId(), source, game); + } + return true; + } +} diff --git a/Mage.Sets/src/mage/cards/o/OsaiVultures.java b/Mage.Sets/src/mage/cards/o/OsaiVultures.java index 0a5fee5a14..598965b596 100644 --- a/Mage.Sets/src/mage/cards/o/OsaiVultures.java +++ b/Mage.Sets/src/mage/cards/o/OsaiVultures.java @@ -10,6 +10,7 @@ import mage.abilities.costs.common.RemoveCountersSourceCost; import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; import mage.abilities.effects.common.continuous.BoostSourceEffect; import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.abilities.hint.common.MorbidHint; import mage.abilities.keyword.FlyingAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -37,7 +38,7 @@ public final class OsaiVultures extends CardImpl { // At the beginning of each end step, if a creature died this turn, put a carrion counter on Osai Vultures. this.addAbility(new ConditionalInterveningIfTriggeredAbility(new BeginningOfEndStepTriggeredAbility( new AddCountersSourceEffect(CounterType.CARRION.createInstance()), TargetController.ANY, false), MorbidCondition.instance, - "At the beginning of each end step, if a creature died this turn, put a carrion counter on {this}.")); + "At the beginning of each end step, if a creature died this turn, put a carrion counter on {this}.").addHint(MorbidHint.instance)); // Remove two carrion counters from Osai Vultures: Osai Vultures gets +1/+1 until end of turn. this.addAbility(new SimpleActivatedAbility(Zone.BATTLEFIELD, new BoostSourceEffect(1, 1, Duration.EndOfTurn), new RemoveCountersSourceCost(CounterType.CARRION.createInstance(2)))); diff --git a/Mage.Sets/src/mage/cards/o/OswaldFiddlebender.java b/Mage.Sets/src/mage/cards/o/OswaldFiddlebender.java index 9580b0370e..2aee9ae25a 100644 --- a/Mage.Sets/src/mage/cards/o/OswaldFiddlebender.java +++ b/Mage.Sets/src/mage/cards/o/OswaldFiddlebender.java @@ -46,7 +46,7 @@ public final class OswaldFiddlebender extends CardImpl { ability.addCost(new SacrificeTargetCost(new TargetControlledPermanent( StaticFilters.FILTER_CONTROLLED_PERMANENT_ARTIFACT_AN ))); - this.addAbility(ability); + this.addAbility(ability.withFlavorWord("Magical Tinkering")); } private OswaldFiddlebender(final OswaldFiddlebender card) { diff --git a/Mage.Sets/src/mage/cards/p/PainfulTruths.java b/Mage.Sets/src/mage/cards/p/PainfulTruths.java index 78ddc7b4bb..9064c4f8d8 100644 --- a/Mage.Sets/src/mage/cards/p/PainfulTruths.java +++ b/Mage.Sets/src/mage/cards/p/PainfulTruths.java @@ -26,7 +26,7 @@ public final class PainfulTruths extends CardImpl { effect.setText("You draw X cards"); getSpellAbility().addEffect(effect); effect = new LoseLifeSourceControllerEffect(ColorsOfManaSpentToCastCount.getInstance()); - effect.setText("and lose X life, where X is the number of colors of mana spent to cast this spell"); + effect.setText("and you lose X life, where X is the number of colors of mana spent to cast this spell"); getSpellAbility().addEffect(effect); } diff --git a/Mage.Sets/src/mage/cards/p/PanopticMirror.java b/Mage.Sets/src/mage/cards/p/PanopticMirror.java index 8faeea8f0d..ee1aabaccf 100644 --- a/Mage.Sets/src/mage/cards/p/PanopticMirror.java +++ b/Mage.Sets/src/mage/cards/p/PanopticMirror.java @@ -32,7 +32,7 @@ public final class PanopticMirror extends CardImpl { // Imprint - {X}, {tap}: You may exile an instant or sorcery card with converted mana cost X from your hand. Ability ability = new SimpleActivatedAbility(Zone.BATTLEFIELD, new PanopticMirrorExileEffect(), new VariableManaCost()); ability.addCost(new TapSourceCost()); - this.addAbility(ability); + this.addAbility(ability.setAbilityWord(AbilityWord.IMPRINT)); // At the beginning of your upkeep, you may copy a card exiled with Panoptic Mirror. If you do, you may cast the copy without paying its mana cost. this.addAbility(new BeginningOfUpkeepTriggeredAbility(new PanopticMirrorCastEffect(), TargetController.YOU, true)); } diff --git a/Mage.Sets/src/mage/cards/p/PathOfAncestry.java b/Mage.Sets/src/mage/cards/p/PathOfAncestry.java index aea19fa06c..6350f97c25 100644 --- a/Mage.Sets/src/mage/cards/p/PathOfAncestry.java +++ b/Mage.Sets/src/mage/cards/p/PathOfAncestry.java @@ -12,6 +12,7 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.CommanderCardType; import mage.constants.Duration; +import mage.constants.Zone; import mage.game.Game; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; @@ -93,7 +94,7 @@ class PathOfAncestryTriggeredAbility extends DelayedTriggeredAbility { } // share creature type with commander - for (Card commander : game.getCommanderCardsFromAnyZones(player, CommanderCardType.COMMANDER_OR_OATHBREAKER)) { + for (Card commander : game.getCommanderCardsFromAnyZones(player, CommanderCardType.COMMANDER_OR_OATHBREAKER, Zone.ALL)) { if (spell.getCard().shareCreatureTypes(game, commander)) { return true; } diff --git a/Mage.Sets/src/mage/cards/p/PhyrexianIngester.java b/Mage.Sets/src/mage/cards/p/PhyrexianIngester.java index 333c7b2efd..770ced6731 100644 --- a/Mage.Sets/src/mage/cards/p/PhyrexianIngester.java +++ b/Mage.Sets/src/mage/cards/p/PhyrexianIngester.java @@ -12,13 +12,7 @@ import mage.abilities.effects.OneShotEffect; import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.CardType; -import mage.constants.SubType; -import mage.constants.Duration; -import mage.constants.Layer; -import mage.constants.Outcome; -import mage.constants.SubLayer; -import mage.constants.Zone; +import mage.constants.*; import mage.filter.common.FilterCreaturePermanent; import mage.filter.predicate.Predicates; import mage.filter.predicate.permanent.TokenPredicate; @@ -50,7 +44,7 @@ public final class PhyrexianIngester extends CardImpl { // Imprint - When Phyrexian Ingester enters the battlefield, you may exile target nontoken creature. Ability ability = new EntersBattlefieldTriggeredAbility(new PhyrexianIngesterImprintEffect(), true); ability.addTarget(new TargetPermanent(filter)); - this.addAbility(ability); + this.addAbility(ability.setAbilityWord(AbilityWord.IMPRINT)); // Phyrexian Ingester gets +X/+Y, where X is the exiled creature card's power and Y is its toughness. this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new PhyrexianIngesterBoostEffect())); } diff --git a/Mage.Sets/src/mage/cards/p/PowerOfPersuasion.java b/Mage.Sets/src/mage/cards/p/PowerOfPersuasion.java index e707e31a30..3e112cb30f 100644 --- a/Mage.Sets/src/mage/cards/p/PowerOfPersuasion.java +++ b/Mage.Sets/src/mage/cards/p/PowerOfPersuasion.java @@ -58,7 +58,7 @@ class PowerOfPersuasionEffect extends OneShotEffect { PowerOfPersuasionEffect() { super(Outcome.Benefit); - staticText = "its owner puts it on the top of bottom of their library"; + staticText = "its owner puts it on the top or bottom of their library"; } private PowerOfPersuasionEffect(final PowerOfPersuasionEffect effect) { diff --git a/Mage.Sets/src/mage/cards/p/PredatorsHowl.java b/Mage.Sets/src/mage/cards/p/PredatorsHowl.java index 666c85e872..d7bd4084e3 100644 --- a/Mage.Sets/src/mage/cards/p/PredatorsHowl.java +++ b/Mage.Sets/src/mage/cards/p/PredatorsHowl.java @@ -6,6 +6,7 @@ import mage.abilities.condition.common.MorbidCondition; import mage.abilities.decorator.ConditionalOneShotEffect; import mage.abilities.effects.Effect; import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.hint.common.MorbidHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -28,6 +29,7 @@ public final class PredatorsHowl extends CardImpl { MorbidCondition.instance, "Create a 2/2 green Wolf creature token.

Morbid — Create three 2/2 green Wolf creature tokens instead if a creature died this turn."); this.getSpellAbility().addEffect(effect); + this.getSpellAbility().addHint(MorbidHint.instance); } private PredatorsHowl(final PredatorsHowl card) { diff --git a/Mage.Sets/src/mage/cards/p/ProsperTomeBound.java b/Mage.Sets/src/mage/cards/p/ProsperTomeBound.java new file mode 100644 index 0000000000..30bb123887 --- /dev/null +++ b/Mage.Sets/src/mage/cards/p/ProsperTomeBound.java @@ -0,0 +1,87 @@ +package mage.cards.p; + +import mage.MageInt; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.BeginningOfEndStepTriggeredAbility; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.ExileTopXMayPlayUntilEndOfTurnEffect; +import mage.abilities.keyword.DeathtouchAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.permanent.token.TreasureToken; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class ProsperTomeBound extends CardImpl { + + public ProsperTomeBound(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{B}{R}"); + + this.addSuperType(SuperType.LEGENDARY); + this.subtype.add(SubType.TIEFLING); + this.subtype.add(SubType.WARLOCK); + this.power = new MageInt(1); + this.toughness = new MageInt(4); + + // Deathtouch + this.addAbility(DeathtouchAbility.getInstance()); + + // Mystic Arcanum — At the beginning of your end step, exile the top card of your library. Until the end of your next turn, you may play that card. + this.addAbility(new BeginningOfEndStepTriggeredAbility( + new ExileTopXMayPlayUntilEndOfTurnEffect( + 1, false, Duration.UntilEndOfYourNextTurn + ), TargetController.YOU, false + ).withFlavorWord("Mystic Arcanum")); + + // Pact Boon — Whenever you play a card from exile, create a Treasure token. + this.addAbility(new ProsperTomeBoundTriggeredAbility()); + } + + private ProsperTomeBound(final ProsperTomeBound card) { + super(card); + } + + @Override + public ProsperTomeBound copy() { + return new ProsperTomeBound(this); + } +} + +class ProsperTomeBoundTriggeredAbility extends TriggeredAbilityImpl { + + ProsperTomeBoundTriggeredAbility() { + super(Zone.BATTLEFIELD, new CreateTokenEffect(new TreasureToken())); + this.flavorWord = "Pact Boon"; + } + + private ProsperTomeBoundTriggeredAbility(final ProsperTomeBoundTriggeredAbility ability) { + super(ability); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.SPELL_CAST + || event.getType() == GameEvent.EventType.LAND_PLAYED; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + return isControlledBy(event.getPlayerId()) && event.getZone() == Zone.EXILED; + } + + @Override + public ProsperTomeBoundTriggeredAbility copy() { + return new ProsperTomeBoundTriggeredAbility(this); + } + + @Override + public String getTriggerPhrase() { + return "Whenever you play a card from exile, "; + } +} diff --git a/Mage.Sets/src/mage/cards/p/ProteanRaider.java b/Mage.Sets/src/mage/cards/p/ProteanRaider.java index 46674dac3b..b65252b7ee 100644 --- a/Mage.Sets/src/mage/cards/p/ProteanRaider.java +++ b/Mage.Sets/src/mage/cards/p/ProteanRaider.java @@ -30,7 +30,7 @@ public final class ProteanRaider extends CardImpl { // Raid — If you attacked with a creature this turn, you may have Protean Raider enter the battlefield as a copy of any creature on the battlefield. Ability ability = new EntersBattlefieldAbility(new CopyPermanentEffect(), true, RaidCondition.instance, - "Raid — If you attacked with a creature this turn, you may have {this} enter the battlefield as a copy of any creature on the battlefield.", ""); + "Raid — If you attacked this turn, you may have {this} enter the battlefield as a copy of any creature on the battlefield.", ""); ability.setAbilityWord(AbilityWord.RAID); ability.addHint(RaidHint.instance); this.addAbility(ability, new PlayerAttackedWatcher()); diff --git a/Mage.Sets/src/mage/cards/p/PurpleWorm.java b/Mage.Sets/src/mage/cards/p/PurpleWorm.java index 60005a96d1..78e3b026b5 100644 --- a/Mage.Sets/src/mage/cards/p/PurpleWorm.java +++ b/Mage.Sets/src/mage/cards/p/PurpleWorm.java @@ -13,7 +13,6 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; import mage.constants.Zone; -import mage.watchers.common.MorbidWatcher; import java.util.UUID; @@ -31,10 +30,10 @@ public final class PurpleWorm extends CardImpl { // This spell costs {2} less to cast if a creature died this turn. Ability ability = new SimpleStaticAbility( - Zone.ALL, new SpellCostReductionSourceEffect(1, MorbidCondition.instance) + Zone.ALL, new SpellCostReductionSourceEffect(2, MorbidCondition.instance) ); ability.setRuleAtTheTop(true); - this.addAbility(ability.addHint(MorbidHint.instance), new MorbidWatcher()); + this.addAbility(ability.addHint(MorbidHint.instance)); // Ward {2} this.addAbility(new WardAbility(new ManaCostsImpl<>("{2}"))); diff --git a/Mage.Sets/src/mage/cards/r/RazorfieldRhino.java b/Mage.Sets/src/mage/cards/r/RazorfieldRhino.java index 44199fd08d..92502e35c9 100644 --- a/Mage.Sets/src/mage/cards/r/RazorfieldRhino.java +++ b/Mage.Sets/src/mage/cards/r/RazorfieldRhino.java @@ -26,7 +26,7 @@ public final class RazorfieldRhino extends CardImpl { // Metalcraft — Razorfield Rhino gets +2/+2 as long as you control three or more artifacts. ContinuousEffect effect1 = new BoostSourceEffect(2, 2, Duration.WhileOnBattlefield); - this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new ConditionalContinuousEffect(effect1, MetalcraftCondition.instance, "Metalcraft — Razorfield Rhino gets +2/+2 as long as you control three or more artifacts")) + this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new ConditionalContinuousEffect(effect1, MetalcraftCondition.instance, "{this} gets +2/+2 as long as you control three or more artifacts")) .setAbilityWord(AbilityWord.METALCRAFT) .addHint(MetalcraftHint.instance)); } diff --git a/Mage.Sets/src/mage/cards/r/ReaperFromTheAbyss.java b/Mage.Sets/src/mage/cards/r/ReaperFromTheAbyss.java index 66c46bee95..adefb7338a 100644 --- a/Mage.Sets/src/mage/cards/r/ReaperFromTheAbyss.java +++ b/Mage.Sets/src/mage/cards/r/ReaperFromTheAbyss.java @@ -1,50 +1,47 @@ - - package mage.cards.r; -import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; -import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.BeginningOfEndStepTriggeredAbility; +import mage.abilities.condition.common.MorbidCondition; import mage.abilities.effects.common.DestroyTargetEffect; +import mage.abilities.hint.common.MorbidHint; import mage.abilities.keyword.FlyingAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.CardType; -import mage.constants.SubType; -import mage.constants.Zone; +import mage.constants.*; +import mage.filter.FilterPermanent; import mage.filter.common.FilterCreaturePermanent; import mage.filter.predicate.Predicates; -import mage.game.Game; -import mage.game.events.GameEvent; -import mage.game.events.GameEvent.EventType; -import mage.target.common.TargetCreaturePermanent; -import mage.watchers.Watcher; -import mage.watchers.common.MorbidWatcher; +import mage.target.TargetPermanent; + +import java.util.UUID; /** - * * @author BetaSteward_at_googlemail.com */ public final class ReaperFromTheAbyss extends CardImpl { - private static final FilterCreaturePermanent filter = new FilterCreaturePermanent("non-Demon creature"); + private static final FilterPermanent filter = new FilterCreaturePermanent("non-Demon creature"); static { filter.add(Predicates.not(SubType.DEMON.getPredicate())); } public ReaperFromTheAbyss(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{3}{B}{B}{B}"); + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{B}{B}{B}"); this.subtype.add(SubType.DEMON); this.power = new MageInt(6); this.toughness = new MageInt(6); this.addAbility(FlyingAbility.getInstance()); - Ability ability = new ReaperFromTheAbyssAbility(); - ability.addTarget(new TargetCreaturePermanent(filter)); - this.addAbility(ability); + Ability ability = new BeginningOfEndStepTriggeredAbility( + Zone.BATTLEFIELD, new DestroyTargetEffect(), + TargetController.ANY, MorbidCondition.instance, false + ); + ability.addTarget(new TargetPermanent(filter)); + this.addAbility(ability.setAbilityWord(AbilityWord.MORBID).addHint(MorbidHint.instance)); } private ReaperFromTheAbyss(final ReaperFromTheAbyss card) { @@ -57,35 +54,3 @@ public final class ReaperFromTheAbyss extends CardImpl { } } - -class ReaperFromTheAbyssAbility extends TriggeredAbilityImpl { - - public ReaperFromTheAbyssAbility() { - super(Zone.BATTLEFIELD, new DestroyTargetEffect(), false); - } - - public ReaperFromTheAbyssAbility(final ReaperFromTheAbyssAbility ability) { - super(ability); - } - - @Override - public ReaperFromTheAbyssAbility copy() { - return new ReaperFromTheAbyssAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.END_TURN_STEP_PRE; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - Watcher watcher = game.getState().getWatcher(MorbidWatcher.class); - return watcher != null && watcher.conditionMet(); - } - - @Override - public String getRule() { - return "Morbid — At the beginning of each end step, if a creature died this turn, destroy target non-demon creature."; - } -} diff --git a/Mage.Sets/src/mage/cards/r/RetreatToHagra.java b/Mage.Sets/src/mage/cards/r/RetreatToHagra.java index 809847beaa..86f40f2d08 100644 --- a/Mage.Sets/src/mage/cards/r/RetreatToHagra.java +++ b/Mage.Sets/src/mage/cards/r/RetreatToHagra.java @@ -28,7 +28,7 @@ public final class RetreatToHagra extends CardImpl { // Landfall- Whenever a land enters the battlefield under your control, // choose one - Target creature gets +1/+0 and gains deathtouch until end of turn; LandfallAbility ability = new LandfallAbility(new BoostTargetEffect(1, 0, Duration.EndOfTurn), false); - Effect effect = new GainAbilityTargetEffect(DeathtouchAbility.getInstance(), Duration.EndOfTurn); + Effect effect = new GainAbilityTargetEffect(DeathtouchAbility.getInstance(), Duration.EndOfTurn).setText("and gains deathtouch until end of turn"); effect.setOutcome(Outcome.Benefit); ability.addEffect(effect); ability.addTarget(new TargetCreaturePermanent()); diff --git a/Mage.Sets/src/mage/cards/r/RogueClass.java b/Mage.Sets/src/mage/cards/r/RogueClass.java new file mode 100644 index 0000000000..5211d568e6 --- /dev/null +++ b/Mage.Sets/src/mage/cards/r/RogueClass.java @@ -0,0 +1,214 @@ +package mage.cards.r; + +import mage.MageObject; +import mage.abilities.Ability; +import mage.abilities.common.DealsDamageToAPlayerAllTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.AsThoughEffectImpl; +import mage.abilities.effects.AsThoughManaEffect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.continuous.GainAbilityControlledEffect; +import mage.abilities.effects.common.continuous.GainClassAbilitySourceEffect; +import mage.abilities.keyword.ClassLevelAbility; +import mage.abilities.keyword.ClassReminderAbility; +import mage.abilities.keyword.MenaceAbility; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.filter.StaticFilters; +import mage.game.ExileZone; +import mage.game.Game; +import mage.players.ManaPoolItem; +import mage.players.Player; +import mage.target.targetpointer.FixedTarget; +import mage.util.CardUtil; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class RogueClass extends CardImpl { + + public RogueClass(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{U}{B}"); + + this.subtype.add(SubType.CLASS); + + // (Gain the next level as a sorcery to add its ability.) + this.addAbility(new ClassReminderAbility()); + + // Whenever a creature you controls deals combat damage to a player, exile the top card of that player's library face down. You may look at it for as long as it remains exiled. + this.addAbility(new DealsDamageToAPlayerAllTriggeredAbility( + new RogueClassExileEffect(), StaticFilters.FILTER_CONTROLLED_A_CREATURE, + false, SetTargetPointer.PLAYER, true, true + )); + + // {1}{U}{B}: Level 2 + this.addAbility(new ClassLevelAbility(2, "{1}{U}{B}")); + + // Creatures you control have menace. + this.addAbility(new SimpleStaticAbility(new GainClassAbilitySourceEffect( + new GainAbilityControlledEffect( + new MenaceAbility(), Duration.WhileOnBattlefield, + StaticFilters.FILTER_CONTROLLED_CREATURES + ), 2 + ))); + + // {2}{U}{B}: Level 3 + this.addAbility(new ClassLevelAbility(3, "{2}{U}{B}")); + + // You may play cards exiled with Rogue Class, and you may spend mana as through it were mana of any color to cast them. + Ability ability = new SimpleStaticAbility(new RogueClassPlayEffect()); + ability.addEffect(new RogueClassManaEffect()); + this.addAbility(new SimpleStaticAbility(new GainClassAbilitySourceEffect(ability, 3))); + } + + private RogueClass(final RogueClass card) { + super(card); + } + + @Override + public RogueClass copy() { + return new RogueClass(this); + } +} + +class RogueClassExileEffect extends OneShotEffect { + + RogueClassExileEffect() { + super(Outcome.Benefit); + staticText = "exile the top card of that player's library face down. " + + "You may look at it for as long as it remains exiled"; + } + + private RogueClassExileEffect(final RogueClassExileEffect effect) { + super(effect); + } + + @Override + public RogueClassExileEffect copy() { + return new RogueClassExileEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + Player opponent = game.getPlayer(getTargetPointer().getFirst(game, source)); + if (controller == null || opponent == null) { + return false; + } + Card card = opponent.getLibrary().getFromTop(game); + if (card == null) { + return false; + } + MageObject sourceObject = source.getSourcePermanentOrLKI(game); + controller.moveCardsToExile( + card, source, game, false, + CardUtil.getExileZoneId(game, source), + sourceObject != null ? sourceObject.getName() : null + ); + card.setFaceDown(true, game); + game.addEffect(new RogueClassLookEffect().setTargetPointer(new FixedTarget(card, game)), source); + return true; + } +} + +class RogueClassLookEffect extends AsThoughEffectImpl { + + RogueClassLookEffect() { + super(AsThoughEffectType.LOOK_AT_FACE_DOWN, Duration.EndOfGame, Outcome.Benefit); + } + + private RogueClassLookEffect(final RogueClassLookEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + return true; + } + + @Override + public RogueClassLookEffect copy() { + return new RogueClassLookEffect(this); + } + + @Override + public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) { + UUID cardId = getTargetPointer().getFirst(game, source); + if (cardId == null) { + discard(); + return false; + } + return source.isControlledBy(affectedControllerId) + && cardId.equals(objectId); + } +} + +class RogueClassPlayEffect extends AsThoughEffectImpl { + + RogueClassPlayEffect() { + super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.Benefit); + staticText = "you may play cards exiled with {this}"; + } + + private RogueClassPlayEffect(final RogueClassPlayEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + return true; + } + + @Override + public RogueClassPlayEffect copy() { + return new RogueClassPlayEffect(this); + } + + @Override + public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) { + if (!source.isControlledBy(affectedControllerId)) { + return false; + } + ExileZone exileZone = game.getState().getExile().getExileZone(CardUtil.getExileZoneId(game, source)); + return exileZone != null && exileZone.contains(CardUtil.getMainCardId(game, objectId)); + } +} + +class RogueClassManaEffect extends AsThoughEffectImpl implements AsThoughManaEffect { + + RogueClassManaEffect() { + super(AsThoughEffectType.SPEND_OTHER_MANA, Duration.WhileOnBattlefield, Outcome.Benefit); + this.staticText = ", and you may spend mana as through it were mana of any color to cast them"; + } + + private RogueClassManaEffect(final RogueClassManaEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + return true; + } + + @Override + public RogueClassManaEffect copy() { + return new RogueClassManaEffect(this); + } + + @Override + public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) { + ExileZone exileZone = game.getState().getExile().getExileZone(CardUtil.getExileZoneId(game, source)); + return source.isControlledBy(affectedControllerId) + && exileZone != null + && exileZone.contains(CardUtil.getMainCardId(game, objectId)); + } + + @Override + public ManaType getAsThoughManaType(ManaType manaType, ManaPoolItem mana, UUID affectedControllerId, Ability source, Game game) { + return mana.getFirstAvailable(); + } +} diff --git a/Mage.Sets/src/mage/cards/r/RustedRelic.java b/Mage.Sets/src/mage/cards/r/RustedRelic.java index be3eff980f..514d30a085 100644 --- a/Mage.Sets/src/mage/cards/r/RustedRelic.java +++ b/Mage.Sets/src/mage/cards/r/RustedRelic.java @@ -26,7 +26,7 @@ public final class RustedRelic extends CardImpl { new ConditionalContinuousEffect( new BecomesCreatureSourceEffect(new RustedRelicToken(), "artifact", Duration.WhileOnBattlefield), MetalcraftCondition.instance, - "Metalcraft — {this} is a 5/5 Golem artifact creature as long as you control three or more artifacts")) + "{this} is a 5/5 Golem artifact creature as long as you control three or more artifacts")) .setAbilityWord(AbilityWord.METALCRAFT) .addHint(MetalcraftHint.instance) ); diff --git a/Mage.Sets/src/mage/cards/s/SabertoothMauler.java b/Mage.Sets/src/mage/cards/s/SabertoothMauler.java index 4ee840e721..abd3aa20b3 100644 --- a/Mage.Sets/src/mage/cards/s/SabertoothMauler.java +++ b/Mage.Sets/src/mage/cards/s/SabertoothMauler.java @@ -7,6 +7,7 @@ import mage.abilities.condition.common.MorbidCondition; import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; import mage.abilities.effects.common.UntapSourceEffect; import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.abilities.hint.common.MorbidHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -37,7 +38,7 @@ public final class SabertoothMauler extends CardImpl { "if a creature died this turn, put a +1/+1 counter on {this} and untap it." ); ability.addEffect(new UntapSourceEffect()); - this.addAbility(ability); + this.addAbility(ability.addHint(MorbidHint.instance)); } private SabertoothMauler(final SabertoothMauler card) { diff --git a/Mage.Sets/src/mage/cards/s/SefrisOfTheHiddenWays.java b/Mage.Sets/src/mage/cards/s/SefrisOfTheHiddenWays.java new file mode 100644 index 0000000000..6ddb6ca649 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SefrisOfTheHiddenWays.java @@ -0,0 +1,57 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.CompletedDungeonTriggeredAbility; +import mage.abilities.common.PutCardIntoGraveFromAnywhereAllTriggeredAbility; +import mage.abilities.effects.common.ReturnFromGraveyardToBattlefieldTargetEffect; +import mage.abilities.effects.keyword.VentureIntoTheDungeonEffect; +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.StaticFilters; +import mage.filter.common.FilterCreatureCard; +import mage.target.common.TargetCardInYourGraveyard; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class SefrisOfTheHiddenWays extends CardImpl { + + private static final FilterCard filter = new FilterCreatureCard("one or more creature cards"); + + public SefrisOfTheHiddenWays(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{W}{U}{B}"); + + this.addSuperType(SuperType.LEGENDARY); + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.WIZARD); + this.power = new MageInt(2); + this.toughness = new MageInt(3); + + // Whenever one or more creature cards are put into your graveyard from anywhere, venture into the dungeon. This ability triggers only once each turn. + this.addAbility(new PutCardIntoGraveFromAnywhereAllTriggeredAbility( + new VentureIntoTheDungeonEffect(), false, filter, TargetController.YOU + ).setTriggersOnce(true)); + + // Create Undead — Whenever you complete a dungeon, return target creature card from your graveyard to the battlefield. + Ability ability = new CompletedDungeonTriggeredAbility(new ReturnFromGraveyardToBattlefieldTargetEffect()); + ability.addTarget(new TargetCardInYourGraveyard(StaticFilters.FILTER_CARD_CREATURE_YOUR_GRAVEYARD)); + this.addAbility(ability.withFlavorWord("Create Undead")); + } + + private SefrisOfTheHiddenWays(final SefrisOfTheHiddenWays card) { + super(card); + } + + @Override + public SefrisOfTheHiddenWays copy() { + return new SefrisOfTheHiddenWays(this); + } +} diff --git a/Mage.Sets/src/mage/cards/s/SemblanceAnvil.java b/Mage.Sets/src/mage/cards/s/SemblanceAnvil.java index afef026fe8..82f3a11d07 100644 --- a/Mage.Sets/src/mage/cards/s/SemblanceAnvil.java +++ b/Mage.Sets/src/mage/cards/s/SemblanceAnvil.java @@ -29,7 +29,7 @@ public final class SemblanceAnvil extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{3}"); // Imprint - When Semblance Anvil enters the battlefield, you may exile a nonland card from your hand. - this.addAbility(new EntersBattlefieldTriggeredAbility(new SemblanceAnvilEffect(), true)); + this.addAbility(new EntersBattlefieldTriggeredAbility(new SemblanceAnvilEffect(), true).setAbilityWord(AbilityWord.IMPRINT)); // Spells you cast that share a card type with the exiled card cost {2} less to cast. this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new SemblanceAnvilCostReductionEffect())); diff --git a/Mage.Sets/src/mage/cards/s/ShessraDeathsWhisper.java b/Mage.Sets/src/mage/cards/s/ShessraDeathsWhisper.java index d54a362bad..28c22acdaf 100644 --- a/Mage.Sets/src/mage/cards/s/ShessraDeathsWhisper.java +++ b/Mage.Sets/src/mage/cards/s/ShessraDeathsWhisper.java @@ -14,7 +14,6 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; import mage.target.common.TargetCreaturePermanent; -import mage.watchers.common.MorbidWatcher; import java.util.UUID; @@ -40,9 +39,9 @@ public final class ShessraDeathsWhisper extends CardImpl { // Whispers of the Grave — At the beginning of your end step, if a creature died this turn, you may pay 2 life. If you do, draw a card. this.addAbility(new BeginningOfEndStepTriggeredAbility( - Zone.BATTLEFIELD, new DoIfCostPaid(new DrawCardSourceControllerEffect(2), + Zone.BATTLEFIELD, new DoIfCostPaid(new DrawCardSourceControllerEffect(1), new PayLifeCost(2)), TargetController.YOU, MorbidCondition.instance, false - ).addHint(MorbidHint.instance), new MorbidWatcher()); + ).addHint(MorbidHint.instance).withFlavorWord("Whispers of the Grave")); } private ShessraDeathsWhisper(final ShessraDeathsWhisper card) { diff --git a/Mage.Sets/src/mage/cards/s/ShoalSerpent.java b/Mage.Sets/src/mage/cards/s/ShoalSerpent.java index 778aa865ef..1c7e1d8a09 100644 --- a/Mage.Sets/src/mage/cards/s/ShoalSerpent.java +++ b/Mage.Sets/src/mage/cards/s/ShoalSerpent.java @@ -48,7 +48,7 @@ class ShoalSerpentEffect extends ContinuousEffectImpl { public ShoalSerpentEffect() { super(Duration.EndOfTurn, Outcome.AddAbility); - staticText = "Until end of turn, {this} loses defender"; + staticText = "{this} loses defender until end of turn"; } public ShoalSerpentEffect(final ShoalSerpentEffect effect) { diff --git a/Mage.Sets/src/mage/cards/s/SiegehornCeratops.java b/Mage.Sets/src/mage/cards/s/SiegehornCeratops.java index 427d94439a..a232f75a5e 100644 --- a/Mage.Sets/src/mage/cards/s/SiegehornCeratops.java +++ b/Mage.Sets/src/mage/cards/s/SiegehornCeratops.java @@ -27,7 +27,7 @@ public final class SiegehornCeratops extends CardImpl { // Enrage — Whenever Siegehorn Ceratops is dealt damage, put two +1/+1 counters on it. this.addAbility(new DealtDamageToSourceTriggeredAbility( new AddCountersSourceEffect(CounterType.P1P1.createInstance(2)) - .setText("put two +1/+1 counter on it"), false, true)); + .setText("put two +1/+1 counters on it"), false, true)); } private SiegehornCeratops(final SiegehornCeratops card) { diff --git a/Mage.Sets/src/mage/cards/s/SilverSeraph.java b/Mage.Sets/src/mage/cards/s/SilverSeraph.java index 20b99c3e77..e17d2334f2 100644 --- a/Mage.Sets/src/mage/cards/s/SilverSeraph.java +++ b/Mage.Sets/src/mage/cards/s/SilverSeraph.java @@ -34,7 +34,7 @@ public final class SilverSeraph extends CardImpl { // Threshold - Other creatures you control get +2/+2 as long as seven or more cards are in your graveyard. Ability ability = new SimpleStaticAbility(Zone.BATTLEFIELD, new ConditionalContinuousEffect( new BoostControlledEffect(2, 2, Duration.WhileOnBattlefield, true), new CardsInControllerGraveyardCondition(7), - "other creatures you control +2/+2 as long as seven or more cards are in your graveyard")); + "other creatures you control get +2/+2 as long as seven or more cards are in your graveyard")); ability.setAbilityWord(AbilityWord.THRESHOLD); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/s/SkeletalSwarming.java b/Mage.Sets/src/mage/cards/s/SkeletalSwarming.java index 1a7ff137ac..b091a84e19 100644 --- a/Mage.Sets/src/mage/cards/s/SkeletalSwarming.java +++ b/Mage.Sets/src/mage/cards/s/SkeletalSwarming.java @@ -25,7 +25,6 @@ import mage.filter.FilterPermanent; import mage.filter.common.FilterControlledPermanent; import mage.filter.common.FilterCreaturePermanent; import mage.game.permanent.token.SkeletonToken; -import mage.watchers.common.MorbidWatcher; import java.util.UUID; @@ -59,7 +58,7 @@ public final class SkeletalSwarming extends CardImpl { MorbidCondition.instance, "create a tapped 1/1 black Skeleton creature token. " + "If a creature died this turn, create two of those tokens instead" ), TargetController.YOU, false - ).addHint(MorbidHint.instance), new MorbidWatcher()); + ).addHint(MorbidHint.instance)); } private SkeletalSwarming(final SkeletalSwarming card) { diff --git a/Mage.Sets/src/mage/cards/s/SkirsdagHighPriest.java b/Mage.Sets/src/mage/cards/s/SkirsdagHighPriest.java index cc7caeeff9..b8df837ea2 100644 --- a/Mage.Sets/src/mage/cards/s/SkirsdagHighPriest.java +++ b/Mage.Sets/src/mage/cards/s/SkirsdagHighPriest.java @@ -9,6 +9,7 @@ import mage.abilities.costs.common.TapSourceCost; import mage.abilities.costs.common.TapTargetCost; import mage.abilities.decorator.ConditionalActivatedAbility; import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.hint.common.MorbidHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.AbilityWord; @@ -45,7 +46,7 @@ public final class SkirsdagHighPriest extends CardImpl { new TapSourceCost(), MorbidCondition.instance); ability.addCost(new TapTargetCost(new TargetControlledCreaturePermanent(2, 2, filter, false))); ability.setAbilityWord(AbilityWord.MORBID); - this.addAbility(ability); + this.addAbility(ability.addHint(MorbidHint.instance)); } private SkirsdagHighPriest(final SkirsdagHighPriest card) { diff --git a/Mage.Sets/src/mage/cards/s/SlaughterhouseBouncer.java b/Mage.Sets/src/mage/cards/s/SlaughterhouseBouncer.java index eb039b4cae..8627abe95d 100644 --- a/Mage.Sets/src/mage/cards/s/SlaughterhouseBouncer.java +++ b/Mage.Sets/src/mage/cards/s/SlaughterhouseBouncer.java @@ -34,9 +34,8 @@ public final class SlaughterhouseBouncer extends CardImpl { Ability ability = new ConditionalInterveningIfTriggeredAbility( new DiesSourceTriggeredAbility(new BoostTargetEffect(-3, -3, Duration.EndOfTurn)), HellbentCondition.instance, - "When {this} dies, if you have no cards in hand, target creature gets -3/-3 until end of turn." + AbilityWord.HELLBENT.formatWord() + "When {this} dies, if you have no cards in hand, target creature gets -3/-3 until end of turn." ); - ability.setAbilityWord(AbilityWord.HELLBENT); ability.addTarget(new TargetCreaturePermanent()); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/s/SnapsailGlider.java b/Mage.Sets/src/mage/cards/s/SnapsailGlider.java index 0461c306db..8711263174 100644 --- a/Mage.Sets/src/mage/cards/s/SnapsailGlider.java +++ b/Mage.Sets/src/mage/cards/s/SnapsailGlider.java @@ -19,7 +19,7 @@ import java.util.UUID; */ public final class SnapsailGlider extends CardImpl { - protected static String rule = "Metalcraft — Snapsail Glider has flying as long as you control three or more artifacts"; + protected static String rule = "{this} has flying as long as you control three or more artifacts"; public SnapsailGlider(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT, CardType.CREATURE}, "{3}"); diff --git a/Mage.Sets/src/mage/cards/s/SomberwaldSpider.java b/Mage.Sets/src/mage/cards/s/SomberwaldSpider.java index d42a2fbbfd..565da40ac6 100644 --- a/Mage.Sets/src/mage/cards/s/SomberwaldSpider.java +++ b/Mage.Sets/src/mage/cards/s/SomberwaldSpider.java @@ -7,9 +7,11 @@ import mage.abilities.common.EntersBattlefieldAbility; import mage.abilities.condition.common.MorbidCondition; import mage.abilities.decorator.ConditionalOneShotEffect; import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.abilities.hint.common.MorbidHint; import mage.abilities.keyword.ReachAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; +import mage.constants.AbilityWord; import mage.constants.CardType; import mage.constants.SubType; import mage.counters.CounterType; @@ -31,7 +33,7 @@ public final class SomberwaldSpider extends CardImpl { // Morbid — Somberwald Spider enters the battlefield with two +1/+1 counters on it if a creature died this turn. this.addAbility(new EntersBattlefieldAbility( new ConditionalOneShotEffect(new AddCountersSourceEffect(CounterType.P1P1.createInstance(2)), MorbidCondition.instance, ""), - "with two +1/+1 counters on it if a creature died this turn")); + "with two +1/+1 counters on it if a creature died this turn").addHint(MorbidHint.instance).setAbilityWord(AbilityWord.MORBID)); } private SomberwaldSpider(final SomberwaldSpider card) { diff --git a/Mage.Sets/src/mage/cards/s/SorcererClass.java b/Mage.Sets/src/mage/cards/s/SorcererClass.java new file mode 100644 index 0000000000..960ac7e99a --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SorcererClass.java @@ -0,0 +1,198 @@ +package mage.cards.s; + +import mage.ConditionalMana; +import mage.MageObject; +import mage.Mana; +import mage.abilities.Ability; +import mage.abilities.SpellAbility; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.common.SpellCastControllerTriggeredAbility; +import mage.abilities.condition.Condition; +import mage.abilities.costs.Cost; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.DrawDiscardControllerEffect; +import mage.abilities.effects.common.continuous.GainAbilityControlledEffect; +import mage.abilities.effects.common.continuous.GainClassAbilitySourceEffect; +import mage.abilities.keyword.ClassLevelAbility; +import mage.abilities.keyword.ClassReminderAbility; +import mage.abilities.mana.ConditionalColoredManaAbility; +import mage.abilities.mana.builder.ConditionalManaBuilder; +import mage.abilities.mana.conditional.ManaCondition; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.stack.Spell; +import mage.players.Player; +import mage.watchers.Watcher; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class SorcererClass extends CardImpl { + + public SorcererClass(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{U}{R}"); + + this.subtype.add(SubType.CLASS); + + // (Gain the next level as a sorcery to add its ability.) + this.addAbility(new ClassReminderAbility()); + + // When Sorcerer Class enters the battlefield, draw two cards, then discard two cards. + this.addAbility(new EntersBattlefieldTriggeredAbility( + new DrawDiscardControllerEffect(2, 2) + )); + + // {U}{R}: Level 2 + this.addAbility(new ClassLevelAbility(2, "{U}{R}")); + + // Creatures you control have "{T}: Add {U} or {R}. Spend this mana only to cast an instant or sorcery spell or to gain a Class level." + Ability ability = new SimpleStaticAbility(new GainAbilityControlledEffect( + new ConditionalColoredManaAbility(Mana.BlueMana(1), new SorcererClassManaBuilder()), + Duration.WhileOnBattlefield, StaticFilters.FILTER_CONTROLLED_CREATURE + ).setText("creatures you control have \"{T}: Add {U} or {R}.")); + ability.addEffect(new GainAbilityControlledEffect( + new ConditionalColoredManaAbility(Mana.RedMana(1), new SorcererClassManaBuilder()), + Duration.WhileOnBattlefield, StaticFilters.FILTER_CONTROLLED_CREATURE + ).setText("Spend this mana only to cast an instant or sorcery spell or to gain a Class level.\"")); + this.addAbility(new SimpleStaticAbility(new GainClassAbilitySourceEffect(ability, 2))); + + // {3}{U}{R}: Level 3 + this.addAbility(new ClassLevelAbility(3, "{3}{U}{R}")); + + // Whenever you cast an instant or sorcery spell, that spell deals damage to each opponent equal to the number of instant or sorcery spells you've cast this turn. + this.addAbility(new SimpleStaticAbility(new GainClassAbilitySourceEffect( + new SpellCastControllerTriggeredAbility( + new SorcererClassEffect(), + StaticFilters.FILTER_SPELL_AN_INSTANT_OR_SORCERY, + false, true + ), 3 + )), new SorcererClassWatcher()); + } + + private SorcererClass(final SorcererClass card) { + super(card); + } + + @Override + public SorcererClass copy() { + return new SorcererClass(this); + } +} + +class SorcererClassManaBuilder extends ConditionalManaBuilder { + + @Override + public ConditionalMana build(Object... options) { + return new SorcererClassConditionalMana(this.mana); + } + + @Override + public String getRule() { + return "Spend this mana only to cast an instant or sorcery spell or to gain a Class level"; + } +} + +class SorcererClassConditionalMana extends ConditionalMana { + + public SorcererClassConditionalMana(Mana mana) { + super(mana); + staticText = "Spend this mana only to cast an instant or sorcery spell or to gain a Class level"; + addCondition(new SorcererClassManaCondition()); + } +} + +class SorcererClassManaCondition extends ManaCondition implements Condition { + + @Override + public boolean apply(Game game, Ability source) { + if (source instanceof SpellAbility) { + MageObject object = game.getObject(source.getSourceId()); + return object != null && object.isInstantOrSorcery(game); + } + return source instanceof ClassLevelAbility; + } + + @Override + public boolean apply(Game game, Ability source, UUID originalId, Cost costsToPay) { + return apply(game, source); + } +} + +class SorcererClassEffect extends OneShotEffect { + + SorcererClassEffect() { + super(Outcome.Benefit); + staticText = "that spell deals damage to each opponent equal " + + "to the number of instant or sorcery spells you've cast this turn"; + } + + private SorcererClassEffect(final SorcererClassEffect effect) { + super(effect); + } + + @Override + public SorcererClassEffect copy() { + return new SorcererClassEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Spell spell = (Spell) getValue("spellCast"); + if (spell == null) { + return false; + } + int count = SorcererClassWatcher.spellCount(source.getControllerId(), game); + if (count < 1) { + return false; + } + for (UUID playerId : game.getOpponents(source.getControllerId())) { + Player player = game.getPlayer(playerId); + if (player == null) { + continue; + } + player.damage(count, spell.getId(), source, game); + } + return true; + } +} + +class SorcererClassWatcher extends Watcher { + + private final Map spellMap = new HashMap<>(); + + SorcererClassWatcher() { + super(WatcherScope.GAME); + } + + @Override + public void watch(GameEvent event, Game game) { + if (event.getType() != GameEvent.EventType.SPELL_CAST) { + return; + } + Spell spell = game.getSpell(event.getTargetId()); + if (spell == null || !spell.isInstantOrSorcery(game)) { + return; + } + spellMap.compute(spell.getControllerId(), (u, i) -> i == null ? 1 : Integer.sum(i, 1)); + } + + @Override + public void reset() { + spellMap.clear(); + super.reset(); + } + + static int spellCount(UUID playerId, Game game) { + SorcererClassWatcher watcher = game.getState().getWatcher(SorcererClassWatcher.class); + return watcher != null ? watcher.spellMap.getOrDefault(playerId, 0) : 0; + } +} diff --git a/Mage.Sets/src/mage/cards/s/SpareDagger.java b/Mage.Sets/src/mage/cards/s/SpareDagger.java new file mode 100644 index 0000000000..2fef867de9 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SpareDagger.java @@ -0,0 +1,84 @@ +package mage.cards.s; + +import mage.abilities.Ability; +import mage.abilities.common.AttacksTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.common.delayed.ReflexiveTriggeredAbility; +import mage.abilities.costs.common.SacrificeAttachmentCost; +import mage.abilities.effects.Effect; +import mage.abilities.effects.common.DamageTargetEffect; +import mage.abilities.effects.common.DoWhenCostPaid; +import mage.abilities.effects.common.continuous.BoostEquippedEffect; +import mage.abilities.effects.common.continuous.GainAbilityWithAttachmentEffect; +import mage.abilities.keyword.EquipAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.game.Game; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class SpareDagger extends CardImpl { + + public SpareDagger(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{1}"); + + this.subtype.add(SubType.EQUIPMENT); + + // Equipped creature gets +1/+0 and has "Whenever this creature attacks, you may sacrifice Spare Dagger. When you do, this creature deals 1 damage to any target." + Ability ability = new SimpleStaticAbility(new BoostEquippedEffect(1, 0)); + ability.addEffect(new SpareDaggerEffect()); + this.addAbility(ability); + + // Equip {1} + this.addAbility(new EquipAbility(1)); + } + + private SpareDagger(final SpareDagger card) { + super(card); + } + + @Override + public SpareDagger copy() { + return new SpareDagger(this); + } +} + +class SpareDaggerEffect extends GainAbilityWithAttachmentEffect { + + SpareDaggerEffect() { + super("and has \"Whenever this creature attacks, you may sacrifice {this}. " + + "When you do, this creature deals 1 damage to any target.\"", + (Effect) null, null, new SacrificeAttachmentCost()); + } + + private SpareDaggerEffect(final SpareDaggerEffect effect) { + super(effect); + } + + @Override + public SpareDaggerEffect copy() { + return new SpareDaggerEffect(this); + } + + @Override + protected Ability makeAbility(Game game, Ability source) { + if (source == null || game == null) { + return null; + } + String sourceName = source.getSourcePermanentIfItStillExists(game).getName(); + ReflexiveTriggeredAbility ability = new ReflexiveTriggeredAbility( + new DamageTargetEffect(1), false, + "This creature deals 1 damage to any target" + ); + return new AttacksTriggeredAbility(new DoWhenCostPaid( + ability, useAttachedCost.copy().setMageObjectReference(source, game), + "Sacrifice " + sourceName + "?" + ), false, "Whenever this creature attacks, you may sacrifice " + + sourceName + ". When you do, this creature deals 1 damage to any target."); + } +} diff --git a/Mage.Sets/src/mage/cards/s/SpikedPitTrap.java b/Mage.Sets/src/mage/cards/s/SpikedPitTrap.java index 0eb4037889..16d39d3d93 100644 --- a/Mage.Sets/src/mage/cards/s/SpikedPitTrap.java +++ b/Mage.Sets/src/mage/cards/s/SpikedPitTrap.java @@ -44,7 +44,7 @@ public final class SpikedPitTrap extends CardImpl { )); // 10-20 | Spike Pit Trap deals 5 damage to that creature. Create a Treasure token. - effect.addTableEntry(1, 9, new DamageTargetEffect( + effect.addTableEntry(10, 20, new DamageTargetEffect( 5, true, "that creature." ), new CreateTokenEffect(new TreasureToken())); } diff --git a/Mage.Sets/src/mage/cards/s/SpiralingDuelist.java b/Mage.Sets/src/mage/cards/s/SpiralingDuelist.java index 7ad2a65f6e..9197404701 100644 --- a/Mage.Sets/src/mage/cards/s/SpiralingDuelist.java +++ b/Mage.Sets/src/mage/cards/s/SpiralingDuelist.java @@ -19,7 +19,7 @@ import java.util.UUID; */ public final class SpiralingDuelist extends CardImpl { - private static final String effectText = "Metalcraft — Spiraling Duelist has double strike as long as you control three or more artifacts."; + private static final String effectText = "{this} has double strike as long as you control three or more artifacts."; public SpiralingDuelist(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{R}{R}"); diff --git a/Mage.Sets/src/mage/cards/s/SpireSerpent.java b/Mage.Sets/src/mage/cards/s/SpireSerpent.java index a10afcbe59..81c34fc7a1 100644 --- a/Mage.Sets/src/mage/cards/s/SpireSerpent.java +++ b/Mage.Sets/src/mage/cards/s/SpireSerpent.java @@ -22,7 +22,7 @@ import java.util.UUID; */ public final class SpireSerpent extends CardImpl { - private static final String abilityText1 = "Metalcraft — As long as you control three or more artifacts, {this} gets +2/+2"; + private static final String abilityText1 = "As long as you control three or more artifacts, {this} gets +2/+2"; public SpireSerpent(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{4}{U}"); diff --git a/Mage.Sets/src/mage/cards/s/StingingStudy.java b/Mage.Sets/src/mage/cards/s/StingingStudy.java index 250ed39c8c..ccee4735de 100644 --- a/Mage.Sets/src/mage/cards/s/StingingStudy.java +++ b/Mage.Sets/src/mage/cards/s/StingingStudy.java @@ -10,6 +10,7 @@ import mage.choices.ChoiceImpl; import mage.constants.CardType; import mage.constants.CommanderCardType; import mage.constants.Outcome; +import mage.constants.Zone; import mage.game.Game; import mage.players.Player; @@ -64,7 +65,7 @@ class StingingStudyEffect extends OneShotEffect { return false; } Set manaValues = new HashSet<>(); - for (Card commander : game.getCommanderCardsFromAnyZones(player, CommanderCardType.ANY)) { + for (Card commander : game.getCommanderCardsFromAnyZones(player, CommanderCardType.ANY, Zone.BATTLEFIELD, Zone.COMMAND)) { manaValues.add(commander.getManaValue()); } int chosenValue = 0; diff --git a/Mage.Sets/src/mage/cards/s/StorvaldFrostGiantJarl.java b/Mage.Sets/src/mage/cards/s/StorvaldFrostGiantJarl.java new file mode 100644 index 0000000000..77bdc88412 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/StorvaldFrostGiantJarl.java @@ -0,0 +1,68 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.Mode; +import mage.abilities.common.EntersBattlefieldOrAttacksSourceTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.continuous.GainAbilityControlledEffect; +import mage.abilities.effects.common.continuous.SetPowerToughnessTargetEffect; +import mage.abilities.keyword.WardAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.filter.StaticFilters; +import mage.target.common.TargetCreaturePermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class StorvaldFrostGiantJarl extends CardImpl { + + public StorvaldFrostGiantJarl(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{4}{G}{W}{U}"); + + this.addSuperType(SuperType.LEGENDARY); + this.subtype.add(SubType.GIANT); + this.power = new MageInt(7); + this.toughness = new MageInt(7); + + // Ward {3} + this.addAbility(new WardAbility(new ManaCostsImpl<>("{3}"))); + + // Other creatures you control have ward {3}. + this.addAbility(new SimpleStaticAbility(new GainAbilityControlledEffect( + new WardAbility(new GenericManaCost(3)), Duration.WhileOnBattlefield, + StaticFilters.FILTER_CONTROLLED_CREATURES, true + ))); + + // Whenever Storvald, Frost Giant Jarl enters the battlefield or attacks, choose one or both — + // • Target creature has base power and toughness 7/7 until end of turn. + Ability ability = new EntersBattlefieldOrAttacksSourceTriggeredAbility( + new SetPowerToughnessTargetEffect(7, 7, Duration.EndOfTurn) + ); + ability.addTarget(new TargetCreaturePermanent()); + + // • Target creature has base power and toughness 1/1 until end of turn. + Mode mode = new Mode(new SetPowerToughnessTargetEffect(1, 1, Duration.EndOfTurn)); + mode.addTarget(new TargetCreaturePermanent()); + ability.addMode(mode); + this.addAbility(ability); + } + + private StorvaldFrostGiantJarl(final StorvaldFrostGiantJarl card) { + super(card); + } + + @Override + public StorvaldFrostGiantJarl copy() { + return new StorvaldFrostGiantJarl(this); + } +} diff --git a/Mage.Sets/src/mage/cards/s/StrataScythe.java b/Mage.Sets/src/mage/cards/s/StrataScythe.java index c9bb0eec60..6905e435e8 100644 --- a/Mage.Sets/src/mage/cards/s/StrataScythe.java +++ b/Mage.Sets/src/mage/cards/s/StrataScythe.java @@ -14,10 +14,7 @@ import mage.abilities.keyword.EquipAbility; import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.CardType; -import mage.constants.SubType; -import mage.constants.Outcome; -import mage.constants.Zone; +import mage.constants.*; import mage.filter.FilterPermanent; import mage.filter.common.FilterLandCard; import mage.filter.predicate.mageobject.NamePredicate; @@ -35,7 +32,7 @@ public final class StrataScythe extends CardImpl { public StrataScythe(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{3}"); this.subtype.add(SubType.EQUIPMENT); - this.addAbility(new EntersBattlefieldTriggeredAbility(new StrataScytheImprintEffect())); + this.addAbility(new EntersBattlefieldTriggeredAbility(new StrataScytheImprintEffect()).setAbilityWord(AbilityWord.IMPRINT)); this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new BoostEquippedEffect(SameNameAsExiledCountValue.getInstance(), SameNameAsExiledCountValue.getInstance()))); this.addAbility(new EquipAbility(Outcome.BoostCreature, new GenericManaCost(3))); } diff --git a/Mage.Sets/src/mage/cards/s/SummaryJudgment.java b/Mage.Sets/src/mage/cards/s/SummaryJudgment.java index 615f1d9159..a6ed77c27e 100644 --- a/Mage.Sets/src/mage/cards/s/SummaryJudgment.java +++ b/Mage.Sets/src/mage/cards/s/SummaryJudgment.java @@ -52,7 +52,7 @@ class SummaryJudgementEffect extends OneShotEffect { super(Outcome.Benefit); staticText = "{this} deals 3 damage to target tapped creature." + "
Addendum — If you cast this spell during your main phase, " + - "it deals 5 damage to that creature instead."; + "it deals 5 damage."; } private SummaryJudgementEffect(final SummaryJudgementEffect effect) { diff --git a/Mage.Sets/src/mage/cards/s/SunCrownedHunters.java b/Mage.Sets/src/mage/cards/s/SunCrownedHunters.java index fbd8fbb216..7d487a7128 100644 --- a/Mage.Sets/src/mage/cards/s/SunCrownedHunters.java +++ b/Mage.Sets/src/mage/cards/s/SunCrownedHunters.java @@ -27,7 +27,7 @@ public final class SunCrownedHunters extends CardImpl { // Enrage — Whenever Sun-Crowned Hunters is dealt damage, it deals 3 damage to target opponent. Ability ability = new DealtDamageToSourceTriggeredAbility( - new DamageTargetEffect(3).setText("it deals 3 damage to target opponent"), false, true + new DamageTargetEffect(3, "it"), false, true ); ability.addTarget(new TargetOpponentOrPlaneswalker()); this.addAbility(ability); diff --git a/Mage.Sets/src/mage/cards/s/SunspringExpedition.java b/Mage.Sets/src/mage/cards/s/SunspringExpedition.java index 79a950295e..5e76cd1882 100644 --- a/Mage.Sets/src/mage/cards/s/SunspringExpedition.java +++ b/Mage.Sets/src/mage/cards/s/SunspringExpedition.java @@ -1,14 +1,9 @@ - - package mage.cards.s; -import java.util.UUID; import mage.abilities.ActivatedAbility; import mage.abilities.common.LandfallAbility; import mage.abilities.common.SimpleActivatedAbility; -import mage.abilities.costs.Cost; -import mage.abilities.costs.Costs; -import mage.abilities.costs.CostsImpl; +import mage.abilities.costs.CompositeCost; import mage.abilities.costs.common.RemoveCountersSourceCost; import mage.abilities.costs.common.SacrificeSourceCost; import mage.abilities.effects.common.GainLifeEffect; @@ -16,24 +11,25 @@ import mage.abilities.effects.common.counter.AddCountersSourceEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.Zone; import mage.counters.CounterType; +import java.util.UUID; + /** - * * @author BetaSteward_at_googlemail.com */ public final class SunspringExpedition extends CardImpl { public SunspringExpedition(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.ENCHANTMENT},"{W}"); + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{W}"); this.addAbility(new LandfallAbility(new AddCountersSourceEffect(CounterType.QUEST.createInstance()), true)); - Costs costs = new CostsImpl<>(); - costs.add(new RemoveCountersSourceCost(CounterType.QUEST.createInstance(3))); - costs.add(new SacrificeSourceCost()); - ActivatedAbility ability = new SimpleActivatedAbility(Zone.BATTLEFIELD, new GainLifeEffect(8), costs); + ActivatedAbility ability = new SimpleActivatedAbility( + new GainLifeEffect(8), new CompositeCost( + new RemoveCountersSourceCost(CounterType.QUEST.createInstance(3)), + new SacrificeSourceCost(), "Remove three quest counters from {this} and sacrifice it" + )); this.addAbility(ability); } @@ -45,5 +41,4 @@ public final class SunspringExpedition extends CardImpl { public SunspringExpedition copy() { return new SunspringExpedition(this); } - } diff --git a/Mage.Sets/src/mage/cards/s/SylvanAnthem.java b/Mage.Sets/src/mage/cards/s/SylvanAnthem.java index 730f293d6c..8953b252eb 100644 --- a/Mage.Sets/src/mage/cards/s/SylvanAnthem.java +++ b/Mage.Sets/src/mage/cards/s/SylvanAnthem.java @@ -3,7 +3,7 @@ package mage.cards.s; import mage.ObjectColor; import mage.abilities.common.EntersBattlefieldControlledTriggeredAbility; import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.effects.common.continuous.BoostAllEffect; +import mage.abilities.effects.common.continuous.BoostControlledEffect; import mage.abilities.effects.keyword.ScryEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -29,7 +29,7 @@ public final class SylvanAnthem extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{G}{G}"); // Green creatures you control get +1/+1. - this.addAbility(new SimpleStaticAbility(new BoostAllEffect( + this.addAbility(new SimpleStaticAbility(new BoostControlledEffect( 1, 1, Duration.WhileOnBattlefield, filter, false ).setText("green creatures you control get +1/+1"))); diff --git a/Mage.Sets/src/mage/cards/t/TitanHunter.java b/Mage.Sets/src/mage/cards/t/TitanHunter.java index 29be00c444..e121bae95e 100644 --- a/Mage.Sets/src/mage/cards/t/TitanHunter.java +++ b/Mage.Sets/src/mage/cards/t/TitanHunter.java @@ -12,6 +12,7 @@ import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.GainLifeEffect; +import mage.abilities.hint.common.MorbidHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -45,7 +46,7 @@ public final class TitanHunter extends CardImpl { new TitanHunterEffect(), TargetController.EACH_PLAYER, false ), condition, "At the beginning of each player's end step, " + "if no creatures died this turn, {this} deals 4 damage to that player." - )); + ).addHint(MorbidHint.instance)); // {1}{B}, Sacrifice a creature: You gain 4 life. Ability ability = new SimpleActivatedAbility(new GainLifeEffect(4), new ManaCostsImpl("{1}{B}")); diff --git a/Mage.Sets/src/mage/cards/t/TragicSlip.java b/Mage.Sets/src/mage/cards/t/TragicSlip.java index 8916202a87..01f57f3e82 100644 --- a/Mage.Sets/src/mage/cards/t/TragicSlip.java +++ b/Mage.Sets/src/mage/cards/t/TragicSlip.java @@ -6,6 +6,7 @@ import mage.abilities.condition.LockedInCondition; import mage.abilities.condition.common.MorbidCondition; import mage.abilities.decorator.ConditionalContinuousEffect; import mage.abilities.effects.common.continuous.BoostTargetEffect; +import mage.abilities.hint.common.MorbidHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -30,6 +31,7 @@ public final class TragicSlip extends CardImpl { new LockedInCondition(MorbidCondition.instance), "Target creature gets -1/-1 until end of turn.
Morbid — That creature gets -13/-13 until end of turn instead if a creature died this turn")); this.getSpellAbility().addTarget(new TargetCreaturePermanent()); + this.getSpellAbility().addHint(MorbidHint.instance); } private TragicSlip(final TragicSlip card) { diff --git a/Mage.Sets/src/mage/cards/t/TreasureChest.java b/Mage.Sets/src/mage/cards/t/TreasureChest.java index b49171fc7e..8796421f19 100644 --- a/Mage.Sets/src/mage/cards/t/TreasureChest.java +++ b/Mage.Sets/src/mage/cards/t/TreasureChest.java @@ -65,7 +65,7 @@ class TreasureChestEffect extends OneShotEffect { TreasureChestEffect() { super(Outcome.Benefit); - staticText = "search your library for a card. If it's an artifact card you may " + + staticText = "search your library for a card. If it's an artifact card, you may " + "put it onto the battlefield. Otherwise, put that card into your hand. Then shuffle"; } diff --git a/Mage.Sets/src/mage/cards/t/TribalFlames.java b/Mage.Sets/src/mage/cards/t/TribalFlames.java index 24eef4f202..dd8cebe2a1 100644 --- a/Mage.Sets/src/mage/cards/t/TribalFlames.java +++ b/Mage.Sets/src/mage/cards/t/TribalFlames.java @@ -7,6 +7,7 @@ import mage.abilities.effects.common.DamageTargetEffect; import mage.abilities.hint.common.DomainHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; +import mage.constants.AbilityWord; import mage.constants.CardType; import mage.target.common.TargetAnyTarget; @@ -24,6 +25,7 @@ public final class TribalFlames extends CardImpl { this.getSpellAbility().addEffect(new DamageTargetEffect(new DomainValue())); this.getSpellAbility().addTarget(new TargetAnyTarget()); this.getSpellAbility().addHint(DomainHint.instance); + this.getSpellAbility().setAbilityWord(AbilityWord.DOMAIN); } private TribalFlames(final TribalFlames card) { diff --git a/Mage.Sets/src/mage/cards/t/TrickstersTalisman.java b/Mage.Sets/src/mage/cards/t/TrickstersTalisman.java new file mode 100644 index 0000000000..785ae3d926 --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/TrickstersTalisman.java @@ -0,0 +1,78 @@ +package mage.cards.t; + +import mage.abilities.Ability; +import mage.abilities.common.DealsCombatDamageToAPlayerTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.costs.common.SacrificeAttachmentCost; +import mage.abilities.effects.CreateTokenCopySourceEffect; +import mage.abilities.effects.Effect; +import mage.abilities.effects.common.DoIfCostPaid; +import mage.abilities.effects.common.continuous.BoostEquippedEffect; +import mage.abilities.effects.common.continuous.GainAbilityWithAttachmentEffect; +import mage.abilities.keyword.EquipAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.game.Game; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class TrickstersTalisman extends CardImpl { + + public TrickstersTalisman(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{U}"); + + this.subtype.add(SubType.EQUIPMENT); + + // Equipped creature gets +1/+1 and has "Whenever this creature deals combat damage to a player, you may sacrifice Trickster's Talisman. If you do, create a token that's a copy of this creature." + Ability ability = new SimpleStaticAbility(new BoostEquippedEffect(1, 1)); + ability.addEffect(new TrickstersTalismanEffect()); + this.addAbility(ability); + + // Equip {2} + this.addAbility(new EquipAbility(2)); + } + + private TrickstersTalisman(final TrickstersTalisman card) { + super(card); + } + + @Override + public TrickstersTalisman copy() { + return new TrickstersTalisman(this); + } +} + +class TrickstersTalismanEffect extends GainAbilityWithAttachmentEffect { + + TrickstersTalismanEffect() { + super("and has \"Whenever this creature deals combat damage to a player, " + + "you may sacrifice {this}. If you do, create a token that's a copy of this creature.\"", + (Effect) null, null, new SacrificeAttachmentCost()); + } + + private TrickstersTalismanEffect(final TrickstersTalismanEffect effect) { + super(effect); + } + + @Override + public TrickstersTalismanEffect copy() { + return new TrickstersTalismanEffect(this); + } + + @Override + protected Ability makeAbility(Game game, Ability source) { + if (game == null || source == null) { + return null; + } + return new DealsCombatDamageToAPlayerTriggeredAbility(new DoIfCostPaid( + new CreateTokenCopySourceEffect(), useAttachedCost.setMageObjectReference(source, game) + ), false, "Whenever this creature deals combat damage to a player, you may sacrifice " + + source.getSourcePermanentIfItStillExists(game).getName() + + ". If you do, create a token that's a copy of this creature.", false); + } +} diff --git a/Mage.Sets/src/mage/cards/t/TwinbladeAssassins.java b/Mage.Sets/src/mage/cards/t/TwinbladeAssassins.java index d67fef4433..617b8c13c6 100644 --- a/Mage.Sets/src/mage/cards/t/TwinbladeAssassins.java +++ b/Mage.Sets/src/mage/cards/t/TwinbladeAssassins.java @@ -5,12 +5,12 @@ import mage.abilities.common.BeginningOfEndStepTriggeredAbility; import mage.abilities.condition.common.MorbidCondition; import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.hint.common.MorbidHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; import mage.constants.TargetController; -import mage.watchers.common.MorbidWatcher; import java.util.UUID; @@ -33,7 +33,7 @@ public final class TwinbladeAssassins extends CardImpl { new DrawCardSourceControllerEffect(1), TargetController.YOU, false ), MorbidCondition.instance, "At the beginning of your end step, " + "if a creature died this turn, draw a card." - ), new MorbidWatcher()); + ).addHint(MorbidHint.instance)); } private TwinbladeAssassins(final TwinbladeAssassins card) { diff --git a/Mage.Sets/src/mage/cards/u/UlvenwaldBear.java b/Mage.Sets/src/mage/cards/u/UlvenwaldBear.java index 57665ac2aa..a7450f9da6 100644 --- a/Mage.Sets/src/mage/cards/u/UlvenwaldBear.java +++ b/Mage.Sets/src/mage/cards/u/UlvenwaldBear.java @@ -8,8 +8,10 @@ import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.condition.common.MorbidCondition; import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; import mage.abilities.effects.common.counter.AddCountersTargetEffect; +import mage.abilities.hint.common.MorbidHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; +import mage.constants.AbilityWord; import mage.constants.CardType; import mage.constants.SubType; import mage.constants.Outcome; @@ -33,7 +35,7 @@ public final class UlvenwaldBear extends CardImpl { Ability ability = new ConditionalInterveningIfTriggeredAbility(new EntersBattlefieldTriggeredAbility(new AddCountersTargetEffect(CounterType.P1P1.createInstance(2), Outcome.BoostCreature)), MorbidCondition.instance, "When {this} enters the battlefield, if a creature died this turn, put two +1/+1 counters on target creature."); ability.addTarget(new TargetCreaturePermanent()); - this.addAbility(ability); + this.addAbility(ability.addHint(MorbidHint.instance).setAbilityWord(AbilityWord.MORBID)); } private UlvenwaldBear(final UlvenwaldBear card) { diff --git a/Mage.Sets/src/mage/cards/u/UnbreakableFormation.java b/Mage.Sets/src/mage/cards/u/UnbreakableFormation.java index 62ced3ec1d..34df150c94 100644 --- a/Mage.Sets/src/mage/cards/u/UnbreakableFormation.java +++ b/Mage.Sets/src/mage/cards/u/UnbreakableFormation.java @@ -51,7 +51,7 @@ class UnbreakableFormationEffect extends OneShotEffect { UnbreakableFormationEffect() { super(Outcome.Benefit); staticText = "
Addendum — If you cast this spell during your main phase, " + - "put a +1/+1 counter on each of those creatures, and they also gain vigilance until end of turn."; + "put a +1/+1 counter on each of those creatures and they gain vigilance until end of turn."; } private UnbreakableFormationEffect(final UnbreakableFormationEffect effect) { diff --git a/Mage.Sets/src/mage/cards/u/UnderdarkRift.java b/Mage.Sets/src/mage/cards/u/UnderdarkRift.java new file mode 100644 index 0000000000..ef8ac9232e --- /dev/null +++ b/Mage.Sets/src/mage/cards/u/UnderdarkRift.java @@ -0,0 +1,90 @@ +package mage.cards.u; + +import mage.abilities.Ability; +import mage.abilities.common.ActivateAsSorceryActivatedAbility; +import mage.abilities.costs.common.ExileSourceCost; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.mana.ColorlessManaAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.filter.FilterPermanent; +import mage.filter.predicate.Predicates; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.TargetPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class UnderdarkRift extends CardImpl { + + private static final FilterPermanent filter = new FilterPermanent("artifact, creature, or planeswalker"); + + static { + filter.add(Predicates.or( + CardType.ARTIFACT.getPredicate(), + CardType.CREATURE.getPredicate(), + CardType.PLANESWALKER.getPredicate() + )); + } + + public UnderdarkRift(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.LAND}, ""); + + // {T}: Add {C}. + this.addAbility(new ColorlessManaAbility()); + + // {5}, {T}, Exile Underdark Rift: Roll a d10. Put a target artifact, creature, or planeswalker into its owner's library just beneath the top X cards of that library, where X is the result. Activate only has a sorcery. + Ability ability = new ActivateAsSorceryActivatedAbility(new UnderdarkRiftEffect(), new GenericManaCost(5)); + ability.addCost(new TapSourceCost()); + ability.addCost(new ExileSourceCost()); + ability.addTarget(new TargetPermanent(filter)); + this.addAbility(ability); + } + + private UnderdarkRift(final UnderdarkRift card) { + super(card); + } + + @Override + public UnderdarkRift copy() { + return new UnderdarkRift(this); + } +} + +class UnderdarkRiftEffect extends OneShotEffect { + + UnderdarkRiftEffect() { + super(Outcome.Benefit); + staticText = "roll a d10. Put a target artifact, creature, or planeswalker into its owner's library " + + "just beneath the top X cards of that library, where X is the result"; + } + + private UnderdarkRiftEffect(final UnderdarkRiftEffect effect) { + super(effect); + } + + @Override + public UnderdarkRiftEffect copy() { + return new UnderdarkRiftEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + Permanent permanent = game.getPermanent(source.getFirstTarget()); + if (player == null || permanent == null) { + return false; + } + int result = player.rollDice(source, game, 10); + player.putCardOnTopXOfLibrary(permanent, game, source, result + 1, true); + return true; + } +} diff --git a/Mage.Sets/src/mage/cards/u/UnholyHunger.java b/Mage.Sets/src/mage/cards/u/UnholyHunger.java index 0298c4b3fb..8e15034517 100644 --- a/Mage.Sets/src/mage/cards/u/UnholyHunger.java +++ b/Mage.Sets/src/mage/cards/u/UnholyHunger.java @@ -27,7 +27,7 @@ public final class UnholyHunger extends CardImpl { // Spell mastery — If there are two or more instant and/or sorcery cards in your graveyard, you gain 2 life. Effect effect = new ConditionalOneShotEffect(new GainLifeEffect(2), - SpellMasteryCondition.instance, "Spell mastery — If there are two or more instant and/or sorcery cards in your graveyard, you gain 2 life"); + SpellMasteryCondition.instance, "
Spell mastery — If there are two or more instant and/or sorcery cards in your graveyard, you gain 2 life"); this.getSpellAbility().addEffect(effect); } diff --git a/Mage.Sets/src/mage/cards/v/VengefulDevil.java b/Mage.Sets/src/mage/cards/v/VengefulDevil.java index 7a339d5d69..5502c23150 100644 --- a/Mage.Sets/src/mage/cards/v/VengefulDevil.java +++ b/Mage.Sets/src/mage/cards/v/VengefulDevil.java @@ -6,6 +6,7 @@ import mage.abilities.common.ActivateIfConditionActivatedAbility; import mage.abilities.condition.common.MorbidCondition; import mage.abilities.costs.common.TapSourceCost; import mage.abilities.effects.common.DamageTargetEffect; +import mage.abilities.hint.common.MorbidHint; import mage.abilities.keyword.HasteAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -39,7 +40,7 @@ public final class VengefulDevil extends CardImpl { ); ability.addTarget(new TargetAnyTarget()); ability.setAbilityWord(AbilityWord.MORBID); - this.addAbility(ability); + this.addAbility(ability.addHint(MorbidHint.instance)); } private VengefulDevil(final VengefulDevil card) { diff --git a/Mage.Sets/src/mage/cards/v/VoloGuideToMonsters.java b/Mage.Sets/src/mage/cards/v/VoloGuideToMonsters.java index c86ac91d54..35cc5c51bb 100644 --- a/Mage.Sets/src/mage/cards/v/VoloGuideToMonsters.java +++ b/Mage.Sets/src/mage/cards/v/VoloGuideToMonsters.java @@ -1,7 +1,6 @@ package mage.cards.v; import mage.MageInt; -import mage.MageObject; import mage.abilities.common.SpellCastControllerTriggeredAbility; import mage.abilities.effects.common.CopyTargetSpellEffect; import mage.cards.CardImpl; @@ -12,11 +11,10 @@ import mage.constants.SuperType; import mage.filter.FilterSpell; import mage.filter.StaticFilters; import mage.filter.common.FilterCreatureSpell; -import mage.filter.predicate.ObjectSourcePlayer; -import mage.filter.predicate.ObjectSourcePlayerPredicate; +import mage.filter.predicate.Predicate; import mage.game.Game; +import mage.game.stack.StackObject; import mage.players.Player; -import mage.util.CardUtil; import java.util.UUID; @@ -30,6 +28,10 @@ public final class VoloGuideToMonsters extends CardImpl { "with a creature you control or a creature card in your graveyard" ); + static { + filter.add(VoloGuideToMonstersPredicate.instance); + } + public VoloGuideToMonsters(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{G}{U}"); @@ -57,24 +59,24 @@ public final class VoloGuideToMonsters extends CardImpl { } } -enum VoloGuideToMonstersPredicate implements ObjectSourcePlayerPredicate> { +enum VoloGuideToMonstersPredicate implements Predicate { instance; @Override - public boolean apply(ObjectSourcePlayer input, Game game) { - Player player = game.getPlayer(input.getPlayerId()); + public boolean apply(StackObject input, Game game) { + Player player = game.getPlayer(input.getControllerId()); if (player != null && player .getGraveyard() .getCards(StaticFilters.FILTER_CARD_CREATURE, game) .stream() - .anyMatch(card -> CardUtil.haveSameNames(card, input.getObject()))) { + .anyMatch(card -> input.shareCreatureTypes(game, card))) { return false; } return game .getBattlefield() - .getActivePermanents(StaticFilters.FILTER_CONTROLLED_CREATURE, input.getPlayerId(), game) + .getActivePermanents(StaticFilters.FILTER_CONTROLLED_CREATURE, input.getControllerId(), game) .stream() - .noneMatch(permanent -> CardUtil.haveSameNames(permanent, input.getObject())); + .noneMatch(permanent -> input.shareCreatureTypes(game, permanent)); } -} \ No newline at end of file +} diff --git a/Mage.Sets/src/mage/cards/w/Wakedancer.java b/Mage.Sets/src/mage/cards/w/Wakedancer.java index 781ba37ea9..fb55d8120b 100644 --- a/Mage.Sets/src/mage/cards/w/Wakedancer.java +++ b/Mage.Sets/src/mage/cards/w/Wakedancer.java @@ -8,6 +8,7 @@ import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.condition.common.MorbidCondition; import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.hint.common.MorbidHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -33,7 +34,7 @@ public final class Wakedancer extends CardImpl { // Morbid — When Wakedancer enters the battlefield, if a creature died this turn, create a 2/2 black Zombie creature token. TriggeredAbility ability = new EntersBattlefieldTriggeredAbility(new CreateTokenEffect(new ZombieToken())); - this.addAbility(new ConditionalInterveningIfTriggeredAbility(ability, MorbidCondition.instance, staticText)); + this.addAbility(new ConditionalInterveningIfTriggeredAbility(ability, MorbidCondition.instance, staticText).addHint(MorbidHint.instance)); } private Wakedancer(final Wakedancer card) { diff --git a/Mage.Sets/src/mage/cards/w/WarNameAspirant.java b/Mage.Sets/src/mage/cards/w/WarNameAspirant.java index de94cf8fe4..76a632da8f 100644 --- a/Mage.Sets/src/mage/cards/w/WarNameAspirant.java +++ b/Mage.Sets/src/mage/cards/w/WarNameAspirant.java @@ -39,7 +39,7 @@ public final class WarNameAspirant extends CardImpl { // Raid — War-Name Aspirant enters the battlefield with a +1/+1 counter on it if you attacked this turn. this.addAbility(new EntersBattlefieldAbility(new AddCountersSourceEffect(CounterType.P1P1.createInstance(1), false), RaidCondition.instance, - "Raid — {this} enters the battlefield with a +1/+1 counter on it if you attacked this turn", + "Raid — {this} enters the battlefield with a +1/+1 counter on it if you attacked this turn.", "{this} enters the battlefield with a +1/+1 counter") .setAbilityWord(AbilityWord.RAID) .addHint(RaidHint.instance), diff --git a/Mage.Sets/src/mage/cards/w/WarlockClass.java b/Mage.Sets/src/mage/cards/w/WarlockClass.java index ae4bda5dc8..8b325b0cf8 100644 --- a/Mage.Sets/src/mage/cards/w/WarlockClass.java +++ b/Mage.Sets/src/mage/cards/w/WarlockClass.java @@ -3,12 +3,14 @@ package mage.cards.w; import mage.abilities.Ability; import mage.abilities.common.BecomesClassLevelTriggeredAbility; import mage.abilities.common.BeginningOfEndStepTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; import mage.abilities.condition.common.MorbidCondition; import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; import mage.abilities.dynamicvalue.common.StaticValue; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.LookLibraryAndPickControllerEffect; import mage.abilities.effects.common.LoseLifeOpponentsEffect; +import mage.abilities.effects.common.continuous.GainClassAbilitySourceEffect; import mage.abilities.hint.common.MorbidHint; import mage.abilities.keyword.ClassLevelAbility; import mage.abilities.keyword.ClassReminderAbility; @@ -18,7 +20,6 @@ import mage.constants.*; import mage.filter.StaticFilters; import mage.game.Game; import mage.players.Player; -import mage.watchers.common.MorbidWatcher; import mage.watchers.common.PlayerLostLifeWatcher; import java.util.UUID; @@ -39,17 +40,17 @@ public final class WarlockClass extends CardImpl { // At the beginning of your end step, if a creature died this turn, each opponent loses 1 life. this.addAbility(new ConditionalInterveningIfTriggeredAbility( new BeginningOfEndStepTriggeredAbility( - new LoseLifeOpponentsEffect(2), TargetController.YOU, false + new LoseLifeOpponentsEffect(1), TargetController.YOU, false ), MorbidCondition.instance, "At the beginning of your end step, " + "if a creature died this turn, each opponent loses 1 life." - ).addHint(MorbidHint.instance), new MorbidWatcher()); + ).addHint(MorbidHint.instance)); // {1}{B}: Level 2 this.addAbility(new ClassLevelAbility(2, "{1}{B}")); // When this Class becomes level 2, look at the top three cards of your library. Put one of them into your hand and the rest into your graveyard. this.addAbility(new BecomesClassLevelTriggeredAbility(new LookLibraryAndPickControllerEffect( - StaticValue.get(1), false, StaticValue.get(1), StaticFilters.FILTER_CARD, + StaticValue.get(3), false, StaticValue.get(1), StaticFilters.FILTER_CARD, Zone.GRAVEYARD, false, false, false, Zone.HAND, false ), 2)); @@ -57,9 +58,11 @@ public final class WarlockClass extends CardImpl { this.addAbility(new ClassLevelAbility(3, "{6}{B}")); // At the beginning of your end step, each opponent loses life equal to the life they lost this turn. - this.addAbility(new BeginningOfEndStepTriggeredAbility( - new WarlockClassEffect(), TargetController.YOU, false - )); + this.addAbility(new SimpleStaticAbility(new GainClassAbilitySourceEffect( + new BeginningOfEndStepTriggeredAbility( + new WarlockClassEffect(), TargetController.YOU, false + ), 3 + ))); } private WarlockClass(final WarlockClass card) { @@ -76,7 +79,7 @@ class WarlockClassEffect extends OneShotEffect { WarlockClassEffect() { super(Outcome.Benefit); - staticText = "At the beginning of your end step, each opponent loses life equal to the life they lost this turn."; + staticText = "each opponent loses life equal to the life they lost this turn."; } private WarlockClassEffect(final WarlockClassEffect effect) { diff --git a/Mage.Sets/src/mage/cards/w/WildShape.java b/Mage.Sets/src/mage/cards/w/WildShape.java index fd11ef1a42..cf62caaa07 100644 --- a/Mage.Sets/src/mage/cards/w/WildShape.java +++ b/Mage.Sets/src/mage/cards/w/WildShape.java @@ -25,7 +25,7 @@ public final class WildShape extends CardImpl { // Choose one. Until end of turn, target creature you control has that base power and toughness, becomes that creature type, and gains that ability. this.getSpellAbility().getModes().setChooseText( - ". Until end of turn, target creature you control has that base power and toughness, " + + "Choose one. Until end of turn, target creature you control has that base power and toughness, " + "becomes that creature type, and gains that ability." ); diff --git a/Mage.Sets/src/mage/cards/w/WizardsSpellbook.java b/Mage.Sets/src/mage/cards/w/WizardsSpellbook.java new file mode 100644 index 0000000000..37f9375a76 --- /dev/null +++ b/Mage.Sets/src/mage/cards/w/WizardsSpellbook.java @@ -0,0 +1,158 @@ +package mage.cards.w; + +import mage.ApprovingObject; +import mage.MageObject; +import mage.abilities.Ability; +import mage.abilities.SpellAbility; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.RollDieWithResultTableEffect; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.Zone; +import mage.filter.StaticFilters; +import mage.game.ExileZone; +import mage.game.Game; +import mage.players.Player; +import mage.target.common.TargetCardInGraveyard; +import mage.util.CardUtil; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class WizardsSpellbook extends CardImpl { + + public WizardsSpellbook(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{5}{U}{U}"); + + // {T}: Exile target instant or sorcery card from a graveyard. Roll a d20. Activate only as a sorcery. + RollDieWithResultTableEffect effect = new RollDieWithResultTableEffect( + 20, "exile target instant or sorcery card " + + "from a graveyard. Roll a d20. Activate only as a sorcery" + ); + Ability ability = new SimpleActivatedAbility(effect, new TapSourceCost()); + ability.addTarget(new TargetCardInGraveyard(StaticFilters.FILTER_CARD_INSTANT_OR_SORCERY)); + this.addAbility(ability); + + // 1-9 | Copy that card. You may cast the copy. + effect.addTableEntry(1, 9, new WizardsSpellbookEffect(1)); + + // 10-19 | Copy that card. You may cast the copy by paying {1} rather than paying its mana cost. + effect.addTableEntry(10, 19, new WizardsSpellbookEffect(2)); + + // 20 | Copy each card exiled with Wizard's Spellbook. You may cast any number of the copies without paying their mana costs. + effect.addTableEntry(20, 20, new WizardsSpellbookEffect(3)); + } + + private WizardsSpellbook(final WizardsSpellbook card) { + super(card); + } + + @Override + public WizardsSpellbook copy() { + return new WizardsSpellbook(this); + } +} + +class WizardsSpellbookEffect extends OneShotEffect { + + private final int level; + + WizardsSpellbookEffect(int level) { + super(Outcome.Benefit); + this.level = level; + switch (level) { + case 1: + staticText = "copy that card. You may cast the copy"; + break; + case 2: + staticText = "copy that card. You may cast the copy by paying {1} rather than paying its mana cost"; + break; + case 3: + staticText = "copy each card exiled with {this}. You may cast any number of the copies without paying their mana costs"; + break; + default: + throw new IllegalArgumentException("Level must be 1-3"); + } + } + + private WizardsSpellbookEffect(final WizardsSpellbookEffect effect) { + super(effect); + this.level = effect.level; + } + + @Override + public WizardsSpellbookEffect copy() { + return new WizardsSpellbookEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + Card card = game.getCard(getTargetPointer().getFirst(game, source)); + if (player == null || card == null) { + return false; + } + UUID exileId = CardUtil.getExileZoneId(game, source); + MageObject sourceObject = source.getSourcePermanentOrLKI(game); + player.moveCardsToExile(card, source, game, true, exileId, sourceObject != null ? sourceObject.getName() : null); + if (level < 3) { + Card copiedCard = game.copyCard(card, source, source.getControllerId()); + if (!player.chooseUse( + Outcome.Benefit, "Cast " + copiedCard.getName() + + (level == 1 ? "?" : " by paying {1}?"), source, game) + ) { + return false; + } + SpellAbility spellAbility = player.chooseAbilityForCast(copiedCard, game, level == 2); + if (spellAbility == null) { + return false; + } + game.getState().setValue("PlayFromNotOwnHandZone" + copiedCard.getId(), Boolean.TRUE); + if (level == 2) { + player.setCastSourceIdWithAlternateMana(copiedCard.getId(), new ManaCostsImpl<>("{1}"), null); + } + player.cast(spellAbility, game, false, new ApprovingObject(source, game)); + game.getState().setValue("PlayFromNotOwnHandZone" + copiedCard.getId(), null); + return true; + } + ExileZone exile = game.getExile().getExileZone(exileId); + if (exile == null || exile.isEmpty()) { + return true; + } + Set cards = new HashSet<>(); + for (Card exiledCard : exile.getCards(game)) { + cards.add(game.copyCard(exiledCard, source, source.getControllerId())); + } + while (!cards.isEmpty()) { + for (Card copiedCard : cards) { + if (!player.chooseUse( + Outcome.PlayForFree, "Cast " + copiedCard.getName() + + " without paying its mana cost?", source, game + )) { + continue; + } + game.getState().setValue("PlayFromNotOwnHandZone" + copiedCard.getId(), Boolean.TRUE); + player.cast( + player.chooseAbilityForCast(copiedCard, game, true), + game, true, new ApprovingObject(source, game) + ); + game.getState().setValue("PlayFromNotOwnHandZone" + copiedCard.getId(), null); + } + if (!player.chooseUse(Outcome.Neutral, "Continue casting exiled cards?", source, game)) { + break; + } + cards.removeIf(c -> game.getState().getZone(c.getId()) != Zone.EXILED); + } + return true; + } +} diff --git a/Mage.Sets/src/mage/cards/w/WoodlandSleuth.java b/Mage.Sets/src/mage/cards/w/WoodlandSleuth.java index f0bbd1665f..c11d2d11e9 100644 --- a/Mage.Sets/src/mage/cards/w/WoodlandSleuth.java +++ b/Mage.Sets/src/mage/cards/w/WoodlandSleuth.java @@ -8,6 +8,7 @@ import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.condition.common.MorbidCondition; import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; import mage.abilities.effects.OneShotEffect; +import mage.abilities.hint.common.MorbidHint; import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -39,7 +40,7 @@ public final class WoodlandSleuth extends CardImpl { // Morbid — When Woodland Sleuth enters the battlefield, if a creature died this turn, return a creature card at random from your graveyard to your hand. TriggeredAbility ability = new EntersBattlefieldTriggeredAbility(new WoodlandSleuthEffect()); - this.addAbility(new ConditionalInterveningIfTriggeredAbility(ability, MorbidCondition.instance, staticText)); + this.addAbility(new ConditionalInterveningIfTriggeredAbility(ability, MorbidCondition.instance, staticText).addHint(MorbidHint.instance)); } private WoodlandSleuth(final WoodlandSleuth card) { diff --git a/Mage.Sets/src/mage/cards/y/YouFindSomePrisoners.java b/Mage.Sets/src/mage/cards/y/YouFindSomePrisoners.java new file mode 100644 index 0000000000..720a3e4425 --- /dev/null +++ b/Mage.Sets/src/mage/cards/y/YouFindSomePrisoners.java @@ -0,0 +1,88 @@ +package mage.cards.y; + +import mage.abilities.Ability; +import mage.abilities.Mode; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.DestroyTargetEffect; +import mage.cards.*; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.constants.Zone; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.players.Player; +import mage.target.common.TargetArtifactPermanent; +import mage.target.common.TargetCardInExile; +import mage.target.common.TargetOpponent; +import mage.util.CardUtil; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class YouFindSomePrisoners extends CardImpl { + + public YouFindSomePrisoners(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{1}{R}"); + + // Choose one — + // • Break Their Chains — Destroy target artifact. + this.getSpellAbility().addEffect(new DestroyTargetEffect()); + this.getSpellAbility().addTarget(new TargetArtifactPermanent()); + this.getSpellAbility().withFirstModeFlavorWord("Break Their Chains"); + + // • Interrogate Them — Exile the top three cards of target opponent's library. Choose one of them. Until the end of your next turn, you may play that card, and you may spend mana as through it were mana of any color to cast it. + Mode mode = new Mode(new YouFindSomePrisonersEffect()); + mode.addTarget(new TargetOpponent()); + this.getSpellAbility().addMode(mode.withFlavorWord("Interrogate Them")); + } + + private YouFindSomePrisoners(final YouFindSomePrisoners card) { + super(card); + } + + @Override + public YouFindSomePrisoners copy() { + return new YouFindSomePrisoners(this); + } +} + +class YouFindSomePrisonersEffect extends OneShotEffect { + + YouFindSomePrisonersEffect() { + super(Outcome.Benefit); + staticText = "exile the top three cards of target opponent's library. " + + "Choose one of them. Until the end of your next turn, you may play that card, " + + "and you may spend mana as though it were mana of any color to cast it"; + } + + private YouFindSomePrisonersEffect(final YouFindSomePrisonersEffect effect) { + super(effect); + } + + @Override + public YouFindSomePrisonersEffect copy() { + return new YouFindSomePrisonersEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + Player opponent = game.getPlayer(source.getFirstTarget()); + if (player == null || opponent == null) { + return false; + } + Cards cards = new CardsImpl(opponent.getLibrary().getTopCards(game, 3)); + player.moveCards(cards, Zone.EXILED, source, game); + TargetCardInExile target = new TargetCardInExile(StaticFilters.FILTER_CARD); + target.setNotTarget(true); + player.choose(Outcome.PlayForFree, cards, target, game); + Card card = cards.get(target.getFirstTarget(), game); + if (card != null) { + CardUtil.makeCardPlayable(game, source, card, Duration.UntilEndOfYourNextTurn, true); + } + return true; + } +} diff --git a/Mage.Sets/src/mage/cards/y/YouFindTheVillainsLair.java b/Mage.Sets/src/mage/cards/y/YouFindTheVillainsLair.java index e6c63acf11..72aa4229d9 100644 --- a/Mage.Sets/src/mage/cards/y/YouFindTheVillainsLair.java +++ b/Mage.Sets/src/mage/cards/y/YouFindTheVillainsLair.java @@ -27,7 +27,7 @@ public final class YouFindTheVillainsLair extends CardImpl { // • Learn Their Secrets — Draw two cards, then discard two cards. this.getSpellAbility().addMode(new Mode( new DrawDiscardControllerEffect(2, 2) - ).withFlavorWord("Learn Their Secrts")); + ).withFlavorWord("Learn Their Secrets")); } private YouFindTheVillainsLair(final YouFindTheVillainsLair card) { diff --git a/Mage.Sets/src/mage/cards/z/ZarielArchdukeOfAvernus.java b/Mage.Sets/src/mage/cards/z/ZarielArchdukeOfAvernus.java new file mode 100644 index 0000000000..8a36922cf4 --- /dev/null +++ b/Mage.Sets/src/mage/cards/z/ZarielArchdukeOfAvernus.java @@ -0,0 +1,60 @@ +package mage.cards.z; + +import mage.abilities.Ability; +import mage.abilities.LoyaltyAbility; +import mage.abilities.common.PlaneswalkerEntersWithLoyaltyCountersAbility; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.GetEmblemEffect; +import mage.abilities.effects.common.continuous.BoostControlledEffect; +import mage.abilities.effects.common.continuous.GainAbilityControlledEffect; +import mage.abilities.keyword.HasteAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.filter.StaticFilters; +import mage.game.command.emblems.ZarielArchdukeOfAvernusEmblem; +import mage.game.permanent.token.DevilToken; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class ZarielArchdukeOfAvernus extends CardImpl { + + public ZarielArchdukeOfAvernus(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.PLANESWALKER}, "{2}{R}{R}"); + + this.addSuperType(SuperType.LEGENDARY); + this.subtype.add(SubType.ZARIEL); + this.addAbility(new PlaneswalkerEntersWithLoyaltyCountersAbility(4)); + + // +1: Creatures you control get +1/+0 and gain haste until end of turn. + Ability ability = new LoyaltyAbility(new BoostControlledEffect( + 1, 0, Duration.EndOfTurn + ).setText("creatures you control get +1/+0"), 1); + ability.addEffect(new GainAbilityControlledEffect( + HasteAbility.getInstance(), Duration.EndOfTurn, + StaticFilters.FILTER_PERMANENT_CREATURES + ).setText("and gain haste until end of turn")); + this.addAbility(ability); + + // 0: Create a 1/1 red Devil creature token with "When this creature dies, it deals 1 damage to any target." + this.addAbility(new LoyaltyAbility(new CreateTokenEffect(new DevilToken()), 0)); + + // −6: You get an emblem with "At the end of the first combat phase on your turn, untap target creature you control. After this phase, there is an additional combat phase." + this.addAbility(new LoyaltyAbility(new GetEmblemEffect(new ZarielArchdukeOfAvernusEmblem()), -6)); + } + + private ZarielArchdukeOfAvernus(final ZarielArchdukeOfAvernus card) { + super(card); + } + + @Override + public ZarielArchdukeOfAvernus copy() { + return new ZarielArchdukeOfAvernus(this); + } +} diff --git a/Mage.Sets/src/mage/cards/z/ZendikarsRoil.java b/Mage.Sets/src/mage/cards/z/ZendikarsRoil.java index 01be0ac8b5..49d27a75e2 100644 --- a/Mage.Sets/src/mage/cards/z/ZendikarsRoil.java +++ b/Mage.Sets/src/mage/cards/z/ZendikarsRoil.java @@ -1,18 +1,15 @@ - package mage.cards.z; -import java.util.UUID; -import mage.abilities.common.EntersBattlefieldControlledTriggeredAbility; -import mage.abilities.effects.Effect; +import mage.abilities.common.LandfallAbility; import mage.abilities.effects.common.CreateTokenEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.filter.common.FilterLandPermanent; import mage.game.permanent.token.ZendikarsRoilElementalToken; +import java.util.UUID; + /** - * * @author LevelX2 */ public final class ZendikarsRoil extends CardImpl { @@ -21,8 +18,7 @@ public final class ZendikarsRoil extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{3}{G}{G}"); // Whenever a land enters the battlefield under your control, create a 2/2 green Elemental creature token. - Effect effect = new CreateTokenEffect(new ZendikarsRoilElementalToken()); - this.addAbility(new EntersBattlefieldControlledTriggeredAbility(effect, new FilterLandPermanent("a land"))); + this.addAbility(new LandfallAbility(new CreateTokenEffect(new ZendikarsRoilElementalToken()))); } private ZendikarsRoil(final ZendikarsRoil card) { diff --git a/Mage.Sets/src/mage/cards/z/ZombieOgre.java b/Mage.Sets/src/mage/cards/z/ZombieOgre.java index 47d2efc414..43f04c185f 100644 --- a/Mage.Sets/src/mage/cards/z/ZombieOgre.java +++ b/Mage.Sets/src/mage/cards/z/ZombieOgre.java @@ -11,7 +11,6 @@ import mage.constants.CardType; import mage.constants.SubType; import mage.constants.TargetController; import mage.constants.Zone; -import mage.watchers.common.MorbidWatcher; import java.util.UUID; @@ -32,7 +31,7 @@ public final class ZombieOgre extends CardImpl { this.addAbility(new BeginningOfEndStepTriggeredAbility( Zone.BATTLEFIELD, new VentureIntoTheDungeonEffect(), TargetController.YOU, MorbidCondition.instance, false - ).addHint(MorbidHint.instance), new MorbidWatcher()); + ).addHint(MorbidHint.instance)); } private ZombieOgre(final ZombieOgre card) { diff --git a/Mage.Sets/src/mage/sets/AdventuresInTheForgottenRealms.java b/Mage.Sets/src/mage/sets/AdventuresInTheForgottenRealms.java index 9611cacb75..5b6a6c2898 100644 --- a/Mage.Sets/src/mage/sets/AdventuresInTheForgottenRealms.java +++ b/Mage.Sets/src/mage/sets/AdventuresInTheForgottenRealms.java @@ -67,6 +67,7 @@ public final class AdventuresInTheForgottenRealms extends ExpansionSet { cards.add(new SetCardInfo("Death-Priest of Myrkul", 95, Rarity.UNCOMMON, mage.cards.d.DeathPriestOfMyrkul.class)); cards.add(new SetCardInfo("Delina, Wild Mage", 138, Rarity.RARE, mage.cards.d.DelinaWildMage.class)); cards.add(new SetCardInfo("Delver's Torch", 10, Rarity.COMMON, mage.cards.d.DelversTorch.class)); + cards.add(new SetCardInfo("Demilich", 53, Rarity.MYTHIC, mage.cards.d.Demilich.class)); cards.add(new SetCardInfo("Demogorgon's Clutches", 96, Rarity.UNCOMMON, mage.cards.d.DemogorgonsClutches.class)); cards.add(new SetCardInfo("Den of the Bugbear", 254, Rarity.RARE, mage.cards.d.DenOfTheBugbear.class)); cards.add(new SetCardInfo("Devoted Paladin", 11, Rarity.COMMON, mage.cards.d.DevotedPaladin.class)); @@ -100,11 +101,13 @@ public final class AdventuresInTheForgottenRealms extends ExpansionSet { cards.add(new SetCardInfo("Fifty Feet of Rope", 244, Rarity.UNCOMMON, mage.cards.f.FiftyFeetOfRope.class)); cards.add(new SetCardInfo("Fighter Class", 222, Rarity.RARE, mage.cards.f.FighterClass.class)); cards.add(new SetCardInfo("Find the Path", 183, Rarity.COMMON, mage.cards.f.FindThePath.class)); + cards.add(new SetCardInfo("Flameskull", 143, Rarity.MYTHIC, mage.cards.f.Flameskull.class)); cards.add(new SetCardInfo("Flumph", 15, Rarity.RARE, mage.cards.f.Flumph.class)); cards.add(new SetCardInfo("Fly", 59, Rarity.UNCOMMON, mage.cards.f.Fly.class)); cards.add(new SetCardInfo("Forest", 278, Rarity.LAND, mage.cards.basiclands.Forest.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Forsworn Paladin", 104, Rarity.RARE, mage.cards.f.ForswornPaladin.class)); cards.add(new SetCardInfo("Froghemoth", 184, Rarity.RARE, mage.cards.f.Froghemoth.class)); + cards.add(new SetCardInfo("Gelatinous Cube", 105, Rarity.RARE, mage.cards.g.GelatinousCube.class)); cards.add(new SetCardInfo("Gloom Stalker", 16, Rarity.COMMON, mage.cards.g.GloomStalker.class)); cards.add(new SetCardInfo("Gnoll Hunter", 185, Rarity.COMMON, mage.cards.g.GnollHunter.class)); cards.add(new SetCardInfo("Goblin Javelineer", 144, Rarity.COMMON, mage.cards.g.GoblinJavelineer.class)); @@ -163,6 +166,7 @@ public final class AdventuresInTheForgottenRealms extends ExpansionSet { cards.add(new SetCardInfo("Minimus Containment", 24, Rarity.COMMON, mage.cards.m.MinimusContainment.class)); cards.add(new SetCardInfo("Minion of the Mighty", 156, Rarity.RARE, mage.cards.m.MinionOfTheMighty.class)); cards.add(new SetCardInfo("Minsc, Beloved Ranger", 227, Rarity.MYTHIC, mage.cards.m.MinscBelovedRanger.class)); + cards.add(new SetCardInfo("Monk Class", 228, Rarity.RARE, mage.cards.m.MonkClass.class)); cards.add(new SetCardInfo("Monk of the Open Hand", 25, Rarity.UNCOMMON, mage.cards.m.MonkOfTheOpenHand.class)); cards.add(new SetCardInfo("Moon-Blessed Cleric", 26, Rarity.UNCOMMON, mage.cards.m.MoonBlessedCleric.class)); cards.add(new SetCardInfo("Mordenkainen", 64, Rarity.MYTHIC, mage.cards.m.Mordenkainen.class)); @@ -170,6 +174,7 @@ public final class AdventuresInTheForgottenRealms extends ExpansionSet { cards.add(new SetCardInfo("Mountain", 274, Rarity.LAND, mage.cards.basiclands.Mountain.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Nadaar, Selfless Paladin", 27, Rarity.RARE, mage.cards.n.NadaarSelflessPaladin.class)); cards.add(new SetCardInfo("Neverwinter Dryad", 195, Rarity.COMMON, mage.cards.n.NeverwinterDryad.class)); + cards.add(new SetCardInfo("Ochre Jelly", 196, Rarity.RARE, mage.cards.o.OchreJelly.class)); cards.add(new SetCardInfo("Old Gnawbone", 197, Rarity.MYTHIC, mage.cards.o.OldGnawbone.class)); cards.add(new SetCardInfo("Orb of Dragonkind", 157, Rarity.RARE, mage.cards.o.OrbOfDragonkind.class)); cards.add(new SetCardInfo("Orcus, Prince of Undeath", 229, Rarity.RARE, mage.cards.o.OrcusPrinceOfUndeath.class)); @@ -200,6 +205,7 @@ public final class AdventuresInTheForgottenRealms extends ExpansionSet { cards.add(new SetCardInfo("Reaper's Talisman", 117, Rarity.UNCOMMON, mage.cards.r.ReapersTalisman.class)); cards.add(new SetCardInfo("Red Dragon", 160, Rarity.UNCOMMON, mage.cards.r.RedDragon.class)); cards.add(new SetCardInfo("Rimeshield Frost Giant", 69, Rarity.COMMON, mage.cards.r.RimeshieldFrostGiant.class)); + cards.add(new SetCardInfo("Rogue Class", 230, Rarity.RARE, mage.cards.r.RogueClass.class)); cards.add(new SetCardInfo("Rust Monster", 161, Rarity.UNCOMMON, mage.cards.r.RustMonster.class)); cards.add(new SetCardInfo("Scaled Herbalist", 204, Rarity.COMMON, mage.cards.s.ScaledHerbalist.class)); cards.add(new SetCardInfo("Scion of Stygia", 70, Rarity.COMMON, mage.cards.s.ScionOfStygia.class)); @@ -212,7 +218,9 @@ public final class AdventuresInTheForgottenRealms extends ExpansionSet { cards.add(new SetCardInfo("Silver Raven", 74, Rarity.COMMON, mage.cards.s.SilverRaven.class)); cards.add(new SetCardInfo("Skeletal Swarming", 232, Rarity.RARE, mage.cards.s.SkeletalSwarming.class)); cards.add(new SetCardInfo("Skullport Merchant", 120, Rarity.UNCOMMON, mage.cards.s.SkullportMerchant.class)); + cards.add(new SetCardInfo("Sorcerer Class", 233, Rarity.RARE, mage.cards.s.SorcererClass.class)); cards.add(new SetCardInfo("Soulknife Spy", 75, Rarity.COMMON, mage.cards.s.SoulknifeSpy.class)); + cards.add(new SetCardInfo("Spare Dagger", 250, Rarity.COMMON, mage.cards.s.SpareDagger.class)); cards.add(new SetCardInfo("Sphere of Annihilation", 121, Rarity.RARE, mage.cards.s.SphereOfAnnihilation.class)); cards.add(new SetCardInfo("Spiked Pit Trap", 251, Rarity.COMMON, mage.cards.s.SpikedPitTrap.class)); cards.add(new SetCardInfo("Split the Party", 76, Rarity.UNCOMMON, mage.cards.s.SplitTheParty.class)); @@ -237,6 +245,7 @@ public final class AdventuresInTheForgottenRealms extends ExpansionSet { cards.add(new SetCardInfo("Treasure Chest", 252, Rarity.RARE, mage.cards.t.TreasureChest.class)); cards.add(new SetCardInfo("Treasure Vault", 261, Rarity.RARE, mage.cards.t.TreasureVault.class)); cards.add(new SetCardInfo("Trelasarra, Moon Dancer", 236, Rarity.UNCOMMON, mage.cards.t.TrelasarraMoonDancer.class)); + cards.add(new SetCardInfo("Trickster's Talisman", 79, Rarity.UNCOMMON, mage.cards.t.TrickstersTalisman.class)); cards.add(new SetCardInfo("Triumphant Adventurer", 237, Rarity.RARE, mage.cards.t.TriumphantAdventurer.class)); cards.add(new SetCardInfo("True Polymorph", 80, Rarity.RARE, mage.cards.t.TruePolymorph.class)); cards.add(new SetCardInfo("Underdark Basilisk", 208, Rarity.COMMON, mage.cards.u.UnderdarkBasilisk.class)); @@ -256,9 +265,11 @@ public final class AdventuresInTheForgottenRealms extends ExpansionSet { cards.add(new SetCardInfo("Wild Shape", 212, Rarity.UNCOMMON, mage.cards.w.WildShape.class)); cards.add(new SetCardInfo("Wish", 166, Rarity.RARE, mage.cards.w.Wish.class)); cards.add(new SetCardInfo("Wizard Class", 81, Rarity.UNCOMMON, mage.cards.w.WizardClass.class)); + cards.add(new SetCardInfo("Wizard's Spellbook", 82, Rarity.RARE, mage.cards.w.WizardsSpellbook.class)); cards.add(new SetCardInfo("Xorn", 167, Rarity.RARE, mage.cards.x.Xorn.class)); cards.add(new SetCardInfo("You Come to a River", 83, Rarity.COMMON, mage.cards.y.YouComeToARiver.class)); cards.add(new SetCardInfo("You Come to the Gnoll Camp", 168, Rarity.COMMON, mage.cards.y.YouComeToTheGnollCamp.class)); + cards.add(new SetCardInfo("You Find Some Prisoners", 169, Rarity.UNCOMMON, mage.cards.y.YouFindSomePrisoners.class)); cards.add(new SetCardInfo("You Find a Cursed Idol", 213, Rarity.COMMON, mage.cards.y.YouFindACursedIdol.class)); cards.add(new SetCardInfo("You Find the Villains' Lair", 84, Rarity.COMMON, mage.cards.y.YouFindTheVillainsLair.class)); cards.add(new SetCardInfo("You Happen On a Glade", 214, Rarity.UNCOMMON, mage.cards.y.YouHappenOnAGlade.class)); @@ -270,6 +281,7 @@ public final class AdventuresInTheForgottenRealms extends ExpansionSet { cards.add(new SetCardInfo("Yuan-Ti Fang-Blade", 128, Rarity.COMMON, mage.cards.y.YuanTiFangBlade.class)); cards.add(new SetCardInfo("Yuan-Ti Malison", 86, Rarity.RARE, mage.cards.y.YuanTiMalison.class)); cards.add(new SetCardInfo("Zalto, Fire Giant Duke", 171, Rarity.RARE, mage.cards.z.ZaltoFireGiantDuke.class)); + cards.add(new SetCardInfo("Zariel, Archduke of Avernus", 172, Rarity.MYTHIC, mage.cards.z.ZarielArchdukeOfAvernus.class)); cards.add(new SetCardInfo("Zombie Ogre", 129, Rarity.COMMON, mage.cards.z.ZombieOgre.class)); } } diff --git a/Mage.Sets/src/mage/sets/ForgottenRealmsCommander.java b/Mage.Sets/src/mage/sets/ForgottenRealmsCommander.java index da14a41a9b..b6a7c190cc 100644 --- a/Mage.Sets/src/mage/sets/ForgottenRealmsCommander.java +++ b/Mage.Sets/src/mage/sets/ForgottenRealmsCommander.java @@ -56,6 +56,7 @@ public final class ForgottenRealmsCommander extends ExpansionSet { cards.add(new SetCardInfo("Chittering Witch", 95, Rarity.RARE, mage.cards.c.ChitteringWitch.class)); cards.add(new SetCardInfo("Choked Estuary", 228, Rarity.RARE, mage.cards.c.ChokedEstuary.class)); cards.add(new SetCardInfo("Cinder Glade", 229, Rarity.RARE, mage.cards.c.CinderGlade.class)); + cards.add(new SetCardInfo("Clay Golem", 58, Rarity.UNCOMMON, mage.cards.c.ClayGolem.class)); cards.add(new SetCardInfo("Cloudblazer", 182, Rarity.UNCOMMON, mage.cards.c.Cloudblazer.class)); cards.add(new SetCardInfo("Cold-Eyed Selkie", 183, Rarity.RARE, mage.cards.c.ColdEyedSelkie.class)); cards.add(new SetCardInfo("Colossal Majesty", 154, Rarity.UNCOMMON, mage.cards.c.ColossalMajesty.class)); @@ -87,6 +88,7 @@ public final class ForgottenRealmsCommander extends ExpansionSet { cards.add(new SetCardInfo("Dragonmaster Outcast", 124, Rarity.MYTHIC, mage.cards.d.DragonmasterOutcast.class)); cards.add(new SetCardInfo("Dragonspeaker Shaman", 330, Rarity.UNCOMMON, mage.cards.d.DragonspeakerShaman.class)); cards.add(new SetCardInfo("Dream Pillager", 125, Rarity.RARE, mage.cards.d.DreamPillager.class)); + cards.add(new SetCardInfo("Ebony Fly", 60, Rarity.UNCOMMON, mage.cards.e.EbonyFly.class)); cards.add(new SetCardInfo("Eel Umbra", 83, Rarity.COMMON, mage.cards.e.EelUmbra.class)); cards.add(new SetCardInfo("Esper Panorama", 235, Rarity.COMMON, mage.cards.e.EsperPanorama.class)); cards.add(new SetCardInfo("Etali, Primal Storm", 126, Rarity.RARE, mage.cards.e.EtaliPrimalStorm.class)); @@ -96,7 +98,9 @@ public final class ForgottenRealmsCommander extends ExpansionSet { cards.add(new SetCardInfo("Explorer's Scope", 205, Rarity.COMMON, mage.cards.e.ExplorersScope.class)); cards.add(new SetCardInfo("Fellwar Stone", 206, Rarity.UNCOMMON, mage.cards.f.FellwarStone.class)); cards.add(new SetCardInfo("Fertile Ground", 158, Rarity.COMMON, mage.cards.f.FertileGround.class)); + cards.add(new SetCardInfo("Fey Steed", 5, Rarity.RARE, mage.cards.f.FeySteed.class)); cards.add(new SetCardInfo("Fiend of the Shadows", 99, Rarity.RARE, mage.cards.f.FiendOfTheShadows.class)); + cards.add(new SetCardInfo("Fiendlash", 31, Rarity.RARE, mage.cards.f.Fiendlash.class)); cards.add(new SetCardInfo("Fleecemane Lion", 185, Rarity.RARE, mage.cards.f.FleecemaneLion.class)); cards.add(new SetCardInfo("Flood Plain", 237, Rarity.UNCOMMON, mage.cards.f.FloodPlain.class)); cards.add(new SetCardInfo("Forbidden Alchemy", 84, Rarity.UNCOMMON, mage.cards.f.ForbiddenAlchemy.class)); @@ -109,6 +113,7 @@ public final class ForgottenRealmsCommander extends ExpansionSet { cards.add(new SetCardInfo("Grasslands", 242, Rarity.UNCOMMON, mage.cards.g.Grasslands.class)); cards.add(new SetCardInfo("Gratuitous Violence", 127, Rarity.RARE, mage.cards.g.GratuitousViolence.class)); cards.add(new SetCardInfo("Greater Good", 160, Rarity.RARE, mage.cards.g.GreaterGood.class)); + cards.add(new SetCardInfo("Grim Hireling", 25, Rarity.RARE, mage.cards.g.GrimHireling.class)); cards.add(new SetCardInfo("Gruul Signet", 207, Rarity.UNCOMMON, mage.cards.g.GruulSignet.class)); cards.add(new SetCardInfo("Gruul Turf", 243, Rarity.UNCOMMON, mage.cards.g.GruulTurf.class)); cards.add(new SetCardInfo("Gryff's Boon", 67, Rarity.UNCOMMON, mage.cards.g.GryffsBoon.class)); @@ -136,6 +141,7 @@ public final class ForgottenRealmsCommander extends ExpansionSet { cards.add(new SetCardInfo("Masterwork of Ingenuity", 209, Rarity.RARE, mage.cards.m.MasterworkOfIngenuity.class)); cards.add(new SetCardInfo("Merfolk Looter", 86, Rarity.UNCOMMON, mage.cards.m.MerfolkLooter.class)); cards.add(new SetCardInfo("Meteor Golem", 210, Rarity.UNCOMMON, mage.cards.m.MeteorGolem.class)); + cards.add(new SetCardInfo("Midnight Pathlighter", 52, Rarity.RARE, mage.cards.m.MidnightPathlighter.class)); cards.add(new SetCardInfo("Mind Stone", 211, Rarity.UNCOMMON, mage.cards.m.MindStone.class)); cards.add(new SetCardInfo("Mishra's Factory", 248, Rarity.UNCOMMON, mage.cards.m.MishrasFactory.class)); cards.add(new SetCardInfo("Moonsilver Spear", 212, Rarity.RARE, mage.cards.m.MoonsilverSpear.class)); @@ -165,6 +171,7 @@ public final class ForgottenRealmsCommander extends ExpansionSet { cards.add(new SetCardInfo("Prairie Stream", 256, Rarity.RARE, mage.cards.p.PrairieStream.class)); cards.add(new SetCardInfo("Prognostic Sphinx", 90, Rarity.RARE, mage.cards.p.PrognosticSphinx.class)); cards.add(new SetCardInfo("Propaganda", 91, Rarity.UNCOMMON, mage.cards.p.Propaganda.class)); + cards.add(new SetCardInfo("Prosper, Tome-Bound", 2, Rarity.MYTHIC, mage.cards.p.ProsperTomeBound.class)); cards.add(new SetCardInfo("Psychic Impetus", 92, Rarity.UNCOMMON, mage.cards.p.PsychicImpetus.class)); cards.add(new SetCardInfo("Puresteel Paladin", 69, Rarity.RARE, mage.cards.p.PuresteelPaladin.class)); cards.add(new SetCardInfo("Radiant Solar", 9, Rarity.RARE, mage.cards.r.RadiantSolar.class)); @@ -186,6 +193,7 @@ public final class ForgottenRealmsCommander extends ExpansionSet { cards.add(new SetCardInfo("Savage Ventmaw", 191, Rarity.UNCOMMON, mage.cards.s.SavageVentmaw.class)); cards.add(new SetCardInfo("Scourge of Valkas", 137, Rarity.RARE, mage.cards.s.ScourgeOfValkas.class)); cards.add(new SetCardInfo("Seaside Citadel", 258, Rarity.UNCOMMON, mage.cards.s.SeasideCitadel.class)); + cards.add(new SetCardInfo("Sefris of the Hidden Ways", 3, Rarity.MYTHIC, mage.cards.s.SefrisOfTheHiddenWays.class)); cards.add(new SetCardInfo("Serum Visions", 94, Rarity.UNCOMMON, mage.cards.s.SerumVisions.class)); cards.add(new SetCardInfo("Shadowblood Ridge", 259, Rarity.RARE, mage.cards.s.ShadowbloodRidge.class)); cards.add(new SetCardInfo("Shamanic Revelation", 171, Rarity.RARE, mage.cards.s.ShamanicRevelation.class)); @@ -204,6 +212,7 @@ public final class ForgottenRealmsCommander extends ExpansionSet { cards.add(new SetCardInfo("Spinerock Knoll", 263, Rarity.RARE, mage.cards.s.SpinerockKnoll.class)); cards.add(new SetCardInfo("Spit Flame", 142, Rarity.RARE, mage.cards.s.SpitFlame.class)); cards.add(new SetCardInfo("Sram, Senior Edificer", 72, Rarity.RARE, mage.cards.s.SramSeniorEdificer.class)); + cards.add(new SetCardInfo("Storvald, Frost Giant Jarl", 55, Rarity.MYTHIC, mage.cards.s.StorvaldFrostGiantJarl.class)); cards.add(new SetCardInfo("Sun Titan", 73, Rarity.MYTHIC, mage.cards.s.SunTitan.class)); cards.add(new SetCardInfo("Sunblast Angel", 74, Rarity.RARE, mage.cards.s.SunblastAngel.class)); cards.add(new SetCardInfo("Sungrass Prairie", 264, Rarity.RARE, mage.cards.s.SungrassPrairie.class)); @@ -227,6 +236,7 @@ public final class ForgottenRealmsCommander extends ExpansionSet { cards.add(new SetCardInfo("Throes of Chaos", 146, Rarity.UNCOMMON, mage.cards.t.ThroesOfChaos.class)); cards.add(new SetCardInfo("Thunderbreak Regent", 147, Rarity.RARE, mage.cards.t.ThunderbreakRegent.class)); cards.add(new SetCardInfo("Unburial Rites", 111, Rarity.UNCOMMON, mage.cards.u.UnburialRites.class)); + cards.add(new SetCardInfo("Underdark Rift", 62, Rarity.UNCOMMON, mage.cards.u.UnderdarkRift.class)); cards.add(new SetCardInfo("Unstable Obelisk", 220, Rarity.UNCOMMON, mage.cards.u.UnstableObelisk.class)); cards.add(new SetCardInfo("Utopia Sprawl", 172, Rarity.UNCOMMON, mage.cards.u.UtopiaSprawl.class)); cards.add(new SetCardInfo("Utter End", 195, Rarity.RARE, mage.cards.u.UtterEnd.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/continuous/LayerTests.java b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/LayerTests.java index 42cbe4458e..af0e70cef3 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/continuous/LayerTests.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/LayerTests.java @@ -1,7 +1,6 @@ - - package org.mage.test.cards.continuous; +import mage.ObjectColor; import mage.constants.CardType; import mage.constants.PhaseStep; import mage.constants.SubType; @@ -16,7 +15,7 @@ import org.mage.test.serverside.base.CardTestPlayerBase; * @author jeffwadsworth */ public class LayerTests extends CardTestPlayerBase { - + @Test public void testMultipleLayeredDependency() { //Conspiracy->Opalescence->Enchanted Evening @@ -24,33 +23,33 @@ public class LayerTests extends CardTestPlayerBase { //Opalescence is dependent on Enchanted Evening //So, the effects should be applied as follows: //Enchanted Evening->Opalescence->Conspiracy - + addCard(Zone.HAND, playerA, "Conspiracy"); // creatures get chosen subtype addCard(Zone.HAND, playerA, "Opalescence"); // enchantments become creatures P/T equal to CMC addCard(Zone.HAND, playerA, "Enchanted Evening"); // all permanents become enchantments - + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 5); addCard(Zone.BATTLEFIELD, playerA, "Plains", 5); addCard(Zone.BATTLEFIELD, playerA, "Island", 5); addCard(Zone.BATTLEFIELD, playerA, "Glorious Anthem", 1); // keep lands alive // all creatures +1/+1 - + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Conspiracy"); setChoice(playerA, "Advisor"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Opalescence"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Enchanted Evening"); - + setStrictChooseMode(true); setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); execute(); assertAllCommandsUsed(); - + assertType("Swamp", CardType.LAND, SubType.ADVISOR); // Subtype changed with Conspiracy assertPowerToughness(playerA, "Swamp", 1, 1); // boosted with Glorious Anthem assertType("Enchanted Evening", CardType.ENCHANTMENT, SubType.ADVISOR); // Subtype changed with Conspiracy assertPowerToughness(playerA, "Enchanted Evening", 6, 6); // boosted with Glorious Anthem - + } - + @Test public void testMycosynthLatticeAndMarchOfTheMachinesAndHumility() { // example from Reddit @@ -61,32 +60,32 @@ public class LayerTests extends CardTestPlayerBase { Does the game get stuck in an endless loop of each card gaining and losing its respective creature-ness and abilities? Answer: No, they all die - */ - + */ + addCard(Zone.HAND, playerA, "Mycosynth Lattice"); // all permanents are artifacts addCard(Zone.HAND, playerA, "March of the Machines"); // artifacts become creatures addCard(Zone.HAND, playerA, "Humility"); // all creatures lose abilities and P/T is 1/1 - + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 10); addCard(Zone.BATTLEFIELD, playerA, "Plains", 10); addCard(Zone.BATTLEFIELD, playerA, "Island", 10); - + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Humility"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "March of the Machines"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Mycosynth Lattice"); - + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); execute(); assertAllCommandsUsed(); - + // everything dies assertPermanentCount(playerA, "Humility", 0); assertPermanentCount(playerA, "March of the Machines", 0); assertPermanentCount(playerA, "Mycosynth Lattice", 0); assertPermanentCount(playerA, "Island", 0); - + } - + @Test public void testBloodMoon_UrborgTombOfYawgmothInteraction() { // Blood Moon : Nonbasic lands are Mountains. @@ -95,7 +94,7 @@ public class LayerTests extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerA, "Blood Moon"); addCard(Zone.BATTLEFIELD, playerA, "Urborg, Tomb of Yawgmoth", 1); // non-basic land addCard(Zone.BATTLEFIELD, playerA, "Plains", 1); - + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); execute(); @@ -112,35 +111,35 @@ public class LayerTests extends CardTestPlayerBase { /*In play there is a Grizzly Bears which has already been Giant Growthed, a Bog Wraith enchanted by a Lignify, and Figure of Destiny with its 3rd ability activated. I then cast a Mirrorweave targeting the Figure of Destiny. What does each creature look like? - */ + */ addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears", 1); addCard(Zone.HAND, playerA, "Giant Growth", 1); addCard(Zone.BATTLEFIELD, playerA, "Bog Wraith", 1); addCard(Zone.HAND, playerA, "Lignify", 1); addCard(Zone.BATTLEFIELD, playerA, "Figure of Destiny", 1); addCard(Zone.HAND, playerA, "Mirrorweave", 1); - + addCard(Zone.BATTLEFIELD, playerA, "Forest", 20); addCard(Zone.BATTLEFIELD, playerA, "Island", 20); addCard(Zone.BATTLEFIELD, playerA, "Plains", 20); - + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Giant Growth", "Grizzly Bears"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lignify", "Bog Wrath"); activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{R/W}:"); activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{R/W}{R/W}{R/W}:"); activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{R/W}{R/W}{R/W}{R/W}{R/W}{R/W}:"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Mirrorweave", "Figure of Destiny"); - + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); execute(); - + assertPermanentCount(playerA, "Figure of Destiny", 3); assertPowerToughness(playerA, "Figure of Destiny", 4, 4, Filter.ComparisonScope.All); assertPowerToughness(playerA, "Figure of Destiny", 8, 8, Filter.ComparisonScope.All); assertPowerToughness(playerA, "Figure of Destiny", 0, 4, Filter.ComparisonScope.All); } - + @Test public void testUrborgWithAnimateLandAndOvinize() { // Animate Land: target land is a 3/3 until end of turn and is still a land. @@ -152,7 +151,7 @@ public class LayerTests extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerA, "Urborg, Tomb of Yawgmoth", 1); addCard(Zone.BATTLEFIELD, playerA, "Forest", 1); addCard(Zone.BATTLEFIELD, playerA, "Island", 2); - + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Animate Land", "Urborg, Tomb of Yawgmoth"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Ovinize", "Urborg, Tomb of Yawgmoth"); setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); @@ -172,25 +171,79 @@ public class LayerTests extends CardTestPlayerBase { one of his Hypnotist's abilities, targeting the Mimic. Aiden attacks with the Mimic, and casts Inside Out before the damage step. Once Inside Out resolves, Nick activates the ability of his other Hypnotist. How much damage will the Mimic deal? - */ + */ addCard(Zone.HAND, playerA, "Scourge of the Nobilis", 1); addCard(Zone.HAND, playerA, "Inside Out", 1); addCard(Zone.BATTLEFIELD, playerA, "Battlegate Mimic", 1); addCard(Zone.BATTLEFIELD, playerA, "Mountain", 8); addCard(Zone.BATTLEFIELD, playerA, "Island", 8); - + addCard(Zone.BATTLEFIELD, playerB, "Wilderness Hypnotist", 2); - + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Scourge of the Nobilis", "Battlegate Mimic"); activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerB, "{T}:", "Battlegate Mimic"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Inside Out", "Battlegate Mimic"); activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerB, "{T}:", "Battlegate Mimic"); - + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); execute(); assertPowerToughness(playerA, "Battlegate Mimic", 4, 2); } - + + @Test + public void testExampleFromReddit2021() { + // Life and Limb, Humility, and Yavimaya, Cradle of Growth + // Result: all lands (including Yavimaya) will be 1/1 green Forest + // lands and Saproling creatures in addition to their other types, + // and have no abilities. + + /* + All Forests and all Saprolings are 1/1 green Saproling creatures + and Forest lands in addition to their other types. (They're affected by summoning sickness.) + */ + addCard(Zone.BATTLEFIELD, playerA, "Life and Limb", 1); + + /* + All creatures lose all abilities and have base power and toughness 1/1. + */ + addCard(Zone.BATTLEFIELD, playerA, "Humility", 1); + + /* + Each land is a Forest in addition to its other land types. + */ + addCard(Zone.BATTLEFIELD, playerA, "Yavimaya, Cradle of Growth", 1); + + // added some lands to the battlefield + + addCard(Zone.BATTLEFIELD, playerA, "Plains", 1); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1); + addCard(Zone.BATTLEFIELD, playerB, "Island", 1); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + assertAllCommandsUsed(); + + // all lands are forests in addition to other types + assertType("Plains", CardType.CREATURE, SubType.FOREST); + assertType("Swamp", CardType.CREATURE, SubType.FOREST); + assertType("Island", CardType.CREATURE, SubType.FOREST); + assertType("Yavimaya, Cradle of Growth", CardType.CREATURE, SubType.FOREST); + + // all lands are 1/1 Saproling creatures + assertPowerToughness(playerA, "Plains", 1, 1); + assertPowerToughness(playerA, "Swamp", 1, 1); + assertPowerToughness(playerB, "Island", 1, 1); + assertPowerToughness(playerA, "Yavimaya, Cradle of Growth", 1, 1); + + // all lands are green + assertColor(playerA, "Plains", ObjectColor.GREEN, true); + assertColor(playerA, "Swamp", ObjectColor.GREEN, true); + assertColor(playerB, "Island", ObjectColor.GREEN, true); + assertColor(playerA, "Yavimaya, Cradle of Growth", ObjectColor.GREEN, true); + + } + } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/copy/CopySpellTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/copy/CopySpellTest.java index 4dc44d1b3c..835bcfdd92 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/copy/CopySpellTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/copy/CopySpellTest.java @@ -9,6 +9,7 @@ import mage.cards.SplitCard; import mage.cards.repository.CardRepository; import mage.constants.PhaseStep; import mage.constants.Zone; +import mage.counters.CounterType; import mage.util.CardUtil; import org.junit.Assert; import org.junit.Test; @@ -522,6 +523,138 @@ public class CopySpellTest extends CardTestPlayerBase { assertAllCommandsUsed(); } + @Test + public void test_CopiedSpellsAndX_1() { + // testing: + // 1. x in copied instant spell (copy X) + // 2. x in copied creature (X=0) + + // test use case with rules: + // https://tappedout.net/mtg-questions/copying-a-creature-with-x-in-its-mana-cost/#c3561513 + // 107.3f If a card in any zone other than the stack has an {X} in its mana cost, the value of {X} is + // treated as 0, even if the value of X is defined somewhere within its text. + + // Whenever you cast an instant or sorcery spell, you may pay {U}{R}. If you do, copy that spell. You may choose new targets for the copy. + // Whenever another nontoken creature enters the battlefield under your control, you may pay {G}{U}. If you do, create a token that’s a copy of that creature. + addCard(Zone.BATTLEFIELD, playerA, "Riku of Two Reflections", 1); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); + addCard(Zone.BATTLEFIELD, playerA, "Island", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + // + // Banefire deals X damage to any target. + addCard(Zone.HAND, playerA, "Banefire", 1); // {X}{R} + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); + // + // 0/0 + // Capricopian enters the battlefield with X +1/+1 counters on it. + addCard(Zone.HAND, playerA, "Capricopian", 1); // {X}{G} + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); + + // 1 + // cast banefire and make copy + // announced X=2 must be copied + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}", 3); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Banefire", playerB); + setChoice(playerA, "X=2"); + setChoice(playerA, "Yes"); // make copy + setChoice(playerA, "No"); // keep target same + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkLife("after spell copy", 1, PhaseStep.PRECOMBAT_MAIN, playerB, 20 - 2 * 2); + + // 2 + // cast creature and copy it as token + // token must have x=0 (dies) + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Capricopian"); + setChoice(playerA, "X=1"); + setChoice(playerA, "Yes"); // make copy + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkPermanentCount("after creature copy", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Capricopian", 1); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + } + + @Test + public void test_CopiedSpellsHasntETB() { + // testing: + // - x in copied creature spell (copy x) + // - copied spells enters as tokens and it hasn't ETB, see rules below + + // 0/0 + // Capricopian enters the battlefield with X +1/+1 counters on it. + addCard(Zone.HAND, playerA, "Capricopian", 1); // {X}{G} + addCard(Zone.BATTLEFIELD, playerA, "Forest", 3); + // + // Grenzo, Dungeon Warden enters the battlefield with X +1/+1 counters on it. + addCard(Zone.HAND, playerA, "Grenzo, Dungeon Warden", 1);// {X}{B}{R} + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + // + // Copy target creature spell you control, except it isn't legendary if the spell is legendary. + // (A copy of a creature spell becomes a token.) + addCard(Zone.HAND, playerA, "Double Major", 2); // {G}{U} + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); + addCard(Zone.BATTLEFIELD, playerA, "Island", 2); + + // 1. Capricopian + // cast and put on stack + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {G}", 3); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Capricopian"); + setChoice(playerA, "X=2"); + // copy of spell + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {G}", 1); + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {U}", 1); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Double Major", "Capricopian", "Capricopian"); + + // 2. Grenzo, Dungeon Warden + // cast and put on stack + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {G}", 2); + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {B}", 1); + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}", 1); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Grenzo, Dungeon Warden"); + setChoice(playerA, "X=2"); + // copy of spell + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {G}", 1); + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {U}", 1); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Double Major", "Grenzo, Dungeon Warden", "Grenzo, Dungeon Warden"); + + // ETB triggers will not trigger here due not normal cast. From rules: + // - The token that a resolving copy of a spell becomes isn’t said to have been “created.” (2021-04-16) + // - A nontoken permanent “enters the battlefield” when it’s moved onto the battlefield from another zone. + // A token “enters the battlefield” when it’s created. See rules 403.3, 603.6a, 603.6d, and 614.12. + // + // So both copies enters without counters: + // - Capricopian copy must die + // - Grenzo, Dungeon Warden must have default PT + + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkPermanentCount("after", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Capricopian", 1); // copy dies + checkPermanentCount("after", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Grenzo, Dungeon Warden", 2); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + // counters checks + int originalCounters = currentGame.getBattlefield().getAllActivePermanents().stream() + .filter(p -> p.getName().equals("Grenzo, Dungeon Warden")) + .filter(p -> !p.isCopy()) + .mapToInt(p -> p.getCounters(currentGame).getCount(CounterType.P1P1)) + .sum(); + int copyCounters = currentGame.getBattlefield().getAllActivePermanents().stream() + .filter(p -> p.getName().equals("Grenzo, Dungeon Warden")) + .filter(p -> p.isCopy()) + .mapToInt(p -> p.getCounters(currentGame).getCount(CounterType.P1P1)) + .sum(); + Assert.assertEquals("original grenzo must have 2x counters", 2, originalCounters); + Assert.assertEquals("copied grenzo must have 0x counters", 0, copyCounters); + } + @Test public void test_SimpleCopy_Card() { Card sourceCard = CardRepository.instance.findCard("Grizzly Bears").getCard(); @@ -628,7 +761,7 @@ public class CopySpellTest extends CardTestPlayerBase { return; } - Assert.fail(infoPrefix + " - " + "ability source must be same: " + ability.toString()); + Assert.fail(infoPrefix + " - " + "ability source must be same: " + ability); } }); } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/triggers/RanarTheEverWatchfulTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/triggers/RanarTheEverWatchfulTest.java new file mode 100644 index 0000000000..10c4525c41 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/triggers/RanarTheEverWatchfulTest.java @@ -0,0 +1,90 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package org.mage.test.cards.triggers; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author jeffwadsworth + */ +public class RanarTheEverWatchfulTest extends CardTestPlayerBase { + + @Test + public void testRanarWithCurseOfTheSwine() { + setStrictChooseMode(true); + // Curse of the Swine targets 4 creatures; 2 non-tokens and 2 tokens with Ranar, the Ever-Watchful on the battlefield + // Result: 2 boar tokens and 1 spirit token for the playerA. 2 boar tokens for playerB + + addCard(Zone.BATTLEFIELD, playerA, "Ranar the Ever-Watchful", 1); + + /* + Whenever one or more cards are put into exile from your hand or a spell or ability you control exiles one or + more permanents from the battlefield, create a 1/1 white Spirit creature token with flying. + */ + + addCard(Zone.BATTLEFIELD, playerA, "Memnite", 1); // Creature 1/1 + addCard(Zone.BATTLEFIELD, playerA, "Serra Angel", 1); // Creature 4/4 Flyer + addCard(Zone.BATTLEFIELD, playerA, "Bog Imp", 1); // Creature 2/1 Flyer + addCard(Zone.BATTLEFIELD, playerB, "Sengir Vampire", 1); // Creature 4/4 Flyer + addCard(Zone.BATTLEFIELD, playerA, "Island", 10); // handle mana for Curse of the Swine + addCard(Zone.BATTLEFIELD, playerB, "Plains", 10); // handle mana for Call the Calvary + addCard(Zone.HAND, playerB, "Call the Cavalry", 1); // generate 2 tokens for playerB + addCard(Zone.HAND, playerA, "Curse of the Swine", 1); // exile target cards and create a boar token for each one exiled + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Call the Cavalry"); // create 2 Knight tokens + + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Curse of the Swine"); + setChoice(playerA, "X=6"); + addTarget(playerA, "Memnite"); + addTarget(playerA, "Serra Angel"); + addTarget(playerA, "Bog Imp"); + addTarget(playerA, "Sengir Vampire"); + addTarget(playerA, "Knight"); + addTarget(playerA, "Knight"); + + setStopAt(4, PhaseStep.POSTCOMBAT_MAIN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Boar", 3); // created token by Curse + assertPermanentCount(playerA, "Spirit", 1); // created token by Ranar + assertPermanentCount(playerB, "Boar", 3); // created token by Curse + assertPermanentCount(playerB, "Spirit", 0); // no Spirit token for playerB + } + + @Test + public void testRanarExiledCardsFromHand() { + setStrictChooseMode(true); + // A card is exiled from playerA's hand + // Result: Due to the card being exiled from playerA's hand, a Spirit token is created for playerA + + addCard(Zone.BATTLEFIELD, playerA, "Ranar the Ever-Watchful", 1); + + /* + Whenever one or more cards are put into exile from your hand or a spell or ability you control exiles one or + more permanents from the battlefield, create a 1/1 white Spirit creature token with flying. + */ + + addCard(Zone.HAND, playerA, "Psychic Theft", 1); // exile instant/sorcery card from target player's hand + addCard(Zone.BATTLEFIELD, playerA, "Island", 2); // handle mana for Psychic Theft + addCard(Zone.HAND, playerA, "Lightning Bolt", 1); // instant spell card + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Psychic Theft", playerA); + setChoice(playerA, "Lightning Bolt"); + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Spirit", 1); // created token by Ranar + assertPermanentCount(playerB, "Spirit", 0); // no Spirit tokens for the other player + } + +} diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/CardIconsTest.java b/Mage.Tests/src/test/java/org/mage/test/serverside/CardIconsTest.java new file mode 100644 index 0000000000..dae5855e90 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/CardIconsTest.java @@ -0,0 +1,224 @@ +package org.mage.test.serverside; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.view.CardView; +import mage.view.GameView; +import mage.view.PlayerView; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * GUI tests: card icons for cards + * + * @author JayDi85 + */ +public class CardIconsTest extends CardTestPlayerBase { + + @Test + public void test_CostX_Spells() { + // Chalice of the Void enters the battlefield with X charge counters on it. + // Whenever a player casts a spell with converted mana cost equal to the number of charge counters on Chalice of the Void, counter that spell. + addCard(Zone.HAND, playerA, "Chalice of the Void", 1); // {X}{X} + addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); + + // hand (not visible) + runCode("card icons in hand", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> { + GameView gameView = getGameView(player); + Assert.assertEquals("must have 1 card in hand", 1, gameView.getHand().values().size()); + CardView cardView = gameView.getHand().values().stream().findFirst().get(); + Assert.assertEquals("must have non x cost card icons in hand", 0, cardView.getCardIcons().size()); + }); + + // cast and put on stack + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Chalice of the Void"); + setChoice(playerA, "X=2"); + + // stack (visible) + runCode("card icons on stack (spell)", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> { + GameView gameView = getGameView(player); + Assert.assertEquals("must have 1 card in stack", 1, gameView.getStack().values().size()); + CardView cardView = gameView.getStack().values().stream().findFirst().get(); + Assert.assertEquals("must have x cost card icons in stack", 1, cardView.getCardIcons().size()); + Assert.assertEquals("x cost text", "x=2", cardView.getCardIcons().get(0).getText()); + }); + + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkPermanentCount("after cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Chalice of the Void", 1); + + // battlefield (card, not visible) + runCode("card icons in battlefield (card)", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> { + GameView gameView = getGameView(player); + PlayerView playerView = gameView.getPlayers().get(0); + Assert.assertEquals("player", player.getName(), playerView.getName()); + CardView cardView = playerView.getBattlefield().values().stream().filter(p -> p.getName().equals("Chalice of the Void")).findFirst().orElse(null); + Assert.assertNotNull("must have 1 chalice in battlefield", cardView); + Assert.assertEquals("must have x cost card icons in battlefield (card)", 1, cardView.getCardIcons().size()); + Assert.assertEquals("x cost text", "x=2", cardView.getCardIcons().get(0).getText()); + }); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + } + + @Test + public void test_CostX_Copies() { + // Grenzo, Dungeon Warden enters the battlefield with X +1/+1 counters on it. + addCard(Zone.HAND, playerA, "Grenzo, Dungeon Warden", 1);// {X}{B}{R} + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + // + // Copy target creature spell you control, except it isn't legendary if the spell is legendary. + addCard(Zone.HAND, playerA, "Double Major", 1); // {G}{U} + addCard(Zone.BATTLEFIELD, playerA, "Forest", 1); + addCard(Zone.BATTLEFIELD, playerA, "Island", 1); + + // cast and put on stack + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {G}", 2); + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {B}", 1); + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}", 1); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Grenzo, Dungeon Warden"); + setChoice(playerA, "X=2"); + + // prepare copy of spell + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {G}", 1); + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {U}", 1); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Double Major", "Grenzo, Dungeon Warden", "Grenzo, Dungeon Warden"); + checkStackSize("before copy spell", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 2); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA, true); + checkStackSize("after copy spell", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 2); + + // stack (copied spell) + runCode("card icons on stack (copied spell)", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> { + GameView gameView = getGameView(player); + Assert.assertEquals("must have 2 cards in stack", 2, gameView.getStack().values().size()); + CardView originalCardView = gameView.getStack().values() + .stream() + .filter(c -> !c.getOriginalCard().isCopy()) + .findFirst() + .get(); + CardView copiedCardView = gameView.getStack().values() + .stream() + .filter(c -> c.getOriginalCard().isCopy()) + .findFirst() + .get(); + Assert.assertNotNull("stack must have original spell", originalCardView); + Assert.assertNotNull("stack must have copied spell", copiedCardView); + Assert.assertNotEquals("must find two spells on stack", originalCardView.getId(), copiedCardView.getId()); + Assert.assertEquals("original spell must have x cost card icons", 1, originalCardView.getCardIcons().size()); + Assert.assertEquals("copied spell must have x cost card icons", 1, copiedCardView.getCardIcons().size()); + Assert.assertEquals("original x cost text", "x=2", originalCardView.getCardIcons().get(0).getText()); + Assert.assertEquals("copied x cost text", "x=2", copiedCardView.getCardIcons().get(0).getText()); + }); + + // must resolve copied creature spell as a token + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkPermanentCount("after cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Grenzo, Dungeon Warden", 2); + + // battlefield (card and copied card as token) + runCode("card icons in battlefield (copied)", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> { + GameView gameView = getGameView(player); + PlayerView playerView = gameView.getPlayers().get(0); + Assert.assertEquals("player", player.getName(), playerView.getName()); + // copied spell goes as token to battlefield, not copied card - so must check isToken + // original + CardView originalCardView = playerView.getBattlefield().values() + .stream() + .filter(p -> p.getName().equals("Grenzo, Dungeon Warden")) + .filter(p -> !p.isToken()) + .findFirst() + .orElse(null); + Assert.assertNotNull("original card must be in battlefield", originalCardView); + Assert.assertEquals("original must have x cost card icons", 1, originalCardView.getCardIcons().size()); + Assert.assertEquals("original x cost text", "x=2", originalCardView.getCardIcons().get(0).getText()); + // + CardView copiedCardView = playerView.getBattlefield().values() + .stream() + .filter(p -> p.getName().equals("Grenzo, Dungeon Warden")) + .filter(p -> p.isToken()) + .findFirst() + .orElse(null); + Assert.assertNotNull("copied card must be in battlefield", copiedCardView); + Assert.assertEquals("copied must have x cost card icons", 1, copiedCardView.getCardIcons().size()); + Assert.assertEquals("copied x cost text", "x=0", copiedCardView.getCardIcons().get(0).getText()); + }); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + } + + @Test + public void test_CostX_Abilities() { + // X icon must be visible only for activated ability, not spell cast + + // {X}{R}, {tap}, Sacrifice Cinder Elemental: Cinder Elemental deals X damage to any target. + addCard(Zone.HAND, playerA, "Cinder Elemental", 1); // {3}{R} + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4); + + // hand (not visible) + runCode("card icons in hand", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> { + GameView gameView = getGameView(player); + Assert.assertEquals("must have 1 card in hand", 1, gameView.getHand().values().size()); + CardView cardView = gameView.getHand().values().stream().findFirst().get(); + Assert.assertEquals("must have non x cost card icons in hand", 0, cardView.getCardIcons().size()); + }); + + // spell cast + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cinder Elemental"); + + // stack (spell cast - not visible) + runCode("card icons on stack (spell cast - not visible)", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> { + GameView gameView = getGameView(player); + Assert.assertEquals("must have 1 card in stack", 1, gameView.getStack().values().size()); + CardView cardView = gameView.getStack().values().stream().findFirst().get(); + Assert.assertEquals("must have not x cost card icons in stack", 0, cardView.getCardIcons().size()); + }); + + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkPermanentCount("after cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cinder Elemental", 1); + + // battlefield (card, not visible) + runCode("card icons in battlefield (card)", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> { + GameView gameView = getGameView(player); + PlayerView playerView = gameView.getPlayers().get(0); + Assert.assertEquals("player", player.getName(), playerView.getName()); + CardView cardView = playerView.getBattlefield().values().stream().filter(p -> p.getName().equals("Cinder Elemental")).findFirst().orElse(null); + Assert.assertNotNull("must have Cinder Elemental in battlefield", cardView); + Assert.assertEquals("must have not x cost card icons in battlefield (card)", 0, cardView.getCardIcons().size()); + }); + + // ACTIVATE ABILITY (x must be visible in stack, but not visible after resolve) + activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "{X}{R}"); + setChoice(playerA, "X=2"); + addTarget(playerA, playerB); + + // stack (ability activated - visible) + runCode("card icons on stack (ability activated - visible)", 3, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> { + GameView gameView = getGameView(player); + Assert.assertEquals("ability activated - must have 1 card in stack", 1, gameView.getStack().values().size()); + CardView cardView = gameView.getStack().values().stream().findFirst().get(); + Assert.assertEquals("ability activated - must have x cost card icons in stack", 1, cardView.getCardIcons().size()); + }); + + // battlefield (ability activated, not visible) + runCode("card icons in battlefield (ability activated)", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> { + GameView gameView = getGameView(player); + PlayerView playerView = gameView.getPlayers().get(0); + Assert.assertEquals("player", player.getName(), playerView.getName()); + CardView cardView = playerView.getBattlefield().values().stream().filter(p -> p.getName().equals("Cinder Elemental")).findFirst().orElse(null); + Assert.assertNotNull("ability activated - must have Cinder Elemental in battlefield", cardView); + Assert.assertEquals("ability activated - must have not x cost card icons in battlefield", 0, cardView.getCardIcons().size()); + }); + + setStrictChooseMode(true); + setStopAt(3, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/MageTestPlayerBase.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/MageTestPlayerBase.java index 533367ec58..2db1d1b67f 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/base/MageTestPlayerBase.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/MageTestPlayerBase.java @@ -25,6 +25,7 @@ import mage.cards.repository.CardRepository; import mage.constants.*; import mage.filter.StaticFilters; import mage.game.Game; +import mage.game.match.Match; import mage.game.match.MatchType; import mage.game.permanent.PermanentCard; import mage.game.tournament.TournamentType; @@ -92,6 +93,8 @@ public abstract class MageTestPlayerBase { */ protected static Game currentGame = null; + protected static Match currentMatch = null; + /** * Player thats starts the game first. By default, it is ComputerA. */ diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java index 0e9e6a09ef..55b6d09d81 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java @@ -16,19 +16,19 @@ import mage.counters.CounterType; import mage.filter.Filter; import mage.filter.FilterCard; import mage.filter.predicate.mageobject.NamePredicate; -import mage.game.ExileZone; -import mage.game.Game; -import mage.game.GameException; -import mage.game.GameOptions; +import mage.game.*; import mage.game.command.CommandObject; +import mage.game.match.MatchOptions; import mage.game.permanent.Permanent; import mage.game.permanent.PermanentCard; import mage.player.ai.ComputerPlayer7; import mage.player.ai.ComputerPlayerMCTS; import mage.players.ManaPool; import mage.players.Player; +import mage.server.game.GameSessionPlayer; import mage.server.util.SystemUtil; import mage.util.CardUtil; +import mage.view.GameView; import org.junit.Assert; import org.junit.Before; import org.mage.test.player.PlayerAction; @@ -216,6 +216,10 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement currentGame = null; } + // prepare fake match (needs for testing some client-server code) + // always 4 seats + MatchOptions matchOptions = new MatchOptions("test match", "test game type", true, 4); + currentMatch = new FreeForAllMatch(matchOptions); currentGame = createNewGameAndPlayers(); activePlayer = playerA; @@ -267,6 +271,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement game.loadCards(deck.getCards(), player.getId()); game.loadCards(deck.getSideboard(), player.getId()); game.addPlayer(player, deck); + currentMatch.addPlayer(player, deck); // fake match return player; } @@ -2116,4 +2121,8 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement Assert.assertFalse(player.getName() + " has lost the game.", player.hasLost()); } + public GameView getGameView(Player player) { + // prepare client-server data for tests + return GameSessionPlayer.prepareGameView(currentGame, player.getId(), null); + } } diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/performance/SerializationTest.java b/Mage.Tests/src/test/java/org/mage/test/serverside/performance/SerializationTest.java index 75f77d6bff..4234a680ca 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/performance/SerializationTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/performance/SerializationTest.java @@ -7,13 +7,13 @@ import mage.cards.repository.CardRepository; import mage.constants.PhaseStep; import mage.constants.Zone; import mage.counters.CounterType; -import mage.game.Game; import mage.game.mulligan.LondonMulligan; import mage.game.permanent.PermanentCard; import mage.game.permanent.PermanentImpl; import mage.remote.traffic.ZippedObjectImpl; import mage.util.CardUtil; import mage.utils.CompressUtil; +import mage.view.GameView; import org.junit.Assert; import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; @@ -76,9 +76,10 @@ public class SerializationTest extends CardTestPlayerBase { setStopAt(1, PhaseStep.END_TURN); execute(); - Object compressed = CompressUtil.compress(currentGame); + GameView gameView = getGameView(playerA); + Object compressed = CompressUtil.compress(gameView); Assert.assertTrue("Must be zip", compressed instanceof ZippedObjectImpl); - Game uncompressed = (Game) CompressUtil.decompress(compressed); - Assert.assertEquals("Must be same", 1, uncompressed.getBattlefield().getAllActivePermanents().size()); + GameView uncompressed = (GameView) CompressUtil.decompress(compressed); + Assert.assertEquals("Must be same", 1, uncompressed.getPlayers().get(0).getBattlefield().size()); } } diff --git a/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java b/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java index 0edcd4fa92..ce0a8efc0c 100644 --- a/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java +++ b/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java @@ -56,7 +56,7 @@ public class VerifyCardDataTest { private static final Logger logger = Logger.getLogger(VerifyCardDataTest.class); - private static final String FULL_ABILITIES_CHECK_SET_CODE = "MH2"; // check all abilities and output cards with wrong abilities texts; + private static final String FULL_ABILITIES_CHECK_SET_CODE = "AFR"; // check all abilities and output cards with wrong abilities texts; private static final boolean AUTO_FIX_SAMPLE_DECKS = false; // debug only: auto-fix sample decks by test_checkSampleDecks test run private static final boolean ONLY_TEXT = false; // use when checking text locally, suppresses unnecessary checks and output messages @@ -102,14 +102,12 @@ public class VerifyCardDataTest { skipListAddName(SKIP_LIST_TYPE, "UNH", "Old Fogey"); // uses summon word as a joke card skipListAddName(SKIP_LIST_TYPE, "UND", "Old Fogey"); skipListAddName(SKIP_LIST_TYPE, "UST", "capital offense"); // uses "instant" instead "Instant" as a joke card - skipListAddName(SKIP_LIST_TYPE, "AFR", "Iron Golem"); // temporary - skipListAddName(SKIP_LIST_TYPE, "AFR", "Silver Raven"); // temporary // subtype + subtypesToIgnore.add("Bard"); // until errata is implemented and on mtgjson + subtypesToIgnore.add("Ranger"); // until errata is implemented and on mtgjson skipListCreate(SKIP_LIST_SUBTYPE); skipListAddName(SKIP_LIST_SUBTYPE, "UGL", "Miss Demeanor"); // uses multiple types as a joke card: Lady, of, Proper, Etiquette - skipListAddName(SKIP_LIST_SUBTYPE, "AFR", "Iron Golem"); // temporary - skipListAddName(SKIP_LIST_SUBTYPE, "AFR", "Silver Raven"); // temporary // number skipListCreate(SKIP_LIST_NUMBER); diff --git a/Mage/src/main/java/mage/abilities/Ability.java b/Mage/src/main/java/mage/abilities/Ability.java index 610d878513..c4f6cda350 100644 --- a/Mage/src/main/java/mage/abilities/Ability.java +++ b/Mage/src/main/java/mage/abilities/Ability.java @@ -552,8 +552,21 @@ public interface Ability extends Controllable, Serializable { Ability addHint(Hint hint); + /** + * For abilities with static icons + * + * @return + */ List getIcons(); + /** + * For abilities with dynamic icons + * + * @param game can be null for static calls like copies + * @return + */ + List getIcons(Game game); + Ability addIcon(CardIcon cardIcon); Ability addCustomOutcome(Outcome customOutcome); diff --git a/Mage/src/main/java/mage/abilities/AbilityImpl.java b/Mage/src/main/java/mage/abilities/AbilityImpl.java index 1fc78638a4..a354670438 100644 --- a/Mage/src/main/java/mage/abilities/AbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/AbilityImpl.java @@ -1342,7 +1342,12 @@ public abstract class AbilityImpl implements Ability { } @Override - public List getIcons() { + final public List getIcons() { + return getIcons(null); + } + + @Override + public List getIcons(Game game) { return this.icons; } diff --git a/Mage/src/main/java/mage/abilities/abilityword/ConstellationAbility.java b/Mage/src/main/java/mage/abilities/abilityword/ConstellationAbility.java index c12c6a2f8b..45acd06a85 100644 --- a/Mage/src/main/java/mage/abilities/abilityword/ConstellationAbility.java +++ b/Mage/src/main/java/mage/abilities/abilityword/ConstellationAbility.java @@ -22,7 +22,6 @@ public class ConstellationAbility extends TriggeredAbilityImpl { public ConstellationAbility(Effect effect) { this(effect, false); - setAbilityWord(AbilityWord.CONSTELLATION); } public ConstellationAbility(Effect effect, boolean optional) { @@ -32,6 +31,7 @@ public class ConstellationAbility extends TriggeredAbilityImpl { public ConstellationAbility(Effect effect, boolean optional, boolean thisOr) { super(Zone.BATTLEFIELD, effect, optional); this.thisOr = thisOr; + setAbilityWord(AbilityWord.CONSTELLATION); } public ConstellationAbility(final ConstellationAbility ability) { diff --git a/Mage/src/main/java/mage/abilities/abilityword/StriveAbility.java b/Mage/src/main/java/mage/abilities/abilityword/StriveAbility.java index 78b056226c..0f63428318 100644 --- a/Mage/src/main/java/mage/abilities/abilityword/StriveAbility.java +++ b/Mage/src/main/java/mage/abilities/abilityword/StriveAbility.java @@ -37,7 +37,8 @@ public class StriveAbility extends SimpleStaticAbility { @Override public String getRule() { - return new StringBuilder("this spell costs ").append(striveCost).append(" more to cast for each target beyond the first.").toString(); + return abilityWord.formatWord() + "This spell costs " + + striveCost + " more to cast for each target beyond the first."; } } diff --git a/Mage/src/main/java/mage/abilities/common/BeginningOfEndStepTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/BeginningOfEndStepTriggeredAbility.java index 61266799aa..fa0b18c43d 100644 --- a/Mage/src/main/java/mage/abilities/common/BeginningOfEndStepTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/BeginningOfEndStepTriggeredAbility.java @@ -118,24 +118,22 @@ public class BeginningOfEndStepTriggeredAbility extends TriggeredAbilityImpl { } private String generateConditionString() { - if (interveningIfClauseCondition != null) { - if (interveningIfClauseCondition.toString().startsWith("if")) { - - //Fixes punctuation on multiple sentence if-then construction - // see -- Colfenor's Urn - if (interveningIfClauseCondition.toString().endsWith(".")) { - return interveningIfClauseCondition.toString() + " "; - } - - return interveningIfClauseCondition.toString() + ", "; - } else { - return "if {this} is " + interveningIfClauseCondition.toString() + ", "; + if (interveningIfClauseCondition == null) { + switch (getZone()) { + case GRAVEYARD: + return "if {this} is in your graveyard, "; } + return ""; } - switch (getZone()) { - case GRAVEYARD: - return "if {this} is in your graveyard, "; + String clauseText = interveningIfClauseCondition.toString(); + if (clauseText.startsWith("if")) { + //Fixes punctuation on multiple sentence if-then construction + // see -- Colfenor's Urn + if (clauseText.endsWith(".")) { + return clauseText + " "; + } + return clauseText + ", "; } - return ""; + return "if " + clauseText + ", "; } } diff --git a/Mage/src/main/java/mage/abilities/common/CastOnlyIfConditionIsTrueEffect.java b/Mage/src/main/java/mage/abilities/common/CastOnlyIfConditionIsTrueEffect.java index 3c32c37aac..1b280f1493 100644 --- a/Mage/src/main/java/mage/abilities/common/CastOnlyIfConditionIsTrueEffect.java +++ b/Mage/src/main/java/mage/abilities/common/CastOnlyIfConditionIsTrueEffect.java @@ -10,7 +10,6 @@ import mage.game.Game; import mage.game.events.GameEvent; /** - * * @author LevelX2 */ public class CastOnlyIfConditionIsTrueEffect extends ContinuousRuleModifyingEffectImpl { @@ -52,7 +51,7 @@ public class CastOnlyIfConditionIsTrueEffect extends ContinuousRuleModifyingEffe private String setText() { StringBuilder sb = new StringBuilder("cast this spell only "); if (condition != null) { - sb.append(' ').append(condition.toString()); + sb.append(condition); } return sb.toString(); } diff --git a/Mage/src/main/java/mage/abilities/common/DealtDamageToSourceTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/DealtDamageToSourceTriggeredAbility.java index 7ccc832d36..97fb969e58 100644 --- a/Mage/src/main/java/mage/abilities/common/DealtDamageToSourceTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/DealtDamageToSourceTriggeredAbility.java @@ -1,12 +1,15 @@ package mage.abilities.common; +import java.util.UUID; + import mage.abilities.TriggeredAbilityImpl; import mage.abilities.effects.Effect; import mage.constants.AbilityWord; import mage.constants.Zone; import mage.game.Game; import mage.game.events.DamagedEvent; +import mage.game.events.DamagedPermanentBatchEvent; import mage.game.events.GameEvent; /** @@ -15,7 +18,6 @@ import mage.game.events.GameEvent; public class DealtDamageToSourceTriggeredAbility extends TriggeredAbilityImpl { private final boolean useValue; - private boolean usedForCombatDamageStep; public DealtDamageToSourceTriggeredAbility(Effect effect, boolean optional) { this(effect, optional, false); @@ -28,7 +30,6 @@ public class DealtDamageToSourceTriggeredAbility extends TriggeredAbilityImpl { public DealtDamageToSourceTriggeredAbility(Effect effect, boolean optional, boolean enrage, boolean useValue) { super(Zone.BATTLEFIELD, effect, optional); this.useValue = useValue; - this.usedForCombatDamageStep = false; if (enrage) { this.setAbilityWord(AbilityWord.ENRAGE); } @@ -37,7 +38,6 @@ public class DealtDamageToSourceTriggeredAbility extends TriggeredAbilityImpl { public DealtDamageToSourceTriggeredAbility(final DealtDamageToSourceTriggeredAbility ability) { super(ability); this.useValue = ability.useValue; - this.usedForCombatDamageStep = ability.usedForCombatDamageStep; } @Override @@ -47,30 +47,34 @@ public class DealtDamageToSourceTriggeredAbility extends TriggeredAbilityImpl { @Override public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.DAMAGED_PERMANENT || event.getType() == GameEvent.EventType.COMBAT_DAMAGE_STEP_POST; + return event.getType() == GameEvent.EventType.DAMAGED_PERMANENT_BATCH; } @Override public boolean checkTrigger(GameEvent event, Game game) { - if (event.getType() == GameEvent.EventType.DAMAGED_PERMANENT && event.getTargetId().equals(getSourceId())) { - if (useValue) { -// TODO: this ability should only trigger once for multiple creatures dealing combat damage. -// If the damaged creature uses the amount (e.g. Boros Reckoner), this will still trigger separately instead of all at once - getEffects().setValue("damage", event.getAmount()); - return true; - } else { - if (((DamagedEvent) event).isCombatDamage()) { - if (!usedForCombatDamageStep) { - usedForCombatDamageStep = true; - return true; - } - } else { - return true; - } + + if (event == null || game == null || this.getSourceId() == null) { + return false; + } + + int damage = 0; + DamagedPermanentBatchEvent dEvent = (DamagedPermanentBatchEvent) event; + for (DamagedEvent damagedEvent : dEvent.getEvents()) { + UUID targetID = damagedEvent.getTargetId(); + if (targetID == null) { + continue; + } + + if (targetID == this.getSourceId()) { + damage += damagedEvent.getAmount(); } } - if (event.getType() == GameEvent.EventType.COMBAT_DAMAGE_STEP_POST) { - usedForCombatDamageStep = false; + + if (damage > 0) { + if (this.useValue) { + this.getEffects().setValue("damage", damage); + } + return true; } return false; } diff --git a/Mage/src/main/java/mage/abilities/common/PutCardIntoGraveFromAnywhereAllTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/PutCardIntoGraveFromAnywhereAllTriggeredAbility.java index 7cbafb00ad..c6f29a7927 100644 --- a/Mage/src/main/java/mage/abilities/common/PutCardIntoGraveFromAnywhereAllTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/PutCardIntoGraveFromAnywhereAllTriggeredAbility.java @@ -14,7 +14,6 @@ import mage.game.events.ZoneChangeEvent; import mage.target.targetpointer.FixedTarget; /** - * * @author LevelX2 */ public class PutCardIntoGraveFromAnywhereAllTriggeredAbility extends TriggeredAbilityImpl { @@ -42,7 +41,8 @@ public class PutCardIntoGraveFromAnywhereAllTriggeredAbility extends TriggeredAb this.filter.add(targetController.getOwnerPredicate()); StringBuilder sb = new StringBuilder("Whenever "); sb.append(filter.getMessage()); - sb.append(" is put into "); + sb.append(filter.getMessage().startsWith("one or more") ? " are" : "is"); + sb.append(" put into "); switch (targetController) { case OPPONENT: sb.append("an opponent's"); @@ -103,6 +103,6 @@ public class PutCardIntoGraveFromAnywhereAllTriggeredAbility extends TriggeredAb @Override public String getTriggerPhrase() { - return ruleText ; + return ruleText; } } diff --git a/Mage/src/main/java/mage/abilities/condition/common/EquippedSourceCondition.java b/Mage/src/main/java/mage/abilities/condition/common/EquippedSourceCondition.java index cdcd889f8f..18e0adf0ba 100644 --- a/Mage/src/main/java/mage/abilities/condition/common/EquippedSourceCondition.java +++ b/Mage/src/main/java/mage/abilities/condition/common/EquippedSourceCondition.java @@ -34,7 +34,7 @@ public enum EquippedSourceCondition implements Condition { @Override public String toString() { - return "equipped"; + return "{this} is equipped"; } } diff --git a/Mage/src/main/java/mage/abilities/dynamicvalue/common/ManacostVariableValue.java b/Mage/src/main/java/mage/abilities/dynamicvalue/common/ManacostVariableValue.java index 9f77849d4d..293b9ddcec 100644 --- a/Mage/src/main/java/mage/abilities/dynamicvalue/common/ManacostVariableValue.java +++ b/Mage/src/main/java/mage/abilities/dynamicvalue/common/ManacostVariableValue.java @@ -7,7 +7,9 @@ import mage.game.Game; import mage.watchers.common.ManaSpentToCastWatcher; public enum ManacostVariableValue implements DynamicValue { - REGULAR, ETB; + + REGULAR, // if you need X on cast/activate (in stack) + ETB; // if you need X after ETB (in battlefield) @Override public int calculate(Game game, Ability sourceAbility, Effect effect) { @@ -15,7 +17,10 @@ public enum ManacostVariableValue implements DynamicValue { return sourceAbility.getManaCostsToPay().getX(); } ManaSpentToCastWatcher watcher = game.getState().getWatcher(ManaSpentToCastWatcher.class); - return watcher != null ? watcher.getAndResetLastXValue(sourceAbility.getSourceId()) : sourceAbility.getManaCostsToPay().getX(); + if (watcher != null) { + return watcher.getAndResetLastXValue(sourceAbility); + } + return 0; } @Override diff --git a/Mage/src/main/java/mage/abilities/effects/common/CreateDelayedTriggeredAbilityEffect.java b/Mage/src/main/java/mage/abilities/effects/common/CreateDelayedTriggeredAbilityEffect.java index 179a59f736..9fe41bcaba 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/CreateDelayedTriggeredAbilityEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/CreateDelayedTriggeredAbilityEffect.java @@ -77,4 +77,9 @@ public class CreateDelayedTriggeredAbilityEffect extends OneShotEffect { } } + @Override + public void setValue(String key, Object value) { + ability.getEffects().setValue(key, value); + super.setValue(key, value); + } } diff --git a/Mage/src/main/java/mage/abilities/effects/common/CreateTokenCopyTargetEffect.java b/Mage/src/main/java/mage/abilities/effects/common/CreateTokenCopyTargetEffect.java index 69e9a2f118..f090a43024 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/CreateTokenCopyTargetEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/CreateTokenCopyTargetEffect.java @@ -49,6 +49,7 @@ public class CreateTokenCopyTargetEffect extends OneShotEffect { private boolean isntLegendary = false; private int startingLoyalty = -1; private final List additionalAbilities = new ArrayList(); + private Permanent savedPermanent = null; public CreateTokenCopyTargetEffect(boolean useLKI) { this(); @@ -133,7 +134,9 @@ public class CreateTokenCopyTargetEffect extends OneShotEffect { targetId = getTargetPointer().getFirst(game, source); } Permanent permanent; - if (useLKI) { + if (savedPermanent != null) { + permanent = savedPermanent; + } else if (useLKI) { permanent = getTargetPointer().getFirstTargetPermanentOrLKI(game, source); } else { permanent = game.getPermanentOrLKIBattlefield(targetId); @@ -319,4 +322,8 @@ public class CreateTokenCopyTargetEffect extends OneShotEffect { public void addAdditionalAbilities(Ability... abilities) { Arrays.stream(abilities).forEach(this.additionalAbilities::add); } + + public void setSavedPermanent(Permanent savedPermanent) { + this.savedPermanent = savedPermanent; + } } diff --git a/Mage/src/main/java/mage/abilities/effects/common/ExileTopXMayPlayUntilEndOfTurnEffect.java b/Mage/src/main/java/mage/abilities/effects/common/ExileTopXMayPlayUntilEndOfTurnEffect.java index 02696d2bc4..2f7bf62e9f 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/ExileTopXMayPlayUntilEndOfTurnEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/ExileTopXMayPlayUntilEndOfTurnEffect.java @@ -3,7 +3,6 @@ package mage.abilities.effects.common; import mage.MageObject; import mage.abilities.Ability; import mage.abilities.Mode; -import mage.abilities.effects.ContinuousEffect; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.asthought.PlayFromNotOwnHandZoneTargetEffect; import mage.cards.Card; @@ -21,21 +20,28 @@ public class ExileTopXMayPlayUntilEndOfTurnEffect extends OneShotEffect { private final int amount; private final boolean showHint; + private final Duration duration; public ExileTopXMayPlayUntilEndOfTurnEffect(int amount) { this(amount, false); } public ExileTopXMayPlayUntilEndOfTurnEffect(int amount, boolean showHint) { + this(amount, showHint, Duration.EndOfTurn); + } + + public ExileTopXMayPlayUntilEndOfTurnEffect(int amount, boolean showHint, Duration duration) { super(Outcome.Benefit); this.amount = amount; this.showHint = showHint; + this.duration = duration; } private ExileTopXMayPlayUntilEndOfTurnEffect(final ExileTopXMayPlayUntilEndOfTurnEffect effect) { super(effect); this.amount = effect.amount; this.showHint = effect.showHint; + this.duration = effect.duration; } @Override @@ -47,21 +53,21 @@ public class ExileTopXMayPlayUntilEndOfTurnEffect extends OneShotEffect { public boolean apply(Game game, Ability source) { Player controller = game.getPlayer(source.getControllerId()); MageObject sourceObject = game.getObject(source.getSourceId()); - if (controller != null && sourceObject != null) { - Set cards = controller.getLibrary().getTopCards(game, amount); - if (!cards.isEmpty()) { - controller.moveCardsToExile(cards, source, game, true, source.getSourceId(), sourceObject.getIdName()); - // remove cards that could not be moved to exile - cards.removeIf(card -> !Zone.EXILED.equals(game.getState().getZone(card.getId()))); - if (!cards.isEmpty()) { - ContinuousEffect effect = new PlayFromNotOwnHandZoneTargetEffect(Zone.EXILED, Duration.EndOfTurn); - effect.setTargetPointer(new FixedTargets(cards, game)); - game.addEffect(effect, source); - } - } + if (controller == null || sourceObject == null) { + return false; + } + Set cards = controller.getLibrary().getTopCards(game, amount); + if (cards.isEmpty()) { return true; } - return false; + controller.moveCardsToExile(cards, source, game, true, source.getSourceId(), sourceObject.getIdName()); + // remove cards that could not be moved to exile + cards.removeIf(card -> !Zone.EXILED.equals(game.getState().getZone(card.getId()))); + if (!cards.isEmpty()) { + game.addEffect(new PlayFromNotOwnHandZoneTargetEffect(Zone.EXILED, duration) + .setTargetPointer(new FixedTargets(cards, game)), source); + } + return true; } @Override @@ -71,11 +77,15 @@ public class ExileTopXMayPlayUntilEndOfTurnEffect extends OneShotEffect { } StringBuilder sb = new StringBuilder(); if (amount == 1) { - sb.append("exile the top card of your library. You may play that card this turn"); + sb.append("exile the top card of your library. "); + sb.append(CardUtil.getTextWithFirstCharUpperCase(duration.toString())); + sb.append(", you may play that card"); } else { sb.append("exile the top "); sb.append(CardUtil.numberToText(amount)); - sb.append(" cards of your library. Until end of turn, you may play cards exiled this way"); + sb.append(" cards of your library. "); + sb.append(CardUtil.getTextWithFirstCharUpperCase(duration.toString())); + sb.append(", you may play cards exiled this way"); } if (showHint) { sb.append(". (You still pay its costs. You can play a land this way only if you have an available land play remaining.)"); diff --git a/Mage/src/main/java/mage/abilities/effects/common/combat/CantBeBlockedByCreaturesAllEffect.java b/Mage/src/main/java/mage/abilities/effects/common/combat/CantBeBlockedByCreaturesAllEffect.java index bb0bbc9ffb..51a6dc9530 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/combat/CantBeBlockedByCreaturesAllEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/combat/CantBeBlockedByCreaturesAllEffect.java @@ -10,7 +10,6 @@ import mage.game.permanent.Permanent; /** * @author LevelX2 */ - public class CantBeBlockedByCreaturesAllEffect extends RestrictionEffect { private final FilterCreaturePermanent filterBlockedBy; @@ -20,8 +19,9 @@ public class CantBeBlockedByCreaturesAllEffect extends RestrictionEffect { super(duration); this.filterCreatures = filterCreatures; this.filterBlockedBy = filterBlockedBy; - staticText = new StringBuilder(filterCreatures.getMessage()).append(" can't be blocked ") - .append(filterBlockedBy.getMessage().startsWith("except by") ? "" : "by ").append(filterBlockedBy.getMessage()).toString(); + staticText = filterCreatures.getMessage() + " can't be blocked " + + (filterBlockedBy.getMessage().startsWith("except by") ? "" : "by ") + + filterBlockedBy.getMessage(); } public CantBeBlockedByCreaturesAllEffect(final CantBeBlockedByCreaturesAllEffect effect) { diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/BecomesCreatureTargetEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/BecomesCreatureTargetEffect.java index 8a1eb66b16..0e200bb74c 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/BecomesCreatureTargetEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/BecomesCreatureTargetEffect.java @@ -182,10 +182,13 @@ public class BecomesCreatureTargetEffect extends ContinuousEffectImpl { sb.append(token.getDescription()); sb.append(' ').append(duration.toString()); if (addStillALandText) { + if (!sb.toString().endsWith("\" ")) { + sb.append(". "); + } if (target.getMaxNumberOfTargets() > 1) { - sb.append(". They're still lands"); + sb.append("They're still lands"); } else { - sb.append(". It's still a land"); + sb.append("It's still a land"); } } return sb.toString().replace(" .", "."); diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityWithAttachmentEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityWithAttachmentEffect.java index 6018a71566..8cfe2280bb 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityWithAttachmentEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityWithAttachmentEffect.java @@ -19,7 +19,7 @@ import mage.target.Target; import mage.target.Targets; import mage.target.targetpointer.FixedTarget; -import java.util.Arrays; +import java.util.Collections; /** * @author TheElk801 @@ -28,8 +28,8 @@ public class GainAbilityWithAttachmentEffect extends ContinuousEffectImpl { private final Effects effects = new Effects(); private final Targets targets = new Targets(); - private final Costs costs = new CostsImpl(); - private final UseAttachedCost useAttachedCost; + private final Costs costs = new CostsImpl<>(); + protected final UseAttachedCost useAttachedCost; public GainAbilityWithAttachmentEffect(String rule, Effect effect, Target target, UseAttachedCost attachedCost, Cost... costs) { this(rule, new Effects(effect), new Targets(target), attachedCost, costs); @@ -40,12 +40,12 @@ public class GainAbilityWithAttachmentEffect extends ContinuousEffectImpl { this.staticText = rule; this.effects.addAll(effects); this.targets.addAll(targets); - this.costs.addAll(Arrays.asList(costs)); + Collections.addAll(this.costs, costs); this.useAttachedCost = attachedCost; - this.generateGainAbilityDependencies(makeAbility(this.effects, this.targets, this.costs), null); + this.generateGainAbilityDependencies(makeAbility(null, null), null); } - public GainAbilityWithAttachmentEffect(final GainAbilityWithAttachmentEffect effect) { + protected GainAbilityWithAttachmentEffect(final GainAbilityWithAttachmentEffect effect) { super(effect); this.effects.addAll(effect.effects); this.targets.addAll(effect.targets); @@ -87,14 +87,13 @@ public class GainAbilityWithAttachmentEffect extends ContinuousEffectImpl { if (permanent == null) { return true; } - Ability ability = makeAbility(this.effects, this.targets, this.costs); + Ability ability = makeAbility(game, source); ability.getEffects().setValue("attachedPermanent", game.getPermanent(source.getSourceId())); - ability.addCost(useAttachedCost.copy().setMageObjectReference(source, game)); permanent.addAbility(ability, source.getSourceId(), game); return true; } - private static Ability makeAbility(Effects effects, Targets targets, Cost... costs) { + protected Ability makeAbility(Game game, Ability source) { Ability ability = new SimpleActivatedAbility(null, null); for (Effect effect : effects) { if (effect == null) { @@ -108,12 +107,15 @@ public class GainAbilityWithAttachmentEffect extends ContinuousEffectImpl { } ability.addTarget(target); } - for (Cost cost : costs) { + for (Cost cost : this.costs) { if (cost == null) { continue; } ability.addCost(cost.copy()); } + if (source != null && game != null) { + ability.addCost(useAttachedCost.copy().setMageObjectReference(source, game)); + } return ability; } } diff --git a/Mage/src/main/java/mage/abilities/effects/common/cost/SpellCostReductionForEachSourceEffect.java b/Mage/src/main/java/mage/abilities/effects/common/cost/SpellCostReductionForEachSourceEffect.java index 5b998cbd3b..5025777ff6 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/cost/SpellCostReductionForEachSourceEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/cost/SpellCostReductionForEachSourceEffect.java @@ -62,7 +62,7 @@ public class SpellCostReductionForEachSourceEffect extends CostModificationEffec if (reduceManaCosts != null) { // color reduce ManaCosts needReduceMana = new ManaCostsImpl<>(); - for (int i = 0; i <= needReduceAmount; i++) { + for (int i = 0; i < needReduceAmount; i++) { needReduceMana.add(reduceManaCosts.copy()); } CardUtil.adjustCost((SpellAbility) abilityToModify, needReduceMana, false); diff --git a/Mage/src/main/java/mage/abilities/effects/common/search/SearchLibraryGraveyardPutInHandEffect.java b/Mage/src/main/java/mage/abilities/effects/common/search/SearchLibraryGraveyardPutInHandEffect.java index 21cd0a5468..c44cb611f3 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/search/SearchLibraryGraveyardPutInHandEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/search/SearchLibraryGraveyardPutInHandEffect.java @@ -35,7 +35,7 @@ public class SearchLibraryGraveyardPutInHandEffect extends OneShotEffect { this.filter = filter; this.forceToSearchBoth = forceToSearchBoth; staticText = (youMay ? "you may " : "") + "search your library and" + (forceToSearchBoth ? "" : "/or") + " graveyard for a card named " + filter.getMessage() - + ", reveal it, and put it into your hand. " + (forceToSearchBoth ? "Then shuffle" : "If you search your library this way, shuffle"); + + ", reveal it, and put it into your hand. " + (forceToSearchBoth ? "Then shuffle" : "If you search your library this way, shuffle it"); } public SearchLibraryGraveyardPutInHandEffect(final SearchLibraryGraveyardPutInHandEffect effect) { diff --git a/Mage/src/main/java/mage/abilities/icon/CardIconCategory.java b/Mage/src/main/java/mage/abilities/icon/CardIconCategory.java index fa1f4ad2f5..0f35c5f0d0 100644 --- a/Mage/src/main/java/mage/abilities/icon/CardIconCategory.java +++ b/Mage/src/main/java/mage/abilities/icon/CardIconCategory.java @@ -1,6 +1,8 @@ package mage.abilities.icon; /** + * For GUI: different icons category can go to different position/panels on the card + * * @author JayDi85 */ public enum CardIconCategory { diff --git a/Mage/src/main/java/mage/abilities/icon/CardIconImpl.java b/Mage/src/main/java/mage/abilities/icon/CardIconImpl.java index 89318cb15a..4ea64fc84a 100644 --- a/Mage/src/main/java/mage/abilities/icon/CardIconImpl.java +++ b/Mage/src/main/java/mage/abilities/icon/CardIconImpl.java @@ -1,9 +1,11 @@ package mage.abilities.icon; +import java.io.Serializable; + /** * @author JayDi85 */ -public class CardIconImpl implements CardIcon { +public class CardIconImpl implements CardIcon, Serializable { private final CardIconType cardIconType; private final String text; diff --git a/Mage/src/main/java/mage/abilities/icon/CardIconType.java b/Mage/src/main/java/mage/abilities/icon/CardIconType.java index c4e8c1111a..f965772632 100644 --- a/Mage/src/main/java/mage/abilities/icon/CardIconType.java +++ b/Mage/src/main/java/mage/abilities/icon/CardIconType.java @@ -29,6 +29,10 @@ public enum CardIconType { ABILITY_INFECT("prepared/flask.svg", CardIconCategory.ABILITY, 100), ABILITY_INDESTRUCTIBLE("prepared/ankh.svg", CardIconCategory.ABILITY, 100), ABILITY_VIGILANCE("prepared/eye.svg", CardIconCategory.ABILITY, 100), + ABILITY_CLASS_LEVEL("prepared/hexagon-fill.svg", CardIconCategory.ABILITY, 100), + // + OTHER_FACEDOWN("prepared/reply-fill.svg", CardIconCategory.ABILITY, 100), + OTHER_COST_X("prepared/square-fill.svg", CardIconCategory.ABILITY, 100), // SYSTEM_COMBINED("prepared/square-fill.svg", CardIconCategory.SYSTEM, 1000), // inner usage, must use last order SYSTEM_DEBUG("prepared/link.svg", CardIconCategory.SYSTEM, 1000); // used for test render dialog diff --git a/Mage/src/main/java/mage/abilities/icon/other/FaceDownCardIcon.java b/Mage/src/main/java/mage/abilities/icon/other/FaceDownCardIcon.java new file mode 100644 index 0000000000..07f59fe863 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/icon/other/FaceDownCardIcon.java @@ -0,0 +1,31 @@ +package mage.abilities.icon.other; + +import mage.abilities.icon.CardIcon; +import mage.abilities.icon.CardIconType; + +/** + * @author JayDi85 + */ +public enum FaceDownCardIcon implements CardIcon { + instance; + + @Override + public CardIconType getIconType() { + return CardIconType.OTHER_FACEDOWN; + } + + @Override + public String getText() { + return ""; + } + + @Override + public String getHint() { + return "Card is face down"; + } + + @Override + public CardIcon copy() { + return instance; + } +} diff --git a/Mage/src/main/java/mage/abilities/icon/other/VariableCostCardIcon.java b/Mage/src/main/java/mage/abilities/icon/other/VariableCostCardIcon.java new file mode 100644 index 0000000000..40b4f23e01 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/icon/other/VariableCostCardIcon.java @@ -0,0 +1,16 @@ +package mage.abilities.icon.other; + +import mage.abilities.icon.CardIconImpl; +import mage.abilities.icon.CardIconType; + +/** + * Showing x cost value + * + * @author JayDi85 + */ +public class VariableCostCardIcon extends CardIconImpl { + + public VariableCostCardIcon(int costX) { + super(CardIconType.OTHER_COST_X, "Announced X = " + costX, "x=" + costX); + } +} diff --git a/Mage/src/main/java/mage/abilities/keyword/ClassLevelAbility.java b/Mage/src/main/java/mage/abilities/keyword/ClassLevelAbility.java index 3061e6d8fd..b317a9be51 100644 --- a/Mage/src/main/java/mage/abilities/keyword/ClassLevelAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/ClassLevelAbility.java @@ -61,6 +61,7 @@ class SetClassLevelEffect extends OneShotEffect { SetClassLevelEffect(int level) { super(Outcome.Benefit); this.level = level; + staticText = "level up to " + level; } private SetClassLevelEffect(final SetClassLevelEffect effect) { @@ -76,9 +77,17 @@ class SetClassLevelEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { Permanent permanent = source.getSourcePermanentIfItStillExists(game); - if (permanent == null || !permanent.setClassLevel(level)) { + if (permanent == null) { return false; } + + int oldLevel = permanent.getClassLevel(); + if (!permanent.setClassLevel(level)) { + return false; + } + + game.informPlayers(permanent.getLogName() + " levelled up from " + oldLevel + " to " + permanent.getClassLevel()); + game.fireEvent(GameEvent.getEvent( GameEvent.EventType.GAINS_CLASS_LEVEL, source.getSourceId(), source, source.getControllerId(), level diff --git a/Mage/src/main/java/mage/abilities/keyword/ClassReminderAbility.java b/Mage/src/main/java/mage/abilities/keyword/ClassReminderAbility.java index b9964bf564..b5fd4b85b9 100644 --- a/Mage/src/main/java/mage/abilities/keyword/ClassReminderAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/ClassReminderAbility.java @@ -2,7 +2,15 @@ package mage.abilities.keyword; import mage.abilities.StaticAbility; import mage.abilities.hint.common.ClassLevelHint; +import mage.abilities.icon.CardIcon; +import mage.abilities.icon.CardIconImpl; +import mage.abilities.icon.CardIconType; import mage.constants.Zone; +import mage.game.Game; +import mage.game.permanent.Permanent; + +import java.util.ArrayList; +import java.util.List; /** * @author TheElk801 @@ -27,4 +35,27 @@ public class ClassReminderAbility extends StaticAbility { public String getRule() { return "(Gain the next level as a sorcery to add its ability.)"; } + + @Override + public List getIcons(Game game) { + if (game == null) { + return this.icons; + } + + // dynamic GUI icon with current level + List res = new ArrayList<>(); + Permanent permanent = this.getSourcePermanentOrLKI(game); + if (permanent == null) { + return res; + } + + CardIcon levelIcon = new CardIconImpl( + CardIconType.ABILITY_CLASS_LEVEL, + "Current class level: " + permanent.getClassLevel(), + String.valueOf(permanent.getClassLevel()) + ); + res.add(levelIcon); + + return res; + } } diff --git a/Mage/src/main/java/mage/abilities/keyword/EquipAbility.java b/Mage/src/main/java/mage/abilities/keyword/EquipAbility.java index 7b0dc758f8..1bc91d6cfc 100644 --- a/Mage/src/main/java/mage/abilities/keyword/EquipAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/EquipAbility.java @@ -28,7 +28,7 @@ public class EquipAbility extends ActivatedAbilityImpl { public EquipAbility(Outcome outcome, Cost cost, Target target) { super(Zone.BATTLEFIELD, new EquipEffect(outcome), cost); this.addTarget(target); - this.timing = TimingRule.SORCERY; + this.timing = TimingRule.SORCERY; } public EquipAbility(final EquipAbility ability) { @@ -50,19 +50,23 @@ public class EquipAbility extends ActivatedAbilityImpl { String targetText = getTargets().get(0) != null ? getTargets().get(0).getFilter().getMessage() : "creature"; String reminderText = " (" + manaCosts.getText() + ": Attach to target " + targetText + ". Equip only as a sorcery. This card enters the battlefield unattached and stays on the battlefield if the creature leaves.)"; - StringBuilder sb = new StringBuilder("Equip "); + StringBuilder sb = new StringBuilder("Equip"); if (!targetText.equals("creature you control")) { - sb.append(targetText); + sb.append(' ').append(targetText); + } + String costText = costs.getText(); + if (costText != null && !costText.isEmpty()) { + sb.append("—").append(costText).append('.'); + } else { sb.append(' '); } - sb.append(costs.getText()); sb.append(manaCosts.getText()); if (costReduceText != null && !costReduceText.isEmpty()) { - sb.append(' '); + sb.append(". "); sb.append(costReduceText); } if (maxActivationsPerTurn == 1) { - sb.append(" Activate only once each turn."); + sb.append(". Activate only once each turn."); } sb.append(reminderText); return sb.toString(); diff --git a/Mage/src/main/java/mage/abilities/keyword/IntimidateAbility.java b/Mage/src/main/java/mage/abilities/keyword/IntimidateAbility.java index 7b487f3a5d..0b4d247a84 100644 --- a/Mage/src/main/java/mage/abilities/keyword/IntimidateAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/IntimidateAbility.java @@ -31,7 +31,7 @@ public class IntimidateAbility extends EvasionAbility implements MageSingleton { @Override public String getRule() { - return "Intimidate"; + return "intimidate"; } @Override diff --git a/Mage/src/main/java/mage/abilities/keyword/MonstrosityAbility.java b/Mage/src/main/java/mage/abilities/keyword/MonstrosityAbility.java index 43d4f9b539..8dfd4d7d0c 100644 --- a/Mage/src/main/java/mage/abilities/keyword/MonstrosityAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/MonstrosityAbility.java @@ -1,12 +1,9 @@ - - package mage.abilities.keyword; import mage.abilities.Ability; import mage.abilities.ActivatedAbilityImpl; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.OneShotEffect; -import mage.abilities.effects.common.counter.AddCountersSourceEffect; import mage.abilities.hint.common.MonstrousHint; import mage.constants.Outcome; import mage.constants.Zone; @@ -19,41 +16,40 @@ import mage.util.CardUtil; /** * Monstrosity - * + *

* 701.28. Monstrosity - * + *

* 701.28a “Monstrosity N” means “If this permanent isn't monstrous, put N +1/+1 counters on it - * and it becomes monstrous.” Monstrous is a condition of that permanent that can be - * referred to by other abilities. - * + * and it becomes monstrous.” Monstrous is a condition of that permanent that can be + * referred to by other abilities. + *

* 701.28b If a permanent's ability instructs a player to “monstrosity X,” other abilities of - * that permanent may also refer to X. The value of X in those abilities is equal to - * the value of X as that permanent became monstrous. - * + * that permanent may also refer to X. The value of X in those abilities is equal to + * the value of X as that permanent became monstrous. + *

* * Once a creature becomes monstrous, it can't become monstrous again. If the creature - * is already monstrous when the monstrosity ability resolves, nothing happens. - * + * is already monstrous when the monstrosity ability resolves, nothing happens. + *

* * Monstrous isn't an ability that a creature has. It's just something true about that - * creature. If the creature stops being a creature or loses its abilities, it will - * continue to be monstrous. - * + * creature. If the creature stops being a creature or loses its abilities, it will + * continue to be monstrous. + *

* * An ability that triggers when a creature becomes monstrous won't trigger if that creature - * isn't on the battlefield when its monstrosity ability resolves. + * isn't on the battlefield when its monstrosity ability resolves. * * @author LevelX2 */ public class MonstrosityAbility extends ActivatedAbilityImpl { - private int monstrosityValue; + private final int monstrosityValue; /** - * * @param manaString * @param monstrosityValue use Integer.MAX_VALUE for monstrosity X. */ public MonstrosityAbility(String manaString, int monstrosityValue) { - super(Zone.BATTLEFIELD, new BecomeMonstrousSourceEffect(monstrosityValue),new ManaCostsImpl(manaString)); + super(Zone.BATTLEFIELD, new BecomeMonstrousSourceEffect(monstrosityValue), new ManaCostsImpl<>(manaString)); this.monstrosityValue = monstrosityValue; this.addHint(MonstrousHint.instance); @@ -72,7 +68,6 @@ public class MonstrosityAbility extends ActivatedAbilityImpl { public int getMonstrosityValue() { return monstrosityValue; } - } @@ -94,28 +89,33 @@ class BecomeMonstrousSourceEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { - Permanent permanent = game.getPermanent(source.getSourceId()); - if (permanent != null && !permanent.isMonstrous() && source instanceof MonstrosityAbility) { - int monstrosityValue = ((MonstrosityAbility) source).getMonstrosityValue(); - // handle monstrosity = X - if (monstrosityValue == Integer.MAX_VALUE) { - monstrosityValue = source.getManaCostsToPay().getX(); - } - new AddCountersSourceEffect(CounterType.P1P1.createInstance(monstrosityValue)).apply(game, source); - permanent.setMonstrous(true); - game.fireEvent(GameEvent.getEvent(GameEvent.EventType.BECOMES_MONSTROUS, source.getSourceId(), source, source.getControllerId(), monstrosityValue)); - return true; + Permanent permanent = source.getSourcePermanentIfItStillExists(game); + if (permanent == null || permanent.isMonstrous()) { + return false; } - return false; + int monstrosityValue = ((MonstrosityAbility) source).getMonstrosityValue(); + // handle monstrosity = X + if (monstrosityValue == Integer.MAX_VALUE) { + monstrosityValue = source.getManaCostsToPay().getX(); + } + permanent.addCounters( + CounterType.P1P1.createInstance(monstrosityValue), + source.getControllerId(), source, game + ); + permanent.setMonstrous(true); + game.fireEvent(GameEvent.getEvent( + GameEvent.EventType.BECOMES_MONSTROUS, source.getSourceId(), + source, source.getControllerId(), monstrosityValue + )); + return true; } private String setText(int monstrosityValue) { StringBuilder sb = new StringBuilder("Monstrosity "); - sb.append(monstrosityValue == Integer.MAX_VALUE ? "X":monstrosityValue) + sb.append(monstrosityValue == Integer.MAX_VALUE ? "X" : monstrosityValue) .append(". (If this creature isn't monstrous, put ") - .append(monstrosityValue == Integer.MAX_VALUE ? "X":CardUtil.numberToText(monstrosityValue)) + .append(monstrosityValue == Integer.MAX_VALUE ? "X" : CardUtil.numberToText(monstrosityValue)) .append(" +1/+1 counters on it and it becomes monstrous.)").toString(); return sb.toString(); } - } diff --git a/Mage/src/main/java/mage/constants/SubType.java b/Mage/src/main/java/mage/constants/SubType.java index 961900b695..70374d6f86 100644 --- a/Mage/src/main/java/mage/constants/SubType.java +++ b/Mage/src/main/java/mage/constants/SubType.java @@ -467,7 +467,8 @@ public enum SubType { XENAGOS("Xenagos", SubTypeSet.PlaneswalkerType), YANGGU("Yanggu", SubTypeSet.PlaneswalkerType), YANLING("Yanling", SubTypeSet.PlaneswalkerType), - YODA("Yoda", SubTypeSet.PlaneswalkerType, true); // Star Wars + YODA("Yoda", SubTypeSet.PlaneswalkerType, true), // Star Wars, + ZARIEL("Zariel", SubTypeSet.PlaneswalkerType); public static class SubTypePredicate implements Predicate { diff --git a/Mage/src/main/java/mage/game/Game.java b/Mage/src/main/java/mage/game/Game.java index d4d83971fc..1e17fcaf0c 100644 --- a/Mage/src/main/java/mage/game/Game.java +++ b/Mage/src/main/java/mage/game/Game.java @@ -542,15 +542,71 @@ public interface Game extends MageItem, Serializable { * @param commanderCardType commander or signature spell * @return */ - default Set getCommanderCardsFromAnyZones(Player player, CommanderCardType commanderCardType) { - // from command zone - Set res = getCommanderCardsFromCommandZone(player, commanderCardType); - - // from battlefield - this.getCommandersIds(player, commanderCardType, true).stream() - .map(this::getPermanent) + default Set getCommanderCardsFromAnyZones(Player player, CommanderCardType commanderCardType, Zone... searchZones) { + Set needZones = Arrays.stream(searchZones).collect(Collectors.toSet()); + if (needZones.isEmpty()) { + throw new IllegalArgumentException("Empty zones list in searching commanders"); + } + Set needCommandersIds = this.getCommandersIds(player, commanderCardType, true); + Set needCommandersCards = needCommandersIds.stream() + .map(this::getCard) .filter(Objects::nonNull) - .forEach(res::add); + .collect(Collectors.toSet()); + Set res = new HashSet<>(); + + // hand + if (needZones.contains(Zone.ALL) || needZones.contains(Zone.HAND)) { + needCommandersCards.stream() + .filter(card -> Zone.HAND.equals(this.getState().getZone(card.getId()))) + .forEach(res::add); + } + + // graveyard + if (needZones.contains(Zone.ALL) || needZones.contains(Zone.GRAVEYARD)) { + needCommandersCards.stream() + .filter(card -> Zone.GRAVEYARD.equals(this.getState().getZone(card.getId()))) + .forEach(res::add); + } + + // library + if (needZones.contains(Zone.ALL) || needZones.contains(Zone.LIBRARY)) { + needCommandersCards.stream() + .filter(card -> Zone.LIBRARY.equals(this.getState().getZone(card.getId()))) + .forEach(res::add); + } + + // battlefield (need permanent card) + if (needZones.contains(Zone.ALL) || needZones.contains(Zone.BATTLEFIELD)) { + needCommandersIds.stream() + .map(this::getPermanent) + .filter(Objects::nonNull) + .forEach(res::add); + } + + // stack + if (needZones.contains(Zone.ALL) || needZones.contains(Zone.STACK)) { + needCommandersCards.stream() + .filter(card -> Zone.STACK.equals(this.getState().getZone(card.getId()))) + .forEach(res::add); + } + + // exiled + if (needZones.contains(Zone.ALL) || needZones.contains(Zone.EXILED)) { + needCommandersCards.stream() + .filter(card -> Zone.EXILED.equals(this.getState().getZone(card.getId()))) + .forEach(res::add); + } + + // command + if (needZones.contains(Zone.ALL) || needZones.contains(Zone.COMMAND)) { + res.addAll(getCommanderCardsFromCommandZone(player, commanderCardType)); + } + + // outside must be ignored (example: second side of MDFC commander after cast) + if (needZones.contains(Zone.OUTSIDE)) { + throw new IllegalArgumentException("Outside zone doesn't supported in searching commanders"); + } + return res; } diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index 57031170bd..e6b60b5d74 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -3185,8 +3185,8 @@ public abstract class GameImpl implements Game, Serializable { @Override public void cheat(UUID ownerId, List library, List hand, List battlefield, List graveyard, List command) { // fake test ability for triggers and events - Ability fakeSourceAbility = new SimpleStaticAbility(Zone.OUTSIDE, new InfoEffect("adding testing cards")); - fakeSourceAbility.setControllerId(ownerId); + Ability fakeSourceAbilityTemplate = new SimpleStaticAbility(Zone.OUTSIDE, new InfoEffect("adding testing cards")); + fakeSourceAbilityTemplate.setControllerId(ownerId); Player player = getPlayer(ownerId); if (player != null) { @@ -3221,6 +3221,8 @@ public abstract class GameImpl implements Game, Serializable { } for (PermanentCard permanentCard : battlefield) { + Ability fakeSourceAbility = fakeSourceAbilityTemplate.copy(); + fakeSourceAbility.setSourceId(permanentCard.getId()); CardUtil.putCardOntoBattlefieldWithEffects(fakeSourceAbility, this, permanentCard, player); } diff --git a/Mage/src/main/java/mage/game/combat/Combat.java b/Mage/src/main/java/mage/game/combat/Combat.java index 871387e5f8..d29ef524ce 100644 --- a/Mage/src/main/java/mage/game/combat/Combat.java +++ b/Mage/src/main/java/mage/game/combat/Combat.java @@ -178,6 +178,7 @@ public class Combat implements Serializable, Copyable { numberCreaturesDefenderAttackedBy.clear(); creaturesForcedToAttack.clear(); maxAttackers = Integer.MIN_VALUE; + attackersTappedByAttack.clear(); } public String getValue() { diff --git a/Mage/src/main/java/mage/game/command/emblems/ZarielArchdukeOfAvernusEmblem.java b/Mage/src/main/java/mage/game/command/emblems/ZarielArchdukeOfAvernusEmblem.java new file mode 100644 index 0000000000..efceb70f38 --- /dev/null +++ b/Mage/src/main/java/mage/game/command/emblems/ZarielArchdukeOfAvernusEmblem.java @@ -0,0 +1,60 @@ +package mage.game.command.emblems; + +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.effects.common.AdditionalCombatPhaseEffect; +import mage.abilities.effects.common.UntapTargetEffect; +import mage.constants.TurnPhase; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.command.Emblem; +import mage.game.events.GameEvent; +import mage.target.common.TargetControlledCreaturePermanent; + +/** + * @author TheElk801 + */ +public final class ZarielArchdukeOfAvernusEmblem extends Emblem { + + // −6: You get an emblem with "At the end of the first combat phase on your turn, untap target creature you control. After this phase, there is an additional combat phase." + public ZarielArchdukeOfAvernusEmblem() { + this.setName("Emblem Zariel"); + this.setExpansionSetCodeForImage("AFR"); + this.getAbilities().add(new ZarielArchdukeOfAvernusEmblemAbility()); + } +} + +class ZarielArchdukeOfAvernusEmblemAbility extends TriggeredAbilityImpl { + + ZarielArchdukeOfAvernusEmblemAbility() { + super(Zone.COMMAND, new UntapTargetEffect()); + this.addEffect(new AdditionalCombatPhaseEffect()); + this.addTarget(new TargetControlledCreaturePermanent()); + } + + private ZarielArchdukeOfAvernusEmblemAbility(final ZarielArchdukeOfAvernusEmblemAbility ability) { + super(ability); + } + + @Override + public ZarielArchdukeOfAvernusEmblemAbility copy() { + return new ZarielArchdukeOfAvernusEmblemAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.END_COMBAT_STEP_PRE; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + return game.isActivePlayer(getControllerId()) + && game.getTurn().getPhase(TurnPhase.COMBAT).getCount() == 0; + } + + @Override + public String getRule() { + return "At the end of the first combat phase on your turn, untap target creature you control. " + + "After this phase, there is an additional combat phase."; + } + +} \ No newline at end of file diff --git a/Mage/src/main/java/mage/game/permanent/Permanent.java b/Mage/src/main/java/mage/game/permanent/Permanent.java index afb3725c4b..ded11c011b 100644 --- a/Mage/src/main/java/mage/game/permanent/Permanent.java +++ b/Mage/src/main/java/mage/game/permanent/Permanent.java @@ -75,6 +75,12 @@ public interface Permanent extends Card, Controllable { int getClassLevel(); + /** + * Level up to next level. + * + * @param classLevel + * @return false on wrong settings (e.g. level up to multiple levels) + */ boolean setClassLevel(int classLevel); void setCardNumber(String cid); diff --git a/Mage/src/main/java/mage/game/permanent/PermanentCard.java b/Mage/src/main/java/mage/game/permanent/PermanentCard.java index 6ce8e1062b..ffa2cb18c4 100644 --- a/Mage/src/main/java/mage/game/permanent/PermanentCard.java +++ b/Mage/src/main/java/mage/game/permanent/PermanentCard.java @@ -220,4 +220,9 @@ public class PermanentCard extends PermanentImpl { public Card getMainCard() { return card.getMainCard(); } + + @Override + public String toString() { + return card.toString(); + } } diff --git a/Mage/src/main/java/mage/game/permanent/PermanentImpl.java b/Mage/src/main/java/mage/game/permanent/PermanentImpl.java index 355c92e488..b903450cc1 100644 --- a/Mage/src/main/java/mage/game/permanent/PermanentImpl.java +++ b/Mage/src/main/java/mage/game/permanent/PermanentImpl.java @@ -1528,6 +1528,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { @Override public boolean setClassLevel(int classLevel) { + // can level up to next (+1) level only if (this.classLevel == classLevel - 1) { this.classLevel = classLevel; return true; diff --git a/Mage/src/main/java/mage/game/permanent/token/EldraziHorrorToken.java b/Mage/src/main/java/mage/game/permanent/token/EldraziHorrorToken.java index 95f042f15b..4c541c96b3 100644 --- a/Mage/src/main/java/mage/game/permanent/token/EldraziHorrorToken.java +++ b/Mage/src/main/java/mage/game/permanent/token/EldraziHorrorToken.java @@ -21,7 +21,7 @@ public final class EldraziHorrorToken extends TokenImpl { } public EldraziHorrorToken() { - super("Eldrazi Horror", "3/2 colorless Eldrazi Horror creature"); + super("Eldrazi Horror", "3/2 colorless Eldrazi Horror creature token"); cardType.add(CardType.CREATURE); subtype.add(SubType.ELDRAZI); subtype.add(SubType.HORROR); diff --git a/Mage/src/main/java/mage/game/permanent/token/GrovetenderDruidsPlantToken.java b/Mage/src/main/java/mage/game/permanent/token/GrovetenderDruidsPlantToken.java index 424cf239d9..2209491466 100644 --- a/Mage/src/main/java/mage/game/permanent/token/GrovetenderDruidsPlantToken.java +++ b/Mage/src/main/java/mage/game/permanent/token/GrovetenderDruidsPlantToken.java @@ -12,7 +12,7 @@ import mage.MageInt; public final class GrovetenderDruidsPlantToken extends TokenImpl { public GrovetenderDruidsPlantToken() { - super("Plant", "1/1 green Plant creature"); + super("Plant", "1/1 green Plant creature token"); cardType.add(CardType.CREATURE); color.setGreen(true); subtype.add(SubType.PLANT); diff --git a/Mage/src/main/java/mage/game/stack/StackAbility.java b/Mage/src/main/java/mage/game/stack/StackAbility.java index 61a3969b13..3a6585c0bd 100644 --- a/Mage/src/main/java/mage/game/stack/StackAbility.java +++ b/Mage/src/main/java/mage/game/stack/StackAbility.java @@ -692,6 +692,11 @@ public class StackAbility extends StackObjectImpl implements Ability { return this.ability.getIcons(); } + @Override + public List getIcons(Game game) { + return this.ability.getIcons(game); + } + @Override public Ability addIcon(CardIcon cardIcon) { throw new IllegalArgumentException("Stack ability is not supports icon adding"); diff --git a/Mage/src/main/java/mage/util/functions/CopyTokenFunction.java b/Mage/src/main/java/mage/util/functions/CopyTokenFunction.java index 452c0cca07..c335c43a0f 100644 --- a/Mage/src/main/java/mage/util/functions/CopyTokenFunction.java +++ b/Mage/src/main/java/mage/util/functions/CopyTokenFunction.java @@ -81,7 +81,11 @@ public class CopyTokenFunction implements Function { for (Ability ability0 : sourceObj.getAbilities()) { Ability ability = ability0.copy(); - ability.newOriginalId(); // The token is independant from the copy from object so it need a new original Id, otherwise there are problems to check for created continuous effects to check if the source (the Token) has still this ability + + // The token is independant from the copy from object so it need a new original Id, + // otherwise there are problems to check for created continuous effects to check if + // the source (the Token) has still this ability + ability.newOriginalId(); target.addAbility(ability); } diff --git a/Mage/src/main/java/mage/watchers/common/ManaSpentToCastWatcher.java b/Mage/src/main/java/mage/watchers/common/ManaSpentToCastWatcher.java index 67c13a3567..7295a7e462 100644 --- a/Mage/src/main/java/mage/watchers/common/ManaSpentToCastWatcher.java +++ b/Mage/src/main/java/mage/watchers/common/ManaSpentToCastWatcher.java @@ -1,6 +1,7 @@ package mage.watchers.common; import mage.Mana; +import mage.abilities.Ability; import mage.constants.WatcherScope; import mage.constants.Zone; import mage.game.Game; @@ -51,8 +52,14 @@ public class ManaSpentToCastWatcher extends Watcher { return manaMap.getOrDefault(sourceId, null); } - public int getAndResetLastXValue(UUID sourceId) { - return xValueMap.getOrDefault(sourceId, 0); + public int getAndResetLastXValue(Ability source) { + if (xValueMap.containsKey(source.getSourceId())) { + // cast normal way + return xValueMap.get(source.getSourceId()); + } else { + // put to battlefield without cast (example: copied spell must keep announced X) + return source.getManaCostsToPay().getX(); + } } @Override diff --git a/Mage/src/test/java/mage/cards/decks/importer/samples/testdeck.json b/Mage/src/test/java/mage/cards/decks/importer/samples/testdeck.json index 70e99295b8..34c54f991c 100644 --- a/Mage/src/test/java/mage/cards/decks/importer/samples/testdeck.json +++ b/Mage/src/test/java/mage/cards/decks/importer/samples/testdeck.json @@ -1008,7 +1008,7 @@ "manaCost": "{1}{U}", "name": "Chart a Course", "number": "48", - "originalText": "Draw two cards. Then discard a card unless you attacked with a creature this turn.", + "originalText": "Draw two cards. Then discard a card unless you attacked this turn.", "originalType": "Sorcery", "printings": [ "JMP",