Alternative spell abilities: added support of modes and other extra things in commander, awaken, jump-start, spectacle, retrace and surge abilities;

This commit is contained in:
Oleg Agafonov 2019-06-21 17:11:44 +04:00
parent a8c047a2be
commit d25ae47104
10 changed files with 138 additions and 89 deletions

View file

@ -126,4 +126,61 @@ public class CommandersCastTest extends CardTestCommander4Players {
assertTappedCount("Forest", true, 2);
assertTappedCount("Mountain", true, 3);
}
@Test
public void test_ModesNormal() {
// Player order: A -> D -> C -> B
// Choose four. You may choose the same mode more than once.
// Create a 2/2 Citizen creature token thats all colors.
// Return target permanent card from your graveyard to your hand.
// Proliferate.
// You gain 4 life.
addCard(Zone.HAND, playerA, "Planewide Celebration", 1); // {5}{G}{G}
addCard(Zone.BATTLEFIELD, playerA, "Forest", 7);
// cast (3 tokens + 4 life)
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Planewide Celebration");
setModeChoice(playerA, "1");
setModeChoice(playerA, "1");
setModeChoice(playerA, "1");
setModeChoice(playerA, "4");
checkPermanentCount("after", 1, PhaseStep.BEGIN_COMBAT, playerA, "Citizen", 3);
checkLife("after", 1, PhaseStep.BEGIN_COMBAT, playerA, 20 + 4);
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
assertAllCommandsUsed();
}
@Test
public void test_ModesCommander() {
// Player order: A -> D -> C -> B
// Choose four. You may choose the same mode more than once.
// Create a 2/2 Citizen creature token thats all colors.
// Return target permanent card from your graveyard to your hand.
// Proliferate.
// You gain 4 life.
addCard(Zone.COMMAND, playerA, "Planewide Celebration", 1); // {5}{G}{G}
addCard(Zone.BATTLEFIELD, playerA, "Forest", 7);
// cast (3 tokens + 4 life)
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Planewide Celebration");
setModeChoice(playerA, "1");
setModeChoice(playerA, "1");
setModeChoice(playerA, "1");
setModeChoice(playerA, "4");
setChoice(playerA, "Yes"); // return commander
checkPermanentCount("after", 1, PhaseStep.BEGIN_COMBAT, playerA, "Citizen", 3);
checkLife("after", 1, PhaseStep.BEGIN_COMBAT, playerA, 20 + 4);
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
assertAllCommandsUsed();
}
}

View file

@ -1,4 +1,3 @@
package mage.abilities;
import mage.abilities.costs.OptionalAdditionalModeSourceCosts;
@ -45,22 +44,23 @@ public class Modes extends LinkedHashMap<UUID, Mode> {
this.put(entry.getKey(), entry.getValue().copy());
}
for (Map.Entry<UUID, Mode> entry : modes.duplicateModes.entrySet()) {
this.put(entry.getKey(), entry.getValue().copy());
duplicateModes.put(entry.getKey(), entry.getValue().copy());
}
this.minModes = modes.minModes;
this.maxModes = modes.maxModes;
this.selectedModes.addAll(modes.getSelectedModes());
this.modeChooser = modes.modeChooser;
this.eachModeOnlyOnce = modes.eachModeOnlyOnce;
this.eachModeMoreThanOnce = modes.eachModeMoreThanOnce;
this.optionalAdditionalModeSourceCosts = modes.optionalAdditionalModeSourceCosts;
this.maxModesFilter = modes.maxModesFilter; // can't change so no copy needed
if (modes.getSelectedModes().isEmpty()) {
this.currentMode = values().iterator().next();
} else {
this.currentMode = get(modes.getMode().getId());
}
this.modeChooser = modes.modeChooser;
this.eachModeOnlyOnce = modes.eachModeOnlyOnce;
this.eachModeMoreThanOnce = modes.eachModeMoreThanOnce;
this.optionalAdditionalModeSourceCosts = modes.optionalAdditionalModeSourceCosts;
this.maxModesFilter = modes.maxModesFilter; // can't change so no copy needed
}
public Modes copy() {

View file

@ -186,6 +186,11 @@ public class SpellAbility extends ActivatedAbilityImpl {
return amount * xMultiplier;
}
public void setCardName(String cardName) {
this.cardName = cardName;
setSpellName();
}
private void setSpellName() {
switch (spellAbilityType) {
case SPLIT_FUSED:

View file

@ -11,20 +11,10 @@ import mage.constants.Zone;
public class CastCommanderAbility extends SpellAbility {
public CastCommanderAbility(Card card) {
super(card.getManaCost(), card.getName(), Zone.COMMAND, SpellAbilityType.BASE);
if (card.getSpellAbility() != null) {
this.getCosts().addAll(card.getSpellAbility().getCosts().copy());
this.getEffects().addAll(card.getSpellAbility().getEffects().copy());
this.getTargets().addAll(card.getSpellAbility().getTargets().copy());
this.setTargetAdjuster(card.getSpellAbility().getTargetAdjuster());
this.setCostAdjuster(card.getSpellAbility().getCostAdjuster());
this.timing = card.getSpellAbility().getTiming();
} else {
throw new IllegalStateException("Cast commander ability must be used with spell ability only: " + card.getName());
}
this.usesStack = true;
this.controllerId = card.getOwnerId();
this.sourceId = card.getId();
super(card.getSpellAbility());
this.setCardName(cardName = card.getName());
zone = Zone.COMMAND;
spellAbilityType = SpellAbilityType.BASE;
}
public CastCommanderAbility(final CastCommanderAbility ability) {

View file

@ -1,7 +1,5 @@
package mage.abilities.keyword;
import java.util.UUID;
import mage.MageInt;
import mage.MageObject;
import mage.abilities.Ability;
@ -13,25 +11,20 @@ import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.continuous.BecomesCreatureTargetEffect;
import mage.abilities.effects.common.counter.AddCountersTargetEffect;
import mage.cards.Card;
import mage.constants.CardType;
import mage.constants.Duration;
import mage.constants.Outcome;
import mage.constants.SpellAbilityType;
import mage.constants.SubType;
import mage.constants.Zone;
import mage.constants.*;
import mage.counters.CounterType;
import mage.filter.common.FilterControlledLandPermanent;
import mage.game.Game;
import mage.game.permanent.token.TokenImpl;
import mage.game.permanent.token.Token;
import mage.target.Target;
import mage.target.common.TargetControlledPermanent;
import mage.target.targetpointer.FixedTarget;
import mage.util.CardUtil;
import org.apache.log4j.Logger;
import java.util.UUID;
/**
*
* @author LevelX2
*/
public class AwakenAbility extends SpellAbility {
@ -44,12 +37,15 @@ public class AwakenAbility extends SpellAbility {
private int awakenValue;
public AwakenAbility(Card card, int awakenValue, String awakenCosts) {
super(new ManaCostsImpl<>(awakenCosts), card.getName() + " with awaken", Zone.HAND, SpellAbilityType.BASE_ALTERNATE);
this.getCosts().addAll(card.getSpellAbility().getCosts().copy());
this.getEffects().addAll(card.getSpellAbility().getEffects().copy());
this.getTargets().addAll(card.getSpellAbility().getTargets().copy());
this.spellAbilityType = SpellAbilityType.BASE_ALTERNATE;
this.timing = card.getSpellAbility().getTiming();
super(card.getSpellAbility());
this.setCardName(card.getName() + " with awaken");
zone = Zone.HAND;
spellAbilityType = SpellAbilityType.BASE_ALTERNATE;
this.getManaCosts().clear();
this.getManaCostsToPay().clear();
this.addManaCost(new ManaCostsImpl<>(awakenCosts));
this.addTarget(new TargetControlledPermanent(new FilterControlledLandPermanent(filterMessage)));
this.addEffect(new AwakenEffect());
this.awakenValue = awakenValue;
@ -57,7 +53,6 @@ public class AwakenAbility extends SpellAbility {
+ " <i>(If you cast this spell for " + awakenCosts + ", also put "
+ CardUtil.numberToText(awakenValue, "a")
+ " +1/+1 counters on target land you control and it becomes a 0/0 Elemental creature with haste. It's still a land.)</i>";
}
public AwakenAbility(final AwakenAbility ability) {
@ -116,17 +111,17 @@ public class AwakenAbility extends SpellAbility {
return effect.apply(game, source);
}
} else // source should never be null, but we are seeing a lot of NPEs from this section
if (source == null) {
logger.fatal("Source was null in AwakenAbility: Create a bug report or fix the source code");
} else if (source.getTargets() == null) {
MageObject sourceObj = source.getSourceObject(game);
if (sourceObj != null) {
Class<? extends MageObject> sourceClass = sourceObj.getClass();
if (sourceClass != null) {
logger.fatal("getTargets was null in AwakenAbility for " + sourceClass.toString() + " : Create a bug report or fix the source code");
if (source == null) {
logger.fatal("Source was null in AwakenAbility: Create a bug report or fix the source code");
} else if (source.getTargets() == null) {
MageObject sourceObj = source.getSourceObject(game);
if (sourceObj != null) {
Class<? extends MageObject> sourceClass = sourceObj.getClass();
if (sourceClass != null) {
logger.fatal("getTargets was null in AwakenAbility for " + sourceClass.toString() + " : Create a bug report or fix the source code");
}
}
}
}
return true;
}
}

View file

@ -1,7 +1,5 @@
package mage.abilities.keyword;
import java.util.UUID;
import mage.Mana;
import mage.abilities.SpellAbility;
import mage.abilities.costs.common.SacrificeTargetCost;
@ -21,8 +19,9 @@ import mage.target.TargetPermanent;
import mage.target.common.TargetControlledCreaturePermanent;
import mage.util.CardUtil;
import java.util.UUID;
/**
*
* @author emerald000
*/
public class EmergeAbility extends SpellAbility {
@ -30,11 +29,15 @@ public class EmergeAbility extends SpellAbility {
private final ManaCosts<ManaCost> emergeCost;
public EmergeAbility(Card card, ManaCosts<ManaCost> emergeCost) {
super(emergeCost, card.getName() + " with emerge", Zone.HAND, SpellAbilityType.BASE_ALTERNATE);
this.getCosts().addAll(card.getSpellAbility().getCosts().copy());
this.getEffects().addAll(card.getSpellAbility().getEffects().copy());
this.getTargets().addAll(card.getSpellAbility().getTargets().copy());
this.timing = card.getSpellAbility().getTiming();
super(card.getSpellAbility());
this.setCardName(card.getName() + " with emerge");
zone = Zone.HAND;
spellAbilityType = SpellAbilityType.BASE_ALTERNATE;
this.getManaCosts().clear();
this.getManaCostsToPay().clear();
this.addManaCost(emergeCost.copy());
this.setRuleAtTheTop(true);
this.emergeCost = emergeCost.copy();
}

View file

@ -36,16 +36,14 @@ public class JumpStartAbility extends SpellAbility {
private boolean replacementEffectAdded = false;
public JumpStartAbility(Card card) {
super(card.getManaCost(), card.getName() + " with jump-start", Zone.GRAVEYARD, SpellAbilityType.BASE_ALTERNATE);
this.getCosts().addAll(card.getSpellAbility().getCosts().copy());
super(card.getSpellAbility());
this.setCardName(card.getName() + " with jump-start");
zone = Zone.GRAVEYARD;
spellAbilityType = SpellAbilityType.BASE_ALTERNATE;
Cost cost = new DiscardTargetCost(new TargetCardInHand());
cost.setText("");
this.addCost(cost);
this.getEffects().addAll(card.getSpellAbility().getEffects().copy());
this.getTargets().addAll(card.getSpellAbility().getTargets().copy());
this.spellAbilityType = SpellAbilityType.BASE_ALTERNATE;
this.timing = card.getSpellAbility().getTiming();
}
public JumpStartAbility(final JumpStartAbility ability) {
@ -132,9 +130,7 @@ class JumpStartReplacementEffect extends ReplacementEffectImpl {
if (event.getTargetId().equals(source.getSourceId())
&& ((ZoneChangeEvent) event).getFromZone() == Zone.STACK
&& ((ZoneChangeEvent) event).getToZone() != Zone.EXILED) {
if (game.getState().getZoneChangeCounter(source.getSourceId()) == source.getSourceObjectZoneChangeCounter() + 1) {
return true;
}
return game.getState().getZoneChangeCounter(source.getSourceId()) == source.getSourceObjectZoneChangeCounter() + 1;
}
return false;

View file

@ -1,4 +1,3 @@
package mage.abilities.keyword;
import mage.abilities.SpellAbility;
@ -11,22 +10,21 @@ import mage.filter.common.FilterLandCard;
import mage.target.common.TargetCardInHand;
/**
*
* @author LevelX2
*/
public class RetraceAbility extends SpellAbility {
public RetraceAbility(Card card) {
super(card.getManaCost(), card.getName() + " with retrace", Zone.GRAVEYARD, SpellAbilityType.BASE_ALTERNATE);
this.getCosts().addAll(card.getSpellAbility().getCosts().copy());
super(card.getSpellAbility());
this.setCardName(card.getName() + " with retrace");
zone = Zone.GRAVEYARD;
spellAbilityType = SpellAbilityType.BASE_ALTERNATE;
Cost cost = new DiscardTargetCost(new TargetCardInHand(new FilterLandCard()));
cost.setText("");
this.addCost(cost);
this.getEffects().addAll(card.getSpellAbility().getEffects().copy());
this.getTargets().addAll(card.getSpellAbility().getTargets().copy());
this.spellAbilityType = SpellAbilityType.BASE_ALTERNATE;
this.timing = card.getSpellAbility().getTiming();
this.setRuleAtTheTop(true);
}
public RetraceAbility(final RetraceAbility ability) {

View file

@ -22,12 +22,15 @@ public class SpectacleAbility extends SpellAbility {
private String rule;
public SpectacleAbility(Card card, ManaCost spectacleCosts) {
super(spectacleCosts, card.getName() + " with spectacle", Zone.HAND, SpellAbilityType.BASE_ALTERNATE);
this.getCosts().addAll(card.getSpellAbility().getCosts().copy());
this.getEffects().addAll(card.getSpellAbility().getEffects().copy());
this.getTargets().addAll(card.getSpellAbility().getTargets().copy());
this.spellAbilityType = SpellAbilityType.BASE_ALTERNATE;
this.timing = card.getSpellAbility().getTiming();
super(card.getSpellAbility());
this.setCardName(card.getName() + " with spectacle");
zone = Zone.HAND;
spellAbilityType = SpellAbilityType.BASE_ALTERNATE;
this.getManaCosts().clear();
this.getManaCostsToPay().clear();
this.addManaCost(spectacleCosts.copy());
this.setRuleAtTheTop(true);
this.rule = "Spectacle " + spectacleCosts.getText()
+ " <i>(You may cast this spell for its spectacle cost rather than its mana cost if an opponent lost life this turn.)</i>";

View file

@ -1,8 +1,5 @@
package mage.abilities.keyword;
import java.util.ArrayList;
import java.util.UUID;
import mage.abilities.SpellAbility;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.cards.Card;
@ -12,8 +9,10 @@ import mage.game.Game;
import mage.players.Player;
import mage.watchers.common.CastSpellLastTurnWatcher;
import java.util.ArrayList;
import java.util.UUID;
/**
*
* @author LevelX2
*/
public class SurgeAbility extends SpellAbility {
@ -23,12 +22,15 @@ public class SurgeAbility extends SpellAbility {
private String rule;
public SurgeAbility(Card card, String surgeCosts) {
super(new ManaCostsImpl<>(surgeCosts), card.getName() + " with surge", Zone.HAND, SpellAbilityType.BASE_ALTERNATE);
this.getCosts().addAll(card.getSpellAbility().getCosts().copy());
this.getEffects().addAll(card.getSpellAbility().getEffects().copy());
this.getTargets().addAll(card.getSpellAbility().getTargets().copy());
this.spellAbilityType = SpellAbilityType.BASE_ALTERNATE;
this.timing = card.getSpellAbility().getTiming();
super(card.getSpellAbility());
this.setCardName(card.getName() + " with surge");
zone = Zone.HAND;
spellAbilityType = SpellAbilityType.BASE_ALTERNATE;
this.getManaCosts().clear();
this.getManaCostsToPay().clear();
this.addManaCost(new ManaCostsImpl<>(surgeCosts));
this.setRuleAtTheTop(true);
this.rule = "Surge " + surgeCosts
+ " <i>(You may cast this spell for its surge cost if you or a teammate has cast another spell this turn.)</i>";