diff --git a/Mage/src/mage/abilities/effects/ContinuousEffects.java b/Mage/src/mage/abilities/effects/ContinuousEffects.java index e7b40eb4cf..2b222098ed 100644 --- a/Mage/src/mage/abilities/effects/ContinuousEffects.java +++ b/Mage/src/mage/abilities/effects/ContinuousEffects.java @@ -67,6 +67,7 @@ public class ContinuousEffects implements Serializable { private ContinuousEffectsList preventionEffects = new ContinuousEffectsList(); private ContinuousEffectsList requirementEffects = new ContinuousEffectsList(); private ContinuousEffectsList restrictionEffects = new ContinuousEffectsList(); + private ContinuousEffectsList restrictionUntapNotMoreThanEffects = new ContinuousEffectsList(); private ContinuousEffectsList asThoughEffects = new ContinuousEffectsList(); private ContinuousEffectsList costModificationEffects = new ContinuousEffectsList(); private ContinuousEffectsList spliceCardEffects = new ContinuousEffectsList(); @@ -98,6 +99,7 @@ public class ContinuousEffects implements Serializable { preventionEffects = effect.preventionEffects.copy(); requirementEffects = effect.requirementEffects.copy(); restrictionEffects = effect.restrictionEffects.copy(); + restrictionUntapNotMoreThanEffects = effect.restrictionUntapNotMoreThanEffects.copy(); asThoughEffects = effect.asThoughEffects.copy(); costModificationEffects = effect.costModificationEffects.copy(); spliceCardEffects = effect.spliceCardEffects.copy(); @@ -114,6 +116,7 @@ public class ContinuousEffects implements Serializable { allEffectsLists.add(preventionEffects); allEffectsLists.add(requirementEffects); allEffectsLists.add(restrictionEffects); + allEffectsLists.add(restrictionUntapNotMoreThanEffects); allEffectsLists.add(asThoughEffects); allEffectsLists.add(costModificationEffects); allEffectsLists.add(spliceCardEffects); @@ -159,6 +162,7 @@ public class ContinuousEffects implements Serializable { preventionEffects.removeInactiveEffects(game); requirementEffects.removeInactiveEffects(game); restrictionEffects.removeInactiveEffects(game); + restrictionUntapNotMoreThanEffects.removeInactiveEffects(game); asThoughEffects.removeInactiveEffects(game); costModificationEffects.removeInactiveEffects(game); spliceCardEffects.removeInactiveEffects(game); @@ -251,7 +255,7 @@ public class ContinuousEffects implements Serializable { HashSet abilities = restrictionEffects.getAbility(effect.getId()); HashSet applicableAbilities = new HashSet(); for (Ability ability : abilities) { - if (!(ability instanceof StaticAbility) || ability.isInUseableZone(game, permanent, false)) { + if (!(ability instanceof StaticAbility) || ability.isInUseableZone(game, null, false)) { if (effect.applies(permanent, ability, game)) { applicableAbilities.add(ability); } @@ -264,6 +268,25 @@ public class ContinuousEffects implements Serializable { return effects; } + public HashMap> getApplicableRestrictionUntapNotMoreThanEffects(Player player, Game game) { + HashMap> effects = new HashMap>(); + for (RestrictionUntapNotMoreThanEffect effect: restrictionUntapNotMoreThanEffects) { + HashSet abilities = restrictionUntapNotMoreThanEffects.getAbility(effect.getId()); + HashSet applicableAbilities = new HashSet(); + for (Ability ability : abilities) { + if (!(ability instanceof StaticAbility) || ability.isInUseableZone(game, null, false)) { + if (effect.applies(player, ability, game)) { + applicableAbilities.add(ability); + } + } + } + if (!applicableAbilities.isEmpty()) { + effects.put(effect, abilities); + } + } + return effects; + } + /** * * @param event @@ -708,6 +731,10 @@ public class ContinuousEffects implements Serializable { 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); diff --git a/Mage/src/mage/abilities/effects/RestrictionEffect.java b/Mage/src/mage/abilities/effects/RestrictionEffect.java index 377db8a158..b0b3479b8f 100644 --- a/Mage/src/mage/abilities/effects/RestrictionEffect.java +++ b/Mage/src/mage/abilities/effects/RestrictionEffect.java @@ -28,12 +28,14 @@ package mage.abilities.effects; +import mage.abilities.Ability; import mage.constants.Duration; import mage.constants.EffectType; import mage.constants.Outcome; -import mage.abilities.Ability; +import mage.filter.FilterPermanent; import mage.game.Game; import mage.game.permanent.Permanent; +import mage.players.Player; /** * @@ -41,13 +43,28 @@ import mage.game.permanent.Permanent; */ public abstract class RestrictionEffect> extends ContinuousEffectImpl { + private boolean notMoreThanRestriction; + private int notMoreThanNumber; + private FilterPermanent notMoreThanNumberFilter; + public RestrictionEffect(Duration duration) { + this(duration, false, 0, null); + } + + public RestrictionEffect(Duration duration, boolean notMoreThanRestriction, int notMoreThanNumber, FilterPermanent notMoreThanNumberFilter) { super(duration, Outcome.Detriment); this.effectType = EffectType.RESTRICTION; + this.notMoreThanRestriction = notMoreThanRestriction; + this.notMoreThanNumber = notMoreThanNumber; + this.notMoreThanNumberFilter = notMoreThanNumberFilter; } public RestrictionEffect(final RestrictionEffect effect) { super(effect); + this.notMoreThanRestriction = effect.notMoreThanRestriction; + if (this.notMoreThanNumberFilter != null) { + this.notMoreThanNumberFilter = effect.notMoreThanNumberFilter.copy(); + } } @Override @@ -57,6 +74,15 @@ public abstract class RestrictionEffect> extends public abstract boolean applies(Permanent permanent, Ability source, Game game); + + /* + * only used for the notMoreThanRestrictions, called to check if the effect shall be applied for a player + * + */ + public boolean appliesNotMoreThan(Player player, Ability source, Game game) { + return false; + } + public boolean canAttack(Game game) { return true; } @@ -77,5 +103,16 @@ public abstract class RestrictionEffect> extends return true; } + public boolean isNotMoreThanRestriction() { + return notMoreThanRestriction; + } + public int getNotMoreThanNumber() { + return notMoreThanNumber; + } + + public FilterPermanent getNotMoreThanNumberFilter() { + return notMoreThanNumberFilter; + } + } diff --git a/Mage/src/mage/abilities/effects/RestrictionUntapNotMoreThanEffect.java b/Mage/src/mage/abilities/effects/RestrictionUntapNotMoreThanEffect.java new file mode 100644 index 0000000000..7c17689a9a --- /dev/null +++ b/Mage/src/mage/abilities/effects/RestrictionUntapNotMoreThanEffect.java @@ -0,0 +1,78 @@ +/* + * 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 mage.abilities.Ability; +import mage.constants.Duration; +import mage.constants.EffectType; +import mage.constants.Outcome; +import mage.filter.common.FilterControlledPermanent; +import mage.game.Game; +import mage.players.Player; + +/** + * + * @author LevelX2 + */ +public abstract class RestrictionUntapNotMoreThanEffect> extends ContinuousEffectImpl { + + private int number; + private FilterControlledPermanent filter; + + public RestrictionUntapNotMoreThanEffect(Duration duration, int number, FilterControlledPermanent filter) { + super(duration, Outcome.Detriment); + this.effectType = EffectType.RESTRICTION_UNTAP_NOT_MORE_THAN; + this.number = number; + this.filter = filter; + } + + public RestrictionUntapNotMoreThanEffect(final RestrictionUntapNotMoreThanEffect effect) { + super(effect); + this.number = effect.number; + if (effect.filter != null) { + this.filter = effect.filter.copy(); + } + } + + @Override + public boolean apply(Game game, Ability source) { + throw new UnsupportedOperationException("Not supported."); + } + + public abstract boolean applies(Player player, Ability source, Game game); + + public int getNumber() { + return number; + } + + public FilterControlledPermanent getFilter() { + return filter; + } + +} diff --git a/Mage/src/mage/constants/EffectType.java b/Mage/src/mage/constants/EffectType.java index aa483ea23c..bfe01d4ec0 100644 --- a/Mage/src/mage/constants/EffectType.java +++ b/Mage/src/mage/constants/EffectType.java @@ -13,6 +13,7 @@ public enum EffectType { REDIRECTION("Redirection Effect"), ASTHOUGH("As Though Effect"), RESTRICTION("Restriction Effect"), + RESTRICTION_UNTAP_NOT_MORE_THAN("Restriction untap not more than Effect"), REQUIREMENT("Requirement Effect"), COSTMODIFICATION("Cost Modification Effect"), SPLICE("Splice Card Effect"); diff --git a/Mage/src/mage/players/PlayerImpl.java b/Mage/src/mage/players/PlayerImpl.java index 25775e5b41..a292670b9d 100644 --- a/Mage/src/mage/players/PlayerImpl.java +++ b/Mage/src/mage/players/PlayerImpl.java @@ -29,6 +29,7 @@ package mage.players; import java.io.Serializable; +import java.security.InvalidParameterException; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -36,6 +37,7 @@ import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Random; import java.util.Set; import java.util.UUID; @@ -56,6 +58,7 @@ import mage.abilities.common.delayed.AtTheEndOfTurnStepPostDelayedTriggeredAbili import mage.abilities.costs.AdjustingSourceCosts; import mage.abilities.costs.AlternativeCost; import mage.abilities.effects.RestrictionEffect; +import mage.abilities.effects.RestrictionUntapNotMoreThanEffect; import mage.abilities.effects.common.LoseControlOnOtherPlayersControllerEffect; import mage.abilities.keyword.FlashbackAbility; import mage.abilities.keyword.HexproofAbility; @@ -82,8 +85,11 @@ import mage.counters.Counter; import mage.counters.CounterType; import mage.counters.Counters; import mage.filter.FilterCard; +import mage.filter.common.FilterControlledPermanent; import mage.filter.common.FilterCreatureForCombat; import mage.filter.common.FilterCreatureForCombatBlock; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.permanent.PermanentIdPredicate; import mage.game.ExileZone; import mage.game.Game; import mage.game.combat.CombatGroup; @@ -99,10 +105,12 @@ import mage.players.net.UserData; import mage.target.Target; import mage.target.TargetAmount; import mage.target.TargetCard; +import mage.target.TargetPermanent; import mage.target.common.TargetCardInLibrary; import mage.target.common.TargetDiscard; import mage.watchers.common.BloodthirstWatcher; import org.apache.log4j.Logger; +import org.omg.CORBA.DynAnyPackage.Invalid; public abstract class PlayerImpl> implements Player, Serializable { @@ -999,18 +1007,156 @@ public abstract class PlayerImpl> implements Player, Ser @Override public void untap(Game game) { - //20091005 - 502.2 - for (Permanent permanent: game.getBattlefield().getAllActivePermanents(playerId)) { - boolean untap = true; - for (RestrictionEffect effect: game.getContinuousEffects().getApplicableRestrictionEffects(permanent, game).keySet()) { - untap &= effect.canBeUntapped(permanent, game); + // create list of all "notMoreThan" effects to track which one are consumed + HashMap>, Integer> notMoreThanEffectsUsage = new HashMap>, Integer>(); + for (Entry> restrictionEffect: game.getContinuousEffects().getApplicableRestrictionUntapNotMoreThanEffects(this, game).entrySet()) { + notMoreThanEffectsUsage.put(restrictionEffect, new Integer(restrictionEffect.getKey().getNumber())); + } + + if (!notMoreThanEffectsUsage.isEmpty()) { + // create list of all permanents that can be untapped generally + List canBeUntapped = new ArrayList(); + for (Permanent permanent: game.getBattlefield().getAllActivePermanents(playerId)) { + boolean untap = true; + for (RestrictionEffect effect: game.getContinuousEffects().getApplicableRestrictionEffects(permanent, game).keySet()) { + untap &= effect.canBeUntapped(permanent, game); + } + if (untap) { + canBeUntapped.add(permanent); + } } - if (untap) { - permanent.untap(game); + // selected permanents to untap + List selectedToUntap = new ArrayList(); + + // player can cancel the seletion of an effect to use a prefered order of restriction effects + boolean playerCanceledSelection; + do { + playerCanceledSelection = false; + // select permanents to untap to consume the "notMoreThan" effects + for(Map.Entry>, Integer> handledEntry: notMoreThanEffectsUsage.entrySet()) { + // select a permanent to untap for this entry + int numberToUntap = handledEntry.getValue().intValue(); + if (numberToUntap > 0) { + + List leftForUntap = getPermanentsThatCanBeUntapped(game, canBeUntapped, handledEntry.getKey().getKey(), notMoreThanEffectsUsage); + + FilterControlledPermanent filter = handledEntry.getKey().getKey().getFilter().copy(); + String message = filter.getMessage(); + // omitt already from other untap effects selected permanents + for (Permanent permanent: selectedToUntap) { + filter.add(Predicates.not(new PermanentIdPredicate(permanent.getId()))); + } + // while targets left and there is still allowed to untap + while (leftForUntap.size() > 0 && numberToUntap > 0) { + // player has to select the permanent he wants to untap for this restriction + Ability ability = handledEntry.getKey().getValue().iterator().next(); + if (ability != null) { + StringBuilder sb = new StringBuilder(message).append(" to untap").append(" (").append(Math.min(leftForUntap.size(), numberToUntap)).append(" in total"); + MageObject effectSource = game.getObject(ability.getSourceId()); + if (effectSource != null) { + sb.append(" from ").append(effectSource.getName()).toString(); + } + sb.append(")"); + filter.setMessage(sb.toString()); + Target target = new TargetPermanent(filter); + if (!this.chooseTarget(Outcome.Untap, target, ability, game)) { + // player canceled, go on with the next effect (if no other effect available, this effect will be active again) + playerCanceledSelection = true; + break; + } + Permanent selectedPermanent = game.getPermanent(target.getFirstTarget()); + if (leftForUntap.contains(selectedPermanent)) { + selectedToUntap.add(selectedPermanent); + numberToUntap--; + // don't allow to select same permanent twice + filter.add(Predicates.not(new PermanentIdPredicate(selectedPermanent.getId()))); + // reduce available untap numbers from other "UntapNotMoreThan" effects if selected permanent applies to their filter too + for (Entry>, Integer> notMoreThanEffect : notMoreThanEffectsUsage.entrySet()) { + if (notMoreThanEffect.getValue().intValue() > 0 && notMoreThanEffect.getKey().getKey().getFilter().match(selectedPermanent, game)) { + notMoreThanEffect.setValue(new Integer(notMoreThanEffect.getValue().intValue() - 1)); + } + } + // update the left for untap list + leftForUntap = getPermanentsThatCanBeUntapped(game, canBeUntapped, handledEntry.getKey().getKey(), notMoreThanEffectsUsage); + // remove already selected permanents + for (Permanent permanent :selectedToUntap) { + if (leftForUntap.contains(permanent)) { + leftForUntap.remove(permanent); + } + } + + } else { + // player selected an permanent that is restricted by another effect, disallow it (so AI can select another one) + filter.add(Predicates.not(new PermanentIdPredicate(selectedPermanent.getId()))); + if (this.isHuman()) { + game.informPlayer(this, "This permanent can't be untapped because of other restricting effect."); + } + } + } + } + } + } + + } while (playerCanceledSelection); + + // show in log which permanents were selected to untap + for(Permanent permanent :selectedToUntap) { + game.informPlayers(new StringBuilder(this.getName()).append(" untapped ").append(permanent.getName()).toString()); + } + // untap if permanent is not concerned by notMoreThan effects or is included in the selectedToUntapList + for (Permanent permanent: canBeUntapped) { + boolean doUntap = true; + if (!selectedToUntap.contains(permanent)) { + // if the permanent is covered by one of the restriction effects, don't untap it + for (Entry>, Integer> notMoreThanEffect : notMoreThanEffectsUsage.entrySet()) { + if (notMoreThanEffect.getKey().getKey().getFilter().match(permanent, game)) { + doUntap = false; + break; + } + } + } + if (permanent != null && doUntap) { + permanent.untap(game); + } + + } + + + } else { + //20091005 - 502.2 + for (Permanent permanent: game.getBattlefield().getAllActivePermanents(playerId)) { + boolean untap = true; + for (RestrictionEffect effect: game.getContinuousEffects().getApplicableRestrictionEffects(permanent, game).keySet()) { + untap &= effect.canBeUntapped(permanent, game); + } + if (untap) { + permanent.untap(game); + } } } } + private List getPermanentsThatCanBeUntapped(Game game, List canBeUntapped, RestrictionUntapNotMoreThanEffect handledEffect, HashMap>, Integer> notMoreThanEffectsUsage) { + List leftForUntap = new ArrayList(); + // select permanents that can still be untapped + for (Permanent permanent: canBeUntapped) { + if (handledEffect.getFilter().match(permanent, game)) { // matches the restricted permanents of handled entry + boolean canBeSelected = true; + // check if the permanent is restriced by another restriction that has left no permanent + for (Entry>, Integer> notMoreThanEffect : notMoreThanEffectsUsage.entrySet()) { + if (notMoreThanEffect.getKey().getKey().getFilter().match(permanent, game) && notMoreThanEffect.getValue().intValue() == 0) { + canBeSelected = false; + break; + } + } + if (canBeSelected) { + leftForUntap.add(permanent); + } + } + } + return leftForUntap; + } + @Override public UUID getId() { return playerId;