/* * 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.effects; import java.io.Serializable; import java.util.*; import java.util.Map.Entry; import mage.MageObject; import mage.abilities.*; import mage.abilities.keyword.SpliceOntoArcaneAbility; import mage.cards.Cards; import mage.cards.CardsImpl; import mage.constants.*; import mage.filter.FilterCard; import mage.filter.predicate.Predicate; import mage.filter.predicate.Predicates; import mage.filter.predicate.mageobject.CardIdPredicate; import mage.game.Game; import mage.game.events.GameEvent; import mage.game.events.GameEvent.EventType; import mage.game.events.ZoneChangeEvent; import mage.game.permanent.Permanent; import mage.game.permanent.PermanentCard; import mage.game.stack.Spell; import mage.players.ManaPoolItem; import mage.players.Player; import mage.target.common.TargetCardInHand; import org.apache.log4j.Logger; /** * @author BetaSteward_at_googlemail.com */ public class ContinuousEffects implements Serializable { private static final Logger logger = Logger.getLogger(ContinuousEffects.class); private long order = 0; //transient Continuous effects private ContinuousEffectsList<ContinuousEffect> layeredEffects = new ContinuousEffectsList<>(); private ContinuousEffectsList<ContinuousRuleModifyingEffect> continuousRuleModifyingEffects = new ContinuousEffectsList<>(); private ContinuousEffectsList<ReplacementEffect> replacementEffects = new ContinuousEffectsList<>(); private ContinuousEffectsList<PreventionEffect> preventionEffects = new ContinuousEffectsList<>(); private ContinuousEffectsList<RequirementEffect> requirementEffects = new ContinuousEffectsList<>(); private ContinuousEffectsList<RestrictionEffect> restrictionEffects = new ContinuousEffectsList<>(); private ContinuousEffectsList<RestrictionUntapNotMoreThanEffect> restrictionUntapNotMoreThanEffects = new ContinuousEffectsList<>(); private ContinuousEffectsList<CostModificationEffect> costModificationEffects = new ContinuousEffectsList<>(); private ContinuousEffectsList<SpliceCardEffect> spliceCardEffects = new ContinuousEffectsList<>(); private final Map<AsThoughEffectType, ContinuousEffectsList<AsThoughEffect>> asThoughEffectsMap = new EnumMap<>(AsThoughEffectType.class); public final List<ContinuousEffectsList<?>> allEffectsLists = new ArrayList<>(); private final ApplyCountersEffect applyCounters; private final PlaneswalkerRedirectionEffect planeswalkerRedirectionEffect; private final AuraReplacementEffect auraReplacementEffect; private final List<ContinuousEffect> previous = new ArrayList<>(); // note all effect/abilities that were only added temporary private final Map<ContinuousEffect, Set<Ability>> temporaryEffects = new HashMap<>(); private final ContinuousEffectSorter sorter = new ContinuousEffectSorter(); public ContinuousEffects() { applyCounters = new ApplyCountersEffect(); planeswalkerRedirectionEffect = new PlaneswalkerRedirectionEffect(); auraReplacementEffect = new AuraReplacementEffect(); collectAllEffects(); } public ContinuousEffects(final ContinuousEffects effect) { this.applyCounters = effect.applyCounters.copy(); this.planeswalkerRedirectionEffect = effect.planeswalkerRedirectionEffect.copy(); this.auraReplacementEffect = effect.auraReplacementEffect.copy(); layeredEffects = effect.layeredEffects.copy(); continuousRuleModifyingEffects = effect.continuousRuleModifyingEffects.copy(); replacementEffects = effect.replacementEffects.copy(); preventionEffects = effect.preventionEffects.copy(); requirementEffects = effect.requirementEffects.copy(); restrictionEffects = effect.restrictionEffects.copy(); restrictionUntapNotMoreThanEffects = effect.restrictionUntapNotMoreThanEffects.copy(); for (Map.Entry<AsThoughEffectType, ContinuousEffectsList<AsThoughEffect>> entry : effect.asThoughEffectsMap.entrySet()) { asThoughEffectsMap.put(entry.getKey(), entry.getValue().copy()); } costModificationEffects = effect.costModificationEffects.copy(); spliceCardEffects = effect.spliceCardEffects.copy(); temporaryEffects.putAll(effect.temporaryEffects); collectAllEffects(); order = effect.order; } private void collectAllEffects() { allEffectsLists.add(layeredEffects); allEffectsLists.add(continuousRuleModifyingEffects); allEffectsLists.add(replacementEffects); allEffectsLists.add(preventionEffects); allEffectsLists.add(requirementEffects); allEffectsLists.add(restrictionEffects); allEffectsLists.add(restrictionUntapNotMoreThanEffects); allEffectsLists.add(costModificationEffects); allEffectsLists.add(spliceCardEffects); for (ContinuousEffectsList continuousEffectsList : asThoughEffectsMap.values()) { allEffectsLists.add(continuousEffectsList); } } public ContinuousEffects copy() { return new ContinuousEffects(this); } public List<RequirementEffect> getRequirementEffects() { return requirementEffects; } public List<RestrictionEffect> getRestrictionEffects() { return restrictionEffects; } public void removeEndOfCombatEffects() { layeredEffects.removeEndOfCombatEffects(); continuousRuleModifyingEffects.removeEndOfCombatEffects(); replacementEffects.removeEndOfCombatEffects(); preventionEffects.removeEndOfCombatEffects(); requirementEffects.removeEndOfCombatEffects(); restrictionEffects.removeEndOfCombatEffects(); for (ContinuousEffectsList asThoughtlist : asThoughEffectsMap.values()) { asThoughtlist.removeEndOfCombatEffects(); } costModificationEffects.removeEndOfCombatEffects(); spliceCardEffects.removeEndOfCombatEffects(); } public void removeEndOfTurnEffects() { layeredEffects.removeEndOfTurnEffects(); continuousRuleModifyingEffects.removeEndOfTurnEffects(); replacementEffects.removeEndOfTurnEffects(); preventionEffects.removeEndOfTurnEffects(); requirementEffects.removeEndOfTurnEffects(); restrictionEffects.removeEndOfTurnEffects(); for (ContinuousEffectsList asThoughtlist : asThoughEffectsMap.values()) { asThoughtlist.removeEndOfTurnEffects(); } costModificationEffects.removeEndOfTurnEffects(); spliceCardEffects.removeEndOfTurnEffects(); } public void removeInactiveEffects(Game game) { layeredEffects.removeInactiveEffects(game); continuousRuleModifyingEffects.removeInactiveEffects(game); replacementEffects.removeInactiveEffects(game); preventionEffects.removeInactiveEffects(game); requirementEffects.removeInactiveEffects(game); restrictionEffects.removeInactiveEffects(game); restrictionUntapNotMoreThanEffects.removeInactiveEffects(game); for (ContinuousEffectsList asThoughtlist : asThoughEffectsMap.values()) { asThoughtlist.removeInactiveEffects(game); } costModificationEffects.removeInactiveEffects(game); spliceCardEffects.removeInactiveEffects(game); } public List<ContinuousEffect> getLayeredEffects(Game game) { List<ContinuousEffect> layerEffects = new ArrayList<>(); for (ContinuousEffect effect : layeredEffects) { switch (effect.getDuration()) { case WhileOnBattlefield: case WhileOnStack: case WhileInGraveyard: HashSet<Ability> abilities = layeredEffects.getAbility(effect.getId()); if (!abilities.isEmpty()) { for (Ability ability : abilities) { // If e.g. triggerd abilities (non static) created the effect, the ability must not be in usable zone (e.g. Unearth giving Haste effect) if (!(ability instanceof StaticAbility) || ability.isInUseableZone(game, null, null)) { layerEffects.add(effect); break; } } } else { logger.error("No abilities for continuous effect: " + effect.toString()); } break; default: layerEffects.add(effect); } } updateTimestamps(layerEffects); layerEffects.sort(sorter); return layerEffects; } /** * Initially effect timestamp is set when game starts in game.loadCard * method. After that timestamp should be updated whenever effect becomes * "actual" meaning it becomes turned on that is defined by * Ability.#isInUseableZone(Game, boolean) method in * #getLayeredEffects(Game). * * @param layerEffects */ private void updateTimestamps(List<ContinuousEffect> layerEffects) { for (ContinuousEffect continuousEffect : layerEffects) { // check if it's new, then set order if (!previous.contains(continuousEffect)) { setOrder(continuousEffect); } } previous.clear(); previous.addAll(layerEffects); } public void setOrder(ContinuousEffect effect) { effect.setOrder(order++); } private List<ContinuousEffect> filterLayeredEffects(List<ContinuousEffect> effects, Layer layer) { List<ContinuousEffect> layerEffects = new ArrayList<>(); for (ContinuousEffect effect : effects) { if (effect.hasLayer(layer)) { layerEffects.add(effect); } } return layerEffects; } public HashMap<RequirementEffect, HashSet<Ability>> getApplicableRequirementEffects(Permanent permanent, Game game) { HashMap<RequirementEffect, HashSet<Ability>> effects = new HashMap<>(); for (RequirementEffect effect : requirementEffects) { HashSet<Ability> abilities = requirementEffects.getAbility(effect.getId()); HashSet<Ability> applicableAbilities = new HashSet<>(); for (Ability ability : abilities) { if (!(ability instanceof StaticAbility) || ability.isInUseableZone(game, ability instanceof MageSingleton ? permanent : null, null)) { if (effect.applies(permanent, ability, game)) { applicableAbilities.add(ability); } } } if (!applicableAbilities.isEmpty()) { effects.put(effect, abilities); } } return effects; } public HashMap<RestrictionEffect, HashSet<Ability>> getApplicableRestrictionEffects(Permanent permanent, Game game) { HashMap<RestrictionEffect, HashSet<Ability>> effects = new HashMap<>(); for (RestrictionEffect effect : restrictionEffects) { HashSet<Ability> abilities = restrictionEffects.getAbility(effect.getId()); HashSet<Ability> applicableAbilities = new HashSet<>(); for (Ability ability : abilities) { if (!(ability instanceof StaticAbility) || ability.isInUseableZone(game, ability instanceof MageSingleton ? permanent : null, null)) { if (effect.applies(permanent, ability, game)) { applicableAbilities.add(ability); } } } if (!applicableAbilities.isEmpty()) { effects.put(effect, abilities); } } return effects; } public HashMap<RestrictionUntapNotMoreThanEffect, HashSet<Ability>> getApplicableRestrictionUntapNotMoreThanEffects(Player player, Game game) { HashMap<RestrictionUntapNotMoreThanEffect, HashSet<Ability>> effects = new HashMap<>(); for (RestrictionUntapNotMoreThanEffect effect : restrictionUntapNotMoreThanEffects) { HashSet<Ability> abilities = restrictionUntapNotMoreThanEffects.getAbility(effect.getId()); HashSet<Ability> applicableAbilities = new HashSet<>(); for (Ability ability : abilities) { if (!(ability instanceof StaticAbility) || ability.isInUseableZone(game, null, null)) { if (effect.applies(player, ability, game)) { applicableAbilities.add(ability); } } } if (!applicableAbilities.isEmpty()) { effects.put(effect, abilities); } } return effects; } public boolean checkIfThereArePayCostToAttackBlockEffects(GameEvent event, Game game) { for (ReplacementEffect effect : replacementEffects) { if (!effect.checksEventType(event, game)) { continue; } if (effect instanceof PayCostToAttackBlockEffect) { HashSet<Ability> abilities = replacementEffects.getAbility(effect.getId()); for (Ability ability : abilities) { // for replacment effects of static abilities do not use LKI to check if to apply if (ability.getAbilityType() != AbilityType.STATIC || ability.isInUseableZone(game, null, event)) { if (effect.getDuration() != Duration.OneUse || !effect.isUsed()) { if (!game.getScopeRelevant() || effect.hasSelfScope() || !event.getTargetId().equals(ability.getSourceId())) { if (effect.applies(event, ability, game) && !((PayCostToAttackBlockEffect) effect).isCostless(event, ability, game)) { return true; } } } } } } } return false; } /** * @param event * @param game * @return a list of all {@link ReplacementEffect} that apply to the current * event */ private HashMap<ReplacementEffect, HashSet<Ability>> getApplicableReplacementEffects(GameEvent event, Game game) { HashMap<ReplacementEffect, HashSet<Ability>> replaceEffects = new HashMap<>(); if (planeswalkerRedirectionEffect.checksEventType(event, game) && planeswalkerRedirectionEffect.applies(event, null, game)) { replaceEffects.put(planeswalkerRedirectionEffect, null); } if (auraReplacementEffect.checksEventType(event, game) && auraReplacementEffect.applies(event, null, game)) { replaceEffects.put(auraReplacementEffect, null); } // boolean checkLKI = event.getType().equals(EventType.ZONE_CHANGE) || event.getType().equals(EventType.DESTROYED_PERMANENT); //get all applicable transient Replacement effects for (ReplacementEffect effect : replacementEffects) { if (!effect.checksEventType(event, game)) { continue; } if (event.getAppliedEffects() != null && event.getAppliedEffects().contains(effect.getId())) { // Effect already applied to this event, ignore it // TODO: Handle also gained effect that are connected to different abilities. continue; } HashSet<Ability> abilities = replacementEffects.getAbility(effect.getId()); HashSet<Ability> applicableAbilities = new HashSet<>(); for (Ability ability : abilities) { // for replacment effects of static abilities do not use LKI to check if to apply if (ability.getAbilityType() != AbilityType.STATIC || ability.isInUseableZone(game, null, event)) { if (effect.getDuration() != Duration.OneUse || !effect.isUsed()) { if (!game.getScopeRelevant() || effect.hasSelfScope() || !event.getTargetId().equals(ability.getSourceId())) { if (effect.applies(event, ability, game)) { applicableAbilities.add(ability); } } } } } if (!applicableAbilities.isEmpty()) { replaceEffects.put(effect, applicableAbilities); } } for (PreventionEffect effect : preventionEffects) { if (!effect.checksEventType(event, game)) { continue; } if (event.getAppliedEffects() != null && event.getAppliedEffects().contains(effect.getId())) { // Effect already applied to this event, ignore it // TODO: Handle also gained effect that are connected to different abilities. continue; } HashSet<Ability> abilities = preventionEffects.getAbility(effect.getId()); HashSet<Ability> applicableAbilities = new HashSet<>(); for (Ability ability : abilities) { if (ability.getAbilityType() != AbilityType.STATIC || ability.isInUseableZone(game, null, event)) { if (effect.getDuration() != Duration.OneUse || !effect.isUsed()) { if (effect.applies(event, ability, game)) { applicableAbilities.add(ability); } } } } if (!applicableAbilities.isEmpty()) { replaceEffects.put((ReplacementEffect) effect, applicableAbilities); } } return replaceEffects; } private boolean checkAbilityStillExists(Ability ability, ContinuousEffect effect, GameEvent event, Game game) { switch (effect.getDuration()) { // effects with fixed duration don't need an object with the source ability (e.g. a silence cast with isochronic Scepter has no more a card object case EndOfCombat: case EndOfGame: case EndOfStep: case EndOfTurn: case OneUse: case Custom: // custom duration means the effect ends itself if needed return true; } if (ability.getSourceId() == null) { // commander replacement effect return true; } MageObject object; if (event.getType() == EventType.ZONE_CHANGE && ((ZoneChangeEvent) event).getFromZone() == Zone.BATTLEFIELD && event.getTargetId().equals(ability.getSourceId())) { object = ((ZoneChangeEvent) event).getTarget(); } else { object = game.getObject(ability.getSourceId()); } if (object == null) { return false; } boolean exists = true; if (!object.getAbilities().contains(ability)) { exists = false; if (object instanceof PermanentCard) { PermanentCard permanent = (PermanentCard) object; if (permanent.isTransformable() && event.getType() == GameEvent.EventType.TRANSFORMED) { exists = permanent.getCard().getAbilities().contains(ability); } } } else if (object instanceof PermanentCard) { PermanentCard permanent = (PermanentCard) object; if (permanent.isFaceDown(game) && !ability.getWorksFaceDown()) { return false; } } else if (object instanceof Spell) { Spell spell = (Spell) object; if (spell.isFaceDown(game) && !ability.getWorksFaceDown()) { return false; } } return exists; } /** * Filters out cost modification effects that are not active. * * @param game * @return */ private List<CostModificationEffect> getApplicableCostModificationEffects(Game game) { List<CostModificationEffect> costEffects = new ArrayList<>(); for (CostModificationEffect effect : costModificationEffects) { HashSet<Ability> abilities = costModificationEffects.getAbility(effect.getId()); for (Ability ability : abilities) { if (!(ability instanceof StaticAbility) || ability.isInUseableZone(game, null, null)) { if (effect.getDuration() != Duration.OneUse || !effect.isUsed()) { costEffects.add(effect); break; } } } } return costEffects; } /** * Filters out splice effects that are not active. * * @param game * @return */ private List<SpliceCardEffect> getApplicableSpliceCardEffects(Game game, UUID playerId) { List<SpliceCardEffect> spliceEffects = new ArrayList<>(); for (SpliceCardEffect effect : spliceCardEffects) { HashSet<Ability> abilities = spliceCardEffects.getAbility(effect.getId()); for (Ability ability : abilities) { if (ability.getControllerId().equals(playerId) && (!(ability instanceof StaticAbility) || ability.isInUseableZone(game, null, null))) { if (effect.getDuration() != Duration.OneUse || !effect.isUsed()) { spliceEffects.add(effect); break; } } } } return spliceEffects; } public boolean asThough(UUID objectId, AsThoughEffectType type, UUID controllerId, Game game) { return asThough(objectId, type, null, controllerId, game); } public boolean asThough(UUID objectId, AsThoughEffectType type, Ability affectedAbility, UUID controllerId, Game game) { List<AsThoughEffect> asThoughEffectsList = getApplicableAsThoughEffects(type, game); for (AsThoughEffect effect : asThoughEffectsList) { HashSet<Ability> abilities = asThoughEffectsMap.get(type).getAbility(effect.getId()); for (Ability ability : abilities) { if (affectedAbility == null) { if (effect.applies(objectId, ability, controllerId, game)) { return true; } } else if (effect.applies(objectId, affectedAbility, ability, game)) { return true; } } } return false; } public ManaType asThoughMana(ManaType manaType, ManaPoolItem mana, UUID objectId, Ability affectedAbility, UUID controllerId, Game game) { // First check existing only effects List<AsThoughEffect> asThoughEffectsList = getApplicableAsThoughEffects(AsThoughEffectType.SPEND_ONLY_MANA, game); for (AsThoughEffect effect : asThoughEffectsList) { HashSet<Ability> abilities = asThoughEffectsMap.get(AsThoughEffectType.SPEND_ONLY_MANA).getAbility(effect.getId()); for (Ability ability : abilities) { if ((affectedAbility == null && effect.applies(objectId, ability, controllerId, game)) || effect.applies(objectId, affectedAbility, ability, game)) { if (((AsThoughManaEffect) effect).getAsThoughManaType(manaType, mana, controllerId, ability, game) == null) { return null; } } } } // then check effects that allow to use other mana types to pay the current mana type to pay asThoughEffectsList = getApplicableAsThoughEffects(AsThoughEffectType.SPEND_OTHER_MANA, game); for (AsThoughEffect effect : asThoughEffectsList) { HashSet<Ability> abilities = asThoughEffectsMap.get(AsThoughEffectType.SPEND_OTHER_MANA).getAbility(effect.getId()); for (Ability ability : abilities) { if ((affectedAbility == null && effect.applies(objectId, ability, controllerId, game)) || effect.applies(objectId, affectedAbility, ability, game)) { ManaType usableManaType = ((AsThoughManaEffect) effect).getAsThoughManaType(manaType, mana, controllerId, ability, game); if (usableManaType != null) { return usableManaType; } } } } return manaType; } /** * Filters out asThough effects that are not active. * * @param AsThoughEffectType type * @param game * @return */ private List<AsThoughEffect> getApplicableAsThoughEffects(AsThoughEffectType type, Game game) { List<AsThoughEffect> asThoughEffectsList = new ArrayList<>(); if (asThoughEffectsMap.containsKey(type)) { for (AsThoughEffect effect : asThoughEffectsMap.get(type)) { HashSet<Ability> abilities = asThoughEffectsMap.get(type).getAbility(effect.getId()); for (Ability ability : abilities) { if (!(ability instanceof StaticAbility) || ability.isInUseableZone(game, null, null)) { if (effect.getDuration() != Duration.OneUse || !effect.isUsed()) { asThoughEffectsList.add(effect); break; } } } } } return asThoughEffectsList; } /** * 601.2e The player determines the total cost of the spell. Usually this is * just the mana cost. Some spells have additional or alternative costs. * Some effects may increase or reduce the cost to pay, or may provide other * alternative costs. Costs may include paying mana, tapping permanents, * sacrificing permanents, discarding cards, and so on. The total cost is * the mana cost or alternative cost (as determined in rule 601.2b), plus * all additional costs and cost increases, and minus all cost reductions. * If the mana component of the total cost is reduced to nothing by cost * reduction effects, it is considered to be {0}. It can’t be reduced to * less than {0}. Once the total cost is determined, any effects that * directly affect the total cost are applied. Then the resulting total cost * becomes “locked in.” If effects would change the total cost after this * time, they have no effect. */ /** * Inspects all {@link Permanent permanent's} {@link Ability abilities} on * the battlefield for * {@link CostModificationEffect cost modification effects} and applies them * if necessary. * * @param abilityToModify * @param game */ public void costModification(Ability abilityToModify, Game game) { List<CostModificationEffect> costEffects = getApplicableCostModificationEffects(game); for (CostModificationEffect effect : costEffects) { if (effect.getModificationType() == CostModificationType.INCREASE_COST) { HashSet<Ability> abilities = costModificationEffects.getAbility(effect.getId()); for (Ability ability : abilities) { if (effect.applies(abilityToModify, ability, game)) { effect.apply(game, ability, abilityToModify); } } } } for (CostModificationEffect effect : costEffects) { if (effect.getModificationType() == CostModificationType.REDUCE_COST) { HashSet<Ability> abilities = costModificationEffects.getAbility(effect.getId()); for (Ability ability : abilities) { if (effect.applies(abilityToModify, ability, game)) { effect.apply(game, ability, abilityToModify); } } } } for (CostModificationEffect effect : costEffects) { if (effect.getModificationType() == CostModificationType.SET_COST) { HashSet<Ability> abilities = costModificationEffects.getAbility(effect.getId()); for (Ability ability : abilities) { if (effect.applies(abilityToModify, ability, game)) { effect.apply(game, ability, abilityToModify); } } } } } /** * Checks all available splice effects to be applied. * * @param abilityToModify * @param game */ public void applySpliceEffects(Ability abilityToModify, Game game) { if (((SpellAbility) abilityToModify).getSpellAbilityType() == SpellAbilityType.SPLICE) { // on a spliced ability of a spell can't be spliced again return; } List<SpliceCardEffect> spliceEffects = getApplicableSpliceCardEffects(game, abilityToModify.getControllerId()); // get the applyable splice abilities List<SpliceOntoArcaneAbility> spliceAbilities = new ArrayList<>(); for (SpliceCardEffect effect : spliceEffects) { HashSet<Ability> abilities = spliceCardEffects.getAbility(effect.getId()); for (Ability ability : abilities) { if (effect.applies(abilityToModify, ability, game)) { spliceAbilities.add((SpliceOntoArcaneAbility) ability); } } } // check if player wants to use splice if (!spliceAbilities.isEmpty()) { Player controller = game.getPlayer(abilityToModify.getControllerId()); if (controller.chooseUse(Outcome.Benefit, "Splice a card?", abilityToModify, game)) { Cards cardsToReveal = new CardsImpl(); do { FilterCard filter = new FilterCard("a card to splice"); ArrayList<Predicate<MageObject>> idPredicates = new ArrayList<>(); for (SpliceOntoArcaneAbility ability : spliceAbilities) { idPredicates.add(new CardIdPredicate((ability.getSourceId()))); } filter.add(Predicates.or(idPredicates)); TargetCardInHand target = new TargetCardInHand(filter); controller.chooseTarget(Outcome.Benefit, target, abilityToModify, game); UUID cardId = target.getFirstTarget(); if (cardId != null) { SpliceOntoArcaneAbility selectedAbility = null; for (SpliceOntoArcaneAbility ability : spliceAbilities) { if (ability.getSourceId().equals(cardId)) { selectedAbility = ability; break; } } if (selectedAbility != null) { SpliceCardEffect spliceEffect = (SpliceCardEffect) selectedAbility.getEffects().get(0); spliceEffect.apply(game, selectedAbility, abilityToModify); cardsToReveal.add(game.getCard(cardId)); spliceAbilities.remove(selectedAbility); } } } while (!spliceAbilities.isEmpty() && controller.chooseUse(Outcome.Benefit, "Splice another card?", abilityToModify, game)); controller.revealCards("Spliced cards", cardsToReveal, game); } } } /** * Checks if an event won't happen because of an rule modifying effect * * @param event * @param targetAbility ability the event is attached to. can be null. * @param game * @param checkPlayableMode true if the event does not really happen but * it's checked if the event would be replaced * @return */ public boolean preventedByRuleModification(GameEvent event, Ability targetAbility, Game game, boolean checkPlayableMode) { for (ContinuousRuleModifyingEffect effect : continuousRuleModifyingEffects) { if (!effect.checksEventType(event, game)) { continue; } for (Ability sourceAbility : continuousRuleModifyingEffects.getAbility(effect.getId())) { if (!(sourceAbility instanceof StaticAbility) || sourceAbility.isInUseableZone(game, null, event)) { if (checkAbilityStillExists(sourceAbility, effect, event, game)) { if (effect.getDuration() != Duration.OneUse || !effect.isUsed()) { effect.setValue("targetAbility", targetAbility); if (effect.applies(event, sourceAbility, game)) { if (targetAbility instanceof ActivatedAbility && ((ActivatedAbility) targetAbility).isCheckPlayableMode()) { checkPlayableMode = true; } if (!checkPlayableMode) { String message = effect.getInfoMessage(sourceAbility, event, game); if (message != null && !message.isEmpty()) { if (effect.sendMessageToUser()) { Player player = game.getPlayer(event.getPlayerId()); if (player != null && !game.isSimulation()) { game.informPlayer(player, message); } } if (effect.sendMessageToGameLog() && !game.isSimulation()) { game.informPlayers(message); } } } return true; } } } } } } return false; } public boolean replaceEvent(GameEvent event, Game game) { boolean caught = false; HashMap<UUID, HashSet<UUID>> consumed = new HashMap<>(); do { HashMap<ReplacementEffect, HashSet<Ability>> rEffects = getApplicableReplacementEffects(event, game); // Remove all consumed effects (ability dependant) for (Iterator<ReplacementEffect> it1 = rEffects.keySet().iterator(); it1.hasNext();) { ReplacementEffect entry = it1.next(); if (consumed.containsKey(entry.getId())) { HashSet<UUID> consumedAbilitiesIds = consumed.get(entry.getId()); if (rEffects.get(entry) == null || consumedAbilitiesIds.size() == rEffects.get(entry).size()) { it1.remove(); } else { Iterator it = rEffects.get(entry).iterator(); while (it.hasNext()) { Ability ability = (Ability) it.next(); if (consumedAbilitiesIds.contains(ability.getId())) { it.remove(); } } } } } // no effects left, quit if (rEffects.isEmpty()) { break; } int index; boolean onlyOne = false; if (rEffects.size() == 1) { ReplacementEffect effect = rEffects.keySet().iterator().next(); HashSet<Ability> abilities; if (effect.getEffectType() == EffectType.REPLACEMENT) { abilities = replacementEffects.getAbility(effect.getId()); } else { abilities = preventionEffects.getAbility(effect.getId()); } if (abilities == null || abilities.size() == 1) { onlyOne = true; } } if (onlyOne) { index = 0; } else { //20100716 - 616.1c Player player = game.getPlayer(event.getPlayerId()); index = player.chooseReplacementEffect(getReplacementEffectsTexts(rEffects, game), game); } // get the selected effect int checked = 0; ReplacementEffect rEffect = null; Ability rAbility = null; for (Map.Entry<ReplacementEffect, HashSet<Ability>> entry : rEffects.entrySet()) { if (entry.getValue() == null) { if (checked == index) { rEffect = entry.getKey(); break; } else { checked++; } } else { HashSet<Ability> abilities = entry.getValue(); int size = abilities.size(); if (index > (checked + size - 1)) { checked += size; } else { rEffect = entry.getKey(); Iterator it = abilities.iterator(); while (it.hasNext() && rAbility == null) { if (checked == index) { rAbility = (Ability) it.next(); } else { it.next(); checked++; } } break; } } } if (rEffect != null) { event.getAppliedEffects().add(rEffect.getId()); caught = rEffect.replaceEvent(event, rAbility, game); } if (caught) { // Event was completely replaced -> stop applying effects to it break; } // add the applied effect to the consumed effects if (rEffect != null) { if (consumed.containsKey(rEffect.getId())) { HashSet<UUID> set = consumed.get(rEffect.getId()); if (rAbility != null) { set.add(rAbility.getId()); } } else { HashSet<UUID> set = new HashSet<>(); if (rAbility != null) { // in case of AuraReplacementEffect or PlaneswalkerReplacementEffect there is no Ability set.add(rAbility.getId()); } consumed.put(rEffect.getId(), set); } } // Must be called here for some effects to be able to work correctly // For example: Vesuva copying a Dark Depth (VesuvaTest:testDarkDepth) // This call should be removed if possible as replacement effects of EntersTheBattlefield events // do no longer work correctly because the entering permanents are not yet on the battlefield (before they were). // game.applyEffects(); } while (true); return caught; } //20091005 - 613 public void apply(Game game) { removeInactiveEffects(game); List<ContinuousEffect> activeLayerEffects = getLayeredEffects(game); List<ContinuousEffect> layer = filterLayeredEffects(activeLayerEffects, Layer.CopyEffects_1); for (ContinuousEffect effect : layer) { HashSet<Ability> abilities = layeredEffects.getAbility(effect.getId()); for (Ability ability : abilities) { effect.apply(Layer.CopyEffects_1, SubLayer.NA, ability, game); } } //Reload layerEffect if copy effects were applied if (!layer.isEmpty()) { activeLayerEffects = getLayeredEffects(game); } layer = filterLayeredEffects(activeLayerEffects, Layer.ControlChangingEffects_2); // apply control changing effects multiple times if it's needed // for cases when control over permanents with change control abilities is changed // e.g. Mind Control is controlled by Steal Enchantment while (true) { for (ContinuousEffect effect : layer) { HashSet<Ability> abilities = layeredEffects.getAbility(effect.getId()); for (Ability ability : abilities) { effect.apply(Layer.ControlChangingEffects_2, SubLayer.NA, ability, game); } } // if control over all permanent has not changed, we can no longer reapply control changing effects if (!game.getBattlefield().fireControlChangeEvents(game)) { break; } // reset control before reapplying control changing effects game.getBattlefield().resetPermanentsControl(); } applyLayer(activeLayerEffects, Layer.TextChangingEffects_3, game); applyLayer(activeLayerEffects, Layer.TypeChangingEffects_4, game); applyLayer(activeLayerEffects, Layer.ColorChangingEffects_5, game); Map<ContinuousEffect, List<Ability>> appliedEffectAbilities = new HashMap<>(); boolean done = false; Map<ContinuousEffect, Set<UUID>> waitingEffects = new LinkedHashMap<>(); Set<UUID> appliedEffects = new HashSet<>(); while (!done) { // loop needed if a added effect adds again an effect (e.g. Level 5- of Joraga Treespeaker) done = true; layer = filterLayeredEffects(activeLayerEffects, Layer.AbilityAddingRemovingEffects_6); for (ContinuousEffect effect : layer) { if (activeLayerEffects.contains(effect) && !appliedEffects.contains(effect.getId())) { // Effect does still exist and was not applied yet Set<UUID> dependentTo = effect.isDependentTo(layer); if (!appliedEffects.containsAll(dependentTo)) { waitingEffects.put(effect, dependentTo); continue; } List<Ability> appliedAbilities = appliedEffectAbilities.get(effect); HashSet<Ability> abilities = layeredEffects.getAbility(effect.getId()); for (Ability ability : abilities) { if (appliedAbilities == null || !appliedAbilities.contains(ability)) { if (appliedAbilities == null) { appliedAbilities = new ArrayList<>(); appliedEffectAbilities.put(effect, appliedAbilities); } appliedAbilities.add(ability); effect.apply(Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, ability, game); done = false; // list must be updated after each applied effect (eg. if "Turn to Frog" removes abilities) activeLayerEffects = getLayeredEffects(game); } } appliedEffects.add(effect.getId()); if (!waitingEffects.isEmpty()) { // check if waiting effects can be applied now for (Iterator<Map.Entry<ContinuousEffect, Set<UUID>>> iterator = waitingEffects.entrySet().iterator(); iterator.hasNext();) { Map.Entry<ContinuousEffect, Set<UUID>> entry = iterator.next(); if (appliedEffects.containsAll(entry.getValue())) { // all dependent to effects are applied now so apply the effect itself appliedAbilities = appliedEffectAbilities.get(entry.getKey()); abilities = layeredEffects.getAbility(entry.getKey().getId()); for (Ability ability : abilities) { if (appliedAbilities == null || !appliedAbilities.contains(ability)) { if (appliedAbilities == null) { appliedAbilities = new ArrayList<>(); appliedEffectAbilities.put(entry.getKey(), appliedAbilities); } appliedAbilities.add(ability); entry.getKey().apply(Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, ability, game); done = false; // list must be updated after each applied effect (eg. if "Turn to Frog" removes abilities) activeLayerEffects = getLayeredEffects(game); } } appliedEffects.add(entry.getKey().getId()); iterator.remove(); } } } } } } layer = filterLayeredEffects(activeLayerEffects, Layer.PTChangingEffects_7); for (ContinuousEffect effect : layer) { HashSet<Ability> abilities = layeredEffects.getAbility(effect.getId()); for (Ability ability : abilities) { if (abilityActive(ability, game)) { effect.apply(Layer.PTChangingEffects_7, SubLayer.CharacteristicDefining_7a, ability, game); } } } for (ContinuousEffect effect : layer) { HashSet<Ability> abilities = layeredEffects.getAbility(effect.getId()); for (Ability ability : abilities) { effect.apply(Layer.PTChangingEffects_7, SubLayer.SetPT_7b, ability, game); } } for (ContinuousEffect effect : layer) { HashSet<Ability> abilities = layeredEffects.getAbility(effect.getId()); for (Ability ability : abilities) { effect.apply(Layer.PTChangingEffects_7, SubLayer.ModifyPT_7c, ability, game); } } applyCounters.apply(Layer.PTChangingEffects_7, SubLayer.Counters_7d, null, game); for (ContinuousEffect effect : layer) { HashSet<Ability> abilities = layeredEffects.getAbility(effect.getId()); for (Ability ability : abilities) { effect.apply(Layer.PTChangingEffects_7, SubLayer.SwitchPT_e, ability, game); } } layer = filterLayeredEffects(activeLayerEffects, Layer.PlayerEffects); for (ContinuousEffect effect : layer) { HashSet<Ability> abilities = layeredEffects.getAbility(effect.getId()); for (Ability ability : abilities) { effect.apply(Layer.PlayerEffects, SubLayer.NA, ability, game); } } layer = filterLayeredEffects(activeLayerEffects, Layer.RulesEffects); for (ContinuousEffect effect : layer) { HashSet<Ability> abilities = layeredEffects.getAbility(effect.getId()); for (Ability ability : abilities) { effect.apply(Layer.RulesEffects, SubLayer.NA, ability, game); } } } private boolean abilityActive(Ability ability, Game game) { MageObject object = game.getObject(ability.getSourceId()); return object != null && object.hasAbility(ability.getId(), game); } private void applyLayer(List<ContinuousEffect> activeLayerEffects, Layer currentLayer, Game game) { List<ContinuousEffect> layer = filterLayeredEffects(activeLayerEffects, currentLayer); if (!layer.isEmpty()) { int numberOfEffects = layer.size(); Set<UUID> appliedEffects = new HashSet<>(); Map<ContinuousEffect, Set<UUID>> waitingEffects = new LinkedHashMap<>(); for (ContinuousEffect effect : layer) { if (numberOfEffects > 1) { // If an effect is dependent to not applied effects yet of this layer, so wait to apply this effect Set<UUID> dependentTo = effect.isDependentTo(layer); if (!appliedEffects.containsAll(dependentTo)) { waitingEffects.put(effect, dependentTo); continue; } } applyContinuousEffect(effect, currentLayer, game); appliedEffects.add(effect.getId()); if (!waitingEffects.isEmpty()) { // check if waiting effects can be applied now for (Entry<ContinuousEffect, Set<UUID>> entry : waitingEffects.entrySet()) { if (appliedEffects.containsAll(entry.getValue())) { // all dependent to effects are applied now so apply the effect itself applyContinuousEffect(entry.getKey(), currentLayer, game); appliedEffects.add(entry.getKey().getId()); } } } } } } private void applyContinuousEffect(ContinuousEffect effect, Layer currentLayer, Game game) { HashSet<Ability> abilities = layeredEffects.getAbility(effect.getId()); for (Ability ability : abilities) { effect.apply(currentLayer, SubLayer.NA, ability, game); } } public HashSet<Ability> getLayeredEffectAbilities(ContinuousEffect effect) { return layeredEffects.getAbility(effect.getId()); } /** * Adds a continuous ability with a reference to a sourceId. It's used for * effects that cease to exist again So this effects were removed again * before each applyEffecs * * @param effect * @param sourceId * @param source */ public void addEffect(ContinuousEffect effect, UUID sourceId, Ability source) { if (!(source instanceof MageSingleton)) { // because MageSingletons may never be removed by removing the temporary effecs they are not added to the temporaryEffects to prevent this effect.setTemporary(true); Set<Ability> abilities = temporaryEffects.get(effect); if (abilities == null) { abilities = new HashSet<>(); temporaryEffects.put(effect, abilities); } else if (abilities.contains(source)) { // this ability (for the continuous effect) is already added return; } abilities.add(source); // add the effect itself } addEffect(effect, source); } public void addEffect(ContinuousEffect effect, Ability source) { if (effect == null) { logger.error("Effect is null: " + source.toString()); return; } else if (source == null) { logger.warn("Adding effect without ability : " + effect.toString()); } switch (effect.getEffectType()) { case REPLACEMENT: case REDIRECTION: ReplacementEffect newReplacementEffect = (ReplacementEffect) effect; replacementEffects.addEffect(newReplacementEffect, source); break; case PREVENTION: PreventionEffect newPreventionEffect = (PreventionEffect) effect; preventionEffects.addEffect(newPreventionEffect, source); break; case RESTRICTION: RestrictionEffect newRestrictionEffect = (RestrictionEffect) effect; restrictionEffects.addEffect(newRestrictionEffect, source); break; case RESTRICTION_UNTAP_NOT_MORE_THAN: RestrictionUntapNotMoreThanEffect newRestrictionUntapNotMoreThanEffect = (RestrictionUntapNotMoreThanEffect) effect; restrictionUntapNotMoreThanEffects.addEffect(newRestrictionUntapNotMoreThanEffect, source); break; case REQUIREMENT: RequirementEffect newRequirementEffect = (RequirementEffect) effect; requirementEffects.addEffect(newRequirementEffect, source); break; case ASTHOUGH: AsThoughEffect newAsThoughEffect = (AsThoughEffect) effect; if (!asThoughEffectsMap.containsKey(newAsThoughEffect.getAsThoughEffectType())) { ContinuousEffectsList<AsThoughEffect> list = new ContinuousEffectsList<>(); allEffectsLists.add(list); asThoughEffectsMap.put(newAsThoughEffect.getAsThoughEffectType(), list); } asThoughEffectsMap.get(newAsThoughEffect.getAsThoughEffectType()).addEffect(newAsThoughEffect, source); break; case COSTMODIFICATION: CostModificationEffect newCostModificationEffect = (CostModificationEffect) effect; costModificationEffects.addEffect(newCostModificationEffect, source); break; case SPLICE: SpliceCardEffect newSpliceCardEffect = (SpliceCardEffect) effect; spliceCardEffects.addEffect(newSpliceCardEffect, source); break; case CONTINUOUS_RULE_MODIFICATION: ContinuousRuleModifyingEffect newContinuousRuleModifiyingEffect = (ContinuousRuleModifyingEffect) effect; continuousRuleModifyingEffects.addEffect(newContinuousRuleModifiyingEffect, source); break; default: layeredEffects.addEffect(effect, source); break; } } public void setController(UUID cardId, UUID controllerId) { for (ContinuousEffectsList effectsList : allEffectsLists) { setControllerForEffect(effectsList, cardId, controllerId); } } private void setControllerForEffect(ContinuousEffectsList<?> effects, UUID sourceId, UUID controllerId) { for (Effect effect : effects) { HashSet<Ability> abilities = effects.getAbility(effect.getId()); for (Ability ability : abilities) { if (ability.getSourceId() != null) { if (ability.getSourceId().equals(sourceId)) { ability.setControllerId(controllerId); } } else if (ability.getZone() != Zone.COMMAND) { logger.fatal("Continuous effect for ability with no sourceId Ability: " + ability); } } } } public void clear() { for (ContinuousEffectsList effectsList : allEffectsLists) { effectsList.clear(); } temporaryEffects.clear(); } public void removeAllTemporaryEffects() { for (Map.Entry<ContinuousEffect, Set<Ability>> entry : temporaryEffects.entrySet()) { switch (entry.getKey().getEffectType()) { case REPLACEMENT: case REDIRECTION: replacementEffects.removeEffects(entry.getKey().getId(), entry.getValue()); break; case PREVENTION: preventionEffects.removeEffects(entry.getKey().getId(), entry.getValue()); break; case RESTRICTION: restrictionEffects.removeEffects(entry.getKey().getId(), entry.getValue()); break; case RESTRICTION_UNTAP_NOT_MORE_THAN: restrictionUntapNotMoreThanEffects.removeEffects(entry.getKey().getId(), entry.getValue()); break; case REQUIREMENT: requirementEffects.removeEffects(entry.getKey().getId(), entry.getValue()); break; case ASTHOUGH: AsThoughEffect newAsThoughEffect = (AsThoughEffect) entry.getKey(); if (!asThoughEffectsMap.containsKey(newAsThoughEffect.getAsThoughEffectType())) { break; } asThoughEffectsMap.get(newAsThoughEffect.getAsThoughEffectType()).removeEffects(entry.getKey().getId(), entry.getValue()); break; case COSTMODIFICATION: costModificationEffects.removeEffects(entry.getKey().getId(), entry.getValue()); break; case SPLICE: spliceCardEffects.removeEffects(entry.getKey().getId(), entry.getValue()); break; case CONTINUOUS_RULE_MODIFICATION: continuousRuleModifyingEffects.removeEffects(entry.getKey().getId(), entry.getValue()); break; default: layeredEffects.removeEffects(entry.getKey().getId(), entry.getValue()); break; } } temporaryEffects.clear(); } public Map<String, String> getReplacementEffectsTexts(HashMap<ReplacementEffect, HashSet<Ability>> rEffects, Game game) { Map<String, String> texts = new LinkedHashMap<>(); for (Map.Entry<ReplacementEffect, HashSet<Ability>> entry : rEffects.entrySet()) { if (entry.getValue() != null) { for (Ability ability : entry.getValue()) { MageObject object = game.getObject(ability.getSourceId()); if (object != null) { texts.put(ability.getId().toString() + '_' + entry.getKey().getId().toString(), object.getName() + ": " + ability.getRule(object.getName())); } else { texts.put(ability.getId().toString() + '_' + entry.getKey().getId().toString(), entry.getKey().getText(null)); } } } else { if (!(entry.getKey() instanceof AuraReplacementEffect) && !(entry.getKey() instanceof PlaneswalkerRedirectionEffect)) { logger.error("Replacement effect without ability: " + entry.getKey().toString()); } } } return texts; } public boolean existRequirementEffects() { return !requirementEffects.isEmpty(); } public UUID getControllerOfSourceId(UUID sourceId) { UUID controllerFound = null; for (PreventionEffect effect : preventionEffects) { HashSet<Ability> abilities = preventionEffects.getAbility(effect.getId()); for (Ability ability : abilities) { if (ability.getSourceId().equals(sourceId)) { if (controllerFound == null || controllerFound.equals(ability.getControllerId())) { controllerFound = ability.getControllerId(); } else { // not unique controller - No solution yet return null; } } } } for (ReplacementEffect effect : replacementEffects) { HashSet<Ability> abilities = replacementEffects.getAbility(effect.getId()); for (Ability ability : abilities) { if (ability.getSourceId() != null) { if (ability.getSourceId().equals(sourceId)) { if (controllerFound == null || controllerFound.equals(ability.getControllerId())) { controllerFound = ability.getControllerId(); } else { // not unique controller - No solution yet return null; } } } else { logger.warn("Ability without sourceId:" + ability.getRule()); } } } return controllerFound; } } class ContinuousEffectSorter implements Comparator<ContinuousEffect>, Serializable { @Override public int compare(ContinuousEffect one, ContinuousEffect two) { return Long.compare(one.getOrder(), two.getOrder()); } }