* Fixed Miracle handling (fixes #447).

This commit is contained in:
LevelX2 2014-10-13 23:41:08 +02:00
parent dce9ea978e
commit 81408b3649
18 changed files with 222 additions and 63 deletions

View file

@ -66,7 +66,7 @@ public class BanishingStroke extends CardImpl {
this.getSpellAbility().addTarget(new TargetPermanent(filter));
// Miracle {W}
this.addAbility(new MiracleAbility(new ManaCostsImpl("{W}")));
this.addAbility(new MiracleAbility(this, new ManaCostsImpl("{W}")));
}
public BanishingStroke(final BanishingStroke card) {

View file

@ -58,7 +58,7 @@ public class BlessingsOfNature extends CardImpl {
this.getSpellAbility().addEffect(new BlessingsOfNatureEffect());
this.getSpellAbility().addTarget(new TargetCreaturePermanentAmount(4));
this.addAbility(new MiracleAbility(new ManaCostsImpl("{G}")));
this.addAbility(new MiracleAbility(this, new ManaCostsImpl("{G}")));
}
public BlessingsOfNature(final BlessingsOfNature card) {

View file

@ -61,7 +61,7 @@ public class BonfireOfTheDamned extends CardImpl {
this.getSpellAbility().addTarget(new TargetPlayer());
// Miracle {X}{R}
this.addAbility(new MiracleAbility(new ManaCostsImpl("{X}{R}")));
this.addAbility(new MiracleAbility(this, new ManaCostsImpl("{X}{R}")));
}
public BonfireOfTheDamned(final BonfireOfTheDamned card) {

View file

@ -58,7 +58,7 @@ public class DevastationTide extends CardImpl {
this.getSpellAbility().addEffect(new DevastationTideEffect());
// Miracle {1}{U}
this.addAbility(new MiracleAbility(new ManaCostsImpl("{1}{U}")));
this.addAbility(new MiracleAbility(this, new ManaCostsImpl("{1}{U}")));
}
public DevastationTide(final DevastationTide card) {

View file

@ -55,7 +55,7 @@ public class EntreatTheAngels extends CardImpl {
this.getSpellAbility().addEffect(new CreateTokenEffect(new AngelToken(), new ManacostVariableValue()));
// Miracle {X}{W}{W}
this.addAbility(new MiracleAbility(new ManaCostsImpl("{X}{W}{W}")));
this.addAbility(new MiracleAbility(this, new ManaCostsImpl("{X}{W}{W}")));
}
public EntreatTheAngels(final EntreatTheAngels card) {

View file

@ -55,7 +55,8 @@ public class ReforgeTheSoul extends CardImpl {
// Each player discards his or her hand, then draws seven cards.
this.getSpellAbility().addEffect(new ReforgeTheSoulEffect());
this.addAbility(new MiracleAbility(new ManaCostsImpl("{1}{R}")));
// Miracle {1}{R}
this.addAbility(new MiracleAbility(this, new ManaCostsImpl("{1}{R}")));
}
public ReforgeTheSoul(final ReforgeTheSoul card) {

View file

@ -65,7 +65,8 @@ public class RevengeOfTheHunted extends CardImpl {
effect.setText("and all creatures able to block it this turn do so");
this.getSpellAbility().addEffect(effect);
this.addAbility(new MiracleAbility(new ManaCostsImpl("{G}")));
// Miracle {G}
this.addAbility(new MiracleAbility(this, new ManaCostsImpl("{G}")));
}
public RevengeOfTheHunted(final RevengeOfTheHunted card) {

View file

@ -53,7 +53,7 @@ public class TemporalMastery extends CardImpl {
this.getSpellAbility().addEffect(ExileSpellEffect.getInstance());
// Miracle {1}{U}
this.addAbility(new MiracleAbility(new ManaCostsImpl("{1}{U}")));
this.addAbility(new MiracleAbility(this, new ManaCostsImpl("{1}{U}")));
}
public TemporalMastery(final TemporalMastery card) {

View file

@ -58,7 +58,7 @@ public class Terminus extends CardImpl {
// Put all creatures on the bottom of their owners' libraries.
this.getSpellAbility().addEffect(new TerminusEffect());
this.addAbility(new MiracleAbility(new ManaCostsImpl("{W}")));
this.addAbility(new MiracleAbility(this, new ManaCostsImpl("{W}")));
}
public Terminus(final Terminus card) {

View file

@ -52,7 +52,8 @@ public class ThunderousWrath extends CardImpl {
this.getSpellAbility().addEffect(new DamageTargetEffect(5));
this.getSpellAbility().addTarget(new TargetCreatureOrPlayer());
this.addAbility(new MiracleAbility(new ManaCostsImpl("{R}")));
// Miracle {R}
this.addAbility(new MiracleAbility(this, new ManaCostsImpl("{R}")));
}
public ThunderousWrath(final ThunderousWrath card) {

View file

@ -54,7 +54,7 @@ public class Vanishment extends CardImpl {
this.getSpellAbility().addTarget(new TargetNonlandPermanent());
// Miracle {U}
this.addAbility(new MiracleAbility(new ManaCostsImpl("{U}")));
this.addAbility(new MiracleAbility(this, new ManaCostsImpl("{U}")));
}
public Vanishment(final Vanishment card) {

View file

@ -17,7 +17,9 @@ public class MiracleTest extends CardTestPlayerBase {
public void testMiracleCost() {
addCard(Zone.BATTLEFIELD, playerA, "Island", 2);
addCard(Zone.BATTLEFIELD, playerA, "Plains", 2);
// Put all creatures on the bottom of their owners' libraries.
addCard(Zone.LIBRARY, playerA, "Terminus");
// Draw a card.
addCard(Zone.HAND, playerA, "Think Twice");
skipInitShuffling();
@ -55,4 +57,61 @@ public class MiracleTest extends CardTestPlayerBase {
assertPermanentCount(playerB, "Elite Vanguard", 0);
}
/**
* Test that you can cast a card by miracle if you don't put it back to library before casting
*/
@Test
public void testMiracleWillWorkFromHand() {
addCard(Zone.BATTLEFIELD, playerA, "Island", 1);
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1);
addCard(Zone.LIBRARY, playerA, "Plains");
addCard(Zone.LIBRARY, playerA, "Forest");
addCard(Zone.LIBRARY, playerA, "Thunderous Wrath"); // must be the top most card
addCard(Zone.HAND, playerA, "Brainstorm");
skipInitShuffling();
castSpell(1, PhaseStep.UPKEEP, playerA, "Brainstorm");
addTarget(playerA, "Forest");
addTarget(playerA, "Plains");
addTarget(playerA, playerB);
setStopAt(1, PhaseStep.DRAW);
execute();
assertGraveyardCount(playerA, "Brainstorm", 1);
assertHandCount(playerA, "Thunderous Wrath", 0);
assertGraveyardCount(playerA, "Thunderous Wrath", 1);
assertHandCount(playerA, 0);
// check Thunderous Wrath was played
assertLife(playerA, 20);
assertLife(playerB, 15);
}
/**
* Test that you can't cast a card by miracle if you put it back to library before casting
*/
@Test
public void testMiracleWontWorkFromLibrary() {
addCard(Zone.BATTLEFIELD, playerA, "Island", 1);
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4);
addCard(Zone.LIBRARY, playerA, "Plains");
addCard(Zone.LIBRARY, playerA, "Forest");
addCard(Zone.LIBRARY, playerA, "Thunderous Wrath");
addCard(Zone.HAND, playerA, "Brainstorm");
skipInitShuffling();
castSpell(1, PhaseStep.UPKEEP, playerA, "Brainstorm");
addTarget(playerA, "Thunderous Wrath");
addTarget(playerA, "Plains");
addTarget(playerA, playerB);
setStopAt(1, PhaseStep.DRAW);
execute();
// check Thunderous Wrath was not played
assertLife(playerA, 20);
assertLife(playerB, 20);
}
}

View file

@ -64,8 +64,10 @@ import java.util.Map;
import java.util.Set;
import java.util.UUID;
import mage.abilities.mana.ManaAbility;
import mage.cards.Card;
import mage.constants.Zone;
import mage.target.TargetSource;
import mage.target.common.TargetCardInHand;
/**
*
@ -395,6 +397,29 @@ public class TestPlayer extends ComputerPlayer {
}
}
if (target instanceof TargetCardInHand) {
for (String targetDefinition: targets) {
String[] targetList = targetDefinition.split("\\^");
boolean targetFound = false;
for (String targetName: targetList) {
for (Card card: this.getHand().getCards(((TargetCardInHand)target).getFilter(), game)) {
if (card.getName().equals(targetName) || (card.getName()+"-"+card.getExpansionSetCode()).equals(targetName)) {
if (((TargetCardInHand)target).canTarget(source.getControllerId(), card.getId(), source, game) && !target.getTargets().contains(card.getId())) {
target.add(card.getId(), game);
targetFound = true;
break;
}
}
}
}
if (targetFound) {
targets.remove(targetDefinition);
return true;
}
}
}
}
return super.chooseTarget(outcome, target, source, game);
}

View file

@ -28,9 +28,21 @@
package mage.abilities.keyword;
import mage.constants.Zone;
import mage.abilities.StaticAbility;
import mage.abilities.Ability;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.costs.Cost;
import mage.abilities.costs.mana.ManaCost;
import mage.abilities.costs.mana.ManaCosts;
import mage.abilities.effects.OneShotEffect;
import mage.cards.Card;
import mage.constants.Outcome;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.events.GameEvent.EventType;
import mage.players.Player;
import mage.target.targetpointer.FixedTarget;
import mage.watchers.common.MiracleWatcher;
/**
* 702.92. Miracle
@ -48,28 +60,52 @@ import mage.abilities.costs.Cost;
* If you don't want to cast it at that time (or you can't cast it, perhaps because
* there are no legal targets available), you won't be able to cast it later for the miracle cost.
*
* RULINGS:
* You still draw the card, whether you use the miracle ability or not. Any ability that
* triggers whenever you draw a card, for example, will trigger. If you don't cast the card
* using its miracle ability, it will remain in your hand.
*
* @author noxx
* You can reveal and cast a card with miracle on any turn, not just your own, if it's the
* first card you've drawn that turn.
*
* You don't have to reveal a drawn card with miracle if you don't wish to cast it at that time.
*
* You can cast a card for its miracle cost only as the miracle triggered ability resolves.
* If you don't want to cast it at that time (or you can't cast it, perhaps because there are
* no legal targets available), you won't be able to cast it later for the miracle cost.
*
* You cast the card with miracle during the resolution of the triggered ability. Ignore any timing
* restrictions based on the card's type.
*
* It's important to reveal a card with miracle before it is mixed with the other cards in your hand.
*
* Multiple card draws are always treated as a sequence of individual card draws. For example, if
* you haven't drawn any cards yet during a turn and cast a spell that instructs you to draw three
* cards, you'll draw them one at a time. Only the first card drawn this way may be revealed and cast
* using its miracle ability.
*
* If the card with miracle leaves your hand before the triggered ability resolves, you won't be able
* to cast it using its miracle ability.
*
* You draw your opening hand before any turn begins. Cards you draw for your opening hand
* can't be cast using miracle.
*
* @author noxx, LevelX2
*/
public class MiracleAbility extends StaticAbility {
private static final String staticRule = " (You may cast this card for its miracle cost when you draw it if it's the first card you drew this turn.)";
public class MiracleAbility extends TriggeredAbilityImpl {
private static final String staticRule = " <i>(You may cast this card for its miracle cost when you draw it if it's the first card you drew this turn.)<i/>";
private String ruleText;
public MiracleAbility(Cost cost) {
super(Zone.BATTLEFIELD, null);
addCost(cost);
ruleText = "Miracle" + cost.getText() + staticRule;
public MiracleAbility(Card card, ManaCosts miracleCosts) {
super(Zone.HAND, new MiracleEffect(miracleCosts), true);
card.addWatcher(new MiracleWatcher());
ruleText = "Miracle " + miracleCosts.getText() + staticRule;
}
public MiracleAbility(MiracleAbility miracleAbility) {
super(miracleAbility);
}
@Override
public String getRule() {
return ruleText;
public MiracleAbility(final MiracleAbility ability) {
super(ability);
this.ruleText = ability.ruleText;
}
@Override
@ -77,4 +113,63 @@ public class MiracleAbility extends StaticAbility {
return new MiracleAbility(this);
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
if (event.getType().equals(EventType.MIRACLE_CARD_REVEALED) && event.getSourceId().equals(getSourceId())) {
// Refer to the card at the zone it is now (hand)
FixedTarget fixedTarget = new FixedTarget(event.getSourceId());
fixedTarget.init(game, this);
getEffects().get(0).setTargetPointer(fixedTarget);
return true;
}
return false;
}
@Override
public String getRule() {
return ruleText;
}
}
class MiracleEffect extends OneShotEffect {
private final ManaCosts miracleCosts;
public MiracleEffect(ManaCosts miracleCosts) {
super(Outcome.Benefit);
this.staticText = "cast this card for it's miracle cost";
this.miracleCosts = miracleCosts;
}
public MiracleEffect(final MiracleEffect effect) {
super(effect);
this.miracleCosts = effect.miracleCosts;
}
@Override
public MiracleEffect copy() {
return new MiracleEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId());
// use target pointer here, so it's the same card that triggered the event (not gone back to library e.g.)
Card card = game.getCard(getTargetPointer().getFirst(game, source));
if (controller != null && card != null) {
ManaCosts costRef = card.getSpellAbility().getManaCostsToPay();
// replace with the new cost
costRef.clear();
costRef.add(miracleCosts);
controller.cast(card.getSpellAbility(), game, false);
// Reset the casting costs (in case the player cancels cast and plays the card later)
costRef.clear();
for (ManaCost manaCost : card.getSpellAbility().getManaCosts()) {
costRef.add(manaCost);
}
return true;
}
return false;
}
}

View file

@ -834,7 +834,6 @@ public abstract class GameImpl implements Game, Serializable {
}
state.getWatchers().add(new MorbidWatcher());
state.getWatchers().add(new CastSpellLastTurnWatcher());
state.getWatchers().add(new MiracleWatcher());
state.getWatchers().add(new SoulbondWatcher());
state.getWatchers().add(new PlayerLostLifeWatcher());
state.getWatchers().add(new BlockedAttackerWatcher());

View file

@ -81,6 +81,7 @@ public class GameEvent {
//player events
ZONE_CHANGE,
DRAW_CARD, DREW_CARD,
MIRACLE_CARD_REVEALED,
DISCARDED_CARD,
CYCLE_CARD, CYCLED_CARD,
CLASH, CLASHED,

View file

@ -70,9 +70,9 @@ public class CardsDrawnDuringDrawStepWatcher extends Watcher {
if (playerId != null) {
Integer amount = amountOfCardsDrawnThisTurn.get(playerId);
if (amount == null) {
amount = Integer.valueOf(1);
amount = 1;
} else {
amount = Integer.valueOf(amount + 1);
amount++;
}
amountOfCardsDrawnThisTurn.put(playerId, amount);
}
@ -82,7 +82,7 @@ public class CardsDrawnDuringDrawStepWatcher extends Watcher {
public int getAmountCardsDrawn(UUID playerId) {
Integer amount = amountOfCardsDrawnThisTurn.get(playerId);
if (amount != null) {
return amount.intValue();
return amount;
}
return 0;
}

View file

@ -32,23 +32,18 @@ import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.UUID;
import mage.constants.Outcome;
import mage.constants.WatcherScope;
import mage.constants.Zone;
import mage.abilities.Ability;
import mage.abilities.costs.mana.ManaCost;
import mage.abilities.costs.mana.ManaCosts;
import mage.abilities.keyword.MiracleAbility;
import mage.cards.Card;
import mage.cards.Cards;
import mage.cards.CardsImpl;
import mage.constants.Outcome;
import mage.constants.WatcherScope;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.players.Player;
import mage.watchers.Watcher;
/**
* Counts amount of cards drawn this turn by players.
* Asks players about Miracle ability to be activated if it the first card drawn this turn.
@ -80,9 +75,9 @@ public class MiracleWatcher extends Watcher {
if (playerId != null) {
Integer amount = amountOfCardsDrawnThisTurn.get(playerId);
if (amount == null) {
amount = Integer.valueOf(1);
amount = 1;
} else {
amount = Integer.valueOf(amount + 1);
amount++;
}
amountOfCardsDrawnThisTurn.put(playerId, amount);
if (amount == 1) {
@ -99,30 +94,12 @@ public class MiracleWatcher extends Watcher {
if (ability instanceof MiracleAbility) {
Player controller = game.getPlayer(ability.getControllerId());
if (controller != null) {
// FIXME: I don't like that I need to call it manually
// it's the place for bugs
game.getContinuousEffects().costModification(ability, game);
ManaCosts<ManaCost> manaCostsToPay = ability.getManaCostsToPay();
Cards cards = new CardsImpl(Zone.PICK);
Cards cards = new CardsImpl();
cards.add(card);
controller.lookAtCards("Miracle", cards, game);
if (controller.chooseUse(Outcome.Benefit, "Use Miracle " + manaCostsToPay.getText() + "?", game)) {
if (controller.chooseUse(Outcome.Benefit, "Reveal card to be able to use Miracle?", game)) {
controller.revealCards("Miracle", cards, game);
ManaCosts costRef = card.getSpellAbility().getManaCostsToPay();
// replace with the new cost
costRef.clear();
for (ManaCost manaCost : manaCostsToPay) {
costRef.add(manaCost);
}
controller.cast(card.getSpellAbility(), game, false);
// Reset the casting costs (in case the player cancels cast and plays the card later)
costRef.clear();
for (ManaCost manaCost : card.getSpellAbility().getManaCosts()) {
costRef.add(manaCost);
}
game.fireEvent(GameEvent.getEvent(GameEvent.EventType.MIRACLE_CARD_REVEALED, card.getId(), card.getId(),controller.getId()));
break;
}
}