forked from External/mage
* 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
413 lines
14 KiB
Java
413 lines
14 KiB
Java
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 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);
|
||
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 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<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 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();
|
||
}
|
||
}
|