[WHO] implemented Disguise ability (new face down type, ability and token image, #10653)

This commit is contained in:
Oleg Agafonov 2024-03-01 11:41:05 +04:00
parent ab787a2b8b
commit 9ea3356b77
16 changed files with 313 additions and 41 deletions

View file

@ -429,6 +429,7 @@ public abstract class AbilityImpl implements Ability {
case BESTOW:
case MORPH:
case MEGAMORPH:
case DISGUISE:
// from Snapcaster Mage:
// If you cast a spell from a graveyard using its flashback ability, you can't pay other alternative costs
// (such as that of Foil). (2018-12-07)

View file

@ -56,7 +56,7 @@ public class BecomesFaceDownCreatureAllEffect extends ContinuousEffectImpl {
if (card != null) {
for (Ability ability : card.getAbilities(game)) {
if (ability instanceof MorphAbility) {
this.turnFaceUpAbilityMap.put(card.getId(), new TurnFaceUpAbility(((MorphAbility) ability).getMorphCosts()));
this.turnFaceUpAbilityMap.put(card.getId(), new TurnFaceUpAbility(((MorphAbility) ability).getFaceUpCosts()));
}
}
}

View file

@ -8,7 +8,9 @@ import mage.abilities.common.TurnFaceUpAbility;
import mage.abilities.costs.Cost;
import mage.abilities.costs.Costs;
import mage.abilities.costs.CostsImpl;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.ContinuousEffectImpl;
import mage.abilities.keyword.WardAbility;
import mage.cards.Card;
import mage.cards.CardImpl;
import mage.cards.repository.TokenInfo;
@ -26,9 +28,11 @@ import java.util.List;
import java.util.UUID;
/**
* Support different face down types: morph/manifest and disguise/cloak
* <p>
* This effect lets the card be a 2/2 face-down creature, with no text, no name,
* no subtypes, and no mana cost, if it's face down on the battlefield. And it
* adds the a TurnFaceUpAbility ability.
* adds the TurnFaceUpAbility and other additional abilities
* <p>
* Warning, if a card has multiple face down abilities then keep only one face up cost
* Example: Mischievous Quanar
@ -51,7 +55,7 @@ public class BecomesFaceDownCreatureEffect extends ContinuousEffectImpl {
}
protected int zoneChangeCounter;
protected Ability turnFaceUpAbility = null;
protected List<Ability> additionalAbilities = new ArrayList<>();
protected MageObjectReference objectReference = null;
protected boolean foundPermanent;
protected FaceDownType faceDownType;
@ -76,9 +80,18 @@ public class BecomesFaceDownCreatureEffect extends ContinuousEffectImpl {
super(duration, Layer.CopyEffects_1, SubLayer.FaceDownEffects_1b, Outcome.BecomeCreature);
this.objectReference = objectReference;
this.zoneChangeCounter = Integer.MIN_VALUE;
// additional abilities
// face up
if (turnFaceUpCosts != null) {
this.turnFaceUpAbility = new TurnFaceUpAbility(turnFaceUpCosts, faceDownType == FaceDownType.MEGAMORPHED);
this.additionalAbilities.add(new TurnFaceUpAbility(turnFaceUpCosts, faceDownType == FaceDownType.MEGAMORPHED));
}
// ward
if (faceDownType == FaceDownType.DISGUISED
|| faceDownType == faceDownType.CLOAKED) {
this.additionalAbilities.add(new WardAbility(new ManaCostsImpl<>("{2}")));
}
staticText = "{this} becomes a 2/2 face-down creature, with no text, no name, no subtypes, and no mana cost";
foundPermanent = false;
this.faceDownType = faceDownType;
@ -87,9 +100,9 @@ public class BecomesFaceDownCreatureEffect extends ContinuousEffectImpl {
protected BecomesFaceDownCreatureEffect(final BecomesFaceDownCreatureEffect effect) {
super(effect);
this.zoneChangeCounter = effect.zoneChangeCounter;
if (effect.turnFaceUpAbility != null) {
this.turnFaceUpAbility = effect.turnFaceUpAbility.copy();
}
effect.additionalAbilities.forEach(ability -> {
this.additionalAbilities.add(ability.copy());
});
this.objectReference = effect.objectReference;
this.foundPermanent = effect.foundPermanent;
this.faceDownType = effect.faceDownType;
@ -139,27 +152,33 @@ public class BecomesFaceDownCreatureEffect extends ContinuousEffectImpl {
foundPermanent = true;
switch (faceDownType) {
case MANIFESTED:
case MANUAL: // sets manifested image
case MANUAL: // sets manifested image // TODO: wtf
permanent.setManifested(true);
break;
case MORPHED:
case MEGAMORPHED:
permanent.setMorphed(true);
break;
case DISGUISED:
permanent.setDisguised(true);
break;
default:
throw new UnsupportedOperationException("FaceDownType not yet supported: " + faceDownType);
}
}
makeFaceDownObject(game, source.getSourceId(), permanent, faceDownType, this.turnFaceUpAbility);
makeFaceDownObject(game, source.getSourceId(), permanent, faceDownType, this.additionalAbilities);
} else if (duration == Duration.Custom && foundPermanent) {
discard();
}
return true;
}
// TODO: implement multiple face down types?!
public static FaceDownType findFaceDownType(Game game, Permanent permanent) {
if (permanent.isMorphed()) {
return BecomesFaceDownCreatureEffect.FaceDownType.MORPHED;
} else if (permanent.isDisguised()) {
return BecomesFaceDownCreatureEffect.FaceDownType.DISGUISED;
} else if (permanent.isManifested()) {
return BecomesFaceDownCreatureEffect.FaceDownType.MANIFESTED;
} else if (permanent.isFaceDown(game)) {
@ -172,7 +191,7 @@ public class BecomesFaceDownCreatureEffect extends ContinuousEffectImpl {
/**
* Convert any object (card, token) to face down (remove/hide all face up information and make it a 2/2 creature)
*/
public static void makeFaceDownObject(Game game, UUID sourceId, MageObject object, FaceDownType faceDownType, Ability turnFaceUpAbility) {
public static void makeFaceDownObject(Game game, UUID sourceId, MageObject object, FaceDownType faceDownType, List<Ability> additionalAbilities) {
String originalObjectInfo = object.toString();
// warning, it's a direct changes to the object (without game state, so no game param here)
@ -200,38 +219,45 @@ public class BecomesFaceDownCreatureEffect extends ContinuousEffectImpl {
//
// so keep all tune face up abilities and other face down compatible
if (ability.getWorksFaceDown()) {
// only face up abilities hidden by default (see below), so no needs in setRuleVisible
//ability.setRuleVisible(true);
// keep face down abilities active, but hide it from rules description
// example: When Dog Walker is turned face up, create two tapped 1/1 white Dog creature tokens
ability.setRuleVisible(false);
// but do not hide default ability (becomes a 2/2 face-down creature)
if (!ability.getRuleVisible() && !ability.getEffects().isEmpty()) {
if (ability.getEffects().get(0) instanceof BecomesFaceDownCreatureEffect) {
ability.setRuleVisible(true);
}
}
continue;
}
if (!ability.getRuleVisible() && !ability.getEffects().isEmpty()) {
if (ability.getEffects().get(0) instanceof BecomesFaceDownCreatureEffect) {
continue;
}
}
// all other can be removed
abilitiesToRemove.add(ability);
}
// add face up abilities
// TODO: add here all possible face up like morph/disguis, manifest/cloak?
// add additional abilities like face up
if (object instanceof Permanent) {
// as permanent
Permanent permanentObject = (Permanent) object;
permanentObject.removeAbilities(abilitiesToRemove, sourceId, game);
if (turnFaceUpAbility != null) {
Ability faceUp = turnFaceUpAbility.copy();
faceUp.setRuleVisible(true);
permanentObject.addAbility(faceUp, sourceId, game);
if (additionalAbilities != null) {
additionalAbilities.forEach(blueprintAbility -> {
Ability newAbility = blueprintAbility.copy();
newAbility.setRuleVisible(true);
permanentObject.addAbility(newAbility, sourceId, game);
});
}
} else if (object instanceof CardImpl) {
// as card
CardImpl cardObject = (CardImpl) object;
cardObject.getAbilities().removeAll(abilitiesToRemove);
if (turnFaceUpAbility != null) {
Ability faceUp = turnFaceUpAbility.copy();
faceUp.setRuleVisible(true);
cardObject.addAbility(faceUp);
if (additionalAbilities != null) {
additionalAbilities.forEach(blueprintAbility -> {
Ability newAbility = blueprintAbility.copy();
newAbility.setRuleVisible(true);
cardObject.addAbility(newAbility);
});
}
}
@ -247,15 +273,15 @@ public class BecomesFaceDownCreatureEffect extends ContinuousEffectImpl {
case MEGAMORPHED:
tokenName = TokenRepository.XMAGE_IMAGE_NAME_FACE_DOWN_MEGAMORPH;
break;
case DISGUISED:
tokenName = TokenRepository.XMAGE_IMAGE_NAME_FACE_DOWN_DISGUISE;
break;
case MANIFESTED:
tokenName = TokenRepository.XMAGE_IMAGE_NAME_FACE_DOWN_MANIFEST;
break;
case CLOAKED:
tokenName = "TODO-CLOAKED";
break;
case DISGUISED:
tokenName = "TODO-DISGUISED";
break;
case MANUAL:
tokenName = TokenRepository.XMAGE_IMAGE_NAME_FACE_DOWN_MANUAL;
break;

View file

@ -1,22 +1,91 @@
package mage.abilities.keyword;
import mage.abilities.Ability;
import mage.abilities.SpellAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.costs.Cost;
import mage.abilities.costs.Costs;
import mage.abilities.costs.CostsImpl;
import mage.abilities.costs.mana.GenericManaCost;
import mage.abilities.costs.mana.ManaCost;
import mage.abilities.effects.common.continuous.BecomesFaceDownCreatureEffect;
import mage.cards.Card;
import mage.constants.SpellAbilityCastMode;
import mage.constants.SpellAbilityType;
import mage.constants.TimingRule;
/**
* @author TheElk801
* TODO: Implement this
* 702.168. Disguise
* <p>
* 702.168a
* Disguise is a static ability that functions in any zone from which you could play the card its on,
* and the disguise effect works any time the card is face down. Disguise [cost] means You may cast this
* card as a 2/2 face-down creature with ward {2}, no name, no subtypes, and no mana cost by paying {3} rather
* than paying its mana cost. (See rule 708, Face-Down Spells and Permanents.)
* <p>
* 702.168b
* To cast a card using its disguise ability, turn the card face down and announce that you are using a disguise ability.
* It becomes a 2/2 face-down creature card with ward {2}, no name, no subtypes, and no mana cost. Any effects or
* prohibitions that would apply to casting a card with these characteristics (and not the face-up cards characteristics)
* are applied to casting this card. These values are the copiable values of that objects characteristics.
* (See rule 613, Interaction of Continuous Effects, and rule 707, Copying Objects.) Put it onto the stack
* (as a face-down spell with the same characteristics), and pay {3} rather than pay its mana cost. This follows the
* rules for paying alternative costs. You can use a disguise ability to cast a card from any zone from which you
* could normally cast it. When the spell resolves, it enters the battlefield with the same characteristics the spell
* had. The disguise effect applies to the face-down object wherever it is, and it ends when the permanent is turned
* face up.
* <p>
* 702.168c
* You cant normally cast a card face down. A disguise ability allows you to do so.
* <p>
* 702.168d
* Any time you have priority, you may turn a face-down permanent you control with a disguise ability face up.
* This is a special action; it doesnt use the stack (see rule 116). To do this, show all players what the
* permanents disguise cost would be if it were face up, pay that cost, then turn the permanent face up.
* (If the permanent wouldnt have a disguise cost if it were face up, it cant be turned face up this way.)
* The disguise effect on it ends, and it regains its normal characteristics. Any abilities relating to the
* permanent entering the battlefield dont trigger when its turned face up and dont have any effect,
* because the permanent has already entered the battlefield.
* <p>
* 702.168e
* If a permanents disguise cost includes X, other abilities of that permanent may also refer to X.
* The value of X in those abilities is equal to the value of X chosen as the disguise special action was taken.
* <p>
* 702.168f
* See rule 708, Face-Down Spells and Permanents, for more information about how to cast cards with a disguise ability.
* <p>
* <p>
* MorphAbility as a reference implementation
*
* @author JayDi85
*/
public class DisguiseAbility extends SpellAbility {
protected static final String ABILITY_KEYWORD = "Disguise";
protected static final String REMINDER_TEXT = "You may cast this card face down for {3} as a 2/2 creature with "
+ "ward {2}. Turn it face up any time for its disguise cost.";
protected Costs<Cost> disguiseCosts;
public DisguiseAbility(Card card, Cost disguiseCost) {
super(new GenericManaCost(3), card.getName());
this.timing = TimingRule.SORCERY;
this.disguiseCosts = new CostsImpl<>();
this.disguiseCosts.add(disguiseCost);
this.setSpellAbilityCastMode(SpellAbilityCastMode.DISGUISE);
this.setSpellAbilityType(SpellAbilityType.BASE_ALTERNATE);
// face down effect (hidden by default, visible in face down objects)
Ability ability = new SimpleStaticAbility(new BecomesFaceDownCreatureEffect(
this.disguiseCosts, BecomesFaceDownCreatureEffect.FaceDownType.DISGUISED));
ability.setWorksFaceDown(true);
ability.setRuleVisible(false);
addSubAbility(ability);
}
private DisguiseAbility(final DisguiseAbility ability) {
super(ability);
this.disguiseCosts = ability.disguiseCosts; // can't be changed TODO: looks buggy, need research
}
@Override
@ -24,8 +93,16 @@ public class DisguiseAbility extends SpellAbility {
return new DisguiseAbility(this);
}
public Costs<Cost> getFaceUpCosts() {
return this.disguiseCosts;
}
@Override
public String getRule() {
return "Disguise";
boolean isMana = disguiseCosts.get(0) instanceof ManaCost;
return ABILITY_KEYWORD + (isMana ? " " : "&mdash;")
+ this.disguiseCosts.getText()
+ (isMana ? ' ' : ". ")
+ " <i>(" + REMINDER_TEXT + ")</i>";
}
}

View file

@ -84,7 +84,7 @@ public class MorphAbility extends SpellAbility {
// face down effect (hidden by default, visible in face down objects)
Ability ability = new SimpleStaticAbility(new BecomesFaceDownCreatureEffect(
morphCosts, (useMegamorph ? FaceDownType.MEGAMORPHED : FaceDownType.MORPHED)));
this.morphCosts, (useMegamorph ? FaceDownType.MEGAMORPHED : FaceDownType.MORPHED)));
ability.setWorksFaceDown(true);
ability.setRuleVisible(false);
addSubAbility(ability);
@ -92,7 +92,7 @@ public class MorphAbility extends SpellAbility {
protected MorphAbility(final MorphAbility ability) {
super(ability);
this.morphCosts = ability.morphCosts; // can't be changed
this.morphCosts = ability.morphCosts; // can't be changed TODO: looks buggy, need research
}
@Override
@ -100,8 +100,8 @@ public class MorphAbility extends SpellAbility {
return new MorphAbility(this);
}
public Costs<Cost> getMorphCosts() {
return morphCosts;
public Costs<Cost> getFaceUpCosts() {
return this.morphCosts;
}
@Override

View file

@ -59,6 +59,7 @@ public interface Card extends MageObject, Ownerable {
boolean turnFaceUp(Ability source, Game game, UUID playerId);
// TODO: need research, is it lost morph and other face down statuses?
boolean turnFaceDown(Ability source, Game game, UUID playerId);
boolean isFlipCard();

View file

@ -25,6 +25,7 @@ public enum TokenRepository {
public static final String XMAGE_IMAGE_NAME_FACE_DOWN_MANIFEST = "Manifest";
public static final String XMAGE_IMAGE_NAME_FACE_DOWN_MORPH = "Morph";
public static final String XMAGE_IMAGE_NAME_FACE_DOWN_MEGAMORPH = "Megamorph";
public static final String XMAGE_IMAGE_NAME_FACE_DOWN_DISGUISE = "Disguise";
public static final String XMAGE_IMAGE_NAME_FACE_DOWN_FORETELL = "Foretell";
public static final String XMAGE_IMAGE_NAME_COPY = "Copy";
public static final String XMAGE_IMAGE_NAME_CITY_BLESSING = "City's Blessing";
@ -288,6 +289,10 @@ public enum TokenRepository {
res.add(createXmageToken(XMAGE_IMAGE_NAME_FACE_DOWN_MEGAMORPH, 2, "https://api.scryfall.com/cards/ta25/15/en?format=image"));
res.add(createXmageToken(XMAGE_IMAGE_NAME_FACE_DOWN_MEGAMORPH, 3, "https://api.scryfall.com/cards/tc19/27/en?format=image"));
// Disguise
// 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"));
// 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

@ -22,12 +22,15 @@ public enum SpellAbilityCastMode {
PROTOTYPE("Prototype"),
MORPH("Morph", false, true, SpellAbilityCastMode.MORPH_ADDITIONAL_RULE),
MEGAMORPH("Megamorph", false, true, SpellAbilityCastMode.MORPH_ADDITIONAL_RULE),
DISGUISE("Disguise", false, true, SpellAbilityCastMode.DISGUISE_ADDITIONAL_RULE),
TRANSFORMED("Transformed", true),
DISTURB("Disturb", true),
MORE_THAN_MEETS_THE_EYE("More than Meets the Eye", true);
private static final String MORPH_ADDITIONAL_RULE = "You may cast this card as a 2/2 face-down creature, with no text,"
+ " no name, no subtypes, and no mana cost by paying {3} rather than paying its mana cost.";
private static final String DISGUISE_ADDITIONAL_RULE = "You may cast this card face down for {3} as a 2/2 creature with "
+ "ward {2}. Turn it face up any time for its disguise cost.";
private final String text;
@ -90,12 +93,18 @@ public enum SpellAbilityCastMode {
break;
case MORPH:
case MEGAMORPH:
case DISGUISE:
if (cardCopy instanceof Spell) {
//Spell doesn't support setName, so make a copy of the card (we're blowing it away anyway)
// TODO: research - is it possible to apply face down code to spell instead workaround with card
cardCopy = ((Spell) cardCopy).getCard().copy();
}
BecomesFaceDownCreatureEffect.makeFaceDownObject(game, null, cardCopy, BecomesFaceDownCreatureEffect.FaceDownType.MORPHED, null);
BecomesFaceDownCreatureEffect.FaceDownType faceDownType = BecomesFaceDownCreatureEffect.FaceDownType.MORPHED;
if (this == DISGUISE) {
faceDownType = BecomesFaceDownCreatureEffect.FaceDownType.DISGUISED;
}
// no needs in additional abilities for spell
BecomesFaceDownCreatureEffect.makeFaceDownObject(game, null, cardCopy, faceDownType, null);
break;
case NORMAL:
case MADNESS:

View file

@ -442,6 +442,10 @@ public interface Permanent extends Card, Controllable {
boolean isMorphed();
void setDisguised(boolean value);
boolean isDisguised();
void setManifested(boolean value);
boolean isManifested();

View file

@ -176,10 +176,12 @@ public class PermanentCard extends PermanentImpl {
@Override
public boolean turnFaceUp(Ability source, Game game, UUID playerId) {
if (super.turnFaceUp(source, game, playerId)) {
// TODO: miss types, abilities, color and other things for restore?!
power.setModifiedBaseValue(power.getBaseValue());
toughness.setModifiedBaseValue(toughness.getBaseValue());
setManifested(false);
setMorphed(false);
setDisguised(false);
return true;
}
return false;

View file

@ -74,6 +74,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
protected boolean suspected;
protected boolean manifested = false;
protected boolean morphed = false;
protected boolean disguised = false;
protected boolean ringBearerFlag = false;
protected boolean canBeSacrificed = true;
protected int classLevel = 1;
@ -186,6 +187,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
this.protectorId = permanent.protectorId;
this.morphed = permanent.morphed;
this.disguised = permanent.disguised;
this.manifested = permanent.manifested;
this.createOrder = permanent.createOrder;
this.prototyped = permanent.prototyped;
@ -1908,6 +1910,16 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
morphed = value;
}
@Override
public boolean isDisguised() {
return disguised;
}
@Override
public void setDisguised(boolean value) {
disguised = value;
}
@Override
public void setRarity(Rarity rarity) {
this.rarity = rarity;

View file

@ -68,7 +68,7 @@ public class CopyTokenFunction {
if (source instanceof PermanentCard) {
// create token from non-token permanent
// morph/manifest must hide all info
// face down must hide all info
PermanentCard sourcePermanent = (PermanentCard) source;
BecomesFaceDownCreatureEffect.FaceDownType faceDownType = BecomesFaceDownCreatureEffect.findFaceDownType(game, sourcePermanent);
if (faceDownType != null) {