implement Sacred Boon and Scars of the Veteran

This commit is contained in:
Steven Knipe 2025-10-31 23:30:40 -07:00 committed by xenohedron
parent f0e551dafb
commit 066fb6dd46
8 changed files with 250 additions and 1 deletions

View file

@ -0,0 +1,83 @@
package mage.cards.s;
import mage.abilities.Ability;
import mage.abilities.DelayedTriggeredAbility;
import mage.abilities.common.delayed.AtTheBeginOfNextEndStepDelayedTriggeredAbility;
import mage.abilities.effects.PreventionEffectData;
import mage.abilities.effects.PreventionEffectImpl;
import mage.abilities.effects.common.counter.AddCountersTargetEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Duration;
import mage.counters.CounterType;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.target.common.TargetCreaturePermanent;
import mage.target.targetpointer.FixedTarget;
import java.util.UUID;
/**
*
* @author notgreat
*/
public final class SacredBoon extends CardImpl {
public SacredBoon(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{1}{W}");
// Prevent the next 3 damage that would be dealt to target creature this turn. At the beginning of the next end step, put a +0/+1 counter on that creature for each 1 damage prevented this way.
this.getSpellAbility().addEffect(new SacredBoonPreventDamageTargetEffect(Duration.EndOfTurn));
this.getSpellAbility().addTarget(new TargetCreaturePermanent());
}
private SacredBoon(final SacredBoon card) {
super(card);
}
@Override
public SacredBoon copy() {
return new SacredBoon(this);
}
}
class SacredBoonPreventDamageTargetEffect extends PreventionEffectImpl {
SacredBoonPreventDamageTargetEffect(Duration duration) {
super(duration, 3, false);
staticText = "Prevent the next 3 damage that would be dealt to target creature this turn. At the beginning of the next end step, put a +0/+1 counter on that creature for each 1 damage prevented this way.";
}
private SacredBoonPreventDamageTargetEffect(final SacredBoonPreventDamageTargetEffect effect) {
super(effect);
}
@Override
public SacredBoonPreventDamageTargetEffect copy() {
return new SacredBoonPreventDamageTargetEffect(this);
}
@Override
public boolean replaceEvent(GameEvent event, Ability source, Game game) {
PreventionEffectData preventionEffectData = preventDamageAction(event, source, game);
if (preventionEffectData.getPreventedDamage() > 0) {
Permanent targetPermanent = game.getPermanent(source.getTargets().getFirstTarget());
if (targetPermanent != null) {
DelayedTriggeredAbility delayedAbility = new AtTheBeginOfNextEndStepDelayedTriggeredAbility(
new AddCountersTargetEffect(CounterType.P0P1.createInstance(preventionEffectData.getPreventedDamage()))
.setTargetPointer(new FixedTarget(targetPermanent, game)));
game.addDelayedTriggeredAbility(delayedAbility, source);
}
}
return false;
}
@Override
public boolean applies(GameEvent event, Ability source, Game game) {
return super.applies(event, source, game) && source.getTargets().getFirstTarget().equals(event.getTargetId());
}
}

View file

@ -0,0 +1,99 @@
package mage.cards.s;
import mage.ObjectColor;
import mage.abilities.Ability;
import mage.abilities.DelayedTriggeredAbility;
import mage.abilities.common.delayed.AtTheBeginOfNextEndStepDelayedTriggeredAbility;
import mage.abilities.costs.AlternativeCostSourceAbility;
import mage.abilities.costs.common.ExileFromHandCost;
import mage.abilities.effects.PreventionEffectData;
import mage.abilities.effects.PreventionEffectImpl;
import mage.abilities.effects.common.counter.AddCountersTargetEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Duration;
import mage.counters.CounterType;
import mage.filter.common.FilterOwnedCard;
import mage.filter.predicate.mageobject.ColorPredicate;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.target.common.TargetAnyTarget;
import mage.target.common.TargetCardInHand;
import mage.target.targetpointer.FixedTarget;
import java.util.UUID;
/**
*
* @author notgreat
*/
public final class ScarsOfTheVeteran extends CardImpl {
private static final FilterOwnedCard filter
= new FilterOwnedCard("a white card from your hand");
static {
filter.add(new ColorPredicate(ObjectColor.WHITE));
}
public ScarsOfTheVeteran(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{4}{W}");
// You may exile a white card from your hand rather than pay Scars of the Veteran's mana cost.
this.addAbility(new AlternativeCostSourceAbility(new ExileFromHandCost(new TargetCardInHand(filter))));
// Prevent the next 7 damage that would be dealt to any target this turn. If its a creature, put a +0/+1 counter on it for each 1 damage prevented this way at the beginning of the next end step.
this.getSpellAbility().addEffect(new ScarsOfTheVeteranPreventDamageTargetEffect(Duration.EndOfTurn));
this.getSpellAbility().addTarget(new TargetAnyTarget());
}
private ScarsOfTheVeteran(final ScarsOfTheVeteran card) {
super(card);
}
@Override
public ScarsOfTheVeteran copy() {
return new ScarsOfTheVeteran(this);
}
}
class ScarsOfTheVeteranPreventDamageTargetEffect extends PreventionEffectImpl {
ScarsOfTheVeteranPreventDamageTargetEffect(Duration duration) {
super(duration, 7, false);
staticText = "Prevent the next 7 damage that would be dealt to any target this turn. If it's a creature, put a +0/+1 counter on it for each 1 damage prevented this way at the beginning of the next end step.";
}
private ScarsOfTheVeteranPreventDamageTargetEffect(final ScarsOfTheVeteranPreventDamageTargetEffect effect) {
super(effect);
}
@Override
public ScarsOfTheVeteranPreventDamageTargetEffect copy() {
return new ScarsOfTheVeteranPreventDamageTargetEffect(this);
}
@Override
public boolean replaceEvent(GameEvent event, Ability source, Game game) {
PreventionEffectData preventionEffectData = preventDamageAction(event, source, game);
if (preventionEffectData.getPreventedDamage() > 0) {
Permanent targetPermanent = game.getPermanent(source.getTargets().getFirstTarget());
if (targetPermanent != null) {
DelayedTriggeredAbility delayedAbility = new AtTheBeginOfNextEndStepDelayedTriggeredAbility(
new AddCountersTargetEffect(CounterType.P0P1.createInstance(preventionEffectData.getPreventedDamage()))
.setTargetPointer(new FixedTarget(targetPermanent, game)));
game.addDelayedTriggeredAbility(delayedAbility, source);
}
return false;
}
return false;
}
@Override
public boolean applies(GameEvent event, Ability source, Game game) {
return super.applies(event, source, game) && source.getTargets().getFirstTarget().equals(event.getTargetId());
}
}

View file

@ -158,6 +158,7 @@ public final class Alliances extends ExpansionSet {
cards.add(new SetCardInfo("Royal Herbalist", "15a", Rarity.COMMON, mage.cards.r.RoyalHerbalist.class, RETRO_ART_USE_VARIOUS)); cards.add(new SetCardInfo("Royal Herbalist", "15a", Rarity.COMMON, mage.cards.r.RoyalHerbalist.class, RETRO_ART_USE_VARIOUS));
cards.add(new SetCardInfo("Royal Herbalist", "15b", Rarity.COMMON, mage.cards.r.RoyalHerbalist.class, RETRO_ART_USE_VARIOUS)); cards.add(new SetCardInfo("Royal Herbalist", "15b", Rarity.COMMON, mage.cards.r.RoyalHerbalist.class, RETRO_ART_USE_VARIOUS));
cards.add(new SetCardInfo("Scarab of the Unseen", 128, Rarity.UNCOMMON, mage.cards.s.ScarabOfTheUnseen.class, RETRO_ART)); cards.add(new SetCardInfo("Scarab of the Unseen", 128, Rarity.UNCOMMON, mage.cards.s.ScarabOfTheUnseen.class, RETRO_ART));
cards.add(new SetCardInfo("Scars of the Veteran", 16, Rarity.UNCOMMON, mage.cards.s.ScarsOfTheVeteran.class, RETRO_ART));
cards.add(new SetCardInfo("School of the Unseen", 141, Rarity.UNCOMMON, mage.cards.s.SchoolOfTheUnseen.class, RETRO_ART)); cards.add(new SetCardInfo("School of the Unseen", 141, Rarity.UNCOMMON, mage.cards.s.SchoolOfTheUnseen.class, RETRO_ART));
cards.add(new SetCardInfo("Seasoned Tactician", 17, Rarity.UNCOMMON, mage.cards.s.SeasonedTactician.class, RETRO_ART)); cards.add(new SetCardInfo("Seasoned Tactician", 17, Rarity.UNCOMMON, mage.cards.s.SeasonedTactician.class, RETRO_ART));
cards.add(new SetCardInfo("Sheltered Valley", 142, Rarity.RARE, mage.cards.s.ShelteredValley.class, RETRO_ART)); cards.add(new SetCardInfo("Sheltered Valley", 142, Rarity.RARE, mage.cards.s.ShelteredValley.class, RETRO_ART));

View file

@ -367,6 +367,7 @@ public final class FifthEdition extends ExpansionSet {
cards.add(new SetCardInfo("Rod of Ruin", 396, Rarity.UNCOMMON, mage.cards.r.RodOfRuin.class, RETRO_ART)); cards.add(new SetCardInfo("Rod of Ruin", 396, Rarity.UNCOMMON, mage.cards.r.RodOfRuin.class, RETRO_ART));
cards.add(new SetCardInfo("Ruins of Trokair", 422, Rarity.UNCOMMON, mage.cards.r.RuinsOfTrokair.class, RETRO_ART)); cards.add(new SetCardInfo("Ruins of Trokair", 422, Rarity.UNCOMMON, mage.cards.r.RuinsOfTrokair.class, RETRO_ART));
cards.add(new SetCardInfo("Sabretooth Tiger", 264, Rarity.COMMON, mage.cards.s.SabretoothTiger.class, RETRO_ART)); cards.add(new SetCardInfo("Sabretooth Tiger", 264, Rarity.COMMON, mage.cards.s.SabretoothTiger.class, RETRO_ART));
cards.add(new SetCardInfo("Sacred Boon", 57, Rarity.UNCOMMON, mage.cards.s.SacredBoon.class, RETRO_ART));
cards.add(new SetCardInfo("Samite Healer", 58, Rarity.COMMON, mage.cards.s.SamiteHealer.class, RETRO_ART)); cards.add(new SetCardInfo("Samite Healer", 58, Rarity.COMMON, mage.cards.s.SamiteHealer.class, RETRO_ART));
cards.add(new SetCardInfo("Sand Silos", 423, Rarity.RARE, mage.cards.s.SandSilos.class, RETRO_ART)); cards.add(new SetCardInfo("Sand Silos", 423, Rarity.RARE, mage.cards.s.SandSilos.class, RETRO_ART));
cards.add(new SetCardInfo("Scaled Wurm", 322, Rarity.COMMON, mage.cards.s.ScaledWurm.class, RETRO_ART)); cards.add(new SetCardInfo("Scaled Wurm", 322, Rarity.COMMON, mage.cards.s.ScaledWurm.class, RETRO_ART));

View file

@ -300,6 +300,7 @@ public final class IceAge extends ExpansionSet {
cards.add(new SetCardInfo("River Delta", 359, Rarity.RARE, mage.cards.r.RiverDelta.class, RETRO_ART)); cards.add(new SetCardInfo("River Delta", 359, Rarity.RARE, mage.cards.r.RiverDelta.class, RETRO_ART));
cards.add(new SetCardInfo("Runed Arch", 334, Rarity.RARE, mage.cards.r.RunedArch.class, RETRO_ART)); cards.add(new SetCardInfo("Runed Arch", 334, Rarity.RARE, mage.cards.r.RunedArch.class, RETRO_ART));
cards.add(new SetCardInfo("Sabretooth Tiger", 215, Rarity.COMMON, mage.cards.s.SabretoothTiger.class, RETRO_ART)); cards.add(new SetCardInfo("Sabretooth Tiger", 215, Rarity.COMMON, mage.cards.s.SabretoothTiger.class, RETRO_ART));
cards.add(new SetCardInfo("Sacred Boon", 50, Rarity.UNCOMMON, mage.cards.s.SacredBoon.class, RETRO_ART));
cards.add(new SetCardInfo("Scaled Wurm", 262, Rarity.COMMON, mage.cards.s.ScaledWurm.class, RETRO_ART)); cards.add(new SetCardInfo("Scaled Wurm", 262, Rarity.COMMON, mage.cards.s.ScaledWurm.class, RETRO_ART));
cards.add(new SetCardInfo("Sea Spirit", 95, Rarity.UNCOMMON, mage.cards.s.SeaSpirit.class, RETRO_ART)); cards.add(new SetCardInfo("Sea Spirit", 95, Rarity.UNCOMMON, mage.cards.s.SeaSpirit.class, RETRO_ART));
cards.add(new SetCardInfo("Seizures", 159, Rarity.COMMON, mage.cards.s.Seizures.class, RETRO_ART)); cards.add(new SetCardInfo("Seizures", 159, Rarity.COMMON, mage.cards.s.Seizures.class, RETRO_ART));

View file

@ -205,7 +205,9 @@ public final class MastersEditionII extends ExpansionSet {
cards.add(new SetCardInfo("Royal Decree", 31, Rarity.RARE, mage.cards.r.RoyalDecree.class, RETRO_ART)); cards.add(new SetCardInfo("Royal Decree", 31, Rarity.RARE, mage.cards.r.RoyalDecree.class, RETRO_ART));
cards.add(new SetCardInfo("Royal Trooper", 32, Rarity.COMMON, mage.cards.r.RoyalTrooper.class, RETRO_ART)); cards.add(new SetCardInfo("Royal Trooper", 32, Rarity.COMMON, mage.cards.r.RoyalTrooper.class, RETRO_ART));
cards.add(new SetCardInfo("Ruins of Trokair", 234, Rarity.UNCOMMON, mage.cards.r.RuinsOfTrokair.class, RETRO_ART)); cards.add(new SetCardInfo("Ruins of Trokair", 234, Rarity.UNCOMMON, mage.cards.r.RuinsOfTrokair.class, RETRO_ART));
cards.add(new SetCardInfo("Sacred Boon", 33, Rarity.UNCOMMON, mage.cards.s.SacredBoon.class, RETRO_ART));
cards.add(new SetCardInfo("Savannah", 235, Rarity.RARE, mage.cards.s.Savannah.class, new CardGraphicInfo(FrameStyle.LEA_ORIGINAL_DUAL_LAND_ART_BASIC, false))); cards.add(new SetCardInfo("Savannah", 235, Rarity.RARE, mage.cards.s.Savannah.class, new CardGraphicInfo(FrameStyle.LEA_ORIGINAL_DUAL_LAND_ART_BASIC, false)));
cards.add(new SetCardInfo("Scars of the Veteran", 34, Rarity.RARE, mage.cards.s.ScarsOfTheVeteran.class, RETRO_ART));
cards.add(new SetCardInfo("Screeching Drake", 63, Rarity.COMMON, mage.cards.s.ScreechingDrake.class, RETRO_ART)); cards.add(new SetCardInfo("Screeching Drake", 63, Rarity.COMMON, mage.cards.s.ScreechingDrake.class, RETRO_ART));
cards.add(new SetCardInfo("Sea Drake", 64, Rarity.RARE, mage.cards.s.SeaDrake.class, RETRO_ART)); cards.add(new SetCardInfo("Sea Drake", 64, Rarity.RARE, mage.cards.s.SeaDrake.class, RETRO_ART));
cards.add(new SetCardInfo("Sea Spirit", 65, Rarity.UNCOMMON, mage.cards.s.SeaSpirit.class, RETRO_ART)); cards.add(new SetCardInfo("Sea Spirit", 65, Rarity.UNCOMMON, mage.cards.s.SeaSpirit.class, RETRO_ART));

View file

@ -0,0 +1,62 @@
package org.mage.test.cards.replacement.prevent;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* @author notgreat
*/
public class SacredBoonTest extends CardTestPlayerBase {
@Test
public void testSacredBoonBigDamage() {
setStrictChooseMode(true);
addCard(Zone.HAND, playerA, "Sacred Boon");
addCard(Zone.BATTLEFIELD, playerA, "Plains", 2);
addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion");
addCard(Zone.HAND, playerB, "Bombard");
addCard(Zone.BATTLEFIELD, playerB, "Mountain", 3);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Sacred Boon", "Silvercoat Lion");
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Bombard", "Silvercoat Lion");
setStopAt(1, PhaseStep.END_TURN);
execute();
assertLife(playerA, 20);
assertLife(playerB, 20);
assertDamageReceived(playerA, "Silvercoat Lion", 1);
assertPowerToughness(playerA, "Silvercoat Lion", 2, 5);
}
@Test
public void testSacredBoonSmallDamage() {
setStrictChooseMode(true);
addCard(Zone.HAND, playerA, "Sacred Boon");
addCard(Zone.BATTLEFIELD, playerA, "Plains", 2);
addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion");
addCard(Zone.HAND, playerB, "Scattershot");
addCard(Zone.BATTLEFIELD, playerB, "Mountain", 3);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Sacred Boon", "Silvercoat Lion");
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Scattershot", "Silvercoat Lion");
setChoice(playerB, false); // don't change scattershot targets
setChoice(playerA, "At the"); // stack triggers - technically incorrect, should be a single trigger
setStopAt(1, PhaseStep.END_TURN);
execute();
assertLife(playerA, 20);
assertLife(playerB, 20);
assertDamageReceived(playerA, "Silvercoat Lion", 0);
assertPowerToughness(playerA, "Silvercoat Lion", 2, 4);
}
}

View file

@ -2037,7 +2037,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
* multiple targets can be seperated by ^; * multiple targets can be seperated by ^;
* no target marks as TestPlayer.NO_TARGET; * no target marks as TestPlayer.NO_TARGET;
* warning, do not support cards with target adjusters - use addTarget instead * warning, do not support cards with target adjusters - use addTarget instead
* @param waitStackResolved if true, wait for stack to resolve * @param waitStackResolved if true, wait for stack to resolve before continuing
*/ */
public void castSpell(int turnNum, PhaseStep step, TestPlayer player, String cardName, String targetName, boolean waitStackResolved) { public void castSpell(int turnNum, PhaseStep step, TestPlayer player, String cardName, String targetName, boolean waitStackResolved) {
castSpell(turnNum, step, player, cardName, targetName); castSpell(turnNum, step, player, cardName, targetName);