mirror of
https://github.com/magefree/mage.git
synced 2025-12-26 21:42:07 -08:00
* 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:
parent
8704b9cb9b
commit
b36f915d74
22 changed files with 537 additions and 179 deletions
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<>();
|
||||
|
|
|
|||
|
|
@ -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++;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue