diff --git a/Mage.Sets/src/mage/cards/g/GhostfireBlade.java b/Mage.Sets/src/mage/cards/g/GhostfireBlade.java index 0a397ede810..cb4606ff3ee 100644 --- a/Mage.Sets/src/mage/cards/g/GhostfireBlade.java +++ b/Mage.Sets/src/mage/cards/g/GhostfireBlade.java @@ -1,7 +1,5 @@ - package mage.cards.g; -import java.util.UUID; import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.costs.mana.GenericManaCost; @@ -11,38 +9,53 @@ import mage.abilities.keyword.EquipAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.SubType; import mage.constants.Outcome; +import mage.constants.SubType; import mage.constants.Zone; import mage.game.Game; import mage.game.permanent.Permanent; import mage.util.CardUtil; +import java.util.Objects; +import java.util.UUID; + /** - * * @author LevelX2 */ public final class GhostfireBlade extends CardImpl { public GhostfireBlade(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.ARTIFACT},"{1}"); + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{1}"); this.subtype.add(SubType.EQUIPMENT); // Equipped creature gets +2/+2 this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new BoostEquippedEffect(2, 2))); - + // Equip {3} - this.addAbility(new EquipAbility(Outcome.BoostCreature, new GenericManaCost(3))); + this.addAbility(new EquipAbility(Outcome.BoostCreature, new GenericManaCost(3))); // todo // Ghostfire Blade's equip ability costs {2} less to activate if it targets a colorless creature. this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new InfoEffect("{this}'s equip ability costs {2} less to activate if it targets a colorless creature"))); } - @Override + + @Override public void adjustCosts(Ability ability, Game game) { if (ability instanceof EquipAbility) { - Permanent targetCreature = game.getPermanent(ability.getTargets().getFirstTarget()); - if (targetCreature != null && targetCreature.getColor(game).isColorless()) { - CardUtil.reduceCost(ability, 2); + if (game.inCheckPlayableState()) { + // checking state + boolean canSelectColorlessCreature = CardUtil.getAllPossibleTargets(ability, game).stream() + .map(game::getPermanent) + .filter(Objects::nonNull) + .anyMatch(permanent -> permanent.getColor(game).isColorless()); + if (canSelectColorlessCreature) { + CardUtil.reduceCost(ability, 2); + } + } else { + // real cast state + Permanent targetCreature = game.getPermanent(ability.getTargets().getFirstTarget()); + if (targetCreature != null && targetCreature.getColor(game).isColorless()) { + CardUtil.reduceCost(ability, 2); + } } } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/GhostfireBladeTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/GhostfireBladeTest.java new file mode 100644 index 00000000000..7e0ef3a5d3d --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/GhostfireBladeTest.java @@ -0,0 +1,43 @@ +package org.mage.test.cards.single; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author JayDi85 + */ + +public class GhostfireBladeTest extends CardTestPlayerBase { + + @Test + public void test_CanPlayWithCostReduce() { + // Equipped creature gets +2/+2. + // Equip {3} + // Ghostfire Blade’s equip ability costs {2} less to activate if it targets a colorless creature. + addCard(Zone.BATTLEFIELD, playerA, "Ghostfire Blade", 1); + // + addCard(Zone.HAND, playerA, "Alpha Myr", 1); // {2}, 2/1 + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); + + checkPlayableAbility("can't play", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Equip {3}", false); + + // add creature and activate cost reduce + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Alpha Myr"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkPlayableAbility("can't play wit no mana", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Equip {3}", false); // no mana after creature cast + + // can play on next turn + checkPlayableAbility("can play", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Equip {3}", true); + activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Equip {3}", "Alpha Myr"); + + setStopAt(3, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + assertAllCommandsUsed(); + + assertPowerToughness(playerA, "Alpha Myr", 2 + 2, 1 + 2); + } + +} diff --git a/Mage/src/main/java/mage/abilities/AbilityImpl.java b/Mage/src/main/java/mage/abilities/AbilityImpl.java index fe26af024e6..774bf1a25ac 100644 --- a/Mage/src/main/java/mage/abilities/AbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/AbilityImpl.java @@ -1226,6 +1226,14 @@ public abstract class AbilityImpl implements Ability { } } + /** + * Dynamic cost modification for ability. + * Example: if it need stack related info (like real targets) then must check two states (game.inCheckPlayableState): + * 1. In playable state it must check all possible use cases (e.g. allow to reduce on any available target and modes) + * 2. In real cast state it must check current use case (e.g. real selected targets and modes) + * + * @param costAdjuster + */ @Override public void setCostAdjuster(CostAdjuster costAdjuster) { this.costAdjuster = costAdjuster; diff --git a/Mage/src/main/java/mage/abilities/costs/CostAdjuster.java b/Mage/src/main/java/mage/abilities/costs/CostAdjuster.java index 7ac35fe9791..b7bc0d23a50 100644 --- a/Mage/src/main/java/mage/abilities/costs/CostAdjuster.java +++ b/Mage/src/main/java/mage/abilities/costs/CostAdjuster.java @@ -8,5 +8,14 @@ import mage.game.Game; */ public interface CostAdjuster { + /** + * Must check playable and real cast states. + * Example: if it need stack related info (like real targets) then must check two states (game.inCheckPlayableState): + * 1. In playable state it must check all possible use cases (e.g. allow to reduce on any available target and modes) + * 2. In real cast state it must check current use case (e.g. real selected targets and modes) + * + * @param ability + * @param game + */ void adjustCosts(Ability ability, Game game); } diff --git a/Mage/src/main/java/mage/abilities/effects/common/cost/SpellsCostModificationThatTargetSourceEffect.java b/Mage/src/main/java/mage/abilities/effects/common/cost/SpellsCostModificationThatTargetSourceEffect.java index da1f412c2a7..3a380982265 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/cost/SpellsCostModificationThatTargetSourceEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/cost/SpellsCostModificationThatTargetSourceEffect.java @@ -1,7 +1,6 @@ package mage.abilities.effects.common.cost; import mage.abilities.Ability; -import mage.abilities.Mode; import mage.abilities.SpellAbility; import mage.constants.CostModificationType; import mage.constants.Duration; @@ -11,13 +10,10 @@ import mage.filter.FilterCard; import mage.game.Game; import mage.game.stack.Spell; import mage.players.Player; -import mage.target.Target; import mage.util.CardUtil; -import java.util.Collection; import java.util.Set; import java.util.UUID; -import java.util.stream.Collectors; /** * @author JayDi85 @@ -119,12 +115,12 @@ public class SpellsCostModificationThatTargetSourceEffect extends CostModificati Spell spell = (Spell) game.getStack().getStackObject(abilityToModify.getId()); if (spell != null && this.spellFilter.match(spell, game)) { // real cast with put on stack - Set allTargets = getAllSelectedTargets(abilityToModify, source, game); + Set allTargets = CardUtil.getAllSelectedTargets(abilityToModify, game); return allTargets.contains(source.getSourceId()); } else { // get playable and other staff without put on stack // used at least for flashback ability because Flashback ability doesn't use stack - Set allTargets = getAllPossibleTargets(abilityToModify, source, game); + Set allTargets = CardUtil.getAllPossibleTargets(abilityToModify, game); switch (this.getModificationType()) { case REDUCE_COST: // reduce all the time @@ -137,27 +133,6 @@ public class SpellsCostModificationThatTargetSourceEffect extends CostModificati return false; } - private Set getAllSelectedTargets(Ability abilityToModify, Ability source, Game game) { - return abilityToModify.getModes().getSelectedModes() - .stream() - .map(abilityToModify.getModes()::get) - .map(Mode::getTargets) - .flatMap(Collection::stream) - .map(Target::getTargets) - .flatMap(Collection::stream) - .collect(Collectors.toSet()); - } - - private Set getAllPossibleTargets(Ability abilityToModify, Ability source, Game game) { - return abilityToModify.getModes().values() - .stream() - .map(Mode::getTargets) - .flatMap(Collection::stream) - .map(t -> t.possibleTargets(abilityToModify.getSourceId(), abilityToModify.getControllerId(), game)) - .flatMap(Collection::stream) - .collect(Collectors.toSet()); - } - public SpellsCostModificationThatTargetSourceEffect withTargetName(String targetName) { this.targetName = targetName; setText(); diff --git a/Mage/src/main/java/mage/cards/CardImpl.java b/Mage/src/main/java/mage/cards/CardImpl.java index b9a1b37f0e7..0147e84c915 100644 --- a/Mage/src/main/java/mage/cards/CardImpl.java +++ b/Mage/src/main/java/mage/cards/CardImpl.java @@ -1,12 +1,6 @@ package mage.cards; import com.google.common.collect.ImmutableList; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.UUID; import mage.MageObject; import mage.MageObjectImpl; import mage.Mana; @@ -33,6 +27,13 @@ import mage.util.SubTypeList; import mage.watchers.Watcher; import org.apache.log4j.Logger; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + public abstract class CardImpl extends MageObjectImpl implements Card { private static final long serialVersionUID = 1L; @@ -395,6 +396,15 @@ public abstract class CardImpl extends MageObjectImpl implements Card { return spellAbility; } + /** + * Dynamic cost modification for card (process only own abilities). + * Example: if it need stack related info (like real targets) then must check two states (game.inCheckPlayableState): + * 1. In playable state it must check all possible use cases (e.g. allow to reduce on any available target and modes) + * 2. In real cast state it must check current use case (e.g. real selected targets and modes) + * + * @param ability + * @param game + */ @Override public void adjustCosts(Ability ability, Game game) { ability.adjustCosts(game); diff --git a/Mage/src/main/java/mage/util/CardUtil.java b/Mage/src/main/java/mage/util/CardUtil.java index e80259326d2..9faaf31d911 100644 --- a/Mage/src/main/java/mage/util/CardUtil.java +++ b/Mage/src/main/java/mage/util/CardUtil.java @@ -4,6 +4,7 @@ import mage.MageObject; import mage.Mana; import mage.abilities.Abilities; import mage.abilities.Ability; +import mage.abilities.Mode; import mage.abilities.SpellAbility; import mage.abilities.costs.VariableCost; import mage.abilities.costs.mana.*; @@ -19,16 +20,15 @@ import mage.game.Game; import mage.game.permanent.Permanent; import mage.game.permanent.token.Token; import mage.game.stack.Spell; +import mage.target.Target; import mage.util.functions.CopyTokenFunction; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.net.URLEncoder; import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.UUID; +import java.util.*; +import java.util.stream.Collectors; /** * @author nantuko @@ -808,4 +808,25 @@ public final class CardUtil { return text; } } + + public static Set getAllSelectedTargets(Ability ability, Game game) { + return ability.getModes().getSelectedModes() + .stream() + .map(ability.getModes()::get) + .map(Mode::getTargets) + .flatMap(Collection::stream) + .map(Target::getTargets) + .flatMap(Collection::stream) + .collect(Collectors.toSet()); + } + + public static Set getAllPossibleTargets(Ability ability, Game game) { + return ability.getModes().values() + .stream() + .map(Mode::getTargets) + .flatMap(Collection::stream) + .map(t -> t.possibleTargets(ability.getSourceId(), ability.getControllerId(), game)) + .flatMap(Collection::stream) + .collect(Collectors.toSet()); + } }