* Added handling of triggered mana to available mana calculation (fixes #585).

This commit is contained in:
LevelX2 2020-07-11 00:53:47 +02:00
parent 5be6e9398a
commit 89249888b5
26 changed files with 544 additions and 96 deletions

View file

@ -20,7 +20,6 @@ import mage.game.events.GameEvent;
import mage.game.events.GameEvent.EventType;
import mage.game.events.ManaEvent;
import mage.game.permanent.Permanent;
import mage.players.Player;
/**
*

View file

@ -53,7 +53,7 @@ class DesolationEffect extends OneShotEffect {
public DesolationEffect() {
super(Outcome.Damage);
this.staticText = "each player who tapped a land for mana this turn sacrifices a land. Desolation deals 2 damage to each player who sacrificed a Plains this way";
this.staticText = "each player who tapped a land for mana this turn sacrifices a land. {this} deals 2 damage to each player who sacrificed a Plains this way";
}
public DesolationEffect(DesolationEffect copy) {
@ -108,7 +108,8 @@ class DesolationWatcher extends Watcher {
if (event.getType() == GameEvent.EventType.UNTAP_STEP_PRE) {
reset();
}
if (event.getType() == GameEvent.EventType.TAPPED_FOR_MANA) {
if (event.getType() == GameEvent.EventType.TAPPED_FOR_MANA
&& !game.inCheckPlayableState()) { // Ignored - see GameEvent.TAPPED_FOR_MANA
UUID playerId = event.getPlayerId();
if (playerId != null) {
Permanent permanent = game.getPermanentOrLKIBattlefield(event.getSourceId());

View file

@ -86,7 +86,7 @@ class ForbiddenOrchardTriggeredAbility extends TriggeredAbilityImpl {
@Override
public boolean checkTrigger(GameEvent event, Game game) {
return event.getSourceId().equals(getSourceId());
return event.getSourceId().equals(getSourceId()) && !game.inCheckPlayableState();
}
@Override

View file

@ -1,5 +1,7 @@
package mage.cards.g;
import java.util.ArrayList;
import java.util.List;
import mage.Mana;
import mage.ObjectColor;
import mage.abilities.Ability;
@ -43,10 +45,10 @@ public final class GauntletOfPower extends CardImpl {
// As Gauntlet of Power enters the battlefield, choose a color.
this.addAbility(new EntersBattlefieldAbility(new ChooseColorEffect(Outcome.Neutral)));
// Creatures of the chosen color get +1/+1.
this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new GauntletOfPowerEffect1()));
this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new GauntletOfPowerBoostEffect()));
// Whenever a basic land is tapped for mana of the chosen color, its controller adds one mana of that color.
this.addAbility(new TapForManaAllTriggeredAbility(new GauntletOfPowerEffectEffect2(), filter, SetTargetPointer.PERMANENT));
this.addAbility(new GauntletOfPowerTapForManaAllTriggeredAbility(new GauntletOfPowerManaEffect2(), filter, SetTargetPointer.PERMANENT));
}
public GauntletOfPower(final GauntletOfPower card) {
@ -59,22 +61,22 @@ public final class GauntletOfPower extends CardImpl {
}
}
class GauntletOfPowerEffect1 extends ContinuousEffectImpl {
class GauntletOfPowerBoostEffect extends ContinuousEffectImpl {
private static final FilterCreaturePermanent filter = new FilterCreaturePermanent();
public GauntletOfPowerEffect1() {
public GauntletOfPowerBoostEffect() {
super(Duration.WhileOnBattlefield, Layer.PTChangingEffects_7, SubLayer.ModifyPT_7c, Outcome.BoostCreature);
staticText = "Creatures of the chosen color get +1/+1";
}
public GauntletOfPowerEffect1(final GauntletOfPowerEffect1 effect) {
public GauntletOfPowerBoostEffect(final GauntletOfPowerBoostEffect effect) {
super(effect);
}
@Override
public GauntletOfPowerEffect1 copy() {
return new GauntletOfPowerEffect1(this);
public GauntletOfPowerBoostEffect copy() {
return new GauntletOfPowerBoostEffect(this);
}
@Override
@ -93,18 +95,18 @@ class GauntletOfPowerEffect1 extends ContinuousEffectImpl {
}
class TapForManaAllTriggeredAbility extends TriggeredManaAbility {
class GauntletOfPowerTapForManaAllTriggeredAbility extends TriggeredManaAbility {
private final FilterPermanent filter;
private final SetTargetPointer setTargetPointer;
public TapForManaAllTriggeredAbility(ManaEffect effect, FilterPermanent filter, SetTargetPointer setTargetPointer) {
public GauntletOfPowerTapForManaAllTriggeredAbility(ManaEffect effect, FilterPermanent filter, SetTargetPointer setTargetPointer) {
super(Zone.BATTLEFIELD, effect, false);
this.filter = filter;
this.setTargetPointer = setTargetPointer;
}
public TapForManaAllTriggeredAbility(TapForManaAllTriggeredAbility ability) {
public GauntletOfPowerTapForManaAllTriggeredAbility(GauntletOfPowerTapForManaAllTriggeredAbility ability) {
super(ability);
this.filter = ability.filter.copy();
this.setTargetPointer = ability.setTargetPointer;
@ -156,8 +158,8 @@ class TapForManaAllTriggeredAbility extends TriggeredManaAbility {
}
@Override
public TapForManaAllTriggeredAbility copy() {
return new TapForManaAllTriggeredAbility(this);
public GauntletOfPowerTapForManaAllTriggeredAbility copy() {
return new GauntletOfPowerTapForManaAllTriggeredAbility(this);
}
@Override
@ -167,14 +169,14 @@ class TapForManaAllTriggeredAbility extends TriggeredManaAbility {
}
}
class GauntletOfPowerEffectEffect2 extends ManaEffect {
class GauntletOfPowerManaEffect2 extends ManaEffect {
public GauntletOfPowerEffectEffect2() {
public GauntletOfPowerManaEffect2() {
super();
staticText = "its controller adds one additional mana of that color";
}
public GauntletOfPowerEffectEffect2(final GauntletOfPowerEffectEffect2 effect) {
public GauntletOfPowerManaEffect2(final GauntletOfPowerManaEffect2 effect) {
super(effect);
}
@ -187,6 +189,18 @@ class GauntletOfPowerEffectEffect2 extends ManaEffect {
return null;
}
@Override
public List<Mana> getNetMana(Game game, Ability source) {
List<Mana> netMana = new ArrayList<>();
if (game != null) {
Mana mana = (Mana) getValue("mana");
if (mana != null) {
netMana.add(mana.copy());
}
}
return netMana;
}
@Override
public Mana produceMana(Game game, Ability source) {
if (game != null) {
@ -202,7 +216,7 @@ class GauntletOfPowerEffectEffect2 extends ManaEffect {
}
@Override
public GauntletOfPowerEffectEffect2 copy() {
return new GauntletOfPowerEffectEffect2(this);
public GauntletOfPowerManaEffect2 copy() {
return new GauntletOfPowerManaEffect2(this);
}
}

View file

@ -1,5 +1,6 @@
package mage.cards.s;
import java.util.ArrayList;
import mage.MageInt;
import mage.Mana;
import mage.abilities.Ability;
@ -11,8 +12,6 @@ import mage.abilities.effects.common.FlipSourceEffect;
import mage.abilities.effects.common.ManaEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.choices.Choice;
import mage.choices.ChoiceColor;
import mage.constants.*;
import mage.filter.FilterPermanent;
import mage.filter.common.FilterControlledLandPermanent;
@ -26,9 +25,10 @@ import mage.game.permanent.Permanent;
import mage.game.permanent.token.TokenImpl;
import mage.players.Player;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import mage.choices.Choice;
import mage.choices.ChoiceColor;
/**
* @author LevelX2
@ -133,9 +133,55 @@ class SasayasEssenceManaEffect extends ManaEffect {
@Override
public List<Mana> getNetMana(Game game, Ability source) {
return new ArrayList<>();
List<Mana> netMana = new ArrayList<>();
Player controller = game.getPlayer(source.getControllerId());
Mana producedMana = (Mana) this.getValue("mana");
Permanent permanent = game.getPermanent(getTargetPointer().getFirst(game, source));
if (controller != null && producedMana != null && permanent != null) {
FilterPermanent filter = new FilterLandPermanent();
filter.add(Predicates.not(new PermanentIdPredicate(permanent.getId())));
filter.add(new NamePredicate(permanent.getName()));
int count = game.getBattlefield().countAll(filter, controller.getId(), game);
if (count > 0) {
if (producedMana.getBlack() > 0) {
netMana.add(Mana.BlackMana(count));
}
if (producedMana.getRed() > 0) {
netMana.add(Mana.RedMana(count));
}
if (producedMana.getBlue() > 0) {
netMana.add(Mana.BlueMana(count));
}
if (producedMana.getGreen() > 0) {
netMana.add(Mana.GreenMana(count));
}
if (producedMana.getWhite() > 0) {
netMana.add(Mana.WhiteMana(count));
}
if (producedMana.getColorless() > 0) {
netMana.add(Mana.ColorlessMana(count));
}
}
}
return netMana;
}
/**
* RULINGS 6/1/2005 If Sasayas Essences controller has four Forests and
* taps one of them for Green, the Essence will add GreenGreenGreen to that
* players mana pool for a total of GreenGreenGreenGreen.
*
* 6/1/2005 If Sasayas Essences controller has four Mossfire Valley and
* taps one of them for RedGreen, the Essence will add three mana (one for
* each other Mossfire Valley) of any combination of Red and/or Green to
* that players mana pool.
*
* 6/1/2005 If Sasayas Essences controller has two Brushlands and taps one
* of them for White, Sasayas Essence adds another White to that players
* mana pool. It wont produce Green or Colorless unless the land was tapped
* for Green or Colorless instead.
*/
@Override
public Mana produceMana(Game game, Ability source) {
Mana newMana = new Mana();

View file

@ -74,6 +74,9 @@ class SavageFirecatTriggeredAbility extends TriggeredAbilityImpl {
@Override
public boolean checkTrigger(GameEvent event, Game game) {
if (game.inCheckPlayableState()) { // Ignored - see GameEvent.TAPPED_FOR_MANA
return false;
}
return game.getCard(event.getSourceId()).isLand() &&
event.getPlayerId().equals(this.controllerId);
}

View file

@ -18,7 +18,6 @@ import mage.constants.Zone;
import mage.filter.common.FilterControlledLandPermanent;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.events.GameEvent.EventType;
import mage.game.permanent.Permanent;
import mage.target.targetpointer.FixedTarget;
@ -72,12 +71,15 @@ class VorinclexTriggeredAbility2 extends TriggeredAbilityImpl {
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == EventType.TAPPED_FOR_MANA;
return event.getType() == GameEvent.EventType.TAPPED_FOR_MANA;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
if (game.getOpponents(controllerId).contains(event.getPlayerId())) {
if (game.inCheckPlayableState()) { // Ignored - see GameEvent.TAPPED_FOR_MANA
return false;
}
if (game.getOpponents(getControllerId()).contains(event.getPlayerId())) {
Permanent permanent = game.getPermanent(event.getSourceId());
if (permanent != null && permanent.isLand()) {
getEffects().get(0).setTargetPointer(new FixedTarget(permanent.getId()));

View file

@ -22,7 +22,8 @@ import mage.filter.common.FilterLandPermanent;
public final class WintersNight extends CardImpl {
private static final FilterLandPermanent filter = new FilterLandPermanent("a player taps a snow land");
{
static {
filter.add(SuperType.SNOW.getPredicate());
}

View file

@ -29,12 +29,15 @@ public class SasayaOrochiAscendantTest extends CardTestPlayerBase {
addCard(Zone.HAND, playerA, "Upwelling", 1); // Enchantment {3}{G}
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Reveal your hand: If you have seven or more land cards in your hand, flip");
activateManaAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}: Add {G}");
activateManaAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}: Add {G}");
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Upwelling");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertPermanentCount(playerA, "Sasaya's Essence", 1);
assertPermanentCount(playerA, "Upwelling", 1);
@ -45,7 +48,89 @@ public class SasayaOrochiAscendantTest extends CardTestPlayerBase {
assertDuplicatedManaOptions(manaOptions);
Assert.assertEquals("mana variations don't fit", 1, manaOptions.size());
assertManaOptions("{G}", manaOptions);
assertManaOptions("{G}{G}{G}", manaOptions);
}
@Test
public void testSasayasEssence2() {
addCard(Zone.HAND, playerA, "Plains", 7);
addCard(Zone.BATTLEFIELD, playerA, "Brushland", 3);
// Reveal your hand: If you have seven or more land cards in your hand, flip Sasaya, Orochi Ascendant.
// Sasaya's Essence: Legendary Enchantment
// Whenever a land you control is tapped for mana, for each other land you control with the same name, add one mana of any type that land produced.
addCard(Zone.BATTLEFIELD, playerA, "Sasaya, Orochi Ascendant", 1);
// Mana pools don't empty as steps and phases end.
addCard(Zone.HAND, playerA, "Upwelling", 1); // Enchantment {3}{G}
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Reveal your hand: If you have seven or more land cards in your hand, flip");
activateManaAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}: Add {G}");
activateManaAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}: Add {G}");
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Upwelling");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertPermanentCount(playerA, "Sasaya's Essence", 1);
assertPermanentCount(playerA, "Upwelling", 1);
assertManaPool(playerA, ManaType.GREEN, 2);
assertLife(playerA, 18);
ManaOptions manaOptions = playerA.getAvailableManaTest(currentGame);
assertDuplicatedManaOptions(manaOptions);
Assert.assertEquals("mana variations don't fit", 3, manaOptions.size());
assertManaOptions("{C}{C}{C}", manaOptions);
assertManaOptions("{G}{G}{G}", manaOptions);
assertManaOptions("{W}{W}{W}", manaOptions);
}
@Test
public void testSasayasEssence3() {
addCard(Zone.HAND, playerA, "Plains", 7);
addCard(Zone.BATTLEFIELD, playerA, "Forest", 3);
// {1}, {T}: Add {R}{G}.
addCard(Zone.BATTLEFIELD, playerA, "Mossfire Valley", 2);
// Reveal your hand: If you have seven or more land cards in your hand, flip Sasaya, Orochi Ascendant.
// Sasaya's Essence: Legendary Enchantment
// Whenever a land you control is tapped for mana, for each other land you control with the same name, add one mana of any type that land produced.
addCard(Zone.BATTLEFIELD, playerA, "Sasaya, Orochi Ascendant", 1);
// Mana pools don't empty as steps and phases end.
addCard(Zone.HAND, playerA, "Upwelling", 1); // Enchantment {3}{G}
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Reveal your hand: If you have seven or more land cards in your hand, flip");
activateManaAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}: Add {G}");
activateManaAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}: Add {G}");
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Upwelling");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertPermanentCount(playerA, "Sasaya's Essence", 1);
assertPermanentCount(playerA, "Upwelling", 1);
assertManaPool(playerA, ManaType.GREEN, 2);
assertLife(playerA, 20);
ManaOptions manaOptions = playerA.getAvailableManaTest(currentGame);
assertDuplicatedManaOptions(manaOptions);
Assert.assertEquals("mana variations don't fit", 4, manaOptions.size());
assertManaOptions("{R}{R}{R}{R}{G}{G}{G}", manaOptions);
assertManaOptions("{R}{R}{R}{G}{G}{G}{G}", manaOptions);
assertManaOptions("{R}{R}{G}{G}{G}{G}{G}", manaOptions);
assertManaOptions("{R}{G}{G}{G}{G}{G}{G}", manaOptions);
}
}

View file

@ -53,7 +53,6 @@ public class ConditionalManaTest extends CardTestPlayerBase {
}
@Test
@Ignore
public void testWorkingWithReflectingPool() {
addCard(Zone.BATTLEFIELD, playerA, "Cavern of Souls", 1); // can give {C] or {any} mana ({any} with restrictions)
addCard(Zone.BATTLEFIELD, playerA, "Reflecting Pool", 1); // must give {C} or {any} mana from the Cavern, but without restrictions
@ -321,7 +320,6 @@ public class ConditionalManaTest extends CardTestPlayerBase {
// and process all available net mana by special call like TriggeredManaAbility->getNetManaForEvent(ManaEvent xxx)
@Test
@Ignore
public void TriggeredManaAbilityMustGivesExtraManaOptions() {
// TriggeredManaAbility must give extra mana options (2 red instead 1)
// Whenever you tap a land for mana, add one mana of any type that land produced.
@ -340,7 +338,6 @@ public class ConditionalManaTest extends CardTestPlayerBase {
}
@Test
@Ignore
public void DictateOfKarametra_AutoPay() {
// Whenever you tap a land for mana, add one mana of any type that land produced.
addCard(Zone.BATTLEFIELD, playerA, "Dictate of Karametra");

View file

@ -5,7 +5,6 @@ import mage.constants.ManaType;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
@ -133,7 +132,6 @@ public class ReflectingPoolTest extends CardTestPlayerBase {
* producing mana
*/
@Test
@Ignore
public void testWithDifferentLands() {
addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion", 1);
@ -215,7 +213,6 @@ public class ReflectingPoolTest extends CardTestPlayerBase {
}
@Test
@Ignore
public void testReflectingPoolAnyManaNeedWithoutCondition() {
// any mana source without conditions (use any mana at any time)
addCard(Zone.BATTLEFIELD, playerA, "Plains", 2);
@ -233,7 +230,6 @@ public class ReflectingPoolTest extends CardTestPlayerBase {
}
@Test
@Ignore
public void testReflectingPoolAnyManaNeedWithCondition() {
// any mana source have condition to use (Reflecting Pool must ignore that condition)
addCard(Zone.BATTLEFIELD, playerA, "Cavern of Souls", 1); // {C} or {any}

View file

@ -0,0 +1,97 @@
package org.mage.test.cards.mana;
import mage.abilities.mana.ManaOptions;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Assert;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
import static org.mage.test.utils.ManaOptionsTestUtils.assertManaOptions;
/**
*
* @author LevelX2
*/
public class TappedForManaRelatedTest extends CardTestPlayerBase {
/**
* This is a new rule that slightly changes how we resolve abilities that
* trigger whenever a permanent is tapped for mana or for mana of a
* specified type. Now, you look at what was actually produced after the
* activated mana ability resolves. So, tapping a Gaea's Cradle while you no
* control no creatures won't cause a Wild Growth attached to it to trigger.
*/
@Test
public void TestCradleWithWildGrowthNoCreatures() {
// {T}: Add {G} for each creature you control.
addCard(Zone.BATTLEFIELD, playerA, "Gaea's Cradle", 1);
addCard(Zone.BATTLEFIELD, playerA, "Forest", 1);
// Enchant land
// Whenever enchanted land is tapped for mana, its controller adds {G}.
addCard(Zone.HAND, playerA, "Wild Growth", 1); // Enchantment {G}
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Wild Growth", "Gaea's Cradle");
setStopAt(3, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerA, "Wild Growth", 1);
ManaOptions manaOptions = playerA.getAvailableManaTest(currentGame);
Assert.assertEquals("mana variations don't fit", 1, manaOptions.size());
assertManaOptions("{G}{G}", manaOptions);
}
// Mana producedc by triggered mana abilities is not calculated in manaOptions calculations yet.
@Test
// @Ignore
public void TestCradleWithWildGrowthTwoCreatures() {
// {T}: Add {G} for each creature you control.
addCard(Zone.BATTLEFIELD, playerA, "Gaea's Cradle", 1);
addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion", 2);
addCard(Zone.BATTLEFIELD, playerA, "Forest", 1);
// Enchant land
// Whenever enchanted land is tapped for mana, its controller adds {G}.
addCard(Zone.HAND, playerA, "Wild Growth", 1); // Enchantment {G}
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Wild Growth", "Gaea's Cradle");
setStopAt(3, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerA, "Wild Growth", 1);
ManaOptions manaOptions = playerA.getAvailableManaTest(currentGame);
Assert.assertEquals("mana variations don't fit", 1, manaOptions.size());
assertManaOptions("{G}{G}{G}{G}", manaOptions);
}
// Mana producedc by triggered mana abilities is not calculated in manaOptions calculations yet.
@Test
// @Ignore
public void TestWildGrowth() {
addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion", 2);
addCard(Zone.BATTLEFIELD, playerA, "Forest", 1);
// Enchant land
// Whenever enchanted land is tapped for mana, its controller adds {G}.
addCard(Zone.HAND, playerA, "Wild Growth", 1); // Enchantment {G}
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Wild Growth", "Forest");
setStopAt(3, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerA, "Wild Growth", 1);
ManaOptions manaOptions = playerA.getAvailableManaTest(currentGame);
Assert.assertEquals("mana variations don't fit", 1, manaOptions.size());
assertManaOptions("{G}{G}", manaOptions);
}
}

View file

@ -63,6 +63,7 @@ import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import mage.Mana;
import static org.mage.test.serverside.base.impl.CardTestPlayerAPIImpl.*;
@ -3275,6 +3276,16 @@ public class TestPlayer implements Player {
return computerPlayer.getManaAvailable(game);
}
@Override
public void addAvailableTriggeredMana(List<Mana> availableTriggeredMana) {
computerPlayer.addAvailableTriggeredMana(availableTriggeredMana);
}
@Override
public List<List<Mana>> getAvailableTriggeredMana() {
return computerPlayer.getAvailableTriggeredMana();
}
@Override
public List<ActivatedAbility> getPlayable(Game game, boolean hidden) {
return computerPlayer.getPlayable(game, hidden);

View file

@ -41,6 +41,7 @@ import mage.target.common.TargetCardInLibrary;
import java.io.Serializable;
import java.util.*;
import mage.Mana;
/**
* @author Quercitron
@ -1037,6 +1038,21 @@ public class PlayerStub implements Player {
return null;
}
@Override
public void addAvailableTriggeredMana(List<Mana> availableTriggeredMan) {
}
@Override
public List<List<Mana>> getAvailableTriggeredMana() {
return null;
}
@Override
public int announceXMana(int min, int max, String message, Game game, Ability ability) {
return 0;
}
@Override
public List<ActivatedAbility> getPlayable(Game game, boolean hidden) {
return null;

View file

@ -209,11 +209,32 @@ public class ManaOptionsTest extends CardTestPlayerBase {
assertManaOptions("{C}{G}{Any}", manaOptions);
}
// Nykthos, Shrine to Nyx
@Test
public void testNykthos4a() {
addCard(Zone.BATTLEFIELD, playerA, "Sedge Scorpion", 4);
addCard(Zone.BATTLEFIELD, playerA, "Forest", 3);
// {T}: Add {C}.
// {2}, {T}: Choose a color. Add an amount of mana of that color equal to your devotion to that color. (Your devotion to a color is the number of mana symbols of that color in the mana costs of permanents you control.)
addCard(Zone.BATTLEFIELD, playerA, "Nykthos, Shrine to Nyx", 1);
setStopAt(1, PhaseStep.UPKEEP);
execute();
ManaOptions manaOptions = playerA.getAvailableManaTest(currentGame);
assertDuplicatedManaOptions(manaOptions);
Assert.assertEquals("mana variations don't fit", 2, manaOptions.size());
assertManaOptions("{C}{G}{G}{G}", manaOptions);
assertManaOptions("{G}{G}{G}{G}{G}", manaOptions);
}
// Nykthos, Shrine to Nyx
// {T}: Add {C}.
// {2}, {T}: Choose a color. Add an amount of mana of that color equal to your devotion to that color. (Your devotion to a color is the number of mana symbols of that color in the mana costs of permanents you control.)
@Test
public void testNykthos4() {
public void testNykthos4b() {
// If a land is tapped for two or more mana, it produces {C} instead of any other type and amount.
// Each spell a player casts costs {1} more to cast for each other spell that player has cast this turn.
addCard(Zone.BATTLEFIELD, playerA, "Damping Sphere", 1);
@ -399,7 +420,6 @@ public class ManaOptionsTest extends CardTestPlayerBase {
}
@Test
@Ignore // TriggeredManaAbilities not supported yet for getAvailableMana
public void testCryptGhast() {
//Extort (Whenever you cast a spell, you may pay {WB}. If you do, each opponent loses 1 life and you gain that much life.)
// Whenever you tap a Swamp for mana, add {B} (in addition to the mana the land produces).

View file

@ -34,6 +34,7 @@ import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;
import mage.abilities.effects.common.ManaEffect;
/**
* @author BetaSteward_at_googlemail.com
@ -171,6 +172,9 @@ public abstract class AbilityImpl implements Ability {
private boolean resolveMode(Game game) {
boolean result = true;
for (Effect effect : getEffects()) {
if (game.inCheckPlayableState() && !(effect instanceof ManaEffect)) {
continue; // Ignored non mana effects - see GameEvent.TAPPED_FOR_MANA
}
if (effect instanceof OneShotEffect) {
boolean effectResult = effect.apply(game, this);
result &= effectResult;

View file

@ -41,6 +41,9 @@ public class TapForManaAllTriggeredAbility extends TriggeredAbilityImpl {
@Override
public boolean checkTrigger(GameEvent event, Game game) {
if (game.inCheckPlayableState()) { // Ignored - see GameEvent.TAPPED_FOR_MANA
return false;
}
Permanent permanent = game.getPermanentOrLKIBattlefield(event.getSourceId());
if (permanent != null && filter.match(permanent, getSourceId(), getControllerId(), game)) {
ManaEvent mEvent = (ManaEvent) event;

View file

@ -34,6 +34,9 @@ public class TapLandForManaAllTriggeredAbility extends TriggeredAbilityImpl {
@Override
public boolean checkTrigger(GameEvent event, Game game) {
if (game.inCheckPlayableState()) { // Ignored - see GameEvent.TAPPED_FOR_MANA
return false;
}
Permanent permanent = game.getPermanentOrLKIBattlefield(event.getSourceId());
if (permanent != null && permanent.isLand()) {
if (setTargetPointer) {

View file

@ -26,7 +26,7 @@ public class ReturnToHandChosenControlledPermanentCost extends CostImpl {
target.setNotTarget(true);
this.addTarget(target);
if (target.getMaxNumberOfTargets() > 1 && target.getMaxNumberOfTargets() == target.getNumberOfTargets()) {
this.text = "return " + CardUtil.numberToText(target.getMaxNumberOfTargets()) + ' '
this.text = "Return " + CardUtil.numberToText(target.getMaxNumberOfTargets()) + ' '
+ target.getTargetName()
+ (target.getTargetName().endsWith(" you control") ? "" : " you control")
+ " to their owner's hand";

View file

@ -14,6 +14,7 @@ import mage.players.Player;
import java.util.ArrayList;
import java.util.List;
import mage.abilities.TriggeredAbility;
/**
* @author BetaSteward_at_googlemail.com
@ -38,6 +39,15 @@ public abstract class ManaEffect extends OneShotEffect {
if (player == null) {
return false;
}
if (game.inCheckPlayableState()) {
// During calculation of the available mana for a player the "TappedForMana" event is fired to simulate triggered mana production.
// By checking the inCheckPlayableState these events are handled to give back only the available mana of instead really producing mana
// So it's important if ManaEffects overwrite the apply method to take care for this.
if (source instanceof TriggeredAbility) {
player.addAvailableTriggeredMana(getNetMana(game, source));
}
return true; // No need to add mana to pool during checkPlayable
}
Mana manaToAdd = produceMana(game, source);
if (manaToAdd != null && manaToAdd.count() > 0) {
checkToFirePossibleEvents(manaToAdd, game, source);
@ -72,11 +82,13 @@ public abstract class ManaEffect extends OneShotEffect {
}
/**
* Produced the mana the effect can produce (DO NOT add it to mana pool -- return all added as mana object to process by replace events)
* Produced the mana the effect can produce (DO NOT add it to mana pool --
* return all added as mana object to process by replace events)
* <p>
* WARNING, produceMana can be called multiple times for mana and spell available calculations
* if you don't want it then overide getNetMana to return max possible mana values
* (if you have choose dialogs or extra effects like new counters in produceMana)
* WARNING, produceMana can be called multiple times for mana and spell
* available calculations if you don't want it then overide getNetMana to
* return max possible mana values (if you have choose dialogs or extra
* effects like new counters in produceMana)
*
* @param game warning, can be NULL for AI score calcs (game == null)
* @param source

View file

@ -38,9 +38,26 @@ public class AddManaOfAnyTypeProducedEffect extends ManaEffect {
@Override
public List<Mana> getNetMana(Game game, Ability source) {
List<Mana> netMana = new ArrayList<>();
Mana types = (Mana) this.getValue("mana"); // TODO: will not work until TriggeredManaAbility fix (see TriggeredManaAbilityMustGivesExtraManaOptions test)
Mana types = (Mana) this.getValue("mana");
if (types != null) {
netMana.add(types.copy());
if (types.getBlack() > 0) {
netMana.add(Mana.BlackMana(1));
}
if (types.getRed() > 0) {
netMana.add(Mana.RedMana(1));
}
if (types.getBlue() > 0) {
netMana.add(Mana.BlueMana(1));
}
if (types.getGreen() > 0) {
netMana.add(Mana.GreenMana(1));
}
if (types.getWhite() > 0) {
netMana.add(Mana.WhiteMana(1));
}
if (types.getColorless() > 0) {
netMana.add(Mana.ColorlessMana(1));
}
}
return netMana;
}

View file

@ -5,7 +5,6 @@ import mage.Mana;
import mage.abilities.Ability;
import mage.abilities.effects.common.ManaEffect;
import mage.game.Game;
import mage.players.Player;
public class BasicManaEffect extends ManaEffect {

View file

@ -11,6 +11,7 @@ import mage.abilities.costs.common.TapSourceCost;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.events.ManaEvent;
import mage.players.Player;
import org.apache.log4j.Logger;
/**
@ -44,24 +45,11 @@ public class ManaOptions extends ArrayList<Mana> {
//if there is only one mana option available add it to all the existing options
List<Mana> netManas = abilities.get(0).getNetMana(game);
if (netManas.size() == 1) {
if (!hasTapCost(abilities.get(0)) || checkTappedForManaReplacement(abilities.get(0), game, netManas.get(0))) {
addMana(netManas.get(0));
}
checkTappedForManaReplacement(abilities.get(0), game, netManas.get(0));
addMana(netManas.get(0));
addTriggeredMana(game, abilities.get(0));
} else if (netManas.size() > 1) {
List<Mana> copy = copy();
this.clear();
// boolean hasTapCost = hasTapCost(abilities.get(0)); // needed if checkTappedForManaReplacement is reactivated
for (Mana netMana : netManas) {
for (Mana mana : copy) {
// checkTappedForManaReplacement seems in some situations to produce endless iterations so deactivated for now: https://github.com/magefree/mage/issues/5023
if (true/* !hasTapCost || checkTappedForManaReplacement(abilities.get(0), game, netMana) */) {
Mana newMana = new Mana();
newMana.add(mana);
newMana.add(netMana);
this.add(newMana);
}
}
}
addManaVariation(netManas, abilities.get(0), game);
}
} else { // mana source has more than 1 ability
@ -69,14 +57,14 @@ public class ManaOptions extends ArrayList<Mana> {
List<Mana> copy = copy();
this.clear();
for (ActivatedManaAbilityImpl ability : abilities) {
boolean hasTapCost = hasTapCost(ability);
for (Mana netMana : ability.getNetMana(game)) {
if (!hasTapCost || checkTappedForManaReplacement(ability, game, netMana)) {
checkTappedForManaReplacement(ability, game, netMana);
for (Mana triggeredManaVariation : getTriggeredManaVariations(game, ability, netMana)) {
SkipAddMana:
for (Mana mana : copy) {
Mana newMana = new Mana();
newMana.add(mana);
newMana.add(netMana);
newMana.add(triggeredManaVariation);
for (Mana existingMana : this) {
if (existingMana.equalManaValue(newMana)) {
continue SkipAddMana;
@ -91,18 +79,48 @@ public class ManaOptions extends ArrayList<Mana> {
this.add(newMana);
}
}
}
}
}
}
}
private boolean checkTappedForManaReplacement(Ability ability, Game game, Mana mana) {
ManaEvent event = new ManaEvent(GameEvent.EventType.TAPPED_FOR_MANA, ability.getSourceId(), ability.getSourceId(), ability.getControllerId(), mana);
if (!game.replaceEvent(event)) {
return true;
private void addManaVariation(List<Mana> netManas, ActivatedManaAbilityImpl ability, Game game) {
List<Mana> copy = copy();
this.clear();
for (Mana netMana : netManas) {
for (Mana mana : copy) {
if (!hasTapCost(ability) || checkTappedForManaReplacement(ability, game, netMana)) {
Mana newMana = new Mana();
newMana.add(mana);
newMana.add(netMana);
this.add(newMana);
}
}
}
return false;
}
private List<List<Mana>> getSimulatedTriggeredManaFromPlayer(Game game, Ability ability) {
Player player = game.getPlayer(ability.getControllerId());
List<List<Mana>> newList = new ArrayList<>();
if (player != null) {
newList.addAll(player.getAvailableTriggeredMana());
player.getAvailableTriggeredMana().clear();
}
return newList;
}
private boolean checkTappedForManaReplacement(Ability ability, Game game, Mana mana) {
if (hasTapCost(ability)) {
ManaEvent event = new ManaEvent(GameEvent.EventType.TAPPED_FOR_MANA, ability.getSourceId(), ability.getSourceId(), ability.getControllerId(), mana);
if (!game.replaceEvent(event)) {
game.fireEvent(event);
return true;
}
return false;
}
return true;
}
private boolean hasTapCost(Ability ability) {
@ -127,31 +145,41 @@ public class ManaOptions extends ArrayList<Mana> {
// no mana costs
if (ability.getManaCosts().isEmpty()) {
if (netManas.size() == 1) {
checkTappedForManaReplacement(ability, game, netManas.get(0));
addMana(netManas.get(0));
addTriggeredMana(game, ability);
} else {
List<Mana> copy = copy();
this.clear();
for (Mana netMana : netManas) {
for (Mana mana : copy) {
Mana newMana = new Mana();
newMana.add(mana);
newMana.add(netMana);
this.add(newMana);
checkTappedForManaReplacement(ability, game, netMana);
for (Mana triggeredManaVariation : getTriggeredManaVariations(game, ability, netMana)) {
for (Mana mana : copy) {
Mana newMana = new Mana();
newMana.add(mana);
newMana.add(triggeredManaVariation);
this.add(newMana);
}
}
}
}
} else // the ability has mana costs
if (netManas.size() == 1) {
checkTappedForManaReplacement(ability, game, netManas.get(0));
subtractCostAddMana(ability.getManaCosts().getMana(), netManas.get(0), ability.getCosts().isEmpty());
addTriggeredMana(game, ability);
} else {
List<Mana> copy = copy();
this.clear();
for (Mana netMana : netManas) {
for (Mana mana : copy) {
Mana newMana = new Mana();
newMana.add(mana);
newMana.add(netMana);
subtractCostAddMana(ability.getManaCosts().getMana(), netMana, ability.getCosts().isEmpty());
checkTappedForManaReplacement(ability, game, netMana);
for (Mana triggeredManaVariation : getTriggeredManaVariations(game, ability, netMana)) {
for (Mana mana : copy) {
Mana newMana = new Mana();
newMana.add(mana);
newMana.add(triggeredManaVariation);
subtractCostAddMana(ability.getManaCosts().getMana(), netMana, ability.getCosts().isEmpty());
}
}
}
}
@ -160,30 +188,30 @@ public class ManaOptions extends ArrayList<Mana> {
List<Mana> copy = copy();
this.clear();
for (ActivatedManaAbilityImpl ability : abilities) {
boolean hasTapCost = hasTapCost(ability);
List<Mana> netManas = ability.getNetMana(game);
if (ability.getManaCosts().isEmpty()) {
for (Mana netMana : netManas) {
if (!hasTapCost || checkTappedForManaReplacement(ability, game, netMana)) {
checkTappedForManaReplacement(ability, game, netMana);
for (Mana triggeredManaVariation : getTriggeredManaVariations(game, ability, netMana)) {
for (Mana mana : copy) {
Mana newMana = new Mana();
newMana.add(mana);
newMana.add(netMana);
newMana.add(triggeredManaVariation);
this.add(newMana);
}
}
}
} else {
for (Mana netMana : netManas) {
if (!hasTapCost || checkTappedForManaReplacement(ability, game, netMana)) {
checkTappedForManaReplacement(ability, game, netMana);
for (Mana triggeredManaVariation : getTriggeredManaVariations(game, ability, netMana)) {
for (Mana previousMana : copy) {
CombineWithExisting:
for (Mana manaOption : ability.getManaCosts().getManaOptions()) {
Mana newMana = new Mana(previousMana);
if (previousMana.includesMana(manaOption)) { // costs can be paid
newMana.subtractCost(manaOption);
newMana.add(netMana);
newMana.add(triggeredManaVariation);
// if the new mana is in all colors more than another already existing than replace
for (Mana existingMana : this) {
Mana moreValuable = Mana.getMoreValuableMana(newMana, existingMana);
@ -211,6 +239,52 @@ public class ManaOptions extends ArrayList<Mana> {
}
}
private List<Mana> getTriggeredManaVariations(Game game, Ability ability, Mana baseMana) {
List<Mana> baseManaPlusTriggeredMana = new ArrayList<>();
baseManaPlusTriggeredMana.add(baseMana);
List<List<Mana>> availableTriggeredManaList = getSimulatedTriggeredManaFromPlayer(game, ability);
for (List<Mana> availableTriggeredMana : availableTriggeredManaList) {
if (availableTriggeredMana.size() == 1) {
for (Mana prevMana : baseManaPlusTriggeredMana) {
prevMana.add(availableTriggeredMana.get(0));
}
} else if (availableTriggeredMana.size() > 1) {
List<Mana> copy = new ArrayList<>(baseManaPlusTriggeredMana);
baseManaPlusTriggeredMana.clear();
for (Mana triggeredMana : availableTriggeredMana) {
for (Mana prevMana : copy) {
Mana newMana = new Mana();
newMana.add(prevMana);
newMana.add(triggeredMana);
baseManaPlusTriggeredMana.add(newMana);
}
}
}
}
return baseManaPlusTriggeredMana;
}
private void addTriggeredMana(Game game, Ability ability) {
List<List<Mana>> netManaList = getSimulatedTriggeredManaFromPlayer(game, ability);
for (List<Mana> triggeredNetMana : netManaList) {
if (triggeredNetMana.size() == 1) {
addMana(triggeredNetMana.get(0));
} else if (triggeredNetMana.size() > 1) {
// Add variations
List<Mana> copy = copy();
this.clear();
for (Mana triggeredMana : triggeredNetMana) {
for (Mana mana : copy) {
Mana newMana = new Mana();
newMana.add(mana);
newMana.add(triggeredMana);
this.add(newMana);
}
}
}
}
}
public void addMana(Mana addMana) {
if (isEmpty()) {
this.add(new Mana());

View file

@ -133,7 +133,7 @@ public class GameEvent implements Serializable {
targetId id of the spell that's cast
playerId player that casts the spell or ability
amount X multiplier to change X value, default 1
*/
*/
CAST_SPELL,
/* SPELL_CAST
x-Costs are already defined
@ -153,13 +153,13 @@ public class GameEvent implements Serializable {
targetId id of the ability to activate / use
sourceId sourceId of the object with that ability
playerId player that tries to use this ability
*/
*/
TAKE_SPECIAL_ACTION, TAKEN_SPECIAL_ACTION, // not used in implementation yet
/* TAKE_SPECIAL_ACTION, TAKEN_SPECIAL_ACTION,
targetId id of the ability to activate / use
sourceId sourceId of the object with that ability
playerId player that tries to use this ability
*/
*/
TRIGGERED_ABILITY,
RESOLVING_ABILITY,
COPY_STACKOBJECT, COPIED_STACKOBJECT,
@ -254,7 +254,13 @@ public class GameEvent implements Serializable {
ENTERS_THE_BATTLEFIELD_CONTROL, // 616.1b
ENTERS_THE_BATTLEFIELD_COPY, // 616.1c
ENTERS_THE_BATTLEFIELD, // 616.1d
TAP, TAPPED, TAPPED_FOR_MANA,
TAP, TAPPED,
TAPPED_FOR_MANA,
/* TAPPED_FOR_MANA
During calculation of the available mana for a player the "TappedForMana" event is fired to simulate triggered mana production.
By checking the inCheckPlayableState these events are handled to give back only the available mana of instead really producing mana.
IMPORTANT: Triggered non mana abilities have to ignore the event if game.inCheckPlayableState is true.
*/
UNTAP, UNTAPPED,
FLIP, FLIPPED,
UNFLIP, UNFLIPPED,
@ -412,12 +418,12 @@ public class GameEvent implements Serializable {
}
private GameEvent(EventType type, UUID customEventType,
UUID targetId, UUID sourceId, UUID playerId, int amount, boolean flag) {
UUID targetId, UUID sourceId, UUID playerId, int amount, boolean flag) {
this(type, customEventType, targetId, sourceId, playerId, amount, flag, null);
}
private GameEvent(EventType type, UUID customEventType,
UUID targetId, UUID sourceId, UUID playerId, int amount, boolean flag, MageObjectReference reference) {
UUID targetId, UUID sourceId, UUID playerId, int amount, boolean flag, MageObjectReference reference) {
this.type = type;
this.customEventType = customEventType;
this.targetId = targetId;

View file

@ -40,6 +40,7 @@ import mage.util.Copyable;
import java.io.Serializable;
import java.util.*;
import mage.Mana;
/**
* @author BetaSteward_at_googlemail.com
@ -636,6 +637,10 @@ public interface Player extends MageItem, Copyable<Player> {
ManaOptions getManaAvailable(Game game);
void addAvailableTriggeredMana(List<Mana> netManaAvailable);
List<List<Mana>> getAvailableTriggeredMana();
List<ActivatedAbility> getPlayable(Game game, boolean hidden);
List<Ability> getPlayableOptions(Ability ability, Game game);

View file

@ -177,6 +177,9 @@ public abstract class PlayerImpl implements Player, Serializable {
protected FilterMana phyrexianColors;
// Used during available mana calculation to give back possible available net mana from triggered mana abilities (No need to copy)
protected final List<List<Mana>> availableTriggeredManaList = new ArrayList<>();
/**
* During some steps we can't play anything
*/
@ -2848,8 +2851,18 @@ public abstract class PlayerImpl implements Player, Serializable {
return game.getBattlefield().getAllActivePermanents(blockFilter, playerId, game);
}
/**
* Returns the mana options the player currently has. That means which combinations of
* mana are available to cast spells or activate abilities etc.
*
* @param game
* @return
*/
@Override
public ManaOptions getManaAvailable(Game game) {
boolean oldState = game.inCheckPlayableState();
game.setCheckPlayableState(true);
ManaOptions availableMana = new ManaOptions();
List<Abilities<ActivatedManaAbilityImpl>> sourceWithoutManaCosts = new ArrayList<>();
@ -2892,9 +2905,33 @@ public abstract class PlayerImpl implements Player, Serializable {
// remove duplicated variants (see ManaOptionsTest for info - when that rises)
availableMana.removeDuplicated();
game.setCheckPlayableState(oldState);
return availableMana;
}
/**
* Used during calculation of available mana to gather the amount of producable triggered mana caused by using mana sources.
* So the set value is only used during the calculation of the mana produced by one source and cleared thereafter
*
* @param netManaAvailable the net mana produced by the triggered mana abaility
*/
@Override
public void addAvailableTriggeredMana(List<Mana> netManaAvailable) {
this.availableTriggeredManaList.add(netManaAvailable);
}
/**
* Used during calculation of available mana to get the amount of producable triggered mana caused by using mana sources.
* The list is cleared as soon the value is retrieved during available mana calculation.
*
* @return
*/
@Override
public List<List<Mana>> getAvailableTriggeredMana() {
return availableTriggeredManaList;
}
// returns only mana producers that don't require mana payment
protected List<MageObject> getAvailableManaProducers(Game game) {
List<MageObject> result = new ArrayList<>();