diff --git a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java index bff6dea4ea..762c5408e7 100644 --- a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java +++ b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java @@ -46,6 +46,7 @@ import mage.choices.ChoiceImpl; import mage.constants.*; import static mage.constants.PlayerAction.REQUEST_AUTO_ANSWER_RESET_ALL; import static mage.constants.PlayerAction.TRIGGER_AUTO_ORDER_RESET_ALL; +import mage.filter.StaticFilters; import mage.filter.common.FilterAttackingCreature; import mage.filter.common.FilterBlockingCreature; import mage.filter.common.FilterCreatureForCombat; @@ -1055,6 +1056,7 @@ public class HumanPlayer extends PlayerImpl { updateGameStatePriority("selectAttackers", game); FilterCreatureForCombat filter = filterCreatureForCombat.copy(); filter.add(new ControllerIdPredicate(attackingPlayerId)); + while (!abort) { if (passedAllTurns || passedUntilEndStepBeforeMyTurn @@ -1063,7 +1065,9 @@ public class HumanPlayer extends PlayerImpl { || passedTurnSkipStack || passedUntilEndOfTurn || passedUntilNextMain))) { - return; + if (checkIfAttackersValid(game)) { + return; + } } Map options = new HashMap<>(); @@ -1108,46 +1112,12 @@ public class HumanPlayer extends PlayerImpl { // attack selected default defender declareAttacker(attacker.getId(), attackedDefender, game, false); } - } else if (response.getBoolean() != null) { - // check if enough attackers are declared - if (!game.getCombat().getCreaturesForcedToAttack().isEmpty()) { - if (!game.getCombat().getAttackers().containsAll(game.getCombat().getCreaturesForcedToAttack().keySet())) { - int forcedAttackers = 0; - StringBuilder sb = new StringBuilder(); - for (UUID creatureId : game.getCombat().getCreaturesForcedToAttack().keySet()) { - boolean validForcedAttacker = false; - if (game.getCombat().getAttackers().contains(creatureId)) { - Set possibleDefender = game.getCombat().getCreaturesForcedToAttack().get(creatureId); - if (possibleDefender.isEmpty() - || possibleDefender.contains(game.getCombat().getDefenderId(creatureId))) { - validForcedAttacker = true; - } - } - if (validForcedAttacker) { - forcedAttackers++; - } else { - Permanent creature = game.getPermanent(creatureId); - if (creature != null) { - sb.append(creature.getIdName()).append(' '); - } - } +// } else if (response.getInteger() != null) { // Ok or F-Key - } - if (game.getCombat().getMaxAttackers() > forcedAttackers) { - int requireToAttack = Math.min(game.getCombat().getMaxAttackers() - forcedAttackers, game.getCombat().getCreaturesForcedToAttack().size() - forcedAttackers); - String message = (requireToAttack == 1 ? " more attacker that is " : " more attackers that are ") - + "forced to attack.\nCreature" - + (requireToAttack == 1 ? "" : "s") + " forced to attack: "; - game.informPlayer(this, sb.insert(0, message) - .insert(0, requireToAttack) - .insert(0, "You have to attack with ").toString()); - continue; - } - } + } else if (response.getBoolean() != null) { // ok button + if (checkIfAttackersValid(game)) { + return; } - return; - } else if (response.getInteger() != null) { - return; } else if (response.getUUID() != null) { Permanent attacker = game.getPermanent(response.getUUID()); if (attacker != null) { @@ -1161,8 +1131,95 @@ public class HumanPlayer extends PlayerImpl { } } + private boolean checkIfAttackersValid(Game game) { + if (!game.getCombat().getCreaturesForcedToAttack().isEmpty()) { + if (!game.getCombat().getAttackers().containsAll(game.getCombat().getCreaturesForcedToAttack().keySet())) { + int forcedAttackers = 0; + StringBuilder sb = new StringBuilder(); + for (UUID creatureId : game.getCombat().getCreaturesForcedToAttack().keySet()) { + boolean validForcedAttacker = false; + if (game.getCombat().getAttackers().contains(creatureId)) { + Set possibleDefender = game.getCombat().getCreaturesForcedToAttack().get(creatureId); + if (possibleDefender.isEmpty() + || possibleDefender.contains(game.getCombat().getDefenderId(creatureId))) { + validForcedAttacker = true; + } + } + if (validForcedAttacker) { + forcedAttackers++; + } else { + Permanent creature = game.getPermanent(creatureId); + if (creature != null) { + sb.append(creature.getIdName()).append(' '); + } + } + + } + if (game.getCombat().getMaxAttackers() > forcedAttackers) { + int requireToAttack = Math.min(game.getCombat().getMaxAttackers() - forcedAttackers, game.getCombat().getCreaturesForcedToAttack().size() - forcedAttackers); + String message = (requireToAttack == 1 ? " more attacker that is " : " more attackers that are ") + + "forced to attack.\nCreature" + + (requireToAttack == 1 ? "" : "s") + " forced to attack: "; + game.informPlayer(this, sb.insert(0, message) + .insert(0, requireToAttack) + .insert(0, "You have to attack with ").toString()); + return false; + } + } + } + // check if enough attackers are declared + // check if players have to be attacked + Set playersToAttackIfAble = new HashSet<>(); + for (Map.Entry> entry : game.getContinuousEffects().getApplicableRequirementEffects(null, true, game).entrySet()) { + RequirementEffect effect = entry.getKey(); + for (Ability ability : entry.getValue()) { + UUID playerToAttack = effect.playerMustBeAttackedIfAble(ability, game); + if (playerToAttack != null) { + playersToAttackIfAble.add(playerToAttack); + } + } + } + if (!playersToAttackIfAble.isEmpty()) { + Set checkPlayersToAttackIfAble = new HashSet<>(playersToAttackIfAble); + for (CombatGroup combatGroup : game.getCombat().getGroups()) { + checkPlayersToAttackIfAble.remove(combatGroup.getDefendingPlayerId()); + } + + for (UUID forcedToAttackId : checkPlayersToAttackIfAble) { + Player forcedToAttack = game.getPlayer(forcedToAttackId); + + for (Permanent attacker : game.getBattlefield().getAllActivePermanents(StaticFilters.FILTER_PERMANENT_CREATURE, getId(), game)) { + + if (game.getContinuousEffects().checkIfThereArePayCostToAttackBlockEffects( + GameEvent.getEvent(GameEvent.EventType.DECLARE_ATTACKER, + forcedToAttackId, attacker.getId(), attacker.getControllerId()), game)) { + continue; + } + // if attacker needs a specific defender to attack so select that one instead + if (game.getCombat().getCreaturesForcedToAttack().containsKey(attacker.getId())) { + Set possibleDefenders = game.getCombat().getCreaturesForcedToAttack().get(attacker.getId()); + if (!possibleDefenders.isEmpty() && !possibleDefenders.contains(forcedToAttackId)) { + continue; + } + } + UUID defendingPlayerId = game.getCombat().getDefendingPlayerId(attacker.getId(), game); + if (playersToAttackIfAble.contains(defendingPlayerId)) { + // already attacks other player taht has to be attacked + continue; + } + if (defendingPlayerId != null || attacker.canAttack(forcedToAttackId, game)) { + game.informPlayer(this, "You are forced to attack " + forcedToAttack.getName() + " or a controlled planeswalker e.g. with " + attacker.getIdName() + "."); + return false; + } + } + } + } + + return true; + } + private void removeAttackerIfPossible(Game game, Permanent attacker) { - for (Map.Entry entry : game.getContinuousEffects().getApplicableRequirementEffects(attacker, game).entrySet()) { + for (Map.Entry entry : game.getContinuousEffects().getApplicableRequirementEffects(attacker, false, game).entrySet()) { RequirementEffect effect = (RequirementEffect) entry.getKey(); if (effect.mustAttack(game)) { if (game.getCombat().getMaxAttackers() >= game.getCombat().getCreaturesForcedToAttack().size() diff --git a/Mage.Sets/src/mage/cards/t/TroveOfTemptation.java b/Mage.Sets/src/mage/cards/t/TroveOfTemptation.java new file mode 100644 index 0000000000..70ad58d291 --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/TroveOfTemptation.java @@ -0,0 +1,109 @@ +/* + * 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.cards.t; + +import java.util.UUID; +import mage.abilities.Ability; +import mage.abilities.common.BeginningOfYourEndStepTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.RequirementEffect; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.game.permanent.token.TreasureToken; +import mage.players.Player; + +/** + * + * @author LevelX2 + */ +public class TroveOfTemptation extends CardImpl { + + public TroveOfTemptation(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{3}{R}"); + + // Each opponent must attack you or a planeswalker you control with at least one creature each combat if able. + addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new TroveOfTemptationForceAttackEffect(Duration.WhileOnBattlefield))); + + // At the beginning of your end step, create a colorless Treasure artifact token with "{t}, Sacrifice this artifact: Add one mana of any color to your mana pool.” + addAbility(new BeginningOfYourEndStepTriggeredAbility(new CreateTokenEffect(new TreasureToken()), false)); + } + + public TroveOfTemptation(final TroveOfTemptation card) { + super(card); + } + + @Override + public TroveOfTemptation copy() { + return new TroveOfTemptation(this); + } +} + +class TroveOfTemptationForceAttackEffect extends RequirementEffect { + + public TroveOfTemptationForceAttackEffect(Duration duration) { + super(duration, true); + staticText = "Each opponent must attack you or a planeswalker you control with at least one creature each combat if able"; + } + + public TroveOfTemptationForceAttackEffect(final TroveOfTemptationForceAttackEffect effect) { + super(effect); + } + + @Override + public TroveOfTemptationForceAttackEffect copy() { + return new TroveOfTemptationForceAttackEffect(this); + } + + @Override + public boolean mustAttack(Game game) { + return false; + } + + @Override + public boolean mustBlock(Game game) { + return false; + } + + @Override + public boolean applies(Permanent permanent, Ability source, Game game) { + Player controller = game.getPlayer(source.getControllerId()); + return controller != null && controller.hasOpponent(game.getActivePlayerId(), game); + } + + @Override + public UUID playerMustBeAttackedIfAble(Ability source, Game game) { + return source.getControllerId(); + } + +} diff --git a/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java b/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java index e1ac2ffa00..f7933a870f 100644 --- a/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java +++ b/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java @@ -243,20 +243,22 @@ public class ContinuousEffects implements Serializable { return effects.stream().filter(effect -> effect.hasLayer(layer)).collect(Collectors.toList()); } - public Map> getApplicableRequirementEffects(Permanent permanent, Game game) { + public Map> getApplicableRequirementEffects(Permanent permanent, boolean playerRealted, Game game) { Map> effects = new HashMap<>(); for (RequirementEffect effect : requirementEffects) { - Set abilities = requirementEffects.getAbility(effect.getId()); - Set 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 (playerRealted == effect.isPlayerRelated()) { + Set abilities = requirementEffects.getAbility(effect.getId()); + Set 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); + if (!applicableAbilities.isEmpty()) { + effects.put(effect, abilities); + } } } return effects; diff --git a/Mage/src/main/java/mage/abilities/effects/RequirementEffect.java b/Mage/src/main/java/mage/abilities/effects/RequirementEffect.java index 93a34bd5ff..89afa2bd49 100644 --- a/Mage/src/main/java/mage/abilities/effects/RequirementEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/RequirementEffect.java @@ -1,16 +1,16 @@ /* * 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 @@ -20,12 +20,11 @@ * 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.util.UUID; @@ -36,20 +35,33 @@ import mage.constants.Outcome; import mage.game.Game; import mage.game.permanent.Permanent; - /** * * @author BetaSteward_at_googlemail.com */ public abstract class RequirementEffect extends ContinuousEffectImpl { + boolean playerRelated; // defines a requirement that is more related to a player than a single creature + public RequirementEffect(Duration duration) { + this(duration, false); + } + + /** + * + * @param duration + * @param playerRelated defines a requirement that is more related to a + * player than a single creature + */ + public RequirementEffect(Duration duration, boolean playerRelated) { super(duration, Outcome.Detriment); this.effectType = EffectType.REQUIREMENT; + this.playerRelated = playerRelated; } public RequirementEffect(final RequirementEffect effect) { super(effect); + this.playerRelated = effect.playerRelated; } @Override @@ -67,6 +79,13 @@ public abstract class RequirementEffect extends ContinuousEffectImpl { return false; } + /** + * Defines the defender a attacker has to attack + * + * @param source + * @param game + * @return + */ public UUID mustAttackDefender(Ability source, Game game) { return null; } @@ -79,4 +98,20 @@ public abstract class RequirementEffect extends ContinuousEffectImpl { return null; } + /** + * Player related check The player returned or controlled planeswalker must + * be attacked with at least one attacker + * + * @param source + * @param game + * @return + */ + public UUID playerMustBeAttackedIfAble(Ability source, Game game) { + return null; + } + + public boolean isPlayerRelated() { + return playerRelated; + } + } diff --git a/Mage/src/main/java/mage/game/combat/Combat.java b/Mage/src/main/java/mage/game/combat/Combat.java index 496520ed0c..1ad1948a1b 100644 --- a/Mage/src/main/java/mage/game/combat/Combat.java +++ b/Mage/src/main/java/mage/game/combat/Combat.java @@ -306,7 +306,9 @@ public class Combat implements Serializable, Copyable { for (Permanent creature : player.getAvailableAttackers(game)) { boolean mustAttack = false; Set defendersForcedToAttack = new HashSet<>(); - for (Map.Entry> entry : game.getContinuousEffects().getApplicableRequirementEffects(creature, game).entrySet()) { + + // check if a creature has to attack + for (Map.Entry> entry : game.getContinuousEffects().getApplicableRequirementEffects(creature, false, game).entrySet()) { RequirementEffect effect = entry.getKey(); if (effect.mustAttack(game)) { mustAttack = true; @@ -379,6 +381,7 @@ public class Combat implements Serializable, Copyable { boolean check = true; int numberOfChecks = 0; UUID attackerToRemove = null; + Player attackingPlayer = game.getPlayer(attackingPlayerId); Check: while (check) { check = false; @@ -387,7 +390,6 @@ public class Combat implements Serializable, Copyable { for (CombatGroup group : groups) { numberAttackers += group.getAttackers().size(); } - Player attackingPlayer = game.getPlayer(attackingPlayerId); if (attackerToRemove != null) { removeAttacker(attackerToRemove, game); } @@ -579,7 +581,7 @@ public class Combat implements Serializable, Copyable { return; } for (Permanent possibleBlocker : game.getBattlefield().getActivePermanents(filterBlockers, attackingPlayer.getId(), game)) { - for (Map.Entry> requirementEntry : game.getContinuousEffects().getApplicableRequirementEffects(possibleBlocker, game).entrySet()) { + for (Map.Entry> requirementEntry : game.getContinuousEffects().getApplicableRequirementEffects(possibleBlocker, false, game).entrySet()) { if (requirementEntry.getKey().mustBlock(game)) { for (Ability ability : requirementEntry.getValue()) { UUID attackingCreatureId = requirementEntry.getKey().mustBlockAttacker(ability, game); @@ -656,7 +658,7 @@ public class Combat implements Serializable, Copyable { // Creature is already blocking but not forced to do so if (creature.getBlocking() > 0) { // get all requirement effects that apply to the creature (e.g. is able to block attacker) - for (Map.Entry> entry : game.getContinuousEffects().getApplicableRequirementEffects(creature, game).entrySet()) { + for (Map.Entry> entry : game.getContinuousEffects().getApplicableRequirementEffects(creature, false, game).entrySet()) { RequirementEffect effect = entry.getKey(); // get possible mustBeBlockedByAtLeastOne blocker for (Ability ability : entry.getValue()) { @@ -678,7 +680,7 @@ public class Combat implements Serializable, Copyable { // Creature is not blocking yet if (creature.getBlocking() == 0) { // get all requirement effects that apply to the creature - for (Map.Entry> entry : game.getContinuousEffects().getApplicableRequirementEffects(creature, game).entrySet()) { + for (Map.Entry> entry : game.getContinuousEffects().getApplicableRequirementEffects(creature, false, game).entrySet()) { RequirementEffect effect = entry.getKey(); // get possible mustBeBlockedByAtLeastOne blocker for (Ability ability : entry.getValue()) { diff --git a/Mage/src/main/java/mage/game/combat/CombatGroup.java b/Mage/src/main/java/mage/game/combat/CombatGroup.java index 3b900692b7..3bab5e6305 100644 --- a/Mage/src/main/java/mage/game/combat/CombatGroup.java +++ b/Mage/src/main/java/mage/game/combat/CombatGroup.java @@ -105,6 +105,10 @@ public class CombatGroup implements Serializable, Copyable { return defenderId; } + public UUID getDefendingPlayerId() { + return defendingPlayerId; + } + public List getAttackers() { return attackers; }