* 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

@ -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++;
}