Resolve "ZNR: Modal Double Faced Cards"

This commit is contained in:
Hans Mackowiak
2020-09-14 19:13:54 +00:00
committed by Sol
parent 5ea03022cd
commit c8d69cbbb9
17 changed files with 198 additions and 114 deletions

View File

@@ -1856,7 +1856,7 @@ public class ComputerUtilCard {
public static AiPlayDecision checkNeedsToPlayReqs(final Card card, final SpellAbility sa) {
Game game = card.getGame();
boolean isRightSplit = sa != null && sa.isRightSplit();
boolean isRightSplit = sa != null && sa.getCardState() != null;
String needsToPlayName = isRightSplit ? "SplitNeedsToPlay" : "NeedsToPlay";
String needsToPlayVarName = isRightSplit ? "SplitNeedsToPlayVar" : "NeedsToPlayVar";

View File

@@ -9,7 +9,8 @@ public enum CardSplitType
Meld(FaceSelectionMethod.USE_ACTIVE_FACE, CardStateName.Meld),
Split(FaceSelectionMethod.COMBINE, CardStateName.RightSplit),
Flip(FaceSelectionMethod.USE_PRIMARY_FACE, CardStateName.Flipped),
Adventure(FaceSelectionMethod.USE_PRIMARY_FACE, CardStateName.Adventure);
Adventure(FaceSelectionMethod.USE_PRIMARY_FACE, CardStateName.Adventure),
Modal(FaceSelectionMethod.USE_ACTIVE_FACE, CardStateName.Modal);
CardSplitType(FaceSelectionMethod calcMode, CardStateName stateName) {
method = calcMode;

View File

@@ -10,6 +10,7 @@ public enum CardStateName {
LeftSplit,
RightSplit,
Adventure,
Modal
;

View File

@@ -532,7 +532,12 @@ public class BoosterGenerator {
Predicate<PaperCard> toAdd = null;
if (operator.equalsIgnoreCase(BoosterSlots.DUAL_FACED_CARD)) {
toAdd = Predicates.compose(Predicates.or(CardRulesPredicates.splitType(CardSplitType.Transform), CardRulesPredicates.splitType(CardSplitType.Meld)),
toAdd = Predicates.compose(
Predicates.or(
CardRulesPredicates.splitType(CardSplitType.Transform),
CardRulesPredicates.splitType(CardSplitType.Meld),
CardRulesPredicates.splitType(CardSplitType.Modal)
),
PaperCard.FN_GET_RULES);
} else if (operator.equalsIgnoreCase(BoosterSlots.LAND)) { toAdd = Predicates.compose(CardRulesPredicates.Presets.IS_LAND, PaperCard.FN_GET_RULES);
} else if (operator.equalsIgnoreCase(BoosterSlots.BASIC_LAND)) { toAdd = IPaperCard.Predicates.Presets.IS_BASIC_LAND;

View File

@@ -81,7 +81,7 @@ public class ImageUtil {
public static boolean hasBackFacePicture(PaperCard cp) {
CardSplitType cst = cp.getRules().getSplitType();
return cst == CardSplitType.Transform || cst == CardSplitType.Flip || cst == CardSplitType.Meld;
return cst == CardSplitType.Transform || cst == CardSplitType.Flip || cst == CardSplitType.Meld || cst == CardSplitType.Modal;
}
public static String getNameToUse(PaperCard cp, boolean backFace) {

View File

@@ -338,28 +338,14 @@ public class Card extends GameEntity implements Comparable<Card> {
public CardStateName getAlternateStateName() {
if (hasAlternateState()) {
if (isSplitCard()) {
if (currentStateName == CardStateName.RightSplit) {
return CardStateName.LeftSplit;
}
else {
return CardStateName.RightSplit;
return currentStateName == CardStateName.RightSplit ? CardStateName.LeftSplit : CardStateName.RightSplit;
} else if (getRules() != null) {
CardStateName changedState = getRules().getSplitType().getChangedStateName();
if (currentStateName != changedState) {
return changedState;
}
}
else if (isFlipCard() && currentStateName != CardStateName.Flipped) {
return CardStateName.Flipped;
}
else if (isDoubleFaced() && currentStateName != CardStateName.Transformed) {
return CardStateName.Transformed;
}
else if (isMeldable() && currentStateName != CardStateName.Meld) {
return CardStateName.Meld;
}
else if (this.isAdventureCard() && currentStateName != CardStateName.Adventure) {
return CardStateName.Adventure;
}
else {
return CardStateName.Original;
}
return CardStateName.Original;
}
else if (isFaceDown()) {
return CardStateName.Original;
@@ -642,7 +628,7 @@ public class Card extends GameEntity implements Comparable<Card> {
}
public boolean turnFaceDown(boolean override) {
if (override || (!isDoubleFaced() && !isMeldable())) {
if (override || !hasBackSide()) {
facedown = true;
if (setState(CardStateName.FaceDown, true)) {
runFacedownCommands();
@@ -738,6 +724,10 @@ public class Card extends GameEntity implements Comparable<Card> {
return getName(currentState);
}
public final String getName(CardStateName stateName) {
return getName(getState(stateName));
}
public final String getName(CardState state) {
if (changedCardNames.isEmpty()) {
return state.getName();
@@ -798,6 +788,14 @@ public class Card extends GameEntity implements Comparable<Card> {
return getRules() != null && getRules().getSplitType() == CardSplitType.Meld;
}
public final boolean isModal() {
return getRules() != null && getRules().getSplitType() == CardSplitType.Modal;
}
public final boolean hasBackSide() {
return isDoubleFaced() || isMeldable() || isModal();
}
public final boolean isFlipCard() {
return hasState(CardStateName.Flipped);
}
@@ -813,6 +811,9 @@ public class Card extends GameEntity implements Comparable<Card> {
public final boolean isBackSide() {
return backside;
}
public final void setBackSide(boolean value) {
backside = value;
}
public boolean isCloned() {
return !clonedStates.isEmpty();
@@ -3380,13 +3381,13 @@ public class Card extends GameEntity implements Comparable<Card> {
public final CardStateName getFaceupCardStateName() {
if (isFlipped() && hasState(CardStateName.Flipped)) {
return CardStateName.Flipped;
} else if (backside && isDoubleFaced() && hasState(CardStateName.Transformed)) {
return CardStateName.Transformed;
} else if (backside && isMeldable() && hasState(CardStateName.Meld)) {
return CardStateName.Meld;
} else {
return CardStateName.Original;
} else if (backside && hasBackSide()) {
CardStateName stateName = getRules().getSplitType().getChangedStateName();
if (hasState(stateName)) {
return stateName;
}
}
return CardStateName.Original;
}
private final CardCloneStates getLastClonedState() {
@@ -6068,20 +6069,13 @@ public class Card extends GameEntity implements Comparable<Card> {
}
public void setSplitStateToPlayAbility(final SpellAbility sa) {
if (isAdventureCard()) {
if (sa.isAdventure()) {
setState(CardStateName.Adventure, true);
CardStateName stateName = sa.getCardState();
if (hasState(stateName)) {
setState(stateName, true);
// need to set backSide value according to the SplitType
if (hasBackSide()) {
setBackSide(getRules().getSplitType().getChangedStateName().equals(stateName));
}
return;
}
if (!isSplitCard()) {
return; // just in case
}
// Split card support
if (sa.isLeftSplit()) {
setState(CardStateName.LeftSplit, true);
} else if (sa.isRightSplit()) {
setState(CardStateName.RightSplit, true);
}
}
@@ -6167,6 +6161,52 @@ public class Card extends GameEntity implements Comparable<Card> {
}
}
if (isModal() && hasState(CardStateName.Modal)) {
if (getState(CardStateName.Modal).getType().isLand() && !getLastKnownZone().is(ZoneType.Battlefield)) {
LandAbility la = new LandAbility(this, player, null);
la.setCardState(CardStateName.Modal);
Card source = CardUtil.getLKICopy(this);
boolean lkicheck = true;
// if Card is Facedown, need to check if MayPlay still applies
if (isFaceDown()) {
source.forceTurnFaceUp();
}
source.setSplitStateToPlayAbility(la);
if (la.canPlay(source)) {
abilities.add(la);
}
if (lkicheck) {
// double freeze tracker, so it doesn't update view
game.getTracker().freeze();
CardCollection preList = new CardCollection(source);
game.getAction().checkStaticAbilities(false, Sets.newHashSet(source), preList);
}
// extra for MayPlay
for (CardPlayOption o : source.mayPlay(player)) {
la = new LandAbility(this, player, o.getAbility());
la.setCardState(CardStateName.Modal);
if (la.canPlay(source)) {
abilities.add(la);
}
}
// reset static abilities
if (lkicheck) {
game.getAction().checkStaticAbilities(false);
// clear delayed changes, this check should not have updated the view
game.getTracker().clearDelayed();
// need to unfreeze tracker
game.getTracker().unfreeze();
}
}
}
return abilities;
}

View File

@@ -240,8 +240,8 @@ public class CardFactory {
c.setState(CardStateName.Flipped, false);
c.setImageKey(cp.getImageKey(true));
}
else if (c.isDoubleFaced() && cp instanceof PaperCard) {
c.setState(CardStateName.Transformed, false);
else if (c.hasBackSide() && cp instanceof PaperCard && cardRules != null) {
c.setState(cardRules.getSplitType().getChangedStateName(), false);
c.setImageKey(cp.getImageKey(true));
}
else if (c.isSplitCard()) {
@@ -251,14 +251,9 @@ public class CardFactory {
c.setRarity(cp.getRarity());
c.setState(CardStateName.RightSplit, false);
c.setImageKey(originalPicture);
} else if (c.isMeldable() && cp instanceof PaperCard) {
c.setState(CardStateName.Meld, false);
c.setImageKey(cp.getImageKey(true));
} else if (c.isAdventureCard()) {
c.setState(CardStateName.Adventure, false);
c.setImageKey(originalPicture);
c.setSetCode(cp.getEdition());
c.setRarity(cp.getRarity());
}
c.setSetCode(cp.getEdition());
@@ -272,7 +267,7 @@ public class CardFactory {
private static void buildAbilities(final Card card) {
for (final CardStateName state : card.getStates()) {
if (card.isDoubleFaced() && state == CardStateName.FaceDown) {
if (card.hasBackSide() && state == CardStateName.FaceDown) {
continue; // Ignore FaceDown for DFC since they have none.
}
card.setState(state, false);
@@ -281,11 +276,7 @@ public class CardFactory {
// ************** Link to different CardFactories *******************
if (state == CardStateName.LeftSplit || state == CardStateName.RightSplit) {
for (final SpellAbility sa : card.getSpellAbilities()) {
if (state == CardStateName.LeftSplit) {
sa.setLeftSplit();
} else {
sa.setRightSplit();
}
sa.setCardState(state);
}
CardFactoryUtil.setupKeywordedAbilities(card);
final CardState original = card.getState(CardStateName.Original);

View File

@@ -4817,7 +4817,7 @@ public class CardFactoryUtil {
if (sa == null) {
return;
}
sa.setAdventure(true);
sa.setCardState(CardStateName.Adventure);
StringBuilder sb = new StringBuilder();
sb.append("Event$ Moved | ValidCard$ Card.Self | Origin$ Stack | ExcludeDestination$ Exile ");

View File

@@ -1769,7 +1769,7 @@ public class Player extends GameEntity implements Comparable<Player> {
return false;
}
public final void playLandNoCheck(final Card land) {
public final Card playLandNoCheck(final Card land) {
land.setController(this, 0);
if (land.isFaceDown()) {
land.turnFaceUp(null);
@@ -1785,6 +1785,7 @@ public class Player extends GameEntity implements Comparable<Player> {
game.getTriggerHandler().runTrigger(TriggerType.LandPlayed, AbilityKey.mapFromCard(land), false);
game.getStack().unfreezeStack();
addLandPlayedThisTurn();
return c;
}
public final boolean canPlayLand(final Card land) {

View File

@@ -17,10 +17,15 @@
*/
package forge.game.spellability;
import org.apache.commons.lang3.ObjectUtils;
import forge.card.CardStateName;
import forge.game.card.Card;
import forge.game.card.CardUtil;
import forge.game.cost.Cost;
import forge.game.player.Player;
import forge.game.staticability.StaticAbility;
import forge.game.zone.ZoneType;
public class LandAbility extends Ability {
@@ -32,25 +37,58 @@ public class LandAbility extends Ability {
public LandAbility(Card sourceCard) {
this(sourceCard, sourceCard.getController(), null);
}
public boolean canPlay(Card newHost) {
final Player p = getActivatingPlayer();
return p.canPlayLand(newHost, false, this);
}
@Override
public boolean canPlay() {
final Card land = this.getHostCard();
Card land = this.getHostCard();
final Player p = this.getActivatingPlayer();
if (this.getCardState() != null) {
if (!land.isLKI()) {
land = CardUtil.getLKICopy(land);
}
CardStateName stateName = getCardState();
if (!land.hasState(stateName)) {
land.addAlternateState(stateName, false);
land.getState(stateName).copyFrom(getHostCard().getState(stateName), true);
}
land.setState(stateName, false);
// need to reset CMC
land.setLKICMC(-1);
land.setLKICMC(land.getCMC());
}
return p.canPlayLand(land, false, this);
}
@Override
public void resolve() {
getActivatingPlayer().playLandNoCheck(getHostCard());
getHostCard().setSplitStateToPlayAbility(this);
final Card result = getActivatingPlayer().playLandNoCheck(getHostCard());
// increase mayplay used
if (getMayPlay() != null) {
getMayPlay().incMayPlayTurn();
}
// if land isn't in battlefield try to reset the card state
if (result != null && !result.isInZone(ZoneType.Battlefield)) {
result.setState(CardStateName.Original, true);
}
}
@Override
public String toUnsuppressedString() {
StringBuilder sb = new StringBuilder("Play land");
if (getHostCard().isModal()) {
sb.append(" (").append(getHostCard().getName(ObjectUtils.defaultIfNull(getCardState(), CardStateName.Original))).append(")");
}
StaticAbility sta = getMayPlay();
if (sta != null) {
Card source = sta.getHostCard();

View File

@@ -227,39 +227,19 @@ public abstract class Spell extends SpellAbility implements java.io.Serializable
}
source.turnFaceDownNoUpdate();
lkicheck = true;
} else if (isAdventure()) {
} else if (getCardState() != null) {
if (!source.isLKI()) {
source = CardUtil.getLKICopy(source);
}
source.setState(CardStateName.Adventure, false);
// need to reset CMC
source.setLKICMC(-1);
source.setLKICMC(source.getCMC());
lkicheck = true;
} else if (source.isSplitCard() && (isLeftSplit() || isRightSplit())) {
if (!source.isLKI()) {
source = CardUtil.getLKICopy(source);
}
if (isLeftSplit()) {
if (!source.hasState(CardStateName.LeftSplit)) {
source.addAlternateState(CardStateName.LeftSplit, false);
source.getState(CardStateName.LeftSplit).copyFrom(
getHostCard().getState(CardStateName.LeftSplit), true);
}
source.setState(CardStateName.LeftSplit, false);
CardStateName stateName = getCardState();
if (!source.hasState(stateName)) {
source.addAlternateState(stateName, false);
source.getState(stateName).copyFrom(getHostCard().getState(stateName), true);
}
if (isRightSplit()) {
if (!source.hasState(CardStateName.RightSplit)) {
source.addAlternateState(CardStateName.RightSplit, false);
source.getState(CardStateName.RightSplit).copyFrom(
getHostCard().getState(CardStateName.RightSplit), true);
}
source.setState(CardStateName.RightSplit, false);
source.setState(stateName, false);
if (getHostCard().hasBackSide()) {
source.setBackSide(getHostCard().getRules().getSplitType().getChangedStateName().equals(stateName));
}
// need to reset CMC

View File

@@ -21,6 +21,8 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import forge.card.CardStateName;
import forge.card.mana.ManaCost;
import forge.game.*;
import forge.game.ability.AbilityFactory;
@@ -113,9 +115,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
private boolean basicLandAbility = false;
private boolean adventure = false;
private SplitSide splitSide = null;
enum SplitSide { LEFT, RIGHT }
private CardStateName stateName = null;
private int totalManaSpent = 0;
@@ -840,26 +840,15 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
mayPlay = sta;
}
public boolean isLeftSplit() {
return splitSide == SplitSide.LEFT;
public CardStateName getCardState() {
return stateName;
}
public boolean isRightSplit() {
return splitSide == SplitSide.RIGHT;
}
public void setNoSplit() {
splitSide = null;
}
public void setLeftSplit() {
splitSide = SplitSide.LEFT;
}
public void setRightSplit() {
splitSide = SplitSide.RIGHT;
public void setCardState(CardStateName stateName0) {
this.stateName = stateName0;
}
public boolean isAdventure() {
return this.adventure;
}
public void setAdventure(boolean adventure) {
this.adventure = adventure;
return this.stateName == CardStateName.Adventure;
}
public SpellAbility copy() {

View File

@@ -21,7 +21,6 @@ import forge.game.Game;
import forge.game.GameType;
import forge.game.ability.AbilityUtils;
import forge.game.card.*;
import forge.game.combat.Combat;
import forge.game.cost.IndividualCostPaymentInstance;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -255,7 +254,7 @@ public class SpellAbilityRestriction extends SpellAbilityVariables {
}
// TODO: this is an exception for Aftermath. Needs to be somehow generalized.
if (this.getZone() != ZoneType.Graveyard && sa.isAftermath() && sa.isRightSplit()) {
if (this.getZone() != ZoneType.Graveyard && sa.isAftermath() && sa.getCardState() != null) {
return false;
}
@@ -310,8 +309,6 @@ public class SpellAbilityRestriction extends SpellAbilityVariables {
*/
public final boolean checkActivatorRestrictions(final Card c, final SpellAbility sa) {
Player activator = sa.getActivatingPlayer();
final Game game = activator.getGame();
final Combat combat = game.getPhaseHandler().getCombat();
if (sa.isSpell()) {
// Spells should always default to "controller" but use mayPlay check.

View File

@@ -0,0 +1,14 @@
Name:Branchloft Pathway
ManaCost:no cost
Types:Land
A:AB$ Mana | Cost$ T | Produced$ G | SpellDescription$ Add {G}.
AlternateMode:Modal
Oracle:Add {G}.
ALTERNATE
Name:Boulderloft Pathway
ManaCost:no cost
Types:Land
A:AB$ Mana | Cost$ T | Produced$ W | SpellDescription$ Add {W}.
Oracle:Add {W}.

View File

@@ -0,0 +1,19 @@
Name:Emeria's Call
ManaCost:4 W W W
Types:Sorcery
A:SP$ Token | Cost$ 4 W W W | TokenAmount$ 2 | TokenScript$ w_4_4_angel_warrior_flying | SubAbility$ DBPumpAll | SpellDescription$ Create two 4/4 white Angel Warrior creature tokens with flying. Non-Angel creatures you control gain indestructible until your next turn.
SVar:DBPumpAll:DB$ PumpAll | ValidCards$ Creature.nonAngel+YouCtrl | KW$ Indestructible | UntilYourNextTurn$ True
AlternateMode:Modal
DeckHas:Ability$Token
Oracle:Create two 4/4 white Angel Warrior creature tokens with flying. Non-Angel creatures you control gain indestructible until your next turn.
ALTERNATE
Name:Emeria, Shattered Skyclave
ManaCost:no cost
Types:Land
K:ETBReplacement:Other:DBTap
SVar:DBTap:DB$ Tap | ETB$ True | Defined$ Self | UnlessCost$ PayLife<3> | UnlessPayer$ You | UnlessAI$ Shockland | StackDescription$ enters the battlefield tapped. | SpellDescription$ As CARDNAME enters the battlefield, you may pay 3 life. If you don't, it enters the battlefield tapped.
A:AB$ Mana | Cost$ T | Produced$ W | SpellDescription$ Add {W}.
Oracle:As Emeria, Shattered Skyclave enters the battlefield, you may pay 3 life. If you dont, it enters the battlefield tapped.\nAdd {W}.

View File

@@ -0,0 +1,7 @@
Name:Angel Warrior
ManaCost:no cost
Types:Creature Angel Warrior
Colors:white
PT:4/4
K:Flying
Oracle:Flying

View File

@@ -220,6 +220,7 @@ public abstract class AbstractGuiGame implements IGuiGame, IMayViewCards {
case Flipped:
case Transformed:
case Meld:
case Modal:
return true;
default:
return false;