From ff3ea852b77be1aba32560e7e7fe55fe145c1b3c Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Sat, 20 Jan 2024 22:13:02 +0100 Subject: [PATCH] CardFactoryUtil: Disguise Keyword --- .../src/main/java/forge/ai/GameState.java | 6 + .../java/forge/ai/simulation/GameCopier.java | 3 + forge-core/src/main/java/forge/ImageKeys.java | 4 +- .../src/main/java/forge/game/GameAction.java | 4 +- .../game/ability/effects/PlayEffect.java | 2 +- .../game/ability/effects/SetStateEffect.java | 10 +- .../src/main/java/forge/game/card/Card.java | 149 +++++++++++------- .../java/forge/game/card/CardFactoryUtil.java | 70 ++++++-- .../java/forge/game/card/CardProperty.java | 6 +- .../main/java/forge/game/card/CardState.java | 15 ++ .../main/java/forge/game/card/CardView.java | 13 +- .../main/java/forge/game/keyword/Keyword.java | 1 + .../forge/game/spellability/SpellAbility.java | 12 +- .../forge/trackable/TrackableProperty.java | 1 + 14 files changed, 215 insertions(+), 81 deletions(-) diff --git a/forge-ai/src/main/java/forge/ai/GameState.java b/forge-ai/src/main/java/forge/ai/GameState.java index b51591d63d4..8b48d675994 100644 --- a/forge-ai/src/main/java/forge/ai/GameState.java +++ b/forge-ai/src/main/java/forge/ai/GameState.java @@ -310,6 +310,9 @@ public abstract class GameState { if (c.isManifested()) { newText.append(":Manifested"); } + if (c.isCloaked()) { + newText.append(":Cloaked"); + } } if (c.getCurrentStateName().equals(CardStateName.Transformed)) { newText.append("|Transformed"); @@ -1280,6 +1283,9 @@ public abstract class GameState { if (info.endsWith("Manifested")) { c.setManifested(true); } + if (info.endsWith("Cloaked")) { + c.setCloaked(true); + } } else if (info.startsWith("Transformed")) { c.setState(CardStateName.Transformed, true); c.setBackSide(true); diff --git a/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java b/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java index 29e83ed8f75..16d8a0ef7af 100644 --- a/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java +++ b/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java @@ -389,6 +389,9 @@ public class GameCopier { if (c.isManifested()) { newCard.setManifested(true); } + if (c.isCloaked()) { + newCard.setCloaked(true); + } } if (c.isMonstrous()) { newCard.setMonstrous(true); diff --git a/forge-core/src/main/java/forge/ImageKeys.java b/forge-core/src/main/java/forge/ImageKeys.java index c0285c51b15..587880425e1 100644 --- a/forge-core/src/main/java/forge/ImageKeys.java +++ b/forge-core/src/main/java/forge/ImageKeys.java @@ -23,7 +23,9 @@ public final class ImageKeys { public static final String HIDDEN_CARD = "hidden"; public static final String MORPH_IMAGE = "morph"; + public static final String DISGUISED_IMAGE = "disguised"; public static final String MANIFEST_IMAGE = "manifest"; + public static final String CLOAKED_IMAGE = "cloaked"; public static final String FORETELL_IMAGE = "foretell"; public static final String BACKFACE_POSTFIX = "$alt"; @@ -406,7 +408,7 @@ public final class ImageKeys { CardEdition ed = StaticData.instance().getEditions().get(setFolder); if (ed != null && !editionAlias.containsKey(setFolder)) { String alias = ed.getAlias(); - Set aliasSet = new HashSet<>(); + Set aliasSet = new HashSet<>(); if (alias != null) { if (!alias.equalsIgnoreCase(setFolder)) aliasSet.add(alias); diff --git a/forge-game/src/main/java/forge/game/GameAction.java b/forge-game/src/main/java/forge/game/GameAction.java index 11ba5524015..52ccf36c2d9 100644 --- a/forge-game/src/main/java/forge/game/GameAction.java +++ b/forge-game/src/main/java/forge/game/GameAction.java @@ -189,7 +189,7 @@ public class GameAction { if (c.isSplitCard()) { boolean resetToOriginal = false; - if (c.isManifested()) { + if (c.isManifested() || c.isCloaked()) { if (fromBattlefield) { // Make sure the card returns from the battlefield as the original card with two halves resetToOriginal = true; @@ -351,7 +351,7 @@ public class GameAction { ReplacementResult repres = game.getReplacementHandler().run(ReplacementType.Moved, repParams); if (repres != ReplacementResult.NotReplaced && repres != ReplacementResult.Updated) { // reset failed manifested Cards back to original - if (c.isManifested() && !c.isInPlay()) { + if ((c.isManifested() || c.isCloaked()) && !c.isInPlay()) { c.forceTurnFaceUp(); } diff --git a/forge-game/src/main/java/forge/game/ability/effects/PlayEffect.java b/forge-game/src/main/java/forge/game/ability/effects/PlayEffect.java index c988f250d0d..445350973ce 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/PlayEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/PlayEffect.java @@ -323,7 +323,7 @@ public class PlayEffect extends SpellAbilityEffect { if (sa.hasParam("CastFaceDown")) { // For Illusionary Mask effect - tgtSA = CardFactoryUtil.abilityMorphDown(tgtCard.getCurrentState(), false); + tgtSA = CardFactoryUtil.abilityCastFaceDown(tgtCard.getCurrentState(), false, "Morph"); } else { tgtSA = controller.getController().getAbilityToPlay(tgtCard, sas); } diff --git a/forge-game/src/main/java/forge/game/ability/effects/SetStateEffect.java b/forge-game/src/main/java/forge/game/ability/effects/SetStateEffect.java index 23226c3a551..375927c456a 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/SetStateEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/SetStateEffect.java @@ -161,10 +161,8 @@ public class SetStateEffect extends SpellAbilityEffect { } boolean hasTransformed = false; - if (sa.isMorphUp()) { + if (sa.isTurnFaceUp()) { hasTransformed = gameCard.turnFaceUp(sa); - } else if (sa.isManifestUp()) { - hasTransformed = gameCard.turnFaceUp(true, true, sa); } else if ("Specialize".equals(mode)) { hasTransformed = gameCard.changeCardState(mode, host.getChosenColor(), sa); host.setChosenColors(null); @@ -182,6 +180,12 @@ public class SetStateEffect extends SpellAbilityEffect { } else if (sa.isManifestUp()) { String sb = p + " has unmanifested " + gameCard.getName(); game.getGameLog().add(GameLogEntryType.STACK_RESOLVE, sb); + } else if (sa.isDisguiseUp()) { + String sb = p + " has undisguised " + gameCard.getName(); + game.getGameLog().add(GameLogEntryType.STACK_RESOLVE, sb); + } else if (sa.isCloakUp()) { + String sb = p + " has uncloaked " + gameCard.getName(); + game.getGameLog().add(GameLogEntryType.STACK_RESOLVE, sb); } else if (hiddenAgenda) { if (gameCard.hasKeyword("Double agenda")) { String sb = p + " has revealed " + gameCard.getName() + " with the chosen names: " + diff --git a/forge-game/src/main/java/forge/game/card/Card.java b/forge-game/src/main/java/forge/game/card/Card.java index 2e77fbb6f58..69a891eba83 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -221,6 +221,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars { private boolean suspected = false; private boolean manifested; + private boolean cloaked; private boolean foretold; private boolean foretoldCostByEffect; @@ -516,6 +517,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars { // Cleared tests, about to change states if (currentStateName.equals(CardStateName.FaceDown) && state.equals(CardStateName.Original)) { this.setManifested(false); + this.setCloaked(false); } currentStateName = state; @@ -732,6 +734,32 @@ public class Card extends GameEntity implements Comparable, IHasSVars { return c; } + public Card cloak(Player p, SpellAbility sa, Map params) { + // Turn Face Down (even if it's DFC). + // Sometimes cards are manifested while already being face down + if (!turnFaceDown(true) && !isFaceDown()) { + return null; + } + + // Just in case you aren't the controller, now you are! + setController(p, game.getNextTimestamp()); + + // Mark this card as "cloaked" + setCloaked(true); + // give it Ward:2 + getFaceDownState().addIntrinsicKeyword("Ward:2", true); + + // Move to p's battlefield + Card c = game.getAction().moveToPlay(this, p, sa, params); + if (c.isInPlay()) { + c.setCloaked(true); + c.turnFaceDown(true); + c.updateStateForView(); + } + + return c; + } + public boolean turnFaceDown() { return turnFaceDown(false); } @@ -762,58 +790,52 @@ public class Card extends GameEntity implements Comparable, IHasSVars { } public boolean turnFaceUp(SpellAbility cause) { - return turnFaceUp(false, true, cause); + return turnFaceUp(true, cause); } - public boolean turnFaceUp(boolean manifestPaid, boolean runTriggers, SpellAbility cause) { - if (isFaceDown()) { - if (manifestPaid && isManifested() && !getRules().getType().isCreature()) { - // If we've manifested a non-creature and we're demanifesting disallow it - - // Unless this creature also has a Morph ability - return false; - } - - CardCollectionView cards = hasMergedCard() ? getMergedCards() : new CardCollection(this); - boolean retResult = false; - for (final Card c : cards) { - boolean result; - if (c.isFlipped() && c.isFlipCard()) { - result = c.setState(CardStateName.Flipped, true); - } else { - result = c.setState(CardStateName.Original, true); - } - - c.facedown = false; - c.turnedFaceUpThisTurn = true; - c.updateStateForView(); //fixes cards with backside viewable - // need to run faceup commands, currently - // it does cleanup the modified facedown state - if (result) { - c.runFaceupCommands(); - } - retResult = retResult || result; - } - if (retResult && hasMergedCard()) { - removeMutatedStates(); - rebuildMutatedStates(cause); - game.getTriggerHandler().clearActiveTriggers(this, null); - game.getTriggerHandler().registerActiveTrigger(this, false); - } - if (retResult && runTriggers) { - // Run replacement effects - getGame().getReplacementHandler().run(ReplacementType.TurnFaceUp, AbilityKey.mapFromAffected(this)); - - // Run triggers - final Map runParams = AbilityKey.mapFromCard(this); - runParams.put(AbilityKey.Cause, cause); - - getGame().getTriggerHandler().registerActiveTrigger(this, false); - getGame().getTriggerHandler().runTrigger(TriggerType.TurnFaceUp, runParams, false); - } - return retResult; + public boolean turnFaceUp(boolean runTriggers, SpellAbility cause) { + if (!isFaceDown()) { + return false; } - return false; + + CardCollectionView cards = hasMergedCard() ? getMergedCards() : new CardCollection(this); + boolean retResult = false; + for (final Card c : cards) { + boolean result; + if (c.isFlipped() && c.isFlipCard()) { + result = c.setState(CardStateName.Flipped, true); + } else { + result = c.setState(CardStateName.Original, true); + } + + c.facedown = false; + c.turnedFaceUpThisTurn = true; + c.updateStateForView(); //fixes cards with backside viewable + // need to run faceup commands, currently + // it does cleanup the modified facedown state + if (result) { + c.runFaceupCommands(); + } + retResult = retResult || result; + } + if (retResult && hasMergedCard()) { + removeMutatedStates(); + rebuildMutatedStates(cause); + game.getTriggerHandler().clearActiveTriggers(this, null); + game.getTriggerHandler().registerActiveTrigger(this, false); + } + if (retResult && runTriggers) { + // Run replacement effects + getGame().getReplacementHandler().run(ReplacementType.TurnFaceUp, AbilityKey.mapFromAffected(this)); + + // Run triggers + final Map runParams = AbilityKey.mapFromCard(this); + runParams.put(AbilityKey.Cause, cause); + + getGame().getTriggerHandler().registerActiveTrigger(this, false); + getGame().getTriggerHandler().runTrigger(TriggerType.TurnFaceUp, runParams, false); + } + return retResult; } public boolean wasTurnedFaceUpThisTurn() { @@ -2143,6 +2165,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars { } else if (keyword.startsWith("Ripple")) { sbLong.append(TextUtil.fastReplace(keyword, ":", " ")).append("\r\n"); } else if (keyword.startsWith("Morph") || keyword.startsWith("Megamorph") + || keyword.startsWith("Disguise") || keyword.startsWith("Escape") || keyword.startsWith("Foretell:") || keyword.startsWith("Madness:")|| keyword.startsWith("Recover") || keyword.startsWith("Reconfigure") || keyword.startsWith("Squad") @@ -2615,6 +2638,9 @@ public class Card extends GameEntity implements Comparable, IHasSVars { if (manifested) { sb.append("Manifested\r\n"); } + if (cloaked) { + sb.append("Cloaked\r\n"); + } String keywordText = keywordsToText(getUnhiddenKeywords(state)); sb.append(keywordText).append(keywordText.length() > 0 ? linebreak : ""); @@ -3172,7 +3198,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars { return false; } for (SpellAbility sa : getSpellAbilities()) { - if (!(sa instanceof SpellPermanent && sa.isBasicSpell()) && !sa.isMorphUp()) { + if (!(sa instanceof SpellPermanent && sa.isBasicSpell()) && !sa.isMorphUp() && !sa.isDisguiseUp()) { return false; } } @@ -3206,7 +3232,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars { if (isInPlay()) { if ((null == mana || false == mana) && isFaceDown() && state.getView().getState() == CardStateName.FaceDown) { for (SpellAbility sa : getState(CardStateName.Original).getNonManaAbilities()) { - if (sa.isManifestUp() || sa.isMorphUp()) { + if (sa.isTurnFaceUp()) { list.add(sa); } } @@ -4425,6 +4451,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars { perpetual.add(p); } + @SuppressWarnings("unchecked") public final void executePerpetual(Map p) { final String category = (String) p.get("Category"); if (category.equals("NewPT")) { @@ -6269,6 +6296,13 @@ public class Card extends GameEntity implements Comparable, IHasSVars { this.manifested = manifested; } + public final boolean isCloaked() { + return cloaked; + } + public final void setCloaked(final boolean cloaked) { + this.cloaked = cloaked; + } + public final boolean isForetold() { // in exile and foretold if (this.isInZone(ZoneType.Exile)) { @@ -7061,10 +7095,13 @@ public class Card extends GameEntity implements Comparable, IHasSVars { } } - if (isInPlay() && isFaceDown() && isManifested()) { - ManaCost cost = oState.getManaCost(); - if (oState.getType().isCreature()) { - abilities.add(CardFactoryUtil.abilityManifestFaceUp(this, cost)); + if (isInPlay() && isFaceDown() && oState.getType().isCreature()) + { + if (isManifested()) { + abilities.add(oState.getManifestUp()); + } + if (isCloaked()) { + abilities.add(oState.getCloakUp()); } } @@ -7387,7 +7424,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars { public void forceTurnFaceUp() { getGame().getTriggerHandler().suppressMode(TriggerType.TurnFaceUp); - turnFaceUp(false, false, null); + turnFaceUp(false, null); getGame().getTriggerHandler().clearSuppression(TriggerType.TurnFaceUp); } diff --git a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java index ca849719b27..e568b3cb2ab 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java +++ b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java @@ -102,7 +102,7 @@ public class CardFactoryUtil { * * @return a {@link forge.game.spellability.SpellAbility} object. */ - public static SpellAbility abilityMorphDown(final CardState cardState, final boolean intrinsic) { + public static SpellAbility abilityCastFaceDown(final CardState cardState, final boolean intrinsic, String key) { final Spell morphDown = new Spell(cardState.getCard(), new Cost(ManaCost.THREE, false)) { private static final long serialVersionUID = -1438810964807867610L; @@ -111,13 +111,16 @@ public class CardFactoryUtil { if (!hostCard.isFaceDown()) { hostCard.setOriginalStateAsFaceDown(); } + CardFactoryUtil.setFaceDownState(hostCard, this); final Game game = hostCard.getGame(); - Map moveParams = AbilityKey.newMap(); - moveParams.put(AbilityKey.LastStateBattlefield, game.copyLastStateBattlefield()); - moveParams.put(AbilityKey.LastStateGraveyard, game.copyLastStateGraveyard()); + CardZoneTable table = new CardZoneTable(game.copyLastStateBattlefield(), game.copyLastStateBattlefield()); + Map params = AbilityKey.newMap(); + params.put(AbilityKey.LastStateBattlefield, table.getLastStateBattlefield()); + params.put(AbilityKey.LastStateGraveyard, table.getLastStateGraveyard()); + params.put(AbilityKey.InternalTriggerTable, table); - hostCard.getGame().getAction().moveToPlay(hostCard, this, moveParams); + hostCard.getGame().getAction().moveToPlay(hostCard, this, params); } @Override @@ -138,9 +141,8 @@ public class CardFactoryUtil { morphDown.setCardState(cardState); morphDown.setDescription("(You may cast this card face down as a 2/2 creature for {3}.)"); - morphDown.setStackDescription("Morph - Creature 2/2"); + morphDown.setStackDescription(key + " - Creature 2/2"); morphDown.setCastFaceDown(true); - morphDown.setBasicSpell(false); morphDown.setIntrinsic(intrinsic); @@ -187,18 +189,48 @@ public class CardFactoryUtil { return morphUp; } + public static SpellAbility abilityDisguiseUp(final CardState cardState, final String costStr, final boolean intrinsic) { + Cost cost = new Cost(costStr, true); + StringBuilder sbCost = new StringBuilder("Disguise"); + sbCost.append(" "); + if (!cost.isOnlyManaCost()) { + sbCost.append("— "); + } + sbCost.append(cost.toString()); - public static SpellAbility abilityManifestFaceUp(final Card sourceCard, final ManaCost manaCost) { + StringBuilder sb = new StringBuilder(); + sb.append("ST$ SetState | Cost$ ").append(costStr).append(" | CostDesc$ ").append(sbCost); + sb.append(" | DisguiseUp$ True | Secondary$ True | IsPresent$ Card.Self+faceDown"); + sb.append(" | Mode$ TurnFaceUp | SpellDescription$ (Turn this face up any time for its disguise cost.)"); + + final SpellAbility morphUp = AbilityFactory.getAbility(sb.toString(), cardState); + + // if Cost has X in cost, need to check source for an SVar for this + if (cost.hasXInAnyCostPart() && cardState.hasSVar("X")) { + morphUp.setSVar("X", cardState.getSVar("X")); + } + + final StringBuilder sbStack = new StringBuilder(); + sbStack.append(cardState.getName()).append(" - turn this card face up."); + morphUp.setStackDescription(sbStack.toString()); + + morphUp.setIntrinsic(intrinsic); + + return morphUp; + } + + public static SpellAbility abilityTurnFaceUp(final CardState sourceCard, String key, String desc) { + final ManaCost manaCost = sourceCard.getManaCost(); String costDesc = manaCost.toString(); // Cost need to be set later StringBuilder sb = new StringBuilder(); - sb.append("ST$ SetState | Cost$ 0 | CostDesc$ Unmanifest ").append(costDesc); - sb.append(" | ManifestUp$ True | Secondary$ True | PresentDefined$ Self | IsPresent$ Card.faceDown+manifested"); - sb.append(" | Mode$ TurnFaceUp | SpellDescription$ (Turn this face up any time for its mana cost.)"); + sb.append("ST$ SetState | Cost$ 0 | ").append("PrecostDesc$ ").append(desc).append(" | CostDesc$ ").append(costDesc); + sb.append(" | ").append(key).append("$ True | Secondary$ True | Mode$ TurnFaceUp | SpellDescription$ (Turn this face up any time for its mana cost.)"); final SpellAbility manifestUp = AbilityFactory.getAbility(sb.toString(), sourceCard); manifestUp.setPayCosts(new Cost(manaCost, true)); + manifestUp.rebuiltDescription(); final StringBuilder sbStack = new StringBuilder(); sbStack.append(sourceCard.getName()).append(" - turn this card face up."); @@ -2760,6 +2792,13 @@ public class CardFactoryUtil { newSA.setAlternativeCost(AlternativeCost.Dash); newSA.setIntrinsic(intrinsic); inst.addSpellAbility(newSA); + } else if (keyword.startsWith("Disguise")) { + final String[] k = keyword.split(":"); + + SpellAbility faceDown = abilityCastFaceDown(card, intrinsic, "Disguise"); + faceDown.putParam("FaceDownKeyword", "Ward:2"); + inst.addSpellAbility(faceDown); + inst.addSpellAbility(abilityDisguiseUp(card, k[1], intrinsic)); } else if (keyword.startsWith("Disturb")) { final String[] k = keyword.split(":"); final Cost disturbCost = new Cost(k[1], true); @@ -3128,12 +3167,12 @@ public class CardFactoryUtil { } else if (keyword.startsWith("Morph")) { final String[] k = keyword.split(":"); - inst.addSpellAbility(abilityMorphDown(card, intrinsic)); + inst.addSpellAbility(abilityCastFaceDown(card, intrinsic, "Morph")); inst.addSpellAbility(abilityMorphUp(card, k[1], false, intrinsic)); } else if (keyword.startsWith("Megamorph")) { final String[] k = keyword.split(":"); - inst.addSpellAbility(abilityMorphDown(card, intrinsic)); + inst.addSpellAbility(abilityCastFaceDown(card, intrinsic, "Morph")); inst.addSpellAbility(abilityMorphUp(card, k[1], true, intrinsic)); } else if (keyword.startsWith("More Than Meets the Eye")) { final String[] n = keyword.split(":"); @@ -4018,9 +4057,12 @@ public class CardFactoryUtil { if (sa.hasParam("FaceDownSetType")) { faceDown.setType(new CardType(Arrays.asList(sa.getParam("FaceDownSetType").split(" & ")), false)); } + if (sa.hasParam("FaceDownKeyword")) { + faceDown.addIntrinsicKeywords(Arrays.asList(sa.getParam("FaceDownKeyword").split(" & "))); + } if (sa.hasParam("FaceDownPower") || sa.hasParam("FaceDownToughness") - || sa.hasParam("FaceDownSetType")) { + || sa.hasParam("FaceDownSetType") || sa.hasParam("FaceDownKeyword")) { final GameCommand unanimate = new GameCommand() { private static final long serialVersionUID = 8853789549297846163L; diff --git a/forge-game/src/main/java/forge/game/card/CardProperty.java b/forge-game/src/main/java/forge/game/card/CardProperty.java index 67ecd0a5941..15d8fcd8e68 100644 --- a/forge-game/src/main/java/forge/game/card/CardProperty.java +++ b/forge-game/src/main/java/forge/game/card/CardProperty.java @@ -1124,10 +1124,14 @@ public class CardProperty { if (card.isPhasedOut()) { return false; } - } else if (property.startsWith("manifested")) { + } else if (property.equals("manifested")) { if (!card.isManifested()) { return false; } + } else if (property.equals("cloaked")) { + if (!card.isCloaked()) { + return false; + } } else if (property.startsWith("DrawnThisTurn")) { if (!card.getDrawnThisTurn()) { return false; diff --git a/forge-game/src/main/java/forge/game/card/CardState.java b/forge-game/src/main/java/forge/game/card/CardState.java index 00ec64cf651..8d5a9b62d8b 100644 --- a/forge-game/src/main/java/forge/game/card/CardState.java +++ b/forge-game/src/main/java/forge/game/card/CardState.java @@ -86,6 +86,9 @@ public class CardState extends GameObject implements IHasSVars { private ReplacementEffect battleTypeRep; private ReplacementEffect sagaRep; + private SpellAbility manifestUp; + private SpellAbility cloakUp; + public CardState(Card card, CardStateName name) { this(card.getView().createAlternateState(name), card); } @@ -759,4 +762,16 @@ public class CardState extends GameObject implements IHasSVars { return n; } + public SpellAbility getManifestUp() { + if (this.manifestUp == null) { + manifestUp = CardFactoryUtil.abilityTurnFaceUp(this, "ManifestUp", "Unmanifest"); + } + return manifestUp; + } + public SpellAbility getCloakUp() { + if (this.cloakUp == null) { + cloakUp = CardFactoryUtil.abilityTurnFaceUp(this, "CloakUp", "Uncloak"); + } + return cloakUp; + } } diff --git a/forge-game/src/main/java/forge/game/card/CardView.java b/forge-game/src/main/java/forge/game/card/CardView.java index 9958a0a2d06..e314daef197 100644 --- a/forge-game/src/main/java/forge/game/card/CardView.java +++ b/forge-game/src/main/java/forge/game/card/CardView.java @@ -145,6 +145,9 @@ public class CardView extends GameEntityView { public boolean isManifested() { return get(TrackableProperty.Manifested); } + public boolean isCloaked() { + return get(TrackableProperty.Cloaked); + } public boolean isFlipCard() { return get(TrackableProperty.FlipCard); @@ -948,6 +951,7 @@ public class CardView extends GameEntityView { set(TrackableProperty.Facedown, c.isFaceDown()); set(TrackableProperty.Foretold, c.isForetold()); set(TrackableProperty.Manifested, c.isManifested()); + set(TrackableProperty.Cloaked, c.isCloaked()); set(TrackableProperty.Adventure, c.isAdventureCard()); set(TrackableProperty.DoubleFaced, c.isDoubleFaced()); set(TrackableProperty.Modal, c.isModal()); @@ -1230,8 +1234,13 @@ public class CardView extends GameEntityView { if (getCard().getZone() == ZoneType.Exile) { return ImageKeys.getTokenKey(getCard().isForeTold() ? ImageKeys.FORETELL_IMAGE : ImageKeys.HIDDEN_CARD); } - return ImageKeys.getTokenKey(getCard().isManifested() ? ImageKeys.MANIFEST_IMAGE - : getType().getCreatureTypes().isEmpty() ? isCreature() ? ImageKeys.MORPH_IMAGE : ImageKeys.HIDDEN_CARD + if (getCard().isManifested()) { + return ImageKeys.getTokenKey(ImageKeys.MANIFEST_IMAGE); + } else if (getCard().isCloaked()) { + return ImageKeys.getTokenKey(ImageKeys.CLOAKED_IMAGE); + } + + return ImageKeys.getTokenKey(getType().getCreatureTypes().isEmpty() ? isCreature() ? ImageKeys.MORPH_IMAGE : ImageKeys.HIDDEN_CARD : getType().getCreatureTypes().toString().toLowerCase().replace(" ", "_").replace("[", "").replace("]","")); } if (canBeShownToAny(viewers)) { diff --git a/forge-game/src/main/java/forge/game/keyword/Keyword.java b/forge-game/src/main/java/forge/game/keyword/Keyword.java index 5480961d611..db5c132ed0f 100644 --- a/forge-game/src/main/java/forge/game/keyword/Keyword.java +++ b/forge-game/src/main/java/forge/game/keyword/Keyword.java @@ -61,6 +61,7 @@ public enum Keyword { DETHRONE("Dethrone", SimpleKeyword.class, false, "Whenever this creature attacks the player with the most life or tied for the most life, put a +1/+1 counter on it."), DEVOUR("Devour", KeywordWithAmount.class, false, "As this creature enters the battlefield, you may sacrifice any number of creatures. This creature enters the battlefield with {%d:+1/+1 counter} on it for each creature sacrificed this way."), DEVOID("Devoid", SimpleKeyword.class, true, "This card has no color."), + DISGUISE("Disguise", KeywordWithCost.class, false, "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."), DISTURB("Disturb", KeywordWithCost.class, false, "You may cast this card from your graveyard transformed for its disturb cost."), DOCTORS_COMPANION("Doctor's companion", Partner.class, true, "You can have two commanders if the other is the Doctor."), DOUBLE_STRIKE("Double Strike", SimpleKeyword.class, true, "This creature deals both first-strike and regular combat damage."), diff --git a/forge-game/src/main/java/forge/game/spellability/SpellAbility.java b/forge-game/src/main/java/forge/game/spellability/SpellAbility.java index 985f1aec234..c3f3fc9205e 100644 --- a/forge-game/src/main/java/forge/game/spellability/SpellAbility.java +++ b/forge-game/src/main/java/forge/game/spellability/SpellAbility.java @@ -519,9 +519,16 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit public boolean isAbility() { return true; } public boolean isActivatedAbility() { return false; } + public boolean isTurnFaceUp() { + return isMorphUp() || isDisguiseUp() || isManifestUp() || isCloakUp(); + } + public boolean isMorphUp() { return this.hasParam("MorphUp"); } + public boolean isDisguiseUp() { + return this.hasParam("DisguiseUp"); + } public boolean isCastFaceDown() { return false; @@ -530,6 +537,9 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit public boolean isManifestUp() { return hasParam("ManifestUp"); } + public boolean isCloakUp() { + return hasParam("CloakUp"); + } public boolean isCycling() { return isAlternativeCost(AlternativeCost.Cycling); @@ -1060,7 +1070,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit } public boolean isBasicSpell() { - return basicSpell && this.altCost == null && getRootAbility().optionalCosts.isEmpty(); + return basicSpell && !isCastFaceDown() && this.altCost == null && getRootAbility().optionalCosts.isEmpty(); } public void setBasicSpell(final boolean basicSpell0) { basicSpell = basicSpell0; diff --git a/forge-game/src/main/java/forge/trackable/TrackableProperty.java b/forge-game/src/main/java/forge/trackable/TrackableProperty.java index 44b639ce12e..cec193e500d 100644 --- a/forge-game/src/main/java/forge/trackable/TrackableProperty.java +++ b/forge-game/src/main/java/forge/trackable/TrackableProperty.java @@ -32,6 +32,7 @@ public enum TrackableProperty { Facedown(TrackableTypes.BooleanType), Foretold(TrackableTypes.BooleanType), Manifested(TrackableTypes.BooleanType), + Cloaked(TrackableTypes.BooleanType), Modal(TrackableTypes.BooleanType), Adventure(TrackableTypes.BooleanType), DoubleFaced(TrackableTypes.BooleanType),