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);
This commit is contained in:
Oleg Agafonov 2020-06-19 13:09:45 +04:00
parent bdaf6454de
commit c2e7b02e13
9 changed files with 341 additions and 177 deletions

View file

@ -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<UUID, ActivatedManaAbilityImpl> 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<UUID, SpecialAction> specialActions = game.getState().getSpecialActions().getControlledBy(playerId, false);
Map<UUID, SpecialAction> 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<UUID, SpecialAction> 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);
}
}
}

View file

@ -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 cant 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 opponents 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);
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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.
*
* <p>
* 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();
}

View file

@ -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;
}

View file

@ -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);
}
/**

View file

@ -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

View file

@ -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<Permanent> getPermanentsThatCanBeUntapped(Game game,
List<Permanent> canBeUntapped,
RestrictionUntapNotMoreThanEffect handledEffect,
Map<Entry<RestrictionUntapNotMoreThanEffect, Set<Ability>>, Integer> notMoreThanEffectsUsage) {
List<Permanent> canBeUntapped,
RestrictionUntapNotMoreThanEffect handledEffect,
Map<Entry<RestrictionUntapNotMoreThanEffect, Set<Ability>>, Integer> notMoreThanEffectsUsage) {
List<Permanent> 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<UUID> 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<ActivatedAbility> 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<UUID> appliedEffects
Ability source, Game game,
boolean tapped, boolean faceDown, boolean byOwner, List<UUID> appliedEffects
) {
Set<Card> 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<Card> 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<Card> cards, Zone toZone,
Ability source, Game game,
boolean tapped, boolean faceDown, boolean byOwner, List<UUID> appliedEffects
Ability source, Game game,
boolean tapped, boolean faceDown, boolean byOwner, List<UUID> 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<Card> cards = new HashSet<>();
cards.add(card);
@ -3916,8 +3944,8 @@ public abstract class PlayerImpl implements Player, Serializable {
@Override
public boolean moveCardsToExile(Set<Card> 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<Card> moveCardsToGraveyardWithInfo(Set<Card> allCards, Ability source,
Game game, Zone fromZone
Game game, Zone fromZone
) {
UUID sourceId = source == null ? null : source.getSourceId();
Set<Card> 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<Card> it = allCards.iterator(); it.hasNext();) {
for (Iterator<Card> 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;