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;
import mage.abilities.keyword.HasteAbility;
import mage.abilities.keyword.MenaceAbility;
import mage.cards.Card;
import mage.cards.ModalDoubleFacedCard;
import mage.constants.CardType;
import mage.constants.PhaseStep;
import mage.constants.SubType;
import mage.constants.Zone;
import mage.game.permanent.PermanentCard;
import mage.game.permanent.PermanentToken;
import mage.util.CardUtil;
import mage.util.ManaUtil;
import mage.view.GameView;
import mage.view.PermanentView;
import org.junit.Assert;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author JayDi85
*/
@ -973,6 +982,129 @@ public class ModalDoubleFacedCardsTest extends CardTestPlayerBase {
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
public void test_FindMovedPermanentByCard() {
// 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)
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);
if (becomesArtifact) {
token.addCardType(CardType.ARTIFACT);

View file

@ -43,32 +43,40 @@ public class CopyTokenFunction {
if (target == 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) {
// create token from another token
Token sourceToken = ((PermanentToken) source).getToken();
sourceObj = sourceToken;
target.setCopySourceCard(((PermanentToken) source).getToken().getCopySourceCard());
Token sourceObj = ((PermanentToken) source).getToken();
target.setCopySourceCard(sourceObj.getCopySourceCard()); // must link with original card
// main side
copyToToken(target, sourceObj, game);
CardUtil.copySetAndCardNumber(target, source);
if (sourceToken.getBackFace() != null) {
copyToToken(target.getBackFace(), sourceToken.getBackFace(), game);
CardUtil.copySetAndCardNumber(target.getBackFace(), sourceToken.getBackFace());
// second side
if (sourceObj.getBackFace() != null) {
copyToToken(target.getBackFace(), sourceObj.getBackFace(), game);
CardUtil.copySetAndCardNumber(target.getBackFace(), sourceObj.getBackFace());
}
return;
}
// from a permanent
// from non-token permanent
if (source instanceof PermanentCard) {
// create token from non-token permanent
// face down must hide all info
// apply face down status
PermanentCard sourcePermanent = (PermanentCard) source;
BecomesFaceDownCreatureEffect.FaceDownType faceDownType = BecomesFaceDownCreatureEffect.findFaceDownType(game, sourcePermanent);
if (faceDownType != null) {
@ -76,15 +84,18 @@ public class CopyTokenFunction {
return;
}
sourceObj = source.getMainCard();
target.setCopySourceCard((Card) sourceObj);
Card sourceObj = source.getMainCard();
target.setCopySourceCard(sourceObj);
// main side
copyToToken(target, sourceObj, game);
CardUtil.copySetAndCardNumber(target, sourceObj);
if (((Card) sourceObj).isTransformable()) {
copyToToken(target.getBackFace(), ((Card) sourceObj).getSecondCardFace(), game);
CardUtil.copySetAndCardNumber(target.getBackFace(), ((Card) sourceObj).getSecondCardFace());
// second side
if (sourceObj.isTransformable()) {
copyToToken(target.getBackFace(), sourceObj.getSecondCardFace(), game);
CardUtil.copySetAndCardNumber(target.getBackFace(), sourceObj.getSecondCardFace());
}
// apply prototyped status
if (((PermanentCard) source).isPrototyped()){
Abilities<Ability> abilities = source.getAbilities();
for (Ability ability : abilities){
@ -96,12 +107,13 @@ public class CopyTokenFunction {
return;
}
// from another object like card (example: Embalm ability)
sourceObj = source;
// from another card (example: Embalm ability)
Card sourceObj = CardUtil.getDefaultCardSideForBattlefield(game, source.getMainCard());
target.setCopySourceCard(source);
// main side
copyToToken(target, sourceObj, game);
CardUtil.copySetAndCardNumber(target, sourceObj);
// second side
if (source.isTransformable()) {
if (target.getBackFace() == null) {
// if you catch this then a weird use case here: card with single face copy another token with double face??