diff --git a/Mage.Sets/src/mage/cards/b/BeamsplitterMage.java b/Mage.Sets/src/mage/cards/b/BeamsplitterMage.java index 7e584d6b672..1163654b047 100644 --- a/Mage.Sets/src/mage/cards/b/BeamsplitterMage.java +++ b/Mage.Sets/src/mage/cards/b/BeamsplitterMage.java @@ -207,7 +207,7 @@ class BeamsplitterMageApplier implements StackObjectCopyApplier { } @Override - public MageObjectReferencePredicate getNextPredicate() { + public MageObjectReferencePredicate getNextNewTargetType(int copyNumber) { if (predicate.hasNext()) { return predicate.next(); } diff --git a/Mage.Sets/src/mage/cards/c/ChefsKiss.java b/Mage.Sets/src/mage/cards/c/ChefsKiss.java index 46b15590b98..8f6438b92c4 100644 --- a/Mage.Sets/src/mage/cards/c/ChefsKiss.java +++ b/Mage.Sets/src/mage/cards/c/ChefsKiss.java @@ -142,7 +142,7 @@ class ChefsKissApplier implements StackObjectCopyApplier { } @Override - public MageObjectReferencePredicate getNextPredicate() { + public MageObjectReferencePredicate getNextNewTargetType(int copyNumber) { if (predicate.hasNext()) { return predicate.next(); } diff --git a/Mage.Sets/src/mage/cards/d/DoubleMajor.java b/Mage.Sets/src/mage/cards/d/DoubleMajor.java index ad7158981e0..f88df808066 100644 --- a/Mage.Sets/src/mage/cards/d/DoubleMajor.java +++ b/Mage.Sets/src/mage/cards/d/DoubleMajor.java @@ -87,7 +87,7 @@ enum DoubleMajorApplier implements StackObjectCopyApplier { } @Override - public MageObjectReferencePredicate getNextPredicate() { + public MageObjectReferencePredicate getNextNewTargetType(int copyNumber) { return null; } } diff --git a/Mage.Sets/src/mage/cards/f/Fork.java b/Mage.Sets/src/mage/cards/f/Fork.java index d7e88255248..6a6be292731 100644 --- a/Mage.Sets/src/mage/cards/f/Fork.java +++ b/Mage.Sets/src/mage/cards/f/Fork.java @@ -80,7 +80,7 @@ enum ForkApplier implements StackObjectCopyApplier { } @Override - public MageObjectReferencePredicate getNextPredicate() { + public MageObjectReferencePredicate getNextNewTargetType(int copyNumber) { return null; } } diff --git a/Mage.Sets/src/mage/cards/i/InkTreaderNephilim.java b/Mage.Sets/src/mage/cards/i/InkTreaderNephilim.java index 499978e3e2b..ff0a225da3b 100644 --- a/Mage.Sets/src/mage/cards/i/InkTreaderNephilim.java +++ b/Mage.Sets/src/mage/cards/i/InkTreaderNephilim.java @@ -133,7 +133,7 @@ class InkTreaderNephilimEffect extends CopySpellForEachItCouldTargetEffect { } @Override - protected List getPossibleTargets(StackObject stackObject, Player player, Ability source, Game game) { + protected List prepareCopiesWithTargets(StackObject stackObject, Player player, Ability source, Game game) { Permanent permanent = source.getSourcePermanentIfItStillExists(game); return game.getBattlefield() .getActivePermanents( diff --git a/Mage.Sets/src/mage/cards/m/MirrorwingDragon.java b/Mage.Sets/src/mage/cards/m/MirrorwingDragon.java index d5d6bf1b288..276d36347be 100644 --- a/Mage.Sets/src/mage/cards/m/MirrorwingDragon.java +++ b/Mage.Sets/src/mage/cards/m/MirrorwingDragon.java @@ -142,7 +142,7 @@ class MirrorwingDragonCopySpellEffect extends CopySpellForEachItCouldTargetEffec } @Override - protected List getPossibleTargets(StackObject stackObject, Player player, Ability source, Game game) { + protected List prepareCopiesWithTargets(StackObject stackObject, Player player, Ability source, Game game) { Permanent permanent = source.getSourcePermanentIfItStillExists(game); return game.getBattlefield() .getActivePermanents( diff --git a/Mage.Sets/src/mage/cards/p/PrecursorGolem.java b/Mage.Sets/src/mage/cards/p/PrecursorGolem.java index b35e5df9ccc..3193a0944c8 100644 --- a/Mage.Sets/src/mage/cards/p/PrecursorGolem.java +++ b/Mage.Sets/src/mage/cards/p/PrecursorGolem.java @@ -140,7 +140,7 @@ class PrecursorGolemCopySpellEffect extends CopySpellForEachItCouldTargetEffect } @Override - protected List getPossibleTargets(StackObject stackObject, Player player, Ability source, Game game) { + protected List prepareCopiesWithTargets(StackObject stackObject, Player player, Ability source, Game game) { Permanent permanent = game.getPermanent((UUID) getValue("targetedGolem")); return game.getBattlefield() .getActivePermanents( diff --git a/Mage.Sets/src/mage/cards/r/RadiantPerformer.java b/Mage.Sets/src/mage/cards/r/RadiantPerformer.java index 5321c85ef88..8ec84b230d2 100644 --- a/Mage.Sets/src/mage/cards/r/RadiantPerformer.java +++ b/Mage.Sets/src/mage/cards/r/RadiantPerformer.java @@ -115,7 +115,7 @@ class RadiantPerformerEffect extends CopySpellForEachItCouldTargetEffect { } @Override - protected List getPossibleTargets(StackObject stackObject, Player player, Ability source, Game game) { + protected List prepareCopiesWithTargets(StackObject stackObject, Player player, Ability source, Game game) { List predicates = new ArrayList<>(); Ability ability = stackObject instanceof Spell ? ((Spell) stackObject).getSpellAbility() : (StackAbility) stackObject; UUID targeted = ability == null ? null : ability diff --git a/Mage.Sets/src/mage/cards/r/Radiate.java b/Mage.Sets/src/mage/cards/r/Radiate.java index f12b71b592f..c50204dd57d 100644 --- a/Mage.Sets/src/mage/cards/r/Radiate.java +++ b/Mage.Sets/src/mage/cards/r/Radiate.java @@ -120,7 +120,7 @@ class RadiateEffect extends CopySpellForEachItCouldTargetEffect { } @Override - protected List getPossibleTargets(StackObject stackObject, Player player, Ability source, Game game) { + protected List prepareCopiesWithTargets(StackObject stackObject, Player player, Ability source, Game game) { List predicates = new ArrayList<>(); UUID targeted = ((Spell) stackObject) .getSpellAbilities() diff --git a/Mage.Sets/src/mage/cards/z/ZadaHedronGrinder.java b/Mage.Sets/src/mage/cards/z/ZadaHedronGrinder.java index 911f0e653b5..b83a15a650a 100644 --- a/Mage.Sets/src/mage/cards/z/ZadaHedronGrinder.java +++ b/Mage.Sets/src/mage/cards/z/ZadaHedronGrinder.java @@ -135,7 +135,7 @@ class ZadaHedronGrinderCopySpellEffect extends CopySpellForEachItCouldTargetEffe } @Override - protected List getPossibleTargets(StackObject stackObject, Player player, Ability source, Game game) { + protected List prepareCopiesWithTargets(StackObject stackObject, Player player, Ability source, Game game) { Permanent permanent = source.getSourcePermanentIfItStillExists(game); return game.getBattlefield() .getActivePermanents( diff --git a/Mage/src/main/java/mage/abilities/effects/common/CopySpellForEachItCouldTargetEffect.java b/Mage/src/main/java/mage/abilities/effects/common/CopySpellForEachItCouldTargetEffect.java index c420de95be1..62346338868 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/CopySpellForEachItCouldTargetEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/CopySpellForEachItCouldTargetEffect.java @@ -17,12 +17,12 @@ import java.util.List; */ public abstract class CopySpellForEachItCouldTargetEffect extends OneShotEffect { - private static final class CopyApplier implements StackObjectCopyApplier { + private static final class ForEachCopyApplier implements StackObjectCopyApplier { - private final Iterator iterator; + private final Iterator newTargetTypes; - private CopyApplier(List predicates) { - this.iterator = predicates.iterator(); + private ForEachCopyApplier(List copiesWithTargets) { + this.newTargetTypes = copiesWithTargets.iterator(); } @Override @@ -30,9 +30,9 @@ public abstract class CopySpellForEachItCouldTargetEffect extends OneShotEffect } @Override - public MageObjectReferencePredicate getNextPredicate() { - if (iterator.hasNext()) { - return iterator.next(); + public MageObjectReferencePredicate getNextNewTargetType(int copyNumber) { + if (newTargetTypes.hasNext()) { + return newTargetTypes.next(); } return null; } @@ -50,7 +50,16 @@ public abstract class CopySpellForEachItCouldTargetEffect extends OneShotEffect protected abstract Player getPlayer(Game game, Ability source); - protected abstract List getPossibleTargets(StackObject stackObject, Player player, Ability source, Game game); + /** + * Prepare copies list. Each item must contain filter for new target (target type). + * + * @param stackObject + * @param player + * @param source + * @param game + * @return + */ + protected abstract List prepareCopiesWithTargets(StackObject stackObject, Player player, Ability source, Game game); @Override public boolean apply(Game game, Ability source) { @@ -59,10 +68,10 @@ public abstract class CopySpellForEachItCouldTargetEffect extends OneShotEffect if (actingPlayer == null || stackObject == null) { return false; } - List predicates = getPossibleTargets(stackObject, actingPlayer, source, game); + List copies = prepareCopiesWithTargets(stackObject, actingPlayer, source, game); stackObject.createCopyOnStack( game, source, actingPlayer.getId(), false, - predicates.size(), new CopyApplier(predicates) + copies.size(), new ForEachCopyApplier(copies) ); return true; } diff --git a/Mage/src/main/java/mage/game/stack/Spell.java b/Mage/src/main/java/mage/game/stack/Spell.java index 4ede3d39fe4..82ea3710eae 100644 --- a/Mage/src/main/java/mage/game/stack/Spell.java +++ b/Mage/src/main/java/mage/game/stack/Spell.java @@ -1058,18 +1058,21 @@ public class Spell extends StackObjectImpl implements Card { } @Override - public void createSingleCopy(UUID newControllerId, StackObjectCopyApplier applier, MageObjectReferencePredicate predicate, Game game, Ability source, boolean chooseNewTargets) { + 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); - if (predicate != null) { - spellCopy.chooseNewTargets(game, newControllerId, true, false, predicate); + + // 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)); } diff --git a/Mage/src/main/java/mage/game/stack/StackAbility.java b/Mage/src/main/java/mage/game/stack/StackAbility.java index 3a6585c0bd9..adb78a1a7e2 100644 --- a/Mage/src/main/java/mage/game/stack/StackAbility.java +++ b/Mage/src/main/java/mage/game/stack/StackAbility.java @@ -607,16 +607,19 @@ public class StackAbility extends StackObjectImpl implements Ability { } @Override - public void createSingleCopy(UUID newControllerId, StackObjectCopyApplier applier, MageObjectReferencePredicate predicate, Game game, Ability source, boolean chooseNewTargets) { + public void createSingleCopy(UUID newControllerId, StackObjectCopyApplier applier, MageObjectReferencePredicate newTargetFilterPredicate, Game game, Ability source, boolean chooseNewTargets) { Ability newAbility = this.copy(); newAbility.newId(); StackAbility newStackAbility = new StackAbility(newAbility, newControllerId); game.getStack().push(newStackAbility); - if (predicate != null) { - newStackAbility.chooseNewTargets(game, newControllerId, true, false, predicate); + + // new targets + if (newTargetFilterPredicate != null) { + newStackAbility.chooseNewTargets(game, newControllerId, true, false, newTargetFilterPredicate); } else if (chooseNewTargets || applier != null) { // if applier is non-null but predicate is null then it's extra newStackAbility.chooseNewTargets(game, newControllerId); } + game.fireEvent(new CopiedStackObjectEvent(this, newStackAbility, newControllerId)); } diff --git a/Mage/src/main/java/mage/game/stack/StackObject.java b/Mage/src/main/java/mage/game/stack/StackObject.java index af396000c54..e48cda583d6 100644 --- a/Mage/src/main/java/mage/game/stack/StackObject.java +++ b/Mage/src/main/java/mage/game/stack/StackObject.java @@ -29,7 +29,7 @@ public interface StackObject extends MageObject, Controllable { Ability getStackAbility(); - boolean chooseNewTargets(Game game, UUID playerId, boolean forceChange, boolean onlyOneTarget, Predicate extraPredicate); + boolean chooseNewTargets(Game game, UUID playerId, boolean forceChange, boolean onlyOneTarget, Predicate newTargetFilterPredicate); boolean canTarget(Game game, UUID targetId); @@ -39,7 +39,7 @@ public interface StackObject extends MageObject, Controllable { void createCopyOnStack(Game game, Ability source, UUID newControllerId, boolean chooseNewTargets, int amount, StackObjectCopyApplier applier); - void createSingleCopy(UUID newControllerId, StackObjectCopyApplier applier, MageObjectReferencePredicate predicate, Game game, Ability source, boolean chooseNewTargets); + void createSingleCopy(UUID newControllerId, StackObjectCopyApplier applier, MageObjectReferencePredicate newTargetFilterPredicate, Game game, Ability source, boolean chooseNewTargets); boolean isTargetChanged(); diff --git a/Mage/src/main/java/mage/game/stack/StackObjectImpl.java b/Mage/src/main/java/mage/game/stack/StackObjectImpl.java index 84795675b50..f24d4e1d73d 100644 --- a/Mage/src/main/java/mage/game/stack/StackObjectImpl.java +++ b/Mage/src/main/java/mage/game/stack/StackObjectImpl.java @@ -42,18 +42,30 @@ public abstract class StackObjectImpl implements StackObject { createCopyOnStack(game, source, newControllerId, chooseNewTargets, amount, null); } - private static final class PredicateIterator implements Iterator { + /** + * Copy logic: + * - multiple copies allows + * - new targets for each copy allows + * - each next copy can have new target type/filter + * - player can choose copies order on stack + *

+ * Code logic: + * - find all possible target types (any target or custom) + * - user can choose next target type to put on stack + * - put all copies with that target type and request next choice + * - implemented by iterator mechanic (target type -> target filter) + */ + private static final class NewTargetTypeIterator implements Iterator { + private final StackObjectCopyApplier applier; private final Player player; private final int amount; private final Game game; - private Map predicateMap = null; - private int anyCount = 0; - private int setCount = 0; - private Iterator iterator = null; - private Choice choice = null; + private Map newTargetTypes = null; + private Iterator currentNewTargetType = null; + private Choice newTargetTypeChoiceDialog = null; - private PredicateIterator(Game game, UUID newControllerId, int amount, StackObjectCopyApplier applier) { + private NewTargetTypeIterator(Game game, UUID newControllerId, int amount, StackObjectCopyApplier applier) { this.applier = applier; this.player = game.getPlayer(newControllerId); this.amount = amount; @@ -65,39 +77,47 @@ public abstract class StackObjectImpl implements StackObject { return true; } - private void makeMap() { - if (predicateMap != null) { + private void prepareNewTargetTypes() { + if (newTargetTypes != null) { + // already prepared return; } - predicateMap = new HashMap<>(); + int currentAnyTargetNumber = 0; + int currentFilteredTargetNumber = 0; + + newTargetTypes = new HashMap<>(); for (int i = 0; i < amount; i++) { - MageObjectReferencePredicate predicate = applier.getNextPredicate(); - if (predicate == null) { - anyCount++; + MageObjectReferencePredicate newTargetType = applier.getNextNewTargetType(i + 1); + if (newTargetType == null) { + currentAnyTargetNumber++; String message = "Any target"; - if (anyCount > 1) { - message += " (" + anyCount + ")"; + if (currentAnyTargetNumber > 1) { + message += " (" + currentAnyTargetNumber + ")"; } - predicateMap.put(message, predicate); + newTargetTypes.put(message, null); continue; } - setCount++; - predicateMap.put(predicate.getName(game), predicate); + currentFilteredTargetNumber++; + newTargetTypes.put(newTargetType.getName(game), newTargetType); } - if ((setCount == 1 && anyCount == 0) || setCount == 0) { - iterator = predicateMap.values().stream().collect(Collectors.toList()).iterator(); + + // if only one target type then choose it by default + if ((currentFilteredTargetNumber == 1 && currentAnyTargetNumber == 0) || currentFilteredTargetNumber == 0) { + currentNewTargetType = newTargetTypes.values().stream().collect(Collectors.toList()).iterator(); } } - private void makeChoice() { - if (choice != null) { + private void prepareFilterChooseDialog() { + // used choices removes from the dialog + if (newTargetTypeChoiceDialog != null) { + // already prepared return; } - choice = new ChoiceImpl(false, ChoiceHintType.CARD); - choice.setMessage("Choose the order of copies to go on the stack"); - choice.setSubMessage("Press cancel to put the rest in any order"); - choice.setChoices(new HashSet<>(predicateMap.keySet())); + newTargetTypeChoiceDialog = new ChoiceImpl(false, ChoiceHintType.CARD); + newTargetTypeChoiceDialog.setMessage("Choose the order of copies to go on the stack"); + newTargetTypeChoiceDialog.setSubMessage("Press cancel to put the rest in any order"); + newTargetTypeChoiceDialog.setChoices(new HashSet<>(newTargetTypes.keySet())); } @Override @@ -105,24 +125,30 @@ public abstract class StackObjectImpl implements StackObject { if (player == null || applier == null) { return null; } - makeMap(); - if (iterator != null) { - return iterator.hasNext() ? iterator.next() : null; + + prepareNewTargetTypes(); + if (currentNewTargetType != null) { + // target type already selected + return currentNewTargetType.hasNext() ? currentNewTargetType.next() : null; } - makeChoice(); - if (choice.getChoices().size() < 2) { - iterator = choice.getChoices().stream().map(predicateMap::get).iterator(); + + prepareFilterChooseDialog(); + if (newTargetTypeChoiceDialog.getChoices().size() < 2) { + // no more unused target types - select the last one + currentNewTargetType = newTargetTypeChoiceDialog.getChoices().stream().map(newTargetTypes::get).iterator(); return next(); } - choice.clearChoice(); - player.choose(Outcome.AIDontUseIt, choice, game); - String chosen = choice.getChoice(); + + // choose next target type for usage + newTargetTypeChoiceDialog.clearChoice(); + player.choose(Outcome.AIDontUseIt, newTargetTypeChoiceDialog, game); + String chosen = newTargetTypeChoiceDialog.getChoice(); if (chosen == null) { - iterator = choice.getChoices().stream().map(predicateMap::get).iterator(); + currentNewTargetType = newTargetTypeChoiceDialog.getChoices().stream().map(newTargetTypes::get).iterator(); return next(); } - choice.getChoices().remove(chosen); - return predicateMap.get(chosen); + newTargetTypeChoiceDialog.getChoices().remove(chosen); + return newTargetTypes.get(chosen); } } @@ -132,9 +158,9 @@ public abstract class StackObjectImpl implements StackObject { if (game.replaceEvent(gameEvent)) { return; } - Iterator predicates = new PredicateIterator(game, newControllerId, gameEvent.getAmount(), applier); + Iterator newTargetTypeIterator = new NewTargetTypeIterator(game, newControllerId, gameEvent.getAmount(), applier); for (int i = 0; i < gameEvent.getAmount(); i++) { - createSingleCopy(newControllerId, applier, predicates.next(), game, source, chooseNewTargets); + createSingleCopy(newControllerId, applier, newTargetTypeIterator.next(), game, source, chooseNewTargets); } Player player = game.getPlayer(newControllerId); if (player == null) { @@ -210,18 +236,17 @@ public abstract class StackObjectImpl implements StackObject { * targets will be, the copy is put onto the stack with those targets. * * @param game - * @param targetControllerId - player that can/has to change the target of - * the spell - * @param forceChange - does only work for targets with maximum of one - * targetId - * @param onlyOneTarget - 114.6b one target must be changed to another - * target - * @param extraPredicate restriction for the new target, if null nothing is - * cheched + * @param targetControllerId - player that can/has to change the target of + * the spell + * @param forceChange - does only work for targets with maximum of one + * targetId + * @param onlyOneTarget - 114.6b one target must be changed to another + * target + * @param newTargetFilterPredicate restriction for the new target (null - can select same targets) * @return */ @Override - public boolean chooseNewTargets(Game game, UUID targetControllerId, boolean forceChange, boolean onlyOneTarget, Predicate extraPredicate) { + public boolean chooseNewTargets(Game game, UUID targetControllerId, boolean forceChange, boolean onlyOneTarget, Predicate newTargetFilterPredicate) { Player targetController = game.getPlayer(targetControllerId); if (targetController != null) { StringBuilder oldTargetDescription = new StringBuilder(); @@ -240,7 +265,7 @@ public abstract class StackObjectImpl implements StackObject { ability.getModes().setActiveMode(mode); oldTargetDescription.append(ability.getTargetDescription(mode.getTargets(), game)); for (Target target : mode.getTargets()) { - Target newTarget = chooseNewTarget(targetController, ability, mode, target, forceChange, extraPredicate, game); + Target newTarget = chooseNewTarget(targetController, ability, mode, target, forceChange, newTargetFilterPredicate, game); // clear the old target and copy all targets from new target target.clearChosen(); for (UUID targetId : newTarget.getTargets()) { @@ -263,21 +288,25 @@ public abstract class StackObjectImpl implements StackObject { /** * Handles the change of one target instance of a mode * - * @param targetController - player that can choose the new target + * @param targetController - player that can choose the new target * @param ability * @param mode * @param target * @param forceChange + * @param newTargetFilterPredicate * @param game * @return */ - private Target chooseNewTarget(Player targetController, Ability ability, Mode mode, Target target, boolean forceChange, Predicate predicate, Game game) { + private Target chooseNewTarget(Player targetController, Ability ability, Mode mode, Target target, boolean forceChange, Predicate newTargetFilterPredicate, Game game) { Target newTarget = target.copy(); - if (predicate != null) { - newTarget.getFilter().add(predicate); + + // filter targets + if (newTargetFilterPredicate != null) { + newTarget.getFilter().add(newTargetFilterPredicate); // If adding a predicate, there will only be one choice and therefore target can be automatic newTarget.setRandom(true); } + newTarget.setEventReporting(false); if (!targetController.getId().equals(getControllerId())) { newTarget.setTargetController(targetController.getId()); // target controller for the change is different from spell controller @@ -318,8 +347,8 @@ public abstract class StackObjectImpl implements StackObject { } else { // build a target definition with exactly one possible target to select that replaces old target Target tempTarget = target.copy(); - if (predicate != null) { - tempTarget.getFilter().add(predicate); + if (newTargetFilterPredicate != null) { + tempTarget.getFilter().add(newTargetFilterPredicate); // If adding a predicate, there will only be one choice and therefore target can be automatic tempTarget.setRandom(true); } diff --git a/Mage/src/main/java/mage/util/functions/StackObjectCopyApplier.java b/Mage/src/main/java/mage/util/functions/StackObjectCopyApplier.java index 1bd74d6212a..c24f6bc3663 100644 --- a/Mage/src/main/java/mage/util/functions/StackObjectCopyApplier.java +++ b/Mage/src/main/java/mage/util/functions/StackObjectCopyApplier.java @@ -13,5 +13,12 @@ public interface StackObjectCopyApplier extends Serializable { void modifySpell(StackObject stackObject, Game game); - MageObjectReferencePredicate getNextPredicate(); + /** + * For multi copies: allows change new target filter for each next copy (e.g. add some restict) + * Return null to use same target type as original spell + * + * @param copyNumber current number of copy, starts with 1 + * @return + */ + MageObjectReferencePredicate getNextNewTargetType(int copyNumber); }