implement [MKM] Cryptic Coat (#12164) and Cloak ability

This commit is contained in:
Susucre 2024-06-06 12:47:07 +02:00 committed by GitHub
parent 8172616449
commit ab280ad2ba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 235 additions and 29 deletions

View file

@ -33,6 +33,7 @@ public class PermanentView extends CardView {
private final boolean morphed;
private final boolean disguised;
private final boolean manifested;
private final boolean cloaked;
private final boolean attachedToPermanent;
// If this card is attached to a permanent which is controlled by a player other than the one which controls this permanent
private final boolean attachedControllerDiffers;
@ -47,6 +48,7 @@ public class PermanentView extends CardView {
this.morphed = permanent.isMorphed();
this.disguised = permanent.isDisguised();
this.manifested = permanent.isManifested();
this.cloaked = permanent.isCloaked();
this.damage = permanent.getDamage();
this.attachments = new ArrayList<>(permanent.getAttachments());
this.attachedTo = permanent.getAttachedTo();
@ -143,6 +145,7 @@ public class PermanentView extends CardView {
this.morphed = permanentView.morphed;
this.disguised = permanentView.disguised;
this.manifested = permanentView.manifested;
this.cloaked = permanentView.cloaked;
this.attachedToPermanent = permanentView.attachedToPermanent;
this.attachedControllerDiffers = permanentView.attachedControllerDiffers;
}
@ -222,4 +225,8 @@ public class PermanentView extends CardView {
public boolean isManifested() {
return manifested;
}
public boolean isCloaked() {
return cloaked;
}
}

View file

@ -0,0 +1,97 @@
package mage.cards.c;
import mage.abilities.Ability;
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.AttachEffect;
import mage.abilities.effects.common.ReturnToHandSourceEffect;
import mage.abilities.effects.common.combat.CantBeBlockedAttachedEffect;
import mage.abilities.effects.common.continuous.BoostEquippedEffect;
import mage.abilities.effects.keyword.ManifestEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.AttachmentType;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.constants.SubType;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.targetpointer.FixedTarget;
import java.util.List;
import java.util.UUID;
/**
* @author Susucr
*/
public final class CrypticCoat extends CardImpl {
public CrypticCoat(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{2}{U}");
this.subtype.add(SubType.EQUIPMENT);
// When Cryptic Coat enters the battlefield, cloak the top card of your library, then attach Cryptic Coat to it.
this.addAbility(new EntersBattlefieldTriggeredAbility(new CrypticCoatEffect()));
// Equipped creature gets +1/+0 and can't be blocked.
Ability ability = new SimpleStaticAbility(new BoostEquippedEffect(1, 0));
ability.addEffect(new CantBeBlockedAttachedEffect(AttachmentType.EQUIPMENT)
.setText("and can't be blocked")
);
this.addAbility(ability);
// {1}{U}: Return Cryptic Coat to its owner's hand.
this.addAbility(new SimpleActivatedAbility(new ReturnToHandSourceEffect(), new ManaCostsImpl<>("{1}{U}")));
}
private CrypticCoat(final CrypticCoat card) {
super(card);
}
@Override
public CrypticCoat copy() {
return new CrypticCoat(this);
}
}
class CrypticCoatEffect extends OneShotEffect {
CrypticCoatEffect() {
super(Outcome.PutCreatureInPlay);
staticText = "cloak the top card of your library, then attach {this} to it";
}
private CrypticCoatEffect(final CrypticCoatEffect effect) {
super(effect);
}
@Override
public CrypticCoatEffect copy() {
return new CrypticCoatEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId());
if (controller == null) {
return false;
}
List<Permanent> cloakedList = ManifestEffect.doManifestCards(
game, source, controller,
controller.getLibrary().getTopCards(game, 1), true
);
if (cloakedList.isEmpty()) {
return false;
}
Permanent cloaked = cloakedList.get(0);
new AttachEffect(Outcome.BoostCreature, "attach {this} to it")
.setTargetPointer(new FixedTarget(cloaked, game))
.apply(game, source);
return true;
}
}

View file

@ -79,6 +79,6 @@ class OrochiSoulReaverManifestEffect extends OneShotEffect {
return false;
}
return ManifestEffect.doManifestCards(game, source, controller, targetPlayer.getLibrary().getTopCards(game, 1));
return !ManifestEffect.doManifestCards(game, source, controller, targetPlayer.getLibrary().getTopCards(game, 1)).isEmpty();
}
}

View file

@ -67,6 +67,6 @@ class ScrollOfFateEffect extends OneShotEffect {
return false;
}
return ManifestEffect.doManifestCards(game, source, controller, new CardsImpl(targetCard.getTargets()).getCards(game));
return !ManifestEffect.doManifestCards(game, source, controller, new CardsImpl(targetCard.getTargets()).getCards(game)).isEmpty();
}
}

View file

@ -86,7 +86,7 @@ class ThievingAmalgamManifestEffect extends OneShotEffect {
return false;
}
return ManifestEffect.doManifestCards(game, source, controller, targetPlayer.getLibrary().getTopCards(game, 1));
return !ManifestEffect.doManifestCards(game, source, controller, targetPlayer.getLibrary().getTopCards(game, 1)).isEmpty();
}
}

View file

@ -162,6 +162,7 @@ class VesuvanShapeshifterFaceDownEffect extends OneShotEffect {
permanent.turnFaceDown(source, game, source.getControllerId());
permanent.setManifested(false);
permanent.setDisguised(false);
permanent.setCloaked(false);
permanent.setMorphed(true); // cause it morph card TODO: smells bad
return permanent.isFaceDown(game);

View file

@ -92,6 +92,7 @@ public final class MurdersAtKarlovManor extends ExpansionSet {
cards.add(new SetCardInfo("Crimestopper Sprite", 49, Rarity.COMMON, mage.cards.c.CrimestopperSprite.class));
cards.add(new SetCardInfo("Crowd-Control Warden", 193, Rarity.COMMON, mage.cards.c.CrowdControlWarden.class));
cards.add(new SetCardInfo("Cryptex", 251, Rarity.RARE, mage.cards.c.Cryptex.class));
cards.add(new SetCardInfo("Cryptic Coat", 50, Rarity.RARE, mage.cards.c.CrypticCoat.class));
cards.add(new SetCardInfo("Culvert Ambusher", 158, Rarity.UNCOMMON, mage.cards.c.CulvertAmbusher.class));
cards.add(new SetCardInfo("Curious Cadaver", 194, Rarity.UNCOMMON, mage.cards.c.CuriousCadaver.class));
cards.add(new SetCardInfo("Curious Inquiry", 51, Rarity.UNCOMMON, mage.cards.c.CuriousInquiry.class));

View file

@ -0,0 +1,50 @@
package org.mage.test.cards.single.mkm;
import mage.constants.EmptyNames;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* @author Susucr
*/
public class CrypticCoatTest extends CardTestPlayerBase {
/**
* {@link mage.cards.c.CrypticCoat Cryptic Coat} {2}{U}
* Artifact Equipment
* When Cryptic Coat enters the battlefield, cloak the top card of your library, then attach Cryptic Coat to it. (To cloak a card, put it onto the battlefield face down as a 2/2 creature with ward {2}. Turn it face up any time for its mana cost if its a creature card.)
* Equipped creature gets +1/+0 and cant be blocked.
* {1}{U}: Return Cryptic Coat to its owners hand.
*/
private static final String coat = "Cryptic Coat";
@Test
public void test_CloakCreature() {
skipInitShuffling();
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, "Island", 6);
addCard(Zone.HAND, playerA, coat);
addCard(Zone.LIBRARY, playerA, "Ancient Crab");
addCard(Zone.HAND, playerB, "Lightning Bolt");
addCard(Zone.BATTLEFIELD, playerB, "Mountain", 3);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, coat);
checkPT("Cloaked is 3/2", 1, PhaseStep.BEGIN_COMBAT, playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 3, 2);
castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Lightning Bolt", EmptyNames.FACE_DOWN_CREATURE.toString());
setChoice(playerB, true); // pay for ward
activateAbility(2, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{U}{U}: Turn this face-down permanent face up.");
setStopAt(2, PhaseStep.BEGIN_COMBAT);
execute();
assertPowerToughness(playerA, "Ancient Crab", 1 + 1, 5);
assertDamageReceived(playerA, "Ancient Crab", 3);
assertTappedCount("Mountain", true, 3);
assertTappedCount("Island", true, 6);
}
}

View file

@ -111,32 +111,36 @@ public class BecomesFaceDownCreatureEffect extends ContinuousEffectImpl {
new TurnFaceUpAbility(turnFaceUpCosts, faceDownType == FaceDownType.MEGAMORPHED)
.setCostAdjuster(costAdjuster)
);
}
switch (faceDownType) {
case MORPHED:
case MEGAMORPHED:
switch (faceDownType) {
case MORPHED:
case MEGAMORPHED:
if (turnFaceUpCosts != null) {
// face up rules replace for cost hide
this.additionalAbilities.add(new SimpleStaticAbility(Zone.ALL, new InfoEffect(
"Turn it face up any time for its morph cost."
)));
break;
case DISGUISED:
case CLOAKED:
// ward
this.additionalAbilities.add(new WardAbility(new ManaCostsImpl<>("{2}")));
}
break;
case DISGUISED:
case CLOAKED:
// Ward {2} -- should not be dependent on turnFaceUpCosts.
this.additionalAbilities.add(new WardAbility(new ManaCostsImpl<>("{2}")));
if (turnFaceUpCosts != null) {
// face up rules replace for cost hide
this.additionalAbilities.add(new SimpleStaticAbility(Zone.ALL, new InfoEffect(
"Turn it face up any time for its disguise/cloaked cost."
)));
break;
case MANUAL:
case MANIFESTED:
// no face up abilities
break;
default:
throw new IllegalArgumentException("Un-supported face down type: " + faceDownType);
}
}
break;
case MANUAL:
case MANIFESTED:
// no face up abilities
break;
default:
throw new IllegalArgumentException("Un-supported face down type: " + faceDownType);
}
staticText = "{this} becomes a 2/2 face-down creature, with no text, no name, no subtypes, and no mana cost";
@ -209,6 +213,9 @@ public class BecomesFaceDownCreatureEffect extends ContinuousEffectImpl {
case DISGUISED:
permanent.setDisguised(true);
break;
case CLOAKED:
permanent.setCloaked(true);
break;
default:
throw new UnsupportedOperationException("FaceDownType not yet supported: " + faceDownType);
}
@ -228,6 +235,8 @@ public class BecomesFaceDownCreatureEffect extends ContinuousEffectImpl {
return BecomesFaceDownCreatureEffect.FaceDownType.DISGUISED;
} else if (permanent.isManifested()) {
return BecomesFaceDownCreatureEffect.FaceDownType.MANIFESTED;
} else if (permanent.isCloaked()) {
return BecomesFaceDownCreatureEffect.FaceDownType.CLOAKED;
} else if (permanent.isFaceDown(game)) {
return BecomesFaceDownCreatureEffect.FaceDownType.MANUAL;
} else {
@ -326,7 +335,7 @@ public class BecomesFaceDownCreatureEffect extends ContinuousEffectImpl {
tokenName = TokenRepository.XMAGE_IMAGE_NAME_FACE_DOWN_MANIFEST;
break;
case CLOAKED:
tokenName = "TODO-CLOAKED";
tokenName = TokenRepository.XMAGE_IMAGE_NAME_FACE_DOWN_CLOAK;
break;
case MANUAL:
tokenName = TokenRepository.XMAGE_IMAGE_NAME_FACE_DOWN_MANUAL;

View file

@ -18,7 +18,7 @@ import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.util.CardUtil;
import java.util.Set;
import java.util.*;
/**
* Manifest
@ -65,6 +65,7 @@ public class ManifestEffect extends OneShotEffect {
private final DynamicValue amount;
private final boolean isPlural;
private final boolean cloakNotManifest;
public ManifestEffect(int amount) {
this(StaticValue.get(amount), amount > 1);
@ -75,9 +76,14 @@ public class ManifestEffect extends OneShotEffect {
}
private ManifestEffect(DynamicValue amount, boolean isPlural) {
this(amount, isPlural, false);
}
private ManifestEffect(DynamicValue amount, boolean isPlural, boolean cloakNotManifest) {
super(Outcome.PutCreatureInPlay);
this.amount = amount;
this.isPlural = isPlural;
this.cloakNotManifest = cloakNotManifest;
this.staticText = setText();
}
@ -85,6 +91,7 @@ public class ManifestEffect extends OneShotEffect {
super(effect);
this.amount = effect.amount;
this.isPlural = effect.isPlural;
this.cloakNotManifest = effect.cloakNotManifest;
}
@Override
@ -100,12 +107,16 @@ public class ManifestEffect extends OneShotEffect {
}
int manifestAmount = amount.calculate(game, source, this);
return doManifestCards(game, source, controller, controller.getLibrary().getTopCards(game, manifestAmount));
return !doManifestCards(game, source, controller, controller.getLibrary().getTopCards(game, manifestAmount), cloakNotManifest).isEmpty();
}
public static boolean doManifestCards(Game game, Ability source, Player manifestPlayer, Set<Card> cardsToManifest) {
public static List<Permanent> doManifestCards(Game game, Ability source, Player manifestPlayer, Set<Card> cardsToManifest) {
return doManifestCards(game, source, manifestPlayer, cardsToManifest, false);
}
public static List<Permanent> doManifestCards(Game game, Ability source, Player manifestPlayer, Set<Card> cardsToManifest, boolean cloakNotManifest) {
if (cardsToManifest.isEmpty()) {
return false;
return Collections.emptyList();
}
// prepare source ability
@ -132,9 +143,10 @@ public class ManifestEffect extends OneShotEffect {
// zcc + 1 for use case with Rally the Ancestors (see related test)
MageObjectReference objectReference = new MageObjectReference(battlefieldCard.getId(), battlefieldCard.getZoneChangeCounter(game) + 1, game);
game.addEffect(new BecomesFaceDownCreatureEffect(manaCosts, objectReference, Duration.Custom, FaceDownType.MANIFESTED), newSource);
game.addEffect(new BecomesFaceDownCreatureEffect(manaCosts, objectReference, Duration.Custom, cloakNotManifest ? FaceDownType.CLOAKED : FaceDownType.MANIFESTED), newSource);
}
List<Permanent> manifested = new ArrayList<>();
// move cards to battlefield as face down
// TODO: possible buggy for multiple cards, see rule 701.34e - it require manifest one by one (card to check: Omarthis, Ghostfire Initiate)
manifestPlayer.moveCards(cardsToManifest, Zone.BATTLEFIELD, source, game, false, true, false, null);
@ -145,18 +157,25 @@ public class ManifestEffect extends OneShotEffect {
if (permanent != null) {
// TODO: permanent already has manifested status, so code can be deleted later
// TODO: add test with battlefield trigger/watcher (must not see normal card, must not see face down status without manifest)
permanent.setManifested(true);
if (cloakNotManifest) {
permanent.setCloaked(true);
} else {
permanent.setManifested(true);
}
manifested.add(permanent);
} else {
// TODO: looks buggy, card can't be moved to battlefield, but face down effect already active
// or it can be face down on another move to battlefield
}
}
return true;
return manifested;
}
private String setText() {
StringBuilder sb = new StringBuilder("manifest the top ");
StringBuilder sb = new StringBuilder();
sb.append(cloakNotManifest ? "cloak" : "manifest");
sb.append(" the top ");
if (isPlural) {
sb.append(CardUtil.numberToText(amount.toString())).append(" cards ");
} else {

View file

@ -40,7 +40,7 @@ public class ManifestTargetPlayerEffect extends OneShotEffect {
return false;
}
return ManifestEffect.doManifestCards(game, source, targetPlayer, targetPlayer.getLibrary().getTopCards(game, amount));
return !ManifestEffect.doManifestCards(game, source, targetPlayer, targetPlayer.getLibrary().getTopCards(game, amount)).isEmpty();
}
private String setText() {

View file

@ -23,6 +23,7 @@ public enum TokenRepository {
// - additional card name for controller like "Morph: face up name"
public static final String XMAGE_IMAGE_NAME_FACE_DOWN_MANUAL = "Face Down";
public static final String XMAGE_IMAGE_NAME_FACE_DOWN_MANIFEST = "Manifest";
public static final String XMAGE_IMAGE_NAME_FACE_DOWN_CLOAK = "Cloak";
public static final String XMAGE_IMAGE_NAME_FACE_DOWN_MORPH = "Morph";
public static final String XMAGE_IMAGE_NAME_FACE_DOWN_DISGUISE = "Disguise";
public static final String XMAGE_IMAGE_NAME_FACE_DOWN_FORETELL = "Foretell";
@ -287,6 +288,10 @@ public enum TokenRepository {
// support only 1 image: https://scryfall.com/card/tmkm/21/a-mysterious-creature
res.add(createXmageToken(XMAGE_IMAGE_NAME_FACE_DOWN_DISGUISE, 1, "https://api.scryfall.com/cards/tmkm/21/en?format=image"));
// Cloak
// support only 1 image: https://scryfall.com/card/tmkm/21/a-mysterious-creature
res.add(createXmageToken(XMAGE_IMAGE_NAME_FACE_DOWN_CLOAK, 1, "https://api.scryfall.com/cards/tmkm/21/en?format=image"));
// Foretell
// https://scryfall.com/search?q=Foretell+unique%3Aprints+otag%3Aassistant-cards&unique=cards&as=grid&order=name
res.add(createXmageToken(XMAGE_IMAGE_NAME_FACE_DOWN_FORETELL, 1, "https://api.scryfall.com/cards/tkhm/23/en?format=image"));

View file

@ -450,6 +450,10 @@ public interface Permanent extends Card, Controllable {
boolean isManifested();
void setCloaked(boolean value);
boolean isCloaked();
boolean isRingBearer();
void setRingBearer(Game game, boolean value);

View file

@ -188,6 +188,7 @@ public class PermanentCard extends PermanentImpl {
setManifested(false);
setMorphed(false);
setDisguised(false);
setCloaked(false);
return true;
}
return false;

View file

@ -73,6 +73,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
protected boolean renowned;
protected boolean suspected;
protected boolean manifested = false;
protected boolean cloaked = false;
protected boolean morphed = false;
protected boolean disguised = false;
protected boolean ringBearerFlag = false;
@ -189,6 +190,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
this.morphed = permanent.morphed;
this.disguised = permanent.disguised;
this.manifested = permanent.manifested;
this.cloaked = permanent.cloaked;
this.createOrder = permanent.createOrder;
this.prototyped = permanent.prototyped;
this.canBeSacrificed = permanent.canBeSacrificed;
@ -1905,6 +1907,16 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
manifested = value;
}
@Override
public boolean isCloaked() {
return cloaked;
}
@Override
public void setCloaked(boolean value) {
cloaked = value;
}
@Override
public boolean isMorphed() {
return morphed;