Merge branch 'refactor_prevention' into 'master'

Refactor Damage Prevention - Step 1

See merge request core-developers/forge!4675
This commit is contained in:
Michael Kamensky
2021-05-08 04:34:44 +00:00
11 changed files with 131 additions and 292 deletions

View File

@@ -2256,11 +2256,12 @@ public class ComputerUtilCombat {
* @return a int.
*/
public final static int getDamageToKill(final Card c) {
int killDamage = c.getLethalDamage() + c.getPreventNextDamageTotalShields();
int damageShield = c.getPreventNextDamageTotalShields();
int killDamage = c.getLethalDamage() + damageShield;
if ((killDamage > c.getPreventNextDamageTotalShields())
if ((killDamage > damageShield)
&& c.hasSVar("DestroyWhenDamaged")) {
killDamage = 1 + c.getPreventNextDamageTotalShields();
killDamage = 1 + damageShield;
}
return killDamage;

View File

@@ -88,7 +88,6 @@ public class GameCopier {
newPlayer.setCounters(Maps.newHashMap(origPlayer.getCounters()));
newPlayer.setLifeLostLastTurn(origPlayer.getLifeLostLastTurn());
newPlayer.setLifeLostThisTurn(origPlayer.getLifeLostThisTurn());
newPlayer.setPreventNextDamage(origPlayer.getPreventNextDamage());
newPlayer.getManaPool().add(origPlayer.getManaPool());
newPlayer.setCommanders(origPlayer.getCommanders()); // will be fixed up below
playerMap.put(origPlayer, newPlayer);

View File

@@ -45,9 +45,7 @@ import forge.game.zone.ZoneType;
public abstract class GameEntity extends GameObject implements IIdentifiable {
protected final int id;
private String name = "";
private int preventNextDamage = 0;
protected CardCollection attachedCards = new CardCollection();
private Map<Card, Map<String, String>> preventionShieldsWithEffects = Maps.newTreeMap();
protected Map<CounterType, Integer> counters = Maps.newHashMap();
protected GameEntity(int id0) {
@@ -192,20 +190,6 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
// then apply static Damage Prevention effects
restDamage = staticDamagePrevention(restDamage, source, isCombat, false);
// then apply ShieldEffects with Special Effect
restDamage = preventShieldEffect(restDamage);
// then do Shield with only number
if (restDamage <= 0) {
restDamage = 0;
} else if (restDamage >= getPreventNextDamage()) {
restDamage = restDamage - getPreventNextDamage();
setPreventNextDamage(0);
} else {
setPreventNextDamage(getPreventNextDamage() - restDamage);
restDamage = 0;
}
// if damage is greater than restDamage, damage was prevented
if (damage > restDamage) {
int prevent = damage - restDamage;
@@ -223,60 +207,8 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
return restDamage;
}
protected abstract int preventShieldEffect(final int damage);
public int getPreventNextDamage() {
return preventNextDamage;
}
public void setPreventNextDamage(final int n) {
preventNextDamage = n;
}
public void addPreventNextDamage(final int n) {
preventNextDamage += n;
}
public void subtractPreventNextDamage(final int n) {
preventNextDamage -= n;
}
public void resetPreventNextDamage() {
preventNextDamage = 0;
}
// PreventNextDamageWithEffect
public Map<Card, Map<String, String>> getPreventNextDamageWithEffect() {
return preventionShieldsWithEffects;
}
public int getPreventNextDamageTotalShields() {
int shields = preventNextDamage;
for (final Map<String, String> value : preventionShieldsWithEffects.values()) {
shields += Integer.valueOf(value.get("ShieldAmount"));
}
return shields;
}
/**
* Adds a damage prevention shield with an effect that happens at time of prevention.
* @param shieldSource - The source card which generated the shield
* @param effectMap - A map of the effect occurring with the damage prevention
*/
public void addPreventNextDamageWithEffect(final Card shieldSource, Map<String, String> effectMap) {
if (preventionShieldsWithEffects.containsKey(shieldSource)) {
int currentShields = Integer.valueOf(preventionShieldsWithEffects.get(shieldSource).get("ShieldAmount"));
currentShields += Integer.valueOf(effectMap.get("ShieldAmount"));
effectMap.put("ShieldAmount", Integer.toString(currentShields));
preventionShieldsWithEffects.put(shieldSource, effectMap);
} else {
preventionShieldsWithEffects.put(shieldSource, effectMap);
}
}
public void subtractPreventNextDamageWithEffect(final Card shieldSource, final int n) {
int currentShields = Integer.valueOf(preventionShieldsWithEffects.get(shieldSource).get("ShieldAmount"));
if (currentShields > n) {
preventionShieldsWithEffects.get(shieldSource).put("ShieldAmount", String.valueOf(currentShields - n));
} else {
preventionShieldsWithEffects.remove(shieldSource);
}
}
public void resetPreventNextDamageWithEffect() {
preventionShieldsWithEffects.clear();
return getGame().getReplacementHandler().getTotalPreventionShieldAmount(this);
}
public abstract boolean hasKeyword(final String keyword);

View File

@@ -2,14 +2,13 @@ package forge.game.ability.effects;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.card.CardCollectionView;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
public class DamagePreventAllEffect extends SpellAbilityEffect {
public class DamagePreventAllEffect extends DamagePreventEffectBase {
@Override
public void resolve(SpellAbility sa) {
final Card source = sa.getHostCard();
@@ -26,14 +25,14 @@ public class DamagePreventAllEffect extends SpellAbilityEffect {
CardCollectionView list = game.getCardsIn(ZoneType.Battlefield);
list = AbilityUtils.filterListByType(list, sa.getParam("ValidCards"), sa);
for (final Card c : list) {
c.addPreventNextDamage(numDam);
addPreventNextDamage(sa, c, numDam);
}
}
if (!players.equals("")) {
for (final Player p : game.getPlayers()) {
if (p.isValid(players, source.getController(), source, sa)) {
p.addPreventNextDamage(numDam);
addPreventNextDamage(sa, p, numDam);
}
}
}

View File

@@ -1,20 +1,16 @@
package forge.game.ability.effects;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import forge.game.GameObject;
import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardUtil;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
public class DamagePreventEffect extends SpellAbilityEffect {
public class DamagePreventEffect extends DamagePreventEffectBase {
@Override
protected String getStackDescription(SpellAbility sa) {
@@ -73,69 +69,25 @@ public class DamagePreventEffect extends SpellAbilityEffect {
final CardCollection untargetedCards = CardUtil.getRadiance(sa);
final boolean targeted = (sa.usesTargeting());
final boolean preventionWithEffect = sa.hasParam("PreventionSubAbility");
for (final GameObject o : tgts) {
numDam = (sa.usesTargeting() && sa.isDividedAsYouChoose()) ? sa.getDividedValue(o) : numDam;
if (o instanceof Card) {
final Card c = (Card) o;
if (c.isInPlay() && (!targeted || c.canBeTargetedBy(sa))) {
if (preventionWithEffect) {
Map<String, String> effectMap = new TreeMap<>();
effectMap.put("EffectString", sa.getSVar(sa.getParam("PreventionSubAbility")));
effectMap.put("ShieldAmount", String.valueOf(numDam));
if (sa.hasParam("ShieldEffectTarget")) {
String effTgtString = "";
List<GameObject> effTgts = new ArrayList<>();
effTgts = AbilityUtils.getDefinedObjects(host, sa.getParam("ShieldEffectTarget"), sa);
for (final Object effTgt : effTgts) {
if (effTgt instanceof Card) {
effTgtString = String.valueOf(((Card) effTgt).getId());
effectMap.put("ShieldEffectTarget", "CardUID_" + effTgtString);
} else if (effTgt instanceof Player) {
effTgtString = ((Player) effTgt).getName();
effectMap.put("ShieldEffectTarget", "PlayerNamed_" + effTgtString);
}
}
}
c.addPreventNextDamageWithEffect(host, effectMap);
} else {
c.addPreventNextDamage(numDam);
}
addPreventNextDamage(sa, o, numDam);
}
} else if (o instanceof Player) {
final Player p = (Player) o;
if (!targeted || p.canBeTargetedBy(sa)) {
if (preventionWithEffect) {
Map<String, String> effectMap = new TreeMap<>();
effectMap.put("EffectString", sa.getSVar(sa.getParam("PreventionSubAbility")));
effectMap.put("ShieldAmount", String.valueOf(numDam));
if (sa.hasParam("ShieldEffectTarget")) {
String effTgtString = "";
List<GameObject> effTgts = new ArrayList<>();
effTgts = AbilityUtils.getDefinedObjects(host, sa.getParam("ShieldEffectTarget"), sa);
for (final Object effTgt : effTgts) {
if (effTgt instanceof Card) {
effTgtString = String.valueOf(((Card) effTgt).getId());
effectMap.put("ShieldEffectTarget", "CardUID_" + effTgtString);
} else if (effTgt instanceof Player) {
effTgtString = ((Player) effTgt).getName();
effectMap.put("ShieldEffectTarget", "PlayerNamed_" + effTgtString);
}
}
}
p.addPreventNextDamageWithEffect(host, effectMap);
} else {
p.addPreventNextDamage(numDam);
}
addPreventNextDamage(sa, o, numDam);
}
}
}
for (final Card c : untargetedCards) {
if (c.isInPlay()) {
c.addPreventNextDamage(numDam);
addPreventNextDamage(sa, c, numDam);
}
}
} // preventDamageResolve

View File

@@ -0,0 +1,76 @@
package forge.game.ability.effects;
import java.util.List;
import forge.GameCommand;
import forge.game.Game;
import forge.game.GameObject;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.player.Player;
import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementHandler;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
import forge.util.TextUtil;
public abstract class DamagePreventEffectBase extends SpellAbilityEffect {
public static void addPreventNextDamage(SpellAbility sa, GameObject o, int numDam) {
final Card hostCard = sa.getHostCard();
final Game game = hostCard.getGame();
final Player player = hostCard.getController();
final String name = hostCard.getName() + "'s Effect";
final String image = hostCard.getImageKey();
StringBuilder sb = new StringBuilder("Event$ DamageDone | ActiveZones$ Command | ValidTarget$ ");
sb.append((o instanceof Card ? "Card.IsRemembered" : "Player.IsRemembered"));
sb.append(" | PreventionEffect$ True | Description$ Prevent the next ").append(numDam).append(" damage.");
String repeffstr = sb.toString();
String effect = "DB$ ReplaceDamage | Amount$ ShieldAmount";
final Card eff = createEffect(sa, player, name, image);
eff.setSVar("ShieldAmount", "Number$" + numDam);
eff.setSVar("PreventedDamage", "Number$0");
eff.addRemembered(o);
SpellAbility replaceDamage = AbilityFactory.getAbility(effect, eff);
if (sa.hasParam("PreventionSubAbility")) {
String subAbString = sa.getSVar(sa.getParam("PreventionSubAbility"));
if (sa.hasParam("ShieldEffectTarget")) {
List<GameObject> effTgts = AbilityUtils.getDefinedObjects(hostCard, sa.getParam("ShieldEffectTarget"), sa);
String effTgtString = "";
for (final GameObject effTgt : effTgts) {
if (effTgt instanceof Card) {
effTgtString = "CardUID_" + String.valueOf(((Card) effTgt).getId());
} else if (effTgt instanceof Player) {
effTgtString = "PlayerNamed_" + ((Player) effTgt).getName();
}
}
subAbString = TextUtil.fastReplace(subAbString, "ShieldEffectTarget", effTgtString);
}
replaceDamage.setSubAbility((AbilitySub) AbilityFactory.getAbility(subAbString, eff));
}
ReplacementEffect re = ReplacementHandler.parseReplacement(repeffstr, eff, true);
re.setOverridingAbility(replaceDamage);
eff.addReplacementEffect(re);
if (o instanceof Card) {
addForgetOnMovedTrigger(eff, "Battlefield");
}
eff.updateStateForView();
game.getTriggerHandler().suppressMode(TriggerType.ChangesZone);
game.getAction().moveTo(ZoneType.Command, eff, sa);
game.getTriggerHandler().clearSuppression(TriggerType.ChangesZone);
game.getEndOfTurn().addUntil(new GameCommand() {
@Override
public void run() {
game.getAction().exile(eff, null);
}
});
}
}

View File

@@ -29,7 +29,7 @@ public class ReplaceDamageEffect extends SpellAbilityEffect {
final ReplacementType event = sa.getReplacementEffect().getMode();
String varValue = sa.getParamOrDefault("VarName", "1");
String varValue = sa.getParamOrDefault("Amount", "1");
@SuppressWarnings("unchecked")
Map<AbilityKey, Object> originalParams = (Map<AbilityKey, Object>) sa.getReplacingObject(AbilityKey.OriginalParams);
@@ -51,6 +51,8 @@ public class ReplaceDamageEffect extends SpellAbilityEffect {
} else if (!StringUtils.isNumeric(varValue)) {
card.setSVar(varValue, "Number$" + prevent);
}
// Set PreventedDamage SVar for PreventionSubAbility
card.setSVar("PreventedDamage", "Number$" + n);
}
// no damage for original target anymore

View File

@@ -5231,83 +5231,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
return restDamage > 0 ? restDamage : 0;
}
protected int preventShieldEffect(final int damage) {
if (damage <= 0) {
return 0;
}
int restDamage = damage;
boolean DEBUGShieldsWithEffects = false;
while (!getPreventNextDamageWithEffect().isEmpty() && restDamage != 0) {
Map<Card, Map<String, String>> shieldMap = getPreventNextDamageWithEffect();
CardCollectionView preventionEffectSources = new CardCollection(shieldMap.keySet());
Card shieldSource = preventionEffectSources.get(0);
if (preventionEffectSources.size() > 1) {
Map<String, Card> choiceMap = Maps.newTreeMap();
List<String> choices = Lists.newArrayList();
for (final Card key : preventionEffectSources) {
String effDesc = shieldMap.get(key).get("EffectString");
int descIndex = effDesc.indexOf("SpellDescription");
effDesc = effDesc.substring(descIndex + 18);
String shieldDescription = key.toString() + " - " + shieldMap.get(key).get("ShieldAmount")
+ " shields - " + effDesc;
choices.add(shieldDescription);
choiceMap.put(shieldDescription, key);
}
shieldSource = getController().getController().chooseProtectionShield(this, choices, choiceMap);
}
if (DEBUGShieldsWithEffects) {
System.out.println("Prevention shield source: " + shieldSource);
}
int shieldAmount = Integer.valueOf(shieldMap.get(shieldSource).get("ShieldAmount"));
int dmgToBePrevented = Math.min(restDamage, shieldAmount);
if (DEBUGShieldsWithEffects) {
System.out.println("Selected source initial shield amount: " + shieldAmount);
System.out.println("Incoming damage: " + restDamage);
System.out.println("Damage to be prevented: " + dmgToBePrevented);
}
//Set up ability
SpellAbility shieldSA;
String effectAbString = shieldMap.get(shieldSource).get("EffectString");
effectAbString = TextUtil.fastReplace(effectAbString, "PreventedDamage", Integer.toString(dmgToBePrevented));
effectAbString = TextUtil.fastReplace(effectAbString, "ShieldEffectTarget", shieldMap.get(shieldSource).get("ShieldEffectTarget"));
if (DEBUGShieldsWithEffects) {
System.out.println("Final shield ability string: " + effectAbString);
}
shieldSA = AbilityFactory.getAbility(effectAbString, shieldSource);
if (shieldSA.usesTargeting()) {
System.err.println(shieldSource + " - Targeting for prevention shield's effect should be done with initial spell");
}
boolean apiIsEffect = (shieldSA.getApi() == ApiType.Effect);
CardCollectionView cardsInCommand = null;
if (apiIsEffect) {
cardsInCommand = getGame().getCardsIn(ZoneType.Command);
}
getController().getController().playSpellAbilityNoStack(shieldSA, true);
if (apiIsEffect) {
CardCollection newCardsInCommand = (CardCollection)getGame().getCardsIn(ZoneType.Command);
newCardsInCommand.removeAll(cardsInCommand);
if (!newCardsInCommand.isEmpty()) {
newCardsInCommand.get(0).setSVar("PreventedDamage", "Number$" + dmgToBePrevented);
}
}
subtractPreventNextDamageWithEffect(shieldSource, restDamage);
restDamage = restDamage - dmgToBePrevented;
if (DEBUGShieldsWithEffects) {
System.out.println("Remaining shields: "
+ (shieldMap.containsKey(shieldSource) ? shieldMap.get(shieldSource).get("ShieldAmount") : "all shields used"));
System.out.println("Remaining damage: " + restDamage);
}
}
return restDamage;
}
// This is used by the AI to forecast an effect (so it must not change the game state)
@Override
public final int staticReplaceDamage(final int damage, final Card source, final boolean isCombat) {
@@ -6205,8 +6128,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
public void onCleanupPhase(final Player turn) {
setDamage(0);
setHasBeenDealtDeathtouchDamage(false);
resetPreventNextDamage();
resetPreventNextDamageWithEffect();
resetReceivedDamageFromThisTurn();
resetDealtDamageToThisTurn();
resetDealtDamageToPlayerThisTurn();

View File

@@ -31,7 +31,6 @@ import java.util.Map.Entry;
import java.util.NavigableMap;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentSkipListMap;
import org.apache.commons.lang3.tuple.ImmutablePair;
@@ -840,83 +839,6 @@ public class Player extends GameEntity implements Comparable<Player> {
return restDamage;
}
protected int preventShieldEffect(final int damage) {
if (damage <= 0) {
return 0;
}
int restDamage = damage;
boolean DEBUGShieldsWithEffects = false;
while (!getPreventNextDamageWithEffect().isEmpty() && restDamage != 0) {
Map<Card, Map<String, String>> shieldMap = getPreventNextDamageWithEffect();
CardCollection preventionEffectSources = new CardCollection(shieldMap.keySet());
Card shieldSource = preventionEffectSources.get(0);
if (preventionEffectSources.size() > 1) {
Map<String, Card> choiceMap = new TreeMap<>();
List<String> choices = new ArrayList<>();
for (final Card key : preventionEffectSources) {
String effDesc = shieldMap.get(key).get("EffectString");
int descIndex = effDesc.indexOf("SpellDescription");
effDesc = effDesc.substring(descIndex + 18);
String shieldDescription = key.toString() + " - " + shieldMap.get(shieldSource).get("ShieldAmount")
+ " shields - " + effDesc;
choices.add(shieldDescription);
choiceMap.put(shieldDescription, key);
}
shieldSource = getController().chooseProtectionShield(this, choices, choiceMap);
}
if (DEBUGShieldsWithEffects) {
System.out.println("Prevention shield source: " + shieldSource);
}
int shieldAmount = Integer.valueOf(shieldMap.get(shieldSource).get("ShieldAmount"));
int dmgToBePrevented = Math.min(restDamage, shieldAmount);
if (DEBUGShieldsWithEffects) {
System.out.println("Selected source initial shield amount: " + shieldAmount);
System.out.println("Incoming damage: " + restDamage);
System.out.println("Damage to be prevented: " + dmgToBePrevented);
}
//Set up ability
SpellAbility shieldSA = null;
String effectAbString = shieldMap.get(shieldSource).get("EffectString");
effectAbString = TextUtil.fastReplace(effectAbString, "PreventedDamage", Integer.toString(dmgToBePrevented));
effectAbString = TextUtil.fastReplace(effectAbString, "ShieldEffectTarget", shieldMap.get(shieldSource).get("ShieldEffectTarget"));
if (DEBUGShieldsWithEffects) {
System.out.println("Final shield ability string: " + effectAbString);
}
shieldSA = AbilityFactory.getAbility(effectAbString, shieldSource);
if (shieldSA.usesTargeting()) {
System.err.println(shieldSource + " - Targeting for prevention shield's effect should be done with initial spell");
}
boolean apiIsEffect = (shieldSA.getApi() == ApiType.Effect);
CardCollectionView cardsInCommand = null;
if (apiIsEffect) {
cardsInCommand = getGame().getCardsIn(ZoneType.Command);
}
getController().playSpellAbilityNoStack(shieldSA, true);
if (apiIsEffect) {
CardCollection newCardsInCommand = new CardCollection(getGame().getCardsIn(ZoneType.Command));
newCardsInCommand.removeAll(cardsInCommand);
if (!newCardsInCommand.isEmpty()) {
newCardsInCommand.get(0).setSVar("PreventedDamage", "Number$" + dmgToBePrevented);
}
}
subtractPreventNextDamageWithEffect(shieldSource, restDamage);
restDamage = restDamage - dmgToBePrevented;
if (DEBUGShieldsWithEffects) {
System.out.println("Remaining shields: "
+ (shieldMap.containsKey(shieldSource) ? shieldMap.get(shieldSource).get("ShieldAmount") : "all shields used"));
System.out.println("Remaining damage: " + restDamage);
}
}
return restDamage;
}
public final void clearAssignedDamage() {
assignedDamage.clear();
assignedCombatDamage.clear();
@@ -2597,8 +2519,6 @@ public class Player extends GameEntity implements Comparable<Player> {
for (final PlayerZone pz : zones.values()) {
pz.resetCardsAddedThisTurn();
}
resetPreventNextDamage();
resetPreventNextDamageWithEffect();
resetNumDrawnThisTurn();
resetNumDiscardedThisTurn();
resetNumForetoldThisTurn();

View File

@@ -29,11 +29,13 @@ import com.google.common.collect.Sets;
import forge.game.CardTraitBase;
import forge.game.Game;
import forge.game.GameEntity;
import forge.game.GameLogEntryType;
import forge.game.IHasSVars;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardState;
@@ -436,4 +438,39 @@ public class ReplacementHandler {
}
return true;
}
/**
* Helper function to get total prevention shield amount (limited to "prevent next N damage effects")
* @param o Affected game entity object
* @return total shield amount
*/
public int getTotalPreventionShieldAmount(GameEntity o) {
final Map<AbilityKey, Object> repParams = AbilityKey.mapFromAffected(o);
repParams.put(AbilityKey.Prevention, true);
repParams.put(AbilityKey.DamageAmount, 1);
List<ReplacementEffect> list = getReplacementList(ReplacementType.DamageDone, repParams, ReplacementLayer.Other);
if (list.isEmpty()) {
return 0;
}
int totalAmount = 0;
for (ReplacementEffect re : list) {
if (re.getOverridingAbility() != null) {
SpellAbility sa = re.getOverridingAbility();
if (ApiType.ReplaceDamage == sa.getApi() && sa.hasParam("Amount")) {
String varValue = sa.getParam("Amount");
if (StringUtils.isNumeric(varValue)) {
totalAmount += Integer.parseInt(varValue);
} else {
varValue = sa.getSVar(varValue);
if (StringUtils.isNumeric(varValue)) {
totalAmount += Integer.parseInt(varValue);
} else if (varValue.startsWith("Number$")) {
totalAmount += Integer.parseInt(varValue.substring(7));
}
}
}
}
}
return totalAmount;
}
}

View File

@@ -3,10 +3,10 @@ ManaCost:W
Types:Instant
A:SP$ ChooseSource | Cost$ W | Choices$ Card,Emblem | SubAbility$ DBEffect | StackDescription$ SpellDescription | SpellDescription$ Prevent the next 3 damage that would be dealt to any target this turn by a source of your choice. You gain 3 life.
SVar:DBEffect:DB$ Effect | ValidTgts$ Creature,Player,Planeswalker | TgtPrompt$ Select any target to prevent damage to | ReplacementEffects$ GraceDamage | ForgetOnMoved$ Battlefield | RememberObjects$ Targeted | SubAbility$ DBGainLife
SVar:GraceDamage:Event$ DamageDone | ValidTarget$ Creature.IsRemembered,Player.IsRemembered | ValidSource$ Card.ChosenCard,Emblem.ChosenCard | ReplaceWith$ GraceDmg | PreventionEffect$ True | Description$ Prevent the next 3 damage that would be dealt to any target this turn by a source of your choice.
SVar:GraceDmg:DB$ ReplaceDamage | VarName$ X
SVar:GraceDamage:Event$ DamageDone | ValidTarget$ Card.IsRemembered,Player.IsRemembered | ValidSource$ Card.ChosenCard,Emblem.ChosenCard | ReplaceWith$ GraceDmg | PreventionEffect$ True | Description$ Prevent the next 3 damage that would be dealt to any target this turn by a source of your choice.
SVar:GraceDmg:DB$ ReplaceDamage | Amount$ ShieldAmount
SVar:DBGainLife:DB$ GainLife | LifeAmount$ 3 | SubAbility$ DBCleanup
SVar:DBCleanup:DB$ Cleanup | ClearChosenCard$ True
SVar:X:Number$3
SVar:ShieldAmount:Number$3
AI:RemoveDeck:All
Oracle:Prevent the next 3 damage that would be dealt to any target this turn by a source of your choice. You gain 3 life.