From bc4aa6931fe5a84a12c81da78a680490caca6783 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Fri, 27 Oct 2023 22:32:11 -0400 Subject: [PATCH] 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 --- .../src/mage/cards/b/BladewheelChariot.java | 60 ++++++ .../src/mage/cards/f/FelidarGuardian.java | 2 +- Mage.Sets/src/mage/cards/k/KelpieGuide.java | 2 +- Mage.Sets/src/mage/cards/o/OathOfTeferi.java | 2 +- Mage.Sets/src/mage/cards/s/SkySwallower.java | 2 +- .../mage/cards/s/SpringLoadedSawblades.java | 54 +++++ Mage.Sets/src/mage/cards/s/SunbirdEffigy.java | 192 ++++++++++++++++++ .../src/mage/cards/s/SunbirdStandard.java | 38 ++++ .../src/mage/cards/t/TheGrimCaptain.java | 112 ++++++++++ .../mage/cards/t/ThroneOfTheGrimCaptain.java | 111 ++++++++++ .../src/mage/sets/TheLostCavernsOfIxalan.java | 6 + .../cards/abilities/keywords/CraftTest.java | 112 ++++++++++ .../dynamicvalue/RoleAssignment.java | 6 +- .../mage/abilities/keyword/CraftAbility.java | 176 ++++++++++++++++ .../main/java/mage/filter/StaticFilters.java | 9 +- ...rgetCardInGraveyardBattlefieldOrStack.java | 8 +- 16 files changed, 884 insertions(+), 8 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/b/BladewheelChariot.java create mode 100644 Mage.Sets/src/mage/cards/s/SpringLoadedSawblades.java create mode 100644 Mage.Sets/src/mage/cards/s/SunbirdEffigy.java create mode 100644 Mage.Sets/src/mage/cards/s/SunbirdStandard.java create mode 100644 Mage.Sets/src/mage/cards/t/TheGrimCaptain.java create mode 100644 Mage.Sets/src/mage/cards/t/ThroneOfTheGrimCaptain.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/CraftTest.java create mode 100644 Mage/src/main/java/mage/abilities/keyword/CraftAbility.java diff --git a/Mage.Sets/src/mage/cards/b/BladewheelChariot.java b/Mage.Sets/src/mage/cards/b/BladewheelChariot.java new file mode 100644 index 00000000000..7ea85da1175 --- /dev/null +++ b/Mage.Sets/src/mage/cards/b/BladewheelChariot.java @@ -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); + } +} diff --git a/Mage.Sets/src/mage/cards/f/FelidarGuardian.java b/Mage.Sets/src/mage/cards/f/FelidarGuardian.java index c1ea8144bf0..77586e84bad 100644 --- a/Mage.Sets/src/mage/cards/f/FelidarGuardian.java +++ b/Mage.Sets/src/mage/cards/f/FelidarGuardian.java @@ -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); } diff --git a/Mage.Sets/src/mage/cards/k/KelpieGuide.java b/Mage.Sets/src/mage/cards/k/KelpieGuide.java index 57e775dc8b9..cc5d1de32d2 100644 --- a/Mage.Sets/src/mage/cards/k/KelpieGuide.java +++ b/Mage.Sets/src/mage/cards/k/KelpieGuide.java @@ -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. diff --git a/Mage.Sets/src/mage/cards/o/OathOfTeferi.java b/Mage.Sets/src/mage/cards/o/OathOfTeferi.java index 53732db55ff..6d5984a8557 100644 --- a/Mage.Sets/src/mage/cards/o/OathOfTeferi.java +++ b/Mage.Sets/src/mage/cards/o/OathOfTeferi.java @@ -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. diff --git a/Mage.Sets/src/mage/cards/s/SkySwallower.java b/Mage.Sets/src/mage/cards/s/SkySwallower.java index 71ec88316db..c11a922fe28 100644 --- a/Mage.Sets/src/mage/cards/s/SkySwallower.java +++ b/Mage.Sets/src/mage/cards/s/SkySwallower.java @@ -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); } diff --git a/Mage.Sets/src/mage/cards/s/SpringLoadedSawblades.java b/Mage.Sets/src/mage/cards/s/SpringLoadedSawblades.java new file mode 100644 index 00000000000..d9c7374a10f --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SpringLoadedSawblades.java @@ -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); + } +} diff --git a/Mage.Sets/src/mage/cards/s/SunbirdEffigy.java b/Mage.Sets/src/mage/cards/s/SunbirdEffigy.java new file mode 100644 index 00000000000..4687065fb73 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SunbirdEffigy.java @@ -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; + } +} diff --git a/Mage.Sets/src/mage/cards/s/SunbirdStandard.java b/Mage.Sets/src/mage/cards/s/SunbirdStandard.java new file mode 100644 index 00000000000..0f82aa34aa4 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SunbirdStandard.java @@ -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); + } +} diff --git a/Mage.Sets/src/mage/cards/t/TheGrimCaptain.java b/Mage.Sets/src/mage/cards/t/TheGrimCaptain.java new file mode 100644 index 00000000000..c03f57f2051 --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/TheGrimCaptain.java @@ -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; + } +} diff --git a/Mage.Sets/src/mage/cards/t/ThroneOfTheGrimCaptain.java b/Mage.Sets/src/mage/cards/t/ThroneOfTheGrimCaptain.java new file mode 100644 index 00000000000..5fd6c80368a --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/ThroneOfTheGrimCaptain.java @@ -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 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 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; + } +} diff --git a/Mage.Sets/src/mage/sets/TheLostCavernsOfIxalan.java b/Mage.Sets/src/mage/sets/TheLostCavernsOfIxalan.java index 83832a4a4ff..3b8da314ad9 100644 --- a/Mage.Sets/src/mage/sets/TheLostCavernsOfIxalan.java +++ b/Mage.Sets/src/mage/sets/TheLostCavernsOfIxalan.java @@ -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)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/CraftTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/CraftTest.java new file mode 100644 index 00000000000..7e411d4fcdb --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/CraftTest.java @@ -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); + } +} diff --git a/Mage/src/main/java/mage/abilities/dynamicvalue/RoleAssignment.java b/Mage/src/main/java/mage/abilities/dynamicvalue/RoleAssignment.java index 829d05f4976..3620fd613d7 100644 --- a/Mage/src/main/java/mage/abilities/dynamicvalue/RoleAssignment.java +++ b/Mage/src/main/java/mage/abilities/dynamicvalue/RoleAssignment.java @@ -56,8 +56,12 @@ public abstract class RoleAssignment implements Serializable { } public int getRoleCount(Cards cards, Game game) { + return getRoleCount(cards.getCards(game), game); + } + + public int getRoleCount(Set cards, Game game) { Map> 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(); } diff --git a/Mage/src/main/java/mage/abilities/keyword/CraftAbility.java b/Mage/src/main/java/mage/abilities/keyword/CraftAbility.java new file mode 100644 index 00000000000..eaaa25a5b8f --- /dev/null +++ b/Mage/src/main/java/mage/abilities/keyword/CraftAbility.java @@ -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... predicates) { + this(manaString, description, targetDescription, 1, 1, predicates); + } + + public CraftAbility(String manaString, String description, String targetDescription, int minTargets, int maxTargets, Predicate... 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... predicates) { + FilterPermanent filterPermanent = new FilterControlledPermanent(); + filterPermanent.add(AnotherPredicate.instance); + FilterCard filterCard = new FilterOwnedCard(); + for (Predicate 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 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; + } +} diff --git a/Mage/src/main/java/mage/filter/StaticFilters.java b/Mage/src/main/java/mage/filter/StaticFilters.java index 3094d079006..a5af3f42c6c 100644 --- a/Mage/src/main/java/mage/filter/StaticFilters.java +++ b/Mage/src/main/java/mage/filter/StaticFilters.java @@ -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 { diff --git a/Mage/src/main/java/mage/target/common/TargetCardInGraveyardBattlefieldOrStack.java b/Mage/src/main/java/mage/target/common/TargetCardInGraveyardBattlefieldOrStack.java index 58aa17d2e49..fcb720cdd16 100644 --- a/Mage/src/main/java/mage/target/common/TargetCardInGraveyardBattlefieldOrStack.java +++ b/Mage/src/main/java/mage/target/common/TargetCardInGraveyardBattlefieldOrStack.java @@ -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";