diff --git a/Mage.Client/src/main/java/mage/client/MageFrame.java b/Mage.Client/src/main/java/mage/client/MageFrame.java index 5b6204f1d8..0f2fe732d2 100644 --- a/Mage.Client/src/main/java/mage/client/MageFrame.java +++ b/Mage.Client/src/main/java/mage/client/MageFrame.java @@ -32,6 +32,7 @@ import java.awt.event.*; import java.awt.image.BufferedImage; import java.io.IOException; import java.io.InputStream; +import java.net.SocketException; import java.util.*; import java.util.List; import java.util.concurrent.Executors; @@ -726,6 +727,12 @@ public class MageFrame extends javax.swing.JFrame implements MageClient { currentConnection.setPassword(password); currentConnection.setHost(server); currentConnection.setPort(port); + String allMAC = ""; + try { + allMAC = currentConnection.getMAC(); + } catch (SocketException ex) { + } + currentConnection.setUserIdStr(System.getProperty("user.name") + ":" + System.getProperty("os.name") + ":" + MagePreferences.getUserNames() + ":" + allMAC); currentConnection.setProxyType(proxyType); currentConnection.setProxyHost(proxyServer); currentConnection.setProxyPort(proxyPort); diff --git a/Mage.Client/src/main/java/mage/client/dialog/TableWaitingDialog.java b/Mage.Client/src/main/java/mage/client/dialog/TableWaitingDialog.java index d6492a0a39..a981e896ed 100644 --- a/Mage.Client/src/main/java/mage/client/dialog/TableWaitingDialog.java +++ b/Mage.Client/src/main/java/mage/client/dialog/TableWaitingDialog.java @@ -54,6 +54,7 @@ import mage.client.util.GUISizeHelper; import mage.client.util.audio.AudioManager; import mage.client.util.gui.TableUtil; import mage.client.util.gui.countryBox.CountryCellRenderer; +import mage.players.PlayerType; import mage.remote.Session; import mage.view.SeatView; import mage.view.TableView; @@ -437,6 +438,7 @@ class UpdateSeatsTask extends SwingWorker { AudioManager.playPlayerJoinedTable(); } else { MageTray.instance.displayMessage("A player left your game."); + AudioManager.playPlayerLeft(); } MageTray.instance.blink(); } @@ -450,7 +452,7 @@ class UpdateSeatsTask extends SwingWorker { int playerCount = 0; if (tableView != null) { for (SeatView seatView : tableView.getSeats()) { - if (seatView.getPlayerId() != null && seatView.getPlayerType().equals("Human")) { + if (seatView.getPlayerId() != null && seatView.getPlayerType() == PlayerType.HUMAN) { playerCount++; } } diff --git a/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Commander.java b/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Commander.java index 23e6ee2ec1..5d3bf90c67 100644 --- a/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Commander.java +++ b/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Commander.java @@ -27,6 +27,7 @@ */ package mage.deck; +import java.util.*; import mage.ObjectColor; import mage.abilities.Ability; import mage.abilities.common.CanBeYourCommanderAbility; @@ -41,8 +42,6 @@ import mage.constants.SetType; import mage.filter.FilterMana; import mage.util.CardUtil; -import java.util.*; - /** * * @author Plopman @@ -71,6 +70,7 @@ public class Commander extends Constructed { banned.add("Gifts Ungiven"); banned.add("Griselbrand"); banned.add("Karakas"); + banned.add("Leovold, Emissary of Trest"); banned.add("Library of Alexandria"); banned.add("Limited Resources"); banned.add("Mox Emerald"); @@ -82,7 +82,6 @@ public class Commander extends Constructed { banned.add("Panoptic Mirror"); banned.add("Primeval Titan"); banned.add("Prophet of Kruphix"); - banned.add("Protean Hulk"); banned.add("Recurring Nightmare"); banned.add("Rofellos, Llanowar Emissary"); banned.add("Sundering Titan"); diff --git a/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Legacy.java b/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Legacy.java index 13fc442863..0483682303 100644 --- a/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Legacy.java +++ b/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Legacy.java @@ -97,6 +97,7 @@ public class Legacy extends Constructed { banned.add("Rebirth"); banned.add("Secret Summoning"); banned.add("Secrets of Paradise"); + banned.add("Sensei's Divining Top"); banned.add("Sentinel Dispatch"); banned.add("Shahrazad"); banned.add("Skullclamp"); diff --git a/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Vintage.java b/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Vintage.java index 1dcdb14614..52c7a29245 100644 --- a/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Vintage.java +++ b/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Vintage.java @@ -82,6 +82,8 @@ public class Vintage extends Constructed { restricted.add("Dig Through Time"); restricted.add("Fastbond"); restricted.add("Flash"); + restricted.add("Gitaxian Probe"); + restricted.add("Gush"); restricted.add("Imperial Seal"); restricted.add("Library of Alexandria"); restricted.add("Lion’s Eye Diamond"); diff --git a/Mage.Server/src/main/java/mage/server/ChatManager.java b/Mage.Server/src/main/java/mage/server/ChatManager.java index e7f921de2b..bc4bf425b0 100644 --- a/Mage.Server/src/main/java/mage/server/ChatManager.java +++ b/Mage.Server/src/main/java/mage/server/ChatManager.java @@ -181,11 +181,14 @@ public enum ChatManager { + "
\\me - shows the history of the current player" + "
\\list or \\l - Show a list of commands" + "
\\whisper or \\w [player name] [text] - whisper to the player with the given name" + + "
\\card Card Name - Print oracle text for card" + "
[Card Name] - Show a highlighted card name" + "
\\ignore - shows current ignore list on this server." + "
\\ignore [username] - add a username to your ignore list on this server." + "
\\unignore [username] - remove a username from your ignore list on this server."; + final Pattern getCardTextPattern = Pattern.compile("^.card *(.*)"); + private boolean performUserCommand(User user, String message, UUID chatId, boolean doError) { String command = message.substring(1).trim().toUpperCase(Locale.ENGLISH); if (doError) { @@ -205,6 +208,25 @@ public enum ChatManager { chatSessions.get(chatId).broadcastInfoToUser(user, message); return true; } + if (command.startsWith("CARD ")) { + Matcher matchPattern = getCardTextPattern.matcher(message.toLowerCase()); + if (matchPattern.find()) { + String cardName = matchPattern.group(1); + CardInfo cardInfo = CardRepository.instance.findPreferedCoreExpansionCard(cardName, true); + if (cardInfo != null) { + cardInfo.getRules(); + message = "" + cardInfo.getName() + ": Cost:" + cardInfo.getManaCosts().toString() + ", Types:" + cardInfo.getTypes().toString() + ", "; + for (String rule : cardInfo.getRules()) { + message = message + rule; + } + } else { + message = "Couldn't find: " + cardName; + + } + } + chatSessions.get(chatId).broadcastInfoToUser(user, message); + return true; + } if (command.startsWith("W ") || command.startsWith("WHISPER ")) { String rest = message.substring(command.startsWith("W ") ? 3 : 9); int first = rest.indexOf(' '); diff --git a/Mage.Server/src/main/java/mage/server/MageServerImpl.java b/Mage.Server/src/main/java/mage/server/MageServerImpl.java index 0d75682167..039a25a81a 100644 --- a/Mage.Server/src/main/java/mage/server/MageServerImpl.java +++ b/Mage.Server/src/main/java/mage/server/MageServerImpl.java @@ -636,7 +636,8 @@ public class MageServerImpl implements MageServer { } }); } else { - logger.error("table not found : " + tableId); + // this can happen if a game ends and a player quits XMage or a match nearly at the same time as the game ends + logger.trace("table not found : " + tableId); } return true; } @@ -1119,12 +1120,12 @@ public class MageServerImpl implements MageServer { public void toggleActivation(final String sessionId, final String userName) throws MageException { execute("toggleActivation", sessionId, () -> UserManager.instance.getUserByName(userName).ifPresent(user - -> { - user.setActive(!user.isActive()); - if (!user.isActive() && user.isConnected()) { - SessionManager.instance.disconnectUser(sessionId, user.getSessionId()); - } - })); + -> { + user.setActive(!user.isActive()); + if (!user.isActive() && user.isConnected()) { + SessionManager.instance.disconnectUser(sessionId, user.getSessionId()); + } + })); } @Override @@ -1159,8 +1160,8 @@ public class MageServerImpl implements MageServer { if (title != null && message != null) { execute("sendFeedbackMessage", sessionId, () -> SessionManager.instance.getSession(sessionId).ifPresent( - session -> FeedbackServiceImpl.instance.feedback(username, title, type, message, email, session.getHost()) - )); + session -> FeedbackServiceImpl.instance.feedback(username, title, type, message, email, session.getHost()) + )); } } diff --git a/Mage.Server/src/main/java/mage/server/Session.java b/Mage.Server/src/main/java/mage/server/Session.java index bdc2744cac..e06a561335 100644 --- a/Mage.Server/src/main/java/mage/server/Session.java +++ b/Mage.Server/src/main/java/mage/server/Session.java @@ -27,6 +27,11 @@ */ package mage.server; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import mage.MageException; import mage.constants.Constants; import mage.interfaces.callback.ClientCallback; @@ -44,12 +49,6 @@ import org.jboss.remoting.callback.Callback; import org.jboss.remoting.callback.HandleCallbackException; import org.jboss.remoting.callback.InvokerCallbackHandler; -import java.util.*; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.ReentrantLock; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - /** * @author BetaSteward_at_googlemail.com */ @@ -218,8 +217,8 @@ public class Session { if (authorizedUser.lockedUntil.compareTo(Calendar.getInstance().getTime()) > 0) { return "Your profile is deactivated until " + SystemUtil.dateFormat.format(authorizedUser.lockedUntil); } else { - UserManager.instance.createUser(userName, host, authorizedUser).ifPresent(user -> - user.setLockedUntil(null) + UserManager.instance.createUser(userName, host, authorizedUser).ifPresent(user + -> user.setLockedUntil(null) ); } @@ -263,7 +262,6 @@ public class Session { ChatManager.instance.sendReconnectMessage(userId); } - return null; } @@ -337,7 +335,7 @@ public class Session { lockSet = true; logger.debug("SESSION LOCK SET sessionId: " + sessionId); } else { - logger.error("CAN'T GET LOCK - userId: " + userId + " hold count: " + lock.getHoldCount()); + logger.warn("CAN'T GET LOCK - userId: " + userId + " hold count: " + lock.getHoldCount()); } Optional _user = UserManager.instance.getUser(userId); if (!_user.isPresent()) { @@ -393,7 +391,7 @@ public class Session { call.setMessageId(messageId++); callbackHandler.handleCallbackOneway(new Callback(call)); } catch (HandleCallbackException ex) { - ex.printStackTrace(); + // ex.printStackTrace(); UserManager.instance.getUser(userId).ifPresent(user -> { logger.warn("SESSION CALLBACK EXCEPTION - " + user.getName() + " userId " + userId); logger.warn(" - method: " + call.getMethod()); diff --git a/Mage.Server/src/main/java/mage/server/SessionManager.java b/Mage.Server/src/main/java/mage/server/SessionManager.java index 5ddc333875..1b66fce0c1 100644 --- a/Mage.Server/src/main/java/mage/server/SessionManager.java +++ b/Mage.Server/src/main/java/mage/server/SessionManager.java @@ -144,8 +144,10 @@ public enum SessionManager { case LostConnection: // user lost connection - session expires countdaoun starts session.userLostConnection(); break; + case ConnectingOtherInstance: + break; default: - logger.error("endSession: unexpected reason " + reason.toString() + " - sessionId: " + sessionId); + logger.trace("endSession: unexpected reason " + reason.toString() + " - sessionId: " + sessionId); } } else { sessions.remove(sessionId); diff --git a/Mage.Sets/src/mage/cards/c/CommitMemory.java b/Mage.Sets/src/mage/cards/c/CommitMemory.java index dcd63f6ed8..1ce9ffe6fa 100644 --- a/Mage.Sets/src/mage/cards/c/CommitMemory.java +++ b/Mage.Sets/src/mage/cards/c/CommitMemory.java @@ -41,9 +41,8 @@ import mage.constants.CardType; import mage.constants.Outcome; import mage.constants.SpellAbilityType; import mage.constants.Zone; +import mage.filter.common.FilterNonlandPermanent; import mage.filter.common.FilterSpellOrPermanent; -import mage.filter.predicate.Predicates; -import mage.filter.predicate.mageobject.CardTypePredicate; import mage.game.Game; import mage.game.permanent.Permanent; import mage.game.stack.Spell; @@ -59,7 +58,7 @@ public class CommitMemory extends SplitCard { private static final FilterSpellOrPermanent filter = new FilterSpellOrPermanent("spell or nonland permanent"); static { - filter.add(Predicates.not(new CardTypePredicate(CardType.LAND))); + filter.setPermanentFilter(new FilterNonlandPermanent()); } public CommitMemory(UUID ownerId, CardSetInfo setInfo) { diff --git a/Mage.Sets/src/mage/cards/d/Dogpile.java b/Mage.Sets/src/mage/cards/d/Dogpile.java new file mode 100644 index 0000000000..18c8329fa0 --- /dev/null +++ b/Mage.Sets/src/mage/cards/d/Dogpile.java @@ -0,0 +1,71 @@ +/* + * Copyright 2010 BetaSteward_at_googlemail.com. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY BetaSteward_at_googlemail.com ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BetaSteward_at_googlemail.com OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of BetaSteward_at_googlemail.com. + */ +package mage.cards.d; + +import java.util.UUID; +import mage.abilities.dynamicvalue.common.PermanentsOnBattlefieldCount; +import mage.abilities.effects.common.DamageTargetEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.TargetController; +import mage.filter.common.FilterAttackingCreature; +import mage.filter.predicate.permanent.ControllerPredicate; +import mage.target.common.TargetCreatureOrPlayer; + +/** + * + * @author LevelX2 + */ +public class Dogpile extends CardImpl { + + private static final FilterAttackingCreature filter = new FilterAttackingCreature("attacking creatures you control"); + + static { + filter.add(new ControllerPredicate(TargetController.YOU)); + } + + public Dogpile(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{3}{R}"); + + // Dogpile deals damage to target creature or player equal to the number of attacking creatures you control. + this.getSpellAbility().addEffect(new DamageTargetEffect(new PermanentsOnBattlefieldCount(filter)). + setText("{this} deals damage to target creature or player equal to the number of attacking creatures you control")); + this.getSpellAbility().addTarget(new TargetCreatureOrPlayer()); + + } + + public Dogpile(final Dogpile card) { + super(card); + } + + @Override + public Dogpile copy() { + return new Dogpile(this); + } +} diff --git a/Mage.Sets/src/mage/cards/o/OratorOfOjutai.java b/Mage.Sets/src/mage/cards/o/OratorOfOjutai.java index d6bc753316..eca4ad6bd0 100644 --- a/Mage.Sets/src/mage/cards/o/OratorOfOjutai.java +++ b/Mage.Sets/src/mage/cards/o/OratorOfOjutai.java @@ -31,9 +31,10 @@ import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; -import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.TriggeredAbilityImpl; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.costs.common.RevealTargetFromHandCost; +import mage.abilities.effects.Effect; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.InfoEffect; import mage.abilities.keyword.DefenderAbility; @@ -47,6 +48,7 @@ import mage.constants.Zone; import mage.filter.FilterCard; import mage.filter.predicate.mageobject.SubtypePredicate; import mage.game.Game; +import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.players.Player; import mage.target.common.TargetCardInHand; @@ -65,7 +67,7 @@ public class OratorOfOjutai extends CardImpl { } public OratorOfOjutai(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{1}{W}"); + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{W}"); this.subtype.add("Bird"); this.subtype.add("Monk"); this.power = new MageInt(0); @@ -79,7 +81,7 @@ public class OratorOfOjutai extends CardImpl { this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new InfoEffect("As an additional cost to cast {this}, you may reveal a Dragon card from your hand"))); // When Orator of Ojutai enters the battlefield, if you revealed a Dragon card or controlled a Dragon as you cast Orator of Ojutai, draw a card. - this.addAbility(new EntersBattlefieldTriggeredAbility(new OratorOfOjutaiEffect()), new DragonOnTheBattlefieldWhileSpellWasCastWatcher()); + this.addAbility(new OratorOfOjutaiTriggeredAbility(new OratorOfOjutaiEffect()), new DragonOnTheBattlefieldWhileSpellWasCastWatcher()); } @Override @@ -88,7 +90,7 @@ public class OratorOfOjutai extends CardImpl { Player controller = game.getPlayer(ability.getControllerId()); if (controller != null) { if (controller.getHand().count(filter, game) > 0) { - ability.addCost(new RevealTargetFromHandCost(new TargetCardInHand(0,1, filter))); + ability.addCost(new RevealTargetFromHandCost(new TargetCardInHand(0, 1, filter))); } } } @@ -104,6 +106,42 @@ public class OratorOfOjutai extends CardImpl { } } +class OratorOfOjutaiTriggeredAbility extends TriggeredAbilityImpl { + + public OratorOfOjutaiTriggeredAbility(Effect effect) { + super(Zone.BATTLEFIELD, effect, false); + } + + public OratorOfOjutaiTriggeredAbility(final OratorOfOjutaiTriggeredAbility ability) { + super(ability); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.ENTERS_THE_BATTLEFIELD; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + //Intervening if must be checked + Permanent sourcePermanent = game.getPermanentOrLKIBattlefield(getSourceId()); + DragonOnTheBattlefieldWhileSpellWasCastWatcher watcher = (DragonOnTheBattlefieldWhileSpellWasCastWatcher) game.getState().getWatchers().get("DragonOnTheBattlefieldWhileSpellWasCastWatcher"); + return event.getTargetId().equals(getSourceId()) + && watcher != null + && watcher.castWithConditionTrue(sourcePermanent.getSpellAbility().getId()); + } + + @Override + public String getRule() { + return "When {this} enters the battlefield, " + super.getRule(); + } + + @Override + public OratorOfOjutaiTriggeredAbility copy() { + return new OratorOfOjutaiTriggeredAbility(this); + } +} + class OratorOfOjutaiEffect extends OneShotEffect { public OratorOfOjutaiEffect() { @@ -122,6 +160,7 @@ class OratorOfOjutaiEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { + //Intervening if is checked again on resolution Player controller = game.getPlayer(source.getControllerId()); if (controller != null) { Permanent sourcePermanent = game.getPermanentOrLKIBattlefield(source.getSourceId()); @@ -129,9 +168,9 @@ class OratorOfOjutaiEffect extends OneShotEffect { DragonOnTheBattlefieldWhileSpellWasCastWatcher watcher = (DragonOnTheBattlefieldWhileSpellWasCastWatcher) game.getState().getWatchers().get("DragonOnTheBattlefieldWhileSpellWasCastWatcher"); if (watcher != null && watcher.castWithConditionTrue(sourcePermanent.getSpellAbility().getId())) { controller.drawCards(1, game); + return true; } } - return true; } return false; } diff --git a/Mage.Sets/src/mage/cards/s/SabertoothAlleyCat.java b/Mage.Sets/src/mage/cards/s/SabertoothAlleyCat.java new file mode 100644 index 0000000000..3324ebeaae --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SabertoothAlleyCat.java @@ -0,0 +1,81 @@ +/* + * Copyright 2010 BetaSteward_at_googlemail.com. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY BetaSteward_at_googlemail.com ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BetaSteward_at_googlemail.com OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of BetaSteward_at_googlemail.com. + */ +package mage.cards.s; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.common.AttacksEachTurnStaticAbility; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.combat.CantBeBlockedByCreaturesSourceEffect; +import mage.abilities.keyword.DefenderAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.Zone; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.mageobject.AbilityPredicate; + +/** + * + * @author LevelX2 + */ +public class SabertoothAlleyCat extends CardImpl { + + public SabertoothAlleyCat(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{R}{R}"); + + this.subtype.add("Cat"); + this.power = new MageInt(2); + this.toughness = new MageInt(1); + + // Sabertooth Alley Cat attacks each turn if able. + this.addAbility(new AttacksEachTurnStaticAbility()); + + // {1}{R}: Creatures without defender can't block Sabertooth Alley Cat this turn. + this.addAbility(new SimpleActivatedAbility( + Zone.BATTLEFIELD, + new CantBeBlockedByCreaturesSourceEffect( + (FilterCreaturePermanent) new FilterCreaturePermanent().add(Predicates.not(new AbilityPredicate(DefenderAbility.class))), + Duration.EndOfTurn + ) + .setText("Creatures without defender can't block {this} this turn"), + new ManaCostsImpl<>("{1}{R}"))); + } + + public SabertoothAlleyCat(final SabertoothAlleyCat card) { + super(card); + } + + @Override + public SabertoothAlleyCat copy() { + return new SabertoothAlleyCat(this); + } +} diff --git a/Mage.Sets/src/mage/cards/s/SeedsOfStrength.java b/Mage.Sets/src/mage/cards/s/SeedsOfStrength.java new file mode 100644 index 0000000000..39eae023a4 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SeedsOfStrength.java @@ -0,0 +1,74 @@ +/* + * Copyright 2010 BetaSteward_at_googlemail.com. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY BetaSteward_at_googlemail.com ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BetaSteward_at_googlemail.com OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of BetaSteward_at_googlemail.com. + */ +package mage.cards.s; + +import java.util.UUID; +import mage.abilities.effects.Effect; +import mage.abilities.effects.common.continuous.BoostTargetEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.filter.common.FilterCreaturePermanent; +import mage.target.common.TargetCreaturePermanent; +import mage.target.targetpointer.SecondTargetPointer; +import mage.target.targetpointer.ThirdTargetPointer; + +/** + * + * @author LevelX2 + */ +public class SeedsOfStrength extends CardImpl { + + public SeedsOfStrength(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{G}{W}"); + + // Target creature gets +1/+1 until end of turn. + this.getSpellAbility().addEffect(new BoostTargetEffect(1, 1, Duration.EndOfTurn)); + this.getSpellAbility().addTarget(new TargetCreaturePermanent(new FilterCreaturePermanent("creature (1st)"))); + // Target creature gets +1/+1 until end of turn. + Effect effect = new BoostTargetEffect(1, 1, Duration.EndOfTurn).setText("
Target creature gets +1/+1 until end of turn."); + effect.setTargetPointer(new SecondTargetPointer()); + this.getSpellAbility().addEffect(effect); + this.getSpellAbility().addTarget(new TargetCreaturePermanent(new FilterCreaturePermanent("creature (2nd)"))); + // Target creature gets +1/+1 until end of turn. + effect = new BoostTargetEffect(1, 1, Duration.EndOfTurn).setText("
Target creature gets +1/+1 until end of turn."); + effect.setTargetPointer(new ThirdTargetPointer()); + this.getSpellAbility().addEffect(effect); + this.getSpellAbility().addTarget(new TargetCreaturePermanent(new FilterCreaturePermanent("creature (3rd)"))); + } + + public SeedsOfStrength(final SeedsOfStrength card) { + super(card); + } + + @Override + public SeedsOfStrength copy() { + return new SeedsOfStrength(this); + } +} diff --git a/Mage.Sets/src/mage/cards/t/TheMimeoplasm.java b/Mage.Sets/src/mage/cards/t/TheMimeoplasm.java index fc9d2d8a78..e9d0598607 100644 --- a/Mage.Sets/src/mage/cards/t/TheMimeoplasm.java +++ b/Mage.Sets/src/mage/cards/t/TheMimeoplasm.java @@ -27,6 +27,7 @@ */ package mage.cards.t; +import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.AsEntersBattlefieldAbility; @@ -37,13 +38,13 @@ import mage.cards.*; import mage.constants.*; import mage.counters.CounterType; import mage.filter.common.FilterCreatureCard; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.mageobject.CardIdPredicate; import mage.game.Game; import mage.game.permanent.Permanent; import mage.players.Player; import mage.target.common.TargetCardInGraveyard; -import java.util.UUID; - /** * * @author emerald000 @@ -51,7 +52,7 @@ import java.util.UUID; public class TheMimeoplasm extends CardImpl { public TheMimeoplasm(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{2}{G}{U}{B}"); + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{G}{U}{B}"); addSuperType(SuperType.LEGENDARY); this.subtype.add("Ooze"); @@ -95,10 +96,12 @@ class TheMimeoplasmEffect extends OneShotEffect { if (new CardsInAllGraveyardsCount(new FilterCreatureCard()).calculate(game, source, this) >= 2) { if (controller.chooseUse(Outcome.Benefit, "Do you want to exile two creature cards from graveyards?", source, game)) { TargetCardInGraveyard targetCopy = new TargetCardInGraveyard(new FilterCreatureCard("creature card to become a copy of")); - TargetCardInGraveyard targetCounters = new TargetCardInGraveyard(new FilterCreatureCard("creature card to determine amount of additional +1/+1 counters")); if (controller.choose(Outcome.Copy, targetCopy, source.getSourceId(), game)) { Card cardToCopy = game.getCard(targetCopy.getFirstTarget()); if (cardToCopy != null) { + FilterCreatureCard filter = new FilterCreatureCard("creature card to determine amount of additional +1/+1 counters"); + filter.add(Predicates.not(new CardIdPredicate(cardToCopy.getId()))); + TargetCardInGraveyard targetCounters = new TargetCardInGraveyard(filter); if (controller.choose(Outcome.Copy, targetCounters, source.getSourceId(), game)) { Card cardForCounters = game.getCard(targetCounters.getFirstTarget()); if (cardForCounters != null) { diff --git a/Mage.Sets/src/mage/cards/t/TorrentialGearhulk.java b/Mage.Sets/src/mage/cards/t/TorrentialGearhulk.java index 0f5bcee906..c0a86a5e3e 100644 --- a/Mage.Sets/src/mage/cards/t/TorrentialGearhulk.java +++ b/Mage.Sets/src/mage/cards/t/TorrentialGearhulk.java @@ -50,6 +50,7 @@ import mage.game.events.ZoneChangeEvent; import mage.players.Player; import mage.target.common.TargetCardInYourGraveyard; import mage.target.targetpointer.FixedTarget; +import org.apache.log4j.Logger; /** * @@ -64,7 +65,7 @@ public class TorrentialGearhulk extends CardImpl { } public TorrentialGearhulk(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.ARTIFACT,CardType.CREATURE},"{4}{U}{U}"); + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT, CardType.CREATURE}, "{4}{U}{U}"); this.subtype.add("Construct"); this.power = new MageInt(5); this.toughness = new MageInt(6); @@ -111,7 +112,7 @@ class TorrentialGearhulkEffect extends OneShotEffect { Player controller = game.getPlayer(source.getControllerId()); if (controller != null) { Card card = game.getCard(this.getTargetPointer().getFirst(game, source)); - if (card != null) { + if (card != null && card.getSpellAbility() != null) { if (controller.chooseUse(outcome, "Cast " + card.getLogName() + '?', source, game)) { if (controller.cast(card.getSpellAbility(), game, true)) { ContinuousEffect effect = new TorrentialGearhulkReplacementEffect(card.getId()); @@ -119,6 +120,9 @@ class TorrentialGearhulkEffect extends OneShotEffect { game.addEffect(effect, source); } } + } else { + Logger.getLogger(TorrentialGearhulkEffect.class).error("Torrential Gearhulk - Instant card without spellAbility : " + card.getName()); + return false; } return true; } diff --git a/Mage.Sets/src/mage/cards/w/WireflyHive.java b/Mage.Sets/src/mage/cards/w/WireflyHive.java index a57ec8e766..e843b17b29 100644 --- a/Mage.Sets/src/mage/cards/w/WireflyHive.java +++ b/Mage.Sets/src/mage/cards/w/WireflyHive.java @@ -35,6 +35,7 @@ import mage.abilities.costs.mana.GenericManaCost; import mage.abilities.effects.common.CreateTokenEffect; import mage.abilities.effects.common.DestroyAllEffect; import mage.abilities.effects.common.FlipCoinEffect; +import mage.abilities.keyword.FlyingAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -74,12 +75,13 @@ public class WireflyHive extends CardImpl { class WireflyToken extends Token { WireflyToken() { - super("Wirefly", "2/2 colorless Insect artifact creature token named Wirefly"); + super("Wirefly", "2/2 colorless Insect artifact creature token with flying named Wirefly"); this.setOriginalExpansionSetCode("DST"); this.getPower().modifyBaseValue(2); this.getToughness().modifyBaseValue(2); this.getSubtype(null).add("Insect"); this.addCardType(CardType.ARTIFACT); this.addCardType(CardType.CREATURE); + this.addAbility(FlyingAbility.getInstance()); } } diff --git a/Mage.Sets/src/mage/sets/MasterpieceSeriesAmonkhet.java b/Mage.Sets/src/mage/sets/MasterpieceSeriesAmonkhet.java index 18b783f003..38829eac36 100644 --- a/Mage.Sets/src/mage/sets/MasterpieceSeriesAmonkhet.java +++ b/Mage.Sets/src/mage/sets/MasterpieceSeriesAmonkhet.java @@ -56,6 +56,7 @@ public class MasterpieceSeriesAmonkhet extends ExpansionSet { cards.add(new SetCardInfo("Attrition", 19, Rarity.SPECIAL, mage.cards.a.Attrition.class)); cards.add(new SetCardInfo("Austere Command", 1, Rarity.SPECIAL, mage.cards.a.AustereCommand.class)); cards.add(new SetCardInfo("Aven Mindcensor", 2, Rarity.SPECIAL, mage.cards.a.AvenMindcensor.class)); + cards.add(new SetCardInfo("Bontu the Glorified", 20, Rarity.SPECIAL, mage.cards.b.BontuTheGlorified.class)); cards.add(new SetCardInfo("Chain Lightning", 26, Rarity.SPECIAL, mage.cards.c.ChainLightning.class)); cards.add(new SetCardInfo("Consecrated Sphinx", 8, Rarity.SPECIAL, mage.cards.c.ConsecratedSphinx.class)); cards.add(new SetCardInfo("Containment Priest", 3, Rarity.SPECIAL, mage.cards.c.ContainmentPriest.class)); @@ -75,6 +76,7 @@ public class MasterpieceSeriesAmonkhet extends ExpansionSet { cards.add(new SetCardInfo("Mind Twist", 24, Rarity.SPECIAL, mage.cards.m.MindTwist.class)); cards.add(new SetCardInfo("Oketra the True", 5, Rarity.SPECIAL, mage.cards.o.OketraTheTrue.class)); cards.add(new SetCardInfo("Pact of Negation", 16, Rarity.SPECIAL, mage.cards.p.PactOfNegation.class)); + cards.add(new SetCardInfo("Rhonas the Indomitable", 28, Rarity.SPECIAL, mage.cards.r.RhonasTheIndomitable.class)); cards.add(new SetCardInfo("Spell Pierce", 17, Rarity.SPECIAL, mage.cards.s.SpellPierce.class)); cards.add(new SetCardInfo("Stifle", 18, Rarity.SPECIAL, mage.cards.s.Stifle.class)); cards.add(new SetCardInfo("Vindicate", 30, Rarity.SPECIAL, mage.cards.v.Vindicate.class)); diff --git a/Mage.Sets/src/mage/sets/RavnicaCityOfGuilds.java b/Mage.Sets/src/mage/sets/RavnicaCityOfGuilds.java index dc7fdfb3d6..603d0bf916 100644 --- a/Mage.Sets/src/mage/sets/RavnicaCityOfGuilds.java +++ b/Mage.Sets/src/mage/sets/RavnicaCityOfGuilds.java @@ -117,6 +117,7 @@ public class RavnicaCityOfGuilds extends ExpansionSet { cards.add(new SetCardInfo("Disembowel", 85, Rarity.COMMON, mage.cards.d.Disembowel.class)); cards.add(new SetCardInfo("Divebomber Griffin", 14, Rarity.UNCOMMON, mage.cards.d.DivebomberGriffin.class)); cards.add(new SetCardInfo("Dizzy Spell", 43, Rarity.COMMON, mage.cards.d.DizzySpell.class)); + cards.add(new SetCardInfo("Dogpile", 120, Rarity.COMMON, mage.cards.d.Dogpile.class)); cards.add(new SetCardInfo("Doubling Season", 158, Rarity.RARE, mage.cards.d.DoublingSeason.class)); cards.add(new SetCardInfo("Dowsing Shaman", 159, Rarity.UNCOMMON, mage.cards.d.DowsingShaman.class)); cards.add(new SetCardInfo("Drake Familiar", 44, Rarity.COMMON, mage.cards.d.DrakeFamiliar.class)); @@ -252,6 +253,7 @@ public class RavnicaCityOfGuilds extends ExpansionSet { cards.add(new SetCardInfo("Rolling Spoil", 179, Rarity.UNCOMMON, mage.cards.r.RollingSpoil.class)); cards.add(new SetCardInfo("Roofstalker Wight", 102, Rarity.COMMON, mage.cards.r.RoofstalkerWight.class)); cards.add(new SetCardInfo("Root-Kin Ally", 180, Rarity.UNCOMMON, mage.cards.r.RootKinAlly.class)); + cards.add(new SetCardInfo("Sabertooth Alley Cat", 140, Rarity.COMMON, mage.cards.s.SabertoothAlleyCat.class)); cards.add(new SetCardInfo("Sacred Foundry", 280, Rarity.RARE, mage.cards.s.SacredFoundry.class)); cards.add(new SetCardInfo("Sadistic Augermage", 103, Rarity.COMMON, mage.cards.s.SadisticAugermage.class)); cards.add(new SetCardInfo("Sandsower", 28, Rarity.UNCOMMON, mage.cards.s.Sandsower.class)); @@ -261,6 +263,7 @@ public class RavnicaCityOfGuilds extends ExpansionSet { cards.add(new SetCardInfo("Scion of the Wild", 182, Rarity.RARE, mage.cards.s.ScionOfTheWild.class)); cards.add(new SetCardInfo("Searing Meditation", 226, Rarity.RARE, mage.cards.s.SearingMeditation.class)); cards.add(new SetCardInfo("Seed Spark", 30, Rarity.UNCOMMON, mage.cards.s.SeedSpark.class)); + cards.add(new SetCardInfo("Seeds of Strength", 227, Rarity.COMMON, mage.cards.s.SeedsOfStrength.class)); cards.add(new SetCardInfo("Seismic Spike", 141, Rarity.COMMON, mage.cards.s.SeismicSpike.class)); cards.add(new SetCardInfo("Selesnya Evangel", 228, Rarity.COMMON, mage.cards.s.SelesnyaEvangel.class)); cards.add(new SetCardInfo("Selesnya Guildmage", 252, Rarity.UNCOMMON, mage.cards.s.SelesnyaGuildmage.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ExertTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ExertTest.java index dccfcf7004..a051159326 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ExertTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ExertTest.java @@ -88,4 +88,31 @@ public class ExertTest extends CardTestPlayerBase { assertLife(playerB, 24); assertTapped(gbInitiate, false); // stolen creature exerted does untap during owner's untap step } + + @Test + public void combatCelebrantExertedCannotAttackDuringNextCombatPhase() { + /* + Combat Celebrant 2R + Creature - Human Warrior 4/1 + If Combat Celebrant hasn't been exerted this turn, you may exert it as it attacks. When you do, untap all other creatures you control and after this phase, there is an additional combat phase. + */ + String cCelebrant = "Combat Celebrant"; + String memnite = "Memnite"; // {0} 1/1 + + addCard(Zone.BATTLEFIELD, playerA, cCelebrant); + addCard(Zone.BATTLEFIELD, playerA, memnite); + + attack(1, playerA, cCelebrant); + attack(1, playerA, memnite); + setChoice(playerA, "Yes"); // exert for extra turn and untap all creatures + attack(1, playerA, cCelebrant); // should not be able to attack again due to "if has not been exerted this turn" + attack(1, playerA, memnite); + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertLife(playerB, 14); // 4 + 1 + 1 (Celebrant once, Memnite twice) + assertTapped(cCelebrant, true); + assertTapped(memnite, true); + } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/other/AuratouchedMageTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/other/AuratouchedMageTest.java index 70259f1138..b5ade08e39 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/other/AuratouchedMageTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/other/AuratouchedMageTest.java @@ -29,6 +29,7 @@ package org.mage.test.cards.abilities.other; import mage.constants.PhaseStep; import mage.constants.Zone; +import org.junit.Ignore; import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; @@ -46,9 +47,9 @@ public class AuratouchedMageTest extends CardTestPlayerBase { * card and put it into your hand. Then shuffle your library. * */ - /* - @Ignore //If someone knows the way to elegantly handle the test mechanism in regards to no valid targets, please modify. The test works fine in practice. - @Test + + //If someone knows the way to elegantly handle the test mechanism in regards to no valid targets, please modify. The test works fine in practice. + @Ignore public void testAuratouchedMageEffectHasMadeIntoTypeArtifact() { //Any Aura card you find must be able to enchant Auratouched Mage as it currently exists, or as it most recently existed on the battlefield if it’s no //longer on the battlefield. If an effect has made the Mage an artifact, for example, you could search for an Aura with “enchant artifact.” @@ -70,7 +71,7 @@ public class AuratouchedMageTest extends CardTestPlayerBase { assertPermanentCount(playerA, "Relic Ward", 1); } - */ + @Test public void testGainsLegalAura() { // Expected result: Brainwash gets placed on Auratouched Mage @@ -89,8 +90,7 @@ public class AuratouchedMageTest extends CardTestPlayerBase { } - /* - @Ignore //If someone knows the way to elegantly handle the test mechanism in regards to no valid targets, please modify. The test works fine in practice. + //If someone knows the way to elegantly handle the test mechanism in regards to no valid targets, please modify. The test works fine in practice. @Test public void testAuratouchedMageNotOnBattlefield() { // Expected result: Auratouched Mage is exiled immediately after entering the battlefield, the legal aura (Brainwash) gets put into controller's hand @@ -113,5 +113,4 @@ public class AuratouchedMageTest extends CardTestPlayerBase { assertLibraryCount(playerA, "Animate Wall", 1); } - */ } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/other/EndTurnEffectTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/other/EndTurnEffectTest.java index 1a0462e4ac..af4fad430f 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/other/EndTurnEffectTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/other/EndTurnEffectTest.java @@ -108,4 +108,37 @@ public class EndTurnEffectTest extends CardTestPlayerBase { assertHandCount(playerB, 0); } + + /** + * Test to remove a Aftermath card from spell + */ + @Test + public void testSpellAftermath() { + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + // Insult Sorcery {2}{R} + // Damage can't be prevented this turn. If a source you control would deal damage this turn, it deals double that damage instead. + // Injury Sorcery {2}{R} + // Aftermath (Cast this spell only from your graveyard. Then exile it.) + // Injury deals 2 damage to target creature and 2 damage to target player. + addCard(Zone.GRAVEYARD, playerA, "Insult // Injury"); + + addCard(Zone.BATTLEFIELD, playerB, "Mountain", 3); + // End the turn. + // At the beginning of your next end step, you lose the game. + addCard(Zone.HAND, playerB, "Glorious End"); //Instant {2}{R} + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Injury", "Silvercoat Lion"); + addTarget(playerA, playerB); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Glorious End", NO_TARGET, "Injury"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertExileCount(playerA, "Insult // Injury", 1); + assertGraveyardCount(playerB, "Glorious End", 0); + assertHandCount(playerA, 0); + assertHandCount(playerB, 0); + + } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/continuous/LayerTests.java b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/LayerTests.java index 43accbb348..8de5dce172 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/continuous/LayerTests.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/LayerTests.java @@ -60,7 +60,7 @@ public class LayerTests extends CardTestPlayerBase { } - @Ignore + @Ignore //Works fine in the game. Test fails, though. public void complexExampleFromLayersArticle() { /*In play there is a Grizzly Bears which has already been Giant Growthed, a Bog Wraith enchanted by a Lignify, and Figure of Destiny with its 3rd ability activated. @@ -79,6 +79,7 @@ public class LayerTests extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Giant Growth", "Grizzly Bears"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lignify", "Bog Wrath"); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{R/W}:"); activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{R/W}{R/W}{R/W}:"); activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{R/W}{R/W}{R/W}{R/W}{R/W}{R/W}:"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Mirrorweave", "Figure of Destiny"); @@ -92,4 +93,56 @@ public class LayerTests extends CardTestPlayerBase { assertPowerToughness(playerA, "Figure of Destiny", 0, 4, Filter.ComparisonScope.All); } + + @Test + public void testUrborgWithAnimateLandAndOvinize() { + // Animate Land: target land is a 3/3 until end of turn and is still a land. + // Ovinize: target creature becomes 0/1 and loses all abilities until end of turn. + // Urborg, Tomb of Yawgmoth : Each land is a Swamp in addition to its other types. + // Expected behavior: Urborg loses all abilities and becomes a 0/1 creature. + addCard(Zone.HAND, playerA, "Animate Land", 1); + addCard(Zone.HAND, playerA, "Ovinize", 1); + addCard(Zone.BATTLEFIELD, playerA, "Urborg, Tomb of Yawgmoth", 1); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 1); + addCard(Zone.BATTLEFIELD, playerA, "Island", 2); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Animate Land", "Urborg, Tomb of Yawgmoth"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Ovinize", "Urborg, Tomb of Yawgmoth"); + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertType("Urborg, Tomb of Yawgmoth", CardType.CREATURE, "Swamp"); // Urborg is a creature + assertPowerToughness(playerA, "Urborg, Tomb of Yawgmoth", 0, 1); // Urborg is a 0/1 creature + + } + + @Ignore //This works fine in the game. Test fails. + public void testFromAnArticle() { + /* + Aiden has a Battlegate Mimic on the battlefield. Nick controls two Wilderness Hypnotists. + Aiden casts a Scourge of the Nobilis, targeting the Mimic; after that resolves Nick activates + one of his Hypnotist's abilities, targeting the Mimic. Aiden attacks with the Mimic, and + casts Inside Out before the damage step. Once Inside Out resolves, Nick activates the ability + of his other Hypnotist. How much damage will the Mimic deal? + */ + addCard(Zone.HAND, playerA, "Scourge of the Nobilis", 1); + addCard(Zone.HAND, playerA, "Inside Out", 1); + addCard(Zone.BATTLEFIELD, playerA, "Battlegate Mimic", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 8); + addCard(Zone.BATTLEFIELD, playerA, "Island", 8); + + addCard(Zone.BATTLEFIELD, playerB, "Wilderness Hypnotist", 2); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Scourge of the Nobilis", "Battlegate Mimic"); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerB, "{T}:", "Battlegate Mimic"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Inside Out", "Battlegate Mimic"); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerB, "{T}:", "Battlegate Mimic"); + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertPowerToughness(playerA, "Battlegate Mimic", 4, 2); + + } + } diff --git a/Mage/src/main/java/mage/abilities/common/LicidAbility.java b/Mage/src/main/java/mage/abilities/common/LicidAbility.java index f9db3f2439..545eb9313a 100644 --- a/Mage/src/main/java/mage/abilities/common/LicidAbility.java +++ b/Mage/src/main/java/mage/abilities/common/LicidAbility.java @@ -27,6 +27,7 @@ */ package mage.abilities.common; +import java.util.ArrayList; import java.util.UUID; import mage.abilities.Ability; import mage.abilities.ActivatedAbilityImpl; @@ -144,14 +145,16 @@ class LicidContinuousEffect extends ContinuousEffectImpl { licid.getSubtype(game).add("Aura"); break; case AbilityAddingRemovingEffects_6: + ArrayList toRemove = new ArrayList<>(); for (Ability ability : licid.getAbilities(game)) { for (Effect effect : ability.getEffects()) { if (effect instanceof LicidEffect) { - licid.getAbilities(game).remove(ability); + toRemove.add(ability); break; } } } + licid.getAbilities(game).removeAll(toRemove); Ability ability = new EnchantAbility("creature"); ability.setRuleAtTheTop(true); licid.addAbility(ability, source.getSourceId(), game); diff --git a/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java b/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java index ea7067e0d2..5d5454c5e1 100644 --- a/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java +++ b/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java @@ -1218,7 +1218,10 @@ public class ContinuousEffects implements Serializable { } } } else { - logger.error("Replacement effect without ability: " + entry.getKey().toString()); + if (!(entry.getKey() instanceof AuraReplacementEffect) + && !(entry.getKey() instanceof PlaneswalkerRedirectionEffect)) { + logger.error("Replacement effect without ability: " + entry.getKey().toString()); + } } } return texts; diff --git a/Mage/src/main/java/mage/abilities/effects/common/combat/CantBeBlockedByAllTargetEffect.java b/Mage/src/main/java/mage/abilities/effects/common/combat/CantBeBlockedByAllTargetEffect.java index 4d1136e4b4..fdd2aefac5 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/combat/CantBeBlockedByAllTargetEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/combat/CantBeBlockedByAllTargetEffect.java @@ -1,7 +1,29 @@ /* - * 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. + * Copyright 2010 BetaSteward_at_googlemail.com. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY BetaSteward_at_googlemail.com ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BetaSteward_at_googlemail.com OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of BetaSteward_at_googlemail.com. */ package mage.abilities.effects.common.combat; diff --git a/Mage/src/main/java/mage/abilities/effects/common/counter/RemoveCounterTargetEffect.java b/Mage/src/main/java/mage/abilities/effects/common/counter/RemoveCounterTargetEffect.java index 5820f6166d..e99b328e2f 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/counter/RemoveCounterTargetEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/counter/RemoveCounterTargetEffect.java @@ -25,7 +25,6 @@ * authors and should not be interpreted as representing official policies, either expressed * or implied, of BetaSteward_at_googlemail.com. */ - package mage.abilities.effects.common.counter; import java.util.HashSet; @@ -47,8 +46,8 @@ import mage.util.CardUtil; * * @author LevelX2 */ - public class RemoveCounterTargetEffect extends OneShotEffect { + private final Counter counter; public RemoveCounterTargetEffect() { @@ -69,23 +68,25 @@ public class RemoveCounterTargetEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { Permanent p = game.getPermanent(targetPointer.getFirst(game, source)); - if(p != null) { + if (p != null) { Counter toRemove = (counter == null ? selectCounterType(game, source, p) : counter); - if(toRemove != null && p.getCounters(game).getCount(toRemove.getName()) >= toRemove.getCount()) { + if (toRemove != null && p.getCounters(game).getCount(toRemove.getName()) >= toRemove.getCount()) { p.removeCounters(toRemove.getName(), toRemove.getCount(), game); - if(!game.isSimulation()) + if (!game.isSimulation()) { game.informPlayers("Removed " + toRemove.getCount() + ' ' + toRemove.getName() - + " counter from " + p.getName()); + + " counter from " + p.getName()); + } return true; } } Card c = game.getCard(targetPointer.getFirst(game, source)); if (c != null && counter != null && c.getCounters(game).getCount(counter.getName()) >= counter.getCount()) { c.removeCounters(counter.getName(), counter.getCount(), game); - if (!game.isSimulation()) + if (!game.isSimulation()) { game.informPlayers(new StringBuilder("Removed ").append(counter.getCount()).append(' ').append(counter.getName()) - .append(" counter from ").append(c.getName()) - .append(" (").append(c.getCounters(game).getCount(counter.getName())).append(" left)").toString()); + .append(" counter from ").append(c.getName()) + .append(" (").append(c.getCounters(game).getCount(counter.getName())).append(" left)").toString()); + } return true; } return false; @@ -93,12 +94,12 @@ public class RemoveCounterTargetEffect extends OneShotEffect { private Counter selectCounterType(Game game, Ability source, Permanent permanent) { Player controller = game.getPlayer(source.getControllerId()); - if(controller != null && !permanent.getCounters(game).isEmpty()) { + if (controller != null && !permanent.getCounters(game).isEmpty()) { String counterName = null; - if(permanent.getCounters(game).size() > 1) { + if (permanent.getCounters(game).size() > 1) { Choice choice = new ChoiceImpl(true); Set choices = new HashSet<>(); - for(Counter counter : permanent.getCounters(game).values()) { + for (Counter counter : permanent.getCounters(game).values()) { if (permanent.getCounters(game).getCount(counter.getName()) > 0) { choices.add(counter.getName()); } @@ -108,8 +109,8 @@ public class RemoveCounterTargetEffect extends OneShotEffect { controller.choose(Outcome.Detriment, choice, game); counterName = choice.getChoice(); } else { - for(Counter counter : permanent.getCounters(game).values()) { - if(counter.getCount() > 0) { + for (Counter counter : permanent.getCounters(game).values()) { + if (counter.getCount() > 0) { counterName = counter.getName(); } } @@ -131,14 +132,13 @@ public class RemoveCounterTargetEffect extends OneShotEffect { } String text = "remove "; - if(counter == null) { + if (counter == null) { text += "a counter"; + } else { + text += CardUtil.numberToText(counter.getCount(), "a") + ' ' + counter.getName(); + text += counter.getCount() > 1 ? " counters" : " counter"; } - else { - text += CardUtil.numberToText(counter.getCount(), "a") + ' ' + counter.getName(); - text += counter.getCount() > 1 ? " counters" : " counter"; - } - text += " from target " + mode.getTargets().get(0).getTargetName(); + text += " from target " + (mode.getTargets().isEmpty() ? " object" : mode.getTargets().get(0).getTargetName()); return text; } } diff --git a/Mage/src/main/java/mage/filter/Filter.java b/Mage/src/main/java/mage/filter/Filter.java index 3f701fa69b..ecd2d46dcb 100644 --- a/Mage/src/main/java/mage/filter/Filter.java +++ b/Mage/src/main/java/mage/filter/Filter.java @@ -27,11 +27,10 @@ */ package mage.filter; +import java.io.Serializable; import mage.filter.predicate.Predicate; import mage.game.Game; -import java.io.Serializable; - /** * @param * @author BetaSteward_at_googlemail.com @@ -45,7 +44,7 @@ public interface Filter extends Serializable { boolean match(E o, Game game); - void add(Predicate predicate); + Filter add(Predicate predicate); boolean checkObjectClass(Object object); diff --git a/Mage/src/main/java/mage/filter/FilterImpl.java b/Mage/src/main/java/mage/filter/FilterImpl.java index b48a94901e..5092338392 100644 --- a/Mage/src/main/java/mage/filter/FilterImpl.java +++ b/Mage/src/main/java/mage/filter/FilterImpl.java @@ -65,8 +65,9 @@ public abstract class FilterImpl implements Filter { } @Override - public final void add(Predicate predicate) { + public final Filter add(Predicate predicate) { predicates.add(predicate); + return this; } @Override diff --git a/Mage/src/main/java/mage/game/permanent/PermanentImpl.java b/Mage/src/main/java/mage/game/permanent/PermanentImpl.java index 998360ec0f..e296f0648f 100644 --- a/Mage/src/main/java/mage/game/permanent/PermanentImpl.java +++ b/Mage/src/main/java/mage/game/permanent/PermanentImpl.java @@ -971,6 +971,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { } game.fireEvent(GameEvent.getEvent(EventType.SACRIFICED_PERMANENT, objectId, sourceId, controllerId)); game.checkStateAndTriggered(); + game.applyEffects(); return true; } return false; diff --git a/Mage/src/main/java/mage/game/turn/Turn.java b/Mage/src/main/java/mage/game/turn/Turn.java index 1de664ff84..dcf5dd8bfa 100644 --- a/Mage/src/main/java/mage/game/turn/Turn.java +++ b/Mage/src/main/java/mage/game/turn/Turn.java @@ -276,7 +276,7 @@ public class Turn implements Serializable { setEndTurnRequested(true); - // 1) All spells and abilities on the stack are exiled. This includes Time Stop, though it will continue to resolve. + // 1) All spells and abilities on the stack are exiled. This includes (e.g.) Time Stop, though it will continue to resolve. // It also includes spells and abilities that can't be countered. while (!game.getStack().isEmpty()) { StackObject stackObject = game.getStack().peekFirst(); diff --git a/Mage/src/main/java/mage/target/targetpointer/ThirdTargetPointer.java b/Mage/src/main/java/mage/target/targetpointer/ThirdTargetPointer.java new file mode 100644 index 0000000000..2fdbf05037 --- /dev/null +++ b/Mage/src/main/java/mage/target/targetpointer/ThirdTargetPointer.java @@ -0,0 +1,87 @@ +/* + * 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.target.targetpointer; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import mage.abilities.Ability; +import mage.cards.Card; +import mage.game.Game; + +/** + * + * @author Ludwig.Hirth + */ +public class ThirdTargetPointer implements TargetPointer { + + private Map zoneChangeCounter = new HashMap<>(); + + public static ThirdTargetPointer getInstance() { + return new ThirdTargetPointer(); + } + + public ThirdTargetPointer() { + } + + public ThirdTargetPointer(ThirdTargetPointer targetPointer) { + this.zoneChangeCounter = new HashMap<>(); + for (Map.Entry entry : targetPointer.zoneChangeCounter.entrySet()) { + this.zoneChangeCounter.put(entry.getKey(), entry.getValue()); + } + } + + @Override + public void init(Game game, Ability source) { + if (source.getTargets().size() > 2) { + for (UUID target : source.getTargets().get(2).getTargets()) { + Card card = game.getCard(target); + if (card != null) { + this.zoneChangeCounter.put(target, card.getZoneChangeCounter(game)); + } + } + } + } + + @Override + public List getTargets(Game game, Ability source) { + ArrayList target = new ArrayList<>(); + if (source.getTargets().size() > 2) { + for (UUID targetId : source.getTargets().get(2).getTargets()) { + Card card = game.getCard(targetId); + if (card != null && zoneChangeCounter.containsKey(targetId) + && card.getZoneChangeCounter(game) != zoneChangeCounter.get(targetId)) { + continue; + } + target.add(targetId); + } + } + return target; + } + + @Override + public UUID getFirst(Game game, Ability source) { + if (source.getTargets().size() > 2) { + UUID targetId = source.getTargets().get(2).getFirstTarget(); + if (zoneChangeCounter.containsKey(targetId)) { + Card card = game.getCard(targetId); + if (card != null && zoneChangeCounter.containsKey(targetId) + && card.getZoneChangeCounter(game) != zoneChangeCounter.get(targetId)) { + return null; + } + } + return targetId; + } + return null; + } + + @Override + public TargetPointer copy() { + return new ThirdTargetPointer(this); + } +}