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 ModalDoubleFacedCard( UUID ownerId, CardSetInfo setInfo, CardType[] typesLeft, SubType[] subTypesLeft, String costsLeft, String secondSideName, CardType[] typesRight, SubType[] subTypesRight, String costsRight ) { this( ownerId, setInfo, new SuperType[]{}, typesLeft, subTypesLeft, costsLeft, secondSideName, new SuperType[]{}, typesRight, subTypesRight, costsRight ); } public ModalDoubleFacedCard( UUID ownerId, CardSetInfo setInfo, SuperType[] superTypesLeft, CardType[] typesLeft, SubType[] subTypesLeft, String costsLeft, String secondSideName, SuperType[] superTypesRight, CardType[] typesRight, SubType[] subTypesRight, String costsRight ) { super(ownerId, setInfo, typesLeft, costsLeft + costsRight, SpellAbilityType.MODAL); // main card name must be same as left side leftHalfCard = new ModalDoubleFacedCardHalfImpl( this.getOwnerId(), setInfo.copy(), superTypesLeft, typesLeft, subTypesLeft, costsLeft, this, SpellAbilityType.MODAL_LEFT ); rightHalfCard = new ModalDoubleFacedCardHalfImpl( this.getOwnerId(), new CardSetInfo(secondSideName, setInfo), superTypesRight, typesRight, subTypesRight, costsRight, this, SpellAbilityType.MODAL_RIGHT ); } public ModalDoubleFacedCard(ModalDoubleFacedCard card) { super(card); 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 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 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 that’s 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 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) { switch (ability.getSpellAbilityType()) { case MODAL_LEFT: return this.leftHalfCard.cast(game, fromZone, ability, controllerId); 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 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 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 isn’t 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 isn’t 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 getAbilities() { return getInnerAbilities(true, true); } @Override public Abilities getInitAbilities() { // must init only parent related abilities, spell card must be init separately return getInnerAbilities(false, false); } public Abilities getSharedAbilities(Game game) { // no shared abilities for mdf cards (e.g. must be left or right only) return new AbilitiesImpl<>(); } @Override public Abilities 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 getInnerAbilities(Game game, boolean showLeftSide, boolean showRightSide) { Abilities 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 getInnerAbilities(boolean showLeftSide, boolean showRightSide) { Abilities 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 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 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 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 that’s 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(); } }