Merge pull request #1151 from klayhamn/master

Adding the Ripple keyword, and two cards that use it
This commit is contained in:
LevelX2 2015-07-27 14:31:57 +02:00
commit 5fb17ce920
4 changed files with 350 additions and 0 deletions

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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
}
}

View file

@ -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(". <i>(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.)</i>");
staticText = sb.toString();
}
}