diff --git a/Mage.Sets/src/mage/cards/d/DrainPower.java b/Mage.Sets/src/mage/cards/d/DrainPower.java index e07e7f3c1c5..e22c3f803d4 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 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..d03d0332667 --- /dev/null +++ b/Mage.Sets/src/mage/cards/w/WordOfCommand.java @@ -0,0 +1,251 @@ + +package mage.cards.w; + +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; +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 sourceController = game.getPlayer(source.getControllerId()); + Player targetPlayer = game.getPlayer(source.getFirstTarget()); + MageObject sourceObject = game.getObject(source.getSourceId()); + Card card = 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()); + 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) { + // 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); + + // 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(); + + 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) { + 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 + manaPool = targetPlayer.getManaPool(); // a rollback creates a new mana pool for the player, so it's necessary to find it again + manaPool.setForcedToPay(false); + game.removeBookmark(bookmark); + targetPlayer.resetStoredBookmark(game); + for (RestrictionEffect eff : game.getContinuousEffects().getRestrictionEffects()) { + if (eff instanceof WordOfCommandCantActivateEffect) { + eff.discard(); + break; + } + } + 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 + } + } + + wordOfCommand = game.getSpell(source.getSourceId()); + if (wordOfCommand != null) { + wordOfCommand.setCommandedBy(controller.getId()); // You control the player until Word of Command finishes resolving + } else { + targetPlayer.setGameUnderYourControl(true, false); + if (!targetPlayer.getTurnControlledBy().equals(controller.getId())) { + controller.getPlayersUnderYourControl().remove(targetPlayer.getId()); + } + } + return true; + } + 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 { + + 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 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.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 4ba79ba3ed6..3a2f6c7ee11 100644 --- a/Mage.Sets/src/mage/sets/MastersEditionIV.java +++ b/Mage.Sets/src/mage/sets/MastersEditionIV.java @@ -298,6 +298,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/costs/mana/ManaCostImpl.java b/Mage/src/main/java/mage/abilities/costs/mana/ManaCostImpl.java index 1c51f624089..082f09c47db 100644 --- a/Mage/src/main/java/mage/abilities/costs/mana/ManaCostImpl.java +++ b/Mage/src/main/java/mage/abilities/costs/mana/ManaCostImpl.java @@ -207,7 +207,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 e7fc43af0bf..dbf25f6df43 100644 --- a/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java +++ b/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java @@ -117,7 +117,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(); @@ -235,10 +237,15 @@ 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; } + 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) { @@ -318,6 +325,31 @@ public class ManaCostsImpl extends ArrayList implements M } // stop using mana of the clicked mana type pool.lockManaType(); + if (!wasUnlockedManaType) { + handleForcedToPayOnlyForCurrentPayment(game, pool, referenceCosts); + } + } + + private void handleForcedToPayOnlyForCurrentPayment(Game game, ManaPool pool, ManaCosts referenceCosts) { + // for Word of Command + if (pool.isForcedToPay()) { + 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(referenceCosts.getX()); + player.getManaPool().restoreMana(pool.getPoolBookmark()); + game.bookmarkState(); + } + } + } + } + + public void forceManaRollback(Game game, ManaPool pool) { + // for Word of Command + handleForcedToPayOnlyForCurrentPayment(game, pool, this); } @Override diff --git a/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java b/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java index 374c7e3db3d..0fd1aee39b3 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)) { diff --git a/Mage/src/main/java/mage/game/Game.java b/Mage/src/main/java/mage/game/Game.java index e499bf960b7..8ab4089a447 100644 --- a/Mage/src/main/java/mage/game/Game.java +++ b/Mage/src/main/java/mage/game/Game.java @@ -398,6 +398,8 @@ public interface Game extends MageItem, Serializable { void playPriority(UUID activePlayerId, boolean resuming); + void resetControlAfterSpellResolve(UUID topId); + boolean endTurn(Ability source); int doAction(MageAction action); diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index abfd93c49dc..8ce147c347a 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -1395,6 +1395,7 @@ public abstract class GameImpl implements Game, Serializable { try { top = state.getStack().peek(); 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 @@ -1408,6 +1409,37 @@ public abstract class GameImpl implements Game, Serializable { } } } + + @Override + public void resetControlAfterSpellResolve(UUID topId) { + // for Word of Command + 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) { + targetPlayer.setGameUnderYourControl(true, false); + informPlayers(turnController.getLogName() + " lost control over " + targetPlayer.getLogName()); + if (targetPlayer.getTurnControlledBy().equals(turnController.getId())) { + turnController.getPlayersUnderYourControl().remove(targetPlayer.getId()); + } + } + } + } + spell.setCommandedBy(null); + } + } + } /** * This checks if the stack gets filled iterated, without ever getting empty diff --git a/Mage/src/main/java/mage/game/stack/Spell.java b/Mage/src/main/java/mage/game/stack/Spell.java index 3d137719f8a..281a6485dd7 100644 --- a/Mage/src/main/java/mage/game/stack/Spell.java +++ b/Mage/src/main/java/mage/game/stack/Spell.java @@ -67,6 +67,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 @@ -121,6 +122,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; @@ -179,6 +181,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; @@ -1024,4 +1032,12 @@ 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; + } + + public UUID getCommandedBy() { + return commandedBy; + } + } diff --git a/Mage/src/main/java/mage/game/stack/SpellStack.java b/Mage/src/main/java/mage/game/stack/SpellStack.java index 8b697ba5345..40b5c963883 100644 --- a/Mage/src/main/java/mage/game/stack/SpellStack.java +++ b/Mage/src/main/java/mage/game/stack/SpellStack.java @@ -37,10 +37,11 @@ public class SpellStack extends ArrayDeque { try { top = this.peek(); 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 e66b118a8b1..f95b01e1361 100644 --- a/Mage/src/main/java/mage/players/ManaPool.java +++ b/Mage/src/main/java/mage/players/ManaPool.java @@ -35,6 +35,8 @@ 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 List poolBookmark = new ArrayList<>(); // mana pool bookmark for rollback purposes private final Set doNotEmptyManaTypes = new HashSet<>(); @@ -43,6 +45,7 @@ public class ManaPool implements Serializable { autoPayment = true; autoPaymentRestricted = true; unlockedManaType = null; + forcedToPay = false; } public ManaPool(final ManaPool pool) { @@ -53,6 +56,10 @@ public class ManaPool implements Serializable { this.autoPayment = pool.autoPayment; 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); } @@ -87,12 +94,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) { @@ -111,6 +118,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)) { @@ -120,7 +128,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; } @@ -436,19 +444,19 @@ public class ManaPool implements Serializable { } public boolean isAutoPayment() { - return autoPayment; + return autoPayment || forcedToPay; } public void setAutoPayment(boolean autoPayment) { this.autoPayment = autoPayment; } - public void setAutoPaymentRestricted(boolean autoPaymentRestricted) { - this.autoPaymentRestricted = autoPaymentRestricted; + public boolean isAutoPaymentRestricted() { + return autoPaymentRestricted || forcedToPay; } - public boolean isAutoPaymentRestricted() { - return autoPaymentRestricted; + public void setAutoPaymentRestricted(boolean autoPaymentRestricted) { + this.autoPaymentRestricted = autoPaymentRestricted; } public ManaType getUnlockedManaType() { @@ -490,4 +498,39 @@ 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; + } + + 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); + } + } } diff --git a/Mage/src/main/java/mage/players/Player.java b/Mage/src/main/java/mage/players/Player.java index 7745a768e07..415dfec7c14 100644 --- a/Mage/src/main/java/mage/players/Player.java +++ b/Mage/src/main/java/mage/players/Player.java @@ -278,6 +278,8 @@ public interface Player extends MageItem, Copyable { */ void setTurnControlledBy(UUID playerId); + List getTurnControllers(); + UUID getTurnControlledBy(); /** @@ -305,6 +307,8 @@ public interface Player extends MageItem, Copyable { */ void setGameUnderYourControl(boolean value); + void setGameUnderYourControl(boolean value, boolean fullRestore); + boolean isTestMode(); void setTestMode(boolean value); @@ -856,6 +860,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 423ae4e208a..c7acd2f44a4 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -155,6 +155,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<>(); @@ -262,6 +263,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; @@ -342,6 +345,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(); @@ -397,6 +402,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; @@ -523,13 +529,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()); @@ -541,6 +547,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 @@ -566,9 +578,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; + } + } } } @@ -983,6 +1013,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;