forked from External/mage
[40K] Implement Anrakyr the Traveller (#11160)
--------- Co-authored-by: gravitybone <7361357+gravitybone@users.noreply.github.com> Co-authored-by: Aquid <Aquid117@users.noreply.github.com>
This commit is contained in:
parent
5e3c0764a5
commit
5b845ef7e4
4 changed files with 371 additions and 1 deletions
143
Mage.Sets/src/mage/cards/a/AnrakyrTheTraveller.java
Normal file
143
Mage.Sets/src/mage/cards/a/AnrakyrTheTraveller.java
Normal file
|
|
@ -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<Card> cards = player.getHand().getCards(filter, game);
|
||||||
|
cards.addAll(player.getGraveyard().getCards(filter, game));
|
||||||
|
|
||||||
|
Map<UUID, List<Card>> cardMap = new HashMap<>();
|
||||||
|
for (Card card : cards) {
|
||||||
|
List<Card> 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<Card> 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<Cost> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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("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("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("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 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("Arcane Signet", 227, Rarity.COMMON, mage.cards.a.ArcaneSignet.class));
|
||||||
cards.add(new SetCardInfo("Arco-Flagellant", 29, Rarity.RARE, mage.cards.a.ArcoFlagellant.class));
|
cards.add(new SetCardInfo("Arco-Flagellant", 29, Rarity.RARE, mage.cards.a.ArcoFlagellant.class));
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1292,7 +1292,22 @@ public final class CardUtil {
|
||||||
void addCard(Card card, Ability source, Game game);
|
void addCard(Card card, Ability source, Game game);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<Card> 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<Card> getCastableComponents(Card cardToCast, FilterCard filter, Ability source, Player player, Game game, SpellCastTracker spellCastTracker, boolean playLand) {
|
||||||
UUID playerId = player.getId();
|
UUID playerId = player.getId();
|
||||||
List<Card> cards = new ArrayList<>();
|
List<Card> cards = new ArrayList<>();
|
||||||
if (cardToCast instanceof CardWithHalves) {
|
if (cardToCast instanceof CardWithHalves) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue