diff --git a/Mage.Sets/src/mage/cards/a/AnrakyrTheTraveller.java b/Mage.Sets/src/mage/cards/a/AnrakyrTheTraveller.java new file mode 100644 index 00000000000..e32d21e1eaa --- /dev/null +++ b/Mage.Sets/src/mage/cards/a/AnrakyrTheTraveller.java @@ -0,0 +1,143 @@ +package mage.cards.a; + +import mage.ApprovingObject; +import mage.MageInt; +import mage.MageObject; +import mage.abilities.Ability; +import mage.abilities.ActivatedAbility; +import mage.abilities.SpellAbility; +import mage.abilities.common.AttacksTriggeredAbility; +import mage.abilities.costs.Cost; +import mage.abilities.costs.Costs; +import mage.abilities.costs.CostsImpl; +import mage.abilities.costs.common.PayLifeCost; +import mage.abilities.effects.OneShotEffect; +import mage.cards.*; +import mage.constants.*; +import mage.filter.FilterCard; +import mage.filter.common.FilterArtifactCard; +import mage.game.Game; +import mage.players.Player; +import mage.target.TargetCard; +import mage.util.CardUtil; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * @author gravitybone + * @author Aquid + */ +public final class AnrakyrTheTraveller extends CardImpl { + + public AnrakyrTheTraveller(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[] { CardType.ARTIFACT, CardType.CREATURE }, "{4}{B}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.NECRON); + this.power = new MageInt(4); + this.toughness = new MageInt(4); + + // Lord of the Pyrrhian Legions — Whenever Anrakyr the Traveller attacks, you may cast an artifact spell from your hand or graveyard by paying life equal to its mana value rather than paying its mana cost. + Ability ability = new AttacksTriggeredAbility(new AnrakyrTheTravellerEffect(), true); + + this.addAbility(ability.withFlavorWord("Lord of the Pyrrhian Legions")); + } + + private AnrakyrTheTraveller(final AnrakyrTheTraveller card) { + super(card); + } + + @Override + public AnrakyrTheTraveller copy() { + return new AnrakyrTheTraveller(this); + } +} + +class AnrakyrTheTravellerEffect extends OneShotEffect { + + private static final FilterCard filter = new FilterArtifactCard("an artifact spell"); + + AnrakyrTheTravellerEffect() { + super(Outcome.AIDontUseIt); + this.staticText = "you may cast " + filter.getMessage() + " from your hand or graveyard by paying life equal to its mana value rather than paying its mana cost."; + } + + private AnrakyrTheTravellerEffect(final AnrakyrTheTravellerEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + + Set cards = player.getHand().getCards(filter, game); + cards.addAll(player.getGraveyard().getCards(filter, game)); + + Map> cardMap = new HashMap<>(); + for (Card card : cards) { + List castableComponents = CardUtil.getCastableComponents(card, filter, source, player, game, null, false); + if (!castableComponents.isEmpty()) { + cardMap.put(card.getId(), castableComponents); + } + } + Card cardToCast; + if (cardMap.isEmpty()) { + return false; + } + Cards castableCards = new CardsImpl(cardMap.keySet()); + TargetCard target = new TargetCard(0, 1, Zone.ALL, filter); + target.withNotTarget(true); + player.choose(Outcome.Benefit, castableCards, target, source, game); + cardToCast = castableCards.get(target.getFirstTarget(), game); + + if (cardToCast == null) { + return false; + } + + List partsToCast = cardMap.get(cardToCast.getId()); + String partsInfo = partsToCast + .stream() + .map(MageObject::getIdName) + .collect(Collectors.joining(" or ")); + if (cardToCast == null + || partsToCast.size() < 1 + || !player.chooseUse( + Outcome.PlayForFree, "Cast spell by paying life equal to its mana value rather than paying its mana cost (" + partsInfo + ")?", source, game + )) { + return false; + } + partsToCast.forEach(card -> game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), Boolean.TRUE)); + + // pay life + // copied from BolassCitadelPlayTheTopCardEffect.applies + PayLifeCost lifeCost = new PayLifeCost(cardToCast.getSpellAbility().getManaCosts().manaValue()); + Costs newCosts = new CostsImpl<>(); + newCosts.add(lifeCost); + newCosts.addAll(cardToCast.getSpellAbility().getCosts()); + player.setCastSourceIdWithAlternateMana(cardToCast.getId(), null, newCosts); + + ActivatedAbility chosenAbility; + chosenAbility = player.chooseAbilityForCast(cardToCast, game, true); + boolean result = false; + if (chosenAbility instanceof SpellAbility) { + result = player.cast( + (SpellAbility) chosenAbility, + game, true, new ApprovingObject(source, game) + ); + } + partsToCast.forEach(card -> game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), null)); + if (player.isComputer() && !result) { + cards.remove(cardToCast); + } + return result; + } + + @Override + public AnrakyrTheTravellerEffect copy() { + return new AnrakyrTheTravellerEffect(this); + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/sets/Warhammer40000.java b/Mage.Sets/src/mage/sets/Warhammer40000.java index 9b2a7b76c53..f8a686a54f5 100644 --- a/Mage.Sets/src/mage/sets/Warhammer40000.java +++ b/Mage.Sets/src/mage/sets/Warhammer40000.java @@ -26,6 +26,7 @@ public final class Warhammer40000 extends ExpansionSet { cards.add(new SetCardInfo("Acolyte Hybrid", 70, Rarity.UNCOMMON, mage.cards.a.AcolyteHybrid.class)); cards.add(new SetCardInfo("Aetherize", 191, Rarity.UNCOMMON, mage.cards.a.Aetherize.class)); cards.add(new SetCardInfo("And They Shall Know No Fear", 9, Rarity.UNCOMMON, mage.cards.a.AndTheyShallKnowNoFear.class)); + cards.add(new SetCardInfo("Anrakyr the Traveller", 28, Rarity.RARE, mage.cards.a.AnrakyrTheTraveller.class)); cards.add(new SetCardInfo("Arcane Sanctum", 264, Rarity.UNCOMMON, mage.cards.a.ArcaneSanctum.class)); cards.add(new SetCardInfo("Arcane Signet", 227, Rarity.COMMON, mage.cards.a.ArcaneSignet.class)); cards.add(new SetCardInfo("Arco-Flagellant", 29, Rarity.RARE, mage.cards.a.ArcoFlagellant.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/_40k/AnrakyrTheTravellerTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/_40k/AnrakyrTheTravellerTest.java new file mode 100644 index 00000000000..545ce9450b3 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/_40k/AnrakyrTheTravellerTest.java @@ -0,0 +1,211 @@ +package org.mage.test.cards.single._40k; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +public class AnrakyrTheTravellerTest extends CardTestPlayerBase { + + // Lord of the Pyrrhian Legions — Whenever Anrakyr the Traveller attacks, you may cast an artifact spell from your + // hand or graveyard by paying life equal to its mana value rather than paying its mana cost. + private static final String ANRAKYR = "Anrakyr the Traveller"; + + private static final String CRYPTEK = "Cryptek"; + + // Multikicker {2} + // Everflowing Chalice enters the battlefield with a charge counter on it for each time it was kicked. + private static final String EVERFLOWING_CHALICE = "Everflowing Chalice"; + + // As an additional cost to cast this spell, discard a card. + private static final String LESSER_MASTICORE = "Lesser Masticore"; + + @Test + public void testCastArtifactCardForLifeFromHand_ShouldCastForLife() { + // prepare + addCard(Zone.BATTLEFIELD, playerA, ANRAKYR); + addCard(Zone.HAND, playerA, CRYPTEK); + + // execute + attack(1, playerA, ANRAKYR); + setChoice(playerA, true); // confirm use of Anrakyr's ability + setChoice(playerA, CRYPTEK); // select Cryptek to cast + setChoice(playerA, true); // confirm casting spell by paying life instead of mana + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + // assert + assertLife(playerA, 20 - 4); + assertPermanentCount(playerA, CRYPTEK, 1); + } + + @Test + public void testNotEnoughLife_ShouldNotCast() { + // prepare + addCard(Zone.BATTLEFIELD, playerA, ANRAKYR); + addCard(Zone.HAND, playerA, CRYPTEK); + setLife(playerA, 3); + + // execute + attack(1, playerA, ANRAKYR); + setChoice(playerA, true); // confirm use of Anrakyr's ability + setChoice(playerA, CRYPTEK); // select Cryptek to cast + setChoice(playerA, true); // confirm casting spell by paying life instead of mana + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + // assert + assertLife(playerA, 3); + assertPermanentCount(playerA, CRYPTEK, 0); + } + + @Test + public void testCastArtifactCardForLifeFromGraveyard_ShouldCastForLife() { + // prepare + addCard(Zone.BATTLEFIELD, playerA, ANRAKYR); + addCard(Zone.GRAVEYARD, playerA, CRYPTEK); + + // execute + attack(1, playerA, ANRAKYR); + setChoice(playerA, true); // confirm use of Anrakyr's ability + setChoice(playerA, CRYPTEK); // select Cryptek to cast + setChoice(playerA, true); // confirm casting spell by paying life instead of mana + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + // assert + assertLife(playerA, 20 - 4); + assertPermanentCount(playerA, CRYPTEK, 1); + assertGraveyardCount(playerA, CRYPTEK, 0); + } + + @Test + public void testLesserMasticore_ShouldCast() { + // prepare + addCard(Zone.BATTLEFIELD, playerA, ANRAKYR); + addCard(Zone.HAND, playerA, LESSER_MASTICORE); + addCard(Zone.HAND, playerA, CRYPTEK); + + // execute + attack(1, playerA, ANRAKYR); + setChoice(playerA, true); // confirm use of Anrakyr's ability + setChoice(playerA, LESSER_MASTICORE); // select Lesser Masticore to cast + setChoice(playerA, true); // confirm casting spell by paying life instead of mana + setChoice(playerA, CRYPTEK); // select Cryptek to discard to pay for Lesser Masticore + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + // assert + assertLife(playerA, 20 - 2); + assertPermanentCount(playerA, LESSER_MASTICORE, 1); + } + + @Test + public void testLesserMasticoreNoCardInHand_ShouldNotCast() { + // prepare + addCard(Zone.BATTLEFIELD, playerA, ANRAKYR); + addCard(Zone.HAND, playerA, LESSER_MASTICORE); + + // execute + attack(1, playerA, ANRAKYR); + setChoice(playerA, true); // confirm use of Anrakyr's ability + setChoice(playerA, LESSER_MASTICORE); // select Lesser Masticore to cast + setChoice(playerA, true); // confirm casting spell by paying life instead of mana + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + // assert + assertLife(playerA, 20); + assertPermanentCount(playerA, LESSER_MASTICORE, 0); + } + + @Test + public void testEverflowingChaliceMultikicker1_ShouldCast() { + // prepare + final String swamp = "Swamp"; + addCard(Zone.BATTLEFIELD, playerA, ANRAKYR); + addCard(Zone.BATTLEFIELD, playerA, swamp); + addCard(Zone.BATTLEFIELD, playerA, swamp); + addCard(Zone.HAND, playerA, EVERFLOWING_CHALICE); + + // execute + attack(1, playerA, ANRAKYR); + setChoice(playerA, true); // confirm use of Anrakyr's ability + setChoice(playerA, EVERFLOWING_CHALICE); // select Everflowing Chalice to cast + setChoice(playerA, true); // confirm casting spell by paying life instead of mana + + // multikicker 1 + setChoice(playerA, true); + setChoice(playerA, false); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + // assert + assertLife(playerA, 20); + assertPermanentCount(playerA, EVERFLOWING_CHALICE, 1); + assertCounterCount(playerA, EVERFLOWING_CHALICE, CounterType.CHARGE, 1); + } + + @Test + public void testEverflowingChaliceMultikicker0_ShouldCast() { + // prepare + addCard(Zone.BATTLEFIELD, playerA, ANRAKYR); + addCard(Zone.HAND, playerA, EVERFLOWING_CHALICE); + + // execute + attack(1, playerA, ANRAKYR); + setChoice(playerA, true); // confirm use of Anrakyr's ability + setChoice(playerA, EVERFLOWING_CHALICE); // select Everflowing Chalice to cast + setChoice(playerA, true); // confirm casting spell by paying life instead of mana + + // multikicker 0 + setChoice(playerA, false); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + // assert + assertLife(playerA, 20); + assertPermanentCount(playerA, EVERFLOWING_CHALICE, 1); + assertCounterCount(playerA, EVERFLOWING_CHALICE, CounterType.CHARGE, 0); + } + + @Test + public void testEverflowingChaliceMultikicker1NoMana_ShouldNotCast() { + // prepare + addCard(Zone.BATTLEFIELD, playerA, ANRAKYR); + addCard(Zone.HAND, playerA, EVERFLOWING_CHALICE); + + // execute + attack(1, playerA, ANRAKYR); + setChoice(playerA, true); // confirm use of Anrakyr's ability + setChoice(playerA, EVERFLOWING_CHALICE); // select Everflowing Chalice to cast + setChoice(playerA, true); // confirm casting spell by paying life instead of mana + + // multikicker 1 + setChoice(playerA, true); + setChoice(playerA, false); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + // assert + assertLife(playerA, 20); + assertPermanentCount(playerA, EVERFLOWING_CHALICE, 0); + } +} \ No newline at end of file diff --git a/Mage/src/main/java/mage/util/CardUtil.java b/Mage/src/main/java/mage/util/CardUtil.java index 4a594234fc4..d5901f7feb4 100644 --- a/Mage/src/main/java/mage/util/CardUtil.java +++ b/Mage/src/main/java/mage/util/CardUtil.java @@ -1292,7 +1292,22 @@ public final class CardUtil { void addCard(Card card, Ability source, Game game); } - private static List getCastableComponents(Card cardToCast, FilterCard filter, Ability source, Player player, Game game, SpellCastTracker spellCastTracker, boolean playLand) { + /** + * Retrieves a list of all castable components from a given card based on certain conditions. + * + * Castable components are parts of a card that can be played or cast, + * such as the adventure and main side of adventure spells or both sides of a fuse card. + * + * @param cardToCast + * @param filter A filter to determine if a card is eligible for casting. + * @param source The ability or source responsible for the casting. + * @param player + * @param game + * @param spellCastTracker An optional tracker for spell casting. + * @param playLand A boolean flag indicating whether playing lands is allowed. + * @return A list of castable components from the input card, considering the provided conditions. + */ + public static List getCastableComponents(Card cardToCast, FilterCard filter, Ability source, Player player, Game game, SpellCastTracker spellCastTracker, boolean playLand) { UUID playerId = player.getId(); List cards = new ArrayList<>(); if (cardToCast instanceof CardWithHalves) {