From 0e3252d256d658bdf6330ba2e784a8ca244e8064 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sun, 24 Apr 2022 12:03:25 -0400 Subject: [PATCH] Implementing Blitz mechanic (WIP) (#8835) * added blitz mechanic (mostly copy/paste of dash) * renamed class * reworked alt cost abilities, greatly reduced redundant code * updated text generation * removed all skips * added test for blitz * changed blitz implementation * [SNC] Implemented Tenacious Underdog --- .../src/mage/cards/c/CaldaiaGuardian.java | 2 +- .../src/mage/cards/c/CaldaiaStrongarm.java | 2 +- Mage.Sets/src/mage/cards/g/GirderGoons.java | 2 +- .../mage/cards/j/JaxisTheTroublemaker.java | 2 +- Mage.Sets/src/mage/cards/m/MayhemPatrol.java | 2 +- Mage.Sets/src/mage/cards/n/NightClubber.java | 2 +- Mage.Sets/src/mage/cards/p/PlasmaJockey.java | 2 +- .../src/mage/cards/p/PugnaciousPugilist.java | 2 +- .../src/mage/cards/r/RiveteersDecoy.java | 2 +- .../mage/cards/r/RiveteersRequisitioner.java | 2 +- .../src/mage/cards/t/TenaciousUnderdog.java | 82 +++++++ .../src/mage/cards/w/WorkshopWarchief.java | 2 +- .../src/mage/sets/NewCapennaCommander.java | 7 - .../src/mage/sets/StreetsOfNewCapenna.java | 8 +- .../cards/abilities/keywords/BlitzTest.java | 145 +++++++++++ .../cards/abilities/keywords/DashTest.java | 19 +- Mage/src/main/java/mage/ObjectColor.java | 18 +- .../condition/common/BlitzedCondition.java | 21 ++ .../condition/common/DashedCondition.java | 16 +- ...rnativeCost2.java => AlternativeCost.java} | 10 +- ...ost2Impl.java => AlternativeCostImpl.java} | 34 +-- .../costs/AlternativeCostSourceAbility.java | 23 +- .../costs/AlternativeSourceCosts.java | 26 +- .../costs/AlternativeSourceCostsImpl.java | 116 +++++++++ .../mage/abilities/keyword/BlitzAbility.java | 97 +++++++- .../mage/abilities/keyword/DashAbility.java | 177 ++------------ .../mage/abilities/keyword/EvokeAbility.java | 147 +---------- .../mage/abilities/keyword/MorphAbility.java | 229 +++--------------- .../mage/abilities/keyword/ProwlAbility.java | 131 +--------- .../main/java/mage/players/PlayerImpl.java | 12 +- Utils/keywords.txt | 2 +- 31 files changed, 620 insertions(+), 722 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/t/TenaciousUnderdog.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/BlitzTest.java create mode 100644 Mage/src/main/java/mage/abilities/condition/common/BlitzedCondition.java rename Mage/src/main/java/mage/abilities/costs/{AlternativeCost2.java => AlternativeCost.java} (91%) rename Mage/src/main/java/mage/abilities/costs/{AlternativeCost2Impl.java => AlternativeCostImpl.java} (74%) create mode 100644 Mage/src/main/java/mage/abilities/costs/AlternativeSourceCostsImpl.java diff --git a/Mage.Sets/src/mage/cards/c/CaldaiaGuardian.java b/Mage.Sets/src/mage/cards/c/CaldaiaGuardian.java index e5253c9968..497a0b66c2 100644 --- a/Mage.Sets/src/mage/cards/c/CaldaiaGuardian.java +++ b/Mage.Sets/src/mage/cards/c/CaldaiaGuardian.java @@ -42,7 +42,7 @@ public final class CaldaiaGuardian extends CardImpl { )); // Blitz {2}{G} - this.addAbility(new BlitzAbility("{2}{G}")); + this.addAbility(new BlitzAbility(this, "{2}{G}")); } private CaldaiaGuardian(final CaldaiaGuardian card) { diff --git a/Mage.Sets/src/mage/cards/c/CaldaiaStrongarm.java b/Mage.Sets/src/mage/cards/c/CaldaiaStrongarm.java index 630a5d480c..cb68fc55ad 100644 --- a/Mage.Sets/src/mage/cards/c/CaldaiaStrongarm.java +++ b/Mage.Sets/src/mage/cards/c/CaldaiaStrongarm.java @@ -35,7 +35,7 @@ public final class CaldaiaStrongarm extends CardImpl { this.addAbility(ability); // Blitz {3}{G} - this.addAbility(new BlitzAbility("{3}{G}")); + this.addAbility(new BlitzAbility(this, "{3}{G}")); } private CaldaiaStrongarm(final CaldaiaStrongarm card) { diff --git a/Mage.Sets/src/mage/cards/g/GirderGoons.java b/Mage.Sets/src/mage/cards/g/GirderGoons.java index 56cecd8048..6a3de2c90d 100644 --- a/Mage.Sets/src/mage/cards/g/GirderGoons.java +++ b/Mage.Sets/src/mage/cards/g/GirderGoons.java @@ -31,7 +31,7 @@ public final class GirderGoons extends CardImpl { ))); // Blitz {3}{B} - this.addAbility(new BlitzAbility("{3}{B}")); + this.addAbility(new BlitzAbility(this, "{3}{B}")); } private GirderGoons(final GirderGoons card) { diff --git a/Mage.Sets/src/mage/cards/j/JaxisTheTroublemaker.java b/Mage.Sets/src/mage/cards/j/JaxisTheTroublemaker.java index 4a571b2aef..256d9da78e 100644 --- a/Mage.Sets/src/mage/cards/j/JaxisTheTroublemaker.java +++ b/Mage.Sets/src/mage/cards/j/JaxisTheTroublemaker.java @@ -48,7 +48,7 @@ public final class JaxisTheTroublemaker extends CardImpl { this.addAbility(ability); // Blitz {1}{R} - this.addAbility(new BlitzAbility("{1}{R}")); + this.addAbility(new BlitzAbility(this, "{1}{R}")); } private JaxisTheTroublemaker(final JaxisTheTroublemaker card) { diff --git a/Mage.Sets/src/mage/cards/m/MayhemPatrol.java b/Mage.Sets/src/mage/cards/m/MayhemPatrol.java index 9240533fe6..567491701d 100644 --- a/Mage.Sets/src/mage/cards/m/MayhemPatrol.java +++ b/Mage.Sets/src/mage/cards/m/MayhemPatrol.java @@ -36,7 +36,7 @@ public final class MayhemPatrol extends CardImpl { this.addAbility(ability); // Blitz {1}{R} - this.addAbility(new BlitzAbility("{1}{R}")); + this.addAbility(new BlitzAbility(this, "{1}{R}")); } private MayhemPatrol(final MayhemPatrol card) { diff --git a/Mage.Sets/src/mage/cards/n/NightClubber.java b/Mage.Sets/src/mage/cards/n/NightClubber.java index f080f6560d..befc9a78fa 100644 --- a/Mage.Sets/src/mage/cards/n/NightClubber.java +++ b/Mage.Sets/src/mage/cards/n/NightClubber.java @@ -33,7 +33,7 @@ public final class NightClubber extends CardImpl { ))); // Blitz {2}{B} - this.addAbility(new BlitzAbility("{2}{B}")); + this.addAbility(new BlitzAbility(this, "{2}{B}")); } private NightClubber(final NightClubber card) { diff --git a/Mage.Sets/src/mage/cards/p/PlasmaJockey.java b/Mage.Sets/src/mage/cards/p/PlasmaJockey.java index de1466d6e0..2b60e54c2b 100644 --- a/Mage.Sets/src/mage/cards/p/PlasmaJockey.java +++ b/Mage.Sets/src/mage/cards/p/PlasmaJockey.java @@ -33,7 +33,7 @@ public final class PlasmaJockey extends CardImpl { this.addAbility(ability); // Blitz {2}{R} - this.addAbility(new BlitzAbility("{2}{R}")); + this.addAbility(new BlitzAbility(this, "{2}{R}")); } private PlasmaJockey(final PlasmaJockey card) { diff --git a/Mage.Sets/src/mage/cards/p/PugnaciousPugilist.java b/Mage.Sets/src/mage/cards/p/PugnaciousPugilist.java index dfab94109f..bbf99910a1 100644 --- a/Mage.Sets/src/mage/cards/p/PugnaciousPugilist.java +++ b/Mage.Sets/src/mage/cards/p/PugnaciousPugilist.java @@ -31,7 +31,7 @@ public final class PugnaciousPugilist extends CardImpl { ))); // Blitz {3}{R} - this.addAbility(new BlitzAbility("{3}{R}")); + this.addAbility(new BlitzAbility(this, "{3}{R}")); } private PugnaciousPugilist(final PugnaciousPugilist card) { diff --git a/Mage.Sets/src/mage/cards/r/RiveteersDecoy.java b/Mage.Sets/src/mage/cards/r/RiveteersDecoy.java index e5332f7725..fa5efd5f99 100644 --- a/Mage.Sets/src/mage/cards/r/RiveteersDecoy.java +++ b/Mage.Sets/src/mage/cards/r/RiveteersDecoy.java @@ -28,7 +28,7 @@ public final class RiveteersDecoy extends CardImpl { this.addAbility(new SimpleStaticAbility(new MustBeBlockedByAtLeastOneSourceEffect())); // Blitz {3}{G} - this.addAbility(new BlitzAbility("{3}{G}")); + this.addAbility(new BlitzAbility(this, "{3}{G}")); } private RiveteersDecoy(final RiveteersDecoy card) { diff --git a/Mage.Sets/src/mage/cards/r/RiveteersRequisitioner.java b/Mage.Sets/src/mage/cards/r/RiveteersRequisitioner.java index fadae84a66..984d5ee768 100644 --- a/Mage.Sets/src/mage/cards/r/RiveteersRequisitioner.java +++ b/Mage.Sets/src/mage/cards/r/RiveteersRequisitioner.java @@ -29,7 +29,7 @@ public final class RiveteersRequisitioner extends CardImpl { this.addAbility(new DiesSourceTriggeredAbility(new CreateTokenEffect(new TreasureToken()))); // Blitz {2}{R} - this.addAbility(new BlitzAbility("{2}{R}")); + this.addAbility(new BlitzAbility(this, "{2}{R}")); } private RiveteersRequisitioner(final RiveteersRequisitioner card) { diff --git a/Mage.Sets/src/mage/cards/t/TenaciousUnderdog.java b/Mage.Sets/src/mage/cards/t/TenaciousUnderdog.java new file mode 100644 index 0000000000..d8b5872454 --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/TenaciousUnderdog.java @@ -0,0 +1,82 @@ +package mage.cards.t; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.costs.common.PayLifeCost; +import mage.abilities.effects.AsThoughEffectImpl; +import mage.abilities.keyword.BlitzAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.game.Game; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class TenaciousUnderdog extends CardImpl { + + public TenaciousUnderdog(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{B}"); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.WARRIOR); + this.power = new MageInt(3); + this.toughness = new MageInt(2); + + // Blitz—{2}{B}{B}, Pay 2 life. + Ability ability = new BlitzAbility(this, "{2}{B}{B}"); + ability.addCost(new PayLifeCost(2)); + this.addAbility(ability); + + // You may cast Tenacious Underdog from your graveyard using its blitz ability. + this.addAbility(new SimpleStaticAbility(Zone.ALL, new TenaciousUnderdogEffect())); + } + + private TenaciousUnderdog(final TenaciousUnderdog card) { + super(card); + } + + @Override + public TenaciousUnderdog copy() { + return new TenaciousUnderdog(this); + } +} + +class TenaciousUnderdogEffect extends AsThoughEffectImpl { + + TenaciousUnderdogEffect() { + super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.EndOfGame, Outcome.PutCreatureInPlay); + staticText = "You may cast {this} from your graveyard"; + } + + private TenaciousUnderdogEffect(final TenaciousUnderdogEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + return true; + } + + @Override + public TenaciousUnderdogEffect copy() { + return new TenaciousUnderdogEffect(this); + } + + @Override + public boolean applies(UUID objectId, Ability affectedAbility, Ability source, Game game, UUID playerId) { + return objectId.equals(source.getSourceId()) + && source.isControlledBy(playerId) + && affectedAbility instanceof BlitzAbility + && game.getState().getZone(source.getSourceId()) == Zone.GRAVEYARD + && game.getCard(source.getSourceId()) != null; + } + + @Override + public boolean applies(UUID sourceId, Ability source, UUID affectedControllerId, Game game) { + return false; + } +} diff --git a/Mage.Sets/src/mage/cards/w/WorkshopWarchief.java b/Mage.Sets/src/mage/cards/w/WorkshopWarchief.java index d0e86492bb..a81d518757 100644 --- a/Mage.Sets/src/mage/cards/w/WorkshopWarchief.java +++ b/Mage.Sets/src/mage/cards/w/WorkshopWarchief.java @@ -38,7 +38,7 @@ public final class WorkshopWarchief extends CardImpl { this.addAbility(new DiesSourceTriggeredAbility(new CreateTokenEffect(new RhinoWarriorToken()))); // Blitz {4}{G}{G} - this.addAbility(new BlitzAbility("{4}{G}{G}")); + this.addAbility(new BlitzAbility(this, "{4}{G}{G}")); } private WorkshopWarchief(final WorkshopWarchief card) { diff --git a/Mage.Sets/src/mage/sets/NewCapennaCommander.java b/Mage.Sets/src/mage/sets/NewCapennaCommander.java index 28fc8545f2..8a0393ed70 100644 --- a/Mage.Sets/src/mage/sets/NewCapennaCommander.java +++ b/Mage.Sets/src/mage/sets/NewCapennaCommander.java @@ -4,16 +4,11 @@ import mage.cards.ExpansionSet; import mage.constants.Rarity; import mage.constants.SetType; -import java.util.Arrays; -import java.util.List; - /** * @author TheElk801 */ public final class NewCapennaCommander extends ExpansionSet { - private static final List unfinished = Arrays.asList("Caldaia Guardian", "Henzie \"Toolbox\" Torre", "Mezzio Mugger", "Wave of Rats"); - private static final NewCapennaCommander instance = new NewCapennaCommander(); public static NewCapennaCommander getInstance() { @@ -302,7 +297,5 @@ public final class NewCapennaCommander extends ExpansionSet { cards.add(new SetCardInfo("Writ of Return", 42, Rarity.RARE, mage.cards.w.WritOfReturn.class)); cards.add(new SetCardInfo("Zndrsplt's Judgment", 240, Rarity.RARE, mage.cards.z.ZndrspltsJudgment.class)); cards.add(new SetCardInfo("Zurzoth, Chaos Rider", 278, Rarity.RARE, mage.cards.z.ZurzothChaosRider.class)); - - cards.removeIf(setCardInfo -> unfinished.contains(setCardInfo.getName())); // remove when shield counters are implemented } } diff --git a/Mage.Sets/src/mage/sets/StreetsOfNewCapenna.java b/Mage.Sets/src/mage/sets/StreetsOfNewCapenna.java index 977e530596..2c4a4fcaea 100644 --- a/Mage.Sets/src/mage/sets/StreetsOfNewCapenna.java +++ b/Mage.Sets/src/mage/sets/StreetsOfNewCapenna.java @@ -4,16 +4,11 @@ import mage.cards.ExpansionSet; import mage.constants.Rarity; import mage.constants.SetType; -import java.util.Arrays; -import java.util.List; - /** * @author TheElk801 */ public final class StreetsOfNewCapenna extends ExpansionSet { - private static final List unfinished = Arrays.asList("Caldaia Strongarm", "Girder Goons", "Jaxis, the Troublemaker", "Mayhem Patrol", "Night Clubber", "Plasma Jockey", "Pugnacious Pugilist", "Riveteers Decoy", "Riveteers Requisitioner", "Tenacious Underdog", "Workshop Warchief", "Ziatora's Envoy"); - private static final StreetsOfNewCapenna instance = new StreetsOfNewCapenna(); public static StreetsOfNewCapenna getInstance() { @@ -260,6 +255,7 @@ public final class StreetsOfNewCapenna extends ExpansionSet { cards.add(new SetCardInfo("Tainted Indulgence", 227, Rarity.UNCOMMON, mage.cards.t.TaintedIndulgence.class)); cards.add(new SetCardInfo("Take to the Streets", 158, Rarity.UNCOMMON, mage.cards.t.TakeToTheStreets.class)); cards.add(new SetCardInfo("Tavern Swindler", 96, Rarity.UNCOMMON, mage.cards.t.TavernSwindler.class)); + cards.add(new SetCardInfo("Tenacious Underdog", 97, Rarity.RARE, mage.cards.t.TenaciousUnderdog.class)); cards.add(new SetCardInfo("Titan of Industry", 159, Rarity.MYTHIC, mage.cards.t.TitanOfIndustry.class)); cards.add(new SetCardInfo("Toluz, Clever Conductor", 228, Rarity.RARE, mage.cards.t.ToluzCleverConductor.class)); cards.add(new SetCardInfo("Topiary Stomper", 160, Rarity.RARE, mage.cards.t.TopiaryStomper.class)); @@ -288,7 +284,5 @@ public final class StreetsOfNewCapenna extends ExpansionSet { cards.add(new SetCardInfo("Xander's Lounge", 260, Rarity.RARE, mage.cards.x.XandersLounge.class)); cards.add(new SetCardInfo("Ziatora's Proving Ground", 261, Rarity.RARE, mage.cards.z.ZiatorasProvingGround.class)); cards.add(new SetCardInfo("Ziatora, the Incinerator", 231, Rarity.MYTHIC, mage.cards.z.ZiatoraTheIncinerator.class)); - - cards.removeIf(setCardInfo -> unfinished.contains(setCardInfo.getName())); // remove when shield counters are implemented } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/BlitzTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/BlitzTest.java new file mode 100644 index 0000000000..9bad1199a9 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/BlitzTest.java @@ -0,0 +1,145 @@ +package org.mage.test.cards.abilities.keywords; + +import mage.abilities.Ability; +import mage.abilities.keyword.HasteAbility; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.game.permanent.Permanent; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author TheElk801 + */ +public class BlitzTest extends CardTestPlayerBase { + + private static final String withBlitz = " with Blitz"; + private static final String decoy = "Riveteers Decoy"; + private static final String underdog = "Tenacious Underdog"; + private static final String will = "Yawgmoth's Will"; + + private void assertBlitzed(String cardName, boolean isBlitzed) { + assertPermanentCount(playerA, cardName, 1); + Permanent permanent = getPermanent(cardName); + Assert.assertEquals( + "Permanent should " + (isBlitzed ? "" : "not ") + "have haste", isBlitzed, + permanent.hasAbility(HasteAbility.getInstance(), currentGame) + ); + Assert.assertEquals( + "Permanent should " + (isBlitzed ? "" : "not ") + "have card draw trigger", isBlitzed, + permanent + .getAbilities(currentGame) + .stream() + .map(Ability::getRule) + .anyMatch("When this creature dies, draw a card."::equals) + ); + } + + @Test + public void testBlitz() { + addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); + addCard(Zone.HAND, playerA, decoy); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, decoy + withBlitz); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, decoy, 1); + assertBlitzed(decoy, true); + } + + @Test + public void testBlitzSacrificed() { + addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); + addCard(Zone.HAND, playerA, decoy); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, decoy + withBlitz); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, decoy, 0); + assertGraveyardCount(playerA, decoy, 1); + assertHandCount(playerA, 1); + } + + @Test + public void testNoBlitz() { + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); + addCard(Zone.HAND, playerA, decoy); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, decoy); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, decoy, 1); + assertBlitzed(decoy, false); + } + + @Test + public void testTenaciousUnderdogNormal() { + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 2); + addCard(Zone.GRAVEYARD, playerA, underdog); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, underdog); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + try { + execute(); + } catch (AssertionError e) { + Assert.assertEquals( + "Shouldn't be able to cast normally from graveyard", + "Missing CAST/ACTIVATE def for turn 1, step PRECOMBAT_MAIN, PlayerA\n" + + "Can't find available command - activate:Cast Tenacious Underdog " + + "(use checkPlayableAbility for \"non available\" checks)", e.getMessage() + ); + } + + assertGraveyardCount(playerA, underdog, 1); + assertLife(playerA, 20); + } + + @Test + public void testTenaciousUnderdogBlitz() { + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 4); + addCard(Zone.GRAVEYARD, playerA, underdog); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, underdog + withBlitz); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + assertAllCommandsUsed(); + + assertBlitzed(underdog, true); + assertLife(playerA, 20 - 2); + } + + @Test + public void testTenaciousUnderdogYawgmothsWill() { + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 5); + addCard(Zone.HAND, playerA, will); + addCard(Zone.GRAVEYARD, playerA, underdog); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, will); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, underdog); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + assertAllCommandsUsed(); + + assertBlitzed(underdog, false); + assertLife(playerA, 20); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/DashTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/DashTest.java index f4e6f61b7f..0691e76bbe 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/DashTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/DashTest.java @@ -8,7 +8,6 @@ import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; /** - * * @author BetaSteward */ public class DashTest extends CardTestPlayerBase { @@ -31,7 +30,6 @@ public class DashTest extends CardTestPlayerBase { * may cast this spell for its dash cost. If you do, it gains haste, and * it's returned from the battlefield to its owner's hand at the beginning * of the next end step.) - * */ @Test public void testDash() { @@ -133,4 +131,21 @@ public class DashTest extends CardTestPlayerBase { assertPermanentCount(playerA, "Warbringer", 2); assertHandCount(playerA, "Warbringer", 0); } + + @Test + public void testRegularCostReduction() { + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + addCard(Zone.BATTLEFIELD, playerA, "Ruby Medallion"); + addCard(Zone.HAND, playerA, "Screamreach Brawler"); + + setStrictChooseMode(true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Screamreach Brawler"); + setChoice(playerA, true); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertPermanentCount(playerA, "Ruby Medallion", 1); + assertPermanentCount(playerA, "Screamreach Brawler", 1); + assertHandCount(playerA, "Screamreach Brawler", 0); + } } diff --git a/Mage/src/main/java/mage/ObjectColor.java b/Mage/src/main/java/mage/ObjectColor.java index 8f41df79ad..4d9292feb0 100644 --- a/Mage/src/main/java/mage/ObjectColor.java +++ b/Mage/src/main/java/mage/ObjectColor.java @@ -1,15 +1,15 @@ package mage; +import mage.constants.ColoredManaSymbol; +import mage.util.Copyable; + import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; -import mage.constants.ColoredManaSymbol; -import mage.util.Copyable; - public class ObjectColor implements Serializable, Copyable, Comparable { public static final ObjectColor WHITE = new ObjectColor("W"); @@ -182,13 +182,13 @@ public class ObjectColor implements Serializable, Copyable, Compara } public void setColor(ObjectColor color) { - this.setBlack(color.isBlack()); - this.setBlue(color.isBlue()); - this.setGreen(color.isGreen()); - this.setRed(color.isRed()); - this.setWhite(color.isWhite()); + this.setBlack(color != null && color.isBlack()); + this.setBlue(color != null && color.isBlue()); + this.setGreen(color != null && color.isGreen()); + this.setRed(color != null && color.isRed()); + this.setWhite(color != null && color.isWhite()); - this.setGold(color.isGold()); + this.setGold(color != null && color.isGold()); } public void addColor(ObjectColor color) { diff --git a/Mage/src/main/java/mage/abilities/condition/common/BlitzedCondition.java b/Mage/src/main/java/mage/abilities/condition/common/BlitzedCondition.java new file mode 100644 index 0000000000..220410a074 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/condition/common/BlitzedCondition.java @@ -0,0 +1,21 @@ +package mage.abilities.condition.common; + +import mage.abilities.Ability; +import mage.abilities.condition.Condition; +import mage.abilities.keyword.BlitzAbility; +import mage.game.Game; + +import java.util.List; + +/** + * @author TheElk801 + */ +public enum BlitzedCondition implements Condition { + instance; + + @Override + public boolean apply(Game game, Ability source) { + List blitzActivations = (List) game.getState().getValue(BlitzAbility.BLITZ_ACTIVATION_VALUE_KEY + source.getSourceId()); + return blitzActivations != null && blitzActivations.contains(game.getState().getZoneChangeCounter(source.getSourceId())); + } +} diff --git a/Mage/src/main/java/mage/abilities/condition/common/DashedCondition.java b/Mage/src/main/java/mage/abilities/condition/common/DashedCondition.java index 64343d69f7..553aef07dd 100644 --- a/Mage/src/main/java/mage/abilities/condition/common/DashedCondition.java +++ b/Mage/src/main/java/mage/abilities/condition/common/DashedCondition.java @@ -1,4 +1,3 @@ - package mage.abilities.condition.common; import mage.abilities.Ability; @@ -6,24 +5,21 @@ import mage.abilities.condition.Condition; import mage.abilities.keyword.DashAbility; import mage.cards.Card; import mage.game.Game; +import mage.util.CardUtil; /** * @author LevelX2 */ - public enum DashedCondition implements Condition { - instance; @Override public boolean apply(Game game, Ability source) { Card card = game.getCard(source.getSourceId()); - if (card != null) { - return card.getAbilities(game).stream() - .filter(DashAbility.class::isInstance) - .anyMatch(d -> ((DashAbility) d).isActivated(source, game)); - - } - return false; + return card != null + && CardUtil.castStream(card + .getAbilities(game) + .stream(), DashAbility.class) + .anyMatch(ability -> ability.isActivated(source, game)); } } diff --git a/Mage/src/main/java/mage/abilities/costs/AlternativeCost2.java b/Mage/src/main/java/mage/abilities/costs/AlternativeCost.java similarity index 91% rename from Mage/src/main/java/mage/abilities/costs/AlternativeCost2.java rename to Mage/src/main/java/mage/abilities/costs/AlternativeCost.java index 767c3d3c1b..c951ac81a3 100644 --- a/Mage/src/main/java/mage/abilities/costs/AlternativeCost2.java +++ b/Mage/src/main/java/mage/abilities/costs/AlternativeCost.java @@ -9,8 +9,8 @@ import mage.game.Game; * * @author LevelX2 */ -public interface AlternativeCost2 extends Cost { - +public interface AlternativeCost extends Cost { + String getName(); /** @@ -41,13 +41,11 @@ public interface AlternativeCost2 extends Cost { /** * If the player intends to pay the alternate cost, the cost will be activated - * */ void activate(); /** - * Reset the activate - * + * Reset the activate */ void reset(); @@ -61,4 +59,6 @@ public interface AlternativeCost2 extends Cost { Cost getCost(); + @Override + AlternativeCost copy(); } diff --git a/Mage/src/main/java/mage/abilities/costs/AlternativeCost2Impl.java b/Mage/src/main/java/mage/abilities/costs/AlternativeCostImpl.java similarity index 74% rename from Mage/src/main/java/mage/abilities/costs/AlternativeCost2Impl.java rename to Mage/src/main/java/mage/abilities/costs/AlternativeCostImpl.java index 5b5b81c4bf..bfbb86d893 100644 --- a/Mage/src/main/java/mage/abilities/costs/AlternativeCost2Impl.java +++ b/Mage/src/main/java/mage/abilities/costs/AlternativeCostImpl.java @@ -1,43 +1,38 @@ - package mage.abilities.costs; +import mage.abilities.costs.mana.ManaCost; import mage.game.Game; /** * Alternative costs * - * @author LevelX2 - * * @param + * @author LevelX2 */ -public class AlternativeCost2Impl> extends CostsImpl implements AlternativeCost2 { +public class AlternativeCostImpl> extends CostsImpl implements AlternativeCost { protected String name; protected String reminderText; - protected String delimiter; + protected boolean isMana; protected boolean activated; - public AlternativeCost2Impl(String name, String reminderText, Cost cost) { - this(name, " ", reminderText, cost); - } - - public AlternativeCost2Impl(String name, String delimiter, String reminderText, Cost cost) { + public AlternativeCostImpl(String name, String reminderText, Cost cost) { this.activated = false; this.name = name; - this.delimiter = delimiter; + this.isMana = cost instanceof ManaCost; if (reminderText != null) { - this.reminderText = "" + reminderText + ""; + this.reminderText = "(" + reminderText + ")"; } this.add(cost); } - public AlternativeCost2Impl(final AlternativeCost2Impl cost) { + public AlternativeCostImpl(final AlternativeCostImpl cost) { super(cost); this.name = cost.name; this.reminderText = cost.reminderText; this.activated = cost.activated; - this.delimiter = cost.delimiter; + this.isMana = cost.isMana; } @Override @@ -57,7 +52,7 @@ public class AlternativeCost2Impl> extends Cos if (onlyCost) { return getText(); } else { - return (name != null ? name : "") + (delimiter != null ? delimiter : "") + getText(); + return (name != null ? name : "") + (isMana ? " " : "—") + getText() + (isMana ? "" : '.'); } } @@ -80,7 +75,7 @@ public class AlternativeCost2Impl> extends Cos * message. * * @param position - if there are multiple costs, it's the postion the cost - * is set (starting with 0) + * is set (starting with 0) * @return */ @Override @@ -92,7 +87,6 @@ public class AlternativeCost2Impl> extends Cos /** * If the player intends to pay the cost, the cost will be activated - * */ @Override public void activate() { @@ -101,7 +95,6 @@ public class AlternativeCost2Impl> extends Cos /** * Reset the activate and count information - * */ @Override public void reset() { @@ -120,8 +113,8 @@ public class AlternativeCost2Impl> extends Cos } @Override - public AlternativeCost2Impl copy() { - return new AlternativeCost2Impl(this); + public AlternativeCostImpl copy() { + return new AlternativeCostImpl<>(this); } @Override @@ -131,5 +124,4 @@ public class AlternativeCost2Impl> extends Cos } return null; } - } diff --git a/Mage/src/main/java/mage/abilities/costs/AlternativeCostSourceAbility.java b/Mage/src/main/java/mage/abilities/costs/AlternativeCostSourceAbility.java index 00f2913ab7..c76f40b12a 100644 --- a/Mage/src/main/java/mage/abilities/costs/AlternativeCostSourceAbility.java +++ b/Mage/src/main/java/mage/abilities/costs/AlternativeCostSourceAbility.java @@ -24,7 +24,7 @@ public class AlternativeCostSourceAbility extends StaticAbility implements Alter private static final String ALTERNATIVE_COST_ACTIVATION_KEY = "AlternativeCostActivated"; - private Costs alternateCosts = new CostsImpl<>(); + private Costs alternateCosts = new CostsImpl<>(); protected Condition condition; protected String rule; protected FilterCard filter; @@ -88,15 +88,15 @@ public class AlternativeCostSourceAbility extends StaticAbility implements Alter @Override public void addCost(Cost cost) { - AlternativeCost2 alternativeCost = convertToAlternativeCost(cost); + AlternativeCost alternativeCost = convertToAlternativeCost(cost); if (alternativeCost != null) { this.alternateCosts.add(alternativeCost); } } - private AlternativeCost2 convertToAlternativeCost(Cost cost) { + private AlternativeCost convertToAlternativeCost(Cost cost) { //return cost != null ? new AlternativeCost2Impl(null, cost.getText(), cost) : null; - return cost != null ? new AlternativeCost2Impl(null, "", "", cost) : null; + return cost != null ? new AlternativeCostImpl(null, "", cost) : null; } @Override @@ -123,7 +123,7 @@ public class AlternativeCostSourceAbility extends StaticAbility implements Alter } Player player = game.getPlayer(ability.getControllerId()); if (player != null) { - Costs alternativeCostsToCheck; + Costs alternativeCostsToCheck; if (dynamicCost != null) { alternativeCostsToCheck = new CostsImpl<>(); alternativeCostsToCheck.add(convertToAlternativeCost(dynamicCost.getCost(ability, game))); @@ -149,7 +149,7 @@ public class AlternativeCostSourceAbility extends StaticAbility implements Alter if (!onlyMana) { ability.getCosts().clear(); } - for (AlternativeCost2 alternateCost : alternativeCostsToCheck) { + for (AlternativeCost alternateCost : alternativeCostsToCheck) { alternateCost.activate(); for (Iterator it = ((Costs) alternateCost).iterator(); it.hasNext(); ) { Cost costDetailed = (Cost) it.next(); @@ -207,14 +207,14 @@ public class AlternativeCostSourceAbility extends StaticAbility implements Alter @Override public boolean isActivated(Ability source, Game game) { - Costs alternativeCostsToCheck; + Costs alternativeCostsToCheck; if (dynamicCost != null) { alternativeCostsToCheck = new CostsImpl<>(); alternativeCostsToCheck.add(convertToAlternativeCost(dynamicCost.getCost(source, game))); } else { alternativeCostsToCheck = this.alternateCosts; } - for (AlternativeCost2 cost : alternativeCostsToCheck) { + for (AlternativeCost cost : alternativeCostsToCheck) { if (cost.isActivated(game)) { return true; } @@ -227,6 +227,11 @@ public class AlternativeCostSourceAbility extends StaticAbility implements Alter return alternateCosts.isEmpty() ? " without paying its mana costs" : " using alternative casting costs"; } + @Override + public void resetCost() { + + } + @Override public String getRule() { if (rule != null) { @@ -245,7 +250,7 @@ public class AlternativeCostSourceAbility extends StaticAbility implements Alter } int numberCosts = 0; String remarkText = ""; - for (AlternativeCost2 alternativeCost : alternateCosts) { + for (AlternativeCost alternativeCost : alternateCosts) { if (numberCosts == 0) { if (alternativeCost.getCost() instanceof ManaCost) { sb.append("pay "); diff --git a/Mage/src/main/java/mage/abilities/costs/AlternativeSourceCosts.java b/Mage/src/main/java/mage/abilities/costs/AlternativeSourceCosts.java index 474bf1bfea..14a4737aec 100644 --- a/Mage/src/main/java/mage/abilities/costs/AlternativeSourceCosts.java +++ b/Mage/src/main/java/mage/abilities/costs/AlternativeSourceCosts.java @@ -1,4 +1,3 @@ - package mage.abilities.costs; import mage.abilities.Ability; @@ -6,7 +5,7 @@ import mage.game.Game; /** * Interface for abilities that add alternative costs to the source. - * + *

* Example of such additional source costs: {@link mage.abilities.keyword.KickerAbility} * * @author LevelX2 @@ -15,35 +14,38 @@ public interface AlternativeSourceCosts { /** * Ask the player if they want to use the alternative costs - * + * * @param ability ability the alternative cost is activated for * @param game - * @return + * @return */ boolean askToActivateAlternativeCosts(Ability ability, Game game); - + /** * Is the alternative spell cost currently available - * + * * @param source spell ability the alternative costs can be paid for * @param game - * @return + * @return */ boolean isAvailable(Ability source, Game game); - + /** * Was the alternative cost activated + * * @param game * @param source * @return */ boolean isActivated(Ability source, Game game); - + /** * Suffix string to use for game log + * * @param game - * @return + * @return */ - String getCastMessageSuffix(Game game); - + String getCastMessageSuffix(Game game); + + void resetCost(); } \ No newline at end of file diff --git a/Mage/src/main/java/mage/abilities/costs/AlternativeSourceCostsImpl.java b/Mage/src/main/java/mage/abilities/costs/AlternativeSourceCostsImpl.java new file mode 100644 index 0000000000..b3c92c197f --- /dev/null +++ b/Mage/src/main/java/mage/abilities/costs/AlternativeSourceCostsImpl.java @@ -0,0 +1,116 @@ +package mage.abilities.costs; + +import mage.abilities.Ability; +import mage.abilities.SpellAbility; +import mage.abilities.StaticAbility; +import mage.abilities.costs.mana.ManaCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.cards.Card; +import mage.constants.Outcome; +import mage.constants.Zone; +import mage.game.Game; +import mage.players.Player; + +import java.util.Iterator; + +/** + * @author TheElk801 + */ +public abstract class AlternativeSourceCostsImpl extends StaticAbility implements AlternativeSourceCosts { + + protected final AlternativeCost alternativeCost; + protected final String reminderText; + private int zoneChangeCounter = 0; + + protected AlternativeSourceCostsImpl(String name, String reminderText, String manaString) { + this(name, reminderText, new ManaCostsImpl<>(manaString)); + } + + protected AlternativeSourceCostsImpl(String name, String reminderText, Cost cost) { + super(Zone.ALL, null); + this.name = name; + this.reminderText = reminderText; + this.alternativeCost = new AlternativeCostImpl<>(name, reminderText, cost); + } + + protected AlternativeSourceCostsImpl(final AlternativeSourceCostsImpl ability) { + super(ability); + this.alternativeCost = ability.alternativeCost.copy(); + this.reminderText = ability.reminderText; + this.zoneChangeCounter = ability.zoneChangeCounter; + } + + @Override + public boolean askToActivateAlternativeCosts(Ability ability, Game game) { + if (ability instanceof SpellAbility) { + handleActivatingAlternativeCosts(ability, game); + } + return isActivated(ability, game); + } + + protected boolean handleActivatingAlternativeCosts(Ability ability, Game game) { + Player player = game.getPlayer(ability.getControllerId()); + if (player == null) { + return false; + } + this.resetCost(); + if (!alternativeCost.canPay(ability, this, player.getId(), game) + || !player.chooseUse(Outcome.Benefit, "Cast this for its " + this.name + " cost? (" + alternativeCost.getText(true) + ')', ability, game)) { + return false; + } + alternativeCost.activate(); + if (zoneChangeCounter == 0) { + Card card = game.getCard(getSourceId()); + if (card != null) { + zoneChangeCounter = card.getZoneChangeCounter(game); + } else { + throw new IllegalArgumentException("source card not found"); + } + } + ability.getManaCostsToPay().clear(); + ability.getCosts().clear(); + for (Iterator it = ((Costs) alternativeCost).iterator(); it.hasNext(); ) { + Cost cost = it.next(); + if (cost instanceof ManaCost) { + ability.getManaCostsToPay().add((ManaCost) cost.copy()); + } else { + ability.getCosts().add(cost.copy()); + } + } + return true; + } + + @Override + public boolean isActivated(Ability ability, Game game) { + Card card = game.getCard(sourceId); + if (card != null && card.getZoneChangeCounter(game) <= zoneChangeCounter + 1) { + return alternativeCost.isActivated(game); + } + return false; + } + + @Override + public Costs getCosts() { + return (Costs) alternativeCost; + } + + @Override + public String getRule() { + return alternativeCost.getText(false) + ' ' + alternativeCost.getReminderText(); + } + + @Override + public void resetCost() { + alternativeCost.reset(); + this.zoneChangeCounter = 0; + } + + @Override + public boolean isAvailable(Ability source, Game game) { + return true; + } + + public String getCastMessageSuffix(Game game) { + return alternativeCost.getCastSuffixMessage(0); + } +} diff --git a/Mage/src/main/java/mage/abilities/keyword/BlitzAbility.java b/Mage/src/main/java/mage/abilities/keyword/BlitzAbility.java index 7c39c566ef..8a68796219 100644 --- a/Mage/src/main/java/mage/abilities/keyword/BlitzAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/BlitzAbility.java @@ -1,16 +1,49 @@ package mage.abilities.keyword; -import mage.abilities.StaticAbility; -import mage.constants.Zone; +import mage.abilities.Ability; +import mage.abilities.SpellAbility; +import mage.abilities.common.DiesSourceTriggeredAbility; +import mage.abilities.common.EntersBattlefieldAbility; +import mage.abilities.common.delayed.AtTheBeginOfNextEndStepDelayedTriggeredAbility; +import mage.abilities.condition.common.BlitzedCondition; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.SacrificeTargetEffect; +import mage.abilities.effects.common.continuous.GainAbilitySourceEffect; +import mage.cards.Card; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.constants.SpellAbilityType; +import mage.game.Game; +import mage.target.targetpointer.FixedTarget; + +import java.util.ArrayList; +import java.util.List; /** * @author TheElk801 */ -public class BlitzAbility extends StaticAbility { +public class BlitzAbility extends SpellAbility { - public BlitzAbility(String manaString) { - // TODO: Implement this - super(Zone.ALL, null); + public static final String BLITZ_ACTIVATION_VALUE_KEY = "blitzActivation"; + protected static final String KEYWORD = "Blitz"; + protected static final String REMINDER_TEXT = "If you cast this spell for its blitz cost, it gains haste " + + "and \"When this creature dies, draw a card.\" Sacrifice it at the beginning of the next end step."; + + public BlitzAbility(Card card, String manaString) { + super(new ManaCostsImpl<>(manaString), card.getName() + " with Blitz"); + this.spellAbilityType = SpellAbilityType.BASE_ALTERNATE; + Ability ability = new EntersBattlefieldAbility( + new GainAbilitySourceEffect(HasteAbility.getInstance(), Duration.Custom, false), + BlitzedCondition.instance, "", "" + ); + ability.addEffect(new GainAbilitySourceEffect(new DiesSourceTriggeredAbility( + new DrawCardSourceControllerEffect(1) + ).setTriggerPhrase("When this creature dies, "))); + ability.addEffect(new BlitzAddDelayedTriggeredAbilityEffect()); + ability.setRuleVisible(false); + addSubAbility(ability); } private BlitzAbility(final BlitzAbility ability) { @@ -21,4 +54,56 @@ public class BlitzAbility extends StaticAbility { public BlitzAbility copy() { return new BlitzAbility(this); } + + @Override + public String getRule() { + return "Blitz"; + } + + @Override + public boolean activate(Game game, boolean noMana) { + if (!super.activate(game, noMana)) { + return false; + } + Object obj = game.getState().getValue(BLITZ_ACTIVATION_VALUE_KEY + getSourceId()); + List blitzActivations; + if (obj != null) { + blitzActivations = (List) obj; + } else { + blitzActivations = new ArrayList<>(); + game.getState().setValue(BLITZ_ACTIVATION_VALUE_KEY + getSourceId(), blitzActivations); + } + blitzActivations.add(game.getState().getZoneChangeCounter(getSourceId())); + return true; + } +} + +class BlitzAddDelayedTriggeredAbilityEffect extends OneShotEffect { + + BlitzAddDelayedTriggeredAbilityEffect() { + super(Outcome.Benefit); + } + + private BlitzAddDelayedTriggeredAbilityEffect(final BlitzAddDelayedTriggeredAbilityEffect effect) { + super(effect); + } + + @Override + public BlitzAddDelayedTriggeredAbilityEffect copy() { + return new BlitzAddDelayedTriggeredAbilityEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + if (game.getPermanentEntering(source.getSourceId()) == null) { + return false; + } + // init target pointer now because the Blitzed creature will only be returned from battlefield zone (now in entering state so zone change counter is not raised yet) + game.addDelayedTriggeredAbility(new AtTheBeginOfNextEndStepDelayedTriggeredAbility( + new SacrificeTargetEffect() + .setText("sacrifice the blitzed creature") + .setTargetPointer(new FixedTarget(source.getSourceId(), game.getState().getZoneChangeCounter(source.getSourceId()) + 1)) + ), source); + return true; + } } diff --git a/Mage/src/main/java/mage/abilities/keyword/DashAbility.java b/Mage/src/main/java/mage/abilities/keyword/DashAbility.java index 5116b8ad62..a7527cf57e 100644 --- a/Mage/src/main/java/mage/abilities/keyword/DashAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/DashAbility.java @@ -1,195 +1,55 @@ package mage.abilities.keyword; import mage.abilities.Ability; -import mage.abilities.DelayedTriggeredAbility; -import mage.abilities.SpellAbility; -import mage.abilities.StaticAbility; import mage.abilities.common.EntersBattlefieldAbility; import mage.abilities.common.delayed.AtTheBeginOfNextEndStepDelayedTriggeredAbility; import mage.abilities.condition.common.DashedCondition; -import mage.abilities.costs.*; -import mage.abilities.costs.mana.ManaCostsImpl; -import mage.abilities.decorator.ConditionalOneShotEffect; +import mage.abilities.costs.AlternativeSourceCostsImpl; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.ReturnToHandTargetEffect; import mage.abilities.effects.common.continuous.GainAbilitySourceEffect; -import mage.cards.Card; import mage.constants.Duration; import mage.constants.Outcome; -import mage.constants.Zone; import mage.game.Game; -import mage.players.Player; import mage.target.targetpointer.FixedTarget; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; - /** * @author LevelX2 */ -public class DashAbility extends StaticAbility implements AlternativeSourceCosts { +public class DashAbility extends AlternativeSourceCostsImpl { protected static final String KEYWORD = "Dash"; - protected static final String REMINDER_TEXT = "(You may cast this spell for its dash cost. " + protected static final String REMINDER_TEXT = "You may cast this spell for its dash cost. " + "If you do, it gains haste, and it's returned from the battlefield to its owner's " - + "hand at the beginning of the next end step.)"; - - protected List alternativeSourceCosts = new LinkedList<>(); - - // needed to check activation status, if card changes zone after casting it - private int zoneChangeCounter = 0; + + "hand at the beginning of the next end step."; public DashAbility(String manaString) { - super(Zone.ALL, null); - name = KEYWORD; - this.addDashCost(manaString); + super(KEYWORD, REMINDER_TEXT, manaString); Ability ability = new EntersBattlefieldAbility( new GainAbilitySourceEffect(HasteAbility.getInstance(), Duration.Custom, false), DashedCondition.instance, "", ""); ability.addEffect(new DashAddDelayedTriggeredAbilityEffect()); ability.setRuleVisible(false); addSubAbility(ability); - } private DashAbility(final DashAbility ability) { super(ability); - this.alternativeSourceCosts.addAll(ability.alternativeSourceCosts); - this.zoneChangeCounter = ability.zoneChangeCounter; } @Override public DashAbility copy() { return new DashAbility(this); } - - public final AlternativeCost2 addDashCost(String manaString) { - AlternativeCost2 evokeCost = new AlternativeCost2Impl(KEYWORD, REMINDER_TEXT, new ManaCostsImpl(manaString)); - alternativeSourceCosts.add(evokeCost); - return evokeCost; - } - - public void resetDash() { - for (AlternativeCost2 cost : alternativeSourceCosts) { - cost.reset(); - } - zoneChangeCounter = 0; - } - - @Override - public boolean isActivated(Ability ability, Game game) { - Card card = game.getCard(sourceId); - if (card != null - && card.getZoneChangeCounter(game) <= zoneChangeCounter + 1) { - for (AlternativeCost2 cost : alternativeSourceCosts) { - if (cost.isActivated(game)) { - return true; - } - } - } - return false; - } - - @Override - public boolean isAvailable(Ability source, Game game) { - return true; - } - - @Override - public boolean askToActivateAlternativeCosts(Ability ability, Game game) { - if (ability instanceof SpellAbility) { - // we must use the controller of the ability here IE: Hedonist's Trove (play from not own hand when you aren't the owner) - Player player = game.getPlayer(ability.getControllerId()); - if (player != null) { - this.resetDash(); - for (AlternativeCost2 dashCost : alternativeSourceCosts) { - if (dashCost.canPay(ability, this, player.getId(), game) - && player.chooseUse(Outcome.Benefit, KEYWORD - + " the creature for " + dashCost.getText(true) + " ?", ability, game)) { - activateDash(dashCost, game); - ability.getManaCostsToPay().clear(); - ability.getCosts().clear(); - for (Iterator it = ((Costs) dashCost).iterator(); it.hasNext(); ) { - Cost cost = (Cost) it.next(); - if (cost instanceof ManaCostsImpl) { - ability.getManaCostsToPay().add((ManaCostsImpl) cost.copy()); - } else { - ability.getCosts().add(cost.copy()); - } - } - } - } - } - } - return isActivated(ability, game); - } - - private void activateDash(AlternativeCost2 cost, Game game) { - cost.activate(); - // remember zone change counter - if (zoneChangeCounter == 0) { - Card card = game.getCard(getSourceId()); - if (card != null) { - zoneChangeCounter = card.getZoneChangeCounter(game); - } else { - throw new IllegalArgumentException("Dash source card not found"); - } - } - } - - @Override - public String getRule() { - StringBuilder sb = new StringBuilder(); - int numberCosts = 0; - String remarkText = ""; - for (AlternativeCost2 dashCost : alternativeSourceCosts) { - if (numberCosts == 0) { - sb.append(dashCost.getText(false)); - remarkText = dashCost.getReminderText(); - } else { - sb.append(" and/or ").append(dashCost.getText(true)); - } - ++numberCosts; - } - if (numberCosts == 1) { - sb.append(' ').append(remarkText); - } - - return sb.toString(); - } - - @Override - public String getCastMessageSuffix(Game game) { - StringBuilder sb = new StringBuilder(); - int position = 0; - for (AlternativeCost2 cost : alternativeSourceCosts) { - if (cost.isActivated(game)) { - sb.append(cost.getCastSuffixMessage(position)); - ++position; - } - } - return sb.toString(); - } - - @Override - public Costs getCosts() { - Costs alterCosts = new CostsImpl<>(); - for (AlternativeCost2 aCost : alternativeSourceCosts) { - alterCosts.add(aCost.getCost()); - } - return alterCosts; - } } class DashAddDelayedTriggeredAbilityEffect extends OneShotEffect { - public DashAddDelayedTriggeredAbilityEffect() { + DashAddDelayedTriggeredAbilityEffect() { super(Outcome.Benefit); - this.staticText = "return the dashed creature from the battlefield to its owner's hand"; } - public DashAddDelayedTriggeredAbilityEffect(final DashAddDelayedTriggeredAbilityEffect effect) { + private DashAddDelayedTriggeredAbilityEffect(final DashAddDelayedTriggeredAbilityEffect effect) { super(effect); } @@ -200,20 +60,15 @@ class DashAddDelayedTriggeredAbilityEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { - if (game.getPermanentEntering(source.getSourceId()) != null) { - OneShotEffect returnToHandEffect = new ReturnToHandTargetEffect(); - ConditionalOneShotEffect mustBeOnBattlefieldToReturn = new ConditionalOneShotEffect(returnToHandEffect, DashAddDelayedTriggeredAbilityEffect::check); - mustBeOnBattlefieldToReturn.setText("return the dashed creature from the battlefield to its owner's hand"); - // init target pointer now because the dashed creature will only be returned from battlefield zone (now in entering state so zone change counter is not raised yet) - mustBeOnBattlefieldToReturn.setTargetPointer(new FixedTarget(source.getSourceId(), game.getState().getZoneChangeCounter(source.getSourceId()) + 1)); - DelayedTriggeredAbility delayedAbility = new AtTheBeginOfNextEndStepDelayedTriggeredAbility(mustBeOnBattlefieldToReturn); - game.addDelayedTriggeredAbility(delayedAbility, source); - return true; + if (game.getPermanentEntering(source.getSourceId()) == null) { + return false; } - return false; - } - - static boolean check(Game game, Ability source) { - return game.getState().getZoneChangeCounter(source.getSourceId()) == source.getSourceObjectZoneChangeCounter() + 1; + // init target pointer now because the dashed creature will only be returned from battlefield zone (now in entering state so zone change counter is not raised yet) + game.addDelayedTriggeredAbility(new AtTheBeginOfNextEndStepDelayedTriggeredAbility( + new ReturnToHandTargetEffect() + .setText("return the dashed creature from the battlefield to its owner's hand") + .setTargetPointer(new FixedTarget(source.getSourceId(), game.getState().getZoneChangeCounter(source.getSourceId()) + 1)) + ), source); + return true; } } diff --git a/Mage/src/main/java/mage/abilities/keyword/EvokeAbility.java b/Mage/src/main/java/mage/abilities/keyword/EvokeAbility.java index ebc021e70f..805cd1e950 100644 --- a/Mage/src/main/java/mage/abilities/keyword/EvokeAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/EvokeAbility.java @@ -1,46 +1,29 @@ package mage.abilities.keyword; import mage.abilities.Ability; -import mage.abilities.SpellAbility; -import mage.abilities.StaticAbility; import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.condition.common.EvokedCondition; -import mage.abilities.costs.*; +import mage.abilities.costs.AlternativeSourceCostsImpl; +import mage.abilities.costs.Cost; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; import mage.abilities.effects.common.SacrificeSourceEffect; -import mage.cards.Card; -import mage.constants.Outcome; -import mage.constants.Zone; -import mage.game.Game; -import mage.players.Player; - -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; /** * @author LevelX2 */ -public class EvokeAbility extends StaticAbility implements AlternativeSourceCosts { +public class EvokeAbility extends AlternativeSourceCostsImpl { protected static final String EVOKE_KEYWORD = "Evoke"; - protected static final String REMINDER_TEXT = "(You may cast this spell for its evoke cost. " - + "If you do, it's sacrificed when it enters the battlefield.)"; - - protected List evokeCosts = new LinkedList<>(); - - // needed to check activation status, if card changes zone after casting it - private int zoneChangeCounter = 0; + protected static final String REMINDER_TEXT = "You may cast this spell for its evoke cost. " + + "If you do, it's sacrificed when it enters the battlefield."; public EvokeAbility(String manaString) { this(new ManaCostsImpl<>(manaString)); } public EvokeAbility(Cost cost) { - super(Zone.ALL, null); - name = EVOKE_KEYWORD; - this.addEvokeCost(cost); + super(EVOKE_KEYWORD, REMINDER_TEXT, cost); Ability ability = new ConditionalInterveningIfTriggeredAbility( new EntersBattlefieldTriggeredAbility(new SacrificeSourceEffect()), EvokedCondition.instance, "Sacrifice {this} when it enters the battlefield and was evoked."); @@ -50,128 +33,10 @@ public class EvokeAbility extends StaticAbility implements AlternativeSourceCost private EvokeAbility(final EvokeAbility ability) { super(ability); - this.evokeCosts.addAll(ability.evokeCosts); - this.zoneChangeCounter = ability.zoneChangeCounter; } @Override public EvokeAbility copy() { return new EvokeAbility(this); } - - public final AlternativeCost2 addEvokeCost(Cost cost) { - AlternativeCost2 evokeCost = new AlternativeCost2Impl<>(EVOKE_KEYWORD, REMINDER_TEXT, cost); - evokeCosts.add(evokeCost); - return evokeCost; - } - - public void resetEvoke() { - for (AlternativeCost2 cost : evokeCosts) { - cost.reset(); - } - zoneChangeCounter = 0; - } - - @Override - public boolean isActivated(Ability ability, Game game) { - Card card = game.getCard(sourceId); - if (card != null - && card.getZoneChangeCounter(game) <= zoneChangeCounter + 1) { - for (AlternativeCost2 cost : evokeCosts) { - if (cost.isActivated(game)) { - return true; - } - } - } - return false; - } - - @Override - public boolean isAvailable(Ability source, Game game) { - return true; - } - - @Override - public boolean askToActivateAlternativeCosts(Ability ability, Game game) { - if (ability instanceof SpellAbility) { - // we must use the controller of the ability here IE: Hedonist's Trove (play from not own hand when you aren't the owner) - Player player = game.getPlayer(ability.getControllerId()); - if (player != null) { - this.resetEvoke(); - for (AlternativeCost2 evokeCost : evokeCosts) { - if (evokeCost.canPay(ability, this, player.getId(), game) - && player.chooseUse(Outcome.Benefit, new StringBuilder(EVOKE_KEYWORD).append(" the creature for ").append(evokeCost.getText(true)).append(" ?").toString(), ability, game)) { - activateEvoke(evokeCost, game); - ability.getManaCostsToPay().clear(); - ability.getCosts().clear(); - for (Iterator it = ((Costs) evokeCost).iterator(); it.hasNext();) { - Cost cost = (Cost) it.next(); - if (cost instanceof ManaCostsImpl) { - ability.getManaCostsToPay().add((ManaCostsImpl) cost.copy()); - } else { - ability.getCosts().add(cost.copy()); - } - } - } - } - } - } - return isActivated(ability, game); - } - - private void activateEvoke(AlternativeCost2 cost, Game game) { - cost.activate(); - // remember zone change counter - if (zoneChangeCounter == 0) { - Card card = game.getCard(getSourceId()); - if (card != null) { - zoneChangeCounter = card.getZoneChangeCounter(game); - } else { - throw new IllegalArgumentException("Evoke source card not found"); - } - } - } - - @Override - public String getRule() { - StringBuilder sb = new StringBuilder(); - int numberCosts = 0; - String remarkText = ""; - for (AlternativeCost2 evokeCost : evokeCosts) { - if (numberCosts == 0) { - sb.append(evokeCost.getText(false)); - remarkText = evokeCost.getReminderText(); - } else { - sb.append(" and/or ").append(evokeCost.getText(true)); - } - ++numberCosts; - } - if (numberCosts == 1) { - sb.append(' ').append(remarkText); - } - - return sb.toString(); - } - - @Override - public String getCastMessageSuffix(Game game) { - StringBuilder sb = new StringBuilder(); - int position = 0; - for (AlternativeCost2 cost : evokeCosts) { - if (cost.isActivated(game)) { - sb.append(cost.getCastSuffixMessage(position)); - ++position; - } - } - return sb.toString(); - } - - @Override - public Costs getCosts() { - Costs alterCosts = new CostsImpl<>(); - for (AlternativeCost2 aCost : evokeCosts) { - alterCosts.add(aCost.getCost()); - } - return alterCosts; - } } diff --git a/Mage/src/main/java/mage/abilities/keyword/MorphAbility.java b/Mage/src/main/java/mage/abilities/keyword/MorphAbility.java index 5c73d1f4c0..3ce967fba1 100644 --- a/Mage/src/main/java/mage/abilities/keyword/MorphAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/MorphAbility.java @@ -1,42 +1,33 @@ package mage.abilities.keyword; -import java.util.Iterator; import mage.MageObject; import mage.ObjectColor; import mage.abilities.Ability; -import mage.abilities.StaticAbility; import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.costs.AlternativeCost2Impl; -import mage.abilities.costs.AlternativeSourceCosts; +import mage.abilities.costs.AlternativeSourceCostsImpl; import mage.abilities.costs.Cost; import mage.abilities.costs.Costs; import mage.abilities.costs.CostsImpl; import mage.abilities.costs.mana.GenericManaCost; import mage.abilities.costs.mana.ManaCost; -import mage.abilities.costs.mana.ManaCosts; import mage.abilities.effects.common.continuous.BecomesFaceDownCreatureEffect; import mage.abilities.effects.common.continuous.BecomesFaceDownCreatureEffect.FaceDownType; -import mage.cards.Card; -import mage.constants.AbilityType; import mage.constants.CardType; -import mage.constants.Outcome; import mage.constants.Rarity; -import mage.constants.Zone; import mage.game.Game; import mage.game.permanent.Permanent; import mage.game.stack.Spell; -import mage.players.Player; /** * 702.36. Morph - * + *

* 702.36a Morph is a static ability that functions in any zone from which you * could play the card it’s on, and the morph effect works any time the card is * face down. "Morph [cost]" means "You may cast this card as a 2/2 face-down * creature, with no text, no name, no subtypes, and no mana cost by paying {3} * rather than paying its mana cost." (See rule 707, "Face-Down Spells and * Permanents.") - * + *

* 702.36b To cast a card using its morph ability, turn it face down. It becomes * a 2/2 face-down creature card, with no text, no name, no subtypes, and no * mana cost. Any effects or prohibitions that would apply to casting a card @@ -50,9 +41,9 @@ import mage.players.Player; * spell resolves, it enters the battlefield with the same characteristics the * spell had. The morph effect applies to the face-down object wherever it is, * and it ends when the permanent is turned face up. # - * + *

* 702.36c You can't cast a card face down if it doesn't have morph. - * + *

* 702.36d If you have priority, you may turn a face-down permanent you control * face up. This is a special action; it doesn't use the stack (see rule 115). * To do this, show all players what the permanent’s morph cost would be if it @@ -62,223 +53,84 @@ import mage.players.Player; * characteristics. Any abilities relating to the permanent entering the * battlefield don’t trigger when it’s turned face up and don’t have any effect, * because the permanent has already entered the battlefield. - * + *

* 702.36e See rule 707, "Face-Down Spells and Permanents," for more information * on how to cast cards with morph. * * @author LevelX2 */ -public class MorphAbility extends StaticAbility implements AlternativeSourceCosts { +public class MorphAbility extends AlternativeSourceCostsImpl { protected static final String ABILITY_KEYWORD = "Morph"; protected static final String ABILITY_KEYWORD_MEGA = "Megamorph"; - protected static final String REMINDER_TEXT = "(You may cast this card face down as a " - + "2/2 creature for {3}. Turn it face up any time for its morph cost.)"; - protected static final String REMINDER_TEXT_MEGA = "(You may cast this card face down " + protected static final String REMINDER_TEXT = "You may cast this card face down as a " + + "2/2 creature for {3}. Turn it face up any time for its morph cost."; + protected static final String REMINDER_TEXT_MEGA = "You may cast this card face down " + "as a 2/2 creature for {3}. Turn it face up any time for its megamorph " - + "cost and put a +1/+1 counter on it.)"; - protected String ruleText; - protected AlternativeCost2Impl alternateCosts = new AlternativeCost2Impl( - ABILITY_KEYWORD, REMINDER_TEXT, new GenericManaCost(3)); + + "cost and put a +1/+1 counter on it."; protected Costs morphCosts; // needed to check activation status, if card changes zone after casting it - private int zoneChangeCounter = 0; - private boolean megamorph; + private final boolean megamorph; public MorphAbility(Cost morphCost) { - this(createCosts(morphCost)); + this(morphCost, false); } public MorphAbility(Cost morphCost, boolean megamorph) { - this(createCosts(morphCost), megamorph); - } - - public MorphAbility(Costs morphCosts) { - this(morphCosts, false); - } - - public MorphAbility(Costs morphCosts, boolean megamorph) { - super(Zone.HAND, null); - this.morphCosts = morphCosts; + super(megamorph ? ABILITY_KEYWORD_MEGA : ABILITY_KEYWORD, megamorph ? REMINDER_TEXT_MEGA : REMINDER_TEXT, new GenericManaCost(3)); + this.morphCosts = new CostsImpl<>(); + this.morphCosts.add(morphCost); this.megamorph = megamorph; this.setWorksFaceDown(true); - StringBuilder sb = new StringBuilder(); - if (megamorph) { - sb.append(ABILITY_KEYWORD_MEGA).append(' '); - } else { - sb.append(ABILITY_KEYWORD).append(' '); - } - name = ABILITY_KEYWORD; - for (Cost cost : morphCosts) { - if (!(cost instanceof ManaCosts)) { - sb.setLength(sb.length() - 1); - sb.append("—"); - break; - } - } - sb.append(morphCosts.getText()); - if (!(morphCosts.get(morphCosts.size() - 1) instanceof ManaCosts)) { - sb.append('.'); - } - sb.append(' '); - if (megamorph) { - sb.append(REMINDER_TEXT_MEGA); - } else { - sb.append(REMINDER_TEXT); - } - - ruleText = sb.toString(); - - Ability ability = new SimpleStaticAbility(Zone.BATTLEFIELD, new BecomesFaceDownCreatureEffect( + Ability ability = new SimpleStaticAbility(new BecomesFaceDownCreatureEffect( morphCosts, (megamorph ? FaceDownType.MEGAMORPHED : FaceDownType.MORPHED))); ability.setWorksFaceDown(true); ability.setRuleVisible(false); addSubAbility(ability); - } public MorphAbility(final MorphAbility ability) { super(ability); - this.zoneChangeCounter = ability.zoneChangeCounter; - this.ruleText = ability.ruleText; - this.alternateCosts = ability.alternateCosts.copy(); this.morphCosts = ability.morphCosts; // can't be changed this.megamorph = ability.megamorph; } - private static Costs createCosts(Cost cost) { - Costs costs = new CostsImpl<>(); - costs.add(cost); - return costs; - } - @Override public MorphAbility copy() { return new MorphAbility(this); } - public void resetMorph() { - alternateCosts.reset(); - zoneChangeCounter = 0; + @Override + public boolean askToActivateAlternativeCosts(Ability ability, Game game) { + switch (ability.getAbilityType()) { + case SPELL: + Spell spell = game.getStack().getSpell(ability.getId()); + if (spell != null) { + spell.setFaceDown(true, game); + if (handleActivatingAlternativeCosts(ability, game)) { + game.getState().setValue("MorphAbility" + ability.getSourceId(), "activated"); + spell.getColor(game).setColor(null); + game.getState().getCreateMageObjectAttribute(spell.getCard(), game).getSubtype().clear(); + } else { + spell.setFaceDown(false, game); + } + } + break; + case PLAY_LAND: + handleActivatingAlternativeCosts(ability, game); + } + return isActivated(ability, game); } public Costs getMorphCosts() { return morphCosts; } - @Override - public boolean isActivated(Ability ability, Game game) { - Card card = game.getCard(sourceId); - if (card != null - && card.getZoneChangeCounter(game) <= zoneChangeCounter + 1) { - return alternateCosts.isActivated(game); - } - return false; - } - - @Override - public boolean isAvailable(Ability source, Game game) { - return true; - } - - @Override - public boolean askToActivateAlternativeCosts(Ability ability, Game game) { - if (ability.getAbilityType() == AbilityType.SPELL) { - Player player = game.getPlayer(ability.getControllerId()); - Spell spell = game.getStack().getSpell(ability.getId()); - if (player != null - && spell != null) { - this.resetMorph(); - spell.setFaceDown(true, game); // so only the back is visible - if (alternateCosts.canPay(ability, this, ability.getControllerId(), game)) { - if (player.chooseUse(Outcome.Benefit, "Cast this card as a 2/2 " - + "face-down creature for " + getCosts().getText() + " ?", ability, game)) { - game.getState().setValue("MorphAbility" - + ability.getSourceId(), "activated"); // Gift of Doom - activateMorph(game); - // change mana costs - ability.getManaCostsToPay().clear(); - ability.getCosts().clear(); - for (Iterator it = this.alternateCosts.iterator(); it.hasNext();) { - Cost cost = (Cost) it.next(); - if (cost instanceof ManaCost) { - ability.getManaCostsToPay().add((ManaCost) cost.copy()); - } else { - ability.getCosts().add(cost.copy()); - } - } - // change spell colors and subtype *TODO probably this needs to be done by continuous effect (while on the stack) - ObjectColor spellColor = spell.getColor(game); - spellColor.setBlack(false); - spellColor.setRed(false); - spellColor.setGreen(false); - spellColor.setWhite(false); - spellColor.setBlue(false); - game.getState().getCreateMageObjectAttribute(spell.getCard(), game).getSubtype().clear(); - } else { - spell.setFaceDown(false, game); - } - } - } - } - if (ability.getAbilityType() == AbilityType.PLAY_LAND) { - Player player = game.getPlayer(ability.getControllerId()); - if (player != null) { - this.resetMorph(); - if (alternateCosts.canPay(ability, this, ability.getControllerId(), game)) { - if (player.chooseUse(Outcome.Benefit, "Cast this card as a 2/2 " - + "face-down creature for " + getCosts().getText() + " ?", ability, game)) { - activateMorph(game); - // change mana costs - ability.getManaCostsToPay().clear(); - ability.getCosts().clear(); - for (Iterator it = this.alternateCosts.iterator(); it.hasNext();) { - Cost cost = (Cost) it.next(); - if (cost instanceof ManaCost) { - ability.getManaCostsToPay().add((ManaCost) cost.copy()); - } else { - ability.getCosts().add(cost.copy()); - } - } - } - } - } - } - return isActivated(ability, game); - } - - private void activateMorph(Game game) { - alternateCosts.activate(); - // remember zone change counter - if (zoneChangeCounter == 0) { - Card card = game.getCard(getSourceId()); - if (card != null) { - zoneChangeCounter = card.getZoneChangeCounter(game); - } else { - throw new IllegalArgumentException("Morph source card not found"); - } - } - } - - @Override - public String getRule(boolean all) { - return getRule(); - } - @Override public String getRule() { - return ruleText; - } - - @Override - public String getCastMessageSuffix(Game game) { - return alternateCosts.getCastSuffixMessage(0); - } - - @Override - @SuppressWarnings({"unchecked"}) - public Costs getCosts() { - return alternateCosts; + boolean isMana = morphCosts.get(0) instanceof ManaCost; + return alternativeCost.getName() + (isMana ? " " : "—") + + morphCosts.getText() + (isMana ? ' ' : ". ") + alternativeCost.getReminderText(); } public static void setPermanentToFaceDownCreature(MageObject mageObject, Game game) { @@ -296,6 +148,5 @@ public class MorphAbility extends StaticAbility implements AlternativeSourceCost ((Permanent) mageObject).setExpansionSetCode(""); ((Permanent) mageObject).setRarity(Rarity.SPECIAL); } - } } diff --git a/Mage/src/main/java/mage/abilities/keyword/ProwlAbility.java b/Mage/src/main/java/mage/abilities/keyword/ProwlAbility.java index d145363f49..7973850eee 100644 --- a/Mage/src/main/java/mage/abilities/keyword/ProwlAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/ProwlAbility.java @@ -1,23 +1,13 @@ package mage.abilities.keyword; import mage.abilities.Ability; -import mage.abilities.SpellAbility; -import mage.abilities.StaticAbility; import mage.abilities.condition.common.ProwlCondition; -import mage.abilities.costs.*; -import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.costs.AlternativeSourceCostsImpl; import mage.abilities.hint.common.ProwlHint; import mage.cards.Card; -import mage.constants.Outcome; -import mage.constants.Zone; import mage.game.Game; -import mage.players.Player; import mage.watchers.common.ProwlWatcher; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; - /** * 702.74. Prowl # *

@@ -30,26 +20,21 @@ import java.util.List; * * @author LevelX2 */ -public class ProwlAbility extends StaticAbility implements AlternativeSourceCosts { +public class ProwlAbility extends AlternativeSourceCostsImpl { private static final String PROWL_KEYWORD = "Prowl"; - private final List prowlCosts = new LinkedList<>(); - private String reminderText; + private static final String reminderText = "You may cast this for its prowl cost if you dealt combat damage to a " + + "player this turn with a creature that shared a creature type with {this}"; public ProwlAbility(Card card, String manaString) { - super(Zone.ALL, null); + super(PROWL_KEYWORD, reminderText, manaString); this.setRuleAtTheTop(true); - this.name = PROWL_KEYWORD; - this.setReminderText(card); - this.addProwlCost(manaString); this.addWatcher(new ProwlWatcher()); this.addHint(ProwlHint.instance); } - public ProwlAbility(final ProwlAbility ability) { + private ProwlAbility(final ProwlAbility ability) { super(ability); - this.prowlCosts.addAll(ability.prowlCosts); - this.reminderText = ability.reminderText; } @Override @@ -57,112 +42,8 @@ public class ProwlAbility extends StaticAbility implements AlternativeSourceCost return new ProwlAbility(this); } - public final AlternativeCost2 addProwlCost(String manaString) { - AlternativeCost2 prowlCost = new AlternativeCost2Impl(PROWL_KEYWORD, - reminderText, new ManaCostsImpl(manaString)); - prowlCosts.add(prowlCost); - return prowlCost; - } - - public void resetProwl() { - for (AlternativeCost2 cost : prowlCosts) { - cost.reset(); - } - } - - @Override - public boolean isActivated(Ability ability, Game game) { - for (AlternativeCost2 cost : prowlCosts) { - if (cost.isActivated(game)) { - return true; - } - } - return false; - } - @Override public boolean isAvailable(Ability source, Game game) { return ProwlCondition.instance.apply(game, source); } - - @Override - public boolean askToActivateAlternativeCosts(Ability ability, Game game) { - if (ability instanceof SpellAbility) { - Player player = game.getPlayer(ability.getControllerId()); - if (player == null) { - return false; - } - if (ProwlCondition.instance.apply(game, ability)) { - this.resetProwl(); - for (AlternativeCost2 prowlCost : prowlCosts) { - if (prowlCost.canPay(ability, this, ability.getControllerId(), game) - && player.chooseUse(Outcome.Benefit, "Cast for " - + PROWL_KEYWORD + " cost " + prowlCost.getText(true) - + " ?", ability, game)) { - prowlCost.activate(); - ability.getManaCostsToPay().clear(); - ability.getCosts().clear(); - for (Iterator it = ((Costs) prowlCost).iterator(); it.hasNext();) { - Cost cost = (Cost) it.next(); - if (cost instanceof ManaCostsImpl) { - ability.getManaCostsToPay().add((ManaCostsImpl) cost.copy()); - } else { - ability.getCosts().add(cost.copy()); - } - } - } - } - } - } - return isActivated(ability, game); - } - - @Override - public String getRule() { - StringBuilder sb = new StringBuilder(); - int numberCosts = 0; - String remarkText = ""; - for (AlternativeCost2 prowlCost : prowlCosts) { - if (numberCosts == 0) { - sb.append(prowlCost.getText(false)); - remarkText = prowlCost.getReminderText(); - } else { - sb.append(" and/or ").append(prowlCost.getText(true)); - } - ++numberCosts; - } - if (numberCosts == 1) { - sb.append(' ').append(remarkText); - } - - return sb.toString(); - } - - @Override - public String getCastMessageSuffix(Game game) { - StringBuilder sb = new StringBuilder(); - int position = 0; - for (AlternativeCost2 cost : prowlCosts) { - if (cost.isActivated(game)) { - sb.append(cost.getCastSuffixMessage(position)); - ++position; - } - } - return sb.toString(); - } - - private void setReminderText(Card card) { - reminderText - = "(You may cast this for its prowl cost if you dealt combat damage to a " - + "player this turn with a creature that shared a creature type with {this})"; - } - - @Override - public Costs getCosts() { - Costs alterCosts = new CostsImpl<>(); - for (AlternativeCost2 aCost : prowlCosts) { - alterCosts.add(aCost.getCost()); - } - return alterCosts; - } } diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index 24d1c18935..d07e39ccf3 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -3607,9 +3607,9 @@ public abstract class PlayerImpl implements Player, Serializable { ManaCostsImpl manaCosts = new ManaCostsImpl(); for (Cost cost : alternateSourceCostsAbility.getCosts()) { // AlternativeCost2 replaced by real cost on activate, so getPlayable need to extract that costs here - if (cost instanceof AlternativeCost2) { - if (((AlternativeCost2) cost).getCost() instanceof ManaCost) { - manaCosts.add((ManaCost) ((AlternativeCost2) cost).getCost()); + if (cost instanceof AlternativeCost) { + if (((AlternativeCost) cost).getCost() instanceof ManaCost) { + manaCosts.add((ManaCost) ((AlternativeCost) cost).getCost()); } } else { if (cost instanceof ManaCost) { @@ -3656,9 +3656,9 @@ public abstract class PlayerImpl implements Player, Serializable { ManaCostsImpl manaCosts = new ManaCostsImpl(); for (Cost cost : ((Ability) alternateSourceCosts).getCosts()) { // AlternativeCost2 replaced by real cost on activate, so getPlayable need to extract that costs here - if (cost instanceof AlternativeCost2) { - if (((AlternativeCost2) cost).getCost() instanceof ManaCost) { - manaCosts.add((ManaCost) ((AlternativeCost2) cost).getCost()); + if (cost instanceof AlternativeCost) { + if (((AlternativeCost) cost).getCost() instanceof ManaCost) { + manaCosts.add((ManaCost) ((AlternativeCost) cost).getCost()); } } else { if (cost instanceof ManaCost) { diff --git a/Utils/keywords.txt b/Utils/keywords.txt index a84f9269e1..eee0993202 100644 --- a/Utils/keywords.txt +++ b/Utils/keywords.txt @@ -6,7 +6,7 @@ Assist|new| Basic landcycling|cost| Battle cry|new| Bestow|card, manaString| -Blitz|manaString| +Blitz|card, manaString| Bloodthirst|number| Bushido|number| Buyback|manaString|