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
This commit is contained in:
Evan Kranzler 2022-04-24 12:03:25 -04:00 committed by GitHub
parent 76daf4bd5a
commit 0e3252d256
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 620 additions and 722 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<ObjectColor>, Comparable<ObjectColor> {
public static final ObjectColor WHITE = new ObjectColor("W");
@ -182,13 +182,13 @@ public class ObjectColor implements Serializable, Copyable<ObjectColor>, 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) {

View file

@ -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<Integer> blitzActivations = (List<Integer>) game.getState().getValue(BlitzAbility.BLITZ_ACTIVATION_VALUE_KEY + source.getSourceId());
return blitzActivations != null && blitzActivations.contains(game.getState().getZoneChangeCounter(source.getSourceId()));
}
}

View file

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

View file

@ -9,7 +9,7 @@ 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
*
*/
void reset();
@ -61,4 +59,6 @@ public interface AlternativeCost2 extends Cost {
Cost getCost();
@Override
AlternativeCost copy();
}

View file

@ -1,43 +1,38 @@
package mage.abilities.costs;
import mage.abilities.costs.mana.ManaCost;
import mage.game.Game;
/**
* Alternative costs
*
* @author LevelX2
*
* @param <T>
* @author LevelX2
*/
public class AlternativeCost2Impl<T extends AlternativeCost2Impl<T>> extends CostsImpl<Cost> implements AlternativeCost2 {
public class AlternativeCostImpl<T extends AlternativeCostImpl<T>> extends CostsImpl<Cost> 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 = "<i>" + reminderText + "</i>";
this.reminderText = "<i>(" + reminderText + ")</i>";
}
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<T extends AlternativeCost2Impl<T>> extends Cos
if (onlyCost) {
return getText();
} else {
return (name != null ? name : "") + (delimiter != null ? delimiter : "") + getText();
return (name != null ? name : "") + (isMana ? " " : "&mdash;") + getText() + (isMana ? "" : '.');
}
}
@ -92,7 +87,6 @@ public class AlternativeCost2Impl<T extends AlternativeCost2Impl<T>> 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<T extends AlternativeCost2Impl<T>> extends Cos
/**
* Reset the activate and count information
*
*/
@Override
public void reset() {
@ -120,8 +113,8 @@ public class AlternativeCost2Impl<T extends AlternativeCost2Impl<T>> 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<T extends AlternativeCost2Impl<T>> extends Cos
}
return null;
}
}

View file

@ -24,7 +24,7 @@ public class AlternativeCostSourceAbility extends StaticAbility implements Alter
private static final String ALTERNATIVE_COST_ACTIVATION_KEY = "AlternativeCostActivated";
private Costs<AlternativeCost2> alternateCosts = new CostsImpl<>();
private Costs<AlternativeCost> 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<AlternativeCost2> alternativeCostsToCheck;
Costs<AlternativeCost> 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<AlternativeCost2> alternativeCostsToCheck;
Costs<AlternativeCost> 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 ");

View file

@ -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.
*
* <p>
* Example of such additional source costs: {@link mage.abilities.keyword.KickerAbility}
*
* @author LevelX2
@ -33,6 +32,7 @@ public interface AlternativeSourceCosts {
/**
* Was the alternative cost activated
*
* @param game
* @param source
* @return
@ -41,9 +41,11 @@ public interface AlternativeSourceCosts {
/**
* Suffix string to use for game log
*
* @param game
* @return
*/
String getCastMessageSuffix(Game game);
void resetCost();
}

View file

@ -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<Cost> it = ((Costs<Cost>) 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<Cost> getCosts() {
return (Costs<Cost>) 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);
}
}

View file

@ -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<Integer> blitzActivations;
if (obj != null) {
blitzActivations = (List<Integer>) 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;
}
}

View file

@ -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<AlternativeCost2> 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<Cost> getCosts() {
Costs<Cost> 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;
}
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;
}
}

View file

@ -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<AlternativeCost2> 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<Cost> getCosts() {
Costs<Cost> alterCosts = new CostsImpl<>();
for (AlternativeCost2 aCost : evokeCosts) {
alterCosts.add(aCost.getCost());
}
return alterCosts;
}
}

View file

@ -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
*
* <p>
* 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.")
*
* <p>
* 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. #
*
* <p>
* 702.36c You can't cast a card face down if it doesn't have morph.
*
* <p>
* 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.
*
* <p>
* 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 = "<i>(You may cast this card face down as a "
+ "2/2 creature for {3}. Turn it face up any time for its morph cost.)</i>";
protected static final String REMINDER_TEXT_MEGA = "<i>(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.)</i>";
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<Cost> 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<Cost> morphCosts) {
this(morphCosts, false);
}
public MorphAbility(Costs<Cost> 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("&mdash;");
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<Cost> createCosts(Cost cost) {
Costs<Cost> 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<Cost> 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<Cost> getCosts() {
return alternateCosts;
boolean isMana = morphCosts.get(0) instanceof ManaCost;
return alternativeCost.getName() + (isMana ? " " : "&mdash;") +
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);
}
}
}

View file

@ -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 #
* <p>
@ -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<AlternativeCost2> 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<Cost> getCosts() {
Costs<Cost> alterCosts = new CostsImpl<>();
for (AlternativeCost2 aCost : prowlCosts) {
alterCosts.add(aCost.getCost());
}
return alterCosts;
}
}

View file

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

View file

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