From a63e024ea45d3856ce5581d3d05de4e2d46f6c03 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Fri, 6 Oct 2017 12:29:14 -0400 Subject: [PATCH 01/18] initial setup for phasing fix --- .../src/main/java/mage/game/permanent/Permanent.java | 4 ++++ .../main/java/mage/game/permanent/PermanentImpl.java | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/Mage/src/main/java/mage/game/permanent/Permanent.java b/Mage/src/main/java/mage/game/permanent/Permanent.java index fedf9aad48..aaf0b42d13 100644 --- a/Mage/src/main/java/mage/game/permanent/Permanent.java +++ b/Mage/src/main/java/mage/game/permanent/Permanent.java @@ -80,8 +80,12 @@ public interface Permanent extends Card, Controllable { boolean phaseIn(Game game); + boolean phaseIn(Game game, boolean indirectPhase); + boolean phaseOut(Game game); + boolean phaseOut(Game game, boolean indirectPhase); + boolean isMonstrous(); void setMonstrous(boolean value); diff --git a/Mage/src/main/java/mage/game/permanent/PermanentImpl.java b/Mage/src/main/java/mage/game/permanent/PermanentImpl.java index f4bd39bb1a..464cbabb42 100644 --- a/Mage/src/main/java/mage/game/permanent/PermanentImpl.java +++ b/Mage/src/main/java/mage/game/permanent/PermanentImpl.java @@ -89,6 +89,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { protected boolean controlledFromStartOfControllerTurn; protected int turnsOnBattlefield; protected boolean phasedIn = true; + protected boolean indirectPhase = false; protected boolean faceDown; protected boolean attacking; protected int blocking; @@ -138,6 +139,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { this.controlledFromStartOfControllerTurn = permanent.controlledFromStartOfControllerTurn; this.turnsOnBattlefield = permanent.turnsOnBattlefield; this.phasedIn = permanent.phasedIn; + this.indirectPhase = permanent.indirectPhase; this.faceDown = permanent.faceDown; this.attacking = permanent.attacking; this.blocking = permanent.blocking; @@ -463,6 +465,11 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { @Override public boolean phaseIn(Game game) { + return phaseIn(game, false); + } + + @Override + public boolean phaseIn(Game game, boolean indirectPhase) { if (!phasedIn) { if (!replaceEvent(EventType.PHASE_IN, game)) { this.phasedIn = true; @@ -478,6 +485,11 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { @Override public boolean phaseOut(Game game) { + return phaseOut(game, false); + } + + @Override + public boolean phaseOut(Game game, boolean indirectPhase) { if (phasedIn) { if (!replaceEvent(EventType.PHASE_OUT, game)) { this.phasedIn = false; From 50d9cb5568ada3145dc69b76a4fe6b19f97fe60b Mon Sep 17 00:00:00 2001 From: Jerek Wilson Date: Mon, 9 Oct 2017 22:04:30 -0400 Subject: [PATCH 02/18] JW-JaggedPoppet: Implemented Jagged Poppet --- .../target/maven-archiver/pom.properties | 2 +- Mage.Sets/src/mage/sets/Dissension.java | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Mage.Server.Plugins/Mage.Game.PennyDreadfulCommanderFreeForAll/target/maven-archiver/pom.properties b/Mage.Server.Plugins/Mage.Game.PennyDreadfulCommanderFreeForAll/target/maven-archiver/pom.properties index 9ea86aa6e9..ed3ff0c559 100644 --- a/Mage.Server.Plugins/Mage.Game.PennyDreadfulCommanderFreeForAll/target/maven-archiver/pom.properties +++ b/Mage.Server.Plugins/Mage.Game.PennyDreadfulCommanderFreeForAll/target/maven-archiver/pom.properties @@ -1,5 +1,5 @@ #Generated by Maven -#Fri Sep 15 22:14:29 CEST 2017 +#Mon Oct 09 21:49:22 EDT 2017 version=1.4.26 groupId=org.mage artifactId=mage-game-pennydreadfulcommanderfreeforall diff --git a/Mage.Sets/src/mage/sets/Dissension.java b/Mage.Sets/src/mage/sets/Dissension.java index 3bc0aaaf64..58724c4ca4 100644 --- a/Mage.Sets/src/mage/sets/Dissension.java +++ b/Mage.Sets/src/mage/sets/Dissension.java @@ -71,7 +71,7 @@ public class Dissension extends ExpansionSet { cards.add(new SetCardInfo("Blood Crypt", 171, Rarity.RARE, mage.cards.b.BloodCrypt.class)); cards.add(new SetCardInfo("Bond of Agony", 38, Rarity.UNCOMMON, mage.cards.b.BondOfAgony.class)); cards.add(new SetCardInfo("Bound // Determined", 149, Rarity.RARE, mage.cards.b.BoundDetermined.class)); - cards.add(new SetCardInfo("Brace for Impact", 5, Rarity.UNCOMMON, mage.cards.b.BraceForImpact.class)); + cards.add(new SetCardInfo("Brace for Impact", 5, Rarity.UNCOMMON, mage.cards.b.BraceForImpact.class)); cards.add(new SetCardInfo("Brain Pry", 39, Rarity.UNCOMMON, mage.cards.b.BrainPry.class)); cards.add(new SetCardInfo("Breeding Pool", 172, Rarity.RARE, mage.cards.b.BreedingPool.class)); cards.add(new SetCardInfo("Bronze Bombshell", 160, Rarity.RARE, mage.cards.b.BronzeBombshell.class)); @@ -118,6 +118,7 @@ public class Dissension extends ExpansionSet { cards.add(new SetCardInfo("Indrik Stomphowler", 86, Rarity.UNCOMMON, mage.cards.i.IndrikStomphowler.class)); cards.add(new SetCardInfo("Infernal Tutor", 46, Rarity.RARE, mage.cards.i.InfernalTutor.class)); cards.add(new SetCardInfo("Isperia the Inscrutable", 114, Rarity.RARE, mage.cards.i.IsperiaTheInscrutable.class)); + cards.add(new SetCardInfo("Jagged Poppet", 115, Rarity.UNCOMMON, mage.cards.j.JaggedPoppet.class)); cards.add(new SetCardInfo("Kill-Suit Cultist", 65, Rarity.COMMON, mage.cards.k.KillSuitCultist.class)); cards.add(new SetCardInfo("Kindle the Carnage", 66, Rarity.UNCOMMON, mage.cards.k.KindleTheCarnage.class)); cards.add(new SetCardInfo("Leafdrake Roost", 116, Rarity.UNCOMMON, mage.cards.l.LeafdrakeRoost.class)); @@ -199,7 +200,7 @@ public class Dissension extends ExpansionSet { cards.add(new SetCardInfo("Trial // Error", 158, Rarity.UNCOMMON, mage.cards.t.TrialError.class)); cards.add(new SetCardInfo("Trygon Predator", 133, Rarity.UNCOMMON, mage.cards.t.TrygonPredator.class)); cards.add(new SetCardInfo("Twinstrike", 134, Rarity.UNCOMMON, mage.cards.t.Twinstrike.class)); - cards.add(new SetCardInfo("Unliving Psychopath", 56, Rarity.RARE, mage.cards.u.UnlivingPsychopath.class)); + cards.add(new SetCardInfo("Unliving Psychopath", 56, Rarity.RARE, mage.cards.u.UnlivingPsychopath.class)); cards.add(new SetCardInfo("Utopia Sprawl", 99, Rarity.COMMON, mage.cards.u.UtopiaSprawl.class)); cards.add(new SetCardInfo("Utvara Scalper", 76, Rarity.COMMON, mage.cards.u.UtvaraScalper.class)); cards.add(new SetCardInfo("Valor Made Real", 20, Rarity.COMMON, mage.cards.v.ValorMadeReal.class)); From 1afcf67e8d59bc61c76db163c157eb2df6dfba7b Mon Sep 17 00:00:00 2001 From: Jerek Wilson Date: Tue, 10 Oct 2017 00:12:26 -0400 Subject: [PATCH 03/18] JW-IceFloe: Implemented Ice Floe --- .../target/maven-archiver/pom.properties | 2 +- Mage.Sets/src/mage/cards/i/IceFloe.java | 80 +++++++++ Mage.Sets/src/mage/cards/j/JaggedPoppet.java | 162 ++++++++++++++++++ Mage.Sets/src/mage/sets/FifthEdition.java | 5 +- Mage.Sets/src/mage/sets/IceAge.java | 11 +- Mage.Sets/src/mage/sets/MastersEditionII.java | 11 +- 6 files changed, 258 insertions(+), 13 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/i/IceFloe.java create mode 100644 Mage.Sets/src/mage/cards/j/JaggedPoppet.java diff --git a/Mage.Server.Plugins/Mage.Game.PennyDreadfulCommanderFreeForAll/target/maven-archiver/pom.properties b/Mage.Server.Plugins/Mage.Game.PennyDreadfulCommanderFreeForAll/target/maven-archiver/pom.properties index ed3ff0c559..819e9c0418 100644 --- a/Mage.Server.Plugins/Mage.Game.PennyDreadfulCommanderFreeForAll/target/maven-archiver/pom.properties +++ b/Mage.Server.Plugins/Mage.Game.PennyDreadfulCommanderFreeForAll/target/maven-archiver/pom.properties @@ -1,5 +1,5 @@ #Generated by Maven -#Mon Oct 09 21:49:22 EDT 2017 +#Mon Oct 09 23:56:07 EDT 2017 version=1.4.26 groupId=org.mage artifactId=mage-game-pennydreadfulcommanderfreeforall diff --git a/Mage.Sets/src/mage/cards/i/IceFloe.java b/Mage.Sets/src/mage/cards/i/IceFloe.java new file mode 100644 index 0000000000..e66f0ce6e3 --- /dev/null +++ b/Mage.Sets/src/mage/cards/i/IceFloe.java @@ -0,0 +1,80 @@ +/* + * 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.i; + +import java.util.UUID; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.common.SkipUntapOptionalAbility; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.DontUntapAsLongAsSourceTappedEffect; +import mage.abilities.effects.common.TapTargetEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Zone; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.mageobject.AbilityPredicate; +import mage.target.common.TargetCreaturePermanent; + +/** + * + * @author anonymous + */ +public class IceFloe extends CardImpl { + + private static final FilterCreaturePermanent filter = new FilterCreaturePermanent("creature without flying"); + + static { + filter.add(Predicates.not(new AbilityPredicate(FlyingAbility.class))); + } + + public IceFloe(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.LAND}, ""); + + // You may choose not to untap Ice Floe during your untap step. + this.addAbility(new SkipUntapOptionalAbility()); + + // {tap}: Tap target creature without flying that's attacking you. It doesn't untap during its controller's untap step for as long as Ice Floe remains tapped. + SimpleActivatedAbility ability = new SimpleActivatedAbility(new TapTargetEffect(), new TapSourceCost()); + ability.addTarget(new TargetCreaturePermanent(filter)); + ability.addEffect(new DontUntapAsLongAsSourceTappedEffect()); + this.addAbility(ability); + } + + public IceFloe(final IceFloe card) { + super(card); + } + + @Override + public IceFloe copy() { + return new IceFloe(this); + } +} diff --git a/Mage.Sets/src/mage/cards/j/JaggedPoppet.java b/Mage.Sets/src/mage/cards/j/JaggedPoppet.java new file mode 100644 index 0000000000..4ac9008fea --- /dev/null +++ b/Mage.Sets/src/mage/cards/j/JaggedPoppet.java @@ -0,0 +1,162 @@ +/* + * 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.j; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.DealsCombatDamageToAPlayerTriggeredAbility; +import mage.abilities.common.DealtDamageToSourceTriggeredAbility; +import mage.abilities.condition.common.HellbentCondition; +import mage.abilities.decorator.ConditionalTriggeredAbility; +import mage.abilities.effects.Effect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.discard.DiscardControllerEffect; +import mage.constants.SubType; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.AbilityWord; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.Zone; +import mage.game.Game; +import mage.players.Player; + +/** + * + * @author anonymous + */ +public class JaggedPoppet extends CardImpl { + + public JaggedPoppet(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{B}{R}"); + + this.subtype.add(SubType.OGRE); + this.subtype.add(SubType.WARRIOR); + this.power = new MageInt(3); + this.toughness = new MageInt(4); + + // Whenever Jagged Poppet is dealt damage, discard that many cards. + Ability dealtDamageAbility = new DealtDamageToSourceTriggeredAbility(Zone.BATTLEFIELD, new JaggedPoppetDealtDamageEffect(), false, false, true); + this.addAbility(dealtDamageAbility); + + // Hellbent - Whenever Jagged Poppet deals combat damage to a player, if you have no cards in hand, that player discards cards equal to the damage. + Ability hellbentAbility = new ConditionalTriggeredAbility( + new DealsCombatDamageToAPlayerTriggeredAbility(new JaggedPoppetDealsDamageEffect(), false,true), + HellbentCondition.instance, + "Hellbent - Whenever {this} deals combat damage to a player, if you have no cards in hand, that player discards cards equal to the damage."); + + hellbentAbility.setAbilityWord(AbilityWord.HELLBENT); + this.addAbility(hellbentAbility); + + + + + } + + public JaggedPoppet(final JaggedPoppet card) { + super(card); + } + + @Override + public JaggedPoppet copy() { + return new JaggedPoppet(this); + } +} + + +class JaggedPoppetDealsDamageEffect extends OneShotEffect { + + public JaggedPoppetDealsDamageEffect() { + super(Outcome.Discard); + //staticText = "it deals that much damage to each creature that player controls"; + } + + public JaggedPoppetDealsDamageEffect(final JaggedPoppetDealsDamageEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + //According to the Balefire Dragon code, This statement gets the player that was dealt the combat damage + Player player = game.getPlayer(targetPointer.getFirst(game, source)); + if (player != null) { + //Call the getValue method of the Effect class to retrieve the amount of damage + int amount = (Integer) getValue("damage"); + + if(amount > 0){ + //Call the player discard function discarding cards equal to damage + player.discard(amount, false, source, game); + } + return true; + } + return false; + } + + @Override + public JaggedPoppetDealsDamageEffect copy() { + return new JaggedPoppetDealsDamageEffect(this); + } +} + + +class JaggedPoppetDealtDamageEffect extends OneShotEffect { + + public JaggedPoppetDealtDamageEffect() { + super(Outcome.Discard); + staticText = "discard that many cards"; + } + + public JaggedPoppetDealtDamageEffect(final JaggedPoppetDealtDamageEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + + //According to the Firedrinker Satyr code, This statement gets the player that controls Jagged Poppet + Player player = game.getPlayer(source.getControllerId()); + if (player != null) { + //Call the getValue method of the Effect class to retrieve the amount of damage + int amount = (Integer) getValue("damage"); + + if(amount > 0){ + //Call the player discard function discarding cards equal to damage + player.discard(amount, false, source, game); + } + return true; + } + return false; + + } + + @Override + public JaggedPoppetDealtDamageEffect copy() { + return new JaggedPoppetDealtDamageEffect(this); + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/sets/FifthEdition.java b/Mage.Sets/src/mage/sets/FifthEdition.java index ec8633bbe7..ed6514151b 100644 --- a/Mage.Sets/src/mage/sets/FifthEdition.java +++ b/Mage.Sets/src/mage/sets/FifthEdition.java @@ -217,6 +217,7 @@ public class FifthEdition extends ExpansionSet { cards.add(new SetCardInfo("Icatian Scout", 313, Rarity.COMMON, IcatianScout.class)); cards.add(new SetCardInfo("Icatian Store", 423, Rarity.RARE, mage.cards.i.IcatianStore.class)); cards.add(new SetCardInfo("Icatian Town", 314, Rarity.RARE, mage.cards.i.IcatianTown.class)); + cards.add(new SetCardInfo("Ice Floe", 424, Rarity.UNCOMMON, mage.cards.i.IceFloe.class)); cards.add(new SetCardInfo("Imposing Visage", 241, Rarity.COMMON, mage.cards.i.ImposingVisage.class)); cards.add(new SetCardInfo("Incinerate", 242, Rarity.COMMON, mage.cards.i.Incinerate.class)); cards.add(new SetCardInfo("Inferno", 243, Rarity.RARE, mage.cards.i.Inferno.class)); @@ -252,7 +253,7 @@ public class FifthEdition extends ExpansionSet { cards.add(new SetCardInfo("Knight of Stromgald", 33, Rarity.UNCOMMON, mage.cards.k.KnightOfStromgald.class)); cards.add(new SetCardInfo("Krovikan Fetish", 34, Rarity.COMMON, mage.cards.k.KrovikanFetish.class)); cards.add(new SetCardInfo("Krovikan Sorcerer", 96, Rarity.COMMON, mage.cards.k.KrovikanSorcerer.class)); - cards.add(new SetCardInfo("Labyrinth Minotaur", 97, Rarity.COMMON, mage.cards.l.LabyrinthMinotaur.class)); + cards.add(new SetCardInfo("Labyrinth Minotaur", 97, Rarity.COMMON, mage.cards.l.LabyrinthMinotaur.class)); cards.add(new SetCardInfo("Leshrac's Rite", 35, Rarity.UNCOMMON, mage.cards.l.LeshracsRite.class)); cards.add(new SetCardInfo("Leviathan", 98, Rarity.RARE, mage.cards.l.Leviathan.class)); cards.add(new SetCardInfo("Ley Druid", 170, Rarity.COMMON, mage.cards.l.LeyDruid.class)); @@ -300,7 +301,7 @@ public class FifthEdition extends ExpansionSet { cards.add(new SetCardInfo("Orcish Artillery", 253, Rarity.UNCOMMON, mage.cards.o.OrcishArtillery.class)); cards.add(new SetCardInfo("Orcish Captain", 254, Rarity.UNCOMMON, mage.cards.o.OrcishCaptain.class)); cards.add(new SetCardInfo("Orcish Oriflamme", 257, Rarity.UNCOMMON, mage.cards.o.OrcishOriflamme.class)); - cards.add(new SetCardInfo("Orcish Squatters", 258, Rarity.RARE, mage.cards.o.OrcishSquatters.class)); + cards.add(new SetCardInfo("Orcish Squatters", 258, Rarity.RARE, mage.cards.o.OrcishSquatters.class)); cards.add(new SetCardInfo("Order of the Sacred Torch", 324, Rarity.RARE, mage.cards.o.OrderOfTheSacredTorch.class)); cards.add(new SetCardInfo("Order of the White Shield", 325, Rarity.UNCOMMON, mage.cards.o.OrderOfTheWhiteShield.class)); cards.add(new SetCardInfo("Orgg", 259, Rarity.RARE, mage.cards.o.Orgg.class)); diff --git a/Mage.Sets/src/mage/sets/IceAge.java b/Mage.Sets/src/mage/sets/IceAge.java index 9c28934691..36c829eeab 100644 --- a/Mage.Sets/src/mage/sets/IceAge.java +++ b/Mage.Sets/src/mage/sets/IceAge.java @@ -61,7 +61,7 @@ public class IceAge extends ExpansionSet { cards.add(new SetCardInfo("Arenson's Aura", 227, Rarity.COMMON, mage.cards.a.ArensonsAura.class)); cards.add(new SetCardInfo("Armor of Faith", 228, Rarity.COMMON, mage.cards.a.ArmorOfFaith.class)); cards.add(new SetCardInfo("Arnjlot's Ascent", 57, Rarity.COMMON, mage.cards.a.ArnjlotsAscent.class)); - cards.add(new SetCardInfo("Ashen Ghoul", 2, Rarity.UNCOMMON, mage.cards.a.AshenGhoul.class)); + cards.add(new SetCardInfo("Ashen Ghoul", 2, Rarity.UNCOMMON, mage.cards.a.AshenGhoul.class)); cards.add(new SetCardInfo("Aurochs", 113, Rarity.COMMON, mage.cards.a.Aurochs.class)); cards.add(new SetCardInfo("Avalanche", 171, Rarity.UNCOMMON, mage.cards.a.Avalanche.class)); cards.add(new SetCardInfo("Balduvian Barbarians", 172, Rarity.COMMON, mage.cards.b.BalduvianBarbarians.class)); @@ -108,7 +108,7 @@ public class IceAge extends ExpansionSet { cards.add(new SetCardInfo("Disenchant", 244, Rarity.COMMON, mage.cards.d.Disenchant.class)); cards.add(new SetCardInfo("Drift of the Dead", 11, Rarity.UNCOMMON, mage.cards.d.DriftOfTheDead.class)); cards.add(new SetCardInfo("Dwarven Armory", 182, Rarity.RARE, mage.cards.d.DwarvenArmory.class)); - cards.add(new SetCardInfo("Earthlink", 363, Rarity.RARE, mage.cards.e.Earthlink.class)); + cards.add(new SetCardInfo("Earthlink", 363, Rarity.RARE, mage.cards.e.Earthlink.class)); cards.add(new SetCardInfo("Elder Druid", 120, Rarity.RARE, mage.cards.e.ElderDruid.class)); cards.add(new SetCardInfo("Elemental Augury", 364, Rarity.RARE, mage.cards.e.ElementalAugury.class)); cards.add(new SetCardInfo("Enduring Renewal", 247, Rarity.RARE, mage.cards.e.EnduringRenewal.class)); @@ -162,6 +162,7 @@ public class IceAge extends ExpansionSet { cards.add(new SetCardInfo("Hyalopterous Lemure", 21, Rarity.UNCOMMON, mage.cards.h.HyalopterousLemure.class)); cards.add(new SetCardInfo("Hydroblast", 72, Rarity.COMMON, mage.cards.h.Hydroblast.class)); cards.add(new SetCardInfo("Hymn of Rebirth", 373, Rarity.UNCOMMON, mage.cards.h.HymnOfRebirth.class)); + cards.add(new SetCardInfo("Ice Floe", 333, Rarity.UNCOMMON, mage.cards.i.IceFloe.class)); cards.add(new SetCardInfo("Iceberg", 73, Rarity.UNCOMMON, mage.cards.i.Iceberg.class)); cards.add(new SetCardInfo("Icequake", 22, Rarity.UNCOMMON, mage.cards.i.Icequake.class)); cards.add(new SetCardInfo("Icy Manipulator", 297, Rarity.UNCOMMON, mage.cards.i.IcyManipulator.class)); @@ -231,7 +232,7 @@ public class IceAge extends ExpansionSet { cards.add(new SetCardInfo("Orcish Healer", 208, Rarity.UNCOMMON, mage.cards.o.OrcishHealer.class)); cards.add(new SetCardInfo("Orcish Librarian", 209, Rarity.RARE, mage.cards.o.OrcishLibrarian.class)); cards.add(new SetCardInfo("Orcish Lumberjack", 210, Rarity.COMMON, mage.cards.o.OrcishLumberjack.class)); - cards.add(new SetCardInfo("Orcish Squatters", 211, Rarity.RARE, mage.cards.o.OrcishSquatters.class)); + cards.add(new SetCardInfo("Orcish Squatters", 211, Rarity.RARE, mage.cards.o.OrcishSquatters.class)); cards.add(new SetCardInfo("Order of the Sacred Torch", 269, Rarity.RARE, mage.cards.o.OrderOfTheSacredTorch.class)); cards.add(new SetCardInfo("Order of the White Shield", 270, Rarity.UNCOMMON, mage.cards.o.OrderOfTheWhiteShield.class)); cards.add(new SetCardInfo("Pale Bears", 144, Rarity.RARE, mage.cards.p.PaleBears.class)); @@ -270,14 +271,14 @@ public class IceAge extends ExpansionSet { cards.add(new SetCardInfo("Silver Erne", 98, Rarity.UNCOMMON, mage.cards.s.SilverErne.class)); cards.add(new SetCardInfo("Skeleton Ship", 379, Rarity.RARE, mage.cards.s.SkeletonShip.class)); cards.add(new SetCardInfo("Skull Catapult", 311, Rarity.UNCOMMON, mage.cards.s.SkullCatapult.class)); - cards.add(new SetCardInfo("Snow Fortress", 312, Rarity.RARE, mage.cards.s.SnowFortress.class)); + cards.add(new SetCardInfo("Snow Fortress", 312, Rarity.RARE, mage.cards.s.SnowFortress.class)); cards.add(new SetCardInfo("Snow-Covered Forest", 347, Rarity.COMMON, mage.cards.s.SnowCoveredForest.class)); cards.add(new SetCardInfo("Snow-Covered Island", 348, Rarity.COMMON, mage.cards.s.SnowCoveredIsland.class)); cards.add(new SetCardInfo("Snow-Covered Mountain", 349, Rarity.COMMON, mage.cards.s.SnowCoveredMountain.class)); cards.add(new SetCardInfo("Snow-Covered Plains", 350, Rarity.COMMON, mage.cards.s.SnowCoveredPlains.class)); cards.add(new SetCardInfo("Snow-Covered Swamp", 351, Rarity.COMMON, mage.cards.s.SnowCoveredSwamp.class)); cards.add(new SetCardInfo("Snow Hound", 277, Rarity.UNCOMMON, mage.cards.s.SnowHound.class)); - cards.add(new SetCardInfo("Soldevi Golem", 313, Rarity.RARE, mage.cards.s.SoldeviGolem.class)); + cards.add(new SetCardInfo("Soldevi Golem", 313, Rarity.RARE, mage.cards.s.SoldeviGolem.class)); cards.add(new SetCardInfo("Soldevi Machinist", 102, Rarity.UNCOMMON, mage.cards.s.SoldeviMachinist.class)); cards.add(new SetCardInfo("Soldevi Simulacrum", 314, Rarity.UNCOMMON, mage.cards.s.SoldeviSimulacrum.class)); cards.add(new SetCardInfo("Songs of the Damned", 48, Rarity.COMMON, mage.cards.s.SongsOfTheDamned.class)); diff --git a/Mage.Sets/src/mage/sets/MastersEditionII.java b/Mage.Sets/src/mage/sets/MastersEditionII.java index 12dda96464..39a525e7d8 100644 --- a/Mage.Sets/src/mage/sets/MastersEditionII.java +++ b/Mage.Sets/src/mage/sets/MastersEditionII.java @@ -76,7 +76,7 @@ public class MastersEditionII extends ExpansionSet { cards.add(new SetCardInfo("Armored Griffin", 5, Rarity.COMMON, mage.cards.a.ArmoredGriffin.class)); cards.add(new SetCardInfo("Armor of Faith", 4, Rarity.COMMON, mage.cards.a.ArmorOfFaith.class)); cards.add(new SetCardInfo("Armor Thrull", 77, Rarity.COMMON, ArmorThrull.class)); - cards.add(new SetCardInfo("Ashen Ghoul", 78, Rarity.UNCOMMON, mage.cards.a.AshenGhoul.class)); + cards.add(new SetCardInfo("Ashen Ghoul", 78, Rarity.UNCOMMON, mage.cards.a.AshenGhoul.class)); cards.add(new SetCardInfo("Aurochs", 153, Rarity.COMMON, mage.cards.a.Aurochs.class)); cards.add(new SetCardInfo("Aysen Bureaucrats", 6, Rarity.COMMON, mage.cards.a.AysenBureaucrats.class)); cards.add(new SetCardInfo("Aysen Crusader", 7, Rarity.UNCOMMON, mage.cards.a.AysenCrusader.class)); @@ -110,7 +110,7 @@ public class MastersEditionII extends ExpansionSet { cards.add(new SetCardInfo("Drift of the Dead", 86, Rarity.COMMON, mage.cards.d.DriftOfTheDead.class)); cards.add(new SetCardInfo("Dwarven Ruins", 227, Rarity.UNCOMMON, mage.cards.d.DwarvenRuins.class)); cards.add(new SetCardInfo("Dystopia", 88, Rarity.RARE, mage.cards.d.Dystopia.class)); - cards.add(new SetCardInfo("Earthlink", 192, Rarity.RARE, mage.cards.e.Earthlink.class)); + cards.add(new SetCardInfo("Earthlink", 192, Rarity.RARE, mage.cards.e.Earthlink.class)); cards.add(new SetCardInfo("Ebon Praetor", 89, Rarity.RARE, mage.cards.e.EbonPraetor.class)); cards.add(new SetCardInfo("Ebon Stronghold", 228, Rarity.UNCOMMON, mage.cards.e.EbonStronghold.class)); cards.add(new SetCardInfo("Elemental Augury", 193, Rarity.RARE, mage.cards.e.ElementalAugury.class)); @@ -146,6 +146,7 @@ public class MastersEditionII extends ExpansionSet { cards.add(new SetCardInfo("Helm of Obedience", 210, Rarity.RARE, mage.cards.h.HelmOfObedience.class)); cards.add(new SetCardInfo("Icatian Javelineers", 15, Rarity.COMMON, IcatianJavelineers.class)); cards.add(new SetCardInfo("Icatian Scout", 17, Rarity.COMMON, IcatianScout.class)); + cards.add(new SetCardInfo("Ice Floe", 232, Rarity.UNCOMMON, mage.cards.i.IceFloe.class)); cards.add(new SetCardInfo("Iceberg", 49, Rarity.UNCOMMON, mage.cards.i.Iceberg.class)); cards.add(new SetCardInfo("Icequake", 94, Rarity.COMMON, mage.cards.i.Icequake.class)); cards.add(new SetCardInfo("Icy Prison", 50, Rarity.COMMON, mage.cards.i.IcyPrison.class)); @@ -163,7 +164,7 @@ public class MastersEditionII extends ExpansionSet { cards.add(new SetCardInfo("Karplusan Giant", 133, Rarity.UNCOMMON, mage.cards.k.KarplusanGiant.class)); cards.add(new SetCardInfo("Kaysa", 170, Rarity.RARE, mage.cards.k.Kaysa.class)); cards.add(new SetCardInfo("Kjeldoran Dead", 98, Rarity.COMMON, mage.cards.k.KjeldoranDead.class)); - cards.add(new SetCardInfo("Kjeldoran Home Guard", 22, Rarity.UNCOMMON, mage.cards.k.KjeldoranHomeGuard.class)); + cards.add(new SetCardInfo("Kjeldoran Home Guard", 22, Rarity.UNCOMMON, mage.cards.k.KjeldoranHomeGuard.class)); cards.add(new SetCardInfo("Kjeldoran Outpost", 233, Rarity.RARE, mage.cards.k.KjeldoranOutpost.class)); cards.add(new SetCardInfo("Knight of Stromgald", 99, Rarity.UNCOMMON, mage.cards.k.KnightOfStromgald.class)); cards.add(new SetCardInfo("Krovikan Fetish", 100, Rarity.COMMON, mage.cards.k.KrovikanFetish.class)); @@ -186,7 +187,7 @@ public class MastersEditionII extends ExpansionSet { cards.add(new SetCardInfo("Orcish Cannoneers", 138, Rarity.UNCOMMON, mage.cards.o.OrcishCannoneers.class)); cards.add(new SetCardInfo("Orcish Captain", 139, Rarity.UNCOMMON, mage.cards.o.OrcishCaptain.class)); cards.add(new SetCardInfo("Orcish Lumberjack", 142, Rarity.COMMON, mage.cards.o.OrcishLumberjack.class)); - cards.add(new SetCardInfo("Orcish Squatters", 143, Rarity.RARE, mage.cards.o.OrcishSquatters.class)); + cards.add(new SetCardInfo("Orcish Squatters", 143, Rarity.RARE, mage.cards.o.OrcishSquatters.class)); cards.add(new SetCardInfo("Orcish Veteran", 144, Rarity.COMMON, OrcishVeteran.class)); cards.add(new SetCardInfo("Order of the Sacred Torch", 25, Rarity.RARE, mage.cards.o.OrderOfTheSacredTorch.class)); cards.add(new SetCardInfo("Order of the White Shield", 26, Rarity.UNCOMMON, mage.cards.o.OrderOfTheWhiteShield.class)); @@ -216,7 +217,7 @@ public class MastersEditionII extends ExpansionSet { cards.add(new SetCardInfo("Sibilant Spirit", 67, Rarity.RARE, mage.cards.s.SibilantSpirit.class)); cards.add(new SetCardInfo("Skeleton Ship", 197, Rarity.RARE, mage.cards.s.SkeletonShip.class)); cards.add(new SetCardInfo("Skull Catapult", 219, Rarity.UNCOMMON, mage.cards.s.SkullCatapult.class)); - cards.add(new SetCardInfo("Snow Fortress", 220, Rarity.UNCOMMON, mage.cards.s.SnowFortress.class)); + cards.add(new SetCardInfo("Snow Fortress", 220, Rarity.UNCOMMON, mage.cards.s.SnowFortress.class)); cards.add(new SetCardInfo("Snow-Covered Forest", 245, Rarity.LAND, mage.cards.s.SnowCoveredForest.class)); cards.add(new SetCardInfo("Snow-Covered Island", 242, Rarity.LAND, mage.cards.s.SnowCoveredIsland.class)); cards.add(new SetCardInfo("Snow-Covered Mountain", 244, Rarity.LAND, mage.cards.s.SnowCoveredMountain.class)); From 3d20e4dbefd0791c07a170a4c4ec164b29ce493c Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Tue, 10 Oct 2017 13:35:41 -0400 Subject: [PATCH 04/18] changed how phasing is handled --- Mage.Sets/src/mage/cards/e/Equipoise.java | 7 ++--- .../src/mage/cards/t/TeferisProtection.java | 6 +++- .../java/mage/game/permanent/Permanent.java | 4 ++- .../mage/game/permanent/PermanentImpl.java | 28 ++++++++++++++++--- .../main/java/mage/players/PlayerImpl.java | 13 ++++----- 5 files changed, 41 insertions(+), 17 deletions(-) diff --git a/Mage.Sets/src/mage/cards/e/Equipoise.java b/Mage.Sets/src/mage/cards/e/Equipoise.java index 7f3f526804..a1606980a8 100644 --- a/Mage.Sets/src/mage/cards/e/Equipoise.java +++ b/Mage.Sets/src/mage/cards/e/Equipoise.java @@ -55,8 +55,7 @@ import mage.target.TargetPlayer; public class Equipoise extends CardImpl { public Equipoise(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.ENCHANTMENT},"{2}{W}"); - + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{W}"); // At the beginning of your upkeep, for each land target player controls in excess of the number you control, choose a land he or she controls, then the chosen permanents phase out. Repeat this process for artifacts and creatures. Ability ability = new BeginningOfUpkeepTriggeredAbility(new EquipoiseEffect(), TargetController.YOU, false); @@ -112,12 +111,12 @@ class EquipoiseEffect extends OneShotEffect { int numberTargetPlayer = game.getBattlefield().count(filter, source.getSourceId(), targetPlayer.getId(), game); int excess = numberTargetPlayer - numberController; if (excess > 0) { - FilterPermanent filterChoose = new FilterPermanent(cardType.toString().toLowerCase() + (excess > 1 ? "s":"") +" of target player"); + FilterPermanent filterChoose = new FilterPermanent(cardType.toString().toLowerCase() + (excess > 1 ? "s" : "") + " of target player"); filterChoose.add(new ControllerIdPredicate(targetPlayer.getId())); filterChoose.add(new CardTypePredicate(cardType)); Target target = new TargetPermanent(excess, excess, filterChoose, true); controller.chooseTarget(outcome, target, source, game); - for (UUID permanentId:target.getTargets()) { + for (UUID permanentId : target.getTargets()) { Permanent permanent = game.getPermanent(permanentId); if (permanent != null) { permanent.phaseOut(game); diff --git a/Mage.Sets/src/mage/cards/t/TeferisProtection.java b/Mage.Sets/src/mage/cards/t/TeferisProtection.java index a08ad3ba12..b9ae7df609 100644 --- a/Mage.Sets/src/mage/cards/t/TeferisProtection.java +++ b/Mage.Sets/src/mage/cards/t/TeferisProtection.java @@ -171,7 +171,11 @@ class TeferisProtectionPhaseOutEffect extends OneShotEffect { Player controller = game.getPlayer(source.getControllerId()); if (controller != null) { for (Permanent permanent : game.getBattlefield().getActivePermanents(StaticFilters.FILTER_CONTROLLED_PERMANENT, controller.getId(), game)) { - permanent.phaseOut(game); + Permanent attachedTo = game.getPermanent(permanent.getAttachedTo()); + // don't phase out auras directly if they're attached to your stuff + if (!(attachedTo != null && attachedTo.getControllerId().equals(controller.getId()))) { + permanent.phaseOut(game); + } } return true; } diff --git a/Mage/src/main/java/mage/game/permanent/Permanent.java b/Mage/src/main/java/mage/game/permanent/Permanent.java index aaf0b42d13..45e8226439 100644 --- a/Mage/src/main/java/mage/game/permanent/Permanent.java +++ b/Mage/src/main/java/mage/game/permanent/Permanent.java @@ -78,9 +78,11 @@ public interface Permanent extends Card, Controllable { boolean isPhasedIn(); + boolean isPhasedOutIndirectly(); + boolean phaseIn(Game game); - boolean phaseIn(Game game, boolean indirectPhase); + boolean phaseIn(Game game, boolean onlyDirect); boolean phaseOut(Game game); diff --git a/Mage/src/main/java/mage/game/permanent/PermanentImpl.java b/Mage/src/main/java/mage/game/permanent/PermanentImpl.java index 464cbabb42..c30051d825 100644 --- a/Mage/src/main/java/mage/game/permanent/PermanentImpl.java +++ b/Mage/src/main/java/mage/game/permanent/PermanentImpl.java @@ -464,18 +464,31 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { } @Override - public boolean phaseIn(Game game) { - return phaseIn(game, false); + public boolean isPhasedOutIndirectly() { + return !phasedIn && indirectPhase; } @Override - public boolean phaseIn(Game game, boolean indirectPhase) { + public boolean phaseIn(Game game) { + return phaseIn(game, true); + } + + @Override + public boolean phaseIn(Game game, boolean onlyDirect) { if (!phasedIn) { - if (!replaceEvent(EventType.PHASE_IN, game)) { + if (!replaceEvent(EventType.PHASE_IN, game) + && ((onlyDirect && !indirectPhase) || (!onlyDirect))) { this.phasedIn = true; + this.indirectPhase = false; if (!game.isSimulation()) { game.informPlayers(getLogName() + " phased in"); } + for (UUID attachedId : this.getAttachments()) { + Permanent attachedPerm = game.getPermanent(attachedId); + if (attachedPerm != null) { + attachedPerm.phaseIn(game, false); + } + } fireEvent(EventType.PHASED_IN, game); return true; } @@ -492,7 +505,14 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { public boolean phaseOut(Game game, boolean indirectPhase) { if (phasedIn) { if (!replaceEvent(EventType.PHASE_OUT, game)) { + for (UUID attachedId : this.getAttachments()) { + Permanent attachedPerm = game.getPermanent(attachedId); + if (attachedPerm != null) { + attachedPerm.phaseOut(game, true); + } + } this.phasedIn = false; + this.indirectPhase = indirectPhase; if (!game.isSimulation()) { game.informPlayers(getLogName() + " phased out"); } diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index 1567246fda..cb5e474cf2 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -1484,16 +1484,15 @@ public abstract class PlayerImpl implements Player, Serializable { // phasing out is known as phasing out "indirectly." An enchantment or Equipment // that phased out indirectly won't phase in by itself, but instead phases in // along with the card it's attached to. - for (UUID attachmentId : permanent.getAttachments()) { - Permanent attachment = game.getPermanent(attachmentId); - if (attachment != null) { - attachment.phaseOut(game); - } + Permanent attachedTo = game.getPermanent(permanent.getAttachedTo()); + if (!(attachedTo != null && attachedTo.getControllerId().equals(this.getId()))) { + permanent.phaseOut(game, false); } - permanent.phaseOut(game); } for (Permanent permanent : phasedOut) { - permanent.phaseIn(game); + if (!permanent.isPhasedOutIndirectly()) { + permanent.phaseIn(game); + } } } From fdf3f831ca487bf0df8946f5d600f495d0691385 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Tue, 10 Oct 2017 14:31:00 -0400 Subject: [PATCH 05/18] updated cards which phase things out to properly handle indirect phasing (#4071) --- Mage.Sets/src/mage/cards/e/Equipoise.java | 9 +- Mage.Sets/src/mage/cards/t/Taniwha.java | 15 ++-- .../src/mage/cards/t/TeferisProtection.java | 12 +-- Mage.Sets/src/mage/cards/t/TeferisRealm.java | 11 ++- .../effects/common/PhaseOutAllEffect.java | 85 +++++++++++++++++++ 5 files changed, 112 insertions(+), 20 deletions(-) create mode 100644 Mage/src/main/java/mage/abilities/effects/common/PhaseOutAllEffect.java diff --git a/Mage.Sets/src/mage/cards/e/Equipoise.java b/Mage.Sets/src/mage/cards/e/Equipoise.java index a1606980a8..46aafab810 100644 --- a/Mage.Sets/src/mage/cards/e/Equipoise.java +++ b/Mage.Sets/src/mage/cards/e/Equipoise.java @@ -27,11 +27,13 @@ */ package mage.cards.e; +import java.util.List; import java.util.Objects; import java.util.UUID; import mage.abilities.Ability; import mage.abilities.common.BeginningOfUpkeepTriggeredAbility; import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.PhaseOutAllEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -116,12 +118,7 @@ class EquipoiseEffect extends OneShotEffect { filterChoose.add(new CardTypePredicate(cardType)); Target target = new TargetPermanent(excess, excess, filterChoose, true); controller.chooseTarget(outcome, target, source, game); - for (UUID permanentId : target.getTargets()) { - Permanent permanent = game.getPermanent(permanentId); - if (permanent != null) { - permanent.phaseOut(game); - } - } + new PhaseOutAllEffect(target.getTargets()).apply(game, source); } } } diff --git a/Mage.Sets/src/mage/cards/t/Taniwha.java b/Mage.Sets/src/mage/cards/t/Taniwha.java index ac9524bd13..6e7897ec3c 100644 --- a/Mage.Sets/src/mage/cards/t/Taniwha.java +++ b/Mage.Sets/src/mage/cards/t/Taniwha.java @@ -27,11 +27,14 @@ */ package mage.cards.t; +import java.util.ArrayList; +import java.util.List; import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.BeginningOfUpkeepTriggeredAbility; import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.PhaseOutAllEffect; import mage.abilities.keyword.PhasingAbility; import mage.abilities.keyword.TrampleAbility; import mage.cards.CardImpl; @@ -41,6 +44,7 @@ import mage.constants.SubType; import mage.constants.Outcome; import mage.constants.SuperType; import mage.constants.TargetController; +import mage.filter.StaticFilters; import mage.filter.common.FilterControlledLandPermanent; import mage.game.Game; import mage.game.permanent.Permanent; @@ -53,7 +57,7 @@ import mage.players.Player; public class Taniwha extends CardImpl { public Taniwha(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{3}{U}{U}"); + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{U}{U}"); addSuperType(SuperType.LEGENDARY); this.subtype.add(SubType.SERPENT); this.power = new MageInt(7); @@ -61,10 +65,10 @@ public class Taniwha extends CardImpl { // Trample this.addAbility(TrampleAbility.getInstance()); - + // Phasing this.addAbility(PhasingAbility.getInstance()); - + // At the beginning of your upkeep, all lands you control phase out. this.addAbility(new BeginningOfUpkeepTriggeredAbility(new TaniwhaEffect(), TargetController.YOU, false)); } @@ -99,10 +103,11 @@ class TaniwhaEffect extends OneShotEffect { public boolean apply(Game game, Ability source) { Player controller = game.getPlayer(source.getControllerId()); if (controller != null) { + List permIds = new ArrayList<>(); for (Permanent permanent : game.getBattlefield().getActivePermanents(new FilterControlledLandPermanent(), controller.getId(), game)) { - permanent.phaseOut(game); + permIds.add(permanent.getId()); } - return true; + return new PhaseOutAllEffect(permIds).apply(game, source); } return false; } diff --git a/Mage.Sets/src/mage/cards/t/TeferisProtection.java b/Mage.Sets/src/mage/cards/t/TeferisProtection.java index b9ae7df609..98701104c9 100644 --- a/Mage.Sets/src/mage/cards/t/TeferisProtection.java +++ b/Mage.Sets/src/mage/cards/t/TeferisProtection.java @@ -27,11 +27,14 @@ */ package mage.cards.t; +import java.util.ArrayList; +import java.util.List; import java.util.UUID; import mage.MageObject; import mage.abilities.Ability; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.ExileSpellEffect; +import mage.abilities.effects.common.PhaseOutAllEffect; import mage.abilities.effects.common.continuous.GainAbilityControllerEffect; import mage.abilities.effects.common.continuous.LifeTotalCantChangeControllerEffect; import mage.abilities.keyword.ProtectionAbility; @@ -170,14 +173,11 @@ class TeferisProtectionPhaseOutEffect extends OneShotEffect { public boolean apply(Game game, Ability source) { Player controller = game.getPlayer(source.getControllerId()); if (controller != null) { + List permIds = new ArrayList<>(); for (Permanent permanent : game.getBattlefield().getActivePermanents(StaticFilters.FILTER_CONTROLLED_PERMANENT, controller.getId(), game)) { - Permanent attachedTo = game.getPermanent(permanent.getAttachedTo()); - // don't phase out auras directly if they're attached to your stuff - if (!(attachedTo != null && attachedTo.getControllerId().equals(controller.getId()))) { - permanent.phaseOut(game); - } + permIds.add(permanent.getId()); } - return true; + return new PhaseOutAllEffect(permIds).apply(game, source); } return false; } diff --git a/Mage.Sets/src/mage/cards/t/TeferisRealm.java b/Mage.Sets/src/mage/cards/t/TeferisRealm.java index d74b69fcb1..da7adaa853 100644 --- a/Mage.Sets/src/mage/cards/t/TeferisRealm.java +++ b/Mage.Sets/src/mage/cards/t/TeferisRealm.java @@ -27,6 +27,7 @@ */ package mage.cards.t; +import java.util.ArrayList; import mage.abilities.Ability; import mage.abilities.common.BeginningOfUpkeepTriggeredAbility; import mage.abilities.effects.OneShotEffect; @@ -45,8 +46,11 @@ import mage.game.permanent.Permanent; import mage.players.Player; import java.util.HashSet; +import java.util.List; import java.util.Set; import java.util.UUID; +import mage.abilities.effects.common.PhaseOutAllEffect; +import mage.filter.common.FilterControlledLandPermanent; /** * @@ -55,7 +59,7 @@ import java.util.UUID; public class TeferisRealm extends CardImpl { public TeferisRealm(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.ENCHANTMENT},"{1}{U}{U}"); + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{U}{U}"); addSuperType(SuperType.WORLD); // At the beginning of each player's upkeep, that player chooses artifact, creature, land, or non-Aura enchantment. All nontoken permanents of that type phase out. @@ -135,10 +139,11 @@ class TeferisRealmEffect extends OneShotEffect { return false; } game.informPlayers(player.getLogName() + " chooses " + choosenType + "s to phase out"); + List permIds = new ArrayList<>(); for (Permanent permanent : game.getBattlefield().getActivePermanents(filter, controller.getId(), game)) { - permanent.phaseOut(game); + permIds.add(permanent.getId()); } - return true; + return new PhaseOutAllEffect(permIds).apply(game, source); } return false; } diff --git a/Mage/src/main/java/mage/abilities/effects/common/PhaseOutAllEffect.java b/Mage/src/main/java/mage/abilities/effects/common/PhaseOutAllEffect.java new file mode 100644 index 0000000000..0ac346eda2 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/effects/common/PhaseOutAllEffect.java @@ -0,0 +1,85 @@ +/* + * 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; + +import java.util.List; +import java.util.UUID; +import mage.abilities.Ability; +import mage.abilities.effects.OneShotEffect; +import mage.constants.Outcome; +import mage.game.Game; +import mage.game.permanent.Permanent; + +/** + * This class should only be used within the application of another effect + * + * @author TheElk801 + */ +public class PhaseOutAllEffect extends OneShotEffect { + + private final List idList; + + public PhaseOutAllEffect(List idList) { + super(Outcome.Neutral); + this.idList = idList; + } + + public PhaseOutAllEffect(final PhaseOutAllEffect effect) { + super(effect); + this.idList = effect.idList; + } + + @Override + public PhaseOutAllEffect copy() { + return new PhaseOutAllEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + // First we phase out everything that isn't attached to anything + // Anything attached to these permanents will phase out indirectly + for (UUID permanentId : idList) { + Permanent permanent = game.getPermanent(permanentId); + if (permanent != null) { + Permanent attachedTo = game.getPermanent(permanent.getAttachedTo()); + if (attachedTo == null) { + permanent.phaseOut(game); + } + } + } + // Once this is done, we'll have permanents which are attached to something but haven't phased out + // These will be phased out directly + for (UUID permanentId : idList) { + Permanent permanent = game.getPermanent(permanentId); + if (permanent != null && permanent.isPhasedIn()) { + permanent.phaseOut(game); + } + } + return true; + } +} From 12f14ea4881ef20e99c8c0b364865946bafbb0d5 Mon Sep 17 00:00:00 2001 From: Jerek Wilson Date: Sun, 15 Oct 2017 11:17:23 -0400 Subject: [PATCH 06/18] JW-RhysticCave: Implemented Rhystic Cave --- .gitignore | 1 + .../target/maven-archiver/pom.properties | 2 +- Mage.Sets/src/mage/cards/r/RhysticCave.java | 143 ++++++++++++++++++ Mage.Sets/src/mage/sets/Prophecy.java | 9 +- 4 files changed, 150 insertions(+), 5 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/r/RhysticCave.java diff --git a/.gitignore b/.gitignore index c14e808037..be7dca92b9 100644 --- a/.gitignore +++ b/.gitignore @@ -132,3 +132,4 @@ Mage.Client/serverlist.txt client_secrets.json dependency-reduced-pom.xml +mage-bundle diff --git a/Mage.Server.Plugins/Mage.Game.PennyDreadfulCommanderFreeForAll/target/maven-archiver/pom.properties b/Mage.Server.Plugins/Mage.Game.PennyDreadfulCommanderFreeForAll/target/maven-archiver/pom.properties index 819e9c0418..fef104a7fb 100644 --- a/Mage.Server.Plugins/Mage.Game.PennyDreadfulCommanderFreeForAll/target/maven-archiver/pom.properties +++ b/Mage.Server.Plugins/Mage.Game.PennyDreadfulCommanderFreeForAll/target/maven-archiver/pom.properties @@ -1,5 +1,5 @@ #Generated by Maven -#Mon Oct 09 23:56:07 EDT 2017 +#Tue Oct 10 20:55:56 EDT 2017 version=1.4.26 groupId=org.mage artifactId=mage-game-pennydreadfulcommanderfreeforall diff --git a/Mage.Sets/src/mage/cards/r/RhysticCave.java b/Mage.Sets/src/mage/cards/r/RhysticCave.java new file mode 100644 index 0000000000..deebe871f6 --- /dev/null +++ b/Mage.Sets/src/mage/cards/r/RhysticCave.java @@ -0,0 +1,143 @@ +/* + * 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.r; + +import java.util.UUID; +import mage.Mana; +import mage.abilities.Ability; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.Effect; +import mage.abilities.effects.common.DoUnlessAnyPlayerPaysEffect; +import mage.abilities.effects.common.ManaEffect; +import mage.abilities.effects.common.continuous.BoostSourceEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.choices.ChoiceColor; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; + +/** + * + * @author anonymous + */ +public class RhysticCave extends CardImpl { + + public RhysticCave(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.LAND}, ""); + + // {tap}: Choose a color. Add one mana of that color to your mana pool unless any player pays {1}. + // Activate this ability only any time you could cast an instant. + this.addAbility(new SimpleActivatedAbility(Zone.BATTLEFIELD, + new DoUnlessAnyPlayerPaysEffect(new RhysticCaveManaEffect(),new GenericManaCost(1)), + new TapSourceCost())); + + } + + public RhysticCave(final RhysticCave card) { + super(card); + } + + @Override + public RhysticCave copy() { + return new RhysticCave(this); + } + + + class RhysticCaveManaEffect extends ManaEffect { + + private final Mana chosenMana; + + public RhysticCaveManaEffect() { + super(); + chosenMana = new Mana(); + this.staticText = "Choose a color. Add one mana of that color to your mana pool "; + } + + public RhysticCaveManaEffect(final RhysticCaveManaEffect effect) { + super(effect); + this.chosenMana = effect.chosenMana.copy(); + } + + @Override + public Mana getMana(Game game, Ability source) { + return null; + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + + if (controller != null) { + ChoiceColor choice = new ChoiceColor(); + choice.setMessage("Choose a color to add mana of that color"); + + if (controller.choose(outcome, choice, game)) { + if (choice.getChoice() != null) { + String color = choice.getChoice(); + switch (color) { + case "Red": + chosenMana.setRed(1); + break; + case "Blue": + chosenMana.setBlue(1); + break; + case "White": + chosenMana.setWhite(1); + break; + case "Black": + chosenMana.setBlack(1); + break; + case "Green": + chosenMana.setGreen(1); + break; + } + } + checkToFirePossibleEvents(chosenMana, game, source); + controller.getManaPool().addMana(chosenMana, game, source); + return true; + } + + } + + return false; + } + + @Override + public Effect copy() { + return new RhysticCaveManaEffect(this); + } + } + +} diff --git a/Mage.Sets/src/mage/sets/Prophecy.java b/Mage.Sets/src/mage/sets/Prophecy.java index 03b140387b..860e2043d4 100644 --- a/Mage.Sets/src/mage/sets/Prophecy.java +++ b/Mage.Sets/src/mage/sets/Prophecy.java @@ -90,7 +90,7 @@ public class Prophecy extends ExpansionSet { cards.add(new SetCardInfo("Flameshot", 90, Rarity.UNCOMMON, mage.cards.f.Flameshot.class)); cards.add(new SetCardInfo("Flowering Field", 9, Rarity.UNCOMMON, mage.cards.f.FloweringField.class)); cards.add(new SetCardInfo("Foil", 34, Rarity.UNCOMMON, mage.cards.f.Foil.class)); - cards.add(new SetCardInfo("Forgotten Harvest", 114, Rarity.RARE, mage.cards.f.ForgottenHarvest.class)); + cards.add(new SetCardInfo("Forgotten Harvest", 114, Rarity.RARE, mage.cards.f.ForgottenHarvest.class)); cards.add(new SetCardInfo("Greel's Caress", 67, Rarity.COMMON, mage.cards.g.GreelsCaress.class)); cards.add(new SetCardInfo("Greel, Mind Raker", 66, Rarity.RARE, mage.cards.g.GreelMindRaker.class)); cards.add(new SetCardInfo("Gulf Squid", 35, Rarity.COMMON, mage.cards.g.GulfSquid.class)); @@ -102,7 +102,7 @@ public class Prophecy extends ExpansionSet { cards.add(new SetCardInfo("Jolrael, Empress of Beasts", 115, Rarity.RARE, mage.cards.j.JolraelEmpressOfBeasts.class)); cards.add(new SetCardInfo("Jolrael's Favor", 116, Rarity.COMMON, mage.cards.j.JolraelsFavor.class)); cards.add(new SetCardInfo("Keldon Arsonist", 92, Rarity.UNCOMMON, mage.cards.k.KeldonArsonist.class)); - cards.add(new SetCardInfo("Keldon Berserker", 93, Rarity.COMMON, mage.cards.k.KeldonBerserker.class)); + cards.add(new SetCardInfo("Keldon Berserker", 93, Rarity.COMMON, mage.cards.k.KeldonBerserker.class)); cards.add(new SetCardInfo("Keldon Firebombers", 94, Rarity.RARE, mage.cards.k.KeldonFirebombers.class)); cards.add(new SetCardInfo("Latulla, Keldon Overseer", 95, Rarity.RARE, mage.cards.l.LatullaKeldonOverseer.class)); cards.add(new SetCardInfo("Lesser Gargadon", 97, Rarity.UNCOMMON, mage.cards.l.LesserGargadon.class)); @@ -128,6 +128,7 @@ public class Prophecy extends ExpansionSet { cards.add(new SetCardInfo("Rebel Informer", 75, Rarity.RARE, mage.cards.r.RebelInformer.class)); cards.add(new SetCardInfo("Rethink", 42, Rarity.COMMON, mage.cards.r.Rethink.class)); cards.add(new SetCardInfo("Reveille Squad", 18, Rarity.UNCOMMON, mage.cards.r.ReveilleSquad.class)); + cards.add(new SetCardInfo("Rhystic Cave", 142, Rarity.UNCOMMON, mage.cards.r.RhysticCave.class)); cards.add(new SetCardInfo("Rhystic Circle", 19, Rarity.COMMON, mage.cards.r.RhysticCircle.class)); cards.add(new SetCardInfo("Rhystic Study", 45, Rarity.COMMON, mage.cards.r.RhysticStudy.class)); cards.add(new SetCardInfo("Rhystic Tutor", 77, Rarity.RARE, mage.cards.r.RhysticTutor.class)); @@ -139,7 +140,7 @@ public class Prophecy extends ExpansionSet { cards.add(new SetCardInfo("Searing Wind", 103, Rarity.RARE, mage.cards.s.SearingWind.class)); cards.add(new SetCardInfo("Shield Dancer", 23, Rarity.UNCOMMON, mage.cards.s.ShieldDancer.class)); cards.add(new SetCardInfo("Silt Crawler", 123, Rarity.COMMON, mage.cards.s.SiltCrawler.class)); - cards.add(new SetCardInfo("Snag", 124, Rarity.UNCOMMON, mage.cards.s.Snag.class)); + cards.add(new SetCardInfo("Snag", 124, Rarity.UNCOMMON, mage.cards.s.Snag.class)); cards.add(new SetCardInfo("Spiketail Drake", 48, Rarity.UNCOMMON, mage.cards.s.SpiketailDrake.class)); cards.add(new SetCardInfo("Spiketail Hatchling", 49, Rarity.COMMON, mage.cards.s.SpiketailHatchling.class)); cards.add(new SetCardInfo("Spitting Spider", 125, Rarity.UNCOMMON, mage.cards.s.SpittingSpider.class)); @@ -163,7 +164,7 @@ public class Prophecy extends ExpansionSet { cards.add(new SetCardInfo("Whip Sergeant", 107, Rarity.UNCOMMON, mage.cards.w.WhipSergeant.class)); cards.add(new SetCardInfo("Whipstitched Zombie", 81, Rarity.COMMON, mage.cards.w.WhipstitchedZombie.class)); cards.add(new SetCardInfo("Wild Might", 134, Rarity.COMMON, mage.cards.w.WildMight.class)); - cards.add(new SetCardInfo("Windscouter", 53, Rarity.UNCOMMON, mage.cards.w.Windscouter.class)); + cards.add(new SetCardInfo("Windscouter", 53, Rarity.UNCOMMON, mage.cards.w.Windscouter.class)); cards.add(new SetCardInfo("Wintermoon Mesa", 143, Rarity.RARE, mage.cards.w.WintermoonMesa.class)); cards.add(new SetCardInfo("Withdraw", 54, Rarity.COMMON, mage.cards.w.Withdraw.class)); cards.add(new SetCardInfo("Zerapa Minotaur", 108, Rarity.COMMON, mage.cards.z.ZerapaMinotaur.class)); From be7d23d15ccc5f189360183fc153daa7298c099c Mon Sep 17 00:00:00 2001 From: Jerek Wilson Date: Wed, 18 Oct 2017 23:53:04 -0400 Subject: [PATCH 07/18] JW-Dead-Iron_Sledge: Implemented Dead-Iron Sledge --- .../target/maven-archiver/pom.properties | 2 +- .../src/mage/cards/d/DeadIronSledge.java | 202 ++++++++++++++++++ Mage.Sets/src/mage/sets/Mirrodin.java | 1 + 3 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 Mage.Sets/src/mage/cards/d/DeadIronSledge.java diff --git a/Mage.Server.Plugins/Mage.Game.PennyDreadfulCommanderFreeForAll/target/maven-archiver/pom.properties b/Mage.Server.Plugins/Mage.Game.PennyDreadfulCommanderFreeForAll/target/maven-archiver/pom.properties index fef104a7fb..a617eb7af6 100644 --- a/Mage.Server.Plugins/Mage.Game.PennyDreadfulCommanderFreeForAll/target/maven-archiver/pom.properties +++ b/Mage.Server.Plugins/Mage.Game.PennyDreadfulCommanderFreeForAll/target/maven-archiver/pom.properties @@ -1,5 +1,5 @@ #Generated by Maven -#Tue Oct 10 20:55:56 EDT 2017 +#Wed Oct 18 23:39:20 EDT 2017 version=1.4.26 groupId=org.mage artifactId=mage-game-pennydreadfulcommanderfreeforall diff --git a/Mage.Sets/src/mage/cards/d/DeadIronSledge.java b/Mage.Sets/src/mage/cards/d/DeadIronSledge.java new file mode 100644 index 0000000000..11b2792013 --- /dev/null +++ b/Mage.Sets/src/mage/cards/d/DeadIronSledge.java @@ -0,0 +1,202 @@ +/* + * 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.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import mage.abilities.Ability; +import mage.abilities.TriggeredAbility; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.BlocksAttachedTriggeredAbility; +import mage.abilities.common.BlocksOrBecomesBlockedTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.effects.Effect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.DestroyAllEffect; +import mage.abilities.effects.common.DestroySourceEffect; +import mage.abilities.effects.common.DestroyTargetEffect; +import mage.abilities.effects.common.continuous.GainAbilityAttachedEffect; +import mage.abilities.keyword.EquipAbility; +import mage.abilities.keyword.FirstStrikeAbility; +import mage.abilities.keyword.TrampleAbility; +import mage.constants.SubType; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.AttachmentType; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.constants.Zone; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.permanent.BlockedByIdPredicate; +import mage.filter.predicate.permanent.BlockingAttackerIdPredicate; +import mage.filter.predicate.permanent.PermanentIdPredicate; +import mage.game.Game; +import mage.game.combat.CombatGroup; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.common.TargetCreaturePermanent; +import mage.target.targetpointer.FirstTargetPointer; +import mage.target.targetpointer.FixedTarget; +import mage.target.targetpointer.FixedTargets; +import mage.util.CardUtil; + +/** + * + * @author jerekwilson + */ +public class DeadIronSledge extends CardImpl { + + public DeadIronSledge(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{1}"); + + this.subtype.add(SubType.EQUIPMENT); + + // Whenever equipped creature blocks or becomes blocked by a creature, destroy both creatures. + this.addAbility(new DeadIronSledgeTriggeredAbility()); + + + // Equip {2} + this.addAbility(new EquipAbility(Outcome.AddAbility, new GenericManaCost(2))); + } + + public DeadIronSledge(final DeadIronSledge card) { + super(card); + } + + @Override + public DeadIronSledge copy() { + return new DeadIronSledge(this); + } + +} +class DeadIronSledgeTriggeredAbility extends TriggeredAbilityImpl { + + private Set possibleTargets = new HashSet<>(); + + DeadIronSledgeTriggeredAbility() { + super(Zone.BATTLEFIELD, new DeadIronSledgeDestroyEffect(), false); + } + + DeadIronSledgeTriggeredAbility(final DeadIronSledgeTriggeredAbility ability) { + super(ability); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.DECLARED_BLOCKERS; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + List targetPermanents = new ArrayList<>(); + Permanent equipment = game.getPermanentOrLKIBattlefield((this.getSourceId())); + if (equipment != null && equipment.getAttachedTo() != null) { + Permanent equippedPermanent = game.getPermanentOrLKIBattlefield((equipment.getAttachedTo())); + if (equippedPermanent != null) { + possibleTargets.clear(); + if(equippedPermanent.isBlocked(game)){ + possibleTargets.add(equippedPermanent.getId()); //add equipped creature to target list + } + String targetName = ""; + if (equippedPermanent.isAttacking()) { + for (CombatGroup group : game.getCombat().getGroups()) { + if (group.getAttackers().contains(equippedPermanent.getId())) { + possibleTargets.addAll(group.getBlockers()); + } + } + targetName = "a creature blocking attacker "; + } else if (equippedPermanent.getBlocking() > 0) { + for (CombatGroup group : game.getCombat().getGroups()) { + if (group.getBlockers().contains(equippedPermanent.getId())) { + possibleTargets.addAll(group.getAttackers()); + } + } + targetName = "a creature blocked by creature "; + } + if (!possibleTargets.isEmpty()) { + this.getTargets().clear(); + + for (UUID creatureId : possibleTargets) { + Permanent target = game.getPermanentOrLKIBattlefield(creatureId); + targetPermanents.add(target); + } + + this.getEffects().get(0).setTargetPointer(new FixedTargets(targetPermanents,game)); + + return true; + } + } + } + return false; + } + + @Override + public TriggeredAbility copy() { + return new DeadIronSledgeTriggeredAbility(this); + } + + @Override + public String getRule() { + return "Whenever equipped creature blocks or becomes blocked by a creature, destroy both creatures."; + } +} + +class DeadIronSledgeDestroyEffect extends OneShotEffect { + + public DeadIronSledgeDestroyEffect() { + super(Outcome.DestroyPermanent); + this.staticText = "destroy both creatures"; + } + + public DeadIronSledgeDestroyEffect(final DeadIronSledgeDestroyEffect effect) { + super(effect); + } + + @Override + public DeadIronSledgeDestroyEffect copy() { + return new DeadIronSledgeDestroyEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + + List targets = this.getTargetPointer().getTargets(game, source); + for(UUID target: targets){ + Permanent permanent = game.getPermanentOrLKIBattlefield(target); + permanent.destroy(target, game, false); + } + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/Mirrodin.java b/Mage.Sets/src/mage/sets/Mirrodin.java index 2348a680ac..3ed231d5a8 100644 --- a/Mage.Sets/src/mage/sets/Mirrodin.java +++ b/Mage.Sets/src/mage/sets/Mirrodin.java @@ -68,6 +68,7 @@ public class Mirrodin extends ExpansionSet { cards.add(new SetCardInfo("Crystal Shard", 159, Rarity.UNCOMMON, mage.cards.c.CrystalShard.class)); cards.add(new SetCardInfo("Culling Scales", 160, Rarity.RARE, mage.cards.c.CullingScales.class)); cards.add(new SetCardInfo("Damping Matrix", 161, Rarity.RARE, mage.cards.d.DampingMatrix.class)); + cards.add(new SetCardInfo("Dead-Iron Sledge", 162, Rarity.UNCOMMON, mage.cards.d.DeadIronSledge.class)); cards.add(new SetCardInfo("Deconstruct", 118, Rarity.COMMON, mage.cards.d.Deconstruct.class)); cards.add(new SetCardInfo("Detonate", 88, Rarity.UNCOMMON, mage.cards.d.Detonate.class)); cards.add(new SetCardInfo("Disarm", 32, Rarity.COMMON, mage.cards.d.Disarm.class)); From 58d3fc2328ae753845cc8dfcbabe429d78fbdd7f Mon Sep 17 00:00:00 2001 From: LevelX2 Date: Sat, 21 Oct 2017 16:13:45 +0200 Subject: [PATCH 08/18] Fixed player leaving/conceding handling. --- .../src/mage/player/ai/ComputerPlayer6.java | 8 +- .../src/mage/player/ai/ComputerPlayer7.java | 10 +- .../mage/player/ai/GameStateEvaluator2.java | 2 +- .../player/ai/simulators/ActionSimulator.java | 2 +- .../src/mage/player/ai/MCTSNode.java | 6 +- .../src/mage/player/ai/ComputerPlayer2.java | 8 +- .../src/mage/player/ai/ComputerPlayer3.java | 12 +- .../mage/player/ai/GameStateEvaluator.java | 2 +- .../src/mage/player/human/HumanPlayer.java | 36 +- .../src/mage/player/human/PlayerResponse.java | 17 +- .../src/mage/cards/w/WatertrapWeaver.java | 2 +- .../multiplayer/PlayerLeftGameRange1Test.java | 746 +++++++++--------- .../java/org/mage/test/player/TestPlayer.java | 7 + .../java/org/mage/test/stub/PlayerStub.java | 10 +- Mage/src/main/java/mage/game/Game.java | 4 +- Mage/src/main/java/mage/game/GameImpl.java | 99 ++- .../main/java/mage/game/combat/Combat.java | 4 +- Mage/src/main/java/mage/game/turn/Phase.java | 20 +- Mage/src/main/java/mage/game/turn/Turn.java | 6 +- Mage/src/main/java/mage/players/Player.java | 2 + .../main/java/mage/players/PlayerImpl.java | 13 +- 21 files changed, 553 insertions(+), 463 deletions(-) diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java index db6da80b74..f281d360e2 100644 --- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java +++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java @@ -519,7 +519,7 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ { logger.trace("interrupted - " + val); return val; } - if (depth <= 0 || SimulationNode2.nodeCount > maxNodes || game.gameOver(null)) { + if (depth <= 0 || SimulationNode2.nodeCount > maxNodes || game.checkIfGameIsOver()) { logger.trace("Add actions -- reached end state, node count=" + SimulationNode2.nodeCount + ", depth=" + depth); val = GameStateEvaluator2.evaluate(playerId, game); UUID currentPlayerId = node.getGame().getPlayerList().get(); @@ -540,7 +540,7 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ { } } - if (game.gameOver(null)) { + if (game.checkIfGameIsOver()) { val = GameStateEvaluator2.evaluate(playerId, game); } else if (!node.getChildren().isEmpty()) { //declared attackers or blockers or triggered abilities @@ -588,7 +588,7 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ { logger.debug("Sim Prio [" + depth + "] -- repeated action: " + action.toString()); continue; } - if (!sim.gameOver(null) && action.isUsesStack()) { + if (!sim.checkIfGameIsOver() && action.isUsesStack()) { // only pass if the last action uses the stack UUID nextPlayerId = sim.getPlayerList().get(); do { @@ -864,7 +864,7 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ { break; case CLEANUP: game.getPhase().getStep().beginStep(game, activePlayerId); - if (!game.checkStateAndTriggered() && !game.gameOver(null)) { + if (!game.checkStateAndTriggered() && !game.checkIfGameIsOver()) { game.getState().setActivePlayerId(game.getState().getPlayerList(game.getActivePlayerId()).getNext()); game.getTurn().setPhase(new BeginningPhase()); game.getPhase().setStep(new UntapStep()); diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer7.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer7.java index 99bce93ce8..48f46b53f5 100644 --- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer7.java +++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer7.java @@ -233,7 +233,7 @@ public class ComputerPlayer7 extends ComputerPlayer6 { return GameStateEvaluator2.evaluate(playerId, game); } // Condition to stop deeper simulation - if (depth <= 0 || SimulationNode2.nodeCount > maxNodes || game.gameOver(null)) { + if (depth <= 0 || SimulationNode2.nodeCount > maxNodes || game.checkIfGameIsOver()) { val = GameStateEvaluator2.evaluate(playerId, game); if (logger.isTraceEnabled()) { StringBuilder sb = new StringBuilder("Add Actions -- reached end state <").append(val).append('>'); @@ -267,7 +267,7 @@ public class ComputerPlayer7 extends ComputerPlayer6 { } } - if (game.gameOver(null)) { + if (game.checkIfGameIsOver()) { val = GameStateEvaluator2.evaluate(playerId, game); } else if (stepFinished) { logger.debug("Step finished"); @@ -481,7 +481,7 @@ public class ComputerPlayer7 extends ComputerPlayer6 { sim.fireEvent(GameEvent.getEvent(GameEvent.EventType.DECLARE_BLOCKERS_STEP_POST, sim.getActivePlayerId(), sim.getActivePlayerId())); Combat simCombat = sim.getCombat().copy(); finishCombat(sim); - if (sim.gameOver(null)) { + if (sim.checkIfGameIsOver()) { val = GameStateEvaluator2.evaluate(playerId, sim); } else if (!counter) { val = simulatePostCombatMain(sim, newNode, depth - 1, alpha, beta); @@ -549,7 +549,7 @@ public class ComputerPlayer7 extends ComputerPlayer6 { logger.debug("interrupted"); return; } - if (!game.gameOver(null)) { + if (!game.checkIfGameIsOver()) { game.getPhase().setStep(step); if (!step.skipStep(game, game.getActivePlayerId())) { step.beginStep(game, game.getActivePlayerId()); @@ -598,7 +598,7 @@ public class ComputerPlayer7 extends ComputerPlayer6 { logger.debug("interrupted"); return; } - if (!game.gameOver(null)) { + if (!game.checkIfGameIsOver()) { game.getTurn().getPhase().endPhase(game, game.getActivePlayerId()); game.getTurn().setPhase(new EndPhase()); if (game.getTurn().getPhase().beginPhase(game, game.getActivePlayerId())) { diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/GameStateEvaluator2.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/GameStateEvaluator2.java index 09f8b29123..580f47ba32 100644 --- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/GameStateEvaluator2.java +++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/GameStateEvaluator2.java @@ -33,7 +33,7 @@ public final class GameStateEvaluator2 { public static int evaluate(UUID playerId, Game game) { Player player = game.getPlayer(playerId); Player opponent = game.getPlayer(game.getOpponents(playerId).iterator().next()); - if (game.gameOver(null)) { + if (game.checkIfGameIsOver()) { if (player.hasLost() || opponent.hasWon()) { return LOSE_GAME_SCORE; } diff --git a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/simulators/ActionSimulator.java b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/simulators/ActionSimulator.java index b18c34dec5..e466891d3e 100644 --- a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/simulators/ActionSimulator.java +++ b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/simulators/ActionSimulator.java @@ -61,7 +61,7 @@ public class ActionSimulator { public int evaluateState() { Player opponent = game.getPlayer(game.getOpponents(player.getId()).iterator().next()); - if (game.gameOver(null)) { + if (game.checkIfGameIsOver()) { if (player.hasLost() || opponent.hasWon()) { return Integer.MIN_VALUE; } diff --git a/Mage.Server.Plugins/Mage.Player.AIMCTS/src/mage/player/ai/MCTSNode.java b/Mage.Server.Plugins/Mage.Player.AIMCTS/src/mage/player/ai/MCTSNode.java index 7eef75f10e..86a6096798 100644 --- a/Mage.Server.Plugins/Mage.Player.AIMCTS/src/mage/player/ai/MCTSNode.java +++ b/Mage.Server.Plugins/Mage.Player.AIMCTS/src/mage/player/ai/MCTSNode.java @@ -79,7 +79,7 @@ public class MCTSNode { this.game = game; this.stateValue = game.getState().getValue(game, targetPlayer); this.fullStateValue = game.getState().getValue(true, game); - this.terminal = game.gameOver(null); + this.terminal = game.checkIfGameIsOver(); setPlayer(); nodeCount = 1; // logger.info(this.stateValue); @@ -90,7 +90,7 @@ public class MCTSNode { this.game = game; this.stateValue = game.getState().getValue(game, targetPlayer); this.fullStateValue = game.getState().getValue(true, game); - this.terminal = game.gameOver(null); + this.terminal = game.checkIfGameIsOver(); this.parent = parent; this.action = action; setPlayer(); @@ -104,7 +104,7 @@ public class MCTSNode { this.combat = combat; this.stateValue = game.getState().getValue(game, targetPlayer); this.fullStateValue = game.getState().getValue(true, game); - this.terminal = game.gameOver(null); + this.terminal = game.checkIfGameIsOver(); this.parent = parent; setPlayer(); nodeCount++; diff --git a/Mage.Server.Plugins/Mage.Player.AIMinimax/src/mage/player/ai/ComputerPlayer2.java b/Mage.Server.Plugins/Mage.Player.AIMinimax/src/mage/player/ai/ComputerPlayer2.java index 681fcac0fb..fa86b2205a 100644 --- a/Mage.Server.Plugins/Mage.Player.AIMinimax/src/mage/player/ai/ComputerPlayer2.java +++ b/Mage.Server.Plugins/Mage.Player.AIMinimax/src/mage/player/ai/ComputerPlayer2.java @@ -330,7 +330,7 @@ public class ComputerPlayer2 extends ComputerPlayer implements Player { return GameStateEvaluator.evaluate(playerId, game); } int val; - if (node.depth > maxDepth || game.gameOver(null)) { + if (node.depth > maxDepth || game.checkIfGameIsOver()) { logger.debug(indent(node.depth) + "simulating -- reached end state"); val = GameStateEvaluator.evaluate(playerId, game); } @@ -357,7 +357,7 @@ public class ComputerPlayer2 extends ComputerPlayer implements Player { } } - if (game.gameOver(null)) { + if (game.checkIfGameIsOver()) { val = GameStateEvaluator.evaluate(playerId, game); } else if (!node.getChildren().isEmpty()) { @@ -403,7 +403,7 @@ public class ComputerPlayer2 extends ComputerPlayer implements Player { logger.debug(indent(node.depth) + "found useless action: " + action); continue; } - if (!sim.gameOver(null) && action.isUsesStack()) { + if (!sim.checkIfGameIsOver() && action.isUsesStack()) { // only pass if the last action uses the stack sim.getPlayer(currentPlayer.getId()).pass(game); sim.getPlayerList().getNext(); @@ -588,7 +588,7 @@ public class ComputerPlayer2 extends ComputerPlayer implements Player { break; case CLEANUP: game.getPhase().getStep().beginStep(game, activePlayerId); - if (!game.checkStateAndTriggered() && !game.gameOver(null)) { + if (!game.checkStateAndTriggered() && !game.checkIfGameIsOver()) { game.getState().setActivePlayerId(game.getState().getPlayerList(game.getActivePlayerId()).getNext()); game.getTurn().setPhase(new BeginningPhase()); game.getPhase().setStep(new UntapStep()); diff --git a/Mage.Server.Plugins/Mage.Player.AIMinimax/src/mage/player/ai/ComputerPlayer3.java b/Mage.Server.Plugins/Mage.Player.AIMinimax/src/mage/player/ai/ComputerPlayer3.java index b523360807..ec7c6f409d 100644 --- a/Mage.Server.Plugins/Mage.Player.AIMinimax/src/mage/player/ai/ComputerPlayer3.java +++ b/Mage.Server.Plugins/Mage.Player.AIMinimax/src/mage/player/ai/ComputerPlayer3.java @@ -184,7 +184,7 @@ public class ComputerPlayer3 extends ComputerPlayer2 implements Player { logger.debug(indent(node.depth) + "interrupted"); return GameStateEvaluator.evaluate(playerId, game); } - if (node.depth > maxDepth || game.gameOver(null)) { + if (node.depth > maxDepth || game.checkIfGameIsOver()) { logger.debug(indent(node.depth) + "simulating -- reached end state"); val = GameStateEvaluator.evaluate(playerId, game); } @@ -204,7 +204,7 @@ public class ComputerPlayer3 extends ComputerPlayer2 implements Player { } } - if (game.gameOver(null)) { + if (game.checkIfGameIsOver()) { val = GameStateEvaluator.evaluate(playerId, game); } else if (stepFinished) { @@ -408,7 +408,7 @@ public class ComputerPlayer3 extends ComputerPlayer2 implements Player { sim.fireEvent(GameEvent.getEvent(GameEvent.EventType.DECLARE_BLOCKERS_STEP_POST, sim.getActivePlayerId(), sim.getActivePlayerId())); Combat simCombat = sim.getCombat().copy(); finishCombat(sim); - if (sim.gameOver(null)) { + if (sim.checkIfGameIsOver()) { val = GameStateEvaluator.evaluate(playerId, sim); } else if (!counter) { @@ -450,7 +450,7 @@ public class ComputerPlayer3 extends ComputerPlayer2 implements Player { return GameStateEvaluator.evaluate(playerId, game); } Integer val = null; - if (!game.gameOver(null)) { + if (!game.checkIfGameIsOver()) { logger.debug(indent(node.depth) + "simulating -- ending turn"); simulateToEnd(game); game.getState().setActivePlayerId(game.getState().getPlayerList(game.getActivePlayerId()).getNext()); @@ -478,7 +478,7 @@ public class ComputerPlayer3 extends ComputerPlayer2 implements Player { logger.debug("interrupted"); return; } - if (!game.gameOver(null)) { + if (!game.checkIfGameIsOver()) { game.getPhase().setStep(step); if (!step.skipStep(game, game.getActivePlayerId())) { step.beginStep(game, game.getActivePlayerId()); @@ -526,7 +526,7 @@ public class ComputerPlayer3 extends ComputerPlayer2 implements Player { logger.debug("interrupted"); return; } - if (!game.gameOver(null)) { + if (!game.checkIfGameIsOver()) { game.getTurn().getPhase().endPhase(game, game.getActivePlayerId()); game.getTurn().setPhase(new EndPhase()); if (game.getTurn().getPhase().beginPhase(game, game.getActivePlayerId())) { diff --git a/Mage.Server.Plugins/Mage.Player.AIMinimax/src/mage/player/ai/GameStateEvaluator.java b/Mage.Server.Plugins/Mage.Player.AIMinimax/src/mage/player/ai/GameStateEvaluator.java index e45943dad8..df9e389d03 100644 --- a/Mage.Server.Plugins/Mage.Player.AIMinimax/src/mage/player/ai/GameStateEvaluator.java +++ b/Mage.Server.Plugins/Mage.Player.AIMinimax/src/mage/player/ai/GameStateEvaluator.java @@ -70,7 +70,7 @@ public final class GameStateEvaluator { public static int evaluate(UUID playerId, Game game, boolean ignoreTapped) { Player player = game.getPlayer(playerId); Player opponent = game.getPlayer(game.getOpponents(playerId).iterator().next()); - if (game.gameOver(null)) { + if (game.checkIfGameIsOver()) { if (player.hasLost() || opponent.hasWon()) return LOSE_SCORE; if (opponent.hasLost() || player.hasWon()) diff --git a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java index 1e934e1915..06a4778646 100644 --- a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java +++ b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java @@ -53,6 +53,7 @@ import mage.filter.common.FilterCreatureForCombat; import mage.filter.common.FilterCreatureForCombatBlock; import mage.filter.predicate.permanent.ControllerIdPredicate; import mage.game.Game; +import mage.game.GameImpl; import mage.game.combat.CombatGroup; import mage.game.draft.Draft; import mage.game.events.GameEvent; @@ -186,13 +187,25 @@ public class HumanPlayer extends PlayerImpl { response.clear(); logger.debug("Waiting response from player: " + getId()); game.resumeTimer(getTurnControlledBy()); - synchronized (response) { - try { - response.wait(); - } catch (InterruptedException ex) { - logger.error("Response error for player " + getName() + " gameId: " + game.getId(), ex); - } finally { - game.pauseTimer(getTurnControlledBy()); + boolean loop = true; + while (loop) { + loop = false; + synchronized (response) { + try { + response.wait(); + } catch (InterruptedException ex) { + logger.error("Response error for player " + getName() + " gameId: " + game.getId(), ex); + } finally { + game.pauseTimer(getTurnControlledBy()); + } + } + if (response.getResponseConcedeCheck()) { + ((GameImpl) game).checkConcede(); + if (game.hasEnded()) { + return; + } + response.clear(); + loop = true; } } if (recordingMacro && !macroTriggeredSelectionFlag) { @@ -1706,6 +1719,15 @@ public class HumanPlayer extends PlayerImpl { } } + @Override + public void signalPlayerConcede() { + synchronized (response) { + response.setResponseConcedeCheck(); + response.notifyAll(); + logger.debug("Set check concede for waiting player: " + getId()); + } + } + @Override public void skip() { synchronized (response) { diff --git a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/PlayerResponse.java b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/PlayerResponse.java index 2f1e5871c2..b4e7b063bd 100644 --- a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/PlayerResponse.java +++ b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/PlayerResponse.java @@ -43,6 +43,7 @@ public class PlayerResponse implements Serializable { private Integer responseInteger; private ManaType responseManaType; private UUID responseManaTypePlayerId; + private Boolean responseConcedeCheck; public PlayerResponse() { clear(); @@ -55,7 +56,8 @@ public class PlayerResponse implements Serializable { + ',' + responseBoolean + ',' + responseInteger + ',' + responseManaType - + ',' + responseManaTypePlayerId; + + ',' + responseManaTypePlayerId + + ',' + responseConcedeCheck; } public PlayerResponse(PlayerResponse other) { @@ -69,6 +71,7 @@ public class PlayerResponse implements Serializable { responseInteger = other.responseInteger; responseManaType = other.responseManaType; responseManaTypePlayerId = other.responseManaTypePlayerId; + responseConcedeCheck = other.responseConcedeCheck; } public void clear() { @@ -78,6 +81,7 @@ public class PlayerResponse implements Serializable { responseInteger = null; responseManaType = null; responseManaTypePlayerId = null; + responseConcedeCheck = null; } public String getString() { @@ -104,6 +108,17 @@ public class PlayerResponse implements Serializable { this.responseBoolean = responseBoolean; } + public Boolean getResponseConcedeCheck() { + if (responseConcedeCheck == null) { + return false; + } + return responseConcedeCheck; + } + + public void setResponseConcedeCheck() { + this.responseConcedeCheck = true; + } + public Integer getInteger() { return responseInteger; } diff --git a/Mage.Sets/src/mage/cards/w/WatertrapWeaver.java b/Mage.Sets/src/mage/cards/w/WatertrapWeaver.java index 2820a85eee..703eb4ae9a 100644 --- a/Mage.Sets/src/mage/cards/w/WatertrapWeaver.java +++ b/Mage.Sets/src/mage/cards/w/WatertrapWeaver.java @@ -61,7 +61,7 @@ public class WatertrapWeaver extends CardImpl { this.power = new MageInt(2); this.toughness = new MageInt(2); - // When Watertrap Weaver enters the battlefield, tap target creature an opponent controls. That creature doesn't untap during its controller's next untap step. + // When Watertrap Weaver enters the battlefield, tap target creature an opponent controls. That creature doesn't untap during its controller's next untap step. EntersBattlefieldTriggeredAbility ability = new EntersBattlefieldTriggeredAbility(new TapTargetEffect()); ability.addEffect(new DontUntapInControllersNextUntapStepTargetEffect("that creature")); ability.addTarget(new TargetCreaturePermanent(filter)); diff --git a/Mage.Tests/src/test/java/org/mage/test/multiplayer/PlayerLeftGameRange1Test.java b/Mage.Tests/src/test/java/org/mage/test/multiplayer/PlayerLeftGameRange1Test.java index 69424f1425..1ac18334e5 100644 --- a/Mage.Tests/src/test/java/org/mage/test/multiplayer/PlayerLeftGameRange1Test.java +++ b/Mage.Tests/src/test/java/org/mage/test/multiplayer/PlayerLeftGameRange1Test.java @@ -1,373 +1,373 @@ -/* - * 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.multiplayer; - -import java.io.FileNotFoundException; -import mage.constants.MultiplayerAttackOption; -import mage.constants.PhaseStep; -import mage.constants.RangeOfInfluence; -import mage.constants.Zone; -import mage.counters.CounterType; -import mage.game.FreeForAll; -import mage.game.Game; -import mage.game.GameException; -import mage.game.permanent.Permanent; -import org.junit.Assert; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestMultiPlayerBase; - -/** - * - * @author LevelX2 - */ -public class PlayerLeftGameRange1Test extends CardTestMultiPlayerBase { - - @Override - protected Game createNewGameAndPlayers() throws GameException, FileNotFoundException { - // Start Life = 2 - Game game = new FreeForAll(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ONE, 0, 2); - // Player order: A -> D -> C -> B - playerA = createPlayer(game, playerA, "PlayerA"); - playerB = createPlayer(game, playerB, "PlayerB"); - playerC = createPlayer(game, playerC, "PlayerC"); - playerD = createPlayer(game, playerD, "PlayerD"); - return game; - } - - /** - * Tests Enchantment to control other permanent - */ - @Test - public void TestControlledByEnchantment() { - addCard(Zone.BATTLEFIELD, playerB, "Rootwater Commando"); - - addCard(Zone.BATTLEFIELD, playerA, "Island", 4); - // Enchant creature - // You control enchanted creature. - addCard(Zone.HAND, playerA, "Control Magic"); - - addCard(Zone.BATTLEFIELD, playerC, "Silvercoat Lion"); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Control Magic", "Rootwater Commando"); - - attack(3, playerC, "Silvercoat Lion", playerB); - - setStopAt(3, PhaseStep.POSTCOMBAT_MAIN); - execute(); - - assertLife(playerB, 0); - assertPermanentCount(playerB, 0); - assertPermanentCount(playerA, "Rootwater Commando", 0); - assertGraveyardCount(playerA, "Control Magic", 1); - - } - - /** - * Tests Sorcery to control other players permanent - */ - @Test - public void TestControlledBySorcery() { - addCard(Zone.BATTLEFIELD, playerB, "Rootwater Commando"); - - addCard(Zone.BATTLEFIELD, playerA, "Island", 4); - // Exchange control of target artifact or creature and another target permanent that shares one of those types with it. - // (This effect lasts indefinitely.) - addCard(Zone.HAND, playerA, "Legerdemain"); // Sorcery - addCard(Zone.BATTLEFIELD, playerA, "Wall of Air"); - - addCard(Zone.BATTLEFIELD, playerC, "Silvercoat Lion"); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Legerdemain", "Rootwater Commando^Wall of Air"); - - attack(3, playerC, "Silvercoat Lion", playerB); - - setStopAt(3, PhaseStep.POSTCOMBAT_MAIN); - execute(); - - assertLife(playerB, 0); - assertGraveyardCount(playerA, "Legerdemain", 1); - assertPermanentCount(playerB, 0); - assertPermanentCount(playerA, "Rootwater Commando", 0); // removed from game because player B left - assertPermanentCount(playerB, "Wall of Air", 0); - assertGraveyardCount(playerA, "Wall of Air", 0); - assertPermanentCount(playerA, "Wall of Air", 1); // Returned back to player A - - } - - /** - * Tests Instant to control other permanent - */ - @Test - public void TestOtherPlayerControllsCreature() { - addCard(Zone.BATTLEFIELD, playerB, "Rootwater Commando"); - - addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4); - // Untap target nonlegendary creature and gain control of it until end of turn. That creature gains haste until end of turn. - addCard(Zone.HAND, playerA, "Blind with Anger"); // Instant - - addCard(Zone.BATTLEFIELD, playerC, "Silvercoat Lion"); - - castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerA, "Blind with Anger", "Rootwater Commando"); - - attack(3, playerC, "Silvercoat Lion", playerB); - - setStopAt(3, PhaseStep.POSTCOMBAT_MAIN); - execute(); - - assertLife(playerB, 0); - assertGraveyardCount(playerA, "Blind with Anger", 1); - assertPermanentCount(playerB, 0); - assertPermanentCount(playerA, "Rootwater Commando", 0); // Removed from game because player C left - assertPermanentCount(playerA, "Rootwater Commando", 0); // Returned back to player A - } - - /** - * Xmage throws an error involving an emblem unable to find the initial - * source if it has a proc. To reproduce, a Planeswalker was taken from an - * original player's control, such as using Scrambleverse to shuffle Jace, - * Unraveler of Secrets, to a second player and then the second player uses - * Jace's ability to create an emblem ("Whenever an opponent casts his or - * her first spell each turn, counter that spell."). Then the original - * player concedes the game and removes the Planeswalker. Once it becomes an - * opponent of the original player's turn and that opponent plays a spell, - * Xmage throws an error and rollsback the turn. - * - * I don't have the actual error report on my due to negligence, but what I - * can recollect is that the error message was along the lines of "The - * emblem cannot find the original source. This turn will be rolled back". - * This error message will always appear when an opponent tries to play a - * spell. Player order: A -> D -> C -> B - */ - @Test - public void TestOtherPlayerPlaneswalkerCreatedEmblem() { - // +1: Scry 1, then draw a card. - // -2: Return target creature to its owner's hand. - // -8: You get an emblem with "Whenever an opponent casts his or her first spell each turn, counter that spell." - addCard(Zone.BATTLEFIELD, playerB, "Jace, Unraveler of Secrets"); - addCounters(1, PhaseStep.DRAW, playerB, "Jace, Unraveler of Secrets", CounterType.LOYALTY, 8); - - addCard(Zone.BATTLEFIELD, playerA, "Island", 6); - // Enchant permanent (Target a permanent as you cast this. This card enters the battlefield attached to that permanent.) - // You control enchanted permanent. - addCard(Zone.HAND, playerA, "Confiscate"); // Enchantment Aura - - addCard(Zone.BATTLEFIELD, playerC, "Plains", 2); - addCard(Zone.HAND, playerC, "Silvercoat Lion"); - - addCard(Zone.BATTLEFIELD, playerD, "Plains", 2); - addCard(Zone.HAND, playerD, "Silvercoat Lion"); - - addCard(Zone.BATTLEFIELD, playerC, "Silvercoat Lion"); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Confiscate", "Jace, Unraveler of Secrets"); - activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "-8: You get an emblem with"); - - castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerA, "Blind with Anger", "Rootwater Commando"); - - attack(3, playerC, "Silvercoat Lion", playerB); - castSpell(3, PhaseStep.POSTCOMBAT_MAIN, playerC, "Silvercoat Lion"); - - castSpell(5, PhaseStep.PRECOMBAT_MAIN, playerD, "Silvercoat Lion"); - - setStopAt(5, PhaseStep.END_TURN); - execute(); - - assertLife(playerB, 0); - assertPermanentCount(playerB, 0); - assertGraveyardCount(playerA, "Confiscate", 1); - assertPermanentCount(playerA, "Jace, Unraveler of Secrets", 0); // Removed from game because player C left the game - assertEmblemCount(playerA, 1); - assertPermanentCount(playerC, "Silvercoat Lion", 2); // Emblem does not work yet on player C, because range 1 - assertGraveyardCount(playerD, "Silvercoat Lion", 1); // Emblem should counter the spell - } - - /** - * Situation: I attacked an opponent with some creatures with True - * Conviction in play. There were multiple "deals combat damage to a - * player"-triggers (Edric, Spymaster of Trest, Daxos of Meletis et al), - * then the opponent lost the game during the first strike combat - * damage-step . In the second combat damage step the triggers went on the - * stack again, although there was no player being dealt damage (multiplayer - * game, so the game wasn't over yet). I don't think these abilities should - * trigger again here. - */ - @Test - public void TestPlayerDiesDuringFirstStrikeDamageStep() { - // Creatures you control have double strike and lifelink. - addCard(Zone.BATTLEFIELD, playerD, "True Conviction"); - // Whenever a creature deals combat damage to one of your opponents, its controller may draw a card. - addCard(Zone.BATTLEFIELD, playerD, "Edric, Spymaster of Trest"); - addCard(Zone.BATTLEFIELD, playerD, "Dross Crocodile", 8); // Creature 5/1 - - attack(2, playerD, "Dross Crocodile", playerC); - - setStopAt(3, PhaseStep.END_TURN); - execute(); - - assertLife(playerC, -3); - assertLife(playerD, 7); - - assertHandCount(playerD, 2); // 1 (normal draw) + 1 from True Convition - assertPermanentCount(playerC, 0); - - } - - /** - * I've encountered a case today where someone conceded on their turn. The - * remaining phases were went through as normal, but my Luminarch Ascension - * did not trigger during the end step. - */ - // Player order: A -> D -> C -> B - @Test - public void TestTurnEndTrigger() { - addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); - // At the beginning of each opponent's end step, if you didn't lose life this turn, you may put a quest counter on Luminarch Ascension. - // {1}{W}: Create a 4/4 white Angel creature token with flying. Activate this ability only if Luminarch Ascension has four or more quest counters on it.. - addCard(Zone.HAND, playerA, "Luminarch Ascension"); // Enchantment {1}{W} - - addCard(Zone.HAND, playerC, "Lightning Bolt"); - addCard(Zone.BATTLEFIELD, playerC, "Mountain", 1); - - addCard(Zone.HAND, playerD, "Silvercoat Lion"); - addCard(Zone.BATTLEFIELD, playerD, "Plains", 2); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Luminarch Ascension"); - - castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerD, "Silvercoat Lion"); - castSpell(2, PhaseStep.BEGIN_COMBAT, playerC, "Lightning Bolt", playerD); - - setStopAt(3, PhaseStep.PRECOMBAT_MAIN); - execute(); - - assertPermanentCount(playerA, "Luminarch Ascension", 1); - assertGraveyardCount(playerC, "Lightning Bolt", 1); - - assertLife(playerD, -1); - Assert.assertFalse("Player D is no longer in the game", playerD.isInGame()); - - assertCounterCount(playerA, "Luminarch Ascension", CounterType.QUEST, 1); // 1 from turn 2 - } - - @Test - public void TestTurnEndTriggerAfterConcede() { - addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); - // At the beginning of each opponent's end step, if you didn't lose life this turn, you may put a quest counter on Luminarch Ascension. - // {1}{W}: Create a 4/4 white Angel creature token with flying. Activate this ability only if Luminarch Ascension has four or more quest counters on it.. - addCard(Zone.HAND, playerA, "Luminarch Ascension"); // Enchantment {1}{W} - - addCard(Zone.HAND, playerD, "Silvercoat Lion"); - addCard(Zone.BATTLEFIELD, playerD, "Plains", 2); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Luminarch Ascension"); - - castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerD, "Silvercoat Lion"); - - concede(2, PhaseStep.BEGIN_COMBAT, playerD); - - setStopAt(3, PhaseStep.PRECOMBAT_MAIN); - execute(); - - assertPermanentCount(playerA, "Luminarch Ascension", 1); - - assertLife(playerD, 2); - Assert.assertFalse("Player D is no longer in the game", playerD.isInGame()); - - assertCounterCount(playerA, "Luminarch Ascension", CounterType.QUEST, 1); // 1 from turn 2 - } - - /** - * Pithing Needle keeps the named card's abilities disabled even after the - * player controlling the Needle loses the game. - * - * I saw it happen during a Commander game. A player cast Pithing Needle - * targeting my Proteus Staff. After I killed him, I still couldn't activate - * the Staff. - */ - @Test - public void TestPithingNeedle() { - addCard(Zone.BATTLEFIELD, playerA, "Plains", 1); - // As Pithing Needle enters the battlefield, name a card. - // Activated abilities of sources with the chosen name can't be activated unless they're mana abilities. - addCard(Zone.HAND, playerA, "Pithing Needle"); // Artifact {1} - addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion", 1); - addCard(Zone.LIBRARY, playerA, "Pillarfield Ox", 1); - - addCard(Zone.BATTLEFIELD, playerD, "Island", 3); - // {2}{U}, {T}: Put target creature on the bottom of its owner's library. That creature's controller reveals cards from the - // top of his or her library until he or she reveals a creature card. The player puts that card onto the battlefield and the - // rest on the bottom of his or her library in any order. Activate this ability only any time you could cast a sorcery. - addCard(Zone.BATTLEFIELD, playerD, "Proteus Staff", 1); - - addCard(Zone.BATTLEFIELD, playerD, "Eager Cadet", 1); - addCard(Zone.LIBRARY, playerD, "Storm Crow", 2); - - addCard(Zone.BATTLEFIELD, playerC, "Island", 3); - addCard(Zone.BATTLEFIELD, playerC, "Proteus Staff", 1); - addCard(Zone.BATTLEFIELD, playerC, "Wall of Air", 1); - addCard(Zone.LIBRARY, playerC, "Wind Drake", 2); - - addCard(Zone.BATTLEFIELD, playerB, "Island", 3); - addCard(Zone.BATTLEFIELD, playerB, "Proteus Staff", 1); - - skipInitShuffling(); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Pithing Needle"); - setChoice(playerA, "Proteus Staff"); - - activateAbility(2, PhaseStep.PRECOMBAT_MAIN, playerD, "{2}{U}", "Silvercoat Lion"); // not allowed - - activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerC, "{2}{U}", "Eager Cadet"); // allowed because Needle out of range - concede(3, PhaseStep.POSTCOMBAT_MAIN, playerA); - - activateAbility(4, PhaseStep.PRECOMBAT_MAIN, playerB, "{2}{U}", "Wall of Air"); // allowed because Needle lost game - - setStopAt(4, PhaseStep.POSTCOMBAT_MAIN); - execute(); - - assertPermanentCount(playerA, 0); - - assertLife(playerA, 2); - Assert.assertFalse("Player A is no longer in the game", playerA.isInGame()); - - Permanent staffPlayerD = getPermanent("Proteus Staff", playerD); - Assert.assertFalse("Staff of player D could not be used", staffPlayerD.isTapped()); - - assertPermanentCount(playerD, "Eager Cadet", 0); - assertPermanentCount(playerD, "Storm Crow", 1); - - Permanent staffPlayerC = getPermanent("Proteus Staff", playerC); - Assert.assertTrue("Staff of player C could be used", staffPlayerC.isTapped()); - - assertPermanentCount(playerC, "Wall of Air", 0); - assertPermanentCount(playerC, "Wind Drake", 1); - - Permanent staffPlayerB = getPermanent("Proteus Staff", playerB); - Assert.assertTrue("Staff of player B could be used", staffPlayerB.isTapped()); - - } -} +/* + * 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.multiplayer; + +import java.io.FileNotFoundException; +import mage.constants.MultiplayerAttackOption; +import mage.constants.PhaseStep; +import mage.constants.RangeOfInfluence; +import mage.constants.Zone; +import mage.counters.CounterType; +import mage.game.FreeForAll; +import mage.game.Game; +import mage.game.GameException; +import mage.game.permanent.Permanent; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestMultiPlayerBase; + +/** + * + * @author LevelX2 + */ +public class PlayerLeftGameRange1Test extends CardTestMultiPlayerBase { + + @Override + protected Game createNewGameAndPlayers() throws GameException, FileNotFoundException { + // Start Life = 2 + Game game = new FreeForAll(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ONE, 0, 2); + // Player order: A -> D -> C -> B + playerA = createPlayer(game, playerA, "PlayerA"); + playerB = createPlayer(game, playerB, "PlayerB"); + playerC = createPlayer(game, playerC, "PlayerC"); + playerD = createPlayer(game, playerD, "PlayerD"); + return game; + } + + /** + * Tests Enchantment to control other permanent + */ + @Test + public void TestControlledByEnchantment() { + addCard(Zone.BATTLEFIELD, playerB, "Rootwater Commando"); + + addCard(Zone.BATTLEFIELD, playerA, "Island", 4); + // Enchant creature + // You control enchanted creature. + addCard(Zone.HAND, playerA, "Control Magic"); + + addCard(Zone.BATTLEFIELD, playerC, "Silvercoat Lion"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Control Magic", "Rootwater Commando"); + + attack(3, playerC, "Silvercoat Lion", playerB); + + setStopAt(3, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertLife(playerB, 0); + assertPermanentCount(playerB, 0); + assertPermanentCount(playerA, "Rootwater Commando", 0); + assertGraveyardCount(playerA, "Control Magic", 1); + + } + + /** + * Tests Sorcery to control other players permanent + */ + @Test + public void TestControlledBySorcery() { + addCard(Zone.BATTLEFIELD, playerB, "Rootwater Commando"); + + addCard(Zone.BATTLEFIELD, playerA, "Island", 4); + // Exchange control of target artifact or creature and another target permanent that shares one of those types with it. + // (This effect lasts indefinitely.) + addCard(Zone.HAND, playerA, "Legerdemain"); // Sorcery + addCard(Zone.BATTLEFIELD, playerA, "Wall of Air"); + + addCard(Zone.BATTLEFIELD, playerC, "Silvercoat Lion"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Legerdemain", "Rootwater Commando^Wall of Air"); + + attack(3, playerC, "Silvercoat Lion", playerB); + + setStopAt(3, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertLife(playerB, 0); + assertGraveyardCount(playerA, "Legerdemain", 1); + assertPermanentCount(playerB, 0); + assertPermanentCount(playerA, "Rootwater Commando", 0); // removed from game because player B left + assertPermanentCount(playerB, "Wall of Air", 0); + assertGraveyardCount(playerA, "Wall of Air", 0); + assertPermanentCount(playerA, "Wall of Air", 1); // Returned back to player A + + } + + /** + * Tests Instant to control other permanent + */ + @Test + public void TestOtherPlayerControllsCreature() { + addCard(Zone.BATTLEFIELD, playerB, "Rootwater Commando"); + + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4); + // Untap target nonlegendary creature and gain control of it until end of turn. That creature gains haste until end of turn. + addCard(Zone.HAND, playerA, "Blind with Anger"); // Instant + + addCard(Zone.BATTLEFIELD, playerC, "Silvercoat Lion"); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerA, "Blind with Anger", "Rootwater Commando"); + + attack(3, playerC, "Silvercoat Lion", playerB); + + setStopAt(3, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertLife(playerB, 0); + assertGraveyardCount(playerA, "Blind with Anger", 1); + assertPermanentCount(playerB, 0); + assertPermanentCount(playerA, "Rootwater Commando", 0); // Removed from game because player C left + assertPermanentCount(playerA, "Rootwater Commando", 0); // Returned back to player A + } + + /** + * Xmage throws an error involving an emblem unable to find the initial + * source if it has a proc. To reproduce, a Planeswalker was taken from an + * original player's control, such as using Scrambleverse to shuffle Jace, + * Unraveler of Secrets, to a second player and then the second player uses + * Jace's ability to create an emblem ("Whenever an opponent casts his or + * her first spell each turn, counter that spell."). Then the original + * player concedes the game and removes the Planeswalker. Once it becomes an + * opponent of the original player's turn and that opponent plays a spell, + * Xmage throws an error and rollsback the turn. + * + * I don't have the actual error report on my due to negligence, but what I + * can recollect is that the error message was along the lines of "The + * emblem cannot find the original source. This turn will be rolled back". + * This error message will always appear when an opponent tries to play a + * spell. Player order: A -> D -> C -> B + */ + @Test + public void TestOtherPlayerPlaneswalkerCreatedEmblem() { + // +1: Scry 1, then draw a card. + // -2: Return target creature to its owner's hand. + // -8: You get an emblem with "Whenever an opponent casts his or her first spell each turn, counter that spell." + addCard(Zone.BATTLEFIELD, playerB, "Jace, Unraveler of Secrets"); + addCounters(1, PhaseStep.DRAW, playerB, "Jace, Unraveler of Secrets", CounterType.LOYALTY, 8); + + addCard(Zone.BATTLEFIELD, playerA, "Island", 6); + // Enchant permanent (Target a permanent as you cast this. This card enters the battlefield attached to that permanent.) + // You control enchanted permanent. + addCard(Zone.HAND, playerA, "Confiscate"); // Enchantment Aura + + addCard(Zone.BATTLEFIELD, playerC, "Plains", 2); + addCard(Zone.HAND, playerC, "Silvercoat Lion"); + + addCard(Zone.BATTLEFIELD, playerD, "Plains", 2); + addCard(Zone.HAND, playerD, "Silvercoat Lion"); + + addCard(Zone.BATTLEFIELD, playerC, "Silvercoat Lion"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Confiscate", "Jace, Unraveler of Secrets"); + activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "-8: You get an emblem with"); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerA, "Blind with Anger", "Rootwater Commando"); + + attack(3, playerC, "Silvercoat Lion", playerB); + castSpell(3, PhaseStep.POSTCOMBAT_MAIN, playerC, "Silvercoat Lion"); + + castSpell(5, PhaseStep.PRECOMBAT_MAIN, playerD, "Silvercoat Lion"); + + setStopAt(5, PhaseStep.END_TURN); + execute(); + + assertLife(playerB, 0); + assertPermanentCount(playerB, 0); + assertGraveyardCount(playerA, "Confiscate", 1); + assertPermanentCount(playerA, "Jace, Unraveler of Secrets", 0); // Removed from game because player C left the game + assertEmblemCount(playerA, 1); + assertPermanentCount(playerC, "Silvercoat Lion", 2); // Emblem does not work yet on player C, because range 1 + assertGraveyardCount(playerD, "Silvercoat Lion", 1); // Emblem should counter the spell + } + + /** + * Situation: I attacked an opponent with some creatures with True + * Conviction in play. There were multiple "deals combat damage to a + * player"-triggers (Edric, Spymaster of Trest, Daxos of Meletis et al), + * then the opponent lost the game during the first strike combat + * damage-step . In the second combat damage step the triggers went on the + * stack again, although there was no player being dealt damage (multiplayer + * game, so the game wasn't over yet). I don't think these abilities should + * trigger again here. + */ + @Test + public void TestPlayerDiesDuringFirstStrikeDamageStep() { + // Creatures you control have double strike and lifelink. + addCard(Zone.BATTLEFIELD, playerD, "True Conviction"); + // Whenever a creature deals combat damage to one of your opponents, its controller may draw a card. + addCard(Zone.BATTLEFIELD, playerD, "Edric, Spymaster of Trest"); + addCard(Zone.BATTLEFIELD, playerD, "Dross Crocodile", 8); // Creature 5/1 + + attack(2, playerD, "Dross Crocodile", playerC); + + setStopAt(3, PhaseStep.END_TURN); + execute(); + + assertLife(playerC, -3); + assertLife(playerD, 7); + + assertHandCount(playerD, 2); // 1 (normal draw) + 1 from True Convition + assertPermanentCount(playerC, 0); + + } + + /** + * I've encountered a case today where someone conceded on their turn. The + * remaining phases were went through as normal, but my Luminarch Ascension + * did not trigger during the end step. + */ + // Player order: A -> D -> C -> B + @Test + public void TestTurnEndTrigger() { + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); + // At the beginning of each opponent's end step, if you didn't lose life this turn, you may put a quest counter on Luminarch Ascension. + // {1}{W}: Create a 4/4 white Angel creature token with flying. Activate this ability only if Luminarch Ascension has four or more quest counters on it.. + addCard(Zone.HAND, playerA, "Luminarch Ascension"); // Enchantment {1}{W} + + addCard(Zone.HAND, playerC, "Lightning Bolt"); + addCard(Zone.BATTLEFIELD, playerC, "Mountain", 1); + + addCard(Zone.HAND, playerD, "Silvercoat Lion"); + addCard(Zone.BATTLEFIELD, playerD, "Plains", 2); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Luminarch Ascension"); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerD, "Silvercoat Lion"); + castSpell(2, PhaseStep.BEGIN_COMBAT, playerC, "Lightning Bolt", playerD); + + setStopAt(3, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertPermanentCount(playerA, "Luminarch Ascension", 1); + assertGraveyardCount(playerC, "Lightning Bolt", 1); + + assertLife(playerD, -1); + Assert.assertFalse("Player D is no longer in the game", playerD.isInGame()); + + assertCounterCount(playerA, "Luminarch Ascension", CounterType.QUEST, 1); // 1 from turn 2 + } + + @Test + public void TestTurnEndTriggerAfterConcede() { + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); + // At the beginning of each opponent's end step, if you didn't lose life this turn, you may put a quest counter on Luminarch Ascension. + // {1}{W}: Create a 4/4 white Angel creature token with flying. Activate this ability only if Luminarch Ascension has four or more quest counters on it.. + addCard(Zone.HAND, playerA, "Luminarch Ascension"); // Enchantment {1}{W} + + addCard(Zone.HAND, playerD, "Silvercoat Lion"); + addCard(Zone.BATTLEFIELD, playerD, "Plains", 2); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Luminarch Ascension"); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerD, "Silvercoat Lion"); + + concede(2, PhaseStep.BEGIN_COMBAT, playerD); + + setStopAt(3, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertPermanentCount(playerA, "Luminarch Ascension", 1); + + assertLife(playerD, 2); + Assert.assertFalse("Player D is no longer in the game", playerD.isInGame()); + + assertCounterCount(playerA, "Luminarch Ascension", CounterType.QUEST, 1); // 1 from turn 2 + } + + /** + * Pithing Needle keeps the named card's abilities disabled even after the + * player controlling the Needle loses the game. + * + * I saw it happen during a Commander game. A player cast Pithing Needle + * targeting my Proteus Staff. After I killed him, I still couldn't activate + * the Staff. + */ + @Test + public void TestPithingNeedle() { + addCard(Zone.BATTLEFIELD, playerA, "Plains", 1); + // As Pithing Needle enters the battlefield, name a card. + // Activated abilities of sources with the chosen name can't be activated unless they're mana abilities. + addCard(Zone.HAND, playerA, "Pithing Needle"); // Artifact {1} + addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion", 1); + addCard(Zone.LIBRARY, playerA, "Pillarfield Ox", 1); + + addCard(Zone.BATTLEFIELD, playerD, "Island", 3); + // {2}{U}, {T}: Put target creature on the bottom of its owner's library. That creature's controller reveals cards from the + // top of his or her library until he or she reveals a creature card. The player puts that card onto the battlefield and the + // rest on the bottom of his or her library in any order. Activate this ability only any time you could cast a sorcery. + addCard(Zone.BATTLEFIELD, playerD, "Proteus Staff", 1); + + addCard(Zone.BATTLEFIELD, playerD, "Eager Cadet", 1); + addCard(Zone.LIBRARY, playerD, "Storm Crow", 2); + + addCard(Zone.BATTLEFIELD, playerC, "Island", 3); + addCard(Zone.BATTLEFIELD, playerC, "Proteus Staff", 1); + addCard(Zone.BATTLEFIELD, playerC, "Wall of Air", 1); + addCard(Zone.LIBRARY, playerC, "Wind Drake", 2); + + addCard(Zone.BATTLEFIELD, playerB, "Island", 3); + addCard(Zone.BATTLEFIELD, playerB, "Proteus Staff", 1); + + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Pithing Needle"); + setChoice(playerA, "Proteus Staff"); + + activateAbility(2, PhaseStep.PRECOMBAT_MAIN, playerD, "{2}{U}", "Silvercoat Lion"); // not allowed + + activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerC, "{2}{U}", "Eager Cadet"); // allowed because Needle out of range + concede(3, PhaseStep.POSTCOMBAT_MAIN, playerA); + + activateAbility(4, PhaseStep.PRECOMBAT_MAIN, playerB, "{2}{U}", "Wall of Air"); // allowed because Needle lost game + + setStopAt(4, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertLife(playerA, 2); + Assert.assertFalse("Player A is no longer in the game", playerA.isInGame()); + + assertPermanentCount(playerA, 0); + + Permanent staffPlayerD = getPermanent("Proteus Staff", playerD); + Assert.assertFalse("Staff of player D could not be used", staffPlayerD.isTapped()); + + assertPermanentCount(playerD, "Eager Cadet", 0); + assertPermanentCount(playerD, "Storm Crow", 1); + + Permanent staffPlayerC = getPermanent("Proteus Staff", playerC); + Assert.assertTrue("Staff of player C could be used", staffPlayerC.isTapped()); + + assertPermanentCount(playerC, "Wall of Air", 0); + assertPermanentCount(playerC, "Wind Drake", 1); + + Permanent staffPlayerB = getPermanent("Proteus Staff", playerB); + Assert.assertTrue("Staff of player B could be used", staffPlayerB.isTapped()); + + } +} 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 74a9d87399..595a54d6e6 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 @@ -57,6 +57,7 @@ import mage.filter.predicate.Predicates; import mage.filter.predicate.mageobject.NamePredicate; import mage.filter.predicate.permanent.SummoningSicknessPredicate; import mage.game.Game; +import mage.game.GameImpl; import mage.game.Graveyard; import mage.game.Table; import mage.game.combat.CombatGroup; @@ -519,6 +520,7 @@ public class TestPlayer implements Player { } if (groups[0].equals("Concede")) { game.concede(getId()); + ((GameImpl) game).checkConcede(); actions.remove(action); } } @@ -1182,6 +1184,11 @@ public class TestPlayer implements Player { computerPlayer.abort(); } + @Override + public void signalPlayerConcede() { + computerPlayer.signalPlayerConcede(); + } + @Override public void abortReset() { computerPlayer.abortReset(); diff --git a/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java b/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java index 54fdb7ac45..e082f0a0b9 100644 --- a/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java +++ b/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java @@ -27,6 +27,8 @@ */ package org.mage.test.stub; +import java.io.Serializable; +import java.util.*; import mage.MageObject; import mage.abilities.*; import mage.abilities.costs.AlternativeSourceCosts; @@ -62,9 +64,6 @@ import mage.target.TargetAmount; import mage.target.TargetCard; import mage.target.common.TargetCardInLibrary; -import java.io.Serializable; -import java.util.*; - /** * * @author Quercitron @@ -702,6 +701,11 @@ public class PlayerStub implements Player { } + @Override + public void signalPlayerConcede() { + + } + @Override public void abortReset() { diff --git a/Mage/src/main/java/mage/game/Game.java b/Mage/src/main/java/mage/game/Game.java index 044e6a3ae7..3956dd24b7 100644 --- a/Mage/src/main/java/mage/game/Game.java +++ b/Mage/src/main/java/mage/game/Game.java @@ -173,7 +173,7 @@ public interface Game extends MageItem, Serializable { UUID getPriorityPlayerId(); - boolean gameOver(UUID playerId); + boolean checkIfGameIsOver(); boolean hasEnded(); @@ -347,6 +347,8 @@ public interface Game extends MageItem, Serializable { void concede(UUID playerId); + void setConcedingPlayer(UUID playerId); + void setManaPaymentMode(UUID playerId, boolean autoPayment); void setManaPaymentModeRestricted(UUID playerId, boolean autoPaymentRestricted); diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index 768703d804..d371a119ed 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -161,6 +161,8 @@ public abstract class GameImpl implements Game, Serializable { private final LinkedList stackObjectsCheck = new LinkedList<>(); // used to check if different sources used the stack // used to set the counters a permanent adds the battlefield (if no replacement effect is used e.g. Persist) protected Map enterWithCounters = new HashMap<>(); + // used to proceed player conceding requests + private final LinkedList concedingPlayers = new LinkedList<>(); // used to handle asynchronous request of a player to leave the game public GameImpl(MultiplayerAttackOption attackOption, RangeOfInfluence range, int freeMulligans, int startLife) { this.id = UUID.randomUUID(); @@ -535,26 +537,58 @@ public abstract class GameImpl implements Game, Serializable { } } - /** - * Starts check if game is over or if playerId is given let the player - * concede. - * - * @param playerId - * @return - */ +// /** +// * Starts check if game is over or if playerId is given let the player +// * concede. +// * +// * @param playerId +// * @return +// */ +// @Override +// public synchronized boolean gameOver(UUID playerId) { +// if (playerId == null) { +// boolean result = checkIfGameIsOver(); +// return result; +// } else { +// logger.debug("Game over for player Id: " + playerId + " gameId " + getId()); +// concedingPlayers.add(playerId); +// Player player = getPlayer(state.getPriorityPlayerId()); +// if (player != null && player.isHuman()) { +// player.signalPlayerConcede(); +// } else { +// checkConcede(); +// } +// return true; +// } +// } @Override - public synchronized boolean gameOver(UUID playerId) { - if (playerId == null) { - boolean result = checkIfGameIsOver(); - return result; + public void setConcedingPlayer(UUID playerId) { + Player player = getPlayer(state.getPriorityPlayerId()); + if (player != null) { + if (!player.hasLeft() && player.isHuman()) { + if (!concedingPlayers.contains(playerId)) { + logger.debug("Game over for player Id: " + playerId + " gameId " + getId()); + concedingPlayers.add(playerId); + player.signalPlayerConcede(); + } + } else { + // no asynchronous action so check directly + checkConcede(); + } } else { - logger.debug("Game over for player Id: " + playerId + " gameId " + getId()); - leave(playerId); - return true; + checkConcede(); + checkIfGameIsOver(); } } - private boolean checkIfGameIsOver() { + public void checkConcede() { + while (!concedingPlayers.isEmpty()) { + leave(concedingPlayers.removeFirst()); + } + } + + @Override + public boolean checkIfGameIsOver() { if (state.isGameOver()) { return true; } @@ -578,7 +612,7 @@ public abstract class GameImpl implements Game, Serializable { } for (Player player : state.getPlayers().values()) { if (!player.hasLeft() && !player.hasLost()) { - logger.debug(new StringBuilder("Player ").append(player.getName()).append(" has won gameId: ").append(this.getId())); + logger.debug("Player " + player.getName() + " has won gameId: " + this.getId()); player.won(this); } } @@ -696,13 +730,13 @@ public abstract class GameImpl implements Game, Serializable { Player player = getPlayer(playerList.get()); boolean wasPaused = state.isPaused(); state.resume(); - if (!gameOver(null)) { + if (!checkIfGameIsOver()) { fireInformEvent("Turn " + state.getTurnNum()); if (checkStopOnTurnOption()) { return; } state.getTurn().resumePlay(this, wasPaused); - if (!isPaused() && !gameOver(null)) { + if (!isPaused() && !checkIfGameIsOver()) { endOfTurn(); player = playerList.getNext(this); state.setTurnNum(state.getTurnNum() + 1); @@ -712,11 +746,11 @@ public abstract class GameImpl implements Game, Serializable { } protected void play(UUID nextPlayerId) { - if (!isPaused() && !gameOver(null)) { + if (!isPaused() && !checkIfGameIsOver()) { playerList = state.getPlayerList(nextPlayerId); Player playerByOrder = getPlayer(playerList.get()); state.setPlayerByOrderId(playerByOrder.getId()); - while (!isPaused() && !gameOver(null)) { + while (!isPaused() && !checkIfGameIsOver()) { if (!playExtraTurns()) { break; } @@ -733,7 +767,7 @@ public abstract class GameImpl implements Game, Serializable { state.setPlayerByOrderId(playerByOrder.getId()); } } - if (gameOver(null) && !isSimulation()) { + if (checkIfGameIsOver() && !isSimulation()) { winnerId = findWinnersAndLosers(); StringBuilder sb = new StringBuilder("GAME END gameId: ").append(this.getId()).append(' '); int count = 0; @@ -816,7 +850,7 @@ public abstract class GameImpl implements Game, Serializable { skipTurn = state.getTurn().play(this, player); } while (executingRollback); - if (isPaused() || gameOver(null)) { + if (isPaused() || checkIfGameIsOver()) { return false; } if (!skipTurn) { @@ -854,7 +888,7 @@ public abstract class GameImpl implements Game, Serializable { saveState(false); - if (gameOver(null)) { + if (checkIfGameIsOver()) { return; } @@ -1245,7 +1279,7 @@ public abstract class GameImpl implements Game, Serializable { clearAllBookmarks(); try { applyEffects(); - while (!isPaused() && !gameOver(null) && !this.getTurn().isEndTurnRequested()) { + while (!isPaused() && !checkIfGameIsOver() && !this.getTurn().isEndTurnRequested()) { if (!resuming) { state.getPlayers().resetPassed(); state.getPlayerList().setCurrent(activePlayerId); @@ -1254,14 +1288,14 @@ public abstract class GameImpl implements Game, Serializable { } fireUpdatePlayersEvent(); Player player; - while (!isPaused() && !gameOver(null)) { + while (!isPaused() && !checkIfGameIsOver()) { try { if (bookmark == 0) { bookmark = bookmarkState(); } player = getPlayer(state.getPlayerList().get()); state.setPriorityPlayerId(player.getId()); - while (!player.isPassed() && player.canRespond() && !isPaused() && !gameOver(null)) { + while (!player.isPassed() && player.canRespond() && !isPaused() && !checkIfGameIsOver()) { if (!resuming) { // 603.3. Once an ability has triggered, its controller puts it on the stack as an object that's not a card the next time a player would receive priority checkStateAndTriggered(); @@ -1270,7 +1304,7 @@ public abstract class GameImpl implements Game, Serializable { resetLKI(); } saveState(false); - if (isPaused() || gameOver(null)) { + if (isPaused() || checkIfGameIsOver()) { return; } // resetPassed should be called if player performs any action @@ -1289,13 +1323,14 @@ public abstract class GameImpl implements Game, Serializable { } resetShortLivingLKI(); resuming = false; - if (isPaused() || gameOver(null)) { + if (isPaused() || checkIfGameIsOver()) { return; } if (allPassed()) { if (!state.getStack().isEmpty()) { //20091005 - 115.4 resolve(); + checkConcede(); applyEffects(); state.getPlayers().resetPassed(); fireUpdatePlayersEvent(); @@ -1609,11 +1644,11 @@ public abstract class GameImpl implements Game, Serializable { public boolean checkStateAndTriggered() { boolean somethingHappened = false; //20091005 - 115.5 - while (!isPaused() && !gameOver(null)) { + while (!isPaused() && !checkIfGameIsOver()) { if (!checkStateBasedActions()) { // nothing happened so check triggers state.handleSimultaneousEvent(this); - if (isPaused() || gameOver(null) || getTurn().isEndTurnRequested() || !checkTriggered()) { + if (isPaused() || checkIfGameIsOver() || getTurn().isEndTurnRequested() || !checkTriggered()) { break; } } @@ -1621,6 +1656,7 @@ public abstract class GameImpl implements Game, Serializable { applyEffects(); // needed e.g if boost effects end and cause creatures to die somethingHappened = true; } + checkConcede(); return somethingHappened; } @@ -1734,7 +1770,6 @@ public abstract class GameImpl implements Game, Serializable { } } - List planeswalkers = new ArrayList<>(); List legendary = new ArrayList<>(); List worldEnchantment = new ArrayList<>(); for (Permanent perm : getBattlefield().getAllActivePermanents()) { @@ -1781,7 +1816,6 @@ public abstract class GameImpl implements Game, Serializable { continue; } } - planeswalkers.add(perm); } if (perm.isWorld()) { worldEnchantment.add(perm); @@ -2288,7 +2322,6 @@ public abstract class GameImpl implements Game, Serializable { * @param playerId */ protected void leave(UUID playerId) { // needs to be executed from the game thread, not from the concede thread of conceding player! - Player player = getPlayer(playerId); if (player == null || player.hasLeft()) { logger.debug("Player already left " + (player != null ? player.getName() : playerId)); diff --git a/Mage/src/main/java/mage/game/combat/Combat.java b/Mage/src/main/java/mage/game/combat/Combat.java index 1ad1948a1b..135919b8ab 100644 --- a/Mage/src/main/java/mage/game/combat/Combat.java +++ b/Mage/src/main/java/mage/game/combat/Combat.java @@ -264,7 +264,7 @@ public class Combat implements Serializable, Copyable { player.selectAttackers(game, attackingPlayerId); } firstTime = false; - if (game.isPaused() || game.gameOver(null) || game.executingRollback()) { + if (game.isPaused() || game.checkIfGameIsOver() || game.executingRollback()) { return; } // because of possible undo during declare attackers it's neccassary to call here the methods with "game.getCombat()." to get the current combat object!!! @@ -461,7 +461,7 @@ public class Combat implements Serializable, Copyable { } while (choose) { controller.selectBlockers(game, defenderId); - if (game.isPaused() || game.gameOver(null) || game.executingRollback()) { + if (game.isPaused() || game.checkIfGameIsOver() || game.executingRollback()) { return; } if (!game.getCombat().checkBlockRestrictions(defender, game)) { diff --git a/Mage/src/main/java/mage/game/turn/Phase.java b/Mage/src/main/java/mage/game/turn/Phase.java index e4b276336e..500709b399 100644 --- a/Mage/src/main/java/mage/game/turn/Phase.java +++ b/Mage/src/main/java/mage/game/turn/Phase.java @@ -95,7 +95,7 @@ public abstract class Phase implements Serializable { } public boolean play(Game game, UUID activePlayerId) { - if (game.isPaused() || game.gameOver(null)) { + if (game.isPaused() || game.checkIfGameIsOver()) { return false; } @@ -104,7 +104,7 @@ public abstract class Phase implements Serializable { if (beginPhase(game, activePlayerId)) { for (Step step : steps) { - if (game.isPaused() || game.gameOver(null)) { + if (game.isPaused() || game.checkIfGameIsOver()) { return false; } if (game.getTurn().isEndTurnRequested() && step.getType()!=PhaseStep.CLEANUP) { @@ -122,7 +122,7 @@ public abstract class Phase implements Serializable { } } - if (game.isPaused() || game.gameOver(null)) { + if (game.isPaused() || game.checkIfGameIsOver()) { return false; } count++; @@ -143,7 +143,7 @@ public abstract class Phase implements Serializable { } public boolean resumePlay(Game game, PhaseStep stepType, boolean wasPaused) { - if (game.isPaused() || game.gameOver(null)) { + if (game.isPaused() || game.checkIfGameIsOver()) { return false; } @@ -157,7 +157,7 @@ public abstract class Phase implements Serializable { resumeStep(game, wasPaused); while (it.hasNext()) { step = it.next(); - if (game.isPaused() || game.gameOver(null)) { + if (game.isPaused() || game.checkIfGameIsOver()) { return false; } currentStep = step; @@ -169,7 +169,7 @@ public abstract class Phase implements Serializable { } } - if (game.isPaused() || game.gameOver(null)) { + if (game.isPaused() || game.checkIfGameIsOver()) { return false; } count++; @@ -206,13 +206,13 @@ public abstract class Phase implements Serializable { if (!currentStep.skipStep(game, activePlayerId)) { game.getState().increaseStepNum(); prePriority(game, activePlayerId); - if (!game.isPaused() && !game.gameOver(null) && !game.executingRollback()) { + if (!game.isPaused() && !game.checkIfGameIsOver() && !game.executingRollback()) { currentStep.priority(game, activePlayerId, false); if (game.executingRollback()) { return; } } - if (!game.isPaused() && !game.gameOver(null) && !game.executingRollback()) { + if (!game.isPaused() && !game.checkIfGameIsOver() && !game.executingRollback()) { postPriority(game, activePlayerId); } } @@ -233,11 +233,11 @@ public abstract class Phase implements Serializable { prePriority(game, activePlayerId); } case PRIORITY: - if (!game.isPaused() && !game.gameOver(null)) { + if (!game.isPaused() && !game.checkIfGameIsOver()) { currentStep.priority(game, activePlayerId, resuming); } case POST: - if (!game.isPaused() && !game.gameOver(null)) { + if (!game.isPaused() && !game.checkIfGameIsOver()) { postPriority(game, activePlayerId); } } diff --git a/Mage/src/main/java/mage/game/turn/Turn.java b/Mage/src/main/java/mage/game/turn/Turn.java index 4ae01ed55e..6b910ad1e6 100644 --- a/Mage/src/main/java/mage/game/turn/Turn.java +++ b/Mage/src/main/java/mage/game/turn/Turn.java @@ -127,7 +127,7 @@ public class Turn implements Serializable { public boolean play(Game game, Player activePlayer) { activePlayer.becomesActivePlayer(); this.setDeclareAttackersStepStarted(false); - if (game.isPaused() || game.gameOver(null)) { + if (game.isPaused() || game.checkIfGameIsOver()) { return false; } @@ -143,7 +143,7 @@ public class Turn implements Serializable { resetCounts(); game.getPlayer(activePlayer.getId()).beginTurn(game); for (Phase phase : phases) { - if (game.isPaused() || game.gameOver(null)) { + if (game.isPaused() || game.checkIfGameIsOver()) { return false; } if (!isEndTurnRequested() || phase.getType() == TurnPhase.END) { @@ -189,7 +189,7 @@ public class Turn implements Serializable { } while (it.hasNext()) { phase = it.next(); - if (game.isPaused() || game.gameOver(null)) { + if (game.isPaused() || game.checkIfGameIsOver()) { return; } currentPhase = phase; diff --git a/Mage/src/main/java/mage/players/Player.java b/Mage/src/main/java/mage/players/Player.java index 261a24c22f..71478eb2f9 100644 --- a/Mage/src/main/java/mage/players/Player.java +++ b/Mage/src/main/java/mage/players/Player.java @@ -444,6 +444,8 @@ public interface Player extends MageItem, Copyable { void abortReset(); + void signalPlayerConcede(); + void skip(); // priority, undo, ... diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index 1567246fda..1792900468 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -2039,9 +2039,9 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public void concede(Game game) { - game.gameOver(playerId); + game.setConcedingPlayer(playerId); lost(game); - this.left = true; +// this.left = true; } @Override @@ -2136,7 +2136,7 @@ public abstract class PlayerImpl implements Player, Serializable { // for draw - first all players that have lost have to be set to lost if (!hasLeft()) { logger.debug("Game over playerId: " + playerId); - game.gameOver(playerId); + game.setConcedingPlayer(playerId); } } @@ -2197,7 +2197,7 @@ public abstract class PlayerImpl implements Player, Serializable { this.draws = true; game.fireEvent(GameEvent.getEvent(GameEvent.EventType.DRAW_PLAYER, null, null, playerId)); game.informPlayers("For " + this.getLogName() + " the game is a draw."); - game.gameOver(playerId); + game.setConcedingPlayer(playerId); } } @@ -3578,6 +3578,11 @@ public abstract class PlayerImpl implements Player, Serializable { abort = false; } + @Override + public void signalPlayerConcede() { + + } + @Override public boolean scry(int value, Ability source, Game game From 8f49988bbf45ade7b4030c32878d598f9944ac69 Mon Sep 17 00:00:00 2001 From: Jerek Wilson Date: Sun, 22 Oct 2017 19:37:53 -0400 Subject: [PATCH 09/18] JW-TalonOfPain: Implemented Talon of Pain --- .../target/maven-archiver/pom.properties | 2 +- Mage.Sets/src/mage/cards/i/IceFloe.java | 2 +- Mage.Sets/src/mage/cards/j/JaggedPoppet.java | 2 +- Mage.Sets/src/mage/cards/r/RhysticCave.java | 2 +- Mage.Sets/src/mage/cards/t/TalonOfPain.java | 156 ++++++++++++++++++ Mage.Sets/src/mage/sets/Darksteel.java | 1 + 6 files changed, 161 insertions(+), 4 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/t/TalonOfPain.java diff --git a/Mage.Server.Plugins/Mage.Game.PennyDreadfulCommanderFreeForAll/target/maven-archiver/pom.properties b/Mage.Server.Plugins/Mage.Game.PennyDreadfulCommanderFreeForAll/target/maven-archiver/pom.properties index a617eb7af6..8603daf326 100644 --- a/Mage.Server.Plugins/Mage.Game.PennyDreadfulCommanderFreeForAll/target/maven-archiver/pom.properties +++ b/Mage.Server.Plugins/Mage.Game.PennyDreadfulCommanderFreeForAll/target/maven-archiver/pom.properties @@ -1,5 +1,5 @@ #Generated by Maven -#Wed Oct 18 23:39:20 EDT 2017 +#Sun Oct 22 00:34:49 EDT 2017 version=1.4.26 groupId=org.mage artifactId=mage-game-pennydreadfulcommanderfreeforall diff --git a/Mage.Sets/src/mage/cards/i/IceFloe.java b/Mage.Sets/src/mage/cards/i/IceFloe.java index e66f0ce6e3..9d163198ea 100644 --- a/Mage.Sets/src/mage/cards/i/IceFloe.java +++ b/Mage.Sets/src/mage/cards/i/IceFloe.java @@ -46,7 +46,7 @@ import mage.target.common.TargetCreaturePermanent; /** * - * @author anonymous + * @author jerekwilson */ public class IceFloe extends CardImpl { diff --git a/Mage.Sets/src/mage/cards/j/JaggedPoppet.java b/Mage.Sets/src/mage/cards/j/JaggedPoppet.java index 4ac9008fea..e8627cd335 100644 --- a/Mage.Sets/src/mage/cards/j/JaggedPoppet.java +++ b/Mage.Sets/src/mage/cards/j/JaggedPoppet.java @@ -49,7 +49,7 @@ import mage.players.Player; /** * - * @author anonymous + * @author jerekwilson */ public class JaggedPoppet extends CardImpl { diff --git a/Mage.Sets/src/mage/cards/r/RhysticCave.java b/Mage.Sets/src/mage/cards/r/RhysticCave.java index deebe871f6..d03d504477 100644 --- a/Mage.Sets/src/mage/cards/r/RhysticCave.java +++ b/Mage.Sets/src/mage/cards/r/RhysticCave.java @@ -50,7 +50,7 @@ import mage.players.Player; /** * - * @author anonymous + * @author jerekwilson */ public class RhysticCave extends CardImpl { diff --git a/Mage.Sets/src/mage/cards/t/TalonOfPain.java b/Mage.Sets/src/mage/cards/t/TalonOfPain.java new file mode 100644 index 0000000000..b00e019e05 --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/TalonOfPain.java @@ -0,0 +1,156 @@ +/* + * 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.t; + +import java.util.Objects; +import java.util.UUID; +import mage.abilities.Ability; +import mage.abilities.TriggeredAbilityImpl; +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.ManacostVariableValue; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.DamageTargetEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.Zone; +import mage.counters.CounterType; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.target.common.TargetCreatureOrPlayer; + +/** + * + * @author jerekwilson + */ +public class TalonOfPain extends CardImpl { + + public TalonOfPain(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{4}"); + + + /* + * Whenever a source you control other than Talon of Pain deals damage to an opponent, + * put a charge counter on Talon of Pain. + */ + this.addAbility(new TalonOfPainTriggeredAbility()); + + + // {X}, {tap}, Remove X charge counters from Talon of Pain: Talon of Pain deals X damage to target creature or player. + Ability ability = new SimpleActivatedAbility(Zone.BATTLEFIELD, new DamageTargetEffect(new ManacostVariableValue()) , new ManaCostsImpl("{X}")); + ability.addCost(new TapSourceCost()); + ability.addCost(new RemoveVariableCountersSourceCost(CounterType.CHARGE.createInstance())); + ability.addTarget(new TargetCreatureOrPlayer()); + this.addAbility(ability); + + + } + + public TalonOfPain(final TalonOfPain card) { + super(card); + } + + @Override + public TalonOfPain copy() { + return new TalonOfPain(this); + } + + private class TalonOfPainTriggeredAbility extends TriggeredAbilityImpl { + + public TalonOfPainTriggeredAbility() { + super(Zone.BATTLEFIELD, new TalonOfPainEffect()); + } + + public TalonOfPainTriggeredAbility(final TalonOfPainTriggeredAbility ability) { + super(ability); + } + + @Override + public TalonOfPainTriggeredAbility copy() { + return new TalonOfPainTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.DAMAGED_PLAYER; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + // to another player + if (!Objects.equals(this.getControllerId(), event.getTargetId())) { + // a source you control other than Talon of Pain + UUID sourceControllerId = game.getControllerId(event.getSourceId()); + if (sourceControllerId != null + && sourceControllerId.equals(this.getControllerId()) + && this.getSourceId() != event.getSourceId() ) { + // return true so the effect will fire and a charge counter will be added + return true; + } + } + return false; + } + + @Override + public String getRule() { + return "Whenever a source you control other than {this} deals damage to another player, " + super.getRule(); + } + } + + private static class TalonOfPainEffect extends OneShotEffect { + + public TalonOfPainEffect() { + super(Outcome.Damage); + this.staticText = "put a charge counter on {this}."; + } + + public TalonOfPainEffect(final TalonOfPainEffect effect) { + super(effect); + } + + @Override + public TalonOfPainEffect copy() { + return new TalonOfPainEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = game.getPermanent(source.getSourceId()); + if (permanent != null) { + permanent.addCounters(CounterType.CHARGE.createInstance(), source, game); + return true; + } + return false; + } + } +} diff --git a/Mage.Sets/src/mage/sets/Darksteel.java b/Mage.Sets/src/mage/sets/Darksteel.java index eae36ce1cd..923073de62 100644 --- a/Mage.Sets/src/mage/sets/Darksteel.java +++ b/Mage.Sets/src/mage/sets/Darksteel.java @@ -157,6 +157,7 @@ public class Darksteel extends ExpansionSet { cards.add(new SetCardInfo("Sword of Fire and Ice", 148, Rarity.RARE, mage.cards.s.SwordOfFireAndIce.class)); cards.add(new SetCardInfo("Sword of Light and Shadow", 149, Rarity.RARE, mage.cards.s.SwordOfLightAndShadow.class)); cards.add(new SetCardInfo("Synod Artificer", 34, Rarity.RARE, mage.cards.s.SynodArtificer.class)); + cards.add(new SetCardInfo("Talon of Pain", 150, Rarity.UNCOMMON, mage.cards.t.TalonOfPain.class)); cards.add(new SetCardInfo("Tangle Golem", 151, Rarity.COMMON, mage.cards.t.TangleGolem.class)); cards.add(new SetCardInfo("Tangle Spider", 85, Rarity.COMMON, mage.cards.t.TangleSpider.class)); cards.add(new SetCardInfo("Tanglewalker", 86, Rarity.UNCOMMON, mage.cards.t.Tanglewalker.class)); From 250f6ee1c10721b252cd88b0aa560ed96150b2ae Mon Sep 17 00:00:00 2001 From: Jerek Wilson Date: Mon, 23 Oct 2017 22:44:40 -0400 Subject: [PATCH 10/18] JW-CardUpdates: Updated Rhystic Cave to announce color choice first before the decision to pay occurs. Made suggested modifications for Talon of Pain --- Mage.Sets/src/mage/cards/r/RhysticCave.java | 46 ++++++++++++--------- Mage.Sets/src/mage/cards/t/TalonOfPain.java | 37 ++++------------- 2 files changed, 33 insertions(+), 50 deletions(-) diff --git a/Mage.Sets/src/mage/cards/r/RhysticCave.java b/Mage.Sets/src/mage/cards/r/RhysticCave.java index d03d504477..88362d5905 100644 --- a/Mage.Sets/src/mage/cards/r/RhysticCave.java +++ b/Mage.Sets/src/mage/cards/r/RhysticCave.java @@ -28,13 +28,16 @@ package mage.cards.r; import java.util.UUID; +import mage.MageObject; import mage.Mana; +import mage.ObjectColor; import mage.abilities.Ability; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.costs.common.TapSourceCost; import mage.abilities.costs.mana.GenericManaCost; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.Effect; +import mage.abilities.effects.common.ChooseColorEffect; import mage.abilities.effects.common.DoUnlessAnyPlayerPaysEffect; import mage.abilities.effects.common.ManaEffect; import mage.abilities.effects.common.continuous.BoostSourceEffect; @@ -43,6 +46,7 @@ import mage.cards.CardSetInfo; import mage.choices.ChoiceColor; import mage.constants.CardType; import mage.constants.Duration; +import mage.constants.Outcome; import mage.constants.Zone; import mage.game.Game; import mage.game.permanent.Permanent; @@ -57,11 +61,15 @@ public class RhysticCave extends CardImpl { public RhysticCave(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.LAND}, ""); - // {tap}: Choose a color. Add one mana of that color to your mana pool unless any player pays {1}. - // Activate this ability only any time you could cast an instant. - this.addAbility(new SimpleActivatedAbility(Zone.BATTLEFIELD, - new DoUnlessAnyPlayerPaysEffect(new RhysticCaveManaEffect(),new GenericManaCost(1)), - new TapSourceCost())); + // {tap}: Choose a color. + Ability ability = new SimpleActivatedAbility(Zone.BATTLEFIELD, + new ChooseColorEffect(Outcome.PutManaInPool), + new TapSourceCost()); + + // Add one mana of that color to your mana pool unless any player pays {1}. Activate this ability only any time you could cast an instant. + ability.addEffect(new DoUnlessAnyPlayerPaysEffect(new RhysticCaveManaEffect(),new GenericManaCost(1))); + + this.addAbility(ability); } @@ -82,7 +90,7 @@ public class RhysticCave extends CardImpl { public RhysticCaveManaEffect() { super(); chosenMana = new Mana(); - this.staticText = "Choose a color. Add one mana of that color to your mana pool "; + this.staticText = "Add one mana of that color to your mana pool "; } public RhysticCaveManaEffect(final RhysticCaveManaEffect effect) { @@ -97,29 +105,27 @@ public class RhysticCave extends CardImpl { @Override public boolean apply(Game game, Ability source) { - Player controller = game.getPlayer(source.getControllerId()); - - if (controller != null) { - ChoiceColor choice = new ChoiceColor(); - choice.setMessage("Choose a color to add mana of that color"); - - if (controller.choose(outcome, choice, game)) { - if (choice.getChoice() != null) { - String color = choice.getChoice(); + Player controller = game.getPlayer(source.getControllerId()); + MageObject mageObject = game.getPermanentOrLKIBattlefield(source.getSourceId()); //get obj reference to Rhystic Cave + if (controller != null) { + if (mageObject != null) { + ObjectColor choice = (ObjectColor) game.getState().getValue(mageObject.getId()+"_color"); + if (choice!= null) { + String color = choice.toString(); switch (color) { - case "Red": + case "R": chosenMana.setRed(1); break; - case "Blue": + case "U": chosenMana.setBlue(1); break; - case "White": + case "W": chosenMana.setWhite(1); break; - case "Black": + case "B": chosenMana.setBlack(1); break; - case "Green": + case "G": chosenMana.setGreen(1); break; } diff --git a/Mage.Sets/src/mage/cards/t/TalonOfPain.java b/Mage.Sets/src/mage/cards/t/TalonOfPain.java index b00e019e05..6cdcfb3dc6 100644 --- a/Mage.Sets/src/mage/cards/t/TalonOfPain.java +++ b/Mage.Sets/src/mage/cards/t/TalonOfPain.java @@ -38,6 +38,7 @@ import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.dynamicvalue.common.ManacostVariableValue; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.DamageTargetEffect; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -47,6 +48,7 @@ import mage.counters.CounterType; import mage.game.Game; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; +import mage.players.Player; import mage.target.common.TargetCreatureOrPlayer; /** @@ -88,7 +90,7 @@ public class TalonOfPain extends CardImpl { private class TalonOfPainTriggeredAbility extends TriggeredAbilityImpl { public TalonOfPainTriggeredAbility() { - super(Zone.BATTLEFIELD, new TalonOfPainEffect()); + super(Zone.BATTLEFIELD, new AddCountersSourceEffect(CounterType.CHARGE.createInstance())); } public TalonOfPainTriggeredAbility(final TalonOfPainTriggeredAbility ability) { @@ -108,7 +110,9 @@ public class TalonOfPain extends CardImpl { @Override public boolean checkTrigger(GameEvent event, Game game) { // to another player - if (!Objects.equals(this.getControllerId(), event.getTargetId())) { + Player controller = game.getPlayer(this.getControllerId()); + if(controller==null){return false;} + if(controller.hasOpponent(event.getTargetId(),game)){ // a source you control other than Talon of Pain UUID sourceControllerId = game.getControllerId(event.getSourceId()); if (sourceControllerId != null @@ -123,34 +127,7 @@ public class TalonOfPain extends CardImpl { @Override public String getRule() { - return "Whenever a source you control other than {this} deals damage to another player, " + super.getRule(); - } - } - - private static class TalonOfPainEffect extends OneShotEffect { - - public TalonOfPainEffect() { - super(Outcome.Damage); - this.staticText = "put a charge counter on {this}."; - } - - public TalonOfPainEffect(final TalonOfPainEffect effect) { - super(effect); - } - - @Override - public TalonOfPainEffect copy() { - return new TalonOfPainEffect(this); - } - - @Override - public boolean apply(Game game, Ability source) { - Permanent permanent = game.getPermanent(source.getSourceId()); - if (permanent != null) { - permanent.addCounters(CounterType.CHARGE.createInstance(), source, game); - return true; - } - return false; + return "Whenever a source you control other than {this} deals damage to an opponent, " + super.getRule(); } } } From 8338a788d6869dabdc8fc17a66aac84d124d681c Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Thu, 26 Oct 2017 11:18:04 -0400 Subject: [PATCH 11/18] fixed Derevi, Empyrial Tactician triggering while Humility is out when entering using its activated ability --- Mage.Sets/src/mage/cards/d/DereviEmpyrialTactician.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Mage.Sets/src/mage/cards/d/DereviEmpyrialTactician.java b/Mage.Sets/src/mage/cards/d/DereviEmpyrialTactician.java index 38fd233b3a..2ac726055c 100644 --- a/Mage.Sets/src/mage/cards/d/DereviEmpyrialTactician.java +++ b/Mage.Sets/src/mage/cards/d/DereviEmpyrialTactician.java @@ -60,7 +60,7 @@ import mage.target.TargetPermanent; public class DereviEmpyrialTactician extends CardImpl { public DereviEmpyrialTactician(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{G}{W}{U}"); + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{G}{W}{U}"); addSuperType(SuperType.LEGENDARY); this.subtype.add(SubType.BIRD); this.subtype.add(SubType.WIZARD); @@ -74,7 +74,7 @@ public class DereviEmpyrialTactician extends CardImpl { Ability ability = new DereviEmpyrialTacticianTriggeredAbility(new MayTapOrUntapTargetEffect()); ability.addTarget(new TargetPermanent()); this.addAbility(ability); - + // {1}{G}{W}{U}: Put Derevi onto the battlefield from the command zone. this.addAbility(new DereviEmpyrialTacticianAbility()); } @@ -132,7 +132,7 @@ class DereviEmpyrialTacticianTriggeredAbility extends TriggeredAbilityImpl { } } - class DereviEmpyrialTacticianAbility extends ActivatedAbilityImpl { +class DereviEmpyrialTacticianAbility extends ActivatedAbilityImpl { public DereviEmpyrialTacticianAbility() { super(Zone.COMMAND, new PutCommanderOnBattlefieldEffect(), new ManaCostsImpl("{1}{G}{W}{U}")); @@ -182,7 +182,7 @@ class PutCommanderOnBattlefieldEffect extends OneShotEffect { } Card card = game.getCard(source.getSourceId()); if (card != null) { - card.putOntoBattlefield(game, Zone.COMMAND, source.getSourceId(), source.getControllerId()); + player.moveCards(card, Zone.BATTLEFIELD, source, game); return true; } return false; From 8ff6368aaf5bc54ed4a3fd1aed82fd736b5cd78b Mon Sep 17 00:00:00 2001 From: Faxn Date: Thu, 26 Oct 2017 16:27:16 -0400 Subject: [PATCH 12/18] #4130 Changed Decree of Justice to attach it's mana cost to the CycleTriggeredAbility instead handling the cost in the card. --- Mage.Sets/src/mage/cards/d/DecreeOfJustice.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Mage.Sets/src/mage/cards/d/DecreeOfJustice.java b/Mage.Sets/src/mage/cards/d/DecreeOfJustice.java index a812b8ff22..7a5cc0a9e8 100644 --- a/Mage.Sets/src/mage/cards/d/DecreeOfJustice.java +++ b/Mage.Sets/src/mage/cards/d/DecreeOfJustice.java @@ -34,6 +34,7 @@ import mage.abilities.costs.mana.GenericManaCost; import mage.abilities.costs.mana.ManaCost; import mage.abilities.costs.mana.ManaCosts; import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.costs.mana.VariableManaCost; import mage.abilities.dynamicvalue.common.ManacostVariableValue; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.CreateTokenEffect; @@ -66,6 +67,7 @@ public class DecreeOfJustice extends CardImpl { // When you cycle Decree of Justice, you may pay {X}. If you do, create X 1/1 white Soldier creature tokens. Ability ability = new CycleTriggeredAbility(new DecreeOfJusticeCycleEffect(), true); + ability.addCost(new ManaCostsImpl<>("{X}")); this.addAbility(ability); } @@ -98,14 +100,12 @@ class DecreeOfJusticeCycleEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { Player player = game.getPlayer(source.getControllerId()); - ManaCosts cost = new ManaCostsImpl<>("{X}"); if (player != null) { - int costX = player.announceXMana(0, Integer.MAX_VALUE, "Announce the value for {X}", game, source); - cost.add(new GenericManaCost(costX)); - if (cost.pay(source, game, source.getSourceId(), source.getControllerId(), false, null)) { - Token token = new SoldierToken(); - token.putOntoBattlefield(costX, game, source.getSourceId(), source.getControllerId()); - } + Token token = new SoldierToken(); + int X = new ManacostVariableValue().calculate(game, source, this); + token.putOntoBattlefield(X, game, source.getSourceId(), source.getControllerId()); + return true; + } return false; } From df2da656d214b24cd086facda97e2bbf92eaf413 Mon Sep 17 00:00:00 2001 From: Faxn Date: Thu, 26 Oct 2017 19:58:10 -0400 Subject: [PATCH 13/18] #4130 Fixing the fix to be more reasonable. --- Mage.Sets/src/mage/cards/d/DecreeOfJustice.java | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Mage.Sets/src/mage/cards/d/DecreeOfJustice.java b/Mage.Sets/src/mage/cards/d/DecreeOfJustice.java index 7a5cc0a9e8..6adb06403a 100644 --- a/Mage.Sets/src/mage/cards/d/DecreeOfJustice.java +++ b/Mage.Sets/src/mage/cards/d/DecreeOfJustice.java @@ -30,6 +30,7 @@ package mage.cards.d; import java.util.UUID; import mage.abilities.Ability; import mage.abilities.common.CycleTriggeredAbility; +import mage.abilities.costs.Cost; import mage.abilities.costs.mana.GenericManaCost; import mage.abilities.costs.mana.ManaCost; import mage.abilities.costs.mana.ManaCosts; @@ -67,7 +68,6 @@ public class DecreeOfJustice extends CardImpl { // When you cycle Decree of Justice, you may pay {X}. If you do, create X 1/1 white Soldier creature tokens. Ability ability = new CycleTriggeredAbility(new DecreeOfJusticeCycleEffect(), true); - ability.addCost(new ManaCostsImpl<>("{X}")); this.addAbility(ability); } @@ -101,12 +101,14 @@ class DecreeOfJusticeCycleEffect extends OneShotEffect { public boolean apply(Game game, Ability source) { Player player = game.getPlayer(source.getControllerId()); if (player != null) { - Token token = new SoldierToken(); - int X = new ManacostVariableValue().calculate(game, source, this); - token.putOntoBattlefield(X, game, source.getSourceId(), source.getControllerId()); - return true; - + int X = player.announceXMana(0, Integer.MAX_VALUE, "Announce the value for {X}", game, source); + Cost cost = new GenericManaCost(X); + if(cost.pay(source, game, source.getSourceId(), source.getControllerId(), false)){ + Token token = new SoldierToken(); + token.putOntoBattlefield(X, game, source.getSourceId(), source.getControllerId()); + return true; + } } - return false; + return false; } } From 80923dbce0745140e2cac98e3316733eb4b7212c Mon Sep 17 00:00:00 2001 From: Faxn Date: Fri, 27 Oct 2017 12:16:58 -0400 Subject: [PATCH 14/18] Clean up unused imports. --- Mage.Sets/src/mage/cards/d/DecreeOfJustice.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Mage.Sets/src/mage/cards/d/DecreeOfJustice.java b/Mage.Sets/src/mage/cards/d/DecreeOfJustice.java index 6adb06403a..b033167814 100644 --- a/Mage.Sets/src/mage/cards/d/DecreeOfJustice.java +++ b/Mage.Sets/src/mage/cards/d/DecreeOfJustice.java @@ -32,10 +32,7 @@ import mage.abilities.Ability; import mage.abilities.common.CycleTriggeredAbility; import mage.abilities.costs.Cost; import mage.abilities.costs.mana.GenericManaCost; -import mage.abilities.costs.mana.ManaCost; -import mage.abilities.costs.mana.ManaCosts; import mage.abilities.costs.mana.ManaCostsImpl; -import mage.abilities.costs.mana.VariableManaCost; import mage.abilities.dynamicvalue.common.ManacostVariableValue; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.CreateTokenEffect; @@ -109,6 +106,6 @@ class DecreeOfJusticeCycleEffect extends OneShotEffect { return true; } } - return false; + return false; } } From c395038fce0ef253d5cf3374f0d9c522f23f605c Mon Sep 17 00:00:00 2001 From: LevelX2 Date: Thu, 2 Nov 2017 11:17:14 +0100 Subject: [PATCH 15/18] Minor changes. --- .gitignore | 1 + Mage.Sets/src/mage/cards/w/WitchEngine.java | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index c14e808037..6c4a378136 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,7 @@ Mage.Server.Plugins/Mage.Game.CommanderDuel/target Mage.Server.Plugins/Mage.Game.CommanderFreeForAll/target/ Mage.Server.Plugins/Mage.Game.FreeForAll/target Mage.Server.Plugins/Mage.Game.MomirDuel/target +Mage.Server.Plugins/Mage.Game.MomirGame/target/ Mage.Server.Plugins/Mage.Game.PennyDreadfulCommanderFreeForAll/target Mage.Server.Plugins/Mage.Game.TinyLeadersDuel/target Mage.Server.Plugins/Mage.Game.TwoPlayerDuel/target diff --git a/Mage.Sets/src/mage/cards/w/WitchEngine.java b/Mage.Sets/src/mage/cards/w/WitchEngine.java index 2d5659e4d3..253c2e544f 100644 --- a/Mage.Sets/src/mage/cards/w/WitchEngine.java +++ b/Mage.Sets/src/mage/cards/w/WitchEngine.java @@ -39,11 +39,11 @@ import mage.abilities.keyword.SwampwalkAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.SubType; import mage.constants.Duration; import mage.constants.Layer; import mage.constants.Outcome; import mage.constants.SubLayer; +import mage.constants.SubType; import mage.constants.Zone; import mage.game.Game; import mage.game.permanent.Permanent; @@ -56,15 +56,15 @@ import mage.target.common.TargetOpponent; public class WitchEngine extends CardImpl { public WitchEngine(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{5}{B}"); + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{5}{B}"); this.subtype.add(SubType.HORROR); this.power = new MageInt(4); this.toughness = new MageInt(4); // Swampwalk this.addAbility(new SwampwalkAbility()); - - // {tap}: Add {B}{B}{B}{B} to your mana pool. Target opponent gains control of Witch Engine. + + // {tap}: Add {B}{B}{B}{B} to your mana pool. Target opponent gains control of Witch Engine. (Activate this ability only any time you could cast an instant.) Ability ability = new SimpleActivatedAbility(Zone.BATTLEFIELD, new BasicManaEffect(Mana.BlackMana(4)), new TapSourceCost()); ability.addEffect(new WitchEngineEffect()); ability.addTarget(new TargetOpponent()); From 02b97aa9b3d2b5892931651dcdde19e766d636c2 Mon Sep 17 00:00:00 2001 From: LevelX2 Date: Thu, 2 Nov 2017 13:43:49 +0100 Subject: [PATCH 16/18] Some fixes to: Merge pull request #4129 from jerekwilson/master - Implementing 5 cards --- .../src/mage/cards/d/DeadIronSledge.java | 60 ++---- .../src/mage/cards/d/DecreeOfJustice.java | 19 +- Mage.Sets/src/mage/cards/i/IceFloe.java | 8 +- Mage.Sets/src/mage/cards/j/JaggedPoppet.java | 49 ++--- Mage.Sets/src/mage/cards/r/RhysticCave.java | 191 ++++++++++-------- Mage.Sets/src/mage/cards/t/TalonOfPain.java | 104 ++++++++-- Mage.Sets/src/mage/cards/w/WitchEngine.java | 2 +- .../DoUnlessAnyPlayerPaysManaEffect.java | 94 +++++++++ 8 files changed, 336 insertions(+), 191 deletions(-) create mode 100644 Mage/src/main/java/mage/abilities/effects/common/DoUnlessAnyPlayerPaysManaEffect.java diff --git a/Mage.Sets/src/mage/cards/d/DeadIronSledge.java b/Mage.Sets/src/mage/cards/d/DeadIronSledge.java index 11b2792013..a93cad40ce 100644 --- a/Mage.Sets/src/mage/cards/d/DeadIronSledge.java +++ b/Mage.Sets/src/mage/cards/d/DeadIronSledge.java @@ -35,42 +35,20 @@ import java.util.UUID; import mage.abilities.Ability; import mage.abilities.TriggeredAbility; import mage.abilities.TriggeredAbilityImpl; -import mage.abilities.common.BlocksAttachedTriggeredAbility; -import mage.abilities.common.BlocksOrBecomesBlockedTriggeredAbility; -import mage.abilities.common.SimpleStaticAbility; import mage.abilities.costs.mana.GenericManaCost; -import mage.abilities.effects.Effect; import mage.abilities.effects.OneShotEffect; -import mage.abilities.effects.common.DestroyAllEffect; -import mage.abilities.effects.common.DestroySourceEffect; -import mage.abilities.effects.common.DestroyTargetEffect; -import mage.abilities.effects.common.continuous.GainAbilityAttachedEffect; import mage.abilities.keyword.EquipAbility; -import mage.abilities.keyword.FirstStrikeAbility; -import mage.abilities.keyword.TrampleAbility; -import mage.constants.SubType; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.AttachmentType; import mage.constants.CardType; -import mage.constants.Duration; import mage.constants.Outcome; +import mage.constants.SubType; import mage.constants.Zone; -import mage.filter.common.FilterCreaturePermanent; -import mage.filter.predicate.Predicates; -import mage.filter.predicate.permanent.BlockedByIdPredicate; -import mage.filter.predicate.permanent.BlockingAttackerIdPredicate; -import mage.filter.predicate.permanent.PermanentIdPredicate; import mage.game.Game; import mage.game.combat.CombatGroup; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; -import mage.players.Player; -import mage.target.common.TargetCreaturePermanent; -import mage.target.targetpointer.FirstTargetPointer; -import mage.target.targetpointer.FixedTarget; import mage.target.targetpointer.FixedTargets; -import mage.util.CardUtil; /** * @@ -80,13 +58,11 @@ public class DeadIronSledge extends CardImpl { public DeadIronSledge(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{1}"); - this.subtype.add(SubType.EQUIPMENT); // Whenever equipped creature blocks or becomes blocked by a creature, destroy both creatures. this.addAbility(new DeadIronSledgeTriggeredAbility()); - - + // Equip {2} this.addAbility(new EquipAbility(Outcome.AddAbility, new GenericManaCost(2))); } @@ -99,12 +75,13 @@ public class DeadIronSledge extends CardImpl { public DeadIronSledge copy() { return new DeadIronSledge(this); } - + } + class DeadIronSledgeTriggeredAbility extends TriggeredAbilityImpl { private Set possibleTargets = new HashSet<>(); - + DeadIronSledgeTriggeredAbility() { super(Zone.BATTLEFIELD, new DeadIronSledgeDestroyEffect(), false); } @@ -112,7 +89,7 @@ class DeadIronSledgeTriggeredAbility extends TriggeredAbilityImpl { DeadIronSledgeTriggeredAbility(final DeadIronSledgeTriggeredAbility ability) { super(ability); } - + @Override public boolean checkEventType(GameEvent event, Game game) { return event.getType() == GameEvent.EventType.DECLARED_BLOCKERS; @@ -126,35 +103,32 @@ class DeadIronSledgeTriggeredAbility extends TriggeredAbilityImpl { Permanent equippedPermanent = game.getPermanentOrLKIBattlefield((equipment.getAttachedTo())); if (equippedPermanent != null) { possibleTargets.clear(); - if(equippedPermanent.isBlocked(game)){ + if (equippedPermanent.isBlocked(game)) { possibleTargets.add(equippedPermanent.getId()); //add equipped creature to target list } - String targetName = ""; if (equippedPermanent.isAttacking()) { for (CombatGroup group : game.getCombat().getGroups()) { if (group.getAttackers().contains(equippedPermanent.getId())) { possibleTargets.addAll(group.getBlockers()); } } - targetName = "a creature blocking attacker "; } else if (equippedPermanent.getBlocking() > 0) { for (CombatGroup group : game.getCombat().getGroups()) { if (group.getBlockers().contains(equippedPermanent.getId())) { possibleTargets.addAll(group.getAttackers()); } } - targetName = "a creature blocked by creature "; } if (!possibleTargets.isEmpty()) { this.getTargets().clear(); - + for (UUID creatureId : possibleTargets) { Permanent target = game.getPermanentOrLKIBattlefield(creatureId); targetPermanents.add(target); } - - this.getEffects().get(0).setTargetPointer(new FixedTargets(targetPermanents,game)); - + + this.getEffects().get(0).setTargetPointer(new FixedTargets(targetPermanents, game)); + return true; } } @@ -166,7 +140,7 @@ class DeadIronSledgeTriggeredAbility extends TriggeredAbilityImpl { public TriggeredAbility copy() { return new DeadIronSledgeTriggeredAbility(this); } - + @Override public String getRule() { return "Whenever equipped creature blocks or becomes blocked by a creature, destroy both creatures."; @@ -191,11 +165,11 @@ class DeadIronSledgeDestroyEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { - - List targets = this.getTargetPointer().getTargets(game, source); - for(UUID target: targets){ - Permanent permanent = game.getPermanentOrLKIBattlefield(target); - permanent.destroy(target, game, false); + for (UUID targetId : this.getTargetPointer().getTargets(game, source)) { + Permanent permanent = game.getPermanent(targetId); + if (permanent != null) { + permanent.destroy(targetId, game, false); + } } return true; } diff --git a/Mage.Sets/src/mage/cards/d/DecreeOfJustice.java b/Mage.Sets/src/mage/cards/d/DecreeOfJustice.java index b033167814..8b13bccdb7 100644 --- a/Mage.Sets/src/mage/cards/d/DecreeOfJustice.java +++ b/Mage.Sets/src/mage/cards/d/DecreeOfJustice.java @@ -54,15 +54,14 @@ import mage.players.Player; public class DecreeOfJustice extends CardImpl { public DecreeOfJustice(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.SORCERY},"{X}{X}{2}{W}{W}"); + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{X}{X}{2}{W}{W}"); - - // create X 4/4 white Angel creature tokens with flying. + // Create X 4/4 white Angel creature tokens with flying. this.getSpellAbility().addEffect(new CreateTokenEffect(new AngelToken(), new ManacostVariableValue())); - + // Cycling {2}{W} this.addAbility(new CyclingAbility(new ManaCostsImpl<>("{2}{W}"))); - + // When you cycle Decree of Justice, you may pay {X}. If you do, create X 1/1 white Soldier creature tokens. Ability ability = new CycleTriggeredAbility(new DecreeOfJusticeCycleEffect(), true); this.addAbility(ability); @@ -79,28 +78,28 @@ public class DecreeOfJustice extends CardImpl { } class DecreeOfJusticeCycleEffect extends OneShotEffect { - + DecreeOfJusticeCycleEffect() { super(Outcome.Benefit); this.staticText = "you may pay {X}. If you do, create X 1/1 white Soldier creature tokens"; } - + DecreeOfJusticeCycleEffect(final DecreeOfJusticeCycleEffect effect) { super(effect); } - + @Override public DecreeOfJusticeCycleEffect copy() { return new DecreeOfJusticeCycleEffect(this); } - + @Override public boolean apply(Game game, Ability source) { Player player = game.getPlayer(source.getControllerId()); if (player != null) { int X = player.announceXMana(0, Integer.MAX_VALUE, "Announce the value for {X}", game, source); Cost cost = new GenericManaCost(X); - if(cost.pay(source, game, source.getSourceId(), source.getControllerId(), false)){ + if (cost.pay(source, game, source.getSourceId(), source.getControllerId(), false)) { Token token = new SoldierToken(); token.putOntoBattlefield(X, game, source.getSourceId(), source.getControllerId()); return true; diff --git a/Mage.Sets/src/mage/cards/i/IceFloe.java b/Mage.Sets/src/mage/cards/i/IceFloe.java index 5daccfa4d8..5dff988908 100644 --- a/Mage.Sets/src/mage/cards/i/IceFloe.java +++ b/Mage.Sets/src/mage/cards/i/IceFloe.java @@ -32,7 +32,6 @@ import mage.abilities.Ability; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.common.SkipUntapOptionalAbility; import mage.abilities.costs.common.TapSourceCost; -import mage.abilities.costs.mana.GenericManaCost; import mage.abilities.effects.common.DontUntapAsLongAsSourceTappedEffect; import mage.abilities.effects.common.TapTargetEffect; import mage.abilities.keyword.FlyingAbility; @@ -47,7 +46,7 @@ import mage.target.common.TargetCreaturePermanent; /** * - + * * @author TheElk801 */ public class IceFloe extends CardImpl { @@ -64,9 +63,8 @@ public class IceFloe extends CardImpl { // You may choose not to untap Ice Floe during your untap step. this.addAbility(new SkipUntapOptionalAbility()); - // {tap}: Tap target creature without flying that's attacking you. It doesn't untap during its controller's untap step for as long as Ice Floe remains tapped. - Ability ability = new SimpleActivatedAbility(Zone.BATTLEFIELD, new TapTargetEffect(), new GenericManaCost(1)); - ability.addCost(new TapSourceCost()); + // {T}: Tap target creature without flying that's attacking you. It doesn't untap during its controller's untap step for as long as Ice Floe remains tapped. + Ability ability = new SimpleActivatedAbility(Zone.BATTLEFIELD, new TapTargetEffect(), new TapSourceCost()); ability.addTarget(new TargetCreaturePermanent(filter)); ability.addEffect(new DontUntapAsLongAsSourceTappedEffect()); this.addAbility(ability); diff --git a/Mage.Sets/src/mage/cards/j/JaggedPoppet.java b/Mage.Sets/src/mage/cards/j/JaggedPoppet.java index e8627cd335..7062dbf0e7 100644 --- a/Mage.Sets/src/mage/cards/j/JaggedPoppet.java +++ b/Mage.Sets/src/mage/cards/j/JaggedPoppet.java @@ -34,15 +34,13 @@ import mage.abilities.common.DealsCombatDamageToAPlayerTriggeredAbility; import mage.abilities.common.DealtDamageToSourceTriggeredAbility; import mage.abilities.condition.common.HellbentCondition; import mage.abilities.decorator.ConditionalTriggeredAbility; -import mage.abilities.effects.Effect; import mage.abilities.effects.OneShotEffect; -import mage.abilities.effects.common.discard.DiscardControllerEffect; -import mage.constants.SubType; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.AbilityWord; import mage.constants.CardType; import mage.constants.Outcome; +import mage.constants.SubType; import mage.constants.Zone; import mage.game.Game; import mage.players.Player; @@ -55,28 +53,23 @@ public class JaggedPoppet extends CardImpl { public JaggedPoppet(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{B}{R}"); - + this.subtype.add(SubType.OGRE); this.subtype.add(SubType.WARRIOR); this.power = new MageInt(3); this.toughness = new MageInt(4); // Whenever Jagged Poppet is dealt damage, discard that many cards. - Ability dealtDamageAbility = new DealtDamageToSourceTriggeredAbility(Zone.BATTLEFIELD, new JaggedPoppetDealtDamageEffect(), false, false, true); - this.addAbility(dealtDamageAbility); - + this.addAbility(new DealtDamageToSourceTriggeredAbility(Zone.BATTLEFIELD, new JaggedPoppetDealtDamageEffect(), false, false, true)); + // Hellbent - Whenever Jagged Poppet deals combat damage to a player, if you have no cards in hand, that player discards cards equal to the damage. Ability hellbentAbility = new ConditionalTriggeredAbility( - new DealsCombatDamageToAPlayerTriggeredAbility(new JaggedPoppetDealsDamageEffect(), false,true), - HellbentCondition.instance, + new DealsCombatDamageToAPlayerTriggeredAbility(new JaggedPoppetDealsDamageEffect(), false, true), + HellbentCondition.instance, "Hellbent - Whenever {this} deals combat damage to a player, if you have no cards in hand, that player discards cards equal to the damage."); - hellbentAbility.setAbilityWord(AbilityWord.HELLBENT); this.addAbility(hellbentAbility); - - - - + } public JaggedPoppet(final JaggedPoppet card) { @@ -89,14 +82,13 @@ public class JaggedPoppet extends CardImpl { } } - class JaggedPoppetDealsDamageEffect extends OneShotEffect { public JaggedPoppetDealsDamageEffect() { super(Outcome.Discard); //staticText = "it deals that much damage to each creature that player controls"; } - + public JaggedPoppetDealsDamageEffect(final JaggedPoppetDealsDamageEffect effect) { super(effect); } @@ -105,11 +97,11 @@ class JaggedPoppetDealsDamageEffect extends OneShotEffect { public boolean apply(Game game, Ability source) { //According to the Balefire Dragon code, This statement gets the player that was dealt the combat damage Player player = game.getPlayer(targetPointer.getFirst(game, source)); - if (player != null) { + if (player != null) { //Call the getValue method of the Effect class to retrieve the amount of damage int amount = (Integer) getValue("damage"); - - if(amount > 0){ + + if (amount > 0) { //Call the player discard function discarding cards equal to damage player.discard(amount, false, source, game); } @@ -120,43 +112,42 @@ class JaggedPoppetDealsDamageEffect extends OneShotEffect { @Override public JaggedPoppetDealsDamageEffect copy() { - return new JaggedPoppetDealsDamageEffect(this); + return new JaggedPoppetDealsDamageEffect(this); } } - class JaggedPoppetDealtDamageEffect extends OneShotEffect { public JaggedPoppetDealtDamageEffect() { super(Outcome.Discard); staticText = "discard that many cards"; } - + public JaggedPoppetDealtDamageEffect(final JaggedPoppetDealtDamageEffect effect) { super(effect); } - + @Override public boolean apply(Game game, Ability source) { - + //According to the Firedrinker Satyr code, This statement gets the player that controls Jagged Poppet Player player = game.getPlayer(source.getControllerId()); - if (player != null) { + if (player != null) { //Call the getValue method of the Effect class to retrieve the amount of damage int amount = (Integer) getValue("damage"); - - if(amount > 0){ + + if (amount > 0) { //Call the player discard function discarding cards equal to damage player.discard(amount, false, source, game); } return true; } return false; - + } @Override public JaggedPoppetDealtDamageEffect copy() { return new JaggedPoppetDealtDamageEffect(this); } -} \ No newline at end of file +} diff --git a/Mage.Sets/src/mage/cards/r/RhysticCave.java b/Mage.Sets/src/mage/cards/r/RhysticCave.java index 88362d5905..6ce08e5651 100644 --- a/Mage.Sets/src/mage/cards/r/RhysticCave.java +++ b/Mage.Sets/src/mage/cards/r/RhysticCave.java @@ -30,26 +30,21 @@ package mage.cards.r; import java.util.UUID; import mage.MageObject; import mage.Mana; -import mage.ObjectColor; import mage.abilities.Ability; -import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.Cost; import mage.abilities.costs.common.TapSourceCost; import mage.abilities.costs.mana.GenericManaCost; -import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.Effect; -import mage.abilities.effects.common.ChooseColorEffect; -import mage.abilities.effects.common.DoUnlessAnyPlayerPaysEffect; +import mage.abilities.effects.common.BasicManaEffect; +import mage.abilities.effects.common.DoUnlessAnyPlayerPaysManaEffect; import mage.abilities.effects.common.ManaEffect; -import mage.abilities.effects.common.continuous.BoostSourceEffect; +import mage.abilities.mana.ActivatedManaAbilityImpl; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.choices.ChoiceColor; import mage.constants.CardType; -import mage.constants.Duration; -import mage.constants.Outcome; import mage.constants.Zone; import mage.game.Game; -import mage.game.permanent.Permanent; import mage.players.Player; /** @@ -61,16 +56,8 @@ public class RhysticCave extends CardImpl { public RhysticCave(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.LAND}, ""); - // {tap}: Choose a color. - Ability ability = new SimpleActivatedAbility(Zone.BATTLEFIELD, - new ChooseColorEffect(Outcome.PutManaInPool), - new TapSourceCost()); - - // Add one mana of that color to your mana pool unless any player pays {1}. Activate this ability only any time you could cast an instant. - ability.addEffect(new DoUnlessAnyPlayerPaysEffect(new RhysticCaveManaEffect(),new GenericManaCost(1))); - - this.addAbility(ability); - + // {T}: Choose a color. Add one mana of that color to your mana pool unless any player pays {1}. Activate this ability only any time you could cast an instant. + this.addAbility(new RhysticCaveManaAbility()); } public RhysticCave(final RhysticCave card) { @@ -81,69 +68,105 @@ public class RhysticCave extends CardImpl { public RhysticCave copy() { return new RhysticCave(this); } - - - class RhysticCaveManaEffect extends ManaEffect { - - private final Mana chosenMana; - - public RhysticCaveManaEffect() { - super(); - chosenMana = new Mana(); - this.staticText = "Add one mana of that color to your mana pool "; - } - - public RhysticCaveManaEffect(final RhysticCaveManaEffect effect) { - super(effect); - this.chosenMana = effect.chosenMana.copy(); - } - - @Override - public Mana getMana(Game game, Ability source) { - return null; - } - - @Override - public boolean apply(Game game, Ability source) { - Player controller = game.getPlayer(source.getControllerId()); - MageObject mageObject = game.getPermanentOrLKIBattlefield(source.getSourceId()); //get obj reference to Rhystic Cave - if (controller != null) { - if (mageObject != null) { - ObjectColor choice = (ObjectColor) game.getState().getValue(mageObject.getId()+"_color"); - if (choice!= null) { - String color = choice.toString(); - switch (color) { - case "R": - chosenMana.setRed(1); - break; - case "U": - chosenMana.setBlue(1); - break; - case "W": - chosenMana.setWhite(1); - break; - case "B": - chosenMana.setBlack(1); - break; - case "G": - chosenMana.setGreen(1); - break; - } - } - checkToFirePossibleEvents(chosenMana, game, source); - controller.getManaPool().addMana(chosenMana, game, source); - return true; - } - - } - - return false; - } - - @Override - public Effect copy() { - return new RhysticCaveManaEffect(this); - } - } - +} + +class RhysticCaveManaAbility extends ActivatedManaAbilityImpl { + + public RhysticCaveManaAbility() { + super(Zone.BATTLEFIELD, new DoUnlessAnyPlayerPaysManaEffect(new RhysticCaveManaEffect(), new GenericManaCost(1), "Pay {1} to prevent mana adding from {this}."), new TapSourceCost()); + this.netMana.add(new Mana(0, 0, 0, 0, 0, 0, 1, 0)); + this.setUndoPossible(false); + } + + public RhysticCaveManaAbility(Zone zone, Mana mana, Cost cost) { + super(zone, new BasicManaEffect(mana), cost); + + } + + public RhysticCaveManaAbility(final RhysticCaveManaAbility ability) { + super(ability); + } + + @Override + public boolean canActivate(UUID playerId, Game game) { + Player player = game.getPlayer(playerId); + if (player != null && !player.isInPayManaMode()) { + return super.canActivate(playerId, game); + } + return false; + } + + @Override + public RhysticCaveManaAbility copy() { + return new RhysticCaveManaAbility(this); + } + + @Override + public String getRule() { + return super.getRule() + " Activate this ability only any time you could cast an instant."; + } +} + +class RhysticCaveManaEffect extends ManaEffect { + + private final Mana chosenMana; + + public RhysticCaveManaEffect() { + super(); + chosenMana = new Mana(); + this.staticText = "Choose a color. Add one mana of that color to your mana pool "; + } + + public RhysticCaveManaEffect(final RhysticCaveManaEffect effect) { + super(effect); + this.chosenMana = effect.chosenMana.copy(); + } + + @Override + public Mana getMana(Game game, Ability source) { + return null; + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + MageObject mageObject = game.getPermanentOrLKIBattlefield(source.getSourceId()); //get obj reference to Rhystic Cave + if (controller != null) { + if (mageObject != null) { + ChoiceColor choice = new ChoiceColor(true); + controller.choose(outcome, choice, game); + if (choice.getColor() != null) { + String color = choice.getColor().toString(); + switch (color) { + case "R": + chosenMana.setRed(1); + break; + case "U": + chosenMana.setBlue(1); + break; + case "W": + chosenMana.setWhite(1); + break; + case "B": + chosenMana.setBlack(1); + break; + case "G": + chosenMana.setGreen(1); + break; + } + } + checkToFirePossibleEvents(chosenMana, game, source); + controller.getManaPool().addMana(chosenMana, game, source); + return true; + } + + } + + return false; + } + + @Override + public Effect copy() { + return new RhysticCaveManaEffect(this); + } } diff --git a/Mage.Sets/src/mage/cards/t/TalonOfPain.java b/Mage.Sets/src/mage/cards/t/TalonOfPain.java index 6cdcfb3dc6..f9ddad3377 100644 --- a/Mage.Sets/src/mage/cards/t/TalonOfPain.java +++ b/Mage.Sets/src/mage/cards/t/TalonOfPain.java @@ -27,23 +27,23 @@ */ package mage.cards.t; -import java.util.Objects; import java.util.UUID; import mage.abilities.Ability; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.common.SimpleActivatedAbility; -import mage.abilities.costs.common.RemoveVariableCountersSourceCost; +import mage.abilities.costs.Cost; +import mage.abilities.costs.VariableCostImpl; +import mage.abilities.costs.common.RemoveCountersSourceCost; import mage.abilities.costs.common.TapSourceCost; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.dynamicvalue.common.ManacostVariableValue; -import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.DamageTargetEffect; import mage.abilities.effects.common.counter.AddCountersSourceEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.Outcome; import mage.constants.Zone; +import mage.counters.Counter; import mage.counters.CounterType; import mage.game.Game; import mage.game.events.GameEvent; @@ -59,23 +59,21 @@ public class TalonOfPain extends CardImpl { public TalonOfPain(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{4}"); - + /* - * Whenever a source you control other than Talon of Pain deals damage to an opponent, + * Whenever a source you control other than Talon of Pain deals damage to an opponent, * put a charge counter on Talon of Pain. */ this.addAbility(new TalonOfPainTriggeredAbility()); - - - // {X}, {tap}, Remove X charge counters from Talon of Pain: Talon of Pain deals X damage to target creature or player. - Ability ability = new SimpleActivatedAbility(Zone.BATTLEFIELD, new DamageTargetEffect(new ManacostVariableValue()) , new ManaCostsImpl("{X}")); + + // {X}, {T}, Remove X charge counters from Talon of Pain: Talon of Pain deals X damage to target creature or player. + Ability ability = new SimpleActivatedAbility(Zone.BATTLEFIELD, new DamageTargetEffect(new ManacostVariableValue()), new ManaCostsImpl("{X}")); ability.addCost(new TapSourceCost()); - ability.addCost(new RemoveVariableCountersSourceCost(CounterType.CHARGE.createInstance())); + ability.addCost(new TalonOfPainRemoveVariableCountersSourceCost(CounterType.CHARGE.createInstance())); ability.addTarget(new TargetCreatureOrPlayer()); this.addAbility(ability); - - + } public TalonOfPain(final TalonOfPain card) { @@ -86,7 +84,7 @@ public class TalonOfPain extends CardImpl { public TalonOfPain copy() { return new TalonOfPain(this); } - + private class TalonOfPainTriggeredAbility extends TriggeredAbilityImpl { public TalonOfPainTriggeredAbility() { @@ -111,13 +109,15 @@ public class TalonOfPain extends CardImpl { public boolean checkTrigger(GameEvent event, Game game) { // to another player Player controller = game.getPlayer(this.getControllerId()); - if(controller==null){return false;} - if(controller.hasOpponent(event.getTargetId(),game)){ + if (controller == null) { + return false; + } + if (controller.hasOpponent(event.getTargetId(), game)) { // a source you control other than Talon of Pain UUID sourceControllerId = game.getControllerId(event.getSourceId()); - if (sourceControllerId != null - && sourceControllerId.equals(this.getControllerId()) - && this.getSourceId() != event.getSourceId() ) { + if (sourceControllerId != null + && sourceControllerId.equals(this.getControllerId()) + && this.getSourceId() != event.getSourceId()) { // return true so the effect will fire and a charge counter will be added return true; } @@ -131,3 +131,69 @@ public class TalonOfPain extends CardImpl { } } } + +class TalonOfPainRemoveVariableCountersSourceCost extends VariableCostImpl { + + protected int minimalCountersToPay = 0; + private String counterName; + + public TalonOfPainRemoveVariableCountersSourceCost(Counter counter) { + this(counter, 0); + } + + public TalonOfPainRemoveVariableCountersSourceCost(Counter counter, String text) { + this(counter, 0, text); + } + + public TalonOfPainRemoveVariableCountersSourceCost(Counter counter, int minimalCountersToPay) { + this(counter, minimalCountersToPay, ""); + } + + public TalonOfPainRemoveVariableCountersSourceCost(Counter counter, int minimalCountersToPay, String text) { + super(counter.getName() + " counters to remove"); + this.minimalCountersToPay = minimalCountersToPay; + this.counterName = counter.getName(); + if (text == null || text.isEmpty()) { + this.text = "Remove X " + counterName + " counters from {this}"; + } else { + this.text = text; + } + } + + public TalonOfPainRemoveVariableCountersSourceCost(final TalonOfPainRemoveVariableCountersSourceCost cost) { + super(cost); + this.minimalCountersToPay = cost.minimalCountersToPay; + this.counterName = cost.counterName; + } + + @Override + public TalonOfPainRemoveVariableCountersSourceCost copy() { + return new TalonOfPainRemoveVariableCountersSourceCost(this); + } + + @Override + public Cost getFixedCostsFromAnnouncedValue(int xValue) { + return new RemoveCountersSourceCost(new Counter(counterName, xValue)); + } + + @Override + public int getMinValue(Ability source, Game game) { + return minimalCountersToPay; + } + + @Override + public int getMaxValue(Ability source, Game game) { + int maxValue = 0; + Permanent permanent = game.getPermanent(source.getSourceId()); + if (permanent != null) { + maxValue = permanent.getCounters(game).getCount(counterName); + } + return maxValue; + } + + @Override + public int announceXValue(Ability source, Game game) { + return source.getManaCostsToPay().getX(); + } + +} diff --git a/Mage.Sets/src/mage/cards/w/WitchEngine.java b/Mage.Sets/src/mage/cards/w/WitchEngine.java index 253c2e544f..51f61adebe 100644 --- a/Mage.Sets/src/mage/cards/w/WitchEngine.java +++ b/Mage.Sets/src/mage/cards/w/WitchEngine.java @@ -64,7 +64,7 @@ public class WitchEngine extends CardImpl { // Swampwalk this.addAbility(new SwampwalkAbility()); - // {tap}: Add {B}{B}{B}{B} to your mana pool. Target opponent gains control of Witch Engine. (Activate this ability only any time you could cast an instant.) + // {T}: Add {B}{B}{B}{B} to your mana pool. Target opponent gains control of Witch Engine. (Activate this ability only any time you could cast an instant.) Ability ability = new SimpleActivatedAbility(Zone.BATTLEFIELD, new BasicManaEffect(Mana.BlackMana(4)), new TapSourceCost()); ability.addEffect(new WitchEngineEffect()); ability.addTarget(new TargetOpponent()); diff --git a/Mage/src/main/java/mage/abilities/effects/common/DoUnlessAnyPlayerPaysManaEffect.java b/Mage/src/main/java/mage/abilities/effects/common/DoUnlessAnyPlayerPaysManaEffect.java new file mode 100644 index 0000000000..2917742425 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/effects/common/DoUnlessAnyPlayerPaysManaEffect.java @@ -0,0 +1,94 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package mage.abilities.effects.common; + +import java.util.UUID; +import mage.MageObject; +import mage.Mana; +import mage.abilities.Ability; +import mage.abilities.Mode; +import mage.abilities.costs.Cost; +import mage.constants.Outcome; +import mage.game.Game; +import mage.players.Player; +import mage.util.CardUtil; + +/** + * + * @author LevelX2 + */ +public class DoUnlessAnyPlayerPaysManaEffect extends ManaEffect { + + private final ManaEffect manaEffect; + private final Cost cost; + private final String chooseUseText; + + public DoUnlessAnyPlayerPaysManaEffect(ManaEffect effect, Cost cost, String chooseUseText) { + this.manaEffect = effect; + this.cost = cost; + this.chooseUseText = chooseUseText; + } + + public DoUnlessAnyPlayerPaysManaEffect(final DoUnlessAnyPlayerPaysManaEffect effect) { + super(effect); + this.manaEffect = (ManaEffect) effect.manaEffect.copy(); + this.cost = effect.cost.copy(); + this.chooseUseText = effect.chooseUseText; + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + MageObject sourceObject = game.getObject(source.getSourceId()); + if (controller != null && sourceObject != null) { + String message = CardUtil.replaceSourceName(chooseUseText, sourceObject.getName()); + boolean result = true; + boolean doEffect = true; + // check if any player is willing to pay + for (UUID playerId : game.getState().getPlayersInRange(controller.getId(), game)) { + Player player = game.getPlayer(playerId); + if (player != null && cost.canPay(source, source.getSourceId(), player.getId(), game) && player.chooseUse(Outcome.Detriment, message, source, game)) { + cost.clearPaid(); + if (cost.pay(source, game, source.getSourceId(), player.getId(), false, null)) { + if (!game.isSimulation()) { + game.informPlayers(player.getLogName() + " pays the cost to prevent the effect"); + } + doEffect = false; + } + } + } + // do the effects if nobody paid + if (doEffect) { + return manaEffect.apply(game, source); + } + return result; + } + return false; + } + + protected Player getPayingPlayer(Game game, Ability source) { + return game.getPlayer(source.getControllerId()); + } + + @Override + public String getText(Mode mode) { + if (!staticText.isEmpty()) { + return staticText; + } + return manaEffect.getText(mode) + " unless any player pays " + cost.getText(); + } + + @Override + public Mana getMana(Game game, Ability source) { + return manaEffect.getMana(game, source); + } + + @Override + public ManaEffect copy() { + return new DoUnlessAnyPlayerPaysManaEffect(this); + } + +} From 9e4beb6b519a5f923176ef2b537fb52c1536afec Mon Sep 17 00:00:00 2001 From: LevelX2 Date: Fri, 3 Nov 2017 14:59:26 +0100 Subject: [PATCH 17/18] * Nettling Impl - Fixed that the conditional delayed destroy ability did not work corretly (fixes #4142). --- Mage.Sets/src/mage/cards/n/NettlingImp.java | 56 +++++++++++++---- .../triggers/delayed/NettlingImplTest.java | 63 +++++++++++++++++++ ...nOfNextEndStepDelayedTriggeredAbility.java | 30 ++++++--- 3 files changed, 131 insertions(+), 18 deletions(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/triggers/delayed/NettlingImplTest.java diff --git a/Mage.Sets/src/mage/cards/n/NettlingImp.java b/Mage.Sets/src/mage/cards/n/NettlingImp.java index bc84103208..7e410528d8 100644 --- a/Mage.Sets/src/mage/cards/n/NettlingImp.java +++ b/Mage.Sets/src/mage/cards/n/NettlingImp.java @@ -30,13 +30,14 @@ package mage.cards.n; import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; +import mage.abilities.common.delayed.AtTheBeginOfNextEndStepDelayedTriggeredAbility; import mage.abilities.condition.Condition; import mage.abilities.condition.InvertCondition; import mage.abilities.condition.common.TargetAttackedThisTurnCondition; import mage.abilities.costs.common.TapSourceCost; import mage.abilities.decorator.ConditionalActivatedAbility; -import mage.abilities.decorator.ConditionalOneShotEffect; -import mage.abilities.effects.common.DestroyTargetAtBeginningOfNextEndStepEffect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.DestroyTargetEffect; import mage.abilities.effects.common.combat.AttacksIfAbleTargetEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -49,6 +50,7 @@ import mage.filter.predicate.permanent.ControllerPredicate; import mage.game.Game; import mage.players.Player; import mage.target.common.TargetCreaturePermanent; +import mage.target.targetpointer.FixedTarget; import mage.watchers.common.AttackedThisTurnWatcher; /** @@ -56,30 +58,33 @@ import mage.watchers.common.AttackedThisTurnWatcher; * @author MTGfan */ public class NettlingImp extends CardImpl { - + final static FilterCreaturePermanent filter = new FilterCreaturePermanent("non-Wall"); static { filter.add(Predicates.not(new SubtypePredicate(SubType.WALL))); - filter.add(new ControlledFromStartOfControllerTurnPredicate()); + filter.add(new ControlledFromStartOfControllerTurnPredicate()); filter.add(new ControllerPredicate(TargetController.ACTIVE)); filter.setMessage("non-Wall creature the active player has controlled continuously since the beginning of the turn."); } - public NettlingImp(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{B}"); - + this.subtype.add(SubType.IMP); this.power = new MageInt(1); this.toughness = new MageInt(1); - // {tap}: Choose target non-Wall creature the active player has controlled continuously since the beginning of the turn. That creature attacks this turn if able. If it doesn't, destroy it at the beginning of the next end step. Activate this ability only during an opponent's turn, before attackers are declared. - Ability ability = new ConditionalActivatedAbility(Zone.BATTLEFIELD, new AttacksIfAbleTargetEffect(Duration.EndOfTurn), new TapSourceCost(), new NettlingImpTurnCondition(), "{T}: Choose target non-Wall creature the active player has controlled continuously since the beginning of the turn. That creature attacks this turn if able. If it doesn't, destroy it at the beginning of the next end step. Activate this ability only during an opponent's turn, before attackers are declared."); - ability.addEffect(new ConditionalOneShotEffect(new DestroyTargetAtBeginningOfNextEndStepEffect(), new InvertCondition(TargetAttackedThisTurnCondition.instance))); + // {T}: Choose target non-Wall creature the active player has controlled continuously since the beginning of the turn. That creature attacks this turn if able. If it doesn't, destroy it at the beginning of the next end step. Activate this ability only during an opponent's turn, before attackers are declared. + Ability ability = new ConditionalActivatedAbility(Zone.BATTLEFIELD, new AttacksIfAbleTargetEffect(Duration.EndOfTurn), + new TapSourceCost(), new NettlingImpTurnCondition(), + "{T}: Choose target non-Wall creature the active player has controlled continuously since the beginning of the turn. " + + "That creature attacks this turn if able. If it doesn't, destroy it at the beginning of the next end step. " + + "Activate this ability only during an opponent's turn, before attackers are declared."); + ability.addEffect(new NettlingImpDelayedDestroyEffect()); ability.addTarget(new TargetCreaturePermanent(filter)); this.addAbility(ability, new AttackedThisTurnWatcher()); - + } public NettlingImp(final NettlingImp card) { @@ -96,7 +101,7 @@ class NettlingImpTurnCondition implements Condition { @Override public boolean apply(Game game, Ability source) { - Player activePlayer = game.getPlayer(game.getActivePlayerId()); + Player activePlayer = game.getPlayer(game.getActivePlayerId()); return activePlayer != null && activePlayer.hasOpponent(source.getControllerId(), game) && game.getPhase().getStep().getType().getIndex() < 5; } @@ -105,3 +110,32 @@ class NettlingImpTurnCondition implements Condition { return ""; } } + +class NettlingImpDelayedDestroyEffect extends OneShotEffect { + + public NettlingImpDelayedDestroyEffect() { + super(Outcome.Detriment); + this.staticText = "If it doesn't, destroy it at the beginning of the next end step"; + } + + public NettlingImpDelayedDestroyEffect(final NettlingImpDelayedDestroyEffect effect) { + super(effect); + } + + @Override + public NettlingImpDelayedDestroyEffect copy() { + return new NettlingImpDelayedDestroyEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + DestroyTargetEffect effect = new DestroyTargetEffect(); + effect.setTargetPointer(new FixedTarget(source.getFirstTarget())); + AtTheBeginOfNextEndStepDelayedTriggeredAbility delayedAbility + = new AtTheBeginOfNextEndStepDelayedTriggeredAbility(Zone.ALL, effect, TargetController.ANY, new InvertCondition(TargetAttackedThisTurnCondition.instance)); + delayedAbility.getDuration(); + delayedAbility.getTargets().addAll(source.getTargets()); + game.addDelayedTriggeredAbility(delayedAbility, source); + return true; + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/triggers/delayed/NettlingImplTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/triggers/delayed/NettlingImplTest.java new file mode 100644 index 0000000000..c2192aadd1 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/triggers/delayed/NettlingImplTest.java @@ -0,0 +1,63 @@ +/* + * 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 org.mage.test.cards.triggers.delayed; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author LevelX2 + */ +public class NettlingImplTest extends CardTestPlayerBase { + + @Test + public void testForcedDidAttack() { + addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1); + + // {T}: Choose target non-Wall creature the active player has controlled continuously since the beginning of the turn. That creature attacks this turn if able. If it doesn't, destroy it at the beginning of the next end step. Activate this ability only during an opponent's turn, before attackers are declared. + addCard(Zone.BATTLEFIELD, playerA, "Nettling Imp", 1); + + activateAbility(2, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Choose", "Silvercoat Lion"); + + setStopAt(5, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertPermanentCount(playerA, "Nettling Imp", 1); + assertPermanentCount(playerB, "Silvercoat Lion", 1); + + assertLife(playerA, 18); + assertLife(playerB, 20); + } + + @Test + public void testForcedDidNotAttack() { + addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1); + + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); + // {T}: Choose target non-Wall creature the active player has controlled continuously since the beginning of the turn. That creature attacks this turn if able. If it doesn't, destroy it at the beginning of the next end step. Activate this ability only during an opponent's turn, before attackers are declared. + addCard(Zone.BATTLEFIELD, playerA, "Nettling Imp", 1); + // Tap target creature. + // Draw a card. + addCard(Zone.HAND, playerA, "Pressure Point", 1); // Instant {1}{W} + + activateAbility(2, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Choose", "Silvercoat Lion"); + castSpell(2, PhaseStep.BEGIN_COMBAT, playerA, "Pressure Point", "Silvercoat Lion"); + setStopAt(3, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertPermanentCount(playerA, "Nettling Imp", 1); + assertGraveyardCount(playerA, "Pressure Point", 1); + assertPermanentCount(playerB, "Silvercoat Lion", 0); + + assertHandCount(playerA, 2); + assertLife(playerA, 20); + assertLife(playerB, 20); + } + +} diff --git a/Mage/src/main/java/mage/abilities/common/delayed/AtTheBeginOfNextEndStepDelayedTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/delayed/AtTheBeginOfNextEndStepDelayedTriggeredAbility.java index 4f16e72e8d..3b533fa098 100644 --- a/Mage/src/main/java/mage/abilities/common/delayed/AtTheBeginOfNextEndStepDelayedTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/delayed/AtTheBeginOfNextEndStepDelayedTriggeredAbility.java @@ -28,7 +28,9 @@ package mage.abilities.common.delayed; import mage.abilities.DelayedTriggeredAbility; +import mage.abilities.condition.Condition; import mage.abilities.effects.Effect; +import mage.constants.Duration; import mage.constants.TargetController; import mage.constants.Zone; import mage.game.Game; @@ -42,6 +44,7 @@ import mage.game.permanent.Permanent; public class AtTheBeginOfNextEndStepDelayedTriggeredAbility extends DelayedTriggeredAbility { private TargetController targetController; + private Condition condition; public AtTheBeginOfNextEndStepDelayedTriggeredAbility(Effect effect) { this(effect, TargetController.ANY); @@ -52,14 +55,20 @@ public class AtTheBeginOfNextEndStepDelayedTriggeredAbility extends DelayedTrigg } public AtTheBeginOfNextEndStepDelayedTriggeredAbility(Zone zone, Effect effect, TargetController targetController) { - super(effect); + this(zone, effect, targetController, null); + } + + public AtTheBeginOfNextEndStepDelayedTriggeredAbility(Zone zone, Effect effect, TargetController targetController, Condition condition) { + super(effect, Duration.EndOfTurn); this.zone = zone; this.targetController = targetController; + this.condition = condition; } public AtTheBeginOfNextEndStepDelayedTriggeredAbility(final AtTheBeginOfNextEndStepDelayedTriggeredAbility ability) { super(ability); this.targetController = ability.targetController; + this.condition = ability.condition; } @Override @@ -69,27 +78,34 @@ public class AtTheBeginOfNextEndStepDelayedTriggeredAbility extends DelayedTrigg @Override public boolean checkTrigger(GameEvent event, Game game) { + boolean correctEndPhase = false; switch (targetController) { case ANY: - return true; + correctEndPhase = true; + break; case YOU: - return event.getPlayerId().equals(this.controllerId); - + correctEndPhase = event.getPlayerId().equals(this.controllerId); + break; case OPPONENT: if (game.getPlayer(this.getControllerId()).hasOpponent(event.getPlayerId(), game)) { - return true; + correctEndPhase = true; } break; - case CONTROLLER_ATTACHED_TO: Permanent attachment = game.getPermanent(sourceId); if (attachment != null && attachment.getAttachedTo() != null) { Permanent attachedTo = game.getPermanent(attachment.getAttachedTo()); if (attachedTo != null && attachedTo.getControllerId().equals(event.getPlayerId())) { - return true; + correctEndPhase = true; } } } + if (correctEndPhase) { + if (condition != null && !condition.apply(game, this)) { + return false; + } + return true; + } return false; } From 9704d21c823472906032d5b75b75a698757e618a Mon Sep 17 00:00:00 2001 From: LevelX2 Date: Fri, 3 Nov 2017 15:50:01 +0100 Subject: [PATCH 18/18] * Fixed a problem with player leaving the game during multiplayer game. --- Mage/src/main/java/mage/game/GameImpl.java | 1 + 1 file changed, 1 insertion(+) diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index d371a119ed..e181422a82 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -573,6 +573,7 @@ public abstract class GameImpl implements Game, Serializable { } } else { // no asynchronous action so check directly + concedingPlayers.add(playerId); checkConcede(); } } else {