mirror of
https://github.com/magefree/mage.git
synced 2025-12-20 10:40:06 -08:00
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:
parent
0e39d6a833
commit
6b86b1053f
3 changed files with 168 additions and 24 deletions
|
|
@ -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 Tergrid’s 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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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??
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue