Merge pull request #14061

* move setPT to Card

* Create DoubleFacedCard and DoubleFacedCardHalf to share code between …

* Create Transforming Double Face Card class

* allow putting either permanent side of a double faced card to the bat…

* refactor exile and return transforming card

* update ModalDoubleFacedCard references to DoubleFacedCard where relev…

* update for GUI

* refactor a disturb card

* refactor more disturb cards for test coverage

* refactor a transform card

* refactor more transform cards for test coverage

* fix Archangel Avacyn

* fix cantPlayTDFCBackSide inconsistency

* fix Double Faced Cards having triggers and static abilities when tran…

* fix Double Faced Cards card view erroring when flipping in client

* fix test_Copy_AsSpell_Backside inconsistency

* enable Spider-Man MDFC

* convert TDFC with saga as the front and add card references to Transf…

* refactor More Than Meets the Eye Card

* refactor a battle

* refactor a craft card

* update comment on PeterParkerTest

* Merge branch 'master' into rework-dfc

* fix Saga TDFC Azusa's Many Journeys

* fix double faced cards adding permanent triggers / effects to game

* move permanents entering map into Battlefield

* convert Room cards for new Permanent structure

* fix disturb not exiling

* Merge branch 'master' into rework-dfc

* fix Eddie Brock Power/Toughness

* fix Miles Morales ability on main card

* fix verify conditions for siege and day/night cards

* change room characteristics to text effect to match game rules

* update verify test to skip DoubleFacedCard in missing card test

* accidentally removed transform condition

* Merge branch 'master' into rework-dfc

* fix verify

* CardUtil - remove unnecessary line from castSingle method
This commit is contained in:
Jmlundeen 2025-11-27 09:24:03 -06:00 committed by GitHub
parent 29557f4334
commit 69e20b1061
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
121 changed files with 3020 additions and 2225 deletions

View file

@ -23,7 +23,7 @@ public class EntersBattlefieldTriggeredAbility extends TriggeredAbilityImpl {
}
public EntersBattlefieldTriggeredAbility(Effect effect, boolean optional) {
super(Zone.ALL, effect, optional); // Zone.All because a creature with trigger can be put into play and be sacrificed during the resolution of an effect (discard Obstinate Baloth with Smallpox)
super(Zone.BATTLEFIELD, effect, optional); // Zone.All doesn't appear to be necessary anymore (discard Obstinate Baloth with Smallpox still works)
this.withRuleTextReplacement(true); // default true to replace "{this}" with "it" or "this creature"
// warning, it's impossible to add text auto-replacement for creatures here (When this creature enters),

View file

@ -17,6 +17,9 @@ import mage.game.events.GameEvent;
import mage.game.events.ZoneChangeEvent;
import mage.game.stack.Spell;
import mage.players.Player;
import mage.util.CardUtil;
import java.util.UUID;
/**
* @author LevelX2
@ -99,11 +102,10 @@ class PutIntoGraveFromAnywhereEffect extends ReplacementEffectImpl {
@Override
public boolean applies(GameEvent event, Ability source, Game game) {
UUID cardId = CardUtil.getMainCardId(game, source.getSourceId()); // for split cards
if (((ZoneChangeEvent) event).getToZone() == Zone.GRAVEYARD
&& event.getTargetId().equals(source.getSourceId())) {
if (condition == null || condition.apply(game, source)) {
return true;
}
&& (event.getTargetId().equals(cardId) || event.getTargetId().equals(source.getSourceId()))) {
return condition == null || condition.apply(game, source);
}
return false;
}

View file

@ -0,0 +1,39 @@
package mage.abilities.common;
import mage.abilities.effects.common.RoomCharacteristicsEffect;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.game.permanent.PermanentImpl;
// For the overall Room card flavor text and mana value effect.
public class RoomAbility extends SimpleStaticAbility {
public RoomAbility() {
super(Zone.BATTLEFIELD, new RoomCharacteristicsEffect());
this.setRuleVisible(true);
this.setRuleAtTheTop(true);
}
protected RoomAbility(final RoomAbility ability) {
super(ability);
}
@Override
public String getRule() {
return "<i>(You may cast either half. That door unlocks on the battlefield. " +
"As a sorcery, you may pay the mana cost of a locked door to unlock it.)</i>";
}
@Override
public RoomAbility copy() {
return new RoomAbility(this);
}
public void applyCharacteristics(Game game, Permanent permanent) {
((RoomCharacteristicsEffect) this.getEffects().get(0)).removeCharacteristics(game, permanent);
}
public void restoreUnlockedStats(Game game, PermanentImpl permanent) {
((RoomCharacteristicsEffect) this.getEffects().get(0)).restoreUnlockedStats(game, permanent);
}
}

View file

@ -12,12 +12,11 @@ import mage.game.Game;
import mage.game.permanent.Permanent;
/**
* Special action for Room cards to unlock a locked half by paying its
* mana cost.
* This ability is only present if the corresponding half is currently
* locked.
* @author oscscull
* Special action for Room cards to unlock a locked half by paying its
* mana
* cost.
* This ability is only present if the corresponding half is currently
* locked.
*/
public class RoomUnlockAbility extends SpecialAction {
@ -61,6 +60,10 @@ public class RoomUnlockAbility extends SpecialAction {
sb.append(isLeftHalf ? "left" : "right").append(" half is locked.)</i>");
return sb.toString();
}
public boolean isLeftHalf() {
return isLeftHalf;
}
}
/**

View file

@ -7,6 +7,7 @@ import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.ContinuousEffectImpl;
import mage.abilities.keyword.TransformAbility;
import mage.cards.Card;
import mage.cards.TransformingDoubleFacedCard;
import mage.constants.*;
import mage.game.Game;
import mage.game.stack.Spell;
@ -20,6 +21,7 @@ import java.util.UUID;
public class SpellTransformedAbility extends SpellAbility {
protected final String manaCost; //This variable is only used for rules text
private boolean ignoreTransformEffect; // TODO: temporary while converting tdfc
public SpellTransformedAbility(Card card, String manaCost) {
super(card.getSecondFaceSpellAbility());
@ -35,7 +37,11 @@ public class SpellTransformedAbility extends SpellAbility {
this.clearManaCosts();
this.clearManaCostsToPay();
this.addCost(new ManaCostsImpl<>(manaCost));
this.addSubAbility(new TransformAbility());
if (!(card instanceof TransformingDoubleFacedCard)) {
this.addSubAbility(new TransformAbility());
} else {
ignoreTransformEffect = true;
}
}
public SpellTransformedAbility(final SpellAbility ability) {
@ -54,6 +60,7 @@ public class SpellTransformedAbility extends SpellAbility {
protected SpellTransformedAbility(final SpellTransformedAbility ability) {
super(ability);
this.manaCost = ability.manaCost;
this.ignoreTransformEffect = ability.ignoreTransformEffect;
}
@Override
@ -65,6 +72,9 @@ public class SpellTransformedAbility extends SpellAbility {
public boolean activate(Game game, Set<MageIdentifier> allowedIdentifiers, boolean noMana) {
if (super.activate(game, allowedIdentifiers, noMana)) {
game.getState().setValue(TransformAbility.VALUE_KEY_ENTER_TRANSFORMED + getSourceId(), Boolean.TRUE);
if (ignoreTransformEffect) {
return true;
}
// TODO: must be removed after transform cards (one side) migrated to MDF engine (multiple sides)
TransformedEffect effect = new TransformedEffect();
game.addEffect(effect, this);

View file

@ -3,10 +3,7 @@ package mage.abilities.effects;
import mage.MageIdentifier;
import mage.abilities.Ability;
import mage.abilities.ActivatedAbility;
import mage.cards.Card;
import mage.cards.ModalDoubleFacedCard;
import mage.cards.SplitCard;
import mage.cards.CardWithSpellOption;
import mage.cards.*;
import mage.constants.*;
import mage.game.Game;
import mage.players.Player;
@ -92,9 +89,9 @@ public abstract class AsThoughEffectImpl extends ContinuousEffectImpl implements
player.setCastSourceIdWithAlternateMana(leftCard.getId(), null, leftCard.getSpellAbility().getCosts(), identifier);
Card rightCard = ((SplitCard) card).getRightHalfCard();
player.setCastSourceIdWithAlternateMana(rightCard.getId(), null, rightCard.getSpellAbility().getCosts(), identifier);
} else if (card instanceof ModalDoubleFacedCard) {
Card leftCard = ((ModalDoubleFacedCard) card).getLeftHalfCard();
Card rightCard = ((ModalDoubleFacedCard) card).getRightHalfCard();
} else if (card instanceof DoubleFacedCard) {
Card leftCard = ((DoubleFacedCard) card).getLeftHalfCard();
Card rightCard = ((DoubleFacedCard) card).getRightHalfCard();
// some MDFC's are land. IE: sea gate restoration
if (!leftCard.isLand(game)) {
player.setCastSourceIdWithAlternateMana(leftCard.getId(), null, leftCard.getSpellAbility().getCosts(), identifier);

View file

@ -2,9 +2,11 @@ package mage.abilities.effects.common;
import mage.MageObject;
import mage.MageObjectReference;
import mage.abilities.Abilities;
import mage.abilities.Ability;
import mage.abilities.effects.ContinuousEffectImpl;
import mage.cards.Card;
import mage.abilities.common.RoomAbility;
import mage.constants.*;
import mage.game.Game;
import mage.game.permanent.Permanent;
@ -63,6 +65,16 @@ public class CopyEffect extends ContinuousEffectImpl {
permanent = game.getPermanentEntering(copyToObjectId);
if (permanent != null) {
copyToPermanent(permanent, game, source);
// Apply Room characteristics since effects aren't applied to entering permanents yet
if (permanent.hasSubtype(SubType.ROOM, game)) {
Abilities<Ability> abilities = permanent.getAbilities();
for (Ability ability : abilities) {
if (ability instanceof RoomAbility) {
((RoomAbility) ability).applyCharacteristics(game, permanent);
break;
}
}
}
// set reference to the permanent later on the battlefield so we have to add already one (if no token) to the zone change counter
int ZCCDiff = 1;
if (permanent instanceof PermanentToken) {

View file

@ -216,6 +216,28 @@ public class CreateTokenCopyTargetEffect extends OneShotEffect {
// create token and modify all attributes permanently (without game usage)
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);
// the active face should have the modified attributes
if (token.isEntersTransformed()) {
applyAdditionsToToken(token.getBackFace());
} else {
applyAdditionsToToken(token);
}
token.putOntoBattlefield(number, game, source, playerId == null ? source.getControllerId() : playerId, tapped, attacking, attackedPlayer, attachedTo);
for (UUID tokenId : token.getLastAddedTokenIds()) { // by cards like Doubling Season multiple tokens can be added to the battlefield
Permanent tokenPermanent = game.getPermanent(tokenId);
if (tokenPermanent != null) {
addedTokenPermanents.add(tokenPermanent);
// TODO: Workaround to add counters to all created tokens, necessary for correct interactions with cards like Chatterfang, Squirrel General and Ochre Jelly / Printlifter Ooze. See #10786
if (counter != null && numberOfCounters > 0) {
tokenPermanent.addCounters(counter.createInstance(numberOfCounters), source.getControllerId(), source, game);
}
}
}
return true;
}
private void applyAdditionsToToken(Token token) {
if (becomesArtifact) {
token.addCardType(CardType.ARTIFACT);
}
@ -281,19 +303,6 @@ public class CreateTokenCopyTargetEffect extends OneShotEffect {
token.removeAbility(ability);
}
}
token.putOntoBattlefield(number, game, source, playerId == null ? source.getControllerId() : playerId, tapped, attacking, attackedPlayer, attachedTo);
for (UUID tokenId : token.getLastAddedTokenIds()) { // by cards like Doubling Season multiple tokens can be added to the battlefield
Permanent tokenPermanent = game.getPermanent(tokenId);
if (tokenPermanent != null) {
addedTokenPermanents.add(tokenPermanent);
// TODO: Workaround to add counters to all created tokens, necessary for correct interactions with cards like Chatterfang, Squirrel General and Ochre Jelly / Printlifter Ooze. See #10786
if (counter != null && numberOfCounters > 0) {
tokenPermanent.addCounters(counter.createInstance(numberOfCounters), source.getControllerId(), source, game);
}
}
}
return true;
}
@Override

View file

@ -3,7 +3,6 @@ package mage.abilities.effects.common;
import mage.abilities.Ability;
import mage.abilities.effects.OneShotEffect;
import mage.cards.Card;
import mage.cards.ModalDoubleFacedCard;
import mage.constants.Outcome;
import mage.constants.Zone;
import mage.game.Game;
@ -37,10 +36,6 @@ public class ReturnToHandAttachedEffect extends OneShotEffect {
return false;
}
Card card = permanent.getMainCard();
// TODO: Once MDFC ZCC increments are fixed properly, can remove this special case. For now must allow so effect works.
if (permanent.getZoneChangeCounter(game) + 1 != card.getZoneChangeCounter(game) && !(card instanceof ModalDoubleFacedCard)) {
return false;
}
return player.moveCards(card, Zone.HAND, source, game);
}
}

View file

@ -1,8 +1,12 @@
package mage.abilities.effects.common;
import mage.MageObject;
import mage.Mana;
import mage.abilities.Abilities;
import mage.abilities.AbilitiesImpl;
import mage.abilities.Ability;
import mage.abilities.SpellAbility;
import mage.abilities.common.RoomUnlockAbility;
import mage.abilities.costs.mana.ManaCost;
import mage.abilities.costs.mana.ManaCosts;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.ContinuousEffectImpl;
@ -15,12 +19,13 @@ import mage.constants.SubLayer;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.game.permanent.PermanentCard;
import mage.util.CardUtil;
import java.util.UUID;
/**
* @author oscscull
* Continuous effect that sets the name and mana value of a Room permanent based
* on its unlocked halves.
*
* Functions as a characteristic-defining ability.
* 709.5. Some split cards are permanent cards with a single shared type line.
* A shared type line on such an object represents two static abilities that
@ -33,11 +38,13 @@ import mage.game.permanent.PermanentCard;
* object's right half."
* These abilities, as well as which half of that permanent a characteristic is
* in, are part of that object's copiable values.
* @author oscscull
*/
public class RoomCharacteristicsEffect extends ContinuousEffectImpl {
public RoomCharacteristicsEffect() {
super(Duration.WhileOnBattlefield, Layer.PTChangingEffects_7, SubLayer.CharacteristicDefining_7a,
super(Duration.WhileOnBattlefield, Layer.TextChangingEffects_3, SubLayer.NA,
Outcome.Neutral);
staticText = "";
}
@ -59,6 +66,107 @@ public class RoomCharacteristicsEffect extends ContinuousEffectImpl {
return false;
}
return removeCharacteristics(game, permanent);
}
public boolean removeCharacteristics(Game game, Permanent permanent) {
Card roomCardBlueprint = getCard(permanent);
if (!(roomCardBlueprint instanceof SplitCard)) {
return false;
}
SplitCard roomCard = (SplitCard) roomCardBlueprint;
// Remove the name based on unlocked halves
String newName = permanent.getName();
boolean isLeftUnlocked = permanent.isLeftDoorUnlocked();
if (!isLeftUnlocked && roomCard.getLeftHalfCard() != null) {
newName = newName.replace(roomCard.getLeftHalfCard().getName() + " // ", "");
}
boolean isRightUnlocked = permanent.isRightDoorUnlocked();
if (!isRightUnlocked && roomCard.getRightHalfCard() != null) {
newName = newName
.replace(" // " + roomCard.getRightHalfCard().getName(), "")
.replace(roomCard.getRightHalfCard().getName(), "");
}
permanent.setName(newName);
// Set the mana value based on unlocked halves
// Create a new Mana object to accumulate the costs
SpellAbility roomCardSpellAbility = roomCard.getSpellAbility().copy();
// Remove the mana from the left half's cost to our total Mana object
if (!isLeftUnlocked) {
ManaCosts<ManaCost> leftHalfManaCost = null;
if (roomCard.getLeftHalfCard() != null && roomCard.getLeftHalfCard().getSpellAbility() != null) {
leftHalfManaCost = roomCard.getLeftHalfCard().getSpellAbility().getManaCosts();
}
if (leftHalfManaCost != null) {
CardUtil.adjustCost(roomCardSpellAbility, leftHalfManaCost, true);
}
}
// Remove the mana from the right half's cost to our total Mana object
if (!isRightUnlocked) {
ManaCosts<ManaCost> rightHalfManaCost = null;
if (roomCard.getRightHalfCard() != null && roomCard.getRightHalfCard().getSpellAbility() != null) {
rightHalfManaCost = roomCard.getRightHalfCard().getSpellAbility().getManaCosts();
}
if (rightHalfManaCost != null) {
CardUtil.adjustCost(roomCardSpellAbility, rightHalfManaCost, true);
}
}
ManaCosts<ManaCost> roomCardManaCosts = roomCardSpellAbility.getManaCostsToPay();
if (roomCardManaCosts.getText().equals("{0}")) {
roomCardManaCosts = new ManaCostsImpl<>();
}
permanent.setManaCost(roomCardManaCosts);
// Remove abilities from locked halves and add unlock abilities
Abilities<Ability> removedLeftAbilities = new AbilitiesImpl<>();
Abilities<Ability> removedRightAbilities = new AbilitiesImpl<>();
Card abilitySource = permanent;
if (permanent.isCopy()) {
abilitySource = (Card) permanent.getCopyFrom();
}
for (Ability ability : abilitySource.getAbilities(game)) {
if (!isLeftUnlocked) {
if (roomCard.getLeftHalfCard() != null && roomCard.getLeftHalfCard().getAbilities().contains(ability)) {
if (!removedLeftAbilities.contains(ability)) {
removedLeftAbilities.add(ability);
}
permanent.removeAbility(ability, null, game);
continue;
}
}
if (!isRightUnlocked) {
if (roomCard.getRightHalfCard() != null && roomCard.getRightHalfCard().getAbilities().contains(ability)) {
if (!removedRightAbilities.contains(ability)) {
removedRightAbilities.add(ability);
}
permanent.removeAbility(ability, null, game);
}
}
}
// Add the Special Action to unlock doors.
// These will ONLY be active if the corresponding half is LOCKED!
if (!removedLeftAbilities.isEmpty()) {
RoomUnlockAbility leftUnlockAbility = new RoomUnlockAbility(roomCard.getLeftHalfCard().getManaCost(), true);
permanent.addAbility(leftUnlockAbility, roomCard.getLeftHalfCard().getId(), game);
}
if (!removedRightAbilities.isEmpty()) {
RoomUnlockAbility rightUnlockAbility = new RoomUnlockAbility(roomCard.getRightHalfCard().getManaCost(), false);
permanent.addAbility(rightUnlockAbility, roomCard.getRightHalfCard().getId(), game);
}
return true;
}
private static Card getCard(Permanent permanent) {
Card roomCardBlueprint;
// Handle copies
@ -74,69 +182,34 @@ public class RoomCharacteristicsEffect extends ContinuousEffectImpl {
} else {
roomCardBlueprint = permanent.getMainCard();
}
return roomCardBlueprint;
}
if (!(roomCardBlueprint instanceof SplitCard)) {
return false;
}
SplitCard roomCard = (SplitCard) roomCardBlueprint;
// Set the name based on unlocked halves
String newName = "";
boolean isLeftUnlocked = permanent.isLeftDoorUnlocked();
if (isLeftUnlocked && roomCard.getLeftHalfCard() != null) {
newName += roomCard.getLeftHalfCard().getName();
}
boolean isRightUnlocked = permanent.isRightDoorUnlocked();
if (isRightUnlocked && roomCard.getRightHalfCard() != null) {
if (!newName.isEmpty()) {
newName += " // "; // Split card name separator
}
newName += roomCard.getRightHalfCard().getName();
}
permanent.setName(newName);
// Set the mana value based on unlocked halves
// Create a new Mana object to accumulate the costs
Mana totalManaCost = new Mana();
// Add the mana from the left half's cost to our total Mana object
if (isLeftUnlocked) {
ManaCosts leftHalfManaCost = null;
if (roomCard.getLeftHalfCard() != null && roomCard.getLeftHalfCard().getSpellAbility() != null) {
leftHalfManaCost = roomCard.getLeftHalfCard().getSpellAbility().getManaCosts();
}
if (leftHalfManaCost != null) {
totalManaCost.add(leftHalfManaCost.getMana());
public void restoreUnlockedStats(Game game, Permanent permanent) {
// remove unlock abilities
for (Ability ability : permanent.getAbilities(game)) {
if (ability instanceof RoomUnlockAbility) {
if (((RoomUnlockAbility) ability).isLeftHalf() && permanent.isLeftDoorUnlocked()) {
permanent.removeAbility(ability, null, game);
} else if (!((RoomUnlockAbility) ability).isLeftHalf() && permanent.isRightDoorUnlocked()) {
permanent.removeAbility(ability, null, game);
}
}
}
// Add the mana from the right half's cost to our total Mana object
if (isRightUnlocked) {
ManaCosts rightHalfManaCost = null;
if (roomCard.getRightHalfCard() != null && roomCard.getRightHalfCard().getSpellAbility() != null) {
rightHalfManaCost = roomCard.getRightHalfCard().getSpellAbility().getManaCosts();
}
if (rightHalfManaCost != null) {
totalManaCost.add(rightHalfManaCost.getMana());
// restore removed abilities
// copies need abilities to be added back to game state for triggers
SplitCard roomCard = (SplitCard) getCard(permanent);
UUID sourceId = permanent.isCopy() ? permanent.getId() : null;
Game gameParam = permanent.isCopy() ? game : null;
if (permanent.isLeftDoorUnlocked()) {
for (Ability ability : roomCard.getLeftHalfCard().getAbilities()) {
permanent.addAbility(ability, sourceId, gameParam, true);
}
}
String newManaCostString = totalManaCost.toString();
ManaCostsImpl newManaCosts;
// If both halves are locked or total 0, it's 0mv.
if (newManaCostString.isEmpty() || totalManaCost.count() == 0) {
newManaCosts = new ManaCostsImpl<>("");
} else {
newManaCosts = new ManaCostsImpl<>(newManaCostString);
if (permanent.isRightDoorUnlocked()) {
for (Ability ability : roomCard.getRightHalfCard().getAbilities()) {
permanent.addAbility(ability, sourceId, gameParam, true);
}
}
permanent.setManaCost(newManaCosts);
return true;
}
}

View file

@ -16,7 +16,7 @@ import mage.abilities.effects.common.InfoEffect;
import mage.abilities.keyword.WardAbility;
import mage.cards.Card;
import mage.cards.CardImpl;
import mage.cards.ModalDoubleFacedCard;
import mage.cards.DoubleFacedCard;
import mage.cards.repository.TokenInfo;
import mage.cards.repository.TokenRepository;
import mage.constants.*;
@ -375,9 +375,9 @@ public class BecomesFaceDownCreatureEffect extends ContinuousEffectImpl {
// it can't transform. If the front face of the card is a creature card, you can turn it face up by paying
// its mana cost. If you do, its front face will be up.
if (card instanceof ModalDoubleFacedCard) {
if (card instanceof DoubleFacedCard) {
// only MDFC uses independent card sides on 2024
return ((ModalDoubleFacedCard) card).getLeftHalfCard();
return ((DoubleFacedCard) card).getLeftHalfCard();
} else {
return card;
}

View file

@ -9,6 +9,7 @@ import mage.abilities.costs.common.ExileSourceCost;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.OneShotEffect;
import mage.cards.Card;
import mage.cards.TransformingDoubleFacedCardHalf;
import mage.constants.*;
import mage.filter.FilterCard;
import mage.filter.FilterPermanent;

View file

@ -300,6 +300,7 @@ class ForetellAddCostEffect extends ContinuousEffectImpl {
if (game.getState().getZone(mainCardId) == Zone.EXILED) {
String foretellCost = (String) game.getState().getValue(mainCardId.toString() + "Foretell Cost");
String foretellSplitCost = (String) game.getState().getValue(mainCardId.toString() + "Foretell Split Cost");
// TODO: clean this up
if (card instanceof SplitCard) {
if (foretellCost != null) {
SplitCardHalf leftHalfCard = ((SplitCard) card).getLeftHalfCard();
@ -363,6 +364,14 @@ class ForetellAddCostEffect extends ContinuousEffectImpl {
ability.setAbilityName(spellCard.getName());
game.getState().addOtherAbility(spellCard, ability);
}
} else if (card instanceof TransformingDoubleFacedCard && foretellCost != null) {
Card frontCard = ((TransformingDoubleFacedCard) card).getLeftHalfCard();
ForetellCostAbility ability = new ForetellCostAbility(foretellCost);
ability.setSourceId(frontCard.getId());
ability.setControllerId(source.getControllerId());
ability.setSpellAbilityType(frontCard.getSpellAbility().getSpellAbilityType());
ability.setAbilityName(frontCard.getName());
game.getState().addOtherAbility(frontCard, ability);
} else if (foretellCost != null) {
ForetellCostAbility ability = new ForetellCostAbility(foretellCost);
ability.setSourceId(card.getId());

View file

@ -18,7 +18,7 @@ import mage.abilities.effects.common.counter.RemoveCounterSourceEffect;
import mage.abilities.triggers.BeginningOfUpkeepTriggeredAbility;
import mage.cards.Card;
import mage.cards.CardsImpl;
import mage.cards.ModalDoubleFacedCard;
import mage.cards.DoubleFacedCard;
import mage.constants.*;
import mage.counters.CounterType;
import mage.filter.StaticFilters;
@ -177,10 +177,10 @@ public class SuspendAbility extends SpecialAction {
* or added by Jhoira of the Ghitu
*/
public static void addSuspendTemporaryToCard(Card card, Ability source, Game game) {
if (card instanceof ModalDoubleFacedCard) {
if (card instanceof DoubleFacedCard) {
// Need to ensure the suspend ability gets put on the left side card
// since counters get added to this card.
card = ((ModalDoubleFacedCard) card).getLeftHalfCard();
card = ((DoubleFacedCard) card).getLeftHalfCard();
}
SuspendAbility ability = new SuspendAbility(0, null, card, false);
ability.setSourceId(card.getId());

View file

@ -1,5 +1,6 @@
package mage.cards;
import mage.MageInt;
import mage.MageObject;
import mage.Mana;
import mage.abilities.Abilities;
@ -72,6 +73,7 @@ public interface Card extends MageObject, Ownerable {
SpellAbility getSecondFaceSpellAbility();
//TODO: remove after tdfc rework
boolean isNightCard();
default boolean meldsWith(Card card) {
@ -250,6 +252,10 @@ public interface Card extends MageObject, Ownerable {
List<UUID> getAttachments();
void setPT(int power, int toughness);
void setPT(MageInt power, MageInt toughness);
/**
* @param attachment can be any object: card, permanent, token
* @param source can be null for default checks like state base

View file

@ -1,5 +1,6 @@
package mage.cards;
import mage.MageInt;
import mage.MageObject;
import mage.MageObjectImpl;
import mage.Mana;
@ -126,6 +127,11 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
nightCard = card.nightCard;
secondSideCardClazz = card.secondSideCardClazz;
secondSideCard = null; // will be set on first getSecondCardFace call if card has one
// TODO: temporary until cards tdfc cards are converted
// can do normal copy after
if (card.secondSideCard instanceof DoubleFacedCardHalf) {
secondSideCard = card.secondSideCard.copy();
}
if (card.secondSideCard instanceof MockableCard) {
// workaround to support gui's mock cards
secondSideCard = card.secondSideCard.copy();
@ -393,6 +399,17 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
this.abilities.setControllerId(ownerId);
}
@Override
public void setPT(int power, int toughness) {
this.setPT(new MageInt(power), new MageInt(toughness));
}
@Override
public void setPT(MageInt power, MageInt toughness) {
this.power = power;
this.toughness = toughness;
}
@Override
public UUID getControllerOrOwnerId() {
return getOwnerId();
@ -517,13 +534,13 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
}
}
// handle half of Modal Double Faces Cards on stack
if (stackObject == null && (this instanceof ModalDoubleFacedCard)) {
stackObject = game.getStack().getSpell(((ModalDoubleFacedCard) this).getLeftHalfCard().getId(),
// handle half of Double Faces Cards on stack
if (stackObject == null && (this instanceof DoubleFacedCard)) {
stackObject = game.getStack().getSpell(((DoubleFacedCard) this).getLeftHalfCard().getId(),
false);
if (stackObject == null) {
stackObject = game.getStack()
.getSpell(((ModalDoubleFacedCard) this).getRightHalfCard().getId(), false);
.getSpell(((DoubleFacedCard) this).getRightHalfCard().getId(), false);
}
}
@ -650,7 +667,7 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
// If a spell or ability instructs a player to transform a permanent that
// isnt represented by a transforming token or a transforming double-faced
// card, nothing happens.
return this.secondSideCardClazz != null || this.nightCard;
return this.secondSideCardClazz != null || this.nightCard || this.secondSideCard != null;
}
@Override
@ -947,7 +964,7 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
}
}
}
if (controller != null && spellAbility != null && !spellAbility.getTargets().isEmpty()){
if (controller != null && spellAbility != null && !spellAbility.getTargets().isEmpty()) {
// Line of code below functionally gets the target of the aura's Enchant ability, then compares to this permanent. Enchant improperly implemented in XMage, see #9583
// Note: stillLegalTarget used exclusively to account for Dream Leash. Can be made canTarget in the event that that card is rewritten (and "stillLegalTarget" removed from TargetImpl).
canAttach &= spellAbility.getTargets().get(0).copy().withNotTarget(true).stillLegalTarget(controller, this.getId(), source, game);

View file

@ -0,0 +1,413 @@
package mage.cards;
import mage.MageInt;
import mage.MageObject;
import mage.ObjectColor;
import mage.abilities.*;
import mage.abilities.costs.mana.ManaCost;
import mage.abilities.costs.mana.ManaCosts;
import mage.constants.*;
import mage.counters.Counter;
import mage.counters.Counters;
import mage.game.Game;
import mage.game.GameState;
import mage.game.events.ZoneChangeEvent;
import mage.util.CardUtil;
import mage.util.SubTypes;
import java.util.List;
import java.util.UUID;
/**
* @author JayDi85 - originally from ModalDoubleFaceCard
*/
public abstract class DoubleFacedCard extends CardImpl implements CardWithHalves {
protected DoubleFacedCardHalf leftHalfCard; // main card in all zone
protected DoubleFacedCardHalf rightHalfCard; // second side card, can be only in stack and battlefield zones
protected DoubleFacedCard(UUID ownerId, CardSetInfo setInfo, CardType[] cardTypes, String costs, SpellAbilityType spellAbilityType) {
super(ownerId, setInfo, cardTypes, costs, spellAbilityType);
}
public DoubleFacedCard(DoubleFacedCard card) {
super(card);
// make sure all parts created and parent ref added
this.leftHalfCard = (DoubleFacedCardHalf) card.getLeftHalfCard().copy();
leftHalfCard.setParentCard(this);
this.rightHalfCard = (DoubleFacedCardHalf) card.getRightHalfCard().copy();
rightHalfCard.setParentCard(this);
}
public DoubleFacedCardHalf getLeftHalfCard() {
return leftHalfCard;
}
public DoubleFacedCardHalf getRightHalfCard() {
return leftHalfCard;
}
public void setParts(DoubleFacedCardHalf leftHalfCard, DoubleFacedCardHalf rightHalfCard) {
// for card copy only - set new parts
this.leftHalfCard = leftHalfCard;
leftHalfCard.setParentCard(this);
this.rightHalfCard = rightHalfCard;
rightHalfCard.setParentCard(this);
}
@Override
public void assignNewId() {
super.assignNewId();
leftHalfCard.assignNewId();
rightHalfCard.assignNewId();
}
@Override
public void setCopy(boolean isCopy, MageObject copiedFrom) {
super.setCopy(isCopy, copiedFrom);
leftHalfCard.setCopy(isCopy, copiedFrom);
rightHalfCard.setCopy(isCopy, copiedFrom);
}
private void setSideZones(Zone mainZone, Game game) {
switch (mainZone) {
case BATTLEFIELD:
case STACK:
throw new IllegalArgumentException("Wrong code usage: you must put to battlefield/stack only real side card (half), not main");
default:
game.setZone(leftHalfCard.getId(), mainZone);
game.setZone(rightHalfCard.getId(), mainZone);
break;
}
checkGoodZones(game, this);
}
@Override
public boolean moveToZone(Zone toZone, Ability source, Game game, boolean flag, List<UUID> appliedEffects) {
if (super.moveToZone(toZone, source, game, flag, appliedEffects)) {
Zone currentZone = game.getState().getZone(getId());
setSideZones(currentZone, game);
return true;
}
return false;
}
@Override
public void setZone(Zone zone, Game game) {
super.setZone(zone, game);
setSideZones(zone, game);
}
@Override
public boolean moveToExile(UUID exileId, String name, Ability source, Game game, List<UUID> appliedEffects) {
if (super.moveToExile(exileId, name, source, game, appliedEffects)) {
Zone currentZone = game.getState().getZone(getId());
setSideZones(currentZone, game);
return true;
}
return false;
}
/**
* Runtime check for good zones and other MDF data
*/
public static void checkGoodZones(Game game, DoubleFacedCard card) {
Card leftPart = card.getLeftHalfCard();
Card rightPart = card.getRightHalfCard();
Zone zoneMain = game.getState().getZone(card.getId());
Zone zoneLeft = game.getState().getZone(leftPart.getId());
Zone zoneRight = game.getState().getZone(rightPart.getId());
// runtime check:
// * in battlefield and stack - card + one of the sides (another side in outside zone)
// * in other zones - card + both sides (need both sides due cost reductions, spell and other access before put to stack)
//
// 712.8a While a double-faced card is outside the game or in a zone other than the battlefield or stack,
// it has only the characteristics of its front face.
//
// 712.8f While a modal double-faced spell is on the stack or a modal double-faced permanent is on the battlefield,
// it has only the characteristics of the face thats up.
Zone needZoneLeft;
Zone needZoneRight;
switch (zoneMain) {
case BATTLEFIELD:
case STACK:
if (zoneMain == zoneLeft) {
needZoneLeft = zoneMain;
needZoneRight = Zone.OUTSIDE;
} else if (zoneMain == zoneRight) {
needZoneLeft = Zone.OUTSIDE;
needZoneRight = zoneMain;
} else {
// impossible
needZoneLeft = zoneMain;
needZoneRight = Zone.OUTSIDE;
}
break;
default:
needZoneLeft = zoneMain;
needZoneRight = zoneMain;
break;
}
if (zoneLeft != needZoneLeft || zoneRight != needZoneRight) {
throw new IllegalStateException("Wrong code usage: MDF card uses wrong zones - " + card
+ "\r\n" + String.format("* main zone: %s", zoneMain)
+ "\r\n" + String.format("* left side: need %s, actual %s", needZoneLeft, zoneLeft)
+ "\r\n" + String.format("* right side: need %s, actual %s", needZoneRight, zoneRight));
}
}
@Override
public boolean removeFromZone(Game game, Zone fromZone, Ability source) {
// zone contains only one main card
return super.removeFromZone(game, fromZone, source);
}
@Override
public void updateZoneChangeCounter(Game game, ZoneChangeEvent event) {
if (isCopy()) { // same as meld cards
super.updateZoneChangeCounter(game, event);
return;
}
super.updateZoneChangeCounter(game, event);
game.getState().updateZoneChangeCounter(leftHalfCard.getId());
game.getState().updateZoneChangeCounter(rightHalfCard.getId());
}
@Override
public Counters getCounters(Game game) {
return getCounters(game.getState());
}
@Override
public Counters getCounters(GameState state) {
return state.getCardState(leftHalfCard.getId()).getCounters();
}
@Override
public boolean addCounters(Counter counter, UUID playerAddingCounters, Ability source, Game game, List<UUID> appliedEffects, boolean isEffect, int maxCounters) {
return leftHalfCard.addCounters(counter, playerAddingCounters, source, game, appliedEffects, isEffect, maxCounters);
}
@Override
public void removeCounters(String counterName, int amount, Ability source, Game game) {
leftHalfCard.removeCounters(counterName, amount, source, game);
}
@Override
public boolean cast(Game game, Zone fromZone, SpellAbility ability, UUID controllerId) {
if (this.leftHalfCard.getSpellAbility() != null) {
this.leftHalfCard.getSpellAbility().setControllerId(controllerId);
}
if (this.rightHalfCard.getSpellAbility() != null) {
this.rightHalfCard.getSpellAbility().setControllerId(controllerId);
}
return super.cast(game, fromZone, ability, controllerId);
}
@Override
public List<SuperType> getSuperType(Game game) {
// CardImpl's constructor can call some code on init, so you must check left/right before
// it's a bad workaround
return leftHalfCard != null ? leftHalfCard.getSuperType(game) : supertype;
}
@Override
public List<CardType> getCardType(Game game) {
// CardImpl's constructor can call some code on init, so you must check left/right before
// it's a bad workaround
return leftHalfCard != null ? leftHalfCard.getCardType(game) : cardType;
}
@Override
public SubTypes getSubtype() {
// rules: While a double-faced card isnt on the stack or battlefield, consider only the characteristics of its front face.
// CardImpl's constructor can call some code on init, so you must check left/right before
return leftHalfCard != null ? leftHalfCard.getSubtype() : subtype;
}
@Override
public SubTypes getSubtype(Game game) {
// rules: While a double-faced card isnt on the stack or battlefield, consider only the characteristics of its front face.
// CardImpl's constructor can call some code on init, so you must check left/right before
return leftHalfCard != null ? leftHalfCard.getSubtype(game) : subtype;
}
@Override
public boolean hasSubtype(SubType subtype, Game game) {
return leftHalfCard.hasSubtype(subtype, game);
}
@Override
public Abilities<Ability> getAbilities() {
return getInnerAbilities(true, true);
}
@Override
public Abilities<Ability> getInitAbilities() {
// must init only parent related abilities, spell card must be init separately
return getInnerAbilities(false, false);
}
public Abilities<Ability> getSharedAbilities(Game game) {
// no shared abilities for mdf cards (e.g. must be left or right only)
return new AbilitiesImpl<>();
}
@Override
public Abilities<Ability> getAbilities(Game game) {
return getInnerAbilities(game, true, true);
}
private boolean isIgnoreDefaultAbility(Ability ability) {
// ignore default play/spell ability from main card (only halves are actual)
// default abilities added on card creation from card type and can't be skipped
// skip cast spell
if (ability instanceof SpellAbility) {
SpellAbilityType type = ((SpellAbility) ability).getSpellAbilityType();
return type == SpellAbilityType.MODAL || type == SpellAbilityType.TRANSFORMED;
}
// skip play land
return ability instanceof PlayLandAbility;
}
private boolean isIgnoreTransformSpellAbility(Ability ability) {
return ability instanceof SpellAbility && ((SpellAbility) ability).getSpellAbilityType() == SpellAbilityType.TRANSFORMED_RIGHT;
}
private Abilities<Ability> getInnerAbilities(Game game, boolean showLeftSide, boolean showRightSide) {
Abilities<Ability> allAbilites = new AbilitiesImpl<>();
for (Ability ability : super.getAbilities(game)) {
if (isIgnoreDefaultAbility(ability)) {
continue;
}
allAbilites.add(ability);
}
if (showLeftSide) {
allAbilites.addAll(leftHalfCard.getAbilities(game));
}
if (showRightSide) {
for (Ability ability: rightHalfCard.getAbilities(game)) {
if (isIgnoreTransformSpellAbility(ability)) {
continue;
}
allAbilites.add(ability);
}
}
return allAbilites;
}
private Abilities<Ability> getInnerAbilities(boolean showLeftSide, boolean showRightSide) {
Abilities<Ability> allAbilites = new AbilitiesImpl<>();
for (Ability ability : super.getAbilities()) {
if (isIgnoreDefaultAbility(ability)) {
continue;
}
allAbilites.add(ability);
}
if (showLeftSide) {
allAbilites.addAll(leftHalfCard.getAbilities());
}
if (showRightSide) {
for (Ability ability: rightHalfCard.getAbilities()) {
if (isIgnoreTransformSpellAbility(ability)) {
continue;
}
allAbilites.add(ability);
}
}
return allAbilites;
}
@Override
public List<String> getRules() {
// rules must show only main side (another side visible by toggle/transform button in GUI)
// card hints from both sides
return CardUtil.getCardRulesWithAdditionalInfo(
this,
this.getInnerAbilities(true, false),
this.getInnerAbilities(true, true)
);
}
@Override
public List<String> getRules(Game game) {
// rules must show only main side (another side visible by toggle/transform button in GUI)
// card hints from both sides
return CardUtil.getCardRulesWithAdditionalInfo(
game,
this,
this.getInnerAbilities(game, true, false),
this.getInnerAbilities(game, true, true)
);
}
@Override
public boolean hasAbility(Ability ability, Game game) {
return super.hasAbility(ability, game);
}
@Override
public ObjectColor getColor() {
return leftHalfCard.getColor();
}
@Override
public ObjectColor getColor(Game game) {
return leftHalfCard.getColor(game);
}
@Override
public ObjectColor getFrameColor(Game game) {
return leftHalfCard.getFrameColor(game);
}
@Override
public void setOwnerId(UUID ownerId) {
super.setOwnerId(ownerId);
abilities.setControllerId(ownerId);
leftHalfCard.getAbilities().setControllerId(ownerId);
leftHalfCard.setOwnerId(ownerId);
rightHalfCard.getAbilities().setControllerId(ownerId);
rightHalfCard.setOwnerId(ownerId);
}
@Override
public ManaCosts<ManaCost> getManaCost() {
return leftHalfCard.getManaCost();
}
@Override
public int getManaValue() {
// Rules:
// The converted mana cost of a modal double-faced card is based on the characteristics of the
// face thats being considered. On the stack and battlefield, consider whichever face is up.
// In all other zones, consider only the front face. This is different than how the converted
// mana cost of a transforming double-faced card is determined.
// on stack or battlefield it must be half card with own cost
return leftHalfCard.getManaValue();
}
@Override
public MageInt getPower() {
return leftHalfCard.getPower();
}
@Override
public MageInt getToughness() {
return leftHalfCard.getToughness();
}
}

View file

@ -1,25 +1,25 @@
package mage.cards;
import mage.MageInt;
import mage.abilities.Ability;
import mage.constants.*;
import mage.game.Game;
import mage.game.events.ZoneChangeEvent;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
/**
* @author JayDi85
* @author JayDi85 - originally from ModalDoubleFaceCardHalf
*/
public class ModalDoubleFacedCardHalfImpl extends CardImpl implements ModalDoubleFacedCardHalf {
public abstract class DoubleFacedCardHalf extends CardImpl implements SubCard<DoubleFacedCard> {
ModalDoubleFacedCard parentCard;
protected DoubleFacedCard parentCard;
public ModalDoubleFacedCardHalfImpl(
public DoubleFacedCardHalf(
UUID ownerId, CardSetInfo setInfo,
SuperType[] cardSuperTypes, CardType[] cardTypes, SubType[] cardSubTypes,
String costs, ModalDoubleFacedCard parentCard, SpellAbilityType spellAbilityType
String costs, DoubleFacedCard parentCard, SpellAbilityType spellAbilityType
) {
super(ownerId, setInfo, cardTypes, costs, spellAbilityType);
this.supertype.addAll(Arrays.asList(cardSuperTypes));
@ -27,7 +27,7 @@ public class ModalDoubleFacedCardHalfImpl extends CardImpl implements ModalDoubl
this.parentCard = parentCard;
}
protected ModalDoubleFacedCardHalfImpl(final ModalDoubleFacedCardHalfImpl card) {
protected DoubleFacedCardHalf(final DoubleFacedCardHalf card) {
super(card);
this.parentCard = card.parentCard;
}
@ -49,6 +49,11 @@ public class ModalDoubleFacedCardHalfImpl extends CardImpl implements ModalDoubl
return parentCard.getCardNumber();
}
@Override
public boolean isTransformable() {
return getOtherSide().isPermanent();
}
@Override
public boolean moveToZone(Zone toZone, Ability source, Game game, boolean flag, List<UUID> appliedEffects) {
return parentCard.moveToZone(toZone, source, game, flag, appliedEffects);
@ -65,25 +70,23 @@ public class ModalDoubleFacedCardHalfImpl extends CardImpl implements ModalDoubl
}
@Override
public ModalDoubleFacedCard getMainCard() {
public Card getMainCard() {
return parentCard;
}
@Override
public void updateZoneChangeCounter(Game game, ZoneChangeEvent event) {
parentCard.updateZoneChangeCounter(game, event);
}
@Override
public void setZone(Zone zone, Game game) {
// see ModalDoubleFacedCard.checkGoodZones for details
// see DoubleFacedCard.checkGoodZones for details
game.setZone(parentCard.getId(), zone);
game.setZone(this.getId(), zone);
// find another side to sync
ModalDoubleFacedCardHalf otherSide;
if (!parentCard.getLeftHalfCard().getId().equals(this.getId())) {
otherSide = parentCard.getLeftHalfCard();
} else if (!parentCard.getRightHalfCard().getId().equals(this.getId())) {
otherSide = parentCard.getRightHalfCard();
} else {
throw new IllegalStateException("Wrong code usage: MDF halves must use different ids");
}
Card otherSide = getOtherSide();
switch (zone) {
case STACK:
@ -96,33 +99,39 @@ public class ModalDoubleFacedCardHalfImpl extends CardImpl implements ModalDoubl
break;
}
ModalDoubleFacedCard.checkGoodZones(game, parentCard);
DoubleFacedCard.checkGoodZones(game, parentCard);
}
public Card getOtherSide() {
Card otherSide;
if (!parentCard.getLeftHalfCard().getId().equals(this.getId())) {
otherSide = parentCard.getLeftHalfCard();
} else if (!parentCard.getRightHalfCard().getId().equals(this.getId())) {
otherSide = parentCard.getRightHalfCard();
} else {
throw new IllegalStateException("Wrong code usage: MDF halves must use different ids");
}
return otherSide;
}
public boolean isBackSide() {
if (parentCard.getLeftHalfCard().getId().equals(this.getId())) {
return false;
} else if (parentCard.getRightHalfCard().getId().equals(this.getId())) {
return true;
} else {
throw new IllegalStateException("Wrong code usage: MDF halves must use different ids");
}
}
@Override
public ModalDoubleFacedCardHalfImpl copy() {
return new ModalDoubleFacedCardHalfImpl(this);
}
@Override
public void setParentCard(ModalDoubleFacedCard card) {
public void setParentCard(DoubleFacedCard card) {
this.parentCard = card;
}
@Override
public ModalDoubleFacedCard getParentCard() {
return this.parentCard;
}
@Override
public void setPT(int power, int toughness) {
this.setPT(new MageInt(power), new MageInt(toughness));
}
@Override
public void setPT(MageInt power, MageInt toughness) {
this.power = power;
this.toughness = toughness;
public DoubleFacedCard getParentCard() {
return parentCard;
}
@Override

View file

@ -1,30 +1,15 @@
package mage.cards;
import mage.MageInt;
import mage.MageObject;
import mage.ObjectColor;
import mage.abilities.*;
import mage.abilities.costs.mana.ManaCost;
import mage.abilities.costs.mana.ManaCosts;
import mage.constants.*;
import mage.counters.Counter;
import mage.counters.Counters;
import mage.game.Game;
import mage.game.GameState;
import mage.game.events.ZoneChangeEvent;
import mage.util.CardUtil;
import mage.util.SubTypes;
import java.util.List;
import java.util.UUID;
/**
* @author JayDi85
*/
public abstract class ModalDoubleFacedCard extends CardImpl implements CardWithHalves {
protected Card leftHalfCard; // main card in all zone
protected Card rightHalfCard; // second side card, can be only in stack and battlefield zones
public abstract class ModalDoubleFacedCard extends DoubleFacedCard {
public ModalDoubleFacedCard(
UUID ownerId, CardSetInfo setInfo,
@ -48,184 +33,21 @@ public abstract class ModalDoubleFacedCard extends CardImpl implements CardWithH
) {
super(ownerId, setInfo, typesLeft, costsLeft + costsRight, SpellAbilityType.MODAL);
// main card name must be same as left side
leftHalfCard = new ModalDoubleFacedCardHalfImpl(
leftHalfCard = new ModalDoubleFacedCardHalf(
this.getOwnerId(), setInfo.copy(),
superTypesLeft, typesLeft, subTypesLeft, costsLeft,
this, SpellAbilityType.MODAL_LEFT
);
rightHalfCard = new ModalDoubleFacedCardHalfImpl(
rightHalfCard = new ModalDoubleFacedCardHalf(
this.getOwnerId(), new CardSetInfo(secondSideName, setInfo),
superTypesRight, typesRight, subTypesRight, costsRight,
this, SpellAbilityType.MODAL_RIGHT
);
this.secondSideCard = rightHalfCard;
}
public ModalDoubleFacedCard(ModalDoubleFacedCard card) {
public ModalDoubleFacedCard(final ModalDoubleFacedCard card) {
super(card);
// make sure all parts created and parent ref added
this.leftHalfCard = card.getLeftHalfCard().copy();
((ModalDoubleFacedCardHalf) leftHalfCard).setParentCard(this);
this.rightHalfCard = card.rightHalfCard.copy();
((ModalDoubleFacedCardHalf) rightHalfCard).setParentCard(this);
}
public ModalDoubleFacedCardHalf getLeftHalfCard() {
return (ModalDoubleFacedCardHalf) leftHalfCard;
}
public ModalDoubleFacedCardHalf getRightHalfCard() {
return (ModalDoubleFacedCardHalf) rightHalfCard;
}
public void setParts(ModalDoubleFacedCardHalf leftHalfCard, ModalDoubleFacedCardHalf rightHalfCard) {
// for card copy only - set new parts
this.leftHalfCard = leftHalfCard;
leftHalfCard.setParentCard(this);
this.rightHalfCard = rightHalfCard;
rightHalfCard.setParentCard(this);
}
@Override
public void assignNewId() {
super.assignNewId();
leftHalfCard.assignNewId();
rightHalfCard.assignNewId();
}
@Override
public void setCopy(boolean isCopy, MageObject copiedFrom) {
super.setCopy(isCopy, copiedFrom);
leftHalfCard.setCopy(isCopy, copiedFrom); // TODO: must check copiedFrom and assign sides? (??? related to #8476 ???)
rightHalfCard.setCopy(isCopy, copiedFrom);
}
private void setSideZones(Zone mainZone, Game game) {
switch (mainZone) {
case BATTLEFIELD:
case STACK:
throw new IllegalArgumentException("Wrong code usage: you must put to battlefield/stack only real side card (half), not main");
default:
// must keep both sides in same zone cause xmage need access to cost reduction, spell
// and other abilities before put it to stack (in playable calcs)
game.setZone(leftHalfCard.getId(), mainZone);
game.setZone(rightHalfCard.getId(), mainZone);
break;
}
checkGoodZones(game, this);
}
@Override
public boolean moveToZone(Zone toZone, Ability source, Game game, boolean flag, List<UUID> appliedEffects) {
if (super.moveToZone(toZone, source, game, flag, appliedEffects)) {
Zone currentZone = game.getState().getZone(getId());
setSideZones(currentZone, game);
return true;
}
return false;
}
@Override
public void setZone(Zone zone, Game game) {
super.setZone(zone, game);
setSideZones(zone, game);
}
@Override
public boolean moveToExile(UUID exileId, String name, Ability source, Game game, List<UUID> appliedEffects) {
if (super.moveToExile(exileId, name, source, game, appliedEffects)) {
Zone currentZone = game.getState().getZone(getId());
setSideZones(currentZone, game);
return true;
}
return false;
}
/**
* Runtime check for good zones and other MDF data
*/
public static void checkGoodZones(Game game, ModalDoubleFacedCard card) {
Card leftPart = card.getLeftHalfCard();
Card rightPart = card.getRightHalfCard();
Zone zoneMain = game.getState().getZone(card.getId());
Zone zoneLeft = game.getState().getZone(leftPart.getId());
Zone zoneRight = game.getState().getZone(rightPart.getId());
// runtime check:
// * in battlefield and stack - card + one of the sides (another side in outside zone)
// * in other zones - card + both sides (need both sides due cost reductions, spell and other access before put to stack)
//
// 712.8a While a double-faced card is outside the game or in a zone other than the battlefield or stack,
// it has only the characteristics of its front face.
//
// 712.8f While a modal double-faced spell is on the stack or a modal double-faced permanent is on the battlefield,
// it has only the characteristics of the face thats up.
Zone needZoneLeft;
Zone needZoneRight;
switch (zoneMain) {
case BATTLEFIELD:
case STACK:
if (zoneMain == zoneLeft) {
needZoneLeft = zoneMain;
needZoneRight = Zone.OUTSIDE;
} else if (zoneMain == zoneRight) {
needZoneLeft = Zone.OUTSIDE;
needZoneRight = zoneMain;
} else {
// impossible
needZoneLeft = zoneMain;
needZoneRight = Zone.OUTSIDE;
}
break;
default:
needZoneLeft = zoneMain;
needZoneRight = zoneMain;
break;
}
if (zoneLeft != needZoneLeft || zoneRight != needZoneRight) {
throw new IllegalStateException("Wrong code usage: MDF card uses wrong zones - " + card
+ "\r\n" + String.format("* main zone: %s", zoneMain)
+ "\r\n" + String.format("* left side: need %s, actual %s", needZoneLeft, zoneLeft)
+ "\r\n" + String.format("* right side: need %s, actual %s", needZoneRight, zoneRight));
}
}
@Override
public boolean removeFromZone(Game game, Zone fromZone, Ability source) {
// zone contains only one main card
return super.removeFromZone(game, fromZone, source);
}
@Override
public void updateZoneChangeCounter(Game game, ZoneChangeEvent event) {
if (isCopy()) { // same as meld cards
super.updateZoneChangeCounter(game, event);
return;
}
super.updateZoneChangeCounter(game, event);
leftHalfCard.updateZoneChangeCounter(game, event);
rightHalfCard.updateZoneChangeCounter(game, event);
}
@Override
public Counters getCounters(Game game) {
return getCounters(game.getState());
}
@Override
public Counters getCounters(GameState state) {
return state.getCardState(leftHalfCard.getId()).getCounters();
}
@Override
public boolean addCounters(Counter counter, UUID playerAddingCounters, Ability source, Game game, List<UUID> appliedEffects, boolean isEffect, int maxCounters) {
return leftHalfCard.addCounters(counter, playerAddingCounters, source, game, appliedEffects, isEffect, maxCounters);
}
@Override
public void removeCounters(String counterName, int amount, Ability source, Game game) {
leftHalfCard.removeCounters(counterName, amount, source, game);
}
@Override
@ -236,201 +58,22 @@ public abstract class ModalDoubleFacedCard extends CardImpl implements CardWithH
case MODAL_RIGHT:
return this.rightHalfCard.cast(game, fromZone, ability, controllerId);
default:
if (this.leftHalfCard.getSpellAbility() != null) {
this.leftHalfCard.getSpellAbility().setControllerId(controllerId);
}
if (this.rightHalfCard.getSpellAbility() != null) {
this.rightHalfCard.getSpellAbility().setControllerId(controllerId);
}
return super.cast(game, fromZone, ability, controllerId);
}
}
@Override
public List<SuperType> getSuperType(Game game) {
// CardImpl's constructor can call some code on init, so you must check left/right before
// it's a bad workaround
return leftHalfCard != null ? leftHalfCard.getSuperType(game) : supertype;
public boolean isTransformable() {
return this.getLeftHalfCard().isPermanent() && this.getRightHalfCard().isPermanent();
}
@Override
public List<CardType> getCardType(Game game) {
// CardImpl's constructor can call some code on init, so you must check left/right before
// it's a bad workaround
return leftHalfCard != null ? leftHalfCard.getCardType(game) : cardType;
public ModalDoubleFacedCardHalf getLeftHalfCard() {
return (ModalDoubleFacedCardHalf) leftHalfCard;
}
@Override
public SubTypes getSubtype() {
// rules: While a double-faced card isnt on the stack or battlefield, consider only the characteristics of its front face.
// CardImpl's constructor can call some code on init, so you must check left/right before
return leftHalfCard != null ? leftHalfCard.getSubtype() : subtype;
}
@Override
public SubTypes getSubtype(Game game) {
// rules: While a double-faced card isnt on the stack or battlefield, consider only the characteristics of its front face.
// CardImpl's constructor can call some code on init, so you must check left/right before
return leftHalfCard != null ? leftHalfCard.getSubtype(game) : subtype;
}
@Override
public boolean hasSubtype(SubType subtype, Game game) {
return leftHalfCard.hasSubtype(subtype, game);
}
@Override
public Abilities<Ability> getAbilities() {
return getInnerAbilities(true, true);
}
@Override
public Abilities<Ability> getInitAbilities() {
// must init only parent related abilities, spell card must be init separately
return getInnerAbilities(false, false);
}
public Abilities<Ability> getSharedAbilities(Game game) {
// no shared abilities for mdf cards (e.g. must be left or right only)
return new AbilitiesImpl<>();
}
@Override
public Abilities<Ability> getAbilities(Game game) {
return getInnerAbilities(game, true, true);
}
private boolean isIgnoreDefaultAbility(Ability ability) {
// ignore default play/spell ability from main card (only halfes are actual)
// default abilities added on card creation from card type and can't be skipped
// skip cast spell
if (ability instanceof SpellAbility && ((SpellAbility) ability).getSpellAbilityType() == SpellAbilityType.MODAL) {
return true;
}
// skip play land
return ability instanceof PlayLandAbility;
}
private Abilities<Ability> getInnerAbilities(Game game, boolean showLeftSide, boolean showRightSide) {
Abilities<Ability> allAbilites = new AbilitiesImpl<>();
for (Ability ability : super.getAbilities(game)) {
if (isIgnoreDefaultAbility(ability)) {
continue;
}
allAbilites.add(ability);
}
if (showLeftSide) {
allAbilites.addAll(leftHalfCard.getAbilities(game));
}
if (showRightSide) {
allAbilites.addAll(rightHalfCard.getAbilities(game));
}
return allAbilites;
}
private Abilities<Ability> getInnerAbilities(boolean showLeftSide, boolean showRightSide) {
Abilities<Ability> allAbilites = new AbilitiesImpl<>();
for (Ability ability : super.getAbilities()) {
if (isIgnoreDefaultAbility(ability)) {
continue;
}
allAbilites.add(ability);
}
if (showLeftSide) {
allAbilites.addAll(leftHalfCard.getAbilities());
}
if (showRightSide) {
allAbilites.addAll(rightHalfCard.getAbilities());
}
return allAbilites;
}
@Override
public List<String> getRules() {
// rules must show only main side (another side visible by toggle/transform button in GUI)
// card hints from both sides
return CardUtil.getCardRulesWithAdditionalInfo(
this,
this.getInnerAbilities(true, false),
this.getInnerAbilities(true, true)
);
}
@Override
public List<String> getRules(Game game) {
// rules must show only main side (another side visible by toggle/transform button in GUI)
// card hints from both sides
return CardUtil.getCardRulesWithAdditionalInfo(
game,
this,
this.getInnerAbilities(game, true, false),
this.getInnerAbilities(game, true, true)
);
}
@Override
public boolean hasAbility(Ability ability, Game game) {
return super.hasAbility(ability, game);
}
@Override
public ObjectColor getColor() {
return leftHalfCard.getColor();
}
@Override
public ObjectColor getColor(Game game) {
return leftHalfCard.getColor(game);
}
@Override
public ObjectColor getFrameColor(Game game) {
return leftHalfCard.getFrameColor(game);
}
@Override
public void setOwnerId(UUID ownerId) {
super.setOwnerId(ownerId);
abilities.setControllerId(ownerId);
leftHalfCard.getAbilities().setControllerId(ownerId);
leftHalfCard.setOwnerId(ownerId);
rightHalfCard.getAbilities().setControllerId(ownerId);
rightHalfCard.setOwnerId(ownerId);
}
@Override
public ManaCosts<ManaCost> getManaCost() {
return leftHalfCard.getManaCost();
}
@Override
public int getManaValue() {
// Rules:
// The converted mana cost of a modal double-faced card is based on the characteristics of the
// face thats being considered. On the stack and battlefield, consider whichever face is up.
// In all other zones, consider only the front face. This is different than how the converted
// mana cost of a transforming double-faced card is determined.
// on stack or battlefield it must be half card with own cost
return leftHalfCard.getManaValue();
}
@Override
public MageInt getPower() {
return leftHalfCard.getPower();
}
@Override
public MageInt getToughness() {
return leftHalfCard.getToughness();
public ModalDoubleFacedCardHalf getRightHalfCard() {
return (ModalDoubleFacedCardHalf) rightHalfCard;
}
}

View file

@ -1,16 +1,34 @@
package mage.cards;
import mage.MageInt;
import mage.constants.CardType;
import mage.constants.SpellAbilityType;
import mage.constants.SubType;
import mage.constants.SuperType;
/**
* @author JayDi85
*/
public interface ModalDoubleFacedCardHalf extends SubCard<ModalDoubleFacedCard> {
import java.util.UUID;
public class ModalDoubleFacedCardHalf extends DoubleFacedCardHalf {
public ModalDoubleFacedCardHalf(
UUID ownerId, CardSetInfo setInfo,
SuperType[] cardSuperTypes, CardType[] cardTypes, SubType[] cardSubTypes,
String costs, ModalDoubleFacedCard parentCard, SpellAbilityType spellAbilityType
) {
super(ownerId, setInfo, cardSuperTypes, cardTypes, cardSubTypes, costs, parentCard, spellAbilityType);
}
protected ModalDoubleFacedCardHalf(final ModalDoubleFacedCardHalf card) {
super(card);
this.parentCard = card.parentCard;
}
@Override
ModalDoubleFacedCardHalf copy();
public ModalDoubleFacedCardHalf copy() {
return new ModalDoubleFacedCardHalf(this);
}
void setPT(int power, int toughness);
void setPT(MageInt power, MageInt toughness);
@Override
public ModalDoubleFacedCard getParentCard() {
return (ModalDoubleFacedCard) parentCard;
}
}

View file

@ -1,27 +1,20 @@
package mage.cards;
import java.util.UUID;
import mage.ObjectColor;
import mage.abilities.Abilities;
import mage.abilities.Ability;
import mage.abilities.common.EntersBattlefieldAbility;
import mage.abilities.common.RoomUnlockAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.common.UnlockThisDoorTriggeredAbility;
import mage.abilities.condition.common.RoomHalfLockedCondition;
import mage.abilities.costs.mana.ManaCosts;
import mage.abilities.decorator.ConditionalContinuousEffect;
import mage.abilities.common.RoomAbility;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.RoomCharacteristicsEffect;
import mage.constants.CardType;
import mage.constants.Duration;
import mage.constants.Outcome;
import mage.constants.SpellAbilityType;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.game.permanent.PermanentToken;
import mage.abilities.effects.common.continuous.LoseAbilitySourceEffect;
import java.util.UUID;
/**
* @author oscscull
@ -43,6 +36,13 @@ public abstract class RoomCard extends SplitCard {
this.getOwnerId(), new CardSetInfo(names[1], setInfo.getExpansionSetCode(), setInfo.getCardNumber(),
setInfo.getRarity(), setInfo.getGraphicInfo()),
types, costsRight, this, SpellAbilityType.SPLIT_RIGHT);
// Add the one-shot effect to unlock a door on cast -> ETB
Ability entersAbility = new EntersBattlefieldAbility(new RoomEnterUnlockEffect());
entersAbility.setRuleVisible(false);
this.addAbility(entersAbility);
this.addAbility(new RoomAbility());
}
protected RoomCard(RoomCard card) {
@ -58,56 +58,6 @@ public abstract class RoomCard extends SplitCard {
this.lastCastHalf = lastCastHalf;
}
protected void addRoomAbilities(Ability leftAbility, Ability rightAbility) {
getLeftHalfCard().addAbility(leftAbility);
getRightHalfCard().addAbility(rightAbility);
this.addAbility(leftAbility.copy());
this.addAbility(rightAbility.copy());
// Add the one-shot effect to unlock a door on cast -> ETB
Ability entersAbility = new EntersBattlefieldAbility(new RoomEnterUnlockEffect());
entersAbility.setRuleVisible(false);
this.addAbility(entersAbility);
// Remove locked door abilities - keeping unlock triggers (or they won't trigger
// when unlocked)
if (leftAbility != null && !(leftAbility instanceof UnlockThisDoorTriggeredAbility)) {
Ability ability = new SimpleStaticAbility(Zone.BATTLEFIELD, new ConditionalContinuousEffect(
new LoseAbilitySourceEffect(leftAbility, Duration.WhileOnBattlefield),
RoomHalfLockedCondition.LEFT, "")).setRuleVisible(false);
this.addAbility(ability);
}
if (rightAbility != null && !(rightAbility instanceof UnlockThisDoorTriggeredAbility)) {
Ability ability = new SimpleStaticAbility(Zone.BATTLEFIELD, new ConditionalContinuousEffect(
new LoseAbilitySourceEffect(rightAbility, Duration.WhileOnBattlefield),
RoomHalfLockedCondition.RIGHT, "")).setRuleVisible(false);
this.addAbility(ability);
}
// Add the Special Action to unlock doors.
// These will ONLY be active if the corresponding half is LOCKED!
if (leftAbility != null) {
ManaCosts leftHalfManaCost = null;
if (this.getLeftHalfCard() != null && this.getLeftHalfCard().getSpellAbility() != null) {
leftHalfManaCost = this.getLeftHalfCard().getSpellAbility().getManaCosts();
}
RoomUnlockAbility leftUnlockAbility = new RoomUnlockAbility(leftHalfManaCost, true);
this.addAbility(leftUnlockAbility.setRuleAtTheTop(true));
}
if (rightAbility != null) {
ManaCosts rightHalfManaCost = null;
if (this.getRightHalfCard() != null && this.getRightHalfCard().getSpellAbility() != null) {
rightHalfManaCost = this.getRightHalfCard().getSpellAbility().getManaCosts();
}
RoomUnlockAbility rightUnlockAbility = new RoomUnlockAbility(rightHalfManaCost, false);
this.addAbility(rightUnlockAbility.setRuleAtTheTop(true));
}
this.addAbility(new RoomAbility());
}
@Override
public Abilities<Ability> getAbilities() {
return this.abilities;
@ -131,6 +81,41 @@ public abstract class RoomCard extends SplitCard {
game.setZone(getLeftHalfCard().getId(), zone);
game.setZone(getRightHalfCard().getId(), zone);
}
public static void setRoomCharacteristics(Permanent permanent, Game game) {
if (!(permanent.getMainCard() instanceof RoomCard)) {
return;
}
setRoomCharacteristics(permanent, (RoomCard) permanent.getMainCard(), game, permanent.isLeftDoorUnlocked(), permanent.isRightDoorUnlocked());
}
// Static method for setting room characteristics on permanents
public static void setRoomCharacteristics(Permanent permanent, RoomCard roomCard, Game game, boolean isLeftUnlocked, boolean isRightUnlocked) {
permanent.setName(roomCard.name);
permanent.setManaCost(roomCard.getManaCost());
// Set color indicator based on unlocked halves
ObjectColor newColor = new ObjectColor();
if (isLeftUnlocked && roomCard.getLeftHalfCard() != null) {
newColor.addColor(roomCard.getLeftHalfCard().getColor());
}
if (isRightUnlocked && roomCard.getRightHalfCard() != null) {
newColor.addColor(roomCard.getRightHalfCard().getColor());
}
permanent.getColor().setColor(roomCard.getColor());
// Get abilities from each half
Abilities<Ability> leftAbilities = roomCard.getLeftHalfCard().getAbilities();
for (Ability ability : leftAbilities) {
permanent.addAbility(ability, roomCard.getLeftHalfCard().getId(), game, true);
}
Abilities<Ability> rightAbilities = roomCard.getRightHalfCard().getAbilities();
for (Ability ability : rightAbilities) {
permanent.addAbility(ability, roomCard.getRightHalfCard().getId(), game,true);
}
}
}
class RoomEnterUnlockEffect extends OneShotEffect {
@ -189,27 +174,3 @@ class RoomEnterUnlockEffect extends OneShotEffect {
}
}
// For the overall Room card flavor text and mana value effect.
class RoomAbility extends SimpleStaticAbility {
public RoomAbility() {
super(Zone.ALL, null);
this.setRuleVisible(true);
this.setRuleAtTheTop(true);
this.addEffect(new RoomCharacteristicsEffect());
}
protected RoomAbility(final RoomAbility ability) {
super(ability);
}
@Override
public String getRule() {
return "<i>(You may cast either half. That door unlocks on the battlefield. " +
"As a sorcery, you may pay the mana cost of a locked door to unlock it.)</i>";
}
@Override
public RoomAbility copy() {
return new RoomAbility(this);
}
}

View file

@ -0,0 +1,66 @@
package mage.cards;
import mage.abilities.SpellAbility;
import mage.constants.*;
import mage.game.Game;
import java.util.UUID;
public abstract class TransformingDoubleFacedCard extends DoubleFacedCard {
public TransformingDoubleFacedCard(
UUID ownerId, CardSetInfo setInfo,
CardType[] typesLeft, SubType[] subTypesLeft, String costsLeft,
String secondSideName,
CardType[] typesRight, SubType[] subTypesRight, String colorRight
) {
this(
ownerId, setInfo,
new SuperType[]{}, typesLeft, subTypesLeft, costsLeft,
secondSideName,
new SuperType[]{}, typesRight, subTypesRight, colorRight
);
}
public TransformingDoubleFacedCard(
UUID ownerId, CardSetInfo setInfo,
SuperType[] superTypesLeft, CardType[] typesLeft, SubType[] subTypesLeft, String costsLeft,
String secondSideName,
SuperType[] superTypesRight, CardType[] typesRight, SubType[] subTypesRight, String colorRight
) {
super(ownerId, setInfo, typesLeft, costsLeft, SpellAbilityType.TRANSFORMED);
// main card name must be same as left side
leftHalfCard = new TransformingDoubleFacedCardHalf(
this.getOwnerId(), setInfo.copy(),
superTypesLeft, typesLeft, subTypesLeft, costsLeft,
this, SpellAbilityType.TRANSFORMED_LEFT
);
rightHalfCard = new TransformingDoubleFacedCardHalf(
this.getOwnerId(), new CardSetInfo(secondSideName, setInfo),
superTypesRight, typesRight, subTypesRight, colorRight, this
);
this.secondSideCard = rightHalfCard;
}
public TransformingDoubleFacedCard(final TransformingDoubleFacedCard card) {
super(card);
}
@Override
public boolean cast(Game game, Zone fromZone, SpellAbility ability, UUID controllerId) {
if (ability.getSpellAbilityType() == SpellAbilityType.BASE) {
return this.leftHalfCard.cast(game, fromZone, ability, controllerId);
}
return super.cast(game, fromZone, ability, controllerId);
}
@Override
public TransformingDoubleFacedCardHalf getLeftHalfCard() {
return (TransformingDoubleFacedCardHalf) leftHalfCard;
}
@Override
public TransformingDoubleFacedCardHalf getRightHalfCard() {
return (TransformingDoubleFacedCardHalf) rightHalfCard;
}
}

View file

@ -0,0 +1,49 @@
package mage.cards;
import mage.ObjectColor;
import mage.abilities.SpellAbility;
import mage.constants.*;
import mage.game.Game;
import java.util.UUID;
public class TransformingDoubleFacedCardHalf extends DoubleFacedCardHalf {
public TransformingDoubleFacedCardHalf(
UUID ownerId, CardSetInfo setInfo,
SuperType[] cardSuperTypes, CardType[] cardTypes, SubType[] cardSubTypes,
String costs, TransformingDoubleFacedCard parentCard, SpellAbilityType spellAbilityType
) {
super(ownerId, setInfo, cardSuperTypes, cardTypes, cardSubTypes, costs, parentCard, spellAbilityType);
}
protected TransformingDoubleFacedCardHalf(final TransformingDoubleFacedCardHalf card) {
super(card);
this.parentCard = card.parentCard;
}
public TransformingDoubleFacedCardHalf(
UUID ownerId, CardSetInfo setInfo,
SuperType[] superTypesRight, CardType[] typesRight, SubType[] subTypesRight, String colorRight, TransformingDoubleFacedCard parentCard) {
super(ownerId, setInfo, superTypesRight, typesRight, subTypesRight, "", parentCard, SpellAbilityType.TRANSFORMED_RIGHT);
this.getColor().setColor(new ObjectColor(colorRight));
}
@Override
public boolean cast(Game game, Zone fromZone, SpellAbility ability, UUID controllerId) {
if (ability.getSpellAbilityCastMode() == SpellAbilityCastMode.DISTURB && !isBackSide()) {
return getOtherSide().cast(game, fromZone, ability, controllerId);
}
return super.cast(game, fromZone, ability, controllerId);
}
@Override
public TransformingDoubleFacedCardHalf copy() {
return new TransformingDoubleFacedCardHalf(this);
}
@Override
public TransformingDoubleFacedCard getParentCard() {
return (TransformingDoubleFacedCard) parentCard;
}
}

View file

@ -5,7 +5,7 @@ import mage.abilities.Ability;
import mage.abilities.costs.mana.ManaCost;
import mage.abilities.costs.mana.ManaCosts;
import mage.cards.CardImpl;
import mage.cards.ModalDoubleFacedCard;
import mage.cards.DoubleFacedCard;
import mage.cards.repository.CardInfo;
import mage.cards.repository.CardRepository;
import mage.util.CardUtil;
@ -35,7 +35,7 @@ public class MockCard extends CardImpl implements MockableCard {
protected List<String> manaCostRightStr;
protected List<String> manaCostStr;
protected String spellOptionName; // adventure/omen spell name
protected boolean isModalDoubleFacedCard;
protected boolean isDoubleFacedCard;
protected int manaValue;
public MockCard(CardInfo card) {
@ -67,7 +67,7 @@ public class MockCard extends CardImpl implements MockableCard {
this.nightCard = card.isNightCard();
if (card.getSecondSideName() != null && !card.getSecondSideName().isEmpty()) {
if (card.getSecondSideName() != null && !card.getSecondSideName().isEmpty() && !card.isDoubleFacedCard()) {
this.secondSideCard = new MockCard(CardRepository.instance.findCardWithPreferredSetAndNumber(card.getSecondSideName(), card.getSetCode(), card.getCardNumber()));
}
@ -75,11 +75,11 @@ public class MockCard extends CardImpl implements MockableCard {
this.spellOptionName = card.getSpellOptionCardName();
}
if (card.isModalDoubleFacedCard()) {
ModalDoubleFacedCard mdfCard = (ModalDoubleFacedCard) card.createCard();
if (card.isDoubleFacedCard()) {
DoubleFacedCard mdfCard = (DoubleFacedCard) card.createCard();
CardInfo mdfSecondSide = new CardInfo(mdfCard.getRightHalfCard());
this.secondSideCard = new MockCard(mdfSecondSide);
this.isModalDoubleFacedCard = true;
this.isDoubleFacedCard = true;
}
this.startingLoyalty = CardUtil.convertLoyaltyOrDefense(card.getStartingLoyalty());
@ -102,7 +102,7 @@ public class MockCard extends CardImpl implements MockableCard {
this.manaCostRightStr = new ArrayList<>(card.manaCostRightStr);
this.manaCostStr = new ArrayList<>(card.manaCostStr);
this.spellOptionName = card.spellOptionName;
this.isModalDoubleFacedCard = card.isModalDoubleFacedCard;
this.isDoubleFacedCard = card.isDoubleFacedCard;
this.manaValue = card.manaValue;
}
@ -157,7 +157,7 @@ public class MockCard extends CardImpl implements MockableCard {
if (spellOptionName != null) {
return getName() + CARD_WITH_SPELL_OPTION_NAME_SEPARATOR + spellOptionName;
} else if (isModalDoubleFacedCard) {
} else if (isDoubleFacedCard) {
return getName() + MODAL_DOUBLE_FACES_NAME_SEPARATOR + this.getSecondCardFace().getName();
} else {
return getName();
@ -181,6 +181,6 @@ public class MockCard extends CardImpl implements MockableCard {
@Override
public boolean isTransformable() {
// must enable toggle mode in deck editor (switch between card sides);
return super.isTransformable() || this.isModalDoubleFacedCard || this.secondSideCard != null;
return super.isTransformable() || this.isDoubleFacedCard || this.secondSideCard != null;
}
}

View file

@ -110,9 +110,9 @@ public class CardInfo {
@DatabaseField
protected String spellOptionCardName;
@DatabaseField
protected boolean modalDoubleFacedCard;
protected boolean doubleFacedCard;
@DatabaseField
protected String modalDoubleFacedSecondSideName;
protected String doubleFacedSecondSideName;
@DatabaseField
protected String meldsToCardName;
@DatabaseField
@ -162,9 +162,9 @@ public class CardInfo {
this.spellOptionCardName = ((CardWithSpellOption) card).getSpellCard().getName();
}
if (card instanceof ModalDoubleFacedCard) {
this.modalDoubleFacedCard = true;
this.modalDoubleFacedSecondSideName = ((ModalDoubleFacedCard) card).getRightHalfCard().getName();
if (card instanceof DoubleFacedCard) {
this.doubleFacedCard = true;
this.doubleFacedSecondSideName = ((DoubleFacedCard) card).getRightHalfCard().getName();
}
if (card.getFrameStyle() != null) {
@ -483,12 +483,12 @@ public class CardInfo {
return spellOptionCardName;
}
public boolean isModalDoubleFacedCard() {
return modalDoubleFacedCard;
public boolean isDoubleFacedCard() {
return doubleFacedCard;
}
public String getModalDoubleFacedSecondSideName() {
return modalDoubleFacedSecondSideName;
public String getDoubleFacedSecondSideName() {
return doubleFacedSecondSideName;
}
@Override

View file

@ -126,7 +126,7 @@ public enum CardRepository {
}
private void addNewNames(CardInfo card, Set<String> namesList) {
// require before call: qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName"...);
// require before call: qb.distinct().selectColumns("name", "doubleFacedSecondSideName"...);
// normal names
int result = card.getName().indexOf(" // ");
@ -141,8 +141,8 @@ public enum CardRepository {
if (card.getSecondSideName() != null && !card.getSecondSideName().isEmpty()) {
namesList.add(card.getSecondSideName());
}
if (card.getModalDoubleFacedSecondSideName() != null && !card.getModalDoubleFacedSecondSideName().isEmpty()) {
namesList.add(card.getModalDoubleFacedSecondSideName());
if (card.getDoubleFacedSecondSideName() != null && !card.getDoubleFacedSecondSideName().isEmpty()) {
namesList.add(card.getDoubleFacedSecondSideName());
}
if (card.getFlipCardName() != null && !card.getFlipCardName().isEmpty()) {
namesList.add(card.getFlipCardName());
@ -166,7 +166,7 @@ public enum CardRepository {
}
try {
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName");
qb.distinct().selectColumns("name", "doubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName");
List<CardInfo> results = cardsDao.query(qb.prepare());
for (CardInfo card : results) {
addNewNames(card, names);
@ -185,7 +185,7 @@ public enum CardRepository {
}
try {
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName");
qb.distinct().selectColumns("name", "doubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName");
qb.where().not().like("types", new SelectArg('%' + CardType.LAND.name() + '%'));
List<CardInfo> results = cardsDao.query(qb.prepare());
for (CardInfo card : results) {
@ -205,7 +205,7 @@ public enum CardRepository {
}
try {
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName");
qb.distinct().selectColumns("name", "doubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName");
Where<CardInfo, Object> where = qb.where();
where.and(
where.not().like("supertypes", '%' + SuperType.BASIC.name() + '%'),
@ -229,7 +229,7 @@ public enum CardRepository {
}
try {
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName");
qb.distinct().selectColumns("name", "doubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName");
qb.where().not().like("supertypes", new SelectArg('%' + SuperType.BASIC.name() + '%'));
List<CardInfo> results = cardsDao.query(qb.prepare());
for (CardInfo card : results) {
@ -249,7 +249,7 @@ public enum CardRepository {
}
try {
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName");
qb.distinct().selectColumns("name", "doubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName");
qb.where().like("types", new SelectArg('%' + CardType.CREATURE.name() + '%'));
List<CardInfo> results = cardsDao.query(qb.prepare());
for (CardInfo card : results) {
@ -269,7 +269,7 @@ public enum CardRepository {
}
try {
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName");
qb.distinct().selectColumns("name", "doubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName");
qb.where().like("types", new SelectArg('%' + CardType.ARTIFACT.name() + '%'));
List<CardInfo> results = cardsDao.query(qb.prepare());
for (CardInfo card : results) {
@ -289,7 +289,7 @@ public enum CardRepository {
}
try {
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName");
qb.distinct().selectColumns("name", "doubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName");
Where<CardInfo, Object> where = qb.where();
where.and(
where.not().like("types", '%' + CardType.CREATURE.name() + '%'),
@ -313,7 +313,7 @@ public enum CardRepository {
}
try {
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName");
qb.distinct().selectColumns("name", "doubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName");
Where<CardInfo, Object> where = qb.where();
where.and(
where.not().like("types", '%' + CardType.ARTIFACT.name() + '%'),
@ -539,7 +539,7 @@ public enum CardRepository {
.eq("flipCardName", new SelectArg(name)).or()
.eq("secondSideName", new SelectArg(name)).or()
.eq("spellOptionCardName", new SelectArg(name)).or()
.eq("modalDoubleFacedSecondSideName", new SelectArg(name));
.eq("doubleFacedSecondSideName", new SelectArg(name));
results = cardsDao.query(queryBuilder.prepare());
} else {
// Check that a full card was found and not a SplitCardHalf

View file

@ -2,9 +2,7 @@ package mage.constants;
import mage.abilities.Ability;
import mage.abilities.keyword.TransformAbility;
import mage.cards.Card;
import mage.cards.Cards;
import mage.cards.CardsImpl;
import mage.cards.*;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
@ -92,6 +90,9 @@ public enum PutCards {
case SHUFFLE:
return player.shuffleCardsToLibrary(card, game, source);
case BATTLEFIELD_TRANSFORMED:
if (card instanceof TransformingDoubleFacedCard) {
card = ((TransformingDoubleFacedCard) card).getRightHalfCard();
}
game.getState().setValue(TransformAbility.VALUE_KEY_ENTER_TRANSFORMED + card.getId(), Boolean.TRUE);
case BATTLEFIELD:
case EXILED:

View file

@ -11,6 +11,9 @@ public enum SpellAbilityType {
SPLIT_FUSED("Split SpellAbility"),
SPLIT_LEFT("LeftSplit SpellAbility"),
SPLIT_RIGHT("RightSplit SpellAbility"),
TRANSFORMED("Transformed SpellAbility"),
TRANSFORMED_LEFT("TransformFront SpellAbility"),
TRANSFORMED_RIGHT("TransformBack SpellAbility"),
MODAL("Modal SpellAbility"), // used for modal double faces cards
MODAL_LEFT("LeftModal SpellAbility"),
MODAL_RIGHT("RightModal SpellAbility"),

View file

@ -56,6 +56,7 @@ import mage.game.mulligan.Mulligan;
import mage.game.permanent.Battlefield;
import mage.game.permanent.Permanent;
import mage.game.permanent.PermanentCard;
import mage.game.permanent.PermanentToken;
import mage.game.stack.Spell;
import mage.game.stack.SpellStack;
import mage.game.stack.StackAbility;
@ -129,8 +130,6 @@ public abstract class GameImpl implements Game {
// For checking "becomes the target" triggers accurately. Cleared on short living LKI reset
protected Map<String, Map<UUID, Set<UUID>>> targetedMap = new HashMap<>();
// Permanents entering the Battlefield while handling replacement effects before they are added to the battlefield
protected Map<UUID, Permanent> permanentsEntering = new HashMap<>();
// used to set the counters a permanent adds the battlefield (if no replacement effect is used e.g. Persist)
protected Map<UUID, Counters> enterWithCounters = new HashMap<>();
@ -214,7 +213,6 @@ public abstract class GameImpl implements Game {
this.lkiShortLiving = CardUtil.deepCopyObject(game.lkiShortLiving);
this.targetedMap = CardUtil.deepCopyObject(game.targetedMap);
this.permanentsEntering = CardUtil.deepCopyObject(game.permanentsEntering);
this.enterWithCounters = CardUtil.deepCopyObject(game.enterWithCounters);
this.state = game.state.copy();
@ -341,13 +339,13 @@ public abstract class GameImpl implements Game {
Card rightCard = ((SplitCard) card).getRightHalfCard();
rightCard.setOwnerId(ownerId);
addCardToState(rightCard);
} else if (card instanceof ModalDoubleFacedCard) {
} else if (card instanceof DoubleFacedCard) {
// left
Card leftCard = ((ModalDoubleFacedCard) card).getLeftHalfCard();
Card leftCard = ((DoubleFacedCard) card).getLeftHalfCard();
leftCard.setOwnerId(ownerId);
addCardToState(leftCard);
// right
Card rightCard = ((ModalDoubleFacedCard) card).getRightHalfCard();
Card rightCard = ((DoubleFacedCard) card).getRightHalfCard();
rightCard.setOwnerId(ownerId);
addCardToState(rightCard);
} else if (card instanceof CardWithSpellOption) {
@ -764,12 +762,12 @@ public abstract class GameImpl implements Game {
@Override
public Permanent getPermanentEntering(UUID permanentId) {
return permanentsEntering.get(permanentId);
return state.getBattlefield().getPermanentsEntering().get(permanentId);
}
@Override
public Map<UUID, Permanent> getPermanentsEntering() {
return permanentsEntering;
return state.getBattlefield().getPermanentsEntering();
}
@Override
@ -2109,6 +2107,7 @@ public abstract class GameImpl implements Game {
newBluePrint = copyFromPermanent.copy();
// reset to original characteristics
newBluePrint.resetLockedStatus(); // reset locked status so room characteristics are correct
newBluePrint.reset(this);
// workaround to find real copyable characteristics of transformed/facedown/etc permanents
@ -2118,7 +2117,9 @@ public abstract class GameImpl implements Game {
BecomesFaceDownCreatureEffect.makeFaceDownObject(this, null, newBluePrint, faceDownType, null);
}
newBluePrint.assignNewId();
if (copyFromPermanent.isTransformed()) {
// TODO: should be able to remove after tdfc rework
if (copyFromPermanent.isTransformed() && (copyFromPermanent instanceof PermanentToken || ((copyFromPermanent instanceof PermanentCard) &&
!(((PermanentCard) copyFromPermanent).getCard() instanceof DoubleFacedCardHalf)))) {
TransformAbility.transformPermanent(newBluePrint, this, source);
}
if (copyFromPermanent.isPrototyped()) {
@ -3818,7 +3819,7 @@ public abstract class GameImpl implements Game {
loadCards(ownerId, hand);
loadCards(ownerId, battlefield
.stream()
.map(PutToBattlefieldInfo::getCard)
.map(PutToBattlefieldInfo::getMainCard)
.collect(Collectors.toList())
);
loadCards(ownerId, graveyard);

View file

@ -670,9 +670,9 @@ public class GameState implements Serializable, Copyable<GameState> {
for (Player player : players.values()) {
player.reset();
}
this.reset();
battlefield.reset(game);
combat.reset(game);
this.reset();
effects.apply(game);
combat.checkForRemoveFromCombat(game);
}
@ -1627,19 +1627,19 @@ public class GameState implements Serializable, Copyable<GameState> {
copiedParts.add(rightCopied);
// sync parts
((SplitCard) copiedCard).setParts(leftCopied, rightCopied);
} else if (copiedCard instanceof ModalDoubleFacedCard) {
} else if (copiedCard instanceof DoubleFacedCard) {
// left
ModalDoubleFacedCardHalf leftOriginal = ((ModalDoubleFacedCard) copiedCard).getLeftHalfCard();
ModalDoubleFacedCardHalf leftCopied = leftOriginal.copy();
DoubleFacedCardHalf leftOriginal = ((DoubleFacedCard) copiedCard).getLeftHalfCard();
DoubleFacedCardHalf leftCopied = (DoubleFacedCardHalf) leftOriginal.copy();
prepareCardForCopy(leftOriginal, leftCopied, newController);
copiedParts.add(leftCopied);
// right
ModalDoubleFacedCardHalf rightOriginal = ((ModalDoubleFacedCard) copiedCard).getRightHalfCard();
ModalDoubleFacedCardHalf rightCopied = rightOriginal.copy();
DoubleFacedCardHalf rightOriginal = ((DoubleFacedCard) copiedCard).getRightHalfCard();
DoubleFacedCardHalf rightCopied = (DoubleFacedCardHalf) rightOriginal.copy();
prepareCardForCopy(rightOriginal, rightCopied, newController);
copiedParts.add(rightCopied);
// sync parts
((ModalDoubleFacedCard) copiedCard).setParts(leftCopied, rightCopied);
((DoubleFacedCard) copiedCard).setParts(leftCopied, rightCopied);
} else if (copiedCard instanceof CardWithSpellOption) {
// right
SpellOptionCard rightOriginal = ((CardWithSpellOption) copiedCard).getSpellCard();

View file

@ -21,6 +21,10 @@ public class PutToBattlefieldInfo {
return card;
}
public Card getMainCard() {
return card.getMainCard();
}
public boolean isTapped() {
return tapped;
}

View file

@ -89,21 +89,31 @@ public final class ZonesHandler {
ZoneChangeInfo info = itr.next();
if (info.event.getToZone().equals(Zone.BATTLEFIELD)) {
Card card = game.getCard(info.event.getTargetId());
if (card instanceof ModalDoubleFacedCard || card instanceof ModalDoubleFacedCardHalf) {
if (card instanceof DoubleFacedCard || card instanceof DoubleFacedCardHalf) {
boolean forceToMainSide = false;
// TODO: move transform key or have some other identifier after tdfc rework
Boolean enterTransformed = (Boolean) game.getState().getValue(TransformAbility.VALUE_KEY_ENTER_TRANSFORMED + card.getId());
if (enterTransformed == null) {
enterTransformed = false;
}
// if effect put half mdf card to battlefield then it must be the main side only (example: return targeted half card to battle)
if (card instanceof ModalDoubleFacedCardHalf && !source.getAbilityType().isPlayCardAbility()) {
if (card instanceof DoubleFacedCardHalf && !source.getAbilityType().isPlayCardAbility() && !enterTransformed) {
forceToMainSide = true;
}
// if effect put mdf card to battlefield then it must be main side only
if (card instanceof ModalDoubleFacedCard) {
if (card instanceof DoubleFacedCard) {
forceToMainSide = true;
}
if (forceToMainSide) {
info.event.setTargetId(((ModalDoubleFacedCard) card.getMainCard()).getLeftHalfCard().getId());
info.event.setTargetId(((DoubleFacedCard) card.getMainCard()).getLeftHalfCard().getId());
}
// if left half is being moved, but entering transformed, change to transformed side
if (enterTransformed && card instanceof DoubleFacedCardHalf && !((DoubleFacedCardHalf) card).isBackSide()) {
info.event.setTargetId(((DoubleFacedCardHalf) card).getOtherSide().getId());
}
}
}
@ -154,10 +164,10 @@ public final class ZonesHandler {
// meld/group cards must be independent (use can choose order)
cardsToMove = ((MeldCard) targetCard).getHalves();
cardsToUpdate.get(toZone).addAll(cardsToMove);
} else if (targetCard instanceof ModalDoubleFacedCard
|| targetCard instanceof ModalDoubleFacedCardHalf) {
} else if (targetCard instanceof DoubleFacedCard
|| targetCard instanceof DoubleFacedCardHalf) {
// mdf cards must be moved as single object, but each half must be updated separately
ModalDoubleFacedCard mdfCard = (ModalDoubleFacedCard) targetCard.getMainCard();
DoubleFacedCard mdfCard = (DoubleFacedCard) targetCard.getMainCard();
cardsToMove = new CardsImpl(mdfCard);
cardsToUpdate.get(toZone).add(mdfCard);
// example: cast left side
@ -296,7 +306,7 @@ public final class ZonesHandler {
} else {
game.setZone(event.getTargetId(), event.getToZone());
}
// update zone in other parts (meld cards, mdf half cards)
cardsToUpdate.entrySet().forEach(entry -> {
for (Card card : entry.getValue().getCards(game)) {
@ -373,8 +383,9 @@ public final class ZonesHandler {
* that isn't a transforming double-faced card onto the battlefield transformed or converted, that card stays in
* its current zone.
*/
// TODO: remove after tdfc rework
boolean wantToTransform = Boolean.TRUE.equals(game.getState().getValue(TransformAbility.VALUE_KEY_ENTER_TRANSFORMED + card.getId()));
if (wantToTransform) {
if (wantToTransform && !(card instanceof DoubleFacedCardHalf)) {
isGoodToMove = card.isTransformable() && card.getSecondCardFace().isPermanent(game);
} else {
isGoodToMove = card.isPermanent(game);
@ -407,8 +418,7 @@ public final class ZonesHandler {
} else if (card instanceof RoomCardHalf) {
// Only the main room card can etb
permanent = new PermanentCard(card.getMainCard(), event.getPlayerId(), game);
}
else if (card instanceof ModalDoubleFacedCard) {
} else if (card instanceof DoubleFacedCard) {
// main mdf card must be processed before that call (e.g. only halves can be moved to battlefield)
throw new IllegalStateException("Unexpected trying of move mdf card to battlefield instead half");
} else if (card instanceof Permanent) {
@ -533,4 +543,4 @@ public final class ZonesHandler {
return card;
}
}
}

View file

@ -18,6 +18,7 @@ import java.util.stream.Collectors;
public class Battlefield implements Serializable {
private final Map<UUID, Permanent> field = new LinkedHashMap<>();
private final Map<UUID, Permanent> permanentsEntering = new LinkedHashMap<>();
public Battlefield() {
}
@ -26,6 +27,9 @@ public class Battlefield implements Serializable {
for (Entry<UUID, Permanent> entry : battlefield.field.entrySet()) {
field.put(entry.getKey(), entry.getValue().copy());
}
for (Entry<UUID, Permanent> entry : battlefield.permanentsEntering.entrySet()) {
permanentsEntering.put(entry.getKey(), entry.getValue().copy());
}
}
public Battlefield copy() {
@ -36,10 +40,14 @@ public class Battlefield implements Serializable {
for (Permanent perm : field.values()) {
perm.reset(game);
}
for (Permanent perm : permanentsEntering.values()) {
perm.reset(game);
}
}
public void clear() {
field.clear();
permanentsEntering.clear();
}
/**
@ -156,6 +164,11 @@ public class Battlefield implements Serializable {
return field.containsKey(key);
}
public Map<UUID, Permanent> getPermanentsEntering() {
return permanentsEntering;
}
public void beginningOfTurn(Game game) {
for (Permanent perm : field.values()) {
perm.beginningOfTurn(game);

View file

@ -479,6 +479,13 @@ public interface Permanent extends Card, Controllable {
boolean wasRoomUnlockedOnCast();
/**
* used to reset the locked status of a room. Only used when copying a room
* or creating a token copy of a room permanent. Could most likely be removed
* after a designation class added.
*/
void resetLockedStatus();
boolean isLeftDoorUnlocked();
boolean isRightDoorUnlocked();

View file

@ -1,8 +1,10 @@
package mage.game.permanent;
import mage.MageObject;
import mage.ObjectColor;
import mage.abilities.Abilities;
import mage.abilities.Ability;
import mage.abilities.common.RoomAbility;
import mage.abilities.costs.mana.ManaCost;
import mage.abilities.costs.mana.ManaCosts;
import mage.abilities.keyword.NightboundAbility;
@ -30,6 +32,8 @@ public class PermanentCard extends PermanentImpl {
protected int maxLevelCounters;
protected int zoneChangeCounter;
protected ObjectColor originalColor;
protected ObjectColor originalFrameColor;
public PermanentCard(Card card, UUID controllerId, Game game) {
super(card.getId(), card.getOwnerId(), controllerId, card.getName()); // card id
@ -46,7 +50,7 @@ public class PermanentCard extends PermanentImpl {
// if you use it in test code or for permanent's copy effects then call CardUtil.getDefaultCardSideForBattlefield for default side
// it's a basic check and still allows to create permanent from instant or sorcery
boolean goodForBattlefield = true;
if (card instanceof ModalDoubleFacedCard) {
if (card instanceof DoubleFacedCard) {
goodForBattlefield = false;
} else if (card instanceof SplitCard) {
// fused spells allowed (it uses main card)
@ -65,9 +69,24 @@ public class PermanentCard extends PermanentImpl {
throw new IllegalArgumentException("Wrong code usage: can't create permanent card from split or mdf: " + card.getName());
}
this.card = card;
// if two permanent sides, set front and second side
if (card instanceof DoubleFacedCardHalf && card.isPermanent() && ((DoubleFacedCardHalf) card).getOtherSide().isPermanent()) {
if (((DoubleFacedCardHalf) card).isBackSide()) {
secondSideCard = card;
this.card = ((DoubleFacedCardHalf) card).getOtherSide().copy();
this.transformed = true;
init(secondSideCard, game);
} else {
secondSideCard = ((DoubleFacedCardHalf) card).getOtherSide().copy();
this.card = card;
init(card, game);
}
} else {
this.card = card;
init(card, game);
}
this.zoneChangeCounter = card.getZoneChangeCounter(game); // local value already set to the raised number
init(card, game);
}
private void init(Card card, Game game) {
@ -75,7 +94,7 @@ public class PermanentCard extends PermanentImpl {
toughness = card.getToughness().copy();
startingLoyalty = card.getStartingLoyalty();
startingDefense = card.getStartingDefense();
copyFromCard(card, game);
copyFromCard(card, game, false);
// if temporary added abilities to the spell/card exist, you need to add it to the permanent derived from that card
Abilities<Ability> otherAbilities = game.getState().getAllOtherAbilities(card.getId());
if (otherAbilities != null) {
@ -86,10 +105,11 @@ public class PermanentCard extends PermanentImpl {
}
// if transformed on ETB
if (card.isTransformable()) {
if (game.getState().getValue(TransformAbility.VALUE_KEY_ENTER_TRANSFORMED + getId()) != null
// TODO: remove after tdfc rework
if (card.isTransformable() && !(card instanceof DoubleFacedCardHalf)) {
if (game.getState().getValue(TransformAbility.VALUE_KEY_ENTER_TRANSFORMED + card.getId()) != null
|| NightboundAbility.checkCard(this, game)) {
game.getState().setValue(TransformAbility.VALUE_KEY_ENTER_TRANSFORMED + getId(), null);
game.getState().setValue(TransformAbility.VALUE_KEY_ENTER_TRANSFORMED + card.getId(), null);
TransformAbility.transformPermanent(this, game, null);
}
}
@ -100,19 +120,37 @@ public class PermanentCard extends PermanentImpl {
this.card = permanent.card.copy();
this.maxLevelCounters = permanent.maxLevelCounters;
this.zoneChangeCounter = permanent.zoneChangeCounter;
this.originalColor = permanent.originalColor.copy();
this.originalFrameColor = permanent.originalFrameColor.copy();
}
@Override
public void reset(Game game) {
// when the permanent is reset, copy all original values from the card
// must copy card each reset so that the original values don't get modified
copyFromCard(card, game);
if (transformed && secondSideCard != null && getCard() instanceof DoubleFacedCardHalf) {
copyFromCard(secondSideCard, game, true);
} else {
copyFromCard(card, game, true);
}
power.resetToBaseValue();
toughness.resetToBaseValue();
super.reset(game);
}
protected void copyFromCard(final Card card, final Game game) {
@Override
protected void initOtherFace(Game game) {
if (!(secondSideCard instanceof DoubleFacedCardHalf)) {
return;
}
if (transformed) {
copyFromCard(secondSideCard, game, false);
} else {
copyFromCard(card, game, false);
}
}
protected void copyFromCard(final Card card, final Game game, boolean isReset) {
// TODO: must research - is it copy all fields or something miss
this.name = card.getName();
this.abilities.clear();
@ -122,6 +160,11 @@ public class PermanentCard extends PermanentImpl {
this.abilities.add(ability.copy());
}
}
} else if (card.getId() != this.getId()) {
// if different id, abilities need to be added to game state for continuous/triggers
for (Ability ability : card.getAbilities()) {
this.addAbility(ability, card.getId(), game, true);
}
} else {
// copy only own abilities; all dynamic added abilities must be added in the parent call
this.abilities = card.getAbilities().copy();
@ -131,13 +174,23 @@ public class PermanentCard extends PermanentImpl {
this.abilities.setSourceId(objectId);
this.cardType.clear();
this.cardType.addAll(card.getCardType());
this.color = card.getColor(game).copy();
this.frameColor = card.getFrameColor(game).copy();
if (!isReset) {
// save color from game state on first creation
this.color = card.getColor(game).copy();
this.frameColor = card.getFrameColor(game).copy();
this.originalColor = card.getColor(game).copy();
this.originalFrameColor = card.getFrameColor(game).copy();
} else {
this.color = originalColor.copy();
this.frameColor = originalFrameColor.copy();
}
this.frameStyle = card.getFrameStyle();
this.manaCost = card.getManaCost().copy();
if (card instanceof PermanentCard) {
this.maxLevelCounters = ((PermanentCard) card).maxLevelCounters;
}
this.power = card.getPower().copy();
this.toughness = card.getToughness().copy();
this.subtype.copyFrom(card.getSubtype());
this.supertype.clear();
this.supertype.addAll(card.getSuperType());
@ -149,7 +202,7 @@ public class PermanentCard extends PermanentImpl {
this.setImageFileName(card.getImageFileName());
this.setImageNumber(card.getImageNumber());
if (card.getSecondCardFace() != null) {
if (card.getSecondCardFace() != null && !(card instanceof DoubleFacedCardHalf)) {
this.secondSideCardClazz = card.getSecondCardFace().getClass();
}
if (card.getMeldsToCard() != null) {
@ -158,6 +211,22 @@ public class PermanentCard extends PermanentImpl {
this.nightCard = card.isNightCard();
this.flipCard = card.isFlipCard();
this.flipCardName = card.getFlipCardName();
// Rooms set characteristics at the end so nothing gets overwritten
if (card instanceof RoomCard) {
RoomCard.setRoomCharacteristics(this, game);
if (!isReset) {
RoomAbility roomAbility = null;
for (Ability ability : this.abilities) {
if (ability instanceof RoomAbility) {
roomAbility = (RoomAbility) ability;
break;
}
}
if (roomAbility != null) {
roomAbility.applyCharacteristics(game, this);
}
}
}
}
@Override

View file

@ -17,6 +17,7 @@ import mage.abilities.hint.HintUtils;
import mage.abilities.keyword.*;
import mage.cards.Card;
import mage.cards.CardImpl;
import mage.abilities.common.RoomAbility;
import mage.constants.*;
import mage.counters.Counter;
import mage.counters.CounterType;
@ -706,12 +707,15 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
+ CardUtil.getSourceLogName(game, source, this.getId()));
this.setTransformed(!this.transformed);
this.transformCount++;
initOtherFace(game);
game.applyEffects(); // not process action - no firing of simultaneous events yet
this.replaceEvent(EventType.TRANSFORMING, game);
game.addSimultaneousEvent(GameEvent.getEvent(EventType.TRANSFORMED, this.getId(), this.getControllerId()));
return true;
}
protected abstract void initOtherFace(Game game);
@Override
public int getTransformCount() {
return transformCount;
@ -2099,6 +2103,12 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
return roomWasUnlockedOnCast;
}
@Override
public void resetLockedStatus() {
leftHalfUnlocked = false;
rightHalfUnlocked = false;
}
@Override
public boolean isLeftDoorUnlocked() {
return leftHalfUnlocked;
@ -2141,15 +2151,27 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
rightHalfUnlocked = true;
}
// Fire door unlock event
// Update intrinsic stats/abilities from unlocking
// find the RoomCharacteristicsEffect applied by this permanent's ability
Abilities<Ability> abilities = this.getAbilities(game);
for (Ability ability : abilities) {
if (ability instanceof RoomAbility) {
((RoomAbility) ability).restoreUnlockedStats(game, this);
break;
}
}
// Create door unlock event
GameEvent event = new GameEvent(GameEvent.EventType.DOOR_UNLOCKED, getId(), source, source.getControllerId());
event.setFlag(isLeftDoor);
game.fireEvent(event);
// Check if room is now fully unlocked
boolean otherDoorUnlocked = isLeftDoor ? rightHalfUnlocked : leftHalfUnlocked;
if (otherDoorUnlocked) {
game.fireEvent(new GameEvent(EventType.ROOM_FULLY_UNLOCKED, getId(), source, source.getControllerId()));
game.addSimultaneousEvent(event);
game.addSimultaneousEvent(new GameEvent(EventType.ROOM_FULLY_UNLOCKED, getId(), source, source.getControllerId()));
} else {
game.fireEvent(event);
}
return true;

View file

@ -5,8 +5,8 @@ import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.costs.mana.ManaCost;
import mage.abilities.keyword.ChangelingAbility;
import mage.abilities.keyword.TransformAbility;
import mage.cards.Card;
import mage.cards.RoomCard;
import mage.constants.EmptyNames;
import mage.game.Game;
import mage.game.events.ZoneChangeEvent;
@ -30,13 +30,13 @@ public class PermanentToken extends PermanentImpl {
this.token = token.copy();
this.token.getAbilities().newOriginalId(); // neccessary if token has ability like DevourAbility()
this.token.getAbilities().setSourceId(objectId);
this.power = new MageInt(token.getPower().getModifiedBaseValue());
this.toughness = new MageInt(token.getToughness().getModifiedBaseValue());
this.copyFromToken(this.token, game, false); // needed to have at this time (e.g. for subtypes for entersTheBattlefield replacement effects)
// if transformed on ETB
if (this.token.isEntersTransformed()) {
TransformAbility.transformPermanent(this, game, null);
this.setTransformed(true);
this.copyFromToken(this.token.getBackFace(), game, false);
} else {
this.copyFromToken(this.token, game, false); // needed to have at this time (e.g. for subtypes for entersTheBattlefield replacement effects)
}
// token's ZCC must be synced with original token to keep abilities settings
@ -53,7 +53,11 @@ public class PermanentToken extends PermanentImpl {
@Override
public void reset(Game game) {
copyFromToken(token, game, true);
if (this.isTransformed()) {
copyFromToken(token.getBackFace(), game, true);
} else {
copyFromToken(token, game, true);
}
super.reset(game);
// Because the P/T objects have there own base value for reset we have to take it from there instead of from the basic token object
this.power.resetToBaseValue();
@ -110,8 +114,12 @@ public class PermanentToken extends PermanentImpl {
if (this.abilities.containsClass(ChangelingAbility.class)) {
this.subtype.setIsAllCreatureTypes(true);
}
this.power = new MageInt(token.getPower().getModifiedBaseValue());
this.toughness = new MageInt(token.getToughness().getModifiedBaseValue());
CardUtil.copySetAndCardNumber(this, token);
if (token.getCopySourceCard() instanceof RoomCard) {
RoomCard.setRoomCharacteristics(this, game);
}
}
@Override
@ -161,4 +169,13 @@ public class PermanentToken extends PermanentImpl {
public MageObject getOtherFace() {
return this.transformed ? token : this.token.getBackFace();
}
@Override
protected void initOtherFace(Game game) {
if (transformed) {
copyFromToken(token.getBackFace(), game, false);
} else {
copyFromToken(token, game, false);
}
}
}

View file

@ -2,6 +2,7 @@ package mage.game.stack;
import mage.*;
import mage.abilities.*;
import mage.abilities.common.SpellTransformedAbility;
import mage.abilities.costs.mana.ActivationManaAbilityStep;
import mage.abilities.costs.mana.ManaCost;
import mage.abilities.costs.mana.ManaCosts;
@ -102,6 +103,11 @@ public class Spell extends StackObjectImpl implements Card {
this.ability = ability;
this.ability.setControllerId(controllerId);
// 712.8c TDFC spell "Its mana value is calculated using the mana cost of its front face"
if(ability instanceof SpellTransformedAbility && manaCost.isEmpty()) {
this.manaCost = card.getMainCard().getManaCost().copy();
this.ability.setSourceId(affectedCard.getId()); // Maybe wrong? Permanent has incorrect id otherwise
}
if (ability.getSpellAbilityCastMode().isFaceDown()) {
// TODO: need research:
// - why it use game param for color and subtype (possible bug?)
@ -1187,6 +1193,16 @@ public class Spell extends StackObjectImpl implements Card {
throw new UnsupportedOperationException("Not supported.");
}
@Override
public void setPT(int power, int toughness) {
throw new UnsupportedOperationException("Not supported.");
}
@Override
public void setPT(MageInt power, MageInt toughness) {
throw new UnsupportedOperationException("Not supported.");
}
@Override
public boolean cantBeAttachedBy(MageObject attachment, Ability source, Game game, boolean silentMode) {
throw new UnsupportedOperationException("Not supported.");

View file

@ -4106,7 +4106,11 @@ public abstract class PlayerImpl implements Player, Serializable {
getPlayableFromObjectSingle(game, fromZone, mainCard.getLeftHalfCard(), mainCard.getLeftHalfCard().getAbilities(game), availableMana, output);
getPlayableFromObjectSingle(game, fromZone, mainCard.getRightHalfCard(), mainCard.getRightHalfCard().getAbilities(game), availableMana, output);
getPlayableFromObjectSingle(game, fromZone, mainCard, mainCard.getSharedAbilities(game), availableMana, output);
} else if (object instanceof CardWithSpellOption) {
} else if (object instanceof TransformingDoubleFacedCard) {
TransformingDoubleFacedCard mainCard = (TransformingDoubleFacedCard) object;
getPlayableFromObjectSingle(game, fromZone, mainCard.getLeftHalfCard(), mainCard.getLeftHalfCard().getAbilities(game), availableMana, output);
getPlayableFromObjectSingle(game, fromZone, mainCard, mainCard.getSharedAbilities(game), availableMana, output);
} else if (object instanceof CardWithSpellOption) {
// adventure must use different card characteristics for different spells (main or adventure)
CardWithSpellOption cardWithSpellOption = (CardWithSpellOption) object;
getPlayableFromObjectSingle(game, fromZone, cardWithSpellOption.getSpellCard(), cardWithSpellOption.getSpellCard().getAbilities(game), availableMana, output);
@ -4258,6 +4262,12 @@ public abstract class PlayerImpl implements Player, Serializable {
boolean isPlaySpell = (ability instanceof SpellAbility);
boolean isPlayLand = (ability instanceof PlayLandAbility);
// ignore backside of TDFC
// TODO: maybe better way to ignore
if (isPlaySpell && ((SpellAbility) ability).getSpellAbilityType() == SpellAbilityType.TRANSFORMED_RIGHT) {
continue;
}
// play land restrictions
if (isPlayLand && game.getContinuousEffects().preventedByRuleModification(
GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, ability.getSourceId(),
@ -4946,8 +4956,9 @@ public abstract class PlayerImpl implements Player, Serializable {
// or "converted," it enters the battlefield with its back face up. If a player is instructed to put a card
// that isn't a transforming double-faced card onto the battlefield transformed or converted, that card stays in
// its current zone.
// TODO: can probably remove/change after tdfc rework, should only be sending transformed side
Boolean enterTransformed = (Boolean) game.getState().getValue(TransformAbility.VALUE_KEY_ENTER_TRANSFORMED + card.getId());
if (enterTransformed != null && enterTransformed && !card.isTransformable()) {
if (enterTransformed != null && enterTransformed && !card.isTransformable() && !(card instanceof TransformingDoubleFacedCardHalf)) {
continue;
}

View file

@ -1261,8 +1261,8 @@ public final class CardUtil {
permCard = card;
} else if (card instanceof CardWithSpellOption) {
permCard = card;
} else if (card instanceof ModalDoubleFacedCard) {
permCard = ((ModalDoubleFacedCard) card).getLeftHalfCard();
} else if (card instanceof DoubleFacedCard) {
permCard = ((DoubleFacedCard) card).getLeftHalfCard();
} else {
permCard = card;
}
@ -1294,8 +1294,8 @@ public final class CardUtil {
// it's ok to return one name only cause NamePredicate can find same card by first name
if (card instanceof SplitCard) {
return ((SplitCard) card).getLeftHalfCard().getName();
} else if (card instanceof ModalDoubleFacedCard) {
return ((ModalDoubleFacedCard) card).getLeftHalfCard().getName();
} else if (card instanceof DoubleFacedCard) {
return ((DoubleFacedCard) card).getLeftHalfCard().getName();
} else {
return card.getName();
}
@ -1669,6 +1669,22 @@ public final class CardUtil {
game.getState().setValue("PlayFromNotOwnHandZone" + rightHalfCard.getId(), Boolean.TRUE);
}
// handle TDFC
if (card instanceof TransformingDoubleFacedCard) {
TransformingDoubleFacedCardHalf frontFace = ((TransformingDoubleFacedCard) card).getLeftHalfCard();
TransformingDoubleFacedCardHalf backFace = ((TransformingDoubleFacedCard) card).getRightHalfCard();
if (manaCost != null) {
// get additional cost if any
Costs<Cost> additionalCostsMDFCLeft = frontFace.getSpellAbility().getCosts();
// set alternative cost and any additional cost
player.setCastSourceIdWithAlternateMana(frontFace.getId(), manaCost, additionalCostsMDFCLeft, MageIdentifier.Default);
}
// allow just the front face
game.getState().setValue("PlayFromNotOwnHandZone" + frontFace.getId(), Boolean.TRUE);
}
// handle adventure cards
if (card instanceof CardWithSpellOption) {
Card creatureCard = card.getMainCard();
@ -1706,9 +1722,9 @@ public final class CardUtil {
game.getState().setValue("PlayFromNotOwnHandZone" + leftHalfCard.getId(), null);
game.getState().setValue("PlayFromNotOwnHandZone" + rightHalfCard.getId(), null);
}
if (card instanceof ModalDoubleFacedCard) {
ModalDoubleFacedCardHalf leftHalfCard = ((ModalDoubleFacedCard) card).getLeftHalfCard();
ModalDoubleFacedCardHalf rightHalfCard = ((ModalDoubleFacedCard) card).getRightHalfCard();
if (card instanceof DoubleFacedCard) {
DoubleFacedCardHalf leftHalfCard = ((DoubleFacedCard) card).getLeftHalfCard();
DoubleFacedCardHalf rightHalfCard = ((DoubleFacedCard) card).getRightHalfCard();
game.getState().setValue("PlayFromNotOwnHandZone" + leftHalfCard.getId(), null);
game.getState().setValue("PlayFromNotOwnHandZone" + rightHalfCard.getId(), null);
}
@ -2100,8 +2116,8 @@ public final class CardUtil {
res.add(mainCard);
res.add(mainCard.getLeftHalfCard());
res.add(mainCard.getRightHalfCard());
} else if (object instanceof ModalDoubleFacedCard || object instanceof ModalDoubleFacedCardHalf) {
ModalDoubleFacedCard mainCard = (ModalDoubleFacedCard) ((Card) object).getMainCard();
} else if (object instanceof DoubleFacedCard || object instanceof DoubleFacedCardHalf) {
DoubleFacedCard mainCard = (DoubleFacedCard) ((Card) object).getMainCard();
res.add(mainCard);
res.add(mainCard.getLeftHalfCard());
res.add(mainCard.getRightHalfCard());

View file

@ -643,8 +643,8 @@ public final class ManaUtil {
secondSide = ((SplitCard) card).getRightHalfCard();
} else if (card instanceof CardWithSpellOption) {
secondSide = ((CardWithSpellOption) card).getSpellCard();
} else if (card instanceof ModalDoubleFacedCard) {
secondSide = ((ModalDoubleFacedCard) card).getRightHalfCard();
} else if (card instanceof DoubleFacedCard) {
secondSide = ((DoubleFacedCard) card).getRightHalfCard();
} else {
secondSide = card.getSecondCardFace();
}

View file

@ -5,7 +5,7 @@ import mage.abilities.Abilities;
import mage.abilities.Ability;
import mage.abilities.effects.common.continuous.BecomesFaceDownCreatureEffect;
import mage.abilities.keyword.PrototypeAbility;
import mage.cards.Card;
import mage.cards.*;
import mage.constants.CardType;
import mage.constants.SuperType;
import mage.game.Game;
@ -91,9 +91,13 @@ public class CopyTokenFunction {
copyToToken(target, sourceObj, game);
CardUtil.copySetAndCardNumber(target, sourceObj);
// second side
if (sourceObj.isTransformable()) {
if (sourceObj.isTransformable() && !(sourceObj instanceof DoubleFacedCardHalf)) {
copyToToken(target.getBackFace(), sourceObj.getSecondCardFace(), game);
CardUtil.copySetAndCardNumber(target.getBackFace(), sourceObj.getSecondCardFace());
} else if (sourceObj.isTransformable() && sourceObj instanceof DoubleFacedCardHalf) {
// double faced card
copyToToken(target.getBackFace(), ((DoubleFacedCardHalf) sourceObj).getOtherSide(), game);
CardUtil.copySetAndCardNumber(target.getBackFace(), ((DoubleFacedCardHalf) sourceObj).getOtherSide());
}
// apply prototyped status
@ -108,6 +112,35 @@ public class CopyTokenFunction {
return;
}
// from double faced card spell
if (source instanceof DoubleFacedCardHalf) {
DoubleFacedCardHalf sourceCard = (DoubleFacedCardHalf) source;
Card frontSide;
Card backSide = null;
if (sourceCard.isTransformable()) {
if (sourceCard.isBackSide()) {
target.setEntersTransformed(true);
frontSide = sourceCard.getOtherSide();
backSide = sourceCard;
} else {
frontSide = sourceCard;
backSide = sourceCard.getOtherSide();
}
} else {
frontSide = sourceCard;
}
// main side
copyToToken(target, frontSide, game);
target.setCopySourceCard(sourceCard);
CardUtil.copySetAndCardNumber(target, frontSide);
// second side
if (backSide != null) {
copyToToken(target.getBackFace(), backSide, game);
CardUtil.copySetAndCardNumber(target, backSide);
}
return;
}
// from another card (example: Embalm ability)
Card sourceObj = CardUtil.getDefaultCardSideForBattlefield(game, source.getMainCard());
target.setCopySourceCard(sourceObj);
@ -121,8 +154,14 @@ public class CopyTokenFunction {
// must create back face??
throw new IllegalStateException("Wrong code usage: back face must be non null: " + target.getName() + " - " + target.getClass().getSimpleName());
}
copyToToken(target.getBackFace(), source.getSecondCardFace(), game);
CardUtil.copySetAndCardNumber(target.getBackFace(), source.getSecondCardFace());
Card secondFace;
if (source instanceof DoubleFacedCard) {
secondFace = ((DoubleFacedCard) source).getRightHalfCard();
} else {
secondFace = source.getSecondCardFace();
}
copyToToken(target.getBackFace(), secondFace, game);
CardUtil.copySetAndCardNumber(target.getBackFace(), secondFace);
}
}