* 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

@ -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
@ -635,6 +636,10 @@ public interface Player extends MageItem, Copyable<Player> {
void untap(Game game);
ManaOptions getManaAvailable(Game game);
void addAvailableTriggeredMana(List<Mana> netManaAvailable);
List<List<Mana>> getAvailableTriggeredMana();
List<ActivatedAbility> getPlayable(Game game, boolean hidden);

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<>();
@ -2891,10 +2904,34 @@ 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<>();