From d2d892a7cbb736361b25aa6b54be3a2400f0b063 Mon Sep 17 00:00:00 2001 From: LevelX2 Date: Sat, 27 Jun 2020 23:47:04 +0200 Subject: [PATCH 1/5] * Fixed that permanents under non owner control sine they are on the battlefield were no exiled if the controller left the game (e.g. Captive Audience) (fixes #5593). --- .../multiplayer/PlayerLeftGameRange1Test.java | 53 +++++- .../base/impl/CardTestPlayerAPIImpl.java | 178 +++++++++--------- ...dUnderControlOfOpponentOfChoiceEffect.java | 145 +++++++------- Mage/src/main/java/mage/game/GameImpl.java | 36 ++-- .../java/mage/game/permanent/Permanent.java | 14 +- .../mage/game/permanent/PermanentImpl.java | 26 +-- .../main/java/mage/players/PlayerImpl.java | 91 +++++---- 7 files changed, 308 insertions(+), 235 deletions(-) diff --git a/Mage.Tests/src/test/java/org/mage/test/multiplayer/PlayerLeftGameRange1Test.java b/Mage.Tests/src/test/java/org/mage/test/multiplayer/PlayerLeftGameRange1Test.java index c37ce869a2..8699e82cd9 100644 --- a/Mage.Tests/src/test/java/org/mage/test/multiplayer/PlayerLeftGameRange1Test.java +++ b/Mage.Tests/src/test/java/org/mage/test/multiplayer/PlayerLeftGameRange1Test.java @@ -1,5 +1,6 @@ package org.mage.test.multiplayer; +import java.io.FileNotFoundException; import mage.constants.MultiplayerAttackOption; import mage.constants.PhaseStep; import mage.constants.RangeOfInfluence; @@ -14,8 +15,6 @@ import org.junit.Assert; import org.junit.Test; import org.mage.test.serverside.base.CardTestMultiPlayerBase; -import java.io.FileNotFoundException; - /** * @author LevelX2 */ @@ -344,4 +343,54 @@ public class PlayerLeftGameRange1Test extends CardTestMultiPlayerBase { Assert.assertTrue("Staff of player B could be used", staffPlayerB.isTapped()); } + + /** + * Captive Audience doesn't work correctly in multiplayer #5593 + * + * Currently, if the controller of Captive Audience leaves the game, Captive + * Audience returns to its owner instead of being exiled. + */ + @Test + public void TestCaptiveAudienceGoesToExile() { + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 4); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); + // Captive Audience enters the battlefield under the control of an opponent of your choice. + // At the beginning of your upkeep, choose one that hasn't been chosen — + // • Your life total becomes 4. + // • Discard your hand. + // • Each opponent creates five 2/2 black Zombie creature tokens. + addCard(Zone.HAND, playerA, "Captive Audience"); // Enchantment {5}{B}{R} + + addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion", 1); + addCard(Zone.BATTLEFIELD, playerA, "Pillarfield Ox", 1); + + setChoice(playerA, "PlayerA"); // Starting Player + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Captive Audience"); + setChoice(playerA, "PlayerD"); + + setModeChoice(playerD, "1"); + + attack(5, playerA, "Silvercoat Lion", playerD); + attack(5, playerA, "Pillarfield Ox", playerD); + + setStopAt(5, PhaseStep.POSTCOMBAT_MAIN); + + setStrictChooseMode(true); + execute(); + + assertAllCommandsUsed(); + + assertLife(playerA, 2); + + Assert.assertFalse("Player D is no longer in the game", playerD.isInGame()); + + assertPermanentCount(playerD, 0); + + assertPermanentCount(playerA, "Captive Audience", 0); + assertGraveyardCount(playerA, "Captive Audience", 0); + assertExileCount(playerA, "Captive Audience", 1); + + } + } diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java index 71ff178235..df9b358c7e 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java @@ -1,5 +1,13 @@ package org.mage.test.serverside.base.impl; +import java.io.FileNotFoundException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; import mage.MageObject; import mage.Mana; import mage.ObjectColor; @@ -35,15 +43,6 @@ import org.mage.test.player.TestPlayer; import org.mage.test.serverside.base.CardTestAPI; import org.mage.test.serverside.base.MageTestPlayerBase; -import java.io.FileNotFoundException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.UUID; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - /** * API for test initialization and asserting the test results. * @@ -275,7 +274,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement } } Assert.assertFalse("Wrong stop command on " + this.stopOnTurn + " / " + this.stopAtStep + " (" + this.stopAtStep.getIndex() + ")" - + " (found actions after stop on " + maxTurn + " / " + maxPhase + ")", + + " (found actions after stop on " + maxTurn + " / " + maxPhase + ")", (maxTurn > this.stopOnTurn) || (maxTurn == this.stopOnTurn && maxPhase > this.stopAtStep.getIndex())); for (Player player : currentGame.getPlayers().values()) { @@ -508,8 +507,8 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * Add a card to specified zone of specified player. * * @param gameZone {@link mage.constants.Zone} to add cards to. - * @param player {@link Player} to add cards for. Use either playerA or - * playerB. + * @param player {@link Player} to add cards for. Use either playerA or + * playerB. * @param cardName Card name in string format. */ @Override @@ -521,10 +520,10 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * Add any amount of cards to specified zone of specified player. * * @param gameZone {@link mage.constants.Zone} to add cards to. - * @param player {@link Player} to add cards for. Use either playerA or - * playerB. + * @param player {@link Player} to add cards for. Use either playerA or + * playerB. * @param cardName Card name in string format. - * @param count Amount of cards to be added. + * @param count Amount of cards to be added. */ @Override public void addCard(Zone gameZone, TestPlayer player, String cardName, int count) { @@ -535,13 +534,13 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * Add any amount of cards to specified zone of specified player. * * @param gameZone {@link mage.constants.Zone} to add cards to. - * @param player {@link Player} to add cards for. Use either playerA or - * playerB. + * @param player {@link Player} to add cards for. Use either playerA or + * playerB. * @param cardName Card name in string format. - * @param count Amount of cards to be added. - * @param tapped In case gameZone is Battlefield, determines whether - * permanent should be tapped. In case gameZone is other than Battlefield, - * {@link IllegalArgumentException} is thrown + * @param count Amount of cards to be added. + * @param tapped In case gameZone is Battlefield, determines whether + * permanent should be tapped. In case gameZone is other than Battlefield, + * {@link IllegalArgumentException} is thrown */ @Override public void addCard(Zone gameZone, TestPlayer player, String cardName, int count, boolean tapped) { @@ -622,7 +621,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * Set player's initial life count. * * @param player {@link Player} to set life count for. - * @param life Life count to set. + * @param life Life count to set. */ @Override public void setLife(TestPlayer player, int life) { @@ -699,7 +698,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * Assert player's life count after test execution. * * @param player {@link Player} to get life for comparison. - * @param life Expected player's life to compare with. + * @param life Expected player's life to compare with. */ @Override public void assertLife(Player player, int life) throws AssertionError { @@ -716,14 +715,14 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * params 3b. all: there is at least one creature with the cardName with the * different p\t params * - * @param player {@link Player} to get creatures for comparison. - * @param cardName Card name to compare with. - * @param power Expected power to compare with. + * @param player {@link Player} to get creatures for comparison. + * @param cardName Card name to compare with. + * @param power Expected power to compare with. * @param toughness Expected toughness to compare with. - * @param scope {@link mage.filter.Filter.ComparisonScope} Use ANY, if you - * want "at least one creature with given name should have specified p\t" - * Use ALL, if you want "all creature with gived name should have specified - * p\t" + * @param scope {@link mage.filter.Filter.ComparisonScope} Use ANY, if you + * want "at least one creature with given name should have specified p\t" + * Use ALL, if you want "all creature with gived name should have specified + * p\t" */ @Override public void assertPowerToughness(Player player, String cardName, int power, int toughness, Filter.ComparisonScope scope) @@ -813,8 +812,8 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * @param cardName * @param ability * @param mustHave true if creature should contain ability, false if it - * should NOT contain it instead - * @param count number of permanents with that ability + * should NOT contain it instead + * @param count number of permanents with that ability * @throws AssertionError */ public void assertAbility(Player player, String cardName, Ability ability, boolean mustHave, int count) throws AssertionError { @@ -847,7 +846,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * Assert permanent count under player's control. * * @param player {@link Player} which permanents should be counted. - * @param count Expected count. + * @param count Expected count. */ @Override public void assertPermanentCount(Player player, int count) throws AssertionError { @@ -863,9 +862,9 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement /** * Assert permanent count under player's control. * - * @param player {@link Player} which permanents should be counted. + * @param player {@link Player} which permanents should be counted. * @param cardName Name of the cards that should be counted. - * @param count Expected count. + * @param count Expected count. */ @Override public void assertPermanentCount(Player player, String cardName, int count) throws AssertionError { @@ -915,8 +914,8 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * Assert counter count on a permanent * * @param cardName Name of the cards that should be counted. - * @param type Type of the counter that should be counted. - * @param count Expected count. + * @param type Type of the counter that should be counted. + * @param count Expected count. */ public void assertCounterCount(String cardName, CounterType type, int count) throws AssertionError { this.assertCounterCount(null, cardName, type, count); @@ -939,8 +938,8 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * Assert counter count on a card in exile * * @param cardName Name of the cards that should be counted. - * @param type Type of the counter that should be counted. - * @param count Expected count. + * @param type Type of the counter that should be counted. + * @param count Expected count. */ public void assertCounterOnExiledCardCount(String cardName, CounterType type, int count) throws AssertionError { //Assert.assertNotEquals("", cardName); @@ -963,8 +962,8 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * Assert counter count on a player * * @param player The player whos counters should be counted. - * @param type Type of the counter that should be counted. - * @param count Expected count. + * @param type Type of the counter that should be counted. + * @param count Expected count. */ public void assertCounterCount(Player player, CounterType type, int count) throws AssertionError { Assert.assertEquals("(Battlefield) Counter counts are not equal (" + player.getName() + ':' + type + ')', count, player.getCounters().getCount(type)); @@ -974,7 +973,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * Assert whether a permanent is a specified type or not * * @param cardName Name of the permanent that should be checked. - * @param type A type to test for + * @param type A type to test for * @param mustHave true if creature should have type, false if it should not */ public void assertType(String cardName, CardType type, boolean mustHave) throws AssertionError { @@ -999,8 +998,8 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * Assert whether a permanent is a specified type * * @param cardName Name of the permanent that should be checked. - * @param type A type to test for - * @param subType a subtype to test for + * @param type A type to test for + * @param subType a subtype to test for */ public void assertType(String cardName, CardType type, SubType subType) throws AssertionError { //Assert.assertNotEquals("", cardName); @@ -1015,7 +1014,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * Assert whether a permanent is not a specified type * * @param cardName Name of the permanent that should be checked. - * @param type A type to test for + * @param type A type to test for */ public void assertNotType(String cardName, CardType type) throws AssertionError { //Assert.assertNotEquals("", cardName); @@ -1027,7 +1026,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * Assert whether a permanent is not a specified subtype * * @param cardName Name of the permanent that should be checked. - * @param subType a subtype to test for + * @param subType a subtype to test for */ public void assertNotSubtype(String cardName, SubType subType) throws AssertionError { //Assert.assertNotEquals("", cardName); @@ -1040,10 +1039,10 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement /** * Assert permanent color * - * @param player player to check - * @param cardName card name on battlefield from player + * @param player player to check + * @param cardName card name on battlefield from player * @param searchColors colors list with searchable values - * @param mustHave must or not must have that colors + * @param mustHave must or not must have that colors */ public void assertColor(Player player, String cardName, ObjectColor searchColors, boolean mustHave) { //Assert.assertNotEquals("", cardName); @@ -1078,7 +1077,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * Assert whether a permanent is tapped or not * * @param cardName Name of the permanent that should be checked. - * @param tapped Whether the permanent is tapped or not + * @param tapped Whether the permanent is tapped or not */ public void assertTapped(String cardName, boolean tapped) throws AssertionError { //Assert.assertNotEquals("", cardName); @@ -1105,8 +1104,8 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * Assert whether X permanents of the same name are tapped or not. * * @param cardName Name of the permanent that should be checked. - * @param tapped Whether the permanent is tapped or not - * @param count The amount of this permanents that should be tapped + * @param tapped Whether the permanent is tapped or not + * @param count The amount of this permanents that should be tapped */ public void assertTappedCount(String cardName, boolean tapped, int count) throws AssertionError { //Assert.assertNotEquals("", cardName); @@ -1128,7 +1127,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement /** * Assert whether a permanent is attacking or not * - * @param cardName Name of the permanent that should be checked. + * @param cardName Name of the permanent that should be checked. * @param attacking Whether the permanent is attacking or not */ public void assertAttacking(String cardName, boolean attacking) throws AssertionError { @@ -1150,7 +1149,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * Assert card count in player's hand. * * @param player {@link Player} who's hand should be counted. - * @param count Expected count. + * @param count Expected count. */ public void assertHandCount(Player player, int count) throws AssertionError { int actual = currentGame.getPlayer(player.getId()).getHand().size(); @@ -1160,9 +1159,9 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement /** * Assert card count in player's hand. * - * @param player {@link Player} who's hand should be counted. + * @param player {@link Player} who's hand should be counted. * @param cardName Name of the cards that should be counted. - * @param count Expected count. + * @param count Expected count. */ public void assertHandCount(Player player, String cardName, int count) throws AssertionError { //Assert.assertNotEquals("", cardName); @@ -1210,7 +1209,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * Assert card count in player's graveyard. * * @param player {@link Player} who's graveyard should be counted. - * @param count Expected count. + * @param count Expected count. */ public void assertGraveyardCount(Player player, int count) throws AssertionError { int actual = currentGame.getPlayer(player.getId()).getGraveyard().size(); @@ -1221,7 +1220,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * Assert card count in exile. * * @param cardName Name of the cards that should be counted. - * @param count Expected count. + * @param count Expected count. */ public void assertExileCount(String cardName, int count) throws AssertionError { //Assert.assertNotEquals("", cardName); @@ -1259,9 +1258,9 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement /** * Assert card count in player's exile. * - * @param owner {@link Player} who's exile should be counted. + * @param owner {@link Player} who's exile should be counted. * @param cardName Name of the cards that should be counted. - * @param count Expected count. + * @param count Expected count. */ public void assertExileCount(Player owner, String cardName, int count) throws AssertionError { //Assert.assertNotEquals("", cardName); @@ -1280,9 +1279,9 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement /** * Assert card count in player's graveyard. * - * @param player {@link Player} who's graveyard should be counted. + * @param player {@link Player} who's graveyard should be counted. * @param cardName Name of the cards that should be counted. - * @param count Expected count. + * @param count Expected count. */ public void assertGraveyardCount(Player player, String cardName, int count) throws AssertionError { assertAliaseSupportInActivateCommand(cardName, true); @@ -1301,7 +1300,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * Assert library card count. * * @param player {@link Player} who's library should be counted. - * @param count Expected count. + * @param count Expected count. */ public void assertLibraryCount(Player player, int count) throws AssertionError { List libraryList = player.getLibrary().getCards(currentGame); @@ -1312,9 +1311,9 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement /** * Assert specific card count in player's library. * - * @param player {@link Player} who's library should be counted. + * @param player {@link Player} who's library should be counted. * @param cardName Name of the cards that should be counted. - * @param count Expected count. + * @param count Expected count. */ public void assertLibraryCount(Player player, String cardName, int count) throws AssertionError { //Assert.assertNotEquals("", cardName); @@ -1353,8 +1352,8 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement } /** - * Raise error on any unused commands, choices or targets - * If you want to test that ability can't be activated then use call checkPlayableAbility() + * Raise error on any unused commands, choices or targets If you want to + * test that ability can't be activated then use call checkPlayableAbility() * * @throws AssertionError */ @@ -1501,8 +1500,8 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * @param player * @param cardName * @param targetName for modes you can add "mode=3" before target name, - * multiple targets can be seperated by ^, not target marks as - * TestPlayer.NO_TARGET + * multiple targets can be seperated by ^, not target marks as + * TestPlayer.NO_TARGET */ public void castSpell(int turnNum, PhaseStep step, TestPlayer player, String cardName, String targetName) { //Assert.assertNotEquals("", cardName); @@ -1525,8 +1524,8 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * @param step * @param player * @param cardName - * @param targetName for modal spells add the mode to the name e.g. - * "mode=2SilvercoatLion^mode3=PillarfieldOx" + * @param targetName for modal spells add the mode to the name e.g. + * "mode=2SilvercoatLion^mode3=PillarfieldOx" * @param spellOnStack */ public void castSpell(int turnNum, PhaseStep step, TestPlayer player, String cardName, String targetName, String spellOnStack) { @@ -1613,7 +1612,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * @param step * @param player * @param ability - * @param targetName use NO_TARGET if there is no target to set + * @param targetName use NO_TARGET if there is no target to set * @param spellOnStack */ public void activateAbility(int turnNum, PhaseStep step, TestPlayer player, String ability, String targetName, String spellOnStack) { @@ -1626,8 +1625,8 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * @param step * @param player * @param ability - * @param targetName if not target has to be defined use the constant - * NO_TARGET + * @param targetName if not target has to be defined use the constant + * NO_TARGET * @param spellOnStack * @param clause */ @@ -1691,9 +1690,12 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement } /** - * For use choices set "Yes" or "No" the the choice string. For X values set - * "X=[xValue]" example: for X=3 set choice string to "X=3". - *
For ColorChoice use "Red", "Green", "Blue", "Black" or "White" + * For use choices set "Yes" or "No" the the choice string.
+ * For X values set "X=[xValue]" example: for X=3 set choice string to + * "X=3".
+ * For ColorChoice use "Red", "Green", "Blue", "Black" or "White"
+ * use command setModeChoice if you have to set a mode from modal + * ability
* * @param player * @param choice @@ -1713,10 +1715,10 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * * @param player * @param choice starting with "1" for mode 1, "2" for mode 2 and so on (to - * set multiple modes call the command multiple times). If a spell mode can - * be used only once like Demonic Pact, the value has to be set to the - * number of the remaining modes (e.g. if only 2 are left the number need to - * be 1 or 2). + * set multiple modes call the command multiple times). If a spell mode can + * be used only once like Demonic Pact, the value has to be set to the + * number of the remaining modes (e.g. if only 2 are left the number need to + * be 1 or 2). */ public void setModeChoice(TestPlayer player, String choice) { player.addModeChoice(choice); @@ -1727,12 +1729,12 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * * @param player * @param target you can add multiple targets by separating them by the "^" - * character e.g. "creatureName1^creatureName2" you can qualify the target - * additional by setcode e.g. "creatureName-M15" you can add [no copy] to - * the end of the target name to prohibit targets that are copied you can - * add [only copy] to the end of the target name to allow only targets that - * are copies. For modal spells use a prefix with the mode number: - * mode=1Lightning Bolt^mode=2Silvercoat Lion + * character e.g. "creatureName1^creatureName2" you can qualify the target + * additional by setcode e.g. "creatureName-M15" you can add [no copy] to + * the end of the target name to prohibit targets that are copied you can + * add [only copy] to the end of the target name to allow only targets that + * are copies. For modal spells use a prefix with the mode number: + * mode=1Lightning Bolt^mode=2Silvercoat Lion */ // TODO: mode options doesn't work here (see BrutalExpulsionTest) public void addTarget(TestPlayer player, String target) { @@ -1764,7 +1766,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement /** * @param player * @param target use TestPlayer.TARGET_SKIP to 0 targets selects or to stop - * "up two xxx" selection + * "up two xxx" selection * @param amount */ public void addTargetAmount(TestPlayer player, String target, int amount) { diff --git a/Mage/src/main/java/mage/abilities/effects/common/EntersBattlefieldUnderControlOfOpponentOfChoiceEffect.java b/Mage/src/main/java/mage/abilities/effects/common/EntersBattlefieldUnderControlOfOpponentOfChoiceEffect.java index 2d3a8c5639..5b1b4f4e94 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/EntersBattlefieldUnderControlOfOpponentOfChoiceEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/EntersBattlefieldUnderControlOfOpponentOfChoiceEffect.java @@ -1,72 +1,73 @@ -/* - * 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.abilities.effects.common; - -import mage.abilities.Ability; -import mage.abilities.effects.ContinuousEffect; -import mage.abilities.effects.OneShotEffect; -import mage.abilities.effects.common.continuous.GainControlTargetEffect; -import mage.constants.Duration; -import mage.constants.Outcome; -import static mage.constants.Outcome.Benefit; -import mage.game.Game; -import mage.game.permanent.Permanent; -import mage.players.Player; -import mage.target.Target; -import mage.target.common.TargetOpponent; -import mage.target.targetpointer.FixedTarget; - -/** - * Use this effect only with EntersBattlefieldAbility like abilities - * - * @author LevelX2 - */ - -public class EntersBattlefieldUnderControlOfOpponentOfChoiceEffect extends OneShotEffect { - - public EntersBattlefieldUnderControlOfOpponentOfChoiceEffect() { - super(Benefit); - staticText = "under the control of an opponent of your choice"; - } - - private EntersBattlefieldUnderControlOfOpponentOfChoiceEffect(final EntersBattlefieldUnderControlOfOpponentOfChoiceEffect effect) { - super(effect); - } - - @Override - public EntersBattlefieldUnderControlOfOpponentOfChoiceEffect copy() { - return new EntersBattlefieldUnderControlOfOpponentOfChoiceEffect(this); - } - - @Override - public boolean apply(Game game, Ability source) { - Player controller = game.getPlayer(source.getControllerId()); - if (controller == null) { - return false; - } - Target target = new TargetOpponent(); - target.setNotTarget(true); - if (!controller.choose(Outcome.Benefit, target, source.getSourceId(), game)) { - return false; - } - Player opponent = game.getPlayer(target.getFirstTarget()); - if (opponent == null) { - return false; - } - Permanent permanent = game.getPermanentEntering(source.getSourceId()); - if (permanent != null) { - game.informPlayers(permanent.getLogName() + " enters the battlefield under the control of " + opponent.getLogName()); - } - ContinuousEffect continuousEffect = new GainControlTargetEffect( - Duration.Custom, true, opponent.getId() - ); - continuousEffect.setTargetPointer(new FixedTarget( - source.getSourceId(), source.getSourceObjectZoneChangeCounter() - )); - game.addEffect(continuousEffect, source); - return true; - } -} \ No newline at end of file +/* + * 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.abilities.effects.common; + +import mage.abilities.Ability; +import mage.abilities.effects.ContinuousEffect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.continuous.GainControlTargetEffect; +import mage.constants.Duration; +import mage.constants.Outcome; +import static mage.constants.Outcome.Benefit; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.Target; +import mage.target.common.TargetOpponent; +import mage.target.targetpointer.FixedTarget; + +/** + * Use this effect only with EntersBattlefieldAbility like abilities + * + * @author LevelX2 + */ +public class EntersBattlefieldUnderControlOfOpponentOfChoiceEffect extends OneShotEffect { + + public EntersBattlefieldUnderControlOfOpponentOfChoiceEffect() { + super(Benefit); + staticText = "under the control of an opponent of your choice"; + } + + private EntersBattlefieldUnderControlOfOpponentOfChoiceEffect(final EntersBattlefieldUnderControlOfOpponentOfChoiceEffect effect) { + super(effect); + } + + @Override + public EntersBattlefieldUnderControlOfOpponentOfChoiceEffect copy() { + return new EntersBattlefieldUnderControlOfOpponentOfChoiceEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller == null) { + return false; + } + Target target = new TargetOpponent(); + target.setNotTarget(true); + if (!controller.choose(Outcome.Benefit, target, source.getSourceId(), game)) { + return false; + } + Player opponent = game.getPlayer(target.getFirstTarget()); + if (opponent == null) { + return false; + } + Permanent permanent = game.getPermanentEntering(source.getSourceId()); + if (permanent != null) { + permanent.setOriginalControllerId(opponent.getId()); // permanent was controlled by this player since the existance of this object so original controller has to be set to the first controller + permanent.setControllerId(opponent.getId()); // neccessary to set already here because spell caster never controlled the permanent (important for rule 800.4a) + game.informPlayers(permanent.getLogName() + " enters the battlefield under the control of " + opponent.getLogName()); + } + ContinuousEffect continuousEffect = new GainControlTargetEffect( + Duration.Custom, true, opponent.getId() + ); + continuousEffect.setTargetPointer(new FixedTarget( + source.getSourceId(), source.getSourceObjectZoneChangeCounter() + )); + game.addEffect(continuousEffect, source); + return true; + } +} diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index 673c2f056f..90a360c070 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -1,5 +1,9 @@ package mage.game; +import java.io.IOException; +import java.io.Serializable; +import java.util.*; +import java.util.Map.Entry; import mage.MageException; import mage.MageObject; import mage.abilities.*; @@ -67,11 +71,6 @@ import mage.util.functions.ApplyToPermanent; import mage.watchers.common.*; import org.apache.log4j.Logger; -import java.io.IOException; -import java.io.Serializable; -import java.util.*; -import java.util.Map.Entry; - public abstract class GameImpl implements Game, Serializable { private static final int ROLLBACK_TURNS_MAX = 4; @@ -562,7 +561,7 @@ public abstract class GameImpl implements Game, Serializable { @Override public void saveState(boolean bookmark) { - if (!simulation && gameStates != null) { + if (!simulation && gameStates != null) { if (bookmark || saveGame) { gameStates.save(state); } @@ -1549,7 +1548,7 @@ public abstract class GameImpl implements Game, Serializable { /** * @param emblem * @param sourceObject - * @param toPlayerId controller and owner of the emblem + * @param toPlayerId controller and owner of the emblem */ @Override public void addEmblem(Emblem emblem, MageObject sourceObject, UUID toPlayerId) { @@ -1567,8 +1566,8 @@ public abstract class GameImpl implements Game, Serializable { /** * @param plane * @param sourceObject - * @param toPlayerId controller and owner of the plane (may only be one per - * game..) + * @param toPlayerId controller and owner of the plane (may only be one per + * game..) * @return boolean - whether the plane was added successfully or not */ @Override @@ -1642,7 +1641,7 @@ public abstract class GameImpl implements Game, Serializable { newBluePrint.reset(this); //getState().addCard(permanent); - if (copyFromPermanent.isMorphed() || copyFromPermanent.isManifested() + if (copyFromPermanent.isMorphed() || copyFromPermanent.isManifested() || copyFromPermanent.isFaceDown(this)) { MorphAbility.setPermanentToFaceDownCreature(newBluePrint); } @@ -1805,7 +1804,7 @@ public abstract class GameImpl implements Game, Serializable { break; } // triggered abilities that don't use the stack have to be executed first (e.g. Banisher Priest Return exiled creature - for (Iterator it = abilities.iterator(); it.hasNext(); ) { + for (Iterator it = abilities.iterator(); it.hasNext();) { TriggeredAbility triggeredAbility = it.next(); if (!triggeredAbility.isUsesStack()) { state.removeTriggeredAbility(triggeredAbility); @@ -1880,7 +1879,7 @@ public abstract class GameImpl implements Game, Serializable { Zone currentZone = this.getState().getZone(card.getId()); String currentZoneInfo = (currentZone == null ? "(error)" : "(" + currentZone.name() + ")"); if (player.chooseUse(Outcome.Benefit, "Move " + card.getIdName() - + " to the command zone or leave it in current zone " + currentZoneInfo + "?", "You can only make this choice once per object", + + " to the command zone or leave it in current zone " + currentZoneInfo + "?", "You can only make this choice once per object", "Move to command", "Leave in current zone " + currentZoneInfo, null, this)) { toMove.add(card); } else { @@ -2596,7 +2595,7 @@ public abstract class GameImpl implements Game, Serializable { } //20100423 - 800.4a Set toOutside = new HashSet<>(); - for (Iterator it = getBattlefield().getAllPermanents().iterator(); it.hasNext(); ) { + for (Iterator it = getBattlefield().getAllPermanents().iterator(); it.hasNext();) { Permanent perm = it.next(); if (perm.isOwnedBy(playerId)) { if (perm.getAttachedTo() != null) { @@ -2621,6 +2620,10 @@ public abstract class GameImpl implements Game, Serializable { for (ContinuousEffect effect : getContinuousEffects().getLayeredEffects(this)) { if (effect.hasLayer(Layer.ControlChangingEffects_2)) { for (Ability ability : getContinuousEffects().getLayeredEffectAbilities(effect)) { + if (effect.getTargetPointer().getTargets(this, ability).contains(perm.getId())) { + effect.discard(); + continue Effects; + } for (Target target : ability.getTargets()) { for (UUID targetId : target.getTargets()) { if (targetId.equals(perm.getId())) { @@ -2630,6 +2633,7 @@ public abstract class GameImpl implements Game, Serializable { } } } + } } } @@ -2641,7 +2645,7 @@ public abstract class GameImpl implements Game, Serializable { player.moveCards(toOutside, Zone.OUTSIDE, null, this); // triggered abilities that don't use the stack have to be executed List abilities = state.getTriggered(player.getId()); - for (Iterator it = abilities.iterator(); it.hasNext(); ) { + for (Iterator it = abilities.iterator(); it.hasNext();) { TriggeredAbility triggeredAbility = it.next(); if (!triggeredAbility.isUsesStack()) { state.removeTriggeredAbility(triggeredAbility); @@ -2661,7 +2665,7 @@ public abstract class GameImpl implements Game, Serializable { // Remove cards from the player in all exile zones for (ExileZone exile : this.getExile().getExileZones()) { - for (Iterator it = exile.iterator(); it.hasNext(); ) { + for (Iterator it = exile.iterator(); it.hasNext();) { Card card = this.getCard(it.next()); if (card != null && card.isOwnedBy(playerId)) { it.remove(); @@ -2671,7 +2675,7 @@ public abstract class GameImpl implements Game, Serializable { //Remove all commander/emblems/plane the player controls boolean addPlaneAgain = false; - for (Iterator it = this.getState().getCommand().iterator(); it.hasNext(); ) { + for (Iterator it = this.getState().getCommand().iterator(); it.hasNext();) { CommandObject obj = it.next(); if (obj.isControlledBy(playerId)) { if (obj instanceof Emblem) { diff --git a/Mage/src/main/java/mage/game/permanent/Permanent.java b/Mage/src/main/java/mage/game/permanent/Permanent.java index 7999673d27..f9612d1925 100644 --- a/Mage/src/main/java/mage/game/permanent/Permanent.java +++ b/Mage/src/main/java/mage/game/permanent/Permanent.java @@ -1,5 +1,8 @@ package mage.game.permanent; +import java.util.List; +import java.util.Set; +import java.util.UUID; import mage.MageObject; import mage.MageObjectReference; import mage.abilities.Ability; @@ -10,12 +13,10 @@ import mage.game.Controllable; import mage.game.Game; import mage.game.GameState; -import java.util.List; -import java.util.Set; -import java.util.UUID; - public interface Permanent extends Card, Controllable { + void setOriginalControllerId(UUID controllerId); + void setControllerId(UUID controllerId); boolean isTapped(); @@ -103,7 +104,8 @@ public interface Permanent extends Card, Controllable { /** * @param source * @param game - * @param silentMode - use it to ignore warning message for users (e.g. for checking only) + * @param silentMode - use it to ignore warning message for users (e.g. for + * checking only) * @return */ boolean cantBeAttachedBy(MageObject source, Game game, boolean silentMode); @@ -217,7 +219,7 @@ public interface Permanent extends Card, Controllable { /** * @param defenderId id of planeswalker or player to attack - can be empty - * to check generally + * to check generally * @param game * @return */ diff --git a/Mage/src/main/java/mage/game/permanent/PermanentImpl.java b/Mage/src/main/java/mage/game/permanent/PermanentImpl.java index b2e8fa813d..44b0261490 100644 --- a/Mage/src/main/java/mage/game/permanent/PermanentImpl.java +++ b/Mage/src/main/java/mage/game/permanent/PermanentImpl.java @@ -1,5 +1,7 @@ package mage.game.permanent; +import java.io.Serializable; +import java.util.*; import mage.MageObject; import mage.MageObjectReference; import mage.ObjectColor; @@ -38,9 +40,6 @@ import mage.util.GameLog; import mage.util.ThreadLocalStringBuilder; import org.apache.log4j.Logger; -import java.io.Serializable; -import java.util.*; - /** * @author BetaSteward_at_googlemail.com */ @@ -184,6 +183,11 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { abilities.setControllerId(controllerId); } + @Override + public void setOriginalControllerId(UUID originalControllerId) { + this.originalControllerId = originalControllerId; + } + /** * Called before each applyEffects or if after a permanent was copied for * the copied object @@ -793,7 +797,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { this.attachedTo = attachToObjectId; this.attachedToZoneChangeCounter = game.getState().getZoneChangeCounter(attachToObjectId); for (Ability ability : this.getAbilities()) { - for (Iterator ite = ability.getEffects(game, EffectType.CONTINUOUS).iterator(); ite.hasNext(); ) { + for (Iterator ite = ability.getEffects(game, EffectType.CONTINUOUS).iterator(); ite.hasNext();) { ContinuousEffect effect = (ContinuousEffect) ite.next(); game.getContinuousEffects().setOrder(effect); // It's important to update the timestamp of the copied effect in ContinuousEffects because it does the action @@ -848,8 +852,8 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { * @param game * @param preventable * @param combat - * @param markDamage If true, damage will be dealt later in applyDamage - * method + * @param markDamage If true, damage will be dealt later in applyDamage + * method * @return */ private int damage(int damageAmount, UUID sourceId, Game game, boolean preventable, boolean combat, boolean markDamage, List appliedEffects) { @@ -1066,9 +1070,9 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { if (game.getPlayer(this.getControllerId()).hasOpponent(sourceControllerId, game) && game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.HEXPROOF, null, sourceControllerId, game) == null && abilities.stream() - .filter(HexproofBaseAbility.class::isInstance) - .map(HexproofBaseAbility.class::cast) - .anyMatch(ability -> ability.checkObject(source, game))) { + .filter(HexproofBaseAbility.class::isInstance) + .map(HexproofBaseAbility.class::cast) + .anyMatch(ability -> ability.checkObject(source, game))) { return false; } @@ -1619,9 +1623,9 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { public boolean moveToExile(UUID exileId, String name, UUID sourceId, Game game, List appliedEffects) { Zone fromZone = game.getState().getZone(objectId); ZoneChangeEvent event = new ZoneChangeEvent(this, sourceId, ownerId, fromZone, Zone.EXILED, appliedEffects); - ZoneChangeInfo.Exile info = new ZoneChangeInfo.Exile(event, exileId, name); + ZoneChangeInfo.Exile zcInfo = new ZoneChangeInfo.Exile(event, exileId, name); - boolean successfullyMoved = ZonesHandler.moveCard(info, game); + boolean successfullyMoved = ZonesHandler.moveCard(zcInfo, game); //20180810 - 701.3d detachAllAttachments(game); return successfullyMoved; diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index 696e98884e..0cba9775d2 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -1,6 +1,10 @@ package mage.players; import com.google.common.collect.ImmutableMap; +import java.io.Serializable; +import java.util.*; +import java.util.Map.Entry; +import java.util.stream.Collectors; import mage.ConditionalMana; import mage.MageObject; import mage.MageObjectReference; @@ -66,11 +70,6 @@ import mage.util.GameLog; import mage.util.RandomUtil; import org.apache.log4j.Logger; -import java.io.Serializable; -import java.util.*; -import java.util.Map.Entry; -import java.util.stream.Collectors; - public abstract class PlayerImpl implements Player, Serializable { private static final Logger logger = Logger.getLogger(PlayerImpl.class); @@ -614,9 +613,9 @@ public abstract class PlayerImpl implements Player, Serializable { && this.hasOpponent(sourceControllerId, game) && game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.HEXPROOF, null, sourceControllerId, game) == null && abilities.stream() - .filter(HexproofBaseAbility.class::isInstance) - .map(HexproofBaseAbility.class::cast) - .anyMatch(ability -> ability.checkObject(source, game))) { + .filter(HexproofBaseAbility.class::isInstance) + .map(HexproofBaseAbility.class::cast) + .anyMatch(ability -> ability.checkObject(source, game))) { return false; } @@ -656,7 +655,7 @@ public abstract class PlayerImpl implements Player, Serializable { game.informPlayers(getLogName() + " discards down to " + this.maxHandSize + (this.maxHandSize == 1 - ? " hand card" : " hand cards")); + ? " hand card" : " hand cards")); } discard(hand.size() - this.maxHandSize, false, null, game); } @@ -805,7 +804,7 @@ public abstract class PlayerImpl implements Player, Serializable { } GameEvent gameEvent = GameEvent.getEvent(GameEvent.EventType.DISCARD_CARD, card.getId(), source == null - ? null : source.getSourceId(), playerId); + ? null : source.getSourceId(), playerId); gameEvent.setFlag(source != null); // event from effect or from cost (source == null) if (game.replaceEvent(gameEvent, source)) { return false; @@ -1842,9 +1841,9 @@ public abstract class PlayerImpl implements Player, Serializable { } private List getPermanentsThatCanBeUntapped(Game game, - List canBeUntapped, - RestrictionUntapNotMoreThanEffect handledEffect, - Map>, Integer> notMoreThanEffectsUsage) { + List canBeUntapped, + RestrictionUntapNotMoreThanEffect handledEffect, + Map>, Integer> notMoreThanEffectsUsage) { List leftForUntap = new ArrayList<>(); // select permanents that can still be untapped for (Permanent permanent : canBeUntapped) { @@ -2553,7 +2552,7 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public boolean searchLibrary(TargetCardInLibrary target, Ability source, Game game, UUID targetPlayerId, - boolean triggerEvents) { + boolean triggerEvents) { //20091005 - 701.14c Library searchedLibrary = null; String searchInfo = null; @@ -2755,7 +2754,7 @@ public abstract class PlayerImpl implements Player, Serializable { /** * @param game * @param appliedEffects - * @param numSides Number of sides the dice has + * @param numSides Number of sides the dice has * @return the number that the player rolled */ @Override @@ -2792,16 +2791,16 @@ public abstract class PlayerImpl implements Player, Serializable { /** * @param game * @param appliedEffects - * @param numberChaosSides The number of chaos sides the planar die - * currently has (normally 1 but can be 5) + * @param numberChaosSides The number of chaos sides the planar die + * currently has (normally 1 but can be 5) * @param numberPlanarSides The number of chaos sides the planar die - * currently has (normally 1) + * currently has (normally 1) * @return the outcome that the player rolled. Either ChaosRoll, PlanarRoll * or NilRoll */ @Override public PlanarDieRoll rollPlanarDie(Game game, List appliedEffects, int numberChaosSides, - int numberPlanarSides) { + int numberPlanarSides) { int result = RandomUtil.nextInt(9) + 1; PlanarDieRoll roll = PlanarDieRoll.NIL_ROLL; if (numberChaosSides + numberPlanarSides > 9) { @@ -2958,7 +2957,8 @@ public abstract class PlayerImpl implements Player, Serializable { /** * @param ability - * @param availableMana if null, it won't be checked if enough mana is available + * @param availableMana if null, it won't be checked if enough mana is + * available * @param sourceObject * @param game * @return @@ -3290,6 +3290,17 @@ public abstract class PlayerImpl implements Player, Serializable { return getPlayable(game, hidden, Zone.ALL, true); } + /** + * Returns a list of all available spells and abilities the player can + * currently cast/activate with his available ressources + * + * @param game + * @param hidden also from hidden objects (e.g. turned face down cards ?) + * @param fromZone of objects from which zone (ALL = from all zones) + * @param hideDuplicatedAbilities if equal abilities exist return only the + * first instance + * @return + */ public List getPlayable(Game game, boolean hidden, Zone fromZone, boolean hideDuplicatedAbilities) { List playable = new ArrayList<>(); if (shouldSkipGettingPlayable(game)) { @@ -3655,7 +3666,7 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public boolean canPaySacrificeCost(Permanent permanent, UUID sourceId, - UUID controllerId, Game game + UUID controllerId, Game game ) { return sacrificeCostFilter == null || !sacrificeCostFilter.match(permanent, sourceId, controllerId, game); } @@ -3808,8 +3819,8 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public boolean moveCards(Card card, Zone toZone, - Ability source, Game game, - boolean tapped, boolean faceDown, boolean byOwner, List appliedEffects + Ability source, Game game, + boolean tapped, boolean faceDown, boolean byOwner, List appliedEffects ) { Set cardList = new HashSet<>(); if (card != null) { @@ -3820,22 +3831,22 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public boolean moveCards(Cards cards, Zone toZone, - Ability source, Game game + Ability source, Game game ) { return moveCards(cards.getCards(game), toZone, source, game); } @Override public boolean moveCards(Set cards, Zone toZone, - Ability source, Game game + Ability source, Game game ) { return moveCards(cards, toZone, source, game, false, false, false, null); } @Override public boolean moveCards(Set cards, Zone toZone, - Ability source, Game game, - boolean tapped, boolean faceDown, boolean byOwner, List appliedEffects + Ability source, Game game, + boolean tapped, boolean faceDown, boolean byOwner, List appliedEffects ) { if (cards.isEmpty()) { return true; @@ -3937,8 +3948,8 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public boolean moveCardsToExile(Card card, Ability source, - Game game, boolean withName, UUID exileId, - String exileZoneName + Game game, boolean withName, UUID exileId, + String exileZoneName ) { Set cards = new HashSet<>(); cards.add(card); @@ -3947,8 +3958,8 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public boolean moveCardsToExile(Set cards, Ability source, - Game game, boolean withName, UUID exileId, - String exileZoneName + Game game, boolean withName, UUID exileId, + String exileZoneName ) { if (cards.isEmpty()) { return true; @@ -3964,14 +3975,14 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public boolean moveCardToHandWithInfo(Card card, UUID sourceId, - Game game + Game game ) { return this.moveCardToHandWithInfo(card, sourceId, game, true); } @Override public boolean moveCardToHandWithInfo(Card card, UUID sourceId, - Game game, boolean withName + Game game, boolean withName ) { boolean result = false; Zone fromZone = game.getState().getZone(card.getId()); @@ -3996,7 +4007,7 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public Set moveCardsToGraveyardWithInfo(Set allCards, Ability source, - Game game, Zone fromZone + Game game, Zone fromZone ) { UUID sourceId = source == null ? null : source.getSourceId(); Set movedCards = new LinkedHashSet<>(); @@ -4004,7 +4015,7 @@ public abstract class PlayerImpl implements Player, Serializable { // identify cards from one owner Cards cards = new CardsImpl(); UUID ownerId = null; - for (Iterator it = allCards.iterator(); it.hasNext(); ) { + for (Iterator it = allCards.iterator(); it.hasNext();) { Card card = it.next(); if (cards.isEmpty()) { ownerId = card.getOwnerId(); @@ -4067,7 +4078,7 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public boolean moveCardToGraveyardWithInfo(Card card, UUID sourceId, - Game game, Zone fromZone + Game game, Zone fromZone ) { if (card == null) { return false; @@ -4096,8 +4107,8 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public boolean moveCardToLibraryWithInfo(Card card, UUID sourceId, - Game game, Zone fromZone, - boolean toTop, boolean withName + Game game, Zone fromZone, + boolean toTop, boolean withName ) { if (card == null) { return false; @@ -4162,7 +4173,7 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public boolean moveCardToExileWithInfo(Card card, UUID exileId, String exileName, UUID sourceId, - Game game, Zone fromZone, boolean withName) { + Game game, Zone fromZone, boolean withName) { if (card == null) { return false; } @@ -4185,7 +4196,7 @@ public abstract class PlayerImpl implements Player, Serializable { game.informPlayers(this.getLogName() + " moves " + (withName ? card.getLogName() + (card.isCopy() ? " (Copy)" : "") : "a card face down") + ' ' + (fromZone != null ? "from " + fromZone.toString().toLowerCase(Locale.ENGLISH) - + ' ' : "") + "to the exile zone"); + + ' ' : "") + "to the exile zone"); } result = true; From 1a0dca906798acbcb87ba0a1f44a46a3e633f29b Mon Sep 17 00:00:00 2001 From: htrajan Date: Sun, 28 Jun 2020 20:24:59 -0700 Subject: [PATCH 2/5] fix M21 bugs --- Mage.Sets/src/mage/cards/g/GarrukSavageHerald.java | 12 +++++++++--- Mage.Sets/src/mage/cards/l/LilianaDeathMage.java | 8 ++++++-- Mage.Sets/src/mage/cards/n/Necromentia.java | 5 +++++ Mage.Sets/src/mage/cards/s/SanctumOfAll.java | 6 ++++-- 4 files changed, 24 insertions(+), 7 deletions(-) diff --git a/Mage.Sets/src/mage/cards/g/GarrukSavageHerald.java b/Mage.Sets/src/mage/cards/g/GarrukSavageHerald.java index 614653fe34..2115e2ddab 100644 --- a/Mage.Sets/src/mage/cards/g/GarrukSavageHerald.java +++ b/Mage.Sets/src/mage/cards/g/GarrukSavageHerald.java @@ -5,6 +5,7 @@ import mage.abilities.LoyaltyAbility; import mage.abilities.common.DamageAsThoughNotBlockedAbility; import mage.abilities.common.PlaneswalkerEntersWithLoyaltyCountersAbility; import mage.abilities.effects.ContinuousEffect; +import mage.abilities.effects.Effect; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.DamageWithPowerFromOneToAnotherTargetEffect; import mage.abilities.effects.common.continuous.GainAbilityControlledEffect; @@ -12,7 +13,12 @@ import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.cards.CardsImpl; -import mage.constants.*; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.constants.Zone; import mage.filter.common.FilterCreaturePermanent; import mage.filter.predicate.mageobject.AnotherTargetPredicate; import mage.game.Game; @@ -39,7 +45,7 @@ public final class GarrukSavageHerald extends CardImpl { this.addAbility(new LoyaltyAbility(new GarrukSavageHeraldEffect(), 1)); // −2: Target creature you control deals damage equal to its power to another target creature. - DamageWithPowerFromOneToAnotherTargetEffect effect = new DamageWithPowerFromOneToAnotherTargetEffect(); + Effect effect = new DamageWithPowerFromOneToAnotherTargetEffect(); effect.setText("Target creature you control deals damage equal to its power to another target creature"); Ability minusAbility = new LoyaltyAbility(effect, -2); @@ -47,7 +53,7 @@ public final class GarrukSavageHerald extends CardImpl { controlledCreature.setTargetTag(1); minusAbility.addTarget(controlledCreature); - FilterCreaturePermanent filter = new FilterCreaturePermanent("Another creature: damage dealt to"); + FilterCreaturePermanent filter = new FilterCreaturePermanent("another creature to deal damage to"); filter.add(new AnotherTargetPredicate(2)); TargetCreaturePermanent anotherTargetCreature = new TargetCreaturePermanent(filter); minusAbility.addTarget(anotherTargetCreature); diff --git a/Mage.Sets/src/mage/cards/l/LilianaDeathMage.java b/Mage.Sets/src/mage/cards/l/LilianaDeathMage.java index 45c189ded9..0e1d8341ce 100644 --- a/Mage.Sets/src/mage/cards/l/LilianaDeathMage.java +++ b/Mage.Sets/src/mage/cards/l/LilianaDeathMage.java @@ -9,7 +9,11 @@ import mage.abilities.effects.common.LoseLifeTargetControllerEffect; import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.*; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.constants.Zone; import mage.filter.StaticFilters; import mage.game.Game; import mage.players.Player; @@ -78,7 +82,7 @@ class LilianaDeathMagePlusEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { Player player = game.getPlayer(source.getControllerId()); - if (player == null || !player.chooseUse(Outcome.Benefit, "Return a creature card from your graveyard to your hand?", source, game)) { + if (player == null) { return false; } Card card = game.getCard(source.getTargets().get(0).getFirstTarget()); diff --git a/Mage.Sets/src/mage/cards/n/Necromentia.java b/Mage.Sets/src/mage/cards/n/Necromentia.java index 985e0fc22e..888d90311d 100644 --- a/Mage.Sets/src/mage/cards/n/Necromentia.java +++ b/Mage.Sets/src/mage/cards/n/Necromentia.java @@ -20,6 +20,7 @@ import mage.target.TargetCard; import mage.target.common.TargetCardInLibrary; import mage.target.common.TargetOpponent; +import java.util.HashSet; import java.util.UUID; /** @@ -94,6 +95,8 @@ class NecromentiaEffect extends OneShotEffect { numberOfCardsExiledFromHand = target.getTargets().size(); controller.moveCards(new CardsImpl(target.getTargets()), Zone.EXILED, source, game); } + } else { + targetPlayer.revealCards(targetPlayer.getName() + "'s Hand", targetPlayer.getHand(), game); } // cards in Library @@ -106,6 +109,8 @@ class NecromentiaEffect extends OneShotEffect { if (controller.choose(Outcome.Exile, cardsInLibrary, targetLib, game)) { controller.moveCards(new CardsImpl(targetLib.getTargets()), Zone.EXILED, source, game); } + } else { + targetPlayer.revealCards(targetPlayer.getName() + "'s Library", new CardsImpl(new HashSet<>(targetPlayer.getLibrary().getCards(game))), game); } targetPlayer.shuffleLibrary(source, game); diff --git a/Mage.Sets/src/mage/cards/s/SanctumOfAll.java b/Mage.Sets/src/mage/cards/s/SanctumOfAll.java index ccb8c70cf7..97386229b2 100644 --- a/Mage.Sets/src/mage/cards/s/SanctumOfAll.java +++ b/Mage.Sets/src/mage/cards/s/SanctumOfAll.java @@ -97,9 +97,11 @@ class SanctumOfAllTriggerEffect extends ReplacementEffectImpl { // Only trigger while you control six or more Shrines int numShrines = SanctumOfAll.count.calculate(game, source, this); if (numShrines >= 6) { - // Only for triggers of Shrines + // Only for triggers of other Shrines Permanent permanent = game.getPermanent(event.getSourceId()); - return permanent != null && permanent.hasSubtype(SubType.SHRINE, game); + return permanent != null + && !permanent.getId().equals(source.getSourceId()) + && permanent.hasSubtype(SubType.SHRINE, game); } } return false; From faa375907ffa53592601a8acd5641336f4ddc1b4 Mon Sep 17 00:00:00 2001 From: htrajan Date: Sun, 28 Jun 2020 21:42:59 -0700 Subject: [PATCH 3/5] withChooseHint --- Mage.Sets/src/mage/cards/g/GarrukSavageHerald.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Mage.Sets/src/mage/cards/g/GarrukSavageHerald.java b/Mage.Sets/src/mage/cards/g/GarrukSavageHerald.java index 2115e2ddab..5d2aaf48a8 100644 --- a/Mage.Sets/src/mage/cards/g/GarrukSavageHerald.java +++ b/Mage.Sets/src/mage/cards/g/GarrukSavageHerald.java @@ -53,10 +53,10 @@ public final class GarrukSavageHerald extends CardImpl { controlledCreature.setTargetTag(1); minusAbility.addTarget(controlledCreature); - FilterCreaturePermanent filter = new FilterCreaturePermanent("another creature to deal damage to"); + FilterCreaturePermanent filter = new FilterCreaturePermanent(); filter.add(new AnotherTargetPredicate(2)); TargetCreaturePermanent anotherTargetCreature = new TargetCreaturePermanent(filter); - minusAbility.addTarget(anotherTargetCreature); + minusAbility.addTarget(anotherTargetCreature.withChooseHint("another creature to deal damage to")); this.addAbility(minusAbility); From 4652ebd790ed0d7d12d2e334c51288b3be31abcf Mon Sep 17 00:00:00 2001 From: htrajan Date: Sun, 28 Jun 2020 23:06:29 -0700 Subject: [PATCH 4/5] [M21] Fix Enthralling Hold (#6745) --- Mage.Sets/src/mage/cards/d/DreamLeash.java | 19 ++++++++-- .../src/mage/cards/e/EnthrallingHold.java | 38 +++++++++++++------ 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/Mage.Sets/src/mage/cards/d/DreamLeash.java b/Mage.Sets/src/mage/cards/d/DreamLeash.java index bccc18cffd..b557bd4bf4 100644 --- a/Mage.Sets/src/mage/cards/d/DreamLeash.java +++ b/Mage.Sets/src/mage/cards/d/DreamLeash.java @@ -1,7 +1,6 @@ package mage.cards.d; -import java.util.UUID; import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.common.AttachEffect; @@ -10,13 +9,15 @@ import mage.abilities.keyword.EnchantAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.SubType; import mage.constants.Outcome; +import mage.constants.SubType; import mage.constants.Zone; import mage.game.Game; import mage.game.permanent.Permanent; import mage.target.TargetPermanent; +import java.util.UUID; + /** * * @author maxlebedev @@ -52,10 +53,20 @@ public final class DreamLeash extends CardImpl { class DreamLeashTarget extends TargetPermanent { + DreamLeashTarget() {} + + private DreamLeashTarget(DreamLeashTarget target) { + super(target); + } + + @Override + public DreamLeashTarget copy() { + return new DreamLeashTarget(this); + } + @Override public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { - - if(super.canTarget(controllerId, id, source, game)){ + if (super.canTarget(controllerId, id, source, game)) { Permanent permanent = game.getPermanent(id); return permanent.isTapped(); } diff --git a/Mage.Sets/src/mage/cards/e/EnthrallingHold.java b/Mage.Sets/src/mage/cards/e/EnthrallingHold.java index 9060709592..fa28c52a39 100644 --- a/Mage.Sets/src/mage/cards/e/EnthrallingHold.java +++ b/Mage.Sets/src/mage/cards/e/EnthrallingHold.java @@ -12,9 +12,8 @@ import mage.constants.CardType; import mage.constants.Outcome; import mage.constants.SubType; import mage.constants.Zone; -import mage.filter.FilterPermanent; -import mage.filter.common.FilterCreaturePermanent; -import mage.filter.predicate.permanent.TappedPredicate; +import mage.game.Game; +import mage.game.permanent.Permanent; import mage.target.TargetPermanent; import mage.target.common.TargetCreaturePermanent; @@ -26,26 +25,19 @@ import java.util.UUID; */ public final class EnthrallingHold extends CardImpl { - private static final FilterPermanent filter = new FilterCreaturePermanent(); - - static { - filter.add(TappedPredicate.instance); - } - public EnthrallingHold(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{3}{U}{U}"); this.subtype.add(SubType.AURA); // Enchant creature - TargetPermanent auraTarget = new TargetCreaturePermanent(); + TargetPermanent auraTarget = new EnthrallingHoldTarget(); this.getSpellAbility().addTarget(auraTarget); this.getSpellAbility().addEffect(new AttachEffect(Outcome.GainControl)); Ability ability = new EnchantAbility(auraTarget.getTargetName()); this.addAbility(ability); // You can't choose an untapped creature as this spell's target as you cast it. - auraTarget.replaceFilter(filter); Effect controlEnchantedEffect = new ControlEnchantedEffect(); controlEnchantedEffect.setText("You can't choose an untapped creature as this spell's target as you cast it.
" + controlEnchantedEffect.getText(null)); @@ -62,3 +54,27 @@ public final class EnthrallingHold extends CardImpl { return new EnthrallingHold(this); } } + +class EnthrallingHoldTarget extends TargetCreaturePermanent { + + EnthrallingHoldTarget() {} + + private EnthrallingHoldTarget(EnthrallingHoldTarget target) { + super(target); + } + + @Override + public EnthrallingHoldTarget copy() { + return new EnthrallingHoldTarget(this); + } + + @Override + public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { + if (super.canTarget(controllerId, id, source, game)) { + Permanent permanent = game.getPermanent(id); + return permanent.isTapped(); + } + return false; + } + +} \ No newline at end of file From ca29e61b1d136530803470717ef3422878fd4276 Mon Sep 17 00:00:00 2001 From: htrajan Date: Mon, 29 Jun 2020 01:41:46 -0700 Subject: [PATCH 5/5] [M21] actually fix Enthralling Hold (#6746) --- Mage.Sets/src/mage/cards/d/DreamLeash.java | 7 ++ .../src/mage/cards/e/EnthrallingHold.java | 9 ++- .../cards/single/m21/EnthrallingHoldTest.java | 79 +++++++++++++++++++ Mage/src/main/java/mage/target/Target.java | 2 + .../src/main/java/mage/target/TargetImpl.java | 16 +++- 5 files changed, 110 insertions(+), 3 deletions(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/m21/EnthrallingHoldTest.java diff --git a/Mage.Sets/src/mage/cards/d/DreamLeash.java b/Mage.Sets/src/mage/cards/d/DreamLeash.java index b557bd4bf4..a68a361904 100644 --- a/Mage.Sets/src/mage/cards/d/DreamLeash.java +++ b/Mage.Sets/src/mage/cards/d/DreamLeash.java @@ -12,6 +12,7 @@ import mage.constants.CardType; import mage.constants.Outcome; import mage.constants.SubType; import mage.constants.Zone; +import mage.filter.StaticFilters; import mage.game.Game; import mage.game.permanent.Permanent; import mage.target.TargetPermanent; @@ -73,4 +74,10 @@ class DreamLeashTarget extends TargetPermanent { return false; } + // See ruling: https://www.mtgsalvation.com/forums/magic-fundamentals/magic-rulings/magic-rulings-archives/253345-dream-leash + @Override + public boolean stillLegalTarget(UUID id, Ability source, Game game) { + Permanent permanent = game.getPermanent(id); + return permanent != null && StaticFilters.FILTER_PERMANENT.match(permanent, game); + } } diff --git a/Mage.Sets/src/mage/cards/e/EnthrallingHold.java b/Mage.Sets/src/mage/cards/e/EnthrallingHold.java index fa28c52a39..f1a53c0a3f 100644 --- a/Mage.Sets/src/mage/cards/e/EnthrallingHold.java +++ b/Mage.Sets/src/mage/cards/e/EnthrallingHold.java @@ -12,6 +12,7 @@ import mage.constants.CardType; import mage.constants.Outcome; import mage.constants.SubType; import mage.constants.Zone; +import mage.filter.StaticFilters; import mage.game.Game; import mage.game.permanent.Permanent; import mage.target.TargetPermanent; @@ -27,7 +28,7 @@ public final class EnthrallingHold extends CardImpl { public EnthrallingHold(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{3}{U}{U}"); - + this.subtype.add(SubType.AURA); // Enchant creature @@ -77,4 +78,10 @@ class EnthrallingHoldTarget extends TargetCreaturePermanent { return false; } + // See ruling: https://www.mtgsalvation.com/forums/magic-fundamentals/magic-rulings/magic-rulings-archives/253345-dream-leash + @Override + public boolean stillLegalTarget(UUID id, Ability source, Game game) { + Permanent permanent = game.getPermanent(id); + return permanent != null && StaticFilters.FILTER_PERMANENT_CREATURE.match(permanent, game); + } } \ No newline at end of file diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/m21/EnthrallingHoldTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/m21/EnthrallingHoldTest.java new file mode 100644 index 0000000000..a322395590 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/m21/EnthrallingHoldTest.java @@ -0,0 +1,79 @@ +package org.mage.test.cards.single.m21; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +public class EnthrallingHoldTest extends CardTestPlayerBase { + + @Test + public void testTappedTarget_untapped_doesNotFizzle() { + // Traxos, Scourge of Kroog enters the battlefield tapped and doesn't untap during your untap step. + addCard(Zone.BATTLEFIELD, playerB, "Traxos, Scourge of Kroog"); + + addCard(Zone.BATTLEFIELD, playerA, "Island", 6); + /* + * {3}{U}{U} + * Enchant creature + * You can't choose an untapped creature as this spell's target as you cast it. + * You control enchanted creature. + */ + addCard(Zone.HAND, playerA, "Enthralling Hold"); + /* + * {U} + * You may tap or untap target artifact, creature, or land. + */ + addCard(Zone.HAND, playerA, "Twiddle"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Enthralling Hold", "Traxos, Scourge of Kroog"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Twiddle", "Traxos, Scourge of Kroog"); + + setChoice(playerA, "Yes"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_COMBAT); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerB, "Traxos, Scourge of Kroog", 0); + assertPermanentCount(playerA, "Traxos, Scourge of Kroog", 1); + assertPermanentCount(playerA, "Enthralling Hold", 1); + } + + @Test + public void testTappedTarget_becomesIllegal_fizzles() { + // Traxos, Scourge of Kroog enters the battlefield tapped and doesn't untap during your untap step. + addCard(Zone.BATTLEFIELD, playerB, "Traxos, Scourge of Kroog"); + + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 5); + addCard(Zone.BATTLEFIELD, playerA, "Island", 2); + /* + * {3}{U}{U} + * Enchant creature + * You can't choose an untapped creature as this spell's target as you cast it. + * You control enchanted creature. + */ + addCard(Zone.HAND, playerA, "Enthralling Hold"); + /* + * {1}{B} + * Destroy target nonblack creature + */ + addCard(Zone.HAND, playerA, "Doom Blade"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Enthralling Hold", "Traxos, Scourge of Kroog"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Doom Blade", "Traxos, Scourge of Kroog"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_COMBAT); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerB, "Traxos, Scourge of Kroog", 0); + assertPermanentCount(playerA, "Traxos, Scourge of Kroog", 0); + + assertGraveyardCount(playerB, "Traxos, Scourge of Kroog", 1); + assertGraveyardCount(playerA, "Enthralling Hold", 1); + assertGraveyardCount(playerA, "Doom Blade", 1); + } +} diff --git a/Mage/src/main/java/mage/target/Target.java b/Mage/src/main/java/mage/target/Target.java index 267c97558b..87c6e89c07 100644 --- a/Mage/src/main/java/mage/target/Target.java +++ b/Mage/src/main/java/mage/target/Target.java @@ -52,6 +52,8 @@ public interface Target extends Serializable { boolean canTarget(UUID id, Ability source, Game game); + boolean stillLegalTarget(UUID id, Ability source, Game game); + boolean canTarget(UUID playerId, UUID id, Ability source, Game game); boolean isLegal(Ability source, Game game); diff --git a/Mage/src/main/java/mage/target/TargetImpl.java b/Mage/src/main/java/mage/target/TargetImpl.java index 9d0eaebd0d..c7e656c70e 100644 --- a/Mage/src/main/java/mage/target/TargetImpl.java +++ b/Mage/src/main/java/mage/target/TargetImpl.java @@ -12,7 +12,14 @@ import mage.game.events.GameEvent.EventType; import mage.players.Player; import mage.util.RandomUtil; -import java.util.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; /** * @author BetaSteward_at_googlemail.com @@ -337,7 +344,7 @@ public abstract class TargetImpl implements Target { illegalTargets.add(targetId); continue; } - if (!canTarget(targetId, source, game)) { + if (!stillLegalTarget(targetId, source, game)) { illegalTargets.add(targetId); } } @@ -473,6 +480,11 @@ public abstract class TargetImpl implements Target { return null; } + @Override + public boolean stillLegalTarget(UUID id, Ability source, Game game) { + return canTarget(id, source, game); + } + @Override public void setNotTarget(boolean notTarget) { this.notTarget = notTarget;