/* * 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 java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.UUID; import mage.MageObject; import mage.MageObjectReference; import mage.Mana; import mage.abilities.costs.*; import mage.abilities.costs.common.PayLifeCost; import mage.abilities.costs.common.TapSourceCost; import mage.abilities.costs.mana.*; import mage.abilities.effects.ContinuousEffect; import mage.abilities.effects.Effect; import mage.abilities.effects.Effects; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.DynamicManaEffect; import mage.abilities.effects.common.ManaEffect; import mage.abilities.keyword.FlashbackAbility; import mage.abilities.mana.ActivatedManaAbilityImpl; import mage.cards.Card; import mage.constants.*; import mage.game.Game; import mage.game.command.Emblem; import mage.game.events.GameEvent; import mage.game.events.ManaEvent; import mage.game.permanent.Permanent; import mage.game.stack.Spell; import mage.game.stack.StackAbility; import mage.players.Player; import mage.target.Target; import mage.target.Targets; import mage.util.GameLog; import mage.util.ThreadLocalStringBuilder; import mage.watchers.Watcher; import org.apache.log4j.Logger; /** * @author BetaSteward_at_googlemail.com */ public abstract class AbilityImpl implements Ability { private static final Logger logger = Logger.getLogger(AbilityImpl.class); private static final ThreadLocalStringBuilder threadLocalBuilder = new ThreadLocalStringBuilder(100); private static final List<Watcher> emptyWatchers = new ArrayList<>(); private static final List<Ability> emptyAbilities = new ArrayList<>(); protected UUID id; protected UUID originalId; protected AbilityType abilityType; protected UUID controllerId; protected UUID sourceId; protected ManaCosts<ManaCost> manaCosts; protected ManaCosts<ManaCost> manaCostsToPay; protected Costs<Cost> costs; protected Costs<Cost> optionalCosts; protected Modes modes; protected Zone zone; protected String name; protected AbilityWord abilityWord; protected boolean usesStack = true; protected boolean ruleAtTheTop = false; protected boolean ruleVisible = true; protected boolean ruleAdditionalCostsVisible = true; protected boolean costModificationActive = true; protected boolean activated = false; protected boolean worksFaceDown = false; protected MageObject sourceObject; protected int sourceObjectZoneChangeCounter; protected List<Watcher> watchers = null; protected List<Ability> subAbilities = null; protected boolean canFizzle = true; 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(); if (ability.watchers != null) { this.watchers = new ArrayList<>(); for (Watcher watcher : ability.watchers) { watchers.add(watcher.copy()); } } if (ability.subAbilities != null) { this.subAbilities = new ArrayList<>(); for (Ability subAbility : ability.subAbilities) { subAbilities.add(subAbility.copy()); } } this.modes = ability.getModes().copy(); this.ruleAtTheTop = ability.ruleAtTheTop; this.ruleVisible = ability.ruleVisible; this.ruleAdditionalCostsVisible = ability.ruleAdditionalCostsVisible; this.costModificationActive = ability.costModificationActive; this.worksFaceDown = ability.worksFaceDown; this.abilityWord = ability.abilityWord; this.sourceObject = ability.sourceObject; this.sourceObjectZoneChangeCounter = ability.sourceObjectZoneChangeCounter; this.canFizzle = ability.canFizzle; } @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) { boolean effectResult = effect.apply(game, this); result &= effectResult; if (logger.isDebugEnabled()) { if (this.getAbilityType() != AbilityType.MANA) { if (!effectResult) { if (this.getSourceId() != null) { MageObject mageObject = game.getObject(this.getSourceId()); if (mageObject != null) { logger.debug("AbilityImpl.resolve: object: " + mageObject.getName()); } } logger.debug("AbilityImpl.resolve: effect returned false -" + effect.getText(this.getModes().getMode())); } } } } else { game.addEffect((ContinuousEffect) effect, this); } /** * All restrained trigger events are fired now. To restrain the * events is mainly neccessary because of the movement of * multiple object at once. If the event is fired directly as * one object moved, other objects are not already in the * correct zone to check for their effects. (e.g. Valakut, the * Molten Pinnacle) */ game.getState().handleSimultaneousEvent(game); game.resetShortLivingLKI(); /** * game.applyEffects() has to be done at least for every effect * that moves cards/permanent between zones, or changes control * of objects so Static effects work as intened if dependant * from the moved objects zone it is in Otherwise for example * were static abilities with replacement effects deactivated * too late Example: * {@link org.mage.test.cards.replacement.DryadMilitantTest#testDiesByDestroy testDiesByDestroy} */ if (effect.applyEffectsAfter()) { game.applyEffects(); game.getState().getTriggers().checkStateTriggers(game); } } } return result; } @Override public boolean activate(Game game, boolean noMana) { Player controller = game.getPlayer(this.getControllerId()); if (controller == null) { return false; } game.applyEffects(); /* 20130201 - 601.2b * If the spell is modal the player announces the mode choice (see rule 700.2). */ if (!getModes().choose(game, this)) { return false; } getSourceObject(game); if (controller.isTestMode()) { if (!controller.addTargets(this, game)) { 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 == AbilityType.SPELL) { game.getContinuousEffects().applySpliceEffects(this, game); } // if ability can be cast for no mana, clear the mana costs now, because additional mana costs must be paid. // For Flashback ability can be set X before, so the X costs have to be restored for the flashbacked ability if (noMana) { if (!this.getManaCostsToPay().getVariableCosts().isEmpty()) { int xValue = this.getManaCostsToPay().getX(); this.getManaCostsToPay().clear(); VariableManaCost xCosts = new VariableManaCost(); xCosts.setAmount(xValue); this.getManaCostsToPay().add(xCosts); } else { this.getManaCostsToPay().clear(); } } if (modes.getAdditionalCost() != null) { ((OptionalAdditionalModeSourceCosts) modes.getAdditionalCost()).addOptionalAdditionalModeCosts(this, game); } // 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 (!activateAlternateOrAdditionalCosts(sourceObject, noMana, controller, game)) { if (getAbilityType() == AbilityType.SPELL && ((SpellAbility) this).getSpellAbilityType() == SpellAbilityType.FACE_DOWN_CREATURE) { return false; } } // 117.6. Some mana costs contain no mana symbols. This represents an unpayable cost. An ability can // also have an unpayable cost if its cost is based on the mana cost of an object with no mana cost. // Attempting to cast a spell or activate an ability that has an unpayable cost is a legal action. // However, attempting to pay an unpayable cost is an illegal action. // // We apply this now, *AFTER* the user has made the choice to pay an alternative cost for the // spell. You can also still cast a spell with an unplayable cost by... not paying it's mana cost. //if (getAbilityType() == AbilityType.SPELL && getManaCostsToPay().isEmpty() && !noMana) { // return false; //} if (getAbilityType() == AbilityType.SPELL && (getManaCostsToPay().isEmpty() && getCosts().isEmpty()) && !noMana) { return false; } // 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 = handleManaXCosts(game, noMana, controller); String announceString = handleOtherXCosts(game, controller); // For effects from cards like Void Winnower x costs have to be set if (this.getAbilityType() == AbilityType.SPELL && game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.CAST_SPELL_LATE, getId(), getSourceId(), getControllerId()), this)) { return false; } handlePhyrexianManaCosts(game, sourceId, controller); for (UUID modeId : this.getModes().getSelectedModes()) { this.getModes().setActiveMode(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 (sourceObject != null && this.getAbilityType() != AbilityType.TRIGGERED) { // triggered abilities check this already in playerImpl.triggerAbility sourceObject.adjustTargets(this, game); } // Flashback abilities haven't made the choices the underlying spell might need for targeting. if (!(this instanceof FlashbackAbility) && !getTargets().isEmpty()) { Outcome outcome = getEffects().isEmpty() ? Outcome.Detriment : getEffects().get(0).getOutcome(); if (getTargets().chooseTargets(outcome, this.controllerId, this, noMana, game) == false) { if ((variableManaCost != null || announceString != null) && !game.isSimulation()) { game.informPlayer(controller, (sourceObject != null ? sourceObject.getIdName() : "") + ": no valid targets"); } return false; // when activation of ability is canceled during target selection } } } // 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 (controller.chooseUse(Outcome.Benefit, "Pay optional cost " + cost.getText() + '?', this, game)) { manaCostsToPay.add((ManaCost) cost); } } } //20100716 - 601.2e if (sourceObject != null) { sourceObject.adjustCosts(this, game); if (sourceObject instanceof Card) { for (Ability ability : ((Card) sourceObject).getAbilities(game)) { if (ability instanceof AdjustingSourceCosts) { ((AdjustingSourceCosts) ability).adjustCosts(this, game); } } } else { for (Ability ability : sourceObject.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 ActivatedManaAbilityImpl && !costs.pay(this, game, sourceId, controllerId, noMana, null)) { logger.debug("activate mana ability failed - non mana costs"); return false; } //20101001 - 601.2e if (costModificationActive) { game.getContinuousEffects().costModification(this, game); } else { costModificationActive = true; } UUID activatorId = controllerId; if ((this instanceof ActivatedAbilityImpl) && ((ActivatedAbilityImpl) this).getActivatorId() != null) { activatorId = ((ActivatedAbilityImpl) this).getActivatorId(); } //20100716 - 601.2f (noMana is not used here, because mana costs were cleared for this ability before adding additional costs and applying cost modification effects) if (!manaCostsToPay.pay(this, game, sourceId, activatorId, false, null)) { return false; // cancel during mana payment } //20100716 - 601.2g if (!costs.pay(this, game, sourceId, activatorId, noMana, null)) { logger.debug("activate failed - non mana costs"); return false; } if (!game.isSimulation()) { // inform about x costs now, so canceled announcements are not shown in the log if (announceString != null) { game.informPlayers(announceString); } if (variableManaCost != null) { int xValue = getManaCostsToPay().getX(); game.informPlayers(controller.getLogName() + " announces a value of " + xValue + " for " + variableManaCost.getText()); } } activated = true; // fire if tapped for mana (may only fire now because else costs of ability itself can be payed with mana of abilities that trigger for that event if (this.getAbilityType() == AbilityType.MANA) { for (Cost cost : costs) { if (cost instanceof TapSourceCost) { Mana mana = null; Effect effect = getEffects().get(0); if (effect instanceof DynamicManaEffect) { mana = ((DynamicManaEffect) effect).getMana(game, this); } else if (effect instanceof ManaEffect) { mana = ((ManaEffect) effect).getMana(game, this); } if (mana != null && mana.getAny() == 0) { // if mana == null or Any > 0 the event has to be fired in the mana effect to know which mana was produced ManaEvent event = new ManaEvent(GameEvent.EventType.TAPPED_FOR_MANA, sourceId, sourceId, controllerId, mana); if (!game.replaceEvent(event)) { game.fireEvent(event); } } break; } } } return true; } @Override public boolean isActivated() { return activated; } @Override public boolean activateAlternateOrAdditionalCosts(MageObject sourceObject, boolean noMana, Player controller, Game game) { boolean alternativeCostisUsed = false; if (sourceObject != null && !(sourceObject instanceof Permanent) && !(this instanceof FlashbackAbility)) { Abilities<Ability> abilities = null; if (sourceObject instanceof Card) { abilities = ((Card) sourceObject).getAbilities(game); } else { sourceObject.getAbilities(); } if (abilities != null) { for (Ability ability : abilities) { // if cast for noMana no Alternative costs are allowed if (!noMana && ability instanceof AlternativeSourceCosts) { AlternativeSourceCosts alternativeSpellCosts = (AlternativeSourceCosts) ability; if (alternativeSpellCosts.isAvailable(this, game)) { if (alternativeSpellCosts.askToActivateAlternativeCosts(this, game)) { // only one alternative costs may be activated alternativeCostisUsed = true; break; } } } if (ability instanceof OptionalAdditionalSourceCosts) { ((OptionalAdditionalSourceCosts) ability).addOptionalAdditionalCosts(this, game); } } } // controller specific alternate spell costs if (!noMana && !alternativeCostisUsed) { if (this.getAbilityType() == AbilityType.SPELL // 117.9a Only one alternative cost can be applied to any one spell as it’s being cast. // So an alternate spell ability can't be paid with Omniscience && ((SpellAbility) this).getSpellAbilityType() != SpellAbilityType.BASE_ALTERNATE) { for (AlternativeSourceCosts alternativeSourceCosts : controller.getAlternativeSourceCosts()) { if (alternativeSourceCosts.isAvailable(this, game)) { if (alternativeSourceCosts.askToActivateAlternativeCosts(this, game)) { // only one alternative costs may be activated alternativeCostisUsed = true; break; } } } } } } return alternativeCostisUsed; } /** * Handles the setting of non mana X costs * * @param controller * @param game * @return announce message */ protected String handleOtherXCosts(Game game, Player controller) { StringBuilder announceString = new StringBuilder(); for (VariableCost variableCost : this.costs.getVariableCosts()) { if (!(variableCost instanceof VariableManaCost) && !((Cost) variableCost).isPaid()) { int xValue = variableCost.announceXValue(this, game); Cost fixedCost = variableCost.getFixedCostsFromAnnouncedValue(xValue); if (fixedCost != null) { costs.add(fixedCost); } // set the xcosts to paid variableCost.setAmount(xValue); ((Cost) variableCost).setPaid(); String message = controller.getLogName() + " announces a value of " + xValue + " (" + variableCost.getActionText() + ')'; announceString.append(message); } } return announceString.toString(); } /** * 601.2b If a cost that will be paid as the spell is being cast includes * Phyrexian mana symbols, the player announces whether he or she intends to * pay 2 life or the corresponding colored mana cost for each of those * symbols. */ private void handlePhyrexianManaCosts(Game game, UUID sourceId, Player controller) { Iterator<ManaCost> costIterator = manaCostsToPay.iterator(); while (costIterator.hasNext()) { ManaCost cost = costIterator.next(); if (cost instanceof PhyrexianManaCost) { PhyrexianManaCost phyrexianManaCost = (PhyrexianManaCost) cost; PayLifeCost payLifeCost = new PayLifeCost(2); if (payLifeCost.canPay(this, sourceId, controller.getId(), game) && controller.chooseUse(Outcome.LoseLife, "Pay 2 life instead of " + phyrexianManaCost.getBaseText() + '?', this, game)) { costIterator.remove(); costs.add(payLifeCost); } } } } /** * Handles X mana costs and sets manaCostsToPay. * * @param game * @param noMana * @param controller * @return variableManaCost for posting to log later */ protected VariableManaCost handleManaXCosts(Game game, boolean noMana, Player controller) { // 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 = controller.announceXMana(variableManaCost.getMinX(), variableManaCost.getMaxX(), "Announce the value for " + variableManaCost.getText(), game, this); int amountMana = xValue * variableManaCost.getMultiplier(); StringBuilder manaString = threadLocalBuilder.get(); if (variableManaCost.getFilter() == null || variableManaCost.getFilter().isGeneric()) { manaString.append('{').append(amountMana).append('}'); } else { String manaSymbol = null; if (variableManaCost.getFilter().isBlack()) { if (variableManaCost.getFilter().isRed()) { manaSymbol = "B/R"; } else { manaSymbol = "B"; } } else if (variableManaCost.getFilter().isRed()) { manaSymbol = "R"; } else if (variableManaCost.getFilter().isBlue()) { manaSymbol = "U"; } else if (variableManaCost.getFilter().isGreen()) { manaSymbol = "G"; } else if (variableManaCost.getFilter().isWhite()) { manaSymbol = "W"; } if (manaSymbol == null) { throw new UnsupportedOperationException("ManaFilter is not supported: " + this.toString()); } for (int i = 0; i < amountMana; i++) { manaString.append('{').append(manaSymbol).append('}'); } } manaCostsToPay.add(new ManaCostsImpl(manaString.toString())); manaCostsToPay.setX(amountMana); } variableManaCost.setPaid(); } } return variableManaCost; } // called at end of turn for each Permanent @Override public void reset(Game game) { } @Override public boolean checkIfClause(Game game) { return true; } @Override public UUID getControllerId() { return controllerId; } @Override public void setControllerId(UUID controllerId) { this.controllerId = controllerId; if (watchers != null) { for (Watcher watcher : watchers) { watcher.setControllerId(controllerId); } } if (subAbilities != null) { for (Ability subAbility : subAbilities) { subAbility.setControllerId(controllerId); } } } @Override public UUID getSourceId() { return sourceId; } @Override public void setSourceId(UUID sourceId) { if (this.sourceId == null) { this.sourceId = sourceId; } else if (!(this instanceof MageSingleton)) { this.sourceId = sourceId; } if (subAbilities != null) { for (Ability subAbility : subAbilities) { subAbility.setSourceId(sourceId); } } if (watchers != null) { for (Watcher watcher : watchers) { watcher.setSourceId(sourceId); } } } @Override public Costs<Cost> getCosts() { return costs; } @Override public ManaCosts<ManaCost> 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<ManaCost> getManaCostsToPay() { return manaCostsToPay; } @Override public Costs<Cost> getOptionalCosts() { return optionalCosts; } @Override public Effects getEffects() { return getModes().getMode().getEffects(); } @Override public Effects getAllEffects() { Effects allEffects = new Effects(); for (Mode mode : getModes().values()) { allEffects.addAll(mode.getEffects()); } return allEffects; } @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 Zone getZone() { return zone; } @Override public List<Watcher> getWatchers() { if (watchers != null) { return watchers; } else { return emptyWatchers; } } @Override public void addWatcher(Watcher watcher) { if (watchers == null) { watchers = new ArrayList<>(); } watcher.setSourceId(this.sourceId); watcher.setControllerId(this.controllerId); watchers.add(watcher); } @Override public List<Ability> getSubAbilities() { if (subAbilities != null) { return subAbilities; } else { return emptyAbilities; } } @Override public void addSubAbility(Ability ability) { if (subAbilities == null) { subAbilities = new ArrayList<>(); } ability.setSourceId(this.sourceId); ability.setControllerId(this.controllerId); subAbilities.add(ability); } @Override public boolean isUsesStack() { return usesStack; } @Override public String getRule() { return getRule(false); } @Override public String getRule(boolean all) { StringBuilder sbRule = threadLocalBuilder.get(); if (all || this.abilityType != AbilityType.SPELL) { if (!manaCosts.isEmpty()) { sbRule.append(manaCosts.getText()); } if (!costs.isEmpty()) { if (sbRule.length() > 0) { sbRule.append(','); } sbRule.append(costs.getText()); } if (sbRule.length() > 0) { sbRule.append(": "); } } String ruleStart = sbRule.toString(); String text = getModes().getText(); String rule; if (!text.isEmpty()) { if (ruleStart.length() > 1) { String end = ruleStart.substring(ruleStart.length() - 2).trim(); if (end.isEmpty() || end.equals(":") || end.equals(".")) { rule = ruleStart + Character.toUpperCase(text.charAt(0)) + text.substring(1); } else { rule = ruleStart + text; } } else { rule = ruleStart + text; } } else { rule = ruleStart; } if (abilityWord != null) { rule = "<i>" + abilityWord + "</i> — " + Character.toUpperCase(rule.charAt(0)) + rule.substring(1); } return rule; } @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 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 Targets getTargets() { return getModes().getMode().getTargets(); } @Override public UUID getFirstTarget() { return getTargets().getFirstTarget(); } @Override public boolean isModal() { return getModes().size() > 1; } @Override public void addMode(Mode mode) { getModes().addMode(mode); } @Override public Modes getModes() { return modes; } @Override public boolean canChooseTarget(Game game) { int found = 0; for (Mode mode : getModes().values()) { if (mode.getTargets().canChoose(sourceId, controllerId, game)) { found++; if (getModes().isEachModeMoreThanOnce()) { return true; } if (found >= getModes().getMinModes()) { return true; } } } return false; } /** * @param game * @param source * @return */ @Override public boolean isInUseableZone(Game game, MageObject source, GameEvent event) { if (!this.hasSourceObjectAbility(game, source, event)) { return false; } if (zone == Zone.COMMAND) { if (this.getSourceId() == null) { // commander effects return true; } MageObject object = game.getObject(this.getSourceId()); // emblem are always actual if (object != null && object instanceof Emblem) { return true; } } UUID parameterSourceId; // for singleton abilities like Flying we can't rely on abilities' source because it's only once in continuous effects // so will use the sourceId of the object itself that came as a parameter if it is not null if (this instanceof MageSingleton && source != null) { parameterSourceId = source.getId(); } else { parameterSourceId = getSourceId(); } // check agains shortLKI for effects that move multiple object at the same time (e.g. destroy all) if (game.getShortLivingLKI(getSourceId(), getZone())) { return true; } // check against current state Zone test = game.getState().getZone(parameterSourceId); return test != null && zone.match(test); } @Override public boolean hasSourceObjectAbility(Game game, MageObject source, GameEvent event) { MageObject object = source; // for singleton abilities like Flying we can't rely on abilities' source because it's only once in continuous effects // so will use the sourceId of the object itself that came as a parameter if it is not null if (object == null) { object = game.getPermanentEntering(getSourceId()); if (object == null) { object = game.getObject(getSourceId()); } } if (object != null) { if (object instanceof Permanent) { if (!((Permanent) object).getAbilities(game).contains(this)) { return false; } return ((Permanent) object).isPhasedIn(); } else if (!object.getAbilities().contains(this)) { // check if it's an ability that is temporary gained to a card Abilities<Ability> otherAbilities = game.getState().getAllOtherAbilities(this.getSourceId()); if (otherAbilities == null || !otherAbilities.contains(this)) { return false; } } } return true; } @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) { if (!(this instanceof MageSingleton)) { // prevent to change singletons this.ruleVisible = ruleVisible; } } @Override public boolean getAdditionalCostsRuleVisible() { return ruleAdditionalCostsVisible; } @Override public void setAdditionalCostsRuleVisible(boolean ruleAdditionalCostsVisible) { this.ruleAdditionalCostsVisible = ruleAdditionalCostsVisible; } @Override public UUID getOriginalId() { return this.originalId; } @Override public void setAbilityWord(AbilityWord abilityWord) { this.abilityWord = abilityWord; } @Override public String getGameLogMessage(Game game) { if (game.isSimulation()) { return ""; } MageObject object = game.getObject(this.sourceId); if (object == null) { // e.g. sacrificed token logger.warn("Could get no object: " + this.toString()); } return new StringBuilder(" activates: ") .append(object != null ? this.formatRule(getModes().getText(), object.getLogName()) : getModes().getText()) .append(" from ") .append(getMessageText(game)).toString(); } protected String getMessageText(Game game) { StringBuilder sb = threadLocalBuilder.get(); MageObject object = game.getObject(this.sourceId); if (object != null) { if (object instanceof StackAbility) { Card card = game.getCard(((StackAbility) object).getSourceId()); if (card != null) { sb.append(GameLog.getColoredObjectIdName(card)); } else { sb.append(GameLog.getColoredObjectIdName(object)); } } else if (object instanceof Spell) { Spell spell = (Spell) object; String castText = spell.getSpellCastText(game); sb.append((castText.startsWith("Cast ") ? castText.substring(5) : castText)); if (spell.getFromZone() == Zone.GRAVEYARD) { sb.append(" from graveyard"); } sb.append(getOptionalTextSuffix(game, spell)); } else { sb.append(GameLog.getColoredObjectIdName(object)); } } else { sb.append("unknown"); } if (object instanceof Spell && ((Spell) object).getSpellAbilities().size() > 1) { if (((Spell) object).getSpellAbility().getSpellAbilityType() == SpellAbilityType.SPLIT_FUSED) { Spell spell = (Spell) object; int i = 0; for (SpellAbility spellAbility : spell.getSpellAbilities()) { i++; String half; if (i == 1) { half = " left"; } else { half = " right"; } if (!spellAbility.getTargets().isEmpty()) { sb.append(half).append(" half targeting "); for (Target target : spellAbility.getTargets()) { sb.append(target.getTargetedName(game)); } } } } else { Spell spell = (Spell) object; int i = 0; for (SpellAbility spellAbility : spell.getSpellAbilities()) { i++; if (i > 1) { sb.append(" splicing "); if (spellAbility.name.length() > 5 && spellAbility.name.startsWith("Cast ")) { sb.append(spellAbility.name.substring(5)); } else { sb.append(spellAbility.name); } } sb.append(getTargetDescriptionForLog(spellAbility.getTargets(), game)); } } } else if (object instanceof Spell && ((Spell) object).getSpellAbility().getModes().size() > 1) { Modes spellModes = ((Spell) object).getSpellAbility().getModes(); for (UUID selectedModeId : spellModes.getSelectedModes()) { Mode selectedMode = spellModes.get(selectedModeId); int item = 0; for (Mode mode : spellModes.values()) { item++; if (mode.getId().equals(selectedMode.getId())) { sb.append(" (mode ").append(item).append(')'); sb.append(getTargetDescriptionForLog(selectedMode.getTargets(), game)); break; } } } } else { sb.append(getTargetDescriptionForLog(getTargets(), game)); } return sb.toString(); } @Override public String getTargetDescription(Targets targets, Game game) { return getTargetDescriptionForLog(targets, game); } protected String getTargetDescriptionForLog(Targets targets, Game game) { StringBuilder sb = new StringBuilder(); // threadLocal StringBuilder can't be used because calling method already uses it if (!targets.isEmpty()) { String usedVerb = null; for (Target target : targets) { if (!target.getTargets().isEmpty()) { if (!target.isNotTarget()) { if (usedVerb == null || usedVerb.equals(" choosing ")) { usedVerb = " targeting "; sb.append(usedVerb); } } else if (target.isNotTarget() && (usedVerb == null || usedVerb.equals(" targeting "))) { usedVerb = " choosing "; sb.append(usedVerb); } sb.append(target.getTargetedName(game)); } } } return sb.toString(); } private String getOptionalTextSuffix(Game game, Spell spell) { StringBuilder sb = new StringBuilder(); for (Ability ability : spell.getAbilities()) { if (ability instanceof OptionalAdditionalSourceCosts) { sb.append(((OptionalAdditionalSourceCosts) ability).getCastMessageSuffix()); } if (ability instanceof AlternativeSourceCosts && ((AlternativeSourceCosts) ability).isActivated(this, game)) { sb.append(((AlternativeSourceCosts) ability).getCastMessageSuffix(game)); } } return sb.toString(); } @Override public void setCostModificationActive(boolean active) { this.costModificationActive = active; } @Override public boolean getWorksFaceDown() { return worksFaceDown; } @Override public void setWorksFaceDown(boolean worksFaceDown) { this.worksFaceDown = worksFaceDown; } @Override public MageObject getSourceObject(Game game) { if (sourceObject == null) { setSourceObject(null, game); if (sourceObject == null) { logger.warn("Source object could not be retrieved: " + this.getRule()); } } return sourceObject; } @Override public MageObject getSourceObjectIfItStillExists(Game game) { MageObject currentObject = game.getObject(getSourceId()); if (currentObject != null) { if (sourceObject == null) { setSourceObject(currentObject, game); } MageObjectReference mor = new MageObjectReference(currentObject, game); if (mor.getZoneChangeCounter() == getSourceObjectZoneChangeCounter()) { // source object has meanwhile not changed zone return currentObject; } } return null; } @Override public int getSourceObjectZoneChangeCounter() { return sourceObjectZoneChangeCounter; } @Override public void setSourceObject(MageObject sourceObject, Game game) { if (sourceObject == null) { this.sourceObject = game.getObject(sourceId); this.sourceObjectZoneChangeCounter = game.getState().getZoneChangeCounter(sourceId); } else { this.sourceObject = sourceObject; this.sourceObjectZoneChangeCounter = this.sourceObject.getZoneChangeCounter(game); } } @Override public boolean canFizzle() { return canFizzle; } @Override public void setCanFizzle(boolean canFizzle) { this.canFizzle = canFizzle; } }