Merge branch 'attach' into 'master'

ChangeZoneAi: Fix NPE for attaching cards

See merge request core-developers/forge!5557
This commit is contained in:
Michael Kamensky
2021-10-15 20:04:18 +00:00
10 changed files with 15 additions and 44 deletions

View File

@@ -699,9 +699,9 @@ public class AiAttackController {
final boolean bAssault = doAssault(ai); final boolean bAssault = doAssault(ai);
// TODO: detect Lightmine Field by presence of a card with a specific trigger // TODO: detect Lightmine Field by presence of a card with a specific trigger
final boolean lightmineField = ComputerUtilCard.isPresentOnBattlefield(ai.getGame(), "Lightmine Field"); final boolean lightmineField = ai.getGame().isCardInPlay("Lightmine Field");
// TODO: detect Season of the Witch by presence of a card with a specific trigger // TODO: detect Season of the Witch by presence of a card with a specific trigger
final boolean seasonOfTheWitch = ComputerUtilCard.isPresentOnBattlefield(ai.getGame(), "Season of the Witch"); final boolean seasonOfTheWitch = ai.getGame().isCardInPlay("Season of the Witch");
// Determine who will be attacked // Determine who will be attacked
GameEntity defender = chooseDefender(combat, bAssault); GameEntity defender = chooseDefender(combat, bAssault);

View File

@@ -1834,10 +1834,6 @@ public class ComputerUtilCard {
return false; return false;
} }
public static boolean isPresentOnBattlefield(final Game game, final String cardName) {
return Iterables.any(game.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals(cardName));
}
public static int getMaxSAEnergyCostOnBattlefield(final Player ai) { public static int getMaxSAEnergyCostOnBattlefield(final Player ai) {
// returns the maximum energy cost of an ability that permanents on the battlefield under AI's control have // returns the maximum energy cost of an ability that permanents on the battlefield under AI's control have
CardCollectionView otb = ai.getCardsIn(ZoneType.Battlefield); CardCollectionView otb = ai.getCardsIn(ZoneType.Battlefield);

View File

@@ -459,8 +459,7 @@ public class AttachAi extends SpellAbilityAi {
* the mandatory * the mandatory
* @return the player * @return the player
*/ */
private static Player attachToPlayerAIPreferences(final Player aiPlayer, final SpellAbility sa, public static Player attachToPlayerAIPreferences(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
final boolean mandatory) {
List<Player> targetable = new ArrayList<>(); List<Player> targetable = new ArrayList<>();
for (final Player player : aiPlayer.getGame().getPlayers()) { for (final Player player : aiPlayer.getGame().getPlayers()) {
if (sa.canTarget(player)) { if (sa.canTarget(player)) {

View File

@@ -54,7 +54,6 @@ import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions; import forge.game.spellability.TargetRestrictions;
import forge.game.staticability.StaticAbilityMustTarget; import forge.game.staticability.StaticAbilityMustTarget;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import forge.util.MyRandom; import forge.util.MyRandom;
public class ChangeZoneAi extends SpellAbilityAi { public class ChangeZoneAi extends SpellAbilityAi {
@@ -1733,7 +1732,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
@Override @Override
public Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> options, Map<String, Object> params) { public Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> options, Map<String, Object> params) {
// Called when attaching Aura to player // Called when attaching Aura to player
return Aggregates.random(options); return AttachAi.attachToPlayerAIPreferences(ai, sa, true);
} }
private boolean doSacAndReturnFromGraveLogic(final Player ai, final SpellAbility sa) { private boolean doSacAndReturnFromGraveLogic(final Player ai, final SpellAbility sa) {

View File

@@ -90,12 +90,12 @@ public class CharmAi extends SpellAbilityAi {
if (AiPlayDecision.WillPlay == aic.canPlaySa(sub)) { if (AiPlayDecision.WillPlay == aic.canPlaySa(sub)) {
chosenList.add(sub); chosenList.add(sub);
if (chosenList.size() == num) { if (chosenList.size() == num) {
return chosenList; // maximum choices reached return chosenList; // maximum choices reached
} }
} }
} }
if (isTrigger && chosenList.size() < min) { if (isTrigger && chosenList.size() < min) {
// Second pass using doTrigger(false) to fulfil minimum choice // Second pass using doTrigger(false) to fulfill minimum choice
choices.removeAll(chosenList); choices.removeAll(chosenList);
for (AbilitySub sub : choices) { for (AbilitySub sub : choices) {
sub.setActivatingPlayer(ai); sub.setActivatingPlayer(ai);

View File

@@ -583,21 +583,11 @@ public class Game {
} }
public boolean isCardInPlay(final String cardName) { public boolean isCardInPlay(final String cardName) {
for (final Player p : getPlayers()) { return Iterables.any(getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals(cardName));
if (p.isCardInPlay(cardName)) {
return true;
}
}
return false;
} }
public boolean isCardInCommand(final String cardName) { public boolean isCardInCommand(final String cardName) {
for (final Player p : getPlayers()) { return Iterables.any(getCardsIn(ZoneType.Command), CardPredicates.nameEquals(cardName));
if (p.isCardInCommand(cardName)) {
return true;
}
}
return false;
} }
public CardCollectionView getColoredCardsInPlay(final String color) { public CardCollectionView getColoredCardsInPlay(final String color) {

View File

@@ -958,6 +958,7 @@ public class CardFactoryUtil {
final Trigger conspireTrigger = TriggerHandler.parseTrigger(trigScript, card, intrinsic); final Trigger conspireTrigger = TriggerHandler.parseTrigger(trigScript, card, intrinsic);
conspireTrigger.setOverridingAbility(AbilityFactory.getAbility(abString, card)); conspireTrigger.setOverridingAbility(AbilityFactory.getAbility(abString, card));
conspireTrigger.setSVar("Conspire", "0"); conspireTrigger.setSVar("Conspire", "0");
inst.addTrigger(conspireTrigger); inst.addTrigger(conspireTrigger);
} else if (keyword.startsWith("Cumulative upkeep")) { } else if (keyword.startsWith("Cumulative upkeep")) {
final String[] k = keyword.split(":"); final String[] k = keyword.split(":");
@@ -995,7 +996,6 @@ public class CardFactoryUtil {
trigger.setOverridingAbility(AbilityFactory.getAbility(transformEff, card)); trigger.setOverridingAbility(AbilityFactory.getAbility(transformEff, card));
inst.addTrigger(trigger); inst.addTrigger(trigger);
} else if (keyword.equals("Decayed")) { } else if (keyword.equals("Decayed")) {
final String attackTrig = "Mode$ Attacks | ValidCard$ Card.Self | Secondary$ True | TriggerDescription$ " + final String attackTrig = "Mode$ Attacks | ValidCard$ Card.Self | Secondary$ True | TriggerDescription$ " +
"When a creature with decayed attacks, sacrifice it at end of combat."; "When a creature with decayed attacks, sacrifice it at end of combat.";
@@ -1542,7 +1542,6 @@ public class CardFactoryUtil {
trigger.setOverridingAbility(AbilityFactory.getAbility(transformEff, card)); trigger.setOverridingAbility(AbilityFactory.getAbility(transformEff, card));
inst.addTrigger(trigger); inst.addTrigger(trigger);
} else if (keyword.startsWith("Partner:")) { } else if (keyword.startsWith("Partner:")) {
// Partner With // Partner With
final String[] k = keyword.split(":"); final String[] k = keyword.split(":");
@@ -1580,6 +1579,7 @@ public class CardFactoryUtil {
final String effect = "DB$ Poison | Defined$ TriggeredTarget | Num$ " + n; final String effect = "DB$ Poison | Defined$ TriggeredTarget | Num$ " + n;
parsedTrigger.setOverridingAbility(AbilityFactory.getAbility(effect, card)); parsedTrigger.setOverridingAbility(AbilityFactory.getAbility(effect, card));
inst.addTrigger(parsedTrigger); inst.addTrigger(parsedTrigger);
} else if (keyword.startsWith("Presence")) { } else if (keyword.startsWith("Presence")) {
final String[] k = keyword.split(":"); final String[] k = keyword.split(":");
@@ -2530,7 +2530,6 @@ public class CardFactoryUtil {
ReplacementEffect re = ReplacementHandler.parseReplacement(rep, host, intrinsic, card); ReplacementEffect re = ReplacementHandler.parseReplacement(rep, host, intrinsic, card);
inst.addReplacement(re); inst.addReplacement(re);
} }
else if (keyword.startsWith("If CARDNAME would be put into a graveyard " else if (keyword.startsWith("If CARDNAME would be put into a graveyard "
+ "from anywhere, reveal CARDNAME and shuffle it into its owner's library instead.")) { + "from anywhere, reveal CARDNAME and shuffle it into its owner's library instead.")) {
@@ -2606,7 +2605,6 @@ public class CardFactoryUtil {
newSA.setIntrinsic(intrinsic); newSA.setIntrinsic(intrinsic);
inst.addSpellAbility(newSA); inst.addSpellAbility(newSA);
} }
} else if (keyword.startsWith("Adapt")) { } else if (keyword.startsWith("Adapt")) {
final String[] k = keyword.split(":"); final String[] k = keyword.split(":");
@@ -2914,7 +2912,6 @@ public class CardFactoryUtil {
foretell.getRestrictions().setZone(ZoneType.Hand); foretell.getRestrictions().setZone(ZoneType.Hand);
foretell.setIntrinsic(intrinsic); foretell.setIntrinsic(intrinsic);
inst.addSpellAbility(foretell); inst.addSpellAbility(foretell);
} else if (keyword.startsWith("Fortify")) { } else if (keyword.startsWith("Fortify")) {
String[] k = keyword.split(":"); String[] k = keyword.split(":");
// Get cost string // Get cost string
@@ -3091,7 +3088,6 @@ public class CardFactoryUtil {
sa.setIntrinsic(intrinsic); sa.setIntrinsic(intrinsic);
sa.setAlternativeCost(AlternativeCost.Outlast); sa.setAlternativeCost(AlternativeCost.Outlast);
inst.addSpellAbility(sa); inst.addSpellAbility(sa);
} else if (keyword.startsWith("Prowl")) { } else if (keyword.startsWith("Prowl")) {
final String[] k = keyword.split(":"); final String[] k = keyword.split(":");
final Cost prowlCost = new Cost(k[1], false); final Cost prowlCost = new Cost(k[1], false);
@@ -3148,7 +3144,6 @@ public class CardFactoryUtil {
sa.setSVar("ScavengeX", "Exiled$CardPower"); sa.setSVar("ScavengeX", "Exiled$CardPower");
sa.setIntrinsic(intrinsic); sa.setIntrinsic(intrinsic);
inst.addSpellAbility(sa); inst.addSpellAbility(sa);
} else if (keyword.startsWith("Encore")) { } else if (keyword.startsWith("Encore")) {
final String[] k = keyword.split(":"); final String[] k = keyword.split(":");
final String manacost = k[1]; final String manacost = k[1];
@@ -3194,7 +3189,6 @@ public class CardFactoryUtil {
AbilitySub cleanupSA = (AbilitySub) AbilityFactory.getAbility(cleanupStr, card); AbilitySub cleanupSA = (AbilitySub) AbilityFactory.getAbility(cleanupStr, card);
delTrigSA.setSubAbility(cleanupSA); delTrigSA.setSubAbility(cleanupSA);
} else if (keyword.startsWith("Spectacle")) { } else if (keyword.startsWith("Spectacle")) {
final String[] k = keyword.split(":"); final String[] k = keyword.split(":");
final Cost cost = new Cost(k[1], false); final Cost cost = new Cost(k[1], false);
@@ -3208,7 +3202,6 @@ public class CardFactoryUtil {
newSA.setIntrinsic(intrinsic); newSA.setIntrinsic(intrinsic);
inst.addSpellAbility(newSA); inst.addSpellAbility(newSA);
} else if (keyword.startsWith("Surge")) { } else if (keyword.startsWith("Surge")) {
final String[] k = keyword.split(":"); final String[] k = keyword.split(":");
final Cost surgeCost = new Cost(k[1], false); final Cost surgeCost = new Cost(k[1], false);
@@ -3222,7 +3215,6 @@ public class CardFactoryUtil {
newSA.setIntrinsic(intrinsic); newSA.setIntrinsic(intrinsic);
inst.addSpellAbility(newSA); inst.addSpellAbility(newSA);
} else if (keyword.startsWith("Suspend") && !keyword.equals("Suspend")) { } else if (keyword.startsWith("Suspend") && !keyword.equals("Suspend")) {
// only add it if suspend has counter and cost // only add it if suspend has counter and cost
final String[] k = keyword.split(":"); final String[] k = keyword.split(":");
@@ -3320,7 +3312,6 @@ public class CardFactoryUtil {
final SpellAbility sa = AbilityFactory.getAbility(effect, card); final SpellAbility sa = AbilityFactory.getAbility(effect, card);
sa.setIntrinsic(intrinsic); sa.setIntrinsic(intrinsic);
inst.addSpellAbility(sa); inst.addSpellAbility(sa);
} else if (keyword.endsWith(" offering")) { } else if (keyword.endsWith(" offering")) {
final String offeringType = keyword.split(" ")[0]; final String offeringType = keyword.split(" ")[0];
final SpellAbility sa = card.getFirstSpellAbility(); final SpellAbility sa = card.getFirstSpellAbility();
@@ -3337,7 +3328,6 @@ public class CardFactoryUtil {
newSA.setDescription(sa.getDescription() + " (" + offeringType + " offering)"); newSA.setDescription(sa.getDescription() + " (" + offeringType + " offering)");
newSA.setIntrinsic(intrinsic); newSA.setIntrinsic(intrinsic);
inst.addSpellAbility(newSA); inst.addSpellAbility(newSA);
} else if (keyword.startsWith("Crew")) { } else if (keyword.startsWith("Crew")) {
final String[] k = keyword.split(":"); final String[] k = keyword.split(":");
final String power = k[1]; final String power = k[1];
@@ -3352,7 +3342,6 @@ public class CardFactoryUtil {
final SpellAbility sa = AbilityFactory.getAbility(effect, card); final SpellAbility sa = AbilityFactory.getAbility(effect, card);
sa.setIntrinsic(intrinsic); sa.setIntrinsic(intrinsic);
inst.addSpellAbility(sa); inst.addSpellAbility(sa);
} else if (keyword.startsWith("Cycling")) { } else if (keyword.startsWith("Cycling")) {
final String[] k = keyword.split(":"); final String[] k = keyword.split(":");
final String manacost = k[1]; final String manacost = k[1];
@@ -3370,7 +3359,6 @@ public class CardFactoryUtil {
sa.setAlternativeCost(AlternativeCost.Cycling); sa.setAlternativeCost(AlternativeCost.Cycling);
sa.setIntrinsic(intrinsic); sa.setIntrinsic(intrinsic);
inst.addSpellAbility(sa); inst.addSpellAbility(sa);
} else if (keyword.startsWith("TypeCycling")) { } else if (keyword.startsWith("TypeCycling")) {
final String[] k = keyword.split(":"); final String[] k = keyword.split(":");
final String type = k[1]; final String type = k[1];
@@ -3396,7 +3384,6 @@ public class CardFactoryUtil {
sa.setAlternativeCost(AlternativeCost.Cycling); sa.setAlternativeCost(AlternativeCost.Cycling);
sa.setIntrinsic(intrinsic); sa.setIntrinsic(intrinsic);
inst.addSpellAbility(sa); inst.addSpellAbility(sa);
} }
} }

View File

@@ -5,7 +5,7 @@ PT:4/2
S:Mode$ ReduceCost | ValidTarget$ Card.Self | Activator$ Player.Opponent | Type$ Spell | Amount$ 1 | Description$ Spells your opponents cast that target CARDNAME cost {1} less to cast. S:Mode$ ReduceCost | ValidTarget$ Card.Self | Activator$ Player.Opponent | Type$ Spell | Amount$ 1 | Description$ Spells your opponents cast that target CARDNAME cost {1} less to cast.
T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Card.Self | Execute$ TrigChoose | TriggerController$ TriggeredCardController | TriggerDescription$ When CARDNAME dies, return it to the battlefield transformed under your control attached to target opponent. T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Card.Self | Execute$ TrigChoose | TriggerController$ TriggeredCardController | TriggerDescription$ When CARDNAME dies, return it to the battlefield transformed under your control attached to target opponent.
SVar:TrigChoose:DB$ Pump | ValidTgts$ Opponent | TgtPrompt$ Choose a opponent | IsCurse$ True | SubAbility$ DBChange SVar:TrigChoose:DB$ Pump | ValidTgts$ Opponent | TgtPrompt$ Choose a opponent | IsCurse$ True | SubAbility$ DBChange
SVar:DBChange:DB$ ChangeZone | Defined$ TriggeredNewCardLKICopy | Origin$ Graveyard | Destination$ Battlefield | AttachedToPlayer$ ParentTarget | Transformed$ True | GainControl$ True SVar:DBChange:DB$ ChangeZone | Defined$ TriggeredNewCardLKICopy | Origin$ Graveyard | Destination$ Battlefield | AttachedToPlayer$ ParentTarget | Transformed$ True | GainControl$ True | AILogic$ Curse
SVar:SacMe:4 SVar:SacMe:4
SVar:MustAttack:True SVar:MustAttack:True
AlternateMode:DoubleFaced AlternateMode:DoubleFaced
@@ -18,7 +18,7 @@ ManaCost:no cost
Colors:black Colors:black
Types:Enchantment Aura Curse Types:Enchantment Aura Curse
K:Enchant player K:Enchant player
A:SP$ Attach | Cost$ 0 | ValidTgts$ Player | AILogic$ Curse A:SP$ Attach | Cost$ 0 | ValidTgts$ Player
S:Mode$ ReduceCost | ValidTarget$ Player.EnchantedBy | Activator$ You | Type$ Spell | Amount$ 1 | Description$ Spells you cast that target enchanted player cost {1} less to cast. S:Mode$ ReduceCost | ValidTarget$ Player.EnchantedBy | Activator$ You | Type$ Spell | Amount$ 1 | Description$ Spells you cast that target enchanted player cost {1} less to cast.
T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ Player.EnchantedBy | TriggerZones$ Battlefield | Execute$ TrigDrain | TriggerDescription$ At the beginning of enchanted player's upkeep, that player loses 1 life and you gain 1 life. T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ Player.EnchantedBy | TriggerZones$ Battlefield | Execute$ TrigDrain | TriggerDescription$ At the beginning of enchanted player's upkeep, that player loses 1 life and you gain 1 life.
SVar:TrigDrain:DB$ LoseLife | Defined$ TriggeredPlayer | LifeAmount$ 1 | SubAbility$ DBGainLife SVar:TrigDrain:DB$ LoseLife | Defined$ TriggeredPlayer | LifeAmount$ 1 | SubAbility$ DBGainLife

View File

@@ -4,6 +4,6 @@ Types:Enchantment Aura Curse
K:Enchant player K:Enchant player
A:SP$ Attach | Cost$ 4 B | ValidTgts$ Player | AILogic$ Curse A:SP$ Attach | Cost$ 4 B | ValidTgts$ Player | AILogic$ Curse
T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | TriggerZones$ Battlefield | OptionalDecider$ You | Execute$ TrigMisfortune | TriggerDescription$ At the beginning of your upkeep, you may search your library for a Curse card that doesn't have the same name as a Curse attached to enchanted player, put it onto the battlefield attached to that player, then shuffle you library. T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | TriggerZones$ Battlefield | OptionalDecider$ You | Execute$ TrigMisfortune | TriggerDescription$ At the beginning of your upkeep, you may search your library for a Curse card that doesn't have the same name as a Curse attached to enchanted player, put it onto the battlefield attached to that player, then shuffle you library.
SVar:TrigMisfortune:DB$ ChangeZone | Origin$ Library | Destination$ Battlefield | ChangeType$ Aura.Curse+NameNotEnchantingEnchantedPlayer | ChangeNum$ 1 | AttachedToPlayer$ EnchantedPlayer | ShuffleNonMandatory$ True SVar:TrigMisfortune:DB$ ChangeZone | Origin$ Library | Destination$ Battlefield | ChangeType$ Aura.Curse+NameNotEnchantingEnchantedPlayer | ChangeNum$ 1 | AttachedToPlayer$ EnchantedPlayer | ShuffleNonMandatory$ True | AILogic$ Curse
SVar:Picture:http://www.wizards.com/global/images/magic/general/curse_of_misfortunes.jpg SVar:Picture:http://www.wizards.com/global/images/magic/general/curse_of_misfortunes.jpg
Oracle:Enchant player\nAt the beginning of your upkeep, you may search your library for a Curse card that doesn't have the same name as a Curse attached to enchanted player, put it onto the battlefield attached to that player, then shuffle. Oracle:Enchant player\nAt the beginning of your upkeep, you may search your library for a Curse card that doesn't have the same name as a Curse attached to enchanted player, put it onto the battlefield attached to that player, then shuffle.

View File

@@ -5,7 +5,7 @@ PT:2/1
K:CARDNAME can't block. K:CARDNAME can't block.
T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Card.Self | Execute$ TrigChoose | TriggerDescription$ When CARDNAME dies, return it to the battlefield transformed under your control attached to target creature or planeswalker an opponent controls. T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Card.Self | Execute$ TrigChoose | TriggerDescription$ When CARDNAME dies, return it to the battlefield transformed under your control attached to target creature or planeswalker an opponent controls.
SVar:TrigChoose:DB$ Pump | ValidTgts$ Creature.OppCtrl,Planeswalker.OppCtrl | TgtPrompt$ Select target creature or planeswalker an opponent controls | IsCurse$ True | SubAbility$ DBChange SVar:TrigChoose:DB$ Pump | ValidTgts$ Creature.OppCtrl,Planeswalker.OppCtrl | TgtPrompt$ Select target creature or planeswalker an opponent controls | IsCurse$ True | SubAbility$ DBChange
SVar:DBChange:DB$ ChangeZone | Defined$ TriggeredNewCardLKICopy | Origin$ Graveyard | Destination$ Battlefield | AttachedTo$ ParentTarget | Transformed$ True | GainControl$ True SVar:DBChange:DB$ ChangeZone | Defined$ TriggeredNewCardLKICopy | Origin$ Graveyard | Destination$ Battlefield | AttachedTo$ ParentTarget | Transformed$ True | GainControl$ True | AILogic$ Curse
SVar:SacMe:1 SVar:SacMe:1
AlternateMode:DoubleFaced AlternateMode:DoubleFaced
Oracle:Vengeful Strangler can't block.\nWhen Vengeful Strangler dies, return it to the battlefield transformed under your control attached to target creature or planeswalker an opponent controls. Oracle:Vengeful Strangler can't block.\nWhen Vengeful Strangler dies, return it to the battlefield transformed under your control attached to target creature or planeswalker an opponent controls.
@@ -17,7 +17,7 @@ ManaCost:no cost
Colors:black Colors:black
Types:Enchantment Aura Types:Enchantment Aura
K:Enchant creature or planeswalker an opponent controls K:Enchant creature or planeswalker an opponent controls
A:SP$ Attach | Cost$ 0 | ValidTgts$ Creature.OppCtrl,Planeswalker.OppCtrl | TgtPrompt$ Select target creature or planeswalker an opponent controls | AILogic$ Curse A:SP$ Attach | Cost$ 0 | ValidTgts$ Creature.OppCtrl,Planeswalker.OppCtrl | TgtPrompt$ Select target creature or planeswalker an opponent controls
T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | TriggerZones$ Battlefield | Execute$ TrigSac | TriggerDescription$ At the beginning of your upkeep, enchanted permanent's controller sacrifices a nonland permanent and loses 1 life. T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | TriggerZones$ Battlefield | Execute$ TrigSac | TriggerDescription$ At the beginning of your upkeep, enchanted permanent's controller sacrifices a nonland permanent and loses 1 life.
SVar:TrigSac:DB$ Sacrifice | Defined$ Player.controlsPermanent.EnchantedBy | SacValid$ Permanent.nonLand | SubAbility$ DBLoseLife SVar:TrigSac:DB$ Sacrifice | Defined$ Player.controlsPermanent.EnchantedBy | SacValid$ Permanent.nonLand | SubAbility$ DBLoseLife
SVar:DBLoseLife:DB$ LoseLife | LifeAmount$ 1 | Defined$ Player.controlsPermanent.EnchantedBy SVar:DBLoseLife:DB$ LoseLife | LifeAmount$ 1 | Defined$ Player.controlsPermanent.EnchantedBy