From 4d798e81b8ca7eddb2f23bea759ae109494d3a61 Mon Sep 17 00:00:00 2001 From: klayhamn Date: Sun, 26 Jul 2015 19:30:15 +0300 Subject: [PATCH 1/4] add thrumming stone and surging dementia --- .../cards/abilities/keywords/RippleTest.java | 112 +++++++++++++++++ .../effects/keyword/RippleEffect.java | 118 ++++++++++++++++++ 2 files changed, 230 insertions(+) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/RippleTest.java create mode 100644 Mage/src/mage/abilities/effects/keyword/RippleEffect.java 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..3048249b38 --- /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 2 less + assertHandCount(playerA, 1); // after cast, one remains + + assertGraveyardCount(playerA, "Surging Dementia", 1); + assertGraveyardCount(playerB, "Island", 1); + } + + @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, none remain + assertGraveyardCount(playerA, "Surging Dementia", 2); + assertGraveyardCount(playerB, "Island", 2); + + } + + @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 2 less + assertHandCount(playerA, 1); // after cast, none remain + assertGraveyardCount(playerA, "Surging Dementia", 3); + assertGraveyardCount(playerB, "Island", 3); + } +} 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..4ad87f442d --- /dev/null +++ b/Mage/src/mage/abilities/effects/keyword/RippleEffect.java @@ -0,0 +1,118 @@ +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; + +import java.util.UUID; + +/** + * @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(); + } + + @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 false; //fizzle + } + + Cards cards = new CardsImpl(); + int count = Math.min(rippleNumber, player.getLibrary().size()); + if (count == 0) { + return true; + } + player.revealCards(sourceObject.getIdName(), cards, game); + for (int i = 0; i < count; i++) { + Card card = player.getLibrary().removeFromTop(game); + cards.add(card); + } + + // Select cards with the same name as the spell on which the ripple effect applies + // 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) + if (targetPointer == null) { + return true; // this might be possible if the "rememberSource" param wasn't used (which would constitute a bug) + } + UUID triggeringSpellID = targetPointer.getFirst(game, source); + Spell spellOnStack = game.getStack().getSpell(triggeringSpellID); + if (spellOnStack == null) { + return true; // spell was countered or exiled, effect should fizzle + } + 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); + // do we need to fire an event here? there is nothing that listens to ripple so far... + return true; + } + + return false; + } + + public RippleEffect(final RippleEffect effect) { + super(effect); + this.rippleNumber = effect.rippleNumber; + this.isTargetSelf = effect.isTargetSelf; + } + + @Override + public RippleEffect copy() { + return new RippleEffect(this); + } + + 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(); + } +} From 9621959922139462d928271f236e7866c4fca520 Mon Sep 17 00:00:00 2001 From: klayhamn Date: Sun, 26 Jul 2015 19:33:24 +0300 Subject: [PATCH 2/4] add thrumming stone and surging dementia --- .../mage/sets/coldsnap/SurgingDementia.java | 67 +++++++++++++++++++ .../mage/sets/coldsnap/ThrummingStone.java | 64 ++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 Mage.Sets/src/mage/sets/coldsnap/SurgingDementia.java create mode 100644 Mage.Sets/src/mage/sets/coldsnap/ThrummingStone.java 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); + } + +} + From 50b1085ebb12f39fb5ceb7f5941d49c583fc7cca Mon Sep 17 00:00:00 2001 From: klayhamn Date: Mon, 27 Jul 2015 02:01:57 +0300 Subject: [PATCH 3/4] fix RippleEffect so that it still triggers even when original spell is exiled or countered --- .../effects/keyword/RippleEffect.java | 53 ++++++++----------- 1 file changed, 21 insertions(+), 32 deletions(-) diff --git a/Mage/src/mage/abilities/effects/keyword/RippleEffect.java b/Mage/src/mage/abilities/effects/keyword/RippleEffect.java index 4ad87f442d..db5dd8a718 100644 --- a/Mage/src/mage/abilities/effects/keyword/RippleEffect.java +++ b/Mage/src/mage/abilities/effects/keyword/RippleEffect.java @@ -16,8 +16,6 @@ import mage.players.Player; import mage.target.TargetCard; import mage.util.CardUtil; -import java.util.UUID; - /** * @author klayhamn */ @@ -37,39 +35,42 @@ public class RippleEffect extends OneShotEffect { 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 false; //fizzle + if (!player.chooseUse(Outcome.Neutral, "Reveal " + rippleNumber + " cards from the top of your library?", source, game)){ + return true; //fizzle } - Cards cards = new CardsImpl(); - int count = Math.min(rippleNumber, player.getLibrary().size()); - if (count == 0) { - return true; - } - player.revealCards(sourceObject.getIdName(), cards, game); - for (int i = 0; i < count; i++) { - Card card = player.getLibrary().removeFromTop(game); - cards.add(card); - } + cards.addAll(player.getLibrary().getTopCards(game, rippleNumber)); // pull top cards + player.revealCards(sourceObject.getIdName(), cards, game); // reveal the cards - // Select cards with the same name as the spell on which the ripple effect applies + // 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) - if (targetPointer == null) { - return true; // this might be possible if the "rememberSource" param wasn't used (which would constitute a bug) + 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); } - UUID triggeringSpellID = targetPointer.getFirst(game, source); - Spell spellOnStack = game.getStack().getSpell(triggeringSpellID); if (spellOnStack == null) { - return true; // spell was countered or exiled, effect should fizzle + return true; // should not happen? } cardNameToRipple = spellOnStack.getName(); } @@ -90,24 +91,12 @@ public class RippleEffect extends OneShotEffect { } // move cards that weren't cast to the bottom of the library player.putCardsOnBottomOfLibrary(cards, game, source, true); - // do we need to fire an event here? there is nothing that listens to ripple so far... return true; } return false; } - public RippleEffect(final RippleEffect effect) { - super(effect); - this.rippleNumber = effect.rippleNumber; - this.isTargetSelf = effect.isTargetSelf; - } - - @Override - public RippleEffect copy() { - return new RippleEffect(this); - } - private void setText() { StringBuilder sb = new StringBuilder("Ripple ").append(rippleNumber); sb.append(". (You may reveal the top "); From 37dbdeb07f6e2aba0ac6b9d85add6c0bb30dfa64 Mon Sep 17 00:00:00 2001 From: klayhamn Date: Mon, 27 Jul 2015 02:11:08 +0300 Subject: [PATCH 4/4] correct some incorrect documentation comments in ripple test --- .../cards/abilities/keywords/RippleTest.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) 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 index 3048249b38..4217495e54 100644 --- 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 @@ -39,11 +39,11 @@ public class RippleTest extends CardTestPlayerBase { execute(); - assertHandCount(playerB, 3); // should have 2 less + assertHandCount(playerB, 3); // should have 1 less assertHandCount(playerA, 1); // after cast, one remains - assertGraveyardCount(playerA, "Surging Dementia", 1); - assertGraveyardCount(playerB, "Island", 1); + assertGraveyardCount(playerA, "Surging Dementia", 1); // 1 cast + assertGraveyardCount(playerB, "Island", 1); // 1 discarded } @Test @@ -70,9 +70,9 @@ public class RippleTest extends CardTestPlayerBase { execute(); assertHandCount(playerB, 2); // should have 2 less - assertHandCount(playerA, 1); // after cast, none remain - assertGraveyardCount(playerA, "Surging Dementia", 2); - assertGraveyardCount(playerB, "Island", 2); + assertHandCount(playerA, 1); // after cast, one remains in hand + assertGraveyardCount(playerA, "Surging Dementia", 2); // 2 were cast + assertGraveyardCount(playerB, "Island", 2); // 2 were discarded } @@ -104,9 +104,9 @@ public class RippleTest extends CardTestPlayerBase { execute(); - assertHandCount(playerB, 1); // should have 2 less - assertHandCount(playerA, 1); // after cast, none remain - assertGraveyardCount(playerA, "Surging Dementia", 3); - assertGraveyardCount(playerB, "Island", 3); + 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 } }