diff --git a/Mage.Sets/src/mage/sets/dragonsoftarkir/SwiftWarkite.java b/Mage.Sets/src/mage/sets/dragonsoftarkir/SwiftWarkite.java index d3a8b1757eb..7cf4b87280b 100644 --- a/Mage.Sets/src/mage/sets/dragonsoftarkir/SwiftWarkite.java +++ b/Mage.Sets/src/mage/sets/dragonsoftarkir/SwiftWarkite.java @@ -126,10 +126,10 @@ class SwiftWarkiteEffect extends OneShotEffect { Permanent creature = game.getPermanent(card.getId()); if (creature != null) { ContinuousEffect effect = new GainAbilityTargetEffect(HasteAbility.getInstance(), Duration.Custom); - effect.setTargetPointer(new FixedTarget(creature.getId())); + effect.setTargetPointer(new FixedTarget(creature.getId(), creature.getZoneChangeCounter(game))); game.addEffect(effect, source); Effect effect2 = new ReturnToHandTargetEffect(); - effect2.setTargetPointer(new FixedTarget(creature.getId())); + effect2.setTargetPointer(new FixedTarget(creature.getId(), creature.getZoneChangeCounter(game))); DelayedTriggeredAbility delayedAbility = new AtTheBeginOfNextEndStepDelayedTriggeredAbility(effect2); delayedAbility.setControllerId(source.getControllerId()); delayedAbility.setSourceId(source.getSourceId()); @@ -147,10 +147,10 @@ class SwiftWarkiteEffect extends OneShotEffect { Permanent creature = game.getPermanent(card.getId()); if (creature != null) { ContinuousEffect effect = new GainAbilityTargetEffect(HasteAbility.getInstance(), Duration.Custom); - effect.setTargetPointer(new FixedTarget(creature.getId())); + effect.setTargetPointer(new FixedTarget(creature.getId(), creature.getZoneChangeCounter(game))); game.addEffect(effect, source); Effect effect2 = new ReturnToHandTargetEffect(); - effect2.setTargetPointer(new FixedTarget(creature.getId())); + effect2.setTargetPointer(new FixedTarget(creature.getId(), creature.getZoneChangeCounter(game))); DelayedTriggeredAbility delayedAbility = new AtTheBeginOfNextEndStepDelayedTriggeredAbility(effect2); delayedAbility.setControllerId(source.getControllerId()); delayedAbility.setSourceId(source.getSourceId()); diff --git a/Mage.Sets/src/mage/sets/magicorigins/MageRingNetwork.java b/Mage.Sets/src/mage/sets/magicorigins/MageRingNetwork.java index 6ee273ea26e..fe953e08e6d 100644 --- a/Mage.Sets/src/mage/sets/magicorigins/MageRingNetwork.java +++ b/Mage.Sets/src/mage/sets/magicorigins/MageRingNetwork.java @@ -34,6 +34,7 @@ import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.costs.common.RemoveVariableCountersSourceCost; import mage.abilities.costs.common.TapSourceCost; import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.dynamicvalue.common.CountersCount; import mage.abilities.dynamicvalue.common.RemovedCountersForCostValue; import mage.abilities.effects.common.counter.AddCountersSourceEffect; import mage.abilities.mana.ColorlessManaAbility; @@ -47,7 +48,7 @@ import mage.counters.CounterType; /** * * @author LoneFox - + * */ public class MageRingNetwork extends CardImpl { @@ -55,17 +56,22 @@ public class MageRingNetwork extends CardImpl { super(ownerId, 249, "Mage-Ring Network", Rarity.UNCOMMON, new CardType[]{CardType.LAND}, ""); this.expansionSetCode = "ORI"; - // {t}: Add {1} to your mana pool. + // {T}: Add {1} to your mana pool. this.addAbility(new ColorlessManaAbility()); - // {1}, {t}: Put a storage counter on Mage-Ring Network. + // {1}, {T}: Put a storage counter on Mage-Ring Network. Ability ability = new SimpleActivatedAbility(Zone.BATTLEFIELD, new AddCountersSourceEffect(CounterType.STORAGE.createInstance()), - new ManaCostsImpl("{1}")); + new ManaCostsImpl("{1}")); ability.addCost(new TapSourceCost()); this.addAbility(ability); - // {t}, Remove X storage counters from Mage-Ring Network: Add {x} to your mana pool. - ability = new DynamicManaAbility(Mana.ColorlessMana, new RemovedCountersForCostValue(), "Add {X} to your mana pool"); + // {T}, Remove X storage counters from Mage-Ring Network: Add {x} to your mana pool. + ability = new DynamicManaAbility( + Mana.ColorlessMana, + new RemovedCountersForCostValue(), + new TapSourceCost(), + "Add {X} to your mana pool", + true, new CountersCount(CounterType.STORAGE)); ability.addCost(new RemoveVariableCountersSourceCost(CounterType.STORAGE.createInstance(), - "Remove X storage counters from {this}")); + "Remove X storage counters from {this}")); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/sets/magicorigins/NissaSageAnimist.java b/Mage.Sets/src/mage/sets/magicorigins/NissaSageAnimist.java index 6f256623cda..7959cf7f6b3 100644 --- a/Mage.Sets/src/mage/sets/magicorigins/NissaSageAnimist.java +++ b/Mage.Sets/src/mage/sets/magicorigins/NissaSageAnimist.java @@ -67,7 +67,6 @@ public class NissaSageAnimist extends CardImpl { this.color.setGreen(true); this.nightCard = true; - this.canTransform = true; this.addAbility(new EntersBattlefieldAbility(new AddCountersSourceEffect(CounterType.LOYALTY.createInstance(3)), false)); diff --git a/Mage.Sets/src/mage/sets/returntoravnica/GraveBetrayal.java b/Mage.Sets/src/mage/sets/returntoravnica/GraveBetrayal.java index d28418b9095..dab0ee466f1 100644 --- a/Mage.Sets/src/mage/sets/returntoravnica/GraveBetrayal.java +++ b/Mage.Sets/src/mage/sets/returntoravnica/GraveBetrayal.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.sets.returntoravnica; import java.util.UUID; @@ -58,21 +57,19 @@ import mage.target.targetpointer.FixedTarget; * * @author LevelX2 */ - public class GraveBetrayal extends CardImpl { - public GraveBetrayal (UUID ownerId) { + public GraveBetrayal(UUID ownerId) { super(ownerId, 67, "Grave Betrayal", Rarity.RARE, new CardType[]{CardType.ENCHANTMENT}, "{5}{B}{B}"); this.expansionSetCode = "RTR"; - // Whenever a creature you don't control dies, return it to the battlefield under // your control with an additional +1/+1 counter on it at the beginning of the // next end step. That creature is a black Zombie in addition to its other colors and types. this.addAbility(new GraveBetrayalTriggeredAbility()); } - public GraveBetrayal (final GraveBetrayal card) { + public GraveBetrayal(final GraveBetrayal card) { super(card); } @@ -108,13 +105,10 @@ class GraveBetrayalTriggeredAbility extends TriggeredAbilityImpl { && ((ZoneChangeEvent) event).getFromZone() == Zone.BATTLEFIELD) { Permanent permanent = (Permanent) game.getLastKnownInformation(event.getTargetId(), Zone.BATTLEFIELD); if (permanent != null && !permanent.getControllerId().equals(this.getControllerId()) && permanent.getCardType().contains(CardType.CREATURE)) { - Card card = (Card)game.getObject(permanent.getId()); + Card card = (Card) game.getObject(permanent.getId()); if (card != null) { Effect effect = new GraveBetrayalEffect(); - effect.setTargetPointer(new FixedTarget(card.getId())); - Integer zoneChanges = card.getZoneChangeCounter(game); - effect.setValue("zoneChanges", zoneChanges); - + effect.setTargetPointer(new FixedTarget(card.getId(), card.getZoneChangeCounter(game))); DelayedTriggeredAbility delayedAbility = new AtTheBeginOfNextEndStepDelayedTriggeredAbility(effect); delayedAbility.setSourceId(this.getSourceId()); delayedAbility.setControllerId(this.getControllerId()); @@ -153,17 +147,14 @@ class GraveBetrayalEffect extends OneShotEffect { public boolean apply(Game game, Ability source) { Card card = game.getCard(targetPointer.getFirst(game, source)); if (card != null) { - Integer zoneChanges = (Integer) getValue("zoneChanges"); - if (card.getZoneChangeCounter(game) == zoneChanges) { - Zone currentZone = game.getState().getZone(card.getId()); - if (card.putOntoBattlefield(game, currentZone, source.getSourceId(), source.getControllerId())) { - Permanent creature = game.getPermanent(card.getId()); - creature.addCounters(CounterType.P1P1.createInstance(), game); - ContinuousEffect effect = new GraveBetrayalContiniousEffect(); - effect.setTargetPointer(new FixedTarget(creature.getId())); - game.addEffect(effect, source); - return true; - } + Zone currentZone = game.getState().getZone(card.getId()); + if (card.putOntoBattlefield(game, currentZone, source.getSourceId(), source.getControllerId())) { + Permanent creature = game.getPermanent(card.getId()); + creature.addCounters(CounterType.P1P1.createInstance(), game); + ContinuousEffect effect = new GraveBetrayalContiniousEffect(); + effect.setTargetPointer(new FixedTarget(creature.getId())); + game.addEffect(effect, source); + return true; } } return false; @@ -220,4 +211,4 @@ class GraveBetrayalContiniousEffect extends ContinuousEffectImpl { return layer == Layer.ColorChangingEffects_5 || layer == Layer.TypeChangingEffects_4; } -} \ No newline at end of file +} diff --git a/Mage.Sets/src/mage/sets/scarsofmirrodin/ArgentSphinx.java b/Mage.Sets/src/mage/sets/scarsofmirrodin/ArgentSphinx.java index 3613ce6d0d4..3863832e516 100644 --- a/Mage.Sets/src/mage/sets/scarsofmirrodin/ArgentSphinx.java +++ b/Mage.Sets/src/mage/sets/scarsofmirrodin/ArgentSphinx.java @@ -25,15 +25,11 @@ * authors and should not be interpreted as representing official policies, either expressed * or implied, of BetaSteward_at_googlemail.com. */ - package mage.sets.scarsofmirrodin; import java.util.UUID; - -import mage.constants.CardType; -import mage.constants.Rarity; -import mage.constants.Zone; import mage.MageInt; +import mage.MageObject; import mage.abilities.Ability; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.common.delayed.AtTheBeginOfNextEndStepDelayedTriggeredAbility; @@ -43,7 +39,10 @@ import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.ReturnFromExileEffect; import mage.abilities.keyword.FlyingAbility; import mage.cards.CardImpl; +import mage.constants.CardType; import mage.constants.Outcome; +import mage.constants.Rarity; +import mage.constants.Zone; import mage.game.Game; import mage.game.permanent.Permanent; @@ -53,7 +52,7 @@ import mage.game.permanent.Permanent; */ public class ArgentSphinx extends CardImpl { - public ArgentSphinx (UUID ownerId) { + public ArgentSphinx(UUID ownerId) { super(ownerId, 28, "Argent Sphinx", Rarity.RARE, new CardType[]{CardType.CREATURE}, "{2}{U}{U}"); this.expansionSetCode = "SOM"; this.subtype.add("Sphinx"); @@ -66,7 +65,7 @@ public class ArgentSphinx extends CardImpl { this.addAbility(ability); } - public ArgentSphinx (final ArgentSphinx card) { + public ArgentSphinx(final ArgentSphinx card) { super(card); } @@ -81,7 +80,7 @@ class ArgentSphinxEffect extends OneShotEffect { private static final String effectText = "Exile {this}. Return it to the battlefield under your control at the beginning of the next end step"; - ArgentSphinxEffect ( ) { + ArgentSphinxEffect() { super(Outcome.Benefit); staticText = effectText; } @@ -93,10 +92,13 @@ class ArgentSphinxEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { Permanent permanent = game.getPermanent(source.getSourceId()); - if (permanent != null) { - if (permanent.moveToExile(source.getSourceId(), "Argent Sphinx Exile", source.getSourceId(), game)) { + MageObject sourceObject = game.getObject(source.getSourceId()); + if (permanent != null && sourceObject != null) { + if (permanent.moveToExile(source.getSourceId(), sourceObject.getIdName(), source.getSourceId(), game)) { //create delayed triggered ability - AtTheBeginOfNextEndStepDelayedTriggeredAbility delayedAbility = new AtTheBeginOfNextEndStepDelayedTriggeredAbility(new ReturnFromExileEffect(source.getSourceId(), Zone.BATTLEFIELD)); + AtTheBeginOfNextEndStepDelayedTriggeredAbility delayedAbility + = new AtTheBeginOfNextEndStepDelayedTriggeredAbility( + new ReturnFromExileEffect(source.getSourceId(), Zone.BATTLEFIELD)); delayedAbility.setSourceId(source.getSourceId()); delayedAbility.setControllerId(source.getControllerId()); delayedAbility.setSourceObject(source.getSourceObject(game), game); diff --git a/Mage.Sets/src/mage/sets/scarsofmirrodin/VenserTheSojourner.java b/Mage.Sets/src/mage/sets/scarsofmirrodin/VenserTheSojourner.java index 2182a3dd8a6..2f4def3ecfd 100644 --- a/Mage.Sets/src/mage/sets/scarsofmirrodin/VenserTheSojourner.java +++ b/Mage.Sets/src/mage/sets/scarsofmirrodin/VenserTheSojourner.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.sets.scarsofmirrodin; import java.util.UUID; @@ -81,8 +80,6 @@ public class VenserTheSojourner extends CardImpl { this.expansionSetCode = "SOM"; this.subtype.add("Venser"); - - this.addAbility(new EntersBattlefieldAbility(new AddCountersSourceEffect(CounterType.LOYALTY.createInstance(3)), false)); // +2: Exile target permanent you own. Return it to the battlefield under your control at the beginning of the next end step. @@ -131,7 +128,7 @@ class VenserTheSojournerEffect extends OneShotEffect { if (getTargetPointer().getFirst(game, source) != null) { Permanent permanent = game.getPermanent(getTargetPointer().getFirst(game, source)); if (permanent != null) { - if (controller.moveCardToExileWithInfo(permanent, source.getSourceId(), sourceObject.getName(), source.getSourceId(), game, Zone.BATTLEFIELD, true)) { + if (controller.moveCardToExileWithInfo(permanent, source.getSourceId(), sourceObject.getIdName(), source.getSourceId(), game, Zone.BATTLEFIELD, true)) { //create delayed triggered ability AtTheBeginOfNextEndStepDelayedTriggeredAbility delayedAbility = new AtTheBeginOfNextEndStepDelayedTriggeredAbility(new ReturnFromExileEffect(source.getSourceId(), Zone.BATTLEFIELD)); delayedAbility.setSourceId(source.getSourceId()); @@ -174,7 +171,8 @@ class VenserTheSojournerSpellCastTriggeredAbility extends TriggeredAbilityImpl { protected FilterSpell filter; /** - * If true, the source that triggered the ability will be set as target to effect. + * If true, the source that triggered the ability will be set as target to + * effect. */ protected boolean rememberSource = false; diff --git a/Mage.Sets/src/mage/sets/thedark/CityOfShadows.java b/Mage.Sets/src/mage/sets/thedark/CityOfShadows.java index 6e4381050e3..8651daa4ad0 100644 --- a/Mage.Sets/src/mage/sets/thedark/CityOfShadows.java +++ b/Mage.Sets/src/mage/sets/thedark/CityOfShadows.java @@ -45,7 +45,7 @@ import mage.target.common.TargetControlledCreaturePermanent; /** * - * @author anonymous + * @author Luna Skyrise */ public class CityOfShadows extends CardImpl { @@ -53,12 +53,14 @@ public class CityOfShadows extends CardImpl { super(ownerId, 113, "City of Shadows", Rarity.RARE, new CardType[]{CardType.LAND}, ""); this.expansionSetCode = "DRK"; - // {tap}, Exile a creature you control: Put a storage counter on City of Shadows. + // {T}, Exile a creature you control: Put a storage counter on City of Shadows. Ability ability = new SimpleActivatedAbility(Zone.BATTLEFIELD, new AddCountersSourceEffect(CounterType.STORAGE.createInstance()), new TapSourceCost()); ability.addCost(new ExileTargetCost(new TargetControlledCreaturePermanent())); - // {tap}: Add {X} to your mana pool, where X is the number of storage counters on City of Shadows. + this.addAbility(ability); + + // {T}: Add {X} to your mana pool, where X is the number of storage counters on City of Shadows. ability = new DynamicManaAbility(Mana.ColorlessMana, new CountersCount(CounterType.STORAGE), - "{tap}: Add {X} to your mana pool, where X is the number of storage counters on City of Shadows"); + "{tap}: Add {X} to your mana pool, where X is the number of storage counters on {this}"); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/sets/zendikar/QuestForTheGravelord.java b/Mage.Sets/src/mage/sets/zendikar/QuestForTheGravelord.java index 540570e9664..ddba7cf5ceb 100644 --- a/Mage.Sets/src/mage/sets/zendikar/QuestForTheGravelord.java +++ b/Mage.Sets/src/mage/sets/zendikar/QuestForTheGravelord.java @@ -32,7 +32,6 @@ import mage.constants.CardType; import mage.constants.Rarity; import mage.constants.Zone; import mage.MageInt; -import mage.ObjectColor; import mage.abilities.common.DiesCreatureTriggeredAbility; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.costs.common.RemoveCountersSourceCost; @@ -53,7 +52,6 @@ public class QuestForTheGravelord extends CardImpl { super(ownerId, 108, "Quest for the Gravelord", Rarity.UNCOMMON, new CardType[]{CardType.ENCHANTMENT}, "{B}"); this.expansionSetCode = "ZEN"; - // Whenever a creature dies, you may put a quest counter on Quest for the Gravelord. this.addAbility(new DiesCreatureTriggeredAbility(new AddCountersSourceEffect(CounterType.QUEST.createInstance()), true)); // Remove three quest counters from Quest for the Gravelord and sacrifice it: Put a 5/5 black Zombie Giant creature token onto the battlefield. @@ -86,4 +84,4 @@ class ZombieToken extends Token { power = new MageInt(5); toughness = new MageInt(5); } -} \ No newline at end of file +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/planeswalker/UginTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/planeswalker/UginTest.java new file mode 100644 index 00000000000..bc396fbfd04 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/planeswalker/UginTest.java @@ -0,0 +1,91 @@ +/* + * 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 org.mage.test.cards.planeswalker; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author LevelX2 + */ +public class UginTest extends CardTestPlayerBase { + + @Test + public void testCard() { + // +2: Ugin, the Spirit Dragon deals 3 damage to target creature or player. + // -X: Exile each permanent with converted mana cost X or less that's one or more colors. + // -10: You gain 7 life, draw 7 cards, then put up to seven permanent cards from your hand onto the battlefield. + addCard(Zone.BATTLEFIELD, playerA, "Ugin, the Spirit Dragon"); // starts with 7 Loyality counters + // Whenever a creature dies, you may put a quest counter on Quest for the Gravelord. + addCard(Zone.BATTLEFIELD, playerA, "Quest for the Gravelord"); + addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion"); + + addCard(Zone.LIBRARY, playerB, "Forest", 2); + addCard(Zone.BATTLEFIELD, playerB, "Forest", 6); + // When Nissa, Vastwood Seer enters the battlefield, you may search your library for a basic Forest card, reveal it, put it into your hand, then shuffle your library. + // Whenever a land enters the battlefield under your control, if you control seven or more lands, exile Nissa, then return her to the battlefield transformed under her owner's control. + // +1: Reveal the top card of your library. If it's a land card, put it onto the battlefield. Otherwise, put it into your hand. + // -2: Put a legendary 4/4 green Elemental creature token named Ashaya, the Awoken World onto the battlefield. + // -7: Untap up to six target lands. They become 6/6 Elemental creatures. They're still lands. + addCard(Zone.HAND, playerB, "Nissa, Vastwood Seer"); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "+2: {source} deals 3 damage to target creature or player.", playerB); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Nissa, Vastwood Seer"); + playLand(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Forest"); + activateAbility(2, PhaseStep.POSTCOMBAT_MAIN, playerB, "-2: Put a legendary 4/4 green Elemental creature token named Ashaya, the Awoken World onto the battlefield."); + + attack(3, playerA, "Silvercoat Lion"); + block(3, playerB, "Ashaya, the Awoken World", "Silvercoat Lion"); + + activateAbility(3, PhaseStep.POSTCOMBAT_MAIN, playerA, "-X: Exile each permanent with converted mana cost X or less that's one or more colors"); + setChoice(playerA, "X=0"); + + setStopAt(3, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, "Ugin, the Spirit Dragon", 1); + assertCounterCount("Ugin, the Spirit Dragon", CounterType.LOYALTY, 9); // 7 + 2 - 0 + + assertGraveyardCount(playerA, "Silvercoat Lion", 1); + assertPermanentCount(playerB, "Ashaya, the Awoken World", 0); + + assertExileCount("Nissa, Vastwood Seer", 1); + + assertCounterCount("Quest for the Gravelord", CounterType.QUEST, 1); + + assertLife(playerA, 20); + assertLife(playerB, 17); + + } + +} diff --git a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java index 683c11da3e7..4b8baa125c1 100644 --- a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java +++ b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java @@ -253,6 +253,9 @@ public class TestPlayer implements Player { int targetsSet = 0; for (Player player : game.getPlayers().values()) { if (player.getName().equals(target)) { + if (ability.getTargets().isEmpty()) { + throw new UnsupportedOperationException("Ability has no targets, but there is a player target set - " + ability.toString()); + } ability.getTargets().get(0).addTarget(player.getId(), ability, game); targetsSet++; break; 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 25b40da1869..d40b5113bff 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 @@ -219,6 +219,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * * @param player {@link Player} to remove all library cards from. */ + @Override public void removeAllCardsFromLibrary(TestPlayer player) { getCommands(player).put(Zone.LIBRARY, "clear"); } @@ -241,6 +242,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * playerB. * @param cardName Card name in string format. */ + @Override public void addCard(Zone gameZone, TestPlayer player, String cardName) { addCard(gameZone, player, cardName, 1, false); } @@ -254,6 +256,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * @param cardName Card name in string format. * @param count Amount of cards to be added. */ + @Override public void addCard(Zone gameZone, TestPlayer player, String cardName, int count) { addCard(gameZone, player, cardName, count, false); } @@ -270,6 +273,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * 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) { if (gameZone.equals(Zone.BATTLEFIELD)) { @@ -324,6 +328,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * @param player {@link Player} to set life count for. * @param life Life count to set. */ + @Override public void setLife(TestPlayer player, int life) { getCommands(player).put(Zone.OUTSIDE, "life:" + String.valueOf(life)); } @@ -714,16 +719,16 @@ 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 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 */ public void assertTappedCount(String cardName, boolean tapped, int count) throws AssertionError { int tappedAmount = 0; Permanent found = null; for (Permanent permanent : currentGame.getBattlefield().getAllActivePermanents()) { if (permanent.getName().equals(cardName)) { - if(permanent.isTapped() == tapped) { + if (permanent.isTapped() == tapped) { tappedAmount++; } found = permanent; diff --git a/Mage.Tests/src/test/java/org/mage/test/utils/ManaOptionsTest.java b/Mage.Tests/src/test/java/org/mage/test/utils/ManaOptionsTest.java index b4b40dd99e2..2f0af102cbf 100644 --- a/Mage.Tests/src/test/java/org/mage/test/utils/ManaOptionsTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/utils/ManaOptionsTest.java @@ -30,6 +30,7 @@ package org.mage.test.utils; import mage.abilities.mana.ManaOptions; import mage.constants.PhaseStep; import mage.constants.Zone; +import mage.counters.CounterType; import org.junit.Assert; import org.junit.Ignore; import org.junit.Test; @@ -290,6 +291,43 @@ public class ManaOptionsTest extends CardTestPlayerBase { Assert.assertEquals("{B}{B}{B}", getManaOption(1, manaOptions)); } + @Test + public void testMageRingNetwork() { + // {T}: Add {1} to your mana pool. + // {T}, {1} : Put a storage counter on Mage-Ring Network. + // {T}, Remove X storage counters from Mage-Ring Network: Add {X} to your mana pool. + addCard(Zone.BATTLEFIELD, playerA, "Mage-Ring Network", 1); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 1); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1); + + setStopAt(1, PhaseStep.UPKEEP); + execute(); + + ManaOptions manaOptions = playerA.getAvailableManaTest(currentGame); + + Assert.assertEquals("mana variations don't fit", 1, manaOptions.size()); + Assert.assertEquals("{1}{W}{B}", getManaOption(0, manaOptions)); + } + + @Test + public void testMageRingNetwork2() { + // {T}: Add {1} to your mana pool. + // {T}, {1} : Put a storage counter on Mage-Ring Network. + // {T}, Remove X storage counters from Mage-Ring Network: Add {X} to your mana pool. + addCard(Zone.BATTLEFIELD, playerA, "Mage-Ring Network", 1); + addCounters(1, PhaseStep.UPKEEP, playerA, "Mage-Ring Network", CounterType.STORAGE, 4); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 1); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1); + + setStopAt(1, PhaseStep.DRAW); + execute(); + + ManaOptions manaOptions = playerA.getAvailableManaTest(currentGame); + + Assert.assertEquals("mana variations don't fit", 1, manaOptions.size()); + Assert.assertEquals("{4}{W}{B}", getManaOption(0, manaOptions)); + } + @Test @Ignore // TriggeredManaAbilities not supported yet for getAvailableMana public void testCryptGhast() { diff --git a/Mage/src/mage/Mana.java b/Mage/src/mage/Mana.java index e24700d45d0..b9b3e04c8fa 100644 --- a/Mage/src/mage/Mana.java +++ b/Mage/src/mage/Mana.java @@ -597,10 +597,11 @@ public class Mana implements Comparable, Serializable, Copyable { } /** - * Returns the mana that is more colored but does not contain one less mana - * in any color but colorless if you call with {1}{W}{R} and {G}{W}{R} you - * get back {G}{W}{R} if you call with {G}{W}{R} and {G}{W}{R} you get back - * {G}{W}{R} if you call with {G}{W}{B} and {G}{W}{R} you get back null + * Returns the mana that is more colored or has a greater amount but does + * not contain one less mana in any color but colorless if you call with + * {1}{W}{R} and {G}{W}{R} you get back {G}{W}{R} if you call with {G}{W}{R} + * and {G}{W}{R} you get back {G}{W}{R} if you call with {G}{W}{B} and + * {G}{W}{R} you get back null * * @param mana1 * @param mana2 @@ -609,7 +610,7 @@ public class Mana implements Comparable, Serializable, Copyable { public static Mana getMoreValuableMana(Mana mana1, Mana mana2) { Mana moreMana; Mana lessMana; - if (mana2.count() > mana1.count() || mana2.getAny() > mana1.getAny() || mana2.getColorless() < mana1.getColorless()) { + if (mana2.countColored() > mana1.countColored() || mana2.getAny() > mana1.getAny() || mana2.count() > mana1.count()) { moreMana = mana2; lessMana = mana1; } else { diff --git a/Mage/src/mage/abilities/dynamicvalue/common/CountersCount.java b/Mage/src/mage/abilities/dynamicvalue/common/CountersCount.java index fd0e3926190..9f6a5312a62 100644 --- a/Mage/src/mage/abilities/dynamicvalue/common/CountersCount.java +++ b/Mage/src/mage/abilities/dynamicvalue/common/CountersCount.java @@ -1,15 +1,14 @@ package mage.abilities.dynamicvalue.common; -import mage.MageObject; import mage.abilities.Ability; import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.effects.Effect; -import mage.constants.Zone; import mage.counters.CounterType; import mage.game.Game; import mage.game.permanent.Permanent; public class CountersCount implements DynamicValue { + private final CounterType counter; public CountersCount(CounterType counter) { diff --git a/Mage/src/mage/abilities/effects/common/DynamicManaEffect.java b/Mage/src/mage/abilities/effects/common/DynamicManaEffect.java index 7747f360756..80df2007eb2 100644 --- a/Mage/src/mage/abilities/effects/common/DynamicManaEffect.java +++ b/Mage/src/mage/abilities/effects/common/DynamicManaEffect.java @@ -44,32 +44,39 @@ public class DynamicManaEffect extends BasicManaEffect { private final Mana computedMana; private final DynamicValue amount; + private final DynamicValue netAmount; private String text = null; private boolean oneChoice; public DynamicManaEffect(Mana mana, DynamicValue amount) { - super(mana); - this.amount = amount; - computedMana = new Mana(); + this(mana, amount, null); } public DynamicManaEffect(Mana mana, DynamicValue amount, String text) { this(mana, amount, text, false); } + public DynamicManaEffect(Mana mana, DynamicValue amount, String text, boolean oneChoice) { + this(mana, amount, text, oneChoice, null); + } + /** * * @param mana * @param amount * @param text - * @param oneChoice is all mana from the same colour or if false the player can choose different colours + * @param oneChoice is all mana from the same colour or if false the player + * can choose different colours + * @param netAmount a dynamic value that calculates the possible available + * mana (e.g. if you have to pay by removing counters from source) */ - public DynamicManaEffect(Mana mana, DynamicValue amount, String text, boolean oneChoice) { + public DynamicManaEffect(Mana mana, DynamicValue amount, String text, boolean oneChoice, DynamicValue netAmount) { super(mana); this.amount = amount; computedMana = new Mana(); this.text = text; this.oneChoice = oneChoice; + this.netAmount = netAmount; } public DynamicManaEffect(final DynamicManaEffect effect) { @@ -78,6 +85,11 @@ public class DynamicManaEffect extends BasicManaEffect { this.amount = effect.amount.copy(); this.text = effect.text; this.oneChoice = effect.oneChoice; + if (effect.netAmount != null) { + this.netAmount = effect.netAmount.copy(); + } else { + this.netAmount = null; + } } @Override @@ -106,9 +118,16 @@ public class DynamicManaEffect extends BasicManaEffect { return null; } - public Mana computeMana(boolean netMana ,Game game, Ability source){ + public Mana computeMana(boolean netMana, Game game, Ability source) { this.computedMana.clear(); - int count = amount.calculate(game, source, this); + int count; + if (netMana && netAmount != null) { + // calculate the maximum available mana + count = netAmount.calculate(game, source, this); + } else { + count = amount.calculate(game, source, this); + } + if (mana.getBlack() > 0) { computedMana.setBlack(count); } else if (mana.getBlue() > 0) { @@ -126,7 +145,7 @@ public class DynamicManaEffect extends BasicManaEffect { Player controller = game.getPlayer(source.getControllerId()); if (controller != null) { ChoiceColor choiceColor = new ChoiceColor(); - for(int i = 0; i < count; i++){ + for (int i = 0; i < count; i++) { if (!choiceColor.isChosen()) { while (!controller.choose(Outcome.Benefit, choiceColor, game)) { if (!controller.isInGame()) { @@ -150,7 +169,7 @@ public class DynamicManaEffect extends BasicManaEffect { } } } - } + } } else { computedMana.setColorless(count); } diff --git a/Mage/src/mage/abilities/mana/DynamicManaAbility.java b/Mage/src/mage/abilities/mana/DynamicManaAbility.java index 72336b74e08..15a44b71c3f 100644 --- a/Mage/src/mage/abilities/mana/DynamicManaAbility.java +++ b/Mage/src/mage/abilities/mana/DynamicManaAbility.java @@ -48,6 +48,7 @@ public class DynamicManaAbility extends ManaAbility { /** * TapSourceCost added by default + * * @param mana * @param amount */ @@ -74,10 +75,24 @@ public class DynamicManaAbility extends ManaAbility { } public DynamicManaAbility(Mana mana, DynamicValue amount, Cost cost, String text, boolean oneChoice) { - super(Zone.BATTLEFIELD, new DynamicManaEffect(mana, amount, text, oneChoice), cost); - manaEffect = (DynamicManaEffect) this.getEffects().get(0); + this(mana, amount, cost, text, oneChoice, null); } + /** + * + * @param mana + * @param amount + * @param cost + * @param text + * @param oneChoice is all mana from the same colour or if false the player + * can choose different colours + * @param netAmount a dynamic value that calculates the possible available + * mana (e.g. if you have to pay by removing counters from source) + */ + public DynamicManaAbility(Mana mana, DynamicValue amount, Cost cost, String text, boolean oneChoice, DynamicValue netAmount) { + super(Zone.BATTLEFIELD, new DynamicManaEffect(mana, amount, text, oneChoice, netAmount), cost); + manaEffect = (DynamicManaEffect) this.getEffects().get(0); + } public DynamicManaAbility(final DynamicManaAbility ability) { super(ability); @@ -95,8 +110,9 @@ public class DynamicManaAbility extends ManaAbility { List newNetMana = new ArrayList<>(); if (game != null) { // TODO: effects from replacement effects like Mana Reflection are not considered yet + // TODO: effects that need a X payment (e.g. Mage-Ring Network) return always 0 newNetMana.add(manaEffect.computeMana(true, game, this)); - } - return newNetMana; + } + return newNetMana; } } diff --git a/Mage/src/mage/abilities/mana/ManaOptions.java b/Mage/src/mage/abilities/mana/ManaOptions.java index 4965a650b13..bb5977e813f 100644 --- a/Mage/src/mage/abilities/mana/ManaOptions.java +++ b/Mage/src/mage/abilities/mana/ManaOptions.java @@ -95,7 +95,7 @@ public class ManaOptions extends ArrayList { Mana moreValuable = Mana.getMoreValuableMana(newMana, existingMana); if (moreValuable != null) { // only keep the more valuable mana - existingMana.setToMana(newMana); + existingMana.setToMana(moreValuable); continue SkipAddMana; } } diff --git a/Mage/src/mage/game/GameImpl.java b/Mage/src/mage/game/GameImpl.java index 07fb43d5805..b14cd2908a0 100644 --- a/Mage/src/mage/game/GameImpl.java +++ b/Mage/src/mage/game/GameImpl.java @@ -44,6 +44,7 @@ import java.util.Random; import java.util.Set; import java.util.Stack; import java.util.UUID; +import mage.MageException; import mage.MageObject; import mage.abilities.Ability; import mage.abilities.ActivatedAbility; @@ -1173,6 +1174,7 @@ public abstract class GameImpl implements Game, Serializable { @Override public void playPriority(UUID activePlayerId, boolean resuming) { + int errorContinueCounter = 0; int bookmark = 0; clearAllBookmarks(); try { @@ -1239,11 +1241,19 @@ public abstract class GameImpl implements Game, Serializable { } } catch (Exception ex) { logger.fatal("Game exception gameId: " + getId(), ex); - ex.printStackTrace(); this.fireErrorEvent("Game exception occurred: ", ex); restoreState(bookmark, ""); bookmark = 0; - continue; + Player activePlayer = this.getPlayer(getActivePlayerId()); + if (errorContinueCounter > 15) { + throw new MageException("Iterated player priority after game exception too often, game ends!"); + } + if (activePlayer != null && !activePlayer.isTestMode()) { + errorContinueCounter++; + continue; + } else { + throw new MageException("Error in testclass"); + } } state.getPlayerList().getNext(); } @@ -1251,6 +1261,7 @@ public abstract class GameImpl implements Game, Serializable { } catch (Exception ex) { logger.fatal("Game exception ", ex); this.fireErrorEvent("Game exception occurred: ", ex); + this.end(); } finally { resetLKI(); clearAllBookmarks(); diff --git a/Mage/src/mage/players/PlayerImpl.java b/Mage/src/mage/players/PlayerImpl.java index a285cde583f..0bc26e40f1f 100644 --- a/Mage/src/mage/players/PlayerImpl.java +++ b/Mage/src/mage/players/PlayerImpl.java @@ -2164,7 +2164,7 @@ public abstract class PlayerImpl implements Player, Serializable { public ManaOptions getManaAvailable(Game game) { ManaOptions available = new ManaOptions(); - List> sourceWithoutCosts = new ArrayList<>(); + List> sourceWithoutManaCosts = new ArrayList<>(); List> sourceWithCosts = new ArrayList<>(); for (Permanent permanent : game.getBattlefield().getAllActivePermanents(playerId)) { boolean canAdd = false; @@ -2183,12 +2183,12 @@ public abstract class PlayerImpl implements Player, Serializable { if (withCost) { sourceWithCosts.add(manaAbilities); } else { - sourceWithoutCosts.add(manaAbilities); + sourceWithoutManaCosts.add(manaAbilities); } } } - for (Abilities manaAbilities : sourceWithoutCosts) { + for (Abilities manaAbilities : sourceWithoutManaCosts) { available.addMana(manaAbilities, game); } for (Abilities manaAbilities : sourceWithCosts) {