mirror of
https://github.com/correl/mage.git
synced 2025-01-11 11:05:23 +00:00
* Fixed Miracle handling (fixes #447).
This commit is contained in:
parent
dce9ea978e
commit
81408b3649
18 changed files with 222 additions and 63 deletions
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue