Ready for review: Implement Craft mechanic (#11352)

* [LCI] Implement Spring-Loaded Sawblades / Bladewheel Chariot

* [LCI] Implement Sunbird Standard / Sunbird Effigy

* card filter needs to have an owner predicate

* [LCI] Implement Throne of the Grim Captain / The Grim Captain

* make default constructor for craft with artifact

* dedupe some code

* refactor constructors for simplicity

* add currently failing test
This commit is contained in:
Evan Kranzler 2023-10-27 22:32:11 -04:00 committed by GitHub
parent 0f7db0c69d
commit bc4aa6931f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 884 additions and 8 deletions

View file

@ -0,0 +1,60 @@
package mage.cards.b;
import mage.MageInt;
import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.costs.common.TapTargetCost;
import mage.abilities.effects.common.continuous.AddCardTypeSourceEffect;
import mage.abilities.keyword.CrewAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Duration;
import mage.constants.SubType;
import mage.filter.common.FilterControlledArtifactPermanent;
import mage.filter.common.FilterControlledPermanent;
import mage.filter.predicate.mageobject.AnotherPredicate;
import mage.filter.predicate.permanent.TappedPredicate;
import mage.target.common.TargetControlledPermanent;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class BladewheelChariot extends CardImpl {
private static final FilterControlledPermanent filter
= new FilterControlledArtifactPermanent("other untapped artifacts you control");
static {
filter.add(AnotherPredicate.instance);
filter.add(TappedPredicate.UNTAPPED);
}
public BladewheelChariot(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "");
this.subtype.add(SubType.VEHICLE);
this.power = new MageInt(5);
this.toughness = new MageInt(5);
this.nightCard = true;
this.color.setWhite(true);
// Tap two other untapped artifacts you control: Bladewheel Chariot becomes an artifact creature until end of turn.
this.addAbility(new SimpleActivatedAbility(new AddCardTypeSourceEffect(
Duration.EndOfTurn, CardType.ARTIFACT, CardType.CREATURE
), new TapTargetCost(new TargetControlledPermanent(2, filter))));
// Crew 1
this.addAbility(new CrewAbility(1));
}
private BladewheelChariot(final BladewheelChariot card) {
super(card);
}
@Override
public BladewheelChariot copy() {
return new BladewheelChariot(this);
}
}

View file

@ -28,7 +28,7 @@ public final class FelidarGuardian extends CardImpl {
// When Felidar Guardian enters the battlefield, you may exile another target permanent you control, then return that card to the battlefield under its owner's control.
Ability ability = new EntersBattlefieldTriggeredAbility(new ExileThenReturnTargetEffect(false, true), true);
ability.addTarget(new TargetControlledPermanent(StaticFilters.FILTER_CONTROLLED_ANOTHER_PERMANENT));
ability.addTarget(new TargetControlledPermanent(StaticFilters.FILTER_CONTROLLED_ANOTHER_TARGET_PERMANENT));
this.addAbility(ability);
}

View file

@ -38,7 +38,7 @@ public final class KelpieGuide extends CardImpl {
// {T}: Untap another target permanent you control.
Ability ability = new SimpleActivatedAbility(new UntapTargetEffect(), new TapSourceCost());
ability.addTarget(new TargetPermanent(StaticFilters.FILTER_CONTROLLED_ANOTHER_PERMANENT));
ability.addTarget(new TargetPermanent(StaticFilters.FILTER_CONTROLLED_ANOTHER_TARGET_PERMANENT));
this.addAbility(ability);
// {T}: Tap target permanent. Activate only if you control eight or more lands.

View file

@ -27,7 +27,7 @@ public final class OathOfTeferi extends CardImpl {
// When Oath of Teferi enters the battlefield, exile another target permanent you control. Return it to the battlefield under its owner's control at the beginning of the next end step.
Ability ability = new EntersBattlefieldTriggeredAbility(new ExileReturnBattlefieldNextEndStepTargetEffect().withTextThatCard(false));
ability.addTarget(new TargetPermanent(StaticFilters.FILTER_CONTROLLED_ANOTHER_PERMANENT));
ability.addTarget(new TargetPermanent(StaticFilters.FILTER_CONTROLLED_ANOTHER_TARGET_PERMANENT));
this.addAbility(ability);
// You may activate the loyalty abilities of planeswalkers you control twice each turn rather than only once.

View file

@ -70,7 +70,7 @@ class SkySwallowerEffect extends OneShotEffect {
return false;
}
return new GainControlAllEffect(Duration.Custom,
StaticFilters.FILTER_CONTROLLED_ANOTHER_PERMANENT,
StaticFilters.FILTER_CONTROLLED_ANOTHER_TARGET_PERMANENT,
opponent.getId()
).apply(game, source);
}

View file

@ -0,0 +1,54 @@
package mage.cards.s;
import mage.abilities.Ability;
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.effects.common.DamageTargetEffect;
import mage.abilities.keyword.CraftAbility;
import mage.abilities.keyword.FlashAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.filter.FilterPermanent;
import mage.filter.common.FilterOpponentsCreaturePermanent;
import mage.filter.predicate.permanent.TappedPredicate;
import mage.target.TargetPermanent;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class SpringLoadedSawblades extends CardImpl {
private static final FilterPermanent filter
= new FilterOpponentsCreaturePermanent("tapped creature an opponent controls");
static {
filter.add(TappedPredicate.TAPPED);
}
public SpringLoadedSawblades(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{1}{W}");
this.secondSideCardClazz = mage.cards.b.BladewheelChariot.class;
// Flash
this.addAbility(FlashAbility.getInstance());
// When Spring-Loaded Sawblades enters the battlefield, it deals 5 damage to target tapped creature an opponent controls.
Ability ability = new EntersBattlefieldTriggeredAbility(new DamageTargetEffect(5, "it"));
ability.addTarget(new TargetPermanent(filter));
this.addAbility(ability);
// Craft with artifact {3}{W}
this.addAbility(new CraftAbility("{3}{W}"));
}
private SpringLoadedSawblades(final SpringLoadedSawblades card) {
super(card);
}
@Override
public SpringLoadedSawblades copy() {
return new SpringLoadedSawblades(this);
}
}

View file

@ -0,0 +1,192 @@
package mage.cards.s;
import mage.MageInt;
import mage.Mana;
import mage.ObjectColor;
import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.costs.common.TapSourceCost;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.effects.Effect;
import mage.abilities.effects.common.continuous.SetBasePowerToughnessSourceEffect;
import mage.abilities.effects.mana.ManaEffect;
import mage.abilities.hint.Hint;
import mage.abilities.keyword.FlyingAbility;
import mage.abilities.keyword.HasteAbility;
import mage.abilities.keyword.VigilanceAbility;
import mage.abilities.mana.SimpleManaAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.Zone;
import mage.game.ExileZone;
import mage.game.Game;
import mage.util.CardUtil;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* @author TheElk801
*/
public final class SunbirdEffigy extends CardImpl {
public SunbirdEffigy(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT, CardType.CREATURE}, "");
this.subtype.add(SubType.BIRD);
this.subtype.add(SubType.CONSTRUCT);
this.power = new MageInt(0);
this.toughness = new MageInt(0);
this.nightCard = true;
// Flying
this.addAbility(FlyingAbility.getInstance());
// Vigilance
this.addAbility(VigilanceAbility.getInstance());
// Haste
this.addAbility(HasteAbility.getInstance());
// Sunbird Effigy's power and toughness are each equal to the number of colors among the exiled cards used to craft it.
this.addAbility(new SimpleStaticAbility(
Zone.ALL, new SetBasePowerToughnessSourceEffect(SunbirdEffigyValue.instance)
).addHint(SunbirdEffigyHint.instance));
// {T}: For each color among the exiled cards used to craft Sunbird Effigy, add one mana of that color.
this.addAbility(new SimpleManaAbility(Zone.BATTLEFIELD, new SunbirdEffigyEffect(), new TapSourceCost()));
}
private SunbirdEffigy(final SunbirdEffigy card) {
super(card);
}
@Override
public SunbirdEffigy copy() {
return new SunbirdEffigy(this);
}
}
enum SunbirdEffigyValue implements DynamicValue {
instance;
@Override
public int calculate(Game game, Ability sourceAbility, Effect effect) {
return Optional
.ofNullable(getColor(game, sourceAbility))
.filter(Objects::nonNull)
.map(ObjectColor::getColorCount)
.orElse(0);
}
@Override
public SunbirdEffigyValue copy() {
return this;
}
@Override
public String getMessage() {
return "colors among the exiled cards used to craft it";
}
@Override
public String toString() {
return "1";
}
static ObjectColor getColor(Game game, Ability source) {
ExileZone exileZone = game
.getExile()
.getExileZone(CardUtil.getExileZoneId(
game,
source.getSourceId(),
game.getState().getZoneChangeCounter(source.getSourceId()) - 2
));
return exileZone == null ? null : exileZone
.getCards(game)
.stream()
.map(card -> card.getColor(game))
.reduce(new ObjectColor(), (c1, c2) -> c1.union(c2));
}
}
enum SunbirdEffigyHint implements Hint {
instance;
@Override
public String getText(Game game, Ability ability) {
ObjectColor color = SunbirdEffigyValue.getColor(game, ability);
if (color == null) {
return null;
}
if (color.isColorless()) {
return "No colors among exiled cards.";
}
return color
.getColors()
.stream()
.map(ObjectColor::getDescription)
.collect(Collectors.joining(", ", "Colors among exiled cards: ", ""));
}
@Override
public Hint copy() {
return this;
}
}
class SunbirdEffigyEffect extends ManaEffect {
public SunbirdEffigyEffect() {
super();
staticText = "for each color among the exiled cards used to craft {this}, add one mana of that color";
}
private SunbirdEffigyEffect(final SunbirdEffigyEffect effect) {
super(effect);
}
@Override
public SunbirdEffigyEffect copy() {
return new SunbirdEffigyEffect(this);
}
@Override
public Mana produceMana(Game game, Ability source) {
Mana mana = new Mana();
if (game == null) {
return mana;
}
ExileZone exileZone = game
.getExile()
.getExileZone(CardUtil.getExileZoneId(game, source, -2));
if (exileZone == null) {
return mana;
}
ObjectColor color = exileZone
.getCards(game)
.stream()
.map(card -> card.getColor(game))
.reduce(new ObjectColor(), (c1, c2) -> c1.union(c2));
if (color.isWhite()) {
mana.increaseWhite();
}
if (color.isBlue()) {
mana.increaseBlue();
}
if (color.isBlack()) {
mana.increaseBlack();
}
if (color.isRed()) {
mana.increaseRed();
}
if (color.isGreen()) {
mana.increaseGreen();
}
return mana;
}
}

View file

@ -0,0 +1,38 @@
package mage.cards.s;
import mage.abilities.keyword.CraftAbility;
import mage.abilities.mana.AnyColorManaAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class SunbirdStandard extends CardImpl {
public SunbirdStandard(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{3}");
this.secondSideCardClazz = mage.cards.s.SunbirdEffigy.class;
// {T}: Add one mana of any color.
this.addAbility(new AnyColorManaAbility());
// Craft with one or more {5}
this.addAbility(new CraftAbility(
"{5}", "one or more", "other permanents " +
"you control and/or cards in your graveyard", 1, Integer.MAX_VALUE
));
}
private SunbirdStandard(final SunbirdStandard card) {
super(card);
}
@Override
public SunbirdStandard copy() {
return new SunbirdStandard(this);
}
}

View file

@ -0,0 +1,112 @@
package mage.cards.t;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.AttacksTriggeredAbility;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.SacrificeOpponentsEffect;
import mage.abilities.keyword.HexproofAbility;
import mage.abilities.keyword.LifelinkAbility;
import mage.abilities.keyword.MenaceAbility;
import mage.abilities.keyword.TrampleAbility;
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.game.permanent.Permanent;
import mage.players.Player;
import mage.target.TargetCard;
import mage.target.common.TargetCardInExile;
import mage.util.CardUtil;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class TheGrimCaptain extends CardImpl {
public TheGrimCaptain(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "");
this.supertype.add(SuperType.LEGENDARY);
this.subtype.add(SubType.SKELETON);
this.subtype.add(SubType.SPIRIT);
this.subtype.add(SubType.PIRATE);
this.power = new MageInt(7);
this.toughness = new MageInt(7);
this.nightCard = true;
this.color.setBlack(true);
// Menace
this.addAbility(new MenaceAbility());
// Trample
this.addAbility(TrampleAbility.getInstance());
// Lifelink
this.addAbility(LifelinkAbility.getInstance());
// Hexproof
this.addAbility(HexproofAbility.getInstance());
// Whenever The Grim Captain attacks, each opponent sacrifices a nonland permanent. Then you may put an exiled creature card used to craft The Grim Captain onto the battlefield under your control tapped and attacking.
Ability ability = new AttacksTriggeredAbility(new SacrificeOpponentsEffect(StaticFilters.FILTER_PERMANENT_NON_LAND));
ability.addEffect(new TheGrimCaptainEffect());
this.addAbility(ability);
}
private TheGrimCaptain(final TheGrimCaptain card) {
super(card);
}
@Override
public TheGrimCaptain copy() {
return new TheGrimCaptain(this);
}
}
class TheGrimCaptainEffect extends OneShotEffect {
TheGrimCaptainEffect() {
super(Outcome.Benefit);
staticText = "Then you may put an exiled creature card used to craft {this} " +
"onto the battlefield under your control tapped and attacking";
}
private TheGrimCaptainEffect(final TheGrimCaptainEffect effect) {
super(effect);
}
@Override
public TheGrimCaptainEffect copy() {
return new TheGrimCaptainEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Player player = game.getPlayer(source.getControllerId());
if (player == null) {
return false;
}
TargetCard target = new TargetCardInExile(
0, 1, StaticFilters.FILTER_CARD_CREATURE,
CardUtil.getExileZoneId(game, source, -2)
);
target.withNotTarget(true);
player.choose(outcome, target, source, game);
Card card = game.getCard(target.getFirstTarget());
if (card == null) {
return false;
}
player.moveCards(card, Zone.BATTLEFIELD, source, game, true, false, false, null);
Permanent permanent = game.getPermanent(card.getId());
if (permanent == null) {
return false;
}
game.getCombat().addAttackingCreature(card.getId(), game);
return true;
}
}

View file

@ -0,0 +1,111 @@
package mage.cards.t;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.costs.common.TapSourceCost;
import mage.abilities.dynamicvalue.common.SubTypeAssignment;
import mage.abilities.effects.common.MillCardsControllerEffect;
import mage.abilities.keyword.CraftAbility;
import mage.cards.Card;
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.FilterPermanent;
import mage.filter.common.FilterControlledPermanent;
import mage.filter.predicate.Predicate;
import mage.filter.predicate.Predicates;
import mage.filter.predicate.mageobject.AnotherPredicate;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.target.common.TargetCardInGraveyardBattlefieldOrStack;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* @author TheElk801
*/
public final class ThroneOfTheGrimCaptain extends CardImpl {
public ThroneOfTheGrimCaptain(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{2}");
this.supertype.add(SuperType.LEGENDARY);
this.secondSideCardClazz = mage.cards.t.TheGrimCaptain.class;
// {T}: Mill two cards.
this.addAbility(new SimpleActivatedAbility(new MillCardsControllerEffect(2), new TapSourceCost()));
// Craft with a Dinosaur, a Merfolk, a Pirate, and a Vampire {4}
this.addAbility(new CraftAbility("{4}", "a Dinosaur, a Merfolk, a Pirate, and a Vampire", new ThroneOfTheGrimCaptainTarget()));
}
private ThroneOfTheGrimCaptain(final ThroneOfTheGrimCaptain card) {
super(card);
}
@Override
public ThroneOfTheGrimCaptain copy() {
return new ThroneOfTheGrimCaptain(this);
}
}
class ThroneOfTheGrimCaptainTarget extends TargetCardInGraveyardBattlefieldOrStack {
private static final FilterCard filterCard =
new FilterCard("a Dinosaur, a Merfolk, a Pirate, or Vampire card");
private static final FilterPermanent filterPermanent =
new FilterControlledPermanent("another Dinosaur, a Merfolk, a Pirate, or Vampire you control");
private static final Predicate<MageObject> predicate = Predicates.or(
SubType.PIRATE.getPredicate(),
SubType.VAMPIRE.getPredicate(),
SubType.DINOSAUR.getPredicate(),
SubType.MERFOLK.getPredicate()
);
static {
filterCard.add(predicate);
filterCard.add(TargetController.YOU.getOwnerPredicate());
filterPermanent.add(predicate);
filterPermanent.add(AnotherPredicate.instance);
}
private static final SubTypeAssignment subtypeAssigner = new SubTypeAssignment(
SubType.PIRATE, SubType.VAMPIRE, SubType.DINOSAUR, SubType.MERFOLK
);
ThroneOfTheGrimCaptainTarget() {
super(4, 4, filterCard, filterPermanent);
}
private ThroneOfTheGrimCaptainTarget(final ThroneOfTheGrimCaptainTarget target) {
super(target);
}
@Override
public ThroneOfTheGrimCaptainTarget copy() {
return new ThroneOfTheGrimCaptainTarget(this);
}
@Override
public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) {
if (!super.canTarget(playerId, id, source, game)) {
return false;
}
Set<Card> cards = this.getTargets().stream().map(uuid -> {
Permanent permanent = game.getPermanent(uuid);
if (permanent != null) {
return permanent;
}
return game.getCard(uuid);
}).filter(Objects::nonNull).collect(Collectors.toSet());
return subtypeAssigner.getRoleCount(cards, game) <= 4;
}
}

View file

@ -23,6 +23,7 @@ public final class TheLostCavernsOfIxalan extends ExpansionSet {
cards.add(new SetCardInfo("Abuelo, Ancestral Echo", 219, Rarity.RARE, mage.cards.a.AbueloAncestralEcho.class));
cards.add(new SetCardInfo("Amalia Benavides Aguirre", 221, Rarity.RARE, mage.cards.a.AmaliaBenavidesAguirre.class));
cards.add(new SetCardInfo("Bartolome del Presidio", 224, Rarity.UNCOMMON, mage.cards.b.BartolomeDelPresidio.class));
cards.add(new SetCardInfo("Bladewheel Chariot", 36, Rarity.UNCOMMON, mage.cards.b.BladewheelChariot.class));
cards.add(new SetCardInfo("Breeches, Eager Pillager", 137, Rarity.RARE, mage.cards.b.BreechesEagerPillager.class));
cards.add(new SetCardInfo("Captain Storm, Plunderer of Cosmium", 227, Rarity.UNCOMMON, mage.cards.c.CaptainStormPlundererOfCosmium.class));
cards.add(new SetCardInfo("Careening Mine Cart", 247, Rarity.UNCOMMON, mage.cards.c.CareeningMineCart.class));
@ -73,14 +74,19 @@ public final class TheLostCavernsOfIxalan extends ExpansionSet {
cards.add(new SetCardInfo("Sanguine Evangelist", 34, Rarity.RARE, mage.cards.s.SanguineEvangelist.class));
cards.add(new SetCardInfo("Skullcap Snail", 119, Rarity.COMMON, mage.cards.s.SkullcapSnail.class));
cards.add(new SetCardInfo("Song of Stupefaction", 77, Rarity.COMMON, mage.cards.s.SongOfStupefaction.class));
cards.add(new SetCardInfo("Spring-Loaded Sawblades", 36, Rarity.UNCOMMON, mage.cards.s.SpringLoadedSawblades.class));
cards.add(new SetCardInfo("Spyglass Siren", 78, Rarity.UNCOMMON, mage.cards.s.SpyglassSiren.class));
cards.add(new SetCardInfo("Sunbird Effigy", 262, Rarity.UNCOMMON, mage.cards.s.SunbirdEffigy.class));
cards.add(new SetCardInfo("Sunbird Standard", 262, Rarity.UNCOMMON, mage.cards.s.SunbirdStandard.class));
cards.add(new SetCardInfo("Swamp", 397, Rarity.LAND, mage.cards.basiclands.Swamp.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Temple of Civilization", 26, Rarity.MYTHIC, mage.cards.t.TempleOfCivilization.class));
cards.add(new SetCardInfo("Temple of Power", 158, Rarity.MYTHIC, mage.cards.t.TempleOfPower.class));
cards.add(new SetCardInfo("The Grim Captain", 266, Rarity.RARE, mage.cards.t.TheGrimCaptain.class));
cards.add(new SetCardInfo("The Belligerent", 225, Rarity.RARE, mage.cards.t.TheBelligerent.class));
cards.add(new SetCardInfo("The Skullspore Nexus", 212, Rarity.MYTHIC, mage.cards.t.TheSkullsporeNexus.class));
cards.add(new SetCardInfo("Thrashing Brontodon", 216, Rarity.UNCOMMON, mage.cards.t.ThrashingBrontodon.class));
cards.add(new SetCardInfo("Threefold Thunderhulk", 265, Rarity.RARE, mage.cards.t.ThreefoldThunderhulk.class));
cards.add(new SetCardInfo("Throne of the Grim Captain", 266, Rarity.RARE, mage.cards.t.ThroneOfTheGrimCaptain.class));
cards.add(new SetCardInfo("Treasure Cove", 267, Rarity.RARE, mage.cards.t.TreasureCove.class));
cards.add(new SetCardInfo("Treasure Map", 267, Rarity.RARE, mage.cards.t.TreasureMap.class));
cards.add(new SetCardInfo("Vito, Fanatic of Aclazotz", 243, Rarity.MYTHIC, mage.cards.v.VitoFanaticOfAclazotz.class));

View file

@ -0,0 +1,112 @@
package org.mage.test.cards.abilities.keywords;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Ignore;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* @author TheElk801
*/
public class CraftTest extends CardTestPlayerBase {
private static final String sawblades = "Spring-Loaded Sawblades";
private static final String chariot = "Bladewheel Chariot";
private static final String relic = "Darksteel Relic";
@Test
public void testExilePermanent() {
addCard(Zone.BATTLEFIELD, playerA, "Plains", 4);
addCard(Zone.BATTLEFIELD, playerA, sawblades);
addCard(Zone.BATTLEFIELD, playerA, relic);
addTarget(playerA, relic);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Craft");
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
assertPermanentCount(playerA, sawblades, 0);
assertPermanentCount(playerA, chariot, 1);
assertPermanentCount(playerA, relic, 0);
assertExileCount(playerA, relic, 1);
}
@Test
public void testExileCard() {
addCard(Zone.BATTLEFIELD, playerA, "Plains", 4);
addCard(Zone.BATTLEFIELD, playerA, sawblades);
addCard(Zone.GRAVEYARD, playerA, relic);
addTarget(playerA, relic);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Craft");
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
assertPermanentCount(playerA, sawblades, 0);
assertPermanentCount(playerA, chariot, 1);
assertGraveyardCount(playerA, relic, 0);
assertExileCount(playerA, relic, 1);
}
private static final String standard = "Sunbird Standard";
private static final String effigy = "Sunbird Effigy";
private static final String thoctar = "Woolly Thoctar";
private static final String watchwolf = "Watchwolf";
private static final String yearling = "Cerodon Yearling";
@Test
public void testEffigy() {
addCard(Zone.BATTLEFIELD, playerA, "Forest", 5);
addCard(Zone.BATTLEFIELD, playerA, standard);
addCard(Zone.BATTLEFIELD, playerA, thoctar);
addCard(Zone.HAND, playerA, thoctar);
addTarget(playerA, thoctar);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Craft");
activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: For each");
castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, thoctar);
setStopAt(3, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
assertPermanentCount(playerA, standard, 0);
assertPermanentCount(playerA, thoctar, 1);
assertPowerToughness(playerA, effigy, 3, 3);
}
@Ignore // test fails due to issue with test player target handling
@Test
public void testEffigyMultiple() {
addCard(Zone.BATTLEFIELD, playerA, "Forest", 5);
addCard(Zone.BATTLEFIELD, playerA, standard);
addCard(Zone.BATTLEFIELD, playerA, yearling);
addCard(Zone.GRAVEYARD, playerA, watchwolf);
addCard(Zone.HAND, playerA, thoctar);
addTarget(playerA, yearling);
addTarget(playerA, watchwolf);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Craft");
activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: For each");
castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, thoctar);
setStopAt(3, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
assertPermanentCount(playerA, standard, 0);
assertPermanentCount(playerA, thoctar, 1);
assertPowerToughness(playerA, effigy, 3, 3);
}
}

View file

@ -56,8 +56,12 @@ public abstract class RoleAssignment<T> implements Serializable {
}
public int getRoleCount(Cards cards, Game game) {
return getRoleCount(cards.getCards(game), game);
}
public int getRoleCount(Set<? extends Card> cards, Game game) {
Map<UUID, Set<T>> attributeMap = new HashMap<>();
cards.getCards(game).forEach(card -> attributeMap.put(card.getId(), this.makeSet(card, game)));
cards.forEach(card -> attributeMap.put(card.getId(), this.makeSet(card, game)));
if (attributeMap.size() < 2) {
return attributeMap.size();
}

View file

@ -0,0 +1,176 @@
package mage.abilities.keyword;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.ActivatedAbilityImpl;
import mage.abilities.costs.Cost;
import mage.abilities.costs.CostImpl;
import mage.abilities.costs.common.ExileSourceCost;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.OneShotEffect;
import mage.cards.Card;
import mage.constants.*;
import mage.filter.FilterCard;
import mage.filter.FilterPermanent;
import mage.filter.common.FilterArtifactCard;
import mage.filter.common.FilterControlledPermanent;
import mage.filter.common.FilterOwnedCard;
import mage.filter.predicate.Predicate;
import mage.filter.predicate.mageobject.AnotherPredicate;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.common.TargetCardInGraveyardBattlefieldOrStack;
import mage.util.CardUtil;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* @author TheElk801
*/
public class CraftAbility extends ActivatedAbilityImpl {
private static final FilterCard artifactFilter = new FilterArtifactCard("artifact");
static {
artifactFilter.add(TargetController.YOU.getOwnerPredicate());
}
private final String description;
private final String manaString;
public CraftAbility(String manaString) {
this(manaString, "artifact", "another artifact you control or an artifact card from your graveyard", CardType.ARTIFACT.getPredicate());
}
public CraftAbility(String manaString, String description, String targetDescription, Predicate<MageObject>... predicates) {
this(manaString, description, targetDescription, 1, 1, predicates);
}
public CraftAbility(String manaString, String description, String targetDescription, int minTargets, int maxTargets, Predicate<MageObject>... predicates) {
this(manaString, description, makeTarget(minTargets, maxTargets, targetDescription, predicates));
}
public CraftAbility(String manaString, String description, TargetCardInGraveyardBattlefieldOrStack target) {
super(Zone.BATTLEFIELD, new CraftEffect(), new ManaCostsImpl<>(manaString));
this.addCost(new ExileSourceCost());
this.addCost(new CraftCost(target));
this.addSubAbility(new TransformAbility());
this.timing = TimingRule.SORCERY;
this.manaString = manaString;
this.description = description;
}
private CraftAbility(final CraftAbility ability) {
super(ability);
this.manaString = ability.manaString;
this.description = ability.description;
}
@Override
public CraftAbility copy() {
return new CraftAbility(this);
}
@Override
public String getRule() {
return "Craft with " + description + ' ' + manaString;
}
private static TargetCardInGraveyardBattlefieldOrStack makeTarget(int minTargets, int maxTargets, String targetDescription, Predicate<MageObject>... predicates) {
FilterPermanent filterPermanent = new FilterControlledPermanent();
filterPermanent.add(AnotherPredicate.instance);
FilterCard filterCard = new FilterOwnedCard();
for (Predicate<MageObject> predicate : predicates) {
filterPermanent.add(predicate);
filterCard.add(predicate);
}
return new TargetCardInGraveyardBattlefieldOrStack(minTargets, maxTargets, filterCard, filterPermanent, targetDescription);
}
}
class CraftCost extends CostImpl {
private final TargetCardInGraveyardBattlefieldOrStack target;
CraftCost(TargetCardInGraveyardBattlefieldOrStack target) {
super();
this.target = target;
target.withNotTarget(true);
}
private CraftCost(final CraftCost cost) {
super(cost);
this.target = cost.target.copy();
}
@Override
public CraftCost copy() {
return new CraftCost(this);
}
@Override
public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) {
return target.canChoose(controllerId, source, game);
}
@Override
public boolean pay(Ability ability, Game game, Ability source, UUID controllerId, boolean noMana, Cost costToPay) {
Player player = game.getPlayer(source.getControllerId());
if (player == null) {
paid = false;
return paid;
}
player.chooseTarget(Outcome.Exile, target, source, game);
Set<Card> cards = target
.getTargets()
.stream()
.map(uuid -> {
Permanent permanent = game.getPermanent(uuid);
if (permanent != null) {
return permanent;
}
return game.getCard(uuid);
})
.filter(Objects::nonNull)
.collect(Collectors.toSet());
player.moveCardsToExile(
cards, source, game, true,
CardUtil.getExileZoneId(game, source),
CardUtil.getSourceName(game, source)
);
paid = true;
return paid;
}
}
class CraftEffect extends OneShotEffect {
CraftEffect() {
super(Outcome.Benefit);
}
private CraftEffect(final CraftEffect effect) {
super(effect);
}
@Override
public CraftEffect copy() {
return new CraftEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Player player = game.getPlayer(source.getControllerId());
Card card = game.getCard(source.getSourceId());
if (player == null || card == null || card.getZoneChangeCounter(game) != source.getSourceObjectZoneChangeCounter() + 1) {
return false;
}
game.getState().setValue(TransformAbility.VALUE_KEY_ENTER_TRANSFORMED + source.getSourceId(), Boolean.TRUE);
player.moveCards(card, Zone.BATTLEFIELD, source, game);
return true;
}
}

View file

@ -375,13 +375,20 @@ public final class StaticFilters {
FILTER_CONTROLLED_A_PERMANENT.setLockedFilter(true);
}
public static final FilterControlledPermanent FILTER_CONTROLLED_ANOTHER_PERMANENT = new FilterControlledPermanent("another target permanent you control");
public static final FilterControlledPermanent FILTER_CONTROLLED_ANOTHER_PERMANENT = new FilterControlledPermanent("another permanent you control");
static {
FILTER_CONTROLLED_ANOTHER_PERMANENT.add(AnotherPredicate.instance);
FILTER_CONTROLLED_ANOTHER_PERMANENT.setLockedFilter(true);
}
public static final FilterControlledPermanent FILTER_CONTROLLED_ANOTHER_TARGET_PERMANENT = new FilterControlledPermanent("another target permanent you control");
static {
FILTER_CONTROLLED_ANOTHER_TARGET_PERMANENT.add(AnotherPredicate.instance);
FILTER_CONTROLLED_ANOTHER_TARGET_PERMANENT.setLockedFilter(true);
}
public static final FilterControlledPermanent FILTER_CONTROLLED_PERMANENTS = new FilterControlledPermanent("permanents you control");
static {

View file

@ -32,7 +32,11 @@ public class TargetCardInGraveyardBattlefieldOrStack extends TargetCard {
protected final FilterSpell filterSpell;
public TargetCardInGraveyardBattlefieldOrStack(int minNumTargets, int maxNumTargets, FilterCard filterGraveyard, FilterPermanent filterBattlefield) {
this(minNumTargets, maxNumTargets, filterGraveyard, filterBattlefield, defaultSpellFilter, null);
this(minNumTargets, maxNumTargets, filterGraveyard, filterBattlefield, null);
}
public TargetCardInGraveyardBattlefieldOrStack(int minNumTargets, int maxNumTargets, FilterCard filterGraveyard, FilterPermanent filterBattlefield, String targetName) {
this(minNumTargets, maxNumTargets, filterGraveyard, filterBattlefield, defaultSpellFilter, targetName);
}
public TargetCardInGraveyardBattlefieldOrStack(int minNumTargets, int maxNumTargets, FilterCard filterGraveyard, FilterPermanent filterBattlefield, FilterSpell filterSpell, String targetName) {
@ -40,7 +44,7 @@ public class TargetCardInGraveyardBattlefieldOrStack extends TargetCard {
this.filterPermanent = filterBattlefield;
this.filterSpell = filterSpell;
this.targetName = targetName != null ? targetName : filter.getMessage()
+ " in a graveyard "
+ " in a graveyard"
+ (maxNumTargets > 1 ? " and/or " : " or ")
+ this.filterPermanent.getMessage()
+ " on the battlefield";