* Special mana payments like convoke/delve - fixed that it can't be used to cast card from graveyard (example: Hogaak, Arisen Necropolis, see #6680);

This commit is contained in:
Oleg Agafonov 2020-06-22 08:34:53 +04:00
parent 6754636f86
commit cd624b2158
4 changed files with 129 additions and 80 deletions

View file

@ -295,4 +295,52 @@ public class ConvokeTest extends CardTestPlayerBaseWithAIHelps {
execute(); execute();
assertAllCommandsUsed(); assertAllCommandsUsed();
} }
@Test
public void test_Other_CastFromGraveayrd_Convoke() {
// https://github.com/magefree/mage/issues/6680
// {5}{B/G}{B/G}
// You can't spend mana to cast this spell.
// Convoke (Your creatures can help cast this spell. Each creature you tap while casting this spell pays for {1} or one mana of that creature's color.)
// Delve (Each card you exile from your graveyard while casting this spell pays for {1}.)
// You may cast Hogaak, Arisen Necropolis from your graveyard.
addCard(Zone.GRAVEYARD, playerA, "Hogaak, Arisen Necropolis", 1);
addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 7);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Hogaak, Arisen Necropolis");
addTarget(playerA, "Balduvian Bears", 7); // convoke pay
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertAllCommandsUsed();
assertPermanentCount(playerA, "Hogaak, Arisen Necropolis", 1);
}
@Test
public void test_Other_CastFromGraveayrd_ConvokeAndDelve() {
// https://github.com/magefree/mage/issues/6680
// {5}{B/G}{B/G}
// You can't spend mana to cast this spell.
// Convoke (Your creatures can help cast this spell. Each creature you tap while casting this spell pays for {1} or one mana of that creature's color.)
// Delve (Each card you exile from your graveyard while casting this spell pays for {1}.)
// You may cast Hogaak, Arisen Necropolis from your graveyard.
addCard(Zone.GRAVEYARD, playerA, "Hogaak, Arisen Necropolis", 1);
addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 2); // convoke (you can't pay normal mana here)
addCard(Zone.GRAVEYARD, playerA, "Balduvian Bears", 5); // delve
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Hogaak, Arisen Necropolis");
addTarget(playerA, "Balduvian Bears", 2); // convoke pay
setChoice(playerA, "Balduvian Bears", 5); // delve pay
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertAllCommandsUsed();
assertPermanentCount(playerA, "Hogaak, Arisen Necropolis", 1);
}
} }

View file

@ -1711,7 +1711,7 @@ public class TestPlayer implements Player {
printAbilities(game, computerPlayer.getPlayable(game, true)); printAbilities(game, computerPlayer.getPlayable(game, true));
printEnd(); printEnd();
} }
Assert.fail("Missing " + choiceType + " def for" Assert.fail("Missing " + choiceType.toUpperCase(Locale.ENGLISH) + " def for"
+ " turn " + game.getTurnNum() + " turn " + game.getTurnNum()
+ ", step " + (game.getStep() != null ? game.getStep().getType().name() : "not started") + ", step " + (game.getStep() != null ? game.getStep().getType().name() : "not started")
+ ", " + this.getName() + ", " + this.getName()

View file

@ -1735,8 +1735,14 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
*/ */
// TODO: mode options doesn't work here (see BrutalExpulsionTest) // TODO: mode options doesn't work here (see BrutalExpulsionTest)
public void addTarget(TestPlayer player, String target) { public void addTarget(TestPlayer player, String target) {
addTarget(player, target, 1);
}
public void addTarget(TestPlayer player, String target, int timesToChoose) {
for (int i = 0; i < timesToChoose; i++) {
player.addTarget(target); player.addTarget(target);
} }
}
/** /**
* Sets a player as target * Sets a player as target
@ -1745,8 +1751,14 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
* @param targetPlayer * @param targetPlayer
*/ */
public void addTarget(TestPlayer player, TestPlayer targetPlayer) { public void addTarget(TestPlayer player, TestPlayer targetPlayer) {
addTarget(player, targetPlayer, 1);
}
public void addTarget(TestPlayer player, TestPlayer targetPlayer, int timesToChoose) {
for (int i = 0; i < timesToChoose; i++) {
player.addTarget("targetPlayer=" + targetPlayer.getName()); player.addTarget("targetPlayer=" + targetPlayer.getName());
} }
}
/** /**
* @param player * @param player

View file

@ -69,6 +69,7 @@ import org.apache.log4j.Logger;
import java.io.Serializable; import java.io.Serializable;
import java.util.*; import java.util.*;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.stream.Collectors;
public abstract class PlayerImpl implements Player, Serializable { public abstract class PlayerImpl implements Player, Serializable {
@ -3105,6 +3106,19 @@ public abstract class PlayerImpl implements Player, Serializable {
} }
protected ActivatedAbility findActivatedAbilityFromPlayable(Card card, ManaOptions manaAvailable, Ability ability, Game game) { protected ActivatedAbility findActivatedAbilityFromPlayable(Card card, ManaOptions manaAvailable, Ability ability, Game game) {
// special mana to pay spell cost
ManaOptions manaFull = manaAvailable.copy();
if (ability instanceof SpellAbility) {
for (AlternateManaPaymentAbility altAbility : card.getAbilities(game).stream()
.filter(a -> a instanceof AlternateManaPaymentAbility)
.map(a -> (AlternateManaPaymentAbility) a)
.collect(Collectors.toList())) {
ManaOptions manaSpecial = altAbility.getManaOptions(ability, game, ability.getManaCostsToPay());
manaFull.addMana(manaSpecial);
}
}
// replace alternative abilities by real play abilities (e.g. morph/facedown static ability by play land) // replace alternative abilities by real play abilities (e.g. morph/facedown static ability by play land)
if (ability instanceof ActivatedManaAbilityImpl) { if (ability instanceof ActivatedManaAbilityImpl) {
// mana ability // mana ability
@ -3113,13 +3127,10 @@ public abstract class PlayerImpl implements Player, Serializable {
} }
} else if (ability instanceof AlternativeSourceCosts) { } else if (ability instanceof AlternativeSourceCosts) {
// alternative cost must be replaced by real play ability // alternative cost must be replaced by real play ability
return findActivatedAbilityFromAlternativeSourceCost(card, manaAvailable, ability, game); return findActivatedAbilityFromAlternativeSourceCost(card, manaFull, 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) { } else if (ability instanceof ActivatedAbility) {
// all other activated ability // all other activated ability
if (canPlay((ActivatedAbility) ability, manaAvailable, card, game)) { if (canPlay((ActivatedAbility) ability, manaFull, card, game)) {
return (ActivatedAbility) ability; return (ActivatedAbility) ability;
} }
} }
@ -3151,20 +3162,6 @@ public abstract class PlayerImpl implements Player, Serializable {
return null; 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) { protected boolean canLandPlayAlternateSourceCostsAbility(Card sourceObject, ManaOptions available, Ability ability, Game game) {
if (sourceObject != null && !(sourceObject instanceof Permanent)) { if (sourceObject != null && !(sourceObject instanceof Permanent)) {
Ability sourceAbility = sourceObject.getAbilities().stream() Ability sourceAbility = sourceObject.getAbilities().stream()
@ -3197,15 +3194,6 @@ public abstract class PlayerImpl implements Player, Serializable {
return false; return false;
} }
private Abilities<ActivatedAbility> getActivatedOnly(Abilities<Ability> list) {
Abilities<ActivatedAbility> res = new AbilitiesImpl<>();
list.stream()
.filter(a -> a instanceof ActivatedAbility)
.map(a -> (ActivatedAbility) a)
.forEach(res::add);
return res;
}
private void getPlayableFromCardAll(Game game, Zone fromZone, Card card, ManaOptions availableMana, List<ActivatedAbility> output) { private void getPlayableFromCardAll(Game game, Zone fromZone, Card card, ManaOptions availableMana, List<ActivatedAbility> output) {
if (fromZone == null || card == null) { if (fromZone == null || card == null) {
return; return;
@ -3214,24 +3202,25 @@ public abstract class PlayerImpl implements Player, Serializable {
// BASIC abilities // BASIC abilities
if (card instanceof SplitCard) { if (card instanceof SplitCard) {
SplitCard splitCard = (SplitCard) card; SplitCard splitCard = (SplitCard) card;
getPlayableFromCardSingle(game, fromZone, splitCard.getLeftHalfCard(), getActivatedOnly(splitCard.getLeftHalfCard().getAbilities(game)), availableMana, output); getPlayableFromCardSingle(game, fromZone, splitCard.getLeftHalfCard(), splitCard.getLeftHalfCard().getAbilities(game), availableMana, output);
getPlayableFromCardSingle(game, fromZone, splitCard.getRightHalfCard(), getActivatedOnly(splitCard.getRightHalfCard().getAbilities(game)), availableMana, output); getPlayableFromCardSingle(game, fromZone, splitCard.getRightHalfCard(), splitCard.getRightHalfCard().getAbilities(game), availableMana, output);
getPlayableFromCardSingle(game, fromZone, splitCard, getActivatedOnly(splitCard.getSharedAbilities(game)), availableMana, output); getPlayableFromCardSingle(game, fromZone, splitCard, splitCard.getSharedAbilities(game), availableMana, output);
} else if (card instanceof AdventureCard) { } else if (card instanceof AdventureCard) {
// adventure must use different card characteristics for different spells (main or adventure) // adventure must use different card characteristics for different spells (main or adventure)
AdventureCard adventureCard = (AdventureCard) card; AdventureCard adventureCard = (AdventureCard) card;
getPlayableFromCardSingle(game, fromZone, adventureCard.getSpellCard(), getActivatedOnly(adventureCard.getSpellCard().getAbilities(game)), availableMana, output); getPlayableFromCardSingle(game, fromZone, adventureCard.getSpellCard(), adventureCard.getSpellCard().getAbilities(game), availableMana, output);
getPlayableFromCardSingle(game, fromZone, adventureCard, getActivatedOnly(adventureCard.getSharedAbilities(game)), availableMana, output); getPlayableFromCardSingle(game, fromZone, adventureCard, adventureCard.getSharedAbilities(game), availableMana, output);
} else { } else {
getPlayableFromCardSingle(game, fromZone, card, getActivatedOnly(card.getAbilities(game)), availableMana, output); getPlayableFromCardSingle(game, fromZone, card, card.getAbilities(game), availableMana, output);
} }
// DYNAMIC ADDED abilities are adds in getAbilities(game) // DYNAMIC ADDED abilities are adds in getAbilities(game)
} }
private void getPlayableFromCardSingle(Game game, Zone fromZone, Card card, Abilities<ActivatedAbility> candidateAbilities, ManaOptions availableMana, List<ActivatedAbility> output) { private void getPlayableFromCardSingle(Game game, Zone fromZone, Card card, Abilities<Ability> candidateAbilities, ManaOptions availableMana, List<ActivatedAbility> output) {
// check "can play" condition as affected controller (BUT play from not own hand zone must be checked as original controller) // check "can play" condition as affected controller (BUT play from not own hand zone must be checked as original controller)
for (ActivatedAbility ability : candidateAbilities.getActivatedAbilities(Zone.ALL)) { // must check all abilities, not activated only
for (Ability ability : candidateAbilities) {
boolean isPlaySpell = (ability instanceof SpellAbility); boolean isPlaySpell = (ability instanceof SpellAbility);
boolean isPlayLand = (ability instanceof PlayLandAbility); boolean isPlayLand = (ability instanceof PlayLandAbility);
@ -3295,7 +3284,7 @@ public abstract class PlayerImpl implements Player, Serializable {
} }
// from non hand mode (with affected controller) // from non hand mode (with affected controller)
if (canActivateAsHandZone) { if (canActivateAsHandZone && ability.getControllerId() != this.getId()) {
UUID savedControllerId = ability.getControllerId(); UUID savedControllerId = ability.getControllerId();
ability.setControllerId(this.getId()); ability.setControllerId(this.getId());
try { try {