From 1b690e5c8c4c66a20314ad53f5d0d5ba3e16bd90 Mon Sep 17 00:00:00 2001 From: LevelX2 Date: Sun, 31 May 2015 18:44:02 +0200 Subject: [PATCH] * Fixed target change handling (e.g. with opponent filter - fixes #574). Added some tests. --- .../java/mage/player/ai/ComputerPlayer.java | 92 ++++--- .../src/mage/player/human/HumanPlayer.java | 20 +- .../avacynrestored/TreacherousPitDweller.java | 6 +- .../sets/fatereforged/MonasteryMentor.java | 1 + .../fatereforged/WhisperwoodElemental.java | 2 +- .../sets/mercadianmasques/Misdirection.java | 2 +- .../src/mage/sets/visions/CityOfSolitude.java | 6 - .../abilities/keywords/ManifestTest.java | 36 +++ .../TargetOpponentGainsControlTest.java | 20 +- .../test/cards/single/MisdirectionTest.java | 70 ++++- .../cards/triggers/SpellCastTriggerTest.java | 30 +- .../java/org/mage/test/player/TestPlayer.java | 8 +- Mage/src/mage/abilities/Ability.java | 2 + Mage/src/mage/abilities/AbilityImpl.java | 1 + .../common/ChooseNewTargetsTargetEffect.java | 1 + Mage/src/mage/game/stack/Spell.java | 215 +-------------- Mage/src/mage/game/stack/StackAbility.java | 199 +------------- Mage/src/mage/game/stack/StackObjImpl.java | 260 ++++++++++++++++++ Mage/src/mage/players/PlayerImpl.java | 3 +- Mage/src/mage/target/Target.java | 4 + Mage/src/mage/target/TargetAmount.java | 3 +- Mage/src/mage/target/TargetImpl.java | 34 ++- 22 files changed, 531 insertions(+), 484 deletions(-) create mode 100644 Mage/src/mage/game/stack/StackObjImpl.java diff --git a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java index f0bce292705..df0f14580ba 100644 --- a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java +++ b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java @@ -204,11 +204,16 @@ public class ComputerPlayer extends PlayerImpl implements Player { if (log.isDebugEnabled()) { log.debug("chooseTarget: " + outcome.toString() + ":" + target.toString()); } - UUID opponentId = game.getOpponents(playerId).iterator().next(); + // sometimes a target aelection can be made from a player that does not control the ability + UUID abilityControllerId = playerId; + if (target.getTargetController() != null && target.getAbilityController() != null) { + abilityControllerId = target.getAbilityController(); + } + UUID opponentId = game.getOpponents(abilityControllerId).iterator().next(); if (target instanceof TargetPlayer) { if (outcome.isGood()) { - if (target.canTarget(playerId, game)) { - target.add(playerId, game); + if (target.canTarget(abilityControllerId, game)) { + target.add(abilityControllerId, game); return true; } if (target.isRequired(sourceId, game)) { @@ -223,8 +228,8 @@ public class ComputerPlayer extends PlayerImpl implements Player { return true; } if (target.isRequired(sourceId, game)) { - if (target.canTarget(playerId, game)) { - target.add(playerId, game); + if (target.canTarget(abilityControllerId, game)) { + target.add(abilityControllerId, game); return true; } } @@ -257,12 +262,12 @@ public class ComputerPlayer extends PlayerImpl implements Player { } if (target instanceof TargetControlledPermanent) { List targets; - targets = threats(playerId, sourceId, ((TargetControlledPermanent) target).getFilter(), game, target.getTargets()); + targets = threats(abilityControllerId, sourceId, ((TargetControlledPermanent) target).getFilter(), game, target.getTargets()); if (!outcome.isGood()) { Collections.reverse(targets); } for (Permanent permanent : targets) { - if (((TargetControlledPermanent) target).canTarget(playerId, permanent.getId(), sourceId, game, false) && !target.getTargets().contains(permanent.getId())) { + if (((TargetControlledPermanent) target).canTarget(abilityControllerId, permanent.getId(), sourceId, game, false) && !target.getTargets().contains(permanent.getId())) { target.add(permanent.getId(), game); return true; } @@ -275,13 +280,13 @@ public class ComputerPlayer extends PlayerImpl implements Player { targets = threats(null, sourceId, ((TargetPermanent) target).getFilter(), game, target.getTargets()); } else { if (outcome.isGood()) { - targets = threats(playerId, sourceId, ((TargetPermanent) target).getFilter(), game, target.getTargets()); + targets = threats(abilityControllerId, sourceId, ((TargetPermanent) target).getFilter(), game, target.getTargets()); } else { targets = threats(opponentId, sourceId, ((TargetPermanent) target).getFilter(), game, target.getTargets()); } } for (Permanent permanent : targets) { - if (((TargetPermanent) target).canTarget(playerId, permanent.getId(), null, game) && !target.getTargets().contains(permanent.getId())) { + if (((TargetPermanent) target).canTarget(abilityControllerId, permanent.getId(), null, game) && !target.getTargets().contains(permanent.getId())) { target.add(permanent.getId(), game); return true; } @@ -309,13 +314,13 @@ public class ComputerPlayer extends PlayerImpl implements Player { List targets; TargetCreatureOrPlayer t = ((TargetCreatureOrPlayer) target); if (outcome.isGood()) { - targets = threats(playerId, sourceId, ((FilterCreatureOrPlayer) t.getFilter()).getCreatureFilter(), game, target.getTargets()); + targets = threats(abilityControllerId, sourceId, ((FilterCreatureOrPlayer) t.getFilter()).getCreatureFilter(), game, target.getTargets()); } else { targets = threats(opponentId, sourceId, ((FilterCreatureOrPlayer) t.getFilter()).getCreatureFilter(), game, target.getTargets()); } for (Permanent permanent : targets) { List alreadyTargetted = target.getTargets(); - if (t.canTarget(playerId, permanent.getId(), null, game)) { + if (t.canTarget(abilityControllerId, permanent.getId(), null, game)) { if (alreadyTargetted != null && !alreadyTargetted.contains(permanent.getId())) { target.add(permanent.getId(), game); return true; @@ -323,8 +328,8 @@ public class ComputerPlayer extends PlayerImpl implements Player { } } if (outcome.isGood()) { - if (target.canTarget(playerId, null, game)) { - target.add(playerId, game); + if (target.canTarget(abilityControllerId, null, game)) { + target.add(abilityControllerId, game); return true; } } else { @@ -341,7 +346,7 @@ public class ComputerPlayer extends PlayerImpl implements Player { if (target instanceof TargetPermanentOrPlayer) { List targets; TargetPermanentOrPlayer t = ((TargetPermanentOrPlayer) target); - List ownedTargets = threats(playerId, sourceId, ((FilterPermanentOrPlayer) t.getFilter()).getPermanentFilter(), game, target.getTargets());; + List ownedTargets = threats(abilityControllerId, sourceId, ((FilterPermanentOrPlayer) t.getFilter()).getPermanentFilter(), game, target.getTargets());; List opponentTargets = threats(opponentId, sourceId, ((FilterPermanentOrPlayer) t.getFilter()).getPermanentFilter(), game, target.getTargets()); if (outcome.isGood()) { targets = ownedTargets; @@ -358,8 +363,8 @@ public class ComputerPlayer extends PlayerImpl implements Player { } } if (outcome.isGood()) { - if (target.canTarget(playerId, null, game)) { - target.add(playerId, game); + if (target.canTarget(abilityControllerId, null, game)) { + target.add(abilityControllerId, game); return true; } } else { @@ -375,8 +380,8 @@ public class ComputerPlayer extends PlayerImpl implements Player { target.add(opponentId, game); return true; } - if (target.canTarget(playerId, null, game)) { - target.add(playerId, game); + if (target.canTarget(abilityControllerId, null, game)) { + target.add(abilityControllerId, game); return true; } if (outcome.isGood()) { // no other valid targets so use a permanent @@ -415,7 +420,7 @@ public class ComputerPlayer extends PlayerImpl implements Player { if (target instanceof TargetCardInYourGraveyard) { List alreadyTargetted = target.getTargets(); - List cards = new ArrayList<>(game.getPlayer(playerId).getGraveyard().getCards(game)); + List cards = new ArrayList<>(game.getPlayer(abilityControllerId).getGraveyard().getCards(game)); while(!cards.isEmpty()) { Card card = pickTarget(cards, outcome, target, null, game); if (card != null && alreadyTargetted != null && !alreadyTargetted.contains(card.getId())) { @@ -428,7 +433,7 @@ public class ComputerPlayer extends PlayerImpl implements Player { if (target instanceof TargetSource) { Set targets; TargetSource t = ((TargetSource) target); - targets = t.possibleTargets(sourceId, playerId, game); + targets = t.possibleTargets(sourceId, abilityControllerId, game); for (UUID targetId : targets) { MageObject targetObject = game.getObject(targetId); if (targetObject != null) { @@ -455,28 +460,33 @@ public class ComputerPlayer extends PlayerImpl implements Player { if (log.isDebugEnabled()) { log.debug("chooseTarget: " + outcome.toString() + ":" + target.toString()); } - UUID opponentId = game.getOpponents(playerId).iterator().next(); + // sometimes a target selection can be made from a player that does not control the ability + UUID abilityControllerId = playerId; + if (target.getAbilityController() != null) { + abilityControllerId = target.getAbilityController(); + } + UUID opponentId = game.getOpponents(abilityControllerId).iterator().next(); if (target instanceof TargetPlayer) { if (outcome.isGood()) { - if (target.canTarget(playerId, playerId, source, game)) { + if (target.canTarget(abilityControllerId, abilityControllerId, source, game)) { target.addTarget(playerId, source, game); return true; } if (target.isRequired(source)) { - if (target.canTarget(playerId, opponentId, source, game)) { + if (target.canTarget(abilityControllerId, opponentId, source, game)) { target.addTarget(opponentId, source, game); return true; } } } else { - if (target.canTarget(playerId, opponentId, source, game)) { + if (target.canTarget(abilityControllerId, opponentId, source, game)) { target.addTarget(opponentId, source, game); return true; } if (target.isRequired(source)) { - if (target.canTarget(playerId, playerId, source, game)) { - target.addTarget(playerId, source, game); + if (target.canTarget(abilityControllerId, abilityControllerId, source, game)) { + target.addTarget(abilityControllerId, source, game); return true; } } @@ -526,12 +536,12 @@ public class ComputerPlayer extends PlayerImpl implements Player { } if (target instanceof TargetControlledPermanent) { List targets; - targets = threats(playerId, source.getSourceId(), ((TargetControlledPermanent)target).getFilter(), game, target.getTargets()); + targets = threats(abilityControllerId, source.getSourceId(), ((TargetControlledPermanent)target).getFilter(), game, target.getTargets()); if (!outcome.isGood()) { Collections.reverse(targets); } for (Permanent permanent: targets) { - if (((TargetControlledPermanent)target).canTarget(playerId, permanent.getId(), source, game)) { + if (((TargetControlledPermanent)target).canTarget(abilityControllerId, permanent.getId(), source, game)) { target.addTarget(permanent.getId(), source, game); if (target.getNumberOfTargets() <= target.getTargets().size() && (!outcome.isGood() || target.getMaxNumberOfTargets() <= target.getTargets().size())) { return true; @@ -545,7 +555,7 @@ public class ComputerPlayer extends PlayerImpl implements Player { List targets; boolean outcomeTargets = true; if (outcome.isGood()) { - targets = threats(playerId, source == null?null:source.getSourceId(), ((TargetPermanent)target).getFilter(), game, target.getTargets()); + targets = threats(abilityControllerId, source == null?null:source.getSourceId(), ((TargetPermanent)target).getFilter(), game, target.getTargets()); } else { targets = threats(opponentId, source == null?null:source.getSourceId(), ((TargetPermanent)target).getFilter(), game, target.getTargets()); @@ -557,7 +567,7 @@ public class ComputerPlayer extends PlayerImpl implements Player { //targets = game.getBattlefield().getActivePermanents(((TargetPermanent)target).getFilter(), playerId, game); } for (Permanent permanent: targets) { - if (((TargetPermanent)target).canTarget(playerId, permanent.getId(), source, game)) { + if (((TargetPermanent)target).canTarget(abilityControllerId, permanent.getId(), source, game)) { target.addTarget(permanent.getId(), source, game); if (!outcomeTargets || target.getMaxNumberOfTargets() <= target.getTargets().size()) { return true; @@ -570,7 +580,7 @@ public class ComputerPlayer extends PlayerImpl implements Player { List targets; TargetCreatureOrPlayer t = ((TargetCreatureOrPlayer)target); if (outcome.isGood()) { - targets = threats(playerId, source.getSourceId(), ((FilterCreatureOrPlayer)t.getFilter()).getCreatureFilter(), game, target.getTargets()); + targets = threats(abilityControllerId, source.getSourceId(), ((FilterCreatureOrPlayer)t.getFilter()).getCreatureFilter(), game, target.getTargets()); } else { targets = threats(opponentId, source.getSourceId(), ((FilterCreatureOrPlayer)t.getFilter()).getCreatureFilter(), game, target.getTargets()); @@ -578,8 +588,8 @@ public class ComputerPlayer extends PlayerImpl implements Player { if (targets.isEmpty()) { if (outcome.isGood()) { - if (target.canTarget(playerId, source, game)) { - target.addTarget(playerId, source, game); + if (target.canTarget(abilityControllerId, source, game)) { + target.addTarget(abilityControllerId, source, game); return true; } } @@ -596,7 +606,7 @@ public class ComputerPlayer extends PlayerImpl implements Player { } for (Permanent permanent : targets) { List alreadyTargetted = target.getTargets(); - if (t.canTarget(playerId, permanent.getId(), source, game)) { + if (t.canTarget(abilityControllerId, permanent.getId(), source, game)) { if (alreadyTargetted != null && !alreadyTargetted.contains(permanent.getId())) { target.addTarget(permanent.getId(), source, game); return true; @@ -605,8 +615,8 @@ public class ComputerPlayer extends PlayerImpl implements Player { } if (outcome.isGood()) { - if (target.canTarget(playerId, source, game)) { - target.addTarget(playerId, source, game); + if (target.canTarget(abilityControllerId, source, game)) { + target.addTarget(abilityControllerId, source, game); return true; } } @@ -634,7 +644,7 @@ public class ComputerPlayer extends PlayerImpl implements Player { return false; } if (target instanceof TargetCardInLibrary) { - List cards = new ArrayList<>(game.getPlayer(playerId).getLibrary().getCards(game)); + List cards = new ArrayList<>(game.getPlayer(abilityControllerId).getLibrary().getCards(game)); Card card = pickTarget(cards, outcome, target, source, game); if (card != null) { target.addTarget(card.getId(), source, game); @@ -643,7 +653,7 @@ public class ComputerPlayer extends PlayerImpl implements Player { return false; } if (target instanceof TargetCardInYourGraveyard) { - List cards = new ArrayList<>(game.getPlayer(playerId).getGraveyard().getCards(game)); + List cards = new ArrayList<>(game.getPlayer(abilityControllerId).getGraveyard().getCards(game)); while(!target.isChosen() && !cards.isEmpty()) { Card card = pickTarget(cards, outcome, target, source, game); if (card != null) { @@ -680,7 +690,7 @@ public class ComputerPlayer extends PlayerImpl implements Player { List targets; boolean outcomeTargets = true; if (outcome.isGood()) { - targets = threats(playerId, source == null?null:source.getSourceId(), ((TargetSpellOrPermanent)target).getPermanentFilter(), game, target.getTargets()); + targets = threats(abilityControllerId, source == null?null:source.getSourceId(), ((TargetSpellOrPermanent)target).getPermanentFilter(), game, target.getTargets()); } else { targets = threats(opponentId, source == null?null:source.getSourceId(), ((TargetSpellOrPermanent)target).getPermanentFilter(), game, target.getTargets()); @@ -692,7 +702,7 @@ public class ComputerPlayer extends PlayerImpl implements Player { //targets = game.getBattlefield().getActivePermanents(((TargetPermanent)target).getFilter(), playerId, game); } for (Permanent permanent: targets) { - if (((TargetPermanent)target).canTarget(playerId, permanent.getId(), source, game)) { + if (((TargetPermanent)target).canTarget(abilityControllerId, permanent.getId(), source, game)) { target.addTarget(permanent.getId(), source, game); if (!outcomeTargets || target.getMaxNumberOfTargets() <= target.getTargets().size()) { return true; @@ -715,7 +725,7 @@ public class ComputerPlayer extends PlayerImpl implements Player { } if (target instanceof TargetCardInOpponentsGraveyard) { List cards = new ArrayList<>(); - for (UUID uuid: game.getOpponents(playerId)) { + for (UUID uuid: game.getOpponents(abilityControllerId)) { Player player = game.getPlayer(uuid); if (player != null) { cards.addAll(player.getGraveyard().getCards(game)); diff --git a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java index 5a3750c595a..9faf9554cea 100644 --- a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java +++ b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java @@ -235,11 +235,15 @@ public class HumanPlayer extends PlayerImpl { @Override public boolean choose(Outcome outcome, Target target, UUID sourceId, Game game, Map options) { updateGameStatePriority("choose(5)", game); + UUID abilityControllerId = playerId; + if (target.getTargetController() != null && target.getAbilityController() != null) { + abilityControllerId = target.getAbilityController(); + } if (options == null) { options = new HashMap<>(); } while (!abort) { - Set targetIds = target.possibleTargets(sourceId, playerId, game); + Set targetIds = target.possibleTargets(sourceId, abilityControllerId, game); if (targetIds == null || targetIds.isEmpty()) { return false; } @@ -251,14 +255,14 @@ public class HumanPlayer extends PlayerImpl { List chosen = target.getTargets(); options.put("chosen", (Serializable)chosen); - game.fireSelectTargetEvent(playerId, target.getMessage(), targetIds, required, getOptions(target, options)); + game.fireSelectTargetEvent(getId(), target.getMessage(), targetIds, required, getOptions(target, options)); waitForResponse(game); if (response.getUUID() != null) { if (!targetIds.contains(response.getUUID())) { continue; } if (target instanceof TargetPermanent) { - if (((TargetPermanent)target).canTarget(playerId, response.getUUID(), sourceId, game, false)) { + if (((TargetPermanent)target).canTarget(abilityControllerId, response.getUUID(), sourceId, game, false)) { target.add(response.getUUID(), game); if(target.doneChosing()){ return true; @@ -306,13 +310,17 @@ public class HumanPlayer extends PlayerImpl { @Override public boolean chooseTarget(Outcome outcome, Target target, Ability source, Game game) { updateGameStatePriority("chooseTarget", game); + UUID abilityControllerId = playerId; + if (target.getAbilityController() != null) { + abilityControllerId = target.getAbilityController(); + } while (!abort) { - Set possibleTargets = target.possibleTargets(source==null?null:source.getSourceId(), playerId, game); + Set possibleTargets = target.possibleTargets(source==null?null:source.getSourceId(), abilityControllerId, game); boolean required = target.isRequired(source); if (possibleTargets.isEmpty() || target.getTargets().size() >= target.getNumberOfTargets()) { required = false; } - game.fireSelectTargetEvent(playerId, target.getMessage(), possibleTargets, required, getOptions(target, null)); + game.fireSelectTargetEvent(getId(), target.getMessage(), possibleTargets, required, getOptions(target, null)); waitForResponse(game); if (response.getUUID() != null) { if (target.getTargets().contains(response.getUUID())) { @@ -320,7 +328,7 @@ public class HumanPlayer extends PlayerImpl { continue; } if (possibleTargets.contains(response.getUUID())) { - if (target.canTarget(playerId, response.getUUID(), source, game)) { + if (target.canTarget(abilityControllerId, response.getUUID(), source, game)) { target.addTarget(response.getUUID(), source, game); if(target.doneChosing()){ return true; diff --git a/Mage.Sets/src/mage/sets/avacynrestored/TreacherousPitDweller.java b/Mage.Sets/src/mage/sets/avacynrestored/TreacherousPitDweller.java index 65723eb5af0..ac03b230859 100644 --- a/Mage.Sets/src/mage/sets/avacynrestored/TreacherousPitDweller.java +++ b/Mage.Sets/src/mage/sets/avacynrestored/TreacherousPitDweller.java @@ -41,6 +41,7 @@ import java.util.UUID; import mage.abilities.TriggeredAbilityImpl; import mage.game.events.EntersTheBattlefieldEvent; import mage.game.events.GameEvent; +import mage.players.Player; /** * @author noxx @@ -126,8 +127,9 @@ class TreacherousPitDwellerEffect extends ContinuousEffectImpl { @Override public boolean apply(Game game, Ability source) { Permanent permanent = (Permanent) source.getSourceObjectIfItStillExists(game); - if (permanent != null) { - return permanent.changeControllerId(source.getFirstTarget(), game); + Player targetOpponent = game.getPlayer(source.getFirstTarget()); + if (permanent != null && targetOpponent != null) { + return permanent.changeControllerId(targetOpponent.getId(), game); } else { discard(); } diff --git a/Mage.Sets/src/mage/sets/fatereforged/MonasteryMentor.java b/Mage.Sets/src/mage/sets/fatereforged/MonasteryMentor.java index e30f0ac8769..73871d05660 100644 --- a/Mage.Sets/src/mage/sets/fatereforged/MonasteryMentor.java +++ b/Mage.Sets/src/mage/sets/fatereforged/MonasteryMentor.java @@ -61,6 +61,7 @@ public class MonasteryMentor extends CardImpl { // Prowess this.addAbility(new ProwessAbility()); + // Whenever you cast a noncreature spell, put a 1/1 white Monk creature token with prowess onto the battlefield. this.addAbility(new SpellCastControllerTriggeredAbility(new CreateTokenEffect(new MonasteryMentorToken()), filter, false)); } diff --git a/Mage.Sets/src/mage/sets/fatereforged/WhisperwoodElemental.java b/Mage.Sets/src/mage/sets/fatereforged/WhisperwoodElemental.java index 2d8174c36d9..aba6948e696 100644 --- a/Mage.Sets/src/mage/sets/fatereforged/WhisperwoodElemental.java +++ b/Mage.Sets/src/mage/sets/fatereforged/WhisperwoodElemental.java @@ -76,7 +76,7 @@ public class WhisperwoodElemental extends CardImpl { Effect effect = new GainAbilityControlledEffect(abilityToGain, Duration.EndOfTurn, filter); effect.setText("Until end of turn, face-up, nontoken creatures you control gain \"When this creature dies, manifest the top card of your library.\""); this.addAbility(new SimpleActivatedAbility( - Zone.BATTLEFIELD, effect, new SacrificeSourceCost())); + Zone.ALL, effect, new SacrificeSourceCost())); } public WhisperwoodElemental(final WhisperwoodElemental card) { diff --git a/Mage.Sets/src/mage/sets/mercadianmasques/Misdirection.java b/Mage.Sets/src/mage/sets/mercadianmasques/Misdirection.java index 092c9a97f51..094470b2e15 100644 --- a/Mage.Sets/src/mage/sets/mercadianmasques/Misdirection.java +++ b/Mage.Sets/src/mage/sets/mercadianmasques/Misdirection.java @@ -60,10 +60,10 @@ public class Misdirection extends CardImpl { super(ownerId, 87, "Misdirection", Rarity.RARE, new CardType[]{CardType.INSTANT}, "{3}{U}{U}"); this.expansionSetCode = "MMQ"; - // You may exile a blue card from your hand rather than pay Misdirection's mana cost. FilterOwnedCard filterCardInHand = new FilterOwnedCard("a blue card from your hand"); filterCardInHand.add(new ColorPredicate(ObjectColor.BLUE)); + // the exile cost can never be paid with the card itself filterCardInHand.add(Predicates.not(new CardIdPredicate(this.getId()))); this.addAbility(new AlternativeCostSourceAbility(new ExileFromHandCost(new TargetCardInHand(filterCardInHand)))); diff --git a/Mage.Sets/src/mage/sets/visions/CityOfSolitude.java b/Mage.Sets/src/mage/sets/visions/CityOfSolitude.java index 2bac394e602..2b246421ef1 100644 --- a/Mage.Sets/src/mage/sets/visions/CityOfSolitude.java +++ b/Mage.Sets/src/mage/sets/visions/CityOfSolitude.java @@ -51,7 +51,6 @@ public class CityOfSolitude extends CardImpl { super(ownerId, 52, "City of Solitude", Rarity.RARE, new CardType[]{CardType.ENCHANTMENT}, "{2}{G}"); this.expansionSetCode = "VIS"; - // Players can cast spells and activate abilities only during their own turns. this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new CityOfSolitudeEffect())); } @@ -87,11 +86,6 @@ class CityOfSolitudeEffect extends ContinuousRuleModifyingEffectImpl { return !game.getActivePlayerId().equals(event.getPlayerId()); } - @Override - public boolean apply(Game game, Ability source) { - return true; - } - @Override public CityOfSolitudeEffect copy() { return new CityOfSolitudeEffect(this); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ManifestTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ManifestTest.java index 775d820ec24..f980b8be196 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ManifestTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ManifestTest.java @@ -362,4 +362,40 @@ public class ManifestTest extends CardTestPlayerBase { assertPermanentCount(playerB, "", 1); } + + /** + * Whisperwood Elemental - Its sacrifice ability doesn't work.. + * + */ + @Test + public void testWhisperwoodElemental() { + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); + // Seismic Rupture deals 2 damage to each creature without flying. + addCard(Zone.HAND, playerA, "Seismic Rupture", 1); + + // At the beginning of your end step, manifest the top card of your library. + // Sacrifice Whisperwood Elemental: Until end of turn, face-up, nontoken creatures you control gain "When this creature dies, manifest the top card of your library." + addCard(Zone.BATTLEFIELD, playerB, "Whisperwood Elemental", 1); + + addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 2); + + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Sacrifice"); + + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Seismic Rupture"); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + // no life gain + assertLife(playerA, 20); + assertLife(playerB, 20); + + assertGraveyardCount(playerA, "Seismic Rupture", 1); + assertGraveyardCount(playerB, "Whisperwood Elemental", 1); + assertGraveyardCount(playerB, "Silvercoat Lion", 2); + + assertPermanentCount(playerB, "", 2); + + } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/control/TargetOpponentGainsControlTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/control/TargetOpponentGainsControlTest.java index 327079c36d2..9665a8c649e 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/control/TargetOpponentGainsControlTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/control/TargetOpponentGainsControlTest.java @@ -31,14 +31,20 @@ public class TargetOpponentGainsControlTest extends CardTestPlayerBase { @Test public void testChangeControlEffectFromTwoCards() { addCard(Zone.HAND, playerA, "Lightning Bolt", 3); - addCard(Zone.HAND, playerA, "Unhallowed Pact", 3); - addCard(Zone.BATTLEFIELD, playerA, "Treacherous Pit-Dweller"); + // Enchant creature + // When enchanted creature dies, return that card to the battlefield under your control. + addCard(Zone.HAND, playerA, "Unhallowed Pact", 1); // {2}{B} + // Undying + // When Treacherous Pit-Dweller enters the battlefield from a graveyard, target opponent gains control of it. + addCard(Zone.BATTLEFIELD, playerA, "Treacherous Pit-Dweller"); // 4/3 addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5); addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1); - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", "Treacherous Pit-Dweller"); - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Unhallowed Pact", "Treacherous Pit-Dweller"); - castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Lightning Bolt", "Treacherous Pit-Dweller"); + castSpell(1, PhaseStep.UPKEEP, playerA, "Lightning Bolt", "Treacherous Pit-Dweller"); // comes back with undying + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Unhallowed Pact", "Treacherous Pit-Dweller"); + + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Lightning Bolt", "Treacherous Pit-Dweller"); // Treacherous Pit-Dweller is now 5/4 castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Lightning Bolt", "Treacherous Pit-Dweller"); setStopAt(1, PhaseStep.END_TURN); @@ -46,8 +52,10 @@ public class TargetOpponentGainsControlTest extends CardTestPlayerBase { // went to graveyard assertGraveyardCount(playerA, "Unhallowed Pact", 1); + assertGraveyardCount(playerA, "Lightning Bolt", 3); // returned back - assertPermanentCount(playerA, "Treacherous Pit-Dweller", 1); + assertGraveyardCount(playerA, "Treacherous Pit-Dweller", 0); + assertPermanentCount(playerB, "Treacherous Pit-Dweller", 1); // opponent gets it because ETB of Dweller resolves always last } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/MisdirectionTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/MisdirectionTest.java index 737f8cd5f82..bbb375a2ccb 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/MisdirectionTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/MisdirectionTest.java @@ -68,6 +68,74 @@ public class MisdirectionTest extends CardTestPlayerBase { assertGraveyardCount(playerA, "Rakshasa's Secret", 1); assertGraveyardCount(playerB, "Misdirection", 1); assertHandCount(playerB, "Silvercoat Lion", 0); - } + + // check to change target permanent creature legal to to a creature the opponent of the spell controller controls + @Test + public void testChangePublicExecution() { + // Destroy target creature an opponent controls. Each other creature that player controls gets -2/-0 until end of turn. + addCard(Zone.HAND, playerA, "Public Execution"); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 6); + /* + Misdirection {3}{U}{U} + Instant + You may exile a blue card from your hand rather than pay Misdirection's mana cost. + Change the target of target spell with a single target. + */ + addCard(Zone.HAND, playerB, "Misdirection"); + addCard(Zone.BATTLEFIELD, playerB, "Pillarfield Ox", 1); + addCard(Zone.BATTLEFIELD, playerB, "Custodian of the Trove", 1); // 4/3 + addCard(Zone.BATTLEFIELD, playerB, "Island", 5); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Public Execution", "Pillarfield Ox"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Misdirection", "Public Execution", "Public Execution"); + addTarget(playerB, "Custodian of the Trove"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertGraveyardCount(playerA, "Public Execution", 1); + assertGraveyardCount(playerB, "Misdirection", 1); + + assertGraveyardCount(playerB, "Custodian of the Trove",1); + assertPermanentCount(playerB, "Pillarfield Ox", 1); + assertPowerToughness(playerB, "Pillarfield Ox", 0, 4); + + } + + // check to change target permanent creature not legal to to a creature the your opponent controls + @Test + public void testChangePublicExecution2() { + // Destroy target creature an opponent controls. Each other creature that player controls gets -2/-0 until end of turn. + addCard(Zone.HAND, playerA, "Public Execution"); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 6); + addCard(Zone.BATTLEFIELD, playerA, "Keeper of the Lens", 1); + /* + Misdirection {3}{U}{U} + Instant + You may exile a blue card from your hand rather than pay Misdirection's mana cost. + Change the target of target spell with a single target. + */ + addCard(Zone.HAND, playerB, "Misdirection"); + addCard(Zone.BATTLEFIELD, playerB, "Pillarfield Ox", 1); + addCard(Zone.BATTLEFIELD, playerB, "Custodian of the Trove", 1); // 4/3 + addCard(Zone.BATTLEFIELD, playerB, "Island", 5); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Public Execution", "Custodian of the Trove"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Misdirection", "Public Execution", "Public Execution"); + addTarget(playerB, "Keeper of the Lens"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertGraveyardCount(playerA, "Public Execution", 1); + assertGraveyardCount(playerB, "Misdirection", 1); + assertPermanentCount(playerA, "Keeper of the Lens", 1); + + assertPermanentCount(playerB, "Pillarfield Ox", 1); + assertPowerToughness(playerB, "Pillarfield Ox", 0, 4); + + assertGraveyardCount(playerB, "Custodian of the Trove",1); + + } } \ No newline at end of file diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/triggers/SpellCastTriggerTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/triggers/SpellCastTriggerTest.java index 97da7f949d1..ce7aaa7db61 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/triggers/SpellCastTriggerTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/triggers/SpellCastTriggerTest.java @@ -65,4 +65,32 @@ public class SpellCastTriggerTest extends CardTestPlayerBase { assertPowerToughness(playerA, "Sunscorch Regent", 5, 4); } -} \ No newline at end of file + /** + * Monastery Mentor triggers are causing a "rollback" error. + */ + @Test + public void testMonasteryMentor() { + // Prowess (Whenever you cast a noncreature spell, this creature gets +1/+1 until end of turn.) + // Whenever you cast a noncreature spell, put a 1/1 white Monk creature token with prowess onto the battlefield. + addCard(Zone.BATTLEFIELD, playerA, "Monastery Mentor", 1); + + addCard(Zone.HAND, playerA, "Lightning Bolt", 2); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerB); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Lightning Bolt", playerB); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertLife(playerA, 20); + assertLife(playerB, 14); + + assertGraveyardCount(playerA, "Lightning Bolt", 2); + assertPermanentCount(playerA, "Monk", 2); + assertPowerToughness(playerA, "Monk", 2, 2); + assertPowerToughness(playerA, "Monk", 1, 1); + + assertPowerToughness(playerA, "Monastery Mentor", 4, 4); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java index e538a04b4a5..ecca9bfc4a9 100644 --- a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java +++ b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java @@ -389,6 +389,10 @@ public class TestPlayer extends ComputerPlayer { @Override public boolean chooseTarget(Outcome outcome, Target target, Ability source, Game game) { if (!targets.isEmpty()) { + UUID abilityControllerId = playerId; + if (target.getTargetController() != null && target.getAbilityController() != null) { + abilityControllerId = target.getAbilityController(); + } if ((target instanceof TargetPermanent) || (target instanceof TargetPermanentOrPlayer)) { for (String targetDefinition: targets) { String[] targetList = targetDefinition.split("\\^"); @@ -408,7 +412,7 @@ public class TestPlayer extends ComputerPlayer { } for (Permanent permanent : game.getBattlefield().getAllActivePermanents((FilterPermanent)target.getFilter(), game)) { if (permanent.getName().equals(targetName) || (permanent.getName()+"-"+permanent.getExpansionSetCode()).equals(targetName)) { - if (((TargetPermanent)target).canTarget(source == null ? this.getId(): source.getControllerId(), permanent.getId(), source, game) && !target.getTargets().contains(permanent.getId())) { + if (((TargetPermanent)target).canTarget(abilityControllerId, permanent.getId(), source, game) && !target.getTargets().contains(permanent.getId())) { if ((permanent.isCopy() && !originOnly) || (!permanent.isCopy() && !copyOnly )) { target.add(permanent.getId(), game); targetFound = true; @@ -446,7 +450,7 @@ public class TestPlayer extends ComputerPlayer { for (String targetName: targetList) { for (Card card: this.getHand().getCards(((TargetCardInHand)target).getFilter(), game)) { if (card.getName().equals(targetName) || (card.getName()+"-"+card.getExpansionSetCode()).equals(targetName)) { - if (((TargetCardInHand)target).canTarget(this.getId(), card.getId(), source, game) && !target.getTargets().contains(card.getId())) { + if (((TargetCardInHand)target).canTarget(abilityControllerId, card.getId(), source, game) && !target.getTargets().contains(card.getId())) { target.add(card.getId(), game); targetFound = true; break; diff --git a/Mage/src/mage/abilities/Ability.java b/Mage/src/mage/abilities/Ability.java index a4c67c4dc75..81393981ce2 100644 --- a/Mage/src/mage/abilities/Ability.java +++ b/Mage/src/mage/abilities/Ability.java @@ -537,4 +537,6 @@ public interface Ability extends Controllable, Serializable { */ MageObject getSourceObjectIfItStillExists(Game game); + + String getTargetDescription(Targets targets, Game game); } diff --git a/Mage/src/mage/abilities/AbilityImpl.java b/Mage/src/mage/abilities/AbilityImpl.java index 50f08e26194..844e6de7c48 100644 --- a/Mage/src/mage/abilities/AbilityImpl.java +++ b/Mage/src/mage/abilities/AbilityImpl.java @@ -1074,6 +1074,7 @@ public abstract class AbilityImpl implements Ability { return sb.toString(); } + @Override public String getTargetDescription(Targets targets, Game game) { return getTargetDescriptionForLog(targets, game); } diff --git a/Mage/src/mage/abilities/effects/common/ChooseNewTargetsTargetEffect.java b/Mage/src/mage/abilities/effects/common/ChooseNewTargetsTargetEffect.java index ee08b2d7417..265b66e9cdd 100644 --- a/Mage/src/mage/abilities/effects/common/ChooseNewTargetsTargetEffect.java +++ b/Mage/src/mage/abilities/effects/common/ChooseNewTargetsTargetEffect.java @@ -72,6 +72,7 @@ public class ChooseNewTargetsTargetEffect extends OneShotEffect { super(effect); this.forceChange = effect.forceChange; this.onlyOneTarget = effect.onlyOneTarget; + this.filterNewTarget = effect.filterNewTarget; } @Override diff --git a/Mage/src/mage/game/stack/Spell.java b/Mage/src/mage/game/stack/Spell.java index 4676f22cf88..03c01660280 100644 --- a/Mage/src/mage/game/stack/Spell.java +++ b/Mage/src/mage/game/stack/Spell.java @@ -37,39 +37,33 @@ import mage.Mana; import mage.ObjectColor; import mage.abilities.Abilities; import mage.abilities.Ability; -import mage.abilities.Mode; import mage.abilities.SpellAbility; import mage.abilities.costs.AlternativeSourceCosts; import mage.abilities.costs.Cost; import mage.abilities.costs.mana.ManaCost; import mage.abilities.costs.mana.ManaCosts; -import mage.abilities.dynamicvalue.common.StaticValue; import mage.abilities.keyword.BestowAbility; import mage.abilities.keyword.MorphAbility; import mage.cards.Card; import mage.cards.SplitCard; import mage.constants.CardType; -import mage.constants.Outcome; import mage.constants.Rarity; import mage.constants.SpellAbilityType; import mage.constants.Zone; import mage.counters.Counter; import mage.counters.Counters; -import mage.filter.FilterPermanent; import mage.game.Game; import mage.game.events.ZoneChangeEvent; import mage.game.permanent.Permanent; import mage.game.permanent.PermanentCard; import mage.players.Player; -import mage.target.Target; -import mage.target.TargetAmount; import mage.util.GameLog; /** * * @author BetaSteward_at_googlemail.com */ -public class Spell implements StackObject, Card { +public class Spell extends StackObjImpl implements Card { private final List spellCards = new ArrayList<>(); private final List spellAbilities = new ArrayList<>(); @@ -313,213 +307,8 @@ public class Spell implements StackObject, Card { } } } - - /** - * Choose new targets for the spell - * - * @param game - * @param playerId Player UUID who changes the targets. - * @return - */ - public boolean chooseNewTargets(Game game, UUID playerId) { - return chooseNewTargets(game, playerId, false, false, null); - } - - /** - * 114.6. Some effects allow a player to change the target(s) of a spell or - * ability, and other effects allow a player to choose new targets for a - * spell or ability. - * - * 114.6a If an effect allows a player to "change the - * target(s)" of a spell or ability, each target can be changed only to - * another legal target. If a target can't be changed to another legal - * target, the original target is unchanged, even if the original target is - * itself illegal by then. If all the targets aren't changed to other legal - * targets, none of them are changed. - * - * 114.6b If an effect allows a player to "change a target" of a - * spell or ability, the process described in rule 114.6a - * is followed, except that only one of those targets may be changed - * (rather than all of them or none of them). - * - * 114.6c If an effect allows a - * player to "change any targets" of a spell or ability, the process - * described in rule 114.6a is followed, except that any number of those - * targets may be changed (rather than all of them or none of them). - * - * 114.6d If an effect allows a player to "choose new targets" for a spell or - * ability, the player may leave any number of the targets unchanged, even - * if those targets would be illegal. If the player chooses to change some - * or all of the targets, the new targets must be legal and must not cause - * any unchanged targets to become illegal. - * - * 114.6e When changing targets or - * choosing new targets for a spell or ability, only the final set of - * targets is evaluated to determine whether the change is legal. - * - * Example: Arc Trail is a sorcery that reads "Arc Trail deals 2 damage to - * target creature or player and 1 damage to another target creature or - * player." The current targets of Arc Trail are Runeclaw Bear and Llanowar - * Elves, in that order. You cast Redirect, an instant that reads "You may - * choose new targets for target spell," targeting Arc Trail. You can change - * the first target to Llanowar Elves and change the second target to - * Runeclaw Bear. - * - * 114.7. Modal spells and abilities may have different targeting - * requirements for each mode. An effect that allows a player to change the - * target(s) of a modal spell or ability, or to choose new targets for a - * modal spell or ability, doesn't allow that player to change its mode. - * (See rule 700.2.) - * - * 706.10c Some effects copy a spell or ability and state that its - * controller may choose new targets for the copy. The player may leave any - * number of the targets unchanged, even if those targets would be illegal. - * If the player chooses to change some or all of the targets, the new - * targets must be legal. Once the player has decided what the copy's - * targets will be, the copy is put onto the stack with those targets. - * - * @param game - * @param playerId - player that can/has to change the taregt 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 filterNewTarget restriction for the new target, if null nothing is cheched - * @return - */ - @Override - public boolean chooseNewTargets(Game game, UUID playerId, boolean forceChange, boolean onlyOneTarget, FilterPermanent filterNewTarget) { - Player player = game.getPlayer(playerId); - if (player != null) { - StringBuilder newTargetDescription = new StringBuilder(); - // Fused split spells or spells where "Splice on Arcane" was used can have more than one ability - for (SpellAbility spellAbility : spellAbilities) { - // Some spells can have more than one mode - for (UUID modeId : spellAbility.getModes().getSelectedModes()) { - Mode mode = spellAbility.getModes().get(modeId); - for (Target target : mode.getTargets()) { - Target newTarget = chooseNewTarget(player, spellAbility, mode, target, forceChange, filterNewTarget, game); - // clear the old target and copy all targets from new target - target.clearChosen(); - for (UUID targetId : newTarget.getTargets()) { - target.addTarget(targetId, newTarget.getTargetAmount(targetId), spellAbility, game, false); - } - - } - newTargetDescription.append(getSpellAbility().getTargetDescription(mode.getTargets(), game)); - } - - } - if (newTargetDescription.length() > 0 && !game.isSimulation()) { - game.informPlayers(this.getName() + " is now " + newTargetDescription.toString()); - } - return true; - } - return false; - } - - /** - * Handles the change of one target instance of a mode - * - * @param player - player that can choose the new target - * @param spellAbility - * @param mode - * @param target - * @param forceChange - * @param game - * @return - */ - private Target chooseNewTarget(Player player, SpellAbility spellAbility, Mode mode, Target target, boolean forceChange, FilterPermanent filterNewTarget, Game game) { - Target newTarget = target.copy(); - newTarget.clearChosen(); - for (UUID targetId : target.getTargets()) { - String targetNames = getNamesOftargets(targetId, game); - // change the target? - if (targetNames != null - && (forceChange || player.chooseUse(mode.getEffects().get(0).getOutcome(), "Change this target: " + targetNames + "?", game))) { - // choose exactly one other target - if (forceChange && target.possibleTargets(this.getSourceId(), getControllerId(), game).size() > 1) { // controller of spell must be used (e.g. TargetOpponent) - int iteration = 0; - do { - if (iteration > 0 && !game.isSimulation()) { - game.informPlayer(player, "You may only select exactly one target that must be different from the origin target!"); - } - iteration++; - newTarget.clearChosen(); - // TODO: Distinction between "spell controller" and "player that can change the target" - here player is used for both - newTarget.chooseTarget(mode.getEffects().get(0).getOutcome(), player.getId(), spellAbility, game); - // check target restriction - if (newTarget.getFirstTarget() != null && filterNewTarget != null) { - Permanent newTargetPermanent = game.getPermanent(newTarget.getFirstTarget()); - if (newTargetPermanent == null || !filterNewTarget.match(newTargetPermanent, game)) { - game.informPlayer(player, "Target does not fullfil the target requirements (" + filterNewTarget.getMessage() +")"); - newTarget.clearChosen(); - } - } - } while (player.isInGame() && (targetId.equals(newTarget.getFirstTarget()) || newTarget.getTargets().size() != 1)); - // choose a new target - } else { - // build a target definition with exactly one possible target to select that replaces old target - Target tempTarget = target.copy(); - if (target instanceof TargetAmount) { - ((TargetAmount)tempTarget).setAmountDefinition(new StaticValue(target.getTargetAmount(targetId))); - } - tempTarget.setMinNumberOfTargets(1); - tempTarget.setMaxNumberOfTargets(1); - boolean again; - do { - again = false; - tempTarget.clearChosen(); - if (!tempTarget.chooseTarget(mode.getEffects().get(0).getOutcome(), player.getId(), spellAbility, game)) { - if (player.chooseUse(Outcome.Benefit, "No target object selected. Reset to original target?", game)) { - // use previous target no target was selected - newTarget.addTarget(targetId, target.getTargetAmount(targetId), spellAbility, game, false); - } else { - again = true; - } - } else { - // if possible add the alternate Target - it may not be included in the old definition nor in the already selected targets of the new definition - if (newTarget.getTargets().contains(tempTarget.getFirstTarget()) || target.getTargets().contains(tempTarget.getFirstTarget())) { - if(player.isHuman()) { - game.informPlayer(player, "This target was already selected from origin spell. You can only keep this target!"); - again = true; - } else { - newTarget.addTarget(targetId, target.getTargetAmount(targetId), spellAbility, game, false); - } - } else if (newTarget.getFirstTarget() != null && filterNewTarget != null) { - Permanent newTargetPermanent = game.getPermanent(newTarget.getFirstTarget()); - if (newTargetPermanent == null || !filterNewTarget.match(newTargetPermanent, game)) { - game.informPlayer(player, "This target does not fullfil the target requirements (" + filterNewTarget.getMessage() +")"); - again = true; - } - } else { - // valid target was selected, add it to the new target definition - newTarget.addTarget(tempTarget.getFirstTarget(), target.getTargetAmount(targetId), spellAbility, game, false); - } - } - } while (again && player.isInGame()); - } - } - // keep the target - else { - newTarget.addTarget(targetId, target.getTargetAmount(targetId), spellAbility, game, false); - } - } - return newTarget; - } - - private String getNamesOftargets(UUID targetId, Game game) { - MageObject object = game.getObject(targetId); - String name = null; - if (object == null) { - Player targetPlayer = game.getPlayer(targetId); - if (targetPlayer != null) { - name = targetPlayer.getLogName(); - } - } else { - name = object.getName(); - } - return name; - } + @Override public void counter(UUID sourceId, Game game) { diff --git a/Mage/src/mage/game/stack/StackAbility.java b/Mage/src/mage/game/stack/StackAbility.java index 29804bbf9bc..835d8bf1014 100644 --- a/Mage/src/mage/game/stack/StackAbility.java +++ b/Mage/src/mage/game/stack/StackAbility.java @@ -54,15 +54,10 @@ import mage.target.Targets; import java.util.ArrayList; import java.util.List; import java.util.UUID; -import mage.abilities.dynamicvalue.common.StaticValue; import mage.cards.Card; import mage.constants.AbilityWord; -import mage.constants.Outcome; -import mage.filter.FilterPermanent; import mage.game.events.GameEvent; -import mage.game.permanent.Permanent; import mage.players.Player; -import mage.target.TargetAmount; import mage.util.GameLog; import mage.watchers.Watcher; @@ -70,7 +65,7 @@ import mage.watchers.Watcher; * * @author BetaSteward_at_googlemail.com */ -public class StackAbility implements StackObject, Ability { +public class StackAbility extends StackObjImpl implements Ability { private static List emptyCardType = new ArrayList<>(); private static List emptyString = new ArrayList<>(); @@ -551,197 +546,9 @@ public class StackAbility implements StackObject, Ability { throw new UnsupportedOperationException("Not supported."); } - /** - * 114.6. Some effects allow a player to change the target(s) of a spell or - * ability, and other effects allow a player to choose new targets for a - * spell or ability. - * - * 114.6a If an effect allows a player to "change the - * target(s)" of a spell or ability, each target can be changed only to - * another legal target. If a target can't be changed to another legal - * target, the original target is unchanged, even if the original target is - * itself illegal by then. If all the targets aren't changed to other legal - * targets, none of them are changed. - * - * 114.6b If an effect allows a player to "change a target" of a - * spell or ability, the process described in rule 114.6a - * is followed, except that only one of those targets may be changed - * (rather than all of them or none of them). - * - * 114.6c If an effect allows a - * player to "change any targets" of a spell or ability, the process - * described in rule 114.6a is followed, except that any number of those - * targets may be changed (rather than all of them or none of them). - * - * 114.6d If an effect allows a player to "choose new targets" for a spell or - * ability, the player may leave any number of the targets unchanged, even - * if those targets would be illegal. If the player chooses to change some - * or all of the targets, the new targets must be legal and must not cause - * any unchanged targets to become illegal. - * - * 114.6e When changing targets or - * choosing new targets for a spell or ability, only the final set of - * targets is evaluated to determine whether the change is legal. - * - * Example: Arc Trail is a sorcery that reads "Arc Trail deals 2 damage to - * target creature or player and 1 damage to another target creature or - * player." The current targets of Arc Trail are Runeclaw Bear and Llanowar - * Elves, in that order. You cast Redirect, an instant that reads "You may - * choose new targets for target spell," targeting Arc Trail. You can change - * the first target to Llanowar Elves and change the second target to - * Runeclaw Bear. - * - * 114.7. Modal spells and abilities may have different targeting - * requirements for each mode. An effect that allows a player to change the - * target(s) of a modal spell or ability, or to choose new targets for a - * modal spell or ability, doesn't allow that player to change its mode. - * (See rule 700.2.) - * - * 706.10c Some effects copy a spell or ability and state that its - * controller may choose new targets for the copy. The player may leave any - * number of the targets unchanged, even if those targets would be illegal. - * If the player chooses to change some or all of the targets, the new - * targets must be legal. Once the player has decided what the copy's - * targets will be, the copy is put onto the stack with those targets. - * - * @param game - * @param playerId - player that can/has to change the target of the ability - * @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 filterNewTarget restriction for the new target, if null nothing is cheched - * @return - */ @Override - public boolean chooseNewTargets(Game game, UUID playerId, boolean forceChange, boolean onlyOneTarget, FilterPermanent filterNewTarget) { - Player player = game.getPlayer(playerId); - if (player != null) { - StringBuilder newTargetDescription = new StringBuilder(); - // Some abilities can have more than one mode - for (UUID modeId : ability.getModes().getSelectedModes()) { - Mode mode = ability.getModes().get(modeId); - for (Target target : mode.getTargets()) { - Target newTarget = chooseNewTarget(player, getStackAbility(), mode, target, forceChange, filterNewTarget, game); - // clear the old target and copy all targets from new target - target.clearChosen(); - for (UUID targetId : newTarget.getTargets()) { - target.addTarget(targetId, newTarget.getTargetAmount(targetId), ability, game, false); - } - - } - newTargetDescription.append(((AbilityImpl)ability).getTargetDescription(mode.getTargets(), game)); - } - - if (newTargetDescription.length() > 0 && !game.isSimulation()) { - game.informPlayers(this.getName() + " is now " + newTargetDescription.toString()); - } - return true; - } - return false; + public String getTargetDescription(Targets targets, Game game) { + return getAbilities().get(0).getTargetDescription(targets, game); } - /** - * Handles the change of one target instance of a mode - * - * @param player - player that can choose the new target - * @param ability - * @param mode - * @param target - * @param forceChange - * @param game - * @return - */ - private Target chooseNewTarget(Player player, Ability ability, Mode mode, Target target, boolean forceChange, FilterPermanent filterNewTarget, Game game) { - Target newTarget = target.copy(); - newTarget.clearChosen(); - for (UUID targetId : target.getTargets()) { - String targetNames = getNamesOfTargets(targetId, game); - // change the target? - if (targetNames != null - && (forceChange || player.chooseUse(mode.getEffects().get(0).getOutcome(), "Change this target: " + targetNames + "?", game))) { - // choose exactly one other target - if (forceChange && target.possibleTargets(this.getSourceId(), getControllerId(), game).size() > 1) { // controller of ability must be used (e.g. TargetOpponent) - int iteration = 0; - do { - if (iteration > 0 && !game.isSimulation()) { - game.informPlayer(player, "You may only select exactly one target that must be different from the origin target!"); - } - iteration++; - newTarget.clearChosen(); - // TODO: Distinction between "ability controller" and "player that can change the target" - here player is used for both - newTarget.chooseTarget(mode.getEffects().get(0).getOutcome(), player.getId(), ability, game); - // check target restriction - if (newTarget.getFirstTarget() != null && filterNewTarget != null) { - Permanent newTargetPermanent = game.getPermanent(newTarget.getFirstTarget()); - if (newTargetPermanent == null || !filterNewTarget.match(newTargetPermanent, game)) { - game.informPlayer(player, "Target does not fullfil the target requirements (" + filterNewTarget.getMessage() +")"); - newTarget.clearChosen(); - } - } - } while (player.isInGame() && (targetId.equals(newTarget.getFirstTarget()) || newTarget.getTargets().size() != 1)); - // choose a new target - } else { - // build a target definition with exactly one possible target to select that replaces old target - Target tempTarget = target.copy(); - if (target instanceof TargetAmount) { - ((TargetAmount)tempTarget).setAmountDefinition(new StaticValue(target.getTargetAmount(targetId))); - } - tempTarget.setMinNumberOfTargets(1); - tempTarget.setMaxNumberOfTargets(1); - boolean again; - do { - again = false; - tempTarget.clearChosen(); - if (!tempTarget.chooseTarget(mode.getEffects().get(0).getOutcome(), player.getId(), ability, game)) { - if (player.chooseUse(Outcome.Benefit, "No target object selected. Reset to original target?", game)) { - // use previous target no target was selected - newTarget.addTarget(targetId, target.getTargetAmount(targetId), ability, game, false); - } else { - again = true; - } - } else { - // if possible add the alternate Target - it may not be included in the old definition nor in the already selected targets of the new definition - if (newTarget.getTargets().contains(tempTarget.getFirstTarget()) || target.getTargets().contains(tempTarget.getFirstTarget())) { - if (player.isHuman()) { - game.informPlayer(player, "This target was already selected from origin ability. You can only keep this target!"); - again = true; - } else { - newTarget.addTarget(targetId, target.getTargetAmount(targetId), ability, game, false); - } - } else if (newTarget.getFirstTarget() != null && filterNewTarget != null) { - Permanent newTargetPermanent = game.getPermanent(newTarget.getFirstTarget()); - if (newTargetPermanent == null || !filterNewTarget.match(newTargetPermanent, game)) { - game.informPlayer(player, "This target does not fullfil the target requirements (" + filterNewTarget.getMessage() +")"); - again = true; - } - } else { - // valid target was selected, add it to the new target definition - newTarget.addTarget(tempTarget.getFirstTarget(), target.getTargetAmount(targetId), ability, game, false); - } - } - } while (again && player.isInGame()); - } - } - // keep the target - else { - newTarget.addTarget(targetId, target.getTargetAmount(targetId), ability, game, false); - } - } - return newTarget; - } - - - private String getNamesOfTargets(UUID targetId, Game game) { - MageObject object = game.getObject(targetId); - String targetNames = null; - if (object == null) { - Player targetPlayer = game.getPlayer(targetId); - if (targetPlayer != null) { - targetNames = targetPlayer.getLogName(); - } - } else { - targetNames = object.getName(); - } - return targetNames; - } - } diff --git a/Mage/src/mage/game/stack/StackObjImpl.java b/Mage/src/mage/game/stack/StackObjImpl.java new file mode 100644 index 00000000000..c4889dc1c90 --- /dev/null +++ b/Mage/src/mage/game/stack/StackObjImpl.java @@ -0,0 +1,260 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package mage.game.stack; + +import java.util.Set; +import java.util.UUID; +import mage.MageObject; +import mage.abilities.Abilities; +import mage.abilities.AbilitiesImpl; +import mage.abilities.Ability; +import mage.abilities.Mode; +import mage.abilities.dynamicvalue.common.StaticValue; +import mage.constants.Outcome; +import mage.filter.FilterPermanent; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.Target; +import mage.target.TargetAmount; + +/** + * + * @author LevelX2 + */ +public abstract class StackObjImpl implements StackObject { + + /** + * Choose new targets for a stack Object + * + * @param game + * @param playerId Player UUID who changes the targets. + * @return + */ + public boolean chooseNewTargets(Game game, UUID playerId) { + return chooseNewTargets(game, playerId, false, false, null); + } + + /** + * 114.6. Some effects allow a player to change the target(s) of a spell or + * ability, and other effects allow a player to choose new targets for a + * spell or ability. + * + * 114.6a If an effect allows a player to "change the + * target(s)" of a spell or ability, each target can be changed only to + * another legal target. If a target can't be changed to another legal + * target, the original target is unchanged, even if the original target is + * itself illegal by then. If all the targets aren't changed to other legal + * targets, none of them are changed. + * + * 114.6b If an effect allows a player to "change a target" of a + * spell or ability, the process described in rule 114.6a + * is followed, except that only one of those targets may be changed + * (rather than all of them or none of them). + * + * 114.6c If an effect allows a + * player to "change any targets" of a spell or ability, the process + * described in rule 114.6a is followed, except that any number of those + * targets may be changed (rather than all of them or none of them). + * + * 114.6d If an effect allows a player to "choose new targets" for a spell or + * ability, the player may leave any number of the targets unchanged, even + * if those targets would be illegal. If the player chooses to change some + * or all of the targets, the new targets must be legal and must not cause + * any unchanged targets to become illegal. + * + * 114.6e When changing targets or + * choosing new targets for a spell or ability, only the final set of + * targets is evaluated to determine whether the change is legal. + * + * Example: Arc Trail is a sorcery that reads "Arc Trail deals 2 damage to + * target creature or player and 1 damage to another target creature or + * player." The current targets of Arc Trail are Runeclaw Bear and Llanowar + * Elves, in that order. You cast Redirect, an instant that reads "You may + * choose new targets for target spell," targeting Arc Trail. You can change + * the first target to Llanowar Elves and change the second target to + * Runeclaw Bear. + * + * 114.7. Modal spells and abilities may have different targeting + * requirements for each mode. An effect that allows a player to change the + * target(s) of a modal spell or ability, or to choose new targets for a + * modal spell or ability, doesn't allow that player to change its mode. + * (See rule 700.2.) + * + * 706.10c Some effects copy a spell or ability and state that its + * controller may choose new targets for the copy. The player may leave any + * number of the targets unchanged, even if those targets would be illegal. + * If the player chooses to change some or all of the targets, the new + * targets must be legal. Once the player has decided what the copy's + * 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 filterNewTarget restriction for the new target, if null nothing is cheched + * @return + */ + @Override + public boolean chooseNewTargets(Game game, UUID targetControllerId, boolean forceChange, boolean onlyOneTarget, FilterPermanent filterNewTarget) { + Player targetController = game.getPlayer(targetControllerId); + if (targetController != null) { + StringBuilder newTargetDescription = new StringBuilder(); + // Fused split spells or spells where "Splice on Arcane" was used can have more than one ability + Abilities objectAbilities = new AbilitiesImpl<>(); + if (this instanceof Spell) { + objectAbilities.addAll(((Spell)this).getSpellAbilities()); + } else { + objectAbilities.addAll(getAbilities()); + } + for (Ability ability : objectAbilities) { + // Some spells can have more than one mode + for (UUID modeId : ability.getModes().getSelectedModes()) { + Mode mode = ability.getModes().get(modeId); + for (Target target : mode.getTargets()) { + Target newTarget = chooseNewTarget(targetController, ability, mode, target, forceChange, filterNewTarget, game); + // clear the old target and copy all targets from new target + target.clearChosen(); + for (UUID targetId : newTarget.getTargets()) { + target.addTarget(targetId, newTarget.getTargetAmount(targetId), ability, game, false); + } + + } + newTargetDescription.append(ability.getTargetDescription(mode.getTargets(), game)); + } + + } + if (newTargetDescription.length() > 0 && !game.isSimulation()) { + game.informPlayers(this.getLogName() + " is now " + newTargetDescription.toString()); + } + return true; + } + return false; + } + + /** + * Handles the change of one target instance of a mode + * + * @param targetController - player that can choose the new target + * @param ability + * @param mode + * @param target + * @param forceChange + * @param game + * @return + */ + private Target chooseNewTarget(Player targetController, Ability ability, Mode mode, Target target, boolean forceChange, FilterPermanent filterNewTarget, Game game) { + Target newTarget = target.copy(); + if (!targetController.getId().equals(getControllerId())) { + newTarget.setTargetController(targetController.getId()); // target controller for the change is different from spell controller + newTarget.setAbilityController(getControllerId()); + } + newTarget.clearChosen(); + for (UUID targetId : target.getTargets()) { + String targetNames = getNamesOftargets(targetId, game); + // change the target? + if (targetNames != null + && (forceChange || targetController.chooseUse(mode.getEffects().get(0).getOutcome(), "Change this target: " + targetNames + "?", game))) { + Set possibleTargets = target.possibleTargets(this.getSourceId(), getControllerId(), game); + // choose exactly one other target - already targeted objects are not counted + if (forceChange && possibleTargets != null && possibleTargets.size() > 1) { // controller of spell must be used (e.g. TargetOpponent) + int iteration = 0; + do { + if (iteration > 0 && !game.isSimulation()) { + game.informPlayer(targetController, "You may only select exactly one target that must be different from the origin target!"); + } + iteration++; + newTarget.clearChosen(); + + newTarget.chooseTarget(mode.getEffects().get(0).getOutcome(), getControllerId(), ability, game); + // check target restriction + if (newTarget.getFirstTarget() != null && filterNewTarget != null) { + Permanent newTargetPermanent = game.getPermanent(newTarget.getFirstTarget()); + if (newTargetPermanent == null || !filterNewTarget.match(newTargetPermanent, game)) { + game.informPlayer(targetController, "Target does not fullfil the target requirements (" + filterNewTarget.getMessage() +")"); + newTarget.clearChosen(); + } + } + } while (targetController.isInGame() && (targetId.equals(newTarget.getFirstTarget()) || newTarget.getTargets().size() != 1)); + // choose a new target + } else { + // build a target definition with exactly one possible target to select that replaces old target + Target tempTarget = target.copy(); + if (target instanceof TargetAmount) { + ((TargetAmount)tempTarget).setAmountDefinition(new StaticValue(target.getTargetAmount(targetId))); + } + tempTarget.setMinNumberOfTargets(1); + tempTarget.setMaxNumberOfTargets(1); + if (!targetController.getId().equals(getControllerId())) { + tempTarget.setTargetController(targetController.getId()); + tempTarget.setAbilityController(getControllerId()); + } + boolean again; + do { + again = false; + tempTarget.clearChosen(); + if (!tempTarget.chooseTarget(mode.getEffects().get(0).getOutcome(), getControllerId(), ability, game)) { + if (targetController.chooseUse(Outcome.Benefit, "No target object selected. Reset to original target?", game)) { + // use previous target no target was selected + newTarget.addTarget(targetId, target.getTargetAmount(targetId), ability, game, false); + } else { + again = true; + } + } else { + // if possible add the alternate Target - it may not be included in the old definition nor in the already selected targets of the new definition + if (newTarget.getTargets().contains(tempTarget.getFirstTarget()) || target.getTargets().contains(tempTarget.getFirstTarget())) { + if(targetController.isHuman()) { + game.informPlayer(targetController, "This target was already selected from origin spell. You can only keep this target!"); + again = true; + } else { + newTarget.addTarget(targetId, target.getTargetAmount(targetId), ability, game, false); + } + } else if (!target.canTarget(getControllerId(), tempTarget.getFirstTarget(), ability, game)) { + if(targetController.isHuman()) { + game.informPlayer(targetController, "This target is not valid!"); + again = true; + } else { + // keep the old + newTarget.addTarget(targetId, target.getTargetAmount(targetId), ability, game, false); + } + } else if (newTarget.getFirstTarget() != null && filterNewTarget != null) { + Permanent newTargetPermanent = game.getPermanent(newTarget.getFirstTarget()); + if (newTargetPermanent == null || !filterNewTarget.match(newTargetPermanent, game)) { + game.informPlayer(targetController, "This target does not fullfil the target requirements (" + filterNewTarget.getMessage() +")"); + again = true; + } + } else { + // valid target was selected, add it to the new target definition + newTarget.addTarget(tempTarget.getFirstTarget(), target.getTargetAmount(targetId), ability, game, false); + } + } + } while (again && targetController.isInGame()); + } + } + // keep the target + else { + newTarget.addTarget(targetId, target.getTargetAmount(targetId), ability, game, false); + } + } + return newTarget; + } + + + private String getNamesOftargets(UUID targetId, Game game) { + MageObject object = game.getObject(targetId); + String name = null; + if (object == null) { + Player targetPlayer = game.getPlayer(targetId); + if (targetPlayer != null) { + name = targetPlayer.getLogName(); + } + } else { + name = object.getName(); + } + return name; + } + +} diff --git a/Mage/src/mage/players/PlayerImpl.java b/Mage/src/mage/players/PlayerImpl.java index ef6a0004790..16ce833b928 100644 --- a/Mage/src/mage/players/PlayerImpl.java +++ b/Mage/src/mage/players/PlayerImpl.java @@ -130,7 +130,6 @@ import mage.target.common.TargetCardInLibrary; import mage.target.common.TargetDiscard; import mage.util.CardUtil; import mage.util.GameLog; -import mage.watchers.common.BloodthirstWatcher; import org.apache.log4j.Logger; @@ -690,7 +689,7 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public Cards discard(int amount, boolean random, Ability source, Game game) { Cards discardedCards = new CardsImpl(); - if (amount >= this.getHand().size()) { + if (this.getHand().size() == 1) { discardedCards.addAll(this.getHand()); while (this.getHand().size() > 0) { discard(this.getHand().get(this.getHand().iterator().next(), game), source, game); diff --git a/Mage/src/mage/target/Target.java b/Mage/src/mage/target/Target.java index c1c5ea41846..0ac5a3c43ba 100644 --- a/Mage/src/mage/target/Target.java +++ b/Mage/src/mage/target/Target.java @@ -38,6 +38,7 @@ import java.io.Serializable; import java.util.List; import java.util.Set; import java.util.UUID; +import mage.players.Player; /** * @@ -108,4 +109,7 @@ public interface Target extends Serializable { // some targets are choosen from players that are not the controller of the ability (e.g. Pandemonium) void setTargetController(UUID playerId); UUID getTargetController(); + void setAbilityController(UUID playerId); + UUID getAbilityController(); + Player getTargetController(Game game, UUID playerId); } diff --git a/Mage/src/mage/target/TargetAmount.java b/Mage/src/mage/target/TargetAmount.java index e9060539e22..36cbb56ebc4 100644 --- a/Mage/src/mage/target/TargetAmount.java +++ b/Mage/src/mage/target/TargetAmount.java @@ -117,10 +117,9 @@ public abstract class TargetAmount extends TargetImpl { if (!amountWasSet) { setAmount(source, game); } - Player player = game.getPlayer(playerId); chosen = remainingAmount == 0; while (remainingAmount > 0) { - if (!player.chooseTargetAmount(outcome, this, source, game)) { + if (!getTargetController(game, playerId).chooseTargetAmount(outcome, this, source, game)) { return chosen; } chosen = remainingAmount == 0; diff --git a/Mage/src/mage/target/TargetImpl.java b/Mage/src/mage/target/TargetImpl.java index c6d46a94c66..ab3eb6ef68e 100644 --- a/Mage/src/mage/target/TargetImpl.java +++ b/Mage/src/mage/target/TargetImpl.java @@ -61,6 +61,7 @@ public abstract class TargetImpl implements Target { protected boolean notTarget = false; protected boolean atRandom = false; protected UUID targetController = null; // if null the ability controller is the targetController + protected UUID abilityController = null; // only used if target controller != ability controller @Override public abstract TargetImpl copy(); @@ -86,6 +87,7 @@ public abstract class TargetImpl implements Target { this.atRandom = target.atRandom; this.notTarget = target.notTarget; this.targetController = target.targetController; + this.abilityController = target.abilityController; } @Override @@ -110,6 +112,12 @@ public abstract class TargetImpl implements Target { @Override public String getMessage() { + String suffix = ""; + if (targetController != null) { + // Hint for the selecting player that the targets must be valid from the point of the ability controller + // e.g. select opponent text may be misleading otherwise + suffix = " (target controlling!)"; + } if (getMaxNumberOfTargets() != 1) { StringBuilder sb = new StringBuilder(); sb.append("Select ").append(targetName); @@ -118,12 +126,13 @@ public abstract class TargetImpl implements Target { } else { sb.append(" (").append(targets.size()).append(")"); } + sb.append(suffix); return sb.toString(); } if (targetName.startsWith("another") || targetName.startsWith("a ") || targetName.startsWith("an ")) { - return "Select " + targetName; + return "Select " + targetName + suffix; } - return "Select a " + targetName; + return "Select a " + targetName + suffix; } @Override @@ -298,7 +307,6 @@ public abstract class TargetImpl implements Target { @Override public boolean chooseTarget(Outcome outcome, UUID playerId, Ability source, Game game) { - Player player = game.getPlayer(playerId); while (!isChosen() && !doneChosing()) { chosen = targets.size() >= getNumberOfTargets(); if (isRandom()) { @@ -316,7 +324,7 @@ public abstract class TargetImpl implements Target { return chosen; } } else { - if (!player.chooseTarget(outcome, this, source, game)) { + if (!getTargetController(game, playerId).chooseTarget(outcome, this, source, game)) { return chosen; } } @@ -433,5 +441,23 @@ public abstract class TargetImpl implements Target { return targetController; } + @Override + public void setAbilityController(UUID playerId) { + this.abilityController = playerId; + } + @Override + public UUID getAbilityController() { + return abilityController; + } + + @Override + public Player getTargetController(Game game, UUID playerId) { + if (getTargetController() != null) { + return game.getPlayer(getTargetController()); + } else { + return game.getPlayer(playerId); + } + } + }