Merge branch 'External-master'
All checks were successful
/ build_release (push) Successful in 27m57s

This commit is contained in:
Failure 2025-04-11 20:17:40 -07:00
commit d2c32ec53f
435 changed files with 12052 additions and 2085 deletions

View file

@ -126,7 +126,7 @@ public interface Ability extends Controllable, Serializable {
/**
* Gets all {@link ManaCosts} associated with this ability. These returned
* costs should never be modified as they represent the base costs before
* any modifications.
* any modifications (only cost adjusters can change it, e.g. set min/max values)
*
* @return All {@link ManaCosts} that must be paid.
*/
@ -151,6 +151,17 @@ public interface Ability extends Controllable, Serializable {
void addManaCostsToPay(ManaCost manaCost);
/**
* Helper method to setup actual min/max limits of current X costs BEFORE player's X announcement
*/
void setVariableCostsMinMax(int min, int max);
/**
* Helper method to replace X by direct value BEFORE player's X announcement
* If you need additional target for X then use CostAdjuster + EarlyTargetCost (example: Bargaining Table)
*/
void setVariableCostsValue(int xValue);
/**
* Gets a map of the cost tags (set while casting/activating) of this ability, can be null if no tags have been set yet.
* Does NOT return the source permanent's tags.
@ -356,7 +367,7 @@ public interface Ability extends Controllable, Serializable {
* - for dies triggers - override and use TriggeredAbilityImpl.isInUseableZoneDiesTrigger inside + set setLeavesTheBattlefieldTrigger(true)
*
* @param sourceObject can be null for static continues effects checking like rules modification (example: Yixlid Jailer)
* @param event can be null for state base effects checking like "when you control seven or more" (example: Endrek Sahr, Master Breeder)
* @param event can be null for state base effects checking like "when you control seven or more" (example: Endrek Sahr, Master Breeder)
*/
boolean isInUseableZone(Game game, MageObject sourceObject, GameEvent event);
@ -533,11 +544,25 @@ public interface Ability extends Controllable, Serializable {
void adjustTargets(Game game);
/**
* Dynamic X and cost modification, see CostAdjuster for more details on usage
*/
Ability setCostAdjuster(CostAdjuster costAdjuster);
CostAdjuster getCostAdjuster();
/**
* Prepare {X} settings for announce
*/
void adjustX(Game game);
void adjustCosts(Game game);
/**
* Prepare costs (generate due game state or announce)
*/
void adjustCostsPrepare(Game game);
/**
* Apply additional cost modifications logic/effects
*/
void adjustCostsModify(Game game, CostModificationType costModificationType);
List<Hint> getHints();

View file

@ -291,6 +291,8 @@ public abstract class AbilityImpl implements Ability {
// fused or spliced spells contain multiple abilities (e.g. fused, left, right)
// optional costs and cost modification must be applied only to the first/main ability
// TODO: need tests with X announced costs, cost modification effects, CostAdjuster, early cost target, etc
// can be bugged due multiple calls (not all code parts below use isMainPartAbility)
boolean isMainPartAbility = !CardUtil.isFusedPartAbility(this, game);
/* 20220908 - 601.2b
@ -326,6 +328,16 @@ public abstract class AbilityImpl implements Ability {
return false;
}
// 20241022 - 601.2b
// Choose targets for costs that have to be chosen early
// Not yet included in 601.2b but this is where it will be
handleChooseCostTargets(game, controller);
// prepare dynamic costs (must be called before any x announce)
if (isMainPartAbility) {
adjustX(game);
}
// 20121001 - 601.2b
// If the spell has a variable cost that will be paid as it's being cast (such as an {X} in
// its mana cost; see rule 107.3), the player announces the value of that variable.
@ -402,7 +414,7 @@ public abstract class AbilityImpl implements Ability {
// Note: ActivatedAbility does include SpellAbility & PlayLandAbility, but those should be able to be canceled too.
boolean canCancel = this instanceof ActivatedAbility && controller.isHuman();
if (!getTargets().chooseTargets(outcome, this.controllerId, this, noMana, game, canCancel)) {
// was canceled during targer selection
// was canceled during target selection
return false;
}
}
@ -428,7 +440,7 @@ public abstract class AbilityImpl implements Ability {
//20101001 - 601.2e
if (isMainPartAbility) {
adjustCosts(game); // still needed for CostAdjuster objects (to handle some types of dynamic costs)
// adjustX already called before any announces
game.getContinuousEffects().costModification(this, game);
}
@ -483,6 +495,7 @@ public abstract class AbilityImpl implements Ability {
// A player can't apply two alternative methods of casting or two alternative costs to a single spell.
switch (((SpellAbility) this).getSpellAbilityCastMode()) {
case FLASHBACK:
case HARMONIZE:
case MADNESS:
case TRANSFORMED:
case DISTURB:
@ -726,6 +739,11 @@ public abstract class AbilityImpl implements Ability {
((EarlyTargetCost) cost).chooseTarget(game, this, controller);
}
}
for (ManaCost cost : getManaCostsToPay()) {
if (cost instanceof EarlyTargetCost && cost.getTargets().isEmpty()) {
((EarlyTargetCost) cost).chooseTarget(game, this, controller);
}
}
}
/**
@ -764,8 +782,15 @@ public abstract class AbilityImpl implements Ability {
if (!variableManaCost.isPaid()) { // should only happen for human players
int xValue;
if (!noMana || variableManaCost.getCostType().canUseAnnounceOnFreeCast()) {
xValue = controller.announceXMana(variableManaCost.getMinX(), variableManaCost.getMaxX(),
"Announce the value for " + variableManaCost.getText(), game, this);
if (variableManaCost.wasAnnounced()) {
// announce by rules
xValue = variableManaCost.getAmount();
} else {
// announce by player
xValue = controller.announceXMana(variableManaCost.getMinX(), variableManaCost.getMaxX(),
"Announce the value for " + variableManaCost.getText(), game, this);
}
int amountMana = xValue * variableManaCost.getXInstancesCount();
StringBuilder manaString = threadLocalBuilder.get();
if (variableManaCost.getFilter() == null || variableManaCost.getFilter().isGeneric()) {
@ -1072,6 +1097,60 @@ public abstract class AbilityImpl implements Ability {
}
}
@Override
public void setVariableCostsMinMax(int min, int max) {
// modify all values (mtg rules allow only one type of X, so min/max must be shared between all X instances)
// base cost
for (ManaCost cost : getManaCosts()) {
if (cost instanceof MinMaxVariableCost) {
MinMaxVariableCost minMaxCost = (MinMaxVariableCost) cost;
minMaxCost.setMinX(min);
minMaxCost.setMaxX(max);
}
}
// prepared cost
for (ManaCost cost : getManaCostsToPay()) {
if (cost instanceof MinMaxVariableCost) {
MinMaxVariableCost minMaxCost = (MinMaxVariableCost) cost;
minMaxCost.setMinX(min);
minMaxCost.setMaxX(max);
}
}
}
@Override
public void setVariableCostsValue(int xValue) {
// only mana cost supported
// base cost
boolean foundBaseCost = false;
for (ManaCost cost : getManaCosts()) {
if (cost instanceof VariableManaCost) {
foundBaseCost = true;
((VariableManaCost) cost).setMinX(xValue);
((VariableManaCost) cost).setMaxX(xValue);
((VariableManaCost) cost).setAmount(xValue, xValue, false);
}
}
// prepared cost
boolean foundPreparedCost = false;
for (ManaCost cost : getManaCostsToPay()) {
if (cost instanceof VariableManaCost) {
foundPreparedCost = true;
((VariableManaCost) cost).setMinX(xValue);
((VariableManaCost) cost).setMaxX(xValue);
((VariableManaCost) cost).setAmount(xValue, xValue, false);
}
}
if (!foundPreparedCost || !foundBaseCost) {
throw new IllegalArgumentException("Wrong code usage: auto-announced X values allowed in mana costs only");
}
}
@Override
public void addEffect(Effect effect) {
if (effect != null) {
@ -1676,17 +1755,6 @@ public abstract class AbilityImpl implements Ability {
}
}
/**
* Dynamic cost modification for ability.<br>
* Example: if it need stack related info (like real targets) then must
* check two states (game.inCheckPlayableState): <br>
* 1. In playable state it must check all possible use cases (e.g. allow to
* reduce on any available target and modes) <br>
* 2. In real cast state it must check current use case (e.g. real selected
* targets and modes)
*
* @param costAdjuster
*/
@Override
public AbilityImpl setCostAdjuster(CostAdjuster costAdjuster) {
this.costAdjuster = costAdjuster;
@ -1694,14 +1762,23 @@ public abstract class AbilityImpl implements Ability {
}
@Override
public CostAdjuster getCostAdjuster() {
return costAdjuster;
public void adjustX(Game game) {
if (costAdjuster != null) {
costAdjuster.prepareX(this, game);
}
}
@Override
public void adjustCosts(Game game) {
public void adjustCostsPrepare(Game game) {
if (costAdjuster != null) {
costAdjuster.adjustCosts(this, game);
costAdjuster.prepareCost(this, game);
}
}
@Override
public void adjustCostsModify(Game game, CostModificationType costModificationType) {
if (costAdjuster != null) {
costAdjuster.modifyCost(this, game, costModificationType);
}
}

View file

@ -6,8 +6,8 @@ import mage.MageObject;
import mage.abilities.costs.mana.ManaCost;
import mage.abilities.costs.mana.VariableManaCost;
import mage.abilities.keyword.FlashAbility;
import mage.cards.AdventureCardSpell;
import mage.cards.Card;
import mage.cards.SpellOptionCard;
import mage.cards.SplitCard;
import mage.constants.*;
import mage.game.Game;
@ -99,7 +99,7 @@ public class SpellAbility extends ActivatedAbilityImpl {
// forced to cast (can be part id or main id)
Set<UUID> idsToCheck = new HashSet<>();
idsToCheck.add(object.getId());
if (object instanceof Card && !(object instanceof AdventureCardSpell)) {
if (object instanceof Card && !(object instanceof SpellOptionCard)) {
idsToCheck.add(((Card) object).getMainCard().getId());
}
for (UUID idToCheck : idsToCheck) {

View file

@ -70,7 +70,7 @@ public class BecomesTargetAnyTriggeredAbility extends TriggeredAbilityImpl {
if (permanent == null || !filterTarget.match(permanent, getControllerId(), this, game)) {
return false;
}
StackObject targetingObject = CardUtil.getTargetingStackObject(event, game);
StackObject targetingObject = CardUtil.getTargetingStackObject(this.getId().toString(), event, game);
if (targetingObject == null || !filterStack.match(targetingObject, getControllerId(), this, game)) {
return false;
}

View file

@ -54,7 +54,7 @@ public class BecomesTargetAttachedTriggeredAbility extends TriggeredAbilityImpl
if (enchantment == null || enchantment.getAttachedTo() == null || !event.getTargetId().equals(enchantment.getAttachedTo())) {
return false;
}
StackObject targetingObject = CardUtil.getTargetingStackObject(event, game);
StackObject targetingObject = CardUtil.getTargetingStackObject(this.getId().toString(), event, game);
if (targetingObject == null || !filter.match(targetingObject, getControllerId(), this, game)) {
return false;
}

View file

@ -63,7 +63,7 @@ public class BecomesTargetControllerTriggeredAbility extends TriggeredAbilityImp
return false;
}
}
StackObject targetingObject = CardUtil.getTargetingStackObject(event, game);
StackObject targetingObject = CardUtil.getTargetingStackObject(this.getId().toString(), event, game);
if (targetingObject == null || !filterStack.match(targetingObject, getControllerId(), this, game)) {
return false;
}

View file

@ -57,7 +57,7 @@ public class BecomesTargetSourceTriggeredAbility extends TriggeredAbilityImpl {
if (!event.getTargetId().equals(getSourceId())) {
return false;
}
StackObject targetingObject = CardUtil.getTargetingStackObject(event, game);
StackObject targetingObject = CardUtil.getTargetingStackObject(this.getId().toString(), event, game);
if (targetingObject == null || !filter.match(targetingObject, getControllerId(), this, game)) {
return false;
}

View file

@ -0,0 +1,44 @@
package mage.abilities.condition.common;
import mage.abilities.Ability;
import mage.abilities.condition.Condition;
import mage.abilities.hint.ConditionHint;
import mage.abilities.hint.Hint;
import mage.game.Game;
import mage.game.stack.Spell;
import mage.watchers.common.SpellsCastWatcher;
import java.util.List;
import java.util.Objects;
/**
* @author balazskristof, Jmlundeen
*/
public enum CastAnotherSpellThisTurnCondition implements Condition {
instance;
private final Hint hint = new ConditionHint(
this, "You've cast another spell this turn"
);
@Override
public boolean apply(Game game, Ability source) {
SpellsCastWatcher watcher = game.getState().getWatcher(SpellsCastWatcher.class);
if (watcher == null) {
return false;
}
List<Spell> spells = watcher.getSpellsCastThisTurn(source.getControllerId());
return spells != null && spells
.stream()
.filter(Objects::nonNull)
.anyMatch(spell -> !spell.getSourceId().equals(source.getSourceId()) || spell.getZoneChangeCounter(game) != source.getSourceObjectZoneChangeCounter());
}
public Hint getHint() {
return hint;
}
@Override
public String toString() {
return "you've cast another spell this turn";
}
}

View file

@ -4,7 +4,7 @@ package mage.abilities.condition.common;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.condition.Condition;
import mage.cards.AdventureCardSpell;
import mage.cards.SpellOptionCard;
import mage.cards.Card;
import mage.cards.ModalDoubleFacedCardHalf;
import mage.cards.SplitCardHalf;
@ -20,7 +20,7 @@ public enum IsBeingCastFromHandCondition implements Condition {
@Override
public boolean apply(Game game, Ability source) {
MageObject object = game.getObject(source);
if (object instanceof SplitCardHalf || object instanceof AdventureCardSpell || object instanceof ModalDoubleFacedCardHalf) {
if (object instanceof SplitCardHalf || object instanceof SpellOptionCard || object instanceof ModalDoubleFacedCardHalf) {
UUID mainCardId = ((Card) object).getMainCard().getId();
object = game.getObject(mainCardId);
}

View file

@ -1,26 +0,0 @@
package mage.abilities.condition.common;
import mage.abilities.Ability;
import mage.abilities.condition.Condition;
import mage.game.Game;
/**
*
* @author LevelX2
*/
public class ModeChoiceSourceCondition implements Condition {
private final String mode;
public ModeChoiceSourceCondition(String mode) {
this.mode = mode;
}
@Override
public boolean apply(Game game, Ability source) {
String chosenMode = (String) game.getState().getValue(source.getSourceId() + "_modeChoice");
return chosenMode != null && chosenMode.equals(mode);
}
}

View file

@ -0,0 +1,19 @@
package mage.abilities.condition.common;
import mage.abilities.Ability;
import mage.abilities.condition.Condition;
import mage.game.Game;
import mage.watchers.common.SpellsCastWatcher;
/**
* @author androosss
*/
public enum YouCastExactOneSpellThisTurnCondition implements Condition {
instance;
@Override
public boolean apply(Game game, Ability source) {
SpellsCastWatcher watcher = game.getState().getWatcher(SpellsCastWatcher.class);
return watcher != null && watcher.getSpellsCastThisTurn(source.getControllerId()).size() == 1;
}
}

View file

@ -1,24 +1,86 @@
package mage.abilities.costs;
import mage.abilities.Ability;
import mage.constants.CostModificationType;
import mage.game.Game;
import java.io.Serializable;
/**
* @author TheElk801
* Dynamic costs implementation to control {X} or other costs, can be used in spells and abilities
* <p>
* Possible use cases:
* - define {X} costs like X cards to discard (mana and non-mana values);
* - define {X} limits before announce (to help in UX and AI logic);
* - define any dynamic costs;
* - use as simple cost increase/reduce effect;
* <p>
* Calls order by game engine:
* - ... early cost target selection for EarlyTargetCost ...
* - prepareX
* - ... x announce ...
* - prepareCost
* - increaseCost
* - reduceCost
* - ... normal target selection and payment ...
*
* @author TheElk801, JayDi85
*/
@FunctionalInterface
public interface CostAdjuster extends Serializable {
/**
* Must check playable and real cast states.
* Example: if it need stack related info (like real targets) then must check two states (game.inCheckPlayableState):
* 1. In playable state it must check all possible use cases (e.g. allow to reduce on any available target and modes)
* 2. In real cast state it must check current use case (e.g. real selected targets and modes)
*
* @param ability
* @param game
* Prepare {X} costs settings or define auto-announced mana values
* <p>
* Usage example:
* - define auto-announced mana value {X} by ability.setVariableCostsValue
* - define possible {X} settings by ability.setVariableCostsMinMax
*/
void adjustCosts(Ability ability, Game game);
default void prepareX(Ability ability, Game game) {
// do nothing
}
/**
* Prepare any dynamic costs
* <p>
* Usage example:
* - add real cost after {X} mana value announce by CardUtil.getSourceCostsTagX
* - add dynamic cost from game data
*/
default void prepareCost(Ability ability, Game game) {
// do nothing
}
/**
* Simple cost reduction effect
*/
default void reduceCost(Ability ability, Game game) {
// do nothing
}
/**
* Simple cost increase effect
*/
default void increaseCost(Ability ability, Game game) {
// do nothing
}
/**
* Default implementation. Override reduceCost or increaseCost instead
* TODO: make it private after java 9+ migrate
*/
default void modifyCost(Ability ability, Game game, CostModificationType costModificationType) {
switch (costModificationType) {
case REDUCE_COST:
reduceCost(ability, game);
break;
case INCREASE_COST:
increaseCost(ability, game);
break;
case SET_COST:
// do nothing
break;
default:
throw new IllegalArgumentException("Unknown mod type: " + costModificationType);
}
}
}

View file

@ -6,20 +6,11 @@ import mage.players.Player;
/**
* @author Grath
* Costs which extend this class need to have targets chosen, and those targets must be chosen during 601.2b step.
* <p>
* Support 601.2b rules for ealry target choice before X announce and other actions
*/
public abstract class EarlyTargetCost extends CostImpl {
public interface EarlyTargetCost {
protected EarlyTargetCost() {
super();
}
void chooseTarget(Game game, Ability source, Player controller);
protected EarlyTargetCost(final EarlyTargetCost cost) {
super(cost);
}
@Override
public abstract EarlyTargetCost copy();
public abstract void chooseTarget(Game game, Ability source, Player controller);
}

View file

@ -0,0 +1,15 @@
package mage.abilities.costs;
/**
* @author jayDi85
*/
public interface MinMaxVariableCost extends VariableCost {
int getMinX();
void setMinX(int minX);
int getMaxX();
void setMaxX(int maxX);
}

View file

@ -10,11 +10,20 @@ import mage.players.Player;
import mage.target.common.TargetCardInHand;
/**
* Used to setup discard cost WITHOUT {X} mana cost
* <p>
* If you have {X} in spell's mana cost then use DiscardXCardsCostAdjuster instead
* <p>
* Example:
* - {2}{U}{R}
* - As an additional cost to cast this spell, discard X cards.
*
* @author LevelX2
*/
public class DiscardXTargetCost extends VariableCostImpl {
protected FilterCard filter;
protected boolean isRandom = false;
public DiscardXTargetCost(FilterCard filter) {
this(filter, false);
@ -30,6 +39,12 @@ public class DiscardXTargetCost extends VariableCostImpl {
protected DiscardXTargetCost(final DiscardXTargetCost cost) {
super(cost);
this.filter = cost.filter;
this.isRandom = cost.isRandom;
}
public DiscardXTargetCost withRandom() {
this.isRandom = true;
return this;
}
@Override
@ -49,6 +64,6 @@ public class DiscardXTargetCost extends VariableCostImpl {
@Override
public Cost getFixedCostsFromAnnouncedValue(int xValue) {
TargetCardInHand target = new TargetCardInHand(xValue, filter);
return new DiscardTargetCost(target);
return new DiscardTargetCost(target, this.isRandom);
}
}

View file

@ -35,10 +35,10 @@ public class PayLoyaltyCost extends CostImpl {
int loyaltyCost = amount;
// apply cost modification
// apply dynamic costs and cost modification
if (ability instanceof LoyaltyAbility) {
LoyaltyAbility copiedAbility = ((LoyaltyAbility) ability).copy();
copiedAbility.adjustCosts(game);
copiedAbility.adjustX(game);
game.getContinuousEffects().costModification(copiedAbility, game);
loyaltyCost = 0;
for (Cost cost : copiedAbility.getCosts()) {

View file

@ -59,10 +59,10 @@ public class PayVariableLoyaltyCost extends VariableCostImpl {
int maxValue = permanent.getCounters(game).getCount(CounterType.LOYALTY);
// apply cost modification
// apply dynamic costs and cost modification
if (source instanceof LoyaltyAbility) {
LoyaltyAbility copiedAbility = ((LoyaltyAbility) source).copy();
copiedAbility.adjustCosts(game);
copiedAbility.adjustX(game);
game.getContinuousEffects().costModification(copiedAbility, game);
for (Cost cost : copiedAbility.getCosts()) {
if (cost instanceof PayVariableLoyaltyCost) {

View file

@ -13,7 +13,7 @@ public enum CommanderManaValueAdjuster implements CostAdjuster {
instance;
@Override
public void adjustCosts(Ability ability, Game game) {
public void reduceCost(Ability ability, Game game) {
CardUtil.reduceCost(ability, GreatestCommanderManaValue.instance.calculate(game, ability, null));
}
}

View file

@ -0,0 +1,73 @@
package mage.abilities.costs.costadjusters;
import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.costs.CostAdjuster;
import mage.abilities.costs.common.DiscardTargetCost;
import mage.abilities.effects.common.InfoEffect;
import mage.cards.Card;
import mage.constants.Zone;
import mage.filter.FilterCard;
import mage.game.Game;
import mage.players.Player;
import mage.target.common.TargetCardInHand;
import mage.util.CardUtil;
/**
* Used to setup discard cost with {X} mana cost
* <p>
* If you don't have {X} then use DiscardXTargetCost instead
* <p>
* Example:
* - {X}{1}{B}
* - As an additional cost to cast this spell, discard X cards.
*
* @author JayDi85
*/
public class DiscardXCardsCostAdjuster implements CostAdjuster {
private final FilterCard filter;
private DiscardXCardsCostAdjuster(FilterCard filter) {
this.filter = filter;
}
@Override
public void prepareX(Ability ability, Game game) {
Player controller = game.getPlayer(ability.getControllerId());
if (controller == null) {
return;
}
int minX = 0;
int maxX = controller.getHand().getCards(this.filter, ability.getControllerId(), ability, game).size();
ability.setVariableCostsMinMax(minX, maxX);
}
@Override
public void prepareCost(Ability ability, Game game) {
int x = CardUtil.getSourceCostsTagX(game, ability, -1);
if (x >= 0) {
ability.addCost(new DiscardTargetCost(new TargetCardInHand(x, x, this.filter)));
}
}
public static void addAdjusterAndMessage(Card card, FilterCard filter) {
addAdjusterAndMessage(card, filter, false);
}
public static void addAdjusterAndMessage(Card card, FilterCard filter, boolean isRandom) {
if (card.getSpellAbility().getManaCosts().getVariableCosts().isEmpty()) {
// how to fix: use DiscardXTargetCost
throw new IllegalArgumentException("Wrong code usage: that's cost adjuster must be used with {X} in mana costs only - " + card);
}
Ability ability = new SimpleStaticAbility(
Zone.ALL, new InfoEffect("As an additional cost to cast this spell, discard X " + filter.getMessage())
);
ability.setRuleAtTheTop(true);
card.addAbility(ability);
card.getSpellAbility().setCostAdjuster(new DiscardXCardsCostAdjuster(filter));
}
}

View file

@ -13,7 +13,7 @@ public enum DomainAdjuster implements CostAdjuster {
instance;
@Override
public void adjustCosts(Ability ability, Game game) {
public void reduceCost(Ability ability, Game game) {
CardUtil.reduceCost(ability, DomainValue.REGULAR.calculate(game, ability, null));
}
}

View file

@ -25,22 +25,29 @@ public class ExileCardsFromHandAdjuster implements CostAdjuster {
}
@Override
public void adjustCosts(Ability ability, Game game) {
if (game.inCheckPlayableState()) {
return;
}
public void reduceCost(Ability ability, Game game) {
Player player = game.getPlayer(ability.getControllerId());
if (player == null) {
return;
}
int cardCount = player.getHand().count(filter, game);
int toExile = cardCount > 0 ? player.getAmount(
0, cardCount, "Choose how many " + filter.getMessage() + " to exile", game
) : 0;
if (toExile > 0) {
ability.addCost(new ExileFromHandCost(new TargetCardInHand(toExile, filter)));
CardUtil.reduceCost(ability, 2 * toExile);
int reduceCount;
if (game.inCheckPlayableState()) {
// possible
reduceCount = 2 * cardCount;
} else {
// real - need to choose
// TODO: need early target cost instead dialog here
int toExile = cardCount == 0 ? 0 : player.getAmount(
0, cardCount, "Choose how many " + filter.getMessage() + " to exile", game
);
reduceCount = 2 * toExile;
if (toExile > 0) {
ability.addCost(new ExileFromHandCost(new TargetCardInHand(toExile, filter)));
}
}
CardUtil.reduceCost(ability, 2 * reduceCount);
}
public static final void addAdjusterAndMessage(Card card, FilterCard filter) {

View file

@ -0,0 +1,37 @@
package mage.abilities.costs.costadjusters;
import mage.abilities.Ability;
import mage.abilities.costs.CostAdjuster;
import mage.cards.Card;
import mage.game.Game;
import mage.game.permanent.Permanent;
/**
* Used for {X} mana cost that must be replaced by imprinted mana value
* <p>
* Example:
* - Elite Arcanist
* - {X}, {T}: Copy the exiled card. ... X is the converted mana cost of the exiled card.
*
* @author JayDi85
*/
public enum ImprintedManaValueXCostAdjuster implements CostAdjuster {
instance;
@Override
public void prepareX(Ability ability, Game game) {
int manaValue = Integer.MAX_VALUE;
Permanent sourcePermanent = game.getPermanent(ability.getSourceId());
if (sourcePermanent != null
&& sourcePermanent.getImprinted() != null
&& !sourcePermanent.getImprinted().isEmpty()) {
Card imprintedInstant = game.getCard(sourcePermanent.getImprinted().get(0));
if (imprintedInstant != null) {
manaValue = imprintedInstant.getManaValue();
}
}
ability.setVariableCostsValue(manaValue);
}
}

View file

@ -29,10 +29,8 @@ public enum LegendaryCreatureCostAdjuster implements CostAdjuster {
);
@Override
public void adjustCosts(Ability ability, Game game) {
int count = game.getBattlefield().count(
filter, ability.getControllerId(), ability, game
);
public void reduceCost(Ability ability, Game game) {
int count = game.getBattlefield().count(filter, ability.getControllerId(), ability, game);
if (count > 0) {
CardUtil.reduceCost(ability, count);
}

View file

@ -32,7 +32,7 @@ public interface ManaCost extends Cost {
ManaOptions getOptions();
/**
* Return all options for paying the mana cost (this) while taking into accoutn if the player can pay life.
* Return all options for paying the mana cost (this) while taking into account if the player can pay life.
* Used to correctly highlight (or not) spells with Phyrexian mana depending on if the player can pay life costs.
* <p>
* E.g. Tezzeret's Gambit has a cost of {3}{U/P}.

View file

@ -72,7 +72,7 @@ public abstract class ManaCostImpl extends CostImpl implements ManaCost {
}
@Override
public ManaOptions getOptions() {
public final ManaOptions getOptions() {
return getOptions(true);
}

View file

@ -439,7 +439,7 @@ public class ManaCostsImpl<T extends ManaCost> extends ArrayList<T> implements M
this.add(new ColoredManaCost(ColoredManaSymbol.lookup(symbol.charAt(0))));
} else // check X wasn't added before
if (modifierForX == 0) {
// count X occurence
// count X occurrence
for (String s : symbols) {
if (s.equals("X")) {
modifierForX++;

View file

@ -3,17 +3,19 @@ package mage.abilities.costs.mana;
import mage.Mana;
import mage.abilities.Ability;
import mage.abilities.costs.Cost;
import mage.abilities.costs.VariableCost;
import mage.abilities.costs.MinMaxVariableCost;
import mage.abilities.costs.VariableCostType;
import mage.abilities.mana.ManaOptions;
import mage.constants.ColoredManaSymbol;
import mage.filter.FilterMana;
import mage.game.Game;
import mage.players.ManaPool;
import mage.util.CardUtil;
/**
* @author BetaSteward_at_googlemail.com, JayDi85
*/
public final class VariableManaCost extends ManaCostImpl implements VariableCost {
public class VariableManaCost extends ManaCostImpl implements MinMaxVariableCost {
// variable mana cost usage on 2019-06-20:
// 1. as X value in spell/ability cast (announce X, set VariableManaCost as paid and add generic mana to pay instead)
@ -23,7 +25,7 @@ public final class VariableManaCost extends ManaCostImpl implements VariableCost
protected int xInstancesCount; // number of {X} instances in cost like {X} or {X}{X}
protected int xValue = 0; // final X value after announce and replace events
protected int xPay = 0; // final/total need pay after announce and replace events (example: {X}{X}, X=3, xPay = 6)
protected boolean wasAnnounced = false;
protected boolean wasAnnounced = false; // X was announced by player or auto-defined by CostAdjuster
protected FilterMana filter; // mana filter that can be used for that cost
protected int minX = 0;
@ -59,6 +61,18 @@ public final class VariableManaCost extends ManaCostImpl implements VariableCost
return 0;
}
@Override
public ManaOptions getOptions(boolean canPayLifeCost) {
ManaOptions res = new ManaOptions();
// limit mana options for better performance
CardUtil.distributeValues(10, getMinX(), getMaxX()).forEach(value -> {
res.add(Mana.GenericMana(value));
});
return res;
}
@Override
public void assignPayment(Game game, Ability ability, ManaPool pool, Cost costToPay) {
// X mana cost always pays as generic mana
@ -86,6 +100,10 @@ public final class VariableManaCost extends ManaCostImpl implements VariableCost
return this.isColorlessPaid(xPay);
}
public boolean wasAnnounced() {
return this.wasAnnounced;
}
@Override
public VariableManaCost getUnpaid() {
return this;
@ -122,18 +140,22 @@ public final class VariableManaCost extends ManaCostImpl implements VariableCost
return this.xInstancesCount;
}
@Override
public int getMinX() {
return minX;
}
@Override
public void setMinX(int minX) {
this.minX = minX;
}
@Override
public int getMaxX() {
return maxX;
}
@Override
public void setMaxX(int maxX) {
this.maxX = maxX;
}

View file

@ -3,35 +3,58 @@ package mage.abilities.dynamicvalue.common;
import mage.abilities.Ability;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.effects.Effect;
import mage.abilities.hint.Hint;
import mage.abilities.hint.ValueHint;
import mage.filter.FilterCard;
import mage.filter.StaticFilters;
import mage.game.Controllable;
import mage.game.Game;
import mage.players.Player;
import java.util.Optional;
import java.util.Set;
public enum CardsInControllerHandCount implements DynamicValue {
instance;
ANY(StaticFilters.FILTER_CARD_CARDS),
CREATURES(StaticFilters.FILTER_CARD_CREATURES),
LANDS(StaticFilters.FILTER_CARD_LANDS);
private final FilterCard filter;
private final ValueHint hint;
CardsInControllerHandCount(FilterCard filter) {
this.filter = filter;
this.hint = new ValueHint(filter.getMessage() + " in your hand", this);
}
@Override
public int calculate(Game game, Ability sourceAbility, Effect effect) {
if (sourceAbility != null) {
Player controller = game.getPlayer(sourceAbility.getControllerId());
if (controller != null) {
return controller.getHand().size();
}
}
return 0;
return Optional
.ofNullable(sourceAbility)
.map(Controllable::getControllerId)
.map(game::getPlayer)
.map(Player::getHand)
.map(Set::size)
.orElse(0);
}
@Override
public CardsInControllerHandCount copy() {
return CardsInControllerHandCount.instance;
return this;
}
@Override
public String getMessage() {
return "cards in your hand";
return this.filter.getMessage() + " in your hand";
}
@Override
public String toString() {
return "1";
}
public Hint getHint() {
return this.hint;
}
}

View file

@ -1,4 +1,3 @@
package mage.abilities.dynamicvalue.common;
import mage.MageInt;
@ -6,6 +5,9 @@ import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.effects.Effect;
import mage.abilities.hint.Hint;
import mage.abilities.hint.ValueHint;
import mage.filter.FilterPermanent;
import mage.filter.StaticFilters;
import mage.game.Game;
@ -13,16 +15,21 @@ import mage.game.Game;
* @author TheElk801
*/
public enum GreatestToughnessAmongControlledCreaturesValue implements DynamicValue {
instance;
ALL(StaticFilters.FILTER_CONTROLLED_CREATURES),
OTHER(StaticFilters.FILTER_OTHER_CONTROLLED_CREATURES);
private final FilterPermanent filter;
private final Hint hint;
GreatestToughnessAmongControlledCreaturesValue(FilterPermanent filter) {
this.filter = filter;
this.hint = new ValueHint("The greatest toughness among " + filter.getMessage(), this);
}
@Override
public int calculate(Game game, Ability sourceAbility, Effect effect) {
return game
.getBattlefield()
.getActivePermanents(
StaticFilters.FILTER_CONTROLLED_CREATURE,
sourceAbility.getControllerId(), game
)
.getActivePermanents(filter, sourceAbility.getControllerId(), game)
.stream()
.map(MageObject::getToughness)
.mapToInt(MageInt::getValue)
@ -32,12 +39,12 @@ public enum GreatestToughnessAmongControlledCreaturesValue implements DynamicVal
@Override
public GreatestToughnessAmongControlledCreaturesValue copy() {
return GreatestToughnessAmongControlledCreaturesValue.instance;
return this;
}
@Override
public String getMessage() {
return "the greatest toughness among creatures you control";
return "the greatest toughness among " + filter.getMessage();
}
@Override
@ -45,4 +52,7 @@ public enum GreatestToughnessAmongControlledCreaturesValue implements DynamicVal
return "X";
}
public Hint getHint() {
return hint;
}
}

View file

@ -6,14 +6,13 @@ import mage.abilities.ActivatedAbility;
import mage.cards.Card;
import mage.cards.ModalDoubleFacedCard;
import mage.cards.SplitCard;
import mage.cards.CardWithSpellOption;
import mage.constants.*;
import mage.game.Game;
import mage.players.Player;
import java.util.UUID;
import mage.cards.AdventureCard;
/**
* @author BetaSteward_at_googlemail.com
*/
@ -103,9 +102,9 @@ public abstract class AsThoughEffectImpl extends ContinuousEffectImpl implements
if (!rightCard.isLand(game)) {
player.setCastSourceIdWithAlternateMana(rightCard.getId(), null, rightCard.getSpellAbility().getCosts(), identifier);
}
} else if (card instanceof AdventureCard) {
} else if (card instanceof CardWithSpellOption) {
Card creatureCard = card.getMainCard();
Card spellCard = ((AdventureCard) card).getSpellCard();
Card spellCard = ((CardWithSpellOption) card).getSpellCard();
player.setCastSourceIdWithAlternateMana(creatureCard.getId(), null, creatureCard.getSpellAbility().getCosts(), identifier);
player.setCastSourceIdWithAlternateMana(spellCard.getId(), null, spellCard.getSpellAbility().getCosts(), identifier);
}

View file

@ -549,9 +549,9 @@ public class ContinuousEffects implements Serializable {
// rules:
// 708.4. In every zone except the stack, the characteristics of a split card are those of its two halves combined.
idToCheck = ((SplitCardHalf) objectToCheck).getMainCard().getId();
} else if (!type.needPlayCardAbility() && objectToCheck instanceof AdventureCardSpell) {
// adventure spell uses alternative characteristics for spell/stack, all other cases must use main card
idToCheck = ((AdventureCardSpell) objectToCheck).getMainCard().getId();
} else if (!type.needPlayCardAbility() && objectToCheck instanceof CardWithSpellOption) {
// adventure/omen spell uses alternative characteristics for spell/stack, all other cases must use main card
idToCheck = ((CardWithSpellOption) objectToCheck).getMainCard().getId();
} else if (!type.needPlayCardAbility() && objectToCheck instanceof ModalDoubleFacedCardHalf) {
// each mdf side uses own characteristics to check for playing, all other cases must use main card
// rules:
@ -688,6 +688,8 @@ public class ContinuousEffects implements Serializable {
* the battlefield for
* {@link CostModificationEffect cost modification effects} and applies them
* if necessary.
* <p>
* Warning, don't forget to call ability.adjustX before any cost modifications
*
* @param abilityToModify
* @param game
@ -695,6 +697,10 @@ public class ContinuousEffects implements Serializable {
public void costModification(Ability abilityToModify, Game game) {
List<CostModificationEffect> costEffects = getApplicableCostModificationEffects(game);
// add dynamic costs from X and other places
abilityToModify.adjustCostsPrepare(game);
abilityToModify.adjustCostsModify(game, CostModificationType.INCREASE_COST);
for (CostModificationEffect effect : costEffects) {
if (effect.getModificationType() == CostModificationType.INCREASE_COST) {
Set<Ability> abilities = costModificationEffects.getAbility(effect.getId());
@ -706,6 +712,7 @@ public class ContinuousEffects implements Serializable {
}
}
abilityToModify.adjustCostsModify(game, CostModificationType.REDUCE_COST);
for (CostModificationEffect effect : costEffects) {
if (effect.getModificationType() == CostModificationType.REDUCE_COST) {
Set<Ability> abilities = costModificationEffects.getAbility(effect.getId());
@ -717,6 +724,7 @@ public class ContinuousEffects implements Serializable {
}
}
abilityToModify.adjustCostsModify(game, CostModificationType.SET_COST);
for (CostModificationEffect effect : costEffects) {
if (effect.getModificationType() == CostModificationType.SET_COST) {
Set<Ability> abilities = costModificationEffects.getAbility(effect.getId());

View file

@ -1,39 +1,41 @@
package mage.abilities.effects.common;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import mage.abilities.Ability;
import mage.abilities.effects.OneShotEffect;
import mage.choices.Choice;
import mage.choices.ChoiceImpl;
import mage.constants.ModeChoice;
import mage.constants.Outcome;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.util.CardUtil;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author LevelX2
*/
public class ChooseModeEffect extends OneShotEffect {
protected final List<String> modes = new ArrayList<>();
protected final String choiceMessage;
protected final List<ModeChoice> modes = new ArrayList<>();
protected final String message;
public ChooseModeEffect(String choiceMessage, String... modes) {
public ChooseModeEffect(ModeChoice... modes) {
super(Outcome.Neutral);
this.choiceMessage = choiceMessage;
this.modes.addAll(Arrays.asList(modes));
this.staticText = setText();
this.message = makeMessage(this.modes);
this.staticText = "choose " + this.message;
}
protected ChooseModeEffect(final ChooseModeEffect effect) {
super(effect);
this.modes.addAll(effect.modes);
this.choiceMessage = effect.choiceMessage;
this.message = effect.message;
}
@Override
@ -48,34 +50,27 @@ public class ChooseModeEffect extends OneShotEffect {
if (sourcePermanent == null) {
sourcePermanent = game.getPermanentEntering(source.getSourceId());
}
if (controller != null && sourcePermanent != null) {
Choice choice = new ChoiceImpl(true);
choice.setMessage(choiceMessage + CardUtil.getSourceLogName(game, source));
choice.getChoices().addAll(modes);
if (controller.choose(Outcome.Neutral, choice, game)) {
if (!game.isSimulation()) {
game.informPlayers(sourcePermanent.getLogName() + ": " + controller.getLogName() + " has chosen " + choice.getChoice());
}
game.getState().setValue(source.getSourceId() + "_modeChoice", choice.getChoice());
sourcePermanent.addInfo("_modeChoice", "<font color = 'blue'>Chosen mode: " + choice.getChoice() + "</font>", game);
return true;
}
if (controller == null || sourcePermanent == null) {
return false;
}
return false;
Choice choice = new ChoiceImpl(true);
choice.setMessage(message + "? (" + CardUtil.getSourceLogName(game, source) + ')');
choice.getChoices().addAll(modes.stream().map(ModeChoice::toString).collect(Collectors.toList()));
if (!controller.choose(Outcome.Neutral, choice, game)) {
return false;
}
game.informPlayers(sourcePermanent.getLogName() + ": " + controller.getLogName() + " has chosen " + choice.getChoice());
game.getState().setValue(source.getSourceId() + "_modeChoice", choice.getChoice());
sourcePermanent.addInfo("_modeChoice", "<font color = 'blue'>Chosen mode: " + choice.getChoice() + "</font>", game);
return true;
}
private String setText() {
StringBuilder sb = new StringBuilder("choose ");
int count = 0;
for (String choice : modes) {
count++;
sb.append(choice);
if (count + 1 < modes.size()) {
sb.append(", ");
} else if (count < modes.size()) {
sb.append(" or ");
}
}
return sb.toString();
private static String makeMessage(List<ModeChoice> modeChoices) {
return CardUtil.concatWithOr(
modeChoices
.stream()
.map(ModeChoice::toString)
.collect(Collectors.toList())
);
}
}

View file

@ -0,0 +1,74 @@
package mage.abilities.effects.common;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.effects.OneShotEffect;
import mage.constants.Outcome;
import mage.game.ExileZone;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.game.permanent.token.Token;
import mage.util.CardUtil;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.function.Function;
/**
* @author TheElk801
*/
public class CreateXXTokenExiledEffectManaValueEffect extends OneShotEffect {
private final Function<Integer, Token> tokenMaker;
public CreateXXTokenExiledEffectManaValueEffect(Function<Integer, Token> tokenMaker, String description) {
super(Outcome.Benefit);
this.tokenMaker = tokenMaker;
staticText = "the exiled card's owner creates an X/X " + description +
"creature token, where X is the mana value of the exiled card";
}
private CreateXXTokenExiledEffectManaValueEffect(final CreateXXTokenExiledEffectManaValueEffect effect) {
super(effect);
this.tokenMaker = effect.tokenMaker;
}
@Override
public CreateXXTokenExiledEffectManaValueEffect copy() {
return new CreateXXTokenExiledEffectManaValueEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Permanent permanentLeftBattlefield = (Permanent) getValue("permanentLeftBattlefield");
ExileZone exile = game.getExile().getExileZone(
CardUtil.getExileZoneId(game, source.getSourceId(), permanentLeftBattlefield.getZoneChangeCounter(game))
);
if (exile == null || exile.isEmpty()) {
return false;
}
// From ZNR Release Notes:
// https://magic.wizards.com/en/articles/archive/feature/zendikar-rising-release-notes-2020-09-10
// If Skyclave Apparition's first ability exiled more than one card owned by a single player,
// that player creates a token with power and toughness equal to the sum of those cards' converted mana costs.
// If the first ability exiled cards owned by more than one player, each of those players creates a token
// with power and toughness equal to the sum of the converted mana costs of all cards exiled by the first ability.
Set<UUID> owners = new HashSet<>();
int totalCMC = exile
.getCards(game)
.stream()
.filter(Objects::nonNull)
.map(card -> {
owners.add(card.getOwnerId());
return card;
})
.mapToInt(MageObject::getManaValue)
.sum();
for (UUID playerId : owners) {
tokenMaker.apply(totalCMC).putOntoBattlefield(1, game, source, playerId);
}
return true;
}
}

View file

@ -5,7 +5,7 @@ import mage.abilities.MageSingleton;
import mage.abilities.effects.AsThoughEffectImpl;
import mage.abilities.effects.ContinuousEffect;
import mage.abilities.effects.OneShotEffect;
import mage.cards.AdventureCardSpell;
import mage.cards.AdventureSpellCard;
import mage.cards.Card;
import mage.constants.AsThoughEffectType;
import mage.constants.Duration;
@ -51,10 +51,10 @@ public class ExileAdventureSpellEffect extends OneShotEffect implements MageSing
Spell spell = game.getStack().getSpell(source.getId());
if (spell != null) {
Card spellCard = spell.getCard();
if (spellCard instanceof AdventureCardSpell) {
if (spellCard instanceof AdventureSpellCard) {
UUID exileId = adventureExileId(controller.getId(), game);
game.getExile().createZone(exileId, "On an Adventure from " + controller.getName());
AdventureCardSpell adventureSpellCard = (AdventureCardSpell) spellCard;
AdventureSpellCard adventureSpellCard = (AdventureSpellCard) spellCard;
Card parentCard = adventureSpellCard.getParentCard();
if (controller.moveCardsToExile(parentCard, source, game, true, exileId, "On an Adventure from " + controller.getName())) {
ContinuousEffect effect = new AdventureCastFromExileEffect();

View file

@ -63,7 +63,7 @@ public class PermanentsEnterBattlefieldTappedEffect extends ReplacementEffectImp
return staticText;
}
return filter.getMessage()
+ " enter tapped"
+ (duration == Duration.EndOfTurn ? " this turn" : "");
+ " enter" + (filter.getMessage().startsWith("each") ? "s" : "")
+ " tapped" + (duration == Duration.EndOfTurn ? " this turn" : "");
}
}

View file

@ -7,6 +7,8 @@ import mage.counters.Counters;
import mage.game.Game;
import mage.util.CardUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
@ -17,15 +19,17 @@ public class ReturnFromGraveyardToBattlefieldWithCounterTargetEffect extends Ret
private final Counters counters;
private final String counterText;
public ReturnFromGraveyardToBattlefieldWithCounterTargetEffect(Counter counter) {
this(counter, false);
public ReturnFromGraveyardToBattlefieldWithCounterTargetEffect(Counter... counters) {
this(false, counters);
}
public ReturnFromGraveyardToBattlefieldWithCounterTargetEffect(Counter counter, boolean additional) {
public ReturnFromGraveyardToBattlefieldWithCounterTargetEffect(boolean additional, Counter... counters) {
super(false);
this.counters = new Counters();
this.counters.addCounter(counter);
this.counterText = makeText(counter, additional);
for (Counter counter : counters) {
this.counters.addCounter(counter);
}
this.counterText = makeText(additional, counters);
}
protected ReturnFromGraveyardToBattlefieldWithCounterTargetEffect(final ReturnFromGraveyardToBattlefieldWithCounterTargetEffect effect) {
@ -47,22 +51,26 @@ public class ReturnFromGraveyardToBattlefieldWithCounterTargetEffect extends Ret
return super.apply(game, source);
}
private String makeText(Counter counter, boolean additional) {
StringBuilder sb = new StringBuilder(" with ");
if (counter.getCount() == 1) {
if (additional) {
sb.append("an additional ").append(counter.getName());
private static String makeText(boolean additional, Counter... counters) {
List<String> strings = new ArrayList<>();
for (Counter counter : counters) {
StringBuilder sb = new StringBuilder();
if (counter.getCount() == 1) {
if (additional) {
sb.append("an additional ").append(counter.getName());
} else {
sb.append(CardUtil.addArticle(counter.getName()));
}
sb.append(" counter");
} else {
sb.append(CardUtil.addArticle(counter.getName()));
sb.append(CardUtil.numberToText(counter.getCount()));
sb.append(additional ? " additional " : " ");
sb.append(counter.getName());
sb.append(" counters");
}
sb.append(" counter");
} else {
sb.append(CardUtil.numberToText(counter.getCount()));
sb.append(additional ? " additional " : " ");
sb.append(counter.getName());
sb.append(" counters");
strings.add(sb.toString());
}
return sb.toString();
return " with " + CardUtil.concatWithAnd(strings);
}
@Override

View file

@ -0,0 +1,52 @@
package mage.abilities.effects.common.continuous;
import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.ContinuousEffectImpl;
import mage.abilities.effects.Effect;
import mage.constants.*;
import mage.game.Game;
import mage.game.permanent.Permanent;
/**
* @author TheElk801
*/
public class GainAnchorWordAbilitySourceEffect extends ContinuousEffectImpl {
private final Ability ability;
private final ModeChoice modeChoice;
public GainAnchorWordAbilitySourceEffect(Effect effect, ModeChoice modeChoice) {
this(new SimpleStaticAbility(effect), modeChoice);
}
public GainAnchorWordAbilitySourceEffect(Ability ability, ModeChoice modeChoice) {
super(Duration.WhileOnBattlefield, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility);
this.staticText = "&bull " + modeChoice + " &mdash; " + ability.getRule();
this.ability = ability;
this.modeChoice = modeChoice;
this.ability.setRuleVisible(false);
this.generateGainAbilityDependencies(ability, null);
}
private GainAnchorWordAbilitySourceEffect(final GainAnchorWordAbilitySourceEffect effect) {
super(effect);
this.modeChoice = effect.modeChoice;
this.ability = effect.ability;
}
@Override
public boolean apply(Game game, Ability source) {
Permanent permanent = source.getSourcePermanentIfItStillExists(game);
if (permanent == null || !modeChoice.checkMode(game, source)) {
return false;
}
permanent.addAbility(ability, source.getSourceId(), game);
return true;
}
@Override
public GainAnchorWordAbilitySourceEffect copy() {
return new GainAnchorWordAbilitySourceEffect(this);
}
}

View file

@ -6,6 +6,7 @@ import mage.abilities.effects.RestrictionEffect;
import mage.abilities.effects.common.ChooseModeEffect;
import mage.constants.CardType;
import mage.constants.Duration;
import mage.constants.ModeChoice;
import mage.constants.Outcome;
import mage.game.Game;
import mage.game.permanent.Permanent;
@ -19,9 +20,6 @@ import java.util.UUID;
*/
public class PlayerCanOnlyAttackInDirectionRestrictionEffect extends RestrictionEffect {
public static final String ALLOW_ATTACKING_LEFT = "Allow attacking left";
public static final String ALLOW_ATTACKING_RIGHT = "Allow attacking right";
public PlayerCanOnlyAttackInDirectionRestrictionEffect(Duration duration, String directionText) {
super(duration, Outcome.Neutral);
staticText = duration + (duration.toString().isEmpty() ? "" : ", ")
@ -39,10 +37,7 @@ public class PlayerCanOnlyAttackInDirectionRestrictionEffect extends Restriction
}
public static Effect choiceEffect() {
return new ChooseModeEffect(
"Choose a direction to allow attacking in.",
ALLOW_ATTACKING_LEFT, ALLOW_ATTACKING_RIGHT
).setText("choose left or right");
return new ChooseModeEffect(ModeChoice.LEFT, ModeChoice.RIGHT);
}
@Override
@ -55,10 +50,13 @@ public class PlayerCanOnlyAttackInDirectionRestrictionEffect extends Restriction
if (defenderId == null) {
return true;
}
String allowedDirection = (String) game.getState().getValue(source.getSourceId() + "_modeChoice");
if (allowedDirection == null) {
return true; // If no choice was made, the ability has no effect.
boolean left;
if (ModeChoice.LEFT.checkMode(game, source)) {
left = true;
} else if (ModeChoice.RIGHT.checkMode(game, source)) {
left = false;
} else {
return false; // If no choice was made, the ability has no effect.
}
Player playerAttacking = game.getPlayer(attacker.getControllerId());
@ -83,18 +81,7 @@ public class PlayerCanOnlyAttackInDirectionRestrictionEffect extends Restriction
}
PlayerList playerList = game.getState().getPlayerList(playerAttacking.getId());
if (allowedDirection.equals(ALLOW_ATTACKING_LEFT)
&& !playerList.getNext().equals(playerDefending.getId())) {
// the defender is not the player to the left
return false;
}
if (allowedDirection.equals(ALLOW_ATTACKING_RIGHT)
&& !playerList.getPrevious().equals(playerDefending.getId())) {
// the defender is not the player to the right
return false;
}
return true;
return (!left || playerList.getNext().equals(playerDefending.getId()))
&& (left || playerList.getPrevious().equals(playerDefending.getId()));
}
}

View file

@ -62,6 +62,9 @@ public class LookTargetHandChooseDiscardEffect extends OneShotEffect {
}
TargetCard target = new TargetCardInHand(upTo ? 0 : num, num, filter);
if (controller.choose(Outcome.Discard, player.getHand(), target, source, game)) {
// TODO: must fizzle discard effect on not full choice
// - tests: affected (allow to choose and discard 1 instead 2)
// - real game: need to check
player.discard(new CardsImpl(target.getTargets()), false, source, game);
}
return true;

View file

@ -0,0 +1,80 @@
package mage.abilities.effects.common.replacement;
import mage.abilities.Ability;
import mage.abilities.effects.ReplacementEffectImpl;
import mage.constants.Duration;
import mage.constants.Outcome;
import mage.game.Controllable;
import mage.game.Game;
import mage.game.events.DefenderAttackedEvent;
import mage.game.events.GameEvent;
import mage.game.events.NumberOfTriggersEvent;
import mage.game.permanent.Permanent;
/**
* @author TheElk801
*/
public class AdditionalTriggersAttackingReplacementEffect extends ReplacementEffectImpl {
private final boolean onlyControlled;
public AdditionalTriggersAttackingReplacementEffect(boolean onlyControlled) {
super(Duration.WhileOnBattlefield, Outcome.Benefit);
this.onlyControlled = onlyControlled;
staticText = "if a creature " + (onlyControlled ? "you control " : "") + "attacking causes a triggered ability " +
"of a permanent you control to trigger, that ability triggers an additional time";
}
private AdditionalTriggersAttackingReplacementEffect(final AdditionalTriggersAttackingReplacementEffect effect) {
super(effect);
this.onlyControlled = effect.onlyControlled;
}
@Override
public AdditionalTriggersAttackingReplacementEffect copy() {
return new AdditionalTriggersAttackingReplacementEffect(this);
}
@Override
public boolean checksEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.NUMBER_OF_TRIGGERS;
}
@Override
public boolean applies(GameEvent event, Ability source, Game game) {
NumberOfTriggersEvent numberOfTriggersEvent = (NumberOfTriggersEvent) event;
Permanent sourcePermanent = game.getPermanent(numberOfTriggersEvent.getSourceId());
if (sourcePermanent == null || !sourcePermanent.isControlledBy(source.getControllerId())) {
return false;
}
GameEvent sourceEvent = numberOfTriggersEvent.getSourceEvent();
if (sourceEvent == null) {
return false;
}
switch (sourceEvent.getType()) {
case ATTACKER_DECLARED:
return !onlyControlled || source.isControlledBy(sourceEvent.getPlayerId());
case DECLARED_ATTACKERS:
return !onlyControlled || game
.getCombat()
.getAttackers()
.stream()
.map(game::getControllerId)
.anyMatch(source::isControlledBy);
case DEFENDER_ATTACKED:
return !onlyControlled || ((DefenderAttackedEvent) sourceEvent)
.getAttackers(game)
.stream()
.map(Controllable::getControllerId)
.anyMatch(source::isControlledBy);
}
return false;
}
@Override
public boolean replaceEvent(GameEvent event, Ability source, Game game) {
event.setAmount(event.getAmount() + 1);
return false;
}
}

View file

@ -1,6 +1,8 @@
package mage.abilities.effects.keyword;
import mage.abilities.Ability;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.dynamicvalue.common.StaticValue;
import mage.abilities.effects.OneShotEffect;
import mage.constants.Outcome;
import mage.counters.CounterType;
@ -15,13 +17,17 @@ import mage.util.CardUtil;
*/
public class EndureSourceEffect extends OneShotEffect {
private final int amount;
private final DynamicValue amount;
public EndureSourceEffect(int amount) {
this(amount, "it");
}
public EndureSourceEffect(int amount, String selfText) {
this(StaticValue.get(amount), selfText);
}
public EndureSourceEffect(DynamicValue amount, String selfText) {
super(Outcome.Benefit);
staticText = selfText + " endures " + amount;
this.amount = amount;
@ -39,12 +45,23 @@ public class EndureSourceEffect extends OneShotEffect {
@Override
public boolean apply(Game game, Ability source) {
Player player = game.getPlayer(source.getControllerId());
if (player == null) {
return doEndure(
source.getSourcePermanentOrLKI(game),
amount.calculate(game, source, this),
game, source
);
}
public static boolean doEndure(Permanent permanent, int amount, Game game, Ability source) {
if (permanent == null || amount < 1) {
return false;
}
Permanent permanent = source.getSourcePermanentIfItStillExists(game);
if (permanent != null && player.chooseUse(
Player controller = game.getPlayer(permanent.getControllerId());
if (controller == null) {
return false;
}
if (permanent.getZoneChangeCounter(game) == game.getState().getZoneChangeCounter(permanent.getId())
&& controller.chooseUse(
Outcome.BoostCreature, "Put " + CardUtil.numberToText(amount, "a") + " +1/+1 counter" +
(amount > 1 ? "s" : "") + " on " + permanent.getName() + " or create " +
CardUtil.addArticle("" + amount) + ' ' + amount + '/' + amount + " Spirit token?",

View file

@ -0,0 +1,197 @@
package mage.abilities.keyword;
import mage.abilities.Ability;
import mage.abilities.SpellAbility;
import mage.abilities.costs.Cost;
import mage.abilities.costs.Costs;
import mage.abilities.effects.ContinuousEffect;
import mage.abilities.effects.ReplacementEffectImpl;
import mage.cards.Card;
import mage.cards.ModalDoubleFacedCard;
import mage.cards.SplitCard;
import mage.constants.*;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.events.ZoneChangeEvent;
import mage.players.Player;
import mage.target.targetpointer.FixedTarget;
import mage.util.CardUtil;
import java.util.UUID;
/**
* Base class for Flashback and Harmonize and any future ability which works similarly
*
* @author TheElk801
*/
public abstract class CastFromGraveyardAbility extends SpellAbility {
protected String abilityName;
private SpellAbility spellAbilityToResolve;
protected CastFromGraveyardAbility(Card card, Cost cost, SpellAbilityCastMode spellAbilityCastMode) {
super(null, "", Zone.GRAVEYARD, SpellAbilityType.BASE_ALTERNATE, spellAbilityCastMode);
this.setAdditionalCostsRuleVisible(false);
this.name = spellAbilityCastMode + " " + cost.getText();
this.addCost(cost);
this.timing = card.isSorcery() ? TimingRule.SORCERY : TimingRule.INSTANT;
}
protected CastFromGraveyardAbility(final CastFromGraveyardAbility ability) {
super(ability);
this.abilityName = ability.abilityName;
this.spellAbilityToResolve = ability.spellAbilityToResolve;
}
@Override
public ActivationStatus canActivate(UUID playerId, Game game) {
// flashback ability dynamicly added to all card's parts (split cards)
if (!super.canActivate(playerId, game).canActivate()) {
return ActivationStatus.getFalse();
}
Card card = game.getCard(getSourceId());
if (card == null) {
return ActivationStatus.getFalse();
}
// Card must be in the graveyard zone
if (game.getState().getZone(card.getId()) != Zone.GRAVEYARD) {
return ActivationStatus.getFalse();
}
// Cards with no Mana Costs cant't be flashbacked (e.g. Ancestral Vision)
if (card.getManaCost().isEmpty()) {
return ActivationStatus.getFalse();
}
// CastFromGraveyard can never cast a split card by Fuse, because Fuse only works from hand
// https://tappedout.net/mtg-questions/snapcaster-mage-and-flashback-on-a-fuse-card-one-or-both-halves-legal-targets/
if (card instanceof SplitCard) {
if (((SplitCard) card).getLeftHalfCard().getName().equals(abilityName)) {
return ((SplitCard) card).getLeftHalfCard().getSpellAbility().canActivate(playerId, game);
} else if (((SplitCard) card).getRightHalfCard().getName().equals(abilityName)) {
return ((SplitCard) card).getRightHalfCard().getSpellAbility().canActivate(playerId, game);
}
} else if (card instanceof ModalDoubleFacedCard) {
if (((ModalDoubleFacedCard) card).getLeftHalfCard().getName().equals(abilityName)) {
return ((ModalDoubleFacedCard) card).getLeftHalfCard().getSpellAbility().canActivate(playerId, game);
} else if (((ModalDoubleFacedCard) card).getRightHalfCard().getName().equals(abilityName)) {
return ((ModalDoubleFacedCard) card).getRightHalfCard().getSpellAbility().canActivate(playerId, game);
}
}
return card.getSpellAbility().canActivate(playerId, game);
}
@Override
public SpellAbility getSpellAbilityToResolve(Game game) {
Card card = game.getCard(getSourceId());
if (card == null || spellAbilityToResolve != null) {
return spellAbilityToResolve;
}
SpellAbility spellAbilityCopy;
if (card instanceof SplitCard) {
if (((SplitCard) card).getLeftHalfCard().getName().equals(abilityName)) {
spellAbilityCopy = ((SplitCard) card).getLeftHalfCard().getSpellAbility().copy();
} else if (((SplitCard) card).getRightHalfCard().getName().equals(abilityName)) {
spellAbilityCopy = ((SplitCard) card).getRightHalfCard().getSpellAbility().copy();
} else {
spellAbilityCopy = null;
}
} else if (card instanceof ModalDoubleFacedCard) {
if (((ModalDoubleFacedCard) card).getLeftHalfCard().getName().equals(abilityName)) {
spellAbilityCopy = ((ModalDoubleFacedCard) card).getLeftHalfCard().getSpellAbility().copy();
} else if (((ModalDoubleFacedCard) card).getRightHalfCard().getName().equals(abilityName)) {
spellAbilityCopy = ((ModalDoubleFacedCard) card).getRightHalfCard().getSpellAbility().copy();
} else {
spellAbilityCopy = null;
}
} else {
spellAbilityCopy = card.getSpellAbility().copy();
}
if (spellAbilityCopy == null) {
return null;
}
spellAbilityCopy.setId(this.getId());
spellAbilityCopy.clearManaCosts();
spellAbilityCopy.clearManaCostsToPay();
spellAbilityCopy.addCost(this.getCosts().copy());
spellAbilityCopy.addCost(this.getManaCosts().copy());
spellAbilityCopy.setSpellAbilityCastMode(this.getSpellAbilityCastMode());
spellAbilityToResolve = spellAbilityCopy;
ContinuousEffect effect = new CastFromGraveyardReplacementEffect();
effect.setTargetPointer(new FixedTarget(getSourceId(), game.getState().getZoneChangeCounter(getSourceId())));
game.addEffect(effect, this);
return spellAbilityToResolve;
}
@Override
public Costs<Cost> getCosts() {
if (spellAbilityToResolve == null) {
return super.getCosts();
}
return spellAbilityToResolve.getCosts();
}
@Override
public String getRule(boolean all) {
return this.getRule();
}
/**
* Used for split card in PlayerImpl method:
* getOtherUseableActivatedAbilities
*
* @param abilityName
*/
public CastFromGraveyardAbility setAbilityName(String abilityName) {
this.abilityName = abilityName;
return this;
}
}
class CastFromGraveyardReplacementEffect extends ReplacementEffectImpl {
public CastFromGraveyardReplacementEffect() {
super(Duration.OneUse, Outcome.Exile);
}
protected CastFromGraveyardReplacementEffect(final CastFromGraveyardReplacementEffect effect) {
super(effect);
}
@Override
public CastFromGraveyardReplacementEffect copy() {
return new CastFromGraveyardReplacementEffect(this);
}
@Override
public boolean replaceEvent(GameEvent event, Ability source, Game game) {
Player controller = game.getPlayer(source.getControllerId());
if (controller == null) {
return false;
}
Card card = game.getCard(event.getTargetId());
if (card == null) {
return false;
}
discard();
return controller.moveCards(
card, Zone.EXILED, source, game, false,
false, false, event.getAppliedEffects()
);
}
@Override
public boolean checksEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.ZONE_CHANGE;
}
@Override
public boolean applies(GameEvent event, Ability source, Game game) {
UUID cardId = CardUtil.getMainCardId(game, source.getSourceId()); // for split cards
if (!cardId.equals(event.getTargetId())
|| ((ZoneChangeEvent) event).getFromZone() != Zone.STACK
|| ((ZoneChangeEvent) event).getToZone() == Zone.EXILED) {
return false;
}
int zcc = game.getState().getZoneChangeCounter(cardId);
return ((FixedTarget) getTargetPointer()).getZoneChangeCounter() + 1 == zcc;
}
}

View file

@ -1,23 +1,8 @@
package mage.abilities.keyword;
import mage.abilities.Ability;
import mage.abilities.SpellAbility;
import mage.abilities.costs.Cost;
import mage.abilities.costs.Costs;
import mage.abilities.effects.ContinuousEffect;
import mage.abilities.effects.ReplacementEffectImpl;
import mage.cards.Card;
import mage.cards.ModalDoubleFacedCard;
import mage.cards.SplitCard;
import mage.constants.*;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.events.ZoneChangeEvent;
import mage.players.Player;
import mage.target.targetpointer.FixedTarget;
import mage.util.CardUtil;
import java.util.UUID;
import mage.constants.SpellAbilityCastMode;
/**
* 702.32. Flashback
@ -33,106 +18,14 @@ import java.util.UUID;
*
* @author nantuko
*/
public class FlashbackAbility extends SpellAbility {
private String abilityName;
private SpellAbility spellAbilityToResolve;
public class FlashbackAbility extends CastFromGraveyardAbility {
public FlashbackAbility(Card card, Cost cost) {
super(null, "", Zone.GRAVEYARD, SpellAbilityType.BASE_ALTERNATE, SpellAbilityCastMode.FLASHBACK);
this.setAdditionalCostsRuleVisible(false);
this.name = "Flashback " + cost.getText();
this.addCost(cost);
this.timing = card.isSorcery() ? TimingRule.SORCERY : TimingRule.INSTANT;
super(card, cost, SpellAbilityCastMode.FLASHBACK);
}
protected FlashbackAbility(final FlashbackAbility ability) {
super(ability);
this.spellAbilityType = ability.spellAbilityType;
this.abilityName = ability.abilityName;
this.spellAbilityToResolve = ability.spellAbilityToResolve;
}
@Override
public ActivationStatus canActivate(UUID playerId, Game game) {
// flashback ability dynamicly added to all card's parts (split cards)
if (super.canActivate(playerId, game).canActivate()) {
Card card = game.getCard(getSourceId());
if (card != null) {
// Card must be in the graveyard zone
if (game.getState().getZone(card.getId()) != Zone.GRAVEYARD) {
return ActivationStatus.getFalse();
}
// Cards with no Mana Costs cant't be flashbacked (e.g. Ancestral Vision)
if (card.getManaCost().isEmpty()) {
return ActivationStatus.getFalse();
}
// Flashback can never cast a split card by Fuse, because Fuse only works from hand
// https://tappedout.net/mtg-questions/snapcaster-mage-and-flashback-on-a-fuse-card-one-or-both-halves-legal-targets/
if (card instanceof SplitCard) {
if (((SplitCard) card).getLeftHalfCard().getName().equals(abilityName)) {
return ((SplitCard) card).getLeftHalfCard().getSpellAbility().canActivate(playerId, game);
} else if (((SplitCard) card).getRightHalfCard().getName().equals(abilityName)) {
return ((SplitCard) card).getRightHalfCard().getSpellAbility().canActivate(playerId, game);
}
} else if (card instanceof ModalDoubleFacedCard) {
if (((ModalDoubleFacedCard) card).getLeftHalfCard().getName().equals(abilityName)) {
return ((ModalDoubleFacedCard) card).getLeftHalfCard().getSpellAbility().canActivate(playerId, game);
} else if (((ModalDoubleFacedCard) card).getRightHalfCard().getName().equals(abilityName)) {
return ((ModalDoubleFacedCard) card).getRightHalfCard().getSpellAbility().canActivate(playerId, game);
}
}
return card.getSpellAbility().canActivate(playerId, game);
}
}
return ActivationStatus.getFalse();
}
@Override
public SpellAbility getSpellAbilityToResolve(Game game) {
Card card = game.getCard(getSourceId());
if (card != null) {
if (spellAbilityToResolve == null) {
SpellAbility spellAbilityCopy = null;
if (card instanceof SplitCard) {
if (((SplitCard) card).getLeftHalfCard().getName().equals(abilityName)) {
spellAbilityCopy = ((SplitCard) card).getLeftHalfCard().getSpellAbility().copy();
} else if (((SplitCard) card).getRightHalfCard().getName().equals(abilityName)) {
spellAbilityCopy = ((SplitCard) card).getRightHalfCard().getSpellAbility().copy();
}
} else if (card instanceof ModalDoubleFacedCard) {
if (((ModalDoubleFacedCard) card).getLeftHalfCard().getName().equals(abilityName)) {
spellAbilityCopy = ((ModalDoubleFacedCard) card).getLeftHalfCard().getSpellAbility().copy();
} else if (((ModalDoubleFacedCard) card).getRightHalfCard().getName().equals(abilityName)) {
spellAbilityCopy = ((ModalDoubleFacedCard) card).getRightHalfCard().getSpellAbility().copy();
}
} else {
spellAbilityCopy = card.getSpellAbility().copy();
}
if (spellAbilityCopy == null) {
return null;
}
spellAbilityCopy.setId(this.getId());
spellAbilityCopy.clearManaCosts();
spellAbilityCopy.clearManaCostsToPay();
spellAbilityCopy.addCost(this.getCosts().copy());
spellAbilityCopy.addCost(this.getManaCosts().copy());
spellAbilityCopy.setSpellAbilityCastMode(this.getSpellAbilityCastMode());
spellAbilityToResolve = spellAbilityCopy;
ContinuousEffect effect = new FlashbackReplacementEffect();
effect.setTargetPointer(new FixedTarget(getSourceId(), game.getState().getZoneChangeCounter(getSourceId())));
game.addEffect(effect, this);
}
}
return spellAbilityToResolve;
}
@Override
public Costs<Cost> getCosts() {
if (spellAbilityToResolve == null) {
return super.getCosts();
}
return spellAbilityToResolve.getCosts();
}
@Override
@ -140,11 +33,6 @@ public class FlashbackAbility extends SpellAbility {
return new FlashbackAbility(this);
}
@Override
public String getRule(boolean all) {
return this.getRule();
}
@Override
public String getRule() {
StringBuilder sbRule = new StringBuilder("Flashback");
@ -170,66 +58,4 @@ public class FlashbackAbility extends SpellAbility {
sbRule.append(" <i>(You may cast this card from your graveyard for its flashback cost. Then exile it.)</i>");
return sbRule.toString();
}
/**
* Used for split card in PlayerImpl method:
* getOtherUseableActivatedAbilities
*
* @param abilityName
*/
public FlashbackAbility setAbilityName(String abilityName) {
this.abilityName = abilityName;
return this;
}
}
class FlashbackReplacementEffect extends ReplacementEffectImpl {
public FlashbackReplacementEffect() {
super(Duration.OneUse, Outcome.Exile);
staticText = "(If the flashback cost was paid, exile this card instead of putting it anywhere else any time it would leave the stack)";
}
protected FlashbackReplacementEffect(final FlashbackReplacementEffect effect) {
super(effect);
}
@Override
public FlashbackReplacementEffect copy() {
return new FlashbackReplacementEffect(this);
}
@Override
public boolean replaceEvent(GameEvent event, Ability source, Game game) {
Player controller = game.getPlayer(source.getControllerId());
if (controller != null) {
Card card = game.getCard(event.getTargetId());
if (card != null) {
discard();
return controller.moveCards(
card, Zone.EXILED, source, game, false, false, false, event.getAppliedEffects());
}
}
return false;
}
@Override
public boolean checksEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.ZONE_CHANGE;
}
@Override
public boolean applies(GameEvent event, Ability source, Game game) {
UUID cardId = CardUtil.getMainCardId(game, source.getSourceId()); // for split cards
if (cardId.equals(event.getTargetId())
&& ((ZoneChangeEvent) event).getFromZone() == Zone.STACK
&& ((ZoneChangeEvent) event).getToZone() != Zone.EXILED) {
int zcc = game.getState().getZoneChangeCounter(cardId);
return ((FixedTarget) getTargetPointer()).getZoneChangeCounter() + 1 == zcc;
}
return false;
}
}

View file

@ -257,7 +257,7 @@ public class ForetellAbility extends SpecialAction {
game.getState().addOtherAbility(rightHalfCard, ability);
}
}
} else if (card instanceof AdventureCard) {
} else if (card instanceof CardWithSpellOption) {
if (foretellCost != null) {
Card creatureCard = card.getMainCard();
ForetellCostAbility ability = new ForetellCostAbility(foretellCost);
@ -268,7 +268,7 @@ public class ForetellAbility extends SpecialAction {
game.getState().addOtherAbility(creatureCard, ability);
}
if (foretellSplitCost != null) {
Card spellCard = ((AdventureCard) card).getSpellCard();
Card spellCard = ((CardWithSpellOption) card).getSpellCard();
ForetellCostAbility ability = new ForetellCostAbility(foretellSplitCost);
ability.setSourceId(spellCard.getId());
ability.setControllerId(source.getControllerId());
@ -360,11 +360,11 @@ public class ForetellAbility extends SpecialAction {
} else if (((ModalDoubleFacedCard) card).getRightHalfCard().getName().equals(abilityName)) {
return ((ModalDoubleFacedCard) card).getRightHalfCard().getSpellAbility().canActivate(playerId, game);
}
} else if (card instanceof AdventureCard) {
} else if (card instanceof CardWithSpellOption) {
if (card.getMainCard().getName().equals(abilityName)) {
return card.getMainCard().getSpellAbility().canActivate(playerId, game);
} else if (((AdventureCard) card).getSpellCard().getName().equals(abilityName)) {
return ((AdventureCard) card).getSpellCard().getSpellAbility().canActivate(playerId, game);
} else if (((CardWithSpellOption) card).getSpellCard().getName().equals(abilityName)) {
return ((CardWithSpellOption) card).getSpellCard().getSpellAbility().canActivate(playerId, game);
}
}
return card.getSpellAbility().canActivate(playerId, game);
@ -391,11 +391,11 @@ public class ForetellAbility extends SpecialAction {
} else if (((ModalDoubleFacedCard) card).getRightHalfCard().getName().equals(abilityName)) {
spellAbilityCopy = ((ModalDoubleFacedCard) card).getRightHalfCard().getSpellAbility().copy();
}
} else if (card instanceof AdventureCard) {
} else if (card instanceof CardWithSpellOption) {
if (card.getMainCard().getName().equals(abilityName)) {
spellAbilityCopy = card.getMainCard().getSpellAbility().copy();
} else if (((AdventureCard) card).getSpellCard().getName().equals(abilityName)) {
spellAbilityCopy = ((AdventureCard) card).getSpellCard().getSpellAbility().copy();
} else if (((CardWithSpellOption) card).getSpellCard().getName().equals(abilityName)) {
spellAbilityCopy = ((CardWithSpellOption) card).getSpellCard().getSpellAbility().copy();
}
} else {
spellAbilityCopy = card.getSpellAbility().copy();

View file

@ -1,19 +1,43 @@
package mage.abilities.keyword;
import mage.MageInt;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.SpellAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.costs.Cost;
import mage.abilities.costs.VariableCostImpl;
import mage.abilities.costs.VariableCostType;
import mage.abilities.costs.common.TapTargetCost;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.common.cost.CostModificationEffectImpl;
import mage.cards.Card;
import mage.constants.Zone;
import mage.constants.*;
import mage.filter.StaticFilters;
import mage.filter.common.FilterControlledPermanent;
import mage.filter.predicate.permanent.PermanentIdPredicate;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.TargetPermanent;
import mage.target.common.TargetControlledPermanent;
import mage.util.CardUtil;
import java.util.Objects;
import java.util.UUID;
/**
* TODO: Implement this
*
* @author TheElk801
*/
public class HarmonizeAbility extends SpellAbility {
public class HarmonizeAbility extends CastFromGraveyardAbility {
private String abilityName;
private SpellAbility spellAbilityToResolve;
public HarmonizeAbility(Card card, String manaString) {
super(new ManaCostsImpl<>(manaString), card.getName(), Zone.GRAVEYARD);
super(card, new ManaCostsImpl<>(manaString), SpellAbilityCastMode.HARMONIZE);
this.addCost(new HarmonizeCost());
this.addSubAbility(new SimpleStaticAbility(Zone.ALL, new HarmonizeCostReductionEffect()).setRuleVisible(false));
}
private HarmonizeAbility(final HarmonizeAbility ability) {
@ -24,4 +48,132 @@ public class HarmonizeAbility extends SpellAbility {
public HarmonizeAbility copy() {
return new HarmonizeAbility(this);
}
@Override
public String getRule() {
return name + " <i>(You may cast this card from your graveyard for its harmonize cost. " +
"You may tap a creature you control to reduce that cost by {X}, " +
"where X is its power. Then exile this spell.)</i>";
}
}
class HarmonizeCostReductionEffect extends CostModificationEffectImpl {
HarmonizeCostReductionEffect() {
super(Duration.WhileOnStack, Outcome.Benefit, CostModificationType.REDUCE_COST);
}
private HarmonizeCostReductionEffect(final HarmonizeCostReductionEffect effect) {
super(effect);
}
@Override
public boolean apply(Game game, Ability source, Ability abilityToModify) {
SpellAbility spellAbility = (SpellAbility) abilityToModify;
int power;
if (game.inCheckPlayableState()) {
power = game
.getBattlefield()
.getActivePermanents(
StaticFilters.FILTER_CONTROLLED_UNTAPPED_CREATURE,
source.getControllerId(), source, game
).stream()
.map(MageObject::getPower)
.mapToInt(MageInt::getValue)
.max()
.orElse(0);
} else {
power = CardUtil
.castStream(spellAbility.getCosts().stream(), HarmonizeCost.class)
.map(HarmonizeCost::getChosenCreature)
.map(game::getPermanent)
.filter(Objects::nonNull)
.map(MageObject::getPower)
.mapToInt(MageInt::getValue)
.map(x -> Math.max(x, 0))
.sum();
}
if (power > 0) {
CardUtil.adjustCost(spellAbility, power);
}
return true;
}
@Override
public boolean applies(Ability abilityToModify, Ability source, Game game) {
return abilityToModify instanceof SpellAbility
&& abilityToModify.getSourceId().equals(source.getSourceId());
}
@Override
public HarmonizeCostReductionEffect copy() {
return new HarmonizeCostReductionEffect(this);
}
}
class HarmonizeCost extends VariableCostImpl {
private UUID chosenCreature = null;
HarmonizeCost() {
super(VariableCostType.ADDITIONAL, "", "");
}
private HarmonizeCost(final HarmonizeCost cost) {
super(cost);
this.chosenCreature = cost.chosenCreature;
}
@Override
public HarmonizeCost copy() {
return new HarmonizeCost(this);
}
@Override
public void clearPaid() {
super.clearPaid();
chosenCreature = null;
}
@Override
public int getMaxValue(Ability source, Game game) {
return game.getBattlefield().contains(StaticFilters.FILTER_CONTROLLED_UNTAPPED_CREATURE, source, game, 1) ? 1 : 0;
}
@Override
public int announceXValue(Ability source, Game game) {
Player player = game.getPlayer(source.getControllerId());
if (player == null || !game.getBattlefield().contains(
StaticFilters.FILTER_CONTROLLED_UNTAPPED_CREATURE, source, game, 1
) || !player.chooseUse(
Outcome.Benefit, "Tap an untapped creature you control for harmonize?", source, game
)) {
return 0;
}
TargetPermanent target = new TargetPermanent(StaticFilters.FILTER_CONTROLLED_UNTAPPED_CREATURE);
target.withNotTarget(true);
target.withChooseHint("for harmonize");
player.choose(Outcome.PlayForFree, target, source, game);
Permanent permanent = game.getPermanent(target.getFirstTarget());
if (permanent == null) {
return 0;
}
chosenCreature = permanent.getId();
return 1;
}
private FilterControlledPermanent makeFilter() {
FilterControlledPermanent filter = new FilterControlledPermanent("tap the chosen creature");
filter.add(new PermanentIdPredicate(chosenCreature));
return filter;
}
@Override
public Cost getFixedCostsFromAnnouncedValue(int xValue) {
return new TapTargetCost(new TargetControlledPermanent(xValue, xValue, makeFilter(), true));
}
public UUID getChosenCreature() {
return chosenCreature;
}
}

View file

@ -268,11 +268,11 @@ class PlotSpellAbility extends SpellAbility {
} else if (((CardWithHalves) mainCard).getRightHalfCard().getName().equals(faceCardName)) {
return ((CardWithHalves) mainCard).getRightHalfCard().getSpellAbility().canActivate(playerId, game);
}
} else if (card instanceof AdventureCard) {
} else if (card instanceof CardWithSpellOption) {
if (card.getMainCard().getName().equals(faceCardName)) {
return card.getMainCard().getSpellAbility().canActivate(playerId, game);
} else if (((AdventureCard) card).getSpellCard().getName().equals(faceCardName)) {
return ((AdventureCard) card).getSpellCard().getSpellAbility().canActivate(playerId, game);
} else if (((CardWithSpellOption) card).getSpellCard().getName().equals(faceCardName)) {
return ((CardWithSpellOption) card).getSpellCard().getSpellAbility().canActivate(playerId, game);
}
}
return card.getSpellAbility().canActivate(playerId, game);
@ -294,11 +294,11 @@ class PlotSpellAbility extends SpellAbility {
} else if (((CardWithHalves) card).getRightHalfCard().getName().equals(faceCardName)) {
spellAbilityCopy = ((CardWithHalves) card).getRightHalfCard().getSpellAbility().copy();
}
} else if (card instanceof AdventureCard) {
} else if (card instanceof CardWithSpellOption) {
if (card.getMainCard().getName().equals(faceCardName)) {
spellAbilityCopy = card.getMainCard().getSpellAbility().copy();
} else if (((AdventureCard) card).getSpellCard().getName().equals(faceCardName)) {
spellAbilityCopy = ((AdventureCard) card).getSpellCard().getSpellAbility().copy();
} else if (((CardWithSpellOption) card).getSpellCard().getName().equals(faceCardName)) {
spellAbilityCopy = ((CardWithSpellOption) card).getSpellCard().getSpellAbility().copy();
}
} else {
spellAbilityCopy = card.getSpellAbility().copy();

View file

@ -1,6 +1,5 @@
package mage.abilities.keyword;
import mage.ApprovingObject;
import mage.MageIdentifier;
import mage.abilities.Ability;
import mage.abilities.SpecialAction;
@ -17,8 +16,11 @@ import mage.abilities.effects.ContinuousEffectImpl;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.counter.RemoveCounterSourceEffect;
import mage.cards.Card;
import mage.cards.CardsImpl;
import mage.cards.ModalDoubleFacedCard;
import mage.constants.*;
import mage.counters.CounterType;
import mage.filter.StaticFilters;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
@ -26,8 +28,6 @@ import mage.players.Player;
import mage.target.targetpointer.FixedTarget;
import mage.util.CardUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
@ -112,7 +112,6 @@ import java.util.UUID;
public class SuspendAbility extends SpecialAction {
private final String ruleText;
private boolean gainedTemporary;
/**
* Gives the card the SuspendAbility
@ -132,6 +131,8 @@ public class SuspendAbility extends SpecialAction {
this.addEffect(new SuspendExileEffect(suspend));
this.usesStack = false;
if (suspend == Integer.MAX_VALUE) {
// example: Suspend X-{X}{W}{W}. X can't be 0.
// TODO: replace by costAdjuster for shared logic
VariableManaCost xCosts = new VariableManaCost(VariableCostType.ALTERNATIVE);
xCosts.setMinX(1);
this.addCost(xCosts);
@ -175,6 +176,11 @@ public class SuspendAbility extends SpecialAction {
* or added by Jhoira of the Ghitu
*/
public static void addSuspendTemporaryToCard(Card card, Ability source, Game game) {
if (card instanceof ModalDoubleFacedCard) {
// Need to ensure the suspend ability gets put on the left side card
// since counters get added to this card.
card = ((ModalDoubleFacedCard) card).getLeftHalfCard();
}
SuspendAbility ability = new SuspendAbility(0, null, card, false);
ability.setSourceId(card.getId());
ability.setControllerId(card.getOwnerId());
@ -204,7 +210,6 @@ public class SuspendAbility extends SpecialAction {
private SuspendAbility(final SuspendAbility ability) {
super(ability);
this.ruleText = ability.ruleText;
this.gainedTemporary = ability.gainedTemporary;
}
@Override
@ -230,10 +235,6 @@ public class SuspendAbility extends SpecialAction {
return ruleText;
}
public boolean isGainedTemporary() {
return gainedTemporary;
}
@Override
public SuspendAbility copy() {
return new SuspendAbility(this);
@ -343,40 +344,16 @@ class SuspendPlayCardEffect extends OneShotEffect {
if (player == null || card == null) {
return false;
}
if (!player.chooseUse(Outcome.Benefit, "Play " + card.getLogName() + " without paying its mana cost?", source, game)) {
// ensure we're getting the main card when passing to CardUtil to check all parts of card
// MDFC points to left half card
card = card.getMainCard();
// cast/play the card for free
if (!CardUtil.castSpellWithAttributesForFree(player, source, game, new CardsImpl(card),
StaticFilters.FILTER_CARD, null, true)) {
return true;
}
// remove temporary suspend ability (used e.g. for Epochrasite)
// TODO: isGainedTemporary is not set or use in other places, so it can be deleted?!
List<Ability> abilitiesToRemove = new ArrayList<>();
for (Ability ability : card.getAbilities(game)) {
if (ability instanceof SuspendAbility && (((SuspendAbility) ability).isGainedTemporary())) {
abilitiesToRemove.add(ability);
}
}
if (!abilitiesToRemove.isEmpty()) {
for (Ability ability : card.getAbilities(game)) {
if (ability instanceof SuspendBeginningOfUpkeepInterveningIfTriggeredAbility
|| ability instanceof SuspendPlayCardAbility) {
abilitiesToRemove.add(ability);
}
}
// remove the abilities from the card
// TODO: will not work with Adventure Cards and another auto-generated abilities list
// TODO: is it work after blink or return to hand?
/*
bug example:
Epochrasite bug: It comes out of suspend, is cast and enters the battlefield. THEN if it's returned to
its owner's hand from battlefield, the bounced Epochrasite can't be cast for the rest of the game.
*/
card.getAbilities().removeAll(abilitiesToRemove);
}
// cast the card for free
game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), Boolean.TRUE);
boolean cardWasCast = player.cast(player.chooseAbilityForCast(card, game, true),
game, true, new ApprovingObject(source, game));
game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), null);
if (cardWasCast && (card.isCreature(game))) {
// creatures cast from suspend gain haste
if ((card.isCreature(game))) {
ContinuousEffect effect = new GainHasteEffect();
effect.setTargetPointer(new FixedTarget(card.getId(), card.getZoneChangeCounter(game) + 1));
game.addEffect(effect, source);

View file

@ -77,7 +77,7 @@ public class WardAbility extends TriggeredAbilityImpl {
if (!getSourceId().equals(event.getTargetId())) {
return false;
}
StackObject targetingObject = CardUtil.getTargetingStackObject(event, game);
StackObject targetingObject = CardUtil.getTargetingStackObject(this.getId().toString(), event, game);
if (targetingObject == null || !game.getOpponents(getControllerId()).contains(targetingObject.getControllerId())) {
return false;
}

View file

@ -1,98 +1,29 @@
package mage.cards;
import mage.abilities.Abilities;
import mage.abilities.AbilitiesImpl;
import mage.abilities.Ability;
import mage.abilities.SpellAbility;
import mage.constants.CardType;
import mage.constants.SpellAbilityType;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.ZoneChangeEvent;
import mage.util.CardUtil;
import java.util.List;
import java.util.UUID;
/**
* @author phulin
*/
public abstract class AdventureCard extends CardImpl {
/* The adventure spell card, i.e. Swift End. */
protected AdventureCardSpell spellCard;
public abstract class AdventureCard extends CardWithSpellOption {
public AdventureCard(UUID ownerId, CardSetInfo setInfo, CardType[] types, CardType[] typesSpell, String costs, String adventureName, String costsSpell) {
super(ownerId, setInfo, types, costs);
this.spellCard = new AdventureCardSpellImpl(ownerId, setInfo, adventureName, typesSpell, costsSpell, this);
this.spellCard = new AdventureSpellCard(ownerId, setInfo, adventureName, typesSpell, costsSpell, this);
}
public AdventureCard(AdventureCard card) {
super(card);
}
public void finalizeAdventure() {
spellCard.finalizeAdventure();
}
protected AdventureCard(final AdventureCard card) {
super(card);
this.spellCard = card.getSpellCard().copy();
this.spellCard.setParentCard(this);
}
public AdventureCardSpell getSpellCard() {
return spellCard;
}
public void setParts(AdventureCardSpell cardSpell) {
// for card copy only - set new parts
this.spellCard = cardSpell;
cardSpell.setParentCard(this);
}
@Override
public void assignNewId() {
super.assignNewId();
spellCard.assignNewId();
}
@Override
public boolean moveToZone(Zone toZone, Ability source, Game game, boolean flag, List<UUID> appliedEffects) {
if (super.moveToZone(toZone, source, game, flag, appliedEffects)) {
Zone currentZone = game.getState().getZone(getId());
game.getState().setZone(getSpellCard().getId(), currentZone);
return true;
}
return false;
}
@Override
public void setZone(Zone zone, Game game) {
super.setZone(zone, game);
game.setZone(getSpellCard().getId(), zone);
}
@Override
public boolean moveToExile(UUID exileId, String name, Ability source, Game game, List<UUID> appliedEffects) {
if (super.moveToExile(exileId, name, source, game, appliedEffects)) {
Zone currentZone = game.getState().getZone(getId());
game.getState().setZone(getSpellCard().getId(), currentZone);
return true;
}
return false;
}
@Override
public boolean removeFromZone(Game game, Zone fromZone, Ability source) {
// zone contains only one main card
return super.removeFromZone(game, fromZone, source);
}
@Override
public void updateZoneChangeCounter(Game game, ZoneChangeEvent event) {
if (isCopy()) { // same as meld cards
super.updateZoneChangeCounter(game, event);
return;
}
super.updateZoneChangeCounter(game, event);
getSpellCard().updateZoneChangeCounter(game, event);
spellCard.finalizeSpell();
}
@Override
@ -103,45 +34,4 @@ public abstract class AdventureCard extends CardImpl {
this.getSpellCard().getSpellAbility().setControllerId(controllerId);
return super.cast(game, fromZone, ability, controllerId);
}
@Override
public Abilities<Ability> getAbilities() {
Abilities<Ability> allAbilities = new AbilitiesImpl<>();
allAbilities.addAll(spellCard.getAbilities());
allAbilities.addAll(super.getAbilities());
return allAbilities;
}
@Override
public Abilities<Ability> getInitAbilities() {
// must init only parent related abilities, spell card must be init separately
return super.getAbilities();
}
@Override
public Abilities<Ability> getAbilities(Game game) {
Abilities<Ability> allAbilities = new AbilitiesImpl<>();
allAbilities.addAll(spellCard.getAbilities(game));
allAbilities.addAll(super.getAbilities(game));
return allAbilities;
}
public Abilities<Ability> getSharedAbilities(Game game) {
// abilities without spellcard
return super.getAbilities(game);
}
public List<String> getSharedRules(Game game) {
// rules without spellcard
Abilities<Ability> sourceAbilities = this.getSharedAbilities(game);
return CardUtil.getCardRulesWithAdditionalInfo(game, this, sourceAbilities, sourceAbilities);
}
@Override
public void setOwnerId(UUID ownerId) {
super.setOwnerId(ownerId);
abilities.setControllerId(ownerId);
spellCard.getAbilities().setControllerId(ownerId);
spellCard.setOwnerId(ownerId);
}
}

View file

@ -1,12 +0,0 @@
package mage.cards;
/**
* @author phulin
*/
public interface AdventureCardSpell extends SubCard<AdventureCard> {
@Override
AdventureCardSpell copy();
void finalizeAdventure();
}

View file

@ -19,11 +19,11 @@ import java.util.stream.Collectors;
/**
* @author phulin
*/
public class AdventureCardSpellImpl extends CardImpl implements AdventureCardSpell {
public class AdventureSpellCard extends CardImpl implements SpellOptionCard {
private AdventureCard adventureCardParent;
public AdventureCardSpellImpl(UUID ownerId, CardSetInfo setInfo, String adventureName, CardType[] cardTypes, String costs, AdventureCard adventureCardParent) {
public AdventureSpellCard(UUID ownerId, CardSetInfo setInfo, String adventureName, CardType[] cardTypes, String costs, AdventureCard adventureCardParent) {
super(ownerId, setInfo, cardTypes, costs, SpellAbilityType.ADVENTURE_SPELL);
this.subtype.add(SubType.ADVENTURE);
@ -35,13 +35,13 @@ public class AdventureCardSpellImpl extends CardImpl implements AdventureCardSpe
this.adventureCardParent = adventureCardParent;
}
public void finalizeAdventure() {
public void finalizeSpell() {
if (spellAbility instanceof AdventureCardSpellAbility) {
((AdventureCardSpellAbility) spellAbility).finalizeAdventure();
}
}
protected AdventureCardSpellImpl(final AdventureCardSpellImpl card) {
protected AdventureSpellCard(final AdventureSpellCard card) {
super(card);
this.adventureCardParent = card.adventureCardParent;
}
@ -83,13 +83,13 @@ public class AdventureCardSpellImpl extends CardImpl implements AdventureCardSpe
}
@Override
public AdventureCardSpellImpl copy() {
return new AdventureCardSpellImpl(this);
public AdventureSpellCard copy() {
return new AdventureSpellCard(this);
}
@Override
public void setParentCard(AdventureCard card) {
this.adventureCardParent = card;
public void setParentCard(CardWithSpellOption card) {
this.adventureCardParent = (AdventureCard) card;
}
@Override
@ -102,6 +102,11 @@ public class AdventureCardSpellImpl extends CardImpl implements AdventureCardSpe
// id must send to main card (popup card hint in game logs)
return getName() + " [" + adventureCardParent.getId().toString().substring(0, 3) + ']';
}
@Override
public String getSpellType() {
return "Adventure";
}
}
class AdventureCardSpellAbility extends SpellAbility {
@ -141,8 +146,8 @@ class AdventureCardSpellAbility extends SpellAbility {
public ActivationStatus canActivate(UUID playerId, Game game) {
ExileZone adventureExileZone = game.getExile().getExileZone(ExileAdventureSpellEffect.adventureExileId(playerId, game));
Card spellCard = game.getCard(this.getSourceId());
if (spellCard instanceof AdventureCardSpell) {
Card card = ((AdventureCardSpell) spellCard).getParentCard();
if (spellCard instanceof AdventureSpellCard) {
Card card = ((AdventureSpellCard) spellCard).getParentCard();
if (adventureExileZone != null && adventureExileZone.contains(card.getId())) {
return ActivationStatus.getFalse();
}

View file

@ -530,8 +530,8 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
}
}
if (stackObject == null && (this instanceof AdventureCard)) {
stackObject = game.getStack().getSpell(((AdventureCard) this).getSpellCard().getId(), false);
if (stackObject == null && (this instanceof CardWithSpellOption)) {
stackObject = game.getStack().getSpell(((CardWithSpellOption) this).getSpellCard().getId(), false);
}
if (stackObject == null) {

View file

@ -0,0 +1,131 @@
package mage.cards;
import mage.abilities.Abilities;
import mage.abilities.AbilitiesImpl;
import mage.abilities.Ability;
import mage.constants.CardType;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.ZoneChangeEvent;
import mage.util.CardUtil;
import java.util.List;
import java.util.UUID;
/**
* @author phulin, jmlundeen
*/
public abstract class CardWithSpellOption extends CardImpl {
/* The adventure/omen spell card, i.e. Swift End. */
protected SpellOptionCard spellCard;
public CardWithSpellOption(UUID ownerId, CardSetInfo setInfo, CardType[] types, String costs) {
super(ownerId, setInfo, types, costs);
}
public CardWithSpellOption(CardWithSpellOption card) {
super(card);
this.spellCard = card.getSpellCard().copy();
this.spellCard.setParentCard(this);
}
public SpellOptionCard getSpellCard() {
return spellCard;
}
public void setParts(SpellOptionCard cardSpell) {
// for card copy only - set new parts
this.spellCard = cardSpell;
cardSpell.setParentCard(this);
}
@Override
public void assignNewId() {
super.assignNewId();
spellCard.assignNewId();
}
@Override
public boolean moveToZone(Zone toZone, Ability source, Game game, boolean flag, List<UUID> appliedEffects) {
if (super.moveToZone(toZone, source, game, flag, appliedEffects)) {
Zone currentZone = game.getState().getZone(getId());
game.getState().setZone(getSpellCard().getId(), currentZone);
return true;
}
return false;
}
@Override
public void setZone(Zone zone, Game game) {
super.setZone(zone, game);
game.setZone(getSpellCard().getId(), zone);
}
@Override
public boolean moveToExile(UUID exileId, String name, Ability source, Game game, List<UUID> appliedEffects) {
if (super.moveToExile(exileId, name, source, game, appliedEffects)) {
Zone currentZone = game.getState().getZone(getId());
game.getState().setZone(getSpellCard().getId(), currentZone);
return true;
}
return false;
}
@Override
public boolean removeFromZone(Game game, Zone fromZone, Ability source) {
// zone contains only one main card
return super.removeFromZone(game, fromZone, source);
}
@Override
public void updateZoneChangeCounter(Game game, ZoneChangeEvent event) {
if (isCopy()) { // same as meld cards
super.updateZoneChangeCounter(game, event);
return;
}
super.updateZoneChangeCounter(game, event);
getSpellCard().updateZoneChangeCounter(game, event);
}
@Override
public Abilities<Ability> getAbilities() {
Abilities<Ability> allAbilities = new AbilitiesImpl<>();
allAbilities.addAll(spellCard.getAbilities());
allAbilities.addAll(super.getAbilities());
return allAbilities;
}
@Override
public Abilities<Ability> getInitAbilities() {
// must init only parent related abilities, spell card must be init separately
return super.getAbilities();
}
@Override
public Abilities<Ability> getAbilities(Game game) {
Abilities<Ability> allAbilities = new AbilitiesImpl<>();
allAbilities.addAll(spellCard.getAbilities(game));
allAbilities.addAll(super.getAbilities(game));
return allAbilities;
}
public Abilities<Ability> getSharedAbilities(Game game) {
// abilities without spellCard
return super.getAbilities(game);
}
public List<String> getSharedRules(Game game) {
// rules without spellCard
Abilities<Ability> sourceAbilities = this.getSharedAbilities(game);
return CardUtil.getCardRulesWithAdditionalInfo(game, this, sourceAbilities, sourceAbilities);
}
@Override
public void setOwnerId(UUID ownerId) {
super.setOwnerId(ownerId);
abilities.setControllerId(ownerId);
spellCard.getAbilities().setControllerId(ownerId);
spellCard.setOwnerId(ownerId);
}
}

View file

@ -0,0 +1,34 @@
package mage.cards;
import mage.abilities.SpellAbility;
import mage.constants.CardType;
import mage.constants.SpellAbilityType;
import mage.constants.Zone;
import mage.game.Game;
import java.util.UUID;
public abstract class OmenCard extends CardWithSpellOption {
public OmenCard(UUID ownerId, CardSetInfo setInfo, CardType[] types, CardType[] typesSpell, String costs, String omenName, String costsSpell) {
super(ownerId, setInfo, types, costs);
this.spellCard = new OmenSpellCard(ownerId, setInfo, omenName, typesSpell, costsSpell, this);
}
public OmenCard(OmenCard card) {
super(card);
}
public void finalizeOmen() {
spellCard.finalizeSpell();
}
@Override
public boolean cast(Game game, Zone fromZone, SpellAbility ability, UUID controllerId) {
if (ability.getSpellAbilityType() == SpellAbilityType.OMEN_SPELL) {
return this.getSpellCard().cast(game, fromZone, ability, controllerId);
}
this.getSpellCard().getSpellAbility().setControllerId(controllerId);
return super.cast(game, fromZone, ability, controllerId);
}
}

View file

@ -0,0 +1,174 @@
package mage.cards;
import mage.abilities.Ability;
import mage.abilities.Modes;
import mage.abilities.SpellAbility;
import mage.abilities.effects.Effect;
import mage.abilities.effects.common.ShuffleIntoLibrarySourceEffect;
import mage.constants.CardType;
import mage.constants.SpellAbilityType;
import mage.constants.SubType;
import mage.constants.Zone;
import mage.game.Game;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
public class OmenSpellCard extends CardImpl implements SpellOptionCard {
private OmenCard omenCardParent;
public OmenSpellCard(UUID ownerId, CardSetInfo setInfo, String omenName, CardType[] cardTypes, String costs, OmenCard omenCard) {
super(ownerId, setInfo, cardTypes, costs, SpellAbilityType.OMEN_SPELL);
this.subtype.add(SubType.OMEN);
OmenCardSpellAbility newSpellAbility = new OmenCardSpellAbility(getSpellAbility(), omenName, cardTypes, costs);
this.replaceSpellAbility(newSpellAbility);
spellAbility = newSpellAbility;
this.setName(omenName);
this.omenCardParent = omenCard;
}
public void finalizeSpell() {
if (spellAbility instanceof OmenCardSpellAbility) {
((OmenCardSpellAbility) spellAbility).finalizeOmen();
}
}
protected OmenSpellCard(final OmenSpellCard card) {
super(card);
this.omenCardParent = card.omenCardParent;
}
@Override
public UUID getOwnerId() {
return omenCardParent.getOwnerId();
}
@Override
public String getExpansionSetCode() {
return omenCardParent.getExpansionSetCode();
}
@Override
public String getCardNumber() {
return omenCardParent.getCardNumber();
}
@Override
public boolean moveToZone(Zone toZone, Ability source, Game game, boolean flag, List<UUID> appliedEffects) {
return omenCardParent.moveToZone(toZone, source, game, flag, appliedEffects);
}
@Override
public boolean moveToExile(UUID exileId, String name, Ability source, Game game, List<UUID> appliedEffects) {
return omenCardParent.moveToExile(exileId, name, source, game, appliedEffects);
}
@Override
public OmenCard getMainCard() {
return omenCardParent;
}
@Override
public void setZone(Zone zone, Game game) {
game.setZone(omenCardParent.getId(), zone);
game.setZone(omenCardParent.getSpellCard().getId(), zone);
}
@Override
public OmenSpellCard copy() {
return new OmenSpellCard(this);
}
@Override
public void setParentCard(CardWithSpellOption card) {
this.omenCardParent = (OmenCard) card;
}
@Override
public OmenCard getParentCard() {
return this.omenCardParent;
}
@Override
public String getIdName() {
// id must send to main card (popup card hint in game logs)
return getName() + " [" + omenCardParent.getId().toString().substring(0, 3) + ']';
}
@Override
public String getSpellType() {
return "Omen";
}
}
class OmenCardSpellAbility extends SpellAbility {
private String nameFull;
private boolean finalized = false;
public OmenCardSpellAbility(final SpellAbility baseSpellAbility, String omenName, CardType[] cardTypes, String costs) {
super(baseSpellAbility);
this.setName(cardTypes, omenName, costs);
this.setCardName(omenName);
}
public void finalizeOmen() {
if (finalized) {
throw new IllegalStateException("Wrong code usage. "
+ "Omen (" + cardName + ") "
+ "need to call finalizeOmen() exactly once.");
}
Effect effect = new ShuffleIntoLibrarySourceEffect();
effect.setText("");
this.addEffect(effect);
this.finalized = true;
}
protected OmenCardSpellAbility(final OmenCardSpellAbility ability) {
super(ability);
this.nameFull = ability.nameFull;
if (!ability.finalized) {
throw new IllegalStateException("Wrong code usage. "
+ "Omen (" + cardName + ") "
+ "need to call finalizeOmen() at the very end of the card's constructor.");
}
this.finalized = true;
}
public void setName(CardType[] cardTypes, String omenName, String costs) {
this.nameFull = "Omen " + Arrays.stream(cardTypes).map(CardType::toString).collect(Collectors.joining(" ")) + " &mdash; " + omenName;
this.name = this.nameFull + " " + costs;
}
@Override
public String getRule(boolean all) {
return this.getRule();
}
@Override
public String getRule() {
StringBuilder sbRule = new StringBuilder();
sbRule.append(this.nameFull);
sbRule.append(" ");
sbRule.append(getManaCosts().getText());
sbRule.append(" &mdash; ");
Modes modes = this.getModes();
if (modes.size() <= 1) {
sbRule.append(modes.getMode().getEffects().getTextStartingUpperCase(modes.getMode()));
} else {
sbRule.append(getModes().getText());
}
sbRule.append(" <i>(Then shuffle this card into its owner's library.)<i>");
return sbRule.toString();
}
@Override
public OmenCardSpellAbility copy() {
return new OmenCardSpellAbility(this);
}
}

View file

@ -0,0 +1,18 @@
package mage.cards;
public interface SpellOptionCard extends SubCard<CardWithSpellOption> {
@Override
SpellOptionCard copy();
/**
* Adds the final shared ability to the card. e.g. Adventure exile effect / Omen shuffle effect
*/
void finalizeSpell();
/**
* Used to get the card type text such as Adventure. Currently only used in {@link mage.game.stack.Spell#getSpellCastText Spell} for logging the spell
* being cast as part of the two part card.
*/
String getSpellType();
}

View file

@ -20,7 +20,7 @@ import java.util.List;
*/
public class MockCard extends CardImpl implements MockableCard {
public static String ADVENTURE_NAME_SEPARATOR = " // ";
public static String CARD_WITH_SPELL_OPTION_NAME_SEPARATOR = " // ";
public static String MODAL_DOUBLE_FACES_NAME_SEPARATOR = " // ";
// Needs to be here, as it is normally calculated from the
@ -34,7 +34,7 @@ public class MockCard extends CardImpl implements MockableCard {
protected List<String> manaCostLeftStr;
protected List<String> manaCostRightStr;
protected List<String> manaCostStr;
protected String adventureSpellName;
protected String spellOptionName; // adventure/omen spell name
protected boolean isModalDoubleFacedCard;
protected int manaValue;
@ -71,8 +71,8 @@ public class MockCard extends CardImpl implements MockableCard {
this.secondSideCard = new MockCard(CardRepository.instance.findCardWithPreferredSetAndNumber(card.getSecondSideName(), card.getSetCode(), card.getCardNumber()));
}
if (card.isAdventureCard()) {
this.adventureSpellName = card.getAdventureSpellName();
if (card.isCardWithSpellOption()) {
this.spellOptionName = card.getSpellOptionCardName();
}
if (card.isModalDoubleFacedCard()) {
@ -101,7 +101,7 @@ public class MockCard extends CardImpl implements MockableCard {
this.manaCostLeftStr = new ArrayList<>(card.manaCostLeftStr);
this.manaCostRightStr = new ArrayList<>(card.manaCostRightStr);
this.manaCostStr = new ArrayList<>(card.manaCostStr);
this.adventureSpellName = card.adventureSpellName;
this.spellOptionName = card.spellOptionName;
this.isModalDoubleFacedCard = card.isModalDoubleFacedCard;
this.manaValue = card.manaValue;
}
@ -155,8 +155,8 @@ public class MockCard extends CardImpl implements MockableCard {
return getName();
}
if (adventureSpellName != null) {
return getName() + ADVENTURE_NAME_SEPARATOR + adventureSpellName;
if (spellOptionName != null) {
return getName() + CARD_WITH_SPELL_OPTION_NAME_SEPARATOR + spellOptionName;
} else if (isModalDoubleFacedCard) {
return getName() + MODAL_DOUBLE_FACES_NAME_SEPARATOR + this.getSecondCardFace().getName();
} else {

View file

@ -106,9 +106,9 @@ public class CardInfo {
@DatabaseField
protected String secondSideName;
@DatabaseField
protected boolean adventureCard;
protected boolean cardWithSpellOption;
@DatabaseField
protected String adventureSpellName;
protected String spellOptionCardName;
@DatabaseField
protected boolean modalDoubleFacedCard;
@DatabaseField
@ -157,9 +157,9 @@ public class CardInfo {
this.secondSideName = secondSide.getName();
}
if (card instanceof AdventureCard) {
this.adventureCard = true;
this.adventureSpellName = ((AdventureCard) card).getSpellCard().getName();
if (card instanceof CardWithSpellOption) {
this.cardWithSpellOption = true;
this.spellOptionCardName = ((CardWithSpellOption) card).getSpellCard().getName();
}
if (card instanceof ModalDoubleFacedCard) {
@ -189,8 +189,8 @@ public class CardInfo {
List<String> manaCostLeft = ((ModalDoubleFacedCard) card).getLeftHalfCard().getManaCostSymbols();
List<String> manaCostRight = ((ModalDoubleFacedCard) card).getRightHalfCard().getManaCostSymbols();
this.setManaCosts(CardUtil.concatManaSymbols(SPLIT_MANA_SEPARATOR_FULL, manaCostLeft, manaCostRight));
} else if (card instanceof AdventureCard) {
List<String> manaCostLeft = ((AdventureCard) card).getSpellCard().getManaCostSymbols();
} else if (card instanceof CardWithSpellOption) {
List<String> manaCostLeft = ((CardWithSpellOption) card).getSpellCard().getManaCostSymbols();
List<String> manaCostRight = card.getManaCostSymbols();
this.setManaCosts(CardUtil.concatManaSymbols(SPLIT_MANA_SEPARATOR_FULL, manaCostLeft, manaCostRight));
} else {
@ -469,12 +469,16 @@ public class CardInfo {
return secondSideName;
}
public boolean isAdventureCard() {
return adventureCard;
public boolean isCardWithSpellOption() {
return cardWithSpellOption;
}
public String getAdventureSpellName() {
return adventureSpellName;
/**
* used for spell card portion of adventure/omen cards
* @return name of the spell
*/
public String getSpellOptionCardName() {
return spellOptionCardName;
}
public boolean isModalDoubleFacedCard() {

View file

@ -147,8 +147,8 @@ public enum CardRepository {
if (card.getMeldsToCardName() != null && !card.getMeldsToCardName().isEmpty()) {
namesList.add(card.getMeldsToCardName());
}
if (card.getAdventureSpellName() != null && !card.getAdventureSpellName().isEmpty()) {
namesList.add(card.getAdventureSpellName());
if (card.getSpellOptionCardName() != null && !card.getSpellOptionCardName().isEmpty()) {
namesList.add(card.getSpellOptionCardName());
}
}
@ -160,7 +160,7 @@ public enum CardRepository {
Set<String> names = new TreeSet<>();
try {
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName");
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName");
List<CardInfo> results = cardsDao.query(qb.prepare());
for (CardInfo card : results) {
addNewNames(card, names);
@ -176,7 +176,7 @@ public enum CardRepository {
Set<String> names = new TreeSet<>();
try {
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName");
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName");
qb.where().not().like("types", new SelectArg('%' + CardType.LAND.name() + '%'));
List<CardInfo> results = cardsDao.query(qb.prepare());
for (CardInfo card : results) {
@ -193,7 +193,7 @@ public enum CardRepository {
Set<String> names = new TreeSet<>();
try {
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName");
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName");
Where<CardInfo, Object> where = qb.where();
where.and(
where.not().like("supertypes", '%' + SuperType.BASIC.name() + '%'),
@ -214,7 +214,7 @@ public enum CardRepository {
Set<String> names = new TreeSet<>();
try {
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName");
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName");
qb.where().not().like("supertypes", new SelectArg('%' + SuperType.BASIC.name() + '%'));
List<CardInfo> results = cardsDao.query(qb.prepare());
for (CardInfo card : results) {
@ -231,7 +231,7 @@ public enum CardRepository {
Set<String> names = new TreeSet<>();
try {
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName");
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName");
qb.where().like("types", new SelectArg('%' + CardType.CREATURE.name() + '%'));
List<CardInfo> results = cardsDao.query(qb.prepare());
for (CardInfo card : results) {
@ -248,7 +248,7 @@ public enum CardRepository {
Set<String> names = new TreeSet<>();
try {
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName");
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName");
qb.where().like("types", new SelectArg('%' + CardType.ARTIFACT.name() + '%'));
List<CardInfo> results = cardsDao.query(qb.prepare());
for (CardInfo card : results) {
@ -265,7 +265,7 @@ public enum CardRepository {
Set<String> names = new TreeSet<>();
try {
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName");
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName");
Where<CardInfo, Object> where = qb.where();
where.and(
where.not().like("types", '%' + CardType.CREATURE.name() + '%'),
@ -286,7 +286,7 @@ public enum CardRepository {
Set<String> names = new TreeSet<>();
try {
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName");
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName");
Where<CardInfo, Object> where = qb.where();
where.and(
where.not().like("types", '%' + CardType.ARTIFACT.name() + '%'),
@ -511,7 +511,7 @@ public enum CardRepository {
queryBuilder.where()
.eq("flipCardName", new SelectArg(name)).or()
.eq("secondSideName", new SelectArg(name)).or()
.eq("adventureSpellName", new SelectArg(name)).or()
.eq("spellOptionCardName", new SelectArg(name)).or()
.eq("modalDoubleFacedSecondSideName", new SelectArg(name));
results = cardsDao.query(queryBuilder.prepare());
} else {

View file

@ -0,0 +1,75 @@
package mage.constants;
import mage.abilities.Ability;
import mage.abilities.condition.Condition;
import mage.game.Game;
import java.util.Objects;
public enum ModeChoice {
KHANS("Khans"),
DRAGONS("Dragons"),
MARDU("Mardu"),
TEMUR("Temur"),
ABZAN("Abzan"),
JESKAI("Jeskai"),
SULTAI("Sultai"),
MIRRAN("Mirran"),
PHYREXIAN("Phyrexian "),
ODD("odd"),
EVEN("even"),
BELIEVE("Believe"),
DOUBT("Doubt"),
NCR("NCR"),
LEGION("Legion"),
BROTHERHOOD("Brotherhood"),
ENCLAVE("Enclave"),
ISLAND("Island"),
SWAMP("Swamp"),
LEFT("left"),
RIGHT("right");
private static class ModeChoiceCondition implements Condition {
private final ModeChoice modeChoice;
ModeChoiceCondition(ModeChoice modeChoice) {
this.modeChoice = modeChoice;
}
@Override
public boolean apply(Game game, Ability source) {
return modeChoice.checkMode(game, source);
}
}
private final String name;
private final ModeChoiceCondition condition;
ModeChoice(String name) {
this.name = name;
this.condition = new ModeChoiceCondition(this);
}
@Override
public String toString() {
return name;
}
public ModeChoiceCondition getCondition() {
return condition;
}
public boolean checkMode(Game game, Ability source) {
return Objects.equals(game.getState().getValue(source.getSourceId() + "_modeChoice"), name);
}
}

View file

@ -15,6 +15,7 @@ public enum SpellAbilityCastMode {
NORMAL("Normal"),
MADNESS("Madness"),
FLASHBACK("Flashback"),
HARMONIZE("Harmonize"),
BESTOW("Bestow"),
PROTOTYPE("Prototype"),
MORPH("Morph", false, true), // and megamorph
@ -91,6 +92,7 @@ public enum SpellAbilityCastMode {
case NORMAL:
case MADNESS:
case FLASHBACK:
case HARMONIZE:
case DISTURB:
case PLOT:
case MORE_THAN_MEETS_THE_EYE:

View file

@ -15,7 +15,8 @@ public enum SpellAbilityType {
MODAL_LEFT("LeftModal SpellAbility"),
MODAL_RIGHT("RightModal SpellAbility"),
SPLICE("Spliced SpellAbility"),
ADVENTURE_SPELL("Adventure SpellAbility");
ADVENTURE_SPELL("Adventure SpellAbility"),
OMEN_SPELL("Omen SpellAbility");
private final String text;

View file

@ -13,6 +13,7 @@ public enum SubType {
ADVENTURE("Adventure", SubTypeSet.SpellType),
ARCANE("Arcane", SubTypeSet.SpellType),
LESSON("Lesson", SubTypeSet.SpellType),
OMEN("Omen", SubTypeSet.SpellType),
TRAP("Trap", SubTypeSet.SpellType),
// Battle subtypes
@ -274,6 +275,7 @@ public enum SubType {
MONGOOSE("Mongoose", SubTypeSet.CreatureType),
MONK("Monk", SubTypeSet.CreatureType),
MONKEY("Monkey", SubTypeSet.CreatureType),
MOOGLE("Moogle", SubTypeSet.CreatureType),
MOONFOLK("Moonfolk", SubTypeSet.CreatureType),
MOUNT("Mount", SubTypeSet.CreatureType),
MOUSE("Mouse", SubTypeSet.CreatureType),

View file

@ -54,6 +54,7 @@ public enum CounterType {
CURRENCY("currency"),
DEATH("death"),
DEATHTOUCH("deathtouch"),
DECAYED("decayed"),
DEFENSE("defense"),
DELAY("delay"),
DEPLETION("depletion"),
@ -184,6 +185,7 @@ public enum CounterType {
PREY("prey"),
PUPA("pupa"),
RAD("rad"),
RALLY("rally"),
REACH("reach"),
REJECTION("rejection"),
REPAIR("repair"),
@ -314,6 +316,8 @@ public enum CounterType {
return new BoostCounter(-2, -2, amount);
case DEATHTOUCH:
return new AbilityCounter(DeathtouchAbility.getInstance(), amount);
case DECAYED:
return new AbilityCounter(new DecayedAbility(), amount);
case DOUBLE_STRIKE:
return new AbilityCounter(DoubleStrikeAbility.getInstance(), amount);
case EXALTED:

View file

@ -1,9 +1,6 @@
package mage.filter.predicate.card;
import mage.cards.AdventureCard;
import mage.cards.Card;
import mage.cards.ModalDoubleFacedCard;
import mage.cards.SplitCard;
import mage.cards.*;
import mage.cards.mock.MockCard;
import mage.constants.SubType;
import mage.constants.SuperType;
@ -55,8 +52,8 @@ public class CardTextPredicate implements Predicate<Card> {
fullName = ((MockCard) input).getFullName(true);
} else if (input instanceof ModalDoubleFacedCard) {
fullName = input.getName() + MockCard.MODAL_DOUBLE_FACES_NAME_SEPARATOR + ((ModalDoubleFacedCard) input).getRightHalfCard().getName();
} else if (input instanceof AdventureCard) {
fullName = input.getName() + MockCard.ADVENTURE_NAME_SEPARATOR + ((AdventureCard) input).getSpellCard().getName();
} else if (input instanceof CardWithSpellOption) {
fullName = input.getName() + MockCard.CARD_WITH_SPELL_OPTION_NAME_SEPARATOR + ((CardWithSpellOption) input).getSpellCard().getName();
}
if (fullName.toLowerCase(Locale.ENGLISH).contains(text.toLowerCase(Locale.ENGLISH))) {
@ -107,8 +104,8 @@ public class CardTextPredicate implements Predicate<Card> {
}
}
if (input instanceof AdventureCard) {
for (String rule : ((AdventureCard) input).getSpellCard().getRules(game)) {
if (input instanceof CardWithSpellOption) {
for (String rule : ((CardWithSpellOption) input).getSpellCard().getRules(game)) {
if (rule.toLowerCase(Locale.ENGLISH).contains(token)) {
found = true;
break;

View file

@ -334,8 +334,8 @@ public abstract class GameImpl implements Game {
Card rightCard = ((ModalDoubleFacedCard) card).getRightHalfCard();
rightCard.setOwnerId(ownerId);
addCardToState(rightCard);
} else if (card instanceof AdventureCard) {
Card spellCard = ((AdventureCard) card).getSpellCard();
} else if (card instanceof CardWithSpellOption) {
Card spellCard = ((CardWithSpellOption) card).getSpellCard();
spellCard.setOwnerId(ownerId);
addCardToState(spellCard);
} else if (card.isTransformable() && card.getSecondCardFace() != null) {

View file

@ -1639,14 +1639,14 @@ public class GameState implements Serializable, Copyable<GameState> {
copiedParts.add(rightCopied);
// sync parts
((ModalDoubleFacedCard) copiedCard).setParts(leftCopied, rightCopied);
} else if (copiedCard instanceof AdventureCard) {
} else if (copiedCard instanceof CardWithSpellOption) {
// right
AdventureCardSpell rightOriginal = ((AdventureCard) copiedCard).getSpellCard();
AdventureCardSpell rightCopied = rightOriginal.copy();
SpellOptionCard rightOriginal = ((CardWithSpellOption) copiedCard).getSpellCard();
SpellOptionCard rightCopied = rightOriginal.copy();
prepareCardForCopy(rightOriginal, rightCopied, newController);
copiedParts.add(rightCopied);
// sync parts
((AdventureCard) copiedCard).setParts(rightCopied);
((CardWithSpellOption) copiedCard).setParts(rightCopied);
}
// main part prepare (must be called after other parts cause it change ids for all)

View file

@ -0,0 +1,34 @@
package mage.game.permanent.token;
import mage.MageInt;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.common.PreventDamageToSourceEffect;
import mage.abilities.keyword.VanishingAbility;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.Duration;
/**
* @author padfoothelix
*/
public final class AlienRhinoToken extends TokenImpl {
public AlienRhinoToken() {
super("Alien Rhino Token", "4/4 white Alien Rhino creature token");
cardType.add(CardType.CREATURE);
color.setWhite(true);
subtype.add(SubType.ALIEN);
subtype.add(SubType.RHINO);
power = new MageInt(4);
toughness = new MageInt(4);
}
private AlienRhinoToken(final AlienRhinoToken token) {
super(token);
}
@Override
public AlienRhinoToken copy() {
return new AlienRhinoToken(this);
}
}

View file

@ -0,0 +1,33 @@
package mage.game.permanent.token;
import mage.MageInt;
import mage.abilities.costs.mana.GenericManaCost;
import mage.abilities.keyword.WardAbility;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.Duration;
/**
* @author padfoothelix
*/
public final class Human11WithWard2Token extends TokenImpl {
public Human11WithWard2Token() {
super("Human Token", "1/1 white Human creature token with ward {2}");
cardType.add(CardType.CREATURE);
color.setWhite(true);
subtype.add(SubType.HUMAN);
power = new MageInt(1);
toughness = new MageInt(1);
this.addAbility(new WardAbility(new GenericManaCost(2)));
}
private Human11WithWard2Token(final Human11WithWard2Token token) {
super(token);
}
@Override
public Human11WithWard2Token copy() {
return new Human11WithWard2Token(this);
}
}

View file

@ -209,10 +209,11 @@ public class Spell extends StackObjectImpl implements Card {
+ " using " + this.getSpellAbility().getSpellAbilityCastMode();
}
if (card instanceof AdventureCardSpell) {
AdventureCard adventureCard = ((AdventureCardSpell) card).getParentCard();
return GameLog.replaceNameByColoredName(card, getSpellAbility().toString(), adventureCard)
+ " as Adventure spell of " + GameLog.getColoredObjectIdName(adventureCard);
if (card instanceof SpellOptionCard) {
CardWithSpellOption parentCard = ((SpellOptionCard) card).getParentCard();
String type = ((SpellOptionCard) card).getSpellType();
return GameLog.replaceNameByColoredName(card, getSpellAbility().toString(), parentCard)
+ " as " + type + " spell of " + GameLog.getColoredObjectIdName(parentCard);
}
if (card instanceof ModalDoubleFacedCardHalf) {
@ -539,8 +540,8 @@ public class Spell extends StackObjectImpl implements Card {
public String getIdName() {
String idName;
if (card != null) {
if (card instanceof AdventureCardSpell) {
idName = ((AdventureCardSpell) card).getParentCard().getId().toString().substring(0, 3);
if (card instanceof SpellOptionCard) {
idName = ((SpellOptionCard) card).getParentCard().getId().toString().substring(0, 3);
} else {
idName = card.getId().toString().substring(0, 3);
}

View file

@ -426,6 +426,16 @@ public class StackAbility extends StackObjectImpl implements Ability {
// Do nothing
}
@Override
public void setVariableCostsMinMax(int min, int max) {
ability.setVariableCostsMinMax(min, max);
}
@Override
public void setVariableCostsValue(int xValue) {
ability.setVariableCostsValue(xValue);
}
@Override
public Map<String, Object> getCostsTagMap() {
return ability.getCostsTagMap();
@ -778,14 +788,23 @@ public class StackAbility extends StackObjectImpl implements Ability {
}
@Override
public CostAdjuster getCostAdjuster() {
return costAdjuster;
public void adjustX(Game game) {
if (costAdjuster != null) {
costAdjuster.prepareX(this, game);
}
}
@Override
public void adjustCosts(Game game) {
public void adjustCostsPrepare(Game game) {
if (costAdjuster != null) {
costAdjuster.adjustCosts(this, game);
costAdjuster.prepareCost(this, game);
}
}
@Override
public void adjustCostsModify(Game game, CostModificationType costModificationType) {
if (costAdjuster != null) {
costAdjuster.modifyCost(this, game, costModificationType);
}
}

View file

@ -743,10 +743,14 @@ public interface Player extends MageItem, Copyable<Player> {
boolean shuffleCardsToLibrary(Card card, Game game, Ability source);
// set the value for X mana spells and abilities
/**
* Set the value for X mana spells and abilities
*/
int announceXMana(int min, int max, String message, Game game, Ability ability);
// set the value for non mana X costs
/**
* Set the value for non mana X costs
*/
int announceXCost(int min, int max, String message, Game game, Ability ability, VariableCost variableCost);
// TODO: rework to use pair's list of effect + ability instead string's map

View file

@ -3679,8 +3679,11 @@ public abstract class PlayerImpl implements Player, Serializable {
if (!copy.canActivate(playerId, game).canActivate()) {
return false;
}
// apply dynamic costs and cost modification
copy.adjustX(game);
if (availableMana != null) {
copy.adjustCosts(game);
// TODO: need research, why it look at availableMana here - can delete condition?
game.getContinuousEffects().costModification(copy, game);
}
boolean canBeCastRegularly = true;
@ -3891,7 +3894,8 @@ public abstract class PlayerImpl implements Player, Serializable {
copyAbility = ability.copy();
copyAbility.clearManaCostsToPay();
copyAbility.addManaCostsToPay(manaCosts.copy());
copyAbility.adjustCosts(game);
// apply dynamic costs and cost modification
copyAbility.adjustX(game);
game.getContinuousEffects().costModification(copyAbility, game);
// reduced all cost
@ -3963,12 +3967,9 @@ public abstract class PlayerImpl implements Player, Serializable {
// 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);
// apply dynamic costs and cost modification
copyAbility.adjustX(game);
game.getContinuousEffects().costModification(copyAbility, game);
// reduced all cost
@ -4068,11 +4069,11 @@ public abstract class PlayerImpl implements Player, Serializable {
getPlayableFromObjectSingle(game, fromZone, mainCard.getLeftHalfCard(), mainCard.getLeftHalfCard().getAbilities(game), availableMana, output);
getPlayableFromObjectSingle(game, fromZone, mainCard.getRightHalfCard(), mainCard.getRightHalfCard().getAbilities(game), availableMana, output);
getPlayableFromObjectSingle(game, fromZone, mainCard, mainCard.getSharedAbilities(game), availableMana, output);
} else if (object instanceof AdventureCard) {
} else if (object instanceof CardWithSpellOption) {
// adventure must use different card characteristics for different spells (main or adventure)
AdventureCard adventureCard = (AdventureCard) object;
getPlayableFromObjectSingle(game, fromZone, adventureCard.getSpellCard(), adventureCard.getSpellCard().getAbilities(game), availableMana, output);
getPlayableFromObjectSingle(game, fromZone, adventureCard, adventureCard.getSharedAbilities(game), availableMana, output);
CardWithSpellOption cardWithSpellOption = (CardWithSpellOption) object;
getPlayableFromObjectSingle(game, fromZone, cardWithSpellOption.getSpellCard(), cardWithSpellOption.getSpellCard().getAbilities(game), availableMana, output);
getPlayableFromObjectSingle(game, fromZone, cardWithSpellOption, cardWithSpellOption.getSharedAbilities(game), availableMana, output);
} else if (object instanceof Card) {
getPlayableFromObjectSingle(game, fromZone, object, ((Card) object).getAbilities(game), availableMana, output);
} else if (object instanceof StackObject) {

View file

@ -25,8 +25,13 @@ public class TargetsCountAdjuster extends GenericTargetAdjuster {
@Override
public void adjustTargets(Ability ability, Game game) {
Target newTarget = blueprintTarget.copy();
int count = dynamicValue.calculate(game, ability, ability.getEffects().get(0));
ability.getTargets().clear();
if (count <= 0) {
return;
}
Target newTarget = blueprintTarget.copy();
newTarget.setMaxNumberOfTargets(count);
Filter filter = newTarget.getFilter();
if (blueprintTarget.getMinNumberOfTargets() != 0) {
@ -35,7 +40,6 @@ public class TargetsCountAdjuster extends GenericTargetAdjuster {
} else {
newTarget.withTargetName(filter.getMessage() + " (up to " + count + " targets)");
}
ability.getTargets().clear();
ability.addTarget(newTarget);
}
}

View file

@ -22,14 +22,7 @@ public class FixedTargets extends TargetPointerImpl {
final ArrayList<MageObjectReference> targets = new ArrayList<>();
public FixedTargets(List<Permanent> objects, Game game) {
this(objects
.stream()
.map(o -> new MageObjectReference(o.getId(), game))
.collect(Collectors.toList()));
}
public FixedTargets(Set<Card> objects, Game game) {
public FixedTargets(Collection<? extends Card> objects, Game game) {
this(objects
.stream()
.map(o -> new MageObjectReference(o.getId(), game))
@ -50,7 +43,7 @@ public class FixedTargets extends TargetPointerImpl {
.collect(Collectors.toList()), game);
}
public FixedTargets(List<MageObjectReference> morList) {
public FixedTargets(Collection<MageObjectReference> morList) {
super();
targets.addAll(morList);
this.setInitialized(); // no need dynamic init

View file

@ -1079,6 +1079,53 @@ public final class CardUtil {
.collect(Collectors.toSet());
}
public static Set<UUID> getAllPossibleTargets(Cost cost, Game game, Ability source) {
return cost.getTargets()
.stream()
.map(t -> t.possibleTargets(source.getControllerId(), source, game))
.flatMap(Collection::stream)
.collect(Collectors.toSet());
}
/**
* Distribute values between min and max and make sure that the values will be evenly distributed
* Use it to limit possible values list like mana options
*/
public static List<Integer> distributeValues(int count, int min, int max) {
List<Integer> res = new ArrayList<>();
if (count <= 0 || min > max) {
return res;
}
if (min == max) {
res.add(min);
return res;
}
int range = max - min + 1;
// low possible amount
if (range <= count) {
for (int i = 0; i < range; i++) {
res.add(min + i);
}
return res;
}
// big possible amount, so skip some values
double step = (double) (max - min) / (count - 1);
for (int i = 0; i < count; i++) {
res.add(min + (int) Math.round(i * step));
}
// make sure first and last elements are good
res.set(0, min);
if (res.size() > 1) {
res.set(res.size() - 1, max);
}
return res;
}
/**
* For finding the spell or ability on the stack for "becomes the target" triggers.
*
@ -1086,13 +1133,22 @@ public final class CardUtil {
* @param game the Game from checkTrigger() or watch()
* @return the StackObject which targeted the source, or null if not found
*/
public static StackObject getTargetingStackObject(GameEvent event, Game game) {
public static StackObject getTargetingStackObject(String checkingReference, GameEvent event, Game game) {
// In case of multiple simultaneous triggered abilities from the same source,
// need to get the actual one that targeted, see #8026, #8378
// Also avoids triggering on cancelled selections, see #8802
String stateKey = "targetedMap" + checkingReference;
Map<UUID, Set<UUID>> targetMap = (Map<UUID, Set<UUID>>) game.getState().getValue(stateKey);
// targetMap: key - targetId; value - Set of stackObject Ids
if (targetMap == null) {
targetMap = new HashMap<>();
} else {
targetMap = new HashMap<>(targetMap); // must have new object reference if saved back to game state
}
Set<UUID> targetingObjects = targetMap.computeIfAbsent(event.getTargetId(), k -> new HashSet<>());
for (StackObject stackObject : game.getStack()) {
Ability stackAbility = stackObject.getStackAbility();
if (stackAbility == null || !stackAbility.getSourceId().equals(event.getSourceId())) {
if (stackAbility == null || !stackAbility.getSourceId().equals(event.getSourceId()) || targetingObjects.contains(stackObject.getId())) {
continue;
}
if (CardUtil.getAllSelectedTargets(stackAbility, game).contains(event.getTargetId())) {
@ -1263,7 +1319,7 @@ public final class CardUtil {
Card permCard;
if (card instanceof SplitCard) {
permCard = card;
} else if (card instanceof AdventureCard) {
} else if (card instanceof CardWithSpellOption) {
permCard = card;
} else if (card instanceof ModalDoubleFacedCard) {
permCard = ((ModalDoubleFacedCard) card).getLeftHalfCard();
@ -1451,9 +1507,9 @@ public final class CardUtil {
if (cardToCast instanceof CardWithHalves) {
cards.add(((CardWithHalves) cardToCast).getLeftHalfCard());
cards.add(((CardWithHalves) cardToCast).getRightHalfCard());
} else if (cardToCast instanceof AdventureCard) {
} else if (cardToCast instanceof CardWithSpellOption) {
cards.add(cardToCast);
cards.add(((AdventureCard) cardToCast).getSpellCard());
cards.add(((CardWithSpellOption) cardToCast).getSpellCard());
} else {
cards.add(cardToCast);
}
@ -1642,9 +1698,9 @@ public final class CardUtil {
}
// handle adventure cards
if (card instanceof AdventureCard) {
if (card instanceof CardWithSpellOption) {
Card creatureCard = card.getMainCard();
Card spellCard = ((AdventureCard) card).getSpellCard();
Card spellCard = ((CardWithSpellOption) card).getSpellCard();
if (manaCost != null) {
// get additional cost if any
Costs<Cost> additionalCostsCreature = creatureCard.getSpellAbility().getCosts();
@ -1682,9 +1738,9 @@ public final class CardUtil {
game.getState().setValue("PlayFromNotOwnHandZone" + leftHalfCard.getId(), null);
game.getState().setValue("PlayFromNotOwnHandZone" + rightHalfCard.getId(), null);
}
if (card instanceof AdventureCard) {
if (card instanceof CardWithSpellOption) {
Card creatureCard = card.getMainCard();
Card spellCard = ((AdventureCard) card).getSpellCard();
Card spellCard = ((CardWithSpellOption) card).getSpellCard();
game.getState().setValue("PlayFromNotOwnHandZone" + creatureCard.getId(), null);
game.getState().setValue("PlayFromNotOwnHandZone" + spellCard.getId(), null);
}
@ -1899,6 +1955,10 @@ public final class CardUtil {
return defaultValue;
}
public static int getSourceCostsTagX(Game game, Ability source, int defaultValue) {
return getSourceCostsTag(game, source, "X", defaultValue);
}
public static String addCostVerb(String text) {
if (costWords.stream().anyMatch(text.toLowerCase(Locale.ENGLISH)::startsWith)) {
return text;
@ -2069,8 +2129,8 @@ public final class CardUtil {
res.add(mainCard);
res.add(mainCard.getLeftHalfCard());
res.add(mainCard.getRightHalfCard());
} else if (object instanceof AdventureCard || object instanceof AdventureCardSpell) {
AdventureCard mainCard = (AdventureCard) ((Card) object).getMainCard();
} else if (object instanceof CardWithSpellOption || object instanceof SpellOptionCard) {
CardWithSpellOption mainCard = (CardWithSpellOption) ((Card) object).getMainCard();
res.add(mainCard);
res.add(mainCard.getSpellCard());
} else if (object instanceof Spell) {
@ -2176,6 +2236,19 @@ public final class CardUtil {
return sb.toString();
}
public static <T> T getEffectValueFromAbility(Ability ability, String key, Class<T> clazz) {
return getEffectValueFromAbility(ability, key, clazz, null);
}
public static <T> T getEffectValueFromAbility(Ability ability, String key, Class<T> clazz, T defaultValue) {
return castStream(
ability.getAllEffects()
.stream()
.map(effect -> effect.getValue(key)),
clazz
).findFirst().orElse(defaultValue);
}
public static <T> Stream<T> castStream(Collection<?> collection, Class<T> clazz) {
return castStream(collection.stream(), clazz);
}

View file

@ -82,8 +82,8 @@ public class CircularList<E> implements List<E>, Iterable<E>, Serializable {
*/
@Override
public E get(int index) {
if (list.size() > this.index) {
return list.get(this.index);
if (list.size() > index) {
return list.get(index);
}
return null;
}

View file

@ -13,10 +13,7 @@ import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.dynamicvalue.common.GetXValue;
import mage.abilities.effects.Effect;
import mage.abilities.mana.*;
import mage.cards.AdventureCard;
import mage.cards.Card;
import mage.cards.ModalDoubleFacedCard;
import mage.cards.SplitCard;
import mage.cards.*;
import mage.choices.Choice;
import mage.constants.ColoredManaSymbol;
import mage.constants.ManaType;
@ -644,8 +641,8 @@ public final class ManaUtil {
Card secondSide;
if (card instanceof SplitCard) {
secondSide = ((SplitCard) card).getRightHalfCard();
} else if (card instanceof AdventureCard) {
secondSide = ((AdventureCard) card).getSpellCard();
} else if (card instanceof CardWithSpellOption) {
secondSide = ((CardWithSpellOption) card).getSpellCard();
} else if (card instanceof ModalDoubleFacedCard) {
secondSide = ((ModalDoubleFacedCard) card).getRightHalfCard();
} else {

View file

@ -29,7 +29,7 @@ public class NumberOfTimesPermanentTargetedATurnWatcher extends Watcher {
if (event.getType() != GameEvent.EventType.TARGETED) {
return;
}
StackObject targetingObject = CardUtil.getTargetingStackObject(event, game);
StackObject targetingObject = CardUtil.getTargetingStackObject(this.getKey(), event, game);
if (targetingObject == null || CardUtil.checkTargetedEventAlreadyUsed(this.getKey(), targetingObject, event, game)) {
return;
}