From 830765996f14bb9bad39e2ad0d37b1988b97f460 Mon Sep 17 00:00:00 2001 From: LevelX2 Date: Wed, 29 Apr 2015 17:37:54 +0200 Subject: [PATCH] * Storm - Fixed handling of countered Storm spells. * Reworked Rebound more rule conform. * Fixed that zone change counter was not raised if a card is moved to stack. --- .../mage/sets/avacynrestored/SecondGuess.java | 3 +- .../dragonsoftarkir/NarsetTranscendent.java | 59 +++-- .../riseoftheeldrazi/CastThroughTime.java | 64 +---- .../mage/sets/theros/PsychicIntrusion.java | 12 +- .../cards/abilities/keywords/ReboundTest.java | 93 +++++++- .../cards/abilities/keywords/StormTest.java | 29 ++- .../cards/single/roe/CastThroughTimeTest.java | 2 + .../abilities/keyword/ReboundAbility.java | 219 +++++------------- .../mage/abilities/keyword/StormAbility.java | 29 ++- Mage/src/mage/cards/CardImpl.java | 1 + Mage/src/mage/game/events/GameEvent.java | 14 +- Mage/src/mage/game/stack/SpellStack.java | 3 +- Mage/src/mage/players/PlayerImpl.java | 3 +- .../target/targetpointer/FixedTarget.java | 5 + .../common/CastSpellLastTurnWatcher.java | 4 +- 15 files changed, 276 insertions(+), 264 deletions(-) diff --git a/Mage.Sets/src/mage/sets/avacynrestored/SecondGuess.java b/Mage.Sets/src/mage/sets/avacynrestored/SecondGuess.java index 508850ac827..110acca0814 100644 --- a/Mage.Sets/src/mage/sets/avacynrestored/SecondGuess.java +++ b/Mage.Sets/src/mage/sets/avacynrestored/SecondGuess.java @@ -39,6 +39,7 @@ import mage.target.TargetSpell; import mage.watchers.common.CastSpellLastTurnWatcher; import java.util.UUID; +import mage.MageObjectReference; /** * @@ -79,7 +80,7 @@ class SecondSpellPredicate implements Predicate { public boolean apply(Spell input, Game game) { CastSpellLastTurnWatcher watcher = (CastSpellLastTurnWatcher) game.getState().getWatchers().get("CastSpellLastTurnWatcher"); - if (watcher.getSpellOrder(input, game) == 2) { + if (watcher.getSpellOrder(new MageObjectReference(input.getId(), game), game) == 2) { return true; } diff --git a/Mage.Sets/src/mage/sets/dragonsoftarkir/NarsetTranscendent.java b/Mage.Sets/src/mage/sets/dragonsoftarkir/NarsetTranscendent.java index acfe95ba890..e7a888462bf 100644 --- a/Mage.Sets/src/mage/sets/dragonsoftarkir/NarsetTranscendent.java +++ b/Mage.Sets/src/mage/sets/dragonsoftarkir/NarsetTranscendent.java @@ -34,6 +34,7 @@ import mage.abilities.DelayedTriggeredAbility; import mage.abilities.LoyaltyAbility; import mage.abilities.common.EntersBattlefieldAbility; import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.ContinuousEffectImpl; import mage.abilities.effects.ContinuousRuleModifyingEffectImpl; import mage.abilities.effects.Effect; import mage.abilities.effects.OneShotEffect; @@ -46,8 +47,10 @@ import mage.cards.CardImpl; import mage.cards.CardsImpl; import mage.constants.CardType; import mage.constants.Duration; +import mage.constants.Layer; import mage.constants.Outcome; import mage.constants.Rarity; +import mage.constants.SubLayer; import mage.constants.Zone; import mage.counters.CounterType; import mage.game.Game; @@ -76,9 +79,7 @@ public class NarsetTranscendent extends CardImpl { // -2: When you cast your next instant or sorcery spell from your hand this turn, it gains rebound. this.addAbility(new LoyaltyAbility(new CreateDelayedTriggeredAbilityEffect(new NarsetTranscendentTriggeredAbility()), -2)); - - - + // -9:You get an emblem with "Your opponents can't cast noncreature spells." this.addAbility(new LoyaltyAbility(new GetEmblemEffect(new NarsetTranscendentEmblem()), -9)); } @@ -132,11 +133,10 @@ class NarsetTranscendentEffect1 extends OneShotEffect { } } - class NarsetTranscendentTriggeredAbility extends DelayedTriggeredAbility { public NarsetTranscendentTriggeredAbility() { - super(new NarsetTranscendentGainAbilityEffect(), Duration.EndOfTurn, true); + super(new NarsetTranscendentGainReboundEffect(), Duration.EndOfTurn, true); } private NarsetTranscendentTriggeredAbility(final NarsetTranscendentTriggeredAbility ability) { @@ -175,33 +175,52 @@ class NarsetTranscendentTriggeredAbility extends DelayedTriggeredAbility { } } -class NarsetTranscendentGainAbilityEffect extends OneShotEffect { +class NarsetTranscendentGainReboundEffect extends ContinuousEffectImpl { - private final Ability ability; - - public NarsetTranscendentGainAbilityEffect() { - super(Outcome.AddAbility); - this.ability = new ReboundAbility(); + public NarsetTranscendentGainReboundEffect() { + super(Duration.Custom, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility); staticText = "it gains rebound"; } - public NarsetTranscendentGainAbilityEffect(final NarsetTranscendentGainAbilityEffect effect) { + public NarsetTranscendentGainReboundEffect(final NarsetTranscendentGainReboundEffect effect) { super(effect); - this.ability = effect.ability; } @Override - public NarsetTranscendentGainAbilityEffect copy() { - return new NarsetTranscendentGainAbilityEffect(this); + public NarsetTranscendentGainReboundEffect copy() { + return new NarsetTranscendentGainReboundEffect(this); } @Override public boolean apply(Game game, Ability source) { - Spell spell = game.getState().getStack().getSpell(getTargetPointer().getFirst(game, source)); - if (spell != null) { - ReboundAbility.addReboundEffectToSpellIfMissing(spell); + Player player = game.getPlayer(source.getControllerId()); + if (player != null) { + Spell spell = game.getStack().getSpell(getTargetPointer().getFirst(game, source)); + if (spell != null) { + Card card = spell.getCard(); + if (card != null) { + addReboundAbility(card, source, game); + } + } else { + discard(); + } + return true; + } + return false; + } + + private void addReboundAbility(Card card, Ability source, Game game) { + boolean found = false; + for (Ability ability : card.getAbilities()) { + if (ability instanceof ReboundAbility) { + found = true; + break; + } + } + if (!found) { + Ability ability = new ReboundAbility(); + game.getState().addOtherAbility(card, ability); } - return true; } } @@ -262,4 +281,4 @@ class NarsetTranscendentCantCastEffect extends ContinuousRuleModifyingEffectImpl } return false; } -} \ No newline at end of file +} diff --git a/Mage.Sets/src/mage/sets/riseoftheeldrazi/CastThroughTime.java b/Mage.Sets/src/mage/sets/riseoftheeldrazi/CastThroughTime.java index e03d3bf6580..a0ede84e902 100644 --- a/Mage.Sets/src/mage/sets/riseoftheeldrazi/CastThroughTime.java +++ b/Mage.Sets/src/mage/sets/riseoftheeldrazi/CastThroughTime.java @@ -27,29 +27,30 @@ */ package mage.sets.riseoftheeldrazi; -import mage.constants.CardType; -import mage.constants.Rarity; +import java.util.Iterator; +import java.util.UUID; import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.ContinuousEffectImpl; import mage.abilities.keyword.ReboundAbility; import mage.cards.Card; import mage.cards.CardImpl; -import mage.constants.*; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.Layer; +import mage.constants.Outcome; +import mage.constants.Rarity; +import mage.constants.SubLayer; +import mage.constants.Zone; import mage.filter.FilterCard; import mage.filter.predicate.Predicates; import mage.filter.predicate.mageobject.CardTypePredicate; import mage.game.Game; -import mage.game.events.GameEvent; -import mage.game.events.ZoneChangeEvent; import mage.game.permanent.Permanent; -import mage.players.Player; -import mage.watchers.Watcher; - -import java.util.Iterator; -import java.util.UUID; import mage.game.stack.Spell; import mage.game.stack.StackObject; +import mage.players.Player; + /** * @author magenoxx_at_gmail.com @@ -132,51 +133,8 @@ class GainReboundEffect extends ContinuousEffectImpl { } if (!found) { Ability ability = new ReboundAbility(); -// card.addAbility(ability); - ability.setControllerId(source.getControllerId()); - ability.setSourceId(card.getId()); game.getState().addOtherAbility(card, ability); } } } } - -//class AttachedReboundAbility extends ReboundAbility {} - -//class LeavesBattlefieldWatcher extends Watcher { -// -// public LeavesBattlefieldWatcher() { -// super("LeavesBattlefieldWatcher", WatcherScope.CARD); -// } -// -// public LeavesBattlefieldWatcher(final LeavesBattlefieldWatcher watcher) { -// super(watcher); -// } -// -// @Override -// public void watch(GameEvent event, Game game) { -// if (event.getType() == GameEvent.EventType.ZONE_CHANGE && event.getTargetId().equals(this.getSourceId())) { -// ZoneChangeEvent zEvent = (ZoneChangeEvent)event; -// if (zEvent.getFromZone() == Zone.BATTLEFIELD) { -// Player player = game.getPlayer(this.getControllerId()); -// if (player != null) { -// for (Card card : player.getHand().getCards(CastThroughTime.filter, game)) { -// Iterator it = card.getAbilities().iterator(); -// while (it.hasNext()) { -// if (it.next() instanceof AttachedReboundAbility) { -// it.remove(); -// } -// } -// } -// } -// } -// } -// } -// -// @Override -// public LeavesBattlefieldWatcher copy() { -// return new LeavesBattlefieldWatcher(this); -// } -// -//} - diff --git a/Mage.Sets/src/mage/sets/theros/PsychicIntrusion.java b/Mage.Sets/src/mage/sets/theros/PsychicIntrusion.java index c3e4c49f732..4d0a3eaa4ed 100644 --- a/Mage.Sets/src/mage/sets/theros/PsychicIntrusion.java +++ b/Mage.Sets/src/mage/sets/theros/PsychicIntrusion.java @@ -214,10 +214,16 @@ class PsychicIntrusionSpendAnyManaEffect extends AsThoughEffectImpl { @Override public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) { - if (objectId.equals(getTargetPointer().getFirst(game, source))) { - if (affectedControllerId.equals(source.getControllerId())) { - return true; + if (objectId.equals(((FixedTarget)getTargetPointer()).getTarget()) + && game.getState().getZoneChangeCounter(objectId) <= ((FixedTarget)getTargetPointer()).getZoneChangeCounter() +1) { + + if (affectedControllerId.equals(source.getControllerId())) { + // if the card moved from exile to spell the zone change counter is increased by 1 + if (game.getState().getZoneChangeCounter(objectId) == ((FixedTarget)getTargetPointer()).getZoneChangeCounter() +1) { + return true; + } } + } else { if (((FixedTarget)getTargetPointer()).getTarget().equals(objectId)) { // object has moved zone so effect can be discarted diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ReboundTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ReboundTest.java index f0fce19f6b7..552e4d9693f 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ReboundTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ReboundTest.java @@ -15,9 +15,16 @@ import org.mage.test.serverside.base.CardTestPlayerBase; */ public class ReboundTest extends CardTestPlayerBase{ + /** + * Test that the spell with rebound is moved to exile if + * the spell resolves + */ + @Test public void testCastFromHandMovedToExile() { addCard(Zone.BATTLEFIELD, playerA, "Island", 1); + + // Target creature gets +1/+0 until end of turn and is unblockable this turn. addCard(Zone.HAND, playerA, "Distortion Strike"); addCard(Zone.BATTLEFIELD, playerB, "Memnite", 1); @@ -27,9 +34,91 @@ public class ReboundTest extends CardTestPlayerBase{ setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); - //check exile and graveyard - + //check exile and graveyard assertExileCount("Distortion Strike", 1); assertGraveyardCount(playerA, 0); } + /** + * Test that the spell with rebound can be cast again + * on the beginning of the next upkeep without paying mana costs + */ + + @Test + public void testRecastFromExile() { + addCard(Zone.BATTLEFIELD, playerA, "Island", 1); + + // Target creature gets +1/+0 until end of turn and is unblockable this turn. + addCard(Zone.HAND, playerA, "Distortion Strike"); + + addCard(Zone.BATTLEFIELD, playerA, "Memnite", 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Distortion Strike", "Memnite"); + + setStopAt(3, PhaseStep.PRECOMBAT_MAIN); + execute(); + + //check exile and graveyard + assertPowerToughness(playerA, "Memnite", 2, 1); + assertExileCount("Distortion Strike", 0); + assertGraveyardCount(playerA, "Distortion Strike", 1); + } + + /** + * Check that a countered spell with rebound + * is not cast again + */ + + @Test + public void testDontRecastAfterCounter() { + addCard(Zone.BATTLEFIELD, playerA, "Island", 1); + + // Target creature gets +1/+0 until end of turn and is unblockable this turn. + addCard(Zone.HAND, playerA, "Distortion Strike"); + addCard(Zone.BATTLEFIELD, playerA, "Memnite", 1); + + addCard(Zone.BATTLEFIELD, playerB, "Island", 2); + addCard(Zone.HAND, playerB, "Counterspell"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Distortion Strike", "Memnite"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Counterspell", "Distortion Strike"); + + setStopAt(3, PhaseStep.PRECOMBAT_MAIN); + execute(); + + //check exile and graveyard + assertGraveyardCount(playerB, "Counterspell", 1); + assertGraveyardCount(playerA, "Distortion Strike", 1); + + assertPowerToughness(playerA, "Memnite", 1, 1); + assertExileCount("Distortion Strike", 0); + } + + + /** + * Check that a fizzled spell with rebound + * is not cast again on the next controllers upkeep + */ + + @Test + public void testDontRecastAfterFizzling() { + addCard(Zone.BATTLEFIELD, playerA, "Island", 1); + + // Target creature gets +1/+0 until end of turn and is unblockable this turn. + addCard(Zone.HAND, playerA, "Distortion Strike"); + addCard(Zone.BATTLEFIELD, playerA, "Memnite", 1); + + addCard(Zone.BATTLEFIELD, playerB, "Mountain", 1); + addCard(Zone.HAND, playerB, "Lightning Bolt", 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Distortion Strike", "Memnite"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Lightning Bolt", "Memnite","Distortion Strike"); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + //check exile and graveyard + assertGraveyardCount(playerB, "Lightning Bolt", 1); + assertGraveyardCount(playerA, "Distortion Strike", 1); + assertGraveyardCount(playerA, "Memnite", 1); + } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/StormTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/StormTest.java index 7f2992f7ac9..9157c8dfa34 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/StormTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/StormTest.java @@ -30,7 +30,6 @@ package org.mage.test.cards.abilities.keywords; import mage.constants.PhaseStep; import mage.constants.Zone; -import mage.counters.CounterType; import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; @@ -137,4 +136,32 @@ public class StormTest extends CardTestPlayerBase { assertLife(playerB, 19); } + /** + * If a spell with storm gets countered, the strom trigger is also stifled, which isn't how its supposed to work. + * For example a Chalic of the Void set to 1 counters Flusterstorm and also counters the storm trigger, which shouldn't happen + */ + + + @Test + public void testStormSpellCountered() { + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); + // Grapeshot deals 1 damage to target creature or player. + // Storm (When you cast this spell, copy it for each spell cast before it this turn. You may choose new targets for the copies.) + addCard(Zone.HAND, playerA, "Grapeshot"); + addCard(Zone.HAND, playerA, "Lightning Bolt"); + + addCard(Zone.HAND, playerB, "Counterspell"); + addCard(Zone.BATTLEFIELD, playerB, "Island", 2); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerB); + + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Grapeshot", playerB); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Counterspell", "Grapeshot"); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertLife(playerB, 16); // 3 (Lightning Bolt) + 1 from Storm copied Grapeshot + } + } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/roe/CastThroughTimeTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/roe/CastThroughTimeTest.java index 5ded9e333a0..fd8e1bc84a1 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/roe/CastThroughTimeTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/roe/CastThroughTimeTest.java @@ -32,6 +32,8 @@ public class CastThroughTimeTest extends CardTestPlayerBase { setStopAt(3, PhaseStep.BEGIN_COMBAT); execute(); + assertGraveyardCount(playerA, "Lightning Bolt", 1); + assertLife(playerA, 20); assertLife(playerB, 14); } diff --git a/Mage/src/mage/abilities/keyword/ReboundAbility.java b/Mage/src/mage/abilities/keyword/ReboundAbility.java index 2ef850ce7df..6bd8c0d6527 100644 --- a/Mage/src/mage/abilities/keyword/ReboundAbility.java +++ b/Mage/src/mage/abilities/keyword/ReboundAbility.java @@ -28,30 +28,25 @@ package mage.abilities.keyword; -import mage.constants.Duration; -import mage.constants.Outcome; -import mage.constants.Zone; -import mage.MageObject; +import java.util.UUID; import mage.abilities.Ability; import mage.abilities.DelayedTriggeredAbility; import mage.abilities.SpellAbility; -import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.SimpleStaticAbility; import mage.abilities.condition.common.MyTurnCondition; -import mage.abilities.effects.Effect; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.ReplacementEffectImpl; import mage.cards.Card; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.constants.Zone; import mage.game.ExileZone; import mage.game.Game; import mage.game.events.GameEvent; -import mage.game.events.GameEvent.EventType; import mage.game.events.ZoneChangeEvent; import mage.game.stack.Spell; -import mage.game.stack.StackObject; import mage.players.Player; -import java.util.UUID; - /** * This ability has no effect by default and will always return false on the call * to apply. This is because of how the {@link ReboundEffect} works. It will @@ -73,163 +68,34 @@ import java.util.UUID; * * @author maurer.it_at_gmail.com, noxx */ -public class ReboundAbility extends TriggeredAbilityImpl { - //20101001 - 702.85 - private boolean installReboundEffect; - private static String reboundText = "Rebound (If you cast this spell from your hand, exile it as it resolves. At the beginning of your next upkeep, you may cast this card from exile without paying its mana cost.)"; + +public class ReboundAbility extends SimpleStaticAbility { public ReboundAbility() { - super(Zone.STACK, null); - this.installReboundEffect = false; + super(Zone.STACK, new ReboundCastFromHandReplacementEffect()); } - public ReboundAbility(final ReboundAbility ability) { + public ReboundAbility(ReboundAbility ability) { super(ability); - this.installReboundEffect = ability.installReboundEffect; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - //Something hit the stack from the hand, see if its a spell with this ability. - if (event.getType() == EventType.ZONE_CHANGE && - ((ZoneChangeEvent) event).getFromZone() == Zone.HAND && - ((ZoneChangeEvent) event).getToZone() == Zone.STACK) { - Card card = (Card) game.getObject(event.getTargetId()); - - if (card.getAbilities(game).contains(this)) { - this.installReboundEffect = true; - } - } - - //Only 'install' the effect on a successfully cast spell otherwise the user - //may cancel before paying its costs and potentially having two copies rebound - if (event.getType() == EventType.SPELL_CAST && this.installReboundEffect) { - Spell spell = game.getStack().getSpell(event.getTargetId()); - if (spell != null && spell.getSourceId().equals(this.getSourceId())) { - addReboundEffectToSpellIfMissing(spell); - this.installReboundEffect = false; - } - } - return false; - } - - @Override - public String getRule() { - return reboundText; } @Override public ReboundAbility copy() { - return new ReboundAbility(this); - } - - static public void addReboundEffectToSpellIfMissing(Spell spell) { - Effect reboundEffect = new ReboundEffect(); - boolean found = false; - for (Effect effect : spell.getSpellAbility().getEffects()) { - if (effect instanceof ReboundEffect) { - found = true; - break; - } - } - if (!found) { - spell.getSpellAbility().addEffect(reboundEffect); - } - } + return new ReboundAbility(this); + } } -/** - * Upon successful resolution of a spell with the {@link ReboundAbility} this effect - * will setup a {@link ReboundCastFromHandReplacementEffect replacement effect} which - * will only work once. It will then setup a {@link ReboundEffectCastFromExileDelayedTrigger delayed trigger} - * which will fire upon the controllers next upkeep. - * - * @author maurer.it_at_gmail.com - */ -class ReboundEffect extends OneShotEffect { - - public ReboundEffect() { - super(Outcome.Benefit); - } - - public ReboundEffect(ReboundEffect effect) { - super(effect); - } - - @Override - public boolean apply(Game game, Ability source) { - Spell sourceSpell = (Spell) game.getObject(source.getId()); - if (sourceSpell == null || !sourceSpell.isCopiedSpell()) { - MageObject mageObject = game.getObject(source.getSourceId()); - if (mageObject instanceof StackObject) { - StackObject sourceCard = (StackObject) mageObject; - ReboundEffectCastFromExileDelayedTrigger trigger = new ReboundEffectCastFromExileDelayedTrigger(sourceCard.getSourceId(), sourceCard.getSourceId()); - trigger.setControllerId(source.getControllerId()); - game.addDelayedTriggeredAbility(trigger); - - game.getContinuousEffects().addEffect(new ReboundCastFromHandReplacementEffect(source.getSourceId()), source); - return true; - } - } - return false; - } - - @Override - public ReboundEffect copy() { - return new ReboundEffect(this); - } -} - -/** - * This replacement effect needs to be created only when the spell with rebound - * successfully resolves. This will help to ensure that interactions with Leyline of the Void - * or any other such effect will not get in the way. This should take precendence - * in such interactions. - * - * @author maurer.it_at_gmail.com - */ class ReboundCastFromHandReplacementEffect extends ReplacementEffectImpl { - private static String replacementText = "Rebound - If you cast {this} from your hand, exile it as it resolves"; - private UUID cardId; - - ReboundCastFromHandReplacementEffect(UUID cardId) { - super(Duration.OneUse, Outcome.Exile); - this.cardId = cardId; - this.staticText = replacementText; + ReboundCastFromHandReplacementEffect() { + super(Duration.WhileOnStack, Outcome.Benefit); + this.staticText = "Rebound (If you cast this spell from your hand, exile it as it resolves. At the beginning of your next upkeep, you may cast this card from exile without paying its mana cost.)"; } ReboundCastFromHandReplacementEffect(ReboundCastFromHandReplacementEffect effect) { super(effect); - this.cardId = effect.cardId; - } - - @Override - public boolean apply(Game game, Ability source) { - throw new UnsupportedOperationException("Not supported."); - } - - @Override - public ReboundCastFromHandReplacementEffect copy() { - return new ReboundCastFromHandReplacementEffect(this); - } - - @Override - public boolean replaceEvent(GameEvent event, Ability source, Game game) { - Spell sourceSpell = (Spell) game.getObject(source.getId()); - if (sourceSpell != null && sourceSpell.isCopiedSpell()) { - return false; - } else { - Card sourceCard = (Card) game.getObject(source.getSourceId()); - Player player = game.getPlayer(sourceCard.getOwnerId()); - if (player != null) { - player.moveCardToExileWithInfo(sourceCard, this.cardId, new StringBuilder(player.getName()).append(" Rebound").toString(), source.getSourceId(), game, Zone.HAND, true); - this.used = true; - return true; - } - } - return false; } + @Override public boolean checksEventType(GameEvent event, Game game) { return event.getType() == GameEvent.EventType.ZONE_CHANGE; @@ -239,26 +105,50 @@ class ReboundCastFromHandReplacementEffect extends ReplacementEffectImpl { public boolean applies(GameEvent event, Ability source, Game game) { if (((ZoneChangeEvent) event).getFromZone() == Zone.STACK && ((ZoneChangeEvent) event).getToZone() == Zone.GRAVEYARD && - source.getSourceId() == this.cardId) { - return true; + event.getSourceId() == source.getSourceId()) { // if countered the source.sourceId is different or null if it fizzles + Spell spell = game.getStack().getSpell(event.getTargetId()); + if (spell != null && spell.getFromZone().equals(Zone.HAND)) { + return true; + } + } + return false; + } + @Override + public boolean replaceEvent(GameEvent event, Ability source, Game game) { + Spell sourceSpell = game.getStack().getSpell(source.getSourceId()); + if (sourceSpell != null && sourceSpell.isCopiedSpell()) { + return false; + } else { + Card sourceCard = game.getCard(source.getSourceId()); + if (sourceCard != null) { + Player player = game.getPlayer(sourceCard.getOwnerId()); + if (player != null) { + // Add the delayed triggered effect + ReboundEffectCastFromExileDelayedTrigger trigger = new ReboundEffectCastFromExileDelayedTrigger(source.getSourceId(), source.getSourceId()); + trigger.setControllerId(source.getControllerId()); + trigger.setSourceObject(source.getSourceObject(game), game); + game.addDelayedTriggeredAbility(trigger); + + player.moveCardToExileWithInfo(sourceCard, sourceCard.getId(), player.getName() + " Rebound", source.getSourceId(), game, Zone.STACK, true); + return true; + } + } } return false; } + @Override + public ReboundCastFromHandReplacementEffect copy() { + return new ReboundCastFromHandReplacementEffect(this); + } + } -/** - * This delayed trigger will tell the framework when its ok to kick off the - * {@link ReboundCastSpellFromExileEffect} by checking the step and the controller - * of this ability. When it is the controllers upkeep step the framework will then - * be told it can kick off the effect. - * - * @author maurer.it_at_gmail.com - */ + class ReboundEffectCastFromExileDelayedTrigger extends DelayedTriggeredAbility { ReboundEffectCastFromExileDelayedTrigger(UUID cardId, UUID sourceId) { - super(new ReboundCastSpellFromExileEffect(cardId)); + super(new ReboundCastSpellFromExileEffect()); setSourceId(sourceId); this.optional = true; } @@ -296,26 +186,23 @@ class ReboundEffectCastFromExileDelayedTrigger extends DelayedTriggeredAbility { class ReboundCastSpellFromExileEffect extends OneShotEffect { private static String castFromExileText = "Rebound - You may cast {this} from exile without paying its mana cost"; - private final UUID cardId; - ReboundCastSpellFromExileEffect(UUID cardId) { + ReboundCastSpellFromExileEffect() { super(Outcome.Benefit); - this.cardId = cardId; staticText = castFromExileText; } ReboundCastSpellFromExileEffect(ReboundCastSpellFromExileEffect effect) { super(effect); - this.cardId = effect.cardId; } @Override public boolean apply(Game game, Ability source) { - ExileZone zone = game.getExile().getExileZone(this.cardId); + ExileZone zone = game.getExile().getExileZone(source.getSourceId()); if (zone == null || zone.isEmpty()) { return false; } - Card reboundCard = zone.get(this.cardId, game); + Card reboundCard = zone.get(source.getSourceId(), game); Player player = game.getPlayer(source.getControllerId()); if (player != null && reboundCard != null) { SpellAbility ability = reboundCard.getSpellAbility(); diff --git a/Mage/src/mage/abilities/keyword/StormAbility.java b/Mage/src/mage/abilities/keyword/StormAbility.java index 8bc5b0b1937..10377a0b1f9 100644 --- a/Mage/src/mage/abilities/keyword/StormAbility.java +++ b/Mage/src/mage/abilities/keyword/StormAbility.java @@ -28,6 +28,7 @@ package mage.abilities.keyword; +import mage.MageObjectReference; import mage.abilities.Ability; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.effects.Effect; @@ -73,6 +74,7 @@ public class StormAbility extends TriggeredAbilityImpl { if (spell instanceof Spell) { for (Effect effect : this.getEffects()) { effect.setValue("StormSpell", spell); + effect.setValue("StormSpellRef", new MageObjectReference(spell.getId(), game)); } return true; } @@ -98,20 +100,23 @@ class StormEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { - Spell spell = (Spell) this.getValue("StormSpell"); - if (spell != null) { + MageObjectReference spellRef = (MageObjectReference) this.getValue("StormSpellRef"); + if (spellRef != null) { CastSpellLastTurnWatcher watcher = (CastSpellLastTurnWatcher) game.getState().getWatchers().get("CastSpellLastTurnWatcher"); - - int stormCount = watcher.getSpellOrder(spell, game) - 1; + int stormCount = watcher.getSpellOrder(spellRef, game) - 1; if (stormCount > 0) { - if (!game.isSimulation()) - game.informPlayers("Storm: " + spell.getName() + " will be copied " + stormCount + " time" + (stormCount > 1 ?"s":"")); - for (int i = 0; i < stormCount; i++) { - Spell copy = spell.copySpell(); - copy.setControllerId(source.getControllerId()); - copy.setCopiedSpell(true); - game.getStack().push(copy); - copy.chooseNewTargets(game, source.getControllerId()); + Spell spell = (Spell) this.getValue("StormSpell"); + if (spell != null) { + if (!game.isSimulation()) { + game.informPlayers("Storm: " + spell.getName() + " will be copied " + stormCount + " time" + (stormCount > 1 ?"s":"")); + } + for (int i = 0; i < stormCount; i++) { + Spell copy = spell.copySpell(); + copy.setControllerId(source.getControllerId()); + copy.setCopiedSpell(true); + game.getStack().push(copy); + copy.chooseNewTargets(game, source.getControllerId()); + } } } return true; diff --git a/Mage/src/mage/cards/CardImpl.java b/Mage/src/mage/cards/CardImpl.java index 28e73490eb0..d248e8ed280 100644 --- a/Mage/src/mage/cards/CardImpl.java +++ b/Mage/src/mage/cards/CardImpl.java @@ -461,6 +461,7 @@ public abstract class CardImpl extends MageObjectImpl implements Card { game.rememberLKI(mainCard.getId(), event.getFromZone(), this); } game.getStack().push(new Spell(this, ability.copy(), controllerId, event.getFromZone())); + updateZoneChangeCounter(game); setZone(event.getToZone(), game); game.fireEvent(event); return game.getState().getZone(mainCard.getId()) == Zone.STACK; diff --git a/Mage/src/mage/game/events/GameEvent.java b/Mage/src/mage/game/events/GameEvent.java index 0f515ccc77a..06838a6e656 100644 --- a/Mage/src/mage/game/events/GameEvent.java +++ b/Mage/src/mage/game/events/GameEvent.java @@ -99,7 +99,17 @@ public class GameEvent { GAIN_LIFE, GAINED_LIFE, LOSE_LIFE, LOST_LIFE, PLAY_LAND, LAND_PLAYED, - CAST_SPELL, SPELL_CAST, + CAST_SPELL, + + /* SPELL_CAST + targetId id of the spell that's cast + sourceId sourceId of the spell that's cast + playerId player that casts the spell + amount not used for this event + flag not used for this event + */ + SPELL_CAST, + ACTIVATE_ABILITY, ACTIVATED_ABILITY, ADD_MANA, MANA_ADDED, @@ -108,7 +118,7 @@ public class GameEvent { sourceId sourceId of the mana source playerId controller of the ability the mana was paid for amount not used for this event - flag indicates a special condition (e.g. TRUE if it's a colored mana from Cavern of Souls) + flag indicates a special condition */ MANA_PAYED, diff --git a/Mage/src/mage/game/stack/SpellStack.java b/Mage/src/mage/game/stack/SpellStack.java index 7f2179d4d77..174ac51cb32 100644 --- a/Mage/src/mage/game/stack/SpellStack.java +++ b/Mage/src/mage/game/stack/SpellStack.java @@ -101,8 +101,9 @@ public class SpellStack extends ArrayDeque { this.remove(stackObject); } stackObject.counter(sourceId, game); - if (!game.isSimulation()) + if (!game.isSimulation()) { game.informPlayers(counteredObjectName + " is countered by " + sourceObject.getLogName()); + } game.fireEvent(GameEvent.getEvent(GameEvent.EventType.COUNTERED, objectId, sourceId, stackObject.getControllerId())); } else if (!game.isSimulation()) { game.informPlayers(counteredObjectName + " could not be countered by " + sourceObject.getLogName()); diff --git a/Mage/src/mage/players/PlayerImpl.java b/Mage/src/mage/players/PlayerImpl.java index 324b04c48ca..6f6a2753cbe 100644 --- a/Mage/src/mage/players/PlayerImpl.java +++ b/Mage/src/mage/players/PlayerImpl.java @@ -931,8 +931,9 @@ public abstract class PlayerImpl implements Player, Serializable { GameEvent event = GameEvent.getEvent(GameEvent.EventType.SPELL_CAST, spell.getSpellAbility().getId(), spell.getSpellAbility().getSourceId(), playerId); event.setZone(fromZone); game.fireEvent(event); - if (!game.isSimulation()) + if (!game.isSimulation()) { game.informPlayers(new StringBuilder(name).append(spell.getActivatedMessage(game)).toString()); + } game.removeBookmark(bookmark); resetStoredBookmark(game); return true; diff --git a/Mage/src/mage/target/targetpointer/FixedTarget.java b/Mage/src/mage/target/targetpointer/FixedTarget.java index a75b8b3d532..3e82b76f26e 100644 --- a/Mage/src/mage/target/targetpointer/FixedTarget.java +++ b/Mage/src/mage/target/targetpointer/FixedTarget.java @@ -65,4 +65,9 @@ public class FixedTarget implements TargetPointer { public UUID getTarget() { return target; } + + public int getZoneChangeCounter() { + return zoneChangeCounter; + } + } diff --git a/Mage/src/mage/watchers/common/CastSpellLastTurnWatcher.java b/Mage/src/mage/watchers/common/CastSpellLastTurnWatcher.java index 8b0a36f626c..6498530c682 100644 --- a/Mage/src/mage/watchers/common/CastSpellLastTurnWatcher.java +++ b/Mage/src/mage/watchers/common/CastSpellLastTurnWatcher.java @@ -115,11 +115,11 @@ public class CastSpellLastTurnWatcher extends Watcher { } } - public int getSpellOrder(Spell spell, Game game) { + public int getSpellOrder(MageObjectReference spell, Game game) { int index = 0; for (MageObjectReference mor : spellsCastThisTurnInOrder) { index++; - if (mor.refersTo(spell, game)) { + if (mor.equals(spell)) { return index; } }