package mage.game; import mage.abilities.Ability; import mage.abilities.keyword.TransformAbility; import mage.cards.*; import mage.constants.Outcome; import mage.constants.Zone; import mage.filter.FilterCard; import mage.game.command.Commander; import mage.game.events.ZoneChangeEvent; import mage.game.events.ZoneChangeGroupEvent; import mage.game.permanent.Permanent; import mage.game.permanent.PermanentCard; import mage.game.permanent.PermanentMeld; import mage.game.permanent.PermanentToken; import mage.game.stack.Spell; import mage.players.Player; import mage.target.TargetCard; import java.util.*; /** * Created by samuelsandeen on 9/6/16. */ public final class ZonesHandler { public static boolean cast(ZoneChangeInfo info, Ability source, Game game) { if (maybeRemoveFromSourceZone(info, game, source)) { placeInDestinationZone(info, 0, source, game); // create a group zone change event if a card is moved to stack for casting (it's always only one card, but some effects check for group events (one or more xxx)) Set cards = new HashSet<>(); Set tokens = new HashSet<>(); Card targetCard = getTargetCard(game, info.event.getTargetId()); if (targetCard instanceof PermanentToken) { tokens.add((PermanentToken) targetCard); } else { cards.add(targetCard); } game.fireEvent(new ZoneChangeGroupEvent( cards, tokens, info.event.getSourceId(), info.event.getSource(), info.event.getPlayerId(), info.event.getFromZone(), info.event.getToZone())); // normal movement game.fireEvent(info.event); return true; } return false; } public static boolean moveCard(ZoneChangeInfo info, Game game, Ability source) { List list = new ArrayList<>(); list.add(info); return !moveCards(list, source, game).isEmpty(); } public static List moveCards(List zoneChangeInfos, Ability source, Game game) { // handle unmelded meld cards (if something moved a melded card to non-battlefield then parts must be moved too) for (ListIterator itr = zoneChangeInfos.listIterator(); itr.hasNext(); ) { ZoneChangeInfo info = itr.next(); if (info.event.getToZone().equals(Zone.BATTLEFIELD)) { continue; } MeldCard card = game.getMeldCard(info.event.getTargetId()); // Copies should be handled as normal cards. if (card != null && !card.isMelded(game) && !card.isCopy()) { // TODO: WTF, never worked code here. Need research possible typo: !card.isMelded(game) -> card.isMelded(game) ZoneChangeInfo.Unmelded unmelded = new ZoneChangeInfo.Unmelded(info, game); if (unmelded.additionalMoves.isEmpty()) { // already moved halves somehow itr.remove(); } else { itr.set(unmelded); } } } // process Modal Double Faces cards (e.g. put card from hand) // rules: // If an effect puts a double-faced card onto the battlefield, it enters with its front face up. // If that front face can’t be put onto the battlefield, it doesn’t enter the battlefield. // For example, if an effect exiles Sejiri Glacier and returns it to the battlefield, // it remains in exile because an instant can’t be put onto the battlefield. for (ListIterator itr = zoneChangeInfos.listIterator(); itr.hasNext(); ) { ZoneChangeInfo info = itr.next(); if (info.event.getToZone().equals(Zone.BATTLEFIELD)) { Card card = game.getCard(info.event.getTargetId()); if (card instanceof ModalDoubleFacedCard || card instanceof ModalDoubleFacedCardHalf) { boolean forceToMainSide = false; // if effect put half mdf card to battlefield then it must be the main side only (example: return targeted half card to battle) if (card instanceof ModalDoubleFacedCardHalf && !source.getAbilityType().isPlayCardAbility()) { forceToMainSide = true; } // if effect put mdf card to battlefield then it must be main side only if (card instanceof ModalDoubleFacedCard) { forceToMainSide = true; } if (forceToMainSide) { info.event.setTargetId(((ModalDoubleFacedCard) card.getMainCard()).getLeftHalfCard().getId()); } } } } zoneChangeInfos.removeIf(zoneChangeInfo -> !maybeRemoveFromSourceZone(zoneChangeInfo, game, source)); int createOrder = 0; for (ZoneChangeInfo zoneChangeInfo : zoneChangeInfos) { if (createOrder == 0 && Zone.BATTLEFIELD.equals(zoneChangeInfo.event.getToZone())) { // All permanents go to battlefield at the same time (=create order) createOrder = game.getState().getNextPermanentOrderNumber(); } placeInDestinationZone(zoneChangeInfo, createOrder, source, game); if (game.getPhase() != null) { // moving cards to zones before game started does not need events game.addSimultaneousEvent(zoneChangeInfo.event); } } return zoneChangeInfos; } private static void placeInDestinationZone(ZoneChangeInfo info, int createOrder, Ability source, Game game) { // Handle unmelded cards if (info instanceof ZoneChangeInfo.Unmelded) { ZoneChangeInfo.Unmelded unmelded = (ZoneChangeInfo.Unmelded) info; Zone toZone = null; for (ZoneChangeInfo additionalMove : unmelded.additionalMoves) { toZone = additionalMove.event.getToZone(); placeInDestinationZone(additionalMove, createOrder, source, game); } // We arbitrarily prefer the bottom half card. This should never be relevant. if (toZone != null) { game.setZone(unmelded.event.getTargetId(), toZone); } return; } // Handle normal cases ZoneChangeEvent event = info.event; Zone toZone = event.getToZone(); Card targetCard = getTargetCard(game, event.getTargetId()); Cards cardsToMove = null; // moving real cards Map cardsToUpdate = new LinkedHashMap<>(); // updating all card's parts (must be ordered LinkedHashMap) cardsToUpdate.put(toZone, new CardsImpl()); cardsToUpdate.put(Zone.OUTSIDE, new CardsImpl()); // if we're moving a token it shouldn't be put into any zone as an object. if (!(targetCard instanceof Permanent) && targetCard != null) { if (targetCard instanceof MeldCard) { // meld/group cards must be independent (use can choose order) cardsToMove = ((MeldCard) targetCard).getHalves(); cardsToUpdate.get(toZone).addAll(cardsToMove); } else if (targetCard instanceof ModalDoubleFacedCard || targetCard instanceof ModalDoubleFacedCardHalf) { // mdf cards must be moved as single object, but each half must be updated separately ModalDoubleFacedCard mdfCard = (ModalDoubleFacedCard) targetCard.getMainCard(); cardsToMove = new CardsImpl(mdfCard); cardsToUpdate.get(toZone).add(mdfCard); // example: cast left side // result: // * main to battlefield // * left to battlefield // * right to outside (it helps to ignore all triggers and other effects from that card) switch (toZone) { case STACK: case BATTLEFIELD: if (targetCard.getId().equals(mdfCard.getLeftHalfCard().getId())) { // play left cardsToUpdate.get(toZone).add(mdfCard.getLeftHalfCard()); cardsToUpdate.get(Zone.OUTSIDE).add(mdfCard.getRightHalfCard()); } else if (targetCard.getId().equals(mdfCard.getRightHalfCard().getId())) { // play right cardsToUpdate.get(toZone).add(mdfCard.getRightHalfCard()); cardsToUpdate.get(Zone.OUTSIDE).add(mdfCard.getLeftHalfCard()); } else { // cast mdf (only on stack) if (!toZone.equals(Zone.STACK)) { throw new IllegalStateException("Wrong mdf card move to " + toZone + " in placeInDestinationZone"); } cardsToUpdate.get(toZone).add(mdfCard.getLeftHalfCard()); cardsToUpdate.get(toZone).add(mdfCard.getRightHalfCard()); } break; default: // move all parts cardsToUpdate.get(toZone).add(mdfCard.getLeftHalfCard()); cardsToUpdate.get(toZone).add(mdfCard.getRightHalfCard()); break; } } else { cardsToMove = new CardsImpl(targetCard); cardsToUpdate.get(toZone).addAll(cardsToMove); } Player owner = game.getPlayer(targetCard.getOwnerId()); switch (toZone) { case HAND: for (Card card : cardsToMove.getCards(game)) { game.getPlayer(card.getOwnerId()).getHand().add(card); } break; case GRAVEYARD: for (Card card : chooseOrder( "order to put in graveyard (last chosen will be on top)", cardsToMove, owner, source, game)) { game.getPlayer(card.getOwnerId()).getGraveyard().add(card); } break; case LIBRARY: if (info instanceof ZoneChangeInfo.Library && ((ZoneChangeInfo.Library) info).top) { // on top for (Card card : chooseOrder( "order to put on top of library (last chosen will be topmost)", cardsToMove, owner, source, game)) { game.getPlayer(card.getOwnerId()).getLibrary().putOnTop(card, game); } } else { // on bottom for (Card card : chooseOrder( "order to put on bottom of library (last chosen will be bottommost)", cardsToMove, owner, source, game)) { game.getPlayer(card.getOwnerId()).getLibrary().putOnBottom(card, game); } } break; case EXILED: for (Card card : cardsToMove.getCards(game)) { if (info instanceof ZoneChangeInfo.Exile && ((ZoneChangeInfo.Exile) info).id != null) { ZoneChangeInfo.Exile exileInfo = (ZoneChangeInfo.Exile) info; game.getExile().createZone(exileInfo.id, exileInfo.name).add(card); } else { game.getExile().getPermanentExile().add(card); } } break; case COMMAND: // There should never be more than one card here. for (Card card : cardsToMove.getCards(game)) { game.addCommander(new Commander(card)); } break; case STACK: // There should never be more than one card here. for (Card card : cardsToMove.getCards(game)) { Spell spell; if (info instanceof ZoneChangeInfo.Stack && ((ZoneChangeInfo.Stack) info).spell != null) { spell = ((ZoneChangeInfo.Stack) info).spell; } else { spell = new Spell(card, card.getSpellAbility().copy(), card.getOwnerId(), event.getFromZone(), game); } spell.syncZoneChangeCounterOnStack(card, game); game.getStack().push(spell); game.getState().setZone(spell.getId(), Zone.STACK); game.getState().setZone(card.getId(), Zone.STACK); } break; case BATTLEFIELD: Permanent permanent = event.getTarget(); game.addPermanent(permanent, createOrder); game.getPermanentsEntering().remove(permanent.getId()); break; default: throw new UnsupportedOperationException("to Zone " + toZone.toString() + " not supported yet"); } } // update zone in main game.setZone(event.getTargetId(), event.getToZone()); // update zone in other parts (meld cards, mdf half cards) cardsToUpdate.entrySet().forEach(entry -> { for (Card card : entry.getValue().getCards(game)) { if (!card.getId().equals(event.getTargetId())) { game.setZone(card.getId(), entry.getKey()); } } }); // reset meld status if (targetCard instanceof MeldCard) { if (event.getToZone() != Zone.BATTLEFIELD) { ((MeldCard) targetCard).setMelded(false, game); } } } public static Card getTargetCard(Game game, UUID targetId) { Card card = game.getCard(targetId); if (card != null) { return card; } card = game.getMeldCard(targetId); if (card != null) { return card; } return game.getPermanent(targetId); } private static boolean maybeRemoveFromSourceZone(ZoneChangeInfo info, Game game, Ability source) { ZoneChangeEvent event = info.event; // Handle Unmelded Cards if (info instanceof ZoneChangeInfo.Unmelded) { ZoneChangeInfo.Unmelded unmelded = (ZoneChangeInfo.Unmelded) info; MeldCard meld = game.getMeldCard(event.getTargetId()); for (Iterator itr = unmelded.additionalMoves.iterator(); itr.hasNext(); ) { ZoneChangeInfo additionalMove = itr.next(); if (!maybeRemoveFromSourceZone(additionalMove, game, source)) { itr.remove(); } else if (Objects.equals(additionalMove.event.getTargetId(), meld.getTopHalfCard().getId())) { meld.setTopLastZoneChangeCounter(meld.getTopHalfCard().getZoneChangeCounter(game)); } else if (Objects.equals(additionalMove.event.getTargetId(), meld.getBottomHalfCard().getId())) { meld.setBottomLastZoneChangeCounter(meld.getBottomHalfCard().getZoneChangeCounter(game)); } } if (unmelded.additionalMoves.isEmpty()) { return false; } // We arbitrarily prefer the bottom half card. This should never be relevant. meld.updateZoneChangeCounter(game, unmelded.additionalMoves.get(unmelded.additionalMoves.size() - 1).event); return true; } // Handle all normal cases Card card = getTargetCard(game, event.getTargetId()); if (card == null) { // if we can't find the card we can't remove it. return false; } boolean isGoodToMove = false; if (info.faceDown) { // any card can be moved as face down (doubled faced cards also support face down) isGoodToMove = true; } else if (event.getToZone().equals(Zone.BATTLEFIELD)) { // non-permanents can't move to battlefield // TODO: possible bug with Nightbound, search all usage of getValue(TransformAbility.VALUE_KEY_ENTER_TRANSFORMED and insert additional check Ability.checkCard /* * 712.14a. If a spell or ability puts a transforming double-faced card onto the battlefield "transformed" * or "converted," it enters the battlefield with its back face up. If a player is instructed to put a card * that isn't a transforming double-faced card onto the battlefield transformed or converted, that card stays in * its current zone. */ boolean wantToTransform = Boolean.TRUE.equals(game.getState().getValue(TransformAbility.VALUE_KEY_ENTER_TRANSFORMED + card.getId())); if (wantToTransform) { isGoodToMove = card.isTransformable() && card.getSecondCardFace().isPermanent(game); } else { isGoodToMove = card.isPermanent(game); } } else { // other zones allows to move isGoodToMove = true; } if (!isGoodToMove) { return false; } // TODO: is it buggy? Card characteristics are global - if you change face down then it will be // changed in original card too, not in blueprint only card.setFaceDown(info.faceDown, game); boolean success = false; if (!game.replaceEvent(event)) { Zone fromZone = event.getFromZone(); if (event.getToZone() == Zone.BATTLEFIELD) { // PUT TO BATTLEFIELD AS PERMANENT // prepare card and permanent (card must contain full data, even for face down) // if needed to take attributes from the spell (e.g. color of spell was changed) card = prepareBlueprintCardFromSpell(card, event, game); // controlling player can be replaced so use event player now Permanent permanent; if (card instanceof MeldCard) { permanent = new PermanentMeld(card, event.getPlayerId(), game); } else if (card instanceof ModalDoubleFacedCard) { // main mdf card must be processed before that call (e.g. only halves can be moved to battlefield) throw new IllegalStateException("Unexpected trying of move mdf card to battlefield instead half"); } else if (card instanceof Permanent) { throw new IllegalStateException("Unexpected trying of move permanent to battlefield instead card"); } else { permanent = new PermanentCard(card, event.getPlayerId(), game); } // put onto battlefield with possible counters game.getPermanentsEntering().put(permanent.getId(), permanent); card.applyEnterWithCounters(permanent, source, game); permanent.setTapped(info instanceof ZoneChangeInfo.Battlefield && ((ZoneChangeInfo.Battlefield) info).tapped); // if need prototyped version if (Zone.STACK == event.getFromZone()) { Spell spell = game.getStack().getSpell(event.getTargetId()); if (spell != null) { permanent.setPrototyped(spell.isPrototyped()); } } permanent.setFaceDown(info.faceDown, game); if (info.faceDown) { // TODO: need research cards with "setFaceDown(false" // TODO: delete after new release and new face down bugs (old code remove face down status from a card for unknown reason), 2024-02-20 //card.setFaceDown(false, game); } // make sure the controller of all continuous effects of this card are switched to the current controller game.setScopeRelevant(true); try { game.getContinuousEffects().setController(permanent.getId(), permanent.getControllerId()); if (permanent.entersBattlefield(source, game, fromZone, true) && card.removeFromZone(game, fromZone, source)) { success = true; event.setTarget(permanent); } else { // revert controller to owner if permanent does not enter game.getContinuousEffects().setController(permanent.getId(), permanent.getOwnerId()); game.getPermanentsEntering().remove(permanent.getId()); } } finally { game.setScopeRelevant(false); } } else if (event.getTarget() != null) { // PUT PERMANENT TO OTHER ZONE (e.g. remove only) Permanent target = event.getTarget(); success = target.removeFromZone(game, fromZone, source) && game.getPlayer(target.getControllerId()).removeFromBattlefield(target, source, game); } else { // PUT CARD TO OTHER ZONE success = card.removeFromZone(game, fromZone, source); } } if (success) { // change ZCC on real enter // warning, tokens creation code uses same zcc logic as cards (+1 zcc on enter to battlefield) // so if you want to change zcc logic here (but I know you don't) then change token code // too in TokenImpl.putOntoBattlefieldHelper // KickerTest do many tests for token's zcc if (event.getToZone() == Zone.BATTLEFIELD && event.getTarget() != null) { event.getTarget().updateZoneChangeCounter(game, event); } else if (!(card instanceof Permanent)) { card.updateZoneChangeCounter(game, event); } } return success; } public static List chooseOrder(String message, Cards cards, Player player, Ability source, Game game) { List order = new ArrayList<>(); if (cards.isEmpty()) { return order; } TargetCard target = new TargetCard(Zone.ALL, new FilterCard(message)); target.setRequired(true); while (player.canRespond() && cards.size() > 1) { player.choose(Outcome.Neutral, cards, target, source, game); UUID targetObjectId = target.getFirstTarget(); order.add(cards.get(targetObjectId, game)); cards.remove(targetObjectId); target.clearChosen(); } order.addAll(cards.getCards(game)); return order; } private static Card prepareBlueprintCardFromSpell(Card card, ZoneChangeEvent event, Game game) { card = card.copy(); if (Zone.STACK == event.getFromZone()) { // TODO: wtf, why only colors!? Must research and remove colors workaround or add all other data like types too Spell spell = game.getStack().getSpell(event.getTargetId()); // old version if (false && spell != null && !spell.isFaceDown(game)) { if (!card.getColor(game).equals(spell.getColor(game))) { // the card that is referenced to in the permanent is copied and the spell attributes are set to this copied card card.getColor(game).setColor(spell.getColor(game)); } } // new version if (true && spell != null && spell.getSpellAbility() != null) { Card characteristics = spell.getSpellAbility().getCharacteristics(game); if (!characteristics.isFaceDown(game)) { if (!card.getColor(game).equals(characteristics.getColor(game))) { // TODO: don't work with prototyped spells (setColor can't set colorless color) card.getColor(game).setColor(characteristics.getColor(game)); } } } } return card; } }