From 69d8fd189857a713e065cc0937536caba13bce2f Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Sun, 5 Jul 2020 01:08:43 +0400 Subject: [PATCH] Battlefield Thaumaturge - fixed that it doesn't allow to cast spells without full available mana (#6698); --- .../mage/cards/b/BattlefieldThaumaturge.java | 65 +++++++++++++++---- .../cards/abilities/keywords/SupportTest.java | 26 ++++---- .../modification/CostReduceForEachTest.java | 44 +++++++++++++ .../java/org/mage/test/player/TestPlayer.java | 3 +- .../base/impl/CardTestPlayerAPIImpl.java | 1 + .../src/main/java/mage/abilities/Ability.java | 9 ++- .../main/java/mage/abilities/AbilityImpl.java | 12 ++++ Mage/src/main/java/mage/abilities/Modes.java | 2 +- .../java/mage/game/stack/StackAbility.java | 14 ++-- 9 files changed, 144 insertions(+), 32 deletions(-) diff --git a/Mage.Sets/src/mage/cards/b/BattlefieldThaumaturge.java b/Mage.Sets/src/mage/cards/b/BattlefieldThaumaturge.java index 635027c5ad..9f6c37ff16 100644 --- a/Mage.Sets/src/mage/cards/b/BattlefieldThaumaturge.java +++ b/Mage.Sets/src/mage/cards/b/BattlefieldThaumaturge.java @@ -1,17 +1,15 @@ - package mage.cards.b; -import java.util.HashSet; -import java.util.Set; -import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; +import mage.abilities.Mode; import mage.abilities.SpellAbility; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.common.continuous.GainAbilitySourceEffect; import mage.abilities.effects.common.cost.CostModificationEffectImpl; import mage.abilities.keyword.HeroicAbility; import mage.abilities.keyword.HexproofAbility; +import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; @@ -22,8 +20,11 @@ import mage.game.stack.Spell; import mage.target.Target; import mage.util.CardUtil; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + /** - * * @author LevelX2 */ public final class BattlefieldThaumaturge extends CardImpl { @@ -37,6 +38,7 @@ public final class BattlefieldThaumaturge extends CardImpl { // Each instant and sorcery spell you cast costs 1 less to cast for each creature it targets. this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new BattlefieldThaumaturgeSpellsCostReductionEffect())); + // Heroic - Whenever you cast a spell that targets Battlefield Thaumaturge, Battlefield Thaumaturge gains hexproof until end of turn. this.addAbility(new HeroicAbility(new GainAbilitySourceEffect(HexproofAbility.getInstance(), Duration.EndOfTurn))); } @@ -64,25 +66,62 @@ class BattlefieldThaumaturgeSpellsCostReductionEffect extends CostModificationEf @Override public boolean apply(Game game, Ability source, Ability abilityToModify) { - Set creaturesTargeted = new HashSet<>(); - for (Target target : abilityToModify.getTargets()) { - for (UUID uuid : target.getTargets()) { - Permanent permanent = game.getPermanent(uuid); - if (permanent != null && permanent.isCreature()) { - creaturesTargeted.add(permanent.getId()); + int reduceAmount = 0; + if (game.inCheckPlayableState()) { + // checking state (search max possible targets) + reduceAmount = getMaxPossibleTargetCreatures(abilityToModify, game); + } else { + // real cast check + Set creaturesTargeted = new HashSet<>(); + for (Target target : abilityToModify.getAllSelectedTargets()) { + if (target.isNotTarget()) { + continue; + } + for (UUID uuid : target.getTargets()) { + Permanent permanent = game.getPermanent(uuid); + if (permanent != null && permanent.isCreature()) { + creaturesTargeted.add(permanent.getId()); + } } } + reduceAmount = creaturesTargeted.size(); } - CardUtil.reduceCost(abilityToModify, creaturesTargeted.size()); + CardUtil.reduceCost(abilityToModify, reduceAmount); return true; } + + private int getMaxPossibleTargetCreatures(Ability ability, Game game) { + // checks only one mode, so it can be wrong in rare use cases with multi-modes (example: mode one gives +2 and mode two gives another +1 -- total +3) + int maxAmount = 0; + for (Mode mode : ability.getModes().values()) { + for (Target target : mode.getTargets()) { + if (target.isNotTarget()) { + continue; + } + Set possibleList = target.possibleTargets(ability.getSourceId(), ability.getControllerId(), game); + possibleList.removeIf(id -> { + Permanent permanent = game.getPermanent(id); + return permanent == null || !permanent.isCreature(); + }); + int possibleAmount = Math.min(possibleList.size(), target.getMaxNumberOfTargets()); + maxAmount = Math.max(maxAmount, possibleAmount); + } + } + return maxAmount; + } + @Override public boolean applies(Ability abilityToModify, Ability source, Game game) { if ((abilityToModify instanceof SpellAbility) && abilityToModify.isControlledBy(source.getControllerId())) { Spell spell = (Spell) game.getStack().getStackObject(abilityToModify.getId()); - return spell != null && StaticFilters.FILTER_SPELL_INSTANT_OR_SORCERY.match(spell, game); + if (spell != null) { + return spell != null && StaticFilters.FILTER_CARD_INSTANT_OR_SORCERY.match(spell, game); + } else { + Card sourceCard = game.getCard(abilityToModify.getSourceId()); + return sourceCard != null && StaticFilters.FILTER_CARD_INSTANT_OR_SORCERY.match(sourceCard, game); + } } return false; } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/SupportTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/SupportTest.java index d8a08c2ee5..c826edcc87 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/SupportTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/SupportTest.java @@ -1,16 +1,14 @@ - package org.mage.test.cards.abilities.keywords; import mage.constants.PhaseStep; import mage.constants.Zone; import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; +import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps; /** - * * @author LevelX2 */ -public class SupportTest extends CardTestPlayerBase { +public class SupportTest extends CardTestPlayerBaseWithAIHelps { /** * Support Ability can target its source. Its cannot really. @@ -18,22 +16,26 @@ public class SupportTest extends CardTestPlayerBase { @Test public void testCreatureSupport() { addCard(Zone.BATTLEFIELD, playerA, "Forest", 7); - // When Gladehart Cavalry enters the battlefield, support 6. + // When Gladehart Cavalry enters the battlefield, support 6. (Put a +1/+1 counter on each of up to six other target creatures.) // Whenever a creature you control with a +1/+1 counter on it dies, you gain 2 life. addCard(Zone.HAND, playerA, "Gladehart Cavalry"); // {5}{G}{G} 6/6 - addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion"); - addCard(Zone.BATTLEFIELD, playerA, "Pillarfield Ox"); + addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion"); // 2/2 + addCard(Zone.BATTLEFIELD, playerA, "Pillarfield Ox"); // 2/4 - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Gladehart Cavalry"); - addTarget(playerA, "Silvercoat Lion^Pillarfield Ox^Gladehart Cavalry");// Gladehart Cavalry should not be allowed + // test framework do not support possible target checks, so allow AI to cast and choose maximum targets + aiPlayPriority(1, PhaseStep.PRECOMBAT_MAIN, playerA); + //castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Gladehart Cavalry"); + //addTarget(playerA, "Silvercoat Lion^Pillarfield Ox^Gladehart Cavalry");// Gladehart Cavalry should not be allowed + setStrictChooseMode(true); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); + assertAllCommandsUsed(); - assertPowerToughness(playerA, "Silvercoat Lion", 3, 3); - assertPowerToughness(playerA, "Pillarfield Ox", 3, 5); - assertPowerToughness(playerA, "Gladehart Cavalry", 6, 6); + assertPowerToughness(playerA, "Silvercoat Lion", 2 + 1, 2 + 1); + assertPowerToughness(playerA, "Pillarfield Ox", 2 + 1, 4 + 1); + assertPowerToughness(playerA, "Gladehart Cavalry", 6, 6); // no counters } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/cost/modification/CostReduceForEachTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/cost/modification/CostReduceForEachTest.java index 14cd2adff9..e8eab146be 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/cost/modification/CostReduceForEachTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/cost/modification/CostReduceForEachTest.java @@ -138,4 +138,48 @@ public class CostReduceForEachTest extends CardTestPlayerBaseWithAIHelps { assertPermanentCount(playerA, "Torgaar, Famine Incarnate", 1); assertLife(playerB, 20 / 2); } + + @Test + public void test_AshnodsAltar_SacrificeCost() { + // Sacrifice a creature: Add {C}{C}. + addCard(Zone.BATTLEFIELD, playerA, "Ashnod's Altar", 1); + // + addCard(Zone.HAND, playerA, "Alloy Myr", 1); // {3} + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3 - 2); + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // give 2 mana on sacrifice + + checkPlayableAbility("must play", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Alloy Myr", true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Alloy Myr"); + setChoice(playerA, "Balduvian Bears"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Alloy Myr", 1); + } + + @Test + public void test_BattlefieldThaumaturge_TargetCostReduce() { + // Each instant and sorcery spell you cast costs {1} less to cast for each creature it targets. + addCard(Zone.BATTLEFIELD, playerA, "Battlefield Thaumaturge", 1); + // + // {3}{R}{R} sorcery + // Shower of Coals deals 2 damage to each of up to three target creatures and/or players. + addCard(Zone.HAND, playerA, "Shower of Coals", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5 - 3); + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears@bear", 3); // add 3 cost reduce on target + + checkPlayableAbility("must play", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Shower of Coals", true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Shower of Coals"); + addTarget(playerA, "@bear.1^@bear.2^@bear.3"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertGraveyardCount(playerA, "Shower of Coals", 1); + } } diff --git a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java index 6658c6923e..9cf06e3bfc 100644 --- a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java +++ b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java @@ -2093,8 +2093,8 @@ public class TestPlayer implements Player { || target.getOriginalTarget() instanceof TargetPermanentOrPlayer || target.getOriginalTarget() instanceof TargetDefender) { for (String targetDefinition : targets) { - checkTargetDefinitionMarksSupport(target, targetDefinition, "="); if (targetDefinition.startsWith("targetPlayer=")) { + checkTargetDefinitionMarksSupport(target, targetDefinition, "="); String playerName = targetDefinition.substring(targetDefinition.indexOf("targetPlayer=") + 13); for (Player player : game.getPlayers().values()) { if (player.getName().equals(playerName) @@ -2119,6 +2119,7 @@ public class TestPlayer implements Player { String[] targetList = targetDefinition.split("\\^"); boolean targetFound = false; for (String targetName : targetList) { + targetFound = false; // must have all valid targets from list boolean originOnly = false; boolean copyOnly = false; if (targetName.endsWith("]")) { diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java index e1a0fab524..1b1e1460a9 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java @@ -1770,6 +1770,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement public void addTarget(TestPlayer player, String target, int timesToChoose) { for (int i = 0; i < timesToChoose; i++) { + assertAliaseSupportInActivateCommand(target, true); player.addTarget(target); } } diff --git a/Mage/src/main/java/mage/abilities/Ability.java b/Mage/src/main/java/mage/abilities/Ability.java index a1ebe909d0..d935970d10 100644 --- a/Mage/src/main/java/mage/abilities/Ability.java +++ b/Mage/src/main/java/mage/abilities/Ability.java @@ -190,13 +190,19 @@ public interface Ability extends Controllable, Serializable { /** * Retrieves all targets that must be satisfied before this ability is put - * onto the stack. + * onto the stack. Warning, return targets from first/current mode only. * * @return All {@link Targets} that must be satisfied before this ability is * put onto the stack. */ Targets getTargets(); + /** + * Retrieves all selected targets, read only. Multi-modes return different targets. + * Works on stack only (after real cast/activate) + */ + Targets getAllSelectedTargets(); + /** * Retrieves the {@link Target} located at the 0th index in the * {@link Targets}. A call to the method is equivalent to @@ -525,6 +531,7 @@ public interface Ability extends Controllable, Serializable { /** * For mtg's instances search, see rules example in 112.10b + * * @param ability * @return */ diff --git a/Mage/src/main/java/mage/abilities/AbilityImpl.java b/Mage/src/main/java/mage/abilities/AbilityImpl.java index e5d754223c..a89235f741 100644 --- a/Mage/src/main/java/mage/abilities/AbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/AbilityImpl.java @@ -846,6 +846,18 @@ public abstract class AbilityImpl implements Ability { return new Targets(); } + @Override + public Targets getAllSelectedTargets() { + Targets res = new Targets(); + for (UUID modeId : this.getModes().getSelectedModes()) { + Mode mode = this.getModes().get(modeId); + if (mode != null) { + res.addAll(mode.getTargets()); + } + } + return res; + } + @Override public UUID getFirstTarget() { return getTargets().getFirstTarget(); diff --git a/Mage/src/main/java/mage/abilities/Modes.java b/Mage/src/main/java/mage/abilities/Modes.java index 7099f3b8af..b06707d34c 100644 --- a/Mage/src/main/java/mage/abilities/Modes.java +++ b/Mage/src/main/java/mage/abilities/Modes.java @@ -23,7 +23,7 @@ public class Modes extends LinkedHashMap { public static final UUID CHOOSE_OPTION_CANCEL_ID = UUID.fromString("0125bd0c-5610-4eba-bc80-fc6d0a7b9de6"); private Mode currentMode; // the current mode of the selected modes - private final List selectedModes = new ArrayList<>(); // all selected modes (this + duplicate), for all code user getSelectedModes to keep modes order + private final List selectedModes = new ArrayList<>(); // all selected modes (this + duplicate), use getSelectedModes all the time to keep modes order private final Map selectedDuplicateModes = new LinkedHashMap<>(); // for 2x selects: copy mode and put it to duplicate list private final Map selectedDuplicateToOriginalModeRefs = new LinkedHashMap<>(); // for 2x selects: stores ref from duplicate to original mode diff --git a/Mage/src/main/java/mage/game/stack/StackAbility.java b/Mage/src/main/java/mage/game/stack/StackAbility.java index ba1d643f66..8cd5170561 100644 --- a/Mage/src/main/java/mage/game/stack/StackAbility.java +++ b/Mage/src/main/java/mage/game/stack/StackAbility.java @@ -1,9 +1,5 @@ package mage.game.stack; -import java.util.ArrayList; -import java.util.EnumSet; -import java.util.List; -import java.util.UUID; import mage.MageInt; import mage.MageObject; import mage.ObjectColor; @@ -34,6 +30,11 @@ import mage.util.GameLog; import mage.util.SubTypeList; import mage.watchers.Watcher; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import java.util.UUID; + /** * @author BetaSteward_at_googlemail.com */ @@ -307,6 +308,11 @@ public class StackAbility extends StackObjImpl implements Ability { return ability.getTargets(); } + @Override + public Targets getAllSelectedTargets() { + return ability.getAllSelectedTargets(); + } + @Override public void addTarget(Target target) { }