From d7da3930b63c0c9624d8ea8feda67abc962088a5 Mon Sep 17 00:00:00 2001 From: L_J Date: Fri, 1 Jun 2018 21:13:20 +0200 Subject: [PATCH 01/11] Implemented Word of Command --- Mage.Sets/src/mage/cards/w/WordOfCommand.java | 228 ++++++++++++++++++ .../src/mage/sets/LimitedEditionAlpha.java | 1 + .../src/mage/sets/LimitedEditionBeta.java | 1 + Mage.Sets/src/mage/sets/MastersEditionIV.java | 1 + Mage.Sets/src/mage/sets/UnlimitedEdition.java | 1 + ...ControlOnOtherPlayersControllerEffect.java | 4 + Mage/src/main/java/mage/game/stack/Spell.java | 12 + 7 files changed, 248 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/w/WordOfCommand.java diff --git a/Mage.Sets/src/mage/cards/w/WordOfCommand.java b/Mage.Sets/src/mage/cards/w/WordOfCommand.java new file mode 100644 index 00000000000..53713f4c0e3 --- /dev/null +++ b/Mage.Sets/src/mage/cards/w/WordOfCommand.java @@ -0,0 +1,228 @@ +/* + * 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.w; + +import java.util.UUID; +import mage.MageObject; +import mage.MageObjectReference; +import mage.abilities.Ability; +import mage.abilities.DelayedTriggeredAbility; +import mage.abilities.effects.Effect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.RestrictionEffect; +import mage.abilities.effects.common.LoseControlOnOtherPlayersControllerEffect; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.constants.Zone; +import mage.filter.FilterCard; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.events.ZoneChangeEvent; +import mage.game.permanent.Permanent; +import mage.game.stack.Spell; +import mage.players.Player; +import mage.target.TargetCard; +import mage.target.common.TargetOpponent; +import mage.target.targetpointer.FixedTarget; + +/** + * + * @author L_J + */ +public final class WordOfCommand extends CardImpl { + + public WordOfCommand(UUID ownerId, CardSetInfo setInfo) { + super(ownerId,setInfo,new CardType[]{CardType.INSTANT},"{B}{B}"); + + // Look at target opponent's hand and choose a card from it. You control that player until Word of Command finishes resolving. The player plays that card if able. While doing so, the player can activate mana abilities only if they're from lands that player controls and only if mana they produce is spent to activate other mana abilities of lands the player controls and/or to play that card. If the chosen card is cast as a spell, you control the player while that spell is resolving. + this.getSpellAbility().addEffect(new WordOfCommandEffect()); + this.getSpellAbility().addTarget(new TargetOpponent()); + } + + public WordOfCommand(final WordOfCommand card) { + super(card); + } + + @Override + public WordOfCommand copy() { + return new WordOfCommand(this); + } +} + +class WordOfCommandEffect extends OneShotEffect { + + public WordOfCommandEffect() { + super(Outcome.GainControl); + this.staticText = "Look at target opponent's hand and choose a card from it. You control that player until Word of Command finishes resolving. The player plays that card if able. While doing so, the player can activate mana abilities only if they're from lands that player controls and only if mana they produce is spent to activate other mana abilities of lands the player controls and/or to play that card. If the chosen card is cast as a spell, you control the player while that spell is resolving"; + } + + public WordOfCommandEffect(final WordOfCommandEffect effect) { + super(effect); + } + + @Override + public WordOfCommandEffect copy() { + return new WordOfCommandEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + Player targetPlayer = game.getPlayer(source.getFirstTarget()); + MageObject sourceObject = game.getObject(source.getSourceId()); + Card card = null; + if (controller != null && targetPlayer != null && sourceObject != null) { + + // Look at target opponent's hand and choose a card from it + TargetCard targetCard = new TargetCard(Zone.HAND, new FilterCard()); + if (controller.choose(Outcome.Discard, targetPlayer.getHand(), targetCard, game)) { + card = game.getCard(targetCard.getFirstTarget()); + } + + // You control that player until Word of Command finishes resolving + controller.controlPlayersTurn(game, targetPlayer.getId()); + while (controller != null && controller.canRespond()) { + if (controller.chooseUse(Outcome.Benefit, "Resolve " + sourceObject.getLogName() + " now" + (card != null ? " and play " + card.getLogName() : "") + '?', source, game)) { + // this is used to give the controller a little space to utilize his player controlling effect (look at face down creatures, hand, etc.) + break; + } + } + + // The player plays that card if able + if (card != null) { + RestrictionEffect effect = new WordOfCommandCantActivateEffect(); + effect.setTargetPointer(new FixedTarget(targetPlayer.getId())); + game.addEffect(effect, source); // While doing so, the player can activate mana abilities only if they're from lands that player controls + // TODO: and only if mana they produce is spent to activate other mana abilities of lands he or she controls and/or play that card (so you can't tap out the player's lands) + if ((card.isLand() && (!targetPlayer.canPlayLand() || !game.getActivePlayerId().equals(targetPlayer.getId()))) + || !targetPlayer.playCard(card, game, false, true, new MageObjectReference(source.getSourceObject(game), game))) { + game.informPlayers(targetPlayer.getLogName() + " didn't play " + card.getLogName()); + // TODO: needs an automatic check for whether the card is castable (so it can't be cancelled if that's the case) + } + + for (RestrictionEffect eff : game.getContinuousEffects().getRestrictionEffects()) { + if (eff instanceof WordOfCommandCantActivateEffect) { + eff.discard(); + } + } + game.getContinuousEffects().removeInactiveEffects(game); + Spell spell = game.getSpell(card.getId()); + if (spell != null) { + spell.setCommandedBy(controller.getId()); // If the chosen card is cast as a spell, you control the player while that spell is resolving + } + } + + if (sourceObject != null) { + Effect effect = new LoseControlOnOtherPlayersControllerEffect(controller.getLogName(), targetPlayer.getLogName()); + effect.setTargetPointer(new FixedTarget(targetPlayer.getId())); + // You control the player until Word of Command finishes resolving + // TODO: using a DelayedTriggeredAbility to end the effect isn't the optimal solution, since effects like Time Stop can stop it from triggering even outside the stack + DelayedTriggeredAbility ability = new WordOfCommandDelayedTriggeredAbility(effect, source.getSourceId()); + ability.setSourceId(controller.getId()); + ability.setControllerId(controller.getId()); + game.addDelayedTriggeredAbility(ability); + if (card != null && !card.isLand()) { // this sets up a lose control effect for when the spell finishes resolving + ability = new WordOfCommandDelayedTriggeredAbility(effect, card.getId()); + ability.setSourceId(controller.getId()); + ability.setControllerId(controller.getId()); + game.addDelayedTriggeredAbility(ability); + } + } else { + controller.resetOtherTurnsControlled(); + targetPlayer.setGameUnderYourControl(true); + } + return true; + } + return false; + } +} + +class WordOfCommandCantActivateEffect extends RestrictionEffect { + + public WordOfCommandCantActivateEffect() { + super(Duration.EndOfTurn); + } + + public WordOfCommandCantActivateEffect(final WordOfCommandCantActivateEffect effect) { + super(effect); + } + + @Override + public WordOfCommandCantActivateEffect copy() { + return new WordOfCommandCantActivateEffect(this); + } + + @Override + public boolean applies(Permanent permanent, Ability source, Game game) { + return !permanent.isLand() && permanent.getControllerId().equals(this.targetPointer.getFirst(game, source)); + } + + @Override + public boolean canUseActivatedAbilities(Permanent permanent, Ability source, Game game) { + return false; + } +} + +class WordOfCommandDelayedTriggeredAbility extends DelayedTriggeredAbility { + + private UUID cardId; + + WordOfCommandDelayedTriggeredAbility(Effect effect, UUID cardId) { + super(effect, Duration.EndOfStep); + this.cardId = cardId; + this.usesStack = false; + } + + WordOfCommandDelayedTriggeredAbility(final WordOfCommandDelayedTriggeredAbility ability) { + super(ability); + this.cardId = ability.cardId; + } + + @Override + public WordOfCommandDelayedTriggeredAbility copy() { + return new WordOfCommandDelayedTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.ZONE_CHANGE; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + ZoneChangeEvent zEvent = (ZoneChangeEvent) event; + if (zEvent.getFromZone() == Zone.STACK && event.getTargetId().equals(cardId)) { + return true; + } + return false; + } +} diff --git a/Mage.Sets/src/mage/sets/LimitedEditionAlpha.java b/Mage.Sets/src/mage/sets/LimitedEditionAlpha.java index 16384462e2a..3175d24d207 100644 --- a/Mage.Sets/src/mage/sets/LimitedEditionAlpha.java +++ b/Mage.Sets/src/mage/sets/LimitedEditionAlpha.java @@ -309,6 +309,7 @@ public final class LimitedEditionAlpha extends ExpansionSet { cards.add(new SetCardInfo("Will-o'-the-Wisp", 135, Rarity.RARE, mage.cards.w.WillOTheWisp.class)); cards.add(new SetCardInfo("Winter Orb", 275, Rarity.RARE, mage.cards.w.WinterOrb.class)); cards.add(new SetCardInfo("Wooden Sphere", 276, Rarity.UNCOMMON, mage.cards.w.WoodenSphere.class)); + cards.add(new SetCardInfo("Word of Command", 136, Rarity.RARE, mage.cards.w.WordOfCommand.class)); cards.add(new SetCardInfo("Wrath of God", 45, Rarity.RARE, mage.cards.w.WrathOfGod.class)); cards.add(new SetCardInfo("Zombie Master", 137, Rarity.RARE, mage.cards.z.ZombieMaster.class)); } diff --git a/Mage.Sets/src/mage/sets/LimitedEditionBeta.java b/Mage.Sets/src/mage/sets/LimitedEditionBeta.java index 4046a234975..a67c9d45eeb 100644 --- a/Mage.Sets/src/mage/sets/LimitedEditionBeta.java +++ b/Mage.Sets/src/mage/sets/LimitedEditionBeta.java @@ -316,6 +316,7 @@ public final class LimitedEditionBeta extends ExpansionSet { cards.add(new SetCardInfo("Will-o'-the-Wisp", 136, Rarity.RARE, mage.cards.w.WillOTheWisp.class)); cards.add(new SetCardInfo("Winter Orb", 276, Rarity.RARE, mage.cards.w.WinterOrb.class)); cards.add(new SetCardInfo("Wooden Sphere", 277, Rarity.UNCOMMON, mage.cards.w.WoodenSphere.class)); + cards.add(new SetCardInfo("Word of Command", 137, Rarity.RARE, mage.cards.w.WordOfCommand.class)); cards.add(new SetCardInfo("Wrath of God", 46, Rarity.RARE, mage.cards.w.WrathOfGod.class)); cards.add(new SetCardInfo("Zombie Master", 138, Rarity.RARE, mage.cards.z.ZombieMaster.class)); } diff --git a/Mage.Sets/src/mage/sets/MastersEditionIV.java b/Mage.Sets/src/mage/sets/MastersEditionIV.java index aa84aa7e5d5..e6f5137a52f 100644 --- a/Mage.Sets/src/mage/sets/MastersEditionIV.java +++ b/Mage.Sets/src/mage/sets/MastersEditionIV.java @@ -324,6 +324,7 @@ public final class MastersEditionIV extends ExpansionSet { cards.add(new SetCardInfo("Wild Griffin", 35, Rarity.COMMON, mage.cards.w.WildGriffin.class)); cards.add(new SetCardInfo("Wild Ox", 174, Rarity.UNCOMMON, mage.cards.w.WildOx.class)); cards.add(new SetCardInfo("Wood Elemental", 175, Rarity.RARE, mage.cards.w.WoodElemental.class)); + cards.add(new SetCardInfo("Word of Command", 103, Rarity.RARE, mage.cards.w.WordOfCommand.class)); cards.add(new SetCardInfo("Xenic Poltergeist", 104, Rarity.UNCOMMON, mage.cards.x.XenicPoltergeist.class)); cards.add(new SetCardInfo("Yotian Soldier", 240, Rarity.COMMON, mage.cards.y.YotianSoldier.class)); cards.add(new SetCardInfo("Zombie Master", 105, Rarity.UNCOMMON, mage.cards.z.ZombieMaster.class)); diff --git a/Mage.Sets/src/mage/sets/UnlimitedEdition.java b/Mage.Sets/src/mage/sets/UnlimitedEdition.java index c2dc591ecf0..a08da1a590d 100644 --- a/Mage.Sets/src/mage/sets/UnlimitedEdition.java +++ b/Mage.Sets/src/mage/sets/UnlimitedEdition.java @@ -316,6 +316,7 @@ public final class UnlimitedEdition extends ExpansionSet { cards.add(new SetCardInfo("Will-o'-the-Wisp", 136, Rarity.RARE, mage.cards.w.WillOTheWisp.class)); cards.add(new SetCardInfo("Winter Orb", 276, Rarity.RARE, mage.cards.w.WinterOrb.class)); cards.add(new SetCardInfo("Wooden Sphere", 277, Rarity.UNCOMMON, mage.cards.w.WoodenSphere.class)); + cards.add(new SetCardInfo("Word of Command", 137, Rarity.RARE, mage.cards.w.WordOfCommand.class)); cards.add(new SetCardInfo("Wrath of God", 46, Rarity.RARE, mage.cards.w.WrathOfGod.class)); cards.add(new SetCardInfo("Zombie Master", 138, Rarity.RARE, mage.cards.z.ZombieMaster.class)); } diff --git a/Mage/src/main/java/mage/abilities/effects/common/LoseControlOnOtherPlayersControllerEffect.java b/Mage/src/main/java/mage/abilities/effects/common/LoseControlOnOtherPlayersControllerEffect.java index ada377e8dab..6abfd75e4f2 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/LoseControlOnOtherPlayersControllerEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/LoseControlOnOtherPlayersControllerEffect.java @@ -59,6 +59,10 @@ public class LoseControlOnOtherPlayersControllerEffect extends OneShotEffect { Player player = game.getPlayer(source.getControllerId()); if (player != null) { player.resetOtherTurnsControlled(); + Player targetPlayer = game.getPlayer(this.getTargetPointer().getFirst(game, source)); + if (targetPlayer != null) { + targetPlayer.setGameUnderYourControl(true); + } return true; } return false; diff --git a/Mage/src/main/java/mage/game/stack/Spell.java b/Mage/src/main/java/mage/game/stack/Spell.java index fc2659965e5..8ed826fa4b3 100644 --- a/Mage/src/main/java/mage/game/stack/Spell.java +++ b/Mage/src/main/java/mage/game/stack/Spell.java @@ -93,6 +93,7 @@ public class Spell extends StackObjImpl implements Card { private boolean faceDown; private boolean countered; private boolean resolving = false; + private UUID commandedBy = null; // for Word of Command private boolean doneActivatingManaAbilities; // if this is true, the player is no longer allowed to pay the spell costs with activating of mana abilies @@ -147,6 +148,7 @@ public class Spell extends StackObjImpl implements Card { this.faceDown = spell.faceDown; this.countered = spell.countered; this.resolving = spell.resolving; + this.commandedBy = spell.commandedBy; this.doneActivatingManaAbilities = spell.doneActivatingManaAbilities; this.targetChanged = spell.targetChanged; @@ -205,6 +207,12 @@ public class Spell extends StackObjImpl implements Card { return false; } this.resolving = true; + if (commandedBy != null && !commandedBy.equals(getControllerId())) { + Player turnController = game.getPlayer(commandedBy); + if (turnController != null) { + turnController.controlPlayersTurn(game, controller.getId()); + } + } if (this.isInstant() || this.isSorcery()) { int index = 0; result = false; @@ -1050,4 +1058,8 @@ public class Spell extends StackObjImpl implements Card { throw new UnsupportedOperationException("Not supported."); //To change body of generated methods, choose Tools | Templates. } + public void setCommandedBy(UUID playerId) { + this.commandedBy = playerId; + } + } From 7cc313bc43a1fb80b8eacba7face877484fb001e Mon Sep 17 00:00:00 2001 From: L_J Date: Sun, 3 Jun 2018 11:20:46 +0200 Subject: [PATCH 02/11] Implemented "force pay mana" for Word of Command --- Mage.Sets/src/mage/cards/w/WordOfCommand.java | 29 ++++++++++++++++--- .../abilities/costs/mana/ManaCostsImpl.java | 20 +++++++++++++ Mage/src/main/java/mage/players/ManaPool.java | 29 +++++++++++++++---- 3 files changed, 69 insertions(+), 9 deletions(-) diff --git a/Mage.Sets/src/mage/cards/w/WordOfCommand.java b/Mage.Sets/src/mage/cards/w/WordOfCommand.java index 53713f4c0e3..f9e0ec9a9ed 100644 --- a/Mage.Sets/src/mage/cards/w/WordOfCommand.java +++ b/Mage.Sets/src/mage/cards/w/WordOfCommand.java @@ -49,6 +49,7 @@ import mage.game.events.GameEvent; import mage.game.events.ZoneChangeEvent; import mage.game.permanent.Permanent; import mage.game.stack.Spell; +import mage.players.ManaPool; import mage.players.Player; import mage.target.TargetCard; import mage.target.common.TargetOpponent; @@ -119,16 +120,36 @@ class WordOfCommandEffect extends OneShotEffect { // The player plays that card if able if (card != null) { + // While doing so, the player can activate mana abilities only if they're from lands that player controls RestrictionEffect effect = new WordOfCommandCantActivateEffect(); effect.setTargetPointer(new FixedTarget(targetPlayer.getId())); - game.addEffect(effect, source); // While doing so, the player can activate mana abilities only if they're from lands that player controls - // TODO: and only if mana they produce is spent to activate other mana abilities of lands he or she controls and/or play that card (so you can't tap out the player's lands) + game.addEffect(effect, source); + + // and only if mana they produce is spent to activate other mana abilities of lands he or she controls and/or play that card + ManaPool manaPool = targetPlayer.getManaPool(); + boolean autoPayment = manaPool.isAutoPayment(); + boolean autoPaymentRestricted = manaPool.isAutoPaymentRestricted(); + manaPool.setAutoPayment(true); + manaPool.setAutoPaymentRestricted(true); + manaPool.setForcedToPay(true); + int bookmark = game.bookmarkState(); + if ((card.isLand() && (!targetPlayer.canPlayLand() || !game.getActivePlayerId().equals(targetPlayer.getId()))) || !targetPlayer.playCard(card, game, false, true, new MageObjectReference(source.getSourceObject(game), game))) { - game.informPlayers(targetPlayer.getLogName() + " didn't play " + card.getLogName()); // TODO: needs an automatic check for whether the card is castable (so it can't be cancelled if that's the case) + game.informPlayers(targetPlayer.getLogName() + " didn't play " + card.getLogName()); } + manaPool.setForcedToPay(false); // duplicate in case of a a new mana pool existing - probably not necessary, but just in case + manaPool.setAutoPayment(autoPayment); + manaPool.setAutoPaymentRestricted(autoPaymentRestricted); + manaPool = targetPlayer.getManaPool(); // a rollback creates a new mana pool for the player, so it's necessary to find it again + manaPool.setForcedToPay(false); + manaPool.setAutoPayment(autoPayment); + manaPool.setAutoPaymentRestricted(autoPaymentRestricted); + game.removeBookmark(bookmark); + targetPlayer.resetStoredBookmark(game); + for (RestrictionEffect eff : game.getContinuousEffects().getRestrictionEffects()) { if (eff instanceof WordOfCommandCantActivateEffect) { eff.discard(); @@ -197,7 +218,7 @@ class WordOfCommandDelayedTriggeredAbility extends DelayedTriggeredAbility { private UUID cardId; WordOfCommandDelayedTriggeredAbility(Effect effect, UUID cardId) { - super(effect, Duration.EndOfStep); + super(effect, Duration.EndOfTurn); this.cardId = cardId; this.usesStack = false; } diff --git a/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java b/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java index f5e8f766ca8..f4148349bdd 100644 --- a/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java +++ b/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java @@ -265,6 +265,10 @@ public class ManaCostsImpl extends ArrayList implements M // if auto payment is inactive and no mana type was clicked manually - do nothing return; } + ManaCosts referenceCosts = null; + if (pool.isForcedToPay()) { + referenceCosts = this.copy(); + } // attempt to pay colorless costs (not generic) mana costs first for (ManaCost cost : this) { if (!cost.isPaid() && cost instanceof ColorlessManaCost) { @@ -344,6 +348,22 @@ public class ManaCostsImpl extends ArrayList implements M } // stop using mana of the clicked mana type pool.lockManaType(); + handleForcedToPayOnlyForCurrentPayment(game, pool, referenceCosts); + } + + private void handleForcedToPayOnlyForCurrentPayment(Game game, ManaPool pool, ManaCosts referenceCosts) { + // for Word of Command + if (pool.isForcedToPay()) { + if (referenceCosts != null && this.getPayment().equals(referenceCosts.getPayment())) { + UUID playerId = pool.getPlayerId(); + Player player = game.getPlayer(playerId); + if (player != null) { + game.undo(playerId); + this.clearPaid(); + game.bookmarkState(); + } + } + } } @Override diff --git a/Mage/src/main/java/mage/players/ManaPool.java b/Mage/src/main/java/mage/players/ManaPool.java index 2448b8a9220..686a64edcff 100644 --- a/Mage/src/main/java/mage/players/ManaPool.java +++ b/Mage/src/main/java/mage/players/ManaPool.java @@ -62,6 +62,7 @@ public class ManaPool implements Serializable { private boolean autoPayment; // auto payment from mana pool: true - mode is active private boolean autoPaymentRestricted; // auto payment from mana pool: true - if auto Payment is on, it will only pay if one kind of mana is in the pool private ManaType unlockedManaType; // type of mana that was selected to pay manually + private boolean forcedToPay; // for Word of Command private final Set doNotEmptyManaTypes = new HashSet<>(); @@ -70,6 +71,7 @@ public class ManaPool implements Serializable { autoPayment = true; autoPaymentRestricted = true; unlockedManaType = null; + forcedToPay = false; } public ManaPool(final ManaPool pool) { @@ -80,6 +82,7 @@ public class ManaPool implements Serializable { this.autoPayment = pool.autoPayment; this.autoPaymentRestricted = pool.autoPaymentRestricted; this.unlockedManaType = pool.unlockedManaType; + this.forcedToPay = pool.forcedToPay; this.doNotEmptyManaTypes.addAll(pool.doNotEmptyManaTypes); } @@ -138,6 +141,7 @@ public class ManaPool implements Serializable { lockManaType(); // pay only one mana if mana payment is set to manually return true; } + for (ManaPoolItem mana : manaItems) { if (filter != null) { if (!filter.match(mana.getSourceObject(), game)) { @@ -465,17 +469,21 @@ public class ManaPool implements Serializable { } public void setAutoPayment(boolean autoPayment) { - this.autoPayment = autoPayment; - } - - public void setAutoPaymentRestricted(boolean autoPaymentRestricted) { - this.autoPaymentRestricted = autoPaymentRestricted; + if (!forcedToPay) { + this.autoPayment = autoPayment; + } } public boolean isAutoPaymentRestricted() { return autoPaymentRestricted; } + public void setAutoPaymentRestricted(boolean autoPaymentRestricted) { + if (!forcedToPay) { + this.autoPaymentRestricted = autoPaymentRestricted; + } + } + public ManaType getUnlockedManaType() { return unlockedManaType; } @@ -515,4 +523,15 @@ public class ManaPool implements Serializable { return itemsCopy; } + public void setForcedToPay(boolean forcedToPay) { + this.forcedToPay = forcedToPay; + } + + public boolean isForcedToPay() { + return forcedToPay; + } + + public UUID getPlayerId() { + return playerId; + } } From 6972aab50e641ec01c6ed66ff5658753bc72b961 Mon Sep 17 00:00:00 2001 From: L_J Date: Sun, 3 Jun 2018 20:25:30 +0200 Subject: [PATCH 03/11] Improved forcedToPay handling --- Mage.Sets/src/mage/cards/w/WordOfCommand.java | 37 +-------------- .../abilities/costs/mana/ManaCostImpl.java | 4 +- .../abilities/costs/mana/ManaCostsImpl.java | 4 +- Mage/src/main/java/mage/players/ManaPool.java | 45 +++---------------- 4 files changed, 14 insertions(+), 76 deletions(-) diff --git a/Mage.Sets/src/mage/cards/w/WordOfCommand.java b/Mage.Sets/src/mage/cards/w/WordOfCommand.java index f9e0ec9a9ed..dc20f3c483f 100644 --- a/Mage.Sets/src/mage/cards/w/WordOfCommand.java +++ b/Mage.Sets/src/mage/cards/w/WordOfCommand.java @@ -1,30 +1,3 @@ -/* - * 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.w; import java.util.UUID; @@ -127,10 +100,6 @@ class WordOfCommandEffect extends OneShotEffect { // and only if mana they produce is spent to activate other mana abilities of lands he or she controls and/or play that card ManaPool manaPool = targetPlayer.getManaPool(); - boolean autoPayment = manaPool.isAutoPayment(); - boolean autoPaymentRestricted = manaPool.isAutoPaymentRestricted(); - manaPool.setAutoPayment(true); - manaPool.setAutoPaymentRestricted(true); manaPool.setForcedToPay(true); int bookmark = game.bookmarkState(); @@ -140,13 +109,9 @@ class WordOfCommandEffect extends OneShotEffect { game.informPlayers(targetPlayer.getLogName() + " didn't play " + card.getLogName()); } - manaPool.setForcedToPay(false); // duplicate in case of a a new mana pool existing - probably not necessary, but just in case - manaPool.setAutoPayment(autoPayment); - manaPool.setAutoPaymentRestricted(autoPaymentRestricted); + manaPool.setForcedToPay(false); // duplicate in case of a new mana pool existing - probably not necessary, but just in case manaPool = targetPlayer.getManaPool(); // a rollback creates a new mana pool for the player, so it's necessary to find it again manaPool.setForcedToPay(false); - manaPool.setAutoPayment(autoPayment); - manaPool.setAutoPaymentRestricted(autoPaymentRestricted); game.removeBookmark(bookmark); targetPlayer.resetStoredBookmark(game); diff --git a/Mage/src/main/java/mage/abilities/costs/mana/ManaCostImpl.java b/Mage/src/main/java/mage/abilities/costs/mana/ManaCostImpl.java index 906dc777a7c..0286595574a 100644 --- a/Mage/src/main/java/mage/abilities/costs/mana/ManaCostImpl.java +++ b/Mage/src/main/java/mage/abilities/costs/mana/ManaCostImpl.java @@ -233,7 +233,9 @@ public abstract class ManaCostImpl extends CostImpl implements ManaCost { return true; } Player player = game.getPlayer(controllerId); - assignPayment(game, ability, player.getManaPool(), costToPay); + if (!player.getManaPool().isForcedToPay()) { + assignPayment(game, ability, player.getManaPool(), costToPay); + } game.getState().getSpecialActions().removeManaActions(); while (!isPaid()) { ManaCost unpaid = this.getUnpaid(); diff --git a/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java b/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java index f4148349bdd..4591d948f3f 100644 --- a/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java +++ b/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java @@ -143,7 +143,9 @@ public class ManaCostsImpl extends ArrayList implements M } Player player = game.getPlayer(controllerId); - assignPayment(game, ability, player.getManaPool(), this); + if (!player.getManaPool().isForcedToPay()) { + assignPayment(game, ability, player.getManaPool(), this); + } game.getState().getSpecialActions().removeManaActions(); while (!isPaid()) { ManaCost unpaid = this.getUnpaid(); diff --git a/Mage/src/main/java/mage/players/ManaPool.java b/Mage/src/main/java/mage/players/ManaPool.java index 686a64edcff..f927460970d 100644 --- a/Mage/src/main/java/mage/players/ManaPool.java +++ b/Mage/src/main/java/mage/players/ManaPool.java @@ -1,30 +1,3 @@ -/* - * 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.players; import java.io.Serializable; @@ -117,12 +90,12 @@ public class ManaPool implements Serializable { * @return */ public boolean pay(ManaType manaType, Ability ability, Filter filter, Game game, Cost costToPay, Mana usedManaToPay) { - if (!autoPayment && manaType != unlockedManaType) { + if (!isAutoPayment() && manaType != unlockedManaType) { // if manual payment and the needed mana type was not unlocked, nothing will be paid return false; } ManaType possibleAsThoughtPoolManaType = null; - if (autoPayment && autoPaymentRestricted && !wasManaAddedBeyondStock() && manaType != unlockedManaType) { + if (isAutoPayment() && isAutoPaymentRestricted() && !wasManaAddedBeyondStock() && manaType != unlockedManaType) { // if automatic restricted payment and there is already mana in the pool // and the needed mana type was not unlocked, nothing will be paid if (unlockedManaType != null) { @@ -151,7 +124,7 @@ public class ManaPool implements Serializable { } } } - if (possibleAsThoughtPoolManaType == null && manaType != unlockedManaType && autoPayment && autoPaymentRestricted && mana.count() == mana.getStock()) { + if (possibleAsThoughtPoolManaType == null && manaType != unlockedManaType && isAutoPayment() && isAutoPaymentRestricted() && mana.count() == mana.getStock()) { // no mana added beyond the stock so don't auto pay this continue; } @@ -465,23 +438,19 @@ public class ManaPool implements Serializable { } public boolean isAutoPayment() { - return autoPayment; + return autoPayment || forcedToPay; } public void setAutoPayment(boolean autoPayment) { - if (!forcedToPay) { - this.autoPayment = autoPayment; - } + this.autoPayment = autoPayment; } public boolean isAutoPaymentRestricted() { - return autoPaymentRestricted; + return autoPaymentRestricted || forcedToPay; } public void setAutoPaymentRestricted(boolean autoPaymentRestricted) { - if (!forcedToPay) { - this.autoPaymentRestricted = autoPaymentRestricted; - } + this.autoPaymentRestricted = autoPaymentRestricted; } public ManaType getUnlockedManaType() { From 492c5ab63e61eb7aebe28272d87badd695bc8fee Mon Sep 17 00:00:00 2001 From: L_J Date: Mon, 4 Jun 2018 01:32:17 +0200 Subject: [PATCH 04/11] Improved Word of Command turn control handling --- Mage.Sets/src/mage/cards/w/WordOfCommand.java | 55 ++----------------- ...ControlOnOtherPlayersControllerEffect.java | 4 -- Mage/src/main/java/mage/game/Game.java | 2 + Mage/src/main/java/mage/game/GameImpl.java | 33 ++++++++++- .../main/java/mage/game/stack/SpellStack.java | 8 ++- 5 files changed, 45 insertions(+), 57 deletions(-) diff --git a/Mage.Sets/src/mage/cards/w/WordOfCommand.java b/Mage.Sets/src/mage/cards/w/WordOfCommand.java index dc20f3c483f..19da3214345 100644 --- a/Mage.Sets/src/mage/cards/w/WordOfCommand.java +++ b/Mage.Sets/src/mage/cards/w/WordOfCommand.java @@ -126,22 +126,10 @@ class WordOfCommandEffect extends OneShotEffect { spell.setCommandedBy(controller.getId()); // If the chosen card is cast as a spell, you control the player while that spell is resolving } } - - if (sourceObject != null) { - Effect effect = new LoseControlOnOtherPlayersControllerEffect(controller.getLogName(), targetPlayer.getLogName()); - effect.setTargetPointer(new FixedTarget(targetPlayer.getId())); - // You control the player until Word of Command finishes resolving - // TODO: using a DelayedTriggeredAbility to end the effect isn't the optimal solution, since effects like Time Stop can stop it from triggering even outside the stack - DelayedTriggeredAbility ability = new WordOfCommandDelayedTriggeredAbility(effect, source.getSourceId()); - ability.setSourceId(controller.getId()); - ability.setControllerId(controller.getId()); - game.addDelayedTriggeredAbility(ability); - if (card != null && !card.isLand()) { // this sets up a lose control effect for when the spell finishes resolving - ability = new WordOfCommandDelayedTriggeredAbility(effect, card.getId()); - ability.setSourceId(controller.getId()); - ability.setControllerId(controller.getId()); - game.addDelayedTriggeredAbility(ability); - } + + Spell wordOfCommand = game.getSpell(sourceObject.getId()); + if (wordOfCommand != null) { + wordOfCommand.setCommandedBy(controller.getId()); // You control the player until Word of Command finishes resolving } else { controller.resetOtherTurnsControlled(); targetPlayer.setGameUnderYourControl(true); @@ -177,38 +165,3 @@ class WordOfCommandCantActivateEffect extends RestrictionEffect { return false; } } - -class WordOfCommandDelayedTriggeredAbility extends DelayedTriggeredAbility { - - private UUID cardId; - - WordOfCommandDelayedTriggeredAbility(Effect effect, UUID cardId) { - super(effect, Duration.EndOfTurn); - this.cardId = cardId; - this.usesStack = false; - } - - WordOfCommandDelayedTriggeredAbility(final WordOfCommandDelayedTriggeredAbility ability) { - super(ability); - this.cardId = ability.cardId; - } - - @Override - public WordOfCommandDelayedTriggeredAbility copy() { - return new WordOfCommandDelayedTriggeredAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.ZONE_CHANGE; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - ZoneChangeEvent zEvent = (ZoneChangeEvent) event; - if (zEvent.getFromZone() == Zone.STACK && event.getTargetId().equals(cardId)) { - return true; - } - return false; - } -} diff --git a/Mage/src/main/java/mage/abilities/effects/common/LoseControlOnOtherPlayersControllerEffect.java b/Mage/src/main/java/mage/abilities/effects/common/LoseControlOnOtherPlayersControllerEffect.java index 3f3621caa86..01bb92c2ee4 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/LoseControlOnOtherPlayersControllerEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/LoseControlOnOtherPlayersControllerEffect.java @@ -33,10 +33,6 @@ public class LoseControlOnOtherPlayersControllerEffect extends OneShotEffect { Player player = game.getPlayer(source.getControllerId()); if (player != null) { player.resetOtherTurnsControlled(); - Player targetPlayer = game.getPlayer(this.getTargetPointer().getFirst(game, source)); - if (targetPlayer != null) { - targetPlayer.setGameUnderYourControl(true); - } return true; } return false; diff --git a/Mage/src/main/java/mage/game/Game.java b/Mage/src/main/java/mage/game/Game.java index 5ae398beeab..21418c8a0cd 100644 --- a/Mage/src/main/java/mage/game/Game.java +++ b/Mage/src/main/java/mage/game/Game.java @@ -393,6 +393,8 @@ public interface Game extends MageItem, Serializable { boolean checkStateAndTriggered(); void playPriority(UUID activePlayerId, boolean resuming); + + void resetControlAfterSpellResolve(Spell spell); boolean endTurn(Ability source); diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index 259d7e53358..350ff0047a6 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -1395,7 +1395,13 @@ public abstract class GameImpl implements Game, Serializable { StackObject top = null; try { top = state.getStack().peek(); - top.resolve(this); + Spell topSpell = getSpell(top.getId()); + if (topSpell != null) { + top.resolve(this); + resetControlAfterSpellResolve(topSpell); + } else { + top.resolve(this); + } } finally { if (top != null) { state.getStack().remove(top, this); // seems partly redundant because move card from stack to grave is already done and the stack removed @@ -1409,6 +1415,31 @@ public abstract class GameImpl implements Game, Serializable { } } } + + @Override + public void resetControlAfterSpellResolve(Spell spell) { + // for Word of Command + if (spell.getCommandedBy() != null) { + UUID commandedBy = spell.getCommandedBy(); + UUID spellControllerId = null; + if (commandedBy.equals(spell.getControllerId())) { + spellControllerId = spell.getSpellAbility().getFirstTarget(); // i.e. resolved spell is Word of Command + } else { + spellControllerId = spell.getControllerId(); // i.e. resolved spell is the target opponent's spell + } + if (commandedBy != null && spellControllerId != null) { + Player turnController = getPlayer(commandedBy); + if (turnController != null) { + Player targetPlayer = getPlayer(spellControllerId); + if (targetPlayer != null) { + informPlayers(turnController.getLogName() + " lost control over " + targetPlayer.getLogName()); + turnController.resetOtherTurnsControlled(); + targetPlayer.setGameUnderYourControl(true); + } + } + } + } + } /** * This checks if the stack gets filled iterated, without ever getting empty diff --git a/Mage/src/main/java/mage/game/stack/SpellStack.java b/Mage/src/main/java/mage/game/stack/SpellStack.java index 71ad426f8bb..ecf91441b47 100644 --- a/Mage/src/main/java/mage/game/stack/SpellStack.java +++ b/Mage/src/main/java/mage/game/stack/SpellStack.java @@ -36,7 +36,13 @@ public class SpellStack extends ArrayDeque { StackObject top = null; try { top = this.peek(); - top.resolve(game); + Spell topSpell = getSpell(top.getId()); + if (topSpell != null) { + top.resolve(game); + game.resetControlAfterSpellResolve(topSpell); + } else { + top.resolve(game); + } } finally { if (top != null) { if (contains(top)) { From 9a5b574c0c51b513e11265cbaa67bfba0f567207 Mon Sep 17 00:00:00 2001 From: L_J Date: Sun, 3 Jun 2018 23:39:40 +0000 Subject: [PATCH 05/11] Added getCommandedBy method --- Mage/src/main/java/mage/game/stack/Spell.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Mage/src/main/java/mage/game/stack/Spell.java b/Mage/src/main/java/mage/game/stack/Spell.java index eb2544e2575..05f8b639662 100644 --- a/Mage/src/main/java/mage/game/stack/Spell.java +++ b/Mage/src/main/java/mage/game/stack/Spell.java @@ -1036,4 +1036,8 @@ public class Spell extends StackObjImpl implements Card { this.commandedBy = playerId; } + public UUID getCommandedBy() { + return commandedBy; + } + } From cb7c222eb94baf6b55110236216562c988bd5b61 Mon Sep 17 00:00:00 2001 From: L_J Date: Mon, 4 Jun 2018 14:30:37 +0200 Subject: [PATCH 06/11] Fixed various Word of Command bugs, implemented mana pool restore methods --- Mage.Sets/src/mage/cards/w/WordOfCommand.java | 8 +--- .../abilities/costs/mana/ManaCostsImpl.java | 8 +++- Mage/src/main/java/mage/game/Game.java | 2 +- Mage/src/main/java/mage/game/GameImpl.java | 46 +++++++++---------- .../main/java/mage/game/stack/SpellStack.java | 11 ++--- Mage/src/main/java/mage/players/ManaPool.java | 28 +++++++++++ 6 files changed, 62 insertions(+), 41 deletions(-) diff --git a/Mage.Sets/src/mage/cards/w/WordOfCommand.java b/Mage.Sets/src/mage/cards/w/WordOfCommand.java index 19da3214345..54c3a66c79f 100644 --- a/Mage.Sets/src/mage/cards/w/WordOfCommand.java +++ b/Mage.Sets/src/mage/cards/w/WordOfCommand.java @@ -4,11 +4,8 @@ import java.util.UUID; import mage.MageObject; import mage.MageObjectReference; import mage.abilities.Ability; -import mage.abilities.DelayedTriggeredAbility; -import mage.abilities.effects.Effect; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.RestrictionEffect; -import mage.abilities.effects.common.LoseControlOnOtherPlayersControllerEffect; import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -18,8 +15,6 @@ import mage.constants.Outcome; import mage.constants.Zone; import mage.filter.FilterCard; import mage.game.Game; -import mage.game.events.GameEvent; -import mage.game.events.ZoneChangeEvent; import mage.game.permanent.Permanent; import mage.game.stack.Spell; import mage.players.ManaPool; @@ -101,6 +96,7 @@ class WordOfCommandEffect extends OneShotEffect { // and only if mana they produce is spent to activate other mana abilities of lands he or she controls and/or play that card ManaPool manaPool = targetPlayer.getManaPool(); manaPool.setForcedToPay(true); + manaPool.storeMana(); int bookmark = game.bookmarkState(); if ((card.isLand() && (!targetPlayer.canPlayLand() || !game.getActivePlayerId().equals(targetPlayer.getId()))) @@ -127,7 +123,7 @@ class WordOfCommandEffect extends OneShotEffect { } } - Spell wordOfCommand = game.getSpell(sourceObject.getId()); + Spell wordOfCommand = game.getSpell(source.getSourceId()); if (wordOfCommand != null) { wordOfCommand.setCommandedBy(controller.getId()); // You control the player until Word of Command finishes resolving } else { diff --git a/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java b/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java index 90a8b7e4ecd..73739a43484 100644 --- a/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java +++ b/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java @@ -237,7 +237,8 @@ public class ManaCostsImpl extends ArrayList implements M @Override public void assignPayment(Game game, Ability ability, ManaPool pool, Cost costToPay) { - if (!pool.isAutoPayment() && pool.getUnlockedManaType() == null) { + boolean wasUnlockedManaType = (pool.getUnlockedManaType() != null); + if (!pool.isAutoPayment() && !wasUnlockedManaType) { // if auto payment is inactive and no mana type was clicked manually - do nothing return; } @@ -324,7 +325,9 @@ public class ManaCostsImpl extends ArrayList implements M } // stop using mana of the clicked mana type pool.lockManaType(); - handleForcedToPayOnlyForCurrentPayment(game, pool, referenceCosts); + if (!wasUnlockedManaType) { + handleForcedToPayOnlyForCurrentPayment(game, pool, referenceCosts); + } } private void handleForcedToPayOnlyForCurrentPayment(Game game, ManaPool pool, ManaCosts referenceCosts) { @@ -336,6 +339,7 @@ public class ManaCostsImpl extends ArrayList implements M if (player != null) { game.undo(playerId); this.clearPaid(); + player.getManaPool().restoreMana(pool.getPoolBookmark()); game.bookmarkState(); } } diff --git a/Mage/src/main/java/mage/game/Game.java b/Mage/src/main/java/mage/game/Game.java index 21418c8a0cd..2f7af72d292 100644 --- a/Mage/src/main/java/mage/game/Game.java +++ b/Mage/src/main/java/mage/game/Game.java @@ -394,7 +394,7 @@ public interface Game extends MageItem, Serializable { void playPriority(UUID activePlayerId, boolean resuming); - void resetControlAfterSpellResolve(Spell spell); + void resetControlAfterSpellResolve(UUID topId); boolean endTurn(Ability source); diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index 350ff0047a6..55c1872ab19 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -1395,13 +1395,8 @@ public abstract class GameImpl implements Game, Serializable { StackObject top = null; try { top = state.getStack().peek(); - Spell topSpell = getSpell(top.getId()); - if (topSpell != null) { - top.resolve(this); - resetControlAfterSpellResolve(topSpell); - } else { - top.resolve(this); - } + top.resolve(this); + resetControlAfterSpellResolve(top.getId()); } finally { if (top != null) { state.getStack().remove(top, this); // seems partly redundant because move card from stack to grave is already done and the stack removed @@ -1417,24 +1412,27 @@ public abstract class GameImpl implements Game, Serializable { } @Override - public void resetControlAfterSpellResolve(Spell spell) { + public void resetControlAfterSpellResolve(UUID topId) { // for Word of Command - if (spell.getCommandedBy() != null) { - UUID commandedBy = spell.getCommandedBy(); - UUID spellControllerId = null; - if (commandedBy.equals(spell.getControllerId())) { - spellControllerId = spell.getSpellAbility().getFirstTarget(); // i.e. resolved spell is Word of Command - } else { - spellControllerId = spell.getControllerId(); // i.e. resolved spell is the target opponent's spell - } - if (commandedBy != null && spellControllerId != null) { - Player turnController = getPlayer(commandedBy); - if (turnController != null) { - Player targetPlayer = getPlayer(spellControllerId); - if (targetPlayer != null) { - informPlayers(turnController.getLogName() + " lost control over " + targetPlayer.getLogName()); - turnController.resetOtherTurnsControlled(); - targetPlayer.setGameUnderYourControl(true); + Spell spell = getSpellOrLKIStack(topId); + if (spell != null) { + if (spell.getCommandedBy() != null) { + UUID commandedBy = spell.getCommandedBy(); + UUID spellControllerId = null; + if (commandedBy.equals(spell.getControllerId())) { + spellControllerId = spell.getSpellAbility().getFirstTarget(); // i.e. resolved spell is Word of Command + } else { + spellControllerId = spell.getControllerId(); // i.e. resolved spell is the target opponent's spell + } + if (commandedBy != null && spellControllerId != null) { + Player turnController = getPlayer(commandedBy); + if (turnController != null) { + Player targetPlayer = getPlayer(spellControllerId); + if (targetPlayer != null) { + informPlayers(turnController.getLogName() + " lost control over " + targetPlayer.getLogName()); + turnController.resetOtherTurnsControlled(); + targetPlayer.setGameUnderYourControl(true); + } } } } diff --git a/Mage/src/main/java/mage/game/stack/SpellStack.java b/Mage/src/main/java/mage/game/stack/SpellStack.java index ecf91441b47..060be88c1f0 100644 --- a/Mage/src/main/java/mage/game/stack/SpellStack.java +++ b/Mage/src/main/java/mage/game/stack/SpellStack.java @@ -36,17 +36,12 @@ public class SpellStack extends ArrayDeque { StackObject top = null; try { top = this.peek(); - Spell topSpell = getSpell(top.getId()); - if (topSpell != null) { - top.resolve(game); - game.resetControlAfterSpellResolve(topSpell); - } else { - top.resolve(game); - } + top.resolve(game); + game.resetControlAfterSpellResolve(top.getId()); } finally { if (top != null) { if (contains(top)) { - logger.warn("StackObject was still on the stack after resoving" + top.getName()); + logger.warn("StackObject was still on the stack after resolving" + top.getName()); this.remove(top, game); } } diff --git a/Mage/src/main/java/mage/players/ManaPool.java b/Mage/src/main/java/mage/players/ManaPool.java index f927460970d..67dbe5fea9a 100644 --- a/Mage/src/main/java/mage/players/ManaPool.java +++ b/Mage/src/main/java/mage/players/ManaPool.java @@ -36,6 +36,7 @@ public class ManaPool implements Serializable { private boolean autoPaymentRestricted; // auto payment from mana pool: true - if auto Payment is on, it will only pay if one kind of mana is in the pool private ManaType unlockedManaType; // type of mana that was selected to pay manually private boolean forcedToPay; // for Word of Command + private final List poolBookmark = new ArrayList<>(); // mana pool bookmark for rollback purposes private final Set doNotEmptyManaTypes = new HashSet<>(); @@ -56,6 +57,9 @@ public class ManaPool implements Serializable { this.autoPaymentRestricted = pool.autoPaymentRestricted; this.unlockedManaType = pool.unlockedManaType; this.forcedToPay = pool.forcedToPay; + for (ManaPoolItem item : pool.poolBookmark) { + poolBookmark.add(item.copy()); + } this.doNotEmptyManaTypes.addAll(pool.doNotEmptyManaTypes); } @@ -503,4 +507,28 @@ public class ManaPool implements Serializable { public UUID getPlayerId() { return playerId; } + + public void storeMana() { + poolBookmark.clear(); + poolBookmark.addAll(getManaItems()); + } + + public List getPoolBookmark() { + List itemsCopy = new ArrayList<>(); + for (ManaPoolItem manaItem : poolBookmark) { + itemsCopy.add(manaItem.copy()); + } + return itemsCopy; + } + + public void restoreMana(List manaList) { + manaItems.clear(); + if (!manaList.isEmpty()) { + List itemsCopy = new ArrayList<>(); + for (ManaPoolItem manaItem : manaList) { + itemsCopy.add(manaItem.copy()); + } + manaItems.addAll(itemsCopy); + } + } } From d19ca838cace79945b3c06ad6027f6d92cf966f6 Mon Sep 17 00:00:00 2001 From: L_J Date: Thu, 7 Jun 2018 05:37:22 +0200 Subject: [PATCH 07/11] Implemented castability check for Word of Command --- Mage.Sets/src/mage/cards/w/WordOfCommand.java | 74 ++++++++++++++++++- .../abilities/costs/mana/ManaCostsImpl.java | 5 ++ .../abilities/effects/ContinuousEffects.java | 2 +- 3 files changed, 76 insertions(+), 5 deletions(-) diff --git a/Mage.Sets/src/mage/cards/w/WordOfCommand.java b/Mage.Sets/src/mage/cards/w/WordOfCommand.java index 54c3a66c79f..dc744bda24b 100644 --- a/Mage.Sets/src/mage/cards/w/WordOfCommand.java +++ b/Mage.Sets/src/mage/cards/w/WordOfCommand.java @@ -4,17 +4,23 @@ import java.util.UUID; import mage.MageObject; import mage.MageObjectReference; import mage.abilities.Ability; +import mage.abilities.SpellAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.AsThoughEffect; +import mage.abilities.effects.AsThoughEffectImpl; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.RestrictionEffect; import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; +import mage.constants.AsThoughEffectType; import mage.constants.Duration; import mage.constants.Outcome; import mage.constants.Zone; import mage.filter.FilterCard; import mage.game.Game; +import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.game.stack.Spell; import mage.players.ManaPool; @@ -99,10 +105,43 @@ class WordOfCommandEffect extends OneShotEffect { manaPool.storeMana(); int bookmark = game.bookmarkState(); - if ((card.isLand() && (!targetPlayer.canPlayLand() || !game.getActivePlayerId().equals(targetPlayer.getId()))) - || !targetPlayer.playCard(card, game, false, true, new MageObjectReference(source.getSourceObject(game), game))) { - // TODO: needs an automatic check for whether the card is castable (so it can't be cancelled if that's the case) - game.informPlayers(targetPlayer.getLogName() + " didn't play " + card.getLogName()); + // check for card playability (Word of Command allows the chosen card to be played "as if it had flash" so we need to invoke such effect to bypass the check) + boolean canPlay = false; + if (card.isLand()) { // we can't use getPlayableInHand(game) in here because it disallows playing lands outside the main step + if (targetPlayer.canPlayLand() + && game.getActivePlayerId().equals(targetPlayer.getId())) { + canPlay = true; + for (Ability ability : card.getAbilities(game)) { + if (!game.getContinuousEffects().preventedByRuleModification(GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, ability.getSourceId(), ability.getSourceId(), targetPlayer.getId()), ability, game, true)) { + canPlay &= true; + } + } + } + } else { + AsThoughEffectImpl effect2 = new WordOfCommandTestFlashEffect(); + game.addEffect(effect2, source); + if (targetPlayer.getPlayableInHand(game).contains(card.getId())) { + canPlay = true; + } + for (AsThoughEffect eff : game.getContinuousEffects().getApplicableAsThoughEffects(AsThoughEffectType.CAST_AS_INSTANT, game)) { + if (eff instanceof WordOfCommandTestFlashEffect) { + eff.discard(); + break; + } + } + } + + if (canPlay) { + while (!targetPlayer.playCard(card, game, false, true, new MageObjectReference(source.getSourceObject(game), game))) { + SpellAbility spellAbility = card.getSpellAbility(); + if (spellAbility != null) { + ((ManaCostsImpl) spellAbility.getManaCosts()).forceManaRollback(game, manaPool); // force rollback if card was deemed playable + } else { + break; + } + } + } else { + game.informPlayers(targetPlayer.getLogName() + " didn't play " + card.getLogName() + (canPlay ? "" : " (card can't be played)")); } manaPool.setForcedToPay(false); // duplicate in case of a new mana pool existing - probably not necessary, but just in case @@ -114,6 +153,7 @@ class WordOfCommandEffect extends OneShotEffect { for (RestrictionEffect eff : game.getContinuousEffects().getRestrictionEffects()) { if (eff instanceof WordOfCommandCantActivateEffect) { eff.discard(); + break; } } game.getContinuousEffects().removeInactiveEffects(game); @@ -161,3 +201,29 @@ class WordOfCommandCantActivateEffect extends RestrictionEffect { return false; } } + +class WordOfCommandTestFlashEffect extends AsThoughEffectImpl { + + public WordOfCommandTestFlashEffect() { + super(AsThoughEffectType.CAST_AS_INSTANT, Duration.EndOfTurn, Outcome.Benefit); + } + + public WordOfCommandTestFlashEffect(final WordOfCommandTestFlashEffect effect) { + super(effect); + } + + @Override + public WordOfCommandTestFlashEffect copy() { + return new WordOfCommandTestFlashEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + return true; + } + + @Override + public boolean applies(UUID affectedSpellId, Ability source, UUID affectedControllerId, Game game) { + return true; + } +} diff --git a/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java b/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java index 73739a43484..7aa19583676 100644 --- a/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java +++ b/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java @@ -346,6 +346,11 @@ public class ManaCostsImpl extends ArrayList implements M } } + public void forceManaRollback(Game game, ManaPool pool) { + // for Word of Command + handleForcedToPayOnlyForCurrentPayment(game, pool, this); + } + @Override public final void load(String mana) { this.clear(); diff --git a/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java b/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java index 7c170cc5f6a..54fe6c7c744 100644 --- a/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java +++ b/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java @@ -543,7 +543,7 @@ public class ContinuousEffects implements Serializable { * @param game * @return */ - private List getApplicableAsThoughEffects(AsThoughEffectType type, Game game) { + public List getApplicableAsThoughEffects(AsThoughEffectType type, Game game) { List asThoughEffectsList = new ArrayList<>(); if (asThoughEffectsMap.containsKey(type)) { for (AsThoughEffect effect : asThoughEffectsMap.get(type)) { From 323894118b91b394a1e894448773b1a93299d29d Mon Sep 17 00:00:00 2001 From: L_J Date: Thu, 7 Jun 2018 16:49:10 +0200 Subject: [PATCH 08/11] Fixed X costs not being properly reset with Word of Command rollback --- Mage.Sets/src/mage/cards/w/WordOfCommand.java | 79 ++++++++++--------- .../abilities/costs/mana/ManaCostsImpl.java | 1 + 2 files changed, 44 insertions(+), 36 deletions(-) diff --git a/Mage.Sets/src/mage/cards/w/WordOfCommand.java b/Mage.Sets/src/mage/cards/w/WordOfCommand.java index dc744bda24b..e2a349ffa76 100644 --- a/Mage.Sets/src/mage/cards/w/WordOfCommand.java +++ b/Mage.Sets/src/mage/cards/w/WordOfCommand.java @@ -105,43 +105,22 @@ class WordOfCommandEffect extends OneShotEffect { manaPool.storeMana(); int bookmark = game.bookmarkState(); - // check for card playability (Word of Command allows the chosen card to be played "as if it had flash" so we need to invoke such effect to bypass the check) - boolean canPlay = false; - if (card.isLand()) { // we can't use getPlayableInHand(game) in here because it disallows playing lands outside the main step - if (targetPlayer.canPlayLand() - && game.getActivePlayerId().equals(targetPlayer.getId())) { - canPlay = true; - for (Ability ability : card.getAbilities(game)) { - if (!game.getContinuousEffects().preventedByRuleModification(GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, ability.getSourceId(), ability.getSourceId(), targetPlayer.getId()), ability, game, true)) { - canPlay &= true; - } - } - } - } else { - AsThoughEffectImpl effect2 = new WordOfCommandTestFlashEffect(); - game.addEffect(effect2, source); - if (targetPlayer.getPlayableInHand(game).contains(card.getId())) { - canPlay = true; - } - for (AsThoughEffect eff : game.getContinuousEffects().getApplicableAsThoughEffects(AsThoughEffectType.CAST_AS_INSTANT, game)) { - if (eff instanceof WordOfCommandTestFlashEffect) { - eff.discard(); - break; - } + boolean canPlay = checkPlayability(card, targetPlayer, game, source); + while (canPlay + && targetPlayer.canRespond() + && !targetPlayer.playCard(card, game, false, true, new MageObjectReference(source.getSourceObject(game), game))) { + SpellAbility spellAbility = card.getSpellAbility(); + if (spellAbility != null) { + spellAbility.getManaCostsToPay().clear(); + spellAbility.getManaCostsToPay().addAll(spellAbility.getManaCosts()); + ((ManaCostsImpl) spellAbility.getManaCostsToPay()).forceManaRollback(game, manaPool); // force rollback if card was deemed playable + canPlay = checkPlayability(card, targetPlayer, game, source); + } else { + break; } } - - if (canPlay) { - while (!targetPlayer.playCard(card, game, false, true, new MageObjectReference(source.getSourceObject(game), game))) { - SpellAbility spellAbility = card.getSpellAbility(); - if (spellAbility != null) { - ((ManaCostsImpl) spellAbility.getManaCosts()).forceManaRollback(game, manaPool); // force rollback if card was deemed playable - } else { - break; - } - } - } else { - game.informPlayers(targetPlayer.getLogName() + " didn't play " + card.getLogName() + (canPlay ? "" : " (card can't be played)")); + if (!canPlay) { + game.informPlayers(targetPlayer.getLogName() + " didn't play " + card.getLogName() + " (card can't be played)"); } manaPool.setForcedToPay(false); // duplicate in case of a new mana pool existing - probably not necessary, but just in case @@ -149,7 +128,6 @@ class WordOfCommandEffect extends OneShotEffect { manaPool.setForcedToPay(false); game.removeBookmark(bookmark); targetPlayer.resetStoredBookmark(game); - for (RestrictionEffect eff : game.getContinuousEffects().getRestrictionEffects()) { if (eff instanceof WordOfCommandCantActivateEffect) { eff.discard(); @@ -174,6 +152,35 @@ class WordOfCommandEffect extends OneShotEffect { } return false; } + + private boolean checkPlayability(Card card, Player targetPlayer, Game game, Ability source) { + // check for card playability + boolean canPlay = false; + if (card.isLand()) { // we can't use getPlayableInHand(game) in here because it disallows playing lands outside the main step + if (targetPlayer.canPlayLand() + && game.getActivePlayerId().equals(targetPlayer.getId())) { + canPlay = true; + for (Ability ability : card.getAbilities(game)) { + if (!game.getContinuousEffects().preventedByRuleModification(GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, ability.getSourceId(), ability.getSourceId(), targetPlayer.getId()), ability, game, true)) { + canPlay &= true; + } + } + } + } else { // Word of Command allows the chosen card to be played "as if it had flash" so we need to invoke such effect to bypass the check + AsThoughEffectImpl effect2 = new WordOfCommandTestFlashEffect(); + game.addEffect(effect2, source); + if (targetPlayer.getPlayableInHand(game).contains(card.getId())) { + canPlay = true; + } + for (AsThoughEffect eff : game.getContinuousEffects().getApplicableAsThoughEffects(AsThoughEffectType.CAST_AS_INSTANT, game)) { + if (eff instanceof WordOfCommandTestFlashEffect) { + eff.discard(); + break; + } + } + } + return canPlay; + } } class WordOfCommandCantActivateEffect extends RestrictionEffect { diff --git a/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java b/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java index 7aa19583676..303384dafc0 100644 --- a/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java +++ b/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java @@ -339,6 +339,7 @@ public class ManaCostsImpl extends ArrayList implements M if (player != null) { game.undo(playerId); this.clearPaid(); + this.setX(0); player.getManaPool().restoreMana(pool.getPoolBookmark()); game.bookmarkState(); } From 5fa69cb8a9fe94aa979177101dc324d0c8b43099 Mon Sep 17 00:00:00 2001 From: L_J Date: Thu, 7 Jun 2018 15:41:47 +0000 Subject: [PATCH 09/11] More X cost rollback related fixes --- .../main/java/mage/abilities/costs/mana/ManaCostsImpl.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java b/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java index 303384dafc0..dbf25f6df43 100644 --- a/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java +++ b/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java @@ -333,13 +333,13 @@ public class ManaCostsImpl extends ArrayList implements M private void handleForcedToPayOnlyForCurrentPayment(Game game, ManaPool pool, ManaCosts referenceCosts) { // for Word of Command if (pool.isForcedToPay()) { - if (referenceCosts != null && this.getPayment().equals(referenceCosts.getPayment())) { + if (referenceCosts != null && this.getText().equals(referenceCosts.getText())) { UUID playerId = pool.getPlayerId(); Player player = game.getPlayer(playerId); if (player != null) { game.undo(playerId); this.clearPaid(); - this.setX(0); + this.setX(referenceCosts.getX()); player.getManaPool().restoreMana(pool.getPoolBookmark()); game.bookmarkState(); } From 3e180267ed44f5d4929dd56b729740da1b40ef84 Mon Sep 17 00:00:00 2001 From: L_J Date: Fri, 8 Jun 2018 20:22:25 +0200 Subject: [PATCH 10/11] Word of Command & Mindslaver interaction fixes --- Mage.Sets/src/mage/cards/w/WordOfCommand.java | 27 +++++++++--- Mage/src/main/java/mage/game/Game.java | 2 +- Mage/src/main/java/mage/game/GameImpl.java | 7 ++- Mage/src/main/java/mage/players/Player.java | 6 +++ .../main/java/mage/players/PlayerImpl.java | 43 +++++++++++++++++-- 5 files changed, 72 insertions(+), 13 deletions(-) diff --git a/Mage.Sets/src/mage/cards/w/WordOfCommand.java b/Mage.Sets/src/mage/cards/w/WordOfCommand.java index e2a349ffa76..d03d0332667 100644 --- a/Mage.Sets/src/mage/cards/w/WordOfCommand.java +++ b/Mage.Sets/src/mage/cards/w/WordOfCommand.java @@ -1,3 +1,4 @@ + package mage.cards.w; import java.util.UUID; @@ -71,11 +72,23 @@ class WordOfCommandEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { - Player controller = game.getPlayer(source.getControllerId()); + Player sourceController = game.getPlayer(source.getControllerId()); Player targetPlayer = game.getPlayer(source.getFirstTarget()); MageObject sourceObject = game.getObject(source.getSourceId()); Card card = null; - if (controller != null && targetPlayer != null && sourceObject != null) { + if (sourceController != null && targetPlayer != null && sourceObject != null) { + Player controller = null; + Spell wordOfCommand = game.getSpell(source.getSourceId()); + if (wordOfCommand != null) { + if (wordOfCommand.getCommandedBy() != null) { + controller = game.getPlayer(wordOfCommand.getCommandedBy()); + } else { + controller = game.getPlayer(sourceController.getTurnControlledBy()); + } + } + if (controller == null) { + controller = sourceController; // reset the controller to avoid NPE + } // Look at target opponent's hand and choose a card from it TargetCard targetCard = new TargetCard(Zone.HAND, new FilterCard()); @@ -140,13 +153,15 @@ class WordOfCommandEffect extends OneShotEffect { spell.setCommandedBy(controller.getId()); // If the chosen card is cast as a spell, you control the player while that spell is resolving } } - - Spell wordOfCommand = game.getSpell(source.getSourceId()); + + wordOfCommand = game.getSpell(source.getSourceId()); if (wordOfCommand != null) { wordOfCommand.setCommandedBy(controller.getId()); // You control the player until Word of Command finishes resolving } else { - controller.resetOtherTurnsControlled(); - targetPlayer.setGameUnderYourControl(true); + targetPlayer.setGameUnderYourControl(true, false); + if (!targetPlayer.getTurnControlledBy().equals(controller.getId())) { + controller.getPlayersUnderYourControl().remove(targetPlayer.getId()); + } } return true; } diff --git a/Mage/src/main/java/mage/game/Game.java b/Mage/src/main/java/mage/game/Game.java index 2f7af72d292..cdb2fae408d 100644 --- a/Mage/src/main/java/mage/game/Game.java +++ b/Mage/src/main/java/mage/game/Game.java @@ -393,7 +393,7 @@ public interface Game extends MageItem, Serializable { boolean checkStateAndTriggered(); void playPriority(UUID activePlayerId, boolean resuming); - + void resetControlAfterSpellResolve(UUID topId); boolean endTurn(Ability source); diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index 55c1872ab19..5127ed4af37 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -1429,12 +1429,15 @@ public abstract class GameImpl implements Game, Serializable { if (turnController != null) { Player targetPlayer = getPlayer(spellControllerId); if (targetPlayer != null) { + targetPlayer.setGameUnderYourControl(true, false); informPlayers(turnController.getLogName() + " lost control over " + targetPlayer.getLogName()); - turnController.resetOtherTurnsControlled(); - targetPlayer.setGameUnderYourControl(true); + if (targetPlayer.getTurnControlledBy().equals(turnController.getId())) { + turnController.getPlayersUnderYourControl().remove(targetPlayer.getId()); + } } } } + spell.setCommandedBy(null); } } } diff --git a/Mage/src/main/java/mage/players/Player.java b/Mage/src/main/java/mage/players/Player.java index 6a51f097456..0bcb72fb3bb 100644 --- a/Mage/src/main/java/mage/players/Player.java +++ b/Mage/src/main/java/mage/players/Player.java @@ -279,6 +279,8 @@ public interface Player extends MageItem, Copyable { */ void setTurnControlledBy(UUID playerId); + List getTurnControllers(); + UUID getTurnControlledBy(); /** @@ -306,6 +308,8 @@ public interface Player extends MageItem, Copyable { */ void setGameUnderYourControl(boolean value); + void setGameUnderYourControl(boolean value, boolean fullRestore); + boolean isTestMode(); void setTestMode(boolean value); @@ -852,6 +856,8 @@ public interface Player extends MageItem, Copyable { Set getUsersAllowedToSeeHandCards(); + void setPayManaMode(boolean payManaMode); + boolean isInPayManaMode(); void setMatchPlayer(MatchPlayer matchPlayer); diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index a21a497df72..b5f0f40ddc4 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -156,6 +156,7 @@ public abstract class PlayerImpl implements Player, Serializable { protected boolean isGameUnderControl = true; protected UUID turnController; + protected List turnControllers = new ArrayList<>(); protected Set playersUnderYourControl = new HashSet<>(); protected Set usersAllowedToSeeHandCards = new HashSet<>(); @@ -263,6 +264,8 @@ public abstract class PlayerImpl implements Player, Serializable { this.isGameUnderControl = player.isGameUnderControl; this.turnController = player.turnController; + this.turnControllers.clear(); + this.turnControllers.addAll(player.turnControllers); this.passed = player.passed; this.passedTurn = player.passedTurn; @@ -343,6 +346,8 @@ public abstract class PlayerImpl implements Player, Serializable { this.isGameUnderControl = player.isGameUnderControl(); this.turnController = player.getTurnControlledBy(); + this.turnControllers.clear(); + this.turnControllers.addAll(player.getTurnControllers()); this.reachedNextTurnAfterLeaving = player.hasReachedNextTurnAfterLeaving(); this.castSourceIdWithAlternateMana = player.getCastSourceIdWithAlternateMana(); this.castSourceIdManaCosts = player.getCastSourceIdManaCosts(); @@ -398,6 +403,7 @@ public abstract class PlayerImpl implements Player, Serializable { this.turns = 0; this.isGameUnderControl = true; this.turnController = this.getId(); + this.turnControllers.clear(); this.playersUnderYourControl.clear(); this.passed = false; @@ -520,13 +526,13 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public void controlPlayersTurn(Game game, UUID playerId) { + Player player = game.getPlayer(playerId); + player.setTurnControlledBy(this.getId()); + game.informPlayers(getLogName() + " controls the turn of " + player.getLogName()); if (!playerId.equals(this.getId())) { this.playersUnderYourControl.add(playerId); - Player player = game.getPlayer(playerId); if (!player.hasLeft() && !player.hasLost()) { player.setGameUnderYourControl(false); - player.setTurnControlledBy(this.getId()); - game.informPlayers(getLogName() + " controls the turn of " + player.getLogName()); } DelayedTriggeredAbility ability = new AtTheEndOfTurnStepPostDelayedTriggeredAbility(new LoseControlOnOtherPlayersControllerEffect(this.getLogName(), player.getLogName())); ability.setSourceId(getId()); @@ -538,6 +544,12 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public void setTurnControlledBy(UUID playerId) { this.turnController = playerId; + this.turnControllers.add(playerId); + } + + @Override + public List getTurnControllers() { + return this.turnControllers; } @Override @@ -563,9 +575,27 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public void setGameUnderYourControl(boolean value) { + setGameUnderYourControl(value, true); + } + + @Override + public void setGameUnderYourControl(boolean value, boolean fullRestore) { this.isGameUnderControl = value; if (isGameUnderControl) { - this.turnController = getId(); + if (fullRestore) { + this.turnControllers.clear(); + this.turnController = getId(); + } else { + if (turnControllers.size() > 0) { + this.turnControllers.remove(turnControllers.size() - 1); + } + if (turnControllers.isEmpty()) { + this.turnController = getId(); + } else { + this.turnController = turnControllers.get(turnControllers.size() - 1); + isGameUnderControl = false; + } + } } } @@ -980,6 +1010,11 @@ public abstract class PlayerImpl implements Player, Serializable { return castSourceIdManaCosts; } + @Override + public void setPayManaMode(boolean payManaMode) { + this.payManaMode = payManaMode; + } + @Override public boolean isInPayManaMode() { return payManaMode; From d03fd6eea39ae08759be22f84880974611720f7b Mon Sep 17 00:00:00 2001 From: L_J Date: Fri, 8 Jun 2018 18:41:06 +0000 Subject: [PATCH 11/11] Drain Power fix for Rhystic Cave (#4894) --- Mage.Sets/src/mage/cards/d/DrainPower.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Mage.Sets/src/mage/cards/d/DrainPower.java b/Mage.Sets/src/mage/cards/d/DrainPower.java index 6a21c6cbe1f..e97d7a89672 100644 --- a/Mage.Sets/src/mage/cards/d/DrainPower.java +++ b/Mage.Sets/src/mage/cards/d/DrainPower.java @@ -78,6 +78,7 @@ class DrainPowerEffect extends OneShotEffect { TargetPermanent target = null; while (true) { + targetPlayer.setPayManaMode(true); manaAbilitiesMap.clear(); for (Permanent permanent : game.getBattlefield().getAllActivePermanents(filter, targetPlayer.getId(), game)) { if (!ignorePermanents.contains(permanent)) { @@ -138,6 +139,7 @@ class DrainPowerEffect extends OneShotEffect { } } } + targetPlayer.setPayManaMode(false); // 106.12. One card (Drain Power) causes one player to lose unspent mana and another to add “the mana lost this way.” (Note that these may be the same player.) // This empties the former player’s mana pool and causes the mana emptied this way to be put into the latter player’s mana pool. Which permanents, spells, and/or