From bb0a9955415632af73d7faa6aff26ff09cde351c Mon Sep 17 00:00:00 2001 From: Daniel Bomar Date: Mon, 22 Feb 2021 13:06:43 -0600 Subject: [PATCH] Tibalt, Cosmic Impostor - fixed that emblem can't cast not owned cards (#7598) * Fixed ability.canChooseTarget not using correct playerId * Fixed Necrotic Plague * Revert "Fixed Necrotic Plague" This reverts commit 7659039670293ce1ea428dad042511d9d75f9da6. * Set target controller on Necrotic Plague and add check in canChooseTarget * Add test for Tibalt + Ephemerate interaction * Tests improved Co-authored-by: Oleg Agafonov --- .../mage/player/ai/SimulatedPlayerMCTS.java | 2 +- Mage.Sets/src/mage/cards/l/LivingLore.java | 2 +- .../src/mage/cards/m/MaelstromArchangel.java | 2 +- .../src/mage/cards/m/MizzixsMastery.java | 4 +-- .../src/mage/cards/n/NecroticPlague.java | 1 + .../src/mage/cards/o/OmnispellAdept.java | 2 +- Mage.Sets/src/mage/cards/o/OracleOfBones.java | 2 +- .../src/mage/cards/p/PossibilityStorm.java | 2 +- Mage.Sets/src/mage/cards/s/Sunforger.java | 2 +- .../cards/single/khm/ValkiGodOfLiesTest.java | 35 +++++++++++++++++++ .../cards/single/m11/NecroticPlagueTest.java | 7 +++- .../org/mage/test/player/RandomPlayer.java | 2 +- .../src/main/java/mage/abilities/Ability.java | 2 +- .../main/java/mage/abilities/AbilityImpl.java | 22 +++++++++--- .../mage/abilities/ActivatedAbilityImpl.java | 2 +- .../java/mage/abilities/SpellAbility.java | 6 ++-- .../cost/CastWithoutPayingManaCostEffect.java | 2 +- .../java/mage/game/stack/StackAbility.java | 4 +-- .../main/java/mage/players/PlayerImpl.java | 12 +++---- .../targetadjustment/TargetAdjuster.java | 2 +- 20 files changed, 84 insertions(+), 31 deletions(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/khm/ValkiGodOfLiesTest.java diff --git a/Mage.Server.Plugins/Mage.Player.AIMCTS/src/mage/player/ai/SimulatedPlayerMCTS.java b/Mage.Server.Plugins/Mage.Player.AIMCTS/src/mage/player/ai/SimulatedPlayerMCTS.java index 031eaf16f4..31b8ed4bac 100644 --- a/Mage.Server.Plugins/Mage.Player.AIMCTS/src/mage/player/ai/SimulatedPlayerMCTS.java +++ b/Mage.Server.Plugins/Mage.Player.AIMCTS/src/mage/player/ai/SimulatedPlayerMCTS.java @@ -118,7 +118,7 @@ public class SimulatedPlayerMCTS extends MCTSPlayer { @Override public boolean triggerAbility(TriggeredAbility source, Game game) { // logger.info("trigger"); - if (source != null && source.canChooseTarget(game)) { + if (source != null && source.canChooseTarget(game, playerId)) { Ability ability; List options = getPlayableOptions(source, game); if (options.isEmpty()) { diff --git a/Mage.Sets/src/mage/cards/l/LivingLore.java b/Mage.Sets/src/mage/cards/l/LivingLore.java index 2d134f83b2..484c58b187 100644 --- a/Mage.Sets/src/mage/cards/l/LivingLore.java +++ b/Mage.Sets/src/mage/cards/l/LivingLore.java @@ -183,7 +183,7 @@ class LivingLoreSacrificeEffect extends OneShotEffect { } } if (exiledCard != null) { - if (exiledCard.getSpellAbility().canChooseTarget(game)) { + if (exiledCard.getSpellAbility().canChooseTarget(game, controller.getId())) { game.getState().setValue("PlayFromNotOwnHandZone" + exiledCard.getId(), Boolean.TRUE); controller.cast(controller.chooseAbilityForCast(exiledCard, game, true), game, true, new ApprovingObject(source, game)); diff --git a/Mage.Sets/src/mage/cards/m/MaelstromArchangel.java b/Mage.Sets/src/mage/cards/m/MaelstromArchangel.java index 6753e8642b..2385d6d596 100644 --- a/Mage.Sets/src/mage/cards/m/MaelstromArchangel.java +++ b/Mage.Sets/src/mage/cards/m/MaelstromArchangel.java @@ -81,7 +81,7 @@ class MaelstromArchangelCastEffect extends OneShotEffect { while (controller.canRespond() && !cancel) { if (controller.chooseTarget(outcome, target, source, game)) { cardToCast = game.getCard(target.getFirstTarget()); - if (cardToCast != null && cardToCast.getSpellAbility().canChooseTarget(game)) { + if (cardToCast != null && cardToCast.getSpellAbility().canChooseTarget(game, controller.getId())) { cancel = true; } } else { diff --git a/Mage.Sets/src/mage/cards/m/MizzixsMastery.java b/Mage.Sets/src/mage/cards/m/MizzixsMastery.java index f87327b2fb..1bde17c138 100644 --- a/Mage.Sets/src/mage/cards/m/MizzixsMastery.java +++ b/Mage.Sets/src/mage/cards/m/MizzixsMastery.java @@ -79,7 +79,7 @@ class MizzixsMasteryEffect extends OneShotEffect { if (card != null) { if (controller.moveCards(card, Zone.EXILED, source, game)) { Card cardCopy = game.copyCard(card, source, source.getControllerId()); - if (cardCopy.getSpellAbility().canChooseTarget(game) + if (cardCopy.getSpellAbility().canChooseTarget(game, controller.getId()) && controller.chooseUse(outcome, "Cast copy of " + card.getName() + " without paying its mana cost?", source, game)) { game.getState().setValue("PlayFromNotOwnHandZone" + cardCopy.getId(), Boolean.TRUE); @@ -135,7 +135,7 @@ class MizzixsMasteryOverloadEffect extends OneShotEffect { if (controller.chooseTarget(Outcome.PlayForFree, copiedCards, targetCard, source, game)) { Card selectedCard = game.getCard(targetCard.getFirstTarget()); if (selectedCard != null - && selectedCard.getSpellAbility().canChooseTarget(game)) { + && selectedCard.getSpellAbility().canChooseTarget(game, controller.getId())) { game.getState().setValue("PlayFromNotOwnHandZone" + selectedCard.getId(), Boolean.TRUE); controller.cast(controller.chooseAbilityForCast(selectedCard, game, true), game, true, new ApprovingObject(source, game)); diff --git a/Mage.Sets/src/mage/cards/n/NecroticPlague.java b/Mage.Sets/src/mage/cards/n/NecroticPlague.java index 1365481d5a..1e279b75a3 100644 --- a/Mage.Sets/src/mage/cards/n/NecroticPlague.java +++ b/Mage.Sets/src/mage/cards/n/NecroticPlague.java @@ -85,6 +85,7 @@ enum NecroticPlagueAdjuster implements TargetAdjuster { ability.setControllerId(creatureController.getId()); ability.getTargets().clear(); TargetPermanent target = new TargetOpponentsCreaturePermanent(); + target.setTargetController(creatureController.getId()); ability.getTargets().add(target); } } diff --git a/Mage.Sets/src/mage/cards/o/OmnispellAdept.java b/Mage.Sets/src/mage/cards/o/OmnispellAdept.java index 36f8927251..d012b3739a 100644 --- a/Mage.Sets/src/mage/cards/o/OmnispellAdept.java +++ b/Mage.Sets/src/mage/cards/o/OmnispellAdept.java @@ -97,7 +97,7 @@ class OmnispellAdeptEffect extends OneShotEffect { } realFilter.add(Predicates.not(new CardIdPredicate(cardToCast.getId()))); // remove card from choose dialog (infinite fix) - if (!cardToCast.getSpellAbility().canChooseTarget(game)) { + if (!cardToCast.getSpellAbility().canChooseTarget(game, controller.getId())) { continue; } diff --git a/Mage.Sets/src/mage/cards/o/OracleOfBones.java b/Mage.Sets/src/mage/cards/o/OracleOfBones.java index d6877e4311..e101d727b3 100644 --- a/Mage.Sets/src/mage/cards/o/OracleOfBones.java +++ b/Mage.Sets/src/mage/cards/o/OracleOfBones.java @@ -95,7 +95,7 @@ class OracleOfBonesCastEffect extends OneShotEffect { if (controller.chooseTarget(outcome, target, source, game)) { cardToCast = game.getCard(target.getFirstTarget()); if (cardToCast != null - && cardToCast.getSpellAbility().canChooseTarget(game)) { + && cardToCast.getSpellAbility().canChooseTarget(game, controller.getId())) { cancel = true; } } else { diff --git a/Mage.Sets/src/mage/cards/p/PossibilityStorm.java b/Mage.Sets/src/mage/cards/p/PossibilityStorm.java index 4d50d90d98..6593c79dcf 100644 --- a/Mage.Sets/src/mage/cards/p/PossibilityStorm.java +++ b/Mage.Sets/src/mage/cards/p/PossibilityStorm.java @@ -127,7 +127,7 @@ class PossibilityStormEffect extends OneShotEffect { if (card != null && sharesType(card, spell.getCardType()) && !card.isLand() - && card.getSpellAbility().canChooseTarget(game)) { + && card.getSpellAbility().canChooseTarget(game, spellController.getId())) { if (spellController.chooseUse(Outcome.PlayForFree, "Cast " + card.getLogName() + " without paying cost?", source, game)) { spellController.cast(card.getSpellAbility(), game, true, new ApprovingObject(source, game)); } diff --git a/Mage.Sets/src/mage/cards/s/Sunforger.java b/Mage.Sets/src/mage/cards/s/Sunforger.java index ed391e9268..6b2d32dee7 100644 --- a/Mage.Sets/src/mage/cards/s/Sunforger.java +++ b/Mage.Sets/src/mage/cards/s/Sunforger.java @@ -179,7 +179,7 @@ class CardCanBeCastPredicate implements Predicate { SpellAbility ability = input.getSpellAbility().copy(); ability.setControllerId(controllerId); input.adjustTargets(ability, game); - return ability.canChooseTarget(game); + return ability.canChooseTarget(game, controllerId); } @Override diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/khm/ValkiGodOfLiesTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/khm/ValkiGodOfLiesTest.java new file mode 100644 index 0000000000..77ed48941e --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/khm/ValkiGodOfLiesTest.java @@ -0,0 +1,35 @@ +package org.mage.test.cards.single.khm; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +public class ValkiGodOfLiesTest extends CardTestPlayerBase { + + @Test + public void ephmerateTest() { + removeAllCardsFromLibrary(playerB); + + addCard(Zone.BATTLEFIELD, playerA, "Badlands", 7); + addCard(Zone.HAND, playerA, "Plains"); + addCard(Zone.HAND, playerA, "Valki, God of Lies"); + addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears"); + addCard(Zone.LIBRARY, playerB, "Ephemerate"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Tibalt, Cosmic Impostor"); + setChoice(playerA, "Tibalt, Cosmic Impostor"); // two etb effects + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "+2: Exile the top card of each player's library."); + playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Plains"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Ephemerate", "Grizzly Bears"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Tibalt, Cosmic Impostor", 1); + assertPermanentCount(playerA, "Grizzly Bears", 1); + assertGraveyardCount(playerB, "Ephemerate", 1); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/m11/NecroticPlagueTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/m11/NecroticPlagueTest.java index 4908839d0c..1a8cb041f2 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/m11/NecroticPlagueTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/m11/NecroticPlagueTest.java @@ -22,8 +22,10 @@ public class NecroticPlagueTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Necrotic Plague", "Sejiri Merfolk"); + setStrictChooseMode(true); setStopAt(2, PhaseStep.PRECOMBAT_MAIN); execute(); + assertAllCommandsUsed(); assertLife(playerA, 20); assertLife(playerB, 20); @@ -38,7 +40,7 @@ public class NecroticPlagueTest extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerA, "Swamp", 4); /** - * Goblin Deathraiders English + * Goblin Deathraiders * Creature — Goblin Warrior 3/1, BR * Trample */ @@ -63,9 +65,12 @@ public class NecroticPlagueTest extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerB, "Sejiri Merfolk"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Necrotic Plague", "Sejiri Merfolk"); + addTarget(playerB, "Goblin Deathraiders"); // target for new necro attach + setStrictChooseMode(true); setStopAt(3, PhaseStep.PRECOMBAT_MAIN); execute(); + assertAllCommandsUsed(); assertLife(playerA, 20); assertLife(playerB, 20); diff --git a/Mage.Tests/src/test/java/org/mage/test/player/RandomPlayer.java b/Mage.Tests/src/test/java/org/mage/test/player/RandomPlayer.java index 8c2d4f2562..c805400875 100644 --- a/Mage.Tests/src/test/java/org/mage/test/player/RandomPlayer.java +++ b/Mage.Tests/src/test/java/org/mage/test/player/RandomPlayer.java @@ -123,7 +123,7 @@ public class RandomPlayer extends ComputerPlayer { @Override public boolean triggerAbility(TriggeredAbility source, Game game) { - if (source != null && source.canChooseTarget(game)) { + if (source != null && source.canChooseTarget(game, playerId)) { Ability ability; List options = getPlayableOptions(source, game); if (options.isEmpty()) { diff --git a/Mage/src/main/java/mage/abilities/Ability.java b/Mage/src/main/java/mage/abilities/Ability.java index add89e498d..244a4d33b1 100644 --- a/Mage/src/main/java/mage/abilities/Ability.java +++ b/Mage/src/main/java/mage/abilities/Ability.java @@ -320,7 +320,7 @@ public interface Ability extends Controllable, Serializable { Modes getModes(); - boolean canChooseTarget(Game game); + boolean canChooseTarget(Game game, UUID playerId); /** * Gets the list of sub-abilities associated with this ability. diff --git a/Mage/src/main/java/mage/abilities/AbilityImpl.java b/Mage/src/main/java/mage/abilities/AbilityImpl.java index ebf5bd8031..ccf8f89897 100644 --- a/Mage/src/main/java/mage/abilities/AbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/AbilityImpl.java @@ -901,24 +901,36 @@ public abstract class AbilityImpl implements Ability { } @Override - public boolean canChooseTarget(Game game) { + public boolean canChooseTarget(Game game, UUID playerId) { if (this instanceof SpellAbility) { if (SpellAbilityType.SPLIT_FUSED.equals(((SpellAbility) this).getSpellAbilityType())) { Card card = game.getCard(getSourceId()); if (card != null) { - return canChooseTargetAbility(((SplitCard) card).getLeftHalfCard().getSpellAbility(), game, getControllerId()) - && canChooseTargetAbility(((SplitCard) card).getRightHalfCard().getSpellAbility(), game, getControllerId()); + return canChooseTargetAbility(((SplitCard) card).getLeftHalfCard().getSpellAbility(), game, playerId) + && canChooseTargetAbility(((SplitCard) card).getRightHalfCard().getSpellAbility(), game, playerId); } return false; } } - return canChooseTargetAbility(this, game, getControllerId()); + return canChooseTargetAbility(this, game, playerId); } private static boolean canChooseTargetAbility(Ability ability, Game game, UUID controllerId) { int found = 0; for (Mode mode : ability.getModes().values()) { - if (mode.getTargets().canChoose(ability.getSourceId(), ability.getControllerId(), game)) { + boolean validTargets = true; + for (Target target : mode.getTargets()) { + UUID abilityControllerId = controllerId; + if (target.getTargetController() != null) { + abilityControllerId = target.getTargetController(); + } + if (!target.canChoose(ability.getSourceId(), abilityControllerId, game)) { + validTargets = false; + break; + } + } + + if (validTargets) { found++; if (ability.getModes().isEachModeMoreThanOnce()) { return true; diff --git a/Mage/src/main/java/mage/abilities/ActivatedAbilityImpl.java b/Mage/src/main/java/mage/abilities/ActivatedAbilityImpl.java index 9abeadaf0f..639287d9c8 100644 --- a/Mage/src/main/java/mage/abilities/ActivatedAbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/ActivatedAbilityImpl.java @@ -194,7 +194,7 @@ public abstract class ActivatedAbilityImpl extends AbilityImpl implements Activa || game.canPlaySorcery(playerId) || null != approvingObject) { if (costs.canPay(this, this, playerId, game) - && canChooseTarget(game)) { + && canChooseTarget(game, playerId)) { this.activatorId = playerId; return new ActivationStatus(true, approvingObject); } diff --git a/Mage/src/main/java/mage/abilities/SpellAbility.java b/Mage/src/main/java/mage/abilities/SpellAbility.java index c44baebfe6..07fc16a999 100644 --- a/Mage/src/main/java/mage/abilities/SpellAbility.java +++ b/Mage/src/main/java/mage/abilities/SpellAbility.java @@ -118,13 +118,13 @@ public class SpellAbility extends ActivatedAbilityImpl { // fused can be called from hand only, so not permitting object allows or other zones checks // see https://www.mtgsalvation.com/forums/magic-fundamentals/magic-rulings/magic-rulings-archives/251926-snapcaster-mage-and-fuse if (game.getState().getZone(splitCard.getId()) == Zone.HAND) { - return new ActivationStatus(splitCard.getLeftHalfCard().getSpellAbility().canChooseTarget(game) - && splitCard.getRightHalfCard().getSpellAbility().canChooseTarget(game), null); + return new ActivationStatus(splitCard.getLeftHalfCard().getSpellAbility().canChooseTarget(game, playerId) + && splitCard.getRightHalfCard().getSpellAbility().canChooseTarget(game, playerId), null); } } return ActivationStatus.getFalse(); } else { - return new ActivationStatus(canChooseTarget(game), approvingObject); + return new ActivationStatus(canChooseTarget(game, playerId), approvingObject); } } } diff --git a/Mage/src/main/java/mage/abilities/effects/common/cost/CastWithoutPayingManaCostEffect.java b/Mage/src/main/java/mage/abilities/effects/common/cost/CastWithoutPayingManaCostEffect.java index f7da8cee55..400db1679e 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/cost/CastWithoutPayingManaCostEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/cost/CastWithoutPayingManaCostEffect.java @@ -76,7 +76,7 @@ public class CastWithoutPayingManaCostEffect extends OneShotEffect { + cardToCast.getName() + " is no land and has no spell ability!"); cancel = true; } - if (cardToCast.getSpellAbility().canChooseTarget(game)) { + if (cardToCast.getSpellAbility().canChooseTarget(game, controller.getId())) { cancel = true; } } diff --git a/Mage/src/main/java/mage/game/stack/StackAbility.java b/Mage/src/main/java/mage/game/stack/StackAbility.java index 12a1efe7e1..fea7c124ab 100644 --- a/Mage/src/main/java/mage/game/stack/StackAbility.java +++ b/Mage/src/main/java/mage/game/stack/StackAbility.java @@ -437,8 +437,8 @@ public class StackAbility extends StackObjImpl implements Ability { } @Override - public boolean canChooseTarget(Game game) { - return ability.canChooseTarget(game); + public boolean canChooseTarget(Game game, UUID playerId) { + return ability.canChooseTarget(game, playerId); } @Override diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index a400dd71a5..a62ee452c6 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -1495,7 +1495,7 @@ public abstract class PlayerImpl implements Player, Serializable { if (sourceObject != null) { sourceObject.adjustTargets(ability, game); } - if (ability.canChooseTarget(game)) { + if (ability.canChooseTarget(game, playerId)) { if (ability.isUsesStack()) { game.getStack().push(new StackAbility(ability, playerId)); } @@ -1539,28 +1539,28 @@ public abstract class PlayerImpl implements Player, Serializable { return useable; case SPLIT_FUSED: if (zone == Zone.HAND) { - if (ability.canChooseTarget(game)) { + if (ability.canChooseTarget(game, playerId)) { useable.put(ability.getId(), (SpellAbility) ability); } } case SPLIT: - if (((SplitCard) object).getLeftHalfCard().getSpellAbility().canChooseTarget(game)) { + if (((SplitCard) object).getLeftHalfCard().getSpellAbility().canChooseTarget(game, playerId)) { useable.put(((SplitCard) object).getLeftHalfCard().getSpellAbility().getId(), ((SplitCard) object).getLeftHalfCard().getSpellAbility()); } - if (((SplitCard) object).getRightHalfCard().getSpellAbility().canChooseTarget(game)) { + if (((SplitCard) object).getRightHalfCard().getSpellAbility().canChooseTarget(game, playerId)) { useable.put(((SplitCard) object).getRightHalfCard().getSpellAbility().getId(), ((SplitCard) object).getRightHalfCard().getSpellAbility()); } return useable; case SPLIT_AFTERMATH: if (zone == Zone.GRAVEYARD) { - if (((SplitCard) object).getRightHalfCard().getSpellAbility().canChooseTarget(game)) { + if (((SplitCard) object).getRightHalfCard().getSpellAbility().canChooseTarget(game, playerId)) { useable.put(((SplitCard) object).getRightHalfCard().getSpellAbility().getId(), ((SplitCard) object).getRightHalfCard().getSpellAbility()); } } else { - if (((SplitCard) object).getLeftHalfCard().getSpellAbility().canChooseTarget(game)) { + if (((SplitCard) object).getLeftHalfCard().getSpellAbility().canChooseTarget(game, playerId)) { useable.put(((SplitCard) object).getLeftHalfCard().getSpellAbility().getId(), ((SplitCard) object).getLeftHalfCard().getSpellAbility()); } diff --git a/Mage/src/main/java/mage/target/targetadjustment/TargetAdjuster.java b/Mage/src/main/java/mage/target/targetadjustment/TargetAdjuster.java index d3e84a8e9f..af173fa18f 100644 --- a/Mage/src/main/java/mage/target/targetadjustment/TargetAdjuster.java +++ b/Mage/src/main/java/mage/target/targetadjustment/TargetAdjuster.java @@ -4,10 +4,10 @@ import mage.abilities.Ability; import mage.game.Game; /** - * * @author TheElk801 */ public interface TargetAdjuster { void adjustTargets(Ability ability, Game game); + }