[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
This commit is contained in:
Evan Kranzler 2024-03-31 12:11:34 -04:00 committed by GitHub
parent fa67b0450f
commit ba20e97b71
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 740 additions and 2 deletions

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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));

View file

@ -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);
}
}

View file

@ -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.

View file

@ -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()) {

View file

@ -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;
}
}

View file

@ -584,7 +584,14 @@ public class Modes extends LinkedHashMap<UUID, Mode> implements Copyable<Modes>
sb.append("<br>");
for (Mode mode : this.values()) {
sb.append("&bull ");
if (mode.getCost() != null) {
// for Spree
sb.append("+ ");
sb.append(mode.getCost().getText());
sb.append(" &mdash; ");
} else {
sb.append("&bull ");
}
sb.append(mode.getEffects().getTextStartingUpperCase(mode));
sb.append("<br>");
}

View file

@ -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 <i>(Choose one or more additional costs.)</i>");
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);
}
}

View file

@ -414,14 +414,17 @@ public class StackAbility extends StackObjectImpl implements Ability {
public void addManaCostsToPay(ManaCost manaCost) {
// Do nothing
}
@Override
public Map<String, Object> 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.");

View file

@ -115,6 +115,7 @@ Soulbond|new|
Soulshift|number|
Skulk|new|
Spectacle|card, cost|
Spree|card|
Squad|new|
Storm|new|
Sunburst|new|