foul-magics/Mage/src/main/java/mage/game/stack/Spell.java
2025-07-22 16:32:41 -04:00

1271 lines
45 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.game.stack;
import mage.*;
import mage.abilities.*;
import mage.abilities.costs.mana.ActivationManaAbilityStep;
import mage.abilities.costs.mana.ManaCost;
import mage.abilities.costs.mana.ManaCosts;
import mage.abilities.keyword.BestowAbility;
import mage.abilities.keyword.PrototypeAbility;
import mage.abilities.keyword.TransformAbility;
import mage.cards.*;
import mage.constants.*;
import mage.counters.Counter;
import mage.counters.Counters;
import mage.filter.FilterMana;
import mage.filter.predicate.mageobject.MageObjectReferencePredicate;
import mage.game.Game;
import mage.game.GameState;
import mage.game.MageObjectAttribute;
import mage.game.events.CopiedStackObjectEvent;
import mage.game.events.ZoneChangeEvent;
import mage.game.permanent.Permanent;
import mage.game.permanent.PermanentCard;
import mage.game.permanent.token.Token;
import mage.players.Player;
import mage.util.CardUtil;
import mage.util.GameLog;
import mage.util.ManaUtil;
import mage.util.SubTypes;
import mage.util.functions.CopyTokenFunction;
import mage.util.functions.StackObjectCopyApplier;
import org.apache.log4j.Logger;
import java.util.*;
/**
* @author BetaSteward_at_googlemail.com
*/
public class Spell extends StackObjectImpl implements Card {
private static final Logger logger = Logger.getLogger(Spell.class);
private final List<SpellAbility> spellAbilities = new ArrayList<>();
private final Card card;
private ManaCosts<ManaCost> manaCost;
private final ObjectColor color;
private final ObjectColor frameColor;
private final FrameStyle frameStyle;
private final SpellAbility ability;
private final Zone fromZone;
private final UUID id;
protected int zoneChangeCounter; // spell's ZCC must be synced with card's on stack or another copied spell
private UUID controllerId;
private boolean copy;
private MageObject copyFrom; // copied card INFO (used to call original adjusters)
private boolean faceDown;
private boolean countered;
private boolean resolving = false;
private UUID commandedByPlayerId = null; // controller of the spell resolve, example: Word of Command
private String commandedByInfo; // info about spell commanded, e.g. source
private boolean prototyped;
private int startingLoyalty;
private int startingDefense;
private ActivationManaAbilityStep currentActivatingManaAbilitiesStep = ActivationManaAbilityStep.BEFORE;
public Spell(Card card, SpellAbility ability, UUID controllerId, Zone fromZone, Game game) {
this(card, ability, controllerId, fromZone, game, false);
}
private Spell(Card card, SpellAbility ability, UUID controllerId, Zone fromZone, Game game, boolean isCopy) {
if (card == null) {
throw new IllegalArgumentException("Wrong code usage: can't create spell without card: " + ability, new Throwable());
}
Card affectedCard = card;
// TODO: must be removed after transform cards (one side) migrated to MDF engine (multiple sides)
if (ability.getSpellAbilityCastMode().isTransformed() && affectedCard.getSecondCardFace() != null) {
// simulate another side as new card (another code part in continues effect from disturb ability)
affectedCard = TransformAbility.transformCardSpellStatic(card, card.getSecondCardFace(), game);
}
if (ability instanceof PrototypeAbility) {
affectedCard = ((PrototypeAbility) ability).prototypeCardSpell(card);
this.prototyped = true;
}
this.card = affectedCard;
this.manaCost = affectedCard.getManaCost().copy();
this.color = affectedCard.getColor(null).copy();
this.frameColor = affectedCard.getFrameColor(null).copy();
this.frameStyle = affectedCard.getFrameStyle();
this.startingLoyalty = affectedCard.getStartingLoyalty();
this.startingDefense = affectedCard.getStartingDefense();
this.id = ability.getId();
this.zoneChangeCounter = affectedCard.getZoneChangeCounter(game); // sync card's ZCC with spell (copy spell settings)
this.ability = ability;
this.ability.setControllerId(controllerId);
if (ability.getSpellAbilityCastMode().isFaceDown()) {
// TODO: need research:
// - why it use game param for color and subtype (possible bug?)
// - is it possible to use BecomesFaceDownCreatureEffect.makeFaceDownObject or like that?
this.faceDown = true;
this.getColor(game).setColor(null);
game.getState().getCreateMageObjectAttribute(this.getCard(), game).getSubtype().clear();
}
if (ability.getSpellAbilityType() == SpellAbilityType.SPLIT_FUSED) {
// if this spell is going to be a copy, these abilities will be copied in copySpell
if (!isCopy) {
SpellAbility left = ((SplitCard) affectedCard).getLeftHalfCard().getSpellAbility().copy();
SpellAbility right = ((SplitCard) affectedCard).getRightHalfCard().getSpellAbility().copy();
left.setSourceId(ability.getSourceId());
right.setSourceId(ability.getSourceId());
spellAbilities.add(left);
spellAbilities.add(right);
}
} else {
spellAbilities.add(ability);
}
this.controllerId = controllerId;
this.fromZone = fromZone;
this.countered = false;
}
protected Spell(final Spell spell) {
this.id = spell.id;
this.zoneChangeCounter = spell.zoneChangeCounter;
for (SpellAbility spellAbility : spell.spellAbilities) {
this.spellAbilities.add(spellAbility.copy());
}
if (spell.spellAbilities.get(0).equals(spell.ability)) {
this.ability = this.spellAbilities.get(0);
} else {
this.ability = spell.ability.copy();
}
this.card = spell.card.copy();
this.fromZone = spell.fromZone;
this.manaCost = spell.getManaCost().copy();
this.color = spell.color.copy();
this.frameColor = spell.color.copy();
this.frameStyle = spell.frameStyle;
this.controllerId = spell.controllerId;
this.copy = spell.copy;
this.copyFrom = (spell.copyFrom != null ? spell.copyFrom.copy() : null);
this.faceDown = spell.faceDown;
this.countered = spell.countered;
this.resolving = spell.resolving;
this.commandedByPlayerId = spell.commandedByPlayerId;
this.commandedByInfo = spell.commandedByInfo;
this.currentActivatingManaAbilitiesStep = spell.currentActivatingManaAbilitiesStep;
this.targetChanged = spell.targetChanged;
this.prototyped = spell.prototyped;
this.startingLoyalty = spell.startingLoyalty;
this.startingDefense = spell.startingDefense;
}
public boolean activate(Game game, Set<MageIdentifier> allowedIdentifiers, boolean noMana) {
setCurrentActivatingManaAbilitiesStep(ActivationManaAbilityStep.BEFORE); // mana payment step started, can use any mana abilities, see AlternateManaPaymentAbility
if (!ability.activate(game, allowedIdentifiers, noMana)) {
return false;
}
// spell can contains multiple abilities to activate (fused split, splice)
for (SpellAbility spellAbility : spellAbilities) {
if (ability.equals(spellAbility)) {
// activated first
continue;
}
boolean payNoMana = noMana;
// costs for spliced abilities were added to main spellAbility, so pay no mana for spliced abilities
payNoMana |= spellAbility.getSpellAbilityType() == SpellAbilityType.SPLICE;
// costs for fused ability pay on first spell activate, so all parts must be without mana
// see https://github.com/magefree/mage/issues/6603
payNoMana |= ability.getSpellAbilityType() == SpellAbilityType.SPLIT_FUSED;
if (!spellAbility.activate(game, allowedIdentifiers, payNoMana)) {
return false;
}
}
setCurrentActivatingManaAbilitiesStep(ActivationManaAbilityStep.NORMAL);
return true;
}
public String getActivatedMessage(Game game, Zone fromZone) {
StringBuilder sb = new StringBuilder();
sb.append(" casts ");
if (isCopy()) {
sb.append("a copied ");
}
sb.append(ability.getGameLogMessage(game));
sb.append(" from ");
sb.append(fromZone.toString().toLowerCase(Locale.ENGLISH));
return sb.toString();
}
public String getSpellCastText(Game game) {
if (this.getSpellAbility().getSpellAbilityCastMode().isFaceDown()) {
// add face down name with object link, so user can look at it from logs
return "a " + GameLog.getColoredObjectIdName(this.getSpellAbility().getCharacteristics(game))
+ " using " + this.getSpellAbility().getSpellAbilityCastMode();
}
if (card instanceof SpellOptionCard) {
CardWithSpellOption parentCard = ((SpellOptionCard) card).getParentCard();
String type = ((SpellOptionCard) card).getSpellType();
return GameLog.replaceNameByColoredName(card, getSpellAbility().toString(), parentCard)
+ " as " + type + " spell of " + GameLog.getColoredObjectIdName(parentCard);
}
if (card instanceof ModalDoubleFacedCardHalf) {
ModalDoubleFacedCard mdfCard = (ModalDoubleFacedCard) card.getMainCard();
return GameLog.replaceNameByColoredName(card, getSpellAbility().toString(), mdfCard)
+ " as mdf side of " + GameLog.getColoredObjectIdName(mdfCard);
}
return GameLog.replaceNameByColoredName(card, getSpellAbility().toString());
}
@Override
public String getExpansionSetCode() {
return card.getExpansionSetCode();
}
@Override
public void setExpansionSetCode(String expansionSetCode) {
throw new IllegalStateException("Wrong code usage: you can't change set code for the spell");
}
@Override
public String getCardNumber() {
return card.getCardNumber();
}
@Override
public void setCardNumber(String cardNumber) {
throw new IllegalStateException("Wrong code usage: you can't change card number for the spell");
}
@Override
public String getImageFileName() {
return card.getImageFileName();
}
@Override
public void setImageFileName(String imageFile) {
throw new IllegalStateException("Wrong code usage: you can't change image file name for the spell");
}
@Override
public Integer getImageNumber() {
return card.getImageNumber();
}
@Override
public void setImageNumber(Integer imageNumber) {
throw new IllegalStateException("Wrong code usage: you can't change image number for the spell");
}
@Override
public boolean resolve(Game game) {
boolean result;
Player controller = game.getPlayer(getControllerId());
if (controller == null) {
return false;
}
this.resolving = true;
// setup new turn controller for spell's resolve, example: Word of Command
// original controller will be reset after spell's resolve
if (commandedByPlayerId != null && !commandedByPlayerId.equals(getControllerId())) {
Player newTurnController = game.getPlayer(commandedByPlayerId);
if (newTurnController != null) {
newTurnController.controlPlayersTurn(game, controller.getId(), commandedByInfo);
}
}
if (this.isInstantOrSorcery(game)) {
int index = 0;
result = false;
boolean legalParts = false;
boolean notTargeted = true;
// check for legal parts
for (SpellAbility spellAbility : this.spellAbilities) {
// if muliple modes are selected, and there are modes with targets, then at least one mode has to have a legal target or
// When resolving a fused split spell with multiple targets, treat it as you would any spell with multiple targets.
// If all targets are illegal when the spell tries to resolve, the spell is countered and none of its effects happen.
// If at least one target is still legal at that time, the spell resolves, but an illegal target can't perform any actions
// or have any actions performed on it.
// if only a spliced spell has targets and all targets ar illegal, the complete spell is countered
if (hasTargets(spellAbility, game)) {
notTargeted = false;
legalParts |= spellAbilityHasLegalParts(spellAbility, game);
}
}
// resolve if legal parts
if (notTargeted || legalParts) {
for (SpellAbility spellAbility : this.spellAbilities) {
// legality of targets is checked only as the spell begins to resolve, not in between modes (spliced spells handeled correctly?)
if (spellAbilityCheckTargetsAndDeactivateModes(spellAbility, game)) {
for (UUID modeId : spellAbility.getModes().getSelectedModes()) {
spellAbility.getModes().setActiveMode(modeId);
result |= spellAbility.resolve(game);
}
index++;
}
}
if (game.getState().getZone(card.getMainCard().getId()) == Zone.STACK) {
if (isCopy()) {
// copied spell, only remove from stack
game.getStack().remove(this, game);
} else {
controller.moveCards(card, Zone.GRAVEYARD, ability, game);
}
}
return result;
}
//20091005 - 608.2b
if (!game.isSimulation()) {
game.informPlayers(getName() + " has been fizzled.");
}
counter(null, /*this.getSpellAbility()*/ game);
return false;
} else if (this.isEnchantment(game) && this.hasSubtype(SubType.AURA, game)) {
if (ability.getTargets().stillLegal(ability, game)) {
boolean bestow = SpellAbilityCastMode.BESTOW.equals(ability.getSpellAbilityCastMode());
if (bestow) {
// before put to play:
// Must be removed first time, after that will be removed by continous effect
// Otherwise effects like evolve trigger from creature comes into play event
card.removeCardType(CardType.CREATURE);
card.addSubType(game, SubType.AURA);
}
UUID permId;
boolean flag;
if (isCopy()) {
Token token = CopyTokenFunction.createTokenCopy(card, game, this);
// The token that a resolving copy of a spell becomes isnt said to have been “created.” (2020-09-25)
if (token.putOntoBattlefield(1, game, ability, getControllerId(), false, false, null, null, false)) {
permId = token.getLastAddedTokenIds().stream().findFirst().orElse(null);
flag = true;
} else {
permId = null;
flag = false;
}
} else {
permId = card.getId();
MageObjectReference mor = new MageObjectReference(getSpellAbility());
game.storePermanentCostsTags(mor, getSpellAbility());
flag = controller.moveCards(card, Zone.BATTLEFIELD, ability, game, false, faceDown, false, null);
}
if (flag) {
if (bestow) {
// card will be copied during putOntoBattlefield, so the card of CardPermanent has to be changed
// TODO: Find a better way to prevent bestow creatures from being effected by creature affecting abilities
Permanent permanent = game.getPermanent(permId);
if (permanent instanceof PermanentCard) {
// after put to play:
// restore removed stats (see "before put to play" above)
permanent.setSpellAbility(ability); // otherwise spell ability without bestow will be set
card.addCardType(CardType.CREATURE);
card.getSubtype().remove(SubType.AURA);
}
}
if (isCopy()) {
Permanent token = game.getPermanent(permId);
if (token == null) {
return false;
}
for (Ability ability2 : token.getAbilities()) {
if (!bestow || ability2 instanceof BestowAbility) {
ability2.getTargets().get(0).add(ability.getFirstTarget(), game);
ability2.getEffects().get(0).apply(game, ability2);
return ability2.resolve(game);
}
}
return false;
}
return ability.resolve(game);
}
if (bestow) {
card.addCardType(game, CardType.CREATURE);
}
return false;
}
// Aura has no legal target and its a bestow enchantment -> Add it to battlefield as creature
if (SpellAbilityCastMode.BESTOW.equals(this.getSpellAbility().getSpellAbilityCastMode())) {
MageObjectReference mor = new MageObjectReference(getSpellAbility());
game.storePermanentCostsTags(mor, getSpellAbility());
if (controller.moveCards(card, Zone.BATTLEFIELD, ability, game, false, faceDown, false, null)) {
Permanent permanent = game.getPermanent(card.getId());
if (permanent instanceof PermanentCard) {
((PermanentCard) permanent).getCard().addCardType(game, CardType.CREATURE);
((PermanentCard) permanent).getCard().removeSubType(game, SubType.AURA);
return true;
}
}
return false;
} else {
//20091005 - 608.2b
if (!game.isSimulation()) {
game.informPlayers(getName() + " has been fizzled.");
}
counter(null, /*this.getSpellAbility()*/ game);
return false;
}
} else if (isCopy()) {
Token token = CopyTokenFunction.createTokenCopy(card, game, this);
// The token that a resolving copy of a spell becomes isnt said to have been “created.” (2020-09-25)
token.putOntoBattlefield(1, game, ability, getControllerId(), false, false, null, null, false);
return true;
} else {
MageObjectReference mor = new MageObjectReference(getSpellAbility());
game.storePermanentCostsTags(mor, getSpellAbility());
return controller.moveCards(card, Zone.BATTLEFIELD, ability, game, false, faceDown, false, null);
}
}
private boolean hasTargets(SpellAbility spellAbility, Game game) {
if (spellAbility.getModes().getSelectedModes().size() < 2) {
return !spellAbility.getTargets().isEmpty();
}
for (UUID modeId : spellAbility.getModes().getSelectedModes()) {
if (!spellAbility.getModes().get(modeId).getTargets().isEmpty()) {
return true;
}
}
return false;
}
/**
* Legality of the targets of all modes are only checked as the spell begins
* to resolve A mode without any legal target (if it has targets at all)
* won't resolve. So modes with targets without legal targets are
* unselected.
*
* @param spellAbility
* @param game
* @return
*/
private boolean spellAbilityCheckTargetsAndDeactivateModes(SpellAbility spellAbility, Game game) {
boolean legalModes = false;
for (Iterator<UUID> iterator = spellAbility.getModes().getSelectedModes().iterator(); iterator.hasNext(); ) {
UUID nextSelectedModeId = iterator.next();
Mode mode = spellAbility.getModes().get(nextSelectedModeId);
if (!mode.getTargets().isEmpty()) {
if (!mode.getTargets().stillLegal(spellAbility, game)) {
spellAbility.getModes().removeSelectedMode(mode.getId());
iterator.remove();
continue;
}
}
legalModes = true;
}
return legalModes;
}
private boolean spellAbilityHasLegalParts(SpellAbility spellAbility, Game game) {
if (spellAbility.getModes().getSelectedModes().size() > 1) {
boolean targetedMode = false;
boolean legalTargetedMode = false;
for (UUID modeId : spellAbility.getModes().getSelectedModes()) {
Mode mode = spellAbility.getModes().get(modeId);
if (!mode.getTargets().isEmpty()) {
targetedMode = true;
if (mode.getTargets().stillLegal(spellAbility, game)) {
legalTargetedMode = true;
}
}
}
if (targetedMode) {
return legalTargetedMode;
}
return true;
} else {
return spellAbility.getTargets().stillLegal(spellAbility, game);
}
}
@Override
public void counter(Ability source, Game game) {
this.counter(source, game, PutCards.GRAVEYARD);
}
@Override
public void counter(Ability source, Game game, PutCards putCard) {
// source can be null for fizzled spells, don't use that code in your ZONE_CHANGE watchers/triggers:
// event.getSourceId().equals
// TODO: fizzled spells are no longer considered "countered" as of current rules; may need refactor
this.countered = true;
if (isCopy()) {
// copied spell, only remove from stack
game.getStack().remove(this, game);
return;
}
Player player = game.getPlayer(source == null ? getControllerId() : source.getControllerId());
if (player != null) {
putCard.moveCard(player, card, source, game, "countered spell");
}
}
public ActivationManaAbilityStep getCurrentActivatingManaAbilitiesStep() {
return this.currentActivatingManaAbilitiesStep;
}
public void setCurrentActivatingManaAbilitiesStep(ActivationManaAbilityStep currentActivatingManaAbilitiesStep) {
this.currentActivatingManaAbilitiesStep = currentActivatingManaAbilitiesStep;
}
@Override
public UUID getSourceId() {
return card.getId();
}
@Override
public UUID getControllerId() {
return this.controllerId;
}
@Override
public UUID getControllerOrOwnerId() {
return getControllerId();
}
@Override
public String getName() {
return card.getName();
}
@Override
public String getIdName() {
String idName;
if (card != null) {
if (card instanceof SpellOptionCard) {
idName = ((SpellOptionCard) card).getParentCard().getId().toString().substring(0, 3);
} else {
idName = card.getId().toString().substring(0, 3);
}
} else {
idName = getId().toString().substring(0, 3);
}
return getName() + " [" + idName + ']';
}
@Override
public String getLogName() {
if (faceDown) {
return "face down spell";
}
return GameLog.getColoredObjectIdName(card);
}
@Override
public void setName(String name) {
}
@Override
public Rarity getRarity() {
return card.getRarity();
}
@Override
public void setRarity(Rarity rarity) {
throw new IllegalArgumentException("Un-supported operation: " + this, new Throwable());
}
@Override
public List<CardType> getCardType(Game game) {
if (faceDown) {
List<CardType> cardTypes = new ArrayList<>();
cardTypes.add(CardType.CREATURE);
return cardTypes;
}
if (SpellAbilityCastMode.BESTOW.equals(this.getSpellAbility().getSpellAbilityCastMode())) {
List<CardType> cardTypes = new ArrayList<>();
cardTypes.addAll(card.getCardType(game));
cardTypes.remove(CardType.CREATURE);
return cardTypes;
}
return card.getCardType(game);
}
@Override
public SubTypes getSubtype() {
return card.getSubtype();
}
@Override
public SubTypes getSubtype(Game game) {
if (SpellAbilityCastMode.BESTOW.equals(this.getSpellAbility().getSpellAbilityCastMode())) {
SubTypes subtypes = card.getSubtype(game);
if (!subtypes.contains(SubType.AURA)) { // do it only once
subtypes.add(SubType.AURA);
}
return subtypes;
}
return card.getSubtype(game);
}
@Override
public boolean hasSubtype(SubType subtype, Game game) {
if (SpellAbilityCastMode.BESTOW.equals(this.getSpellAbility().getSpellAbilityCastMode())) { // workaround for Bestow (don't like it)
SubTypes subtypes = card.getSubtype(game);
if (!subtypes.contains(SubType.AURA)) { // do it only once
subtypes.add(SubType.AURA);
}
if (subtypes.contains(subtype)) {
return true;
}
}
return card.hasSubtype(subtype, game);
}
@Override
public List<SuperType> getSuperType(Game game) {
return card.getSuperType(game);
}
public List<SpellAbility> getSpellAbilities() {
return spellAbilities;
}
@Override
public Abilities<Ability> getAbilities() {
return card.getAbilities();
}
@Override
public Abilities<Ability> getInitAbilities() {
return new AbilitiesImpl<>();
}
@Override
public Abilities<Ability> getAbilities(Game game) {
return card.getAbilities(game);
}
@Override
public boolean hasAbility(Ability ability, Game game) {
return card.hasAbility(ability, game);
}
@Override
public ObjectColor getColor() {
return color;
}
@Override
public ObjectColor getColor(Game game) {
if (game != null) {
MageObjectAttribute mageObjectAttribute = game.getState().getMageObjectAttribute(getId());
if (mageObjectAttribute != null) {
return mageObjectAttribute.getColor();
}
}
return color;
}
@Override
public ObjectColor getFrameColor(Game game) {
return frameColor;
}
@Override
public FrameStyle getFrameStyle() {
return frameStyle;
}
@Override
public ManaCosts<ManaCost> getManaCost() {
return this.manaCost;
}
@Override
public void setManaCost(ManaCosts<ManaCost> costs) {
this.manaCost = costs.copy();
}
/**
* 202.3b When calculating the converted mana cost of an object with an {X}
* in its mana cost, X is treated as 0 while the object is not on the stack,
* and X is treated as the number chosen for it while the object is on the
* stack.
*
* @return
*/
@Override
public int getManaValue() {
int cmc = 0;
if (faceDown) {
return 0;
}
for (SpellAbility spellAbility : spellAbilities) {
cmc += spellAbility.getConvertedXManaCost(getCard());
}
cmc += this.manaCost.manaValue();
return cmc;
}
@Override
public MageInt getPower() {
return card.getPower();
}
@Override
public MageInt getToughness() {
return card.getToughness();
}
@Override
public int getStartingLoyalty() {
return this.startingLoyalty;
}
@Override
public void setStartingLoyalty(int startingLoyalty) {
this.startingLoyalty = startingLoyalty;
}
@Override
public int getStartingDefense() {
return startingDefense;
}
@Override
public void setStartingDefense(int startingDefense) {
this.startingDefense = startingDefense;
}
@Override
public UUID getId() {
return id;
}
@Override
public UUID getOwnerId() {
return card.getOwnerId();
}
public void addSpellAbility(SpellAbility spellAbility) {
spellAbilities.add(spellAbility);
}
@Override
public void addAbility(Ability ability) {
throw new UnsupportedOperationException("Not supported.");
}
// To add abilities to permanent spell copies in a StackObjectCopyApplier which will persist into the resulting token.
public void addAbilityForCopy(Ability ability) {
card.addAbility(ability);
}
@Override
public SpellAbility getSpellAbility() {
return ability;
}
public void setControllerId(UUID controllerId) {
this.ability.setControllerId(controllerId);
for (SpellAbility spellAbility : spellAbilities) {
spellAbility.setControllerId(controllerId);
}
this.controllerId = controllerId;
}
@Override
public void setOwnerId(UUID controllerId) {
}
@Override
public List<String> getRules() {
return card.getRules();
}
@Override
public List<String> getRules(Game game) {
return card.getRules(game);
}
@Override
public void setFaceDown(boolean value, Game game) {
faceDown = value;
}
@Override
public boolean turnFaceUp(Ability source, Game game, UUID playerId) {
throw new IllegalStateException("Spells un-support turn face up commands");
}
@Override
public boolean turnFaceDown(Ability source, Game game, UUID playerId) {
throw new IllegalStateException("Spells un-support turn face up commands");
}
@Override
public boolean isFaceDown(Game game) {
return faceDown;
}
@Override
public boolean isFlipCard() {
return false;
}
@Override
public String getFlipCardName() {
return null;
}
@Override
public boolean isTransformable() {
return false;
}
@Override
public Card getSecondCardFace() {
return card.getSecondCardFace();
}
@Override
public SpellAbility getSecondFaceSpellAbility() {
return null;
}
@Override
public boolean isNightCard() {
return false;
}
public boolean isPrototyped() {
return prototyped;
}
@Override
public Spell copy() {
return new Spell(this);
}
/**
* Copy current spell on stack, but do not put copy back to stack (you can modify and put it later)
* <p>
* Warning, don't forget to call CopyStackObjectEvent and CopiedStackObjectEvent before and after copy
* CopyStackObjectEvent can change new copies amount, see Twinning Staff
* <p>
* Warning, don't forget to call spell.setZone before push to stack
*
* @param game
* @param newController controller of the copied spell
* @return
*/
public Spell copySpell(Game game, Ability source, UUID newController) {
// copied spells must use copied cards
// spell can be from card's part (mdf/adventure), but you must copy FULL card
Card copiedMainCard = game.copyCard(this.card.getMainCard(), source, newController);
// find copied part
Map<UUID, MageObject> mapOldToNew = CardUtil.getOriginalToCopiedPartsMap(this.card.getMainCard(), copiedMainCard);
if (!mapOldToNew.containsKey(this.card.getId())) {
throw new IllegalStateException("Can't find card id after main card copy: " + copiedMainCard.getName());
}
Card copiedPart = (Card) mapOldToNew.get(this.card.getId());
// copy spell
Spell spellCopy = new Spell(copiedPart, this.ability.copySpell(this.card, copiedPart), this.controllerId, this.fromZone, game, true);
UUID copiedSourceId = spellCopy.ability.getSourceId();
// non-fused spell:
// this.spellAbilities.get(0) is alias (NOT copy) of this.ability
// this.spellAbilities.get(1) is first spliced card (if any)
// fused spell:
// this.spellAbilities.get(0) is left half
// this.spellAbilities.get(1) is right half
// this.spellAbilities.get(2) is first spliced card (if any)
// for non-fused spell, ability was already added to spellAbilities in constructor and must not be copied again
// for fused spell, all of spellAbilities must be copied here
boolean skipFirst = (this.ability.getSpellAbilityType() != SpellAbilityType.SPLIT_FUSED);
for (SpellAbility spellAbility : this.getSpellAbilities()) {
if (skipFirst) {
skipFirst = false;
continue;
}
SpellAbility newAbility = spellAbility.copy(); // e.g. spliced spell
newAbility.newId();
newAbility.setSourceId(copiedSourceId);
spellCopy.addSpellAbility(newAbility);
}
spellCopy.setCopy(true, this);
spellCopy.setControllerId(newController);
spellCopy.syncZoneChangeCounterOnStack(this, game);
return spellCopy;
}
@Override
public boolean removeFromZone(Game game, Zone fromZone, Ability source) {
return card.removeFromZone(game, fromZone, source);
}
@Override
public boolean moveToZone(Zone zone, Ability source, Game game, boolean flag) {
return moveToZone(zone, source, game, flag, null);
}
@Override
public boolean moveToZone(Zone zone, Ability source, Game game, boolean flag, List<UUID> appliedEffects) {
// 706.10a If a copy of a spell is in a zone other than the stack, it ceases to exist.
// If a copy of a card is in any zone other than the stack or the battlefield, it ceases to exist.
// These are state-based actions. See rule 704.
if (this.isCopy() && zone != Zone.STACK) {
return true;
}
return card.moveToZone(zone, source, game, flag, appliedEffects);
}
@Override
public boolean moveToExile(UUID exileId, String name, Ability source, Game game) {
return moveToExile(exileId, name, source, game, null);
}
@Override
public boolean moveToExile(UUID exileId, String name, Ability source, Game game, List<UUID> appliedEffects) {
if (this.isCopy()) {
// copied spell, only remove from stack
game.getStack().remove(this, game);
return true;
}
return this.card.moveToExile(exileId, name, source, game, appliedEffects);
}
@Override
public boolean putOntoBattlefield(Game game, Zone fromZone, Ability source, UUID controllerId) {
throw new UnsupportedOperationException("Unsupported operation");
}
@Override
public boolean putOntoBattlefield(Game game, Zone fromZone, Ability source, UUID controllerId, boolean tapped) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public boolean putOntoBattlefield(Game game, Zone fromZone, Ability source, UUID controllerId, boolean tapped, boolean facedown) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public boolean putOntoBattlefield(Game game, Zone fromZone, Ability source, UUID controllerId, boolean tapped, boolean facedown, List<UUID> appliedEffects) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public boolean getUsesVariousArt() {
return card.getUsesVariousArt();
}
@Override
public void setUsesVariousArt(boolean usesVariousArt) {
card.setUsesVariousArt(usesVariousArt);
}
@Override
public List<Mana> getMana() {
return card.getMana();
}
@Override
public boolean cast(Game game, Zone fromZone, SpellAbility ability, UUID controllerId) {
throw new UnsupportedOperationException("Unsupported operation");
}
@Override
public Ability getStackAbility() {
return this.ability;
}
@Override
public void assignNewId() {
throw new UnsupportedOperationException("Unsupported operation");
}
@Override
public int getZoneChangeCounter(Game game) {
// spell's zcc can't be changed after put to stack
return zoneChangeCounter;
}
@Override
public void updateZoneChangeCounter(Game game, ZoneChangeEvent event) {
throw new UnsupportedOperationException("Unsupported operation");
}
@Override
public void setZoneChangeCounter(int value, Game game) {
throw new UnsupportedOperationException("Unsupported operation");
}
/**
* Sync ZCC with card on stack
*
* @param card
* @param game
*/
public void syncZoneChangeCounterOnStack(Card card, Game game) {
this.zoneChangeCounter = card.getZoneChangeCounter(game);
}
/**
* Sync ZCC with copy spell on stack
*
* @param spell
* @param game
*/
public void syncZoneChangeCounterOnStack(Spell spell, Game game) {
this.zoneChangeCounter = spell.getZoneChangeCounter(game);
}
@Override
public void addInfo(String key, String value, Game game) {
// do nothing
}
public Zone getFromZone() {
return this.fromZone;
}
@Override
public void setCopy(boolean isCopy, MageObject copyFrom) {
this.copy = isCopy;
this.copyFrom = (copyFrom != null ? copyFrom.copy() : null);
}
/**
* Game processing a copies as normal cards, so you don't need to check spell's copy for move/exile.
* Use this only in exceptional situations or to skip unaffected code/choices.
*
* @return
*/
@Override
public boolean isCopy() {
return this.copy;
}
@Override
public MageObject getCopyFrom() {
return this.copyFrom;
}
@Override
public Counters getCounters(Game game) {
return card.getCounters(game);
}
@Override
public Counters getCounters(GameState state) {
return card.getCounters(state);
}
@Override
public boolean addCounters(Counter counter, Ability source, Game game) {
return card.addCounters(counter, source, game);
}
@Override
public boolean addCounters(Counter counter, UUID playerAddingCounters, Ability source, Game game) {
return card.addCounters(counter, playerAddingCounters, source, game);
}
@Override
public boolean addCounters(Counter counter, UUID playerAddingCounters, Ability source, Game game, boolean isEffect) {
return card.addCounters(counter, playerAddingCounters, source, game, isEffect);
}
@Override
public boolean addCounters(Counter counter, UUID playerAddingCounters, Ability source, Game game, List<UUID> appliedEffects) {
return card.addCounters(counter, playerAddingCounters, source, game, appliedEffects);
}
@Override
public boolean addCounters(Counter counter, UUID playerAddingCounters, Ability source, Game game, List<UUID> appliedEffects, boolean isEffect) {
return card.addCounters(counter, playerAddingCounters, source, game, appliedEffects, isEffect);
}
@Override
public boolean addCounters(Counter counter, UUID playerAddingCounters, Ability source, Game game, List<UUID> appliedEffects, boolean isEffect, int maxCounters) {
return card.addCounters(counter, playerAddingCounters, source, game, appliedEffects, isEffect, maxCounters);
}
@Override
public int removeCounters(String counterName, int amount, Ability source, Game game, boolean isDamage) {
return card.removeCounters(counterName, amount, source, game, isDamage);
}
@Override
public int removeCounters(Counter counter, Ability source, Game game, boolean isDamage) {
return card.removeCounters(counter, source, game, isDamage);
}
@Override
public int removeAllCounters(Ability source, Game game, boolean isDamage) {
return card.removeAllCounters(source, game, isDamage);
}
@Override
public int removeAllCounters(String counterName, Ability source, Game game, boolean isDamage) {
return card.removeAllCounters(counterName, source, game, isDamage);
}
public Card getCard() {
return card;
}
@Override
public Card getMainCard() {
return card.getMainCard();
}
@Override
public FilterMana getColorIdentity() {
return ManaUtil.getColorIdentity(this);
}
@Override
public void setZone(Zone zone, Game game) {
card.setZone(zone, game);
game.getState().setZone(this.getId(), zone);
}
@Override
public void setSpellAbility(SpellAbility ability) {
throw new UnsupportedOperationException("Not supported.");
}
public boolean isCountered() {
return countered;
}
public boolean isResolving() {
return resolving;
}
@Override
public void applyEnterWithCounters(Permanent permanent, Ability source, Game game) {
card.applyEnterWithCounters(permanent, source, game);
}
@Override
public void createSingleCopy(UUID newControllerId, StackObjectCopyApplier applier, MageObjectReferencePredicate newTargetFilterPredicate, Game game, Ability source, boolean chooseNewTargets) {
Spell spellCopy = this.copySpell(game, source, newControllerId);
if (applier != null) {
applier.modifySpell(spellCopy, game);
}
spellCopy.setZone(Zone.STACK, game); // required for targeting ex: Nivmagus Elemental
game.getStack().push(spellCopy);
// new targets
if (newTargetFilterPredicate != null) {
spellCopy.chooseNewTargets(game, newControllerId, true, false, newTargetFilterPredicate);
} else if (chooseNewTargets || applier != null) { // if applier is non-null but predicate is null then it's extra
spellCopy.chooseNewTargets(game, newControllerId);
}
game.fireEvent(new CopiedStackObjectEvent(this, spellCopy, newControllerId));
}
@Override
public boolean canBeCopied() {
return this.getSpellAbility().canBeCopied();
}
@Override
public boolean isAllCreatureTypes(Game game) {
return card.isAllCreatureTypes(game);
}
@Override
public void setIsAllCreatureTypes(boolean value) {
card.setIsAllCreatureTypes(value);
}
@Override
public void setIsAllCreatureTypes(Game game, boolean value) {
card.setIsAllCreatureTypes(game, value);
}
@Override
public boolean isAllNonbasicLandTypes(Game game) {
return card.isAllNonbasicLandTypes(game);
}
@Override
public void setIsAllNonbasicLandTypes(boolean value) {
card.setIsAllNonbasicLandTypes(value);
}
@Override
public void setIsAllNonbasicLandTypes(Game game, boolean value) {
card.setIsAllNonbasicLandTypes(game, value);
}
@Override
public List<UUID> getAttachments() {
throw new UnsupportedOperationException("Not supported.");
}
@Override
public boolean cantBeAttachedBy(MageObject attachment, Ability source, Game game, boolean silentMode) {
throw new UnsupportedOperationException("Not supported.");
}
@Override
public boolean addAttachment(UUID permanentId, Ability source, Game game) {
throw new UnsupportedOperationException("Not supported.");
}
@Override
public boolean removeAttachment(UUID permanentId, Ability source, Game game) {
throw new UnsupportedOperationException("Not supported.");
}
/**
* Add temporary turn controller while resolving (e.g. all choices will be made by another player)
* Example: Word of Command
*
* @param newTurnControllerId
* @param info additional info for game logs
*/
public void setCommandedBy(UUID newTurnControllerId, String info) {
this.commandedByPlayerId = newTurnControllerId;
this.commandedByInfo = info;
}
public UUID getCommandedByPlayerId() {
return commandedByPlayerId;
}
public String getCommandedByInfo() {
return commandedByInfo == null ? "" : commandedByInfo;
}
@Override
public void looseAllAbilities(Game game) {
throw new UnsupportedOperationException("Spells should not loose all abilities. Check if this operation is correct.");
}
@Override
public String toString() {
return ability.toString();
}
@Override
public List<CardType> getCardTypeForDeckbuilding() {
throw new UnsupportedOperationException("Must call for cards only.");
}
@Override
public boolean hasCardTypeForDeckbuilding(CardType cardType) {
return false;
}
@Override
public boolean hasSubTypeForDeckbuilding(SubType subType) {
return false;
}
}