implement [MH3] Primal Prayers ; use choice panel for cast mode choice ; allow some restricted "as thought as it had flash" to work only on matching alternative cast. (#12420)

This commit is contained in:
Susucre 2024-07-30 15:47:39 +02:00 committed by GitHub
parent 503e842b51
commit c0eab28626
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
67 changed files with 1105 additions and 596 deletions

View file

@ -69,7 +69,10 @@ public enum MageIdentifier {
XandersPactAlternateCast,
TheTombOfAclazotzWatcher,
MeTheImmortalAlternateCast,
WithoutPayingManaCostAlternateCast;
WithoutPayingManaCostAlternateCast,
AlurenAlternateCast,
OfferingAlternateCast,
PrimalPrayersAlternateCast;
/**
* Additional text if there is need to differentiate two very similar effects

View file

@ -25,10 +25,7 @@ import mage.target.targetadjustment.TargetAdjuster;
import mage.watchers.Watcher;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.*;
/**
* Practically everything in the game is started from an Ability. This interface
@ -280,12 +277,17 @@ public interface Ability extends Controllable, Serializable {
/**
* Activates this ability prompting the controller to pay any mandatory
*
* @param game A reference the {@link Game} for which this ability should be
* activated within.
* @param noMana Whether or not {@link ManaCosts} have to be paid.
* @param game A reference the {@link Game} for which this ability should be
* activated within.
* @param allowedIdentifiers Restrict alternative/regular cost depending (if contain MageIdentifier.Default, there is no restriction)
* @param noMana Whether or not {@link ManaCosts} have to be paid.
* @return True if this ability was successfully activated.
*/
boolean activate(Game game, boolean noMana);
boolean activate(Game game, Set<MageIdentifier> allowedIdentifiers, boolean noMana);
default boolean activate(Game game, boolean noMana) {
return activate(game, new HashSet<>(Arrays.asList(MageIdentifier.Default)), noMana);
}
boolean isActivated();
@ -472,7 +474,7 @@ public interface Ability extends Controllable, Serializable {
*/
String getGameLogMessage(Game game);
boolean activateAlternateOrAdditionalCosts(MageObject sourceObject, boolean noMana, Player controller, Game game);
boolean activateAlternateOrAdditionalCosts(MageObject sourceObject, Set<MageIdentifier> allowedIdentifiers, boolean noMana, Player controller, Game game);
/**
* Finds the source object regardless of its zcc. Can be LKI from battlefield in some cases.

View file

@ -19,6 +19,9 @@ import mage.abilities.hint.Hint;
import mage.abilities.icon.CardIcon;
import mage.abilities.mana.ActivatedManaAbilityImpl;
import mage.cards.Card;
import mage.choices.Choice;
import mage.choices.ChoiceHintType;
import mage.choices.ChoiceImpl;
import mage.constants.*;
import mage.game.Game;
import mage.game.command.Dungeon;
@ -255,7 +258,7 @@ public abstract class AbilityImpl implements Ability {
}
@Override
public boolean activate(Game game, boolean noMana) {
public boolean activate(Game game, Set<MageIdentifier> allowedIdentifiers, boolean noMana) {
Player controller = game.getPlayer(this.getControllerId());
if (controller == null) {
return false;
@ -302,7 +305,9 @@ public abstract class AbilityImpl implements Ability {
// or her intentions to pay any or all of those costs (see rule 601.2e).
// A player can't apply two alternative methods of casting or two alternative costs to a single spell.
if (isMainPartAbility) {
activateAlternateOrAdditionalCosts(sourceObject, noMana, controller, game);
if (!activateAlternateOrAdditionalCosts(sourceObject, allowedIdentifiers, noMana, controller, game)) {
return false;
}
}
// 117.6. Some mana costs contain no mana symbols. This represents an unpayable cost. An ability can
@ -451,8 +456,11 @@ public abstract class AbilityImpl implements Ability {
return activated;
}
/**
* @return true if choices for the activation were made (can be to activate with the regular cost)
*/
@Override
public boolean activateAlternateOrAdditionalCosts(MageObject sourceObject, boolean noMana, Player controller, Game game) {
public boolean activateAlternateOrAdditionalCosts(MageObject sourceObject, Set<MageIdentifier> allowedIdentifiers, boolean noMana, Player controller, Game game) {
boolean canUseAlternativeCost = true;
boolean canUseAdditionalCost = true;
@ -494,48 +502,104 @@ public abstract class AbilityImpl implements Ability {
canUseAlternativeCost = false;
}
boolean alternativeCostUsed = false;
if (sourceObject != null && !(sourceObject instanceof Permanent)) {
// it's important to apply alternative cost first
// example: Omniscience gives free mana as alternative, but Entwine ability adds {2} as additional
Abilities<Ability> abilities = CardUtil.getAbilities(sourceObject, game);
// TODO: Why the check for permanent?
if (sourceObject == null || sourceObject instanceof Permanent) {
return true;
}
// 1. ALTERNATIVE COSTS
for (Ability ability : abilities) {
// if cast for noMana no Alternative costs are allowed
if (canUseAlternativeCost && !noMana && ability instanceof AlternativeSourceCosts) {
AlternativeSourceCosts alternativeSpellCosts = (AlternativeSourceCosts) ability;
if (alternativeSpellCosts.isAvailable(this, game)) {
if (alternativeSpellCosts.askToActivateAlternativeCosts(this, game)) {
// only one alternative costs may be activated
alternativeCostUsed = true;
break;
}
}
}
}
// controller specific alternate spell costs
if (canUseAlternativeCost && !noMana && !alternativeCostUsed) {
for (AlternativeSourceCosts alternativeSourceCosts : controller.getAlternativeSourceCosts()) {
if (alternativeSourceCosts.isAvailable(this, game)) {
if (alternativeSourceCosts.askToActivateAlternativeCosts(this, game)) {
// only one alternative costs may be activated
alternativeCostUsed = true;
break;
}
}
}
}
// it's important to apply alternative cost first
// example: Omniscience gives free mana as alternative, but Entwine ability adds {2} as additional
Abilities<Ability> abilities = CardUtil.getAbilities(sourceObject, game);
// 2. ADDITIONAL COST
for (Ability ability : abilities) {
if (canUseAdditionalCost && ability instanceof OptionalAdditionalSourceCosts) {
((OptionalAdditionalSourceCosts) ability).addOptionalAdditionalCosts(this, game);
// 1. ALTERNATIVE COSTS
// Collect all possible alternatives costs:
List<AlternativeSourceCosts> possibleAlternatives = new ArrayList<>();
for (Ability ability : abilities) {
// if cast for noMana no Alternative costs are allowed
if (canUseAlternativeCost && !noMana && ability instanceof AlternativeSourceCosts) {
AlternativeSourceCosts alternativeSpellCosts = (AlternativeSourceCosts) ability;
if (alternativeSpellCosts.isAvailable(this, game)
&& alternativeSpellCosts.canActivateAlternativeCostsNow(this, game)
&& (allowedIdentifiers.contains(MageIdentifier.Default) || allowedIdentifiers.contains(ability.getIdentifier()))) {
possibleAlternatives.add(alternativeSpellCosts);
}
}
}
return alternativeCostUsed;
// controller specific alternate spell costs
if (canUseAlternativeCost && !noMana) {
for (AlternativeSourceCosts alternativeSourceCosts : controller.getAlternativeSourceCosts()) {
if (alternativeSourceCosts.isAvailable(this, game)
&& alternativeSourceCosts.canActivateAlternativeCostsNow(this, game)
&& (allowedIdentifiers.contains(MageIdentifier.Default) || allowedIdentifiers.contains(alternativeSourceCosts.getIdentifier()))) {
possibleAlternatives.add(alternativeSourceCosts);
}
}
}
Player player = game.getPlayer(getControllerId());
if (player == null) {
// No controller to activate.
return false;
}
Choice choice = new ChoiceImpl(false); // not required, cancelling will cancel the cast (as you could do once in the pay mana mode).
choice.setSubMessage("for casting " + CardUtil.getSourceLogName(game, "", this, "", ""));
AlternativeSourceCosts alternativeChosen = null;
if (!possibleAlternatives.isEmpty()) {
// At least one alternative cost is available.
// We open a menu for the player to choose up to one.
boolean mustChooseAlternative = !(allowedIdentifiers.contains(MageIdentifier.Default) || allowedIdentifiers.contains(getIdentifier()));
choice.setMessage(
mustChooseAlternative
? "Choose an alternative cost"
: "You may choose an alternative cost"
);
Map<String, Integer> sort = new LinkedHashMap<>();
int i;
for (i = 0; i < possibleAlternatives.size(); i++) {
String key = Integer.toString(i + 1);
sort.put(key, i);
AlternativeSourceCosts alternative = possibleAlternatives.get(i);
MageObject object = alternative.getSourceObject(game);
choice.withItem(
key,
possibleAlternatives.get(i).getAlternativeCostText(this, game),
i,
object != null ? ChoiceHintType.GAME_OBJECT : null,
object != null ? object.getId().toString() : null
);
}
if (!mustChooseAlternative) {
// add the non-alternative cast as the last option.
String key = Integer.toString(i + 1);
sort.put(key, i);
choice.withItem(
key,
"Cast with no alternative cost: " + this.getManaCosts().getText(),
i,
ChoiceHintType.GAME_OBJECT,
sourceObject.getId().toString()
);
}
if (!player.choose(Outcome.Benefit, choice, game)) {
return false;
}
String choiceKey = choice.getChoiceKey();
if (sort.containsKey(choiceKey)) {
int choiceNumber = sort.get(choiceKey);
if (choiceNumber < possibleAlternatives.size()) {
alternativeChosen = possibleAlternatives.get(choiceNumber);
}
}
}
if (alternativeChosen != null) {
alternativeChosen.activateAlternativeCosts(this, game);
}
// 2. ADDITIONAL COST
for (Ability ability : abilities) {
if (canUseAdditionalCost && ability instanceof OptionalAdditionalSourceCosts) {
((OptionalAdditionalSourceCosts) ability).addOptionalAdditionalCosts(this, game);
}
}
return true;
}
/**

View file

@ -1,6 +1,7 @@
package mage.abilities;
import mage.ApprovingObject;
import mage.MageIdentifier;
import mage.MageObject;
import mage.abilities.condition.Condition;
import mage.abilities.costs.Cost;
@ -215,8 +216,8 @@ public abstract class ActivatedAbilityImpl extends AbilityImpl implements Activa
}
@Override
public boolean activate(Game game, boolean noMana) {
if (!hasMoreActivationsThisTurn(game) || !super.activate(game, noMana)) {
public boolean activate(Game game, Set<MageIdentifier> allowedIdentifiers, boolean noMana) {
if (!hasMoreActivationsThisTurn(game) || !super.activate(game, allowedIdentifiers, noMana)) {
return false;
}
ActivationInfo activationInfo = getActivationInfo(game);

View file

@ -16,6 +16,7 @@ import mage.players.Player;
import mage.util.CardUtil;
import java.util.*;
import java.util.stream.Collectors;
/**
* @author BetaSteward_at_googlemail.com
@ -79,10 +80,20 @@ public class SpellAbility extends ActivatedAbilityImpl {
* can be casted that are affected by the CastAsInstant effect.
* (i.e. Vizier of the Menagerie and issue #5816)
*/
public boolean spellCanBeActivatedRegularlyNow(UUID playerId, Game game) {
private static final Set<MageIdentifier> activationSetAllowAll = new HashSet();
static {
activationSetAllowAll.add(MageIdentifier.Default);
}
/**
* @return the set of cast method MageIdentifer that are allowed to be cast at this time (if MageIdentifier.Default is in it, there is no restriction)
*/
public Set<MageIdentifier> spellCanBeActivatedNow(UUID playerId, Game game) {
MageObject object = game.getObject(sourceId);
if (object == null) {
return false;
return Collections.emptySet();
}
// forced to cast (can be part id or main id)
@ -93,23 +104,48 @@ public class SpellAbility extends ActivatedAbilityImpl {
}
for (UUID idToCheck : idsToCheck) {
if (game.getState().getValue("PlayFromNotOwnHandZone" + idToCheck) != null) {
return (Boolean) game.getState().getValue("PlayFromNotOwnHandZone" + idToCheck); // card like Chandra, Torch of Defiance +1 loyal ability)
if ((Boolean) game.getState().getValue("PlayFromNotOwnHandZone" + idToCheck)) {
return activationSetAllowAll;
} else {
return Collections.emptySet();
}
}
}
return !game.getContinuousEffects().asThough(sourceId, AsThoughEffectType.CAST_AS_INSTANT, this, playerId, game).isEmpty() // check this first to allow Offering in main phase
|| timing == TimingRule.INSTANT
// OfferingAbility is doing side effects in its asThough computation.
// so we call it before the timing check.
// TODO: maybe Offering could be reworked with the MageIdentifier solution of linking
// CAST_AS_INSTANT with alternative cast methods?
Set<ApprovingObject> asInstantApprovers = game.getContinuousEffects()
.asThough(sourceId, AsThoughEffectType.CAST_AS_INSTANT, this, playerId, game);
if (timing == TimingRule.INSTANT
|| object.isInstant(game)
|| object.hasAbility(FlashAbility.getInstance(), game)
|| game.canPlaySorcery(playerId);
|| game.canPlaySorcery(playerId)) {
return activationSetAllowAll;
}
// In case there is a need for as AsThoughEffectType.CAST_AS_INSTANT, we do collect the MageIdentifer of the approving objects
// to match later on with the real method to cast. When only non-Default MageIdentifier are used, only some of the alternative
// cast are possible to activate.
Set<MageIdentifier> setOfIdentifier = new HashSet<>();
setOfIdentifier.addAll(
asInstantApprovers
.stream()
.map(ApprovingObject::getApprovingAbility)
.map(Ability::getIdentifier)
.collect(Collectors.toSet())
);
return setOfIdentifier;
}
@Override
public ActivationStatus canActivate(UUID playerId, Game game) {
// spells can be cast from non hand zones, so must use custom check
// no super.canActivate() call
if (this.spellCanBeActivatedRegularlyNow(playerId, game)) {
Set<MageIdentifier> allowedIdentifiers = this.spellCanBeActivatedNow(playerId, game);
if (!allowedIdentifiers.isEmpty()) {
if (spellAbilityType == SpellAbilityType.SPLIT
|| spellAbilityType == SpellAbilityType.SPLIT_AFTERMATH) {
return ActivationStatus.getFalse();

View file

@ -1,7 +1,6 @@
package mage.abilities.common;
import mage.MageIdentifier;
import mage.MageObject;
import mage.MageObjectReference;
import mage.abilities.Ability;
import mage.abilities.SpellAbility;
@ -96,8 +95,10 @@ class CastFromGraveyardOnceEffect extends AsThoughEffectImpl {
if (spellAbility.getManaCosts().isEmpty()) {
return false;
}
return spellAbility.spellCanBeActivatedRegularlyNow(playerId, game)
&& filter.match(cardToCheck, playerId, source, game);
Set<MageIdentifier> allowedToBeCastNow = spellAbility.spellCanBeActivatedNow(playerId, game);
if (allowedToBeCastNow.contains(MageIdentifier.Default)) {
return filter.match(cardToCheck, playerId, source, game);
}
}
return false;
}

View file

@ -1,5 +1,6 @@
package mage.abilities.common;
import mage.MageIdentifier;
import mage.abilities.Ability;
import mage.abilities.SpellAbility;
import mage.abilities.costs.mana.ManaCostsImpl;
@ -10,6 +11,7 @@ import mage.constants.*;
import mage.game.Game;
import mage.game.stack.Spell;
import java.util.Set;
import java.util.UUID;
/**
@ -48,6 +50,7 @@ public class SpellTransformedAbility extends SpellAbility {
this.setSpellAbilityCastMode(SpellAbilityCastMode.TRANSFORMED);
//when casting this way, the card must have the TransformAbility from elsewhere
}
protected SpellTransformedAbility(final SpellTransformedAbility ability) {
super(ability);
this.manaCost = ability.manaCost;
@ -59,8 +62,8 @@ public class SpellTransformedAbility extends SpellAbility {
}
@Override
public boolean activate(Game game, boolean noMana) {
if (super.activate(game, noMana)) {
public boolean activate(Game game, Set<MageIdentifier> allowedIdentifiers, boolean noMana) {
if (super.activate(game, allowedIdentifiers, noMana)) {
game.getState().setValue(TransformAbility.VALUE_KEY_ENTER_TRANSFORMED + getSourceId(), Boolean.TRUE);
// TODO: must be removed after transform cards (one side) migrated to MDF engine (multiple sides)
TransformedEffect effect = new TransformedEffect();

View file

@ -7,7 +7,6 @@ import mage.abilities.condition.Condition;
import mage.abilities.costs.mana.ManaCost;
import mage.cards.Card;
import mage.constants.AbilityType;
import mage.constants.Outcome;
import mage.constants.Zone;
import mage.filter.FilterCard;
import mage.game.Game;
@ -50,13 +49,13 @@ public class AlternativeCostSourceAbility extends StaticAbility implements Alter
}
/**
* @param cost alternate cost to pay
* @param cost alternate cost to pay
* @param condition only if the condition is true it's possible to use the
* alternate costs
* @param rule if != null used as rule text
* @param filter filters the cards this alternate cost can be applied to
* @param onlyMana if true only the mana costs are replaced by this costs,
* other costs stay untouched
* alternate costs
* @param rule if != null used as rule text
* @param filter filters the cards this alternate cost can be applied to
* @param onlyMana if true only the mana costs are replaced by this costs,
* other costs stay untouched
*/
public AlternativeCostSourceAbility(Cost cost, Condition condition, String rule, FilterCard filter, boolean onlyMana) {
super(Zone.ALL, null);
@ -115,69 +114,88 @@ public class AlternativeCostSourceAbility extends StaticAbility implements Alter
}
@Override
public boolean askToActivateAlternativeCosts(Ability ability, Game game) {
if (ability != null && AbilityType.SPELL == ability.getAbilityType()) {
if (filter != null) {
Card card = game.getCard(ability.getSourceId());
if (!filter.match(card, ability.getControllerId(), ability, game)) {
return false;
}
}
Player player = game.getPlayer(ability.getControllerId());
if (player != null) {
Costs<AlternativeCost> alternativeCostsToCheck;
if (dynamicCost != null) {
alternativeCostsToCheck = new CostsImpl<>();
alternativeCostsToCheck.add(convertToAlternativeCost(dynamicCost.getCost(ability, game)));
} else {
alternativeCostsToCheck = this.alternateCosts;
}
String costChoiceText;
if (dynamicCost != null) {
costChoiceText = dynamicCost.getText(ability, game);
} else {
costChoiceText = alternativeCostsToCheck.isEmpty() ? "Cast without paying its mana cost?" : "Pay alternative costs? (" + alternativeCostsToCheck.getText() + ')';
}
if (alternativeCostsToCheck.canPay(ability, ability, ability.getControllerId(), game)
&& player.chooseUse(Outcome.Benefit, costChoiceText, this, game)) {
if (ability instanceof SpellAbility) {
ability.getManaCostsToPay().removeIf(VariableCost.class::isInstance);
CardUtil.reduceCost((SpellAbility) ability, ability.getManaCosts());
} else {
ability.clearManaCostsToPay();
}
if (!onlyMana) {
ability.clearCosts();
}
for (AlternativeCost alternateCost : alternativeCostsToCheck) {
alternateCost.activate();
for (Iterator it = ((Costs) alternateCost).iterator(); it.hasNext();) {
Cost costDetailed = (Cost) it.next();
if (costDetailed instanceof ManaCost) {
ability.addManaCostsToPay((ManaCost) costDetailed.copy());
} else if (costDetailed != null) {
ability.addCost(costDetailed.copy());
}
}
}
// Those cost have been paid, we want to store them.
if (dynamicCost != null) {
rememberDynamicCost(game, ability, alternativeCostsToCheck);
}
// save activated status
doActivate(game, ability);
} else {
return false;
}
} else {
public boolean canActivateAlternativeCostsNow(Ability ability, Game game) {
if (ability == null || !AbilityType.SPELL.equals(ability.getAbilityType())) {
return isActivated(ability, game);
}
if (filter != null) {
Card card = game.getCard(ability.getSourceId());
if (!filter.match(card, ability.getControllerId(), ability, game)) {
return false;
}
}
return isActivated(ability, game);
Player player = game.getPlayer(ability.getControllerId());
if (player == null) {
return false;
}
Costs<AlternativeCost> alternativeCostsToCheck;
if (dynamicCost != null) {
alternativeCostsToCheck = new CostsImpl<>();
alternativeCostsToCheck.add(convertToAlternativeCost(dynamicCost.getCost(ability, game)));
} else {
alternativeCostsToCheck = this.alternateCosts;
}
return alternativeCostsToCheck.canPay(ability, ability, ability.getControllerId(), game);
}
@Override
public String getAlternativeCostText(Ability ability, Game game) {
if (dynamicCost != null) {
return "Cast with alternative cost: " + dynamicCost.getText(ability, game) + CardUtil.getSourceLogName(game, this);
} else {
Costs<AlternativeCost> alternativeCostsToCheck;
if (dynamicCost != null) {
alternativeCostsToCheck = new CostsImpl<>();
alternativeCostsToCheck.add(convertToAlternativeCost(dynamicCost.getCost(ability, game)));
} else {
alternativeCostsToCheck = this.alternateCosts;
}
return alternativeCostsToCheck.isEmpty()
? "Cast without paying its mana cost" + CardUtil.getSourceLogName(game, this)
: "Cast with alternative cost: " + alternativeCostsToCheck.getText() + CardUtil.getSourceLogName(game, this);
}
}
@Override
public boolean activateAlternativeCosts(Ability ability, Game game) {
Costs<AlternativeCost> alternativeCostsToCheck;
if (dynamicCost != null) {
alternativeCostsToCheck = new CostsImpl<>();
alternativeCostsToCheck.add(convertToAlternativeCost(dynamicCost.getCost(ability, game)));
} else {
alternativeCostsToCheck = this.alternateCosts;
}
if (ability instanceof SpellAbility) {
ability.getManaCostsToPay().removeIf(VariableCost.class::isInstance);
CardUtil.reduceCost((SpellAbility) ability, ability.getManaCosts());
} else {
ability.clearManaCostsToPay();
}
if (!onlyMana) {
ability.clearCosts();
}
for (AlternativeCost alternateCost : alternativeCostsToCheck) {
alternateCost.activate();
for (Iterator it = ((Costs) alternateCost).iterator(); it.hasNext(); ) {
Cost costDetailed = (Cost) it.next();
if (costDetailed instanceof ManaCost) {
ability.addManaCostsToPay((ManaCost) costDetailed.copy());
} else if (costDetailed != null) {
ability.addCost(costDetailed.copy());
}
}
}
// Those cost have been paid, we want to store them.
if (dynamicCost != null) {
rememberDynamicCost(game, ability, alternativeCostsToCheck);
}
// save activated status
doActivate(game, ability);
return true;
}
protected void doActivate(Game game, Ability ability) {
@ -217,8 +235,8 @@ public class AlternativeCostSourceAbility extends StaticAbility implements Alter
* @param game
* @param source
* @param alternativeCostOriginalId you must save originalId on card's
* creation
* @param searchPrevZCC true on battlefield, false on stack
* creation
* @param searchPrevZCC true on battlefield, false on stack
* @return
*/
public static boolean getActivatedStatus(Game game, Ability source, UUID alternativeCostOriginalId, boolean searchPrevZCC) {
@ -280,8 +298,8 @@ public class AlternativeCostSourceAbility extends StaticAbility implements Alter
sb.append(CardUtil.concatWithAnd(alternateCosts
.stream()
.map(cost -> cost.getCost() instanceof ManaCost
? "pay " + cost.getText(true)
: cost.getText(true))
? "pay " + cost.getText(true)
: cost.getText(true))
.map(CardUtil::getTextWithFirstCharLowerCase)
.collect(Collectors.toList())));
if (condition == null || alternateCosts.size() == 1) {

View file

@ -1,5 +1,6 @@
package mage.abilities.costs;
import mage.MageIdentifier;
import mage.abilities.Ability;
import mage.game.Game;
@ -10,16 +11,34 @@ import mage.game.Game;
*
* @author LevelX2
*/
public interface AlternativeSourceCosts {
public interface AlternativeSourceCosts extends Ability {
/**
* Ask the player if they want to use the alternative costs
* If non-Default, allow to link this cost to permission abilities
*/
MageIdentifier getIdentifier();
/**
* Check that the alternative ability can be used for the ability.
*
* @param ability ability the alternative cost is activated for
* @param game
* @return
*/
boolean askToActivateAlternativeCosts(Ability ability, Game game);
boolean canActivateAlternativeCostsNow(Ability ability, Game game);
String getAlternativeCostText(Ability ability, Game game);
/**
* activate a specific alternative cost.
* A check to canActivateAlternativeCostsNow should be made before-end
* to check if it is valid.
*
* @param ability ability the alternative cost is activated for
* @param game
* @return
*/
boolean activateAlternativeCosts(Ability ability, Game game);
/**
* Is the alternative spell cost currently available

View file

@ -1,11 +1,10 @@
package mage.abilities.costs;
import mage.abilities.Ability;
import mage.abilities.SpellAbility;
import mage.abilities.StaticAbility;
import mage.abilities.costs.mana.ManaCost;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.constants.Outcome;
import mage.constants.AbilityType;
import mage.constants.Zone;
import mage.game.Game;
import mage.players.Player;
@ -21,8 +20,9 @@ public abstract class AlternativeSourceCostsImpl extends StaticAbility implement
protected final AlternativeCost alternativeCost;
protected final String reminderText;
protected final String activationKey;
protected static String getActivationKey(String name){
return name+"ActivationKey";
protected static String getActivationKey(String name) {
return name + "ActivationKey";
}
protected AlternativeSourceCostsImpl(String name, String reminderText, String manaString) {
@ -44,24 +44,24 @@ public abstract class AlternativeSourceCostsImpl extends StaticAbility implement
this.activationKey = ability.activationKey;
}
@Override
public boolean askToActivateAlternativeCosts(Ability ability, Game game) {
if (ability instanceof SpellAbility) {
handleActivatingAlternativeCosts(ability, game);
public boolean canActivateAlternativeCostsNow(Ability ability, Game game) {
if (ability == null || !AbilityType.SPELL.equals(ability.getAbilityType())) {
return isActivated(ability, game);
}
return isActivated(ability, game);
Player player = game.getPlayer(ability.getControllerId());
return player != null && alternativeCost.canPay(ability, this, player.getId(), game);
}
protected boolean handleActivatingAlternativeCosts(Ability ability, Game game) {
Player player = game.getPlayer(ability.getControllerId());
if (player == null) {
return false;
}
@Override
public String getAlternativeCostText(Ability ability, Game game) {
return "Cast with " + this.name + " alternative cost: " + alternativeCost.getText(true) + CardUtil.getSourceLogName(game, this);
}
@Override
public boolean activateAlternativeCosts(Ability ability, Game game) {
this.resetCost();
if (!alternativeCost.canPay(ability, this, player.getId(), game)
|| !player.chooseUse(Outcome.Benefit, "Cast this for its " + this.name + " cost? (" + alternativeCost.getText(true) + ')', ability, game)) {
return false;
}
ability.setCostsTag(activationKey, null);
alternativeCost.activate();

View file

@ -16,8 +16,6 @@ import mage.filter.StaticFilters;
import mage.game.Game;
import mage.players.Player;
import java.util.UUID;
public class CastFromHandWithoutPayingManaCostEffect extends ContinuousEffectImpl {
private final AlternativeCostSourceAbility alternativeCastingCostAbility;
@ -54,18 +52,13 @@ public class CastFromHandWithoutPayingManaCostEffect extends ContinuousEffectImp
return new CastFromHandWithoutPayingManaCostEffect(this);
}
@Override
public void init(Ability source, Game game, UUID activePlayerId) {
super.init(source, game, activePlayerId);
alternativeCastingCostAbility.setSourceId(source.getSourceId());
}
@Override
public boolean apply(Layer layer, SubLayer sublayer, Ability source, Game game) {
Player controller = game.getPlayer(source.getControllerId());
if (controller == null) {
return false;
}
alternativeCastingCostAbility.setSourceId(source.getSourceId());
controller.getAlternativeSourceCosts().add(alternativeCastingCostAbility);
return true;
}

View file

@ -1,5 +1,6 @@
package mage.abilities.keyword;
import mage.MageIdentifier;
import mage.abilities.Ability;
import mage.abilities.SpellAbility;
import mage.abilities.common.DiesSourceTriggeredAbility;
@ -19,6 +20,8 @@ import mage.constants.TimingRule;
import mage.game.Game;
import mage.target.targetpointer.FixedTarget;
import java.util.Set;
/**
* @author TheElk801
*/
@ -73,8 +76,8 @@ public class BlitzAbility extends SpellAbility {
}
@Override
public boolean activate(Game game, boolean noMana) {
if (!super.activate(game, noMana)) {
public boolean activate(Game game, Set<MageIdentifier> allowedIdentifiers, boolean noMana) {
if (!super.activate(game, allowedIdentifiers, noMana)) {
return false;
}
this.setCostsTag(BLITZ_ACTIVATION_VALUE_KEY, null);

View file

@ -1,6 +1,7 @@
package mage.abilities.keyword;
import mage.ApprovingObject;
import mage.MageIdentifier;
import mage.MageObjectReference;
import mage.Mana;
import mage.abilities.SpellAbility;
@ -20,6 +21,7 @@ import mage.players.Player;
import mage.target.common.TargetSacrifice;
import mage.util.CardUtil;
import java.util.Set;
import java.util.UUID;
/**
@ -101,7 +103,7 @@ public class EmergeAbility extends SpellAbility {
}
@Override
public boolean activate(Game game, boolean noMana) {
public boolean activate(Game game, Set<MageIdentifier> allowedIdentifiers, boolean noMana) {
Player controller = game.getPlayer(this.getControllerId());
if (controller != null) {
TargetSacrifice target = new TargetSacrifice(filter);
@ -110,7 +112,7 @@ public class EmergeAbility extends SpellAbility {
Permanent creature = game.getPermanent(target.getFirstTarget());
if (creature != null) {
CardUtil.reduceCost(this, creature.getManaValue());
if (super.activate(game, false)) {
if (super.activate(game, allowedIdentifiers, false)) {
MageObjectReference mor = new MageObjectReference(creature, game);
if (creature.sacrifice(this, game)) {
this.setCostsTag(EMERGE_ACTIVATION_CREATURE_REFERENCE, mor); //Can access with LKI afterwards

View file

@ -1,5 +1,6 @@
package mage.abilities.keyword;
import mage.MageIdentifier;
import mage.abilities.SpellAbility;
import mage.abilities.costs.Cost;
import mage.abilities.costs.Costs;
@ -15,6 +16,7 @@ import mage.game.Game;
import mage.target.common.TargetCardInYourGraveyard;
import mage.util.CardUtil;
import java.util.Set;
import java.util.UUID;
/**
@ -97,8 +99,8 @@ public class EscapeAbility extends SpellAbility {
}
@Override
public boolean activate(Game game, boolean noMana) {
if (super.activate(game, noMana)) {
public boolean activate(Game game, Set<MageIdentifier> allowedIdentifiers, boolean noMana) {
if (super.activate(game, allowedIdentifiers, noMana)) {
game.getState().setValue(CASTED_WITH_ESCAPE_KEY + getSourceId().toString() + (getSourceObjectZoneChangeCounter() + 1), Boolean.TRUE);
return true;
}

View file

@ -1,5 +1,6 @@
package mage.abilities.keyword;
import mage.MageIdentifier;
import mage.MageObjectReference;
import mage.abilities.Ability;
import mage.abilities.SpecialAction;
@ -17,6 +18,7 @@ import mage.players.Player;
import mage.util.CardUtil;
import java.util.List;
import java.util.Set;
import java.util.UUID;
/**
@ -78,7 +80,8 @@ public class PlotAbility extends SpecialAction {
// Not Allowed from other zones
return ActivationStatus.getFalse();
}
if (!card.getSpellAbility().spellCanBeActivatedRegularlyNow(playerId, game)) {
Set<MageIdentifier> allowedToBeCastNow = card.getSpellAbility().spellCanBeActivatedNow(playerId, game);
if (!allowedToBeCastNow.contains(MageIdentifier.Default) && !allowedToBeCastNow.contains(card.getSpellAbility().getIdentifier())) {
return ActivationStatus.getFalse();
}
return super.canActivate(playerId, game);

View file

@ -1,6 +1,7 @@
package mage.abilities.keyword;
import mage.ApprovingObject;
import mage.MageIdentifier;
import mage.abilities.SpellAbility;
import mage.abilities.costs.mana.ManaCost;
import mage.abilities.dynamicvalue.common.OpponentsLostLifeCount;
@ -10,6 +11,7 @@ import mage.constants.SpellAbilityType;
import mage.constants.Zone;
import mage.game.Game;
import java.util.Set;
import java.util.UUID;
/**
@ -53,9 +55,9 @@ public class SpectacleAbility extends SpellAbility {
}
@Override
public boolean activate(Game game, boolean noMana) {
if (super.activate(game, noMana)) {
this.setCostsTag(SPECTACLE_ACTIVATION_VALUE_KEY,null);
public boolean activate(Game game, Set<MageIdentifier> allowedIdentifiers, boolean noMana) {
if (super.activate(game, allowedIdentifiers, noMana)) {
this.setCostsTag(SPECTACLE_ACTIVATION_VALUE_KEY, null);
return true;
}
return false;

View file

@ -1,6 +1,7 @@
package mage.abilities.keyword;
import mage.ApprovingObject;
import mage.MageIdentifier;
import mage.abilities.SpellAbility;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.cards.Card;
@ -10,6 +11,7 @@ import mage.game.Game;
import mage.players.Player;
import mage.watchers.common.CastSpellLastTurnWatcher;
import java.util.Set;
import java.util.UUID;
/**
@ -63,8 +65,8 @@ public class SurgeAbility extends SpellAbility {
}
@Override
public boolean activate(Game game, boolean noMana) {
if (super.activate(game, noMana)) {
public boolean activate(Game game, Set<MageIdentifier> allowedIdentifiers, boolean noMana) {
if (super.activate(game, allowedIdentifiers, noMana)) {
this.setCostsTag(SURGE_ACTIVATION_VALUE_KEY, null);
return true;
}

View file

@ -1,6 +1,7 @@
package mage.abilities.keyword;
import mage.ApprovingObject;
import mage.MageIdentifier;
import mage.abilities.Ability;
import mage.abilities.SpecialAction;
import mage.abilities.TriggeredAbilityImpl;
@ -27,6 +28,7 @@ import mage.util.CardUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
/**
@ -216,7 +218,8 @@ public class SuspendAbility extends SpecialAction {
if (card == null) {
return ActivationStatus.getFalse();
}
if (!card.getSpellAbility().spellCanBeActivatedRegularlyNow(playerId, game)) {
Set<MageIdentifier> allowedToBeCastNow = card.getSpellAbility().spellCanBeActivatedNow(playerId, game);
if (!allowedToBeCastNow.contains(MageIdentifier.Default) && !allowedToBeCastNow.contains(card.getSpellAbility().getIdentifier())) {
return ActivationStatus.getFalse();
}
return super.canActivate(playerId, game);

View file

@ -6,7 +6,6 @@ import mage.abilities.costs.Cost;
import mage.abilities.effects.mana.AddConditionalColorlessManaEffect;
import mage.abilities.effects.mana.BasicManaEffect;
import mage.constants.Zone;
import mage.game.Game;
public class ActivateIfConditionManaAbility extends ActivatedManaAbilityImpl {
@ -26,11 +25,6 @@ public class ActivateIfConditionManaAbility extends ActivatedManaAbilityImpl {
super(ability);
}
@Override
public boolean activate(Game game, boolean noMana) {
return super.activate(game, noMana);
}
@Override
public String getRule() {
return super.getRule() + " Activate only if " + condition.toString() + '.';

View file

@ -1,6 +1,7 @@
package mage.abilities.mana;
import mage.MageIdentifier;
import mage.Mana;
import mage.abilities.costs.Cost;
import mage.abilities.effects.mana.AddManaOfAnyColorEffect;
@ -12,6 +13,7 @@ import mage.util.CardUtil;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
/**
* @author LevelX2, Susucr
@ -50,9 +52,9 @@ public class LimitedTimesPerTurnActivatedManaAbility extends ActivatedManaAbilit
}
@Override
public boolean activate(Game game, boolean noMana) {
public boolean activate(Game game, Set<MageIdentifier> allowedIdentifiers, boolean noMana) {
if (canActivate(this.controllerId, game).canActivate()) {
return super.activate(game, noMana);
return super.activate(game, allowedIdentifiers, noMana);
}
return false;
}

View file

@ -311,7 +311,10 @@ public class ChoiceImpl implements Choice {
// no key answer found, try to match by text starting with
for (String needChoice : answers) {
for (Map.Entry<String, String> currentChoice : this.getKeyChoices().entrySet()) {
if (currentChoice.getValue().startsWith(needChoice)) {
String choiceValue = currentChoice.getValue();
// Clean any html part (for easier unit test matching)
String cleanedChoiceValue = choiceValue.replaceAll("<[^<>]*>", "");
if (choiceValue.startsWith(needChoice) || cleanedChoiceValue.startsWith(needChoice)) {
if (removeSelectAnswerFromList) {
this.setChoiceByKey(currentChoice.getKey(), false);
answers.remove(needChoice);
@ -324,7 +327,9 @@ public class ChoiceImpl implements Choice {
// string mode
for (String needChoice : answers) {
for (String currentChoice : this.getChoices()) {
if (currentChoice.equals(needChoice)) {
// Clean any html part (for easier unit test matching)
String cleanedChoiceValue = currentChoice.replaceAll("<[^<>]*>", "");
if (currentChoice.equals(needChoice) || cleanedChoiceValue.equals(needChoice)) {
if (removeSelectAnswerFromList) {
this.setChoice(needChoice, false);
answers.remove(needChoice);

View file

@ -161,10 +161,10 @@ public class Spell extends StackObjectImpl implements Card {
this.startingDefense = spell.startingDefense;
}
public boolean activate(Game game, boolean noMana) {
public boolean activate(Game game, Set<MageIdentifier> allowedIdentifiers, boolean noMana) {
setCurrentActivatingManaAbilitiesStep(ActivationManaAbilityStep.BEFORE); // mana payment step started, can use any mana abilities, see AlternateManaPaymentAbility
if (!ability.activate(game, noMana)) {
if (!ability.activate(game, allowedIdentifiers, noMana)) {
return false;
}
@ -182,7 +182,7 @@ public class Spell extends StackObjectImpl implements Card {
// see https://github.com/magefree/mage/issues/6603
payNoMana |= ability.getSpellAbilityType() == SpellAbilityType.SPLIT_FUSED;
if (!spellAbility.activate(game, payNoMana)) {
if (!spellAbility.activate(game, allowedIdentifiers, payNoMana)) {
return false;
}
}

View file

@ -388,8 +388,8 @@ public class StackAbility extends StackObjectImpl implements Ability {
}
@Override
public boolean activate(Game game, boolean noMana) {
return ability.activate(game, noMana);
public boolean activate(Game game, Set<MageIdentifier> allowedIdentifiers, boolean noMana) {
return ability.activate(game, allowedIdentifiers, noMana);
}
@Override
@ -584,7 +584,7 @@ public class StackAbility extends StackObjectImpl implements Ability {
}
@Override
public boolean activateAlternateOrAdditionalCosts(MageObject sourceObject, boolean noMana, Player controller, Game game) {
public boolean activateAlternateOrAdditionalCosts(MageObject sourceObject, Set<MageIdentifier> allowedIdentifiers, boolean noMana, Player controller, Game game) {
throw new UnsupportedOperationException("Not supported yet.");
}

View file

@ -1231,6 +1231,7 @@ public abstract class PlayerImpl implements Player, Serializable {
// Use ability copy to avoid problems with targets and costs on recast (issue https://github.com/magefree/mage/issues/5189).
SpellAbility ability = originalAbility.copy();
Set<MageIdentifier> allowedIdentifiers = originalAbility.spellCanBeActivatedNow(getId(), game);
ability.setControllerId(getId());
ability.setSourceObjectZoneChangeCounter(game.getState().getZoneChangeCounter(ability.getSourceId()));
@ -1265,7 +1266,6 @@ public abstract class PlayerImpl implements Player, Serializable {
MageIdentifier identifier = approvingObject == null
? MageIdentifier.Default
: approvingObject.getApprovingAbility().getIdentifier();
if (!getCastSourceIdWithAlternateMana().getOrDefault(ability.getSourceId(), Collections.emptySet()).contains(identifier)) {
// identifier has no alternate cast entry for that sourceId, using Default instead.
identifier = MageIdentifier.Default;
@ -1291,7 +1291,7 @@ public abstract class PlayerImpl implements Player, Serializable {
spell.getSpellAbility().getId(), spell.getSpellAbility(), playerId, approvingObject);
castEvent.setZone(fromZone);
game.fireEvent(castEvent);
if (spell.activate(game, noMana)) {
if (spell.activate(game, allowedIdentifiers, noMana)) {
GameEvent castedEvent = GameEvent.getEvent(GameEvent.EventType.SPELL_CAST,
ability.getId(), ability, playerId, approvingObject);
castedEvent.setZone(fromZone);
@ -1673,7 +1673,7 @@ public abstract class PlayerImpl implements Player, Serializable {
* @return
*/
public static Map<UUID, SpellAbility> getCastableSpellAbilities(Game game, UUID playerId, MageObject object, Zone zone, boolean noMana) {
// it uses simple check from spellCanBeActivatedRegularlyNow
// it uses simple check from spellCanBeActivatedNow
// reason: no approved info here (e.g. forced to choose spell ability from cast card)
LinkedHashMap<UUID, SpellAbility> useable = new LinkedHashMap<>();
Abilities<Ability> allAbilities;
@ -1695,7 +1695,8 @@ public abstract class PlayerImpl implements Player, Serializable {
// If the card has any mandatory additional costs, those must be paid to cast the spell.
// (2021-02-05)
if (!noMana) {
if (spellAbility.spellCanBeActivatedRegularlyNow(playerId, game)) {
Set<MageIdentifier> allowedToBeCastNow = spellAbility.spellCanBeActivatedNow(playerId, game);
if (allowedToBeCastNow.contains(MageIdentifier.Default) || allowedToBeCastNow.contains(spellAbility.getIdentifier())) {
useable.put(spellAbility.getId(), spellAbility); // example: Chandra, Torch of Defiance +1 loyal ability
}
return useable;
@ -1735,10 +1736,12 @@ public abstract class PlayerImpl implements Player, Serializable {
}
}
return useable;
default:
if (spellAbility.spellCanBeActivatedRegularlyNow(playerId, game)) {
default: {
Set<MageIdentifier> allowedToBeCastNow = spellAbility.spellCanBeActivatedNow(playerId, game);
if (allowedToBeCastNow.contains(MageIdentifier.Default) || allowedToBeCastNow.contains(spellAbility.getIdentifier())) {
useable.put(spellAbility.getId(), spellAbility);
}
}
}
}
return useable;
@ -3622,19 +3625,29 @@ public abstract class PlayerImpl implements Player, Serializable {
game.getContinuousEffects().costModification(copy, game);
}
boolean canBeCastRegularly = true;
if (copy instanceof SpellAbility && copy.getManaCosts().isEmpty() && copy.getCosts().isEmpty()) {
// 117.6. Some mana costs contain no mana symbols. This represents an unpayable cost...
// 117.6a (...) If an alternative cost is applied to an unpayable cost,
// including an effect that allows a player to cast a spell without paying its mana cost, the alternative cost may be paid.
canBeCastRegularly = false;
Set<MageIdentifier> allowedIdentifiers = null;
if (copy instanceof SpellAbility) {
if (copy.getManaCosts().isEmpty() && copy.getCosts().isEmpty()) {
// 117.6. Some mana costs contain no mana symbols. This represents an unpayable cost...
// 117.6a (...) If an alternative cost is applied to an unpayable cost,
// including an effect that allows a player to cast a spell without paying its mana cost, the alternative cost may be paid.
canBeCastRegularly = false;
}
allowedIdentifiers = ((SpellAbility) copy).spellCanBeActivatedNow(playerId, game);
if (!allowedIdentifiers.contains(MageIdentifier.Default)) {
// If the timing restriction is lifted only for specific MageIdentifier, the default cast can not be used.
canBeCastRegularly = false;
}
}
if (canBeCastRegularly && canPayMinimumManaCost(copy, availableMana, game)) {
return true;
}
// ALTERNATIVE COST FROM dynamic effects
for (MageIdentifier identifier : getCastSourceIdWithAlternateMana().getOrDefault(copy.getSourceId(), new HashSet<>())) {
if (allowedIdentifiers != null && !(allowedIdentifiers.contains(MageIdentifier.Default) || allowedIdentifiers.contains(identifier))) {
continue;
}
ManaCosts alternateCosts = getCastSourceIdManaCosts().get(copy.getSourceId()).get(identifier);
Costs<Cost> costs = getCastSourceIdCosts().get(copy.getSourceId()).get(identifier);
@ -3767,123 +3780,146 @@ public abstract class PlayerImpl implements Player, Serializable {
*/
protected boolean canPlayCardByAlternateCost(Card sourceObject, ManaOptions availableMana, Ability ability, Game game) {
// TODO: Why is the "sourceObject instanceof Permanent" in there?
if (sourceObject != null && !(sourceObject instanceof Permanent)) {
Ability copyAbility; // for alternative cost and reduce tries
for (Ability alternateSourceCostsAbility : sourceObject.getAbilities(game)) {
// if cast for noMana no Alternative costs are allowed
if (alternateSourceCostsAbility instanceof AlternativeSourceCosts) {
if (((AlternativeSourceCosts) alternateSourceCostsAbility).isAvailable(ability, game)) {
if (alternateSourceCostsAbility.getCosts().canPay(ability, ability, playerId, game)) {
ManaCostsImpl manaCosts = new ManaCostsImpl<>();
for (Cost cost : alternateSourceCostsAbility.getCosts()) {
// AlternativeCost2 replaced by real cost on activate, so getPlayable need to extract that costs here
if (cost instanceof AlternativeCost) {
if (((AlternativeCost) cost).getCost() instanceof ManaCost) {
manaCosts.add((ManaCost) ((AlternativeCost) cost).getCost());
}
} else {
if (cost instanceof ManaCost) {
manaCosts.add((ManaCost) cost);
}
}
}
if (manaCosts.isEmpty()) {
return true;
} else {
if (availableMana == null) {
return true;
}
// alternative cost reduce
copyAbility = ability.copy();
copyAbility.clearManaCostsToPay();
copyAbility.addManaCostsToPay(manaCosts.copy());
copyAbility.adjustCosts(game);
game.getContinuousEffects().costModification(copyAbility, game);
// reduced all cost
if (copyAbility.getManaCostsToPay().isEmpty()) {
return true;
}
for (Mana mana : copyAbility.getManaCostsToPay().getOptions()) {
if (availableMana.enough(mana)) {
return true;
}
}
}
}
if (sourceObject == null || (sourceObject instanceof Permanent)) {
return false;
}
Ability copyAbility; // for alternative cost and reduce tries
Set<MageIdentifier> allowedIdentifiers = null;
if (ability instanceof SpellAbility) {
// This returns the set of MageIdentifier that allow to play the card at that timing.
// If MageIdentifier.Default is in the set, everything is allowed to be cast at this time.
// If not, then only the AlternativeSourceCosts with a matching MageIdentifier are allowed at this time.
allowedIdentifiers = ((SpellAbility) ability).spellCanBeActivatedNow(getId(), game);
}
// Try the source specific AlternativeSourceCosts
for (Ability alternateSourceCostsAbility : sourceObject.getAbilities(game)) {
// if cast for noMana no Alternative costs are allowed
if (!(alternateSourceCostsAbility instanceof AlternativeSourceCosts)) {
continue;
}
if (!((AlternativeSourceCosts) alternateSourceCostsAbility).isAvailable(ability, game)) {
continue;
}
if (!alternateSourceCostsAbility.getCosts().canPay(ability, ability, playerId, game)) {
continue;
}
if (allowedIdentifiers != null && !allowedIdentifiers.contains(MageIdentifier.Default)
&& !allowedIdentifiers.contains(alternateSourceCostsAbility.getIdentifier())) {
continue;
}
ManaCostsImpl manaCosts = new ManaCostsImpl<>();
for (Cost cost : alternateSourceCostsAbility.getCosts()) {
// AlternativeCost2 replaced by real cost on activate, so getPlayable need to extract that costs here
if (cost instanceof AlternativeCost) {
if (((AlternativeCost) cost).getCost() instanceof ManaCost) {
manaCosts.add((ManaCost) ((AlternativeCost) cost).getCost());
}
} else {
if (cost instanceof ManaCost) {
manaCosts.add((ManaCost) cost);
}
}
}
// controller specific alternate spell costs
for (AlternativeSourceCosts alternateSourceCosts : getAlternativeSourceCosts()) {
if (alternateSourceCosts instanceof Ability) {
if (alternateSourceCosts.isAvailable(ability, game)) {
if (((Ability) alternateSourceCosts).getCosts().canPay(ability, ability, playerId, game)) {
ManaCostsImpl manaCosts = new ManaCostsImpl<>();
for (Cost cost : ((Ability) alternateSourceCosts).getCosts()) {
// AlternativeCost2 replaced by real cost on activate, so getPlayable need to extract that costs here
if (cost instanceof AlternativeCost) {
if (((AlternativeCost) cost).getCost() instanceof ManaCost) {
manaCosts.add((ManaCost) ((AlternativeCost) cost).getCost());
}
} else {
if (cost instanceof ManaCost) {
manaCosts.add((ManaCost) cost);
}
}
}
if (manaCosts.isEmpty()) {
return true;
} else {
if (availableMana == null) {
return true;
}
// Add a copy of the dynamic cost for this ability if there is one.
// E.g. from Kentaro, the Smiling Cat
if (alternateSourceCosts instanceof AlternativeCostSourceAbility) {
DynamicCost dynamicCost = ((AlternativeCostSourceAbility) alternateSourceCosts).getDynamicCost();
if (dynamicCost != null) {
Cost cost = dynamicCost.getCost(ability, game);
// TODO: I don't know if this first if-check is needed, I don't think any of the dynamics values are alternative costs.
if (cost instanceof AlternativeCost) {
cost = ((AlternativeCost) cost).getCost();
}
if (cost instanceof ManaCost) {
manaCosts.add((ManaCost) cost);
}
}
}
// alternative cost reduce
copyAbility = ability.copy();
copyAbility.clearManaCostsToPay();
copyAbility.addManaCostsToPay(manaCosts.copy());
copyAbility.adjustCosts(game);
game.getContinuousEffects().costModification(copyAbility, game);
if (manaCosts.isEmpty()) {
return true;
} else {
// TODO: Why is it returning true if availableMana == null, one would think it should return false
if (availableMana == null) {
return true;
}
// reduced all cost
if (copyAbility.getManaCostsToPay().isEmpty()) {
return true;
}
// alternative cost reduce
copyAbility = ability.copy();
copyAbility.clearManaCostsToPay();
// TODO: IDE warning:
// Unchecked assignment: 'mage.abilities.costs.mana.ManaCosts' to
// 'java.util.Collection<? extends mage.abilities.costs.mana.ManaCost>'.
// Reason: 'manaCosts' has raw type, so result of copy is erased
copyAbility.addManaCostsToPay(manaCosts.copy());
copyAbility.adjustCosts(game);
game.getContinuousEffects().costModification(copyAbility, game);
for (Mana mana : copyAbility.getManaCostsToPay().getOptions()) {
if (availableMana.enough(mana)) {
return true;
}
}
}
}
// reduced all cost
if (copyAbility.getManaCostsToPay().isEmpty()) {
return true;
}
// controller specific alternate spell costs
for (AlternativeSourceCosts alternateSourceCosts : getAlternativeSourceCosts()) {
if (!(alternateSourceCosts instanceof Ability)) {
continue;
}
if (!alternateSourceCosts.isAvailable(ability, game)) {
continue;
}
if (!((Ability) alternateSourceCosts).getCosts().canPay(ability, ability, playerId, game)) {
continue;
}
if (allowedIdentifiers != null && !allowedIdentifiers.contains(MageIdentifier.Default)
&& !allowedIdentifiers.contains(((Ability) alternateSourceCosts).getIdentifier())) {
continue;
}
ManaCostsImpl manaCosts = new ManaCostsImpl<>();
for (Cost cost : ((Ability) alternateSourceCosts).getCosts()) {
// AlternativeCost2 replaced by real cost on activate, so getPlayable need to extract that costs here
if (cost instanceof AlternativeCost) {
if (((AlternativeCost) cost).getCost() instanceof ManaCost) {
manaCosts.add((ManaCost) ((AlternativeCost) cost).getCost());
}
} else {
if (cost instanceof ManaCost) {
manaCosts.add((ManaCost) cost);
}
}
}
for (Mana mana : copyAbility.getManaCostsToPay().getOptions()) {
if (availableMana.enough(mana)) {
return true;
}
}
}
}
// Add a copy of the dynamic cost for this ability if there is one.
// E.g. from Kentaro, the Smiling Cat
if (alternateSourceCosts instanceof AlternativeCostSourceAbility) {
DynamicCost dynamicCost = ((AlternativeCostSourceAbility) alternateSourceCosts).getDynamicCost();
if (dynamicCost != null) {
Cost cost = dynamicCost.getCost(ability, game);
// TODO: I don't know if this first if-check is needed, I don't think any of the dynamics values are alternative costs.
if (cost instanceof AlternativeCost) {
cost = ((AlternativeCost) cost).getCost();
}
if (cost instanceof ManaCost) {
manaCosts.add((ManaCost) cost);
}
}
}
if (manaCosts.isEmpty()) {
return true;
} else {
// TODO: Why is it returning true if availableMana == null, one would think it should return false
if (availableMana == null) {
return true;
}
// alternative cost reduce
copyAbility = ability.copy();
copyAbility.clearManaCostsToPay();
// TODO: IDE warning:
// Unchecked assignment: 'mage.abilities.costs.mana.ManaCosts' to
// 'java.util.Collection<? extends mage.abilities.costs.mana.ManaCost>'.
// Reason: 'manaCosts' has raw type, so result of copy is erased
copyAbility.addManaCostsToPay(manaCosts.copy());
copyAbility.adjustCosts(game);
game.getContinuousEffects().costModification(copyAbility, game);
// reduced all cost
if (copyAbility.getManaCostsToPay().isEmpty()) {
return true;
}
for (Mana mana : copyAbility.getManaCostsToPay().getOptions()) {
if (availableMana.enough(mana)) {
return true;
}
}
}