From d57b99d6e43fdea0efede83e488c69a61c0f02d4 Mon Sep 17 00:00:00 2001 From: Susucre <34709007+Susucre@users.noreply.github.com> Date: Fri, 30 May 2025 20:31:36 +0200 Subject: [PATCH] implement [MIR] Shadowbane --- Mage.Sets/src/mage/cards/s/Shadowbane.java | 96 ++++++++++ Mage.Sets/src/mage/sets/Mirage.java | 2 +- .../test/cards/single/mir/ShadowbaneTest.java | 177 ++++++++++++++++++ ...eventNextDamageFromChosenSourceEffect.java | 5 +- 4 files changed, 276 insertions(+), 4 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/s/Shadowbane.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/mir/ShadowbaneTest.java diff --git a/Mage.Sets/src/mage/cards/s/Shadowbane.java b/Mage.Sets/src/mage/cards/s/Shadowbane.java new file mode 100644 index 00000000000..df5992a53f5 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/Shadowbane.java @@ -0,0 +1,96 @@ +package mage.cards.s; + +import mage.MageObject; +import mage.abilities.Ability; +import mage.abilities.effects.PreventionEffectData; +import mage.abilities.effects.common.PreventNextDamageFromChosenSourceEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.TargetSource; + +import java.util.UUID; + +/** + * @author Susucr + */ +public final class Shadowbane extends CardImpl { + + public Shadowbane(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{1}{W}"); + + // The next time a source of your choice would deal damage to you and/or creatures you control this turn, prevent that damage. If damage from a black source is prevented this way, you gain that much life. + this.getSpellAbility().addEffect(new ShadowbanePreventionEffect()); + } + + private Shadowbane(final Shadowbane card) { + super(card); + } + + @Override + public Shadowbane copy() { + return new Shadowbane(this); + } +} + +class ShadowbanePreventionEffect extends PreventNextDamageFromChosenSourceEffect { + + ShadowbanePreventionEffect() { + super(Duration.EndOfTurn, false, ShadowbanePreventionApplier.instance); + } + + private ShadowbanePreventionEffect(final ShadowbanePreventionEffect effect) { + super(effect); + } + + @Override + public PreventNextDamageFromChosenSourceEffect copy() { + return new ShadowbanePreventionEffect(this); + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + if (!super.applies(event, source, game)) { + return false; + } + UUID controllerId = source.getControllerId(); + UUID targetId = event.getTargetId(); + if (targetId.equals(controllerId)) { + return true; // damage to you + } + Permanent permanent = game.getPermanent(targetId); + return StaticFilters.FILTER_CONTROLLED_CREATURE.match(permanent, controllerId, source, game); // damage to creatures you control + } + +} + +enum ShadowbanePreventionApplier implements PreventNextDamageFromChosenSourceEffect.ApplierOnPrevention { + instance; + + public String getText() { + return "If damage from a black source is prevented this way, you gain that much life"; + } + + public boolean apply(PreventionEffectData data, TargetSource targetSource, GameEvent event, Ability source, Game game) { + if (data == null || data.getPreventedDamage() <= 0) { + return false; + } + MageObject sourceObject = game.getObject(targetSource.getFirstTarget()); + if (!sourceObject.getColor(game).isBlack()) { + return false; + } + int prevented = data.getPreventedDamage(); + Player controller = game.getPlayer(source.getControllerId()); + if (controller == null) { + return false; + } + controller.gainLife(prevented, game, source); + return true; + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/sets/Mirage.java b/Mage.Sets/src/mage/sets/Mirage.java index 2361973dafb..0cdd12819c7 100644 --- a/Mage.Sets/src/mage/sets/Mirage.java +++ b/Mage.Sets/src/mage/sets/Mirage.java @@ -295,7 +295,7 @@ public final class Mirage extends ExpansionSet { cards.add(new SetCardInfo("Serene Heart", 242, Rarity.COMMON, mage.cards.s.SereneHeart.class, RETRO_ART)); cards.add(new SetCardInfo("Sewer Rats", 139, Rarity.COMMON, mage.cards.s.SewerRats.class, RETRO_ART)); cards.add(new SetCardInfo("Shadow Guildmage", 140, Rarity.COMMON, mage.cards.s.ShadowGuildmage.class, RETRO_ART)); -// cards.add(new SetCardInfo("Shadowbane", 38, Rarity.UNCOMMON, mage.cards.s.Shadowbane.class, RETRO_ART)); + cards.add(new SetCardInfo("Shadowbane", 242, Rarity.UNCOMMON, mage.cards.s.Shadowbane.class)); cards.add(new SetCardInfo("Shallow Grave", 141, Rarity.RARE, mage.cards.s.ShallowGrave.class, RETRO_ART)); cards.add(new SetCardInfo("Shaper Guildmage", 91, Rarity.COMMON, mage.cards.s.ShaperGuildmage.class, RETRO_ART)); cards.add(new SetCardInfo("Shauku's Minion", 283, Rarity.UNCOMMON, mage.cards.s.ShaukusMinion.class, RETRO_ART)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/mir/ShadowbaneTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/mir/ShadowbaneTest.java new file mode 100644 index 00000000000..3de86bef715 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/mir/ShadowbaneTest.java @@ -0,0 +1,177 @@ +package org.mage.test.cards.single.mir; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author Susucr + */ +public class ShadowbaneTest extends CardTestPlayerBase { + + /** + * {@link mage.cards.s.Shadowbane Shadowbane} {1}{W} + * The next time a source of your choice would deal damage to you and/or creatures you control this turn, prevent that damage. If damage from a black source is prevented this way, you gain that much life. + */ + private static final String shadowbane = "Shadowbane"; + + @Test + public void test_DamageOnCreature_Prevent_SourceNotBlack() { + addCard(Zone.HAND, playerA, shadowbane, 1); + addCard(Zone.BATTLEFIELD, playerB, "Goblin Piker", 1); // 2/1 + addCard(Zone.BATTLEFIELD, playerA, "Caelorna, Coral Tyrant"); // 0/8 + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerA, shadowbane); + setChoice(playerA, "Goblin Piker"); // source to prevent from + + attack(2, playerB, "Goblin Piker", playerA); + block(2, playerA, "Caelorna, Coral Tyrant", "Goblin Piker"); + + setStrictChooseMode(true); + setStopAt(2, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertDamageReceived(playerA, "Caelorna, Coral Tyrant", 0); + assertLife(playerA, 20); // no life gain + } + + @Test + public void test_DamageOnCreature_Prevent_SourceBlack() { + addCard(Zone.HAND, playerA, shadowbane, 1); + addCard(Zone.BATTLEFIELD, playerB, "Cabal Evangel", 1); // 2/2 black + addCard(Zone.BATTLEFIELD, playerA, "Caelorna, Coral Tyrant"); // 0/8 + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerA, shadowbane); + setChoice(playerA, "Cabal Evangel"); // source to prevent from + + attack(2, playerB, "Cabal Evangel", playerA); + block(2, playerA, "Caelorna, Coral Tyrant", "Cabal Evangel"); + + setStrictChooseMode(true); + setStopAt(2, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertDamageReceived(playerA, "Caelorna, Coral Tyrant", 0); + assertLife(playerA, 20 + 2); // life gain + } + + @Test + public void test_DamageOnYou_Prevent_SourceNotBlack() { + addCard(Zone.HAND, playerA, shadowbane, 1); + addCard(Zone.BATTLEFIELD, playerB, "Goblin Piker", 1); // 2/1 + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerA, shadowbane); + setChoice(playerA, "Goblin Piker"); // source to prevent from + + attack(2, playerB, "Goblin Piker", playerA); + + setStrictChooseMode(true); + setStopAt(2, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertLife(playerA, 20); + } + + @Test + public void test_DamageOnYou_Prevent_SourceBlack() { + addCard(Zone.HAND, playerA, shadowbane, 1); + addCard(Zone.BATTLEFIELD, playerB, "Cabal Evangel", 1); // 2/2 black + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerA, shadowbane); + setChoice(playerA, "Cabal Evangel"); // source to prevent from + + attack(2, playerB, "Cabal Evangel", playerA); + + setStrictChooseMode(true); + setStopAt(2, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertLife(playerA, 20 + 2); + } + + @Test + public void test_SpellDamage_Prevent_SourceNotBlack() { + addCard(Zone.HAND, playerA, shadowbane, 1); + addCard(Zone.HAND, playerB, "Lightning Bolt", 1); + addCard(Zone.BATTLEFIELD, playerB, "Mountain", 1); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Lightning Bolt", playerA); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, shadowbane, null, "Lightning Bolt"); + setChoice(playerA, "Lightning Bolt"); // source to prevent from + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertLife(playerA, 20); + } + + @Test + public void test_SpellDamage_Prevent_SourceBlack() { + addCard(Zone.HAND, playerA, shadowbane, 1); + addCard(Zone.HAND, playerB, "Agonizing Syphon", 1); // 3 damage to any target. gain 3 life + addCard(Zone.BATTLEFIELD, playerB, "Swamp", 4); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Agonizing Syphon", playerA); + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerA, shadowbane, null, "Agonizing Syphon"); + setChoice(playerA, "Agonizing Syphon"); // source to prevent from + + setStrictChooseMode(true); + setStopAt(2, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertLife(playerA, 20 + 3); + assertLife(playerB, 20 + 3); + } + + @Test + public void test_SpellDamage_Prevent_Famine_SourceBlack() { + addCard(Zone.HAND, playerA, shadowbane, 1); + addCard(Zone.HAND, playerB, "Famine", 1); // Famine deals 3 damage to each creature and each player. + addCard(Zone.BATTLEFIELD, playerB, "Swamp", 5); + addCard(Zone.BATTLEFIELD, playerA, "Ageless Guardian"); // 1/4 + addCard(Zone.BATTLEFIELD, playerB, "Dune Beetle"); // 1/4 + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Famine"); + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerA, shadowbane, null, "Famine"); + setChoice(playerA, "Famine"); // source to prevent from + + setStrictChooseMode(true); + setStopAt(2, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertDamageReceived(playerA, "Ageless Guardian", 0); // prevented + assertDamageReceived(playerB, "Dune Beetle", 3); // not prevented on opponent's creature + assertLife(playerB, 20 - 3); // not prevented on opponent + assertLife(playerA, 20 + 3 * 2); // 2 preventions of 3 damage + } + + @Test + public void test_SpellDamage_Prevent_FieryConfluence() { + addCard(Zone.HAND, playerA, shadowbane, 1); + addCard(Zone.HAND, playerB, "Fiery Confluence", 3); // three instances of "Fiery Confluence deals 2 damage to each opponent." + addCard(Zone.BATTLEFIELD, playerB, "Mountain", 4); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Fiery Confluence"); + setModeChoice(playerB, "2"); // Fiery Confluence deals 2 damage to each opponent + setModeChoice(playerB, "2"); // Fiery Confluence deals 2 damage to each opponent + setModeChoice(playerB, "2"); // Fiery Confluence deals 2 damage to each opponent + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerA, shadowbane, null, "Fiery Confluence"); + setChoice(playerA, "Fiery Confluence"); // source to prevent from + + setStrictChooseMode(true); + setStopAt(2, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertLife(playerA, 20 - 2 * 2); // only 1 of the 3 damage instance is prevented + } +} diff --git a/Mage/src/main/java/mage/abilities/effects/common/PreventNextDamageFromChosenSourceEffect.java b/Mage/src/main/java/mage/abilities/effects/common/PreventNextDamageFromChosenSourceEffect.java index 626cea7d43c..a537c79e50f 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/PreventNextDamageFromChosenSourceEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/PreventNextDamageFromChosenSourceEffect.java @@ -96,7 +96,7 @@ public class PreventNextDamageFromChosenSourceEffect extends PreventionEffectImp @Override public boolean replaceEvent(GameEvent event, Ability source, Game game) { PreventionEffectData data = preventDamageAction(event, source, game); - this.used = true; + discard(); if (onPrevention != null) { onPrevention.apply(data, targetSource, event, source, game); } @@ -105,8 +105,7 @@ public class PreventNextDamageFromChosenSourceEffect extends PreventionEffectImp @Override public boolean applies(GameEvent event, Ability source, Game game) { - return !this.used - && super.applies(event, source, game) + return super.applies(event, source, game) && (!toYou || event.getTargetId().equals(source.getControllerId())) && event.getSourceId().equals(targetSource.getFirstTarget()); }