foul-magics/Mage/src/main/java/mage/cards/DoubleFacedCard.java
Jmlundeen 69e20b1061
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
2025-11-27 09:24:03 -06:00

413 lines
14 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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();
}
}