mirror of
https://github.com/correl/mage.git
synced 2024-12-26 03:00:11 +00:00
Battlefield Thaumaturge - fixed that it doesn't allow to cast spells without full available mana (#6698);
This commit is contained in:
parent
8e819ee614
commit
69d8fd1898
9 changed files with 144 additions and 32 deletions
|
@ -1,17 +1,15 @@
|
||||||
|
|
||||||
package mage.cards.b;
|
package mage.cards.b;
|
||||||
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.UUID;
|
|
||||||
import mage.MageInt;
|
import mage.MageInt;
|
||||||
import mage.abilities.Ability;
|
import mage.abilities.Ability;
|
||||||
|
import mage.abilities.Mode;
|
||||||
import mage.abilities.SpellAbility;
|
import mage.abilities.SpellAbility;
|
||||||
import mage.abilities.common.SimpleStaticAbility;
|
import mage.abilities.common.SimpleStaticAbility;
|
||||||
import mage.abilities.effects.common.continuous.GainAbilitySourceEffect;
|
import mage.abilities.effects.common.continuous.GainAbilitySourceEffect;
|
||||||
import mage.abilities.effects.common.cost.CostModificationEffectImpl;
|
import mage.abilities.effects.common.cost.CostModificationEffectImpl;
|
||||||
import mage.abilities.keyword.HeroicAbility;
|
import mage.abilities.keyword.HeroicAbility;
|
||||||
import mage.abilities.keyword.HexproofAbility;
|
import mage.abilities.keyword.HexproofAbility;
|
||||||
|
import mage.cards.Card;
|
||||||
import mage.cards.CardImpl;
|
import mage.cards.CardImpl;
|
||||||
import mage.cards.CardSetInfo;
|
import mage.cards.CardSetInfo;
|
||||||
import mage.constants.*;
|
import mage.constants.*;
|
||||||
|
@ -22,8 +20,11 @@ import mage.game.stack.Spell;
|
||||||
import mage.target.Target;
|
import mage.target.Target;
|
||||||
import mage.util.CardUtil;
|
import mage.util.CardUtil;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
|
||||||
* @author LevelX2
|
* @author LevelX2
|
||||||
*/
|
*/
|
||||||
public final class BattlefieldThaumaturge extends CardImpl {
|
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.
|
// 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()));
|
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.
|
// 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)));
|
this.addAbility(new HeroicAbility(new GainAbilitySourceEffect(HexproofAbility.getInstance(), Duration.EndOfTurn)));
|
||||||
}
|
}
|
||||||
|
@ -64,25 +66,62 @@ class BattlefieldThaumaturgeSpellsCostReductionEffect extends CostModificationEf
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean apply(Game game, Ability source, Ability abilityToModify) {
|
public boolean apply(Game game, Ability source, Ability abilityToModify) {
|
||||||
Set<UUID> creaturesTargeted = new HashSet<>();
|
int reduceAmount = 0;
|
||||||
for (Target target : abilityToModify.getTargets()) {
|
if (game.inCheckPlayableState()) {
|
||||||
for (UUID uuid : target.getTargets()) {
|
// checking state (search max possible targets)
|
||||||
Permanent permanent = game.getPermanent(uuid);
|
reduceAmount = getMaxPossibleTargetCreatures(abilityToModify, game);
|
||||||
if (permanent != null && permanent.isCreature()) {
|
} else {
|
||||||
creaturesTargeted.add(permanent.getId());
|
// real cast check
|
||||||
|
Set<UUID> 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;
|
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<UUID> 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
|
@Override
|
||||||
public boolean applies(Ability abilityToModify, Ability source, Game game) {
|
public boolean applies(Ability abilityToModify, Ability source, Game game) {
|
||||||
if ((abilityToModify instanceof SpellAbility)
|
if ((abilityToModify instanceof SpellAbility)
|
||||||
&& abilityToModify.isControlledBy(source.getControllerId())) {
|
&& abilityToModify.isControlledBy(source.getControllerId())) {
|
||||||
Spell spell = (Spell) game.getStack().getStackObject(abilityToModify.getId());
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,14 @@
|
||||||
|
|
||||||
package org.mage.test.cards.abilities.keywords;
|
package org.mage.test.cards.abilities.keywords;
|
||||||
|
|
||||||
import mage.constants.PhaseStep;
|
import mage.constants.PhaseStep;
|
||||||
import mage.constants.Zone;
|
import mage.constants.Zone;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.mage.test.serverside.base.CardTestPlayerBase;
|
import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
|
||||||
* @author LevelX2
|
* @author LevelX2
|
||||||
*/
|
*/
|
||||||
public class SupportTest extends CardTestPlayerBase {
|
public class SupportTest extends CardTestPlayerBaseWithAIHelps {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Support Ability can target its source. Its cannot really.
|
* Support Ability can target its source. Its cannot really.
|
||||||
|
@ -18,22 +16,26 @@ public class SupportTest extends CardTestPlayerBase {
|
||||||
@Test
|
@Test
|
||||||
public void testCreatureSupport() {
|
public void testCreatureSupport() {
|
||||||
addCard(Zone.BATTLEFIELD, playerA, "Forest", 7);
|
addCard(Zone.BATTLEFIELD, playerA, "Forest", 7);
|
||||||
// When Gladehart Cavalry enters the battlefield, support 6.
|
// When Gladehart Cavalry enters the battlefield, support 6. <i>(Put a +1/+1 counter on each of up to six other target creatures.)</i>
|
||||||
// Whenever a creature you control with a +1/+1 counter on it dies, you gain 2 life.
|
// 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.HAND, playerA, "Gladehart Cavalry"); // {5}{G}{G} 6/6
|
||||||
|
|
||||||
addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion");
|
addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion"); // 2/2
|
||||||
addCard(Zone.BATTLEFIELD, playerA, "Pillarfield Ox");
|
addCard(Zone.BATTLEFIELD, playerA, "Pillarfield Ox"); // 2/4
|
||||||
|
|
||||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Gladehart Cavalry");
|
// test framework do not support possible target checks, so allow AI to cast and choose maximum targets
|
||||||
addTarget(playerA, "Silvercoat Lion^Pillarfield Ox^Gladehart Cavalry");// Gladehart Cavalry should not be allowed
|
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);
|
setStopAt(1, PhaseStep.BEGIN_COMBAT);
|
||||||
execute();
|
execute();
|
||||||
|
assertAllCommandsUsed();
|
||||||
|
|
||||||
assertPowerToughness(playerA, "Silvercoat Lion", 3, 3);
|
assertPowerToughness(playerA, "Silvercoat Lion", 2 + 1, 2 + 1);
|
||||||
assertPowerToughness(playerA, "Pillarfield Ox", 3, 5);
|
assertPowerToughness(playerA, "Pillarfield Ox", 2 + 1, 4 + 1);
|
||||||
assertPowerToughness(playerA, "Gladehart Cavalry", 6, 6);
|
assertPowerToughness(playerA, "Gladehart Cavalry", 6, 6); // no counters
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -138,4 +138,48 @@ public class CostReduceForEachTest extends CardTestPlayerBaseWithAIHelps {
|
||||||
assertPermanentCount(playerA, "Torgaar, Famine Incarnate", 1);
|
assertPermanentCount(playerA, "Torgaar, Famine Incarnate", 1);
|
||||||
assertLife(playerB, 20 / 2);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2093,8 +2093,8 @@ public class TestPlayer implements Player {
|
||||||
|| target.getOriginalTarget() instanceof TargetPermanentOrPlayer
|
|| target.getOriginalTarget() instanceof TargetPermanentOrPlayer
|
||||||
|| target.getOriginalTarget() instanceof TargetDefender) {
|
|| target.getOriginalTarget() instanceof TargetDefender) {
|
||||||
for (String targetDefinition : targets) {
|
for (String targetDefinition : targets) {
|
||||||
checkTargetDefinitionMarksSupport(target, targetDefinition, "=");
|
|
||||||
if (targetDefinition.startsWith("targetPlayer=")) {
|
if (targetDefinition.startsWith("targetPlayer=")) {
|
||||||
|
checkTargetDefinitionMarksSupport(target, targetDefinition, "=");
|
||||||
String playerName = targetDefinition.substring(targetDefinition.indexOf("targetPlayer=") + 13);
|
String playerName = targetDefinition.substring(targetDefinition.indexOf("targetPlayer=") + 13);
|
||||||
for (Player player : game.getPlayers().values()) {
|
for (Player player : game.getPlayers().values()) {
|
||||||
if (player.getName().equals(playerName)
|
if (player.getName().equals(playerName)
|
||||||
|
@ -2119,6 +2119,7 @@ public class TestPlayer implements Player {
|
||||||
String[] targetList = targetDefinition.split("\\^");
|
String[] targetList = targetDefinition.split("\\^");
|
||||||
boolean targetFound = false;
|
boolean targetFound = false;
|
||||||
for (String targetName : targetList) {
|
for (String targetName : targetList) {
|
||||||
|
targetFound = false; // must have all valid targets from list
|
||||||
boolean originOnly = false;
|
boolean originOnly = false;
|
||||||
boolean copyOnly = false;
|
boolean copyOnly = false;
|
||||||
if (targetName.endsWith("]")) {
|
if (targetName.endsWith("]")) {
|
||||||
|
|
|
@ -1770,6 +1770,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
|
||||||
|
|
||||||
public void addTarget(TestPlayer player, String target, int timesToChoose) {
|
public void addTarget(TestPlayer player, String target, int timesToChoose) {
|
||||||
for (int i = 0; i < timesToChoose; i++) {
|
for (int i = 0; i < timesToChoose; i++) {
|
||||||
|
assertAliaseSupportInActivateCommand(target, true);
|
||||||
player.addTarget(target);
|
player.addTarget(target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -190,13 +190,19 @@ public interface Ability extends Controllable, Serializable {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves all targets that must be satisfied before this ability is put
|
* 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
|
* @return All {@link Targets} that must be satisfied before this ability is
|
||||||
* put onto the stack.
|
* put onto the stack.
|
||||||
*/
|
*/
|
||||||
Targets getTargets();
|
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
|
* Retrieves the {@link Target} located at the 0th index in the
|
||||||
* {@link Targets}. A call to the method is equivalent to
|
* {@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
|
* For mtg's instances search, see rules example in 112.10b
|
||||||
|
*
|
||||||
* @param ability
|
* @param ability
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -846,6 +846,18 @@ public abstract class AbilityImpl implements Ability {
|
||||||
return new Targets();
|
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
|
@Override
|
||||||
public UUID getFirstTarget() {
|
public UUID getFirstTarget() {
|
||||||
return getTargets().getFirstTarget();
|
return getTargets().getFirstTarget();
|
||||||
|
|
|
@ -23,7 +23,7 @@ public class Modes extends LinkedHashMap<UUID, Mode> {
|
||||||
public static final UUID CHOOSE_OPTION_CANCEL_ID = UUID.fromString("0125bd0c-5610-4eba-bc80-fc6d0a7b9de6");
|
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 Mode currentMode; // the current mode of the selected modes
|
||||||
private final List<UUID> selectedModes = new ArrayList<>(); // all selected modes (this + duplicate), for all code user getSelectedModes to keep modes order
|
private final List<UUID> selectedModes = new ArrayList<>(); // all selected modes (this + duplicate), use getSelectedModes all the time to keep modes order
|
||||||
private final Map<UUID, Mode> selectedDuplicateModes = new LinkedHashMap<>(); // for 2x selects: copy mode and put it to duplicate list
|
private final Map<UUID, Mode> selectedDuplicateModes = new LinkedHashMap<>(); // for 2x selects: copy mode and put it to duplicate list
|
||||||
private final Map<UUID, UUID> selectedDuplicateToOriginalModeRefs = new LinkedHashMap<>(); // for 2x selects: stores ref from duplicate to original mode
|
private final Map<UUID, UUID> selectedDuplicateToOriginalModeRefs = new LinkedHashMap<>(); // for 2x selects: stores ref from duplicate to original mode
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,5 @@
|
||||||
package mage.game.stack;
|
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.MageInt;
|
||||||
import mage.MageObject;
|
import mage.MageObject;
|
||||||
import mage.ObjectColor;
|
import mage.ObjectColor;
|
||||||
|
@ -34,6 +30,11 @@ import mage.util.GameLog;
|
||||||
import mage.util.SubTypeList;
|
import mage.util.SubTypeList;
|
||||||
import mage.watchers.Watcher;
|
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
|
* @author BetaSteward_at_googlemail.com
|
||||||
*/
|
*/
|
||||||
|
@ -307,6 +308,11 @@ public class StackAbility extends StackObjImpl implements Ability {
|
||||||
return ability.getTargets();
|
return ability.getTargets();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Targets getAllSelectedTargets() {
|
||||||
|
return ability.getAllSelectedTargets();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addTarget(Target target) {
|
public void addTarget(Target target) {
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue