Modal double-faced cards - improved support with copy effects (fixed that copied token has abilities from both sides, closes #10146, closes #8476);

This commit is contained in:
Oleg Agafonov 2024-05-26 13:01:36 +04:00
parent 0e39d6a833
commit 6b86b1053f
3 changed files with 168 additions and 24 deletions

View file

@ -1,18 +1,27 @@
package org.mage.test.cards.cost.modaldoublefaced; package org.mage.test.cards.cost.modaldoublefaced;
import mage.abilities.keyword.HasteAbility; import mage.abilities.keyword.HasteAbility;
import mage.abilities.keyword.MenaceAbility;
import mage.cards.Card; import mage.cards.Card;
import mage.cards.ModalDoubleFacedCard; import mage.cards.ModalDoubleFacedCard;
import mage.constants.CardType;
import mage.constants.PhaseStep; import mage.constants.PhaseStep;
import mage.constants.SubType; import mage.constants.SubType;
import mage.constants.Zone; import mage.constants.Zone;
import mage.game.permanent.PermanentCard; import mage.game.permanent.PermanentCard;
import mage.game.permanent.PermanentToken;
import mage.util.CardUtil; import mage.util.CardUtil;
import mage.util.ManaUtil; import mage.util.ManaUtil;
import mage.view.GameView;
import mage.view.PermanentView;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase; import org.mage.test.serverside.base.CardTestPlayerBase;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/** /**
* @author JayDi85 * @author JayDi85
*/ */
@ -973,6 +982,129 @@ public class ModalDoubleFacedCardsTest extends CardTestPlayerBase {
assertPermanentCount(playerA, "Birgi, God of Storytelling", 1); assertPermanentCount(playerA, "Birgi, God of Storytelling", 1);
} }
@Test
public void test_Copy_TokenFromCard_MustIgnoreSecondSide() {
// bug: copied tokens of MDF cards has abilities from both sides
// https://github.com/magefree/mage/issues/8476
// 707.8a
// If an effect creates a token that is a copy of a transforming permanent or a transforming double-faced
// card not on the battlefield, the resulting token is a transforming token that has both a front face
// and a back face. The characteristics of each face are determined by the copiable values of the same
// face of the permanent it is a copy of, as modified by any other copy effects that apply to that permanent.
// If the token is a copy of a transforming permanent with its back face up, the token enters the battlefield
// with its back face up. This rule does not apply to tokens that are created with their own set of
// characteristics and enter the battlefield as a copy of a transforming permanent due to a replacement effect.
// MDFC is not transforming doubled-faced card, so token must have only single non-transformable side
// {2}{R}, {T}: Create a token that's a copy of target creature card in your graveyard, except it's an artifact
// in addition to its other types. It gains haste. Sacrifice it at the beginning of the next end step.
addCard(Zone.BATTLEFIELD, playerA, "Feldon of the Third Path");
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3);
//
// 1
// Tergrid, God of Fright
// Legendary Creature - God
// Menace
// Whenever an opponent sacrifices a nontoken permanent or discards a permanent card, you may put that card onto
// the battlefield under your control from their graveyard.
// 2
// Tergrid's Lantern
// Legendary Artifact
// 4/5
// {T}: Target player loses 3 life unless they sacrifice a nonland permanent or discard a card.
// {3}{B}: Untap Tergrids Lantern.
addCard(Zone.GRAVEYARD, playerA, "Tergrid, God of Fright", 1); // {2}{R}
// find and keep image data
Map<String, String> imageData = new HashMap<>();
imageData.put("set code", "");
imageData.put("card number", "");
imageData.put("image number", "");
imageData.put("use var art", "");
runCode("collect", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> {
Assert.assertEquals(1, playerA.getGraveyard().size());
Card card = playerA.getGraveyard().getCards(game).stream().findFirst().orElse(null);
Assert.assertNotNull(card);
imageData.put("set code", card.getExpansionSetCode());
imageData.put("card number", card.getCardNumber());
imageData.put("image number", String.valueOf(card.getImageNumber()));
imageData.put("use var art", String.valueOf(card.getUsesVariousArt()));
});
// prepare token from MDFC
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}{R}, {T}: Create a token");
addTarget(playerA, "Tergrid, God of Fright");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
// SERVER SIDE
// from side 1
checkType("server must use side 1 - type", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Tergrid, God of Fright", CardType.CREATURE, true);
checkSubType("server must use side 1 - subtype", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Tergrid, God of Fright", SubType.GOD, true);
checkPT("server must use side 1 - PT", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Tergrid, God of Fright", 4, 5);
checkAbility("server must use side 1 - menace ability", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Tergrid, God of Fright", MenaceAbility.class, true);
// from copy effect
checkType("server must use effect - artifact", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Tergrid, God of Fright", CardType.ARTIFACT, true);
checkAbility("server must use effect - haste ability", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Tergrid, God of Fright", HasteAbility.class, true);
// from side 2
runCode("check side 2", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> {
PermanentToken permanent = (PermanentToken) game.getBattlefield().getAllPermanents()
.stream()
.filter(p -> p.getName().equals("Tergrid, God of Fright"))
.findFirst()
.orElse(null);
Assert.assertNotNull(permanent);
// MDFC on battlefield has only one side (not transformable)
Assert.assertFalse("server must not be transformable", permanent.isTransformable());
Assert.assertNull("server must have not other side", permanent.getOtherFace());
List<String> rules = permanent.getRules(game);
Assert.assertTrue("server must ignore side 2 - untap ability", rules.stream().noneMatch(r -> r.contains("Untap")));
Assert.assertTrue("server must ignore side 2 - target player ability", rules.stream().noneMatch(r -> r.contains("Target player loses")));
Assert.assertEquals("server image data - set code", imageData.get("set code"), permanent.getExpansionSetCode());
Assert.assertEquals("server image data - card number", imageData.get("card number"), permanent.getCardNumber());
Assert.assertEquals("server image data - image number", imageData.get("image number"), String.valueOf(permanent.getImageNumber()));
Assert.assertEquals("server image data - use var art", imageData.get("use var art"), String.valueOf(permanent.getUsesVariousArt()));
});
setStrictChooseMode(true);
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
execute();
assertPowerToughness(playerA, "Tergrid, God of Fright", 4, 5);
// CLIENT SIDE
GameView gameView = getGameView(playerA);
PermanentView permanentView = gameView.getMyPlayer().getBattlefield().values()
.stream()
.filter(p -> p.getName().equals("Tergrid, God of Fright"))
.findFirst()
.orElse(null);
Assert.assertNotNull(permanentView);
List<String> rules = permanentView.getRules();
// from side 1
Assert.assertTrue("client must use side 1 - type", permanentView.getTypeText().contains("Creature"));
Assert.assertTrue("client must use side 1 - subtype", permanentView.getSubTypes().contains(SubType.GOD));
Assert.assertEquals("client must use side 1 - P", "4", permanentView.getPower());
Assert.assertEquals("client must use side 1 - T", "5", permanentView.getToughness());
Assert.assertTrue("client must use side 1 - menace ability", rules.stream().anyMatch(r -> r.contains("Menace")));
// from copy effect
Assert.assertTrue("client must use effect - artifact", permanentView.getTypeText().contains("Artifact"));
Assert.assertTrue("client must use effect - haste ability", rules.stream().anyMatch(r -> r.contains("Haste")));
// from side 2
Assert.assertTrue("client must ignore side 2 - untap ability", rules.stream().noneMatch(r -> r.contains("Untap")));
Assert.assertTrue("client must ignore side 2 - target player ability", rules.stream().noneMatch(r -> r.contains("Target player loses")));
// image data
Assert.assertEquals("client image data - set code", imageData.get("set code"), permanentView.getExpansionSetCode());
Assert.assertEquals("client image data - card number", imageData.get("card number"), permanentView.getCardNumber());
Assert.assertEquals("client image data - image number", imageData.get("image number"), String.valueOf(permanentView.getImageNumber()));
Assert.assertEquals("client image data - use var art", imageData.get("use var art"), String.valueOf(permanentView.getUsesVariousArt()));
}
@Test @Test
public void test_FindMovedPermanentByCard() { public void test_FindMovedPermanentByCard() {
// original problem: you must be able to find a card after move it to battlefield // original problem: you must be able to find a card after move it to battlefield

View file

@ -205,7 +205,7 @@ public class CreateTokenCopyTargetEffect extends OneShotEffect {
} }
// create token and modify all attributes permanently (without game usage) // create token and modify all attributes permanently (without game usage)
Token token = CopyTokenFunction.createTokenCopy(copyFrom, game); // needed so that entersBattlefied triggered abilities see the attributes (e.g. Master Biomancer) Token token = CopyTokenFunction.createTokenCopy(copyFrom, game); // needed so that entersBattlefield triggered abilities see the attributes (e.g. Master Biomancer)
applier.apply(game, token, source, targetId); applier.apply(game, token, source, targetId);
if (becomesArtifact) { if (becomesArtifact) {
token.addCardType(CardType.ARTIFACT); token.addCardType(CardType.ARTIFACT);

View file

@ -43,32 +43,40 @@ public class CopyTokenFunction {
if (target == null) { if (target == null) {
throw new IllegalArgumentException("Target can't be null"); throw new IllegalArgumentException("Target can't be null");
} }
// A copy contains only the attributes of the basic card or basic Token that's the base of the permanent
// else gained abililies would be copied too.
target.setEntersTransformed(source instanceof Permanent && ((Permanent) source).isTransformed());
MageObject sourceObj;
// from another token // apply transformed status on ETB
target.setEntersTransformed(source instanceof Permanent && ((Permanent) source).isTransformed());
// 707.8a
// If an effect creates a token that is a copy of a transforming permanent or a transforming double-faced
// card not on the battlefield, the resulting token is a transforming token that has both a front face
// and a back face. The characteristics of each face are determined by the copiable values of the same
// face of the permanent it is a copy of, as modified by any other copy effects that apply to that permanent.
// If the token is a copy of a transforming permanent with its back face up, the token enters the battlefield
// with its back face up. This rule does not apply to tokens that are created with their own set of
// characteristics and enter the battlefield as a copy of a transforming permanent due to a replacement effect.
// from token permanent
if (source instanceof PermanentToken) { if (source instanceof PermanentToken) {
// create token from another token // create token from another token
Token sourceToken = ((PermanentToken) source).getToken(); Token sourceObj = ((PermanentToken) source).getToken();
sourceObj = sourceToken; target.setCopySourceCard(sourceObj.getCopySourceCard()); // must link with original card
target.setCopySourceCard(((PermanentToken) source).getToken().getCopySourceCard()); // main side
copyToToken(target, sourceObj, game); copyToToken(target, sourceObj, game);
CardUtil.copySetAndCardNumber(target, source); CardUtil.copySetAndCardNumber(target, source);
if (sourceToken.getBackFace() != null) { // second side
copyToToken(target.getBackFace(), sourceToken.getBackFace(), game); if (sourceObj.getBackFace() != null) {
CardUtil.copySetAndCardNumber(target.getBackFace(), sourceToken.getBackFace()); copyToToken(target.getBackFace(), sourceObj.getBackFace(), game);
CardUtil.copySetAndCardNumber(target.getBackFace(), sourceObj.getBackFace());
} }
return; return;
} }
// from a permanent // from non-token permanent
if (source instanceof PermanentCard) { if (source instanceof PermanentCard) {
// create token from non-token permanent // create token from non-token permanent
// face down must hide all info // apply face down status
PermanentCard sourcePermanent = (PermanentCard) source; PermanentCard sourcePermanent = (PermanentCard) source;
BecomesFaceDownCreatureEffect.FaceDownType faceDownType = BecomesFaceDownCreatureEffect.findFaceDownType(game, sourcePermanent); BecomesFaceDownCreatureEffect.FaceDownType faceDownType = BecomesFaceDownCreatureEffect.findFaceDownType(game, sourcePermanent);
if (faceDownType != null) { if (faceDownType != null) {
@ -76,15 +84,18 @@ public class CopyTokenFunction {
return; return;
} }
sourceObj = source.getMainCard(); Card sourceObj = source.getMainCard();
target.setCopySourceCard((Card) sourceObj); target.setCopySourceCard(sourceObj);
// main side
copyToToken(target, sourceObj, game); copyToToken(target, sourceObj, game);
CardUtil.copySetAndCardNumber(target, sourceObj); CardUtil.copySetAndCardNumber(target, sourceObj);
if (((Card) sourceObj).isTransformable()) { // second side
copyToToken(target.getBackFace(), ((Card) sourceObj).getSecondCardFace(), game); if (sourceObj.isTransformable()) {
CardUtil.copySetAndCardNumber(target.getBackFace(), ((Card) sourceObj).getSecondCardFace()); copyToToken(target.getBackFace(), sourceObj.getSecondCardFace(), game);
CardUtil.copySetAndCardNumber(target.getBackFace(), sourceObj.getSecondCardFace());
} }
// apply prototyped status
if (((PermanentCard) source).isPrototyped()){ if (((PermanentCard) source).isPrototyped()){
Abilities<Ability> abilities = source.getAbilities(); Abilities<Ability> abilities = source.getAbilities();
for (Ability ability : abilities){ for (Ability ability : abilities){
@ -96,12 +107,13 @@ public class CopyTokenFunction {
return; return;
} }
// from another object like card (example: Embalm ability) // from another card (example: Embalm ability)
sourceObj = source; Card sourceObj = CardUtil.getDefaultCardSideForBattlefield(game, source.getMainCard());
target.setCopySourceCard(source); target.setCopySourceCard(source);
// main side
copyToToken(target, sourceObj, game); copyToToken(target, sourceObj, game);
CardUtil.copySetAndCardNumber(target, sourceObj); CardUtil.copySetAndCardNumber(target, sourceObj);
// second side
if (source.isTransformable()) { if (source.isTransformable()) {
if (target.getBackFace() == null) { if (target.getBackFace() == null) {
// if you catch this then a weird use case here: card with single face copy another token with double face?? // if you catch this then a weird use case here: card with single face copy another token with double face??