diff --git a/Mage.Sets/src/mage/sets/coldsnap/SurgingDementia.java b/Mage.Sets/src/mage/sets/coldsnap/SurgingDementia.java new file mode 100644 index 0000000000..72a52c42be --- /dev/null +++ b/Mage.Sets/src/mage/sets/coldsnap/SurgingDementia.java @@ -0,0 +1,67 @@ +/* + * Copyright 2010 BetaSteward_at_googlemail.com. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY BetaSteward_at_googlemail.com ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BetaSteward_at_googlemail.com OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of BetaSteward_at_googlemail.com. + */ +package mage.sets.coldsnap; + +import java.util.UUID; + +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.discard.DiscardCardYouChooseTargetEffect; +import mage.abilities.effects.common.discard.DiscardControllerEffect; +import mage.abilities.effects.common.discard.DiscardTargetEffect; +import mage.abilities.effects.keyword.RippleEffect; +import mage.cards.CardImpl; +import mage.constants.CardType; +import mage.constants.Rarity; +import mage.target.TargetPlayer; + +/** + * + * @author klayhamn + */ +public class SurgingDementia extends CardImpl { + + public SurgingDementia(UUID ownerId) { + super(ownerId, 72, "Surging Dementia", Rarity.COMMON, new CardType[]{CardType.SORCERY}, "{1}{B}"); + this.expansionSetCode = "CSP"; + + // Ripple 4 + this.getSpellAbility().addEffect(new RippleEffect(4)); + // Target player discards a card. + this.getSpellAbility().getEffects().add(new DiscardTargetEffect(1)); + this.getSpellAbility().getTargets().add(new TargetPlayer()); + } + + public SurgingDementia(final SurgingDementia card) { + super(card); + } + + @Override + public SurgingDementia copy() { + return new SurgingDementia(this); + } +} diff --git a/Mage.Sets/src/mage/sets/coldsnap/ThrummingStone.java b/Mage.Sets/src/mage/sets/coldsnap/ThrummingStone.java new file mode 100644 index 0000000000..2678180435 --- /dev/null +++ b/Mage.Sets/src/mage/sets/coldsnap/ThrummingStone.java @@ -0,0 +1,64 @@ +/* + * Copyright 2010 BetaSteward_at_googlemail.com. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY BetaSteward_at_googlemail.com ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BetaSteward_at_googlemail.com OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of BetaSteward_at_googlemail.com. + */ +package mage.sets.coldsnap; + +import java.util.UUID; +import mage.abilities.common.SpellCastControllerTriggeredAbility; +import mage.abilities.effects.keyword.RippleEffect; +import mage.cards.CardImpl; +import mage.constants.*; +import mage.filter.FilterSpell; + +/** + * + * @author klayhamn + */ +public class ThrummingStone extends CardImpl { + + //applies to all spells + private static final FilterSpell anySpellFilter = new FilterSpell(); + + public ThrummingStone(UUID ownerId) { + super(ownerId, 142, "Thrumming Stone", Rarity.RARE, new CardType[]{CardType.ARTIFACT}, "{5}"); + this.expansionSetCode = "CSP"; + this.supertype.add("Legendary"); + + addAbility(new SpellCastControllerTriggeredAbility(new RippleEffect(4, false), anySpellFilter, false, true)); + } + + public ThrummingStone(final ThrummingStone card) { + super(card); + } + + @Override + public ThrummingStone copy() { + return new ThrummingStone(this); + } + +} + diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/RippleTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/RippleTest.java new file mode 100644 index 0000000000..4217495e54 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/RippleTest.java @@ -0,0 +1,112 @@ +package org.mage.test.cards.abilities.keywords; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author klayhamn + */ +public class RippleTest extends CardTestPlayerBase { + + /** + * 702.59.Ripple + * 702.59a Ripple is a triggered ability that functions only while the card with ripple is on the stack. “Ripple N” means + * “When you cast this spell, you may reveal the top N cards of your library, or, if there are fewer than N cards in your + * library, you may reveal all the cards in your library. If you reveal cards from your library this way, you may cast any + * of those cards with the same name as this spell without paying their mana costs, then put all revealed cards not cast + * this way on the bottom of your library in any order.” + * 702.59b If a spell has multiple instances of ripple, each triggers separately. + */ + + @Test + public void testRippleWhenSameCardNotFound() { + + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 4); + addCard(Zone.HAND, playerA, "Surging Dementia",2 ); + addCard(Zone.LIBRARY, playerA, "Swamp", 4); + + + addCard(Zone.HAND, playerB, "Island", 3); + addCard(Zone.LIBRARY, playerB, "Island", 3); + + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Surging Dementia", playerB); + setChoice(playerA, "Yes"); + + setStopAt(2, PhaseStep.END_TURN); + + execute(); + + assertHandCount(playerB, 3); // should have 1 less + assertHandCount(playerA, 1); // after cast, one remains + + assertGraveyardCount(playerA, "Surging Dementia", 1); // 1 cast + assertGraveyardCount(playerB, "Island", 1); // 1 discarded + } + + @Test + public void testRippleWhenSameCardFoundOnce() { + + removeAllCardsFromLibrary(playerA); + + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 4); + addCard(Zone.HAND, playerA, "Surging Dementia",2 ); + addCard(Zone.LIBRARY, playerA, "Surging Dementia",1); + addCard(Zone.LIBRARY, playerA, "Swamp", 3); + + + addCard(Zone.HAND, playerB, "Island", 3); + addCard(Zone.LIBRARY, playerB, "Island", 3); + + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Surging Dementia", playerB); + setChoice(playerA, "Yes"); + addTarget(playerA, playerB); + + setStopAt(2, PhaseStep.END_TURN); + + execute(); + + assertHandCount(playerB, 2); // should have 2 less + assertHandCount(playerA, 1); // after cast, one remains in hand + assertGraveyardCount(playerA, "Surging Dementia", 2); // 2 were cast + assertGraveyardCount(playerB, "Island", 2); // 2 were discarded + + } + + @Test + public void testRippleWhenSameCardFoundMoreThanOnce() { + + + removeAllCardsFromLibrary(playerA); + skipInitShuffling(); + + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 4); + addCard(Zone.HAND, playerA, "Surging Dementia",2 ); + + addCard(Zone.LIBRARY, playerA, "Surging Dementia",1); + addCard(Zone.LIBRARY, playerA, "Swamp", 2); + addCard(Zone.LIBRARY, playerA, "Surging Dementia",1); + addCard(Zone.LIBRARY, playerA, "Swamp", 2); + + + addCard(Zone.HAND, playerB, "Island", 3); + addCard(Zone.LIBRARY, playerB, "Island", 3); + + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Surging Dementia", playerB); + setChoice(playerA, "Yes"); + addTarget(playerA, playerB); + + setStopAt(2, PhaseStep.END_TURN); + + execute(); + + assertHandCount(playerB, 1); // should have 3 less + assertHandCount(playerA, 1); // after cast, one remains in hand + assertGraveyardCount(playerA, "Surging Dementia", 3); // 3 were cast + assertGraveyardCount(playerB, "Island", 3); // 3 were discarded + } +} diff --git a/Mage/src/mage/abilities/effects/keyword/RippleEffect.java b/Mage/src/mage/abilities/effects/keyword/RippleEffect.java new file mode 100644 index 0000000000..db5dd8a718 --- /dev/null +++ b/Mage/src/mage/abilities/effects/keyword/RippleEffect.java @@ -0,0 +1,107 @@ +package mage.abilities.effects.keyword; + +import mage.MageObject; +import mage.abilities.Ability; +import mage.abilities.effects.OneShotEffect; +import mage.cards.Card; +import mage.cards.Cards; +import mage.cards.CardsImpl; +import mage.constants.Outcome; +import mage.constants.Zone; +import mage.filter.FilterCard; +import mage.filter.predicate.mageobject.NamePredicate; +import mage.game.Game; +import mage.game.stack.Spell; +import mage.players.Player; +import mage.target.TargetCard; +import mage.util.CardUtil; + +/** + * @author klayhamn + */ +public class RippleEffect extends OneShotEffect { + + protected int rippleNumber; + protected boolean isTargetSelf; // is the source of the ripple also the target of the ripple? + + public RippleEffect(int rippleNumber) { + this(rippleNumber, true); // by default, the source is also the target + } + + public RippleEffect(int rippleNumber, boolean isTargetSelf) { + super(Outcome.PlayForFree); + this.rippleNumber = rippleNumber; + this.isTargetSelf = isTargetSelf; + this.setText(); + } + + public RippleEffect(final RippleEffect effect) { + super(effect); + this.rippleNumber = effect.rippleNumber; + this.isTargetSelf = effect.isTargetSelf; + } + + @Override + public RippleEffect copy() { + return new RippleEffect(this); + } + + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + MageObject sourceObject = game.getObject(source.getSourceId()); + if (player != null) { + if (!player.chooseUse(Outcome.Neutral, "Reveal " + rippleNumber + " cards from the top of your library?", source, game)){ + return true; //fizzle + } + Cards cards = new CardsImpl(); + cards.addAll(player.getLibrary().getTopCards(game, rippleNumber)); // pull top cards + player.revealCards(sourceObject.getIdName(), cards, game); // reveal the cards + + // Find out which card should be rippled + // FIXME: I'm not sure the "isTargetSelf" flag is the most elegant solution + String cardNameToRipple; + if (isTargetSelf) { // if the ripple applies to the same card that triggered it + cardNameToRipple = sourceObject.getName(); + } else { // if the ripple is caused by something else (e.g. Thrumming Stone) + Spell spellOnStack = game.getStack().getSpell(targetPointer.getFirst(game, source)); + if (spellOnStack == null) { // if the ripple target got countered or exiled + spellOnStack = (Spell) game.getLastKnownInformation(targetPointer.getFirst(game, source), Zone.STACK); + } + if (spellOnStack == null) { + return true; // should not happen? + } + cardNameToRipple = spellOnStack.getName(); + } + + FilterCard sameNameFilter = new FilterCard("card(s) with the name: \"" + cardNameToRipple + "\" to cast without paying their mana cost"); + sameNameFilter.add(new NamePredicate(cardNameToRipple)); + TargetCard target1 = new TargetCard(Zone.LIBRARY, sameNameFilter); + target1.setRequired(false); + + // Choose cards to play for free + while (player.isInGame() && cards.count(sameNameFilter, game) > 0 && player.choose(Outcome.PlayForFree, cards, target1, game)) { + Card card = cards.get(target1.getFirstTarget(), game); + if (card != null) { + player.cast(card.getSpellAbility(), game, true); + cards.remove(card); + } + target1.clearChosen(); + } + // move cards that weren't cast to the bottom of the library + player.putCardsOnBottomOfLibrary(cards, game, source, true); + return true; + } + + return false; + } + + private void setText() { + StringBuilder sb = new StringBuilder("Ripple ").append(rippleNumber); + sb.append(". (You may reveal the top "); + sb.append(CardUtil.numberToText(rippleNumber)); + sb.append(" cards of your library. You may cast any revealed cards with the same name as this spell without paying their mana costs. Put the rest on the bottom of your library.)"); + staticText = sb.toString(); + } +}