Ghostfire Blade - fixed that it can't be played without full available mana (#6698);

This commit is contained in:
Oleg Agafonov 2020-07-02 20:37:59 +04:00
parent 2a31e8063b
commit 6dccaee9a4
7 changed files with 127 additions and 48 deletions

View file

@ -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);
}
}
}
}

View file

@ -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 Blades 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);
}
}

View file

@ -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;

View file

@ -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);
}

View file

@ -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<UUID> allTargets = getAllSelectedTargets(abilityToModify, source, game);
Set<UUID> 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<UUID> allTargets = getAllPossibleTargets(abilityToModify, source, game);
Set<UUID> 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<UUID> 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<UUID> 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();

View file

@ -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);

View file

@ -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<UUID> 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<UUID> 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());
}
}