Fix casting Transformed (#10778)

* Combine casting Transformed into a shared SpellAbility, apply transform effect before spell is cast

* Minor cleanup

* Use effect.apply() rather than game.applyEffects()

* Add test with Maskwood Nexus
This commit is contained in:
ssk97 2023-09-15 14:56:32 -07:00 committed by GitHub
parent 8859637844
commit b6dbc782be
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 259 additions and 172 deletions

View file

@ -71,6 +71,95 @@ public class DisturbTest extends CardTestPlayerBase {
execute();
}
/**
* All attributes of the spell should be the back face's
* however the MV of the spell is the front face's
*/
@Test
public void test_SpellAttributesOnStack2() {
// Disturb {4}{U}
// Waildrifter
addCard(Zone.GRAVEYARD, playerA, "Galedrifter", 1); // {3}{U}
addCard(Zone.BATTLEFIELD, playerA, "Volcanic Island", 6);
addCard(Zone.HAND, playerA, "Lightning Bolt", 1);
checkPlayableAbility("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Waildrifter using Disturb", true);
// cast with disturb
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Waildrifter using Disturb");
checkStackObject("on stack", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Waildrifter using Disturb", 1);
runCode("check stack", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> {
// Stack must contain another card side, so spell/card characteristics must be diff from main side (only mana value is same)
Spell spell = (Spell) game.getStack().getFirst();
Assert.assertEquals("Waildrifter", spell.getName());
Assert.assertEquals(1, spell.getCardType(game).size());
Assert.assertEquals(CardType.CREATURE, spell.getCardType(game).get(0));
Assert.assertEquals(2, spell.getSubtype(game).size());
Assert.assertEquals(SubType.HIPPOGRIFF, spell.getSubtype(game).get(0));
Assert.assertEquals(SubType.SPIRIT, spell.getSubtype(game).get(1));
Assert.assertEquals(2, spell.getPower().getValue());
Assert.assertEquals(2, spell.getToughness().getValue());
Assert.assertEquals("U", spell.getColor(game).toString());
Assert.assertEquals(4, spell.getManaValue()); // {3}{U}
Assert.assertEquals("{4}{U}", spell.getSpellAbility().getManaCosts().getText());
});
// must be transformed
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
checkPermanentCount("after", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Waildrifter", 1);
checkPT("after", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Waildrifter", 2, 2);
checkSubType("after", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Waildrifter", SubType.SPIRIT, true);
// must be exiled on die
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", "Waildrifter");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
checkExileCount("after die", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Waildrifter", 0);
checkExileCount("after die", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Galedrifter", 1);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertTappedCount("Volcanic Island",true,6); //5+1
}
@Test
public void test_SpellAttributesTrigger() {
// Disturb {5}{W}{W}
// Sinner's Judgment
addCard(Zone.GRAVEYARD, playerA, "Faithbound Judge", 1);
addCard(Zone.BATTLEFIELD, playerA, "Plains", 7);
addCard(Zone.BATTLEFIELD, playerA, "Firebrand Archer", 1);
//
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Sinner's Judgment using Disturb",playerB);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertPermanentCount(playerA, 9);
assertPermanentCount(playerA, "Sinner's Judgment", 1);
assertLife(playerB, 19);
}
@Test
public void test_SpellAttributesIndirectTrigger() {
// Disturb {1}{U}
// Hook-Haunt Drifter
addCard(Zone.GRAVEYARD, playerA, "Baithook Angler", 1);
addCard(Zone.BATTLEFIELD, playerA, "Island", 2);
addCard(Zone.BATTLEFIELD, playerA, "Lys Alana Huntmaster", 1);
addCard(Zone.BATTLEFIELD, playerA, "Maskwood Nexus", 1);
// Transform's copy effect must not override other spell modifications
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Hook-Haunt Drifter using Disturb");
setChoice(playerA, true);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertPermanentCount(playerA, "Elf Warrior Token", 1);
}
/**
* Relevant ruling:
* To determine the total cost of a spell, start with the mana cost or alternative cost
@ -128,6 +217,27 @@ public class DisturbTest extends CardTestPlayerBase {
execute();
}
@Test
public void test_ConditionalCostModifications() {
// Disturb {5}{W}{W}
// Sinner's Judgment
addCard(Zone.GRAVEYARD, playerA, "Faithbound Judge", 1);
addCard(Zone.BATTLEFIELD, playerA, "Plains", 7);
addCard(Zone.BATTLEFIELD, playerA, "Transcendent Envoy", 1); //-1 if Aura
addCard(Zone.BATTLEFIELD, playerB, "Reidane, God of the Worthy", 1); //+2 if noncreature MV>=4, should NOT apply
addCard(Zone.BATTLEFIELD, playerA, "Pearl Medallion", 1); //-1 if white
//net -2
//
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Sinner's Judgment using Disturb",playerB);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertPermanentCount(playerA, "Sinner's Judgment", 1);
assertTappedCount("Plains", true, 5);
}
/**
* Relevant rule:
* If you copy a permanent spell cast this way (perhaps with a card like Double Major), the copy becomes

View file

@ -154,4 +154,24 @@ public class BattleDuelTest extends BattleBaseTest {
assertGraveyardCount(playerA, kaladesh, 1);
assertPermanentCount(playerA, "Thopter Token", 1);
}
@Test
public void testSpellCardTypeTrigger() {
addCard(Zone.BATTLEFIELD, playerA, "Plateau", 3 + 6);
addCard(Zone.BATTLEFIELD, playerA, "Oketra's Monument");
addCard(Zone.BATTLEFIELD, playerA, "Deeproot Champion");
addCard(Zone.HAND, playerA, "Invasion of Dominaria");
addCard(Zone.HAND, playerA, impact);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Invasion of Dominaria");
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, impact, "Invasion of Dominaria");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertPermanentCount(playerA, "Serra Faithkeeper", 1);
assertPermanentCount(playerA, "Warrior Token", 1);
assertPowerToughness(playerA, "Deeproot Champion",3,3);
}
}

View file

@ -32,9 +32,6 @@ public class LazotepConvertTest extends CardTestPlayerBase {
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
currentGame.debugMessage("Graveyard: "+currentGame.getPlayer(playerA.getId()).getGraveyard().stream()
.map(x -> currentGame.getObject(x).getClass().getSimpleName()).collect(Collectors.toList()));
assertPermanentCount(playerA, "Mutagen Connoisseur", 1);
assertGraveyardCount(playerA, "Mutagen Connoisseur", 1);
assertSubtype("Mutagen Connoisseur", SubType.ZOMBIE);

View file

@ -4,7 +4,6 @@ import mage.ApprovingObject;
import mage.abilities.Ability;
import mage.abilities.StaticAbility;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.ContinuousEffectImpl;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.keyword.TransformAbility;
import mage.cards.Card;
@ -13,9 +12,7 @@ import mage.counters.CounterType;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.game.stack.Spell;
import mage.players.Player;
import mage.target.targetpointer.FixedTarget;
/**
* @author TheElk801
@ -109,39 +106,9 @@ class SiegeDefeatedEffect extends OneShotEffect {
return true;
}
game.getState().setValue("PlayFromNotOwnHandZone" + card.getSecondCardFace().getId(), Boolean.TRUE);
if (player.cast(card.getSecondFaceSpellAbility(), game, true, new ApprovingObject(source, game))) {
game.getState().setValue(TransformAbility.VALUE_KEY_ENTER_TRANSFORMED + card.getId(), Boolean.TRUE);
game.addEffect(new SiegeTransformEffect().setTargetPointer(new FixedTarget(card, game)), source);
}
SpellTransformedAbility transformedSpell = new SpellTransformedAbility(card.getSecondFaceSpellAbility());
player.cast(transformedSpell, game, true, new ApprovingObject(source, game));
game.getState().setValue("PlayFromNotOwnHandZone" + card.getSecondCardFace().getId(), null);
return true;
}
}
class SiegeTransformEffect extends ContinuousEffectImpl {
public SiegeTransformEffect() {
super(Duration.WhileOnStack, Layer.CopyEffects_1, SubLayer.CopyEffects_1a, Outcome.BecomeCreature);
}
private SiegeTransformEffect(final SiegeTransformEffect effect) {
super(effect);
}
@Override
public SiegeTransformEffect copy() {
return new SiegeTransformEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Spell spell = game.getSpell(getTargetPointer().getFirst(game, source));
if (spell == null || spell.getCard().getSecondCardFace() == null) {
return false;
}
// simulate another side as new card (another code part in spell constructor)
TransformAbility.transformCardSpellDynamic(spell, spell.getCard().getSecondCardFace(), game);
return true;
}
}

View file

@ -0,0 +1,112 @@
package mage.abilities.common;
import mage.abilities.Ability;
import mage.abilities.SpellAbility;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.ContinuousEffectImpl;
import mage.abilities.keyword.TransformAbility;
import mage.cards.Card;
import mage.constants.*;
import mage.game.Game;
import mage.game.stack.Spell;
import java.util.UUID;
/**
* @author weirddan455, JayDi85, notgreat
*/
public class SpellTransformedAbility extends SpellAbility {
protected final String manaCost; //This variable is only used for rules text
public SpellTransformedAbility(Card card, String manaCost) {
super(card.getSecondFaceSpellAbility());
this.newId();
// getSecondFaceSpellAbility() already verified that second face exists
this.setCardName(card.getSecondCardFace().getName());
this.spellAbilityType = SpellAbilityType.BASE_ALTERNATE;
this.setSpellAbilityCastMode(SpellAbilityCastMode.TRANSFORMED);
this.manaCost = manaCost;
this.clearManaCosts();
this.clearManaCostsToPay();
this.addManaCost(new ManaCostsImpl<>(manaCost));
this.addSubAbility(new TransformAbility());
}
public SpellTransformedAbility(final SpellAbility ability) {
super(ability);
this.newId();
this.manaCost = null;
this.getManaCosts().clear();
this.getManaCostsToPay().clear();
this.spellAbilityType = SpellAbilityType.BASE_ALTERNATE;
this.setSpellAbilityCastMode(SpellAbilityCastMode.TRANSFORMED);
//when casting this way, the card must have the TransformAbility from elsewhere
}
protected SpellTransformedAbility(final SpellTransformedAbility ability) {
super(ability);
this.manaCost = ability.manaCost;
}
@Override
public SpellTransformedAbility copy() {
return new SpellTransformedAbility(this);
}
@Override
public boolean activate(Game game, boolean noMana) {
if (super.activate(game, noMana)) {
game.getState().setValue(TransformAbility.VALUE_KEY_ENTER_TRANSFORMED + getSourceId(), Boolean.TRUE);
// TODO: must be removed after transform cards (one side) migrated to MDF engine (multiple sides)
TransformedEffect effect = new TransformedEffect();
game.addEffect(effect, this);
effect.apply(game, this); //Apply the effect immediately
return true;
}
return false;
}
@Override
public ActivationStatus canActivate(UUID playerId, Game game) {
if (super.canActivate(playerId, game).canActivate()) {
Card card = game.getCard(getSourceId());
if (card != null) {
return card.getSpellAbility().canActivate(playerId, game);
}
}
return ActivationStatus.getFalse();
}
}
class TransformedEffect extends ContinuousEffectImpl {
public TransformedEffect() {
super(Duration.WhileOnStack, Layer.CopyEffects_1, SubLayer.CopyEffects_1a, Outcome.BecomeCreature);
staticText = "";
}
private TransformedEffect(final TransformedEffect effect) {
super(effect);
}
@Override
public TransformedEffect copy() {
return new TransformedEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Spell spell = game.getSpell(source.getSourceId());
if (spell == null || spell.getCard().getSecondCardFace() == null) {
return false;
}
// simulate another side as new card (another code part in spell constructor)
TransformAbility.transformCardSpellDynamic(spell, spell.getCard().getSecondCardFace(), game);
return true;
}
}

View file

@ -1,13 +1,9 @@
package mage.abilities.keyword;
import mage.abilities.Ability;
import mage.abilities.SpellAbility;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.ContinuousEffectImpl;
import mage.abilities.common.SpellTransformedAbility;
import mage.cards.Card;
import mage.constants.*;
import mage.game.Game;
import mage.game.stack.Spell;
import java.util.UUID;
@ -22,35 +18,18 @@ import java.util.UUID;
* 702.146b A resolving transforming double-faced spell that was cast using its disturb
* ability enters the battlefield with its back face up.
*
* @author weirddan455, JayDi85
* @author notgreat, weirddan455, JayDi85
*/
public class DisturbAbility extends SpellAbility {
private final String manaCost;
private SpellAbility spellAbilityToResolve;
public class DisturbAbility extends SpellTransformedAbility {
public DisturbAbility(Card card, String manaCost) {
super(card.getSecondFaceSpellAbility());
this.newId();
// getSecondFaceSpellAbility() already verified that second face exists
this.setCardName(card.getSecondCardFace().getName());
super(card, manaCost);
this.zone = Zone.GRAVEYARD;
this.spellAbilityType = SpellAbilityType.BASE_ALTERNATE;
this.setSpellAbilityCastMode(SpellAbilityCastMode.DISTURB);
this.manaCost = manaCost;
this.clearManaCosts();
this.clearManaCostsToPay();
this.addManaCost(new ManaCostsImpl<>(manaCost));
this.addSubAbility(new TransformAbility());
}
private DisturbAbility(final DisturbAbility ability) {
super(ability);
this.manaCost = ability.manaCost;
this.spellAbilityToResolve = ability.spellAbilityToResolve;
}
@Override
@ -58,17 +37,6 @@ public class DisturbAbility extends SpellAbility {
return new DisturbAbility(this);
}
@Override
public boolean activate(Game game, boolean noMana) {
if (super.activate(game, noMana)) {
game.getState().setValue(TransformAbility.VALUE_KEY_ENTER_TRANSFORMED + getSourceId(), Boolean.TRUE);
// TODO: must be removed after transform cards (one side) migrated to MDF engine (multiple sides)
game.addEffect(new DisturbEffect(), this);
return true;
}
return false;
}
@Override
public ActivationStatus canActivate(UUID playerId, Game game) {
if (super.canActivate(playerId, game).canActivate()) {
@ -91,36 +59,3 @@ public class DisturbAbility extends SpellAbility {
+ " <i>(You may cast this card transformed from your graveyard for its disturb cost.)</i>";
}
}
class DisturbEffect extends ContinuousEffectImpl {
public DisturbEffect() {
super(Duration.WhileOnStack, Layer.CopyEffects_1, SubLayer.CopyEffects_1a, Outcome.BecomeCreature);
staticText = "";
}
private DisturbEffect(final DisturbEffect effect) {
super(effect);
}
@Override
public DisturbEffect copy() {
return new DisturbEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Spell spell = game.getSpell(source.getSourceId());
if (spell == null || spell.getFromZone() != Zone.GRAVEYARD) {
return false;
}
if (spell.getCard().getSecondCardFace() == null) {
return false;
}
// simulate another side as new card (another code part in spell constructor)
TransformAbility.transformCardSpellDynamic(spell, spell.getCard().getSecondCardFace(), game);
return true;
}
}

View file

@ -1,43 +1,22 @@
package mage.abilities.keyword;
import mage.abilities.Ability;
import mage.abilities.SpellAbility;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.ContinuousEffectImpl;
import mage.abilities.common.SpellTransformedAbility;
import mage.cards.Card;
import mage.constants.*;
import mage.game.Game;
import mage.game.stack.Spell;
/**
* @author weirddan455, JayDi85, TheElk801 (based heavily on disturb)
* @author notgreat, TheElk801
*/
public class MoreThanMeetsTheEyeAbility extends SpellAbility {
public class MoreThanMeetsTheEyeAbility extends SpellTransformedAbility {
private final String manaCost;
private SpellAbility spellAbilityToResolve;
public MoreThanMeetsTheEyeAbility(Card card, String manaCost) {
super(card.getSecondFaceSpellAbility());
this.newId();
// getSecondFaceSpellAbility() already verified that second face exists
this.setCardName(card.getSecondCardFace().getName());
this.spellAbilityType = SpellAbilityType.BASE_ALTERNATE;
super(card, manaCost);
this.setSpellAbilityCastMode(SpellAbilityCastMode.MORE_THAN_MEETS_THE_EYE);
this.manaCost = manaCost;
this.clearManaCosts();
this.clearManaCostsToPay();
this.addManaCost(new ManaCostsImpl<>(manaCost));
this.addSubAbility(new TransformAbility());
}
private MoreThanMeetsTheEyeAbility(final MoreThanMeetsTheEyeAbility ability) {
super(ability);
this.manaCost = ability.manaCost;
this.spellAbilityToResolve = ability.spellAbilityToResolve;
}
@Override
@ -45,17 +24,6 @@ public class MoreThanMeetsTheEyeAbility extends SpellAbility {
return new MoreThanMeetsTheEyeAbility(this);
}
@Override
public boolean activate(Game game, boolean noMana) {
if (super.activate(game, noMana)) {
game.getState().setValue(TransformAbility.VALUE_KEY_ENTER_TRANSFORMED + getSourceId(), Boolean.TRUE);
// TODO: must be removed after transform cards (one side) migrated to MDF engine (multiple sides)
game.addEffect(new MoreThanMeetsTheEyeEffect(), this);
return true;
}
return false;
}
@Override
public String getRule(boolean all) {
return this.getRule();
@ -67,31 +35,3 @@ public class MoreThanMeetsTheEyeAbility extends SpellAbility {
+ " <i>(You may cast this card converted for " + this.manaCost + ".)</i>";
}
}
class MoreThanMeetsTheEyeEffect extends ContinuousEffectImpl {
public MoreThanMeetsTheEyeEffect() {
super(Duration.WhileOnStack, Layer.CopyEffects_1, SubLayer.CopyEffects_1a, Outcome.BecomeCreature);
staticText = "";
}
private MoreThanMeetsTheEyeEffect(final MoreThanMeetsTheEyeEffect effect) {
super(effect);
}
@Override
public MoreThanMeetsTheEyeEffect copy() {
return new MoreThanMeetsTheEyeEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Spell spell = game.getSpell(source.getSourceId());
if (spell == null || spell.getCard().getSecondCardFace() == null) {
return false;
}
// simulate another side as new card (another code part in spell constructor)
TransformAbility.transformCardSpellDynamic(spell, spell.getCard().getSecondCardFace(), game);
return true;
}
}

View file

@ -44,6 +44,12 @@ public enum SpellAbilityCastMode {
if (this.equals(BESTOW)) {
BestowAbility.becomeAura(cardCopy);
}
if (this.isTransformed){
Card tmp = card.getSecondCardFace();
if (tmp != null) {
cardCopy = tmp.copy();
}
}
return cardCopy;
}
}

View file

@ -774,7 +774,7 @@ public class Spell extends StackObjectImpl implements Card {
@Override
public Card getSecondCardFace() {
return null;
return card.getSecondCardFace();
}
@Override