From c2e7b02e13d4f4a39abf773f351db0f36285ee9a Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Fri, 19 Jun 2020 13:09:45 +0400 Subject: [PATCH] Reworked and improved special mana payment abilities (convoke, delve, assist, improvise): * now it can be used to calc and find available mana and playable abilities; * now tests and AI can use that abilities; * now it follows mtg's rules and restrictions for mana activation order (rule 601.2f, see #768); --- .../src/mage/player/human/HumanPlayer.java | 89 ++++++---- .../asthough/PlayFromNonHandZoneTest.java | 115 +++++++----- .../java/mage/abilities/SpecialAction.java | 50 ++++-- .../costs/mana/ActivationManaAbilityStep.java | 25 +++ .../mana/AlternateManaPaymentAbility.java | 24 ++- .../effects/ContinuousEffectImpl.java | 6 +- .../mana/ActivatedManaAbilityImpl.java | 20 ++- Mage/src/main/java/mage/game/stack/Spell.java | 23 ++- .../main/java/mage/players/PlayerImpl.java | 166 ++++++++++-------- 9 files changed, 341 insertions(+), 177 deletions(-) create mode 100644 Mage/src/main/java/mage/abilities/costs/mana/ActivationManaAbilityStep.java diff --git a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java index feb685d60cb..e38c162e8d6 100644 --- a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java +++ b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java @@ -10,6 +10,7 @@ import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.RequirementEffect; import mage.abilities.hint.HintUtils; import mage.abilities.mana.ActivatedManaAbilityImpl; +import mage.abilities.mana.ManaAbility; import mage.cards.Card; import mage.cards.Cards; import mage.cards.decks.Deck; @@ -61,6 +62,8 @@ import static mage.constants.PlayerAction.TRIGGER_AUTO_ORDER_RESET_ALL; */ public class HumanPlayer extends PlayerImpl { + private static final boolean ALLOW_USERS_TO_PUT_NON_PLAYABLE_SPELLS_ON_STACK_WORKAROUND = false; // warning, see workaround's info on usage + private transient Boolean responseOpenedForAnswer = false; // can't get response until prepared target (e.g. until send all fire events to all players) private final transient PlayerResponse response = new PlayerResponse(); @@ -1039,7 +1042,7 @@ public class HumanPlayer extends PlayerImpl { if (response.getString() != null && response.getString().equals("special")) { - specialAction(game); + activateSpecialAction(game, null); } else if (response.getUUID() != null) { boolean result = false; MageObject object = game.getObject(response.getUUID()); @@ -1057,6 +1060,24 @@ public class HumanPlayer extends PlayerImpl { } if (actingPlayer != null) { useableAbilities = actingPlayer.getPlayableActivatedAbilities(object, zone, game); + + // GUI: workaround to enable users to put spells on stack without real available mana + // (without highlighting, like it was in old versions before June 2020) + // Reason: some gain ability adds cost modification and other things to spells on stack only, + // e.g. xmage can't find playable ability before put that spell on stack (wtf example: Chief Engineer, + // see ConvokeTest) + // TODO: it's a BAD workaround -- users can't see that card/ability is broken and will not report to us, AI can't play that ability too + // Enable it on massive broken cards/abilities only or for manual tests + if (ALLOW_USERS_TO_PUT_NON_PLAYABLE_SPELLS_ON_STACK_WORKAROUND) { + if (object instanceof Card) { + for (Ability ability : ((Card) object).getAbilities(game)) { + if (ability instanceof SpellAbility && ((SpellAbility) ability).canActivate(actingPlayer.getId(), game).canActivate() + || ability instanceof PlayLandAbility) { + useableAbilities.putIfAbsent(ability.getId(), (ActivatedAbility) ability); + } + } + } + } } if (object instanceof Card @@ -1221,7 +1242,7 @@ public class HumanPlayer extends PlayerImpl { } else if (response.getString() != null && response.getString().equals("special")) { if (unpaid instanceof ManaCostsImpl) { - specialManaAction(unpaid, game); + activateSpecialAction(game, unpaid); } } else if (response.getManaType() != null) { // this mana type can be paid once from pool @@ -1336,14 +1357,26 @@ public class HumanPlayer extends PlayerImpl { if (object == null) { return; } - if (AbilityType.SPELL.equals(abilityToCast.getAbilityType())) { + + // GUI: for user's information only - check if mana abilities allows to use here (getUseableManaAbilities already filter it) + // Reason: when you use special mana ability then normal mana abilities will be restricted to pay. Users + // can't see lands as playable and must know the reason (if they click on land then they get that message) + if (abilityToCast.getAbilityType() == AbilityType.SPELL) { Spell spell = game.getStack().getSpell(abilityToCast.getSourceId()); - if (spell != null && !spell.isResolving() - && spell.isDoneActivatingManaAbilities()) { - game.informPlayer(this, "You can no longer use activated mana abilities to pay for the current spell. Cancel and recast the spell and activate mana abilities first."); - return; + boolean haveManaAbilities = object.getAbilities().stream().anyMatch(a -> a instanceof ManaAbility); + if (spell != null && !spell.isResolving() && haveManaAbilities) { + switch (spell.getCurrentActivatingManaAbilitiesStep()) { + // if you used special mana ability like convoke then normal mana abilities will be restricted to use, see Convoke for details + case BEFORE: + case NORMAL: + break; + case AFTER: + game.informPlayer(this, "You can no longer use activated mana abilities to pay for the current spell (special mana pay already used). Cancel and recast the spell to activate mana abilities first."); + return; + } } } + Zone zone = game.getState().getZone(object.getId()); if (zone != null) { LinkedHashMap useableAbilities = getUseableManaAbilities(object, zone, game); @@ -1844,7 +1877,13 @@ public class HumanPlayer extends PlayerImpl { draft.firePickCardEvent(playerId); } - protected void specialAction(Game game) { + /** + * Activate special action (normal or mana) + * + * @param game + * @param unpaidForManaAction - set unpaid for mana actions like convoke + */ + protected void activateSpecialAction(Game game, ManaCost unpaidForManaAction) { if (gameInCheckPlayableState(game)) { return; } @@ -1853,7 +1892,7 @@ public class HumanPlayer extends PlayerImpl { return; } - Map specialActions = game.getState().getSpecialActions().getControlledBy(playerId, false); + Map specialActions = game.getState().getSpecialActions().getControlledBy(playerId, unpaidForManaAction != null); if (!specialActions.isEmpty()) { updateGameStatePriority("specialAction", game); @@ -1863,39 +1902,13 @@ public class HumanPlayer extends PlayerImpl { } waitForResponse(game); - if (response.getUUID() != null) { - if (specialActions.containsKey(response.getUUID())) { - activateAbility(specialActions.get(response.getUUID()), game); - } - } - } - } - - protected void specialManaAction(ManaCost unpaid, Game game) { - if (gameInCheckPlayableState(game)) { - return; - } - - if (!canRespond()) { - return; - } - - Map specialActions = game.getState().getSpecialActions().getControlledBy(playerId, true); - if (!specialActions.isEmpty()) { - updateGameStatePriority("specialAction", game); - prepareForResponse(game); - if (!isExecutingMacro()) { - game.fireGetChoiceEvent(playerId, name, null, new ArrayList<>(specialActions.values())); - } - waitForResponse(game); - if (response.getUUID() != null) { if (specialActions.containsKey(response.getUUID())) { SpecialAction specialAction = specialActions.get(response.getUUID()); - if (specialAction != null) { - specialAction.setUnpaidMana(unpaid); - activateAbility(specialActions.get(response.getUUID()), game); + if (unpaidForManaAction != null) { + specialAction.setUnpaidMana(unpaidForManaAction); } + activateAbility(specialAction, game); } } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/asthough/PlayFromNonHandZoneTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/asthough/PlayFromNonHandZoneTest.java index 1c0480fc1b8..1e145efc24c 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/asthough/PlayFromNonHandZoneTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/asthough/PlayFromNonHandZoneTest.java @@ -273,7 +273,7 @@ public class PlayFromNonHandZoneTest extends CardTestPlayerBase { * the discard was required. In the first log, when it was cast after * Angelic Purge the discard was not required. */ - + @Test public void castFromExileButWithAdditionalCostTest() { // Ninjutsu {2}{U}{B} @@ -282,7 +282,7 @@ public class PlayFromNonHandZoneTest extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerB, "Fallen Shinobi", 1); // Creature 5/4 addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion"); addCard(Zone.HAND, playerB, "Pillarfield Ox"); - + addCard(Zone.LIBRARY, playerB, "Pillarfield Ox"); // Card to draw on turn 2 // As an additional cost to cast Tormenting Voice, discard a card. // Draw two cards. @@ -290,14 +290,14 @@ public class PlayFromNonHandZoneTest extends CardTestPlayerBase { // As an additional cost to cast this spell, sacrifice a creature. // Flying, Trample addCard(Zone.LIBRARY, playerA, "Demon of Catastrophes"); // Creature {2}{B}{B} 6/6 - + skipInitShuffling(); attack(2, playerB, "Fallen Shinobi"); - + castSpell(2, PhaseStep.POSTCOMBAT_MAIN, playerB, "Tormenting Voice"); setChoice(playerB, "Pillarfield Ox"); // Discord for Tormenting Voice - + castSpell(2, PhaseStep.POSTCOMBAT_MAIN, playerB, "Demon of Catastrophes"); setChoice(playerB, "Silvercoat Lion"); // Sacrifice for Demon @@ -309,20 +309,20 @@ public class PlayFromNonHandZoneTest extends CardTestPlayerBase { assertLife(playerA, 15); assertPermanentCount(playerB, "Fallen Shinobi", 1); - - assertGraveyardCount(playerA, "Tormenting Voice", 1); - assertGraveyardCount(playerB, "Pillarfield Ox", 1); // Discarded for Tormenting Voice - - + + assertGraveyardCount(playerA, "Tormenting Voice", 1); + assertGraveyardCount(playerB, "Pillarfield Ox", 1); // Discarded for Tormenting Voice + + assertPermanentCount(playerB, "Demon of Catastrophes", 1); - assertGraveyardCount(playerB, "Silvercoat Lion", 1); // sacrificed for Demon - + assertGraveyardCount(playerB, "Silvercoat Lion", 1); // sacrificed for Demon + assertHandCount(playerB, "Pillarfield Ox", 1); assertHandCount(playerB, 3); // 2 from Tormenting Voice + 1 from Turn 2 assertExileCount(playerA, 0); // Both exiled cards are cast - } - - + } + + @Test public void castFromExileButWithAdditionalCost2Test() { // Ninjutsu {2}{U}{B} @@ -331,7 +331,7 @@ public class PlayFromNonHandZoneTest extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerB, "Fallen Shinobi", 1); // Creature 5/4 addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion"); addCard(Zone.HAND, playerB, "Pillarfield Ox"); - + addCard(Zone.BATTLEFIELD, playerA, "Amulet of Kroog"); // Just to exile for Angelic Purge // As an additional cost to cast Tormenting Voice, discard a card. @@ -341,18 +341,18 @@ public class PlayFromNonHandZoneTest extends CardTestPlayerBase { // As an additional cost to cast Angelic Purge, sacrifice a permanent. // Exile target artifact, creature, or enchantment. addCard(Zone.LIBRARY, playerA, "Angelic Purge"); // Sorcery {2}{W} - + skipInitShuffling(); attack(2, playerB, "Fallen Shinobi"); - + castSpell(2, PhaseStep.POSTCOMBAT_MAIN, playerB, "Angelic Purge"); setChoice(playerB, "Silvercoat Lion"); // Sacrifice for Purge addTarget(playerB, "Amulet of Kroog"); // Exile with Purge - + castSpell(2, PhaseStep.POSTCOMBAT_MAIN, playerB, "Tormenting Voice"); setChoice(playerB, "Pillarfield Ox"); // Discord for Tormenting Voice - + setStopAt(2, PhaseStep.END_TURN); setStrictChooseMode(true); @@ -361,27 +361,27 @@ public class PlayFromNonHandZoneTest extends CardTestPlayerBase { assertLife(playerA, 15); assertPermanentCount(playerB, "Fallen Shinobi", 1); - - assertGraveyardCount(playerA, "Angelic Purge", 1); - assertGraveyardCount(playerB, "Silvercoat Lion", 1); // sacrificed for Purge - assertGraveyardCount(playerA, "Tormenting Voice", 1); - assertGraveyardCount(playerB, "Pillarfield Ox", 1); // Discarded for Tormenting Voice - + assertGraveyardCount(playerA, "Angelic Purge", 1); + assertGraveyardCount(playerB, "Silvercoat Lion", 1); // sacrificed for Purge + + assertGraveyardCount(playerA, "Tormenting Voice", 1); + assertGraveyardCount(playerB, "Pillarfield Ox", 1); // Discarded for Tormenting Voice + assertHandCount(playerB, 3); // 2 from Tormenting Voice + 1 from Turn 2 draw - + assertExileCount(playerA, 1); // Both exiled cards are cast assertExileCount(playerA, "Amulet of Kroog", 1); // Exiled with Purge - } - - @Test - public void castAdventureWithFallenShinobiTest() { + } + + @Test + public void castAdventureWithFallenShinobiTest() { // Ninjutsu {2}{U}{B} // Whenever Fallen Shinobi deals combat damage to a player, that player exiles the top two cards // of their library. Until end of turn, you may play those cards without paying their mana cost. addCard(Zone.BATTLEFIELD, playerB, "Fallen Shinobi", 1); // Creature 5/4 addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion"); - + addCard(Zone.BATTLEFIELD, playerA, "Amulet of Kroog"); // Just to exile for Angelic Purge /* Curious Pair {1}{G} @@ -391,23 +391,23 @@ public class PlayFromNonHandZoneTest extends CardTestPlayerBase { * Treats to Share {G} * Sorcery — Adventure * Create a Food token. - */ - addCard(Zone.LIBRARY, playerA, "Curious Pair"); + */ + addCard(Zone.LIBRARY, playerA, "Curious Pair"); // As an additional cost to cast Angelic Purge, sacrifice a permanent. // Exile target artifact, creature, or enchantment. addCard(Zone.LIBRARY, playerA, "Angelic Purge"); // Sorcery {2}{W} - + skipInitShuffling(); attack(2, playerB, "Fallen Shinobi"); - + castSpell(2, PhaseStep.POSTCOMBAT_MAIN, playerB, "Angelic Purge"); setChoice(playerB, "Silvercoat Lion"); // Sacrifice for Purge addTarget(playerB, "Amulet of Kroog"); // Exile with Purge - + castSpell(2, PhaseStep.POSTCOMBAT_MAIN, playerB, "Treats to Share"); - + setStopAt(2, PhaseStep.END_TURN); setStrictChooseMode(true); @@ -416,17 +416,46 @@ public class PlayFromNonHandZoneTest extends CardTestPlayerBase { assertLife(playerA, 15); assertPermanentCount(playerB, "Fallen Shinobi", 1); - + assertGraveyardCount(playerA, "Angelic Purge", 1); - assertGraveyardCount(playerB, "Silvercoat Lion", 1); // sacrificed for Purge + assertGraveyardCount(playerB, "Silvercoat Lion", 1); // sacrificed for Purge assertPermanentCount(playerB, "Food", 1); assertExileCount(playerA, "Curious Pair", 1); - + assertHandCount(playerB, 1); // 1 from Turn 2 draw - + assertExileCount(playerA, 2); // Both exiled cards are cast assertExileCount(playerA, "Amulet of Kroog", 1); // Exiled with Purge } - + + @Test + public void test_ActivateFromOpponentCreature() { + // Players can’t search libraries. Any player may pay {2} for that player to ignore this effect until end of turn. + addCard(Zone.BATTLEFIELD, playerB, "Leonin Arbiter", 1); + // + // {3}{U}{U} + // Search target opponent’s library for an artifact card and put that card onto the battlefield under your control. Then that player shuffles their library. + addCard(Zone.HAND, playerA, "Acquire", 2); + addCard(Zone.BATTLEFIELD, playerA, "Island", 5 * 2 + 2); + addCard(Zone.LIBRARY, playerB, "Alpha Myr", 1); + + // first cast -- can't search library + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Acquire", playerB); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + // second cast -- unlock library and search + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}: Any player may"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + // + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Acquire", playerB); + addTarget(playerA, "Alpha Myr"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Alpha Myr", 1); + } } diff --git a/Mage/src/main/java/mage/abilities/SpecialAction.java b/Mage/src/main/java/mage/abilities/SpecialAction.java index b4cc07c7313..fd0a85092f5 100644 --- a/Mage/src/main/java/mage/abilities/SpecialAction.java +++ b/Mage/src/main/java/mage/abilities/SpecialAction.java @@ -1,18 +1,22 @@ - - package mage.abilities; +import mage.abilities.costs.mana.AlternateManaPaymentAbility; import mage.abilities.costs.mana.ManaCost; +import mage.abilities.mana.ManaOptions; import mage.constants.AbilityType; import mage.constants.Zone; +import mage.game.Game; +import mage.game.stack.Spell; +import mage.game.stack.StackObject; + +import java.util.UUID; /** - * - * @author BetaSteward_at_googlemail.com + * @author BetaSteward_at_googlemail.com, JayDi85 */ public abstract class SpecialAction extends ActivatedAbilityImpl { - private boolean manaAction; + private final AlternateManaPaymentAbility manaAbility; // mana actions generates on every pay cycle, no need to copy it protected ManaCost unpaidMana; public SpecialAction() { @@ -20,22 +24,23 @@ public abstract class SpecialAction extends ActivatedAbilityImpl { } public SpecialAction(Zone zone) { - this(zone, false); + this(zone, null); } - public SpecialAction(Zone zone, boolean manaAction) { + + public SpecialAction(Zone zone, AlternateManaPaymentAbility manaAbility) { super(AbilityType.SPECIAL_ACTION, zone); this.usesStack = false; - this.manaAction = manaAction; + this.manaAbility = manaAbility; } public SpecialAction(final SpecialAction action) { super(action); - this.manaAction = action.manaAction; this.unpaidMana = action.unpaidMana; + this.manaAbility = action.manaAbility; } public boolean isManaAction() { - return manaAction; + return manaAbility != null; } public void setUnpaidMana(ManaCost manaCost) { @@ -45,4 +50,29 @@ public abstract class SpecialAction extends ActivatedAbilityImpl { public ManaCost getUnpaidMana() { return unpaidMana; } + + public ManaOptions getManaOptions(Ability source, Game game, ManaCost unpaid) { + if (manaAbility != null) { + return manaAbility.getManaOptions(source, game, unpaid); + } + return null; + } + + @Override + public ActivationStatus canActivate(UUID playerId, Game game) { + if (isManaAction()) { + // limit play mana abilities by steps + int currentStepOrder = 0; + if (!game.getStack().isEmpty()) { + StackObject stackObject = game.getStack().getFirst(); + if (stackObject instanceof Spell) { + currentStepOrder = ((Spell) stackObject).getCurrentActivatingManaAbilitiesStep().getStepOrder(); + } + } + if (currentStepOrder > manaAbility.useOnActivationManaAbilityStep().getStepOrder()) { + return ActivationStatus.getFalse(); + } + } + return super.canActivate(playerId, game); + } } diff --git a/Mage/src/main/java/mage/abilities/costs/mana/ActivationManaAbilityStep.java b/Mage/src/main/java/mage/abilities/costs/mana/ActivationManaAbilityStep.java new file mode 100644 index 00000000000..27a89c47664 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/costs/mana/ActivationManaAbilityStep.java @@ -0,0 +1,25 @@ +package mage.abilities.costs.mana; + +/** + * Some special AlternateManaPaymentAbility must be restricted to pay before or after mana abilities. + * Game logic: if you use special mana ability then normal mana abilities must be restricted and vice versa, + * see Convoke for more info and rules + * + * @author JayDi85 + */ + +public enum ActivationManaAbilityStep { + BEFORE(0), // assist + NORMAL(1), // all activated mana abilities + AFTER(2); // convoke, delve, improvise + + private final int stepOrder; + + ActivationManaAbilityStep(int stepOrder) { + this.stepOrder = stepOrder; + } + + public int getStepOrder() { + return stepOrder; + } +} diff --git a/Mage/src/main/java/mage/abilities/costs/mana/AlternateManaPaymentAbility.java b/Mage/src/main/java/mage/abilities/costs/mana/AlternateManaPaymentAbility.java index d9df07ba03b..e3769cbcecc 100644 --- a/Mage/src/main/java/mage/abilities/costs/mana/AlternateManaPaymentAbility.java +++ b/Mage/src/main/java/mage/abilities/costs/mana/AlternateManaPaymentAbility.java @@ -1,18 +1,17 @@ - package mage.abilities.costs.mana; import mage.abilities.Ability; +import mage.abilities.mana.ManaOptions; import mage.game.Game; /** * Interface for abilities that allow the player to pay mana costs of a spell in alternate ways. * For the payment SpecialActions are used. - * + *

* Example of such an alternate payment ability: {@link mage.abilities.keyword.DelveAbility} * - * @author LevelX2 + * @author LevelX2, JayDi85 */ -@FunctionalInterface public interface AlternateManaPaymentAbility { /** * Adds the special action to the state, that allows the user to do the alternate mana payment @@ -22,4 +21,21 @@ public interface AlternateManaPaymentAbility { * @param unpaid unapaid mana costs of the spell */ void addSpecialAction(Ability source, Game game, ManaCost unpaid); + + /** + * All possible mana payments that can make that ability (uses to find playable abilities) + * + * @param source + * @param game + * @param unpaid + * @return + */ + ManaOptions getManaOptions(Ability source, Game game, ManaCost unpaid); + + /** + * Mana payment step where you can use it + * + * @return + */ + ActivationManaAbilityStep useOnActivationManaAbilityStep(); } \ No newline at end of file diff --git a/Mage/src/main/java/mage/abilities/effects/ContinuousEffectImpl.java b/Mage/src/main/java/mage/abilities/effects/ContinuousEffectImpl.java index e2f8e2f159e..37671eed41a 100644 --- a/Mage/src/main/java/mage/abilities/effects/ContinuousEffectImpl.java +++ b/Mage/src/main/java/mage/abilities/effects/ContinuousEffectImpl.java @@ -1,10 +1,10 @@ package mage.abilities.effects; -import java.util.*; import mage.MageObjectReference; import mage.abilities.Ability; import mage.abilities.CompoundAbility; import mage.abilities.MageSingleton; +import mage.abilities.costs.mana.ActivationManaAbilityStep; import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.dynamicvalue.common.DomainValue; import mage.abilities.dynamicvalue.common.SignInversionDynamicValue; @@ -20,6 +20,8 @@ import mage.game.stack.StackObject; import mage.players.Player; import mage.target.targetpointer.TargetPointer; +import java.util.*; + /** * @author BetaSteward_at_googlemail.com, JayDi85 */ @@ -416,7 +418,7 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu StackObject stackObject = game.getStack().getFirst(); return !(stackObject instanceof Spell) || !Zone.LIBRARY.equals(((Spell) stackObject).getFromZone()) - || ((Spell) stackObject).isDoneActivatingManaAbilities(); + || ((Spell) stackObject).getCurrentActivatingManaAbilitiesStep() == ActivationManaAbilityStep.AFTER; // mana payment finished } return true; } diff --git a/Mage/src/main/java/mage/abilities/mana/ActivatedManaAbilityImpl.java b/Mage/src/main/java/mage/abilities/mana/ActivatedManaAbilityImpl.java index 5ebda8934b3..85eae1ea4a7 100644 --- a/Mage/src/main/java/mage/abilities/mana/ActivatedManaAbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/mana/ActivatedManaAbilityImpl.java @@ -10,6 +10,8 @@ import mage.constants.AsThoughEffectType; import mage.constants.TimingRule; import mage.constants.Zone; import mage.game.Game; +import mage.game.stack.Spell; +import mage.game.stack.StackObject; import java.util.ArrayList; import java.util.List; @@ -54,10 +56,24 @@ public abstract class ActivatedManaAbilityImpl extends ActivatedAbilityImpl impl && null == game.getContinuousEffects().asThough(sourceId, AsThoughEffectType.ACTIVATE_AS_INSTANT, this, controllerId, game)) { return ActivationStatus.getFalse(); } - // check if player is in the process of playing spell costs and they are no longer allowed to use activated mana abilities (e.g. because they started to use improvise) + + // check if player is in the process of playing spell costs and they are no longer allowed to use + // activated mana abilities (e.g. because they started to use improvise or convoke) + if (!game.getStack().isEmpty()) { + StackObject stackObject = game.getStack().getFirst(); + if (stackObject instanceof Spell) { + switch (((Spell) stackObject).getCurrentActivatingManaAbilitiesStep()) { + case BEFORE: + case NORMAL: + break; + case AFTER: + return ActivationStatus.getFalse(); + } + } + } + //20091005 - 605.3a return new ActivationStatus(costs.canPay(this, sourceId, controllerId, game), null); - } /** diff --git a/Mage/src/main/java/mage/game/stack/Spell.java b/Mage/src/main/java/mage/game/stack/Spell.java index a9b5dd2b93f..11a2562aa7d 100644 --- a/Mage/src/main/java/mage/game/stack/Spell.java +++ b/Mage/src/main/java/mage/game/stack/Spell.java @@ -1,6 +1,5 @@ package mage.game.stack; -import java.util.*; import mage.MageInt; import mage.MageObject; import mage.Mana; @@ -11,6 +10,7 @@ import mage.abilities.Mode; import mage.abilities.SpellAbility; import mage.abilities.costs.AlternativeSourceCosts; import mage.abilities.costs.Cost; +import mage.abilities.costs.mana.ActivationManaAbilityStep; import mage.abilities.costs.mana.ManaCost; import mage.abilities.costs.mana.ManaCosts; import mage.abilities.keyword.BestowAbility; @@ -32,6 +32,11 @@ import mage.players.Player; import mage.util.GameLog; import mage.util.SubTypeList; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.UUID; + /** * @author BetaSteward_at_googlemail.com */ @@ -62,7 +67,7 @@ public class Spell extends StackObjImpl implements Card { 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 + private ActivationManaAbilityStep currentActivatingManaAbilitiesStep = ActivationManaAbilityStep.BEFORE; public Spell(Card card, SpellAbility ability, UUID controllerId, Zone fromZone) { this.card = card; @@ -118,12 +123,12 @@ public class Spell extends StackObjImpl implements Card { this.resolving = spell.resolving; this.commandedBy = spell.commandedBy; - this.doneActivatingManaAbilities = spell.doneActivatingManaAbilities; + this.currentActivatingManaAbilitiesStep = spell.currentActivatingManaAbilitiesStep; this.targetChanged = spell.targetChanged; } public boolean activate(Game game, boolean noMana) { - setDoneActivatingManaAbilities(false); // used for e.g. improvise + setCurrentActivatingManaAbilitiesStep(ActivationManaAbilityStep.BEFORE); // mana payment step started, can use any mana abilities, see AlternateManaPaymentAbility if (!ability.activate(game, noMana)) { return false; @@ -147,7 +152,7 @@ public class Spell extends StackObjImpl implements Card { return false; } } - setDoneActivatingManaAbilities(true); // can be activated again maybe during the resolution of the spell (e.g. Metallic Rebuke) + setCurrentActivatingManaAbilitiesStep(ActivationManaAbilityStep.NORMAL); return true; } @@ -401,12 +406,12 @@ public class Spell extends StackObjImpl implements Card { } } - public boolean isDoneActivatingManaAbilities() { - return doneActivatingManaAbilities; + public ActivationManaAbilityStep getCurrentActivatingManaAbilitiesStep() { + return this.currentActivatingManaAbilitiesStep; } - public void setDoneActivatingManaAbilities(boolean doneActivatingManaAbilities) { - this.doneActivatingManaAbilities = doneActivatingManaAbilities; + public void setCurrentActivatingManaAbilitiesStep(ActivationManaAbilityStep currentActivatingManaAbilitiesStep) { + this.currentActivatingManaAbilitiesStep = currentActivatingManaAbilitiesStep; } @Override diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index 742f456bb48..bbc47b11935 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -1,9 +1,6 @@ package mage.players; import com.google.common.collect.ImmutableMap; -import java.io.Serializable; -import java.util.*; -import java.util.Map.Entry; import mage.ConditionalMana; import mage.MageObject; import mage.MageObjectReference; @@ -15,6 +12,7 @@ import mage.abilities.common.PlayLandAsCommanderAbility; import mage.abilities.common.WhileSearchingPlayFromLibraryAbility; import mage.abilities.common.delayed.AtTheEndOfTurnStepPostDelayedTriggeredAbility; import mage.abilities.costs.*; +import mage.abilities.costs.mana.AlternateManaPaymentAbility; import mage.abilities.costs.mana.ManaCost; import mage.abilities.costs.mana.ManaCosts; import mage.abilities.costs.mana.ManaCostsImpl; @@ -68,6 +66,10 @@ import mage.util.GameLog; import mage.util.RandomUtil; import org.apache.log4j.Logger; +import java.io.Serializable; +import java.util.*; +import java.util.Map.Entry; + public abstract class PlayerImpl implements Player, Serializable { private static final Logger logger = Logger.getLogger(PlayerImpl.class); @@ -611,9 +613,9 @@ public abstract class PlayerImpl implements Player, Serializable { && this.hasOpponent(sourceControllerId, game) && game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.HEXPROOF, null, sourceControllerId, game) == null && abilities.stream() - .filter(HexproofBaseAbility.class::isInstance) - .map(HexproofBaseAbility.class::cast) - .anyMatch(ability -> ability.checkObject(source, game))) { + .filter(HexproofBaseAbility.class::isInstance) + .map(HexproofBaseAbility.class::cast) + .anyMatch(ability -> ability.checkObject(source, game))) { return false; } @@ -653,7 +655,7 @@ public abstract class PlayerImpl implements Player, Serializable { game.informPlayers(getLogName() + " discards down to " + this.maxHandSize + (this.maxHandSize == 1 - ? " hand card" : " hand cards")); + ? " hand card" : " hand cards")); } discard(hand.size() - this.maxHandSize, false, null, game); } @@ -802,7 +804,7 @@ public abstract class PlayerImpl implements Player, Serializable { } GameEvent gameEvent = GameEvent.getEvent(GameEvent.EventType.DISCARD_CARD, card.getId(), source == null - ? null : source.getSourceId(), playerId); + ? null : source.getSourceId(), playerId); gameEvent.setFlag(source != null); // event from effect or from cost (source == null) if (game.replaceEvent(gameEvent, source)) { return false; @@ -1811,9 +1813,9 @@ public abstract class PlayerImpl implements Player, Serializable { } private List getPermanentsThatCanBeUntapped(Game game, - List canBeUntapped, - RestrictionUntapNotMoreThanEffect handledEffect, - Map>, Integer> notMoreThanEffectsUsage) { + List canBeUntapped, + RestrictionUntapNotMoreThanEffect handledEffect, + Map>, Integer> notMoreThanEffectsUsage) { List leftForUntap = new ArrayList<>(); // select permanents that can still be untapped for (Permanent permanent : canBeUntapped) { @@ -2522,7 +2524,7 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public boolean searchLibrary(TargetCardInLibrary target, Ability source, Game game, UUID targetPlayerId, - boolean triggerEvents) { + boolean triggerEvents) { //20091005 - 701.14c Library searchedLibrary = null; String searchInfo = null; @@ -2724,7 +2726,7 @@ public abstract class PlayerImpl implements Player, Serializable { /** * @param game * @param appliedEffects - * @param numSides Number of sides the dice has + * @param numSides Number of sides the dice has * @return the number that the player rolled */ @Override @@ -2761,16 +2763,16 @@ public abstract class PlayerImpl implements Player, Serializable { /** * @param game * @param appliedEffects - * @param numberChaosSides The number of chaos sides the planar die - * currently has (normally 1 but can be 5) + * @param numberChaosSides The number of chaos sides the planar die + * currently has (normally 1 but can be 5) * @param numberPlanarSides The number of chaos sides the planar die - * currently has (normally 1) + * currently has (normally 1) * @return the outcome that the player rolled. Either ChaosRoll, PlanarRoll * or NilRoll */ @Override public PlanarDieRoll rollPlanarDie(Game game, List appliedEffects, int numberChaosSides, - int numberPlanarSides) { + int numberPlanarSides) { int result = RandomUtil.nextInt(9) + 1; PlanarDieRoll roll = PlanarDieRoll.NIL_ROLL; if (numberChaosSides + numberPlanarSides > 9) { @@ -2927,7 +2929,7 @@ public abstract class PlayerImpl implements Player, Serializable { /** * @param ability - * @param available if null, it won't be checked if enough mana is available + * @param available if null, it won't be checked if enough mana is available * @param sourceObject * @param game * @return @@ -3076,7 +3078,6 @@ public abstract class PlayerImpl implements Player, Serializable { protected ActivatedAbility findActivatedAbilityFromPlayable(Card card, ManaOptions manaAvailable, Ability ability, Game game) { // replace alternative abilities by real play abilities (e.g. morph/facedown static ability by play land) - if (ability instanceof ActivatedManaAbilityImpl) { // mana ability if (((ActivatedManaAbilityImpl) ability).canActivate(this.getId(), game).canActivate()) { @@ -3085,8 +3086,11 @@ public abstract class PlayerImpl implements Player, Serializable { } else if (ability instanceof AlternativeSourceCosts) { // alternative cost must be replaced by real play ability return findActivatedAbilityFromAlternativeSourceCost(card, manaAvailable, ability, game); + } else if (ability instanceof AlternateManaPaymentAbility) { + // alternative mana pay like convoke (tap creature to pay) + return findActivatedAbilityFromAlternateManaPaymentAbility(card, manaAvailable, (AlternateManaPaymentAbility) ability, game); } else if (ability instanceof ActivatedAbility) { - // activated ability + // all other activated ability if (canPlay((ActivatedAbility) ability, manaAvailable, card, game)) { return (ActivatedAbility) ability; } @@ -3119,6 +3123,20 @@ public abstract class PlayerImpl implements Player, Serializable { return null; } + protected ActivatedAbility findActivatedAbilityFromAlternateManaPaymentAbility(Card card, ManaOptions manaAvailable, AlternateManaPaymentAbility ability, Game game) { + // alternative mana payment allows to pay mana for spell ability + SpellAbility spellAbility = card.getSpellAbility(); + if (spellAbility != null) { + ManaOptions manaSpecial = ability.getManaOptions(spellAbility, game, spellAbility.getManaCostsToPay()); + ManaOptions manaFull = manaAvailable.copy(); + manaFull.addMana(manaSpecial); + if (canPlay(spellAbility, manaFull, card, game)) { + return spellAbility; + } + } + return null; + } + protected boolean canLandPlayAlternateSourceCostsAbility(Card sourceObject, ManaOptions available, Ability ability, Game game) { if (sourceObject != null && !(sourceObject instanceof Permanent)) { Ability sourceAbility = sourceObject.getAbilities().stream() @@ -3221,37 +3239,45 @@ public abstract class PlayerImpl implements Player, Serializable { boolean canActivateAsHandZone = permittingObject != null || (fromZone == Zone.GRAVEYARD && canPlayCardsFromGraveyard()); + boolean possibleToPlay = false; - // as affected controller - UUID savedControllerId = ability.getControllerId(); - ability.setControllerId(this.getId()); - try { - boolean possibleToPlay = false; + // spell/hand abilities (play from all zones) + // need permitingObject or canPlayCardsFromGraveyard + if (canActivateAsHandZone + && ability.getZone().match(Zone.HAND) + && (isPlaySpell || isPlayLand)) { + possibleToPlay = true; + } - // spell/hand abilities (play from all zones) - // need permitingObject or canPlayCardsFromGraveyard - if (canActivateAsHandZone - && ability.getZone().match(Zone.HAND) - && (isPlaySpell || isPlayLand)) { - possibleToPlay = true; + // zone's abilities (play from specific zone) + // no need in permitingObject + if (fromZone != Zone.ALL && ability.getZone().match(fromZone)) { + possibleToPlay = true; + } + + if (!possibleToPlay) { + continue; + } + + // direct mode (with original controller) + ActivatedAbility playAbility = findActivatedAbilityFromPlayable(card, availableMana, ability, game); + if (playAbility != null && !output.contains(playAbility)) { + output.add(playAbility); + continue; + } + + // from non hand mode (with affected controller) + if (canActivateAsHandZone) { + UUID savedControllerId = ability.getControllerId(); + ability.setControllerId(this.getId()); + try { + playAbility = findActivatedAbilityFromPlayable(card, availableMana, ability, game); + if (playAbility != null && !output.contains(playAbility)) { + output.add(playAbility); + } + } finally { + ability.setControllerId(savedControllerId); } - - // zone's abilities (play from specific zone) - // no need in permitingObject - if (fromZone != Zone.ALL && ability.getZone().match(fromZone)) { - possibleToPlay = true; - } - - if (!possibleToPlay) { - continue; - } - - ActivatedAbility playAbility = findActivatedAbilityFromPlayable(card, availableMana, ability, game); - if (playAbility != null && !output.contains(playAbility)) { - output.add(playAbility); - } - } finally { - ability.setControllerId(savedControllerId); } } } @@ -3270,8 +3296,10 @@ public abstract class PlayerImpl implements Player, Serializable { boolean previousState = game.inCheckPlayableState(); game.setCheckPlayableState(true); try { + // basic mana ManaOptions availableMana = getManaAvailable(game); availableMana.addMana(manaPool.getMana()); + // conditional mana for (ConditionalMana conditionalMana : manaPool.getConditionalMana()) { availableMana.addMana(conditionalMana); } @@ -3364,7 +3392,7 @@ public abstract class PlayerImpl implements Player, Serializable { // activated abilities from battlefield objects if (fromAll || fromZone == Zone.BATTLEFIELD) { - for (Permanent permanent : game.getBattlefield().getAllActivePermanents(playerId)) { + for (Permanent permanent : game.getBattlefield().getAllActivePermanents()) { boolean canUseActivated = permanent.canUseActivatedAbilities(game); List battlePlayable = new ArrayList<>(); getPlayableFromCardAll(game, Zone.BATTLEFIELD, permanent, availableMana, battlePlayable); @@ -3624,7 +3652,7 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public boolean canPaySacrificeCost(Permanent permanent, UUID sourceId, - UUID controllerId, Game game + UUID controllerId, Game game ) { return sacrificeCostFilter == null || !sacrificeCostFilter.match(permanent, sourceId, controllerId, game); } @@ -3777,8 +3805,8 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public boolean moveCards(Card card, Zone toZone, - Ability source, Game game, - boolean tapped, boolean faceDown, boolean byOwner, List appliedEffects + Ability source, Game game, + boolean tapped, boolean faceDown, boolean byOwner, List appliedEffects ) { Set cardList = new HashSet<>(); if (card != null) { @@ -3789,22 +3817,22 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public boolean moveCards(Cards cards, Zone toZone, - Ability source, Game game + Ability source, Game game ) { return moveCards(cards.getCards(game), toZone, source, game); } @Override public boolean moveCards(Set cards, Zone toZone, - Ability source, Game game + Ability source, Game game ) { return moveCards(cards, toZone, source, game, false, false, false, null); } @Override public boolean moveCards(Set cards, Zone toZone, - Ability source, Game game, - boolean tapped, boolean faceDown, boolean byOwner, List appliedEffects + Ability source, Game game, + boolean tapped, boolean faceDown, boolean byOwner, List appliedEffects ) { if (cards.isEmpty()) { return true; @@ -3906,8 +3934,8 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public boolean moveCardsToExile(Card card, Ability source, - Game game, boolean withName, UUID exileId, - String exileZoneName + Game game, boolean withName, UUID exileId, + String exileZoneName ) { Set cards = new HashSet<>(); cards.add(card); @@ -3916,8 +3944,8 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public boolean moveCardsToExile(Set cards, Ability source, - Game game, boolean withName, UUID exileId, - String exileZoneName + Game game, boolean withName, UUID exileId, + String exileZoneName ) { if (cards.isEmpty()) { return true; @@ -3933,14 +3961,14 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public boolean moveCardToHandWithInfo(Card card, UUID sourceId, - Game game + Game game ) { return this.moveCardToHandWithInfo(card, sourceId, game, true); } @Override public boolean moveCardToHandWithInfo(Card card, UUID sourceId, - Game game, boolean withName + Game game, boolean withName ) { boolean result = false; Zone fromZone = game.getState().getZone(card.getId()); @@ -3965,7 +3993,7 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public Set moveCardsToGraveyardWithInfo(Set allCards, Ability source, - Game game, Zone fromZone + Game game, Zone fromZone ) { UUID sourceId = source == null ? null : source.getSourceId(); Set movedCards = new LinkedHashSet<>(); @@ -3973,7 +4001,7 @@ public abstract class PlayerImpl implements Player, Serializable { // identify cards from one owner Cards cards = new CardsImpl(); UUID ownerId = null; - for (Iterator it = allCards.iterator(); it.hasNext();) { + for (Iterator it = allCards.iterator(); it.hasNext(); ) { Card card = it.next(); if (cards.isEmpty()) { ownerId = card.getOwnerId(); @@ -4036,7 +4064,7 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public boolean moveCardToGraveyardWithInfo(Card card, UUID sourceId, - Game game, Zone fromZone + Game game, Zone fromZone ) { if (card == null) { return false; @@ -4065,8 +4093,8 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public boolean moveCardToLibraryWithInfo(Card card, UUID sourceId, - Game game, Zone fromZone, - boolean toTop, boolean withName + Game game, Zone fromZone, + boolean toTop, boolean withName ) { if (card == null) { return false; @@ -4131,7 +4159,7 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public boolean moveCardToExileWithInfo(Card card, UUID exileId, String exileName, UUID sourceId, - Game game, Zone fromZone, boolean withName) { + Game game, Zone fromZone, boolean withName) { if (card == null) { return false; } @@ -4154,7 +4182,7 @@ public abstract class PlayerImpl implements Player, Serializable { game.informPlayers(this.getLogName() + " moves " + (withName ? card.getLogName() + (card.isCopy() ? " (Copy)" : "") : "a card face down") + ' ' + (fromZone != null ? "from " + fromZone.toString().toLowerCase(Locale.ENGLISH) - + ' ' : "") + "to the exile zone"); + + ' ' : "") + "to the exile zone"); } result = true;