From 2be3f20320e4948f54108ea5bf9c2549cb527eaf Mon Sep 17 00:00:00 2001 From: "Johnny.Hastings@gmail.com" <> Date: Fri, 31 Mar 2017 18:33:17 -0500 Subject: [PATCH 1/2] Implemented Soul Burn card for the two sets it's a part of: Ice Age and Invasion. --- Mage.Sets/src/mage/cards/s/SoulBurn.java | 186 ++++++++++++++++++ Mage.Sets/src/mage/sets/IceAge.java | 5 +- Mage.Sets/src/mage/sets/Invasion.java | 2 +- .../oneshot/damage/SoulBurnTest.java | 179 +++++++++++++++++ .../main/java/mage/abilities/AbilityImpl.java | 6 +- 5 files changed, 374 insertions(+), 4 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/s/SoulBurn.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/abilities/oneshot/damage/SoulBurnTest.java diff --git a/Mage.Sets/src/mage/cards/s/SoulBurn.java b/Mage.Sets/src/mage/cards/s/SoulBurn.java new file mode 100644 index 00000000000..a6be472b811 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SoulBurn.java @@ -0,0 +1,186 @@ +/* + * 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.cards.s; + +import java.util.UUID; + +import mage.abilities.Ability; +import mage.abilities.costs.VariableCost; +import mage.abilities.costs.mana.VariableManaCost; +import mage.abilities.effects.OneShotEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.filter.FilterMana; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.common.TargetCreatureOrPlayer; + +/** + * @author Johnny E. Hastings + */ +public class SoulBurn extends CardImpl { + + public static final FilterMana filterBlackOrRed = new FilterMana(); + + static { + filterBlackOrRed.setBlack(true); + filterBlackOrRed.setRed(true); + } + + public SoulBurn(UUID ownerId, CardSetInfo setInfo) { + super(ownerId,setInfo,new CardType[]{CardType.SORCERY},"{X}{2}{B}"); + + // Spend only black or red mana on X. + // Soul Burn deals X damage to target creature or player. You gain life equal to the damage dealt for each black mana spent on X; not more life than the player's life total before Soul Burn dealt damage, or the creature's toughness. + this.getSpellAbility().addTarget(new TargetCreatureOrPlayer()); + this.getSpellAbility().addEffect(new SoulBurnEffect()); + VariableCost variableCost = this.getSpellAbility().getManaCostsToPay().getVariableCosts().get(0); + if (variableCost instanceof VariableManaCost) { + ((VariableManaCost) variableCost).setFilter(filterBlackOrRed); + ((VariableManaCost) variableCost).setFilter(filterBlackOrRed); + } + } + + public SoulBurn(final SoulBurn card) { + super(card); + } + + @Override + public SoulBurn copy() { + return new SoulBurn(this); + } +} + +class SoulBurnEffect extends OneShotEffect { + + public SoulBurnEffect() { + super(Outcome.Damage); + staticText = "Soul Burn deals X damage to target creature or player for each black or red mana spent on X. You gain life equal to the damage dealt for each black mana spent; not more life than the player's life total before Soul Burn dealt damage, or the creature's toughness."; + } + + public SoulBurnEffect(final SoulBurnEffect effect) { + super(effect); + } + + /*** + * @param game + * @param source + * @return + */ + @Override + public boolean apply(Game game, Ability source) { + + // Get the colors we care about. (This isn't racist, honestly.) + int amountBlack = source.getManaCostsToPay().getPayment().getBlack(); + int amountRed = source.getManaCostsToPay().getPayment().getRed(); + + // Get the colors we don't really care about. (See note above.) + int amountWhite = source.getManaCostsToPay().getPayment().getWhite(); + int amountGreen = source.getManaCostsToPay().getPayment().getGreen(); + int amountBlue = source.getManaCostsToPay().getPayment().getBlue(); + int amountColorless = source.getManaCostsToPay().getPayment().getColorless(); + + // Figure out what was spent on the spell in total, determine proper values for + // black and red, minus initial casting cost. + int totalColorlessForCastingCost = amountWhite + amountGreen + amountBlue + amountColorless; + int amountOffsetByColorless = 0; + if (totalColorlessForCastingCost > 0) { + amountOffsetByColorless = totalColorlessForCastingCost; + if (amountOffsetByColorless > 2) { + // The game should never let this happen, but I'll check anyway since I don't know + // the guts of the game [yet]. + amountOffsetByColorless = 2; + } + } + + // Remove 1 black to account for casting cost. + amountBlack--; + + // Determine if we need to offset the red or black values any further due to the + // amount of non-red and non-black paid. + if (amountOffsetByColorless < 2) { + int amountToOffsetBy = 2 - amountOffsetByColorless; + + if (amountRed > 0) { + if (amountRed >= amountToOffsetBy) { + // Pay all additional unpaid casting cost with red. + amountRed = amountRed - amountToOffsetBy; + } else { + // Red paid doesn't cover the 2 default required by the spell. + // Pay some in red, and some in black. + // If we're here, red is 1, and amountToOffetBy is 2. + // That means we can subtract 1 from both red and black. + amountRed--; + amountBlack--; + } + } else { + // Pay all additional unpaid casting cost with black. + amountBlack = amountBlack - amountToOffsetBy; + } + } + + int totalXAmount = amountBlack + amountRed; + + int lifetogain = amountBlack; + if (totalXAmount > 0) { + Permanent permanent = game.getPermanent(getTargetPointer().getFirst(game, source)); + if (permanent != null ) { + if (permanent.getToughness().getValue() < lifetogain) { + lifetogain = permanent.getToughness().getValue(); + } + permanent.damage(totalXAmount, source.getSourceId(), game, false, true); + } else { + Player player = game.getPlayer(getTargetPointer().getFirst(game, source)); + if (player != null) { + if (player.getLife() < lifetogain) { + lifetogain = player.getLife(); + } + player.damage(totalXAmount, source.getSourceId(), game, false, true); + } else { + return false; + } + } + Player controller = game.getPlayer(source.getControllerId()); + if (controller != null) { + controller.gainLife(lifetogain, game); + } else { + return false; + } + } + return true; + } + + @Override + public SoulBurnEffect copy() { + return new SoulBurnEffect(this); + } + +} diff --git a/Mage.Sets/src/mage/sets/IceAge.java b/Mage.Sets/src/mage/sets/IceAge.java index c8904941765..1605d5537f9 100644 --- a/Mage.Sets/src/mage/sets/IceAge.java +++ b/Mage.Sets/src/mage/sets/IceAge.java @@ -27,10 +27,10 @@ */ package mage.sets; -import mage.cards.CardGraphicInfo; import mage.cards.ExpansionSet; -import mage.constants.Rarity; import mage.constants.SetType; +import mage.constants.Rarity; +import mage.cards.CardGraphicInfo; /** * @@ -247,6 +247,7 @@ public class IceAge extends ExpansionSet { cards.add(new SetCardInfo("Soldevi Simulacrum", 314, Rarity.UNCOMMON, mage.cards.s.SoldeviSimulacrum.class)); cards.add(new SetCardInfo("Songs of the Damned", 48, Rarity.COMMON, mage.cards.s.SongsOfTheDamned.class)); cards.add(new SetCardInfo("Soul Barrier", 103, Rarity.UNCOMMON, mage.cards.s.SoulBarrier.class)); + cards.add(new SetCardInfo("Soul Burn", 361, Rarity.COMMON, mage.cards.s.SoulBurn.class)); cards.add(new SetCardInfo("Soul Kiss", 50, Rarity.COMMON, mage.cards.s.SoulKiss.class)); cards.add(new SetCardInfo("Spoils of Evil", 51, Rarity.RARE, mage.cards.s.SpoilsOfEvil.class)); cards.add(new SetCardInfo("Stampede", 153, Rarity.RARE, mage.cards.s.Stampede.class)); diff --git a/Mage.Sets/src/mage/sets/Invasion.java b/Mage.Sets/src/mage/sets/Invasion.java index e682d03e317..4099bbcccee 100644 --- a/Mage.Sets/src/mage/sets/Invasion.java +++ b/Mage.Sets/src/mage/sets/Invasion.java @@ -75,7 +75,6 @@ public class Invasion extends ExpansionSet { cards.add(new SetCardInfo("Aura Mutation", 232, Rarity.RARE, mage.cards.a.AuraMutation.class)); cards.add(new SetCardInfo("Aura Shards", 233, Rarity.UNCOMMON, mage.cards.a.AuraShards.class)); cards.add(new SetCardInfo("Backlash", 234, Rarity.UNCOMMON, mage.cards.b.Backlash.class)); - cards.add(new SetCardInfo("Barrin's Spite", 235, Rarity.RARE, mage.cards.b.BarrinsSpite.class)); cards.add(new SetCardInfo("Benalish Emissary", 5, Rarity.UNCOMMON, mage.cards.b.BenalishEmissary.class)); cards.add(new SetCardInfo("Benalish Heralds", 6, Rarity.UNCOMMON, mage.cards.b.BenalishHeralds.class)); cards.add(new SetCardInfo("Benalish Lancer", 7, Rarity.COMMON, mage.cards.b.BenalishLancer.class)); @@ -287,6 +286,7 @@ public class Invasion extends ExpansionSet { cards.add(new SetCardInfo("Slimy Kavu", 170, Rarity.COMMON, mage.cards.s.SlimyKavu.class)); cards.add(new SetCardInfo("Slinking Serpent", 274, Rarity.UNCOMMON, mage.cards.s.SlinkingSerpent.class)); cards.add(new SetCardInfo("Smoldering Tar", 275, Rarity.UNCOMMON, mage.cards.s.SmolderingTar.class)); + cards.add(new SetCardInfo("Soul Burn", 351, Rarity.COMMON, mage.cards.s.SoulBurn.class)); cards.add(new SetCardInfo("Sparring Golem", 312, Rarity.UNCOMMON, mage.cards.s.SparringGolem.class)); cards.add(new SetCardInfo("Spinal Embrace", 276, Rarity.RARE, mage.cards.s.SpinalEmbrace.class)); cards.add(new SetCardInfo("Spirit of Resistance", 38, Rarity.RARE, mage.cards.s.SpiritOfResistance.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/oneshot/damage/SoulBurnTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/oneshot/damage/SoulBurnTest.java new file mode 100644 index 00000000000..29140f0313e --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/oneshot/damage/SoulBurnTest.java @@ -0,0 +1,179 @@ +package org.mage.test.cards.abilities.oneshot.damage; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author Johnny E. Hastings + */ +public class SoulBurnTest extends CardTestPlayerBase { + + @Test + public void testDamageOpponentAllBlackMana() { + addCard(Zone.BATTLEFIELD, playerA, "Swamp"); + addCard(Zone.BATTLEFIELD, playerA, "Swamp"); + addCard(Zone.BATTLEFIELD, playerA, "Swamp"); + addCard(Zone.BATTLEFIELD, playerA, "Swamp"); + addCard(Zone.BATTLEFIELD, playerA, "Swamp"); + addCard(Zone.HAND, playerA, "Soul Burn"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Soul Burn", playerB); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertLife(playerA, 22); + assertLife(playerB, 18); + } + + @Test + public void testDamageOpponentOneBlackFourRedMana() { + addCard(Zone.BATTLEFIELD, playerA, "Swamp"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain"); + addCard(Zone.HAND, playerA, "Soul Burn"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Soul Burn", playerB); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertLife(playerA, 20); + assertLife(playerB, 18); + } + + @Test + public void testDamageOpponentAllKindsOfMana() { + addCard(Zone.BATTLEFIELD, playerA, "Swamp"); + addCard(Zone.BATTLEFIELD, playerA, "Island"); + addCard(Zone.BATTLEFIELD, playerA, "Plains"); + addCard(Zone.BATTLEFIELD, playerA, "Swamp"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain"); + addCard(Zone.HAND, playerA, "Soul Burn"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Soul Burn", playerB); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertLife(playerA, 21); + assertLife(playerB, 17); + } + + @Test + public void testDamageSelfAllSwamps() { + addCard(Zone.BATTLEFIELD, playerA, "Swamp"); + addCard(Zone.BATTLEFIELD, playerA, "Swamp"); + addCard(Zone.BATTLEFIELD, playerA, "Swamp"); + addCard(Zone.BATTLEFIELD, playerA, "Swamp"); + addCard(Zone.HAND, playerA, "Soul Burn"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Soul Burn", playerA); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertLife(playerA, 20); + assertLife(playerB, 20); + } + + @Test + public void testDamageSelfWithSwampsAndMountains() { + addCard(Zone.BATTLEFIELD, playerA, "Swamp"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain"); + addCard(Zone.HAND, playerA, "Soul Burn"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Soul Burn", playerA); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertLife(playerA, 18); + assertLife(playerB, 20); + } + + @Test + public void testDamageSmallCreatureAllSwamps() { + addCard(Zone.BATTLEFIELD, playerA, "Swamp"); + addCard(Zone.BATTLEFIELD, playerA, "Swamp"); + addCard(Zone.BATTLEFIELD, playerA, "Swamp"); + addCard(Zone.BATTLEFIELD, playerA, "Swamp"); + addCard(Zone.BATTLEFIELD, playerA, "Swamp"); + addCard(Zone.HAND, playerA, "Soul Burn"); + + addCard(Zone.BATTLEFIELD, playerB, "Bog Imp"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Soul Burn", "Bog Imp"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertPermanentCount(playerB, "Bog Imp", 0); + assertLife(playerA, 21); + assertLife(playerB, 20); + } + + @Test + public void testDamageSmallCreatureSwampsAndMountains() { + addCard(Zone.BATTLEFIELD, playerA, "Swamp"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain"); + addCard(Zone.HAND, playerA, "Soul Burn"); + + addCard(Zone.BATTLEFIELD, playerB, "Bog Imp"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Soul Burn", "Bog Imp"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertPermanentCount(playerB, "Bog Imp", 0); + assertLife(playerA, 20); + assertLife(playerB, 20); + } + + @Test + public void testDamageBigCreatureAllSwamps() { + addCard(Zone.BATTLEFIELD, playerA, "Swamp"); + addCard(Zone.BATTLEFIELD, playerA, "Swamp"); + addCard(Zone.BATTLEFIELD, playerA, "Swamp"); + addCard(Zone.BATTLEFIELD, playerA, "Swamp"); + addCard(Zone.BATTLEFIELD, playerA, "Swamp"); + addCard(Zone.HAND, playerA, "Soul Burn"); + + addCard(Zone.BATTLEFIELD, playerB, "Craw Wurm"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Soul Burn", "Craw Wurm"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertPermanentCount(playerB, "Craw Wurm", 1); + assertLife(playerA, 22); + assertLife(playerB, 20); + } + + @Test + public void testDamageBigCreatureSwampsAndMountains() { + addCard(Zone.BATTLEFIELD, playerA, "Swamp"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain"); + addCard(Zone.BATTLEFIELD, playerA, "Swamp"); + addCard(Zone.HAND, playerA, "Soul Burn"); + + addCard(Zone.BATTLEFIELD, playerB, "Craw Wurm"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Soul Burn", "Craw Wurm"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertPermanentCount(playerB, "Craw Wurm", 1); + assertLife(playerA, 21); + assertLife(playerB, 20); + } + +} diff --git a/Mage/src/main/java/mage/abilities/AbilityImpl.java b/Mage/src/main/java/mage/abilities/AbilityImpl.java index cc30fc971fe..fa69633d82a 100644 --- a/Mage/src/main/java/mage/abilities/AbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/AbilityImpl.java @@ -558,7 +558,11 @@ public abstract class AbilityImpl implements Ability { } else { String manaSymbol = null; if (variableManaCost.getFilter().isBlack()) { - manaSymbol = "B"; + if (variableManaCost.getFilter().isRed()) { + manaSymbol = "B/R"; + } else { + manaSymbol = "B"; + } } else if (variableManaCost.getFilter().isRed()) { manaSymbol = "R"; } else if (variableManaCost.getFilter().isBlue()) { From 675c3346ac5016ec7b3e3a45a10c7666cec77eaf Mon Sep 17 00:00:00 2001 From: "Johnny.Hastings@gmail.com" Date: Sun, 2 Apr 2017 10:17:39 -0500 Subject: [PATCH 2/2] Removed duplicate line in SoulBurn, used correct reference to Soul Burn as [this] in SoulBurnEffect. --- Mage.Sets/src/mage/cards/s/SoulBurn.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Mage.Sets/src/mage/cards/s/SoulBurn.java b/Mage.Sets/src/mage/cards/s/SoulBurn.java index a6be472b811..8d85b1b3467 100644 --- a/Mage.Sets/src/mage/cards/s/SoulBurn.java +++ b/Mage.Sets/src/mage/cards/s/SoulBurn.java @@ -65,7 +65,6 @@ public class SoulBurn extends CardImpl { VariableCost variableCost = this.getSpellAbility().getManaCostsToPay().getVariableCosts().get(0); if (variableCost instanceof VariableManaCost) { ((VariableManaCost) variableCost).setFilter(filterBlackOrRed); - ((VariableManaCost) variableCost).setFilter(filterBlackOrRed); } } @@ -83,7 +82,7 @@ class SoulBurnEffect extends OneShotEffect { public SoulBurnEffect() { super(Outcome.Damage); - staticText = "Soul Burn deals X damage to target creature or player for each black or red mana spent on X. You gain life equal to the damage dealt for each black mana spent; not more life than the player's life total before Soul Burn dealt damage, or the creature's toughness."; + staticText = "{this} deals X damage to target creature or player for each black or red mana spent on X. You gain life equal to the damage dealt for each black mana spent; not more life than the player's life total before Soul Burn dealt damage, or the creature's toughness."; } public SoulBurnEffect(final SoulBurnEffect effect) {