forked from External/mage
* refactor: added helper emblems instead rad counter's inherent emblems (use initGameDefaultHelperEmblems to define new card hints or other fake objects); * refactor: added card hints support for emblems, planes and other command objects; * GUI: added storm counter as default card hint (use hints tool to see it, closes #12360);
435 lines
16 KiB
Java
435 lines
16 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
|
||
*/
|
||
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<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 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<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) {
|
||
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<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 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 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();
|
||
}
|
||
}
|