From ba20e97b712ebc6ad88e8c9a166a65e5563d2257 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sun, 31 Mar 2024 12:11:34 -0400 Subject: [PATCH] [OTJ] Implementing "spree" mechanic (#12018) * [OTJ] Implement Unfortunate Accident * fix errors * a few more things * [OTJ] Implement Three Steps Ahead * [OTJ] Implement Caught in the Crossfire * [OTJ] Implement Insatiable Avarice * add test * [OTJ] Implement Explosive Derailment * [OTJ] Implement Requisition Raid * [OTJ] Implement Rustler Rampage * add comment to test * [OTJ] Implement Metamorphic Blast * [OTJ] Implement Final Showdown * rework cost addition, add test * move cost application to its own loop --- .../mage/cards/c/CaughtInTheCrossfire.java | 53 +++++++++++ .../src/mage/cards/e/ExplosiveDerailment.java | 46 +++++++++ Mage.Sets/src/mage/cards/f/FinalShowdown.java | 94 ++++++++++++++++++ .../src/mage/cards/i/InsatiableAvarice.java | 48 ++++++++++ .../src/mage/cards/m/MetamorphicBlast.java | 48 ++++++++++ .../src/mage/cards/r/RequisitionRaid.java | 86 +++++++++++++++++ .../src/mage/cards/r/RustlerRampage.java | 80 ++++++++++++++++ .../src/mage/cards/t/ThreeStepsAhead.java | 53 +++++++++++ .../src/mage/cards/u/UnfortunateAccident.java | 46 +++++++++ .../mage/sets/OutlawsOfThunderJunction.java | 9 ++ .../cards/abilities/keywords/SpreeTest.java | 95 +++++++++++++++++++ .../src/main/java/mage/abilities/Ability.java | 8 ++ .../main/java/mage/abilities/AbilityImpl.java | 16 ++++ Mage/src/main/java/mage/abilities/Mode.java | 12 +++ Mage/src/main/java/mage/abilities/Modes.java | 9 +- .../mage/abilities/keyword/SpreeAbility.java | 28 ++++++ .../java/mage/game/stack/StackAbility.java | 10 +- Utils/keywords.txt | 1 + 18 files changed, 740 insertions(+), 2 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/c/CaughtInTheCrossfire.java create mode 100644 Mage.Sets/src/mage/cards/e/ExplosiveDerailment.java create mode 100644 Mage.Sets/src/mage/cards/f/FinalShowdown.java create mode 100644 Mage.Sets/src/mage/cards/i/InsatiableAvarice.java create mode 100644 Mage.Sets/src/mage/cards/m/MetamorphicBlast.java create mode 100644 Mage.Sets/src/mage/cards/r/RequisitionRaid.java create mode 100644 Mage.Sets/src/mage/cards/r/RustlerRampage.java create mode 100644 Mage.Sets/src/mage/cards/t/ThreeStepsAhead.java create mode 100644 Mage.Sets/src/mage/cards/u/UnfortunateAccident.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/SpreeTest.java create mode 100644 Mage/src/main/java/mage/abilities/keyword/SpreeAbility.java diff --git a/Mage.Sets/src/mage/cards/c/CaughtInTheCrossfire.java b/Mage.Sets/src/mage/cards/c/CaughtInTheCrossfire.java new file mode 100644 index 00000000000..71dab2fa0f8 --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/CaughtInTheCrossfire.java @@ -0,0 +1,53 @@ +package mage.cards.c; + +import mage.abilities.Mode; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.effects.common.DamageAllEffect; +import mage.abilities.keyword.SpreeAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.mageobject.OutlawPredicate; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class CaughtInTheCrossfire extends CardImpl { + + private static final FilterPermanent filter = new FilterCreaturePermanent("outlaw creature"); + private static final FilterPermanent filter2 = new FilterCreaturePermanent("non-outlaw creature"); + + static { + filter.add(OutlawPredicate.instance); + filter.add(Predicates.not(OutlawPredicate.instance)); + } + + public CaughtInTheCrossfire(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{R}{R}"); + + // Spree + this.addAbility(new SpreeAbility(this)); + + // + {1} -- Caught in the Crossfire deals 2 damage to each outlaw creature. + this.getSpellAbility().addEffect(new DamageAllEffect(2, filter)); + this.getSpellAbility().withFirstModeCost(new GenericManaCost(1)); + + // + {1} -- Caught in the Crossfire deals 2 damage to each non-outlaw creature. + this.getSpellAbility().addMode(new Mode(new DamageAllEffect(2, filter2)) + .withCost(new GenericManaCost(1))); + } + + private CaughtInTheCrossfire(final CaughtInTheCrossfire card) { + super(card); + } + + @Override + public CaughtInTheCrossfire copy() { + return new CaughtInTheCrossfire(this); + } +} diff --git a/Mage.Sets/src/mage/cards/e/ExplosiveDerailment.java b/Mage.Sets/src/mage/cards/e/ExplosiveDerailment.java new file mode 100644 index 00000000000..1bd815956a3 --- /dev/null +++ b/Mage.Sets/src/mage/cards/e/ExplosiveDerailment.java @@ -0,0 +1,46 @@ +package mage.cards.e; + +import mage.abilities.Mode; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.effects.common.DamageTargetEffect; +import mage.abilities.effects.common.DestroyTargetEffect; +import mage.abilities.keyword.SpreeAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.target.common.TargetArtifactPermanent; +import mage.target.common.TargetCreaturePermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class ExplosiveDerailment extends CardImpl { + + public ExplosiveDerailment(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{R}"); + + // Spree + this.addAbility(new SpreeAbility(this)); + + // + {2} -- Explosive Derailment deals 4 damage to target creature. + this.getSpellAbility().addEffect(new DamageTargetEffect(4)); + this.getSpellAbility().addTarget(new TargetCreaturePermanent()); + this.getSpellAbility().withFirstModeCost(new GenericManaCost(2)); + + // + {2} -- Destroy target artifact. + this.getSpellAbility().addMode(new Mode(new DestroyTargetEffect()) + .addTarget(new TargetArtifactPermanent()) + .withCost(new GenericManaCost(2))); + } + + private ExplosiveDerailment(final ExplosiveDerailment card) { + super(card); + } + + @Override + public ExplosiveDerailment copy() { + return new ExplosiveDerailment(this); + } +} diff --git a/Mage.Sets/src/mage/cards/f/FinalShowdown.java b/Mage.Sets/src/mage/cards/f/FinalShowdown.java new file mode 100644 index 00000000000..7cc0142b8ac --- /dev/null +++ b/Mage.Sets/src/mage/cards/f/FinalShowdown.java @@ -0,0 +1,94 @@ +package mage.cards.f; + +import mage.abilities.Ability; +import mage.abilities.Mode; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.DestroyAllEffect; +import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; +import mage.abilities.effects.common.continuous.LoseAllAbilitiesAllEffect; +import mage.abilities.keyword.IndestructibleAbility; +import mage.abilities.keyword.SpreeAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.players.Player; +import mage.target.TargetPermanent; +import mage.target.common.TargetControlledCreaturePermanent; +import mage.target.targetpointer.FixedTarget; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class FinalShowdown extends CardImpl { + + public FinalShowdown(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{W}"); + + // Spree + this.addAbility(new SpreeAbility(this)); + + // + {1} -- All creatures lose all abilities until end of turn. + this.getSpellAbility().addEffect(new LoseAllAbilitiesAllEffect( + StaticFilters.FILTER_PERMANENT_CREATURES, Duration.EndOfTurn + ).setText("all creatures lose all abilities until end of turn")); + this.getSpellAbility().withFirstModeCost(new GenericManaCost(1)); + + // + {1} -- Choose a creature you control. It gains indestructible until end of turn. + this.getSpellAbility().addMode(new Mode(new FinalShowdownEffect()).withCost(new GenericManaCost(1))); + + // + {3}{W}{W} -- Destroy all creatures. + this.getSpellAbility().addMode(new Mode(new DestroyAllEffect(StaticFilters.FILTER_PERMANENT_CREATURES)) + .withCost(new ManaCostsImpl<>("{3}{W}{W}"))); + } + + private FinalShowdown(final FinalShowdown card) { + super(card); + } + + @Override + public FinalShowdown copy() { + return new FinalShowdown(this); + } +} + +class FinalShowdownEffect extends OneShotEffect { + + FinalShowdownEffect() { + super(Outcome.Benefit); + staticText = "choose a creature you control. It gains indestructible until end of turn"; + } + + private FinalShowdownEffect(final FinalShowdownEffect effect) { + super(effect); + } + + @Override + public FinalShowdownEffect copy() { + return new FinalShowdownEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null || !game.getBattlefield().contains( + StaticFilters.FILTER_CONTROLLED_CREATURE, source, game, 1 + )) { + return false; + } + TargetPermanent target = new TargetControlledCreaturePermanent(); + target.withNotTarget(true); + player.choose(outcome, target, source, game); + game.addEffect(new GainAbilityTargetEffect( + IndestructibleAbility.getInstance(), Duration.EndOfTurn + ).setTargetPointer(new FixedTarget(target.getFirstTarget(), game)), source); + return true; + } +} diff --git a/Mage.Sets/src/mage/cards/i/InsatiableAvarice.java b/Mage.Sets/src/mage/cards/i/InsatiableAvarice.java new file mode 100644 index 00000000000..8c404e52275 --- /dev/null +++ b/Mage.Sets/src/mage/cards/i/InsatiableAvarice.java @@ -0,0 +1,48 @@ +package mage.cards.i; + +import mage.abilities.Mode; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.DrawCardTargetEffect; +import mage.abilities.effects.common.LoseLifeTargetEffect; +import mage.abilities.effects.common.search.SearchLibraryPutOnLibraryEffect; +import mage.abilities.keyword.SpreeAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.target.TargetPlayer; +import mage.target.common.TargetCardInLibrary; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class InsatiableAvarice extends CardImpl { + + public InsatiableAvarice(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{B}"); + + // Spree + this.addAbility(new SpreeAbility(this)); + + // + {2} -- Search your library for a card, then shuffle and put that card on top. + this.getSpellAbility().addEffect(new SearchLibraryPutOnLibraryEffect(new TargetCardInLibrary(), false)); + this.getSpellAbility().withFirstModeCost(new GenericManaCost(2)); + + // + {B}{B} -- Target player draws three cards and loses 3 life. + this.getSpellAbility().addMode(new Mode(new DrawCardTargetEffect(3)) + .addEffect(new LoseLifeTargetEffect(3).setText("and loses 3 life")) + .addTarget(new TargetPlayer()) + .withCost(new ManaCostsImpl<>("{B}{B}"))); + } + + private InsatiableAvarice(final InsatiableAvarice card) { + super(card); + } + + @Override + public InsatiableAvarice copy() { + return new InsatiableAvarice(this); + } +} diff --git a/Mage.Sets/src/mage/cards/m/MetamorphicBlast.java b/Mage.Sets/src/mage/cards/m/MetamorphicBlast.java new file mode 100644 index 00000000000..4252782f89b --- /dev/null +++ b/Mage.Sets/src/mage/cards/m/MetamorphicBlast.java @@ -0,0 +1,48 @@ +package mage.cards.m; + +import mage.abilities.Mode; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.effects.common.DrawCardTargetEffect; +import mage.abilities.effects.common.continuous.BecomesCreatureTargetEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.game.permanent.token.custom.CreatureToken; +import mage.target.TargetPlayer; +import mage.target.common.TargetCreaturePermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class MetamorphicBlast extends CardImpl { + + public MetamorphicBlast(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{U}"); + + // Spree + // + {1} -- Until end of turn, target creature becomes a white Rabbit with base power and toughness 0/1. + this.getSpellAbility().addEffect(new BecomesCreatureTargetEffect(new CreatureToken( + 0, 1, "white Rabbit with base power and toughness 0/1" + ).withSubType(SubType.RABBIT).withColor("W"), false, false, Duration.EndOfTurn)); + this.getSpellAbility().addTarget(new TargetCreaturePermanent()); + this.getSpellAbility().withFirstModeCost(new GenericManaCost(1)); + + // + {3} -- Target player draws two cards. + this.getSpellAbility().addMode(new Mode(new DrawCardTargetEffect(2)) + .addTarget(new TargetPlayer()) + .withCost(new GenericManaCost(3))); + } + + private MetamorphicBlast(final MetamorphicBlast card) { + super(card); + } + + @Override + public MetamorphicBlast copy() { + return new MetamorphicBlast(this); + } +} diff --git a/Mage.Sets/src/mage/cards/r/RequisitionRaid.java b/Mage.Sets/src/mage/cards/r/RequisitionRaid.java new file mode 100644 index 00000000000..0dd899a0324 --- /dev/null +++ b/Mage.Sets/src/mage/cards/r/RequisitionRaid.java @@ -0,0 +1,86 @@ +package mage.cards.r; + +import mage.abilities.Ability; +import mage.abilities.Mode; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.DestroyTargetEffect; +import mage.abilities.keyword.SpreeAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.counters.CounterType; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.target.TargetPlayer; +import mage.target.common.TargetArtifactPermanent; +import mage.target.common.TargetEnchantmentPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class RequisitionRaid extends CardImpl { + + public RequisitionRaid(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{W}"); + + // Spree + this.addAbility(new SpreeAbility(this)); + + // + {1} -- Destroy target artifact. + this.getSpellAbility().addEffect(new DestroyTargetEffect()); + this.getSpellAbility().addTarget(new TargetArtifactPermanent()); + this.getSpellAbility().withFirstModeCost(new GenericManaCost(1)); + + // + {1} -- Destroy target enchantment. + this.getSpellAbility().addMode(new Mode(new DestroyTargetEffect()) + .addTarget(new TargetEnchantmentPermanent()) + .withCost(new GenericManaCost(1))); + + // + {1} -- Put a +1/+1 counter on each creature target player controls. + this.getSpellAbility().addMode(new Mode(new RequisitionRaidEffect()) + .addTarget(new TargetPlayer()) + .withCost(new GenericManaCost(1))); + } + + private RequisitionRaid(final RequisitionRaid card) { + super(card); + } + + @Override + public RequisitionRaid copy() { + return new RequisitionRaid(this); + } +} + +class RequisitionRaidEffect extends OneShotEffect { + + RequisitionRaidEffect() { + super(Outcome.Benefit); + staticText = "put a +1/+1 counter on each creature target player controls"; + } + + private RequisitionRaidEffect(final RequisitionRaidEffect effect) { + super(effect); + } + + @Override + public RequisitionRaidEffect copy() { + return new RequisitionRaidEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + for (Permanent permanent : game.getBattlefield().getActivePermanents( + StaticFilters.FILTER_CONTROLLED_CREATURE, + getTargetPointer().getFirst(game, source), source, game + )) { + permanent.addCounters(CounterType.P1P1.createInstance(), source, game); + } + return true; + } +} diff --git a/Mage.Sets/src/mage/cards/r/RustlerRampage.java b/Mage.Sets/src/mage/cards/r/RustlerRampage.java new file mode 100644 index 00000000000..16dff102d58 --- /dev/null +++ b/Mage.Sets/src/mage/cards/r/RustlerRampage.java @@ -0,0 +1,80 @@ +package mage.cards.r; + +import mage.abilities.Ability; +import mage.abilities.Mode; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; +import mage.abilities.keyword.DoubleStrikeAbility; +import mage.abilities.keyword.SpreeAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.target.TargetPlayer; +import mage.target.common.TargetCreaturePermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class RustlerRampage extends CardImpl { + + public RustlerRampage(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{W}"); + + // Spree + this.addAbility(new SpreeAbility(this)); + + // + {1} -- Untap all creatures target player controls. + this.getSpellAbility().addEffect(new RustlerRampageEffect()); + this.getSpellAbility().addTarget(new TargetPlayer()); + this.getSpellAbility().withFirstModeCost(new GenericManaCost(1)); + + // + {1} -- Target creature gains double strike until end of turn. + this.getSpellAbility().addMode(new Mode(new GainAbilityTargetEffect(DoubleStrikeAbility.getInstance())) + .addTarget(new TargetCreaturePermanent()) + .withCost(new GenericManaCost(1))); + } + + private RustlerRampage(final RustlerRampage card) { + super(card); + } + + @Override + public RustlerRampage copy() { + return new RustlerRampage(this); + } +} + +class RustlerRampageEffect extends OneShotEffect { + + RustlerRampageEffect() { + super(Outcome.Benefit); + staticText = "untap all creatures target player controls"; + } + + private RustlerRampageEffect(final RustlerRampageEffect effect) { + super(effect); + } + + @Override + public RustlerRampageEffect copy() { + return new RustlerRampageEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + for (Permanent permanent : game.getBattlefield().getActivePermanents( + StaticFilters.FILTER_CONTROLLED_CREATURE, + getTargetPointer().getFirst(game, source), source, game + )) { + permanent.untap(game); + } + return true; + } +} diff --git a/Mage.Sets/src/mage/cards/t/ThreeStepsAhead.java b/Mage.Sets/src/mage/cards/t/ThreeStepsAhead.java new file mode 100644 index 00000000000..fb09123cee1 --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/ThreeStepsAhead.java @@ -0,0 +1,53 @@ +package mage.cards.t; + +import mage.abilities.Mode; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.CounterTargetEffect; +import mage.abilities.effects.common.CreateTokenCopyTargetEffect; +import mage.abilities.effects.common.DrawDiscardControllerEffect; +import mage.abilities.keyword.SpreeAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.filter.StaticFilters; +import mage.target.TargetPermanent; +import mage.target.TargetSpell; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class ThreeStepsAhead extends CardImpl { + + public ThreeStepsAhead(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{U}"); + + // Spree + this.addAbility(new SpreeAbility(this)); + + // + {1}{U} -- Counter target spell. + this.getSpellAbility().addEffect(new CounterTargetEffect()); + this.getSpellAbility().addTarget(new TargetSpell()); + this.getSpellAbility().withFirstModeCost(new ManaCostsImpl<>("{1}{U}")); + + // + {3} -- Create a token that's a copy of target artifact or creature you control. + this.getSpellAbility().addMode(new Mode(new CreateTokenCopyTargetEffect()) + .addTarget(new TargetPermanent(StaticFilters.FILTER_CONTROLLED_PERMANENT_ARTIFACT_OR_CREATURE)) + .withCost(new GenericManaCost(3))); + + // + {2} -- Draw two cards, then discard a card. + this.getSpellAbility().addMode(new Mode(new DrawDiscardControllerEffect(2, 1)) + .withCost(new GenericManaCost(2))); + } + + private ThreeStepsAhead(final ThreeStepsAhead card) { + super(card); + } + + @Override + public ThreeStepsAhead copy() { + return new ThreeStepsAhead(this); + } +} diff --git a/Mage.Sets/src/mage/cards/u/UnfortunateAccident.java b/Mage.Sets/src/mage/cards/u/UnfortunateAccident.java new file mode 100644 index 00000000000..dc3b4e29c36 --- /dev/null +++ b/Mage.Sets/src/mage/cards/u/UnfortunateAccident.java @@ -0,0 +1,46 @@ +package mage.cards.u; + +import mage.abilities.Mode; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.DestroyTargetEffect; +import mage.abilities.keyword.SpreeAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.game.permanent.token.MercenaryToken; +import mage.target.common.TargetCreaturePermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class UnfortunateAccident extends CardImpl { + + public UnfortunateAccident(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{B}"); + + // Spree + this.addAbility(new SpreeAbility(this)); + + // + {2}{B} -- Destroy target creature. + this.getSpellAbility().addEffect(new DestroyTargetEffect()); + this.getSpellAbility().addTarget(new TargetCreaturePermanent()); + this.getSpellAbility().withFirstModeCost(new ManaCostsImpl<>("{2}{B}")); + + // + {1} -- Create a 1/1 red Mercenary creature token with "{T}: Target creature you control gets +1/+0 until end of turn. Activate only as a sorcery." + this.getSpellAbility().addMode(new Mode(new CreateTokenEffect(new MercenaryToken())) + .withCost(new GenericManaCost(1))); + } + + private UnfortunateAccident(final UnfortunateAccident card) { + super(card); + } + + @Override + public UnfortunateAccident copy() { + return new UnfortunateAccident(this); + } +} diff --git a/Mage.Sets/src/mage/sets/OutlawsOfThunderJunction.java b/Mage.Sets/src/mage/sets/OutlawsOfThunderJunction.java index b9f3c483968..8c4b893445b 100644 --- a/Mage.Sets/src/mage/sets/OutlawsOfThunderJunction.java +++ b/Mage.Sets/src/mage/sets/OutlawsOfThunderJunction.java @@ -59,6 +59,7 @@ public final class OutlawsOfThunderJunction extends ExpansionSet { cards.add(new SetCardInfo("Cactusfolk Sureshot", 199, Rarity.UNCOMMON, mage.cards.c.CactusfolkSureshot.class)); cards.add(new SetCardInfo("Calamity, Galloping Inferno", 116, Rarity.RARE, mage.cards.c.CalamityGallopingInferno.class)); cards.add(new SetCardInfo("Canyon Crab", 40, Rarity.UNCOMMON, mage.cards.c.CanyonCrab.class)); + cards.add(new SetCardInfo("Caught in the Crossfire", 117, Rarity.UNCOMMON, mage.cards.c.CaughtInTheCrossfire.class)); cards.add(new SetCardInfo("Caustic Bronco", 82, Rarity.RARE, mage.cards.c.CausticBronco.class)); cards.add(new SetCardInfo("Colossal Rattlewurm", 159, Rarity.RARE, mage.cards.c.ColossalRattlewurm.class)); cards.add(new SetCardInfo("Concealed Courtyard", 268, Rarity.RARE, mage.cards.c.ConcealedCourtyard.class)); @@ -77,10 +78,12 @@ public final class OutlawsOfThunderJunction extends ExpansionSet { cards.add(new SetCardInfo("Eriette's Lullaby", 10, Rarity.COMMON, mage.cards.e.EriettesLullaby.class)); cards.add(new SetCardInfo("Eroded Canyon", 256, Rarity.COMMON, mage.cards.e.ErodedCanyon.class)); cards.add(new SetCardInfo("Ertha Jo, Frontier Mentor", 203, Rarity.UNCOMMON, mage.cards.e.ErthaJoFrontierMentor.class)); + cards.add(new SetCardInfo("Explosive Derailment", 122, Rarity.COMMON, mage.cards.e.ExplosiveDerailment.class)); cards.add(new SetCardInfo("Failed Fording", 47, Rarity.COMMON, mage.cards.f.FailedFording.class)); cards.add(new SetCardInfo("Fake Your Own Death", 87, Rarity.COMMON, mage.cards.f.FakeYourOwnDeath.class)); cards.add(new SetCardInfo("Ferocification", 123, Rarity.UNCOMMON, mage.cards.f.Ferocification.class)); cards.add(new SetCardInfo("Festering Gulch", 257, Rarity.COMMON, mage.cards.f.FesteringGulch.class)); + cards.add(new SetCardInfo("Final Showdown", 11, Rarity.MYTHIC, mage.cards.f.FinalShowdown.class)); cards.add(new SetCardInfo("Fleeting Reflection", 49, Rarity.UNCOMMON, mage.cards.f.FleetingReflection.class)); cards.add(new SetCardInfo("Forest", 276, Rarity.LAND, mage.cards.basiclands.Forest.class, FULL_ART_BFZ_VARIOUS)); cards.add(new SetCardInfo("Forlorn Flats", 258, Rarity.COMMON, mage.cards.f.ForlornFlats.class)); @@ -104,6 +107,7 @@ public final class OutlawsOfThunderJunction extends ExpansionSet { cards.add(new SetCardInfo("High Noon", 15, Rarity.RARE, mage.cards.h.HighNoon.class)); cards.add(new SetCardInfo("Holy Cow", 16, Rarity.COMMON, mage.cards.h.HolyCow.class)); cards.add(new SetCardInfo("Honest Rutstein", 207, Rarity.UNCOMMON, mage.cards.h.HonestRutstein.class)); + cards.add(new SetCardInfo("Insatiable Avarice", 91, Rarity.RARE, mage.cards.i.InsatiableAvarice.class)); cards.add(new SetCardInfo("Inspiring Vantage", 269, Rarity.RARE, mage.cards.i.InspiringVantage.class)); cards.add(new SetCardInfo("Intimidation Campaign", 208, Rarity.UNCOMMON, mage.cards.i.IntimidationCampaign.class)); cards.add(new SetCardInfo("Intrepid Stablemaster", 169, Rarity.UNCOMMON, mage.cards.i.IntrepidStablemaster.class)); @@ -131,6 +135,7 @@ public final class OutlawsOfThunderJunction extends ExpansionSet { cards.add(new SetCardInfo("Map the Frontier", 170, Rarity.UNCOMMON, mage.cards.m.MapTheFrontier.class)); cards.add(new SetCardInfo("Marauding Sphinx", 56, Rarity.UNCOMMON, mage.cards.m.MaraudingSphinx.class)); cards.add(new SetCardInfo("Marchesa, Dealer of Death", 220, Rarity.RARE, mage.cards.m.MarchesaDealerOfDeath.class)); + cards.add(new SetCardInfo("Metamorphic Blast", 57, Rarity.UNCOMMON, mage.cards.m.MetamorphicBlast.class)); cards.add(new SetCardInfo("Mine Raider", 135, Rarity.COMMON, mage.cards.m.MineRaider.class)); cards.add(new SetCardInfo("Miriam, Herd Whisperer", 221, Rarity.UNCOMMON, mage.cards.m.MiriamHerdWhisperer.class)); cards.add(new SetCardInfo("Mobile Homestead", 245, Rarity.UNCOMMON, mage.cards.m.MobileHomestead.class)); @@ -163,12 +168,14 @@ public final class OutlawsOfThunderJunction extends ExpansionSet { cards.add(new SetCardInfo("Reach for the Sky", 178, Rarity.COMMON, mage.cards.r.ReachForTheSky.class)); cards.add(new SetCardInfo("Reckless Lackey", 140, Rarity.COMMON, mage.cards.r.RecklessLackey.class)); cards.add(new SetCardInfo("Redrock Sentinel", 247, Rarity.UNCOMMON, mage.cards.r.RedrockSentinel.class)); + cards.add(new SetCardInfo("Requisition Raid", 26, Rarity.UNCOMMON, mage.cards.r.RequisitionRaid.class)); cards.add(new SetCardInfo("Resilient Roadrunner", 141, Rarity.UNCOMMON, mage.cards.r.ResilientRoadrunner.class)); cards.add(new SetCardInfo("Rictus Robber", 102, Rarity.UNCOMMON, mage.cards.r.RictusRobber.class)); cards.add(new SetCardInfo("Rise of the Varmints", 179, Rarity.UNCOMMON, mage.cards.r.RiseOfTheVarmints.class)); cards.add(new SetCardInfo("Rodeo Pyromancers", 143, Rarity.COMMON, mage.cards.r.RodeoPyromancers.class)); cards.add(new SetCardInfo("Rooftop Assassin", 103, Rarity.COMMON, mage.cards.r.RooftopAssassin.class)); cards.add(new SetCardInfo("Roxanne, Starfall Savant", 228, Rarity.RARE, mage.cards.r.RoxanneStarfallSavant.class)); + cards.add(new SetCardInfo("Rustler Rampage", 27, Rarity.UNCOMMON, mage.cards.r.RustlerRampage.class)); cards.add(new SetCardInfo("Ruthless Lawbringer", 229, Rarity.UNCOMMON, mage.cards.r.RuthlessLawbringer.class)); cards.add(new SetCardInfo("Sandstorm Verge", 263, Rarity.UNCOMMON, mage.cards.s.SandstormVerge.class)); cards.add(new SetCardInfo("Scalestorm Summoner", 144, Rarity.UNCOMMON, mage.cards.s.ScalestormSummoner.class)); @@ -195,11 +202,13 @@ public final class OutlawsOfThunderJunction extends ExpansionSet { cards.add(new SetCardInfo("Swamp", 274, Rarity.LAND, mage.cards.basiclands.Swamp.class, FULL_ART_BFZ_VARIOUS)); cards.add(new SetCardInfo("Terror of the Peaks", 149, Rarity.MYTHIC, mage.cards.t.TerrorOfThePeaks.class)); cards.add(new SetCardInfo("This Town Ain't Big Enough", 74, Rarity.UNCOMMON, mage.cards.t.ThisTownAintBigEnough.class)); + cards.add(new SetCardInfo("Three Steps Ahead", 75, Rarity.RARE, mage.cards.t.ThreeStepsAhead.class)); cards.add(new SetCardInfo("Thunder Lasso", 35, Rarity.UNCOMMON, mage.cards.t.ThunderLasso.class)); cards.add(new SetCardInfo("Tomb Trawler", 250, Rarity.UNCOMMON, mage.cards.t.TombTrawler.class)); cards.add(new SetCardInfo("Trained Arynx", 36, Rarity.COMMON, mage.cards.t.TrainedArynx.class)); cards.add(new SetCardInfo("Treasure Dredger", 110, Rarity.UNCOMMON, mage.cards.t.TreasureDredger.class)); cards.add(new SetCardInfo("Tumbleweed Rising", 187, Rarity.COMMON, mage.cards.t.TumbleweedRising.class)); + cards.add(new SetCardInfo("Unfortunate Accident", 111, Rarity.UNCOMMON, mage.cards.u.UnfortunateAccident.class)); cards.add(new SetCardInfo("Unscrupulous Contractor", 112, Rarity.UNCOMMON, mage.cards.u.UnscrupulousContractor.class)); cards.add(new SetCardInfo("Vengeful Townsfolk", 37, Rarity.COMMON, mage.cards.v.VengefulTownsfolk.class)); cards.add(new SetCardInfo("Vial Smasher, Gleeful Grenadier", 235, Rarity.UNCOMMON, mage.cards.v.VialSmasherGleefulGrenadier.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/SpreeTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/SpreeTest.java new file mode 100644 index 00000000000..d15f224a4a2 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/SpreeTest.java @@ -0,0 +1,95 @@ +package org.mage.test.cards.abilities.keywords; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author TheElk801 + */ +public class SpreeTest extends CardTestPlayerBase { + + private static final String accident = "Unfortunate Accident"; + // Instant {B} + // Spree + // + {2}{B} -- Destroy target creature. + // + {1} -- Create a 1/1 Mercenary token + + private static final String bear = "Grizzly Bears"; + + @Test + public void testFirstMode() { + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1 + 3); + addCard(Zone.BATTLEFIELD, playerA, bear, 1); + addCard(Zone.HAND, playerA, accident); + + setModeChoice(playerA, "1"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, accident, bear); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertGraveyardCount(playerA, bear, 1); + assertPermanentCount(playerA, "Mercenary Token", 0); + assertTappedCount("Swamp", true, 1 + 3); + } + + @Test + public void testSecondMode() { + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1 + 1); + addCard(Zone.HAND, playerA, accident); + + setModeChoice(playerA, "2"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, accident); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, "Mercenary Token", 1); + assertTappedCount("Swamp", true, 1 + 1); + } + + @Test + public void testBothModes() { + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1 + 3 + 1); + addCard(Zone.BATTLEFIELD, playerA, bear, 1); + addCard(Zone.HAND, playerA, accident); + + setModeChoice(playerA, "1"); + setModeChoice(playerA, "2"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, accident, bear); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertGraveyardCount(playerA, bear, 1); + assertPermanentCount(playerA, "Mercenary Token", 1); + assertTappedCount("Swamp", true, 1 + 3 + 1); + } + + private static final String electromancer = "Goblin Electromancer"; + + @Test + public void testReduction() { + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1 + 3 + 1 - 1); + addCard(Zone.BATTLEFIELD, playerA, bear, 1); + addCard(Zone.BATTLEFIELD, playerA, electromancer, 1); + addCard(Zone.HAND, playerA, accident); + + setModeChoice(playerA, "1"); + setModeChoice(playerA, "2"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, accident, bear); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertGraveyardCount(playerA, bear, 1); + assertPermanentCount(playerA, "Mercenary Token", 1); + assertTappedCount("Swamp", true, 1 + 3 + 1 - 1); + } +} diff --git a/Mage/src/main/java/mage/abilities/Ability.java b/Mage/src/main/java/mage/abilities/Ability.java index 1c285a8e6e2..d538c8fe84d 100644 --- a/Mage/src/main/java/mage/abilities/Ability.java +++ b/Mage/src/main/java/mage/abilities/Ability.java @@ -514,6 +514,14 @@ public interface Ability extends Controllable, Serializable { */ Ability withFirstModeFlavorWord(String flavorWord); + /** + * Sets cost word for first mode + * + * @param cost + * @return + */ + Ability withFirstModeCost(Cost cost); + /** * Creates the message about the ability casting/triggering/activating to * post in the game log before the ability resolves. diff --git a/Mage/src/main/java/mage/abilities/AbilityImpl.java b/Mage/src/main/java/mage/abilities/AbilityImpl.java index ed704181abb..8665ab1d665 100644 --- a/Mage/src/main/java/mage/abilities/AbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/AbilityImpl.java @@ -311,6 +311,16 @@ public abstract class AbilityImpl implements Ability { return false; } + // apply mode costs if they have them + for (UUID modeId : this.getModes().getSelectedModes()) { + Cost cost = this.getModes().get(modeId).getCost(); + if (cost instanceof ManaCost) { + this.addManaCostsToPay((ManaCost) cost.copy()); + } else if (cost != null) { + this.costs.add(cost.copy()); + } + } + // unit tests only: it allows to add targets/choices by two ways: // 1. From cast/activate command params (process it here) // 2. From single addTarget/setChoice, it's a preffered method for tests (process it in normal choose dialogs like human player) @@ -1141,6 +1151,12 @@ public abstract class AbilityImpl implements Ability { return this; } + @Override + public Ability withFirstModeCost(Cost cost) { + this.modes.getMode().withCost(cost); + return this; + } + @Override public String getGameLogMessage(Game game) { if (game.isSimulation()) { diff --git a/Mage/src/main/java/mage/abilities/Mode.java b/Mage/src/main/java/mage/abilities/Mode.java index 427e18bcfaf..8c6e6747e4a 100644 --- a/Mage/src/main/java/mage/abilities/Mode.java +++ b/Mage/src/main/java/mage/abilities/Mode.java @@ -1,5 +1,6 @@ package mage.abilities; +import mage.abilities.costs.Cost; import mage.abilities.effects.Effect; import mage.abilities.effects.Effects; import mage.target.Target; @@ -17,6 +18,7 @@ public class Mode implements Serializable { protected final Targets targets; protected final Effects effects; protected String flavorWord; + protected Cost cost = null; /** * Optional Tag to distinguish this mode from others. * In the case of modes that players can only choose once, @@ -39,6 +41,7 @@ public class Mode implements Serializable { this.effects = mode.effects.copy(); this.flavorWord = mode.flavorWord; this.modeTag = mode.modeTag; + this.cost = mode.cost != null ? mode.cost.copy() : null; } public UUID setRandomId() { @@ -107,4 +110,13 @@ public class Mode implements Serializable { this.flavorWord = flavorWord; return this; } + + public Mode withCost(Cost cost) { + this.cost = cost; + return this; + } + + public Cost getCost() { + return cost; + } } diff --git a/Mage/src/main/java/mage/abilities/Modes.java b/Mage/src/main/java/mage/abilities/Modes.java index 4b0b117cefc..c612205e1fc 100644 --- a/Mage/src/main/java/mage/abilities/Modes.java +++ b/Mage/src/main/java/mage/abilities/Modes.java @@ -584,7 +584,14 @@ public class Modes extends LinkedHashMap implements Copyable sb.append("
"); for (Mode mode : this.values()) { - sb.append("&bull "); + if (mode.getCost() != null) { + // for Spree + sb.append("+ "); + sb.append(mode.getCost().getText()); + sb.append(" — "); + } else { + sb.append("&bull "); + } sb.append(mode.getEffects().getTextStartingUpperCase(mode)); sb.append("
"); } diff --git a/Mage/src/main/java/mage/abilities/keyword/SpreeAbility.java b/Mage/src/main/java/mage/abilities/keyword/SpreeAbility.java new file mode 100644 index 00000000000..c3f9672ac7c --- /dev/null +++ b/Mage/src/main/java/mage/abilities/keyword/SpreeAbility.java @@ -0,0 +1,28 @@ +package mage.abilities.keyword; + +import mage.abilities.StaticAbility; +import mage.cards.Card; +import mage.constants.Zone; + +/** + * @author TheElk801 + */ +public class SpreeAbility extends StaticAbility { + + public SpreeAbility(Card card) { + super(Zone.ALL, null); + this.setRuleVisible(false); + card.getSpellAbility().getModes().setChooseText("Spree (Choose one or more additional costs.)"); + card.getSpellAbility().getModes().setMinModes(1); + card.getSpellAbility().getModes().setMaxModes(Integer.MAX_VALUE); + } + + private SpreeAbility(final SpreeAbility ability) { + super(ability); + } + + @Override + public SpreeAbility copy() { + return new SpreeAbility(this); + } +} diff --git a/Mage/src/main/java/mage/game/stack/StackAbility.java b/Mage/src/main/java/mage/game/stack/StackAbility.java index 722e44b704d..5348235281b 100644 --- a/Mage/src/main/java/mage/game/stack/StackAbility.java +++ b/Mage/src/main/java/mage/game/stack/StackAbility.java @@ -414,14 +414,17 @@ public class StackAbility extends StackObjectImpl implements Ability { public void addManaCostsToPay(ManaCost manaCost) { // Do nothing } + @Override public Map getCostsTagMap() { return ability.getCostsTagMap(); } + @Override - public void setCostsTag(String tag, Object value){ + public void setCostsTag(String tag, Object value) { ability.setCostsTag(tag, value); } + @Override public AbilityType getAbilityType() { return ability.getAbilityType(); @@ -539,6 +542,11 @@ public class StackAbility extends StackObjectImpl implements Ability { throw new UnsupportedOperationException("Not supported."); } + @Override + public Ability withFirstModeCost(Cost cost) { + throw new UnsupportedOperationException("Not supported."); + } + @Override public boolean activateAlternateOrAdditionalCosts(MageObject sourceObject, boolean noMana, Player controller, Game game) { throw new UnsupportedOperationException("Not supported yet."); diff --git a/Utils/keywords.txt b/Utils/keywords.txt index e29a81de8e4..ea8d86bbc49 100644 --- a/Utils/keywords.txt +++ b/Utils/keywords.txt @@ -115,6 +115,7 @@ Soulbond|new| Soulshift|number| Skulk|new| Spectacle|card, cost| +Spree|card| Squad|new| Storm|new| Sunburst|new|