* Copy spell - improved support, now all copied spells are independent (bug example: Seasons Past fizzled after copy resolve, see #7634, 10f8022043);

This commit is contained in:
Oleg Agafonov 2021-03-06 12:25:53 +04:00
parent 8704b9cb9b
commit b36f915d74
22 changed files with 537 additions and 179 deletions

View file

@ -88,6 +88,8 @@ public interface Ability extends Controllable, Serializable {
/**
* Gets the id of the object which put this ability in motion.
*
* WARNING, MageSingleton abilities contains dirty data here, so you can't use sourceId with it
*
* @return The {@link java.util.UUID} of the object this ability is
* associated with.
*/

View file

@ -13,11 +13,9 @@ import mage.constants.*;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.players.Player;
import mage.util.CardUtil;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.*;
/**
* @author BetaSteward_at_googlemail.com
@ -176,9 +174,17 @@ public class SpellAbility extends ActivatedAbilityImpl {
return new SpellAbility(this);
}
public SpellAbility copySpell() {
public SpellAbility copySpell(Card originalCard, Card copiedCard) {
// all copied spells must have own copied card
Map<UUID, MageObject> mapOldToNew = CardUtil.getOriginalToCopiedPartsMap(originalCard, copiedCard);
if (!mapOldToNew.containsKey(this.getSourceId())) {
throw new IllegalStateException("Can't find source id after copy: " + originalCard.getName() + " -> " + copiedCard.getName());
}
UUID copiedSourceId = mapOldToNew.getOrDefault(this.getSourceId(), copiedCard).getId();
SpellAbility spell = new SpellAbility(this);
spell.id = UUID.randomUUID();
spell.newId();
spell.setSourceId(copiedSourceId);
return spell;
}

View file

@ -6,6 +6,8 @@ import mage.game.Game;
import mage.game.stack.Spell;
/**
* Addendum If you cast this spell during your main phase, you get some boost
*
* @author LevelX2
*/
@ -23,6 +25,6 @@ public enum AddendumCondition implements Condition {
return true;
}
Spell spell = game.getSpell(source.getSourceId());
return spell != null && !spell.isCopy();
return spell != null && !spell.isCopy(); // copies are not casted
}
}

View file

@ -80,8 +80,9 @@ public abstract class CopySpellForEachItCouldTargetEffect<T extends MageItem> ex
return false;
}
// generate copies for each possible target, but do not put it to stack (use must choose targets in custom order later)
Spell copy = spell.copySpell(source.getControllerId(), game);
// TODO: add support if multiple copies? See Twinning Staff
// generate copies for each possible target, but do not put it to stack (must choose targets in custom order later)
Spell copy = spell.copySpell(game, source, source.getControllerId());
modifyCopy(copy, game, source);
Target sampleTarget = targetsToBeChanged.iterator().next().getTarget(copy);
sampleTarget.setNotTarget(true);
@ -93,7 +94,8 @@ public abstract class CopySpellForEachItCouldTargetEffect<T extends MageItem> ex
obj = game.getPlayer(objId);
}
if (obj != null) {
copy = spell.copySpell(source.getControllerId(), game);
// TODO: add support if multiple copies? See Twinning Staff
copy = spell.copySpell(game, source, source.getControllerId());
try {
modifyCopy(copy, (T) obj, game, source);
if (!filter.match((T) obj, source.getSourceId(), actingPlayer.getId(), game)) {
@ -168,6 +170,8 @@ public abstract class CopySpellForEachItCouldTargetEffect<T extends MageItem> ex
for (UUID chosenId : chosenIds) {
Spell chosenCopy = targetCopyMap.get(chosenId);
if (chosenCopy != null) {
// COPY DONE, can put to stack
chosenCopy.setZone(Zone.STACK, game);
game.getStack().push(chosenCopy);
game.fireEvent(new CopiedStackObjectEvent(spell, chosenCopy, source.getControllerId()));
toDelete.add(chosenId);

View file

@ -55,7 +55,7 @@ public class EpicEffect extends OneShotEffect {
if (spell == null) {
return false;
}
spell = spell.copySpell(source.getControllerId(), game);
spell = spell.copySpell(game, source, source.getControllerId()); // it's a fake copy, real copy with events in EpicPushEffect
// Remove Epic effect from the spell
Effect epicEffect = null;
for (Effect effect : spell.getSpellAbility().getEffects()) {

View file

@ -1,6 +1,5 @@
package mage.abilities.effects.common;
import java.util.UUID;
import mage.abilities.Ability;
import mage.abilities.MageSingleton;
import mage.abilities.effects.AsThoughEffectImpl;
@ -18,6 +17,8 @@ import mage.players.Player;
import mage.target.targetpointer.FixedTarget;
import mage.util.CardUtil;
import java.util.UUID;
/**
* @author phulin
*/
@ -48,7 +49,7 @@ public class ExileAdventureSpellEffect extends OneShotEffect implements MageSing
Player controller = game.getPlayer(source.getControllerId());
if (controller != null) {
Spell spell = game.getStack().getSpell(source.getId());
if (spell != null && !spell.isCopy()) {
if (spell != null) {
Card spellCard = spell.getCard();
if (spellCard instanceof AdventureCardSpell) {
UUID exileId = adventureExileId(controller.getId(), game);

View file

@ -36,7 +36,7 @@ public class ExileSpellEffect extends OneShotEffect implements MageSingleton {
Player controller = game.getPlayer(source.getControllerId());
if (controller != null) {
Spell spell = game.getStack().getSpell(source.getId());
if (spell != null && !spell.isCopy()) {
if (spell != null) {
Card spellCard = spell.getCard();
if (spellCard != null) {
controller.moveCards(spellCard, Zone.EXILED, source, game);

View file

@ -11,7 +11,6 @@ import mage.game.stack.Spell;
import mage.players.Player;
/**
*
* @author LevelX2
*/
public class ReturnToLibrarySpellEffect extends OneShotEffect {
@ -20,7 +19,7 @@ public class ReturnToLibrarySpellEffect extends OneShotEffect {
public ReturnToLibrarySpellEffect(boolean top) {
super(Outcome.Neutral);
staticText = "Put {this} on "+ (top ? "top":"the bottom") + " of its owner's library";
staticText = "Put {this} on " + (top ? "top" : "the bottom") + " of its owner's library";
this.toTop = top;
}
@ -34,7 +33,7 @@ public class ReturnToLibrarySpellEffect extends OneShotEffect {
Player controller = game.getPlayer(source.getControllerId());
if (controller != null) {
Spell spell = game.getStack().getSpell(source.getSourceId());
if (spell != null && !spell.isCopy()) {
if (spell != null) {
Card spellCard = spell.getCard();
if (spellCard != null) {
controller.moveCardToLibraryWithInfo(spellCard, source, game, Zone.STACK, toTop, true);

View file

@ -1,4 +1,3 @@
package mage.abilities.effects.common;
import mage.abilities.Ability;
@ -11,7 +10,6 @@ import mage.game.stack.Spell;
import mage.players.Player;
/**
*
* @author nantuko
*/
public class ShuffleSpellEffect extends OneShotEffect implements MageSingleton {
@ -34,7 +32,7 @@ public class ShuffleSpellEffect extends OneShotEffect implements MageSingleton {
// We have to use the spell id because in case of copied spells, the sourceId can be multiple times on the stack
Spell spell = game.getStack().getSpell(source.getId());
if (spell != null) {
if (controller.moveCards(spell, Zone.LIBRARY, source, game) && !spell.isCopy()) {
if (controller.moveCards(spell, Zone.LIBRARY, source, game)) {
Player owner = game.getPlayer(spell.getCard().getOwnerId());
if (owner != null) {
owner.shuffleLibrary(source, game);

View file

@ -18,7 +18,7 @@ import java.util.UUID;
public abstract class AdventureCard extends CardImpl {
/* The adventure spell card, i.e. Swift End. */
protected Card spellCard;
protected AdventureCardSpell spellCard;
public AdventureCard(UUID ownerId, CardSetInfo setInfo, CardType[] types, CardType[] typesSpell, String costs, String adventureName, String costsSpell) {
super(ownerId, setInfo, types, costs);
@ -28,13 +28,19 @@ public abstract class AdventureCard extends CardImpl {
public AdventureCard(AdventureCard card) {
super(card);
this.spellCard = card.getSpellCard().copy();
((AdventureCardSpell) this.spellCard).setParentCard(this);
this.spellCard.setParentCard(this);
}
public Card getSpellCard() {
public AdventureCardSpell getSpellCard() {
return spellCard;
}
public void setParts(AdventureCardSpell cardSpell) {
// for card copy only - set new parts
this.spellCard = cardSpell;
cardSpell.setParentCard(this);
}
@Override
public void assignNewId() {
super.assignNewId();

View file

@ -55,6 +55,14 @@ public abstract class ModalDoubleFacesCard extends CardImpl {
return (ModalDoubleFacesCardHalf) rightHalfCard;
}
public void setParts(ModalDoubleFacesCardHalf leftHalfCard, ModalDoubleFacesCardHalf 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();

View file

@ -42,6 +42,14 @@ public abstract class SplitCard extends CardImpl {
((SplitCardHalf) rightHalfCard).setParentCard(this);
}
public void setParts(SplitCardHalf leftHalfCard, SplitCardHalf rightHalfCard) {
// for card copy only - set new parts
this.leftHalfCard = leftHalfCard;
leftHalfCard.setParentCard(this);
this.rightHalfCard = rightHalfCard;
rightHalfCard.setParentCard(this);
}
public SplitCardHalf getLeftHalfCard() {
return (SplitCardHalf) leftHalfCard;
}

View file

@ -74,6 +74,7 @@ import java.io.IOException;
import java.io.Serializable;
import java.util.*;
import java.util.Map.Entry;
import java.util.stream.Collectors;
public abstract class GameImpl implements Game, Serializable {
@ -552,7 +553,7 @@ public abstract class GameImpl implements Game, Serializable {
// copied cards removes, but delayed triggered possible from it, see https://github.com/magefree/mage/issues/5437
// TODO: remove that workround after LKI rework, see GameState.copyCard
if (card == null) {
card = (Card) state.getValue(GameState.COPIED_FROM_CARD_KEY + cardId.toString());
card = (Card) state.getValue(GameState.COPIED_CARD_KEY + cardId.toString());
}
return card;
}
@ -1421,7 +1422,7 @@ public abstract class GameImpl implements Game, Serializable {
errorContinueCounter++;
continue;
} else {
throw new MageException("Error in testclass");
throw new MageException("Error in unit tests");
}
} finally {
setCheckPlayableState(false);
@ -1734,7 +1735,7 @@ public abstract class GameImpl implements Game, Serializable {
@Override
public Card copyCard(Card cardToCopy, Ability source, UUID newController) {
return state.copyCard(cardToCopy, source, this);
return state.copyCard(cardToCopy, newController, this);
}
/**
@ -1947,48 +1948,67 @@ public abstract class GameImpl implements Game, Serializable {
somethingHappened = true;
}
// 704.5e 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.
// (Isochron Scepter) 12/1/2004: If you don't want to cast the copy, you can choose not to; the copy ceases to exist the next time state-based actions are checked.
Iterator<Card> copiedCards = this.getState().getCopiedCards().iterator();
while (copiedCards.hasNext()) {
Card card = copiedCards.next();
if (card instanceof SplitCardHalf || card instanceof AdventureCardSpell || card instanceof ModalDoubleFacesCardHalf) {
continue; // only the main card is moves, not the halves (cause halfes is not copied - it uses original card -- TODO: need to fix (bugs with same card copy)?
// 704.5e
// 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.
// (Isochron Scepter) 12/1/2004: If you don't want to cast the copy, you can choose not to; the copy ceases
// to exist the next time state-based actions are checked.
//
// Copied cards can be stored in GameState.copiedCards or in game state value (until LKI rework)
Set<Card> allCopiedCards = new HashSet<>();
allCopiedCards.addAll(this.getState().getCopiedCards());
Map<String, Object> stateSavedCopiedCards = this.getState().getValues(GameState.COPIED_CARD_KEY);
allCopiedCards.addAll(stateSavedCopiedCards.values()
.stream()
.map(object -> (Card) object)
.filter(Objects::nonNull)
.collect(Collectors.toList())
);
for (Card copiedCard : allCopiedCards) {
// 1. Zone must be checked from main card only cause mdf parts can have different zones
// (one side on battlefield, another side on outsize)
// 2. Copied card creates in OUTSIDE zone and put to stack manually in the same code,
// so no SBA calls before real zone change (you will see here only unused cards like Isochron Scepter)
// (Isochron Scepter) 12/1/2004: If you don't want to cast the copy, you can choose not to; the copy ceases
// to exist the next time state-based actions are checked.
Zone zone = state.getZone(copiedCard.getMainCard().getId());
if (zone == Zone.BATTLEFIELD || zone == Zone.STACK) {
continue;
}
Zone zone = state.getZone(card.getId());
if (zone != Zone.BATTLEFIELD && zone != Zone.STACK) {
// TODO: remember LKI of copied cards here after LKI rework
switch (zone) {
case GRAVEYARD:
for (Player player : getPlayers().values()) {
if (player.getGraveyard().contains(card.getId())) {
player.getGraveyard().remove(card);
break;
}
// TODO: remember LKI of copied cards here after LKI rework
switch (zone) {
case GRAVEYARD:
for (Player player : getPlayers().values()) {
if (player.getGraveyard().contains(copiedCard.getId())) {
player.getGraveyard().remove(copiedCard);
break;
}
break;
case HAND:
for (Player player : getPlayers().values()) {
if (player.getHand().contains(card.getId())) {
player.getHand().remove(card);
break;
}
}
break;
case HAND:
for (Player player : getPlayers().values()) {
if (player.getHand().contains(copiedCard.getId())) {
player.getHand().remove(copiedCard);
break;
}
break;
case LIBRARY:
for (Player player : getPlayers().values()) {
if (player.getLibrary().getCard(card.getId(), this) != null) {
player.getLibrary().remove(card.getId(), this);
break;
}
}
break;
case LIBRARY:
for (Player player : getPlayers().values()) {
if (player.getLibrary().getCard(copiedCard.getId(), this) != null) {
player.getLibrary().remove(copiedCard.getId(), this);
break;
}
break;
case EXILED:
getExile().removeCard(card, this);
break;
}
copiedCards.remove();
}
break;
case EXILED:
getExile().removeCard(copiedCard, this);
break;
}
// remove copied card info
this.getState().getCopiedCards().remove(copiedCard);
this.getState().removeValue(GameState.COPIED_CARD_KEY + copiedCard.getId().toString());
}
List<Permanent> legendary = new ArrayList<>();

View file

@ -6,10 +6,7 @@ import mage.abilities.*;
import mage.abilities.effects.ContinuousEffect;
import mage.abilities.effects.ContinuousEffects;
import mage.abilities.effects.Effect;
import mage.cards.AdventureCard;
import mage.cards.Card;
import mage.cards.ModalDoubleFacesCard;
import mage.cards.SplitCard;
import mage.cards.*;
import mage.constants.Zone;
import mage.designations.Designation;
import mage.filter.common.FilterCreaturePermanent;
@ -56,7 +53,9 @@ public class GameState implements Serializable, Copyable<GameState> {
private static final Logger logger = Logger.getLogger(GameState.class);
private static final ThreadLocalStringBuilder threadLocalBuilder = new ThreadLocalStringBuilder(1024);
public static final String COPIED_FROM_CARD_KEY = "CopiedFromCard";
// save copied cards between game cycles (lki workaround)
// warning, do not use another keys with same starting text cause copy code search and clean all related values
public static final String COPIED_CARD_KEY = "CopiedCard";
private final Players players;
private final PlayerList playerList;
@ -845,7 +844,12 @@ public class GameState implements Serializable, Copyable<GameState> {
}
public void addCard(Card card) {
setZone(card.getId(), Zone.OUTSIDE);
// all new cards and tokens must enter from outside
addCard(card, Zone.OUTSIDE);
}
private void addCard(Card card, Zone zone) {
setZone(card.getId(), zone);
// add card specific abilities to game
for (Ability ability : card.getInitAbilities()) {
@ -853,28 +857,6 @@ public class GameState implements Serializable, Copyable<GameState> {
}
}
public void removeCopiedCard(Card card) {
if (copiedCards.containsKey(card.getId())) {
copiedCards.remove(card.getId());
cardState.remove(card.getId());
zones.remove(card.getId());
zoneChangeCounter.remove(card.getId());
}
// TODO Watchers?
// TODO Abilities?
if (card instanceof SplitCard) {
removeCopiedCard(((SplitCard) card).getLeftHalfCard());
removeCopiedCard(((SplitCard) card).getRightHalfCard());
}
if (card instanceof ModalDoubleFacesCard) {
removeCopiedCard(((ModalDoubleFacesCard) card).getLeftHalfCard());
removeCopiedCard(((ModalDoubleFacesCard) card).getRightHalfCard());
}
if (card instanceof AdventureCard) {
removeCopiedCard(((AdventureCard) card).getSpellCard());
}
}
/**
* Used for adding abilities that exist permanent on cards/permanents and
* are not only gained for a certain time (e.g. until end of turn).
@ -1021,6 +1003,27 @@ public class GameState implements Serializable, Copyable<GameState> {
return values.get(valueId);
}
/**
* Return values list starting with searching key.
* <p>
* Usage example: if you want to find all saved values from related ability/effect
*
* @param startWithValue
* @return
*/
public Map<String, Object> getValues(String startWithValue) {
if (startWithValue == null || startWithValue.isEmpty()) {
throw new IllegalArgumentException("Can't use empty search value");
}
Map<String, Object> res = new HashMap<>();
for (Map.Entry<String, Object> entry : this.values.entrySet()) {
if (entry.getKey().startsWith(startWithValue)) {
res.put(entry.getKey(), entry.getValue());
}
}
return res;
}
/**
* Best only use immutable objects, otherwise the states/values of the
* object may be changed by AI simulation or rollbacks, because the Value
@ -1035,6 +1038,15 @@ public class GameState implements Serializable, Copyable<GameState> {
values.put(valueId, value);
}
/**
* Remove saved value
*
* @param valueId
*/
public void removeValue(String valueId) {
values.remove(valueId);
}
/**
* Other abilities are used to implement some special kind of continuous
* effects that give abilities to non permanents.
@ -1251,47 +1263,92 @@ public class GameState implements Serializable, Copyable<GameState> {
return copiedCards.values();
}
public Card copyCard(Card cardToCopy, Ability source, Game game) {
// main card
Card copiedCard = cardToCopy.copy();
copiedCard.assignNewId();
copiedCard.setOwnerId(source.getControllerId());
copiedCard.setCopy(true, cardToCopy);
copiedCards.put(copiedCard.getId(), copiedCard);
addCard(copiedCard);
/**
* Make full copy of the card and all of the card's parts and put to the game.
*
* @param mainCardToCopy
* @param newController
* @param game
* @return
*/
public Card copyCard(Card mainCardToCopy, UUID newController, Game game) {
// runtime check
if (!mainCardToCopy.getId().equals(mainCardToCopy.getMainCard().getId())) {
// copyCard allows for main card only, if you catch it then check your targeting code
throw new IllegalArgumentException("Wrong code usage. You can copy only main card.");
}
// other faces
// must copy all card's parts
// zcc and zone must be new cause zcc copy logic need card usage info here, but it haven't:
// * reason 1: copied land must be played (+1 zcc), but copied spell must be put on stack and cast (+2 zcc)
// * reason 2: copied card or spell can be used later as blueprint for real copies (see Epic ability)
List<Card> copiedParts = new ArrayList<>();
// main part (prepare must be called after other parts)
Card copiedCard = mainCardToCopy.copy();
copiedParts.add(copiedCard);
// other parts
if (copiedCard instanceof SplitCard) {
// left
Card leftCard = ((SplitCard) copiedCard).getLeftHalfCard(); // TODO: must be new ID (bugs with same card copy)?
copiedCards.put(leftCard.getId(), leftCard);
addCard(leftCard);
SplitCardHalf leftOriginal = ((SplitCard) copiedCard).getLeftHalfCard();
SplitCardHalf leftCopied = leftOriginal.copy();
prepareCardForCopy(leftOriginal, leftCopied, newController);
copiedParts.add(leftCopied);
// right
Card rightCard = ((SplitCard) copiedCard).getRightHalfCard();
copiedCards.put(rightCard.getId(), rightCard);
addCard(rightCard);
SplitCardHalf rightOriginal = ((SplitCard) copiedCard).getRightHalfCard();
SplitCardHalf rightCopied = rightOriginal.copy();
prepareCardForCopy(rightOriginal, rightCopied, newController);
copiedParts.add(rightCopied);
// sync parts
((SplitCard) copiedCard).setParts(leftCopied, rightCopied);
} else if (copiedCard instanceof ModalDoubleFacesCard) {
// left
Card leftCard = ((ModalDoubleFacesCard) copiedCard).getLeftHalfCard(); // TODO: must be new ID (bugs with same card copy)?
copiedCards.put(leftCard.getId(), leftCard);
addCard(leftCard);
ModalDoubleFacesCardHalf leftOriginal = ((ModalDoubleFacesCard) copiedCard).getLeftHalfCard();
ModalDoubleFacesCardHalf leftCopied = leftOriginal.copy();
prepareCardForCopy(leftOriginal, leftCopied, newController);
copiedParts.add(leftCopied);
// right
Card rightCard = ((ModalDoubleFacesCard) copiedCard).getRightHalfCard();
copiedCards.put(rightCard.getId(), rightCard);
addCard(rightCard);
ModalDoubleFacesCardHalf rightOriginal = ((ModalDoubleFacesCard) copiedCard).getRightHalfCard();
ModalDoubleFacesCardHalf rightCopied = rightOriginal.copy();
prepareCardForCopy(rightOriginal, rightCopied, newController);
copiedParts.add(rightCopied);
// sync parts
((ModalDoubleFacesCard) copiedCard).setParts(leftCopied, rightCopied);
} else if (copiedCard instanceof AdventureCard) {
Card spellCard = ((AdventureCard) copiedCard).getSpellCard();
copiedCards.put(spellCard.getId(), spellCard);
addCard(spellCard);
// right
AdventureCardSpell rightOriginal = ((AdventureCard) copiedCard).getSpellCard();
AdventureCardSpell rightCopied = rightOriginal.copy();
prepareCardForCopy(rightOriginal, rightCopied, newController);
copiedParts.add(rightCopied);
// sync parts
((AdventureCard) copiedCard).setParts(rightCopied);
}
// main part prepare (must be called after other parts cause it change ids for all)
prepareCardForCopy(mainCardToCopy, copiedCard, newController);
// add all parts to the game
copiedParts.forEach(card -> {
copiedCards.put(card.getId(), card);
addCard(card);
});
// copied cards removes from game after battlefield/stack leaves, so remember it here as workaround to fix freeze, see https://github.com/magefree/mage/issues/5437
// TODO: remove that workaround after LKI will be rewritten to support cross-steps/turns data transition and support copied cards
this.setValue(COPIED_FROM_CARD_KEY + copiedCard.getId(), cardToCopy.copy());
copiedParts.forEach(card -> {
this.setValue(COPIED_CARD_KEY + card.getId(), card.copy());
});
return copiedCard;
}
private void prepareCardForCopy(Card originalCard, Card copiedCard, UUID newController) {
copiedCard.assignNewId();
copiedCard.setOwnerId(newController);
copiedCard.setCopy(true, originalCard);
}
public int getNextPermanentOrderNumber() {
return permanentOrderNumber++;
}

View file

@ -766,8 +766,31 @@ public class Spell extends StackObjImpl implements Card {
return new Spell(this);
}
public Spell copySpell(UUID newController, Game game) {
Spell spellCopy = new Spell(this.card, this.ability.copySpell(), this.controllerId, this.fromZone, game);
/**
* 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);
boolean firstDone = false;
for (SpellAbility spellAbility : this.getSpellAbilities()) {
if (!firstDone) {
@ -939,6 +962,12 @@ public class Spell extends StackObjImpl implements Card {
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 skip unaffected code/choices
*
* @return
*/
@Override
public boolean isCopy() {
return this.copy;
@ -1006,6 +1035,7 @@ public class Spell extends StackObjImpl implements Card {
@Override
public void setZone(Zone zone, Game game) {
card.setZone(zone, game);
game.getState().setZone(this.getId(), Zone.STACK);
}
@Override
@ -1039,8 +1069,8 @@ public class Spell extends StackObjImpl implements Card {
return null;
}
for (int i = 0; i < gameEvent.getAmount(); i++) {
spellCopy = this.copySpell(newControllerId, game);
game.getState().setZone(spellCopy.getId(), Zone.STACK); // required for targeting ex: Nivmagus Elemental
spellCopy = this.copySpell(game, source, newControllerId);
spellCopy.setZone(Zone.STACK, game); // required for targeting ex: Nivmagus Elemental
game.getStack().push(spellCopy);
if (chooseNewTargets) {
spellCopy.chooseNewTargets(game, newControllerId);

View file

@ -1228,39 +1228,66 @@ public final class CardUtil {
* @return
*/
public static Set<UUID> getObjectParts(MageObject object) {
Set<UUID> res = new HashSet<>();
Set<UUID> res = new LinkedHashSet<>(); // set must be ordered
List<MageObject> allParts = getObjectPartsAsObjects(object);
allParts.forEach(part -> {
res.add(part.getId());
});
return res;
}
public static List<MageObject> getObjectPartsAsObjects(MageObject object) {
List<MageObject> res = new ArrayList<>();
if (object == null) {
return res;
}
if (object instanceof SplitCard || object instanceof SplitCardHalf) {
SplitCard mainCard = (SplitCard) ((Card) object).getMainCard();
res.add(object.getId());
res.add(mainCard.getId());
res.add(mainCard.getLeftHalfCard().getId());
res.add(mainCard.getRightHalfCard().getId());
res.add(mainCard);
res.add(mainCard.getLeftHalfCard());
res.add(mainCard.getRightHalfCard());
} else if (object instanceof ModalDoubleFacesCard || object instanceof ModalDoubleFacesCardHalf) {
ModalDoubleFacesCard mainCard = (ModalDoubleFacesCard) ((Card) object).getMainCard();
res.add(object.getId());
res.add(mainCard.getId());
res.add(mainCard.getLeftHalfCard().getId());
res.add(mainCard.getRightHalfCard().getId());
res.add(mainCard);
res.add(mainCard.getLeftHalfCard());
res.add(mainCard.getRightHalfCard());
} else if (object instanceof AdventureCard || object instanceof AdventureCardSpell) {
AdventureCard mainCard = (AdventureCard) ((Card) object).getMainCard();
res.add(object.getId());
res.add(mainCard.getId());
res.add(mainCard.getSpellCard().getId());
res.add(mainCard);
res.add(mainCard.getSpellCard());
} else if (object instanceof Spell) {
// example: activate Lightning Storm's ability from the spell on the stack
res.add(object.getId());
res.addAll(getObjectParts(((Spell) object).getCard()));
res.add(object);
res.addAll(getObjectPartsAsObjects(((Spell) object).getCard()));
} else if (object instanceof Commander) {
// commander can contains double sides
res.add(object.getId());
res.addAll(getObjectParts(((Commander) object).getSourceObject()));
res.add(object);
res.addAll(getObjectPartsAsObjects(((Commander) object).getSourceObject()));
} else {
res.add(object.getId());
res.add(object);
}
return res;
}
/**
* Find mapping from original to copied card (e.g. map left side with copied left side, etc)
*
* @param originalCard
* @param copiedCard
* @return
*/
public static Map<UUID, MageObject> getOriginalToCopiedPartsMap(Card originalCard, Card copiedCard) {
List<UUID> oldIds = new ArrayList<>(CardUtil.getObjectParts(originalCard));
List<MageObject> newObjects = new ArrayList<>(CardUtil.getObjectPartsAsObjects(copiedCard));
if (oldIds.size() != newObjects.size()) {
throw new IllegalStateException("Found wrong card parts after copy: " + originalCard.getName() + " -> " + copiedCard.getName());
}
Map<UUID, MageObject> mapOldToNew = new HashMap<>();
for (int i = 0; i < oldIds.size(); i++) {
mapOldToNew.put(oldIds.get(i), newObjects.get(i));
}
return mapOldToNew;
}
}