* Cost reduction effects - fixed that some cards with cost reduction can't be played (example: Price of Fame, see #6685, #6684);

This commit is contained in:
Oleg Agafonov 2020-06-27 05:40:45 +04:00
parent 2252648f01
commit 520d75dba9
7 changed files with 70 additions and 32 deletions

View file

@ -1,7 +1,5 @@
package mage.cards.m; package mage.cards.m;
import java.util.Collection;
import java.util.UUID;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility; import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.condition.Condition; import mage.abilities.condition.Condition;
@ -17,6 +15,9 @@ import mage.game.stack.StackObject;
import mage.target.Target; import mage.target.Target;
import mage.target.TargetSpell; import mage.target.TargetSpell;
import java.util.Collection;
import java.util.UUID;
/** /**
* @author TheElk801 * @author TheElk801
*/ */
@ -27,7 +28,7 @@ public final class MysticalDispute extends CardImpl {
// This spell costs {2} less to cast if it targets a blue spell. // This spell costs {2} less to cast if it targets a blue spell.
this.addAbility(new SimpleStaticAbility( this.addAbility(new SimpleStaticAbility(
Zone.ALL, new SpellCostReductionSourceEffect(2, MysticalDisputeCondition.instance) Zone.ALL, new SpellCostReductionSourceEffect(2, MysticalDisputeCondition.instance).setCanWorksOnStackOnly(true)
).setRuleAtTheTop(true)); ).setRuleAtTheTop(true));
// Counter target spell unless its controller pays {3}. // Counter target spell unless its controller pays {3}.

View file

@ -1,7 +1,5 @@
package mage.cards.n; package mage.cards.n;
import java.util.Collection;
import java.util.UUID;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.Mode; import mage.abilities.Mode;
import mage.abilities.common.SimpleStaticAbility; import mage.abilities.common.SimpleStaticAbility;
@ -21,6 +19,9 @@ import mage.game.stack.StackObject;
import mage.target.Target; import mage.target.Target;
import mage.target.TargetStackObject; import mage.target.TargetStackObject;
import java.util.Collection;
import java.util.UUID;
/** /**
* @author Rafbill * @author Rafbill
*/ */
@ -41,7 +42,7 @@ public final class NotOfThisWorld extends CardImpl {
this.getSpellAbility().addTarget(new TargetStackObject(filter)); this.getSpellAbility().addTarget(new TargetStackObject(filter));
// Not of This World costs {7} less to cast if it targets a spell or ability that targets a creature you control with power 7 or greater. // Not of This World costs {7} less to cast if it targets a spell or ability that targets a creature you control with power 7 or greater.
this.addAbility(new SimpleStaticAbility(Zone.ALL, new SpellCostReductionSourceEffect(7, NotOfThisWorldCondition.instance))); this.addAbility(new SimpleStaticAbility(Zone.ALL, new SpellCostReductionSourceEffect(7, NotOfThisWorldCondition.instance).setCanWorksOnStackOnly(true)));
} }
private NotOfThisWorld(final NotOfThisWorld card) { private NotOfThisWorld(final NotOfThisWorld card) {
@ -86,8 +87,8 @@ enum NotOfThisWorldCondition implements Condition {
.flatMap(Collection::stream) .flatMap(Collection::stream)
.map(game::getPermanentOrLKIBattlefield) .map(game::getPermanentOrLKIBattlefield)
.anyMatch(permanent -> permanent != null && filter.match( .anyMatch(permanent -> permanent != null && filter.match(
permanent, sourceSpell.getSourceId(), sourceSpell.getControllerId(), game permanent, sourceSpell.getSourceId(), sourceSpell.getControllerId(), game
)); ));
} }
@Override @Override

View file

@ -1,6 +1,5 @@
package mage.cards.p; package mage.cards.p;
import java.util.UUID;
import mage.abilities.common.SimpleStaticAbility; import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.condition.Condition; import mage.abilities.condition.Condition;
import mage.abilities.condition.common.SourceTargetsPermanentCondition; import mage.abilities.condition.common.SourceTargetsPermanentCondition;
@ -16,6 +15,8 @@ import mage.filter.FilterPermanent;
import mage.filter.common.FilterCreaturePermanent; import mage.filter.common.FilterCreaturePermanent;
import mage.target.common.TargetCreaturePermanent; import mage.target.common.TargetCreaturePermanent;
import java.util.UUID;
/** /**
* @author TheElk801 * @author TheElk801
*/ */
@ -35,7 +36,7 @@ public final class PriceOfFame extends CardImpl {
// This spell costs {2} less to cast if it targets a legendary creature. // This spell costs {2} less to cast if it targets a legendary creature.
this.addAbility(new SimpleStaticAbility( this.addAbility(new SimpleStaticAbility(
Zone.ALL, new SpellCostReductionSourceEffect(2, condition) Zone.ALL, new SpellCostReductionSourceEffect(2, condition).setCanWorksOnStackOnly(true)
).setRuleAtTheTop(true)); ).setRuleAtTheTop(true));
// Destroy target creature. // Destroy target creature.

View file

@ -1,6 +1,5 @@
package mage.cards.s; package mage.cards.s;
import java.util.UUID;
import mage.abilities.common.SimpleStaticAbility; import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.condition.Condition; import mage.abilities.condition.Condition;
import mage.abilities.condition.common.SourceTargetsPermanentCondition; import mage.abilities.condition.common.SourceTargetsPermanentCondition;
@ -20,6 +19,8 @@ import mage.filter.common.FilterControlledCreaturePermanent;
import mage.target.common.TargetControlledCreaturePermanent; import mage.target.common.TargetControlledCreaturePermanent;
import mage.target.common.TargetCreaturePermanent; import mage.target.common.TargetCreaturePermanent;
import java.util.UUID;
/** /**
* @author TheElk801 * @author TheElk801
*/ */
@ -35,7 +36,7 @@ public final class SavageStomp extends CardImpl {
// Savage Stomp costs {2} less to cast if it targets a Dinosaur you control. // Savage Stomp costs {2} less to cast if it targets a Dinosaur you control.
this.addAbility(new SimpleStaticAbility( this.addAbility(new SimpleStaticAbility(
Zone.ALL, new SpellCostReductionSourceEffect(2, condition) Zone.ALL, new SpellCostReductionSourceEffect(2, condition).setCanWorksOnStackOnly(true)
).setRuleAtTheTop(true)); ).setRuleAtTheTop(true));
// Put a +1/+1 counter on target creature you control. Then that creature fights target creature you don't control. // Put a +1/+1 counter on target creature you control. Then that creature fights target creature you don't control.

View file

@ -2,14 +2,13 @@ package org.mage.test.cards.cost.modification;
import mage.constants.PhaseStep; import mage.constants.PhaseStep;
import mage.constants.Zone; import mage.constants.Zone;
import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase; import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps;
/** /**
* @author JayDi85 * @author JayDi85
*/ */
public class CostReduceWithConditionTest extends CardTestPlayerBase { public class CostReduceWithConditionTest extends CardTestPlayerBaseWithAIHelps {
@Test @Test
public void test_PriceOfFame_Normal() { public void test_PriceOfFame_Normal() {
@ -31,9 +30,9 @@ public class CostReduceWithConditionTest extends CardTestPlayerBase {
} }
@Test @Test
@Ignore public void test_PriceOfFame_Reduce_Manual() {
// TODO: implement workaround like putToStackAsNonPlayable for abilities, see https://github.com/magefree/mage/issues/6685 // https://github.com/magefree/mage/issues/6685
public void test_PriceOfFame_Reduce() {
// {3}{B} // {3}{B}
// This spell costs {2} less to cast if it targets a legendary creature. // This spell costs {2} less to cast if it targets a legendary creature.
// Destroy target creature. // Destroy target creature.
@ -50,4 +49,26 @@ public class CostReduceWithConditionTest extends CardTestPlayerBase {
assertGraveyardCount(playerB, "Anje Falkenrath", 1); assertGraveyardCount(playerB, "Anje Falkenrath", 1);
} }
@Test
public void test_PriceOfFame_Reduce_AI() {
// https://github.com/magefree/mage/issues/6685
// {3}{B}
// This spell costs {2} less to cast if it targets a legendary creature.
// Destroy target creature.
addCard(Zone.HAND, playerA, "Price of Fame", 1);
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 4 - 2);
addCard(Zone.BATTLEFIELD, playerB, "Anje Falkenrath", 1);
// AI must see and play that cards too
aiPlayPriority(1, PhaseStep.PRECOMBAT_MAIN, playerA);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertAllCommandsUsed();
assertGraveyardCount(playerB, "Anje Falkenrath", 1);
}
} }

View file

@ -1,27 +1,28 @@
package mage.abilities.effects.common.cost; package mage.abilities.effects.common.cost;
import mage.constants.Duration;
import mage.constants.EffectType;
import mage.constants.Outcome;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.effects.ContinuousEffectImpl; import mage.abilities.effects.ContinuousEffectImpl;
import mage.abilities.effects.CostModificationEffect; import mage.abilities.effects.CostModificationEffect;
import mage.constants.CostModificationType; import mage.constants.CostModificationType;
import mage.constants.Duration;
import mage.constants.EffectType;
import mage.constants.Outcome;
import mage.game.Game; import mage.game.Game;
/** /**
* Simple implementation of a {@link CostModificationEffect} offering simplified * Simple implementation of a {@link CostModificationEffect} offering simplified
* construction to setup the object for use by the mage framework. * construction to setup the object for use by the mage framework.
*
* @author maurer.it_at_gmail.com * @author maurer.it_at_gmail.com
*/ */
public abstract class CostModificationEffectImpl extends ContinuousEffectImpl implements CostModificationEffect { public abstract class CostModificationEffectImpl extends ContinuousEffectImpl implements CostModificationEffect {
private final CostModificationType modificationType; private final CostModificationType modificationType;
public CostModificationEffectImpl ( Duration duration, Outcome outcome, CostModificationType type) { // if effect need real stack object to check then mark it as stack only (example: apply cost reduction if you target human creature)
private boolean worksOnStackOnly = false;
public CostModificationEffectImpl(Duration duration, Outcome outcome, CostModificationType type) {
super(duration, outcome); super(duration, outcome);
this.effectType = EffectType.COSTMODIFICATION; this.effectType = EffectType.COSTMODIFICATION;
this.modificationType = type; this.modificationType = type;
@ -30,23 +31,33 @@ public abstract class CostModificationEffectImpl extends ContinuousEffectImpl im
public CostModificationEffectImpl(final CostModificationEffectImpl effect) { public CostModificationEffectImpl(final CostModificationEffectImpl effect) {
super(effect); super(effect);
this.modificationType = effect.modificationType; this.modificationType = effect.modificationType;
this.worksOnStackOnly = effect.worksOnStackOnly;
} }
/** /**
* Overridden and 'no-op' implementation put in place. * Overridden and 'no-op' implementation put in place.
* *
* @see #apply(mage.game.Game, mage.abilities.Ability, mage.abilities.Ability)
*
* @param game * @param game
* @param source * @param source
* @return * @return
* @see #apply(mage.game.Game, mage.abilities.Ability, mage.abilities.Ability)
*/ */
@Override @Override
public final boolean apply ( Game game, Ability source ) { return false; } public final boolean apply(Game game, Ability source) {
return false;
}
@Override @Override
public CostModificationType getModificationType(){ public CostModificationType getModificationType() {
return this.modificationType; return this.modificationType;
} }
public CostModificationEffectImpl setCanWorksOnStackOnly(boolean worksOnStackOnly) {
this.worksOnStackOnly = worksOnStackOnly;
return this;
}
public boolean canWorksOnStackOnly() {
return this.worksOnStackOnly;
}
} }

View file

@ -93,7 +93,9 @@ public class SpellCostReductionSourceEffect extends CostModificationEffectImpl {
@Override @Override
public boolean applies(Ability abilityToModify, Ability source, Game game) { public boolean applies(Ability abilityToModify, Ability source, Game game) {
if (abilityToModify.getSourceId().equals(source.getSourceId()) && (abilityToModify instanceof SpellAbility)) { if (abilityToModify.getSourceId().equals(source.getSourceId()) && (abilityToModify instanceof SpellAbility)) {
return condition == null || condition.apply(game, source); // some conditions can works after put on stack, so skip it in get playable (allows user to put card on stack anyway)
boolean skipCondition = game.inCheckPlayableState() && canWorksOnStackOnly();
return condition == null || skipCondition || condition.apply(game, source);
} }
return false; return false;
} }