mirror of
https://github.com/magefree/mage.git
synced 2026-01-25 12:49:39 -08:00
Ready for review - Implement Waterbend mechanic (#14105)
* implement waterbending mechanic * properly implement WaterbendXCost * add tests * fix verify failure
This commit is contained in:
parent
bb20afb240
commit
c60a097f49
17 changed files with 423 additions and 171 deletions
|
|
@ -7,10 +7,9 @@ import mage.abilities.common.EntersBattlefieldAbility;
|
|||
import mage.abilities.condition.Condition;
|
||||
import mage.abilities.costs.*;
|
||||
import mage.abilities.costs.common.PayLifeCost;
|
||||
import mage.abilities.costs.mana.ManaCost;
|
||||
import mage.abilities.costs.mana.ManaCosts;
|
||||
import mage.abilities.costs.mana.ManaCostsImpl;
|
||||
import mage.abilities.costs.mana.VariableManaCost;
|
||||
import mage.abilities.costs.common.WaterbendCost;
|
||||
import mage.abilities.costs.common.WaterbendXCost;
|
||||
import mage.abilities.costs.mana.*;
|
||||
import mage.abilities.effects.ContinuousEffect;
|
||||
import mage.abilities.effects.Effect;
|
||||
import mage.abilities.effects.Effects;
|
||||
|
|
@ -25,6 +24,7 @@ import mage.choices.ChoiceHintType;
|
|||
import mage.choices.ChoiceImpl;
|
||||
import mage.constants.*;
|
||||
import mage.filter.FilterMana;
|
||||
import mage.filter.StaticFilters;
|
||||
import mage.game.Game;
|
||||
import mage.game.command.Dungeon;
|
||||
import mage.game.command.Emblem;
|
||||
|
|
@ -39,8 +39,10 @@ import mage.game.stack.StackAbility;
|
|||
import mage.players.Player;
|
||||
import mage.target.Target;
|
||||
import mage.target.TargetCard;
|
||||
import mage.target.TargetPermanent;
|
||||
import mage.target.Targets;
|
||||
import mage.target.common.TargetCardInLibrary;
|
||||
import mage.target.common.TargetControlledPermanent;
|
||||
import mage.target.targetadjustment.GenericTargetAdjuster;
|
||||
import mage.target.targetadjustment.TargetAdjuster;
|
||||
import mage.util.CardUtil;
|
||||
|
|
@ -50,6 +52,7 @@ import mage.watchers.Watcher;
|
|||
import org.apache.log4j.Logger;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author BetaSteward_at_googlemail.com
|
||||
|
|
@ -349,6 +352,7 @@ public abstract class AbilityImpl implements Ability {
|
|||
// Phyrexian mana symbols, the player announces whether they intend to pay 2
|
||||
// life or the corresponding colored mana cost for each of those symbols.
|
||||
AbilityImpl.handlePhyrexianCosts(game, this, this, this.getManaCostsToPay());
|
||||
AbilityImpl.handleWaterbendingCosts(game, this, this, this.getManaCostsToPay());
|
||||
|
||||
// 20241022 - 601.2b
|
||||
// Not yet included in 601.2b but this is where it will be
|
||||
|
|
@ -687,6 +691,38 @@ public abstract class AbilityImpl implements Ability {
|
|||
}
|
||||
}
|
||||
|
||||
public static void handleWaterbendingCosts(Game game, Ability source, Ability abilityToPay, ManaCosts manaCostsToPay) {
|
||||
Player controller = game.getPlayer(source.getControllerId());
|
||||
if (controller == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int total = CardUtil
|
||||
.castStream(manaCostsToPay, WaterbendCost.class)
|
||||
.mapToInt(WaterbendCost::manaValue)
|
||||
.sum();
|
||||
if (total < 1) {
|
||||
return;
|
||||
}
|
||||
TargetPermanent target = new TargetControlledPermanent(
|
||||
0, total, StaticFilters.FILTER_CONTROLLED_UNTAPPED_ARTIFACT_OR_CREATURE, true
|
||||
);
|
||||
target.withChooseHint("to tap for waterbending");
|
||||
controller.choose(Outcome.Tap, target, source, game);
|
||||
Set<Permanent> permanents = target
|
||||
.getTargets()
|
||||
.stream()
|
||||
.map(game::getPermanent)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
for (Permanent permanent : permanents) {
|
||||
permanent.tap(source, game);
|
||||
}
|
||||
manaCostsToPay.removeIf(WaterbendCost.class::isInstance);
|
||||
abilityToPay.addCost(new GenericManaCost(total - permanents.size()));
|
||||
game.fireEvent(GameEvent.getEvent(GameEvent.EventType.WATERBENDED, source.getSourceId(), source, controller.getId(), total));
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare and pay Phyrexian style effects like replace mana by life
|
||||
* Must be called after original Phyrexian mana processing and after cost modifications, e.g. on payment
|
||||
|
|
@ -779,54 +815,59 @@ public abstract class AbilityImpl implements Ability {
|
|||
}
|
||||
}
|
||||
}
|
||||
if (variableManaCost != null) {
|
||||
if (!variableManaCost.isPaid()) { // should only happen for human players
|
||||
int xValue;
|
||||
if (!noMana || variableManaCost.getCostType().canUseAnnounceOnFreeCast()) {
|
||||
if (variableManaCost.wasAnnounced()) {
|
||||
// announce by rules
|
||||
xValue = variableManaCost.getAmount();
|
||||
} else {
|
||||
// announce by player
|
||||
xValue = controller.announceX(variableManaCost.getMinX(), variableManaCost.getMaxX(),
|
||||
"Announce the value for " + variableManaCost.getText(), game, this, true);
|
||||
}
|
||||
|
||||
int amountMana = xValue * variableManaCost.getXInstancesCount();
|
||||
StringBuilder manaString = threadLocalBuilder.get();
|
||||
if (variableManaCost.getFilter() == null || variableManaCost.getFilter().isGeneric()) {
|
||||
manaString.append('{').append(amountMana).append('}');
|
||||
} else {
|
||||
String manaSymbol = null;
|
||||
if (variableManaCost.getFilter().isBlack()) {
|
||||
if (variableManaCost.getFilter().isRed()) {
|
||||
manaSymbol = "B/R";
|
||||
} else {
|
||||
manaSymbol = "B";
|
||||
}
|
||||
} else if (variableManaCost.getFilter().isRed()) {
|
||||
manaSymbol = "R";
|
||||
} else if (variableManaCost.getFilter().isBlue()) {
|
||||
manaSymbol = "U";
|
||||
} else if (variableManaCost.getFilter().isGreen()) {
|
||||
manaSymbol = "G";
|
||||
} else if (variableManaCost.getFilter().isWhite()) {
|
||||
manaSymbol = "W";
|
||||
}
|
||||
if (manaSymbol == null) {
|
||||
throw new UnsupportedOperationException("ManaFilter is not supported: " + this);
|
||||
}
|
||||
for (int i = 0; i < amountMana; i++) {
|
||||
manaString.append('{').append(manaSymbol).append('}');
|
||||
}
|
||||
}
|
||||
addManaCostsToPay(new ManaCostsImpl<>(manaString.toString()));
|
||||
getManaCostsToPay().setX(xValue, amountMana);
|
||||
setCostsTag("X", xValue);
|
||||
}
|
||||
variableManaCost.setPaid();
|
||||
}
|
||||
if (variableManaCost == null) {
|
||||
return variableManaCost;
|
||||
}
|
||||
if (variableManaCost.isPaid()) {
|
||||
return variableManaCost;
|
||||
} // should only happen for human players
|
||||
int xValue;
|
||||
if (!noMana || variableManaCost.getCostType().canUseAnnounceOnFreeCast()) {
|
||||
if (variableManaCost.wasAnnounced()) {
|
||||
// announce by rules
|
||||
xValue = variableManaCost.getAmount();
|
||||
} else {
|
||||
// announce by player
|
||||
xValue = controller.announceX(variableManaCost.getMinX(), variableManaCost.getMaxX(),
|
||||
"Announce the value for " + variableManaCost.getText(), game, this, true);
|
||||
}
|
||||
|
||||
int amountMana = xValue * variableManaCost.getXInstancesCount();
|
||||
StringBuilder manaString = threadLocalBuilder.get();
|
||||
if (!(variableManaCost instanceof WaterbendXCost)) {
|
||||
if (variableManaCost.getFilter() == null || variableManaCost.getFilter().isGeneric()) {
|
||||
manaString.append('{').append(amountMana).append('}');
|
||||
} else {
|
||||
String manaSymbol;
|
||||
if (variableManaCost.getFilter().isBlack()) {
|
||||
if (variableManaCost.getFilter().isRed()) {
|
||||
manaSymbol = "B/R";
|
||||
} else {
|
||||
manaSymbol = "B";
|
||||
}
|
||||
} else if (variableManaCost.getFilter().isRed()) {
|
||||
manaSymbol = "R";
|
||||
} else if (variableManaCost.getFilter().isBlue()) {
|
||||
manaSymbol = "U";
|
||||
} else if (variableManaCost.getFilter().isGreen()) {
|
||||
manaSymbol = "G";
|
||||
} else if (variableManaCost.getFilter().isWhite()) {
|
||||
manaSymbol = "W";
|
||||
} else {
|
||||
throw new UnsupportedOperationException("ManaFilter is not supported: " + this);
|
||||
}
|
||||
for (int i = 0; i < amountMana; i++) {
|
||||
manaString.append('{').append(manaSymbol).append('}');
|
||||
}
|
||||
}
|
||||
addManaCostsToPay(new ManaCostsImpl<>(manaString.toString()));
|
||||
} else {
|
||||
addManaCostsToPay(new WaterbendCost(amountMana));
|
||||
}
|
||||
getManaCostsToPay().setX(xValue, amountMana);
|
||||
setCostsTag("X", xValue);
|
||||
}
|
||||
variableManaCost.setPaid();
|
||||
|
||||
return variableManaCost;
|
||||
}
|
||||
|
|
@ -1701,15 +1742,15 @@ public abstract class AbilityImpl implements Ability {
|
|||
|
||||
@Override
|
||||
public void initSourceObjectZoneChangeCounter(Game game, boolean force) {
|
||||
if (!(this instanceof MageSingleton) && (force || sourceObjectZoneChangeCounter == 0 )) {
|
||||
if (!(this instanceof MageSingleton) && (force || sourceObjectZoneChangeCounter == 0)) {
|
||||
setSourceObjectZoneChangeCounter(getCurrentSourceObjectZoneChangeCounter(game));
|
||||
}
|
||||
}
|
||||
|
||||
private int getCurrentSourceObjectZoneChangeCounter(Game game){
|
||||
private int getCurrentSourceObjectZoneChangeCounter(Game game) {
|
||||
int zcc = game.getState().getZoneChangeCounter(getSourceId());
|
||||
Permanent p = game.getPermanentEntering(getSourceId());
|
||||
if (p != null && !(p instanceof PermanentToken)){
|
||||
if (p != null && !(p instanceof PermanentToken)) {
|
||||
// If the triggered ability triggered while the permanent is entering the battlefield
|
||||
// then add 1 zcc so that it triggers as if the permanent was already on the battlefield
|
||||
// So "Enters with counters" causes "Whenever counters are placed" to trigger with battlefield zcc
|
||||
|
|
|
|||
|
|
@ -1,26 +1,31 @@
|
|||
package mage.abilities.costs.common;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.costs.Cost;
|
||||
import mage.abilities.costs.CostImpl;
|
||||
import mage.game.Game;
|
||||
|
||||
import java.util.UUID;
|
||||
import mage.Mana;
|
||||
import mage.abilities.costs.mana.GenericManaCost;
|
||||
|
||||
/**
|
||||
* TODO: Implement properly
|
||||
* 701.67. Waterbend
|
||||
* <p>
|
||||
* 701.67a “Waterbend [cost]” means “Pay [cost]. For each generic mana in that cost,
|
||||
* you may tap an untapped artifact or creature you control rather than pay that mana.”
|
||||
* <p>
|
||||
* 701.67b If a waterbend cost is part of the total cost to cast a spell or activate an ability
|
||||
* (usually because the waterbend cost itself is an additional cost), the alternate method to pay for mana
|
||||
* described in rule 701.67a may be used only to pay for the amount of generic mana in the waterbend cost,
|
||||
* even if the total cost to cast that spell or activate that ability includes other generic mana components.
|
||||
* <p>
|
||||
* If you need Waterbend {X} then use {@link WaterbendXCost}
|
||||
* If using as an additional cost for a spell, add an ability with an InfoEffect for proper text generation (see WaterWhip)
|
||||
*
|
||||
* @author TheElk801
|
||||
*/
|
||||
public class WaterbendCost extends CostImpl {
|
||||
public class WaterbendCost extends GenericManaCost {
|
||||
|
||||
public WaterbendCost(int amount) {
|
||||
this("{" + amount + '}');
|
||||
}
|
||||
|
||||
public WaterbendCost(String mana) {
|
||||
super();
|
||||
this.text = "waterbend " + mana;
|
||||
super(amount);
|
||||
for (int i = 0; i < amount; i++) {
|
||||
options.add(Mana.ColorlessMana(i));
|
||||
}
|
||||
}
|
||||
|
||||
private WaterbendCost(final WaterbendCost cost) {
|
||||
|
|
@ -33,12 +38,7 @@ public class WaterbendCost extends CostImpl {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean pay(Ability ability, Game game, Ability source, UUID controllerId, boolean noMana, Cost costToPay) {
|
||||
return false;
|
||||
public String getText() {
|
||||
return "waterbend " + super.getText();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
package mage.abilities.costs.common;
|
||||
|
||||
import mage.abilities.costs.VariableCostType;
|
||||
import mage.abilities.costs.mana.VariableManaCost;
|
||||
|
||||
/**
|
||||
* Used for Waterbend {X} costs, otherwise use {@link WaterbendCost}
|
||||
* If using as an additional cost for a spell, add an ability with an InfoEffect for proper text generation (see WaterbendersRestoration)
|
||||
*
|
||||
* @author TheElk801
|
||||
*/
|
||||
public class WaterbendXCost extends VariableManaCost {
|
||||
|
||||
public WaterbendXCost() {
|
||||
this(0);
|
||||
}
|
||||
|
||||
public WaterbendXCost(int minX) {
|
||||
super(VariableCostType.NORMAL);
|
||||
this.setMinX(minX);
|
||||
}
|
||||
|
||||
private WaterbendXCost(final WaterbendXCost cost) {
|
||||
super(cost);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WaterbendXCost copy() {
|
||||
return new WaterbendXCost(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getText() {
|
||||
return "waterbend {X}";
|
||||
}
|
||||
}
|
||||
|
|
@ -5,20 +5,27 @@ import mage.abilities.Ability;
|
|||
import mage.abilities.AbilityImpl;
|
||||
import mage.abilities.costs.*;
|
||||
import mage.abilities.costs.common.PayLifeCost;
|
||||
import mage.abilities.costs.common.WaterbendCost;
|
||||
import mage.abilities.mana.ManaOptions;
|
||||
import mage.constants.ColoredManaSymbol;
|
||||
import mage.constants.ManaType;
|
||||
import mage.constants.Outcome;
|
||||
import mage.filter.Filter;
|
||||
import mage.filter.StaticFilters;
|
||||
import mage.game.Game;
|
||||
import mage.game.events.GameEvent;
|
||||
import mage.game.permanent.Permanent;
|
||||
import mage.players.ManaPool;
|
||||
import mage.players.Player;
|
||||
import mage.target.TargetPermanent;
|
||||
import mage.target.Targets;
|
||||
import mage.target.common.TargetControlledPermanent;
|
||||
import mage.util.CardUtil;
|
||||
import mage.util.ManaUtil;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @param <T>
|
||||
|
|
@ -162,6 +169,7 @@ public class ManaCostsImpl<T extends ManaCost> extends ArrayList<T> implements M
|
|||
if (payingPlayer != null) {
|
||||
int bookmark = game.bookmarkState();
|
||||
handlePhyrexianManaCosts(ability, payingPlayer, source, game);
|
||||
handleWaterbendingCosts(ability, payingPlayer, source, game);
|
||||
if (pay(ability, game, source, payingPlayerId, false, null)) {
|
||||
game.removeBookmark(bookmark);
|
||||
return true;
|
||||
|
|
@ -195,6 +203,33 @@ public class ManaCostsImpl<T extends ManaCost> extends ArrayList<T> implements M
|
|||
tempCosts.pay(source, game, source, payingPlayer.getId(), false, null);
|
||||
}
|
||||
|
||||
private void handleWaterbendingCosts(Ability abilityToPay, Player payingPlayer, Ability source, Game game) {
|
||||
int total = CardUtil
|
||||
.castStream(this, WaterbendCost.class)
|
||||
.mapToInt(WaterbendCost::manaValue)
|
||||
.sum();
|
||||
if (total < 1) {
|
||||
return;
|
||||
}
|
||||
TargetPermanent target = new TargetControlledPermanent(
|
||||
0, total, StaticFilters.FILTER_CONTROLLED_UNTAPPED_ARTIFACT_OR_CREATURE, true
|
||||
);
|
||||
target.withChooseHint("to tap for waterbending");
|
||||
payingPlayer.choose(Outcome.Tap, target, source, game);
|
||||
Set<Permanent> permanents = target
|
||||
.getTargets()
|
||||
.stream()
|
||||
.map(game::getPermanent)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
for (Permanent permanent : permanents) {
|
||||
permanent.tap(source, game);
|
||||
}
|
||||
this.removeIf(WaterbendCost.class::isInstance);
|
||||
this.add(new GenericManaCost(total - permanents.size()));
|
||||
game.fireEvent(GameEvent.getEvent(GameEvent.EventType.WATERBENDED, source.getSourceId(), source, payingPlayer.getId(), total));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ManaCosts<T> getUnpaid() {
|
||||
ManaCosts<T> unpaid = new ManaCostsImpl<>();
|
||||
|
|
|
|||
|
|
@ -478,6 +478,17 @@ public final class StaticFilters {
|
|||
FILTER_CONTROLLED_PERMANENT_ARTIFACT_OR_CREATURE.setLockedFilter(true);
|
||||
}
|
||||
|
||||
public static final FilterControlledPermanent FILTER_CONTROLLED_UNTAPPED_ARTIFACT_OR_CREATURE = new FilterControlledPermanent("untapped artifact or creature you control");
|
||||
|
||||
static {
|
||||
FILTER_CONTROLLED_UNTAPPED_ARTIFACT_OR_CREATURE.add(TappedPredicate.UNTAPPED);
|
||||
FILTER_CONTROLLED_UNTAPPED_ARTIFACT_OR_CREATURE.add(Predicates.or(
|
||||
CardType.ARTIFACT.getPredicate(),
|
||||
CardType.CREATURE.getPredicate()
|
||||
));
|
||||
FILTER_CONTROLLED_UNTAPPED_ARTIFACT_OR_CREATURE.setLockedFilter(true);
|
||||
}
|
||||
|
||||
public static final FilterControlledPermanent FILTER_CONTROLLED_ARTIFACT_OR_OTHER_CREATURE = new FilterControlledPermanent("another creature or an artifact");
|
||||
|
||||
static {
|
||||
|
|
|
|||
|
|
@ -3711,62 +3711,63 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
* @return
|
||||
*/
|
||||
protected boolean canPlay(ActivatedAbility ability, ManaOptions availableMana, MageObject sourceObject, Game game) {
|
||||
if (!ability.isManaActivatedAbility()) {
|
||||
ActivatedAbility copy = ability.copy(); // Copy is needed because cost reduction effects modify e.g. the mana to activate/cast the ability
|
||||
if (!copy.canActivate(playerId, game).canActivate()) {
|
||||
return false;
|
||||
if (ability.isManaActivatedAbility()) {
|
||||
return false;
|
||||
}
|
||||
ActivatedAbility copy = ability.copy(); // Copy is needed because cost reduction effects modify e.g. the mana to activate/cast the ability
|
||||
if (!copy.canActivate(playerId, game).canActivate()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// apply dynamic costs and cost modification
|
||||
copy.adjustX(game);
|
||||
if (availableMana != null) {
|
||||
// TODO: need research, why it look at availableMana here - can delete condition?
|
||||
game.getContinuousEffects().costModification(copy, game);
|
||||
}
|
||||
boolean canBeCastRegularly = true;
|
||||
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);
|
||||
|
||||
boolean canPutToPlay = true;
|
||||
if (alternateCosts != null && !alternateCosts.canPay(copy, copy, playerId, game)) {
|
||||
canPutToPlay = false;
|
||||
}
|
||||
if (costs != null && !costs.canPay(copy, copy, playerId, game)) {
|
||||
canPutToPlay = false;
|
||||
}
|
||||
|
||||
// apply dynamic costs and cost modification
|
||||
copy.adjustX(game);
|
||||
if (availableMana != null) {
|
||||
// TODO: need research, why it look at availableMana here - can delete condition?
|
||||
game.getContinuousEffects().costModification(copy, game);
|
||||
}
|
||||
boolean canBeCastRegularly = true;
|
||||
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)) {
|
||||
if (canPutToPlay) {
|
||||
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);
|
||||
|
||||
boolean canPutToPlay = true;
|
||||
if (alternateCosts != null && !alternateCosts.canPay(copy, copy, playerId, game)) {
|
||||
canPutToPlay = false;
|
||||
}
|
||||
if (costs != null && !costs.canPay(copy, copy, playerId, game)) {
|
||||
canPutToPlay = false;
|
||||
}
|
||||
|
||||
if (canPutToPlay) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// ALTERNATIVE COST from source card (any AlternativeSourceCosts)
|
||||
if (AbilityType.SPELL.equals(ability.getAbilityType())) {
|
||||
return canPlayCardByAlternateCost(game.getCard(ability.getSourceId()), availableMana, copy, game);
|
||||
}
|
||||
// ALTERNATIVE COST from source card (any AlternativeSourceCosts)
|
||||
if (AbilityType.SPELL.equals(ability.getAbilityType())) {
|
||||
return canPlayCardByAlternateCost(game.getCard(ability.getSourceId()), availableMana, copy, game);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
@ -4110,7 +4111,7 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
TransformingDoubleFacedCard mainCard = (TransformingDoubleFacedCard) object;
|
||||
getPlayableFromObjectSingle(game, fromZone, mainCard.getLeftHalfCard(), mainCard.getLeftHalfCard().getAbilities(game), availableMana, output);
|
||||
getPlayableFromObjectSingle(game, fromZone, mainCard, mainCard.getSharedAbilities(game), availableMana, output);
|
||||
} else if (object instanceof CardWithSpellOption) {
|
||||
} else if (object instanceof CardWithSpellOption) {
|
||||
// adventure must use different card characteristics for different spells (main or adventure)
|
||||
CardWithSpellOption cardWithSpellOption = (CardWithSpellOption) object;
|
||||
getPlayableFromObjectSingle(game, fromZone, cardWithSpellOption.getSpellCard(), cardWithSpellOption.getSpellCard().getAbilities(game), availableMana, output);
|
||||
|
|
@ -4254,8 +4255,7 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
|
||||
Game game = originalGame.createSimulationForPlayableCalc();
|
||||
ManaOptions availableMana = getManaAvailable(game); // get available mana options (mana pool and conditional mana added (but conditional still lose condition))
|
||||
boolean fromAll = fromZone.equals(Zone.ALL);
|
||||
if (hidden && (fromAll || fromZone == Zone.HAND)) {
|
||||
if (hidden && fromZone.match(Zone.HAND)) {
|
||||
for (Card card : hand.getCards(game)) {
|
||||
for (Ability ability : card.getAbilities(game)) { // gets this activated ability from hand? (Morph?)
|
||||
if (ability.getZone().match(Zone.HAND)) {
|
||||
|
|
@ -4300,7 +4300,7 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
}
|
||||
}
|
||||
|
||||
if (fromAll || fromZone == Zone.GRAVEYARD) {
|
||||
if (fromZone.match(Zone.GRAVEYARD)) {
|
||||
for (UUID playerId : game.getState().getPlayersInRange(getId(), game)) {
|
||||
Player player = game.getPlayer(playerId);
|
||||
if (player == null) {
|
||||
|
|
@ -4312,7 +4312,7 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
}
|
||||
}
|
||||
|
||||
if (fromAll || fromZone == Zone.EXILED) {
|
||||
if (fromZone.match(Zone.EXILED)) {
|
||||
for (ExileZone exile : game.getExile().getExileZones()) {
|
||||
for (Card card : exile.getCards(game)) {
|
||||
getPlayableFromObjectAll(game, Zone.EXILED, card, availableMana, playable);
|
||||
|
|
@ -4321,7 +4321,7 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
}
|
||||
|
||||
// check to play revealed cards
|
||||
if (fromAll) {
|
||||
if (fromZone.match(Zone.ALL)) {
|
||||
for (Cards revealedCards : game.getState().getRevealed().values()) {
|
||||
for (Card card : revealedCards.getCards(game)) {
|
||||
// revealed cards can be from any zones
|
||||
|
|
@ -4331,7 +4331,7 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
}
|
||||
|
||||
// outside cards
|
||||
if (fromAll || fromZone == Zone.OUTSIDE) {
|
||||
if (fromZone.match(Zone.OUTSIDE)) {
|
||||
// companion cards
|
||||
for (Cards companionCards : game.getState().getCompanion().values()) {
|
||||
for (Card card : companionCards.getCards(game)) {
|
||||
|
|
@ -4349,7 +4349,7 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
}
|
||||
|
||||
// check if it's possible to play the top card of a library
|
||||
if (fromAll || fromZone == Zone.LIBRARY) {
|
||||
if (fromZone.match(Zone.LIBRARY)) {
|
||||
for (UUID playerInRangeId : game.getState().getPlayersInRange(getId(), game)) {
|
||||
Player player = game.getPlayer(playerInRangeId);
|
||||
if (player != null && player.getLibrary().hasCards()) {
|
||||
|
|
@ -4365,7 +4365,7 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
// TODO: remove direct hand check (reveal fix in Sen Triplets)?
|
||||
// human games: cards from opponent's hand must be revealed before play
|
||||
// AI games: computer can see and play cards from opponent's hand without reveal
|
||||
if (fromAll || fromZone == Zone.HAND) {
|
||||
if (fromZone.match(Zone.HAND)) {
|
||||
for (UUID playerInRangeId : game.getState().getPlayersInRange(getId(), game)) {
|
||||
Player player = game.getPlayer(playerInRangeId);
|
||||
if (player != null && !player.getHand().isEmpty()) {
|
||||
|
|
@ -4383,7 +4383,7 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
List<ActivatedAbility> activatedAll = new ArrayList<>();
|
||||
|
||||
// activated abilities from battlefield objects
|
||||
if (fromAll || fromZone == Zone.BATTLEFIELD) {
|
||||
if (fromZone.match(Zone.BATTLEFIELD)) {
|
||||
for (Permanent permanent : game.getBattlefield().getAllActivePermanents()) {
|
||||
boolean canUseActivated = permanent.canUseActivatedAbilities(game);
|
||||
List<ActivatedAbility> currentPlayable = new ArrayList<>();
|
||||
|
|
@ -4398,7 +4398,7 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
}
|
||||
|
||||
// activated abilities from stack objects
|
||||
if (fromAll || fromZone == Zone.STACK) {
|
||||
if (fromZone.match(Zone.STACK)) {
|
||||
for (StackObject stackObject : game.getState().getStack()) {
|
||||
List<ActivatedAbility> currentPlayable = new ArrayList<>();
|
||||
getPlayableFromObjectAll(game, Zone.STACK, stackObject, availableMana, currentPlayable);
|
||||
|
|
@ -4410,7 +4410,7 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
}
|
||||
|
||||
// activated abilities from objects in the command zone (emblems or commanders)
|
||||
if (fromAll || fromZone == Zone.COMMAND) {
|
||||
if (fromZone.match(Zone.COMMAND)) {
|
||||
for (CommandObject commandObject : game.getState().getCommand()) {
|
||||
List<ActivatedAbility> currentPlayable = new ArrayList<>();
|
||||
getPlayableFromObjectAll(game, Zone.COMMAND, commandObject, availableMana, currentPlayable);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue