/* * Copyright 2010 BetaSteward_at_googlemail.com. All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are * permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, this list of * conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, this list * of conditions and the following disclaimer in the documentation and/or other materials * provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY BetaSteward_at_googlemail.com ``AS IS'' AND ANY EXPRESS OR IMPLIED * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BetaSteward_at_googlemail.com OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * The views and conclusions contained in the software and documentation are those of the * authors and should not be interpreted as representing official policies, either expressed * or implied, of BetaSteward_at_googlemail.com. */ package mage.abilities; import mage.constants.AbilityType; import mage.constants.EffectType; import mage.constants.Outcome; import mage.constants.Zone; import mage.MageObject; import mage.abilities.costs.*; import mage.abilities.costs.mana.ManaCost; import mage.abilities.costs.mana.ManaCosts; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.costs.mana.VariableManaCost; import mage.abilities.effects.*; import mage.abilities.mana.ManaAbility; import mage.cards.Card; import mage.choices.Choice; import mage.choices.Choices; import mage.game.Game; import mage.game.permanent.PermanentCard; import mage.target.Target; import mage.target.Targets; import org.apache.log4j.Logger; import java.util.ArrayList; import java.util.List; import java.util.UUID; import mage.abilities.keyword.EntwineAbility; import mage.constants.SpellAbilityType; /** * * @author BetaSteward_at_googlemail.com */ public abstract class AbilityImpl> implements Ability { private static final transient Logger logger = Logger.getLogger(AbilityImpl.class); protected UUID id; protected UUID originalId; protected AbilityType abilityType; protected UUID controllerId; protected UUID sourceId; protected ManaCosts manaCosts; protected ManaCosts manaCostsToPay; protected Costs costs; protected ArrayList alternativeCosts = new ArrayList(); protected Costs optionalCosts; protected Modes modes; protected Zone zone; protected String name; protected boolean usesStack = true; protected boolean ruleAtTheTop = false; protected boolean ruleVisible = true; @Override public abstract T copy(); public AbilityImpl(AbilityType abilityType, Zone zone) { this.id = UUID.randomUUID(); this.originalId = id; this.abilityType = abilityType; this.zone = zone; this.manaCosts = new ManaCostsImpl(); this.manaCostsToPay = new ManaCostsImpl(); this.costs = new CostsImpl(); this.optionalCosts = new CostsImpl(); this.modes = new Modes(); } public AbilityImpl(final AbilityImpl ability) { this.id = ability.id; this.originalId = ability.originalId; this.abilityType = ability.abilityType; this.controllerId = ability.controllerId; this.sourceId = ability.sourceId; this.zone = ability.zone; this.name = ability.name; this.usesStack = ability.usesStack; this.manaCosts = ability.manaCosts; this.manaCostsToPay = ability.manaCostsToPay.copy(); this.costs = ability.costs.copy(); this.optionalCosts = ability.optionalCosts.copy(); for (AlternativeCost cost: ability.alternativeCosts) { this.alternativeCosts.add((AlternativeCost)cost.copy()); } this.modes = ability.modes.copy(); this.ruleAtTheTop = ability.ruleAtTheTop; this.ruleVisible = ability.ruleVisible; } @Override public UUID getId() { return id; } @Override public void newId() { if (!(this instanceof MageSingleton)) { this.id = UUID.randomUUID(); } getEffects().newId(); } @Override public void newOriginalId() { this.id = UUID.randomUUID(); this.originalId = id; getEffects().newId(); } @Override public AbilityType getAbilityType() { return this.abilityType; } @Override public boolean resolve(Game game) { boolean result = true; //20100716 - 117.12 if (checkIfClause(game)) { for (Effect effect: getEffects()) { if (effect instanceof OneShotEffect) { if (!(effect instanceof PostResolveEffect)) { result &= effect.apply(game, this); } } else { game.addEffect((ContinuousEffect) effect, this); } // some effects must be applied before next effect is resolved, because effect is dependend. if (effect.applyEffectsAfter()) { game.applyEffects(); } } } return result; } @Override public boolean activate(Game game, boolean noMana) { /* 20130201 - 601.2b * If the spell is modal the player announces the mode choice (see rule 700.2). */ if (!modes.choose(game, this)) { return false; } /* 20130201 - 601.2b * If the player wishes to splice any cards onto the spell (see rule 702.45), he * or she reveals those cards in his or her hand. */ if (this.abilityType.equals(AbilityType.SPELL)) { game.getContinuousEffects().applySpliceEffects(this, game); } Card card = game.getCard(sourceId); if (card != null) { card.adjustChoices(this, game); } for (UUID modeId :this.getModes().getSelectedModes()) { this.getModes().setMode(this.getModes().get(modeId)); if (getChoices().size() > 0 && getChoices().choose(game, this) == false) { logger.debug("activate failed - choice"); return false; } } // 20130201 - 601.2b // If the spell has alternative or additional costs that will be paid as it's being cast such // as buyback, kicker, or convoke costs (see rules 117.8 and 117.9), the player announces his // or her intentions to pay any or all of those costs (see rule 601.2e). // A player can't apply two alternative methods of casting or two alternative costs to a single spell. if (card != null) { for (Ability ability : card.getAbilities()) { if (ability instanceof AlternativeSourceCosts) { if (((AlternativeSourceCosts)ability).askToActivateAlternativeCosts(this, game)) { // only one alternative costs may be activated break; } } if (ability instanceof OptionalAdditionalSourceCosts) { ((OptionalAdditionalSourceCosts)ability).addOptionalAdditionalCosts(this, 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. VariableManaCost variableManaCost = handleXCosts(game, noMana); for (UUID modeId :this.getModes().getSelectedModes()) { this.getModes().setMode(this.getModes().get(modeId)); //20121001 - 601.2c // 601.2c The player announces his or her choice of an appropriate player, object, or zone for // each target the spell requires. A spell may require some targets only if an alternative or // additional cost (such as a buyback or kicker cost), or a particular mode, was chosen for it; // otherwise, the spell is cast as though it did not require those targets. If the spell has a // variable number of targets, the player announces how many targets he or she will choose before // he or she announces those targets. The same target can't be chosen multiple times for any one // instance of the word "target" on the spell. However, if the spell uses the word "target" in // multiple places, the same object, player, or zone can be chosen once for each instance of the // word "target" (as long as it fits the targeting criteria). If any effects say that an object // or player must be chosen as a target, the player chooses targets so that he or she obeys the // maximum possible number of such effects without violating any rules or effects that say that // an object or player can't be chosen as a target. The chosen players, objects, and/or zones // each become a target of that spell. (Any abilities that trigger when those players, objects, // and/or zones become the target of a spell trigger at this point; they'll wait to be put on // the stack until the spell has finished being cast.) if (card != null) { card.adjustTargets(this, game); } if (getTargets().size() > 0 && getTargets().chooseTargets(getEffects().get(0).getOutcome(), this.controllerId, this, game) == false) { if (variableManaCost != null) { game.informPlayers(new StringBuilder(card != null ? card.getName(): "").append(": no valid targets with this value of X").toString()); } else { logger.debug("activate failed - target"); } return false; } } // end modes // TODO: Handle optionalCosts at the same time as already OptionalAdditionalSourceCosts are handled. for (Cost cost : optionalCosts) { if (cost instanceof ManaCost) { cost.clearPaid(); if (game.getPlayer(this.controllerId).chooseUse(Outcome.Benefit, "Pay optional cost " + cost.getText() + "?", game)) { manaCostsToPay.add((ManaCost) cost); } } } //20100716 - 601.2e if (card != null) { card.adjustCosts(this, game); for (Ability ability : card.getAbilities()) { if (ability instanceof AdjustingSourceCosts) { ((AdjustingSourceCosts)ability).adjustCosts(this, game); } } } // this is a hack to prevent mana abilities with mana costs from causing endless loops - pay other costs first if (this instanceof ManaAbility && !costs.pay(this, game, sourceId, controllerId, noMana)) { logger.debug("activate mana ability failed - non mana costs"); return false; } //20101001 - 601.2e game.getContinuousEffects().costModification(this, game); UUID activatorId = controllerId; if ((this instanceof ActivatedAbilityImpl) && ((ActivatedAbilityImpl)this).getActivatorId()!= null) { activatorId = ((ActivatedAbilityImpl)this).getActivatorId(); } if (!useAlternativeCost(game)) { //20100716 - 601.2f if (!manaCostsToPay.pay(this, game, sourceId, activatorId, noMana)) { logger.debug("activate failed - mana"); return false; } } //20100716 - 601.2g if (!costs.pay(this, game, sourceId, activatorId, noMana)) { logger.debug("activate failed - non mana costs"); return false; } return true; } /** * Handles the announcement of X mana costs and sets manaCostsToPay. * * @param game * @param noMana * @return variableManaCost for late check */ protected VariableManaCost handleXCosts(Game game, boolean noMana) { // 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. // TODO: Handle announcing other variable costs here like: RemoveVariableCountersSourceCost VariableManaCost variableManaCost = null; for (ManaCost cost: manaCostsToPay) { if (cost instanceof VariableManaCost) { variableManaCost = (VariableManaCost) cost; break; // only one VariableManCost per spell (or is it possible to have more?) } } if (variableManaCost != null) { int xValue; if (!variableManaCost.isPaid()) { // should only happen for human players if (!noMana) { xValue = game.getPlayer(this.controllerId).announceXMana(variableManaCost.getMinX(), Integer.MAX_VALUE, "Announce the value for " + variableManaCost.getText(), game, this); int amountMana = xValue * variableManaCost.getMultiplier(); StringBuilder manaString = new StringBuilder(); if (variableManaCost.getFilter() == null || variableManaCost.getFilter().isColorless()) { manaString.append("{").append(amountMana).append("}"); } else { String manaSymbol = null; if (variableManaCost.getFilter().isBlack()) { manaSymbol = "B"; } else if (variableManaCost.getFilter().isRed()) { manaSymbol = "R"; } else if (variableManaCost.getFilter().isBlue()) { manaSymbol = "U"; } else if (variableManaCost.getFilter().isGreen()) { manaSymbol = "G"; } else if (variableManaCost.getFilter().isWhite()) { manaSymbol = "W"; } if (manaSymbol == null) { throw new UnsupportedOperationException("ManaFilter is not supported: " +this.toString() ); } for (int i = 0; i < amountMana; i++) { manaString.append("{").append(manaSymbol).append("}"); } } manaCostsToPay.add(new ManaCostsImpl(manaString.toString())); manaCostsToPay.setX(amountMana); } variableManaCost.setPaid(); } xValue = getManaCostsToPay().getX(); game.informPlayers(new StringBuilder(game.getPlayer(this.controllerId).getName()).append(" announced a value of ").append(xValue).append(" for ").append(variableManaCost.getText()).toString()); } return variableManaCost; } @Override public void reset(Game game) {} protected boolean useAlternativeCost(Game game) { for (AlternativeCost cost: alternativeCosts) { if (cost.isAvailable(game, this)) { if (game.getPlayer(this.controllerId).chooseUse(Outcome.Neutral, "Use alternative cost " + cost.getName(), game)) { return cost.pay(this, game, sourceId, controllerId, false); } } } return false; } @Override public boolean checkIfClause(Game game) { return true; } @Override public UUID getControllerId() { return controllerId; } @Override public void setControllerId(UUID controllerId) { this.controllerId = controllerId; } @Override public UUID getSourceId() { return sourceId; } @Override public void setSourceId(UUID sourceId) { this.sourceId = sourceId; } @Override public Costs getCosts() { return costs; } @Override public ManaCosts getManaCosts() { return manaCosts; } /** * Should be used by {@link mage.abilities.effects.CostModificationEffect cost modification effects} * to manipulate what is actually paid before resolution. * * @return */ @Override public ManaCosts getManaCostsToPay ( ) { return manaCostsToPay; } @Override public List getAlternativeCosts() { return alternativeCosts; } @Override public Costs getOptionalCosts() { return optionalCosts; } @Override public Effects getEffects() { return modes.getMode().getEffects(); } @Override public Effects getEffects(Game game, EffectType effectType) { Effects typedEffects = new Effects(); for (Effect effect: getEffects()) { if (effect.getEffectType() == effectType) { typedEffects.add(effect); } } return typedEffects; } @Override public Choices getChoices() { return modes.getMode().getChoices(); } @Override public Zone getZone() { return zone; } @Override public boolean isUsesStack() { return usesStack; } @Override public String getRule() { return getRule(false); } @Override public String getRule(boolean all) { StringBuilder sbRule = new StringBuilder(); if (all || this.abilityType != AbilityType.SPELL) { if (manaCosts.size() > 0) { sbRule.append(manaCosts.getText()); } if (costs.size() > 0) { if (sbRule.length() > 0) { sbRule.append(","); } sbRule.append(costs.getText()); } if (sbRule.length() > 0) { sbRule.append(": "); } } String text = modes.getText(); if (!text.isEmpty()) { if (sbRule.length() > 1) { String end = sbRule.substring(sbRule.length()-2).trim(); if (end.isEmpty() || end.equals(":") || end.equals(".")) { sbRule.append(Character.toUpperCase(text.charAt(0))).append(text.substring(1)); } else { sbRule.append(text); } } else { sbRule.append(text); } } return sbRule.toString(); } @Override public String getRule(String source) { return formatRule(getRule(), source); } protected String formatRule(String rule, String source) { String replace = rule; if (rule != null && source != null && !source.isEmpty()) { replace = rule.replace("{this}", source); replace = replace.replace("{source}", source); } return replace; } @Override public void addCost(Cost cost) { if (cost != null) { if (cost instanceof ManaCost) { this.addManaCost((ManaCost)cost); } else { this.costs.add(cost); } } } @Override public void addManaCost(ManaCost cost) { if (cost != null) { this.manaCosts.add(cost); this.manaCostsToPay.add(cost); } } @Override public void addAlternativeCost(AlternativeCost cost) { if (cost != null) { this.alternativeCosts.add(cost); } } @Override public void addOptionalCost(Cost cost) { if (cost != null) { this.optionalCosts.add(cost); } } @Override public void addEffect(Effect effect) { if (effect != null) { getEffects().add(effect); } } @Override public void addTarget(Target target) { if (target != null) { getTargets().add(target); } } @Override public void addChoice(Choice choice) { if (choice != null) { getChoices().add(choice); } } @Override public Targets getTargets() { return modes.getMode().getTargets(); } @Override public UUID getFirstTarget() { return getTargets().getFirstTarget(); } @Override public boolean isModal() { return this.modes.size() > 1; } @Override public void addMode(Mode mode) { this.modes.addMode(mode); } @Override public Modes getModes() { return modes; } @Override public boolean canChooseTarget(Game game) { for (Mode mode: modes.values()) { if (mode.getTargets().canChoose(sourceId, controllerId, game)) { return true; } } return false; } @Override public boolean isInUseableZone(Game game, MageObject source, boolean checkLKI) { // emblem are always actual if (zone.equals(Zone.COMMAND)) { return true; } // try LKI first if (checkLKI) { MageObject lkiTest = game.getShortLivingLKI(getSourceId(), zone); if (lkiTest != null) { return true; } } MageObject object; UUID parameterSourceId; // for singleton abilities like Flying we can't rely on abilities' source // so will use the one that came as a parameter if it is not null if (this instanceof MageSingleton && source != null) { object = source; parameterSourceId = source.getId(); } else { object = game.getObject(getSourceId()); parameterSourceId = getSourceId(); } if (object != null && !object.getAbilities().contains(this)) { boolean found = false; // unfortunately we need to handle double faced cards separately and only this way if (object instanceof PermanentCard && ((PermanentCard)object).canTransform()) { PermanentCard permanent = (PermanentCard)object; found = permanent.getSecondCardFace().getAbilities().contains(this) || permanent.getCard().getAbilities().contains(this); } if (!found) { return false; } } // check against current state Zone test = game.getState().getZone(parameterSourceId); return test != null && zone.match(test); } @Override public String toString() { return getRule(); } @Override public boolean getRuleAtTheTop() { return ruleAtTheTop; } @Override public void setRuleAtTheTop(boolean ruleAtTheTop) { this.ruleAtTheTop = ruleAtTheTop; } @Override public boolean getRuleVisible() { return ruleVisible; } @Override public void setRuleVisible(boolean ruleVisible) { this.ruleVisible = ruleVisible; } @Override public UUID getOriginalId() { return this.originalId; } }